Python慢,為啥還有大公司用?
來自:簡書,作者:我愛學python ???鏈接:
https://www.jianshu.com/p/e18e01ad7ad9
前言
PyCon 是全世界最大的以 Python 編程語言 為主題的技術大會,大會由 Python 社區(qū)組織,每年舉辦一次。在 Python 2017 上,Instagram 的工程師們帶來了一個有關 Python 在 Instagram 的主題演講,同時還分享了 Instagram 如何將整個項目運行環(huán)境升級到 Python 3 的故事。本文為該次演講的內容摘要,由 Python 愛好者朱雷撰寫。Instagram 是一款移動端的照片與視頻分享軟件,由 Kevin Systrom 和 Mike Krieger 在 2010 年創(chuàng)辦。Instagram 在發(fā)布后開始快速流行。于 2012 年被 Facebook 以 10 億美元的價格收購。而當時 Instagram 的員工僅有區(qū)區(qū) 13 名。如今,Instagram 的總注冊用戶達到 30 億,月活用戶超過 7 億 (作為對比,微信最新披露的月活躍用戶為 9.38 億)。而令人吃驚的是,這么高的訪問量背后,竟完全是由以速度慢著稱的 Python + Django 支撐。
為什么選擇 Python 和 Django
Instagram 選擇 Django 的原因很簡單,Instagram 的兩位創(chuàng)始人 (Kevin Systrom and Mike Krieger) 都是產(chǎn)品經(jīng)理出身。在他們想要創(chuàng)造 Instagram 時,Django 是他們所知道的最穩(wěn)定和成熟的技術之一。時至今日,即使已經(jīng)擁有超過 30 億的注冊用戶。Instagram 仍然是 Python 和 Django 的重度使用者。Instagram 的工程師 Hui Ding 說到:『一直到用戶 ID 已經(jīng)超過了 32bit int 的限額(約為 20 億),Django 本身仍然沒有成為我們的瓶頸所在?!?/span>不過,除了使用 Django 的原生功能外,Instagram 還對 Django 做了很多定制化工作:- 擴展 Django Models 使其支持 Sharding (一種數(shù)據(jù)庫分片技術)。
- 手動關閉 GC(垃圾回收)來提升 Python 內存管理效率,他們同樣也寫過一篇博客來說明這件事情:Dismissing Python Garbage Collection at Instagram。
- 在位于不同地理位置的多個數(shù)據(jù)中心部署整套系統(tǒng)。
Python 語言的優(yōu)勢所在
Instagram 的聯(lián)合創(chuàng)始人 Mike Krieger 說過:『我們的用戶根本不關心 Instagram 使用了哪種關系數(shù)據(jù)庫,他們當然也不關心 Instagram 是用什么編程語言開發(fā)的?!?/span>所以,Python 這種 簡單 而且 實用至上 的編程語言最終贏得了 Instagram 的青睞。他們認為,使用 Python 這種簡單的語言有助于塑造 Instagram 的工程師文化,那就是:- 專注于定位問題、解決問題 - 而不是工具本身的各種花花綠綠的特性
- 使用那些經(jīng)過市場驗證過的成熟技術方案 - 而不用被工具本身的問題所煩擾
- 用戶至上:專注于用戶所能看到的新特性,為用戶帶去價值
At Instagram, our bottleneck is development velocity, not pure code execution.所以,最終的結論是:你完全可以使用 Python 語言來實現(xiàn)一個超過幾十億用戶使用的產(chǎn)品,而根本不用擔心語言或框架本身的性能瓶頸。
如何提升運行效率
但是,即使是選用了擁有諸多好處的 Python 和 Django。在 Instagram 的用戶數(shù)迅速增長的過程中,性能問題還是出現(xiàn)了:服務器數(shù)量的增長率已經(jīng)慢慢的超過了用戶增長率。Instagram 是怎么應對這個問題的呢?他們使用了這些手段來緩解性能問題:- 開發(fā)工具來幫助調優(yōu):Instagram 開發(fā)了很多涵蓋各個層面的工具,來幫助他們進行性能調優(yōu)以及找到性能瓶頸。
- 使用 C/C++ 來重寫部分組件:把那些穩(wěn)定而且對性能最敏感的組件,使用 C 或 C++ 來重寫,比如訪問 memcache 的 library。
- 使用 Cython:Cython 也是他們用來提升 Python 效率的法寶之一。
為什么要升級到 Python 3
在相當長的一段時間,Instagram 都跑在 Python 2.7 + Django 1.3 的組合之上。在這個已經(jīng)落后社區(qū)很多年的環(huán)境上,他們的工程師們還打了非常非常多的小 patch。難道他們要被永遠卡在這個版本上嗎?所以,在經(jīng)過一系列的討論后,他們最終做出一個重大的決定:升級到 Python 3??!事實上,Instagram 目前已經(jīng)完成了將運行環(huán)境遷移到 Python 3 的工作 - 他們的整套服務已經(jīng)在 Python 3 上跑了好幾個月了。那么他們是怎么做到的呢?接下來便是由 Instagram 工程師 Lisa guo 帶來的 Instagram 如何遷移到 Python 3 的故事。對于 Instagram 來說,下面這些因素是推動他們將運行環(huán)境遷移到 Python 3 的主要原因:- 新特性:類型注解 Type Annotations
def?compose_from_max_id(max_id):
'''@param str max_id'''圖中函數(shù)的 max_id 參數(shù)究竟是什么類型呢?int?tuple?或是 list? 等等,函數(shù)文檔里面說它是 str 類型。但隨著時間推移,萬一這個參數(shù)的類型發(fā)生變化了呢?如果某位粗心的工程師修改代碼的同時忘了更新文檔,那就會給函數(shù)的使用者帶來很大麻煩,最終還不如沒有注釋呢。
2、性能
Instagram 的整個 Django Stack 都跑在 uwsgi 之上,全部使用了同步的網(wǎng)絡 IO。這意味著同一個 uwsgi 進程在同一時間只能接收并處理一個請求。這讓如何調優(yōu)每臺機器上應該運行的 uwsgi 進程數(shù)成了一個麻煩事:為了更好利用 CPU,使用更多的進程數(shù)?但那樣會消耗大量的內存。而過少的進程數(shù)量又會導致 CPU 不能被充分利用。為此,他們決定跳過 Python 2 中哪些蹩腳的異步 IO 實現(xiàn) (可憐的 gevent、tornado、twisted 眾),直接升級到 Python 3,去探索標準庫中的 asyncio 模塊所能帶來的可能性。3、社區(qū)因為 Python 社區(qū)已經(jīng)停止了對 Python 2 的支持。如果把整個運行環(huán)境升級到 Python 3,Instagram 的工程師們就能和 Python 社區(qū)走的更近,可以更好的把他們的工作回饋給社區(qū)。遷移方案在 Instagram,進行 Python 3 的遷移需要必須滿足兩個前提條件:- 不停機,不能有任何的服務因此不可用
- 不能影響產(chǎn)品新特性的開發(fā)
- Instagram 的 Codebase 每天都在頻繁更新,在開發(fā) Python 3 分支的過程中,讓新分支與現(xiàn)有 master 分支保持同步開銷極大,同時極易出錯
- 最終將 Python 3 分支這個改動非常多的分支合并回 Master 擁有非常高的風險
- 只有少數(shù)幾個工程師在 Python 3 分支上專職負責升級工作,其他想幫助遷移工作的工程師無法參與進來
正式遷移到 Python 3
既然要讓整個 codebase 同時兼容 Python 2 和 Python 3,那么首先要符合這點的就是那些被大量使用的第三方 package。針對第三方 package,Instagram 做到了下面幾點:- 拒絕引入所有不兼容 Python 3 的新 package
- 去掉所有不再使用的 package
- 替換那些不兼容 Python 3 的 package
- Instagram 的單元測試沒有做到 100% 的代碼覆蓋率
- 很多第三方模塊都使用了 mock 技術,而 mock 的行為與真實的線上服務可能會有所不同

