News

Facebook Icon Twitter Icon Linkedin Icon

AnyMind Group

Facebook Icon Twitter Icon Linkedin Icon

[Tech Blog] Scala, Akkaで動くAnyChatプロダクトのバックエンドについて

こんにちは、AnyMindでマネージャーをやっている柴田です。AnyChatのTechLeadも兼任しています。 先日、AnyMind GroupはAnyChatというプロダクトをリリースしました。 この記事では、AnyChatの大まかな紹介と、それを実現するためにBackendがどのような技術を用いて設計しているのかを理由を交えてご紹介したいと思います。

AnyChatとは

プロダクト開発の背景と狙いはプレスリリースを参照頂ければと思いますが、AnyChatはチャットコマースプラットフォームを目指して開発されました。 機能としてはCRMツールの一種と考えており、ユーザーの属性や購入履歴といった行動履歴を元に、適切な商品のレコメンド、ブランドイメージに合ったカスタマーサポートの提供支援などを通じてLTVを最大化させることを目指しています。

AnyMindの他プロダクト同様にグローバルに展開できることを念頭に置いて設計していまして、まずは接続するチャットツールとしてLINEを選んでいますが、今後はグローバルで広く使われているFacebook(Instagram)や、WhatsAppなどのチャットツールと連携していきます。Shopについても同様に、Shopifyなど複数のオンラインストアと連携していく予定です。

システムの全体像

チャットシステムに求められる機能の大前提は「ユーザーがいつ問い合わせを行っても即座に応答できること」でしょう。 ソフトウェアの世界においては、Reactive Manifestoの言葉を借りて、即応性・耐障害性・弾力性、があると言い換えることができると思います。

Reactive Manifestを取り上げましたが、AnyChatのBackend設計においてもリアクティブシステムを目指して設計・開発をしています。

システムの全体像は以下のようになっていまして、それぞれのマイクロサービスで最適な技術・設計を取り入れることができるようにしています。

システム全体としてはインフラに主にGCPを採用しており、Kubernetes(GKE Autopilot)上に各マイクロサービスをデプロイしています。Kubernetesを選んでいる理由は、もはやデファクトスタンダードだからと言っても良いのですが、オートスケーリング、ローリングアップデートの細かな調整ができ、Podがクラッシュしてもクラスタ内で自己修復してくれるからです。

サービス間の通信プロトコルはgRPCを使っています。こちらももはや一般的な技術ですが、ストリーミング送信をサポートしていること、クライアントコードを自動生成してくれることが理由です。 ちなみに、AnyChatの各マイクロサービスは他プロダクトからも使用されるという事情があるためCloud Endpointを使ってgRPCエンドポイントをpublicにおいています。今回の記事では割愛しますが、興味がある方はこちらの記事をご覧ください。

[Tech Blog] Publish gRPC API using Cloud Endpoints & GKE

バックエンドの言語はScalaを採用しています。理由は、後述しますがAkka Clusterを使いたいからと、ドメインロジックを簡潔に書けるからです。 またドメインロジックはZIOを用いて記述しています。初めはCatsを使っていたのですが、Monad Transformerへの変換やLiftなどが混在するコードと比べると、ZIOは一つの型で多様な表現ができて便利なメソッドも多いため、ドメインロジックを素直に書くことにより適していると考えています。

ここから、それぞれのマイクロサービスでどのような設計方針があり、どういう技術を選んでいるのかをご説明します。

LINE Service

このサービスはLINEとのやり取りの全般を行うことが責務となっています。ユーザーがLINE上で友達申請やメッセージを送ると、LINEからwebhookが飛んでくるのでこのシステムで受けて適切な応答を返します。

冒頭で述べたリアクティブシステムを目指しているのは主にこのサービスで、ユーザーがいつアクションしてもデータの取りこぼしなくすぐに応答できること、またそれぞれのユーザーの属性に合った内容を返せることを目指しています。

使用技術としてはAkka Clusterを中心にメッセージ駆動を取り入れています。LINE Botとユーザーとの会話それぞれにIDを振りActorを割り当てます。こうすることで各会話のStateはActor上で一貫性のある形で効率的に扱えますし、各Actorはクラスター内で分散して配置されるので水平にスケールさせることができます。 ActorはLINEから送られてくるイベントをなるべくそのままCassandra(AWS KeySpace)に永続化します。その後、Akka Projectionを用いてユーザーへの応答やCloud Spannerへの保存といった後続処理を行います。Spannerのデータは管理画面などからの読み取りに最適化されており、CQRS + ESの典型的なパターンかなと思います。

Shop Service

このサービスはオンラインストアとのやり取りの全般を行うことが責務となっています。一般にストアにはCustomer、Product、OrderというEntityがあるので、それらのコピーを保持したり、ユーザーの代わりに購入を行ったり、Customerの更新やOrderが発生したイベントを検知し上位レイヤー(CRM Service)に通知します。

