爆速でGo!

GoGo!

2019年は「柔よく剛を制す」

2019年ももう12分の1が終わってしまったので、ひっそりとメモ程度に記す。
北風と太陽で言えば、去年はほとんど北風だったので今年は太陽を取り入れていくという話。
タイトルの通り今年のテーマは「柔よく剛を制す」と決めているが、この"柔"には大きく3つの意味がある。

  • 柔軟に頭を切り替える
  • 世界としなやかに繋がる
  • 遊び心を取り入れる

柔軟に頭を切り替える

幼少期からとにかく脳が不器用で、一つに集中するとそれしか考えられない。豊かな人生を送るためにもコンテキストスイッチのオーバーヘッドを減らしたい。

世界としなやかに繋がる

昨年は英語ブログを開始したり、クラウドネイティブ系のOSSの議論に参加したりと、海外と繋がる場面が増えてきた。

英語

ネイティブと難なくコミュニケーションを取れるレベルまでいきたい。
この国は言語の壁によって孤立しがちだが、一歩外に出ると無限の世界が広がっていることを知った。
日本で働いていても常に世界の一次情報と繋がっていたいし、世界のトップデベロッパー達と現地で議論したい。

OSS

オープンな技術の最前線にいると熱狂の毎日を送れることはよく分かった。
創作意欲が刺激されてる時が最も興奮するのは間違いないので、学生最後の1年間を思いっきりつぎ込む。

遊び心を取り入れる

必要以上に自分を取り締まりすぎる傾向があり、どんどん固く細かい人間に近づいている。地元の友だちからも固いと言われてしまった。
このままだと自分が作った小さい世界に閉じ込めてられてしまう気がするので、肩の力を抜き、生活に遊び心を取り入れる。

さいごに

一番好きな一節を以下に記す。

あふれる情熱から、人が何ら報酬を受けることなく、ただでも喜んで追求するようなことを職業とする特権を、神はほんの一握りの人間にしか与えてくれないものだ。

「人月の神話 p286」 - Frederick Phillips Brooks, Jr 著 - 滝沢徹・牧野祐子・富澤昇 訳

この情熱を絶やさないためにも、もっと燃やすためにも、柔らかく生きていく。

モジュールの深さについて

Click here for English version

この記事は、スタンフォード大学John Ousterhout教授の著書「A Philosophy of Software Design」にて説明されているdeep moduleという概念について、本人の許可を得てまとめたものです。ただし本書の内容を詳しく紹介するのではなく、主に私の考察を書き綴ったものです。

最初に私の考える良いモジュールを定義付け、次にそれを実現するdeep moduleという概念について説明します。

良いモジュールとは

良いモジュールとはなんでしょうか。ここでいうモジュールというのは、クラス・構造体やサブシステム、さらにマイクロサービス等の分化されたソフトウェアを指し、言語は問いません。

小さいモジュール?

「Small is beautiful」というUnix哲学があるように、小さいことは価値になります。モジュールを小さく分割することでモジュールの仕事が明確になりやすく、結果的に保守が容易になります。
確かにそれは大切なことですが、そもそもなんのためにモジュールを作るのでしょうか?
主な理由の一つとして、関係性の高い要素が集まることで実装の複雑さを下げるというものが挙げられます。それを踏まえた上でこのコードを見て下さい。
Javaでファイルを開く際、以下のように3つの小さなクラスが必要です。

gist.github.com

FileInputStreamは初歩的なI/Oのみを提供し、BufferedInputStreamは、FileInputStreamで得られたバイト入力ストリームをバッファリング読み込み付きのバイト入力ストリームに変換します。 そしてObjectInputStreamシリアライズされたオブジェクトを操作する機能を提供します。
見ての通りファイルを開くためだけに3つもオブジェクトを生成しなければなりません。クラスを小さくしたことで、各クラスの仕事は明確になりました。しかし関連性の高いFileInputStreamとBufferedInputStreamが独立してしまっていることで、もしユーザーがBufferedInputStreamを作り忘れた場合はバッファリングされずに遅くなってしまいます。つまり実装の複雑さが外部に漏れてしまっているということです。
実装の複雑さが漏れることが必ずしも悪いとは限りません。ただ以下のことは認識しておくべきです。

  • モジュールが小さいということは、持っている情報量が少ないということ
  • 持っている情報量が少ないと、実装の複雑さが外部に漏れやすい
  • 実装の複雑さが漏れていると、モジュールの実装変更が周りのモジュールに影響を与えやすくなる

Small is beautifulであることは間違いないのですが、関連性の高い要素を分割してしまうと保守性が下がってしまうことが分かりました。
なので、小ささを意識しすぎると良いモジュールは作れないことが分かります。

高凝集?疎結合

