処理速度を向上させようとして失敗した話
気付けばもう2018年です。
去年は結局ブログのエントリを1本しか出しませんでした。
ブログのエントリは、twitterよりはまとまった話がしやすいですし大概追いやすいので、少し去年の四方山話をまとめておこうと思います。
閑話休題。
去年の12月、実装していたプログラムの納期が迫っていた頃の失敗談です。
そのプログラムは、複数のラジコンを制御するようなプログラムで、
MQTT*1を受信した際の処理とリアルタイム処理の二本柱で制御していました。
このリアルタイム処理が曲者で、
「0.2秒毎にDBのテーブルXの全レコードを最新の状態に更新し処理Aを行う」スレッドAと
「0.5秒毎にDBのテーブルXの全レコードを最新の状態に更新し処理Bを行う」スレッドBが併存していました*2。
この「全レコードを最新の状態に更新」は処理A、処理Bとも”直前”*3に行う必要があるため、まとめることができなかったわけです。
レコード件数は高々10件程度ですが、
プログラムの性質上、当該更新処理にかかる数十ミリ秒は馬鹿にできない時間でした。
かと言って、スレッドでの処理間隔を不用意に開けることはできません。
なぜなら処理間隔が開きすぎるとラジコンの制御が間に合わず、ラジコン同士が衝突したりコースアウトしたりするリスクが増大するためです。
なんとかできないか……。
そこで着目したのは「MQTT受信処理」の方でした。
下記のように処理を修正すれば全レコードを更新するタイミングが減ると考え、実際にプログラムを修正して様子を見ました。
- 特定のMQTTを受信した際にスレッドA,Bで行っている処理を行うべきタイミングかチェックする
- 処理を行うべきタイミングでなければ、行うべきタイミングになるまで待機し、再度チェックを掛ける*4
結論から言うと、これは大失敗でした。
修正後のプログラムを走らせると、10分くらい経過した時点でDBエラーが発生するようになりました。
ログを検証すると「全レコードを更新している途中のスレッドが殺された」ために起きていた現象のようでした。
プログラムを修正した際に一部のスレッドは「そのスレッドが請負う処理を行う必要がなくなった場合*5、外部から当該スレッドを殺す」ようにしていました。
「全レコードを更新している途中でスレッドを殺したらそりゃダメよなぁ」ということで、「レコード更新中はフラグを立てて、外部からスレッドを殺せないようにする」よう修正を掛けましたが、これは何故か失敗。
「面倒だし、スレッドを殺すこと自体を止めよう」と再度修正を掛けましたが、今度はスレッドが無尽蔵に作成され、処理がパンクする事態に。
ここに至ってこの修正部分にこれ以上時間を費やすことは不毛であると判断し、結局、以前通りのリアルタイム処理に巻き戻しました。
この件の教訓としては、概ね以下のような感じです。
- スレッドを外部から殺すような処理を設ける場合、DBなどの外部リソースを変更している最中にスレッドを殺さないようにする
- そもそも一度立てたスレッドはできるだけ外部から殺さない。スレッド内の適切なタイミングで終了チェック処理などを設ける
- (上記に関連して)スレッドなどの非同期処理は基本Fire&Forget(撃ちっぱなし)の精神を厳守する
- 常駐スレッドが無尽蔵に立つような設計は厳に慎む(例えあるスレッドの生存期間が短くても、そのスレッドが後続のスレッドを延々と立て続けるなら、それは常駐と変わらない)
- エンバグなどで困った時のために、修正部分をいつでも切り捨てられるようにする(修正用ブランチを切ってそこで処理するなど)
- 処理速度が致命的に遅いわけでないなら、わざわざ処理速度を上げるための修正をしない。プログラムの修正には常にエンバグのリスクが伴う