# go-cache-lru: три версии за день и сутки под лимитом


Позавчера я писал про [go-cache-lru](/2026/10/27/go-cache-lru/), реализацию GOCACHEPROG, которая держит кеш сборки Go в заданном размере. Тот пост заканчивался списком того, что не проверено: вытеснение в реальной работе ни разу не срабатывало, а Windows не поддерживалась. 28 октября вышли версии 0.2.0, 0.3.0 и 0.4.0[^releases]. Ниже — что в них изменилось и как кеш прожил следующие сутки.

## 0.2.0: вытеснение без пауз и тихий демон

Утром 28 октября кеш дошёл до 25 ГиБ из 30, не вытеснив ни одной записи[^lock]. Первый прогон вытеснения должен был удалить около 5 ГиБ, и тут всплыла проблема: файлы объектов удалялись под мьютексом кеша. Пока шла пачка из 256 удалений, ждали все `get` и `put` всех команд `go`. Теперь под блокировкой объект только помечается, а файл удаляется после её снятия. Бенчмарк на 2000 объектах по 256 КиБ показал, как изменилась задержка `get` во время вытеснения:

| | До | После |
|---|---|---|
| Максимальная задержка `get` | 30–38 мс | 0.12–0.16 мс |
| `get` дольше 1 мс | 4 | 0 |
| `get`, обслуженных за время вытеснения | около 650 | около 30 000 |

Само вытеснение быстрее не стало, но сборки его больше не ждут.

Запись объектов стала дешевле. Тело `put` приходит в base64 и декодируется кусками не больше 768 байт, а `io.Copy` превращал каждый кусок в отдельный системный вызов `write`. На холодной сборке, которая положила в кеш 1.8 ГиБ, демон потратил на эти вызовы 12.7 с из 16 с процессорного времени[^writes]. Буфер перед временным файлом поднял скорость записи в бенчмарке со 156–175 до 486–500 МБ/с. Второе узкое место — сокет между командой `go` и демоном: macOS даёт unix-сокетам буферы по 8 КиБ, и данные шли мелкими порциями. Обе стороны теперь просят 1 МиБ. На холодном `go test -race` большого сервиса процессорное время демона упало с 8.5 до 7.8 с, а промежуточного процесса — с 3.4 до 2.1 с.

Демон перестал просыпаться без дела. Без единой команды `go` он всё равно просыпался примерно раз в секунду: индекс сбрасывался по таймеру каждые 100 мс, метка `go clean -cache` проверялась каждые 2 с, а `stats.json` перезаписывался каждые 30 с. Теперь каждый таймер работает только при наличии работы. По `top -stats idlew` за три минуты простоя после сборки было 129 пробуждений, стало 0.

Ещё два изменения про удобство. Демон замечает, что его бинарник заменили, и завершается, так что после обновления больше не нужен `go-cache-lru stop`. `stats -v` показывает, чем занят кеш: объекты по размеру и по времени последнего обращения. Раньше для этого приходилось лезть в `index.db` через `sqlite3`, и так выяснилось, что 72 % кеша в 25 ГиБ занимали 407 архивов пакетов примерно по 90 МиБ[^breakdown].

## 0.3.0: история по часам

Накопительные счётчики плохо показывают, как кеш работает сейчас: в процент попаданий с первого запуска входит прогрев. На одной машине `stats` давал 96.7 % за 22 часа, хотя за последние три с половиной часа было 98.7 %[^hourly]. Увидеть второе число можно было, только вычитая два снимка руками.

Теперь демон хранит изменения счётчиков по часам за 7 дней в той же базе SQLite, вместе с пиковым размером кеша за час. `stats` показывает текущий час, сутки и неделю, `stats -v` добавляет почасовую таблицу. По этой истории `stats` даёт совет про `--max-size`. Пока часов с командами `go` меньше 24, он ждёт данных. Если вытеснений не было, а пик не дошёл до 60 % лимита, совет — пик плюс 25 %. Если вытеснения были, он сравнивает долю промахов в час после вытеснения с обычной. Когда после трёх и более вытеснений промахов в полтора раза больше, он предлагает лимит на 50 % выше. Пороги — первая прикидка, их надо проверить на настоящей истории.

Обновление стало мягче. В 0.2.0 демон, заметив новый бинарник, сразу переставал принимать подключения, и новые команды `go` ждали, пока закончатся старые. Теперь он продолжает обслуживать и выходит в первый момент, когда к нему не подключена ни одна команда `go`.

Управляющим командам больше не нужен `--dir`. `stats`, `trim`, `clean` и `stop` берут флаги из `GOCACHEPROG` в окружении или в `go env`, поэтому статистика теперь — просто `go-cache-lru stats`.

## 0.4.0: Windows

Блокировка демона и запуск в фоне работали через `flock` и `setsid`, которых в Windows нет. Теперь там используются `LockFileEx` и отдельная группа процессов, а различия собраны в файлах `*_windows.go`. По дороге нашлись две ошибки: путь с буквой диска ломал URI базы SQLite, а два одновременных `put` одного объекта переименовывали второй файл поверх первого, что Windows запрещает, пока файл кто-то читает. Содержимое у них одинаковое, поэтому теперь остаётся файл, который уже лежит на диске. Тесты гоняются в CI и на Windows, бинарники выходят zip-архивами.

