Go の GraphQL API のパフォーマンス改善のために分散トレーシングを導入した話

この記事はJX通信社Advent CalendarGraphQL Advent Calendarの1日目です。

JX通信社でNewsDigestというアプリを開発しているyamitzkyです。

NewsDigest では、アプリから利用する API に GraphQL を利用 しています。本番での利用を始めてからちょうど1年を過ぎました。

f:id:yamitzky:20181130001740p:plain

JX 通信社ではプログラミング言語として Python が使われることが多く、この GraphQL API も Python で作ってサーバーレス環境(AWS Lambda)にデプロイ していました。しかし、Lambda では要件が合わなくなってしまったため、現在では Amazon ECS で作った Docker クラスタ内で動いています。また、非サーバーレス化に合わせて、パフォーマンス要件を満たすために Go でのリプレイスを行いました。

f:id:yamitzky:20181130130648p:plain

この マイグレーションに伴って最も困難だったところがパフォーマンスチューニング です。 今回の記事では、Go で作った GraphQL API をどのようにパフォーマンスチューニングしたのかを紹介します。

ライブラリ選定

GraphQL の API は、一般的な API とは異なり、リクエストのパースやレスポンスの構築が難しい 、という点が挙げられます。RESTful API であれば「JSON」や「URLに対して正規表現をかける」など、言語標準のライブラリだけで簡単に実現しやすいのですが、GraphQL の場合は GraphQL の専用のスキーマやクエリなどの仕様があります。そのため、 ライブラリ選定が重要 になってきます。

f:id:yamitzky:20181130130618p:plain

NewsDigest では、 99designs/gqlgen というライブラリを選定しました*1。gqlgen はスキーマファーストで、冗長なボイラープレートが少なく、type safe で、 検証したライブラリの中で最も良いパフォーマンス (ns/op)でした。個人的な意見ですが、Go で GraphQL をやるのであれば、 gqlgen 一択だと思います。

他に検討したライブラリは以下のとおりです。*2

  • graphql-python/graphene:元々利用していたライブラリ。パフォーマンスが悪かったため不採用
  • graphql-go/graphql:スキーマ定義を Go でやる必要があり冗長だったのと、型安全ではなかったため不採用
  • graph-gophers/graphql-go:一個一個の resolver 定義が必要で冗長だったため不採用
  • samsarahq/thunder:パフォーマンスは gqlgen よりも少し良かったが、Interface に対応しておらず、移行できなかったため不採用
  • playlyfe/go-graphql:メンテが止まっていたため不採用

GraphQL API のパフォーマンスチューニング

ユーザー体験を最大化するためには、なるべく API のレスポンスにかかる時間を短くしたいです。

通常の API のチューニングであれば、「どのエンドポイントが遅いのか?」をまず探ると思うのですが、GraphQL API の場合は /graphql という単一のエンドポイントしかありません。

f:id:yamitzky:20181130125335p:plain

そこで、GraphQL の API でボトルネックを探る際には、 「どのリソースが遅いのか」をトレーシング するための、専用のツールなどを入れる必要があります。

gqlgen のトレーシング

GraphQL のパフォーマンスのメトリクスを取る際は、一般的には Apollo Tracing などが使われることが多いと思います。Apollo Tracing を使うと、どのリソースの解決に時間がかかっているかなどがよくわかります。 (この例では、 book のフィールドに author というものがあります)

f:id:yamitzky:20181130113741p:plain

しかし、Apollo Tracing への対応は、ライブラリ依存です。2018年3月当時は、gqlgen が対応していなかったため、この方法でのトレーシングはできませんでした*3。そのかわり OpenTracing というトレーシングに当時から対応していたため、こちらで対応することにしました。

OpenTracing / 分散トレーシング

OpenTracing は分散トレーシング(Distributed Tracing)のための規格のようなものです。あくまで規格なので、OpenTracing 自体はプログラムや個別の OSS ではありません。Jaeger や Zipkin のような OSS や、DATADOG のようなウェブサービスが、OpenTracing の規格に対応しています。

