尧图精选

【基于 Swoole+Hyperf 的微服务实战】 第五周·周五:网关聚合:批次请求合并与响应裁剪

🕒 发布时间:2026/9/15 7:51:12 📁 来源:尧图网络
今天我们进入的主题是网关聚合批次请求合并与响应裁剪。虽然昨天我们已经在网关中实现了并发请求聚合与基本的字段过滤但今天我们要更进一步解决一个关键性能瓶颈当需要获取多个同类资源时N次单独调用会造成网络开销爆炸。我们将学习如何将多个独立请求合并为一个批量请求Batch Request并配合更精细的响应裁剪打造一个既高效又灵活的网关聚合层。今日目标理解批次请求合并的原理对比单次调用、并发调用和批量调用的性能差异。在网关中实现一个批量RPC调用优化器自动收集一定时间窗口内对同一服务的请求合并为一个批量请求。增强响应裁剪功能支持嵌套字段过滤如author(username,email)。创建一个新的聚合接口/feed/v2展示合并与裁剪的综合效果。编写压测对比脚本证明批量合并带来的性能提升。一、环境准备约 20 分钟继续使用网关项目hyperf-gateway(端口 9500) 和后端服务hyperf-app(端口 9501, 9502) 已启动。确保昨日/feed接口可用但我们今天创建 v2 版本。进入网关容器docker-composeexecswoolebashcd/var/www/hyperf-gateway确认用户服务支持批量获取用户。目前我们只有GetUserById单个获取为了演示批量合并我们在hyperf-app的用户服务中增加一个BatchGetUsers方法。二、知识核心批次合并与嵌套裁剪约 1 小时1. 批次请求合并的价值在一个聚合接口中如果我们需要展示 20 篇文章的作者信息每种调用单独发一个 RPC就会产生 20 次网络往返。即使这些调用是协程并发执行的网络连接数仍然很多且后端服务压力也会增大。如果有一个批量查询接口一次调用就能返回所有需要的用户数据那么网络调用次数从 20 降到 1性能提升显著。合并策略静态合并在编写聚合代码时直接调用批量接口。适用于已知需要批量获取的场景。动态合并使用一个请求缓冲器在极短的时间窗口内收集对同一服务的所有请求然后合并为一个批量请求将结果分发给各个等待者。这种方式对业务代码透明但实现较复杂如使用 Swoole Channel。今天我们实现静态合并并简单介绍动态合并思路。2. 嵌套字段裁剪昨天的裁剪中间件只能进行第一层 Key 过滤无法处理如author{id,username}这样的嵌套结构。今天我们将实现支持对象嵌套的字段裁剪甚至可以解析类似 GraphQL 的简单语法。3. 实现方案在hyperf-app的用户服务中新增BatchGetUsersRPC 方法接收一组用户 ID返回用户数组。网关侧在聚合时先收集所有作者 ID然后发起一次批量 RPC 调用而不是多次单调用。增强ResponseFilterMiddleware使其支持点号或括号嵌套语法。三、实战批量合并与高级裁剪约 2.5 小时步骤 1为用户服务增加批量接口切换到hyperf-app项目编辑app/JsonRpc/UserServiceInterface.php增加方法声明publicfunctionbatchGetUsers(array$userIds):array;编辑app/JsonRpc/UserService.php和UserService2.php实现该方法publicfunctionbatchGetUsers(array$userIds):array{$users[];foreach($userIdsas$id){$user$this-getUserById($id);if(!empty($user)){$users[]$user;}}return$users;}确保UserServiceV2也实现该方法或继承。重启hyperf-app使 RPC 接口生效。步骤 2在网关中创建批量 RPC 调用工具尽管网关可以直接通过 HTTP 调用 JSON-RPC 端点但为了代码整洁我们创建一个服务类来封装批量调用。新建app/Service/UserServiceClient.php?phpnamespaceApp\Service;useHyperf\Guzzle\ClientFactory;useHyperf\Di\Annotation\Inject;classUserServiceClient{#[Inject]privateClientFactory$clientFactory;publicfunctionbatchGetUsers(array$userIds):array{if(empty($userIds))return[];$client$this-clientFactory-create();$responses[];// 使用协程并发请求多个不对我们要演示批量就发一个请求包含所有 ID。// JSON-RPC 支持数组参数我们可以这样调用$payload[jsonrpc2.0,methoduser/BatchGetUsers,params[$userIds],id1,];$resp$client-post(http://hyperf-app:9502,[json$payload,timeout5,]);$resultjson_decode((string)$resp-getBody(),true);return$result[result]??[];}// 单次获取作为备用publicfunctiongetUserById(int$id):array{$client$this-clientFactory-create();$resp$client-post(http://hyperf-app:9502,[json[jsonrpc2.0,methoduser/GetUserById,params[$id],id$id,],timeout5,]);$resjson_decode((string)$resp-getBody(),true);return$res[result]??[];}}步骤 3创建/feed/v2聚合接口新建app/Controller/FeedV2Controller.php?phpnamespaceApp\Controller;useHyperf\HttpServer\Annotation\Controller;useHyperf\HttpServer\Annotation\RequestMapping;useHyperf\Di\Annotation\Inject;useApp\Service\UserServiceClient;useHyperf\Guzzle\ClientFactory;#[Controller(prefix:/feed/v2)]classFeedV2ControllerextendsAbstractController{#[Inject]privateClientFactory$clientFactory;#[Inject]privateUserServiceClient$userServiceClient;#[RequestMapping(path:,methods:get)]publicfunctionindex(){$client$this-clientFactory-create();// 1. 获取文章列表$articlesResp$client-get(http://hyperf-app:9501/articles?per_page10);$articlesDatajson_decode((string)$articlesResp-getBody(),true);$articles$articlesData[data]??($articlesData[data][data]??[]);if(empty($articles)){return[code200,data[]];}// 2. 提取所有作者 ID$authorIdsarray_unique(array_column($articles,author_id));// 3. 使用批量接口一次性获取所有作者$authors[];try{$authorsList$this-userServiceClient-batchGetUsers($authorIds);foreach($authorsListas$user){$authors[$user[id]]$user;}}catch(\Throwable$e){// 降级逐个获取或返回空foreach($authorIdsas$id){$authors[$id][id$id,usernameunknown];}}// 4. 组装$feed[];foreach($articlesas$article){$authorId$article[author_id]??0;$article[author]$authors[$authorId]??null;$feed[]$article;}return[code200,data$feed];}}将此控制器也加入网关自处理路由白名单修改GatewayMiddleware中的$selfRoutes添加/feed/v2。步骤 4增强响应裁剪支持嵌套字段改进昨天的ResponseFilterMiddleware使其支持fields参数中的嵌套语法例如fieldsid,title,author(id,username)。修改app/Middleware/ResponseFilterMiddleware.php增加解析嵌套字段的逻辑privatefunctionfilterArray(array$data,array$allowed):array{$result[];$nested[];// 存放需要嵌套过滤的字段规则// 解析允许列表将嵌套规则分离$simpleAllowed[];foreach($allowedas$field){if(str_contains($field,()){// 解析如 author(id,email)preg_match(/(\w)\(([^)])\)/,$field,$matches);if($matches){$nested[$matches[1]]array_map(trim,explode(,,$matches[2]));}}else{$simpleAllowed[]$field;}}foreach($dataas$key$value){if(is_int($key)){// 列表项递归过滤$result[$key]is_array($value)?$this-filterArray($value,$allowed):$value;continue;}if(in_array($key,$simpleAllowed)||isset($nested[$key])){if(is_array($value)isset($nested[$key])){// 如果是关联数组且需要嵌套过滤$value$this-filterArray($value,$nested[$key]);}$result[$key]$value;}}return$result;}更新FeedV2Controller的中间件注解或路由中间件使其使用增强版过滤中间件。步骤 5动态合并思路可选拓展展示一个极简的批量请求合并缓冲器思路。新建app/Service/BatchRequestBuffer.php?phpnamespaceApp\Service;useSwoole\Coroutine\Channel;useSwoole\Coroutine\WaitGroup;classBatchRequestBuffer{privatestatic?Channel$channull;privatestaticarray$buffer[];privatestatic$timerIdnull;publicstaticfunctionadd(int$userId,callable$callback){if(!self::$chan){self::$channewChannel(100);go(function(){while(true){$userIdself::$chan-pop();self::$buffer[]$userId;// 定时器每 10ms 处理一批if(count(self::$buffer)1){\Swoole\Timer::after(10,function(){$idsself::$buffer;self::$buffer[];// 批量查询并回调$client\Hyperf\Utils\ApplicationContext::getContainer()-get(UserServiceClient::class);$users$client-batchGetUsers($ids);foreach($usersas$user){// 通知等待方这里简化}});}}});}self::$chan-push($userId);// 实际需要返回协程结果这里省略复杂实现}}此代码仅为演示思路实际生产环境可基于hyperf/guzzle的协程特性或使用专业的批处理库。四、成果测试与对比约 1 小时1. 测试批量合并效果首先保证hyperf-app用户服务已更新并重启。请求新聚合接口curl-HAuthorization: Bearer tokenhttp://localhost:9500/feed/v2应返回带有作者信息的 Feed。对比测试分别请求/feed并发单次调用和/feed/v2批量调用在hyperf-app的用户服务日志中观察被调用的次数。批量接口应只触发一次BatchGetUsers而非多次GetUserById。在网关控制器中记录时间$startmicrotime(true);// ... 处理$costmicrotime(true)-$start;return[code200,cost$cost,data$feed];对比两个接口的延迟批量接口在作者数量多时优势明显。2. 测试嵌套字段裁剪请求/feed/v2?fieldsid,title,author(id,username)验证返回结果中每个文章只包含 id、title 和 author 对象且 author 内部仅有 id 和 username没有 email 和 created_at。3. 测试降级停止用户服务端口 9502再次请求/feed/v2应触发降级逻辑作者显示为unknown文章列表正常。4. 压力测试使用ab或wrk对/feed/v2进行小规模压测观察 QPS 和错误率验证批量调用在高并发下不会产生性能倒退。五、今日作业与学习产出提交代码将UserServiceClient、FeedV2Controller、增强版ResponseFilterMiddleware以及hyperf-app中的BatchGetUsers实现提交到 Git。完善合并机制研究如何将批量合并应用到其他服务如商品服务并让网关聚合器自动判断何时使用批量。尝试实现动态请求缓冲器让业务代码无感知地享受批量优化。学习笔记画出 N1 问题在微服务中的体现及解决方案对比图单点调用、并发调用、批量调用。对比 RESTful、RPC 和 GraphQL 在数据聚合上的优缺点思考为什么 GraphQL 天然解决此类问题。挑战任务使用Hyperf 的自定义注解创建一个Batchable注解自动将标注的方法参数收集并合并调用。实现响应缓存对聚合结果进行短时间缓存并配合 Nacos 动态控制缓存 TTL。通过今天的学习你掌握了微服务网关聚合的高级技巧——批量请求合并与嵌套字段裁剪这使你的网关能够以更少的网络开销为客户端提供定制化的、高性能的数据视图。下一周我们将进入分布式事务与异步消息的世界继续拓展架构深度。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →