Docker «Too Many Requests» при завантаженні образів: виправте обмеження реєстру як слід

Було корисно?

Помилка завжди одна й та сама. Ваш деплой «здається зеленим», поки раптом ні — і тоді всі вузли починають вигукувати:
toomanyrequests, 429, pull rate limit exceeded. Раптом ваша «незмінна інфраструктура»
виглядає зовсім мінливою: вона перетворюється на купу Pod’ів у стані Pending і провалених CI-завдань.

Тротлінг реєстру вже не рідкісний крайній випадок. Це передбачуваний результат сучасної поведінки: ефемерні ранери, автоскейлінг кластерів,
паралельні збірки, multi-arch образи та колективна нездатність лишити все як є. Виправимо це правильно — діагностуємо, що саме тротлить,
зупинимо хвилі pull’ів, закешуємо, де можна, і зробимо ваш pipeline знову нудним.

Що насправді означає “too many requests”

«Too many requests» — це не одне явище. Це родина тротлів, що відбуваються на різних шарах, і виправлення залежить від того, який шар вас б’є.
Більшість команд трактують це як проблему Docker. Насправді це здебільшого проблема системного дизайну з Docker-подібним симптомом.

Звичайні прояви

  • HTTP 429 з кінцевої точки реєстру: класичне обмеження швидкості. Ви перевищуєте квоти по IP, по користувачу, по токену або по організації.
  • HTTP 403 з “denied: requested access”, що трапляється лише під навантаженням: іноді реєстр повертає оманливі помилки авторизації, коли вводить ліміти.
  • Kubernetes ImagePullBackOff / ErrImagePull з “toomanyrequests”: kubelet тягне образи на багатьох вузлах одночасно. Реєстр каже «ні».
  • Збої CI, коли паралельні джоби знову й знову тягнуть той самий базовий образ. Образ «закешований» лише теоретично.
  • Не тротлінг, але схоже: DNS-збої, проблеми з MTU, корпоративні проксі чи TLS-інтерцепція можуть створити шторм повторних спроб, що нагадує лімітування.

Де може застосовуватися тротлінг

Тротлінг може відбуватися на реєстрі, на CDN перед реєстром, на корпоративному egress-проксі або на вашому NAT-шлюзі.
Навіть ви самі можете тротлити себе: conntrack-таблиці, вичерпання епhemeral-портів або локальне дзеркало з недостатніми ресурсами.

Корисна ментальна модель: «docker pull» — це не один запит. Це послідовність отримань токенів, запитів маніфестів і завантажень шарів — часто багато шарів, іноді для кількох архітектур.
Помножте це на 200 CI-завдань або 500 вузлів, і ви сконструювали генератор DoS за допомогою YAML.

Одна перефразована думка від John Allspaw (операції/надійність): Надійність походить від проєктування на випадок відмов, а не від сподівання, що відмов не буде.
Ставтеся до тротлінгу реєстру так само: як до відомого режиму відмов, навколо якого треба спроєктувати систему.

Жарт №1: Pull storm — це просто спосіб вашої інфраструктури сказати, що вона сумує за днями, коли у відмов була одна коренева причина.

Швидкий покроковий план діагностики (перший/другий/третій)

Коли продакшн горить, у вас немає часу на інтерпретативні танці з логами. Ось найшвидший шлях до вузького місця.

Перший: підтвердьте, що це реальне лімітування (а не мережа)

  1. На ураженому вузлі/ранері відтворіть з одним pull (не весь деплой).
  2. Шукайте HTTP 429 і заголовки RateLimit.
  3. Перевірте, чи збігаються відмови з NAT IP (всі вузли виходять через один IP? Ви ділите квоту).

Другий: визначте площу ураження та патерн трафіку

  1. Це один образ/тег чи все підряд?
  2. Це багато вузлів одночасно (масштабування кластера, rolling restart, заміна вузлів)?
  3. Це паралелізм CI (50 джобів стартують одночасно) чи робочі станції розробників (понеділок зранку)?

