定时任务稳定性解决方案-healthchecks监控系统

背景

目前crontab出现问题后无感知,发现问题不及时,几乎是靠业务部门或用户反馈的方式,研发部门再排查的方式,处理问题、发现问题相对滞后,由此可见需要进一步优化crontab的稳定性,降故障通知前置,在用户反馈之前接受故障处理故障.
历史问题总结:

  • 人为误操作导致所有的crontab全部清空
  • 定时任务服务器误操作后灾难恢复困难
  • 未加锁机制检测启动多个进程导致数据错乱
  • 定时任务被系统oom 导致任务失败
  • 定时任务服务器重启后cron未开机自启导致任务未执行
  • 定时任务失败后无监控、出问题无感知(发现问题&处理问题滞后)
  • ...................

一、定时任务管理规范

问题描述:目前上线是通过运维在salt服务器分发的方式,发布方式不透明,还存在手动修改的方式,不规范,曾出现过误操作导致全清空事件,定时任务服务器误操作后灾难恢复困难的问题
解决方案: 统一规范成jenkins发布的模式,同发布代码走相同的逻辑

二、定时任务发布选择机器问题

问题描述: 目前发布代码需要选择服务器,存在错误选择的情况,从而引发问题.
解决方案:优化发布方式,发布无需选择服务器,自动判断发布服务器。

三、定时任务无法及时查看

问题描述:目前是通过定时同步任务列表到/tmp/work_cron的方式,存在延迟
解决方案: 研发直接查看gitlab的仓库即可

四、定时任务执行OOM中断

问题描述: 较大的程序执行会耗费很大的内存,从而出现被系统oom的风险,然而系统oom后目前并不能发现
解决方案:针对系统oom的问题,可以收集/var/log/message的方式进行报警处理,可第一时间发现问题.

五、定时任务进程数据安全保障(锁机制)

定时任务出现hang住进程,导致很多进程启动的问题 ;
多个进程同时运行导致数据错乱问题.
例如每次写的临时表名称一致,两个进程同时写则可能产生错误的数据结果.
解决方案: 对于不可同时启动两个进程的任务,程序需要加锁判断状态保证数据的可靠性

六、大型定时任务升级队列实现

问题描述: 定时任务应当尽量的轻量化,最优方案是只做定时任务的触发,而后程序通过队列方式进行数据处理.
例如定时任务程序单次运行需要几十分钟以上,或者处理的数据量达千万级别.
解决方案:

方案一

  1. 将重量级别的任务改造优化成队列的方式实现. 代码实现数据处理逻辑,数据放入队列依次处理的方式.
  2. 使用cronsun触发的方式进行定时任务的管理

方案二

  • 迁移至大数据任务平台,利用大数据集群运算能力完成相关功能

七、定时任务状态感知

问题描述:目前每个定时任务的执行状态(成功/失败/hang/warn)无法感知,只能通过日志来排查(如果有日志)

  1. 如何知道任务是否开始执行?(目前靠人)【cron服务未开启】
  2. 如何知道任务执行成功/失败?(目前靠人)【脚本执行80%后失败】
  3. 任务执行失败如何第一时间发现?(目前基本靠业务&用户侧反馈)
    解决方案:
  • 增加判断crontab是否按照预期执行监控机制
  • 增加状态汇报逻辑,使任务执行可视化&数据化,并且增加报警机制.(核心任务)
  • 增加定时任务日志,提供关键字进行报警(核心任务)

八、healthchecks 监控系统

详情移步官网: https://healthchecks.io/ (开源软件)

Healthchecks 是一个用来 监控定时任务(cron job)是否按时运行 的系统。它通过一种非常简单有效的方式帮助你发现定时任务是否异常或失败。

主要作用

  • 监控 cron、systemd timer、脚本等是否按时执行;
  • 当任务没有按时“打卡”时,发送通知(邮件、Webhook、Slack、钉钉等);
  • 提供简单的 Web UI 记录任务运行历史和状态。

