Spring Cloud与Kubernetes集成实践:服务发现、配置管理和网关的边界
2026/9/19 3:30:35 网站建设 项目流程

简介:面向微服务架构落地与容器化改造的实践型方案讲解,围绕Kubernetes与Spring Cloud的互补关系,梳理服务发现、负载均衡、熔断隔离、配置管理、CI/CD等关键环节在真实业务中的组合用法。以网易云容器平台管理30余个微服务、每周400多次构建部署的实战为案例,呈现基础设施层、容器引擎层、K8s编排层、DevOps自动化层的分层设计,并给出技术选型建议与实施路径。包内为单个docx文档,共1个文件,约527KB,便于阅读与标注;已有203人学习,适合有一定Java基础和容器使用经验、正在规划微服务架构或准备引入K8s的工程师与架构师。文档重点解答K8s去中心化服务发现与Spring Cloud Eureka方案的差异,并针对高可用、故障恢复、灰度发布等场景提供综合解决思路,可帮助读者减少自行摸索成本、明确两套框架的职责边界。

1. 微服务化实践:当 Spring Cloud 遇上 Kubernetes,先想清楚谁在做服务发现

一个常见的团队争吵场景:业务已经用 Spring Cloud 跑了两三年,Eureka 注册中心、Zuul 网关、Config Server 配置中心一应俱全。现在运维说要上 Kubernetes,于是第一轮会议就在争论——K8s 自带 Service 发现和负载均衡,是不是可以把 Eureka、Ribbon 甚至 Zuul 全部撤掉?这是个好问题,但答案远比“能”或“不能”复杂。Kubernetes 的 Service 工作机制在传输层,Spring Cloud 的注册中心机制在应用层,两者解决的是不同精度的流量治理问题。这篇博客按一线落地视角,把这套组合的边界、取舍、配置和排错梳理清楚,适合正在做容器化迁移或从零规划微服务架构的团队参考。

2. Spring Cloud 与 Kubernetes 的核心概念与能力边界

2.1 两层服务发现:谁在管哪一层的流量

很多团队对“Kubernetes 自带服务发现”的理解是:我把 Spring Cloud 服务打包成镜像,在 K8s 里创建若干个 Deployment,每个 Deployment 配一个 Service,然后服务之间用 Service 名互相调用就完事了。这个理解没有错,但不完整。Kubernetes 的 Service 发现是七层之下的:kube-proxy 通过 iptables 或 IPVS 规则把 Service VIP 转发到某个 Endpoint 上的 Pod IP。应用自身感知不到对端实例,只看到一个稳定的虚拟地址。

而 Spring Cloud 的服务发现,比如通过 Eureka 或 Nacos,暴露的是应用层视角:服务名对应多个实例地址,实例有状态,有心跳,有下线通知,可以选择负载均衡策略,并且在调用失败时由客户端侧重试或熔断。这两套机制的差异,直接决定了它们不是替代关系。如果把注册中心撤掉,走 K8s Service 的 ClusterIP,流量分发粒度是 TCP 连接级,你无法在 HTTP 层拿到精确的每个请求的状态码来判断该不该重试。

业界常见的做法是两者并存。Kubernetes 负责容器编排、资源调度、副本管理、滚动发布;Spring Cloud 保留应用层的服务治理能力,比如 Nacos 里的权重、保护阈值、服务分组。我在实际项目中见过几种不同派别的架构判断,最保守的是全量保留 Spring Cloud 全家桶,最激进的是连 Feign 都不用了,直接 RestTemplate 加 Service 名访问。这两个极端我都踩过坑。

2.1.1 Kubernetes 的探针与 Spring Boot 健康检查怎么对接

