2014年のウェブシステムアーキテクチャ

(Monitoring Casual Talk in Kyotoで発表してきたので、ブログエントリにまとめ直しました)

2013年はインフラ周りの技術的な進化が大きく、いくつかのエポックメイキングな概念と実装が産まれました。個人的には特に以下の2つが大きいと思っています。

  • AWSの本格普及期
  • DockerとImmutable Infrastructure

これらを踏まえて、2014年のウェブシステムの進化の方向性を考えてみます。また、それによるモニタリングへの影響もあわせて考えます。だいぶ長くなってしまったので、急ぐ人は最後に結論をまとめましたので、そちらからどうぞ!

2013年という時代背景

AWSが本格普及期を迎えているのは、言わずもがなのことで、Re:Inventでの246件という膨大のセッション数などにその勢いが表われています。 また、DockerはLXC (LinuX Container)をベースとしたコンテナ型仮想化技術で今年の夏前にリリースされ、現在、開発が活発に進められています。Immutable Infrastructureは、このところを周りで話題になった概念で一度構築したサーバに変更を加えず使いすてにしよう、という概念です。詳しくは、今さら聞けない Immutable Infrastructureに良くまとめられていますので参照してください。

これらを踏まえると、2014年のウェブシステムアーキテクチャは、状態を持たない Stateless Server と状態を持つ Stateful Server の2種類で扱いが変っていくだろうと考えています。

Stateless Serverの扱い

状態を持たない Stateless Server は、アプリケーションサーバーやリバースプロキシサーバー、キャッシュサーバー、ロードバランサーなどのことです。これらのサーバーは、本質的には状態を持つ必要がないため、いつ壊れてもよいし、再作成も容易にすることができます。そのため、負荷に応じて必要十分な数だけフレキシブルに増減させることが、もっとも効率的です。サーバーを増やしたり、減らしたり、設定を更新したり、切り戻したりするオペレーションコストをいかに減らすかが勝負どころです。

前述したImmutable Infrastructureという概念は、これらの状態を持たないサーバーに対し、変更を加えない、という制約を加えることで、このオペレーションコストの最小化を狙っています。これにより、自動的にサーバーを増減させる Auto Scaling や、アプリケーションをデプロイする際にアプリケーションサーバーを全て入れ替える Blue-Green Deployment といった手法が現実的に使えるようになります。

また、これまで Provisioningツール(サーバーをセットアップするためのツール)として、Chefpuppetなどが登場し進化してきましたが、これらのツールが目指していた冪等性(再実行しても結果が一定になる性質)の保証が不要となり、Chefやpuppetの設定を構築、維持するためのコストを減らすことができます。

冪等性が保証されている状態を構築したり維持することは、設定がすっきりと記述されて気持ちよいのですが、多くの人が使うにはそのためのコストは高すぎるという問題があると感じていました。そのため、Immutable Infrastructureという概念を取り入れることで、冪等性を気にする必要がない手法が使われるようになり、よりシンプル、かつ汎用的なツールによる環境セットアップ手法に回帰すると考えています。また、configspecのような軽量で多少抽象化されたツールが普及する可能性もあります。

Dockerの登場

AmazonNetflixで実践されているImmutable InfrastructreやBlue-Green Deploymentは、EC2上でAMIをイメージを作りつつ、インスタンスを起動停止させているようです。しかし、次のような問題があります。

ここにDockerを使うことで、起動・停止の高速化とコストの最適化を図ることができます。

Dockerを利用した開発プロセス

またDockerを利用することで、開発、テスト、プロダクション環境を全て同一のDockerイメージを使うことが可能となります。具体的なフローとしては、次のようになります。

  • 手元で開発しつつ、Dockerイメージを作成
  • Dockerイメージをイメージリポジトリに登録
  • JenkinsでCIテストする
    • テストが通るとデプロイ可能な状態に
  • Dockerイメージを本番サーバにデプロイ
  • 本番サーバで起動イメージを差し替えてDocker再起動

本番サーバへのデプロイや、Docker再起動、ロードバランサーへの組込みは、複数のホストを効率的に管理するためのクラスタ管理ツールの役割りとなります。

Cluster Management / Orchestration

