💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
Yar 是一个轻量级, 高效的[RPC](https://developer.51cto.com/art/201906/597963.htm)(是远程过程调用(Remote Procedure Call)的缩写形式)框架, 它提供了一种简单方法来让PHP项目之间可以互相远程调用对方的本地方法. 并且Yar也提供了并行调用的能力. 可以支持同时调用多个远程服务的方法 是[Laruence大佬](https://www.laruence.com/2020/03/16/5578.html)还在微博的时候为了优化微博的性能而开发的一个工具,Yar的并行调用在微博被大量应用以降低用户请求耗时 ## 需求 如果需要使用Msgpack作为打包协议, 则需要在configure的时候加上--enable-msgpack, 并且要保证Msgpack扩展也安装到PHP内. [ https://pecl.php.net/package/yar](https://pecl.php.net/package/yar) * [预定义常量](https://www.php.net/manual/en/yar.constants.php) * [例子](https://www.php.net/manual/en/yar.examples.php) * [Yar\_Server](https://www.php.net/manual/en/class.yar-server.php)— Yar\_Server类 * [Yar\_Server :: \_\_ construct](https://www.php.net/manual/en/yar-server.construct.php)—创建一个HTTP RPC 服务器 * [Yar\_Server :: handle](https://www.php.net/manual/en/yar-server.handle.php)—启动RPC服务器 <br> * [Yar\_Client](https://www.php.net/manual/en/class.yar-client.php)— Yar\_Client类 * [Yar\_Client :: \_\_ call](https://www.php.net/manual/en/yar-client.call.php)(string`$method`,array`$parameters`) :void—调用远程服务 * [Yar\_Client :: \_\_ construct](https://www.php.net/manual/en/yar-client.construct.php)(string`$url`\[,array`$options`\] )—创建一个客户端实例 * [Yar\_Client :: setOpt](https://www.php.net/manual/en/yar-client.setopt.php)(int`$name`,[mixed](https://www.php.net/manual/en/language.pseudo-types.php#language.types.mixed)`$value`) :[Yar\_Client](https://www.php.net/manual/en/class.yar-client.php)—设置调用的配置 <br> * [Yar\_Concurrent\_Client](https://www.php.net/manual/en/class.yar-concurrent-client.php)— Yar\_Concurrent\_Client类 * [Yar\_Concurrent\_Client :: call](https://www.php.net/manual/en/yar-concurrent-client.call.php)(string`$uri`,string`$method`\[,array`$parameters`\[,[callable](https://www.php.net/manual/en/language.types.callable.php)`$callback`\[,[callable](https://www.php.net/manual/en/language.types.callable.php)`$error_callback`\[,array`$options`\]\]\]\] ) :int—注册一个并行的服务调用 * [Yar\_Concurrent\_Client :: loop](https://www.php.net/manual/en/yar-concurrent-client.loop.php)(\[[callable](https://www.php.net/manual/en/language.types.callable.php)`$callback`\[,[callable](https://www.php.net/manual/en/language.types.callable.php)`$error_callback`\]\] ) :bool—发送所有注册的并行调用 * [Yar\_Concurrent\_Client :: reset](https://www.php.net/manual/en/yar-concurrent-client.reset.php)(void) :bool—清除所有已注册的调用 <br> * [Yar\_Server\_Exception](https://www.php.net/manual/en/class.yar-server-exception.php)— Yar\_Server\_Exception类 * [Yar\_Server\_Exception :: getType](https://www.php.net/manual/en/yar-server-exception.gettype.php)—获取异常的原始类型 <br/> * [Yar\_Client\_Exception](https://www.php.net/manual/en/class.yar-client-exception.php)— Yar\_Client\_Exception类 * [Yar\_Client\_Exception :: getType](https://www.php.net/manual/en/yar-client-exception.gettype.php)—获取异常的原始类型 传统的Web应用, 一个进程, 一个请求, 天经地义. 然而, 当一个请求的处理中, 涉及到多出数据源, 并且他们之间具有一定的不依赖性. 还是传统的Web应用, 一个应用随着业务快速增长, 开发人员的流转, 就会慢慢的进入一个恶性循环, 代码量上只有加法没有了减法. 因为随着系统变复杂, 牵一发就会动全局, 而新来的维护者, 对原有的体系并没有那么多时间给他让他全面掌握. 即使有这么多时间, 要想掌握以前那么多的维护者的思维的结合, 也不是一件容易的事情... 那么, 长次以往, 这个系统将会越来越不可维护.... 到一个大型应用进入这个恶性循环, 那么等待他的只有重构了. 那么, 能不能对这个系统做解耦呢? 我们已经做了很多解耦了, 数据, 中间件, 业务, 逻辑, 等等, 各种分层. 但到Web应用这块, 还能怎么分呢, MVC我们已经做过了.... 基于此, Yar或许能解决你遇到的这俩个问题... Yar是一个非常轻量级的RPC框架, 我在实现Yar的时候, 追求了极致的轻量级, 它使用非常简单, 对于Server端: ~~~ <?php class API { /** * the doc info will be generated automatically into service info page. * @params * @return */ public function api($parameter, $option = "foo") { } protected function client_can_not_see() { } } $service = new Yar_Server(new API()); $service->handle(); ?> ~~~ 和Soap使用方法很相像吧? 是的, 就这样, 你的API类就可以对外提供服务了.. Yar为了方便开发, 把文档和接口绑定到了一起, 对于上面的例子, 如果我们是简单的GET请求这个接口地址的话, 我们就会看到如下的信息页面: ![](https://img.kancloud.cn/a1/1a/a11a27c18fb822c5a8ee438ee416ceea_673x221.png) 这样, 我们可以在注释中,把接口的信息标注好, 就可以让文档和接口在一起了. 而对于Client端来说, 简单的串行调用, 会非常之简单: ``` <?php $client = new Yar_Client("http://host/api/"); $result = $client->api("parameter); ?> ``` 这样一来, 如果你有多个服务, 你只需要一个client. 那么, 最激动人心的并行化调用呢? ``` <?php function callback($retval, $callinfo) { var_dump($retval); } Yar_Concurrent_Client::call("http://host/api/", "api", array("parameters"), "callback"); Yar_Concurrent_Client::call("http://host/api/", "api", array("parameters"), "callback"); Yar_Concurrent_Client::call("http://host/api/", "api", array("parameters"), "callback"); Yar_Concurrent_Client::call("http://host/api/", "api", array("parameters"), "callback"); Yar_Concurrent_Client::loop(); //send ?> ``` 这样, 所有的请求会一次发出, 只要有任何一个请求完成, 回调函数"callback"就会被立即调用. 这里还有一个细节, Yar见缝插针的不会浪费任何时间, 在这些请求发送完成以后, Yar会调用一次callback, 和普通的请求返回回调不同, 这次的调用的$callinfo参数为空. 这样一来, 我们就可以先发送请求, 然后再第一次回调, 继续做我们当前进程的工作, 等所有工作结束以后, 再交给Yar去获取并行RPC的响应 ``` <?php function callback($retval, $callinfo) { if ($callinfo == NULL) { //做本地的逻辑 return TRUE; } //RPC请求返回, 返回值在$retval } ``` 有了这些, 我们就可以把一个Web应用中, 多个数据源并行处理, 从而也能把这些逻辑解耦, 分开部署... ### **链接持久化** 也就是YAR\_OPT\_PERSISTENT, 这个配置之前一直有,只不过之前是设计为跨请求的持久化,这次做了优化,变成了基于PHP请求生命期的链接保持。 也就是说,当你对一个Yar\_Client设置了YAR\_OPT\_PERSISTEN为true的话,在一次RPC调用结束后,Yar不会销毁这个链接,从而加速后续的针对同样这个Client的RPC调用,我们来看个例子: ``` <?php function bench($client) { $start = microtime(true); $client->header("connection"); echo microtime(true) - $start, "s\n"; } $client = new Yar_Client("http://remote_host/index.php"); $client->setOpt(YAR_OPT_PERSISTENT, 1); bench($client); bench($client); bench($client); ``` 输出: ``` 0.090613842010498s 0.045492887496948s 0.045512914657593s ``` 可见,第二次调用的时候,耗时降低了一半,这是因为节省了TCP链接建立的耗时。 最后,链接会在PHP请求结束以后,整体释放,也就是PHP请求生命期的存活,不会垮请求,也不用担心内存泄漏。 不过很遗憾,这个并不能用于加速并行调用: ``` function callback($retval, $callinfo) { global $start; if ($callinfo) { echo "Num ", $callinfo["sequence"] , " costs: ", microtime(true) - $start, "s\n"; } } Yar_Concurrent_Client::call("http://remote_host/index.php", "header", array("connection"), NULL, NULL, array(YAR_OPT_PERSISTENT=>1)); $start = microtime(true); Yar_Concurrent_Client::loop("callback", function($error) { var_dump($error); }); $start = microtime(true); Yar_Concurrent_Client::loop("callback", function($error) { var_dump($error); }); ``` 我们会发现俩次调用没有加速的效果: ``` Num 1 costs: 0.091023921966553s Num 1 costs: 0.090677976608276s ``` 这跟Yar底层使用的libcurl有关系,从libcurl的官方文档关于curl_multi_add_handle: >[info]When an easy interface is added to a multi handle, it will use a shared connection cache owned by the multi handle. Removing and adding new easy handles will not affect the pool of connections or the ability to do connection re-use. 也就是说,只要我把一个我们打开的libcurl cp通过curl_multi_add_handle加入到并行队列,这个cp就会使用libcurl multi自己管理的共享连接池,不会受Yar自己管理的persistent与否影响了。而这个共享是指在整个一次libcurl multi的请求过程中的共享,但事实上因为我们一次会发出所有请求,不会存在一个请求完成下一个请求开始的情况,那么也就不会存在复用了。 不过,我后来观察到在某些版本之上的libcurl,比如我测试的7.58.0下,可以看到加速的效果(这个是本机测试,所以看起来速度快很多): ``` Num 1 costs: 0.0017080307006836s Num 1 costs: 0.0010600090026855s ``` 后续有时间需要慢慢研究下,从libcurl哪个版本开始的变化。 自定义DNS 这个是来自HuangeChaodian网友的PR,基本上是有的时候,我们需要自定义某个Host的DNS解析结果,比如在测试的时候,把一个Hostname指向本地。 诚然,我们可以使用编辑hosts来达到这个效果, 但如果能在代码中切换,还是会方便不少,看例子: ``` <?php $client = new Yar_Client("http://remote_host/index.php"); $client->setOpt(YAR_OPT_RESOLVE, "remote_host:80:127.0.0.1"); ``` 这样,Yar就会把remote_host解析为127.0.0.1了, 可以方便我们做一些本地调试。