说实话,第一次把一套微服务往Kubernetes(K8s)上搬的时候,我整个人都是懵的。文档读了一堆,教程看了一打,真到自己动手部署,才发现“踩坑”这两个字根本不是夸张修辞,而是真实的生活写照。今天就把我这几个月来在集群部署、微服务拆分、容器化落地过程中遇到的那些坑,一股脑儿倒出来,希望能帮到正在路上跋涉的你。
从单体到微服务:拆还是不拆?这是个问题
在决定拆分之前,我得先问自己一个问题:我的应用真的需要微服务吗?
很多人有个误区,觉得微服务是先进的、高大上的,单体应用是落后的、过时的。其实不是这么回事。微服务是一种架构风格,它带来灵活性和可扩展性的同时,也带来了复杂的运维成本。如果你的应用规模还小,团队也有限,强行拆分可能只会给自己找麻烦。
我当时的判断标准很简单:
- 业务复杂度:如果不同功能模块之间的耦合度低,可以独立演进,那拆分有价值。
- 团队规模:如果多个团队可以并行开发不同服务,拆分能提升效率。
- 扩展需求:如果某些模块负载高、需要独立扩展,拆分是必要的。
- 技术异构:如果不同模块适合用不同的技术栈,拆分能发挥各自优势。
我的应用是一个电商系统,初期是单体架构,后来业务增长,发现订单、用户、商品、库存这几个核心模块耦合严重,独立部署频繁冲突,才决定拆分。
容器化:Dockerfile不是越短越好
容器化的第一步是写Dockerfile。很多教程喜欢教人写“精简版”Dockerfile,用多阶段构建、 Alpine镜像之类的。但我要说,实用比精简更重要。
我的教训是:一开始我用的是Alpine Linux作为基础镜像,因为体积小。结果部署到K8s后,发现不少依赖库在Alpine上编译有问题,glibc版本不兼容,调试花了我整整两天。后来换回Ubuntu基础镜像,虽然镜像大了几个G,但稳定多了。
一个实用的Dockerfile应该考虑:
- 可读性:别人能看懂你在做什么
- 可维护性:升级依赖时容易修改
- 可重现性:每次构建结果一致
- 安全性:及时更新基础镜像和依赖
这是我的一个典型Dockerfile:
FROM ubuntu:22.04
# 设置非交互模式,避免安装时的提示
ENV DEBIAN_FRONTEND=noninteractive
# 安装基础依赖
RUN apt-get update && apt-get install -y \
curl \
wget \
git \
python3 \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
# 设置工作目录
WORKDIR /app
# 复制依赖文件
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt
# 复制应用代码
COPY . .
# 暴露端口
EXPOSE 8080
# 启动命令
CMD ["python3", "app.py"]
注意几点:
- 不要合并过多的RUN命令:虽然每多一个RUN层会增加镜像体积,但 readability 更重要。出问题的时候,你知道是哪一步出错。
- 使用.noninteractive:避免apt-get安装时卡在交互式提示上。
- 明确版本:基础镜像用具体版本号,不要用latest,保证可重现性。
- 只复制必要的文件:先复制依赖文件,安装依赖,再复制代码。这样代码修改不会invalidate依赖缓存。
K8s集群部署:YAML文件不是复制粘贴就完事
写好Dockerfile、构建镜像、推到镜像仓库后,就到了K8s部署环节。这时候你会发现,K8s的YAML文件比Dockerfile复杂得多。
Deployment:不仅仅是 replicas
Deployment是K8s里最常用的资源对象,用来管理Pod的部署。很多人一开始写Deployment YAML时,只关注replicas数量,觉得这就是全部。实际上,Deployment的配置项非常多,每个都可能踩坑。
我的第一个Deployment YAML长这样:
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: myregistry/order-service:1.0.0
ports:
- containerPort: 8080
看起来没问题,对吧?但部署后发现,Pod启动后很快又重启,状态一直是CrashLoopBackOff。排查了很久,才发现是健康检查没配。K8s不知道你的应用是否真正健康,可能应用还没启动完,它就认为服务挂了,然后把Pod杀掉重启。
加上健康检查后:
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: myregistry/order-service:1.0.0
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 3
这里有两个关键概念:
- livenessProbe:探测容器是否还在运行。如果失败,K8s会重启容器。
- readinessProbe:探测容器是否准备好接收流量。如果失败,K8s会把它从Service的Endpoint列表中移除。
这两个_probe的区别很重要。livenessProbe关注的是“进程是否还活着”,readinessProbe关注的是“应用是否准备好服务”。你的应用启动可能需要30秒,但livenessProbe如果在10秒后就探测,可能会误杀正在启动的容器。
Service:ClusterIP、NodePort、LoadBalancer
Service是K8s里用来暴露应用的方式。我的应用内部各服务之间用ClusterIP通信,对外暴露用LoadBalancer。
但这里有个坑:Cloud Provider的差异。我在AWS上用EKS,Service类型LoadBalancer会自动创建一个ELB。但在本地Minikube集群上,LoadBalancer类型会一直卡在Pending状态,因为Minikube没有真实的云提供商。
解决办法是用NodePort类型,然后通过Ingress或端口转发来访问。生产环境再换成LoadBalancer。
ConfigMap和Secret:不要把敏感信息硬编码
我的应用需要数据库连接字符串、API密钥等配置。一开始我把这些都硬编码在环境变量里,部署后发现密码泄露风险太大。
改用ConfigMap和Secret后:
# ConfigMap:存放非敏感配置
apiVersion: v1
kind: ConfigMap
metadata:
name: order-service-config
data:
DATABASE_HOST: "mysql-service"
DATABASE_PORT: "3306"
LOG_LEVEL: "info"
---
# Secret:存放敏感信息
apiVersion: v1
kind: Secret
metadata:
name: order-service-secret
type: Opaque
data:
DATABASE_USER: YWRtaW4= # base64编码的"admin"
DATABASE_PASSWORD: c2VjdXJlcGFzcw== # base64编码的"securepass"
然后在Deployment里引用:
envFrom:
- configMapRef:
name: order-service-config
- secretRef:
name: order-service-secret
注意:Secret在K8s里默认是明文存储在etcd里的,只是base64编码,不是加密。如果需要真正的加密,要考虑使用如Sealed Secrets、External Secrets Operator等方案。
微服务拆分后的通信问题
服务拆分后,服务之间怎么通信?这是个大问题。
REST vs gRPC
我的应用内部服务间调用,一开始用REST(HTTP+JSON),后来发现性能不够,尤其是服务间调用频繁的场景。换成gRPC后,性能提升了不止一个量级。
gRPC的优势:
- 性能高:二进制协议(Protocol Buffers),比JSON序列化快得多
- 类型安全:IDL定义接口,编译时检查
- 多语言支持:官方支持多种语言
- 流式通信:支持服务端流、客户端流、双向流
但gRPC也有缺点:
- 调试麻烦:不能像REST那样直接用浏览器或curl调试,需要专用工具
- 兼容性要求高:IDL变更需要 careful management
- 浏览器不支持:前端不能直接调用gRPC服务
在我的场景里,内部服务间用gRPC,对外暴露用REST,是个不错的折中方案。
服务发现与负载均衡
K8s的Service对象本身就提供了服务发现和负载均衡。服务A调用服务B时,只需要知道服务B的Service Name(通常是<service-name>.<namespace>.svc.cluster.local),K8s会自动把请求负载均衡到服务B的各个Pod上。
但这里有个坑:DNS解析延迟。K8s内部用CoreDNS做DNS解析,如果CoreDNS负载高或网络延迟大,DNS解析可能很慢,影响服务间调用。
我的解决方案:
- 监控CoreDNS的健康状态和性能
- 适当增加CoreDNS的副本数
- 考虑使用服务端负载均衡(如Envoy Sidecar),减少DNS解析频率
链路追踪
微服务拆分后,一个请求可能经过多个服务。当出现问题时,怎么追踪这个请求的完整链路?
我引入了Jaeger作为链路追踪系统。在每个服务里集成Jaeger的SDK,自动注入trace ID,这样就能在一个统一的界面里看到请求的完整链路。
但集成Jaeger也有坑:性能开销。链路追踪会有一定的性能开销,尤其是采样率设置不当的时候。我一开始采样率设为100%,发现对数据库和API响应时间都有明显影响。后来调整为采样率10%,只在出现问题时提高采样率,平衡了可观测性和性能。
存储问题:Stateless服务 vs Stateful服务
K8s设计理念是Stateless,但现实业务中很多服务是有状态的,比如数据库、缓存、消息队列。
有状态服务的挑战
我的订单服务依赖MySQL数据库。K8s里部署MySQL,有几个关键问题:
- 数据持久化:Pod可能被调度到不同节点,数据怎么保证不丢失?
- 网络稳定性:MySQL连接需要稳定,Pod重启后IP变化怎么办?
- 主从复制:如果需要高可用,主从复制怎么管理?
解决方案:StatefulSet + PVC
对于有状态服务,K8s提供了StatefulSet资源类型。StatefulSet保证Pod的名称是有序的、唯一的,并且Pod重启后网络标识不变。
但即使这样,直接在生产环境用K8s原生的StatefulSet管理MySQL还是有些复杂。我最终选择了云服务商提供的托管数据库服务,比如AWS的RDS、Google的Cloud SQL。这些服务提供了高可用、自动备份、自动扩展等功能,比自己管理靠谱得多。
对于缓存服务(如Redis),我用了Redis Operator,它自动化了Redis集群的部署和管理。
StorageClass和PVC
如果用原生K8s存储,需要理解StorageClass和PersistentVolumeClaim(PVC)的概念。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: "standard"
resources:
requests:
storage: 10Gi
注意accessModes:
- ReadWriteOnce:只能被一个Pod读写,适合数据库
- ReadOnlyMany:多个Pod只读,适合共享配置
- ReadWriteMany:多个Pod读写,适合共享文件系统
storageClassName:不同云提供商有不同的StorageClass,需要提前确认。
网络策略:安全不是可有可无
很多教程在讲K8s部署时,会跳过Network Policy。但我认为,网络策略是生产环境必须考虑的。
没有Network Policy,K8s集群里的所有Pod默认可以互相通信。这意味着,如果攻击者进入你的集群,他可以随意访问任何服务。
我的做法:
- 为每个命名空间定义默认拒绝所有入站流量的Network Policy
- 按需开放特定服务间的通信
- 对外暴露的服务用Ingress控制
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: order-service
spec:
podSelector: {}
policyTypes:
- Ingress
这个Policy的意思是:在order-service命名空间里,默认拒绝所有入站流量。然后,再按需开放:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-order-to-user
namespace: order-service
spec:
podSelector:
matchLabels:
app: order-service
ingress:
- from:
- podSelector:
matchLabels:
app: user-service
ports:
- port: 8080
这样,只有user-service的Pod可以访问order-service的8080端口。
CI/CD:自动化部署不是可选项
微服务和K8s的复杂度,决定了手工部署是不现实的。必须建立CI/CD流水线,实现自动化构建、测试、部署。
我用的工具链:
- 代码托管:GitLab
- CI/CD:GitLab CI
- 镜像构建:GitLab Container Registry
- K8s部署:Helm
GitLab CI配置
stages:
- build
- test
- deploy
build:
stage: build
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
test:
stage: test
script:
- docker run --rm $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA npm test
deploy:
stage: deploy
script:
- helm upgrade --install order-service ./charts/order-service \
--set image.tag=$CI_COMMIT_SHA \
--namespace order-service
only:
- main
这个配置很简单,但有几个关键点:
- 镜像标签用Commit SHA:保证可追溯,出问题可以回退到特定版本
- 测试阶段:每个服务都要有自己的测试,CI/CD不仅是部署,也是质量保障
- 部署条件:只在main分支触发部署,避免频繁发布
Helm:K8s应用的包管理器
Helm是K8s的包管理器,用来管理YAML模板。我的每个服务都有一个Helm Chart,包含Deployment、Service、ConfigMap、Secret等所有资源。
使用Helm的好处:
- 模板化:不同环境(dev、staging、prod)可以用不同的values文件
- 版本管理:Helm Release有版本,方便回退
- 依赖管理:Chart可以依赖其他Chart
我的Chart结构:
charts/
order-service/
Chart.yaml
values.yaml
values-dev.yaml
values-staging.yaml
values-prod.yaml
templates/
deployment.yaml
service.yaml
configmap.yaml
secret.yaml
networkpolicy.yaml
hpa.yaml
不同环境的差异通过values文件覆盖:
# values-prod.yaml
replicaCount: 5
image:
tag: "1.0.0"
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
autoscaling:
enabled: true
minReplicas: 5
maxReplicas: 20
监控与日志:可观测性三角
部署完不是结束,而是开始。没有监控和日志,生产环境就是黑盒。
监控:Prometheus + Grafana
我用了Prometheus做指标采集,Grafana做可视化。关键指标包括:
- 应用指标:QPS、响应时间、错误率、成功率
- 资源指标:CPU、内存、磁盘、网络
- 业务指标:订单量、用户数、转化率
Prometheus的难点在于指标定义和告警规则。指标定义太多会占用大量存储,太少又会遗漏关键信息。告警规则要合理,太多告警会导致“告警疲劳”,太少又会漏掉重要问题。
我的经验:
- 指标分层:基础设施层、应用层、业务层
- 告警分级:P0(立即处理)、P1(尽快处理)、P2(计划处理)
- 告警收敛:相关告警合并,避免同一问题触发多个告警
日志:EFK栈
日志用Elasticsearch + Fluentd + Kibana(EFK)栈。Fluentd采集各Pod的日志,Elasticsearch存储,Kibana查询和可视化。
日志采集有几个坑:
- 日志格式:统一用JSON格式,方便解析
- 日志轮转:容器内日志文件要限制大小,避免占满磁盘
- 日志级别:生产环境用INFO级别,DEBUG级别按需开启
”`json { “timestamp”: “2024-01-01T12:00:00Z”, “level”: “INFO”, “service”: “order-service”, “trace_id”: “abc123”, “span_id”: “def456”, “message”: “Order created”, “user_id”: “12345”, “order_id”: “ORD-202
