大規模サービス技術入門読んだ

まとまりないけど、感想を簡単に。

RDBMSの分割方法の話から、大規模サービスでパフォーマンス改善するためにどうアルゴリズムを生かしていくかや、大規模サービスのWebインフラはこんな感じでやってるぞーということが書いてあってよかった。 

普通なら退屈なアルゴリズムの部分も、実戦ではこう使えるし、アルゴリズムの知識だけではダメで、それを応用してくことが大事ーということだったり、ハードやソフトウェアの進化でお安く富豪的に解決できる事もあるから、時にはナイーブな実装試して検証して、割り切ることも大事だぞーと話しててなるほどとなった。 

この本でも特に多くページを割いていた印象があるのだけど、大規模サービスではやっぱりAppサーバーよりもDBサーバーの方が大変でやることが多いのかーとなっていた。でも、n+1について言及がなくて、はてなの場合はそこまで悩む事はなかったのかと。もしくは基本的なところなので外したか?

サーバーの仮想化についても、わかりやすく説明していて、仮想化してあるとリソースを余す事なく使えていいですねーとなった。(さらに発展させるとDockerに)

大規模サービスとか運用したことないし、今後もしそういったサービスを運用する機会があった時にそもそも経験したことがなくて、この辺がめちゃくちゃコンプレックスみたいな感じだったのだけど、この本で幾分知識をつけれたので今後のインフラやミドルウェア選定、アプリケーション実装に前よりは少しは自信持てるかなぁ。

出版されて8年くらい経っているのだけど、陳腐化してる技術もそこまでなくて、割とコンピュータのベースにあるような技術やわりと今でも応用が効きそうな大規模サービスでのパフォーマンス改善のヒントがたくさん書いてあるので、もし本屋などで見かけたら読んで見るといいかもしれない。

さあ、次は何を読もうか。

WebAPI The Good Partsを再読した

そういえば昔に読んだな、 とかおもってまた引っ張り出して読んでいた。

Web API: The Good Parts

Web API: The Good Parts

改めて学びがあったトピックは

  • バージョン番号をどこに入れるといいんだっけ?
  • ページネーションの仕様について(相対参照・絶対参照)
  • オーケストレーション層(実質BFFと解釈している)についての話が簡単に
  • パブリックなAPIとして運用していく色々なTips(レートリミットなど)

このあたり。

オーケストレーションは昔のRebuildでも話したそうだけど、(今度また聴いてみる) やはりNexflixすごいよなぁとなった。

あとはセキュリティの話があったんだけど、だいたいこれはAPIにかかわらない内容でなるほどーとなっていた。

全体的に、パブリックにエンジニアにAPIが公開を前提として話を進めているためか、 イケてないURL設計とかレスポンス内容ってダサいから、特別な理由がない限り利用者の印象良くないからやめようぜよく言ってた気がした。

公開APIとしての運用面のノウハウ、エラーレスポンスをこうするとわかりやすいみたい話はあんまりネットなくて良い感じにまとまっているので、 APIを始めて設計したりする時に参考になるところはいくつかあるのでちょっとおすすめしたい。

Docker HubのSource Repositoryを変更したい場合

Docker HubでGithubあたりの自動ビルドを有効にしている時に、Source RepositoryのRename時に参照するGithubリポジトリ変更したい場合がある。 この変更を忘れると、Github上でエイリアスを設定していてもDocker Hubには適用されず、自動でビルドされない状態になる。

ではどうすればいいか

直接設定を変更する解決方法はない。

取るとすれば下記の2つが有力。

1. 潔くDocker Hub上のリポジトリを削除して、再設定する。

削除されることが許容できればこれが一番手っ取り早い。 削除されることのデメリットは、Docker Hub上のStar数がリセットされるとかそのくらいだとはと思う。

2. Docker Cloudでビルド先を設定して、Docker Hubではなく、Docker Cloudでビルドする

この辺を参照してみてほしいんだけど、面倒だし、Docker Cloudという別のサービス使わないと行けないのがとにかく良くないので、 特にこだわりがなければ、この対応よりも、前者のほうが簡単でラクでいいんでは。と思っている。

No way to change source project for automated build · Issue #313 · docker/hub-feedback · GitHub

なんでこれ調べていたか