Immutable infrastructureの実践により、サーバ増減や切り替えが頻繁に発生するようになり、クラスタ管理ツール(最近の用語だとOrchestration tool)の重要性が増大します。クラスタ管理ツールは、重量級のものから軽量級までいろいろな選択肢がありますが、基本的には、次の機能が求められます。

  • 同じ役割りを担う複数のホストを管理する
  • ホストが増減した時に、周辺システム(ロードバランサーなど)との協調動作させる

この領域は、現在様々な実装が表われてきているところで、どれか一つに集約する、というよりは、それぞれの環境に合わせて、いくつかのツールが共存することになるのでは、と予想しています。ここでは、個人的に注目しているツール群について紹介します。

  • Serf
    • シンプルなクラスタ管理ツール
    • ゴシッププロトコルによる協調動作
    • クラスタに組込む/外すなどのイベントをトリガーに任意のコマンドを実行できる
  • Apache Mesos
  • Flynn
    • Dockerを利用したPaaSのOSS実装
    • Mesosより軽量な実装
  • SmartStack
    • SOAによるアーキテクチャの構築を容易にするツール
    • Nerve .. サービス登録/発見
    • Synapse .. HAProxyベースでのルーティング

使い勝手のよいクラスタ管理ツールは、ロードバランサー(LVS, HAProxy, ELBなど)との協調動作が容易で、筋の良いオートスケーリングとService Discoveryのサポートがされたものになるだろうと考えています。実際にプロダクション環境に適用すると、Deploy直後の負荷の吸収など種々の実装上の問題を解決する必要が出てきますが、このような問題をうまく解決できるようなツールが生き残っていく可能性が高いと思います。

ログ管理

Immutable Infrastructureを実践するには、各ホストで状態を持ってはいけないので、ログのようなまさに状態が保存された要素はできる限り速やかに外に出す必要があります。そのためには、fluentdのようなログ転送ツールが全てのホストに配置されることは必須になるでしょう。

なにか問題が発生した際には各サーバにログインするのではなく、集められた中央ログサーバで処理することになりますので、ElasticSearch + Kibanaのような便利なデータ可視化ツールの重要性も増してくるでしょう。

Stateful Serverの扱い

これまで状態を持たないサーバについて考察してきましたが、状態を持つ Stateful Server となると状況は一変し、まだまだ泥臭いオペレーションが必要とされます。 Stateful Serverは、DBサーバ、ファイルサーバなどのデータソースですが、2014年以降は状態を持つサーバー群をいかにうまく扱うかが焦点となるでしょう。

ただ、ここ数年のハードウェア進化、特にSSDの普及と成熟化により、小〜中規模の用途においてはスケールアップで対処することが一般的となってきています。従来は、master-slave構成としていたものが、slaveへの参照を不要としmasterのみでの運用とすることで、開発上の懸念を減らしたり、オペレーションコストを下げる選択肢も一般的になりそうです。(参考: データベースのmasterとslaveの使い分けの話。2014年版 )

また、Amazon RDSで十分な用途もあるでしょう。Amazon RDSはマスターフェイルオーバーの時間が遅いことが最大の問題ですので、ここが克服されれば応用範囲はさらに広がりそうです。

一方、大規模な用途においては、やはりスケールアウトさせるしかなく、いかにデータ分割するか、参照負荷を分散させるか、というこれまでのオペレーションが鍵となります。

この領域で2013年に注目されたものは、FacebookMySQL Pool ScannerMySQLオペレーションの自動化を行うという未来を見せてくれています。具体的には、「レプリ投入」「マスタフェイルオーバー」「キャパシティプランニング」「開発用スペアの用意」といったあたりが自動化されているようです。

当面、MySQLのようなデータソースの扱いは鍵となりますが、MySQL Pool Scannerのようなものを構築管理することな並大抵のことではなく、人員を含め、相当にかっちりとした運用体制が必要となりそうです。

2014年はPercona Toolkitを核に、各種オペレーションをサポートするツールがいろいろ出てくることになるだろうと思います。

安定的なDBクラスタ運用を行うためのDBAの悩みはまだまだ続きますね。

2014年のモニタリング

最後に2014年のモニタリングについて考えます。モニタリングについても、Stateless ServerとStateful Serverで考え方を多少変えることが必要となりそうです。

Stateless Serverのモニタリング

Stateless Serverのモニタリングは、Auto-scaling機能、つまりホストの増減とそれに対応したロードバランサーとの協調動作が重要になります。各ホストのCPU usage, load average, memory usage, network loadなどを監視し、それをトリガーとして、ホスト増減を行い、合わせてロードバランサーとの協調動作を行う必要があります。また、ホストがどんどん新しくなるので、負荷観測の連続性をどのように確保するか、などメトリクス情報の保存や可視化に関する課題もあります。