Третій: оберіть клас пом’якшення

  • Короткострокове пом’якшення: уповільніть pulls (стаггеринг деплойmente), повторно використовуйте вузли, pre-pull, збільшіть backoff, зменшіть конкарренцію.
  • Середньострокове: аутентифікуйте pulls, pin-те digests, зменшіть розмір/шари образу, припиніть перевбудовувати ідентичні теги.
  • Довгострокове: додайте кеш-проксі/дзеркало, запустіть приватний реєстр, реплікуйте критичні образи та налаштуйте CI для повторного використання кешу.

Цікаві факти та історичний контекст

  • Тротлінг Docker Hub посилили у 2020, і багато «безкоштовних» робочих процесів, що покладалися на анонімні pulls, стали крихкими за одну ніч.
  • Розповсюдження контейнерних образів запозичує ідеї з Git і пакетних репозиторіїв, але на відміну від apt/yum, образи великі й тягнуться паралельно — добре для швидкості, погано для квот.
  • OCI-spec уніфікував формат, що підвищило портативність, але й дозволило інструментам однаково сильно навантажувати ті самі реєстри.
  • Content-addressed шари означають, що ідентичні шари повторно використовуються в різних тегах і образах — якщо кеші залишаються. Ефемерні ранери втрачають цю перевагу.
  • CDN фронтують більшість публічних реєстрів; ви можете отримувати ліміти на краю мережі, навіть якщо origin-реєстр у порядку.
  • Kubernetes зробив pull storms нормою: один деплой може викликати сотні майже одночасних pulls під час churn вузлів або автоскейлу.
  • Multi-arch образи збільшили кількість запитів: клієнт може спочатку завантажити індекс-маніфест, потім архітектурно-специфічні маніфести, а потім шари.
  • NAT-шлюзи концентрують ідентичність: тисяча вузлів за одним публічним IP може виглядати як один надто нетерплячий клієнт.
  • Авторизація реєстру використовує bearer-токени: кожен pull може включати запити до token service; ці кінцеві точки можуть тротлитися окремо.

Практичні завдання: команди, виводи та рішення

Це польові інструменти. Кожне завдання включає команду, що ви можете побачити, і яке рішення прийняти далі. Запускайте їх на вузлі/ранері, що дає збій.
Не «виправляйте» нічого, поки не зробите принаймні перші чотири.

Task 1: Reproduce a single pull with verbose-ish output

cr0x@server:~$ docker pull nginx:1.25
1.25: Pulling from library/nginx
no matching manifest for linux/amd64 in the manifest list entries

Meaning: This isn’t throttling. It’s an architecture mismatch (common on ARM runners or weird base images).
Decision: Fix the image/tag/platform selection before chasing rate limits.

cr0x@server:~$ docker pull redis:7
7: Pulling from library/redis
toomanyrequests: You have reached your pull rate limit. You may increase the limit by authenticating and upgrading

Meaning: This is real rate limiting from the registry (classic Docker Hub wording).
Decision: Move immediately to authentication + caching/mirroring; slowing down may buy time but won’t fix the class of problem.

Task 2: Identify your egress IP (are you sharing quota behind NAT?)

cr0x@server:~$ curl -s https://ifconfig.me
203.0.113.42

Meaning: That’s the public IP the registry sees.
Decision: If many nodes/runners show the same IP, assume quota is effectively shared. Plan for a mirror or split egress.

Task 3: Check Docker daemon and container runtime logs for 429 and auth churn

cr0x@server:~$ sudo journalctl -u docker --since "30 min ago" | tail -n 30
Jan 03 10:41:22 node-7 dockerd[1432]: Error response from daemon: toomanyrequests: Rate exceeded
Jan 03 10:41:22 node-7 dockerd[1432]: Attempting next endpoint for pull after error: Get "https://registry-1.docker.io/v2/": too many requests

Meaning: The daemon is getting throttled at the registry endpoint.
Decision: Continue to measure pull concurrency and introduce caching/mirroring.

Task 4: Confirm registry identity and headers (429 vs proxy)

cr0x@server:~$ curl -I -s https://registry-1.docker.io/v2/ | head
HTTP/2 401
content-type: application/json
docker-distribution-api-version: registry/2.0
www-authenticate: Bearer realm="https://auth.docker.io/token",service="registry.docker.io"

