跳到主要内容

自动化与证书生命周期

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_QUEUECNAME_HEALTH_QUEUE)。

ACME 证书签发

任务流程

acme_issue_task(Celery 任务名 cirrus.acme.issue_certificate)负责 orchestrate 签发流程:

  1. 生成任务级令牌(使用 Celery request.id 或随机十六进制)。
  2. asyncio.run 中调用 _acme_issue_task_async(domain, token)
  3. 获取 Redis 锁 cdn:acme:lock:{domain},TTL 由 ACME_LOCK_TTL(默认 900 秒)控制;若锁已存在则跳过。
  4. 将任务 ID 写入 cdn:acme:task:{domain},便于操作人员观察。
  5. cdn:acme:{domain} 状态标记为 "running"
  6. 通过调用 acme_common.pyensure_acme_registered,与 acme-dns 协作(使用 httpx.AsyncClient)确保已注册 ACME 账号。
  7. 可选地检查 _acme-challenge CNAME 是否就绪(ENFORCE_ACME_CNAME_CHECKWAIT_FOR_CNAMECNAME_WAIT_SECS)。
  8. 加载或生成 ACME 账号密钥(cdn:acmeacct:global)与域名私钥(cdn:acmecertkey:{domain})。
  9. 通过 issue_certificate_with_sewer 调用 sewer 完成签发。
  10. 将完整链 PEM 与私钥写入 cdn:cert:{domain},更新状态为 "issued",并缓存签发时间。
  11. 删除 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)执行以下步骤:

  1. 获取全局扫描锁 cdn:acme:renew:scan_lock,避免多实例重叠扫描。
  2. 遍历 cdn:domains 中启用 use_acme_dns01 的域名。
  3. 跳过正在加锁或已排队的域名。
  4. 调用 is_cert_expiring_soon 判定证书是否即将到期(阈值 ACME_RENEW_BEFORE_DAYS,默认 30 天)。
  5. 按上限 ACME_RENEW_MAX_PER_SCAN(默认 10)入队签发任务,并将状态标记为 "queued"
  6. 记录被跳过的域名(锁定或未到期)与逐域错误。
  7. 释放扫描锁,即便异常也会清理保证。

任务返回结构化字典,汇总排队续期与跳过原因,便于可观测。

节点健康检查

cname_health_check_task(任务名 cirrus.cname.health_check)按 NodeHealthSettings 的间隔运行:

  • 调用 _cname_health_check_task_async,后者执行 cname/health.pyperform_health_checks
  • 对每个节点发起 http://<ip>:<port>/healthz 的 HTTP GET 请求(IPv6 地址带方括号)。
  • 当失败次数达到 fails_to_down 阈值时递增计数并将节点置为不可用;成功次数达到 succs_to_up 时恢复。
  • 当节点激活状态变化时发布 cdn:cname:dirty,触发 DNS 更新。
  • 返回包含节点 ID、状态(healthyfaileddownrecoveredno-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_startacme_doneacme_fail。续期扫描记录 acme_auto_renew_queuedacme_auto_renew_error
  • Redis 键:操作人员可通过 cdn:acme:{domain} 观察状态流转(initregisteredqueuedrunningissuedfailed)。
  • 指标:Celery 原生不导出 Prometheus 指标,但日志与 Redis 数据已提供可观测基础。第 9 章讨论进一步增强手段。

自动化使证书保持有效、节点清单保持准确,无需人工介入。下一章将说明 DNS 与流量工程层如何消费这些自动化数据,将客户端路由至健康的边缘节点。