當前位置:
首頁 > 知識 > Python + Django 如何支撐了 7 億月活用戶的 Instagram?

Python + Django 如何支撐了 7 億月活用戶的 Instagram?

(點擊

上方藍字

,快速關注我們)




來源:伯樂在線 - piglei


如有好文章投稿,請點擊 → 這裡了解詳情







PyCon 簡介



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




Instagram 簡介




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 的故事。




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




Python @Instagram




為什麼選擇 Python 和 Django



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 做了很多定製化工作:






  • 擴展 Django Models 使其支持 Sharding (一種資料庫分片技術),Instagram Engneering 博客專門為這件事情寫過一篇博客,可參閱:Sharding & IDs at Instagram



  • 手動關閉 GC(垃圾回收)來提升 Python 內存管理效率,他們同樣也寫過一篇博客來說明這件事情:Dismissing Python Garbage Collection at Instagram



  • 在位於不同地理位置的多個數據中心部署整套系統




Python 語言的優勢所在




Instagram 的聯合創始人 Mike Krieger 說過: 『我們的用戶根本不關心 Instagram 使用了哪種關係資料庫,他們當然也不關心 Instagram 是用什麼編程語言開發的。』




所以,Python 這種 簡單 而且 實用至上 的編程語言最終贏得了 Instagram 的青睞。他們認為,使用 Python 這種簡單的語言有助於塑造 Instagram 的工程師文化,那就是:






  • 專註於定位問題、解決問題

    – 而不是工具本身的各種花花綠綠的特性



  • 使用那些經過市場驗證過的成熟技術方案

    – 而不用被工具本身的問題所煩擾



  • 用戶至上

    :專註於用戶所能看到的新特性,為用戶帶去價值




但是,即使使用 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 所能帶來的性能可能性。




升級到 Python 3




在相當長的一段時間,Instagram 都跑在 Python 2.7 + Django 1.3 的組合之上。在這個已經落後社區很多年的環境上,他們的工程師們還打了非常非常多的小 patch。難道他們要被永遠卡在這個版本上嗎?




所以,在經過一系列的討論後,他們最終做出一個重大的決定:升級到 Python 3!!




事實上,Instagram 目前已經完成了將運行環境遷移到 Python 3 的工作 – 他們的整套服務已經在 Python 3 上跑了好幾個月了。那麼他們是怎麼做到的呢?接下來便是由 Instagram 工程師 Lisa guo 帶來的 Instagram 如何遷移到 Python 3 的故事。




Instagram 升級到 Python 3 的故事




為什麼要升級到 Python 3




對於 Instagram 來說,下面這些因素是推動他們將運行環境遷移到 Python 3 的主要原因:




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




看看下面這段代碼:





def compose_from_max_id

(

max_id

)

:


"""@param str max_id"""




圖中函數的 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 來幫助他們。




使用 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 用戶。







圖:循序漸進的發布流程




遷移過程的技術問題




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




Unicode 相關的字元串問題




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"))




不同 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 進行,那代碼運行時就會出現下面的錯誤:





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

.

st




這段代碼的用處是挨個編譯 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}"




遷移到 Python 3.6 後的性能提升




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

性能問題




在 Instagram,他們使用兩個主要指標來衡量他們的服務性能:






  • 每次請求產生的 CPU 指令數(越低越好)



  • 每秒能夠處理的請求數(越高越好)




所以,當所有的遷移工作完成後,他們非常驚喜的發現:

第一個性能指標,每次請求產生的 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 下:







圖: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 的演講視頻時間不長,但是內容很豐富,在編寫此文前,我完全沒有想到最終的文章會這麼長。




那些,Instagram 的視頻可以給我們哪些啟示呢?






  • Python + Django 的組合完全可以負載用戶數以 10 億記的服務,如果你正準備開始一個項目,放心使用 Python 吧!



  • 完善的單元測試對於複雜項目是非常有必要的。如果沒有那『成千上萬的單元測試』。很難想像 Instagram 的遷移項目可以成功進行下去。



  • 開發者和同事也是你的產品用戶,利用好他們。用他們為你的新特性發布前多一道測試。



  • 完全基於主分支的開發流程,可以給你更快的迭代速度。前提是擁有完善的單元測試和持續部署流程。



  • Python 3 是大勢所趨,如果你正準備開始一個新項目,無需遲疑,擁抱 Python 3 吧!




好了,就到這兒吧。Happy Hacking!




看完本文有收穫?請轉

發分享給更多人


關注「Python開發者」,提升Python技能


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python開發者 的精彩文章:

Python 圖像處理庫 Pillow 入門
使用 VASPy 快速處理 VASP 文件以及數據可視化
Django 通道簡要介紹

TAG:Python開發者 |

您可能感興趣

使用Centos7.5+Nginx+Gunicorn+Django+Python3部署blog項目
Python3的range比Python2的xrange功能更強大
Python中使用Type hinting 和 annotations
Google App Engine正式支持Python3.7
在Python中使用Elasticsearch
python小點dian兒:使用merge還是join
Python 的 ChatOps 庫:Opsdroid 和 Errbot
為什麼Python如此火?Why Python is so popular?
Python 特殊函數(lambda,map,filter,reduce)
Python 模塊 urllib.parse
python操作neo4j
Python Exceptions介紹
python中list,array,mat,tuple大小及類型
Python幫助Youtube打敗了Google Video
Python中的lambda函數
Python async/await 介紹
Python模塊——contextlib和urllib
Process-Forest-Window進程日誌分析工具;python版的BloodHound
使用 Python 和 Prometheus 跟蹤天氣
Python3.7新特性:Data Class