とうとう今年もアドベントカレンダーの時期がやってきました。体調を崩したのを言い訳に初日は華麗にスルーし、2日目から書き始めたいと思います。
ロバストネス原則
Be conservative in what you do, be liberal in what you accept from others.
これは "Robustness Principle" (ロバストネス原則) という有名な一文で、 Postel's law (ポステルの法則) とも呼ばれます。
この記事では、インターネットの礎を支えたこの思想について、どういう状況で有効なのかを掘り下げます。また、マイクロサービスに対して非常に有効であることや、その応用について書きます。
ロバストネス原則の意味
conservativeは"保守的な", liberalは"寛大な", という意味の語で、和訳すると
"行うことには保守的に、受理するものには寛大に" ということになります。
つまり、あるインターネット上のホストのソフトウェアは、外へ送信するデータに関しては仕様に則って厳密に正しいものを送信し、
外から受け取るデータは、仕様に則っていなかったとしても解釈して動作すべき、ということです。
この一文のの出典は TCPを規定した RFC 793 (1981年) です(それより前のRFCにも何度か登場しているようです)。
また、1989年の RFC 1122 - Requirements for Internet Hosts -- Communication Layers の中でも、ほぼ同じ一文があります。RFC 793の方では一文がRobustness Principleとして単独で書かれていますが、RFC 1122の方には補足説明もあるので、それを要約してみます。参考文献にある和訳を参考にしました。(参考文献にRFCの原文と有志の方による和訳のリンクを載せました。)
- ソフトウェアは、考え得るエラーを、それがどれだけ起こり得なそうであっても、処理できるよう書かれるべきである。
- ネットワークには、起こりうる最悪の影響を及ぼすように設計されたパケットを送信する、悪意を持ったエンティティで満たされている、と想定するべきだ。
- この仮定は、適切に保護された(=堅牢な)設計を導く。
- そうすれば、ただの人的な悪意だけならそこまでひどいことにならない。
- 例えば、列挙値(Enum)として3つの値が(仕様で)定義されているところ、突然仕様にない4つ目の値が送られてきたとしても、それによって壊れてはならない。
ロバストネス原則が重要なケース
上記の中で個人的に面白いと思ったのは、列挙値の例が挙げられていることです。これについて少し掘り下げてみます。
例えば皆さんは普段プログラミングで、列挙可能な値についてswitch文を書いたとき、defaultで規定外の値が来る可能性を想定するでしょうか?
// animalTypeはdog, cat, cowが想定されている。
switch(animalType) {
case AnimalType.DOG: // ...
case AnimalType.CAT: // ...
case AnimalType.COW: // ...
default: // ここを書くか?
}
おそらくJavaではしないでしょう、Enum型によって規定外の値が来ることはあり得ないことが静的に保証されています。
JavaScriptではどうでしょうか。型がないのでJavaのようにはいきませんが、それでも規定外の値が来た時に大真面目に回復を試みるコードは普通書かないのではないでしょうか。switch文にdefaultを書いて、その中に throw new Error(`unknown animalType ${animalType}`)
とするくらいが関の山でしょう。そのようなコードを書いて「これはポステルの法則に反する」などと言う人はいないと思います。
つまり、あるプログラミング言語でのオブジェクトなりがすべき振る舞いと、インターネットのホストがすべき振る舞いの違いは、違いがあると言えます。この違いは、 周辺の環境に置くことの出来る信頼の度合 であると考えられます。
Javaは静的型付け(Enum)により、未定義の値が入ってこないことが確定しています。JavaScriptではそうはいきませんが、少なくとも同じプロセス内で動くコードの中では、基本的にお互いを信用するというのが前提としてあります。
では、インターネットはどうでしょうか?
それぞれのノードは、自分とは全く違う知らないノードとやりとりをしており、各ノードは独立していながらも、インターネットとしての協調動作が求められます。
同じコンピュータ、同じプロセスの中で動くプログラムと比べたら、ネットワークは本質的に不確かなものです。
このジョン・ポステルの一文が現在まで広く使われている理由は、インターネットの例に限らず、分散的な環境一般に対して同じことが言えるからでしょう。結論として、 分散環境では、各ノードに自律性が求められる のです。
実世界で例えるなら、家族と接するときと赤の他人と接する時では振る舞いが変わるという感じでしょうか。家族に対しては依存するのが当たり前ですが、社会で他人と接する場合はそれぞれの個人が当然に自律性を求められるわけです。
ロバストネス原則とマイクロサービス
マイクロサービスと自律性
マイクロサービスは本質的に分散システムです。マイクロサービスを指向するときは、この事実から目を背けることはできません。
つまり、他のサービスは本質的に不確かなものと仮定して、その上でサービスを実装・運用することになります(それにより、例えばシステム障害への耐性を得ることができます)
Sam Newmanの著書「マイクロサービスアーキテクチャ」では、その冒頭で、マイクロサービスを 協調して動作する小規模で自律的なサービス と定義されています。小規模は名前から当然として、各サービスが自律していて、それらが協調動作する、というのは、Postelの言葉が出た背景のインターネットの状況と同じです。
これはマイクロサービスを実際に推し進めていくほど、非常に本質的な定義だと感じます。
Tolerant Reader
Martin Fowlerは彼のサイトblikiの中の稿"Tolerant Reader" (耐性のあるリーダー) で、ロバストネス原則を引用し、これを複数のWebサービスを疎結合にして統治するための掟としています。
TolerantはRobustとほぼ同義で、Readerとはサービスの呼び出し側 (Consumer) のことです。Martin Fowlerは、Consumerが以下のことを守るべきだと言っています。
only take the elements you need, ignore anything you don't.
和訳すると、(Consumerは) 必要なエレメントだけを使い、不要なものは無視せよ となります。
具体例を出します。Providerが、あるWeb APIのレスポンスで以下のようなJSONデータを返すとしましょう:
{
"author": {
"name": "qsona",
"age": 17
},
"title": "Microservices",
"description": "協調動作する小規模で自律したサービス"
}
これを受け取ったConsumerはどうするでしょうか。例えば、素直にレスポンス形式に従って以下のようにクラスを作り、マッピングすることになると思います。(言語は何でもよいです)
class User {
static fromJSON(json) {
const user = new User();
user.name = json.name;
user.age = json.age;
return user;
}
}
class Article {
static fromJSON(json) {
const article = new Article();
article.author = User.fromJSON(json.author);
article.title = json.title;
article.description = json.description;
return article;
}
}
// response bodyとして受け取ったjsonからクラスのインスタンスを生成する
Article.fromJSON(json);
しかし、実はこのConsumerは、その機能の中で、authorのnameとtitleしか使わなかったとします。qsonaが17歳であることには興味がなく、descriptionも使わない。
そうであれば、例えば以下のように、必要最低限のデータだけをマップするべきです。
class Article {
static fromJSON(json) {
const article = new Article();
article.authorName = User.fromJSON(json.author.name);
article.title = json.title;
return article;
}
}
// response bodyとして受け取ったjsonからクラスのインスタンスを生成する
Article.fromJSON(json);
ロバストネス原則の補足の文では、仕様を守らないホストを「悪意のあるホスト」と仮定していましたが、マイクロサービスでは基本的に悪意のあるサービスはいません。では、なぜこの原則を守る必要があるのでしょうか。Providerのうっかりミスや、そもそもConsumerとProviderで想定がずれているというようなことも有り得ますが、それだけではありません。
Martin Fowlerは、サービスは進化するものであり、そのために以下が必要と述べています。
ensure that a provider can make changes to support new demands while causing minimal breakage to their existing clients.
サービスは進化する過程で、機能を変更したくなることがあります。場合によってはAPIに破壊的な変更を加えたくなることもあるでしょう。(それを避けるためにAPIバージョニングという手法はありますが、ここでは一旦差し置きます)
その時に、 無用に 破壊されるクライアントは少ないに越したことはありません。
author.ageやdescriptionがProviderから返されなくなった場合、先のすべてマップしている例ではどうなるでしょうか。たとえプログラム中でそれらが使われていなかったとしても、アプリケーション内で使うインスタンスに値をマップしている時点で、問題になる可能性があります。静的型付けの言語だったら、実行時エラーになるでしょう。動的型付けだったしても、クラスに記述されていては、何かの拍子に使われようとしてエラーになったりしそうです。
Tolerant Readerでも述べられていますが、さらにひどいのは、古いRPC技術を利用していると、いくらプログラミング側で気をつけていてもバインディングの層で問答無用で破壊されることがあり、このようなRPC技術はそもそもすすめられません。(なお、最新のProtocol Buffersを利用したgRPCではこのようなことは起きないので安心して利用できます。この点については別で述べたいと思います。)
Tolerant Readerのプログラミングパターン
外部APIを叩いてそれをプログラミングする際の、実際にTolerantを意識したプログラミングパターンについて簡単に述べます。
まず、Repositoryのような層を作って、外部APIに影響されるレイヤーを隔離すべきです。
そして、Tolerant Readerの思想に基づき必要な値だけをマップします。そうすれば、不要な値はキー名すらプログラム中に登場せず、明らかにプログラム中で使っていないことがわかりやすくなります。
特に、レスポンス中の1つのプリミティブな値しかいらないのであれば、Repositoryのメソッドはその値を単独で返すのが良いです。
class UserAgeRepository
def self.age(user_id)
body = http_client.get('https://hoge.com/path/to/user', { user_id: user_id })
body['age'] # ageというプリミティブな値だけを返している
end
end
上記は単純な例ですが、これで十分Tolerantかというと、そうではないかもしれません。
例えば、ageは数値を想定していますが、数値ではなくオブジェクトで返ってくるようになったら、どうでしょうか。想定外の結果を引き起こしそうです。
次項で議論するように、あまりイレギュラーな状況まで考えるとプログラミングのコストが増大してしまうという問題もあります。まずは不要なキーを使わないことを明示するというところから、始めると良いでしょう。
どこまでロバストネスを指向すべきか
不要なキーをマップしないのは、そうするべきとして、
例えば必要なキーがなくなるなどのケースをどこまで想定してプログラミングすべきかという問題はあります。必要なキーが返ってこなくなってもある程度動作を保証するには、わざわざ動作を想定したり、そのテスト、その場合のロギングなど、明らかに考えることが増えて面倒になります。今までの議論から導くなら、答えは、未来も含めた信用度次第ということになります。
まず、マイクロサービスということで社内のサービスがProviderであることを考えます。信用度には、現在信用できるかと、未来まで仕様が変わらないかの2つがあります。少なくともProviderは現在の状態を信用できるようにするべきです。それはProvider側にAPIドキュメントや型チェックを導入するなどがあります。
一方で未来の状態は誰にも分からないですが、容易に変わることが想定でき対処も容易なら、Tolerantにすべきです。そうでなく、基本的には変わらないものだったり、Tolerantにするのが難しいケースもあります。そのようなケースでは、未来の破壊的変更に備えるため、契約という概念を持ち出すのが有効でしょう。マイクロサービスと契約については明日以降に書きたいと思います。
外部のAPIを叩くときはどうでしょうか。やはり、Googleのように信頼できるAPIを叩くときと、名も知らない野良のAPIを叩くときでは、方針が異なってしかるべきです。
信頼のおける友人に貸したお金は、返ってくることを想定しておいても良いですが、名前も知らない人に貸した金が返ってくる前提で計算するのは危ないでしょう。不確かなものには依存せず、自律することが求められるのです。
ポエム
RFC 793はTCPの仕様について書かれていて、1981年に発行されています。また、RFC 1122はインターネットのシステムの中でホストシステムの実装がどうあるべきかについて書かれており、1989年に発行されています。1989年ですら日本ではまだ一般の人がインターネットを使うことはほぼない、黎明期といって良い時期でしたが、世界的にはすでに広がってきており、実際に輻輳崩壊が起きるなどしていたようです。こういったことを予期していたのでしょう、まさに堅牢(Robust)なインターネットを作るために各ノードが守るべき原則がこのロバストネス原則です。個人的な話ですが、Ilya Grigorik氏の著書「ハイパフォーマンス ブラウザネットワーキング」を読んだことでより一層強い感動を覚えたので、興味がある方にはぜひ読むことをおすすめします。
まとめ
- ロバストネス原則は、本質的に不確定さのある分散環境において、各ノードを自律させ、系として強くするために非常に有効な原則である。
- マイクロサービスは本質的に分散環境なので、この原則を守ることは有効。
- マイクロサービスでは、現在だけでなく未来の不確定さまで考慮し、ConsumerをTolerant Readerにする。
- ConsumerをどこまでTolerantにすべきかは、Providerの(現在・未来における)信頼度による。
- 少なくとも現在を十分に信用できるなら、破壊的変更を検知できるよう、契約の概念を持ち出すことができる。
参考文献
ジョン・ポステル(Wikipedia)
RFC 1122 - Requirements for Internet Hosts - Communication Layers
Requirements for Internet Hosts (RFC 1122の和訳) 1.2.2 頑強さの指針
TolerantReader (Martin Fowler)
O'Reilly Japan - マイクロサービスアーキテクチャ
O'Reilly Japan - ハイパフォーマンス ブラウザネットワーキング