こんにちは、qsonaです。マイクロサービス愛好者(?)です。活動としては、Microservices Meetup というイベントを、所属している株式会社FiNCにて運営しています。
初日ということで、「マイクロサービスとは何なのか」と風呂敷を広げたテーマで書いてみたいと思います。
幸いなことに、原典に近い文章を、日本語に翻訳していただいている記事があります。
Microservices / James Lewis, Martin Fowler
@kimito_k さんによる翻訳
ある程度マイクロサービスでの開発を経験した後で読むと、この記事は驚くほど網羅性があると感じます。たまに読み返すと毎度新しい発見があります。
さて、マイクロサービスを初めて知るような人のために、あえてマイクロサービスを「一言」で表すなら、自分の考えは以下です。
マイクロサービス = 「超」疎結合
自分がマイクロサービスの話をもう少し噛み砕いてする時などは、そこを出発点にして話すようにしています。
意外と疎結合の概念をちゃんと説明出来る・応用できるというのは、簡単なことではなく、おろそかになりがちであることもあります。
マイクロサービスと疎結合
疎結合とは
一言で言うと
疎結合とは、細分化された個々のコンポーネント同士の結びつきが比較的緩やかで、独立性が強い状態のことである。
(出典: IT用語辞典[疎結合])
ここでいう「コンポーネント」というのは、何かのまとまりであれば何でも良く、種類も大きさもまちまちです。
密結合と疎結合の例
ここでは、 パズル をやることで クエスト を進めていくようなゲームを想定して、
「パズルを完了した時に、クエストを進める」というものを考えてみます。
パズルを完了するときに、以下を行います:
- クエストに付与するポイントを計算する。
- 今進めているクエストにポイントを付与する。
これを疑似コードで説明してみます。
class Puzzle {
// パズル終了!!
finish() {
// パズル自体の終了処理...
// 現在進行中のクエストを取得する
var currentQuest = Quest.getCurrent();
// このパズルから付与するポイントを計算する
var point = this.calcQuestPoint();
// クエストにポイントを付与する
currentQuest.addPoint(point);
}
calcQuestPoint() {
// パズルの進捗状況から計算する...
return this.xxx + this.yyy * this.zzz;
}
}
class Quest {
static getCurrent() {
// いま進めているクエストを取得
return new Quest();
}
addPoint(point) {
this.point += point;
// 一定ポイントたまったらクエスト完了とする
if (point >= 100) {
this.complete();
}
}
}
ここでは PuzzleクラスとQuestクラスが「コンポーネント」に値します。これは疎結合な状態でしょうか?
いいえ、違います。上のコード例は、次のような特徴があります。
PuzzleがQuestのことを多く知っている。
これだけの例を見ても、Puzzleコンポーネントは少なくとも、Questについて次のような特徴を知っています。
- 現在進行中のクエストは1つである。
- クエストはaddPointというメソッドを持っている。
- つまり、クエストはポイント制である。
- パズル完了時にクエストに対して与えるポイントの、計算方法。
そのため、例えばQuestに次のような変更をしたくなったら、Puzzleに手を加えなければならないでしょう。
- 複数のクエストを同時に進行できるようにする。
- ポイント制以外のクエストもある。
- パズル完了時の、クエストに与えるポイントの計算方法を変える。
ここでは良し悪しは論じません(2日目以降に・・!)。ただ単に、そうなっているというだけです。
では、これを疎結合にするにはどうしたら良いのでしょうか?
1つの方法が、 イベント駆動 にすることです。
Puzzle完了時に、「Puzzle完了したよ」というイベントを発火し、処理は全てQuestに任せる、という方法です。こうすれば、Puzzleは一切Questのことを知らなくて良くなります。
class Puzzle {
// パズル終了!!
finish() {
// パズル自体の終了処理...
// あとのことはPuzzleは一切知らない
Event.emit('finish puzzle', this);
}
}
// ---- ここまでにQuestに関する記述が一切ない ----
// ---- ここからQuest ----
class Quest {
static getCurrent() {
// いま進めているクエストを取得
return new Quest();
}
static calcQuestPointByPuzzle(puzzle) {
// パズルの進捗状況から計算する...
return puzzle.xxx + puzzle.yyy * puzzle.zzz;
}
addPoint(point) {
this.point += point;
// 一定ポイントたまったらクエスト完了とする
if (point >= 100) {
this.complete();
}
}
}
// パズル完了イベントを受け取る
Event.on('finish puzzle', (puzzle) => {
var currentQuest = Quest.getCurrent();
// クエストポイントを付与する
currentQuest.addPoint(this.calcQuestPoint());
});
この例ではQuestはPuzzleのことをガッツリ知っています。その依存関係を抽象化するような話もありますが、マイクロサービスのコンテキストからは少しずれるため、これで一旦置いておきます。
繰り返しですが、ここでは良し悪しを論じているわけではありません。2日目以降、マイクロサービスの良さや難しさを説明するのにこの例を使います。
疎結合のメリット
ITProさんの記事から引用します。(2005年とやや古めですが、概念として特段変わるものではないですね。)
まず一つ目が,再利用性の向上。同じ機能を必要とする他のソフトウェアを開発する際に,部品をそのまま使い回せる。一から開発する必要がなくなり,ソフトウェア開発の効率が向上する。
二つ目は,保守性が上がること。システムが適切な単位に分割されていれば,不具合があった場合に問題の個所を特定しやすい。さらにその部分に変更を加えても,影響を受けるのは部品内に限定できる。検証作業の手間が軽減される。
そして三つ目は,システムを柔軟に変更できるようになること。部品を少し拡張したり,そっくり別のものに入れ替えるだけで,機能拡張や仕様の変更に対処できる可能性が出てくる。
PuzzleとQuestの例では、2番目と3番目の恩恵を受けられるでしょう。つまり、
Questに関する修正はQuest内に閉じ、Puzzleに影響が出ることがない
ということが挙げられます。
マイクロサービスを疎結合にするための1つの要件
マイクロサービスで明確に気をつけるべき点が1つあって、
全てのサービス間連携はサービスインターフェイスを通じて行われるべきである
ということです。Amazonではこれを破ると解雇されるそうですね:)
サービスインターフェイスの一例を挙げると、例えばWeb標準であるHTTP通信を用いたJSONでのAPIです。ここが標準的であればそれだけ、サービスごとにどんな技術を使うかの選択が広がるので、疎結合度が高まる、と言えると思います。
マイクロサービスにおける疎結合
初めに、マイクロサービスを一言で表すと、超疎結合であると書きました。疎結合はコンポーネントの独立をもたらすのですが、極めて疎な状態にすることで、具体的にどんな恩恵が得られるでしょうか?
技術選択
サービスインターフェイスを満たせさえすればどんな言語やDBを使っても構わないので、基本的にはWebサーバ実装がある言語や、そこからアダプタがあるDBであれば、サービスの事情に合わせて何でも自由に選択できます。仮に同じスタックを使っていても、バージョンアップが容易だったりするのは嬉しいですね。
デプロイを独立してできる
疎結合がもたらす物理的な恩恵の一つです。
組織・意思決定の独立
サービスごとの独立した意思決定を可能にします。初めに紹介した原典でも、「コンウェイの法則」について語られています。上記のデプロイが独立にできることとも関連します。
障害耐性
1つのモノリシックなサービスの場合、どこかのコードでバグが出ていると、コード的にも全ての箇所で影響を受ける可能性があり、また物理的にもサービスごと落ちて使えなくなってしまう危険があります。
マイクロサービス化していると、他サービスが落ちた場合に、そもそも関連のないサービスには影響が及びませんし、適切にエラー処理をしていれば影響を最小限にすることができます。
これをきちんとやりきろうとすると難しい面もあるのですが、普段から「他サービスが落ちている時の影響を最低限にする」方に倒しながらプログラミングすると、ある程度までは自然と達成できます。
同じサービス内で境界が曖昧な状態でやると、ともすればただのエラーの握りつぶしになりがちなところです。全く別の物に分かれているからこそ、握りつぶしではなく正しくハンドリングする、と捉えることができますね。
再利用性
認証や課金など、共通基盤的なサービスが主に該当します。アプリケーション内のチャットや通知機能などアプリケーションの共通機能として使われやすいというのもありますね。
モジュール分割と違い、マイクロサービスは使いたい時にすでに存在しているので、組み込まずにそのまま使える、というのも考慮すべき点です。
(ただ、実はこれは早い段階から強く求めるべきではないことが多いと思っています)
まとめ
上記のように、サービス間が極めて疎結合な状態になることによって様々な恩恵があります。
逆にこれらは正しく疎結合化できていないと、部分的あるいは全く受けられない恩恵になってしまいます。一例が、サービスインターフェイスを通さずに他サービスのDBを直接参照してしまうことです。これをすると、上記のいくつかには制約が生まれてしまうことがわかると思います。
最後に
一時期マイクロサービスはややバズワード化した影響の反動もあり、最近では、マイクロサービスの難しさ、あるいは否定的な話も多くなってきました。
結局、マイクロサービスをやるためには、今回取り上げた疎結合をはじめとしたプログラミング設計を、普通以上によく理解する必要があります。
「マイクロサービスが辛い」という言説を見かけたら、あるいは言いたくなったら、自分は以下のようなことをまず考えてみるようにしています。
- それは「疎結合」に由来する一般的な問題ではないか。
- 疎結合に関する、一般的な解決法に帰着できないか。
マイクロサービス自体はまだ生まれて日が浅いですが、疎結合という概念には歴史があり、いろいろな説明や手法がありますから、まずはその知見を利用するべき、という考えです。
エキスキューズのようになることを承知で書くと、筆者の理解も未熟です。例えば疎結合を満たすための手法としてObserver Patternやイベント駆動、PubSubモデル、 CQRS(コマンド/クエリ分離原則)などがあると思いますが、筆者はこれらの違いをうまく説明できません。記事中にも理解の未熟さが表れている箇所があると思います(遠慮なくご指摘ください)。
次回以降では、この考え方を基にもうすこし具体的なマイクロサービスにおける例を考察したいと思います。