В облаке ваш сервис почти наверняка стоит на SSD. Но жёсткие диски (HDD, hard
disk drive) никуда не делись: на них лежат холодные данные, бэкапы, архивы
объектных хранилищ, потому что гигабайт на HDD в несколько раз дешевле. И главное
— физика HDD объясняет, почему базы данных, файловые системы и брокеры
сообщений устроены именно так. Привычка писать последовательно, а не вразнобой,
родилась из боли работы с жёстким диском и сохранилась в журналах Postgres,
логах Kafka и LSM-деревьях.
В прошлой главе мы разобрали, как ОС общается с любым
устройством. Теперь посмотрим, что это за устройство и почему обращаться к нему
надо с умом.
Похоже на проигрыватель винила: крутится пластинка, над ней игла на рычаге.
Чтобы услышать нужную песню, иглу подводят к дорожке и ждут, пока начало трека
доедет до неё.
Пластина (platter) — круглый диск с магнитным покрытием. Их в корпусе
несколько, они насажены на общую ось и вращаются вместе.
Шпиндель (spindle) — ось с мотором. Скорость вращения в RPM
(оборотов в минуту): 5400 и 7200 у обычных дисков, 10 000 и 15 000 у старых
серверных.
Дорожка (track) — концентрическое кольцо на поверхности пластины. На
одной поверхности их сотни тысяч.
Сектор (sector) — участок дорожки, минимальная единица чтения и записи.
Исторически 512 байт, у современных дисков физически 4 КБ (часто с эмуляцией
512-байтных секторов для совместимости). Прочитать полсектора диск не умеет.
Головка (head) — читает и пишет, парит над поверхностью на расстоянии в
единицы нанометров. Головки всех поверхностей закреплены на одном рычаге и
двигаются вместе.
Вид на пластину сверху: ________________ / дорожки \ / ____________ \ / / ______ \ \ | | / () \ | | () — шпиндель | | \______/ | | \ \____________/ / \ ● ←─── головка на рычаге \________________ /│ │ рычаг двигает головку к центру и от центра
Чтобы прочитать сектор, нужно поставить головку над его дорожкой и дождаться,
пока сектор проедет под ней.
T_доступа = T_seek + T_rotation + T_transfer seek — перевести головку на нужную дорожку ~4–10 мс rotation — дождаться, пока сектор доедет до головки ~2–4 мс transfer — прочитать данные, пока сектор под головкой доли мс
Seek (позиционирование) — механическое движение рычага: разогнать,
остановить, точно навестись на дорожку. Средний seek у дисков на 7200 RPM
порядка 8–9 мс. Короткий прыжок на соседнюю дорожку — меньше миллисекунды,
через весь диск — 15–20 мс.
Rotation (вращательная задержка) — головка над дорожкой, но нужный сектор
может быть где угодно на окружности. В среднем ждём половину оборота.
Transfer (передача) — само чтение. Диск выдаёт 150–250 МБ/с, поэтому
4 КБ читаются за сотые доли миллисекунды.
Вывод: две из трёх составляющих — механика, и именно они занимают почти всё
время. Электроника передаёт данные быстро, но сначала надо дождаться, пока
железо физически встанет в нужное место.
rotation: 4.17 мсtransfer: 0.026 мсдоступ: 13.19 мсrandom 4К: 76 IOPS = 0.30 МБ/сsequential: 150 МБ/с, разница в 507 раз
Из 13 мс на передачу данных уходит 0,026 мс, остальное — ожидание механики.
Получается около 76 операций в секунду (IOPS, input/output operations per
second) и 0,3 МБ/с. Последовательное чтение того же диска даёт 150 МБ/с:
механика оплачивается один раз в начале, дальше данные идут потоком, пока
пластина крутится. Разница в сотни раз.
На практике случайные IOPS у HDD чуть выше расчётных (диск переупорядочивает
очередь запросов, о чём ниже), порядка 100–200. Но порядок разницы с
последовательным доступом сохраняется: 100–200 IOPS — это потолок одного
HDD, и это число полезно помнить. Если база на HDD делает 500 случайных
чтений в секунду, ей не хватит одного диска, сколько бы CPU ни было.
Отсюда архитектурные решения, которые вы встретите дальше по курсу:
базы данных пишут изменения сначала в последовательный журнал (WAL), а
разбрасывают их по местам потом, пачками (см.
fsync и durability);
Kafka хранит сообщения как лог, который только дописывается в конец, поэтому
нормально работает даже на HDD;
LSM-деревья копят записи в памяти и сбрасывают большими последовательными
кусками (см. LFS);
файловые системы стараются класть блоки одного файла рядом (см.
реализацию ФС).
Раз seek такой дорогой, а в очереди к диску обычно ждут несколько запросов,
их можно переупорядочить так, чтобы головка двигалась меньше. Этим занимается
планировщик ввода-вывода (I/O scheduler). Задача похожа на
планирование процессов, только ресурс — движение головки.
Пример, на котором будем сравнивать политики: головка над дорожкой 53, в
очереди запросы к дорожкам 98, 183, 37, 122, 14, 124, 65, 67 (в порядке
поступления). Мерить будем суммарный путь головки в дорожках — он примерно
пропорционален суммарному времени seek.
SSTF (shortest seek time first) каждый раз выбирает запрос, ближайший к
текущему положению головки.
от 53: ближе всех 65 (12) → 65 от 65: 67 (2) → 67 от 67: 37 (30) или 98 (31)? 37 → 37 от 37: 14 (23) → 14 от 14: остались 98..183, ближе 98 (84) → 98 дальше 122 (24), 124 (2), 183 (59) 53 → 65 → 67 → 37 → 14 → 98 → 122 → 124 → 183 шаги: 12 + 2 + 30 + 23 + 84 + 24 + 2 + 59 = 236
Почти втрое короче FIFO. Обратите внимание на шаг от 67: дорожка 37 ближе 98 на
одну дорожку, и это решение разворачивает головку влево. Жадный выбор хорош
локально, но у него есть беда — голодание. Если рядом с головкой постоянно
появляются новые запросы, далёкий запрос (например, к дорожке 183) может ждать
сколько угодно. Та же проблема, что у SJF при планировании процессора: жадность
к ближнему обижает дальних.
SCAN (его называют «лифтом», elevator) едет в одну сторону, обслуживая всё
по пути, потом разворачивается и едет обратно. Как лифт, который сначала везёт
всех вверх, потом всех вниз.
едем вверх от 53: 65 → 67 → 98 → 122 → 124 → 183 разворот, едем вниз: 37 → 14 если разворачиваться на последнем запросе (183): (183 − 53) + (183 − 14) = 130 + 169 = 299 если ехать до края диска (пусть это дорожка 199): (199 − 53) + (199 − 14) = 146 + 185 = 331
Строго говоря, SCAN доезжает до края диска, а вариант, который разворачивается
на последнем запросе, называется LOOK. На практике используют именно его, и
в задаче к этой главе, и в симуляторе ниже «SCAN» разворачивается на крайнем
запросе — то есть даёт 299.
SCAN немного проигрывает SSTF по пути (299 против 236), зато никто не голодает:
любой запрос будет обслужен самое позднее за два прохода головки.
У SCAN есть перекос: дорожки в середине диска головка проезжает дважды за
цикл (туда и обратно), а крайние — один раз, и там запросы в среднем ждут
дольше. C-SCAN (circular SCAN) обслуживает запросы только при движении в
одну сторону, а доехав до конца, быстро возвращается в начало без обслуживания.
Время ожидания становится равномернее для всех дорожек.
Политика
Путь в примере
Плюс
Минус
FIFO
640
справедливо, просто
головка мечется
SSTF
236
меньше всего пути
голодание дальних запросов
SCAN (LOOK)
299
нет голодания, предсказуемо
середина обслуживается чаще краёв
C-SCAN
—
равномерное ожидание
холостой возврат
Все три политики вы реализуете в Go-задаче
«Суммарное перемещение головки диска», а в симуляторе
ниже можно переключать политики на том же примере и смотреть путь головки.
Головка ходит по цилиндрам. Переключайте политику (FIFO, SSTF, SCAN) и смотрите, как меняется путь головки: суммарное перемещение примерно пропорционально времени на seek. SCAN здесь разворачивается на самом дальнем запросе, а не на краю диска (в литературе это вариант LOOK), поэтому его цифры чуть лучше, чем у «настоящего» лифта.
Современный диск знает свою геометрию точнее ОС: где какой сектор, с какой
скоростью вращается пластина прямо сейчас. Поэтому значительная часть
планирования живёт в прошивке. Через механизм NCQ (native command queuing)
ОС отдаёт диску до 32 запросов сразу, и диск сам выбирает порядок, учитывая и
seek, и вращение. Такую политику называют SPTF (shortest positioning time
first): иногда выгоднее съездить на дальнюю дорожку, где нужный сектор подъедет
через миллисекунду, чем на ближнюю, где ждать почти полный оборот.
Планировщик ОС делает остальное: склеивает соседние запросы в один большой
(merging), следит, чтобы запросы не ждали слишком долго, и делит диск между
процессами. В Linux текущий планировщик устройства видно так:
cat /sys/block/sda/queue/scheduler
В квадратных скобках — активный, например [mq-deadline] kyber bfq none.
mq-deadline упорядочивает запросы, но гарантирует каждому крайний срок (лечит
голодание), bfq делит пропускную способность между процессами по весам,
none не делает ничего. Для HDD обычно стоит mq-deadline или bfq, для NVMe —
none: SSD нечего экономить на позиционировании, и лишний слой только добавляет
задержку (почему — в следующей главе).
Проверь себя· Жёсткие диски и планирование
Какая составляющая времени доступа к HDD обычно самая дорогая?
Почему случайный доступ к HDD на порядки медленнее последовательного?
Диск вращается со скоростью 7200 RPM. Чему примерно равна средняя вращательная задержка (rotation), в миллисекундах? Округлите до целого.
В чём главный недостаток политики SSTF (ближайший первым)?
Какие политики планирования борются с голоданием дальних запросов за счёт движения головки «как лифт»?
Почему из 13 мс случайного чтения 4 КБ на передачу данных уходит меньше 0,1 мс?
Чему равна средняя вращательная задержка у диска 5400 RPM?
Почему в примере SSTF после дорожки 67 идёт к 37, а не к 98?
Посчитайте суммарный путь для примера из главы по C-SCAN: едем вверх до дорожки 199, возвращаемся на 0 без обслуживания и снова едем вверх. Сколько получится с учётом холостого возврата и без него?
Показать ответы
Ответы. (1) Остальное время — механика: seek около 9 мс и в среднем полоборота около 4,2 мс. (2) Оборот 60 000 / 5400 ≈ 11,1 мс, половина — около 5,6 мс. (3) До 37 — 30 дорожек, до 98 — 31. (4) Вверх 53 → 199: 146; возврат 199 → 0: 199; вверх до 37 (обслужив 14): 37. С возвратом 382, без него 183. Учитывать ли возврат — вопрос модели: головка физически движется, но быстрее и без обслуживания.
Из чего складывается время доступа к HDD? Seek + rotation + transfer.
Seek и rotation — механика, миллисекунды; transfer для небольших блоков —
доли миллисекунды.
Как посчитать вращательную задержку? Оборот = 60 000 / RPM мс, в среднем
ждём половину: 7200 RPM → ~4,2 мс.
Сколько IOPS даёт HDD и почему random в сотни раз медленнее sequential?
Порядка 100–200 IOPS: каждая случайная операция платит ~10 мс механики. При
последовательном чтении механика оплачивается один раз.
Как это повлияло на архитектуру БД? WAL и логи пишутся последовательно,
LSM сбрасывают данные большими кусками, ФС кладёт блоки файла рядом.
Чем плох SSTF и как помогает SCAN? SSTF может бесконечно откладывать
дальние запросы. SCAN/LOOK обходит диск как лифт и обслуживает каждый запрос
за ограниченное время.
Зачем C-SCAN? Выравнивает время ожидания между серединой и краями.
Кто планирует на самом деле? И ОС (слияние, дедлайны, справедливость), и
прошивка диска через NCQ (SPTF с учётом вращения).