这几年“云原生应用开发”从一个前端概念变成实打实的工程实践,我们团队从最初只做简单容器化,到后来管理一套上规模的 Kubernetes 集群,再到现在把 AI 大模型服务、Agent 应用全部纳入云原生体系,整个过程踩过的坑比想象中多得多。很多朋友问我,云原生到底是不是大厂专属,中小团队玩得起吗?我的答案是:如果只是把应用用容器跑起来,门槛不高;但要做到微服务、自动化、可观测性这一整套体系都转过来,需要的是方法而不是资源。
这篇文章我把自己的实操经验完整梳理一遍,从核心思路讲到底层细节,再到一份可以照着做的实战记录,最后是这几年积累的问题排查笔记。无论你是在做传统业务应用改造,还是准备把 AI 应用、大模型服务接入云原生体系,都能从里面找到用得上的东西。
1. 云原生应用开发到底在解决什么问题
1.1 云原生的本质:开发范式的整体切换
先纠正一个常见误区。很多人觉得“我的应用部署在云服务器上,就是在做云原生”,不是的。云原生不是部署位置的问题,它是一种开发范式的整体切换。CNCF 对云原生有一套经典的描述,落到工程层面核心就是四个关键词:容器化、微服务、声明式 API 和 DevOps。
容器化,解决的是“应用怎么打包”的问题。代码、运行时、依赖全部塞进标准化的镜像里,任何环境都能跑,不再有“我本地上是好的,生产环境怎么不行”这种经典事故。微服务,解决的是“应用怎么拆”的问题。把一个大单体拆成多个可独立开发、独立部署的小服务,团队可以并行迭代,故障边界也能收窄。声明式 API,解决的是“基础设施怎么管”的问题。你不再写一堆脚本去“操作”服务器,而是把期望状态写进 YAML 描述文件,交给 Kubernetes 这类平台去“趋同”现实。DevOps,解决的是“应用怎么交付”的问题。从代码提交、单元测试、镜像构建、流水线部署到监控告警,全部自动化,人为干预越少,交付质量越稳定。
这四件事连在一起,就是一条完整的“从代码到服务”的生产链条。用一个不太严谨但好懂的类比:容器是标准集装箱,Kubernetes 是港口调度系统,声明式 API 是集装箱上的清单标签,DevOps 是港口自动化流水线。过去我们运输货物靠人力搬运、手工记账,云原生把整套流程标准化和自动化了。
1.2 传统开发模式到底差在哪
我是从传统 Java Web 开发一路走过来的,所以对传统模式的痛点体会很深。最典型的问题是环境割裂:开发用 Windows 或 Mac,测试用虚拟机,生产是裸服务器,每个环境依赖版本都不一样,经常出现“部署成功但运行失败”的情况。
第二个痛点是扩展能力受限。单体应用要扛流量,只能纵向加机器,数据库连接、缓存连接都压在同一个进程里,扩机器带来的收益越来越小。而且单体部署一次要等很久,出了故障影响面是整个业务,代码都进同一个仓库,团队协作时冲突不断。
第三个痛点是交付链路漫长。传统模式里,开发、测试、运维是三个“部门”,交接靠文档和口头沟通,一个版本从提交到上线快则几天、慢则一周。遇到紧急修复,只能加班熬夜手动部署,极大依赖人的可靠性。云原生的价值就在这些点上逐个击破。我见过不少团队,做了容器化和 CI/CD 之后,发布频率从两周一次直接提到每天多次,故障恢复时间也大幅缩短。当然,任何架构都不是银弹,云原生也有自己的复杂性,但方向和收益是明确的。
为了更直观,我列一个对比表。这张表也是每次给团队做技术分享时我都会用的:
| 维度 | 传统开发模式 | 云原生开发模式 |
|---|---|---|
| 环境管理 | 手动配置,依赖运维 | 镜像交付,环境一致 |
| 应用架构 | 单体应用优先 | 微服务按需拆分 |
| 部署方式 | 手工脚本 + 人为操作 | 声明式 API + 自动化发布 |
| 扩展方式 | 纵向扩容为主 | 水平扩缩容,按需调度 |
| 故障隔离 | 故障影响整个应用 | 故障限制在单个服务 |
| 可观测性 | 日志文件 + 手工排查 | 指标 + 日志 + 链路追踪统一接入 |
| 交付频率 | 低,周期长 | 高,可支撑每日多次发布 |
2. 动手前必须掌握的四个核心细节
2.1 容器化:镜像不是越大越好
容器化是云原生应用开发的第一步,也是很多人容易做错的第一步。很多初学者最快的做法是把 Dockerfile 里所有依赖一股脑装进镜像,结果一个 Java 应用镜像两三个 G,推送和拉取都慢,启动也慢。实际上,镜像的体积控制直接关系到构建速度和部署速度,包括拉镜像的耗时,在流量高峰或滚动发布时影响很大。
关于体积,最常用的手段是多阶段构建。以 Java 应用为例,很多人会用 maven 镜像去构建,如果只有一个阶段,构建工具和依赖都会留在最终镜像里,体积自然大。多阶段构建的思路是:第一阶段在带 JDK 和构建工具的环境里编译打包,第二阶段只把编译产物拷贝到一个精简运行时镜像里。这样最终镜像只包含运行所需的 JRE 和 JAR 包,体积能缩小 60% 以上。
我给出一个可以参考的 Dockerfile:
# 第一阶段:编译 FROM maven:3.8.8-eclipse-temurin-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 第二阶段:运行 FROM eclipse-temurin:17-jre-alpine WORKDIR /app COPY --from=builder /app/target/*.jar app.jar EXPOSE 8080 ENV TZ=Asia/Shanghai RUN addgroup -S appuser && adduser -S appuser -G appuser USER appuser ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"]几个容易被忽略的细节:
第一,-XX:MaxRAMPercentage=75这个参数。Java 在容器里默认的堆内存设置不是拿容器限制而是拿宿主机内存来算的,不加这个参数,JVM 可能申请超量内存导致被 OOM Kill。我们之前就是因为这个问题调度了好几次,改了容器内存限制也没用,必须让 JVM 感知到容器配额。
第二,创建非 root 用户并切换到它。容器默认启动用户是 root,如果应用被攻破,攻击者直接就是容器内最高权限。虽然 K8s 层面有安全上下文可以限制,但在基础镜像这一层就把用户降级是最简单可靠的做法。
第三,时区问题。很多基础镜像默认是 UTC 时间,如果不设置TZ=Asia/Shanghai,日志里的时间会和本地差八个小时。别小看这个细节,排查线上问题时很致命。
第四,基础镜像的选型。Alpine 体积小,但某些 native 库(比如某些 SDK、加密库)兼容性差;Debian 系列的镜像更兼容但体积大一些。稳妥的做法是先用官方镜像,遇到兼容性问题再考虑换发行版,不要一上来就追求最小体积。
2.2 Kubernetes 资源管理:配额和调度没那么神秘
容器化只是第一步,应用上了 Kubernetes 之后,第一个要面对的问题就是资源管理。很多团队一开始部署时不给 Pod 设置资源 request 和 limit,结果多个 Pod 把节点内存吃满,直接触发系统 OOM,整个节点上的服务全部挂掉。
request 和 limit 是两层含义:request 是调度依据,调度器会根据所有 Pod 的 request 总量来分配节点,确保 Kubernetes 认为你有足够的空间;而 limit 是运行时限制,超出 limit,进程会被杀掉或者 CPU 被节流。一个常见的做法是 request 设为稳定运行所需资源,limit 设为突发时允许榨取的上限。
举个例子,一个普通的业务服务,压测下来稳定内存在 800MB 左右,最高能冲到 1.2GB。那么合理配置是 request 1GB 内存,limit 1.5GB,CPU 则根据实际压测来定。没有压测数据之前,先给一个保守值,后续按监控数据持续调整,这个过程我们内部叫“资源画像”。资源画像不是一次性工作,应用每做一次大的版本变更,都应该重新评估。
关于 CPU 有一个容易踩的坑:CPU 的 request 单位为整数或毫核(1000m 等于一核),但是 limit 设得太小会导致 CPU 节流,程序看起来没死,但吞吐量上不去。排查问题的时候要看容器 CPU throttle 的指标,别只看平均 CPU。
另外就是 GPU 资源。现在 AI 应用开发、大模型服务部署越来越多,GPU 调度是云原生开发避不开的话题。Kubernetes 通过 device plugin 暴露 GPU 资源,调度的时候用nvidia.com/gpu这个扩展资源来声明。GPU 是不可共享的基础设施,不能像 CPU 内存一样超卖,一旦某个 Pod 申请了配额,这块 GPU 就被独占,其他 Pod 无法复用。
我在实际项目里遇到过“GPU 配额已不够预冻结”的情况,团队在一个共享集群上同时跑多个大模型推理服务的排期测试,GPU 都被提前申请完了,后续任务只能阻塞等待释放。这里就暴露了一个很重要的问题:GPU 配额不仅仅是部署层面的技术问题,更是项目管理和资源治理的问题。应对方案通常有几个:
- 按项目优先级划分不同的 Namespace,给每个 Namespace 设置 ResourceQuota,防止单个项目把 GPU 全部占完。
- 推理服务按实际负载设置副本数和资源申请,不用的服务及时缩容或下线释放 GPU。
- 对周期性任务(比如模型评估、批量推理)用 Job 或 CronJob 跑,跑完自动释放。
- 提前做好资源规划,把 GPU 资源预留机制和审批流程嵌进团队协作流程里,而不是等配额耗尽才去协调。
2.3 微服务拆分:边界比框架更重要
微服务是云原生应用开发里最容易被过度设计的部分。我见过太多一上来就把系统拆成几十个小服务的团队,最后被分布式事务、服务治理这些复杂度淹没。合理的拆分不是拍脑袋,应当遵循几个原则。
第一个原则是“按业务能力拆”,而不是“按技术层拆”。比如电商系统可以拆成订单服务、库存服务、支付服务、用户服务,每个服务都包含自己独立的数据存储和业务逻辑,而不是把 Controller、Service、DAO 分别拆成独立服务。后者拆出来的不是微服务,是分布式单体,比单体还难维护。
第二个原则是“绞杀者模式”。我不建议把单体应用一次性重构成微服务,那风险太大。更稳妥的方式是:单体继续活着,每次把一块业务边界清楚的功能抽取出来变成独立服务,然后通过网关逐步切换流量。所谓绞杀者,就是一个一个割掉旧系统,让它最终被新架构替代。这个过程可以按季度甚至按年推进,每个迭代都有可验证的结果。
第三个原则是“数据独有”。每个微服务都应该拥有自己的数据存储,服务之间不能直接读写对方的数据库,只能通过 API 或者消息通信。这是微服务边界成立的基石。如果业务上要求强一致性,那么就要评估是不是真的适合拆出来,因为跨服务的事务处理复杂度是指数上升的。
在服务通信上,我的经验是轻量级同步调用用 REST,内部高性能调用用 gRPC,异步解耦用消息队列。三者各有适用场景。比如查询类接口用 REST 就够,模型推理这种需要性能和强类型契约的,用 gRPC 更合适;而像订单创建之后发通知这类不要求立即完成的动作,丢到消息队列里处理更稳。
分布式事务方面,我不建议引入重量级分布式事务框架,Saga 模式的代码编排方式更适合云原生环境。简单说,Saga 是一个长流程拆成多个本地事务,每个本地事务完成后,触发下一个事务;中间的某一步失败,就按反向顺序执行补偿事务。这个模式对数据库的压力小,也更适合微服务之间通过消息通信的天然解耦。
2.4 CI/CD 流水线:交付自动化的关键环节
CI/CD 是云原生应用开发里投入产出比最高的一环。我们团队刚开始搞微服务时,手动部署一次要 30 分钟,还经常因为漏配环境变量出问题。做完流水线之后,提交代码到自动上线,全流程基本在 10 分钟内完成,而且因为每一步都是脚本化、可回溯的,失误率大幅下降。
一个完整的 CI/CD 流水线通常包含几个阶段:代码拉取、单元测试、依赖安全扫描、镜像构建、镜像推送、部署到测试环境、集成测试、部署到生产环境。各阶段是环环相扣的,任何一步失败都要中断流水线,不能带着问题往后流。
镜像构建和推送这一步,建议把镜像 TAG 和代码 commit 关联起来,比如用 Git 短 hash 做 tag。这样生产环境跑的是哪个代码版本,一目了然。部署阶段,在 Kubernetes 里可以用 kubectl set image 触发滚动更新,也可以引入 Argo CD 这类 GitOps 工具,把部署清单都放在 Git 仓库里,通过 merge request 触发同步,实现真正的“Git 仓库是唯一事实来源”。
这里补充一下 GitOps 的理念:服务器的 YAML 清单都保存在 Git 仓库,任何人要改环境,都要提交 PR,然后由自动化工具监听仓库变化并同步到集群。这个模式的好处是:所有变更都有记录、可审计、可回滚。我们团队用 Argo CD 之后,线上配置变更全部走 Git 提交,谁改了什么、什么时候改的,一查便知。
流水线的环境管理上,我们一般分三套环境:dev、staging、prod。dev 环境开发自测;staging 尽量与 prod 一致,跑集成测试和上线前的演练;prod 才是最终环境。一套清晰的 CI/CD 流水线,是团队规模和业务复杂度增长之后还能保持交付节奏的核心底座。
3. 完整实操:一个云原生应用从 0 到上线的全记录
3.1 需求与架构设计
理论说了这么多,接下来我把一个真实项目的缩略版实操过程完整复盘出来。这个项目是一个内部 AI 助手平台,核心能力是把公司知识库和大模型推理服务做一个简单封装,对内提供问答和检索。
架构上拆成三个服务:
- gateway-service:统一入口,负责认证、路由、限流。
- user-service:用户和权限管理。
- model-service:封装大模型推理调用,管理远程模型服务的请求转发和结果缓存。
技术栈选择上,我们没有用一个重量级的微服务框架,而是采用 Spring Boot(user-service 和 gateway 用)加 Go(model-service 用)。为什么 model-service 用 Go?因为模型推理服务的本质是 IO 密集和网络转发,Go 的并发模型在这种场景下更轻量,部署产物也是一个单一二进制,镜像体积比 Java 小一个数量级。
Kubernetes 集群层面,我们用的单 master 加三 worker 节点,规模不大但足够承载整个业务。
3.2 镜像构建实操记录
这个项目里我重点演示一下多阶段构建在 Go 服务上的应用。Go 的多阶段构建效果比 Java 更夸张,编译阶段的镜像可以跑到一两 GB,但最终运行镜像可以压到几十 MB。
下面是 model-service 的 Dockerfile:
# 编译阶段 FROM golang:1.22-alpine AS builder WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o /out/model-service . # 运行阶段 FROM alpine:3.19 RUN apk add --no-cache ca-certificates tzdata && \ addgroup -S appgroup && adduser -S appuser -G appgroup WORKDIR /app COPY --from=builder /out/model-service . ENV TZ=Asia/Shanghai USER appuser EXPOSE 8080 ENTRYPOINT ["./model-service"]注意这里CGO_ENABLED=0很关键。Go 默认会启用 CGO,编译出来的二进制会动态链接一些库,运行镜像必须带这些库。禁用 CGO 之后编译产物才是纯静态二进制,在一个精简的 Alpine 镜像上就能直接跑。之前有同事在本地跑 Go 服务没问题,一容器化就报no such file or directory,就是这个问题导致的。
镜像构建完,我们直接在 CI 流水线里执行 docker build,并把构建产物推送到团队私有的镜像仓库。TAG 用 Git commit hash,拉取策略设为 IfNotPresent,减少不必要的重复拉取。
3.3 Kubernetes 部署配置实操
核心部署文件以 model-service 为例。先解决几个基础概念:Namespace 用来做逻辑隔离,Deployment 管理 Pod 副本数和滚动更新,Service 提供稳定的内部 DNS 地址,Ingress 负责外部流量接入。
下面是部署清单:
apiVersion: v1 kind: Namespace metadata: name: ai-platform --- apiVersion: apps/v1 kind: Deployment metadata: name: model-service namespace: ai-platform spec: replicas: 2 selector: matchLabels: app: model-service template: metadata: labels: app: model-service spec: containers: - name: model-service image: registry.internal/ai-platform/model-service:abc1234 resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "500m" ports: - containerPort: 8080 readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 15 periodSeconds: 20 --- apiVersion: v1 kind: Service metadata: name: model-service namespace: ai-platform spec: selector: app: model-service ports: - port: 80 targetPort: 8080有几个点要说明。第一,资源配额不是随手写的。model-service 压测时平均内存是 400MB 左右,我们把这个服务的 request 设为 512Mi,limit 设为 1Gi。日常运行时,两个副本加起来约 1GB 内存,在 4 核节点上完全没问题。limit 的作用是防止流量突发时把整个节点内存吃光。
第二,readiness 和 liveness 探针是云原生应用的安全带。readiness 探针控制流量路由,只有服务真正可用了才把流量打进来;liveness 探针负责判断进程是否卡死,如果连续多次失败,Kubernetes 会自动重启容器。不配置探针,发布过程很容易出现“旧 Pod 还在服务但新 Pod 还没就绪”的窗口期。
配置完 Deployment,我们把 Ingress 也配置好,把ai.internal.example.com路由到 gateway-service。这样外部访问路径是 Ingress -> gateway-service -> (user-service 或 model-service),层次清晰。
3.4 AI 服务的 GPU 部署实践
model-service 本身不直接占用 GPU,它只是转发请求,真正的 GPU 消费在底层的大模型推理服务上。我们为推理服务单独建立了一个 GPU 节点池。
部署推理服务的 yaml 核心片段如下:
spec: containers: - name: llm-inference image: registry.internal/ai-platform/llm-inference:latest resources: requests: memory: "16Gi" nvidia.com/gpu: 1 limits: memory: "16Gi" nvidia.com/gpu: 1GPU 的声明方式是用nvidia.com/gpu: 1,前提是节点上安装了 NVIDIA device plugin。这里有一个容易被忽略的点:requests 和 limits 里的 GPU 数量必须一致,因为 GPU 不参与超卖,配额声明多少就用多少,不会像 CPU 或内存那样在 request 和 limit 之间有弹性空间。
关于 GPU 资源调度,调度器是“谁先申请谁先用”。这也是前面提到的“GPU 配额已不够预冻结”现象的根因。我们在集群里把推理服务放在单独的 Namespace,并设置了配额上限,比如:
apiVersion: v1 kind: ResourceQuota metadata: name: gpu-quota namespace: inference spec: hard: nvidia.com/gpu: "4"同时,给不同项目设置不同的配额:核心业务给 2 块、实验性项目给 1 块、剩余留作机动。配额耗尽时,实验性项目让位,核心业务优先保障。这个流程在团队协作中非常关键。很多技术团队在共享 GPU 上踩的坑,本质上是没有提前约定“谁优先、谁让位”的规则。
3.5 可观测性三件套的接入
应用部署上去了,接下来必须做可观测性。我们的做法是把日志、指标、链路追踪三件套整体接入,缺一不可。
指标采集用 Prometheus,我们的每个服务都暴露一个/metrics端点,由 Prometheus 定时抓取。Grafana 负责展示,我会给每个服务画一张核心指标大盘:CPU、内存、QPS、P95 延迟、错误率。有了这套面板,服务的健康状况一目了然,遇到流量突增能快速定位。
链路追踪我们选的是 OpenTelemetry,统一把 trace 数据发送到 Jaeger。每个请求进来都在入口生成 Trace ID,经过 gateway、model-service、大模型推理链路时逐段埋点。在线联调排障场景下,Trace ID 的价值特别大,用户报一个慢请求,直接拿 Trace ID 去查链路,一分钟就能定位到是网关转发慢还是模型推理本身慢。
日志这块,我们没有用传统 EFK 那套重方案,而是用 Loki。Loki 不建立倒排索引,只对日志内容做压缩和标签索引,资源消耗小很多。部署到 Kubernetes 上,各服务把日志输出到标准输出,Fluent Bit 采集并转发,Loki 聚合存储。排障时在 Grafana 的 Explore 面板里直接搜索关键字,体验比 kubectl logs 好得多。
这里我要强调一件事:可观测性不是上线之后才补的,而是应用开发阶段就要埋好点。我们内部有规范,任何新服务上线前,必须提供 /metrics、结构化日志和链路追踪至少一项能力,否则不允许发布。这条规范看起来有点严,但真的能省下大把排障时间。
4. 常见问题与排查技巧实录
4.1 容器层面的高频问题
容器化阶段最常见的坑是“容器启动即退出”。很多人第一反应是去看应用日志,但很多时候进程根本没能跑起来,日志是空的。排查顺序应该是:先看启动输出,再检查 EntryPoint 和 CMD 是否有误,然后手动进容器验证依赖是否齐全。如果是动态链接的二进制放到精简镜像里,大概率就是缺库的问题。
另一个高频问题是镜像构建缓慢。根源通常是依赖缓存没做好。以 npm 或 Go 为例,先单独拷贝依赖描述文件并执行依赖下载,再拷贝源码,这一层缓存能命中,就不会每次变更代码都重复下载依赖。Java 的依赖预拉取同理,这个道理我已经放在前面的 Dockerfile 示例里了,实操中很管用。
还有就是盲目的“最小镜像”陷阱。有人为了追求极度精简,基础镜像用 scratch,结果应用里面需要 CA 证书、时区数据都没了,服务对外调用 HTTPS 直接失败。精简的前提是理解应用依赖,而不是一味的体积崇拜。
4.2 Kubernetes 运行时的疑难杂症
Kubernetes 运行时的经典问题首推 CrashLoopBackOff。这是 Pod 启动后立刻崩溃,被反复重启的状态。排查顺序:先看应用日志,确认是不是缺配置或连接不上依赖服务;再看 describe pod 的事件信息,判断是镜像拉取失败还是探针失败。如果探针配置不正确,容器可能实际健康但被 liveness 杀掉,这在配置探测参数时尤其容易踩。
服务间无法访问也常见。优先排查 Service 的 selector 是否匹配到 Pod 的 label,这是最典型的“查了半天 DNS 没问题、端口没问题、但就是不通”的原因。其次是检查 NetworkPolicy 是否限制了流量,我们遇到过在共享集群里加了 NetworkPolicy 之后,服务间互相访问全部超时,排查了很久才发现是策略问题。
Pod 一直 Pending 是资源问题的最直接体现。describe pod 会告诉你调度失败的原因:可能是节点资源不足、GPU 配额不够、或者污点与容忍不匹配。我们之前遇到过 Pod 被调度到没有 GPU 的节点,因为节点标签和调度器选择器没对齐。所以涉及 GPU 的 Pod,建议给 GPU 节点打上专用 label,并在 Deployment 里配置 nodeSelector,精确引导调度。
内存问题方面,Java 服务在容器里特别容易出问题,就是前面说的 JVM 不感知容器内存限制。还有其他语言的应用,直接申请内存超过 limit,会被 OOM Kill,表现为 Pod 不断重启但容器内日志无明显报错。排查这种问题要看节点层事件和容器内存使用曲线。
4.3 微服务与发布流程的实战教训
微服务化之后,服务雪崩是遇到最多次的问题。一个服务响应变慢,调用方同步等待,线程池被占满,故障像多米诺骨牌一样往外蔓延。解决这件事靠的是两个习惯:调用方设置超时和熔断,以及服务自身做隔离。超时是底线,每个跨服务调用都必须有明确的 timeout,而且要测一下 timeout 本身是否符合预期;熔断是保护伞,连续失败一定次数就快速失败,不再往后端继续压。
发布流程里常见的翻车点是“不同环境配置不一致”。dev 环境连接的是开发数据库,staging 连接的是预发布库,如果哪次发布把配置文件带错了,线上事故就跑不掉。我们现在的策略很直接:所有配置全部放进 ConfigMap 和 Secret(敏感信息),由 CI/CD 流水线统一注入,代码里面不写任何环境相关的配置。这个习惯救了我们很多次。
还有一个 CI/CD 环节的坑:镜像 TAG 不变化导致“发布不生效”。部署清单里写的是固定 TAG,某次构建失败后旧 TAG 被重新覆盖,发布之后发现线上跑的怎么还是旧版本,一查发现 TAG 一样但镜像内容变了。痛定思痛之后,我们强制所有 TAG 都带 commit hash,同一个 TAG 只对应一次构建,杜绝覆盖写。
4.4 问题排查速查表
我把这些年踩过的典型坑整理成一张速查表,按症状、可能原因、排查建议三栏展开,方便大家在实际遇到问题时快速对照。
| 症状 | 可能原因 | 排查与解决建议 |
|---|---|---|
| 容器启动即退出 | 动态链接依赖缺失 | 查看启动输出;手动进容器验证 |
| 镜像构建速度慢 | 依赖缓存失效 | 先拷贝依赖描述文件再拷贝源码 |
| Pod 一直 Pending | 资源不足或调度不匹配 | describe pod 查看调度失败原因;配置 nodeSelector |
| 服务间访问不通 | selector 标签不匹配 | 检查 Service 与 Pod label 是否一致 |
| Java 容器被 OOM Kill | JVM 未感知容器内存限制 | 加 -XX:MaxRAMPercentage 参数 |
| 发布后跑的是旧镜像 | TAG 覆盖写 | TAG 带 commit hash,单一 TAG 对应一次构建 |
| GPU 任务排队阻塞 | 配额被提前占用 | 按业务优先级分配 GPU 配额,Job 跑完自动释放 |
| 跨服务调用变慢引发雪崩 | 没有超时和熔断 | 设置合理超时,引入熔断降级 |
排查问题的大原则是从外向里:先看网络和调度,再看进程和日志,最后看代码逻辑。很多新手上来就跳进日志细节,反而漏掉了集群事件里可能直指根本原因的线索。
这套实践里,我觉得最值得反复强调的还是那句老话:云原生应用开发最难的并不是技术本身,而是整个团队能不能把思维方式转过来。容器技术学起来很快,Kubernetes 命令背一背也不难,难的是让每个人都理解“声明式、自动化、可观测性”这套理念,并且在日常开发里坚持执行。我在实际项目里见过太多团队,容器化做了、集群上了,但发布还是靠人肉操作,出问题还是靠 SSH 进去翻日志,只能说形似而神不似。我的建议是,小团队起步时不要贪多,先把容器化和 CI/CD 做扎实,再逐步引入微服务拆分和可观测性,一步一个脚印,比什么都重要。