Nginxでリクエスト毎に発番して、Railsのログに書き込むまで

いきなりまとめ

  • マイクロサービスとかでいくつものアプリケーションに対してリクエストを行うような構造のアプリケーションはリクエストIDを設定しておき、異なるマイクロサービスでも同一のIDがログ上で記録されるので、ログのチェックなどがはかどって便利。
  • Nginx 1.10以降で利用できる $request_id を使うと簡単に リクエストごとに発番できる。
  • Railsは HEADER X-Request-Id を取得してログにタグを簡単に設定できる。

なぜ必要なのかをもっと詳しく

Railsアプリケーションの手前でNginxでTLSを終端していたり、 複数のアプリケーションをまたいだりしているマイクロサービスでアプリケーションを実装していると、もし大げさなサービスを構築していなくとも、リクエストが多いとログを確認するのが大変なので、 なにかとNginxでリクエストごとに生成したIDを振ったものをProxyしているアプリケーションに渡してやることでログの確認を補助することができる。

やりたいこと・サンプルアプリケーションの構成

  1. 今回は前段にTLS終端を想定した、Nginxでロードバランサを立てる。
  2. 1.のNginxアプリケーション(コンテナ)でNginxでUUIDを割り当て、Proxyする。
  3. 2のコンテナから、ProxyからのアクセスにX-Request-Id が設定されている場合は、それをそのまま X-Request-Id に設定する。設定されていない場合は、3のコンテナでリクエストIDを発番する。
  4. 更にRailsアプリケーション用にProxyして、ProxyHeaderに書き込まれた X-Request-IdRailsのログにタグとして設定して書き込む

図解

サンプルリポジトリ

github.com

これをGit cloneしてきて、docker-compose up ってすればシュシュッとサンプルのアプリケーションが動くので、よかったら動かしてみて欲しい。

TLS終端用のNginx側の設定

リクエストごとにIDを発番する機構はNginx1.10以上であれば、$request_id を利用して、簡単に設定することができる。

http://nginx.org/en/docs/http/ngx_http_core_module.html#var_request_id

  server {
    listen       80;
    server_name  _;
    client_max_body_size 4G;
    keepalive_timeout 5;

    proxy_set_header    Host    $host;
    proxy_set_header    X-Real-IP $remote_addr;
    proxy_set_header    X-Forwarded-Host $host;
    proxy_set_header    X-Forwarded-Server $host;
    proxy_set_header    X-Forwarded-For http;

    location / {
      # X-Request-IdをProxy Headerに設定
      proxy_set_header X-Request-Id $request_id;
      proxy_pass http://nginx_rails_proxy;
    }
  }

上記は設定の抜粋なので、下記にすべて記載したものを掲載しておく。*1

nginx_proxy_uuid_rails/nginx.conf at master · webuilder240/nginx_proxy_uuid_rails · GitHub

Rails Proxy用のNginxコンテナの設定

基本的にはTLS終端用のNginxの設定と同じ。RailsアプリケーションへのProxy設定部分以外で異なるのは、 先程のTLS終端用のコンテナから受け取った X-Request-Id をProxyするアプリケーションに対して ProxyHeaderに設定してあげればOKということ。 もし X-Request-Idがない場合は、このNginxでリクエストIDを生成して設定すればOK。

  server {
    listen       80;
    server_name  _;
    client_max_body_size 4G;
    keepalive_timeout 5;

   # この生成したRequestIdをProxy用のRequest IDに設定。
    set $proxy_request_id $request_id;
    if ($http_x_request_id) {
      # Proxy Headerに `X-Request-Id`が設定されている場合、 Proxy用のリクエストIDに設定する
      set $proxy_request_id $http_x_request_id;
    }

    root /usr/src/app/public;

    try_files $uri/index.html $uri.html $uri @app;

    location @app {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      # Header, Proxy Headerに `X-Request-Id` を設定する。
      add_header X-Request-Id $proxy_request_id;
      proxy_set_header X-Request-Id $proxy_request_id;
      proxy_redirect off;
      proxy_pass http://app_server;
    }

    error_page 500 502 503 504 /500.html;
    location = /500.html {
      root /usr/src/app/public;
    }
  }

