简介:面向微服务架构设计与落地团队的专题文档,基于 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 IP,Pod重建后IP会变,注册中心里会产生过期实例记录。
| 能力项 | Spring Cloud注册中心 | Kubernetes Service |
|---|---|---|
| 实例级发现 | 支持,客户端主动拉取 | 不感知实例,只暴露稳定VIP |
| 负载均衡 | 客户端侧,Ribbon/Spring Cloud LoadBalancer | kube-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-file=application.yml kubectl set env deployment/order-service --from=configmap/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 --from=build /app/target/order-service.jar ./app.jar USER appuser EXPOSE 8080 ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "/app/app.jar"]这个Dockerfile的多阶段构建把编译环境和运行环境分离,最终镜像里没有Maven和源码,体积能控制在200MB以内。注意dependency:go-offline的作用是提前拉取全部依赖,让后续的源码拷贝和package阶段不需要再频繁联网,本地构建速度会有明显提升。
-XX:MaxRAMPercentage=75.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: - StripPrefix=1 - id: user-service uri: http://user-service:8081 predicates: - Path=/api/user/** filters: - StripPrefix=1 server: port: 8083StripPrefix=1的含义是截掉路径上的第一段后再转发到下游。客户端请求/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 { ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>( "nacos-service:8848", "public", "gateway-flow-rules", source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}) ); 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 --namespace=default 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互补的关系贯穿始终,治理能力交给框架,调度能力交给集群,团队要做的只是把这套边界持续维护好。
本文还有配套的精品资源,点击获取