ShellScript
Linux
OneLiner
ワンライナー

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

ゆめみさんの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件並べています。

視覚的にもわかりやすいように、パーセンテージを示すバーもセットで表示してみました。


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


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

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

ワンライナーの中身的には先程のCPUのものとほぼ同じです。

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


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

こちらは、duコマンドで指定のディレクトリ配下のディレクトリをサイズ順に並べるように表示しています。

再帰的にディレクトリ配下を一気に確認できるようにしてもよかったですが、応答のスピードのことも考え、ディレクトリは1階層下のもののみを対象にしています。

その代わり、表示されたディレクトリを選択してドリルダウンで辿っていけるようにしてみました。

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

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;
}

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

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を常に表示しておくためのワンライナーです。

alias bneck='while true; do bneck.cpu; bneck.mem; sleep 1; echo -e "\033[24A\033[22M\033[2K\033[1A"; done'

ここまでの各ワンライナーを実行すると以下のようになります。

asciicast


まとめ

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


  • エスケープシーケンス


    • ターミナル画面のクリア方法

    • 色付け、装飾方法

    • カーソル移動方法



  • awk


    • awkのprintfを使った表示揃えの方法

    • alias設定の中でのprintf利用時のシングルクォーテーションの書き方



  • read


    • 1文字入力の受付と、入力文字の非表示化



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