本篇文章會(huì)向讀者展示幾個(gè)架構(gòu)設(shè)計(jì)的關(guān)鍵點(diǎn),使一個(gè)社交應(yīng)用能夠成為真正的下一代社交產(chǎn)品。以下幾個(gè)屬性將會(huì)影響到架構(gòu)的設(shè)計(jì):
a)可用性
b)可擴(kuò)展性
c)性能和靈活性可擴(kuò)展
目標(biāo)
a)確保用戶的內(nèi)容數(shù)據(jù)能夠很方便的被其他用戶發(fā)現(xiàn)和獲取.
b)確保內(nèi)容推送是相關(guān)的,不僅在語(yǔ)義上,也是從用戶設(shè)備的角度。
c)確保實(shí)時(shí)更新生成、推送和分析。
d)盡可能地節(jié)省用戶的資源。
e)不論服務(wù)器負(fù)載變化如何,用戶體驗(yàn)應(yīng)保持不變。
f)確保應(yīng)用整體上是安全的
總之,我們要處理一個(gè)相當(dāng)大的挑戰(zhàn),我們必須處理不斷擴(kuò)大的海量用戶生成的內(nèi)容數(shù)據(jù),不斷增長(zhǎng)的用戶,和一個(gè)不斷迭代的新項(xiàng)目,同時(shí)必須確保性能足夠出色。為了應(yīng)對(duì)上述的挑戰(zhàn),我們必須學(xué)習(xí)架構(gòu)某些關(guān)鍵的元素,這將影響到系統(tǒng)的設(shè)計(jì)。以下是一些關(guān)鍵的決定和分析。
數(shù)據(jù)存儲(chǔ)
數(shù)據(jù)和數(shù)據(jù)模型的存儲(chǔ)是一個(gè)好架構(gòu)的關(guān)鍵設(shè)計(jì)之一。一個(gè)社交產(chǎn)品應(yīng)該能夠處理多種類型的數(shù)據(jù),因此首先得充分分析數(shù)據(jù)并透徹理解,之后再設(shè)計(jì)數(shù)據(jù)模型和數(shù)據(jù)存儲(chǔ)。
第一步,我們要確定哪些數(shù)據(jù)是經(jīng)常查詢的熱點(diǎn)數(shù)據(jù),哪些不是經(jīng)常需要的那些數(shù)據(jù)(如歸檔數(shù)據(jù)用于分析)。對(duì)于高頻訪問(wèn)的數(shù)據(jù),它必須總是可用,能夠快速讀寫(xiě)和水平可擴(kuò)展。目前我們所有業(yè)務(wù)場(chǎng)景使用的都是MySQL,即使我們的用例不一定需要使用關(guān)系數(shù)據(jù)庫(kù)系統(tǒng)。隨著我們數(shù)據(jù)的增長(zhǎng),我們的讀寫(xiě)將成為我們應(yīng)用程序性能瓶頸。我們應(yīng)該為每秒鐘數(shù)十億的查詢做好準(zhǔn)備。
讓我們對(duì)我們的數(shù)據(jù)進(jìn)行分類:
a)主要的數(shù)據(jù)或靜態(tài)形式的數(shù)據(jù),如用戶資料
b)語(yǔ)義數(shù)據(jù)
c)用戶產(chǎn)生的內(nèi)容數(shù)據(jù)
d)會(huì)話數(shù)據(jù)
找到一個(gè)高效的數(shù)據(jù)存儲(chǔ)方式,滿足所有這些類型的數(shù)據(jù),真的很難。因此,我們將為每個(gè)數(shù)據(jù)類型選擇特定的數(shù)據(jù)存儲(chǔ)方式。
靜態(tài)數(shù)據(jù):對(duì)于靜態(tài)數(shù)據(jù),最好是選擇基于文檔的存儲(chǔ)方式,其中鍵和值都是可查詢的。我們可以選擇如MongoDB這種文檔型數(shù)據(jù)庫(kù),選擇MongoDB最大的優(yōu)勢(shì)是它提供了在文檔級(jí)別的ACID。
MongoDB可以在多個(gè)分布式數(shù)據(jù)中心的范圍內(nèi)進(jìn)行縮放。它將允許我們使用副本集來(lái)保持冗余,從而解決我們的可用性問(wèn)題。
數(shù)據(jù)分片是一個(gè)重要的考慮因素,數(shù)據(jù)分片可以確保數(shù)據(jù)的擴(kuò)展與查詢速度。幸運(yùn)的是,MongoDB透明的支持了數(shù)據(jù)分片。
關(guān)聯(lián)的或關(guān)系數(shù)據(jù)(核心數(shù)據(jù)):我們大部分?jǐn)?shù)據(jù)本質(zhì)上是關(guān)聯(lián)的,例如,A是B的朋友,C是A和B的朋友,這樣高度語(yǔ)義的數(shù)據(jù)最適合圖處理模型。我們應(yīng)將這樣的數(shù)據(jù)存儲(chǔ)在圖數(shù)據(jù)庫(kù),如Neo4j。這樣做的優(yōu)勢(shì)很明顯;我們可以存儲(chǔ)所有關(guān)聯(lián)數(shù)據(jù)的節(jié)點(diǎn),從而節(jié)省了計(jì)算數(shù)據(jù)之間連接關(guān)系的額外步驟。圖形數(shù)據(jù)模型也將有助于我們捕捉到屬性之間的關(guān)系。當(dāng)試圖探索關(guān)聯(lián)數(shù)據(jù)時(shí),豐富的屬性關(guān)系絕對(duì)是關(guān)鍵。圖數(shù)據(jù)庫(kù)支持ACID規(guī)則以及自動(dòng)索引。
再次聲明,我們的要求是達(dá)到可用性和可擴(kuò)展性。我們可能會(huì)有成百上千的并發(fā)事務(wù),同時(shí)寫(xiě)入數(shù)據(jù)庫(kù),同時(shí)會(huì)有數(shù)百和數(shù)千查詢請(qǐng)求。它應(yīng)該能夠處理一個(gè)數(shù)據(jù)集上的許多字節(jié),超過(guò)十億每秒的讀取速度。
我們將會(huì)需要一個(gè)系統(tǒng),幫助我們自動(dòng)伸縮寫(xiě)入和讀取。其他需要考慮的因素是數(shù)據(jù)分片,這是系統(tǒng)可伸縮的關(guān)鍵。
Neo4j已經(jīng)被設(shè)計(jì)為可水平擴(kuò)展,并且有數(shù)據(jù)冗余功能來(lái)保證可用性。但到目前為止,它還不支持?jǐn)?shù)據(jù)分片。我們可能需要更多的分析,才能做出抉擇。其他可供選擇的圖數(shù)據(jù)庫(kù)有FlockDB、AllegroGraph和InfiniteGraph。
二進(jìn)制數(shù)據(jù)(UGC):我們還必須處理大量的與用戶相關(guān)的二進(jìn)制數(shù)據(jù)。處理二進(jìn)制數(shù)據(jù)不太容易,考慮到它們的規(guī)模。上面已經(jīng)討論過(guò),我們需要一個(gè)系統(tǒng)可以運(yùn)行相當(dāng)高的性能,秒級(jí)別(尖峰),當(dāng)決定在哪里存儲(chǔ)時(shí),可伸縮和可用性是最關(guān)鍵的素。我們不能依靠磁盤(pán)文件系統(tǒng)來(lái)存儲(chǔ)我們的二進(jìn)制數(shù)據(jù)。我們必須考慮可用性和可擴(kuò)展性,文件系統(tǒng)的緩存會(huì)消耗大量的CPU。相反的,我們應(yīng)該依靠一個(gè)現(xiàn)有的可用的系統(tǒng),例如亞馬遜S3,S3是非常流行的對(duì)象存儲(chǔ)系統(tǒng),具有可用性和彈性存儲(chǔ)。
我們也可以考慮谷歌云存儲(chǔ)或Rackspace的云文件等,但S3似乎是明顯的贏家,它提供更優(yōu)質(zhì)的服務(wù)。
S3已經(jīng)支持?jǐn)?shù)據(jù)分區(qū)。S3能夠水平伸縮,冷熱數(shù)據(jù)拆分,并根據(jù)keys分區(qū)。但是只實(shí)現(xiàn)存儲(chǔ)數(shù)據(jù)是不夠的,與這些內(nèi)容相關(guān)的元數(shù)據(jù)必須能夠被搜索,并且搜索可伸縮,速度夠快。我們也可以嘗試一些新的東西,如圖像的自動(dòng)維度識(shí)別,基于內(nèi)容自動(dòng)打標(biāo)簽等。這是一個(gè)潛在的知識(shí)產(chǎn)權(quán)領(lǐng)域。我們將在文章的索引部分討論索引需求。但現(xiàn)在,讓我們只需要注意,我們將用標(biāo)識(shí)符存儲(chǔ)內(nèi)容,并且在某個(gè)地方做了索引。似乎亞馬遜的S3最適合這種情況。
Session數(shù)據(jù)
正確的認(rèn)識(shí)和理解session數(shù)據(jù)是非常重要的。Session數(shù)據(jù)將幫助我們保持用戶的狀態(tài)。Session數(shù)據(jù)必須使用與服務(wù)器無(wú)關(guān)的方式,方便我們服務(wù)端可伸縮部署。這將有助于保持我們的設(shè)計(jì)靈活,確保session不會(huì)綁定到特定的節(jié)點(diǎn)或服務(wù)器。
我們得用一種新的方式來(lái)更新用戶的實(shí)際session,如果用戶的session終止,我們?nèi)匀豢梢詭椭脩魪囊粋€(gè)地方,他離開(kāi)的地方重新恢復(fù)信息。
這是特別重要的,在我們的場(chǎng)景中,連接是不可靠的,數(shù)據(jù)丟包是很正常的。數(shù)據(jù)必須能夠被跨節(jié)點(diǎn)訪問(wèn),因此需要可用性和可擴(kuò)展性。我們可以很好的使用MongoDB本身來(lái)保存數(shù)據(jù)。后來(lái),我們想轉(zhuǎn)移到純粹的鍵值存儲(chǔ),如Redis。
注:所有推薦和離線作業(yè)都應(yīng)該只運(yùn)行在非服務(wù)節(jié)點(diǎn)上。
索引
索引是我們系統(tǒng)的關(guān)鍵。用戶可以搜索任何內(nèi)容,這是我們的主要用例之一。為了提升搜索性能,我們必須非常認(rèn)真地對(duì)待索引。這里有兩點(diǎn)需要考慮:首先是,創(chuàng)建索引本身,然后就是索引系統(tǒng)本身。
為了做一個(gè)有意義的搜索系統(tǒng),我們必須設(shè)計(jì)一個(gè)實(shí)時(shí)索引,針對(duì)一段時(shí)間窗口的實(shí)時(shí)數(shù)據(jù)進(jìn)行處理。首先,我們可以寫(xiě)一個(gè)非常簡(jiǎn)單的系統(tǒng),對(duì)產(chǎn)生的內(nèi)容數(shù)據(jù)做倒排索引。后來(lái),隨著輸入數(shù)據(jù)的增加,我們可以方便地用實(shí)時(shí)數(shù)據(jù)處理引擎取代它,如Apache的Storm,這是一個(gè)分布式的,容錯(cuò)和高度可擴(kuò)展的系統(tǒng)。它可以負(fù)責(zé)生成索引的邏輯。
索引系統(tǒng):由于Lucene受歡迎程度和其性能,因此,Lucene是一個(gè)顯而易見(jiàn)的好選擇;它的性能是無(wú)與倫比的。我們可以使用SolrCloud。它已經(jīng)透明的支持分片,復(fù)制和讀寫(xiě)方面的容錯(cuò)。
隊(duì)列&消息推送
每次我們的應(yīng)用程序被觸發(fā)一個(gè)事件,我們將需要向他/她的追隨者/朋友推送消息。重要的是,我們的系統(tǒng)不能錯(cuò)過(guò)任何這些信息,更重要的是,能夠在發(fā)生故障時(shí)恢復(fù)這些事件。為了達(dá)到這些要求,我們必須尋找一個(gè)隊(duì)列解決方案。我們可以使用ActiveMQ,這是最可靠的隊(duì)列軟件。它支持集群的高可用性,支持分布式隊(duì)列。
消息推送是另一個(gè)領(lǐng)域,要把通知發(fā)送給我們的用戶。在這里我們需要估計(jì)一下規(guī)模。我們應(yīng)該準(zhǔn)備好支持像nps這樣上億的規(guī)模。這里有許多選擇,但也許pyapns、CommandIQ和APP Booster才是最流行的。
我們需要自己管理一些事情,特別是要保證消息傳遞可靠性,即使用戶的設(shè)備處于離線狀態(tài)。我建議我們實(shí)現(xiàn)一個(gè)雙向的系統(tǒng),保持狀態(tài)的通知,并在后臺(tái)持久化到磁盤(pán)。所以每次一個(gè)通知失敗時(shí),它的狀態(tài)都被處理并標(biāo)上狀態(tài)碼,添加到重試隊(duì)列中。最后,當(dāng)通知被送達(dá),移出重試出列。
緩存策略
像我們這樣的系統(tǒng),我們的目標(biāo)是使其支撐十億RPS,因此,好的緩存策略是極重要的。我們的業(yè)務(wù)邏輯會(huì)在多層緩存中,并且能夠智能的清除失效緩存。讓我們看看最頂層緩存。
應(yīng)用層緩存(內(nèi)容緩存):為了最大限度地減少緩存未命中,并確保緩存始終是最新的數(shù)據(jù),我們必須尋找一個(gè)從未過(guò)期的緩存,并始終保持?jǐn)?shù)據(jù)。這基本上意味著在一般使用情況下,我們將永遠(yuǎn)不用查詢我們的數(shù)據(jù)庫(kù),因此節(jié)省了大量的資源。我們還應(yīng)該確保我們緩存的數(shù)據(jù)總是以一種不需要額外處理的格式,隨時(shí)準(zhǔn)備好呈現(xiàn)。這基本上意味著將我們的在線負(fù)載轉(zhuǎn)換為離線負(fù)載,從而節(jié)省了延遲。要做到這一點(diǎn),我們必須確保每一次的內(nèi)容被輸入到系統(tǒng)中,我們要做兩件事情:
a)原內(nèi)容是非規(guī)格化形式保存在緩存。為了安全起見(jiàn),我們將永遠(yuǎn)設(shè)置一個(gè)有效的期限。
b)原內(nèi)容也寫(xiě)在我們的數(shù)據(jù)存儲(chǔ)區(qū)中。
我們使用Redis來(lái)做這個(gè)緩存,Redis是一種具有良好故障恢復(fù)的內(nèi)存緩存。它具有高度的可擴(kuò)展性,較新的版本透明的支持了數(shù)據(jù)分片。支持主從節(jié)點(diǎn)配置。最好的部分是,我們能夠保存任何格式的數(shù)據(jù),這使得它很容易做增量寫(xiě),這是至關(guān)重要的,我們支持內(nèi)容feeds
還值得指出的是,我們需要支持對(duì)大內(nèi)容對(duì)象進(jìn)行大量的讀 - 修改 - 寫(xiě)操作和少量讀,Redis是已知的,對(duì)這些操作在性能方面是最好的。
緩存代理:反向代理層的緩存也是至關(guān)重要的。它有助于減少直接請(qǐng)求我們服務(wù)器的負(fù)載,從而減少延遲。為了使代理服務(wù)器緩存更有效,需要正確設(shè)置HTTP響應(yīng)頭。代理服務(wù)器有很多種,但最受歡迎的是nginx和ATS。
二級(jí)緩存(代碼級(jí)緩存):這是一個(gè)實(shí)體數(shù)據(jù)的本地存儲(chǔ),用于提高應(yīng)用程序的性能。它有助于通過(guò)減少昂貴的數(shù)據(jù)庫(kù)調(diào)用以提高性能,保持實(shí)體數(shù)據(jù)的本地化。EhCache是一個(gè)很受歡迎的選擇。
客戶端緩存:這實(shí)際上是設(shè)備或?yàn)g覽器緩存。所有靜態(tài)項(xiàng)目都應(yīng)該盡可能地緩存。如果API響應(yīng)HTTP緩存頭已經(jīng)被合理設(shè)置,很多相關(guān)資源的內(nèi)容都會(huì)被緩存。我們應(yīng)確保其如預(yù)期的那樣工作。除此之外,我們應(yīng)該盡可能緩存其他內(nèi)容,可以使用設(shè)備自己的內(nèi)存,或使用SQLite。所有昂貴的對(duì)象都應(yīng)該緩存。例如NSDateFormatter和NSCalendar,初始化緩慢,應(yīng)該盡可能多的重用。iOS Lot可以調(diào)整和應(yīng)用,但是在這里,它是超出我們的研究范圍。
數(shù)據(jù)壓縮
考慮到我們的用戶主要是要處理大量的圖像和視頻,需要下載大量的數(shù)據(jù),所以優(yōu)化下載大小是非常重要的。它將節(jié)省用戶的數(shù)據(jù)量,提高應(yīng)用程序的性能體驗(yàn)。
其他要考慮的方面,如我們的網(wǎng)絡(luò),我們的用戶主要是在非LTE網(wǎng)絡(luò),使用2.5G或3G,需要考慮帶寬,并且連接通常是不可靠的,數(shù)據(jù)使用成本高。在這種情況下,智能壓縮是一個(gè)關(guān)鍵的需求。
但是實(shí)際上圖像壓縮和視頻壓縮并不是想象中那么直接簡(jiǎn)單,往往需要進(jìn)行深入的分析。我們所處理的圖像和視頻,可以無(wú)損和有損,這取決于用戶的設(shè)備質(zhì)量。所以我建議使用多個(gè)壓縮技術(shù)來(lái)處理這種情況。在這種情況下,我們可以嘗試幀內(nèi)壓縮和幀間壓縮技術(shù)。
但總的來(lái)說(shuō)我們可以采用zpaq和fp8來(lái)應(yīng)對(duì)所有壓縮需求。我們也可以嘗試非常適合我們業(yè)務(wù)場(chǎng)景的WebP。一般情況下,我們的API會(huì)使用gzip,我們API response總是經(jīng)過(guò)gzip壓縮過(guò)的。
數(shù)據(jù)轉(zhuǎn)碼
考慮到我們需要處理多個(gè)設(shè)備,多個(gè)操作系統(tǒng)和屏幕分辨率,我們的內(nèi)容存儲(chǔ)和處理時(shí)應(yīng)與設(shè)備無(wú)關(guān)。但服務(wù)層應(yīng)該基于用戶的設(shè)備,理解并調(diào)整響應(yīng)的內(nèi)容。所以,圖像和視頻的轉(zhuǎn)碼是必不可少的。
我們的應(yīng)用程序需要收集設(shè)備的配置,如內(nèi)存、編碼和屏幕分辨率,作為API的上下文。我們的API應(yīng)該使用此上下文來(lái)修改/選擇內(nèi)容版本。基于我們接受到的設(shè)備上下文,我們可以預(yù)先準(zhǔn)備好一些最頻繁被請(qǐng)求的版本的內(nèi)容。
我們可以使用FFMPEG轉(zhuǎn)碼,F(xiàn)FMPEG是最可靠和應(yīng)用最廣的轉(zhuǎn)碼框架。我們可以修改FFMPEG,使其滿足我們的需求。轉(zhuǎn)碼是在數(shù)據(jù)輸入端完成的。
傳輸協(xié)議
考慮到我們的網(wǎng)絡(luò)場(chǎng)景(非LTE,不可靠的連接等),關(guān)鍵是要盡可能地節(jié)省資源,使通信盡可能地輕量。我建議我們所有的HTTP請(qǐng)求都使用okhttp客戶端,okhttp使用SPDY協(xié)議,能夠彈性處理連接失敗,透明恢復(fù)。
我們所有的通訊需求,都應(yīng)該切換到MQTT,這是一個(gè)輕量級(jí)的機(jī)器對(duì)機(jī)器的連接協(xié)議。
安全問(wèn)題
保證我們應(yīng)用程序的安全是非常重要的。我們整體架構(gòu)都要有安全上的考慮。我在這里只談架構(gòu)為滿足安全要求做出的改變,我們不談實(shí)施過(guò)程的改變。
這里是一些必須添加到架構(gòu)里的:
1. 我們所有的用戶數(shù)據(jù)必須加密。MongoDB和Neo4j已經(jīng)支持存儲(chǔ)加密。在這基礎(chǔ)上,我們可以決定加密哪些用戶關(guān)鍵信息。所有與數(shù)據(jù)庫(kù)相關(guān)的傳輸調(diào)用必須啟用加密。
2. 安全套接字層:所有代理服務(wù)器的訪問(wèn)都應(yīng)該使用SSLed。代理服務(wù)器可以充當(dāng)SSL終止點(diǎn)。
3. 我們所有的API端點(diǎn)應(yīng)該運(yùn)行在非默認(rèn)端口,并且必須實(shí)現(xiàn)OAuth。
4. 所有的DB讀取都應(yīng)該通過(guò)Rest endpoints。
5. 有關(guān)密碼的配置必須特殊處理。密碼必須hashed,文件應(yīng)該被限制只能在應(yīng)用啟動(dòng)時(shí)讀取。這允許我們通過(guò)文件系統(tǒng)權(quán)限來(lái)控制應(yīng)用程序身份實(shí)例。只有應(yīng)用程序用戶可以讀,但不能寫(xiě),其他用戶不可以讀取。所有類似的配置都要用keydb打包并需要密碼。
組件
以下是我們架構(gòu)用到的組件:
1. 負(fù)載均衡器:這層是用來(lái)轉(zhuǎn)發(fā)所有對(duì)代理服務(wù)器的請(qǐng)求,基于定制的策略。這一層也將有助于我們通過(guò)基于容量重定向的方式來(lái)保障可用性。
2. 代理服務(wù)器:所有即將到來(lái)的調(diào)用都必須以這里為入口。這也是我們SSL的終止點(diǎn)。它緩存所有基于策略定義的HTTP請(qǐng)求。FE層:該層運(yùn)行一個(gè)node服務(wù)器。
3. 數(shù)據(jù)輸入引擎:這個(gè)組件涉及所有內(nèi)容的輸入,它做了一系列的工作:非規(guī)范化模型,轉(zhuǎn)碼,緩存等。將來(lái)如果可以的話,所有內(nèi)容的處理,都可以在這里完成。
4. Rest服務(wù):這層負(fù)責(zé)與所有DB交互,并返回?cái)?shù)據(jù)。它的訪問(wèn)是受OAuth保護(hù)的。這可以用Tomcat容器以及edge緩存來(lái)實(shí)現(xiàn)。
5. 事件處理:這層處理所有的事件,主要負(fù)責(zé)分發(fā)的功能。它讀取ActiveMQ并使用通知引擎生成通知。
6. 推薦引擎:這個(gè)組件通過(guò)分析所有收集到的用戶動(dòng)態(tài)來(lái)做推薦。根據(jù)實(shí)際收集到的動(dòng)態(tài),我們可以部署各種基于親和力的算法。我們可以使用Apache Mahout提供的各種算法接口
系統(tǒng)的邏輯視圖:
結(jié)語(yǔ)
本篇文章更像是對(duì)關(guān)鍵組件高抽象層次的分析。如果需要實(shí)施的建議,可以做一個(gè)階段性的方式,但如果我們需要擴(kuò)展性并支持真正的用例,必須遵循我提出的這些規(guī)范。我沒(méi)有提起任何設(shè)計(jì)領(lǐng)域相關(guān)的內(nèi)容。這只是設(shè)計(jì)階段,需要更深入的分析和了解系統(tǒng)的當(dāng)前狀態(tài)。