Raspberry Pi の温度管理をソフトウェアで頑張る

この記事は日本Androidの会秋葉原支部ロボット部 Advent Calendar 2020 の12/07分の記事です.

内容は 日本Androidの会秋葉原支部ロボット部 第96回勉強会 で発表した内容を加筆修正したものです.

はじめに

Raspberry Pi という英国発の教育向けとして2012年に発売された安価なシングルボードコンピュータがあります.教育向けとして発売されましたが趣味にもよく使われています.OSは標準ではLinux(DebianベースのRaspberry Pi OS)が採用されています.

私はもっぱら省電力のLinuxマシンとして使うことが多いです.

トラブル

今夏空調のない部屋の自宅サーバの横でRaspberry Pi 3 model B + Raspberry Pi OS arm64(β)で計算をさせていたのですが,しばらく動かしていると固まるようになりました.

再起動すればしばらく動きますがしばらくするとやはり固まります.これをどうにか出来ないかと調べてみました.

ログの取得

まずはログを録ってみます.

crontabで1分毎に情報を記録
* * * * *       printf "`date +\%s`,`cat /sys/class/thermal/thermal_zone0/temp`,`echo "obase=2; ibase=16; \`vcgencmd get_throttled | cut -f2 -dx\`" | bc`,`vcgencmd measure_clock arm|cut -f2 -d=`\n" >> ~/.temp.log

内容はこんな感じです.(外気温度も録ればよかった)

UNIX Time
date +%s
SoC温度
/sys/class/thermal/thermal_zone0/temp
スロットリング周りのフラグ
vcgencmd get_throttled
arm周波数
vcgencmd measure_clock arm

ログがファイルに書かれる間にフリーズしてデータが失われるのを防ぐために /etc/fstab のマウントオプションに sync オプションも付けておきます.(再起動かremountで反映)

ログを取得している状態で負荷を掛けます.今回は /dev/urandom をcatすることで計算させました.今回のRaspberry Pi 3 model Bは4coreあるので4つ動かしています.

今回のテストで使った負荷(いつもはvanity address/vttとかとか)
$ cat /dev/urandom > /dev/null &
$ cat /dev/urandom > /dev/null &
$ cat /dev/urandom > /dev/null &
$ cat /dev/urandom > /dev/null &

熱が原因?

しばらく動かしてRaspberry Piが固まった後にログを確認してみます.
SoCの温度が85度を何度か記録した後に固まっているようです.
85度というのはRaspberry Pi OSでの標準のSoC制限温度のようです.この温度の5度前(標準では80度)からサーマルスロットリングが始まるようです.

サーマルスロットリングでクロックが下がって温度が下がれば問題無さそうだけど80度からクロックが下がっても85度を超えて固まってしまっているようです.
ベータ版のRaspberry Pi OS amd64を使っているせいかもしれないと思い,標準のRaspberry Pi OS armhf(32bit)版に変更して同様に試してみましたが同様の動きのようです.

正攻法としてはヒートシンク,ファンの増設や空調を入れるとよさそうですが,金欠なのでとりあえずソフトだけでどうにか出来ないかと試しました.

SoC制限温度を下げる

まずSoCの制限温度ですが,公式フォーラムで70度以下にしたほうがいいという書き込みを見かけました.逆に100度でも大丈夫という人も居るのですが安全側の70度にしてみます.

この設定は /boot/config.txt でパラメータを設定できます.以下は70度に設定たときの例です.この状態で再起動すると反映されます.

temp_limit=70

再起動後以下のコマンドで設定が反映されているか確認が出来ます.

$ vcgencmd get_config int | grep ^temp_limit=
temp_limit=70

この状態で負荷を掛けると70度を越えるくらいで固まりました.やはり制限温度を越えると固まってい舞うようです.

SoCの最大周波数を下げてみる

Raspberry Pi 3 model B のSoCは最大周波数1.2GHzです.これを下げてみます.

/boot/config.txtarm_freq= で設定できます.以下は800MHzに設定したときの例です.再起動で反映されます.

arm_freq=800

再起動後に設定が反映されているか確認します.

$ vcgencmd get_config int | grep ^arm_freq=
arm_freq=800

この状態で負荷を掛けるとやはり固まります.まあサーマルスロットリングが効いても固まるので仕方がない感じです.

SoCの最小周波数を下げてみる(これが効くのでは?)

次にSoC最小周波数を下げてみます.既定値は600MHzで,サーマルスロットリングでもここまで下がっているのでこれを更に下げると温度が下がりそうな気がします.

/boot/config.txtarm_freq_min= で設定できます.以下は400MHzに設定したときの例です.再起動で反映されます.

arm_freq_min=400

しかし再起動後に確認してみると600MHzより下には設定できないみたいで600MHzになってしまいます.

$ vcgencmd get_config int | grep ^arm_freq_min=
arm_freq_min=600

この状態で負荷を掛けるとやはり600Mhzまでしか下がらず固まります.

残念ながらRaspberry Pi のスロットリングでは無理そうです.

maxcpusでコアを制限してみる

Linuxのブートパラメータで maxcpus を指定することでコアを制限できます.Raspberry Pi の場合は /boot/cmdline.txt で設定できます.

設定後以下のコマンドで確認できます.

$ grep -o -E 'maxcpus=.{0,9} ' /proc/cmdline
maxcpus=1
$ grep ^processor /proc/cpuinfo | wc -l
1

これでCPU core1津で動作しています.しかし最大周波数を600MHzかつ1coreでも同様にフリーズしてしまいました.

cpufreqでクロック制御

IntelCPUのNotePCなどではcoufreqを使ってこのあたりの制御をするのですが,これでも600mHzより下には下げられないようで駄目でした.

LimitCPUで指定プロセスの制限を行う

LimitCPUは指定プロセスを監視し,CPU利用率や%で制限するプログラムです.Linux, MacOS, *BSDなどのUNIX-Likesystemで利用できます.
Raspberry Pi OSではcpulimitパッケージとしてパッケージングされており,コマンドもcoulimitです.

cpulimitの導入
$ sudo apt install cpulimit

cpulimitコマンドに制限したいプロセスIDやプロセス名と制限を指定することで動作します.

cat からはじまるプロセスを50%に制限
$ pgrep ^cat | xargs -n1 -I{} sh -c "cpulimit -p {} -l 50 -v &"

cpulimitで50%に制限してみたt頃温度が下がるのを確認できました.数日動かしても固まらなくなったようです.
定期的にSoCの温度を確認して制限を変更していくと良さそうでうs.

LimitCPUはCPUlimitの開発が止まった後のフォークですが,その後CPUlimitが新しく開発が始まっているようです.詳細は以下のページを参照してください.

cgroupでCPUリソース制限(未確認)

LimitCPUが効いたので恐らくcgroupでのCPUリソース制限でも大丈夫そうです.(未確認)

おわりに

現在は気温も下がり制限などしなくても問題ありません.でもきっと来夏にまた起こると思うのでそこでまた確認するつもりです.

しかし,今回の解決方法はCPUのリソースを制限して温度を下げて居るので計算量は減っています.空調を入れたりCPUファンを導入するのが正攻法になるのかなと思います.
CPUファンはサードパーティーから各種発売されているのでそれらを使うかDIYする感じになると思います.

そういえば最近Raspberry Pi OSの設定コマンドの raspi-config の中に Set behaviour of GPIO fan というメニューが出来たり,Raspberry Pi 4には公式のCPUファンが発売されているのでこれらを使うのが良さそうです.

特定プロセスのcpu利用率を制限するcpulimitを試す

先日mysqldump + xz 圧縮の間に pv を挟んで帯域制限をして xz の負荷を下げました.

しかしこの方法ではそんなに負荷がない mysqldump もずっと動かしっぱなしで db にもよろしく無いです.mysqldump は先に済ませて xz だけを制限することにします.

before
umask 0266 && nice -n 19 ionice -c 3 /usr/bin/mysqldump --defaults-file=/mnt/backup/micro/.my-backup.cnf --single-transaction --quick --all-databases --events | pv -L 128k 2>/dev/null | nice -n 19 ionice -c 3 /usr/bin/xz -9 > /mnt/backup/micro/`date +\%F_\%T_$$`.sql.xz
after
DUMP="/mnt/backup/micro/`date +\%F_\%T_$$`.sql"; umask 0266 && nice -n 19 ionice -c 3 /usr/bin/mysqldump --defaults-file=/mnt/backup/micro/.my-backup.cnf --single-transaction --quick --all-databases --events > ${DUMP} && pv -L 128k nice -n 19 ionice -c 3 /usr/bin/xz -9 > ${DUMP}.xz && rm ${DUMP}