アプリケーションサーバーのデプロイを管理するために、Dockerイメージ(やAMI、gitのハッシュ値)のバージョン管理や、CIによるテストの統合があるとより便利となるでしょう。ただし、このあたりは先のクラスタ管理システムと機能的に重複しますので、統合か棲み分けが必要となりそうです。

Stateful Serverのモニタリング

Stateful Serverについては、包括的な解が当面は存在しなさそうですので、従来通り、きっちりとモニタリングすることが重要となります。特にスケールアップ/アウトが必要なタイミングを察知することは重要です。

また、各種オペレーション用のツールセットとの連携ができるとより有効になるでしょう。例えば、ホスト障害が発生した際のレプリカ作成やバックアップからの復元を容易にしたり、サービスリリース前のキャパシティプランニングのサポートなどが求められます。

ここでの進化は、もうしばらく連続的なものとなりそうです。

システム全体でのリソース管理の重要性の高まり

またクラウド時代になり、リソース増減の柔軟性は上がっているため、システム全体でのリソース管理の重要性はますます増大するでしょう。例えば、負荷に対応したリソース消費増が容易となっているのですが、一度増強したシステムが改善されずに、その状態で放置されてしまう、というようなことが想定されます。また、EC2の例で言えば、適切なインスタンスタイプの選定、Reserved Instanceをタイムリーに調達することも、選択肢が増えている分、頭の痛い問題となりつつあります。

そのため、Stateless Server、Stateful Serverの両方について、リソース管理を適切に行ない、リソースが必要十分な状態を保持しつつ、投入したリソースが効率的に消費されていることを管理することが重要となります。アプリケーションについてスケールアウトで満足するのではなく、リソース単位あたりのパフォーマンスも重視されるようになると考えています。これまでアプリケーションをリソース単位あたりのパフォーマンスを管理することはそれほど重視されてきませんでしたが、2014年には徐々に重視されるようになるでしょう。

まとめ

長々と書いてきましたが、ここ数年のインフラ・クラウド周りの進化が2014年にはだいぶ形になるだろうな、と予感しています。ざっとまとめると以下のような予想を立てています。

  • アプリケーションサーバーなどの状態をもたないホストは使い捨て可能なImmutableなものになり、開発プロセスにも組込まれていく
  • データベースサーバーなど状態を持つホストは、まだまだ従来からの連続的な進化が続く
  • クラウドでリソースを柔軟に使えるようになったが、リソースを効率的に使うための取り組みが注目されるようになる

さて、2014年も引き続き注目ですね!

The Benchmark with Go REST API Server

I gave a presentation about lightweight REST API Server by Go, and performance comparison with Go, Perl and Ruby at GoCon 2013 autumn.

The slide about benchmarking result is as follows. This shows milliseconds per request with 10,000 sequential requests at various conditions, which are go/perl/ruby, messagepack/json, and mysql SQL query/innodb memcached plugin.

"direct memcached (innodb)" is direct accesses with memcached protocol to mysql, using innodb memcached plugin. "direct mysql" is direct accesses with SQL query to mysql. The target server was invoked on AWS m1.large instance, and a benchmark tool was invoked on another m1.larget instance. detail conditions of this benchmark are shown in the above slides. Also I uploaded these server implementations and the benchmark tool to https://github.com/stanaka/go-rest-api-server .

Conclusion

  • Go + memcached plugin + messagepack = Fastest!!
    • except direct query and direct memcached (innodb)
  • Go shows good performance and small footprint.
  • mysql library of Go needs to be improvement.
  • innodb memcached plugin works well especially for read queries.

Feedback

If there is objection or vagueness at benchmark conditions, please let me know. There may be mistakes about treating mysql libraries of go and ruby, which causes significant performance reductions. I'll revise the results if it would be considerable.

多段fluentd + mongodb のハマリ所

fluentdを多段構成にして、mongodbに出力するところでハマったのでメモ。

上の構成のように、各サーバにfluentd + out_forwardを置き、集約するログサーバにfluentd + out_mongoでmongodbに出力している場合に、上段のfluentdでbuffer_chunk_limitを10mより大きい値にしていると、エラーになることがあります。

