Как система кэширования на основе памяти, 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) должны выполняться в заданный момент времени, а временные события — это абстракция сервера от таких операций синхронизации, таких как очистка просроченного ключа, статистика состояния службы и т. д.
Как показано на рисунке выше, Redis абстрагирует события файлов и события времени. Тренер времени будет прослушивать таблицу событий ввода-вывода. Как только событие файла будет готово, Redis расставит приоритеты файловых событий, а затем обработает события времени. Во всех вышеперечисленных обработках событий Redis начинает с单线程
Обработка форм, поэтому Redis является однопоточным. Кроме того, как показано на рисунке ниже, Redis разработал собственный обработчик событий ввода-вывода на основе режима Reactor, то есть обработчик файловых событий Redis использует технологию мультиплексирования ввода-вывода при обработке событий ввода-вывода и отслеживает множественный сокет связан с различными функциями обработки событий для сокета, а многоклиентская параллельная обработка реализуется через один поток.
Благодаря такой конструкции при обработке данных исключается операция блокировки, что не только делает реализацию достаточно простой, но и обеспечивает ее высокую производительность. Конечно, однопоточность 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, и производительность может не значительно улучшиться.
Красная часть на рисунке выше — это многопоточная часть, реализованная 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
междубезблокировочная очередьитрубопроводДля обмена данными общая производительность будет лучше.
резюме
Представлено в Redis 4.0Lazy Free
поток, который решает такие проблемы, как удаление большого ключа, вызывающее блокировку сервера, представленный в версии 6.0I/O Thread
Thread, который официально реализует многопоточность, но по сравнению с Таиром он не очень элегантен, и прирост производительности невелик, по стресс-тесту производительность многопоточной версии в два раза выше, чем у однопоточной версия, а многопоточная версия Таира — однопоточная, в 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
Реализация стала более полной, и действительно стоит ожидать использования модуля для оптимизации медленных операций.