効率性と拡張性

□未翻訳

□翻訳中

□翻訳完了(Omi Chiba)

■レビュー(中垣健志)

効率性と拡張性

Efficiency and Scalability

scalability

web2pyは簡単にデプロイとセットアップができるように設計されています。これは効率性や拡張性に妥協しているという意味ではなく、拡張するには調整が必要な場合があるということです。

web2py is designed to be easy to deploy and to setup. This does not mean that it compromises on efficiency or scalability, but it means you may need to tweak it to make it scalable.

この節ではローカル・ロードバランスを提供するNATの後ろに複数のweb2pyインストレーションをする場合を考えて見ます。

In this section we assume multiple web2py installations behind a NAT server that provides local load-balancing.

この場合、ある条件化であればデフォルトの設定でweb2pyは動作します。具体的には、それぞれのweb2pyアプリケーションの全てのインスタンスが同一のデータベースサーバーと同じファイルにアクセスする必要があります。後者の条件は次のフォルダを共有させることで実現が可能です。

In this case, web2py works out-of-the-box if some conditions are met. In particular, all instances of each web2py application must access the same database servers and must see the same files. This latter condition can be implemented by making the following folders shared:

1.

2.

3.

4.

applications/myapp/sessions

applications/myapp/errors

applications/myapp/uploads

applications/myapp/cache

共有フォルダはファイルロックをサポートしなければいけません。方法としてはZFS(ZFSはSun Microsystemsに開発され、推奨される選択肢です。)、NFS(ファイルロッキングを有効にするためにnlockmgrデーモンを実行する必要があるかもしれません。)、またはSamba(SMB)です。

The shared folders must support file locking. Possible solutions are ZFS (ZFS was developed by Sun Microsystems and is the preferred choice.), NFS (With NFS you may need to run thenlockmgr daemon to allow file locking.), or Samba (SMB).

web2pyフォルダ全体やアプリケーションフォルダ全体を共有することも可能ですが、ネットワーク帯域使用量が無駄に増加するだけなので良い方法ではありません。

It is possible to share the entire web2py folder or the entire applications folder, but this is not a good idea because this would cause a needless increase of network bandwidth usage.

共有される必要があるがトランザクションの安全性(ひとつのクライアントだけがセッションファイルに一度にアクセスする、キャッシュはグローバルロックを常に必要とする、uploadsとerrorsは一度で書き込み/多くのファイルの読み込み)を必要としないリソースを共有ファイルシステムに移動することでデータベースの負荷を減らすため、上記の説明はとても拡張性が高いと考えます。

We believe the configuration discussed above to be very scalable because it reduces the database load by moving to the shared filesystems those resources that need to be shared but do not need transactional safety (only one client at a time is supposed to access a session file, cache always needs a global lock, uploads and errors are write once/read many files).

理想的には、データベースと共有ストレージは両方ともRAID構成であるべきです。データベースを共有フォルダと同じストレージに保存する間違いをしないでください、新しいボトルネックを作ってしまいます。

Ideally, both the database and the shared storage should have RAID capability. Do not make the mistake of storing the database on the same storage as the shared folders, or you will create a new bottleneck there.

状況に応じて、追加の最適化を実行する必要があり、それについては後述します。具体的には、どのようにこれらの共有フォルダを一つずつ排除するか、そして代わりに関連するデータをデータベースに保存する方法を説明します。これは技術的に可能ですが、かならずしも良い方法ではありません。それでも、そうする必要がある理由があります。そのような理由の一つは共有フォルダを自由にセットアップできない場合です。

On a case-by-case basis, you may need to perform additional optimizations and we will discuss them below. In particular, we will discuss how to get rid of these shared folders one-by-one, and how to store the associated data in the database instead. While this is possible, it is not necessarily a good solution. Nevertheless, there may be reasons to do so. One such reason is that sometimes we do not have the freedom to set up shared folders.

効率性のテクニック

Efficiency Tricks

web2pyアプリケーションコードはリクエストのたびに実行されるので、コードの量を最小限にしたいです。以下の方法があります:

