自动化与证书生命周期
Cirrus CDN 的异步工作流由 Celery 驱动:包括 ACME 证书签发、续期扫描以及节点健康检查。本章解析 src/cirrus/celery_app.py 所定义的任务调度、锁策略与外部集成。
Celery 配置
celery_app.py 声明的 Celery 实例同时使用 Redis 作为 broker 与结果存储:
- Broker URL 默认形如
redis://[password@]host:port/0。 - 结果存储默认使用 1 号数据库。
- 序列化方式仅启用 JSON;时区默认为 UTC。
Celery beat 启动的计划任务包括:
acme_renewal_scan——基于 Cron(默认每小时整点)执行cirrus.acme.scan_and_renew。cname_node_health——通过celery.schedules.schedule配置的周期任务,间隔由节点健康设置(NODE_HEALTH_INTERVAL_SECS)决定。
队列名称可通过环境变量配置(ACME_RENEW_QUEUE、CNAME_HEALTH_QUEUE)。
ACME 证书签发
任务流程
acme_issue_task(Celery 任务名 cirrus.acme.issue_certificate)负责 orchestrate 签发流程:
- 生成任务级令牌(使用 Celery
request.id或随机十六进制)。 - 在
asyncio.run中调用_acme_issue_task_async(domain, token)。 - 获取 Redis 锁
cdn:acme:lock:{domain},TTL 由ACME_LOCK_TTL(默认 900 秒)控制;若锁已存在则跳过。 - 将任务 ID 写入
cdn:acme:task:{domain},便于操作人员观察。 - 将
cdn:acme:{domain}状态标记为"running"。 - 通过调用
acme_common.py的ensure_acme_registered,与 acme-dns 协作(使用httpx.AsyncClient)确保已注册 ACME 账号。 - 可选地检查
_acme-challengeCNAME 是否就绪(ENFORCE_ACME_CNAME_CHECK、WAIT_FOR_CNAME、CNAME_WAIT_SECS)。 - 加载或生成 ACME 账号密钥(
cdn:acmeacct:global)与域名私钥(cdn:acmecertkey:{domain})。 - 通过
issue_certificate_with_sewer调用sewer完成签发。 - 将完整链 PEM 与私钥写入
cdn:cert:{domain},更新状态为"issued",并缓存签发时间。 - 删除
cdn:acme:task:{domain}以及所持锁,完成解锁。
若发生异常,会将状态设为 "failed" 并记录 acme_fail 日志;finally 块负责清理锁,避免死锁。
外部服务
- acme-dns(容器
acmedns)——处理挑战记录的更新。ensure_acme_registered会注册新账号并将凭据存入 Redis。 - Caddy(容器
caddy)——提供本地 ACME 目录(https://caddy:4431/acme/local/directory)。worker 会将根 CA 拷贝至/app/certs/root-ca.crt(见docker/entrypoint.sh)以建立信任。
证书续期扫描
acme_scan_and_renew_task(任务名 cirrus.acme.scan_and_renew)执行以下步骤:
- 获取全局扫描锁
cdn:acme:renew:scan_lock,避免多实例重叠扫描。 - 遍历
cdn:domains中启用use_acme_dns01的域名。 - 跳过正在加锁或已排队的域名。
- 调用
is_cert_expiring_soon判定证书是否即将到期(阈值ACME_RENEW_BEFORE_DAYS,默认 30 天)。 - 按上限
ACME_RENEW_MAX_PER_SCAN(默认 10)入队签发任务,并将状态标记为"queued"。 - 记录被跳过的域名(锁定或未到期)与逐域错误。
- 释放扫描锁,即便异常也会清理保证。
任务返回结构化字典,汇总排队续期与跳过原因,便于可观测。
节点健康检查
cname_health_check_task(任务名 cirrus.cname.health_check)按 NodeHealthSettings 的间隔运行:
- 调用
_cname_health_check_task_async,后者执行cname/health.py的perform_health_checks。 - 对每个节点发起
http://<ip>:<port>/healthz的 HTTP GET 请求(IPv6 地址带方括号)。 - 当失败次数达到
fails_to_down阈值时递增计数并将节点置为不可用;成功次数达到succs_to_up时恢复。 - 当节点激活状态变化时发布
cdn:cname:dirty,触发 DNS 更新。 - 返回包含节点 ID、状态(
healthy、failed、down、recovered、no-address)及可选错误信息的数组。
Redis 工具
辅助函数使用 _create_async_redis() 创建的 redis.asyncio.Redis 客户端:
perform_health_checks复用调用方提供的 Redis 客户端;_cname_health_check_task_async会在任务结束后关闭连接。- ACME 任务通过
try/finally包裹 Redis 操作,确保出错时也能关闭连接。
锁与并发控制
- 域名锁——
cdn:acme:lock:{domain}防止同一域名并发签发。 - 任务键——
cdn:acme:task:{domain}方便操作人员查看并避免重复执行。 - 扫描锁——
cdn:acme:renew:scan_lock保证续期扫描在多个 worker 中互斥。 - 发布/订阅事件——
cname/service.py中的publish_zone_dirty在域名或节点变更时触发 DNS 刷新,确保组件最终一致。
错误处理与重试
- Celery 使用默认重试策略(无自动重试)。失败会写入日志并通过 Redis 状态键暴露,便于操作人员调查后重新触发。
- 证书签发捕获所有异常,将状态设为
"failed"并确保释放锁,避免无限阻塞。 - 续期扫描会记录上游异常,并在结果中包含错误信息,方便仪表盘或告警系统消费。
可观测性钩子
- 日志:API 在排队时记录
acme_queued,Celery 任务依次输出acme_start、acme_done、acme_fail。续期扫描记录acme_auto_renew_queued与acme_auto_renew_error。 - Redis 键:操作人员可通过
cdn:acme:{domain}观察状态流转(init、registered、queued、running、issued、failed)。 - 指标:Celery 原生不导出 Prometheus 指标,但日志与 Redis 数据已提供可观测基础。第 9 章讨论进一步增强手段。
自动化使证书保持有效、节点清单保持准确,无需人工介入。下一章将说明 DNS 与流量工程层如何消费这些自动化数据,将客户端路由至健康的边缘节点。