Trelloのアーキテクチャ

http://nodeup.com/fiftyfour

1 comment | 0 points | by WazanovaNews 3年弱前


Jshiike 3年弱前 | ▲upvoteする | link

Trelloのアーキテクチャについてのアップデートです。2012年1月にブログで紹介されたものと、昨年11月の最新状況をまとめてみます。

まずは、当初のアーキテクチャ:

UIをクライアントサイドで生成し、プッシュでの更新を受け入れるシングルページアプリ。Client/ServerともにJavaScript、2011年5月以降はCoffeeScriptで書いている。

1) The Client

TrelloのサーバはHTML、クライアント側のコードをほぼ扱っていない。Trelloのページは2Kのシェルで、一つの圧縮されたJavaScriptファイル(サードパーティのライブラリと圧縮したCoffeeScriptとMustacheのテンプレを含む。)とCSSファイル(インライン化された画像を含むLESSのソースを圧縮したもの。)からなるクライアントアプリを引っ張ってくる仕組みになっている。クライアントアプリは250K以下のサイズで、AmazonのCloudFront CDNにキャッシュされているので、遅延なく配信できる。それなりに帯域が確保できる環境であれば、アプリをブラウザで0.5秒ほどで立ち上げることができ、それ以降はキャッシュが効いた状態になる。平行して、最初のページのコンテンツはAJAXデータ読み込みをし、サーバとWebSocket接続を確立している。

Backbone.js (client-side MVC)

Backbone.jsは、サーバからviewとともに送られるmodelをレンダリングし、簡単に、

  • 1. viewで生成されたHTMLの中でDOMイベントをウォッチし、対応するmodelにあるメソッドに紐付け、そしてサーバと再度同期する。
  • 2. 変更のためにmodelをウォッチし、変更を反映するためにmodelのHTMLブロックを再度レンダリングする。

ことができる。これでメンテしやすいクライアントサイドになっている。更新とクライアントサイドのmodelの再利用をするために、クライアントサイドのmodelのキャッシュの仕組みをつくった。

HTML5 pushState

クライアントアプリ全体をブラウザのウィンドウで読み込むかたちになっているが、ページ遷移で時間を無駄にしたくない。ページ間の移動にはHTML5 pushStateを使っているので、ロケーションバーに適切で一貫したリンクを付与し、遷移に際し、データを読み込み、適切なBackbone.jsのコントローラに渡す。

Mustache

Mustacheの 'Less is more' のアプローチのおかげで、テンンプレとクライアントロジックがごっちゃに混ざってヒドい状態になることなく、テンプレコードの再利用ができている。

2) Pushing and Polling

リアルタイムアップデート自体は目新しくないが、ユーザ同士が共有して使うサービスなので重要な機能になる。

Socket.IO and WebSockets

ブラザがサポート(Chrome / Firefox / Safari)していれば、WebSocket接続を使い、サーバは他のユーザの変更を概ね1秒以内にブラウザにプッシュすることができる。

AJAX polling

[初期のアーキテクチャ図]

ファンシーな仕組みではないが、役に立っている。WebSocketをサポートしていないブラウザであれば、ユーザがアクティブであれば、数秒ごとに更新のためのAJAXリクエストをして、ユーザがアイドル状態になれば、10秒ごとにポーリングしている。サーバの設定のおかげで、HTTPSリクエストのオーバーヘッドはかなり小さく抑えられて、TCP接続をオープンにしているので、ポーリングすることである程度のユーザエクスペリエンスを担保することができている。 Cometも試したが、当時は安定してなかったので、フォールバックとして利用するのはあきらめた。

Trelloをローンチしてまもなく、急激なトラフィック増に対応できなかった際も、WebSocketをポーリングに切り替えることでことなきを得た。

3) The Server

Node.js

Trelloは大量のオープン接続を必要とするサービスであるので、イベント駆動型でノンブロッキングタイプのサーバが適していると判断し、Node.jsを試した。シングルページアプリのプロトタイプづくりにも適していることがわかり、下記の仕組みを付加して本格採用することにした。

HAProxy

webサーバのロードバランスにHAProxyを利用。マシン間のラウンドロビンでTCPのバランスをとり、それ以外は全てNode.jsに任せる。WebSocketをサポートできるように接続を長くオープンにし、TCP接続をAJAXポーリングに再利用する。

Redis

サーバプロセスで共有しなくてはいけないが、ディスクに整合性をもたせるほどでもない一時的なデータには、Redisを利用している。セッションのアクティビティレベルや一時的なOpenIDキーなどはRedisに保管され、もしそのデータが消失してもアプリ側で支障がでないようなつくりにしている。allkeys-lruを有効にし、5倍ほど広いスペースを確保して実行されていて、最近アクセスされてないデータは自動的に廃棄され、必要なときに再構築される。