Meaning: 401 here is normal; it proves you’re hitting the expected registry and auth flow.
Decision: If you see corporate proxy headers or an unexpected server, you may be throttled or blocked upstream by your proxy/CDN path.

Task 5: Inspect per-node image cache (are you re-pulling because nodes are fresh?)

cr0x@server:~$ docker images --digests | head
REPOSITORY   TAG     DIGEST                                                                    IMAGE ID       CREATED        SIZE
nginx        1.25    sha256:2f7f7d3f2c0a7a6e1f6b0c1a3bcbf5b0e6c2e0d2a3a2e9a0b1c2d3e4f5a6b7c   8c3a9d2f1b2c   2 weeks ago    192MB

Meaning: The digest indicates content addressability; if you pin this digest, you can be more deterministic.
Decision: If nodes don’t have the image, you either need pre-pulls, longer-lived nodes, or a mirror that makes cache hits local.

Task 6: Check Kubernetes events for pull storms and backoff

cr0x@server:~$ kubectl get events -A --sort-by='.lastTimestamp' | tail -n 12
default   8m12s   Warning   Failed     pod/api-7c8d9c6d9c-7lqjv     Failed to pull image "nginx:1.25": toomanyrequests: Rate exceeded
default   8m11s   Normal    BackOff    pod/api-7c8d9c6d9c-7lqjv     Back-off pulling image "nginx:1.25"

Meaning: Kubelet is repeatedly retrying. Retries increase request volume. Volume increases throttling. You see the loop.
Decision: Stop the loop: pause rollouts, reduce replica churn, and put a cache in front of the registry.

Task 7: Quantify how many nodes are pulling simultaneously

cr0x@server:~$ kubectl get pods -A -o wide | awk '$4=="ContainerCreating" || $4=="Pending"{print $1,$2,$4,$7}' | head
default api-7c8d9c6d9c-7lqjv ContainerCreating node-12
default api-7c8d9c6d9c-k3q2m ContainerCreating node-14
default api-7c8d9c6d9c-px9z2 ContainerCreating node-15

Meaning: This is a live pull storm: many pods blocked on image pulls at once across nodes.
Decision: Stagger rollout or scale in, then implement pre-pulling or a DaemonSet cache warm-up, plus mirror/caching.

Task 8: Validate imagePullPolicy isn’t sabotaging you

cr0x@server:~$ kubectl get deploy api -o jsonpath='{.spec.template.spec.containers[0].imagePullPolicy}{"\n"}'
Always

Meaning: Always guarantees a registry hit even if the image exists locally. That’s fine for “latest” habits; it’s awful under rate limiting.
Decision: If you use immutable tags or digests, change to IfNotPresent and pin images properly.

Task 9: Check if you’re using “latest” (a polite way to say “non-deterministic”)

cr0x@server:~$ kubectl get deploy api -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
myorg/api:latest

Meaning: You can’t reason about caching when tags float. Every node might legitimately need a fresh pull at the same time.
Decision: Stop using :latest in production. Use a version tag and/or pin by digest.

Task 10: Confirm containerd is the actual runtime (and where to configure mirrors)

cr0x@server:~$ kubectl get nodes -o jsonpath='{.items[0].status.nodeInfo.containerRuntimeVersion}{"\n"}'
containerd://1.7.13

Meaning: You need containerd mirror configuration, not Docker daemon configuration (even if you still say “docker pull” out of habit).
Decision: Configure registry mirrors in containerd and restart it carefully (drain node, restart, uncordon).

Task 11: Inspect containerd registry config for mirrors (common on Kubernetes nodes)

cr0x@server:~$ sudo grep -n "registry" -n /etc/containerd/config.toml | head -n 30
122:[plugins."io.containerd.grpc.v1.cri".registry]
123:  config_path = ""

Meaning: No per-registry mirror config is currently used (or it’s external via config_path).
Decision: Add a mirror endpoint for Docker Hub (or whichever registry) via proper containerd configuration.

Task 12: Validate DNS and TLS quickly (rate limit lookalikes)

