📨

アクターモデルや akka について独学してわかってきたことをまとめてみる

2024/12/25に公開

どうも皆さんこんにちは.
鞍馬ぽめると申します, よろしくお願いいたします.

はじめに

この一年, まったりではありますが Akka の独学を進めていました.  
今回は, 学んできてわかったことなどをまとめてみようと思います.

なお, 間違いがあればご指摘いただけると嬉しいです.
独学で進めてきたため正しい理解ができていない可能性もあるため, ご指摘いただきより学びを深めたいと思っています m_ _m

学習用に用いたリポジトリは下記になります, 参考にしてみてください.

https://github.com/kuramapommel/til-akka-typed/tree/v0.1.1

CQRS + ES

アクターモデルや akka の話をする前に, CQRS + ES という考えについて抑えておきます.

ドメイン駆動設計における "集約" および "リポジトリ" と "一覧画面" の相性の悪さ

ドメイン駆動設計を取り入れると, ひとつのトランザクション整合の単位として "集約" を用いることになり, 集約の永続化および再構築のために "リポジトリ" を用いることになりますよね.
しかしこの集約およびリポジトリは, いわゆる "一覧画面" のような機能とは以下の観点で非常に相性が悪いです.

  • リポジトリから集約を取り出す際に, 複数集約取り出すことを考慮していないため DB アクセスが複数回発生してしまう
  • 集約は一覧画面で必要のないプロパティも保持している

"更新" と "参照" では適したデータ構造が異なる

そもそも "更新" と "参照" では適したデータ構造が異なるようです.
リレーショナルデータベースの場合, 更新対象のデータは極力正規化されていたほうが更新影響が小さく, 一方で参照対象のデータは極力非正規化状態のほうが複雑なクエリを発行する必要がなくなります.

正規化のメリット・デメリットという観点で下記の記事がわかりやすかったです.

https://zenn.dev/shiso/articles/c35e10a8ae9505

同じデータを更新と参照の両方で利用しようとすると, このコンフリクトの中で良い塩梅のテーブルレイアウトを考えることになりますが, これは複雑性を高めます.

更新と参照の責務を明確に分離する CQRS (Command Query Responsibility Segregation) という設計方針

この問題に対応するための考え方として CQRS (Command Query Responsibility Segregation) という設計方針があります.

https://docs.aws.amazon.com/ja_jp/prescriptive-guidance/latest/modernization-data-persistence/cqrs-pattern.html

コマンド(更新)とクエリ(参照)の望ましいデータ構造が異なるのであれば, それぞれを分離して作ってしまえばよいという考え方です.

CQRS を利用することで, コマンド側 (Write Model) はドメイン駆動設計を取り入れ, クエリ側 (Read Model) は純粋なクエリだけにすることができるため, それぞれの得意を活かしながら関心事を分けることができシンプルな設計にすることができます.

ちなみに Write Model から Read Model へのデータの連携には Read Model Updater を介することになります. つまり, 結果整合です.
それぞれの責務が明確になることからシンプルな設計にはなりますが, その実装はイージーではなく, フレームワークやライブラリに頼らずに実装することは難しいです.

最新の状態を保存するかこれまでの履歴(イベント)を保存するか

データの永続化にはステートソーシングという考え方と イベントソーシング という考え方があります.

https://docs.aws.amazon.com/ja_jp/prescriptive-guidance/latest/modernization-data-persistence/service-per-team.html

ステートソーシングとは集約の最新の状態を保存しておく考え方で, 一般的に広く使われている永続化の考え方です.
一方でイベントソーシングとは集約の振る舞いによって発生した "イベント" に注目し, 過去に発生したすべてのイベントを口座明細のように履歴保存しておく考え方で, 過去のイベントをすべて順に再計算することで集約の最新状態を再構築することができるという考えに基づいています.

最新の状態を取得するために毎回過去のイベントをすべて順に再計算するとなると, イベントが増えるほどに性能に問題が現れることは想像できると思います. この問題の解決のために定期的に snapshot を取得し, snapshot 以降のイベントのみを再計算することで集約を再構築するというのが基本的な考えになります.
しかし, こちらもその実装はイージーではなく, フレームワークやライブラリに頼らずに実装することは難しいです.

アクターモデルというパラダイム

アクターモデル とは生成, 送信, 状態変化, 監督の4つの操作を持つ軽量プロセス(OOP でいうオブジェクトのようなもの)であるアクターが, 互いにメッセージを非同期に送り合うことでスケーラビリティを保ちながら並行処理を実現する概念のことをいうようです.

https://ja.wikipedia.org/wiki/アクターモデル

メッセージを送り合うアクター

アクターはそれぞれ独自のアドレスを持ち, アクター間は互いのアドレスに向けてメッセージを投げ合います.
アクターは各自自身のポストのようなもの(メッセージボックス:キューのようなもの)を持ち, 届いたメッセージはそのポストに投函されます. アクターは自身のポストから順にメッセージを読み, そのメッセージに適した振る舞いを実施します.

このようにしてアクター同士がメッセージを送り合い, 送られてきたメッセージに合わせた振る舞いを実施することで, アプリケーションとしての処理を実現します.

4つの操作を持つアクター

アクターは以下の4つの操作を持ちます.

  • 生成
    • アクターは別のアクターを生成することができる
    • 生成したアクターと生成されたアクターはそれぞれが親子関係となり, すべてのアクターは生成によってヒエラルキを構築する
  • 送信
    • アクターは別のアクターに対してメッセージを送信することができる
    • すべてのアクターは自分専用のメッセージボックス(キューのようなもの)を持ち, 送られてきたメッセージはメッセージボックスに投函される
    • アクターはメッセージに合わせた振る舞いを起こす
  • 状態変化
    • メッセージボックスに投函されたメッセージは必ず一件ずつ順番に捌かれるようになっており, メッセージに合わせて振る舞い, 自身の状態を変化させる
    • アクターは自身の状態を変化させることで, メッセージに対する振る舞いを切り替えることができる
      • ex) “寝起き” 状態の時に “挨拶して” というメッセージを受け取ると “おはよう” と応えるが, “就寝前” 状態の時に同じ “挨拶して” というメッセージを受け取ると “お休み” と応える
  • 監督
    • アクターは自身の子アクターを監督する責任があり, 子アクターに異常が発生した場合に適した対策を行う
      • どのような対策を行うかは設定したストラテジ(戦略)に従う

Akka という Toolkit

アクターモデルを簡単に実現することができる Toolkit で, Scala の開発元でもある Lightbend 社によって Scala 及び Java 向けに開発, 提供されています.

アクターモデルを簡単に実装することができるだけではなく, イベントソーシングによる永続化を実現できる akka-persistence, 複数プロセス間でのクラスタリングを実現する akka-cluster, ストリーミングを実装できる akka-streams など, アクターモデルを活用するためのエコシステムが充実しています.

アクターモデルを簡単に実装することができるライブラリ

純粋なアクターだけであれば下記のコードだけで実装することができます(公式のサンプルコードを参照).

https://github.com/akka/akka/blob/v2.10.0/samples/akka-quickstart-scala/src/main/scala/com/example/HelloWorld.scala#L11-L20

このアクターは以下のような特徴を持ちます.

  • 返信先(ここでは message.replyTo に設定されているアクター)に対して Greeted(message.whom, context.self) というメッセージを送信する

サンプルコードのように HelloWorld アクターと HelloWorldBot アクターのメッセージ送り合いは簡単に実装することができます.
メッセージの送信には tell と ask があります. tell はいわゆる fire-and-forget でありメッセージを送信したらそのアクターは責任を放棄します. 一方で ask は非同期処理の完了を待ちます.
tell の代わりに !, ask の代わりに ? を使用することができます.

イベントソーシングによる永続化に対応している

akka-persistence というモジュールを導入することでイベントソーシングを用いた永続化を簡単に実現することができます.

https://doc.akka.io/libraries/akka-core/current/typed/persistence.html

イベントソーシングでは毎回すべてのイベントを再計算して最新の状態を再構築するとパフォーマンスが落ちてしまうため, 定期的に snapshot を取得し snapshot 以降のイベントのみを再計算することで最新の状態を再構築する実装が推奨されますが, 前述の通りこれは簡単な実装ではない点において導入のハードルがあります.
しかし, akka-persistence を導入することで下記の実装のみでこれらを導入することができるようになります.

https://github.com/kuramapommel/til-akka-typed/blob/v0.1.1/sample/src/main/scala/com/kuramapommel/til_akka_typed/adapter/aggregate/ProductActor.scala#L88-L97

akka.persistence.typed.scaladsl.EventSourcedBehavior を生成することでイベントソーシングによる永続化が可能になります.
commandHandler によってコマンド(メッセージ)受信時の振る舞い, eventHandler によってイベントに基づく再計算時の振る舞いを決め, その後の snapshotWhen によって snapshot のタイミングについてのオプション設定が可能です.

複数のプロセスでクラスタリングできる

akka-cluster というモジュールを導入することで複数のプロセスでクラスタリングできます.

https://doc.akka.io/libraries/akka-core/current/typed/cluster.html

クラスター内でアクターは一意になることが保証されます. これにより, 対象のアクターに対するメッセージは必ず順序どおりに処理されることが保証されます.
また, アクターの持つ位置透過性の特性はクラスタ内でも有効に働き, アクターへのメッセージは ShardRegion と呼ばれるメッセージの仕分け施設に対して送信することで対象のアクターにメッセージを届けてくれるため, アクターが存在する具体的なプロセスを把握する必要がなくなります.

また, クラスター内ではアクターの存在するプロセスがアクセス不能になった場合, 対象のアクターを他のプロセスで引き継いでくれる機能も備えています.
システム全体がダウンしない限りクラスタ内のいづれかのプロセスが生きていればアクターは引き継がれるため, 高レジリエンスなシステムを構築しやすくなっています.

クラスターシャーディングは下記の実装で実現することができます.

https://github.com/kuramapommel/til-akka-typed/blob/v0.1.1/sample/src/main/scala/com/kuramapommel/til_akka_typed/adapter/aggregate/ShardedProductActor.scala#L24-L37

akka.cluster.sharding.typed.scaladsl.ClusterSharding を用いて ShardRegion を生成初期化し, 受け取ったメッセージを akka.cluster.sharding.typed.ShardingEnvelope にラップして ShardRegion に送信することで, メッセージをクラスター内に存在する対象のアクターに対して送信してくれるようになります.

akka-management を用いることで kubernetes 上でのクラスタリングも可能

akka-cluster の複数プロセス間でのクラスタリングは kubernetes 上の Pod 間でも実現することができます.
そのためには akka-management というモジュールを導入する必要があります.

https://doc.akka.io/libraries/akka-management/current/index.html

akka-management は kubernetes のヘルスチェック機構を利用し, Pod の起動状況に合わせてクラスター管理を行ってくれます.
もちろん kubernetes のスケール(Pod 数の増減)にも柔軟に対応してくれるため, より高レジリエンスなシステムの構築をすることができます.

akka-management の起動のためには下記の実装と設定が必要です.

設定

https://github.com/kuramapommel/til-akka-typed/blob/v0.1.1/sample/src/main/resources/akka-management.conf#L5-L24

実装

https://github.com/kuramapommel/til-akka-typed/blob/v0.1.1/sample/src/main/scala/com/kuramapommel/til_akka_typed/Main.scala#L62-L63

また, ヘルスチェックのためには下記の実装が必要です(たぶんこれは任意です, 試していないですが設定しない場合デフォルトのものが使われると思います)

https://github.com/kuramapommel/til-akka-typed/blob/v0.1.1/sample/src/main/scala/com/kuramapommel/til_akka_typed/HealthChecker.scala

システムレジリエンスとリアクティブシステム

システムレジリエンスという考え方

システムのレジリエンスという考え方が注目されているようです. resilience とは「回復」や「弾性」という意味を持つ単語であり, システムの文脈においては障害発生時に被害の局所化と速やかな自己回復を可能とする概念のことを指します.
2020 年に東京証券取引所の大規模なシステム障害が発生したことがありましたね, CIO の方の説明が素晴らしいとエンジニア界隈で話題になった記憶があります.

https://www.youtube.com/watch?v=ACFLlMXhlWg

この件の再発防止報告書( siryo_japanese_20210325.pdf )には下記のように記されています.

これまでの「ネバーストップ」をスローガンとする信頼性向上の取組みのみならず、「レジリエンス」(障害回復力)の向上も同様に重視して取り組む

東証さんのような社会インフラと呼べるプロダクトにおいてもその必要性が謳われるようになり, 社会的にも信頼性の観点においてシステムレジリエンス向上の必要性が高まってきているようです.

リアクティブシステムとは

リアクティブシステムは リアクティブ宣言 にて定義されています.
リアクティブ宣言とは 2014.09.16 に Scala/Akka の開発元である Lightbend 社がリアクティブシステムの定義及びリアクティブ原則を公開したものです. 2024.03.30 時点において3万名以上が署名しています.
リアクティブ宣言によると, 即応性, 耐障害性, 弾力性, メッセージ駆動の4つの特性を併せ持つものがリアクティブシステムとなります. これはより柔軟で疎結合でスケーラビリティがあり, それにより開発が容易になり変更を受け入れやすくなります. また, 障害に対する耐性と障害発生時の対処を備えます. つまりリアクティブシステムを構築することはシステムレジリエンスの向上につながるという理解です.

Akka はまさにこのリアクティブシステムの構築に適した Toolkit と言えるため, 学習難易度は高いですが学ぶ価値の高いものであるとぼくは考えています.
今後実務でも使っていきたいですね.

独学する中で詰まったところ

ここまではネット上で調べたり ChatGPT に聞くと教えてもらえるようなことばかりに触れてきましたが, ここからはぼくが実際に独学を進めるうえで詰まったところや難しいと感じたところをピックアップしてまとめて行こうと思います.
正直いたるところで詰まりまくったたのですが, ここで上げているもの以外の詰まっている様子については, 下記のツイートにリプライツリーでその都度呟いているので, 気になる方は除いてみてください.

akka persistence を使ったコンテナを Apple Silicon で動かすとき

これは Akka の問題というものでもないのですが, akka-persisntence は開発用の永続化に LevelDB を利用しています.

https://github.com/google/leveldb

Apple Silicon で Docker コンテナを起動する際に, 明示的に --platform linux/arm64 を指定しなければ起動できなくなってしまうため注意が必要でした.

akka-cluster において split brain の問題を解決する設定を入れていなければクラスタ内でノードが  down した際に他のノードで引き継がれない

Split Brain という問題があります.
詳しくは下記のリポジトリや動画がわかりやすかったので説明を頼ってしまいます(すみません).

この問題を解決するために, SplitBrainResolver というものを設定する必要があります.
具体的には下記の設定をします.

akka.cluster.downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider"

当時これが動いて喜んでいる様子がこちらです笑

https://x.com/nullpommel/status/1801251157295383032

dyanamodb を使用したジャーナルテーブルとスナップショットテーブルの初期化

dynamodb をジャーナルテーブル(イベントの永続先)とスナップショットテーブル( snapshot の永続先)に使用する場合は, akka-persistence-dynamodb モジュールを用いる必要があります.

https://doc.akka.io/libraries/akka-persistence-dynamodb/current/index.html

テーブルの作成ついては, 下記のディレクトリに存在する create-tables.sh を実行します.

これに気づくまでは, どのようなテーブルの構成にすればよいのかがわからず詰まってしまいました.

akka-managemanet を起動するタイミング

kubernetes api を利用して Pod のヘルスチェックを行いますが, akka-management の起動のタイミングを誤ってしまうとこれが有効に働かなくなってしまいます.
それだけではなく, そもそもクラスターの構築ができなくなってしまいます.

上述の akka-management の説明で記載したように akka-mangagement は ActorSystem に渡す Behaviors の中で起動する必要があります.
最初は ActorSystem 起動後に akka-mangagement を起動してしまい, クラスターの構築ができず困りました.

Akka 以外にイベントソーシングを実現できる(らしい)ライブラリなど

イベントソーシングは様々な問題を解決してくれる強力な永続化の設計思想ですが, 実装が難しいという点でどうしてもフレームワークやライブラリに頼る必要があります.
今回学んだ Akka 以外にもイベントソーシングのためのライブラリはいくつかあるようですので紹介しようと思います.
※ 実際に詳しく調査をしたわけではないのでぼくの認識が正しいかは定かではありません.

さいごに

今回 Scala3 や kubernetes も合わせて学んできました.
アクターモデルや Akka だけでなく CQRS + ES など広く学ぶことができたのでエンジニアスキルが向上できた実感があります.

minikube 上で akka-management によるクラスタリングが動いたときはものすごく嬉しかったです, 新しい技術を学ぶのは楽しいですね.

https://x.com/nullpommel/status/1868316635897204919

ただ, 実際はドキュメントは英語ばかりで日本語の情報はまだまだ少なく, DeepL や ChatGPT を駆使して和訳しながら頑張って読み解いたりしていましたが正直かなり大変でした.
日本語のドキュメントが充実すると, もっと普及するんじゃないかなと思いました.

今後は下記のようなことも学んでいきたいと考えています.

  • 今回は Read Model の実装だけだったので, DynamoDB Streams を利用した Read Model Updater や Read Model 側の実装
  • CQRS は GraphQL とも相性が良いと言われているので GraphQL との組み合わせ
  • エラー発生時に打ち消しのイベントを発行することでハンドリングする処理
    • この辺は Saga パターンってやつが活かせそう?

システムレジリエンスが注目されるなかマイクロサービスの構築として CQRS + ES やアクターモデルの知識は今後必要になってくると考えているので, 引き続き学習を続けていきたいと思います.

長くなりましたが, 最後までお付き合いいただきましてありがとうございました.

参考文献

他にも色々あります. Twitter のツリーを見ていただけると悶え苦しみながらいろいろな資料を漁っていた姿が見れると思います.

また, 沢山の方に色々教えていただきました.
とくにかとじゅん @j5ik2o さんには Twitter 上でたくさんのことを教えていただきました, 本当にありがとうございました, この場を借りてお礼させてください m_ _m

https://x.com/j5ik2o/status/1807830280302219342

Discussion