Подробно объясните механизм управления памятью Redis и его реализацию.

Redis
Подробно объясните механизм управления памятью Redis и его реализацию.

Redis — это база данных типа «ключ-значение», хранящаяся в памяти, и ее управление памятью очень важно. Содержание управления памятью в этой статье включает в себя: ленивое удаление и удаление просроченных ключей с истекшим сроком действия, а также стратегии контроля переполнения памяти.

максимальный предел памяти

Redis использует параметр maxmemory для ограничения максимального объема доступной памяти.Значение по умолчанию равно 0, что означает неограниченность. Основные цели ограничения памяти:

  • Используется в сценариях кэширования, когда превышен предел памяти maxmemory, используйте стратегии удаления, такие как LRU, для освобождения места.
  • Предотвращает использование памяти, превышающее объем физической памяти сервера. Поскольку по умолчанию Redis будет максимально использовать память сервера, может быть недостаточно памяти сервера, что приведет к завершению процесса Redis.

maxmemory ограничивает объем памяти, фактически используемый Redis, то есть память, соответствующую статистике used_memory.由于内存碎片率的存在,实际消耗的内存 可能会比maxmemory设置的更大,实际使用时要小心这部分内存溢出。具体Redis 内存监控的内容请查看Статья для понимания мониторинга памяти Redis и потребления памяти.

По умолчанию Redis использует память сервера на неопределенный срок.Чтобы предотвратить исчерпание системной памяти в крайних случаях, рекомендуется, чтобы все процессы Redis были настроены с maxmemory. В случае обеспечения доступности физической памяти все экземпляры Redis в системе могут настроить параметр maxmemory для достижения цели свободного масштабирования памяти.

стратегия восстановления памяти

В Redis существует примерно два механизма очистки памяти: один — удалить объекты «ключ-значение», срок действия которых истек; другой — активировать стратегию управления удалением памяти, когда объем памяти достигает максимального значения, принудительно удалить выбранные объекты «ключ-значение». .

удалить ключевой объект с истекшим сроком действия

Все ключевые ключи Redis могут устанавливать атрибуты истечения срока действия, которые хранятся на таблице истечения срока годности. Результаты таблицы значений ключей и таблица истечения срока годности показаны на следующем рисунке. Когда Redis сохраняет большое количество ключей, точное удаление истечения срока действия каждого ключа может потреблять много CPU, заблокируйте основную нить Redis и перетаскивают производительность Redis. Следовательно, Redis принимает ленивый удаление и механизм удаления задач для достижения ключевые ключи. Отралайн памяти.

Ленивое удаление означает, что когда клиент оперирует ключом с атрибутом тайм-аута, он проверяет, истек ли срок действия ключа, а затем выполняет операцию удаления синхронно или асинхронно и возвращает, что срок действия ключа истек. Это может сэкономить затраты на ЦП, и нет необходимости поддерживать отдельный список сроков действия для обработки удаления ключей с истекшим сроком действия.

Стратегия ленивого удаления ключей с истекшим сроком действия реализуется функцией db.c/expireifNeeded.Перед выполнением всех команд чтения и записи в базу данных вызывается expireifNeeded, чтобы проверить, истек ли срок действия ключей, выполняемых командой. Если срок действия ключа истек, expireifNeeded удалит ключ с истекшим сроком действия из таблицы значений ключа и таблицы срока действия, а затем освободит пространство соответствующего объекта синхронно или асинхронно. Исходный код отображается в версии Redis 4.0.

expireIfNeeded сначала получает время истечения, соответствующее ключу из таблицы истечения.Если текущее время превысило время истечения (исполнение Lua-скрипта имеет особую логику, подробности см. в комментариях к коду), начинается процесс удаления ключа. Процесс удаления ключа выполняет три основные функции:

  • Один из них — распространить команду операции удаления, уведомить подчиненный экземпляр и сохранить ее в буфере AOF.
  • Во-вторых, для записи событий keyspace,
  • Третий — выполнить асинхронное удаление или асинхронное удаление в зависимости от того, включен ли параметр lazyfree_lazy_expire.
