背景与问题
很多团队第一次做云原生改造,并不是从“零开始设计微服务”,而是面对一个已经跑了几年、功能不断叠加的单体系统:
- 一个应用包打天下
- 发布依赖人工 SSH 上机
- 流量高峰靠“临时加机器”
- 数据库和缓存是瓶颈
- 一旦某台机器有问题,恢复过程靠经验和运气
我见过不少项目,单体阶段其实也能跑得不错,但随着业务增长,问题会变得很具体:
- 可用性不足:单机部署或双机热备,容灾能力有限
- 扩缩容不灵活:节假日流量上来时,机器加不动,应用发布也不敢做
- 环境不一致:测试能跑,线上不行,通常是配置、依赖或系统差异导致
- 交付效率低:每次发布都像一次“手术”
- 资源利用率低:有的机器 CPU 很闲,内存却不够;有的服务反过来
这时候,把系统迁移到高可用 Kubernetes 集群,往往不是为了追时髦,而是为了回答三个非常现实的问题:
- 如何让服务挂一台不影响整体
- 如何让发布、扩容、回滚标准化
- 如何根据业务增长做容量规划
但这里有个误区:从单体迁移到 Kubernetes,不等于立刻拆成一堆微服务。
更稳妥的路径,通常是:
先容器化,再集群化,再高可用,最后按业务边界逐步拆分。
本文就从这个角度展开:不是空谈概念,而是按一条工程上真正能落地的路线,讲清楚设计、部署与容量规划。
方案对比与迁移思路
在正式上 Kubernetes 之前,先把常见方案放在一起看一下。
方案对比
| 方案 | 优点 | 缺点 | 适用阶段 |
|---|---|---|---|
| 单体 + 虚拟机/物理机 | 简单直接,学习成本低 | 扩容慢,发布重,故障域大 | 初创、低流量 |
| 单体 + Docker Compose | 容器化快,环境一致性提升 | 编排能力有限,高可用弱 | 过渡期 |
| 单体 + Kubernetes | 调度、伸缩、发布、恢复标准化 | 学习成本高,基础设施复杂 | 中大型业务 |
| 微服务 + Kubernetes | 解耦彻底,弹性最佳 | 架构复杂度高,治理成本大 | 成熟团队 |
对大多数中级团队来说,更推荐的路径是:
- 单体应用容器化
- 迁移到 Kubernetes,先保持单体逻辑不变
- 引入高可用控制面与多副本业务部署
- 对状态组件做外置或托管
- 按访问链路、模块边界逐步拆服务
这样做的好处是:把系统风险拆成几次小风险,而不是一次大爆炸。
核心原理
这一部分,我们重点讲“高可用 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[对象存储]
这个架构强调几个点:
- 控制面奇数节点部署,通常是 3 台起步
- 业务 Pod 跨节点分布,避免单节点故障导致全量中断
- 状态组件外置,降低迁移复杂度
- 入口统一通过 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: OOMKilledBack-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. 最小权限原则
- 不要默认用
defaultServiceAccount - 为应用单独创建 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 不要拍脑袋
建议流程:
- 先压测
- 观察 CPU、内存、GC、响应时间
- 设定 requests 为常态值附近
- 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 集群,最容易犯的错误,不是技术不会,而是一次想做太多:
- 想同时拆微服务
- 想同时重构数据库
- 想同时改发布流程和监控体系
工程上更稳的做法是:
- 先让单体应用具备容器化和无状态运行能力
- 再迁移到 Kubernetes 获得标准化部署与弹性能力
- 补齐控制面高可用、探针、PDB、HPA、监控告警
- 最后基于真实流量和容量数据做拆分决策
如果要给一个可执行建议,我会这样落地:
- 小团队:先做“单体上 K8s”,不要急着拆
- 中等规模业务:优先补高可用、监控、容量规划
- 高峰明显的业务:把压测和扩容预案当成上线前置条件
- 状态重、事务强的模块:别急着云原生化,先外置状态再谈拆分
最后再强调一句:
Kubernetes 解决的是运行与交付问题,不会自动修复糟糕的应用设计。
把边界划清楚,你的迁移项目会顺利很多;把目标拆成几步做,系统和团队都会更稳。