vlambda博客
学习文章列表

缓存之王 | Redis最佳实践&开发规范&FAQ

点击上方 蓝色字体 ,选择“ 设为星标
回复”资源“获取更多资源
本文是来自阿里云2021版最新Redis最佳实践指南。文档可以在云栖社区下载。

Redis–从问题说起

(一)Run-to-Completion in a solo thread–Redis最大的问题

event到来后,交由后端单线程执行。等每个event处理完成后,才处理下一个;单线程run-to-completion就是没有dispatcher,没有后端的multi-worker。所以如果慢查询,诸如单机版的keys、lrange、hgetall等拖慢了一次查询,那么后面的请求就会被拖慢。

缓存之王 | Redis最佳实践&开发规范&FAQ

使用Sentinel判活的trick:
  • Ping命令判活:ping命令同样受到慢查询影响,如果引擎被卡住,则ping失败;

  • Duplex Failure:sentinel由于慢查询切备(备变主)再遇到慢查询,Redis将出现OOS。

(二)扩展为集群版,问题可解?

既然一个Redis进程不行,采用分布式方案扩展成集群版可以吗?集群板确实能解决一部分问题,常见的请求是可以分散到不同DB上的。
但是,集群版也还是解决不了单个DB被卡住的问题,因为Redis的key hash规则是按照外面的一层PK来做的,没有按照里面的子key或者是field的来做,如果用户调用了跨分片的命令,如mget,访问到出问题的db,仍会block住,问题还是会存在。

缓存之王 | Redis最佳实践&开发规范&FAQ

(三)Protocol问题–大量客户端与引擎Fully-Meshed问题

采用Redis协议(RESP)的问题:
  • 扩展性太差:基于Question-Answer模式,由于在Question/Answer里面没有对应的Sequence的存在,(如果不做复杂的转换wrapper层)存储引擎端没法match请求和响应,只能采用Run-To-Completion来挂住链接;缓存之王 | Redis最佳实践&开发规范&FAQ

  • C10K的问题:当引擎挂住太多active链接的时候,性能下降太多。测试结果是当有10k active连接时,性能下降30-35%,由于引擎端挂住的链接不能被返回,用户大量报错。缓存之王 | Redis最佳实践&开发规范&FAQ

(四)“Could not get a resource from the pool”

如下图所示,由于Redis执行Run-To-Completion特性,客户端只能采用连接池的方案;Redis协议不支持连接池收敛,是因为Message没有ID,所以Request和Response对不起来。连接池具体运作方式是每次查询,都要先从连接池拿出一根连接来,当服务端返回结果后,再放回连接池。

缓存之王 | Redis最佳实践&开发规范&FAQ

如果用户返回的及时,那么连接池一直保有的连接数并不高,但是一旦服务端未及时返回,客户端又有新的请求,就只能再checkout一根连接。
当Engine层出现慢查询,就会让请求返回的慢,造成的后果是很容易让用户把连接池用光,当应用机器特别多的情况,按每个client 连接池50个max link来算,很容易打到10K链接的限制,导致engine回调速度慢。
当连接池被checkout完,就会爆没有连接的异常:"Could not get a resource from the pool",这是非常常见的错误,有恶性循环的逻辑在里面。比如说服务端返回的慢,连接池的连接就会创建的很快,用户很容易达到1万条,创建的连接越多,性能越差,返回越慢,服务容易血崩。

Redis–不要触碰边界

Redis的边界 --红色区域代表危险 上述罗列的问题,是为了让我们在开发业务的时候,不要触碰Redis的边界。下面从计算、存储、网络三个维度出发,总结了这张图:

缓存之王 | Redis最佳实践&开发规范&FAQ

计算方面:Wildcard、Lua并发、1对N PUBSUB、全局配置/热点,会大量消耗计算资源(高计算消耗);
存储方面:Streaming慢消费、Bigkey等,造成高存储消耗。
网络方面:Keys等扫全表、Huge Batch mget/mset、大Value、Bigkey Range (如hgetall,smembers),造成高网络消耗。
Redis的边界总结:
  • 高并发不等于高吞吐 大 Value 的问题:高速存储并不会有特别大的高吞吐收益,相反会很危险;

  • 数据倾斜和算力倾斜 bigKey 的问题:break掉存储的分配律;热点的问题,本质上是cpu上的分配律不满足;大 Range 的问题:对NoSQL的慢查询和导致的危害没有足够的重视。

  • 存储边界 Lua使用不当造成的成本直线上升;数据倾斜带来的成本飙升,无法有效利用;

  • 对于 Latency 的理解问题(RT高) 存储引擎的 Latency 都是P99 Latency,如:99.99%在1ms以内,99.5%在3ms以内,等;偶发性时延高是必然的。这个根因在于存储引擎内部的复杂性和熵。

