# sptask-monitor
**Repository Path**: GeekDot/sptask-monitor
## Basic Information
- **Project Name**: sptask-monitor
- **Description**: 脚本任务通用监控系统
- **Primary Language**: Python
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2020-07-25
- **Last Updated**: 2020-12-19
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
数据平台-任务监控系统
极客点儿
2020-07-25
### 一、项目背景
-
数据平台 `edison` 项目中有100多个任务,每个任务之间都是独立且无耦合。由于任务众多,在执行过程中经常会发生任务中断的情况,为了解决这些任务的中断问题,急需开发一套能实时监控任务状态的监控系统
### 二、项目需求
-
1. 不能改动现有任务的执行流程和代码逻辑
2. 不能影响服务器上各个任务的正常运行
3. 监控系统要足够轻量,以最简洁的方式嵌入到任务中
4. 任务在启动、停止监控系统时要足够简洁、方便、快速
5. 监控系统的监测频率需达到实时监控的级别
### 三、项目设计
-
#### 设计语言:
- Python
#### 设计模式:
- AOP
#### 两大核心模块:
- 监控程序:记录模块 & 装饰器
- 监控程序:监控模块 & 守护进程
#### 两大辅助模块:
- 监控程序:检测模块 & 心跳检测
- 监控程序:统计模块 & 日常报表
#### 五个工具模块:
- 监控路径模块
- 数据库处理模块
- 日志输出模块
- 时间日期模块
- 邮件处理模块
#### 两个存储模块:
- DB
- pids
#### 一个定时任务模块:
- crontab
### 四、项目逻辑
-
#### 1. 监控项目实现的逻辑细节
##### 记录模块运行流程 & 功能
因为记录模块的本质是装饰器,装饰器的性质就是可以在函数运行前后进行功能扩展
1. 当一个任务启动以后,任务将 `main` 函数作为参数传递给记录模块并附带一个任务 id
2. 记录模块通过任务 id 从任务列表中拿到任务相关信息,并将任务相关信息初始化
3. 初始化包括:在任务运行详情表中新建一条记录,并将任务信息、任务开始时间、执行状态写入到任务详情中
4. 除了将任务信息写到表中,初始化做的最重要的一件事是:将任务详情的 `id` 和任务的 `pid` 以 `id·pid` 的格式写到文件(文件名称也是以 `id·pid` 命名)中,并存储在 `pids` 目录下
5. 接下来就是监控模块不断地扫描 `pids` 目录并检查任务是否中断,这个到监控模块的时候详细说明
6. 当任务执行完成后,记录模块会先删除 `pids` 目录下当前的任务 `id·pid` 文件,然后将任务结束时间、任务运行时间、运行状态写入到任务详情中
##### 监控模块运行流程 & 功能
首先监控模块是常驻内存的,是一个守护进程,监控模块启动后,每隔 3 秒就扫描一次 `pids` 目录
1. 监控模块判断 `pids` 目录下是否存在 `id·pid` 文件
2. 如果没有文件,则回到上述步骤 1,等待 3 秒继续下一轮循环
3. 如果有文件,并且任务进程的 `pid` 存在,那么此时数据库状态应该更新`[正在执行]`状态
4. 如果有文件,并且任务进程的 `pid` 不存在,那么此时数据库状态应该更新`[执行中断]`状态,并且发送邮件通知任务中断,最后把 `id·pid` 文件删除
5. 执行完成后,回到上述步骤 1,等待 3 秒继续下一轮循环
##### 监控系统的监控原理
下面的表格是描述文件和 `pid` 之间的逻辑关系图,是监控系统的监控原理,也是整个监控系统的核心思想
**文件和进程状态:**
- `0`:不存在
- `1`:存在
**任务状态:**
- `1`:未执行
- `2`:开始执行
- `3`:正在执行
- `4`:执行中断
- `5`:执行完成
接下来就揭晓下面这张表格的秘密,原理很简单
首先文件只有两种状态,要么存在就是 `1`,要么不存在就是 `0`,`pid` 状态也是同理
这样的话,两个参数每个参数都有两种状态,通过排列组合后会得到四种结果
例如:文件`[存在]`,进程`[存在]`,那么结果是`[正在执行]`
有一种情况是不可能出现的,就是下表中第三行。理由是:文件不存在的话,那么进程肯定是不会存在的。因为文件都没有生成,那么程序压根就没执行(不要说权限或者其他的什么问题,这些问题都不属于本次讨论范围内)
详情参考下表:
|文件|文件状态|进程|进程状态|执行结果|
|:---:|:---:|:---:|:---:|:---:|
|F|1|P|1|3|
|F|1|P|0|4|
|F|0|P|1|error|
|F|0|P|0|5|
#### 2. 其他辅助工具类模块
上述两大模块就是监控系统的核心功能,但是为了能有更好、更完善、更流畅的应用体验,增加了几个辅助类模块和工具模块
##### 监控程序:检测模块
刚刚说过,监控模块是一个守护进程,要常驻内存的。但是万一因为某些原因,导致最核心的监控模块断掉怎么办呢?如果监控模块断了,那么就不会监控到任何的任务了,这个问题该怎么解决呢?
其实这也很简单,那就增加一个心跳检测功能,每分钟检测一次监控模块是否断掉,监控原理和监控模块类似,也是判断 `pid` 是否存在
而且检测模块是放在定时任务 `crontab` 中,不用担心检测任务会断掉(当然 crontab 也是一个服务,也会断掉的,具体解决方案放在项目复盘中讨论,在此不做详细说明)
##### 监控程序:统计模块
统计模块就是日常报表,是针对于昨天全天,所有任务的执行详情情况,进行统计、汇总、分析并且定时以表格的形式发送到邮箱
##### 其他工具模块
为了监控系统编码的完整统一、架构清晰、层次分明、模块独立以及方便日后扩展功能而提供的全局工具类模块,这个不过多做解释
### 五、项目流程图
-

