上海一家 私募 的 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) Secret 与 ConfigMap 一级 资源 与 工作负载 解耦,改 配置 不 用 重建 镜像;(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 -d、docker 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-prod;kube-system与kube-public留 给 集群 自身。PersistentVolumeClaim——StatefulSet 绑 的 存储 请求。集群 的StorageClass(云 上gp3AWS /pd-ssdGCP /managed-premiumAzure / 阿里云alicloud-disk-essd;本地kind用local-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.requests 与 resources.limits 的 cpu 与 memory。feed-handler-consumer 的 取值:requests.cpu: 100m、requests.memory: 512Mi、limits.cpu: 1、limits.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 pod 与 kubectl 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-data 与 ts-data、env_file: .env 引用。(b) 建 .env 写 POSTGRES_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.limits 和 readinessProbe。(g) kind load docker-image feed-handler:1.0.0 --name feed-dev 把 镜像 推 进 集群。(h) kubectl create namespace feed-dev 后 kubectl 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、有 状态 工作负载 用 算子)。
提示
Pending,跑 kubectl describe pod <pod> -n feed-dev 看 Events:——kind 集群 上 最 常见 的 原因 是 PVC 绑 不 上(没有 默认 StorageClass)或 镜像 拉 不 到(忘 了 kind load docker-image)。提示
kubectl rollout undo 报 「no previous revision found」,确认 你 在 两 次 apply 之间 实际 改 了 manifest——修订 历史 只 记录 pod template 的 变更,仅 改 replicas 数 不 算。必备 组件 回顾
本课 交付物 对 合约 的 映射:
- Fenced
```yaml块——生产 等级docker-compose.yml,五 服务(kafka、timescaledb、feed-handler-producer、feed-handler-consumer、feed-handler-monitor)、命名 网络feed-net、命名 卷kafka-data+ts-data、健康 检查、depends_on: service_healthy、restart: unless-stopped、env_file: .env。 - Fenced
```yaml块——consumerDeploymentmanifest,replicas: 3、strategy: RollingUpdate(maxSurge: 25% / maxUnavailable: 25%)、image: feed-handler:1.0.0、command: ['python', '-m', 'feed_handler', 'consumer']、envFrom: [configMapRef, secretRef]、resources.requests: {cpu: 100m, memory: 512Mi}、resources.limits: {cpu: 1, memory: 2Gi}、readinessProbe与livenessProbe。 - Fenced
```yaml块——timescaledbStatefulSet,serviceName: timescaledb-headless、volumeClaimTemplates(accessModes: [ReadWriteOnce]、storage: 10Gi、storageClassName: standard)、生产 指向 Strimzi / Crunchy / Patroni 算子 的 注释。 - Fenced
```yaml块——ConfigMapfeed-handler-config(KAFKA_BOOTSTRAP_SERVERS、TOPIC、LOG_LEVEL)与Secretfeed-handler-secrets(Opaque、base64 编码PG_DSN、注释 「base64 是 编码、不 是 加密;生产 用 集群 KMS 做 静态 加密」)。 - Fenced
```bash块——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。 - Inline-code 列表 八 个 Kubernetes 原语:
Pod、Deployment、StatefulSet、Service、ConfigMap、Secret、Namespace、PersistentVolumeClaim。 - Inline-code 列表 资源 请求 + 限制 worked-example 取值:
requests.cpu: 100m、requests.memory: 512Mi、limits.cpu: 1、limits.memory: 2Gi。 - 上面 的 练习 加 两 个 渐进式 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.0、timescale/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 集群,并 演练 一 条 一 行 命令 即 可 完成 的 回滚 路径。