Medium開發團隊談架構設計

desaco發表於2016-02-29

背景

  說到底,Medium是個社交網路,人們可以在這裡分享有意思的故事和想法。據統計,目前累積的使用者閱讀時間已經超過14億分鐘,合兩千六百年。

  我們支援著每個月兩千五百萬的讀者以及每週數以萬計的文章釋出。我們不想Medium的文章以閱讀量為成功的依據,而是觀點取勝。在Medium,文章的觀點比作者的名頭更重要。在這裡,對話促進想法,並且很看重文字的力量。

  我是Medium開發團隊的負責人,此前在Google工作,負責開發Google+和Gmail,還創立了Closure專案。業餘時間我喜歡滑雪跳傘和叢林冒險。

  團隊介紹

  說起團隊我非常自豪,這是一群富有好奇心而且想法豐富的天才,大家湊到一塊是想做大事的。

  團隊以跨功能的任務驅動,這樣每個人既可以專攻,又可以毫無壓力的對整個架構有所貢獻。我們的理念就是接觸的方面越多,對團隊的鍛鍊越大。更多關於團隊的理念見此

  在工作組織方面,我們有著很大的自由度,當然作為一個公司組成,我們還是有季度目標的,並且鼓勵敏捷開發模式。我們使用GitHub進行code review和問題跟蹤,用Google Apps作為郵件、文件和表單系統。跟很多團隊習慣使用Trello不同,我們是Slack和slack機器人的重度使用者。

  原始架構

  最開始的時候,Medium部署在EC2上,用Node.js實現,後來公測的時候遷移到了DynamoDB

  其中有個節點用來處理圖片,負責將複雜的處理工作轉向GraphicsMagick。還有一個節點用作後臺的SQS佇列處理。

  我們用SES處理郵件,S3做靜態元素伺服器,CloudFront做CDN,nginx作為反向代理,Datadog用來監控,Pagerduty用來告警。

  線上編輯器用了TinyMCE。上線之前我們已經開始使用Closure編譯器以及部分的Closure庫,但是模板還是用的Handlebars

  當前架構

  雖然Medium表面看起來很簡單,但是瞭解其後臺的複雜性後,你會大吃一驚。有人會說,這就是個部落格啊,用Rails之類的一週就能搞定了。

  總之,閒話不多說,我們自底向上介紹以後再做判斷。

  執行環境

  Medium目前執行在Amazon虛擬私有云,使用Ansible做系統管理,它支援配置檔案模式,我們將檔案納入程式碼版本管理,這樣就可以隨時回滾隨時掌控。

  Medium的後臺是個面向服務的架構,執行了大概二十幾個產品服務。劃分服務的依據取決於這部分功能的獨立性,以及對資源的使用特性。

  Medium的主體仍然是Node.js完成,方便前端和後端的程式碼共享,主要是文章編輯和釋出這個過程。Node大部分時候不錯,但阻塞event迴圈的時候會有效能問題。為了緩解,我們在每臺機器上啟動多個Node例項,將對效能要求比較高的任務分配給專門的例項。同時我們還深入V8執行時環境檢視更加細節的耗時,基本上是JSON去序列化的時候的物件具體化耗時較多。

  我們還用Go語言做了一些輔助服務。因為Go非常容易編譯打包和釋出。相比Java語言的冗長羅嗦和虛擬機器,Go語言在型別安全方面做的很到位。就個人習慣來講,我比較喜歡在團隊內部推廣強型別語言,因為這類語言能夠提高專案的清晰度,不糾結。

  目前靜態元素大部分是通過CloudFlare提供的,還有5%通過Fastly,5%通過CloudFront,這麼做是為了讓兩者的快取得到更新,用於一些緊急的情況。最近我們在應用流量上也使用了CloudFlare,當時主要是為了防止DDOS攻擊,但隨之而來的效能提升也是我們願意看到的。

  我們使用Nginx和HAProxy做反向代理和負載均衡,來滿足我們所需功能的維恩圖。

  我們仍然使用Datadog來監控,Pagerduty來告警。現在又增加了ELK(ElasticsearchLogstashKibana)來進行產品問題除錯。

  資料庫

  DynamoDB仍然是我們的主力資料庫,但是用起來也不是毫無問題。目前遇到的比較棘手的是大V使用者展開和虛擬event過程中的熱鍵問題。我們專門在資料庫前面做了一個Redis快取叢集,來緩解這些問題。到底為開發者優化還是為產品穩定性優化的問題通常會引發爭執,我們也一直在嘗試中和兩者的矛盾。

  目前我們開始在儲存新資料上使用Amazon Aurora,它可以提供更靈活的查詢和過濾功能。

  我們使用Neo4J儲存Medium網路中實體之間的關係,執行在有兩個副本的主節點上。使用者、文章、標籤和收藏都屬於圖中的節點。邊則是在實體建立和使用者進行推薦高亮等動作時生成。我們通過在圖中游走來過濾和推薦文章。

  資料平臺

  早期我們對資料非常渴望,不斷嘗試資料分析框架來輔助商業和產品決策。最近我們則是利用同樣的框架來反饋產品系統,支援Explore等資料驅動功能。

  我們採用Amazon Redshift作為資料倉儲,為生產工具提供可變儲存和處理系統。我們持續將諸如使用者和文章等核心資料從Dynamo匯入Redshift,還將諸如文章被瀏覽被滾動等event日誌從S3匯入Redshift。

  任務通過一個內部排程和監控工具Conduit排程。我們用了一個基於斷言的排程模型,只有條件滿足的時候,任務才會執行。從產品角度來講,這是不可或缺的:資料製造方應該與資料消費方隔離,還要簡化配置,保持系統的可預見和可除錯性。

  Redshift的SQL檢索目前執行不錯,但我們時不時需要讀取和儲存資料,所以後期增加了Apache Spark作為ETL,Spark具有很好的靈活性和擴充套件能力。隨著產品的推進,估計後面Spark會成為我們資料流水線的主要工具。

  我們使用Protocol Buffers作為schema來確保分散式系統的各層次間保持同步,包括移動應用、web服務和資料倉儲等。通過定製化的選項,我們將schema標記上更加細化的配置,如帶有表名和索引,以及長度等校驗約束。

  使用者也需要保持同步,這樣移動端和網頁端就可以保持日誌的一致性了,同時方便產品科學家們用同樣的方式解析欄位。我們幫助專案成員從.proto檔案中生成訊息、欄位和文件等內容,進而利用所得資料開展研究。

  圖片伺服器

  我們的圖片伺服器現在用Go語言實現,採用瀑布型策略來提供處理過的圖片。伺服器使用groupcache,是memcahce的替代品,可以幫助減輕伺服器之間的重複工作。而記憶體級快取則是用了一個S3的持續快取。圖片的處理是請求來觸發的。這給了我們的架構設計師靈活改變圖片展示的自由度,為不同平臺優化,而且避免了大量的生成不同尺寸圖片的操作。

  目前Medium對圖片主要支援放縮和裁剪,但原始版本中還支援顏色清洗和銳化等操作。處理動圖很痛苦,具體後續可以寫一篇文章來解釋。

  文字標註

  文字標註是個有意思的功能,用了一個小型Go伺服器,跟PhantomJS介面形成渲染程式。

  我一直想要把渲染程式換到Pango,但是在實踐過程中,能在HTML中擺放圖片的能力的確更靈活。而從功能的使用頻率來看,這意味著更容易開發和管控。

  自定義域名

  我們允許使用者為其Medium文章設定個性化域名。我們想做成單點登入且HTTPS全覆蓋,因此實現起來頗有難度。我們專門準備了一批HAProxy伺服器用來管理證照,並向主要應用伺服器引導流量。初始化一個域的時候需要一些手動的工作,但是通過與Namecheap的定製化整合,我們將其大部分轉換為自動化。證照驗證和釋出連結由專門服務負責。

  網站前端

  網頁端這塊,我們有自主研發的單網頁應用框架,使用Closure標準庫。我們使用Closure模板渲染客戶端和服務端,然後使用Closure編譯器來縮減程式碼並劃分模組。編輯器是我們網頁端應用最複雜的部分,具體參見Nick此前的文章。

  iOS

  我們的兩個應用都是原生的,儘量避免使用網頁檢視。

  在iOS上,我們使用了一系列的自建框架,以及系統原生元件。在網路層,我們用NSURLSession發起請求,用Mantle解析JSON並對映到模型。我們還有一層基於NSKeyedArchiver的緩衝層。對於將條目渲染為共同主題的列表,我們有一個通用方法,這讓我們能夠快速為不同型別的內容構建新列表。文章介面是一個定製佈局的UICollectionView。我們使用共享元件來渲染全文介面和預覽介面。

  應用程式碼的每一次提交都會編譯後推送給Medium員工,這樣我們能夠很快嘗試新版本。應用商店的版本是滯後於新版本的,但我們也一直在嘗試更快的釋出,雖然可能僅僅是幾處小更新。

  對於測試,我們使用XCTest和OCMock。

  Android

  在Android方面,我們與當前的SDK和支援庫版本保持一致。我們並沒有使用任何複雜的框架,而是傾向於為重複出現的問題構建持續性的模式。我們利用guava彌補Java中所有的缺失。另一方面來講,我們也傾向於使用第三方庫來解決特別的問題。我們還利用protocol buffers定義了API,用以生成應用中的物件。

  我們利用mockitorobolectric。我們會開發一些高層測試來運轉activity和poke:剛新增screen或要重構的時候,先建立一些基本的版本,隨著我們復現bug它們也會進化。我們還會開發一些底層測試,來檢測一個特定的類:隨著新功能的增加我們會建立測試,這能夠幫助我們思考和設計底層是如何互動的。

  每個提交都會作為alpha版本自動推送到play商店,然後到Medium員工(包括我們的Hatch,Medium內部版)。推送大部分發生在週五,我們會把alpha版本傳送給測試小組,請他們用整個週末進行測試。然後,週一我們會從beta版推進至正式產品版。因為最近一批程式碼總是隨時可以推送,因此一旦發現很嚴重的bug,我們就可以立即修復正式產品版。當我們懷疑某些新功能的時候,可以給測試小組更長的時間。開發比較亢奮的時候,也可能釋出地更加頻繁。

  AB測試及其他

  我們所有的客戶端都用了伺服器端提供的功能標記,稱為variants,用於AB測試以及指導未完成功能的開發。

  剩下還有一些框架相關的內容我沒有提及:Algolia讓我們在搜尋相關功能上快速迭代,SendGrid處理郵件,Urban Airship用來傳送提醒,SQS用來處理佇列,Bloomd用作布隆過濾器,PubSubHubbubSuperfeedr用作提供RSS等等。

  編譯、測試和部署

  我們積極擁抱持續整合技術,隨時隨地準備釋出,使用Jenkins來負責相關事宜。

  我們曾經使用Make作為編譯系統,但是後來遷移到Pants

  測試方面我們採用單元測試和HTTP層面功能測試兩者結合的方式。所有提交的程式碼都需要通過測試才能夠合併。我們跟Box團隊合作,利用Cluster Runner來分散式執行測試,保證效率,而且能夠和GitHub很好的整合在一起。

  我們大概不到15分鐘就可以把某階段的系統部署,順利編譯通過,留作正式產品的備選。主應用伺服器通常一天要部署五次,多的時候十次。

  我們採用藍綠部署。正式產品版本的流量傳送給一個canary例項,釋出程式會監控部署過程的錯誤率,必要時候通過調整內部DNS回滾。

  面向未來

  到此,講了足夠多的乾貨!為了重構產品,獲得更好的閱讀體驗,還有很長的路要走。我們仍然在努力為作者和釋出者設計更多的功能。打比方來講,線上閱讀還是一片綠地,面對它有著無限可能,我們始終抱著開放的心態設計和實現功能。未來我們會努力用各種功能為使用者提供高質量內容和價值。

相關文章