从为Redis的Amazon Elasticache选择Java驱动程序中学到的经验教训 - 第2部分
#aws #java #redis #elasticache

在本系列的part one中,我们将Jedis视为基于Java的Redis驱动程序,当时与Elasticache结合使用,以簇模式重新使用。我们了解到,在故障转移期间,吉迪斯(Jedis)需要一些时间来弄清楚新主体才能恢复与Elasticache群集的连接。在这篇文章中,我分享了从切换到生菜作为我们基于Java的应用程序的驱动程序中学到的一些教训。

一个不同的驱动程序

Lettuce是Redis的基于Java的驱动程序,是Redis使用Spring Framework时的默认驱动程序。生菜是由Netty构建的完全非阻止重新限制客户端,可提供反应性,异步和同步数据访问。

作为驱动程序,我们发现与吉迪斯(Jedis)相比,它与使用非常相似。与您在下面的代码中看到的那样,配置客户端非常容易做到。

RedisURI redisUri = RedisURI.Builder.redis(REDIS_HOST)
                                    .withSsl(true)
                                    .build();

RedisClusterClient clusterClient = RedisClusterClient.create(redisUri);

StatefulRedisClusterConnection<String, String> connection = clusterClient.connect();

syncCommands = connection.sync();

使用Jedis,我们配置了一个连接池,但是您可能会从上面的代码中注意到,我们没有使用连接池。生菜连接被设计为线程安全,因此可以在多个线程之间共享一个连接,默认情况下可以在生菜连接auto-reconnect之间共享一个连接。如果需要,您仍然可以配置连接池,但是我们在情况下不需要它。

生菜状态为 cloud Ready ,听起来很有希望。

通过基于云的主要重新服务进行战斗测试。生菜采用各种云产品的细节以无缝集成以获得最佳性能。

AWS博客包含a solid article on ElastiCache for Redis client performance。在AWS团队进行的测试中,我们可以看到Jedis的速度是生菜的两倍。如果表现是您的“唯一”关注点,这是一个有趣的事实。

Lettuce vs Jedis performance

鉴于生菜是用Netty构建的,我们还立即注意到对我们lambda功能的初始化时间(冷启动)产生了很大的影响。 Netty在执行时确实很快,但是需要一些时间来初始化。新的Lambda Snapstart functionality可能会有所帮助。

实例化与生菜的授权连接大约 1500 ms ,这是与Jedis相比的两倍。

对于生菜,我们将运行相同的测试方案,我们将通过从测试lambda函数中返回群集节点列表来跟踪群集中的节点数量。我们的Lambda吐出有关群集和不同节点角色的当前信息。

{
  "nodes": "254eaace08fafb42d26750b8721d1fd5b152f0bd test-cluster-0001-001.test-cluster.ifrpha.euw1.cache.amazonaws.com:6379@1122 master,fail - 1679323815172 1679323813195 1 connected
5f41504ecd68d90956060e219ebf8ec32782c4e2 test-cluster-0001-003.test-cluster.ifrpha.euw1.cache.amazonaws.com:6379@1122 myself,slave fa0e9e04c33409b48a8d486e27277bd313e01c27 0 1679323902000 3 connected
fa0e9e04c33409b48a8d486e27277bd313e01c27 test-cluster-0001-002.test-cluster.ifrpha.euw1.cache.amazonaws.com:6379@1122 master - 0 1679323903029 3 connected 0-16383"
}

故障转移方案

就像吉迪斯一样,我们想结合以下两个方案来测试生菜:

  1. 主要节点失败。
  2. 维护升级将旋转并将节点更新为新版本Redis(从6.0.x到6.2.x)。

主节点故障转移

就像在part one中一样,我们将测试主要节点故障转移的影响,并查看生菜客户如何处理情况。生菜似乎非常可配置和灵活,因此我们测试了几个不同的方案。

尝试1:简单设置

我们的第一次尝试是使用前面显示的简单客户端配置。我们不使用任何连接池,并且在连接到集群时具有非常简单的同步客户端。我们与群集配置端点连接,以便REDIS的Elasticache可以使我们可以看到群集拓扑。为了分析故障转移的影响,我们开始使用CloudWatch日志见解。它使我们能够扫描故障转移期间的例外数量。通过在我们的lambda日志组上使用以下查询,我们可以看到所有包含Java异常的日志消息。

fields @timestamp, @message, @logStream, @log
| filter @message like /(?i)(Exception)/
| sort @timestamp desc
| limit 2000

从我们可以看到的情况下,在故障转移期间会抛出很多例外,但是,例外发生了大约 2 分钟,之后客户能够正确处理请求。

Graph showing number of errors

大多数例外属于超时和连接错误的类别:

io.lettuce.core.RedisCommandTimeoutException: Command timed out after 1 minute(s)


io.lettuce.core.cluster.topology.DefaultClusterTopologyRefresh$CannotRetrieveClusterPartitions: Cannot retrieve cluster partitions

两者似乎都与生菜试图从主/主集群节点获取值的事实有关,并试图与刚刚“下降”的主节点重新连接。

尝试2:拓扑刷新

