跳转到内容
123xiao | 无名键客

《集群架构实战:从单体迁移到高可用 Kubernetes 集群的设计、部署与容量规划》

字数: 0 阅读时长: 1 分钟

背景与问题

很多团队第一次做云原生改造,并不是从“零开始设计微服务”,而是面对一个已经跑了几年、功能不断叠加的单体系统:

  • 一个应用包打天下
  • 发布依赖人工 SSH 上机
  • 流量高峰靠“临时加机器”
  • 数据库和缓存是瓶颈
  • 一旦某台机器有问题,恢复过程靠经验和运气

我见过不少项目,单体阶段其实也能跑得不错,但随着业务增长,问题会变得很具体:

  1. 可用性不足:单机部署或双机热备,容灾能力有限
  2. 扩缩容不灵活:节假日流量上来时,机器加不动,应用发布也不敢做
  3. 环境不一致:测试能跑,线上不行,通常是配置、依赖或系统差异导致
  4. 交付效率低:每次发布都像一次“手术”
  5. 资源利用率低:有的机器 CPU 很闲,内存却不够;有的服务反过来

这时候,把系统迁移到高可用 Kubernetes 集群,往往不是为了追时髦,而是为了回答三个非常现实的问题:

  • 如何让服务挂一台不影响整体
  • 如何让发布、扩容、回滚标准化
  • 如何根据业务增长做容量规划

但这里有个误区:从单体迁移到 Kubernetes,不等于立刻拆成一堆微服务。
更稳妥的路径,通常是:

先容器化,再集群化,再高可用,最后按业务边界逐步拆分。

本文就从这个角度展开:不是空谈概念,而是按一条工程上真正能落地的路线,讲清楚设计、部署与容量规划。


方案对比与迁移思路

在正式上 Kubernetes 之前,先把常见方案放在一起看一下。

方案对比

方案优点缺点适用阶段
单体 + 虚拟机/物理机简单直接,学习成本低扩容慢,发布重,故障域大初创、低流量
单体 + Docker Compose容器化快,环境一致性提升编排能力有限,高可用弱过渡期
单体 + Kubernetes调度、伸缩、发布、恢复标准化学习成本高,基础设施复杂中大型业务
微服务 + Kubernetes解耦彻底,弹性最佳架构复杂度高,治理成本大成熟团队

对大多数中级团队来说,更推荐的路径是:

  1. 单体应用容器化
  2. 迁移到 Kubernetes,先保持单体逻辑不变
  3. 引入高可用控制面与多副本业务部署
  4. 对状态组件做外置或托管
  5. 按访问链路、模块边界逐步拆服务

这样做的好处是:把系统风险拆成几次小风险,而不是一次大爆炸。


核心原理

这一部分,我们重点讲“高可用 Kubernetes 集群”到底在保障什么。

1. 高可用不是只看 Pod 副本数

很多人会说:Deployment 配 3 个副本,不就高可用了?
其实这只解决了应用层实例冗余,但还不够。

完整的高可用,至少包含三层:

  • 控制面高可用:API Server、etcd、Scheduler、Controller Manager 不单点
  • 节点层高可用:Worker 节点故障不影响服务整体
  • 应用层高可用:Pod 多副本、服务发现、滚动发布、探针自愈

2. Kubernetes 的几个关键对象

从单体迁移时,你最需要理解这几个对象:

  • Deployment:定义副本数、镜像版本、滚动更新策略
  • Service:给一组 Pod 提供稳定访问入口
  • Ingress:暴露 HTTP/HTTPS 路由
  • ConfigMap / Secret:配置与敏感信息解耦
  • HPA:根据 CPU/内存等指标自动扩缩容
  • PDB:限制同一时间可中断 Pod 数量,避免维护时全杀光

3. 单体迁移的关键原则

无状态优先

如果你的单体应用还把上传文件写本地磁盘、把 Session 放内存,那迁移会很痛。
更推荐先改成:

  • Session 放 Redis
  • 文件放对象存储或共享存储
  • 配置从环境变量或配置中心注入
  • 日志输出到 stdout/stderr

