天天看點

7 億月活使用者背後的技術實作究竟怎樣?Instagram 是這樣說的   PyCon 簡介   Instagram 簡介   Python @Instagram

pycon 是全世界最大的以 python 程式設計語言 為主題的技術大會。大會由 python 社群組織,每年舉辦一次。在大會上,來自世界各地的 python 使用者與核心開發者齊聚一堂,共同分享 python 世界的新鮮事、python 語言的應用案例、使用技巧等等内容。

instagram 是一款移動端的照片與視訊分享軟體,由 kevin systrom 和 mike krieger 在 2010 年創辦。instagram 在釋出後開始快速流行。于 2012 年被 facebook 以 10 億美元的價格收購。而當時 instagram 的員工僅有區區 13 名。

如今,instagram 的總注冊使用者達到 30 億,月活使用者超過 7 億 (作為對比,微信最新披露的月活躍使用者為 9.38 億)。而令人吃驚的是,這麼高的通路量背後,竟完全是由以速度慢著稱的 python + django 支撐。

在 python 2017 上,instagram 的工程師們帶來了一個有關 python 在 instagram 的主題演講,同時還分享了 instagram 如何将整個項目運作環境更新到 python 3 的故事。

本文為該次演講的内容摘要。

instagram 選擇 django 的原因很簡單,instagram 的兩位創始人 (kevin systrom and mike krieger) 都是産品經理出身。在他們想要創造 instagram 時,django 是他們所知道的最穩定和成熟的技術之一。

時至今日,即使已經擁有超過 30 億的注冊使用者。instagram 仍然是 python 和 django 的重度使用者。instagram 的工程師 hui ding 說到: 『一直到使用者 id 已經超過了 32bit int 的限額(約為 20 億),django 本身仍然沒有成為我們的瓶頸所在。』

不過,除了使用 django 的原生功能外,instagram 還對 django 做了很多定制化工作:

● 在位于不同地理位置的多個資料中心部署整套系統

instagram 的聯合創始人 mike krieger 說過: 『我們的使用者根本不關心 instagram 使用了哪種關系資料庫,他們當然也不關心 instagram 是用什麼程式設計語言開發的。』

是以,python 這種 簡單 而且 實用至上 的程式設計語言最終赢得了 instagram 的青睐。他們認為,使用 python 這種簡單的語言有助于塑造 instagram 的工程師文化,那就是:

1. 專注于定位問題、解決問題 - 而不是工具本身的各種花花綠綠的特性 2. 使用那些經過市場驗證過的成熟技術方案 - 而不用被工具本身的問題所煩擾 3. 使用者至上:專注于使用者所能看到的新特性,為使用者帶去價值

但是,即使使用 python 語言有這麼多好處,它還是很慢,不是嗎?

不過,這對于 instagram 不是問題,因為他們認為:『instagram 的最大瓶頸在于開發效率,而不是代碼的執行效率』

at instagram, our bottleneck is development velocity, not pure code execution.

是以,最終的結論是:你完全可以使用 python 語言來實作一個超過幾十億使用者使用的産品,而根本不用擔心語言或架構本身的性能瓶頸。

但是,即使是選用了擁有諸多好處的 python 和 django。在 instagram 的使用者數迅速增長的過程中,性能問題還是出現了:伺服器數量的增長率已經慢慢的超過了使用者增長率。instagram 是怎麼應對這個問題的呢?

他們使用了這些手段來緩解性能問題:

● 開發工具來幫助調優:instagram 開發了很多涵蓋各個層面的工具,來幫助他們進行性能調優以及找到性能瓶頸。 ● 使用 c/c++ 來重寫部分元件:把那些穩定而且對性能最敏感的元件,使用 c 或 c++ 來重寫,比如通路 memcache 的 library。 ● 使用 cython:cython 也是他們用來提升 python 效率的法寶之一。

除了上面這些手段,他們還在探索異步 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 的主要原因:

1. 新特性:類型注解 type annotations

看看下面這段代碼:

圖中函數的 max_id 參數究竟是什麼類型呢?int?tuple?或是 list? 等等,函數文檔裡面說它是 str 類型。

