-
線上連接gone away問題
我們業務中需要執行一個時間超過8小時的腳本,然後發生了連接gone away的問題,然後開始找原因,首先我們的僞代碼如下:
app = create_app()
def prod_script():
with app.app_context():
machine_objs = DiggerMapper.get_all()
time.sleep(8*3600) # 替代線上執行比較久的代碼
machine_objs = DiggerMapper.get_all() # 再執行這次數據庫查詢時發生連接gone away的報錯
return True
通過排查原因基本定位爲,在執行代碼的時候本次連接時間比較久,超過了mysql的默認最長空閒連接時間8小時就被斷開了,所以再次進行數據庫查詢時就報錯連接gone away。
-
解決方案
ok,知道了錯誤發生的原因,我們的基本思路就是,給每次查詢結束以後我們就把數據庫連接放回連接池中,然後下次要使用時再次從連接池中取連接就好了。代碼如下:
app = create_app()
def prod_script():
with app.app_context():
machine_objs = DiggerMapper.get_all()
db.session.close()
time.sleep(8*3600) # 替代線上執行比較久的代碼
machine_objs = DiggerMapper.get_all()
return True
那麼這樣就解決問題了嗎?我們知道mysql在空閒連接超過8小時後會主動斷開連接,所以連接池中的空閒連接在超過8小時後也是會被mysql斷開的,那麼下次進行數據庫操作時依然會繼續報錯,我們還需要加上一條設置放在config文件中的SQLALCHEMY_POOL_RECYCLE = 3600 這條設置的意思是讓連接池每隔一小時重新和mysql建立連接,這樣我們取到連接都是可用連接了。
通過以上兩步就基本上可以解決連接gone away問題了。
那麼都到這一步了, 知其然要知其所以然,都是這個session在進行操作,那麼這個session是怎麼工作的呢?我們去找找它的源碼。
-
session源碼
我們在創建app前通常會先實例化SQLAlchemy,然後執行init_app(app),那麼找session我們先看它實例化的代碼做了什麼工作:
db = SQLAlchemy()
def create_app():
app = Flask(__name__)
db.init_app(app)
return app
class SQLAlchemy(object):
def __init__(self, app=None, use_native_unicode=True, session_options=None,
metadata=None, query_class=BaseQuery, model_class=Model,
engine_options=None):
self.use_native_unicode = use_native_unicode
self.Query = query_class
self.session = self.create_scoped_session(session_options)
self.Model = self.make_declarative_base(model_class, metadata)
self._engine_lock = Lock()
self.app = app
self._engine_options = engine_options or {}
_include_sqlalchemy(self, query_class)
if app is not None:
self.init_app(app)
我們看到session是create_scoped_session(session_options)生成的,然後進入這個方法看到:
def create_scoped_session(self, options=None):
if options is None:
options = {}
scopefunc = options.pop('scopefunc', _app_ctx_stack.__ident_func__)
options.setdefault('query_cls', self.Query)
return orm.scoped_session(
self.create_session(options), scopefunc=scopefunc
)
上面看到相當於session是由 orm.scoped_session方法生成的,其中self.create_session(options)方法在被調用時默認生成一個SignallingSession對象,我們再看看orm.scoped_session方法發生了什麼:
class scoped_session(object):
def __init__(self, session_factory, scopefunc=None):
self.session_factory = session_factory
if scopefunc:
self.registry = ScopedRegistry(session_factory, scopefunc)
else:
self.registry = ThreadLocalRegistry(session_factory)
# self.registry 類似如下
self.registry = {"線程id": SignallingSession, "線程id2": SignallingSession}
好吧,scoped_session是一個類,實例化以後有個屬性self.registry,如果之前有看過flask源碼知道像請求上下文和應用上下文一樣,它是一個基於線程安全的字典,每一個線程有自己的session類。
這是你可能有個疑問既然這裏返回的是一個scoped_session對象,但是類中並沒有query等方法那麼db.session.query(), db.session.close()這些是怎麼來的,答案就是在文件下還有一段立即執行的代碼如下:
ScopedSession = scoped_session
"""Old name for backwards compatibility."""
def instrument(name):
def do(self, *args, **kwargs):
return getattr(self.registry(), name)(*args, **kwargs)
return do
for meth in Session.public_methods:
setattr(scoped_session, meth, instrument(meth))
這段代碼的意思就是把SqlAchemy的Session類中有的所有方法,也給上面我們提到默認SignallingSession對象加上,這樣我們就可以調用query等方法了。並且也相當於每次執行數據庫操作時都會創建或者共用之前創建的session連接,
然後我們在代碼中從來沒有主動關閉連接的代碼,flask-sqlachemy是怎麼幫我們關閉連接的呢?答案是它裏面在請求完成後主動幫我們關閉了,在init_app的代碼中有如下代碼:
def init_app(self, app):
"""This callback can be used to initialize an application for the
use with this database setup. Never use a database in the context
of an application not initialized that way or connections will
leak.
"""
if (
'SQLALCHEMY_DATABASE_URI' not in app.config and
'SQLALCHEMY_BINDS' not in app.config
):
warnings.warn(
'Neither SQLALCHEMY_DATABASE_URI nor SQLALCHEMY_BINDS is set. '
'Defaulting SQLALCHEMY_DATABASE_URI to "sqlite:///:memory:".'
)
app.config.setdefault('SQLALCHEMY_DATABASE_URI', 'sqlite:///:memory:')
app.config.setdefault('SQLALCHEMY_BINDS', None)
app.config.setdefault('SQLALCHEMY_NATIVE_UNICODE', None)
app.config.setdefault('SQLALCHEMY_ECHO', False)
app.config.setdefault('SQLALCHEMY_RECORD_QUERIES', None)
app.config.setdefault('SQLALCHEMY_POOL_SIZE', None)
app.config.setdefault('SQLALCHEMY_POOL_TIMEOUT', None)
app.config.setdefault('SQLALCHEMY_POOL_RECYCLE', None)
app.config.setdefault('SQLALCHEMY_MAX_OVERFLOW', None)
app.config.setdefault('SQLALCHEMY_COMMIT_ON_TEARDOWN', False)
track_modifications = app.config.setdefault(
'SQLALCHEMY_TRACK_MODIFICATIONS', None
)
app.config.setdefault('SQLALCHEMY_ENGINE_OPTIONS', {})
if track_modifications is None:
warnings.warn(FSADeprecationWarning(
'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and '
'will be disabled by default in the future. Set it to True '
'or False to suppress this warning.'
))
# Deprecation warnings for config keys that should be replaced by SQLALCHEMY_ENGINE_OPTIONS.
utils.engine_config_warning(app.config, '3.0', 'SQLALCHEMY_POOL_SIZE', 'pool_size')
utils.engine_config_warning(app.config, '3.0', 'SQLALCHEMY_POOL_TIMEOUT', 'pool_timeout')
utils.engine_config_warning(app.config, '3.0', 'SQLALCHEMY_POOL_RECYCLE', 'pool_recycle')
utils.engine_config_warning(app.config, '3.0', 'SQLALCHEMY_MAX_OVERFLOW', 'max_overflow')
app.extensions['sqlalchemy'] = _SQLAlchemyState(self)
@app.teardown_appcontext
def shutdown_session(response_or_exc):
if app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN']:
warnings.warn(
"'COMMIT_ON_TEARDOWN' is deprecated and will be"
" removed in version 3.1. Call"
" 'db.session.commit()'` directly instead.",
DeprecationWarning,
)
if response_or_exc is None:
self.session.commit()
self.session.remove()
return response_or_exc
其中shutdown_session方法就是在請求結束後,主動幫我們關閉session連接放回連接池中。
-
總結
綜上源碼所述,解釋了
- 爲什麼我們可以隨時關閉session連接,因爲每次數據庫操作都可以重新創建連接
- 爲什麼設置SQLALCHEMY_POOL_RECYCLE 配置,因爲讓連接保持活躍不被mysql斷開
- 遇到執行比較久的代碼怎麼做,在適當的時候我們主動關閉session連接放回連接池中保證連接的可用