状态外置

Kubernetes 非常适合跑无状态业务。数据库、Redis、MQ 不是不能跑在集群里,但对于大多数团队来说:

  • 生产优先使用托管版或独立高可用集群
  • 不要把业务迁移和数据库重构绑定在同一个时间窗口

健康检查可观测

你的单体应用必须明确暴露至少两个端点:

  • /healthz:进程活着
  • /readyz:依赖可用、能接流量

这点非常关键。我当时踩过一个坑:应用进程明明还在,但数据库连接池已经打满,请求进来全超时。没有 readiness probe 时,K8s 还是会把流量打进去。


高可用集群架构设计

下面给一个比较稳妥的生产思路:3 控制面 + N Worker + 外部负载均衡 + 外置状态组件

flowchart TD
    U[用户流量] --> LB[外部负载均衡器]
    LB --> ING[Ingress Controller]
    ING --> SVC[Service]
    SVC --> POD1[Pod A]
    SVC --> POD2[Pod B]
    SVC --> POD3[Pod C]

    subgraph K8s控制面
        APIS[3 x API Server]
        ETCD[3 x etcd]
        SCH[Scheduler]
        CM[Controller Manager]
    end

    subgraph Worker节点
        POD1
        POD2
        POD3
    end

    POD1 --> REDIS[Redis/Session]
    POD2 --> DB[MySQL主从/托管数据库]
    POD3 --> OSS[对象存储]

这个架构强调几个点:

  1. 控制面奇数节点部署,通常是 3 台起步
  2. 业务 Pod 跨节点分布,避免单节点故障导致全量中断
  3. 状态组件外置,降低迁移复杂度
  4. 入口统一通过 Ingress,便于证书、限流、灰度控制

控制面通信视角

sequenceDiagram
    participant User as 用户
    participant LB as LB
    participant Ingress as Ingress
    participant Service as Service
    participant Pod as App Pod
    participant API as API Server
    participant ETCD as etcd

    User->>LB: 发起 HTTP 请求
    LB->>Ingress: 转发流量
    Ingress->>Service: 匹配路由
    Service->>Pod: 负载均衡到某个副本
    Pod-->>User: 返回响应

    API->>ETCD: 读取/写入集群状态
    API->>Pod: 调度与生命周期管理

取舍分析:为什么不是一步拆微服务

很多架构设计文章一上来就讲服务拆分,但真实项目里,先问三个问题:

  • 你的团队是否已经具备服务治理能力?
  • 链路追踪、监控、日志是否已经成体系?
  • 数据一致性问题是否有明确方案?

如果这三件事都没有,先上 Kubernetes 承载单体,往往比“硬拆微服务”更现实。

推荐迁移阶段

阶段 1:容器化单体

目标:

  • 应用打成镜像
  • 配置外置
  • 日志标准输出
  • 健康检查可用

阶段 2:迁移到 Kubernetes

目标:

  • Deployment + Service + Ingress 跑起来
  • 多副本
  • 滚动更新
  • 资源 request/limit 基线明确

阶段 3:高可用增强

目标:

  • 控制面 HA
  • PodDisruptionBudget
  • HPA/VPA 或手工容量策略
  • 监控告警完善

阶段 4:按边界拆分服务

优先拆:

  • 读多写少、边界清晰的模块
  • 异步任务模块
  • 对外接口模块

不优先拆:

  • 强事务耦合模块
  • 数据模型高度共享模块
  • 团队还没准备好的模块

实战代码(可运行)

下面给一个可运行的示例:用一个简单的 Node.js 单体 Web 服务,演示如何容器化并部署到 Kubernetes。

1. 应用代码

app.js

const express = require('express');
const app = express();

const port = process.env.PORT || 8080;
const version = process.env.APP_VERSION || 'v1';

app.get('/', (req, res) => {
  res.json({
    message: 'hello from monolith on kubernetes',
    version,
    hostname: process.env.HOSTNAME || 'unknown'
  });
});

app.get('/healthz', (req, res) => {
  res.status(200).send('ok');
});

