Weitere ähnliche Inhalte
Ähnlich wie 不安定な環境の中でのバッチ処理~JobQueueシステムQudoを使った事例~ (20)
不安定な環境の中でのバッチ処理~JobQueueシステムQudoを使った事例~
- 3. バッチ処理とは
バッチ処理とは、コンピュータで1つの流れのプログラム群を順次に実行
あらかじめ定めた処理を一度に行
すること。
うことを示すコンピュータ用語。反対語は対話処理またはリアルタ
イム処理。
~(中略)~
バッチジョブは一度設定されると人間の手を
煩わせることなく動作する。そのため入力データもスクリプ
トやコマンド行パラメータを通して予め用意される。この点でユー
ザーの入力を必要とする対話型プログラムと
は対極にある。
Wikipediaより
2
- 5. エンジニアが直面する様々な現実
• facebookがエラーかえしくるんですけど・・・
• ファイルロックしてよみとれなかった・・・
• DBがロックされていて・・・
• 外部サーバーがメンテ中で・・・
• EC2APIのパースに失敗した・・・
• 唐突にDNSが・・・
• いつの間に(APIの)仕様が変わって・・・
• 構築期間が短すぎて・・・
4
- 14. System is unstable and uncontrolable
みんなの不満 システム エンジニアの不満
そんなことも予想できな 仕様が曖昧で・・・
かったのかー! ベンダーの・・・
炎上
「仕様がよくない」といえるケースも多いかもしれないが、
安定的にシステムを運用するは難しいのが現実 13
- 15. 例外が通用しないバッチ処理
Web処理 バッチ処理
ん?落ちてる? 終わらない?!
トラフィックが増えたし、
やっぱあの機能重かったね どういうことなんだ!
バッチ処理にはバッチ処理なりの要件が存在する
14
- 18. hirobanex的バッチ処理要件
• 再実行可能な単位で処理が区切れている
• 途中でdieして止まっていてほしくない
• どこまで終わったかログがとれている
• 例外が発生したら、実行ケース別にログれる
• 複数回リトライできる
• リトライする場合ある程度間隔をあける
• 最終的に失敗しても手動で簡単に再実行できる
17
- 21. 【要件】スキップ機能
あれ、終わった~?
とまってました・・・
あっそ・・・
dieしたやつスキップして先に進めるようにして
おけばよかったなぁ・・・
20
- 22. 【要件】ケース別エラーログ
で、なんで止まったの??
わかりません・・・
どうなっているんだ・・・
ちゃんと、エラーログをケース別に吐いて
おけばよかったなぁ・・・
21
- 25. 【要件】Retry間隔設定
短期間に
何度もリトライしたら
DB落ちちゃった・・・
リトライの間隔をいい塩梅に設定して
おけばよかったなぁ・・・
24
- 26. 【要件】再実行な失敗保存
さすがに、もう終わったよね???
あ、一部がちょっと・・・
いいかげんしてよっ!!
最終的に失敗しても楽に再実行できるようにして
おけばよかったなぁ・・・
25
- 27. hirobanex的バッチ処理要件まとめ
要件 概要
明確な
再実行可能な単位で処理が区切れている
処理単位
スキップ
途中でdieしてとまらないようになっている
機能
進捗把握 どこまで終わったのかわかる
ケース別
例外が発生した場合、実行ケース別にログがとれる
エラーログ
Retry設定 複数回リトライできる
Retry
リトライする場合ある程度間隔をあける
間隔設定
再実行な
最終的に失敗した場合でも、手軽に簡単に再実行できる
失敗保存
26
- 28. Job Queueシステムの概要①
Client Client Client
Process Process Process
Job Server
Worker Worker Worker
Process Process Process
よくあるチャート
27
- 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. 本要件別Job Queue機能比較①
要件 Gearman Q4M TheSchwartz Qudo
明確な
処理単位 ○ ○ ○ ○
スキップ
機能 ○ ○ ○ ○
進捗把握 × ○ ○ ○
ケース別
エラーログ × × ○ ○
Retry設定 ○ × ○ ○
Retry
間隔設定 ○ × ○ ○
再実行な
失敗保存 × × × ○
29
- 31. 本要件別Job Queue機能比較②
要件 Gearman Q4M TheSchwartz Qudo
明確な
処理単位 ○ ○ ○ ○
スキップ
Job Queueを使えば満たされる
機能 ○ ○ ○ ○
ジョブサーバーをジョブが消失しないDBを
進捗把握 × ○ ○
使えば満たされる ○
ケース別
エラーログ × × ○ ○
Q4Mは独自に実装
する必要があるが他
Retry設定 ○ × ○ ○
は実装されているの
Retry
間隔設定 ○ × ○ ○
で満たされる
再実行な
失敗保存 × × × ○
30
- 32. TheSchwartz VS Qudo①
要件 TheSchwartz Qudo
多様なシリアライザーを使いたい × ○
ジョブが永遠とループするのを防ぐ × ○
最終的に失敗しても楽に再実行できる × ○
31
- 33. TheSchwartz VS Qudo②
要件 TheSchwartz Qudo
多様なシリアライザーを使いたい × ○
TheSchwartzでも継承とか
Class::Triggerとか
ジョブが永遠とループするのを防ぐ × ○
Class::MethodModifierと
かがんばればできるけど、
Qudoは拡張性が高い
最終的に失敗しても楽に再実行できる × ○
TheSchwartzをすでに使っているところをQudoにリプ
レイスするほどではないが、新規ならQudoがベスト 32
- 35. 【実装編】アジェンダ
• インストールとか
• Qudoのインスタンスの生成
• 処理の定義
• 処理の登録 ~ひとつ場合~
• 処理の登録 ~複数の場合~
• 処理をする ~通常の場合~
• 処理をする ~実際の場合~
• 無限ループの中のエラーハンドリング
• max_retries = 1でerror時の再登録
• max_retries > 1でerror時の再登録
• 動作確認テストをする
34
- 36. インストールとか
モジュール
cpanm Qudo
ジョブサーバー
qudo
--db=my_app_qudo
--user=root
--pass=pass
--rdbms=mysql
--use_innodb
35
- 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. 処理の定義
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. 処理の登録 ~ひとつ場合~
#!/usr/bin/env perl
use 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. 処理の登録 ~複数の場合~
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. 処理をする ~通常の場合~
# worker.pl perl worker.pl & 的な感じで無限ループプロセスをおいておく
#!/usr/bin/env perl
use 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. 処理をする ~実際の場合~
# worker.pl perl worker.pl & 的な感じで無限ループプロセスをおいておく
#!/usr/bin/env perl
use 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. 無限ループの中のエラーハンドリング
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. 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. 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 update
CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY funcname (funcname),
KEY retried_fg (retried_fg)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
明確な スキップ ケース別 Retry間隔 再実行な 多様なシリ ワーカーの
進捗把握 Retry設定
処理単位 機能 エラーログ 設定 失敗保存 アライザー 専有回避
44
- 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. 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. 動作確認テストをする
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
- 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. 不安定な処理の例
- 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. 複数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