メルカリWeb開発チームの誕生

杉浦颯太氏(以下、杉浦):よろしくお願いします。本日は「Web Application as a Microservice」というタイトルで、Webについてお話をしようと思います。

まずはじめに自己紹介をさせてください。私は杉浦颯太と申します。メルカリのバックエンドエンジニアとして、後ほど紹介するWeb Re-architectチームでテックリードとして働いております。

個人的な話をすると、夢はJavaScriptになることです。

(会場笑)

何とぞよろしくお願いいたします。

まず最初に「こちらの画面を見たことあるよ」という方は、どれぐらいいらっしゃいますか?

(会場挙手)

半分以上いますかね? ありがとうございます。こちらの画面は何かといいますと、Web版メルカリの画面です。メルカリは始まって以来、基本はiOS/Androidで提供を行っていますが、同じようにWebでも機能提供を行っております。

Web版では出品、購入、検索といった、ひととおりメルカリを使うための機能の提供をしています。リリースしてから2、3年ほど経ちますが、当時から今までのタイミングで少しずつ、2、3回の変化がありました。

まず1つ目の変化としては、メルカリの組織の変化です。基調講演にもあったように、エンジニアの数がどんどん増えて、環境もどんどん変わっています。2つ目は技術的な変化で、Webにまつわる技術がどんどん進化していく、そういう変化がありました。

メルカリWebがその変化についていくために、このWeb版を作り変えるJP Web Re-architectチームが、今年7月から正式に立ち上がりました。現在はフロントエンドエンジニアが4人、SETエンジニアが1人、そして、バックエンドエンジニアが私という構成で、開発を進めています。本日はこのJP Web Re-architectチームの取り組みについて、詳細をお話していきます。

技術進化のスピードが速いWebの世界でついていけるアーキテクチャを目指して

まず最初に、JP Web Re-architectチームのゴールについてお話をします。

1つ目は、変更に強い柔軟なアーキテクチャ、2つ目には開発チームのスケーラビリティの向上があります。

まず1つ目に、変更に強い柔軟なアーキテクチャですが、この「変更に強い」というのはどういう意味を指すでしょうか? みなさんご存知のとおり、Webにまつわる技術の進化というのは恐ろしく早いです。

例えばブラウザまわりでいうと、最近よく聞くワードとして「PWA」をみなさん一度はお聞きしたことがあると思うんですが、出てきてから本当にすぐに流行り始めました。あとは、有名なものではなくても、毎週、毎月、毎日のように、ブラウザにまつわるAPIは、出てきては消え、あるいは採用されてというふうに、日々進化を遂げています。

また、ブラウザにかぎらず、いわゆるフロントエンドという領域においても、とくにnpmというエコシステムのもとに、非常に多くの技術やアプローチがライブラリとして、日々公開されています。このように、非常に速い技術的な進化のスピードに、僕たちのアーキテクチャもどんどんついていけるために変更する、これが1つ目のゴールです。

ここには4つを挙げていますが、これらは、私たちのチームが一番最初の段階で採用する技術です。

現状や現在のチームメンバーとして「これがベストだ」という意思決定によってこの4つを選択していますが、先ほど言ったようにWebの世界は進化が早いので、もしかしたら半年後には……わからないですよ?

もしかしたら「僕たちにはReactではなくてAngularが合っている」「GraphQLよりやっぱりREST APIだ」となるかもしれない。もしそうなった時に「今のアーキテクチャを簡単に捨てるのか?」「マイグレーションするのか?」わからないですが、それらの意思決定の選択肢の幅を広げる、そういったアーキテクチャを目指しています。

多くのエンジニアが在籍する開発チームでスケーラビリティを向上

次に、開発チームのスケーラビリティの向上です。「スケーラビリティとは何を指すのか?」という話ですが、一番最初にメルカリWebがリリースされた時、メルカリ全体のエンジニアの数は、今の半分以下でした。

