线上问题排查实例分析|Redis使用不同编码引发的问题

前言

某个周末的晚上突然收到一波耗时上升报警,仔细一看报警消息,原来是出现了慢查请求导致集群耗时大幅上升,此时业务同学也收到上游服务受影响报警。在处理问题过程中,运维同学发现 Redis 集群中只有部分实例出现 cpu 利用率上升,慢查日志也集中在这几个实例,而上游业务此时没有上线或是业务模型变化。因为是少量热 key 访问导致部分 Redis 实例负载高,执行限流对业务有损,执行扩容也无法达到快速止损的目标,幸好业务侧有提前制定的降级预案,快速沟通后采用了业务侧降级方案进行了止损。

问题回顾

虽然故障通过业务侧降级预案及时止损, 但作为 Redis 服务提供方,进行复盘弄清问题根因,制定预案是必须要做的工作,这样才能避免问题再次发生或是发生时更快止损。通过了解业务场景,发现是因为城市A大雨,排队订单上升3倍。咦?这个情景有点熟悉,以前也是这个城市在大雨时出过这个问题,这也是为什么业务侧会有一个降级预案存在。了解背景后,接下来就要接受业务同学们的灵魂拷问了。

  • 为什么昨天同一时间段,该集群 QPS 更高但是没有出现类似问题?

  • 这个集群存储了多个城市的数据,为什么只有城市A这个城市下大雨会出问题,而别的城市没有问题,难道是别的城市不下大雨?或是大家不用排队功能?

答案显然不是,一定还有技术层面的问题没有搞明白,接下来带大家回顾当时我们的排查问题思路。

  • 首先从观察故障期间的一些现象入手,我们发现如下几个现象:
    当时出现了5个热 key,都属于同一个城市A,该城市因为当天晚上大雨导致排队订单量超过平时3倍;

  • 这5个热 key 都是 hash 数据结构,里面存储的每个 key 有400多个元素;

  • 同样的业务逻辑,另外一个城市B,也是5个 hash 表,QPS 比城市A这个城市高,key 中的元素数量也比城市A要高,但是城市B反而并没有出现耗时高的问题;

  • 在出现问题的时刻,这5个热 key 上大部分命令都是 HEXISTS(作用是查看某个 field 是否在 hash 表中),此命令是O(1)复杂度。

此时脑中灵光一现,在查看热 key 的 meta 信息时,发现城市A的这5个 key 使用的内部编码是 ziplist,此时再对比一下城市B的5个 key,发现使用的 hashtable 编码,这个发现让我们似乎看到解决问题的曙光,这是目前发现的城市A和城市B最大的不同之处,后续的分析也证明了这个方向是正确的。

问题剖析

为什么 Redis 的 hash 数据结构会采用两种不同的编码方式,不同编码方式又会带来什么样的不同后果呢?

Redis 为什么内部使用不同编码?
Redis 使用对象来表示数据库中的键和值, 每次当我们在 Redis 的数据库中新创建一个键值对时, 我们至少会创建两个对象, 一个对象用作键值对的键(键对象), 另一个对象用作键值对的值(值对象)。例如:SET mykey "HelloWorld"这个命令会创建两个 Redis 对象,一个字符串对象"mykey"做 key,一个字符串对象"HelloWorld"作为 value。Redis 对象的结构体如下:

typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 指向底层实现数据结构的指针
void *ptr;
// ...
} robj;

在这里插入图片描述
一个 Redis 对象可以有多个类型(type), 如字符串、列表、哈希、集合、有序集合等,同时每种类型也可以设置为不同的编码方式(encoding):int、 hash(ht)、zipmap、ziplist、intset、skiplist、embstr、quicklist、 stream。通过 encoding 属性来设定对象所使用的编码, 而不是为特定类型的对象关联一种固定的编码, 极大地提升了 Redis 的灵活性和效率, 使得 Redis 可以根据不同的使用场景来为一个对象设置不同的编码, 从而优化对象在某一场景下的效率。

两种编码方式的数据结构