nginx_proxy_uuid_rails/nginx.conf at master · webuilder240/nginx_proxy_uuid_rails · GitHub

Railsの設定

Rails側の設定は至って簡単。 application.rbに下記設定を追記すればOK。 なお、X-Request-Idが設定されていない場合は、Rails側で生成したものが反映される。

require_relative 'boot'

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module NginxProxyUuid
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.2
    config.log_tags = [:request_id]
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.
  end
end

ログのフォーマットはこんな感じ。

[43a62d59ee9f05db194539c71e113601] Started GET "/" for 172.20.0.2 at 2018-07-28 05:00:26 +0000
[43a62d59ee9f05db194539c71e113601] Cannot render console from 172.20.0.4! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
[43a62d59ee9f05db194539c71e113601] Processing by Rails::WelcomeController#index as HTML

*1:ここではNginxのアクセスログの設定を省略しているが、ちゃんとRequestIdを記録しておこう https://github.com/webuilder240/nginx_proxy_uuid_rails/blob/master/containers/nginx_tls/nginx.conf#L15

Rails + SQLServer 航海日誌:実際に運用している編

実際にSQLServerRailsを利用して、アプリケーションを運用した知見をまとめておく。 この記事の続き。

webuilder240.hatenablog.com

いきなりまとめ

  • DB周りで何か問題あったとすればMySQLとの違いはユニーク制約の違いくらい.
  • 逆に言えば他でハマるようなことはなかった。
  • 1年近くこの構成で5〜6個近いアプリケーションを運用しているが、特にDB起因での障害とかで大きなものはいまのところはない*1
  • AzureでMySQL / PostgresqlのRDSっぽいサービスがリリースされたので、AzureからSQLServer一択ということではなくなり旨味がかなり減った。
  • 前回話していたSQLServer for LinuxをDockerで利用すれば開発環境のDBも困らない状態に。
  • 動作スピードについては未検証・未計測だが大きな差異はないと思ってる。
  • MySQLSQLServerを両方をメンテするのは辛いので、事情がなければサポートするRDBMSは1つだけにしたほうがいい。
  • 最近は特に事情がなければ、Azure Database for MySQLを基本的には使ってる。

Azure Database for MySQL / PostgreSQLについて

まずはこの一番大きなトピックからです。 みなさんご存知かと思いますが、かねてから噂されていたサービスがPreviewですがリリースを発表しました。

azure.microsoft.com

こちらもDB無停止でのスケールアップが可能なので、AzureでSQLServerを利用する理由が1つ減ってしまったように感じています。 まぁSQLServerに比べると割高なのは否めないですが… 現状MySQL / PostgreSQLで動かしているようなサービスの場合は、こちらに移行させるのが一番いいと思います。*2

PHPアプリケーションサーバーをWebAppsで幾分ラクラクに運用できることを考えても、 大型WordPressを運用する環境でAzureも検討材料の1つとして候補になるのではないかなーと思っています。

ただし、AWSでいうRDS相当なので性能がAuroraくらいのものかというとそういうわけではないです。

開発DBについて

DockerでSQLServerを運用できるということは前回軽く話していたけど、 実際に試してみて問題なく使えそうなので、現在開発環境のDBとして利用しています。 あとはCIをSQLServerでも回したかったので、そちらでもMSSQL Dockerを使っています。

CircleCIでSQLServerMySQLでテストを回している話

まだCircleCI1.0を利用している勢なのですが、現在開発中のアプリケーションでは両方のRDBMSでテストを回しています。 この辺については少し旬が過ぎてしまった感もあるのですが、また今後ブログにしたいと思います。

そもそも複数のRDBMSをサポートするのしんどい話

弊社ではまだまだプロダクトの中でそこまで分析系のクエリを書いていないので、まだまだマシだと思っているけど、 union allはどうしてもActiveRecordでは使えない(複雑なArelのコードを書くことで実現できるだろうが、どっちにしろメンテコストは高くつく)ので、 基本的には生でSQLを書かなければ行けないと思ってて、そういう箇所が数箇所だけある。

