雲計算之celery+django+supervisor實現定時任務

一. celery概要
    1. 概念
        分佈式任務隊列,django中所有需要用到異步操作的,或者定時操作的,都可以使用celery
    2. 組件
        Celery Beat : 任務調度器. Beat 進程會讀取配置文件的內容, 週期性的將配置中到期需要執行的任務發送給任務隊列.
        Celery Worker : 執行任務的消費者, 通常會在多臺服務器運行多個消費者, 提高運行效率.
        Broker : 消息代理, 隊列本身. 也稱爲消息中間件.
        Producer : 任務生產者. 調用 Celery API , 函數或者裝飾器, 而產生任務並交給任務隊列處理的都是任務生產者.
        Result Backend : 任務處理完成之後保存狀態信息和結果, 以供查詢.
    3. 工作流程
        producer發佈任務,傳遞給broker,borker 傳遞給 beat,beat 將任務調度給worker,worker執行任務並將結果存入數據庫(redis等)

二. celery搭建
    1. 安裝redis
        官網下載對應版本的redis tar包
        tar -xf redis-3.2.12.tar.gz
        cd redis-3.2.12
        make
        make install
        ./utils/install_server.sh
    2. 安裝celery
        pip3.6 install celery
    3. 使用celery簡單測試
        1). 編寫tasks.py
            import time
            from celery import Celery
            # 使用redis作爲broker(消息隊列)
            celery = Celery('tasks', broker='redis://localhost:6379/0')
            # 創建任務函數
            @celery.task
            def sendmail(mail):
                print("sending mail to %s..."%(mail['to']))
                time.sleep(2.0)
                print('mail sent.')

        2). 進入tasks.py目錄啓動 celery 處理任務
            celery  -A tasks worker --loglevel=info
            說明:celery對tasks任務啓動一個worker線程,日誌級別爲info
        3). 另開命令行進入tasks.py目錄編寫測試腳本 test.py
            from tasks import sendmail
            import time
            while 1:
              print(sendmail.delay(dict(to='[email protected]')))
              time.sleep(1)
            調用delay函數,是將任務函數加入到隊列中
        可以看到worker線程有對應的處理輸出,測試celery完成,目前celery可以使用了,但是還不能放入後臺運行,可以使用supervisor進行後臺運行管理
    4. supervisor 安裝使用
        1). 使用pip安裝supervisor
            pip3.6 install supervisor
        2). 生成supervisor配置文件
            echo_supervisord_conf > /etc/supervisord.conf
        3). 修改supervisor配置文件
            vim /etc/supervisord.conf
                # supervisor子配置目錄
                [include]
                files = /etc/supervisor/*.conf
                # supervisor web管理界面
                [inet_http_server]
                port=192.168.89.133:9001
                username=user
                password=123
        4). 創建子配置文件目錄和子配置文件
            mkdir /etc/supervisor
            vim /etc/supervisor/test.conf
                [program:testpy]
                command=python /opt/celery/test1.py              ; 要執行的程序
                ;process_name=%(program_name)s
                ;numprocs=1
                directory=/opt/celery/              ; 要進入的項目目錄(先進入,再執行)
                ;umask=022
                priority=999                             ; 任務要執行的優先級
                startsecs = 5                            ; 啓動 5 秒後沒有異常退出,就當作已經正常啓動了
                autorestart = true                       ; 程序異常退出後自動重啓
                startretries = 3                         ; 啓動失敗自動重試次數,默認是 3
                stdout_logfile = /opt/celery/supervisor.log
        5). 編寫測試腳本/opt/celery/test1.py
            import time
            while 1:
              with open('/tmp/bb.txt','a') as fobj:
                fobj.write('111\n')
              time.sleep(1)
        3). 通過配置文件啓動supervisor
            supervisord -c /etc/supervisord.conf
        4). 查看運行狀態
            supervisorctl status
                testpy                           RUNNING   pid 61435, uptime 0:02:32
            注意報錯:gave up: testpy entered FATAL state, too many start retries too quickly
            原因:
                1. 腳本啓動比較慢,將配置文件中startsecs調大一些
                2. 腳本運行退出,非死循環狀態,也會報這個錯 , 這種情況也不算報錯,執行完成自然退出,配置文件中設置了重啓3次,重啓執行完後自然又會退出
            supervisorctl stop testpy 停止任務
            supervisorctl start testpy 啓動任務
    5. 使用 supervisor 控制 celery 半個小時更新一次
        1). 將第4步的test1.py腳本更換成第3步的test.py腳本,celery就被supervisor控制
        2). 定義時間,半個小時更新一次腳本
           a. 創建supervisor子配置文件[調度器] /etc/supervisor/get_version_beat.conf
                [program:get_version_beat]
                # 執行命令
                directory=/opt/gits/orange
                command = /root/venv/bin/celery --workdir=/opt/gits/orange -A webhook beat -l info
                # 日誌配置
                loglevel = info
                stdout_logfile = /tmp/get_version_beat.log
                stderr_logfile = /tmp/get_version_beat.log
                stdout_logfile_maxbytes = 50MB
                stdout_logfile_backups = 1
                # 給每個進程命名,便於管理
                process_name = get_version_beat%(process_num)s
                # 啓動的進程數,設置成雲服務器的vCPU數
                numprocs_start = 1
                numprocs = 1
                # 設置自啓和重啓
                autostart = true
                autorestart = true
                redirect_stderr = True
           b. 創建supervisor子配置文件[消費者] /etc/supervisor/get_version_worker.conf
                [program:get_version_worker]
                # 執行用戶
                # user = root
                # 執行的命令
                directory=/opt/gits/orange
                command = /root/venv/bin/celery --workdir=/opt/gits/orange -A webhook worker -l info
                # 日誌文件配置
                loglevel = info
                stdout_logfile = /tmp/get_version_worker.log
                stderr_logfile = /tmp/get_version_worker.log
                stdout_logfile_maxbytes = 50MB
                stdout_logfile_backups = 1
                # 給每個進程命名,便於管理
                process_name = get_version_worker%(process_num)s
                # 啓動的進程數,設置成雲服務器的vCPU數
                numprocs_start = 1
                numprocs = 1
                # 設置自啓和重啓
                autostart = true
                autorestart = true
                redirect_stderr = True
           c. 到項目根目錄配置celery.py文件
                ... ...
                app.conf.update(
                    CELERYBEAT_SCHEDULE = {
                        ... ...
                        'get_version':{
                            'task': 'celery_app.tasks.get_version',
                            'schedule': crontab(minute='1', hour='1', day_of_week='*', day_of_month='*', month_of_year='*'),
                        }
                    }
                }
           d. 進入到celery_app目錄,進行功能編寫
                vim get_version.py
                    import paramiko
                    import requests
                    import datetime
                    import redis
                    import json
                    import os
                    import subprocess
                    from threading import Timer
                    import logging

                    logger = logging.getLogger('t3logger')

                    class GetVersion():
                        def __init__(self):
                            pass

                        def par_ver(self, host0, app_name):
                            client = paramiko.SSHClient()
                            client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
                            client.connect(hostname=host0, port=2222, username='dc')
                            stdin, stdout, stderr = client.exec_command("awk '/jfrog/' /data/scripts/deploy_%s.sh | tail -1 | awk '{print $4}' | awk -F/ '{print $4}'" % (app_name))
                            out = stdout.read().decode('utf-8')
                            err = stderr.read().decode('utf-8')
                            if out == '':
                                out = '0'
                            client.close()
                            out = out.strip()
                            return out

                        def chaoshi(self, args, timeout):
                            p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                            timer = Timer(timeout, lambda process: process.kill(), [p])
                            try:
                                timer.start()
                                stdout, stderr = p.communicate()
                                return_code = p.returncode
                                if stdout != b'':
                                    return True
                                else:
                                    return False
                            finally:
                                timer.cancel()

                        def main(self):
                            os.system('rm -f /tmp/hosts_questions.txt')
                            # 此處爲數據接口,此功能不贅述
                            all_keys = requests.get("http://10.0.0.1:10080/assets/inventory/--list/None/")
                            all_objs = all_keys.json()
                            i = 1
                            objs = []
                            for item in all_objs:
                                if item == 'all' or item == '_meta':
                                    continue
                                if 't3_data_apps' in item:
                                    env = item.split('_ktz_data_apps_')[0]
                                    center = 'ktz_data_apps'
                                    app_name = item.split('_ktz_data_apps_')[-1]
                                elif 'ktz_m' in item:
                                    env = item.split('_ktz_m_')[0]
                                    center = 'ktz_m'
                                    app_name = item.split('_ktz_m_')[-1]
                                else:
                                    env = item.split('_')[0]
                                    center = item.split('_')[1]
                                    app_name = item.split('_')[-1]
                                hosts = all_objs[item]['hosts']
                                str = ''
                                for host in hosts:
                                    str += host + ','
                                hosts_str = str
                                host0 = all_objs[item]['hosts'][0]
                                result = self.chaoshi(['telnet', host0, '53742'], 2)
                                if result == False:
                                    os.system('echo %s >> /tmp/hosts_questions.txt' % (host0))
                                    continue
                                ver = self.par_ver(host0, app_name)
                                now = datetime.datetime.now().strftime('%Y-%m-%d')
                                objs.append([i, center, app_name, ver, hosts_str, now, env])
                                i += 1
                            red = redis.Redis(host='localhost', port=6379, db=1)
                            objs_json = json.dumps(objs)
                            red.set('versions', objs_json)


                    if __name__ == '__main__':
                        gv = GetVersion()
                        gv.main()
           e. 進入到celery_app目錄的tasks.py文件,引入get_version功能
                from celery_app.get_version import GetVersion
                @shared_task
                def get_version():
                    """
                    週期性收集服務器上對應的服務版本信息
                    :return:
                    """
                    info = '週期性收集服務器上對應的服務版本信息'
                    logger.info(info)
                    gv = GetVersion()
                    gv.main()
           f. 啓動django服務
           g. 重啓supervisord服務
                killall -9 supervisord
                supervisord -c /etc/supervisord.conf
    6. 問題排錯
           問題1:查看日誌,發現beat啓動錯誤
                tail -10f /tmp/get_version_beat.log
                    celery beat v4.3.0 (rhubarb) is starting.
                    ERROR: Pidfile (celerybeat.pid) already exists.
                排錯思路:不用創建beat調度器,因爲已經存在了
                    1. 去掉get_version_beat.conf
                        rm -f /etc/supervisor/get_version_beat.conf
                    2. 啓動supervisor查看是否還有錯誤
                        tail -10f /tmp/get_version_worker.log
                            [2019-12-16 17:38:34,897: INFO/MainProcess] Connected to redis://127.0.0.1:6379/8
                            [2019-12-16 17:38:34,903: INFO/MainProcess] mingle: searching for neighbors
                            [2019-12-16 17:38:35,945: WARNING/MainProcess] /root/venv/lib/python3.6/site-packages/celery/app/control.py:54: DuplicateNodenameWarning: Received multiple replies from node name: celery@k8snode01.
                            Please make sure you give each node a unique nodename using
                            the celery worker `-n` option.
                              pluralize(len(dupes), 'name'), ', '.join(sorted(dupes)),
                            [2019-12-16 17:38:35,946: INFO/MainProcess] mingle: all alone
                            [2019-12-16 17:38:35,954: WARNING/MainProcess] /root/venv/lib/python3.6/site-packages/celery/fixups/django.py:202: UserWarning: Using settings.DEBUG leads to a memory leak, never use this setting in production environments!
                              warnings.warn('Using settings.DEBUG leads to a memory leak, never '
                            [2019-12-16 17:38:35,954: INFO/MainProcess] celery@k8snode01 ready.
                        supervisorctl status
                            get_version_worker:get_version_worker1   RUNNING   pid 7598, uptime 0:01:55
                        啓動正常,並無錯誤,等待運行結果結束
                        雖然沒有大問題,但是看get_version_worker.log日誌,查看報錯 UserWarning: Using settings.DEBUG leads to a memory leak,主項目settings配置文件debug改爲false即可
           問題2:結果並沒有實現,沒有創建/tmp/abc.txt測試文件,沒有將數據導入到redis庫中,不知道原因是啥?
                排錯思路:
                    1. 直接運行 get_version.py 沒有問題,數據也可以導入到redis庫中,說明 get_version.py 腳本沒有問題
                    2. 檢查 celery_app 目錄下 tasks.py 腳本, 引入GetVersion類並實例化調用方法,沒有問題
                    3. 'get_version':{ 冒號後面沒有空格,重新運行
                        tail -10f /tmp/supervisord.log
                            'get_version_worker1' with pid 103842 顯示這個任務已經啓動
                        tail -10f /tmp/get_version_worker.log
                        日誌都沒啥變化,不是這個原因引起的。
                    4. 第二天早上查看,日誌神奇的出現了,而且redis也有了對應的鍵值對,猜測是 celery schedule 的單位問題
                        於是將schedule後面寫上3600(秒)
                        重啓django服務,更新 supervisorctl 配置
                        supervisorctl update
                        supervisorctl reload (重啓supervisord)
                        supervisorctl status
                            get_version_worker:get_version_worker1   RUNNING   pid 32703, uptime 0:02:08
                        啓動時間:9:30;測試時間:10:30-40之間
                        結果:10:50後還沒有數據。
                        分析:收集信息需要時間,1100個服務,每個花費5s收集,總共需要1100*5/60=91分鐘,9:30+91=11:00左右結束,還沒有寫入,11:20查看一次,
                        建議:先測試,收集少量數據,就不會花費太多時間
                        結果:到下午 13:30 redis 裏都沒有數據
                        先排除日誌報錯:debug=false和名稱相同的錯,配置了啓動位置加了 -n get_version_worker
                    5. 查看 supervisorctl 是否可以控制celery
                        使用 gv.main()函數(將字符串傳入reids中) 腳本測試
                    6. 第三天早上查看,redis裏又存入了versions鍵值對,在/tmp下也創建了相應的測試文件abc.txt,時間是今天的9:00
                        思考:supervisorctl 裏的任務壓根沒有運行,但是數據還是創建了,可能是celery自己創建的,並不受supervisor控制
                        排錯:
                            ss -anptu | grep celery
                            發現有很多celery進程在運行,殺死celery和supervisord進程,刪除redis中的鍵值
                            killall -9 celery
                            killall -9 supervisord
                            redis-cli
                            > select 1
                            > keys *
                                1) "versions"
                            > del versions
                            重新啓動django和supervisord
                            supervisord -c /etc/supervisord.conf
                            ss anptu | grep celery ; ss anptu | grep supervisord 查看進程已經存在
                            查看日誌也沒有什麼報錯
                            進入redis並沒有看到數據存入庫中,淚奔
                            /etc/supervisor/下沒有配置beat,添加配置,重啓supervisord和celery,發現版本信息可以存入到redis庫中,踩了好幾天的坑,問題終於解決了。
                    問題排錯總結:
                        看來第5步/etc/supervisor/get_version_beat.conf這個文件還不能刪,要回顧一下celery定時任務原理,如果沒有beat,就相當於沒有了調度器,沒有任務調度器,配置了也不會執行。
三. 總結
    使用任何一項新的技術,一定要將原理搞清楚,弄明白,然後再對照着原理搭建自己的任務,每個環境都不漏下,就很大可能會走通,不會出現什麼問題。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章