Подход к исходному коду: процесс запуска Redis

Redis
Подход к исходному коду: процесс запуска Redis

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

Redis можно условно разделить на две части: серверную и клиентскую (читатели жалуются: вы слишком общие). При использовании мы сначала запускаем сервер, а затем запускаем клиент. Клиент отправляет команду на сервер, и сервер возвращает результат клиенту после обработки. Начнем с «головы» и посмотрим, что делает сервер Redis при запуске.

Для языка C основной функцией является запись программы, Redis не исключение. Основная функция Redis записана в файле server.c. Поскольку Redis запускает сложный процесс, необходимо определить ряд условий, таких как кластер, режим сторожевого и т. д. Мы вводим только одиночный Redis, запускающий процесс некоторых из наиболее важных шагов.

Инициализировать глобальное состояние сервера

Если параметр test используется при запуске команды redis-server, указанный тест будет выполнен первым. Далее вызывается функция initServerConfig(), которая инициализирует сервер глобальной переменной типа redisServer. В структуре redisServer очень много полей, из-за нехватки места мы их здесь перечислять не будем, если их разделить на категории, то их можно разделить на следующие категории:

  • General
  • Modules
  • Networking
  • RDB / AOF loading information
  • Fast pointers to often looked up command
  • Fields used only for stats
  • Configuration
  • AOF / RDB persistence
  • Logging
  • Replication
  • Synchronous replication
  • Limits
  • Blocked clients
  • Sort parameters
  • Zip structure config
  • time cache
  • Pubsub
  • Cluster
  • Scripting
  • Lazy free
  • Latency monitor
  • Assert & bug reporting
  • System hardware info

Если функцию initServerConfig() можно описать одним предложением, то она используется для инициализации значения по умолчанию для переменной, которую можно настроить в файле конфигурации (обычно с именем redis.conf). Обычно используемые переменные — это номер порта сервера, уровень журнала и т. д.

набор-рекомендация таблица

В функции initServerConfig() вызывается функция populateCommandTable() для установки таблицы команд сервера Структура таблицы команд следующая.