容器编排侧最核心的概念是探针。Kubernetes 的 livenessProbe 决定何时重启容器,readinessProbe 决定何时把流量打进来。Spring Boot 的 Actuator 提供了 /actuator/health 端点,但直接把它接到探针上是不够的。因为 Actuator 默认只检查应用自身的存活,不会感知到依赖的下游。Spring Boot 2.3 之后引入了可用性探针,通过配置 management.endpoint.health.probes.add-additional-paths 暴露 /actuator/health/liveness 和 /actuator/health/readiness,并在 2.4 版本之后把 probes 相关逻辑做了默认化。

我在生产环境里的配置模板一般是这样的:

livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 3 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 15 periodSeconds: 5

关键参数是 initialDelaySeconds,它给 Spring Boot 足够的时间完成数据库连接池初始化和配置拉取;readiness 的 periodSeconds 要比 liveness 短,这样新实例就绪状态的反馈更快。如果你的 Spring Boot 版本在 2.3 以下,需要把探针打到应用自己的健康检查端点,但务必排除掉 Redis、数据库这些外部依赖的健康明细项,否则一次数据库抖动会让整个 Pod 被反复重启。

2.2 配置管理的重定位:从 Config Server 到 ConfigMap 与 Kubernetes Secrets

Spring Cloud Config 的初衷是把配置从代码里剥离出来,集中管理。但在 K8s 环境里,ConfigMap 和 Secret 成为了更底层的配置载体。很多团队纠结要不要把 Config Server 下掉。我的判断是:看你的配置变更频率和是否需要动态刷新。如果配置跟随环境走,且发布节奏和镜像构建对齐,直接上 ConfigMap 挂载到文件系统,比 Config Server 少一层网络依赖。

ConfigMap 挂载到 Spring Boot 应用时,有一个很大的坑:Spring Boot 默认从 application.properties 读取配置,如果 ConfigMap 的挂载路径不对,应用会启动失败。常见做法是把 ConfigMap 的 key 写成 application.properties 或 application.yml,然后挂载到 /config 目录,并在启动命令中通过 --spring.config.additional-location 指向这个目录,这样既保留了默认配置兜底,又能让外部配置覆盖内部配置。注意,这个方案在配置文件名冲突时以 additional-location 为最高优先级,别把容器里本身存在的 application.yml 给覆盖了。

配置的动态刷新在 K8s 场景有两种路径:一是用 Spring Cloud Kubernetes 的 config-watcher,监听 ConfigMap 变更后通过 Spring Cloud Bus 或 actuator 的 refresh 端点通知应用;另一种是直接把配置放到 Nacos,让 Spring Cloud Alibaba 的机制处理动态刷新。后一种在大家已经用了 Nacos 的情况下更省事,因为 ConfigMap 的变更事件从 kubelet 传到容器内文件系统有一定延迟,在极端情况下可达数秒甚至更长。

3. 注册中心的取舍与负载均衡策略调整

3.1 Eureka、Nacos 能不能撤:三个判断维度

第一个维度是服务间调用的治理精度。K8s Service 自带的负载均衡是 kube-proxy 的转发规则,它在 Pod 重启后能很快剔除 Endpoint,但对 HTTP 层的 503、超时这类错误感知不到。Feign 加 Ribbon 或 Spring Cloud LoadBalancer 则能在客户端做请求级重试。第二个维度是多集群场景。企业项目实战中,一个应用可能横跨多个 K8s 集群,Service 的 DNS 解析只在本集群内生效,跨集群调用必须有服务网格或注册中心来处理,比如通过 Nacos 同步多集群的实例信息。第三个维度是灰度发布。Deployment 可以通过 maxSurge 和 maxUnavailable 做滚动发布,但如果需要精准的按权重、按请求头分流,K8s 原生 Service 做不到,必须依赖网关层或注册中心的能力。

如果团队对这三个维度都有明确的承接方案,比如用 Service Mesh 的 VirtualService 做按 header 路由,那确实可以把注册中心下掉。但我见过不少团队拆了 Eureka 之后测试发现,服务重启瞬间,调用方的连接池里还缓存着旧 IP,一旦连接建立失败,需要等一个 TCP 超时周期才能重试,这个等待时间在三到五秒之间,对线上体验是灾难。

