標籤:redis disque 非同步 高效能
簡介
horae是一個基於redis
和disque
實現的輕量級
、高效能
的非同步任務執行器,它的核心是disque
提供的任務隊列,而隊列有先進先出
的時序關係,顧得名:horae
。
horae : 時序女神,希臘神話中司掌季節時間和人間秩序的三女神,又譯“荷萊”。
horae的關注點不是佇列服務的實現本身(已經有不少佇列服務的實現了),而是希望藉助於redis
與disque
提供的純記憶體的高效能的隊列機制,實現一個非同步任務執行器。它可以自由配置任務來自哪種佇列服務,它不關注任務執行的最終狀態(它寫向哪裡)或與哪個系統互動,它給你提供一個執行器以及簡單地編寫任務執行邏輯的方式。
取決於需求,這個執行器在要求不高的時候,只需要一個單節點的redis伺服器,即可運轉。
如果你願意犧牲一點效能,來換取更高的隊列可靠性保障(這種情況我強烈推薦你使用AMQP協議以及它的開源隊列實現:RabbitMQ
)。如果你想這樣,那麼這個執行器也是可用的,只是你需要自己去實現跟RabbitMQ互動的細節。你可以用它串連各種其他隊列來消費訊息並執行任務,它具有充分的擴充性與自由度。但我仍然推薦你使用disque
。
適用情境搶購/秒殺
搶購業務是典型的短時高並發情境,傳統行業裡的類似於學生選課
也可以歸結這類情境。
社交關係處理
純記憶體計算/計數器的情境,比如把社交系統裡的好友、關係搬到記憶體中處理。
耗時的web請求
常見的耗時web請求,比如產生PDF
、網頁抓取
、資料備份
、郵件/簡訊發送
等。
分布式系統前端緩衝隊列
將它置於應用伺服器之後,核心服務之前,作為請求的緩衝隊列使用。
概括起來就是伺服器峰值扛壓
、非同步處理
、純記憶體計算
,當然你把它用成普通隊列也是可以的。
高效能
目前支援disque
跟redis
這兩種佇列服務(主推disque
,redis
的隊列暫時以list
資料介面的lpush
&brpop
實現,但它不是高可靠的,並且沒有ack機制)。這兩種純記憶體的隊列首先保證了消費任務的效能。具體任務的執行效能,取決於使用情境,這裡分析兩種情境:
純記憶體&單線程&無鎖
如果任務處理器消費的訊息是完全儲存於記憶體中的,那麼需要盡量將同構的各任務訪問的資料進行隔離(隔離的手段是對key劃分命名空間),如果實在沒辦法隔離,可以使用單隊列單線程無鎖的處理方式。
通用&多線程&多隊列
如果是通用的應用情境,比如訪問資料庫,因為資料庫有成熟的資料一致性保證。所以,你可以將任務劃分到多個不同的隊列,並利用多個線程來並發執行以加快任務的處理效率。
當然最推薦的使用方式是:用redis
作為配置、協調、管控中心,用disque
做佇列服務,任務需要訪問的資料儘可能儲存於redis
中。
高可用一主多從
執行器在運行時實行的是:Master單節點運行,多個Slave做Standby的機制來保證服務的可用性。事實上,從Master下線到其中一個Slave成功競選為Master需要數個心跳周期的時間。因為執行器作為隊列的消費者跟隊列是完全解耦的,所以短暫的暫停消費對整個系統的可用性不會產生太大影響。
心跳機制
Master跟Slave之間通過redis-Pubsub
來維持心跳。目前的設計是Master單向publish
心跳,Slavesubscribe
Master的心跳。這麼設計的原因是簡單,並且考慮到每個Slave都是無狀態的執行器,並不會涉及到狀態的維護與同步問題,所以Master不需要關心Slave的存活。
競爭Master
一旦Master下線(比如因為故障宕機),需要快速得從多個Slave中選舉出一個新的Master,選舉的演算法非常多,並且非常複雜。
通常選舉Master的方式會由一個獨立的承擔Manager
角色的節點來完成,如果不存在這樣一個節點那麼通常會基於分布式選舉演算法來實現(Zookeeper
比較擅長這個)。這裡簡單得採用類似於競爭分布式鎖的實現方式來搶佔Master。
如何判斷Master是否下線?這是一個非常關鍵的問題,因為如果產生誤判,將會給整體系統服務造成一段空檔期,這是一個不小的時間開銷。採用的判斷方式是雙重檢測:
- Slave訂閱Master的heartbeat channel,判斷心跳是否逾時
- Slave去Master的資料結構中去擷取Master自己重新整理的心跳時間戳記,並跟目前時間對比,判斷是否逾時
具體的實現方式:每個服務都會有一個heartbeat線程,Master的heartbeat線程做兩件事情:
- refresh自己的心跳時間戳記
- publish自己的心跳到
heartbeat
channel
Slave的heartbeat線程做上面的雙重檢測,Slave會等待幾個心跳周期,如果在這段時間內,兩種檢測都認為Master失去心跳,則判斷Master下線。
Master下線後,就涉及到多個Slave競爭Master的問題,這裡我們在競爭鎖的時候沒有採用阻塞等待的方式,而是採用了一種危險性相對小的方式:tryLock
:
private boolean tryLockMaster() { long currentTime = RedisConfigUtil.redisTime(configContext); String val = String.valueOf(currentTime + Constants.DEFAULT_MASTER_LOCK_TIMEOUT + 1); Jedis jedis = null; try { jedis = RedisConfigUtil.getJedis(configContext).get(); boolean locked = jedis.setnx(Constants.KEY_LOCK_FOR_MASTER, val) == 1; if (locked) { jedis.expire(Constants.KEY_LOCK_FOR_MASTER, Constants.DEFAULT_MASTER_LOCK_TIMEOUT); return true; } } finally { if (jedis != null) RedisConfigUtil.returnResource(jedis); } return false; }
只有判斷Master下線之後,才會調用tryLockMaster
,它僅僅是嘗試獲得鎖,如果擷取成功,將給鎖設定一個很短的到期時間,這裡跟跟心跳到期時間相同。如果擷取失敗將繼續檢測心跳。擷取鎖的Slave會立即變為Master並迅速重新整理自己的心跳,這樣,其他Slave檢測Master下線就會失敗,將不會再去調用tryLockMaster
。避免了通常情況下,一直阻塞、競爭鎖這一條路。
擴充性擴充功能
得益於Redis的PubSub
,我們可以實現很多類似於指令下發->執行
的feature,比如即時擷取任務的執行進度、讓各伺服器彙報自己的狀態等。因為時間關係,目前這塊只是留了一個擴充口:
- 上行頻道:執行器有一個
upstream
channel,用於上傳各節點的本機資訊。
- 下行頻道:系統有一個
downstream
channel,用於被動接受來自上遊的資訊/指令。
這裡上下遊的語義是:所有服務節點均為下遊,redis
配置中心應該算是中心節點,在上遊你可以定製一個管控台,用於管理redis
配置中心並向下遊的服務節點下髮指令。
擴充佇列服務
如果你想擴充它,希望它支援另一種佇列服務(為了方便表述,這裡假設你想支援RabbitMQ)。那麼你需要做以下幾步:
- 在package:
com.github.horae.exchanger
包下建立類:RabbitConnManager
用於管理client 到 RabbitMQ的串連
- 同樣在package:
com.github.horae.exchanger
包下建立類:RabbitExchanger
用於實現訊息的出隊與入隊邏輯,該類需實現TaskExchanger
- 在
TaskExchangerManager
的createTaskExchanger
方法內加入新的分支判斷。
- 在
partition.properties
下可以配置新的partition,在matrix中指定RabbitMQ
需要注意的是:TaskExchanger
的dequeue
介面方法,預設的行為是block
形式的。如果你擴充的隊列不支援block形式的消費,那可能需要你自己實現,實現的方式可以藉助於java.util.concurrent.BlockingQueue
。
多種可靠性層級
隊列的可靠性牽扯到整個分布式系統的可靠性,這是一個無法迴避的問題。如果你說用redis
實現的隊列,是否能做到既保持高效能
又能兼具高可靠
,答案是不能
。或者說它不是一個專業的佇列服務(不然redis的作者也沒有必要再另起disque
項目了)。如果從可靠性的角度而言,我給幾個主流的佇列服務器(或者可以提供佇列服務)的排名是:RabbitMQ
> Kafka
> Disque
> Redis
。雖然這個執行器內建支援了disque
和redis
作為隊列的實現,但它跟你選擇的佇列服務沒有非常緊的耦合關係,你可以選擇其他佇列服務,通常你只需要實現這麼幾個功能入隊訊息
、出隊訊息
、ack訊息
、管理串連
。
分區
對我而言分區
的概念來自於Kafka,但這裡的分區跟Kafka性質不太一樣。首先我們來看為什麼有這樣的需求?
作為一個無狀態的服務,它可以長時間運行(某種程度上,這有點像Storm)而不必下線。為了充分榨取CPU的價值。我們可能希望在一次服務的生命週期內讓它運行多個異構服務(所謂異構任務,就是不同性質的任務)。因此我們有必要將多個異構任務區分開來,而這個手段就是分區
。說它不同於kafka的原因是:它更多是一種邏輯上的劃分,而不是kafka物理上按分區儲存訊息。我們來看一個分區隔離了哪些東西:
partition.root=p0,p1p0.matrix=redisp0.host=127.0.0.1p0.port=6379p0.class=com.github.horae.task.RedisTaskp1.matrix=disquep1.host=127.0.0.1p1.port=7711p1.class=com.github.horae.task.DisqueTask
- matrix : 哪種隊列實現服務,目前支援
disque
/redis
- host : 佇列服務器的host
- port : 佇列服務器的port
- class : 處理隊列任務的實作類別的完全限定名
從上面的隔離方式來看,這裡的分區也能做到對任務隊列的物理隔離。上面配置了兩個分區,兩個分區分別對應了兩種佇列服務。分區跟佇列服務的對應關係沒有限制,甚至多個分區對應一個佇列服務器也可行,因為還有一個分區到隊列名稱的映射關係:
如:
綜述:分區隔離了異構任務的隊列,而佇列儲存體於何種佇列服務、儲存於何處、以及任務的處理邏輯完全取決於配置。
上面的解析明確了分區跟任務處理類的對應關係。為了便於管理,一個分區也有其獨立的線程池來將異構任務的線程隔離開來。
編寫任務處理器
在你編寫一個任務處理器之前,你應該意識到你編寫的任務處理器充當的是隊列的消費者
。接下來你需要瞭解的是,你編寫的任務處理器將在一個線程池中運行,而線程池的管理,需要你關心,但你需要知道:一個任務隊列將會對應一個線程
。你需要知道的就是這麼多,下面來編寫一個任務處理器:
- 首先你需要建立一個新的maven工程
- 在horae發布包的庫目錄下(
./horae/libs
)找到以horae
開頭的jar檔案,加入到你的maven依賴中,只是一個本地依賴:
<dependency> <groupId>com.github.horae</groupId> <artifactId>horae</artifactId> <version>0.1.0</version> <scope>system</scope> <systemPath>/usr/local/horae/libs/horae-0.1.0.jar</systemPath> </dependency>
- 你需要建立一個類,繼承
TaskTemplate
,並實現run
方法,下面是一個模板:
public void run() { try { signal.await(); //implement task process business logic } catch (InterruptedException e) { } }
public RedisTestTask(CountDownLatch signal, String queueName, Map<String, Object> configContext, Partition partition, TaskExchanger taskExchanger) { super(signal, queueName, configContext, partition, taskExchanger); }
在run方法的第一句,你需要調用一個CountDownLatch
執行個體的await
方法來將其阻塞住。解釋一下,為什麼需要這麼做?
其實,每個服務在啟動的時候,都會立即讀取redis內配置的隊列,並初始化線程池,進入執行就緒狀態。這一步,所有的服務,無論是Master,Slave都是一樣的。但區別就區別在這句:
signal.await();
當啟動的是master節點,那麼該signal會立即釋放訊號(通過signal.countDown()
),所有任務處理器都立即開始執行。
而啟動的是slave節點,則將會一直在上面這句代碼這裡阻塞,直到master下線,而該節點競爭到master之後,會立即釋放解除阻塞訊號,後續代碼會立即執行。
因此這麼做可以使得在master下線之後,所有Slave都以最快的速度進入任務執行狀態,雖然對一些Slave節點而言,這有些浪費系統資源。
編譯工程並打包jar,注意不用包含上面的maven依賴,它已經存在於horae
可執行檔類庫中。
將產生的jar放置於./horae/libs/
下,它將會被自動添加到classpath
中
編輯設定檔./horae/conf/partition.properties
,建立/修改一個分區的p{x}.class
,值為你剛剛編寫的任務實作類別的完全限定名。
安裝部署
以下安裝步驟在Mac OS X系統驗證通過(Linux系類似,但存在一些不同)。Mac使用者需要預裝Homebrew
brew install jsvc
brew install redis
因為disque目前還沒有一個穩定的版本,所以暫時被homebrew暫存在head-only 倉庫中,安裝命令略有不同:
brew install --HEAD homebrew/head-only/disque
mvn assembly:assembly
cd ${project_baseDir}/target/horae.zip /usr/localunzip /usr/local/horae.zip
sudo vi /usr/local/horae/bin/horae.sh
sudo vi /usr/local/horae/conf/${service/redisConf/partition}.properties
sudo sh /usr/local/horae/bin/horae.sh ${start/stop/restart}
注意事項
- conf下的service.properties中的配置項
master
在所有節點中只能有一個被設定為true。如果它下線,將不能以master的身份再次啟動。
- 因為jsvc需要寫進程號(pid),所以盡量以系統管理員身份執行,將horae.sh裡的
user
配置為root
,並以sudo
執行
關於disque
目前disque仍處於alpha版本,命令也還在調整中。雖然已被支援,但無論是disque的server以及其java client:jedisque
都存在bug,因此暫時不推薦使用,請至少等到發布stable版本再使用。
自實現的jedisque
串連池。目前jedisque的用戶端還沒有提供串連池機制,它跟redis的主流java client:jedis
出自同一個開發人員手筆。考慮到jedis
內部使用的是apache commons-pool
實現串連池機制,在實現jedisque
的時候也使用的是同樣的方案,等jedisque
官方提供串連池之後,會採用官方串連池。
disque
的開發過程中,對命令和命令參數可能會進行調整,horae
也會對此進行跟進。雖然,disque
的stable版本還未發布,但redis作者的水準和口碑有目共睹,所以你有理由相信它能給你帶來驚喜。
本項目的開源地址:https://github.com/yanghua/horae
更多內容請訪問:http://vinoyang.com
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
一個基於redis和disque實現的輕量級非同步任務執行器