
以前做微服务,Eureka 或 Nacos 几乎是标配。现在全面上 K8s,不少团队发现硬塞一套注册/配置中心纯属折腾。Spring Cloud Kubernetes 这几年成熟了不少,尤其是 Boot 3.x 和 Spring Cloud 2023 版本对齐之后,直接用 K8s 的 Service 和 ConfigMap 就能把服务治理的活儿干完。这篇把线上跑通的配置、踩过的坑和优雅下线的细节理一遍,全是实打实的生产经验。
1. 架构演进:为什么要把注册/配置中心让给 K8s?
早年用 Nacos 或 Eureka,服务治理确实方便,但容器化之后痛点就出来了。最明显的就是架构冗余,独立部署的注册/配置集群占资源不说,还得专人盯着运维。更麻烦的是时间差问题:K8s 调度器把 Pod 调度到节点、分配 IP,再到应用启动往注册中心报个到,中间有延迟。这段空窗期,注册中心里要么没这个实例,要么状态还是老的,流量打过去直接掉坑里。另外,服务调用多绕一次注册中心拉元数据,链路长了,故障面也跟着扩大。
K8s 原生其实早就把这事解了。Service 配合 EndpointSlice 加 CoreDNS,本身就是声明式的服务发现。kube-proxy(现在基本都用 IPVS 模式)直接在节点侧维护四层负载均衡规则,DNS 解析返回的就是实打实的 Pod IP 列表。Spring Cloud Kubernetes 干的活很实在:不改动 Spring Cloud 的编程模型,把注册和配置治理直接对接到 K8s API Server。底层还是那套 API Watch 机制,但上层开发体验没变,不用额外搭中间件,运维成本直接砍掉一大块。
2. 核心组件集成:依赖、配置与属性映射
Spring Boot 3.x 强烈建议直接用官方 kubernetes-client 系的 Starter。fabric8 那个客户端社区早就停更了,旧依赖在 Boot 3 下各种兼容问题,别给自己埋雷。
2.1 依赖引入
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-client-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-client-discovery</artifactId>
</dependency>
<!-- 替代 Ribbon,必须显式引入 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
2.2 核心配置(application.yml)
yaml
spring:
application:
name: order-service
cloud:
kubernetes:
config:
enabled: true
name: ${spring.application.name}
namespace: ${POD_NAMESPACE:default}
discovery:
enabled: true
all-namespaces: false
primary-service-only: true
lifecycle:
timeout-per-shutdown-phase: 30s
server:
shutdown: graceful
这里 POD_NAMESPACE 是通过 K8s Downward API 注入到环境变量的,别硬编码。primary-service-only: true 能过滤掉 Headless Service 的噪声,只拿标准 Service 的端点。
2.3 配置映射与属性绑定
ConfigMap 里的数据会通过 API 直接灌进 Spring 的 Environment。强类型绑定还是用 @ConfigurationProperties 最稳妥:
yaml
# K8s ConfigMap: order-service
data:
app:
rate-limit: 1000
db-timeout: 5000ms
java
@ConfigurationProperties(prefix = "app")
@Component
public class AppProperties {
private int rateLimit;
private Duration dbTimeout;
// getters/setters 省略
}
服务发现那边不用写任何注册代码。Spring Cloud LoadBalancer 会依赖底层的 KubernetesDiscoveryClient 定期拉取 EndpointSlice 的 IP 列表,你只要在 RestClient 或 WebClient 上打个 @LoadBalanced,调用时自动走本地负载均衡。
3. 配置动态刷新:EVENT 监听与热更避坑
Spring Cloud Kubernetes 支持两种监听模式,默认是 EVENT,线上基本只推这种。POLLING 是定时去拉,除非网络环境特殊,否则别碰。
事件驱动的链路其实不复杂:客户端跟 API Server 建 Watch 连接,ConfigMap 一有改动,Server 推 ADDED/MODIFIED 事件。框架这边触发 KubernetesPropertySourceLocator 重新拉数据,合并到 Environment,然后调 ContextRefresher 发 EnvironmentChangeEvent 和 RefreshScopeRefreshedEvent。挂上 @RefreshScope 的 Bean 就会被销毁,下次调用重新创建实例。
但线上真正跑起来,这里坑最多。@RefreshScope 本质是 AOP 代理加本地缓存,Bean 一重建,里头的本地缓存、定时任务调度、线程池状态全没了。如果配置变更刚好撞上线上高并发,状态丢失直接引发业务异常。稳妥的做法是把易变配置抽到独立的 @ConfigurationProperties 里,跟核心生命周期 Bean 彻底解耦。另外,数据库连接池、Redis 客户端这种底层资源,热更风险极高,要么别放 ConfigMap,要么在 @EventListener(RefreshScopeRefreshedEvent.class) 里自己写逻辑平滑切换,别指望框架自动帮你关旧连接。
K8s 控制面偶尔会抖,一次配置变更可能连推好几条事件。虽然框架内置了去重,但业务侧最好还是加个简单的防抖或者版本号比对,避免无效刷新打满 CPU。
4. 优雅下线:PreStop、连接排空与流量切换
K8s 滚动更新默认发 SIGTERM,但如果不配优雅停机,线上大概率会飘 502/503。很多人以为设个 server.shutdown: graceful 就万事大吉,结果压测一打,旧 Pod 刚收到信号,新流量已经切过去了,直接报错。
真实的排空链路得拆开看:K8s 标记旧 Pod 为 Terminating,同时 把它从 EndpointSlice 摘掉。但 kube-proxy 把规则同步到节点的 iptables/IPVS 表里,是有 5 到 15 秒异步延迟的。流量在 LB 层还没完全切走的时候,容器如果已经开始关 Tomcat/Undertow 了,新请求进来直接拒掉。
工业界的标准解法是在 Deployment 的 preStop 里硬塞一个 sleep:
yaml
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
这 15 秒就是等 LB 层慢慢把旧 IP 踢干净。这段时间容器还活着,健康检查照常返回 200,流量自然被新 Pod 接管。15 秒一过,Spring Boot 的 graceful 机制才开始干活:标记 Web 容器为 PAUSE,拒收新连接,存量请求继续处理,直到 timeout-per-shutdown-phase 设的 30 秒到期强制退出。这里别去瞎改 tomcat.connection-timeout,容易和优雅停机逻辑冲突,默认值就行。
5. 安全与权限:RBAC、ServiceAccount 与 Secret
K8s 默认开 RBAC,Pod 想读 ConfigMap 或监听 Service 变化,必须绑好 ServiceAccount 和权限。别偷懒用默认的 sa,线上一定要最小化授权。
yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: order-sa
namespace: prod
automountServiceAccountToken: true
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: order-config-reader
rules:
- apiGroups: [""]
resources: ["configmaps", "secrets", "endpoints"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: order-rb
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: order-config-reader
subjects:
- kind: ServiceAccount
name: order-sa
namespace: prod
Secret 注入千万别直接扔进环境变量。/proc/1/environ 在容器里是明文可读的,随便哪个 sidecar 或者调试工具都能 dump 出来。生产一律走 volume 挂载,权限压到 0400。密码和证书轮转现在基本都上 Secrets Store CSI Driver 接 Vault 或云厂商 KMS 了,应用无感轮转,比写 reload 逻辑稳定得多。
6. 故障演练与压测基线
这套方案光看配置不够,得拿线上数据说话。强制删 Pod(kubectl delete --force)的时候,能不能靠 preStop + graceful shutdown + 重试机制(比如 Resilience4j Retry)兜住请求,测过才知道。网络分区或者 DNS 解析变慢时,EndpointSlice 同步会卡,这时候建议 Pod 跨 AZ 打散,用 topologySpreadConstraints 控一下分布。Readiness 超时别设太长,LoadBalancer 把重试策略配好,局部网络抖动能自愈。
ConfigMap 要是误删或者格式写错,Spring 启动默认会直接抛异常挂掉。记得在配置里开 spring.cloud.kubernetes.config.fail-fast=false,让应用先带着本地默认值起来保活,同时告警马上打出来,给排查留时间。压测指标看几个硬数:滚动更新期间 5xx 比例能不能压到 0.05% 以下(配合 maxSurge: 25% 和 maxUnavailable: 0);EVENT 模式下配置刷新延迟基本在 1-2 秒内;高并发(比如 500 QPS)下连接池排空成功率得稳在 99.8% 以上。监控埋点直接用 Micrometer,重点盯 Spring Cloud 缓存指标、HTTP 5xx 计数和优雅停机耗时。
7. 生产 Checklist 与经验总结
最后列几个线上容易翻车的点,部署前挨个过一遍:
- 版本必须对齐 :Boot 3.2+ / Spring Cloud 2023.0.x /
kubernetes-client6.8+。别跨大版本混用,Spring Cloud 的兼容性矩阵得严格对照。 - 资源配额别省 :Pod 的
requests/limits必须给足。API Server 有 QPS 限流,Pod 没资源调度慢了,配置拉取超时直接导致启动失败。 - JVM DNS 缓存 :Java 默认 DNS 缓存策略在某些版本里是永久的。K8s 环境下必须加 JVM 参数
-Dsun.net.inetaddr.ttl=30,不然旧 Pod IP 残留在本地缓存,流量全打过去,排空再优雅也没用。 - ConfigMap 没后悔药:K8s 原生不保留 ConfigMap 历史版本。强烈建议走 GitOps(比如 ArgoCD),或者定期打 etcd 快照。线上改配置前,先在非预发环境验证 YAML 语法。
- 多环境隔离 :别靠硬编码区分环境。靠 Namespace 隔离资源,结合
spring.profiles.active加载不同覆盖配置,干净也安全。
把服务发现和配置治理收敛到 K8s 控制面,能省不少中间件运维的活儿,但稳定性靠的不是"上了云原生就自动高可用",而是对 EndpointSlice 同步机制、LB 排空策略和 Spring 生命周期的细节把控。这套方案在千万级 QPS 的生产集群里跑了一年多,坑基本趟平了。照着这个基线走,能避开线上 90% 的启动抖动和发布中断问题。
🎁 福利时间
如果你正在备战面试或者想要学习其他知识,给大家推荐一个宝藏知识库,作者整理了一些列 Java 程序员需要掌握的核心知识,有需要的自取不谢。
知识库地址:https://farerboy.com/
