中小企业上云踩坑实录:从Docker单机到K8s集群架构转型避坑指南
前阵子,我帮一家做电商的小公司重构他们的系统架构。他们的技术负责人老张,是个典型的实干派工程师,技术还行,但之前在云上的经验基本为零。他们一开始用Docker单机跑服务,觉得挺爽,容器化嘛,迁移方便,环境一致。结果业务一跑起来,订单量上来,各种问题就冒出来了——服务挂了重启要半小时,扩容全靠手,监控靠肉眼,日志靠grep。最后实在扛不住,决定上K8s。
这一上,坑就没停过。今天我就把这套”血泪史”整理出来,希望能帮到正在考虑或已经在路上的你。
一、先说说他们踩的第一个坑:以为Docker单机能解决一切
老张他们一开始的做法,就是每个服务一个Docker容器,用docker-compose管理。服务不多(大概8个核心服务),单机性能也还行,看着挺稳。
但问题出在以下这些地方:
服务挂了,重启全靠手动
有一次双11大促,订单服务因为内存溢出崩了,运维同学发现的时候已经过了二十分钟。这二十分钟里,用户下单失败,客服被打爆。他们想要自动重启,就写了个监控脚本,每隔三十秒检查进程状态。但问题是,进程可能挂得比较频繁,手动写的脚本根本不够用,还经常漏报。
扩容是个噩梦
业务高峰期,用户访问量暴增,单机性能跟不上。他们想水平扩展,加机器。问题来了:Docker单机模式下,每台机器都要单独部署、单独配置,镜像分发、配置同步、服务发现,全靠自己搞。有一次,他们买了三台新服务器,部署了整整一天,而且部署完还发现配置有差异,服务不稳定。
日志管理一团糟
每个容器的日志都输出到本机,想查某个请求的链路日志,得在这台机器上grep,再在那台机器上grep,有时候日志还被轮转覆盖掉了。监控全靠人肉排查,效率极低。
备份和迁移也麻烦
有一次他们想把业务从AWS迁移到阿里云,结果是每个容器的数据卷都要手动备份,迁移完还要逐个检查服务是否正常。整个过程折腾了两天,还差点出事故。
这些问题,本质上都是因为单机模式缺乏统一的调度、编排和管理能力。服务多了之后,手动运维的成本呈指数级上升。
二、决定上K8s,但第一个大坑就来了:选型和规划
老张看到上面那些问题,决定上K8s。他们调研了一圈,发现K8s的托管方案有这些选择:
- AWS EKS:AWS原生,和AWS生态集成好,但价格贵
- 阿里云ACK:国内首选,性价比高,生态完善
- 腾讯云TKE:也不错,但老张他们业务主要在华东,阿里云资源更近
- 自建K8s集群:免费,但要自己维护,成本其实不低
他们最终选了阿里云ACK,因为业务在国内,延迟低,而且阿里云的文档和社区都比较完善。
但这里有个坑:他们低估了K8s的学习曲线。
老张之前只接触过Docker,对K8s的概念一知半解。他们一开始想自己搭建一套,但折腾了两周,光是搞定网络插件(他们选了Calico)、存储插件(选了NFS),就花了大部分时间。后来干脆直接上了托管版ACK,省事很多,但后续配置的问题也接踵而至。
三、网络配置:这里坑最多
K8s的网络模型和传统Docker网络差别很大。在Docker单机模式下,容器之间可以通过docker-compose里的服务名直接访问。但在K8s里,网络模型完全不同:
- 每个Pod有独立的IP
- Service提供稳定的访问入口
- Ingress处理外部流量
他们一开始完全没理解这些概念,直接把Docker Compose的配置搬到K8s里,结果服务之间完全访问不通。
举个例子,他们的用户服务调用订单服务:
# Docker Compose 配置
services:
user-service:
image: user-service:1.0
networks:
- backend
order-service:
image: order-service:1.0
networks:
- backend
networks:
backend:
driver: bridge
在Docker Compose里,user-service可以通过order-service:8080直接访问订单服务。但在K8s里,这个配置完全失效。
正确的K8s配置应该是:
# Order Service 的 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: order-service:1.0
ports:
- containerPort: 8080
---
# Order Service 的 Service
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: ClusterIP
---
# User Service 的 Deployment(调用方)
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 2
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: user-service:1.0
env:
- name: ORDER_SERVICE_URL
value: "http://order-service:80" # 通过Service名访问
关键点:
- Service是稳定的访问入口。Pod的IP可能会变,但Service的DNS名不会变。
- 通过Service名访问。在User Service里,用
http://order-service:80来访问订单服务,而不是直接访问某个Pod的IP。 - 注意网络插件的选择。他们选了Calico,这个插件性能不错,但配置复杂。如果是小型集群,可以直接用阿里云默认的Terway,更简单。
还有一个常见的问题:跨节点通信。在K8s里,不同节点上的Pod要能互相通信,这依赖于网络插件。如果网络插件配置不对,就会出现”同一个集群里的服务互相访问不了”的问题。
四、存储问题:数据持久化千万别忽略
K8s的存储模型也比较复杂。默认情况下,Pod里的数据是临时的,Pod被删除后数据就没了。但对于数据库、文件存储等场景,需要持久化存储。
他们一开始没有配置持久化存储,直接把MySQL部署在Pod里。结果有一次,Pod因为资源不足被重启,数据全没了。那次事故后,他们赶紧改用云上的RDS(阿里云的MySQL托管服务),不再自己管理数据库。
对于文件存储,他们用了阿里云的NAS(网络附加存储),配置方式如下:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-storage
spec:
accessModes:
- ReadWriteMany
storageClassName: "nas"
resources:
requests:
storage: 100Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: file-service
spec:
replicas: 2
selector:
matchLabels:
app: file-service
template:
metadata:
labels:
app: file-service
spec:
containers:
- name: file-service
image: file-service:1.0
volumeMounts:
- name: storage
mountPath: /data
volumes:
- name: storage
persistentVolumeClaim:
claimName: app-storage
要点:
- 优先使用云厂商的托管服务。数据库用RDS,缓存用Redis,别自己部署在K8s里。
- 文件存储用NAS或OSS。NAS适合需要多Pod共享读写的场景,OSS适合大文件存储。
- 注意StorageClass的配置。不同的StorageClass对应不同的存储类型和性能,要根据业务需求选择。
五、配置管理:ConfigMap和Secret要用对
他们一开始把所有的配置(包括数据库密码、API Key等敏感信息)都硬编码在镜像里。这有个大问题:每次改配置都要重新构建镜像、重新部署,而且敏感信息容易泄露。
正确的做法是使用K8s的ConfigMap和Secret:
# ConfigMap:存放非敏感配置
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
DATABASE_HOST: "rm-xxx.mysql.rds.aliyuncs.com"
DATABASE_PORT: "3306"
REDIS_HOST: "r-xxx.redis.rds.aliyuncs.com"
REDIS_PORT: "6379"
LOG_LEVEL: "info"
---
# Secret:存放敏感信息
apiVersion: v1
kind: Secret
metadata:
name: app-secret
type: Opaque
data:
DATABASE_PASSWORD: "base64编码的密码"
API_KEY: "base64编码的key"
---
# 在Deployment中引用
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 2
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: user-service:1.0
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: DATABASE_HOST
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secret
key: DATABASE_PASSWORD
这样做的的好处:
- 配置和镜像分离。改配置不需要重新构建镜像。
- 敏感信息加密存储。Secret在etcd里是加密的。
- 统一管理。所有配置都在K8s里,方便审计和版本管理。
六、自动扩缩容:HPA和手动扩容的区别
他们一开始用的是手动扩容——业务高峰期,运维手动加Pod数量。这种方式问题很多:反应慢,容易遗漏,而且加完可能资源又过剩了。
后来他们配置了HPA(Horizontal Pod Autoscaler),让K8s根据CPU使用率自动扩缩容:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
这个配置的含义是:
- 订单服务的副本数最少2个,最多10个
- 当CPU使用率超过70%时,自动增加副本数
- 当CPU使用率下降时,自动减少副本数
配置完HPA后,他们发现扩缩容还是很慢。原因是Pod启动需要时间,从创建到真正能提供服务,可能要几十秒。为了解决这个问题,他们做了两件事:
- 优化镜像大小。把镜像从2GB优化到500MB,启动速度快了很多。
- 预启动预热。在业务高峰期前,手动增加Pod数量,避免HPA来不及响应。
七、监控和日志:别等出了问题才想起来
他们一开始没有配置监控,全靠用户反馈才知道服务有问题。后来装了Prometheus + Grafana做监控,Loki做日志收集。
监控配置示例:
# Prometheus 的 Deployment(简化版)
apiVersion: apps/v1
kind: Deployment
metadata:
name: prometheus
spec:
replicas: 1
selector:
matchLabels:
app: prometheus
template:
metadata:
labels:
app: prometheus
spec:
containers:
- name: prometheus
image: prom/prometheus:v2.45.0
ports:
- containerPort: 9090
volumeMounts:
- name: config
mountPath: /etc/prometheus
- name: data
mountPath: /prometheus
volumes:
- name: config
configMap:
name: prometheus-config
- name: data
persistentVolumeClaim:
claimName: prometheus-data
---
# Prometheus 的 ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-config
data:
prometheus.yml: |
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
关键配置说明:
scrape_interval: 15s:每15秒采集一次指标kubernetes_sd_configs:自动发现K8s里的Pod__meta_kubernetes_pod_annotation_prometheus_io_scrape:通过Pod的注解控制是否被采集
日志方面,他们用了EFK栈(Elasticsearch + Fluentd + Kibana):
# Fluentd 的 DaemonSet(每个节点运行一个Pod)
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd
spec:
selector:
matchLabels:
app: fluentd
template:
metadata:
labels:
app: fluentd
spec:
containers:
- name: fluentd
image: fluentd/elasticsearch:latest
env:
- name: FLUENT_ELASTICSEARCH_HOST
value: "elasticsearch.default.svc.cluster.local"
- name: FLUENT_ELASTICSEARCH_PORT
value: "9200"
DaemonSet的特点是每个节点运行一个Pod,适合日志收集这种需要采集本机日志的场景。
八、安全方面:这些坑千万别踩
安全是上云后最容易被忽视的问题。他们一开始几乎没有做安全配置,结果被扫描到漏洞,差点被阿里云冻结账号。
主要的安全问题:
- 容器以root用户运行。很多镜像默认以root运行,这是很不安全的。应该在Dockerfile里指定非root用户:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
# 创建非root用户并切换
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
CMD ["node", "server.js"]
- 网络策略缺失。K8s默认允许所有Pod之间互相访问,这很危险。应该配置NetworkPolicy限制访问:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: order-service-policy
spec:
podSelector:
matchLabels:
app: order-service
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: user-service
ports:
- port: 8080
这个策略的意思是:只允许标记为app: user-service的Pod访问order-service的8080端口。
- RBAC权限过大。他们一开始给ServiceAccount赋予了cluster-admin权限,这是非常危险的。应该遵循最小权限原则:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: order-service-role
namespace: default
rules:
- apiGroups: [""]
resources: ["pods", "services"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: order-service-binding
namespace: default
subjects:
- kind: ServiceAccount
name: order-service-sa
namespace: default
roleRef:
kind: Role
name: order-service-role
apiGroup: rbac.authorization.k8s.io
- 镜像安全扫描。应该定期对镜像进行安全扫描,可以使用Trivy等工具:
# 使用Trivy扫描镜像
trivy image my-registry/order-service:1.0
# 输出扫描结果,发现漏洞后修复
九、CI/CD流程:自动化部署是关键
他们一开始是手动部署,每次发布都要 SSH 到服务器上,拉代码,构建镜像,推送,然后手动更新K8s配置。这种方式效率低,还容易出错。
后来他们搭建了基于GitLab CI的自动化部署流程:
# .gitlab-ci.yml
stages:
- build
- test
- deploy
variables:
DOCKER_REGISTRY: "registry.cn-hangzhou.aliyuncs.com/myproject"
KUBE_CONFIG: "/path/to/kubeconfig"
build:
stage: build
script:
- docker build -t $DOCKER_REGISTRY/order-service:$CI_COMMIT_SHA .
- docker push $DOCKER_REGISTRY/order-service:$CI_COMMIT_SHA
- docker tag $DOCKER_REGISTRY/order-service:$CI_COMMIT_SHA $DOCKER_REGISTRY/order-service:latest
- docker push $DOCKER_REGISTRY/order-service:latest
test:
stage: test
script:
- docker run --rm $DOCKER_REGISTRY/order-service:$CI_COMMIT_SHA npm test
deploy:
stage: deploy
script:
- kubectl set image deployment/order-service order-service=$DOCKER_REGISTRY/order-service:$CI_COMMIT_SHA
- kubectl rollout status deployment/order-service
only:
- main
这个CI/CD流程的特点:
- 每次提交代码都自动构建和测试。
- 推送到main分支才自动部署。
- 使用具体commit SHA作为版本号,方便回滚。
部署完成后,可以通过以下命令查看状态:
# 查看Deployment状态
kubectl get deployment order-service
# 查看Pod状态
kubectl get pods -l app=order-service
# 查看部署历史
kubectl rollout history deployment/order-service
# 回滚到上一个版本
kubectl rollout undo deployment/order-service
十、成本优化:上云不是越贵越好
上云后,成本是个大问题。他们一开始没有注意成本控制,结果每个月账单出来吓一跳。
主要成本优化策略:
使用预留实例。对于长期运行的服务,使用预留实例可以节省40-60%的成本。
合理配置资源请求和限制。在Deployment里配置requests和limits:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: order-service:1.0
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
requests:调度时至少分配这么多资源limits:最多使用这么多资源
使用集群自动伸缩。配置Cluster Autoscaler,当集群资源不足时自动添加节点,资源空闲时自动删除节点。
定期清理无用资源。比如不再使用的PVC、镜像等。
十一、一些实用的经验总结
经过这段时间的折腾,我总结了几个关键点:
1. 从小处着手,不要一次性迁移所有服务
建议先迁移一个不核心的服务,积累经验后再逐步迁移其他服务。老张他们一开始就想把所有服务都迁移到K8s,结果问题层出不穷。
2. 善用云厂商的托管服务
数据库用RDS,缓存用Redis,消息队列用RocketMQ,别自己部署在K8s里。云厂商的托管服务稳定可靠,还能节省运维成本。
3. 重视监控和日志
没有监控就像盲人摸象。一定要在上线前配置好监控和日志收集,出现问题能快速定位。
4. 做好灾备和回滚预案
每次发布前,确保有回滚方案。K8s的rollout功能很方便,可以随时回滚到上一个版本。
5. 持续学习和改进
K8s的生态在快速发展,新版本特性很多。要持续关注官方文档和社区动态,学习最佳实践。
十二、最后的话
从Docker单机到K8s集群,这条路走得很艰难,但也学到了很多。老张他们现在系统稳定多了,扩容、监控、部署都自动化了,团队效率提升了不少。
如果你也在考虑上云或者已经上路,希望这篇实录能帮到你。记住,每个坑都是经验,每个问题都是成长的机会。有什么问题,欢迎交流!
