高並發的概念及應對方法


為什么學習高並發?

作為一名非CS科班出生的同學,在經過多年IT從業之后,明顯能感受到職業生涯發展的后繼無力,由於從事的是傳統金融行業,對應的公司其實內心深處是不重視IT部門的,而我這種IT從業人員雖然已經是團隊或者是部門非常重要的人員,但是最后再發展下去也就是一個業務專家,業務專家本質上的知識不是自身的知識體系,而是公司的知識體系,而只有技術知識體系才是自己的。於是早在18年我就開始了自己的轉型學習之路,前期學習了網絡知識、Java並發編程,再要想學習JVM時,命運無情的枷鎖打斷了我的學習進程,19年就在沉悶和潛伏中度過,終於到了20年了,也即將邁入而立之年,給自己定個目標,今年上半年一定要轉型完成進入一家心儀的公司,在職業生涯的黃金時期認認真真的再CODE一下。先說一下轉型目標,希望能從事高並發、高性能相關的開發工作,為什么會定這個目標?除了自己對這個領域感興趣外,確實也能從某些知名大型企業的社會招聘能看到這些領域的需求崗位是比較旺盛的,截圖如下:

 

 

 

 

 

 

什么是高並發?

再談論什么是高並發之前,我覺得非常有必要先搞清楚並發、並行和高並發這幾個概念,至少對於非CS科班出身的我,一直就不知道這幾個是什么高深的內容。

並發、並行和高並發

網易公開課《清華大學公開課:7.3進程的特點》中是這么定義的並發和並行,並發是指在一個時間段內有多個進程在執行,只不過在人的角度看,因為這個計算機角度的時間實在是太短暫了,人根本就感受不到是多個進程,看起來像是同時進行,這種是並發,而並行指的是在同一時刻有多個進程在同時執行。一個是時間段內發生的,一個是某一時刻發生的,如果是在只有一個CPU的情況下,是無法實現並行的,因為同一時刻只能有一個進程被調度執行,如果此時同時要執行其他進程則必須上下文切換,這種只能稱之為並發,而如果是多個CPU的情況下,就可以同時調度多個進程,這種就可以稱之為並行。從這里我們可以總結出並發和並行的差異,首先粒度不一樣,並發針對的是時間段,並行針對的是時間點,其次行為也不一樣,並發的動作側重於處理行為,並行的動作側重於執行行為,只不過人的視角和機器的視角差異導致看起來都是同時執行的。

那么什么是高並發呢?其實高並發的意思和前面說的並發的意思不止是差了一個“高”字,而是個寬泛得多的概念。高並發是指可以讓軟件系統在一段時間內能夠處理大量的請求。比如每秒鍾可以完成10萬個請求。這是互聯網系統的一個重要的特征。不像並發說的是“處理”,並行說的是“執行”,高並發說的是最終效果。只要能達到效果,不管怎么實現都行。因此,極端一點高並發甚至並不一定需要並行,只要處理速度快的足夠滿足要求就可以。如啟動一個nginx的OS進程,它只能用到一個CPU核心,也就不可能並行。但是他如果能每秒能處理10萬個請求,而業務需求只要求8萬個請求就可以了,那么這個單進程的nginx本身就算高並發了。通過這段話,我們能明白高並發是最終的結果,為了實現這個結果,技術上可能會用到非常多的技術方案,這些技術方案大量應用各種並發的集中人類智慧的各種方法,並盡可能的並行除了並發和並行,高並發還需要:

  • 數據表普遍被分庫分表,否則單機放不下,或者查詢性能不足
  • 解決分布式事務
  • 因為機器都可能壞,為了保證少數機器壞掉不會影響處理的性能,必須引入HA機制
  • 因為系統都有極限,超過極限響應能力就會急劇下降。因此必須引入限流的方案來保護系統
  • 這么復雜的系統會涉及到N個service,N個存儲,N個隊列…… 這些資源的管理又成為了新的問題,這又需要對集群和服務做管理
  • 這么多服務,肯定要解決分布式的Tracing和報警問題
  • ……