そこに関してはどうしてもSQLServerMySQLでは使用できるSQL文に多少違い・挙動の違いが存在するので、どうしてもコードをMySQLSQLServerで分けて書くということをしなくてはならない。 とは言え、自分は生SQLが得意なエンジニアではないのを自覚しているし、いまのところは2箇所で済んでいるけど、コレが増えるとまずいなぁとも感じている。 そもそもダッシュボードの項目部分だったり、分析クエリに関してはRDBMSではなくTresureDataなどの分析基盤などを別途利用した方が良さそうな感じはしている。

さいごに

  • 複数のRDBMSをサポートするのしんどいし、サービスとしてそこまでメリットがないと思っているので、SQLServerのサポートはすっぱり捨てて全部MySQLにまとめたいですね。

*1:Solidus使っててなにかバグ引いた人はいたっぽいけど...

*2:かなり翻弄されてしまっている感が…

PAY.JP Platformを試してみた

最近、弊社でもひいきにしているPAYさんから、 これまで開発中だったお待ちかねのプラットフォーマー向けのサービス、 PAY.JP Platformをローンチしたという知らせを聞いたので、早速試してみることにした。

PAY.JP Platformとは

例えばEコマースプラットフォームサービスのように、商品を販売するショップと購入を行うカスタマーのビジネスでは、ショップの登録・クレジットカード審査・管理から、ショップと購入者間の決済、そしてプラットフォーマーとショップへの入金処理までをAPIで組み込むことができます。 他にも各種シェアリングエコノミーサービス、CtoC/BtoC/BtoBサービスのプラットフォーム、クラウドファンディングといった、さまざまなマーケットプレイス型のビジネスユースの決済として活用することが可能です。

とにかくまぁ、公式でも説明しているので、詳しくはそちらを参照してほしい。

pay.jp

類似サービスにStirpeがあるけど、これのConnectという機能がStirpeのプラットフォーマー向けの機能になっていて、 これが似たようなプロダクトであると言える。

マーチャントという言葉について

ちょっと公式の説明で出てきた、マーチャントとプラットフォーマーという言葉だけど、 マーチャントという言葉がわかりにくい気がしたので、以下のように解釈するとしっくり来るということを補足しておく。

マーチャントとは、プラットフォームを利用して商品を販売したり、クラウドファウンディングであれば資金を得る起案者である。 つまり、プラットフォームを利用して、販売したりサービスを提供するユーザーの事を指している。

ほとんど既存のAPIの利用が可能

既存のPAY.JPのサービスとの差分だけど、支払い部分に関しては、殆どないと思ってOK。

ざっくりやることを書くと、

  • ① サービスでPAY.JPのAPIを利用して、マーチャントを作り審査を行えるようにする。
  • ② マーチャントのトークンを利用して、決済を行う

①は、ココの審査はPAY.JPが行うものなので、審査自体をプラットフォーマーができるようにする。 だけなので、自分たちで審査を代行したりみたいなことはやらなくてOK。多分プラットフォーム向けに対応する場合、新規に実装する部分の8割はこの部分だろう。 これらの画面に関してはAPIが現状提供されているので、APIを利用して自分で実装する必要がある。

②に関して言えばPAY.JPが絡む部分の書き換えはあまりない。 ちゃんとマーチャントのトークンを取得できるようにしておき、Charge Objectに決済手数料を設定するだけでOK。

こんな感じである。 プラットフォーム対応を行う際に決済部分のエンドポイントを切り替えたりということはなく、 既存のAPISDKに対してすこし拡張するだけで対応が完了できる。

他社製品との違い

基本的には他社製品である、Stripe Connectとコア機能はおなじようなものに感じる。 現状PAY.JP Platformのほうがローンチ直後というものあるのだろうが、 Stripe Connectよりもシンプルであること、安いことをウリにしているような印象を感じた。

いくつか注意点もある

現在も大絶賛開発中なので、いくつか現状にない機能もあるので導入前に確認してみることを推奨する。

通知用Webhookがマーチャント単位でも、プラットフォーマー向けにもない。

定額課金ないし、プランに対して platform_fee を設定することができないので、 まだ定額課金に対してプラットフォームの使用料を請求することができない。*1

弊社ではPAY.JPの定額課金に頼った実装構成になっているので、PAY.JP Platformを利用するには、PAYさんが対応してくれるのを待つか、 PAY.JPの定額課金を利用するのではなく、自社で定額課金を管理する実装を書くかの2択になると思う。

今すぐに導入を急いでいるわけでもまぁないので、様子見でも良いのだろうけど、 他の事情があって、PAY.JPの定額課金を利用するのをやめて、自社で定額課金を管理する実装を書く事を検討しているので、 どっちでやっていくかは現在もお悩み中という感じ…

*1:今後対応予定だが、PAYさんはこのあたりの仕様をどうするかを現在悩んでいるとのでした。

メッセージングアプリの永続化サービス・ミドルウェアの選定について

色々有識者の方に相談に乗ってもらったのでシェア。

要件的にはまず完成を目指して、チューニングはちょっとあとからということで進んでいたのだけど、 せっかく作るのだから少ない労力である程度の規模までは戦えるようにしたいよねーくらいなレベルで、少し考えて永続化するミドルウェア・サービスを選定してみることにした。 *1

RDBMS

まずこれは一番扱いやすいしすでに資産としてある。 技術者も多いので真っ先に思い浮かぶんだけど、有識者と相談して、

  • 「メッセージングで利用するのであれば、すぐに限界来るだろうし...」
  • トランザクションが不要な要件ではもったいない気がする...」
  • 「とりあえずちょっと考える時間あるし、他あたってダメそうなら…」

というまぁ当たり前の結論になって次の選択肢を検討した。

Redis

これも候補として上がってきた。 Pairy何かが最初期はRedisでチャットのメッセージを保存していたようだ。(後にDynamoDBに移行している。)

techblog.timers-inc.com

techblog.timers-inc.com

あとは、これは組み合わせで利用する用途だけど、まずアプリケーションはRedisにメッセージを書き込んで、 一定時間毎にRDBMSなんかにBulkInsertするような仕組みもいいね、という話にもなった。

ただデータの永続化が難しいこと、AzureのRedisCacheでクラスターをシュッと立てるにしてもかなりお高い値段なのと、管理が大変そうなので、現実的ではない... なので、「Redisだけ」という選択肢はすぐに外れた。 また、SidekiqあたりでRDBMSへの書き込みを非同期に行うケースも結構ありなのなとは思った。

HBase

正直、キャッチアップしている余裕もないしスモールスタートできなそうということで候補からはあっさり外した。

DynamoDB

プライシングでもスモールスタートできるし、 データベース構造でもメッセージングに最適だと思っていて、初めて登場してからしばらく立っていて実装も枯れている。*2 RubySDKもあり、今回の案件で自分がが求めているものに一番近いように感じた。

aws.amazon.com

ただ、社内的な制約があって弊社ではメインのクラウドサービスにAzureを利用していて、 できればAzure内のサービスで完結させておきたいという気持ちがある。そこで候補に上がったのがCosmosDBだった。

CosmosDB

AzureでのDynamoDBの代替サービスだけど... 自分は100%そうじゃないと思っていて、理由としてはCosmosDBが複数のAPI(接続方法)に対応しているからである。 その中でサポートしているものから、候補を模索していくことにした。

docs.microsoft.com

MongoDB API

結構最終候補くらいまで残っていた。

docs.microsoft.com

理由はRDBMSよりもスケールアウトしやすいのと、そこまでの整合性が必要ではなかったこと。 Mongoはメインで利用している言語(Ruby)のサポートもそれなりにあって、結構知見があったことから。 ただ、CosmosDBでもいくつかの接続APIがあり、それが異なることによって、スループットの差が「多少」生じるとのことだった。 Mongoはスキーマレスだけど、大量Write,Readのあるようなチャットに向くソリューションかどうかわからないので、一旦他の方法を検討してみることに。