まず、out_mongoでbuffer_chunk_limitを10m以内にしないといけない理由は、fluentdからMongoDBへ連携する際の注意点 #fluentdを参考にしてください。

ここで多段構成の場合、上流の buffer_chunk_limitが大きいと上流から大きなサイズのデータの塊が流れてくることがあります。それを受けとったfluentdはそれをそのままoutput pluginに流す実装となっているようです。そのため、末端のout_mongoだけ buffer_chunk_limit 10m としていても、上流から来たデータの塊をそのままmongodbにinsertしようとして、エラーとなるようです。

ちなみにこの場合のエラーメッセージとしては以下のようなものが出てきます。"next retry will be"といってますが、chunkが分割されるわけではないので、永遠に失敗し続けるわけです。

2013-02-22 13:46:56 +0900 [warn]: temporarily failed to flush the buffer, next retry will be at 2013-02-22 13:46:58 +0900. error="Exceded maximum insert size of 16,000,000 bytes" instance=260250280 time=1361508416

対策としては、末端にout_mongo (や、buffer_chunk_limit に制限を加える必要があるplugin)を使う場合、その上流の全てのfluentdでその上限値(out_mongoの場合 10m)に設定する必要がある、ということになりそうです。

設定を修正した後は、mongodbで8000lines/secぐらいまでの性能は確認できました。ちなみにこの時のIOPSが400〜500ぐらいだったので、IO性能はまだまだいけそう。前の記事でmongodbのinsert性能が低いような話を書きましたが、原因は別のところにあったので訂正します。

修正後の設定は以下のようになります。上流のfluentdのbuffer_chunk_limitもきっちり10mにしておきましょう!

  • 上流サーバ
<source>
  type tail
  format ltsv
  path /var/log/httpd/access_log.ltsv
  tag proxy.access_log
  pos_file /var/tmp/fluent.log.pos
</source>

<match **>
  type forward

  buffer_type memory
  # buffer_chunk_limit must be under 10m if out_mongo is used in downstream.
  buffer_chunk_limit 10m
  buffer_queue_limit 128
  flush_interval 1s
  retry_limit 10
  retry_wait 5s
  send_timeout 5s
  recover_wait 5s
  heartbeat_interval 1s
  phi_threshold 10
  hard_timeout 10s
  <server>
    host fluentd
    port 24224
  </server>
</match>
  • ログサーバ
<source>
  type forward
  port 24224
  bind 0.0.0.0
</source>

<match **>
  type mongo
  tag_mapped

  capped
  capped_size 10m

  nodes mongod
  database fluent
  collection debug

  buffer_type memory
  buffer_chunk_limit 10m
  buffer_queue_limit 1280
  flush_interval 1s
  retry_limit 10
  retry_wait 5s
</match>

fluentd + mongodb+ node.js でリアルタイムにグラフを描く

追記 2/22

毎回微妙に追記していますが、今回も追記です。最後にmongodbのinsert性能について80lines/secで厳しくなった、と書いてますが、環境か設定まわりがあやしいので訂正します。もうすこし検証してみようと思います。 → 検証して fluentd側の設定の問題であることが分かりました。詳しくは、http://blog.stanaka.org/entry/2013/02/22/171053

追記ここまで