web2py application code is executed on every request, so you want to minimize this amount of code. Here is what you can do:

  • 一度だけmigrate=Trueを実行してから全てのテーブルにmigrate=Falseをセットする。

    • adminを使ってアプリケーションをバイトコードコンパイルする。

    • 制限のあるキーセットを使ってcache.ramをできる限り使用する、そうしないと任意に拡大してしまう。

    • モデル内のコードを最小化する:ここに関数を定義せずそれを必要とするコントローラに定義する、もっといい方法としては、モジュールに関数を定義して必要に応じてインポートして使用する。

    • たくさんの関数を同じコントローラに入れないで、たくさんのコントローラにいくつかの関数を定義する。

    • セッションを変更しない全てのコントローラや関数でsession.forget(response)を実行する。

    • web2py cronを使わず、代わりにバックグラウンドプロセスを利用するようにする。web2py cronは大量のPythonインスタンスを起動し過剰にメモリを使用する。

    • Run once with migrate=True then set all your tables to migrate=False.

    • Bytecode compile your app using admin.

    • Use cache.ram as much as you can but make sure to use a finite set of keys, or else the amount of cache used will grow arbitrarily.

    • Minimize the code in models: do not define functions there, define functions in the controllers that need them or - even better - define functions in modules, import them and use those functions as needed.

    • Do not put many functions in the same controller but use many controllers with few functions.

    • Call session.forget(response) in all controllers and/or functions that do not change the session.

    • Try to avoid web2py cron, and use a background process instead. web2py cron can start too many Python instances and cause excessive memory usage.

データベースでセッション処理

Sessions in Database

セッションフォルダの代わりにデータベースにセッションを保存するようにweb2pyに指示することが可能です。セッションの保存に同じデータベースをすることになるとしても、それぞれの個別のアプリケーションに対して設定が必要です。

It is possible to instruct web2py to store sessions in a database instead of in the sessions folder. This has to be done for each individual web2py application, although they may all use the same database to store sessions.

データベース接続を作成

Given a database connection

1.

db = DAL(...)

接続を確立する同じモデルファイル内に次のコードを追加するだけで、データベースにセッションを保存することができます。

you can store the sessions in this database (db) by simply stating the following, in the same model file that establishes the connection:

1.

テーブルがまだ存在していなければwe2pyはweb2py_session_アプリケーション名という以下のフィールドを持ったテーブルを内部で作成します。

If it does not exist already, web2py creates, under the hood, a table in the database calledweb2py_session_appname containing the following fields:

1.

2.

3.

4.

5.

6.

Field('locked', 'boolean', default=False),

Field('client_ip'),

Field('created_datetime', 'datetime', default=now),

Field('modified_datetime', 'datetime'),

Field('unique_key'),

Field('session_data', 'text')

"unique_key"はクッキー内のセッションを特定するためのuuidキーです。"session_data"はcPickledセッションデータです。

"unique_key" is a uuid key used to identify the session in the cookie. "session_data" is the cPickled session data.

データベースアクセスを最小限にするために、必要が無い場合はセッションを保存しないようにするべきです:

To minimize database access, you should avoid storing sessions when they are not needed with:

1.

session.forget()

この変更により"sessions"フォルダはアクセスされなくなるので共有フォルダとする必要は無くなります。

With this tweak the "sessions" folder does not need to be a shared folder because it will no longer be accessed.

セッションが無効の場合は、sessionform.acceptsに渡すべきでは無く、session.flashやCRUDも使用できない点に注意してください。

Notice that, if sessions are disabled, you must not pass the session to form.accepts and you cannot use session.flash nor CRUD.

HAProxyで高度なロードバランサ

HAProxy a High Availability Load Balancer

HAProxy

もし複数のweb2pyプロセスを複数のマシンで実行する必要がある場合は、データベースにセッションを保存したり、キャッシュする代わりに、sticky sessionsを利用したロードバランサを使用するオプションがあります。

If you need multiple web2py processes running on multiple machines, instead of storing sessions in the database or in cache, you have the option to use a load balancer with sticky sessions.

