php-msf原始碼解讀【轉】

sunsky303發表於2017-12-26

php-msf: https://github.com/pinguo/php-msf

百度腦圖 – php-msf 原始碼解讀: http://naotu.baidu.com/file/cc7b5a49dfed46001d22222b1afa99ba?token=c9628331e99143c2

原始碼解讀也做了一段時間了, 總結一下自己的心得:

  • 抓住 生命週期, 讓程式碼在你腦海中 跑起來
  • 分析架構, 關鍵字 分層 邊界 隔離

一個好的框架, 弄清楚 生命週期 和 架構, 基本就已經到了 熟悉 的狀態了, 之後是填充細節和編碼熟練了

這裡再介紹幾個次重要的心得:

  • 弄明白這個工具擅長幹什麼, 適合幹什麼. 這個資訊也非常容易獲取到, 工具的文件通常都會顯眼標註出來, 可以通過這些 功能/特性, 嘗試以點見面
  • 從工程化的角度去看這個專案, 主要和上面的 架構 區分, 在處理核心業務, 也就是上面的 功能/特性 外, 工程化還涉及到 安全/測試/編碼規範/語言特性 等方面, 這些也是平時在寫業務程式碼時思考較少並且實踐較少的部分
  • 工具的使用, 推薦我現在使用的組合: phpstorm + 百度腦圖 + Markdown筆記 + blog

和 php-msf 的淵源等寫技術生活相關的 blog 再來和大家八, 直接上菜.

生命週期 & 架構

官方文件製作了一張非常好的圖: 處理請求流程圖. 推薦各位同仁, 有閒暇時製作類似的圖, 對思維很有的幫助.

根據這張圖來思考 生命週期 & 架構, 這裡就不贅述了, 這裡分析一下 msf 中一些技術點:

  • 協程相關知識
  • msf 中技術點摘錄

協程

我會用我的方式來講解, 如果需要深入瞭解的, 可以看我後面推薦的資源.

類 vs 物件 是一組很重要的概念. 類代表我們對事物的抽象, 這個抽象的能力在我們以後會一直用到, 希望大家有意識的培養這方面的意識, 至少可以起到觸類旁通的作用. 物件是 例項化 的類, 是 真正幹活的, 我們要討論的 協程, 就是這樣一個 真正幹活的 角色.

協程從哪裡來, 到哪裡去, 它是幹什麼的?

想一想這幾個簡單的問題, 也許你對協程的理解就更深刻了, 記住這幾個關鍵詞:

  • 產生. 需要有地方來產生協程, 你可能不需要知道細節, 但是需要知道什麼時候發生了
  • 排程. 肯定是有很多協程一起工作的, 所以需要排程, 怎麼排程的呢?
  • 銷燬. 是否會銷燬? 什麼時候銷燬?

現在, 我們再來看看協程的使用方式對比, 這裡注意一下, 我沒有用 協程的實現方式對比, 因為很多時候, 需求實際是這樣的:

怎麼實現我不管, 我選最好用的.

// msf - 單次協程排程
$response = yield $this->getRedisPool(`tw`)->get(`apiCacheForABCoroutine`);

// msf - 併發協程呼叫
$client1 = $this->getObject(Client::class, [`http://www.baidu.com/`]);
yield $client1->goDnsLookup();
$client2 = $this->getObject(Client::class, [`http://www.qq.com/`]);
yield $client2->goDnsLookup();
$result[] = yield $client1->goGet(`/`);
$result[] = yield $client2->goGet(`/`);

大致 是這樣的一個等式: 使用協程 = 加上 yield, 所以搞清楚哪些地方需要加上 yield 就好了 — 有阻塞IO的地方, 比如 檔案IO, 網路IO(redis/mysql/http) 等.

當然, 大致 就是還有需要注意的地方

  • 協程排程順序, 如果不注意, 就可能會退化成同步呼叫.
  • 呼叫鏈: 使用 yield 的呼叫鏈上, 都需要加上 yield. 比如下面這樣:
function a_test() {
    return yield $this->getRedisPool(`tw`)->get(`apiCacheForABCoroutine`);
}
$res = yield a_test(); // 如果不加 yield, 就變成了同步執行

對比一下 swoole2.0 的協程方案:

$server = new SwooleHttpServer("127.0.0.1", 9502, SWOOLE_BASE);

$server->set([
    `worker_num` => 1,
]);

