由於程式設計不合理或者瞬間高併發訪問時,很有可能會觸發OOM(Out of memory),這裡指的是作業系統級別的OOM。具體什麼是OOM,以及怎樣發生這裡不在贅述,因為筆者認為這是IT從業工作者的基本常識了。本篇主要記錄一下生產環境時對發生OOM的程式進行監控,便於我們及時發現以及事後問題的覆盤。
在做這個監控時,筆者也做了很多考察搜尋,幻想著會有那麼一兩個成熟的開源軟體能實現這個監控,事與願違,筆者並未找到這樣的工具,無奈之下,只好自己動手實現了一個略顯粗糙的程式來達到我的目的。
實現思路:
Apr 18 12:11:25 php001 kernel: Out of memory: Kill process 13546 (php-fpm) score 31 or sacrifice child
系統每次觸發OOM時會在/var/log/message檔案下記下當時系統的執行狀況,以及被殺程式的pid和評分。因此筆者從此檔案入手,寫下了這樣一個指令碼。
1、目錄結構如下:
oom_monitor位於共享儲存NFS上,生產環境的機器都掛載了該NFS,因此所有機器上都會有這麼一個目錄
[admin@prod.001:/mnt/alinas]$ tree -L 1 oom_monitor/
oom_monitor/
├── bin #存放可執行的指令碼檔案
└── log #存放日誌檔案
[admin@prod.001:/mnt/alinas]$ tree -L 1 oom_monitor/bin/
oom_monitor/bin/
├── oom_check.sh #過濾"Out of memory"從/var/log/message,並生成對應檔案儲存在log目錄下,用於後面的傳送警報
├── oom_dingding.py #傳送OOM對應資訊到釘釘群
├── oom_mail.py #傳送OOM對應資訊到郵箱
└── oom_send.sh #用於觸發傳送報警資訊到釘釘和郵箱
2、具體內容如下:
[admin@prod.001:/mnt/nfs/oom_monitor/bin]$ cat oom_check.sh
#!/bin/sh
#獲取主機名
host_name=`hostname`
#定義獲取到的OOM日誌儲存位置
oom_scrape_file=/mnt/nfs/oom_monitor/log/oom_scrape_$host_name
#定義上次發生OOM時的對應資訊,用於去重,防止重複報警
old_oom_scrape_file=/mnt/nfs/oom_monitor/log/old_oom_scrape_$host_name
#獲取OOM報警資訊
msg=$(sudo grep -i "out of memory" /var/log/messages|awk 'END {print}')
#獲取報警產生的時間
time=`echo $msg | awk '{print $3}'`
#獲取被殺的程式型別(java/php/mysql/...)
killed=`echo $msg | awk 'END {print $12}'`
#獲取上次OOM的資訊
old_msg=`cat $old_oom_scrape_file`
#判斷兩次資訊是否相同,相同則不再記錄此次資訊,防止重複報警
if [ "$msg" == "$old_msg" ];then
exit 1
else
#如果兩次報警資訊不相同則把這次獲取的資訊覆蓋上次的資訊
[ ! -z "$msg" ] && echo "$msg" > $old_oom_scrape_file
fi
#記錄此次資訊持久化到檔案
if [ ! -z "$msg" ];then
echo > $oom_scrape_file
echo -e "發生時間: $time" >> $oom_scrape_file
echo -e "日誌資訊: $msg" >> $oom_scrape_file
echo -e "被殺程式: $killed" >> $oom_scrape_file
echo -e "發生主機: $host_name" >> $oom_scrape_file
fi
[admin@prod.001:/mnt/nfs/oom_monitor/bin]$ cat oom_send.sh
#!/bin/sh
#定義獲取到的OOM日誌儲存位置、OOM傳送報警指令碼位置
#這裡用*的原因是:機器很多,每臺機器一個檔案,所以用*
oom_scrape_file=/mnt/nfs/oom_monitor/log/oom_scrape*
oom_warn_script=/mnt/nfs/oom_monitor/bin/oom_mail.py
oom_dingding_script=/mnt/nfs/oom_monitor/bin/oom_dingding.py
History_file=/mnt/nfs/oom_monitor/log/history_oom_scrape
basedir=/mnt/nfs/oom_monitor/log/
cd $basedir
for file in `ls $oom_scrape_file`
do
hostname=`grep "發生主機" $file|awk '{print $2}'`
warn_time=`grep "發生時間" $file |awk '{print $2}'`
killed=`grep "被殺程式" $file |awk '{print $2}'`
#傳送郵件報警,把日誌資訊全部發出去
/usr/local/bin/python3 $oom_warn_script $file
#傳送釘釘報警
/usr/local/bin/python3 $oom_dingding_script $hostname $killed $warn_time
#報警資訊發出後把日誌資訊追加到歷史資訊檔案中,然後刪除對應的oom資訊檔案,防止重複報警
cat $file >> $History_file && mv $file /tmp
done
[admin@prod.001:/mnt/nfs/oom_monitor/bin]$ cat oom_mail.py
#!/usr/bin/env python3
'''
當收到系統OOM時,觸發指令碼發出郵件報警資訊,資訊格式如下:
您的主機「hostname」發生OOM,具體資訊如下:
/var/log/message中的資訊
'''
import os
import smtplib
from email.mime.text import MIMEText
from email.header import Header
import time
import sys
def send_email(file_name):
try:
# 讀取測試報告中的內容作為郵件的內容
with open(file_name, 'r', encoding='utf8') as f:
mail_body = f.read()
# 發件人地址
send_addr = '此處替換為發件人的郵箱使用者名稱'
# 收件人地址
reciver_addr = ['接收人a的郵箱地址','接收人b的郵箱地址',]
# 傳送郵箱的伺服器地址,這裡用的是阿里雲的
mail_server = 'smtp.mxhichina.com'
now = time.strftime("%Y-%m-%d %H:%M:%S")
# 郵件標題
subject = '[OOM報警觸發]' + now
# 發件人的郵箱及郵箱密碼
username = '此處替換為發件人的郵箱使用者名稱'
password = '此處替換為發件人的郵箱密碼'
# 郵箱的內容和標題
message = MIMEText(mail_body, 'html', 'utf8')
message['From'] = send_addr
message['To'] = ','.join(reciver_addr)
message['Subject'] = Header(subject, charset='utf8')
# 傳送郵件,使用的使smtp協議
smtp = smtplib.SMTP()
#埠注意下,通常伺服器的25埠是關閉的,所以我這裡用了80、或者465也闊以
smtp.connect(mail_server,80)
smtp.login(username, password)
smtp.sendmail(send_addr, message['To'].split(','), message.as_string())
smtp.quit()
print("郵件傳送成功!")
except:
print("傳送郵件失敗!")
send_email(file_name=sys.argv[-1])
[admin@prod.001:/mnt/nfs/oom_monitor/bin]$ cat oom_dingding.py
#!/usr/bin/env python3
import json
import requests
import datetime
import sys
def sendmessage(hostname,killed,warn_time):
now_time=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
#替換成你自己的釘釘群的webhook
url = 'https://oapi.dingtalk.com/robot/send?access_token=917b00b5faee51bcb67e862c13b0a0ff605f0f74f4f692c9a70fe32351'
HEADERS = {
"Content-Type": "application/json;charset=utf-8"
}
message='''
【OOM報警觸發】:
報警主機:%s
被殺程式:%s
報警時間:%s
''' %(hostname,killed,warn_time)
String_textMsg = {
"msgtype": "text",
"text": {"content": message},
"at": {
"atMobiles": [
"110120119" # 如果需要@某人,這裡寫他的手機號
],
"isAtAll": 0 # 如果需要@所有人,這些寫1
}
}
String_textMsg = json.dumps(String_textMsg)
res = requests.post(url, data=String_textMsg, headers=HEADERS)
print(res.text)
sendmessage(sys.argv[-3],sys.argv[-2],sys.argv[-1])
3、結果演示: