рекомендации explain.tensor.ru
рекомендации explain.tensor.ru

У одного из клиентов нашей системы мониторинга 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

Включаем этот параметр, загружаем план запроса из тела функции:

план запроса из функции get_rawdata_str
план запроса из функции get_rawdata_str

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

JIT в плане запроса
JIT в плане запроса

Но почему у клиента включился JIT, а на наших серверах нет ?

Вообще JIT появился в 11 версии, здесь было обсуждение по поводу его включения.

В 11 версии он остался выключенным по умолчанию, а во всех версиях, начиная с 12, JIT включен.

При этом кроме опций конфига для использования JIT требуется соблюдение нескольких условий:

  1. сборка PG должна быть произведена с опцией --with-llvm , в официальных пакетах это так и есть, а в случае сборки из исходников наличие опции проверить можно командой:

pg_config --configure | grep with-llvm
  1. наличие провайдера 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 мс:

план запроса без JIT
план запроса без JIT

Проблема решена. Но чтобы она не повторялась, рекомендуем запускать 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'

Комментарии (44)


  1. rPman
    23.04.2024 06:28
    +4

    Интересно, при каких условиях jit-оптимизация начнет увеличивать производительность?


    1. MGorkov Автор
      23.04.2024 06:28
      +5

      В документации пишут

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

      по поводу tuple deforming есть интересные результаты

      For large number of attributes JIT-ing of deform tuple can improve speed up to two time.


      1. rPman
        23.04.2024 06:28

        спасибо, по ссылке таблицы с десятком миллионов случайных записей (числа) операции sum ускорились на 30%.... скорее всего чтобы это стало заметно, вычислений нужно действительно много


      1. Fell-x27
        23.04.2024 06:28
        +2

        Интересно, а можно явно указать, чтобы конкретный запрос делался с JIT? А то есть в системе запросы весом по 20 минут... они, конечно, не рилтаймовые, они, конечно, выполняются раз 5 дней и чисто внутренние, но все же.


        1. MGorkov Автор
          23.04.2024 06:28
          +8

          Можно установить параметр на уровне сессии или транзакции, для этого соответственно выполнить SET jit=on или SET LOCAL jit=on


          1. Fell-x27
            23.04.2024 06:28

            Отлично! Спасибо большое! Интересно, даст ли прирост. В запросе много вычислений. В теории, должно отлично лечь на JIT.


  1. Kassiy_Pontiy_Pilat
    23.04.2024 06:28
    +2

    Для 1С лучше выключать или нет? Что то особо нигде не упоминается jit в рекомендациях


    1. MGorkov Автор
      23.04.2024 06:28
      +2

      это лучше уточнить у консалтеров по 1С, но думаю что по умолчанию надо выключить


  1. 1nd1go
    23.04.2024 06:28
    +20

    Выглядит как симптоматическое лечение. Я бы ожидал, что JIT должен отрабатывать все-таки один раз на запрос, дальше он скэшируется, и больше в этом запросе не должно наблюдаться проблем. У клиента дашборд тормозил при первом запросе или при повторных запросах также?

    Также, интересно проверить, не будет ли воспроизводится пробелема с JITом, но без докера?


    1. MGorkov Автор
      23.04.2024 06:28
      +7

      JIT
      JIT

      тормозил постоянно, при повторных запросах тоже.

      я проверил на standalone PG 15.5 - JIT не кэшируется, два первых запроса длительностью 3-5 сек, затем JIT выключил и стало менее 30 мс


      1. usego
        23.04.2024 06:28
        +32

        JIT не кэшируется

        интересней было бы изучить эту проблему, а не лечить опухоль на пальце ампутацией руки.


        1. snakers4
          23.04.2024 06:28

          Оффтоп, но при запуске JIT-скомпилированных ML моделей наблюдаются похожие приколы.


      1. 1nd1go
        23.04.2024 06:28

        понятно. Да, судя по документации jitа в pg много с настройками не разгуляешься.


      1. yannmar
        23.04.2024 06:28
        +5

        Может быть так, что запросы формируются так, что каждый запрос с точки зрения базы уникален? Может их там как-нибудь неаккуратно конкатенацией собирают без использования препаред стейтментов? Вот и приходится базе каждый запрос заново компилировать?


        1. Sap_ru
          23.04.2024 06:28
          +9

          Там проблема в том, что бинарный код после JIT жёстко привязан к адресному пространству и запросу, а потому:
          1) каждый новый запрос будет занимать всё новые адреса. Выгружать это всё не получится, так как у кода жёсткая привязка к адресам. То есть выгрузить можно, но если при попытке загрузить из кэша обратно окажется, что по этому адресу уже есть какой-то другой запрос, то что тогда делать? Жонглировать запросами в адресном пространстве СУБД? Так себе идея, особенно учитывая, что обрабатывающих запросы потоков много, а адресное пространство у них общее - что делать если разные потоки захотят исполнять разные запросы, которые были скомпилированы по одному адресу? А ещё (все) ОС и процессор очень не любят динамического изменения кода.
          В качестве решения предлагается относительного легковесное исправление всех адресов при извлечении запроса из кэша. Но это достаточного тяжёлая операция. Кроме того при патчинге адресов возникают некоторые конфликты с оптимизацией.
          2) генерируемый JIT код очень жёстко оптимизируется под конкретный запрос. Например, у вас там проверка на ноль, какая-нибудь или на константу - этот будет очень круто заоптимизировано при JIT, и если в другом запросе константа другая, то ой - наш старый кэш уже не подходит. Если же мы начнём генерировать универсальный код без оптимизации констант и сравнений, то это сильно снизит степень оптимизации кода и замедлит сложные и длительные запросы, ради которых этот JIT и задумывался. Кроме того, конкретно в PG (но в целом это общая проблема всех СУБД) вся оптимизация завязана на планировщик запросов (что логично), и нужно отключать часть оптимизаций уже на уровне планировщика, а значит потенциально замедлять вообще все запросы вне зависимости от JIT и нашего кэша.

          Короче, на данный момент создать эффективный кэш оптимизированного бинарного кода невозможно. Не придумано пока никакого нормального общего решения и всё. А все предполагаемые пути решения проблемы ведут к тому, что нужно с ноля писать планировщик и оптимизатор, которые изначально архитектурно будут рассчитаны на последующую работу JIT и кэша. А это громадная работа и переписывание примерно три всего кода СУБД. Причём, всё равно придётся идти на компромиссы в какие-то моменты. Например, нужно, чтобы планировщик очень точно считал вычислительную сложность запросов, а это само по себе нерешённая пока задача (особенно в PG).


    1. andrydl
      23.04.2024 06:28
      +3

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


  1. Tzimie
    23.04.2024 06:28

    А вот не надо все что угодно совать в докер


    1. gmini
      23.04.2024 06:28
      +28

      при чем тут собственно докер? Если бы установили на хост со включенным jit было бы то же самое. Наоборот, докер тут может помочь получать воспроизводимый результат


    1. AlexGluck
      23.04.2024 06:28
      +14

      Вот бы автор убрал из заголовка, что проблема в докере, чтобы такие существа как вы не считали, что новая упаковка (вместо привычных deb\rpm\tgz) как то влияет на функционирование систем.


      1. Tzimie
        23.04.2024 06:28

        Я просто DBA high load систем. 100 Тб базы, 2тб памяти сервера с правильным пробросом numa (как там у докера с этим?), так что желание завернуть все в докер смешно


        1. AlexGluck
          23.04.2024 06:28
          +9

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


          1. Tzimie
            23.04.2024 06:28

            А корректный проброс numa? С этим даже у VMware проблемы


            1. AlexGluck
              23.04.2024 06:28
              +9

              Проброс куда и зачем? NUMA это по сути области памяти разделённые на домены между процессорами, когда вы настраиваете ограничения работы процесса чтобы он работал только в рамках конкретного домена, это в линуксе закрепление за определёнными ядрами. А раз вы для процесса устанавливаете параметры, то что вам мешает воспользоваться инструментом который проще предоставляет такие возможности? И конечно инструментарий не накладывает никаких ограничений, что процесс контейнеризован, что нет, это такой же процесс в операционной системе, значит докер никак на это не влияет, только кудахтанье в интернете без знаний матчасти.


              1. Tzimie
                23.04.2024 06:28

                Сюрприз! Некоторые умные процессы (SQL server) так оптимизируют свою деятельность, чтобы threads минимизировали cross-numa interactions (numa aware). Поэтому если для них numa "mispresented", то вы получаете performance penalty


                1. AlexGluck
                  23.04.2024 06:28
                  +12

                  Переведу вашу белиберду на более понятный язык:

                  Хочу донести до вашего сведения, что программное обеспечение может обладать сложной логикой, которая во время выполнения вносит улучшения (оптимизации) работы, для уменьшения частоты переключения "потоков выполнения" (threads) на другие домены NUMA.

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

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

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


                  1. slonpts
                    23.04.2024 06:28
                    +3

                    Благодарю от имени всех, кто не разбирается в вопросе!

                    Это важное дело - публично опровергать ложные утверждения


                1. sebres
                  23.04.2024 06:28

                  Если речь про что-то типа NUMA Affinity или CPU Pinning и т.п., то будет достаточно добавить --cap-add=sys_nice и/или прописать кастомный seccomp, разрешающий numactl в контейнере.
                  Если docker только про упаковку, и безопасность не парит совсем (равнозначный хосту контейнер), можно дать ему --privileged и забыть про привилегии совсем.

                  > А корректный проброс numa? С этим даже у VMware проблемы

                  Ну, vmware - vmware рознь...
                  Если это про Tanzu и ко (контейнеризация) - никогда не слышал про те "проблемы"... Можно ссыль?
                  Если же про VMs, то как раз у контейнеризации тут должно быть всё много лучше и проще чем у "настоящей" виртуализации, собственно по определению обеих... Ибо контейнерная технология напрямую использует физические ресурсы ядра операционной хост-системы, а не эмулирует их как виртуальные компоненты для каждого экземпляра гостевой VM.
                  Т.е. как изоляция, так и противоположное действие много проще там (capabilities, namespaces, control groups, mapping и вот это вот всё на уровне ядра).
                  Поэтому собственно контейнеры - это больше про упаковку, чем про "безопасное", полностью изолированное от хост-системы и других гостей, окружение (как оно декларируется у виртуализации).
                  По этой же причине совершенно нормально пускать контейнеры в гостевой VM, при том что наоборот - это скорее исключение (и имеет смысл только в узко-специфических случаях, типа легаси софта виртуализации и т.п.)


      1. snakers4
        23.04.2024 06:28

        Если автор бы убрал из заголовка, статью бы прочитало кратно меньше людей. Но заголовок вроде "в такой-то версии официального Docker-образа постгреса jit-флаг включен по умолчанию, что тормозит какие-то запросы" не привлечет аудиторию.


  1. chemtech
    23.04.2024 06:28
    +35

    Поправьте пожалуйста заголовок. Проблема не в docker, а в jit.


    1. MGorkov Автор
      23.04.2024 06:28

      ставим PG в конфигурации по умолчанию (т.е. грубо yum install postgresql15-server) - проблемы нет, ставим также докер (docker pull postgres) - проблема есть. По идее образ докера надо также делать без провайдера JIT, и дополнительный образ с JIT кому это действительно необходимо.


      1. AlexGluck
        23.04.2024 06:28
        +6

        У меня такая же нога, но болит. В конфигурации по умолчанию, которая принята в компании "Х" проблема воспроизводится. Как долго вы ошибку выжившего будете предоставлять как аргумент некорректному заголовку статьи у вас?


      1. MerdedSpade
        23.04.2024 06:28
        +7

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

        Также судя по всему, PostgreSQL собирается без LLVM не во всех дистрибутивах (в Debian и некоторых других собирается с ним), следовательно его отсутствие не является стандартной конфигурацией, а лишь решением мейнтейнера пакета, который мог не включить LLVM например в целях избежания конфликтов или притягивания довольно большой зависимости.


        1. MGorkov Автор
          23.04.2024 06:28
          +1

          его отсутствие не является стандартной конфигурацией, а лишь решением мейнтейнера пакета

          В этом и есть проблема, фактический набор функционала PostgreSQL зависит от мейнтейнера пакета/образа.

          В пакетах и образах для Debian, Ubuntu, FreeBSD, Docker провайдер JIT включен в основной пакет/образ при установке PG-сервера и таким образом включен по умолчанию.

          В Redhat/Centos, SUSE, MacOS, Windows - при установке основного пакета/образа JIT не устанавливается, т.е. выключен.


      1. kemko
        23.04.2024 06:28

        Тогда максимум проблема в настройках конкретного docker-образа. Так что заголовок всё ещё обманывает.


  1. lorc
    23.04.2024 06:28
    +7

    Проблема решена. Но чтобы она не повторялась, рекомендуем запускать PostgreSQL в Docker с выключенным по умолчанию JIT.

    Проблема не решена, к сожалению. Такое поведение PostgreSQL должно расцениваться как ошибка. И я очень удивлен что оно попало в релиз. Надо докопаться до реальной сути проблемы. Вы не пробовали написать в mailing list на эту тему? Если это реальная бага в postgres, то её надо хотя-бы зарепортить. Либо это какая-то ваша мисконфигурация в базе/системе/где-то еще и тогда ее нужно найти и устранить.


    1. Buzzzzer
      23.04.2024 06:28
      +10

      Проблема тут только в том, что в официальный докер-образ postgres включён пакет postgresql-llvmjit, а в инструкции по развороту на хосте ничего по postgresql-llvmjit и ни про какого другого провайдера не написано. На хосте, если следовать manам, jit не работает из-за отсутствия либы, а в докере работает.

      Бага в документации, имхо.


      1. lorc
        23.04.2024 06:28
        +13

        Ну в любом случае это следует прояснить с разработчиками. Если бага в документации - пусть исправят документацию, если бага в докер-файле - пусть исправят докер-файл.

        Но вообще, включение JIT не должно приводить к просадкам. Тем более к ТАКИМ просадкам. Как по мне - это бага где-то глубже. А наличие postgresql-llvmjit просто провоцирует её.

        И вообще, come on, если ваша компания занимается разработкой тулинга для postgres, то вы явно должно понимать внутренности postgres лучше других и репортить такие штуки разработчикам самого движка.


  1. Hellpain
    23.04.2024 06:28
    +12

    Так тут docker не причем, дело в jit. На докер и так много клевещут, поправьте заголовок пожалуйста


  1. breninsul
    23.04.2024 06:28
    +16

    Короче - докер не при чем...


  1. AnotherAnkor
    23.04.2024 06:28
    +1

    Мне кажется или в документации было прямым текстом написано не запускать в docker, если надо быстродействие?


    1. AlexGluck
      23.04.2024 06:28

      А как контейнеризация влияет на производительность? Старые заблуждения о том что нельзя СУБД упаковывать в контейнеры ошибочно попали в документацию.


      1. AnotherAnkor
        23.04.2024 06:28

        У меня всегда есть сомнения, что кто-то может знать о продукте больше, чем его создатель.


        1. AlexGluck
          23.04.2024 06:28
          +1

          Если немного нигилистки подойти и сомневаться даже в создателе, то иногда можно найти баги и ошибки у создателя.


  1. dimas
    23.04.2024 06:28

    Образ postgres не на базе alpine был? Не пробовали воспроизвести на базе другого дистра?