摘要:基于擴展實現真正的數據庫連接池這種方案中,項目占用的連接數僅僅為。一種是連接暫時不再使用,其占用狀態解除,可以從使用者手中交回到空閑隊列中這種我們稱為連接的歸隊。源碼剖析系列目錄
作者:bromine
鏈接:https://www.jianshu.com/p/1a7...
來源:簡書
著作權歸作者所有,本文已獲得作者授權轉載,并對原文進行了重新的排版。
Swoft Github: https://github.com/swoft-clou...
對于基于php-fpm的傳統php-web應用,包括且不限于Mysql,Redis,RabbitMq,每次請求到來都需要為其新建一套獨享的的連接,這直接帶來了一些典型問題:
連接開銷:連接隨著http請求到來而新建,隨著請求返回而銷毀,大量連接新建銷毀是對系統資源的浪費。
連接數量過高:每一個請求都需要一套自己的連接,系統連接數和并發數會成一個近線性的關系。如果系統并發量達到了1w,那么就需要建立1w個對應的連接,這對于Mysql之類的后端服務而言,是一個大的負荷。
空閑連接:假設我們有一個接口使用了一個Mysql連接。該接口在一開始進行一次sql查詢后,后面的操作都是sql無關的,那么該請求占據的空閑連接完全就是一種資源的浪費。
對于異步系統而言,這個問題變得更加的嚴峻。一個請求處理進程要對同一個服務進行并發的操作,意味著這個請求要持有1個以上同類的連接,這對于系統壓力而言,無疑是雪上加霜了,所以連接池對于基于Swoole的Web框架而言已經是一個必需實現的機制了。
Swoft連接池的生命周期與進程模型連接池作為一個SCOPE為SINGLETON的典型Bean,
其實例最早會在SwoftBeanBeanFactory::reload()階段被初始化。
對于RPC或者HTTP請求而言,關系最密切的進程肯定是Worker和Task進程了。
對于這兩者而言 SwoftBeanBeanFactory::reload()會在swoole的onWorkerStart事件的回調階段階段被調用。
//SwoftBootstrapServerServerTrait(HttpServer和RpcServer都使用了該性狀) /** * OnWorkerStart event callback * * @param Server $server server * @param int $workerId workerId * @throws InvalidArgumentException */ public function onWorkerStart(Server $server, int $workerId) { // Init Worker and TaskWorker $setting = $server->setting; $isWorker = false; if ($workerId >= $setting["worker_num"]) { // TaskWorker ApplicationContext::setContext(ApplicationContext::TASK); ProcessHelper::setProcessTitle($this->serverSetting["pname"] . " task process"); } else { // Worker $isWorker = true; ApplicationContext::setContext(ApplicationContext::WORKER); ProcessHelper::setProcessTitle($this->serverSetting["pname"] . " worker process"); } $this->fireServerEvent(SwooleEvent::ON_WORKER_START, [$server, $workerId, $isWorker]); //beforeWorkerStart()內部會調用BeanFactory::reload(); $this->beforeWorkerStart($server, $workerId, $isWorker); }
這意味著此時的連接池對象的生命周期是 進程全局期而不是程序全局期
將進程池設計為進程全局期,而不是共享程度最高的程序全局期原因,個人認為主要有3個
多個進程同時對一個連接進行讀寫會導致數據傳輸錯亂,需要保證連接不會被同時訪問。
Worker進程對程序全局期的對象進行寫操作時會導致寫時復制,產生一個進程全局期的副本,程序全局期較難維持。
使用進程全局期的話可以利用現有的Bean機制管理對象,減少的特殊編碼。
Process中的連接池//SwoftProcessProcessBuilder.php /** * After process * * @param string $processName * @param bool $boot 該參數即Process 注解的boot屬性 */ private static function beforeProcess(string $processName, $boot) { if ($boot) { BeanFactory::reload(); $initApplicationContext = new InitApplicationContext(); $initApplicationContext->init(); } App::trigger(ProcessEvent::BEFORE_PROCESS, null, $processName); } }
Swoft中的Process有兩種:一種是定義Process 注解的boot屬性為true的 前置進程,這種進程隨系統啟動而啟動的 ;另一種是定義Process 注解的boot屬性為false的 用戶自定義進程 ,該類進程需要用戶在需要的時候手動調用ProcessBuilder::create()啟動 。
但是無論是何者,最終都會在Process中調用beforeProcess()進行子進程的初始化。對于 boot為true的 前置進程 ,由于其啟動時父進程還未初始化bean容器,所以會多帶帶進行bean容器初始化,而對于boot為false的其他 用戶自定義進程,其會直接繼承父進程的Ioc容器。
Swoft基本上遵守著一個進程擁有一個多帶帶連接池的規則,這樣所有進程中的連接都是獨立的,保證了連接
不會被同時讀寫。唯獨在Process中有一個特例。如果對先使用依賴連接池的服務,如對Mysql進行CRUD,再調用ProcessBuilder::create()啟動 用戶自定義進程,由于用戶自定義進程 會直接繼承父進程的Bean容器而不重置,這時子進程會獲得父進程中的連接池和連接。
/** * The adapter of command * @Bean() */ class HandlerAdapter { /** * before command * * @param string $class * @param string $command * @param bool $server */ private function beforeCommand(string $class, string $command, bool $server) { if ($server) { return; } $this->bootstrap(); BeanFactory::reload(); // 初始化 $spanId = 0; $logId = uniqid(); $uri = $class . "->" . $command; $contextData = [ "logid" => $logId, "spanid" => $spanId, "uri" => $uri, "requestTime" => microtime(true), ]; RequestContext::setContextData($contextData); } }
命令行腳本擁有自己多帶帶的Bean容器,其情況和Process相似且更簡單,嚴格遵循一個進程一個連接池,這里不再累述。
假設Worker數目為j,Task數目為k,Process數為l,Command數為m,每個進程池內配置最大連接數為n,部署機器數為x,不難看出每個swoft項目占用的連接數為(j+k+l+m)*n*x。
天峰本人曾經提過另一種基于Swoole的連接池模型。
Rango-<基于swoole擴展實現真正的PHP數據庫連接池>
這種方案中,項目占用的連接數僅僅為k*x。
除了Task進程各個進程并不直接持有連接池,而是通過向Task進程提交指令(task(),sendMessage())讓其代為進行連接池相關服務的操作,至少需要額外的一次進程間通信(默認為Unix Socket)。
該方案雖然能夠更好的復用連接和節省連接數,但機制實現并不方便。從另一個角度去看,Swoft的連接池方案是為了解決使用Swoole時,單進程并發執行的連接數要求問題;Range提出的連接池方案是為了解決超大流量系統下對Mysql等服務的壓力控制問題。兩者適合不同的場景,其目的和意義在一定程度下是重合的,但并不是完全一樣的。
連接池根據當前是否協程環境選擇一種合適的隊列結構作為連接的容器。
SplQueue:SplQueue是PHP標準庫的數據結構,底層是一個雙向鏈表,在隊列操作這種特化場景下,性能遠高于底層使用鏈表+哈希表實現的array()數據結構。
SwooleCoroutineChannel是Swoole提供的協程相關的數據結構,不僅提供了常規的隊列操作。在協程環境下,當其隊列長度從0至1之間切換時,會自動讓出協程控制權并喚醒對應的生產者或消費者。
連接的獲取SwoftPoolConnectionPool.php abstract class ConnectionPool implements PoolInterface { /** * Get connection * * @throws ConnectionException; * @return ConnectionInterface */ public function getConnection():ConnectionInterface { //根據執行環境選擇容器 if (App::isCoContext()) { $connection = $this->getConnectionByChannel(); } else { $connection = $this->getConnectionByQueue(); } //連接使用前的檢查和重新連接 if ($connection->check() == false) { $connection->reconnect(); } //加入到全局上下文中,事務處理和資源相關的監聽事件會用到 $this->addContextConnection($connection); return $connection; } }
SwoftPoolConnectionPool.php /** * Get connection by queue * * @return ConnectionInterface * @throws ConnectionException */ private function getConnectionByQueue(): ConnectionInterface { if($this->queue == null){ $this->queue = new SplQueue(); } if (!$this->queue->isEmpty()) { //隊列存在可用連接直接獲取 return $this->getEffectiveConnection($this->queue->count(), false); } //超出隊列最大長度 if ($this->currentCount >= $this->poolConfig->getMaxActive()) { throw new ConnectionException("Connection pool queue is full"); } //向隊列補充連接 $connect = $this->createConnection(); $this->currentCount++; return $connect; }
SwoftPoolConnectionPool.php /** * Get effective connection * * @param int $queueNum * @param bool $isChannel * * @return ConnectionInterface */ private function getEffectiveConnection(int $queueNum, bool $isChannel = true): ConnectionInterface { $minActive = $this->poolConfig->getMinActive(); //連接池中連接少于數量下限時直接獲取 if ($queueNum <= $minActive) { return $this->getOriginalConnection($isChannel); } $time = time(); $moreActive = $queueNum - $minActive; $maxWaitTime = $this->poolConfig->getMaxWaitTime(); //檢查多余的連接,如等待時間過長,表示當前所持連接數暫時大于需求值,且易失效,直接釋放 for ($i = 0; $i < $moreActive; $i++) { /* @var ConnectionInterface $connection */ $connection = $this->getOriginalConnection($isChannel);; $lastTime = $connection->getLastTime(); if ($time - $lastTime < $maxWaitTime) { return $connection; } $this->currentCount--; } return $this->getOriginalConnection($isChannel); }
加點注釋就非常清晰了,此處不再贅述。
連接的釋放連接的釋放有兩種不同的容易引起歧義的用法,為此我們做以下定義:
一種是連接已經不再使用了,可以關閉了,這種我們稱為 連接的銷毀。
一種是連接暫時不再使用,其占用狀態解除,可以從使用者手中交回到空閑隊列中,這種我們稱為 連接的歸隊。
一般通過unset變量,或者通過其他手段清除連接變量的所有引用,等待Zend引擎實現鏈接資源清理。
這一點在上文的getEffectiveConnection()中出現過。執行到$this->currentCount--;的時候 ,連接已經出隊了,而$connection變量會在下個循環時作為循環變量被替換或者方法返回時作為局部變量被清除,連接資源的引用清0.引用降到0的資源會在下次gc執行時被回收,所以你沒看到主動的連接釋放代碼也很正常。
如果你的代碼在其他地方引用了這連接而沒管理好,可能會導致資源泄露。
/** * Class AbstractConnect */ abstract class AbstractConnection implements ConnectionInterface { //SwoftPoolAbstractConnection.php /** * @param bool $release */ public function release($release = false) { if ($this->isAutoRelease() || $release) { $this->pool->release($this); } } }
//SwoftPoolConnectionPool.php /** * Class ConnectPool */ abstract class ConnectionPool implements PoolInterface { /** * Release connection * * @param ConnectionInterface $connection */ public function release(ConnectionInterface $connection) { $connectionId = $connection->getConnectionId(); $connection->updateLastTime(); $connection->setRecv(true); $connection->setAutoRelease(true); if (App::isCoContext()) { $this->releaseToChannel($connection); } else { $this->releaseToQueue($connection); } $this->removeContextConnection($connectionId); } }
當用戶使用完某個連接后,比如執行了完了一條sql后,應當調用連接的release()方法。
連接本身是持有連接池的反向連接,在用戶調用ConnectionInterface->release()方法時,并不會馬上銷毀自身,而是清理自身的標記,調用PoolInterface->release()重新加入到連接池中。
//SwoftEventListenersResourceReleaseListener.php /** * Resource release listener * * @Listener(AppEvent::RESOURCE_RELEASE) */ class ResourceReleaseListener implements EventHandlerInterface { /** * @param SwoftEventEventInterface $event * @throws InvalidArgumentException */ public function handle(EventInterface $event) { // Release system resources App::trigger(AppEvent::RESOURCE_RELEASE_BEFORE); $connectionKey = PoolHelper::getContextCntKey(); $connections = RequestContext::getContextDataByKey($connectionKey, []); if (empty($connections)) { return; } /* @var SwoftPoolConnectionInterface $connection */ foreach ($connections as $connectionId => $connection) { if (!$connection->isRecv()) { Log::error(sprintf("%s connection is not received ,forget to getResult()", get_class($connection))); $connection->receive(); } Log::error(sprintf("%s connection is not released ,forget to getResult()", get_class($connection))); $connection->release(true); } } }
考慮到用戶可能會在使用完后沒有釋放連接造成連接泄露,Swoft會在Rpc/Http請求或者Task結束后觸發一個Swoft.resourceRelease事件(注:Swoft是筆者添加的前綴,方便讀者區分Swoole相關事件和Swoft相關事件),將連接強制收包并歸隊。
Swoft源碼剖析系列目錄:https://segmentfault.com/a/11...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/28976.html
摘要:作者鏈接來源簡書著作權歸作者所有,本文已獲得作者授權轉載,并對原文進行了重新的排版。同時順手整理個人對源碼的相關理解,希望能夠稍微填補學習領域的空白。系列文章只會節選關鍵代碼輔以思路講解,請自行配合源碼閱讀。 作者:bromine鏈接:https://www.jianshu.com/p/2f6...來源:簡書著作權歸作者所有,本文已獲得作者授權轉載,并對原文進行了重新的排版。Swoft...
摘要:和服務關系最密切的進程是中的進程組,絕大部分業務處理都在該進程中進行。隨后觸發一個事件各組件通過該事件進行配置文件加載路由注冊。事件每個請求到來時僅僅會觸發事件。服務器生命周期和服務基本一致,詳情參考源碼剖析功能實現 作者:bromine鏈接:https://www.jianshu.com/p/4c0...來源:簡書著作權歸作者所有,本文已獲得作者授權轉載,并對原文進行了重新的排版。S...
摘要:在中的應用官網源碼解讀號外號外歡迎大家我們開發組定了一個就線下聚一次的小目標上一篇源碼解讀反響還不錯不少同學推薦再加一篇講解一下中使用到的功能幫助大家開啟的實戰之旅服務器開發涉及到的相關技術領域的知識非常多不日積月累打好基礎是很難真正 date: 2017-12-14 21:34:51title: swoole 在 swoft 中的應用 swoft 官網: https://www.sw...
摘要:官方在文檔沒有提供完整的但我們還是可以在單元測試中找得到的用法。解決的問題是分散在引用各處的橫切關注點。橫切關注點指的是分布于應用中多處的功能,譬如日志,事務和安全。通過將真正執行操作的對象委托給實現了能提供許多功能。源碼剖析系列目錄 作者:bromine鏈接:https://www.jianshu.com/p/e13...來源:簡書著作權歸作者所有,本文已獲得作者授權轉載,并對原文進...
摘要:作者鏈接來源簡書著作權歸作者所有,本文已獲得作者授權轉載,并對原文進行了重新的排版。前言為應用提供一個完整的容器作為依賴管理方案,是功能,模塊等功能的實現基礎。的依賴注入管理方案基于服務定位器。源碼剖析系列目錄 作者:bromine鏈接:https://www.jianshu.com/p/a23...來源:簡書著作權歸作者所有,本文已獲得作者授權轉載,并對原文進行了重新的排版。Swof...
閱讀 2008·2021-11-24 09:39
閱讀 1143·2021-09-10 11:25
閱讀 1769·2021-09-08 10:42
閱讀 3733·2021-09-06 15:00
閱讀 2498·2019-08-30 15:54
閱讀 3116·2019-08-29 17:08
閱讀 3272·2019-08-29 11:26
閱讀 2840·2019-08-28 18:27