Spring Cloud与Kubernetes叠加的微服务治理落地实践 📅 发布时间:2026/9/19 14:43:04 👁 浏览次数: 简介面向微服务架构设计与落地团队的专题文档基于 Kubernetes 与 Spring Cloud 两大主流技术栈深入剖析网易云容器平台的微服务化实践路径。内容覆盖容器与微服务的互补关系、服务发现与治理、负载均衡、集群容错、配置管理等关键议题并结合 30 微服务每周 400 次构建部署的一线经验梳理出基础设施层、容器引擎层、K8s 编排层、DevOps 工具层的分层架构。文档还对比了 Spring Cloud 带注册中心方案与 K8s 去中心化服务发现方案的异同并从面向终端用户、数据一致性要求、同步异步通信、计算型或 IO 型任务等维度拆解不同微服务形态的实际管理策略。资源共 1 个 docx 文件约 527KB虽体积精简但内容密度较高。已有 203 人学习适合正在做容器化改造、微服务拆分或 DevOps 平台建设的 Java 工程师、架构师参考可快速获取一线团队的技术选型思路与踩坑经验。1. 微服务化为什么绕不开Spring Cloud与Kubernetes的叠加微服务化做到一定规模团队迟早会遇到同一个问题Spring Cloud把服务治理做得很顺手Kubernetes又把容器编排的能力铺到了基础设施层两套体系看似都能做服务发现、负载均衡和配置管理到底该让谁负责哪一块如果只是简单地把Spring Cloud应用塞进Pod里跑起来那只能算容器化离微服务化还差得很远。真正意义上的微服务化实践需要先把治理边界划清楚再决定改造路径最后才能让整个系统在故障、流量波峰和版本迭代面前保持稳定。这篇内容面向的是已经在用Spring Cloud做业务开发、同时开始接手Kubernetes集群的工程师。你会看到一套从服务拆分、镜像构建、资源编排到流量治理的完整落地顺序以及我在生产环境里踩过的几个关键坑。没有教科书式的架构图只有可以直接抄走的YAML、配置和命令以及每个步骤背后的取舍理由。2. 先理清Spring Cloud与Kubernetes的治理边界再动手2.1 服务注册发现Eureka与Kubernetes Service各自负责什么Spring Cloud体系里服务注册与发现通常交给Eureka或Nacos服务提供方启动时把自己的IP和端口注册到注册中心消费方通过应用名拉取实例列表再配合Ribbon或Spring Cloud LoadBalancer做客户端负载均衡。这套机制在虚拟机或物理机部署时代非常好用但到了Kubernetes环境情况发生了变化。Kubernetes内置了Service和Endpoints机制Pod的IP变化由kube-proxy或CoreDNS接管服务名解析和TCP/UDP层的负载均衡由集群自己完成。这就出现了两套注册发现机制并存的情况Spring Cloud应用内部的调用走Eureka集群层面的南北向流量走Kubernetes Service。如果你不做任何调整服务间的HTTP调用依然会走Eureka列表而外部流量进到集群后先经过Ingress或NodePort再转到对应的Service。常见做法是让Eureka只负责Spring Cloud应用之间的客户端发现而Kubernetes Service负责稳定的访问入口和Pod的自动绑定。把Eureka从Kubernetes的机制里剥离出去意味着你的应用在集群里依然需要注册自己但实例IP已经变成了Pod IPPod重建后IP会变注册中心里会产生过期实例记录。能力项Spring Cloud注册中心Kubernetes Service实例级发现支持客户端主动拉取不感知实例只暴露稳定VIP负载均衡客户端侧Ribbon/Spring Cloud LoadBalancerkube-proxy的iptables/ipvs规则健康检查心跳续约就绪探针与端点同步故障摘除注册中心主动剔除探针失败自动摘除Endpoints外部系统集成需要额外暴露API天然支持DNS和ClusterIP如果你的服务全部跑在Kubernetes里没有外部遗留系统需要调用我一般会建议逐步弱化Eureka的作用直接通过Kubernetes Service做服务发现。订单服务调用用户服务时用http://user-service:8080这样的地址由CoreDNS完成解析流量进入Service后打到具体的Pod上。这样做的最大好处是减少了一个中间依赖注册中心不再是可用性的单点。如果暂时不能去掉Eureka那么至少要把Eureka本身的部署搬进Kubernetes用Deployment保证多副本搭配Headless Service让Eureka集群互相发现。注意Eureka的三个节点之间需要稳定的域名通信Headless Service能让每个Pod拿到独立的DNS记录否则集群会因为无法互相注册而分裂。2.2 配置管理Spring Cloud Config与ConfigMap的取舍Spring Cloud Config Server在传统微服务架构里承担着配置中心的角色配置文件存放在Git仓库客户端通过bootstrap.yml拉取远程配置支持配置刷新和加密处理。到了Kubernetes环境配置管理的选择变得微妙起来。Kubernetes原生的ConfigMap和Secret适合管理非敏感和敏感配置通过环境变量或文件挂载的方式注入到Pod中。ConfigMap的更新需要滚动重启应用才能生效除非你自己实现热加载。Spring Cloud Config则支持运行时刷新用RefreshScope标注的Bean可以在/actuator/refresh后重新绑定配置。我的做法是应用本身的业务配置数据库地址、开关项、业务参数继续放Spring Cloud Config保持配置中心和代码仓库联动而部署层面的配置JVM参数、环境标识、日志级别放到ConfigMap里因为它们跟集群调度强相关。两者不是互斥关系Spring Cloud Config Server可以从环境变量读取自己的配置而各微服务应用则保留bootstrap.yml指向Config Server的地址。这里有另一个关键选择Config Server自身的高可用。Config Server本身就是一个Spring Cloud应用部署进Kubernetes后至少跑两个副本并用Service暴露。客户端配置的spring.cloud.config.uri指向这个Service的稳定域名。注意如果Config Server的地址写死成Pod IP留存配置全局替换即可如果客户端在集群外则需要走Ingress或NodePort。apiVersion: v1 kind: ConfigMap metadata: name: order-service-config data: application.yml: | server: port: 8080 spring: datasource: url: jdbc:mysql://mysql-service:3306/order_db username: order_user password: ${DB_PASSWORD} logging: level: com.example.order: DEBUGkubectl create configmap order-service-config --from-fileapplication.yml kubectl set env deployment/order-service --fromconfigmap/order-service-config上面这段命令的含义是先把application.yml写入ConfigMap再用kubectl set env把ConfigMap里的配置以环境变量的形式注入到Deployment中。如果你的应用读取的是文件而不是环境变量可以用volume挂载的方式把ConfigMap映射到/config目录再设置SPRING_CONFIG_LOCATION指向该目录。这里容易踩一个坑ConfigMap的变更不会自动触发Pod重建应用侧不会感知到配置更新。需要手动执行kubectl rollout restart deployment/order-service让配置生效。如果是敏感性配置比如数据库密码Secret比ConfigMap更合适因为Secret可以单独设置RBAC权限避免团队里所有人都有权限看到。2.3 负载均衡与调用链的常见分工Spring Cloud的客户端负载均衡跟Kubernetes Service的服务端负载均衡经常在同一个请求链路里叠加。服务A调用服务B时A的Ribbon从注册中心拿到B的实例列表按照轮询或加权策略选择目标实例然后发起请求。Kubernetes层的Service负载均衡发生在Pod层面的流量转发出入口一般只对集群外部请求或通过Service访问的调用生效。如果你的服务间调用还走Eureka和Ribbon那么请求从PodA到PodB时实际流量已经由kube-proxy做了一次转发但目标IP是明确的PodIP不是ClusterIP。如果走Kubernetes Service发现那请求先到ClusterIP再由iptables或ipvs规则随机转发到某个Pod。两套机制叠加后会出现一种现象Ribbon认为某个实例不可用会主动重试而Kubernetes的Service探针失败后也会摘除端点但这两套健康检查机制彼此不感知。调用链跟踪在Kubernetes环境里的分工比较明确Spring Cloud Sleuth负责生成TraceId和SpanId把链路信息注入到日志和请求头中Zipkin负责收集和展示Kubernetes负责提供Pod级别的标签和元数据方便你在Zipkin里按节点或服务名过滤。把spring.application.name和management.metrics.tags.application配置好就能在Zipkin里直接用应用名检索链路。spring: zipkin: base-url: http://zipkin-service:9411 sleuth: sampler: probability: 1.0采样率在压测或排查问题时可以临时调成1.0生产环境一般0.1就够用全部采样会让Zipkin的存储压力急剧上升。Kubernetes侧不需要为链路追踪做额外配置但建议把Pod的resources.limits和请求的QPS关联起来观察排查延迟问题时把Zipkin数据和Prometheus指标对照看比单看一类数据更容易定位瓶颈。3. 把Spring Cloud应用容器化并生成Kubernetes部署清单3.1 用Dockerfile构建可复现的Spring Cloud镜像服务拆分完成后第一步是把Spring Cloud应用做成镜像。这里要解决两个问题镜像构建的可重复性以及运行时参数的可配置性。先看一个比较干净的Dockerfile。FROM maven:3.8.6-eclipse-temurin-17 AS build WORKDIR /app COPY pom.xml . RUN mvn -q dependency:go-offline COPY src ./src RUN mvn -q -DskipTests package FROM eclipse-temurin:17-jre WORKDIR /app RUN useradd -r -u 10001 appuser COPY --frombuild /app/target/order-service.jar ./app.jar USER appuser EXPOSE 8080 ENTRYPOINT [java, -XX:MaxRAMPercentage75.0, -jar, /app/app.jar]这个Dockerfile的多阶段构建把编译环境和运行环境分离最终镜像里没有Maven和源码体积能控制在200MB以内。注意dependency:go-offline的作用是提前拉取全部依赖让后续的源码拷贝和package阶段不需要再频繁联网本地构建速度会有明显提升。-XX:MaxRAMPercentage75.0这个参数在容器环境里非常重要。如果你直接用-Xmx写死堆内存当Pod的limits.memory调整时JVM不会感知到容器限制容易OOM或浪费内存。设置为75%意味着JVM根据容器可用内存自动算堆上限留出25%给Metaspace、线程栈和JIT编译器。构建时注意如果你的Spring Cloud应用依赖Nacos或Eureka构建阶段不需要连接这些服务它们只在运行阶段通过环境变量注入地址。docker build -t registry.example.com/order-service:1.0.0 . docker push registry.example.com/order-service:1.0.0推进私有仓库后部署时用imagePullPolicy: IfNotPresent避免每次Pod重建都去拉取镜像。本地环境集群可以设置imagePullPolicy: Never来提升冷启动速度。3.2 用Deployment和Service发布第一个微服务Deployment是管理无状态服务最合适的控制器。把Spring Cloud应用看成无状态进程后Deployment的副本数可以弹性伸缩Pod重建不会丢数据。下面这份YAML是部署order-service的最小可用清单同时配好了探针、资源限制和日志输出。apiVersion: apps/v1 kind: Deployment metadata: name: order-service labels: app: order-service spec: replicas: 3 selector: matchLabels: app: order-service template: metadata: labels: app: order-service spec: containers: - name: order-service image: registry.example.com/order-service:1.0.0 ports: - containerPort: 8080 env: - name: SPRING_PROFILES_ACTIVE value: k8s - name: DB_PASSWORD valueFrom: secretKeyRef: name: db-secret key: password resources: requests: memory: 512Mi cpu: 250m limits: memory: 1Gi cpu: 1 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 20 periodSeconds: 10 livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 60 periodSeconds: 30探针路径是Spring Boot 2.3以上版本引入的独立健康分组/actuator/health/readiness表示服务是否能够接收流量/actuator/health/liveness表示进程是否需要被重启。默认的/actuator/health里如果包含Redis、数据库等依赖的检查项一旦这些依赖短暂抖动就可能触发liveness探针失败Kubernetes会替你把Pod杀掉重建造成不必要的重启风暴。把两类探针分组后readiness探针可以包含全部外部依赖liveness探针只检查进程本身的状态。接下来创建对应的Service让其他微服务能够通过稳定域名访问到这些Pod。apiVersion: v1 kind: Service metadata: name: order-service spec: selector: app: order-service ports: - port: 8080 targetPort: 8080 type: ClusterIPService的selector必须跟Deployment的template.metadata.labels完全匹配否则Endpoints会为空。创建后可以通过kubectl get endpoints order-service验证后端Pod是否已成功绑定。注意Service上的port是别人访问这个服务时用的端口targetPort是Pod里容器实际监听的端口两者可以不同。当某个服务对外暴露的端口需要跟容器端口解耦时这个设计就很有用。kubectl apply -f deployment.yaml kubectl apply -f service.yaml kubectl rollout status deployment/order-servicerollout status会阻塞当前终端直到新版本部署完成适合在CI脚本里用来确认发布完成。如果部署失败用kubectl get pods查看哪个Pod没有进入Running状态再用kubectl logs和kubectl describe pod查看事件和容器日志。3.3 用Kubernetes Dashboard创建Pod作为新服务发布对刚接触Kubernetes的团队来说命令行提交YAML有门槛用Dashboard的图形界面创建一个新的Pod用于服务发布是快速验证配置的方式之一。热词里也专门提到了「kubernetes dashboard怎么创建一个新的Pod作为新服务发布」这里给出操作步骤。登录Dashboard后在左侧导航进入「工作负载」页签点击「创建」按钮会看到一个文本框支持直接粘贴YAML。Dashboard的创建过程本质上是把文本框内容提交给Kubernetes API Server跟kubectl apply等价没有超能力但可以省去本地的kubectl配置。进入「工作负载」-「Deployments」点击右上角的「创建」。把上一节写的Deployment YAML粘贴到文本框。点击「上传」后页面会自动跳转到该Deployment的详情页你可以实时查看Pod状态、CPU和内存使用曲线。如果YAML语法或资源定义有错误Dashboard会直接显示错误信息比如error validating data: ValidationError(Deployment.spec) missing required field selector。Dashboard部署新Pod的便捷之处在于你可以在界面上直接查看Pod的容器日志不用记住kubectl logs命令。当Pod一直处在ContainerCreating状态时详情页会显示事件列表常见的失败原因包括镜像不存在、镜像拉取凭证错误、资源请求超过集群剩余量。这些信息在命令行里用kubectl describe也能看但Dashboard把多个界面的信息聚合到了一起团队里的新成员更容易上手。如果你的集群没有部署Dashboard用命令行也能达成同样的目的。Dashboard只是Kubernetes API Server的一个Web客户端它不改变Kubernetes本身的工作方式。发布一个新服务的核心流程永远是构建镜像、定义Deployment、创建Service然后验证Endpoints。4. 用Gateway和Sentinel打通微服务流量治理4.1 Spring Cloud Gateway作为统一入口的配置要点服务拆分后每个微服务有不同的端口和上下文路径直接暴露内部服务地址既不方便统一鉴权也没法做集中限流。Spring Cloud Gateway在Spring Cloud体系里是标准的API网关它基于WebFlux能够把请求路由到下游服务并且可以统一添加过滤器链。网关实例自身作为一个Spring Cloud应用部署到Kubernetes里外部流量通过Ingress进入网关再由网关路由到各微服务。网关和各服务之间走Kubernetes集群内部的Service域名避免流量绕到集群外再回来。spring: application: name: gateway-service cloud: gateway: routes: - id: order-service uri: http://order-service:8080 predicates: - Path/api/order/** filters: - StripPrefix1 - id: user-service uri: http://user-service:8081 predicates: - Path/api/user/** filters: - StripPrefix1 server: port: 8083StripPrefix1的含义是截掉路径上的第一段后再转发到下游。客户端请求/api/order/list时网关先匹配order-service路由截掉/api并把/order/list转发给order-service。如果你的下游服务本身就带有完整前缀比如/api/order那么不需要配置StripPrefix直接设置Path/api/order/**即可。网关在Kubernetes中的部署方式跟普通Spring Cloud服务没本质区别但有几个参数需要特别留意。server.port要跟Deployment里的containerPort保持一致spring.cloud.gateway.routes里配置的URI使用Service域名时不需要写端口前的http://之外的内容建议给网关单独设置比业务服务更大的resources.limits因为网关承担了聚合流量和过滤器逻辑。网关层的过滤器可以处理跨域、JWT校验、请求日志和灰度发布。把JWT校验的逻辑放在全局过滤器里每个下游服务都不需要重复写鉴权代码。灰度发布时可以根据Header或Cookie里的标识把流量路由到新版本服务的Service。4.2 用Sentinel做熔断限流并持久化规则Spring Cloud Gateway承担了流量入口的角色但仅靠网关本身的限流配置远远不够。Sentinel是面向微服务的流量治理组件支持QPS限流、并发线程数限流、熔断降级和系统自适应保护。接入Gateway后Sentinel可以直接对路由维度做限流。资源名在网关场景下默认是路由ID例如order-service。你在Sentinel控制台里对order-service设置一个QPS维度的流控规则阈值设成1000超过的请求会被快速失败或者排队等待具体行为取决于FlowRule里的controlBehavior参数。规则默认保存在内存里Sentinel控制台推送规则后网关实例一旦重启规则就会丢失。生产环境需要把规则持久化到配置中心。常见做法是用Nacos作为规则存储Sentinel的DataSource通过Nacos拉取规则并监听变更。Configuration public class SentinelNacosDataSource { PostConstruct public void init() throws Exception { ReadableDataSourceString, ListFlowRule flowRuleDataSource new NacosDataSource( nacos-service:8848, public, gateway-flow-rules, source - JSON.parseObject(source, new TypeReferenceListFlowRule() {}) ); FlowRuleManager.register2Property(flowRuleDataSource.getProperty()); } }Nacos收到规则变更后推送到应用应用再更新内存中的规则缓存整个过程不需要重启网关。这套方式把规则从应用代码里抽离出来限流阈值的调整完全交给运维和SRE。在生产环境里我还习惯给每个微服务单独加Sentinel依赖而只在网关层做第一道限流。因为网关只能看到经过自己的流量如果某些服务间调用走的是Spring Cloud内部的客户端发现网关看不到这部分流量仍需要在服务端设置兜底限流。4.3 多实例伸缩时网关会话与容错网关和服务成Deployment后副本数伸缩时Pod IP会发生更替。如果业务里有基于Session的登录态需要在网关层做会话保持和Session共享。Spring Cloud Gateway本身不推荐保存用户会话在本地内存可以接入Redis保存Session网关任意一个Pod处理请求都能读取到会话状态。spring: redis: host: redis-service port: 6379 timeout: 3000ms session: store-type: redis会话存储切换到Redis后网关副本数可以从1扩到10用户的登录状态不会丢失。但这里要额外关心Redis的可用性如果Redis故障所有新会话会创建失败已登录用户也拿不到会话。建议Redis部署成主从或哨兵模式或者使用托管实例。网关层的容错涉及重试、超时和熔断。spring.cloud.gateway.httpclient.connect-timeout和response-timeout这两个配置项控制网关与下游服务建连和响应的超时时间默认值偏大在慢依赖场景下会拖垮网关的线程池。spring: cloud: gateway: httpclient: connect-timeout: 1000 response-timeout: 3s把连接超时设为1秒响应超时设为3秒一次下游故障最多占住网关线程3秒。配合Sentinel对路由做熔断当某个下游服务连续错误率达到阈值时后续请求不再进入该服务直接返回兜底内容或快速失败避免故障蔓延。5. 微服务化实践的优雅上下线与本地联调技巧5.1 配置preStop钩子实现实例下线先摘流量发布新版本或手动缩容时Kubernetes会先向Pod发送SIGTERM信号Spring Boot应用收到信号后开始关闭。如果请求正在处理中直接终止会导致用户请求失败或数据不一致。解决这个问题需要配置preStop钩子给应用留出一个处理存量请求的窗口。lifecycle: preStop: exec: command: - sh - -c - sleep 15preStop钩子在SIGTERM发出之前执行sleep 15秒意味着Pod在真正关闭前先等15秒。这段时间内Endpoints已经将该Pod从Service的可用实例列表里移除新的流量不会进来已经处于处理中的请求得以完成。这个15秒并不是拍脑袋定出来的需要结合应用的接口平均响应时间和就绪探针的periodSeconds来确定。如果接口调用链比较长比如一个下单请求涉及订单、库存、支付三个服务整个请求链路耗时可能超过5秒那么preStop的sleep时间建议设置成超过最大响应时间一般10到20秒之间够用。不要设置太短否则长请求仍会被切断也不要设置太长时间因为Pod被标记为Terminating后集群要等preStop执行完才会真正杀掉容器拖长发布窗口。有了preStop钩子还要保证Spring Boot应用在收到SIGTERM后优雅停机。在application.yml里增加下面的配置server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 20sshutdown: graceful让Spring Boot在停机时不再接收新请求等待已处理的请求完成timeout-per-shutdown-phase限制了每次关闭允许等待的最长时长超时后强制终止。把preStop的sleep时间跟这两个参数配合起来就能做到流量摘除、存量请求处理、服务下线三个阶段互不冲突。5.2 本地开发用kt-connect拦截远程服务实现联调微服务化走到后期本地开发会遇到一个典型场景你在本地启动了一个服务想调试一个跨服务接口但本地没有依赖的其他服务实例。常见做法是把所有服务都在本地启动这在服务数量变多后既占内存又会因为环境不一致而出现诡异问题。可以用kt-connect或Telepresence这类工具实现本地服务与Kubernetes集群网络的互通。以kt-connect为例它允许你把本地服务注入到Kubernetes集群中或者把集群里的某个Service流量转发到本地。这样远端集群里的服务调用你的服务的请求可以直接落到你本地的进程里打断点、看日志都和纯本地开发一致。ktctl connect --namespacedefault ktctl exchange order-service --expose 8080:8080第一条命令建立本地到集群的网络隧道让你本地的进程能访问集群内的Service域名第二条命令把集群里名为order-service的Deployment流量全部拦到本地的8080端口。你本地启动了同一个服务的代码后集群内其他服务调用order-service时请求实际打到了本地进程联调效果等同于你直接在集群里运行了这个服务。注意ktctl exchange会替换掉集群里原Deployment的实例相当于临时把该服务的副本数置为0并转发流量。调试结束后需要执行ktctl exit恢复现场。这个机制不适合在多人同时调试同一个服务时使用建议开发环境准备一套独立的集群命名空间专门做联调。如果把preStop钩子、优雅停机和kt-connect组合起来整个团队的开发效率会有一个质的提升日常联调不再需要把全量服务跑在本地发布验证时也不再担心Pod关闭导致的请求中断。这套方案里Kubernetes和Spring Cloud互补的关系贯穿始终治理能力交给框架调度能力交给集群团队要做的只是把这套边界持续维护好。本文还有配套的精品资源点击获取