controller-runtime cache определяет поведение Kubernetes контроллеров. Понимание этой модели напрямую влияет на latency, память и консистентность чтения.
Первый сбой в ментальной модели обычно проявляется под нагрузкой. Инженер ожидает, что r.Get() и r.List() ходят в kube-apiserver и возвращают актуальное состояние. На практике контроллер начинает принимать решения на основе устаревших данных, растёт потребление памяти, а поведение становится трудно объяснимым. Деградация усиливается, когда увеличивается количество reconcile-циклов и объектов. Корневая причина одна: controller-runtime cache воспринимается как оптимизация, хотя это фундамент всей архитектуры.
Реальная модель противоположна ожиданиям. controller-runtime читает из локального in-memory cache, который заполняется через list + watch. Это снижает нагрузку на control plane и делает чтения почти бесплатными по latency. Но компромисс очевиден: данные могут быть устаревшими (stale reads), а cache может занимать гигабайты памяти. Это прагматичный выбор Kubernetes: уменьшить давление на API server и etcd ценой eventual consistency и роста memory footprint.
Архитектура строится вокруг стандартного pipeline из client-go. Reflector делает initial list и затем держит watch с использованием resourceVersion. Это гарантирует, что события не теряются между snapshot и стримом изменений. Далее DeltaFIFO буферизует события и сохраняет их порядок для каждого объекта. Важно: он не схлопывает промежуточные состояния, поэтому обработчик может получить цепочку изменений. Indexer хранит объекты в памяти как map с индексами, что объясняет микросекундную latency для r.Get(). SharedIndexInformer связывает всё вместе и раздаёт события подписчикам, включая контроллеры.
Поверх этого слоя работает workqueue. Здесь происходит дедупликация по ключу namespace/name. Даже если объект получил несколько обновлений подряд, в очереди будет один элемент. Это критично для throughput: контроллер не перегружается событиями, а reconcile выполняется на уже актуальном состоянии из cache. Такой дизайн разделяет ответственность: informer обрабатывает поток событий, workqueue контролирует частоту обработки.
Отдельное внимание — консистентности. Когда контроллер читает объект, он получает версию с определённым resourceVersion. При записи через r.Update API server проверяет, что версия совпадает с текущей. Если нет — возвращается 409 Conflict. Это механизм optimistic concurrency control. Он делает систему безопасной без блокировок, но требует явной обработки конфликтов. Попытка игнорировать это приводит к неустойчивому поведению контроллера.
Инициализация тоже важна. manager прогревает cache до запуска reconcile. Это значит, что первый r.Get() уже работает с полностью заполненным snapshot. Нет состояния “пустого cache”. Если нужно читать напрямую из API до старта, используется отдельный APIReader. Это редкий, но осознанный обход стандартной модели.
Результат такого дизайна — предсказуемая нагрузка на API server и высокий throughput контроллеров. Но цена — сложность reasoning. Нужно учитывать, что:
- чтения идут из cache, а не из API,
- данные могут быть устаревшими,
- события приходят как поток, а не как финальное состояние,
- reconcile работает с дедуплицированной очередью.
Метрики в исходном материале не приводятся, но поведение системы хорошо объясняется архитектурой. Если держать в голове модель “read from cache, write to API, sync via watch”, большинство неожиданных эффектов становится объяснимыми.
В индустрии это давно устоявшийся подход. Kubernetes изначально строился вокруг watch, а не polling. controller-runtime лишь упаковывает эту модель в удобный API. Поэтому основная задача инженера — не выучить функции, а принять ограничения системы и проектировать контроллеры с учётом eventual consistency и особенностей cache.