← 返回模块
3.6.5.3beta 可读 · 未来付费校验通过内容版本 2026-05-24

容器编排:Compose 与 Kubernetes

3.6.5 · 构建、部署与容器化 · 编程

上海一家 私募 的 quant 把 L2 产出的 feed-handler:1.0.0 镜像推到 内部 registry,问 平台 组 怎么 把 它 部署 到 测试 集群。负责 工程师 直接 反 问:「你 的 docker-compose.yml 本地 长 什么 样?你 的 manifests/ 在 测试 集群 长 什么 样?」quant 两份 都 没有,手 上 只 有 镜像。集群 凭据 与 云 管 的 StorageClass 平台 组 会 帮 你 接,但 YAML——compose 文件、Deployment、Service、ConfigMap、Secret、资源 请求、就绪 探针——是 开发者 自己的 责任。本课 就 是 写 这 些 YAML 的 地方。学 完 本课,你 会 写 出 一份 五 服务 的 docker-compose.yml 在 笔记本 上 起 起 3.6.4 的 完整 流式 栈,也 会 写 出 一个 manifests/ 目录 让 同 一份 栈 在 本地 kind 集群 上 起 来。

两 层 规则

docker compose 是 开发者 笔记本、CI 集成 测试、以及 任何 单 主机 可以 手动 重启 的 工作负载 的 正确 工具。Kubernetes 是 测试、生产、以及 任何 需要 多 主机 调度、节点 故障 自动 恢复、声明式 漂移 校正 的 工作负载 的 正确 工具。Kubernetes 比 compose 多 出 的 五 项 能力——也是 维护 一个 集群 这 几个 月 投入 的 回报——是:(a) ​多 主机 调度​ 节点 挂 了 pod 还 能 活(compose 没有 多 主机 概念);(b) ​声明式 持续 校正​​(compose 是 一次性 apply;Kubernetes 跑 控制 循环 持续 把 集群 状态 与 manifest 对齐);(c) SecretConfigMap 一级 资源​ 与 工作负载 解耦,改 配置 不 用 重建 镜像;(d) ​滚动 更新 + 修订 历史​ 加 安全 回滚 kubectl rollout undo;(e) ​集群 级 关注点​​(RBAC、网络 策略、service mesh、PDB、自动 伸缩)compose 直接 不 建模。规则:本地 用 compose;测试 与 生产 用 Kubernetes。

第 一 部分——Docker Compose 完整 形态

按 3.6.4 capstone 栈 写 docker-compose.yml。五 个 服务、命名 网络、命名 卷、健康 检查、有序 启动、重启 策略。每一行 都 是 有意 写 的:

services:
  kafka:
    image: confluentinc/cp-kafka:7.6.0
    environment:
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_NODE_ID: 1
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
      KAFKA_LISTENER_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      CLUSTER_ID: q1Sh-9_ISia_zwGINzRvyQ
    ports: ["19092:19092"]
    healthcheck:
      test: ["CMD", "kafka-topics", "--bootstrap-server", "localhost:9092", "--list"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes: ["kafka-data:/var/lib/kafka/data"]
    networks: [feed-net]
    restart: unless-stopped

  timescaledb:
    image: timescale/timescaledb:latest-pg16
    env_file: .env
    ports: ["5432:5432"]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes: ["ts-data:/var/lib/postgresql/data"]
    networks: [feed-net]
    restart: unless-stopped

  feed-handler-producer:
    image: feed-handler:1.0.0
    command: ["producer"]
    environment:
      KAFKA_BOOTSTRAP_SERVERS: kafka:9092
    depends_on:
      kafka:
        condition: service_healthy
    networks: [feed-net]
    restart: unless-stopped

  feed-handler-consumer:
    image: feed-handler:1.0.0
    command: ["consumer"]
    env_file: .env
    environment:
      KAFKA_BOOTSTRAP_SERVERS: kafka:9092
    depends_on:
      kafka:
        condition: service_healthy
      timescaledb:
        condition: service_healthy
    networks: [feed-net]
    restart: unless-stopped

  feed-handler-monitor:
    image: feed-handler:1.0.0
    command: ["monitor"]
    networks: [feed-net]
    restart: unless-stopped

networks:
  feed-net:
    driver: bridge

volumes:
  kafka-data:
  ts-data:

逐节 纪律。每 个 服务 都 写 healthcheck 以 0 / 非 0 退出 表态,depends_on: condition: service_healthy 才 能 工作。每个 有状态 服务 都 挂 命名 volume(生产 绝不 host-bind:host-bind 把 数据 生命 周期 绑 在 特定 主机 目录 上、破 可移植性)。restart: unless-stopped 在 失败 与 宿主 重启 时 都 拉 回 来;但 手动 docker compose stop 不 拉 起。网络 命名(feed-net),服务 间 DNS 走 服务 名 工作——kafka:9092 就是 网络 内 的 bootstrap-server URL。凭据 来自 gitignored .env,通过 env_file: 引用,绝不 内联。.env.example 带 占位 值 提交 进 git,告诉 同事 要 设 哪些 变量。

验证 栈:docker compose up -ddocker compose ps(期望 五 个 healthy)、docker compose logs -f feed-handler-consumer(期望 3.6.4 L4 的 结构化 JSON 日志)、docker compose down -v(拆 栈 含 命名 卷)。

第 二 部分——Kubernetes 八 个 原语

每个 quant developer 必须 掌握 的 八 个 原语,按 这个 顺序:

  • Pod——调度 单位;一 个 或 多 个 容器 共享 网络 命名空间(彼此 走 localhost)和 共享 卷。几乎 不 直接 创建;由 控制器 拥有。
  • Deployment——无状态 服务 的 控制器;声明 replicas、滚动 更新 策略(默认 maxSurge: 25% / maxUnavailable: 25%)、kubectl rollout undo 用 的 修订 历史。
  • StatefulSet——有状态 服务 的 控制器;每 个 pod 需要 稳定 主机名、稳定 PVC、有序 启动 与 拆除(kafka-0 先 起 kafka-1)。标准 用例:数据库(TimescaleDB)或 broker(Kafka)。
  • Service——基于 pod 选择器 的 集群 内 稳定 DNS + 负载 均衡。三 种 类型:ClusterIP(默认,仅 内部,virtual IP 平衡 到 匹配 pod)、NodePort(在 每 个 节点 IP 的 高 端口 暴露——测试 用)、LoadBalancer(云 管 外部 负载 均衡——生产 用)。DNS:<service>.<namespace>.svc.cluster.local;同 namespace 内 短名 即 可。
  • ConfigMap——非 secret 配置(topic 名、bootstrap-server URL、日志 级别、symbol 列表);以 env 或 /etc/config/<key> 文件 挂载;任何 有 集群 读 权限 的 人 都 能 看 到。
  • Secret——凭据(Postgres 密码、Kafka SASL 密码、mTLS 证书);​​默认 base64 存储,不 是 加密​​;生产 集群 在 API server 层 配置 静态 加密 via 集群 KMS(AWS KMS / GCP KMS / Azure Key Vault / 国产 HSM / 阿里云 KMS);以 env 或 /etc/secrets/<key> 文件 挂载;sealed-secrets-controller 与 external-secrets-operator 让 Secret 加密 后 进 git、只 在 集群 内 解密(指向 GitOps)。
  • Namespace——隔离、RBAC、资源 配额 的 逻辑 分区。规则:一 环境 一 namespace,绝不 跨 环境 共用。feed-dev / feed-staging / feed-prodkube-systemkube-public 留 给 集群 自身。
  • PersistentVolumeClaim——StatefulSet 绑 的 存储 请求。集群 的 StorageClass(云 上 gp3 AWS / pd-ssd GCP / managed-premium Azure / 阿里云 alicloud-disk-essd;本地 kindlocal-path)控制 实际 provision。

掌握 这 八 个;生产 有 状态 工作负载 用 算子(Kafka 用 Strimzi,Postgres 用 Crunchy 或 Patroni);N >= 2 环境 用 Helm。

代表性 Deployment 示例

feed-handler-consumer 的:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: feed-handler-consumer
  namespace: feed-dev
  labels:
    app: feed-handler
    component: consumer
spec:
  replicas: 3
  selector:
    matchLabels:
      app: feed-handler
      component: consumer
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
  template:
    metadata:
      labels:
        app: feed-handler
        component: consumer
    spec:
      containers:
        - name: consumer
          image: feed-handler:1.0.0
          command: ["python", "-m", "feed_handler", "consumer"]
          envFrom:
            - configMapRef:
                name: feed-handler-config
            - secretRef:
                name: feed-handler-secrets
          resources:
            requests:
              cpu: 100m
              memory: 512Mi
            limits:
              cpu: 1
              memory: 2Gi
          readinessProbe:
            exec:
              command: ["python", "-c", "import feed_handler"]
            initialDelaySeconds: 10
            periodSeconds: 30
            timeoutSeconds: 5
          livenessProbe:
            exec:
              command: ["python", "-c", "import feed_handler"]
            initialDelaySeconds: 30
            periodSeconds: 60

代表性 StatefulSet 示例

timescaledb 的:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: timescaledb
  namespace: feed-dev
spec:
  serviceName: timescaledb-headless
  replicas: 1
  selector:
    matchLabels:
      app: timescaledb
  template:
    metadata:
      labels:
        app: timescaledb
    spec:
      containers:
        - name: timescaledb
          image: timescale/timescaledb:latest-pg16
          envFrom:
            - secretRef:
                name: feed-handler-secrets
          ports: [{containerPort: 5432}]
          volumeMounts:
            - name: ts-data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
    - metadata:
        name: ts-data
      spec:
        accessModes: [ReadWriteOnce]
        resources:
          requests:
            storage: 10Gi
        storageClassName: standard
# prod uses the Strimzi (Kafka) and Crunchy / Patroni (Postgres) operators rather than raw StatefulSets

serviceName: timescaledb-headless 是 headless service 模式,给 每 个 replica 一 个 稳定 DNS(timescaledb-0.timescaledb-headless.feed-dev.svc.cluster.local)。volumeClaimTemplates 给 每 个 replica 创建 一 个 名字 稳定(ts-data-timescaledb-0)、随 pod 重启 不 失 的 PVC。

ConfigMap 与 Secret

apiVersion: v1
kind: ConfigMap
metadata:
  name: feed-handler-config
  namespace: feed-dev
data:
  KAFKA_BOOTSTRAP_SERVERS: kafka:9092
  TOPIC: ticks.sse.510300
  LOG_LEVEL: INFO

---
apiVersion: v1
kind: Secret
metadata:
  name: feed-handler-secrets
  namespace: feed-dev
type: Opaque
data:
  PG_DSN: cG9zdGdyZXNxbDovL3Bvc3RncmVzOnBvc3RncmVzQHRpbWVzY2FsZWRiOjU0MzIvd2FyZWhvdXNl
# base64 is encoding, NOT encryption; production uses cluster KMS for encryption-at-rest

资源 请求 与 限制——生产 单 一 最 重要 纪律

每 个 生产 pod 模板 都 必须 同时 声明 resources.requestsresources.limitscpumemoryfeed-handler-consumer 的 取值:requests.cpu: 100mrequests.memory: 512Milimits.cpu: 1limits.memory: 2Gi。单位 m 是 milli-CPU(1000m = 1 核);Mi 是 mebibyte;Gi 是 gibibyte。requests 告诉 调度器 该 pod 需要 多少 才 能 放(不 设 requests,调度器 把 pod 当 0 算,迟早 超 售 崩溃)。limits 限制 实际 消耗(不 设 limits,跑 飞 的 pod 会 把 整 个 节点 拖 死;内核 通过 cgroups 强制)。合 规则:requests 给 调度器、limits 给 内核;不 设 requests 会 超 售 崩;不 设 limits 跑 飞 拖 死 节点;不 测 量 写 出 来 的 请求 与 限制 都 是 错 的——先 测、再 写、再 迭代。撞 上 limits.memory 的 pod 以 退出 码 137、事件 原因 OOMKilled 被 杀——kubectl describe podkubectl get events 都 能 看 到。

kind 集群 起 起

六 条 命令 的 端到端 顺序:

kind create cluster --name feed-dev
kind load docker-image feed-handler:1.0.0 --name feed-dev
kubectl create namespace feed-dev
kubectl apply -f manifests/ -n feed-dev
kubectl get pods -n feed-dev -w
kubectl logs -f deployment/feed-handler-consumer -n feed-dev

kind load docker-image 把 本地 镜像 推到 集群 内的 containerd 镜像库,kubelet 就 不会 去 拉 不存在 的 registry。kubectl apply -f manifests/ 把 目录 内 每 个 YAML 应用 进 集群;kubectl get pods -n feed-dev -w 看 滚动 起 来 直到 五 个 pod Running

Helm、Kustomize、GitOps 点 到 为止

原生 manifest 适合 一 环境 与 教学 清晰(本课 用 这种)。N >= 2 环境 且 差异 不 trivial 用 Helm chart:一 个 Chart.yaml、每 环境 一 个 values.yaml、模板 化 的 templates/deployment.yaml,通过 helm install feed-handler ./chart -f values.prod.yaml -n feed-prod 渲染。Kustomize 是 overlay 模板 的 替代:base/ 目录 加 overlays/<env>/kustomization.yaml patch。ArgoCD 与 Flux 是 推 模式 kubectl apply from CI 的 GitOps 拉 模式 替代。HPA / KEDA 自动 伸缩,Istio / Linkerd / Cilium service mesh——都 是 forward-pointer。

纪律 总结

compose 本地,Kubernetes 部署。每 个 生产 pod 设 资源 请求 与 限制。有状态 服务 用 StatefulSet + 稳定 PVC。非 secret 配置 进 ConfigMap,凭据 进 Secret 并 牢 记 「base64 是 编码,不 是 加密」。一 环境 一 namespace。N >= 2 环境 用 Helm。生产 有 状态 工作负载 用 算子。本课 用 原生 manifest 强调 教学。

练习

Exercise

取 L2 构建 的 feed-handler:1.0.0 镜像,用 两 种 方式 编排。

A 部分——Docker Compose 本地:(a) 按 本课 写 docker-compose.yml:五 个 服务、全部 健康 检查、命名 网络 feed-net、命名 卷 kafka-datats-dataenv_file: .env 引用。(b) 建 .envPOSTGRES_PASSWORD=postgres(只 用于 开发),建 .env.example 同 key 用 占位 值(这 一 份 ​要​ 提交 进 git)。(c) docker compose up -d 起 栈,docker compose ps 验证 五 个 服务 都 healthy,再 docker compose logs -f feed-handler-consumer 看 3.6.4 L4 的 结构化 JSON 日志 流。(d) docker compose down -v 拆 栈。

B 部分——Kubernetes 本地 集群:(e) kind create cluster --name feed-dev 建 集群。(f) 在 manifests/ 内 写 Kubernetes manifest——namespace.yaml(Namespace feed-dev)、kafka-statefulset.yaml + kafka-service.yaml(单 broker StatefulSet 带 # prod uses Strimzi 注释)、timescaledb-statefulset.yaml + timescaledb-service.yaml(单 节点 StatefulSet 带 # prod uses Crunchy/Patroni 注释)、feed-handler-producer-deployment.yaml(replicas 1)、feed-handler-consumer-deployment.yaml(replicas 3)、feed-handler-monitor-deployment.yaml(replicas 1)、config.yaml(本课 的 ConfigMap)、secrets.yaml(base64 编码 PG_DSN 的 Secret 加 解释 「base64 不是 加密」的 注释)。每 个 工作负载 模板 ​必须​resources.requests + resources.limitsreadinessProbe。(g) kind load docker-image feed-handler:1.0.0 --name feed-dev 把 镜像 推 进 集群。(h) kubectl create namespace feed-devkubectl apply -f manifests/ -n feed-dev。(i) kubectl get pods -n feed-dev -w 看 五 个 pod 都 Running。(j) kubectl logs -f deployment/feed-handler-consumer -n feed-dev 看 日志 流。(k) 试 滚动 更新:把 consumer Deployment manifest 内 replicas: 3 改 为 replicas: 5、重新 apply、看 滚动 过程,再 kubectl rollout undo deployment/feed-handler-consumer -n feed-dev 回滚。用 一 句话 写 出 如果 这 是 测试 或 生产 部署(而 不 是 kind 集群)会 有 哪些 不同(提示:托管 StorageClass、registry endpoint、RBAC、NetworkPolicy、有 状态 工作负载 用 算子)。

提示
若 pod 长 时间 停 在 Pending,跑 kubectl describe pod <pod> -n feed-devEvents:——kind 集群 上 最 常见 的 原因 是 PVC 绑 不 上(没有 默认 StorageClass)或 镜像 拉 不 到(忘 了 kind load docker-image)。
提示
kubectl rollout undo 报 「no previous revision found」,确认 你 在 两 次 apply 之间 实际 改 了 manifest——修订 历史 只 记录 pod template 的 变更,仅 改 replicas 数 不 算。

必备 组件 回顾

本课 交付物 对 合约 的 映射:

  1. Fenced ```yaml 块——生产 等级 docker-compose.yml,五 服务(kafkatimescaledbfeed-handler-producerfeed-handler-consumerfeed-handler-monitor)、命名 网络 feed-net、命名 卷 kafka-data + ts-data、健康 检查、depends_on: service_healthyrestart: unless-stoppedenv_file: .env
  2. Fenced ```yaml 块——consumer Deployment manifest,replicas: 3strategy: RollingUpdatemaxSurge: 25% / maxUnavailable: 25%)、image: feed-handler:1.0.0command: ['python', '-m', 'feed_handler', 'consumer']envFrom: [configMapRef, secretRef]resources.requests: {cpu: 100m, memory: 512Mi}resources.limits: {cpu: 1, memory: 2Gi}readinessProbelivenessProbe
  3. Fenced ```yaml 块——timescaledb StatefulSetserviceName: timescaledb-headlessvolumeClaimTemplatesaccessModes: [ReadWriteOnce]storage: 10GistorageClassName: standard)、生产 指向 Strimzi / Crunchy / Patroni 算子 的 注释。
  4. Fenced ```yaml 块——ConfigMap feed-handler-configKAFKA_BOOTSTRAP_SERVERSTOPICLOG_LEVEL)与 Secret feed-handler-secrets(Opaque、base64 编码 PG_DSN、注释 「base64 是 编码、不 是 加密;生产 用 集群 KMS 做 静态 加密」)。
  5. Fenced ```bash 块——kind 集群 六 条 启动 命令:kind create cluster --name feed-devkind load docker-image feed-handler:1.0.0 --name feed-devkubectl create namespace feed-devkubectl apply -f manifests/ -n feed-devkubectl get pods -n feed-dev -wkubectl logs -f deployment/feed-handler-consumer -n feed-dev
  6. Inline-code 列表 八 个 Kubernetes 原语:PodDeploymentStatefulSetServiceConfigMapSecretNamespacePersistentVolumeClaim
  7. Inline-code 列表 资源 请求 + 限制 worked-example 取值:requests.cpu: 100mrequests.memory: 512Milimits.cpu: 1limits.memory: 2Gi
  8. 上面 的 练习 加 两 个 渐进式 Hint。

中国 区 锚点

国内 量化 firm 的 编排 走 同 一 套 沪深300 ETF(510300)合成 流 形态:kind worked example 集群 形态 与 平台 组 在 自建 集群 或 阿里云 ACK / 腾讯云 TKE / 华为云 CCE 上 部署 给 上证 SSE / 深证 SZSE / CFFEX 行情 入口 的 manifest 一致;生产 上 把 local-path StorageClass 换 成 阿里云 ESSD(alicloud-disk-essd)或 腾讯云 CBS、把 ConfigMap 内的 TOPIC 换 成 ticks.sse.510300 / ticks.sse.510050 / ticks.cffex.if2503 即可——manifest 形态 不变。私募 内部 凭据 走 自建 Vault 或 KMS 加 sealed-secrets-controller;50ETF / 300ETF / 沪深300ETF 与 CFFEX 期指 都 走 同 一 套 Deployment 形态。T+1 结算 周期、涨跌停 板 风控 在 应用 层 处理,不 影响 manifest 与 资源 请求 / 限制 的 形态。

国内 集群 还 有 一 项 实操 经验 值得 在 本 节 锚 住:基础 镜像 与 算子 镜像 的 拉取 速度 在 国内 公有云 与 自建 集群 上 差异 显著,因此 imagePullPolicy: IfNotPresent 是 默认 配置,配合 集群 节点 预拉 关键 镜像(confluentinc/cp-kafka:7.6.0timescale/timescaledb:latest-pg16、自有 feed-handler:1.0.0)以 减少 滚动 更新 时 的 等待 时间。阿里云 ACK / 腾讯云 TKE 提供 自动 镜像 加速 接入 上游 Harbor 或 ACR 的 pull-through cache,命名 空间 内的 Deployment manifest 写 法 不变 只 是 镜像 前缀 变 成 内网 域名。私募 量化 firm 的 量化 业务 团队 与 平台 团队 通过 namespace 与 ConfigMap 切 出 一 个 业务 自治 区,集群 级 RBAC 限制 业务 团队 只 能 在 自己 namespace 内 apply manifest,平台 团队 拥有 集群 范围 的 ClusterRole 与 算子 镜像。涨跌停 在 数据 层 由 上游 行情 网关 标 字段,consumer 拿 到 后 直接 走 业务 分支,不 触 manifest。沪深300 ETF(300ETF / 沪深300ETF)与 上证 50ETF(50ETF)的 入库 形态 在 manifest 层 完全 一致,只 在 ConfigMap 内 改 TOPIC 值 与 WINDOW_NS。CFFEX 股指 期货 IF / IC / IH 在 主力 合约 换月 时 的 重启 路径 走 kubectl rollout restart deployment/feed-handler-consumer,不 改 manifest。

阅读 清单

Kubernetes 官方 中文 文档 kubernetes.io/zh-cn/docs/;Helm 中文 文档 helm.sh/zh/docs/;阿里云 ACK 文档 help.aliyun.com/product/85222.html;腾讯云 TKE 文档 cloud.tencent.com/document/product/457;公有 云 厂商 容器 服务 帮助 中心;极客 时间《深入剖析 Kubernetes》系列;极客 时间《Kubernetes 编排实战》系列;极客 时间《容器实战高手课》系列;《Kubernetes 权威指南》第 5 版;KubeSphere 官方 中文 文档;Rancher 中文 文档;OpenKruise 项目 文档。一条 额外 注释:国内 量化 firm 的 Kubernetes 集群 多 由 平台 / DevOps 团队 维护;quant developer 负责 自己 服务 的 Deployment / Service / ConfigMap / Secret manifest,集群 配置(CNI / RBAC / ingress controller / observability stack)是 平台 团队 边界。

通往 L4 的 桥

下 一 课 把 L1 + L2 + L3 通过 CI 流水线 串 起来,让 本地 构建 的 同 一 份 镜像 在 每次 合 入 main 时 自动 部署 到 真 实 的 kind 集群,并 演练 一 条 一 行 命令 即 可 完成 的 回滚 路径。