関連性の高い要素が集まり(高凝集)、それぞれが依存し合わない(疎結合)ことは非常に大切です。上記コードは低凝集・密結合の良い例です。
関連性の高いたくさんの要素を隠し持つメリットはたくさんあります。
"関連性の高い"要素を持つことで設計が明確になり、保守と拡張が容易になり、再利用性が高まります。
また、"たくさんの"要素をもつことでモジュールの機能が強力になります。
さらに、"隠し"持つことで疎結合化も促進することが出来ます。
よって、高凝集というのは非常に大事な概念だということが分かります。

また、モジュールの疎結合化がソフトウェアに良い影響を与えるのは言うまでもないでしょう。ただ疎結合を保つためには、モジュールを繋ぐインターフェースを良いものにする必要があります。

良いインターフェースとは

「インターフェースはシンプルに保て」とよく言われますが、このシンプルという言葉は曖昧なので噛み砕いていきます。
まずはモジュールのインターフェースに何が求められているのかを理解する必要があります。
モジュールが他のモジュールを利用するためには、インターフェースという名の契約を結びます。各モジュールは契約を前提に実装されるので、契約が変わるとその契約に関わっている全てのモジュールの実装を変更しなければなりません。しかしソフトウェアというものは、ほとんどの場合変更されます。
この矛盾の中でも崩れないことがインターフェースには求められています。すなわち良いインターフェースとは、不変(追加以外の変更を許さない)なインターフェースだと考えています。
そして可能な限り、インターフェースを利用するユーザーにとって使いやすくあるべきです。
しかしこの「使いやすさ」に関しての私の考えは、Clojure作者であるRich HickeySimplicity Mattersで述べていた考え方に影響を受けています。使いやすさというのは相対的な概念で、何が便利なのかはユーザーによると考えています。
ユーザーをユースケースの観点で大きく2つに分けると、エッジケースで利用するユーザーと一般的なケースで利用するユーザーに分かれます。
インターフェースが複雑になってしまう主な理由として、エッジケースを許容してしまうということが挙げられます。そのため、一般的なユースケースに絞って使いやすさを追求するべきです。
先程のJavaのコードをもう一度見て下さい。ファイル操作時にはバッファリングするのが一般的なケースのはずです。にも関わらずBufferedInputStreamクラスを独立させることで、バッファリングしないというエッジケースを許容していました。そのためインターフェースが複雑化してしまったということです。 改めて良いインターフェースをまとめると、「一般的なケースで使いやすく不変なインターフェース」と考えることが出来ます。

結論

上記を踏まえて、私の考える良いモジュールの定義は「関連性の高いたくさんの要素を隠し持ち、一般的なケースで使いやすく不変なインターフェースによって強力な機能を提供するもの」です。
次章から、それを実現するdeep moduleという概念について説明します。

deep module

たくさんの要素を隠し持つためには、ある程度実装は複雑になります。しかし良いモジュールにするためには、実装の複雑さを感じさせないインターフェースで強力な機能を提供しなければなりません。そんなモジュールのことを、John Ousterhout教授はdeep moduleと呼んでいます。本書では以下のように書かれています。

"they allow a lot of functionality to be accessed through a simple interface."
(John Ousterhout, A Philosophy of Software Design.)

f:id:NakaWatch:20190113164855p:plain
deep and shallow modules

上図は本書に記載されている図を参考に作成したものです。
長方形が一つのモジュールを表しています。上端の横の長さがインターフェースの複雑さを表していて、縦の長さが機能の強力さ(モジュールがもたらす利益)を表しています。横に長ければ長いほどインターフェースが複雑で、縦に長ければ長いほど機能が強力だということです。John Ousterhout教授は左の縦長な長方形のようなモジュールをdeepと表現しており、最も良いと考えています。
つまり、モジュール設計の際には以下の視点が必要だということです。

  • 機能は強力だけど、インターフェースも負けないくらい複雑になっていないか?
  • 逆にインターフェースは簡単だけど、機能も負けないくらい貧弱ではないか?

インターフェースの複雑さというのはコストで、機能の強力さというのは利益です。利益がコストを上回っていれば、それはdeep moduleといえます。 ただこの見極めが難しいので、良い例をたくさん見ることが大切です。

例:ファイルI/O

deep moduleを体現している例として、Unix系OSのファイルI/Oに関わるシステムコールが挙げられます。
標準的ファイルI/Oモデルから外れなければ、使うシステムコールは基本的に以下5つのみです。

  • open()
  • read()
  • write()
  • close()
  • lseek()

Unix I/Oの実装は長年に渡って進化し続けているのにも関わらず、この5つのシステムコールは変わっていません。これは理想的なdeep moduleといえます。
カーネルは本来ユーザアプリケーションを安定して動作せるために設計されているため、インターフェースがほとんど変わりません。そのため、カーネル周辺のインターフェースをじっくりと眺めてみると参考になると思います。

まとめ