分散トレーシングというのは、一般的にはマイクロサービスのためのトレーシングに使われます。マイクロサービスの計測では、「ある1つのリクエスト」が、複数のマイクロサービスへのリクエストになり、ボトルネックが探りにくい、という問題があります。まさに、GraphQL API の計測が抱えていた問題と一緒です。

f:id:yamitzky:20181130125652p:plain

そこで、どの通信にどれくらい時間がかかったかや、どこで失敗したかを探りたい、というのが分散トレーシングの目的となります。

f:id:yamitzky:20181130125910p:plain

Jaeger の選定

OpenTracing は規格でしかないので、gqlgen の計測をする OSS の選択肢はいくつかあります。その中でも、Jaeger という OSS を選定しました。今回は、分散トレーシング自体初めてでいろいろわからなかったというのもあり、DATADOG のようなサービスは選定から外していました。

https://www.jaegertracing.io/img/jaeger-logo.png

  • Go 製の OSS
  • ストレージとして Elasticsearch と Cassandra に公式対応
  • Go、Python、Java、Node、C/C++ などに対応
  • HTTP だけでもメトリクスを集められる*4
  • アイコンが可愛い

分散トレーシングは個別のリクエストのトレーシングが注目されることが多く、「全体的にはどのリソースが遅いのか?」という統計的なものを得られる OSS は少ないのではないかと思います。その点 Jaeger は Elasticsearch に対応しており、 Kibana 上で集計して見ることもできるので良かったです。

Jaeger と Elasticsearch によるボトルネックの確認

こちらは実際の Jaeger の画面です(一部加工済み)。こちらの画面を見ると、どこでエラーが起きていて、どこの処理に時間がかかっているのがわかります。この場合、Query_piyo (piyo というリソース)の redis の処理が、ボトルネックとなっていることがわかります。

f:id:yamitzky:20181130122935p:plain

ただしここでわかるのは、あくまで個別のリクエストについてです。そこで、Kibana を使って、全体的なメトリクスを確認します(operationName で絞るのがポイントです)。こうしてみると、全体的には hoge のリソースの取得遅いようです。

f:id:yamitzky:20181130123612p:plain

(一部加工済みです)

Jaeger を入れてみてわかったこと

Jaeger を試してみてわかったのは、分散トレーシングの仕組み自体は、「GraphQL API」や「マイクロサービス」に限って便利なわけではない 、ということです。GraphQL であれば「どのリソースがボトルネックか」を知りたく、マイクロサービスであれば「どのマイクロサービスがボトルネックか」を知りたいのと同様、一般的なモノリスな API であっても「DB がボトルネックか、Redis がボトルネックか、アプリケーションがボトルネックか...」というのは知りたい情報です。実際、NewsDigest での利用方法でも、Redis やデータベースのアクセスのタイミングでトレーシングを仕込んでおり、Redis がボトルネックであることに気づいたりもしました。

f:id:yamitzky:20181130130248p:plain

APM サービスは他にもあるので、分散トレーシングの仕組みをわざわざ入れなくてももっと賢い方法はあるかもしれませんが、トレーシングが規格化されているのは特定のウェブサービスに依存しなくて済むのでいいなと感じました。

余談

今年の ISUCON は、GraphQL API のチューニング・・・とまではいかないまでも、マイクロサービスがお題になったりしないかな、と予想していました(笑)

JX 通信社では GraphQL API をもっと速くしてくれるサーバーサイドエンジニアを募集中です。

*1:当時は、vektah/gqlgen でした

*2:2018年3月に検証したため、現在は異なる可能性があります

*3:検証していませんが、現在は対応済みのようです。ありがとうございます。 https://github.com/99designs/gqlgen/pull/404

*4:一般的には、 HTTP 通信でマイクロサービス用の計測をすると遅い(無駄)なので、 udp を使うことが多いと思います。JX のインフラは AWS の Application Load Balancer を使うことが多いので、 HTTP で集められるのは助かりました

リリースを気軽に祝う文化を作るために 〜喋るSlack botを作った話〜

VPoEの小笠原(@yamitzky)です。今回は、僕が勝手にやっている「プログラミングで社内を活性化させる仕組み」について紹介します。

その取り組みの一つが、JX通信社の“しゃべる” Slackbot の「speakerbot」です。こちらは、エンジニア Hub の記事でも少し紹介いただいています。

