python第三方庫系列之十四--集羣化部署定時任務apscheduler庫

        如果將定時任務部署在一臺服務器上,那麼這個定時任務就是整個系統的單點,這臺服務器出現故障的話會影響服務。對於可以冗餘的任務(重複運行不影響服務),可以部署在多臺服務器上,讓他們同時執行,這樣就可以很簡單的避免單點。但是如果任務不允許冗餘,最多只能有一臺服務器執行任務,那麼前面的方法顯然行不通。本篇文章就向大家介紹如何避免這種互斥任務的單點問題,最後再介紹一下基於APScheduler的分佈式定時任務框架,這個框架是通過多個項目的實踐總結而成的。

        對於運行在同一臺服務器上的兩個進程,可以通過加鎖實現互斥執行,而對於運行在多個服務器上的任務仍然可以通過用加鎖實現互斥,不過這個鎖是分佈式鎖。這個分佈式鎖並沒有那麼神祕,實際上只要一個提供原子性的數據庫即可。比如,在數據庫的locks表裏有一個記錄(lock record),包含屬性:

name:鎖的名字,互斥的任務需要用名字相同的鎖。
active_ip:持有鎖的服務器的ip。
update_time:上次持有鎖的時間,其他非活躍的服務器通過這個屬性判斷活躍的服務器是否超時,如果超時,則會爭奪鎖。

        一個持有鎖的服務器通過不斷的發送心跳,來更新這個記錄,心跳的內容就是持有鎖的時間戳(update_time),以及本機ip。也就是說,通過發送心跳來保證當前的服務器是活躍的,而其他服務器通過lock record中的update_time來判斷當前活躍的服務器是否超時,一旦超時,其他的服務器就會去爭奪鎖,接管任務的執行,併發送心跳更新active_ip。

        通過上面描述,這個框架中最重要的兩個概念就是分佈式鎖和心跳。下面看一下分佈式定時任務框架中是如何實現這兩點的。當然,這個框架依賴於APScheduler,所以必須安裝這個模塊,具體APScheduler的介紹見我的上一篇文章:python第三方庫系列之十三--定時任務apscheduler庫,因爲依賴APScheduler,所以這個框架很簡單,只有一個類:

import datetime
import socket
import struct
import fcntl

from apscheduler.scheduler import Scheduler

def get_ip(ifname):
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    return socket.inet_ntoa(fcntl.ioctl(
        s.fileno(),
        0x8915,
        struct.pack('256s', ifname[:15])
    )[20:24])


class MutexScheduler(Scheduler):
    def __init__(self, local_ip, gconfig={}, **options):
        Scheduler.__init__(self, gconfig, **options)
        #self.ip = get_ip(settings.NETWORK_INTERFACE)
        self.ip = local_ip

    def mutex(self, lock=None, heartbeat=None, lock_else=None,
              unactive_interval=datetime.timedelta(seconds=10)):

        def mutex_func_gen(func):
            def mtx_func():
                if lock:
                    lock_rec = lock()
                    now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
                    now = datetime.datetime.strptime(now, "%Y-%m-%d %H:%M:%S")
                    # execute mutex job when the server is active, or the other server is timeout.
                    if not lock_rec or lock_rec['active_ip'] == self.ip or (
                        lock_rec['update_time'] and now - lock_rec['update_time'] >= unactive_interval):
                        if lock_rec:
                            del lock_rec['active_ip']
                            del lock_rec['update_time']
                        if not lock_rec:
                            lock_rec = {}
                        lock_attrs = func(**lock_rec)
                        if not lock_attrs:
                            lock_attrs = {}
                            # send heart beat
                        heartbeat(self.ip, now, **lock_attrs)
                    else:
                        lock_else(lock_rec)
                else:
                    func()

            return mtx_func

        self.mtx_func_gen = mutex_func_gen

        def inner(func):
            return func

        return inner

    def cron_schedule(self, **options):
        def inner(func):
            if hasattr(self, 'mtx_func_gen'):
                func = self.mtx_func_gen(func)
            func.job = self.add_cron_job(func, **options)
            return func

        return inner

        mutex方法是核心,通過裝飾器的方式提供互斥功能。在使用時:

@sched.mutex(lock = my_lock, heartbeat = my_heartbeat)  
@sched.cron_schedule(second = '*')  
def my_job(**attrs):  
    print 'my_job ticks'  
#mutex裝飾器必須用在cron_schedule裝飾器之前,mutex主要是組裝job。mutex的參數有:
#lock:函數,用於獲取鎖記錄(lock record),函數原型:lock()。lock的返回值時dict,就是鎖記錄內容。
#heartbeat:函數,用於發出心跳,函數原型:heartbeat(ip, now, **attrs)。ip是本機ip;now是當前時間戳;attrs是一個dict,用於在鎖記錄中存放一些其他用戶自定義信息。
#lock_else:函數,在沒有獲得鎖時執行,函數原型:lock_else(lock_rec)。lock_rec是鎖記錄,包含active_ip,update_time以及用戶自定義的屬性。
#unactive_interval:datetime.timedelta類型,超時時間,也就是說當前時間減去update_time大於unactive_interval的話,就代表超時。類中默認值unactive_interval=datetime.timedelta(seconds=10)是默認10s。
#在使用這個類時,必須實現自己的lock,heartbeat以及lock_else函數。

        job的原型是job(**attrs),attrs就是存放在鎖記錄中的用戶自定義屬性,job可以有dict類型的返回值,這個返回值會存入鎖記錄中。

        下面,看一下具體使用的例子,使用的mongodb存放分佈式鎖。

import apscheduler.events  
import datetime  
import time  
import pymongo  
import sys  
import mtxscheduler  
  
sched = mtxscheduler.MutexScheduler()  

mongo = pymongo.Connection(host = '127.0.0.1', port = 27017)  
lock_store = mongo['lockstore']['locks']  
  
def lock():  
    conn = connect_adms_db()
	lock_name = 'xxx'
    sql = "select name, active_ip, update_time from locks where name='%s';" % lock_name
    log.info("sql:%s" % sql)
    res = conn.execute(sql)[0]
    conn.close()
    tuple = {'name': res["name"], 'active_ip': res["active_ip"], 'update_time': res["update_time"]}
    return tuple
  
def hb(ip, now, **attrs):  
    attrs['active_ip'] = ip  
    attrs['update_time'] = now  
	conn = connect_adms_db()
	lock_name = 'xxx'
    sql = "update locks set active_ip='%(ip)s', update_time='%(update_time)s' " \
          "where name='%(name)s'" % {'ip': ip, 'update_time': now, 'name': lock_name}
    log.info("sql:%s" % sql)
    res = conn.execute(sql)
    conn.close()
  
def le(lock_rec):  
    if lock_rec:  
        print 'active ip', lock_rec['active_ip']  
    else:  
         print 'lock else'  
  
i = 0  
 
@sched.mutex(lock = lock, heartbeat = hb, lock_else = le)  
@sched.cron_schedule(second = '*')  
def job(**attr):  
    global i  
    i += 1  
    print i  
  
def err_listener(ev):  
    if ev.exception:  
        print sys.exc_info()  
              
sched.add_listener(err_listener, apscheduler.events.EVENT_JOB_ERROR)  
  
sched.start()  
time.sleep(10)  

        這個任務很簡單就是定時打印整數序列。同時在兩臺服務器上部署運行,可以發現只有一臺服務器會輸出整數序列。
        還有使用redis,mongodb存儲鎖。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章