3.1.1 保留 Nacos 时的负载均衡策略配置

当我们决定保留 Nacos,用 Spring Cloud Alibaba 体系,负载均衡策略需要使用 Spring Cloud LoadBalancer。在 Spring Cloud 2020.0 之后,Ribbon 已被移除,原本的 IRule 配置方式不再生效。此时要自定义负载均衡规则,需要实现 ServiceInstanceListSupplier 接口。一个经典做法是通过 Nacos 返回的实例元数据里带权重,结合 RoundRobin 或 Random 策略组合。

@Configuration public class LoadBalancerConfig { @Bean public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier( ConfigurableApplicationContext context) { return ServiceInstanceListSupplier.builder() .withDiscoveryClient() .withWeighted() .withHealthChecks() .build(context); } }

这段配置的核心是 withWeighted(),它会根据 Nacos 注册时携带的 weight 元数据做加权随机。注意,这里的健康检查逻辑默认走 Spring Boot Actuator 的 /actuator/health 端点,但请求频率受缓存时间控制。我一般建议把缓存时间调成 5 秒以上,否则健康检查请求会在高并发下迅速打满实例。参数配置项是 spring.cloud.loadbalancer.cache.ttl,单位是秒。

3.2 Kubernetes Service 的会话保持与 Spring Cloud 的会话冲突

在微服务化改造过程中,最容易忽略的是会话状态。传统单体应用里,Session 存在应用内存里,用户请求通过负载均衡的 sticky session 打到同一台机器。K8s 环境下,Pod 随时可能被重新调度,如果继续依赖实例内存保存 Session,用户会频繁掉线。常见做法是使用 Redis 存储 Session,Spring Session 提供了现成的支持,只需引入 spring-session-data-redis 并把 Cookie 的序列化策略配置好。

K8s Service 的 sessionAffinity 参数设置为 ClientIP 时,kube-proxy 会尽量把同一个客户端 IP 的请求转到同一个 Pod,但这只在四层有意义。如果服务间调用是 HTTP 且使用了网关,网关到下游服务这个环节的客户端 IP 是网关节点的 IP,sessionAffinity 就失效了。所以在 K8s 和 Spring Cloud 的组合里,设计原则是无状态化,把所有会话状态下沉到外部存储,而不是依赖任何一层的亲和性策略。

4. Spring Cloud Gateway 与 Kubernetes Ingress 的职责划分

4.1 Ingress 负责南北流量接入,Gateway 负责东西与策略

一个企业级微服务架构里,最外层通常是一个负载均衡器或云平台的 SLB,然后是 Kubernetes Ingress,负责按域名把流量分发到对应的 Service。在 Ingress 层到微服务应用之间,还有一个重要的位置任务:鉴权、限流、路由重写。这个位置由 Spring Cloud Gateway 或同类网关来承接。关键要理解它不是和 Ingress 竞争流量入口,而是和 Ingress 分层协作。

Ingress 通常用 Nginx Ingress Controller,它在性能上表现优秀,能处理 TLS 终结、WebSocket 升级、静态路由。但 Nginx 的 Lua 脚本能力有限,实现复杂的灰度发布逻辑相对繁琐。Spring Cloud Gateway 是响应式网关,基于 Netty 和 WebFlux,天然适配 Spring Cloud 生态,可以轻松读取 JWT 并解析出用户角色,然后根据角色把请求路由到不同的服务版本。这就是一个典型的分层协作:Ingress 认域名,Gateway 认用户身份和业务语义。

提示:在流量链路里不要叠太多网关层。我见过一个项目里,请求依次经过 SLB、Ingress、Gateway、Service,链路长到排障时一脸茫然。Ingress 加 Gateway 已经够多,如果再有别的网关,建议架构评审时直接拒绝。