// 需要在協程 server 的非同步回撥函式中
$server->on(`Request`, function ($request, $response) {

    $tcpclient = new SwooleCoroutineClient(SWOOLE_SOCK_TCP); // 需要配合使用協程客戶端
    $tcpclient->connect(`127.0.0.1`, 9501,0.5)
    $tcpclient->send("hello world
");

    $redis = new SwooleCoroutineRedis();
    $redis->connect(`127.0.0.1`, 6379);
    $redis->setDefer(); // 標註延遲收包, 實現併發呼叫
    $redis->get(`key`);

    $mysql = new SwooleCoroutineMySQL();
    $mysql->connect([
        `host` => `127.0.0.1`,
        `user` => `user`,
        `password` => `pass`,
        `database` => `test`,
    ]);
    $mysql->setDefer();
    $mysql->query(`select sleep(1)`);

    $httpclient = new SwooleCoroutineHttpClient(`0.0.0.0`, 9599);
    $httpclient->setHeaders([`Host` => "api.mp.qq.com"]);
    $httpclient->set([ `timeout` => 1]);
    $httpclient->setDefer();
    $httpclient->get(`/`);

    $tcp_res  = $tcpclient->recv();
    $redis_res = $redis->recv();
    $mysql_res = $mysql->recv();
    $http_res  = $httpclient->recv();

    $response->end(`Test End`);
});
$server->start();

使用 swoole2.0 的協程方案, 好處很明顯:

  • 不用加 yield 了
  • 併發呼叫不用刻意注意 yield 的順序了, 使用 defer() 延遲收包即可

但是, 沒辦法直接用 使用協程 = 加上 yield 這樣一個簡單的等式了, 上面的例子需要配合使用 swoole 協程 server + swoole 協程 client:

  • server 在非同步回撥觸發時 生成協程
  • client 觸發 協程排程
  • 非同步回撥執行結束時 銷燬協程

這就導致了 2 個問題:

  • 不在 swoole 協程 server 的非同步回撥中怎麼辦: 使用 SwooleCoroutine::create() 顯式生成協程
  • 需要使用其他的協程 Client 怎麼辦: 這是 Swoole3 的目標, Swoole2.0 可以考慮用協程 task 來偽裝

這樣看起來, 好像 使用協程 = 加上 yield 這樣要簡單一些? 我不這樣認為, 補充一些觀點, 大家自己斟酌:

  • 使用 yield 的方式, 基於 php 生成器 + 自己實現 PHP 協程排程器, 想要用起來不出錯, 比如上面 協程排程順序, 你還是需要去弄清楚這塊的實現
  • Swoole2.0 的原生方式, 理解起來其實更容易, 只需要知道協程 生成/排程/銷燬 的時機就可以用好
  • Swoole2.0 這樣非同步回撥中頻繁建立和銷燬協程, 是否十分損耗效能? — 不會的, 實際是一些記憶體操作, 比程式/物件小很多

想要繼續深入瞭解的同學, 可以繼續閱讀下面的文章:

php7 下協程實現: https://segmentfault.com/a/1190000012457145
在PHP中使用協程實現多工排程 | 鳥哥: http://www.laruence.com/2015/05/28/3038.html
php-msf doc – 協程原理: https://pinguo.gitbooks.io/php-msf-docs/chapter-2/2.3-%E5%8D%8F%E7%A8%8B%E5%8E%9F%E7%90%86.html
swoole 底層和 php yield 實現協程, 本質是一樣的, 畢竟都是 c 語言實現: https://github.com/php/php-src/blob/master/Zend/zend_generators.c

有時候 書讀百遍其義自見 還是很有道理的. 我希望我使用的 協程生成/排程/銷燬 的角度, 能給大家帶來幫助.

感謝韓老大, msf 的開發者, swoft 的小夥伴, 在這個過程中, 對我給予的耐心幫助.

msf 中技術點摘錄

msf 在設計上有很多出彩的地方, 很多程式碼都值得借鑑.

請求上下文 Context

這是從 fpm 到 swoole http server 非常重要的概念. fpm 是多程式模式, 雖然 $_POST 等變數, 被稱之為超全域性變數, 但是, 這些變數在不同 fpm 程式間是隔離的. 但是到了 swoole http server 中, 一個 worker 程式, 會非同步處理多個請求, 簡單理解就是下面的等式:

fpm worker : http request = 1 : 1
swoole worker : http request = 1 : n

所以, 我們就需要一種新的方式, 來進行 request 間的隔離.

在程式語言裡, 有一個專業詞彙 scope(作用域). 通常會使用 scope/生命週期, 所以我一直強調的生命週期的概念, 真的很重要.

swoole 本身是實現了隔離的:

$http = new swoole_http_server("127.0.0.1", 9501);
$http->on(`request`, function ($request, $response) {
    $response->end("<h1>Hello Swoole. #".rand(1000, 9999)."</h1>");
});
$http->start();

msf 在 Context 上還做了一層封裝, 讓 Context 看起來 為所欲為:

// 你幾乎可以用這種方式, 完成任何需要的邏輯
$this->getContext()->xxxModule->xxxModuleFunction();

細節可以檢視 src/Helpers/Context.php 檔案

物件池

物件池這個概念, 大家可能比較陌生, 目的是減少物件的頻繁建立與銷燬, 以此來提升效能, msf 做了很好的封裝, 使用很簡單:

// getObject() 就可以了
/** @var DemoModel $demoModel */
$demoModel = $this->getObject(DemoModel::class, [1, 2]);

注意一下這行註釋, 加上這個才有程式碼提示的效果的, 原理可以看我之前的 blog – 聊一聊 php 程式碼提示

物件池的具體程式碼在 src/Base/Pool.php 下:

  • 底層使用反射來實現物件的動態建立
public function get($class, ...$args)
{
    $poolName = trim($class, `\`);

    if (!$poolName) {
        return null;
    }

    $pool     = $this->map[$poolName] ?? null;
    if ($pool == null) {
        $pool = $this->applyNewPool($poolName);
    }

    if ($pool->count()) {
        $obj = $pool->shift();
        $obj->__isConstruct = false;
        return $obj;
    } else {
        // 使用反射
        $reflector         = new ReflectionClass($poolName);
        $obj               = $reflector->newInstanceWithoutConstructor();

        $obj->__useCount   = 0;
        $obj->__genTime    = time();
        $obj->__isConstruct = false;
        $obj->__DSLevel    = Macro::DS_PUBLIC;
        unset($reflector);
        return $obj;
    }
}

感興趣的同學可以去了解一下 反射, 可以給語言增加很多靈活性

  • 使用 SplStack 來管理物件
private function applyNewPool($poolName)
{
    if (array_key_exists($poolName, $this->map)) {
        throw new Exception(`the name is exists in pool map`);
    }
    $this->map[$poolName] = new SplStack();

    return $this->map[$poolName];
}

// 管理物件
$pool->push($classInstance);
$obj = $pool->shift();

msf doc 這塊的文章非常值得一讀, 特別是 php程式記憶體優化, 對我觸動很大: https://pinguo.gitbooks.io/php-msf-docs/chapter-5/5.6-%E5%AF%B9%E8%B1%A1%E6%B1%A0.html

連線池 & 代理

  • 連線池 Pools

連線池的概念就不贅述了, 我們來直接看 msf 中的實現, 程式碼在 src/Pools/AsynPool.php下:

public function __construct($config)
{
    $this->callBacks = [];
    $this->commands  = new SplQueue();
    $this->pool      = new SplQueue();
    $this->config    = $config;
}

這裡使用的 SplQueue 來管理連線和需要執行的命令. 可以和上面對比一下, 想一想為什麼一個使用 SplStack, 一個使用 SplQueue.

  • 代理 Proxy

代理是在連線池的基礎上進一步的封裝, msf 提供了 2 種封裝方式:

  • 主從 master slave
  • 叢集 cluster

檢視示例 AppControllersRedis 中的程式碼:

class Redis extends Controller
{
    // Redis連線池讀寫示例
    public function actionPoolSetGet()
    {
        yield $this->getRedisPool(`p1`)->set(`key1`, `val1`);
        $val = yield $this->getRedisPool(`p1`)->get(`key1`);

        $this->outputJson($val);
    }

    // Redis代理使用示例(分散式)
    public function actionProxySetGet()
    {
        for ($i = 0; $i <= 100; $i++) {
            yield $this->getRedisProxy(`cluster`)->set(`proxy` . $i, $i);
        }

        $val = yield $this->getRedisProxy(`cluster`)->get(`proxy22`);
        $this->outputJson($val);
    }

    // Redis代理使用示例(主從)
    public function actionMaserSlaveSetGet()
    {
        for ($i = 0; $i <= 100; $i++) {
            yield $this->getRedisProxy(`master_slave`)->set(`M` . $i, $i);
        }

        $val = yield $this->getRedisProxy(`master_slave`)->get(`M66`);
        $this->outputJson($val);
    }
}

代理就是在連線池的基礎上進一步 搞事情. 以 主從 模式為例:

  • 主從策略: 讀主庫, 寫從庫

代理做的事情:

  • 判斷是讀操作還是寫操作, 選擇相應的庫去執行

公共庫

msf 推行 公共庫 的做法, 希望不同功能元件可以做到 可插拔, 這一點可以看 laravel 框架和 symfony 框架, 都由框架核心加一個個的 package 組成. 這種思想我是非常推薦的, 但是仔細看 百度腦圖 – php-msf 原始碼解讀 這張圖的話, 就會發現類與類之間的依賴關係, 分層/邊界 做得並不好. 如果看過我之前的 blog – laravel原始碼解讀 / blog – yii原始碼解讀, 進行對比就會感受很明顯.

但是, 這並不意味著 程式碼不好, 至少功能正常的程式碼, 幾乎都能算是好程式碼. 從功能之外建立的 優越感, 更多的是對 美好生活的嚮往 — 還可以更好一點.

AOP

php AOP 擴充套件: http://pecl.php.net/package/aop
PHP-AOP擴充套件介紹 | rango: http://rango.swoole.com/archives/83

AOP, 面向切面程式設計, 韓老大 的 blog – PHP-AOP擴充套件介紹 | rango 可以看看.

需不需要了解一個新事物, 先看看這個事物有什麼作用:

AOP, 將業務程式碼和業務無關的程式碼進行分離, 場景有 日誌記錄 / 效能統計 / 安全控制 / 事務處理 / 異常處理 / 快取 等等.

這裡引用一段 程式設計師DD – 翟永超的公眾號 文章裡的程式碼, 讓大家感受下:

  • 同樣是 CRUD, 不使用 AOP
@PostMapping("/delete")
public Map<String, Object> delete(long id, String lang) {
  Map<String, Object> data = new HashMap<String, Object>();

  boolean result = false;
  try {
    // 語言(中英文提示不同)
    Locale local = "zh".equalsIgnoreCase(lang) ? Locale.CHINESE : Locale.ENGLISH;

    result = configService.delete(id, local);

    data.put("code", 0);

  } catch (CheckException e) {
    // 引數等校驗出錯,這類異常屬於已知異常,不需要列印堆疊,返回碼為-1
    data.put("code", -1);
    data.put("msg", e.getMessage());
  } catch (Exception e) {
    // 其他未知異常,需要列印堆疊分析用,返回碼為99
    log.error(e);

    data.put("code", 99);
    data.put("msg", e.toString());
  }

  data.put("result", result);

  return data;
}
  • 使用 AOP
@PostMapping("/delete")
public ResultBean<Boolean> delete(long id) {
  return new ResultBean<Boolean>(configService.delete(id));
}

程式碼只用一行, 需要的特性一個沒少, 你是不是也想寫這樣的 CRUD 程式碼?

配置檔案管理

先明確一下配置管理的痛點:

  • 是否支撐熱更新, 常駐記憶體需要考慮
  • 考慮不同環境: dev test production
  • 方便使用

熱更其實可以算是常駐記憶體伺服器的整體需求, 目前 php 常用的解決方案是 inotify, 可以參考我之前的 blog – swoft 原始碼解讀 .

msf 使用第三方庫來解析處理配置檔案, 這裡著重提一個 array_merge() 的細節:

$a = [`a` => [
    `a1` => `a1`,
]];

$b = [`a` => [
    `b1` => `b1`,
]];

$arr = array_merge($a, $b); // 注意, array_merge() 並不會迴圈合併
var_dump($arr);

// 結果
array(1) {
  ["a"]=>
  array(1) {
    ["b1"]=>
    string(2) "b1"
  }
}

msf 中使用配置:

$ids = $this->getConfig()->get(`params.mock_ids`, []);

// 對比一下 laravel
$ids = cofnig(`params.mock_ids`, []);

看起來 laravel 中要簡單一些, 其實是通過 composer autoload 來載入函式, 這個函式對實際的操作包裝了一層. 至於要不要這樣做, 就看自己需求了.

寫在最後

msf 最複雜的部分在 服務啟動階段, 繼承也很長:

Child -> Server -> HttpServer -> MSFServer -> AppServer, 有興趣可以挑戰一下.

另外一個比較難的點, 是 MongoDbTask 實現原理.

msf 還封裝了很多有用的功能, RPC / 訊息佇列 / restful, 大家根據文件自己探索即可.

相關文章