架构
Caddy 是一个独立的、自包含的、静态的二进制文件,由于使用 Go 编写,因此没有任何外部依赖。这些特性构成了项目愿景的重要组成部分,因为它们简化了部署并减少了在生产环境中的繁琐故障排查。
如果没有动态链接,那么它如何扩展呢?Caddy 具有一个新颖的插件架构,它将其功能扩展到远超任何其他 Web 服务器的水平,甚至超过那些具有外部(动态链接)依赖的服务器。
我们的"减少活动部件"理念最终会带来更可靠、更易管理、成本更低的站点 — 尤其是在规模扩大时。本半技术性文档描述了我们如何通过软件工程实现这一目标。
概述
Caddy 由命令、核心库和模块组成。
命令提供您可能熟悉的命令行界面。这是您从操作系统启动进程的方式。这里的代码和逻辑相当少,只有引导核心以用户期望的方式运行所需的内容。我们有意避免使用标志和环境变量进行配置,除非它们与引导配置有关。
核心库,或称 Caddy 的"核心",主要管理配置。它可以 Run()
一个新配置或 Stop()
一个正在运行的配置。它还为模块提供各种实用工具、类型和值。
模块完成其他所有工作。许多模块内置在 Caddy 中,这些被称为_标准模块_。这些模块被认为对大多数用户最有用。
Caddy 核心
在其核心,Caddy 仅加载初始配置("config"),或者如果没有配置,则打开一个套接字以在稍后接受新配置。
Caddy 配置 是一个 JSON 文档,在其顶层有一些字段:
{
"admin": {},
"logging": {},
"apps": {•••},
...
}
Caddy 的核心原生地知道如何处理其中的一些字段:
但其他顶层字段(如 [apps
](/docs/json/apps/))对 Caddy 的核心来说是不透明的。实际上,Caddy 对 apps
中的字节所知道的只是将它们反序列化为一个可以调用两个方法的接口类型:
Start()
Stop()
... 就这些。当加载配置时,它在每个应用程序上调用 Start()
,当卸载配置时,在每个应用程序上调用 Stop()
。
当应用模块启动时,它启动应用的模块生命周期。
模块生命周期
有两种类型的模块:主机模块_和_访客模块。
主机模块(或"父"模块)是加载其他模块的模块。
访客模块(或"子"模块)是被加载的模块。所有模块都是访客模块 — 甚至应用模块也是。
模块按以下顺序加载、配置和验证、使用,然后被清理:
- 加载
- 配置和验证
- 使用
- 清理
当第一次加载配置时,Caddy 通过初始化所有配置的应用模块来启动模块生命周期。从那里开始,每个应用模块都会继续进行剩余的过程。
加载阶段
加载模块涉及将其 JSON 字节反序列化为内存中的类型值。这...基本上就是这样。它只是将 JSON 解码为一个值。
配置阶段
这个阶段是大多数设置工作进行的地方。所有模块都有机会在加载后配置自己。
由于 JSON 编码的任何属性都已经被解码,因此这里只需要进行额外的设置。配置期间最常见的任务是设置访客模块。换句话说,配置主机模块也会导致配置其访客模块,一直延续下去。
您可以通过在我们的文档中遍历 Caddy 的 JSON 结构来了解这一点。任何您看到 {•••}
的地方都是可以使用访客模块的地方;当您点击进去时,您可以一直探索,直到没有更多的访客模块。
其他常见的配置任务是设置将在模块生命周期内使用的内部值,或标准化输入。例如,http.matchers.remote_ip
模块使用配置阶段从其接收的字符串输入中解析 CIDR 值。这样,它不必在每个 HTTP 请求期间都这样做,因此效率更高。
验证也可以在配置阶段进行。如果模块的结果配置无效,可以在此处返回错误,这将中止整个配置加载过程。
使用阶段
一旦访客模块被配置和验证,它就可以被其主机模块使用。这具体意味着什么取决于每个主机模块。
每个模块都有一个 ID,由命名空间和该命名空间中的名称组成。例如,http.handlers.reverse_proxy
是一个 HTTP 处理器,因为它位于 http.handlers
命名空间中,其名称是 reverse_proxy
。http.handlers
命名空间中的所有模块都满足相同的接口,为主机模块所知。因此,http
应用知道如何加载和使用这些类型的模块。
清理阶段
当配置需要停止时,所有模块都会被卸载。如果模块分配了任何应该被释放的资源,它有机会在清理阶段进行释放。
插入
模块 — 或任何 Caddy 插件 — 通过为模块的包添加一个 import
来"插入"到 Caddy 中。通过导入包,模块自行注册 到 Caddy 核心,因此当 Caddy 进程启动时,它通过名称知道每个模块。它甚至可以在模块值和名称之间建立关联,反之亦然。
管理配置
更改正在运行的服务器的活动配置(通常称为"重载")在高并发和服务器需要的数千个参数的情况下可能很棘手。Caddy 使用一种设计优雅地解决了这个问题,该设计具有许多好处:
- 不会中断正在运行的服务
- 可以进行精细的配置更改
- 仅需一个锁(在后台)
- 所有重载都具有原子性、一致性、隔离性,并且大部分具有持久性("ACID")
- 最小的全局状态
配置重载通过配置新模块来工作,如果所有模块都成功,则清理旧模块。在短暂的时间内,两个配置同时运行。
每个配置都与一个上下文关联,该上下文包含所有模块状态,因此大多数状态永远不会超出配置的范围。这对于正确性、性能和简单性来说都是好消息!
然而,有时真正的全局状态是必要的。例如,反向代理可能会跟踪其上游的健康状况;由于每个上游在全局范围内只有一个,如果每次进行微小的配置更改时都忘记它们,那将是很糟糕的。幸运的是,Caddy 提供了类似于语言运行时垃圾收集器的工具来保持全局状态的整洁。
一个明显的在线配置更新方法是同步访问每个配置参数,即使在热路径中也是如此。这在性能和复杂性方面都是令人难以置信的糟糕 — 尤其是在规模扩大时 — 所以 Caddy 不使用这种方法。
相反,配置被视为不可变的原子单元:要么整个被替换,要么什么都不改变。管理 API 端点 — 通过遍历结构允许细粒度更改 — 仅修改配置的内存表示,从中生成并加载一个全新的配置文档。这种方法在简单性、性能和一致性方面具有巨大的好处。由于只有一个锁,Caddy 可以轻松处理快速重载。