モジュール設計者は、たくさんの視点を持ちながら設計することを求められます(e.g. 凝集度、結合度、可逆性、直交性...etc)。
しかし、抽象的な世界をコードに落とし込む上に常に変わり続ける「ソフトウェア」というものに、全ての視点を取り入れるのは困難だということは誰もが感じているはずです。
そこで、「このモジュールは深いか?」というたった一つの問いを投げかけてみてはいかがでしょうか? John Ousterhout教授の素晴らしい概念であるdeep moduleについて、一人でも多くの方が理解して頂けたら幸いです。
また、興味を持った方は本書を購入することを強くオススメします。

余談ですが、deep moduleについて知った時、以下ツイートのようなことを考えました。これについては別エントリでまとめるかもしれません。

*間違いやご意見等ありましたら、Twitter等で連絡して頂けると大変助かります。

2018年良かった〇〇

2018年の様々な「良かった」をまとめました。

良かった買物

これに関しては、買ってよかったものというサービスにまとめてあるので割愛します。 katteyokatta.morishin.me

良かったPodcast

トータルテンボスのぬきさしならナイト

1月に聴き始めて以来、トータルテンボスの大ファンになりました。ばっかばかしいのが好きな人にはおすすめです。

バイリンガルニュース

英語と日本語が入り混じって最新のマニアックなニュースについて伝えてくれる有名な番組です。英語力低い状態で英語をずっと聴いていると段々話題が分からなくなっていくのですが、この番組は途中途中日本語が入るのでリラックスして英語に触れられます。

Turing Complete FM

GoogleソフトウェアエンジニアのRui Ueyamaさんが低レイヤの技術についてひたすら喋る番組です。

Kubernetes Podcast from Google

k8sや周辺のクラウドネイティブ技術について伝えてくれる番組です。個人的にはEnvoy作者のMatt Kleinが出演したときが神回です。

良かった飲食店

和門 おく山

言わずもがな。

寿司大

始発で豊洲に行き、4時間並んで食べました。個人的にはブリとしめ鯖で泣きそうになりました。

ローストホース

会員制なので会員の方に連れて行って頂きました。

JINDARI

インドで数年間修行を積んだ店主による本気のカレーを数種類食べました。

エミリア

麺のモチモチ感が異常で、麺が長めなのがまた良いです。

イマカツ 六本木本店

ささみカツで初めて感動し、一時毎日のように通ってました。

鳥竹 総本店

疲労を全回復してくれる居酒屋です。

肴処やおよろず

なかむラー油が購入できるので、仙台行ったら必ず行きたい店です。

良かったお酒

日本酒

純米酒好きは祖父譲りです。

ワイン

さすが王様。

シャルドネ100%の泡。

  • ホシャール・ペール・エ・フィス

レバノンのワインを初めて飲みました。

良かった旅行

屋久

一人で行き、白谷雲水峡を登ったり適当にサイクリングしたりしました。
屋久島はそこらじゅうにパワースポットなり自然遺産なりがあり、適当にプラプラしてるとこんな場所にたどり着いたりするので、無理に予定入れなくても楽しめました。

湯河原

友人の別荘に泊まり、広大な景色を見ながらひたすら温泉に入りました。
東京駅から2時間かからず行けるので、本当に癒やされたいならベストプレイスかもしれません。

良かった体験

MEN'S RIZEのヒゲ脱毛

肌が弱く麻酔が出来ないため気を失うほど痛いけれど、日々のHX(ひげ剃り体験)がかなり向上しました。

SUMMER SONIC 2018

初めてのフェスだったのですが、2つの学びを得ました。
当時の自分はカリスマのエンジニアになるという自己実現欲求が前に出すぎていました。そこでNickelbackという最上級レベルの自己実現族を見て、進み方が大きく変わりました。
そしてDAOKOを見て、自分の世界観と社会の需要がマッチすることの尊さを知りました。

良かったチャレンジ

ISUCON出場

ISUCONとは、Iikanjini Speed Up CONtest(いい感じにスピードアップコンテスト)のことで、Webサービスをチューニングして高速化するコンテストです。
出場した1392名、528組の中で30組のみが進める本戦に、学生枠という制度を使って進むことが出来ました。詳しくは以下ブログにまとめてあります。

nakawatch.hatenablog.com

nakawatch.hatenablog.com

英語ブログの開始

もっと多くの人からフィードバックを受けたいという思いと英語学習を兼ねて、自分の技術ブログを英訳し始めました。これがかなり良く、定期的に英文を書く習慣がつくと、もっと細かいニュアンスを伝えたいという欲求が生まれるため、普段読む英文を見る目も変わってきました。

medium.com

技術ブログがちょいバズり

今までオープンな場で評価を受けたことがなかったので、アウトプットが間違っていないことが確認できて安心しました。

nakawatch.hatenablog.com

初LT

Dive into mixi nightStudent Goにて登壇させて頂きました。今後登壇機会も増えてくるので、かなりいい経験になりました。

speakerdeck.com