その当時「Web版にとって何が一番大事だったか?」というと、理想のアーキテクチャを追い求めることではなくて、明日、来週、1時間後、そのタイミングでなるべく早くお客さまに価値や機能を提供する、ということでした。

ただ、先ほども言ったように、メルカリのエンジニア組織はスケールしてきています。とくにフロントエンドエンジニアの数が非常に増えていて、僕が入社した2017年の1月時点ではUSとJPで合わせて4人だけでしたが、現在は16人という体制で開発を行っています。

エンジニアの増加に対して、開発スピードが落ちるようではもったいないので、たくさんいるエンジニアがきちんと協調して開発できるようなアーキテクチャを目指しています。

これが現在のメルカリWebのアーキテクチャです。

このように1つのPHPサーバーを立て、ここから複数の機能提供を行っています。具体的には、先ほど言ったメルカリとしての基本機能である、購入、出品、検索といったものです。2つ目に、メルカリボックスというQ&Aのサイトがあります。そして3つ目は、お客さまに向けてのご案内をするメルカリガイドです。

ここでは3つだけを挙げましたが、他にもいくつかの機能提供を行っていたり、ページによってはWebViewで遷移をするだけでも、このPHPサーバーを使ってデプロイされています。

このアーキテクチャがダメかと言うとそうではなくて、リリース当時ないし今までは非常に良い選択でした。なぜなら、例えばサービスを立てるごとにリリースサイクルを作らなくていいとか、あとはサーバーを新しく設定する必要もないですよね。

新しいサービスを立てたいのであれば、同じ構成のサーバーを1つプッシュしてあげればいいですし、CIなんかのセッティングも1つのリポジトリですむので、非常に節約になりました。

しかし、先ほども言ったように、エンジニア組織がスケールしていく、チームの数が増えていく。ここの図にあるのは、たった3チームですが「100チームになった時にどうなるのか?」を想像した時に、いまのアーキテクチャはベストではないと考えています。

JP Web Re-architectチームが目指すゴール

では「理想のアーキテクチャはどのようなかたちになっているか?」というと、ストリクトなゴールではないのですが、イメージとしてはこのようなかたちになります。

例えば「メルカリのメイン機能は、GraphQLとSSRサーバーを採用しましょう」という意思決定をします。それはリポジトリとしても分離するし、リリースサイクル、CI、テストといった、そういったことをすべて分離します。

一方でメルカリボックスは「SPAがいい」と言うかもしれないし、メルカリガイドに関しては「SPAみたいなリッチな機能はいらないから、シンプルないままでどおりのサーバーを立てましょう」、そういった技術の意思決定をチームごとに自由にできるようにしています。

ここまで、JP Web Re-architectチームが目指しているゴールについてお話しました。では、このゴールを一体どのように達成するのでしょうか?

私たちは現在、先ほどお見せしたPHPの大きなサーバーで1つの巨大なリポジトリを持って、サービスを運用しております。つまり、モノリシックサービスであるメルカリWebを、マイクロサービスということで、このセッションのタイトルのとおりにマイグレーションします。

ここで、もしかしたらこんなことを思った方がいるかもしれませんが、問いかけてみます。この1つの巨大なメルカリWebを、マイクロサービスにいきなり全部移行することは可能でしょうか?

可能か不可能かで言うとおそらく可能なんですけれども、先ほども言ったように、今は1つのモノリシックサービスの中に、少なくともさっきスライドでお見せした3つの機能、プラス、あれと同じぐらいの大きな機能がたくさん乗っかっていて、それらがバックエンドと密結合していて、コードとしても複雑度が増しています。

「では、それを一気にマイクロサービス化できるのか?」というと、短い期間で今の限られたリソースの中で行うのは、少なくともほぼ不可能に近いと言いますか、先が見えない作業になってしまいます。

プロジェクトの目的はリニューアルではなくリアーキテクティング

そこで、僕たちは最初に、モノリシックサービスと4つのマイクロサービスを協調させたかたちで開発を行います。

現在のモノリシックサービスにあるいくつかの機能を、マイクロサービスとして切り出して、最初はモノリシックサービスと協調するかたちにします。そしてゆくゆくは、すべてがマイクロサービスプラットフォーム上で動くようにマイグレーションをします。

図にするとこのようなイメージです。

一番左側が現在のアーキテクチャですね。1つのサービスのなかにすべてのFeatureが乗っかっていて、すべて同じサーバーからホスティングされています。

これを1つずつドメインロジックに基づいた境界で切って、マイクロサービスにしていきます。そして、いずれはスライドの一番右側のように、すべてがマイクロサービス化して協調するかたちが私たちのゴールです。

「なぜこのような手法を取るか?」という話ですが、このプロジェクトの目的はリニューアルではなくて、リアーキテクティングです。ですから、一番最初は画面もドラスティックな変更は行わずに、見た目は同じでも裏側は少しずつ新しくする、というアプローチを取っています。

これを行うことによって、先ほども言った途方もない作業ではなくて、小さいチャレンジを行うことで、小さい成功や失敗というどちらかの体験を積み重ねていきます。そして、いわゆるリアーキテクティングのコツをつかむことによって、どんどんノウハウを貯めてリアーキテクティングを加速させたい、というふうに思っています。

では、ここにおける小さい単位、最小スコープとは何でしょうか? 僕たちは一番最初に、それをページとしてマイグレーションすることにしました。これは「今現在クライアントから来るリクエストが、どのようにさばかれるか?」という図なんですけれども、/jp/配下のすべてのリクエストは、モノリシックサービスにリクエストが来ます。

例えば、ここから「/jp/topをマイグレーションしたい」となった時は、そのリクエストだけをハンドリングして、マイクロサービスへ流します。

これによって、1ページずつ影響範囲を抑えたままで、マイクロサービスへ移行します。このリクエストのハンドリング方法については、後ほど詳しく解説します。

この移行するページの順番の決め方ですが、3つの指標で考えています。

まず1つ目は難易度です。あまりに難しいページから始めると、失敗する可能性が高くなってしまうので、なるべく簡単そうなものから始めます。あとは、改修とのバッティングです。これは同じタイミングで改修を入れると、調整が発生するためですね。

3つ目は、これが意外と重要で「十分なトラフィックがあるか?」ということです。もしぜんぜんアクセスされないページをマイグレーションしてしまうと、その時のインパクトが見づらくなってしまいます。例えば「パフォーマンスが落ちていないか?」「エラーが発生していないか?」「各種コンバージョンやGMVに影響が出てないか?」ということがわかりづらいためです。

JP Web Re-architectがフォーカスするのはマイクロサービス

ここまで、私たちのチームが目指しているゴールと、それを「どのように達成するのか?」というお話をしました。ここからは、このゴールを目指すための技術的な手法について、お話をします。

このゴールを達成することにおいて、僕たちがチャレンジしなくてはいけない課題が3つあります。

まず1つ目は「モノリシックサービスとマイクロサービスをどう共存させるか?」、2つ目に「フロントエンドとバックエンドをどのように分離するか?」、3つ目に「セッションをどのように同期させるか?」。これを1つずつお話していきます。

今日、みなさんがたぶん一番見たい図ではないでしょうか?

具体的に僕たちは最初、このようなアーキテクチャでリリースをしようと考えています。ミドルウェアや、いくつかスライドには載っていない、省略されているものもありますが、JP Web Re-architectチームとしてフォーカスしているのは、これらのマイクロサービスです。

下の灰色が現在動いているモノリシックサービス、青色が4つのマイクロサービス、そして、赤色はCDNですね。このアーキテクチャを用いて、先ほど紹介した3つのチャレンジを行っていきます。順番にお話をしましょう。

まず1つ目のチャレンジとして、マイクロサービスとモノリシックサービスの共存です。この課題を解決するために、私たちはWeb Gatewayと呼ばれるマイクロサービスを実装しています。

