---
title: "Жёсткие диски и планирование запросов"
description: "В облаке ваш сервис почти наверняка стоит на SSD. Но жёсткие диски (HDD, hard disk drive) никуда не делись: на них лежат холодные данные, бэкапы, архивы объектных хранилищ, потому…"
canonical: https://graphlms.ru/os/book/hdd
course: "Операционные системы"
updated: 2026-10-12
lang: ru
publisher: GraphLMS
---

# Жёсткие диски и планирование запросов

> Источник: https://graphlms.ru/os/book/hdd — курс «Операционные системы», GraphLMS.

## Зачем разбирать устаревшее железо

В облаке ваш сервис почти наверняка стоит на SSD. Но жёсткие диски (HDD, hard
disk drive) никуда не делись: на них лежат холодные данные, бэкапы, архивы
объектных хранилищ, потому что гигабайт на HDD в несколько раз дешевле. И главное
— физика HDD объясняет, почему базы данных, файловые системы и брокеры
сообщений устроены именно так. Привычка писать последовательно, а не вразнобой,
родилась из боли работы с жёстким диском и сохранилась в журналах Postgres,
логах Kafka и LSM-деревьях.

В [прошлой главе](/os/book/io-devices) мы разобрали, как ОС общается с любым
устройством. Теперь посмотрим, что это за устройство и почему обращаться к нему
надо с умом.

## Что внутри

Похоже на проигрыватель винила: крутится пластинка, над ней игла на рычаге.
Чтобы услышать нужную песню, иглу подводят к дорожке и ждут, пока начало трека
доедет до неё.

- **Пластина** (platter) — круглый диск с магнитным покрытием. Их в корпусе
  несколько, они насажены на общую ось и вращаются вместе.
- **Шпиндель** (spindle) — ось с мотором. Скорость вращения в **RPM**
  (оборотов в минуту): 5400 и 7200 у обычных дисков, 10 000 и 15 000 у старых
  серверных.
- **Дорожка** (track) — концентрическое кольцо на поверхности пластины. На
  одной поверхности их сотни тысяч.
- **Сектор** (sector) — участок дорожки, минимальная единица чтения и записи.
  Исторически 512 байт, у современных дисков физически 4 КБ (часто с эмуляцией
  512-байтных секторов для совместимости). Прочитать полсектора диск не умеет.
- **Головка** (head) — читает и пишет, парит над поверхностью на расстоянии в
  единицы нанометров. Головки всех поверхностей закреплены на одном рычаге и
  двигаются вместе.

```text
   Вид на пластину сверху:

        ________________
      /     дорожки      \
     /   ____________      \
    /   /   ______   \      \
   |   |   /  ()  \   |      |     () — шпиндель
   |   |   \______/   |      |
    \   \____________/      /
     \                     ● ←─── головка на рычаге
      \________________   /│
                           │ рычаг двигает головку
                             к центру и от центра
```

Чтобы прочитать сектор, нужно поставить головку над его дорожкой и дождаться,
пока сектор проедет под ней.

## Три составляющие времени доступа

```text
  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 КБ читаются за сотые доли миллисекунды.

Время одного оборота легко посчитать:

```text
  T_оборота (мс) = 60 000 / RPM

   5400 RPM → 11,1 мс/оборот → средняя rotation ≈ 5,6 мс
   7200 RPM →  8,3 мс/оборот → средняя rotation ≈ 4,2 мс
  15000 RPM →  4,0 мс/оборот → средняя rotation ≈ 2,0 мс
```

Вывод: две из трёх составляющих — механика, и именно они занимают почти всё
время. Электроника передаёт данные быстро, но сначала надо дождаться, пока
железо физически встанет в нужное место.

## Случайный доступ против последовательного

Посчитаем на числах. Диск 7200 RPM, средний seek 9 мс, последовательная
скорость 150 МБ/с. Сколько стоит прочитать один случайный блок на 4 КБ?

```go play
package main

import "fmt"

func main() {
	const (
		rpm        = 7200.0
		seekMs     = 9.0   // средний seek из паспорта диска
		transferMB = 150.0 // МБ/с при чтении подряд
		blockKB    = 4.0
	)

	rotationMs := 60000.0 / rpm / 2                  // в среднем полоборота
	transferMs := blockKB / 1024 / transferMB * 1000 // 4 КБ при 150 МБ/с
	accessMs := seekMs + rotationMs + transferMs

	iops := 1000 / accessMs
	randomMBs := iops * blockKB / 1024

	fmt.Printf("rotation:   %.2f мс\n", rotationMs)
	fmt.Printf("transfer:   %.3f мс\n", transferMs)
	fmt.Printf("доступ:     %.2f мс\n", accessMs)
	fmt.Printf("random 4К:  %.0f IOPS = %.2f МБ/с\n", iops, randomMBs)
	fmt.Printf("sequential: %.0f МБ/с, разница в %.0f раз\n", transferMB, transferMB/randomMBs)
}
```

```text
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](/os/book/fsync-durability));
- Kafka хранит сообщения как лог, который только дописывается в конец, поэтому
  нормально работает даже на HDD;
