不安定な環境の中でのバッチ処理~JobQueueシステムQudoを使った事例~

4,055 views

Published on

YAPC::Asia2012の一日目で発表した資料です。報告ブログ記事に補足あるのでこれも参照して下さい。

http://hirobanex.net/article/2012/10/1349050265

YAPCの紹介ページ。http://yapcasia.org/2012/talk/show/2c531ede-c1ac-11e1-860d-28556aeab6a4

0 Comments
12 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
4,055
On SlideShare
0
From Embeds
0
Number of Embeds
655
Actions
Shares
0
Downloads
28
Comments
0
Likes
12
Embeds 0
No embeds

No notes for slide

不安定な環境の中でのバッチ処理~JobQueueシステムQudoを使った事例~

  1. 1. 不安定な環境の中でのバッチ処理~DBストア型JobQueueシステムQudoを使った事例~ <メニュー> イントロダクション編 要件編 実装編 デモ編 Hirobanex(Akabane Hiroyuki) 2012-09-28@YAPC::Asia2012
  2. 2. <イントロダクション編> ~あるある探検隊~ 1
  3. 3. バッチ処理とはバッチ処理とは、コンピュータで1つの流れのプログラム群を順次に実行 あらかじめ定めた処理を一度に行すること。うことを示すコンピュータ用語。反対語は対話処理またはリアルタイム処理。~(中略)~バッチジョブは一度設定されると人間の手を煩わせることなく動作する。そのため入力データもスクリプトやコマンド行パラメータを通して予め用意される。この点でユーザーの入力を必要とする対話型プログラムとは対極にある。 Wikipediaより 2
  4. 4. ITシステムにまつわるギャップ みんなの期待 システム エンジニアの小言 これで安心、ミスはありえない!! おいおい・・・ こっちは結構大変 スピーディーに なんだけどなぁ・・・なんでもできる!! 3
  5. 5. エンジニアが直面する様々な現実• facebookがエラーかえしくるんですけど・・・• ファイルロックしてよみとれなかった・・・• DBがロックされていて・・・• 外部サーバーがメンテ中で・・・• EC2APIのパースに失敗した・・・• 唐突にDNSが・・・• いつの間に(APIの)仕様が変わって・・・• 構築期間が短すぎて・・・ 4
  6. 6. 外部WebAPI(facebook/twitterなど) がERRORかえしてきた・・・ 5
  7. 7. FILE ROCKしてよみとれなかった・・・ 6
  8. 8. DBのROW/TABLE ROCKされていて UPDATEできなかった・・・ 7
  9. 9. 古い外部サーバーにバックアップしているんだけど、外部サーバーがメンテ 中で送信できてなかった・・・ 8
  10. 10. EC2でサーバー運用していてサーバー 足そうとしたら、EC2APIのパースに失 敗したとかって壊れてしまった・・・ 9
  11. 11. 唐突にDNSが名前解決できなく なっていた・・・ 10
  12. 12. いつの間に(APIの)仕様が変わって、 エラーをはきまくっていた・・・ 11
  13. 13. なんか作れって言われたんだけど、期間が短すぎてなんかバグがあったりよくわからないことが多い気がする・・・ 12
  14. 14. System is unstable and uncontrolable みんなの不満 システム エンジニアの不満 そんなことも予想できな 仕様が曖昧で・・・ かったのかー! ベンダーの・・・ 炎上「仕様がよくない」といえるケースも多いかもしれないが、 安定的にシステムを運用するは難しいのが現実 13
  15. 15. 例外が通用しないバッチ処理 Web処理 バッチ処理 ん?落ちてる? 終わらない?! トラフィックが増えたし、やっぱあの機能重かったね どういうことなんだ! バッチ処理にはバッチ処理なりの要件が存在する 14
  16. 16. <要件編>~バッチ要件とJob Queue~ 15
  17. 17. 【要件編】アジェンダ• hirobanex的バッチ処理要件• Job Queueシステムの概要• 本要件別のJob Queueの機能比較• TheSchwartz VS Qudo 16
  18. 18. hirobanex的バッチ処理要件• 再実行可能な単位で処理が区切れている• 途中でdieして止まっていてほしくない• どこまで終わったかログがとれている• 例外が発生したら、実行ケース別にログれる• 複数回リトライできる• リトライする場合ある程度間隔をあける• 最終的に失敗しても手動で簡単に再実行できる 17
  19. 19. 素人エンジニアのバッチ処理なんか、「これ毎月2日に一括処理してまとめておいて~」って言われたから、ごにょごにょ一枚のスクリプトに書いて 実行しったった!!! 18
  20. 20. 【要件】明確な処理単位なんか変なところでとまってしまったんだけど、 よくわからんから最初からやり直し。 いつになったら帰れるかわからず疲弊中・・・ 一つの処理をメソッドにまとめて おけばよかったなぁ・・・ 19
  21. 21. 【要件】スキップ機能 あれ、終わった~? とまってました・・・ あっそ・・・dieしたやつスキップして先に進めるようにして おけばよかったなぁ・・・ 20
  22. 22. 【要件】ケース別エラーログ で、なんで止まったの?? わかりません・・・ どうなっているんだ・・・ちゃんと、エラーログをケース別に吐いて おけばよかったなぁ・・・ 21
  23. 23. 【要件】進捗把握で、とりあえず、どこまで終わった??たぶん、半分くらい・・・ ・・・どこまで終わったかログって おけばよかったなぁ・・・ 22
  24. 24. 【要件】Retry設定 なんかわからんけど、同じケースでも何回かやると うまくいくんだけどなぁ・・・ 何回かリトライする設定にして おけばよかったなぁ・・・ 23
  25. 25. 【要件】Retry間隔設定 短期間に 何度もリトライしたら DB落ちちゃった・・・リトライの間隔をいい塩梅に設定して おけばよかったなぁ・・・ 24
  26. 26. 【要件】再実行な失敗保存 さすがに、もう終わったよね??? あ、一部がちょっと・・・ いいかげんしてよっ!!最終的に失敗しても楽に再実行できるようにして おけばよかったなぁ・・・ 25
  27. 27. hirobanex的バッチ処理要件まとめ 要件 概要 明確な 再実行可能な単位で処理が区切れている処理単位スキップ 途中でdieしてとまらないようになっている 機能進捗把握 どこまで終わったのかわかるケース別 例外が発生した場合、実行ケース別にログがとれるエラーログRetry設定 複数回リトライできる Retry リトライする場合ある程度間隔をあける間隔設定再実行な 最終的に失敗した場合でも、手軽に簡単に再実行できる失敗保存 26
  28. 28. Job Queueシステムの概要① Client Client ClientProcess Process Process Job ServerWorker Worker WorkerProcess Process Process よくあるチャート 27
  29. 29. Job Queueシステムの概要② Client Client Client Process 2 処理B Process 4 Process 付属情報 処理A結果 処理方法Aの登録 処理A 処理方法Bの登録 1$worker->register_function( 付属情報 処理B結果 $worker->register_function( {処理A} => sub { my $job = @_; Job Server {処理B} => sub { my $job = @_; ================= ================= warn "hirobanex"; 処理B warn "nekokak"; ================= 付属情報 処理A結果 ================= return xxx; 処理A return xxx;}); 付属情報 処理B結果 }); 3 Worker Worker Worker Process Process Process Workerに予め登録されている処理を Clientが指定し、Workerが非同期で処理 28
  30. 30. 本要件別Job Queue機能比較① 要件 Gearman Q4M TheSchwartz Qudo 明確な処理単位 ○ ○ ○ ○スキップ 機能 ○ ○ ○ ○進捗把握 × ○ ○ ○ケース別エラーログ × × ○ ○Retry設定 ○ × ○ ○ Retry間隔設定 ○ × ○ ○再実行な失敗保存 × × × ○ 29
  31. 31. 本要件別Job Queue機能比較② 要件 Gearman Q4M TheSchwartz Qudo 明確な処理単位 ○ ○ ○ ○スキップ Job Queueを使えば満たされる 機能 ○ ○ ○ ○ ジョブサーバーをジョブが消失しないDBを進捗把握 × ○ ○ 使えば満たされる ○ケース別エラーログ × × ○ ○ Q4Mは独自に実装 する必要があるが他Retry設定 ○ × ○ ○ は実装されているの Retry間隔設定 ○ × ○ ○ で満たされる再実行な失敗保存 × × × ○ 30
  32. 32. TheSchwartz VS Qudo① 要件 TheSchwartz Qudo 多様なシリアライザーを使いたい × ○ジョブが永遠とループするのを防ぐ × ○最終的に失敗しても楽に再実行できる × ○ 31
  33. 33. TheSchwartz VS Qudo② 要件 TheSchwartz Qudo 多様なシリアライザーを使いたい × ○ TheSchwartzでも継承とか Class::Triggerとか ジョブが永遠とループするのを防ぐ × ○ Class::MethodModifierと かがんばればできるけど、 Qudoは拡張性が高い最終的に失敗しても楽に再実行できる × ○TheSchwartzをすでに使っているところをQudoにリプ レイスするほどではないが、新規ならQudoがベスト 32
  34. 34. <実装編>~Qudoを使った実装例~ 33
  35. 35. 【実装編】アジェンダ• インストールとか• Qudoのインスタンスの生成• 処理の定義• 処理の登録 ~ひとつ場合~• 処理の登録 ~複数の場合~• 処理をする ~通常の場合~• 処理をする ~実際の場合~• 無限ループの中のエラーハンドリング• max_retries = 1でerror時の再登録• max_retries > 1でerror時の再登録• 動作確認テストをする 34
  36. 36. インストールとか モジュールcpanm Qudo ジョブサーバーqudo --db=my_app_qudo --user=root --pass=pass --rdbms=mysql --use_innodb 35
  37. 37. Qudoのインスタンス生成use Qudo;my $qudo = Qudo->new( #WorkerもClientもこのインスタンスを使用 datasources => +[ +{ dsn => dbi:mysql:my_app_qudo;, username => root, password => pass, }, Hookに好きな処理を追加できるのが ], default_hooks => [qw/ TheSchwartzに対する優位性 Qudo::Hook::Serialize::JSON #引数情報をJSONにシリアライズ Qudo::Hook::ForceQuitJob #予め決めた時間を超えたらdie(ギッハブ) MyApp::Hook::NotifyReachMaxRetry #オレオレ例。再実行な失敗保存(後述) /], manager_abilities => [qw/ #処理可能な処理名(後から追加も可能) MyApp::Worker::Simple MyApp::Worker::OnceEveryTreeDie /],); 明確な スキップ ケース別 Retry間隔 再実行な 多様なシリ ワーカーの 進捗把握 Retry設定処理単位 機能 エラーログ 設定 失敗保存 アライザー 専有回避 36
  38. 38. 処理の定義package MyApp::Worker::OnceEveryTreeDie; #クラス名が処理名になるuse strict;use warnings;use base Qudo::Worker;sub set_job_status { 1 } #ジョブの実行結果の記録オプションsub max_retries { 5 } #リトライする回数sub retry_delay { 5 } #リトライするときにあける間隔の秒数sub grab_for { 60*5 } #ジョブを他のワーカーからブロックしておく秒数sub work { #処理内容の定義 my ($class, $job) = @_; # -ここはホントは別クラスにしたほうがテストしやすい-------- if (int(rand(3)) == 0) { Qudo::Hook::ForceQuitJobを使っておくと、 die "error!!"; grab_forの時間で過ぎたら一旦dieしてくれる }else{ ので、Workerプロセスが変な爆弾踏んでも処 print "success!!¥n"; 理から開放されるからひとまず安心 } # --------------------------------------------------------- $job->completed;} 明確な スキップ ケース別 Retry間隔 再実行な 多様なシリ ワーカーの 進捗把握 Retry設定処理単位 機能 エラーログ 設定 失敗保存 アライザー 専有回避 37
  39. 39. 処理の登録 ~ひとつ場合~#!/usr/bin/env perluse strict;use warnings;use Qudo;my $qudo = Qudo->new(...);$qudo->enqueue( MyApp::Worker::OnceEveryTreeDie, #第一引数で処理名を指定 +{ #第二引数で付属情報を指定 arg => +{ #シリアライザーをHookで入れていばRefも渡せる OnceEveryTreeDie => 1, moge => 2, }, run_after => Int, uniqkey => Int, priority => Int, },}); 明確な スキップ ケース別 Retry間隔 再実行な 多様なシリ ワーカーの 進捗把握 Retry設定処理単位 機能 エラーログ 設定 失敗保存 アライザー 専有回避 38
  40. 40. 処理の登録 ~複数の場合~my @jobs = ( ["Func1",{arg => { hoge => 1, moge => 2}, priority => 1 }], ["Func1",{arg => { hoge => 2, moge => 3}, priority => 1 }], ["Func2",{arg => { foo => 5, bar => 5}, priority => 5 }], ["Func2",{arg => { foo => 9, bar => 9}, priority => 5 }],);bulk_enqueue(¥@jobs);sub bulk_enqueue { my $jobs = shift; my $dsn = $qudo->shuffled_databases; my $db = $qudo->manager->driver_for($dsn); my $txn = $db->txn_scope; for my $job (@$jobs) { $qudo->manager->enqueue(@$job, $dsn); } $txn->commit;} 明確な スキップ ケース別 Retry間隔 再実行な 多様なシリ ワーカーの 進捗把握 Retry設定処理単位 機能 エラーログ 設定 失敗保存 アライザー 専有回避 39
  41. 41. 処理をする ~通常の場合~# worker.pl perl worker.pl & 的な感じで無限ループプロセスをおいておく#!/usr/bin/env perluse strict;use warnings;use MyApp::Worker::OnceEveryTreeDie;my $qudo = Qudo->new(...);#処理できる処理名を登録$qudo->manager->register_abilities("MyApp::Worker::OnceEveryTreeDie");$qudo->work();-----<Qudoのworkメソッド抜粋>--------------------------sub work { my ($self, $work_delay) = @_;(中略) while (1) { #無限ループ sleep $work_delay unless $manager->work_once; }} 明確な スキップ ケース別 Retry間隔 再実行な 多様なシリ ワーカーの 進捗把握 Retry設定処理単位 機能 エラーログ 設定 失敗保存 アライザー 専有回避 40
  42. 42. 処理をする ~実際の場合~# worker.pl perl worker.pl & 的な感じで無限ループプロセスをおいておく#!/usr/bin/env perluse strict;use warnings;use Qudo::Parallel::Manager;my $worker = Qudo::Parallel::Manager->new( databases => [+{...},...], #Qudoインスタンスの生成と同じ default_hooks => [qw/Qudo::Hook::Serialize::JSON/], manager_abilities => [qw/MyApp::Worker::OnceEveryTreeDie/], work_delay => 1, max_workers => 5, min_spare_workers => 5, max_spare_workers => 5, max_request_par_chiled => 5, auto_load_worker => 1, ); •Forkで高速化}; •メモリリーク対策$worker->run; •ジョブの処理中にWorkerをKillしても処理後にとまる対策 明確な スキップ ケース別 Retry間隔 再実行な 多様なシリ ワーカーの 進捗把握 Retry設定処理単位 機能 エラーログ 設定 失敗保存 アライザー 専有回避 41
  43. 43. 無限ループの中のエラーハンドリングmy $res; Qudo::Workerのwork_safelyメソッドを抜粋eval #evalトラップ $res = $class->work($job);};if ( (my $e = $@) || ! $job->is_completed ) { if ( $job->retry_cnt < $class->max_retries ) { $job->reenqueue( { grabbed_until => 0, retry_cnt => $job->retry_cnt + 1, retry_delay => $class->retry_delay, } ); } else { $job->dequeue; } $job->failed("$e" || Job did not ...); #Qudoのerrorテーブルにエラーを格納} else { $job->dequeue;} 明確な スキップ ケース別 Retry間隔 再実行な 多様なシリ ワーカーの 進捗把握 Retry設定処理単位 機能 エラーログ 設定 失敗保存 アライザー 専有回避 42
  44. 44. max_retries = 1でerror時の再登録use Qudo;my $qudo = Qudo->new(...);my $exceptions = $qudo->exception_list;my ($db, $exception) = each %$exceptions;while ( my ($db, $exception) = each %$exceptions ) { $qudo->manager->enqueue_from_failed_job( $exception, $db );} 複数回リトライしていると、リトライしたすべて exeption_logテーブルに残ってしまうので、これだと同じ ジョブを何個もreenqueueしてしまう ↓ 次のページ参照 明確な スキップ ケース別 Retry間隔 再実行な 多様なシリ ワーカーの 進捗把握 Retry設定処理単位 機能 エラーログ 設定 失敗保存 アライザー 専有回避 43
  45. 45. max_retries > 1でerror時の再登録① ジョブの結果を一旦移せるようなテーブルを用意CREATE TABLE worker_error_log( id int(10) unsigned NOT NULL auto_increment, funcname varchar(255) binary NOT NULL, arg mediumblob, uniqkey varchar(255) DEFAULT NULL, priority int(10) unsigned DEFAULT NULL, retried_fg tinyint(1) unsigned NOT NULL default 0, updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP on updateCURRENT_TIMESTAMP, PRIMARY KEY (id), KEY funcname (funcname), KEY retried_fg (retried_fg)) ENGINE=InnoDB DEFAULT CHARSET=utf8; 明確な スキップ ケース別 Retry間隔 再実行な 多様なシリ ワーカーの 進捗把握 Retry設定処理単位 機能 エラーログ 設定 失敗保存 アライザー 専有回避 44
  46. 46. max_retries > 1でerror時の再登録②package MyApp::Worker::Hook::NotifyReachMaxRetry; 独自のHookを用意use base Qudo::Hook;sub hook_point { post_work }sub load { my ($class, $klass) = @_; $klass->hooks->{post_work}->{notify_reach_max_retry} = sub { my $job = shift; #max_retriesを超えてなおかつエラーだったらさっき用意したテーブルに入れる if ($job->is_failed && ( $job->funcname->max_retries <= ($job->retry_cnt) )) { $db->insert(worker_error_log,{ funcname => $job->funcname, arg => $job->arg, uniqkey => $job->uniqkey, priority => $job->priority + 100, #失敗している時点で優先順位は高いはず }); #アラートメールとかする } };}sub unload { delete $_[1]->hooks->{post_work}->{notify_reach_max_retry} }1; 明確な スキップ ケース別 Retry間隔 再実行な 多様なシリ ワーカーの 進捗把握 Retry設定処理単位 機能 エラーログ 設定 失敗保存 アライザー 専有回避 45
  47. 47. max_retries > 1でerror時の再登録③ 再登録するmy @worker_error_log = $db->search(worker_error_log,{retried_fg => 0})->all;my @jobs = map { my $row = $_; [$row->funcname,{ arg => $row->arg, uniqkey => $row->uniqkey, priority => $row->priority, }];} @worker_error_log;my @update_ids = map {$_->id} @worker_error_log;my $txn = $db->txn_scope; $db->update(worker_error_log, { retried_fg => 1 }, { id => { in => ¥@update_ids } } ); bulk_enqueue(¥@jobs);$txn->commit; 明確な スキップ ケース別 Retry間隔 再実行な 多様なシリ ワーカーの 進捗把握 Retry設定処理単位 機能 エラーログ 設定 失敗保存 アライザー 専有回避 46
  48. 48. 動作確認テストをするuse Test::More;use Qudo;#qudoのDBをテストでたちあげる(Test::mysqldでもなんでも)my $qudo = Qudo->new(...);#subtest enqueue => sub { $qudo->enqueue(MyApp::Worker::OnceEveryTreeDie,+{}); my (undef,$job_count) = %{container(qudo)->job_count()}; is $job_count,1;};subtest work => sub { $qudo->manager->register_abilities("MyApp::Worker::OnceEveryTreeDie"); $qudo->manager->work_once; my (undef,$job_count) = %{container(qudo)->job_count()}; is $job_count,0; #実際のMyApp::Worker::OnceEveryTreeDieの中身もテスト?};#qudoのテストだちあげたDBをけすdone_testing; 明確な スキップ ケース別 Retry間隔 再実行な 多様なシリ ワーカーの 進捗把握 Retry設定処理単位 機能 エラーログ 設定 失敗保存 アライザー 専有回避 47
  49. 49. <デモ編>~実感!!~ 48
  50. 50. デモ一覧<メニュー>• シンプルな処理の例• 不安定な処理の例• 複数retry後の失敗Job蓄積の例<demo sample code>• https://github.com/hirobanex/QudoSample 49
  51. 51. シンプルな処理の例- Simpleの処理を見てもらう(lib/MyApp/Worker/Simple.pm)- enqueue - enqueue.plで登録する内容をみてもらう(vi script/simple/enqueue.pl) - enqueue(perl script/simple/enqueue.pl)- ジョブがたまったのをみてもらう(select * from job;) - JSONになっているよ- 処理する(perl ./script/simple/worker.pl)- ジョブがきえたのをみてもらう - select * from job ¥G - select * from job_status ¥G 50
  52. 52. 不安定な処理の例- OnceEveryTreeDieの処理を見てもらう(lib/MyApp/Worker/OnceEveryTreeDie.pm)- enqueue - enqueue.plで登録する内容をみてもらう,複数個いれる(vi script/once_over_tree_die/enqueue.pl) - enqueue(perl script/once_over_tree_die/enqueue.pl)- ジョブがたまったのをみてもらう - truncate job_status; - truncate exception_log; - select * from func ; - select * from job;- 処理する(perl ./script/once_over_tree_die/worker.pl) - (リトライカウントが増えている)select * from job; - (リトライカウントが増えている)select * from job; - (リトライカウントが増えている)select * from job; - 失敗の記録、10回生功した記録が残っている(select * from job_status; - エラーが2回分入っている(select * from exception_log ¥G 51
  53. 53. 複数retry後の失敗Job蓄積の例- Dieの処理を見てもらう(lib/MyApp/Worker/Die.pm)- enqueue - enqueue.plで登録する内容をみてもらう(vi script/max_retry/enqueue.pl) - enqueue(perl script/max_retry/enqueue.pl)- ジョブがたまったのをみてもらう - select * from func ; - select * from job ¥G- 処理する(perl ./script/max_retry/worker.pl) - 何も表示されません - (リトライカウントが増えている)select * from job ¥G- ジョブがきえたのをみてもらう - select * from job ¥G - リトライした回数分入っている(select * from job_status;) - エラー6回分入っている(select * from exception_log ¥G)- 独自実装に入っているか - Hookを確認(lib/MyApp/Worker/Hook/NotifyReachMaxRetry.pm) - テーブルを確認(select * from worker_error_log;) - reenqueue.plを実行(perl ./script/max_retry/reenqueue.pl) - テーブルを確認,retried_fgがたっている(select * from worker_error_log;)- ジョブに入っているか確認(select * from job;) 52
  54. 54. Thanks nekokak(Qudo Auther)!! Thanks Hachioji.pm!! Thanks Perl Mongers!! 53
  55. 55. LTソンとても盛り上がっているので是非一回見に行って下さいっ!! 54

×