PyCon大會Python主題演講摘要

PyCon 是全國際最大的以 Python 編程言語 爲主題的技能大會。大會由 Python 社區組織,每一年舉行一次。在大會上,來自國際各地的 Python 用戶與中心開發者齊聚一堂,共同同享 Python 國際的新鮮事、Python 言語的應用案例、運用技巧等等內容。python

Instagram 簡介

<p "="">Instagram 是一款移動端的照片與視頻同享軟件,由 Kevin Systrom 和 Mike Krieger 在 2010 年創辦。Instagram 在發佈後開端快速流行。於 2012 年被 Facebook 以 10 億美圓的價格收買。而其時 Instagram 的員工僅有區區 13 名。<p "="">現在,Instagram 的總註冊用戶到達 30 億,月活用戶超越 7 億 (做爲對比,微信最新披露的月活潑用戶爲 9.38 億)。而使人吃驚的是,這麼高的拜訪量背後,竟完全是由以速度慢著稱的 Python + Django 支撐。<p "="">在 Python 2017 上,Instagram 的工程師們帶來了一個有關 Python 在 Instagram 的主題演講,一塊兒還同享了 Instagram 怎麼將整個項目運轉環境晉級到 Python 3 的故事。<p "="">本文爲該次演講的內容摘要。git

Python @Instagram

爲何挑選 Python 和 Django

<p "="">Instagram 挑選 Django 的緣由很簡略,Instagram 的兩位創始人 (Kevin Systrom and Mike Krieger) 都是產品經理出身。在他們想要創造 Instagram 時,Django 是他們所知道的最穩定和成熟的技能之一。<p "="">時至今日,即便現已具備超越 30 億的註冊用戶。Instagram 依然是 Python 和 Django 的重度運用者。Instagram 的工程師 Hui Ding 說到: 『一直到用戶 ID 現已超越了 32bit int 的限額(約爲 20 億),Django 自身依然沒有成爲我們的瓶頸地點。』<p "="">不過,除了運用 Django 的原生功用外,Instagram 還對 Django 作了許多定製化做業:<ul "="">數據庫

Python 言語的優點地點

<p "="">Instagram 的聯合創始人 Mike Krieger 說過: 『我們的用戶根本不關心 Instagram 運用了哪一種聯繫數據庫,他們固然也不關心 Instagram 是用什麼編程言語開發的。』<p "="">因此,Python 這種 簡略 而且 實用至上 的編程言語終究贏得了 Instagram 的喜好。他們覺得,運用 Python 這種簡略的言語有助於刻畫 Instagram 的工程師文明,那就是:<ol "="">編程

  • 專心於定位問題、處理問題 - 而不是東西自身的各類花花綠綠的特性
  • 運用那些通過商場驗證過的成熟技能計劃 - 而不用被東西自身的問題所幹擾
  • 用戶至上:專心於用戶所能看到的新特性,爲用戶帶去價值

<p "="">但是,即便運用 Python 言語有這麼多優勢,它還是很慢,不是嗎?<p "="">不過,這關於 Instagram 不是問題,因爲他們覺得:『Instagram 的最大瓶頸在於開發功率,而不是代碼的履行功率』<blockquote "="">json

At Instagram, our bottleneck is development velocity, not pure code execution.後端

<p "="">因此,終究的結論是:你完全可以運用 Python 言語來完結一個超越幾十億用戶運用的產品,而根本不用憂慮言語或框架自身的功用瓶頸。微信

怎麼提升運轉功率

<p "="">但是,即就是選用了具備諸多優勢的 Python 和 Django。在 Instagram 的用戶數迅速增加的進程中,功用問題還是呈現了:效勞器數量的增加率現已慢慢的超越了用戶增加率。Instagram 是怎麼應對這個問題的呢?<p "="">他們運用了這些手法來緩解功用問題:<ul "="">網絡

  • 開發東西來協助調優:Instagram 開發了許多包括各個層面的東西,來協助他們進行功用調優以及找到功用瓶頸。
  • 運用 C/C++ 來重寫部分組件:把那些穩定而且對功用最敏感的組件,運用 C 或 C++ 來重寫,比方拜訪 memcache 的 library。
  • 運用 Cython:Cython 也是他們用來提升 Python 功率的法寶之一。