但随着時間推移,萬一這個參數的類型發生變化了呢?如果某位粗心的工程師修改代碼的同時忘了更新文檔,那就會給函數的使用者帶來很大麻煩,最終還不如沒有注釋呢。

2. 性能

instagram 的整個 django stack 都跑在 uwsgi 之上,全部使用了同步的網絡 io。這意味着同一個 uwsgi 程序在同一時間隻能接收并處理一個請求。這讓如何調優每台機器上應該運作的 uwsgi 程序數成了一個麻煩事:

為了更好利用 cpu,使用更多的程序數?但那樣會消耗大量的記憶體。而過少的程序數量又會導緻 cpu 不能被充分利用。

為此,他們決定跳過 python 2 中哪些蹩腳的異步 io 實作 (可憐的 gevent、tornado、twisted 衆),直接更新到 python 3,去探索标準庫中的 asyncio 子產品所能帶來的可能性。

3. 社群

因為 python 社群已經停止了對 python 2 的支援。如果把整個運作環境更新到 python 3,instagram 的工程師們就能和 python 社群走的更近,可以更好的把他們的工作回饋給社群。

在 instagram,進行 python 3 的遷移需要必須滿足兩個前提條件:

1. 不停機,不能有任何的服務是以不可用

2. 不能影響産品新特性的開發

但是,在 instagram 的開發環境中,要滿足上面這兩點來完成遷移到 python 3.6 這種龐大的工程是非常困難的。

基于主分支的開發流程

即便使用了以多分支功能著稱的 git,instagram 所有的開發工作都是主要在 master 分支上進行的,instagram 所奉行的開發哲學是:『不管是多大的新特性或代碼重構,都應該拆解成較小的 commit 來進行。』

那些被合并進 master 分支的代碼,都将在一個小時内被釋出到線上環境。而這樣的釋出過程每天将會發生上百次。在這麼頻繁的釋出頻率下,如何在滿足之前的那兩個前提下來完成遷移變得尤其困難。

很多人在處理這類問題時,第一個蹦進腦子的想法就是: 『讓我們建立一個分支,當我們開發完後,再把分支合并進來』

但在 instagram 這麼高的疊代頻率上,使用一個獨立分支并不是好主意:

1. instagram 的 codebase 每天都在頻繁更新,在開發 python 3 分支的過程中,讓新分支與現有 master 分支保持同步開銷極大,同時極易出錯

2. 最終将 python 3 分支這個改動非常多的分支合并回 master 擁有非常高的風險

3. 隻有少數幾個工程師在 python 3 分支上專職負責更新工作,其他想幫助遷移工作的工程師無法參與進來

挨個替換接口

還有一個方案就是,挨個替換 instagram 的 api 接口。但是 instagram 的不同接口共享着很多通用子產品。這個方案要實施起來也非常困難。

微服務

還有一個方案就是将 instagram 改造成微服務架構。通過将那些通用子產品重寫成 python 3 版本的微服務來一步步完成遷移工作。

但是這個方案需要重新組織海量的代碼。同時,當發生在程序内的函數調用變成 rpc 後 ,整個站點的延遲會變大。此外,更多的微服務也會引入更高的部署複雜度。

是以,既然 instagram 的開發哲學是:小步前進,快速疊代。他們最終決定的方案是:一步一步來,最終讓 master 分支上的代碼同時相容 python 2 和 python 3 。

既然要讓整個 codebase 同時相容 python 2 和 python 3,那麼首先要符合這點的就是那些被大量使用的第三方 package。針對第三方 package,instagram 做到了下面幾點:

● 拒絕引入所有不相容 python 3 的新 package ● 去掉所有不再使用的 package ● 替換那些不相容 python 3 的 package

使用 modernize 時,有一個小技巧:每次修複多個檔案的一個相容問題,而不是一下修複一個檔案中的多個相容問題。 這樣可以讓 code review 過程簡單很多,因為 reviewer 每次隻需要關注一個問題。

對于 python 這種靈活性極強的動态語言來說,除了真正去執行代碼外,幾乎沒有其他比較好的檢查代碼錯誤的手段。

前面提到,instagram 所有被合并到 master 的代碼送出會在一個小時内上線到線上環境,但這不是沒有前提條件的。在上線前,所有的送出都需要通過成千上萬個單元測試。

