Эволюция модели Redis IO

интервью Redis задняя часть
Эволюция модели Redis IO

предисловие

Как одна из наиболее широко используемых баз данных nosql, Redis претерпел множество обновлений. До версии 4.0 мультиплексирование одного потока + ввода-вывода позволяло производительности Redis достигать очень высокого уровня. Автор также сказал, что причина, по которой он разработан как однопоточный, заключается в том, что узкое место Redis находится не в процессоре, а однопоточный не должен учитывать накладные расходы на блокировку, вызванные многопоточностью. Однако с течением времени одиночный поток все меньше подходит для некоторых сценариев приложений, например, для решения проблемы, связанной с тем, что удаление больших ключей приведет к блокировке основного потока, в redis4.0 есть асинхронный поток. Ввиду того, что один поток не может соответствовать более высокому уровню параллелизма из-за невозможности использовать характеристики многоядерного процессора, в redis6.0 также был введен многопоточный режим. Поэтому говорить, что Redis является однопоточным, все более и более неточно.

модель событий

Redis сама по себе является программой, управляемой событиями.файл событияисобытие временидля завершения соответствующей функции. Событие файла на самом деле является абстракцией сокета, который абстрагирует каждое событие сокета в событие файла.Redis разработала собственный обработчик сетевых событий на основе модели Reactor. Так что же такое шаблон Reactor?

коммуникация

Подумайте над вопросом, как наш сервер получает наши данные? Сначала обе стороны должны установить TCP-соединение, после чего можно отправлять и получать данные. Отправитель отправляет данные в буфер сокета, ждет, пока система возьмет данные из буфера, а затем отправляет данные через сетевую карту.После получения данных сетевая карта получателя скопирует данные в буфер сокета, а затем ждать приложения. Это примерный процесс отправки и получения данных.

Стоимость копирования данных

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

Как данные узнают, в какой сокет отправлять?

Ядро поддерживает так много сокетов, откуда данные с сетевой карты узнают, в какой сокет доставлять? Ответ — порт, а сокет — четверка:ip(client)+ port(client)+ip(server)+port(server)(Будьте осторожны, не говорите, что теоретический максимальный параллелизм машины равен 65535, помимо портов есть ips, что должно быть число портов * количество ips), вот почему компьютер может открывать несколько программ одновременно в то же время.

Как уведомить программу о получении данных сокета

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

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

Когда в состоянии выполнения находится несколько процессов, из-за технологии квантования времени ЦП процессы в состоянии выполнения будут выполняться ЦП в течение определенного периода времени, как если бы они выполнялись одновременно, что является называетсяпараллелизм. Когда мы создаем сокетное соединение, это выглядит так:

sockfd = socket(AF_INET, SOCK_STREAM, 0)
connect(sockfd, ....)
recv(sockfd, ...)
doSometing()

Операционная система создаст дескриптор fd для каждого сокета, и этот fd указывает на созданный нами объект сокета, который содержитбуфер,очередь ожидания обработки.... Для процесса, создающего сокет, если данные не поступят, он застрянет вrecvВ этот момент процесс зависнет в очереди ожидания объекта сокета, для ЦП этот процессблокироватьДа, процессор он фактически не занимает, он ждет прихода данных.

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

  1. recv может получить только один fd, что, если вы хотите получить несколько fd? Прохождение цикла while немного менее эффективно.
  2. В дополнение к чтению данных, процесс также должен обрабатывать следующую логику.Когда данные не поступают, процесс находится в состоянии блокировки, даже если цикл while используется для мониторинга нескольких fds.Блокируются ли другие сокеты, потому что один из recvs заблокирован Блокировка всего процесса.

Ввиду вышеуказанных проблем, т.Reactorузор имультиплексирование ввода-выводаПоявилась технология.

Reactor

Reactor — это высокопроизводительный режим обработки ввода-вывода.В режиме Reactor основная программа отвечает только за контроль наличия файлового дескриптора.событие происходит, это очень важно, основная программа не занимается чтением и записью файловых дескрипторов. Итак, кто может читать и писать файловые дескрипторы? Ответ — другие рабочие программы.Когда в сокете происходит событие, доступное для чтения и записи, основная программа уведомляет рабочую программу.Именно рабочая программа фактически читает и записывает данные из сокета. Преимущество этого режима в том, что основная программа может выполнять параллелизм без блокировки, а основная программа очень легкая. События могут быть поставлены в очередь для выполнения рабочими процессами. Через режим Reactor нам нужно только зарегистрировать событие и обработчик (функция обратного вызова), соответствующий событию, в Reactor, например:

type Reactor interface{
   RegisterHandler(WriteCallback func(), "writeEvent");
   RegisterHandler(ReadCallback func(), "readEvent");
}

