読者です 読者をやめる 読者になる 読者になる

Redis::Fast の reconnect について

この記事は tech kayac advent calendar 9日目の記事です。

インフラチームの @tkuchikiです。

最近検証した Redis::Fast の reconnect について紹介します。

Redis::Fast とは

Redis::Fast は、弊社 @shogo82148 作の高速な perl の Redis クライアントです。

社内の perl で書かれたアプリケーションは、Redis を使う場合、ほぼこのモジュールを使っています。

なぜ reconnect を考慮しないといけないのか

Redis Sentinel や ElastiCache for Redis を使い、フェイルオーバーできる冗長構成を組んだ場合のことを考えます。
Master がダウンした際、自動で Slave を Master に昇格してくれるためダウンタイムが短くて済みます。
この場合、Master が切り替わった時点で別のサーバに reconnect しなくてはなりませんが、
もし、reconnect に失敗すると、例え自動で Slave を Master に昇格してくれたとしても接続できないので非常に困ります。
そのため、冗長構成を組む際は、切り替わった環境へ正常に接続できることが重要になります。

どのようなケースで再接続に失敗するのか

ElastiCache for Redis を例に上げると、
Master ノードがダウンすると、Slave ノードが Master に昇格し、
ドメインの向き先 IP アドレスが変わります。
永続接続の場合、Redis::Fast のインスタンスを生成した時だけ DNS を参照するような実装になっていると、
ドメインの向き先が変わったことに気がつく術がありません。
幸い、Redis::Fast は reconnect 時に DNS を参照し直す実装になっているので問題ありません。
また、都度接続の場合も接続の度にインスタンスを生成するので問題になることはない(はず)です。

Redis::Fast の reconnect について

Redis::Fast は reconnect が充実しており、

  • TCPコネクション切断
  • Redis との接続が Timeout
  • サーバに接続できない

など、大抵のケースで reconnect してくれます。

では、どういうケースでは reconnect してくれないかというと、それはトランザクションです。
Redis には、MULTI、EXEC(DISCARD)、WATCH というトランザクション機構があります。
トランザクション中に接続が切断した場合、 トランザクションがリセットされるケースが多いと思いますが、
もし、トランザクション中に接続が切れても reconnect できると中途半端な状態でコマンドが実行される可能性があります。
例えば、MULTI したあとに 3 つのコマンドを実行して EXEC するというケースで、
1つめのコマンドを実行した直後に接続が切れて reconnect すると、
トランザクションがリセットされて 2 つ目と 3 つ目のコマンドだけが実行されてしまいます。
トランザクションの途中で reconnect した場合、リセットされたコマンドを再度実行するような作りになっていれば、
問題ないかもしれませんが、例外を吐いて最初からやり直すのが一般的だと思います。

RDBMS(MySQL) ではどうなってるかも見てみましょう。

perl で有名な O/R マッパーである DBIx::Class にも独自に再接続する機構が入っており、
こちらもトランザクション内では reconnect しないようになっています。

また、MySQL にも reconnect の仕組みがありますが、MySQL :: MySQL 5.6 リファレンスマニュアル :: 23.8.16 自動再接続動作の制御 を見ると、
ロールバックする、オートコミットモードが解除される、テーブルロックが解除されるなど、
嬉しくない挙動をするため使わないほうが無難そうです。
DBIx::Class が独自の再接続機構を持っているのはこのためでしょう。

Redis::Fast では reconnect すると問題があるケース以外は(たぶん) reconnect してくれます。

Redis のトランザクションについては、Redis Documentation (Japanese Translation) トランザクション
を参照していただくと理解が深まると思います。

reconnect の実験

実際に reconnect してくれるか確認するために、以下の実験を行いました。

  • TCP 接続を切断
  • 擬似フェイルオーバー

TCP接続を切断する

$  sudo tcpkill -9 -i lo port 6379
tcpkill: listening on lo [port 6379]

とした状態で、

#!/usr/bin/perl
use strict;
use warnings;
use utf8;
use Data::Dumper;
use Redis::Fast;
use Time::HiRes qw/usleep/;

my $redis = Redis::Fast->new(
    reconnect => 1, server => '127.0.0.1:6379',
    cnx_timeout => 1, read_timeout => 1, write_timeout => 1
);

while (1) {
    print Dumper $redis->ping();
    usleep(100000);
}

1;

を実行すると、PING を打つ -> 接続を切断 -> reconnect -> PING... を繰り返す挙動を確認できました。

擬似フェイルオーバー

サーバを 2 台用意し、それぞれで Redis を起動します(サーバA、Bと呼称)。

  1. サーバ A の Redis に接続
  2. サーバ A の iptables で Redis への通信を DROP
  3. サーバ A の Redis をダウンさせる
  4. ドメインの向き先をサーバ B に変更
  5. サーバ A の iptables を FLUSH

という手順で擬似的に ElastiCache for Redis のフェイルオーバーの挙動を再現した実験を行いましたが、
reconnect してくれました。

まとめ

Redis::Fast の reconnect について紹介しました。
Redis::Fast は弊社で年単位で安定稼働している実績がありますし、
Redis 公式が推奨している Redis.pm よりも高速に動作するので是非使ってみてください!

記事の公開当日まで問題があるケースがないか調べて、
これだ!というケースを見つけて Pull Request まで作ったのですが、
検証をやり直すとやっぱり問題ないかも?となったので、
引き続き検証を行い、もし改善点があって、改善した場合はどこかでお知らせ致します。

10日目は、同期の butchi_y です!
お楽しみに!!