Redis–阿里内部开发规约

Redis的使用建议
推荐:
  • 确定场景,是缓存(cache)还是存储型;

  • Cache的使用原则是:“无它也可,有它更强”;

  • 永远不要强依赖Cache,它会丢,也会被淘汰;

  • 优先设计合理的数据结构和逻辑;

  • 设计避免bigKey,就避免了80%的问题;

  • Keyspace能分开,就多申请几个Redis实例;

  • pubsub不适合做消息分发;

  • 尽量避免用lua做事务。

不建议:
  • 我的服务对RT很敏感。>> 低RT能让我的服务运行的更好;

  • 我把存储都公用在一个redis里。>> 区分cache和内存数据库用法,区分应用;

  • 我有一个大排行榜/大集合/大链表/消息队列;我觉得服务能力足够了。>> 尽量拆散,服务能力不够可通过分布式集群版可以打散;

  • 我有一个特别大的Value,存在redis里,访问能好些。>> redis吞吐量有瓶颈。

(一)BigKey–洪水猛兽

BigKey,我们称之为洪水猛兽,据初步统计,80%问题由bigKey导致。如下图所示:集群中有4个分片,每个分片大约有102个key,实际上是均匀分布。图中第三个key叫key301,hash301,中间有一个放了200w的hash,但因为根据hash301打散的这个key是个 bigkey,严重造成数据倾斜。

缓存之王 | Redis最佳实践&开发规范&FAQ

别的key只用了10%或20%的内存,key301用了约80%,而且大概率是热点。上图的使用用法,有可能造成有一个分片内存满了,访问出了问题,但是其他分片却用的很闲。问题分片的访问比较热,造成网卡打满,或者CPU打满,导致限流,服务可能就夯住了。

(二)Redis LUA JIT

下面的示意图表示了一次脚本的执行过程,客户端调用EVAL script之后会产生SCRIPT Load的行为,Lua JIT开始编译生成字节码,这时产生一个SHA字符串,表示 bytecode的缓存。Loading bytecode之后,开始执行脚本,还需要保证在副本上执行成功,最后unload和Cleaning,整个过程结束。

缓存之王 | Redis最佳实践&开发规范&FAQ

示意图中有3个火形图标,表示耗费CPU的程度,脚本的compile-load-run-unload非常耗费CPU。整个lua相当于把复杂事务推送到Redis中执行,如果稍有不慎CPU会爆,引擎算力耗光后挂住redis。对上述的情况,Redis做了一些优化,比如“Script + EVALSHA”,可以先把脚本在redis中预编译和加载(不会unload和clean),使用EVALSHA执行,会比纯EVAL省CPU,但是Redis重启/切换/变配bytecode cache会失效,需要reload,仍是缺陷方案。建议使用复杂数据结构,或者module来取代lua。
  • 对于JIT技术在存储引擎中而言,“EVAL is evil”,尽量避免使用lua耗费内存和计算资源(省事不省心);

  • 某些SDK(如Redisson)很多高级实现都内置使用lua,开发者可能莫名走入CPU运算风暴中,须谨慎。

(三)Pubsub/Transaction/Pipeline

Pubsub的典型场景 Pubsub适合悲观锁和简单信号,不适合稳定的更新,因为可能会丢消息。在1对N的消息转发通道中,服务瓶颈。还有模糊通知方面,算力瓶颈。在channel和client比较多的情况下,造成CPU打满、服务夯住。Transaction Transaction是一种伪事物,没有回滚条件;集群版需要所有key使用hashtag保证,代码比较复杂,hashtag也可能导致算力和存储倾斜;Lua中封装了multi-exec,但更耗费CPU,比如编译、加载时,经常出现问题。
Pipeline
Pipeline用的比较多,如下面的示意图,实际上是把多个请求封装在一个请求中,合并在一个请求里发送,服务端一次性返回,能够有效减少IO,提高执行效率。需要注意的是,用户需要聚合小的命令,避免在pipeline里做大range。注意Pipeline中的批量任务不是原子执行的(从来不是),所以要处理Pipeline其中部分命令失败的场景。

缓存之王 | Redis最佳实践&开发规范&FAQ

(四)KEYS 命令

KEYS命令,一定会出问题,即使当前没有,客户数据量上涨后必然引发慢查,出现后无能为力。这种情况,需要在一开始就提前预防,可以在控制台通过危险命令禁用,禁止掉keys命令,出现时也可以使用一些手段优化。KEYS命令的模糊匹配:
  • Redis存储key是无序的,匹配时必然全表扫描, key数目一多必然卡住,所以一定要去优化。如下图所例子中所示,修改为hash结构:

  • 可以从全表扫描变为点查/部分range, 虽然hash结构中field太多也会慢,但比keys性能提升一个到两个数量级。

