Medium開發團隊談架構設計
背景
說到底,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(Elasticsearch、Logstash、Kibana)來進行產品問題調試。
數據庫
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,用以生成應用中的對象。
我們利用mockito和robolectric。我們會開發一些高層測試來運轉activity和poke:剛添加screen或要重構的時候,先創建一些基本的版本,隨著我們復現bug它們也會進化。我們還會開發一些底層測試,來檢測一個特定的類:隨著新功能的增加我們會創建測試,這能夠幫助我們思考和設計底層是如何交互的。
每個提交都會作為alpha版本自動推送到play商店,然后到Medium員工(包括我們的Hatch,Medium內部版)。推送大部分發生在周五,我們會把alpha版本發送給測試小組,請他們用整個周末進行測試。然后,周一我們會從beta版推進至正式產品版。因為最近一批代碼總是隨時可以推送,因此一旦發現很嚴重的bug,我們就可以立即修復正式產品版。當我們懷疑某些新功能的時候,可以給測試小組更長的時間。開發比較亢奮的時候,也可能發布地更加頻繁。
AB測試及其他
我們所有的客戶端都用了服務器端提供的功能標記,稱為variants,用于AB測試以及指導未完成功能的開發。
剩下還有一些框架相關的內容我沒有提及:Algolia讓我們在搜索相關功能上快速迭代,SendGrid處理郵件,Urban Airship用來發送提醒,SQS用來處理隊列,Bloomd用作布隆過濾器,PubSubHubbub和Superfeedr用作提供RSS等等。
編譯、測試和部署
我們積極擁抱持續集成技術,隨時隨地準備發布,使用Jenkins來負責相關事宜。
我們曾經使用Make作為編譯系統,但是后來遷移到Pants。
測試方面我們采用單元測試和HTTP層面功能測試兩者結合的方式。所有提交的代碼都需要通過測試才能夠合并。我們跟Box團隊合作,利用Cluster Runner來分布式運行測試,保證效率,而且能夠和GitHub很好的整合在一起。
我們大概不到15分鐘就可以把某階段的系統部署,順利編譯通過,留作正式產品的備選。主應用服務器通常一天要部署五次,多的時候十次。
我們采用藍綠部署。正式產品版本的流量發送給一個canary實例,發布進程會監控部署過程的錯誤率,必要時候通過調整內部DNS回滾。
面向未來
到此,講了足夠多的干貨!為了重構產品,獲得更好的閱讀體驗,還有很長的路要走。我們仍然在努力為作者和發布者設計更多的功能。打比方來講,線上閱讀還是一片綠地,面對它有著無限可能,我們始終抱著開放的心態設計和實現功能。未來我們會努力用各種功能為用戶提供高質量內容和價值。