SaltStack源码解析 — salt ‘*’ test.ping执行过程

前言

 

本文我们来学习salt ‘*’ test.ping命令实现的整个过程,涉及的组件比较多,将有助于更进一步了解SaltStack的运行机制。

 

总体概述

 

salt ‘*’ test.ping涉及的组件比较多,包括Master Req Server,Master Publisher,Minion,Master EventPubliser等,现在概要地介绍下整个实现流程:

  • 1、在salt-master机器执行salt ‘*’ test.ping命令;
  • 2、salt向Master Req Server发送带publish命令的消息;
  • 3、Master Req Server收到消息后向publish_pull.ipc push消息;
  • 4、Master Publisher向Minion Publish消息;
  • 5、Minion收到消息后启动一个新进程来执行消息中fun指定的函数;
  • 6、函数执行完成后,返回结果给Master Req Server;
  • 7、Master Req Server把结果push给Master EventPublisher;
  • 8、这时因为salt客户端订阅了Master EventPublisher,将收到返回结果,之后直接输出到终端。整个流程结束。

 

活动图

 

为了更清楚了解整个流程,下面是活动图:
源码解析

 

salt ‘*’ test.ping函数调用图

 

源码解析

 

Master Req Server处理salt test.ping消息函数调用图

 

源码解析

 

Minion处理Master Pub消息函数调用图

 

源码解析

 

Master Req Server处理Minion数据返回函数调用图

 

源码解析

SaltStack源码解析 — Master与Minion认证过程

前言

 

在Minion进程启动的时候,先会连接Master的Req Server请求认证,直到认证成功才会继续。下面我们分析其认证过程。

 

总体概述

 

  • 1、完成Minion进程启动前,Minon向Master Req Server发送cmd为_auth的payload消息;
  • 2、Master Req Server收到Minion的消息后,使用_auth方法执行认证处理;
  • 3、开始对key文件作如下判断,如果key在minions_rejected目录,则拒绝连接;如果key在minions目录,验证pub key是否相等,如果不相等,加入到minions_denied;如果key不在minions_pre目录,则把key写入到minions_pre目录;如果minions_pre不存在,如果pub key验证不通过,加入到minions_denied目录。

 

minion认证请求过程活动图

 

源码解析

 

minion发送认证请求函数调用图

 

源码解析

 

master处理minion验证请求函数调用图

 

源码解析

SaltStack源码解析 — salt-minion启动过程

功能介绍

 

salt-minion负责接收salt-master的消息,执行相应的指令。

 

环境介绍

 

  • 系统:CentOS-7
  • python版本:2.7.5
  • saltstack版本:2015.5.2 (Lithium)

 

场景描述

 

本节分析salt-minion启动过程,我们使用如下命令启动salt-minion:

  1. /usr/bin/salt-minion -d

 

总体分析

 

salt-minion启动过程总体分析如下:
1、使用parsers.MinionOptionParser类解析命令行参数及配置文件,此类继承了MasterOptionParser类,与salt-master解析过程差不多;
2、加载必要的模块,如grains,pillar;
3、开始连接salt-master的Req Server,评估与master的验证状态,如果master没有把minion的key加入信任列表,minion则循环验证直到验证通过;
4、发送消息通过master,minion已启动;
5、订阅salt-master pub服务器。

 

详细分析

 

函数调用流程图

 

源码解析

 

核心类功能介绍

 

类关系图

源码解析

MinionOptionParser类

MinionOptionParser类继承了MasterOptionParser,覆盖了MasterOptionParser的setup_config方法,改为由之前的master配置文件为minion配置文件。MasterOptionParser的讲解可以参考SaltStack源码解析 — salt-master启动过程

 

Minion类

这个应该算是比较重要的类了,包括了与master通信的所有方法。比如_do_socket_recv,负责接收master pub信息;_do_event_poll,负责处理事件;authenticate负责处理与master的验证。

SaltStack源码解析 — salt-master启动过程

环境介绍

 

我们使用如下环境来分析SaltStack源码:

  • 系统:CentOS-7
  • python版本:2.7.5
  • saltstack版本:2015.5.2 (Lithium)

我们使用如下方法来安装salt-master:

  1. rpm -Uvh http://mirrors.opencas.cn/epel/7/x86_64/e/epel-release-7-5.noarch.rpm
  2. yum install salt-master

 

场景描述

 

本节分析salt-master启动过程,我们使用如下命令启动salt-master:

  1. /usr/bin/salt-master -d

 

前置阅读

 

 

总体分析

 

salt-master的启动过程可简单分为两块,一是解析配置文件和命令行参数,二是启动所需进程。
对于解析命令行参数,主要使用到python标准模块optparse,解析master配置文件使用yaml模块。
主要过程为:
1、导入salt.syspaths作为命令行参数默认值,导入salt.config作为master配置文件默认值;
2、ConfigDirMixIn,LogLevelMixIn,RunUserMixin等几个类使用optparse的add_option注册命令行选项;
3、调用parse_args函数,解析命令行参数,解析master配置文件;
4、调用self.master.start(),注册sigusr1和sigusr2信号,分别用来输出堆栈和profile信息;
5、接着启动所需进程,如maintenance进程,publisher进程,master event进程等。

 

详细分析

 

函数调用流程图

 

源码解析

 

核心类功能介绍

 

类关系图

图片源码解析

MasterOptionParser类

此类主要用来解析命令行参数和yaml配置文件。继承了python标准optparse.OptionParser类,以前这六个类ConfigDirMixIn,LogLevelMixIn,RunUserMixin,DaemonMixIn,PidfileMixin,SaltfileMixIn分别用来注册配置文件路径,日志等级,守护进程,pid文件路径,saltfile路径等命令行选项。重写了optparse.OptionPaser的parse_args方法,添加对master yaml配置文件的解析。

 

Maintenance进程

Maintenance进程是用来做master的常规维护工作,如维护定时任务.

 

Publisher Server

绑定了默认的4505 zmq pub端口和publish_pull.ipc pull类型ipc,只要向publish_pull.ipc push数据,凡是subcribe 4505 publisher的client都将收到数据,具体此server什么作用还没有看.

 

EventPublisher Server

绑定了master_event_pub.ipc pub和master_event_pull.ipc pull,只要向master_event_pull.ipc push数据,订阅master_event_pub.ipc的client将收到消息.

 

ReqServer Server

启动了多个Mworker进程,每个进程连接workers.ipc REP,再启用了zmq_device,绑定tcp port 4506 ROUTER和workers.ipc DEALER,这样只要向4506 发送REQ数据,将会负载均衡到Mworker进程,Mworker进程收到请求后执行_handle_payload函数,也就是执行相应的模块.

使用zabbix根据时间监控多行格式的日志

我们目前想使用zabbix每五分钟监控一个错误日志文件,如果监控到有错误产生,就发邮件告警。像标准的访问日志,如nginx的access log,一行表示一条日志,解析起来比较容易,但当日志不是一行一条时,如tomcat,glassfish的日志,如下:
[2015-07-17T14:24:04.552+0800] [glassfish 4.0] [SEVERE] [AS-WEB-CORE-00037] [javax.enterprise.web.core] [tid: _ThreadID=26 _ThreadName=http-listener-1(3)] [timeMillis: 1437114244552] [levelValue: 1000] [[
An exception or error occurred in the container during the request processing
java.lang.IllegalArgumentException
at org.glassfish.grizzly.http.util.CookieParserUtils.parseClientCookies(CookieParserUtils.java:353)
at org.glassfish.grizzly.http.util.CookieParserUtils.parseClientCookies(CookieParserUtils.java:336)
at org.glassfish.grizzly.http.Cookies.processClientCookies(Cookies.java:220)
at org.glassfish.grizzly.http.Cookies.get(Cookies.java:131)
at org.glassfish.grizzly.http.server.Request.parseCookies(Request.java:1911)
at org.glassfish.grizzly.http.server.Request.getCookies(Request.java:1505)
at org.apache.catalina.connector.Request.parseSessionCookiesId(Request.java:4077)
at org.apache.catalina.connector.CoyoteAdapter.postParseRequest(CoyoteAdapter.java:649)
at org.apache.catalina.connector.CoyoteAdapter.doService(CoyoteAdapter.java:297)
]]

