
У одного из клиентов нашей системы мониторинга PostgreSQL серверов возникла проблема сильного замедления запросов при запуске базы в Docker. В этой статье расскажем о возможных последствиях использования PostgreSQL в Docker с конфигурацией по умолчанию.
Клиент обратился с проблемой - тормозит интерфейс при отображении логов. Анализ показал, что причиной является долгое выполнение запроса (приводим в сокращенном виде):
SELECT rc.pack , rc.recno , rc.ts , rc.type , rc.duration , coalesce(rc.unparsed, '') unparsed , rc.dt::text , get_rawdata_str(rc.dt, rc.pack, rc.recno) "text" , ( SELECT ... ) query , regexp_replace(( SELECT ... )::text, '^\[(.*)\]$', '\1') context , ( SELECT ... ) parameters , ( SELECT ... ) exectime , ( SELECT ... ) plan , ( SELECT ... ) "lock" , err.error , err.errargs , err.msg FROM record rc LEFT JOIN LATERAL( SELECT ... ) err ON TRUE WHERE (rc.pack, rc.dt) = ($1::uuid, $2::date) ORDER BY recno;
Загружаем план запроса на explain.tensor.ru :

Видим, что все время ушло на корневой узел "Nested Loop Left Join" .
Это произошло в результате вызова функций в столбцах корневого JOIN, но там или get_rawdata_str или regexp_replace . При этом обе функции не отображаются в плане, а все их время и ресурсы отражены в корневом узле:

Так как база тестовая и данных немного, то regexp_replace не может потреблять много ресурсов.
Проверяем PL/pgSQL функцию get_rawdata_str - она генерирует свой план, но он не отображается при вызове EXPLAIN ANALYZE . Получить план запроса из такой функции возможно лишь с помощью модуля auto_explain при включенном параметре:
SET auto_explain.log_nested_statements = on;
В этом случае в логе будут записи типа CONTEXT с планом вызываемой функции:
CONTEXT: SQL function "get_rawdata_str" statement 1
Включаем этот параметр, загружаем план запроса из тела функции:

Видим что сам запрос отрабатывает за 309 мс, а вот JIT еще 1.7 сек :

Но почему у клиента включился JIT, а на наших серверах нет ?
Вообще JIT появился в 11 версии, здесь было обсуждение по поводу его включения.
В 11 версии он остался выключенным по умолчанию, а во всех версиях, начиная с 12, JIT включен.
При этом кроме опций конфига для использования JIT требуется соблюдение нескольких условий:
сборка PG должна быть произведена с опцией --with-llvm , в официальных пакетах это так и есть, а в случае сборки из исходников наличие опции проверить можно командой:
pg_config --configure | grep with-llvm
наличие провайдера JIT, по умолчанию это llvmjit , который устанавливается из пакета
postgresql-llvmjit.
При развороте сервера по инструкции устанавливаются только пакеты postgresql-server , а также зависимые postgresql и postgresql-libs , при этом провайдер JIT по умолчанию не устанавливается.
На нашем сервере мы также не устанавливаем провайдер, поэтому у нас JIT выключен.
Кстати, проверить работоспособность JIT можно так:
SELECT pg_jit_available();
Клиент сообщил, что базу развернули в Docker, так как это была тестовая инсталляция с небольшим количеством данных.
Похоже в докер-образе PostgreSQL JIT включен и работает из коробки. Проверяем:
docker pull postgres docker run --name postgres -e POSTGRES_PASSWORD=password -d postgres docker exec -it postgres find / -name "*jit*" /usr/lib/postgresql/16/lib/llvmjit_types.bc /usr/lib/postgresql/16/lib/llvmjit.so /usr/lib/postgresql/16/lib/bitcode/postgres/jit /usr/lib/postgresql/16/lib/bitcode/postgres/jit/jit.bc /usr/share/postgresql-common/server/test-with-jit.conf docker exec -it postgres psql -U postgres -Atc 'SELECT pg_jit_available();' t
Так и есть - в образе добавлен провайдер JIT, а значит при превышении порога стоимости запроса часть операций будет оптимизирована.
Решение о применении оптимизаций JIT принимается на основании общей стоимости запроса. В случае превышения значения jit_above_cost (по умолчанию 100 000) производится JIT-оптимизация выражений в WHERE, агрегатах, целевых списках и проекциях, а также преобразование кортежей при загрузке их с диска в память. При этом в плане запроса в разделе JIT будут включены параметры Expressions и Deforming:
JIT: Options: Inlining false, Optimization false, Expressions true, Deforming true
Если же стоимость запроса превысила jit_inline_above_cost (по умолчанию 500 000) или jit_optimize_above_cost (по умолчанию также 500 000) то тела небольших функций и операторов будут встроены в код и может применяться дорогостоящая оптимизация. В плане это будет отображено в параметрах Inlining и Optimization:
JIT: Options: Inlining true, Optimization true, Expressions true, Deforming true
В нашем запросе в теле функции get_rawdata_str несколько LEFT JOIN на таблицах с большим количеством строк, в результате стоимость запроса составила очень большие значения и планировщик подключил JIT для оптимизации.
Просим клиента отключить JIT:
ALTER SYSTEM SET jit=off; SELECT pg_reload_conf();
И время выполнения запроса снизилось до 30 мс:

Проблема решена. Но чтобы она не повторялась, рекомендуем запускать PostgreSQL в Docker с выключенным по умолчанию JIT.
Для этого добавим в команду запуска опцию отключения JIT:
docker run --name postgres -e POSTGRES_PASSWORD=postgres -d postgres -c jit=off
Или создаем свой конфиг и запускаем с ним PostgreSQL:
docker run -i --rm postgres cat /usr/share/postgresql/postgresql.conf.sample > my-postgres.conf echo "jit=off" >> my-postgres.conf docker run -d --name postgres -v "$PWD/my-postgres.conf":/etc/postgresql/postgresql.conf -e POSTGRES_PASSWORD=password postgres -c 'config_file=/etc/postgresql/postgresql.conf'
