山姆鍋在過去的文章中 , 數次提到 「 參與者模式 (Actor Model)」 這個名詞 , 也使用 比喻的方式 解釋它的概念 。 在多種分散式編程模式中 , 對山姆鍋來說 , 這是最不會讓頭腦打結的方法 。

本文山姆鍋試圖整理寫過的一個參與者框架 , 雖然不完整 , 希望還是多少對參與者模式如何運作的了解有所幫助 。

設計目標 (Design Goals)

  • 參與者無須知道溝通的對象所在的主機 。
  • 參與者無須知道訊息如何傳遞 。
  • 訊息結構可以擴展 , 容許應用程式彈性利用 。
  • 支援 「 訊息通知 (notification)」 的通訊模式 。
  • 支援 「 請求 - 回應 (request-response)」 通訊模式 。
  • 訊息傳輸協定 (transport protocol) 可以更換 。

物件模型 (Object Model)

框架定義了一些應用程式該繼承或使用的類別 , 底下說明框架中主要的類別 。

Simple actor framework

Actor

代表框架中的 「 參與者 」 物件 。 每個參與者都有一個全域 (global) 唯一的名稱 (name), 在同一個引擎 (engine), 這個名稱必須是唯一 。

每個參與者會有一個信箱 (mailbox), 存放該參與者收到但尚未處理的訊息 。 在此框架 , 參與者不會明確呼叫來 從信箱取得訊息 , 而是由框架推送 (push) 給參與者物件 。

ActorRef

代表參與者的 「 參考 」 物件 。 這個物件封裝與一個參與者溝通的必要資訊 , 如 : 參與者名稱 、 所在引擎的網路位址等 。 同時提供幾個關鍵方法 :

  • tell: 用來傳送通知 / 一般訊息給此參與者 。
  • ask: 用來送出請求訊息給此參與者 。
  • get_proxy: 用來取得一個特別的代理 (proxy) 物件 , 此代理物件可以把方法呼叫轉化成請求訊息 。

ActorProxy

參與者的代理物件 , 用來把傳送請求訊息轉化成對此代理物件的呼叫 。 運用到 Python 的 __getattribute__ 方法 , 攔截方法呼叫並轉成訊息送給對應的參與者 。 ActorRefget_proxy 方法會回傳一個 ActorProxy 物件 。

ActorContext

針對每個參與者物件 , 框架會配置一個 ActorContext 物件 。 此物件擔任參與者與框架之間的主要介面 , 同時封裝關於此參與者的重要資訊 。 底下是此類別主要方法 :

  1. get_name(self):

    取的此參與者的名稱 。

  2. get_self_ref(self):

    取得此參與者的 「 參考 」 物件 。 當需要以非同步的方式接受通知時 , 需要像事件源註冊 , 此時便會需要取得自己 的參考物件 。 因為所有溝通都只能透過參考物件 , 直接回傳參與者物件並沒有用處 。

  3. get_sender_ref(self):

    取的目前傳送目前訊息的參與者參考物件 。 這個方法只在處理訊息時可以用 , 底層使用 Greenlet local 物件 來記錄與傳遞當前的訊息傳送者 , 是一種類似 Thread Local Storage 的機制 。

  4. spawn(self, actor_cls, *args, **kwargs):

    派生出新的參與者 。

  5. make_ref(self, actor_name, address):

    根據參與者名稱與網路位址建構一個 ActorRef( 參考 ) 物件 。

  6. serialize_ref(self, actor_ref):

    把一個 ActorRef 物件序列化成適合網路傳遞或儲存的格式 。

  7. deserialize_ref(self, ref_str):

    把經過序列化的 ActorRef 物件反序列化成物件 。

Message

