Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

F1e600f2c53b0456e0b6c34009d4127e?s=47 sanposhiho
June 23, 2022
1k

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

F1e600f2c53b0456e0b6c34009d4127e?s=128

sanposhiho

June 23, 2022
Tweet

Transcript

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

  2. 2 2022年4月新卒入社
 
 
 Mercari/Search Quality team
 メルカリの検索周りのバックエンド開発をしています
 Kubernetesが好きです。
 Kensei

    Nakada (@sanposhiho)

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

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

    メッセージを受け取った際の振る舞いを定義する 4
  5. 5 例1 Actor 1がActor 2に内部の状態の変更をリクエストする 5

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

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

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

    に起こらない」という状態を目指す。 8
  9. 9 アクターモデルを使用している言語

  10. 10 Erlang アクターモデルを採用 • アクター = ErlangVM上の一つのプロセス • プロセス同士は完全に分離 (メモリ空間など)

    → あるプロセスがクラッシュしても他のプロセスに影響しにくい • ホットスワップ: プログラム全体の再起動をせずにプロセスを入れ替える 10
  11. 11 Erlang 明示的に、アクター同士の通信を記述する 11 spawn: プロセスの開始 
 再帰的に呼び出す


  12. 12 Erlang 12 spawn: プロセスの開始 
 pong(): 再帰的に実行 
 !

    : 他のプロセスへのメッセージの送信 
 (→は公式ドキュ メントより引用)
  13. 13 Swift Swift5.5より並行処理に関する機能が多くサポートされた。 → その一環でアクターが言語レベルでサポートされた 13

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

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

  16. 16 16

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

  18. 18 ライブラリの設計

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

  20. 20 ライブラリの設計デザイン 1. ユーザーがメソッドを定義する 2. アクターの構造体が生成され、その構造体はユーザーが定義したメソッドを全ても つ 3. アクターの構造体に対して、メソッド呼び出しを行うと、 内部的にはアクター的な振る舞いをしており、非同期に処理が行われる。

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

    • google/wire: 依存性注入(DI)のためのフレームワーク 21
  22. 22 ライブラリの設計デザイン 1. ユーザーがメソッドを定義する 2. アクターの構造体が生成され、その構造体はユーザーが定義したメソッドを全ても つ 3. アクターの構造体に対して、メソッド呼び出しを行うと、 内部的にはアクター的な振る舞いをしており、非同期に処理が行われる。

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

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

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

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

  27. 27 27

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

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

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

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

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

    数の処理を実行しない。) 3. 処理結果をFutureに送信する。 32
  33. 33 同期的には仮の値であるFutureを返す。 メソッドが呼ばれると、すぐにFutureを作成して、それを返却する。 33

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

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

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

  37. 37 この go func (){ ... }で囲われた部分が 軽量スレッド内の処理 
 ロック。軽量スレッド終了時にロックの解除。

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

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

    ジを処理できない。 すると、アクターBの処理は永遠に終わらないのでデッドロック 39
  40. 40 既存のGoのアクターモデルライブラリとの比較 protoactor-go、ergoなどが存在 - Swiftのアクターようなデザインのものは存在しないので、デザインが他と大きく異 なる。 - オブジェクト指向に慣れている開発者がそのままの感覚で使用できる。 - interfaceやモックなど、既存のオブジェクト指向プログラミングを前提とした

    Goのエコシステムを全 てそのまま使用できる。 - Futureへの処理結果の送信の箇所を、ジェネリクスで実現しているため、メッセー ジングが型安全である。 - 他のライブラリはメッセージングに型がつかない。例えば、誤った型のメッセージを送信した場合 に、開発者は気がつくことができない。 40
  41. 41 Thanks.

  42. 42 今後の展望idea達 appendix

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

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

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

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

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

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