Cloud Native环境中Spring Session Redis的生产注意事项

这是有关Spring Session Redis的第二篇文章。 在上一篇文章中,我解释了该框架背后的思想并提供了一个快速演示。

在本文中,我将提供一些在部署由Spring Session Redis支持的应用程序时的最佳实践和生产注意事项。


Redis主/从设置

至少,Redis应该以主从配置部署。 单个实例显然是任何系统的单点故障。
如果主服务器出现问题或不可用,则主从服务器将允许系统故障转移到其他实例。

Redis使用哨兵的概念来确定谁是当前的主人。 可以将Sentinels视为旁观者,他们不断检查实际主服务器或从属服务器的运行状况。 当发现主服务器出现问题时,哨兵基本上会投票决定应提升从属成为新的主服务器。 我不会在本文中介绍所有详细信息,但是以下链接提供了对您所拥有选项的深入说明。 至少,您应该在一个单独的机器/ VM上设置一个主机和两个从机的配置。

+----+ 

M1

S1
+----+

+----+
+----+
R2
----+----
R3
S2

S3
+----+ +----+
Configuration: quorum = 2

Spring Session Redis使用Spring Data Redis框架与Redis对话。 这意味着将相同的考虑因素和最佳实践应用于两个框架。

当使用基于哨兵的配置时,您不提供redis主机/端口信息来引导数据redis。 相反,您应该提供主服务器的属性和前哨URL列表。 每个哨兵进程都有其自己的配置文件,该文件列出了主Redis服务器。 例如:

 sentinel monitor mymaster 127.0.0.1 6379 2 
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1

无需在配置中指定从站,因为它们是可自动发现的。 同样,请参阅此处以获取完整的详细信息。 一旦配置了主,从和哨兵,您将需要在应用程序中更改spring数据redis配置,以与哨兵而不是主机/端口一起使用。 例如,在本地计算机上,它将如下所示:

  spring.redis.sentinel.master = mymaster #Redis服务器的名称。  spring.redis.sentinel.nodes =本地主机:6379,本地主机:6380,本地主机:6381 
#以逗号分隔的host:port对列表。

Redis集群设置

另一种方法是使用Redis集群,这意味着您将拥有多个主服务器。 数据实际上是在所有可用的主服务器上进行分片/分区的,这提高了数据库的性能,因为每个节点仅需要处理请求的一个子集。 此处提供了有关Redis集群设置的良好教程。

Redis Labs提供了Redis的企业版。 它还提供了通过DNS代理与群集进行交互的功能。 由于您不再需要了解集群的拓扑,因此极大地简化了客户端的工作,因此无需更改客户端即可对数据库进行更改。 真正需要提供的是Redis代理主机和端口。 您可以在此处下载Redis Labs的试用版。

泳池设置

让我们看一下Spring Session Redis可用的spring应用程序属性:

如果您确实决定采用开放源代码路线,请记住Redis是单线程应用程序。 监视运行您的Redis数据库的计算机或VM可能会提供不可靠的结果。 例如,如果在双核计算机上运行Redis,则VM将报告最大负载时CPU利用率为50%,而Redis进程本身可能报告CPU利用率为100%。 如果要在四核计算机或VM上运行Redis,则VM将报告25%的CPU利用率,而Redis进程本身实际上已达到100%。 确保监视Redis进程本身。

监控命令

Redis具有一些绝对不应在生产环境中使用的命令。

  • KEYS *在数据库中的所有键上运行并计算它们的总数。 同样,Redis是一个单线程数据库。 这意味着在此昂贵的过程中, 数据库对您的应用程序无响应 ! 切勿在生产环境中运行此命令。
  • SMEMBERS是一个命令,它返回给定集中每个键存储的值。 如果运行命令的目的仅仅是为了计算活动会话的数量,请不要使用此命令。 它的基数为O(n)(类似于上面的KEYS命令),并且可能在生产过程中“停止运行”。 根据您的Spring Session设置,您可能拥有一个包含键FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME的集合,该FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME可以保存系统中的所有/许多会话,这使得此命令与KEYS并没有太大区别。
  • SCARD 是返回给定集中值大小的命令,并且基数为O(1)。 因此,可以在生产环境中安全地使用它。
  • INFO命令可提供有关数据库的大量信息,并且可以安全地使用。 更有趣的项目之一是Instantaneous_ops_per_sec 。 这表明Redis每秒执行的操作数量。 监视以识别负载意外增加是一项重要的项目。

