文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

手把手教你在Windows下设置分布式队列Celery的心跳轮询

2024-12-03 10:05

关注

1 前言

大家好,我是吴老板。用Celery 官方的话来说,Celery 是一个非常优秀的分布式队列,可应用于分布式共享中间队列和定时任务等等。

2 版本的差异

Celery 有很多个版本,各版本之间的差异可谓不小,比如最新的 Celery6.0 版本在稳定性远不如 Celery4.0,所以在使用不同版本的时候,系统给到我们的反馈可能并不能如我们所愿。

3 服务

在 windows 下挂在 Celery 服务有时候会出现不稳定的情况(unix中暂时未发现这种情况),比如在执行定时任务的时候,过了一段时间之后,Celery 出现了假死状态,以至于不能按照我们指定的时间点去执行任务。

这些任务只是加入到待运行队列中(堆积在 Redis 中),只能人为重启 Celery 服务之后才能将堆积的任务释放出来运行。

这样一来,第一是定时任务在指定时间点没有正常运行,其二是在其他时间运行了这些任务,很可能会产生更新数据不及时,时间节点混乱的问题,不仅达不到业务需求,还会反受其害。

4 设置心跳

为了解决 Celery 在 windows 中的这种弊端,可以为 Celery 任务队列设置一个心跳时间,比如每一分钟或者每五分钟向 Redis 数据库发送一次数据以保证队列始终是活跃的状态,这样只要你的电脑不关机并保持网络畅通(如果是远程 Redis),Celery 任务队列服务就不会出现假死状态。

5 举个栗子

我总是很喜欢用示例来说话,前些时间在对某平台的商家后台进行数据采集的时候,为了使用时能自动获取该网站的 cookie ,

用Pyppeteer 写了一个自动化登陆的脚本,和往常一样仍在 Celery 队列中并迅速的启动服务。