employment.en-japan.com

speakerbot の機能は、Slack で送った文章を、社内のスピーカーで自動で読み上げるだけのものですが、NewsDigest が速報ニュースを検知したときに読み上げたり、アプリリリースなどの「良いこと」があったときに皆で祝ったりと、いろいろと活用されています。

お祝いのとき↓ f:id:yamitzky:20180927224133p:plain

速報ニュースを検知したとき↓ f:id:yamitzky:20180927224116p:plain

speakerbotの仕組み

speakerbotは、 Python 製の Slack bot で、スピーカーにつながった社内の Raspberry Pi 上で動かしています。Amazon Polly という音声読み上げサービスを使っています。

f:id:yamitzky:20180927221547p:plain

なぜspeakerbotを作ったのか

アプリの大型リリースなどを月例会や打ち上げで祝うこともあるのですが、もっと日常的な「良いこと」が、チームを横断して気軽に祝えれば良いなと思っていました。例えば、ちょっとした改善がうまくいったとか、新しく契約ができたとかです。

とはいえ、全員の前で言葉で発表するのはちょっと勇気がいる人もいるので、 Slack bot に代わりに発表させればうまくいくのではないか?と思って作りました。

speakerbotと報道テクノロジーの関係

speakerbotの「重大ニュースを読み上げる」という機能の源流は、報道機関の現場にあります。共同通信の加盟社の報道フロアでは、 速報ニュースがあると社内放送で読み上げられる仕組み になっており、通称「ピーコ」と呼ばれています(「ピーピポピポピポ」という音がなるため;参考)。

f:id:yamitzky:20180927223154p:plain

speakerbotの場合は機械音声による読み上げですが、まさに「報道×テクノロジー」が生んだbotなのです・・・!

speakerbotの工夫したポイント

speakerbot で気軽に祝うために、いくつか工夫していることを紹介します。

あえて #general で見えるように書き込む

speakerbot に読み上げさせる際には、#general上でメンションを飛ばす形の運用にしています。

ヘッドホンをつけている働いている人もいて、音声だけだと聞き逃してしまう可能性があるので、あえて文字として残るようにしています。

リアクションを自動で残す

一番最初(左)のリアクションは、ランダムに bot が勝手につけます。そうすることで、コメントを書かなくてもリアクションしやすい環境を作っています。

f:id:yamitzky:20180927223635p:plain

読み上げスピードを遅くする

Amazon Polly には、音声の読み上げスピードを調整する機能があります。

読み上げスピードが早いと、気づいたときには聞き逃してしまったりするので、かなりゆっくりめで喋らせるなどの細かい工夫もしています。

speakerbot が製品にも役立った

「自動音声で重大ニュースを読み上げる」という仕組みは、AI緊急情報サービス「FASTALERT」にも組み込みました。

プロジェクトには関係なく作ったものが、結果的に製品に組み込まれて、プレスリリースにつながり、サービス利用者からのフィードバックにも繋がったので、とても良い体験でした。

おわりに

まだ speakerbot を使ってないメンバーも居たりするので、積極的に利用を促していくのは今後の課題です。引き続き、プログラミングで社内を活性化していきたいと思います。

www.wantedly.com

Nginx と自前の認証システムを組み合わせてセキュアなリソースを制限する

こんにちは、Pythonエンジニアの @kimihiro_n です。
ブログを書くとハイボールが飲め… 会社のエンジニアブログをはてなに作ってもらったので初投稿してみます。


Nginx で静的なファイルを配信する際に、認証をかませて配信対象を制限したいときってありますよね。 ページ自体のHTMLを表示する際にはログインが必須だけれども、そこで使っている画像やCSS, JSはそのまま見れてしまうという状況は場合によって好ましくありません。

静的ファイルに対する制限を手っ取り早く実現するには、 Basic 認証をかけてあげるのがシンプルです。ただ Basic 認証だとブラウザ側のダイアログが出てしまったりして、サービスとしてそのまま使いづらいです。DBとは別にユーザーの管理が必要になったりもしますし実用性に欠いてしまいます。