int expireIfNeeded(redisDb *db, robj *key) {
    // 获取键的过期时间
    mstime_t when = getExpire(db,key);
    mstime_t now;
    // 键没有过期时间
    if (when < 0) return 0;
    // 实例正在从硬盘 laod 数据,比如说 RDB 或者 AOF
    if (server.loading) return 0;

    // 当执行lua脚本时,只有键在lua一开始执行时
    // 就到了过期时间才算过期,否则在lua执行过程中不算失效
    now = server.lua_caller ? server.lua_time_start : mstime();

    // 当本实例是slave时,过期键的删除由master发送过来的
    // del 指令控制。但是这个函数还是将正确的信息返回给调用者。
    if (server.masterhost != NULL) return now > when;
    // 判断是否未过期
    if (now <= when) return 0;

    // 代码到这里,说明键已经过期,而且需要被删除
    server.stat_expiredkeys++;
    // 命令传播,到 slave 和 AOF
    propagateExpire(db,key,server.lazyfree_lazy_expire);
    // 键空间通知使得客户端可以通过订阅频道或模式, 来接收那些以某种方式改动了 Redis 数据集的事件。
    notifyKeyspaceEvent(NOTIFY_EXPIRED,
        "expired",key,db->id);
    // 如果是惰性删除,调用dbAsyncDelete,否则调用 dbSyncDelete
    return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
                                         dbSyncDelete(db,key);
}

Вышеприведенный рисунок представляет собой схематическую диаграмму распространения команд записи, и распространение команд удаления соответствует ей. Функция propagateExpire сначала вызывает функцию feedAppendOnlyFile для синхронизации команды с буфером AOF, а затем вызывает функцию replicationFeedSlaves для синхронизации команды со всеми ведомыми устройствами. Механизм репликации Redis можно посмотретьПодробное объяснение процесса репликации Redis.

// 将命令传递到slave和AOF缓冲区。maser删除一个过期键时会发送Del命令到所有的slave和AOF缓冲区
void propagateExpire(redisDb *db, robj *key, int lazy) {
    robj *argv[2];
    // 生成同步的数据
    argv[0] = lazy ? shared.unlink : shared.del;
    argv[1] = key;
    incrRefCount(argv[0]);
    incrRefCount(argv[1]);
    // 如果开启了 AOF 则追加到 AOF 缓冲区中
    if (server.aof_state != AOF_OFF)
        feedAppendOnlyFile(server.delCommand,db->id,argv,2);
    // 同步到所有 slave
    replicationFeedSlaves(server.slaves,db->id,argv,2);

    decrRefCount(argv[0]);
    decrRefCount(argv[1]);
}

Функция dbAsyncDelete сначала вызовет dictDelete для удаления ключа в таблице с истекшим сроком действия, а затем обработает объект "ключ-значение" в таблице "ключ-значение". Он выберет, следует ли освободить объект значения напрямую или передать его био для асинхронного освобождения объекта значения в соответствии с пространством, занимаемым значением. Основанием для суждения является то, превышает ли расчетный размер значения пороговое значение LAZYFREE_THRESHOLD. И ключевой объект, и объект dictEntry освобождаются напрямую.

#define LAZYFREE_THRESHOLD 64
int dbAsyncDelete(redisDb *db, robj *key) {
    // 删除该键在过期表中对应的entry
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);

    // unlink 该键在键值表对应的entry
    dictEntry *de = dictUnlink(db->dict,key->ptr);
    // 如果该键值占用空间非常小,懒删除反而效率低。所以只有在一定条件下,才会异步删除
    if (de) {
        robj *val = dictGetVal(de);
        size_t free_effort = lazyfreeGetFreeEffort(val);
        // 如果释放这个对象消耗很多,并且值未被共享(refcount == 1)则将其加入到懒删除列表
        if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
            atomicIncr(lazyfree_objects,1);
            bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
            dictSetVal(db->dict,de,NULL);
        }
    }

    // 释放键值对,或者只释放key,而将val设置为NULL来后续懒删除
    if (de) {
        dictFreeUnlinkedEntry(db->dict,de);
        // slot 和 key 的映射关系是用于快速定位某个key在哪个 slot中。
        if (server.cluster_enabled) slotToKeyDel(key);
        return 1;
    } else {
        return 0;
    }
}