遷移過程的技術問題
Instagram 在遷移到 Python 3 時碰到很多問題,下面是最典型的幾個:Unicode 相關的字符串問題Python 3 相比 Python 2 最大的改動之一,就是在語言內部對 unicode 的處理。在 Python 2 中,文本類型 (也就是 unicode) 和二進制類型 (也就是 str) 的邊界非常模糊。很多函數(shù)的參數(shù)既可以是文本,也可以是二進制。但是在 Python 3 中,文本類型和二進制類型的字符串被完全的區(qū)分開了。于是,下面這段在 Python 2 下可以正常運行的代碼在 Python 3 下就會報錯:mymac?= hmac.new('abc') TypeError: key: expected bytes or bytearray, but got?'str'[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-tQT44Q0M-1570179360052)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]解決辦法其實很簡單,只要加上判斷:如果 value 是文本類型,就將其轉換為二進制。如下所示:
'''
遇到問題沒人解答?小編創(chuàng)建了一個Python學習交流QQ群:尋找有志同道合的小伙伴,
互幫互助,群里還有不錯的視頻學習教程和PDF電子書!
'''
value =?'abc'if?isinstance(value, six.text_type): value = value.encode(encoding='utf-8') mymac = hmac.new(value)但是,在整個代碼庫中,像上面這樣的情況非常多。作為開發(fā)人員,如果需要在調用每個函數(shù)時都要想想:這里到底是應該編碼成二進制,或者是解碼成文本呢?將會是非常大的負擔。于是 Instagram 封裝了一些名為 ensure_str()、ensure_binary()、ensure_text() 的幫助函數(shù),開發(fā)人員只需對那些不確定類型的字符串,使用這些幫助函數(shù)先做一次轉換就好。
mymac?= hmac.new(ensure_binary('abc'))[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Ls5jOGEl-1570179360053)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]不同 Python 版本的 pickle 差異Instagram 的代碼中大量使用了 pickle。比如用它序列化某個對象,然后將其存儲在 memcache 中。如下面的代碼所示:
memcache_data?= pickle.dumps(data, pickle.HIGHEST_PROTOCOL)data = pickle.loads(memcache_data)問題在于,Python 2 與 Python 3 的 pickle 模塊是有差別的。如果上文的第一行代碼,剛好是由 Python 3 運行的服務進行序列化后存入 memcache。而反序列化的過程卻是由 Python 2 進行,那代碼運行時就會出現(xiàn)下面的錯誤:
ValueError: unsupported pickle protocol: 4這是由于在 Python 3 中,pickle.HIGHEST_PROTOCOL 的值為 4,而 Python 2 中的的 pickle 最高支持的版本號卻是 2。那么如何解決這個問題呢?Instagram 最終選擇讓 Python 2 和 Python 3 使用完全不同的 namespace 來訪問 memcache。通過將二者的數(shù)據(jù)讀寫完全隔開來解決這個問題。迭代器在 Python 3 中,很多內置函數(shù)被修改成了只返成迭代器 Iterator:
map()?filter() dict.items()[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-GLVPUDc0-1570179360059)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]迭代器有諸多好處,最大的好處就是,使用迭代器不需要一次性分配大量內存,所以它的內存效率比較高。但是迭代器有一個天然的特點,當你對某個迭代器做了一次迭代,訪問完它的內容后,就沒法再次訪問那些內容了。迭代器中的所有內容都只能被訪問一次。在 Instagram 的 Python 3 遷移過程中,就因為迭代器的這個特性被坑了一次,看看下面這段代碼:
'''
遇到問題沒人解答?小編創(chuàng)建了一個Python學習交流QQ群:尋找有志同道合的小伙伴,
互幫互助,群里還有不錯的視頻學習教程和PDF電子書!
'''
CYTHON_SOURCES = [a.pyx, b.pyx, c.pyx] builds = map(BuildProcess, CYTHON_SOURCES)while?any(not?build.done()?for?build?in?builds): pending = [build?for?build?in?builds?if?not?build.started()] 這段代碼的用處是挨個編譯 Cython 源文件。當他們把運行環(huán)境切換到 Python 3 后,一個奇怪的問題出現(xiàn)了:CYTHON_SOURCES 中的第一個文件永遠都被跳過了編譯。為什么呢?這都是迭代器的鍋。在 Python 3 中,map() 函數(shù)不再返回整個 list,而是返回一個迭代器。于是,當?shù)诙写a生成 builds 這個迭代器后,第三行代碼的 while 循環(huán)迭代了 builds,剛好取出了第一個元素。于是之后的 pending 對象便里面永遠少了那第一個元素。這個問題解決起來也挺簡單的,你只要手動的吧 builds 轉換成 list 就可以了:
builds?= list(map(BuildProcess, CYTHON_SOURCES))但是這類 bug 非常難定位到。如果用戶的 feeds 里面永遠少了那最新的第一條,用戶很少會注意到。字典的順序看看下面這段代碼:
>>> testdict = {'a': 1, 'b': 2, 'c': 3}>>> json.dumps(testdict)
它會輸出什么結果呢?# Python2'{"a": 1, "c": 3, "b": 2}'# Python 3.5.1'{"c": 3, "b": 2, "a": 1}' # or'{"c": 3, "a": 1, "b": 2}'# Python 3.6'{"a": 1, "b": 2, "c": 3}'
在不同的 Python 版本下,這個 json dumps 的結果是完全不一樣的。甚至在 3.5.1 中,它會完全隨機的返回兩個不同的結果。Instagram 有一段判斷配置文件是否發(fā)生變動的模塊,就是因為這個原因出了問題。這個問題的解決辦法是,在調用 json.dumps 傳入 sort_keys=True 參數(shù):>>> json.dumps(testdict, sort_keys=True)'{"a": 1, "b": 2, "c": 3}'遷移到 Python 3.6 后的性能提升
當 Instagram 解決了這些奇奇怪怪的版本差異問題后,還有一個巨大的謎題困擾著他們:性能問題。在 Instagram,他們使用兩個主要指標來衡量他們的服務性能:- 每次請求產(chǎn)生的 CPU 指令數(shù)(越低越好)
- 每秒能夠處理的請求數(shù)(越高越好)
if uwsgi.opt.get('optimize_mem', None) == 'True': optimize_mem()
注意到那段... ... == 'True'了嗎?在 Python 3 中,這個條件判斷總是不會被滿足。問題就在于 unicode。在將代碼中的'True'換成?b'True'(也就是將文本類型換成二進制,這種判斷在 Python 2 中完全不區(qū)分的)后,問題解決了。所以,最終因為加上了一個小小的字母?'b',程序的整體性能提升了 12%。完美切換
在今年二月份,Instagram 的后端代碼的運行環(huán)境完全切換到了 Python 3 下:
當所有的代碼都都遷移到 Python 3 運行環(huán)境后:- 節(jié)約了 12% 的整體 CPU 使用率(Django/uwsgi)
- 節(jié)約了 30% 的內存使用(celery)
- 類型注解:Instagram 的整個 codebase 里已經(jīng)有 2% 的代碼添加上了類型注解,同時他們還開發(fā)了一些工具來輔助開發(fā)者添加類型提示
- asyncio:他們在單個接口中利用 asynio 平行的去做多件事情,最終降低了 20-30% 的請求延遲。
- 社區(qū):他們與 Intel 的工程師聯(lián)合,幫助他們更好的對 CPU 利用率進行調優(yōu)。同時還開發(fā)了很多新的工具,幫助他們進行性能調優(yōu)
Instagram 帶給我們的啟示
Instagram 的演講視頻時間不長,但是內容很豐富,在編寫此文前,我完全沒有想到最終的文章會這么長。那么總結一下,Instagram 的視頻可以給我們哪些啟示呢?- Python + Django 的組合完全可以負載用戶數(shù)以 10 億記的服務,如果你正準備開始一個項目,放心使用 Python 吧!
- 完善的單元測試對于復雜項目是非常有必要的。如果沒有那『成千上萬的單元測試』。很難想象 Instagram 的遷移項目可以成功進行下去。
- 開發(fā)者和同事也是你的產(chǎn)品用戶,利用好他們。用他們?yōu)槟愕男绿匦园l(fā)布前多一道測試。
- 完全基于主分支的開發(fā)流程,可以給你更快的迭代速度。前提是擁有完善的單元測試和持續(xù)部署流程。
- Python 3 是大勢所趨,如果你正準備開始一個新項目,無需遲疑,擁抱 Python 3 吧!
戀習Python關注戀習Python,Python都好練
推薦閱讀:
Python問:年輕人,你渴望力量嗎?(附:Python詳細代碼)
好文章,我在看??
評論
圖片
表情