到此我們可以知道,高並發其實是一個技術體系,甚至不同的情況下高並發的指標效果還不一樣,為了達到這個效果,我們會使用很多能並發處理的技術,為了能達到這個效果,我們會在應用系統的每一層做不同的處理,為了能達到這個效果,我們還可以根據語言的不同特性來選擇實現最終效果的編程語言。

 

如何應對高並發?

在這一節,我會概括性的總結我所學習的應對高並發的方式方法,之所以說是概括性,是因為高並發這個領域涉及的技術和知識點都不是可以速成的,也不是我這個傳統IT行業從業人員實踐所得,而是從網絡、書本中獲取的其他人的經驗總結,后續我會從中選取高並發中的連接處理、RPC相關內容做一個較為深入的學習和總結,希望以此作為突破口,實現自己上半年的目標。

從一個完整的HTTP請求說起

高並發意味着單位時間內系統能處理的請求數很高,也就是說系統所能承載的HTTP請求很多,那要應對高並發,就要從HTTP請求處理層面開始,如下是我理解的一個完整的HTTP請求所經歷的流程:

1、DNS域名解析

將請求域名解析為IP地址。

2、與IP地址對應的服務器網卡建立連接,TCP的三次握手,連接建立並占用

3、服務器操作系統通過連接讀取和處理請求

3.1 從連接中讀取字節流(IO密集)

3.2 將讀取到的字節流轉換成HTTP請求(CPU密集)

4、服務器操作系統將HTTP請求轉發給WEB Server或者Application Server

4.1 Application在開發邏輯架構中一般會分層,分為表現層、業務層和持久層

5、Application進行業務邏輯處理並准備響應Response

6、Response准備完成,Response通過網卡回寫到用戶的瀏覽器(IO密集)

7、TCP連接三次揮手,斷開連接

從這個過程中,我們對一個HTTP的請求能有一個感性的認識,基於這個感性的認識,我們能知道這里面幾個關鍵的點:域名解析、連接處理、系統分層,從這幾個關鍵點其實就可以提取出一些應對方法。

域名解析

域名解析這個環節有一個應對高並發的方法就是CDN,CDN 就是將靜態的資源分發到位於多個地理位置機房中的服務器上,因此它能很好地解決數據就近訪問的問題,也就加快了靜態資源的訪問速度。搭建CDN主要有兩個關鍵點,一是如何將用戶的請求映射到 CDN 節點上,二是如何根據用戶的地理位置信息選擇到比較近的節點。

將用戶的請求映射到 CDN 節點其實就是域名解析(DNS)的過程,DNS解析的過程大致如下:

首先查看本機的host文件,查看是否有該域名對應的IP地址;

如果沒有就請求Local DNS是否有域名解析結果的緩存,如果有就返回標識是否從非權威DNS返回的結果;

如果還是沒有就進入DNS迭代查詢的過程,先查詢根DNS的地址(如.com/.cn/.org), 再請求頂級DNS得到二級域名服務器地址(如baidu.com);再從二級域名服務器中查詢到 子域名對應的 IP 地址(如www.baidu.com),返回這個 IP 地址的同時標記這個結果是來自於權威 DNS 的結果,同時寫入 Local DNS 的解析結果緩存,這樣下一次的解析同一個域名就不需要做 DNS 的迭代查詢了。

根據用戶的地理位置信息選擇到比較近的節點則是GSLB的作用,GSLB(Global Server Load Balance,全局負載均衡)的含義是對於部署在不同地域的服務器之間做負載均衡,下面可能管理了很多的本地負載均衡組件。它有兩方面的作用:一方面,它是一種負載均衡服務器,指的是讓流量平均分配使得下面管理的服務器的負載更平均;另一方面,它還需要保證流量流經的服務器與流量源頭在地緣上是比較接近的。

連接處理

這一節,我們關注網絡連接處理IO層面的一些應對之策。

Tomcat如何應對

