數據庫長連接
長連接是指程序之間的連接在建立之後,就一直打開,被後續程序重用。使用長連接的初衷是減少連接的開銷。
先看看官方文檔是怎麼講Django長連接的。翻譯得可能不太得體,原文參見Django databases。
Django長連接
長連接(Persistent connections)是爲了避免在每個請求中都重新建立數據庫連接的開銷。在Django中,數據庫連接由CONN_MAX_AGE
控制,這個參數定義了每個連接的最長壽命。可以爲每個DB單獨設置CONN_MAX_AGE
。
CONN_MAX_AGE
的默認值是0,在每個請求結束時,將關閉數據庫連接。要啓用長連接,請將CONN_MAX_AGE
設置爲正數秒。對於不限時的長連接,請將其設置爲None
。
連接管理
Django在首次進行數據庫查詢時會建立與數據庫的連接。它保持此連接打開,並在後續請求中重用它。連接一旦超過CONN_MAX_AGE
定義的最大壽命或不再可用,Django就會關閉連接。
具體來講,沒有連接分兩種情況:1、這是第一個連接,2、或者先前的連接已關閉。如果Django沒有連接,它會在需要時自動建立與數據庫的連接。
在每個請求開始時,Django會關閉那些達到其最大壽命的連接。如果你的數據庫在一段時間後會關閉空閒連接,則應將CONN_MAX_AGE
設置爲較小的值,以便Django不會試圖使用已由DB終止的連接。(這個問題可能只會影響流量非常低的網站。)
在每個請求結束時,Django會在連接達到最大壽命或者處於不可恢復的錯誤狀態時關閉連接。如果在處理請求時發生任何數據庫錯誤,Django會檢查連接是否仍然有效,如果沒有則關閉它。因此,數據庫錯誤最多隻影響一個請求;如果DB連接變得不可用,則下一個請求將獲取新連接。
注意
由於每個線程都維護自己的連接,因此你的數據庫必須至少支持與工作線程一樣多的併發連接。
有時,大多數views
都不會訪問數據庫,例如,因爲它是外部系統的數據庫,或者歸功於緩存。在這種情況下,應該將CONN_MAX_AGE
設置爲較小的值甚至0,因爲維護不太可能重用的連接沒有意義。這將有助於將與數據庫的併發連接數量保持在較小的值。
開發模式的Server爲它處理的每個請求創建一個新線程,無視長連接的作用。在開發過程中不需要啓用長連接。
當Django建立與數據庫的連接時,它會根據所使用的後端設置恰當的參數。如果啓用長連接,則不會再對每個請求重複設置。如果修改連接的隔離級別或時區等參數,則應在每個請求結束時恢復Django的默認值,在每個請求開始時強制使用適當的值,或者禁用長連接。
數據庫連接池
爲啥Django不支持連接池
我們看看Google Group裏各路大神的討論吧。
- 第三方工具已經提供了,更專注做得更好。Django並不需要做全棧。
- 用從pool裏取連接代替新建連接,向pool歸還連接代替關閉連接,然後在worker在整個請求期間都持有連接並不是真正的連接池。這需要跟worker數一樣多的數據庫連接,除了能在各個worker循環使用外,基本跟長連接是等效的。長連接也有自己的優點,消除了新建連接的開銷,避免的池化的複雜性,適用於不需要手動管理事務的中小型站點。
- 首先要操心的不是數據庫,AWS之類的雲計算已經很牛了,按需擴容,多關注下緩存吧。
- MySQL的連接非常輕量和高效,大量的Web應用都沒有使用連接池。
- ……
連接池的優點
數據庫連接池(Connection pooling)的核心思想是連接複用,通過建立一個數據庫連接池以及一套連接使用、分配和管理策略,使得該連接池中的連接可以得到高效、安全的複用。主要有以下優勢:
- 減少資源開銷:減少連接的創建,避免了數據庫連接初始化和釋放過程的時間和資源開銷,加快系統的響應速度
- 統一連接管理:預先設定超時時間、連接數量,避免數據庫連接操作中可能出現的資源泄露,增強系統的穩定性
其實,我們在連接池裏建立的連接生存週期也可以比較長,這樣能充分利用長連接的優點減少連接的開銷;同時,連接池又可以幫助我們更方便管理DB連接,減少後端服務到MySQL的連接數。
爲何要在Django中用連接池
說了這麼多,有兩點是確定的:
- Django原生支持長連接,但不支持連接池
- 連接池還是有很多優點的,也有很多成熟的三方庫支持
Django服務,一般情況下每個線程都維護自己的連接,有多少線程就會就有多少連接;如果採用分佈式部署,線程數較多,則會建立較多的連接。不僅非常消耗資源,還可能出現MySQL連接數不夠用的情況。
舉個例子:5個子服務 * 50個容器 * 10個進程 * 20個線程 = 50000個連接(Mysql5.5,Mysql5.6,Mysql5.7:默認的最大連接數都是151,理論上限爲:100000;實際幹到2W就很不容易了)。
假設我們能讓20個線程,複用一個size=10
的連接池,這樣就能減少一半的數據庫連接。這對支持服務水平擴展,降低數據庫負載是非常有幫助的。那麼我們接下來看看,如何在Django中用上DB連接池。
Django連接池方案
SQLAlchemy Patch
SQLAlchemy
有一個成熟的連接池實現,支持Django使用連接池,首先考慮的就是SQLAlchemy
。
Github上的輪子:
從源碼看,patch主要做了三件事情:
- 創建和返回新建的
SQLAlchemy pool
- 從
connection pool
取connect - hack掉Django自己的connect方法
所以,patch主要在是connect()
方法上做了文章,實際使用的依然是Django的ORM,而不是SQLAlchemy的ORM。知道了原理,那我們可以對照着自己造個輪子,動手擼一個簡化版本的連接池patch吧。
自己造輪子
新建一個db_pool_patch.py
# -*- coding: utf-8 -*-
from django.conf import settings
from sqlalchemy.pool import manage
POOL_PESSIMISTIC_MODE = getattr(settings, "DJ_ORM_POOL_PESSIMISTIC", False)
POOL_SETTINGS = getattr(settings, 'DJ_ORM_POOL_OPTIONS', {})
POOL_SETTINGS.setdefault("recycle", 3600)
def is_iterable(value):
"""Check if value is iterable."""
try:
_ = iter(value)
return True
except TypeError:
return False
class HashableDict(dict):
def __hash__(self):
items = [(n, tuple(v)) for n, v in self.items() if is_iterable(v)]
return hash(tuple(items))
class ManagerProxy(object):
def __init__(self, manager):
self.manager = manager
def __getattr__(self, key):
return getattr(self.manager, key)
def connect(self, *args, **kwargs):
if 'conv' in kwargs:
kwargs['conv'] = HashableDict(kwargs['conv'])
if 'ssl' in kwargs:
kwargs['ssl'] = HashableDict(kwargs['ssl'])
return self.manager.connect(*args, **kwargs)
def patch_mysql():
from django.db.backends.mysql import base as mysql_base
if not hasattr(mysql_base, "_Database"):
mysql_base._Database = mysql_base.Database
manager = manage(mysql_base._Database, **POOL_SETTINGS)
mysql_base.Database = ManagerProxy(manager)
def patch_sqlite3():
from django.db.backends.sqlite3 import base as sqlite3_base
if not hasattr(sqlite3_base, "_Database"):
sqlite3_base._Database = sqlite3_base.Database
sqlite3_base.Database = manage(sqlite3_base._Database, **POOL_SETTINGS)
def install_patch():
patch_mysql()
patch_sqlite3()
這裏,我們用不到60行代碼,就實現了對mysql
和sqlite3
的patch。
爲了方便跟蹤connection pool
的工作情況,還可以建一個db_pool_listen.py
添加一些監聽函數並打印log。
# -*- coding: utf-8 -*-
from django.conf import settings
from sqlalchemy import event, exc
from sqlalchemy.pool import Pool
from log import logger
@event.listens_for(Pool, "checkout")
def _on_checkout(dbapi_connection, connection_record, connection_proxy):
logger.debug("connection retrieved from pool")
if settings.POOL_PESSIMISTIC_MODE:
cursor = dbapi_connection.cursor()
try:
cursor.execute("SELECT 1")
except:
# raise DisconnectionError - pool will try
# connecting again up to three times before raising.
raise exc.DisconnectionError()
finally:
cursor.close()
@event.listens_for(Pool, "checkin")
def _on_checkin(*args, **kwargs):
logger.debug("connection returned to pool")
@event.listens_for(Pool, "connect")
def _on_connect(*args, **kwargs):
logger.debug("connection created")
在settings.py
中配置一下pool的參數:
DJ_ORM_POOL_OPTIONS = {
"pool_size": 20,
"max_overflow": 0,
"recycle": 3600, # the default value
}
POOL_PESSIMISTIC_MODE = True
在Django項目的wsgi.py
中安裝我們新建的patch:
import db_pool_patch
db_pool_patch.install_patch()
if settings.DEBUG:
import db_pool_listen
application = get_wsgi_application()
測試結果及改進
然後訪問接口http://127.0.0.1:8000/book/get?book_id=1
,就可以觀察到下面的log啦。
2019-08-29 22:17:10,140 db_pool_patch - db_pool_listen.py - DEBUG - connection retrieved from pool
(0.000) SELECT "book_tab"."book_id", "book_tab"."title", "book_tab"."author" FROM "book_tab" WHERE "book_tab"."book_id" = 1; args=(1,)
2019-08-29 22:17:10,141 db_pool_patch - views.py - INFO - some shit code begin to run ...
2019-08-29 22:17:15,144 db_pool_patch - views.py - INFO - hmm, shit code finished, it took 5 seconds
[29/Aug/2019 22:17:15] "GET /book/get?book_id=1 HTTP/1.1" 200 62
2019-08-29 22:17:15,146 db_pool_patch - db_pool_listen.py - DEBUG - connection returned to pool
nice, 我們的連接池已經可以正常工作了。但是從log裏可以看出,我們的patch有個問題,在整個請求處理期間一直持有連接,容易導致pool裏的連接不夠用;實際上一查完數據,就不需要繼續持有DB連接了。
def test_request(request):
obj = TestModel.objects.get(id=1)
do_something_that_takes_1000_seconds(obj) # 這時DB連接依然被佔用,但實際已經不需要了
如前文提到的那樣:
用從pool裏取連接代替新建連接,向pool歸還連接代替關閉連接,然後在worker在整個請求期間都持有連接並算不上
真正的連接池
。這需要跟worker數一樣多的數據庫連接,除了能在各個worker循環使用外,基本跟長連接是等效的。
那麼,我們怎麼解決這個問題呢?在訪問完數據庫後,馬上歸還連接,而不是在請求完成時再歸還。因爲我們使用的依然是Django ORM
,在請求期間持有連接本質上是Django ORM
的行爲。所以可以patch一下Django ORM
,執行完數據庫操作後,及時釋放連接。
from django.db.models.sql import compiler
from django.db.models.sql.constants import MULTI
def install_django_orm_patch():
execute_sql = compiler.SQLCompiler.execute_sql
# Django 1.11
def patched_execute_sql(self, result_type=MULTI, chunked_fetch=False):
result = execute_sql(self, result_type, chunked_fetch)
if not self.connection.in_atomic_block:
self.connection.close() # return connection to pool by db_pool_patch
return result
compiler.SQLCompiler.execute_sql = patched_execute_sql
insert_execute_sql = compiler.SQLInsertCompiler.execute_sql
def patched_insert_execute_sql(self, return_id=False):
result = insert_execute_sql(self, return_id)
if not self.connection.in_atomic_block:
self.connection.close() # return connection to pool by db_pool_patch
return result
compiler.SQLInsertCompiler.execute_sql = patched_insert_execute_sql
再次訪問接口,打印的log如下:
2019-08-29 22:13:22,373 db_pool_patch - db_pool_listen.py - DEBUG - connection retrieved from pool
(0.000) SELECT "book_tab"."book_id", "book_tab"."title", "book_tab"."author" FROM "book_tab" WHERE "book_tab"."book_id" = 1; args=(1,)
2019-08-29 22:13:22,374 db_pool_patch - db_pool_listen.py - DEBUG - connection returned to pool
2019-08-29 22:13:22,374 db_pool_patch - views.py - INFO - some shit code begin to run ...
2019-08-29 22:13:27,378 db_pool_patch - views.py - INFO - hmm, shit code finished, it took 5 seconds
[29/Aug/2019 22:13:27] "GET /book/get?book_id=1 HTTP/1.1" 200 62
不錯,可以看出,DB連接在shit code
開始運行前,就已經歸還給連接池了;這樣就降低了單個連接的持有時間,能顯著提高連接的使用率和pool的性能。
小結
本文主要介紹了Django長連接,比較了長連接和連接池,實現了基於SQLAlchemy Patch
的Django數據庫連接池方案,並做了一定的改進。
本文轉自:https://lockshell.com/2019/08/28/django-db-connection-pool/
非常感謝原作者!