cr0x@server:~$ getent hosts registry-1.docker.io
2600:1f18:2148:bc02:6d3a:9d22:6d91:9ef2 registry-1.docker.io
54.85.133.21 registry-1.docker.io

Meaning: DNS resolves. If this fails intermittently, kubelet retries can mimic throttling with a similar operational impact.
Decision: If DNS is flaky, fix DNS first. Otherwise proceed to registry quota/caching.

Task 13: Check for conntrack or ephemeral port pressure during pull storms (self-inflicted throttling)

cr0x@server:~$ sudo conntrack -S | head
entries  18756
searched 421903
found    13320
new      9321
invalid  12
ignore   0
delete   534
delete_list 24
insert   9321
insert_failed 0
drop     0
early_drop 0

Meaning: If insert_failed or drops climb during storms, you’re losing connections locally, causing retries and more load.
Decision: Tune conntrack, reduce concurrency, or fix node sizing. Don’t blame the registry until your own house is in order.

Task 14: See if your CI runners are caching anything at all

cr0x@server:~$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          3         1         1.2GB     1.1GB (90%)
Containers      1         0         12MB      12MB (100%)
Local Volumes   0         0         0B        0B
Build Cache     0         0         0B        0B

Meaning: Your runner is basically amnesiac. Build cache is zero; images are mostly reclaimable. That’s a recipe for repeated pulls.
Decision: Use persistent runners, shared cache (BuildKit), or pre-populated images. Or accept that you need a local proxy cache.

Task 15: Pin by digest and test a pull (reduces tag churn and surprises)

cr0x@server:~$ docker pull nginx@sha256:2f7f7d3f2c0a7a6e1f6b0c1a3bcbf5b0e6c2e0d2a3a2e9a0b1c2d3e4f5a6b7c
sha256:2f7f7d3f2c0a7a6e1f6b0c1a3bcbf5b0e6c2e0d2a3a2e9a0b1c2d3e4f5a6b7c: Pulling from library/nginx
Digest: sha256:2f7f7d3f2c0a7a6e1f6b0c1a3bcbf5b0e6c2e0d2a3a2e9a0b1c2d3e4f5a6b7c
Status: Image is up to date for nginx@sha256:2f7f7d3f2c0a7a6e1f6b0c1a3bcbf5b0e6c2e0d2a3a2e9a0b1c2d3e4f5a6b7c

Meaning: Digest pins make caching and rollouts predictable. If the image exists locally, the runtime can skip downloading layers.
Decision: For production, prefer digest pinning (or at least immutable version tags) and align pull policy accordingly.

Рішення, що працюють (і чому)

1) Authenticate pulls (yes, even for public images)

Анонімні pulls трактуються як суспільна утиліта. За утиліти наглядають і лічать. Якщо ваш продакшн залежить від анонімних pulls, він залежить від «щедрості когось іншого».
Це не стратегія — це ставлення.

Аутентифікація може підвищити ліміти й покращити атрибуцію. Також легше відслідкувати, хто і що тягне.
У Kubernetes це часто означає imagePullSecrets. У CI — це docker login з токеном і переконання, що джоби не ділять одну сильно тротловану ідентифікацію.

2) Stop pulling the same thing 500 times: add a caching mirror

Дзеркало/проксі кеш реєстру — доросле рішення. Ваш кластер має тягнути з чогось, що ви контролюєте (або принаймні ближче),
що потім один раз підвантажує з публічного реєстру і обслуговує багато клієнтів.

Варіанти включають:

  • Docker Registry у режимі proxy cache: просто, працює, але ви маєте це оперувати (диск, HA, бекапи).
  • Harbor proxy cache: важчий, але дає корпоративний контроль (проекти, RBAC, реплікація).
  • Артефактні реєстри хмарних провайдерів з pull-through кешуванням або паттернами реплікації (варіюється залежно від провайдера; перевіряйте ліміти).

Дзеркало має стояти близько до вузлів (той самий регіон/VPC), щоб зменшити латентність і трафік. Воно повинно використовувати швидкий диск і витримувати паралельні завантаження шарів.
І мати достатньо місця. Нічого не говорить «професійна експлуатація», як кеш, який вилучає гарячі шари кожну годину.

