go-cache-lru: три версии за день и сутки под лимитом
Позавчера я писал про go-cache-lru, реализацию GOCACHEPROG, которая держит кеш сборки Go в заданном размере. Тот пост заканчивался списком того, что не проверено: вытеснение в реальной работе ни разу не срабатывало, а Windows не поддерживалась. 28 октября вышли версии 0.2.0, 0.3.0 и 0.4.01. Ниже — что в них изменилось и как кеш прожил следующие сутки.
0.2.0: вытеснение без пауз и тихий демон
Утром 28 октября кеш дошёл до 25 ГиБ из 30, не вытеснив ни одной записи2. Первый прогон вытеснения должен был удалить около 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 с процессорного времени3. Буфер перед временным файлом поднял скорость записи в бенчмарке со 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 МиБ4.
0.3.0: история по часам
Накопительные счётчики плохо показывают, как кеш работает сейчас: в процент попаданий с первого запуска входит прогрев. На одной машине stats давал 96.7 % за 22 часа, хотя за последние три с половиной часа было 98.7 %5. Увидеть второе число можно было, только вычитая два снимка руками.
Теперь демон хранит изменения счётчиков по часам за 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 по-прежнему не замерен.