轻量级高性能PHP框架ycroute(中级): 基于yar的RPC服务 - 像调用本地函数一样调用远程函数

目录

  • 框架介绍
  • 运行环境
  • 代码结构
  • 路由配置
  • 过滤验签
  • 控制层
  • 加载器
  • 模型层
  • 数据交互dao层(可选)
  • Redis缓存操作
  • 数据库操作
  • 配置加载
  • 公共类加载
  • 公共函数
  • 日志模块
  • 视图层
  • RPC 介绍 - 像调用本地函数一样调用远程函数
  • RPC Server
  • RPC Client
  • RPC 并行调用
  • 附录 - Core_Model 中的辅助极速开发函数

RPC 介绍 - 像调用本地函数一样调用远程函数

GitHub下载地址: https://github.com/caohao-php/ycroute

传统web应用弊端

传统的Web应用, 一个应用随着业务快速增长, 开发人员的流转, 就会慢慢的进入一个恶性循环, 代码量上只有加法没有了减法. 因为随着系统变复杂, 牵一发就会动全局, 而新来的维护者, 对原有的体系并没有那么多时间给他让他全面掌握. 即使有这么多时间, 要想掌握以前那么多的维护者的思维的结合, 也不是一件容易的事情…

那么, 长次以往, 这个系统将会越来越不可维护…. 到一个大型应用进入这个恶性循环, 那么等待他的只有重构了.

那么, 能不能对这个系统做解耦呢? 我们已经做了很多解耦了, 数据, 中间件, 业务, 逻辑, 等等, 各种分层. 但到Web应用这块, 还能怎么分呢, MVC我们已经做过了….

解决利器—微服务

目前比较流行的解决方案是微服务,它可以让我们的系统尽可能快地响应变化,微服务是指开发一个单个小型的但有业务功能的服务,每个服务都有自己的处理和轻量通讯机制,可以部署在单个或多个服务器上。微服务也指一种种松耦合的、有一定的有界上下文的面向服务架构。也就是说,如果每个服务都要同时修改,那么它们就不是微服务,因为它们紧耦合在一起;如果你需要掌握一个服务太多的上下文场景使用条件,那么它就是一个有上下文边界的服务,这个定义来自DDD领域驱动设计。

相对於单体架构和SOA,它的主要特点是组件化、松耦合、自治、去中心化,体现在以下几个方面:

  • 一组小的服务

    服务粒度要小,而每个服务是针对一个单一职责的业务能力的封装,专注做好一件事情。

  • 独立部署运行和扩展

    每个服务能够独立被部署并运行在一个进程内。这种运行和部署方式能够赋予系统灵活的代码组织方式和发布节奏,使得快速交付和应对变化成为可能。

  • 独立开发和演化

    技术选型灵活,不受遗留系统技术约束。合适的业务问题选择合适的技术可以独立演化。服务与服务之间采取与语言无关的API进行集成。相对单体架构,微服务架构是更面向业务创新的一种架构模式。

  • 独立团队和自治

    团队对服务的整个生命周期负责,工作在独立的上下文中,自己决策自己治理,而不需要统一的指挥中心。团队和团队之间通过松散的社区部落进行衔接。

我们可以看到整个微服务的思想就如我们现在面对信息爆炸、知识爆炸是一样的:通过解耦我们所做的事情,分而治之以减少不必要的损耗,使得整个复杂的系统和组织能够快速的应对变化。

微服务的基石—RPC服务框架

微服务包含的东西非常多,这里我们只讨论RPC服务框架,ycroute框架基于Yar扩展为我们提供了RPC跨网络的服务调用基础,Yar是一个非常轻量级的RPC框架, 使用非常简单, 对于Server端和Soap使用方法很像,而对于客户端,你可以像调用本地对象的函数一样,调用远程的函数。

RPC Server

安装环境 (客户端服务端都需要安装)

扩展: yar.so

扩展: msgpack.so 可选,一个高效的二进制打包协议,用于客户端和服务端之间包传输,还可以选php、json, 如果要使用Msgpack做为打包协议, 就需要安装这个扩展。

服务加载

