摘要:說多了都是淚,我之前排查內(nèi)存泄漏的問題,超高并發(fā)的程序跑了個月后就崩潰。以前寫中間件的時候,就總是把用戶當(dāng),要盡量考慮各種情況避免內(nèi)存泄漏。
從 Java 到 Python
本文為我和同事的共同研究成果
當(dāng)跨語言的時候,有些東西在一門語言中很常見,但到了另一門語言中可能會很少見。
例如 C# 中,經(jīng)常會關(guān)注拆箱裝箱,但到了 Java 中卻發(fā)現(xiàn),根本沒人關(guān)注這個。
后來才知道,原來是因?yàn)?Java 中沒有真泛型,就算放到泛型集合中,一樣會裝箱。既然不可避免,那也就沒人去關(guān)注這塊的性能影響了。
而 C# 中要是寫出這樣的代碼,那你明天不用來上班了。
同樣的場景發(fā)生在了學(xué)習(xí) Python 的過程中。
什么?數(shù)據(jù)庫連接竟然沒有連接池?。?/p>
完全不可理解啊,Java 中不用連接池對性能影響挺大的。
Python 程序員是因?yàn)?Python 本來就慢,然后就自暴自棄了嗎?
突然想到一個笑話
問:為什么 Python 程序員很少談?wù)搩?nèi)存泄漏?
答:因?yàn)?Python 重啟很快。
? 說多了都是淚,我之前排查 Java 內(nèi)存泄漏的問題,超高并發(fā)的程序跑了1-2個月后就崩潰。我排查了好久,Java GC 參數(shù)也研究了很多,最后還是通過控制變量法找到了原因。
如果在 Python 中,多簡單的事啊,寫一個定時重啟腳本,解決…
問題來源那本文的問題怎么來的呢?正是我給公司的代碼加上連接池后產(chǎn)生的。一加連接池,就會有一定幾率出現(xiàn);一去掉連接池,就不會有。
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()
一段很簡單的代碼,基本上整個項(xiàng)目中所有的數(shù)據(jù)庫查詢都是這么寫的,本來沒任何問題。
但當(dāng)我給底層加上連接池后,問題來了。
這邊后報出這樣一個異常:"NoneType" object has no attribute "__getitem__"
意思就是說cursor.fetchone() 取出來的結(jié)果是None。
但是,代碼在調(diào)用之前命名已經(jīng)檢查過affected rows了,根據(jù)文檔cursor.execute()返回的就是affected rows。
文檔也是這么寫的:Returns long integer rows affected, if any。
解決問題第一步:網(wǎng)上找答案什么測試驅(qū)動開發(fā),敏捷開發(fā),我覺得都不對,一句話形容我們那應(yīng)該是:基于 Google 的 Bug 驅(qū)動開發(fā)。?
可惜網(wǎng)上無任何結(jié)果,去 stackoverflow 上問也沒人知道。
感覺又來到了一片無人區(qū)……
目前唯一能確認(rèn)的就是和連接池相關(guān)了。
大致分析下應(yīng)該是和連接復(fù)用有關(guān),代碼沒寫好?底層連接池并發(fā)處理的代碼有 Bug?
先抓個詳細(xì)的異??纯窗?。
解決問題第二步:分析異常日志我們項(xiàng)目用了 Sentry,一個異常跟蹤系統(tǒng)??梢园褕箦e時的調(diào)用堆棧和臨時變量都記錄下來。
第一個有用的信息是,我們竟然發(fā)現(xiàn)cursor.execute()的返回結(jié)果在 Sentry 上記錄的是18446744073709552000。
這是一個非常詭異的數(shù)字,因?yàn)樗咏?b>2^64-1 (18446744073709551615),而且還比它大了一點(diǎn)。
網(wǎng)上也找不到太多相關(guān)資料,和這個數(shù)字相關(guān)的都是 Javascript 相關(guān)的問題。
因?yàn)?Javascript 中是無法表示 2^64-1 的,相關(guān)討論:傳送門
簡單的一句話解釋就是:這個數(shù)字超過了 Javascript Integer 的最大范圍,所以底層用 Float 來表示了,所以導(dǎo)致丟失了精度。
但我們的程序沒用 Javascript。到了這邊,我們的第一反應(yīng)一定是,要么 MySQL 出了 Bug。要么 MySQL-Python 出了 Bug。
解決問題第三步:一層層看源碼分析先看 MySQL-Python 源碼,cursor.execute()內(nèi)部調(diào)用了affected_rows()方法得到了這個數(shù)字,而affected_rows()這個方法內(nèi)部使用 C 實(shí)現(xiàn)了。
MySQL-Python 的 C 部分源碼很簡單,沒什么邏輯:
return PyLong_FromUnsignedLongLong(mysql_affected_rows(&(self->connection)));
看樣子也沒什么特別的,這里就兩個地方可能有問題,PyLong_FromUnsignedLongLong()和mysql_affected_rows()。
先自己嘗試寫了一段代碼,調(diào)用PyLong_FromUnsignedLongLong()函數(shù),發(fā)現(xiàn)無論如何都不會出現(xiàn)18446744073709552000這個數(shù)字。
然后看 MySQL 源碼,mysql_affected_rows() 返回類型是my_ulonglong,源碼中其實(shí)是這么定義的:
typedef unsigned long long my_ulonglong;
也就是說,在 C 代碼中,這個數(shù)字最大就是2^64-1 (18446744073709551615),不可能返回18446744073709552000的。
然后在mysql_affected_rows()的官方文檔中又發(fā)現(xiàn)了一些有用的信息:
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這種方式來判斷命中行數(shù)時,明明出錯了,我卻會得到True的結(jié)果了。
很明顯 MySQL-Python 寫的是有問題的,同事聯(lián)系了 MySQL-Python 的作者,作者承認(rèn)了這里的問題,把代碼修復(fù)了,下一個版本會修復(fù)。
神奇的數(shù)字但是,看源碼發(fā)現(xiàn)的東西還是沒解決我們的問題,為什么我們的到的數(shù)字是18446744073709552000,而不是18446744073709551615?
整個調(diào)用鏈我們都檢查過了,不可能出現(xiàn)這個數(shù)字。
然后一個周末,我在快睡醒的時候突然想到了一個問題,這個數(shù)字是不是在 Python 報錯的時候,還是18446744073709551615,而到了 Sentry 中,就變成了18446744073709552000?
因?yàn)?Sentry Web 界面用的是 ajax,而 Javascript 中轉(zhuǎn)換這個數(shù)字的時候就會出錯。
最后一驗(yàn)證,果然是 Sentry 的問題,Javascript 真的處處是坑。
好了,到了這一步,等 MySQL-Python 作者修復(fù)完后,我們的代碼也就不會報錯了。問題解決?
但是,MySQL 官方卻沒有說為什么這里會出現(xiàn)-1,而且為什么去掉了連接池就不會報錯?
就算我們的代碼不報錯了,但如果這里的返回數(shù)字不符合我們預(yù)期或者說不可控的話,會導(dǎo)致更多隱形的數(shù)據(jù)上的問題。
Root Cause目前為止,依然沒找到 Root Cause。
別動,看好了,我要用壓測大法了!既然這個問題是在高并發(fā)使用連接池時出現(xiàn)的,那就壓測看看能不能重現(xiàn)吧。
用了同樣的代碼,10個進(jìn)程,沒有 sleep。沒想到不需要一分鐘,這個問題就會立刻重現(xiàn)。
而且每次重現(xiàn)時,都會有一些 MySQL 底層的警告,說出現(xiàn)了錯誤的調(diào)用順序。
這時,我試了一下加了一行代碼:
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()
加完后就再也沒看到任何錯誤了。
嗯,這里我們的代碼寫的是不到位,我后來仔細(xì)看了官方教程,是有主動關(guān)閉cursor的代碼的。(偷偷告訴你們,這里都是 CTO 以前寫的 ?)
粗略看了下cursor.close()的代碼,里面其實(shí)就是在把未讀完的數(shù)據(jù)讀完:while self.nextset(): pass。
那這里出問題的原因也就好理解了,高并發(fā)情況下復(fù)用連接池,如果上一次請求由于某些原因沒有讀完所有數(shù)據(jù),后面直接復(fù)用這個連接的時候,就會出現(xiàn)問題了。
然后,我又奇怪了,連接池框架在關(guān)閉連接的時候不應(yīng)該做清理工作嗎?
Java JDBC 源碼也看過不少了,Connection關(guān)閉的時候會清理Statement,Statement關(guān)閉的時候會清理ResultSet。因?yàn)閱蝹€連接只會在單線程中操作,是線程安全的,所以實(shí)現(xiàn)這樣的自動清理是非常簡單的。
以前寫 Java 中間件的時候,就總是把用戶當(dāng)?,要盡量考慮各種情況避免內(nèi)存泄漏。我們默認(rèn)都是認(rèn)為用戶是從來不會去調(diào)用close方法的。所以常常會想方設(shè)法幫用戶去自動處理。
解決問題最后要來解決問題了,代碼量很大,所有調(diào)用都改一遍其實(shí)也不難,因?yàn)檫@里都是有規(guī)律的,正則啊腳本啊什么的齊上陣,總是能解決的。
但是,其實(shí)也可以像 JDBC 那樣搞自動關(guān)閉。
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()
每次創(chuàng)建的連接包一下,就解決問題了。
源地址:http://www.dozer.cc/2016/07/mysql-connection-pool-in-python.html
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/38091.html
摘要:一普通連接方法使用模塊普通方式連接。返回結(jié)果表示影響的行數(shù)。查詢時不需要操作,插入更新刪除時需要提交。模塊點(diǎn)此下載類繼承自,表示一個新的連接池。如果需要新的連接池,按照如下格式新增即可。一個連接池可同時提供多個實(shí)例對象。 一、普通 MySQL 連接方法 ??使用模塊 MySQLdb 普通方式連接。 #!/usr/bin/env python # _*_ coding:utf-8 _*_...
摘要:另外,項(xiàng)目在單元測試中使用的是的內(nèi)存數(shù)據(jù)庫,這樣開發(fā)者運(yùn)行單元測試的時候不需要安裝和配置復(fù)雜的數(shù)據(jù)庫,只要安裝好就可以了。而且,數(shù)據(jù)庫是保存在內(nèi)存中的,會提高單元測試的速度。是實(shí)現(xiàn)層的基礎(chǔ)。項(xiàng)目一般會使用數(shù)據(jù)庫來運(yùn)行單元測試。 OpenStack中的關(guān)系型數(shù)據(jù)庫應(yīng)用 OpenStack中的數(shù)據(jù)庫應(yīng)用主要是關(guān)系型數(shù)據(jù)庫,主要使用的是MySQL數(shù)據(jù)庫。當(dāng)然也有一些NoSQL的應(yīng)用,比如Ce...
閱讀 2291·2019-08-30 15:53
閱讀 2510·2019-08-30 12:54
閱讀 1376·2019-08-29 16:09
閱讀 779·2019-08-29 12:14
閱讀 808·2019-08-26 10:33
閱讀 2569·2019-08-23 18:36
閱讀 3026·2019-08-23 18:30
閱讀 2186·2019-08-22 17:09