1. 项目背景与整体架构思考
1.1 为什么把微服务搬上容器与Kubernetes
先说背景。我之前在维护一套非常典型的微服务系统:用户、订单、支付、库存、消息通知,一共十几个服务,每个服务都有自己的配置、日志、依赖环境和启动方式。这套系统最早是直接跑在虚拟机上的,部署一次全靠运维手工敲命令,某个服务崩了要等监控报警之后人肉去重启,新版本上线基本要挑凌晨低峰期通宵操作。
后来我接手做容器化改造,核心目标就三个:让部署变成可重复的、让资源利用率提上去、让扩容缩容从小时级变成分钟级。Docker负责把应用和它的运行环境打包成标准镜像,解决“在我机器上明明是好的”这类环境问题;Kubernetes(以下简称K8s)负责调度这些容器,解决“服务之间怎么互相发现、怎么管理流量、挂了怎么恢复、流量大了怎么扩”这些问题。两者配合,微服务才真正有了“云原生”的味道,或者说,才具备了随时随地、以统一方式把服务跑起来的能力。
这套组合解决的最直接痛点,是环境一致性和交付效率。Docker镜像把代码、运行时、依赖、配置合并成一个不可变工件,要么这个镜像在所有环境都能跑,要么在哪都跑不起来。K8s则把多台服务器抽象成一个资源池,应用不需要关心自己跑在哪台机器上,也不需要手动维护服务注册信息,集群内部的DNS和服务发现机制会把这一切处理好。
1.2 改造前的现状诊断
正式动手之前,先花了大概一周时间梳理现状。这是很多人容易跳过的步骤,但恰恰是最值得投入的一步。我当时整理出这样几个关键问题:
- 服务依赖关系不清晰,有些服务之间互相调用,但部署顺序完全靠运维脑子记。
- 配置散落在各个启动脚本里,改一个数据库地址要改十几个地方,漏改一个就是线上事故。
- 日志没有统一采集,出了问题要在每台机器上翻文件。
- 健康检查基本没有,服务假死但端口还开着的情况很常见。
这些问题的本质,就是“基础设施没有产品化”。把服务跑起来这件事,依赖的是经验而不是代码,这正好是容器化最擅长解决的领域。
改造的思路不是把现有架构推翻重来,而是遵循渐进式策略。先选两三个独立性强、对上下游影响小的服务做试点,跑通全流程之后再逐步辐射到全部服务。这样既控制了风险,也能在早期把坑都踩一遍,为后续大规模迁移积累经验。
1.3 技术选型的关键决策
技术选型这块,当时有过几轮讨论。容器运行时没有悬念,Docker 是事实标准,生态成熟、文档丰富,团队里大家多多少少都接触过。集群管理这块讨论过是直接上云托管的K8s服务,还是自建集群。托管方案省心,但考虑到项目环境需要可控性,最终选择了自建K8s集群的方案。网络插件选了Calico,因为它支持NetworkPolicy,可以在后期做精细的访问控制。Ingress控制器用了Nginx Ingress,主要是社区活跃、规则配置直观、排障资料多。
这里有个经验:选型标准不必追求“最先进”,而应该追求“团队最容易维护”。一个团队都能看懂的Nginx Ingress,比一个功能更强但没人敢动的Service Mesh方案,在实际生产中要可靠得多。
2. 容器化的核心细节与实操要点
2.1 从Dockerfile到镜像的规范化
容器化改造的第一件事,是把每个服务写成一份规范的Dockerfile。这不是把Dockerfile写出来能构建就完事了,里面有很多影响镜像质量、构建速度、运行安全的关键细节。
Dockerfile的构建阶段一定要分层,把不变的依赖层放在前面,把经常变的代码层放在后面。这样代码每次更新,只需要重新构建最上层的层,利用Docker的层缓存能大幅加快构建速度。举个例子,一个典型的多阶段构建长这样:
# 阶段一:编译 FROM golang:1.20 AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /app/server . # 阶段二:运行时镜像 FROM alpine:3.18 RUN apk add --no-cache ca-certificates tzdata WORKDIR /app COPY --from=builder /app/server . USER nobody EXPOSE 8080 ENTRYPOINT ["./server"]这个写法有个很实际的收益:第一阶段的依赖下载、编译,第二阶段的包管理、系统配置,都在各自的层里。修改业务代码时,COPY . .之前的层全部走缓存,秒级完成。而最终运行镜像只有一层alpine加上一个二进制文件,体积不到20MB,比直接把整个golang编译器塞进运行环境要干净得多。
多阶段构建的第二个好处是安全。构建阶段依赖的编译工具链、包管理器、源码文件全部留在builder层,不会进入最终镜像。最终镜像做到最小化,攻击面自然就小了。运行用户设置成nobody,避免容器内以root身份运行,这个过程虽然只是在Dockerfile里加一行USER nobody,但其意义在于把容器的权限边界收窄,防止容器逃逸时权限过大。
2.2 镜像仓库管理与版本规范
镜像构建好之后,要推送到镜像仓库统一管理。私有镜像仓库的选择很多,我当时的选型标准很简单:存储后端可配置、支持访问控制、部署简单。最终选用的是一个轻量级的开源镜像仓库,后端存储直接对接了对象存储,这样镜像数据不会因为节点故障丢失。
镜像命名规范一定要提早定下来,后面你会知道这有多重要。一套简单有效的命名规则可以是:
- 仓库名:
registry.example.com/team-name/service-name - 标签:
<git提交短哈希>-<构建序号>,比如a3f2c1-87
用Git提交哈希作为标签,好处是能精确追溯“这个镜像对应哪个代码版本”。大部分实际生产事故的排查,最终都会回到“线上跑的到底是哪一版代码”这个问题上。如果标签是latest、v2、final这种,迟早会出问题。
还要注意镜像的清理策略。镜像仓库跑半年,不清理的话几百GB就没了。定个保留规则,比如每个服务保留最近30个镜像,或者保留最近90天内的镜像,配合定期的垃圾回收任务,仓库体积能控制得很好。
2.3 配置与敏感信息管理
容器化之后,配置管理就成了一个大话题。配置直接写进Dockerfile或者打进镜像,是新手最容易犯的错误,它带来的直接后果是:每次配置变更都要重新构建镜像、重新发布,完全失去了配置灵活调整的意义。而且数据库密码、API密钥这类敏感信息进了镜像,镜像一旦被拉走,等于所有账号密码一起打包带走。
我当时采用的分层方案是:
- 非敏感配置:用K8s的ConfigMap管理,比如日志级别、开关项、端口号。这些配置以文件或环境变量的方式挂载进容器,改完配置只需滚动重启服务,不用重新构建镜像。
- 敏感配置:存到K8s的Secret里,比如数据库密码、第三方密钥、证书。Secret可以配合加密存储,也能限制集群内的访问权限。
重点说一下配置与代码的解耦。服务启动时读取配置的方式要注意,如果服务依赖环境变量,那就要把所有配置项统一映射到环境变量上。之前维护的代码里,服务同时读配置文件、环境变量、甚至启动参数,三种方式并存,排查“为什么这个配置没生效”简直是一场噩梦。容器化的过程中,我顺手统一了这个逻辑:所有配置优先读环境变量,环境变量由K8s的ConfigMap和Secret注入,这样整个配置链条非常清晰。
3. Kubernetes集群搭建与网络方案选型
3.1 集群架构与节点角色规划
集群搭建这个环节,会非常直接地影响后面所有部署工作的顺利程度。别急着上一个多节点的生产集群,先把单节点环境玩熟了再扩大规模。
生产环境建议用三节点起步,一个master加两个worker。为什么至少三节点?因为高可用需要冗余,master挂了要有备份顶上。我见过有人一台虚拟机既跑master又跑worker,美其名曰“节省资源”,实际上这台机器一挂,整个集群一起陪葬。特别是etcd这种关键组件,作为集群状态存储,对网络延迟和磁盘IO都很敏感,单独规划才能保证集群控制面的稳定。
节点的操作系统建议用轻量级Linux发行版,关闭swap,这样可以保证Kubelet的内存限制能精确生效。内核参数方面,需要调整net.bridge.bridge-nf-call-iptables等参数,让K8s的Service流量规则能正确作用于包过滤,这些在安装文档里都有详细说明,关键是不要用默认内核参数直接跑。
3.2 网络方案对比与选型
K8s的网络模型要求每个Pod都能获得一个集群内唯一的IP地址,这个IP跟宿主机网络是不重叠的,Pod之间可以直接通过IP互相访问,不需要NAT。为了实现这个模型,必须安装一个CNI网络插件。
当时对比了Flannel、Calico和Cilium。Flannel最轻量,纯Overlay模式,性能有一定损耗。Calico支持BGP路由模式和IPIP模式,性能更好,并且原生支持NetworkPolicy,可以在网络层做访问控制。Cilium基于eBPF,性能最优秀,功能也最丰富,但引入的抽象层级更多,排查问题时的知识门槛更高。
最终选择Calico。原因很简单:团队对BGP和路由的概念更熟悉,Calico配置NetworkPolicy的方式比较直观,文档也够齐全,出了问题能在社区找到大量类似案例。
网络规划有个关键细节:Pod网段和Service网段要提前规划好,并且避免和物理网络的网段冲突。有人贪方便,Pod网段随意设了一个跟办公网冲突的地址段,第二天业务方说“你们集群的IP把我们打印机全搞废了”,这种事在真实环境中一点都不罕见。
3.3 部署工具的摩擦与经验
集群部署工具这块,不同选择会带来完全不同的体验。生产环境我用的是kubeadm,它把初始化、证书、令牌这些流程自动化了,适合标准化的多节点集群安装。
但在实际操作中,kubeadm只是第一步。装完之后还需要配置一系列基础组件:DNS插件、网络插件、dashboard、metrics-server。每个组件都有各自的版本兼容性要求,踩过最典型的坑是:装了新版本的kubeadm,但网络插件版本太老,导致Pod的网络一直起不来。镜像拉不起来也是新手的高频问题,所以在开始部署之前,先手动把用到的镜像拉一遍,确认网络和镜像仓库的连通性,别装到一半才发现拉不动镜像。
4. 微服务部署实战:从YAML到生产
4.1 工作负载类型的取舍
K8s里描述应用运行状态的核心资源有好几类:Deployment、StatefulSet、DaemonSet、Job。对无状态微服务,Deployment是绝大多数场景的标准解。它内置了滚动更新、副本管理、故障自愈这些能力,一个服务想从两个副本升到五个副本,一行代码的事。
有状态服务,比如数据库、消息队列,需要用到StatefulSet。它有稳定的网络标识、稳定的存储,Pod重建之后身份不改变。但我的建议是:除非你真的很懂这个中间件在K8s上的运维方式,否则不要轻易把有状态服务直接跑到K8s上。当时几个核心数据库、缓存、消息队列,依然部署在虚拟机或独立机器上,应用通过内部DNS去连接它们。这个决策帮我们省掉了无数个深夜排查存储故障的麻烦。K8s擅长的是调度无状态服务,有状态应用跑在K8s上的复杂度,普通团队扛不住。
4.2 一个完整的部署YAML实例
直接上一个生产环境中实际使用的Deployment配置片段:
apiVersion: apps/v1 kind: Deployment metadata: name: order-service namespace: production spec: replicas: 3 selector: matchLabels: app: order-service template: metadata: labels: app: order-service spec: containers: - name: order-service image: registry.example.com/team/order-service:a3f2c1-87 ports: - containerPort: 8080 resources: requests: memory: 512Mi cpu: 250m limits: memory: 1Gi cpu: 500m readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 15 periodSeconds: 20这些配置里不能改错的核心点是:resources.requests决定的是调度器能不能把Pod安排到某个节点上,Pod申请的资源至少要大于等于这个值;resources.limits决定的是容器最多能用多少资源,防止一个服务的异常流量打爆整个节点。两者的差值要留合理余地,完全一致会导致服务无法借助节点上临时可用的空闲资源。
就绪探针和存活探针的标准:一个是“流量能不能进”,一个是“进程要不要重启”。readinessProbe返回失败时,K8s会把该Pod从Service的后端列表中摘除,但Pod不会被杀掉。livenessProbe返回失败则意味着进程已经僵死,Kubelet会帮你杀掉重启。这两个探针的实现要区分开,很多服务只有一个health接口同时承担两种判断,真出问题时,Go程死锁但HTTP端口还通,探针判断一切正常,流量照样往里灌,故障就不好查了。
4.3 Service与Ingress的流量路径
Deployment承载了应用,但应用之间要互相调用,需要Service提供稳定的虚拟IP和DNS名字。在K8s里,A服务访问B服务只需要用http://b-service:8080这样的地址,K8s自带的DNS解析机制会把服务名解析到对应的ClusterIP上。这个机制解决了微服务架构中最头疼的服务发现问题。
从集群外部访问服务,则需要Ingress。Ingress负责把外部的HTTP/HTTPS请求按域名、路径分发到集群内部的不同Service。一个典型的Ingress配置:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: shop-ingress spec: rules: - host: api.example.com http: paths: - path: /order pathType: Prefix backend: service: name: order-service port: number: 8080这里有个容易忽略的细节:如果你的微服务之间有互相调用的内部API,这些API应该走内部Service名称,而不是Ingress。走Ingress绕一大圈到外部代理再回来,既增加了延迟,也把内网接口暴露给了外部,关键是路由规则根本管不住这些内部调用,链路一变直接失败。
4.4 滚动更新与版本回滚策略
上线新版本,多用Deployment的滚动更新机制。K8s默认会用新的Pod逐个替换旧的Pod,保证服务不中断。但有一个参数一定要调整:maxUnavailable和maxSurge。默认值有时候太激进,比如一次更新直接把所有旧Pod杀掉,流量还没切完,服务就整体崩掉。
我习惯把滚动更新配置成:
strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 maxSurge: 1这个配置的含义是:更新过程中,保证旧的Pod至少全部在线,每次最多多创建一个新的Pod来替换。虽然更新速度慢一点,但安全性高得多。尤其是有大量长连接或消息队列消费者的服务,直接把Pod杀完再拉起会导致消息积压和连接重置,设置maxUnavailable: 0能避免这种问题。
回滚操作同样重要。发布之后发现问题,最稳妥的命令是:
kubectl rollout undo deployment/order-serviceK8s会自动把Deployment回退到上一个Revision。如果你的镜像标签用的是git提交哈希,回滚前后能明确看到版本的变化,排查问题时省力很多。
5. 服务治理与弹性伸缩实践
5.1 自动伸缩的三层结构
微服务上了K8s之后,最具吸引力的能力之一就是弹性伸缩。K8s的自动伸缩分三层:HPA控制Pod数量,VPA调整Pod资源大小,cluster-autoscaler调整集群节点数量。
实际生产中最常用的是HPA,它根据CPU使用率、内存使用率或自定义指标来调整Pod副本数。一个标准的HPA配置:
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: 60HPA的决策机制有一定延迟,通常要等指标采集周期和评估周期都走完才会触发扩容,时间在30秒到几分钟不等。对于突发的流量高峰,比如大促秒杀,单纯靠HPA的反应速度不够,你需要提前扩容或者使用其他方式。当时我们的做法是:HPA兜底,关键服务在活动开始前手工预先把副本数打上去,事后降下来。K8s的优雅之处在于,扩容只是改一个replicas字段的事,脚本自动化起来非常方便。
5.2 健康检查与自愈机制
微服务架构下的故障排查有个经典困境:调用链太长,一个服务异常会造成下游连锁的超时和重试风暴。K8s的自愈机制能在一定程度上缓解这个问题:节点挂了由StatefulSet/Deployment在其他节点重新拉起Pod;容器OOM被Kill由Kubelet重启;探针失败的容器被剔除和重启。
但这些机制生效的前提是,探针设计得合理。有几个容易踩的坑:
- 探针的超时时间和periodSeconds不要设置得太短,避免因为一次偶发延迟就频繁重启容器。GC引起的延迟在Java服务里很常见,探针稍微超时一下,容器就被重启了,反而引发更严重的抖动。
- 探针不要依赖外部服务。如果health接口里面去查询数据库,数据库慢了一秒,探针超时,K8s把整个容器杀掉,数据库恢复之后,容器还是起不来,因为每次启动探针都要再查一次数据库。正确的做法是health接口只检查进程内部状态,外部依赖的健康交给业务逻辑去处理。
- 滚动更新期间,readinessProbe要设置足够的
initialDelaySeconds,给服务一个启动和初始化时间。Java服务启动可能要几十秒,你设置5秒就开始探活,那它永远就绪不了。
5.3 资源配额与命名空间规划
把多个微服务部署在同一套集群,一定要提前做好命名空间规划和资源配额。我强烈建议按环境划分命名空间,比如production、staging、development,而不是所有环境混用一套命名空间。命名空间隔离既能让资源配额精确落地,也能避免测试环境误连接生产数据库这类非常常见的低级事故。
每个命名空间设置ResourceQuota和LimitRange,这样一个命名空间内所有Pod的CPU、内存总量是受限的。新服务申请资源时,不会无限制地把集群吃光。当时配合这个策略,定义了每个团队的资源申请规范,创建Pod时必须显式声明resources.requests和resources.limits,没有声明的一律用LimitRange默认值兜底。
6. 日志采集、监控告警与故障排查实录
6.1 日志采集方案
容器化之后,日志的收集方式发生了根本变化。以前在虚拟机上是写到文件里,运维去机器上tail。在K8s里,Pod是随时会被调度的,容器是随时可能被清理的,日志写到容器本地文件里,Pod一删就全没了。
标准方案是让容器把日志写到标准输出,然后由日志采集Agent统一收集。我当时用的是一个轻量级的日志采集器,以DaemonSet的方式跑在所有节点上,收集容器日志并发送到集中式日志平台。这套方案的好处是业务方无需关心日志的存储和流转,日志采集对业务完全透明。
有个细节值得强调:有些中间件默认不是输出到标准输出的,比如Nginx默认写access.log文件。在容器里跑这些服务,要手动把日志重定向到stdout,否则采集Agent收不到。日志规范这块,建议按JSON格式化输出,后面排查问题时可以根据字段快速过滤,比grep一整行日志高效得多。
6.2 监控体系:从指标到告警
监控告警是全链路里不可或缺的部分。K8s集群本身的监控、应用业务的监控、中间件的监控要分层规划。
集群层的监控用了一套以Prometheus为核心的开源方案。Prometheus采集K8s各组件和节点的指标,Grafana负责图表展示,Alertmanager负责告警通知。核心指标包括节点CPU/内存、Pod数量、容器重启次数、API Server延迟等。业务层的监控则依靠在服务里暴露/metrics接口,以Prometheus标准格式输出QPS、错误率、响应延迟等关键指标。如果你的服务是用主流语言开发的,Prometheus客户端库通常是现成的,接入成本不高。
告警规则的设置大有讲究,不要一开始就加一堆规则,那只会导致告警疲劳,真正出问题时告警被淹没在噪音里。我的经验是遵循“金字塔原则”:
- 最底层:基础设施告警,节点宕机、API Server不可用,这属于最高优先级,直接召唤团队。
- 中间层:服务可用性告警,比如某个Deployment就绪副本数低于期望值、错误率连续5分钟超标。
- 最高层:业务的SLO告警,比如订单创建成功率低于99.9%持续10分钟。
每一条告警规则要配明晰的注释和负责人,没有处理预案的告警规则不如不建。
6.3 实战中的典型故障与排查思路
写几个我在实际踩坑中积累的排查思路,这里以真实遇到的情况为例,但用虚拟代号来描述。
故障一:Pod一直处于Pending状态。排查思路是先看事件:kubectl describe pod xxx。最常见的原因是资源不足、节点不可调度、或者有污点没有容忍。有一次发现某个命名空间已经把ResourceQuota用满了,新Pod怎么都调度不上去,事件里明确写着exceeded quota。这种问题看事件是最直接的。
故障二:服务A调用服务B间歇性超时。这种问题往往不是K8s本身的bug,而是网络层面的问题。排查思路:先看Service的Endpoints是否正确,Pod是否都在Ready状态;然后在客户端容器里做DNS解析、直连Pod IP测试;最后抓包看包在网络路径上的行为。之前遇到一个典型案例,是Calico的IPIP模式在某些节点上需要额外开启ipip内核模块,没开启的节点上Pod通信会不定期地丢包,折腾了一整天才定位到。
故障三:滚动更新后部分请求504。典型原因:旧Pod还在处理请求,但更新过程中集群开始移除旧的Pod,Kubelet发SIGTERM给主进程,应用还没处理完当前请求就退出。解法是一方面配置terminationGracePeriodSeconds给进程足够的优雅退出时间,另一方面应用要实现SIGTERM信号处理逻辑,收到信号之后停止接收新请求,处理完存量请求再退出。这个坑几乎每个做容器化改造的团队都会碰到,提前重视这个配置远比事后排查要省心。
6.4 排查工具与日志追溯
最后分享几个我日常依赖的命令和技巧:
kubectl get events --sort-by='.lastTimestamp':看全局事件,适合排查集群层面的异常。kubectl logs -f pod-name -c container-name --previous:查看崩溃容器的上一份日志,排查陷入CrashLoopBackOff的容器时有用。kubectl exec -it pod-name -- sh:进入容器排查运行环境、网络连通性等。kubectl get pods -o wide:查看Pod跑到哪个节点、实际IP是多少。
日志追溯方面,一定要给每个请求加上traceId或者requestId,贯穿从入口到各个微服务调用的全链路。这个看似微小的约定,在你需要从一条报错日志反查整个调用链的时候,价值会最大化。容器和K8s已经帮你把调度、重启、扩容这些事情自动化了,但“定位问题根因”这件事,永远离不开清晰规范的日志和可观测性建设。
7. 安全加固与最佳实践沉淀
7.1 镜像安全与准入控制
安全建设在容器化改造中容易被忽视,但它一旦出错,代价会很高。镜像本身的安全是第一道防线。构建镜像时要遵循一个原则:基础镜像尽量选官方维护的、体积小的精简版本。扫描镜像里有没有已知漏洞,这个现在有成熟的开源工具可以做,把它纳入CI流水线里,每次构建完自动扫一遍,高危漏洞直接阻断发布。
K8s准入控制这块,可以通过 Admission Controller 来限制哪些镜像可以被拉取部署。当时我配置了一条规则:所有镜像必须来自私有仓库,禁止从公共镜像仓库直接拉取镜像,从源头控制供应链风险。同时限制了RunAsUser、RunAsNonRoot这些安全上下文,禁止容器以root用户运行。
7.2 网络安全策略
微服务之间不是默认全通的,而是要有最小化的访问控制。Calico的NetworkPolicy可以实现:只允许指定的生产命名空间内的服务访问order-service的8080端口,其余的全部拒绝。这类策略一开始要花时间梳理各服务间的依赖关系,但之后网络层面的安全性会有质的提升。
布NetworkPolicy的过程中,要反复验证策略没有误伤正常的服务调用。建议先在测试环境把策略切换成审计模式,只记录不应策略的流量而不实际阻断,观察一周的审计日志后,再切换到强制模式。当时采用这个渐进策略,确实避免了好几次事前没发现的“断网事故”。
7.3 备份、容灾与恢复演练
K8s集群本身的数据都在etcd里,包括所有资源对象、配置、Secret。每天做一次etcd快照,快照存到对象存储或者远程机器上,这是最简单的K8s备份方案。另外,对StatefulSet绑定的云硬盘,也要有快照策略。
备份做了是一回事,能不能恢复是另一回事。我特别建议每季度做一次“真实环境的恢复演练”,在临时环境里拉一次etcd快照,尝试重新恢复整个集群的全量资源。第一次演练的时候就会发现一堆问题:备份文件损坏、恢复流程只覆盖了部分资源、依赖的镜像仓库在上层网络不可达等。这些问题只有亲手演练才能暴露。
7.4 团队协作与发布流程规范
容器化的收益最终要落到团队协作上。当时定的发布流程是:代码合并触发CI,构建镜像、扫描漏洞、推送到镜像仓库,然后在CD系统里选择目标环境,确认之后自动更新对应Deployment的镜像标签。整个过程标准、可重复,上线流程从原来的“心惊胆战”变成了“按按钮”。
这个流程跑顺之后,还有一个意外收获:因为每个环境用的镜像都是可追溯到具体代码提交的,出了问题能快速对比各个环境之间的差异,再也不用去挨个机器上查版本了。
8. 项目复盘与经验沉淀
整个容器化改造做下来,最大的体会是“做减法比做加法难”。Docker和K8s确实提供了很多能力,但如果你把改造想象成“给每个服务都配上最完整的K8s全家桶”,那项目一定会失控。真正合理的策略是,按服务的实际需求决定用到哪一层,简单的服务用Deployment加Service就足够了,没必要一开始就搞复杂的网格和全链路遥测。
复盘的时候,我把整个项目的关键节点和踩过的坑整理成了一份内部文档,大纲大致是这样的:
- 改造前的基础设施盘点清单:服务清单、依赖关系图、端口规划、配置项清单。
- 容器化阶段检查清单:Dockerfile规范、镜像命名规范、配置与代码解耦情况、日志输出规范。
- 集群建设阶段检查清单:节点规划、网络方案、etcd备份、基础监控。
- 部署阶段检查清单:Deployment配置规范、探针配置、滚动更新参数、资源配额。
- 安全与治理阶段检查清单:镜像扫描、准入控制、网络策略、备份恢复演练。
这份文档后来成了团队内部的“施工手册”,新项目上线或者老项目改造基本照着走,效率提升非常明显。技术细节会过时,但这些基于实际踩坑经验沉淀下来的检查项,价值反而越来越稳定。
如果你正在准备做微服务的容器化改造,最后想给你一条个人建议:不要试图一次性把生产集群和全流程都配到完美,先把一个小服务跑通整个链路,再逐步扩展。稳定与可控,永远比炫技与激进更可靠。