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"]
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
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 流水线阶段设计
我习惯把流水线分成这几个阶段:
- 代码检查:静态分析、单元测试、代码规范检查
- 镜像构建:多阶段构建,推送到私有镜像仓库
- 镜像扫描:用Trivy扫描漏洞,高危漏洞直接阻断
- 部署到测试环境:自动部署到dev/staging
- 集成测试:跑自动化回归测试,验证核心交易逻辑
- 灰度发布:逐步替换生产环境实例
# 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
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%。
15.5 知识体系总览
下面这张图,把容器化与编排的核心逻辑串起来了:
嗯,这一章的内容就到这里。容器化不是银弹,但它确实解决了环境一致性、弹性伸缩、自动化部署这些做市商系统的核心痛点。你想想看,以前上线要折腾一晚上,现在点个按钮,几分钟就搞定了,这就是技术带来的价值。
交易系统化学习资料 微信Strategy888888