4.2 Spring Cloud Gateway 与 Sentinel 结合做流量防护

热词里提到的 Sentinel 和 Gateway 的组合,是当前企业项目实战中很标准的一套方案。Sentinel 可以通过接入 Spring Cloud Gateway 的适配器,为路由规则配置流控、熔断和系统保护。关键在于参数设计。

先引入依赖,然后在网关的配置文件里做规则初始化:

spring: cloud: gateway: routes: - id: order-service-route uri: lb://order-service predicates: - Path=/api/order/** filters: - StripPrefix=1 - name: SentinelGatewayFilter args: fallback-url: /fallback/order

规则在代码里加载:

@PostConstruct public void initGatewayRules() { FlowRule flowRule = new FlowRule(); flowRule.setResource("order-service-route"); flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS); flowRule.setCount(1000); flowRule.setLimitApp("default"); FlowRuleManager.loadRules(Collections.singletonList(flowRule)); DegradeRule degradeRule = new DegradeRule(); degradeRule.setResource("order-service-route"); degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_RT); degradeRule.setRt(200); degradeRule.setTimeWindow(10); DegradeRuleManager.loadRules(Collections.singletonList(degradeRule)); }

注意这里的代码逻辑:FLOW_GRADE_QPS 表示按 QPS 限流,count 是每秒最大通过的请求数;DEGRADE_GRADE_RT 表示按响应时间熔断,当接口的平均响应时间超过 200 毫秒且持续一段时间后,触发熔断,timeWindow 是熔断窗口的秒数。规则以网关路由 id 作为资源名,这意味着一个路由下的所有接口共享同一个流控阈值。如果接口之间差异大,建议按 Path 维度拆开路由,而不是把整条路由做成一个保护单元。

4.3 网关层的调用链监控与排错思路

Gateway 作为所有请求的必经之地,如果这里打点不全,后面排障就难了。使用 Spring Cloud Sleuth 或者 Micrometer Tracing 可以给每个请求分配 traceId,在网关的 Filter 里把它注入到请求头往下游传递。日志中记录这个 traceId 和耗时,排查时按 traceId 聚合日志。为了拿到准确的耗时拆解,我习惯在 Gateway 的前置和后置 Filter 各打一次时间戳,同时在响应头里回写 X-Trace-Id,这样前端排查单个请求时可以直接给我们报一个 id。

常见的网关问题集中在超时配置上。Spring Cloud Gateway 默认的响应超时时间受 Netty 的全局配置控制,一个设置不当的参数会导致所有下游慢接口全部超时。全局配置如下:

spring: cloud: gateway: httpclient: connect-timeout: 3000 response-timeout: 10s server: netty: connection-timeout: 3000

connect-timeout 的单位是毫秒,response-timeout 支持 Duration 格式。这两个值不是随便拍的。如果下游有批量查询接口,正常响应就超过 5 秒,那么网关 3 秒的超时必然误伤。对待这类问题,建议给特定路由单独配置超时,覆盖全局默认值。

5. 微服务化改造中的容器编排落地路径

5.1 从 Spring Boot 到镜像构建的最佳实践

把 Spring Boot 服务容器化的第一步是镜像构建。很多团队直接从 Dockerfile 里把 jar 包复制到 jre 基础镜像,做法不够精细。合理考虑时区、编码、内存配置和基础镜像选择。

推荐多阶段构建的思路,以下是生产里用过的模板:

FROM maven:3.8.8-eclipse-temurin-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests FROM eclipse-temurin:17-jre ENV TZ=Asia/Shanghai RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone WORKDIR /app COPY --from=builder /build/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"]

第二阶段镜像里没有 Maven,没有编译残留,运行时镜像体积会大幅缩小。MaxRAMPercentage=75 是 JVM 在容器内自动识别内存上限的推荐参数,它让堆大小跟随容器的 limit 动态调整,避免在 K8s 里设置固定 Xmx 导致 OOMKilled。很多人踩过的坑是只设置了内存 limit 但没设置这个 JVM 参数,结果容器被 OOM 了而 JVM 的 Heap 还没触发 Full GC。

