- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
最近碰到一个 case,一个 Redis 实例的内存突增,used_memory最大时达到了 78.9G,而该实例的maxmemory配置却只有 16G,最终导致实例中的数据被大量驱逐.
以下是问题发生时INFO MEMORY的部分输出内容.
# Memory
used_memory:84716542624
used_memory_human:78.90G
used_memory_rss:104497676288
used_memory_rss_human:97.32G
used_memory_peak:84716542624
used_memory_peak_human:78.90G
used_memory_peak_perc:100.00%
used_memory_overhead:75682545624
used_memory_startup:906952
used_memory_dataset:9033997000
used_memory_dataset_perc:10.66%
allocator_allocated:84715102264
allocator_active:101370822656
allocator_resident:102303637504
total_system_memory:810745470976
total_system_memory_human:755.07G
used_memory_lua:142336
used_memory_lua_human:139.00K
used_memory_scripts:6576
used_memory_scripts_human:6.42K
number_of_cached_scripts:13
maxmemory:17179869184
maxmemory_human:16.00G
maxmemory_policy:volatile-lru
allocator_frag_ratio:1.20
allocator_frag_bytes:16655720392
内存突增导致数据被驱逐,是 Redis 中一个较为常见的问题。很多童鞋在面对这类问题时往往缺乏清晰的分析思路,常常误以为是复制、RDB 持久化等操作引起的。接下来,我们看看如何系统地分析这类问题.
本文主要包括以下几部分:
used_memory
是怎么来的?used_memory
?used_memory
内存通常会被用于哪些场景?used_memory
超过maxmemory
后,是否一定会触发驱逐?used_memory
增长时,具体是哪一部分的内存消耗导致的。当我们执行INFO命令时,Redis 会调用genRedisInfoString函数来生成其输出.
// server.c
sds genRedisInfoString(const char *section) {
...
/* Memory */
if (allsections || defsections || !strcasecmp(section,"memory")) {
...
size_t zmalloc_used = zmalloc_used_memory();
...
if (sections++) info = sdscat(info,"\r\n");
info = sdscatprintf(info,
"# Memory\r\n"
"used_memory:%zu\r\n"
"used_memory_human:%s\r\n"
"used_memory_rss:%zu\r\n"
...
"lazyfreed_objects:%zu\r\n",
zmalloc_used,
hmem,
server.cron_malloc_stats.process_rss,
...
lazyfreeGetFreedObjectsCount()
);
freeMemoryOverheadData(mh);
}
...
return info;
}
可以看到,used_memory 的值来自 zmalloc_used,而 zmalloc_used 又是通过zmalloc_used_memory()函数获取的.
// zmalloc.c
size_t zmalloc_used_memory(void) {
size_t um;
atomicGet(used_memory,um);
return um;
}
zmalloc_used_memory() 的实现很简单,就是以原子方式读取 used_memory 的值.
used_memory是一个静态变量,其类型为redisAtomic size_t,其中redisAtomic是_Atomic类型的别名。_Atomic是 C11 标准引入的关键字,用于声明原子类型,保证在多线程环境中对该类型的操作是原子的,避免数据竞争.
#define redisAtomic _Atomic
static redisAtomic size_t used_memory = 0;
used_memory 的更新主要通过两个宏定义实现:
#define update_zmalloc_stat_alloc(__n) atomicIncr(used_memory,(__n))
#define update_zmalloc_stat_free(__n) atomicDecr(used_memory,(__n))
其中,update_zmalloc_stat_alloc(__n)是在分配内存时调用,它通过原子操作让 used_memory 加__n.
而update_zmalloc_stat_free(__n)则是在释放内存时调用,它通过原子操作让 used_memory 减__n.
这两个宏确保了在内存分配和释放过程中used_memory的准确更新,并且避免了并发操作带来的数据竞争问题.
在通过内存分配器(常用的内存分配器有 glibc's malloc、jemalloc、tcmalloc,Redis 中一般使用 jemalloc)中的函数分配或释放内存时,会同步调用update_zmalloc_stat_alloc或update_zmalloc_stat_free来更新 used_memory 的值.
在 Redis 中,内存管理主要通过以下两个函数来实现:
// zmalloc.c
void *ztrymalloc_usable(size_t size, size_t *usable) {
ASSERT_NO_SIZE_OVERFLOW(size);
void *ptr = malloc(MALLOC_MIN_SIZE(size)+PREFIX_SIZE);
if (!ptr) return NULL;
#ifdef HAVE_MALLOC_SIZE
size = zmalloc_size(ptr);
update_zmalloc_stat_alloc(size);
if (usable) *usable = size;
return ptr;
#else
...
#endif
}
void zfree(void *ptr) {
...
if (ptr == NULL) return;
#ifdef HAVE_MALLOC_SIZE
update_zmalloc_stat_free(zmalloc_size(ptr));
free(ptr);
#else
...
#endif
}
其中, 。
ztrymalloc_usable
函数用于分配内存。该函数首先会调用malloc
分配内存。如果分配成功,则会通过 update_zmalloc_stat_alloc
更新 used_memory 的值。zfree
函数用于释放内存。在释放内存之前,先通过update_zmalloc_stat_free
调整 used_memory 的值,然后再调用free
释放内存。这种机制保证了 Redis 能够准确跟踪内存的分配和释放情况,从而有效地管理内存使用.
used_memory主要由两部分组成:
used_memory_dataset
。used_memory_overhead
。需要注意的是,used_memory_dataset 并不是根据 Key 的数量及 Key 使用的内存计算出来的,而是通过 used_memory 减去 used_memory_overhead 得到的.
接下来,我们重点分析下used_memory_overhead 的来源。实际上,Redis 提供了一个单独的函数-getMemoryOverheadData,专门用于计算这一部分的内存开销.
// object.c
struct redisMemOverhead *getMemoryOverheadData(void) {
int j;
// mem_total 用于累积总的内存开销,最后会赋值给 used_memory_overhead。
size_t mem_total = 0;
// mem 用于计算每一部分的内存使用量。
size_t mem = 0;
// 调用 zmalloc_used_memory() 获取 used_memory。
size_t zmalloc_used = zmalloc_used_memory();
// 使用 zcalloc 分配一个 redisMemOverhead 结构体的内存。
struct redisMemOverhead *mh = zcalloc(sizeof(*mh));
...
// 将 Redis 启动时的内存使用量 server.initial_memory_usage 加入到总内存开销中。
mem_total += server.initial_memory_usage;
mem = 0;
// 将复制积压缓冲区的内存开销加入到总内存开销中。
if (server.repl_backlog)
mem += zmalloc_size(server.repl_backlog);
mh->repl_backlog = mem;
mem_total += mem;
/* Computing the memory used by the clients would be O(N) if done
* here online. We use our values computed incrementally by
* clientsCronTrackClientsMemUsage(). */
// 计算客户端内存开销
mh->clients_slaves = server.stat_clients_type_memory[CLIENT_TYPE_SLAVE];
mh->clients_normal = server.stat_clients_type_memory[CLIENT_TYPE_MASTER]+
server.stat_clients_type_memory[CLIENT_TYPE_PUBSUB]+
server.stat_clients_type_memory[CLIENT_TYPE_NORMAL];
mem_total += mh->clients_slaves;
mem_total += mh->clients_normal;
// 计算 AOF 缓冲区和 AOF Rewrite Buffer 的内存开销
mem = 0;
if (server.aof_state != AOF_OFF) {
mem += sdsZmallocSize(server.aof_buf);
mem += aofRewriteBufferSize();
}
mh->aof_buffer = mem;
mem_total+=mem;
// 计算 Lua 脚本缓存的内存开销
mem = server.lua_scripts_mem;
mem += dictSize(server.lua_scripts) * sizeof(dictEntry) +
dictSlots(server.lua_scripts) * sizeof(dictEntry*);
mem += dictSize(server.repl_scriptcache_dict) * sizeof(dictEntry) +
dictSlots(server.repl_scriptcache_dict) * sizeof(dictEntry*);
if (listLength(server.repl_scriptcache_fifo) > 0) {
mem += listLength(server.repl_scriptcache_fifo) * (sizeof(listNode) +
sdsZmallocSize(listNodeValue(listFirst(server.repl_scriptcache_fifo))));
}
mh->lua_caches = mem;
mem_total+=mem;
// 计算数据库的内存开销:遍历所有数据库 (server.dbnum)。对于每个数据库,计算主字典 (db->dict) 和过期字典 (db->expires) 的内存开销。
for (j = 0; j < server.dbnum; j++) {
redisDb *db = server.db+j;
long long keyscount = dictSize(db->dict);
if (keyscount==0) continue;
mh->total_keys += keyscount;
mh->db = zrealloc(mh->db,sizeof(mh->db[0])*(mh->num_dbs+1));
mh->db[mh->num_dbs].dbid = j;
mem = dictSize(db->dict) * sizeof(dictEntry) +
dictSlots(db->dict) * sizeof(dictEntry*) +
dictSize(db->dict) * sizeof(robj);
mh->db[mh->num_dbs].overhead_ht_main = mem;
mem_total+=mem;
mem = dictSize(db->expires) * sizeof(dictEntry) +
dictSlots(db->expires) * sizeof(dictEntry*);
mh->db[mh->num_dbs].overhead_ht_expires = mem;
mem_total+=mem;
mh->num_dbs++;
}
// 将计算的 mem_total 赋值给 mh->overhead_total。
mh->overhead_total = mem_total;
// 计算数据的内存开销 (zmalloc_used - mem_total) 并存储在 mh->dataset。
mh->dataset = zmalloc_used - mem_total;
mh->peak_perc = (float)zmalloc_used*100/mh->peak_allocated;
/* Metrics computed after subtracting the startup memory from
* the total memory. */
size_t net_usage = 1;
if (zmalloc_used > mh->startup_allocated)
net_usage = zmalloc_used - mh->startup_allocated;
mh->dataset_perc = (float)mh->dataset*100/net_usage;
mh->bytes_per_key = mh->total_keys ? (net_usage / mh->total_keys) : 0;
return mh;
}
基于上面代码的分析,可以知道 used_memory_overhead 由以下几部分组成:
server.initial_memory_usage:Redis 启动时的内存使用量,对应 INFO 中used_memory_startup.
mh->repl_backlog:复制积压缓冲区的内存开销,对应 INFO 中的mem_replication_backlog.
mh->clients_slaves:从库的内存开销。对应 INFO 中的mem_clients_slaves.
mh->clients_normal:其它客户端的内存开销,对应 INFO 中的mem_clients_normal.
mh->aof_buffer:AOF 缓冲区和 AOF 重写缓冲区的内存开销,对应 INFO 中的mem_aof_buffer。AOF 缓冲区是数据写入 AOF 之前使用的缓冲区。AOF 重写缓冲区是 AOF 重写期间,用于存放新增数据的缓冲区.
mh->lua_caches:Lua 脚本缓存的内存开销,对应 INFO 中的used_memory_scripts。Redis 5.0 新增的.
字典的内存开销,这部分内存在 INFO 中没有显示,需要通过MEMORY STATS查看.
17) "db.0"
18) 1) "overhead.hashtable.main"
2) (integer) 2536870912
3) "overhead.hashtable.expires"
4) (integer) 0
在这些内存开销中,used_memory_startup 基本不变,mem_replication_backlog 受 repl-backlog-size 的限制,used_memory_scripts 开销一般不大,而字典的内存开销则与数据量的大小成正比.
所以,重点需要注意的主要有三项:mem_clients_slaves,mem_clients_normal 和mem_aof_buffer.
client-query-buffer-limit
限制。client-output-buffer-limit
控制。如果数据超过软硬限制并持续一段时间,客户端会被关闭。在 Redis 7 中,还会统计以下项的内存开销:
mem_cluster_links
。used_memory_functions
。overhead.hashtable.slot-to-keys
。此外,Redis 7 引入了 Multi-Part AOF,这个特性移除了 AOF 重写缓冲区.
需要注意的是,mh->repl_backlog 和 mh->clients_slaves 的内存计算方式也发生了变化.
在 Redis 7 之前,mh->repl_backlog 统计的是复制积压缓冲区的大小,mh->clients_slaves 统计的是所有从节点客户端的内存使用量.
if (server.repl_backlog)
mem += zmalloc_size(server.repl_backlog);
mh->repl_backlog = mem;
mem_total += mem;
mem = 0;
// 遍历所有从节点客户端,累加它们的输出缓冲区、输入缓冲区的内存使用量以及客户端对象本身的内存占用。
if (listLength(server.slaves)) {
listIter li;
listNode *ln;
listRewind(server.slaves,&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
mem += getClientOutputBufferMemoryUsage(c);
mem += sdsAllocSize(c->querybuf);
mem += sizeof(client);
}
}
mh->clients_slaves = mem;
因为每个从节点都会分配一个独立的复制缓冲区(即从节点对应客户端的输出缓冲区),所以当从节点的数量增加时,这种实现方式会造成内存的浪费。不仅如此,当client-output-buffer-limit设置过大且从节点数量过多时,还容易导致主节点 OOM.
针对这个问题,Redis 7 引入了一个全局复制缓冲区。无论是复制积压缓冲区(repl-backlog),还是从节点的复制缓冲区都是共享这个缓冲区.
replBufBlock结构体用于存储全局复制缓冲区的一个块.
typedef struct replBufBlock {
int refcount; /* Number of replicas or repl backlog using. */
long long id; /* The unique incremental number. */
long long repl_offset; /* Start replication offset of the block. */
size_t size, used;
char buf[];
} replBufBlock;
每个replBufBlock包含一个refcount字段,用于记录该块被多少个复制实例(包括主节点的复制积压缓冲区和从节点)所引用.
当新的从节点添加时,Redis 不会为其分配新的复制缓冲区块,而是增加现有replBufBlock的refcount.
相应地,在 Redis 7 中,mh->repl_backlog 和 mh->clients_slaves 的内存计算方式也发生了变化.
if (listLength(server.slaves) &&
(long long)server.repl_buffer_mem > server.repl_backlog_size)
{
mh->clients_slaves = server.repl_buffer_mem - server.repl_backlog_size;
mh->repl_backlog = server.repl_backlog_size;
} else {
mh->clients_slaves = 0;
mh->repl_backlog = server.repl_buffer_mem;
}
if (server.repl_backlog) {
/* The approximate memory of rax tree for indexed blocks. */
mh->repl_backlog +=
server.repl_backlog->blocks_index->numnodes * sizeof(raxNode) +
raxSize(server.repl_backlog->blocks_index) * sizeof(void*);
}
mem_total += mh->repl_backlog;
mem_total += mh->clients_slaves;
具体而言,如果全局复制缓冲区的大小大于repl-backlog-size,则复制积压缓冲区(mh->repl_backlog)的大小取 repl-backlog-size,剩余部分视为从库使用的内存(mh->clients_slaves)。如果全局复制缓冲区的大小小于等于 repl-backlog-size,则直接取全局复制缓冲区的大小.
此外,由于引入了一个 Rax 树来索引全局复制缓冲区中的部分节点,复制积压缓冲区还需要计算 Rax 树的内存开销.
很多人有个误区,认为只要 used_memory 大于 maxmemory ,就会触发数据的驱逐。但实际上不是.
数据被驱逐需满足以下条件:
其中,mem_not_counted_for_evict的值可以通过 INFO 命令获取,它的大小是在freeMemoryGetNotCountedMemory函数中计算的.
size_t freeMemoryGetNotCountedMemory(void) {
size_t overhead = 0;
int slaves = listLength(server.slaves);
if (slaves) {
listIter li;
listNode *ln;
listRewind(server.slaves,&li);
while((ln = listNext(&li))) {
client *slave = listNodeValue(ln);
overhead += getClientOutputBufferMemoryUsage(slave);
}
}
if (server.aof_state != AOF_OFF) {
overhead += sdsalloc(server.aof_buf)+aofRewriteBufferSize();
}
return overhead;
}
freeMemoryGetNotCountedMemory函数统计了所有从节点的复制缓存区、AOF 缓存区和 AOF 重写缓冲区的总大小.
所以,在 Redis 判断是否需要驱逐数据时,会从used_memory中剔除从节点复制缓存区、AOF 缓存区以及 AOF 重写缓冲区的内存占用.
最后,分享一个脚本.
这个脚本能够帮助我们快速分析 Redis 的内存使用情况。通过输出结果,我们可以直观地查看 Redis 各个部分的内存消耗情况并识别当 used_memory 增加时,具体是哪一部分的内存消耗导致的.
脚本地址:https://github.com/slowtech/dba-toolkit/blob/master/redis/redis_mem_usage_analyzer.py 。
# python3 redis_mem_usage_analyzer.py -host 10.0.1.182 -p 6379
Metric(2024-09-12 04:52:42) Old Value New Value(+3s) Change per second
==========================================================================================
Summary
---------------------------------------------
used_memory 16.43G 16.44G 1.1M
used_memory_dataset 11.93G 11.93G 22.66K
used_memory_overhead 4.51G 4.51G 1.08M
Overhead(Total) 4.51G 4.51G 1.08M
---------------------------------------------
mem_clients_normal 440.57K 440.52K -18.67B
mem_clients_slaves 458.41M 461.63M 1.08M
mem_replication_backlog 160M 160M 0B
mem_aof_buffer 0B 0B 0B
used_memory_startup 793.17K 793.17K 0B
used_memory_scripts 0B 0B 0B
mem_hashtable 3.9G 3.9G 0B
Evict & Fragmentation
---------------------------------------------
maxmemory 20G 20G 0B
mem_not_counted_for_evict 458.45M 461.73M 1.1M
mem_counted_for_evict 15.99G 15.99G 2.62K
maxmemory_policy volatile-lru volatile-lru
used_memory_peak 16.43G 16.44G 1.1M
used_memory_rss 16.77G 16.77G 1.32M
mem_fragmentation_bytes 345.07M 345.75M 232.88K
Others
---------------------------------------------
keys 77860000 77860000 0.0
instantaneous_ops_per_sec 8339 8435
lazyfree_pending_objects 0 0 0.0
该脚本每隔一段时间(由 -i 参数决定,默认是 3 秒)采集一次 Redis 的内存数据。然后,它会将当前采集到的数据(New Value)与上一次的数据(Old Value)进行对比,计算出每秒的增量(Change per second).
输出主要分为四大部分:
mem_counted_for_evict
超过 maxmemory
时,才会触发数据驱逐。如果发现mem_clients_normal或mem_clients_slaves比较大,可指定 --client 查看客户端的内存使用情况.
# python3 redis_mem_usage_analyzer.py -host 10.0.1.182 -p 6379 --client
ID Address Name Age Command User Qbuf Omem Total Memory
----------------------------------------------------------------------------------------------------
216 10.0.1.75:37811 721 psync default 0B 232.83M 232.85M
217 10.0.1.22:35057 715 psync default 0B 232.11M 232.13M
453 10.0.0.198:51172 0 client default 26B 0B 60.03K
...
其中, 。
结果按 Total Memory 从大到小的顺序输出.
最后此篇关于Redis内存突增时,如何定量分析其内存使用情况的文章就讲到这里了,如果你想了解更多关于Redis内存突增时,如何定量分析其内存使用情况的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我正在创建一个 sql server 存储过程,它将输入作为逗号分隔的 productid 或选项“全部”。当用户传入逗号分隔的产品 ID 时,查询应输出所有产品 ID 的数据。我使用“IN”语句执行
我有一个自动生成的 Web 服务客户端。我有很多复杂的类,我必须对其进行模式匹配。现在我的结构如下所示: val response = client.getResponse response matc
关闭。这个问题是opinion-based .它目前不接受答案。 想改进这个问题?更新问题,以便 editing this post 可以用事实和引用来回答它. 7年前关闭。 Improve this
我需要正确的 tsql 语法来解决这个问题: Select * from table where var_A='10' select * from table where var_B='10' 何时使
我遇到了这个问题。每当我运行程序并在需要时键入字母 m 时,我的 if 语句都不会识别它。有人知道为什么吗?我已经这样做了一个小时,但没有结果。 #include #include #includ
我从数据库列名称“你有护照”创建了一个表,用户回答是或否我如何将 css 应用到这个动态工作的表。 table, th, td { border: 1px solid black;
我对 LocationListener 类的 onStatusChanged 有一些疑问。 它知道它可以呈现三种状态:AVAILABLE、TEMPORARILY_UNAVAILABLE 和 OUT_O
当引入新的异常类型时,我总是不确定如何正确地做到这一点。有共同约定吗?你怎么做呢? 我对您组织它们的范围感兴趣(将它们保留在它们所使用的单元中?在组件级别有一个单元?包级别?应用程序?) 这也会影响命
我使用以下内容创建了日期维度: https://www.codeproject.com/Articles/647950/Create-and-Populate-Date-Dimension-for-D
您好,我正在使用 Android 完全 Kiosk 浏览器,该浏览器使用 chrome Webview。但是 javascript 中的某些方法或函数无法正常工作,例如 window.print()。
我有以下代码: public void OpenFile(string FileName) { if (FileName == null)
获取索引越界异常 for (int recordData = 0; recordData < recordDataList.size(); recordData++) {
我使用它在发生错误时在登录中显示一条消息: × Invalid user or password
这是我的场景,我有一个异常列表,其中包含来自不同层次结构的任意异常,下面的代码快照将解释我需要做什么 private List connectionExceptions; try { // tryin
我尝试动态更新 Jtextpane 中的左缩进。但我不能!这是我尝试过的! DefaultStyledDocument document = (DefaultStyledDocument) textp
我不知道为什么这个异常不起作用...... import java.util.*; public class a { public static void main(String[] args
我目前在 case 中使用多个 when 时遇到问题。当我删除第二个当时,它就起作用了。这是什么问题? 报告的MYSQL错误为: #1064 - You have an error in your S
例如,我有一个表记录用户查看和下载文件的事件, file_id user activity 2 Tim view 1 Ron
这是一个非常愚蠢的问题,但我需要一点安慰/帮助。我有当前的“递归”情况: void add( int value ) { // do something ... // if ( conditi
我尝试使用以下代码在按钮数组上注册回调。但我似乎无法理解如何绑定(bind)回调中需要的字符串。任何建议将不胜感激! for (var i = 0; i < this.car_types.length
我是一名优秀的程序员,十分优秀!