于是,他們開始加入 python 3 來執行所有的單元測試。一開始,隻有極少數的單元測試能夠在 python 3 環境下通過,但随着 instagram 的工程師們不斷的修複那些失敗的單元測試,最終所有的單元測試都可以在 python 3 環境下成功執行。

單元測試的局限性

但是,單元測試也是有局限性的:

● instagram 的單元測試沒有做到 100% 的代碼覆寫率 ● 很多第三方子產品都使用了 mock 技術,而 mock 的行為與真實的線上服務可能會有所不同

是以,當所有的單元測試都被修複後,他們開始線上上正式使用 python 3 來運作服務。

這個過程并不是一蹴而就的。首先,所有的 instagram 工程師開始通路到這些使用 python 3 來執行的新服務,然後是 facebook 的所有雇員,随後是 0.1%、20% 的使用者,最終 python 3 覆寫到了所有的 instagram 使用者。

7 億月活使用者背後的技術實作究竟怎樣?Instagram 是這樣說的   PyCon 簡介   Instagram 簡介   Python @Instagram

循序漸進的釋出流程

instagram 在遷移到 python 3 時碰到很多問題,下面是最典型的幾個:

unicode 相關的字元串問題

python 3 相比 python 2 最大的改動之一,就是在語言内部對 unicode 的處理。

在 python 2 中,文本類型 (也就是 unicode) 和二進制類型 (也就是 str) 的邊界非常模糊。很多函數的參數既可以是文本,也可以是二進制。但是在 python 3 中,文本類型和二進制類型的字元串被完全的區分開了。

于是,下面這段在 python 2 下可以正常運作的代碼在 python 3 下就會報錯:

解決辦法其實很簡單,隻要加上判斷:如果 value 是文本類型,就将其轉換為二進制。如下所示:

但是,在整個代碼庫中,像上面這樣的情況非常多。作為開發人員,如果需要在調用每個函數時都要想想: 這裡到底是應該編碼成二進制,或者是解碼成文本呢? 将會是非常大的負擔。

于是 instagram 封裝了一些名為 ensure_str()、ensure_binary()、ensure_text() 的幫助函數,開發人員隻需對那些不确定類型的字元串,使用這些幫助函數先做一次轉換就好。

不同 python 版本的 pickle 差異

instagram 的代碼中大量使用了 pickle。比如用它序列化某個對象,然後将其存儲在 memcache 中。如下面的代碼所示:

問題在于,python 2 與 python 3 的 pickle 子產品是有差别的。

如果上文的第一行代碼,剛好是由 python 3 運作的服務進行序列化後存入 memcache。而反序列化的過程卻是由 python 2 進行,那代碼運作時就會出現下面的錯誤:

這是由于在 python 3 中,pickle.highest_protocol 的值為 4,而 python 2 中的的 pickle 最高支援的版本号卻是 2。那麼如何解決這個問題呢?

instagram 最終選擇讓 python 2 和 python 3 使用完全不同的 namespace 來通路 memcache。通過将二者的資料讀寫完全隔開來解決這個問題。

疊代器

在 python 3 中,很多内置函數被修改成了隻返成疊代器 iterator:

疊代器有諸多好處,最大的好處就是,使用疊代器不需要一次性配置設定大量記憶體,是以它的記憶體效率比較高。

但是疊代器有一個天然的特點,當你對某個疊代器做了一次疊代,通路完它的内容後,就沒法再次通路那些内容了。疊代器中的所有内容都隻能被通路一次。

在 instagram 的 python 3 遷移過程中,就因為疊代器的這個特性被坑了一次,看看下面這段代碼:

這段代碼的用處是挨個編譯 cython 源檔案。當他們把運作環境切換到 python 3 後,一個奇怪的問題出現了:cython_sources 中的第一個檔案永遠都被跳過了編譯。為什麼呢?

這都是疊代器的鍋。在 python 3 中,map() 函數不再傳回整個 list,而是傳回一個疊代器。

于是,當第二行代碼生成 builds 這個疊代器後,第三行代碼的 while 循環疊代了 builds,剛好取出了第一個元素。于是之後的 pending 對象便裡面永遠少了那第一個元素。

