来源:Zohaib Sibte Hassan from Doordash and RedisConf 2020 (redisconf.com/) organized by Redis Labs (redislabs.com)
翻译:Wen Hui
转载:中间件小哥
javascript
- Cache stampede问题:
Cache stampede问题又叫作cache miss storm,是指在高并发场景中,缓存同时失效致使大量请求透过缓存同时访问数据库的问题。
如上图所示:
服务器a,b 访问数据的前两次请求由于redis缓存中的键尚未过时,因此会直接经过缓存获取并返回(如上图绿箭头所示),但当缓存中的键过时后,大量请求会直接访问数据库来获取数据,致使在没有来得及更新缓存的状况下重复进行数据库读请求 (如上图的蓝箭头),从而致使系统比较大的时延。另外,由于缓存须要被应用程序更新,在这种状况下,若是同时有多个并发请求,会重复更新缓存,致使重复的写请求。
前端
针对以上问题,做者提出一下第一种比较简单的解决方案,主要思路是经过在客户端中,经过给每一个键的过时时间引入随机因子来避免大量的客户端请求在同一时间检测到缓存过时并向数据库发送读数据请求。在以前,咱们定义键过时的条件为:
Timestamp+ttl > now()
如今咱们定义一个gap值,表示每一个客户端键最大的提早过时时间,并经过随机化将每一个客户端的提早过时时间映射到 0 到gap之间的一个值, 这样以来,新的过时条件为:
Timestamp+ttl +(rand()*gap)> now()
经过这种方式,因为不一样客户端请求拿到的键过时时间不同,在缓存没有被更新的状况下,能够在必定程度上避免同时有不少请求访问数据库。从而致使比较大的系统延时。
客户端的实例程序以下:
java
另外一种更好的方法是将提早过时时间作一个小的更改,经过取随机函数的对数来将每一个客户端检查的键提早过时时间更均匀的分布在0到gap的区间内(由于随机函数取对数为负值,因此整个提早过时的时间也须要取反),从而得到更好的性能提高(具体的数学证实在Optimal Probabilistic Cache Stampede Prevention https://cseweb.ucsd.edu/~avattani/papers/cache_stampede.pdf 这篇文章中)。node
经过应用以上键的提早过时机制,咱们看到总体的cache miss现象有明显的缓解。git
- Debouncing
Debouncing在这里指的是在较短期内若是有多个相同key的数据读请求,能够合并成一个来处理,并同时等待数据的读请求完成。做者在这里介绍了可使用相似javascript 的promise机制来处理请求,具体的步骤以下:
1) 每一个读请求提供一个L1 Cache Miss函数并返回一个promise,这个promise会去读相应的L2 Cache或数据库(若是L2 Cache也Miss的话)
2) 当多个读请求使用debouncer访问相同Key id时,只有第一个请求会调用L1 Cache Miss函数,并当即返回一个promise。
3) 当剩下的读请求到达而且Promise没有返回时,函数会当即返回第一个读请求L1 Cache Miss函数所返回的promise。
4) 全部读请求都会等待这个Promise完成。
5) 若是当前的Promise完成并返回,接下来的读请求将重复这个过程。
总体流程以下图程序所示:
在Java中,Caffeine Cache()缓存库也用到相似的设计来实现。
做者经过使用benchmark tool进行比较,经过使用debouncing的设计使得系统吞吐量有了较大的提升。以下图所示:
github
- Big Key
Big Key是指包含数据量很大的键,在具体应用中,有以下几个例子:
1) 缓存过的编译前的元数据(例如前端使用的试图,菜单等)
2) 机器学习模型。
3) 消息队列和具体消息。
4) 更多的关于Redis流(stream)的例子。
在这种状况下,咱们能够经过使用数据压缩算法来解决big key的问题。选择压缩算法的时候咱们须要考虑如下几点:
1) 压缩率(compress ratio)
2) 是否轻量,不能耗费过多的资源
3) 稳定性,是否进行过详尽的测试,以及社区支持等。
在选择算法的时候咱们须要平衡上述几点,例如不能为了提升1% 的压缩率而使用额外20%资源。
在比较压缩算法的时候,可使用lzbench(https://github.com/inikep/lzbench)来比较各种压缩算法的性能(https://morotti.github.io/lzbench-web/)。另外压缩算法的性能和具体的数据有直接的关系,因此建议你们本身动手尝试来比较各种压缩算法的性能差别。
具体的例子(doordash):
Chick-Fil-A 的菜单: 64220 bytes(序列化json)
起司公司产品清单: 350333 bytes(序列化json)
即便单独拿出来这些数据进行传输不会有太大问题,但若是有大量相似的公司须要屡次传输,那么对网络和CPU负载是至关高的。
在具体选择压缩算法过程当中,做者比较了LZ4和Snappy,并获得了如下结论:
1) 在平均状况下,LZ4比Snappy的压缩率要高一点,但做者使用本身的数据做比较发现结论正好相反,LZ4 38.54% 和Snappy 39.71%
2) 压缩速率相比二者差很少,LZ4会比Snappy慢一点点。
3) 再解压方面,LZ4比Snappy快得多,在一些测试场景下会有两倍的差距。
经过以上结论做者选择LZ4 做为菜单传输的压缩算法,并进行Redis Benchmark测试,使用压缩算法能够对Redis的读写吞吐量有很大提升,具体以下:
另外整个系统的网络流量使用和系统延时也有比较明显的下降:web
因此做者建议若是使用Redis存储Big Key时,可使用压缩算法来提升系统吞吐量和下降网络负载。redis
- Hot Key
Hot Key(热键)问题指的是在系统中有多个分区(partition),但由于某一个特定的键频繁的被访问,致使全部的请求都会转到某一个特定的分区中,从而致使某个特定分区资源耗尽而其余分区闲置的问题。在一些状况下不能使用L1缓存来解决这个问题,由于在这些场景下你须要不断地从L2 cache或数据库中获取最新的数据。Hot Key问题主要出如今Read Intensive的应用当中。
解决Redis 的 Hot Key问题的一个潜在方案是能够经过主从复制的方式来将读请求分散到多个replica中。以下图:
可是这种设计没有从根本解决hot key的问题,因此咱们设计系统的目标是尽可能使每一个请求都分散到不一样的cluster nodes中,以下图所示:算法
因此做者提出了以下针对Redis Hot key的解决方案,主要是经过Redis特有的Key Hash Tag来实现的。咱们知道, 在Redis集群模式下,Redis会对每一个键使用CRC16 算法并取模来决定这个键写在哪一个Key Slot中,并存入相应的分区,但若是咱们在键的名字中使用大括号{},则只有大括号里面的字符会用来计算键的槽和相应的分区,而不是整个键。举个例子,若是咱们有个键:doordash,在正常状况下redis会使用doordash来计算相应的key slot和分区,但若是咱们有另一个键:{copy:0} doordash,咱们则只会使用copy:0来计算key slot和分区。以此为基础,咱们能够对Hot key作相应的copy以下:数据库
Hot Key doordash如今有三个副本,咱们能够把这三个副本均匀分布在redis cluster中。而后在写入数据的时候同时写入这三个副本到每个分区中,在客户端读取过程当中,经过生成从0-2随机值而后生成特定的副本key,再去相应的分区中读取值。示例程序以下:
在这种方式中相同的键值须要被复制屡次在不一样的分区中,但由于这个键值会被访问屡次,因此这个复制操做也是值得的。Future在redis 6中,可使用RESP3协议和Redis服务器端对客户端缓存的支持,来提升L1缓存的提早逐出时间,并减小使用网络资源。另外,使用proxy可使客户端请求路由变得更直接。第三点做者提到的是redis 6.0中引入了多线程io,能够显著提升cpu利用率和提升系统吞吐量。