app.get('/readyz', (req, res) => {
  // 真实场景下这里可以增加数据库、Redis 等依赖检查
  res.status(200).send('ready');
});

app.listen(port, () => {
  console.log(`app listening on ${port}`);
});

package.json

{
  "name": "monolith-k8s-demo",
  "version": "1.0.0",
  "description": "demo app for kubernetes migration",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "^4.19.2"
  }
}

2. Dockerfile

FROM node:18-alpine

WORKDIR /app
COPY package.json ./
RUN npm install --production

COPY app.js ./

EXPOSE 8080

ENV PORT=8080
CMD ["npm", "start"]

3. 本地构建与运行

docker build -t monolith-k8s-demo:1.0.0 .
docker run -p 8080:8080 monolith-k8s-demo:1.0.0
curl http://localhost:8080/

如果你看到 JSON 返回,说明容器化已经成功。


Kubernetes 部署清单

1. Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: monolith-demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: monolith-demo
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: monolith-demo
    spec:
      containers:
        - name: app
          image: monolith-k8s-demo:1.0.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
          env:
            - name: APP_VERSION
              value: "v1"
          resources:
            requests:
              cpu: "200m"
              memory: "256Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 10
            timeoutSeconds: 2
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /readyz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
            timeoutSeconds: 2
            failureThreshold: 3

2. Service

apiVersion: v1
kind: Service
metadata:
  name: monolith-demo-svc
spec:
  selector:
    app: monolith-demo
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
  type: ClusterIP

3. Ingress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: monolith-demo-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
    - host: monolith.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: monolith-demo-svc
                port:
                  number: 80

4. PodDisruptionBudget

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: monolith-demo-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: monolith-demo

5. HPA

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: monolith-demo-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: monolith-demo
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

6. 一次性部署命令

kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f ingress.yaml
kubectl apply -f pdb.yaml
kubectl apply -f hpa.yaml

验证:

kubectl get pods -o wide
kubectl get svc
kubectl get ingress
kubectl describe hpa monolith-demo-hpa

容量估算:别等资源打满才想起来规划

容量规划是很多团队最容易“后补票”的部分。实际上,在从单体迁移到 Kubernetes 时,容量估算应该前置。

1. 先建立估算模型

至少要有以下输入:

  • 峰值 QPS
  • 平均响应时间
  • 单请求 CPU 消耗
  • 单实例内存稳定占用
  • 发布期间冗余比例
  • 节点可分配资源
  • 故障冗余要求(N+1 / N+2)

一个实用的简化公式:

Pod 数量估算

所需 Pod 数 ≈ 峰值 QPS / 单 Pod 安全承载 QPS

节点数量估算

节点数 ≈ 所需总 CPU / 单节点可分配 CPU
节点数 ≈ 所需总内存 / 单节点可分配内存
最终取较大值,并预留 20%~30% 冗余

2. 例子

假设单体服务经过压测得到:

  • 峰值 QPS:1200
  • 单 Pod 安全承载:200 QPS
  • 单 Pod requests:200m CPU / 256Mi 内存
  • 单 Pod limits:500m CPU / 512Mi 内存
  • 生产希望至少 3 副本,且支持滚动发布
  • 单节点可分配资源:6 vCPU / 12 GiB

则:

业务副本数

1200 / 200 = 6 Pod

考虑滚动发布时 maxSurge=1、流量波动和节点故障,建议至少部署到 8 Pod 的容量水平。

CPU 需求

按 request 估算:

8 * 0.2 = 1.6 vCPU

按 limit 上限估算:

8 * 0.5 = 4 vCPU

内存需求

按 request 估算:

8 * 256Mi = 2048Mi ≈ 2Gi

按 limit 上限估算:

8 * 512Mi = 4096Mi ≈ 4Gi

如果你的节点还有日志采集、监控 Agent、Ingress、CoreDNS 等系统组件,不能把节点资源全给业务。
真实规划里,我一般建议:

  • 系统预留 15%
  • 业务弹性预留 20%
  • 至少能容忍 1 个 Worker 故障

