如何提高缓存一致性

一个典型的Web应用程序引入了一个内存缓存像memcache或redis以减少在主数据库上读取热数据的负载。 最原始的设计看起来像下图。


+--------------------------------+ +------------+ +----------------+
| database <--------+ web server +--------> cache |
| mssql, mysql, oracle, postgres | +------------+ | memcache/redis |
+--------------------------------+ +----------------+

不幸的是,这个设计是非常普遍的,因为它引入了许多问题。 我看到一些大规模应用程序仍然在使用这种设计架构,他们使用一堆黑客技术来克服这些问题,增加了系统操作的复杂性,有时就表现为展示给最终用户的数据不一致。

问题1.每个Web服务器实例通过连接池连接缓存服务
在大型应用程序中,有时几千个Web服务器实例(尤其是像Ruby这样较慢的语言)和托管Web应用程序。 每个人都必须直接与底层基础web应用程序代码。 这包括主数据库,如MSSQL,MySQL,Oracle,Postgres和缓存服务如Memcache或Redis。 每个web服务器实例都拥有与每个数据库或高速缓存服务连接的连接池。


--------------------------------------------------------------------------
| database (mssql, mysql, oracle, postgres) |
+----^--^-----------^--^-----------^--^-----------^--^-----------^--^----+
| | | | | | | | | |
N connections | | | | | | | | | |
| | | | | | | | | |
+------------+ +------------+ +------------+ +------------+ +------------+
| web server | | web server | | web server | | web server | | web server |
+------------+ +------------+ +------------+ +------------+ +------------+
| | | | | | | | | |
N connections | | | | | | | | | |
| | | | | | | | | |
-----v--v-----------v--v-----------v--v-----------v--v-----------v--v-----
| cache (memcache, redis) |
+------------------------------------------------------------------------+


连接池的这么多连接会耗费Web服务器资源,包括数据库或缓存服务资源。 在设计一个大型系统连接到内存缓存或Redis的服务器的连接数为10,000或20,000并不少见。

问题2.许多Web应用程序请求必须执行缓set操作
类似于HTTP请求可以如何发出多个SQL INSERT或UPDATE语句,针对高速缓存服务会发出多个SET操作。 即使这些都是异步完成,它们仍然在Web服务器上消耗资源,如果Web服务器只需要关心更新主数据库,这种浪费将是巨大的。

问题3.没有容错。 如果缓存set操作失败,则会丢失数据
上图的Web应用程序如何操作的典型顺序将被设计如下。

1.更新主数据库(MSSQL,MySQL,Oracle,Postgres等)。
2.如果事务失败返回HTTP错误。
3.如果事务成功将SET操作发送到缓存服务器(memcache,redis等)。

任何SET操作都可能会失败,即使在重试后,缓存服务就与主数据库不一致,这可能导致用户看到不正确的信息。 更糟糕的是取决于应用程序是如何设计的,这会导致用户看到部分正确和部分不正确信息的部分失败情况 。

一些缓存服务协议支持在一个命令中发送多个SET操作,但一些不支持。并非所有Web应用程序都足够聪明,可以将在代码的不同区域中发生的SET操作分组为单个命令。如果是这种情况下,你可能有部分失败,有些地方的SET操作成功,而一些地方会操作失败 。

除重试之外,web应用程序并不能做更多事情来最终纠正缺失的缓存SET操作。它必须重试或在某个时间点放弃。 缓存将服务与主数据库将出现不一致,直到缓存通过TTL失效或一些其他方式失效。

消息中间件
有时,这通过诸如Kafka的消息传递中间件来解决,其中web应用将SET操作推入Kafka,并且消费者从Kafka拉取改变并对缓存服务执行SET操作。这极大地增加了缓存一致性,并允许缓存在短时间或长时间故障后仍然保持运行。

这会在系统中引入延迟。 用户可能无法及时看到更改后结果。 一些Web应用程序通过执行粘性会话和在Web应用程序内存中缓存来隐藏数据不一致来解决这个问题。如果Web服务器失败并请求路由到不同的Web服务器实例,则仍然可能存在过时的结果。 这引入了系统的请求路由层中的复杂性。


