LoginSignup
1
2

Flask の MySQL 接続と Life Cycle

Last updated at Posted at 2020-06-03

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)

参考 https://github.com/admiralobvious/flask-mysqldb

結果

  • 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なんだかなぁと感じたりしている。

おしまい

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2