從0到100——知乎架構(gòu)變遷史
也許很多人還不知道,知乎在規(guī)模上是僅次于百度貼吧和豆瓣的中文互聯(lián)網(wǎng)最大的UGC(用戶生成內(nèi)容)社區(qū)。知乎創(chuàng)業(yè)三年來,從0開始,到現(xiàn)在已經(jīng)有了100多臺(tái)服務(wù)器。目前知乎的注冊用戶超過了1100萬,每個(gè)月有超過8000萬人使用;網(wǎng)站每個(gè)月的PV超過2.2億,差不多每秒鐘的動(dòng)態(tài)請(qǐng)求超過2500。
在ArchSummit北京2014大會(huì)上,知乎聯(lián)合創(chuàng)始人兼 CTO 李申申帶來了知乎創(chuàng)業(yè)三年多來的首次全面技術(shù)分享(幻燈片下載)。本文系根據(jù)演講內(nèi)容整理而成。
初期架構(gòu)選型
在2010年10月真正開始動(dòng)手做知乎這個(gè)產(chǎn)品時(shí),包含李申申在內(nèi),最初只有兩位工程師;到2010年12月份上線時(shí),工程師是四個(gè)。
知乎的主力開發(fā)語言是Python。因?yàn)镻ython簡單且強(qiáng)大,能夠快速上手,開發(fā)效率高,而且社區(qū)活躍,團(tuán)隊(duì)成員也比較喜歡。
知乎使用的是Tornado框架。因?yàn)樗С之惒?,很適合做實(shí)時(shí)Comet應(yīng)用,而且簡單輕量,學(xué)習(xí)成本低,再就是有FriendFeed 的成熟案例,F(xiàn)acebook 的社區(qū)支持。知乎的產(chǎn)品有個(gè)特性,就是希望跟瀏覽器端建立一個(gè)長連接,便于實(shí)時(shí)推送Feed和通知,所以Tornado比較合適。
最初整個(gè)團(tuán)隊(duì)的精力全部放在產(chǎn)品功能的開發(fā)上,而其他方面,基本上能節(jié)約時(shí)間、能省的都用最簡單的方法來解決,當(dāng)然這在后期也帶來了一些問題。
最初的想法是用云主機(jī),節(jié)省成本。知乎的第一臺(tái)服務(wù)器是512MB內(nèi)存的Linode主機(jī)。但是網(wǎng)站上線后,內(nèi)測受歡迎程度超出預(yù)期,很多用戶反饋網(wǎng)站很慢??鐕W(wǎng)絡(luò)延遲比想象的要大,特別是國內(nèi)的網(wǎng)絡(luò)不均衡,全國各地用戶訪問的情況都不太一樣。這個(gè)問題,再加上當(dāng)時(shí)要做域名備案,知乎又回到了自己買機(jī)器找機(jī)房的老路上。
買了機(jī)器、找了機(jī)房之后又遇到了新的問題,服務(wù)經(jīng)常宕掉。當(dāng)時(shí)服務(wù)商的機(jī)器內(nèi)存總是出問題,動(dòng)不動(dòng)就重啟。終于有一次機(jī)器宕掉起不來了,這時(shí)知乎就做了Web和數(shù)據(jù)庫的高可用。創(chuàng)業(yè)就是這樣一個(gè)情況,永遠(yuǎn)不知道明早醒來的時(shí)候會(huì)面臨什么樣的問題。
這是當(dāng)時(shí)那個(gè)階段的架構(gòu)圖,Web和數(shù)據(jù)庫都做了主從。當(dāng)時(shí)的圖片服務(wù)托管在又拍云上。 除了主從,為了性能更好還做了讀寫分離。為解決同步問題,又添加了一個(gè)服務(wù)器來跑離線腳本,避免對(duì)線上服務(wù)造成響應(yīng)延遲。另外,為改進(jìn)內(nèi)網(wǎng)的吞吐量延遲, 還更換了設(shè)備,使整個(gè)內(nèi)網(wǎng)的吞吐量翻了20倍。
在2011年上半年時(shí),知乎對(duì)Redis已經(jīng)很依賴。除了最開始的隊(duì)列、搜索在用,后來像Cache也開始使用,單機(jī)存儲(chǔ)成為瓶頸,所以引入了分片,同時(shí)做了一致性。
知乎團(tuán)隊(duì)是一個(gè)很相信工具的團(tuán)隊(duì),相信工具可以提升效率。工具其實(shí)是一個(gè)過程,工具并沒有所謂的最好的工具,只有最適合的工具。而且它是在整個(gè)過程中,隨著整個(gè)狀態(tài)的變化、環(huán)境的變化在不斷發(fā)生變化的。知乎自己開發(fā)或使用過的工具包括Profiling(函數(shù)級(jí)追蹤請(qǐng)求,分析調(diào)優(yōu))、Werkzeug(方便調(diào)試的工具)、Puppet(配置管理)和Shipit(一鍵上線或回滾)等。
日志系統(tǒng)
知乎最初是邀請(qǐng)制的,2011年下半年,知乎上線了申請(qǐng)注冊,沒有邀請(qǐng)碼的用戶也可以通過填寫一些資料申請(qǐng)注冊知乎。用戶量又上了一個(gè)臺(tái)階,這時(shí)就有了一些發(fā)廣告的賬戶,需要掃除廣告。日志系統(tǒng)的需求提上日程。
這個(gè)日志系統(tǒng)必須支持分布式收集、集中存儲(chǔ)、實(shí)時(shí)、可訂閱和簡單等特性。當(dāng)時(shí)調(diào)研了一些開源系統(tǒng),比如Scribe總體不錯(cuò),但是不支持訂閱。Kafka是Scala開發(fā)的,但是團(tuán)隊(duì)在Scala方面積累較少,F(xiàn)lume也是類似,而且比較重。所以開發(fā)團(tuán)隊(duì)選擇了自己開發(fā)一個(gè)日志系統(tǒng)——Kids(Kids Is Data Stream)。顧名思義,Kids是用來匯集各種數(shù)據(jù)流的。
Kids參考了Scribe的思路。Kdis在每臺(tái)服務(wù)器上可以配置成Agent或 Server。Agent直接接受來自應(yīng)用的消息,把消息匯集之后,可以打給下一個(gè)Agent或者直接打給中心Server。訂閱日志時(shí),可以從 Server上獲取,也可以從中心節(jié)點(diǎn)的一些Agent上獲取。
具體細(xì)節(jié)如下圖所示:
知乎還基于Kids做了一個(gè)Web小工具(Kids Explorer),支持實(shí)時(shí)看線上日志,現(xiàn)在已經(jīng)成為調(diào)試線上問題最主要的工具。
Kids已經(jīng)開源,放到了Github上。
事件驅(qū)動(dòng)的架構(gòu)
知乎這個(gè)產(chǎn)品有一個(gè)特點(diǎn),最早在添加一個(gè)答案后,后續(xù)的操作其實(shí)只有更新通知、更新動(dòng) 態(tài)。但是隨著整個(gè)功能的增加,又多出了一些更新索引、更新計(jì)數(shù)、內(nèi)容審查等操作,后續(xù)操作五花八門。如果按照傳統(tǒng)方式,維護(hù)邏輯會(huì)越來越龐大,維護(hù)性也會(huì) 非常差。這種場景很適合事件驅(qū)動(dòng)方式,所以開發(fā)團(tuán)隊(duì)對(duì)整個(gè)架構(gòu)做了調(diào)整,做了事件驅(qū)動(dòng)的架構(gòu)。
這時(shí)首先需要的是一個(gè)消息隊(duì)列,它應(yīng)該可以獲取到各種各樣的事件,而且對(duì)一致性有很高的 要求。針對(duì)這個(gè)需求,知乎開發(fā)了一個(gè)叫Sink的小工具。它拿到消息后,先做本地的保存、持久化,然后再把消息分發(fā)出去。如果那臺(tái)機(jī)器掛掉了,重啟時(shí)可以 完整恢復(fù),確保消息不會(huì)丟失。然后它通過Miller開發(fā)框架,把消息放到任務(wù)隊(duì)列。Sink更像是串行消息訂閱服務(wù),但任務(wù)需要并行化處理, Beanstalkd就派上了用場,由其對(duì)任務(wù)進(jìn)行全周期管理。架構(gòu)如下圖所示:
舉例而言,如果現(xiàn)在有用戶回答了問題,首先系統(tǒng)會(huì)把問題寫到MySQL里面,把消息塞到Sink,然后把問題返回給用戶。Sink通過Miller把任務(wù)發(fā)給 Beanstalkd,Worker自己可以找到任務(wù)并處理。
最開始上線時(shí),每秒鐘有10個(gè)消息,然后有70個(gè)任務(wù)產(chǎn)生?,F(xiàn)在每秒鐘有100個(gè)事件,有1500個(gè)任務(wù)產(chǎn)生,就是通過現(xiàn)在的事件驅(qū)動(dòng)架構(gòu)支撐的。
頁面渲染優(yōu)化
知乎在2013年時(shí)每天有上百萬的PV,頁面渲染其實(shí)是計(jì)算密集型的,另外因?yàn)橐@取數(shù)據(jù),所以也有IO密集型的特點(diǎn)。這時(shí)開發(fā)團(tuán)隊(duì)就對(duì)頁面進(jìn)行了組件化,還升級(jí)了數(shù)據(jù)獲取機(jī)制。知乎按照整個(gè)頁面組件樹的結(jié)構(gòu),自上而下分層地獲取數(shù)據(jù),當(dāng)上 層的數(shù)據(jù)已經(jīng)獲取了,下層的數(shù)據(jù)就不需要再下去了,有幾層基本上就有幾次數(shù)據(jù)獲取。
結(jié)合這個(gè)思路,知乎自己做了一套模板渲染開發(fā)框架——ZhihuNode。
經(jīng)歷了一系列改進(jìn)之后,頁面的性能大幅度提升。問題頁面從500ms 減少到150ms,F(xiàn)eed頁面從1s減少到600ms。
面向服務(wù)的架構(gòu)(SOA)
隨著知乎的功能越來越龐雜,整個(gè)系統(tǒng)也越來越大。知乎是怎么做的服務(wù)化呢?
首先需要一個(gè)最基本的RPC框架,RPC框架也經(jīng)歷了好幾版演進(jìn)。
第一版是Wish,它是一個(gè)嚴(yán)格定義序列化的模型。傳輸層用到了STP,這是自己寫的很 簡單的傳輸協(xié)議,跑在TCP上。一開始用的還不錯(cuò),因?yàn)橐婚_始只寫了一兩個(gè)服務(wù)。但是隨著服務(wù)增多,一些問題開始出現(xiàn),首先是 ProtocolBuffer會(huì) 生成一些描述代碼,很冗長,放到整個(gè)庫里顯得很丑陋。另外嚴(yán)格的定義使其不便使用。這時(shí)有位工程師開發(fā)了新的RPC框架——Snow。它使用簡單的 JSON做數(shù)據(jù)序列化。但是松散的數(shù)據(jù)定義面對(duì)的問題是,比如說服務(wù)要去升級(jí),要改寫數(shù)據(jù)結(jié)構(gòu),很難知道有哪幾個(gè)服務(wù)在使用,也很難通知它們,往往錯(cuò)誤就 發(fā)生了。于是又出了第三個(gè)RPC框架,寫RPC框架的工程師,希望結(jié)合前面兩個(gè)框架的特點(diǎn),首先保持Snow簡單,其次需要相對(duì)嚴(yán)格的序列化協(xié)議。這一版 本引入了 Apache Avro。同時(shí)加入了特別的機(jī)制,在傳輸層和序列化協(xié)議這一層都做成了可插拔的方式,既可以用JSON,也可以用Avro,傳輸層可以用STP,也可以用 二進(jìn)制協(xié)議。
再就是搭了一個(gè)服務(wù)注冊發(fā)現(xiàn),只需要簡單的定義服務(wù)的名字就可以找到服務(wù)在哪臺(tái)機(jī)器上。同時(shí),知乎也有相應(yīng)的調(diào)優(yōu)的工具,基于Zipkin開發(fā)了自己的 Tracing系統(tǒng)。
按照調(diào)用關(guān)系,知乎的服務(wù)分成了3層:聚合層、內(nèi)容層和基礎(chǔ)層。按屬性又可以分成3類:數(shù)據(jù)服務(wù)、邏輯服務(wù)和通道服務(wù)。數(shù)據(jù)服務(wù)主要是一些要做特殊數(shù)據(jù)類型的存儲(chǔ),比如圖片服務(wù)。邏輯服務(wù)更多的是CPU密集、計(jì)算密集的操作,比如答案格式的定義、解析等。通道服務(wù)的特點(diǎn)是沒有存儲(chǔ),更多是做一個(gè)轉(zhuǎn)發(fā),比如說Sink。
這是引入服務(wù)化之后整體的架構(gòu)。
演講中還介紹了基于AngularJS開發(fā)知乎專欄的新實(shí)踐。后續(xù)我們將在網(wǎng)站上發(fā)布演講視頻,敬請(qǐng)期待。