【Python】輕量級分散式任務排程系統-RQ

datapeng發表於2016-09-08
一 前言   
   Redis Queue 一款輕量級的P分散式非同步任務佇列,基於Redis作為broker,將任務存到redis裡面,然後在後臺執行指定的Job。就目前而言有三套成熟的工具celery,huey ,rq 。按照功能和使用複雜度來排序的話也是 celery>huey>rq. 因為rq 簡單,容易上手,所以自己做的系統也會使用RQ作為分散式任務排程系統。
二 安裝 
   因為RQ 依賴於Redis 故需要安裝版本>= 2.6.0.具體安裝方法請參考《Redis初探》。*nix 系統環境下安裝RQ:
  1. pip install rq
無需其他配置即可以使用RQ。
三 原理
   RQ 主要由三部分構成 Job ,Queues,Worker 構成。job也就是開發定義的函式用來實現具體的功能。呼叫RQ 把job 放入佇列Queues,Worker 負責從redis裡面獲取任務並執行,根據具體情況返回函式的結果。
3.1 關於job
   一個任務(job)就是一個Python物件,具體表現為在一個工作(後臺)程式中非同步呼叫一個函式。任何Python函式都可以非同步呼叫,簡單的將函式與引數追加到佇列中,這叫做入隊(enqueueing)。
