時鐘輪在 RPC 中的應用

大家好,我是不才陳某~

今天這篇文章介紹一下 RPC 中如何使用時鐘輪實現定時任務,比如調用端的超時處理、定時心跳....

定時任務帶來了什麼問題?

在講解時鐘輪之前,我們先來聊聊定時任務。相信你在開發的過程中,很多場景都會使用到定時任務,在 RPC 框架中也有很多地方會使用到它。就以調用端請求超時的處理邏輯爲例,下面我們看一下 RPC 框架是如果處理超時請求的。

在講解 Future 的時候說過:無論是同步調用還是異步調用,調用端內部實行的都是異步,而調用端在向服務端發送消息之前會創建一個 Future,並存儲這個消息標識與這個 Future 的映射,當服務端收到消息並且處理完畢後向調用端發送響應消息,調用端在接收到消息後會根據消息的唯一標識找到這個 Future,並將結果注入給這個 Future。

那在這個過程中,如果服務端沒有及時響應消息給調用端呢?調用端該如何處理超時的請求?

沒錯,就是可以利用定時任務。每次創建一個 Future,我們都記錄這個 Future 的創建時間與這個 Future 的超時時間,並且有一個定時任務進行檢測,當這個 Future 到達超時時間並且沒有被處理時,我們就對這個 Future 執行超時邏輯。

那定時任務該如何實現呢?

有種實現方式是這樣的,也是最簡單的一種。每創建一個 Future 我們都啓動一個線程,之後 sleep,到達超時時間就觸發請求超時的處理邏輯。

這種方式吧,確實簡單,在某些場景下也是可以使用的,但弊端也是顯而易見的。就像剛纔我講的那個 Future 超時處理的例子,如果我們面臨的是高併發的請求,單機每秒發送數萬次請求,請求超時時間設置的是 5 秒,那我們要創建多少個線程用來執行超時任務呢?超過 10 萬個線程,這個數字真的夠嚇人了。

別急,我們還有另一種實現方式。我們可以用一個線程來處理所有的定時任務,還以剛纔那個 Future 超時處理的例子爲例。假設我們要啓動一個線程,這個線程每隔 100 毫秒會掃描一遍所有的處理 Future 超時的任務,當發現一個 Future 超時了,我們就執行這個任務,對這個 Future 執行超時邏輯。

這種方式我們用得最多,它也解決了第一種方式線程過多的問題,但其實它也有明顯的弊端。

同樣是高併發的請求,那麼掃描任務的線程每隔 100 毫秒要掃描多少個定時任務呢?如果調用端剛好在 1 秒內發送了 1 萬次請求,這 1 萬次請求要在 5 秒後纔會超時,那麼那個掃描的線程在這個 5 秒內就會不停地對這 1 萬個任務進行掃描遍歷,要額外掃描 40 多次(每 100 毫秒掃描一次,5 秒內要掃描近 50 次),很浪費 CPU。

在我們使用定時任務時,它所帶來的問題,就是讓 CPU 做了很多額外的輪詢遍歷操作,浪費了 CPU,這種現象在定時任務非常多的情況下,尤其明顯。

什麼是時鐘輪?

這個問題也不難解決,我們只要找到一種方式,減少額外的掃描操作就行了。比如我的一批定時任務是 5 秒之後執行,我在 4.9 秒之後纔開始掃描這批定時任務,這樣就大大地節省了 CPU。這時我們就可以利用時鐘輪的機制了。

我們先來看下我們生活中用到的時鐘。

很熟悉了吧,時鐘有時針、分針和秒針,秒針跳動一週之後,也就是跳動 60 個刻度之後,分針跳動 1 次,分針跳動 60 個刻度,時針走動一步。

而時鐘輪的實現原理就是參考了生活中的時鐘跳動的原理。

在時鐘輪機制中,有時間槽和時鐘輪的概念,時間槽就相當於時鐘的刻度,而時鐘輪就相當於秒針與分針等跳動的一個週期,我們會將每個任務放到對應的時間槽位上。

時鐘輪的運行機制和生活中的時鐘也是一樣的,每隔固定的單位時間,就會從一個時間槽位跳到下一個時間槽位,這就相當於我們的秒針跳動了一次;

時鐘輪可以分爲多層,下一層時鐘輪中每個槽位的單位時間是當前時間輪整個週期的時間,這就相當於 1 分鐘等於 60 秒鐘;當時鍾輪將一個週期的所有槽位都跳動完之後,就會從下一層時鐘輪中取出一個槽位的任務,重新分佈到當前的時鐘輪中,當前時鐘輪則從第 0 槽位從新開始跳動,這就相當於下一分鐘的第 1 秒。