Pound88とHQProxy89はsticky sessionsを提供する二つのHTTPロードバランサとリバースプロキシです。ここでは商用VPSホスティングでより一般的な後者について説明します。

Pound88 and HAProxy89 are two HTTP load balancers and Reverse proxies that provides sticky sessions. Here we discuss the latter because it seems to be more common on commercial VPS hosting.

sticky sessionsは、一度セッションクッキーが発行されたら、ロードバランサが常にクライアントからのセッションへのリクエストを同じサーバにルーティングします。こうすることでファイルシステムを共有せずにローカルのファイルシステムにセッションを保存することができます。

By sticky sessions, we mean that once a session cookie has been issued, the load balancer will always route requests from the client associated to the session, to the same server. This allows you to store the session in the local filesystem without need for a shared filesystem.

HAProxyを使うには:

To use HAProxy:

始めに、Ubuntu テストマシンにインストールします:

First, install it, on out Ubuntu test machine:

1.

sudo apt-get -y install haproxy

二つ目に、設定ファイル"/etc/haproxy.cfg"を以下のように編集します:

Second edit the configuration file "/etc/haproxy.cfg" to something like this:

1.

2.

3.

4.

5.

6.

7.

8.

9.

10.

11.

12.

13.

14.

15.

16.

17.

18.

19.

20.

21.

22.

23.

24.

25.

26.

# this config needs haproxy-1.1.28 or haproxy-1.2.1

global

log 127.0.0.1 local0

maxconn 1024

daemon

defaults

log global

mode http

option httplog

option httpchk

option httpclose

retries 3

option redispatch

contimeout 5000

clitimeout 50000

srvtimeout 50000

listen 0.0.0.0:80

balance url_param WEB2PYSTICKY

balance roundrobin

server L1_1 10.211.55.1:7003 check

server L1_2 10.211.55.2:7004 check

server L1_3 10.211.55.3:7004 check

appsession WEB2PYSTICKY len 52 timeout 1h

listenディレクティブはどのポートで接続を待つかをHAProxyに指示します。Serverディレクティブはプロキシサーバがどこにあるかを指示します。appsessionディレクトリはsticky sessionを作成しWEB2PYSTICKYというクッキーをこの目的で使用します。

The listen directive tells HAProxy, which port to wait for connection from. The serverdirective tells HAProxy where to find the proxied servers. The appsession directory makes a sticky session and uses the a cookie called WEB2PYSTICKY for this purpose.

三つ目に、この設定を有効にしHAProxyを起動します:

Third, enable this config file and start HAProxy:

1.

/etc/init.d/haproxy restart

以下のURLにPoundの同様のセットアップ方法の手順があります

You can find similar instructions to setup Pound at the URL

http://web2pyslices.com/main/slices/take_slice/33

セッションの掃除

Clean Up Sessions

もしファイルシステムにセッションを保存する場合は、すぐ容量が増加するので本番環境では特に気をつけてください。web2pyは次のスクリプトを提供します:

If you choose to keep your sessions in the filesystem, you should be aware that on a production environment, they pile up fast. web2py provides a script called:

1.

scripts/sessions2trash.py

バックグラウンドで実行され、定期的に一定期間アクセスされていない全てのセッションを削除します。これがスクリプトの中身です:

that when run in the background, periodically deletes all sessions that have not been accessed for a certain amount of time. This is the content of the script:

1.

2.

3.

4.

5.

6.

7.

8.

9.

10.

11.

12.

SLEEP_MINUTES = 5

EXPIRATION_MINUTES = 60

import os, time, stat

path = os.path.join(request.folder, 'sessions')

while 1:

now = time.time()

for file in os.listdir(path):

filename = os.path.join(path, file)

t = os.stat(filename)[stat.ST_MTIME]

if now - t > EXPIRATION_MINUTES * 60:

os.unlink(filename)

time.sleep(SLEEP_MINUTES * 60)

次のコマンドでスクリプトを流せます:

You can run the script with the following command:

1.

nohup python web2py.py -S myapp -R scripts/sessions2trash.py &

myappはあなたのアプリケーション名です。

where myapp is the name of your application.