すでにログインのシステムが自前であるのであれば、システムと連携してよしなに出し分けできると嬉しいですね。Nginx には http_auth_request_module というモジュールが用意されており、これを用いることで任意の認証バックエンドと連携してリソースのアクセスを制限することが可能です。

http_auth_request_module とは

http://nginx.org/en/docs/http/ngx_http_auth_request_module.html

ngx_http_auth_request_module は Nginx 1.5.4 以降で導入されたカスタムモジュールです。リソースへアクセスする際に、プレリクエストとして認証サーバに問い合わせを行い、その結果に応じてアクセス制御を行います。 モジュールはオプションなので自分で Nginx をコンパイルする場合は、--with-http_auth_request_module というフラグを有効にしてコンパイルする必要があります。手元の Nginx に含まれているかどうかは以下のコマンドで確認が可能です。

nginx -V 2>&1 | grep -- 'http_auth_request_module'

Docker Hubにあがっている公式 Image の nginx:alpine にはすでに有効化された状態で入っていました。

$ docker run nginx:alpine nginx -V 2>&1 | grep -- http_auth_request_module
configure arguments: ...()... --with-http_auth_request_module ...()...

図: 連携イメージ f:id:nsmr_jx:20180822192711p:plain

ユーザーがプライベートなファイルにアクセスしようとすると、Nginx が 認証先として指定したサーバーへ確認のリクエストを飛ばしてくれます。認証側のサーバーは、リクエストを見て認証済かどうかを返します。この返り値に使われるのがステータスコードで、200番台のときは許可、401や403を返すと不許可となります。許可であれば Nginx はプライベートファイルをユーザーに返してくれます。 ステータスコードだけ通ればいいので、既存のログインシステムとかでも連携しやすそうですね。

実際に試してみる

https://github.com/pistatium/nginx_auth_sample

Docker-compose で簡単に試せる環境をつくってみました。

f:id:nsmr_jx:20180822191119p:plain:w200

Login ボタンを押すとログイン状態になり、/private/ 以下に置いた iframe 内のコンテンツを見ることができます。Logout ボタンを押せば再び見れなくなります。

server {
    server_name _;
    listen      80;
    access_log  /dev/stdout;
    error_log  /dev/stderr warn;
    root        /var/www/html;
    index  index.html;

    location /private/ {
        auth_request /auth/is_login;
    }
    
    location /auth {
        proxy_pass http://backend:8888;
        proxy_redirect off;
        proxy_set_header   Host $http_host;
    }
}

Nginx の設定はこんな感じです。 location /private/ のディレクティブで auth_request /auth/is_login; という指定をしています。これは /private/ 以下のファイルへアクセスする際に、/auth/is_login へ権限があるかを確認しにいくよう指定しています。/auth/is_login が 200 番台のステータスコードを返してくれればファイルを見ることができます。

設定の肝はこれだけなのですが、認証サーバーが別途ないことには試せないので、Python + Flask で擬似ログインシステムを作りました。/auth 以下のリクエストはすべてこのサーバーへ飛んでいきます。

@app.route('/auth/is_login')
def is_login():
    if not request.cookies.get(SESSION_KEY):
        abort(401)
    return ''  # 200 (204 を返してもいいかもしれない)

/auth/is_login のコード抜粋です。疑似ログインなので、特定の Cookie がセットされてればログインしたことにしています。実際は DB を見にいったりして正しいログイン状態であるかチェックする必要があります。 Nginxへ返すレスポンスは「ログインしていれば空のレスポンス(ステータスコード200)」 を、「ログインしていなければ401を返す」だけなので簡単ですね。 なお、プライベートなファイルにアクセスするたびに Nginx から認証のプレリクエストが飛ぶので、スループットを上げるには別途キャッシュなどの工夫が必要になりそうです。

これだけで Nginx のプロキシに対して独自の認証を組み込むことができました。静的なファイルの配信ではアクセス制御がおざなりになりがちですが、Nginx 上で弾けるのであれば積極的に使っていきたいですね。

参考

レッドアローで行く 秩父二泊三日の旅 「JX Summer Camp 2018」

f:id:numatch-jx:20180814180727j:plain

こんにちは、Androidエンジニアのぬまっちです。(@nuMatch)
5月に社内で技術書ビブリオバトルの記事を執筆して以来のブログになります。

www.wantedly.com

今年も7/25〜7/27の日程で合宿に行ってきました!
わたくし、ぬまっちが幹事を仰せつかりましたので、 ご紹介レポートをお届けしたいと思います。

レッドアロー号の指定席で秩父へ

今年の合宿の場所は秩父!池袋からレッドアローに乗って約1時間半、指定席に乗って快適な旅路です。

f:id:numatch-jx:20180811221402j:plainf:id:numatch-jx:20180811221340j:plain
車内にて

駅からはバスで迎えに来て頂き、今年の研修場所に到着です。
今年の宿は「梁山泊」さんに泊まらせていただきました。
ただし研修会場は宿とは別棟にありますので、まずはそちらに直行です!

f:id:numatch-jx:20180814180943j:plain

梁山泊ホール
ここが研修場所になります。
到着次第、息つく暇もなく研修が始まります。

f:id:numatch-jx:20180814181042j:plain

今年は開発合宿、ではない

去年までならば

「それじゃ、ここからは各自のテーマに沿って開発スタート!」  

とエンジニアがもくもくと開発に入っていくところでしたが今年は違います。

JX Summer Camp 2018

今年はエンジニアだけが参加する開発メインの合宿ではなく、
ビジネスチームも同時参加のSummer Campなのです!

普段の業務では中々取り組めない事をテーマにしてみようという初の取り組みになります。

f:id:numatch-jx:20180814181249j:plain f:id:numatch-jx:20180814181231j:plain

エンジニアとビジネスサイドが同じタイミングで一緒にブレストしたり、講習を受ける機会は中々作れないのでとても新鮮な体験になったと思います。
自分はエンジニアとしての合宿参加だったのですが、ビジネスサイドの業務を体験出来たのは新鮮でした!

f:id:numatch-jx:20180814181411j:plain f:id:numatch-jx:20180811230307j:plain

宿で思いっきりはしゃぐ

幹事として部屋割りに頭を悩ます必要はありませんでした。なぜなら全員一つの大部屋なのです。

f:id:numatch-jx:20180811230833j:plain

晩ごはんもみんなで仲良く一緒に。

f:id:numatch-jx:20180815012841j:plain

温泉に入ったり、お酒片手にボードゲームで懇親を深めたりで和気あいあい。チーム関係なく楽しい夜を過ごせました。

f:id:numatch-jx:20180814181858j:plain

畳の上でWelcome Contribution会

晩ごはんを食べてからも合宿は続きます!

エンジニアが普段できない開発をやってみるWelcome Contribution会が開催されました。畳の上で!

f:id:numatch-jx:20180811231717j:plain f:id:numatch-jx:20180815013013p:plain

まさに合宿感が出ている風景ですね。 チームの垣根を超えた開発体験が出来て良かったと思います。

ギャラリー

f:id:numatch-jx:20180815013043j:plain
夜はSwitchで盛り上がる一面も

f:id:numatch-jx:20180811231947j:plain
外でお弁当食べてリフレッシュ!

合宿参加者の声

  • 大部屋での作業はまったりしつつ聞くこともできて集中もできたので良かった
  • 別チームの課題を共有できた
  • 特急利用していけるのが快適だった
  • 他チームの具体的な仕事の話を一緒の空間で聞けたのは新鮮だった。
  • 大部屋だったので皆が集まるのに狭い思いをすることなく懇親できたのはよかった。
  • 24時間風呂使えるのよかった(前回は時間決まってたので)
  • このような機会がないと時間を作りにくいような講習を受けられたので凄く良かった。勉強になった。
  • 温泉でツルツルになれたのは良かった。

Next Camp is ... ?

いかがでしたでしょうか。年に一度の合宿はJX通信社の文化になりつつあります。

最後になりましたが、研修場所と宿への往復でバスを出して頂いたり、
細かいお願いにも快く対応して下さいました梁山泊さん、ありがとうございました!

秩父小鹿野温泉旅館 梁山泊

次はどんな合宿が開かれるのでしょうか?
今から次回が楽しみです。

f:id:numatch-jx:20180814182335j:plain

OSS 版 Redash を Docker と AWS でセキュアに運用する

