どこまでワンライナーで実現できるか!? サーバ負荷のボトルネック箇所をすばやく確認できるようにしてみる

ゆめみさんのAdvent Calendarのイベントに乗っかってチャレンジしてみました。

# やってみたこと

よく、サーバが重いなーっといったときにpsコマンドやtopコマンドでリソースの状況をチェックしたり、ディスクがいっぱいになってきて何を消そうかというときに容量の多いディレクトリがどこかをチェックしたり行うかと思います。

こういったものをちょっとワンライナーを工夫してすばやく・わかりやすく確認できるようにしてみようと思います。
普段からよく使っているものというわけではなく、今回のこの機会に便利になるようなワンライナーを設定してみたという話です。

ただ、単純に見れるようにするだけだとあれなので、どこまでコンソール上で状況をワンライナーで表現できるかを試しています。

# 作成したワンライナー

今回は4つの設定を試してます。

* CPU使用率のTop10のプロセスリストを表示
* メモリ使用率のTop10のプロセスリストを表示
* ディレクトリサイズのTop10を表示
* CPU、メモリ使用率Top10を常時表示

CentOS7系のbash前提となります。その他環境だと動かないかもしれないのでご了承ください。
必要なパッケージは極力ミニマムで済むようにしています。

利用しているのはdu,awk,expr,echo,ps,head,cut,read,while,xargs,ifとかぐらいです。

## CPUの使用率のTop 10プロセスリスト(bneck.cpu)

psコマンドの実行結果のなかからpcpuのカラム情報を抽出し、パーセンテージの高いものから順に10件並べています。
視覚的にもわかりやすいように、パーセンテージを示すバーもセットで表示してみました。