3) Pin by digest, and make pull policy match reality

Якщо ви pin-ите за digest і тримаєте imagePullPolicy: IfNotPresent, ви отримуєте детермінований контент і менше звернень до реєстру.
Якщо ж ви тримаєте плавні теги і Always, ви свідомо обираєте звертатися до реєстру. Це може бути прийнятно для малого дев-кластера, але безвідповідально в масштабі.

4) Pre-pull images deliberately (warm caches)

Якщо кластер збирається масштабуватися або ви знаєте, що деплой зачепить кожен вузол, підвантажте образ заздалегідь на кожен вузол до переключення трафіку.
Паттерн старої школи: нудний і дуже ефективний.

Підхід у Kubernetes: DaemonSet, який тягне образ (можливо нічого більше не робить), щоб вузли закешували його. Потім деплоїте реальне навантаження.
Це розподіляє pulls у часі і робить помилки видимими до основного релізу.

5) Reduce concurrency where it hurts

Ви цілком можете тротлити себе менше, ставши менш ентузіастичними. Зменшіть паралельність джобів у CI, що всі тягнуть один і той же базовий образ.
Стегеруйте хвилі деплоїв. Уникайте рестартів по всьому кластеру в робочі години, якщо вам не подобається увага.

6) Make images smaller and more layer-reusable

Ліміти стосуються кількості запитів, але пропускна здатність і час все ще важливі. Менші образи означають швидші pulls, менше паралельних з’єднань, менше повторних спроб і менше часу в зоні ризику.
Також: менше шарів може зменшити загальну кількість запитів, але не ганяйтесь за «одним шаром» ціною втрати кешабельності. Хороший дизайн шарів — це мистецтво.

7) Control egress identity (split NAT, don’t concentrate all pulls)

Якщо весь ваш флот виходить через один NAT IP, ви зробили один IP відповідальним за найгірший момент: подію масштабування.
Розгляньте кілька egress IP, публічний egress на вузлах (обережно) або приватне з’єднання до вашого реєстру/дзеркала, коли можливо.

Жарт №2: NAT-шлюзи — як кавомашини в офісі: всі ними користуються, поки понеділок зранку не доведе, що це була помилка.

Помилки специфічні для Kubernetes (бо kubelet ніколи не тягне «трохи»)

Kubernetes перетворює «завантажити образ» на розподілену, конкурентну активність. Це ефективно, коли реєстр терплячий і кеші теплі.
Це катастрофа, коли у вас churn вузлів, холодні кеші й зовнішній реєстр зі суворими квотами.

ImagePullBackOff — це мультиплікатор

Backoff задумано, щоб зменшити навантаження, але в великих кластерах він стає механізмом синхронізації: багато вузлів падають, потім багато вузлів повторно намагаються одночасно,
особливо після мережевих глюків або відновлення реєстру. В результаті виникає thundering herd.

Node churn створює холодні кеші

Autoscaling, spot-інстанси та агресивна рециркуляція вузлів — усе це нормально, доки ви не усвідомите, що постійно створюєте нові машини з порожніми кешами.
Якщо реєстр тротлиться, холодні кеші — не дрібниця; це тригер простою.

Pull policy і дисципліна тегів важать більше, ніж здається

Kubernetes за замовчуванням ставить imagePullPolicy в Always при використанні :latest.
Це Kubernetes чемно радить вам не використовувати :latest, якщо вам важлива стабільність. Послухайте його.

containerd vs Docker: налаштуйте потрібне

Багато команд все ще «фіксять Docker» на вузлах Kubernetes, що працюють на containerd. Виправлення не спрацьовує, бо застосоване до сервісу, який не в шляху.
Спочатку ідентифікуйте runtime, а потім налаштуйте його підтримку mirror’ів правильно.

CI/CD: чому ваші ранери найгірші завантажувачі

CI-системи оптимізовані під пропускну здатність і одноразовість. Це добре для безпеки й відтворюваності.
Це також означає, що вони чудово підходять для багаторазового завантаження того самого базового образу, поки реєстр не скаже «йди геть».