struct redisCommand redisCommandTable[] = {
    {"module",moduleCommand,-2,"as",0,NULL,0,0,0,0,0},
    {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
    {"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0},
    ...
}

Значение каждого пункта:

  1. имя: имя команды
  2. функция: имя функции, соответствующее команде. Функция, которая будет выполняться, когда redis-сервер обрабатывает команду
  3. arity: количество параметров команды, если это -N, значит больше или равно N
  4. sflags: флаги команд, определяющие тип команды (чтение/запись/администрирование...)
  5. flags: битовая маска, рассчитанная Redis на основе sflags
  6. get_keys_proc: необязательная функция, используемая, когда следующие три элемента не могут указать, какие параметры являются ключами.
  7. first_key_index: первый параметр — это ключ
  8. last_key_index: последний параметр является ключом
  9. key_step: «размер шага» ключа, например, key_step MSET равен 2, потому что его параметры имеют вид key, val, key, val
  10. микросекунды: количество микросекунд, необходимое для выполнения команды
  11. вызовы: общее количество вызовов команды

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

server.delCommand = lookupCommandByCString("del");
server.multiCommand = lookupCommandByCString("multi");
server.lpushCommand = lookupCommandByCString("lpush");
server.lpopCommand = lookupCommandByCString("lpop");
server.rpopCommand = lookupCommandByCString("rpop");
server.zpopminCommand = lookupCommandByCString("zpopmin");
server.zpopmaxCommand = lookupCommandByCString("zpopmax");
server.sremCommand = lookupCommandByCString("srem");
server.execCommand = lookupCommandByCString("exec");
server.expireCommand = lookupCommandByCString("expire");
server.pexpireCommand = lookupCommandByCString("pexpire");
server.xclaimCommand = lookupCommandByCString("xclaim");
server.xgroupCommand = lookupCommandByCString("xgroup");
Инициализировать сторожевой режим

После инициализации переменной путь и параметры команды запуска будут сохранены для следующего перезапуска. Если запущенная служба находится в дозорном режиме, для инициализации дозорного режима вызываются методы initSentinelConfig() и initSentinel(). Студенты, которые не знают о Sentinel, могут прочитать его.здесь. И initSentinelConfig(), и initSentinel() находятся в файле sentinel.c. Функция initSentinelConfig отвечает за инициализацию номера порта дозорного и сброс защищенного режима сервера. Функция initSentinel отвечает за настройку таблицы команд для поддержки только команд sentinel и инициализацию формата данных sentinelState.

Исправить постоянные файлы

Если режим запуска redis-check-rdb/aof, то две функции redis_check_rdb_main() или redis_check_aof_main() будут выполняться для восстановления постоянного файла, но то, что делает функция redis_check_rdb_main, уже было сделано во время процесса запуска Redis, поэтому Здесь ничего делать не нужно, просто сделайте так, чтобы эта функция загрузила ошибку напрямую.

параметры обработки

Если это простой параметр, такой как -v или --version, -h или --help, соответствующий метод будет вызван напрямую, и информация будет напечатана. Если используются другие файлы конфигурации, измените server.exec_argv. Для другой информации они будут преобразованы в строки и добавлены в файл конфигурации, например "--port 6380" будут преобразованы в "порт 6380\n" и добавлены в файл конфигурации. В это время Redis вызовет функцию loadServerConfig() для загрузки файла конфигурации, которая перезапишет значение переменной, которая ранее инициализировала файл конфигурации по умолчанию.

initServer()

Функция initServer() отвечает за завершение инициализации серверной переменной. Сначала настройте обработку сигналов (кроме SIGHUP и SIGPIPE), затем создайте несколько двусторонних списков для отслеживания клиентов, подчиненных устройств и т. д.

server.current_client = NULL;
server.clients = listCreate();
server.clients_index = raxNew();
server.clients_to_close = listCreate();
server.slaves = listCreate();
server.monitors = listCreate();
server.clients_pending_write = listCreate();
server.slaveseldb = -1; /* Force to emit the first SELECT command. */
server.unblocked_clients = listCreate();
server.ready_keys = listCreate();
server.clients_waiting_acks = listCreate();
Shared object

Функция createSharedObjects() создаст некоторые общие объекты и сохранит их в глобальной общей переменной.Для разных команд может быть одно и то же возвращаемое значение (например, ошибка). Таким образом, не нужно каждый раз при возврате добавлять новые объекты и сохранять их в памяти. Этот дизайн требует больше времени при запуске Redis в обмен на меньшую задержку в работе.

shared.crlf = createObject(OBJ_STRING,sdsnew("\r\n"));
shared.ok = createObject(OBJ_STRING,sdsnew("+OK\r\n"));
shared.err = createObject(OBJ_STRING,sdsnew("-ERR\r\n"));
shared.emptybulk = createObject(OBJ_STRING,sdsnew("$0\r\n\r\n"));
shared.czero = createObject(OBJ_STRING,sdsnew(":0\r\n"));
shared.cone = createObject(OBJ_STRING,sdsnew(":1\r\n"));
shared.cnegone = createObject(OBJ_STRING,sdsnew(":-1\r\n"));
shared.nullbulk = createObject(OBJ_STRING,sdsnew("$-1\r\n"));
shared.nullmultibulk = createObject(OBJ_STRING,sdsnew("*-1\r\n"));
shared.emptymultibulk = createObject(OBJ_STRING,sdsnew("*0\r\n"));
shared.pong = createObject(OBJ_STRING,sdsnew("+PONG\r\n"));
shared.queued = createObject(OBJ_STRING,sdsnew("+QUEUED\r\n"));
shared.emptyscan = createObject(OBJ_STRING,sdsnew("*2\r\n$1\r\n0\r\n*0\r\n"));
shared.wrongtypeerr = createObject(OBJ_STRING,sdsnew(
    "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n"));
shared.nokeyerr = createObject(OBJ_STRING,sdsnew(
    "-ERR no such key\r\n"));
Shared integers

В дополнение к некоторым из приведенных выше возвращаемых значений функция createSharedObjects() также создает некоторые общие целочисленные объекты. Для Redis существует множество типов (таких как списки или наборы), для которых требуются некоторые целые числа (например, количества), и эти уже созданные целочисленные объекты можно повторно использовать без перераспределения памяти и их создания. Это снова жертвует временем запуска ради времени выполнения.

Добавить повторяющиеся события

Функция initServer() вызывает функцию aeCreateEventLoop() (файл ae.c) для добавления событий цикла и возвращает результат элементу el сервера. Redis использует разные функции для совместимости с каждой платформой, epoll используется на платформе Linux, kqueue используется на BSD, если нет, в конечном итоге будет использоваться select. Redis опрашивает новые подключения и события ввода-вывода и вовремя реагирует на поступление новых событий.

База данных распределения

Redis инициализирует необходимую базу данных и присваивает результат элементу db сервера.

server.db = zmalloc(sizeof(redisDb)*server.dbnum);
Прослушивание TCP-порта

listenToPort() используется для инициализации некоторых файловых дескрипторов для прослушивания адреса и порта, настроенных сервером. Функция listenToPort определит, следует ли прослушивать IPv4 или IPv6 в соответствии с адресом в параметре, и соответственно вызовет функцию anetTcpServer() или anetTcp6Server().Если адрес не указан в параметре, он будет принудительно привязан к 0.0 .0.0

Инициализировать пул ключей LRU

evictionPoolAlloc() (в файле evict.c) используется для инициализации пула ключей LRU, а политика истечения срока действия ключей Redis представляет собой приблизительный алгоритм LRU.

void evictionPoolAlloc(void) {
    struct evictionPoolEntry *ep;
    int j;

    ep = zmalloc(sizeof(*ep)*EVPOOL_SIZE);
    for (j = 0; j < EVPOOL_SIZE; j++) {
        ep[j].idle = 0;
        ep[j].key = NULL;
        ep[j].cached = sdsnewlen(NULL,EVPOOL_CACHED_SDS_SIZE);
        ep[j].dbid = 0;
    }
    EvictionPoolLRU = ep;
}
Server cron

Затем функция initServer() сгенерирует еще несколько списков и словарей для базы данных и pub/sub, сбросит некоторые состояния и отметит время запуска системы. После этого Redis выполнит функцию aeCreateTimeEvent() (в файле ae.c), чтобы создать новое событие, которое проходит через функцию serverCron(). serverCron() по умолчанию выполняется каждые 100 миллисекунд.

 /* Create the timer callback, this is our way to process many background
  * operations incrementally, like clients timeout, eviction of unaccessed
  * expired keys and so forth. */
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
    serverPanic("Can't create event loop timers.");
    exit(1);
}

Видно, что когда в коде создается событие цикла, функция serverCron() указана для выполнения каждую миллисекунду, что означает немедленный запуск цикла, но будет использоваться возвращаемое значение функции serverCron(). в качестве временного интервала для следующего выполнения. По умолчанию 1000/server.hz. server.hz ​​увеличивается с количеством клиентов.

Функция serverCron() выполняет множество задач, которые выполняются периодически, включая повторное хэширование, сохранение в фоновом режиме, повторную очистку AOF, очистку ключей с истекшим сроком действия, обмен виртуальной памятью, синхронизацию главных и подчиненных узлов и т. д. Короче говоря, почти все временные задачи Redis, о которых можно подумать, обрабатываются в функции serverCron().

Открытие файлов AOF
/* Open the AOF file if needed. */
if (server.aof_state == AOF_ON) {
    server.aof_fd = open(server.aof_filename,
                         O_WRONLY|O_APPEND|O_CREAT,0644);
    if (server.aof_fd == -1) {
        serverLog(LL_WARNING, "Can't open the append-only file: %s",
                  strerror(errno));
        exit(1);
    }
}
максимальный предел памяти

Для 32-разрядных систем максимальный объем памяти составляет 4 ГБ. Если пользователь не указывает максимальный объем памяти, который Redis может использовать, ограничение по умолчанию составляет 3 ГБ.

/* 32 bit instances are limited to 4GB of address space, so if there is
     * no explicit limit in the user provided configuration we set a limit
     * at 3 GB using maxmemory with 'noeviction' policy'. This avoids
     * useless crashes of the Redis instance for out of memory. */
if (server.arch_bits == 32 && server.maxmemory == 0) {
    serverLog(LL_WARNING,"Warning: 32 bit instance detected but no memory limit set. Setting 3 GB maxmemory limit with 'noeviction' policy now.");
    server.maxmemory = 3072LL*(1024*1024); /* 3 GB */
    server.maxmemory_policy = MAXMEMORY_NO_EVICTION;
}
Запуск Redis-сервера

Если Redis настроен на работу в фоновом режиме, Redis попытается записать файл pid.Путь по умолчанию — /var/run/redis.pid. На данный момент сервер Redis запущен, но осталось еще кое-что сделать.

Загрузить данные с диска

Если есть файл AOF или файл дампа (файл AOF имеет более высокий приоритет, если оба существуют), функция loadDataFromDisk() отвечает за загрузку данных с диска в память.

окончательные настройки

Каждый раз при входе в событие цикла вызывается функция beforeSleep(), которая делает следующее:

  • Если сервер является узлом в кластере, вызовите функцию clusterBeforeSleep().
  • выполнить быстрый цикл
  • Если клиент был заблокирован в предыдущем событии цикла, отправьте запрос ACK всем подчиненным узлам.
  • Разблокировать заблокированное состояние клиента, который был заблокирован во время синхронного резервного копирования
  • Проверьте, не заблокированы ли клиенты из-за блокирующих команд, если да, разблокируйте их.
  • Запишите буфер AOF на диск
  • Тема выпускает GIL
Введите событие основного цикла

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

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
    }
}

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

参考:Redis: under the hood