$30 off During Our Annual Pro Sale. View Details »

Goにおけるアクターモデルの実現に 向けたライブラリの設計と実装

sanposhiho
June 23, 2022
1.9k

Goにおけるアクターモデルの実現に 向けたライブラリの設計と実装

sanposhiho

June 23, 2022
Tweet

More Decks by sanposhiho

Transcript

  1. 1
    Kensei Nakada (@sanposhiho)
    Goにおけるアクターモデルの実現に
    向けたライブラリの設計と実装
    1

    View Slide

  2. 2
    2022年4月新卒入社



    Mercari/Search Quality team

    メルカリの検索周りのバックエンド開発をしています

    Kubernetesが好きです。

    Kensei Nakada (@sanposhiho)


    View Slide

  3. 3
    アクターモデルとは

    View Slide

  4. 4
    アクターモデルとは
    ● アクターと呼ばれるオブジェクトを中心とした考え方
    ● アクターは自身のデータを他のアクターから直接参照させない
    ● アクター間でのメッセージパッシングを行い処理をリクエスト
    (データをもらったり)
    ● メッセージを受け取った際の振る舞いを定義する
    4

    View Slide

  5. 5
    例1
    Actor 1がActor 2に内部の状態の変更をリクエストする
    5

    View Slide

  6. 6
    例2
    Actor 1がUser Aの名前をActor 2に教えてもらう
    → 非同期に返信という形で情報を受け取る
    6

    View Slide

  7. 7
    アクターモデルの利点
    送られてきたメッセージをキューに貯め、一つずつメッセージを処理
    → 単一の処理(スレッド)のみが特定の時間にそのデータにアクセスすることを保証
    同時に状態を変更するようなメッセージがきても、絶対に同時には処理が実行されない
    ため、
    並行処理におけるレースコンディションなどが起こり得ない
    7

    View Slide

  8. 8
    アクターモデルの利点
    レースコンディション等への通常の対応策 例: 排他制御
    当たり前だけど、しかしこういった対策は正しく行われておらず、
    レースコンディションが起こりうる状態でも、コンパイルは通る
    → アクターモデルのライブラリにより、
    ライブラリを使用した部分に関しては「コンパイルが通れば、レースコンディションが絶対
    に起こらない」という状態を目指す。
    8

    View Slide

  9. 9
    アクターモデルを使用している言語

    View Slide

  10. 10
    Erlang
    アクターモデルを採用
    ● アクター = ErlangVM上の一つのプロセス
    ● プロセス同士は完全に分離 (メモリ空間など)
    → あるプロセスがクラッシュしても他のプロセスに影響しにくい
    ● ホットスワップ: プログラム全体の再起動をせずにプロセスを入れ替える
    10

    View Slide

  11. 11
    Erlang
    明示的に、アクター同士の通信を記述する
    11
    spawn: プロセスの開始 

    再帰的に呼び出す


    View Slide

  12. 12
    Erlang
    12
    spawn: プロセスの開始 

    pong(): 再帰的に実行 

    ! : 他のプロセスへのメッセージの送信 

    (→は公式ドキュ
    メントより引用)

    View Slide

  13. 13
    Swift
    Swift5.5より並行処理に関する機能が多くサポートされた。
    → その一環でアクターが言語レベルでサポートされた
    13

    View Slide

  14. 14
    Swiftにおけるアクター
    ● 「メソッド呼び出し」に似た感覚でメッセージの送信+返信の受け取りを行う。
    ● 振る舞いはクラスのメソッドのような見た目で定義される。
    14

    View Slide

  15. 15
    Swiftにおけるアクター
    ● 「メソッド呼び出し」に似た感覚でメッセージの送信+返信の受け取りを行う。
    ● 振る舞いはクラスのメソッドのような見た目で定義される。
    15

    View Slide

  16. 16
    16

    View Slide

  17. 17
    Swiftにおけるアクター
    呼び出しもメソッド呼び出しのような形で行う
    → 内部的にはメッセージを送信し、結果を返信として受け取っていると見做せる
    (実際の実装は知りません)
    → 同期的にメソッド呼び出すわけではなく、非同期的に処理されるため、返信にアクセス
    するには処理の終了を待つ必要がある。
    17

    View Slide

  18. 18
    ライブラリの設計

    View Slide

  19. 19
    ライブラリの設計デザイン
    目標: Swiftのような、メソッドを使う感じでアクターを使用できるようなデザインを実現し
    たい
    19

    View Slide

  20. 20
    ライブラリの設計デザイン
    1. ユーザーがメソッドを定義する
    2. アクターの構造体が生成され、その構造体はユーザーが定義したメソッドを全ても

    3. アクターの構造体に対して、メソッド呼び出しを行うと、
    内部的にはアクター的な振る舞いをしており、非同期に処理が行われる。
    20

    View Slide

  21. 21
    Go におけるコード生成
    Goではコード生成により、ユーザーに機能を提供するライブラリがいくつか存在
    ● golang/mock: モックのためのフレームワーク
    ● ent/ent: エンティティフレームワーク(ORM)
    ● google/wire: 依存性注入(DI)のためのフレームワーク
    21

    View Slide

  22. 22
    ライブラリの設計デザイン
    1. ユーザーがメソッドを定義する
    2. アクターの構造体が生成され、その構造体はユーザーが定義したメソッドを全ても

    3. アクターの構造体に対して、メソッド呼び出しを行うと、
    内部的にはアクター的な振る舞いをしており、非同期に処理が行われる。
    22

    View Slide

  23. 23
    ライブラリの設計デザイン
    アクターの構造体が生成され、その構造体はユーザーが定義したメソッドを全てもつ
    23

    View Slide

  24. 24
    ライブラリの設計デザイン
    アクターの構造体がコード生成によって生成され、その構造体はユーザーが定義した
    interfaceのメソッドを全てもつ
    →ユーザーはアクターの構造体の初期化時に、interfaceを満たす構造体を
    使用する。
    →アクターは初期化時に渡された構造体を内部の振る舞いとして使用する。
    24

    View Slide

  25. 25
    ツールによるアクターの生成
    /user/user.go に以下のinterface定義が存在するとする。
    以下のコマンドにより生成する。
    25

    View Slide

  26. 26
    簡単な使用例
    1. アクターの生成。
    interfaceを満たす構造体を作成し、生成されたコードに含まれる、New関数を呼び出
    すことでactorが生成される。
    26

    View Slide

  27. 27
    27

    View Slide

  28. 28
    簡単な使用例
    2. 作成されたアクターの使用
    アクターには事前にinterfaceに定義したメソッドが実装されているので、
    メソッドを呼び出すことで、アクターに処理を指示することができる。
    28

    View Slide

  29. 29
    簡単な使用例
    2. 作成されたアクターの使用
    アクターはメソッドが呼ばれると、アクターモデルに基づいて、非同期に処理を行う。
    そのため、通常のプログラミングのように結果は同期的に返却されない。
    代わりに仮の値としてFutureが返却される。
    29

    View Slide

  30. 30
    簡単な使用例
    3. Futureから結果を受け取る
    受け取ったFutureのGetメソッドを呼び出すことで、処理の結果を受け取ることができ
    る。
    - この時点で処理が終わっている場合は、即時結果を受け取れる。
    - この時点で処理が終わっていない場合は、アクターが結果を処理するまで待ちが
    発生する。
    30

    View Slide

  31. 31
    細かな内部実装を軽く紹介
    おさらい:
    アクターのメソッドが呼ばれると、アクターは…
    1. 同期的には仮の値であるFutureを返す。
    2. 非同期に処理を行う。
    3. 処理結果をFutureに送信する。
    31

    View Slide

  32. 32
    細かな内部実装を軽く紹介
    おさらい:
    アクターのメソッドが呼ばれると、アクターは…
    1. 同期的には仮の値であるFutureを返す。
    2. 非同期に処理を行う。
    - (複数のスレッドから同時に複数のメソッドを呼ばれていた場合でも同時に複
    数の処理を実行しない。)
    3. 処理結果をFutureに送信する。
    32

    View Slide

  33. 33
    同期的には仮の値であるFutureを返す。
    メソッドが呼ばれると、すぐにFutureを作成して、それを返却する。
    33

    View Slide

  34. 34
    非同期に処理を行う。
    複数のスレッドから同時に複数のメソッドを呼ばれていた場合でも同時に複数の処理を
    実行しない (= 同時に一つの処理のみを実行する。)
    → 
    このためにアクターはそれぞれ内部に排他制御のためのロックを一つ持っている。
    内部の処理に移る前にロックをして他の処理が同時に実行されないようにする。
    34

    View Slide

  35. 35
    非同期に処理を行う。
    非同期処理のためにGoroutineを実行し、その内部で処理を行う。
    内部の処理に移る前にロックをして他の処理が実行されないようにする。
    35

    View Slide

  36. 36
    処理結果をFutureに送信する。
    FutureにはSendという結果をFutureに対して送信するためのメソッドが用意されてい
    るので、それを用いて同期的に返却したFutureに結果を送信する。
    36

    View Slide

  37. 37
    この go func (){ ... }で囲われた部分が 軽量スレッド内の処理 

    ロック。軽量スレッド終了時にロックの解除。 

    内部の処理を実行

    結果をFutureに送信 

    ↓Futureを作成。

    Futureを返却

    37

    View Slide

  38. 38
    アクターリエントランシー
    Swiftを参考にリエントランシーと呼ばれる仕組みを導入。
    アクターの処理の中で、Futureで他のアクターの処理の終了を待っている間(=
    future.Getで待ちが発生している間)は、そのアクターは他の処理を行うことができる。
    → これにより、デッドロックを防いでいる。
    38

    View Slide

  39. 39
    アクターリエントランシー
    リエントランシー無しだと、デッドロックが起こってしまう例:
    1. アクターAとアクターBが存在し、アクターAがアクターBにメッセージを送り、結果を
    まつ。
    2. アクターBはそのメッセージの処理中に、アクターAにメッセージを送り、結果をま
    つ。
    アクターAは1でアクターBのメッセージを待っているため、アクターBが2で送ったメッセー
    ジを処理できない。
    すると、アクターBの処理は永遠に終わらないのでデッドロック
    39

    View Slide

  40. 40
    既存のGoのアクターモデルライブラリとの比較
    protoactor-go、ergoなどが存在
    - Swiftのアクターようなデザインのものは存在しないので、デザインが他と大きく異
    なる。
    - オブジェクト指向に慣れている開発者がそのままの感覚で使用できる。
    - interfaceやモックなど、既存のオブジェクト指向プログラミングを前提とした Goのエコシステムを全
    てそのまま使用できる。
    - Futureへの処理結果の送信の箇所を、ジェネリクスで実現しているため、メッセー
    ジングが型安全である。
    - 他のライブラリはメッセージングに型がつかない。例えば、誤った型のメッセージを送信した場合
    に、開発者は気がつくことができない。
    40

    View Slide

  41. 41
    Thanks.

    View Slide

  42. 42
    今後の展望idea達
    appendix

    View Slide

  43. 43
    Non-reentrant アクター
    リエントランシーが有効でないアクターを生成できるオプションの作成。
    Non-reentrantなアクターは前述のデッドロックを起こす可能性があるが、
    それを動的に発見する機能を同時に提供することが、技術的に実装可能である。
    (静的に発見することは難しい。)
    43

    View Slide

  44. 44
    障害の検知、伝搬
    Erlangではアクターに別のアクターを監視させるのに便利な仕組みがある。
    アクターが死んだ場合に、再起動などの任意の処理を別のアクターから行わせることが
    できるとよい。
    44

    View Slide

  45. 45
    内部状態へのアクセスの流出を防ぐ静的解析ツール
    例えば、ユーザーがアクター内部の状態にアクセスできるポインターを返却するメソッド
    を定義していた場合、
    その帰り値を用いてユーザーはアクターの内部の状態にアクターのメソッドを介さずにア
    クセスできてしまう。
    →このような実装は静的に発見することが可能なので、発見し、注意を促すツールを実
    装できる。
    45

    View Slide

  46. 46
    アクターの宣言的な管理
    アクターの状態を一つのDBなどに置き、宣言的に管理する。
    宣言的な管理により、分散システムの構築にとても強みをもてるようになる。
    実装はシンプルで、各ホストに一つずつ”DBを監視して、自分のホスト内のアクターの
    状態を管理するコンポーネント”を作成しておけば良い。
    46

    View Slide

  47. 47
    宣言的な管理による、他のホストでのアクターの起動
    他のホストのアクターを同時に管理できると、分散システムが構築しやすい。
    宣言的に管理しておくことで、「host Bでactor Aを起動する」というふうに登録すること
    により、host Bで起動しているアクターを管理するコンポーネント(前述) がactorAを起
    動してくれる。
    47

    View Slide

  48. 48
    他のホストに存在するアクターへのメッセージング
    他のホストへのメッセージングも同時に実装する必要がある。
    48

    View Slide