人間とウェブの未来

「ウェブの歴史は人類の歴史の繰り返し」という観点から色々勉強しています。

ngx_mrubyのTCPロードバランシングを使ってFluentdのTCP通信とグラフ画像へのHTTP通信を動的に集約する

昨日、ngx_mrubyのTCPロードバランシング機能に対応した記事を書きました。

hb.matsumoto-r.jp

というのも、実は以下に説明するようなFluetnd+Norika+GrowthForecastを利用したスケールアウト型のシステムを簡単に作りたかったからです。

ということで、まずはプロトタイプ設計みたいなものをフワっと考えてみました。

実現したい事

まずやりたいこととして、

  • 1000台以上のサーバ群からのFluentdによるTCPデータをそれぞれの転送元サーバに紐づく任意のバックエンドサーバに振り分けたい
    • 転送元サーバ群はそれほど増減しない
    • バックエンドサーバをスケールアウト型に増やす場合に、転送元サーバの設定は変更したくない
  • 転送されたログは転送先サーバ内でグラフ化されるので、任意のHTTPクライアントによって取得したい転送元サーバのグラフ画像を適切なバックエンド(Fluentdから見た転送先)サーバからレスポンスしたい
    • http://.../server001.phy.example.jp/...みたいなリクエスト
  • サーバ間の紐付け管理データは単一のファイルやデータベースで楽に管理したい
    • 設定反映はnginxのreloadレベルでやりたい
  • 設定変更やスケールアウト時に複数箇所を修正にまわるような運用は避けたい
  • スケールアウトは同一のバックエンドを並列に増やして任意のタイミングで管理データを書き換えるだけ
  • 業務の用途にあわせてサーバ単位で流すログ解析サーバを分けられる
    • このサービスはこのバックエンドにして、この別のサービスは同じバックエンドに入れない、とか

ということで以下のアーキテクチャと、ngx_mrubyのTCPロードバランシグ対応を行って実現しようと考えたわけです。

f:id:matsumoto_r:20151104152113p:plain

このアーキテクチャの特徴として、

  • とあるサーバからのFluentdのTCPデータの転送先とHTTPクライアントからのとあるサーバのグラフ閲覧はngx_mrubyで動的に紐付けられる
    • 転送元サーバやHTTPクライアントから見たら単一のサーバIPアドレスなのでクライアントの設定がシンプル
    • TCPデータのひも付けは転送元であるサーバIPアドレスとバックエンドのサーバIPアドレスにする
    • HTTPクライアントからのグラフ閲覧はURLベースでバックエンドのサーバと紐付ける
  • とあるサーバとバックエンドのひも付けはTCPとHTTPで二種類あるがそれを単一の管理ファイルやDBで管理する
    • スケールアウト時はバックエンドサーバを用意して管理ファイルを修正するだけ
  • サービスや業務用途単位で、バックエンドのログ解析サーバをまるごと分離できる
    • とあるバックエンドにサービス用のログと社内用途のログが混ざらないメリットがある

もともとは、用途に合わせてFluentdやNorikra、GrowthForecastを別サーバに分離しようと思っていたのですが、そもそもFluentdとNorikraとGrowthForecast間の通信や処理が大量に発生するのと、運用者ができるだけわかりやすい構成にしつつ運用・保守が簡単にできるように、分離せずにまとめたうえでバックエンドとしてスケールアウトできるようにしました。

実装例

サーバー紐付け管理データがJSONの場合

例えば以下のような、sever_map.jsonを用意します。ここは、DBでもなんでも良いです。ngx_mruby起動時に取得してメモリに置いておけるような所にデータを置いておきます。

また、そのデータに対してngx_mrubyでAPIを書いて、さくっとデータを更新できるようなI/Fを用意しておくのも良いですね。

{
  "server001.phy.exmaple.jp": {
    "stream": [
      "fng001.phy.exmaple.jp:12345",
      "fng001.phy.exmaple.jp:12346"
    ],
    "http": [
      "fng001.phy.exmaple.jp:80"
    ]
  },
  "server002.phy.example.jp": {
    "stream": [
      "fng002.phy.example.jp:12345"
    ],
    "http": [
      "fng001.phy.example.jp:80"
    ]
  }
}

TCPのFluentdロードバランシングの実装イメージ

