【基于 Swoole+Hyperf 的微服务实战】 第四周·周六:用户服务的灰度发布与自动伸缩
2026/9/12 19:02:50 网站建设 项目流程

今天我们进入第四周周六综合实战日。本周我们从服务注册发现、负载均衡到配置中心与多环境管理,已经掌握了微服务动态治理的核心技能。今天将通过一个贴近生产的场景——“用户服务的灰度发布与自动伸缩”——来集中演练这些能力。你将亲手实现一个UserService的v1和v2版本,通过Nacos动态配置灰度比例,结合Consul的注册与发现,让文章服务根据配置自动将部分流量导向新版本,同时模拟服务实例的弹性伸缩,全过程无需重启任何消费者。


今日目标

  1. 创建用户服务v2(返回增强数据),与已有的v1共存,分别注册到Consul的不同服务名或使用标签区分。
  2. 在Nacos中配置灰度策略(如user_service_rollout: { "v2_percent": 30 }),动态控制流量分配。
  3. 在文章服务的聚合层实现按比例路由逻辑,根据Nacos配置决定调用v1或v2,实现无感灰度发布。
  4. 演练动态伸缩:启动额外用户服务实例,观察Consul自动发现及负载均衡器平摊流量;停止实例,验证自动剔除。
  5. 使用压测工具在灰度切换和伸缩过程中持续发送请求,确认服务连续性。

一、环境准备与版本规划(约30分钟)

继续在hyperf-app项目中工作,确保所有容器启动:

docker-composeexecswoolebashcd/var/www/hyperf-app
1. 当前服务现状
  • 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-v1UserService-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.phpconsumers数组中,我们之前只有一个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);}}

但这种方式需要代理工厂支持,且要结合消费者配置。更简单的是我们预先在消费者配置中定义好UserServiceUserService-v2,然后在代码中通过#[Inject]分别注入不同的属性,利用#[Inject]name@RpcClientservice属性区分。

实际上,#[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,应出现UserServiceUserService-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同时进行灰度切换,确保吞吐量平稳,无错误。


五、今日作业与学习产出

  1. 提交代码:将UserServiceV2server.php修改、services.php修改、OrderAggregator修改等提交到 Git,附带 Nacos 配置示例。
  2. 绘制架构图
    • 灰度发布时的服务拓扑图:包括 v1/v2 两组实例、Consul 注册、Nacos 灰度配置、消费者的流量分发逻辑。
    • 画出一次灰度请求的完整流程图:请求进入 -> 读取灰度配置 -> 随机选择版本 -> 对应服务名的 RPC 调用 -> 返回。
  3. 学习笔记
    • 总结灰度发布的实现方式,对比服务网格(如 Istio)与代码层控制的优缺点。
    • 探讨 Nacos 如何保障配置变更的实时性与一致性。
  4. 挑战任务
    • 实现A/B 测试:基于请求头中的X-User-Id哈希来选择版本,确保同一用户始终看到同一版本。
    • 结合 Hyperf 的自定义负载均衡器,实现更高级的流量控制(如按百分比直接在选择节点时完成,而不需要两个消费者)。
    • 使用Docker Compose 多容器部署,演示真实的多实例启动/停止,配合 Consul 观察全自动伸缩。

通过今天的综合实战,你亲手打造了一个支持灰度发布和动态伸缩的微服务集群,将本周所有知识融会贯通。这标志着你已经具备构建企业级微服务治理平台的核心能力。下周我们将进入 API 网关与安全防护的篇章,继续深化架构。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询