从裸机部署到K8s迁移:一个传统IDC客户的上云全过程
从裸机部署到K8s迁移:一个传统IDC客户的上云全过程
摘要:一个跑了三年的电商平台,从裸机Nginx+Java+MySQL架构迁移到K8s。不是一步到位,经历了三个阶段:先容器化、再编排、最后上K8s。本文完整记录这个过程——哪些环节比预想的顺利、哪些坑踩得猝不及防、最终效果怎么样。
关键词:K8s迁移、容器化、Docker、裸机部署、IDC上云
分类:K8s / 运维 / IDC
起点:一台机器跑一切
客户做电商的,跑了三年。架构简单到不能再简单:
一台物理服务器(16核64G)
├── Nginx(反向代理+静态资源)
├── Tomcat(Java应用)
├── MySQL 5.7
├── Redis
└── 定时任务(crontab)
所有东西跑在同一台机器上。没有容器、没有编排、没有自动化部署。上线就是把jar包传上去,重启Tomcat。
三年了也没出过大问题。用户量稳定在日活几千,一台机器完全够用。
但今年他们拿到了一笔融资,要扩品类、做推广,预计半年内用户量翻好几倍。现有架构的问题会越来越明显:
单点故障。 机器挂了全站不可用。没有备份节点。
部署粗暴。 每次上线都是一次冒险。传jar包、重启Tomcat、祈祷不出问题。回滚更痛苦——得手动把旧jar包换回去再重启。
扩展性为零。 用户量涨了只能升级硬件。加CPU、加内存、换SSD。升级一次停机一次。
数据库跟应用绑在一起。 数据库跑在同一台机器上,应用和数据库抢CPU、抢内存、抢IO。
他们问我怎么办。我的建议是分三步走,不要一步到位。
第一阶段:拆分和容器化(第1-2个月)
第一步:把数据库从应用服务器上拆出来
这是最优先的事情。数据库和应用在同一台机器上,不光抢资源,还导致任何一方的扩容都要动另一方。
# 新开一台服务器专门跑MySQL
# 迁移过程:
# 1. 新机器装MySQL
# 2. 用mysqldump导出旧数据
mysqldump -u root -p --single-transaction --routines --triggers --all-databases > full_dump.sql
# 3. 导入新机器
mysql -u root -p < full_dump.sql
# 4. 改应用的数据库连接地址
# application.yml
spring:
datasource:
url: jdbc:mysql://新数据库IP:3306/mydb
# 5. 重启应用验证
MySQL拆出去之后,应用服务器的CPU和内存占用降了一大截。数据库也有了独立的资源池,不再受应用影响。
拆数据库这个过程比预想的顺利。停了15分钟的机做切换,数据量不大(十几个GB),导出导入加起来20分钟搞定。
第二步:容器化
把应用装进Docker容器。这一步的目的是让部署标准化——不管在哪个环境,容器里的应用行为是一致的。
写Dockerfile:
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
# 构建镜像
docker build -t my-app:1.0.0 .
# 本地测试
docker run -d -p 8080:8080 -e SPRING_DATASOURCE_URL=jdbc:mysql://数据库IP:3306/mydb my-app:1.0.0
容器化本身不难。Java应用打好jar包塞进Docker镜像就行了。
但踩了几个坑:
坑1:配置文件硬编码了IP地址。
# application.yml 里写死了
spring:
datasource:
url: jdbc:mysql://10.0.1.60:3306/mydb
redis:
host: 10.0.1.60
容器化之后这些配置要改成环境变量注入:
spring:
datasource:
url: ${DB_URL:jdbc:mysql://localhost:3306/mydb}
redis:
host: ${REDIS_HOST:localhost}
docker run -d -p 8080:8080 \
-e DB_URL=jdbc:mysql://数据库IP:3306/mydb \
-e REDIS_HOST=redisIP \
my-app:1.0.0
改配置不难,但项目里散布着几十处硬编码的IP地址,一个个找出来改掉花了一整天。
坑2:日志写在容器里。
原来应用日志写在 /var/log/app/ 下面。容器化之后这个路径在容器内部,容器重启日志就没了。
改成stdout输出:
<!-- logback.xml -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
容器化之后日志通过 docker logs 查看。后面K8s阶段再接日志收集系统。
坑3:文件存储。
用户上传的图片存在本地磁盘。容器化之后文件存在容器里,容器没了文件就没了。
短期方案:挂载宿主机目录作为持久存储。
docker run -d -p 8080:8080 \
-v /data/uploads:/app/uploads \
my-app:1.0.0
长期方案:迁移到对象存储(OSS/S3)。这个放到第三阶段做。
第三步:Nginx也容器化
FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY static/ /usr/share/nginx/html/static/
docker build -t my-nginx:1.0.0 .
现在架构变成了:
物理机1:Nginx容器 → 代理到应用容器
物理机1:Java应用容器
物理机2:MySQL
还是在同一台物理机上,但应用已经容器化了。下一步上编排。
第二阶段:Docker Compose编排(第2-3个月)
在正式上K8s之前,先用Docker Compose把容器编排起来。这一步的目的不是"上K8s",而是理清楚服务之间的依赖关系和启动顺序。
# docker-compose.yml
version: '3.8'
services:
nginx:
image: my-nginx:1.0.0
ports:
- "80:80"
- "443:443"
depends_on:
- app
restart: always
app:
image: my-app:1.0.0
environment:
- DB_URL=jdbc:mysql://mysql:3306/mydb
- REDIS_HOST=redis
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
restart: always
mysql:
image: mysql:5.7
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_PASSWORD}
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
restart: always
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
restart: always
volumes:
mysql-data:
redis-data:
# 一键启动所有服务
docker-compose up -d
# 查看状态
docker-compose ps
Docker Compose阶段踩的坑:
数据库初始化。 MySQL容器第一次启动时需要初始化数据库和导入数据。不能只靠docker-compose up。
mysql:
image: mysql:5.7
volumes:
- mysql-data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql # 首次启动自动执行
健康检查很重要。 没有healthcheck的话,docker-compose启动顺序只保证容器"启动",不保证服务"就绪"。MySQL容器起来了但还没初始化完,应用容器就连不上数据库,启动失败。
加上healthcheck和 depends_on: condition: service_healthy 之后,应用容器会等MySQL真正就绪了才启动。
环境变量管理。 密码不能写在docker-compose.yml里(会提交到Git)。用.env文件:
# .env(加入.gitignore)
MYSQL_PASSWORD=xxxxx
DB_URL=jdbc:mysql://mysql:3306/mydb
Docker Compose跑了一段时间,稳定性没问题。但它只适合单机部署——所有容器跑在一台物理机上。要做多机部署、自动扩缩容、滚动更新,还是得上K8s。
第三阶段:迁移到K8s(第3-5个月)
为什么上K8s
Docker Compose解决了部署标准化的问题,但没解决:
单机限制。 所有容器在一台机器上,机器挂了全完。
手动扩缩。 用户量涨了只能手动加机器、改配置。
没有滚动更新。 每次部署还是要停一下。
K8s解决的就是这些问题。但K8s本身也有学习和运维成本。不要为了上K8s而上K8s——如果单机Docker Compose能满足需求,不上也行。
客户的用户量预期要翻几倍,而且后续要拆微服务。这才有了上K8s的必要性。
搭集群
用kubeadm搭了一个最小可用集群:
# 主节点初始化
kubeadm init --pod-network-cidr=10.244.0.0/16 --apiserver-advertise-address=主节点IP
# 配置kubectl
mkdir -p $HOME/.kube
cp /etc/kubernetes/admin.conf $HOME/.kube/config
# 安装网络插件(Calico)
kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml
# 加入工作节点
kubeadm join 主节点IP:6443 --token xxx --discovery-token-ca-cert-hash sha256:xxx
# 验证集群状态
kubectl get nodes
NAME STATUS ROLES AGE VERSION
master-1 Ready control-plane 5d v1.28.2
worker-1 Ready <none> 5d v1.28.2
worker-2 Ready <none> 5d v1.28.2
3个节点(1主2工作),对这个规模的业务够了。后面用户量涨了再加工作节点。
把Docker Compose转成K8s资源
Docker Compose里的每个service对应K8s的一个Deployment+Service。
应用服务:
# app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 2
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: my-app:1.0.0
ports:
- containerPort: 8080
env:
- name: DB_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: db-url
- name: REDIS_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: redis-host
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2000m"
memory: "2Gi"
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 60
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: app-service
spec:
selector:
app: my-app
ports:
- port: 8080
targetPort: 8080
Nginx(Ingress替代):
K8s里不用单独的Nginx容器做反向代理,用Ingress:
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- www.example.com
secretName: tls-secret
rules:
- host: www.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-service
port:
number: 8080
# 安装Ingress Controller
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/baremetal/deploy.yaml
敏感信息用Secret:
kubectl create secret generic app-secrets \
--from-literal=db-url='jdbc:mysql://数据库IP:3306/mydb' \
--from-literal=mysql-password='xxxxx'
非敏感配置用ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
redis-host: "redis-service"
数据库要不要迁到K8s里
这个问题纠结了很久。最终决定:MySQL和Redis不进K8s,继续跑在物理机上。
原因:
MySQL有状态。 K8s擅长管无状态服务,有状态服务(数据库)的运维复杂度高得多。PVC、StorageClass、数据备份、主从复制——在K8s里管这些比在裸机上难不少。
数据安全。 客户的数据库跑了三年了,数据是命脉。迁移到K8s里的PV(PersistentVolume)上,万一PV出问题数据丢了,风险太大。
性能。 K8s里的Pod通过网络访问存储,跟裸机直连本地SSD比,IO性能有差距。数据库对IO延迟敏感,不值得为了"全容器化"牺牲性能。
最终架构:
K8s集群(2工作节点):
├── Ingress Controller(替代Nginx)
├── Java应用(2个Pod,自动扩缩)
└── 未来的新服务(搜索、推荐等)
K8s集群外(裸机):
├── MySQL(独立服务器)
├── Redis(独立服务器)
└── 对象存储(文件上传)
数据库放在K8s外面不是"不完整",是务实的选择。很多团队为了"架构好看"把数据库塞进K8s,结果运维难度上去了,稳定性反而下来了。
迁移过程中的几个坑
坑1:Pod IP不能直接从外部访问
K8s的Pod IP是集群内部的,外部访问不了。需要通过Service或Ingress暴露。
刚开始的时候他们直接ping Pod IP发现不通,以为是网络配错了。其实是K8s的网络模型就是这样的——Pod IP只在集群内部可达。
# 在集群内部可以访问Pod IP
kubectl exec -it debug-pod -- curl http://10.244.1.5:8080/health
# 外部访问要通过Service或Ingress
curl -H "Host: www.example.com" http://IngressController的IP/
坑2:健康检查配错了导致Pod反复重启
Liveness Probe配得太激进了:
# 问题配置
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 10 # Java应用启动要30秒以上
periodSeconds: 5
failureThreshold: 3
Java应用启动要30多秒,但liveness probe只等了10秒就开始检查。应用还没起来,探针连续3次失败,K8s直接杀掉Pod重启。然后又等10秒又杀。反复重启。
# 修正后的配置
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 60 # 给足启动时间
periodSeconds: 30
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
liveness和readiness的区别:liveness失败→K8s杀掉Pod重启;readiness失败→从Service的Endpoints里摘掉,不接受新流量,但不重启。
Java应用的initialDelaySeconds一定要给够。Spring Boot启动30-60秒是正常的,探针太早检查只会造成无意义的重启。
坑3:资源requests设得太高导致调度失败
一开始把requests设得跟limits一样高:
resources:
requests:
cpu: "2000m"
memory: "2Gi"
limits:
cpu: "2000m"
memory: "2Gi"
两个工作节点各8核16G,两个Pod各request 2核2G,加上系统组件,节点的可分配资源很快就满了。第三个Pod调度不上去。
改成requests低于limits:
resources:
requests:
cpu: "500m" # 保底0.5核
memory: "1Gi" # 保底1G
limits:
cpu: "2000m" # 最多2核
memory: "2Gi" # 最多2G
CPU可以超分,request设低一点不影响正常运行,但让调度器有更多空间安排Pod。
坑4:Docker Compose到K8s的环境变量不通用
Docker Compose里的环境变量直接写在docker-compose.yml或者.env文件里。K8s里要用Secret和ConfigMap。
应用代码里的环境变量引用方式不用改(都是从环境变量读),但运维流程完全变了。
# Docker Compose时代:改.env文件,docker-compose restart
# K8s时代:改ConfigMap,kubectl rollout restart
kubectl edit configmap app-config
kubectl rollout restart deployment/app
这一步没有技术难度,但运维习惯要调整。
迁移后的效果
部署流程
迁移前:
传jar包到服务器 → 手动替换旧jar → 重启Tomcat → 验证 → 出问题手动回滚
迁移后:
CI/CD构建Docker镜像 → 推送到镜像仓库 → kubectl set image → 滚动更新 → 出问题kubectl rollout undo
# 滚动更新
kubectl set image deployment/app app=my-app:1.1.0
# 验证
kubectl rollout status deployment/app
# 回滚(如果出问题)
kubectl rollout undo deployment/app
部署从"一次冒险"变成了"一个命令"。回滚也是一个命令的事。
可用性
迁移前:
1台服务器,挂了全站不可用
恢复时间:手动处理,几分钟到几小时
迁移后:
2个Pod分布在不同节点,一个Pod挂了另一个顶上
K8s自动拉起新的Pod
恢复时间:秒级(Pod自动重建)
扩容
迁移前:
用户量涨了 → 升级硬件 → 停机 → 换机器 → 恢复
周期:几天
迁移后:
用户量涨了 → 加副本 → kubectl scale deployment/app --replicas=4
周期:几秒(Pod启动时间)
加节点:几分钟(新服务器加入集群)
资源利用率
迁移前单机部署,高峰期CPU到80%就紧张了,低峰期CPU只有10%。资源浪费严重。
迁移后K8s的requests和limits机制让资源可以超分。两个节点8核16G,实际跑4个Pod总共request 2核4G,但高峰期可以burst到8核16G。低峰期资源释放出来给其他任务。
费用对比
迁移前:
1台物理服务器托管:约3000元/月
MySQL在同一台上(不额外收费)
迁移后:
K8s集群(3台较小的服务器):约4500元/月
MySQL独立服务器:约2000元/月
合计:约6500元/月
月费从3000涨到了6500。多花了一倍多。
但换来的是:自动扩缩容、滚动更新、秒级故障恢复、支持后续微服务拆分。对一个拿了融资要快速发展的团队来说,这个投入是值得的。
如果用户量一直不大、不需要快速迭代,其实不用上K8s。Docker Compose甚至裸机部署都够用。上不上K8s要看业务阶段和团队能力,不是"别人都上了我也要上"。
一个没有走的弯路
客户最初想一步到位:直接上云厂商的托管K8s(ACK/EKS/TKE),把数据库也迁到云RDS。
我拦住了。
原因:
他的服务器刚续了一年托管费。 搬到云上等于浪费了这笔钱。
他对K8s没有经验。 先在自己的裸机集群上踩坑、熟悉K8s的运维,再考虑上云。直接上托管K8s出了问题更难排查——你不知道是K8s的问题还是云平台的问题。
数据库跑了三年很稳定。 迁到云RDS要改连接方式、改备份策略、改运维流程。没有必要为了"全上云"而冒险。
先在自己的机器上跑K8s,等团队熟悉了K8s运维、业务稳定了、托管费到期了,再考虑上云迁移。那时候迁移的成本和风险都小很多。
总结
这个迁移历时5个月,分三个阶段:
第1-2月:拆分+容器化
数据库从应用服务器拆出来
应用和Nginx打包成Docker镜像
解决配置外部化、日志、文件存储等问题
第2-3月:Docker Compose编排
用docker-compose管理容器
理清服务依赖和启动顺序
解决健康检查、环境变量、数据持久化
第3-5月:迁移到K8s
搭建K8s集群
把Docker Compose转成K8s资源
数据库继续裸机,不进K8s
灰度切换,观察稳定后全量迁移
几个核心经验:
不要一步到位。 先容器化再编排再K8s。每一步都能独立验证和回退。一步到位出了问题你不知道是哪层的问题。
数据库不一定进K8s。 有状态服务在K8s里运维复杂度高。数据库跑了几年很稳定的话,没必要为了"架构好看"迁移。
健康检查要认真配。 liveness和readiness的区别搞清楚,initialDelaySeconds给足。配错了Pod反复重启是最常见的新手坑。
资源requests别设太高。 设太高节点很快就满了,新Pod调度不上去。CPU可以超分,requests设低一点没问题。
先在自己的机器上跑。 熟悉了K8s运维再上云。直接上托管K8s出了问题排查更难。
有问题评论区聊。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)