Spring Boot 容器化链路:Harbor、Nacos、Rureka、NFS

发布时间:2026/10/28 3:29:37
Spring Boot 容器化链路:Harbor、Nacos、Rureka、NFS 这套组合我第一次拼起来的时候最耗时间的不是写业务代码而是把配置从哪来、镜像从哪拉、文件往哪写、别人怎么找到我这四件事在集群里对齐。一个 Spring Boot 服务要真正跑在 Rancher 编排的集群里中间隔着镜像仓库、配置中心、注册中心、共享存储四道关任何一道没对齐表现都是同一个Pod 起不来或者起来了但没人调得到它。这篇内容讲的就是这条完整链路——Spring Boot 项目构建成镜像推进 Harbor由 Rancher 6.3 编排调度启动阶段从 Nacos 拉配置、向 Rureka 注册中心报到业务文件落到 NFS 共享目录。适合已经写过 Spring Boot、但第一次把服务往容器集群上搬的同学也适合环境已经跑起来、但三天两头出小毛病想系统梳理一遍的老手。我会把每个环节为什么这么做讲透顺带把踩过的坑和排查链路都摆出来你可以直接照着复现。1. 先把五个组件的责任边界划清楚1.1 每个组件管什么、不管什么这套技术栈最大的风险不是某个组件配错而是职责越界。我见过把数据库连接串打进镜像的也见过拿注册中心当配置中心用的短期都能跑运维半年之后没人敢动。组件它负责的那一段明确不该它管的事越界后的典型症状Harbor存镜像、管版本、控权限不该存运行时配置和业务文件改个参数要重新构建镜像Nacos集中管理配置、推送变更不该存服务实例的实时状态配置里塞了一堆心跳数据Rureka服务注册、心跳、实例列表下发不该承担配置分发重启一次注册信息全丢NFS多副本共享的持久化目录不该当数据库或高并发写入盘文件锁冲突、随机读写极慢Rancher 6.3编排调度、发布回滚、资源配额不该手工进容器改文件节点重启后改动全部消失标题里写的 Rureka就是我们项目里那套注册中心组件的叫法职责跟常见服务注册发现组件完全一致应用启动时上报自己的地址定时心跳续约消费方按服务名拉实例列表。后面统一用这个名字不再纠结它底层是哪一套。判定的标准其实很简单这份数据变了需不需要重新构建镜像需要那就是镜像的事不需要但要重启才生效那是配置不需要重启就变那是运行时状态。按这条线切五个组件的边界基本不会错。1.2 一个 Pod 从创建到接流量的完整时序理解顺序比记配置重要。脑子里有了这条线任何一环出问题你都知道该去哪看。Rancher 把 Deployment 下发到某个节点kubelet 开始创建 Pod拉镜像阶段kubelet 读取命名空间里的imagePullSecrets带着凭据去 Harbor 认证并拉取指定 tag 的镜像容器进程启动Spring Boot 在bootstrap阶段注意是 bootstrap不是 application就去连 Nacos按namespace group dataId三元组把远端配置拉下来合并到 EnvironmentSpring 上下文继续初始化数据源、线程池这些 Bean 用远端配置里的参数创建应用启动完成向 Rureka 发起注册上报自己的 IP、端口和元数据同时开始定时心跳容器内的readinessProbe开始探测/actuator/health连续成功若干次后 Pod 被标记为 ReadyService 的 Endpoints 里出现这个 Pod IP流量开始进来业务写入的文件通过 PVC 落到 NFS 服务器上的实际目录。这条链路里有两个容易被忽略的点。第一个是第 3 步发生在第 5 步之前如果 Nacos 连不上且配置了快速失败应用根本走不到注册那一步你会看到 Pod 反复重启但注册中心里一个实例都没有。第二个是第 6 步和第 5 步是并行的注册成功不等于能接流量探针没过照样不进 Endpoints。排查时把这两条分开看能省一大半时间。1.3 三种最常见的越界写法第一种把application-prod.yml直接COPY进镜像环境变量都不留。看着省事实际是给自己挖坑测试环境和生产环境要打两个镜像tag 还得区分一旦发错版本就是事故。正确做法是镜像里只保留application.yml的骨架所有环境差异项走 Nacos 或者环境变量。第二种把配置中心的dataId按实例来分每个 Pod 一份。这等于把配置中心退化成了本地文件还多了一次网络调用。配置应该按应用 环境维度组织同一个应用的所有副本读同一份差异通过命名空间隔离。第三种把日志目录挂到 NFS 上然后开 DEBUG。NFS 适合存用户上传的附件、生成的报表这类写一次读多次的文件日志这种高频追加写的场景放上去轻则写入延迟飙升重则把 NFS 服务端的锁资源打满连累所有挂载了这块存储的应用。日志走标准输出交给集群的日志采集组件处理。2. 把 Spring Boot 打成能上集群的镜像2.1 为什么用多阶段构建而不是本机打包再 COPY很多人习惯本机mvn clean package出一个 jar然后写个三行的 Dockerfile 把 jar 拷进去。这个做法在单人开发时没问题一旦多人协作就暴露三个问题构建机的 JDK 版本不一致导致字节码版本对不上构建产物不可复现同一个 commit 在不同人机器上打出来的包可能不一样本地仓库里可能有 SNAPSHOT 依赖构建成功但集群上跑不起来。多阶段构建把编译和运行彻底分开第一个阶段用完整的 Maven JDK 镜像编译第二个阶段只保留 JRE 和 jar。好处是构建过程完全在容器里完成任何人任何机器跑出来的结果一致最终镜像里没有 Maven、没有源码、没有本地仓库缓存体积能压到原来的一半以下。代价是首次构建要下载全量依赖比较慢。解决办法是在COPY src之前先COPY pom.xml并执行一次dependency:go-offline这样只要pom.xml没变依赖层就能命中缓存后续构建只重新编译业务代码。2.2 一份可以直接抄的 Dockerfile# ---------- 构建阶段 ---------- FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build # 先只拷贝 pom让依赖层独立成层命中缓存 COPY pom.xml . RUN mvn -B -q dependency:go-offline # 再拷贝源码编译 COPY src ./src RUN mvn -B -q clean package -DskipTests # 利用 Spring Boot 分层能力拆出依赖层 RUN java -Djarmodelayertools -jar target/*.jar extract --destination /build/layers # ---------- 运行阶段 ---------- FROM eclipse-temurin:17-jre-jammy ENV TZAsia/Shanghai \ LANGC.UTF-8 \ JAVA_OPTS-XX:MaxRAMPercentage70 -XX:UseG1GC -Dfile.encodingUTF-8 RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone \ groupadd -r app useradd -r -g app app WORKDIR /app COPY --frombuilder /build/layers/dependencies/ ./ COPY --frombuilder /build/layers/spring-boot-loader/ ./ COPY --frombuilder /build/layers/snapshot-dependencies/ ./ COPY --frombuilder /build/layers/application/ ./ RUN mkdir -p /app/data /app/logs chown -R app:app /app USER app EXPOSE 8080 ENTRYPOINT [sh, -c, exec java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher]这段里有几个细节值得单独说。jarmodelayertools是 Spring Boot 自带的分层能力它把 fat jar 拆成dependencies第三方依赖几乎不变、spring-boot-loader、snapshot-dependencies、application业务代码每次都变四层。这么做之后每次发版只有最后一层变化前面几层全部命中节点上的镜像缓存拉取时间从几十秒降到几秒。集群规模越大这个优化的收益越明显。exec java里的exec不能省。不加的话sh是 1 号进程Java 是子进程你发SIGTERM的时候信号到不了 JVM优雅停机逻辑根本不执行容器被硬杀正在处理的请求全部断连。MaxRAMPercentage70代替固定的-Xmx是因为容器里的内存配额随时可能调整硬编码的堆大小要么浪费要么 OOM。留 30% 给堆外内存、元空间、线程栈和 JVM 自身开销这个比例在绝大多数业务服务上是安全的。2.3 时区、编码、非 root 用户这些小事时区不对是最隐蔽的坑之一。容器默认 UTC日志时间戳比北京时间差 8 小时排查问题时对不上业务方的描述能来回扯半小时。ENV TZ加上软链/etc/localtime两步都要做只设环境变量有些基础镜像不认。编码问题在 Windows 开发的团队里更常见。LANGC.UTF-8和-Dfile.encodingUTF-8双保险避免容器内读中文文件名、生成 CSV 报表时出现乱码。非 root 用户这条很多团队为了省事直接用 root 跑。短期没感觉一旦挂载了 NFS容器内的 root 会被服务端的root_squash映射成匿名用户写文件直接 Permission denied——这个问题在本地开发环境永远复现不了只有上了集群才炸。提前用普通用户跑能把这个坑挡在上线之前。2.4 镜像 tag 的命名规则别用latest。latest意味着我不知道这是哪个版本出问题想回滚都找不到回滚目标而且imagePullPolicy: Always配合latest会导致每次重启都去拉一次镜像Harbor 挂了整个集群都起不来。我习惯用分支名-短commit-时间戳比如release-3f8a1c2-20240612。这个 tag 自带三个信息从哪个分支出的、对应哪个 commit、什么时候构建的。出问题直接拿 tag 去代码库定位不用问任何人。3. Harbor 侧的准备项目、机器人账号与拉取凭据3.1 项目划分与镜像命名Harbor 的层级是项目/仓库:tag对应到集群里就是harbor.example.internal/order-service/app:release-3f8a1c2-20240612。项目这一层建议按环境 用途划分比如prod、staging、base基础镜像。不要所有东西都往library里塞后面权限没法控。镜像路径里的仓库名用应用名跟 Nacos 的spring.application.name、Rureka 里注册的服务名保持一致。三处同名排查问题时不用做映射转换看到哪个都能对上。3.2 为什么用机器人账号而不是个人账号用个人账号拉镜像最直接的后果是这个人的账号一旦停用或者改了密码所有引用该凭据的工作负载全部拉取失败。而且个人账号通常有推送权限等于把写权限发给了每一个需要拉镜像的节点。Harbor 的机器人账号Robot Account是为这种场景设计的只勾选pull权限不绑定具体的人可以设置有效期。给每个集群或者每个环境建一个独立的机器人账号权限最小化出问题也能快速定位是哪个环境在用。创建之后会拿到一个类似robot$prod-cluster的用户名和一串 token 作为密码这两个值只在创建时显示一次务必当场存进密码管理工具。3.3 在命名空间里创建拉取凭据kubectl create secret docker-registry harbor-cred \ --docker-serverharbor.example.internal \ --docker-usernamerobot$prod-cluster \ --docker-password创建时生成的那串token \ --namespaceprod创建完在 Deployment 的spec.template.spec里引用spec: template: spec: imagePullSecrets: - name: harbor-cred如果嫌每个 Deployment 都写一遍麻烦可以把 secret 挂到 ServiceAccount 上这样该 SA 下的所有 Pod 自动带上凭据。不过我倾向于显式写因为隐式绑定在排查为什么这个 Pod 拉不到镜像时会多绕一层。3.4 拉取失败的四种返回分别指向不同问题报错关键词大概率原因验证方式unauthorized/401凭据没生效或机器人账号权限不足换一台机器手动docker login试manifest unknown/not foundtag 写错或镜像推到了别的项目去 Harbor 界面按 tag 搜一下x509: certificate signed by unknown authorityHarbor 用的是自签证书节点不信任在节点上curl -v https://harbor地址no such host/i/o timeout节点 DNS 解析不了仓库域名进节点nslookup或ping第一种和第二种占九成。特别是第一种很多时候 secret 建在了default命名空间Deployment 却在prod里跨命名空间是读不到 secret 的报错看起来一模一样。第三种情况如果 Harbor 用的是 HTTP 而不是 HTTPS需要在每个集群节点的容器运行时配置里把仓库地址加进insecure-registries。这一步在 Rancher 里改起来比较麻烦因为要改节点上的运行时配置文件然后重启服务滚动做的时候注意别把所有节点一起重启否则集群会短暂不可用。我的建议是能给 Harbor 配上正式证书就别用 HTTP省掉这一整类问题。4. Nacos 里怎么组织配置半年后还能看懂4.1 命名空间、分组、dataId 三级怎么分工这三层不是随便填的各有用意。我的用法是命名空间对应环境dev、staging、prod。命名空间之间完全隔离改错了环境也不会串。分组对应业务域ORDER、USER、COMMON。跨应用共享的配置放COMMON分组。dataId对应应用order-service.yaml。一个应用一份主配置个别需要拆分的再加后缀。这么分的好处是新人接手时按我要改哪个环境、哪个域、哪个应用三步就能定位到具体文件不用在一堆xxx-prod-v2-final.yaml里猜。4.2 Spring Boot 2.4 之后 bootstrap 不生效的问题这是升级框架时最常见的坑。Spring Boot 2.4 重构了配置加载机制bootstrap.yml默认不再被加载了。表现是本地跑得好好的配置也确实写在bootstrap.yml里但应用启动后读到的全是本地默认值Nacos 上的配置完全没生效。两个解法选一个就行方案一在pom.xml里加依赖dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-bootstrap/artifactId /dependency加完之后老的bootstrap.yml方式继续可用改动最小适合存量项目。方案二在application.yml里用spring.config.importspring: config: import: - optional:nacos:order-service.yaml?groupORDERrefreshEnabledtrue这种方式更符合新版的设计思路但要注意加上optional:前缀否则 Nacos 连不上时应用直接启动失败。生产环境我反而不加optional——配置中心不可用的时候服务带着错误的配置启动起来危害更大宁可起不来。4.3 一份可用的 bootstrap.ymlspring: application: name: order-service cloud: nacos: server-addr: nacos.example.internal:8848 discovery: namespace: 6f1e2d3c-4b5a-6789-0abc-def012345678 group: ORDER metadata: version: v1 config: namespace: ${spring.cloud.nacos.discovery.namespace} group: ORDER file-extension: yaml shared-configs: ->spring: cloud: nacos: discovery: ip: ${POD_IP:} network-interface: eth0POD_IP通过 Downward API 注入env: - name: POD_IP valueFrom: fieldRef: fieldPath: status.podIP这么做的好处是注册的地址跟 kubelet 看到的完全一致省掉一层推导。5.2 心跳、租约与优雅下线注册之后客户端会按固定间隔默认 5 秒左右发送心跳续约注册中心超过一定时间默认 15 秒收不到心跳就把实例标记为不健康再超过一定时间默认 30 秒直接剔除。容器滚动发布的时候这个过程跟 k8s 的 Pod 生命周期是两套独立机制很容易打架。典型场景k8s 决定终止一个 Pod立刻发SIGTERM应用进程收到信号开始关闭。如果关闭得太快注册中心还没收到主动注销的请求实例列表里还留着这个 IP调用方继续往这儿发请求全部连接被拒。解决方案是三步一起上lifecycle: preStop: exec: command: [sh, -c, sleep 15] terminationGracePeriodSeconds: 45第一步preStop里 sleep 15 秒。这 15 秒里容器还在跑但 k8s 已经从 Endpoints 里摘掉了这个 Pod不再有新流量进来同时应用有机会向注册中心发送注销请求。第二步应用层面注册关闭钩子SIGTERM时主动 deregister然后等待正在处理的请求完成。第三步terminationGracePeriodSeconds要给够。它的含义是从发 SIGTERM 到强杀最多等多久。这个值必须大于preStop的 sleep 时间 应用自身的关闭耗时否则 k8s 会在应用还没关完的时候直接 SIGKILL前面两步白做。45 秒是个比较稳妥的取值15 秒 preStop 20 秒等待在途请求 10 秒缓冲。5.3 健康检查别指向依赖下游的接口/actuator/health默认会检查数据库、Redis 等下游组件的连通性。如果有人把它配成livenessProbe那么数据库抖动一下所有 Pod 都会被判定为不健康然后重启——本来是下游的问题结果把自己也搞崩了雪上加霜。正确的分工是livenessProbe只判断进程是否还活着、是否死锁用/actuator/health/liveness它只反映应用自身的状态readinessProbe判断能不能接流量可以用/actuator/health/readiness它会考虑下游依赖依赖不可用时把 Pod 摘出 Endpoints但不重启startupProbe专门给启动慢的应用用启动完成前不执行前两个探针避免启动过程中被误杀。Spring Boot 2.3 之后内置了这两个分组需要在配置里打开management: endpoint: health: probes: enabled: true health: livenessstate: enabled: true readinessstate: enabled: true5.4 多副本与元数据同一个服务起多个副本时注册中心会拿到多个实例。如果各副本的能力不一样比如有的机器内存大、能处理大任务可以在metadata里打标签消费方按标签做路由。metadata: version: v1 zone: zone-a weight: 100灰度发布就靠这个新版本先起一个副本version: v2通过网关把 1% 的流量导过去观察没问题再逐步放大。比一次性全量滚动安全得多出问题影响面可控。6. NFS容器里唯一有状态的那部分6.1 为什么不能用 hostPath 和 emptyDiremptyDir的生命周期跟 Pod 绑定Pod 一删数据就没了只适合放临时缓存。hostPath把节点上的目录挂进容器数据确实不会随 Pod 消失但它把 Pod 和节点绑死了——下次调度到另一台机器数据就找不到了多副本的时候每个副本看到的是各自节点上的目录彼此不共享。用户上传的附件、生成的报表、导入的 Excel 这类东西必须满足两个条件Pod 重建后还在多个副本看到的是同一份。这就只能上网络存储。NFS 是最简单的一种配置成本低小团队用足够了。6.2 用 nfs-subdir-external-provisioner 动态供给手动创建 PV 的方式在小规模下能用但每加一个应用就要建一对 PV/PVC路径还得手工规划很容易冲突。用动态供给省事得多装一个 provisioner声明一个 StorageClass之后每个 PVC 自动在 NFS 上生成一个子目录。apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-app provisioner: k8s-sigs.io/nfs-subdir-external-provisioner parameters: archiveOnDelete: true reclaimPolicy: Retain volumeBindingMode: ImmediatearchiveOnDelete: true意味着 PVC 删除时不会真删数据而是把目录改名加个 archived 前缀。这个参数救过我一次——误删了一个 PVC数据还在改个名字重新挂上就恢复了。存储成本远比数据丢失便宜生产环境务必打开。reclaimPolicy: Retain是 PVC 删了之后 PV 保留手动确认无误再清理。配合archiveOnDelete双保险。6.3 目录怎么规划一个应用挂一个 PVC内部再分几个子目录volumeMounts: - name: app-data mountPath: /app/data/upload subPath: upload - name: app-data mountPath: /app/data/export subPath: export用subPath的好处是同一个 PVC 可以挂到多个路径不必为每个子目录建一个 PVC。但要注意subPath的目录必须提前存在k8s 不会自动创建Pod 会一直卡在ContainerCreating事件里提示failed to create subPath directory。解决办法是在镜像构建时就把目录建好前面的 Dockerfile 里mkdir -p /app/data就是干这个的或者用一个 initContainer 预先创建。不要把临时文件目录挂到 NFS 上。很多框架会在临时目录里放大量小文件上传缓冲、序列化临时对象本地磁盘几毫秒的事走网络存储就是几十毫秒积少成多直接把接口拖慢一个数量级。临时目录用emptyDir配一个sizeLimit就够了。6.4 权限root_squash 与非 root 用户的冲突这是 NFS 上最常见、也最难自己想到的问题。NFS 服务端默认开启root_squash客户端以 root 身份发起的访问在服务端会被映射成nobody用户。如果你的容器以 root 跑写文件时服务端认为你是个匿名用户有没有权限取决于目录的属主和权限位。目录是 root 建的 755那写入直接失败。表现是ls能看cat能读一写就Permission denied。日志里可能只有一行java.io.IOException: Permission denied看不出跟 NFS 有什么关系。三种处理方式按推荐顺序首选容器用非 root 用户跑并且在 Pod 里指定fsGroupsecurityContext: runAsUser: 1000 runAsGroup: 1000 fsGroup: 1000fsGroup会让 kubelet 在挂载时把目录的组改成指定 GID 并赋予组读写权限。NFS 场景下这个机制的支持程度取决于 provisioner 和底层实现不是所有环境都生效需要实测。次选provisioner 创建目录时指定 UID/GID。nfs-subdir-external-provisioner 支持在 StorageClass 里配置onDelete和目录权限也可以在 provisioner 的 Deployment 里设置运行用户让生成的目录属主就是应用用户。最后服务端配置no_root_squash或者把目录权限放到 777。前者等于放弃了 NFS 的权限隔离后者是安全问题都不建议在生产用。真到了只能用这两招的地步说明存储方案本身需要重新评估。6.5 NFS 不适合承载的东西明确几个禁区能省掉很多事故数据库数据目录NFS 的文件锁实现跟本地文件系统差异很大数据库依赖的fcntl锁在 NFS 上行为不可靠轻则性能暴跌重则数据损坏。数据库要么用云盘要么用本地 SSD 主从。高频追加写的日志前文说过会拖垮整个 NFS 服务端。大量小文件的随机读写NFS 每次操作都有网络往返小文件场景下延迟放大非常明显。压缩包、大附件这种顺序读写的大文件才是它的舒适区。需要跨 Pod 强一致锁的场景NFS 的锁机制在容器环境下表现不稳定分布式锁请用专门的组件。7. Rancher 6.3 上把工作负载编排起来7.1 三层结构集群、项目、命名空间Rancher 的层级比原生 k8s 多了一层项目。项目是 Rancher 自己的概念用来把若干命名空间打包做统一的权限和配额管理。实际使用中我按业务线 项目环境 命名空间来分比如订单业务项目下有order-dev、order-staging、order-prod三个命名空间。这样分的好处是资源配额可以按项目设置一个业务线吃满了不会影响其他业务线权限也能按项目授予开发只能进 dev 和 stagingprod 需要单独申请。不同版本的 Rancher 在界面位置上会有差异但底层都是生成标准的 k8s 资源所以下面的 YAML 在任何版本上都通用。习惯写 YAML 之后我更少用界面操作因为 YAML 能进代码库、能 review、能 diff。7.2 一份完整的工作负载定义apiVersion: apps/v1 kind: Deployment metadata: name: order-service namespace: order-prod spec: replicas: 3 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: order-service template: metadata: labels: app: order-service version: v1 spec: imagePullSecrets: - name: harbor-cred terminationGracePeriodSeconds: 45 containers: - name: app image: harbor.example.internal/prod/order-service:release-3f8a1c2-20240612 imagePullPolicy: IfNotPresent ports: - containerPort: 8080 env: - name: POD_IP valueFrom: fieldRef: fieldPath: status.podIP - name: JAVA_OPTS value: -XX:MaxRAMPercentage70 -XX:UseG1GC resources: requests: cpu: 500m memory: 1Gi limits: cpu: 2 memory: 1Gi startupProbe: httpGet: path: /actuator/health/readiness port: 8080 failureThreshold: 30 periodSeconds: 5 livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 periodSeconds: 10 failureThreshold: 3 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 periodSeconds: 5 failureThreshold: 2 lifecycle: preStop: exec: command: [sh, -c, sleep 15] volumeMounts: - name: app-data mountPath: /app/data/upload subPath: upload volumes: - name: app-data persistentVolumeClaim: claimName: order-service-data几个参数的选择逻辑说明一下。maxUnavailable: 0配maxSurge: 1意思是滚动更新时先起一个新的等它就绪了再停一个旧的全程可用副本数不低于设定值。代价是更新过程中会多占一份资源好处是任何时刻都有足够的实例接流量。对可用性要求高的服务应该这么配。startupProbe的failureThreshold: 30×periodSeconds: 5 150 秒意思是给应用最多 150 秒的启动时间。Spring Boot 应用连上 Nacos、初始化数据源、预热缓存超过这个时间的不少。这个值要按实际情况调给短了会导致启动中被误杀陷入CrashLoopBackOff。requests和limits的内存设成一样是为了避免节点内存超卖导致 OOMKilled。CPU 可以设得不一样requests决定调度时的资源分配limits决定运行时的上限。CPU 可压缩内存不可压缩所以内存必须锁死。7.3 探针参数怎么算探针参数不是拍脑袋定的有几个基本关系startupProbe的总时长failureThreshold × periodSeconds必须大于应用的最长启动时间留 30% 余量livenessProbe的failureThreshold × periodSeconds决定了故障检测的灵敏度设太短容易误杀太长故障恢复慢。10 秒 × 3 次 30 秒是个平衡点readinessProbe可以比 liveness 更灵敏5 秒 × 2 次 10 秒依赖恢复后能快速重新纳入流量。另外initialDelaySeconds在使用startupProbe之后就不需要了因为 startupProbe 成功之前 liveness 和 readiness 都不会执行。7.4 发布与回滚Rancher 界面上有版本历史和回滚按钮底层对应的是 ReplicaSet 的保留。默认revisionHistoryLimit是 10意味着能回滚到最近 10 个版本。如果发版频繁建议调大一点20 比较合适。命令行回滚# 看历史 kubectl rollout history deployment/order-service -n order-prod # 回滚到上一个版本 kubectl rollout undo deployment/order-service -n order-prod # 回滚到指定版本 kubectl rollout undo deployment/order-service -n order-prod --to-revision3 # 观察滚动过程 kubectl rollout status deployment/order-service -n order-prod回滚之前先想清楚一件事配置回不回滚镜像回滚了但 Nacos 上的配置还是新的两者版本不匹配可能引用到不存在的字段。所以配置变更最好跟代码发布绑定打同一个版本标记回滚时一起回。8. 上线后最常撞上的几类故障与排查链路8.1 Pod 卡在 ImagePullBackOff先看事件这是信息量最大的地方kubectl describe pod pod名 -n order-prod | tail -30事件里会明确写出是 401、not found 还是证书问题对照第 3.4 节的表格定位。如果事件里显示的是no such host先排除 DNS如果是unauthorized检查 secret 是否在同一个命名空间、用户名密码是否正确机器人账号的 token 有没有复制全。有一个隐蔽情况secret 建的时候用的--docker-server跟镜像地址里的域名不完全一致。比如 secret 里写的是harbor.example.internal镜像地址写的是harbor.example.internal:443k8s 认为这是两个不同的仓库凭据匹配不上。端口号要么都带要么都不带。8.2 启动成功但注册中心里没有实例按这个顺序查先确认应用日志里有没有注册成功的记录。如果没有看注册中心的地址、命名空间、分组配得对不对检查网络连通性。从 Pod 里curl一下注册中心的地址容器的网络策略可能限制了出向流量看注册上去的 IP 是不是 Pod IP。如果是 127.0.0.1 或者某个内网地址说明网卡选错了按第 5.1 节的方式指定看分组对不对。客户端注册的分组和服务端查询的分组不一致等于在同一个注册中心的两个平行世界里谁都看不见谁。最后一种最难查因为两边都正常工作就是不互相认。有个小技巧在注册中心控制台上按服务名搜索确认实例确实存在然后检查消费方配置的分组。8.3 配置改了但应用读到的还是旧值分几种情况如果完全没生效先确认命名空间 ID 填对了第 4.4 节再看 dataId 和 group 是否完全匹配大小写敏感如果重启后生效、热更新不生效检查RefreshScope有没有加或者有没有配置refreshEnabled如果部分字段生效部分不生效检查那些字段是不是静态的、或者是不是在 Bean 初始化时就固化了。ConfigurationProperties覆盖不到的地方通常是因为那个值在构造函数里就被读走了。8.4 挂载目录写不进去按这个顺序排查# 1. 确认挂载成功了 kubectl exec -it pod -n order-prod -- df -h | grep app # 2. 看目录属主和权限 kubectl exec -it pod -n order-prod -- ls -ld /app/data/upload # 3. 用应用用户试写 kubectl exec -it pod -n order-prod -- su -s /bin/sh app -c touch /app/data/upload/test.txt如果第 1 步就看不到挂载点说明 PVC 没绑定上回去看 PVC 状态如果看得到但属主是nobody是root_squash导致的按第 6.4 节处理如果是Read-only file system检查 PV 的accessModes是不是写成了ReadOnlyMany。8.5 反复重启 CrashLoopBackOff# 看上一次崩溃的日志 kubectl logs pod -n order-prod --previous--previous这个参数很多人不知道它能看到容器上一次退出的日志。如果应用启动阶段就崩了当前的日志可能是空的必须加这个参数。常见原因连不上配置中心并且配了快速失败、数据源参数错误导致连接池初始化失败、堆内存超出 limits 被 OOMKilledkubectl describe pod里会有Last State: Terminated, Reason: OOMKilled、启动时间超过 startupProbe 的容忍范围被强制杀掉。区分 OOMKilled 和进程自身退出很简单OOMKilled 会在 Pod 状态里明确标注退出码是 137进程自身异常的退出码通常是 1 或者具体的异常码。8.6 一份速查表现象第一条命令高频根因ImagePullBackOffkubectl describe pod凭据不在同命名空间 / 域名带端口不一致CrashLoopBackOffkubectl logs --previous配置中心连不上 / 堆内存超限Running 但不 Readykubectl describe pod看探针readiness 路径错 / 依赖未就绪注册中心没实例进 Podcurl注册中心分组不一致 / 网卡选错配置不生效对比 dataId 与命名空间 ID回退到 public / 前缀缺失写入 Permission deniedls -ld挂载目录root_squash / fsGroup 未生效滚动发布期间大量超时kubectl get endpointspreStop 太短 / 未主动注销这套链路我前后搭过三四个环境最大的感受是大部分故障都不是某个组件坏了而是两个组件之间的约定没对齐。镜像地址和 secret 里的域名要一致Nacos 的命名空间 ID 要一致注册中心和消费方的分组要一致容器内用户和 NFS 目录属主要一致。每个一致单看都是小事凑在一起就是一套能自愈的系统。还有一个实操上的建议把上面这份 Deployment、StorageClass、bootstrap.yml 整理成一个模板仓库新服务上线直接改名字复制。配置项多了之后靠记忆去对齐这些约定迟早会出错把它固化成模板出错概率能降一个数量级。最初的模板可能只有二十行跑上一年会慢慢长到几百行每一条都是某次事故换来的别嫌它臃肿。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询