- LSM-деревья копят записи в памяти и сбрасывают большими последовательными
  кусками (см. [LFS](/os/book/lfs));
- файловые системы стараются класть блоки одного файла рядом (см.
  [реализацию ФС](/os/book/fs-implementation)).

## Планирование запросов к диску

Раз seek такой дорогой, а в очереди к диску обычно ждут несколько запросов,
их можно переупорядочить так, чтобы головка двигалась меньше. Этим занимается
**планировщик ввода-вывода** (I/O scheduler). Задача похожа на
[планирование процессов](/os/book/scheduling), только ресурс — движение головки.

Пример, на котором будем сравнивать политики: головка над дорожкой **53**, в
очереди запросы к дорожкам **98, 183, 37, 122, 14, 124, 65, 67** (в порядке
поступления). Мерить будем суммарный путь головки в дорожках — он примерно
пропорционален суммарному времени seek.

### FIFO — в порядке поступления

```text
  53 → 98 → 183 → 37 → 122 → 14 → 124 → 65 → 67

  шаги: 45 + 85 + 146 + 85 + 108 + 110 + 59 + 2 = 640
```

Головка мечется через весь диск: 183 → 37, 122 → 14, 14 → 124. Справедливо,
но 640 дорожек пути.

### SSTF — ближайший первым

**SSTF** (shortest seek time first) каждый раз выбирает запрос, ближайший к
текущему положению головки.

```text
  от 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 — лифт

**SCAN** (его называют «лифтом», elevator) едет в одну сторону, обслуживая всё
по пути, потом разворачивается и едет обратно. Как лифт, который сначала везёт
всех вверх, потом всех вниз.

```text
  едем вверх от 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), зато никто не голодает:
любой запрос будет обслужен самое позднее за два прохода головки.

### C-SCAN — всегда в одну сторону

У SCAN есть перекос: дорожки в середине диска головка проезжает дважды за
цикл (туда и обратно), а крайние — один раз, и там запросы в среднем ждут
дольше. **C-SCAN** (circular SCAN) обслуживает запросы только при движении в
одну сторону, а доехав до конца, быстро возвращается в начало без обслуживания.
Время ожидания становится равномернее для всех дорожек.

| Политика | Путь в примере | Плюс | Минус |
|---|---|---|---|
| FIFO | 640 | справедливо, просто | головка мечется |
| SSTF | 236 | меньше всего пути | голодание дальних запросов |
| SCAN (LOOK) | 299 | нет голодания, предсказуемо | середина обслуживается чаще краёв |
| C-SCAN | — | равномерное ожидание | холостой возврат |

Все три политики вы реализуете в Go-задаче
[«Суммарное перемещение головки диска»](/os/tasks/disk-seek), а в симуляторе
ниже можно переключать политики на том же примере и смотреть путь головки.

> Интерактивный симулятор «Планирование запросов к диску»: Головка ходит по цилиндрам. Переключайте политику (FIFO, SSTF, SCAN) и смотрите, как меняется путь головки: суммарное перемещение примерно пропорционально времени на seek. SCAN здесь разворачивается на самом дальнем запросе, а не на краю диска (в литературе это вариант LOOK), поэтому его цифры чуть лучше, чем у «настоящего» лифта. Запустить: https://graphlms.ru/os/book/hdd

## Кто на самом деле планирует

Современный диск знает свою геометрию точнее ОС: где какой сектор, с какой
скоростью вращается пластина прямо сейчас. Поэтому значительная часть
планирования живёт в прошивке. Через механизм **NCQ** (native command queuing)
ОС отдаёт диску до 32 запросов сразу, и диск сам выбирает порядок, учитывая и
seek, и вращение. Такую политику называют **SPTF** (shortest positioning time
first): иногда выгоднее съездить на дальнюю дорожку, где нужный сектор подъедет
через миллисекунду, чем на ближнюю, где ждать почти полный оборот.

Планировщик ОС делает остальное: склеивает соседние запросы в один большой
(merging), следит, чтобы запросы не ждали слишком долго, и делит диск между
процессами. В Linux текущий планировщик устройства видно так:

```bash
cat /sys/block/sda/queue/scheduler
```

В квадратных скобках — активный, например `[mq-deadline] kyber bfq none`.
`mq-deadline` упорядочивает запросы, но гарантирует каждому крайний срок (лечит
голодание), `bfq` делит пропускную способность между процессами по весам,
`none` не делает ничего. Для HDD обычно стоит `mq-deadline` или `bfq`, для NVMe —
`none`: SSD нечего экономить на позиционировании, и лишний слой только добавляет
задержку (почему — в [следующей главе](/os/book/ssd)).

> Квиз по разделу: см. вопросы в конце документа или на https://graphlms.ru/os/book/hdd

## Частые ошибки

- Ожидать от HDD тысяч случайных IOPS: потолок одного диска — порядка 100–200.
- Оценивать диск по последовательной скорости из паспорта, когда нагрузка случайная.
- Путать SCAN и LOOK: в задаче и симуляторе курса разворот происходит на крайнем запросе.
- Ставить на NVMe планировщик, рассчитанный на HDD, и наоборот оставлять `none` на HDD.
- Забывать, что SSTF может бесконечно откладывать дальние запросы.

## Проверьте себя

1. Почему из 13 мс случайного чтения 4 КБ на передачу данных уходит меньше 0,1 мс?
2. Чему равна средняя вращательная задержка у диска 5400 RPM?
3. Почему в примере SSTF после дорожки 67 идёт к 37, а не к 98?
4. Посчитайте суммарный путь для примера из главы по 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 с учётом вращения).

## Вопросы для самопроверки

**Квиз «Жёсткие диски и планирование»**

1. Какая составляющая времени доступа к HDD обычно самая дорогая?
   - Seek — подведение головки к дорожке (верно)
   - Проверка контрольной суммы
   - Декодирование команды контроллером
   - Transfer — передача данных
   - Пояснение: Seek — это физическое перемещение тяжёлого рычага с головкой, чистая механика, единицы миллисекунд. Transfer (передача данных) выполняется быстрой электроникой. Rotation тоже механика, но seek обычно дороже.
2. Почему случайный доступ к HDD на порядки медленнее последовательного?
   - Контроллер не умеет читать вразнобой
   - При случайном доступе seek и rotation платятся перед каждым кусочком, а при последовательном — один раз (верно)
   - Случайные данные хуже сжимаются
   - Случайный доступ всегда идёт через swap
   - Пояснение: При последовательном чтении дорогая механика (seek+rotation) оплачивается один раз, дальше идёт дешёвый поток. При случайном — перед каждым блоком новый seek и rotation. Отсюда разница примерно в 100 раз.
3. Диск вращается со скоростью 7200 RPM. Чему примерно равна средняя вращательная задержка (rotation), в миллисекундах? Округлите до целого.
   - Ответ: 4
   - Пояснение: Один оборот = 60000/7200 ≈ 8.33 мс. Средняя rotation — половина оборота (нужный сектор в среднем на полпути), то есть ≈ 4.17 мс, округляя — 4 мс.
4. В чём главный недостаток политики SSTF (ближайший первым)?
   - Голодание: далёкие дорожки могут ждать бесконечно, если рядом постоянно появляются близкие (верно)
   - Она требует знать длину запроса заранее
   - Она работает только на SSD
   - Она всегда медленнее FIFO
   - Пояснение: SSTF жадно берёт ближайший запрос. Если головка крутится в плотном облаке близких запросов, далёкий запрос может ждать сколь угодно долго — это starvation. SCAN/C-SCAN решают это гарантированным обходом.
5. Какие политики планирования борются с голоданием дальних запросов за счёт движения головки «как лифт»?
   - SCAN (верно)
   - C-SCAN (верно)
   - FIFO
   - SSTF
   - Пояснение: SCAN и C-SCAN проходят диск как лифт (до края и назад / по кругу), гарантируя, что каждая дорожка рано или поздно будет обслужена. FIFO не оптимизирует seek, а SSTF как раз страдает голоданием.
6. Чем C-SCAN отличается от обычного SCAN?
   - C-SCAN работает без seek
   - C-SCAN обслуживает запросы по пути в обе стороны
   - C-SCAN, доехав до края, быстро перескакивает в начало и снова едет в ту же сторону, давая равномерные задержки (верно)
   - C-SCAN — это то же самое, что FIFO
   - Пояснение: Обычный SCAN проходит середину дважды за цикл, обслуживая её чаще краёв. C-SCAN после края делает холостой прыжок в начало и едет всегда в одну сторону — задержки по всем дорожкам распределяются равномернее.
