今天我们进入第四周周六综合实战日。本周我们从服务注册发现、负载均衡到配置中心与多环境管理,已经掌握了微服务动态治理的核心技能。今天将通过一个贴近生产的场景——“用户服务的灰度发布与自动伸缩”——来集中演练这些能力。你将亲手实现一个UserService的v1和v2版本,通过Nacos动态配置灰度比例,结合Consul的注册与发现,让文章服务根据配置自动将部分流量导向新版本,同时模拟服务实例的弹性伸缩,全过程无需重启任何消费者。
今日目标
- 创建用户服务v2(返回增强数据),与已有的v1共存,分别注册到Consul的不同服务名或使用标签区分。
- 在Nacos中配置灰度策略(如
user_service_rollout: { "v2_percent": 30 }),动态控制流量分配。 - 在文章服务的聚合层实现按比例路由逻辑,根据Nacos配置决定调用v1或v2,实现无感灰度发布。
- 演练动态伸缩:启动额外用户服务实例,观察Consul自动发现及负载均衡器平摊流量;停止实例,验证自动剔除。
- 使用压测工具在灰度切换和伸缩过程中持续发送请求,确认服务连续性。
一、环境准备与版本规划(约30分钟)
继续在hyperf-app项目中工作,确保所有容器启动:
docker-composeexecswoolebashcd/var/www/hyperf-app1. 当前服务现状
- HTTP服务(文章/订单): 9501
- 用户服务v1: 9502 (UserService, 实现类
App\JsonRpc\UserService) - 用户服务v2: 我们新增一个端口9503,使用不同的实现类
App\JsonRpc\UserServiceV2,返回数据额外包含version: "v2"和extra_info字段。 - 商品服务: 9504
- Consul、Nacos已集成。
2. 版本共存策略
在Consul中,v1和v2可以作为两个不同的服务注册(如UserService-v1和UserService-v2),或者作为同一服务的不同标签。我们选择更清晰的两个独立服务名,消费者根据配置动态选择调用哪个服务。这样就能完全模拟蓝绿部署。
3. 配置灰度参数
在Nacos的hyperf-app配置中增加:
{"user_service_rollout":{"v2_enabled":true,"v2_percent":30}}我们将实现:消费者在每次调用时,生成一个随机数(0-100),若小于v2_percent则调用v2,否则调用v1。
二、知识核心:灰度发布与流量控制(约20分钟)
灰度发布(金丝雀发布)是指先让一部分用户使用新版本,验证无问题后逐步扩大比例直至全量替换。在微服务中通常通过服务网格或API网关实现,我们今天在客户端负载均衡层实现一个简易版本。
关键点:
- 服务注册:新旧版本作为独立服务存在,各自有健康实例。
- 动态配置:灰度比例存放在Nacos,可实时调整。
- 流量路由:消费者根据配置决定实例选择,并配合普通负载均衡。
- 回滚:将比例调回0即可瞬间切回旧版本。
三、实战:构建灰度发布与动态伸缩(约3小时)
步骤 1:创建用户服务v2实现
新建app/JsonRpc/UserServiceV2.php:
<?phpnamespaceApp\JsonRpc;useHyperf\RpcServer\Annotation\RpcService;#[RpcService(name:"UserService-v2",protocol:"jsonrpc-http",server:"jsonrpc-v2")]classUserServiceV2implementsUserServiceInterface{publicfunctiongetUserById(int$userId):array{$base=[1=>['id'=>1,'username'=>'admin','email'=>'admin@example.com','avatar'=>''],2=>['id'=>2,'username'=>'editor','email'=>'editor@example.com','avatar'=>''],];$user=$base[$userId]??[];$user['version']='v2';$user['extra_info']='新版特性:显示会员等级';return$user;}publicfunctiongetUserByUsername(string$username):array{$map=['admin'=>1,'editor'=>2];if(isset($map[$username])){return$this->getUserById($map[$username]);}return[];}}注意:server指向一个新的服务端名称jsonrpc-v2,我们将在server.php中添加对应端口。
步骤 2:添加v2服务端监听
编辑config/autoload/server.php,增加:
['name'=>'jsonrpc-v2','type'=>Server::SERVER_HTTP,'host'=>'0.0.0.0','port'=>9505,'sock_type'=>SWOOLE_SOCK_TCP,'callbacks'=>[Event::ON_REQUEST=>[\Hyperf\JsonRpc\HttpServer::class,'onRequest'],],],如果需要多节点,后续可以再增加jsonrpc-v2-2等。
步骤 3:配置消费者发现两个服务
在config/autoload/services.php的consumers数组中,我们之前只有一个UserService。现在需要增加一个UserService-v2的消费者配置,但为了避免写死两个客户端,我们可以通过通用的RPC客户端工厂动态创建。简单起见,我们保留原有的UserService客户端(指向v1),再新增一个UserServiceV2客户端。
添加消费者配置:
['name'=>'UserService-v2','load_balancer'=>App\LoadBalancer\CustomRoundRobin::class,'registry'=>['protocol'=>'consul','address'=>env('CONSUL_URI','http://consul:8500'),],'refresh_time'=>5,'options'=>['connect_timeout'=>2.0,'recv_timeout'=>2.0,'retry_count'=>1,'pool'=>['min_connections'=>1,'max_connections'=>10],'circuit_breaker'=>true,'circuit_breaker_options'=>['failure_count'=>3,'open_timeout'=>30],],],同时保留原有的UserService消费者(指向v1)。
步骤 4:在聚合服务中实现灰度路由
修改app/Service/OrderAggregator.php或创建一个更通用的UserServiceRouter。我们直接在OrderAggregator中注入两个客户端,并根据Nacos配置决定调用哪个。
但注意,我们不能直接使用#[RpcClient]注入两个同名接口,因为接口相同但服务名不同。我们可以用工厂模式或使用Hyperf\RpcClient\ProxyFactory动态获取。更简单的方法是:注入两个不同的接口?我们可以让v2实现一个标记接口。但为了快速实现,我们在OrderAggregator中注入UserServiceInterface的两个实例,通过#[Inject]结合@RpcClient无法直接区分。这里介绍一种实战常用方法:使用抽象工厂。
新建app/Rpc/RpcClientFactory.php:
<?phpnamespaceApp\Rpc;useHyperf\Contract\ContainerInterface;useHyperf\RpcClient\Client;useApp\JsonRpc\UserServiceInterface;classRpcClientFactory{publicfunction__construct(privateContainerInterface$container){}publicfunctioncreateUserClient(string$serviceName):UserServiceInterface{// 通过容器获取RpcClient代理,动态指定服务名return$this->container->get(\Hyperf\RpcClient\ProxyFactory::class)->create(UserServiceInterface::class,$serviceName);}}但这种方式需要代理工厂支持,且要结合消费者配置。更简单的是我们预先在消费者配置中定义好UserService和UserService-v2,然后在代码中通过#[Inject]分别注入不同的属性,利用#[Inject]的name或@RpcClient的service属性区分。
实际上,#[RpcClient]注解可以指定name属性来关联services.php中的消费者名称。我们可以这样写:
#[RpcClient(name:"UserService")]// 对应 v1privateUserServiceInterface$userServiceV1;#[RpcClient(name:"UserService-v2")]// 对应 v2privateUserServiceInterface$userServiceV2;这两个属性可以同时存在于同一个类中。所以我们在OrderAggregator中这样注入:
useHyperf\RpcClient\Annotation\RpcClient;#[RpcClient(name:"UserService")]privateUserServiceInterface$userServiceV1;#[RpcClient(name:"UserService-v2")]privateUserServiceInterface$userServiceV2;注意UserService-v2的消费者名称必须与services.php中定义的一致。
步骤 5:实现灰度选择逻辑
在OrderAggregator中增加获取 Nacos 配置的依赖,并修改getUserInfo方法:
useHyperf\Config\Annotation\Value;#[Value("user_service_rollout")]privatearray$rolloutConfig;privatefunctiongetUserInfo(int$userId):array{$v2Enabled=$this->rolloutConfig['v2_enabled']??false;$v2Percent=(int)($this->rolloutConfig['v2_percent']??0);$useV2=false;if($v2Enabled&&$v2Percent>0){$rand=mt_rand(1,100);if($rand<=$v2Percent){$useV2=true;}}try{if($useV2){return$this->userServiceV2->getUserById($userId);}else{return$this->userServiceV1->getUserById($userId);}}catch(\Throwable$e){// 降级逻辑return$this->fallbackUser();}}这样,当 Nacos 中v2_percent为 30 时,大约 30% 的请求会调用v2,其余走v1。
步骤 6:注册v2服务并启动
确保UserServiceV2类文件存在且注解正确。重启 Hyperf:
php bin/hyperf.php start检查 Consul UI,应出现UserService和UserService-v2两个服务,分别有一个实例(或我们为v2也配置多个)。
步骤 7:动态伸缩演练
为方便演示,我们可以为UserService再手动注册一个不存在的实例(或用API注册),观察负载均衡变化;或者使用docker-compose scale启动多个真实实例。采用简单方法:保留之前的多节点配置(如 jsonrpc2 9503 仍属于 UserService)。这样 UserService 有两个节点。
在 Nacos 中将v2_percent设为 100,查看订单接口,应全部返回带version: v2的数据;设为 0,全部返回旧版。
步骤 8:持续压测与平滑过渡
使用脚本循环请求订单接口,同时动态修改 Nacos 中的v2_percent:
# 在终端持续请求whiletrue;docurl-shttp://localhost:9501/orders/1;echo;sleep0.1;done在 Nacos 控制台将v2_percent从 0 逐步改为 50,再改为 100,观察输出中 v2 内容逐渐增多直到全部替换,且无任何异常或中断。
演示缩容:通过 Consul API 注销一个 v1 实例,流量自动转移到剩余实例,不中断服务。
四、成果测试与验证(约1小时)
测试清单
| 检验项 | 方法 | 通过标准 |
|---|---|---|
| v2服务注册成功 | Consul UI 查看UserService-v2 | 出现实例,健康检查通过 |
| 消费者能同时发现v1和v2 | 启动后,RPC调用无报错 | 订单接口正常返回 |
| 灰度比例动态调整 | 修改 Nacos 中v2_percent,统计多次请求 | 实际v2调用比例接近设定值 |
| 比例0时完全回滚 | 将v2_enabled设为 false | 所有请求返回v1版本 |
| 动态扩容(新增v2实例) | 手动注册额外v2节点,等待刷新 | 负载均衡日志显示新节点,流量分配 |
| 动态缩容 | 注销某个v1节点,请求不受影响 | 可用节点列表更新,错误率0 |
| 故障隔离 | 停止v2所有实例,v2调用触发熔断,回退到v1降级 | 订单仍能正常返回降级用户信息 |
| 配置实时生效 | 修改 Nacos 配置后,无需重启 | 新请求按照新比例执行 |
压测与观察
使用ab -n 500 -c 20 http://localhost:9501/orders/1同时进行灰度切换,确保吞吐量平稳,无错误。
五、今日作业与学习产出
- 提交代码:将
UserServiceV2、server.php修改、services.php修改、OrderAggregator修改等提交到 Git,附带 Nacos 配置示例。 - 绘制架构图:
- 灰度发布时的服务拓扑图:包括 v1/v2 两组实例、Consul 注册、Nacos 灰度配置、消费者的流量分发逻辑。
- 画出一次灰度请求的完整流程图:请求进入 -> 读取灰度配置 -> 随机选择版本 -> 对应服务名的 RPC 调用 -> 返回。
- 学习笔记:
- 总结灰度发布的实现方式,对比服务网格(如 Istio)与代码层控制的优缺点。
- 探讨 Nacos 如何保障配置变更的实时性与一致性。
- 挑战任务:
- 实现A/B 测试:基于请求头中的
X-User-Id哈希来选择版本,确保同一用户始终看到同一版本。 - 结合 Hyperf 的自定义负载均衡器,实现更高级的流量控制(如按百分比直接在选择节点时完成,而不需要两个消费者)。
- 使用Docker Compose 多容器部署,演示真实的多实例启动/停止,配合 Consul 观察全自动伸缩。
- 实现A/B 测试:基于请求头中的
通过今天的综合实战,你亲手打造了一个支持灰度发布和动态伸缩的微服务集群,将本周所有知识融会贯通。这标志着你已经具备构建企业级微服务治理平台的核心能力。下周我们将进入 API 网关与安全防护的篇章,继续深化架构。