caddy架构
Caddy 是一个单一的、自包含的、静态二进制文件,没有外部依赖,因为它是用 Go 语言编写的。这些特性是项目愿景的重要组成部分,因为它们简化了部署,并减少了生产环境中繁琐的故障排查。
如果没有动态链接,那么它如何扩展呢?Caddy 拥有一种新颖的插件架构,使其功能远远超出了其他任何 Web 服务器,即使是那些具有外部(动态链接)依赖的服务器。
我们“减少活动部件”的理念最终实现了更可靠、更易于管理、成本更低的网站——尤其是在大规模部署时。这份半技术性文档描述了我们如何通过软件工程实现这一目标。
概述
Caddy 包含一个命令、核心库和模块。
命令
提供了你熟悉的命令行界面。它是你从操作系统启动进程的方式。这里的代码和逻辑非常简洁,只包含启动核心所需的必要内容。我们有意避免使用标志和环境变量进行配置,除非它们与启动配置相关。
核心库
,即 Caddy 的“核心”,主要负责管理配置。它可以运行一个新配置(Run()
),也可以停止一个正在运行的配置(Stop()
)。它还为模块提供了各种工具、类型和值。
模块
负责完成其他所有工作。许多模块是内置在 Caddy 中的,这些被称为标准模块
。这些模块被认为对大多数用户最有用。
Caddy 核心
在核心层面,Caddy 仅仅加载一个初始配置(“config”),或者如果没有配置,则打开一个套接字以接受后续的配置。
Caddy 的配置是一个 JSON 文档,其顶层有一些字段:
{ "admin": {}, "logging": {}, "apps": {•••}, ...}
Caddy 核心知道如何原生处理其中的一些字段:
-
admin
,以便它可以设置管理 API 并管理进程; -
logging
,以便它可以输出日志。
但其他顶层字段(如 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 可以轻松处理快速重新加载。