スタートアップへの参画

株式会社レキピオという会社に2ヶ月間ほどジョインさせて頂きました。ここで学んだ事と出会った人は素晴らしく、現在の自分の骨にもなっています。

良かったインターンシップ

株式会社メルカリ 2月〜6月

上場前後というホットな時期にジョインさせて頂き、Go Boldスピリットは今でも自分の心に宿っています。自分が組織とパフォーマンスに興味を持ち、クラウドネイティブ技術を追うきっかけとなった場所でもあります。
上場前ということで動きも目まぐるしく、最初はメルカリアッテ、次にメルカリメゾンズ、最後はメルカリNOWという様々なサービスのAPI開発に携わらせて頂きました。異動の度にキャッチアップの必要があるので、ひたすらソースコードリーディングをしました。業界を引っ張っている方々のコードを血眼になって読んだことが、ソフトウェアエンジニア中尾涼の血肉となっていることは紛れもない事実です。

株式会社ミクシィ 7月〜8月

家よりも温かい空気感に包まれながら開発をさせて頂きました。役員クラスの方々や世界一のソシャゲであるモンストを支えるSREの方々等、中々お会いできない方々とお話する機会がたくさんあり、そこで学んだことの多くが自分の考え方のベースとなっています。業務内容は、AIジムのココサイズという新規事業の決済周りの面白いところを触らせて頂きました。

株式会社サイバーエージェント 9月〜現在

2020年度に新卒として入社する予定の会社です。入って1週間くらいで「ここだ」と思い、すぐに選考に進ませて頂き、内定承諾させて頂きました。
前述したように、メルカリさんで組織とパフォーマンスに興味を持ち、ドリームチームでモノづくりをしまくる30代を迎えたいという夢を抱き、クラウドネイティブ技術に興味を持ちました。そんな自分に必要な環境は、ある程度トラフィックが集まるサービスの開発チームのDX (Developer Experience) を上げる基盤構築を主体的に行える環境です。希望が叶って配属されたAWAチームはまさに求めていた環境でした。

良かった2018年

nakawatch.hatenablog.com

持ちつ持たれつを宣言して始まった2018年は、マンパワーに支えられて幕を閉じます。
「あれってまだ今年なの?」と思うことが多々あるくらい濃い一年でした。来年もよろしくお願いします。

Envoyによる分散トレーシングの実現と、未来

Click here for English version

この記事はMicroservices Advent Calendarの9日目の記事です。前回は@moomooyaさんのマイクロサービス化にあたって、gRPCを導入しようとしている話でした。
この記事のタイトルは、先日のGoCon2018 Autumnにて発表された「OpenCensusによるAPMの実現と、未来」というセッション名がかっこよかったので、表現を拝借しました。

分散トレーシングの必要性

マイクロサービスのような分散アーキテクチャは複数のサービスにまたがって処理されているため、サービス間の通信を追跡することが難しくなります。そのため、

  • 障害発生時の原因究明が難しくなる
  • パフォーマンス低下の原因究明が難しくなる

そこで、分散トレーシングツールで可視化して上記の問題を解決する必要があります。

用語

分散トレーシングを学ぶにあたって、以下2つの用語は必ず覚える必要があります。

用語 意味
スパン システム内の作業の論理単位
トレース 1回の操作に含まれるスパンの集合体

何故Envoyなのか

Envoyを使わずとも、ZipkinJaeger等のトレース可視化プロバイダが提供するツールがあれば、分散トレーシングを実現することは可能です。
しかし実現のためには、トレース毎の識別子を生成してそれを伝搬していき、最終的にコレクターというコンポーネントに送ることが必須です。そのために各サービスに言語毎のクライアントライブラリを導入して、専用の実装をする必要があります。
Polyglotが前提にある分散アーキテクチャであるマイクロサービスアーキテクチャにおいて、これは大きなコストを生む原因になります。

そこで、インバウンドとアウトバウンド双方のトラフィックを前提に作られているEnvoyをサイドカーとして配置しておけば、この問題を解決してネットワークとアプリケーションを分離することが出来ます。
このような、全サービス間通信をサイドカープロキシを経由させることによってマイクロサービス固有の課題の解決を行うアーキテクチャサービスメッシュと呼びます。
ただ、スパン間の親子関係等のコンテキストを適切に伝搬させるためには、アプリ内で特定の識別子を伝搬させる実装をする必要があります。これに関しては後述します。

Envoyのトレーシングアーキテクチャ

利用するトレースプロバイダにもよりますが、多くの場合以下のようなコンポーネントで構成されたアーキテクチャとなります。プロバイダによってStorageを提供してくれるものもあれば、自前で運用する必要があるものもあります。

f:id:NakaWatch:20181215162055p:plain

分散トレーシングでは通常、トレース内で一意の識別子を伝搬させることでスパンを関連付けていきます。
この一連の流れの中で、Envoyの仕事は大きく分けて3つあります。トレースの生成とトレースの伝搬、そしてコレクターへの送信です。

