EnglishРусский Map
Linux preemption models

Linux 7.0 снижает пропускную способность PostgreSQL в два раза

title
Linux 7.0 снижает пропускную способность PostgreSQL в два раза
type
summary
summary
Удаление PREEMPT_NONE в Linux 7.0 позволяет вытеснять процесс посреди page fault с захваченным spinlock'ом PostgreSQL, что сжигает CPU под нагрузкой
tags
linux, postgresql, kernel, performance
created
2026-04-30
updated
2026-07-29
lang
ru
source_updated
2026-07-29
translated
2026-09-01
translator
lllm/antigravity/gemini-3.7-flash-medium

3 апреля 2026 года инженер AWS Salvatore Dipietro сообщил в LKML, что под одной и той же нагрузкой pgbench (сценарий simple-update) на 96-vCPU Graviton4 производительность упала с 98 565 TPS на Linux 6.x до 50 751 TPS на Linux 7.0 - пропускная способность просела вдвое исключительно из-за обновления ядра. perf показал, где оседало время: 56% CPU тратилось на активное ожидание внутри s_lock, вызываемого из StrategyGetBuffer в менеджере буферов postgresql.

Что изменилось

В Linux 7.0 модель PREEMPT_NONE убрали из доступных linux-preemption-models на современных процессорных архитектурах. Дистрибутивы, использовавшие PREEMPT_NONE по умолчанию для серверных нагрузок, переключились на PREEMPT_LAZY - модель вытеснения, добавленную в 6.12 с прицелом на высокую пропускную способность. Для подавляющего большинства задач она и правда служит прямой заменой. PostgreSQL же натолкнулся на редкий сценарий, где это не так.

Почему именно PostgreSQL

PostgreSQL кэширует данные в области shared_buffers - в бенчмарке она составляла 120 GB. Когда backend'у нужна страница, которой ещё нет в кэше, StrategyGetBuffer выбирает слот для вытеснения. Этот выбор защищён единственным глобальным spinlock. Критическая секция рассчитана на десятки наносекунд: весь смысл spinlock'а в том, что поток освобождает его быстрее, чем кто-либо успеет заметить ожидание.

Здесь сталкиваются два фактора:

  1. Пул разделяемых буферов выделяется лениво. При первом обращении к любому байту страницы Linux размером 4 KB в этой области возникает minor page fault - ядро выделяет физическую страницу и обновляет таблицу страниц. При 120 GB shared_buffers со страницами по 4 KB это даёт ~31 миллион потенциальных page fault'ов первого обращения, размазанных по всему времени работы, а не только на старте.
  2. StrategyGetBuffer читает и пишет в разделяемую память прямо под spinlock'ом. Часть этих обращений оказывается как раз первым касанием.

В итоге backend может словить page fault прямо с захваченным spinlock'ом. С PREEMPT_NONE ядро обрабатывало fault, а удерживающий блокировку процесс продолжал выполняться до её освобождения - остальные потоки крутились в цикле считанные микросекунды. С PREEMPT_LAZY планировщик вправе вытеснить держателя блокировки прямо посреди обработки fault'а. Теперь блокировка остаётся захваченной на время обработки fault'а плюс интервал до тех пор, пока планировщик снова не отдаст процессу квант времени. Все остальные backend'ы на машине всё это время вхолостую сжигают CPU в spin-циклах, умножая потери на число ожидающих потоков. На 96 vCPU с 1024 клиентами этого множителя хватило, чтобы просадить пропускную способность вдвое.

Уже существующее решение

У PostgreSQL есть настройка huge_pages. Если переключиться со страниц 4 KB на 2 MB или 1 GB huge-pages, арифметика кардинально меняется:

  • 4 KB: ~31 000 000 потенциальных page fault'ов
  • 2 MB: ~61 440 потенциальных page fault'ов
  • 1 GB: ~120 потенциальных page fault'ов

Срабатывают сразу два эффекта. Fault'ы первого обращения становятся настолько редкими, что поток со spinlock'ом почти никогда на них не натыкается. Заодно резко снижается нагрузка на TLB: гораздо меньшее число записей покрывает тот же объём памяти, рабочий набор целиком помещается в TLB, и StrategyGetBuffer работает на полной скорости. Просадка исчезает.

В статье рекомендуется выставлять huge_pages = on вместо значения по умолчанию try. Тогда PostgreSQL откажется запускаться, если huge pages не настроены на уровне ОС, вместо того чтобы молча откатываться на обычные страницы.

Предложенное решение

Peter Zijlstra (инженер Intel и автор изменений в модели вытеснения) предложил перевести PostgreSQL на Restartable Sequences (rseq) - механизм Linux, позволяющий пространству пользователя пометить критическую секцию, чтобы при вытеснении ядро сообщало об этом, а код перезапускался заново. В PostgreSQL к предложению отнеслись прохладно: оно перекладывает работу на базу данных ради возврата поведения, которое раньше работало из коробки, и идёт вразрез с принципом ядра "не ломать userspace". Как сформулировано в статье: регрессия реальна, но предложенное решение перекладывает издержки не в ту сторону.

Почему это важно не только для Postgres

Паттерн "короткая критическая секция + spinlock + скрытый внутри системный вызов или fault" опасен сам по себе. Под PREEMPT_NONE проблема почти не проявлялась, потому что планировщик принципиально не снимал выполняющийся процесс с процессора. Отказ от этой страховки бьёт по любым программам, которые неявно на неё полагались. PostgreSQL стал самым заметным примером, так как под нагрузкой конкуренция упирается в один глобальный lock, но ровно та же схема встречается и в других долгоживущих многопоточных системах с разделяемой памятью.

Источники