總共有四個具體 (concrete) 的訊息類別 , 分別為 :

  1. Message:

    代表一般或者通知訊息 , 用在不需要接收者回應的情境 。

  2. Request:

    代表請求的訊息 , 預期接受者回傳成功或者失敗的回應 。

  3. Result:

    代表請求執行成功的回應訊息 , 通常也會內涵執行結果 。

  4. Error:

    代表請求執行錯誤的回應訊息 , 包含基本錯誤訊息 。

訊息相關的類別定義在 : https://github.com/sampot/simpleactors/blob/master/ava/actor/message.py

ActorEngine

此類別定義實作參與者框架的主要邏輯 , 如派生參與者物件 、 遞送訊息與處理訊息逾時等等工作 。 每個引擎物件會有一個網路位址用來跟其它引擎通訊 。 如果參與通訊的參與者都屬於同一個引擎 , 則訊息會採取捷徑方式處理並不會到網路層 。

通訊模式

參與者之間只能透過訊息傳遞來互相溝通 , 而且參與者只能跟它已經有 「 參考 」 物件的對象通訊 。 參與者之間可以分成兩種溝通模式 :

  • 通知模式 (notification):

    這種模式很容易理解 , 就是訊息送出去後就不管了 , 也可以說是 fire-and-forget 模式 。 在框架中 , tell 操作就是用來實現這種通訊模式 。

  • 請求 - 回應模式 (request-response):

    這種模式下 , 送出訊息的參與者會預期接收的參與者會給予回應 , 不管是表示成功或失敗結果 。 在框架中 , 底下三種訊息類別分別代表請求 、 成功的結果 、 失敗的例外訊息 :

    1. Request:

      封裝一個代表請求的訊息 , 請求訊息包含要接收者執行的動作 (action) 以及參數 。

    2. Result:

      封裝代表請求的執行結果 。

    3. Error:

      封裝網路或者請求錯誤的資訊 。 逾時 (timeout) 沒有收到回應 , 框架也會以一個 Error 訊息通知參與者 。

    參與者藉由呼叫 ask 方法來送出請求 , 並根據回應來判斷是成功 、 失敗或者逾時 。

參與者實作概觀

參與者需要以一個類別實現 , 這個類別不一定需要繼承自框架提供的 Actor 類別 , 但參與者類別至少需要符合下列要求 :

  1. 如果有建構子 , 除了 `actor_context` 這個具名參數 (named parameter) 外 , 其餘參數都必須有預設值 。

有兩個特別的方法 , 用來處理訊息跟要求 :

  • handle_message: 只有在此參與者收到一般訊息 ( 通知訊息 ) 時 , 框架才會呼叫此方法 。
  • handle_action: 當參與者收到請求訊息 , 但沒有對應的處理方法時 , 框架會呼叫這個方法 。

參與者類別如果有定義 on_ 開頭的方法 , 如果收到對應動作 (action) 的請求訊息 , 框架會自動呼叫該方法 , 這時就不會再呼叫 handle_action。 例如 : 參與者收到一個叫做 'hello' 的動作要求 , 則框架會呼叫 on_hello 這個方法 。

具體的範例請參考使用情境中的 PongActor 類別 。

使用情境 (Use Scenarios)

底下使用情境使用一個叫做 `PongActor` 的參與者實作 , 程式碼如下 :

class PongActor(object):
    instance = None

    def __init__(self, actor_context):
        self.context = actor_context
        print("PongActor created.")
        self.called = False
        self.last_msg = None
        PongActor.instance = self

    def on_ping(self, s):
        self.called = True
        self.last_msg = s
        return s

    def on_error(self):
        self.called = True
        raise RuntimeError('Pong error')

    def on_get_sender(self):
        self.called = True
        return self.context.get_sender_ref().actor_name

    def handle_message(self, msg):
        self.called = True
        self.last_msg = msg

    def handle_action(self, *args, **kwargs):
        self.called = True
        self.last_msg = kwargs
        return kwargs

這個參與者也實際被用在框架的測試案例中 , 那些額外的屬性都是為了測試需要 。

取得 ActorContext 物件

