Эволюция многопоточности Redis

Redis

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

  • Можно использовать только одно ядро ​​ЦП;
  • Если удаленный ключ будет слишком большим (например, в типе Set миллионы объектов), это приведет к блокировке сервера на несколько секунд;
  • QPS сложно улучшить.

В ответ на вышеуказанные проблемы Redis был представлен в версиях 4.0 и 6.0 соответственно.Lazy Freeа также多线程IO, и постепенно перейти на многопоточность, о которой будет подробно рассказано ниже.

принцип одной нити

Говорят, что Redis является однопоточным, так как же отражается однопоточность? Как поддерживать одновременные запросы клиентов? Чтобы разобраться в этих вопросах, давайте сначала разберемся, как работает Redis.

Сервер Redis — это программа, управляемая событиями, и сервер должен обрабатывать следующие два типа событий:

  • 文件事件: сервер Redis подключается к клиенту (или другим серверам Redis) через сокеты, а событие файла является абстракцией операции сервера с сокетом; связь между сервером и клиентом генерирует соответствующее событие файла, и сервер будет прослушивать и обрабатывать эти события, чтобы завершить серию сетевых операций связи, таких как подключениеaccept,read,write,closeЖдать;
  • 时间事件: некоторые операции на сервере Redis (например, функция serverCron) должны выполняться в заданный момент времени, а временные события — это абстракция сервера от таких операций синхронизации, таких как очистка просроченного ключа, статистика состояния службы и т. д.

事件调度http://note.youdao.com/yws/res/69489/642EDA3CC49142B1BF1ABD5FDEC89C44

Как показано на рисунке выше, Redis абстрагирует события файлов и события времени. Тренер времени будет прослушивать таблицу событий ввода-вывода. Как только событие файла будет готово, Redis расставит приоритеты файловых событий, а затем обработает события времени. Во всех вышеперечисленных обработках событий Redis начинает с单线程Обработка форм, поэтому Redis является однопоточным. Кроме того, как показано на рисунке ниже, Redis разработал собственный обработчик событий ввода-вывода на основе режима Reactor, то есть обработчик файловых событий Redis использует технологию мультиплексирования ввода-вывода при обработке событий ввода-вывода и отслеживает множественный сокет связан с различными функциями обработки событий для сокета, а многоклиентская параллельная обработка реализуется через один поток.

多路复用件http://note.youdao.com/yws/res/69485/3BF073D64B054C87AC203040FBAD246F

Благодаря такой конструкции при обработке данных исключается операция блокировки, что не только делает реализацию достаточно простой, но и обеспечивает ее высокую производительность. Конечно, однопоточность Redis относится только к его обработке событий.На самом деле Redis не является однопоточным.Например, при создании RDB-файла он будет разветвлять дочерний процесс для его достижения.Конечно, это не то, что эта статья собирается обсудить.

Ленивый свободный механизм

Как известно выше, Redis работает в однопоточном режиме при обработке клиентских команд, и скорость обработки очень высока.В этот период он не будет отвечать на другие запросы клиента.Однако, если клиент отправляет команду, которая занимает много времени время для Redis, например удаление ключа Set, содержащего миллионы объектов, или выполнение операций flushdb, flushall, серверу Redis необходимо освободить большой объем памяти, что приведет к зависанию сервера на несколько секунд, что приведет к катастрофе. для кеш-системы с высокой нагрузкой. Чтобы решить эту проблему, он был представлен в Redis 4.0.Lazy Free,будет慢操作Асинхронный, что также является шагом к многопоточности в обработке событий.

Как пишет автор в своем блоге, для решения慢操作, можно использовать прогрессивную обработку, то есть добавить событие времени, например, при удалении ключа Set с миллионами объектов каждый раз удаляется только часть данных в большом ключе, и, наконец, удаление большого ключ реализован. Однако эта схема может привести к тому, что скорость повторного использования будет отставать от скорости создания, что в конечном итоге приведет к исчерпанию памяти. Поэтому окончательная реализация Redis заключается в асинхронизации удаления больших ключей и использовании неблокирующего удаления (соответствующего командеUNLINK), высвобождение пространства большого ключа реализовано отдельным потоком, основной поток выполняет только отмену отношения, и может быстро вернуться и продолжить обработку других событий, чтобы избежать блокировки сервера на долгое время.

удалить (DELкоманда) в качестве примера, чтобы увидеть, как реализован Redis, ниже приведена запись функции удаления, среди которыхlazyfree_lazy_user_delследует ли изменитьDELПоведение команды по умолчанию, после включения, выполняетсяDELбудетUNLINKисполнение формы.

void delCommand(client *c) {
    delGenericCommand(c,server.lazyfree_lazy_user_del);
}

