综合练习与部署直觉
这一课你将能做到
- 按正确顺序把 5 个进程完整跑起来,并用端到端验收清单验证注册、路由、Feign、熔断、配置、指标全链路
- 为每个服务编写多阶段 Dockerfile,用 docker-compose 一键拉起整套微服务
- 建立 K8s 部署的正确直觉:Deployment/Service/Ingress 各管什么、K8s DNS 和 Eureka 什么关系
为什么要学
前面八课的知识点都是「单件」:注册中心一个、网关一个、Feign 一套。但面试官问「你的微服务怎么部署的」「某个服务挂了流量怎么办」,考的是把单件串成系统的能力。本课做三件事:
- 串——5 个进程按依赖顺序拉起,走一遍完整验收,查漏补缺;
- 容器化——
java -jar 手动起 5 个窗口的方式在同事电脑、测试环境都复制不了,Docker 镜像 + compose 让「一键起全套」成为可能;
- 给方向——真实生产的容器编排叫 Kubernetes,本课不深入 K8s,但会把核心概念与已学知识对上号,让你后面学 K8s 时不是从零开始。
贯穿项目仍是 CodeForge 拆分:eureka-server(8761)、config-server(8888)、api-gateway(8080)、user-service(8081)、problem-service(8082)、judge-service(8083),父工程 com.example:cloud-demo。
先建立图像
启动顺序拓扑
① eureka-server :8761 ←── 一切的前提,最先起
▲ 注册
② config-server :8888 ←── 业务服务要从它拉配置
▲ 拉配置
③ user-service :8081 ④ problem-service :8082 ⑤ judge-service :8083
│ client 们起完成后注册进 Eureka
▼
⑥ api-gateway :8080 ←── 最后起(路由虽然不依赖服务先在,但人验收要看全链路)
记忆口诀:先注册中心,再配置中心,然后业务服务,最后网关。 业务服务启动时会连 Eureka、连 Config,两者不在就报错重试;网关路由到不了服务只会 503,不会启动失败,所以顺序上最宽容。
生产部署视图(本课终点)
本地开发: 5 个 IDEA 窗口 / mvn spring-boot:run
↓ 容器化
测试环境: 每服务一个镜像 + docker-compose.yml 一键编排
↓ 编排升级
生产环境: K8s:Deployment(多副本) + Service(稳定地址) + Ingress(外部入口)
跟着做
步骤 1:按顺序启动 5 个进程
# 终端 1:注册中心
mvn -pl eureka-server spring-boot:run
# 终端 2:配置中心(等 eureka 起完,约 15 秒)
mvn -pl config-server spring-boot:run
# 终端 3~5:业务服务(等 config 起完)
mvn -pl user-service spring-boot:run
mvn -pl problem-service spring-boot:run
mvn -pl judge-service spring-boot:run
# 终端 6:网关
mvn -pl api-gateway spring-boot:run
打开 http://localhost:8761,Eureka 控制台应看到 4 个 client(user、problem、judge、gateway)全部 UP。
步骤 2:端到端验收清单
逐条执行,每条对应前面某一课的知识点——任何一条失败,回到对应课程排查:
| # | 验收项 | 命令/操作 | 验证的课程 |
|---|
| 1 | 注册中心 4 个 client 全 UP | 打开 |
http://localhost:8761
| 2 | 经网关访问题目接口通 | curl http://localhost:8080/api/problems/1 | 网关路由 |
| 3 | 业务服务从配置中心拉到了值 | curl http://localhost:8082/features | 配置中心 |
| 4 | 跨服务 Feign 调用通 | curl http://localhost:8081/dashboard/1001/problem/1 | OpenFeign |
| 5 | 停掉 problem-service 后学习面板仍可用(返回兜底文案且毫秒级响应) | 停服务后重复第 4 条 | 熔断降级 |
| 6 | 重启 problem-service,熔断器回到 CLOSED | curl http://localhost:8081/actuator/circuitbreakers | 熔断状态机 |
| 7 | 配置热更新生效 | 改 config-repo 值后 curl -X POST .../actuator/refresh | 配置刷新 |
| 8 | 指标端点有数据、日志带 traceId | curl http://localhost:8083/actuator/prometheus;grep traceId | 可观测性 |
步骤 3:给每个服务写多阶段 Dockerfile
在每个服务的模块目录下建 Dockerfile(以 problem-service 为例):
# ============ 第一阶段:构建 ============
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
COPY src ./src
RUN mvn -B package -DskipTests
# ============ 第二阶段:运行 ============
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /build/target/problem-service-1.0.0.jar app.jar
EXPOSE 8082
ENTRYPOINT ["java", "-jar", "app.jar"]
- 多阶段构建:构建层带着 Maven 和全部依赖(1GB+),运行层只有 JRE + jar(约 200MB),镜像又小又干净;
- 分层缓存:先 COPY pom.xml 再 COPY src,改业务代码时 Maven 依赖层命中缓存,构建提速明显;
- 运行镜像用
17-jre 而不是 JDK(运行不需要编译器)。
docker build -t cloud-demo/problem-service:1.0.0 -f problem-service/Dockerfile problem-service/
docker build -t cloud-demo/user-service:1.0.0 -f user-service/Dockerfile user-service/
# 其余同理
步骤 4:docker-compose 编排整套系统
父工程根目录 docker-compose.yml:
services:
eureka-server:
image: cloud-demo/eureka-server:1.0.0
ports: [ "8761:8761" ]
config-server:
image: cloud-demo/config-server:1.0.0
ports: [ "8888:8888" ]
depends_on:
- eureka-server
environment:
EUREKA_CLIENT_SERVICEURL_DEFAULTZONE: http://eureka-server:8761/eureka/
user-service:
image: cloud-demo/user-service:1.0.0
depends_on:
- eureka-server
- config-server
environment:
EUREKA_CLIENT_SERVICEURL_DEFAULTZONE: http://eureka-server:8761/eureka/
SPRING_CONFIG_IMPORT: optional:configserver:http://config-server:8888
problem-service:
image: cloud-demo/problem-service:1.0.0
depends_on:
- eureka-server
- config-server
environment:
EUREKA_CLIENT_SERVICEURL_DEFAULTZONE: http://eureka-server:8761/eureka/
SPRING_CONFIG_IMPORT: optional:configserver:http://config-server:8888
judge-service:
image: cloud-demo/judge-service:1.0.0
depends_on:
- eureka-server
- config-server
environment:
EUREKA_CLIENT_SERVICEURL_DEFAULTZONE: http://eureka-server:8761/eureka/
api-gateway:
image: cloud-demo/api-gateway:1.0.0
ports: [ "8080:8080" ]
depends_on:
- user-service
- problem-service
environment:
EUREKA_CLIENT_SERVICEURL_DEFAULTZONE: http://eureka-server:8761/eureka/
docker compose up -d # 一键起全套
docker compose ps # 看状态
docker compose down # 一键停
- 服务名即主机名:compose 会建内部网络,容器之间直接用
http://eureka-server:8761、http://config-server:8888 互访——服务名就是 DNS 名;
- 环境变量覆盖 yml:
EUREKA_CLIENT_SERVICEURL_DEFAULTZONE 覆盖 application.yml 里的 eureka.client.service-url.defaultZone(Spring Boot 宽松绑定把下划线映射为点),镜像不变、配置随环境变,这正是十二因子的做法;
depends_on 只管启动顺序不管就绪:config-server 进程起了不代表 8888 能响应,业务服务可能要靠 Spring 的重试机制连上——所以配置里用了 optional: 前缀 + 良好的重试,别指望 depends_on 解决一切。
步骤 5:踩一遍「容器内 localhost」陷阱(必踩教育坑)
如果 user-service 的 yml 里写死 configserver:http://localhost:8888,容器化后必然连不上——容器里的 localhost 是容器自己,不是宿主机也不是别的容器。修改方法只有两种:
- 环境变量覆盖(上面 compose 的做法)——推荐;
- 配置文件里用服务名:
http://config-server:8888(compose 网络内有效)。
把 localhost 从所有服务间调用的配置里清除干净,是容器化最大的思维转换。
步骤 6:建立 K8s 直觉(只建立直觉,不展开)
| 你在 Spring Cloud 学的 | K8s 里对应物 | 关系说明 |
|---|
| Eureka 注册中心 | K8s Service + 集群 DNS(problem-service.default.svc.cluster.local) | K8s 自带服务发现,同功能二选一 |
| api-gateway 路由 | Ingress / Service | 外部流量入口可交给 Ingress |
手动 docker run 多副本 | Deployment 副本数 | replicas: 3 声明 3 个实例,K8s 自动维持 |
| Eureka 心跳摘除故障实例 | K8s liveness/readiness 探针 | 探针失败自动重启/摘流 |
| Config Server | ConfigMap / Secret | 配置进 K8s 原生对象,可挂载或注入环境变量 |
| 手动改 yml 重启发布 | 滚动更新(Rolling Update) | 新版本逐个替换旧 Pod,全程不掉线 |
结论分两种场景:部署在 K8s 里,服务发现可直接用 K8s DNS,Eureka 可以退役(配置也多交给 ConfigMap);混合部署或需要应用层健康状态感知时,保留 Eureka。两种都成立,面试说清「为什么选」比「选哪个」重要。
步骤 7:优雅停机(上线质量的细节分)
server:
shutdown: graceful # 收到停机信号后停止接新请求,等在处理请求完成
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
配合 compose/K8s 的 SIGTERM(而不是 kill -9),发布时用户无感。K8s 滚动更新 + 就绪探针 + 优雅停机,就是「发布中零请求失败」的三件套。
讲清楚要点
- 启动顺序的因果:业务服务启动要做两件依赖别人的事——连 Config 拉配置、连 Eureka 注册;所以注册中心和配置中心必须先活。网关不注册到别人的下游,最后起只是习惯,不是硬约束。
- 多阶段 Dockerfile 的价值:构建环境(Maven、源码、依赖缓存)与运行环境(JRE、jar)彻底分离,镜像小、攻击面小、拉取快;「先拷 pom 后拷 src」利用 Docker 分层缓存加速重复构建。
- compose 的三件事:建内部网络(服务名互访)、编排依赖(depends_on 控制启动顺序)、注入环境变量(同一镜像适配不同环境)。它管不了「就绪」和「自愈」,那是 K8s 的活。
- 容器网络是思维分水岭:进了容器,
localhost 就是你自己。服务间地址一律用「服务名」或环境变量注入,「IP + localhost 写死在 yml」是反模式。
- K8s 与 Spring Cloud 是部分重叠关系:K8s 在平台层解决了服务发现、配置、负载均衡、自愈;Spring Cloud 在应用层提供了跨平台的等价物。上了 K8s 后 Eureka/Config 可以简化,但 Feign 的声明式调用、Resilience4j 的熔断降级、Micrometer 的可观测依然有价值——平台管不了业务降级。
- 发布质量三件套:优雅停机(server.shutdown=graceful)+ 就绪探针(没准备好不接流量)+ 滚动更新(逐个替换副本)。三者合起来才能做到发布期间用户零失败。
你可能会懵的地方
-
docker compose 起来后服务互相连不上?
按顺序查:①容器都在 docker compose ps 且没有重启循环(docker compose logs 服务名);②地址用的是服务名不是 localhost;③环境变量名拼对没有(EUREKA_CLIENT_SERVICEURL_DEFAULTZONE 是全大写下划线);④docker compose down 后旧网络残留,重新 up 一次。
-
depends_on 了为什么服务还是启动失败?
depends_on 只保证「容器的启动顺序」,不保证「应用已就绪」。config-server 容器起 5 秒,Tomcat 可能还没监听。解法:业务服务的 Config 连接配重试(spring.cloud.config.retry)或 optional: 前缀;K8s 用 readiness 探针才是正解。
-
上了 K8s,前面学的 Eureka、Config、Gateway 白学了吗?
没有。①面试和存量项目里 Eureka 依然是主流方案之一;②K8s 的 Service 只做「服务发现 + 负载均衡」,业务熔断降级(Resilience4j)、声明式调用(Feign)、可观测(Micrometer)K8s 都不管,那些能力照样要;③很多公司 Eureka 和 K8s 长期共存(跨集群、IDC 混部)。
-
本地开发也需要 Docker 吗?
分层看:中间件(MySQL/Redis/Mongo)用 Docker 一行起最方便;业务服务用 IDEA 直接跑——改代码热重启快;整套联调或给别人演示时才 docker compose up。全容器化开发在 Spring Boot 的重构循环里太慢。
-
微服务数量多了,compose 管不动怎么办?
compose 的定位是单机编排,超过 10 来个服务就该上 K8s(或多机 compose swarm)。判断标准不是服务数量,而是「是否需要多机、自愈、弹性伸缩」——需要,就直接上 K8s,别在 compose 上过度投资。
错法对照
| 错法 | 问题 | 正解 |
|---|
yml 里写死 localhost:8888 直接做镜像 | 容器内 localhost 是容器自身,必然连不上 | 环境变量注入地址,或用 compose 服务名 |
全部依赖 depends_on 保证可用性 | 只保证启动顺序,不保证就绪 | 应用侧重试/optional + 生产用 K8s 探针 |
| 运行镜像用完整 JDK + 全量构建产物 | 镜像 1GB+,拉取慢攻击面大 | 多阶段构建,运行层只用 JRE |
发布直接 kill -9 | 在途请求腰斩,用户收到 502 | graceful shutdown + SIGTERM |
| Eureka 和 K8s DNS 同时启用且都注册 | 双份发现路径,摘流行为不一致难以排查 | 明确主发现机制,另一侧降级或退役 |
卡点急救
docker compose up 后某服务一直重启:docker compose logs --tail=100 服务名 看启动日志——九成是连不上 Eureka/Config(地址写错或对应服务没起),修正环境变量后 docker compose up -d --force-recreate 服务名。
- 宿主机 curl 不通容器端口:确认 compose 里写了
ports: ["8080:8080"](映射了才通);没映射的服务只能进容器网络访问(docker compose exec user-service curl ...)——这也正是「网关要映射、业务服务可不暴露」的安全实践。
- 镜像构建时 jar 找不到:第一阶段产物路径必须与 pom 的
finalName/版本一致;确认模块版本号是 1.0.0,或者干脆在 Dockerfile 里用通配再 COPY 重命名。
面试常问
-
描述一个微服务从开发到上线的完整流程。
①代码与配置分离(配置进 Config/Git);②CI 流水线:编译、测试、构建 Docker 镜像(多阶段构建)并推送镜像仓库;③部署:K8s Deployment 声明副本数,滚动更新逐个替换 Pod;④就绪探针通过后才接入流量,liveness 探针故障自愈;⑤发布期间优雅停机保证在途请求完成;⑥可观测兜底:Prometheus 指标告警 + 日志 traceId 排查。能按「构建 → 部署 → 流量 → 回滚」四段讲清楚即可。
-
K8s 的服务发现和 Eureka 什么关系?上 K8s 后还需要注册中心吗?
K8s Service + 集群 DNS 提供平台层服务发现:Pod 伸缩/重建时 Service 自动更新 Endpoint,天然免维护。上 K8s 后自建系统一般退役 Eureka 用 K8s DNS;但跨集群、虚机混合部署、或需要应用层心跳感知时保留 Eureka 也有价值。Spring Cloud Kubernetes 项目能把 K8s API 桥接成 DiscoveryClient,让既有代码平滑迁移。答案要点:不是替代关系而是层次关系,按部署形态选。
-
滚动发布怎么做到用户无感?
三件套:①优雅停机——旧实例收到 SIGTERM 后停止接新请求、等在途请求处理完再退出(server.shutdown=graceful);②就绪探针——新实例真正可服务后才被 Service 挂上流量;③滚动更新——Deployment 控制新旧副本平滑交替,任何时刻都有健康实例承接。再叠加发布前的镜像可回滚(tag 化),出问题一键回到上一版本。
自测三问
- 五个进程的正确启动顺序是什么?为什么业务服务必须在 Config 之后?
- compose 里服务之间怎么互相访问?什么机制让服务名变成可解析的地址?
- K8s 的哪两个资源分别对应「多副本管理」和「稳定访问地址」?
小结一句话
把注册、配置、网关、Feign、熔断、可观测串成一条能跑通、能验收、能容器化的完整链路——之后进 K8s 的世界时,你会发现每个新概念都有一个你已经学过的原型。
延伸