Cassandra API

MongoDB互換だけでなく、Cassandra互換も用意されていた。

docs.microsoft.com

Cassandraの構造が一番メッセージングに適している、かつマネージドなので非常に良い選択肢として模索していた。 が、Cassandraの構造からメッセージングサービスのデータ構造を設計するに妙に難しさを感じていて、 心が折れてしまったので、ほかを当たってみることにした。

あとは扱えない技術者が少ないのがネックかも。

Firebase Firestore

Firebase RealTime Databaseのクエリ周りをいい感じにしたプロダクト。

firebase.google.com

RealTime Databaseよりもクエリ周りが便利なのと、WebフロントエンドやiOS, AndroidSDKからも書き込みが直接可能で、 Realtime Database共々、チャットアプリを作成するのによく利用されているイメージがある。

更に永続化だけではなく、 フロントエンドから読み込みを行うことでリアルタイムで追加・更新・削除処理が簡単に行えるので、 Websocketあたりのミドルウェアを自前で用意する必要もなくなった。 永続化のサービス・ミドルウェアの選定というところから少しスコープが外れているかもしれないが、これは本当に大きかった。

ただ、永続化サービスという意味ではちょっと違うかもしれない…?

いろいろ考え、有識者に相談した結果

RDBMSとFirebase(Firestore)の併用案にひとまず落ち着いた。 基本的にはメッセージの参照・読み込みをFirestoreに、それ以外のリソースに関してはAPIを経由して、RDBMSを参照するようにしている。 メッセージの書き込みをAPIを経由して書き込み、APIサーバから非同期でFirestoreにWriteを行っている。

やはり、チャットの大部分を占める読み込みをFirestoreに任せることで、 Websocketの実装や管理が不要だったのが一番大きいかなと。

別にFirestoreだけでもいいじゃんということなんだけど、Firestoreだけでの運用は、

  • やっぱりベンダーロックインが怖いこと。
  • フロントエンドから色々書き込むのは処理が煩雑になりがちになってしまうこと。
  • 既存のアプリケーションの1機能として入れるので、サーバーサイドを通したほうが実装と管理がラク。
    • チャットに投稿できるかどうかをサーバーサイドで判定してから、Firestoreに流すとかやりたい。
  • RealtimeDatabaseに比べてクエリ周りで便利になったとは言え、検索周りでAPIあったほうが今後便利では

ということになりこの案を採用して現在は動いている。*3

そんな感じで、現在はメッセージングアプリケーションを作っている最中だ。 サーバーサイドの観点では結構手抜き構成にしてあったのが幸いして、 苦労しがちなフロントエンド周りに時間を掛けることができて非常に助かっている。

*1:いきなりチャ社の新アーキテクチャのような設計は難しいし、そんな時間がそもそもない。用途的にも2〜3年で1億メッセージも行かないでしょうと踏んでいる。

*2:ソースとしてWikipediaが良いかはわからんが、2012年に出来たとのこと。Amazon DynamoDB - Wikipedia

*3:有識者・僕ともに、サーバーサイドエンジニア寄りだったというのと、既存の資産があったので。

Railsでのお手軽なキャッシュ戦略

キャッシュの主な動機は、

  • パフォーマンスの向上
  • より多くのトラフィックを捌けるように
  • サーバー・リソースの節約

が主だとおもう。 今回、Webメディアの運用が決まったあたりで、サーバーサイドのキャッシュを真面目に考えてみたので記録を簡単に残しておく。

キャッシュで気をつけるべき点

キャッシュで気をつけるべき点はもうキャッシュコントロールの一言に尽きる。 これを気をつけていい感じに取ってパフォーマンスを向上させるのが今回のお仕事。

主にRailsでのキャッシュ戦略について

  1. Railsでフラグメントキャッシュを取る
  2. Railsでページキャッシュを取る
  3. Nginxでキャッシュをとる

