一個基於redis和disque實現的輕量級非同步任務執行器

來源:互聯網
上載者:User

標籤:redis   disque   非同步   高效能   

簡介

horae是一個基於redisdisque實現的輕量級高效能的非同步任務執行器,它的核心是disque提供的任務隊列,而隊列有先進先出的時序關係,顧得名:horae

horae : 時序女神,希臘神話中司掌季節時間和人間秩序的三女神,又譯“荷萊”。

horae的關注點不是佇列服務的實現本身(已經有不少佇列服務的實現了),而是希望藉助於redisdisque提供的純記憶體的高效能的隊列機制,實現一個非同步任務執行器。它可以自由配置任務來自哪種佇列服務,它不關注任務執行的最終狀態(它寫向哪裡)或與哪個系統互動,它給你提供一個執行器以及簡單地編寫任務執行邏輯的方式。

取決於需求,這個執行器在要求不高的時候,只需要一個單節點的redis伺服器,即可運轉。

如果你願意犧牲一點效能,來換取更高的隊列可靠性保障(這種情況我強烈推薦你使用AMQP協議以及它的開源隊列實現:RabbitMQ)。如果你想這樣,那麼這個執行器也是可用的,只是你需要自己去實現跟RabbitMQ互動的細節。你可以用它串連各種其他隊列來消費訊息並執行任務,它具有充分的擴充性與自由度。但我仍然推薦你使用disque

適用情境搶購/秒殺

搶購業務是典型的短時高並發情境,傳統行業裡的類似於學生選課也可以歸結這類情境。

社交關係處理

純記憶體計算/計數器的情境,比如把社交系統裡的好友、關係搬到記憶體中處理。

耗時的web請求

常見的耗時web請求,比如產生PDF網頁抓取資料備份郵件/簡訊發送等。

分布式系統前端緩衝隊列

將它置於應用伺服器之後,核心服務之前,作為請求的緩衝隊列使用。

概括起來就是伺服器峰值扛壓非同步處理純記憶體計算,當然你把它用成普通隊列也是可以的。

高效能

目前支援disqueredis這兩種佇列服務(主推disqueredis的隊列暫時以list資料介面的lpush&brpop實現,但它不是高可靠的,並且沒有ack機制)。這兩種純記憶體的隊列首先保證了消費任務的效能。具體任務的執行效能,取決於使用情境,這裡分析兩種情境:

純記憶體&單線程&無鎖

如果任務處理器消費的訊息是完全儲存於記憶體中的,那麼需要盡量將同構的各任務訪問的資料進行隔離(隔離的手段是對key劃分命名空間),如果實在沒辦法隔離,可以使用單隊列單線程無鎖的處理方式。

通用&多線程&多隊列

如果是通用的應用情境,比如訪問資料庫,因為資料庫有成熟的資料一致性保證。所以,你可以將任務劃分到多個不同的隊列,並利用多個線程來並發執行以加快任務的處理效率。

當然最推薦的使用方式是:用redis作為配置、協調、管控中心,用disque做佇列服務,任務需要訪問的資料儘可能儲存於redis中。

高可用一主多從

執行器在運行時實行的是:Master單節點運行,多個Slave做Standby的機制來保證服務的可用性。事實上,從Master下線到其中一個Slave成功競選為Master需要數個心跳周期的時間。因為執行器作為隊列的消費者跟隊列是完全解耦的,所以短暫的暫停消費對整個系統的可用性不會產生太大影響。

心跳機制

Master跟Slave之間通過redis-Pubsub來維持心跳。目前的設計是Master單向publish心跳,SlavesubscribeMaster的心跳。這麼設計的原因是簡單,並且考慮到每個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
  • TaskExchangerManagercreateTaskExchanger方法內加入新的分支判斷。
  • partition.properties下可以配置新的partition,在matrix中指定RabbitMQ

需要注意的是:TaskExchangerdequeue介面方法,預設的行為是block形式的。如果你擴充的隊列不支援block形式的消費,那可能需要你自己實現,實現的方式可以藉助於java.util.concurrent.BlockingQueue

多種可靠性層級

隊列的可靠性牽扯到整個分布式系統的可靠性,這是一個無法迴避的問題。如果你說用redis實現的隊列,是否能做到既保持高效能又能兼具高可靠,答案是不能。或者說它不是一個專業的佇列服務(不然redis的作者也沒有必要再另起disque項目了)。如果從可靠性的角度而言,我給幾個主流的佇列服務器(或者可以提供佇列服務)的排名是:RabbitMQ > Kafka > Disque > Redis。雖然這個執行器內建支援了disqueredis作為隊列的實現,但它跟你選擇的佇列服務沒有非常緊的耦合關係,你可以選擇其他佇列服務,通常你只需要實現這麼幾個功能入隊訊息出隊訊息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

  • 安裝jsvc
brew install jsvc
  • 安裝redis
brew install redis
  • 安裝disque

因為disque目前還沒有一個穩定的版本,所以暫時被homebrew暫存在head-only 倉庫中,安裝命令略有不同:

brew install --HEAD homebrew/head-only/disque
  • horae源碼編譯、打包
mvn assembly:assembly
  • 拷貝打包檔案到目標檔案夾,並解壓縮
cd ${project_baseDir}/target/horae.zip /usr/localunzip /usr/local/horae.zip
  • 配置可執行檔,主要是命令與路徑
sudo vi /usr/local/horae/bin/horae.sh
  • 配置conf下的設定檔
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實現的輕量級非同步任務執行器

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.