+------------------------------------------------------------------------+
| database (mssql, mysql, oracle, postgres) |
+----^--^-----------^--^-----------^--^-----------^--^-----------^--^----+
| | | | | | | | | |
N connections | | | | | | | | | |
| | | | | | | | | |
+----+--+----+ +----+--+----+ +----+--+----+ +----+--+----+ +----+--+----+
| web server | | web server | | web server | | web server | | web server |
+----+--+----+ +----+--+----+ +----+--+----+ +----+--+----+ +----+--+----+
| | | | | | | | | |
N connections | | | | | | | | | |
| | | | | | | | | |
+----v--v-----------v--v-----------v--v-----------v--v-----------v--v----+
| message queue (kafka, rabbitmq) |
+----------------------------------^--^----------------------------------+
| |
N connections | |
| |
+------+--+------+
| kafka consumer |
+------+--+------+
| |
N connections | |
| |
+----------------------------------v--v----------------------------------+
| cache (memcache, redis) |
+------------------------------------------------------------------------+

这大大降低了对缓存服务的连接负载,但是引入了很多的操作复杂性:

1.部署和操作像Kafka这样的高吞吐量消息系统。
2.部署和操作多个消费者进程,这些进程消耗Kafka中的消息,并对缓存服务执行SET操作以在消费者失败时生存。

问题4.与主数据库没有顺序一致性
莱斯利·兰波特描述顺序一致性如下。

任何执行的结果与所有处理器的操作以某种顺序执行相同,并且每个单独处理器的操作按照其程序指定的顺序出现在该序列中。

上图大大提高容错性和减少了损失,但更新并未按照顺序执行。一个用户可能看到的部分数据最新和部分数据是陈旧的结果。 潜在深层操作可能会失败,而随后的操作则会成功。可见的更改顺序可能是无序的。一些应用可能对这种不一致性更敏感。 一些应用可能需要严格的部分顺序。即使顺序不重要,提供顺序一致性会有更好的用户体验和更少的混乱。

解决方案:MySQL binlog复制
上图显示了一个共享的消息队列解决方式,但是部署一台带有容错的系统是不容易的,运行平稳同样不容易。 如果您使用带有复制的数据库,系统中已经有一个队列,您可能不需要部署另一个队列,用像Kafka这样的新基础架构来解决其中的一些问题。


+----------+---+---+---+---+---+ binlog replication +--------------------------+
| MySQL | 1 | 2 | 3 | 4 | 5 <------------------------+ MySQL replication client |
+----------+---+---+---+---+---+ +--------------------------+
MySQL binlog
binlog positions

MySQL有一个binlog复制协议,用于主/辅助复制。这实质上是一个复制队列,具有记录顺序的所有交易所示。

这不是一个流行的解决方案,但我说,为什么不能使用呢? 它可以工作得很好。您可以编写一个应用程序,该应用程序可以使用MySQL binlog复制协议,该协议使用二进制日志条目并对缓存服务执行SET操作。有两种方法可以使用binlog数据。

1.解释原始SQL语法并执行SET操作。
2.Web应用程序将缓存键作为注释嵌入SQL中。

这两个选项都很好,因为如果你需要和目标系统支持原子多集操作,你甚至可以在binlog语句中获得每个事务的事务范围。我喜欢第二个选项,因为它更容易解析,应用程序已经有这些信息在大多数情况下。


+------------+ +------------+ +------------+ +------------+ +------------+
| web server | | web server | | web server | | web server | | web server |
+------------+ +------------+ +------------+ +------------+ +------------+
| | | | | | | | | |
N connections | | | | | | | | | |
| | | | | | | | | |
+----v--v-----------v--v-----------v--v-----------v--v-----------v--v----+
| database (mssql, mysql,,oracle, postgres) |
+------------------------------------^-----------------------------------+
|
1 connection |
|
+---------------------------+
| binlog replication client |
+---------------------------+
| |
N connections | |
| |
+----------------------------------v--v----------------------------------+
| cache (memcache, redis) |
+------------------------------------------------------------------------+

上图展示了使用binlog复制的整体结构。

好处
1.大幅降低缓存服务上的连接负载。 Web服务器只连接到数据库。
2.顺序一致性,因为我们读取数据库的提交日志进入了缓存服务。
3.可能连接到任何的MySQL复制品中复制链,因为它们都是顺序相一致。

我喜欢卡夫卡,没有什么可反对的,我自己使用它。减少了基础架构,简化了架构并降低了操作复杂性。 通过将MySQL提交日志复制到缓存服务,我们增加了一致性,并在数据库和缓存服务之间获得了严格的部分顺序。

(注:引入EventSourcing + Kafka 会增加提高一致性。)


Improving cache consistency