この記事はMicroservices Advent Calendarの6日目の記事です。前回は@moomooyaさんのマイクロサービスの技術異質性について検証してみた話。でした。
現状とマイクロサービス化について
今年の8月より株式会社MERYにジョインして、現在はMERYのマイクロサービス化の推進を担当しています。
入社時の時点でおおよそ10個ほどのサービスがECS上で稼働しており、マイクロサービス的な運用はすでに行われていました。ただ裏側では多くのサービスが1つのDBを共有し、ほぼ同じで微妙に異なるモデルが複数のサービスにコピーして置かれているなどのつらさがあり、いわゆる分散されたモノリスに近い状況にありました。
そこで、そのうちの一つのサービス(記事CMS)のリニューアルプロジェクトと合わせて、共有された巨大DBの前に複数のInternal API
を配置し、それぞれのAPIが自分の責務となるロジックを管理できるよう、内部のアーキテクチャを刷新することで、各サービスをより変更に強くしていくことにしました。なお、現在ようやく実装に着手できたくらいのフェーズです。順々にサービスをリニューアルしていきながら、1サービス1DBの形にしていければと思っています。
これまで各サービスは全てRuby on Rails
を使ってRuby
で実装され、サービス間通信はREST
で行なっていました。しかし、新規に作るInternal API
はGo言語
で実装し、サービス間の通信はgRPCで行っています。この記事では、gRPC導入にあたって検討したことについて記述していきます。
なぜgRPCなのか
gRPCには以下のような利点があります。
- HTTP2なので省トラフィックかつ、高速に通信できる
- パラメーターやレスポンスを型付けした状態で扱うことができる
- 定義ファイル(protoファイル)から、protocコマンドで各言語のサーバー/クライアントインターフェースを自動生成できる
後の二つについてはOpenAPI(Swagger)でもある程度近しいことはできます。しかしgRPCの高速なところはマイクロサービスをどんどん増やしていくことを考えると、非常に魅力的であり、この点がポイントとなって採用に至りました(他にも相互streamingが簡単に実現できるところなども長所だと思いますが、今のところ残念ながら使う予定がないです)。
一方でgRPC導入には懸念点がいくつか存在しました。これについても個別に述べていきます。
Application Load Balancer(ALB)が使えない
MERYではAWSを使っており、サービス間の通信はALBを経由して行なっています。ただし、現在のALBはHTTP2での通信に対応していないため(正確にはHTTP2のリクエストを受け取ることはできるが、バックエンドのサーバーにはHTTP1.1でproxyしてしまう)、このまま導入することはできませんでした。
そのため、ECS Service Discoveryを使って対応することにしました。ECS Service Discovery
はDNSラウンドロビンなので、ALBに比べると効率的な負荷分散に少し不安があるかも?と思っていますが、今回リニューアルするサービスは比較的負荷の心配が少ないので、今の所は許容しています。
他の手段としては、NLBを使うか、NGINX(少し前にgRPCに対応した)を使うなども考えられます。
ローカル開発時に動作確認したいときにどうするか
RESTであれば、curlで適当にリクエストを作って、動作確認したりすると思います。しかしgRPCは当然非対応なので、curlは使えません。grpc-gateway
でRESTのproxyサーバーを立ててもいいのですが、開発用途のためだけに使うのは面倒です。
ただ、すでにいくつかのgRPC用のクライアントが存在します。
あと、簡易なクライアントを自作する手もあると思います(protocでほとんど生成できるから、サクッとできそう)。
言語間の対応状況がバラバラ
gRPCは公式で、C / C++ / Ruby / Python / PHP / C# / Objective-C / Java / Go / NodeJS / WebJS / Dartの生成に対応しています。WebJSのプロジェクトであるgRPC-Webが、10月にv1になり、話題になりました。
ただ、gRPC-Web
を使うにはEnvoyを中間に立てる必要があります。実際の運用を考えると、RESTのBFF(Backends For Frontend)を中間に立てる方が、アグリゲーションなどもできてメリットがありそう・・・と感じてしまいます。少し敷居が高いと思います。
生成に対応しているとはいっても、各言語でのgRPCの実装状況はまちまちであり、使える機能には差があります。例えば、GoとJavaにはClient Load Balancingの機能が実装されていますが、Rubyにはありません。複数の言語間でやりとりできることがgRPCの魅力の一つではあるものの、現状ではGoやJavaなどのgRPCでよく使われていそうな言語の方がやっぱり安心なのかなーとも感じています(ちなみに今回リニューアルするサービスではBFFとしてRails、internal APIとしてGoを使うので、RubyとGoの間でgRPCを行います。クックパッドなどRubyでgRPCを使っているところもあるので、複雑なことをしなければ問題はないはずです)。
protoファイルをどこに置くべきか?
各サービスはそれぞれ自分のGitリポジトリを持っています。ではprotoファイルはどこに置くべきなんでしょうか?少なくとも、最新のprotoファイルか、もしくはその生成コードを全サービスで共有できるようにしておく必要があります。そうしないと、サーバー側とクライアント側での不整合は避けるのが難しくなってきてしまいます。利用するサービスが2-3個であれば、事故は起きにくいかもしれませんが、十数個のサービスが利用するAPIができた場合、適切な管理方法がなければ、漏れは確実に発生すると考えなくてはなりません。
いくつか方法が考えられます。
- 案1:proto管理リポジトリで全てのprotoファイルを管理する
- git submoduleを使って各サービスは更新があったらpullする
- メルカリはこれ: https://speakerdeck.com/kazegusuri/grpc-and-rest-with-grpc-in-practice?slide=65
- 案2: サーバーが自分のprotoファイルを管理し、使う側で必要なものだけ自動収集する
- protodepsというツールがある
- https://github.com/stormcat24/protodep
- 案3: サーバーが自分のprotoファイルを管理し、更新時はクライアント各リポジトリに一括してpush/pull requestする
- サーバーが全クライアントを把握する必要があり、いまいち
- 案4: サーバーが自分のprotoファイルを管理するが、クライアントはCIで自分が保持しているprotoが最新かどうか検査する
- 自分が使っているprotoが最新かどうかチェックするツールを作って、CIで実行する
パターン2かな・・・と思っていますが、サービスが増えて複雑になりそうなタイミングで再度検討するつもりです。後、今思ったけど、案4は案1/2と併用できますね。そこまでやるかは別として。
protoファイル記述時の命名ルール
REST APIの仕様を定義する際には、一定の命名規則に沿って行なっています。
gRPCの時も何かしらルールを定めたい(できれば誰かが考えてくれたスタンダード的なやつに従いたい・・・)と思ってます。そうでないとレビューの判断基準がまちまちになってしまいます。
ひとまず現在はgoogleの命名規則に沿って行なっていますが、RESTと結構違うのでいいのかなあと思ってます。押し切ってしまったけど、よかったのかな・・・まあいいか。ちなみにこの設計ガイドは、全体的にとても参考になったので、これからprotoファイルを初めて書く方は、先に目を通しておくとよいと思います。日本語で読めるし、おすすめです。
おわりに
所属先のマイクロサービスへの取り組みと、gRPC導入にあたって検討したことをまとめました。
gRPCは高速で堅牢かつ生産性が高いので、マイクロサービス化を進めるにあたり、有力な選択肢になり得ると思います。ただ、導入にあたっては新しく検討が必要なことも多くあり、それぞれ適切に対応していく必要があります。
次回は12/9に投稿される@nakabonneさんです。よろしくお願いします!