ZealsでのMicroservices設計を徹底解説 ~Clean Architectureを添えて~
メリークリスマス!!
2020年の初投稿が Zeals Advent Calendar の23日目のエントリーになってしまったパンディーです!!ZealsではGolangでのMicroservices開発やKubernetesベースのインフラ構築に携わっています。
今年はコロナの影響で各業界で大きな変化がありましたが、Zealsはこの1年で大きく成長することができました。そして自分がZealsに入ってから1年半が経とうとしています。
今回のエントリーでは、当時の2019年後半からスタートした、Microservices開発、特にその設計方法やプラクティスについてご紹介します!
TL;DR;
- Clean Architectureを採用し、Microservicesを変更に強くしよう!
- Microservices設計時に最も重要なのは Entity!!
- Usecaseはサービスの説明書。ドキュメンテーションの重要性
Background
まずは簡単に2019年中頃の状況について振り返り、Microservices化の背景を紹介しましょう。
もともと、Zealsというチャットコマースのサービスは、中核となる2つのアプリケーションによって成り立っていました。
1つがRuby on RailsとReactで開発されたチャットボットの社内管理画面。
メインのユーザーはシナリオ設計担当のメンバーです。彼らがクライアントごとに最適なユーザーストーリーを設定したり、一括配信の設定をしたりするために使われます。
もう一つがPythonで開発されたチャットボットへの送受信サーバーです。
このアプリケーションがLINEやFacebookといったZealsが提供しているプラットフォームのAPIとやり取りを行います。
一見、シンプルに見えますが社内やクライアントからの要望に急ピッチで応えるために、 フレキシブルな配信方法やリッチなメッセージ表現など、様々な機能が追加されていました。当然、これらのドメインロジックは複数のアプリケーションに散らばっており、いくつかの機能は全く同じロジックをPythonとRubyで書いていることもありました。
その状態でさらに開発が進むと、メンテナンスが困難になり、変更にも弱くなってしまうことが目に見えていました。また、当時は配信処理のパフォーマンスはあまり重視されていませんでしたが、大規模なクライアントを想定すると、特定の機能に対するスケーラビリティも高めていく必要がありました。
そういった背景があり、Microservices化のプロジェクトがスタートしました。
How We Should Design Mircroservices?
Microservices化を進めるにあたり、まず始めに考えるべきことは設計原則です。
明確な意図や軸が無い状態で設計すると、なんとなくドメインロジックを共通化しただけのMicroservicesになってしまいます。
Microservicesは様々なアプリケーションからコールされることを想定しなければならないため、下記の要件を設計の軸としました。
- 直感的な Interface
- 内部の実装を自由に変更できる
- Interface が変わらない限り、呼び出し元に影響を与えない
どれも一般的な Microservices の設計原則ですが、これらを常に意識して設計する必要があります。特に2つ目と3つ目は非常に重要です。ここが適切に設計できていれば、下記のDiscordの事例のように『部分的なコンポーネントをパフォーマンス改善のために、言語レベルで作り変える』といったことも可能になります。
そして、当時、上記のようなことを考えていると、 Clean Architecture が自然と連想されました。(きっと同時期に『Clean Architecture 達人に学ぶソフトウェアの構造と設計』を読んでいたからです)
Clean Architecture自体の解説はこのエントリーでは省きますが、設計哲学の根底には
- ソフトウェアを変更に強くする。
- そのために、コンポーネント間の依存関係を 変更しやすいもの から、 変更しにくいもの に依存させる。
という考え方があります。
下記はClean Architectureの提唱者である Robert C. Martin が自身のブログ内に掲載している有名な図ですが、このエントリーでは Entities
と Use Cases
にフォーカスしたいと思います。
Entities
は そのサービスが扱う人やモノや概念、そしてビジネスルールをモデル化 したものUse Cases
はEntities
を使って、 サービスが提供する一連のプロセスをパッケージ したもの
ここで Microservices の設計原則を思い出してください。
『直感的なInterface』は 『DatabaseやProgramming Languageに依存していない純粋なビジネスルール』 と考えると理解しやすくなりませんか?
また、Clean ArchitectureではEntityやUsecaseは 実装に依存しないもの として定義します。そのため、Microservicesの Interface (具体的にはgRPCの Service
) と Usecases を一致させると 『interface が変わらない限り、呼び出し元に影響を与えない』 が実現できます。
当時の私はパズルのピースがピタッとはまった時のような気持ち良さを感じました。
Clean ArchitectureベースでMicroservicesを設計していくと、使いやすく、変更に強くすることができると確信しました。
Practices for Microservices Design
ここからはClean ArchitectureをベースとしたMicroservicesを設計するためのプラクティスを紹介します。
Microservicesの設計においては、Clean ArchitectureのEntityとUsecaseが関係してきます。
特にEntityの設計は非常に重要、かつ難しいテーマです。これをうまく設計できるかどうかで、Microservices全体の使いやすさが大きく変わってしまいます。
What’s Protocol Buffers?
ZealsではgRPCを採用しているため、実際にEntityを定義する際には、Protocol Buffersを使用します。
(既にProtocol Buffersをバリバリ使っている方はこのセクションはスキップして大丈夫です)
簡単に紹介をしておくと、Protocol Buffersはプログラミング言語やプラットフォームに依存しないIDL(Interface Description Language) です。もともとは、(同じくGoogleが開発した)gRPCのためのIDLという印象が強いですが、IDL単体として見ても優れています。個人的には、一度Protocol Buffersに慣れてしまうと、OpenAPI Specification (Swagger) には戻れないと感じています。詳しく知りたい方は yugui さんの素晴らしいエントリーをご覧ください。
Entity Design
まずは、Clean Architectureをベースとしない、基本的なgRPCのスキーマ定義を見てみましょう。
サンプルとしてgRPCの公式ドキュメントが提供している Basics tutorial を紹介します。
下記は利用されている .proto
ファイルの一部です。(全てご覧になりたい方は こちら )
message
と定義されているものは、いわゆるモデルです。
実際に各言語のコードを生成すると、JavaScriptならclass、Golangならstructが生成されます。
// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest integer).
// Latitudes should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
int32 latitude = 1;
int32 longitude = 2;
}// A RouteSummary is received in response to a RecordRoute rpc.
//
// It contains the number of individual points received, the number of
// detected features, and the total distance covered as the cumulative sum of
// the distance between each point.
message RouteSummary {
// The number of points received.
int32 point_count = 1;
// The number of known features passed while traversing the route.
int32 feature_count = 2;
// The distance covered in metres.
int32 distance = 3;
// The duration of the traversal in seconds.
int32 elapsed_time = 4;
}
次に、service
と定義しているのは、messageを使って表現された、RPC(Remote Procedure Call) の Interface です。
// Interface exported by the server.
service RouteGuide {
// A simple RPC.
//
// Obtains the feature at a given position.
//
// A feature with an empty name is returned if there’s no feature at the given
// position.
rpc GetFeature(Point) returns (Feature) {}
// A client-to-server streaming RPC.
//
// Accepts a stream of Points on a route being traversed, returning a
// RouteSummary when traversal is completed.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
}
これだけを見ると、『Clean Architectureのような大げさな設計思想は必要なのか?』と思うかもしれません。
しかし、Clean Architecture の Entity を意識するかどうかで、message の定義は大きく変わってきます。
どういうことでしょうか?
多くの場合、Microservices を導入する際には既にある程度の規模にサービスが成長していると思います。つまり、採用しているプログラミング言語のclassやRDBのテーブルとして、スキーマ定義は既に存在しています。
そうなると、Protocol Buffers で message を定義する際に、特に何も意識していないと既存のスキーマ定義をそのまま message として定義したくなりませんか?
ここで Clean Architecture の Entity
は『特定の技術やプラットフォームに依存すべきではない』という原則を思い出してください。既存のスキーマを流用した場合、この原則に反する Entity が少なからず定義されてしまうでしょう。
実際に、ZealsではメインのデータベースとしてRDB(MySQL)を採用しており、テーブル定義は Ruby on Rails の Active Record によって管理されています。このRubyのclassをそのまま message に流用した場合、 『EntityはRDBに依存してしまう』 ということになります。
より具体的な例としては、RDBの中間テーブルがあります。
例えば、users
テーブルと chatbots
テーブルの多対多のリレーションを表現するために user_chatbot_associations
という中間テーブルがあるとします。これはRDBであれば一般的ですが、Entity 単体として表現すべきではありません。
なぜなら、仮にドキュメントDBだった場合はこのような中間テーブルは作成せずに、『users
ドキュメントに chatbot_ids
のようなフィールドを追加するだけ』という設計も考えられます。要するに、Clean Architectureで言うところの Infrastructure
レイヤー(プログラミング言語やライブラリ、データベースや通信プロトコルなど)の話を Entity
レイヤーに持ち出してしまっている のです。
ただ、このようなデータベース依存については、Entity設計においては解決しやすいトピックだと思います。
Zealsでは、下記のような チャットボット特有のEntity は非常に慎重に設計する必要があります。
- 複数のチャットボットプラットフォーム
- チャット内のUIコンポーネント(メッセージテキストや画像、ユーザー回答を選択するボタン、さらにはレイアウトまで)
Zealsでは、現状ではLINEとFacebookのみでサービスを提供していますが、海外展開も視野に入れた場合、第3、第4のプラットフォームを提供する可能性があります。また、自社ブランドのチャットボットを提供し、本当の意味でプラットフォーマーとなる展開も想定しなければなりません。
当然ですが、各プラットフォームごとにAPI仕様もは異なっており、チャットボットやユーザー情報の属性情報も異なってきます。 それらを適切に抽象化し、拡張性を担保したEntity設計 が要求されます。(そうでなければ、プラットフォームが増えるたびに、if文が増えていく・・・というカオスなコードが想像できますね笑)
また、UIコンポーネントについても、プラットフォームごとに表現力に大きな違いがあります。Facebookはシンプルで汎用的なレイアウトのみを提供していますが、LINEなどはリッチなコンテンツ表現も可能です。特に Flex Message はCSSを書いて、自由にレイアウトを設定できるので、Entityとしてどのように表現すべきか未だに分かりません笑
そもそも、Clean Architecture の Entity は『特定の技術やUIに依存しない』と謳っているにも関わらず、UIコンポーネントを表現しなければならないという・・・
なんという矛盾でしょう!!
冒頭で『Entity設計は非常に重要かつ難しい』と書いたのは、こういった理由が存在します。
おそらくMicroservices開発を行う上で、最も難しいことの1つだと思いますが、同時に、最も面白い領域の1つでもあります。
Usecase Design
さて、ここまではEntity設計について具体例を交えて紹介してきました。
次はいよいよMicroservicesの Interface にあたる、gRPCの `service` を Protocol Buffers で定義することになります。Clean Architectureをベースにした場合、この service
定義がそのまま Usecase
に該当します。
ただ、Entity設計が適切に行えていれば、自ずと Usecase も 特定の技術や実装に依存しないもの になるでしょう。
さて、ここで重要なのはドキュメンテーション、具体的には .proto
ファイル内のコメントです。
RPCのパラメーターや返り値は、基本的にはEntityやそのフィールドになるはずなので、コメントも実装に依存しないものになるべきです。さらに言うと、 実装が全く分からない状態でも、何のために、どのようなタイミングでRPCをコールすべきか表現している必要があります。
例として、『事前定義された条件をもとに、一括配信の対象ユーザーを絞り込むRPC』のコメントを紹介します。
service EndUserService {
// Filter filters a list of end users related with a given chatbot ID by using a given filter ID.
// End users is filtered by the following conditions.
// 1. Whether the attribute ID of the end user matches filter’s one.
// 2. Whether inflow date and time of the end user matches filter’s one.
// 3. Whether the chatbot has permission to send a message to the end user.
// 4. Whether the end users belong to the specified percentiles of CV prediction score.
rpc Filter(FilterRequest) returns (stream e.EndUser) {}
コメントの文言は全て、具体的な実装には触れず、Zealsのサービスドメインやビジネスロジック(Entityで表現していること)を知っていれば、理解できるように 記述しています。これはつまり、 非エンジニアである、セールスやシナリオ設計のメンバーにも伝わること を意味しています。
これが実現できれば、非常に使いやすいMicroservicesのInterfaceになると考えています。
そして、いつか .proto
ファイル自体が『Zealsというサービスの説明書』になる日がやってくることを目指しています。システムが一定以上の規模になってくると、『ドキュメントが実装と乖離する』という問題が出てきますが、コードの一番近くに書いておけばその心配もありません。
Conclusion
このエントリーでは、Clean ArchitectureをベースとしたMicroservicesの設計とプラクティスについて紹介しました。
実際のところ、ZealsのMicroservices開発はまだまだ始まったばかりなので、今後も試行錯誤は続いていくでしょう。(特に、Usecaseの粒度、つまり各RPCに持たせる責務など)
それでもClean Architectureの根底にある『変更に強くする』という軸をぶらさなければ、Zealsの成長を支えるシステムに成長すると信じています。
明日のAdvent Calendar 24日目はZealsのフロントエンドエンジニア Kim が投稿してくれます!皆様お楽しみに!!