このWeb Gatewayの役割は何かというと、マイクロサービスとモノリシックサービスの両方へのバランシングを行います。この初期に関しては、モノリシックサービスとの共存という意味合いが強いですが、将来的にはWebをマイクロサービス化していくために使われていきます。

お客様の気持ちになってサービスを考える

このWeb Gatewayの機能は、主に3つです。まず1つ目にパスによる制御です。

先ほど紹介したように、モノリシックサービスのマイグレーションは、ページ単位で行っていきます。

なので、例えば「/hogeページをマイグレーションしたい」となったら、それはメルカリサービスへハンドリングします。/mogeはメルカリボックスへ、それ以外のリクエストはすべて既存のモノリシックサービスへ、そのように制御するという機能を実装します。

このハンドリングは、このようなコンフィグを書いて実装するイメージですね。

2つ目に公開範囲の制御です。こちらのWIPタグが付いているのは、実はけっこう実現が難しくて、いろいろ揉んでいる最中なのですが、実現したいことをお話します。

この公開範囲の制御で何をしたいかというと、例えば「特定のページをマイクロサービス化しましょう」となった時に、それを100パーセント一気に切り替えると、いろいろリスクがありますよね。例えば「実際に本番のトラフィックを流したら、インフラがスケールできずに死んでしまった」「意図しないエラーがあった」など、何が起こるかが予想できません。

そこで、これをパーセンテージで段階的に公開できるようにします。例えば、一番最初に100パーセントをモノリシックサービスに流していたトラフィックを、10パーセントだけ新しいマイクロサービスに分担しましょう。

この10パーセントを流してる間に、ユーザーのエラーログや各種モニタリングを見て、問題がなければ、すべて100パーセントに切り替える。これによって、より安全に、より低リスクに、マイグレーションを行えるようにします。

最後にSession persistenceです。例えば、AとB、2つのクライアントがあったとします。

クライアントAは先ほどの公開範囲の制御の機能によって、メルカリボックスのほうへ、クライアントBはモノリシックサービスへ振り分けられたとしましょう。

お客さまの気持ちになって考えてみると、画面のデザインが若干変わった時に、この次のリクエストというのは、新しいマイクロサービスではなくて、古いモノリシックサービスのほうへ行ってほしいはずです。

ですから、Web GatewayではHTTPのセッションをクッキーでハンドリングするのであれば、そのクッキーヘッダーを見て、その内容によって「90パーセントのほうに当選してるのか?」「10パーセントのほうに当選してるのか?」というふうに、動的な振り分けを行えるようにします。これもちょっと難しいですが、WIPです。何が難しいか知りたい方は、ぜひAsk the Speakerに来てください。

バックエンドのマイクロサービス化を目指すメルカリ

1つ目のチャレンジをお話しました。2つ目に、フロントエンドとバックエンドの分離です。バックエンドのマイクロサービス化について、本日の基調講演やいろいろなセッションを聞いていただいて感じていると思いますが、メルカリは今、さまざまなバックエンドをマイクロサービス化しようとしています。これはフロントエンドにとってどのような意味があるでしょうか?

これが現在の僕たちのアーキテクチャです。

1つのモノリシックのメルカリWebがあって、そこからメルカリAPIという1つの巨大なバックエンドがあります。このフロントエンド側に、僕たちはNext.jsによるSSRサーバーを新しく立てようとしていますが、ここからメルカリAPIを叩くようになります。これはまだシンプルですよね。

ただ「メルカリWebでオファー機能を実装したいです」となった時は、現在本番で運用されているOffer Serviceを叩く必要があります。

まあまあ、これぐらいならまだいけそうだと。では、先ほど発表にあったListing Serviceの切り出しが終わったらどうでしょう?

今は出品機能がWebで動いているので、当然裏側もマイグレーションする必要があります。

といったように、バックエンドのマイクロサービス化が進んでいくと、この矢印がどんどん増えていくわけですよね。そうなってくると課題になるのは、日々増えていくマイクロサービスです。たった1年で79個になっているので、これの仕様をすべて把握しなければいけません。

