在使用redis-rb时,在ElastiCache发生故障切换时需要注意的方法

遇到的情况

这次讨论的是关于仅在ElastiCache主群集中发生故障的情况。

根据文件中的说明,在多AZ环境中进行自动故障转移,目前的情况如下。

在昇格过程完成后(通常几分钟内),你可以立即继续进行写入操作。不需要更改写入端点,因为 ElastiCache 会同步昇格的副本的 DNS。

如下所述:

当前的要点是,“无需更改写入的终端节点即可反映DNS”。

产生的问题

Redis::CommandError READONLY You can't write against a read only slave.

当发生自动故障转移时,我们正在运营的Rails应用程序遇到了上述错误无法停止的情况。

最后,通过重新启动 Web 应用程序和工作程序,情况得以改善。

根据这个错误信息,由于在故障转移后仍然持续连接到同一个集群,导致对读取副本进行了写入操作。

没有重新连接到升级后的主要集群。

由于自动故障转移,DNS的目标地址已更改为升级后的主要集群,但是从Rails应用程序建立的TCP连接仍然指向以前的主要集群。因此,在故障转移时,必须更改连接目标。

但是,redis-rb并不会在出现上述的Redis::CommandError时进行重新连接。因此,该错误会被传递到redis-rb的外部。

据我们所知,有人在以下的问题中讨论了这个事情。
https://github.com/redis/redis-rb/issues/543

虽然情况有所不同,但是 redis-rb 看起来对 Sentinel 的支持相当完善。

在其他的库中是如何处理的?

Sidekiq 旁路执行

Sidekiq使用redis-rb库连接到Redis,但对当前问题的响应如下。

def self.redis
  raise ArgumentError, "requires a block" unless block_given?
  redis_pool.with do |conn|
    retryable = true
    begin
      yield conn
    rescue Redis::CommandError => ex
      #2550 Failover can cause the server to become a slave, need
      # to disconnect and reopen the socket to get back to the master.
      (conn.disconnect!; retryable = false; retry) if retryable && ex.message =~ /READONLY/
      raise
    end
  end
end

如果连接到READONLY时,请重新连接并参考直接错误消息。

Ioredis

这是一个 JavaScript 库。

在之前的问题中,提供了一个钩子来在错误发生时重新连接。

为了处理这次的响应,可能需要像上文提到的 Sidekiq 一样,在错误消息中编写钩子来进行处理。

Fast的Redis

这是Perl的库。

关于 Redis::Fast 的重新连接

这篇博客非常详细,解释也十分易懂!

该如何应对?

我认为理想的情况是,在讨论中问题得到解决并提供了某种解决方案或引子。

除了redis-rb之外,我认为也可以寻找其他可以应对这个问题的gem,但在这里,我们将考虑在继续使用redis-rb的前提下制定解决方案。

像 Sidekiq 一样,在外部捕捉异常并重新连接。

由于在连接到Redis.current的地方每次都要执行这个操作非常麻烦,因此似乎需要一些改进。

确保每次都能顺利连接

我在Rails的初始化器中使用了Redis.current = Redis.new的代码,并在应用程序中使用Redis.current,但是如果每次都使用Redis.new来创建实例,就不会发生类似本次的情况。

对redis-rb进行修改

变成Monkey Patch后,跟上redis-rb的更新是件困难的事情。。

def ensure_connected
  disconnect if @pending_reads > 0

  attempts = 0

  begin
    attempts += 1

    if connected?
      unless inherit_socket? || Process.pid == @pid
        raise InheritedError,
          "Tried to use a connection from a child process without reconnecting. " +
          "You need to reconnect to Redis after forking " +
          "or set :inherit_socket to true."
      end
    else
      connect
    end

    yield
  rescue BaseConnectionError
    # ★★ ここに READONLY のエラーを入れることができれば、reconnect_attempts オプションでリトライに持っていける。
    disconnect

    if attempts <= @options[:reconnect_attempts] && @reconnect
      retry
    else
      raise
    end
  rescue Exception
    disconnect
    raise
  end
end

上述的 ensure_connected 方法被嵌入到每个 Redis 命令调用的内部。
如★所注释的,可以将此次错误作为 BaseConnectionError 来处理。

reconnect_attempts的默认值是1。
https://github.com/redis/redis-rb/blob/master/lib/redis/client.rb#L21

您还可以在这里捕获 Redis::CommandError 并像 Sidekiq 一样查看错误消息。

另外,还可以考虑在引发 Redis::CommandError 的位置引发 BaseConnectionError。方法如下所示:

def format_reply(reply_type, line)
  case reply_type
  when MINUS    then format_error_reply(line)
  when PLUS     then format_status_reply(line)
  when COLON    then format_integer_reply(line)
  when DOLLAR   then format_bulk_reply(line)
  when ASTERISK then format_multi_bulk_reply(line)
  else raise ProtocolError.new(reply_type)
  end
end

def format_error_reply(line)
  # ここでメッセージを見て、BaseConnection を投げることもできる。
  CommandError.new(line.strip)
end

已经有人在进行这个实现,您可以在以下链接找到:https://github.com/craigmcnamara/redis-elasticache

似乎还提交了一个要求将其纳入redis-rb的问题,嗯,这个怎么样呢?

最后

我认为应该根据应用程序的运作情况来确定如何处理。

这次只以ElastiCache为例,但基本上我们需要认真考虑故障切换时的重新连接处理的问题。

bannerAds