如何設計一個異步Web服務——任務調度

接上一篇《如何設計一個異步Web服務——接口部分html

 

Application已經將任務信息發到了Service服務器中,接下來,Service服務器改如何對自身的資源進行合理分配以知足Application對功能、性能、用戶體驗等各方面的需求呢?服務器

 

能夠從以下幾個方向入手去考慮:微信

  1. 當task提交到Service後,咱們但願Service可以儘量快的完成這個task並返回結果。
  2. 當大量task同時提交Service後,咱們但願Service不要由於須要同時處理大量task致使性能降低,甚至失去響應。
  3. 當有多個task被提交到Service時,咱們不但願某一個task佔用了全部計算資源,致使其餘task長時間處於等待狀態。

根據上面的要求,咱們會產生以下的設計要求:併發

  1. 根據第一點要求,爲了可以儘快完成一個task,咱們可使用多線程(或多進程)技術,將一個task拆分爲多個子task而後並行處理,充分利用多核CPU的計算資源。
  2. 根據第二點要求,咱們須要爲Service實現一個任務隊列,以避免大量併發請求致使Service計算資源被耗盡。同時,大量的併發也會致使CPU爲進行資源調度浪費許多計算資源。
  3. 根據第三點要求,雖然Service會使用任務隊列對任務進行排隊處理,但咱們仍然但願有少許的task是並行進行的。
  4. 另外,一個task被拆分爲許多子task後,若是爲每一個子task建立一個單獨的線程去處理,會致使CPU將大量時間消耗在線程的建立、銷燬過程當中。因此,應該使用線程池(或進程池)技術。

 

下面,咱們就根據上述的這些要求開始設計。異步

 

首先,咱們須要一個http服務器做爲接收Application請求的接口。而後,咱們建立一個QueenAnt(蟻后)類來負責任務和資源的調度,同時還須要若干個WorkerAnt(工蟻)類來處理各個具體的task。性能

 

注意,這裏的QueenAnt類是靜態的,或者也能夠用單例模式建立。前面提到咱們須要使用線程池(或進程池)技術,因此,在QueenAnt類被實例化之後首先就須要把這個線程池建立出來,並建立若干線程放入這個池中。其中,每一個線程都會實例化一個WorkerAnt類來等待QueenAnt發過來的task。這個地方還有一個問題,那就是咱們的線程池中到底建立幾個線程最優?這個問題我留到後面說明。ui

當這些準備好了之後,Service就能夠等待Application的請求了。spa

當Application向Service發出addTask的請求時,http服務器會將這個請求通知給QueenAnt,並返回QueenAnt返回的taskId。線程

QueenAnt在收到task請求後,除了返回taskId,還須要對這個task的相關信息進行初始化,好比設置task的狀態信息,將task添加到任務隊列等等。

等這些結束之後,QueenAnt就開始針對已經收到的task進行任務調度和資源分配了。咱們定義一個allocateResource方法來處理相關的邏輯。該方法將會指定threadPool中的哪一個具體線程會來處理這個task。這以後,咱們就能夠把task相關的數據發給這個指定的thread進行處理了。而當有task完成時,處理該task的線程中的WorkerAnt就會發送相關信息給QueenAnt,調用QueenAnt的taskEndCallback方法,讓QueenAnt從新分配資源。

 

當WorkerAnt完成某一個task以後,他須要將這個task的相關信息返回給QueenAnt。同時標記本身爲空閒狀態,以便QueenAnt再進行資源分配。

QueenAnt在收到WorkerAnt關於task完成的消息後,他也須要更新於這個task的相關狀態信息,並在此根據threadPool和taskQueue的具體狀況從新進行資源分配。

 

到這裏,咱們就經過上圖描述的邏輯,知足了設計要求中的第二和第四點要求。那第一和第三點要求呢,就得經過allocateResource這個方法去實現了。

下面咱們詳細講一下allocateResource這個方法的內部邏輯。

這裏先聲明一下後文的描述方法,咱們把Application發過來的一個任務叫作"task",而把由這個任務拆分出來的許許多多的小任務叫作"子task"。

 

可能有人會產生疑惑,根據設計要求中的第一點,咱們應該把task拆分爲子task。可上面的設計中,咱們放入taskQueue的倒是Application傳過來的task,是否是差一個拆分的步驟呢?

其實並非這樣,這樣的設計是由於開頭的考慮方向中的第三點和設計要求中第三點,都要求一個task不能夠佔用全部的計算資源。這樣說可能不太好理解,咱們來舉個例子:

首先,Application向Service提交了task01,該task共20個子task,須要Service滿負荷運行5分鐘才能完成。

到第3分鐘的時候,Application又向Service提交了task02,該task共4個子task,須要Service滿負荷運行1分鐘便可完成。

咱們來分析一下這個場景。若是咱們在將task01加入taskQueue以前,就將其拆分爲許多的子task。並把threadPool中的資源依次分給這些子task。那麼到第3分鐘加入task02的各個子task的時候,因爲task01的子task沒有完成,task02只好處於等待狀態。並且須要等task01的幾乎全部子task都完成之後,才能進入處理中的狀態,這一等就是10分鐘。這顯然違背了咱們考慮方向中的第三點和設計要求中第三點。

 

那麼,怎樣設計這個allocateResource的邏輯才能既知足設計要求中的第一點,又能知足第三點了?個人思路是這樣的。

 