/* This command implements DEL and LAZYDEL. */
void delGenericCommand(client *c, int lazy) {
    int numdel = 0, j;

    for (j = 1; j < c->argc; j++) {
        expireIfNeeded(c->db,c->argv[j]);
        // 根据配置确定DEL在执行时是否以lazy形式执行
        int deleted  = lazy ? dbAsyncDelete(c->db,c->argv[j]) :
                              dbSyncDelete(c->db,c->argv[j]);
        if (deleted) {
            signalModifiedKey(c,c->db,c->argv[j]);
            notifyKeyspaceEvent(NOTIFY_GENERIC,
                "del",c->argv[j],c->db->id);
            server.dirty++;
            numdel++;
        }
    }
    addReplyLongLong(c,numdel);
}

Синхронное удаление очень простое, пока удаляются ключ и значение, если есть внутренняя ссылка, выполняется рекурсивное удаление, которое здесь не представлено. Давайте рассмотрим асинхронное удаление. Когда Redis перерабатывает объекты, он сначала вычисляет доход от переработки. Только когда доход от переработки превышает определенное значение, он будет упакован как задание и добавлен в очередь асинхронной обработки. В противном случае он будет быть непосредственно переработаны синхронно, что является более эффективным. Расчет дохода от переработки также очень прост, например,Stringтип, возвращаемое значение равно 1, иSetТип, доход восстановления - это количество элементов в коллекции.

/* Delete a key, value, and associated expiration entry if any, from the DB.
 * If there are enough allocations to free the value object may be put into
 * a lazy free list instead of being freed synchronously. The lazy free list
 * will be reclaimed in a different bio.c thread. */
#define LAZYFREE_THRESHOLD 64
int dbAsyncDelete(redisDb *db, robj *key) {
    /* Deleting an entry from the expires dict will not free the sds of
     * the key, because it is shared with the main dictionary. */
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);

    /* If the value is composed of a few allocations, to free in a lazy way
     * is actually just slower... So under a certain limit we just free
     * the object synchronously. */
    dictEntry *de = dictUnlink(db->dict,key->ptr);
    if (de) {
        robj *val = dictGetVal(de);
        // 计算value的回收收益
        size_t free_effort = lazyfreeGetFreeEffort(val);

        /* If releasing the object is too much work, do it in the background
         * by adding the object to the lazy free list.
         * Note that if the object is shared, to reclaim it now it is not
         * possible. This rarely happens, however sometimes the implementation
         * of parts of the Redis core may call incrRefCount() to protect
         * objects, and then call dbDelete(). In this case we'll fall
         * through and reach the dictFreeUnlinkedEntry() call, that will be
         * equivalent to just calling decrRefCount(). */
        // 只有回收收益超过一定值,才会执行异步删除,否则还是会退化到同步删除
        if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
            atomicIncr(lazyfree_objects,1);
            bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
            dictSetVal(db->dict,de,NULL);
        }
    }

    /* Release the key-val pair, or just the key if we set the val
     * field to NULL in order to lazy free it later. */
    if (de) {
        dictFreeUnlinkedEntry(db->dict,de);
        if (server.cluster_enabled) slotToKeyDel(key->ptr);
        return 1;
    } else {
        return 0;
    }
}

представленa threaded lazy free, Redis реализует дляSlow OperationизLazyоперация, позволяющая избежать удаления больших ключей,FLUSHALL,FLUSHDBвызвать блокировку сервера. Конечно, при реализации этой функции важно не только введениеlazy freeПотоки также улучшают структуру хранения агрегатного типа Redis. Потому что Redis использует много общих объектов внутри, например кэши вывода на стороне клиента. Конечно, Redis не использует блокировки, чтобы избежать конфликтов потоков.Конкуренция блокировок приведет к снижению производительности.Вместо этого он удаляет общие объекты и напрямую использует копии данных, как показано ниже в 3.x и 6.x.ZSetРазличные реализации значения узла.

// 3.2.5版本ZSet节点实现,value定义robj *obj
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    robj *obj;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;

// 6.0.10版本ZSet节点实现,value定义为sds ele
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;

Удаление общих объектов не только реализуетlazy freeЭта функция также позволяет Redis перейти к многопоточности, как сказал автор:

Теперь, когда значения агрегированных типов данных полностью не разделены, а буферы вывода клиента также не содержат общих объектов, есть много возможностей для эксплуатации.Например, наконец-то можно реализовать многопоточный ввод-вывод в Redis, так что разные клиенты обслуживаются разными потоками.Это означает, что у нас будет глобальная блокировка только при доступе к базе данных, но клиенты читают/записывают системные вызовы и даже парсинг отправляемой клиентом команды могут происходить в разных потоках.

Многопоточный ввод-вывод и его ограничения

Redis был представлен в версии 4.0.Lazy Free, с тех пор Redis имеетLazy FreeПоток специально используется для восстановления больших ключей, и в то же время общий объект агрегатного типа также удаляется, что дает возможность многопоточности Redis также оправдал свои ожидания и был реализован в версии 6.0.多线程I/O.

Принцип реализации

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

多线程IO实现http://note.youdao.com/yws/res/69692/AD32F3FA6C794ADDA62849413DB172E0

Красная часть на рисунке выше — это многопоточная часть, реализованная Redis, которая использует несколько ядер для разделения нагрузки ввода-вывода при чтении и записи. существует事件处理线程Каждый раз при получении события чтения все готовые события чтения назначаютсяI/O线程, а вообще ждатьI/O线程После завершения операции чтения事件处理线程Запустите обработку задачи, а после завершения обработки также назначьте событие записи наI/O线程, жду всехI/OПоток завершает операцию записи.

В качестве примера возьмем обработку события чтения, см.事件处理线程Процесс назначения задачи:

int handleClientsWithPendingReadsUsingThreads(void) {
    ...

    /* Distribute the clients across N different lists. */
    listIter li;
    listNode *ln;
    listRewind(server.clients_pending_read,&li);
    int item_id = 0;
    // 将等待处理的客户端分配给I/O线程
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        int target_id = item_id % server.io_threads_num;
        listAddNodeTail(io_threads_list[target_id],c);
        item_id++;
    }
    
    ...

    /* Wait for all the other threads to end their work. */
    // 轮训等待所有I/O线程处理完
    while(1) {
        unsigned long pending = 0;
        for (int j = 1; j < server.io_threads_num; j++)
            pending += io_threads_pending[j];
        if (pending == 0) break;
    }

    ...

    return processed;
}

I/O线程Поток обработки:

void *IOThreadMain(void *myid) {
    ...

    while(1) {
        ...

        // I/O线程执行读写操作
        while((ln = listNext(&li))) {
            client *c = listNodeValue(ln);
            // io_threads_op判断是读还是写事件
            if (io_threads_op == IO_THREADS_OP_WRITE) {
                writeToClient(c,0);
            } else if (io_threads_op == IO_THREADS_OP_READ) {
                readQueryFromClient(c->conn);
            } else {
                serverPanic("io_threads_op value is unknown");
            }
        }
        listEmpty(io_threads_list[id]);
        io_threads_pending[id] = 0;

        if (tio_debug) printf("[%ld] Done\n", id);
    }
}

ограничение

С точки зрения реализации, описанной выше, многопоточность версии 6.0 не является полной многопоточностью.I/O线程Только одновременные операции чтения или записи могут выполняться во время事件处理线程Он всегда находится в состоянии ожидания, а не в конвейерной модели, много накладных расходов на циклическое обучение.

Принцип реализации многопоточности Tair

По сравнению с многопоточностью версии 6.0, реализация многопоточности в Tair более элегантна. Как показано ниже, Tair'sMain ThreadОтвечает за установление клиентского соединения и т.д.IO ThreadОтвечает за чтение запросов, отправку ответов, разбор команд и т. д.Worker ThreadПотоки используются исключительно для обработки событий.IO ThreadПрочитать запрос пользователя и разобрать его, а затем поставить результат разбора в очередь в виде команды и отправить наWorker Threadиметь дело с.Worker ThreadПосле обработки команды ответ генерируется и отправляется в другую очередь через другую очередь.IO Thread. Чтобы улучшить параллелизм потоков,IO ThreadиWorker Threadмеждубезблокировочная очередьитрубопроводДля обмена данными общая производительность будет лучше.Tair多线程实现http://note.youdao.com/yws/res/69698/A1936D930F2845008AF77FE43013303C

резюме

Представлено в Redis 4.0Lazy Freeпоток, который решает такие проблемы, как удаление большого ключа, вызывающее блокировку сервера, представленный в версии 6.0I/O ThreadThread, который официально реализует многопоточность, но по сравнению с Таиром он не очень элегантен, и прирост производительности невелик, по стресс-тесту производительность многопоточной версии в два раза выше, чем у однопоточной версия, а многопоточная версия Таира — однопоточная, в 3 раза больше многопоточной версии. По мнению автора, многопоточность Redis — это не более чем две идеи,I/O threadingиSlow commands threading, как пишет автор в своем блоге:

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

Вместо этого я действительно хочу много медленных потоков операций, и с системой модулей Redis мы уже находимся в правильном направлении.Однако в будущем (не уверен, что в Redis 6 или 7) мы получим блокировку на уровне ключа в модульная система, чтобы потоки могли полностью получить контроль над ключом для обработки медленных операций.Теперь модули могут реализовывать команды и могут создавать ответ для клиента совершенно отдельным способом, но все же для доступа к общему набору данных необходима глобальная блокировка: это уйдет.

Авторы Redis предпочитают использовать кластеризацию для решения проблемыI/O threading, особенно в контексте собственного прокси-сервера Redis Cluster Proxy, выпущенного в версии 6.0, что упрощает использование кластера. Кроме того, автор предпочитаетslow operations threading(например, выпущенный в версии 4.0Lazy Free) для решения проблемы многопоточности. Последующие версии будутIO ThreadРеализация стала более полной, и действительно стоит ожидать использования модуля для оптимизации медленных операций.

использованная литература