在經過了初的業務原型驗證和上線運行期之后,用戶業務進入了高速成長階段。在這一階段,業務重點不再是方向上的調整,而是在原來基礎上的不斷深挖、擴展;開發不僅是功能的實現,還需要兼顧成本和性能;系統不再是單體架構,還會涉及系統的擴展和多系統之間的通信;高可用也不僅是服務自動拉起或者并行擴展,還需要考慮數據可靠、對用戶影響,以及服務等級協議(SLA)。
本文將以上述挑戰為出發點,介紹如何通過引入新的工具、新的架構,對原有系統進行升級和優化,來更好滿足這一階段需求,并為產品的進一步發展打下基礎。
隨著用戶業務的發展,原來的功能已經無法滿足要求,需要增強或者增加新的功能。在用戶數和訪問量達到一定規模后,原先單體架構下的簡單功能,如計數和排序,將變得復雜;隨著業務深入,定期舉行的秒殺、促銷等活動,給系統帶了巨大的壓力;由于數據量的飛速增長,單純的數據庫或者內存檢索已經無法滿足不斷增加的各種查詢需求;隨著業務數據量的增加,產品價值的提高,如何收集系統運行數據,分析業務運行狀態也成了基本需求。接下來我們聚焦這一階段的關鍵業務需求,并給出相應的解決方案。
在單體架構下,通過簡單的內存數據和對應算法就可以實現計數和排序功能。但是在大量數據和多節點協作的環境下,基于單點內存操作的實現會遇到高并發、數據同步、實時獲取等問題。在這一階段,通用方法是使用Redis的原生命令來實現計數和排序。
計數
在Redis中可用于計數的對象有字符串(string)、哈希表(hash)和有序集合(zset)3種,對應的命令分別是incr/incrby、hincrby和zincrby。
網站可以從用戶的訪問、交互中收集到有價值的信息。通過記錄各個頁面的被訪問次數,我們可以根據基本的訪問計數信息來決定如何緩存頁面,從而減少頁面載入時間并提升頁面的響應速度,優化用戶體驗。
計數器
要實現網頁點擊量統計,需要設計一個時間序列計數器,對網頁的點擊量按不同的時間精度(1s、5s、1min、5min、1h、5h、1d等)計數,以對網站和網頁監視和分析。
數據建模以網頁的地址作為KEY,定義一個有序集合(zset),內部各成員(member)分別由計數器的精度和計數器的名字組成,所有成員的分值(score)都是0。這樣所有精度的計數器都保存在了這個有序集合里,不包含任何重復元素,并且能夠允許一個接一個地遍歷所有元素。
對于每個計數器及每種精度,如網頁的點擊量計數器和5s,設計使用一個哈希表(hash)對象來存儲網頁在每5s時間片之內獲得的點擊量。其中,哈希表的每個原生的field都是某個時間片的開始時間,而原生的field對應的值則存儲了網頁在該時間片內獲得的點擊量。如圖1所示。
PRECESION = [1, 5, 60, 300, 3600, 18000, 86400] def update_counter(conn, name, count=1, now=None):
----now = now or time.time() ----pipe = conn.pipeline() ----for prec in PRECESION: --------pnow = int(now / prec) * prec --------hash = '%s:%s' % (prec, name) --------pipe.zadd('http:/.....xxxxxx', hash, 0) --------pipe.hincrby('count:' + hash, pnow, count) ----pipe.execute()
def get_counter(conn, name, precision): ----hash = '%s:%s' % (precision, name)
----data = conn.hgetall('count:' + hash)
----to_return = []
----for key, value in data.iteritems():
--------to_return.append((int(key), int(value))) ----to_return.sort()
----return to_return;
當然,這里只介紹了網頁點擊量數據的存儲模型,如果我們一味地對計數器進行更新而不執行任何清理操作的話,那么程序終將會因為存儲了過多的數據而導致內存不足,由于我們事先已經將所有已知的計數器都記錄到一個有序集合里面,所以對計數器進行清理只需要遍歷這個有序集合,并刪除其中的舊計數器即可。
排序
在Redis中可用于排序的有天然有序的有序集合(zset)和鍵(keys)類型中的SORT命令,其中SORT命令的功能非常強大,不僅可以對列表(list)、集合(set)和有序集合(zset)進行排序,還可以完成與關系型數據庫中的連接查詢相類似的任務,下面分別以兩個例子來介紹各自的應用。
帖子排序
論壇中的帖子通常會有各種排序方式方便用戶查看,比如按發帖時間排序、按回復時間排序、按回復數量排序、按閱讀量排序等,這些TOP N場景對響應時間要求比較高,非常適宜用有序集合(zset)來緩存排序信息,其中排序字段即為分值(score)字段。
127.0.0.1:6379> zadd page_rank 10 .com 8 bing.com 6 163.com 9 baidu.com (integer) 4 127.0.0.1:6379> zrange page_rank 0 -1 withscores 1) "163.com" 2) "6" 3) "bing.com" 4) "8" 5) "baidu.com" 6) "9" 7) ".com" 8) "10"
SORT命令
SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC | DESC] [ALPHA] [STORE destination]
SORT命令提供了多種參數,可以對列表,集合和有序集合進行排序,此外還可以根據降序升序來對元素進行排序(DESC、ASC);將元素看作是數字還是二進制字符串來進行排序(ALPHA);使用排序元素之外的其他值作為權重來進行排序(BY pattern)。
下面代碼清單展示了SORT命令的具體功能使用。
1.順序
127.0.0.1:6379> lpush mylist 30 10 8 19 (integer) 4 127.0.0.1:6379> sort price 1) "8" 2) "10" 3) "19" 4) "30"
2.逆序
127.0.0.1:6379> sort price desc 1) "30" 2) "19" 3) "10" 4) "8"
3.使用alpha修飾符對字符串進行排序
127.0.0.1:6379> lpush website www.163.com www.kaola.com www.baidu.com (integer) 3 127.0.0.1:6379> sort website alpha 1) "www.163.com" 2) "www.baidu.com" 3) "www.kaola.com"
127.0.0.1:6379> rpush num 1 4 2 7 9 6 5 3 8 10 (integer) 10 127.0.0.1:6379> sort num limit 0 5 1) "1" 2) "2" 3) "3" 4) "4" 5) "5"
可以使用外部key的數據作為權重,代替默認的直接對比鍵值的方式來進行排序。假設現在有用戶數據如表1所示。
| uid | user_name_{uid} | user_level_{uid} |
|---|---|---|
| 1 | helifu | 888 |
| 2 | netease | 666 |
| 3 | kaola | 777 |
| 4 | ncr | 4444 |
以下將哈希表(hash)作為by和get的參數,by和get選項都可以用key->field的格式來獲取哈希表中的域的值,其中key表示哈希表鍵,而field則表示哈希表的域。
1.數據輸入到Redis中
127.0.0.1:6379> hmset user_info_1 name helifu level 888 OK 127.0.0.1:6379> hmset user_info_2 name netease level 666 OK 127.0.0.1:6379> hmset user_info_3 name kaola level 777 OK 127.0.0.1:6379> hmset user_info_4 name ncr level 4444 OK
2.by選項
通過使用by選項,讓uid按其他鍵的元素來排序。
例如以下代碼讓uid鍵按照user_info_*->level的大小來排序。
127.0.0.1:6379> sort uid by user_info_*->level 1) "2" 2) "3" 3) "1" 4) "4"
3.get選項
使用get選項,可以根據排序的結果來取出相應的鍵值。
例如以下代碼先讓uid鍵按照user_info_*->level的大小來排序,然后再取出
user_info_ *->name的值。 127.0.0.1:6379> sort uid by user_info_*->level get user_info_*->name 1) "netease" 2) "kaola" 3) "helifu" 4) "ncr"
現在的排序結果要比只使用by選項要直觀得多。
4.排序獲取多個外部key
可以同時使用多個get選項,獲取多個外部鍵的值。
127.0.0.1:6379> sort uid get # get user_info_*->level get user_info_*->name 1) "1" 2) "888" 3) "helifu" 4) "2" 5) "666" 6) "netease" 7) "3" 8) "777" 9) "kaola" 10) "4" 11) "4444" 12) "ncr"
5.不排序獲取多個外部key
127.0.0.1:6379> sort uid by not-exists-key get # get user_info_*->level get user_info_*->name 1) "4" 2) "4444" 3) "nc" 4) "3" 5) "777" 6) "kaola" 7) "2" 8) "666" 9) "netease" 10) "1" 11) "888" 12) "helifu"
127.0.0.1:6379> lrange old 0 -1 1) "1" 2) "3" 3) "5" 6) "2" 7) "4" 127.0.0.1:6379> sort old store new (integer) 5 127.0.0.1:6379> type new list 127.0.0.1:6379> lrange new 0 -1 1) "1" 2) "2" 3) "3" 4) "4" 5) "5"
SORT命令的時間復雜度用公式表示為O(N+M*log(M)),其中N為要排序的列表或集合內的元素數量,M為要返回的元素數量。如果只是使用SORT命令的get選項獲取數據而沒有進行排序,時間復雜度為O(N)。
云環境下的實踐
在云服務中實現計數和排序,可以自己使用云主機搭建Redis服務,也可以使用云計算服務商提供的Redis服務。
對于高可用和性能有要求的場景,建議使用云計算服務商提供的Redis服務。專業的服務商會從底層到應用本身進行良好的優化,可用率、性能指標也遠高于自己搭建的Redis實例。同時,由于服務商提供了各種工具,開發運維成本也更低。
以網易云為例,網易云基礎服務提供了名為NCR(Netease Cloud Redis)的緩存服務,兼容開源Redis協議。并根據用戶具體使用需求和場景,提供了主從版本和分布式集群版本兩種架構。
主從服務版
如圖2所示,主從版本實例都提供一主一從兩個Redis實例,分別部署在不同可用域的節點上,以確保服務安全可靠。在單點故障時,主從服務通過主備切換來實現高可用。
主從版本使用較低的成本提供了高可用服務,但是也存在無法并行擴展等問題,因此適合數據量有限、對高可用有要求的產品使用。
分布式集群
分布式集群采用官方Redis集群方案,gossip/p2p的無中心節點設計實現,無代理設計客戶端直接與Redis集群的每個節點連接,計算出Key所在節點直接在對應的Redis節點上執行命令,如圖3所示,詳細的過程請參考后續Redis Cluster的相關介紹。
分布式集群采用多活模式,支持并行擴展,因此在性能、可用率方面有明顯優勢。但是由于分布式集群少需要3個節點,因此成本會較高,適合對可用率、性能有較高要求的用戶使用。
把秒殺服務單列出來進行分析,主要有下面兩個原因。
處理秒殺的指導思路
秒殺的核心問題就是極高并發處理,由于系統要在瞬時承受平時數十倍甚至上百倍的流量,這往往超出系統上限,因此處理秒殺的核心思路是流控和性能優化。
流控
盡可能在上游攔截和限制請求,限制流入后端的量,保證后端系統正常。
因為無論多少人參與秒殺,實際成交往往是有限的,而且遠小于參加秒殺的人數,因此可以通過前端系統進行攔截,限制終流入系統的請求數量,來保證系統正常進行。
在客戶端進行訪問限制,較為合適的做法是屏蔽用戶高頻請求,比如在網頁中設置5s一次訪問限制,可以防止用戶過度刷接口。這種做法較為簡單,用戶體驗也尚可,可以攔截大部分小白用戶的異常訪問,比如狂刷F5。關鍵是要明確告知用戶,如果像一些搶購系統那樣假裝提交一個排隊頁面但又不回應任何請求,就是赤裸裸的欺騙了。
對客戶端,特別是頁面端的限流,對稍有編程知識或者網絡基礎的用戶而言沒有作用(可以簡單修改JS或者模擬請求),因此服務端流控是必要的。服務端限流的配置方法有很多種,現在的主流Web服務器一般都支持配置訪問限制,可以通過配置實現簡單的流控。
但是這種限制一般都在協議層。如果要實現更為精細的訪問限制(根據業務邏輯限流),可以在后端服務器上,對不同業務實現訪問限制。常見做法是可以通過在內存或緩存服務中加入請求訪問信息,來實現訪問量限制。
上述的流控做法只能限制用戶異常訪問,如果正常訪問的用戶數量很多,就有后端系統壓力過大甚至異常宕機的可能,因此需要后端系統流量控制。
對于后端系統的訪問限制可以通過異步處理、消息隊列、并發限制等方式實現。核心思路是保證后端系統的壓力維持在可以正常處理的水平。對于超過系統負載的請求,可以選擇直接拒絕,以此來對系統進行保護,保證在極限壓力的情況下,系統有合理范圍內的處理能力。
系統架構優化
除了流控之外,提高系統的處理能力也是非常重要的,通過系統設計和架構優化,可以提高系統的吞吐量和抗壓能力。關于通用系統性能的提升,已經超出本節的范圍,這里只會提幾點和秒殺相關的優化。
系統擴容
這項內容是在云計算環境下才成為可能,相對于傳統的IT行業,云計算提供了快速的系統交付能力(min VS. day),因此可以做到按需分配,在業務需要時實現資源的并行擴展。
對一次成功的秒殺活動來說,無論如何限流,如何優化系統,終產生數倍于正常請求的壓力是很正常的。因此臨時性的系統擴容必不可少,系統擴容包括以下3個方面。
秒殺服務實踐
一般來說,流控的實現,特別是業務層流控,依賴于業務自身的設計,因此云計算提供的服務在于更多、更完善的基礎設計,來支持用戶進行更簡單的架構優化和擴容能力。
系統架構優化
通過CDN服務和對象存儲服務來分離靜態資源,實現靜態資源的加速,避免服務器被大量靜態資源請求過度占用。要實現異步的消息處理,可以使用隊列服務來傳輸消息,以達到消息異步化和流控。
系統擴容
云服務會提供按需計費的資源分配方式和分鐘級甚至秒級的資源交付能力,根據需要快速進行資源定制和交付。
內部系統可以通過負載均衡等服務實現并行擴展,在網易云基礎服務中,用戶可以直接使用Kubernetes的Replication Controller服務實現在線水平擴容。對于對外提供的Web系統,可以通過負載均衡服務實現水平在線擴展。
對于后端系統來說,建議使用云計算服務商提供的基礎服務來實現并行擴展。例如,網易云基礎服務就提供了分布式緩存服務和數據庫服務,支持在線擴容。
搜索,是用戶獲取信息的主要方式。在日常生活中,我們不管是購物(淘寶)、吃飯(大眾點評、美團網)還是旅游(攜程、去哪兒),都離不開搜索的應用。搜索幾乎成為每個網站、APP甚至是操作系統的標配。在用戶面前,搜索通常只是展示為一個搜索框,非常干凈簡潔,但它背后的原理可沒那么簡單,一個框的背后包含的是一整套搜索引擎的原理。假如我們需要搭建一個搜索系統為用戶提供服務,我們又需要了解什么呢?
基本原理
首先,我們需要知道全文檢索的基本原理,了解全文檢索系統在實際應用中是如何工作的。
通常,在文本中查找一個內容時,我們會采取順序查找的方法。譬如現在手頭上有一本計算機書籍,我們需要查找出里面包含了“計算機”和“人工智能”關鍵字的章節。一種方法就是從頭到尾閱讀這本計算機書籍,在每個章節都留心是否包含了“計算機”和“人工智能”這兩個詞。這種線性掃描就是簡單的計算機文檔檢索方式。這個過程通常稱為grepping,它來自于Unix下的一個文本掃描命令grep。在文本內進行grepping掃描很快,使用現代計算機會更快,并且在掃描過程中還可以通過使用正則表達式來支持通配符查找。總之,在使用現代計算機的條件下,對一個規模不大的文檔集進行線性掃描非常簡單,根本不需要做額外的處理。但是,很多情況下只采用上述掃描方式是遠遠不夠的,我們需要做更多的處理。這些情況如下所述。
此時,我們不能再采用上面的線性掃描方式。一種非線性掃描的方式是事先給文檔建立索引(Index)。回到上面所述的例子,假設我們讓計算機對整書本預先做一遍處理,找出書中所有的單詞都出現在了哪幾個章節(由于單詞會重復,通常都不會呈現爆炸式增長),并記錄下來。此時再查找“計算機”和“人工智能”單詞出現在哪幾個章節中,只需要將保存了它們出現過的章節做合并等處理,即可快速尋找出結果。存儲單詞的數據結構在信息檢索的專業術語中叫“倒排索引”(Inverted Index),因為它和正向的從章節映射到單詞關系相反,是倒著的索引映射關系。
這種先對整個文本建立索引,再根據索引在文本中進行查找的過程就是全文檢索(Full-text Search)的過程。圖4展示了全文檢索的一般過程。
首先是數據收集的過程(Gather Data),數據可以來源于文件系統、數據庫、Web抓取甚至是用戶輸入。數據收集完成后我們對數據按上述的原理建立索引(Index Documents),并保存至Index索引庫中。在圖的右邊,用戶使用方面,我們在頁面或API等獲取到用戶的搜索請求(Get Users’ Query),并根據搜索請求對索引庫進行查詢(Search Index),然后對所有的結果進行打分、排序等操作,終形成一個正確的結果返回給用戶并展示(Present Search Results)。當然在實現流程中還包含了數據抓取/爬蟲、鏈接分析、分詞、自然語言處理、索引結構、打分排序模型、文本分類、分布式搜索系統等技術,這是簡單抽象的流程描述。關于索引過程和搜索過程更詳細的技術就不做更多介紹了,感興趣的同學請參考其他專業書籍。
開源框架
根據上面的描述我們知道了全文檢索的基本原理,但要是想自己從頭實現一套搜索系統還是很困難的,沒有一個專業的團隊、一定的時間基本上做出不來,而且系統實現之后還需要面臨生產環境等各種問題的考驗,研發和維護成本都無比巨大。不過,現代的程序開發環境早已今非昔比,開源思想深入人心,開源軟件大量涌現。沒有特殊的需求,沒有人會重新開發一套軟件。我們可以站在開源巨人的肩膀上,直接利用開源軟件的優勢。目前市面上有較多的開源搜索引擎和框架,比較成熟和活躍的有以下幾種。
我們分別介紹這幾種開源方案,并比較一下它們的優劣。
Lucene
Lucene是一個Java語言開發的搜索引擎類庫,也是目前火的搜索引擎內核類庫。但需要注意的是,Lucene本身還不是一套完整的搜索引擎解決方案,它作為一個類庫只是包含了核心的搜索、索引等功能,如果想使用Lucene,還需要做一些額外的開發工作。
優點:
缺點:
Solr
Solr是基于Lucene開發、并由開發Lucene的同一幫人維護的一套全文搜索系統,Solr的版本通常和Lucene一同發布。Solr是流行的企業級搜索引擎之一。
優點:
缺點:
Elasticsearh
Elasticsearch是一個實時的分布式搜索和分析引擎,和Solr一樣,它底層基于Lucene框架。但它具有簡單、易用、功能/性能強大等優點,雖然晚于Solr出現,但迅速成長,已成為目前當仁不讓的熱門搜索引擎系統。
優點:
缺點:
Sphinx
Sphinx是基于C++ 開發的一個全文檢索引擎,從開始設計時就注重性能、搜索相關性及整合簡單性等方面。Sphinx可以批量索引和搜索SQL數據庫、NoSQL存儲中的數據,并提供和SQL兼容的搜索查詢接口。
優點:
缺點:
應用選型
通過上面的介紹,相信大家對各個開源搜索系統所適用的場景有了一定了解。
如果是中小型企業,想快速上手使用搜索功能,可以選擇Elasticsearch或Solr。如果對搜索性能和節省資源要求比較苛刻,可以考慮嘗試Sphinx。如果有很多定制化的搜索功能需求,可以考慮在Lucene基礎上做自定義開發。如果用于日志搜索,或者有很多的統計、分析等需求,可以選擇Elasticsearch。
開源方案實踐
如上述介紹,在實際開發當中已有了很多現成的開源方案可供選擇,但我們還是需要額外再做一些事情。譬如集群搭建、維護和管理、高可用設計、故障恢復、基本的機房申請及機器采購部署等。這也需要投入較高的人力和成本(雖然較自己研發已經節省很多),并且還需要配備專業的搜索開發及運維人員。
而在網易云中,我們基于開源Elasticsearch系統提供了一套簡單的方案。我們把專業、復雜的搜索系統服務化和簡單化,并降低準入門檻和成本,讓用戶可以直接使用平臺化的搜索服務。我們提供了全面的近實時、高可用、高可靠、高擴展等搜索系統強大功能,易于用戶使用。用戶使用網易云的云搜索后不再需要處理搜索系統的搭建與配置工作,而只需要在云搜索服務的產品管理平臺申請建立服務實例并配置索引數據格式,申請完成后云搜索平臺就會自動生成索引服務實例并提供全文檢索服務。
日志,一組隨時間增加的有序記錄,是開發人員熟悉的一種數據。通常,日志可以用來搜索查看關鍵狀態、定位程序問題,以及用于數據統計、分析等。
日志也是企業生產過程中產生的偉大財富。日志可以用來進行商業分析、用戶行為判斷和企業戰略決策等,良好地利用日志可以產生巨大的價值。所以,日志的收集不管是在開發運維還是企業決策中都十分重要。
第三方數據收集服務
在日志收集領域內,目前已經存在了種類繁多的日志收集工具,比較典型的有:rsyslog、syslog-ng、Logstash、FacebookScribe、ClouderaFlume、Fluentd和GraylogCollector等。
rsyslog是syslog的增強版,Fedora、Ubuntu、RHEL6、CentOS6、Debian等諸多Linux發行版都已使用rsyslog替換syslog作為默認的日志系統。rsyslog采用C語言實現,占用的資源少,性能高。專注于安全性及穩定性,適用于企業級別日志記錄需求。rsyslog可以傳輸100萬+/s(日志條數)的數據到本地目的地。即使通過網絡傳輸到遠程目的地,也能達到幾萬至幾十萬條/每秒的級別。rsyslog默認使用inotify實現文件監聽(可以更改為polling模式,實時性較弱),實時收集日志數據。
rsyslog支持實時監聽日志文件、支持通配符匹配目錄下的所有文件(支持輸出通配符匹配的具體文件名)、支持記錄文件讀取位置、支持文件Rotated、支持日志行首格式判斷(多行合并成一行)、支持自定義tag、支持直接輸出至file/mysql/kafka/elasticsearch等、支持自定義日志輸出模板,以及支持日志數據流控。
syslog-ng具有開源、可伸縮和可擴展等特點,使用syslog-ng,你可以從任何來源收集日志,以接近實時的處理,輸出到各種各樣的目的源。syslog-ng靈活地收集、分析、分類和關聯日志,存儲和發送到日志分析工具。syslog-ng支持常見的輸入,支持BSDsyslog(RFC3164)、RFC5424協議、JSON和journald消息格式。數據提取靈活,內置一組解析器,可以構建非常復雜事情。簡化復雜的日志數據,syslog-ng patterndb可以轉化關聯事件為一個統一格式。支持數據庫存儲,包括SQL(MySQL,PostgreSQL,Oracle)、MongoDB和Redis。syslog-ng支持消息隊列,支持高級消息隊列協議(AMQP)和面向簡單的文本消息傳遞協議(STOMP)與更多的管道。syslog-ng設計原則主要包括更好消息過濾粒度和更容易在不同防火墻網段轉發信息。前者能夠進行基于內容和優先權/facility的過濾。后者支持主機鏈,即使日志消息經過了許多計算機的轉發,也可以找出原發主機地址和整個轉發鏈。
Logstash是一個開源的服務器端數據處理管道,采集多種數據源的數據,轉換后發送到指定目的源。數據通常以各種格式分散在許多孤立系統上。Logstash支持種類豐富的inputs,以事件的方式從各種源獲取輸入,包括日志、Web應用、數據存儲設備和AWS服務等。在數據流從源到存儲設備途中,Logstash過濾事件,識別字段名,以便結構化數據,更有利于數據分析和創造商業價值。除了Elasticsearch,Logstash支持種類豐富的outputs,可以把數據轉發到合適的目的源。表2是幾個典型產品的特性對比。
| 名稱 | 開發語言 | 性能 | 所占資源 | 支持I/O插件種類 | 社區活躍度 |
|---|---|---|---|---|---|
| rsyslog | C | 高 | 少 | 中 | 中 |
| syslog-ng | C | 高 | 少 | 中 | 中 |
| LogStash/Beats | LogStash:Ruby Beats:Go | 中 | 多 | 多 | 高 |
| FacebookScribe | C++ | 高 | 少 | 少 | 中 |
| ClouderaFlume | Java | 中 | 多 | 中 | 中 |
| Fluentd | Ruby | 中 | 多 | 多 | 高 |
技術選型
由于日志收集的需求并非很復雜,此類工具大體上比較相似,用戶只需要根據其特性選擇合適自己需求的產品。
通常來說,對于日志收集客戶端資源占用要求較高的,可以選擇C語音開發的rsyslog、syslog-ng。對于易用性要求較高,可以選擇Logstash、Beats。對于日志收集后接入的后端有特殊需求,可以參考Fluentd是否可以滿足。如果公司用的是Java技術棧,可以選用Cloudera Flume。
除了基本的功能需求之外,一個互聯網產品往往還有訪問性能、高可用、可擴展等需要,這些統稱為非功能需求。一般來說,功能需求往往可以通過開發業務模塊來滿足,而非功能需求往往要從系統架構設計出發,從基礎上提供支持。
隨著用戶的增加,系統出現問題的影響也會增大。試想一下,一個小公司的主頁,或者個人開發維護的一個App的無法訪問,可能不會有多少關注。而支付寶、微信的宕機,則會直接被推到新聞頭條(2015年支付寶光纖被挖路機挖斷),并且會給用戶帶來嚴重的影響:鼓足勇氣表白卻發現信息丟失?掏出手機支付卻發生支付失敗,關鍵是還沒帶現金!在用戶使用高峰時,一次故障就會給產品帶來很大的傷害,如果頻繁出現故障則基本等同于死刑判決。
同樣,對一個小產品來說,偶發的延時、卡頓可能并不會有大的影響(可能已經影響到了用戶,只是范圍、概率較小)。而對于一個較為成熟的產品,良好的性能則是影響產品生死存亡的基本問題。試想一下,如果支付寶、微信經常出現卡頓、變慢,甚至在訪問高峰時崩潰,那它們還能支撐起現在的用戶規模,甚至成為基礎的服務設施嗎?可以說,良好的訪問性能,是一個產品從幼稚到成熟所必須解決的問題,也是一個成功產品的必備因素。實際上,很多有良好創意、商業前景很好的互聯網產品,就是因無法滿足用戶增長帶來的性能壓力而夭折。
隨著性能需求的不斷增長,所需要考慮的因素越多,出問題的概率也越大。因此,用戶數的不斷增長帶來的挑戰和問題幾乎呈幾何倍數增加,如果沒有良好的設計和規劃,隨著產品和業務的不斷膨脹,我們往往會陷入“修改→引入新問題→繼續調整→引入更多問題”的泥潭中無法自拔。
在這一階段,架構設計的重點不再是業務本身功能實現和架構的構建,而是如何通過優化系統架構,來滿足系統的高可用、并行擴展和系統加速等需要。
可擴展性是大規模系統穩定運行的基石。隨著互聯網用戶的不斷增加,一個成功產品的用戶量往往是數以億計,無論多強大的單點都無法滿足這種規模的性能需求。因此系統的擴展是一個成功互聯網產品的必然屬性,無法進行擴展的產品,注定沒有未來。
由于擴展性是一個非常大的范疇,并沒有一個四海皆準的手段或者技術來實現,因此本節主要介紹較為通用的可擴展系統設計,并以網易云為例,來介紹基礎設施對可擴展性的支持。
要實現系統的并行擴展,需要對原有的系統進行服務化拆分。在服務實現時,主要有兩種實現方式,分別是無狀態服務和有狀態服務。
特點
無狀態服務
指的是服務在處理請求時,不依賴除了請求本身外的其他內容,也不會有除了響應請求之外的額外操作。如果要實現無狀態服務的并行擴展,只需要對服務節點進行并行擴展,引入負載均衡即可。
有狀態服務
指的是服務在處理一個請求時,除了請求自身的信息外,還需要依賴之前的請求處理結果。
對于有狀態服務來說,服務本身的狀態也是正確運行的一部分,因此有狀態服務相對難以管理,無法通過簡單地增加實例個數來實現并行擴展。
對比
從技術的角度來看,有狀態服務和無狀態服務只是服務的兩種狀態,本身并沒有優劣之分。在實際的業務場景下,有狀態服務和無狀態服務相比,有各自的優勢。
有狀態服務的特性如下。
無狀態服務的特性如下。
總體來看,有狀態服務在服務架構較為簡單時,有易開發、高并發等優勢,而無狀態服務的優勢則體現在服務管理、并行擴展方面。隨著業務規模的擴大、系統復雜度的增加,無狀態服務的這些優勢,會越來越明顯。因此,對于一個大型系統而言,我們更推薦無狀態化的服務設計。
實踐
下面,我們根據不同的服務類型,來分析如何進行狀態分離。
常見的狀態信息
狀態分離
要把有狀態的服務改造成無狀態的服務,一般有以下兩種做法。
Web服務狀態分離
在Web服務中,兩種狀態分離模式都可以實現狀態分離。
服務器本身狀態分離
對于依賴本地存儲的服務,優先做法是把數據保存在公共的第三方存儲服務中,根據內容的不同,可以保存在對象存儲服務或者數據庫服務中。
如果很難把數據提取到外部存儲上,也不建議使用本地盤保存,而是建議通過掛載云硬盤的方式來保持本地狀態信息。這樣在服務異常時可以直接把云硬盤掛載在其他節點上來實現快速恢復。
對于網絡信息,好的做法是不要通過IP地址,而是通過域名來進行訪問。這樣當節點異常時,可以直接通過修改域名來實現快速的異常恢復。在網易云基礎服務中,我們提供了基于域名的服務訪問機制,直接使用域名來訪問內部服務,減少對網絡配置的依賴。
在線水平擴展能力是一個分布式系統需要提供的基本能力,也是在架構設計時需要滿足的重要功能點。而水平擴展能力也是業務發展的硬性需求,從產品的角度出發,產品的業務流量往往存在著很大波動,具體如下。
準備工作
產品對水平擴展的需求是一直存在的,但是受制于傳統IT行業按天甚至按周計算的資源交付能力,彈性伸縮一直是一個美好的愿望。直到云計算這個基礎設施完善之后,才使彈性伸縮的實現成為了可能。如果要實現彈性伸縮,需要以下幾點的支持。
實現要點
前端系統一般都會采用無狀態化服務設計,擴展相對簡單。在實踐中,有多種擴展方案,如通過DNS服務水平擴展、使用專有的apiserver、在SDK端分流及接入負載均衡等。其中負載均衡方案使用廣,綜合效果也好,可以滿足絕大多數場景下的需要,下面就以負載均衡服務為例,介紹前端系統水平擴展的實現要點。
協議選擇
負載均衡服務分為4層和7層服務,這兩種并不是截然分開的,而是有兼容關系。4層負載均衡可以支持所有7層的流量轉發,而且性能和效率一般也會更好。而7層負載均衡服務的優勢在于解析到了應用層數據(HTTP層),因此可以理解應用層的協議內容,從而做到基于應用層的高級轉發和更精細的流量控制。
對于HTTP服務,建議直接采用7層負載均衡,而其他所有類型的服務,如WebSocket、MySQL和Redis等,則可以直接使用TCP負載均衡。
無狀態服務
前端系統可擴展性需要在系統設計層面進行保證,較為通用的做法是無狀態化的服務設計。因為無狀態,所以在系統擴展時只需要考慮外網設施的支持,而不需要改動服務代碼。對于有狀態的服務,則盡量服務改造把狀態分離出去,將狀態拆分到可以擴展的第三方服務中去。
高級流量轉發
對于7層負載均衡來說,由于解析到了協議層,因此可以基于應用層的內容進行流量轉發。除了常用的粘性會話(Sticky Session)外,常用的轉發規則有基于域名和URL的流量轉發兩種。
后端系統,一般指用戶接入端(Web系統、長連接服務器)和各種中間件之后的后臺系統。在這一階段,重要的后端系統就是兩種,緩存服務和數據庫服務。下面,我們分別以Redis緩存服務和MySQL數據庫為例,來介紹后端系統水平擴展的技術和核心技術點。
Redis水平擴展
Redis去年發布了3.0版本,官方支持了Redis cluster即集群模式。至此結束了Redis沒有官方集群的時代,在官方集群方案以前應用廣泛的就屬Twitter發布的Twemproxy(https://github.com//twemproxy),國內的有豌豆莢開發的Codis(https://github. com/wandoulabs/codis)。
下面我們介紹一下Twemproxy和Redis Cluster兩種集群水平擴展。
Twemporxy+Sentinel方案
Twemproxy,也叫nutcraker,是Twitter開源的一個Redis和Memcache快速/輕量級代理服務器。
Twemproxy內部實現多種hash算法,自動分片到后端多個Redis實例上,Twemproxy支持失敗節點自動刪除,它會檢測與每個節點的連接是否健康。為了避免單點故障,可以平行部署多個代理節點(一致性hash算法保證key分片正確),Client可以自動選擇一個。
有了這些特性,再結合負載均衡和Sentinel就可以架構出Redis集群,如圖5所示。
水平擴展實現就可以將一對主從實例加入Sentinel中,并通知Twemporxy更新配置加入新節點,將部分key通過一致性hash算法分布到新節點上。
Twemproxy方案缺點如下。
Redis Cluster方案
Redis Cluster是Redis官方推出的集群解決方案,其設計的重要目標就是方便水平擴展,在1000個節點的時候仍能表現良好,并且可線性擴展。
Redis Cluster和傳統的集群方案不一樣,在設計的時候,就考慮到了去中心化、去中間件,也就是說,集群中的每個節點都是平等的關系,每個節點都保存各自的數據和整個集群的狀態。
數據的分配也沒有使用傳統的一致性哈希算法,取而代之的是一種叫做哈希槽(hash slot)的方式。Redis Cluster默認分配了16384個slot,當我們set一個key時,會用CRC16算法來取模得到所屬的slot,然后將這個key分到哈希槽區間的節點上,具體算法是CRC16(key) % 16384。舉個例子,假設當前集群有3個節點,那么:
集群拓撲結構如圖4-3所示,此處不再重復給出。
Redis Cluster水平擴展很容易操作,新節點加入集群中,通過redis-trib管理工具將其他節點的slot遷移部分到新節點上面,遷移過程并不影響客戶端使用,如圖6所示。
為了保證數據的高可用性,Redis Cluster加入了主從模式,一個主節點對應一個或多個從節點,主節點提供數據存取,從節點則是從主節點實時備份數據,當這個主節點癱瘓后,通過選舉算法在從節點中選取新主節點,從而保證集群不會癱瘓。
Redis Cluster其他具體細節可以參考官方文檔,這里不再詳細介紹。Redis Cluster方案缺點如下。
數據庫水平擴展
單機數據庫的性能由于物理硬件的限制會達到瓶頸,隨著業務數據量和請求訪問量的不斷增長,產品方除了需要不斷購買成本難以控制的高規格服務器,還要面臨不斷迭代的在線數據遷移。在這種情況下,無論是海量的結構化數據還是快速成長的業務規模,都迫切需要一種水平擴展的方法將存儲成本分攤到成本可控的商用服務器上。同時,也希望通過線性擴容降低全量數據遷移對線上服務帶來的影響,分庫分表方案便應運而生。
分庫分表的原理是將數據按照一定的分區規則Sharding到不同的關系型數據庫中,應用再通過中間件的方式訪問各個Shard中的數據。分庫分表的中間件,隱藏了數據Sharding和路由訪問的各項細節,使應用在大多數場景下可以像單機數據庫一樣,使用分庫分表后的分布式數據庫。
分布式數據庫
網易早在2006年就開始了分布式數據庫(DDB)的研究工作,經過10年的發展和演變,DDB的產品形態已全面趨于成熟,功能和性能得到了眾多產品的充分驗證。
圖7是DDB的完整架構,由cloudadmin、LVS、DDB Proxy、SysDB及數據節點組成。
分布式執行計劃
分布式執行計劃定義了SQL在分庫分表環境中各個數據庫節點上執行的方法、順序和合并規則,是DDB實現中為復雜的一環。如SQL:select * from user order by id limit 10 offset 10。
這個SQL要查詢ID排名在10~20之間的user信息,這里涉及全局ID排序和全局LIMIT OFFSET兩個合并操作。對全局ID排序,DDB的做法是將ID排序下發給各個數據庫節點,在DBI層再進行一層歸并排序,這樣可以充分利用數據庫節點的計算資源,同時將中間件層的排序復雜度降到低,例如一些需要用到臨時文件的排序場景,如果在中間件做全排序會導致極大的開銷。
對全局LIMIT OFFSET,DDB的做法是將OFFSET累加到LIMIT中下發,因為單個數據節點中的OFFSET沒有意義,且會造成錯誤的數據偏移,只有在中間件層的全局OFFSET才能保證OFFSET的準確性。
所以后下發給各個DBN的SQL變為:select * from user order by id limit 20。又如SQL:select avg(age) from UserTet group by name可以通過EXPLAIN語法得到SQL的執行計劃,如圖8所示。
上述SQL包含GROUP BY分組和AVG聚合兩種合并操作,與全局ORDER BY類似,GROUP BY也可以下發給數據節點、中間件層做一個歸并去重,但是前提要將GROUP BY的字段同時作為ORDER BY字段下發,因為歸并的前提是排序。對AVG聚合,不能直接下發,因為得到所有數據節點各自的平均值,不能求出全局平均值,需要在DBI層把AVG轉化為SUM和COUNT再下發,在結果集合并時再求平均。
DDB執行計劃的代價取決于DBI中的排序、過濾和連接,在大部分場景下,排序可以將ORDER BY下發簡化為一次性歸并排序,這種情況下代價較小,但是對GROUP BY和ORDER BY同時存在的場景,需要優先下發GROUP BY字段的排序,以達到歸并分組的目的,這種情況下,就需要將所有元素做一次全排序,除非GROUP BY和ORDER BY字段相同。
DDB的連接運算有兩種實現,種是將連接直接下發,若連接的兩張表數據分布完全相同,并且在分區字段上連接,則滿足連接直接下發的條件,因為在不同數據節點的分區字段必然沒有相同值,不會出現跨庫連接的問題。第二種是在不滿足連接下發條件時,會在DBI內部執行Nest Loop算法,驅動表的順序與FROM表排列次序一致,此時若出現ORDER BY表次序與表排列次序不一致,則不滿足ORDER BY下發條件,也需要在DBI內做一次全排序。
分庫分表的執行計劃代價相比單機數據庫而言,更加難以掌控,即便是相同的SQL模式,在不同的數據分布和分區字段使用方式上,也存在很大的性能差距,DDB的使用要求開發者和DBA對執行計劃的原理具有一定認識。
分庫分表在分區字段的使用上很有講究,一般建議應用中80%以上的SQL查詢通過分區字段過濾,使SQL可以單庫執行。對于那些沒有走分區字段的查詢,需要在所有數據節點中并行下發,這對線程和CPU資源是一種極大的消耗,伴隨著數據節點的擴展,這種消耗會越來越劇烈。另外,基于分區字段跨庫不重合的原理,在分區字段上的分組、聚合、DISTINCT、連接等操作,都可以直接下發,這樣對中間件的代價往往小。
分布式事務
分布式事務是個歷久彌新的話題,分庫分表、分布式事務的目的是保障分庫數據一致性,而跨庫事務會遇到各種不可控制的問題,如個別節點永久性宕機,像單機事務一樣的ACID是無法奢望的。另外,業界的CAP理論也告訴我們,對分布式系統,需要將數據一致性和系統可用性、分區容忍性放在天平上一起考慮。
兩階段提交協議(簡稱2PC)是實現分布式事務較為經典的方案,適用于中間件這種數據節點無耦合的場景。2PC的核心原理是通過提交分階段和記日志的方式,記錄下事務提交所處的階段狀態,在組件宕機重啟后,可通過日志恢復事務提交的階段狀態,并在這個狀態節點重試,如Coordinator重啟后,通過日志可以確定提交處于Prepare還是PrepareAll狀態,若是前者,說明有節點可能沒有Prepare成功,或所有節點Prepare成功但還沒有下發Commit,狀態恢復后給所有節點下發RollBack;若是PrepareAll狀態,需要給所有節點下發Commit,數據庫節點需要保證Commit冪等。與很多其他一致性協議相同,2PC保障的是終一致性。
2PC整個過程如圖9所示。
在網易DDB中,DBI和Proxy組件都作為Coordinator存在,2PC實現時,記錄Prepare和PrepareAll的日志必須sync,以保障重啟后恢復狀態正確,而Coordinator后的Commit日志主要作用是回收之前日志,可異步執行。
由于2PC要求Coordinator記日志,事務吞吐率受到磁盤I/O性能的約束,為此DDB實現了GROUP I/O優化,可極大程度提升2PC的吞吐率。2PC本質上說是一種阻塞式協議,兩階段提交過程需要大量線程資源,因此CPU和磁盤都有額外消耗,與單機事務相比,2PC在響應時間和吞吐率上相差很多,從CAP角度出發,可以認為2PC在一定程度上成全了C,犧牲了A。
另外,目前MySQL流行的5.5和5.6版本中,XA事務日志無法復制到從節點,這意味著主庫一旦宕機,切換到從庫后,XA的狀態會丟失,可能造成數據不一致,MySQL 5.7版本在這方面已經有所改善。
雖然2PC有諸多不足,我們依然認為它在DDB中有實現價值,DDB作為中間件,其迭代周期要比數據庫這種底層服務頻繁,若沒有2PC,一次更新或重啟就可能造成應用數據不一致。從應用角度看,分布式事務的現實場景常常無法規避,在有能力給出其他解決方案前,2PC也是一個不錯的選擇。
對購物轉賬等電商和金融業務,中間件層的2PC大問題在于業務不可見,一旦出現不可抗力或意想不到的一致性破壞,如數據節點永久性宕機,業務難以根據2PC的日志進行補償。金融場景下,數據一致性是命根,業務需要對數據有百分之百的掌控力,建議使用TCC這類分布式事務模型,或基于消息隊列的柔性事務框架,請參考第5章,這兩種方案都在業務層實現,業務開發者具有足夠掌控力,可以結合SOA框架來架構。原理上說,這兩種方案都是大事務拆小事務,小事務變本地事務,后通過冪等的Retry來保障終一致性。
彈性擴容
分庫分表數據庫中,在線數據遷移也是核心需求,會用在以下兩種場景中。
無論是彈性擴容,還是表重分布,都可當作DDB以表或庫為單位的一次完整在線數據遷移。該過程分為全量遷移和增量遷移兩個階段,全量遷移是將原庫或原表中需要遷移的數據DUMP出來,并使用工具按照分區策略導入到新庫新表中。增量遷移是要將全量遷移過程中產生的增量數據更新按照分區策略應用到新庫新表。
全量遷移的方案相對簡單,使用DDB自帶工具按照特定分區策略DUMP和Load即可。對增量遷移,DDB實現了一套獨立的遷移工具Hamal來訂閱各個數據節點的增量更新,Hamal內部又依賴DBI模塊將增量更新應用到新庫新表,如圖10所示。
Hamal作為獨立服務,與Proxy一樣由DDB統一配置和管理,每個Hamal進程負責一個數據節點的增量遷移,啟動時模擬Slave向原庫拉取Binlog存儲本地,之后實時通過DBI模塊應用到新庫新表,除了基本的遷移功能外,Hamal具備以下兩個特性。
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。