生成ドキュメント

tracingオブジェクトを設定することで、トレースを生成することが可能になります。

伝搬ドキュメント
x-request-idという識別子を伝播して、呼び出されたサービス間のロギングを相関させていきます。
そして前述したように、スパン間の親子関係を明確にするためには別の識別子の伝搬も必要です。そしてEnvoyはこの識別子をHTTPリクエストヘッダーに乗せて通信します。そのため、特定のヘッダーから識別子を取り出して次に通信するサービスへのリクエストヘッダーに追加する必要があります。つまり、少なからずコードの修正は必要になるということです。
Envoyにはインバウンドトラフィックとアウトバウンドトラフィックを関連付ける手段がないので、これはアプリケーション開発者の責任だということです。
また、これはトレース可視化プロバイダ毎に異なる識別子が必要になるため、自分が使うプロバイダのドキュメントをよく読む必要があります。

送信
執筆時点(2018/12/9)で対応しているトレース可視化プロバイダは以下です。プラガブルになるよう設計されておりドライバを実装するだけで良いので、新たなプロバイダをサポートすることも難しくありません。

ドキュメントに従って、トレースを送信するコレクタークラスタを指定します。例えばZipkinの場合は以下のように定義します。

static_resources:
  clusters:
  - name: zipkin
    connect_timeout: 1s
    type: static
    lb_policy: round_robin
    hosts:
    - socket_address:
        address: 10.5.0.2
        port_value: 9411
tracing:
  http:
    name: envoy.zipkin
    config:
      collector_cluster: zipkin
      collector_endpoint: "/api/v1/spans"

触って理解する

Envoyは公式のサンドボックスがいくつか用意されています。今回はこの中のjaeger-tracingを触りながら、分散トレーシングの一連の流れの理解を深めていきたいと思います。

起動

$ git clone https://github.com/envoyproxy/envoy.git
$ cd envoy/examples/jaeger-tracing
$ docker-compose up -d

front-proxy, service1, service2 という3つのコンテナが起動します。

リクエスト送信

