Redis 7.0 新特性之maxmemory-clients:限制客户端内存总使用量

背景

之前分享个 case(Redis 内存突增时,如何定量分析其内存使用情况 (https://mp.weixin.qq.com/s/28Cgo4o97AUc2L5qKpTSIw),一个 Redis 实例的内存突增,used_memory最大时达到了 78.9G,而该实例的maxmemory配置却只有 16G,最终导致实例中的数据被大量驱逐。

导致这个问题的一个常见原因是客户端占用的内存过多。

Redis 中,客户端内存主要包括三部分:输入缓冲区(暂存客户端命令)、输出缓冲区(缓存发送给客户端的数据),以及客户端对象本身的开销。

其中,输入缓冲区可通过client-query-buffer-limit限制,输出缓冲区可通过client-output-buffer-limit限制。

但这两个参数只能限制单个客户端

在客户端数量较多的情况下,即使单个客户端占用不大,客户端内存的总量仍可能失控。

为了解决这一问题,Redis 7.0 引入了maxmemory-clients,用于限制所有客户端可使用的内存总量。

下面看看具体的实现细节。

配置

c 复制代码
standardConfig static_configs[] = {
...
    createSSizeTConfig("maxmemory-clients", NULL, MODIFIABLE_CONFIG, -100, SSIZE_MAX, server.maxmemory_clients, 0, MEMORY_CONFIG | PERCENT_CONFIG, NULL, applyClientMaxMemoryUsage),
...
};

maxmemory-clients 的默认值为 0,最小值为 -100,对应的内部变量是server.maxmemory_clients

该参数既可以设置为正数,也可以设置为负数:

  • 正数:表示客户端内存总使用量的上限。因为该参数的类型是 MEMORY_CONFIG,所以可以指定 kb/mb/gb 之类的单位。不指定,则默认是字节。

  • 负数:表示按 maxmemory 的百分比限制客户端内存。例如:maxmemory-clients = -50表示客户端内存总量不得超过 maxmemory 的 50%。

这一点是在getClientEvictionLimit函数中实现的。

c 复制代码
size_t getClientEvictionLimit(void) {
    size_t maxmemory_clients_actual = SIZE_MAX;

    if (server.maxmemory_clients < 0 && server.maxmemory > 0) {
        unsigned long long maxmemory_clients_bytes = (unsigned long long)((double)server.maxmemory * -(double) server.maxmemory_clients / 100);
        if (maxmemory_clients_bytes <= SIZE_MAX)
            maxmemory_clients_actual = maxmemory_clients_bytes;
    }
    else if (server.maxmemory_clients > 0)
        maxmemory_clients_actual = server.maxmemory_clients;
    else
        return 0;

    /* Don't allow a too small maxmemory-clients to avoid cases where we can't communicate
     * at all with the server because of bad configuration */
    if (maxmemory_clients_actual < 1024*128)
        maxmemory_clients_actual = 1024*128;

    return maxmemory_clients_actual;
}

实现细节

当通过CONFIG SET命令调整maxmemory-clients的值时,会调用applyClientMaxMemoryUsage函数进行处理。

c 复制代码
static int applyClientMaxMemoryUsage(const char **err) {
    ...
    if (server.maxmemory_clients != 0)
        initServerClientMemUsageBuckets();
    ...
    if (server.maxmemory_clients == 0)
        freeServerClientMemUsageBuckets();
    return 1;
}

可以看到,当server.maxmemory_clients的值不为 0,会调用initServerClientMemUsageBuckets()

c 复制代码
void initServerClientMemUsageBuckets() {
    if (server.client_mem_usage_buckets)
        return;
    server.client_mem_usage_buckets = zmalloc(sizeof(clientMemUsageBucket)*CLIENT_MEM_USAGE_BUCKETS);
    for (int j = 0; j < CLIENT_MEM_USAGE_BUCKETS; j++) {
        server.client_mem_usage_buckets[j].mem_usage_sum = 0;
        server.client_mem_usage_buckets[j].clients = listCreate();
    }
}

该函数用于初始化server.client_mem_usage_buckets数组,数组长度由宏CLIENT_MEM_USAGE_BUCKETS决定,默认 19。

每个元素表示一个桶(bucket)。 每个桶维护两类信息:

  • mem_usage_sum:该桶内所有客户端的内存占用总和。
  • clients:属于该桶的客户端列表。

当客户端内存发生变化时,Redis 会通过updateClientMemUsageAndBucket更新该客户端的内存使用情况(客户端使用的内存,对应client list输出中的tot-mem),并根据内存大小将客户端分配到对应的桶中:

  • 小于 32KB 的客户端进入 0 号桶;
  • 32KB~64KB 的客户端进入 1 号桶;
  • 之后每个桶的范围按 2 倍递增;
  • ≥ 4GB 的客户端进入 18 号桶。

此外,Redis 还会通过clientsCron()周期性地更新部分客户端的内存使用情况。 clientsCron() 的执行频率由server.hz控制,默认每秒 10 次。

适用的客户端类型

需要注意的是,并非所有客户端都会被统计内存并参与驱逐。具体判断逻辑如下:

c 复制代码
int clientEvictionAllowed(client *c) {
    if (server.maxmemory_clients == 0 || c->flags & CLIENT_NO_EVICT) {
        return 0;
    }
    int type = getClientType(c);
    return (type == CLIENT_TYPE_NORMAL || type == CLIENT_TYPE_PUBSUB);
}

可以看到,只有满足以下条件的客户端才会被统计内存并参与驱逐:

  1. maxmemory_clients不为 0。
  2. 客户端未设置 CLIENT_NO_EVICT,在 Redis 7.0 中,支持通过CLIENT NO-EVICT ON命令显式关闭驱逐。
  3. 客户端类型为 NORMAL 或 PUBSUB。也就是说,复制相关客户端不会被驱逐。

客户端驱逐细节

客户端驱逐是在evictClients函数中实现的。

c 复制代码
void evictClients(void) {
    // 如果 client_mem_usage_buckets 没被初始化,则直接返回
    if (!server.client_mem_usage_buckets)
        return;
    // 从最大客户端内存桶开始驱逐
    int curr_bucket = CLIENT_MEM_USAGE_BUCKETS-1;
    listIter bucket_iter;
    listRewind(server.client_mem_usage_buckets[curr_bucket].clients, &bucket_iter);
    // 获取客户端允许使用的最大内存
    size_t client_eviction_limit = getClientEvictionLimit();
    if (client_eviction_limit == 0)
        return;
    // 循环驱逐,直到客户端总内存降到阈值以下或所有可驱逐客户端已释放
    while (server.stat_clients_type_memory[CLIENT_TYPE_NORMAL] +
           server.stat_clients_type_memory[CLIENT_TYPE_PUBSUB] >= client_eviction_limit) {
        // 获取当前桶的下一个客户端
        listNode *ln = listNext(&bucket_iter);
        if (ln) {
            client *c = ln->value;
            // 生成客户端信息字符串,用于日志
            sds ci = catClientInfoString(sdsempty(),c);
            serverLog(LL_NOTICE, "Evicting client: %s", ci);
            // 释放客户端占用的资源
            freeClient(c);
            sdsfree(ci);
            // stat_evictedclients对应的是info stats中的evicted_clients
            server.stat_evictedclients++;
        } else {
            // 当前桶已空,切换到下一个较小客户端桶
            curr_bucket--;
            // 所有桶都已经遍历完,但内存仍超过阈值,记录警告
            if (curr_bucket < 0) {
                serverLog(LL_WARNING, "Over client maxmemory after evicting all evictable clients");
                break;
            }
            listRewind(server.client_mem_usage_buckets[curr_bucket].clients, &bucket_iter);
        }
    }
}

可以看到,该函数从最大内存桶开始驱逐,优先淘汰占用内存最多的客户端。

对于被驱逐的客户端,会在日志中打印以下内容。

bash 复制代码
* Evicting client: id=993566 addr=243.247.151.0:46084 laddr=172.17.0.2:7379 fd=774 name= age=6 idle=1 flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 qbuf=0 qbuf-free=20474 argv-mem=0 multi-mem=0 rbs=4096 rbp=0 obl=0 oll=1 omem=3145752 tot-mem=3171096 events=rw cmd=get user=default redir=-1 resp=2

该函数的调用场景主要有两个:

  1. beforeSleep:在处理完本轮所有命令、即将进入下一轮事件循环阻塞前执行。该阶段会处理客户端读写与阻塞状态、集群与复制维护、Key 过期、AOF 刷盘、异步释放客户端,以及客户端内存驱逐等操作。
  2. processCommand(client *c):在完整读取并解析一条客户端命令后调用,是所有命令的必经路径,用于执行命令合法性校验、ACL 权限检查、集群重定向判断、客户端内存限制、服务器内存淘汰、只读从库校验等一系列前置检查。
相关推荐
Macbethad12 分钟前
基于WPF与.NET的洁净厂房数据孪生平台技术方案:架构、实现与SEMI标准实践
数据库·系统架构
咏方舟【长江支流】33 分钟前
【开源】跨语言·跨平台·跨数据库(6) ——一种ORM缓存的接口实现和容错处理
数据库·缓存·开源·咏方舟-长江支流·用宝框架
努力进修42 分钟前
破除工业 AI 业务落地壁垒:多模时序融合架构重塑设备全维度数据价值
数据库·人工智能·架构
静水楼台x1 小时前
spring security
java·数据库·spring
深度研习笔记1 小时前
OpenCV工业视觉进阶14|SQLite违规数据库存储+历史记录查询+Excel报表导出,完成项目商用数据闭环(验收必备)
数据库·opencv·sqlite
要开心吖ZSH3 小时前
一文搞懂:JDK 21 虚拟线程 vs N种异步方案,到底该怎么选?
java·数据库·jdk·虚拟线程
guodingdingh8 小时前
软件开发工作问题总结0718
java·开发语言·数据库
GuWenyue10 小时前
Cursor黑盒拆解!1套LangChain.js手写Mini编程Agent,自动生成React项目,效率提升60%
前端·数据库·人工智能
Miao1213112 小时前
某海外住宿平台如何在大规模场景下实现指标一致性:Minerva 指标平台实践
大数据·数据库·人工智能
CodexDave12 小时前
MySQL事务隔离级别与MVCC机制解析
前端·数据库·mysql·nginx·性能优化·负载均衡