3. 容量规划状态图

stateDiagram-v2
    [*] --> 基线压测
    基线压测 --> 计算单Pod承载
    计算单Pod承载 --> 估算副本数
    估算副本数 --> 估算节点数
    估算节点数 --> 故障冗余校验
    故障冗余校验 --> 发布冗余校验
    发布冗余校验 --> 监控回收修正
    监控回收修正 --> [*]

集群部署建议

本文不展开写 kubeadm 全流程,但在架构上,建议至少遵循下面的部署原则。

控制面

  • 3 台控制平面节点
  • etcd 奇数节点部署
  • API Server 前放一个稳定 VIP 或外部负载均衡器
  • 控制面节点尽量独立,不混跑高负载业务

Worker 节点

  • 至少 3 台,便于副本分散
  • 为业务节点打标签,方便后续按业务类型调度
  • 开启资源限制与驱逐策略,避免单 Pod 吃光整机

网络与存储

  • CNI 选成熟方案,如 Calico
  • Ingress Controller 选 NGINX Ingress 或云厂商托管方案
  • 有状态存储优先外置,实在要上 CSI,也要先评估故障恢复流程

监控与日志

  • 指标:Prometheus + Grafana
  • 日志:EFK 或 Loki
  • 告警:延迟、错误率、CPU、内存、重启次数、节点状态、磁盘压力

常见坑与排查

这部分是迁移里最“值钱”的经验区。我把常见问题按现象来讲。

1. Pod 一直重启

常见原因

  • 启动命令写错
  • livenessProbe 过于激进
  • 应用启动慢,探针提前判死
  • 内存不足被 OOMKilled

排查命令

kubectl get pods
kubectl describe pod <pod-name>
kubectl logs <pod-name> --previous

重点看什么

  • Reason: OOMKilled
  • Back-off restarting failed container
  • 探针失败事件
  • 容器退出码

解决建议

  • 先放宽探针参数
  • 把 JVM/Node/Python 运行时内存参数和容器 limit 对齐
  • 启动慢的应用增加 initialDelaySeconds

2. 服务明明启动了,但流量就是进不来

常见原因

  • readinessProbe 未通过
  • Service selector 和 Pod label 不匹配
  • Ingress 配置错
  • NetworkPolicy 拦截
  • 应用只监听 127.0.0.1

排查命令

kubectl get endpoints monolith-demo-svc
kubectl describe svc monolith-demo-svc
kubectl describe ingress monolith-demo-ingress
kubectl exec -it <pod-name> -- netstat -tunlp

经验提醒

如果 endpoints 为空,十有八九是:

  • selector 配错
  • readiness 失败

这两个问题我见得最多。


3. 滚动发布时出现短暂 502/超时

常见原因

  • maxUnavailable 设置过大
  • readinessProbe 返回过早,服务还没准备好
  • 应用未优雅关闭,旧 Pod 直接被杀
  • 连接池、缓存预热没完成

解决建议

  • Deployment 设置 maxUnavailable: 0
  • 增加 preStop 钩子和 terminationGracePeriodSeconds
  • 应用接收 SIGTERM 后停止接新请求,再处理完存量请求

示例:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: monolith-demo
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: app
          image: monolith-k8s-demo:1.0.0
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 10"]

4. HPA 不生效

常见原因

  • metrics-server 没装
  • Pod 没设置 requests
  • CPU 指标不稳定
  • 应用瓶颈不在 CPU,而在数据库或 I/O

排查命令

kubectl top pod
kubectl get hpa
kubectl describe hpa monolith-demo-hpa

经验提醒

HPA 不是万能按钮。
如果瓶颈在数据库连接数,单纯扩 Pod 可能会把数据库打得更惨。


5. 节点资源还有很多,Pod 却调度不上

常见原因

  • requests 设太大
  • 节点 taint/toleration 不匹配
  • 反亲和规则太严格
  • PVC 绑定失败

排查命令

kubectl describe pod <pod-name>
kubectl get nodes
kubectl describe node <node-name>