あまり調査 -> 実装 -> リリースまでの時間がなかったことから、お手軽に対応できそうな方法を中心に検討してみることにしました。

今回、CDNでのキャッシュ戦略は取っていないが、有効な手段だと思う。 後日検証を行って追加する予定である。順を追って簡単に説明する。

フラグメントキャッシュ

キャッシュコントロール

キャッシュ効果 ◯

Railsで一番ポピュラーに取られている(と思われる)キャッシュ手法である。

ただ、どうしてもWebアプリケーション(というかUnicornやPuma)で処理を行う以上、 前段のNginxで処理を完了させるキャッシュの方式よりも効果は少ない。 とはいえDBへのアクセスは減らせるのと、キャッシュコントロールの難易度が他に比べるとかなり楽にできるので、 ログイン処理のような動的コンテンツがあるようなWebページでも導入が容易だと思う。

その他のやりやすさ

他にもサーバーサイド側でデバイス判定して、 HTMLの出し分けだったりでどうしても動的な処理がサーバーサイド側で発生する場合でもそこまで心配せずに導入することが可能。 また、HTMLの一部分だけをキャッシュするという手法も取れるので他の手法に比べて考える事、 キャッシュを取ることによってできなくなることはほとんど無いように感じる。

ページキャッシュ

キャッシュコントロール

RailsRuby)側でのファイル操作が可能なので、記事を更新したときに更新した記事だけの、ページキャッシュを削除したりということがひとまず可能。 後述するNginxのキャッシュよりもキャッシュのコントロールが幾分ラク。 ただどうしても静的ファイルとして出力するので、色々柔軟性には欠ける。

キャッシュ効果 ◎

静的ファイルを直接nginxから呼び出せば効果はばつぐんだ。

その他のやりやすさ

サーバーサイドでデバイス判定して、ページ構成を変更しているようなページを処理するときには注意が必要。 それぞれで異なるページキャッシュを作らないと行けないし、PC用のページキャッシュは必ずPCに、SP用のページキャッシュを必ずSPに返すために工夫が必要だし、 ドキュメントだけを眺めてもそのやり方は書いていないので、自前で実装と言うか工夫が必要になる。 後述するnginxのキャッシュの方がオフィシャルにデバイスによって取得するキャッシュキーを変更できる機能を持っている分、nginxのキャッシュの方が良いのではないか?と思っている。

nginxのキャッシュ

キャッシュ効果 ◎

当然ながらキャッシュの効果は絶大。 Railsのページキャッシュとのスピード面の計測はしていないけど、まぁ変わらないのではないかと思っている。

キャッシュコントロール ×

ただキャッシュのコントロールについては良くないと思っており、 どこかを更新した際には、nginxのすべてのキャッシュファイルを削除するしか無いと思っている。(どのファイルがどの記事のキャッシュかを判別する情報がない) ただ、NginxでキャッシュをとりつつRailsでSessionのブラウザCookieベースで管理する場合、 色々工夫が必要で大変なので、CookieベースのSession管理を取りやめRedisやDBでSessionをStoreする方が良いかもしれない。

結局どの戦略を取ったか

動的に便利にやりたい部分があったというのと、実装までに期間が余り取れなかったという理由もあって、ひとまずフラグメントキャッシュで様子を見てみようということにした。 動作として特に問題が発生していないのと、結構潤沢にサーバーを使える環境というのもあったので、この方法で対応した。

そして、1000万以上PVくらいのWebメディアを半年くらい運用していたのだけどとりたてて問題は発生しなかった。 とりあえずDBアクセスをなくすだけでも効果は絶大なんだな、ということを実感しました。

予算等の見直しや要件に変更が入った場合は2や3への変更を視野に入れる必要もあったのかな、と思っている。

まとめ

  • キャッシュは色々便利だけど、最初はなるべくキャッシュは利用しない方向でできるだけ頑張る
  • 最初は簡単なのでフラグメントキャッシュから始めるといいです。
    • nginxのページキャッシュやRailsのページキャッシュの導入はその後検討でいいと思う。