如何无效化缓存-分布式缓存问题
在过去的6年中,我一直参与构建中间件平台(wso2)。 我们大多数的客户部署都是群集,它们始终需要高可用性,有时还需要可伸缩性。
我们大多数服务器都是无状态的(也就是说,它们将状态保存在数据库中)。 我们的CEP服务器和邮件代理是两个值得注意的例外,但现在暂时将其忽略。
设置无状态服务器集群很容易(或者我们相信)。 我们设置服务器并放置一个负载均衡器(F5,HA Proxy,mod_proxy,Nginx)以分配负载。 不幸的是,我们还需要更多。 我们需要处理集群中服务器之间的安全性,会话,节流和工件部署。
问题:令牌失效
让我们考虑一下安全性。 用户的登录信息,获取令牌(例如OAuth),启动会话并继续调用服务器。 每次调用都无法访问数据库以获取令牌,这太昂贵了,因此我们必须缓存令牌。 现在,如果令牌被吊销,我们必须从数据库中删除令牌,然后使所有节点中的缓存无效。 这是令牌失效问题。
这个问题有一个神奇的答案。 分布式缓存。 使条目无效时,它将与所有节点通信并使条目无效。
分布式缓存问题
所有这些都很好。 一段时间以来,一切工作正常。 最终,噩梦来了。 分布式缓存系统很脆弱。 我们看到系统不稳定,死机或死了。
让我解释一下原因。 当我使缓存无效时,分布式缓存必须知道哪些节点具有该条目(或向所有节点发送消息)。 为此,它需要知道系统中的节点是什么。 窍门是系统中的所有节点必须就系统中的哪些节点达成共识。 这是分布式共识。 为了解决上述问题,需要维护一个群组通信系统。
维持会员团体意味着很多麻烦。 让我列出一些。
他们没有很好地扩展的第一个问题。 使用五个以上的节点运行此操作很棘手。
第二个问题是,当一个节点发生故障时,其他节点将检测到该故障并将其从组中删除。 (有几种方法可以做到这一点,它们通常涉及心跳和共识算法)。 问题是存在很多误报。
当节点被加载时(例如平均负载是内核数的两倍),它不能足够快地响应心跳,其他节点认为该节点已失败。 他们将重新排列组并重新排列数据。在VM和Docker的世界中,网络不是很可靠。 网络本身的延迟峰值可能超过心跳线限制。 节点也更频繁地死亡。
现在几乎没有问题了。 当一个节点被推定为死节点时,其他节点将重新排列数据。 假定的失败节点不知道它已失败并继续工作。 几分钟后,它将重新加入。 当它重新加入时,系统需要再次重新洗改数据。 如果您不走运,则可能会遇到节点不断进出的情况,系统将继续自我修复。
如果要重新创建此问题,请选择您喜欢的分布式缓存之一,加载大约1G数据,然后在那些服务器中运行一些cpu切换过程。
您会告诉我,我必须确保服务器未加载。 那真是胡扯。 这将"稳定"和"平稳降级"付诸东流。 问题在于,即使是短期负载峰值也会导致系统级重置和维修。 另外,加载服务器时,我需要做的最后一件事就是开始本科证修复和数据改组。
如果您进行大量监视和手动操作以确保未加载节点,则可以毫无问题地运行分布式缓存。 但是,很难为中小型部署提供这种关注。
此外,上述问题的严重版本是脑裂。 也就是说,将节点网络划分为两个集群。 然后,系统必须确保只有一半在工作(否则,您可能会遇到冲突的更新和陈旧数据)。
请注意,关键问题不是共识算法,而是检测到故障后的数据改组。 加载节点后,将检测到错误故障,这将导致数据混排,这将给服务器带来更多负载。 这种积极的反馈循环是灾难的根源。
我看不到针对分布式缓存对负载峰值和网络延迟峰值的高敏感性的解决方案。 在VM和docker实例以及SDN(软件定义网络)的世界中,性能和延迟可变性将变得更糟,而不是更好。
在这一点上,您感觉就像是那个故事的隐士,他带来了一只小猫追赶老鼠,最终结了婚,不得不上班。 请记住,所有这些都是因为我们想撤销令牌而开始的。
我相信分布式缓存是解决此问题的错误解决方案。 我不会轻易地说出来。 在过去的五年中,我们使用了EhCache,Infinispan,Zookeeper和Hazelcast。 在现实条件下,没有什么可以阻止的。 他们可以通过为期一周的发布测试,但运行数月后在客户站点失败。
如果尝试使用分布式缓存进行限制,会话复制或工件部署,也会发生相同的问题。 我不会在这篇博文中继续介绍它。
替代解决方案
还有其他方法可以解决问题。
解决方案1:使用本地缓存和缓存超时
最简单的解决方案是使用本地缓存,设置较短的缓存超时,而不执行任何操作。 如果令牌被吊销,它将在超时后刷新。
我同意这并不适用于所有情况。 在关键的用例中,您需要立即撤销。 但是,值得一问的是,您的用例是否值得每年额外花费10万美元。 您可能会花更多的钱来解决上述问题。
解决方案2:显式地使每个本地缓存中的缓存条目无效
想法是,当令牌被吊销后,我们与每台服务器通信并显式地使缓存无效。 我们需要向每个服务器添加服务API,以使缓存无效。 但是,添加API相对简单。
不幸的是,我们还没有完成。 您如何获得当前服务器的列表? 如何从分布式协调框架(如Hazelcast或Zookeeper)中获取它。 有可能的。 但是,如果您这样做了,那么您将遇到较早的麻烦,减去了数据改组。 实际上,数据改组是这里最困难的问题之一,没有它,您可能会很好。
另一方面,有一个更简单的解决方案。 我们可以为每个节点提供集群中其他节点的列表作为配置。 这要简单得多。 但是,如果要添加一个新节点,或者要有某种方法在运行时刷新节点列表,则必须重新启动集群(例如,将其放入配置文件中,让服务器每15分钟检查一次,以及 根据需要使用rysnc在所有节点中进行更新)。
最后,我们的困难尚未结束。 列表中的节点可能已发生故障或没有响应,或者网络已分区。 让我们在下一部分中讨论解决方案。
解决方案3:明确失效并可靠交付
当我们尝试撤消缓存条目时,如果节点发生故障,那么我们有几种选择。
我们可以重试,直到缓存超时达到中间等待时间。 只要无效的数量相对较少,就应该可以。我们可以使用持久性消息传递系统(例如WSO2 MB或ActiveMQ)来传递无效消息。 然后,即使该节点不可用,该节点返回时也会拾取无效消息。 该解决方案非常稳定。 但是,这意味着您需要消息系统的高可用性部署。 由于消息传递系统上的负载很小,因此很好地理解了这种部署。解决方案4:使用会话关联和显式无效
如果您已经使用会话亲缘关系,那么有一个更简单的解决方案。 在这种情况下,缓存条目将仅驻留在该客户端受会话亲缘关系绑定的节点中。 若要使无效,请发送一个无效请求,该请求的客户端令牌设置为会话,并且该无效请求将路由到唯一的节点
结论
分布式缓存通常用作缓存/令牌无效的解决方案。 尽管它提供了看起来很简单的解决方案,但我们看到了很多不稳定因素。 结果系统很脆弱,并且对性能峰值非常敏感。 对于中小型高可用性部署来说,这种复杂性很难说服。 我建议针对此问题,我们应该使用更简单的替代方法,而不是分布式缓存。