### 六、编码实现
-

### 七、项目中遇到的坑
-
#### 1. 处理时间、路径、日志等问题
统一基础功能工具模块,新建 `tools` 包,将需要的基础模块添加到 `tools` 包中
统一路径处理、统一时间处理、统一日志处理、统一邮件处理、统一配置文件、统一数据库处理、统一状态处理
#### 2. 回调id隐患
pymysql 模块是 python 中连接数据库的包,但由于没有自带获取回调 id 的功能,所以没办法直接拿到 id。但是可以通过 `cursor.lastrowid` 获取最后一次插入数据的 id
由于MySQL的特性,每次连接都是启动一个线程,只要不 commit,此时操作后的数据只能本线程看得到,所以获取最后一次插入数据的 id 和其他程序并不冲突,而这一个点会为以后埋下一个巨大的隐患
#### 3. 文件错乱
**出现问题:**
理论上是一个进程是一个唯一的 id 和 pid,修改状态也是各个进程需要改写自己的即可。但是由于当时写文件的时候,每次写入状态都要写一次文件,在写文件的时候加了判断。如果 pids 目录中有文件就忽略,没有文件就写入。但是判断的时候路径是从基础模块路径中拿的,后来改动了基础模块路径,这里的判断就不生效了,所以每次写状态的时候都会写文件。而由于每次都会获取最后一个 id,就导致文件的错乱,最后结果就是数据库中的状态群魔乱舞,状态修改的乱七八糟......
**解决方案:**
不在写文件的函数中进行判断,而是在写数据库时候判断,如果是第一次插入就写文件,除此之外不会再对文件有任何操作,这样就能保证文件是安全的
#### 4. 进程堆砌
**出现问题:**
在测试的时候发现一个问题,程序报错发邮件,状态是 `[执行中断]` endTime 和 runTime 都没有数据,看上去似乎很正常。但是就下来诡异的事情发生了,等了一会儿,这个 `[执行中断]` 的状态居然变成了 `[执行完成]` 状态。后来经过排查发现,原来是 pid 堆砌在一起了
检测是从文件中拿的 pid 进行从进程中中过滤(grep),例如:拿到的文件 pid 是 9527,但是操作系统中的进程很多,可能拿到了 3 个 pid,分别是:9527、95271、95279
当时程序进行判断的是用 `==`,而且从系统中拿的都需要进行特殊字符清洗,所以结果就成了
if db_pid == os_pid:
pass
添加数据后:
if 9527 == 95279527195279:
pass
**解决方案:**
**1. 用路径 + 文件的方式过滤**
可能导致路径问题和其他问题
**2. 使用列表(数组)**
if db_pid in os_pid:
pass
添加数据后:
if 9527 in [9527, 95271, 95279]:
pass
#### 5. 临界状态
**出现问题:**
程序执行完成后,没有中断,正常情况下状态为 `[执行完成]` 才对,但是诡异的出现了程序 `[执行完成]` 但状态还是 `[正在执行]`(不管什么样的微观世界都很诡异,量子世界就如此诡异)
所谓临界状态:是考虑程序在程序微观世界内,发生在几微秒的时间内,遇到所有有可能问题
监控模块可能在第 10μs 的时候拿到了任务的 pid,在第 11μs 的时候记录模块可能执行完成释放了 pid,并将数据库的状态改为[执行完成],而此时的监控模块拿到的还是 10μs 时候的 pid,没来得及进行下一轮判断就直接将此,时数据库的状态[执行完成]改为[正在执行],这样就导致一个悖论出现,数据是执行完成后的数据,而状态却是运行时候的状态......
**解决方案:**
在[执行完成]和[执行中断]写入状态之前,需要进行判断。如果[执行完成]和[执行中断]要写入状态时,则需要判断数据库中不是[执行完成],这样才不会改乱状态
#### 6. 实时监控
所谓实时监控:是不设置延时时间,sleep(0) 这样会导致频繁读取硬盘,影响正常任务,增加状态判断,是为了监控系统在临界状态、及时监控的时候都能保证写入数据库的状态是正确的
#### 7. 析构顺序
对于宏观程序,当任务执行完成时比如需要删除文件和改写数据库状态,这时候其实先操作那个都可以。但是对于微观程序,必须先删除文件,要不然监控模块会拿到文件 pid 却找不到进程从而出问题
#### 8. 监控没有及时报警,小米事件(邮件只发给我自己)
#### 9. 添加任务监控时,有语法错误(把全局变量放入局部变量之中)
### 八、项目复盘
-
#### 项目的成果:
已开发完成一套稳定、实时监控、功能完善的监控系统
#### 项目的优势:
1. 能实时稳定的监控所有服务器上正在运行的任务
2. 能快速追踪定位到中断任务的详细信息并及时发送报警邮件
3. 完善的监控日志、监控自检、邮件报警、任务执行记录的日常报表
#### 项目的不足:
1. 监控系统只能追踪定位到任务中断的详细信息,具体中断的原因无从得知,因为中断原因需要日志系统完成。就目前来说监控系统还没有发挥出最大的威力,只发挥出一半的威力
2. `crontab` 是整个监控系统和数据平台项目的核心,cron 中断的问题还没有解决。不过已有解决方案,即使用服务器集群分布式监测。另一种解决方案就是将整个项目重构,将所有的任务都作为主程序的子进程,这样就可以有主进程得知子进程(任务)的运行状态
### 九、个人复盘
-
#### 个人的优势:
1. 能独立解决问题
2. 能快速定位并且修复程序中出现的BUG
#### 个人的不足:
1. 经验不足,导致刚开始好多细节没有考虑到
2. 对公司业务流程和数据平台任务不熟悉
3. 细节处理不够完善,暂时还达不到完美效果
#### 需要的提升:
1. 提升专业技能,数据库知识和数据分析能力
2. 理解公司业务流程和数据平台相关任务
3. 严谨对待每一行代码,对代码有敬畏之心,克己律人
#### 后续的规划:
1. 理解数据平台的所有相关任务,重构数据平台任务
2. 开发数据平台日志系统