架构
Caddy 是一个单一的、自包含的、静态二进制文件,且没有任何外部依赖,因为它是用 Go 编写的。这些特性构成了项目愿景的重要部分,因为它们简化了部署并减少了生产环境中繁琐的故障排查。
既然没有动态链接,那么它如何扩展?Caddy 采用了一种新颖的插件架构,使其功能远超其他任何 Web 服务器,即便那些服务器具有外部(动态链接的)依赖。
我们“更少的活动部件”的理念最终带来更可靠、更易管理、成本更低的网站——尤其是在大规模时。本文档半技术性地描述了我们如何通过软件工程实现这一目标。
概览
Caddy 由一个命令、核心库和模块组成。
命令 提供了你应该熟悉的 命令行界面。这是你从操作系统启动进程的方式。这里的代码和逻辑量相当少,只包含以用户期望的方式引导核心所需的内容。我们刻意避免将标志和环境变量用于配置(除非它们用于引导配置)。
核心库,或称 Caddy 的“核心”,主要负责管理配置。它可以 Run() 一个新配置或 Stop() 一个正在运行的配置。它还为模块提供各种实用工具、类型和值。
模块 负责其余的一切。许多模块内置于 Caddy 中,称为 标准模块。这些模块被认为对大多数用户最有用。
Caddy 核心
在其核心,Caddy 只是加载初始配置(“配置”),或者如果没有配置,则打开一个套接字以便稍后接受新的配置。
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 提供了类似于语言运行时垃圾收集器的设施 来保持全局状态整洁。
一种直观的在线配置更新方法是同步访问每一个配置参数,即使是在热点路径中也是如此。这在性能和复杂性方面极其糟糕——尤其是在大规模时——因此 Caddy 不使用这种方法。
相反,配置被视为不可变的、原子的单元:要么整个替换,要么什么都不改动。管理 API 端点——它们通过遍历结构允许细粒度更改——仅修改内存中的配置表示,从中生成并加载一个全新的配置文档。这种方法在简洁性、性能和一致性方面带来了巨大好处。由于只有一个锁,Caddy 能够轻松处理快速重载。