爲了方便你瞭解時鐘輪的運行機制,我們用一個場景例子來模擬下,一起看下這個場景。

假設我們的時鐘輪有 10 個槽位,而時鐘輪一輪的週期是 1 秒,那麼我們每個槽位的單位時間就是 100 毫秒,而下一層時間輪的週期就是 10 秒,每個槽位的單位時間也就是 1 秒,並且當前的時鐘輪剛初始化完成,也就是第 0 跳,當前在第 0 個槽位。

好,現在我們有 3 個任務,分別是任務 A(90 毫秒之後執行)、任務 B(610 毫秒之後執行)與任務 C(1 秒 610 毫秒之後執行),我們將這 3 個任務添加到時鐘輪中,任務 A 被放到第 0 槽位,任務 B 被放到第 6 槽位,任務 C 被放到下一層時間輪的第 1 槽位,如下面這張圖所示。

當任務 A 剛被放到時鐘輪,就被即刻執行了,因爲它被放到了第 0 槽位,而當前時間輪正好跳到第 0 槽位(實際上還沒開始跳動,狀態爲第 0 跳);600 毫秒之後,時間輪已經進行了 6 跳,當前槽位是第 6 槽位,第 6 槽位所有的任務都被取出執行;1 秒鐘之後,當前時鐘輪的第 9 跳已經跳完,從新開始了第 0 跳,這時下一層時鐘輪從第 0 跳跳到了第 1 跳,將第 1 槽位的任務取出,分佈到當前的時鐘輪中,這時任務 C 從下一層時鐘輪中取出並放到當前時鐘輪的第 6 槽位;1 秒 600 毫秒之後,任務 C 被執行。

看完了這個場景,相信你對時鐘輪的機制已經有所瞭解了。在這個例子中,時鐘輪的掃描週期仍是 100 毫秒,但是其中的任務並沒有被過多的重複掃描,它完美地解決了 CPU 浪費的問題。

這個機制其實不難理解,但實現起來還是很有難度的,其中要注意的問題也很多.

時鐘輪在 RPC 中的應用

通過剛纔對時鐘輪的講解,相信你可以看出,它就是用來執行定時任務的,可以說在 RPC 框架中只要涉及到定時相關的操作,我們就可以使用時鐘輪。

那麼 RPC 框架在哪些功能實現中會用到它呢?

剛纔我舉例講到的調用端請求超時處理,這裏我們就可以應用到時鐘輪,我們每發一次請求,都創建一個處理請求超時的定時任務放到時鐘輪裏,在高併發、高訪問量的情況下,時鐘輪每次只輪詢一個時間槽位中的任務,這樣會節省大量的 CPU。

調用端與服務端啓動超時也可以應用到時鐘輪,以調用端爲例,假設我們想要讓應用可以快速地部署,例如 1 分鐘內啓動,如果超過 1 分鐘則啓動失敗。我們可以在調用端啓動時創建一個處理啓動超時的定時任務,放到時鐘輪裏。

除此之外,你還能想到 RPC 框架在哪些地方可以應用到時鐘輪嗎?還有定時心跳。RPC 框架調用端定時向服務端發送心跳,來維護連接狀態,我們可以將心跳的邏輯封裝爲一個心跳任務,放到時鐘輪裏。

這時你可能會有一個疑問,心跳是要定時重複執行的,而時鐘輪中的任務執行一遍就被移除了,對於這種需要重複執行的定時任務我們該如何處理呢?在定時任務的執行邏輯的最後,我們可以重設這個任務的執行時間,把它重新丟回到時鐘輪裏。

總結

今天我們主要講解了時鐘輪的機制,以及時鐘輪在 RPC 框架中的應用

這個機制很好地解決了定時任務中,因每個任務都創建一個線程,導致的創建過多線程的問題,以及一個線程掃描所有的定時任務,讓 CPU 做了很多額外的輪詢遍歷操作而浪費 CPU 的問題。

時鐘輪的實現機制就是模擬現實生活中的時鐘,將每個定時任務放到對應的時間槽位上,這樣可以減少掃描任務時對其它時間槽位定時任務的額外遍歷操作。

在時間輪的使用中,有些問題需要你額外注意:

結合這些特點,我們就可以視具體的業務場景而定,對時鐘輪的週期和時間槽數進行設置。

在 RPC 框架中,只要涉及到定時任務,我們都可以應用時鐘輪,比較典型的就是調用端的超時處理、調用端與服務端的啓動超時以及定時心跳等等。

Java 後端面試官 Java 後端全棧面試題精選,數據庫、消息隊列、架構..... 你想要知道的都在這裏!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/P5eks37k4BjAYkvJj_YUSg