首先,咱們給task加上兩個屬性threadRequirement和runningThread。threadRequirement表示,爲了完成這個task,若是給其每一個子task分配一個線程,那麼一共須要多少個線程,隨着子task的完成,這個數值會愈來愈小,最後變爲0即表示這個task已經所有完成。runningThread表示,當前有幾個線程正在處理這個task的子task。

 

而後,allocateResource這個方法有兩個地方會調用,一是當Service收到新的task請求的時候。二是當某個子task完成,QueenAnt中的taskEndCallback被調用的時候。

allocateResource在給task分配資源的時候,應遵照如下幾個準則:

  1. taskQueue中處於等待狀態的task應該儘量的少。
  2. 同時進行的task的數量不得超過threadPool中線程的總數。
  3. 每一個task都應該至少有一個線程在處理其子task。
  4. 在知足以上條件的狀況下,threadRequirement最小的task分配到全部剩餘的空閒線程資源。

 

這樣說可能有些抽象。咱們仍是來舉個上面那個例子,假設threadPool中共4個線程,task01的threadRequirement爲20,task02的threadRequirement爲2。過程以下:

  1. QueenAnt收到task01的請求後開始調用allocateResource方法。且當前threadPool中有空閒的線程資源。
  2. 根據準則1,咱們看到taskQueue中當前有一個task,就是task01。
  3. 當前沒有正在進行的task數量沒有達到threadPool中的線程總數4,知足準則2。因而將task01從taskQueue中取出準備爲其分配線程資源。
  4. 爲知足準則3,咱們將threadPool中的一個空閒線程thread01分配給task01的一個子task:childTask01。
  5. 爲知足準則4,咱們將threadPool中剩下的全部空閒線程都分配給task01,這樣以來,task01的4個子task,childTask01~childTask04同時接受處理。
  6. 一分鐘後,childTask01~childTask04相繼結束,taskEndCallback被觸發,allocateResource再次被調用。重複上面的步驟3和步驟4。childTask05~childTask08開始接受處理。
  7. 又一分鐘後,childTask05~childTask08相繼結束,taskEndCallback被觸發,allocateResource再次被調用。重複上面的步驟3和步驟4。childTask09~childTask12開始接受處理。
  8. 一秒鐘後,QueenAnt收到task02的請求後開始調用allocateResource方法。但當前threadPool中沒有空閒的線程資源,因此方法退出,task02停留在taskQueue中等待。
  9. 大概59秒之後,task01的一個childTask09結束,taskEndCallback被觸發,allocateResource再次被調用。
  10. 根據準則2,當前正在進行的task只有task01,遠沒達到threadPool中線程的總數4,因此咱們能夠將task02從taskQueue中取出準備爲其分配線程資源。
  11. 根據準則3,每一個task至少分配一個線程資源,而當前task02的runningThread爲0。因此咱們把剛纔處理task01中childTask09的線程thread01分配給task02。就這樣,task02也開始運行起來了。
  12. 接下來,threadPool中的thread02和thread03也相繼完成了task01的childTask10和childTask11,並觸發taskEndCallback調用allocateResource。
  13. 此時,咱們根據準則4,會把剛剛釋放出來的thread02和thread03兩個線程資源都分配給task02。這時,task01只有一個線程資源thread04在處理,而其餘三個線程資源都被用來處理task02了。
  14. 再接下來,thread04處理完task01的childTask12後,根據準則3又會被分配給task01處理childTask13。

 

大概的邏輯就是上面這樣了。步驟看起來雖然略顯複雜,但其實只有掌握了前面說的4個準則,allocateResource的邏輯仍是很好實現:

 

至此,關於Service任務調度和資源分配的設計也結束了。

 

下面,咱們來講一下前面遺留的一個問題:線程池中到底建立幾個線程最優?

爲何最後要特別來談談這個問題,是由於市面上有一種叫作超線程的CPU虛擬化的技術。好比Intel公司的酷睿i3系列CPU,明明是兩個物理核心,在Windows的任務管理中,或在Linux系統的top命令下,顯示的倒是4個核心,由於CPU在硬件層面將兩個物理核心模擬爲4個邏輯核心了。根據咱們上面的設計,天然是但願threadPool中的線程數量越多越好,但是也不能太多。由於多個線程同時爭用一個CPU核心的資源是沒有必要的。因此,若是是4核的CPU,咱們通常會起4個線程放入threadPool。但是在這種使用了超線程技術的CPU平臺上,若是你把線程數目配置爲與CPU邏輯核心數目一致倒是沒有必要的。我在i3平臺上實測數據以下:

線程數

總耗時(s)

CPU 0 使用率

CPU 1 使用率

CPU 2 使用率

CPU 3 使用率

1

22.4

1

0

98

0

2

12.6

2

98

0

97

3

11.2

78

64

97

4

4

10.5

98

99

100

98

 

我想上面的數據已經很好地說明了問題,雖然是4個邏輯核心,雖然你可讓4個線程同時運行,但其實在CPU物理層面,同時運行的指令最多就兩個。也就是4個線程中每兩個線程去爭用一個物理核心的運算資源。

其結果就是,性能上的微小進步卻帶來了CPU使用率的大幅飆升,反而使得用來做爲接口的httpServer響應時間變長。

 

如需轉載,請註明轉自:http://www.cnblogs.com/silenttiger/p/4135461.html

 

歡迎關注個人微信公衆號:老虎的小窩
微信公衆號 老虎的小窩

相關文章
相關標籤/搜索