データベースでファイルアップロード処理

Upload Files in Database

デフォルトで、SQLFORMによって処理されたアップロードファイルは安全に名前変更されフィルシステムの"uploads"フォルダに保存されます。web2pyに対してフォルダの代わりにデータベースにアップロードファイルを保存させることも可能です。

By default, all uploaded files handled by SQLFORMs are safely renamed and stored in the filesystem under the "uploads" folder. It is possible to instruct web2py to store uploaded files in the database instead.

次のテーブルを考えてみます:

Now, consider the following table:

1.

2.

3.

db.define_table('dog',

Field('name')

Field('image', 'upload'))

dog.imageはuploadタイプです。犬の名前と同じレコードにアップロードした画像を保存するには、blogフィールドを追加しuploadフィールドにリンクさせるようテーブル定義を修正する必要があります。

where dog.image is of type upload. To make the uploaded image go in the same record as the name of the dog, you must modify the table definition by adding a blob field and link it to the upload field:

1.

2.

3.

4.

db.define_table('dog',

Field('name')

Field('image', 'upload', uploadfield='image_data'),

Field('image_data', 'blob'))

"image_data"は新しいblogフィールドは任意の名前です。

Here "image_data" is just an arbitrary name for the new blob field.

3行目は通常通りアップロードされた画像の名前を安全に変更し、変更された新しい名前をimageフィールドに保存し、ファイルシステムに保存する代わりに"image_data"というuploadfieldにデータを保存します。この全ての処理がSQLFORMによって自動で実行され他のコードを変更する必要はありません。

Line 3 instructs web2py to safely rename uploaded images as usual, store the new name in the image field, and store the data in the uploadfield called "image_data" instead of storing the data on the filesystem. All of this is be done automatically by SQLFORMs and no other code needs to be changed.

この変更で"uploads"フォルダは必要なくなります。

With this tweak, the "uploads" folder is no longer needed.

Google App Engineでは、デフォルトでuploadfieldが自動作成されるので、uploadfieldを定義しなくてもデータベースに保存されます。

On Google App Engine, files are stored by default in the database without the need to define an uploadfield, since one is created by default.

チケットの収集

Collecting Tickets

デフォルトで、web2pyはチケット(errors)をローカルのファイルシステムに保存します。一番よくあるエラーの原因は本番環境でのデータベースの問題であるため、データベースにチケットを直接保存することは意味がないからです。

By default, web2py stores tickets (errors) on the local file system. It would not make sense to store tickets directly in the database, because the most common origin of error in a production environment is database failure.

あまり発生しないのでチケットの保存はボトルネックになりません。そこで複数のサーバで構成された本番環境では、共有フォルダに保存されるのが適当です。そうはいっても管理者だけがチケットを取り出す必要があるので、共有されていないローカルの"errors"フォルダにチケットを保存して定期的に収集して削除しても大丈夫です。

Storing tickets is never a bottleneck, because this is ordinarily a rare event. Hence, in a production environment with multiple concurrent servers, it is more than adequate to store them in a shared folder. Nevertheless, since only the administrator needs to retrieve tickets, it is also OK to store tickets in a non-shared local "errors" folder and periodically collect them and/or clear them.

定期的にローカルのチケットをデータベースに移動するという方法があります。

One possibility is to periodically move all local tickets to a database.

この目的のために、web2pyは次のスクリプトを提供します:

For this purpose, web2py provides the following script:

1.

scripts/tickets2db.py

以下を含みます:

which contains:

1.

2.

3.

4.

5.

6.

7.

8.

9.

10.

11.

12.

13.

14.

15.

16.

17.

18.

19.

20.

21.

22.

23.

24.

25.

26.

27.

28.

29.

30.

31.

32.

33.

34.

35.

36.

37.

38.

39.

40.

41.

42.

43.

44.

45.

46.

47.

48.

49.

50.

51.

52.

import sys

import os

import time

import stat

import datetime

from gluon.utils import md5_hash

from gluon.restricted import RestrictedError

SLEEP_MINUTES = 5

DB_URI = 'sqlite://tickets.db'