3.2 關於Queue
   將任務加入到佇列之前需要初始化一個連線到指定Redis的Queue

  1. q=Queue(connection=redis_conn)
  2. from rq_test import hello
  3. result = q.enqueue(hello,'yangyi')
   queue有如下屬性:
   timeout :指定任務最長執行時間,超過該值則被認為job丟失,對於備份任務 需要設定一個比較長的時間 比如24h。
   result_ttl :儲存任務返回值的有效時間,超過該值則失效。
   ttl :specifies the maximum queued time of the job before it'll be cancelled
   depends_on :specifies another job (or job id) that must complete before this job will be queued
   job_id : allows you to manually specify this job's job_id
   at_front :will place the job at the front of the queue, instead of the back
   kwargs and args : lets you bypass the auto-pop of these arguments, ie: specify a timeout argument for the underlying job function.
  需要關注的是 depends_on ,透過該屬性可以做級聯任務A-->B ,只有當A 執行成功之後才能執行B .
  透過指定佇列的名字,我們可以把任務加到一個指定的佇列中:
  1. q = Queue("low", connection = redis_conn)
  2. q.enqueue(hello, "楊一"
 對於例子中的Queue("low"),具體使用的時候可以替換"low"為任意的複合業務邏輯名字,這樣就可以根據業務的需要靈活地歸類的任務了。一般會根據優先順序給佇列命名(如:high, medium, low).
 如果想要給enqueue傳遞引數的情況,可以使用enqueue_call方法。在要傳遞超時引數的情況下:
  1. q = Queue("low", connection = redis_conn)
  2. q.enqueue_call(func=hello, args= ("楊一",),timeout = 30)
3.3 關於worker
   Workers將會從給定的佇列中不停的迴圈讀取任務,當所有任務都處理完畢就等待新的work到來。每一個worker在同一時間只處理一個任務。在worker中,是沒有併發的。如果你需要併發處理任務,那就需要啟動多個worker。
   目前的worker實際上是fork一個子程式來執行具體的任務,也就是說rq不適合windows系統。而且RQ的work是單程式的,如果想要併發執行佇列中的任務提高執行效率需要使用threading針對每個任務進行fork執行緒。
   worker的生命週期有以下幾個階段組成:
   1 啟動,載入Python環境
   2 註冊,worker註冊到系統上,讓系統知曉它的存在。
   3 開始監聽。從給定的redis佇列中取出一個任務。如果所有的佇列都是空的且是以突發模式執行的,立即退出。否則,等待新的任務入隊。
   4 分配一個子程式。分配的這個子程式在故障安全的上下文中執行實際的任務(呼叫佇列中的任務函式)
   5 處理任務。處理實際的任務。
   6 迴圈。重複執行步驟3。
四 如何使用
   簡單的開發一個deamon 函式,用於後端非同步呼叫,注意任意函式都可以加入佇列,必須能夠在入隊的時候 被程式訪問到。
 
  1. #!/usr/bin/env python
  2. #-*- coding:utf-8 -*-
  3. def hello(name):
  4.     print "hello ,%s"%name
  5.     ip='192.168.0.1'
  6.     num=1024
  7.     return name,ip,num
  8. def workat(name):
  9.     print "hello %s ,you r workat youzan.com "%(name)
4.1 構建佇列,將任務物件新增到佇列裡面

  1. >>> from redis import Redis,ConnectionPool
  2. >>> from rq import Queue
  3. >>> pool = ConnectionPool(db=0, host='127.0.0.1', port=6379,
  4. ... password='yangyi')
  5. >>> redis_conn = Redis(connection_pool=pool)
  6. >>> q=Queue(connection=redis_conn)
  7. >>> from rq_test import hello
  8. >>>
  9. >>> result = q.enqueue(hello,'yangyi')
  10. >>> result = q.enqueue(hello,'youzan.com')
先例項化一個Queue類q,然後透過enqueue方法釋出任務。第一個引數是執行的函式名,後面是函式執行所需的引數,可以是args也可以是kwargs,案例中是一個字串。
然後會返回一個Job類的例項,後面會具體介紹Job類的例項具體的api。

4.2啟動worker ,從日誌上可以看到執行了utils.hello('yangyi') utils.hello('youzan.com') 。當然這個只是簡單的呼叫介紹,生產環境還要寫的更加健壯,針對函式執行的結果進行相應的業務邏輯處理。 
  1. root@rac2:~# >python woker.py
  2. 23:44:48 RQ worker u'rq:worker:rac2.3354' started, version 0.6.0
  3. 23:44:48 Cleaning registries for queue: default
  4. 23:44:48
  5. 23:44:48 *** Listening on default...
  6. 23:44:48 default: utils.hello('yangyi') (63879f7c-b453-4405-a262-b9a6b6568b68)
  7. hello ,yangyi
  8. 23:44:48 default: Job OK (63879f7c-b453-4405-a262-b9a6b6568b68)
  9. 23:44:48 Result is kept for 500 seconds
  10. 23:44:48
  11. 23:44:48 *** Listening on default...
  12. 23:45:12 default: utils.hello('youzan.com') (e4e9ed62-c476-45f2-b66a-4b641979e731)
  13. hello ,youzan.com
  14. 23:45:12 default: Job OK (e4e9ed62-c476-45f2-b66a-4b641979e731)
  15. 23:45:12 Result is kept for 500 seconds
需要說明的是其實 worker的啟動順序應該在job放入佇列之前,一直監聽rq裡面是否有具體的任務,當然如果worker晚於job 加入佇列啟動,job的狀態會顯示為 queued 狀態。
4.3 檢視作業執行的情況
當任務加入佇列,queue.enqueue()方法返回一個job例項。其定義位於rq.job檔案中,可以去檢視一下它的API,主要用到的API有:
  1. >>> from rq import job
  2. >>> job = q.enqueue(hello,'youzan.com')
  3. >>> job.get_id() ##獲取任務的id ,如果沒有指定 ,系統會自動分配一個隨機的字串。
  4. u'17ad0b3a-195e-49d5-8d31-02837ccf5fa6'
  5. >>> job = q.enqueue(hello,'youzan.com')
  6. >>> print job.get_status() ##獲取任務的處理狀態
  7. finished
  8. >>> step1=q.enqueue(workat,) ##故意不傳遞引數,讓函式執行失敗,則獲取的狀態值是 failed
    >>> print step1.get_status()
    failed
  9. >>> print job.result # 當任務沒有執行的時候返回None,否則返回非空值,如果 函式 hello() 有return 的值,會賦值給result
  10. None
  11. 當我們把worker 監聽程式停止,然後重新發布任務,檢視此時任務的在佇列的狀態,會顯示為 queued
  12. >>> job = q.enqueue(hello,'youzan')
  13. >>> print job.get_status()
  14. queued
  15. >>> print job.to_dict() #把job例項轉化成一個字典,我們主要關注狀態。
  16. {u'origin': u'default', u'status': u'queued', u'description': u"rq_test.hello('youzan')", u'created_at': '2016-09-06T08:00:40Z', u'enqueued_at': '2016-09-06T08:00:40Z', u'timeout': 180, u'data': '\x80\x02(X\r\x00\x00\x00rq_test.helloq\x01NU\x06youzanq\x02\x85q\x03}q\x04tq\x05.'}
  17. >>> job.cancel() # 取消作業,儘管作業已經被執行,也可以取消
  18. >>> print job.to_dict()
  19. {u'origin': u'default', u'status': u'queued', u'description': u"rq_test.hello('youzan')", u'created_at': '2016-09-06T08:00:40Z', u'enqueued_at': '2016-09-06T08:00:40Z', u'timeout': 180, u'data': '\x80\x02(X\r\x00\x00\x00rq_test.helloq\x01NU\x06youzanq\x02\x85q\x03}q\x04tq\x05.'}
  20. >>> print job.get_status()
  21. queued
  22. >>>
  23. >>> job.delete() # 從redis佇列中刪除該作業
  24. >>> print job.get_status()
  25. None
  26. >>> print job.to_dict()
  27. {u'origin': u'default', u'description': u"rq_test.hello('youzan')", u'created_at': '2016-09-06T08:00:40Z', u'enqueued_at': '2016-09-06T08:00:40Z', u'timeout': 180, u'data': '\x80\x02(X\r\x00\x00\x00rq_test.helloq\x01NU\x06youzanq\x02\x85q\x03}q\x04tq\x05.'}
五 參考文章
[1]  
[2] 翻譯 - Python RQ Job 
[3] 翻譯 - Python RQ Workers  
[4]  這位博主寫了很多rq相關的實踐經驗,值得參考。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/29371470/viewspace-2124677/,如需轉載,請註明出處,否則將追究法律責任。

相關文章