Устранение неполадок и анализ Nginx OOM для стресс-теста соединения длиной в миллион

Nginx оптимизация производительности

В недавнем стресс-тесте с соединением длиной в миллион четыре устройства Nginx 32C 128G часто испытывали OOM, а мониторинг памяти при возникновении проблемы был следующим.

Процесс осмотра записывается следующим образом.

Симптом

Это среда стресс-тестирования для отправки и получения сообщений с помощью веб-сокета миллионов длинных подключений.Клиентский jmeter использует сотни машин и проходит через четыре Nginx к серверной службе.Упрощенная структура развертывания показана на рисунке ниже.

nginx oom

При поддержании миллионов подключений без отправки данных все нормально и память Nginx стабильна. При отправке и получении большого количества данных память Nginx начинает расти со скоростью сотни мегабайт в секунду, пока занимаемая память не приблизится к 128 ГБ, а процесс пробуждения начинает часто убивать систему OOM. Каждый из 32 рабочих процессов занимает около 4 ГБ памяти. Вывод dmesg -T показан ниже.

[Fri Mar 13 18:46:44 2020] Out of memory: Kill process 28258 (nginx) score 30 or sacrifice child
[Fri Mar 13 18:46:44 2020] Killed process 28258 (nginx) total-vm:1092198764kB, anon-rss:3943668kB, file-rss:736kB, shmem-rss:4kB

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

Анализ процесса устранения неполадок

Чтобы получить этот вопрос, сначала проверьте состояние сетевого соединения между Nginx и клиентом, используйтеss -ntКоманда видит, что Send-Q большого количества ESTABLISH-подключений в Nginx очень велик, а Recv-Q клиента очень велик. Вывод секции ss на стороне Nginx показан ниже.

State      Recv-Q Send-Q Local Address:Port     Peer Address:Port
ESTAB      0      792024 1.1.1.1:80               2.2.2.2:50664
...

Иногда вы можете увидеть больше нулевых окон при захвате пакетов на клиенте jmeter, как показано ниже.

На данный момент есть несколько основных направлений.Первое подозрение, что jmeter-клиент имеет ограниченную вычислительную мощность, а в транзитном Nginx скапливается много сообщений.

Для проверки идеи найдите способ сделать дамп памяти nginx. Поскольку дамп памяти может легко завершиться ошибкой при условии высокого использования памяти на более позднем этапе, дамп начинается вскоре после того, как объем памяти начинает увеличиваться.

Сначала используйте pmap для просмотра распределения памяти любого из рабочих процессов, здесь 4199, результат использования команды pmap следующий.

pmap -x  4199 | sort -k 3 -n -r

00007f2340539000  475240  461696  461696 rw---   [ anon ]
...

затем используйтеcat /proc/4199/smaps | grep 7f2340539000Найдите начальный и конечный адреса сегмента памяти, как показано ниже.

cat /proc/3492/smaps  | grep 7f2340539000

7f2340539000-7f235d553000 rw-p 00000000 00:00 0

Затем используйте gdb для подключения к процессу и сделайте дамп памяти.

gdb -pid 4199

dump memory memory.dump 0x7f2340539000 0x7f235d553000

Затем используйте команду strings для просмотра читаемого строкового содержимого этого файла дампа, вы можете увидеть большое количество содержимого запросов и ответов.

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

location / {
    proxy_pass http://xxx;
    proxy_set_header    X-Forwarded-Url  "$scheme://$host$request_uri";
    proxy_redirect      off;
    proxy_http_version  1.1;
    proxy_set_header    Upgrade $http_upgrade;
    proxy_set_header    Connection "upgrade";
    proxy_set_header    Cookie $http_cookie;
    proxy_set_header    Host $host;
    proxy_set_header    X-Forwarded-Proto $scheme;
    proxy_set_header    X-Real-IP $remote_addr;
    proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
    client_max_body_size        512M;
    client_body_buffer_size     64M;
    proxy_connect_timeout       900;
    proxy_send_timeout          900;
    proxy_read_timeout          900;
    proxy_buffer_size        64M;
    proxy_buffers            64 16M;
    proxy_busy_buffers_size        256M;
    proxy_temp_file_write_size    512M;
}

Вы можете видеть, что значение proxy_buffers установлено очень большим. Далее давайте смоделируем влияние непостоянства скорости отправки и получения исходящего и входящего потоков на использование памяти Nginx.

Имитация увеличения памяти Nginx

Здесь я имитирую клиент, который медленно получает пакеты, и внутренний сервер с обильными ресурсами на другой стороне, а затем наблюдаю, изменится ли память Nginx.

nginx_oom_fast_slow

