Django ORM 資料庫生命週期

浮生若夢的程式設計發表於2018-02-05

起因

有時線上上會遇到“MySQL Server gone away”的錯誤,但是是以一定概率出現的,今天又遇上了,不得不逼迫自己徹底去解決這個事情。老實說,一般遇到這種看不懂的錯誤,心裡還是挺懵的。一來這種錯誤一般暴露自己底層知識理解不夠透徹;二來是框架畢竟封裝了太多,讓人看得見森林卻看不見樹木,比如 Django 這種封裝很完善的框架。要命的是第一種,基礎知識不是一下子就能補回來的,又不是調 API,「讀幾遍文件就能擼起袖子 work」。

一開始,我既不知道從何下手,也不知道如何組織關鍵詞,只得用上老套路,把錯誤資訊貼上到搜尋引擎裡,一遍一遍地搜尋,同時不斷微調關鍵詞。

在閱讀了一些資料後,大概明白了一些原理,以及順利地提出了自己滿意的解法。

Django 如何做的

這個問題歸根到底是「如何管理資料庫連線的生命週期」的問題。

一個資料庫連線,一頭繫著 client (即 App,這裡我們簡化為 Django ),一頭繫著 server (MySQL Server),其狀態是由這兩者加上其他因素(姑且先排除)決定的。client 這端自然好控制,自始至終都在我們的控制範圍之內,然而 server 那端就不好控制了,加上 my.cnf 各種奇奇怪怪的配置,皆可影響 server 的行為,所以 server 在我們的控制範圍之外。

在 Web App 中,資料庫連線的生命週期一般和 HTTP Request 的生命週期一致,即:在 Request 開始時,初始化好連線(一般從 Connection Pool 中取一個);在 Request 結束後,將連線釋放( 一般是還回到 Pool 中 )。但這是比較理想的情況,「連線到底是不是可用,到底是不是好的」這個問題沒有解決。

Django 的做法是:利用其訊號機制(Django 中的一種解耦手段),在 Request 開始和結束的時候,將不可用的連線清理掉,從而確保整個 Request 週期內,所取到的連線都是沒問題的。至於如何判斷一個連線的可用與不可用,方法是多種多樣的,比如 MySQL,可以 ping 下 server,看連線是否正常。

Diango的處理程式碼如下:

django\db\__init__.py

# 其實是一個 callback
def close_old_connections(**kwargs):
    for conn in connections.all():
        conn.close_if_unusable_or_obsolete()


# 訊號註冊
# 可以看到, 主要核心邏輯在於 close_old_connections
signals.request_started.connect(close_old_connections)
signals.request_finished.connect(close_old_connections)
複製程式碼

針對我的需求,我的做法

我們的專案重度使用 Django,簡化的邏輯如下

Web Server( 對外提供 REST API )                     Thrift Server( 對內部提供RPC API )
 ............................
           ORM Model
           DB
複製程式碼

其中,Web Server 這塊自然不用多說,各種需要操心的細節,Django 都替我們管理好了。主要是 Thrift Server 這裡,它也是直接使用 Django 的 ORM Model,所以也需要實現一套「連線管理的訊號註冊」。

根據前面的說明,我們只需要實現 Thrift Request 開始和結束時的訊號註冊即可,即:如法炮製,呼叫 close_old_connections 。但我們有一個針對 Thrift handler 的裝飾器,我發現只需要在那裡面呼叫 close_old_connections 就可以了。

大致邏輯如下

def decorator(method):
    def _wrapper(self, *args):
        try:
            # 開始時,清理無效連線
            close_old_connections()
            
            method_result = method(self, *args)
        exception Exception as e:
            raise ThriftServerException(e)
        else:
            return method_result
        finally:
            # 結束後,清理無效連線 (似乎必要性不大 ? )
            close_old_connections()
            
    return _wrapper
複製程式碼

Thrift Hanlder 使用:

class Handler(Iface):
    @decorator
    def interface1(self, request):
        xxx
複製程式碼

啟示

框架一般封裝了太多東西,抽象出來的層級數目也很多,一般如果需要實現自己非常個性化的需求,就比較難受了,需要深入到框架的原始碼中去,才能有效解決問題。所以正如廚師需要熟悉他的工具一樣,開發者也需要熟悉他依賴的框架,用 Django 就要熟悉 Django,用 Spring 就要熟悉 Spring 。

相關文章