我们在 framework/application/controllers/Rpcserver.php 中将 Model 层作为服务,提供给远程的其它程序调用,RPC Client 便可以像调用本地函数一样,调用远程的服务,如下我们将 UserinfoModel 和 TradeModel 两个模型层提供给远程程序调用。

class RpcserverController extends Core_Controller {
    public function init() {
        parent::init(); //必须
    }

    //用户信息服务
    public function userinfoModelAction() {
    	$user_model = Loader::model('UserinfoModel'); //模型层
        $yar_server = new Yar_server($user_model);
		$yar_server->handle();
		exit;
    }
	
    //支付服务
    public function tradeModelAction() {
    	$trade_model = Loader::model('TradeModel'); //模型层
        $yar_server = new Yar_server($trade_model);
		$yar_server->handle();
		exit;
    }
}

上面一共提供了2个服务,UserinfoModel 和 TradeModel 分别通过http://localhost/index.php?c=rpcserver&m=userinfoModel 和 http://localhost/index.php?c=rpcserver&m=tradeModel 来访问,我们来看看 UserinfoModel 一共有哪些服务:

Image

从上图可以看到,UserinfoModel 类的所有 public 方法都会被当做服务提供,包括他继承的父类 public 方法。

服务校验

为了安全,我们最好对客户端发起的RPC服务请求做校验。在 framework/application/plugins/Filter.php 中做校验:

class FilterPlugin extends Yaf_Plugin_Abstract {
    var $params;

    //路由之前调用
    public function routerStartUp ( Yaf_Request_Abstract $request , Yaf_Response_Abstract $response) {
        $this->params = & $request->getParams();

        $this->_auth();
        
        if(!empty($this->params['rpc'])) {
        	$this->_rpc_auth(); //rpc 调用校验
    	}
    }
    
    //rpc调用校验
    protected function _rpc_auth()
    {
       	$signature = $this->get_rpc_signature($this->params);
       	if($signature != $this->params['signature']) {
       		$this->response_error(1, 'check failed');
       	}
    }
    
    //rpc签名计算,不要改函数名,在RPC客户端中 system/YarClientProxy.php 我们也会用到这个函数,做签名。
    public function get_rpc_signature($params) 
    {
    	$secret = 'MJCISDYFYHHNKBCOVIUHFUIHCQWE';
    	unset($params['signature']);
    	ksort($params);
	    reset($params);
	    unset($auth_params['callback']);
	    unset($auth_params['_']);
	    $str = $secret;
	    foreach ($params as $value) {
		    $str = $str . trim($value);
	    }
			
	    return md5($str);
    }
    
    ...
    
}

切记不要修改签名生成函数 get_rpc_signature 的名字和参数,因为在 RPC Client 我们也会利用这个函数做签名,如果需要修改,请在 system/YarClientProxy.php 中做相应修改,以保证客户端和服务器之间的调用正常。

RPC Client

yar 除了支持 http 之外,还支持tcp, unix domain socket传输协议,不过ycroute中只用了 http ,当然 http 也可以开启 keepalive 以获得更高的传输性能,只不过相比 socket, http 协议还是多了不少的协议头部的开销。

安装环境

扩展: yar.so

扩展: msgpack.so 可选,一个高效的二进制打包协议,用于客户端和服务端之间包传输,还可以选php、json, 如果要使用Msgpack做为打包协议, 就需要安装这个扩展。

调用逻辑

例子:

class UserController extends Core_Controller {

    ...
    
    //获取用户信息(从远程)
    public function getUserInfoByRemoteAction() {
        $userId = $this->params['userid'];
        
        if (empty($userId)) {
            $this->response_error(10000017, "user_id is empty");
        }
        
    	$model = Loader::remote_model('UserinfoModel');
    	$userInfo = $model->getUserinfoByUserid($userId);
    	$this->response_success($userInfo);
    }
    
    ...
}

通过 $model = Loader::remote_model(‘UserinfoModel’); 可以获取远程 UserinfoModel,参数是framework/application/config/rpc.php配置里的键值:

$remote_config['UserinfoModel']['url'] = "http://localhost/index.php?c=rpcserver&m=userinfoModel&rpc=true";  //服务地址
$remote_config['UserinfoModel']['packager'] = FALSE;         //RPC包类型,FALSE则选择默认,可以为 "json", "msgpack", "php", msgpack 需要安装扩展
$remote_config['UserinfoModel']['persitent'] = FALSE;        //是否长链接,需要服务端支持keepalive
$remote_config['UserinfoModel']['connect_timeout'] = 1000;   //连接超时(毫秒),默认 1秒 
$remote_config['UserinfoModel']['timeout'] = 5000;           //调用超时(毫秒), 默认 5 秒
$remote_config['UserinfoModel']['debug'] = TRUE;             //DEBUG模式,调用异常是否会打印到屏幕,线上关闭

$remote_config['TradeModel']['url'] = "http://localhost/index.php?c=rpcserver&m=tradeModel&rpc=true";
$remote_config['TradeModel']['packager'] = FALSE;
$remote_config['TradeModel']['persitent'] = FALSE;
$remote_config['TradeModel']['connect_timeout'] = 1000; 
$remote_config['TradeModel']['timeout'] = 5000;       
$remote_config['TradeModel']['debug'] = TRUE;            

这样,我们就可以把 model 当成本地对象一样调用远程 UserinfoModel 的成员方法。

url签名

调用远程服务的时候,system/YarClientProxy.php 会从配置中获取服务的 url, 然后调用 FilterPlugin::get_rpc_signature 方法对 URL 做签名,并将签名参数拼接到 url 结尾,发起调用。

class YarClientProxy {
	
	...
	
	public static function get_signatured_url($url) {
		$get = array();
		$t = parse_url($url, PHP_URL_QUERY);
		parse_str($t, $get);
		$get['timestamp'] = time();
		$get['auth'] = rand(11111111, 9999999999);
		$signature = FilterPlugin::get_rpc_signature($get);
		return $url . "&timestamp=" . $get['timestamp'] . "&auth=" . $get['auth'] . "&signature=" . $signature;
	}
	
	...
}

调用异常日志

日志位于 /data/app/logs/localhost 下,localhost 为项目域名。

[root@gzapi: /data/app/logs/localhost]# ls
yar_client_proxy.20190214.log.wf

