引子

监控是最体现一个运维部门的意识和技术水平的。Logs, metrics, and traces are often known as the three pillars of observability. 可观测性成熟度是不断提高的,你永远不能击中靶心,只能不断靠近他。但是如果连靶子都没有,那就有点说不过去了。

我2019年曾经写过 一键部署Zabbix、Grafana、Icinga、SmokePing监控系统 ,也写过 运维的境界 ,在一键部署里面,我分享了部署的源代码,但是因为目前网络安全形势越来越严峻,说得越多,暴露得越多,死得越快。所以暂时不再公开自动化部署代码,实在有需要的,可以私下找我拿,并等待我有空剔除敏感信息后给你。

升级

在我写上一篇文章时,Zabbix还是4.x,经历了这么久后,Zabbix 6.0LTS已经在今年发布,Ubuntu 2204 LTS也于4月份准时发布了,在Ubuntu发布初期,Zabbix还没有适配2204的包,我一直在等,后续终于有了。

Zabbix 6.0 是LTS,6.2不是,但是考虑到,反正我每次都会升级到最新,还是直接追新吧。

从4到6,改变还是非常得大,虽然可以一步步升级上去,但是软件升级大家都知道,升级有时候无法应用新版的配置项,老的数据有可能在升级过程中留下垃圾。操作系统我从来都不会进行大的版本升级,一定是使用“不可变服务器”的理念,重新安装并配置。比如Zabbix,Trigger expression变化很大,导致很多升级后还要根据Changes来微调很麻烦。

为了简化,丢掉包袱,所以重新部署Zabbix并重新添加监控项。通过API把旧数据拉出来,写到新版,然后再慢慢调整一些自定义的。Zabbix的API非常完善,也有Python的SDK(from pyzabbix import ZabbixAPI),所以导入导出很简单。

这里部署,将原来架构里面的SmokePing干掉了,SmokePing实在太老了,性能也很低。目前在测试一个类似的商业产品。

配置

Zabbix 6.0 完善了VMWare的监控。VMware自带一些性能监控,配套软件也有完善的监控,就是有一些经费成本。而且自身查询速度和保留时限有一定限制,所以,有必要使用Zabbix再建立起一个第三方监控。VMware有自动发现虚拟机机制,但是考虑到虚拟机机器名和自建host name会存在冲突,所以关于VMware独立部署了一套Zabbix,只用来观察VMWare相关的性能和事后倒查,不介入处理。

新版Dashboard支持多个Page幻灯片展示。增加了geomap。支持proxy reload配置,支持监控项的立刻检查,方便太多了。

开源软件安装部署很简单,其实更加关键的是配置理念技巧和使用过程中的一些方法论。

Zabbix6自带了300多个模板,比4.x的80个多了几倍,但是也带来干扰太多的问题,常用的不超过10个,所以建立了一个模板组“常用模板”,将最常用的模板加入这个分组授权给最终用户。为了隔离,更清晰分辨出自定义模板,也定义了一个模板组“自定义模板”,所有自定义模板都放入这个组,方便下次升级或者调整。

相比Elastic Stack,Elastic Stack也自带很多开箱可用监控项,也有报警机制,但是权限的灵活度完全没有Zabbix高,所以你可以一起用,Zabbix可作为整个大部门的统一的监控中心,一些要求比较高的应用,可自行再安装Elastic Stack来单独监控。

Zabbix里,用户和host之间不能直接授权,必须通过host groups,灵活度很高,但是对于小团队而言,也会增加复杂度。

Zabbix Agent有了新版agent2,由于很多操作系统默认不带agent2的包,所以被监控服务器还是只用agent即可,Zabbix Server和Proxy可以安装agent2。

新版有了secret macros,密码也可以接Vault。secret macros让宏在UI端只能设置不能查看,也不会被导出。安全了很多,可以放心设置密码了。

Zabbix的告警很灵活,但是没有Owner的概念,理论上,即使你不配置,应当在触发trigger的时候发送给该host对应的owner,但是没有,必须一个个加,这里会比较不灵活一点,而且会担心漏,如果有解决方法记得告诉我。

一旦系统搭建起来后,监控项可慢慢推进,可先添加一些比较多的比如交换机类似的只监控ICMP Ping,把量加上去,再加一些服务器,先用Linux By Zabbix Agent,把CPU内存硬盘先监控起来,再添加一些中间件和服务。再加入Web的监控。一些非标准化安装的服务添加起来会比较麻烦,比如有些网站群部署用的非标准部署的MySQL,很烦。

也需要评估监控和安全的关系。

Web的监控配置还比较麻烦,而且查错手段很少。新版还有Services和SLA的计算。

等一切完善起来后,再遇到问题后找到问题会导致的特征,加入监控,使得下次问题再出现,就会比用户先一步知道。

报警的动态清零也非常重要,我经常看到有些系统报警非常多,但是管理员已经司空见惯了,看这种系统,如同进入一个垃圾场。有些告警确实无法处理掉,但是应当通过某种非常显式的方式Ignore掉,否则新来的人他不知道这个问题是个新问题还是已经很久无法解决的问题了,也会导致多个问题叠加,比如有些服务器的灯,只有一个闪烁的告警状态,如果你让他闪烁不去处理,会导致新的问题出现你无法察觉。

针对特定端口的监控

Zabbix自带了包括HTTP、HTTPS、SMTP等端口的监控,但是有时候我们需要监控一个自定义的端口是否通断没有相关的模板,又由于一个模板只能监控一个端口,所以建立了5个“TCP Port 12345 Service”模板用来给host使用。可以从HTTP Service模板Full Clone一个新的模板,并且改名,再定义一个宏变量{$TCP_PORT},在宏变量里面设定端口值,将Key设定为net.tcp.service[tcp,,{$TCP_PORT}]即可。

如果某个host的端口需要监控的超过5个,则自行建立一个Template,在Template增加Item和Trigger。

TLS证书到期监控

传统方法

在agent1的时代,会使用自定义脚本来监控。

由于传入的参数会直接进入系统以Zabbix用户运行,所以对参数做了判断,防止不必要的Shell Injection。

#!/bin/bash

re='^[a-z0-9\-\_\.]+(\.xmu\.edu\.cn)$'
if ! [[ $1 =~ $re ]]; then
    echo "error: Not Xmu DNS" >&2
    echo "-1"
    exit 1
fi

port="443"
if ! [ -z "$2" ]; then
    port=$2
fi

re='^[0-9]+$'
if ! [[ $port =~ $re ]]; then
    echo "error: port is not number" >&2
    echo "-2"
    exit 1
fi

tls_end_date_str=$(echo | openssl s_client -servername $1 -connect $1:${port} 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | sed -e 's#notAfter=##')

tls_end_date=$(date -d "${tls_end_date_str}" '+%s')
now_date=$(date '+%s')

diff="$((${tls_end_date} - ${now_date}))"

echo $((${diff} / (60 * 60 * 24)))

然后放到

- name: Copy check_tls
  ansible.builtin.copy:
    src: check_tls.sh
    dest: /usr/lib/zabbix/externalscripts
    owner: zabbix
    group: zabbix
    mode: 0774
  become: true

专门建立一个Template用来监控TLS证书,Item的Type为“External check”,Key就写 check_tls.sh[“dog.xmu.edu.cn”] ,返回值选择float,因为有可能有负的天数,一天更新一次。如果直接建立在host上也可,但是为了规范,建议放在独立的Template里。

agent2新版方法

Website certificate by Zabbix agent 2新版自带,Key为web.certificate.get[{$CERT.WEBSITE.HOSTNAME},{$CERT.WEBSITE.PORT},{$CERT.WEBSITE.IP}]。每个要检查的地址新建一个hosts,IP地址随便写,配置域名即可。

对于MySQL的监控

1.Install Zabbix agent and MySQL client.

2.Copy template_db_mysql.conf into folder with Zabbix agent configuration (/etc/zabbix/zabbix_agentd.d/ by default). Don’t forget to restart zabbix-agent.

3.Create MySQL user for monitoring. For example:

CREATE USER ‘zbx_monitor’@’%’ IDENTIFIED BY ‘password’;

GRANT REPLICATION CLIENT,PROCESS,SHOW DATABASES,SHOW VIEW ON . TO ‘zbx_monitor’@’%’;

For more information read the MySQL documentation https://dev.mysql.com/doc/refman/8.0/en/grant.html , please.

4.Create .my.cnf in home directory of Zabbix agent for Linux (/var/lib/zabbix by default) or my.cnf in c:\ for Windows. For example:

[client]

user=’zbx_monitor’

password=’password’

翻译成Ansible脚本就是

- name: Install zabbix-agent
  ansible.builtin.package:
    pkg: zabbix-agent
    state: latest # noqa package-latest
  become: true

- name: Set zabbix-agentd conf server name
  ansible.builtin.lineinfile:
    path: /etc/zabbix/zabbix_agentd.conf
    regexp: 'Server='
    line: 'Server=SOME_IP'
  become: true
  notify: restart zabbix-agent

