摘要:說多了都是淚,我之前排查內存泄漏的問題,超高并發的程序跑了個月后就崩潰。以前寫中間件的時候,就總是把用戶當,要盡量考慮各種情況避免內存泄漏。
從 Java 到 Python
本文為我和同事的共同研究成果
當跨語言的時候,有些東西在一門語言中很常見,但到了另一門語言中可能會很少見。
例如 C# 中,經常會關注拆箱裝箱,但到了 Java 中卻發現,根本沒人關注這個。
后來才知道,原來是因為 Java 中沒有真泛型,就算放到泛型集合中,一樣會裝箱。既然不可避免,那也就沒人去關注這塊的性能影響了。
而 C# 中要是寫出這樣的代碼,那你明天不用來上班了。
同樣的場景發生在了學習 Python 的過程中。
什么?數據庫連接竟然沒有連接池!?
完全不可理解啊,Java 中不用連接池對性能影響挺大的。
Python 程序員是因為 Python 本來就慢,然后就自暴自棄了嗎?
突然想到一個笑話
問:為什么 Python 程序員很少談論內存泄漏?
答:因為 Python 重啟很快。
? 說多了都是淚,我之前排查 Java 內存泄漏的問題,超高并發的程序跑了1-2個月后就崩潰。我排查了好久,Java GC 參數也研究了很多,最后還是通過控制變量法找到了原因。
如果在 Python 中,多簡單的事啊,寫一個定時重啟腳本,解決…
問題來源那本文的問題怎么來的呢?正是我給公司的代碼加上連接池后產生的。一加連接池,就會有一定幾率出現;一去掉連接池,就不會有。
db = get_connection() try: cursor = db.cursor() if not cursor.execute("SELECT id FROM user WHERE name = %s", ["david"]) > 0: return None return cursor.fetchone()[0] finally: db.close()
一段很簡單的代碼,基本上整個項目中所有的數據庫查詢都是這么寫的,本來沒任何問題。
但當我給底層加上連接池后,問題來了。
這邊后報出這樣一個異常:"NoneType" object has no attribute "__getitem__"
意思就是說cursor.fetchone() 取出來的結果是None。
但是,代碼在調用之前命名已經檢查過affected rows了,根據文檔cursor.execute()返回的就是affected rows。
文檔也是這么寫的:Returns long integer rows affected, if any。
解決問題第一步:網上找答案什么測試驅動開發,敏捷開發,我覺得都不對,一句話形容我們那應該是:基于 Google 的 Bug 驅動開發。?
可惜網上無任何結果,去 stackoverflow 上問也沒人知道。
感覺又來到了一片無人區……
目前唯一能確認的就是和連接池相關了。
大致分析下應該是和連接復用有關,代碼沒寫好?底層連接池并發處理的代碼有 Bug?
先抓個詳細的異常看看吧。
解決問題第二步:分析異常日志我們項目用了 Sentry,一個異常跟蹤系統。可以把報錯時的調用堆棧和臨時變量都記錄下來。
第一個有用的信息是,我們竟然發現cursor.execute()的返回結果在 Sentry 上記錄的是18446744073709552000。
這是一個非常詭異的數字,因為它接近2^64-1 (18446744073709551615),而且還比它大了一點。
網上也找不到太多相關資料,和這個數字相關的都是 Javascript 相關的問題。
因為 Javascript 中是無法表示 2^64-1 的,相關討論:傳送門
簡單的一句話解釋就是:這個數字超過了 Javascript Integer 的最大范圍,所以底層用 Float 來表示了,所以導致丟失了精度。
但我們的程序沒用 Javascript。到了這邊,我們的第一反應一定是,要么 MySQL 出了 Bug。要么 MySQL-Python 出了 Bug。
解決問題第三步:一層層看源碼分析先看 MySQL-Python 源碼,cursor.execute()內部調用了affected_rows()方法得到了這個數字,而affected_rows()這個方法內部使用 C 實現了。
MySQL-Python 的 C 部分源碼很簡單,沒什么邏輯:
return PyLong_FromUnsignedLongLong(mysql_affected_rows(&(self->connection)));
看樣子也沒什么特別的,這里就兩個地方可能有問題,PyLong_FromUnsignedLongLong()和mysql_affected_rows()。
先自己嘗試寫了一段代碼,調用PyLong_FromUnsignedLongLong()函數,發現無論如何都不會出現18446744073709552000這個數字。
然后看 MySQL 源碼,mysql_affected_rows() 返回類型是my_ulonglong,源碼中其實是這么定義的:
typedef unsigned long long my_ulonglong;
也就是說,在 C 代碼中,這個數字最大就是2^64-1 (18446744073709551615),不可能返回18446744073709552000的。
然后在mysql_affected_rows()的官方文檔中又發現了一些有用的信息:
An integer greater than zero indicates the number of rows affected or retrieved. Zero indicates that no records were updated for an UPDATE statement, no rows matched the WHERE clause in the query or that no query has yet been executed. -1 indicates that the query returned an error or that, for a SELECT query, mysql_affected_rows() was called prior to calling mysql_store_result().
Because mysql_affected_rows() returns an unsigned value, you can check for -1 by comparing the return value to (my_ulonglong)-1 (or to (my_ulonglong)~0, which is equivalent).
好了,遇到第一個坑了,為什么 MySQL 官方文檔說這里可能有-1,而 MySQL-Python 的文檔中卻沒說?而且返回類型是無符號的,-1就變成18446744073709551615了。
那么如果我用if cursor.execute() > 0這種方式來判斷命中行數時,明明出錯了,我卻會得到True的結果了。
很明顯 MySQL-Python 寫的是有問題的,同事聯系了 MySQL-Python 的作者,作者承認了這里的問題,把代碼修復了,下一個版本會修復。
神奇的數字但是,看源碼發現的東西還是沒解決我們的問題,為什么我們的到的數字是18446744073709552000,而不是18446744073709551615?
整個調用鏈我們都檢查過了,不可能出現這個數字。
然后一個周末,我在快睡醒的時候突然想到了一個問題,這個數字是不是在 Python 報錯的時候,還是18446744073709551615,而到了 Sentry 中,就變成了18446744073709552000?
因為 Sentry Web 界面用的是 ajax,而 Javascript 中轉換這個數字的時候就會出錯。
最后一驗證,果然是 Sentry 的問題,Javascript 真的處處是坑。
好了,到了這一步,等 MySQL-Python 作者修復完后,我們的代碼也就不會報錯了。問題解決?
但是,MySQL 官方卻沒有說為什么這里會出現-1,而且為什么去掉了連接池就不會報錯?
就算我們的代碼不報錯了,但如果這里的返回數字不符合我們預期或者說不可控的話,會導致更多隱形的數據上的問題。
Root Cause目前為止,依然沒找到 Root Cause。
別動,看好了,我要用壓測大法了!既然這個問題是在高并發使用連接池時出現的,那就壓測看看能不能重現吧。
用了同樣的代碼,10個進程,沒有 sleep。沒想到不需要一分鐘,這個問題就會立刻重現。
而且每次重現時,都會有一些 MySQL 底層的警告,說出現了錯誤的調用順序。
這時,我試了一下加了一行代碼:
db = get_connection() cursor = None try: cursor = db.cursor() if not cursor.execute("SELECT id FROM user WHERE name = %s", ["david"]) > 0: return None return cursor.fetchone()[0] finally: if cursor: # new code cursor.close() # new code db.close()
加完后就再也沒看到任何錯誤了。
嗯,這里我們的代碼寫的是不到位,我后來仔細看了官方教程,是有主動關閉cursor的代碼的。(偷偷告訴你們,這里都是 CTO 以前寫的 ?)
粗略看了下cursor.close()的代碼,里面其實就是在把未讀完的數據讀完:while self.nextset(): pass。
那這里出問題的原因也就好理解了,高并發情況下復用連接池,如果上一次請求由于某些原因沒有讀完所有數據,后面直接復用這個連接的時候,就會出現問題了。
然后,我又奇怪了,連接池框架在關閉連接的時候不應該做清理工作嗎?
Java JDBC 源碼也看過不少了,Connection關閉的時候會清理Statement,Statement關閉的時候會清理ResultSet。因為單個連接只會在單線程中操作,是線程安全的,所以實現這樣的自動清理是非常簡單的。
以前寫 Java 中間件的時候,就總是把用戶當?,要盡量考慮各種情況避免內存泄漏。我們默認都是認為用戶是從來不會去調用close方法的。所以常常會想方設法幫用戶去自動處理。
解決問題最后要來解決問題了,代碼量很大,所有調用都改一遍其實也不難,因為這里都是有規律的,正則啊腳本啊什么的齊上陣,總是能解決的。
但是,其實也可以像 JDBC 那樣搞自動關閉。
class AutoCloseCursorConnection(object): cursor = None conn = None def __init__(self, conn): self.conn = conn def __getattr__(self, key): return getattr(self.conn, key) def cursor(self, *args, **kwargs): self.cursor = self.conn.cursor(*args, **kwargs) return self.cursor def close(self): if self.cursor: self.cursor.close() self.conn.close()
每次創建的連接包一下,就解決問題了。
源地址:http://www.dozer.cc/2016/07/mysql-connection-pool-in-python.html
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/38091.html
摘要:一普通連接方法使用模塊普通方式連接。返回結果表示影響的行數。查詢時不需要操作,插入更新刪除時需要提交。模塊點此下載類繼承自,表示一個新的連接池。如果需要新的連接池,按照如下格式新增即可。一個連接池可同時提供多個實例對象。 一、普通 MySQL 連接方法 ??使用模塊 MySQLdb 普通方式連接。 #!/usr/bin/env python # _*_ coding:utf-8 _*_...
摘要:另外,項目在單元測試中使用的是的內存數據庫,這樣開發者運行單元測試的時候不需要安裝和配置復雜的數據庫,只要安裝好就可以了。而且,數據庫是保存在內存中的,會提高單元測試的速度。是實現層的基礎。項目一般會使用數據庫來運行單元測試。 OpenStack中的關系型數據庫應用 OpenStack中的數據庫應用主要是關系型數據庫,主要使用的是MySQL數據庫。當然也有一些NoSQL的應用,比如Ce...
閱讀 2219·2019-08-30 15:53
閱讀 2444·2019-08-30 12:54
閱讀 1187·2019-08-29 16:09
閱讀 718·2019-08-29 12:14
閱讀 746·2019-08-26 10:33
閱讀 2461·2019-08-23 18:36
閱讀 2950·2019-08-23 18:30
閱讀 2111·2019-08-22 17:09