[ERROR] [2019-02-14 18:57:13] [0] [index.php|23 => | => User.php|61 => YarClientProxy.php|46] [218.30.116.3] [/index.php?c=user&m=getUserInfoByRemote&userid=6818810&token=c9bea5dee1f49488e2b4b4645ff3717e1] [] [] - “yar_client_call_error URL=[http://tr.gaoqu.site/index.php?c=rpcserver&m=userinfoModel&rpc=true] , Remote_model=[UserinfoModel] Func=[getUserinfoByUserid] Exception=[server responsed non-200 code ‘500’]”

RPC 并行调用

yar框架支持并行调用,可以同时调用多个服务,这样可以充分利用CPU性能,避免IO等待,提升系统性能,按照yar的流程,你首先得一个个注册服务,然后发送注册的调用,然后reset 重置调用。在ycroute 中,一个函数就可以了。

用 Loader::concurrent_call($call_params); 来并行调用RPC服务, 其中 call_params是调用参数数组。

如下数组包含4个元素,每个调用都包含 model, method 两个必输参数,以及 parameters, callback , error_callback 三个可选参数。

  • model : 服务名,是framework/application/config/rpc.php配置里的键值。
  • method : 调用函数
  • parameters : 函数的参数,是一个数组,数组的个数为参数的个数
  • callback : 回调函数,调用成功之后回调,针对的是各自的回调。
  • error_callback : 调用失败之后会回调这个函数,其中调用超时不会回调该方法, 针对的也是各自的回调。
class UserController extends Core_Controller {
    //获取用户信息(并行远程调用)
    public function multipleGetUsersInfoByRemoteAction() {
    	$userId = $this->params['userid'];
    	
    	$call_params = array();
    	$call_params[] = ['model' => 'UserinfoModel', 
                          'method' => 'getUserinfoByUserid', 
                          'parameters' => array($userId), 
                          "callback" => array($this, 'callback1')];
    					  
    	$call_params[] = ['model' => 'UserinfoModel', 
                          'method' => 'getUserInUserids', 
                          'parameters' => array(array(6860814, 6870818)), 
                          "callback" => array($this, 'callback2'),
                          "error_callback" => array($this, 'error_callback')];
    					  
    	$call_params[] = ['model' => 'UserinfoModel', 
                          'method' => 'getUserByName', 
                          'parameters' => array('CH.smallhow')];
			  
    	//不存在的方法
    	$call_params[] = ['model' => 'UserinfoModel', 
                          'method' => 'unknownMethod', 
                          'parameters' => array(),
                          "error_callback" => array($this, 'error_callback')];
                          
    	Loader::concurrent_call($call_params);
    	echo json_encode($this->retval);
    	exit;
    }
    
    //回调函数1
    public function callback1($retval, $callinfo) {
    	$this->retval['callback1']['retval'] = $retval;
    	$this->retval['callback1']['callinfo'] = $callinfo;
    }
    
    //回调函数2
    public function callback2($retval, $callinfo) {
    	$this->retval['callback2']['retval'] = $retval;
    	$this->retval['callback2']['callinfo'] = $callinfo;
    }
    
    //错误回调
    public function error_callback($type, $error, $callinfo) {
    	$tmp['type'] = $type;
    	$tmp['error'] = $error;
    	$tmp['callinfo'] = $callinfo;
    	$this->retval['error_callback'][] = $tmp;
    }
}

我特意将第4个调用的method设置一个不存在的函数,大家可以看下上面的并行调用的结果:

{
    "error_callback":[
        {
            "type":4,
            "error":"call to undefined api ::unknownMethod()",
            "callinfo":{
                "sequence":4,
                "uri":"http://tr.gaoqu.site/index.php?c=rpcserver&m=userinfoModel&rpc=true×tamp=1550142590&auth=5930400101&signature=fc0ed911c624d9176523544421a0248d",
                "method":"unknownMethod"
            }
        }
    ],
    "callback1":{
        "retval":{
            "user_id":"6818810",
            "appid":"wx385863ba15f573b6",
            "open_id":"oXtwn4wkPO4FhHmkan097DpFobvA",
            "union":null,
            "session_key":"Et1yjxbEfRqVmCVsYf5qzA==",
            "nickname":"芒果",
            "city":"Yichun",
            "province":"Jiangxi",
            "country":"China",
            "avatar_url":"https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83epqg7FwyBUGd5xMXxLQXgW2TDEBhnNjPVla8GmKiccP0pFiaLK1BGpAJDMiaoyGHR9Nib2icIX9Na4Or0g/132",
            "gender":"1",
            "form_id":"",
            "token":"5a350bc05bbbd9556f719a0b8cf2a5ed",
            "amount":"0",
            "last_login_time":"2018-10-04 16:01:27",
            "regist_time":"2018-06-29 21:24:45",
            "updatetime":"2018-10-04 16:01:27"
        },
        "callinfo":{
            "sequence":1,
            "uri":"http://tr.gaoqu.site/index.php?c=rpcserver&m=userinfoModel&rpc=true×tamp=1550142590&auth=8384256613&signature=c0f9c944ae070d2eb38c8e9638723a2e",
            "method":"getUserinfoByUserid"
        }
    },
    "callback2":{
        "retval":{
            "6860814":{
                "user_id":"6860814",
                "nickname":"Smile、格调",
                "avatar_url":"https://wx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKNE5mFLk33q690Xl1N6mrehQr0ggasgk8Y4cuaUJt4CNHORwq8rVjwET7H06F3aDjU5UiczjpD4nw/132",
                "city":"Guangzhou"
            },
            "6870818":{
                "user_id":"6870818",
                "nickname":"Yang",
                "avatar_url":"https://wx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTLTKBoU1tdRicImnUHyr43FdMulSHRhAlsQwuYgAyOlrwQaLGRoFEHbgfVuyEV1K1VU2NMmm0slS4w/132",
                "city":"Hengyang"
            }
        },
        "callinfo":{
            "sequence":2,
            "uri":"http://tr.gaoqu.site/index.php?c=rpcserver&m=userinfoModel&rpc=true×tamp=1550142590&auth=7249482640&signature=26c419450bb4747ac166fbaa4a242b77",
            "method":"getUserInUserids"
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章