<p "="">除了上面這些手法,他們還在探索異步 IO 以及新的 Python Runtime 所能帶來的功用可能性。架構

晉級到 Python 3

<p "="">在至關長的一段時間,Instagram 都跑在 Python 2.7 + Django 1.3 的組合之上。在這個現已落後社區許多年的環境上,他們的工程師們還打了十分十分多的小 patch。難道他們要被永久卡在這個版別上嗎?<p "="">因此,在通過一系列的評論後,他們終究作出一個重大的決議:晉級到 Python 3!!<p "="">事實上,Instagram 如今現已完結了將運轉環境搬遷到 Python 3 的做業 - 他們的整套效勞現已在 Python 3 上跑了好幾個月了。那麼他們是怎麼作到的呢?接下來就是由 Instagram 工程師 Lisa guo 帶來的 Instagram 怎麼搬遷到 Python 3 的故事。app

Instagram 晉級到 Python 3 的故事

爲何要晉級到 Python 3

<p "="">關於 Instagram 來講,下面這些因素是推進他們將運轉環境搬遷到 Python 3 的主要緣由:

1. 新特性:類型註解 Type Annotations

<p "="">看看下面這段代碼:

def compose_from_max_id(max_id): '''@param str max_id''' 

<p "="">圖中函數的 max_id 參數到底是什麼類型呢?int?tuple?或是 list? 等等,函數文檔裏邊說它是 str 類型。<p "="">但隨着時間推移,萬一這個參數的類型發做改變了呢?假如某位大意的工程師修正代碼的一塊兒忘了更新文檔,那就會給函數的運用者帶來很大麻煩,終究還不如沒有註釋呢。

2. 功用

<p "="">Instagram 的整個 Django Stack 都跑在 uwsgi 之上,所有運用了同步的網絡 IO。這意味着同一個 uwsgi 進程在同一時間只能接收並處理一個懇求。這讓怎麼調優每臺機器上應該運轉的 uwsgi 進程數成了一個麻煩事:<p "="">爲了更好使用 CPU,運用更多的進程數?但那樣會消耗許多的內存。而過少的進程數量又會致使 CPU 不能被充分使用。<p "="">爲此,他們決議跳過 Python 2 中哪些糟糕的異步 IO 完結 (不幸的 gevent、tornado、twisted 衆),直接晉級到 Python 3,去探索規範庫中的 asyncio 模塊所能帶來的可能性。

3. 社區

<p "="">因爲 Python 社區現已停止了對 Python 2 的支撐。假如把整個運轉環境晉級到 Python 3,Instagram 的工程師們就能和 Python 社區走的更近,可以更好的把他們的做業回饋給社區。

判定搬遷計劃

<p "="">在 Instagram,進行 Python 3 的搬遷需求有必要滿意兩個前提條件:<ol "="">

  • 不停機,不能有任何的效勞於是不可用
  • 不能影響產品新特性的開發

<p "="">但是,在 Instagram 的開發環境中,要滿意上面這兩點來完結搬遷到 Python 3.6 這種巨大的工程是十分困難的。

根據主分支的開發流程

<p "="">即便運用了以多分支功用著稱的 git,Instagram 一切的開發做業都是主要在 master 分支上進行的,Instagram 所奉行的開發哲學是:『無論是多大的新特性或代碼重構,都應該拆解成較小的 Commit 來進行。』<p "="">那些被兼併進 master 分支的代碼,都將在一個小時內被髮布到線上環境。而這樣的發佈進程天天將會發做上百次。在這麼頻頻的發佈頻率下,怎麼在滿意以前的那兩個前提下來完結搬遷變得特別困難。

被棄用的搬遷計劃