實際工作當中,Tomcat作為容器承擔着與用戶建立連接、解析請求、轉發請求、回寫響應的工作,Connector作為Tomcat兩個核心組件之一,主要任務是實際負責接收瀏覽器發來的連接請求,創建一個Request和Response對象分別用於和請求端交換數據,把產生的Request和Response對象傳給后續處理這個請求的工作線程。先給個結論,默認情況下Tomcat7.0應對大量連接的能力不如Tomcat8.5,為什么呢?可以來看下Connector中關於protocol的設置:

7.0 protocol- The default value is HTTP/1.1 which uses an auto-switching mechanism to select either a blocking Java based connector or an APR/native based connector.

8.5 protocol- The default value is HTTP/1.1 which uses an auto-switching mechanism to select either a Java NIO based connector or an APR/native based connector.

APR/native全稱Apache Portable Runtime,由Apache基金會提供維護支持,非tomcat自帶的組件,需要獨立安裝,能應對大量的連接,但是如果在未安裝該組件的情況下,我們可以發現默認情況下7.0使用的是blocking Java而8.5使用的是Java NIO,由此是不是可以猜想是IO處理方式的不同決定了應對高並發的效果不一樣。

 

IO模型

在繼續探討連接處理層面的應對之策前,我們先來回顧一下Unix環境下的網絡IO模型。

阻塞式IO模型(blocking I/O):

 

 

 

進程阻塞調用,一旦線程創建過多,線程的創建銷毀成本高,線程本身的內存占用大,CPU不斷的進行線程的上下文切換。

非阻塞式IO模型(nonblocking I/O):

 

 

 

應用線程在IO數據准備階段,忙等待。

IO多路復用模型(I/O multiplexing):

 

 

 

相對於同步阻塞IO模型,借由操作系統提供的select/poll功能使用少量線程管理大量連接,用戶進程通過select/poll獲取當前實際ready的socket,然后在進程中阻塞的將內核態拷貝到用戶態。在Linux系統中,有更高效的epoll實現。實際使用中,socket常會被設置成以non-blocking方式訪問。

信號驅動式IO模型(signal-driven IO):

 

 

 

異步IO模型(asynchronous I/O):

 

 

 

IO模型區別總覽:

 

 

 

 

Java NIO

在了解完網絡IO模型之后,我們再回來看下Tomcat中的Java NIO是如何體現這個IO模型的。

Java NIO中的三個核心概念:Channel、Selector和Buffer。

Channel是可IO操作的硬件、文件、socket、其他程序組件的包裝。

Selector是select/poll/epoll包裝。

Buffer是數據容器,和Channel成對出現,數據只能從Channel中讀取和寫入。

JDK1.4引入的NIO,Selector使用Linux poll實現。

JDK6 & JDK 5 update 9支持epoll(Linux Kernel >= 2.6)。

JDK7 支持NIO2,引入異步IO的四個通道:

AsynchronousSocketChannel

AsynchronousServerSocketChannel

AsynchronousFileChannel

AsynchronousDatagramChannel

值得注意的是,這四個通道運行在Linux中仍是通過epoll實現,並非真正意義的異步IO。

至此我們可以知道我們工作中經常使用的Tomcat是如何在IO模型上應對高並發了。

再談Tomcat 線程模型

 

 

 

Acceptor負責接收Socket並將之封裝為NioChannel,通過NioChannel的register注冊到Poller中。

Poller實際是基於Java NIO Selector實現的,主要功能為獲取當前可讀取的NioChannel並將其分發給實際的Worker。

Worker是實際負責請求處理的線程池,將請求轉發給應用層處理。

一個新的問題

高並發帶來的請求流量都很大,巨大的流量沖擊到Tomcat后,如果Worker處理不過來,系統會發生什么呢?

三個重要的tomcat connector參數:acceptCount,maxConnections,maxThreads

若沒有空閑的Worker(即到達了maxThreads),新來的請求不得不接受長時間的等待,一直等到有新的Worker為止。

