所謂高并發,指的是同一時間可以處理大量的WEB請求,這個指標用來衡量一個架構的體量和性能。這里的大量如何評估呢?1000算不算?10000算不算?
對于中小型的站點來說,可能并發100多就很不錯了,但對于像淘寶這樣的大型站點,單憑一個接口調用的量就有可能達到百萬的并發。在雙11這樣的大型活動場景里,淘寶的并發請求數都能達到上億次,這樣的體量無論是在國內還是在國際都是排在前列的。而本章節要講述的內容是如何設計一個可以承載巨量并發請求的架構。
要想設計一個高并發的架構,首先要搞清楚架構的分層,因為每一個層面都有可能造成影響高并發的瓶頸點。找到瓶頸點后,只要把瓶頸點解除,自然會提升整個架構的并發處理能力。我們先來看一個綜合分層的架構圖:
對于大型網站來說增加CDN這一層是非常有必要的,CDN(Content Delivery Network,內容分發網絡),它屬于網絡范疇的一個技術,它依靠部署在各個區域的邊緣服務器,實現負載均衡、內容分發調度等功能。它使得用戶就近獲取內容,降低網絡堵塞,提供用戶訪問響應速度。
來舉一個通俗點的例子:小明公司做了了一個針對全國用戶的業務,服務器放在了北京,但是深圳用戶在訪問網站的時候非常卡頓,有時候甚至訪問不到。經排查,造成該問題的原因是深圳用戶所在網絡到北京的機房延遲非常大。小明想到了一個辦法,他在深圳的某機房假設了一臺服務器,把北京服務器上的文件傳輸到深圳的服務器上,當深圳用戶訪問網站時,讓該用戶直接去訪問深圳的服務器,而不是訪問北京的服務器。同理,其他城市也效仿深圳假設了類似的服務器,這樣全國各地的用戶訪問公司業務都很順暢了。
例子中的解決方案其實就是CDN實現原理,當然,真正的CDN技術要復雜得多,要考慮很多問題,比如邊緣服務器的分布、機房的網絡、帶寬、服務器的存儲、智能DNS解析、邊緣服務器到真實服務器之間的網絡優化、靜態和動態資源的區分、緩存的優化、壓縮、SSL等等問題。關于這些細節技術我不做過多解釋,但希望大家能通過我的描述了解CDN在架構中存在的意義。
CDN是處于整個架構體系中最前端的一層,它是直接面對用戶的,CDN會把靜態的請求(圖片、js、css等)直接消化掉,然后把動態的請求往后傳遞。實際上,一個網站(比如,淘寶網)超過80%的請求都是靜態的請求,那也就意味如果前端架設了CDN,即使并發1億,也只有2000萬到了后端的WEB上。那么你可能會問,CDN能支持8000萬的并發嗎?這個主要取決于CDN廠商的實力,如果他們搞10000個節點(即邊緣服務器),每個節點上消化8000并發,如果搞10萬個節點,每個節點只需要消
化800個并發而已。然而,一臺普通的Nginx服務器(配置2核CPU4G內存)輕松處理5萬個并發(前提是做過優化,并且處理的請求是靜態請求、或者只是轉發請求)。
這一層,其實就是一個反向代理(或者叫做分發器),它的主要作用是把用戶的請求按照預設的算法發送給它后面的WEB服務器。該層在實現上大致分為兩類:四層和七層(網絡OSI七層模型),Nginx的負載均衡就屬于七層,而LVS屬于四層。從吞吐量上來分析,四層的負載均衡更有優勢。
所以,要想實現高并發,負載均衡這一層必須要使用四層技術,其中LVS就是一款不錯的開源負載均衡軟件。LVS有三種實現模式:
在這種模式下,負載均衡器上有設置iptables nat表的規則,實現了把用戶的請求數據包轉發到后端的Real Server(即WEB
Server)上,而且還要把WEB Server的響應數據傳遞給用戶,這樣負載均衡器很容易成為一個瓶頸,當并發量很大時,一定會
影響整個架構的性能。
LVS的DR模式和NAT不一樣,負載均衡器只需要分發用戶的請求,而WEB Server的返回數據并不通過負載均衡器傳遞,數據直
接由WEB Server自己處理。這樣就解決了NAT模式的瓶頸問題。但是,DR模式有一個要求:負載均衡器和WEB Server必須在同一個內部網絡(要求在相同的廣播域內),這是因為DR模式下,數據包的目的MAC地址被修改為了WEB Server的MAC地址。
LVS的IP Tunnel模式和DR模式類似,負載均衡器只需要分發用戶的請求,而WEB Server的返回數據并不通過負載均衡器傳遞,數據直接由WEB Server自己處理。這樣就解決了NAT模式的瓶頸問題。和DR模式不同的是,IP Tunnel模式不需要保證分發器和Real Server在同一個網絡環境,因為這種模式下,它會把數據包的目的IP地址更改為Real Server的IP地址。這種模式,可以實現跨機房、跨地域的負載均衡。
對于以上三種模式來說,IP Tunnl模式更適合用在高并發的場景下。但有一點需要注意,這臺作為負載均衡器的服務器無論是自身的網卡性能,還是它所處的網絡環境里的網絡設備都有很高的要求。
可能你會有疑問,這臺負載均衡器終究只是一個入口,一臺機器頂多支撐10萬并發,對于1000萬、2000萬的并發怎么實現?答案是:疊加!一臺10萬,100臺就是1000萬,200臺就是2000萬……
還有個問題,如何讓一個域名(如,www.google.com)訪問這200臺負載均衡器?請思考一下上一小節的CDN技術,它就可以讓一個域名指向到成千上萬的邊緣服務器上。沒錯,智能DNS解析可以把全國甚至世界各地的請求智能地解析到最優的邊緣服務器上。當然,DNS也可以不用智能,大不了直接添加幾百條A記錄唄,最終也會把用戶的請求均衡地分發到這幾百個節點上。
如果最前端使用了CDN,那么在WEB這一層處理的請求絕大多數為動態的請求。什么是動態的請求?除了靜態的就是動態的,那什么是靜態的?前面提到過的圖片、js、css、html、音頻、視頻等等都屬于靜態資源,當然另外還有太多,大家可以參考第一篇文章《HTTP掃盲》的MIME Type。
再來說這個動態,你可以這樣理解:凡是涉及到數據庫存取操作的請求都屬于動態請求。比如,一個網站需要注冊用戶才可以正常訪問里面的內容,你注冊的用戶信息(用戶名、密碼、手機號、郵箱等)存入到了數據庫里,每次你登錄該網站,都需要到數據庫里查詢用戶名和密碼,來驗證你輸入的是否是正確的。
如果到了WEB這一層全都是動態的請求的話,那么并發量的多少主要取決于WEB層后端的DB層或者Cache層。也就是說要想提升WEB層服務器的并發性能,必須首先要提升DB層或者Cache層的并發性能。
我們不妨來一個假設:要求架構能支撐1000萬并發(動態),假設單臺WEB Server支撐1000并發,則需要1萬臺服務器。實際生產環境中,單臺機器支撐1000并發已經非常厲害啦,至少在我的運維生涯里,單臺WEB Server最大動態并發量并沒有達到過這么大。
我提供一組數據,你自然就可以估算出并發量。在這我拿PHP的應用舉例:一個PHP的網站,單個PHP-FPM進程耗費內存在2M-20M(假設耗費內存10M),1000個并發也就意味著同時有1000個PHP-FPM進程,耗費內存為1000*10M=10G,再加上留給系統1G內存,所以1000并發至少需要11G內存。
按照上面的估算,2000并發則需要21G內存,10000并發則需要101G內存,這個僅僅是理論值。實際上,并發量不僅跟內存有關系,跟CPU同樣也有關系。如果服務器有4核CPU,則理論上僅僅支持4個進程同時占用CPU計算,也就是說僅能支持4個并發。當然,CPU計算那么快,進程會來回切換排隊占用CPU,這樣能夠實現即使只有4核CPU,依然能夠支持幾百甚至上千的并發。但無論如何,CPU的核數越大,該服務器能支撐的并發也就越大。
對于高并發的架構,WEB Server必然會做負載均衡集群,單臺WEB Server的配置通常會選擇4核8G這樣的配置(這個配置,最好是根據自己業務的特性選擇合適的,畢竟現在大多企業都使用公有云或者私有云,服務器的配置可以定制),然后由這樣的機器來組成一個大型集群,最終實現高并發的需求。
增加這一層的目的是為了減輕DB層的壓力,Cache層有一個特點:數據的讀寫發生在內存里,跟磁盤并沒有關系。正是因為這個特點,保證了數據的讀寫速度非常快。假如沒有Cache層,并發1000萬的動態請求意味著這1000萬會直接透傳到DB層(如MySQL),1000萬的并發就會造成1000萬對磁盤的讀寫操作。我想大家都明白,磁盤的讀寫速度遠遠低于內存的讀寫速度,要想支撐1000萬并發讀寫是不現實的。
當然,Cache層主要針對讀操作,而且它僅僅是緩存一部分DB層的熱數據(頻繁讀取的那部分數據)。舉一個下例子:有一次公司的某個業務臨時做了一個推廣活動,結果導致訪問量暴漲10倍,原本的服務器架構并不能支撐這么大的量,結果可想而知。當時,我們的解決方案是:把查詢量非常大的數據緩存到Memcached里面,然后在沒有增加硬件的情況下順利抗了過去。可見這一
個Cache層所起到的作用是多么關鍵。
可以作為Cache的角色通常是NoSQL,如Memcached、Redis等。在第3章《常見WEB集群架構》中我曾提到過Memcached,架構圖如下:
作為Cache角色時,Redis和Memcached用法基本一致。其實,拋開這個Cache角色,NoSQL也可以獨立作為DB層,這主要取決于業務邏輯是否支持拿NoSQL作為數據存儲引擎,畢竟NoSQL的數據結構和關系型數據庫比還是比較簡單的,有些復雜場景無法實現。但為了實現高并發,我們可以嘗試同時使用傳統的關系型數據庫和NoSQL數據庫存儲數據。
既然Memcached和Redis都可以作為Cache角色,那么到底用哪一個可以支撐更大的并發量呢?其實這兩者各有千秋,不能盲目地下結論說哪個更快或者更好。得根據你的業務選擇適合的服務。由于Redis屬于單線程,故只能使用單核,而Memcached屬于多線程的,從而可以使用多核,所以在比較上,平均每一個核上Redis在存儲小數據時比Memcached性能更高。而在100k以
上的數據中,Memcached性能要高于Redis,雖然Redis最近也在存儲大數據的性能上進行優化,但是比起Memcached,還是稍有遜色。如果不考慮存儲數據大小,肯定Memcached性能更好,畢竟它是多線程的。
另外你需要了解,Memcached的數據只能存內存,不能存到磁盤,而Redis支持把內存的數據鏡像一份到磁盤上,而且還可以記錄日志(通過這個日志來獲取數據)。Memcached只能存簡單的K-V格式的數據,而Redis支持更多的數據類型(如,list、hash)。
無論你用哪一種作為Cache,我們都需要為其做一個高可用負載均衡集群,這樣才可以滿足高并發的需求。
可以說DB層是整個架構體系中非常關鍵的一層,也是瓶頸所在。原因無他,只因它涉及到對磁盤的讀寫。所以,為了提升性能,對服務器磁盤要求很高,至少是15000r/m的SAS硬盤而且需要做RAID10,如果選擇SSD盤更優。
最簡單暴力提升并發數量的辦法是服務器的堆積(即,做負載均衡集群),但DB層跟WEB層不一樣,它涉及到數據存儲到磁盤
里,服務器可以累加,但是磁盤在累加的同時,如何保證所有的服務器能讀寫同一份數據?這是一個很大的問題,所以單純的服務器堆積只適合小規模的業務,對于并發上千萬的業務并不適用。并發量大的站點,意味著數據量也是非常大的(如,TB級別),如果單個數據庫上TB,那一定是一個災難,無論是讀寫還是備份都將是極大的問題。
那如何解決這個問題呢?既然大了不行,那就將大的庫切割成小的庫即可。你可不要把這個切割單純地想象成切割文件。我們可以從兩個維度來實現切割。
大型網站為了應對日益復雜的業務場景,通過使用分而治之的手段將整個網站業務分成不同的產品線,如大型購物網站就會將首頁、商鋪、訂單、買家、賣家、倉儲、物流、售后服務等拆分成不同的產品項,這樣數據庫自然也拆分為了多個數據庫,原來TB級的數據,變成了GB級。如果覺得還不夠細化,我們可以繼續把商鋪進一步拆分,比如個人類的、企業類的、明星類的、普通類的等等。總之,你可以根據業務特性想到幾百種拆分方法,最終一塊大蛋糕變成了幾十甚至幾百塊小蛋糕,吃起來就簡單多了。
業務拆分是產品經理設計的,但是這個分庫分表只能是DBA操刀。如果一個幾千GB的大庫讀寫很慢的話,但分成1000個幾GB的小庫后,讀寫速度一定是有質的飛躍。同理,表也是可以像庫那樣劃分的。分庫分表需要借助數據庫中間件來完成。比如MySQL分庫分表比較好的中間件MyCAT就不錯。
有了以上兩個拆分原則,無論多大的庫,我們都可以劃分為比較小的庫,這樣即使使用傳統的架構依然可以輕松應付。最終的DB層架構就成了蜂窩狀的一組一組的小單元,每一個單元獨立做高可用以及負載均衡集群。
一個大型的網站,一定少不了消息隊列這一層。在前面第3章《常見WEB集群架構》一文中就提到過它,它主要解決的問題是:解耦合、異步處理、流量削峰等。以下三個應用場景曾在第3章出現過,也許你現在看會有更深層次的體會。
用戶上傳圖片到服務器,要求人臉識別系統識別該上傳圖片,傳統的做法是:用戶上傳圖片 → 服務接收到圖片開始識別圖片 → 系統判斷圖片是否合法 → 反饋給用戶是否成功。這個要涉及兩個系統:
而使用消息隊列,流程會變成這樣:
用戶上傳圖片后,圖片上傳系統將圖片信息寫入消息隊列,直接返回成功;而人臉識別系統則定時從消息隊列中取數據,完成對新增圖片的識別。
此時圖片上傳系統并不需要關心人臉識別系統是否對這些圖片信息的處理、以及何時對這些圖片信息進行處理。事實上,由于用戶并不需要立即知道人臉識別結果,人臉識別系統可以選擇不同的調度策略,按照閑時、忙時、正常時間,對隊列中的圖片信息進行處理。
用戶到一個網站注冊賬號,系統需要發送注冊郵件并驗證短信。傳統的處理流程如下:
這種方式下,需要最終發送短信驗證后再返回給客戶端。
另外一種方式就是異步處理,即注冊郵件和短信同時發送,流程如下:
當用戶填寫完注冊信息并成功寫入消息隊列后,就可以反回成功的信息給客戶端,從而客戶端不需要再等待系統發郵件和發短信,不僅客戶端不用等,而且處理客戶端請求的那個工作進程也不需要等(這個特性非常重要,它是實現高并發的一個重要手段),這個就是異步處理的優勢。
很典型的應用就是購物網站秒殺活動,一般由于瞬時訪問量過大,服務器接收過大,會導致流量暴增,相關系統無法處理請求甚至崩潰。而加入消息隊列后,系統可以從消息隊列中取數據,相當于消息隊列做了一次緩沖。
該方法可以讓請求先入消息隊列,而不是由業務處理系統直接處理,極大地減少了業務處理系統的壓力。另外隊列長度可以做限制,比如隊列長度為100,則該系統只能有100人可以秒殺到商品,排在100名后的用戶無法秒殺到商品,而返回活動已結束或商品已售完的信息。
總之,消息隊列的引入極大提升了整個架構的并發能力。從WEB層接收到動態的請求后,Cache層過濾掉一部分,然后請求逐一地發送到DB層,在這個過程中,查詢時間很長的請求可以單獨摘出來,把它搞到消息隊列里,這樣WEB層和DB層只處理那種快速有結果的查詢,并發量自然很大。而消息隊列會慢慢消化掉這些特殊的查詢,或許你有疑問,這種查詢慢的請求也很多怎么
辦?不也同樣影響到并發量嗎?畢竟最終的查詢到了DB層。不要忘記消息隊列本身就有削峰的能力,如果有大量的這種查詢,那么就讓它們排好隊列,慢慢消化,總之不讓它們影響到DB層的正常查詢。
可以提供消息隊列的服務有那么多(RabitMQ、ActiveMQ、Kafka、ZeroMQ、MetaMQ、Beanstalk、Redis等等),到底選擇哪一種?最好是讓研發同事來定吧,只有研發團隊最了解自己代碼的邏輯架構,適合自己的才是最好的。事實上,無論你用哪一種消息隊列服務,它都不會成為整個架構的瓶頸點。當然,你最好做一個分布式的集群,這樣能夠保證它的橫向擴容或者縮容。
關于存儲,目前的解決方案我歸類為以下幾種:
就是服務器自身的磁盤,對于像DB層這樣關鍵的角色,我們通常會用高性能磁盤做RAID10。特點:方便維護、穩定、性能非常好、容量有限、擴容不方便。
主要有三類:NAS、SAN、DAS。
NAS:類似Linux系統做的NFS服務,它是建立在操作系統層面上的一種共享存儲解決方案,它是一種商業產品。NAS比較適合小規模網站。特點:容量大、擴容不方便、吞吐量一般(受網絡環境影響)、穩定性好、成本高。
SAN:也是一種商業的共享存儲解決方案,支持普通網絡或者光纖接入,比NAS更加底層。特點:容量大、擴容不方便、性能好(比NAS強很多)、穩定性好、成本高昂。
DAS:磁盤陣列,支持RAID,商業的存儲。特點:容量大、擴容不方便、不支持共享、性能好、穩定性好。
隨著云計算、大數據技術的日益流行,分布式共享存儲技術越來越成熟,無論是商業的還是開源的都有不少優秀的解決方案。比如,開源的有HDFS、FastDFS、MFS、GlusterFS、Ceph、Swift等。這類存儲有一些共同特點:方便擴容、容量可以無限大、性能一般(網絡會成為瓶頸)、成本低、穩定性好。
本節的存儲層我也歸類為三大類:WEB層面的存儲(比如存儲代碼、圖片、js、css、視頻、音頻等靜態文件)、日志、DB層面的存儲(即數據庫的數據存儲)。
這三類存儲,最要命的是DB層的存儲,前面我也提到過DB層很關鍵,而決定DB層性能的因素中這個數據存儲(磁盤)性能起到決定性作用。解決方案我也提到了,就是“大變小,一變多,自己管自己”。正常情況下巨量的數據庫必然會使用大容量存儲設備,這樣最終的結果是—慢!所以,我們需要分模塊、分庫分表,最終單臺機器的本地磁盤就可以支撐這些巨量的數據,讀寫速度不會被網絡等因素影響。
日志類和WEB層靜態文件的存儲可以選擇分布式共享存儲解決方案,因為這類的存儲不需要太高的吞吐量,它們所占用的空間比較大,而且會越來越大。
當你看完以上內容后,可能你的心中還是沒有一個完整的答案,所以這個總結很關鍵!
1)高并發網站一定會使用CDN,而且需要把靜態文件存儲在邊緣服務器上。
2)負載均衡一定要使用四層的,比如LVS,如果是LVS,選擇IP Tunnel模式。
3)WEB層把靜態的請求交給CDN處理,所以只處理動態的請求,要支持橫向擴容,可以方便地通過加機器來增加并發量。
4)增加Cache層,把熱數據搞到這一層,減少對DB層地壓力。對這一層做分布式集群架構設計,方便擴容。
5)增加消息隊列,實現解耦合、流量削峰,從而提升整個架構地并發能力。
6)DB層要通過拆分業務、分庫分表來實現大變小、一變多,對單獨模塊做高可用負載均衡集群,從而提升并發能力。
7)DB層的存儲使用本地磁盤,日志類、靜態文件類使用分布式文件存儲。