Когда клиент инициирует Redisset key valueкоманда, то такой командный запрос будет записан в буфер сокета. Когда Reactor отслеживает, что в соответствующем буфере сокета есть данные, тогда сокет в это время доступен для чтения, и Reactor инициирует событие чтения, введя заранее.ReadCallbackФункция обратного вызова для завершения разбора команды и выполнения команды. Когда в буфере сокета будет достаточно места для записи, соответствующий Reactor сгенерирует доступное для записи событие, и будет выполнено предварительное внедрение.WriteCallbackПерезвони. при инициацииset key valueПосле завершения выполнения рабочая программа в это время запишет OK в буфер сокета, и, наконец, клиент получит записанное OK из буфера сокета. В redis, будь то ReadCallback или WriteCallback, все они выполняются одним потоком.Если они приходят одновременно, то должны ставиться в очередь.Это режим по умолчанию до redis6.0, а также наиболее распространенный .однопоточный редис.

После всего процесса можно обнаружить, что основная программа Reactor работает очень быстро, потому что ей не нужно выполнять реальное чтение и запись, а все остальное делает рабочая программа: чтение и запись ввода-вывода, разбор команд, выполнение команд , и возврат результатов.. , этот момент очень важен.

мультиплексор ввода/вывода

Из вышеизложенного мы знаем, что Reactor — это абстрактная теория и шаблон, как его реализовать? Как отслеживать приход событий сокета? . Самый простой способ - это опрос. Поскольку мы не знаем, когда приходит событие сокета, мы продолжаем спрашивать ядро. Предполагая, что сейчас есть соединения сокета 1w, мы должны запрашивать ядро ​​1w раз в цикле, что, очевидно, очень дорогой.

Переход из пользовательского режима в режим ядра предполагает переключение контекста (контекста).ЦП должен защищать сцену.Перед входом в ядро ​​нужно сохранить состояние регистра, а состояние нужно восстановить из регистра после ядро возвращается Это много накладных расходов.

Из-за чрезмерных накладных расходов при традиционном методе опроса появляется мультиплексор ввода-вывода.select,poll,evport,kqueue,epoll. Redis определяет соответствующие правила с помощью макроса #include в исходном коде реализации программы мультиплексирования ввода-вывода.Программа автоматически выберет библиотеку функций мультиплексирования ввода-вывода с наивысшей производительностью в системе в качестве библиотеки функций мультиплексирования ввода-вывода. Redis во время компиляции Низкоуровневая реализация мультиплексора /O:

// Include the best multiplexing layer supported by this system. The following should be ordered by performances, descending.
# ifdef HAVE_EVPORT
# include "ae_evport.c"
# else
    # ifdef HAVE_EPOLL
    # include "ae_epoll.c"
    # else
        # ifdef HAVE_KQUEUE
        # include "ae_kqueue.c"
        # else
        # include "ae_select.c"
        # endif
    # endif
# endif

Здесь мы в основном представляем два очень классических мультиплексора.selectиepoll, select — это мультиплексор ввода-вывода первого поколения, как select решает проблему постоянного опроса из пользовательского режима в режим ядра?

select

Поскольку каждый опрос доставляет много хлопот, select одновременно передает ядру коллекцию fds пакета сокетов, а затем ядро ​​самостоятельно проходит через fds, а затем оценивает статус доступности для чтения и записи для каждого fd. определенный fd удовлетворен, пользователь должен судить сам.

fds = []int{fd1,fd2,...}
for {
	select (fds)
	for i:= 0; i < len(fds); i++{
		if isReady(fds[i]) {
			   read()
     }
	 }
}

Недостатки выбора: когда процесс прослушивает несколько сокетов, select добавит очередь ожидания всех сокетов в ядре к процессу (многие к одному), так что, когда в одном из сокетов есть данные, он сообщит процессору в момент в то же время разбудите процесс из состояния блокировки, дождитесь планирования процессором и удалите процесс из очереди ожидания всех сокетов.Когда процессор запускает процесс, сам процесс проходит в пакете наборов fds, Мы не знаем, в каком fd есть данные, поэтому мы можем пройти их все только один раз, поэтому для fd, у которого нет данных, они будут потрачены впустую. Поскольку каждый выбор должен пройти через набор сокетов, большое количество наборов сокетов повлияет на общую эффективность, поэтому select поддерживает максимум 1024 параллелизма.

epoll

Если есть способ избежать обхода всех сокетов, когда приходит сообщение из сокета, вместо слепого опроса нужно активировать только соответствующий fd сокета, будет ли эффективность выше? Появление epoll должно решить эту проблему:

epfd = epoll_create()
epoll_ctl(epfd, fd1, fd2...)
for {
  epoll_wait()
  for fd := range fds {
    doSomething()
  }
}
  1. Сначала создайте объект epoll с помощью epoll_create, который вернет дескриптор fd, который, как и дескриптор сокета, также управляется в коллекции fds.
  2. Через epoll_ctl привяжите сокет fd, который нужно отслеживать, к объекту epoll.
  3. Получите сокет fd с данными через epoll_wait.Если сокет не имеет данных, он заблокируется здесь.Если есть данные, он вернет коллекцию fds с данными.
Как это делает epoll?

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

Эволюция от однопоточного к многопоточному

один поток

