(本記事は Go Conference 2019 Autumn にて無料配布した冊子『WANTEDLY TECHBOOK GoCon Edition vol.2』からの掲載です)
配布した冊子の前半では Go の導入にあたってどのような工夫をしてきたのかを紹介しました。そこに書かれていたように、新しいプログラミング言語を導入するにはそれなりの整備コストがかかります。それではなぜそこまでして Go を導入したのでしょうか。本記事では Go を導入した背景について説明していきたいと思います。
なぜ Go か
技術的・事業的背景
どのプログラミング言語を採用するかや、どのようなアーキテクチャを選定するかというようなことは非常に影響範囲の大きい決断になるため、会社全体の技術的・事業的なコンテキストと切り離しては語れません。そこでまずは Wantedly の技術的・事業的な背景について、この後の話をするために必要最低限の部分を紹介したいと思います。
Wantedly ではもともと、一つの Ruby on Rails のサーバーで全てのサービスを作っていました。したがって、全てのエンジニアは Rails を読めるし、全てのバックエンドエンジニアは Rails が書ける、という状態でした。そういう環境で、なぜ Go を使うことに意味があったのでしょうか(適切な技術選定であるとは、そこに根拠があるということで、この文章はGoの導入を根拠付けるためのものです)。
アーキテクチャの観点ではモノリシックな Rails サーバーで数年の開発を続けた後、Wantedly Chat、Wantedly People と比較的大きめのサービスをリリースする際に変更を行ってきました。これが Go の導入と関係しているため、話をそこから始めたいと思います。
第一段階のアーキテクチャ変更(マルチサービス化)
この記事の文脈において重要な最初のアーキテクチャ変更は、2015年の Wantedly Chat 開発時に行われました。具体的には、Wantedly Chat というサービスのために専用の Rails サーバーを立てる、という変更です。これにより、複数の Web サービスに対してそれぞれ対応するメインサーバーが存在する、という状況になりました。トリガーとしてはチャットというアプリケーションの特性に由来していますが、同時に一般的な目線として違う Web サービスでかつ違う開発チームなのだからサーバー/コードベースを分けよう、ということがあったと思います。
この変化をどのように捉えるかですが、これはアーキテクチャとしてはマイクロサービスというよりはマルチサービスと呼んだ方が良いものだと思います。なぜなら、一つの Web サービスに対して一つのサーバー(サービス)を用意する、という意味ではサーバーの粒度自体は細かくなっていないためです。単に事業が増えたので横並びにサーバーが増えた、ということです。 サーバーがたくさんある即ちマイクロサービス・アーキテクチャではない 、という風に考える方が、この後の変化との比較をする上で分かりやすくなるため、この変化はマルチサービス化とします。
ちなみにこのときの実装言語はまだどちらも Ruby on Rails でした。インフラレベルではここから2016年にかけて Kubernetes を導入していて、次に紹介する Wantedly People はその上で開発しています。
第二段階のアーキテクチャ変更(マイクロサービス化)
二回目の重要なアーキテクチャ変更は、2016年に Wantedly People という新規事業の開発と同時に行われました。下図は現在の Wantedly People のアーキテクチャ図です。メインサーバーの周辺に数多くの異なる役割を持つサーバーがコンポーネントとして存在し、Wantedly People のバックエンド全体を構成しています。1
ではなぜこのようなアーキテクチャになったのでしょうか。Wantedly People において非常に分かりやすい必要性があって導入されたのは Python のマイクロサービスでしたので、そこから話を始めます。Wantedly Poeple はまず名刺管理のアプリケーションとして開発されたのですが、重要なのは名刺の読み取り機能でした。名刺の読み取りというのは問題として捉えると、入力がカメラで撮った写真で、出力が名前や会社名などに構造化されたプロフィール情報のリストになります。そしてこの問題をどのように解くかということですが、複数のサブタスクに処理を分解します。例えば、1) 写真からの矩形検出, 2) それぞれの矩形からの文字列検出, 3) 検出された文字列のクラス分類 のようなタスクに分解します。そうすると、これらそれぞれは機械学習を使って処理する類のタスクになります。そういったタスクは入力と出力が明確なので、関数というソフトウェア・コンポーネントにすることが可能です。あとで述べますが、ソフトウェア・コンポーネントの単位として妥当であることがマイクロサービスの基本的な条件であるので、これはマイクロサービスの条件を満たしています。そのため、それぞれのタスクを行うサーバーを Python で実装しました。これがマイクロサービス・アーキテクチャにする必要があった大きな理由の一つです。
話を進めましょう。分かりやすくするため、先ほどの図に実装言語を重ねると以下のようになります。アプリケーション全体を見たとき、機械学習タスク以外にもコンポーネント化できるドメインがいくつかありました。そのようなドメインでマイクロサービスにすべきものを主に Go で実装しています。例えば、通知サービス、設定管理サービス、トラッキングリンク発行サービスといった具合です。もちろん、いきなり本番の大事なところに導入した訳ではなく、社内勉強会を行ったり Wantedly Chat の非常に小さいサーバーに使ってみるという草の根の活動が Wantedly People の開発開始以前からここまでにありました(配布した冊子の前半にあたる内容です)。
ただし、実際のところ、このような図に書いた状態はかなりリアーキテクチャを行って整理された後の状態であり、 Wantedly People を約3年間開発する中では成功した Go のサーバーもあれば失敗した Go のサーバーもありました。それらのサーバーを紹介してみたいと思います。
Go で作って成功したサーバー
Go で作って成功したのは、通知サービス(notifications)、設定管理サービス(options)、トラッキングリンク発行サービス(link-tracker)などのサーバー群でした。これらはそれぞれ API も数種類しかなく、API を利用するユーザーが意識すべきデータの種類も片手で数えられる程度のものであり、明確な責務を担いきちんと抽象化されているという点でソフトウェア・コンポーネントとして望ましい性質を持っていました。
先ほど、ソフトウェア・コンポーネントの単位として妥当であることがマイクロサービスの基本的な条件であるという観点を提示しました。この観点に立てばこれらの事実は、Go で作って成功したサーバーはマイクロサービスであった、と言い換えることができるでしょう(ではマイクロサービスでないサーバーを Go で書くのか、という話が同時に生まれます)。
なお、混同されがちですが「マイクロサービス=コードベースが小さい」ではないということを補足しておきます。例えば、通知サービス(notifications)は比較的大量の通知を送信するために Google Cloud Pub/Sub を経由してデータを受け取り、複数のタスクを同時並行処理するといった多少高度な仕組みを備えています。しかしこれらの実装は抽象化によって隠蔽されているため、使う側の視点で言えばシンプルに保たれています。むしろ API が小さくてコードが多いのであればそれは複雑性を抑えながら多くの仕事をしていて価値がある可能性があります。ここでは、きちんと限定された責務を持っているということをもってマイクロサービスと言っています。
より掘り下げた話をすると、Go で作って良かったマイクロサービスには大きく二種類あります。一つはそのマイクロサービスが担うドメインに対応するデータを自分で保持する場合と、自身は何も持たずに他のマイクロサービスの API を叩いてその結果を集約したりさらに次の API に渡したりと言ったアグリゲーションを行う場合です。後者はアグリゲーション・レイヤーとしての利用という用途で捉えていて、このような Go の使い方も Wantedly People 全体を見ると一定数あります。ただいずれにしても、きちんと責務が定義されてインターフェイスによって抽象化されているサーバーであるという意味では利用する側から見て大きな違いはありません。
Go で作って失敗したサーバー
それでは Go で作って成功しなかったサーバーとはどのようなものでしょうか。People の開発開始当初は Go に対する期待もあり、メインサーバーも Go で書こうとしていました(アーキテクチャ図における中央のサーバーです)。ここで言うメインサーバーというものが何を持って他と区別されるかというと、その事業をやっている開発チームのデフォルトの実装対象となるもの、に該当するかどうかです。例えば、ユーザーにメールを送るというような雑多な施策があったとき、妥当なマイクロサービスがなければそれを行うのはメインサーバーの役割になります。
より具体的には、当初 people-go という名前のメインサーバーを作ったのですが、結果として見るとそこにコードは追加されず、デフォルトの実装対象にはなりませんでした。代わりに、一部間借りしていた Wantedly Chat の Rails サーバーに色々追加される形になりました。その後 people-go は責務を限定し card-scanner という名刺読み取りのためのアグリゲーション・サーバーとして転用することができましたが、メインサーバーを Go で書くと言う試み自体は成功しませんでした。
デフォルトの実装対象になれなかった理由は色々あると思いますが、Go に慣れていなかったという一時的な事情はあるにせよ、本質的な理由は動くものを作るまでのリードタイムだったと思います。Wantedly は Rails で長い間開発を行ってきたので、アプリケーション改善の速度は Rails のものが基準となりますが、Go で同じくことをやるのは難しいというのが試した結果の結論でした。例えば Rails サーバーからメールを送るなら ActionMailer を使えば一瞬でできますが、Go ではそこまでの速度は出せません。後により詳しく説明しますが、これは本質的には特定ドメインのライブラリの有無や品質の問題ではないため、今のところメインサーバーは Rails で書くようにしています。
そもそもメインサーバーは必要なのか
ところで、仮にメインサーバーを Go で書くのが難しいとして、メインサーバーなるものは必要なのか、というような話はあると思います。これについては、次のように考えています。
前提として、新規サービスでは施策を次々とリリースする必要があり、またどのようなドメインを扱うかやそこに含まれるモデルといったものも時々刻々と移り変わっていきます。マイクロサービス・アーキテクチャでこの不確実性にどのように対処するのが良いでしょうか。これに対する対処の極端なものとして2つ考えられます。一つは、マイクロサービス・アーキテクチャ全体のレベルで調整する方針です。具体的に言うと、とあるマイクロサービスの責務を拡張したり、新規のマイクロサービスを作ったり、と言ったことを都度行なっていきます。もう一つは、どのようなマイクロサービスにも該当しないと判断した場合のデフォルトの実装対象を決定しておき、迷った場合はそこに実装する。その上で、一つの独立したマイクロサービスとして成立することが十分に確認できた段階や始めからそう確信できる場合にマイクロサービスにする、というような方法です。
現実にはこの中間のところでバランスを取ることになりますが、どのポイントを取るにしろ未来のサービスの仕様がユーザーの反応や外的環境など予測不可能な要因にも依存する以上、ある時点でアーキテクチャが決定できないというようなことは確実にありえます。この時メインサーバーのようなものがないとどんどんと過度にマイクロサービス化をしていくことになり、例えば一つの変更をするのに多くのマイクロサービスに手を入れなければならない、というような事態が起きます。逆に言えば、デフォルトの実装場所というものを用意しておくことでが全体のアーキテクチャの健全性を支えるわけです。
性質 \ 用途 | マイクロサービス | メインサービス |
---|---|---|
責務 | 明確 | 曖昧 |
抽象化 | 必須 | 必須ではない |
期待される動作リードタイム | 長い | 短い |
コードの追加 | 少ない | 多い |
一般的考察と結論
以上の実例を振り返ると、Go で積極的に書くべきところとそうでないところが見えてきます。
まず Go で積極的に書くべきところは「機械学習なら Python」など特定の言語で書くことを強く要求しないマイクロサービスです。一方、例えば単純な CRUD API の作成の容易さは Rails の方に軍配が上がります。これは言ってしまえば Rails の ActiveRecord と ActionController というライブラリのコンビネーションから来ています。一定の制約を置くことで高いベロシティを出すという素晴らしいフレームワーク化の好例です(詳細が気になる方は 『Ruby on Railsの正体と向き合い方 − Rails Developer Meetup 2019』などをご参照ください)。しかし、問題は単にライブラリやフレームワークの有無なのでしょうか。ここには技術選定という観点でもう少し踏み込んで考察すべきポイントがあるように思います。もしライブラリの有無だとしたら、Go でどのようにしてそのようなライブラリ(もしくはフレームワーク)を作るか、ということを考えるのが良さそうです。しかし、もし違いが言語の特性に由来するのであれば、対処は違ったものになります。例えば、異なる言語を組み合わせて使う、というようなアプローチです。なぜなら、ライブラリなどのエコシステムは時間が経つと変わりえますが、プログラミング言語の特性というものは原則としてそこまで変わることがないためです。
言語特性
まず Go でマイクロサービスを書くと良い理由です。まず素朴な理由として Go は静的型付け言語であり、これに言語仕様のシンプルさが加わりコードリーティングやちょっとしたマイクロサービスの挙動変更が他言語をメインとするプログラマーでも比較的容易に行えます。また同様に静的型付け言語であることからメモリ効率や空間効率に優れています。詳しくはコラムで触れていますが、マイクロサービスにするモチベーションとして特定ドメインで高いパフォーマンスが要求されるというようなことはあるため、並行処理機構と相まってマイクロサービスに適しているケースが多くなります。さらに重要なこととして、静的型付け言語で Google エコシステムとの相性も良いことから Protocl Buffers を使ってインターフェイス駆動で開発することが非常にフィットします。Protocl Buffers を使うことで、適切な抽象化が行いやすくなりマイクロサービスとしても適切なものを作りやすい、ということがあります。
次にどうして Go では Rails の代替が出来ないのか、ということの理由です。Go は静的型付け言語であり、Rails は動的型付け言語である Ruby を使っています。よく、静的型付け言語はエラーを検出してくれるので動的型付け言語より良い、という単純化された話が出ます。実際、Wantedly において Go で書いていこうという時期にその大きな理由の一つとしてもそれが挙げられていました。もちろんこれは状況によっては真なのですが、このようなケースにおいては、静的型付け言語と動的型付け言語の特性を考えた上で、達成したいことが何なのかをよく考えた方が良いでしょう。静的型付け言語はコンパイル時に多くの性質を検査します。これによって多くの凡庸なミスから生まれるエラーを検出できます。ただし、それは言葉を変えれば「正しいプログラムでないと走らせることができない」ということを意味します。これは一定の未定義動作に対してもプログラマに定義を要求することを含みます(例えば Haskell のような極端に非常に型の制約が強い言語で開発を行ってみるとこのことは明瞭に理解できるように思います)。動的型付け言語はその逆で、間違っているプログラムであってもとりあえず走らせることができます。従って、仕様がかっちり決まったものに対しては(仕様の定義をコンパイラが要求するので)静的型付け言語はワークしやすいですし、プロトタイピングが重要な役割を果たす開発においては動的型付け言語には一定の価値があります。したがって、「そもそもどのようなものを作るか」が焦点となっており、かつそれを明らかにする方法がプロトタイピングであれば、動的型付け言語の方が適している可能性がある、ということが重要なポイントの一つになります。そして、Rails もこの技術的特性を開発の生産性を高めるのに最大限利用しています。
性質 \ 言語 | Golang | Ruby |
---|---|---|
言語設計 | 静的型付け | 動的型付け |
実行効率 | 高い | 低い |
動作までのリードタイム | 長い | 短い |
動作までに保証できること | 多い | 少ない |
言語仕様 | 簡素 | 豊富 |
フレームワーク特性
もう一つは、Go の Ruby の設計思想の違いと、そこから導かれるフレームワーク特性の違いという話があります。Go はミニマルであることを志向している言語です。そこで、Go でフレームワークを作るとしても抽象化の層は薄く剥がしやすいものとなりますし、使うかわからないような機能がデフォルトで入るというようなことはないでしょう(実際にこのような Go にフィットしたフレームワークとして Wantedly では grapi というものを開発しました、これは次節で話題にします)。一方、Ruby は開発者の楽しさを重視する言語であり、哲学はありますがミニマル志向ではありません。Ruby で支持されている Rails もまた非常に多くの API を提供しますし、HTMLの記述からメールの送信までも行うフルスタックのフレームワークです。なので Rails サーバー一つでアプリケーションを丸ごと作ることができます。こういった特性を持つ Rails だからこそサービス開発に使われてきたわけですが、これはマイクロサービス・アーキテクチャでは諸刃の剣の面があります。まず、よく出来たマイクロサービスは単一の責務を負うもので、マイクロサービス・アーキテクチャはそのようなソフトウェア・コンポーネントの集合からシステムを構成することが必要です。その観点で見ると、Rails というのは何でも簡単に出来すぎてしまいます。端的に言えば「とりあえずここに入れておこう」というのが簡単なのですね(ただ先ほど述べたように、とりあえず入れておけるデフォルトの開発場所を作る、と言うのは一つの妥当な選択です。つまり、例外的にこれはメインサーバーとしては良い性質であることにも注意が必要です)。そして、その簡単にするためのコードは Rails という巨大なソフトウェアに同梱されているため、Rails のバージョンアップなどのメンテナンスコストも大きくなりがちです。Go はこの逆ですね。下図は Go と Rails を組み合わせた場合と、Rails だけでマイクロサービスも作った場合の両方を、インターフェイスの規模感とアプリケーションコードの量と言う観点で示しています。2
以上のような大枠の技術理解があるため、People では Ruby と Go をサーバーサイドの主要プログラミング言語として採用しています。そして Go はプロトタイピングなど漸進的な仕様の試行錯誤が必要は領域よりは、作りたいものが予め明確な領域であるマイクロサービスの実装に多く利用することにしています。
補足:マイクロサービスとして切り出すかどうかの判断
よく、マイクロサービス・アーキテクチャではどのような単位でマイクロサービスにするのか、というような議題が挙がります。すでに一定の言及をしていますが、ここで改めて少しまとめてみます。
まず、マイクロサービス・アーキテクチャというような格好の良い名前がついていたとしても、あくまでソフトウェアであることに注意した方が良いと思っています。例えば、オライリーの「マイクロサービス・アーキテクチャ」では「マイクロサービスはビジネス・ドメインで切る」というような話が出てきますが、これもあくまでマイクロサービス・アーキテクチャの応用形態の一つ、というくらいに捉えて、あまり教条的にならない方が良いと思います。一般的にソフトウェアは複雑なので、独立して理解できる部品に分けるという工夫が成されます。こういった部品―ソフトウェア・コンポーネント―の単位の一つとして、マイクロサービスがあります。普通、ソフトウェア・コンポーネントに切り出すことは、何らかの抽象化を通じて行います。なぜなら、もしそれが抽象ではなく具象、平たく言えば実装として理解されるのであれば、複雑さは減っておらず、ソフトウェア・コンポーネントとして切り出す意味がないからです。
と言うことで、マイクロサービスに切り出すかどうかの判断基準の第一は、それがソフトウェア・コンポーネントとして適切であるか、ということになり、その帰結として抽象として適切であるか、を考えることになります。抽象として適切であるかの一つの目安として、インターフェイスと実装の比率があります。つまり狭いインターフェイスで多くの仕事をするようなものの方が得るものが多い、ということです(例えば、長針と短針を持つあの「時計」という抽象は、その裏で時刻を正確に刻む仕組みを全く理解することなくあのインターフェイスさえ理解していれば遍く利用できるので良い抽象であると言えるでしょう)。どのようなソフトウェア・コンポーネントが良いのか、という観点では "A Phylosophy of Software Design" という本が実践的です。例えば Wantedly には connections というソーシャルグラフを扱うマイクロサービスがあります。この場合、ソーシャルグラフを扱う処理は複雑になることが想定されますが、グラフという抽象化に基づいた API を提供することでインターフェイスはシンプルになるという見通しが立つので、ソフトウェア・コンポーネントとして適切であろう、という風に考えました。これは ”Module should be deeper" という設計原則として紹介されています。
とは言え、単にソフトウェア・コンポーネントにしたいだけであれば「ライブラリに切り出す」という操作もあり得ます。ライブラリを比較対象としたときの、マイクロサービスの特性はいろいろありそうです。思いつくままに挙げていきましょうか。例えば、Web API あるいは gRPC など、言語非依存のプロトコルによって抽象化されているので、そのソフトウェア・コンポーネントを利用する側とその実装の間で異なる技術が使える、ということがあります。これは「技術的異質性」とか言われますね。他にも、例えばサーバー・プロセスの数や CPU などの計算資源がある程度独立であること、があります。ライブラリはあくまで使う側の計算資源に依拠して動作するため、ここは重要な違いです。計算資源として独立であることによって、障害の分離(fault isolation)が可能になります。これに関連して、デプロイ―ソフトウェア・コンポーネントのアップデート―はライブラリの場合は利用する側が明示的に行い、かつ利用する側のアップデートとして行われますが、マイクロサービスの場合はコンポーネントを提供する側が一括して行う形になります。別の言い方をすれば、複数のバージョンが同時に存在し続けるということがありません。これらのことをまとめて、僕はマイクロサービス・アーキテクチャという技術を、プログラミング言語非依存の通信プロトコルを採用した分散システム、と理解しています。後者だけであれば Erlang のような技術がありますし、マイクロサービス・アーキテクチャのうち分散システムであることに由来する課題は当然 Erlang でも存在し、それに対して一定の解決が成されています。なので一つの言語でやらない、と言うのは明記されても良い設計上の選択だと思います。
と言うことで、マイクロサービスに切り出すかどうかの判断基準の第二は、それが分散システムのコンポーネントであるべきか、ということになります。分かりやすいところで言えば、複数の利用者に対して一貫した動作を提供したい場合はマイクロサービスである方が良いですし、耐障害性などの動機もあり得ます。次の紹介する connections というマイクロサービスでは、ソーシャルグラフというサービスの各所から利用されうるものを Wantedly というプラットフォームとして一貫して提供する必要があるため、ライブラリよりはマイクロサービスの方が望ましい選択でした。耐障害性と言う観点でも、ソーシャルグラフが一時的に利用できないケースでは、例えば「知り合いに関連した動作ロジックは作動せず一般的な動作ロジックが採用される」というようなフォールバック動作が考えられるでしょう。
残る技術的異質性についてはどうでしょうか。僕は、これはマイクロサービスの切り出しについての判断とは割と独立した観点だと思っています。と言うのは、もし例えば Rails で書いていたけど Python も使いたい、というようなことだけであれば、モノリシックな Python サーバーを立てれば良いだけだからです。とはいえ技術的異質性はよく話題に上がりますし、実際それが必要になったケースもありました。ここでは詳細を書くことはできませんが、技術的異質性と分散システムを導入するメリットが大きいがソフトウェア・コンポーネントの単位としては完全ではないときにどうすべきか、という課題があるとだけ述べておきます(一応、"Wantedly Tech Book 7" に詳細を掲載しています)。
なぜ grapi か
言語の話をしたので、フレームワークの話にも少し触れておきたいと思います。すでに書いたように Wantedly では Go をマイクロサービスの実装言語として見出して使っています。そのためのフレームワークとして Wantedly ではgrapiというものを作ったので、これの背景に触れたいと思います。
そもそもなぜフレームワークが必要なのかというところですが、分かりやすい課題として次のようなことがありました。grapi導入以前、Goでいくつものマイクロサービスが作られていましたが、実際にどのように実装するかという how の部分はバラバラでした。正確に言えば近い時期に作られたマイクロサービスは似た構成を持っているがどれも少しずつ違う、といったことでしょうか。例えば、Rails で言えば Model-View-Controller という土台となる構成があります。もちろんそこに Service 層を追加する、というような判断はそれぞれでありますが、土台があるのであくまでそこからの拡張として捉えることができます。これがないと、そもそもの書き方が実装者の力量に大きく依存してしまったり、また各々に力量があったとしてもコンセプトが違うのでコードリーディングの際に無駄なオーバーヘッドが生じていました。とは言え、Golang では無駄な抽象化は嫌われます。そこと折り合いをつけつつ、フレームワーク化をすることで全体の生産性を大きく上げたのが grapi でした。
単にフレームワーク化するというだけでは何を作るかは決まりません。ソフトウェアにはコンセプトが必要で、コンセプトを実現する設計上の選択が必要です。ここでは、そういった設計上の選択について、1)開発方法、2)責任範囲、3)設計の雛形の3点を採り上げてみたいと思います。
開発方法:インターフェイス駆動開発
grapi では API を Protocol Buffers で記述します。通常、先に Protocol Buffers で API 定義だけを Pull Request にして出し、それをレビューした後に実装に取り掛かります。これは「インターフェイス駆動開発」と呼ばれたりもしますが、いくつか重要な意味があります。まず、実装と切り離してレビューが行われるので、構造的に実装を上手く抽象化できていない API はレビューで引っかけることができます。Goで書かれるサーバーはマイクロサービスであり、マイクロサービスはAPIを通して抽象化されているべきなので、このチェック機構を強制することには意味があります。
もちろん単にプロセスを縛るだけでは良い開発者体験とは言えず長く続きません。そこで grapi は Go のコード生成を行い実装の助けもになるような機能を統合しています。Ruby on Rails のようなコマンドからのコード生成や、Protocol Buffers からのコード生成です。インターフェイス駆動開発というのは分かっている人は最初から分かっていますが、人によっては開発の考え方を変える必要があるものなので、このようなガイドがあることによって比較的導入が容易になります。
責任範囲:ミニマルスタック
マイクロサービス、つまり責務が明確で少数のインターフェイスを持つサーバーに適切なフレームワークはどのようなものでしょうか。例えば、Rails で言えば ActionMailer のようなメールを送る機構は備えるべきでしょうか?あるいは ActiveRecord のような、リレーショナル・データベースを上手く操作するような、かつモデルと特定のユースケースを密結合させたようなライブラリを同梱するべきでしょうか?この例で言えばどちらも No になります。もう一度、メインサービスと比較した時のマイクロサービスの性質を載せます。
性質 \ 用途 | マイクロサービス | メインサービス |
---|---|---|
責務 | 明確 | 曖昧 |
抽象化 | 必須 | 必須ではない |
期待される動作リードタイム | 長い | 短い |
コードの追加 | 少ない | 多い |
この表から言えることとして、次のようなことがあります。すなわち、責務が明確で少数の API を持ち、コードの追加頻度は少ないようなサーバーで、期待される動作までのリードタイムが長くて良いのであれば、フレームワークの責任範囲はミニマルスタックが良いということです。更に言えば、フレームワークの利用者に提供するインターフェイスも少ない方が良いということがあります。
もし仮にこれを Rails の「フルスタック、インターフェイス多め」で置き換えてみたらどうでしょうか。責務が明確な小さいサーバーなのでフルスタックで提供されているインターフェイスやライブラリのほとんどは使われることはありません。したがって無駄なメンテナンスコストを払うことになります。さらに、提供されるスタックやインターフェイスによって機能追加が高速にできたとしても、その機能追加自体が時々しか行われず逆に利用される方が圧倒的に多いのであればそのメリットは少ないです。むしろ利用することが多いのであれば確実に動いて欲しいです。
このような設計は既存の技術に対して相対的なものであり、技術的なポジショニングであると言えます。つまり grapi は Rails に対して、ミニマルスタック、インターフェイス少なめであるように設計することで価値を上げている、と言えます。
設計の雛形: クリーンアーキテクチャ
Goの用途として、高いパフォーマンスが要求される部分を抽象化してマイクロサービスに切り出すとケースもありますが、grapiにはこのための工夫が少しだけしてあります。それは (これは責任範囲がミニマルスタックであることからも必然なのですが)O/R Mapper のようなライブラリは同梱せずに、サーバーの設計の雛形としてクリーンアーキテクチャを提供するという選択です。
例えば「ユーザー同士のソーシャルグラフを扱うマイクロサービス」のようなものを考えたときには、シンプルなデータ構造だが大量のデータを扱う必要があったり、データの検索がグラフのトラバーサルになったりと、必ずしもリレーショナル・データベースが適切とは限りません。
分かりやすい話、もしシンプルな API の裏で特殊なデータベースが使われていてそれが上手く目的の仕事を果たしてくれるのならそれは嬉しいということです。さらに API として抽象化を行うことは、単に特異なミドルウェアのことを知らなくて良いという使う側の視点だけでなく、データベース選定という設計上の決断を隠蔽することで利用データベースの変更に対してモジュラーに働きます。grapi がリレーショナル・データベースの利用と結合せずにクリーンアーキテクチャを採用しているのは、このような用途を見込んでいるためです。
フレームワーク比較
まとめると、以下のような設計上の選択を行ったのがgrapiです。「言語」とその言語の「用途」をつなぐものとしてフレームワークを設計することで技術のポジショニングをより明確にしています。
性質 \ フレームワーク | grapi | Rails |
---|---|---|
提供する開発手法 | インターフェイス定義が先 | 制約しない |
責任を持つ範囲 | ミニマルスタック | フルスタック |
提供するインターフェース | 少ない | 多い |
メンテナンスコスト | 小さい | 大きい |
データベース | 汎用 | RDB前提 |
ここまで、Ruby と Go を「言語」「用途」「フレームワーク」の三つの観点から比較しました。もちろん、全ての点に置いて相性の良い性質を備えているわけではないですが、あるプログラミング言語や既成のフレームワークといった技術と、異なるユースケースを整理した上で、技術の採用やフレームワークの作成といったことを行うようにしています。それによって、全体として価値を最大化することを意図しています。一つの考え方として技術選定の参考になれば幸いです。
おわりに
Go Conference の本ということでGo言語の周辺にまつわる話題を紹介しました。逆に紹介しなかった内容としては、例えばマイクロサービス・アーキテクチャ全体を適切にマネジメントするためのチーム構成、同アーキテクチャにおける新機能開発の際の開発プロセスなどがあります。
また、この記事ではマイクロサービス・ネイティブなサービス開発においてどのような技術選択が妥当かという話で、モノリスの段階的な解体においては同一言語での実装という制約があることもあるため、もう少し違った根拠づけが必要になると思います。
-
線分はあくまでイメージです。確かに main -> microservice という依存関係を持つことが多いですが、例外もあります。また マイクロサービスの API が直接クライアントの叩く API としての要件をみたいしている場合、main を通さない API call も Wantedly People では許容しています。 ↩
-
余談ですが、このような見方をするとサービスメッシュというものが何なのかについても理解しやすいと思います(プロセスが分かれているが故にアプリケーションサーバーの実装言語に依存しない共通機能の括りだし、というフレームワークよりも更に共通化されたレイヤーとして見ることができます)。 ↩