鮎の魚拓

今日も鬱々と、ミステリ読んだりコード書いて生きています。

Resque(perl ver)を触っていた

message queueとして長らくQudoのお世話になっていたのだが、もろもろきっかけがありjobの仕組みを考え直すことになった
Qudoを使っていて感じたペインポイントをまとめてみる

Qudoの運用でぱっと思い出せるペインポイント

雑な前提として、自分がここ一年ほど運用していたプロジェクトではかなり多量のjobを投げていた


  • enqueueやdequeueのクエリが遅い場合がある
    • slow queryにがんがん流れてきていた
  • qudoのmysqlをアプリのデータを持つmysqlから独立させていなかったのだが、qudoのクエリでサーバ負荷がかなり高まっていた
    • qudoのmysqlを別サーバに分離させたところ劇的にサーバ負荷が改善された
  • mysqlのスケールアウトが割りと面倒
    • クエリが重いので、ある程度規模が大きくなると負荷的に1セットでは足りなくなる
    • worker用のサーバを立てるのと比べてmysql増やすのは大事になりがち(オンプレ並感
    • jobの消失はなるべく避けたいので、master1台にするわけにもいかず、また一台死んだくらいで休日対応もできればしたくない、と考えると3台セットにして足していくことになる
    • 増殖するmysql
    • それなりのスペックのサーバを割り当てることになる。低スペックのサーバを割り当ててもいいが、3台1セットで増やしていくなら数がすごいことになる


だいたいmysqlで運用しなければならないことに起因していて苦労を背負っていたようなきがする。オンプレだったので余計に、ということもあるけど、AWSなどでもEC2を増やすのに比べてRDSのインスタンスを増やすのは比較的ことが大きのではないかと思う。なるべくbackendの部分は台数増やさなくてもスケールしてほしいし、アプリ側からはチューニングの余地がほぼないのでenqueueとdequeueは高速に動いてほしい


そんなわけで、SaaSに寄せないパターンを採択するならRedisを使う系のものを試してみたいなあと考えていたのと、preforkするWorkerの実装が比較的楽そうなもの、ということでResqueに行き着いた。RubyならばSidkiqなどの選択肢もありそうだが、PerlのプロジェクトだったのでPerl実装のあるResqueをとりあえず試している

github.com
基本的な使い方はSYNOPSISとかadvent calendarにあるので省く


起動スクリプトの周りの話

systemdから起動スクリプト叩いて、そこからpreforkしたworkerを立ち上げて使いたい。まじめにやるとシグナルの取り回しなどそこそこ面倒くさい*1*2のだけど、Resque::Workerに最初からそのあたりのことが実装されているので、preforkする側ではシグナルをtrapするだけで済んでくれている。実装読みつつ書いた結果、だいたい以下のような起動スクリプトで行けそう

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;

use Resque;
use Parallel::Prefork;

my $pm = Parallel::Prefork->new(+{
    max_workers => 3,

    trap_signals => +{
        TERM => 'TERM',
        INT  => 'INT',
        QUIT => 'QUIT',
        HUP  => 'QUIT',
        USR1 => 'USR1',
        USR2 => 'USR2',
        CONT => 'CONT',
    },
});


$pm->start(sub {
    my $resque = Resque->new(redis => 'localhost');
    my $worker = $resque->worker;

    $worker->add_queue('hoge-queue');

    $worker->work;
});

$pm->wait_all_children;

Resqueのシグナルのハンドリングはこのあたり
ResqueのWorkerは実際にjobを実行するとき、自身で行うのではなく、一度forkして子プロセスに行わせる仕様になっている。TERMやINITを受け取ると、実際に処理を行うプロセスを容赦なくSIGKILLして自身も終了する。QUITの場合は子プロセスを無理やり殺すことはない。restartの際はsystemdからHUPを送りつけたいので、QUITに直して渡してやる

前述の通り、forkしてから使い捨ての子プロセスで実際の処理が行われるので、max_reqs_per_child的なことは考える必要がなさそう

足りなさそうなこと

retryやjobのtimeoutなどがほしければ、jobの実装の方で別途用意する必要がありそう