MONTHの10日目の記事です。こんにちは、メルペイのバックエンドエンジニアの柴田(@yoshiki_shibata)です。Engineを使用して、マイクロサービスアーキテクチャを採用した多数のマイクロサービスから構成されています。モノリシックなサービス実装では複数層のライブラリ(あるいはコンポーネント)から構成されるのに対して、マイクロサービスアーキテクチャでは複数層のマイクロサービスから構成されます。Interface)仕様です。モノリシックなサービス実装であれば、それを構成する各ライブラリ(あるいはコンポーネント)のAPI仕様ですし、マイクロサービスアーキテクチャであれば、各マイクロサービスのAPI仕様です。この記事では、マイクロサービス間の通信に用いているgRPCのAPI仕様をどのように記述しているかを紹介します。API仕様に求められる内容どのようなAPI仕様であっても、そこに記述されなければならない事柄は基本的に同じです。programming)の観点から、以下の事柄もきちんと仕様に記述されている必要があります。不正なパラメータ値が渡された場合、どのようなエラーが返される(あるいは例外がスローされる)のか不正な状態や不正な順序で呼び出された場合、どのようなエラーが返される(あるいは例外がスローされる)のかJava言語であれば、これらはすべてJavadoc形式でソースコードに書いていきますし、1.)に対しては、NullPointerException、IllegalArgumentException、IndexOutOfBoundExceptionなどを、2.)に対してはIllegalStateExceptionなどを使います。第3版』に書かれています。メルペイのバックエンドの各マイクロサービスはGo言語を用いて実装されており、マイクロサービス間の通信はgRPCを用いています。Java言語と違って、gRPCを用いたAPI仕様の書き方の標準は存在しません。gRPCを用いたとしても書くべき事柄は同じであり、Javadocに相当する書き方の標準がないので、記述方法を工夫する必要があります。このブログでは、私自身が工夫した記述方法を説明します。.protoファイルに記述gRPCは、RPCの定義を.protoファイルに書いてprotocでコンパイルしてスタブを生成します。Callであり、手続き(procedure)を定義する訳です。呼び出しのパラメータ、呼び出し結果のレスポンスなどを構造体(struct)として定義します。エラーを通知するためにステータスコードを返すことができるようになっています。ステータスコードは、Javaにたとえるとメソッドがスローする例外に相当します。gRPCのAPI仕様の標準的な記述方法はありませんし、ステータスコードを.protoファイルに記述するための構文もありません。担当するマイクロサービスのAPI仕様を策定する際に、マイクロサービスのgRPCの定義(.protoファイル)とAPI仕様が乖離することを避けるために、API仕様はすべて.protoファイルにコメントとして記述することにしました。次のようなサンプルが掲載されています。この例を使って、採用した書き方を説明します。番号(1、2、3、4)は私が説明用につけたものです。1;}serviceの説明1には、定義するサービスの説明を書く必要があります。この例では、サービスにRPCが一つしか定義されていませんが、通常は複数のRPC定義が書かれますので、サービスが提供する機能の概要を書く必要があります。サービスによっては、数行ではなく、10行以上の説明になることもあるかと思います。サービスの説明だけでなく、サービスが提供するすべてのRPCで共通に返すステータスコード(エラーコード)の説明も書くことになります。RPCの説明とエラーの記述2には、RPCの説明が簡潔に一行で書いてあれば十分だと判断しました。RPCの細かな振る舞いやリクエスパラメータにおける事前条件を説明しようとすると、パラメータである構造体やレスポンスである構造体のフィールドの定義がすぐに参照できるように同じ箇所に書かれている方が都合がよいので、2に書くには不適切だと判断しました。私が担当したマイクロサービスではRPCの定義が40個以上あったことも、service定義部分で個々のRPCの説明を書くのが不適切だと判断した理由です*3。3は、RPCに対応したリクエストパラメータの構造体定義に対応する説明部分なので、そのRPCの説明(振る舞い)を書くのはこの部分が適切だと判断しました。以下のことも記述することにしました。許されない値が設定されていた場合に返されるエラーたとえば、上記の例では、実際には何も書かれていません。nameが空でもよいのか、空を許すとしたらそれは何を意味することになるのか、空を許さないとしたら空の場合どのステータスコードが返されるのかが何も書かれていません。RPCを呼び出すために何らかの認証が必要なのかどうかも記述されていません。そのようなエラーの記述は、RPCおよびリクエストの各フィールドの説明と同じ部分に書くのが都合がよいので、3の部分に書くことにしました。不正なパラメータの場合、単純にリクエストのフィールドの値が仕様で要求される形式や取り得る値の範囲を満たさないのであれば、ステータスコードはInvalidArgumentでよいかと思います。なくて、たとえばリクエストのフィールドで指定されたデータがデータベースに無いのであれば、NotFoundかもしれません。どちらであっても、どのような場合に、どのステータスコードが返されるかをきちんと記述する必要があります。gRPCには成功のOKを含めて標準のステータスコードが17個定義されています(Go言語用の定義はこちら)。どのような場合に、どのようなステータスコード(エラーコード)を返すかは、きちんと設計し、かつ、API仕様に明確に記述しなければなりません。3の部分に明確に記述する必要があるということです*4。4のレスポンスについては、その内容を理解するために必要な説明を書く必要があります。定義から自明の場合には、何も書かなくてもよいかもしれません。実際に私が英語で書いた、あるマイクロサービスの.protoファイル(1個のservice、46個のrpc)はAPI仕様も含めて2000行以上あり、企業秘密なのでここで示すことはできません。例として、上記のGreeterを例として書き直したとしてたら次のようになります。1;}UnknwonとInternalAPI仕様には、gRPCで提供するサービスが提供する機能に応じて、どのようなステータスコードが返されるかを記述することが重要だと述べました。UnknownとInternalに関しては注意が必要です。Go言語のgRPC用のミドルウェアでは、statusパッケージを使わずに生成したerrorを返すと、コードとしてUnknownが返されます。データベースへアクセスした結果のerrorをそのままRPCのerrorとして返すと、コードはUnknownとなります。Unknownとは適切にステータスコードが指定されなかったことを意味しますので、API仕様でUnknownを返すことがあるとは決して書かれるべきではありません。返すステータスコードとしてのInternalは、呼び出した側の問題ではなく、呼び出された側の何らかの不変式(invariant)が成立していないときに返します。言い換えると、設計上のバグと考えられるものはInternalを返すことになります。Java言語で言えばAssertionErrorが明示的にスローされるような場合です。ソフトウェアを完璧に作るのは容易でありませんので、設計上の誤りと想定される事象が起きたときはInternalを返すことになりますが、すべてのRPCに共通することなので、サービスの説明に書いておくだけでよいと思います。UnknownやInternalは、サービスインの前にすべて発生しないようにテストを実施していることが理想です。前述のUnknownの例だけでなく、一つのマイクロサービスが依存している他のマイクロサービスが返すエラーをそのまま伝搬して返すと、不適切なエラーとなることが多いです。RPCが提供する機能に対する適切なステータスコードを返すためには、「エラー翻訳」(Javaで言うところの「例外翻訳」*5)を行う必要があります。適切なエラー翻訳を行うためには、RPCが提供する機能に対して概念的に正しいステータスコードを仕様に定義して、マイクロサービスの実装でそのステータスコードへ変換する必要があります。README.mdの自動生成メルカリ/メルペイの.protoファイルは、GitHub上のあるリポジトリで管理されており、.protoファイルが更新されると、自動的にREADME.mdファイルが生成されるように設定されています。コメントとしてAPI仕様を書く場合には、.protoファイルからコメントを抽出して、オンラインドキュメントを生成してくれるツールが求める形式で書くのがよいと思います。ツールとしてはprotoc-gen-docなどがありますので、それを使用するのであれば、ツールに合わせてAPI仕様を書くのがよいかと思います。まとめきちんとしたAPI仕様を書いていない場合、そのAPIのテストコードは正常ケースだけだったり、不正なパラメータが渡されたときにどのように振る舞うかは、実装のソースコードを見ないと分からなかったりします。「エラー翻訳」(「例外翻訳」)を行っていない実装は、いつのまにか返されるエラーが変わってしまっていることも起こり得ます。不備が多いAPI仕様は、不具合が多いAPI実装を生み出し、結果として長期的な開発コストを増加させます。これは、サードパーティへ提供するソフトウェアではなくても、会社内で閉じているソフトウェアでも、長期的な開発コストを増加させる結果となります。(Javadocのような)API仕様記述の業界標準が存在しないからといって、gRPCの定義でAPI仕様をを何も書かなくてもよいわけではありません。何らかの工夫をして書く必要があります。私が担当したマイクロサービスでは、.protoファイルにすべてコメントとして記述することを選択しましたが、よりよい方法が存在するかもしれませんし、今後登場するかもしれません。偶発的プログラミングを行わないこと」*3:SpannerのAPIのようにすべてここに書かれているものも存在します。*4:SpannerのAPIでは残念ながらどのようなステータスコードが返されるかは、書かれていません。採用情報メルカリは常に優秀なソフトウェアエンジニアを募集しています。ご興味を持たれた方はぜひご応募ください。skills月別アーカイブスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。