一応動くけどcrontabなので1行で書いてあって見にくいしあまりいけてないですね.
せめてファイルに分けたほうが良さそう.

LimitCPU

pvでもいいのですが,cpu利用率を制限できないかなと思いました.xzのオプションでは見当たらずLinux環境なので cgroups で制限しようかとも思ったのですが, LimitCPU というものを見つけました.これは SIGSTOPSIGCONT のPOSIXシグナルをプロセスに送信することにより実現しているので,POSIX環境ならどこでも動きそうなのでこちらを試してみました.

LimitCPUはメンテされなくなったCPUlimitのフォークでコマンドやパッケージ名は cpulimit です.(混乱する><)

導入
$ sudo apt install cpulimit
usage
$ cpulimit -h
CPUlimit version 2.1
Usage: cpulimit TARGET [OPTIONS...] [-- PROGRAM]
   TARGET must be exactly one of these:
      -p, --pid=N        pid of the process
      -e, --exe=FILE     name of the executable program file
                         The -e option only works when
                         cpulimit is run with admin rights.
      -P, --path=PATH    absolute path name of the
                         executable program file
   OPTIONS
      -b  --background   run in background
      -c  --cpu=N        override the detection of CPUs on the machine.
      -l, --limit=N      percentage of cpu allowed from 1 up.
                         Usually 1 - 200, but can be higher
                         on multi-core CPUs (mandatory)
      -q, --quiet        run in quiet mode (only print errors).
      -k, --kill         kill processes going over their limit
                         instead of just throttling them.
      -r, --restore      Restore processes after they have
                         been killed. Works with the -k flag.
      -s, --signal=SIG   Send this signal to the watched process when cpulimit exits.
                         Signal should be specificed as a number or
                         SIGTERM, SIGCONT, SIGSTOP, etc. SIGCONT is the default.
      -v, --verbose      show control statistics
      -z, --lazy         exit if there is no suitable target process,
                         or if it dies
          --             This is the final CPUlimit option. All following
                         options are for another program we will launch.
      -h, --help         display this help and exit

制限はプロセスIDでの制限,プログラム名での制限,指定したプログラムを制限することが可能です.
cpuが複数ある場合は -l の値はcpu 1つあたり100として,100 * cpu数 を元に指定します.cpu 2つで 50% 利用したい場合は -l 100 になると思います.

プロセスID 1234 のプログラムをcpuを2つ利用,cpuを50%(2コアなので実際は100%分)
$ cpulimit -c 2 -p 1234 -l 50
xzというプログラムをcpu利用率を25%に制限
$ cpulimit -c 2 -l 50 - xz

※xzの前の - はなくてもいいが cpulimit のオプションの最後を示す.

cpuを1つだけ利用.cpu利用率を25%に制限しつつxzを実行できます.
$ cpulimit -c 1 -l 25 -z -  xz -9 datafile
  • -c : 利用するcpu数

  • -p : 制限するプロセスID

  • -l : 制限するcpu利用率

  • -z : 指定したプロセスが完了したらcpulimitも終了する(通常は関しを続けて同じ条件に一致するプロセスが現れたら制限する)

今回はxzを制限したいのでこんな感じに.
あまり変わりませんね…….

DUMP="/mnt/backup/micro/`date +\%F_\%T_$$`.sql"; umask 0266 && nice -n 19 ionice -c 3 /usr/bin/mysqldump --defaults-file=/mnt/backup/micro/.my-backup.cnf --single-transaction --quick --all-databases --events > ${DUMP} && nice -n 19 ionice -c 3 cpulimit -c1 -l 25 - /usr/bin/xz -9 ${DUMP}

バックアップ専用ユーザとして分けてあるのでそのユーザで cpulimit -c 1 -l 25 - xz とかしてxzという名前のプロセスを全部制限してしまうのもありかもしれません.monit とかで監視させればいい感じかも?そうすると,

DUMP="/mnt/backup/micro/`date +\%F_\%T_$$`.sql"; umask 0266 && nice -n 19 ionice -c 3 /usr/bin/mysqldump --defaults-file=/mnt/backup/micro/.my-backup.cnf --single-transaction --quick --all-databases --events > ${DUMP} && /usr/bin/xz -9 ${DUMP}

あまり変わりませんね…….

環境
$ dpkg-query -W cpulimit xz-utils
cpulimit        2.2-1
$ lsb_release -d
Description:    Ubuntu 16.04.5 LTS
$ uname -m
x86_64