更多的請求到達,Acceptor發現創建的NioChannel抵達maxConnections時,阻塞。

更多的請求到達,OS層面的連接仍會繼續進行,抵達acceptCount時,OS拒絕連接,Client端顯示Connection refused。

這個時候一個新的應對方式也就產生了-異步化。

基於Servlet API的異步方式

在servlet3.0引入了基於servlet API實現的異步處理方案,流程圖如下:

 

 

 

Tomcat的工作線程(Worker)在支持異步的Servlet中通過調用如下命令開啟異步處理:

AsyncContext ServletRequest.startAsync(Request request,Response response)

Tomcat的工作線程會將當前的request,response對象的引用放入AsyncContext中,並將AsyncContext交付給一個新的線程處理,保持response的打開狀態,然后立即返回Tomcat工作線程池等待處理其他的請求。

新的線程應該是什么?

新的線程是業務線程池產生的,並且不是一個線程池,而是根據業務設計的多級線程池組。例如區分核心業務和非核心業務線程池。通過線程池的隔離,保證非核心業務的抖動不會影響到核心業務,另一個好處就是保留對業務線程池的監控、運維、降級等。新的線程主要做業務處理,通過response回寫響應並且關閉response。

AsyncContext.complete()

Spring 3.2開始支持Servlet 3.0,通過異步化,我們將解析和業務處理徹底剝離,同時業務處理間區分不同的線程池保證隔離性。

異步處理不會降低響應的時間,但是會提高吞吐量,從而應對高並發。

Reactive Stack

一個相對於servlet-stack獨立的全新的技術棧reactive stack:Spring-WebFlux。

 

 

 

從圖中可以看出這個全新的stack是基於Reactive Streams。

最后總結一下,連接處理層面應對高並發的思路就是:

阻塞變非阻塞,同步變異步,核心就是充分利用單機性能,壓榨CPU。

系統分層

這一節,我們將從系統分層這個層面來看每一層可以采用的應對之策。

業務層

本節主要關注在業務層,面對高並發場景下對於業務邏輯實現相關的處理方案。

緩存

緩存是一種存儲數據的組件,它的作用就是讓對數據的請求能更快的返回。高並發的場景下,如果能快速的返回請求所需要的數據,對於系統持久層是一種相當好的保護措施,對於系統來說也能提升吞吐量。常見的緩存技術有靜態緩存、分布式緩存和熱點本地緩存。

靜態緩存在 Web 1.0 時期是非常著名的,它一般通過生成 Velocity 模板或者靜態 HTML 文件來實現靜態緩存,在 Nginx 上部署靜態緩存可以減少對於后台應用服務器的壓力。這種緩存只能針對靜態數據來緩存,對於動態請求就無能為力了。那么我們如何針對動態請求做緩存呢?這時你就需要分布式緩存了。

分布式緩存性能強勁,通過一些分布式的方案組成集群可以突破單機的限制。所以在整體架構中,分布式緩存承擔着非常重要的角色。

對於靜態的資源的緩存可以選擇靜態緩存,對於動態的請求可以選擇分布式緩存,那么什么時候要考慮熱點本地緩存呢?答案是當我們遇到極端的熱點數據查詢的時候。熱點本地緩存主要部署在應用服務器的代碼中,用於阻擋熱點查詢對於分布式緩存節點或者數據庫的壓力。 

消息隊列

系統的初級階段基本上是讀多寫少,所以在應對高並發時業務層會先加入緩存組件,希望通過緩存阻擋大量的讀請求。隨着系統的不斷發展,高並發系統開始涌入大量的寫請求,此時會使用到消息隊列來應對高並發的場景。

異步解耦和削峰填谷是消息隊列的主要作用,其中異步處理可以簡化業務流程中的步驟,提升系統性能;削峰填谷可以削去到達秒殺系統的峰值流量,讓業務邏輯的處理更加緩和;解耦可以將系統和其他系統解耦開,這樣一個系統的任何變更都不會影響到另一個系統。

