20、微服务架构:服务拆分原则、服务发现、负载均衡、API网关模式
微服务架构,说白了就是把一个大单体应用拆成一堆小服务。每个服务独立部署、独立演进。听起来很美好,但拆不好就是灾难。我见过太多团队一上来就拆了十几个服务,结果连个接口都调不通。
这一章,咱们就聊聊怎么拆、怎么找、怎么均衡、怎么统一入口。嗯,都是我在项目中踩过的坑。
一、服务拆分原则:别为了拆而拆
服务拆分不是越细越好。你想想看,拆成几十个服务,每个服务就两三个接口,那运维成本直接起飞。我个人习惯遵循以下几个原则:
- 业务边界原则:按业务领域拆分。比如用户服务、订单服务、支付服务。每个服务有清晰的业务边界。
- 数据独立原则:每个服务拥有自己的数据库。别共享数据库,否则耦合度比单体还高。
- 团队自治原则:一个服务由一个团队全权负责。如果团队只有两三个人,别拆太细。
- 变更频率原则:经常一起变更的功能,别拆开。否则一次改动要跨多个服务发布。
核心观点:服务拆分的目的是降低复杂度,而不是增加复杂度。拆完之后,每个服务应该比原来的单体更简单。
我在项目中遇到过这样一个案例:有个团队把用户管理拆成了「用户注册服务」「用户登录服务」「用户信息查询服务」三个独立服务。结果每次用户注册完还要调登录服务生成 token,再调查询服务获取用户信息。一个注册流程变成了三次 RPC 调用。后来我建议他们合并成一个用户服务,反而更清爽。
二、服务发现:服务之间怎么找到对方?
服务拆开了,A 服务怎么知道 B 服务的 IP 和端口?总不能写死在配置文件里吧。服务发现就是解决这个问题的。
服务发现有两种模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| 客户端发现 | 客户端直接查询注册中心,获取服务实例列表 | 小规模、对延迟敏感 |
| 服务端发现 | 客户端请求负载均衡器,由负载均衡器查询注册中心 | 大规模、需要统一管理 |
在 Ktor 生态里,我常用 Consul 或者 Kubernetes 自带的 DNS 来做服务发现。Ktor 客户端支持通过 ServiceDiscovery 接口集成。
// Ktor 客户端集成 Consul 服务发现
val client = HttpClient(CIO) {
install(ServiceDiscovery) {
consul {
host = "consul.example.com"
port = 8500
}
}
}
// 直接通过服务名调用
val response = client.get("http://user-service/api/users/123")
小提示:服务发现一定要配合健康检查。如果服务挂了但注册中心没感知,那调用方就会疯狂报错。我曾经就因为健康检查间隔设得太长,导致线上故障持续了 5 分钟。
三、负载均衡:别让一个服务累死
一个服务通常部署多个实例。负载均衡就是决定请求发给哪个实例。常见的策略有:
- 轮询:按顺序轮流分配。简单,但不考虑实例负载。
- 最少连接:分配给当前活跃连接最少的实例。适合长连接场景。
- 一致性哈希:根据请求的某个特征(如用户 ID)哈希到固定实例。适合需要缓存亲和性的场景。
在 Ktor 中,负载均衡通常和服务发现配合使用。Ktor 客户端内置了负载均衡支持:
val client = HttpClient(CIO) {
install(ServiceDiscovery) {
consul { /* ... */ }
loadBalancer = LoadBalancer.roundRobin()
}
}
注意:不要盲目使用轮询。如果某个实例处理慢,轮询会把请求继续发过去,导致该实例雪崩。我个人习惯用最少连接策略,或者配合熔断器一起使用。
四、API 网关模式:统一入口,统一治理
服务多了之后,客户端要记住每个服务的地址,还要处理认证、限流、日志等公共逻辑。API 网关就是把这些公共逻辑抽出来,放在一个统一的入口层。
API 网关的核心职责:
- 路由转发:根据请求路径转发到对应的后端服务
- 认证鉴权:统一校验 token,避免每个服务重复实现
- 限流熔断:防止某个服务被突发流量打垮
- 协议转换:比如将外部 HTTP 请求转为内部 gRPC 调用
- 日志监控:统一记录请求日志和指标
在 Ktor 中,你可以自己写一个网关服务,也可以集成现成的方案如 Kong、Zuul、Spring Cloud Gateway。我个人倾向于用 Ktor 自建轻量级网关,因为灵活且性能好。
// 一个简单的 Ktor API 网关示例
fun Application.gatewayModule() {
install(Routing) {
// 路由到用户服务
route("/api/users") {
handle {
val response = userServiceClient.get("/api/users${request.uri}")
call.respond(response)
}
}
// 路由到订单服务
route("/api/orders") {
handle {
val response = orderServiceClient.get("/api/orders${request.uri}")
call.respond(response)
}
}
}
}
避坑指南:我曾经在网关层做了太多业务逻辑,结果网关变成了一个超级单体。记住,网关只做路由和公共治理,业务逻辑一定要下沉到后端服务。
五、整体架构图
下面这张图展示了微服务架构的核心组件和调用关系。你可以看到客户端只和 API 网关通信,网关负责路由到后端服务,服务之间通过服务发现找到彼此。
六、总结
微服务架构不是银弹。拆得好,系统灵活可扩展;拆不好,运维复杂度翻倍。我个人建议:
- 先从 2-3 个服务开始,别一上来就搞几十个
- 服务发现和负载均衡一定要配合健康检查
- API 网关只做路由和公共治理,别塞业务逻辑
- 监控和日志一定要跟上,否则出问题你都不知道哪个服务挂了
嗯,这一章就到这里。记住,架构是演进的,不是设计出来的。先跑起来,再优化。