Protocol Buffersを見ればわかるという話もありつつも、その裏側にある仕様も把握しなければいけないですし、複数のgRPCサーバーと通信することになるので、そこのパフォーマンスを意識する必要が出てきてしまう、という問題があります。

この2つの問題をフロントエンドエンジニアに意識させないために、僕たちはBackends For Frontendsという考え方を使います。おそらく厳密な定義はなくて、たぶんチームによって解釈が変わってくると思いますが、僕たちにおけるBackends For Frontendsは、複数のバックエンドのマイクロサービスを束ねるための層として使っています。

そして、このBackends For Frontends(BFF)と、それより上のレイヤーというのは、責務を完全に分離します。BFF層より下はバックエンドエンジニアが責任を持って開発するし、技術選定も自由にやります。そして、フロントエンドエンジニアはそのBFFを使うだけなので、フロントエンド側のことに注力してもらって、UXの改善に向かって開発をしてもらう。僕たちはこのような作戦で考えています。

「では、実際にBFFとして何を採用するのか?」という話ですが、僕たちは最初、Next.jsによってSRRプラスSPAを考えていて、SPAと相性の良いであろうGraphQLを採用しています。そして、実装としてApollo Serverを使っています。

セッション同期の常態化を実現するシンプルな構造

最後、3つ目のチャレンジとしてSession Service、セッション同期の問題があります。

このセッション同期問題とは何かというと、一番最初においては、モノリシックサービスとマイクロサービス、いわば2つのサーバーが同じドメイン配下で展開されます。そして将来的には、複数のマイクロサービスが同じドメイン配下に展開されていく未来がある。そうなった時に、その複数のサービス間でログイン状態を同期する必要があります。

簡単に説明すると、例えば未ログインユーザーがいて、モノリシックページのほうにログインしましたと。

そうしたら、このユーザーのステータスとしては、ログイン済みユーザーになるわけですね。

この後に新しいマイクロサービスにアクセスが来たと。URIによって振り分けされるので、こちら側へハンドリングされた時に、レスポンスとしてはログイン済みユーザーに表示するページを返さなければいけません。

「このような同期をどうやって行うか?」というのが、セッション同期の問題ですね。

これを解決するために、私たちはこの責務を負うだけのマイクロサービスを1つ作ります。

このサービスが行うことは、先ほども言ったようなセッションの整合性の担保と、それに紐付くデータの管理等をすべてやって、このマイクロサービスを利用する側からそれらの事項を隠蔽します。

それによって、Webのマイクロサービスがいくつになったとしても、このセッションサービスに問い合わせをすればログイン状態が常に同期される、そのような状態を目指しています。

「では、どのように同期するのか?」という話ですが、おそらく一番シンプルな構造はこれです。

どのような構成になってるかというと、例えばSSRサーバーが「セッション状態がほしい」とリクエストをSession Serviceに飛ばしてきました。このSession Serviceは、今セッションのデータを保持しているモノリシックサービスに、毎回リクエストを投げます。そうすると当然、そのデータが返ってきます。いわばプロキシですよね。これがおそらく一番シンプルな構造です。

ただ、ここに注目してほしいのですが、毎回モノリシックサービスに問い合わせにいくのは、大丈夫でしょうか?

高速かつ永続するキャッシュストレージを使用したSession Service

ここでとある有名な表をみなさんと見たいと思います。

(スライドを指して)これは「エンジニアが知るべきさまざまなレイテンシー」という有名な表で、一番最初はピーター・ノーヴィグさんが提唱したものです。

L1キャッシュのレイテンシーがありますが、ここで1つ取り上げてみます。例えば、同じデータセンター内のラウンドトリップタイムは、0.5ミリセックです。この0.5ミリセックは、お客さまからすると、そこまでダメージはないというか、無視できる範囲ですよね。

