[Dd]enzow(ill)? with DB and Python

DBとか資格とかPythonとかの話をつらつらと

Flaskで書かれたWEBアプリをBottleに書き換えるときの勘所

今週はISUCON7予選ですね。ここのところ毎回ですが今回もISUCON関連のネタです。ISUCONのPython実装はFlaskで作成されているケースが多いですが、これをBottleに書き換えるとパフォーマンスが上がるケースがあります。しかし書き換えにはいくつか注意点があります。

なぜ、FlaskとBottleでパフォーマンスが違うのか

FlaskのほうがBottleに比べると高機能です。もちろんDjangoといったフルスタックフレームワークと比べればFlaskはシンプルですが、それでもBottleにくらべると同じ処理でもフレームワーク内部の処理量が結構違います。

ISUCONで出題されるレベルのアプリケーションであれば、Bottleの範囲に収まる事が多いためフレームワークのオーバヘッドの分、パフォーマンスが稼げる可能性があります。

書き換えるときの注意点

今回は@showwinさんが作成されたオリジナルのISUCONであるISOCON1を参考に見ていきます。

from flask import Flask, request, abort, session, render_template, redirect

このあたりがBottleでどうなるのかという点がポイントです。

app.route

Flaskでは

app = Flask(__name__, static_folder=str(static_folder), static_url_path='')

こんな感じでappを作り、

@app.route('/login', methods=['POST'])
def post_login():
:

といった感じでルーティングを組み立てています。Bottleではこんな感じになります。

import bottle
:
app = bottle.default_app()
:
@app.post('/login')
def post_login():
:

ここらへんは違和感ないですね。

request

リクエストオブジェクトについてはFlaskとBottleで大差はありません。

from flask import Flask, request,....
:
def db():
    if hasattr(request, 'db'):
        return request.db
    else:
from bottle import request
:
def db():
    if hasattr(request, 'db'):
        return request.db
    else:

abort

エラーレスポンスを戻すときに使いますが、abort()自体は差はありません。

from flask import ...., abort...

def authenticated():
    if not current_user():
        abort(401)
from bottle import abort

def authenticated():
    if not current_user():
        abort(401)

ただし、エラーページの扱いが違います。

@app.errorhandler(401)
def authentication_error(error):
    return render_template('login.html', message='ログインに失敗しました'), 401
@app.error(401)
def authentication_error(error):
    # bottleでは,401でタプルを戻すと死ぬ
    return render_template('login.html', message='ログインに失敗しました')  

app.error()デコレータになったり、returnでタプルを戻すと死ぬので注意です。

session

sessionはだいぶ違います。Flaskは標準でもっていますが、Bottleはプラグインが必要です。

from flask import ..., session,...
:
def current_user():
    if 'user_id' in session:
        cur = db().cursor()
        cur.execute('SELECT * FROM users WHERE id = %s', (str(session['user_id']),))
        return cur.fetchone()
    else:
        return None
from beaker.middleware import SessionMiddleware
:
# セッション管理の設定
session_opts = {
    'session.type': 'file',
    'session.data_dir': '/tmp/data',
    'session.cookie_expires': True,
    'session.auto': True
}
:
def current_user():
    session = bottle.request.environ.get('beaker.session')
    if 'user_id' in session:
        cur = db().cursor()
        cur.execute('SELECT * FROM users WHERE id = %s', (str(session['user_id']),))
        return cur.fetchone()
    else:
        return None
:
# sessionの有効化
app = SessionMiddleware(app, session_opts)

beakerモジュールを別途導入した上で、SessionMiddlewareを使う必要があります。またsessionを使うにはbottle.request.environ.get('beaker.session')として取得しなければいけません。結構このあたりは手間ですね。また、session_optsの最適解がイマイチわかりません。

render_template

テンプレートをレンダリングする方法は異なりますが、揃えることは可能です。特にFlaskでJinja2を使っているのであれば、そのままBottleもJinja2を使えばいいだけです。

from flask import ..., render_template
:
@app.route('/')
def get_index():
    :
    :
    return render_template('index.html', products=products, current_user=current_user())
from bottle import TEMPLATE_PATH
from bottle import jinja2_template as render_template
import pathlib

# テンプレート探索先に追加
TEMPLATE_PATH.append(pathlib.Path(__file__).resolve().parent / 'templates')

@app.route('/')
def get_index():
    :
    :
    return render_template('index.html', products=products, current_user=current_user())

TEMPLATE_PATHの調整は必要ですが、bottle.jinja2_templaterender_templateにしてしまえば差し替えるのは容易です。

ルーティングのフィルタ

地味に差があって混乱します。

# Flask では <データ型:変数名>
@app.route('/users/<int:user_id>')
def get_mypage(user_id):
    cur = db().cursor()
    :
# Bottleでは<変数名:データ型>
@app.get('/users/<user_id:int>')
def get_mypage(user_id):
    cur = db().cursor()
    :

逆になっているので、差し替えるときはそれぞれ書き換えなければいけません。

リクエストのフック

DBとの接続を閉じる等、リクエスト終了毎に何かを行うような場合のHOOKの書き方が異なります。

@app.teardown_request
def close_db(exception=None):
    if hasattr(request, 'db'):
        request.db.close()
@bottle.hook('after_request')
def close_db(exception=None):
    if hasattr(request, 'db'):
        request.db.close()

@app.teardown_requestではなく@bottle.hook('after_request')としてデコレータを付与します。

まとめ

なかなか機会が絞られる知識のTips集みたいになってしまいましたが、FlaskをBottleに書き換えることができれば多少はパフォーマンスが稼げますので頭の片隅においておくといいのではないでしょうか。