В последнее время на линии появилась задержка отклика интерфейса P99, которая сильно колеблется, и позже она была оптимизирована. График линии задержки ответа выглядит следующим образом:
После того, как оптимизация была завершена около 11:00 11 декабря, P99 стабилизировался со средним значением около 70 мс.
Поговорим о процессе оптимизации.
1 Подумайте о процессе выполнения интерфейса
Всего этот интерфейс пройдет через три службы и, наконец, вернется к клиенту. Последовательность выполнения следующая:
В соответствии с процессом, показанным стрелкой, сначала осуществляется доступ к сервису 1, и на уровень интерфейса возвращается результат сервиса 1. При запросе сервиса 2 сервис 2 запрашивает сервис 3, а затем возвращает результат на уровень интерфейса.
2 Анализ
Затем наблюдались соответственно сервис 1, сервис 2 и сервис 3. Основные наблюдаемые показатели следующие:
- Задержка внешнего ответа службы
- Загрузка процессора
- сетевой джиттер
После наблюдения с этими показателями службы 2 и службы 3 все в порядке.
Колебание внешней задержки ответа сервиса 2 очень похоже на флуктуацию интерфейса, а затем анализирует сервис 2. Служба 2 — это служба с интенсивным вводом-выводом со средним числом запросов в секунду около 3 тыс.
К основным операциям ввода-вывода относятся:
- Одноточечное чтение Redis
- Чтение из кластера Redis
- чтение базы данных
- Вытягивание двух http-интерфейсов
- звонок в другую службу
Отклик кластера Redis быстрый, в среднем около 5 мс (плюс потребление сети туда-сюда), база данных около 10 мс, у http-интерфейса только случайные медленные запросы, а вызовы других служб не вызывают проблем.
Наконец, обнаружено, что время отклика одноточечного Redis слишком велико.
Как показано на рисунке, каждый запрос, полученный службой 2, трижды обращается к одноточечному Redis, и три раза в сумме составляют почти 100 мс, а затем анализируют одноточечный Redis.
Обнаружено, что ЦП этого Redis имеет следующую тенденцию колебаний
В основном он колеблется каждую минуту.
Я сразу отреагировал на то, что bgsave включили (в основном bgsave раз в минуту), потому что у меня был подобный опыт раньше, я просто выключил bgsave и снова наблюдал.
Пока бизнес стабилизировался.
3 решения
Онлайн-бгсейв нельзя закрывать постоянно, в случае сбоя будет потерян большой объем данных.
Конкретные планы таковы:
- Сначала откройте bgsave этой машины
- Подать заявку на подчиненный сервер и синхронизировать данные с этой машины
- После завершения синхронизации главный узел закрывает bgsave, а подчиненный узел открывает bgsave.
Таким образом, bgsave больше не влияет на чтение и запись главного узла, а подчиненный узел также можно использовать для предотвращения потери данных.
4 Исследование причины колебания ЦП, вызванного bgsave
Прежде всего, поговорим о механизме выполнения bgsave. При выполнении bgsave (независимо от того, каким образом он выполняется) дочерний процесс будет разветвлен первым, и дочерний процесс запишет снимок базы данных на жесткий диск, а родительский процесс продолжит обработку запроса клиента.
Итак, когда в обычное время нет bgsave, статус процесса выглядит следующим образом:
При bgsave статус процесса следующий:
Верхний ЦП, занимающий 100%, — это дочерний процесс от форка, который при выполнении bgsave полностью монополизирует ЦП (красное поле вверху).
Следовательно, делается вывод, что флуктуация этого ЦП нормальна, и каждый пик вызван дочерним процессом bgsave.
5. Интерфейс, соответствующий исследованию задержки, вызванной bgsave
По поводу форка на официальном сайте redis есть такое описание:
RDB disadvantages
- RDB is NOT good if you need to minimize the chance of data loss in case Redis stops working (for example after a power outage). You can configure different save points where an RDB is produced (for instance after at least five minutes and 100 writes against the data set, but you can have multiple save points). However you'll usually create an RDB snapshot every five minutes or more, so in case of Redis stopping working without a correct shutdown for any reason you should be prepared to lose the latest minutes of data.
- RDB needs to fork() often in order to persist on disk using a child process. Fork() can be time consuming if the dataset is big, and may result in Redis to stop serving clients for some millisecond or even for one second if the dataset is very big and the CPU performance not great. AOF also needs to fork() but you can tune how often you want to rewrite your logs without any trade-off on durability.
Здесь упоминаются недостатки RDB, а второй пункт объясняет проблемы, которые вызовет форк.
Общая идея такова: для сохранения данных на жестком диске RDB необходимо часто разветвлять дочерний процесс. Если набор данных слишком велик, выполнение fork() может занять очень много времени.Если набор данных очень велик, это может привести к тому, что сервер Redis сгенерирует отказ в обслуживании на несколько миллисекунд или даже секунд, и производительность процессора резко упадет.
Продолжительность этой паузы зависит от системы, в которой находится Redis.Для реального оборудования, виртуальных машин VMWare или виртуальных машин KVM время для дочерних процессов fork увеличивается на 10-20 мс на каждый 1 ГБ памяти, занимаемый процессом Redis.Для Xen виртуальные машины С точки зрения машины, каждый раз, когда процесс Redis занимает 1 ГБ памяти, время разветвления дочернего процесса должно увеличиваться на 200–300 мс.
Но для Redis с большим количеством обращений 10-20 мс — это долго (наш Redis занимает около 10 Г памяти, а предполагаемое время паузы — около 100 мс).
На данный момент причина задержки ответа интерфейса ясна:
Так как redis работает в одном процессе, то при разветвлении дочернего процесса, если это займет слишком много времени, сервер остановится, и redis не сможет продолжать обработку запросов, что в дальнейшем приведет к тому, что все клиенты отправят запросы к redis зависнет, и реакция интерфейса изменится.медленно.
6 Углубленный анализ вилочного механизма
Узнав причину, давайте взглянем на исходный код (часть форка) redis, выполняющего bgsave:
Если комментарии будут проанализированы, застрявшая вилка вызовет удар.
// 执行bgsave
int rdbSaveBackground(char * filename, rdbSaveInfo * rsi) {
pid_t childpid;
long long start;
if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
server.dirty_before_bgsave = server.dirty;
server.lastbgsave_try = time(NULL);
openChildInfoPipe();
// 记录执行fork的起始时间,用于计算fork的耗时
start = ustime();
// 在这里执行fork !!
// 由此可见,如果fork卡住,下面执行父进程的else条件就会卡住,子进程的执行也需要fork完成后才会开始
if ((childpid = fork()) == 0) {
// fork()返回了等于0的值,说明执行成功,
int retval;
// 下面是子进程的执行过程
/* Child */
closeListeningSockets(0);
redisSetProcTitle("redis-rdb-bgsave");
// 子进程执行硬盘的写操作
retval = rdbSave(filename, rsi);
if (retval == C_OK) {
size_t private_dirty = zmalloc_get_private_dirty( - 1);
if (private_dirty) {
serverLog(LL_NOTICE, "RDB: %zu MB of memory used by copy-on-write", private_dirty / (1024 * 1024));
}
server.child_info_data.cow_size = private_dirty;
sendChildInfo(CHILD_INFO_TYPE_RDB);
}
// 子进程执行完毕退出,返回执行结果给父进程,0 - 成功,1 - 失败
exitFromChild((retval == C_OK) ? 0 : 1);
} else {
// 下面是父进程的执行过程
/* Parent */
// 计算fork的执行时间
server.stat_fork_time = ustime() - start;
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024 * 1024 * 1024);
/* GB per second. */
latencyAddSampleIfNeeded("fork", server.stat_fork_time / 1000);
if (childpid == -1) { // fork出错,打印错误日志
closeChildInfoPipe();
server.lastbgsave_status = C_ERR;
serverLog(LL_WARNING, "Can't save in background: fork: %s", strerror(errno));
return C_ERR;
}
serverLog(LL_NOTICE, "Background saving started by pid %d", childpid);
server.rdb_save_time_start = time(NULL);
server.rdb_child_pid = childpid;
server.rdb_child_type = RDB_CHILD_TYPE_DISK;
updateDictResizePolicy();
return C_OK;
}
return C_OK;
/* unreached */
}
Описание возвращаемого значения метода fork():
Return Value
On success, the PID of the child process is returned in the parent, and 0 is returned in the child. On failure, -1 is returned in the parent, no child process is created, and errno is set appropriately.
Это означает, что в случае успешного форка PID процесса будет возвращен родительскому процессу, а дочернему процессу из форка будет возвращен 0. Если вилка не удалась, верните -1 родительскому процессу, дочерний процесс не создается и установите код системной ошибки.
Видно, что поток выполнения fork выглядит следующим образом:
Давайте посмотрим на меры предосторожности для fork() в Linux.
Notes
Under Linux, fork() is implemented using copy-on-write pages, so the only penalty that it incurs is the time and memory required to duplicate the parent's page tables, and to create a unique task structure for the child. Since version 2.3.3, rather than invoking the kernel's fork() system call, the glibc fork() wrapper that is provided as part of the NPTL threading implementation invokes clone(2) with flags that provide the same effect as the traditional system call. (A call to fork() is equivalent to a call to clone(2) specifying flags as just SIGCHLD.) The glibc wrapper invokes any fork handlers that have been established using pthread_atfork(3).
Первый абзац описывает некоторые проблемы с fork(). Суть в следующем:
В системе Linux fork() реализуется стратегией копирования при записи, поэтому проблема, которую он принесет, заключается в том, что время и память, необходимые для копирования родительского процесса и создания уникальной структуры процесса для дочернего процесса.
7 Дополнение - модель процесса памяти
Ядро системы откроет пространство виртуальной памяти для каждого процесса, которое распределяется следующим образом.
Дочерний процесс fork эквивалентен клону родительского процесса, видно, что если в родительском процессе имеется большой объем данных, время клонирования будет больше.
Справочная документация
- Redis in Action. Josiah L. Carison
- Официальный сайт Redis: Redis Persistence, ссылка: https://redis.io/topics/persistence
- Исходный код Redis: rdb.c, ссылка: https://github.com/antirez/redis/blob/unstable/src/rdb.c
- Справочная страница Linux: fork(), ссылка: https://linux.die.net/man/2/fork
Добро пожаловать в мой публичный аккаунт WeChat