ngx_mrubyのDockerイメージのベースイメージを新しいものに変更して、 特に問題なくMergeしてもらったのだけど、この時に1年くらい前から自動ビルドがされてないですねー。ということがわかって少し調べていた。

1年くらい前にタイミングでngx_mrubyはAuthor名が変わって、Github上のリポジトリ名が変更されていたのが原因だったらしい。

今回は面倒なので、2.を提案して対応していただいた。(その節はありがとうございました!🙏)

あまり日の目を見ないPassengerにスポットを当てる

Phusion Passengerとは

非公式にはmod_railsとmod_rackとも呼ばれている。 役割としても、Apacheにインストールして、そのまま動かす当たりはやはり、mod_phpとかと被ってる。

イマドキだと、PumaやUnicornRubyのAppサーバーとしての主流だけど、こっちは「nginx + php-fpm」に近いイメージ。

今だからというのはあるけど、PassengerはDockerが無かったりした時代ではある程度メジャーな手法だったんだろうし、何より当時としては簡単なインストール方法だったのだ。

メリット

ApacheやNginxのモジュールとして稼働する

これのメリットとしては、特別にRailsアプリケーション用のコマンドを用意する必要がない、 すなわちsudo service apache2 (nginx) restartでだけで再起動できるし、色々できるというわけだ。

複数アプリケーションを立てたときのメモリの有効活用

これは具体的に言うと、Passengerでは一定期間リクエストがないインスタンスに関しては終了させ、 またリクエストがあったときにインスタンスを生成するという仕組みを取っているので、トラフィックが少ないサイトを沢山まとめて1つのサーバーで管理したい時に重宝する。 この終了までの期間に関してはPassengerの設定で変更することも可能である。

定期的な終了による小規模アプリケーションの安定稼働

Unicorn当たりでよくあるパターンは、ActiveRecordなどのCacheをどんどん溜めてしまってメモリが溜まってしまうというものだ。 Pumaは本番で余り触ったことがないので、よくわからないのだが、Unicornの場合は、unicorn-woker-killer によって、 一定回数のリクエストや、プロセス単体のメモリ容量によってプロセスの再起動を行う対策を取るのが一般的ではある。

Passengerではリクエストが無いときにはインスタンスを終了させるので、小規模なアプリケーションの場合は定期的に再起動を行っていることになり、 メモリ解放について気を使うことは、小規模なアプリケーションに限って言えば他のアプリケーションサーバーに比べると少ないのかもしれない。

デメリット

Apacheに同梱されているので、静的ファイルを返すときでもApache + Passengerが動く

結構痛い。 なるべく余計な処理をさせないようにするには、 静的ファイルを返すときにはアプリケーションサーバー(Passenger)は動いてほしくないものである。 静的ファイルが多めのサイトを運用する場合は避けたほうがいいかもしれない。

一応対策もあるが、それは別途Nginxを立ててReverseProxyする方法。 静的ファイルはNginxから、動的に処理しないといけないところをApacheでという方法。 でもそれだったら、Unicornとか、Puma使えば良くないですかということになる…

ビジネス向けでないとDeploy時・再起動時にダウンタイムが発生する

コレはPhusion Passengerの機能制限。有料のビジネス版を利用しないとリスタート時にダウンタイムが発生する。 すなわちgraceful startができない。 どのくらいダウンタイムが発生して、どのくらいの時間かは未検証だけど、止まるということは頭に入れておいてほしい。

どういうときに利用するのか

止むにやまれない事情で、少メモリ環境で、複数アプリケーションを立ち上げる案件

インフラの費用がなかったり、管理上1つのVMインスタンスで管理したいという時。あんまりないねー。

小規模なRedmineを自前で立てる必要がある場合

いや、もうDocker使えよ…という話なんだろうけど、複数のRedimineアプリケーションを1つのサーバーに立てる時に何かと重宝する。 あと、単純に資料が多いというの公式のインストールマニュアルがあるしね。

積極的に使う理由が余り見当たらない…

普通にアプリケーション作っているんだったら、やっぱり、PumaかUnicornが無難。 元々Passenger使っていたとか、複数のRailsアプリケーションを1つのサーバーに同居させて管理する場合のみ、Passengerを使うべきであるというのが自分の出した答えである。

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