業務拆分

正常情況下,初期為了系統能盡快上線,都會是以單體架構的形式出現,而隨着流量和請求的增多,單體架構開始出現一些問題:

數據庫連接數可能會成為系統的瓶頸;

內部成員的溝通成本問題;

代碼提交,分支管理,項目編譯等問題;

模塊相互依賴,強耦合,一旦出問題容易牽一發而動全身;

………

這個時候,為了系統能達到支撐高並發的效果,便會對系統做業務拆分,以應對上述眾多問題,拿我們保險系統來說,最早期的保險核心系統,其實契約、保全、財務、理賠甚至是各種報表都是集中在一個系統中,雖然保險是一個低頻行為,不太具有高並發的特性,但是在系統發展的過程中,也會有類似的拆分,將契約、保全、財務、理賠等從一個系統拆分成多個系統,而高並發系統則是使用了更細粒度的拆分,以提供服務的形式來應對高並發,在流量高峰期可以動態擴容,平穩流量,避免系統崩潰。 

RPC

服務拆分之后,原本的單體系統就會變成分布式系統,原來在同一個進程里面兩個方法調用就會變成跨進程跨網絡的兩個方法調用,此時就會引入RPC框架來解決跨網絡通信問題。RPC框架封裝了網絡調用的細節,可以實現像調用本地服務一樣調用遠程部署的服務。

一個完整的RPC的步驟如下:

在一次 RPC 調用過程中,客戶端首先會將調用的類名、方法名、參數名、參數值等信息,序列化成二進制流;

然后客戶端將二進制流通過網絡發送給服務端;

服務端接收到二進制流之后將它反序列化,得到需要調用的類名、方法名、參數名和參數值,再通過動態代理的方式調用對應的方法得到返回值;

服務端將返回值序列化,再通過網絡發送給客戶端;

客戶端對結果反序列化之后,就可以得到調用的結果了。

從這個過程中,我們可以提取出RPC的核心過程就是網絡傳輸和序列化。其中網絡傳輸可以選擇Netty,而序列化則可以選擇Thrift或者Protobuf。

RPC能夠解決服務之間跨網絡通信的問題,但是對於RPC來說,又是如何知道自己該調用誰或者誰會調用自己呢?這個時候就會引入注冊中心來解決這個問題,比如ZooKeeper、ETCD、Eureka等。 

API網關

API 網關(API Gateway)不是一個開源組件,而是一種架構模式,它是將一些服務共有的功能整合在一起,獨立部署為單獨的一層,用來解決一些服務治理的問題。你可以把它看作系統的邊界,它可以對出入系統的流量做統一的管控。對於高並發系統,API網關的搭建是非常有必要的,因為它可以實現如下作用:

1、提供客戶端一個統一的接入地址,API 網關可以將用戶的請求動態路由到不同的業務服務上,並且做一些必要的協議轉換工作。在系統中,微服務對外暴露的協議可能不同:有些提供的是 HTTP 服務;有些已經完成 RPC 改造,對外暴露 RPC 服務;有些遺留系統可能還暴露的是 Web Service 服務。API 網關可以對客戶端屏蔽這些服務的部署地址以及協議的細節,給客戶端的調用帶來很大的便捷。

2、在 API 網關中,可以植入一些服務治理的策略,比如服務的熔斷、降級、流量控制和分流等等。

3、客戶端的認證和授權的實現,也可以放在 API 網關中。不同類型的客戶端使用的認證方式是不同的。手機 APP 可以使用 Oauth 協議認證,HTML5 端和 Web 端使用 Cookie 認證,內部服務使用自研的 Token 認證方式。這些認證方式在 API 網關上可以得到統一處理,應用服務不需要了解認證的細節。

4、API 網關還可以做一些與黑白名單相關的事情,比如針對設備 ID、用戶 IP、用戶 ID 等維度的黑白名單。

5、在 API 網關中也可以做一些日志記錄的事情,比如記錄 HTTP 請求的訪問日志。

