监控ETL任务状态的脚本如何编写

wen 实用脚本 14

监控ETL任务状态的脚本如何编写:从原理到实战的完整指南

📖 目录导读

  • 为什么需要监控ETL任务状态? – 数据管线稳定性的核心挑战
  • ETL任务状态监控的核心技术原理 – 状态码、日志、心跳与异常捕获
  • 编写监控脚本前的准备工作 – 环境、依赖与状态定义
  • 实战:用Python编写ETL状态监控脚本 – 完整代码解析(含日志监控、超时检测、告警通知)
  • 高级技巧:集成告警与可视化 – 钉钉/Slack通知、Grafana看板
  • 常见问题FAQ – 脚本死循环、历史日志干扰、多任务并发处理
  • 总结与最佳实践 – 让你的ETL监控“永不下线”

为什么需要监控ETL任务状态?

Q: 不监控ETL,直接跑,出问题再手动查日志不行吗?
A: 在大数据场景下,ETL任务往往在凌晨批量执行,如果某一步骤失败(如源数据库连接超时、目标表被锁、数据倾斜导致OOM),不监控意味着数据延迟数小时,甚至产生错误数据写入下游。一份2023年的行业报告显示,超过60%的数据仓库事故是由于ETL状态未被及时发现导致的。

监控ETL任务状态的脚本如何编写

核心痛点:

  • 任务挂起:进程未崩溃,但陷入死循环,消耗资源不推进。
  • 数据不一致:增量ETL时,某批次失败导致部分数据重复或丢失。
  • 性能退化:任务运行时间从30分钟暴增到2小时,不监控无法感知。

ETL任务状态监控的技术原理

1 关键状态定义

状态 含义 触发条件
RUNNING 正在执行 进程存在且未超时
SUCCESS 正常结束 退出码为0,且输出结果文件/表行数符合预期
FAILED 执行出错 退出码非0,或捕获到异常
TIMEOUT 执行超时 运行时间 > 预设阈值
STUCK 资源占用但无进展 日志文件长时间无更新(heartbeat缺失)

2 监控手段三剑客

  • 进程级监控ps aux | grep etl_job 或系统守护进程
  • 日志级监控:定时检测日志文件最后修改时间、关键词(如“ERROR”“FATAL”)
  • 状态文件/心跳机制:ETL脚本每隔5秒向特定文件写入时间戳,监控脚本检查时间差

编写监控脚本前的准备工作

1 环境配置

pip install requests  # 用于告警通知
pip install psutil    # 进程监控

2 定义监控配置文件(config.yaml)

jobs:
  - name: "daily_user_etl"
    pid_file: "/var/run/etl_user.pid"
    log_path: "/var/log/etl/user_etl.log"
    max_run_time: 3600  # 秒
    heartbeat_interval: 30  # 心跳更新间隔
alert:
  webhook_url: "https://open.feishu.cn/open-apis/bot/v2/hook/xxx"

Q: 为什么不用bash脚本而用Python?
A: Python能更好地处理JSON配置、HTTP请求、日期计算、正则匹配日志中的复杂模式,Bash在字符串切分和跨平台兼容性上较弱。


实战:用Python编写ETL状态监控脚本

📝 完整代码(示例部分,可扩展)

