iT邦幫忙

2023 iThome 鐵人賽

DAY 25
0
Modern Web

30 天上手! PHP 微服務入門與開發系列 第 25

第二十五章、Anser-Saga:深入執行週期的高可用性元件 - PHP 微服務入門與開發

  • 分享至 

  • xImage
  •  

從前兩章的實作中,我們了解了如何使用 Anser 的交易措施,在保持微服務資料一致性的情況下,也能有一些手段提高協作器的可用性。在經歷了兩章的實作,你應該會好奇快照所儲存的媒介中,長得像這個樣子得資料結構:

Anser 的高可用性元件採用的是序列化(Serialization)的方式對執行週期中的協作器進行快照,再將這些快照一一地儲存起來。序列化是一個非常值得探討的技術,它允許我們將執行中的協作器的狀態捕捉並保存,無論是為了備份、搬移還是錯誤恢復。

執行週期內的協作器備份行為

如上圖所示,Anser 的高可用性元件透過序列化,將協作器不同步驟的狀態進行序列化,再透過備份這些序列化後的快照,確保在面臨硬體故障、網路中斷或其他不可預見的問題時,也能保證協作器的狀態不會丟失,並且可以在必要時恢復和重新執行。

Anser 的協作器序列化

物件序列化是一個將物件的狀態轉換為字串的過程,這會使物件處於一個可以被儲存到一個檔案或是資料庫中的狀態,若你願意也可以透過網路傳輸到任何地方。序列化後的物件可以在之後進行反序列化,恢復成原來的物件。

在 Anser 因為在協作器的備份上採用了序列化作為解決方案,因此在開發上有一些你需要注意的點。

匿名函式序列化

在 PHP 語言中,我們通常可以使用內建的方法 serialize()unserialize() 進行序列化與反序列化。然而,當遇到匿名函式時,serialize() 卻不能用來直接續序列化匿名函式。這是因為匿名函式本身是一個特別的存在,我們能夠將匿名函數儲存在一個變數中,透過這個變數的宣告匿名函式本身就可以於不同的實體間互相傳輸或留存參照。在匿名函式尚未被執行之前,儲存匿名函式的變數就只是個單純的變數罷了。

在 Anser 的整體設計中,考量到協作器的步驟在實際執行的過程中存在著極高的不確定性,因此我們在選擇合適的設計模式時,採用了以匿名函式來應對大多數在執行時期所需處理的邏輯,這也為整個序列化的工作埋下了一些難點。

你可以關注一下在上一章被儲存的協作器中的某個步驟被序列化後的樣子:

{
    "@type": "SDPMlab\\Anser\\Orchestration\\Step",
    "number": 4,
    "actionList": {
        "walletCharge": {
            "@closure": true,
            "serializer": "Zumba\\JsonSerializer\\ClosureSerializer\\SuperClosureSerializer",
            "value": "O:47:\"Laravel\\SerializableClosure\\SerializableClosure\":1:{s:12:\"serializable\";O:46:\"Laravel\\SerializableClosure\\Serializers\\Signed\":2:{s:12:\"serializable\";s:656:\"O:46:\"Laravel\\SerializableClosure\\Serializers\\Native\":5:{s:3:\"use\";a:0:{}s:8:\"function\";s:437:\"static function (\\Orchestrators\\CreateOrderOrchestrator $runtimeOrch) {\n                    $userKey = $runtimeOrch->getStepAction('userInfo')->getMeaningData()['data']['u_key'];\n                    exit;\n                    $total = $runtimeOrch->getStepAction('createOrder')->getMeaningData()['total'];\n                    return $runtimeOrch->userService->walletChargeAction($userKey, $runtimeOrch->orderId, $total);\n                }\";s:5:\"scope\";s:37:\"Orchestrators\\CreateOrderOrchestrator\";s:4:\"this\";N;s:4:\"self\";s:32:\"00000000000000350000000000000000\";}\";s:4:\"hash\";s:44:\"0g+eU7CxmmzFSgMdr1c0+jfv4UKgMz4J0vS//8EsWuc=\";}}"
        }
    },
    "isSuccess": false,
    "dynamicAction": null
}

在上述的 JSON 片段中,顯示的是一個 Anser 協作器中的一個步驟。這個序列化的過程由 zumba/json-serializer 進行實作,匿名函式的序列化則是透過 laravel/serializable-closure 進行處理。

這個物件描述了一個 Orchestration Step,它包含了步驟編號、行動列表、以及這個步驟是否成功等資訊。在行動列表中,有一個名為 walletCharge 的行動,這個行動就包含了一個序列化後的匿名函式。

這樣看似解決了匿名函式的序列化問題,而這會為實際的開發帶來怎麼樣的影響?

有的,你應該會發現在整個系列文章的所有範例中,任何提及匿名函式的原始碼都採用 static 靜態關鍵字進行宣告:

$this->setStep()
    ->addAction(
        alias: 'walletCharge',
        action: static function (CreateOrderOrchestrator $runtimeOrch) {
            //hide
        }
    );

在預設的情況下,若是處於類別內的匿名函數,都將享有 $this 的指向。換句話說,PHP 替我們考慮到了開發的便利性,自動將匿名函式與所在的類別進行自動繫結,這意味著你能夠在匿名函式中使用 $this 指向類別的本身,可以省下使用 use() 進行參數傳遞的動作。

然而,在序列化匿名函式時,這個特性可能會引發問題。當被繫結的$this 也被序列化時,序列化的程序就會去捕捉 $this 類別的整個實體,接著於類別的實體中再次找到匿名函式,在匿名函式中又捕捉到 $this 類別的整個實體......在周而復始下,形成了循環依賴的匿名函式序列化,直到記憶體溢位的發生才會終止。

這種循環序列化的情況是個嚴重問題,我們需要採取一些預防措施來避憾事發生,而這個預防措施就是採用 static 來阻絕所有 PHP 「善意」的自動繫結。

同時,在 Anser 中於執行時期被呼叫的匿名函式通常會傳入當下的協作器實體,因此你只需要操作這個實體就等於是存取本身的類別了。若是有一些較為敏感的成員變數無法以 public 的方式做宣告,你也可以透過 use() 將參照給傳遞到匿名函數中,就像這樣:

$orderProducts = $this->orderProducts;
$this->setStep()->setCompensationMethod('rollbackOrder')
    ->addAction(
        alias: 'createOrder',
        action: static function (CreateOrderOrchestrator $runtimeOrch) use ($orderProducts) {
            //hide
        }
    );

協作器的成員變數

你可以依照你的需求於協作器內部儲存一些靜態的變數或者是你自己的類別,唯獨需要注意這些類別的實體也可能隨著序列化的過程被一同序列化。因此你應該需要注意哪些類別的成員變數需要被序列化,或是哪些類別的成員變數在反序列化時需要被初始化。

你可以參考 PHP 的魔術方法 __sleep 以及 __wakeup

  • __sleep() 方法:

    此方法會在 serialize() 時執行。通常用於清理物件,並回傳一個包含需要被序列化的成員變數名稱組成的陣列。
    例如:你可以斷開與資料庫的連接,或者清除不需要序列化的大型資料結構。
    class MyObject
    {
        public $connection;
        public $largeDataStructure;
        public $importantData;
    
        public function __sleep()
        {
            // 斷開連接
            $this->connection = null;
            // 清除大資料結構
            $this->largeDataStructure = null;
    
            // 返回需要序列化的成員變數名稱
            return ['importantData'];
        }
    }
    
  • __wakeup() 方法:

    此方法會在 unserialize() 時執行。通常用於重新建立那些在 __sleep() 方法中被刪除或修改的資源。
    例如,你可以重新連接到資料庫,或者重新建立被刪除的資源。
    class MyObject
    {
        public $connection;
        public $largeDataStructure;
        public $importantData;
    
        public function __wakeup()
        {
            // 重新建立連接
            $this->connection = new DatabaseConnection();
    
            // 初始化大型資料結構
            $this->largeDataStructure = $this->initLargeDataStructure();
        }
    }
    

在你的協作器中,你可以利用這兩個方法來控制哪些成員變數需要被序列化,以及在反序列化時如何初始化你的物件。透過合理地使用 __sleep__wakeup 方法,你可以減少序列化的開銷,避免不必要的錯誤,並確保你的協作器在序列化和反序列化後仍能正常工作。

結語

本章,我們了解了 Anser 是以何種方式備份協作器的快照,並需要使用哪種設計方法來避免可能發生的錯誤。自此,我們正式結束了 Anser 所有需要學習的內容以及如何以 PHP 建構起一個保障資料一致性的服務溝通模式。

相信閱讀到現在的你,應該對如何在 PHP 中管理大量外部服務小有心得。再接下來的章節中,我們將談談如何在框架中加入 Anser,以及如何使用 PHP 進行高效能的微服務開發。


上一篇
第二十四章、Anser-Saga:重新執行被中斷的協作器快照 - PHP 微服務入門與開發
下一篇
第二十六章、Anser: 與框架整合,以 CodeIgniter4 為例 - PHP 微服務入門與開發
系列文
30 天上手! PHP 微服務入門與開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言