持久層

本節主要關注在高並發場景下,數據持久化層所需要關注的內容。

讀寫分離

持久層最先可能會用到的應對高並發的方法就是讀寫分離。對於高並發系統來說,一開始都是讀多寫少,大量的讀請求經過業務層到達持久層之后對數據庫產生了極大的壓力,此時就會准備一個和生產庫一致的數據庫來單獨接收處理讀請求,一般這個叫做從庫,也會被稱為讀庫,高並發流量中的讀請求會被引流到從庫,而寫請求還是在主庫寫,主庫和從庫之間依靠主從復制機制來確保兩個庫的數據近乎實時一致,這樣用戶在讀請求的時候幾乎發現不了數據的不一致。

分庫分表

讀寫分離主要是為了應對讀請求,那如果寫請求的流量大,此時又會有什么應對的方案呢?常見的一種處理方法就是分庫分表。

分庫分表是一種常見的將數據分片的方式,它的基本思想是依照某一種策略將數據盡量平均地分配到多個數據庫節點或者多個表中。不同於主從復制時數據是全量地被拷貝到多個節點,分庫分表后,每個節點只保存部分的數據,這樣可以有效地減少單個數據庫節點和單個數據表中存儲的數據量,在解決了數據存儲瓶頸的同時也能有效地提升數據查詢的性能。同時,因為數據被分配到多個數據庫節點上,那么數據的寫入請求也從請求單一主庫變成了請求多個數據分片節點,在一定程度上也會提升並發寫入的性能。

數據庫分庫分表的方式有兩種:一種是垂直拆分,另一種是水平拆分。垂直拆分的原則一般是按照業務類型來拆分,核心思想是專庫專用,將業務耦合度比較高的表拆分到單獨的庫中。比如前面業務拆分時提到的將保險核心拆成契約、保全、財務和理賠等各大核心系統,這就是一種垂直拆分。和垂直拆分的關注點不同,垂直拆分的關注點在於業務相關性,而水平拆分指的是將單一數據表按照某一種規則拆分到多個數據庫和多個數據表中,關注點在數據的特點。拆分的規則有下面這兩種:

  1. 按照某一個字段的哈希值做拆分,這種拆分規則比較適用於實體表,比如說用戶表,內容表,我們一般按照這些實體表的 ID 字段來拆分。
  2. 另一種比較常用的是按照某一個字段的區間來拆分,比較常用的是時間字段。你知道在內容表里面有“創建時間”的字段,而我們也是按照時間來查看一個人發布的內容。我們可能會要看昨天的內容,也可能會看一個月前發布的內容,這時就可以按照創建時間的區間來分庫分表,比如說可以把一個月的數據放入一張表中,這樣在查詢時就可以根據創建時間先定位數據存儲在哪個表里面,再按照查詢條件來查詢。

NoSQL

讀寫分離和分庫分表是改善數據庫持久層應對並發能力的利器,但是在高並發的持續不斷的積累下,傳統關系型數據庫已經很難應對數據量上的瓶頸,此時便可以利用NoSQL來幫助解決這些問題,因為它有着天生分布式的能力,能夠提供優秀的讀寫性能,可以很好地補充傳統關系型數據庫的短板。 

 

 

本文從宏觀上描述了高並發的概念及相關的應對方法,或概括性、或略微詳細的描述了自己的理解,后續我會選擇里面我感興趣的NIO、RPC等連接處理和服務相關的領域做較為深入的學習和總結。

  

 

參考資料:

https://www.zhihu.com/question/307100151?utm_source=wechat_timeline

http://tomcat.apache.org/tomcat-7.0-doc/config/http.html

http://tomcat.apache.org/tomcat-8.5-doc/config/http.html

https://docs.spring.io/spring-framework/docs/5.0.0.BUILD-SNAPSHOT/spring-framework-reference/html/web-reactive.html

極客時間《高並發系統40問》

《UNIX Network Programming.Volume 1.Third Edition.The Sockets Networking API》

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM