Motive

지난 글에서 smartdsmartctl의 간단한 기능에 대해 소개하였다. 나의 궁극적인 목표는 이 S.M.A.R.T. raw 정보를 적절하게 가공하고, 늘 파악하기 쉽도록 실시간 모니터링을 구축하는 것.

환경

– 작업 노드: 그냥 보통 서버
– OS: Ubuntu 18.04
– Shell: Bash 4.4

Parsing data

아래와 같은 커맨드를 입력하면 구체적인 드라이브의 속성을 출력해 준다는 것은 이미 이전 글에서 다뤘다.

$ sudo smartctl -A /dev/sda
smartctl 6.6 2016-05-31 r4324 [x86_64-linux-5.0.0-23-generic] (local build)
Copyright (C) 2002-16, Bruce Allen, Christian Franke, www.smartmontools.org

=== START OF READ SMART DATA SECTION ===
SMART Attributes Data Structure revision number: 1
Vendor Specific SMART Attributes with Thresholds:
ID# ATTRIBUTE_NAME          FLAG     VALUE WORST THRESH TYPE      UPDATED  WHEN_FAILED RAW_VALUE
  5 Reallocated_Sector_Ct   0x0033   100   100   010    Pre-fail  Always       -       0
  9 Power_On_Hours          0x0032   099   099   000    Old_age   Always       -       1320
 12 Power_Cycle_Count       0x0032   099   099   000    Old_age   Always       -       4
177 Wear_Leveling_Count     0x0013   100   100   000    Pre-fail  Always       -       0
179 Used_Rsvd_Blk_Cnt_Tot   0x0013   100   100   010    Pre-fail  Always       -       0
181 Program_Fail_Cnt_Total  0x0032   100   100   010    Old_age   Always       -       0
182 Erase_Fail_Count_Total  0x0032   100   100   010    Old_age   Always       -       0
183 Runtime_Bad_Block       0x0013   100   100   010    Pre-fail  Always       -       0
187 Reported_Uncorrect      0x0032   100   100   000    Old_age   Always       -       0
190 Airflow_Temperature_Cel 0x0032   072   062   000    Old_age   Always       -       28
195 Hardware_ECC_Recovered  0x001a   200   200   000    Old_age   Always       -       0
199 UDMA_CRC_Error_Count    0x003e   100   100   000    Old_age   Always       -       0
235 Unknown_Attribute       0x0012   099   099   000    Old_age   Always       -       1
241 Total_LBAs_Written      0x0032   099   099   000    Old_age   Always       -       364715870

출력이 잘 되는거까지는 좋은데 각 속성ID별 값들을 활용하기에는 약간 아쉬운 출력 포맷이다. (nvidia-smi는 쿼리용 출력 기능을 다 제공하던데… 대기업에서 개발하는 프로그램이라 다르긴 다른건가) 속성ID와 각각의 ID에 따른 raw_value들만 나열해서 출력해주는 간단한 스크립트를 짜보았다.

#!/bin/bash

################################################################################
#   < smarter.sh >
#
#   Script for parsing smartctl output
################################################################################

# Check sudo
if [ "$EUID" -ne 0 ]
    then echo "This script must be run as root."
    exit 0
fi