编码方式1: ziplist

<zlbytes><zltail><zllen><entry>...<entry><zlend>

在这里插入图片描述

编码方式2: hashtable 编码

在这里插入图片描述
为什么要使用 ziplist 编码?

  • 在哈希对象包含的元素比较少时, Redis 使用压缩列表作为列表对象的底层实现,因为压缩列表比双端链表更节约内存,并且在元素数量较少时,在内存中以连续块方式保存的压缩列表比起双端链表可以更快被载入到 CPU 缓存中;

  • 随着列表对象包含的元素越来越多,使用压缩列表来保存元素性能就会变差,Redis 就会把这个列表对象在底层从压缩列表编码转成 hashtable 编码,hashtable 编码是一个更适合保存大量元素的双端链表。

命令执行时对两种编码结构处理逻辑

当执行 hset/hmset 命令时,代码中会去检查哈希的对象编码以及相关条件来判断该采用哪种编码方式,大致流程如下:

1、检查 value 大小
先是在 hashTypeTryConversion() 中检查,当编码是 OBJ_ENCODING_ZIPLIST,并且写入的 value > server.hash_max_ziplist_value(我们线上默认设置为64 Bytes),会执行转码函数 hashTypeConvert()

2、检查 field 的个数
在 hashTypeSet() 中,如果编码是 OBJ_ENCODING_ZIPLIST,并且元素个数 >server.hash_max_ziplist_entries(我们线上默认设置为512个),就会执行转码函数 hashTypeConvert()

3、hashTypeConvert() 函数实现
在 hashTypeConvert()中 会调用 hashTypeConvertZiplist() 函数,先创建一个字典 dict,然后逐个复制 ziplist 中的 field 和 value 并创建对应 Redis 对象后放入 dict 中,在 ziplist 中的数据全部复制并加入 dict 后,释放原来的 ziplist,然后把 key 指向这个 dict,这样这个哈希对象的编码就从 ziplist 转换成了 hashtable。这个过程是阻塞式的,直到 hashTypeConvert 函数执行完毕,hset/hmset 命令才算完成。

源码如下:

void hashTypeConvertZiplist(robj *o, int enc) {serverAssert(o->encoding == OBJ_ENCODING_ZIPLIST);if (enc == OBJ_ENCODING_ZIPLIST) {/* Nothing to do... */} else if (enc == OBJ_ENCODING_HT) {hashTypeIterator *hi;dict *dict;int ret;hi = hashTypeInitIterator(o);dict = dictCreate(&hashDictType, NULL);while (hashTypeNext(hi) != C_ERR) {robj *field, *value;field = hashTypeCurrentObject(hi, OBJ_HASH_KEY);field = tryObjectEncoding(field);value = hashTypeCurrentObject(hi, OBJ_HASH_VALUE);value = tryObjectEncoding(value);ret = dictAdd(dict, field, value);if (ret != DICT_OK) {serverLogHexDump(LL_WARNING,"ziplist with dup elements dump",o->ptr,ziplistBlobLen(o->ptr));serverAssert(ret == DICT_OK);}}hashTypeReleaseIterator(hi);zfree(o->ptr);o->encoding = OBJ_ENCODING_HT;o->ptr = dict;} else {serverPanic("Unknown hash encoding");}
}

两种编码的时间复杂度对比

说完了一些背景知识,下面该进入真正的主题了,为什么城市A这次热 key 问题会造成业务影响,首先我们来看一下哈希对象使用 ziplist 和 hashtable 两种编码不同命令的时间复杂度:

命令ziplist 编码实现方法hashtable 编码的实现方法
HSET首先调用 ziplistPush 函数, 将键推入到压缩列表的表尾, 然后再次调用用ziplistPush函数, 将值推入到压缩列表的表尾。时间复杂度:O(1)调用 dictAdd 函数, 将新节点添加到字典里面。时间复杂度:O(1)
HGET首先调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 然后调用 ziplistNext 函数, 将指针移动到键节点旁边的值节点, 最后返回值节点。时间复杂度:O(N)调用 dictFind 函数, 在字典中查找给定键, 然后调用 dictGetVal 函数, 返回该键所对应的值。时间复杂度:O(1)
HEXISTS调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 如果找到的话说明键值对存在, 没找到的话就说明键值对不存在。时间复杂度:O(N)调用 dictFind 函数, 在字典中查找给定键, 如果找到的话说明键值对存在, 没找到的话就说明键值对不存在。时间复杂度:O(1)
HDEL调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 然后将相应的键节点、 以及键节点旁边的值节点都删除掉。时间复杂度:O(N)调用 dictDelete 函数, 将指定键所对应的键值对从字典中删除掉。时间复杂度:O(1)
HLEN调用 ziplistLen 函数, 取得压缩列表包含节点的总数量, 将这个数量除以 2 , 得出的结果就是压缩列表保存的键值对的数量。时间复杂度:O(1)调用 dictSize 函数, 返回字典包含的键值对数量, 这个数量就是哈希对象包含的键值对数量。时间复杂度:O(1)
HGETALL遍历整个压缩列表, 用 ziplistGet 函数返回所有键和值(都是节点)。时间复杂度:O(N)遍历整个字典, 用 dictGetKey 函数返回字典的键, 用 dictGetVal 函数返回字典的值。时间复杂度:O(N)

注:字典 dict 结构的操作,可以简单认为是O(1),虽然如果 hash 有冲突时会多个 key
放到链表,相应的很多操作就需要遍历这个链表,但是一般这个链表不会太大,跟整个 hash 表大小比较来说可以认为O(1)。

从上面的对比不难看出:HEXISTS 命令使用 hashtable 时间复杂度为O(1),而使用 ziplist 编码为O(N),当出问题时,城市A存储的 hash 表元素为400个(采用 ziplist 编码),而城市B的 hash 表元素个数超过了512个(采用hashtable编码),这就解释了为什么访问城市A的 key 性能更差

测试方案

讲完了理论,接下来就需要用实际测试来证明上面的推论(这里仅针对 HEXISTS 一个命令做性能测试说明)。

测试方法如下:

  • 在同一个 Redis 实例分别生成两个 hash 对象:mykey001 和 mykey002,同样都是500个 filed,每个 value=60B;

  • 当压测 mykey001 时,hash-max-ziplist-entries 参数=512,编码将会使用 ziplist,压测 mykey002 时 hash-max-ziplist-entries 参数=256,因此编码为 hashtable;

  • 每轮发送相同的压力,这里测试时为2万 QPS,命令为 HEXISTS。

测试用例

测试数据: 两轮测试 QPS 是相同的
在这里插入图片描述
测试数据: P99耗时对比
在这里插入图片描述
测试数据: CPU 利用率对比
在这里插入图片描述
测试数据: 哈希对象占用内存对比

编码方式500个元素
ziplist4.3MB
hashtable19.6MB

测试结论

  • hashtable 编码时(HEXISTS命令)的P99耗时下降14%(70us -> 60us)

  • hashtable 编码时(HEXISTS命令)Redis 的 cpu 利用率下降72%(50% vs 14%)

  • hashtable 编码的内存占用是 ziplist 的4.6倍

补充说明:

  • 每个哈希对象存储500个 field,每个 field 长度8个 Byte, 每个 value 是一个整数类型的时间戳

  • 内存占用通过查看 info 中 used_memory_rss 指标统计的。这个指标其实从 /proc/$pid/stat 采集的,应该是最精确的内存占用数据了。单个对象是通过 debug object 查看,这个不太精确。

一个小插曲,之前测试时,使用 debug object 在获取 key 的内存占用量,忽略一个问题,这个命令是复用了 rdbSaveObject 这个函数,在这个函数中计算Redis 对象的长度时,如果系统参数 rdbcompression 设置为 yes,且长度超过20B就进行压缩,就导致了 debug object 输出的 serializedlength 值失真,在3.2.8版本上 ziplist 在 rdbSaveObject 函数中会进行压缩,hashtable 不会做压缩,因此 ziplist 编码时产生的 rdb 文件会小很多。在3.2.8之后的版本针对 hashtable 在 rdbSaveObject 函数中也做了压缩,产生的 rdb 文件大小相差就没那么大,这里只针对该case跑的3.2.8版本做了测试。

