Disqus のスケール - Django 編

やり直し。2010 年の Django Con のスライドより。

Disqus は多くのサイトに組み込まれているサービスのため、メンテナンスによる停止が難しい。

>>> サーバの構成

Server Architecture

  • エッジロードバランサーに HAProxy: heartbeat 構成。レポートが素敵。
  • HTTP ゲートウェイキャッシュに Varnish
  • Django サーバとして Apache + mod_wsgi: 30 台ぐらい。メモリリークを防ぐために maximum-requests をセット。Ganglia で監視
  • キャッシュに memcached: 25 台ぐらい
  • PostgreSQL ロードバランサーに HAProxy/PgBouncer: コネクションプール用。
  • PostgreSQL: 10 台ぐらい。Slony-I で非同期レプリケーションとフェイルオーバー。
  • ログは syslog-ng: pgFouine でスロークエリーをロギング。

全部でサーバ 100 台ぐらい。他にユーティリティサーバ 15 台、後は HAProxy と heartbeat 用に 20 台ぐらいの構成。Varnish と syslog-ng を除けば Django のデプロイ手順に掲載されているような教科書通り (良い意味です) の構成です。(なんで Apache なの?nginx + gunicorn 速いよって突っ込まれてますね

>>> データベースのパーティショニング

Django はアプリケーションレベルで簡単に垂直パーティショニングができます。アプリケーションごとに DB 分けたり、DB をアプリケーションレベルでルーティングできます (ドキュメント)。水平パーティショニング (a.k.a シャーディング) もアプリケーションで書ける (なるほど・・・)。

>>> キャッシュの削除

Django は QuerySet の結果をキャッシュするけど、それがかなりメモリを消費する。なので SkinnyQuerySet を作った (たぶん Johnny Cache の方が有名?)。view に付き QuerySet を再利用しない場合はキャッシュしない方が良い。そして再利用しない場合がほとんど。

>>> 更新はアトミックに

save() はスレッドセーフではありません。update() の方がよきに計らってくれる (スライド: save vs update)。django-tips-and-tricks より。余談ですが、 Django 1.6 より アトミック更新 がサポートされますよ!あと、Django 1.6 からは autocommit です。Django 1.6 を使いましょう。

>>> キャッシュフレームワーク

Memcached のバックエンドには python-memcached ではなく pylibmc を使う。1.3 以降で pylibmc 使えるようになっています。理由は書いてありませんでしたが、ベンチ結果とか、ノンブロッキングリクエストが出来るとか、TCP nodelay 等の色んなオプションがサポートされたりしています。

>>> CI

  • Fabric を使ってデプロイ
  • Jenkins (Hudson) と Selenium (django-selenium) を使ってテスト (7万行, カバレッジ 73%, 実行に要 20 分) を自動化
  • ポストコミットフックを使って Pyflakes のような簡単なテストを実行
  • 簡単に以前のバージョンに戻せるようにしている

>>> バグトラック

Sentry を利用 (旧 django-db-log)。結果を redmine に。

基本に忠実な感じで。2年前のスライドで、他にも色んなハックが載っていましたが、ほとんど Django 1.6 でカバーされていました。1.6 楽しみですね!

Disqus のスケール - Django で月間80億PVを処理する

私が把握してる限り Django で一番大きなサービス Disqus のスケール (執筆時点ではサービスダウンしてる)。元ネタは Scaling Django to 8 Billion Page Views です。月間80億PV45k req/s のほぼすべてのトラフィックを Django で処理しているとのこと。抄訳になるかな。

WAF は高速開発とパフォーマンス新しい人が入ってすぐに開発に参加できることとカスタマイズ等のトレードオフがあります。この記事ではそのトレードオフである高速開発とパフォーマンスをどう両立させるか、Disqus のノウハウが紹介されています。

>>> なぜ WAF (Web Application Framework) は遅いのか

最初に思い浮かぶのは、アプリケーションに必要ではないボイラープレート (django.contrib とか?) や不要なコードがあるため。そもそも Django の思想が Python 同様 "バッテリー同梱 (batteries included)" のため、Django は他の Python 製 WAF よりボイラープレートが多いです。Disqus 曰く "実は言語や WAF は遅さにあまり関係ない" それより "ネットワーク内の他のサービスとの通信" のオーバーヘッドが原因とのこと。これは一般的によく言われてることですし、大規模になればなるほどそう。Disqus の場合は PostgreSQL, redis, Cassandra , Memcached 等のサービスが使われているそうです。DB へのスロークエリーやネットワークのレイテンシーは Django のようなボイラープレートによるオーバーヘッドを軽く上回ります。この待ち時間を回避する一番メジャーな方法はキャッシュの使用です。Django では キャッシュフレームワーク を使用します。Django でキャッシュを使うのは簡単ですし、バックエンドに Memcached を使うと充分高速化できます。

はい。ここまでは一般的な話です。こっから。

>>> 45k req/seq を処理する

キャッシュしたところで、html テンプレートや json のレンダリング、等、Django ミドルウェアの処理など毎秒 45000 回も不要な処理が発生します。45k req/seq のうち同じレスポンスがあるならいちいち処理させるのはもったいない。結果が同じものはレスポンス全体をキャッシュし、他のすべての処理をスキップします。

Varnish の使用

Varnish は LB (HAProxy) と Django バックエンドの間に HTTP ゲートウェイキャッシュとして導入。同じレスポンスを処理する際は Django サーバではなく Varnish が返す。以前はブラックボックス的に Varnish を使ってたけど、色々と試した結果 Django サーバへのリクエストは 45k req/seq のうち 15k req/seq (64% 削れたらしい) になったとのこと (そんなにあるんだ・・・てか結局 15k req/seq はさばくんですね)。

で Django と Varnish について Django Con US 2013Varnish User Group で発表されたようです (タイトル 'Varnish によるスケール' に変更したい)。

要約すると・・・

  • 適宜 304 Not Modified を返す。クライアントにはキャッシュしてるやつを使ってもらう。Firebug とか Page Speed とかで確認できます (参考)。Django 側では django.views.decorators.http.last_modified デコレータ (本家ドキュメントでは django.views.decorators.http.condition を使うか、django.views.decorators.http.etag との併用が推奨されてます) を使って Last-Modified ヘッダーを返せます
  • django.views.decorators.cache.cache_control で同様の機能が提供されているが、HTTP パースや WSGI、ミドルウェアの処理等が実行されてしまう。
  • TTL は 5秒ぐらい (短く) 設定する
  • Varnish 初期状態では Cookie ヘッダーをキャッシュしない。sessionid 毎にキャッシュするよう変更する。
  • URL 設計とか大事
  • Varnish OOM 頻発するしクラッシュ多かった。。。4-6 時間に一度 Varnish をリスタートしている。それと SSD 使おう! (え・・・
  • DDoS 対策としても使える

よし。Varnish 勉強しましょう!(Django えっ...

virtualenvwrapper でプロジェクト管理とか

ちょうど 1 年前にリリースされてた機能だけど、恥ずかしながら知らなかった。@t2y 先生が紹介していらっしゃって、後で試してみようと思ってたのですが...。virtualenvwrapper ってインストール時にグローバルな site-packages に放り込んで、後は。。。って感じ。あんま見直したことなかったのですが、プロジェクト管理以外にも結構色んな機能が追加されてるんですね。mkvirtualenv, workon だけじゃない!

ドキュメントを和訳してくださっている @t2y さんに多謝。基本的な機能やコマンド (と思いこんでいた mkvirtualenv, workon, etc...) についてはvirtualenv, virtualenvwrapper, pip を使う方法 by @IanMLewis さん, Pythonを取り巻く開発環境 (PyCon JP 2012資料 #pyconjp) by @ymotongpoo さんの記事が参考になります。

>>> mkvirtualenv

v3.3 から新しいオプションが増えてたんですね。これも知りませんでした。

  • -a <path/to/project>: プロジェクトに新しい env を関連付ける (後述)
  • -i <library_to_install>: env 作成と同時に、インストールしたいライブラリを指定する
    mkvirtualenv -i django -i django-celery-with-redis <env_name> のように複数指定できる
  • -r <path to requirements file>: env を作り requirements.txt 等指定したファイルに記載したライブラリを一括でインストールできる。個人的にこれは一番うれしいかも
    mkvirtualenv -r ./requirements.txt <env_name> ラクダーーー

>>> mktmpenv

これも v3.3 から。ユニークな名前で env 作成してくれる。

>>> cdvirtualenv, cdsitepackages

これは前からある。結構使う。私は activate, deactivate はよく書き換えるので、後述の cdproject と一緒に使うと幸せになれる。

>>> lsvirtualenv, lssitepackages

これも前からある。あんま使ったことない。workon, pip freeze -l でなれちゃってる。でも出力結果が細かかったり、lssitepackages は入力速くなりそう。

>>> add2virtualenv

これも前から。でも使ってる人あんま見たことない。add2virtualenv <path/to/external_libs> で、指定したディレクトリをカレント env に追加する。Django とか PySide 等でかいプロジェクトをいちいちチェックアウトしたくない場合を想定してあるらしい。これはシンボリックリンク、PIP_DOWNLOAD_CACHE, pip2pi とか色々解決方法があるけど、プロジェクトごとにライブラリのバージョン違ったりするので、今のとこ env 毎に全部インストールしちゃってる。

>>> toggleglobalsitepackages

v3.3 から。これ知らなかったのは辛い...。グローバル site-packages に入れてるのは、bpython, tox, py.test, mock, virtualenvwrapper などなど。setup.py で test_requires に書いてあるやつとか, requirements.txt にも書いてないライブラリーは基本グローバルに放り込んでる。グローバルが汚れるのはもちろん嫌だけど、env もなるべく汚したくない。使いたい時だけグローバル site-packages をパスに追加する。動作は virtualenv でやってたのと同様に "no-global-site-packages.txt" を作成したり、削除したりしてるだけです。ありがたい。

>>> mkproject

本題。v3.3 から。もともとプラグインで提供されてたらしい。1 プロジェクト 1 env
mkproject [-t <project_template>] [mkvirtualenv options] <env_name>

環境変数で指定された PROJECT_HOME にプロジェクトディレクトリを追加、かつ WORKON_HOME に対応する env を作ってくれる。env 名はプロジェクトディレクトリ名になります。

プロジェクト作成時用の素敵なテンプレート機能も用意されています。テンプレートは github, bitbucket, django, flask 等色々。例えば mkproject -t github <env_name> ってやると github 上に env 名のプロジェクトを作ってくれます。mkproject -t django <env_name> ってやると、django-admin create project <env_name> を実行して、スケルトン作ってくれます。

>>> setvirtualenvproject

setvirtualenvproject <virtualenv_path> <project_path> で既存の env とプロジェクトディレクトリをひもづけてくれる。

>>> cdproject

アクティブな env に関連付けられたプロジェクトディレクトリに飛んでくれる。コンソール開いて workon <env_name>; cdproject ってやると env 適用して、対応するプロジェクトディレクトリをカレントディレクトリにしてくれるという。素敵ですね!

たまにはグローバルな site-packages に入れっぱなしのライブラリーも見直してみるもんですね。