VPoEの小笠原(@yamitzky)です。

JX通信社では、NewsDigestFASTALERT の KPI 管理や、データ分析の用途などで、 Redash を活用しています。

ただしセキュリティ上の観点から、Redash の OSS 版をプライベートなネットワーク内で運用しています。セキュリティ的には良いのですが、機能的な面ではマネージド版に劣る部分があります。例えば、Slack の bot や、Google Spreadsheet との連携などはできなくなってしまいます。そこで、新しくOSSを作ってこれらの課題を解決しています。その他にも、通常のマネージド版にはない機能を拡張したりもしています。

2年半 Redash を運用して溜まった知見を紹介します。

Docker クラスター上での Redash のホスト

f:id:yamitzky:20180704000651p:plain

JX通信社のインフラは、基本的には AWS 上にあります。

Redash の公式 Docker イメージを利用し、Amazon ECS を使った社内の Docker クラスター上にデプロイしてホストしています。後述する各種 OSS も、同様に Docker 化してデプロイしています。また、バージョンのアップデートは、Docker イメージのタグを切り替えることによって対応しています。

先述したように、セキュリティ的な観点から Redash はプライベート環境で運用されており、社外からは直接アクセスできないようになっています。

弊社の他サービスでも使ってる Docker クラスターにデプロイすることで、余剰リソースで Redash を使ってることになるので、かなり安価にホストできていることになります。また、WEB+DB PRESSに寄稿させていただいたようなサーバーレスログ基盤を運用しているため、ログデータの管理なども安価です。

OSS 版 Slackbot

Redash には Slack の bot がありますが、Redash がプライベートネットワーク内にあると公式 bot (外部ネットワーク)からアクセスできないために、動きません。

JX通信社では、hakobera さんが開発したものをフォークした、 yamitzky/redash を使っています。もちろん、Dockerイメージ もあります。

フォークした版は、元のものに比べて次のような追加機能があります。

  • グラフだけではなく、テーブルやダッシュボードの結果の投稿
  • 結果の投稿時に、クエリ名などを補完する (検索するとき便利です)
  • puppeteer への移行
  • オフィシャル Docker イメージの提供

f:id:yamitzky:20180705190958p:plain

Google Spreadsheet への連携

Google Spreadsheet は、IMPORTDATA 関数でインターネット上の CSV ファイルをインポートする機能があります。これを使うと、Redash の結果を Spreadsheet へインポートすることができます。

しかし、Redash がプライベート環境にある場合、外部ネットワークにある Spreadsheet のサーバーから Redash にアクセスできないので、動きません。

そこで yamitzky/redash-to-spreadsheet を開発し、一定の命名規則で作った Spreadsheet に、Redash のクエリ結果を自動でエクスポートするようにしています。こちらも Docker イメージがあります。

例えば、 Q135: ほげほげ というタイトルの Spreadsheet を作ると、クエリID「135」のクエリの結果が、定期的に保存されます。

追加のデータソース「iTunes Connect App Analytics」

ここからは、通常のRedash にはない機能です

Redash には、特定のフォーマットの URL(API) をデータソースとして扱う機能があります。

そこで、yamitzky/itunes-connect-analytics-api を作り、iTunes Connect の App Analytics の情報を Redash 向けに加工し、Redash 上からクラッシュ情報やストア情報をクエリできるようにしました。こちらも Docker イメージがあります。

ログ可視化ツール

こちらはまだ開発途中なのですが、 Redash で集計したユーザーログを、SPA 上でわかりやすく可視化する OSSyamitzky/redash-log-viewer」を作っています。ユーザーの行動をより定性的に追って、気づきを得たり、さらに使いやすいものにしていくのが目的です。

f:id:yamitzky:20180704000716p:plain

まとめ

最初に上げた構成図に紹介した OSS も含めて拡張した結果、こんな感じになっています。

f:id:yamitzky:20180704001116p:plain

Redash は OSS であり、API も完備されているということもあり、かなり拡張性があるものだと感じています。

引き続き、外部OSS という形で Redash のエコシステムをより便利なものにしていきたいです。

JX通信社では一緒にログ基盤を作るエンジニアを募集しています