# 使用ziplist编码
redis-cli -p 5555 config set hash-max-ziplist-entries 512
OK
redis-cli -p 5555 config set rdbcompression yes
OK
redis-cli -p 5555 debug object mykey002
Value at:0x7fdf72868d40 refcount:1 encoding:ziplist serializedlength:3692 lru:5959713 lru_seconds_idle:35
redis-cli -p 5555 config set rdbcompression no
OK
redis-cli -p 5555 debug object mykey002
Value at:0x7fdf72868d40 refcount:1 encoding:ziplist serializedlength:34585 lru:5959713 lru_seconds_idle:35# 使用ht编码
redis-cli -p 5555 config set hash-max-ziplist-entries 256
OK
redis-cli -p 5555 hset mykey002 mykey000000000000000000000001500 8c1dfc543d72c0826fc0968276932c88af7ed1deebd74d9c95c129fe6371500
(integer) 1
redis-cli -p 5555 config set rdbcompression yes
OK
redis-cli -p 5555 debug object mykey002
Value at:0x7fdf72868d40 refcount:1 encoding:hashtable serializedlength:33651 lru:5959748 lru_seconds_idle:0
redis-cli -p 5555 config set rdbcompression no
OK
redis-cli -p 5555 debug object mykey002
Value at:0x7fdf72868d40 refcount:1 encoding:hashtable serializedlength:33666 lru:5959748 lru_seconds_idle:

优化思路

经过上面的测试我们会发现 ziplist 在成本上会有优势,但是对于某些原来是O(1)的操作会变成O(N),如果追求性能,对成本不关注,或是只需对少数 Key 追求性能,可以从以下方面来进行优化:

1、Redis 服务端优化:
通过调整 hash-max-ziplist-entries 或是 hash-max-ziplist-value 配置项,从而控制哈希对象的编码方式,例如把 hash-max-ziplist-entries 从512改成256,那么超过256个 field 就会自动转换成 hashtable,但是要注意,如果你的哈希对象大部分都是超过256个 field,这么修改有可能会造成内存使用量大幅上升的情况,需要注意你的钱包。

2、业务侧优化方式:
在服务端做参数调整虽然简单,但是需要根据业务实际情况确定参数,且实际使用中元素数量会发生变化,调整参数没有可操作的条件,且在 key 数量多的时候还会造成不必要的成本浪费。如果是只有部分哈希对象需要关注性能,可以考虑在业务层针对单个 key 做优化。

  • 满足 hash-max-ziplist-value 条件
    例如在哈希对象中存入一个特殊 field,让这个 field 的 value 超过64B,这样这个 key 就自动变成 hashtable 编码,但是需要业务能识别这个特殊的 field,避免出现 bug。

  • 满足 hash-max-ziplist-entries 条件
    很多业务的哈希对象是经过一次拆分了,通过取模 hash 的方式拆分到多个哈希对象,也可以通过将拆分的哈希对象减少,从而达到满足单个哈希对象中 field 个数超过512个的条件。

3、业务使用建议:

  • 尽量避免热 key 现象
    我们使用的是 Redis 的 cluster 版本,如果把业务的大部分流量都集中在某个或某几个 key 上,就无法充分发挥分布式集群的作用,压力都集中在个别的 Redis 实例上,从而出现热点瓶颈。

  • 使用 list/hash/set/zset 等数据结构时要注意性能问题
    需要根据自己的单个 key 中的 field 个数,value 大小,以及使用的命令等综合考虑性能和成本,如果需要了解自己的 key 的实际编码个数可以通过命令:object encoding 查看。

结语