Ефемерні ранери викидають дві найбільші переваги

  • Layer cache: content-addressed шари допомагають лише якщо ви їх зберігаєте.
  • Build cache: BuildKit може уникнути повторних завантажень і збірок, але не якщо кожен джоб стартує з чистого диску.

Паралелізм не безкоштовний

Провайдери CI та команди люблять паралельність. Реєстри — ні. Якщо вам потрібні 50 паралельних джобів, дайте їм спільне дзеркало у вашій мережі й аутентифікацію.
Інакше ви просто платите, щоб швидше виявити ліміти.

Дисципліна тегів зменшує безглузді pulls

Повторне використання того самого тега для різного контенту («ми знову перезаписали dev») змушує клієнтів перевіряти і перезавантажувати.
Використовуйте унікальні теги для кожної збірки і залишайте рухомий «людський» тег лише як вказівник.

Три корпоративні історії з практики

Інцидент: неправильне припущення («публічні образи — майже безкоштовні»)

Середньої величини SaaS-компанія керувала Kubernetes-кластером з агресивним автоскейлом. Команда використовувала переважно публічні базові образи — відомі, звичні —
і припустила, що інтернет усе витримає. У них був внутрішній реєстр, але лише для своїх образів. Усе інше тягнулося прямо з публічного реєстру.

В один завантажений робочий ранок вузли почали перезапускатися частіше через окремий rollout оновлення ядра в поєднанні зі spot-churn.
Нові вузли приєднувались із порожніми кешами. Kubelet зробив те, що він робить: усе потягнув. Одночасно. По всіх вузлах.

За кілька хвилин поди нагромадилися в ContainerCreating, а потім впали в ImagePullBackOff.
Розробник на чергуванні побачив «too many requests» і подумав, що це транзієнт. Він перезапустив кілька вузлів — ще більше створив холодних кешів і ще більше pulls.
Графік невдалих pulls став сходами невдалих рішень.

Коренева причина не була в «поганому Docker». Це було неправильне припущення: що ємність публічного реєстру й квоти відображають їхню поведінку масштабування.
Виправлення було простим, але не швидким: аутентифікація, проксі-кеш для публічного реєстру та pre-pull критичних образів під час провізії вузлів.
Після цього churn вузлів залишився дратівливим, але перестав бути тригером простою.

Оптимізація, що вилетіла в бік: «видалимо кеші, щоб зекономити диск»

Інша компанія пишалася своїми худими вузлами. Вони запускали job на кожному вузлі, щоб агресивно очищати образи. Диски виглядали ідеально.
Місячні слайди інфра-звіту виглядали чудово: «Ми зменшили марнотратне зберігання на 40%». Усі кивали.

Потім вони ввели canary-стратегію, що часто рулила по флоту. Кожен rollout викликав хвилю pulls.
Але оскільки job prune видалив більшість шарів, кожен вузол поводився, ніби він щойно створений.

Реєстр почав їх тротлити час від часу. Коли тротлінг вдарив, поди не проходили readiness, canary тримався, автоматика повторювалася, і весь pipeline продовжував страждати.
Веселий момент: інцидент був періодичним і тому ідеально витрачав людський час.

«Оптимізація» заощадила диск і витратила надійність. Вирішення — припинити трактувати кеші образів як сміття.
Вони встановили розумні пороги диску, зберегли базовий набір гарячих образів на вузлах і перенесли агресивне прибирання у непік.
Зберігання дорожче простою, і також дорожче за людей.

Сумно, але правильно, що врятувало день: «pre-pull і pin»