最近は、fluentd + mongodb でログを蓄積していろいろ便利に使っているわけですが、数分に一回集計スクリプトを周したり、 GrowthForecast の画面をリロードしまくるのではなく、もっとリアルタイムで見たい! という欲求が募ってきたので、 node.js を使って実装してみました。( https://github.com/stanaka/realtime-log-visualization )

mongodb の Tailable cursor

fluentd からのログを(ほぼ)リアルタイムにブラウザ上に出すという実装は、これまでに mongodb + node.js を使って、 polling して最新のデータの取得を取得したり、 mongo-tail で一旦ファイルに書き出したり、という手法が実装されてきたようです。が、それらの実装では効率が良いとは言えず、またもっとリアルタイムにしたいので、より直接的に繋げたいですよね。

ここで mongodb のドキュメントを良く見てみると Capped collection に対して、 Tailable cursor という仕組みで、その collection への追加レコードを push で受け取ることができる機能があったので、それを利用してみます。そもそもこの Tailable cursor は、Unixコマンドの tail -f にインスパイアされたものとのことで、まさに期待する機能です。

今回は proxy のログを受け取って、ステータスコードごとのrequests/secをグラフ化するツールにしてみました。システム構成は、 となります。proxyからmongodbまでは、当然 proxy のログを LTSV形式 で出力して、そのまま mongodb に突っ込むのが基本ですね。

node.js から mongodb に接続するには mongoose を使います。 node.js からブラウザへデータを push するために WebSocketではなく、 SSE (Sever-sent event) を使ってみました。グラフの描画は、データをどんどん追加していけるグラフ描画ライブラリの Rickshaw を使ってみました。

コードを見る

コードは、 https://github.com/stanaka/realtime-log-visualization にありますが、主要部分を見ていきます。まず、SSEでデータのpushを受け取るエンドポイントは "/update" としています。tailable で最新レコードを追い掛けるためにも、まずはその時点での最新のレコードを取得します。

exports.update = function(req, res) {
  req.socket.setTimeout(Infinity);

  var stream;
  Log.findOne().sort({_id:-1}).slaveOk().exec(function(err, item){

そこから tailable を指定して、 stream を起こします。ここで、 gt で最新のレコードを指定しておかないと、頭から全レコードが push されて悲惨なことになるので注意しましょう。

    stream = Log.find().gt('_id', item._id).sort({'$natural': 1}).tailable().stream();

stream からデータを受け取ったら、 SSE でブラウザに push します。

    var messageCount = 0;

    stream.on('data', function(doc){
      messageCount++;
      res.write('id: ' + messageCount + '\n');
      var msg = JSON.stringify({
        status: doc.status,
        reqtime:  doc.reqtime,
        req:    doc.req
      });
      res.write("data: "+msg+"\n\n");
    });
  });  

あとは、SSE コネクションの確立処理と、切断時の対処を書いておきます。

  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  });
  res.write('\n');
 
  req.on("close", function() {
    stream.destroy();
  });
});

クライアント側では、EventSource オブジェクトを作って、 SSE で push されるのを待ち受けます。データが push されたら、グラフに描画します。

script(src='http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js')
script(src='http://jquery-json.googlecode.com/files/jquery.json-2.2.min.js')
...
script(type='text/javascript')
  var source = new EventSource('/update');
 
  source.addEventListener('message', function(e) {
    var data = JSON.parse(e.data);

    if(s = (data.status.match(/^(\d)/))[0]){
      counter[s] = counter[s] == null ? 0 : counter[s] + 1;
    };
    var now = new Date().getTime() / 1000;
    if(now > last_render + tv){
      var data = {
        "2xx": counter[2],
        "3xx": counter[3],
        "4xx": counter[4],
        "5xx": counter[5]
      };
      counter = new Array();
      last_render = now;

      graph.series.addData(data);
      graph.render();

実行画面はこんな感じです。時間とともに、リアルタイムにグラフが描画されていきます。データは、テスト用データを生成する generate_data.js から mongodbに挿入させています。

もうちょっとちゃんと実装すれば、アプリケーションのデプロイ時や障害発生時など、状況の変化をできるだけ早く把握したい場合に使える技になりそうです。

mongoose の Tailable cursor は、 node.js の stream で実装されているので、 polling に比べて負荷も低く、シンプルで美しい実装ですね。この実装をベースに、 fluentd + mongodb からのリアルタイムビジュアライゼーションをいくつか試してみようと思います。

まとめ

  • fluentd + mongodb + node.js でリアルタイムでグラフを描画させてみました
  • mongodb の tailable を利用した stream による実装でシンプルで効率的です
  • これでサーバーでのLTSVでのログ出力から、手元のブラウザでのグラフ描画まで一気通貫ですね!

このシステムで proxy のログの一部を 800 lines/sec ぐらいの勢いで流したら、 mongodb のところで一瞬で詰 ってしまいました。SSD を搭載したサーバにしておいたのに! 仕方ないので、 fluent-plugin-sampling-filter で流量を10分の1ぐらいにして凌ぎました。やはり mongodb のところがボトルネックになりますね。 ← ここのmongodbのinsert性能上限のところは数字があやしいので、訂正します。詳しくはコメント覧の議論を参照ください。 → 検証して fluentd側の設定の問題であることが分かりました。詳しくは、こちらも参照ください http://blog.stanaka.org/entry/2013/02/22/171053

流量の多いところで使うには、ここのボトルネックの解消が絶対に必要なので、よりパフォーマンスの高い方法を探ろうと思います。