今更ながら「大規模サービス技術者入門」を読み返したんだす。
AhoCorasickのこと今までアホコラシックて読んでたけど、
正しくはエイホコラシック(Wikipedia調べ)だそうなんだす。
ソースコード -> pyahocorasick
failure linkを作るところを理解するのに時間がかかった。
トライ木を構築した後、根から順繰りに次ぎのノードを見てって、最長接尾辞を決定するところ。
再帰でつくってみた。
最終的にこんな感じでテスト。
今更ながら「大規模サービス技術者入門」を読み返したんだす。
AhoCorasickのこと今までアホコラシックて読んでたけど、
正しくはエイホコラシック(Wikipedia調べ)だそうなんだす。
ソースコード -> pyahocorasick
failure linkを作るところを理解するのに時間がかかった。
トライ木を構築した後、根から順繰りに次ぎのノードを見てって、最長接尾辞を決定するところ。
再帰でつくってみた。
最終的にこんな感じでテスト。
buildout を使って Flask + Google App Engine な環境を構築してみたメモ。
buildout とは、Pythonベースの環境構築ツール。
日本語による記事が少なくて非常にとっつきにくいですが、慣れると簡単便利です。
こちらが今回書いた物。一応、Linux(CentOS5), MacOSX10.6で動作確認済み。
flask-gae-template
以下のコマンドを入力する。
(gitがない場合、代わりに上記サイトのDownloadsからダウンロード&解凍すればOK)
$ git clone https://github.com/utahta/flask-gae-template.git $ cd flask-gae-template $ curl -LO http://svn.zope.org/*checkout*/zc.buildout/trunk/bootstrap/bootstrap.py $ python bootstrap.py -d $ ./bin/buildout
するといろいろディレクトリが作成されたり、GoogleAppEngineのSDKやFlaskがダウンロードされたりする。
いろいろ作成された中のsrcという名のディレクトリが、作業場。
とこれだけでセットアップは完了。
残るは動作確認。
次のコマンドを打って、
$ ./bin/dev_appserver src/
ウェブサーバを起動し、http://localhost:8080にアクセスしてエラーがなければ成功です。
作業場の名前(src)が気に食わなかったら、buildout.cfgのこの部分を好きな名前に書き換えて、もう1度./bin/buildoutすると良い感じです。
GoogleAppEngineLauncher等、既にインストール済みのヤツを使いたかったら、buildout.cfgのこの行をコメントアウトなり削除なりして、パスを指定してあげると良い感じです。
たとえば、/usr/local/google_appengineにインストールしたやつを使いたかったら、
sdk-directory = /usr/local/google_appengine
とします。
Flask で始める Google AppEngine アプリ開発 – TIM Labs
Google App Engine の開発をbuildoutで行う — Python Hack-a-thon 4: ハンズオン v1.0 documentation
Google App Engine 上で Flask を動かす用のスケルトンプログラムを作った。
git 使い専用。
buildout 以外のアプローチ。(buildout ややこしいんですもん)
git clone して copy コマンドを叩くと、プロジェクトの雛形がつくられる。
$ git clone https://utahta@github.com/utahta/flask-gae-skeleton.git $ cd flask-gae-skeleton $ git submodule update --init $ python copy_skeleton.py -t /path/to/project_name
/path/to/project_name下に雛形がコピられる。
自分の環境は Mac & GoogleAppEngineLauncher.app なので、
この後、GUIから/path/to/project_nameを設定して開始ボタンを押したりする。
スケルトンプログラム自体は元々、blossom / flask-gae-skeleton というものがあって、
これをフォークしてシンプルにしただけ。
こーゆーアプローチもありなんじゃないかということで。
Python + FlaskでMVC的なサンプルをつくったメモ。
ひとまずMVCについて言及されている記事をまとめ。
MVCはWikipediaが詳しい。
以下は調べた中でもわりと個人的にしっくりきた記事たち。
Web Applicationを綺麗に設計するためのMVACという考え方 – Dive into the Tech World!
Re: @kazuho: handlersocket plugin や mycached を使えば memcached は不要か、それとも使うべきケースがあるか。考察せよ [10点] – blog.nomadscafe.jp
Life is beautiful: Ruby on Railsの「えせMVC」の弊害
the { buckblogs :here }: Skinny Controller, Fat Model
共通して言われている気がするのが下記2点。守れば比較的キレイに設計できるかも。
これはやっとけみたいなことが多いわりに絶対的なお約束がないMVC。
これはやっとけを全部守ってたらカオスになりそうなのでこの2点。
ModelでDBI, ORM, …という各機能をラップしてあげるイメージが一番しっくりきた。
これでごちゃごちゃしたソースコードが少しはスッキリしてくれるとありがたい。
上記記事を参考にしながらPython+FlaskでつくったMVC的構造のサンプルアプリをペタリ。
https://github.com/utahta/Flask-MVC-Pattern
projects/create で何かしらを登録し、projects/ で登録した何かしらを表示する至極簡素な機能付き。
以下、ソースコード(抜粋)。
コントローラ(myapp/controller/projects.py)でルーティング&入力値チェック。
from flask import Module, render_template, redirect, url_for, request from flaskext.wtf import Form, TextField, validators from myapp.model.projects import ProjectsModel projects = Module(__name__) @projects.route('/') def index(): model = ProjectsModel() # リスト表示 return render_template('projects/index.html', model=model) class CreateForm(Form): """ /projects/create 用のフォームクラス 入力値チェックも担う """ title = TextField(u'タイトル', [validators.Length(min=1, max=25)]) description = TextField(u'説明', [validators.Length(min=1, max=100)]) @projects.route('/create', methods=("GET", "POST")) def create(): model = ProjectsModel() form = CreateForm(request.form) if request.method == 'POST' and form.validate(): # チェック通ったらデータを保存し、/projects へリダイレクト model.save(form.title.data, form.description.data) return redirect(url_for('projects.index')) # 作成画面を表示 return render_template('projects/create.html', form=form)
Flask-WTFというFlaskの拡張機能を使ってフォームの値をチェック。
class CreateForm(Form) の部分。
モデル側に定義しようか一瞬迷うも、/createでしか使わないことを考えコントローラ側に定義。
プログラムがもっと大きくなったらまた考える。
モデル(myapp/model/projects.py)でデータ取得&保存。
from myapp import db from myapp.db.orm.projects import Projects class ProjectsModel(object): def get_entries(self): return db.session.query(Projects).all() def save(self, title, description): db.session.add(Projects(title, description)) db.session.commit()
ORMオブジェクトのProjectsをProjectsModelが操作。ラップップ。
ORMはSQLAlchemyを使用。
仕組みが単純なのでやってることも単純。
プログラムがもっと大きくなっても、データの一貫性保証とかもろもろここで処理する。
デザインはセンス。センスは経験。
ということで、以前つくったウェブアプリをFlaskで書き直して経験値ためる。
ついでにGAEに移植できたらいいかしらん。
Flask 基礎を勉強中。
OS:CentOS 5.4
Python-2.6.6 インストール。
$ pythonbrew install 2.6.6 $ pythonbrew switch 2.6.6
Flask インストール。
$ pip install Flask
$ vi main.py
from flask import Flask app = Flask(__name__) @app.route('/') def hello(): return 'Hello Flask.' if __name__ == '__main__': app.run()
$ python main.py * Running on http://127.0.0.1:5000/
from flask import Flask app = Flask(__name__) @app.route('/user/<username>') def show_username(username): return 'User. name:%s' % username @app.route('/post/<int:post_id>') def show_post(post_id): return 'Post. post_id:%s' % post_id if __name__ == '__main__': app.run()
from flask import Flask, redirect, url_for app = Flask(__name__) @app.route('/') def hello(): return 'Hello Flask.' @app.route('/redirect') def redirect_hello(): return redirect(url_for('hello')) if __name__ == '__main__': app.run()
from flask import Flask, request app = Flask(__name__) @app.route('/') def index(): return 'Get. name:%s' % request.values.get('name', '') if __name__ == '__main__': app.run()
ディレクトリ構成
/
main.py
|-- static
| |-- style.css
|-- templates
|-- index.htmlmain.py
from flask import Flask, render_template app = Flask(__name__) @app.route('/') @app.route('/<name>') def index(name=None): return render_template('index.html', name=name) if __name__ == '__main__': app.run()
style.css
h1{ font-size: 12px; font-weight: normal; }
index.html
<!doctype html>
<head>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" type="text/css" />
<title>Hello from Flask</title>
</head>
<body>
{% if name %}
<h1>Hello {{ name }}!</h1>
{% else %}
<h1>Hello World!</h1>
{% endif %}
</body>
</html>from flask import Flask, session, url_for, redirect app = Flask(__name__) app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT' @app.route('/') def index(): if 'username' in session: return 'Logged in. <a href="%s">Logout</a>' % url_for('logout') return '<a href="%s">Login</a>' % url_for('login') @app.route('/login') def login(): session['username'] = 'user' return redirect(url_for('index')) @app.route('/logout') def logout(): session.pop('username', None) return redirect(url_for('index')) if __name__ == '__main__': app.run(debug=True)
この1ヶ月、土日の休みを使いTUBELONGERってWebアプリをつくってました。
言語はPython。
Pythonは日本語の取り扱いが若干面倒だったりするけど、書いてて楽なので好き。
以下、PythonでWebアプリをつくるにあたって調べたり分かったりしたことのメモ。
OS:さくらVPS CentOS 5.5
サーバ:Nginx + Gunicorn
データベース:MySQL
当初はGoogle App Engineで開発を進めてたけど、データストアの使い方を調べるのにいちいち時間をとられたり、Cron的な動作に制限があったりといろいろ面倒くさかったので、途中からroot権限のあるサーバでいちから構築する方向に切り替えました。
やっぱりroot権限は自由度が半端じゃないです。
GAEがダメってわけではないです。
むしろ僕の知識が足りなすぎて学習コストが尋常じゃなかったのでまた今度にしました。
Pythonには、山のようにフレームワークが存在します。
当初は、Pylonsでつくろうと意気込んでいたのだけど、やればやるほど学ばなければいけないことが増えていくように感じたので、紆余曲折ありつつ途中からTornadoに切り替えました。
他にRubyでいうSinatraに近いFlaskというフレームワークがあってそれも良さそうでした。
Tornadoを選んだ理由は、GAEのwebappに非常に似ていた(当初webappで作成してたアプリの移植が簡単そうだった)こと、ドキュメントが完全に日本語訳されていたことが挙げられます。ただドキュメントがわりと簡素なので、詳細を知りたければソースコードを読まざるを得ないかも。面倒だったけれど、ソースコードはシンプルで読みやすく、勉強になりました。
ちなみにTornadoは、WSGI(WSGIについてはリンク先のWikipediaを参照)で運用してます。TornadoをWSGIで運用することは公に推奨されていないようだけど、いくつか非同期の機能が使えなくなるだけで元々その機能を使う予定がない場合は問題ないだろうというのと、問題があるとしたら速度的なところだろうと思って、FlaskとTornado(WSGI)両方のベンチマークをとって比較したけれど大した差は見られなかったので、まぁいいかという判断です。
Tornadoは、FriendFeedで使われていたフレームワークが元になっていて、シングルスレッド・ノンブロッキングで動作します。
通常シングルスレッドで動作する為、1リクエストの処理が遅いと全体の性能が大きく劣化します。例えばTornadoサーバが1つ動いている場合、1リクエストに1秒かかると全体の性能も1秒に1リクエストしかこなせない程度に劣化します。シングルスレッドなので当たり前ですね。故にTornadoサーバにはHTTP非同期処理の仕組みが備わっていて、負荷の高いであろう処理は別のWEBサーバで行わせて、そのWEBサーバに大してHTTP非同期処理して性能劣化を防いだりするそうな。
面倒だったので、僕はWSGI+gunicorn:geventを選択しました。
他にもTemplate機能や他便利機能が揃ってます。
CSRF対策機能とかMySQLdbのラッパとか便利です。
総合して、Pylonsほど高機能ではないが、web.pyほどシンプルでもない、中間的な立ち位置のフレームワークに感じました。
サーバは、Gunicornを選びました。
Gunicornは、WSGIを備えたサーバプログラムで、RubyのUnicornを元につくられたプロジェクト。
この上で、Tornadoを動作させています。
理由は、一番使い勝手が良さそうだったから。(ついったー上でNginx+Gunicornがアツイみたいな発言も見かけたのも大きかった)
他にも似たところで、Apache + mod_wsgiやuwsgiといったものがあるようです。
ちなみにGunicornは、ワーカーというものをプラグインで選ぶことが出来ます。
今回は、geventというコルーチンベースのものを選択。
内部でlibeventを使っていて、比較的パフォーマンスが良いらしい。
これとNginxを組み合わせて運用してます。
Gunicornをバックエンド、Nginxをフロントエンドとして使ってます。
構築する環境が、さくらVPSということでメモリ容量が少々不安だった為、Apacheよりメモリ消費の少なそうなNginxを選んでみた。
Gunicornと連携する設定は、ここを参考にしました。(というかほぼ丸コピペ)
ファイルに何らかの変更を加えた後、その修正を反映するためにGunicornの再起動が必要だったりするのだけど、いちいちPIDを調べてKILLしたりするのが面倒だったので、Supervisordというプロセス管理ツールを導入してみました。
(Gunicornの設定ファイルにこれを追加することで自動再読み込みさせられるのだけど、本番環境でやるのは気が引けた)
Supervisordを使うと、以下のように簡単に稼働状況を確認したり再起動できたりして、なにかと楽できます。
これは、導入して正解でした。
$ supervisorctl supervisor> status gunicorn gunicorn RUNNING pid 17275, uptime 3 days, 9:21:22 supervisor> restart gunicorn gunicorn: stopped gunicorn: started
以上、ざっくりとつかってみたフレームワークやそれについての雑感をつらつらと書いてみた。
何かの参考になれば幸いです。
設定ファイルの中身だとかインストールの手順だとかは省いてしまったけど、いずれ隙をみつけてメモりたい。
mod_wsgi を入れようとしたら、糞ハマったのでメモり。
環境:
CentOS5
httpd-2.2.17
mod_wsgi-3.3
Python-2.7.1
/home/user 以下にインストール
httpd, mod_wsgi, python は、ソースからコンパイルしたものを使いました。
とりあえず、/home/user ディレクトリに実行権限を付け忘れていたことが原因。
すごくざんねんです。
具体的なエラーは下記の通り。
$ ps aux | grep httpd root 27203 0.0 0.1 5816 3252 ? SNs 12:46 0:00 /path/to/httpd-2.2.17/bin/httpd -k start daemon 27210 0.0 0.0 0 0 ? ZN 12:46 0:00 [httpd] <defunct> daemon 27211 0.0 0.0 0 0 ? ZN 12:46 0:00 [httpd] <defunct>
なぜか子プロセスがゾンビ化。
Apache のログを見ると、mod_wsgi の初期化部分でコケてた。
[Sun Jan 16 12:45:10 2011] [info] mod_wsgi (pid=27185): Python home /path/to/Python-2.7.1. [Sun Jan 16 12:45:10 2011] [info] mod_wsgi (pid=27185): Initializing Python. ImportError: No module named site
ちなみに Apache のログレベルはinfo。
mod_wsgiのソースを眺めたところ、mod_wsgi.cで呼び出されているPy_Initialize()内でコケてた。
/home/userディレクトリに実行権限をつけたった。
たぶんpythonを/home/user下にインストールしてたため、daemonユーザだとリード権限がなくて、siteというモジュールを読みにいけなかったのだろうと推測。
最終的にPython-2.6.6をenable-sharedつきでコンパイルしたものを使い、httpdの立ち上げに成功。。。
そして、ウェブページにアクセスしForbiddenという文字列を見て、実行権限の問題に気づきました。
まったくアホですね。
問題の切り分けが下手だと徹夜するという教訓でした。
ここからダウンロードしたはてなのデータを実際に圧縮してみた。
ダウンロードしたやつの中に入っている eid_tags.txt を使う。
$ curl -LO http://image.gihyo.co.jp/assets/files/book/2010/978-4-7741-4307-1/hugedatabook_samplecode.zip $ unzip hugedatabook_samplecode.zip $ du -h hugedatabook_samplecode/hgdata_example/06/eid_tags.txt 172M hugedatabook_samplecode/hgdata_example/06/eid_tags.txt
大きさは、だいたい172MBぐらいらしい。
フォーマットは、「はてなタグ<タブ>ID,ID,ID…」的なかんじ。
こいつを適切に読みつつ、IDの部分にVBCodeを適用してやります。
はてなタグは文字列。IDは整数(4byte)。
IDは、小さい順にソートされているので、前後の差分を取ってそいつを圧縮してやると効率がよいとのこと。
本の中では、ギャップと表現されてました。
例で表すと以下のとおり。
整数列:1, 5, 10, 12, 16 差分 :1, 4, 5, 2, 4
— 圧縮 —
$ time ./chap6_vbencode.py eid_tags.txt > eid_tags.txt.vb real 1m37.259s user 1m35.743s sys 0m0.917s $ du -h eid_tags.vb 40M eid_tags.txt.vb
— 解凍 —
$ time ./chap6_vbdecode.py eid_tags.txt.vb > eid_tags2.txt real 0m29.463s user 0m27.269s sys 0m0.941s $ diff eid_tags2.txt eid_tags.txt
最初は圧縮するまで2分半もかかりましたが、もろもろ最適化したら1分半になりました。
でもサンプルに付属していた Perl スクリプトだと1分かからないぐらい。
この速度差はなんだろ。。。
おもむろにいろいろ測ったところ、「数値を文字列化している部分」と「pack関数周り」が怪しげだった。
その部分だけ実行するスクリプトを回してみたところ、性能に約5倍ほど差がでた。
$ cat a.py
#!/usr/bin/env python
from struct import pack
for i in xrange(19997779):
bytes = [1, 10, 100]
pack('%dB' % len(bytes), *bytes)
$ time ./a.py
real 0m45.475s
user 0m45.293s
sys 0m0.075s
---------------------------------------------
$ cat a.pl
#!/usr/bin/env perl
for($i=0; $i<19997779; $i++){
@bytes = (1, 10, 100);
pack('C*', @bytes);
}
$ time ./a.pl
real 0m9.943s
user 0m9.930s
sys 0m0.009sもうすこし絞りたいと、数値を文字列化している部分を直値に直したら性能差は約2倍まで縮まった。
$ cat a.py
#!/usr/bin/env python
from struct import pack
for i in xrange(19997779):
bytes = [1, 10, 100]
pack('3B', *bytes)
$ time ./a.py
real 0m17.557s
user 0m17.472s
sys 0m0.023sしかし、直値での実装方法は不明。
packはさらに奥が深そうなので放置。
この問題は迷宮入りです。
圧縮後ファイルサイズは、約40MBになった。
172MB → 40MBなので、圧縮率23%ぐらい。凄い。
圧縮ファイルを解凍して元ファイルと差がないことを確認し、おしまいにしました。
Variable Byte Code はアルゴリズムさえ知っていれば、実装は意外と簡単だった。
作成したソースコードはgistに貼り付けた。
よる年の瀬の最中、Web開発者のための大規模サービス技術入門という本を読んでる。
ソーシャルゲームが流行っている昨今、大規模サービスの運用論や方法論について、少しは学んでおくか的なノリで読んでる。
こいつは、株式会社はてなで行われたインターン実習が元になってて、負荷対策の基本等が分かりやすく実習形式で解説されてるオシャレな本だ。
OSのページキャッシュの仕組みとかデータの分散とか検索アルゴリズム等、もろもろ興味深いのだ。
今回は、整数データをコンパクトに持つ為の圧縮技術VBCodeをPythonで実装してみたメモり。(本の中ではPerl)
はてなのインターン生に混じった気分で、疑似コードを参考に実装してたら、なんだかんだ1時間ぐらいかかった。
もっとさくっと組めたらいいのに。
つっかかった点は以下2点。
・Pythonにおけるバイトコードの取り扱い(pack, unpack)
・128というマジックコード
おもうさま翻弄された。
特に後者の128ってマジックコード。
2進数であらわすと10000000で、VBCodeのキモになる値。
足したり割ったり眺めたりしてようやく理解したけど、擬似コードがn << 7のようにC言語ライクに書いてくれてたら、もう少し早く理解できたかもな!
できたソースコードはgistに貼り付けた。
GREEエンジニアブログのグリーの大規模分散ストレージ戦略(nanofs) Vol.2を見ていて、Apache の Request Counting ってなんぞやと思ったので、調べたメモ。
Apache の mod_proxy_balancer モジュールに組み込まれているロードバランサのスケジューラアルゴリズムの1つ。
やってきたリクエストが、各ワーカーにちゃんと分担されるようにというか詳細は、こちら。
リンク先に擬似コードが示されていたので、試しにいくつかの言語で実装してみた。
lbfactor が重み、lbstatus が優先度みたいなものっぽい。
#!/usr/bin/env python class Worker(object): def __init__(self, name, factor): self.name = name self.lbfactor = factor self.lbstatus = 0 self.debug_count = 0 def request_counting(workers): """Request Counting Algorithm""" candidate = None total_factor = 0 for worker in workers: worker.lbstatus += worker.lbfactor total_factor += worker.lbfactor if not candidate or worker.lbstatus > candidate.lbstatus: candidate = worker candidate.lbstatus -= total_factor return candidate if __name__ == "__main__": workers = [Worker('A', 70), Worker('B', 30)] for i in xrange(10): worker = request_counting(workers) print "(%s) %s" % (worker.name, ", ".join(["%3d" % w.lbstatus for w in workers])) worker.debug_count += 1 for worker in workers: print "%s:%3d" % (worker.name, worker.debug_count)
#!/usr/bin/env perl use strict; use warnings; { package Worker; sub new{ my $pkg = shift; my %hash = ( name => shift, lbfactor => shift, lbstatus => 0, debug_count => 0, ); return bless \%hash, $pkg; } } package main; sub request_counting{ my $candidate = undef; my $total_factor = 0; my $workers = shift; foreach my $worker (@$workers){ $worker->{lbstatus} += $worker->{lbfactor}; $total_factor += $worker->{lbfactor}; if(!defined($candidate) or $worker->{lbstatus} > $candidate->{lbstatus}){ $candidate = $worker; } } $candidate->{lbstatus} -= $total_factor; return $candidate; } my @workers = (Worker->new('A', 70), Worker->new('B', 30)); for(my $i = 0; $i < 10; $i++){ my $worker = request_counting(\@workers); my $debug_str = ""; foreach my $tmp (@workers){ $debug_str .= ($debug_str eq "") ? "" : ", "; $debug_str .= sprintf("%3d", $tmp->{lbstatus}); } printf("(%s) %s\n", $worker->{name}, $debug_str); $worker->{debug_count}++; } foreach my $worker (@workers){ printf("%s:%3d\n", $worker->{name}, $worker->{debug_count}); }
#!/usr/bin/env ruby class Worker def initialize(name, lbfactor) @name = name @lbfactor = lbfactor @lbstatus = 0 @debug_count = 0 end def get_name() return @name end def get_lbfactor() return @lbfactor end def add_lbstatus(lbstatus) @lbstatus += lbstatus end def sub_lbstatus(lbstatus) @lbstatus -= lbstatus end def get_lbstatus() return @lbstatus end def incl_debug_count() @debug_count += 1 end def get_debug_count() return @debug_count end end def request_counting(workers) candidate = nil total_factor = 0 for worker in workers: worker.add_lbstatus(worker.get_lbfactor) total_factor += worker.get_lbfactor if not candidate or worker.get_lbstatus > candidate.get_lbstatus: candidate = worker end end candidate.sub_lbstatus(total_factor) return candidate end workers = [Worker.new('A', 70), Worker.new('B', 30)] for i in 1..10 worker = request_counting(workers) debug_str = "" for tmp in workers debug_str += (debug_str == "") ? "" : ", " debug_str += sprintf("%3d", tmp.get_lbstatus) end printf("(%s) %s\n", worker.get_name, debug_str) worker.incl_debug_count end for worker in workers printf("%s:%3d\n", worker.get_name, worker.get_debug_count) end
(A) -30, 30 (B) 40, -40 (A) 10, -10 (A) -20, 20 (A) -50, 50 (B) 20, -20 (A) -10, 10 (A) -40, 40 (B) 30, -30 (A) 0, 0 A: 7 B: 3
ちゃんと重みに基づいて分散された。
いつか、バランシング処理を書く機会があったら、使ってみようかな。