4. 服务层设计(一):微服务拆分原则与服务注册发现
好,咱们进入服务层设计。这一章我打算聊聊微服务怎么拆,以及拆完之后怎么让它们互相找到对方。
很多人一上来就搞微服务,结果拆得稀碎,调用链比蜘蛛网还乱。我见过一个团队,把一个人脸识别拆成了十几个服务,每次请求要串七八个 RPC,延迟直接爆炸。说白了,微服务拆分不是越细越好,而是找到那个「恰到好处」的粒度。
4.1 微服务拆分原则:三个核心服务
在人脸识别这个场景里,我习惯按「处理阶段」来拆。每个阶段做的事情不一样,资源消耗也不一样,混在一起反而不好管理。
你想想看,一张人脸图片进来,要经历哪些步骤?
- 先找到脸 —— 人脸检测
- 把脸转成数学向量 —— 特征提取
- 拿这个向量去数据库里比 —— 比对服务
这三个阶段,就是三个天然的微服务边界。
| 服务名称 | 核心职责 | 资源瓶颈 | 典型技术栈 |
|---|---|---|---|
| 人脸检测服务 | 从图片中定位人脸区域,返回坐标和置信度 | CPU/GPU 计算密集 | MTCNN, RetinaFace, OpenCV |
| 特征提取服务 | 将人脸区域编码为固定维度的特征向量 | GPU 显存密集 | FaceNet, ArcFace, InsightFace |
| 比对服务 | 计算向量相似度,返回 Top-K 匹配结果 | 内存/IO 密集 | Faiss, Milvus, 向量数据库 |
核心原则:每个服务只做一件事,并且把这件事做到极致。检测服务不管特征提取,比对服务不碰图片解码。边界清晰,才能独立扩缩容。
4.2 为什么这么拆?
我在项目中遇到过一个问题:一开始把检测和特征提取放在同一个服务里。结果呢?检测模块升级了模型,特征提取也得跟着重新部署。更麻烦的是,检测是 CPU 密集型,特征提取是 GPU 密集型,混在一起根本没法独立扩缩容。
拆开之后,好处很明显:
- 独立扩缩容:大促期间图片量暴增,我只扩检测服务的实例数就行。特征提取和比对服务保持稳定,不用跟着一起折腾。
- 独立升级:检测模型从 MTCNN 换成 RetinaFace,只改检测服务。特征提取从 FaceNet 换成 ArcFace,只改特征提取服务。互不影响。
- 故障隔离:比对服务挂了,检测和特征提取还能继续处理图片,只是暂时查不了结果。不至于整个系统瘫痪。
我的经验:拆服务的时候,先看「资源消耗」和「变更频率」。如果两个模块的资源消耗差异很大,或者变更频率不同,就应该拆开。否则,强行拆只会增加网络开销。
4.3 服务注册与发现:为什么需要?
服务拆完了,问题来了:检测服务怎么知道特征提取服务在哪台机器上?
你可能会说,配个 IP 地址不就行了?嗯,小规模可以。但一旦上了几十个实例,IP 天天变,手动配就是噩梦。我见过有人把 IP 写死在配置文件里,结果扩缩容的时候,运维小哥改配置改到崩溃。
这时候就需要服务注册与发现。说白了,就是让服务启动的时候自己报个到,告诉注册中心「我在这里,我叫什么名字,我的 IP 和端口是多少」。其他服务要调用它的时候,去注册中心查一下就行。
4.4 Consul vs Nacos:怎么选?
目前主流的方案有两个:Consul 和 Nacos。我两个都用过,说说我的感受。
| 对比维度 | Consul | Nacos |
|---|---|---|
| 开发语言 | Go | Java |
| 一致性协议 | Raft | Raft + Distro |
| 健康检查 | 支持 TCP/HTTP/gRPC | 支持 TCP/HTTP/MySQL |
| 配置管理 | 需配合其他工具 | 内置配置中心 |
| 生态集成 | 与 HashiCorp 生态紧密 | 与 Spring Cloud Alibaba 紧密 |
| 性能 | 单机 10k+ QPS | 单机 10k+ QPS |
避坑指南:我曾经在 Consul 上踩过一个坑——它的健康检查默认是 10 秒一次。如果服务实例突然挂了,其他服务要等 10 秒才能感知到。在高并发场景下,这 10 秒里会有大量请求打到已经挂掉的实例上。解决方案是把检查间隔调到 2-3 秒,同时配合客户端侧的重试和熔断。
4.5 架构图:服务注册与发现流程
下面这张图展示了三个核心服务如何通过注册中心互相发现。我建议你仔细看看,尤其是服务启动和调用时的交互顺序。
4.6 实战:服务注册与发现配置
我拿 Nacos 举个例子。假设你的检测服务要注册到 Nacos,配置大概是这样的:
# application.yml
spring:
application:
name: face-detection-service
cloud:
nacos:
discovery:
server-addr: 192.168.1.100:8848
namespace: face-recognition-prod
group: FACE-SERVICE-GROUP
heart-beat-interval: 3000 # 心跳间隔 3 秒
heart-beat-timeout: 9000 # 心跳超时 9 秒
ip-delete-timeout: 15000 # 实例删除超时 15 秒
server:
port: 8081
然后,特征提取服务要调用检测服务时,通过 Nacos 客户端获取地址:
// 使用 Nacos 客户端获取服务实例
NamingService naming = NacosFactory.createNamingService("192.168.1.100:8848");
List<Instance> instances = naming.selectInstances("face-detection-service", true);
// 负载均衡:随机选一个
Instance target = instances.get(ThreadLocalRandom.current().nextInt(instances.size()));
String url = String.format("http://%s:%d/detect", target.getIp(), target.getPort());
// 发起 RPC 调用
RestTemplate restTemplate = new RestTemplate();
FaceDetectResponse response = restTemplate.postForObject(url, request, FaceDetectResponse.class);
小技巧:生产环境建议把 Nacos 部署成集群模式,至少 3 个节点。单节点挂了,整个服务发现就瘫痪了。另外,客户端侧一定要做本地缓存——即使注册中心挂了,也能用缓存的地址列表继续工作一段时间。
4.7 避坑指南
- 服务名不要乱起:我见过有人用「service1」「service2」这种名字,半年后自己都分不清哪个是哪个。建议用「业务-功能」的命名规范,比如
face-detection、face-feature、face-match。 - 健康检查要全面:不只是检查进程是否活着,还要检查服务是否真的能处理请求。我习惯在健康检查接口里顺便检查一下依赖的资源(比如 GPU 是否可用、数据库连接池是否正常)。
- 版本管理:服务升级的时候,老版本和新版本会同时存在一段时间。Nacos 支持元数据,可以在注册的时候带上版本号,调用方根据版本号做灰度路由。
我曾经踩过的坑:有一次我把 Nacos 的心跳间隔设成了 30 秒,结果服务实例挂了之后,其他服务继续往它上面发请求,持续了将近 30 秒。那段时间的失败率直接飙到 30%。后来我把心跳间隔调到 3 秒,超时时间调到 9 秒,配合客户端重试,才把问题解决。