$ curl -v http://localhost:8000/trace/1
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8000 (#0)
> GET /trace/1 HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: text/html; charset=utf-8
< content-length: 89
< server: envoy
< date: Sat, 08 Dec 2018 06:10:24 GMT
< x-envoy-upstream-service-time: 27
<
Hello from behind Envoy (service 1)! hostname: 580a7e50d809 resolvedhostname: 172.18.0.5
* Connection #0 to host localhost left intact

front-proxyservice1service2 という順に通信が行われます。

可視化
ブラウザでhttp://localhost:16686にアクセスして下さい。
複数のスパンが一つのトレースとなって表示されているのが確認できます。

f:id:NakaWatch:20181208155100p:plain

サービスの実装を変更
サービスのサンプル実装はexamples/front-proxy/service.pyにあります。
注目すべき実装は以下の部分で、ここで受け取った識別子をヘッダーに乗せて伝搬させています。

    if int(os.environ['SERVICE_NAME']) == 1 :
        for header in TRACE_HEADERS_TO_PROPAGATE:
            if header in request.headers:
                 # ここをコメントアウト
#               headers[header] = request.headers[header]
        ret = requests.get("http://localhost:9000/trace/2", headers=headers)

Zipkin互換のあるプロバイダの場合、この識別子についてはB3 Propagationというリポジトリにて詳述されています。
試しにこの処理をコメントアウトして、再度リクエストを送ってみましょう。

f:id:NakaWatch:20181208154918p:plain

スパンがそれぞれ別のトレースと認識されてしまっているのが分かります。 スパン間で識別子を伝搬させないと正しくトレースを取得出来ない事が確認できました。

未来

全体的なアーキテクチャに始まり最終的にコードを触ることで、大まかな仕組みを理解して頂けたでしょうか?
先日(2018/11/28)、EnvoyはCNCFの卒業プロジェクトに認定されました。 その時のブログ内でも繰り返して述べられている通り、Envoyの目標は、アプリケーション開発者からネットワークを抽象化してビジネスロジックに集中できるようにすることです。
これはEnvoyに限らず、クラウドネイティブが主流になった現代では多くの人が、アプリケーション開発者が創作活動に集中できる環境を望んでいます。
2016年9月にLyftがEnvoyをOSSとして公開した時、本人達の想像を超えるほど反響がありました。これは、クラウドネイティブを推進するにあたってネットワークプロキシが非常に重要なポジションを占めていることを表しています。今後アプリケーションからネットワークを抽象化するようなプロジェクトがどんどん立ち上がるとしても、内部では常にネットワークプロキシがアプリケーションに寄り添っているはずです。そのため、プロキシへの理解を深めておくことはクラウドネイティブ時代において非常に大事だと思います。 筆者も微力ながら議論に参加していき、アプリケーション開発者にとっての最高の環境作りに貢献していきたいと思います。

*間違いを見つけたり情報が古くなっていたりしたらTwitter等でご指摘頂けると幸いです。

Service Meshをスモールスタートするという選択

Click here for English version

今記事は、CyberAgent Developers Advent Calendar 2018 7日目の記事です。
こんにちは、20卒内定者で現在AWAでアルバイトをしている中尾涼(@nakabonne)です。
現在マイクロサービス界隈でService Meshというアーキテクチャが注目されています。
実現のために様々な製品がありますが、私はsxdsという小さなcontrol-planeを作りました。
今記事ではService Meshを小さく始める手段について、sxdsを使用しながら紹介していきます。

github.com

対象読者

  • Service Meshの導入を検討している人
  • Service Meshへの理解を深めたい人
  • マイクロサービスに興味がある人

Service Meshとは

乱暴に説明すると、マイクロサービス化することで起きる様々な問題を解決するためのアーキテクチャの一つです。

Service Meshの定義については、様々な記事で紹介されてるので、今記事では割愛します。

Service Meshの実現手段

control-plane, data-planeそれぞれを実現するツールは、以下のようなものがあります。
ここでは執筆時点(2018/12/7)で代表格とされているもののみ説明します。

control-plane

執筆時点でのcontrol-planeとしてはIstioが有名です。
Istioはdata-planeを内包しているため、ほぼこれ一つでService Meshを実現することが出来ます。
非常に優れたツールではありますが、もちろん銀の弾丸ではありません。
Istioはオーケストレーションツールからクラスタ情報等を収集することで効果を発揮します。
つまり、オーケストレーションツールを使っていない環境では効果を発揮することが出来ません。
また、Istioは多くの機能が提供されていますが、ごく僅かな機能しか使わない場合は同じく効果を十分に発揮することが出来ません。

data-plane

Istioのデフォルトdata-planeにも採用されていて、先日発表されたApp Meshでも使われているEnvoyが、代表格といえるでしょうか。
EnvoyはL7プロキシです。特徴は、インバウンドとアウトバウンドトラフィックの仲介を前提に作られているため、分散トレーシングや動的なサービスディスカバリをプロキシレベルで実現出来る点です。
また、優れたスレッドモデルによってパフォーマンスが非常に高いという点も挙げられます。Envoyでは、全てのハードウェアスレッドにワーカーを生成してそれをメインのスレッドが制御しています。
C++を選んだのも、そのモデルを実現できる唯一のプロダクショングレードな言語だからとのことです。スレッドモデルに関してはブログに詳しく記載されています。

control-plane自作

紹介したとおり素晴らしいツールはたくさんあります。しかし私が当初欲しかった機能は、サービスディスカバリのためにdata-planeのルートテーブルを動的に更新するというものだけでした。
また、ほとんどのサービスがAWS の ECS というコンテナ管理サービスの上で動いていたため、k8s等のオーケストレーションツールを使っていませんでした。
そこで私はdata-planeにEnvoyを採用し、それを制御するsxdsというcontrol-planeを作りました。
次の節から作り方を詳しく説明していきます。

インターフェース

Envoyと通信するためのインターフェースはdata-plane-apiとして公開されており、ルートテーブルを動的に更新するためにはこれを満たすAPIを作る必要があります。
執筆時点でこのAPI定義に準拠しているプロキシはEnvoyのみですが、作者のMatt Kleinはdata-plane-apiを汎用的にしていきたいとブログにて発言しています。
そのため、将来このAPI定義が汎用的になった場合、自作したcontrol-planeがプラガブルになります。
また、data-plane-apiを満たしたAPIをxdsと呼びます。

Goによる実装

Envoyは独自のプロトコルであるxds protocolを定義しています。
これに従ってgRPCのbidiストリームかロングポーリングのRESTでxdsを実装します。
xds protocolを忠実に守って実装しないといけないわけですが、GoとJavaのみこの実装が含まれる公式ライブラリが公開されています。

sxdsではGoを選択し、go-control-planeをインポートしてxdsサーバーを実装しました。

はい、これだけでxdsの実装は終わりです。ただ、go-control-planeはdata-planeの設定をメモリにキャッシュする必要があるため、以下のようにキャッシュするためのRESTサーバーも立てています。

全体図はこんな感じです。

f:id:NakaWatch:20181124102325p:plain

静的リソースの管理

Envoyへのレスポンスに必要な静的なリソースを管理する必要があります。
sxdsはLDS/CDS/RDS/EDSという4つのサービスを提供しているため、これらに必要な以下のリソースを定義しておく必要があります。

また、リソースのバージョンも明示する必要があります。
無駄なパケットのやりとりを防ぐため、バージョンに変更が無い時はレスポンスを返さないという決まりがxds protocolに定義されています。
sxdsではこれらをjsonで定義し、前述したキャッシュするためのサーバーにPUTします。

Envoyの設定

sxdsのクラスタ情報は静的なので、static_resourcesとして定義します。
そしてそのクラスタをdynamic_resourcesとして指定します。

実行

Envoyの実行

sxdsはメモリ消費を抑えるため、ノードタイプ毎にリソースをキャッシュします。
そのためEnvoyのノードタイプを設定する必要があり、ノードIDの命名規則{node_type}-{node_id}とすることでこれを設定します。
また、sxdsへのdiscovery requestのために自身のクラスタ情報が必要なので、クラスタ名も指定します。
このノードIDとクラスタ名は、設定ファイルに定義することも可能です。

$ envoy --service-node sidecar-app1 --service-cluster app1

sxdsの実行

以下のように2つのサーバーが起動します。

$ sxds
2018-12-01T10:00:00.518+0900    INFO    xds/server.go:56    xDS server is listening {"conf": {"Port":8081}}
2018-12-01T10:00:00.518+0900    INFO    cacher/server.go:58 cacher server is listening  {"conf": {"Port":8082}}

リソースの設定

先程作成したjsonファイルをcacherサーバーに送信します。
そうしたらEnvoyに設定が反映されることが確認出来ると思います。

$ curl -XPUT http://{IP_ADDRESS}:8082/resources/sidecar -d @sidecar.json

まとめ

sxdsの紹介をしていきましたが、もちろんこれも完璧ではありません。
control-planeを冗長化した際は扱いづらくなるし、リソースjsonを永続化する仕組みも必要です。
伝えたいことは、envoyproxyの公式ライブラリを使えば低コストでcontrol-planeを作ることが出来て、必要な機能だけ自作するという手段も十分考えられるということです。
既存のサービスをマイクロサービス化する場合、必ずしもモダンな環境であるとは限りません。
そういった中で、環境に合わせて簡単に自作出来るという手段を持っていることは大きな武器となります。
ここまで読んで下さったあなたの頭に、Service Meshをスモールスタートするという選択肢が芽生えたとしたら幸いです。

isucon8本戦で惨敗したからせめて良いブログを書く

ISUCON8の本戦に、メルカリで一緒だった@zaq1tomo@inatonixzin-gonicというチーム名で出場しました。
結果は学生9位、全体で19位だったので、やったこととかまとめます。
予選エントリはこちらです↓

nakawatch.hatenablog.com

やったこと

チューニング対象は、isucoinという取引所アプリでした。
言語は予選と同じGoです。

github.com

担当も予選と同じです。

@nakabonne: インフラ担当
@zaq1tomo: アプリ担当
@inatonix: アプリ担当

序盤

@nakabonne

序盤は主にプロファイリングの準備です。

今回はdockerでデーモン化されていたため、パフォーマンスが少し気になる上にプロファイリングが少し面倒でした。
当初はdockerを剥がしてsystemdに乗せ換えようとしましたが、工数に対するインパクトが見込めず一旦後回しにしました。
また、nginxの公式イメージを見ると標準出力をdocker logsに流しており、アクセスログがうまく拾えず若干詰まりました。

@inatonix, @zaq1tomo
  • マニュアルとコード読んで仕様把握
  • pprofでプロファイリング

方針決め

各プロファイリング結果毎に見ていきます。

リソース

f:id:NakaWatch:20181020231333p:plain

実行時にCPU使用率は100%に張り付き、半分以上はmysqlが食っていました。
メモリもギリギリで、こちらも同じく大半はmysqlのキャッシュによるものでした。
この時点で早いうちにDBを外に切り出すことは決めました。

スロークエリ

f:id:NakaWatch:20181020233032p:plain

全tradeをソートしてから取得するクエリがまず第一関門っぽかったので、初動は決まりました。
二つ目もクエリ数減らせないか調査の必要がありそうだと話した気がします。

アクセスログ

f:id:NakaWatch:20181020233208p:plain

POST /orders GET /info から見ていく必要があるのは一目瞭然で、エンドポイントが少ないことから一筋縄ではいかない匂いがプンプンでした。

中盤

@nakabonne
  • mysqlを別サーバーに移行
  • nginx基本チューニング
  • このクエリやばそうとかつぶやく

f:id:NakaWatch:20181020235709p:plain

mysqlを切り出し、サーバー1をapp+nginxのみの構成にしたためCPUに半分くらい空きが出来ました。
CPUがボトルになるまではしばらくappは一台で動かそうとなりました。

@inatonix, @zaq1tomo
  • latest tradeから取得するフィールドをidのみにする修正

この@zaq1tomoの修正で500点→3200点に伸びました。

  • trade取得時にLIMITかける修正
つかの間の1位

この修正で5136点まで伸びました。そしてこの瞬間、我々zin-gonicは1位に躍り出ました。
ただ、この時を最後に我々が笑顔になることはありませんでした。。

f:id:NakaWatch:20181021001612p:plain

終盤

@nakabonne
  • shareボタンを有効に、、

CPUに余裕が出来たところで、リソースを使い切りたいと思っていたところ、@inatonixが怪しいものを見つけました。

// TODO: trueにするとシェアボタンが有効になるが、アクセスが増えてヤバイので一旦falseにしておく
res["enable_share"] = false

どうやらシェアボタンを有効にすると取引成立時にシェアされてユーザーが流れ込み、一気に負荷があがるそうで、これだ!と有効にしました。
が、GET /infoタイムアウトが頻発。
DBサーバーを見てみるとCPUが使い切られていたので、ロックされてるのかな?と思いつつとりあえずこれはまだ早いと判断して、そっとブランチを切り替えました。
講評を聞く限りここで負荷を上げても耐えられるようにすることが鍵だったらしいです。

  • 悪あがき

f:id:NakaWatch:20181020234905p:plain

このブランチ量を見れば悪あがきをしていたことが分かります。
もちろん何も意味をなしていません。

@inatonix, @zaq1tomo
  • POST /orders で呼ばれる AddOrders() というメソッドをなんとかしようと奮闘
  • shareボタンを有効にしたときのGET /Infoを直そうと奮闘

oh...

感想

問題作成していただいたDeNAさん、カヤックさん、主催のLINEさん、サーバー提供していただいたConoHaさん、本当にありがとうございました。
今回の優勝者は学生であり、それを讃えている皆さんを見て、改めて最高の業界に来てしまったと感じました。
もしisucon9があるならば、来年も学生枠使えるので(もはやそんな枠があるかは不明)出場したいと思います。

gRPCは何故誕生したのか

Click here for English version

gRPCという言葉はすっかり有名になり、日本でも「社内でgRPCを導入した」「gRPCサーバーを構築する方法」といったような記事が溢れかえるようになりました。
これにより、gRPCの導入ハードルは下がり、比較的簡単にプロダクションレディなgRPCサーバーを作ることが出来るようになりました。
しかしそもそもどういった背景でgRPCが誕生したのでしょうか。

背景

gRPC Blogには、以下のように記載されています。

GoogleはstubbyというRPCインフラストラクチャ持っており、10年以上Googleのデータセンター内のマイクロサービスを接続してきた。
統一されたクロスプラットフォームのRPCインフラストラクチャを持つことで、全体の効率、セキュリティ、信頼性の改善の展開を可能にし、その期間の信じられないほどの成長を支えた。

stubbyには多くの素晴らしい機能があるが、社内のインフラに密接に結びつきすぎて一般公開には適していなかった。
SPDY, HTTP/2, QUICの登場により Stubbyをもっと汎用的に作り直して、モバイル、IoT、クラウド等のユースケースに適用させるべき時期が来たことが明らかになった。

また、Google Cloud Platform Blogには、以下のように記載されています。

高度にスケーラブルで疎結合のシステムを構築することは、常に困難だった。 モバイル機器やIoT機器の普及、データ量の急増、顧客の期待の高まりに伴い、インターネット規模で効率的かつ確実にシステムを開発して運用することが重要である。
この種の環境では、開発者は複数の言語、フレームワーク、テクノロジー、複数の第一および第三者サービスを扱うことがある。 これにより、サービス契約を定義して実施したり、認証や承認、ヘルスチェック、ロードバランシング、ロギングと監視、トレースなどの横断的な機能を全体にわたって一貫性を持たせることが難しくなる。 今日のクラウドネイティブの世界では、新しいサービスをすばやく追加する必要があり、各サービスの期待は柔軟で弾力性があり、可用性が高く、構成可能であることが特に課題となる。
過去15年間、Googleはこれらの問題を内部的にStubbyというRPCフレームワークで解決した。このRPCフレームワークは、数十億回のリクエストをインターネット規模で処理できるコアRPCレイヤーで構成されている。 今この技術は、gRPCと呼ばれるオープンソースプロジェクトの一環として、誰でも利用可能である。 これは、私たちがGoogleで楽しむのと同じスケーラビリティ、パフォーマンス、機能をコミュニティ全体に提供することを目的としている。

以上を踏まえ、誕生の経緯を雑にまとめるとこんな感じでしょうか

  • スケーラブルで疎結合なシステムを作るのは難しいけど、モバイルとか普及している現代はやらないといけない
  • GoogleではクロスプラットフォームのRPCフレームワーク使って対応していた
  • でもGoogle内部のインフラにめっちゃ依存してた
  • HTTP/2とか出てきて、Googleのインフラに依存せずとも標準規格で同等以上のものを作れるようになった
  • 標準規格ベースで作り変えて一般公開しよう!

参考

gRPCについて理解を深めたい場合は、以下から眺めていくと徐々に分かっていくと思います

間違い等ありましたら、コメントやtwitter等でご指摘頂けると嬉しいです。