Почему запись одного байта на диск стоит четыре килобайта
Если вы хоть раз меняли один байт в файле и удивлялись, почему это заняло не меньше времени, чем запись целого мегабайта — вы наткнулись на фундаментальное свойство дисков. Ни HDD, ни SSD не умеют писать байтами. Они пишут блоками фиксированного размера, и изменение одного бита внутри блока почти всегда означает, что диску пришлось прочитать, изменить и переписать весь блок целиком. Разберём, почему так устроено на аппаратном уровне и что с этим знанием делать на практике — особенно если вы администрируете СУБД.
Содержание
- Почему диск не умеет писать байтами
- Что значит read-modify-write и почему это дорого
- Как это выглядит на уровне ОС: page cache и dirty pages
- Почему выравнивание записей по границе блока критично для СУБД
- SSD добавляет свой уровень: страница флеша и erase-блок
- Как проверить свою нагрузку и увидеть RMW на практике
- Практические следствия для эксплуатации
Почему диск не умеет писать байтами
У классического жёсткого диска физический предел — это сектор. Исторически сектор был 512 байт, и это не архитектурная прихоть, а следствие того, как устроена магнитная головка и дорожка: записывающая головка формирует на дорожке последовательность намагниченных областей, каждая из которых кодирует один бит, но чтобы надёжно найти начало и конец записываемого участка, различить его границы и приложить код коррекции ошибок, диску нужен минимальный «атом» записи с собственным заголовком, полем данных и контрольной суммой. Дробить его на более мелкие куски бессмысленно — накладные расходы на служебные поля съедят всю экономию.
Современные HDD давно перешли на Advanced Format с сектором 4096 байт (это называют 4Kn или 512e — эмуляция старого 512-байтного интерфейса поверх физических секторов по 4КБ), потому что более крупный сектор снижает долю служебных байт (заголовков и ECC) относительно полезных данных и повышает плотность записи.
У SSD причина другая, но результат тот же. Флеш-память NAND физически не умеет перезаписывать отдельную ячейку — только стирать целыми блоками (erase block, обычно единицы мегабайт) и записывать страницами (page, чаще всего 4КБ, хотя у части современных дисков страница крупнее). Записать можно только в предварительно стёртую страницу; переписать поверх уже занятой страницы контроллер не может — сначала данные (если их нужно сохранить) переносятся в другое место, потом старый блок стирается целиком. Из-за этого минимальная единица, с которой вообще имеет смысл разговаривать с диском о записи, — это страница флеша, и в подавляющем большинстве современных SSD она равна 4КБ.
Так у HDD и у SSD совершенно независимо получился один и тот же практический стандарт — блок в 4096 байт как минимальная единица ввода-вывода, с которой имеет дело файловая система и которую видит приложение через stat:
$ stat -f /
Block size: 4096
Fundamental block size: 4096
Что значит read-modify-write и почему это дорого
Представьте, что вам нужно изменить один байт в середине файла — например, обновить счётчик в записи базы данных. Диск физически не может «дотянуться» до этого байта и переписать только его. Вместо этого происходит следующая последовательность:
- Read — контроллер (или ОС через кеш страниц) читает весь блок 4КБ, в котором находится нужный байт, в память.
- Modify — в оперативной памяти изменяется именно тот байт (или несколько байт), которые нужно было изменить.
- Write — весь блок 4КБ целиком записывается обратно на диск, заменяя старый.
Это и называется read-modify-write (RMW) — классический паттерн, который встречается не только на уровне диска, но и на уровне RAID (полосы RAID 5/6), файловых систем и даже CPU-кешей, где та же логика работает с кеш-линиями по 64 байта. Суть везде одна: у устройства есть минимальная адресуемая единица, и любая операция мельче этой единицы вырождается в операцию над всей единицей целиком.
Практическое следствие: изменение 1 байта данных на самом деле означает как минимум одно чтение 4096 байт и одну запись 4096 байт — это в 4096 раз больше физического трафика, чем «логический» размер изменения. На SSD добавляется ещё одна деталь — если страница уже занята и вы не можете переписать её напрямую, контроллеру, возможно, придётся сначала прочитать соседние действующие страницы того же erase-блока, перенести их в новое место (garbage collection), и только потом стереть освободившийся блок. Это отдельная и более тяжёлая тема — write amplification на SSD, но корень проблемы тот же: несовпадение размера вашей логической записи с физической единицей устройства.
Важная оговорка: на практике далеко не каждая запись байта реально доходит до диска именно так — операционная система буферизует запись через page cache, и если несколько байт одного блока меняются подряд за короткое время, физически на диск может уйти всего одна операция записи блока, а не несколько. Но сути это не меняет: единица, с которой в итоге работает диск, всё равно блок, а не байт.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверКак это выглядит на уровне ОС: page cache и dirty pages
Приложение не пишет напрямую на диск (если явно не открыло файл с O_DIRECT). Запись идёт через page cache — область оперативной памяти, где ядро хранит копии блоков файлов. Когда процесс вызывает write(), ядро находит нужную страницу кеша (или читает её с диска, если её там ещё нет — это и есть шаг read из RMW), изменяет байты в памяти и помечает страницу как «грязную» (dirty). Физическая запись на диск откладывается — периодический демон (pdflush/writeback в современных ядрах) сбрасывает грязные страницы пачками, по таймеру или когда их накопилось слишком много.
Это выгодно: если приложение делает много мелких изменений подряд в одном и том же блоке, ОС может «схлопнуть» их в одну физическую запись вместо нескольких RMW-циклов. Посмотреть текущее состояние грязных страниц можно так:
$ cat /proc/meminfo | grep -i dirty
Dirty: 2148 kB
Writeback: 0 kB
Если приложению критично гарантировать, что данные физически попали на диск (а не просто лежат грязной страницей в памяти), оно вызывает fsync() — и вот тут-то откладывания заканчиваются, диск обязан выполнить реальную запись блока. Именно поэтому базы данных, которые вызывают fsync на каждый commit, гораздо чувствительнее к цене одной записи блока, чем, скажем, лог-файл, который пишется асинхронно.
Почему выравнивание записей по границе блока критично для СУБД
Здесь начинается самое важное для практика. Если структура данных вашего приложения (страница СУБД, запись индекса) не выровнена по границе физического блока диска, одна логическая операция может задеть сразу два физических блока вместо одного.
Представим страницу данных размером 4КБ, но она начинается со смещения 2048 байт от начала файла (не кратного 4096). Тогда эта единственная страница физически перекрывает два блока по 4КБ каждый — и любое изменение внутри неё требует read-modify-write не одного блока, а двух. Вы буквально удвоили физический ввод-вывод на ровном месте, просто из-за смещения.
Именно поэтому все серьёзные СУБД (PostgreSQL, MySQL/InnoDB) проектируют размер своей внутренней страницы кратным размеру блока файловой системы и стараются держать файлы данных выровненными по границам блоков:
| СУБД | Размер страницы по умолчанию | Настраивается |
|---|---|---|
| PostgreSQL | 8 КБ (BLCKSZ) | Только при сборке из исходников, на готовом бинарнике — нет |
| MySQL/InnoDB | 16 КБ (innodb_page_size) | Да, при инициализации инстанса |
| SQLite | 4 КБ (можно 512Б–64КБ) | Да, PRAGMA page_size до создания БД |
Все эти значения кратны 4096 — не совпадение, а прямое следствие того, что 4КБ — доминирующий физический размер блока на современных дисках. Если бы страница СУБД была, скажем, 6000 байт, каждая вторая страница задевала бы границу физического блока «вкривь», и число реальных RMW-операций на диске выросло бы без всякой пользы для приложения.
Отдельная историческая грабля — выравнивание разделов. На старых системах разделы диска нередко начинались со смещения 63 сектора (512Б) по историческим причинам совместимости с DOS-разметкой — это ломало выравнивание для 4КБ-блоков поверх. Современные parted/fdisk по умолчанию выравнивают начало раздела на 1 МБ, кратный и 4КБ, и типичному размеру RAID-страйпа, так что для новых серверов проблема в основном решена — но если вы работаете со старым образом диска или мигрировали виртуальную машину без пересоздания разметки, стоит проверить:
$ parted /dev/sda unit s print
$ blockdev --getpbsz /dev/sda # физический размер блока
$ blockdev --getss /dev/sda # логический размер сектора
Если физический и логический размеры сектора отличаются (типично 512 логический / 4096 физический — режим 512e), у диска есть внутренняя эмуляция, которая сама добавляет накладные расходы на невыровненные операции — контроллеру приходится делать RMW на своей стороне, о чём файловая система может даже не знать.
SSD добавляет свой уровень: страница флеша и erase-блок
На SSD ситуация на порядок сложнее, потому что физическая карта устройства скрыта от ОС слоем FTL (Flash Translation Layer) внутри контроллера диска. ОС и файловая система видят «блочное устройство» с логическими блоками по 512Б или 4КБ, но реальное расположение данных на кристаллах NAND контроллер перекладывает как считает нужным — в том числе постоянно переносит «горячие» данные, чтобы равномерно изнашивать ячейки (wear leveling).
Из-за этого слоя абстракции даже идеально выровненная с точки зрения файловой системы запись может не совпасть с реальной физической страницей флеша — это зависит от прошивки конкретного контроллера, и повлиять на это напрямую нельзя. Но принцип «пишите блоками, кратными 4КБ, и не дробите логическую единицу данных пополам между блоками» снижает вероятность лишней работы независимо от того, что происходит внутри контроллера.
Отдельная механика, о которой стоит знать: команда TRIM сообщает SSD, какие логические блоки больше не содержат нужных данных (например, после удаления файла), чтобы контроллер мог заранее стереть соответствующие физические страницы и не делать это в последний момент, в разгар записи. Если TRIM не доходит до диска (диск за RAID-контроллером без поддержки TRIM, отключённый discard в fstab), SSD со временем деградирует по скорости записи именно потому, что чаще вынужден делать полный цикл read-erase-write в реальном времени вместо фоновой уборки. Подробнее о том, как обвал происходит на живой базе, разбирали в статье про фоновый TRIM, из-за которого база замирала на пятнадцать секунд, а про сам механизм физического износа ячеек — в статье о том, как SSD умирает медленно.
Как проверить свою нагрузку и увидеть RMW на практике
Прежде чем что-то оптимизировать, стоит увидеть проблему. Утилита iostat с флагом -x показывает размер операций ввода-вывода:
$ iostat -x 1
Device r/s w/s rkB/s wkB/s avgrq-sz %util
sda 2.0 150.0 32.0 4800.0 32.0 45.2
Если wkB/s делить на w/s, получится средний размер записи в килобайтах на одну операцию. Если он заметно меньше 4КБ и при этом близок к, например, 512 байт — это сигнал, что приложение делает мелкие невыровненные записи, и диск на своей стороне достраивает их до полного блока за счёт RMW, которую вы напрямую в iostat не увидите, но она есть.
Для файловой системы полезно посмотреть непосредственно размер блока:
$ tune2fs -l /dev/sda1 | grep -i "block size"
Block size: 4096
Для PostgreSQL размер страницы фиксирован на момент сборки кластера и виден так:
$ psql -c "SHOW block_size;"
block_size
------------
8192
Если вы мигрируете инстанс с диска на диск (например, при переезде на другой тариф или в другой дата-центр), стоит убедиться, что новый раздел выровнен так же аккуратно, как старый — иначе можно незаметно потерять часть производительности записи именно из-за рассинхронизации границ, а не из-за характеристик самого диска. Разница особенно заметна на нагрузке с большим числом мелких транзакций — что типично для OLTP-баз, в отличие от, скажем, последовательной записи логов, где эффект RMW почти не ощущается, потому что записи и так идут блоками.
Практические следствия для эксплуатации
Из всего механизма read-modify-write вытекает несколько практических правил, которые стоит держать в голове при эксплуатации серверов с СУБД или другой I/O-интенсивной нагрузкой:
- Не меняйте
page_sizeСУБД без причины и не делайте его некратным 4096. Значения по умолчанию (8КБ у PostgreSQL, 16КБ у InnoDB) выбраны разработчиками не случайно — они кратны типичному физическому блоку и одновременно достаточно велики, чтобы амортизировать накладные расходы служебных полей. - Следите за выравниванием разделов, особенно на старых образах или при миграции виртуальных машин между гипервизорами —
parted ... unit s printпокажет реальное смещение начала раздела. - Используйте
blockdev --getpbsz, чтобы узнать физический размер блока диска — если он 4096, а логический сектор ОС видит как 512 (режим 512e), знайте, что часть трансляции происходит внутри контроллера и вы не полностью контролируете этот слой. - Не отключайте TRIM/discard на SSD без веской причины — без него write amplification нарастает со временем, а именно она усиливает эффект дорогих мелких записей, разбирали это в статье про ресурс SSD и TBW.
- Батчите мелкие записи на уровне приложения, где это возможно — если логика позволяет накопить несколько изменений и сбросить их одной операцией
write/fsync, вы напрямую снижаете число RMW-циклов, а не просто надеетесь на page cache. - Для нагрузок с большим числом случайных мелких записей выбирайте NVMe, а не SATA SSD — очереди команд NVMe лучше справляются именно с потоком мелких операций, у SATA AHCI глубина очереди ограничена сильнее. Подробнее — в статье про очереди NVMe и почему один поток не выжимает из диска максимум.
Все эти правила не устраняют read-modify-write как явление — устранить его нельзя, это фундаментальное свойство блочных устройств. Но они снижают частоту случаев, когда одна логическая операция приложения превращается в две или больше физических операций диска.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Можно ли вообще писать на диск байтами, минуя блочность?
Нет, ни один массовый диск потребительского или серверного класса так не работает — это ограничение аппаратного уровня (сектор HDD, страница NAND SSD), а не программного. Обойти его нельзя, можно только минимизировать количество лишних RMW-циклов выравниванием и батчингом.
Значит ли это, что мелкие записи всегда медленнее крупных?
Не совсем линейно. Одна запись 4КБ и одна запись 4МБ (при последовательном размещении) могут занять сопоставимое по накладным расходам время на операцию — цена именно в количестве отдельных операций RMW, а не в объёме данных внутри одного блока. Поэтому 1000 записей по 100 байт в разных блоках обойдутся заметно дороже, чем одна запись 100КБ подряд.
Влияет ли это на чтение так же, как на запись?
Нет, для чистого чтения RMW не возникает — диск просто читает нужный блок целиком, лишней работы там не появляется. Проблема именно в модификации: чтобы изменить часть блока, сначала нужно знать содержимое всего блока.
Помогает ли RAID решить эту проблему?
Отчасти наоборот — на RAID 5/6 та же логика read-modify-write работает ещё и на уровне страйпа: изменение части страйпа требует пересчёта контрольной суммы чётности, а для этого нужно прочитать остальные диски страйпа. Это отдельный и более тяжёлый случай той же механики.
Стоит ли настраивать page_size СУБД под конкретный SSD?
Как правило нет — производители дисков и разработчики СУБД уже согласовали разумные значения по умолчанию, и без измерений на конкретной нагрузке менять их вслепую скорее навредит, чем поможет. Если есть подозрение на проблему выравнивания, сначала проверьте blockdev и parted, а не сам page_size.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →