マイクロサービスにも使われている「アクターモデル」による並列処理プログラミング入門

こんにちは、サイオステクノロジー技術部 武井です。今回は、マイクロサービスにも使われている「アクターモデル」という技法で、並列処理プログラミングをしてみたいと思います。

アクターモデルとは?

並列処理プログラミング技法の1つです。マイクロソフトが提供しているマイクロサービス「Azure Service Fabric」や、LINEのメッセージ処理にも採用されています。このモデルが提唱されたのは、1973年とだいぶ昔なのですが、スケールアウトを得意とするクラウドインフラと相性がよく、再び脚光を浴びるようになったようです。

「ムーアの法則」が終焉に近づいている昨今、インフラはスケールアップよりスケールアウトによって、性能向上を図るのが主流となっています。それに追従して、プログラムもスケールアウト前提で作り込むことが求められ、アクターモデルに基づいて実装されたプログラムは、インフラのスケールアウトに比例して、その性能を向上させることが可能です。

アクターモデルを採用した有名なフレームワークに「Akka」があります。Akkaは、Scala及びJava向けに作られた、アクターモデルをベースにしたフレームワークです。Akkaは耐障害性、スケーラビリティ、即応性などを実現するために、大変多くの機能を備えています。ただ、機能が多すぎて、アクターモデルを学ぶ教材にしては、少々目的にかなわない部分があります。Akkaの使い方の説明だけで終わってしまいます。

そこで、今回は、超シンプルな実装例を交えて、わかりやすく解説することを目的としております。本記事で紹介するアクターモデルのサンプルはJavaで実装しており、なるべくJava標準で提供されているクラスライブラリのみを使って説明することで、アクターモデルの本質をご理解頂くことを目的としています。

アクターモデルのない世界

細かい説明は抜きにして、まず、アクターモデルのない世界では、どのようなシステム構成を取るのかを以下に説明しています。

Screen-Shot-2018-04-27-at-7.14.41-1024x526

上記は一般的なメッセージキューイングシステムです。キュー(RabbitMQなど)に登録されたメッセージを、コンシューマーと呼ばれるプログラムが取り出し、処理を行います。このあたりの詳細については、こちらの記事をご覧頂くとわかりやすいかもしれません。

では、コンシューマーの処理スピードがちょっと物足りなくなってきた場合を想定します。もっともっと短時間にたくさんのメッセージを処理したくなってきました。そんな場合、以下のような構成にするのが一般的と思います。

Screen-Shot-2018-04-27-at-7.21.29-1024x680

コンシューマーの数を1つから2つに増やして、スケールアウトします。しかし、上記の構成は、複数のコンシューマーが1つのキューを参照するので、同じメッセージをダブって取得しないよう、排他処理を考える必要があります。上記の例はまだ非常に単純な例ですが、システムがもっと複雑になると、排他処理ももっともっと複雑になってきます。これでは、おちおちスケールアウトもできません。

アクターモデルのある世界

アクターモデルのある世界ではどの様になるでしょうか。アクターモデルでは以下のような構成になっています。

Screen-Shot-2018-04-27-at-7.27.40

アクター自体は、本記事で後に紹介する実装で言えば、アクタークラスから作成されたインスタンスに相当し、そのインスタンスはスレッドとキューを持っています。スレッド及びキューは、それぞれ「アクターモデルのない世界」でいうところの「コンシューマー」「キュー」に相当します。コンシューマーはシングルスレッドで実装され、キューはjava.util.Queueを実装したConcurrentLinkedQueueで実現します。

ここで、先程と同様にキュー内のメッセージを短時間でもっとたくさんの数を処理したくなったとします。アクターモデルでは、以下のようにスケールアウトします。

Screen-Shot-2018-04-27-at-7.37.39

単純にアクターを1つ増やしただけです。たったこれだけです。アクターはそれ自体にキューを内包しているので、排他処理を考慮する必要がありません。先程も説明しましたが、アクター内のスレッドはシングルスレッドで動作します。そのため、キューに登録されたメッセージを順々にシリアルに取得して処理を行う(たしかConcurrentLinkedQueueはFIFO)ので、今までのように排他処理に頭を悩ませるようなこともありません。

ただ、アクターにメッセージを投げるプログラムは、複数あるアクターにどのような方法でメッセージを投げるかを考慮しなければいけません。Akkaフレームワークでは、Routerという機能が該当し、Round Robin(複数のアクターに均等にメッセージを投げる)やBroadcast(複数のアクターに同じメッセージを投げる)といったルールがあります。

そういったことを考慮しなければならないにせよ、このように単純にスケールアウト可能な構成は、昨今のスケールアウトを基本としたクラウドインフラと相性がよく、とても魅力的に感じます。

アクターを作ってみる

説明だけではやっぱりよくわからないので、実際にアクターを作ってみることにします。実装言語はJava、必要な前提知識は、基本的なスレッド処理だけです。Akkaのドキュメントを見ると、非常に複雑な構成に見えますが、基本的にはこれからご説明するような実装がベースとなっています(実際中身のソースコードを見たわけではないですが、多分そうです)。

今回のソースコードの構成は以下のとおりです。

Screen-Shot-2018-04-27-at-7.53.08-1024x574

Executor アクターを呼び出すクラスです。アクターに投げるメッセージの作成及びそのメッセージをアクターに投げる処理を行っています。
AbstractActor
Actorの抽象クラスです。キューの生成、キューにメッセージを登録するtellメソッドの定義、メッセージの処理方法を定義するメソッドの定義(実装は継承先で行う)をしています。
GreetingActor AbstractActorを継承したクラスです。メッセージの処理方法を定義しています。Messageインターフェースを実装したGreetingMessageの内容を標準出力に出力しています。
Message キューに格納するメッセージのインターフェースです。キューに格納する前のメッセージを複製するために、Cloneableインターフェースとcloneメソッドを実装しています。
GreetingMessage Messageインターフェースを実装したクラスです。キューに格納するメッセージのクラスを定義しています。

では、これらのソースコードを以下に記載します。

■ Massage.java

■ GreetingMessage.java

■ AbstractActor.java

■ GreetingActor.java

■ Executor.java

ソースコード全体を通して見るとわかるのですが、synchronized句がありません。アクター内ではシングルスレッドで動作しているので、先程、ご説明したように排他処理を考慮する必要がありません。

スケールアウトしたい

では、アクター1つでは処理が追いつかなくなったことを想定して、アクターをスケールアウトしてみましょう。

■ Executor.java

actor2というインスタンスを追加しただけです。実際は、このactor2というインスタンスは、別のマシンで実行する必要があるのでしょうが、今回は説明の都合上、同じマシン同じJVM上で複数のアクターを作りましたが、マシンが別でも基本的には同じことです。

最後に

いかがでしょうか?以前は、性能向上するためには、マルチスレッドでプログラミングをし、性能向上に伴いスケールアップ、スレッド数増加というのが主流でしたが、これから、スケールアウトを前提としたアクターモデルのような方法が主流になっていくのではないかと思います。

ちなみに、先程もご説明しましたが、このアクターモデルを採用したAkkaというフレームワークがあります。

https://akka.io

耐障害性、スケーラビリティ、即応性、Self-Healing等、魅力的な機能がいっぱいです。弊社サービス「Azure課金管理サービス」のバッチに適用して、速度向上を狙おうと、今ワクワクしています。

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です