輕量級高性能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"
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章