這個問題解決起來也挺簡單的,你隻要手動的吧 builds 轉換成 list 就可以了:

但是這類 bug 非常難定位到。如果使用者的 feeds 裡面永遠少了那最新的第一條,使用者很少會注意到。

字典的順序

它會輸出什麼結果呢?

在不同的 python 版本下,這個 json dumps 的結果是完全不一樣的。甚至在 3.5.1 中,它會完全随機的傳回兩個不同的結果。instagram 有一段判斷配置檔案是否發生變動的子產品,就是因為這個原因出了問題。

這個問題的解決辦法是,在調用 json.dumps 傳入 sort_keys=true 參數:

當 instagram 解決了這些奇奇怪怪的版本差異問題後,還有一個巨大的謎題困擾着他們:性能問題。

在 instagram,他們使用兩個主要名額來衡量他們的服務性能:

● 每次請求産生的 cpu 指令數(越低越好) ● 每秒能夠處理的請求數(越高越好)

是以,當所有的遷移工作完成後,他們非常驚喜的發現:第一個性能名額,每次請求産生的 cpu 指令數居然足足下降了 12% !!!

但是,按理說第二個名額 - 每秒請求數也應該獲得接近 12% 的提升。不過最後的變化卻是 0%。究竟是出了什麼問題呢?

他們最終定位到,是由于不同 python 版本下的記憶體優化配置不同,導緻 cpu 指令數下降帶來的性能提升被抵消了。那為什麼不同 python 版本下的記憶體優化配置會不一樣呢?

這是他們用來檢查 uwsgi 配置的代碼:

注意到那段 ... ... == 'true' 了嗎?在 python 3 中,這個條件判斷總是不會被滿足。問題就在于 unicode。在将代碼中的 'true' 換成 b'true'(也就是将文本類型換成二進制,這種判斷在 python 2 中完全不區分的)後,問題解決了。

是以,最終因為加上了一個小小的字母 'b',程式的整體性能提升了 12%。

在今年二月份,instagram 的後端代碼的運作環境完全切換到了 python 3 下:

7 億月活使用者背後的技術實作究竟怎樣?Instagram 是這樣說的   PyCon 簡介   Instagram 簡介   Python @Instagram

instagram 版本遷移時間線

當所有的代碼都都遷移到 python 3 運作環境後:

● 節約了 12% 的整體 cpu 使用率(django/uwsgi) ● 節約了 30% 的記憶體使用(celery)

同時,在整個遷移期間,instagram 的月活使用者經曆了從 4 億到 6億 的巨大增長。産品也釋出了評論過濾、直播等非常多新功能。

那麼,那幾個最開始驅動他們遷移到 python 3 的目的呢?

● 類型注解:instagram 的整個 codebase 裡已經有 2% 的代碼添加上了類型注解,同時他們還開發了一些工具來輔助開發者添加類型提示 ● asyncio:他們在單個接口中利用 asynio 平行的去做多件事情,最終降低了 20-30% 的請求延遲。 ● 社群:他們與 intel 的工程師聯合,幫助他們更好的對 cpu 使用率進行調優。同時還開發了很多新的工具,幫助他們進行性能調優

instagram 的演講視訊時間不長,但是内容很豐富,在編寫此文前,我完全沒有想到最終的文章會這麼長。

那麼,instagram 的視訊可以給我們哪些啟示呢?

● python + django 的組合完全可以負載使用者數以 10 億記的服務,如果你正準備開始一個項目,放心使用 python 吧! ● 完善的單元測試對于複雜項目是非常有必要的。如果沒有那『成千上萬的單元測試』。很難想象 instagram 的遷移項目可以成功進行下去。 ● 開發者和同僚也是你的産品使用者,利用好他們。用他們為你的新特性釋出前多一道測試。 ● 完全基于主分支的開發流程,可以給你更快的疊代速度。前提是擁有完善的單元測試和持續部署流程。 ● python 3 是大勢所趨,如果你正準備開始一個新項目,無需遲疑,擁抱 python 3 吧!

好了,就到這兒吧。happy hacking!

====================================分割線================================

本文作者:ai研習社