ALLOW_DUPLICATES = True

path = os.path.join(request.folder, 'errors')

db = SQLDB(DB_URI)

db.define_table('ticket', SQLField('app'), SQLField('name'),

SQLField('date_saved', 'datetime'), SQLField('layer'),

SQLField('traceback', 'text'), SQLField('code', 'text'))

hashes = {}

while 1:

for file in os.listdir(path):

filename = os.path.join(path, file)

if not ALLOW_DUPLICATES:

file_data = open(filename, 'r').read()

key = md5_hash(file_data)

if key in hashes:

continue

hashes[key] = 1

error = RestrictedError()

error.load(request, request.application, filename)

modified_time = os.stat(filename)[stat.ST_MTIME]

modified_time = datetime.datetime.fromtimestamp(modified_time)

db.ticket.insert(app=request.application,

date_saved=modified_time,

name=file,

layer=error.layer,

traceback=error.traceback,

code=error.code)

os.unlink(filename)

db.commit()

time.sleep(SLEEP_MINUTES * 60)

このスクリプトは編集される必要があります。DB_URIストリングを変更することであなたのデータベースサーバに接続し次のコマンドを実行します:

This script should be edited. Change the DB_URI string so that it connects to your database server and run it with the command:

1.

nohup python web2py.py -S myapp -M -R scripts/tickets2db.py &

myappはあなたのアプリケーション名です。

where myapp is the name of your application.

このスクリプトはバックグラウンドで実行され5分おきに"ticket"というデータベースサーバーのテーブルにチケットを移動しローカルチケットを削除します。もしALLOW_DUPLICATEにFalseがセットされていたら、異なるタイプのエラーのみを保存します。この変更で、"errors”フォルダはローカルでだけアクセスされるので共有する必要が無くなります。

This script runs in the background and moves all tickets every 5 minutes to a table called "ticket" on the database server and removes the local tickets. If ALLOW_DUPLICATES is set to False, it will only store tickets that correspond to different types of errors. With this tweak, the "errors" folder does not need to be a shared folder any more, since it will only be accessed locally.

Memcache

memcache

web2pyが提供する二つのタイプのキャッシュ: cache.ramcache.diskを説明してきました。どちらも複数のサーバによる分散環境で動作しますが、想像通りには動作しません。具体的には、cache.ramはサーバレベルでだけキャッシュします;ですので意味がありません。cache.diskはファイルロックをサポートする"cache"フォルダが共有されていない場合は同様にサーバレベルでキャッシュします;ですのでスピードアップでは無く、主要なボトルネックになります。

We have shown that web2py provides two types of cache: cache.ram and cache.disk. They both work on a distributed environment with multiple concurrent servers, but they do not work as expected. In particular, cache.ram will only cache at the server level; thus it becomes useless. cache.disk will also cache at the server level unless the "cache" folder is a shared folder that supports locking; thus, instead of speeding things up, it becomes a major bottleneck.

memacheを2つの代わりに使うことで解決できます。web2pyにはmemcache APIがあります。

The solution is not to use them, but to use memcache instead. web2py comes with a memcache API.

memcacheを使うには、例えば0_memcache.pyという新規のモデルファイルを作成し、次のコードを記述(または追記)します:

To use memcache, create a new model file, for example 0_memcache.py, and in this file write (or append) the following code:

1.

2.

3.

4.

from gluon.contrib.memcache import MemcacheClient

memcache_servers = ['127.0.0.1:11211']

cache.memcache = MemcacheClient(request, memcache_servers)

cache.ram = cache.disk = cache.memcache

最初の行はmemcacheをインポートします。2行目はmemcache socket(サーバ:ポート)のリストです。3行目はcache.memcacheを定義します。4行目はcache.ramとcache.diskをmemcacheのために再定義します。

The first line imports memcache. The second line has to be a list of memcache sockets (server:port). The third line defines cache.memcache. The fourth line redefines cache.ram andcache.disk in terms of memcache.

Memcacheオブジェクト向けの完全に新しいキャッシュオブジェクトを定義するために一つだけを再定義することもできます。