- name: Copy template_db_mysql.conf into folder with Zabbix agent configuration
  ansible.builtin.copy:
    src: template_db_mysql.conf
    dest: /etc/zabbix/zabbix_agentd.conf.d/
    owner: root
    group: root
    mode: 0644
  become: true
  notify: restart zabbix-agent

- name: Add mysql user
  mysql_user:
    name: "zbx_monitor"
    password: 'SOME_PASSWORD'
    priv: '*.*:REPLICATION CLIENT,PROCESS,SHOW DATABASES,SHOW VIEW'
    state: present
    login_unix_socket: /var/run/mysqld/mysqld.sock
  no_log: true
  become: true

- name: Ensure zabbix agent home dir
  ansible.builtin.file:
    path: '/var/lib/zabbix'
    state: directory
    mode: 0755
    owner: zabbix
    group: zabbix
  become: true

- name: Copy conf
  ansible.builtin.template:
    src: ".my.cnf.j2"
    dest: "/var/lib/zabbix/.my.cnf"
    owner: zabbix
    group: zabbix
    mode: 0664
  become: true
  notify: restart zabbix-agent

- name: Set zabbix home
  block:
    - name: Set zabbix home dir to use .my.cnf
      ansible.builtin.user:
        name: zabbix
        home: /var/lib/zabbix
      become: true
  rescue:
    - name: Kill zabbix agent process
      ansible.builtin.command: "pkill -u zabbix"
      changed_when: false
      become: true
  always:
    - name: Rerun set zabbix home
      ansible.builtin.user:
        name: zabbix
        home: /var/lib/zabbix
      become: true

- name: Start zabbix-agent
  ansible.builtin.service:
    name: zabbix-agent
    state: started
    enabled: true
  become: true

Apache2

Apache2要开status模块,一般默认也都开了,一定要注意限制好IP地址。自带的宏{$APACHE.PROCESS_NAME}默认是httpd,Ubuntu是apache2,要改下否则监控不到。其他默认即可。

企业微信发送提醒

Zabbix 6.0的提醒从原先的3个扩展到了33个,得益于WebHook的普及。我们企业微信发送信息用得比较多,一种方法是在企业微信建立机器人通过WebHook来发送,另外就是直接用Script脚本。以下是我用的代码:

#!/usr/bin/env python3

import json
import sys

import requests
import urllib3
from wechat_config import WECHAT_APP_ID_AGENT_ID, WECHAT_CORP_ID, WECHAT_CORP_SECRET

WECHAT_QIYE_GETTOKEN_ENDPOINT = (
    "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={}&corpsecret={}"
)
WECHAT_QIYE_MESSAGE_SEND_ENDPOINT = (
    "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={}"
)
WECHAT_ACCESSKEY_FILENAME = "/tmp/zabbix_wechat_accesstoken.json"

urllib3.disable_warnings()


def get_access_token_from_server():
    url = WECHAT_QIYE_GETTOKEN_ENDPOINT.format(WECHAT_CORP_ID, WECHAT_CORP_SECRET)

    try:
        r = requests.get(url=url, verify=False)
        return r.json()["access_token"]
    except Exception:
        raise Exception("get access token error. ensure id and password is correct.")


def fresh_access_token_and_cached():
    access_token = get_access_token_from_server()

    with open(WECHAT_ACCESSKEY_FILENAME, "w") as f:
        f.write(access_token)

    return access_token


def get_access_token():
    try:
        with open(WECHAT_ACCESSKEY_FILENAME, "r") as f:
            return f.readline()
    except (FileNotFoundError):
        pass

    return fresh_access_token_and_cached()


def send_and_return_status(access_token, data):
    url = WECHAT_QIYE_MESSAGE_SEND_ENDPOINT.format(access_token)
    r = requests.post(url=url, data=json.dumps(data), verify=False)
    # print(vars(r))
    send_result = r.json()
    return send_result["errcode"] == 0


def send_wechat_message(user, agent_id, subject, content):
    data = {
        "touser": user,
        "msgtype": "text",
        "agentid": agent_id,
        "text": {"content": "{}\n{}".format(subject, content)},
        "safe": "0",
    }

    is_send_success = False
    access_token = get_access_token()
    try:
        is_send_success = send_and_return_status(access_token, data)
    except Exception:
        pass

    if not is_send_success:
        access_token = fresh_access_token_and_cached()
        is_send_success = send_and_return_status(access_token, data)

    if is_send_success:
        # print("successed.")
        pass
    else:
        raise Exception("send error, maybe user donot exists.")


if __name__ == "__main__":
    user = sys.argv[1]
    subject = str(sys.argv[2])
    content = str(sys.argv[3])

    send_wechat_message(user, WECHAT_APP_ID_AGENT_ID, subject, content)