dictUnlink удаляет значение ключа из таблицы значений ключа, но не освобождает ключ, val и соответствующие объекты записи таблицы, а возвращает их напрямую, а затем вызывает dictFreeUnlinkedEntry для их освобождения. dictDelete — родственная ему функция, но она напрямую освобождает соответствующий объект. Нижний уровень обоих реализуется путем вызова dictGenericDelete. Родственная функция dbSyncDelete для dbAsyncDelete d предназначена для прямого вызова dictDelete для удаления ключей с истекшим сроком действия.

void dictFreeUnlinkedEntry(dict *d, dictEntry *he) {
    if (he == NULL) return;
    // 释放key对象
    dictFreeKey(d, he);
    // 释放值对象,如果它不为null
    dictFreeVal(d, he);
    // 释放 dictEntry 对象
    zfree(he);
}

Redis имеет собственный биомеханизм, в основном связанный с удалением AOF, логикой ленивого удаления и закрытием большого файла fd. Функция bioCreateBackgroundJob добавляет задание, освобождающее объект-значение, в очередь, а функция bioProcessBackgroundJobs берет задание из очереди и выполняет соответствующие операции в соответствии с типом.

void *bioProcessBackgroundJobs(void *arg) {
    .....
    while(1) {
        listNode *ln;

        ln = listFirst(bio_jobs[type]);
        job = ln->value;
        if (type == BIO_CLOSE_FILE) {
            close((long)job->arg1);
        } else if (type == BIO_AOF_FSYNC) {
            aof_fsync((long)job->arg1);
        } else if (type == BIO_LAZY_FREE) {
            // 根据参数来决定要做什么。有参数1则要释放它,有参数2和3是释放两个键值表
            // 过期表,也就是释放db 只有参数三是释放跳表
            if (job->arg1)
                lazyfreeFreeObjectFromBioThread(job->arg1);
            else if (job->arg2 && job->arg3)
                lazyfreeFreeDatabaseFromBioThread(job->arg2,job->arg3);
            else if (job->arg3)
                lazyfreeFreeSlotsMapFromBioThread(job->arg3);
        }
        zfree(job);
        ......
    }
}

dbSyncDelete напрямую удаляет ключ с истекшим сроком действия и освобождает ключ, значение и объекты DictEntry.

int dbSyncDelete(redisDb *db, robj *key) {
    // 删除过期表中的entry
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
    // 删除键值表中的entry
    if (dictDelete(db->dict,key->ptr) == DICT_OK) {
        // 如果开启了集群,则删除slot 和 key 映射表中key记录。
        if (server.cluster_enabled) slotToKeyDel(key);
        return 1;
    } else {
        return 0;
    }
}

Однако при использовании только этого метода возникает проблема утечки памяти: если ключ с истекшим сроком действия не был использован, он не будет вовремя удален, в результате чего память не будет вовремя освобождена. Из-за этого Redis также предоставляет другой механизм запланированного удаления задач в качестве дополнения к отложенному удалению.