Trelloの中でのRedisの最も面白い使い方は、modelへの変更をブラウザまで送るための、短いポーリングを利用したフォールバックである。オブジェクトがサーバ側で変更になった際、JSONメッセージを全ての適切なWebSocketに送って、クライアントに通知。そして同じメッセージを、変更の影響を受けるmodelのために長さの決まったリストに(リストに今まで何件のメッセージが貯まっているかも含め。)保管する。そしてクライアントがサーバにポーリングして、前回以降オブジェクトに変更があるかどうか確認すると、全てのサーバのレスポンスをパーミッションチェックのレベルまで処理できる。Redisは驚く程早いので、CPUに影響を与えることなく数千件のチェックを処理することができる。

Redisはpub/subサーバにも利用していて、オブジェクトの変更のメッセージを全てのサーバのプロセスに送る役割を担っている。

MongoDB

StackOverflowのチームにヒアリングすると、SQLサーバを利用していても、パフォーマンスをだすためにほとんどのデータは非正規化したかたちで保管していて、必要なときのみ正規化していることがわかった。MongoDBでは、素早く書き込みするためのリレーショナルDB機能(例えば、任意のjoin)をあきらめることで、より早く読込みができ、非正規化をうまくサポートできるようになっている。カードのデータを、データベースの一つのドキュメントに保管でき、かつ、ドキュメントのサブフィールドにクエリをかける(もしくはそれをインデックスする)ことができる。サービスが急成長しているので、読込み/書込みのキャパを調整できるのはありがたい。また、MongoDBはレプリケーション、バックアップ、復元にも便利。

ドキュメントの保管を厳格な仕組みにしていないことのもう一つのメリットは、DBスキーマ統合に煩わされることなく、同じDBに対して新しいTrelloのバージョンアップを実行できること。DB更新に際してサービスを停止する必要がほぼない。この方式は開発にとってもありがたい。バグのソースを探すのに、hg-bisect (もしくはgit-bisect) コマンドとリレーショナルテストDBを使っていると、テストDBをアップグレード/ダウングレードする(もしくは必要なプロパティで新しいものを用意する)ときの追加作業のせいで、とにかく時間がかかってしまう可能性がある。

続いて、2013年11月時点での最新の状況のアップデート。

WebSocketについてはwsを利用。

全てのユーザのアタッチメントは、Amazon S3に保管。

HAProxyではSSL Terminationを利用。

Amazonへ移行後に、ノードのクラスタ部分に問題がでた。リクエストを異なるワーカーに配布する際に、少数のワーカーが全てのWebSocket接続をさばこうとしてロードバランスが大きく崩れた。それを回避するために、HAProxyを各webサーバのローカルに置き、更に、ワーカーをまとめた一つのクラスタマスタの替わりに、2つずつのワーカーを担当する16個のクラスタマスタを用意して、HAProxyがそれらの16個のポートの面倒をみるかたちに変更した。

多用しているモジュールは、Async.jsUnderscore.jsExpressHogan.jsAWS SDK for JavaScriptも最近は利用している。

本番リリースの頻度は週3〜5回程度。緊急の修正は別途随時実施。パッケージプロセスでは、CoffeeScriptとLESSファイルをプレコンパイルして圧縮し、CDNにアップする。次に、静的アセットの準備もできれば、ステージングもしくは本番環境にアップ。9台の本番webサーバと2台のステージングサーバ。サーバ1台ごとに、SSH経由でtarballをアップし、プールから外してリクエストを止め、socketをkillし、ノードプロセスも一つずつkillしていく。プロセスが全部終了すると、再起動し、ローカルホストがリクエストにレスポンスをし始めるまでcurlし続け、そのwebプロセスをHAProxyに渡す。そして次のサーバの作業に取り組む。このプロセスは全て自動化されていて、かつ管理画面 (Graphiteベース) で見える化されているので、何かあればすぐにロールバックできるようになっている。近い将来には、APIを整理してクライアント側をサーバ側とは別にデプロイするかたちにすることで、モバイル/webクライアントがもっと頻度をあげて更新できるようにする。また大掛かりなクライアント側の改修も対応しやすくなる。

JASONログをlogstash+ Elasticsearch + kibanaで処理。数日前にChromeとFirefoxでスタックトレースがうまく取れてないことがわかり、Amazon CloudFrontはクロスオリジンで正しいヘッダーを返してくれないことがあったので、CloudFlareに移行したが、ルートドメインCNAMEで問題があり、最終的にサブドメインを採用することでやっとエラーのモニタリングが安定してきた。

Fog Creekの提供するオープンソースプロジェクトはこちら


[2013] ワザノバTop100アクセスランキング


#fogcreek #trello #開発スタイル #アーキテクチャ #redis #mongodb #aws #haproxy #socket.io #websocket #node.js

Back