Pythonでスピードを上げるためにjoblibを使うけど、並列度を設定するn_jobsを盲目的に自動の-1に設定してることが多い気がするけど、これはケースによっては全くおいしくないという話。
演算ベースでの高速化であれば確かにハードウェア実行可能スレッド数に合わせてくれる-1は正しいのだけど、外部API呼び出しやDB処理なんかの外部要因遅延を隠蔽するためのスレッド処理の場合には全くおいしくない。
Google CloudのAPIに一括処理を投げる必要があって、更新対象が最大ケースで1000以上あったのでシリアルにやってると数分かかってやってられないのでjoblibで並列化、しかし-1ではそれなりに速くなるけどやっぱり1分近くかかって正直やってられない待ち時間。
外部遅延ならスレッドは処理せず待機してる時間が長いわけなので、待機時間にリクエストを投げてしまえるはずなのでn_jobsを増やしてやれば高速化するはず。
というわけで、このn_jobs適正値を探ってみた。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
for N in range(1,17): start_at = datetime.datetime.now() cores = multiprocessing.cpu_count() ret = Parallel(n_jobs=cores*N, verbose=0, require="sharedmem")( [ delayed(set_role)( role=role, principal=principal, ) for principal in principals ] ) print("N:{}, Elapsed time: {}".format(N, datetime.datetime.now() - start_at)) |
set_roleは権限系APIを叩いてプリンシパルに一括でロールを当てる処理をしてる(非インフラの人が許可された権限範囲で事前定義ロールをグループに当てるためのWebベースの管理システムなのでterraformとか使わずプログラムで書く必要があった。 Google自体のグループアカウントにするとグループの誰がやったかわからなくなるので、ユーザアカウントに対して当てる要件もある都合、対象者分ぐるぐる回す事に。 ケースによってはカスタムロールをいじって、ロール側の権限を付与するやり方もあるんだけど、管理的に専用ロールが適切なケースで利用する)
multiprocessingライブラリのcpu_countでスレッド数が取れるので、これに適当な値を乗算して程よい値はいくつなのか探る。
開発環境は普段使いのM3 Airなのでcores=8状態で、-1相当のN=1 → n_jobs=8スレッドから初めて8*16=128スレッドまでの所要時間を計測。
結果は・・・
N:1, Elapsed time: 0:00:47.784505
N:2, Elapsed time: 0:00:24.832650
N:3, Elapsed time: 0:00:17.227296
N:4, Elapsed time: 0:00:13.570150
N:5, Elapsed time: 0:00:11.049064
N:6, Elapsed time: 0:00:09.215467
N:7, Elapsed time: 0:00:07.924053
N:8, Elapsed time: 0:00:07.062545
N:9, Elapsed time: 0:00:06.400178
N:10, Elapsed time: 0:00:06.014051
N:11, Elapsed time: 0:00:05.332783
N:12, Elapsed time: 0:00:05.117406
N:13, Elapsed time: 0:00:05.256838
N:14, Elapsed time: 0:00:04.916923
N:15, Elapsed time: 0:00:05.087625
N:16, Elapsed time: 0:00:05.570203
こんな感じになった。
うん、-1設定は全然おいしくなかったね。
2倍にすると普通に半減、4倍で更に半減、8倍でも更に半減、14が最速でそこから遅くなってる。
11〜13あたりも結構変動しているので、この辺でCPU処理と遅延時間のバランスが釣り合ってる感じだね。 実行環境依存なので、実際は本番環境計測した方が正確になる、今回はこれを本番のCloud Runに突っ込んで計測して最速値N=12だったのでそれを設定した、CPUがMacより遅いかGC環境内でAPI応答が速い都合かで少し並列度低めの方が速い結果になったのだと思う。
n_jobs=-1とn_jobs=cores*14で約10倍の実時間差が出ているので、盲目的に-1だとほんとに活かせてない状態。 外部遅延を隠蔽したい場合はケースに適切な並列度を設定してあげた方が良いねっていうお話。 ちなみに、並列にしてないと6分位かかってLBタイムアウト起きる。
なお、この状態だとAPI叩くペース速すぎてレートリミットが発生する恐れがあるので、メトリクスを読んで実行前に余裕があるか確認して待機したり、あえて一定処理ごとに遅延を突っ込んだりする実装をした(遅い問題に対応した結果、速すぎ問題に対応するという・・・) PubSubとかで投げて裏側で当てるの待っても良いけど下手にサービス組み合わせるのも複雑になるし、GCサポに依頼して上限開放してもらう手もあるんだけど、頻繁に権限いじるわけでもないし最大ケース想定なので多少遅延突っ込むくらいなら問題ないかなと。
(2)