PyCon 是全世界最大的以 Python 編程語言 爲主題的技術大會,大會由 Python 社區組織,每一年舉辦一次。在 Python 2017 上,Instagram 的工程師們帶來了一個有關 Python 在 Instagram 的主題演講,同時還分享了 Instagram 如何將整個項目運行環境升級到 Python 3 的故事。本文爲該次演講的內容摘要,由 Python 愛好者朱雷撰寫,聊聊架構經受權發佈。git
Instagram 是一款移動端的照片與視頻分享軟件,由 Kevin Systrom 和 Mike Krieger 在 2010 年創辦。Instagram 在發佈後開始快速流行。於 2012 年被 Facebook 以 10 億美圓的價格收購。而當時 Instagram 的員工僅有區區 13 名。數據庫
現在,Instagram 的總註冊用戶達到 30 億,月活用戶超過 7 億 (做爲對比,微信最新披露的月活躍用戶爲 9.38 億)。而使人吃驚的是,這麼高的訪問量背後,竟徹底是由以速度慢著稱的 Python + Django 支撐。編程
Instagram 選擇 Django 的緣由很簡單,Instagram 的兩位創始人 (Kevin Systrom and Mike Krieger) 都是產品經理出身。在他們想要創造 Instagram 時,Django 是他們所知道的最穩定和成熟的技術之一。json
時至今日,即便已經擁有超過 30 億的註冊用戶。Instagram 仍然是 Python 和 Django 的重度使用者。Instagram 的工程師 Hui Ding 說到: 『一直到用戶 ID 已經超過了 32bit int 的限額(約爲 20 億),Django 自己仍然沒有成爲咱們的瓶頸所在。』後端
不過,除了使用 Django 的原生功能外,Instagram 還對 Django 作了不少定製化工做:服務器
Instagram 的聯合創始人 Mike Krieger 說過: 『咱們的用戶根本不關心 Instagram 使用了哪一種關係數據庫,他們固然也不關心 Instagram 是用什麼編程語言開發的。』微信
因此,Python 這種 簡單 並且 實用至上 的編程語言最終贏得了 Instagram 的青睞。他們認爲,使用 Python 這種簡單的語言有助於塑造 Instagram 的工程師文化,那就是:網絡
可是,即便使用 Python 語言有這麼多好處,它仍是很慢,不是嗎?架構
不過,這對於 Instagram 不是問題,由於他們認爲:『Instagram 的最大瓶頸在於開發效率,而不是代碼的執行效率』。app
At Instagram, our bottleneck is development velocity, not pure code execution.
因此,最終的結論是:你徹底可使用 Python 語言來實現一個超過幾十億用戶使用的產品,而根本不用擔憂語言或框架自己的性能瓶頸。
如何提高運行效率
可是,即便是選用了擁有諸多好處的 Python 和 Django。在 Instagram 的用戶數迅速增加的過程當中,性能問題仍是出現了:服務器數量的增加率已經慢慢的超過了用戶增加率。Instagram 是怎麼應對這個問題的呢?
他們使用了這些手段來緩解性能問題:
除了上面這些手段,他們還在探索異步 IO 以及新的 Python Runtime 所能帶來的性能可能性。
在至關長的一段時間,Instagram 都跑在 Python 2.7 + Django 1.3 的組合之上。在這個已經落後社區不少年的環境上,他們的工程師們還打了很是很是多的小 patch。難道他們要被永遠卡在這個版本上嗎?
因此,在通過一系列的討論後,他們最終作出一個重大的決定:升級到 Python 3!!
事實上,Instagram 目前已經完成了將運行環境遷移到 Python 3 的工做 - 他們的整套服務已經在 Python 3 上跑了好幾個月了。那麼他們是怎麼作到的呢?接下來即是由 Instagram 工程師 Lisa guo 帶來的 Instagram 如何遷移到 Python 3 的故事。
對於 Instagram 來講,下面這些因素是推進他們將運行環境遷移到 Python 3 的主要緣由:
看看下面這段代碼:
def compose_from_max_id(max_id): '''@param str max_id'''
圖中函數的 max_id 參數到底是什麼類型呢?int?tuple?或是 list? 等等,函數文檔裏面說它是 str 類型。
但隨着時間推移,萬一這個參數的類型發生變化了呢?若是某位粗心的工程師修改代碼的同時忘了更新文檔,那就會給函數的使用者帶來很大麻煩,最終還不如沒有註釋呢。
Instagram 的整個 Django Stack 都跑在 uwsgi 之上,所有使用了同步的網絡 IO。這意味着同一個 uwsgi 進程在同一時間只能接收並處理一個請求。這讓如何調優每臺機器上應該運行的 uwsgi 進程數成了一個麻煩事:
爲了更好利用 CPU,使用更多的進程數?但那樣會消耗大量的內存。而過少的進程數量又會致使 CPU 不能被充分利用。
爲此,他們決定跳過 Python 2 中哪些蹩腳的異步 IO 實現 (可憐的 gevent、tornado、twisted 衆),直接升級到 Python 3,去探索標準庫中的 asyncio 模塊所能帶來的可能性。
由於 Python 社區已經中止了對 Python 2 的支持。若是把整個運行環境升級到 Python 3,Instagram 的工程師們就能和 Python 社區走的更近,能夠更好的把他們的工做回饋給社區。
在 Instagram,進行 Python 3 的遷移須要必須知足兩個前提條件:
不停機,不能有任何的服務所以不可用
不能影響產品新特性的開發
可是,在 Instagram 的開發環境中,要知足上面這兩點來完成遷移到 Python 3.6 這種龐大的工程是很是困難的。
即使使用了以多分支功能著稱的 git,Instagram 全部的開發工做都是主要在 master 分支上進行的,Instagram 所奉行的開發哲學是:『無論是多大的新特性或代碼重構,都應該拆解成較小的 Commit 來進行。』
那些被合併進 master 分支的代碼,都將在一個小時內被髮布到線上環境。而這樣的發佈過程天天將會發生上百次。在這麼頻繁的發佈頻率下,如何在知足以前的那兩個前提下來完成遷移變得尤爲困難。
不少人在處理這類問題時,第一個蹦進腦子的想法就是: 『讓咱們建立一個分支,當咱們開發完後,再把分支合併進來』。但在 Instagram 這麼高的迭代頻率上,使用一個獨立分支並非好主意:
還有一個方案就是,挨個替換 Instagram 的 API 接口。可是 Instagram 的不一樣接口共享着不少通用模塊。這個方案要實施起來也很是困難。
還有一個方案就是將 Instagram 改形成微服務架構。經過將那些通用模塊重寫成 Python 3 版本的微服務來一步步完成遷移工做。
可是這個方案須要從新組織海量的代碼。同時,當發生在進程內的函數調用變成 RPC 後 ,整個站點的延遲會變大。此外,更多的微服務也會引入更高的部署複雜度。
因此,既然 Instagram 的開發哲學是:小步前進,快速迭代。他們最終決定的方案是:一步一步來,最終讓 master 分支上的代碼同時兼容 Python 2 和 Python 3 。
既然要讓整個 codebase 同時兼容 Python 2 和 Python 3,那麼首先要符合這點的就是那些被大量使用的第三方 package。針對第三方 package,Instagram 作到了下面幾點:
在代碼的遷移過程當中,他們使用了工具 modernize 來幫助他們。
使用 modernize 時,有一個小技巧:每次修復多個文件的一個兼容問題,而不是一下修復一個文件中的多個兼容問題。 這樣可讓 Code Review 過程簡單不少,由於 Reviewer 每次只須要關注一個問題。
對於 Python 這種靈活性極強的動態語言來講,除了真正去執行代碼外,幾乎沒有其餘比較好的檢查代碼錯誤的手段。
前面提到,Instagram 全部被合併到 master 的代碼提交會在一個小時內上線到線上環境,但這不是沒有前提條件的。在上線前,全部的提交都須要經過成千上萬個單元測試。
因而,他們開始加入 Python 3 來執行全部的單元測試。一開始,只有極少數的單元測試可以在 Python 3 環境下經過,但隨着 Instagram 的工程師們不斷的修復那些失敗的單元測試,最終全部的單元測試均可以在 Python 3 環境下成功執行。
可是,單元測試也是有侷限性的:
因此,當全部的單元測試都被修復後,他們開始在線上正式使用 Python 3 來運行服務。
這個過程並非一蹴而就的。首先,全部的 Instagram 工程師開始訪問到這些使用 Python 3 來執行的新服務,而後是 Facebook 的全部僱員,隨後是 0.1%、20% 的用戶,最終 Python 3 覆蓋到了全部的 Instagram 用戶。
Instagram 在遷移到 Python 3 時碰到不少問題,下面是最典型的幾個:
Python 3 相比 Python 2 最大的改動之一,就是在語言內部對 unicode 的處理。
在 Python 2 中,文本類型 (也就是 unicode) 和二進制類型 (也就是 str) 的邊界很是模糊。不少函數的參數既能夠是文本,也能夠是二進制。可是在 Python 3 中,文本類型和二進制類型的字符串被徹底的區分開了。
因而,下面這段在 Python 2 下能夠正常運行的代碼在 Python 3 下就會報錯:
mymac = hmac.new('abc') TypeError: key: expected bytes or bytearray, but got 'str'
解決辦法其實很簡單,只要加上判斷:若是 value 是文本類型,就將其轉換爲二進制。以下所示:
value = 'abc' if isinstance(value, six.text_type): value = value.encode(encoding='utf-8') mymac = hmac.new(value)
可是,在整個代碼庫中,像上面這樣的狀況很是多。做爲開發人員,若是須要在調用每一個函數時都要想一想: 這裏究竟是應該編碼成二進制,或者是解碼成文本呢? 將會是很是大的負擔。
因而 Instagram 封裝了一些名爲 ensure_str()、ensure_binary()、ensure_text() 的幫助函數,開發人員只需對那些不肯定類型的字符串,使用這些幫助函數先作一次轉換就好。
mymac = hmac.new(ensure_binary('abc'))
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 進行,那代碼運行時就會出現下面的錯誤:
ValueError: unsupported pickle protocol: 4
這是因爲在 Python 3 中,pickle.HIGHEST_PROTOCOL 的值爲 4,而 Python 2 中的的 pickle 最高支持的版本號倒是 2。那麼如何解決這個問題呢?
Instagram 最終選擇讓 Python 2 和 Python 3 使用徹底不一樣的 namespace 來訪問 memcache。經過將兩者的數據讀寫徹底隔開來解決這個問題。
在 Python 3 中,不少內置函數被修改爲了只返成迭代器 Iterator:
map() filter() dict.items()
迭代器有諸多好處,最大的好處就是,使用迭代器不須要一次性分配大量內存,因此它的內存效率比較高。
可是迭代器有一個自然的特色,當你對某個迭代器作了一次迭代,訪問完它的內容後,就無法再次訪問那些內容了。迭代器中的全部內容都只能被訪問一次。
在 Instagram 的 Python 3 遷移過程當中,就由於迭代器的這個特性被坑了一次,看看下面這段代碼:
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()] <do some work>
這段代碼的用處是挨個編譯 Cython 源文件。當他們把運行環境切換到 Python 3 後,一個奇怪的問題出現了:CYTHON_SOURCES 中的第一個文件永遠都被跳過了編譯。爲何呢?
這都是迭代器的鍋。在 Python 3 中,map() 函數再也不返回整個 list,而是返回一個迭代器。
因而,當第二行代碼生成 builds 這個迭代器後,第三行代碼的 while 循環迭代了 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 有一段判斷配置文件是否發生變更的模塊,就是由於這個緣由出了問題。
這個問題的解決辦法是,在調用 json.dumps 傳入 sort_keys=True 參數:
>>> json.dumps(testdict, sort_keys=True) '{"a": 1, "b": 2, "c": 3}'
當 Instagram 解決了這些奇奇怪怪的版本差別問題後,還有一個巨大的謎題困擾着他們:性能問題。
在 Instagram,他們使用兩個主要指標來衡量他們的服務性能:
因此,當全部的遷移工做完成後,他們很是驚喜的發現:第一個性能指標,每次請求產生的 CPU 指令數竟然足足降低了 12% !!!
可是,按理說第二個指標 - 每秒請求數也應該得到接近 12% 的提高。不過最後的變化倒是 0%。到底是出了什麼問題呢?
他們最終定位到,是因爲不一樣 Python 版本下的內存優化配置不一樣,致使 CPU 指令數降低帶來的性能提高被抵消了。那爲何不一樣 Python 版本下的內存優化配置會不同呢?
這是他們用來檢查 uwsgi 配置的代碼:
if uwsgi.opt.get('optimize_mem', None) == 'True': optimize_mem()
注意到那段... ... == 'True'
了嗎?在 Python 3 中,這個條件判斷老是不會被知足。問題就在於 unicode。在將代碼中的'True'
換成 b'True'
(也就是將文本類型換成二進制,這種判斷在 Python 2 中徹底不區分的)後,問題解決了。
因此,最終由於加上了一個小小的字母 'b'
,程序的總體性能提高了 12%。
在今年二月份,Instagram 的後端代碼的運行環境徹底切換到了 Python 3 下:
當全部的代碼都都遷移到 Python 3 運行環境後:
同時,在整個遷移期間,Instagram 的月活用戶經歷了從 4 億到 6 億 的巨大增加。產品也發佈了評論過濾、直播等很是多新功能。
那麼,那幾個最開始驅動他們遷移到 Python 3 的目的呢?
Instagram 的演講視頻時間不長,可是內容很豐富,在編寫此文前,我徹底沒有想到最終的文章會這麼長。
那麼,Instagram 的視頻能夠給咱們哪些啓示呢?
Python + Django 的組合徹底能夠負載用戶數以 10 億記的服務,若是你正準備開始一個項目,放心使用 Python 吧!
完善的單元測試對於複雜項目是很是有必要的。若是沒有那『成千上萬的單元測試』。很難想象 Instagram 的遷移項目能夠成功進行下去。
開發者和同事也是你的產品用戶,利用好他們。用他們爲你的新特性發布前多一道測試。
徹底基於主分支的開發流程,能夠給你更快的迭代速度。前提是擁有完善的單元測試和持續部署流程。
Python 3 是大勢所趨,若是你正準備開始一個新項目,無需遲疑,擁抱 Python 3 吧!
好了,就到這兒吧。Happy Hacking!
原文連接:instagram-pycon-2017