高負荷なネットワークアプリケーションにおいて、他のコアが空いているにも関わらず、CPU0 の softirq(%soft) に負荷が集中することがよくある。Linuxでロードバランサやキャッシュサーバをマルチコアスケールさせるためのカーネルチューニングでその詳細について詳しく解説されているが、対応策として、RPS (Receive Packet Steering)とRFS (Receive Flow Steering)の設定を行うことで、負荷をCPUの各コアに分散させることができる。

この設定は次の三つのコマンドで実現できるが、再起動すると消えてしまう。ここでは再起動しても消えないためにどうすればいいかを書いていく。

echo "f" > /sys/class/net/eth0/queues/rx-0/rps_cpus
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries

まず上記の3つの設定で使われている値の説明から。
rps_cpusにfを設定しているのは、fが2進数表記で1111なので、4コアを使用するという意味。もし8コアあるのなら、echo "ff"とすればいいが、CentOS 6では実際のコア数より多い"fff"を設定しても自動で実際のコア数に応じた値になるようになる。そのため、コア数に関わらず適当に多めのecho "ffff"としておけばいいだろう。
rps_flow_cntとrps_sock_flow_entriesはRFSの設定値となる。rps_sock_flow_entriesは通常32768を設定するのが一般的なので固定値とする。rps_flow_cntはNIC キューごとのフロー数を設定でき、合計がrps_sock_flow_entriesを越えてはならない。NIC キューの個数は/sys/class/net/eth0/queues/rx-Nが何個あるかでわかるので、この関係性を式に表すと自動で計算ができる。

以上のロジックでinitスクリプトを簡潔に実装することで、CPUコア数やNICキュー数によらず適切に、起動時に必ずRPSとRFSの設定がされる。
rps_updateというinitスクリプトを以下のように作成する。startさえできればいいので、最低限の実装しかしていない。

#!/bin/sh
# chkconfig: 2345 90 90
# description: update rps and rfs configuration.
 
rps_sock_flow_entries=32768
 
case "$1" in
  start)
    echo 'update rps and rfs';;
  *)
    echo 'Usage: rps_update {start}'
    exit 1;;
esac
 
for e in $(ls -d /sys/class/net/eth*)
do
  rxs=$(ls -d ${e}/queues/rx-* | wc -l)
  rps_flow_cnt=$(($rps_sock_flow_entries / $rxs))
 
  for r in $(ls -d ${e}/queues/rx-*)
  do
    echo "ffff" > ${r}/rps_cpus
    echo $rps_flow_cnt > ${r}/rps_flow_cnt
  done
done
 
echo $rps_sock_flow_entries > /proc/sys/net/core/rps_sock_flow_entries

initスクリプトに適切な権限を与え、chkconfigに登録する。

chmod ugo+x /etc/init.d/rps_update
chkconfig --add rps_update
chkconfig rps_update --list
service rps_update start

あとは正しく設定されているか確認する。

cat /sys/class/net/eth*/queues/rx-*/rps_cpus | head -3
0000000f
0000000f
0000000f
cat /sys/class/net/eth*/queues/rx-*/rps_flow_cnt | head -3
8192
8192
8192
cat /proc/sys/net/core/rps_sock_flow_entries
32768

以上で完成だ。さて、ここではじめにあげたサイトの中で、RPSとRFSの設定の永続化方法を述べた別サイトへのリンクが張られている。今回そのサイトの方法を用いず、initスクリプトを作成した理由を述べる。
そのサイトでは/etc/udev/rules.d/70-persistent-net.rulesを用いて設定すると書かれていた。しかし、使用しているCentOS 6の環境では、/etc/udev/rules.d/70-persistent-net.rules/dev/nullへのシンボリックリンクとなっている。/dev/nullへシンボリックリンクを張ると、CentOS 6.0 で NIC と ethX の対応を固定化するで書かれている効果があるため、現状を変更してRPS,RFSの設定を書くことはできない。

CentOS 6.0 のデフォルトでは、MAC アドレスと ethX を対応付けており、交換により MAC が変化すると、自動的に新たな対応関係が作られるようになっているため、ethX がずれてしまいます。対応関係は、/etc/udev/rules.d/70-persistent-net.rules に設定されます。(中略)この 70-persistent-net.rules を無効化します。少々、荒っぽいやり方ですが、ルールが自動生成されないように、/dev/null へシンボリックリンクを張ってしまいます。