## Сутки на 0.4.0

Демон v0.4.0 запустился 28 октября в 17:22, через несколько секунд после тега, и проработал без перезапуска до вечера 29-го. Лимит прежний: `--max-size=30GiB`, `--target-size=25GiB`. Вот `stats -v` на 29 октября, 17:10, путь к домашнему каталогу заменён на `~`:

```
$ go-cache-lru stats -v
daemon          pid 48388, version v0.4.0, up 23h47m56s, 0 active sessions
directory       ~/.cache/go-lru
size            25.8 GiB of 30.0 GiB (target 25.0 GiB), 86.0%
entries         76155 actions, 14056 objects, 0 pinned
counters since  2026-10-27 16:38:51
hits            3542620 (97.4%), 775.3 GiB served
misses          95312
puts            174754, 80.3 GiB written
evictions       66727 entries, 50.6 GiB
cleans          0 explicit, 1 by go clean -cache
sessions        2337
this hour       no go commands
last 24 hours   hit rate 98.0%, 1639228 gets, 28.7 GiB written, 6 evictions
last 7 days     hit rate 97.9%, 2169649 gets, 38.5 GiB written, 7 evictions
advice          not enough history yet: 12 of 24 active hours

objects by size
  < 64 KiB               5675    43.2 MiB    0.2%
  64 KiB - 1 MiB         5823     2.4 GiB    9.3%
  1 MiB - 10 MiB         2096     4.8 GiB   18.7%
  10 MiB - 100 MiB        460    18.3 GiB   70.9%
  >= 100 MiB                2   253.8 MiB    1.0%

objects by last access
  < 1h                   1127   375.2 MiB    1.4%
  1h - 6h               12929    25.4 GiB   98.6%
  6h - 24h                  0         0 B    0.0%
  >= 24h                    0         0 B    0.0%

hourly, last 24 hours
  hour       gets  hit rate     written evictions   peak size
  18:00       710     99.7%     2.0 MiB         0    29.4 GiB
  09:00      9140     98.3%    94.2 MiB         0    29.5 GiB
  10:00     43177     99.6%    60.8 MiB         0    29.5 GiB
  11:00    303009     97.7%     5.0 GiB         1    30.0 GiB
  12:00    437705     97.8%     8.2 GiB         2    30.0 GiB
  13:00    152856     97.1%     3.5 GiB         1    30.0 GiB
  14:00    332627     98.3%     6.9 GiB         1    30.0 GiB
  15:00    352247     98.3%     5.0 GiB         1    30.0 GiB
  16:00      7757     99.6%     5.3 MiB         0    25.8 GiB
```

Главное здесь — столбец `peak size`. С 11 до 16 часов в кеш записали 28.6 ГиБ, а пик за каждый час остался на 30.0 ГиБ. Шесть раз кеш упирался в лимит, и шесть раз демон сжимал его до цели, убирая по 5–5.5 ГиБ за прогон. За всё время счётчиков вытеснено 66 727 записей, 50.6 ГиБ. До go-cache-lru кеш на этой машине дорастал до 66–85 ГБ и забивал диск.

Точнее, в байтах пик выходит за лимит на 0.4–31 МиБ. Вытеснение стартует в фоне после записи, которая перешагнула лимит, и пока оно идёт, успевают лечь ещё несколько объектов. В пересчёте на 30 ГиБ это меньше 0.1 %.

Доля попаданий от вытеснения не просела: в часы с вытеснением она 97.1–98.3 %, за сутки — 98 %. Кеш отдал 775.3 ГиБ при 80.3 ГиБ записанного.

Две строки стоит прочитать отдельно. В `objects by last access` всё, что осталось, использовалось за последние шесть часов, более старое вытеснено. Сборка ветки, к которой я не прикасался со вчера, начнёт с промахов. Стоит ли это лишних гигабайт на диске, должен ответить совет: на момент снимка у него 12 часов с командами `go` из 24 нужных. А `objects by size` подтверждает картину из 0.2.0: 460 объектов от 10 до 100 МиБ занимают 71 % места, и сколько всего поместится под лимит, решают они.

## Что не проверено

Совет по размеру ещё ни разу не выдал рекомендацию, и пороги в нём пока не сверены с настоящей историей. Запуск с холодным page cache по-прежнему не замерен.

[^releases]: [Релизы go-cache-lru](https://github.com/juev/go-cache-lru/releases), бинарники для macOS, Linux и Windows.
[^lock]: [Issue #3](https://github.com/juev/go-cache-lru/issues/3), удаление файлов вне блокировки кеша.
[^writes]: [Issue #12](https://github.com/juev/go-cache-lru/issues/12), профиль и замеры записи объектов.
[^breakdown]: [Issue #5](https://github.com/juev/go-cache-lru/issues/5), `stats -v` и флаг `--version`.
[^hourly]: [Issue #14](https://github.com/juev/go-cache-lru/issues/14), почасовая статистика и правила совета.

