架构
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,该 ID 由命名空间和该命名空间中的名称组成。例如,http.handlers.reverse_proxy
是一个 HTTP 处理程序,因为它位于 http.handlers
命名空间中,并且其名称为 reverse_proxy
。http.handlers
命名空间中的所有模块都满足相同接口,宿主模块知道该接口。因此,http
应用知道如何加载和使用这些类型的模块。
清理阶段
当需要停止配置时,所有模块都会被卸载。如果模块分配了任何应该释放的资源,它有机会在清理阶段执行此操作。
插入
模块——或任何 Caddy 插件——通过为模块的包添加 import
来“插入”到 Caddy 中。通过导入包,模块将其自身注册到 Caddy 核心,因此当 Caddy 进程启动时,它会按名称知道每个模块。它甚至可以在模块值和名称之间以及反之亦然地进行关联。
管理配置
更改运行中服务器的活动配置(通常称为“重新加载”)对于服务器所需的高并发性和数千个参数来说可能很棘手。Caddy 使用一种具有许多优点的设计优雅地解决了这个问题
- 运行中服务不会中断
- 可以进行细粒度的配置更改
- 只需要一个锁(在后台)
- 所有重新加载都是原子性、一致性、隔离性和大部分持久性 (“ACID”) 的
- 最小的全局状态
您可以在 此处观看关于 Caddy 2 设计的视频。
配置重新加载的工作原理是配置新模块,如果一切成功,则清理旧模块。在短暂的时间内,两个配置同时运行。
每个配置都与一个 上下文 相关联,该上下文保存所有模块状态,因此大多数状态永远不会逃脱配置的范围。这对正确性、性能和简洁性来说是个好消息!
但是,有时真正需要全局状态。例如,反向代理可能会跟踪其上游的健康状况;由于每个上游在全球范围内只有一个,因此如果每次进行微小的配置更改都忘记它们,那将是不好的。幸运的是,Caddy 提供了类似于 语言运行时垃圾回收器的工具来保持全局状态整洁。
在线配置更新的一种显而易见的方法是同步对每个配置参数的访问,即使在热路径中也是如此。这在性能和复杂性方面都非常糟糕——尤其是在大规模部署时——因此 Caddy 不使用这种方法。
相反,配置被视为不可变的原子单元:要么替换整个配置,要么什么都不改变。admin API 端点——允许通过遍历结构进行细粒度更改——仅改变配置的内存表示,从中生成并加载全新的配置文档。这种方法在简洁性、性能和一致性方面具有巨大的优势。由于只有一个锁,Caddy 可以轻松处理快速重新加载。