參與者只有在被建構的當下才有機會取的它對應的 ActorContext 物件 。 參與者類別必須定義一個接受名為 actor_context 的具名參數 , 框架在產生此參與者物件時會帶入 ActorContext 物件 。

例如下面的 __init__ 宣告 :

def __init__(self, actor_context):
    self._context = actor_context

這個方法可以有其它參數 , 只要它們都有預設值 。

為了判斷方法是否接受一個名為 actor_context 的參數 , 使用了 Python 內建的 inspect 套件 。 該技巧應該有參考價值 , 特別收錄下列建立參與者物件的片段 :

argspec = inspect.getargspec(actor_cls.__init__)
if 'actor_context' in argspec.args:
    #  判斷是否有  'actor_context'  這個參數 
    #  有的話 , 插入  ActorContext  物件的具名參數 。
    kwargs['actor_context'] = context
    _actor = actor_cls(*args, **kwargs)

其中 ,actor_cls 是參與者類別 。

ActorContext 是參與者物件與框架互動的介面 , 除非是不需要其它參與者通訊的參與者 , 不然大概都會採用這個方式取的 ActorContext 物件 。

派生一個參與者 (Spawn Actors)

假設 actor_engine 是參與者引擎物件 , 則可以使用下面片段來派生新的參與者 :

pong_ref = actor_engine.spawn(PongActor)

其中 ,PongActor 是參與者的實作類別 ,spawn 方法會傳回該參與者物件的 「 參考 」。 透過這個 「 參考 」 物件 , 其它參與者便可以跟它溝通 。

上述的方法是針對框架本身要派生新的參與者物件時使用 , 參與者物件則透過關聯的 ActorContext 物件 :

pong_ref = actor_context.spawn(PongActor)

傳送通知訊息給參與者

假設 pong_ref 是一個參與者的 「 參考 」 物件 , 下面片段示範如何送一個通知訊息給它 :

msg = message.Message()
msg.receiver = pong_ref.actor_name
msg.msg_id = b'msgid1234'

pong_ref.tell(msg)

過程中沒有錯誤的話 ,PongActorhandle_message 方法會被呼叫來處理此訊息 。

要求參與者處理一個動作

req = message.Request()
req.receiver = pong_ref.actor_name
req.action = 'ping'  # 1
req.arguments = {'s': 'hello'}

result = pong_ref.ask(req)
text = result.get(1)

上述程式片段會呼叫到 PongActoron_ping 這個方法 。

使用代理物件簡化請求 - 回應

同樣假設 pong_refPongActor 的一個參考物件 , 下面程式片段示範如何使用參與者代理物件來簡化請求 :

pong = pong_ref.get_proxy()
text = pong.ping(s='world')

PongActoron_ping 方法同樣會被呼叫 , 只是在程式碼這個層級看起來比較容易理解 , 但背後還是採用標準的訊息傳遞機制在運作 。

實作構想

  1. 一開始設計時 , 實作就決定採用 Gevent 跟 Greenlet。 每個 Actor 都由一個 greenlet 來負責 。
  2. 訊息的實際傳送由原始碼中的 net 套件所負責 , 它本身也自成一個通訊框架 , 由於主要目的是說明參與者框架 , 就不在此詳述 。

已知限制

  1. 框架不維護參與者之間的階層關係 (hierarchical relationships), 所以當參與者意外結束時 , 並不會通知上層負責監管的參與者 。
  2. 訊息傳遞是基於 UDP 協定 , 跨主機通訊時 , 訊息有可能會遺失 、 順序顛倒 , 這些都是參與者實作要處理 。

結語

山姆鍋越寫越覺得這個框架還真的蠻陽春的 , 不過還好山姆鍋夠阿 Q, 會正面思考 , 總覺得會有些值得參考的地方吧 !

參考資料

simepleactors: https://github.com/sampot/simpleactors

本文所討論的參與者框架原始碼 。