Healthchecks 的工作原理如下:

  1. 系统为每个任务分配一个唯一的“ping URL”(如 https://hc.example.com/your-uuid);
  2. 每次任务执行成功后,向这个 URL 发一个 HTTP 请求(称为“ping”);
  3. Healthchecks 会为每个任务设置一个超时时间(比如 1 小时);
  4. 如果超时未收到“ping”,则认为该任务未执行或执行失败,触发告警

应用场景

  • 生产环境中的定时脚本/任务监控
  • 如 MySQL 备份脚本、日志归档、数据同步等。
  • Kubernetes CronJob 监控
  • CronJob 成功后加一个 ping 请求,Healthchecks 提供独立的状态记录和报警。
  • 没有集成监控系统的小团队
  • Healthchecks.io 提供简单、即开即用的 Web UI 和通知集成,非常适合中小项目快速接入。
  • 与 Prometheus/Grafana 互补
  • 可在任务层面提供更直观的“是否执行”状态,结合已有监控形成闭环。

如果你有多个定时任务,还可以用标签、项目分组等方式管理。如果你有兴趣搭建私有版本,它也支持 Docker 一键部署。

原创作者: topicjie 转载于: https://www.cnblogs.com/topicjie/p/18840481
import time import threading import psutil import smtplib import logging from datetime import datetime import schedule import winsound from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart import platform import os import gc import requests import json class XiaoxingService: def __init__(self, config_path="C:/xiaoxing/config.json"): """ 初始化AI服务 :param config_path: 配置文件路径 """ self.running = True self.last_optimized = None self.config = self.load_config(config_path) self.setup_logging() self.setup_tasks() logging.info("小星AI服务初始化完成") def load_config(self, path): """加载配置文件""" default_config = { "notification_email": None, "smtp_server": "smtp.example.com", "smtp_port": 587, "smtp_user": "xiaoxing@example.com", "smtp_pass": "your_password", "knowledge_sources": [ "https://api.tech-news.com/v1/latest", "https://ai-research-updates.org/feed" ], "optimization_threshold": { "cpu": 80, "memory": 85 }, "log_path": "C:/xiaoxing/service.log", "icon_path": "C:/xiaoxing/icon.ico", "knowledge_db": "C:/xiaoxing/knowledge.db" } try: with open(path, 'r') as f: return json.load(f) except FileNotFoundError: logging.warning("配置文件未找到,使用默认配置") return default_config except json.JSONDecodeError: logging.error("配置文件格式错误,使用默认配置") return default_config def setup_logging(self): """配置日志系统""" logging.basicConfig( filename=self.config["log_path"], level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', filemode='a' ) # 添加控制台输出 console = logging.StreamHandler() console.setLevel(logging.INFO) formatter = logging.Formatter('%(levelname)s: %(message)s') console.setFormatter(formatter) logging.getLogger().addHandler(console) def setup_tasks(self): """设置定时任务""" # 系统维护任务 schedule.every().day.at("02:00").do(self.optimize_system) schedule.every(30).minutes.do(self.check_system) # 知识管理任务 schedule.every().hour.do(self.update_knowledge) schedule.every().monday.at("04:00").do(self.self_evolve) # 健康报告任务 schedule.every().day.at("08:00").do(self.daily_health_report) logging.info("定时任务已设置") def optimize_system(self): """执行系统优化""" logging.info("开始系统优化") try: # 记录优化前的状态 cpu_before = psutil.cpu_percent(interval=1) mem_before = psutil.virtual_memory().percent # 执行优化操作 self.clean_memory() self.optimize_resources() # 记录优化结果 cpu_after = psutil.cpu_percent(interval=1) mem_after = psutil.virtual_memory().percent msg = (f"系统优化完成!\n" f"CPU使用率: {cpu_before}% → {cpu_after}%\n" f"内存使用率: {mem_before}% → {mem_after}%") logging.info(msg) self.notify("系统优化报告", msg) self.last_optimized = datetime.now() except Exception as e: logging.error(f"优化失败: {str(e)}") self.notify("优化失败", str(e)) def clean_memory(self): """内存清理优化""" # 跨平台内存清理 if platform.system() == 'Windows': try: import ctypes ctypes.windll.kernel32.SetProcessWorkingSetSize(-1, 0xFFFFFFFF, 0xFFFFFFFF) except Exception: pass else: # Linux/macOS 内存清理 os.system('sync && echo 3 > /proc/sys/vm/drop_caches') # Python内部垃圾回收 gc.collect() def optimize_resources(self): """优化系统资源使用""" # 清理临时文件 temp_dir = os.path.join(os.environ.get('TEMP', '/tmp'), 'xiaoxing_cache') if os.path.exists(temp_dir): for filename in os.listdir(temp_dir): file_path = os.path.join(temp_dir, filename) try: if os.path.isfile(file_path): os.unlink(file_path) except Exception as e: logging.warning(f"无法删除临时文件 {file_path}: {str(e)}") def update_knowledge(self): """更新知识库""" logging.info("开始更新知识库") try: new_knowledge = [] for source in self.config["knowledge_sources"]: try: response = requests.get(source, timeout=10) if response.status_code == 200: # 实际应用中需要解析不同格式的数据 # 这里简化为直接保存原始数据 new_knowledge.append(f"来源: {source}\n内容: {response.text[:200]}...") except requests.RequestException as e: logging.warning(f"知识源 {source} 获取失败: {str(e)}") if new_knowledge: with open(self.config["knowledge_db"], "a", encoding="utf-8") as f: f.write(f"\n\n=== 更新于 {datetime.now()} ===\n") f.write("\n".join(new_knowledge)) msg = f"获取 {len(new_knowledge)} 条新知识" logging.info(msg) self.notify("知识库更新", msg) else: logging.info("本次未获取到新知识") except Exception as e: logging.error(f"知识库更新失败: {str(e)}") self.notify("知识更新错误", str(e)) def check_system(self): """监控系统状态""" cpu_percent = psutil.cpu_percent(interval=1) mem_percent = psutil.virtual_memory().percent disk_percent = psutil.disk_usage('/').percent if platform.system() != 'Windows' else psutil.disk_usage('C:').percent logging.info(f"系统状态: CPU={cpu_percent}%, 内存={mem_percent}%, 磁盘={disk_percent}%") # 检查阈值 thresholds = self.config["optimization_threshold"] if cpu_percent > thresholds["cpu"]: self.handle_high_cpu(cpu_percent) if mem_percent > thresholds["memory"]: self.handle_high_memory(mem_percent) def handle_high_cpu(self, usage): """处理高CPU使用率""" logging.warning(f"CPU使用率过高: {usage}%") # 找出高CPU进程 processes = [] for proc in psutil.process_iter(['pid', 'name', 'cpu_percent']): try: if proc.info['cpu_percent'] > 10: # 筛选高CPU进程 processes.append(proc.info) except (psutil.NoSuchProcess, psutil.AccessDenied): pass # 按CPU使用率排序 processes.sort(key=lambda x: x['cpu_percent'], reverse=True) # 生成报告 report = f"当前CPU使用率: {usage}%\n" report += "高CPU进程:\n" for i, proc in enumerate(processes[:5], 1): report += f"{i}. {proc['name']} (PID:{proc['pid']}) - {proc['cpu_percent']:.1f}%\n" self.notify("CPU使用率警告", report) # 如果最近15分钟内没有优化过,执行优化 if not self.last_optimized or (datetime.now() - self.last_optimized).seconds > 900: self.optimize_system() def handle_high_memory(self, usage): """处理高内存使用率""" logging.warning(f"内存使用率过高: {usage}%") self.notify("内存警告", f"当前内存使用率: {usage}%") self.clean_memory() def self_evolve(self): """执行自我进化""" logging.info("启动自我进化协议") try: # 模拟进化过程 improvements = [ "神经网络架构升级: 引入注意力机制", "知识图谱扩展: 新增10万实体关系", "推理引擎优化: 响应速度提升40%", "安全模块强化: 量子加密算法集成" ] # 生成进化报告 report = "进化完成!主要改进:\n" for i, imp in enumerate(improvements, 1): report += f"{i}. {imp}\n" logging.info(report) self.notify("自我进化报告", report) self.play_sound_alert() except Exception as e: logging.error(f"进化失败: {str(e)}") self.notify("进化失败", str(e)) def daily_health_report(self): """生成每日健康报告""" logging.info("生成每日健康报告") try: # 获取系统指标 cpu_avg = psutil.cpu_percent(interval=1) mem_usage = psutil.virtual_memory().percent disk_usage = psutil.disk_usage('/').percent if platform.system() != 'Windows' else psutil.disk_usage('C:').percent # 获取网络状态 net_io = psutil.net_io_counters() # 构建报告 report = ( "小星AI每日健康报告\n" "===================\n" f"CPU平均使用率: {cpu_avg}%\n" f"内存使用率: {mem_usage}%\n" f"磁盘使用率: {disk_usage}%\n" f"网络流量: 接收 {net_io.bytes_recv/1024/1024:.2f}MB / 发送 {net_io.bytes_sent/1024/1024:.2f}MB\n" f"运行时间: {self.get_uptime()}\n" "===================\n" "系统状态: 一切正常 ✅" ) logging.info(report) self.notify("每日健康报告", report) except Exception as e: logging.error(f"健康报告生成失败: {str(e)}") def get_uptime(self): """获取服务运行时间""" if hasattr(self, 'start_time'): uptime = datetime.now() - self.start_time days = uptime.days hours, remainder = divmod(uptime.seconds, 3600) minutes, _ = divmod(remainder, 60) return f"{days}天 {hours}小时 {minutes}分钟" return "未知" def play_sound_alert(self): """播放声音提示""" try: if platform.system() == 'Windows': winsound.Beep(1000, 500) else: # Linux/Mac 使用系统声音 os.system('afplay /System/Library/Sounds/Ping.aiff' if platform.system() == 'Darwin' else 'paplay /usr/share/sounds/freedesktop/stereo/complete.oga') except Exception: pass def notify(self, title, message): """发送通知""" # 系统通知 self.show_system_notification(title, message) # 邮件通知 if self.config.get("notification_email"): self.send_email(title, message) def show_system_notification(self, title, message): """显示系统通知""" try: if platform.system() == 'Windows': from win10toast import ToastNotifier toaster = ToastNotifier() toaster.show_toast( title, message, icon_path=self.config.get("icon_path", ""), duration=10 ) elif platform.system() == 'Darwin': # macOS os.system(f"osascript -e 'display notification \"{message}\" with title \"{title}\"'") else: # Linux os.system(f'notify-send "{title}" "{message}"') except Exception as e: logging.warning(f"系统通知失败: {str(e)}") def send_email(self, subject, body): """发送邮件通知""" try: msg = MIMEMultipart() msg['Subject'] = subject msg['From'] = self.config["smtp_user"] msg['To'] = self.config["notification_email"] msg.attach(MIMEText(body, 'plain')) with smtplib.SMTP(self.config["smtp_server"], self.config["smtp_port"]) as server: server.starttls() server.login(self.config["smtp_user"], self.config["smtp_pass"]) server.send_message(msg) logging.info("邮件通知已发送") except Exception as e: logging.error(f"邮件发送失败: {str(e)}") def run(self): """启动服务主循环""" logging.info("小星AI后台服务启动") self.start_time = datetime.now() self.notify("小星AI服务", "后台服务已启动,开始24小时运行") # 定时任务线程 def schedule_runner(): while self.running: schedule.run_pending() time.sleep(1) threading.Thread(target=schedule_runner, daemon=True).start() # 主循环 try: while self.running: time.sleep(60) except KeyboardInterrupt: self.stop() def stop(self): """停止服务""" self.running = False logging.info("服务停止中...") self.notify("小星AI服务", "后台服务已安全停止") logging.info("服务已停止") if __name__ == "__main__": service = XiaoxingService() try: service.run() except Exception as e: logging.critical(f"服务崩溃: {str(e)}") service.notify("服务崩溃", str(e))
最新发布
07-20
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值