#!/usr/bin/env python3
import os
import time
import json
import requests
import psutil
from datetime import datetime
import yaml
class ETLMonitor:
    def __init__(self, config_path='config.yaml'):
        with open(config_path, 'r') as f:
            self.config = yaml.safe_load(f)
        self.alert_url = self.config['alert']['webhook_url']
    def check_job_status(self, job):
        """检查单个任务状态"""
        result = {'name': job['name'], 'status': 'UNKNOWN'}
        # 1. 检查进程是否存活
        pid = self._read_pid(job['pid_file'])
        if pid and psutil.pid_exists(pid):
            result['process_alive'] = True
            # 2. 检查运行时间是否超时
            proc = psutil.Process(pid)
            run_duration = time.time() - proc.create_time()
            if run_duration > job['max_run_time']:
                result['status'] = 'TIMEOUT'
                result['detail'] = f"运行时间 {run_duration:.0f}s 超过阈值 {job['max_run_time']}s"
            else:
                # 3. 检查心跳(日志更新时间)
                last_modified = os.path.getmtime(job['log_path'])
                if time.time() - last_modified > job['heartbeat_interval'] * 3:
                    result['status'] = 'STUCK'
                    result['detail'] = "日志最后更新超过心跳间隔3倍"
                else:
                    # 4. 检查日志是否有错误
                    if self._check_log_error(job['log_path']):
                        result['status'] = 'FAILED'
                        result['detail'] = "日志包含ERROR/FATAL关键字"
                    else:
                        result['status'] = 'RUNNING'
                        result['detail'] = "正常运行中"
        else:
            # 进程不存在:检测是否有退出标志
            if os.path.exists(job['pid_file']):
                os.remove(job['pid_file'])
            # 检查最近一次完成记录
            result['status'] = 'FINISHED_OR_CRASHED'
            result['detail'] = "进程不存在,请检查日志确认完成还是崩溃"
        return result
    def _read_pid(self, pid_path):
        try:
            with open(pid_path, 'r') as f:
                return int(f.read().strip())
        except:
            return None
    def _check_log_error(self, log_path, keywords=['ERROR', 'FATAL', 'ORA-']):
        """检测日志中最近200行是否含错误关键字"""
        try:
            with open(log_path, 'rb') as f:
                # 读取末尾8KB
                f.seek(-min(8192, os.path.getsize(log_path)), 2)
                tail = f.read().decode('utf-8', errors='ignore')
                for kw in keywords:
                    if kw in tail:
                        return True
            return False
        except:
            return False
    def send_alert(self, job_result):
        """发送告警到飞书/钉钉"""
        if job_result['status'] in ['TIMEOUT', 'FAILED', 'STUCK']:
            msg = {
                "msg_type": "text",
                "content": {
                    "text": f"🚨 ETL告警\n任务:{job_result['name']}\n状态:{job_result['status']}\n详情:{job_result['detail']}\n时间:{datetime.now()}"
                }
            }
            requests.post(self.alert_url, json=msg, timeout=5)
    def run_cycle(self):
        """主循环"""
        while True:
            for job in self.config['jobs']:
                status = self.check_job_status(job)
                print(f"[{datetime.now()}] {job['name']}: {status['status']}")
                if status['status'] != 'RUNNING':
                    self.send_alert(status)
            time.sleep(60)  # 每分钟检查一次
if __name__ == "__main__":
    monitor = ETLMonitor()
    monitor.run_cycle()

🔧 使用说明

  1. 将上述代码保存为 etl_monitor.py
  2. 创建 config.yaml 填入具体任务参数
  3. 执行 nohup python3 etl_monitor.py & 后台运行
  4. 日志会自动输出实时状态

高级技巧:集成告警与可视化

1 多通道告警

除了HTTP通知,还可发送:

  • 钉钉:改用 https://oapi.dingtalk.com/robot/send?access_token=xxx
  • 企业微信https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx
  • 邮件:使用 smtplib 发送失败摘要

2 将指标写入Prometheus

在脚本中增加时间戳记录:

from prometheus_client import Gauge, start_http_server
etl_status = Gauge('etl_job_status', '1=ok, 0=fail', ['job_name'])

暴露/metrics端口,配合Grafana绘制实时仪表盘。

Q: 监控脚本本身崩溃了怎么办?
A: 使用systemd管理监控脚本,设置Restart=always,或者使用进程守护工具如supervisord


常见问题FAQ

Q1:脚本发现进程存在但日志长时间不更新,怎么区分是真的阻塞还是日志缓冲?

A: 配合心跳文件机制,让ETL脚本每30秒向独立的心跳文件写入时间戳,监控脚本仅依赖心跳文件,避免日志刷盘延迟干扰。

Q2:多个ETL任务共用一个日志文件怎么办?

A: 在配置中为每个任务指定独立日志文件,如果无法更改,使用正则匹配任务标识(如 [daily_user] 前缀)。

Q3:如何避免监控脚本在任务正常结束时误报“进程不存在”?

A: 在ETL脚本退出前,创建一个.done标记文件,监控脚本先检测done文件,如果存在则跳过告警并清理该文件。


总结与最佳实践

编写ETL监控脚本的核心是建立多维度检测机制

  1. 进程存活 + 时间超限(最基础)
  2. 心跳超时 + 日志错误(检测阻塞)
  3. 数据量变化(可选:比如目标表行数是否增加)

最佳实践清单:

  • ✅ 监控脚本本身使用 systemdsupervisor 保持常驻
  • ✅ 每轮检查间隔不超过任务运行时间的1/10(如任务1小时,检查间隔≤6分钟)
  • ✅ 告警消息包含任务名、上下文(如最后100行日志)、直接操作建议
  • ✅ 设置“静默期”:任务启动后前60秒不告警,避免启动时临时文件未就绪的误报
  • ✅ 保留历史的监控数据(写入数据库),用于分析任务执行趋势

不要过于依赖“进程存在”这一指标——许多ETL框架(如Apache Airflow)有外部任务调度器,进程可能已被调度器回收,但任务实际失败,最佳方案是将监控脚本与ETL框架的状态API结合(如读取Airflow的dag_run.status)。

通过本指南,您应该能够独立编写一个生产可用的ETL状态监控脚本,实际部署时,根据您的ETL框架和告警偏好调整细节即可。

抱歉,评论功能暂时关闭!