Redis поддерживают задачу внутреннего времени, по умолчанию работает в 10 раз в секунду (по контролю конфигурации). Siming Task Удаление Логика ключей срок действия ключей использует адаптивный алгоритм, в соответствии с коэффициентом истечения срока действия ключа, используя быструю и медленную клавишу режима рецензии, поток, как показано ниже.

  • 1) Запланированная задача сначала вычисляет и вычисляет максимальное время выполнения этого цикла, количество баз для проверки и количество баз, сканируемых каждой базой данных в зависимости от быстрого и медленного режима (количество ключей, сканируемых медленным модель и время выполнения больше, чем у быстрого режима) и связанные с ними пороговые конфигурации, количество ключей.
    1. Начиная с базы данных, которая не была просканирована последней запланированной задачей, по очереди просматривайте каждую базу данных.
  • 3) Случайным образом выберите ключи ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP из базы данных, если окажется, что это ключ с истекшим сроком действия, вызовите функцию activeExpireCycleTryExpire, чтобы удалить его.
  • 4) Если время выполнения превышает установленное максимальное время выполнения, выйдите и установите в следующий раз использование медленного режима для выполнения.
  • 5) Если время ожидания не истекло, определите, просрочены ли 25% выбранных ключей, и если да, продолжите сканирование текущей базы данных и перейдите к шагу 3. В противном случае начните сканирование следующей базы данных.

Политика периодического удаления реализуется функцией expire.c/activeExpireCycle. eventLoop->beforesleep и в цикле, управляемом событиями redis Базы данных периодических операцийCron будет вызывать activeExpireCycle для обработки ключей с истекшим сроком действия. Однако значения типов, передаваемые ими, различаются: одно — ACTIVE_EXPIRE_CYCLE_SLOW, а другое — ACTIVE_EXPIRE_CYCLE_FAST. ActiveExpireCycle проходит каждую базу данных несколько раз в указанное время, случайным образом проверяет время истечения срока действия некоторых ключей с истекшим сроком действия из словаря expires и удаляет ключи с истекшим сроком действия.Соответствующий исходный код выглядит следующим образом.

void activeExpireCycle(int type) {
    // 上次检查的db
    static unsigned int current_db = 0; 
    // 上次检查的最大执行时间
    static int timelimit_exit = 0;
    // 上一次快速模式运行时间
    static long long last_fast_cycle = 0; /* When last fast cycle ran. */

    int j, iteration = 0;
    // 每次检查周期要遍历的DB数
    int dbs_per_call = CRON_DBS_PER_CALL;
    long long start = ustime(), timelimit, elapsed;

    ..... // 一些状态时不进行检查,直接返回

    // 如果上次周期因为执行达到了最大执行时间而退出,则本次遍历所有db,否则遍历db数等于 CRON_DBS_PER_CALL
    if (dbs_per_call > server.dbnum || timelimit_exit)
        dbs_per_call = server.dbnum;

    // 根据ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC计算本次最大执行时间
    timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
    timelimit_exit = 0;
    if (timelimit <= 0) timelimit = 1;
    // 如果是快速模式,则最大执行时间为ACTIVE_EXPIRE_CYCLE_FAST_DURATION
    if (type == ACTIVE_EXPIRE_CYCLE_FAST)
        timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */
    // 采样记录
    long total_sampled = 0;
    long total_expired = 0;
    // 依次遍历 dbs_per_call 个 db
    for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
        int expired;
        redisDb *db = server.db+(current_db % server.dbnum);
        // 将db数增加,一遍下一次继续从这个db开始遍历
        current_db++;

        do {
            ..... // 申明变量和一些情况下 break
            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
            // 主要循环,在过期表中进行随机采样,判断是否比率大于25%
            while (num--) {
                dictEntry *de;
                long long ttl;

                if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                ttl = dictGetSignedIntegerVal(de)-now;
                // 删除过期键
                if (activeExpireCycleTryExpire(db,de,now)) expired++;
                if (ttl > 0) {
                    /* We want the average TTL of keys yet not expired. */
                    ttl_sum += ttl;
                    ttl_samples++;
                }
                total_sampled++;
            }
            // 记录过期总数
            total_expired += expired;
            // 即使有很多键要过期,也不阻塞很久,如果执行超过了最大执行时间,则返回
            if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
                elapsed = ustime()-start;
                if (elapsed > timelimit) {
                    timelimit_exit = 1;
                    server.stat_expired_time_cap_reached_count++;
                    break;
                }
            }
            // 当比率小于25%时返回
        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
    }
    .....// 更新一些server的记录数据
}