重点看调度事件里的原因,Kubernetes 通常写得很直白。


安全/性能最佳实践

这部分我建议不要等“系统稳定后再补”。很多基础项应该在第一次上线就到位。

安全最佳实践

1. 最小权限原则

  • 不要默认用 default ServiceAccount
  • 为应用单独创建 RBAC
  • 禁止容器随意访问集群敏感 API

2. 镜像安全

  • 使用精简基础镜像,如 alpine 或 distroless
  • 固定镜像版本,不用 latest
  • 上线前做镜像漏洞扫描

3. Secret 管理

  • 密码、Token、证书放 Secret
  • 更高要求场景接入外部密钥系统,如 Vault
  • 不要把密钥直接写进镜像或 Git 仓库

4. 容器运行时安全

  • 尽量非 root 运行
  • 关闭不必要的 Linux capability
  • 只读根文件系统能开就开

示例:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: monolith-demo
spec:
  template:
    spec:
      containers:
        - name: app
          image: monolith-k8s-demo:1.0.0
          securityContext:
            runAsNonRoot: true
            runAsUser: 10001
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true

性能最佳实践

1. requests/limits 不要拍脑袋

建议流程:

  1. 先压测
  2. 观察 CPU、内存、GC、响应时间
  3. 设定 requests 为常态值附近
  4. limits 预留峰值空间,但不要高得离谱

2. 单体应用先做“无状态化优化”

重点关注:

  • Session 外置
  • 文件外置
  • 连接池大小合理
  • 启动时间优化
  • 优雅停机支持

3. 发布策略与扩容策略联动

如果业务波峰明显,建议:

  • 日常 3~5 副本保底
  • 活动前手工预扩容
  • HPA 负责兜底,不要全靠它临场救火

4. 观测先行

至少监控这些指标:

  • Pod CPU / 内存
  • 容器重启次数
  • 应用 QPS、P95/P99 延迟
  • 错误率
  • 节点磁盘与网络
  • 数据库连接数与慢查询

一份更落地的迁移清单

如果你准备真的把一个单体系统搬上 Kubernetes,我建议按下面清单执行。

应用侧

  • 支持容器化打包
  • 配置通过环境变量注入
  • 日志输出到标准输出
  • 暴露 /healthz/readyz
  • Session、文件、缓存等状态外置
  • 支持优雅停机

集群侧

  • 3 控制面高可用
  • 至少 3 Worker
  • CNI、Ingress、metrics-server 安装完成
  • 监控、日志、告警接入完成
  • 命名空间、RBAC、镜像仓库准备完成

发布侧

  • Deployment 滚动更新策略明确
  • PDB 已配置
  • HPA 或手工扩容预案明确
  • 回滚命令演练过
  • 高峰期变更冻结策略明确

总结

从单体迁移到高可用 Kubernetes 集群,最容易犯的错误,不是技术不会,而是一次想做太多

  • 想同时拆微服务
  • 想同时重构数据库
  • 想同时改发布流程和监控体系

工程上更稳的做法是:

  1. 先让单体应用具备容器化和无状态运行能力
  2. 再迁移到 Kubernetes 获得标准化部署与弹性能力
  3. 补齐控制面高可用、探针、PDB、HPA、监控告警
  4. 最后基于真实流量和容量数据做拆分决策

如果要给一个可执行建议,我会这样落地:

  • 小团队:先做“单体上 K8s”,不要急着拆
  • 中等规模业务:优先补高可用、监控、容量规划
  • 高峰明显的业务:把压测和扩容预案当成上线前置条件
  • 状态重、事务强的模块:别急着云原生化,先外置状态再谈拆分

最后再强调一句:
Kubernetes 解决的是运行与交付问题,不会自动修复糟糕的应用设计。
把边界划清楚,你的迁移项目会顺利很多;把目标拆成几步做,系统和团队都会更稳。


分享到:

上一篇
《微服务架构中基于服务网格的灰度发布与流量治理实战》
下一篇
《Spring Boot 中基于 Spring Cache + Redis 的多级缓存实战:从热点数据防穿透到一致性治理》