通过这次问题分析,我们可以看到 Redis 内部提供的不同编码会带来不同的性能和成本差别,建议大家在使用 Redis 时,也可以多了解自己的访问场景,根据实际情况来做一些调优。同时也提醒我们,时刻保持对问题根因的探究精神,才可以使我们自己的技术和业务能力不断提升。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/286762.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

考研数学|《1800》《1000》《880》《660》怎么选?怎么刷?看这一篇就够了!

25考研选资料&#xff0c;主打一个听人劝&#xff0c;吃饱饭 有很多讲义&#xff0c;比如张宇30讲&#xff0c;汤家凤高等数学讲义&#xff0c;李永乐复习全书&#xff0c;武忠祥高等数学基础篇等等。 然后习题也有很多&#xff0c;比如1000题&#xff0c;1800题&#xff0c;…

ICCV 2023 Oral | 人类语言演化中学习最优图像颜色编码

人类的语言是一种对复杂世界的高度简洁的编码&#xff0c;特别是语言中颜色的概念&#xff0c;成功地将原本极大的色彩空间&#xff08;如256三次方真色彩空间&#xff09;压缩至5到10种颜色。受此启发&#xff0c;来自上海交大&#xff0c;日本理化学研究所&#xff0c;东京大…

vue2 中使用音频

vue2 中使用音频 在 template 页面 写入 audio 标签 <template><div><audio ref"moreAudio" :src"moreAudioSrc"></audio><audio ref"noAudio" :src"noAudioSrc"></audio></div> </t…

百能云板开启高品质铝基PCB线路板定制服务

铝基板是一种具有良好散热功能的金属基覆铜板&#xff0c;一般单面板由三层结构所组成&#xff0c;分别是电路层&#xff08;铜箔&#xff09;、绝缘层和金属基层。用于高端使用的也有设计为双面板&#xff0c;结构为电路层、绝缘层、铝基、绝缘层、电路层。极少数应用为多层板…

iOS开发进阶(九):OC混合开发嵌套H5应用并互相通信

文章目录 一、前言二、嵌套H5应用并实现双方通信2.1 WKWebView 与JS 原生交互2.1.1 H5页面嵌套2.1.2 常用代理方法2.1.3 OC调用JS方法2.1.4 JS调用OC方法 2.2 JSCore 实现原生与H5交互2.2.1 OC调用H5方法并传参2.2.2 H5给OC传参 2.3 UIWebView的基本用法2.3.1 H5页面嵌套2.3.2 …

Linux 理解文件系统、磁盘结构、软硬链接

目录 一、理解磁盘结构 1、磁盘的物理结构 2、硬件层面理解 3、磁盘的具体物理存储结构 4、进行逻辑抽象 5、磁盘文件的管理 6、创建新文件的过程 二、理解文件系统 1、文件的构成 2、为何选择4KB而非512字节作为基本单位? 3、文件系统的组成 数据块&#xff08;Data Blocks&a…

flask_restful规范返回值

使用方法 导入 flask_restful.marshal_with 装饰器 定义一个字典变量来指定需要返回的标准化字段&#xff0c;以及该字段的数据类型 在请求方法中&#xff0c;返回自定义对象的时候&#xff0c; flask_restful 会自动的读 取对象模型上的所有属性。 组装成一个符合标准化参…

WordPress网站已经安装了SSL证书,但浏览器仍然提示不安全

WordPress网站已经安装了SSL证书&#xff0c;但浏览器仍然提示不安全 昨天我们新建了一个WordPress的网站&#xff0c;在已经安装了SSL证书的情况下&#xff0c;访问网站仍然会提示不安全。 我们使用的是Hostease提供的虚拟主机产品&#xff0c;之前从未出过这样的情况&#x…

rust中字符串String常用方法和注意事项

Rust 中通常说的字符串指的是&#xff1a;String 和 &str(字符串字面值、或者叫字符串切片)这两种类型。str是rust中基础字符串类型&#xff0c;String是标准库里面的类型。Rust 中的字符串本质上是&#xff1a;Byte的集合&#xff08;Vec<u8>&#xff09; 基础类型…

javaWeb在线考试系统