脚本是这样的(非常接近实际的伪代码,没办法,保命要紧)

  1. # -*- coding: utf-8 -*- 
  2. from db.redisCurd import RedisQueue 
  3. import asyncio 
  4. import random 
  5. import tkinter 
  6. from pyppeteer.launcher import launch 
  7. from platLogin.config import USERNAME, PASSWORD, LOGIN_URL 
  8.  
  9. class Login(): 
  10.     def __init__(self, shopId): 
  11.         self.shopId = shopId 
  12.         self.RedisQueue = RedisQueue("cookie"
  13.  
  14.     def screen_size(self): 
  15.         tk = tkinter.Tk() 
  16.         width = tk.winfo_screenwidth() 
  17.         height = tk.winfo_screenheight() 
  18.         tk.quit() 
  19.         return {'width': width, 'height': height} 
  20.  
  21.     async def login(self, username, password, url): 
  22.         browser = await launch( 
  23.             { 
  24.                 'headless'False
  25.                 'dumpio'True 
  26.             }, 
  27.             args=['--no-sandbox''--disable-infobars''--user-data-dir=./userData'], 
  28.         ) 
  29.         page = await browser.newPage()  # 启动新的浏览器页面 
  30.  
  31.         try: 
  32.             await page.setViewport(viewport=self.screen_size()) 
  33.             await page.setJavaScriptEnabled(enabled=True)  # 启用js 
  34.             await page.setUserAgent( 
  35.                 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299' 
  36.             ) 
  37.             await self.page_evaluate(page) 
  38.             await page.goto(url) 
  39.             await asyncio.sleep(2) 
  40.             # 输入用户名,密码 
  41.             await page.evaluate(f'document.querySelector("#userName").value=""'
  42.             await page.type('#userName', username, {'delay': self.input_time_random() - 50})  # delay是限制输入的时间 
  43.             await page.evaluate('document.querySelector("#passWord").value=""'
  44.             await page.type('#passWord'password, {'delay': self.input_time_random()}) 
  45.             await page.waitFor(6000) 
  46.  
  47.             loginImgVcode = await page.waitForSelector('#checkCode')   
  48.             await loginImgVcode.screenshot({'path''./loginImg.png'}) 
  49.             await page.waitFor(6000) 
  50.  
  51.             res = use_cjy("./loginImg.png"
  52.             pic_str = res.get("pic_str") if res.get("err_str") == "OK" else "1234" 
  53.  
  54.             await page.waitFor(6000) 
  55.             await page.type('#checkWord', pic_str, {'delay': self.input_time_random() - 50}) 
  56.             await page.waitFor(6000) 
  57.  
  58.             await page.click('#subMit'
  59.             await page.waitFor(6000) 
  60.             await asyncio.sleep(2) 
  61.             await self.get_cookie(page) 
  62.             await page.waitFor(3000) 
  63.             await self.page_close(browser) 
  64.             return {'code': 200, 'msg''登陆成功'
  65.         except
  66.             return {'code': -1, 'msg''出错'
  67.  
  68.         finally: 
  69.             await page.waitFor(3000) 
  70.             await self.page_close(browser) 
  71.  
  72.     # 获取登录后cookie 
  73.     async def get_cookie(self, page): 
  74.         cookies_list = await page.cookies() 
  75.         cookies = '' 
  76.         for cookie in cookies_list: 
  77.             str_cookie = '{0}={1}; ' 
  78.             str_cookie = str_cookie.format(cookie.get('name'), cookie.get('value')) 
  79.             cookies += str_cookie 
  80.         # 将cookie 放入 cookie 池 
  81.         self.RedisQueue.put_hash(self.shopId, cookies) 
  82.         return cookies 
  83.  
  84.     async def page_evaluate(self, page): 
  85.         await page.evaluate('''() =>{ Object.defineProperties(navigator,{ webdriver:{ get: () => undefined } }) }'''
  86.         await page.evaluate('''() =>{ window.navigator.chrome = { runtime: {},  }; }'''
  87.         await page.evaluate( 
  88.             '''() =>{ Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] }); }'''
  89.         await page.evaluate( 
  90.             '''() =>{ Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5,6], }); }'''
  91.         await page.waitFor(3000) 
  92.  
  93.     async def page_close(self, browser): 
  94.         for _page in await browser.pages(): 
  95.             await _page.close() 
  96.         await browser.close() 
  97.  
  98.     def input_time_random(self): 
  99.         return random.randint(100, 151) 
  100.  
  101.     def run(self, username=USERNAME, password=PASSWORD, url=LOGIN_URL): 
  102.         loop = asyncio.get_event_loop() 
  103.         i_future = asyncio.ensure_future(self.login(username, password, url)) 
  104.         loop.run_until_complete(i_future) 
  105.         return i_future.result() 
  106.  
  107.  
  108. if __name__ == '__main__'
  109.     Z = Login(shopId="001"
  110.     Z.run() 

Celery 任务文件是这样的

  1. # -*- coding: utf-8 -*- 
  2. from __future__ import absolute_import 
  3. import os 
  4. import sys 
  5. import time 
  6. from db.redisCurd import RedisQueue 
  7. from send_msg.weinxin import Send_msg 
  8. base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 
  9. sys.path.append(base_dir) 
  10. from logger.logger import log_v 
  11. from celery import Task 
  12. from platLogin.login import Login  # 登陆类 
  13. from celery import Celery 
  14.  
  15. randomQueue = RedisQueue("cookie"
  16.  
  17. celery_app = Celery('task'
  18. celery_app.config_from_object('celeryConfig'
  19.  
  20. S = Send_msg() 
  21.  
  22. dl_dict = { 
  23.     'demo': { 
  24.         'cookie'''
  25.         'loginClass''Login'
  26.     } 
  27.  
  28. # todo 这是三种运行的状态 
  29. class task_status(Task): 
  30.     def on_success(self, retval, task_id, args, kwargs):  
  31.         log_v.info('任务信息 -> id:{} , arg:{} , successful ..... Done'.format(task_id, args)) 
  32.  
  33.     def on_failure(self, exc, task_id, args, kwargs, einfo):   
  34.         log_v.error('task id:{} , arg:{} , failed ! error : {}'.format(task_id, args, exc)) 
  35.  
  36.     def on_retry(self, exc, task_id, args, kwargs, einfo):  
  37.         log_v.warning('task id:{} , arg:{} , retry !  info: {}'.format(task_id, args, exc)) 
  38.  
  39.  
  40. # todo 随便找个hash key作为轮询对象, celery在win10系统可能不太稳定,有时候会有连接断开的情况 
  41. @celery_app.task(base=task_status) 
  42. def get_cookie_status(platName="demo"): 
  43.     try: 
  44.         # log_v.debug(f'[+] 轮询 {platName} 定时器启动 ..... Done'
  45.         randomQueue.get_hash(platName).decode() 
  46.         log_v.debug(f'[+] 轮询 {platName} 成功 ..... Done'
  47.         return "Erp 轮询成功" 
  48.     except
  49.         return "Erp 轮询失败" 
  50.  
  51.  
  52. @celery_app.task(base=task_status) 
  53. def set_plat_cookie(platName="demo", shopId=None): 
  54.     log_v.debug(f"[+] {platName} 正在登陆"
  55.     core = eval(dl_dict[platName]['loginClass'])(shopId=shopId) 
  56.     result = core.run() 
  57.     return result 

Celery 配置文件是这样的

  1. from __future__ import absolute_import 
  2. import datetime 
  3. from kombu import Exchange, Queue 
  4. from celery.schedules import crontab 
  5. from urllib import parse 
  6.  
  7. BROKER_URL = f'redis://root:{parse.quote("你的不规则密码")}@主机:6379/15' 
  8.  
  9. # 导入任务,如tasks.py 
  10. CELERY_IMPORTS = ('monitor.tasks',) 
  11.  
  12. # 列化任务载荷的默认的序列化方式 
  13. CELERY_TASK_SERIALIZER = 'json' 
  14.  
  15. # 结果序列化方式 
  16. CELERY_RESULT_SERIALIZER = 'json' 
  17. CELERY_ACCEPT_CONTENT = ['json'
  18.  
  19. CELERY_TIMEZONE = 'Asia/Shanghai'  # 指定时区,不指定默认为 'UTC' 
  20. # CELERY_TIMEZONE='UTC' 
  21.  
  22. CELERYBEAT_SCHEDULE = { 
  23.     'add-every-60-seconds': { 
  24.         'task''tasks.get_cookie_status'
  25.         'schedule': datetime.timedelta(minutes=1),  # 每 1 分钟执行一次 
  26.         'args': ()  # 任务函数参数 
  27.     }, 

启动服务

  1. celery -A tasks beat -l INFO 
  2. celery -A tasks worker -l INFO -c 2 

以 2 个线程启动消费者队列服务并启用定时任务,当发现当前平台的 cookie 不可用时,我会向 Celery 发送一个信号(就是调用了前面的set_plat_cookie 这个方法),消费者得到这个任务这个就会执行自动化脚本以获取 cookie 并储存在 Redis 中,使用时在从 Redis 中获取就能正常请求到该平台的数据。

在空闲时间,Celery中的 get_cookie_status 方法会每隔一分钟向 Redis 请求数据,这就是我们设置的 1分钟心跳。

这样不管我们的 Celery 是否是后台启动,都不会出现假死、卡死的状态,则万事大吉矣!!

6 总结

 

本文为了解决 Celery 在 windows 中的这种弊端,为 Celery 任务队列设置一个心跳时间,比如每一分钟或者每五分钟向 Redis 数据库发送一次数据以保证队列始终是活跃的状态,这样只要你的电脑不关机并保持网络畅通(如果是远程 Redis),Celery 任务队列服务都不会出现假死、卡死的状态。

 

来源:Python爬虫与数据挖掘内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