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