【基于 Swoole+Hyperf 的微服务实战】第三周·周三 RPC 客户端与自定义负载均衡
【基于 Swoole+Hyperf 的微服务实战】第三周·周三 RPC 客户端与自定义负载均衡
今天我们进入第三周周三,主题是RPC 客户端与自定义负载均衡。昨天我们实现了基本的 JSON-RPC 调用,今天要深入客户端的配置与扩展,让你能掌控多个服务实例之间的流量分配,为生产环境的高可用打下基础。你将亲手实现一个自定义的轮询负载均衡器,并观察请求如何在多个节点间分布。
今日目标
- 理解
@RpcClient注解的底层代理机制和配置。 - 掌握服务消费者配置,能手动指定多个服务节点。
- 理解负载均衡器接口
Hyperf\LoadBalancer\LoadBalancerInterface,并能编写自己的策略。 - 启动多个用户服务节点(通过多端口模拟),验证自定义负载均衡器的效果。
- 通过控制台日志和请求计数,观察负载分布,理解均衡算法的重要性。
一、环境准备(约 20 分钟)
继续使用hyperf-app项目,确保容器启动。
docker-composeexecswoolebashcd/var/www/hyperf-app我们今天需要模拟多个用户服务节点。由于是单机开发,我们可以让同一个 Swoole 进程监听多个端口来模拟多实例。Hyperf 支持在server.php中配置多个服务端,我们就在原有jsonrpc的基础上增加一个jsonrpc2服务。
同时确认已经安装了负载均衡组件,它随hyperf/json-rpc自动安装。
二、知识核心:客户端代理与负载均衡接口(约 1 小时)
1.@RpcClient注解背后
当你写:
#[RpcClient(name:"UserService",protocol:"jsonrpc-http",server:"jsonrpc")]privateUserServiceInterface$userService;Hyperf 会通过 AOP 生成一个动态代理对象,这个代理实现了UserServiceInterface。当你调用$this->userService->getUserById(1)时,实际执行流程:
- 代理对象拦截方法调用。
- 从消费者配置中获取对应服务名的所有节点信息(
nodes)。 - 通过负载均衡器选择一个节点。
- 使用协程化的 HTTP 或 TCP 客户端发送 JSON-RPC 请求。
- 将返回结果反序列化并返回。
消费者配置在config/autoload/services.php,可以配置多个节点:
'consumers'=>[['name'=>'UserService','nodes'=>[['host'=>'127.0.0.1','port'=>9502],['host'=>'127.0.0.1','port'=>9503],],],],默认的负载均衡器是随机选择,也可以指定为轮询、加权等。
2. 负载均衡器接口
所有负载均衡器必须实现Hyperf\LoadBalancer\LoadBalancerInterface:
interfaceLoadBalancerInterface{publicfunctionselect(array$nodes):Node;publicfunctionaddNode(Node$node):void;publicfunctionremoveNode(Node$node):void;publicfunctiongetNodes():array;}select()是核心,根据算法返回一个Hyperf\LoadBalancer\Node对象。Hyperf 内置了:
RandomLoadBalancer:随机RoundRobinLoadBalancer:轮询WeightedRoundRobinLoadBalancer:加权轮询ConsistentHashLoadBalancer:一致性哈希(需要参数)
我们可以自定义一个,例如基于 IP 哈希或带权重的自定义算法,然后通过配置指定给消费者。
三、实战:多节点模拟与自定义轮询负载均衡(约 3 小时)
步骤 1:添加第二个 JSON-RPC 服务端口
编辑config/autoload/server.php,在servers数组中增加第二个 JSON-RPC 服务,使用相同回调:
['name'=>'jsonrpc2',// 名称不同'type'=>Server::SERVER_HTTP,'host'=>'0.0.0.0','port'=>9503,'sock_type'=>SWOOLE_SOCK_TCP,'callbacks'=>[Event::ON_REQUEST=>[\Hyperf\JsonRpc\HttpServer::class,'onRequest'],],],现在应用会同时监听 9502 和 9503 两个端口,都提供 UserService。
步骤 2:配置消费者多节点
编辑config/autoload/services.php,添加两个节点:
<?phpreturn['consumers'=>[['name'=>'UserService','nodes'=>[['host'=>'127.0.0.1','port'=>9502],['host'=>'127.0.0.1','port'=>9503],],],],];此时如果不清除缓存,RPC 客户端会使用默认的随机负载均衡器。为了验证,我们需要替换为我们自定义的轮询均衡器。
步骤 3:创建自定义负载均衡器:简单轮询
创建app/LoadBalancer/CustomRoundRobin.php:
<?phpnamespaceApp\LoadBalancer;useHyperf\LoadBalancer\LoadBalancerInterface;useHyperf\LoadBalancer\Node;classCustomRoundRobinimplementsLoadBalancerInterface{privatearray$nodes=[];privateint$currentIndex=0;publicfunctionselect(array$nodes):Node{if(empty($nodes)){thrownew\RuntimeException('No nodes available');}$count=count($nodes);$node=$nodes[$this->currentIndex%$count];$this->currentIndex=($this->currentIndex+1)%$count;return$node;}publicfunctionaddNode(Node$node):void{$this->nodes[]=$node;}publicfunctionremoveNode(Node$node):void{$key=array_search($node,$this->nodes,true);if($key!==false){unset($this->nodes[$key]);$this->nodes=array_values($this->nodes);}}publicfunctiongetNodes():array{return$this->nodes;}}说明:我们维护一个$currentIndex,每次select递增,实现轮询。注意select接收的$nodes是由消费者配置传入的节点数组(Node对象),我们可以直接基于它进行索引选择。
步骤 4:配置消费者使用自定义均衡器
在services.php中为UserService消费者指定均衡器类:
'consumers'=>[['name'=>'UserService','load_balancer'=>App\LoadBalancer\CustomRoundRobin::class,'nodes'=>[['host'=>'127.0.0.1','port'=>9502],['host'=>'127.0.0.1','port'=>9503],],],],步骤 5:修改用户服务实现以便区分节点
为了肉眼观察请求被路由到哪个端口,我们让UserService在返回数据中带上当前服务端口。可以通过获取当前请求的端口来实现吗?由于服务端由 Swoole 处理,我们可以在getUserById中简单返回一个静态值,但为了区分,可以在构造函数中接收配置吗?更简单的方法是:在UserService中通过 Swoole 的Server信息获取当前监听端口。但由于服务类是无状态的,获取端口需要依赖注入 Request 对象,在 RPC 回调中可能获取不到 HTTP 请求对象。简单起见,我们让UserService返回一个包含“节点标识”的字段,可以通过添加环境变量或配置文件区分不同的实现。但因为我们是在同一个进程中监听两个端口,使用的是同一个UserService实例,无法直接区分。为了演示效果,我们可以在UserService中随机返回两个端口之一?这样无法证明负载均衡生效。更准确的做法是:在消费者调用时,记录所选节点的信息。我们可以通过切面或中间件在客户端记录。但为了直观,我们可以在文章控制器中,调用 RPC 后打印某个标记。不过这也不能区分节点。
更好的方式:在自定义负载均衡器的select方法中记录日志,每次选择节点时打印节点地址。这样我们就能从日志中清晰看到轮询过程。
修改CustomRoundRobin:
publicfunctionselect(array$nodes):Node{if(empty($nodes)){thrownew\RuntimeException('No nodes available');}$count=count($nodes);$node=$nodes[$this->currentIndex%$count];$this->currentIndex=($this->currentIndex+1)%$count;// 记录日志$logger=\Hyperf\Utils\ApplicationContext::getContainer()->get(\Psr\Log\LoggerInterface::class);$logger->info(sprintf("RPC请求路由到节点: %s:%d",$node->host,$node->port));return$node;}这样每次 RPC 调用时,都会在hyperf.log中输出一行日志,方便观察。
步骤 6:在文章控制器中触发多次调用
为了测试,我们可以在ArticleController中添加一个测试方法,连续多次调用getUserById,然后返回日志提示。或者直接写一个临时接口:
#[RequestMapping(path:'test-lb',methods:'get')]publicfunctiontestLoadBalance(){$logs=[];for($i=0;$i<5;$i++){$author=$this->userService->getUserById(1);// 这里不好直接获取节点,但日志已记录$logs[]=$author;}return['logs'=>$logs,'message'=>'查看运行时日志中路由记录'];}访问该接口,然后查看runtime/logs/hyperf.log,应该能看到类似:
[INFO] RPC请求路由到节点: 127.0.0.1:9502 [INFO] RPC请求路由到节点: 127.0.0.1:9503 [INFO] RPC请求路由到节点: 127.0.0.1:9502 [INFO] RPC请求路由到节点: 127.0.0.1:9503 [INFO] RPC请求路由到节点: 127.0.0.1:9502完美证明轮询工作正常。
步骤 7:调整权重模拟生产场景(可选)
我们可以进一步创建一个简单的加权随机负载均衡器,权重来自节点配置的weight属性。在services.php中可以为节点增加weight:
'nodes'=>[['host'=>'127.0.0.1','port'=>9502,'weight'=>3],['host'=>'127.0.0.1','port'=>9503,'weight'=>1],],然后创建App\LoadBalancer\CustomWeightedRandom.php,实现加权随机逻辑。但今天时间有限,我们只做轮询,有兴趣可以课后完成。
四、成果测试与验证(约 1 小时)
1. 测试步骤
- 重启服务
php bin/hyperf.php start。 - 访问
http://localhost:9501/articles/test-lb。 - 查看
runtime/logs/hyperf.log,确认出现交替的节点记录。
2. 故障转移测试(手动)
停止一个节点(通过修改server.php暂时注释掉jsonrpc2服务,或直接模拟端口不可用),再次请求,观察均衡器是否会报错。如果只有jsonrpc一个节点,负载均衡器会总是选择那个节点。如果两个节点都失效,连接会抛出异常。这为明天的熔断降级做铺垫。
3. 验证清单
| 检验项 | 方法 | 通过标准 |
|---|---|---|
| 多节点配置 | 查看server.php和services.php | 服务启动日志显示 9502 和 9503 监听 |
| 自定义负载均衡器 | 访问测试接口,查看日志 | 日志中交替出现两个端口 |
| 随机与轮询的区别 | 临时移除自定义均衡器,使用默认 | 随机日志无规律,轮询有明显交替 |
| 节点权重处理(可选) | 实现加权随机 | 权重高的节点被访问次数更多 |
| RPC 调用功能不受影响 | 文章详情仍能获取作者信息 | 返回正常 |
4. 思考题
- 为什么需要负载均衡?单节点压力大、无法水平扩展。负载均衡可以将流量分散到多个实例,提高系统吞吐和容错。
- 轮询的缺陷:如果节点性能不均,轮询可能导致性能差的节点负载过高。加权轮询可解决部分问题,但需要动态调整权重。
- 一致性哈希:当节点频繁变动时,一致性哈希可以最小化数据重新分布,适合有状态服务(如缓存服务器),但在无状态的 RPC 中多用于保证同一请求始终到同一节点(如灰度发布)。
五、今日作业与学习产出
- 提交代码:将自定义负载均衡器、
server.php、services.php、测试控制器提交到 Git。 - 扩展实践:
- 实现一个基于 IP 哈希的负载均衡器,让同一客户端 IP 的请求始终路由到同一节点(可以在
select中通过上下文获取请求 IP,但 RPC 客户端可能没有请求 IP 概念,可通过传入参数来哈希)。 - 尝试启动真实的多个 Docker 容器来模拟多实例,然后通过 Nacos 或 Consul 注册中心动态发现节点(提前预习)。
- 实现一个基于 IP 哈希的负载均衡器,让同一客户端 IP 的请求始终路由到同一节点(可以在
- 学习笔记:
- 绘制
@RpcClient代理调用流程图,标注负载均衡器的位置。 - 总结不同负载均衡策略的适用场景(随机、轮询、加权、一致性哈希、最少连接)。
- 绘制
- 挑战任务:
- 使用 Swoole 的
Channel实现一个简单的健康检查协程,定期检测所有节点是否可用,并动态更新负载均衡器的节点列表(类似服务注册中心的健康检查)。
- 使用 Swoole 的
通过今天的学习,你已经掌握了微服务通信中至关重要的客户端负载均衡能力,能根据业务需求灵活分配流量。明天我们将引入熔断器,让调用链路更加健壮,避免雪崩效应。