生菜具有称为topology refresh的功能,该功能允许客户端对服务器进行轮询以获取群集拓扑的更新视图。我们想知道启用这是否会对我们的客户失去与主要节点的联系这一事实产生重大影响。

ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
                // default refresh period = 60 seconds
                .enablePeriodicRefresh()
                .enableAllAdaptiveRefreshTriggers()
                .build();

clusterClient.setOptions(
    ClusterClientOptions
      .builder()
      .socketOptions(SocketOptions.builder().build())
      .timeoutOptions(TimeoutOptions.enabled())
      .topologyRefreshOptions(topologyRefreshOptions)
      .pingBeforeActivateConnection(true)
      .build()
);

Graph showing number of errors for attempt 2

基于日志见解图,我们可以看到,由于故障转移过程中产生的错误数量的数量较小,而且数量也较小,但是我们的应用程序在从集群中阅读时仍有阅读问题。日志相似,而且数量仍然相当大,因此我们想了解有关其他不同配置选项的更多信息。

尝试3:更改阅读偏好

默认情况下,就像吉迪斯(Jedis)一样,从主节点 /碎片中读取。但是,生菜还有一个额外的选择,吉迪斯没有称为read preference。如果您的工作负载允许,则可以告诉生菜也可以在主节点下降时从副本节点读取。在读取的群集中,这可能是一个非常有价值的环境。

使用connection.setReadFrom,您可以选择选择特定的偏好。一些可用选项包括:

  • 上游(主节点)

  • upstream_preferred(主/上游,如果上游不可用,则落回复制品)

  • replica_preferred(如果没有可用的副本,请从副本中读取,然后回到上游)

  • replica(仅从读取复制品中读取)

在我们的情况下,我们将其配置为ussstream_preferred,因此在主节点故障转移的情况下,我们将失败到副本。

StatefulRedisClusterConnection<String, String> connection = clusterClient.connect();
// removed topology refresh for readability (it's included in the actual configuration)

connection.setReadFrom(ReadFrom.UPSTREAM_PREFERRED);

syncCommands = connection.sync();

我们可以看到对故障转移过程中产生的错误消息数量的明显影响。

Graph showing number of errors for attempt 2

从日志中,我们仍然可以看到一些错误消息,表明该集群在故障转移期间已下降。我们为什么仍然收到这些消息?

尝试4:读取偏好并允许群集“下降”时读取

进行了更多的研究后,我们了解到,默认情况下,当主节点 /碎片标记为“ down”时,默认情况下不允许从副本进行读取操作。但是,对于重读工作负载,这可能不是问题。如果愿意,当群集通过更改参数标记为向下标记时,可以允许读取。

  • cluster-allow-reads-当降落时 <yes/no>:如果将其设置为否,因为默认情况下,当redis群集中的一个节点将在群集被标记为失败时停止服务所有流量,当一个节点无法达到大师法定人数时,或者当没有满足全面覆盖范围时。这样可以防止从没有意识到群集变化的节点中读取潜在不一致的数据。可以将此选项设置为“是”,以允许在失败状态期间从节点进行读取,这对于想要优先读取可用性但仍希望防止不一致写入的应用程序很有用。当仅使用一个或两个碎片使用redis群集时,也可以使用它,因为它允许节点在主失败时继续写入写入,但自动故障转移是不可能的。

在AWS CDK中,您可以定义您提供给群集的自定义参数组。您可以根据默认可用组基础参数组;例如“ default.redis6.x.cluster.on”

CfnParameterGroup parameterGroup = CfnParameterGroup.Builder
  .create(this, "elasticache-param-group")
  .cacheParameterGroupFamily("redis6.x")
  .description("Custom Parameter Group for redis6.x cluster")
  .properties(
    Map.of(
      "cluster-enabled", "yes",
      "cluster-allow-reads-when-down", "yes")
    )
  .build();

CfnReplicationGroup replicationGroup = CfnReplicationGroup.Builder.create(this, "replicationgroup-id")
                .cacheNodeType(props.getCacheNodeType())
                .engine("redis")
                .engineVersion(props.getEngineVersion())
                .autoMinorVersionUpgrade(true)
                .automaticFailoverEnabled(true)
                .cacheParameterGroupName(parameterGroup.getRef())
                .multiAzEnabled(true)
                .atRestEncryptionEnabled(true)
                .transitEncryptionEnabled(true)
...

配置上述更改后,我们注意到,在故障转移期间我们仍在执行30个请求时,“无缝”切换到副本节点,我们的lambda函数正常运行。失败期间没有产生错误。看起来很有希望!

集群引擎版本升级

下一个方案是运行集群引擎升级。为了验证版本升级的影响,我们从Redis 6.0.x版本迁移到6.2.x。我们了解到,就像吉迪斯(Jedis)一样,我们没有看到任何明显的影响(至少在我们配置了上述所有选项之后)。升级发生了,而该服务的恒定负载为每秒30个请求,而无需产生任何错误。

概括

通过生菜进行此练习,我们了解到,在从弹性搜索中读取REDIS群集时,生菜具有一些高级功能。通过调整客户端和服务器配置,我们能够执行主节点故障转移,而无需在群集上生成任何错误。但是,如果性能比可用性更重要,则可能要坚持使用JEDIS,因为对于某些操作似乎最快的2倍。