这个时候解析起来就相对复杂,我们可以使用如下脚本来取得最近五分钟的日志:

  1. #!/bin/bash
  2.  
  3. # 取得前5分钟时间
  4. LAST_MINUTE=$(date -d ‘-5 minute’ +%H%M%S)
  5. # 初始化日志条数
  6. LOG_NUM=0
  7. # 最大获取日志条数
  8. MAX_LOG=3
  9. # 初始化最终匹配日志
  10. LOG_CONTENT=""
  11. # 初始化包含时间行的匹配值
  12. LOG_DATE_MATCH=false
  13. # 设置日志路径
  14. LOG_PATH="/data/log/glassfish/domain1/server.log"
  15.  
  16. while read line;do
  17.  
  18.      # 匹配包含时间的行
  19.      if echo "$line" | grep -q ‘^[20’;then
  20.           # 根据包含时间行获取出特定时间格式,如181320
  21.           date_time=$(echo $line | grep -E -o "[0-9]{2}:[0-9]{2}:[0-9]{2}" | tr -d ‘:’)
  22.  
  23.           date_time=$(echo $date_time | sed ‘s/^0//’)
  24.           LAST_MINUTES=$(echo $LAST_MINUTES | sed ‘s/^0//’)
  25.           # 当前行的时间是否大于5分钟前的时间         
  26.           if [[ "$date_time" -gt "$LAST_MINUTE" ]];then
  27.                LOG_CONTENT="$LOG_CONTENTn$log_entry"
  28.                ((LOG_NUM++))
  29.                LOG_DATE_MATCH=true
  30.                log_entry="$linen"
  31.           else
  32.                LOG_DATE_MATCH=false
  33.                continue
  34.           fi
  35.  
  36.      else
  37.           # 只当前面日志时间满足条件时才设置log_entry值
  38.           if $LOG_DATE_MATCH;then
  39.                log_entry="$log_entryn$line"
  40.           fi   
  41.      fi
  42.  
  43.      # 限制最大获取行数
  44.      if [[ "$LOG_NUM" -gt "$MAX_LOG" ]];then
  45.           break
  46.      fi
  47.  
  48. done < $LOG_PATH
  49.  
  50. # 输出全部日志
  51. echo -n -e "$LOG_CONTENT"

前面的脚本按顺序读取的,但当日志文件比较大时,获取日志的效率就非常低了,所以推荐下面倒序读取日志的方法,更高效。

  1. #!/bin/bash
  2.  
  3. # 取得前5分钟时间
  4. LAST_MINUTE=$(date -d ‘-5 minute’ +%H%M%S)
  5. # 初始化日志条数
  6. LOG_NUM=0
  7. # 最大获取日志条数
  8. MAX_LOG=3
  9. # 初始化最终匹配日志
  10. LOG_CONTENT=""
  11. # 设置日志路径
  12. LOG_PATH="/data/log/glassfish/domain1/server.log"
  13.  
  14. while read line;do
  15.  
  16.      # 匹配包含时间的行
  17.      if echo "$line" | grep -q ‘^[20’;then
  18.           # 根据包含时间行获取出特定时间格式,如181320
  19.           date_time=$(echo $line | grep -E -o "[0-9]{2}:[0-9]{2}:[0-9]{2}" | tr -d ‘:’)
  20.         
  21.           # 当前行的时间是否大于5分钟前的时间   
  22.           if [[ "$date_time" > "$LAST_MINUTE" ]];then
  23.                ((LOG_NUM++))
  24.                log_entry="$linen$log_entry"
  25.                LOG_CONTENT="$LOG_CONTENTn$log_entry"
  26.           else
  27.                break
  28.           fi
  29.  
  30.           log_entry=""
  31.  
  32.      else
  33.           log_entry="$linen$log_entry"
  34.      fi
  35.  
  36.      # 限制最大获取行数
  37.      if [[ "$LOG_NUM" > "$MAX_LOG" ]];then
  38.           break
  39.      fi
  40.  
  41. done < <(tac $LOG_PATH)
  42.  
  43. # 输出全部日志
  44. echo -n -e "$LOG_CONTENT"

之后就可以在zabbix添加一个监控项用来获取日志内容,触发器就使用{itemName.strlen(0)}#0表达式来检测获取到的日志内容是否不为空。itemName为监控项名称。

python optparse模块源码解析

功能介绍

 

python的optparse模块用来解析命令行参数。支持多种参数数据类型,如字符串,布尔,常数等;能自动生成帮助与usage等信息。

 

环境介绍

 

我们在如下环境来分析optparse模块源码:
系统:CentOS-7
Python版本:2.7.5

 

场景描述

 

使用官方使用的例子来进行分析:

  1. from optparse import OptionParser
  2. parser = OptionParser()
  3. parser.add_option("-f", "–file", dest="filename",
  4.                   help="write report to FILE", metavar="FILE")
  5. parser.add_option("-q", "–quiet",
  6.                   action="store_false", dest="verbose", default=True,
  7.                   help="don’t print status messages to stdout")
  8.  
  9. (options, args) = parser.parse_args()

 

总体分析

 

optparse模块总体设计比较清晰,首先实例化OptionParser,parser = OptionParser(),实例化中主要是初始化相关选项的实例属性,如短选项self._short_opt,长选项self._long_opt;接着通过parser实例的add_option方法来注册将要获取的命令行选项,主要涉及选项名称,默认值,参数数据类型,方便下一步的解析操作对命令行参数进行必要的检测;最后是调用parser的parse_args方法通过上一步add_option注册的选项来检测命令行参数是否合法,并且返回选项名及其值的字典,这样就可以通过选项名来获取值。

 

详细分析

 

函数调用流程图

 

源码解析

 

核心类功能介绍

 

类关系图

源码解析

OptionContainer类

作用
存储注册选项的数据,如短选项,长选项,关键词(定义选项数据类型等)。
主要属性:
option_list: 存储所有选项定义数据;
_short_opt: 存储短选项定义数据;
_long_opt: 存储长选项定义数据;
defaults: 各选项默认值。
主要方法:
add_option(self, *args, **kwargs): 注册选项,调用Option类存储选项相关数据,并存储在OptionContainer的相关属性中。

 

Option类

作用
解析并记录选项所有数据,如长选项,短选项,数据类型,默认值等。
主要属性
_short_opts: 短选项,如-f;
_long_opts: 长选项,如–file;
action: 对选项值作的操作,如store_true,存储为布尔true;
type: 数据类型;
dest: 设置选项值存储到的变量;
default: 设置选项默认值。
主要方法
主要是_check_*对关键词检查的方法,如_check_type是检查type的值是否合法,并返回正确的类型。

 

OptionParser类

作用
解析命令行参数,此类继承了OptionContainer类,所以可以通过OptionContainer中定义的选项数据来对传入的命令行参数作合法性检查,并返回正确的数据。解析完成之后返回选项名与值的字典。
主要属性
values: 就是解析完成后返回的字典。
主要方法
parse_args(self, args=None, values=None): 解析命令行参数,并返回选项名与值的字典。

inode占用100%时硬盘无法写入文件故障处理

故障现象

 

分区无法写入文件。

 

故障分析

 

执行df -h命令发现空间占用不到70%,执行df -hi,发现某分区IUse%值为100%,说明inode已经用完,应该是某些目录下存在大量的小文件导致。

 

解决方法

 

大量小文件分布有两种可能,一是只有一个或少量目录下存在大量小文件,这种情况我们可以使用如下命令来找出这个异常目录:

  1. find / -type d -size +10M

此命令作用是找出大小大于10M的目录(目录大小越大,表示目录下的文件越多)。
第二种可能是,大量的小文件分布在大量的目录下,这时候上面的命令可能找不出异常的目录,需要以下命令:

  1. cd /
  2. find */ ! -type l | cut -d / -f 1 | uniq -c

此命令作用是找出目录下文件总数,可能需要执行多次,直到找出具体的目录。比如上面的命令找出了/data目录下存在大量的小文件,但/data/目录还有很多目录,这时候我们还需要继续执行:

  1. cd /data
  2. find */ ! -type l | cut -d / -f 1 | uniq -c

直到找出具体的目录。

 

故障总结

 

对inode占用进行监控,并且收到inode告警时应及时使用以上方法来定位问题,并反馈给相应人员从根源解决。

Nginx写IO占用高故障处理

故障现象

 

突然收到一台服务器负载过高告警,紧接着网站打开缓慢。

 

故障分析

 

  • 1、登录服务器,使用top命令看到Cpu行的iowait达到了70%以上,所以断定是IO负载过高的原因;
  • 2、接着使用iotop -o命令发现,Nginx的写IO特别大,并且在上一步的top命令看到Nginx的进程状态为D,表示Nginx在等待IO已经为僵死状态;
  • 3、这时候是清楚知道是Nginx在对文件系统进行大量的写操作导致的系统负载过高了,但还是不能知道具体Nginx在写什么文件导致的负载压力,所以我们还需要继续追查下去;
  • 4、我们找到其中一个nginx worker进程的pid,使用lsof -p pid列出来的文件发现除了一些系统库文件及日志文件,还有相当多的fastcgi_temp/xxx文件,有可能与这些文件有关联;
  • 5、再次使用strace -p pid追踪,发现nginx进程对某个fd进行大量的写操作,与lsof命令列出来的文件刚好符合;
  • 6、使用iostat 1输出的大量写io的分区也与fastcgi_temp所在分区相符合;
  • 7、猜测可能是外部正在上传大量的大文件给php-fpm,于是通过EZHTTP的小工具来查看实时流量,发现入站流量其实不大。

 

分析结果

 

根据以上的故障分析,非常有可能是本机的某些程序通过http上传大量大文件。因为对程序逻辑不熟悉,也只是猜测。为了尽快恢复服务,决定实施以下解决方案。

 

解决方案

 

既然清楚知道了fastcgi_temp io压力大,目前也无法短时间从根本上解决问题,所以决定把fastcgi_temp指向/dev/shm,也就是映射到了内存,重启nginx之后服务恢复了正常。最终原因还需要开发配合解决。

分析统计MySQL general日志 找出查询次数最多的SQL

当我们需要优化MySQL查询时,第一想到的是开启慢日志,慢日志可以看到执行消耗超过一定时间的SQL语句和未使用索引的SQL。但如果我们想分析所有SQL查询的分布,即哪类SQL查询次数最多的时候,我们可以开启general log来统计。

 

开启general log

 

  1. mysql> show  variables like ‘%general%’;

+——————+————————————-+
| Variable_name | Value |
+——————+————————————-+
| general_log | OFF |
| general_log_file | /usr/local/mysql/data/localhost.log |
+——————+————————————-+

  1. mysql> set global general_log = "ON";

 

analysis-general-log.py脚本

 

  1. #!/usr/bin/python
  2.  
  3. # sort and count mysql general log
  4. # Author: Jason
  5. # Url: devops.webres.wang
  6. # Email: admin#webres.wang
  7. # Created: UTC 2015-02-15 17:51:53
  8.  
  9. import re
  10. import sys
  11. import os
  12.  
  13. if len(sys.argv) == 2:
  14.     logPath = sys.argv[1]
  15.     if not os.path.exists(logPath):
  16.         print ("file " + logPath + " does not exists.")
  17.         sys.exit(1)
  18. else:
  19.     print ("Usage: " + sys.argv[0] + " logPath")
  20.     sys.exit(1)
  21.  
  22. logFo = open(logPath)
  23. match = 0
  24.  
  25. for line in logFo:
  26.     line = re.sub(r"n","",line)
  27.     if match == 0:
  28.         # match line begin with numbers
  29.         lineMatch = re.match(r"s+[0-9]+s+.*",line,flags=re.I)
  30.         if lineMatch:
  31.             lineTmp = lineMatch.group(0)
  32.             match = match + 1
  33.             continue
  34.  
  35.     elif match == 1:
  36.         # match line begin with numbers
  37.         lineMatch = re.match(r"s+[0-9]+s+.*",line,flags=re.I)
  38.         if lineMatch:
  39.             # match only query
  40.             lineMatchQuery = re.match(r".*Querys+(.*)",lineTmp,flags=re.I)
  41.             if lineMatchQuery:
  42.                 lineTmp = lineMatchQuery.group(1)
  43.                 # remove extra space
  44.                 lineTmp = re.sub(r"s+", " ",lineTmp)
  45.                 # replace values (value) to values (x)
  46.                 lineTmp = re.sub(r"valuess*(.*?)", "values (x)",lineTmp,flags=re.I)
  47.                 # replace filed = ‘value’ to filed = ‘x’
  48.                 lineTmp = re.sub(r"(=|>|<|>=|<=)s*(‘|").*?2","\1 ‘x’",lineTmp)
  49.                 # replace filed = value to filed = x
  50.                 lineTmp = re.sub(r"(=|>|<|>=|<=)s*[0-9]+","\1 x",lineTmp)
  51.                 # replace like ‘value’ to like ‘x’
  52.                 lineTmp = re.sub(r"likes+(‘|").*?1","like ‘x’",lineTmp,flags=re.I)
  53.                 # replace in (value) to in (x)
  54.                 lineTmp = re.sub(r"ins+(.*?)","in (x)",lineTmp,flags=re.I)
  55.                 # replace limit x,y to limit
  56.                 lineTmp = re.sub(r"limit.*","limit",lineTmp,flags=re.I)
  57.                 
  58.                 print (lineTmp)
  59.  
  60.             match = 1
  61.             lineTmp = lineMatch.group(0)
  62.         else:   
  63.             lineTmp += line
  64.             match = 1
  65.  
  66. logFo.close()

使用方法:

  1. analysis-general-log.py general.log | sort | uniq -c | sort -nr


1032 SELECT * FROM wp_comments WHERE ( comment_approved = ‘x’ OR comment_approved = ‘x’ ) AND comment_post_ID = x ORDER BY comment_date_gmt DESC
653 SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id in (x) ORDER BY meta_id ASC
527 SELECT FOUND_ROWS()
438 SELECT t.*, tt.* FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id WHERE tt.taxonomy = ‘x’ AND t.term_id = x limit
341 SELECT option_value FROM wp_options WHERE option_name = ‘x’ limit
329 SELECT t.*, tt.*, tr.object_id FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON tt.term_id = t.term_id INNER JOIN wp_term_relationships AS tr ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy in (x) AND tr.object_id in (x) ORDER BY t.name ASC
311 SELECT wp_posts.* FROM wp_posts WHERE 1= x AND wp_posts.ID in (x) AND wp_posts.post_type = ‘x’ AND ((wp_posts.post_status = ‘x’)) ORDER BY wp_posts.post_date DESC
219 SELECT wp_posts.* FROM wp_posts WHERE ID in (x)
218 SELECT tr.object_id FROM wp_term_relationships AS tr INNER JOIN wp_term_taxonomy AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy in (x) AND tt.term_id in (x) ORDER BY tr.object_id ASC
217 SELECT wp_posts.* FROM wp_posts WHERE 1= x AND wp_posts.ID in (x) AND wp_posts.post_type = ‘x’ AND ((wp_posts.post_status = ‘x’)) ORDER BY wp_posts.menu_order ASC
202 SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1= x AND wp_posts.post_type = ‘x’ AND (wp_posts.post_status = ‘x’) ORDER BY wp_posts.post_date DESC limit
118 SET NAMES utf8
115 SET SESSION sql_mode= ‘x’
115 SELECT @@SESSION.sql_mode
112 SELECT option_name, option_value FROM wp_options WHERE autoload = ‘x’
111 SELECT user_id, meta_key, meta_value FROM wp_usermeta WHERE user_id in (x) ORDER BY umeta_id ASC
108 SELECT YEAR(min(post_date_gmt)) AS firstdate, YEAR(max(post_date_gmt)) AS lastdate FROM wp_posts WHERE post_status = ‘x’
108 SELECT t.*, tt.* FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id WHERE tt.taxonomy in (x) AND tt.count > x ORDER BY tt.count DESC limit
107 SELECT t.*, tt.* FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id WHERE tt.taxonomy in (x) AND t.term_id in (x) ORDER BY t.name ASC
107 SELECT * FROM wp_users WHERE ID = ‘x’
106 SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1= x AND wp_posts.post_type = ‘x’ AND (wp_posts.post_status = ‘x’) AND post_date > ‘x’ ORDER BY wp_posts.post_date DESC limit
106 SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1= x AND wp_posts.post_type = ‘x’ AND (wp_posts.post_status = ‘x’) AND post_date > ‘x’ ORDER BY RAND() DESC limit
105 SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1= x AND wp_posts.post_type = ‘x’ AND (wp_posts.post_status = ‘x’) AND post_date > ‘x’ ORDER BY wp_posts.comment_count DESC limit

Zabbix监控Memcached PHP-FPM Tomcat Nginx MySQL 网站日志

Zabbix作为监控软件非常的灵活,支持的数据类型非常丰富,比如数字(无正负),数字(浮点),日志,文字等。我们需要做的就是使用脚本来收集好数据,然后zabbix收集并画图,设置告警线。这里我们来学习使用Zabbix监控Memcached、PHP-FPM、Tomcat、Nginx、MySQL及网站日志。

 

Memcached监控

 

自定义键值

 

  1. UserParameter=memcached.stat[*],/data/sh/memcached-status.sh "$1"

memcached-status.sh脚本内容为:

  1. #!/bin/bash
  2.  
  3. item=$1
  4. ip=127.0.0.1
  5. port=11211
  6. (echo "stats";sleep 0.5) | telnet $ip $port 2>/dev/null | grep "STAT $itemb" | awk ‘{print $3}’

 

导入模板

 

memcached zabbix模板下载

 

PHP-FPM监控

 

配置php-fpm状态页

 

打开php-fpm.conf配置文件,添加如下配置后重启php:

  1. pm.status_path = /fpm_status

 

自定义键值

 

  1. UserParameter=php-fpm[*],/data/sh/php-fpm-status.sh "$1"

php-fpm-status.sh脚本内容:

  1. #!/bin/bash
  2. ##################################
  3. # Zabbix monitoring script
  4. #
  5. # php-fpm:
  6. #  – anything available via FPM status page
  7. #
  8. ##################################
  9. # Contact:
  10. [email protected]
  11. ##################################
  12. # ChangeLog:
  13. #  20100922        VV        initial creation
  14. ##################################
  15.  
  16. # Zabbix requested parameter
  17. ZBX_REQ_DATA="$1"
  18.  
  19. # FPM defaults
  20. URL="http://localhost/fpm_status"
  21. WGET_BIN="/usr/bin/wget"
  22.  
  23. #
  24. # Error handling:
  25. #  – need to be displayable in Zabbix (avoid NOT_SUPPORTED)
  26. #  – items need to be of type "float" (allow negative + float)
  27. #
  28. ERROR_NO_ACCESS_FILE="-0.9900"
  29. ERROR_NO_ACCESS="-0.9901"
  30. ERROR_WRONG_PARAM="-0.9902"
  31. ERROR_DATA="-0.9903" # either can not connect /        bad host / bad port
  32.  
  33. # save the FPM stats in a variable for future parsing
  34. FPM_STATS=$($WGET_BIN -q $URL -O – 2> /dev/null)
  35.  
  36. # error during retrieve
  37. if [ $? -ne 0 -o -z "$FPM_STATS" ]; then
  38.   echo $ERROR_DATA
  39.   exit 1
  40. fi
  41.  
  42. #
  43. # Extract data from FPM stats
  44. #
  45. RESULT=$(echo "$FPM_STATS" | sed -n -r "s/^$ZBX_REQ_DATA: +([0-9]+)/1/p")
  46. if [ $? -ne 0 -o -z "$RESULT" ]; then
  47.     echo $ERROR_WRONG_PARAM
  48.     exit 1
  49. fi
  50.  
  51. echo $RESULT
  52.  
  53. exit 0

 

导入模板

 

php-fpm zabbix模板下载

 

Tomcat监控

 

刚开始决定监控Tomcat时,使用的是JMX,不过这货设置太复杂了,而且对防火墙要求还挺高,需要开放几个端口。只好使用Tomcat自带的状态页来监控了。

 

自定义键值

 

  1. UserParameter=tomcat.status[*],/data/sh/tomcat-status.py $1

因为需要解析到xml,所以还是决定用python实现比较方便。
/data/sh/tomcat-status.py脚本内容:

  1. #!/usr/bin/python
  2. import urllib2
  3. import xml.dom.minidom
  4. import sys
  5.  
  6. url = ‘http://127.0.0.1:8080/manager/status?XML=true’
  7. username = ‘username’
  8. password = ‘password’
  9.  
  10. passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
  11. passman.add_password(None, url, username, password)
  12. authhandler = urllib2.HTTPBasicAuthHandler(passman)
  13. opener = urllib2.build_opener(authhandler)
  14. urllib2.install_opener(opener)
  15. pagehandle = urllib2.urlopen(url)
  16. xmlData = pagehandle.read()
  17. doc = xml.dom.minidom.parseString(xmlData) 
  18.  
  19. item = sys.argv[1]
  20.  
  21. if item == "memory.free":
  22. print  doc.getElementsByTagName("memory")[0].getAttribute("free")
  23. elif item == "memory.total":
  24. print  doc.getElementsByTagName("memory")[0].getAttribute("total")
  25. elif item == "memory.max":
  26. print  doc.getElementsByTagName("memory")[0].getAttribute("max")
  27. elif item == "threadInfo.maxThreads":
  28. print  doc.getElementsByTagName("threadInfo")[0].getAttribute("maxThreads")
  29. elif item == "threadInfo.currentThreadCount":
  30. print  doc.getElementsByTagName("threadInfo")[0].getAttribute("currentThreadCount")
  31. elif item == "threadInfo.currentThreadsBusy":
  32. print  doc.getElementsByTagName("threadInfo")[0].getAttribute("currentThreadsBusy")
  33. elif item == "requestInfo.maxTime":
  34. print  doc.getElementsByTagName("requestInfo")[0].getAttribute("maxTime")
  35. elif item == "requestInfo.processingTime":
  36. print  doc.getElementsByTagName("requestInfo")[0].getAttribute("processingTime")
  37. elif item == "requestInfo.requestCount":
  38. print  doc.getElementsByTagName("requestInfo")[0].getAttribute("requestCount")
  39. elif item == "requestInfo.errorCount":
  40. print  doc.getElementsByTagName("requestInfo")[0].getAttribute("errorCount")
  41. elif item == "requestInfo.bytesReceived":
  42. print  doc.getElementsByTagName("requestInfo")[0].getAttribute("bytesReceived")
  43. elif item == "requestInfo.bytesSent":
  44. print  doc.getElementsByTagName("requestInfo")[0].getAttribute("bytesSent")
  45. else:
  46. print "unsupport item."

这个脚本是监控Tomcat7的,Tomcat6没有试过,应该区别在状态页的url以及管理页面的用户密码设置上。以上脚本可运行需要在tomcat-users.xml里添加用户,至少权限为manager-status。

 

导入模板

 

tomcat zabbix模板下载

 

Nginx监控

 

配置Nginx状态页

 

在nginx配置文件server{}中加入:

  1. location /nginx_status {
  2.     stub_status on;
  3.     access_log off;
  4. }

 

自定义键值

 

  1. UserParameter=nginx[*],/data/sh/nginx-status.sh "$1"

nginx-status.sh脚本内容:

  1. #!/bin/bash
  2. ##################################
  3. # Zabbix monitoring script
  4. #
  5. # nginx:
  6. #  – anything available via nginx stub-status module
  7. #
  8. ##################################
  9. # Contact:
  10. [email protected]
  11. ##################################
  12. # ChangeLog:
  13. #  20100922        VV        initial creation
  14. ##################################
  15.  
  16. # Zabbix requested parameter
  17. ZBX_REQ_DATA="$1"
  18. ZBX_REQ_DATA_URL="$2"
  19.  
  20. # Nginx defaults
  21. URL="http://127.0.0.1/nginx_status"
  22. WGET_BIN="/usr/bin/wget"
  23.  
  24. #
  25. # Error handling:
  26. #  – need to be displayable in Zabbix (avoid NOT_SUPPORTED)
  27. #  – items need to be of type "float" (allow negative + float)
  28. #
  29. ERROR_NO_ACCESS_FILE="-0.9900"
  30. ERROR_NO_ACCESS="-0.9901"
  31. ERROR_WRONG_PARAM="-0.9902"
  32. ERROR_DATA="-0.9903" # either can not connect /        bad host / bad port
  33.  
  34. # save the nginx stats in a variable for future parsing
  35. NGINX_STATS=$($WGET_BIN -q $URL -O – 2> /dev/null)
  36.  
  37. # error during retrieve
  38. if [ $? -ne 0 -o -z "$NGINX_STATS" ]; then
  39.   echo $ERROR_DATA
  40.   exit 1
  41. fi
  42.  
  43. #
  44. # Extract data from nginx stats
  45. #
  46. case $ZBX_REQ_DATA in
  47.   active_connections)   echo "$NGINX_STATS" | head -1             | cut -f3 -d’ ‘;;
  48.   accepted_connections) echo "$NGINX_STATS" | grep -Ev ‘[a-zA-Z]’ | cut -f2 -d’ ‘;;
  49.   handled_connections)  echo "$NGINX_STATS" | grep -Ev ‘[a-zA-Z]’ | cut -f3 -d’ ‘;;
  50.   handled_requests)     echo "$NGINX_STATS" | grep -Ev ‘[a-zA-Z]’ | cut -f4 -d’ ‘;;
  51.   reading)              echo "$NGINX_STATS" | tail -1             | cut -f2 -d’ ‘;;
  52.   writing)              echo "$NGINX_STATS" | tail -1             | cut -f4 -d’ ‘;;
  53.   waiting)              echo "$NGINX_STATS" | tail -1             | cut -f6 -d’ ‘;;
  54.   *) echo $ERROR_WRONG_PARAM; exit 1;;
  55. esac
  56.  
  57. exit 0

 