春季会议Cron Job

我将最好的(或最差的)保存下来。 在部署到生产环境之前,您应该了解框架中的实现细节。 我认为最好的解释方法是举一个例子。

我将从在测试机上运行演示项目开始。 我正在运行一台Eureka服务器,一台Config服务器,一台网关和一台订单管理微服务。

对于此模拟,通过将以下内容添加到启动脚本中,我将服务器会话超时更改为仅2分钟,而不是默认的30分钟:

  mvn spring-boot:运行-Dspring.redis.host = localhost -Dspring.redis.port = 12000 -Dserver.session.timeout = 2 -Dserver.port = 0 

当我转到Redislabs监视仪表板时,该图非常无聊:

现在,我将添加更多服务器实例,这样我将拥有8个网关和8个订单管理实例。 由于我在单台测试机上运行,​​因此这并不完全代表性能测试环境,但是更改立即显而易见:

哇! 刚刚发生了什么? 服务器应该完全空闲,因为有0个连接的客户端。 但是,每一分钟,每一分钟,每秒的读取和操作次数都会如此小小的峰值。 我敢肯定,这没什么可惊的?

现在,让我们开始无限期地登录系统。 为此,我将使用一个简单的curl命令,并从单个线程进行所有登录,以创建稳定,可预测的系统登录流。 另外,我只登录8个网关中的一个实例。

 对于((i = 1; i  / dev / null; 做完了 

我的期望是Redis的负载会增加,最终会平均到代表传入请求的简单,可预测的扁平线。 我真正得到的是:

看起来真的很可疑,不是吗? 再一次,每分钟大约在:00秒标记附近,每秒的操作次数突然增加,每秒读取一次Redis。 如您所见,尖峰现在更加令人担忧。 它不再是从0到5的峰值,而是从每秒约700到几乎4000运维的峰值。 如果我们查看“其他命令”图,则会看到类似的模式,表明其中一些命令不是读写操作:

这里发生了什么?

要理解这一点,我们需要更深入地了解源代码。

Spring Session Redis在名为RedisOperationsSessionRepository的Spring Repository类中配置系统。 它处理了框架的许多方面,其中包括如何清除过期的会话:

如您所见, cleanupExpiredSessions方法被安排为每分钟运行一次。 这意味着您在生产环境中拥有的所有实例将在同一时间每分钟同时连接到Redis。 他们将在此cleanExpiredSessions()方法中做什么?

此代码块:

  • 四舍五入到最后一分钟
  • 循环遍历此后到期的所有键,然后将它们从Redis中删除(我们现在知道图中的“其他命令”是什么)。
  • “触摸”会话以确保将其删除。 在Redis中,密钥要么在下次访问时延迟过期,要么通过随机读取数据库中的密钥而被动失效。

同样,这是在连接到Redis的每个实例上同时完成的。

您可能想知道为什么我们甚至需要此实现。 如前一篇文章所述,Redis可以自行使密钥失效,并且确实为会话设置了失效期限。

原因是Redis不能确切保证何时删除这些密钥。 很长时间可能会无法访问此会话,这意味着它实际上不会过期,也不会被删除。 尽管这似乎不是问题(会话将在下一次访问时立即终止),但是开发人员可能已经实现了SessionExpiredEvent侦听器,这些侦听器有望在会话终止后立即执行(例如,清理Web套接字)。 因此,框架会尽最大努力使最接近实际到期时间的密钥到期。

但是,等等,还有更多!

我们还没有完成。

在@Configuration类RedisHttpSessionConfiguration中创建了一个侦听器,该侦听器已注册为接收来自Redis的删除和过期事件。 相关的源代码如下。 redisMessageListenerContainer创建侦听器,而setConfigureRedisAction告诉Redis在任何密钥删除或到期时通知此特定的调用客户端(服务器实例):

回到RedisOperationsSessionRepository,我们看到以下代码:

需要消化的代码很多,但要点如下(没有双关语):

  • 每次删除或终止会话时,都会调用onMessage方法。
  • 在内部,它再次连接到Redis,以获取即将过期/删除的密钥的会话详细信息。
  • 然后,它发布SessionDeletedEventSessionExpiredEvent ,以允许用户使用处理程序 bean自定义删除/过期。 Spring文档将其描述为您可能想要清理Web套接字的地方(如果您碰巧使用了它们)。

因此,回顾一下:

  • 每分钟,每个服务器实例将连接到Redis以删除所有过期的会话。 如您所料,这是很多冗余的调用,因为只有第一个服务器才能真正实现此目的。
  • 然后,Redis将通知每个实例该会话已删除。
  • 然后,每个服务器实例将连接到Redis以获取即将到期的会话的会话详细信息。

这是一次完成的许多活动。 让我们对生产环境进行一些实际计算:

如果我们碰巧有100个服务器实例,并且每分钟有2500个会话到期(这是一个相当普遍的生产用例),这意味着每分钟至少有500,000个电话在完全相同的一秒钟内访问Redis

这可能会成为一个严重的问题,您可能超出了典型Redis集群的能力。

但是,还有一个更大的问题。

通常,为了支持不断增加的负载,您甚至可以在某些云环境中进行自动扩展,将更多服务器实例添加到生产环境中。 但是,在其默认设置下,您添加到生产环境中的服务器越多,问题就变得越严重,您将生产置于更大的风险中。

注意:我在此项目上打开了github问题。 该团队回应说,他们添加了一个属性,以便在需要时更改cron计划,并认为将侦听器配置保持在适当位置以允许开发人员自定义过期处理很重要。 我完全尊重,但是作为开发人员的您应该意识到这一折衷。

减轻

有几种方法可以解决此问题:

  • 正如您在上面的代码示例中看到的那样,在Spring Session的最新版本中(1.3.1是撰写本文时的最新发行版),有一种方法可以覆盖1分钟的cron计划默认值。 在以前的版本中,它被硬编码为1分钟,因此至少我们现在可以更改它。 尽管如此,在所有实例上都将同时触发cron,这在我看来仍然是不可取的。
  • 如果您不希望数据库中的某些会话停留的时间超过其原始到期时间,则只需将此cron作业设置为不可能的值即可。 一种方法是将其设置为不现实的日期,例如2月31日。 在您的配置中,您可以添加以下内容:
  spring.session.cleanup.cron.expression = 0 0 5 31 2 ? 
  • 如果您不希望在会话过期时收到通知(即,您不需要在会话过期时执行的自定义实现),则可以禁用Redis的事件通知。
    请注意,必须在所有类型的所有服务器上禁用它们。 即使这些服务器之一更改Redis数据库配置以启用通知,所有连接的客户端(所有服务器)仍将收到到期事件。 禁用此功能的方法是更改
    从默认的ConfigureRedisAction到NO_OP实现:
  • 如果您确实想实现自定义会话过期/删除事件处理程序,那么它会有些麻烦。
    您想要做的是禁用消息侦听器
    除一台外的所有服务器。 开发仅处理会话到期事件的专用微服务。 请记住,所有实例都连接到相同的Redis数据库,因此该专用微服务将接收所有会话过期事件。 这个专用的微服务“守护程序”在生产环境中将只有一个实例,或者可能有两个实例来支持故障转移。 主要思想是让最少数量的实例处理会话到期事件,以允许系统的其余部分正常运行。 设置专用微服务后,您可以在所有其他服务器上禁用侦听器,如下所示:

注意:Spring Session Redis的新里程碑2.0.0版本似乎已删除@Scheduled cron批注,但是似乎只是在配置上有所不同。 如果2.0.0提供了一种完全禁用cron的简单方法,我将更新本文。

在本文中,我解释了将Spring Session Redis支持的应用程序部署到生产环境时的一些生产注意事项。 进行严格的性能测试始终很重要。 我建议使用提供良好监视可见性的工具。 正如您在上面看到的,如果没有基于图形的监视工具,这很容易被忽略。

祝您好运,编码愉快!

奥德·Shopen