概要
Thread.new()
を使いマルチスレッド処理してたところ、Herokuにてリソース不足エラーが発生。
can't create Thread: Resource temporarily unavailable (ThreadError)
スレッドエラーのハンドリングと、ベストエフォートでスレッドを使うリトライ機構を作成した。
ちなみにHerokuで実行できるプロセス・スレッド数はかなり限られているので、ローカル環境との差異に注意。
2020/08現在
free, hobby and standard-1x dynos support no more than 256
standard-2x and private-s dynos support no more than 512
performance-m and private-m dynos support no more than 16384
performance-l and private-l dynos support no more than 32768
ローカル環境がMacOSであれば、sysctl kern.num_taskthreads
というコマンドで1プロセスあたりの最大スレッド数が調べられる。
$ sysctl kern.num_taskthreads
kern.num_taskthreads: 4096
注意
通常より少し負荷がかかったらスレッドが不足するような処理は、まず設計ミスしている可能性が高い。
別サーバーに処理を委譲できないか、APIの仕様的に少ないリクエスト数で済ませる方法はないかなど、立てなければいけないスレッドの数を減らす方法を先に考える。
コード
def retry_threads(times: 3)
try = 0
begin
try += 1
Thread.new { yield }
rescue ThreadError
sleep(1 * try)
retry if try < times
raise
end
end
使えるスレッドがない場合、数秒待ちリトライする。
1回目のリトライでは1秒、2回目のリトライでは2秒...と待ちの秒数を可変にし、タイムロスを防ぐ。
待ち時間をマイクロ秒にして、細かくリトライを管理するやり方もあり。
実際の使用方法
def heavy_task(url)
# 重い処理
end
# urls = ["...","...",...]
threads = []
urls.all each do |url|
threads << retry_threads{ heavy_task(url) }
end
threads.each(&:join)
ベンチマーク
このリトライ機構で、スレッドを立てる許容度が実際どれぐらい上がったのか調査した。
条件
- 10秒かかる処理がリトライ対象
- 1秒づつリトライ待ち時間を増やす
- リトライは3回まで
- ローカル環境で実行(MacOS Catalina)
- 最大スレッド数: 4096
- 処理できたタスク数をベンチマークの値にする
計測コード
def heavy_task
sleep(10)
end
def retry_threads(times: 3)
try = 0
begin
try += 1
Thread.new { yield }
rescue ThreadError
sleep(1 * try)
retry if try < times
p $count
raise
end
end
def no_retry_threads()
begin
Thread.new { yield }
rescue ThreadError
p $count
raise
end
end
$count = 0
# retryなし
loop do
no_retry_threads{ heavy_task }
$count += 1
end
# retryあり
loop do
retry_threads{ heavy_task }
$count += 1
end
結果
ローカル環境(最大スレッド数: 4096)で、10秒かかるタスクを処理した数の値。
リトライなし | リトライあり |
---|---|
4094 | 212888 |
参考
- class Thread
- Ruby で例外を捕捉して一定回数 retry するメソッド
- Maximum Number Of Threads (Linux/MacOS) + OutOfMemory unable to create new native thread
-
プロセスとスレッドの違いとは?
- 1プログラムに1プロセス、1プロセスにつき1以上のスレッドを持つことが可能