这个例子里面,Product1前缀可以提取成为hash的KEY,如果要去product1前缀的所有东西,其实可以下发一个HGETALL,这样的就是优化了。

缓存之王 | Redis最佳实践&开发规范&FAQ

(五)除去KEYS,下面命令依然危险

  • hgetall,smembers,lrange,zrange,exhgetall 直接与数据结构的subkey(field)多少相关,O(n),携带value爆网卡。建议使用scan来替代。

  • bitop,bitset 设置过远的bit会直接导致OOM。

  • flushall,flushdb 数据丢失。

用户在操作的时候,需要很小心,因为会清空数据库。在阿里云Redis控制台里面点清除数据时,需要使用二次校验,避免随意清除数据。另外还可以单独清理过期数据,对其他正常访问的数据没有影响。
  • 配置中和ziplist相关的参数 Redis在存储相关数据结构时,数据量比较小,底层使用了ziplist结构,达到一定的量级,比如key/field变多了,会转换数据结构。当结构在ziplist结构体下时,算力开销变大,部分查询变O(n)级别,匹配变O(m*n),极端情况容易打满CPU,不过占用的内存确实变少了(需要评估带来的收益是否匹配付出的代价?)。

建议用户尽量使用默认参数。
规范总结 [ Just FYI ] 1.选型:用户需要确定场景是cache还是内存数据库使用
  • Cache场景,关闭AOF;内存数据库选择双副本

  • 如果keyspace能够分开,就申请不同的实例来隔离

2.使用:避免触发高速存储的边界
  • set/hash/zset/list/tairhash/bloom/gis等大key(内部叫做godkey)不要超过3000,会记录sillylog

  • 避免使用keys,hgetall,lrange0-1等大range(使用scan替代)

  • 避免使用大value(10k以上就算大value,50k会记录)

3.SDK:使用规范
  • 严禁设置低读超时和紧密重试(建议设置200ms以下read timeout)

  • 需要接受P99时延,对超时和慢做容错处理

  • 尽量使用扩展数据结构,避免使用lua

  • 尽量避免pubsub和blocking的API

4.接受主动运维
  • 在阿里云上,如果机器宕机,或者是机器后面有风险,我们会做主动运维保证服务的稳定性。

Redis–常见问题处理

(一)Tair/Redis内存模型

内存控制是Redis的精华部分,大部分遇到的问题都是跟内存有,Tair/Redis内存模型,如下图所示,总内存分为3个部分:链路内存(动态)、数据内存、管理内存(静态)。
  • 链路内存(动态):主要包括Input buff、Output buff等,Input buff与Output buff跟每个客户端的连接有关系,正常情况下比较小,但是当Range操作的时候,或者有大key收发比较慢的时候,这两个区的内存会增大,影响数据区,甚至会造成OOM。还包括JIT Overhead、Fake Lua Link,包含了Code cache执行缓存等等。

  • 数据内存:用户数据区,就是用户实际存储的value。

  • 管理内存(静态):是静态buff,启动的时候比较小,比较恒定。这个区域主要管理data的hash开销,当key非常多的时候,比如几千万、几个亿,会占用非常大的内存。还包括Repl-buff、aof-buff(32M~64M)通常来说比较小。

缓存之王 | Redis最佳实践&开发规范&FAQ

OOM场景,大都是动态内存管理失效,例如限流的影响(plus timer mem),限流的时候请求出不去,导致请求堆积后动态内存极速飙升,造成OOM;无所畏惧的Lua脚本也有可能造成OOM。原生的Redis被定义为“缓存”,在动态内存上控制比较粗糙。Tair对这部分做了加强,致力于footprint control,售卖内存接近User Dataset。

(二)缓存分析–内存分布统计、bigKey,key pattern

对于内存,阿里云有现成的功能一键分析,使用入口在“实例管理”-》“CloudDBA”下面的“缓存分析”,热Key分析无需主动触发。数据源支持历史备份集,现有备份集,可以准实时或者对历史备份做分析。支持线上所有的社区版和企业版。也支持线上所有的架构,包括标准版、读写分离版、集群版。
使用入口
  • “实例管理”-->“CloudDBA”-->“缓存分析”-->“立即分析”;热Key分析无需主动触发。

数据源
  • 支持已有备份集;

  • 支持自动新建备份集。

支持版本
  • 社区版(2.8~6.0);

  • 企业版(Tair)。

支持架构
  • 标准版;

  • 读写分离版;

  • 集群版。

下图所示,是阿里云控制台使用截图,这个功能比较常用,已开放OpenApi,可被集成。

