2011-12-31

翻訳: Google App Engine で、アプリケーションの応答をよくする 15 の方法

大規模ウェブサービスのプラクティスを紹介している、High Scalability の記事「15 Ways To Make Your Application Feel More Responsive Under Google App Engine」を訳しました。


記事は Java 前提で、私は Python で Google App Engine を使っているのですが、それでも面白いなと思ったので、有効な方法は使っていこうとしています。ついでなので翻訳したものを残しておくことにしました。




軽量な査定用フィードバックサービスを提供している Small Improvements が、Performance issues on GAE, and how we resolved them という素晴らしい記事を書いている。どのようにして、ほとんどのリクエストを 300ms から 800ms でさばき、memcache が効いていなくても2秒、速いページなら150ms 程度でさばけるようになったかの事例になっている。全体として、ものすごく速くしているわけではないけれど、PaaS として GAE が気に入っているなら、検討すべき内容だろう。

パフォーマンスが悪いときに、できることが限られているというのが、PaaS のやっかいなところだ。でも、Small Improvements の人たちは賢くかつ辛抱強く改良をして、詳細な方法とその結果を提供してくれている。アドバイスはGAE に特化したいるけれど、別の状況にも当てはまるだろう。以下に、彼らが実施した、パフォーマンスの小さな向上(small improvemens) の15の方法をあげる。

  1. App Engine が遅い日もある、と知る。 App Engine はパフォーマンスが悪い日がある。設計段階で、潜在的にレイテンシが変化することを考慮しておく必要がある。常にベストな状況であることを想定しないこと。
  2. クエリよりも GET する。 Datastore から、ID で直接取得するのは 250ms 。クエリするのには 800ms かかる。
  3. GET のバッチと、スレッドローカルキャッシュを組み合わせる。キーごとに個別にGETせず、ひとつのバッチにまとめて GETする。結果を HashMap に保存しておいてレンダリングに使う。モジュール化したアプリケーションでは、内部状態の小さな断片を、引き回すのは避けたいだろうから、ThreadLocal に置いておき、いつでも各モジュールからアクセスできるようにする。
  4. クエリの結果は Memcache に保存する。 appstats ツールを使ってどのクエリが、キャッシュするに値するほど使われているからを見つけ出す。
  5. ログイン前にmemcache を温める。 ユーザが完全にログインするより前に Ajax を発行して、データをキャッシュしておく。
  6. ログイン後に memcache を温める。 ユーザがログインしたら、次に読み出しそうな5ページをキャッシュしておく。
  7. 救いようがないほど遅いときには、つなぎのページを表示する。 最初のクエリの時間を測り、もしも遅ければより速いページへ遷移させる。そのページが閲覧されている間に、元のページをキャッシュする。
  8. memcache を使えないといは、非同期に読み出すことで、行儀よくしょぼくする。クエリが遅いなら、その場では全情報をレンダリングしない。クエリが終わったところから順番に描画する。
  9. アクティブでないときにも再キャッシュ。ユーザがしばらくアクティブでない状態が続いたら、キャッシュが失効しそうなデータを再キャッシュするようなリクエストを、ブラウザから飛ばす。こうすればユーザが戻ってきたときにキャッシュに残っている。
  10. データを非正規化する。「このマネージャの部下は誰か」のような 300ms かかるようなクエリは、結果を予め保存しておき、人同士の関係が変わったときに再計算する。こうすれば GET で結果を取得できるので、速い。
  11. JAR を使う。 これ最強。何千ものクラスがあったら、ロードするのに 400ms はかかる、これはディスクアクセスがあるからだ。すべてのクラスを JAR に入れておけば、ロード時間が飛躍的に向上する。
  12. ウォームアップリクエスト。 できるかぎり GAE の warmup 機能を使う。コードパスを吟味して memcache にデータを書き、主な UI を描画し、鍵(キー)となるクエリを発行し、ログインをシミュレートし、時刻の計算をする。こういう処理は VM とデータをウォームアップする。
  13. 書込は後回しにする。 時間がかかったり、たくさんのクエリを発行した後は、書込処理をタスクキューにいれる。
  14. 非同期でメールする。 メール送信はタスクキューから行う。
  15. 非同期でクエリする。複数のクエリを並列に実行する。

2011-12-12

six

2011 Python アドベントカレンダー のエントリです。Python 2 と Python 3 の差異を吸収するラッパ集として six というライブラリを触ってみました。

Python 3 に同梱されている 2to3 というツールは、ひとつの Python 2 のソースから、 Python 3 用のソースを自動生成します。したがって、もとの Python 2 用コードと Python 3 用コードのふたつのバージョンを持つことになります。

一方、six がやろうとしているのは、ひとつのソースコードを Python 2 と Python 3 からも呼び出せるようにすることです。提供されるのは、基本的な定数や、ラッパ関数です。

文字列リテラルのフェイク

Python 3 と言えば文字列です。

# coding: utf-8

print(u'うほ')

Python 3 ではこれはエラーになります。six.u() 関数は、文字列リテラルのフェイクになってくれるそうです。

# coding: utf-8

import six
print(six.u('うほ'))

実行してみましょう。

$ python2.7 foo.py

ãã»
$ python3.2 foo.py
うほ

えー!six.u() の引数は、 unicode-escape でエンコードされた文字列じゃないとだめだそうです。「うほ」の場合は "\\u3046\\u307b" です。これはあんまりじゃないかと。しかも、

$ python2.7 foo.py
うほ
$ python3.2 foo.py
\u3046\u307b

ええぇぇ。そりゃまあ、リテラル表記を関数で代用するんですから、大変だと思いますよ。

見なかったことにして、six.b() に行きましょう。これは b'…' のフェイクです。

x = six.b('AB')
print(x[0]+x[1])
$ python2.7 foo.py
AB
$ python3.2 foo.py
131

oh… このあたりの挙動は 石本さんのブログ で書かれていますが、リテラルのフェイクは難しいようです。

気をとりなおして別の関数を見ましょう。それは文字列なのか、それともバイト列なのかを知りたいとき、isinstance とかを使うわけです。が、Python 3 では、このあたりの型や継承関係が変わっています。

x = u'foo'
if isinstance(x, basestring):
    print(x.encode('ascii'))

y = b'ABC'
if isinstance(y, str):
    print(y.decode('ascii'))

Python 2 では、上のように書いたりしたんですが、Python 3 では basestring は存在しませんし、str はバイナリを表しません。そこで、six の text_type と binary_type を使います。

x = six.u('foo')
if isinstance(x, six.text_type):
    print(x.encode('ascii'))

y = six.b('ABC')
if isinstance(y, six.binary_type):
    print(y.decode('ascii'))

実行してみると…

$ python2.7 foo.py
foo ABC $ python2.7 foo.py b'foo' ABC

お、これはわりとちゃんと動きますね。text_type は unicode (py2) か、str (py3) のエイリアスになっています。binary_type は同じく str と bytes です。

辞書のイテレータ

Python 2 の iteritems, iterkeys, itervalues は、Python 3 では items, keys, values に名前が変わっています。そして、Python 2 の items, keys, values はなくなっています。

この差異を吸収するのが、 six.iteritems などの関数です。

for k,v in six.iteritems({'spam':99, 'ham':0}):
    print(k,v)
$ python2.7 foo.py ('ham', 0) ('spam', 99) $ python3.2 foo.py ham 0 spam 99

辞書のメソッドを使わずに、six.iter* を使うことで、必ずイテレータを返すようになります。

表示が違うのは、print(k,v) の解釈の違いです。 Python 2 では、タプル (k,v) をプリントしています。一方、Python 3 では、k と v ふたつの引数をとって、それぞれをスペース区切りで表示します。

six には、これを吸収する関数もあります。

six.print_(1,2,3)

実行すると…

$ python2.7 foo.py
1 2 3
$ python3.2 foo.py
1 2 3

まとめ

言語仕様が変わって、オフィシャルに 2to3 が用意されるくらいですから、ラッパくらいでは吸収し切れない、というのがよく分かりました。個人的な感想は、2to3のほうが筋がいいんではないか、と思いました。

あしたは @nonNoise さんにバトンタッチです。よろしくお願いします。

2011-12-13 追記: pre タグがおかしかったのなど、諸々修正。blogspot って wysiwig エディタでコード書くのがいまいち。

2011-12-04

mock.patch をデコレータ、コンテクストマネージャとして使う

patch はコンテクストマネージャでもあるので、with を使った書き方ができます。with でつくられたブロックの中でだけ、モックが機能するようになります。
>>> from mock import patch
>>> with patch('random.random') as m:
...     import random
...     random.random()  # もうモックになっている。戻り値は Mock オブジェクト
...     m.return_value = 0.5
...     random.random()  # 戻り値は 0.5
... 
<mock.Mock object at 0x423ed0>
0.5
>>> random.random()  # モノホンの random 関数
0.84432022018162511
with ブロックの中に入った時点で、random.random はモックになってしまいます。必須ではありませんが、 as m のように書いて、変数 m でモックへアクセスできます。patch().start() の戻り値と同じです。
with ブロックを抜けるときに、パッチャー の stop() メソッドが実行されたのと同じ状態になり、モックではなくなります。
複数のモックを使いたいときには、with ブロックをネストするわけですが、それはちょっと鬱陶しいですよね。インデントが深くなりすぎるかも、なので。そんな時は 標準ライブラリの contextlib.nested を使います。
>>> from contextlib import nested
>>> from mock import patch
>>> from __future__ import with_statement
>>> with nested(patch('random.random'), patch('random.randint')) as (m, n):
...     m.return_value = 0.5
...     n.return_value = 3  # random.randint のモック
patch は、デコレータとしても使えます。
>>> from mock import patch
>>> @patch('random.random') 
... def func(m):
...     import random
...     print random.random()
...     m.return_value = 0.5
...     print random.random()
... 
>>> func()
<mock.Mock object at 0x429730>
0.5
>>> random.random()
0.94254256426687633
関数を patch でデコレートすると、元の関数にMock オブジェクトが渡されるようになります。上の例では、第1引数 m が random.random に対応するモックです。このモックが使われているスコープ内、つまりこの関数内でモックが有効になっています。関数を抜けると(正確には、関数を抜けて、デコレートしている外側のスコープを抜けると)、モックではなくなります。したがって、 func() を呼び出した後で、random.random() を呼び出すと本来の戻り値になります。
unittest モジュールを使ったテストを書く時には、テスト関数に、デコレータからモックを受け取るように書きます。
class MyTest(TestCase):
 @patch('random.random')
 def test1(self, m):
  import random
  m.return_value = 10.0
  self.assertEqual(random.random(), 10.0)
というわけで mock オブジェクトの機能でした。機能を知ることと、使えることはまた別なので、実際にテストでどうするのがいいか、は、別の機会に。私自身、試行錯誤しております。