簡単のため、エラー処理などは省略しています。

沢山存在する転送元Fluentdからport24224に対してTCPにのったデータが飛んできます。Fluentdの転送の仕方をHTTP/1.xにしてもよかったのですが、今回はTCPを扱う事にしています。

また、転送元のIPアドレスとバックエンドのIPアドレスを、TCPロードバランシングレベルで任意に紐付けたいという要求があります。その紐付けは、HTTPでの紐付けにおいても流用したいです。

それを実現するためには、ngx_mrubyをTCPロードバランシング機能に対応させ、以下に説明するような実装が必要となります。(もし既存のソフトウェアでできる方法があればぜひ教えて下さい!)

stream {
  # 転送元と転送先の紐付けデータをnginx起動時に読んでおく
  mruby_stream_worker_init_code '
    Userdata.new.server_map = JSON.parse(open("/path/to/server_map.json", &:read))
  ';

  upstream dynamic_fng {
    # dummy address
    server 127.0.0.1: 24224;
  }

  server {
    # Fluentdの受け口、転送元はこのサーバに転送してくる
    listen fng-proxy.phy.exmaple.jp:24224;
    mruby_stream_code '
      c = Nginx::Stream::Connection.new("dynamic_fng")
      map = Userdata.new.server_map
      remote_host = c.remote_ip
      # 転送元IPアドレスにしたがって転送先を決定する
      upstreams = map[remote_host]["stream"]
      # 転送先がマルチポート・マルチプロセスの場合はランダムに決定
      c.upstream_server = upstreams[rand(upstreams.length)]
    ';
    proxy_pass dynamic_fng;
  }
}

HTTPプロキシの実装イメージ

続いて、HTTPクライアントから適切にグラフ画像を取得するための実装です。

クライアントからURLとして、例えばhttp://.../graph/server001.phy.exmaple.jp/test001.exmaple.jp/count_2xxがGETされるとします。

http {
  # 転送元と転送先の設定を読んでおく
  mruby_worker_init_code '
    Userdata.new.server_map = JSON.parse(open("/path/to/server_map.json", &:read))
  ';

  server {
    # グラフ閲覧の受け口、HTTPクライアントのレスポンスはすべてここに集約される
    listen fng-proxy.phy.exmaple.jp:80;

    location /graph {
      mruby_set $upstream '
        # URLベースで取得した転送元サーバのホスト名を得る
        req_server = Nginx::Request.new.uri.split("/")[2]
        map = Userdata.new.server_map
        # ホスト名から対象のグラフ画像があるバックエンドサーバを決定する
        upstreams = map[req_server]["http"]
        upstreams[rand(upstreams.length)]
      ';
      proxy_pass $upstream;
    }
  }
}

これらによって、バックエンドのfluentd+norikra+gfのサーバが複数になっても、管理データをメンテしてnginxに反映するだけでよくなります。

また、大量のサーバ群にいるtd-agentの設定はその都度変える必要がないし、クライアントからのHTTPアクセスもちゃんとgraphを作っているGrowthForecastにプロキシされて適切なグラフが返されます。

まとめ

以上のように、TCPやHTTP接続をうまくngx_mrubyで集約して、スケールアウト時の変更を楽にしようという設計と簡単なコンセプト実装を考えてみました。

ざっとTCPロードバランサを調べてみても、柔軟にsrc-ipとdst-ipから動的にバランシングを決定したりできるようなものが見つからなかったので、ngx_mrubyでTCPロードバランシングを対応させることで実現しようとしてみました。

これによって、Fluentdの転送元やHTTPクライアント側からみると、単一のサーバへのアクセスになるので、クライアントに優しいシンプルな受け口になったと思います。

後は、

  • FluentdをHTTPで転送した場合とくらべて性能はどうか
  • 通信を集約するngx_mrubyのスケールはどうするか
    • もし必要になったら現状サーバを増やして設定を転送元の設定に追加する
    • その際にはデータを外に持った方が良いかも
  • 大量にある転送元サーバと転送先サーバの紐付けのメンテにおいて、転送元が増えてもうまいことバックエンドを決定するデータの持ち方を考える

あたりをもう少し考えたいなぁと思います。

今後は、実際に色々と設計を見直しつつ、大きな環境で試していこうと思います。