ではここでクイズです。このSession Serviceとモノリシックサービス、現在運用しているサービスの物理的な距離は何キロメートルでしょう? 

……「キロメートル」と言ってしまいましたね。まあ、1キロ以上というのは確定してしまったんですけれども。

この距離がどれぐらいかというと、約800キロメートルです。

想像してみてください。「同じデータセンター内だと0.5ミリセックなのが、800キロメートルだと何ミリセックになってしまうか?」という話です。

ということで、北海道と東京の間で通信をしなければなりません。新しいマイクロサービスをホスティングするGKEは東京リージョンを使用していて、現在運用しているサービスは北海道にあります。ですから、この間の距離をきちんと意識した設計を行う必要が出てきます。

そこで、僕たちが考えているSession Serviceの構成は、どのようにするかというと、北海道に一度アクセスした時に、そのデータを手元にキープしておきます。そして高速化のためのキャッシュストレージ、プラス、永続化のためのストレージを使用します。

この永続化のストレージは、GCPから提供されるマネージ側データベースを考えていて、現在はCloud SpannerかCloud Datastoreのどちらかを検討しています。これにより日本を縦断する回数をなるべく抑えていく、という作戦を立てています。

そして、技術スタックとしては、gRPCサーバー、プラス、Node.jsです。gRPCに関しては、マイクロサービスプラットフォームとの相性や通信のオーバーヘッドを考えて選択しています。そしてNode.jsに関しては、メルカリ全体としてはGolangが主流になっていますが、チームとの相性を考えてJavaScriptを選択しました。

「実際どのように北海道から取ってきたセッションを保持するのか?」という簡単な説明をすると、まず最初にSSRサーバーからSession Serviceへ問い合わせが来た時は、手元のキャッシュを見ます。

これが一番早いパターンです。手元にキャッシュがない場合はストレージを見て、そのストレージにもない場合は北海道にアクセスします。

北海道にアクセスすると、おそらくデータがある、ないしは、新しいセッションを作ってくれるので、この得たセッション情報をもとに認証情報の同期をします。

そして、その後にそのデータを手元のストレージに保存して、次回以降のアクセスに関しては、北海道へと毎回通信せずに、手元のストレージを見ます。これによってレイテンシーを改善します。

今は最高のアーキテクチャを実現するための種まきのフェーズ

一番大事に意識していることは「シンプルに保つ」ということです。

たぶん複雑にしようと思えば、めちゃくちゃ複雑にできるのですが、非常に難易度が上がってしまうので、「一番最初は手元にあれば使う、なければ取りにいく」というようなシンプルな構成にしています。

そして、北海道から東京へのアクセスはしません。また、その際に同期上によるセキュリティホールや整合性の問題は発生しうるので、そのあたりはきちんと精査をして穴を埋めていくような感じでやっています。

ここまで、JP Web Re-architectチームが一番最初に実現したい、技術的な詳細をお伝えしてきました。最後に簡単にまとめです。

今は「いわば種まきのフェーズかな」と思っています。赤ちゃんみたいな状態の1ページをホスティングするだけかもしれないけれど、それであってもまずは一番最初の最高のアーキテクチャというものを実現したい。

そして、フロントエンドエンジニアとバックエンドエンジニアがそれぞれの領域で集中して、自分たちの使いたい技術を使って開発できるような環境を作り、まずは開発者の体験を上げていきましょう。

そのために僕たちが今やっているチャレンジとして、これらがあります。

技術スタックに関しては、「今までメルカリがチャレンジしたことのないような技術ばかりを採用している」と言っても過言ではないですし、アーキテクチャもかなりドラスティックな変更になります。そして開発体制自体も、エンジニアのスケールもそうですし、さまざまなチャレンジを行っています。

本当にさまざまなテクニカルなチャレンジをしていますが、すべては最高のメルカリWebのためにやっています。最高のアーキテクチャを作って、最高の体験をお客さまにお届けする、そのために僕たちはこのチャレンジをしています。ご清聴ありがとうございました。

(会場拍手)