15、容器化与编排:Docker镜像构建、Kubernetes部署、CI/CD流水线、灰度发布

做市商系统跑在裸机上?我劝你趁早放弃这个想法。

几年前我接手过一套老系统,每次上线都要运维手动拷贝jar包、重启进程,一搞就是大半夜。更可怕的是,一旦某个依赖版本对不上,整个环境就炸了。后来我们全面转向容器化,才算是真正解放了生产力。

这一章,我就把我们在结构化产品做市商系统上,从Docker镜像构建到K8s灰度发布的完整实践,掰开了讲给你听。

15.1 Docker镜像构建:从“能用”到“最优”

Docker镜像构建,说白了就是把你的应用和它需要的运行环境打包成一个“集装箱”。但怎么打包,学问很大。

15.1.1 基础镜像选择

我见过有人直接用ubuntu:latest做基础镜像,一个镜像1个多G。做市商系统对延迟敏感,镜像越大,拉取越慢,启动越慢。

我个人习惯用Alpine或者Distroless镜像。比如我们的行情处理服务,用golang:1.21-alpine编译,最终镜像只有20多M。

# 多阶段构建示例
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o market-maker

FROM alpine:3.19
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /app/market-maker /usr/local/bin/
EXPOSE 8080
CMD ["market-maker"]
避坑指南:我曾经在基础镜像里忘了装ca-certificates,结果服务调用外部API时一直报TLS错误,排查了整整两个小时。记住,Alpine镜像默认不带CA证书。

15.1.2 镜像分层优化

Docker镜像是由一层层只读文件系统叠加的。每一行RUN指令都会产生一个新层。层数越多,镜像越大,构建越慢。

我常用的优化技巧:

  • 合并RUN指令:把多个apt-get install写在一行,用&&连接
  • 清理缓存:安装完依赖后立即清理apt缓存,减少层体积
  • 利用构建缓存:把不常变的依赖拷贝放在前面,代码拷贝放在后面
# 坏的实践:多层且不清理缓存
RUN apt-get update
RUN apt-get install -y python3
RUN apt-get install -y redis-tools
# 好的实践:合并指令并清理
RUN apt-get update && apt-get install -y \
    python3 \
    redis-tools \
    && rm -rf /var/lib/apt/lists/*

15.2 Kubernetes部署:让服务自动“跳舞”

镜像构建好了,接下来就是部署。Kubernetes(K8s)是目前容器编排的事实标准。做市商系统对高可用和弹性伸缩要求极高,K8s正好派上用场。

15.2.1 核心资源对象

在K8s里,我们主要用这几个东西:

资源类型 用途 做市商场景
Deployment 管理无状态服务的副本和滚动更新 行情推送、订单路由
StatefulSet 管理有状态服务,保证Pod标识稳定 Redis集群、Kafka
Service 提供稳定的网络入口和负载均衡 对外暴露API
ConfigMap/Secret 管理配置和敏感信息 交易所API密钥、数据库密码

举个例子,我们的订单路由服务部署文件长这样:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-router
  labels:
    app: order-router
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-router
  template:
    metadata:
      labels:
        app: order-router
    spec:
      containers:
      - name: order-router
        image: registry.internal/market-maker/order-router:1.2.3
        ports:
        - containerPort: 8080
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
          limits:
            memory: "1Gi"
            cpu: "1000m"
        env:
        - name: REDIS_ADDR
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: redis.addr
注意:资源限制一定要设。我曾经没设limits,结果某个服务内存泄漏,直接把整个Node打爆了,影响了同节点上的其他做市策略。

15.2.2 健康检查与自愈

做市商系统最怕什么?服务挂了没人知道。K8s的存活探针和就绪探针就是干这个的。

  • livenessProbe:检查容器是否还活着,死了就重启
  • readinessProbe:检查服务是否就绪,没就绪就不给流量
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5
readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 3

15.3 CI/CD流水线:从代码到上线,全自动化

手动部署?那是上个时代的事了。我们的CI/CD流水线,从代码提交到灰度发布,全程无人值守。

15.3.1 流水线阶段设计

我习惯把流水线分成这几个阶段:

  1. 代码检查:静态分析、单元测试、代码规范检查
  2. 镜像构建:多阶段构建,推送到私有镜像仓库
  3. 镜像扫描:用Trivy扫描漏洞,高危漏洞直接阻断
  4. 部署到测试环境:自动部署到dev/staging
  5. 集成测试:跑自动化回归测试,验证核心交易逻辑
  6. 灰度发布:逐步替换生产环境实例
# GitLab CI 示例片段
stages:
  - lint
  - test
  - build
  - scan
  - deploy-dev
  - deploy-prod

build-image:
  stage: build
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

scan-image:
  stage: scan
  script:
    - trivy image --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
我的经验:镜像扫描一定要放在部署之前。有一次扫描发现基础镜像里有个CVE-2023-xxxxx的高危漏洞,我们立刻换了基础镜像版本,避免了生产事故。

15.4 灰度发布:让风险可控

做市商系统直接跟真金白银打交道,全量发布的风险太大了。灰度发布,说白了就是让一小部分流量先跑新版本,观察没问题再全量推。

15.4.1 K8s原生滚动更新

K8s的Deployment自带滚动更新策略,可以控制每次更新的Pod数量:

strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1        # 最多比期望多1个Pod
    maxUnavailable: 0  # 更新期间不能有Pod不可用

但这种方式太粗糙了。它只能按Pod数量滚动,不能按流量比例控制。

15.4.2 基于Service Mesh的灰度发布

我们用的是Istio,通过VirtualService和DestinationRule实现精细化的流量控制。

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: order-router-vs
spec:
  hosts:
  - order-router
  http:
  - match:
    - headers:
        canary:
          exact: "true"
    route:
    - destination:
        host: order-router
        subset: v2
      weight: 100
  - route:
    - destination:
        host: order-router
        subset: v1
      weight: 90
    - destination:
        host: order-router
        subset: v2
      weight: 10

这个配置的意思是:

  • 如果请求头里带了canary: true,全部路由到v2版本(内部测试用)
  • 其他请求,90%走v1,10%走v2

观察一段时间后,如果v2的延迟、错误率、交易成功率都正常,就把v2的权重逐步调到100%。

我曾经踩过的坑:灰度发布时只关注了业务指标,忘了监控资源使用。结果v2版本有个内存泄漏,灰度到50%时,集群内存被打满,影响了v1的服务。从那以后,我每次灰度都会同时监控CPU、内存、网络IO。

15.5 知识体系总览

下面这张图,把容器化与编排的核心逻辑串起来了:

容器化与编排核心知识体系 源代码 CI/CD 流水线 Docker 镜像 Kubernetes 集群 Deployment | Service | ConfigMap | Secret 部署策略:滚动更新 → 灰度发布(Istio流量控制) 关键点:镜像分层优化 | 健康检查 | 资源限制 | 灰度监控 避坑:基础镜像选型 | 缓存清理 | 漏洞扫描 | 内存泄漏监控

嗯,这一章的内容就到这里。容器化不是银弹,但它确实解决了环境一致性、弹性伸缩、自动化部署这些做市商系统的核心痛点。你想想看,以前上线要折腾一晚上,现在点个按钮,几分钟就搞定了,这就是技术带来的价值。


无相订单流研究社 微信Lucian808555