导入模板

 

nginx zabbix模板下载

 

MySQL监控

 

MySQL的监控,zabbix是默认支持的,已经有现成的模板,现成的键值,我们需要做的只是在/var/lib/zabbix里新建一个.my.cnf文件,内容如下:

  1. [client]
  2. host=127.0.0.1
  3. port=1036
  4. user=root
  5. password=root

 

网站日志监控

 

配置日志格式

 

我们假设你用的web服务器是Nginx,我们添加一个日志格式,如下:

  1. log_format withHost  ‘$remote_addrt$remote_usert$time_localt$hostt$requestt’
  2.                 ‘$statust$body_bytes_sentt$http_referert’
  3.                 ‘$http_user_agent’;

我们使用tab作分隔符,为了方便awk识别列的内容,以防出错。
然后再设置全局的日志,其它server就不需要设置日志了:

  1. access_log  /data/home/logs/nginx/$host.log withHost;

 

定时获取一分钟日志

 

设置一个定时任务:

  1. * * * * * /data/sh/get_nginx_access.sh

脚本内容为:

  1. #!/bin/bash
  2.  
  3. logDir=/data/home/logs/nginx/
  4. logNames=`ls ${logDir}/*.*.log  |awk -F"/" ‘{print $NF}’`
  5.  
  6. for $logName in $logNames;
  7. do
  8. #设置变量
  9. split_log="/tmp/split_$logName"
  10. access_log="${logDir}/$logName"
  11. status_log="/tmp/$logName"
  12.  
  13. #取出最近一分钟日志
  14. tac $access_log  | awk ‘
  15. BEGIN{
  16. FS="t"
  17. OFS="t"
  18. cmd="date -d "1 minute ago" +%H%M%S"
  19. cmd|getline oneMinuteAgo
  20. }
  21. {
  22. $3 = substr($3,13,8)
  23. gsub(":","",$3)
  24. if ($3>=oneMinuteAgo){
  25. print
  26. } else {
  27. exit;
  28. }
  29. }’ > $split_log
  30.  
  31.  
  32. #统计状态码个数
  33. awk -F’t’ ‘{
  34. status[$4" "$6]++
  35. }
  36. END{
  37. for (i in status)
  38. {
  39. print i,status[i]
  40. }
  41. }
  42. ‘ $split_log  > $status_log
  43. done