一、简介 在线考试系统是现代教育中一项重要的辅助教学工具&#xff0c;它为学生提供了便捷的考试方式&#xff0c;同时也为教师提供了高效的考试管理方式。我设计了一个基于JavaWeb的在线考试系统&#xff0c;该系统包括三个角色&#xff1a;管理员、老师和学生。管理员拥有菜…

特别澄清:关于ChatGPT辅助论文写作的重要说明

“高扬&#xff0c;快&#xff0c;教我用ChatGPT写论文&#xff0c;明天要交稿&#xff01;” “高师傅&#xff0c;ChatGPT如何能生成调查数据&#xff0c;我想直接拿来用。” “高老师&#xff0c;ChatGPT能不能一下子把论文生成出来&#xff0c;不用修改&#xff0c;直接就能…

微信小程序实战:无痛集成腾讯地图服务

在移动互联网时代,地图服务无疑是应用程序中最常见也最实用的功能之一。无论是导航定位、附近搜索还是路线规划,地图服务都能为用户提供极大的便利。在微信小程序开发中,我们可以轻松集成腾讯地图服务,为小程序赋能增值体验。本文将详细介绍如何在微信小程序中集成使用腾讯地图…

代码随想录算法训练营第四十六天|139.单词拆分、56. 携带矿石资源(第八期模拟笔试)

139.单词拆分 刷题https://leetcode.cn/problems/word-break/description/文章讲解https://programmercarl.com/0139.%E5%8D%95%E8%AF%8D%E6%8B%86%E5%88%86.html视频讲解https://www.bilibili.com/video/BV1pd4y147Rh/?vd_sourceaf4853e80f89e28094a5fe1e220d9062 题解&…

【Rust】——提取函数消除重复代码和泛型

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

【Git】日志功能

1. git日志显示 # 显示前3条日志 git log -3# 单行显示 git log --oneline# 图表日志 git log --graph# 显示更改摘要 git log --stat# 显示更改位置 git log --patch 或 git log -p# 查看指定文件的提交历史记录 git log {filename}例子1&#xff1a;单行显示 例子2&#xff…

基于springboot+vue的客户信息管理系统

作者主页&#xff1a;Java码库 主营内容&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app等设计与开发。 收藏点赞不迷路 关注作者有好处 文末获取源码 技术选型 【后端】&#xff1a;Java 【框架】&#xff1a;spring…

C++ primer 第十五章

1.OPP:概述 面向对象程序设计的核心思想是数据抽象、继承和动态绑定。 通过继承联系在一起的类构成一种层次关系&#xff0c;在层次关系的根部的是基类&#xff0c;基类下面的类是派生类 基类负责定义在层次关系中所有类共同拥有的成员&#xff0c;而每个派生类定义各自特有…

原生数据开发软件 TablePlus for mac

一款非常好用的本地原生数据开发软件&#xff1a;TablePlus激活版。 软件下载&#xff1a;TablePlus for mac v3.11.0激活版 这款优秀的数据库编辑工具支持 MySQL、SQL Server、PostgreSQL 等多种数据库&#xff0c;具备备份、恢复、云同步等功能。它可以帮助您轻松编辑数据库中…

新能源汽车充电桩消防安全视频智能可视化监管建设方案

一、方案背景 据应急管理部门统计公布的数据显示&#xff0c;仅2023年第一季度&#xff0c;新能源汽车自燃率就上涨了32%&#xff0c;平均每天就有8辆新能源汽车发生火灾&#xff08;含自燃&#xff09;。在已查明起火原因中&#xff0c;58%源于电池问题&#xff0c;19%源于碰…

【Unity】UI九宫格

什么是九宫格&#xff1f; 顾名思义&#xff0c;九宫格就是指UI切成9个格子&#xff0c;9个格子可以任意拉伸。 1、3、7、9不拉伸。 2、8水平拉伸。 4、6垂直拉伸。 5既可以水平也可以垂直拉伸。 怎么切九宫格&#xff1f; 选中图片&#xff0c;改成Sprite模式&#xff0c;点…