# Check number of arguments
if [ $# -ne 1 ]
then
    echo "[Error] Number of arguments mush be 1, which is any disk"
    echo "        Example: ./smarter.sh /dev/sda"
    exit 0
fi

# Which disk to be checked
    DISK=${1}
    if [ ! -e ${DISK} ]; then
    echo "Disk" ${DISK} "not found"
    exit 0
fi

# Change input field separator to cut line by line
IFS_backup="${IFS}"
IFS=$'\n'

# Erase header part not used
CUTLINE=`smartctl -A ${DISK} | grep -n 'ID#'`
CUTLINE=${CUTLINE%:*}
RAW=(`smartctl -A ${DISK} | sed -e '1,'${CUTLINE}'d'`)

# Restore IFS
IFS=${IFS_backup}

# Divide them into each array
N=${#RAW[*]}
for (( i=0; i<=$(( N -1 )); i++ ))
do
    ARRAY=(${RAW[${i}]})
    echo -n ${ARRAY[0]} ${ARRAY[9]}" " 
done

exit 1

– Line 9~13는 스크립트가 sudo 권한으로 실행되고 있는지를 체크하는 부분이다. smartctl은 디바이스를 제어하고 분석하므로 관리자 권한이 필수이다.
– Line 15~21는 인자의 개수가 1개가 맞는지 확인하는 부분이다. 스크립트를 인자없이 실행하면 에러 메시지를 띄우면서 예시를 보여준다.
– Line 23~28는 입력된 인자를 읽어 그 드라이브가 정말로 존재하는지 확인한다. 없는 드라이브를 검사할 수는 없기에…
– Line 30~32과 line 40~41은 smartctl 출력을 읽어서 한 줄씩 배열 원소로 할당하기 위한 부분이다. 구체적인 설명은 http://bahndal.egloos.com/583019 를 참고.
– Line 34~37은 출력 내용의 처음에 쓰잘데기 없는 내용 다 치워버리는 부분이다. 내가 관심있는 값은 오직 raw value뿐이기 때문.
– Line 42~48는 parsing이 끝난 attribute ID와 raw value를 번갈아가며 쉘에 출력해준다.

이 스크립트를 실행하면 다음과 같이 결과를 출력해 준다.

$ sudo ./smarter.sh /dev/sda
5 0 9 1320 12 4 177 0 179 0 181 0 182 0 183 0 187 0 190 28 195 0 199 0 235 1 241 364715870

Calculate Health & Save to DB

#!/bin/bash

################################################################################
#   < SmartMon.sh >
#
#   Script for recording S.M.A.R.T. attributes to influxDB server.
################################################################################

# influxDB
DBSERVER='xxxxx'
USERNAME='xxxxx'
PASSWORD='xxxxx'
DATABASE='status'
SERIES='smart'
HOSTNAME='this_computer'
DEVICE=('/dev/sda')

# Critical threshold table
declare -A THR_CRIT
THR_CRIT[5]=1
THR_CRIT[10]=1
THR_CRIT[184]=1
THR_CRIT[187]=1
THR_CRIT[188]=1
THR_CRIT[196]=1
THR_CRIT[197]=1
THR_CRIT[198]=1
THR_CRIT[201]=1

# Warning threshold table
declare -A THR_WARN
THR_WARN[1]=1
THR_WARN[9]=80000
THR_WARN[194]=60

# Send quary regularly
LEN=${#DEVICE[@]}
while :
do
    for (( i=0; i<${LEN}; i++ ))
    do
        echo For ${DEVICE[i]},
        RESULT=(`./smarter/smarter.sh ${DEVICE[i]}`)
        if [ $? -eq 0 ]; then
            echo "Error accured"
            exit 0
        fi

        LEN2=${#RESULT[@]}
        IS_CRIT=0
        IS_WARN=0
        for (( j=0; j<$((${LEN2}/2)); j++ ))
        do
            ID[j]=${RESULT[$((j*2))]}
            VAL[j]=${RESULT[$((j*2+1))]}

            if [ ! -v ${THR_CRIT[${ID[j]}]} ]; then
                if [ "${VAL[j]}" -ge "${THR_CRIT[${ID[j]}]}" ]; then
                    echo Critical for ${ID[j]} with value ${VAL[j]} by threshold ${THR_CRIT[${ID[j]}]}
                    IS_CRIT=1
                fi
            fi

            if [ ! -v ${THR_WARN[${ID[j]}]} ]; then
                if [ "${VAL[j]}" -ge "${THR_WARN[${ID[j]}]}" ]; then
                    echo Warning for ${ID[j]} with value ${VAL[j]} by threshold ${THR_WARN[${ID[j]}]}
                    IS_WARN=1
                fi
            fi
        done

        # Decide status
        if   [ ${IS_CRIT} == 1 ]; then
            STATUS=0
        elif [ ${IS_WARN} == 1 ]; then
            STATUS=1
        else
            STATUS=2
        fi

        echo "Crit:  " ${IS_CRIT}
        echo "Warn:  " ${IS_WARN}
        echo "Status:" ${STATUS}
        echo ""

        influx -host ${DBSERVER} -username ${USERNAME} -password ${PASSWORD} -database ${DATABASE} -execute "INSERT ${SERIES},host=${HOSTNAME},device=${DEVICE[i]} status=${STATUS}"
    done

    sleep 1800
done

– Line 9~16: DB 설정. 본인의 DB서버 주소와 계정, DB명 등을 적어넣으면 된다.
– Line 18~28: 나의 critical threshold 설정. 예를 들면 5번 ID의 attribute가 1이상이면 드라이브가 치명적인 상태라고 정의한다. 누구든지 자기 입맛에 맞게 설정해주면 됨. 예를 들어서, “나는 1번 ID의 값이 1 이상만 돼도 드라이브가 맛탱이가 갔다고 보는데?” 라는 사람은 THR_CRIT[1]=1 를 적어넣어주면 된다.
– Line 30~34: 나의 warning threshold 설정. 예를 들면 9번 ID의 attribute가 80,000 이상이면 (드라이브를 8만 시간 이상 사용하면) 슬슬 불안불안 한 상태라고 정의한다.
– Line 37, 40: 설정한 드라이브에 대한 루프다. Line 16에서 설정한 배열에 있는 드라이브를 순차적으로 검사한다.
– Line 42~47: 위에서 parsing용으로 만들어 둔 스크립트를 실행해서 결과값을 가져온다.
– Line 49~79: Critical threshold와 warning threshold를 읽어와서 드라이브 상태를 검사하고 상태가 좋은지 (2), 불안한지 (1), 맛이 갔는지 (0)를 판별한다.
– Line 86: 드라이브 상태를 DB서버에 기록한다. 나는 influx를 쓰고 있는데, mysql을 쓴다면 mysql 쿼리를 해당 위치에 적용하면 될 것이다.

솔직히 설명을 좀 대충 쓴 감이 있는데 원하는 사람이 있다면 그 부분을 더 자세히 적겠음.

위의 쉘 스크립트는 bash 4 이상에서만 작동할 것임. (아마도?) Line 19, 31에서 선언한 threshold 값들이 연관 배열을 사용하기 때문이다. ( https://linuxhint.com/associative_array_bash/ ) 본인의 bash 버전이 낮다면 4 이상을 다운받아서 설치하면 된다.

DB서버에 기록된 각 드라이브의 health는 line 89에 의하면 30분 마다 기록된다. 이렇게 저장된 검진기록을 적절한 툴로 모니터에 띄우면 됨. 자바스크립트를 써도 좋고 php를 써도 좋다. 나는 개인적으로 Grafana를 쓰고 있음. 그 외에 오픈소스로 여러 DB 모니터링 시스템이 공개되어 있다. 예쁘게 상태 띄우기는 이 글의 주제에서 벗어나므로 생략하도록 한다.

Service 등록

매번 서버를 리붓할 때마다 이 스크립트를 실행해 두기에는 귀찮으므로 systemctl에 데몬을 등록해서 시스템이 시작될 때 자동으로 실행되도록 해주자.

[Unit]
Description=SmartMon
After=influxd.service

[Service]
Type=simple
User=root
WorkingDirectory=[스크립트의 위치]
ExecStart=[스크립트의 위치]/smartMon.sh
Restart=always


[Install]
WantedBy=multi-user.target

해당 내용을 텍스트 에디터로 저장한다. 파일명은 대충 smartMon.service 정도면 되려나? 이 파일을 /etc/systemd/system에 가져다 두고, (보통은 링크를 걸어 두는게 편함.)

$ sudo systemctl daemon-reload
$ sudo systemctl start smartMon

을 해주면 해당 스크립트가 실행될 것이며, 리붓 후에도 자동으로 시작될 것이다.

3번째 줄은 이 service를 influxd가 실행된 이후에 실행하자는 뜻인데, DB서버 접속을 로컬이 아닌 외부로 한다면 필요 없는 라인이므로 지워도 무방하다.

Furthermore…

스크립트에서 호스트네임을 자동으로 읽거나, 디바이스 리스트를 자동으로 생성하는 기능향상, 혹은 config 파일을 분리하는 작업 등을 구상하긴 했는데 귀찮아서 그냥 이대로 쓰는 중임.

2019. 9. 24. 업데이트

호스트네임은 HOSTNAME을 쓰고, 디바이스 리스트는 ls /dev/sd?를 써서 해결했음.

#!/bin/bash

HOSTNAME=`hostname`
DEVICE=(`ls /dev/sd?`)

위의 smartMon.sh 스크립트에서 해당 부분을 위와 같이 수정하면 된다.


0개의 댓글

답글 남기기

Avatar placeholder