You could choose to redefine only one of them to define a totally new cache object pointing to the Memcache object.

この変更で"cache"フォルダはアクセスされ無くなるので、共有フォルダである必要が無くなります。

With this tweak the "cache" folder does not need to be a shared folder any more, since it will no longer be accessed.

このコードはmemcacheサーバがローカルネットワークで動作していることが前提です。サーバのセットアップ方法はmemcacheドキュメントを参照してください。

This code requires having memcache servers running on the local network. You should consult the memcache documentation for information on how to setup those servers.

Memcacheでセッション処理

Sessions in Memcache

セッションが必要だがロードバランサでsticky sessionsを使用したく無い場合は、memcacheにセッションを保存するオプションがあります:

If you do need sessions and you do not want to use a load balancer with sticky sessions, you have the option to store sessions in memcache:

1.

2.

from gluon.contrib.memdb import MEMDB

session.connect(request,response,db=MEMDB(cache.memcache))

アプリケーションの削除

Removing Applications

removing application

本番環境では、デフォルトアプリケーション:admin、examples、welcomeをインストールしないほうが良いかもしれません。小さいですが必要のないアプリケーションだからです。

In a production setting, it may be better not to install the default applications: admin,examples and welcome. Although these applications are quite small, they are not necessary.

これらのアプリケーションを削除するのは、applicationsフォルダの対象のフォルダを削除するだけで簡単です。

Removing these applications is as easy as deleting the corresponding folders under the applications folder.

レプリカデータベースの使用

Using Replicated Databases

高いパフォーマンスを必要とする本番環境では、たくさんのレプリカスレーブを持つマスター・スレーブ・データベース構成やいくつかのレプリカサーバがあるかもしれません。DALはこの状況を処理でき、リクエストパラメータによる条件で異なるサーバに接続できます。それを実現するAPIは6章で説明済みです。以下は例です:

In a high performance environment you may have a master-slave database architecture with many replicated slaves and perhaps a couple of replicated servers. The DAL can handle this situation and conditionally connect to different servers depending on the request parameters. The API to do this was described in Chapter 6. Here is an example:

1.

2.

from random import shuffle

db = DAL(shuffle(['mysql://...1','mysql://...2','mysql://...3']))

この場合、異なるHTTPリクエストがランダムで異なるデータベースによって処理され、それぞれのDBが同じような頻度で使用されます。

In this case, different HTTP requests will be served by different databases at random, and each DB will be hit more or less with the same probability.

シンプルなRound-Robinを実装することもできます

We can also implement a simple Round-Robin

1.

2.

3.

4.

5.

6.

def fail_safe_round_robin(*uris):

i = cache.ram('round-robin', lambda: 0, None)

uris = uris[i:]+uris[:i] # rotate the list of uris

cache.ram('round-robin', lambda: (i+1)%len(uris), 0)

return uris

db = DAL(fail_safe_round_robin('mysql://...1','mysql://...2','mysql://...3'))

これはある意味フェイルセーフで、もしリクエストに割り当てられたデータベースサーバの接続が失敗したら、DALは順番で次のサーバに接続を試みます。

This is fail-safe in the sense that if the database server assigned to the request fails to connect, DAL will try the next one in the order.

リクエストされたアクションやコントローラによって異なるデータベースに接続することも可能です。マスター・スレーブデータベース構成で、あるアクションは読み取り専用で、別のユーザは読み込み/書き込みできます。前者はスレーブDBサーバに安全に接続でき、後者はマスターに接続されるべきです。以下のように記述できます:

It is also possible to connect to different databases depending on the requested action or controller. In a master-slave database configuration, some action performs only a read and some person both read/write. The format can safely connect to a slave db server, while the latter should connect to a master. So you can do:

1.

2.

3.

4.

if request.action in read_only_actions:

db = DAL(shuffle(['mysql://...1','mysql://...2','mysql://...3']))

else:

db = DAL(shuffle(['mysql://...3','mysql://...4','mysql://...5']))

1、2、3はスレーブで3、4、5はマスターです。

where 1,2,3 are slaves and 3,4,5 are masters.