这个定时任务是每分钟执行,因为我们监控的频率是每分钟。添加这个任务是为了取得最近一分钟各域名的日志,以及统计各域名的所有状态码个数,方便zabbix来获取所需的数据。

 

自定义键值

 

  1. UserParameter=nginx.detect,/data/sh/nginx-detect.sh
  2. UserParameter=nginx.access[*],awk -v sum=0 -v domain=$1 -v code=$2 ‘{if($$1 == domain && $$2 == code ){sum+=$$3} }END{print sum}’ /tmp/$1.log
  3. UserParameter=nginx.log[*],awk -F’t’ -v domain=$1 -v code=$2 -v number=$3 -v sum=0 -v line="" ‘{if ($$4 == domain && $$6 == code ){sum++;line=line$$5"n" }}END{if (sum > number) print line}’ /tmp/split_$1.log | sort | uniq -c | sort -nr | head -10 | sed -e ‘s/^/<p>/’ -e ‘s/$/</p>/’

nginx-detect.sh脚本内容为:

  1. #!/bin/bash
  2.  
  3. function json_head {
  4.     printf "{"
  5.     printf ""data":["
  6. }
  7.  
  8. function json_end {
  9.     printf "]"
  10.     printf "}"
  11. }
  12.  
  13. function check_first_element {
  14.     if [[ $FIRST_ELEMENT -ne 1 ]]; then
  15.         printf ","
  16.     fi
  17.     FIRST_ELEMENT=0
  18. }
  19.  
  20. FIRST_ELEMENT=1
  21. json_head
  22.  
  23. logNames=`ls /data/home/logs/nginx/*.*.log |awk -F"/" ‘{print $NF}’`
  24. for logName in $logNames;
  25. do
  26. while read domain code count;do
  27.         check_first_element
  28.         printf "{"
  29.         printf ""{#DOMAIN}":"$domain","{#CODE}":"$code""
  30.         printf "}"
  31. done < /tmp/$logName
  32. done
  33. json_end

这里我们定义了三个键值,nginx.detect是为了发现所有域名及其所有状态码,nginx.access[*]是为了统计指定域名的状态码的数量,nginx.log[*]是为了测试指定域名的状态码超过指定值时输出排在前十的url。我们监控nginx访问日志用到了zabbix的自动发现功能,当我们增加域名时,不需要修改脚本,zabbix会帮助我们自动发现新增的域名并作监控。

 

配置探索规则

 

添加一个探索规则,用来发现域名及状态码,如图:
监控

 

配置监控项原型

 

监控所有的域名及状态码:
监控
域名状态码404超过200次监控:
监控
域名状态码500超过50次监控:
监控

 

配置触发器

 

404状态码超过200告警:
监控

监控
500状态码超过50告警:
监控