缓存之王 | Redis最佳实践&开发规范&FAQ

下图所示,是缓存分析报告,可以看到每一个DB内存分布统计,包括不同类型的数据结构内存统计,key对应的元素数分级统计,可以统计到总体上大概有多少个大key;统计 key过期时间分布,可以发现过期时间设置的是否合理。Top 100 BigKey(按内存),可以发现具体有哪些大key,业务上可以参照这个做优化。Top 100 BigKey前缀是做了key pattern统计,如果key是按照业务模块来制定的前缀,可以统计到各个业务上用了多少内存,也可以大体上指导业务优化。

缓存之王 | Redis最佳实践&开发规范&FAQ

(三)热Key分析

阿里云提供了在线和离线两种热Key分析方式:在线实时分析热key
  • 使用入口:“实例管理” --> “CloudDBA” --> “缓存分析” -->“HotKey”;

  • 使用须知:Tair版,或Redis版本>=redis4.0;

  • 精确统计(非采样),能抓出当前所有 Per Key QPS > 3000的记录;

离线分析热key
  • 方法1:缓存分析也可以分析出相对较热的key,通过工具实现;

  • 方法2:最佳实践,imonitor命令 + redis-faina 分析出热点Key;缓存之王 | Redis最佳实践&开发规范&FAQ

    (四)Tair/Redis全链路诊断

Tair/Redis全链路诊断,从“APP端的SDK”到“网络”到“VIP”到“ Proxy”再到“DB”,每个部分都有可能会出问题。问题排查包括:前端排查和后端排查。前段排查首先需要确定是一台出问题,还是全部有问题,如果是一台出问题,大概率是客户端自己的问题,包括:
  • ECS

  • Load,内存等;

  • PPS限制。

  • 客户端

  • 链接池满;

  • RT高(跨地域,gc等);

  • 建链接慢(K8s DNS等);

  • 大查询,发快收慢。

  • 网络

  • 丢包,收敛;

  • 运营商网络抖动。

后端排查:主要是慢查和CPU排查,包括“VIP”、“ Proxy”、“DB”。Tair/Redis 80%的问题是RT(latency)相关。
  • VIP(SLB/NGLB)

  • 建链接瓶颈(极少);

  • 流量不均衡(少);

  • 流量瓶颈(极少)。

  • Proxy

  • 分发慢查;

  • 流量高(扩容proxy);

  • 消息堆积;

  • Backend网络抖动。

  • DB

  • 容量,CPU,流量(见前文);

  • 主机故障,HA速度;

  • 慢查询。缓存之王 | Redis最佳实践&开发规范&FAQ

    (五)Tair/Redis诊断报告

对于全链路诊断,我们推出了诊断报告功能,可以对某个时间段发起“一键诊断”,这里主要是后端排查,目前都是“DB”相关,可以看到有哪些异常情况发生。如下图所示:
核心曲线:核心指标的曲线,可以看哪些时间点,哪些节点有峰值。
慢请求:展示了Top 10节点的Top 10慢命令统计;
性能水位:可以看到哪些指标、哪些节点超过了预设水位,或者是这些节点是不是发生了倾斜,对发现问题有很大的帮助。
诊断:准实时的对过去最近半小时,1小时,或者对过去某一天、某几天的诊断。

缓存之王 | Redis最佳实践&开发规范&FAQ

(六)Tair/Redis慢日志

设置合理的Proxy和DB慢日志采集参数
  • slowlog-log-slower-than:DB分片上慢日志阈值,不可设置过低!;

  • slowlog-max-len:DB分片slowlog链表最大保持长度;

  • rt_threshold_ms:Proxy上慢日志阈值,不可设置过低!。缓存之王 | Redis最佳实践&开发规范&FAQ

以上建议使用默认的参数,不要设置过小,因为如果这些阈值设置的过小,那么DB在采集慢日志的时候会频繁记录,可能造成引擎的性能降低,所以尽量使用默认参数。
慢日志查询功能分为历史慢日志和实时慢日志,入口也不相同,区别在于历史慢日志可获取近72小时内的慢日志。实时慢日志能抓出当前所有分片slowlog,但是有一个局限性,如果节点发生了HA或者手动清理慢日志,这部分慢日志就没有了。使用入口如下图所示:
历史慢日志
  • 使用入口:“实例管理” --> “日志管理” --> “慢日志”;

  • 使用须知:Tair版,或Redis版本>=redis4.0,具体查看帮助文档;

  • 可获取近72小时内的慢日志。

实时慢日志
  • 使用入口:“实例管理” --> “CloudDBA” --> “慢请求”;

  • 实时获取,能抓出当前所有分片slowlog。缓存之王 | Redis最佳实践&开发规范&FAQ