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
 中的字节所能做的仅仅是将它们反序列化为一个接口类型,并调用两个方法:

  1. Start()
  2. Stop()
    仅此而已。在加载配置时,它会调用每个应用模块的 Start()
     方法,而在卸载配置时,它会调用每个应用模块的 Stop()
     方法。

当应用模块启动时,它会启动该模块的生命周期。

模块生命周期

有两种类型的模块:宿主模块
(或“父模块”)和客户端模块
(或“子模块”)。

宿主模块
是加载其他模块的模块。

客户端模块
是被加载的模块。所有模块都是客户端模块——即使是应用模块。

模块的加载、配置、验证、使用和清理的顺序如下:

  1. 加载

  2. 配置和验证

  3. 使用

  4. 清理

当配置首次加载时,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 可以轻松处理快速重新加载。

江达小记