歡迎大家前往騰訊雲社區,獲取更多騰訊海量技術實踐干貨哦~
作者:韓偉
-
承載量是分布式系統存在的原因
當一個互聯網業務獲得大眾歡迎的時候,最顯著碰到的技術問題,就是服務器非常繁忙。當每天有1000萬個用戶訪問你的網站時,無論你使用什么樣的服務器硬件,都不可能只用一台機器就承載的了。因此,在互聯網程序員解決服務器端問題的時候,必須要考慮如何使用多台服務器,為同一種互聯網應用提供服務,這就是所謂“分布式系統”的來源。
然而,大量用戶訪問同一個互聯網業務,所造成的問題並不簡單。從表面上看,要能滿足很多用戶來自互聯網的請求,最基本的需求就是所謂性能需求:用戶反應網頁打開很慢,或者網游中的動作很卡等等。而這些對於“服務速度”的要求,實際上包含的部分卻是以下幾個:高吞吐、高並發、低延遲和負載均衡。
高吞吐,意味着你的系統,可以同時承載大量的用戶使用。這里關注的整個系統能同時服務的用戶數。這個吞吐量肯定是不可能用單台服務器解決的,因此需要多台服務器協作,才能達到所需要的吞吐量。而在多台服務器的協作中,如何才能有效的利用這些服務器,不致於其中某一部分服務器成為瓶頸,從而影響整個系統的處理能力,這就是一個分布式系統,在架構上需要仔細權衡的問題。
高並發是高吞吐的一個延伸需求。當我們在承載海量用戶的時候,我們當然希望每個服務器都能盡其所能的工作,而不要出現無謂的消耗和等待的情況。然而,軟件系統並不是簡單的設計,就能對同時處理多個任務,做到“盡量多”的處理。很多時候,我們的程序會因為要選擇處理哪個任務,而導致額外的消耗。這也是分布式系統解決的問題。
低延遲對於人數稀少的服務來說不算什么問題。然而,如果我們需要在大量用戶訪問的時候,也能很快的返回計算結果,這就要困難的多。因為除了大量用戶訪問可能造成請求在排隊外,還有可能因為排隊的長度太長,導致內存耗盡、帶寬占滿等空間性的問題。如果因為排隊失敗而采取重試的策略,則整個延遲會變的更高。所以分布式系統會采用很多請求分揀和分發的做法,盡快的讓更多的服務器來出來用戶的請求。但是,由於一個數量龐大的分布式系統,必然需要把用戶的請求經過多次的分發,整個延遲可能會因為這些分發和轉交的操作,變得更高,所以分布式系統除了分發請求外,還要盡量想辦法減少分發的層次數,以便讓請求能盡快的得到處理。
由於互聯網業務的用戶來自全世界,因此在物理空間上可能來自各種不同延遲的網絡和線路,在時間上也可能來自不同的時區,所以要有效的應對這種用戶來源的復雜性,就需要把多個服務器部署在不同的空間來提供服務。同時,我們也需要讓同時發生的請求,有效的讓多個不同服務器承載。所謂的負載均衡,就是分布式系統與生俱來需要完成的功課。
由於分布式系統,幾乎是解決互聯網業務承載量問題,的最基本方法,所以作為一個服務器端程序員,掌握分布式系統技術就變得異常重要了。然而,分布式系統的問題,並非是學會用幾個框架和使用幾個庫,就能輕易解決的,因為當一個程序在一個電腦上運行,變成了又無數個電腦上同時協同運行,在開發、運維上都會帶來很大的差別。
分布式系統提高承載量的基本手段
分層模型(路由、代理)
使用多態服務器來協同完成計算任務,最簡單的思路就是,讓每個服務器都能完成全部的請求,然后把請求隨機的發給任何一個服務器處理。最早期的互聯網應用中,DNS輪詢就是這樣的做法:當用戶輸入一個域名試圖訪問某個網站,這個域名會被解釋成多個IP地址中的一個,隨后這個網站的訪問請求,就被發往對應IP的服務器了,這樣多個服務器(多個IP地址)就能一起解決處理大量的用戶請求。
然而,單純的請求隨機轉發,並不能解決一切問題。比如我們很多互聯網業務,都是需要用戶登錄的。在登錄某一個服務器后,用戶會發起多個請求,如果我們把這些請求隨機的轉發到不同的服務器上,那么用戶登錄的狀態就會丟失,造成一些請求處理失敗。簡單的依靠一層服務轉發是不夠的,所以我們會增加一批服務器,這些服務器會根據用戶的Cookie,或者用戶的登錄憑據,來再次轉發給后面具體處理業務的服務器。
除了登錄的需求外,我們還發現,很多數據是需要數據庫來處理的,而我們的這些數據往往都只能集中到一個數據庫中,否則在查詢的時候就會丟失其他服務器上存放的數據結果。所以往往我們還會把數據庫單獨出來成為一批專用的服務器。
至此,我們就會發現,一個典型的三層結構出現了:接入、邏輯、存儲。然而,這種三層結果,並不就能包醫百病。例如,當我們需要讓用戶在線互動(網游就是典型) ,那么分割在不同邏輯服務器上的在線狀態數據,是無法知道對方的,這樣我們就需要專門做一個類似互動服務器的專門系統,讓用戶登錄的時候,也同時記錄一份數據到它那里,表明某個用戶登錄在某個服務器上,而所有的互動操作,要先經過這個互動服務器,才能正確的把消息轉發到目標用戶的服務器上。
又例如,當我們在使用網上論壇(BBS)系統的時候,我們發的文章,不可能只寫入一個數據庫里,因為太多人的閱讀請求會拖死這個數據庫。我們常常會按論壇板塊來寫入不同的數據庫,又或者是同時寫入多個數據庫。這樣把文章數據分別存放到不同的服務器上,才能應對大量的操作請求。然而,用戶在讀取文章的時候,就需要有一個專門的程序,去查找具體文章在哪一個服務器上,這時候我們就要架設一個專門的代理層,把所有的文章請求先轉交給它,由它按照我們預設的存儲計划,去找對應的數據庫獲取數據。
根據上面的例子來看,分布式系統雖然具有三層典型的結構,但是實際上往往不止有三層,而是根據業務需求,會設計成多個層次的。為了把請求轉交給正確的進程處理,我們而設計很多專門用於轉發請求的進程和服務器。這些進程我們常常以Proxy或者Router來命名,一個多層結構常常會具備各種各樣的Proxy進程。這些代理進程,很多時候都是通過TCP來連接前后兩端。然而,TCP雖然簡單,但是卻會有故障后不容易恢復的問題。而且TCP的網絡編程,也是有點復雜的。——所以,人們設計出更好進程間通訊機制:消息隊列。
盡管通過各種Proxy或者Router進程能組建出強大的分布式系統,但是其管理的復雜性也是非常高的。所以人們在分層模式的基礎上,想出了更多的方法,來讓這種分層模式的程序變得更簡單高效的方法。
並發模型(多線程、異步)
當我們在編寫服務器端程序是,我們會明確的知道,大部分的程序,都是會處理同時到達的多個請求的。因此我們不能好像HelloWorld那么簡單的,從一個簡單的輸入計算出輸出來。因為我們會同時獲得很多個輸入,需要返回很多個輸出。在這些處理的過程中,往往我們還會碰到需要“等待”或“阻塞”的情況,比如我們的程序要等待數據庫處理結果,等待向另外一個進程請求結果等等……如果我們把請求一個挨着一個的處理,那么這些空閑的等待時間將白白浪費,造成用戶的響應延時增加,以及整體系統的吞吐量極度下降。
所以在如何同時處理多個請求的問題上,業界有2個典型的方案。一種是多線程,一種是異步。在早期的系統中,多線程或多進程是最常用的技術。這種技術的代碼編寫起來比較簡單,因為每個線程中的代碼都肯定是按先后順序執行的。但是由於同時運行着多個線程,所以你無法保障多個線程之間的代碼的先后順序。這對於需要處理同一個數據的邏輯來說,是一個非常嚴重的問題,最簡單的例子就是顯示某個新聞的閱讀量。兩個++操作同時運行,有可能結果只加了1,而不是2。所以多線程下,我們常常要加很多數據的鎖,而這些鎖又反過來可能導致線程的死鎖。
因此異步回調模型在隨后比多線程更加流行,除了多線程的死鎖問題外,異步還能解決多線程下,線程反復切換導致不必要的開銷的問題:每個線程都需要一個獨立的棧空間,在多線程並行運行的時候,這些棧的數據可能需要來回的拷貝,這額外消耗了CPU。同時由於每個線程都需要占用棧空間,所以在大量線程存在的時候,內存的消耗也是巨大的。而異步回調模型則能很好的解決這些問題,不過異步回調更像是“手工版”的並行處理,需要開發者自己去實現如何“並行”的問題。
異步回調基於非阻塞的I/O操作(網絡和文件),這樣我們就不用在調用讀寫函數的時候“卡”在那一句函數調用,而是立刻返回“有無數據”的結果。而Linux的epoll技術,則利用底層內核的機制,讓我們可以快速的“查找”到有數據可以讀寫的連接\文件。由於每個操作都是非阻塞的,所以我們的程序可以只用一個進程,就處理大量並發的請求。因為只有一個進程,所以所有的數據處理,其順序都是固定的,不可能出現多線程中,兩個函數的語句交錯執行的情況,因此也不需要各種“鎖”。從這個角度看,異步非阻塞的技術,是大大簡化了開發的過程。由於只有一個線程,也不需要有線程切換之類的開銷,所以異步非阻塞成為很多對吞吐量、並發有較高要求的系統首選。
int epoll_create(int size);//創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
緩沖技術
在互聯網服務中,大部分的用戶交互,都是需要立刻返回結果的,所以對於延遲有一定的要求。而類似網絡游戲之類服務,延遲更是要求縮短到幾十毫秒以內。所以為了降低延遲,緩沖是互聯網服務中最常見的技術之一。
早期的WEB系統中,如果每個HTTP請求的處理,都去數據庫(MySQL)讀寫一次,那么數據庫很快就會因為連接數占滿而停止響應。因為一般的數據庫,支持的連接數都只有幾百,而WEB的應用的並發請求,輕松能到幾千。這也是很多設計不良的網站人一多就卡死的最直接原因。為了盡量減少對數據庫的連接和訪問,人們設計了很多緩沖系統——把從數據庫中查詢的結果存放到更快的設施上,如果沒有相關聯的修改,就直接從這里讀。
最典型的WEB應用緩沖系統是Memcache。由於PHP本身的線程結構,是不帶狀態的。早期PHP本身甚至連操作“堆”內存的方法都沒有,所以那些持久的狀態,就一定要存放到另外一個進程里。而Memcache就是一個簡單可靠的存放臨時狀態的開源軟件。很多PHP應用現在的處理邏輯,都是先從數據庫讀取數據,然后寫入Memcache;當下次請求來的時候,先嘗試從Memcache里面讀取數據,這樣就有可能大大減少對數據庫的訪問。
然而Memcache本身是一個獨立的服務器進程,這個進程自身並不帶特別的集群功能。也就是說這些Memcache進程,並不能直接組建成一個統一的集群。如果一個Memcache不夠用,我們就要手工用代碼去分配,哪些數據應該去哪個Memcache進程。——這對於真正的大型分布式網站來說,管理一個這樣的緩沖系統,是一個很繁瑣的工作。
因此人們開始考慮設計一些更高效的緩沖系統:從性能上來說,Memcache的每筆請求,都要經過網絡傳輸,才能去拉取內存中的數據。這無疑是有一點浪費的,因為請求者本身的內存,也是可以存放數據的。——這就是促成了很多利用請求方內存的緩沖算法和技術,其中最簡單的就是使用LRU算法,把數據放在一個哈希表結構的堆內存中。
而Memcache的不具備集群功能,也是一個用戶的痛點。於是很多人開始設計,如何讓數據緩存分不到不同的機器上。最簡單的思路是所謂讀寫分離,也就是緩存每次寫,都寫到多個緩沖進程上記錄,而讀則可以隨機讀任何一個進程。在業務數據有明顯的讀寫不平衡差距上,效果是非常好的。
然而,並不是所有的業務都能簡單的用讀寫分離來解決問題,比如一些在線互動的互聯網業務,比如社區、游戲。這些業務的數據讀寫頻率並沒很大的差異,而且也要求很高的延遲。因此人們又再想辦法,把本地內存和遠端進程的內存緩存結合起來使用,讓數據具備兩級緩存。同時,一個數據不在同時的復制存在所有的緩存進程上,而是按一定規律分布在多個進程上。——這種分布規律使用的算法,最流行的就是所謂“一致性哈希”。這種算法的好處是,當某一個進程失效掛掉,不需要把整個集群中所有的緩存數據,都重新修改一次位置。你可以想象一下,如果我們的數據緩存分布,是用簡單的以數據的ID對進程數取模,那么一旦進程數變化,每個數據存放的進程位置都可能變化,這對於服務器的故障容忍是不利的。
Orcale公司旗下有一款叫Coherence的產品,是在緩存系統上設計比較好的。這個產品是一個商業產品,支持利用本地內存緩存和遠程進程緩存協作。集群進程是完全自管理的,還支持在數據緩存所在進程,進行用戶定義的計算(處理器功能),這就不僅僅是緩存了,還是一個分布式的計算系統。
存儲技術(NoSQL)
相信CAP理論大家已經耳熟能詳,然而在互聯發展的早期,大家都還在使用MySQL的時候,如何讓數據庫存放更多的數據,承載更多的連接,很多團隊都是絞盡腦汁。甚至於有很多業務,主要的數據存儲方式是文件,數據庫反而變成是輔助的設施了。
然而,當NoSQL興起,大家突然發現,其實很多互聯網業務,其數據格式是如此的簡單,很多時候根部不需要關系型數據庫那種復雜的表格。對於索引的要求往往也只是根據主索引搜索。而更復雜的全文搜索,本身數據庫也做不到。所以現在相當多的高並發的互聯網業務,首選NoSQL來做存儲設施。最早的NoSQL數據庫有MangoDB等,現在最流行的似乎就是Redis了。甚至有些團隊,把Redis也當成緩沖系統的一部分,實際上也是認可Redis的性能優勢。
NoSQL除了更快、承載量更大以外,更重要的特點是,這種數據存儲方式,只能按照一條索引來檢索和寫入。這樣的需求約束,帶來了分布上的好處,我們可以按這條主索引,來定義數據存放的進程(服務器)。這樣一個數據庫的數據,就能很方便的存放在不同的服務器上。在分布式系統的必然趨勢下,數據存儲層終於也找到了分布的方法。
本文來自 韓大 微信公眾號