在使用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为例,但基本上我们需要认真考虑故障切换时的重新连接处理的问题。