Достигните функцию ActiveExpirecycletryExpire On и Expireifneded подобные, не повторяйте их здесь.

int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) {
    long long t = dictGetSignedIntegerVal(de);
    if (now > t) {
        sds key = dictGetKey(de);
        robj *keyobj = createStringObject(key,sdslen(key));

        propagateExpire(db,keyobj,server.lazyfree_lazy_expire);
        if (server.lazyfree_lazy_expire)
            dbAsyncDelete(db,keyobj);
        else
            dbSyncDelete(db,keyobj);
        notifyKeyspaceEvent(NOTIFY_EXPIRED,
            "expired",keyobj,db->id);
        decrRefCount(keyobj);
        server.stat_expiredkeys++;
        return 1;
    } else {
        return 0;
    }
}

Ключом к регулярной стратегии удаления является то, как долго и как часто выполняется удаление:

  • Если операция удаления слишком частая или время выполнения слишком велико, это не очень удобно для процессорного времени, и слишком много процессорного времени расходуется на удаление ключей с истекшим сроком действия.
  • Если операция удаления выполняется слишком мало или время выполнения слишком мало, ключ с истекшим сроком действия не может быть удален вовремя, что приводит к пустой трате памяти.

стратегия контроля переполнения памяти

Когда память, используемая Redis, достигает верхнего предела maxmemory, срабатывает соответствующая стратегия управления переполнением. Конкретная стратегия контролируется параметром maxmemory-policy.Redis поддерживает 6 стратегий, а именно:

  • 1) noeviction: политика по умолчанию, которая не будет удалять какие-либо данные, отклонит все операции записи и вернет сообщение об ошибке клиента (ошибка). Команда OOM не разрешена при использовании памяти, в настоящее время Redis реагирует только на операции чтения.
  • 2) Volatile-LRU: Удаляет атрибут тайм-аута настроек клавиши (истекает) в соответствии с алгоритмом LRU, пока не будет достаточным пространством. Если нет никакого удаления ключа объекта, верните в политику NoVeiction.
  • 3) allkeys-lru: удалять ключи по алгоритму LRU, независимо от того, установлен ли для данных атрибут тайм-аута, пока не освободится достаточно места.
  • 4) allkeys-random: случайным образом удалять все ключи, пока не освободится достаточно места.
  • 5) volatile-random: случайным образом удалять ключи с истекшим сроком действия, пока не освободится достаточно места.
  • 6) volatile-ttl: в соответствии с атрибутом ttl объекта «ключ-значение» удалите данные, срок действия которых истекает недавно. Если нет, вернитесь к стратегии невыселения.

Политику управления переполнением памяти можно динамически настроить с помощью инструкции config set maxmemory-policy {policy}. Redis предоставляет множество стратегий управления переполнением пространства, которые мы можем выбрать в соответствии с потребностями нашего бизнеса.

При установке стратегии volatile-lru для обеспечения того, чтобы ключ имел атрибут истечения срока действия, он может быть удален в соответствии с LRU, ключ без установки постоянного тайм-аута. Также можно использовать стратегию allkeys-lru, чтобы стать чистым сервером кеша Redis.

Когда Redis удаляется из-за переполнения памяти для ключа удаления, вы можете просмотреть количество ключей, которые были удалены с текущего сервера Redis, выполнив команду info stats.

Каждый раз, когда Redis выполняет команду, если установлен параметр maxmemory, он будет пытаться освободить память. Когда Redis работает в состоянии переполнения памяти (used_memory>maxmemory) и задана политика non-noeviction, операция высвобождения памяти будет часто запускаться, что влияет на производительность сервера Redis.На этот момент нужно обратить внимание. .

Личный блог, добро пожаловать в игру