Команда фінансових сервісів керувала регульованими робочими навантаженнями з жорстким контролем змін. Їхній процес релізу був повільний, що дратувало розробників,
але він мав звичку, яка виплатила орендну плату: кожен release candidate був pinned за digest і перед релізом pre-pull`нувся по всьому кластеру.

Одного вечора публічний реєстр почав тротлити в їхньому регіоні через зовнішню подію. Інші команди панікували й відкатувалися.
Ця команда майже не помітила. Їхні вузли вже мали потрібні шари локально, і деплой використовував IfNotPresent.

Вони все ще бачили помилки в фоні для несуміжних образів, але продакшн-реліз пройшов безпомилково.
Звіт на чергуванні був майже соромним: «Без впливу на клієнтів. Спостерігався зовнішній тротлінг. Продовжили як планувалося.»
Це мрія: зовнішній світ може горіти, а ваша система — байдужа.

Урок не в тому, щоб «бути повільними як фінанси». Урок у тому, що нудні практики — pinning, pre-pull, контрольовані релізи — створюють запас.
Запас — це те, що не дає зовнішнім залежностям перетворюватися на відмови.

Типові помилки: симптом → коренева причина → виправлення

1) Симптом: “toomanyrequests” тільки під час деплоїв

Коренева причина: хвилі pull’ів через одночасні релізи, масштабування або заміну вузлів.

Виправлення: stagger релізи, pre-pull через DaemonSet, додати кешуюче дзеркало і припинити використання плаваючих тегів.

2) Симптом: працює на ноутбуках, падає в CI

Коренева причина: машини розробників мають теплі кеші; CI-ранери ефемерні й стартують холодними щоразу.

Виправлення: персистентні ранери або спільний кеш, аутентифікація pulls, проксі-кеш в мережі.

3) Симптом: випадкові 403/401 під навантаженням

Коренева причина: тротлінг token service або неправильне трактування помилок авторизації під лімітом.

Виправлення: аутентифікуйтесь належним чином, уникайте спільного використання токенів при масовій паралельності, і перевіряйте заголовки/логи щоб відрізнити 429 від помилок auth.

4) Симптом: лише один кластер/регіон уражений

Коренева причина: конкретний egress IP перегрітий, або регіональний CDN PoP застосовує суворішу політику.

Виправлення: розділити egress/NAT, розгорнути регіональне дзеркало, або реплікувати критичні образи ближче.

5) Симптом: «ми маємо дзеркало», але pulls все одно йдуть на Docker Hub

Коренева причина: дзеркало налаштовано для Docker daemon, а вузли використовують containerd; або hostname дзеркала не довірений; або оновлено не всі вузли.

Виправлення: підтвердьте runtime, налаштуйте mirror на правильному рівні, прокатіть з draining вузлів і протестуйте контрольний pull.

6) Симптом: тротлінг погіршав після змін з “cleanup”

Коренева причина: агресивне прибирання видалило гарячі шари; кожен rollout став холодним стартом.

Виправлення: тримайте базові образи, налаштуйте пороги GC і синхронізуйте прибирання з реальними ризиками (тиск на диск), а не з естетикою.

7) Симптом: збої pull виглядають як тротлінг, але немає 429

Коренева причина: DNS-флапи, MTU/TLS-інтерцепція, проксі-resets, вичерпання conntrack або локальне насичення мережі.

Виправлення: перевірте DNS/TLS, стежте за conntrack/портами, дивіться логи проксі і зменшіть одночасні pulls, поки не виправите мережеві фундаментальні проблеми.

Чеклісти / покроковий план

Phase 0: stabilize production (today)

  1. Зупиніть thundering herd: зупиніть/уповільніть релізи; тимчасово зменшіть репліки, якщо безпечно.
  2. Аутентифікуйте pulls для уражених систем негайно (CI і вузли кластера там, де можливо).
  3. Pin the release artifact (тег або digest), щоб повторні спроби не тягнули рухомий об’єкт.
  4. Зменшіть concurrency: паралельність CI, maxUnavailable/maxSurge для rollout, агресивність autoscaler.
  5. Оберіть один тестовий вузол і підтвердіть чистий шлях pull перед повторною спробою релізу.

Phase 1: stop depending on public registry behavior (this week)

  1. Розгорніть кешуюче дзеркало реєстру поруч з кластером/ранерами.
  2. Налаштуйте containerd/Docker на використання дзеркала (і підтвердіть, що воно реально використовується).
  3. Pre-pull критичні образи на вузлах (DaemonSet warm-up або bootstrap вузла).
  4. Виправте теги/pull policy: припиніть використовувати :latest, використовуйте IfNotPresent з immutable тегами/digest.
  5. Зробіть помилки видимими: алерт на ImagePullBackOff та 429 в логах реєстру.

Phase 2: make it boring and resilient (this quarter)

  1. Реплікуйте залежності: mirror/replicate сторонні образи у свій реєстр.
  2. Прийміть стратегію базових образів: менше базових образів, стандартизовані, регулярно оновлювані, зберігаються внутрішньо.
  3. Capacity plan для кеша: диск, IOPS, concurrency і вимоги до HA; ставтеся до нього як до продакшну.
  4. Керуйте concurrency: політики rollout у кластері, контролі паралельності CI і guardrails для масштабування.
  5. Проводьте game days: симулюйте відмови/тротлінг реєстру; впевніться, що система деградує контрольовано.

FAQ

1) Is this only a Docker Hub problem?

No. Any registry can throttle: public registries, cloud registries, and your own registry behind a load balancer.
Docker Hub is just the most famous place to get a 429 and a life lesson.

2) Why does rate limiting hit us “randomly”?

Because your traffic is bursty. Deploys, autoscaling, and CI fan-out create spikes.
Quotas are often enforced per window, per IP, or per token; once you cross the line, everyone sharing that identity suffers.

3) If we authenticate, are we done?

Authentication helps, but it doesn’t eliminate the architectural issue. You can still exceed authenticated quotas, and you can still melt your NAT/proxy.
Use auth as table stakes, not as the entire plan.

4) What’s the single best long-term fix?

A caching mirror/proxy close to your compute. It converts “internet dependency” into “local dependency,” and local dependencies are at least your problem to solve.

5) Should we pin by digest everywhere?

For production deployments, yes when feasible. Digests make rollouts deterministic and align nicely with IfNotPresent.
For dev workflows, immutable version tags may be enough, but “latest” is still a trap.

6) We already use IfNotPresent. Why are we still pulling?

Because the image isn’t present on that node (new node, pruned cache, different architecture), or because the tag points to new content and the runtime checks anyway.
Verify local cache presence and stop reusing tags for different builds.

7) Can we just increase Kubernetes backoff and be fine?

Backoff reduces immediate pressure but doesn’t solve the underlying demand. Also, synchronized backoff across many nodes can create waves of retries.
Use backoff tuning only as a stabilizer while you implement caching and policy fixes.

8) What about air-gapped or regulated environments?

You’ll end up running your own registry and curating images internally. The upside: no external throttling.
The downside: you must own patching, scanning, and availability. It’s still the right move for many regulated shops.

9) How do we know our mirror is actually being used?

Check runtime config on the node, then observe mirror logs/metrics during a pull. Also compare DNS lookups and outbound connections:
if nodes still talk to the public registry directly, your mirror is ornamental.

10) Could the bottleneck be our storage?

Yes—especially for self-hosted caches. If your proxy cache sits on slow disk, it will serialize layer reads and make pulls slow,
causing more concurrent pull attempts and more retries. Fast storage and concurrency matter.

Висновок: практичні наступні кроки

Тротлінг реєстру — це не випадкова аномалія. Це очікуваний результат сучасної еластичної інфраструктури, що б’є по зовнішньому сервісу.
Ваше завдання — не сподіватися, що це не повториться. Ваше завдання — зробити це неактуальним.

  1. Сьогодні: підтвердіть 429 vs мережеві проблеми, зупиніть pull storm, аутентифікуйтесь, pin-те релізний артефакт.
  2. Цього тижня: розгорніть кешуюче дзеркало близько до compute, налаштуйте реальний runtime (containerd/Docker) і pre-pull критичні образи.
  3. Цього кварталу: інтерналізуйте сторонні залежності, стандартизуйте базові образи і керуйте concurrency, щоб «автомасштабування» не означало «авто-відмову».

Зробіть доставку образів нудною. Нудною — це те, чого ви хочете о 3-й ночі.

← Попередня
ZFS на корені: як встановити, щоб відкат дійсно працював
Наступна →
PostgreSQL vs OpenSearch: гібридна пошукова конфігурація, яка справді працює

Залишити коментар