<p "="">創立一個新分支<p "="">許多人在處理這類問題時,榜首個蹦進腦子的主意就是: 『讓我們創立一個分支,當我們開發完後,再把分支兼併進來』<p "="">但在 Instagram 這麼高的迭代頻率上,運用一個獨立分支並非好主意:<ol "="">

  • Instagram 的 Codebase 天天都在頻頻更新,在開發 Python 3 分支的進程中,讓新分支與現有 master 分支堅持同步開支極大,一塊兒極易犯錯
  • 終究將 Python 3 分支這個改動十分多的分支兼併回 Master 具備十分高的風險
  • 只需少許幾個工程師在 Python 3 分支上專職擔任晉級做業,其餘想協助搬遷做業的工程師沒法參與進來

<p "="">挨個替換接口<p "="">還有一個計劃就是,挨個替換 Instagram 的 API 接口。但是 Instagram 的不一樣接口同享着許多通用模塊。這個計劃要施行起來也十分困難。<p "="">微效勞<p "="">還有一個計劃就是將 Instagram 改形成微效勞架構。通過將那些通用模塊重寫成 Python 3 版別的微效勞來一步步完結搬遷做業。<p "="">但是這個計劃需求從新組織海量的代碼。一塊兒,當發做在進程內的函數調用變成 RPC 後 ,整個站點的推遲會變大。此外,更多的微效勞也會引進更高的部署雜亂度。<p "="">因此,已然 Instagram 的開發哲學是:小步前進,快速迭代。他們終究決議的計劃是:一步一步來,終究讓 master 分支上的代碼一塊兒兼容 Python 2 和 Python 3 。

開端搬遷做業

<p "="">已然要讓整個 codebase 一塊兒兼容 Python 2 和 Python 3,那麼首先要契合這點的就是那些被許多運用的第三方 package。針對第三方 package,Instagram 作到了下面幾點:<ul "="">

  • 拒絕引進一切不兼容 Python 3 的新 package
  • 去掉一切再也不運用的 package
  • 替換那些不兼容 Python 3 的 package

<p "="">在代碼的搬遷進程中,他們運用了東西 modernize 來協助他們。<p "="">運用 modernize 時,有一個小技巧:每次修復多個文件的一個兼容問題,而不是一下修復一個文件中的多個兼容問題。 這樣可以讓 Code Review 進程簡略許多,因爲 Reviewer 每次只需求關注一個問題。

運用單元測試來協助搬遷

<p "="">關於 Python 這種靈活性極強的動態言語來講,除了真實去履行代碼外,幾乎沒有其餘比較好的查看代碼錯誤的手法。<p "="">前面提到,Instagram 一切被兼併到 master 的代碼提交會在一個小時內上線到線上環境,但這不是沒有前提條件的。在上線前,一切的提交都需求通過不可勝數個單元測試。<p "="">因此,他們開端參加 Python 3 來履行一切的單元測試。一開端,只需極少許的單元測試可以在 Python 3 環境下通過,但隨着 Instagram 的工程師們不斷的修復那些失敗的單元測試,終究一切的單元測試都可以在 Python 3 環境下成功履行。

單元測試的侷限性

<p "="">但是,單元測試也是有侷限性的:<ul "="">

  • Instagram 的單元測試沒有作到 100% 的代碼覆蓋率
  • 許多第三方模塊都運用了 mock 技能,而 mock 的行爲與真實的線上效勞可能會有所不一樣

<p "="">因此,當一切的單元測試都被修復後,他們開端在線上正式運用 Python 3 來運轉效勞。<p "="">這個進程並非一蹴而就的。首先,一切的 Instagram 工程師開端拜訪到這些運用 Python 3 來履行的新效勞,而後是 Facebook 的一切僱員,隨後是 0.1%、20% 的用戶,終究 Python 3 覆蓋到了一切的 Instagram 用戶。

圖:循序漸進的發佈流程

搬遷進程的技能問題

<p "="">Instagram 在搬遷到 Python 3 時碰到許多問題,下面是最典型的幾個:

Unicode 相關的字符串問題

<p "="">Python 3 相比 Python 2 最大的改動之一,就是在言語內部 諮詢入庫對 unicode 的處理。<p "="">在 Python 2 中,文本類型 (也就是 unicode) 和二進制類型 (也就是 str) 的邊界十分模糊。許多函數的參數既可以是文本,也可以是二進制。但是在 Python 3 中,文本類型和二進制類型的字符串被完全的區分開了。<p "="">因此,下面這段在 Python 2 下可以正常運轉的代碼在 Python 3 下就會報錯:

mymac = hmac.new('abc') TypeError: key: expected bytes or bytearray, but got 'str' 

<p "="">處理辦法其實很簡略,只需加上判別:假如 value 是文本類型,就將其轉換爲二進制。以下所示:

value = 'abc' if isinstance(value, six.text_type): value = value.encode(encoding='utf-8') mymac = hmac.new(value)

但是,在整個代碼庫中,像上面這樣的情況十分多。做爲開發人員,假如需求在調用每一個函數時都要想一想: 這裏究竟是應該編碼成二進制,或者是解碼成文本呢? 將會是十分大的負擔。

<p "="">因此 Instagram 封裝了一些名爲 ensure_str()、ensure_binary()、ensure_text() 的協助函數,開發人員只需對那些不判定類型的字符串,運用這些協助函數先作一次轉換就好。

mymac = hmac.new(ensure_binary('abc'))

不一樣 Python 版別的 pickle 差別

<p "="">Instagram 的代碼中許多運用了 pickle。比方用它序列化某個目標,而後將其存儲在 memcache 中。以下面的代碼所示:

memcache_data = pickle.dumps(data, pickle.HIGHEST_PROTOCOL)
data = pickle.loads(memcache_data)

問題在於,Python 2 與 Python 3 的 pickle 模塊是有不一樣的。

<p "="">假如上文的榜首行代碼,恰好是由 Python 3 運轉的效勞進行序列化後存入 memcache。而反序列化的進程倒是由 Python 2 進行,那代碼運轉時就會呈現下面的錯誤:

ValueError: unsupported pickle protocol: 4 

<p "="">這是因爲在 Python 3 中,pickle.HIGHEST_PROTOCOL 的值爲 4,而 Python 2 中的的 pickle 最高支撐的版別號倒是 2。那麼怎麼處理這個問題呢?<p "="">Instagram 終究挑選讓 Python 2 和 Python 3 運用完全不一樣的 namespace 來拜訪 memcache。通過將兩者的數據讀寫完全隔開來處理這個問題。

迭代器

<p "="">在 Python 3 中,許多內置函數被修正成了只返成迭代器 Iterator:

map()
filter()
dict.items()

迭代器有諸多優勢,最大的優勢就是,運用迭代器不需求一次性分配許多內存,因此它的內存功率比較高。

<p "="">但是迭代器有一個自然的特色,當你對某個迭代器作了一次迭代,拜訪完它的內容後,就沒法再次拜訪那些內容了。迭代器中的一切內容都只能被拜訪一次。<p "="">在 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()]

<p "="">這段代碼的用處是挨個編譯 Cython 源文件。當他們把運轉環境切換到 Python 3 後,一個古怪的問題呈現了:CYTHON_SOURCES 中的榜首個文件永久都被跳過了編譯。爲何呢?<p "="">這都是迭代器的鍋。在 Python 3 中,map() 函數再也不回來整個 list,而是回來一個迭代器。<p "="">因此,當第二行代碼生成 builds 這個迭代器後,第三行代碼的 while 循環迭代了 builds,恰好取出了榜首個元素。因此以後的 pending 目標便裏邊永久少了那榜首個元素。<p "="">這個問題處理起來也挺簡略的,你只需手動的吧 builds 轉換成 list 就可以了:

builds = list(map(BuildProcess, CYTHON_SOURCES))

<p "="">但是這類 bug 十分難定位到。假如用戶的 feeds 裏邊永久少了那最新的榜首條,用戶不多會注意到。

字典的次序

<p "="">看看下面這段代碼:

>>> testdict = {'a': 1, 'b': 2, 'c': 3} >>> json.dumps(testdict)

<p "="">它會輸出什麼成果呢?

# 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}' 

<p "="">在不一樣的 Python 版別下,這個 json dumps 的成果是完全不同的。甚至在 3.5.1 中,它會完全隨機的回來兩個不一樣的成果。Instagram 有一段判別裝備文件是否發做改變的模塊,就是因爲這個緣由出了問題。<p "="">這個問題的處理辦法是,在調用 json.dumps 傳入 sort_keys=True 參數:

>>> json.dumps(testdict, sort_keys=True) '{"a": 1, "b": 2, "c": 3}' 

搬遷到 Python 3.6 後的功用提升

<p "="">當 Instagram 處理了這些奇古怪怪的版別差別問題後,還有一個巨大的謎題困擾着他們:功用問題。<p "="">在 Instagram,他們運用兩個主要指標來衡量他們的效勞功用:<ul "="">

  • 每次懇求發生的 CPU 指令數(越低越好)
  • 每秒可以處理的懇求數(越高越好)

<p "="">因此,當一切的搬遷做業完結後,他們十分驚喜的發現:榜首個功用指標,每次懇求發生的 CPU 指令數竟然足足降低了 12% !!!<p "="">但是,按理說第二個指標 - 每秒懇求數也應該得到挨近 12% 的提升。不過最後的改變倒是 0%。到底是出了什麼問題呢?<p "="">他們終究定位到,是因爲不一樣 Python 版別下的內存優化裝備不一樣,致使 CPU 指令數降低帶來的功用提升被抵消了。那爲何不一樣 Python 版別下的內存優化裝備會不同呢?<p "="">這是他們用來查看 uwsgi 裝備的代碼:

if uwsgi.opt.get('optimize_mem', None) == 'True': optimize_mem()

注意到那段 ... ... == 'True' 了嗎?在 Python 3 中,這個條件判別老是不會被滿意。問題就在於 unicode。在將代碼中的 'True' 換成 b'True'(也就是將文本類型換成二進制,這種判別在 Python 2 中完全不區分的)後,問題處理了。

<p "="">因此,終究因爲加上了一個小小的字母 'b',程序的總體功用提升了 12%。

結論

<p "="">在今年二月份,Instagram 的後端代碼的運轉環境完全切換到了 Python 3 下: 

圖:Instagram 版別搬遷時間線

<p "="">當一切的代碼都都搬遷到 Python 3 運轉環境後:<ul "="">

  • 節省了 12% 的總體 CPU 運用率(Django/uwsgi)
  • 節省了 30% 的內存運用(celery)

<p "="">一塊兒,在整個搬遷期間,Instagram 的月活用戶閱歷了從 4 億到 6億 的巨大增加。產品也發佈了評論過濾、直播等十分多新功用。<p "="">那麼,那幾個最開端驅動他們搬遷到 Python 3 的意圖呢?<ul "="">

  • 類型註解:Instagram 的整個 codebase 裏現已有 2% 的代碼增長上了類型註解,一塊兒他們還開發了一些東西來輔佐開發者增長類型提示
  • asyncio:他們在單個接口中使用 asynio 平行的去作多件做業,終究下降了 20-30% 的懇求推遲。
  • 社區:他們與 Intel 的工程師聯合,協助他們更好的對 CPU 使用率進行調優。一塊兒還開發了許多新的東西,協助他們進行功用調優

Instagram 帶給我們的啓示

<p "="">Instagram 的演講視頻時間不長,但是內容很豐富,在編寫此文前,我完全沒有想到終究的文章會這麼長。<p "="">那麼,Instagram 的視頻可以給我們哪些啓示呢?<ul "="">

  • Python + Django 的組合完全可以負載用戶數以 10 億記的效勞,假如你正準備開端一個項目,放心運用 Python 吧!
  • 完善的單元測試關於雜亂項目是十分有必要的。假如沒有那『不可勝數的單元測試』。很難幻想 Instagram 的搬遷項目可以成功進行下去。
  • 開發者和同事也是你的產品用戶,使用好他們。用他們爲你的新特性發布前多一道測試。
  • 完全根據主分支的開發流程,可以給你更快的迭代速度。前提是具備完善的單元測試和繼續部署流程。
  • Python 3 是大勢所趨,假如你正準備開端一個新項目,無需躊躇,擁抱 Python 3 吧!

<p "="">好了,就到這兒吧。Happy Hacking!

相關文章
相關標籤/搜索