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