Клиент медленного приема пакетов написан на golang и использует TCP для имитации отправки HTTP-запроса.Код выглядит следующим образом.

package main

import (
	"bufio"
	"fmt"
	"net"
	"time"
)

func main() {
	conn, _ := net.Dial("tcp", "10.211.55.10:80")
	text := "GET /demo.mp4 HTTP/1.1\r\nHost: ya.test.me\r\n\r\n"

	fmt.Fprintf(conn, text)
	for ; ; {
		_, _ = bufio.NewReader(conn).ReadByte()
		time.Sleep(time.Second * 3)
		println("read one byte")
	}
}

Запустите pidstat на тестовом Nginx для отслеживания изменений памяти

pidstat -p pid -r 1 1000

Запустив приведенный выше код golang, изменения памяти рабочего процесса Nginx выглядят следующим образом.

04:12:13 — время запуска программы golang.Видно, что за очень короткий промежуток времени использование памяти Nginx выросло до 464136 КБ (около 450 МБ) и будет оставаться в течение длительного времени. время.

В то же время стоит отметить, что размер настройки proxy_buffers указан для одного подключения, при отправке нескольких подключений использование памяти будет продолжать расти. Ниже приведен результат одновременного запуска двух процессов golang в памяти Nginx.

Видно, что при подключении двух медленных клиентов память поднялась до более чем 900 Мб.

решение

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

proxy_buffering off;

После реальных измерений, после изменения этого значения в среде стресс-теста и уменьшения значения proxy_buffer_size, память стабилизировалась на уровне около 20G и больше не взлетела Скриншот использования памяти показан ниже.

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

Повторите тест прямо сейчас в тестовой среде, и результаты будут следующими.

Видно, что на этот раз объем памяти увеличился примерно на 64M. Почему это увеличение на 64M? Взгляните на документацию Nginx для proxy_buffering (Nginx.org/en/docs/red-glowing…

When buffering is enabled, nginx receives a response from the proxied server as soon as possible, saving it into the buffers set by the proxy_buffer_size and proxy_buffers directives. If the whole response does not fit into memory, a part of it can be saved to a temporary file on the disk. Writing to temporary files is controlled by the proxy_max_temp_file_size and proxy_temp_file_write_size directives.

When buffering is disabled, the response is passed to a client synchronously, immediately as it is received. nginx will not try to read the whole response from the proxied server. The maximum size of the data that nginx can receive from the server at a time is set by the proxy_buffer_size directive.

Видно, что при включенном proxy_buffering Nginx будет получать и хранить как можно больше контента, возвращаемого бэкенд-сервером, в свой собственный буфер, максимальный размер которого равен proxy_buffer_size * proxy_buffers памяти.

Если сообщение, возвращаемое бэкендом, очень большое, память не поместится, и оно будет помещено в файл на диске. Временные файлы определяются двумя директивами proxy_max_temp_file_size и proxy_temp_file_write_size, которые здесь не раскрываются.

Когда proxy_buffering выключен, Nginx не будет считывать как можно больше данных с прокси-сервера, а будет считывать максимум данных proxy_buffer_size за раз и отправлять их клиенту.

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

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

Анализ исходного кода Nginx

Исходный код чтения ответа бэкенда и записи его в локальный буфер находится вsrc/event/ngx_event_pipe.cв методе ngx_event_pipe_read_upstream. Этот метод в конечном итоге вызовет ngx_create_temp_buf для создания буфера памяти. Количество созданных раз и размер каждого буфера задаются Определяемые p->bufs.num (количество буферов) и p->bufs.size (размер каждого буфера), эти два значения являются значениями параметров proxy_buffers, которые мы указали в файле конфигурации. Эта часть исходного кода показана ниже.

static ngx_int_t
ngx_event_pipe_read_upstream(ngx_event_pipe_t *p)
{
    for ( ;; ) {

        if (p->free_raw_bufs) {
            // ...
        } else if (p->allocated < p->bufs.num) { // p->allocated 目前已分配的缓冲区个数,p->bufs.num 缓冲区个数最大大小
            /* allocate a new buf if it's still allowed */
            b = ngx_create_temp_buf(p->pool, p->bufs.size); // 创建大小为 p->bufs.size 的缓冲区
            if (b == NULL) {
                return NGX_ABORT;
            }
            p->allocated++;
        } 
    }
}

Интерфейс отладки исходного кода Nginx выглядит следующим образом.

постскриптум

В этом процессе также есть некоторые вспомогательные методы оценки, такие как отслеживание процесса выделения и освобождения памяти с помощью инструментов strace и systemtap, которые здесь не раскрываются.Эти инструменты являются артефактами для анализа программ черного ящика.

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

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