构建完成后我们要打上镜像 tag 并推送到私有仓库,这里镜像 tag 策略要慎重。我坚持用 git commit SHA 作为 tag,不用 latest。因为 K8s 的镜像拉取策略 ImagePullPolicy 设置为 IfNotPresent 时,latest 会导致每个节点上相同的 tag 不会重新拉取,生产环境使用固定 SHA 最稳妥。

5.2 一次性写对 Deployment 配置:探针、资源、滚动发布

Deployment 是 Kubernetes 里面最核心的工作负载概念,它同时管理多副本、自愈、滚动更新三件事。下面是微服务场景的一个标准配置模板:

apiVersion: apps/v1 kind: Deployment metadata: name: order-service spec: replicas: 3 selector: matchLabels: app: order-service strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: metadata: labels: app: order-service spec: containers: - name: order-service image: registry.example.com/order-service:20240521.01 imagePullPolicy: IfNotPresent ports: - containerPort: 8080 protocol: TCP resources: requests: cpu: 500m memory: 512Mi limits: cpu: "2" memory: 2Gi readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 20 periodSeconds: 5 failureThreshold: 3 livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 60 periodSeconds: 10 failureThreshold: 3 lifecycle: preStop: exec: command: ["sleep", "20"]

这里几个参数需要重点解读。maxSurge=1 表示滚动发布时最多允许超出期望副本数一个 Pod;maxUnavailable=0 表示永远不允许多个副本同时不可用。这个组合的发布速度偏保守,但流量无损。preStop 里 sleep 20 是为 Spring Cloud 服务的优雅下线赢得时间,后面会细说。

资源设置方面,requests 是调度时使用的规格,limits 是运行时限制。不要只设置 limits 不设置 requests,否则调度器无法判断节点是否装得下,容易导致节点过载。requests.cpu 和 limits.cpu 的单位换算:500m 是半核,1000m 是一核。如果想用自动扩缩容,还需要配 HPA,它的核心参数是 targetCPUUtilizationPercentage,当 CPU 实际使用率超过目标比例,扩容 Pod 数量。

5.2.1 用 HPA 与 Vertical Pod Autoscaler 的边界决定

HPA 基于 Pod 的 CPU 和内存使用率或自定义 metrics 扩缩容,响应尺度是分钟级。对于突发流量,HPA 的扩容存在观察窗口,通常默认 30 秒以上的冷启动时间。应对突发流量的常见做法是设置好 minReplicas,保证基础水位,同时把 HPA 的 scaleUp 策略调成 immediate,并配合 metrics 的容差为 5%,让流量一涨就扩。

Spring Cloud 微服务做 HPA 时必须注意 Feign 连接池大小和线程池大小。如果服务副本数突增,但连接池配置偏小,扩容带来的收益会被线程阻塞抵消。我一般建议把 HPA 与网关层限流联动。网关配好每台实例的 QPS 上限后,根据目标单实例能扛的峰值流量,推算出总副本数需求,再给 HPA 一个最大量约束。这个规划思路能避免扩容到很高数量后,数据库连接数被打爆。

5.3 Kubernetes Dashboard 在微服务运维中的实际定位

搜索热词里有 Kubernetes Dashboard,这里说下它在微服务化实践中的价值。Kubernetes Dashboard 是官方维护的 Web UI,它让人可以直观地查看 Deployment、Pod、Service、ConfigMap 的状态。但要注意:它不是标准的运维入口。生产环境我建议只给运维和开发人员开只读权限,避免误操作导致线上事故。

通过 Dashboard 创建新 Pod 作为新服务发布,这个操作从技术上是可行的,但不推荐。原因在于 Dashboard 创建的是独立的 ReplicationController 或裸 Pod,它没有 Deployment 的滚动升级、回滚能力。企业项目实战中,服务应该通过 CI/CD 流水线发布,把镜像 tag 更新到 Deployment 的 manifest 上,Dashboard 只作为可观测的辅助工具。如果团队没有 CI/CD 系统,最低限度的做法是维护好各个环境下的 yaml 文件,执行 kubectl apply 发布,而不是靠 Dashboard 点击操作。

6. 优雅下线的最后一块拼图:让流量先走再收容器

6.1 Kubernetes 与 Spring Cloud 对下线时刻的认知偏差

Pod 被删除时,Kubernetes 的执行顺序是先触发 preStop 钩子,再发送 SIGTERM 信号,等 grace period 结束后发送 SIGKILL。而 Spring Cloud 服务收到 SIGTERM 后,Spring 容器开始关闭,包括把当前实例从注册中心反注册。两个容器编排侧和应用侧的动作是并行还是串行,决定了下线流程是否会切到正在处理的请求。

实际问题在于时序。Kubernetes 在发送 SIGTERM 之前,Service 的 Endpoints 已经把这个 Pod 从转发列表里摘除了,但这里存在两个已知延迟。第一,kube-proxy 在所有节点上同步新的转发规则需要时间;第二,Nacos 或 Eureka 服务端感知到实例下线并通知所有客户端也有延迟。如果在摘除动作完成前就杀掉了 Pod,那些还持有旧缓存或还没刷新规则的下游客户端,会把请求发到一个正在关闭的应用上,结果出现 connection refused。

6.2 preStop 加 sleep 与注册中心的配合方式

解决这个问题的通用做法是把 preStop 命令设置为等待,给摘除动作留出缓冲。刚才的 Deployment 配置里已经出现过 sleep 20。预设的经验值是多少?这个时间取决于两个因素:K8s 集群规模造成的规则收敛时间,以及注册中心的心跳间隔。Nacos 的临时实例是最佳实践默认值,客户端下线时主动向服务端发送注销请求,通常在秒级。如果只调了 sleep,不去触发注册中心的主动下线,sleep 的意义就减半。

更完整的方式是同时做到多件事:preStop 里先调用注册中心的注销接口或依赖 Spring 的优雅下线机制,再 sleep 若干秒,让已经建立的长连接跑完存量请求。Spring Boot 2.3 之后的优雅停机配置也很关键,在 application.yml 里设置 server.shutdown=graceful,配合 spring.lifecycle.timeout-per-shutdown-phase=20s,Spring 会等待当前请求处理完再关闭容器。线上配置是 preStop sleep 20 加上 graceful shutdown 超时 20 秒,双保险,既切流量又等存量请求完成。

6.3 验证优雅下线的完整操作与常见问题

线上要验证这个逻辑,最简单的办法是持续向服务发压,观察灰度发布时的错误率曲线。用 kubectl rollout restart deployment/order-service 触发滚动发布,在压测日志里抓取状态码,精细到 5 秒粒度。如果出现零星 502,多半是 gateway 到下游的 TCP 连接被重置。

当应用主动下线能力已经内置时,kubectl delete pod 的发布方式与滚动发布有区别:前者是直接移除 Pod,后者会先起新 Pod 再删旧 Pod。滚动发布本身对流量干扰小,因为副本数有 minReadySeconds 保障。而 manual delete pod 是我做故障演练时常用的手段,它用于验证应用在随机丢 Pod 时是否自愈。

排障时如果发现滚动发布总是有微量错误,一个方向是调整 DNS 的缓存配置。Pod 内使用的 coredns 缓存了 Service 服务的上游地址,默认的 ndots 和缓存机制可能导致解析到了一个刚删除的 IP。在 K8s 里配置 dnsConfig 的 ndots: 2 或调整 CoreDNS 的缓存 TTL,也是微服务化实践中被反复验证过的一个方案。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询