Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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


Slide 3

Slide 3 text

3 アクターモデルとは

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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


Slide 12

Slide 12 text

12 Erlang 12 spawn: プロセスの開始 
 pong(): 再帰的に実行 
 ! : 他のプロセスへのメッセージの送信 
 (→は公式ドキュ メントより引用)

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

16 16

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

18 ライブラリの設計

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

27 27

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

37 この go func (){ ... }で囲われた部分が 軽量スレッド内の処理 
 ロック。軽量スレッド終了時にロックの解除。 
 内部の処理を実行
 結果をFutureに送信 
 ↓Futureを作成。
 Futureを返却
 37

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

41 Thanks.

Slide 42

Slide 42 text

42 今後の展望idea達 appendix

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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