Flask の MySQL 接続と Life Cycle
mysql has gone away が現れるようになり、前から気になってたflask appのlife cycleについて理解しようとしてみた。
結局原因が途中でわかったので尻すぼみだけど、記録として残しておく。
わかってないこと
- appはいつ起動していつ終わるのか?
- urlに対する1 requestで app起動、コンテンツ返して終了、と思っていたが、gone awayってことはconnectionをkeepしている可能性がある
- MySQL接続はいつ切れるのか?
- MySQL(app) ではない独自の接続を多数持っている。これらは切断されないのか?
- 元のinstanceが廃棄されれば切断されると思うが、廃棄されているのか?
きれいなflaskは毎回切断している
以下のコードで検証
from flask import Flask
from flask_mysqldb import MySQL
app = Flask(__name__)
app.config['MYSQL_USER'] = 'report_local'
app.config['MYSQL_PASSWORD'] = 'report_local'
app.config['MYSQL_HOST'] = 'sys-test-v.dev.com'
app.config['MYSQL_DB'] = 'mysql'
app.config['MYSQL_PORT'] = 20306
app.config['MYSQL_CURSORCLASS'] = 'DictCursor'
mysql = MySQL(app)
@app.route('/')
def users():
cur = mysql.connection.cursor()
cur.execute('''SELECT CONNECTION_ID(); ''') # mysql connection idを取得
rv = cur.fetchall()
return "app obj id = %s, %s" % (id(app), str(rv)) # <----- appのobject idも返す
if __name__ == '__main__':
app.run(debug=True)
結果
- appは作り直されない
- MySQLは切断されていることがわかった
# first time
app obj id = 4424594128, ({'CONNECTION_ID()': 7196},)
# second time
app obj id = 4424594128, ({'CONNECTION_ID()': 7197},)
# and so on...
app obj id = 4424594128, ({'CONNECTION_ID()': 7198},)
appが作り直されないのは、app自体はあくまで初回の python run.py をした時点 = Web Serverを起動した時点で同時に生成されるから・・なんだろうか。
自分のいけてない MySQLdb で検証
show processlist; を見ていたら、やっぱり自分のflaskは切断してなかった。大量にコネクションが残ってた。
webサーバとアプリサーバが完全にわからない
appが作り直されない理由がよくわからない。
これは、根本的にwebサーバを理解していないのが問題なんだと思う。
webサーバは、起動するとソケット(ポート)をlistenにして接続を待ち受ける。
リクエストが来たら、定められた位置にあるファイルを取って渡すだけだ。
そこにあるのがアプリなら、アプリを起動する・・・・そう思っていけれど。
web serverとpython applicationの間は、どうなっているんだろうか?
web server processを造ったとき、同時に受けてであるアプリも作られる。
アプリ側も待機しないといけない。socketかportで、web-serverからのリクエストに答えるために
ずっと起動してlistenし続けている。これがアプリサーバか。
pythonのhttp serverだと、全部ひとつで完結していて、その結果アプリサーバが起動する・・・
当然、起動したときのものはずっと残る・・・
single threadの場合は、そのアプリが永遠に行き続ける・・のだろうか。
multi threadだったら? アプリサーバは、あくまでメインスレッドが起動するだけ?
リクエストに応じてappが作られる?むー・・それだとつじつまが合わない気がする
simple な web serverで検証
たぶんflaskのdev serverはこれを使ってるんじゃなかろうか。
import http.server
import datetime
class App():
def show(self):
return "Hello %s" % datetime.datetime.now()
app = App()
class myHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type','text/html')
self.end_headers()
self.wfile.write(str.encode("app id=%s, %s" % (str(id(app)), app.show())))
return
server_address = ("", 8000)
simple_server = http.server.HTTPServer(server_address, myHandler )
simple_server.serve_forever()
参考 https://docs.python.org/3/library/http.server.html#http.server.HTTPServer
で、結局原因はclass変数だった
appが作り直されない理由がよくわからない。
これを書いて数週間経った今の理解で行くと、appインスタンスは作り直される。インスタンスは作り直されるが、app クラス は最初に作ったときのままだ。
pythonではクラス変数は最初の起動から永遠に再利用される。
今回の原因は、appから呼び出している自作クラスのmysql connection poolをいれる変数がclass変数だった。
class MyClass:
dbpool = {} # ←これが原因
def __init__(self):
self.dbpool = {} # ←インスタンス変数にして解決した
class変数はアプリが最初に起動された状態を永遠に保持するので、コンテンツを返した後も残り続ける。
自分のclassは、self.dbpoolの中にconnectionオブジェクトが残っている限り再利用する構造にしていたので、
オブジェクトは残るが、オブジェクト自体は数時間で接続が切れている、という状態だった。
切れている接続を再利用すると mysql has gone away になる。
いま考えれば con.close() する時に self.dbpool の中を空にしていなかったのかもしれない。
いずれにしても、インスタンス変数にした結果、アクセスのたびに self.dbpool が初期化されるようになったので
古いconnectionオブジェクトを再利用することはなくなった。そういえばself.dbpoolを空にする修正も一応した気がしてきた。
個人的には ↓ に近いような問題だと思っていて、いや普通なのかもしれないけど、pythonなんだかなぁと感じたりしている。
Pythonのこれ、流石に言語としてどうなのという気持ちになるんだが… pic.twitter.com/degIU4tPeI
— 幸福のデータ科学㈱教祖テラモナギ (@teramonagi) May 21, 2020
おしまい