例えばShopifyの場合であれば、Admin APIの使用に制限があったり応答速度が十分でないケースがあるため、読み取り専用のコピーを持つことが必要になります。 また、どのストアでも認証ロジックやアクセストークンの管理など固有の実装が必要になったり、Orderのフォーマットが異なったりするため、そういった差異を吸収し、なるべくシンプルなAPIを提供できるように設計しています。

こちらはあくまで外部のストアのデータが正で、このサービスで扱うデータはコピーとみなせるので、可用性などの非機能要件は厳しくなく、技術的な難しさはあまりありません。

CRM Service

このサービスは他マイクロサービスからのリクエストや通知されるイベントから、顧客毎のChannelの管理や後続の処理に繋げることが責務となっています。 例えばオンラインストアで商品が購入された際にLINEでOrderの詳細を通知するとか、LINEでユーザーから問い合わせを受けたら過去の購入履歴からオススメ商品をリコメンドするなどです。

このサービスではLINEユーザーとShopのCustomerを繋ぐという役割上、LINE ServiceもShop Serviceも知っていますし、アカウント連携なども扱うのでユーザートークンをどう扱うかなどもロジックに入ってきます。多様な処理を行う必要があるため、他マイクロサービスよりもドメインロジックが複雑化しやすく、場合によってはSagaのようなrollback処理が必要になります。 ただ非機能要件は厳しくなく、問題があってもクライアントにエラーを返したり、リトライで解決できれば問題ないケースがほとんどです。

Console Service

このサービスは、Frontendからのリクエストを受け付けたりユーザー認証を行うことが責務となっています。

技術的には、Frontendとの通信プロトコルにはGraphQLを用いていて、ライブラリとしてはZIOフレンドリーであるCalibanを採用しています。まだ調整中ですが、GraphQLのsubscriptionを用いて、ユーザーの問い合わせを受けてリアルタイムに画面を更新したり、ブラウザへ通知を送る機能も準備しています。

認証はAuth0を用いており、今後はAnyMindの他プロダクトからのSingle Sign On機能も検討しています。

今後取り組みたいことや課題

Non-blocking DB access

現状、DBとしてSpannerとPostgreSQLを使用していて、ライブラリとしては双方で使えて慣れているScalikeJDBCを使用しているのですが、JDBCはBlockingな処理なのでもう少し最適化したいと考えています。 ただ、その場合PostgreSQLはおそらく問題ないとしても、Spannerは公式のAsyncなクエリビルダーを使うかPostgresモードを選ぶかを調査する必要があり、まだ手を付けられていないです。

AkkaのActorロジックの拡充とチューニング

Actor内でのイベントの処理は同期的に行わなければ状態の一貫性が保てません。しかしチャットの自動応答のロジックを組んでいくにあたって、機械学習APIなどと連携してQ&Aの応答を返したり、商品の在庫や配送状況に応じて応答内容を変えるといったことを今後取り組みたいと考えています。 そういった機能追加に対して更新が競合することなく、パフォーマンスも高いように設計することはなかなか難易度が高そうだなと感じています。

Scala3やZIO2.0への対応

僕はIDEにIntellij IDEAを用いていて、Scala3への対応がまだ不十分だと感じているのでScala2系を使い続けているのですが、どこかのタイミングで3系に移行したいですね。 例えばOpaque TypeやEnum Typeなどを使えばより型安全に直感的にドメインロジックを記述できるのでは無いかなと思います。

また、ZIO2系がRC2まで来ているので、こちらも正式にリリースされたら移行を始めたいと考えています。 こちらは使い勝手が大きく変わる変更は無さそうで、一部の冗長な記述の修正とパフォーマンス改善がメリットかなと考えています。

マーケティングオートメーション機能の拡充

AnyChatはチャットという重要な顧客との接点を握るプラットフォームであり、かつオンラインストアと繋ぐことでユーザー毎の売上も細かく分析することが可能なので、LTV最大化のためにできることは非常に多いと考えています。 どの頻度でキャンペーンの通知を送るべきかに始まり、ユーザーの属性に合わせた商品のリコメンドやQAアルゴリズムの開発、チャット上で完結する購入体験など、やりたいことはまだまだあります。

まとめ

AnyChatは、チャットシステムを作るという技術的な難しさと、LTV最大化というマーケター・カスタマーサポートと連携して機能を開発していくビジネスの面白さの両方を持っているプロダクトだと思います。 さらにグローバルに展開していく上では、サーバーをどのリージョンに用意するかであったり、国ごとに異なるマーケティング手法への対応も考慮する必要がありそうです。 プロダクトに興味を持っていただけたエンジニア・PMの方はこちらからぜひご応募ください。

https://anymindgroup.com/career/

また、こちらはAnyMindのエンジニア達が書いているブログです。よろしければこちらもご覧ください。

最後に、AnyMindはScalaMatsuri 2022へスポンサーさせて頂いています。当日に弊社バーチャルブースも開設しますので、AnyChat開発含めご質問があればお気軽にお立ち寄りください。(僕は2日目に参加します。)

https://scalamatsuri.org/en/sponsors

Latest News