В сочетании с идеей Reactor и высокопроизводительным режимом ввода-вывода epoll Redis разработал высокопроизводительную архитектуру сетевого ввода-вывода:Однопоточное мультиплексирование ввода-вывода, мультиплексор ввода-вывода отвечает за получение сетевых событий ввода-вывода, а события, наконец, помещаются в очередь в очередь для обработки.Это самая примитивная однопоточная модель.Зачем использовать один поток? Потому что однопоточный редис уже может достигать нагрузки в 10w qps (если делать какие-то сложные операции по сбору, она будет снижена), что удовлетворяет большинству сценариев приложений, а однопоточному не нужно учитывать проблему блокировки, вызванную многопоточным Чтобы удовлетворить ваши требования, вы также можете настроить режим сегментирования, чтобы разрешить разным узлам обрабатывать разные ключи сегментирования, чтобы грузоподъемность вашего сервера Redis могла линейно увеличиваться по мере роста узлов.

Асинхронный поток

Есть такая проблема в однопоточном режиме, когда на удаление большого набора или хэша (не непрерывной памяти) уходит очень много времени, то производительность однопоточного такова, что другим командам, которые еще стоят в очереди, приходится ждать. Когда все больше и больше заказов ждут, случаются плохие вещи. Поэтому redis4.0 создал асинхронный поток для удаления больших ключей. Используйте unlink вместо del для выполнения удаления, поэтому, когда мы отключим связь, Redis обнаружит, нужно ли помещать удаленный ключ в асинхронный поток для выполнения (например, количество наборов превышает 64...), если значение равно достаточно большой, то он будет обрабатываться в асинхронном потоке и не повлияет на основной поток. Точно так же и flushall, и flushdb поддерживают асинхронный режим. Кроме того, Redis также поддерживает режим того, требуются ли асинхронные потоки для обработки в определенных сценариях (по умолчанию закрыты):

lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no
  • lazyfree-lazy-eviction: при наличии стратегии исключения, которая устанавливает максимальную память для Redis, в это время будет запущено асинхронное удаление.Недостаток асинхронного удаления в этом сценарии заключается в том, что если удаление несвоевременно, память не может быть освобождены вовремя.
  • lazyfree-lazy-expire: Для ключей с ttl при их очистке redis не выполняется синхронное удаление, а для их удаления добавляются асинхронные потоки.
  • replica-lazy-flush: когда ведомый узел присоединяется, он выполняет сброс для очистки своих данных.Если сброс занимает много времени, больше данных будет накапливаться в буфере репликации, и более поздний ведомый будет синхронизировать данные относительно Включить реплику После -lazy-flush сброс слейва можно обрабатывать асинхронным реди-мейдом, тем самым повышая скорость синхронизации.
  • lazyfree-lazy-server-del: эта опция выполняется для некоторых инструкций, таких как переименование поля.RENAME key newkey, Если новый ключ существует в это время, он удалит старое значение нового ключа для переименования. Если старое значение слишком велико, это вызовет блокировку. Когда этот параметр включен, он также будет передан асинхронному потоку. ... работать так, чтобы основной поток не блокировался.

Многопоточность

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

# io-threads 4 # work线程数
# io-threads-do-reads no # 是否开启

Какова роль многопоточности?

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

So for instance if you have a four cores boxes, try to use 2 or 3 I/O threads, if you have a 8 cores, try to use 6 threads.

Если он включен, то официально рекомендуется открывать 2-3 потока ввода-вывода для 4-ядерной машины, если ядер 8, то можно открыть 6 потоков ввода-вывода.

Принцип многопоточности

Следует отметить, что многопоточность Redis обрабатывает только чтение и запись ввода-вывода сокета.

  1. Сервер redis отслеживает клиентский запрос через EventLoop, при поступлении запроса основной поток не сразу его парсит и выполняет, а помещает в глобальную очередь чтения client_pending_read и помечает каждого клиента флагом CLIENT_PENDING_READ.
  2. Затем основной поток распределяет все задачи между потоком ввода-вывода и самим основным потоком с помощью стратегии RR (Round-robin).
  3. Каждый поток (включая основной поток и подпотоки) только читает и анализирует параметры запроса через клиентский флаг CLIENT_PENDING_READ в соответствии с назначенными задачами (команды здесь не выполняются).
  4. Основной поток будет занят опросом и ожиданием завершения выполнения всех потоков ввода-вывода. Каждый поток ввода-вывода будет поддерживать локальную очередь io_threads_list и локальный атомарный счетчик io_threads_pending. Задачи между потоками изолированы и не будут перекрываться. Когда поток ввода-вывода завершает выполнение задача После этого io_threads_pending[index] = 0, когда все io_threads_pending равны 0, это когда задача завершена.
  5. После выполнения всех операций чтения основной поток выполняется путем обхода очереди client_pending_read.настоящий исполнительдействие.
  6. После завершения чтения, разбора и выполнения команды результат должен быть отправлен клиенту. Основной поток добавит клиента, которому необходимо ответить, в глобальную очередь client_pending_write.
  7. Основной поток проходит через очередь client_pending_write, а затем распределяет все задачи между потоком ввода-вывода и основным потоком с помощью стратегии RR (циклического перебора), позволяя им записывать данные обратно клиенту.

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