株式会社ユーザベースの技術チームブログです。ElixirライブラリのFlowを使ってAPIを優しく呼び出す2019-12-16ElixirライブラリのFlowを使ってAPIを優しく呼び出すElixirTweetこんにちは。SPEEDA開発チームの上村です。先日同僚が書いたElixirのコードを眺めていた際に、とあるバッチ処理内でback-pressureを用いてAPIサーバを優しく呼び出している処理を見かけました。そのコード内で用いられていたライブラリを調べてみるとなかなかに面白かったので折角なので今回まとめてみました。本エントリでは、APIサーバを優しく呼び出すことを「APIサーバが過負荷にならないくらいの負荷で呼び出す」と定義し、それを実現する方法の1つとしてElixir製のライブラリであるFlowを用いたback-pressureの手法について述べていきます。FlowとはFlowはElixir製のコレクションを並列分散処理することに特化したライブラリです。Flowを用いて、あるテキストファイルに含まれる単語の出現頻度を数え上げるコードを書くと以下のようになります。Enum.to_list()細かなFlowのAPIについて特に知らなくても、コードを見るとこのコードが何をしたいのかの雰囲気が伝わってくると思います。(GenStageについては後述)。GenStageのラッパーには、MessageQueueをGenStageに連携させることに特化しているBroadwayも存在します。GenStageとはGenStageのリポジトリを見ると、GenStageはproducerとconsumer間でイベントを交換するための仕様と述べられています。イベントと言われるとちょっとふわっとしている印象を受けますが、データ処理のトリガーというニュアンスで捉えても大丈夫だと思います。GenStageでは、イベントの処理を行う単位をステージと呼び、ステージはproducer、producer_consumer、consumerのいずれかに属するものとして扱います。これらのステージを組み合わせてパイプラインを構成し、イベントを処理する仕組みを、GenStageは提供します。各ステージの役割を把握するために下記のようなパイプラインを考えてみます。[C]この例において、Aはproducer、Bはproducer_consumer、Cはconsumerに対応します。producer/producer_consumer/consumerはGenStageでは以下のような役割を持つものと説明されています。producerは、イベントの送り元であるため、イベントをconsumerへと受け渡します(つまり、sourceとなります)producerは、イベントの受け手であるため、イベントをprocuerから受け取ります(つまり、sinkとなります)producer_consumerは、producerでもありconsumerでもあります(つまり、前後のステージからの見方次第で異なる役割に見えることになります)イベントを処理するためにはconsumerがproducerを購読しなければなりません。そのため、producerはconsumerからの催促を待ち受けていると表現されたりもします。パイプライン内の各ステージにイベントが伝搬する際のトリガーは、consumerの役割を持つステージがproducerの役割を持つステージに対して処理対象のイベントの需要がどれくらいあるかを伝えることです。producerはその需要ときっちり同じ量のイベントをconsumerに対して送出することをきっかけに、先のパイプラインのフローのようなイベントの処理が行われます。このようなconsumer側が処理可能なイベント量のみをproducerに伝えることで処理中のイベント量を管理するような方法は一般的にback-pressureと呼ばれています。オプションを設定することでコントロールされます。対応するproducerと連結しているフロー内に存在するイベントの数が少なくなりすぎないように、フロー内にイベントを補充する閾値を設定します。Bがイベントを処理します。Aに対して新たに伝えて処理を続けることになります。:max_demandと:min_demandのパラメータのイメージは下記のようになります。Aを購読している場合を想定しています)まず、:max_demand相当のイベントが送出されます。->(->が各イベントに対応。:max_demandの数だけイベントが存在)処理が進むと、パイプラインのイベントは:min_demand個まで捌けた状態になります。:mix_demand個のイベントの送出需要を伝えて、パイプライン内のイベント総量が増えた状態にします。最初の状態に近しい状態になります。->以上のようなイメージで:max_demandと:min_demandのパラメータは作用します。ちなみに、FlowはGenStageがベースとなっているため、このようなback-pressureの仕組みが背後で動いていることを理解しておくとパラメータチューニングの際に役に立ちます。Flowの利用例Flowを使ってAPIを優しく呼び出すサンプルコードを示します。利用するAPIはランダムに200のステータスコードか503のステータスコードを返します。サンプルコード内では、503のステータスコードを受け取った際は最大3回リトライする処理を簡素ながらリトライ処理として実装しています。サンプルコードこちらに置いてあります。リポジトリ内のディレクトリツリーは以下のようになっています。fragile_api_clientランダムにステータスコード200かステータスコード503をレスポンスとして返すAPI(fragile_api)、そのAPIをFlowを使って呼び出すクライアント(fragile_api_client)の2つのディレクトリから構成されます。本エントリがElixir関連のものなので、折角なのでfragile_apiはElixirのWebフレームワーク群の中で著名なPhoenixを用いて実装しました。Flowの依存関係fragile_api_client内のmix.exsに以下のような依存関係を追加しています。endAPIを呼び出す総回数は何回でも良いのですが、この例では30回としています。1..30で先のGenStageの説明におけるproducerの役割となるステージを1つ作成します。consumerの役割のステージを3つ作成します。Flowを使った処理はここまでで、最後にEnum.to_listとEnum.reduceを使って処理結果のサマリーを作成しています。上記のコードを実行した結果は以下となります。Phoenixアプリのモニタリング状況の可視化までは至りませんでした...。本エントリではFlow経由でGenStageのback-presssureを用いてAPIを優しく呼び出す方法について述べました。FlowはGenStageが抽象化したback-pressureを更に抽象化することで、クライアントコード側の処理をより直感的に書けるようにしてくれます。consumer/producerの購読関係を明示的に一つ一つ設定することなく、サッとイベントのパイプラインを書くことができるのは非常に魅力的です。バッチ処理などでデータを多段に処理する際の選択肢として、Flowの採用を考えてみるのも面白いのではないかと思います。18:51ElixirライブラリのFlowを使ってAPIを優しく呼び出すTweet関連記事2019-10-07Ringアプリケーションで例外をいい感じにハンドリングする方法(Ductでの解説も含む)こんにちは!こんにちは!SPEEDA開発チームのあやぴーです。社...2019-08-05Istioでマイクロサービスのテスタビリティを向上させるSPEEDAの開発チームの石橋です。Kubernetesのネットワーク...こんにちは。SPEEDA開発チームの鈴木です。»プロフィール『経済情報で、世界を変える』をミッションに、企業・業界情報プラットフォーム「SPEEDA」やソーシャル経済ニュース「NewsPicks」、ABMを強力にサポートする「FORCAS」、スタートアップの情報収集を圧倒的効率化「entrepedia」などを提供する株式会社ユーザベースの技術チームブログです。多国通貨」をハンズオンしたら快適で楽しかったGo言語でオブジェクト指向プログラミングの基本(型編)月別アーカイブ検索@Uzabase_Techさんのツイートスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。