```shell

alias bneck.cpu='echo -e "\033[37;44;1mCPU Top10\033[m"; echo -e "%CPU PID COMMAND"| awk '\''{printf "\033[0;36;4m%-30s %-10s %s\033[m\n",$1,$2,$3}'\''; ps ahxo pcpu,comm,pid k -pcpu |head -n 10 | while read line; do value=`echo ${line} |xargs |cut -d " " -f 1`; proc_comm=`echo ${line} |xargs |cut -d " " -f 2`; pid=`echo ${line} |xargs | cut -d " " -f 3`; percent=`expr ${value%.*} / 5`;bar=`for i in \`seq 0 ${percent}\`;do echo -n "#"; done`; if [ ${value%.*} -gt 50 ]; then color_code=31; elif [ ${value%.*} -gt 30 ]; then color_code=33; else color_code=32; fi; echo -e "$value $bar $pid $proc_comm $color_code" | awk '\''{printf "%-7s [\033[0;%d;1m%-20s\033[0;m] %-10s %s\n",$1,$5,$2,$3,$4}'\''; done'
```
このコマンドを実行するとこんな感じ。

![oneliner1.png](https://qiita-image-store.s3.amazonaws.com/0/45851/43fd8736-d434-c52b-2758-91f7932b0ae4.png)

## メモリの使用率のTop 10プロセスリスト(bneck.mem)

次にメモリの使用率のTop 10です。こちらもCPUと同じくpsコマンドのpmemカラムの情報をピックアップして表示するようにしています。
ワンライナーの中身的には先程のCPUのものとほぼ同じです。

```shell
alias bneck.mem='echo -e "\033[37;44;1mMemory Top10\033[m"; echo -e "%MEM PID COMMAND"| awk '\''{printf "\033[0;36;4m%-30s %-10s %s\033[m\n",$1,$2,$3}'\''; ps ahxo pmem,comm,pid k -pmem |head -n 10 | while read line; do value=`echo ${line} |xargs |cut -d " " -f 1`; proc_comm=`echo ${line} |xargs |cut -d " " -f 2`; pid=`echo ${line} |xargs | cut -d " " -f 3`; percent=`expr ${value%.*} / 5`;bar=`for i in \`seq 0 ${percent}\`;do echo -n "#"; done`; if [ ${value%.*} -gt 50 ]; then color_code=31; elif [ ${value%.*} -gt 30 ]; then color_code=33; else color_code=32; fi; echo -e "$value $bar $pid $proc_comm $color_code" | awk '\''{printf "%-7s [\033[0;%d;1m%-20s\033[0;m] %-10s %s\n",$1,$5,$2,$3,$4}'\''; done'
```

![oneliner2.png](https://qiita-image-store.s3.amazonaws.com/0/45851/430cc876-d705-42e6-763c-4ecfaa25ea01.png)


## ディレクトリサイズのTop10(bneck.dir)

こちらは、duコマンドで指定のディレクトリ配下のディレクトリをサイズ順に並べるように表示しています。
再帰的にディレクトリ配下を一気に確認できるようにしてもよかったですが、応答のスピードのことも考え、ディレクトリは1階層下のもののみを対象にしています。
その代わり、表示されたディレクトリを選択してドリルダウンで辿っていけるようにしてみました。

指定のディレクトリのみの情報をとりたかったので、aliasだと引数渡せなかったので、ここだけはaliasではなくfunctionとして設定してます。

```shell
function bneck.dir () {
  total_size=`du -s -b $1|cut -f 1`; kib_total_size=`expr $total_size / 1024`; mib_total_size=`expr $kib_total_size / 1024`; gib_total_size=`expr $mib_total_size / 1024`; if [ $gib_total_size -ne 0 ]; then view_total_size=${gib_total_size}GiB; elif [ $mib_total_size -ne 0 ]; then view_total_size=${mib_total_size}MiB; elif [ $kib_total_size -ne 0 ]; then view_total_size=${kib_total_size}KiB; else view_total_size=${total_size}B; fi; echo -e "\033[37;41;1m $1 (total: $view_total_size)\033[m\033[37;44;1m directory size Top10\033[m"; echo -e "SIZE PUSED DIR"| awk '{printf "\033[0;36;4m%-15s %-27s %s\033[m\n",$1,$2,$3}'; count=0; select_col=2; command_result=`du -b -d1 $1 | sort -rn |head -n 10`; IFS=$'\n'; while true; do for line in $command_result; do if [ $count -eq 0 ]; then count=1; continue; else count=`expr $count + 1`; fi; size=`echo ${line} |cut -f 1`; kib_size=`expr $size / 1024`; mib_size=`expr $kib_size / 1024`; gib_size=`expr $mib_size / 1024`; if [ $gib_size -ne 0 ]; then view_size=${gib_size}GiB; elif [ $mib_size -ne 0 ]; then view_size=${mib_size}MiB; elif [ $kib_size -ne 0 ]; then view_size=${kib_size}KiB; else view_size=${size}B; fi; dir_name=`echo ${line} |cut -f 2`;percent=`expr ${size} \* 100 / ${total_size}`;bar_num=`expr $percent / 5`;bar=`for i in \`seq 0 ${bar_num}\`; do echo -n '#'; done`; if [ $bar_num -gt 10 ]; then color_code=31; elif [ $bar_num -gt 6 ]; then color_code=33; else color_code=32; fi;if [ $select_col -eq $count ]; then select_dir=$dir_name; selected_color="30;43"; else unset selected_color; fi; echo -e "$view_size ${percent}% $color_code $bar $dir_name $selected_color"| awk '{printf "%-15s %4s [\033[0;%d;1m%-20s\033[m] \033[%sm%s\033[m\n",$1,$2,$3,$4,$6,$5}'; done; read -sn1 key; up_count=`expr $count - 1`; if [ "$key" = "j" -a $select_col -lt $count ]; then select_col=`expr $select_col + 1`; echo -e "\033[${up_count}A\033[0J\033[1A"; count=0; elif [ "$key" = "k" -a $select_col -gt 2 ]; then select_col=`expr $select_col - 1`; echo -e "\033[${up_count}A\033[0J\033[1A"; count=0; elif [ "$key" = "" -o "$key" = "l" ]; then echo -e "\033[`expr $count + 1`A\033[10M\033[1A"; bneck.dir $select_dir; elif [ "$key" = "h" ];then echo -e "\033[`expr $count + 1`A\033[10M\033[1A"; new_dir=${1%/*}; if [ -z $new_dir ]; then new_dir="/"; fi; bneck.dir $new_dir; else echo -e "\033[${up_count}A\033[10M\033[1A"; count=0; fi; done;
}
```

わかりやすく改行してみるとこんな感じです。(あまり良い書き方ではないかと思いますが。。)

```shell
function bneck.dir () {
  total_size=`du -s -b $1|cut -f 1`
 kib_total_size=`expr $total_size / 1024`
 mib_total_size=`expr $kib_total_size / 1024`
 gib_total_size=`expr $mib_total_size / 1024`
 if [ $gib_total_size -ne 0 ]; then
   view_total_size=${gib_total_size}GiB
 elif [ $mib_total_size -ne 0 ]; then
   view_total_size=${mib_total_size}MiB
 elif [ $kib_total_size -ne 0 ]; then
   view_total_size=${kib_total_size}KiB
 else
   view_total_size=${total_size}B
 fi
 echo -e "\033[37;41;1m $1 (total: $view_total_size)\033[m\033[37;44;1m directory size Top10\033[m"
 echo -e "SIZE PUSED DIR"| awk '{printf "\033[0;36;4m%-15s %-27s %s\033[m\n",$1,$2,$3}'

 count=0
 select_col=2
 command_result=`du -b -d1 $1 | sort -rn |head -n 10`
 while true
 do
   for line in $command_result
   do
     if [ $count -eq 0 ]; then
       count=1
       continue
     else
       count=`expr $count + 1`
     fi
     size=`echo ${line} |cut -f 1`
     kib_size=`expr $size / 1024`
     mib_size=`expr $kib_size / 1024`
     gib_size=`expr $mib_size / 1024`
     if [ $gib_size -ne 0 ]; then
       view_size=${gib_size}GiB
     elif [ $mib_size -ne 0 ]; then
       view_size=${mib_size}MiB
     elif [ $kib_size -ne 0 ]; then
       view_size=${kib_size}KiB
     else
       view_size=${size}B
     fi
     dir_name=`echo ${line} |cut -f 2`
     percent=`expr ${size} \* 100 / ${total_size}`
     bar_num=`expr $percent / 5`
     bar=`for i in \`seq 0 ${bar_num}\`; do echo -n '#'; done`
     if [ $bar_num -gt 10 ]; then
       color_code=31
     elif [ $bar_num -gt 6 ]; then
       color_code=33
     else
       color_code=32
     fi
     if [ $select_col -eq $count ]; then
       select_dir=$dir_name
       selected_color="30;43"
     else
       unset selected_color
     fi
     echo -e "$view_size ${percent}% $color_code $bar $dir_name $selected_color"| awk '{printf "%-15s %4s [\033[0;%d;1m%-20s\033[m] \033[%sm%s\033[m\n",$1,$2,$3,$4,$6,$5}'
   done
   read -sn1 key
   up_count=`expr $count - 1`
   if [ "$key" = "j" -a $select_col -lt $count ]; then
     select_col=`expr $select_col + 1`
     echo -e "\033[${up_count}A\033[0J\033[1A"
     count=0
   elif [ "$key" = "k" -a $select_col -gt 2 ]; then
     select_col=`expr $select_col - 1`
     echo -e "\033[${up_count}A\033[0J\033[1A"; count=0
   elif [ "$key" = "" -o "$key" = "l" ]; then
     echo -e "\033[`expr $count + 1`A\033[10M\033[1A"
     bneck.dir $select_dir
   elif [ "$key" = "h" ];then
     echo -e "\033[`expr $count + 1`A\033[10M\033[1A"
     new_dir=${1%/*}
     if [ -z $new_dir ]; then
       new_dir="/"
     fi
     bneck.dir $new_dir
   else
     echo -e "\033[${up_count}A\033[10M\033[1A"
     count=0
   fi
 done;
}
```

実行結果はこんな感じ。



## CPU、メモリ使用率Top10を常時表示(bneck)

最後は先程のCPU、メモリ使用率Top10を常に表示しておくためのワンライナーです。

```shell
alias bneck='while true; do bneck.cpu; bneck.mem; sleep 1; echo -e "\033[24A\033[22M\033[2K\033[1A"; done'
```
ここまでの各ワンライナーを実行すると以下のようになります。

[![asciicast](https://asciinema.org/a/C9GATFQ7Q4iKuXeEZyvlbNk9P.svg)](https://asciinema.org/a/C9GATFQ7Q4iKuXeEZyvlbNk9P)

# まとめ

ちょっと便利にするためのお手軽なワンライナーとは程遠い形のものですが、このちょっとした機会をきっかけに以下のような知識を得ました。

* エスケープシーケンス
    * ターミナル画面のクリア方法
    * 色付け、装飾方法
    * カーソル移動方法
* awk
    * awkのprintfを使った表示揃えの方法
    * alias設定の中でのprintf利用時のシングルクォーテーションの書き方
* read
    * 1文字入力の受付と、入力文字の非表示化

お遊び要素もふくめて実施したことではありますが、***手を動かしてやってみる***と何かと知見は得られるものですね。