<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Alexandr Kruchkov</title><generator>teletype.in</generator><description><![CDATA[Alexandr Kruchkov]]></description><image><url>https://img2.teletype.in/files/d7/1a/d71aebd3-1e54-44df-8b93-df48a3b6628e.png</url><title>Alexandr Kruchkov</title><link>https://teletype.in/@kruchkov_alexandr</link></image><link>https://teletype.in/@kruchkov_alexandr?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=kruchkov_alexandr</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/kruchkov_alexandr?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/kruchkov_alexandr?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Thu, 28 May 2026 04:51:52 GMT</pubDate><lastBuildDate>Thu, 28 May 2026 04:51:52 GMT</lastBuildDate><item><guid isPermaLink="true">https://teletype.in/@kruchkov_alexandr/K-2Mql5Hsok</guid><link>https://teletype.in/@kruchkov_alexandr/K-2Mql5Hsok?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=kruchkov_alexandr</link><comments>https://teletype.in/@kruchkov_alexandr/K-2Mql5Hsok?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=kruchkov_alexandr#comments</comments><dc:creator>kruchkov_alexandr</dc:creator><title>Поправить пятисотки на приложении в кубе - это просто!</title><pubDate>Thu, 07 May 2026 11:47:02 GMT</pubDate><description><![CDATA[<img src="https://img4.teletype.in/files/b5/9d/b59d250d-4201-4db4-b3f1-0dbd3a7a06bc.png"></img>Не мог не вставить эту глупую картинку peka face, потому что дебажить пятисотки в кубере в облаках на самом деле не так просто, как многим кажется, а заголовок статьи конечно же же наполнен жиром сарказма и иронии.]]></description><content:encoded><![CDATA[
  <figure id="Vjla" class="m_retina">
    <img src="https://img4.teletype.in/files/b5/9d/b59d250d-4201-4db4-b3f1-0dbd3a7a06bc.png" width="256" />
  </figure>
  <p id="OvBQ">Не мог не вставить эту глупую картинку peka face, потому что дебажить пятисотки в кубере в облаках на самом деле не так просто, как многим кажется, а заголовок статьи конечно же же наполнен жиром сарказма и иронии. </p>
  <p id="O4om">Итак - начнём.</p>
  <p id="ciEn"></p>
  <h2 id="vsZ5">Четыре недели с 502: как мы гасили ошибки в бот-сервисе одну за другой</h2>
  <p id="ZPmi"></p>
  <p id="rBKj">Расскажу историю о том, как суммарно около четырёх недель мы последовательно находили и устраняли причины 502-ошибок в одном из наших сервисов.</p>
  <p id="AYlG">Большая часть этого времени ушла не на сами фиксы - а на то, чтобы вообще понять что происходит. Пока не было нормальных логов и метрик, двигаться было некуда: каждая следующая причина становилась видна только тогда, когда предыдущая была закрыта и шум от неё уходил. Плюс нужно было просто накопить статистику - увидеть паттерн, а не единичный случай.</p>
  <blockquote id="wMop">Одна из тех историй, когда, починив что-то одно, видишь следующее - и так несколько итераций подряд.</blockquote>
  <p id="yBs1"></p>
  <h3 id="j3OQ">Кто такой бот-сервис</h3>
  <p id="aWso">У нас есть сервис, который занимается обработкой входящих вебхуков от внешнего корпоративного чат-инструмента - назовём его просто бот-сервис.</p>
  <p id="rPvI">Он крутится в Kubernetes, за ним AWS ALB, трафик идёт от ботового фреймворка вендора.</p>
  <p id="4c91">Написан на Node.js + Express. Ничего экзотического.</p>
  <p id="T87C">Схема выглядит так:</p>
  <pre id="gMNz">  Vendor Bot Framework
          |
          | HTTPS, keep-alive
          v
  +------------------+
  |       DNS        |   Route53 / Cloudflare
  +--------+---------+
           |
           v
  +------------------+   +----------------------------------+
  |     AWS ALB      |&lt;--| AWS Load Balancer Controller     |
  +--------+---------+   | - следит за K8s Endpoints        |
           |             | - регистрирует/дерегистрирует    |
           |             |   поды в target group ALB        |
    +------+------+      +----------------------------------+
    v             v
+----------+ +----------+
|  Pod 1   | |  Pod 2   |   (Pod N ...)
|  Node.js | |  Node.js |
|  :3000   | |  :3000   |
+----------+ +----------+</pre>
  <p id="oFII">Именно в этой цепочке и живут все проблемы, о которых пойдёт речь.</p>
  <p id="aF19"></p>
  <h3 id="SeOs">Как сервис стал боевым</h3>
  <p id="0db6">Долгое время бот-сервис жил в относительно тихом режиме: небольшая нагрузка, в основном внутреннее использование. Мониторинг был минимальный - сервис работает, и ладно.</p>
  <p id="aGFV">Потом сервис начал получать реальный трафик от живых пользователей. Стали потихоньку поглядывать на метрики - рестартов нет, CPU и память в пределах нормы, всё выглядело спокойно.</p>
  <p id="uGxJ">И вот однажды прилетел вопрос: &quot;а почему у нас 502 ошибки?&quot;</p>
  <p id="PiXy"></p>
  <h3 id="USG4">Первые 502: клиент жалуется, мы в темноте</h3>
  <p id="5Bkl">На тот момент не было почти никакой зацепки. Метрики приложения есть, Prometheus есть, Grafana есть. Но конкретно по ALB - тишина. ALB стоял давно, логирование для него никто не включал: всё работало, зачем.</p>
  <p id="GH2y">Понятно было только одно: где-то есть 502. Откуда, от кого, когда именно, что в это время делал бэкенд - непонятно совсем.</p>
  <p id="DFNj">Попробовали несколько вещей наугад - ничего не изменилось. Нужна была хоть какая-то зацепка.</p>
  <p id="CaM3"></p>
  <h3 id="gjTG">Первая гипотеза: keepAlive таймауты</h3>
  <p id="oYlV">Погуглили &quot;node.js alb 502&quot; - нашли сразу две статьи, которые объясняли одну и ту же механику. Ссылки будут в конце статьи.</p>
  <p id="lsIM">Суть такая: у Node.js http.Server дефолтный keepAliveTimeout равен 5 секундам.</p>
  <p id="a36I">У AWS ALB дефолтный idle timeout - 60 секунд.</p>
  <p id="HQSz">То есть ALB держит соединение открытым 60 секунд, а Node.js закрывает его уже через 5.</p>
  <p id="3wmD">Когда ALB пытается отправить новый запрос по уже закрытому соединению - получаем 502 и ECONNRESET на стороне приложения.</p>
  <p id="dK5v">Исправляется просто: выставить keepAliveTimeout на сервере чуть больше, чем таймаут ALB, чтобы Node.js никогда не закрывал соединение раньше балансера.</p>
  <pre id="NUcQ">server.keepAliveTimeout = 65000;
server.headersTimeout = 66000;</pre>
  <p id="Nhcb"></p>
  <blockquote id="BQd6">(headersTimeout должен быть чуть больше keepAliveTimeout - иначе Node.js может отбросить запрос, который пришёл в самый последний момент перед закрытием соединения)</blockquote>
  <p id="i3z9">Добавили это в server.ts, задеплоили. Стало заметно лучше - <u>часть 502 пропала</u>. Казалось, нашли причину и дальше заживём.</p>
  <p id="Fu5c">Но нет.</p>
  <p id="i48S">Ошибок стало меньше, но они не исчезли. Значит, была ещё какая-то другая причина - или несколько. И без нормальных данных двигаться дальше было некуда.</p>
  <p id="D8At"></p>
  <h3 id="hC9O">Шаг 0. Включаем логи ALB</h3>
  <p id="uKnt">Зашли в настройки load balancer в AWS Console - Attributes - Access logs - Enable.</p>
  <p id="Lzvn">Указали S3-бакет, включили все доступные на тот момент поля.</p>
  <p id="CWgW">Настроили сбор из S3 в OpenSearch через существующий pipeline.</p>
  <p id="pHY3">После этого у нас появился полноценный лог каждого запроса через балансер, включая:</p>
  <ul id="jpWg">
    <li id="rSak"><code>elb_status_code</code> - что ответил сам ALB клиенту</li>
    <li id="bYiw"><code>target_status_code</code> - что ответил под балансеру (или &quot;-&quot;, если не ответил вообще)</li>
    <li id="lHuF"><code>target_processing_time</code> - сколько под думал</li>
    <li id="X8Va"><code>request</code> - метод, URL, протокол</li>
    <li id="EESU"><code>user_agent</code> - кто стучится</li>
  </ul>
  <p id="dLYh">С этого момента стало можно нормально разбираться.</p>
  <p id="oONX"></p>
  <h3 id="otm3">Причина #1 - Rolling deploy без preStop hook</h3>
  <p id="9zSY">Полез в OpenSearch - там теперь лежат ALB access-логи.</p>
  <p id="xjWS">Запрос простой: ищем <code>elb_status_code</code>: 502 по нашему домену за последнюю неделю.</p>
  <p id="mmEn">Картина такая: несколько одиночных 502 в разное время суток, и один раз сразу 10 штук подряд. Примеры из лога:</p>
  <ul id="x0lz">
    <li id="6KZb">1 запрос, target_processing_time 0.084s, ответа от пода нет</li>
    <li id="n2GR">1 запрос, 0.476s, ответа нет</li>
    <li id="X4WT">1 запрос, 2.069s, ответа нет</li>
    <li id="3P6z">1 запрос, 1.284s, ответа нет</li>
    <li id="M56M">10 запросов подряд, ответа нет ни от одного</li>
  </ul>
  <p id="8FTo"><code>target_status_code</code>: null и <code>target_processing_time</code> меньше нескольких секунд - ALB до пода достучался, а вот HTTP-ответа так и не получил.</p>
  <blockquote id="mcQ2">Это не таймаут приложения, это обрыв соединения на уровне TCP.</blockquote>
  <p id="10G4">Первый же вопрос: что происходило в это время с подами?</p>
  <p id="R9y5">Иду в Prometheus, смотрю kube_pod_container_status_restarts_total - рестартов нет.</p>
  <p id="lLLu">Смотрю kube_pod_info - количество подов менялось.</p>
  <p id="8Rdo">А значит что? <u>Правильно - роллинг-деплой.</u></p>
  <p id="iYwp">Алгоритм роллинг-деплоя в Kubernetes примерно такой:</p>
  <ol id="xMpz">
    <li id="t1CH">Kubernetes посылает SIGTERM умирающему поду</li>
    <li id="U6Dq">Одновременно убирает его из Endpoints</li>
    <li id="kJ19">ALB обновляет свой список целей - но не мгновенно, это занимает несколько секунд</li>
  </ol>
  <p id="dd1K">В этот зазор ALB ещё продолжает гнать трафик на уже умирающий под.</p>
  <p id="EAAy">Под получает SIGTERM, начинает завершаться - и новый запрос от ALB уже некому обработать. Отсюда 502.</p>
  <p id="Y43r">Решение классическое - preStop хук: заставляем контейнер немного подождать перед завершением, чтобы ALB успел дерегистрировать цель.</p>
  <pre id="LFTo">lifecycle:
  preStop:
    exec:
      command: [&quot;sleep&quot;, &quot;15&quot;]</pre>
  <p id="j7P2"></p>
  <p id="aC1c"><u>Небольшое отступление для тех, кто на свежих версиях Kubernetes.</u></p>
  <p id="ZLW9">Начиная с версии 1.29 появилась фича PodLifecycleSleepAction - нативный sleep прямо в preStop без костыля через exec и системную команду. В 1.30 она включена по умолчанию, в 1.34 вышла в GA(beta?).</p>
  <p id="D3rR">Если у вас k8s &gt;= 1.30, можно писать так:</p>
  <pre id="tkle">lifecycle:
  preStop:
    sleep:
      seconds: 15</pre>
  <p id="NwZX"></p>
  <p id="Mku7">Чище, без зависимости от наличия sleep в образе, сразу понятно что происходит.</p>
  <p id="6lHn">У нас на тот момент была более старая версия кластера, поэтому пошли через exec.</p>
  <p id="0H1a">Добавили в деплоймент, задеплоили. Это был PR #1.</p>
  <p id="D120">Вместе с ним отключили автоскейлинг воркер-компонента - он создавал лишний шум деплоев без реальной надобности (PR #2).</p>
  <p id="WQ5a">После этого единичные 502 при деплоях пропали. Казалось, всё.</p>
  <p id="wL3Q"></p>
  <h3 id="fRa0">Причина #2 - OOMKill</h3>
  <p id="LTxr">Вскоре прилетело сразу 10 штук 502 за один раз.</p>
  <p id="QGo8">Снова OpenSearch - время совпадает, снова <code>target_status_code</code>: null.</p>
  <p id="mucV">Иду смотреть что было с подами в это время.</p>
  <p id="XLxb">В Prometheus смотрю </p>
  <pre id="kpq7">kube_pod_container_status_last_terminated_reason{reason=&quot;OOMKilled&quot;}.</pre>
  <p id="9BlA">Один из подов получил OOMKill - память стрельнула до ~800 MiB при лимите 800 Mi.</p>
  <p id="kb14">Под умер, ALB не успел дерегистрировать - 10 одновременных запросов получили 502.</p>
  <p id="yRxV">Смотрю лимиты в деплойменте - да, 800 Mi, и это явно было на грани.</p>
  <p id="CjK6">Откуда такой спайк - скорее всего, транзиентный пик нагрузки, в Node.js такое бывает при обработке нескольких тяжёлых запросов одновременно.</p>
  <p id="AuS7">Подняли лимит памяти до 1536 Mi (PR #3). После этого OOMKill больше не повторялся.</p>
  <p id="klbT"></p>
  <h3 id="WLru">Причина #3 - CPU throttling -&gt; liveness probe перезапускает контейнеры</h3>
  <p id="V7dw">Спустя несколько дней - новый инцидент, уже серьёзнее. Пять 502, target_processing_time от 13 до 24 секунд. Три пода.</p>
  <p id="AzEz">Это уже не про деплой и не про память.</p>
  <p id="Dr8h">Лезу в Prometheus, смотрю container_cpu_cfs_throttled_seconds_total.</p>
  <p id="LnPT">Картина неприятная:</p>
  <ul id="LZD4">
    <li id="iKFd">35.7% throttling</li>
    <li id="xHLv">34.4%</li>
    <li id="9Vie">35.5%</li>
  </ul>
  <p id="nidL">Треть всего CPU-времени поды просто не получают из-за лимита.</p>
  <p id="6iQ3">Node.js - однопоточный event loop. Когда CFS-шедулер Linux троттлит процесс, event loop стоит.</p>
  <p id="Jhdn">Стоит event loop - не отвечают HTTP-эндпоинты.</p>
  <p id="4N2H">Не отвечают эндпоинты - liveness probe падает.</p>
  <p id="OpNT">Смотрю конфигурацию проб через kubectl:</p>
  <pre id="EWVL">livenessProbe:
  periodSeconds: 10
  failureThreshold: 6
  timeoutSeconds: 10</pre>
  <p id="kLqT">То есть нужно 6 подряд неудачных проб с периодом 10 секунд - 60 секунд при жёстком троттлинге.</p>
  <p id="cBBI">CPU-лимит в 200m - это 0.2 ядра. При нагрузке это катастрофически мало для Node.js с event loop.</p>
  <p id="rf8I">Поднимаем CPU limit с 200m до 500m (PR #4).</p>
  <p id="6fdT">После деплоя картина в Prometheus:</p>
  <ul id="DWa7">
    <li id="Jtcl">4.3% throttling</li>
    <li id="dVlk">8.6%</li>
    <li id="fFA8">9.6%</li>
  </ul>
  <p id="0eB5">С 35% упало до 4-10%. Лайвнес-килы прекратились.</p>
  <p id="HWDO"></p>
  <h3 id="wvWH">Причина #4 - Graceful shutdown: Node.js и SIGTERM</h3>
  <p id="Gau0">Казалось бы, всё починили. Но нет.</p>
  <p id="sfb7">После деплоя PR #4 начали появляться два новых паттерна в OpenSearch:</p>
  <p id="KhRQ">502-ошибки:</p>
  <ul id="xeQt">
    <li id="tZtI">target_processing_time: 0.078s, target_status_code: null</li>
    <li id="sCUF">target_processing_time: 3.924s, target_status_code: null</li>
  </ul>
  <p id="7CBj">460-ошибки (client closed connection):</p>
  <ul id="X0zR">
    <li id="zXWw">день 1 - 32</li>
    <li id="IhO7">день 2 - 12</li>
    <li id="wZTE">день 3 - 2</li>
    <li id="RS0O">день 4 - 1</li>
    <li id="Jk1p">день 5 - 2</li>
  </ul>
  <p id="LRQC">Всё это - только от user-agent вендора, только во время деплоев.</p>
  <p id="HLLK">preStop: sleep 15 у нас есть, это проверено через kubectl describe rs.</p>
  <p id="wHiH">Тогда почему?</p>
  <p id="8v9I">Пауза на подумать.</p>
  <p id="c5g6">preStop hook даёт поду 15 секунд до того, как Kubernetes пошлёт SIGTERM.</p>
  <p id="hnr4">Но что происходит после SIGTERM?</p>
  <p id="ddg7">По умолчанию Node.js http.Server на SIGTERM не делает ничего специального.</p>
  <p id="4qcW">Процесс завершается, все open TCP-соединения - сброшены. Мгновенно.</p>
  <p id="Kfky">А бот-фреймворк вендора держит keep-alive соединения с сервером.</p>
  <p id="ZzGm">Одно соединение - много запросов. Когда соединение рвётся прямо во время обработки запроса - ALB получает 502. Когда клиент пробует переиспользовать соединение, которого уже нет - 460.</p>
  <p id="yrAP">Значит нам нужен явный обработчик SIGTERM, который:</p>
  <p id="jlfs">1. Перестаёт принимать новые соединения (server.close())</p>
  <p id="GycN">2. Ждёт пока завершатся активные запросы</p>
  <p id="ZbiR">3. Только тогда выходит</p>
  <pre id="Tv3l">server.keepAliveTimeout = 65000;
server.headersTimeout = 66000;

process.on(&#x27;SIGTERM&#x27;, () =&gt; {
  logger.info(&#x27;SIGTERM received, closing HTTP server&#x27;);
  server.close(() =&gt; {
    logger.info(&#x27;HTTP server closed&#x27;);
    process.exit(0);
  });

  // backstop: если активный запрос завис и server.close() никогда не вызовет колбэк -
  // выходим принудительно за 2 секунды до SIGKILL, чтобы завершение было контролируемым
  setTimeout(() =&gt; {
    logger.warn(&#x27;Forced exit after timeout&#x27;);
    process.exit(1);
  }, 25000).unref();
});
</pre>
  <p id="5RkL">Добавили в src/server.ts, открыли PR #5. Ревью прошло без замечаний.</p>
  <p id="vRz6">Но после деплоя ожидаемых строк в логах не появилось.</p>
  <p id="0m9A">Ни &quot;SIGTERM received&quot;, ни &quot;HTTP server closed&quot; - ничего.</p>
  <p id="lySq">Обработчик как будто не существовал.</p>
  <p id="7jA0"></p>
  <h3 id="EP9H">Причина #5 - Node.js не получает SIGTERM: yarn как PID 1</h3>
  <p id="ZnLL">Полезли смотреть Dockerfile.</p>
  <pre id="N8PA">CMD [&quot;yarn&quot;, &quot;start&quot;]

А start в package.json:

&quot;start&quot;: &quot;NODE_ENV=production node -r newrelic --enable-source-maps build/src/server.js&quot;</pre>
  <p id="nJ3K">Вот и проблема. Когда контейнер стартует через CMD [&quot;yarn&quot;, &quot;start&quot;], процесс yarn становится PID 1 в контейнере. Yarn Classic (1.x) не пробрасывает сигналы дочерним процессам. Kubernetes посылает SIGTERM yarn, yarn его просто игнорирует, node - дочерний процесс - не получает ничего.</p>
  <p id="Mawe">Вся цепочка:</p>
  <pre id="SubA">PID 1: yarn
  -&gt; sh -c &quot;NODE_ENV=production node ...&quot;
       -&gt; node build/src/server.js   &lt;- SIGTERM сюда не доходит</pre>
  <p id="gC8q">Решение простое - убрать yarn из цепочки и запустить node напрямую как PID 1.</p>
  <pre id="szlt">ENV NODE_ENV=production
CMD [&quot;node&quot;, &quot;-r&quot;, &quot;newrelic&quot;, &quot;--enable-source-maps&quot;, &quot;build/src/server.js&quot;]</pre>
  <p id="Dsue">NODE_ENV=production был захардкожен в yarn start - просто перенесли в ENV в Dockerfile.</p>
  <p id="UxfQ">Теперь:</p>
  <pre id="n5re">PID 1: node build/src/server.js   &lt;- получает SIGTERM напрямую от Kubernetes</pre>
  <p id="H8JY">Это PR #6.</p>
  <p id="rBG1"></p>
  <h3 id="T612">Причина #6 - server.close() не закрывает idle-соединения</h3>
  <p id="eMFv">После деплоя PR #6 shutdown-логи наконец появились. &quot;SIGTERM received&quot; - есть. &quot;HTTP server closed&quot; - есть. Но несколько 460 всё равно проскакивали.</p>
  <p id="ySKe">Смотрю тайминг: 460 появляются в самом конце жизни пода, уже после того как server.close() отработал.</p>
  <p id="mi1T">Тонкость в том, что server.close() перестаёт принимать новые соединения и ждёт завершения активных запросов - но idle keep-alive соединения при этом не закрывает. Бот-фреймворк вендора держит пул таких соединений открытыми между запросами. Если соединение было idle в момент shutdown - server.close() его не трогает. Оно висит до истечения terminationGracePeriodSeconds, потом прилетает SIGKILL от Kubernetes - и в этот момент соединение рвётся без предупреждения. Клиент пытается использовать мёртвое соединение - получает 460.</p>
  <p id="TGvb">Решение: явно закрыть все idle-соединения до вызова server.close().</p>
  <pre id="nDHI">server.closeIdleConnections();
server.close(() =&gt; {
  process.exit(0);
});
</pre>
  <p id="5VvC">closeIdleConnections() немедленно завершает соединения, по которым нет активных запросов. Активные - продолжают обрабатываться до конца. Именно такой порядок нужен.</p>
  <p id="Hghm">Важно: closeIdleConnections() появился в Node.js 18.2.0. На более старых версиях (16.x и ниже) метода нет - понадобится либо обновить Node.js, либо вручную отслеживать и закрывать idle-соединения.</p>
  <p id="hgvS">Это PR #7.</p>
  <p id="DoaX"></p>
  <h3 id="FUpa">Итоговая схема: все слои и настройки</h3>
  <pre id="yd7k">  Настройки соединений:

  +-----------------------------------------------+
  | Слой          | Параметр          | Значение   |
  +-----------------------------------------------+
  | AWS ALB       | idle timeout      | 60s        |
  | Node.js       | keepAliveTimeout  | 65s (&gt;60s) |
  | Node.js       | headersTimeout    | 66s (&gt;65s) |
  | Pod           | CPU limit         | 500m       |
  | Pod           | memory limit      | 1536Mi     |
  +-----------------------------------------------+

  keepAliveTimeout должен быть больше ALB idle timeout,
  иначе Node.js закроет соединение раньше ALB -&gt; 502.

  Завершение пода (rolling deploy / OOMKill / restart):

  t= 0s  K8s удаляет под из Endpoints
         K8s запускает preStop hook
         |
         +-- preStop: sleep 15s
         |   (ждём пока ALB дерегистрирует под,
         |    иначе запросы идут на умирающий под -&gt; 502)
         |
  t=15s  preStop завершён
         K8s отправляет SIGTERM -&gt; node (PID 1)
         |   (именно node, не yarn -- Yarn Classic сигналы не пробрасывает)
         |
         +-- server.closeIdleConnections()
         |   (закрываем idle keep-alive прямо сейчас,
         |    иначе доживут до SIGKILL и дадут 460)
         |
         +-- server.close()
         |   (перестаём принимать новые соединения,
         |    ждём завершения активных запросов)
         |
         +-- setTimeout(force exit, 25s)
             (страховка: если запрос завис и server.close()
              не вызвал колбэк -- выходим до SIGKILL)

  t=?s   активные запросы завершились -&gt; process.exit(0)

  t=30s  K8s: SIGKILL  (terminationGracePeriodSeconds)

  Итого окно для активных запросов: 30 - 15 = 15s</pre>
  <p id="4o0D"></p>
  <h3 id="sN99">Что добавляли по пути</h3>
  <p id="mHlK">Помимо параметров и кода, по ходу расследования добавляли и инструменты обсервабилити:</p>
  <ul id="lxZ9">
    <li id="7Ozf">Логирование - добавили явное логирование события получения SIGTERM и закрытия HTTP-сервера. Раньше это было чёрным ящиком - не было понятно, что происходило с сервером в момент завершения.</li>
    <li id="eLFc">Анализ ALB-логов - настроили регулярный разбор target_status_code, target_processing_time и elb_status_code в OpenSearch. Раньше смотрели только на наличие 5xx - теперь ещё и на то, ответил ли под вообще.</li>
    <li id="rpza">Prometheus-запросы - добавили в регулярный осмотр метрики CFS-троттлинга (container_cpu_cfs_throttled_seconds_total) и количество терминированных контейнеров с причиной (kube_pod_container_status_last_terminated_reason). Без этого CPU-проблема выглядела бы просто как &quot;иногда 502&quot;.</li>
  </ul>
  <p id="Vemj"></p>
  <h3 id="aOQA">Итог</h3>
  <p id="Qbun">keepAlive fix - keepAliveTimeout 65s, headersTimeout 66s - убирает 502 от гонки таймаутов Node.js vs ALB</p>
  <ul id="AQIL">
    <li id="aRbR">PR #1 - preStop: sleep 15 - фиксит 502 при роллинг-деплоях</li>
    <li id="RtZm">PR #2 - отключение автоскейлинга воркера - убирает лишние деплои</li>
    <li id="9sp0">PR #3 - memory limit 800Mi -&gt; 1536Mi - фиксит OOMKill при пиках нагрузки</li>
    <li id="GERE">PR #4 - CPU limit 200m -&gt; 500m - фиксит CPU throttle -&gt; liveness probe перезапускает контейнеры</li>
    <li id="WLxq">PR #5 - graceful shutdown на SIGTERM - фиксит 502/460 при завершении подов</li>
    <li id="9I6k">PR #6 - node как PID 1 в Dockerfile - фиксит то что SIGTERM вообще не доходил до Node.js</li>
    <li id="ZiTJ">PR #7 - server.closeIdleConnections() перед server.close() - фиксит 460 от idle keep-alive соединений</li>
  </ul>
  <p id="XiQM">Суммарно на весь дебаг ушло около четырёх недель. Не потому что фиксы сложные - большинство из них несложные. А потому что без логов и метрик каждый следующий шаг был невозможен: сначала нужно было включить ALB логи, потом накопить статистику, потом разобраться с первой причиной - и только тогда стала видна вторая. Каждый закрытый слой убирал шум и открывал следующий. На самом  деле было 9 исправлений, но я смог вспомнить только 7 из них.</p>
  <p id="PbYE">После всех фиксов ошибки 502/504/460 ушли полностью, клиенты рады, сервис работает на отлично.</p>
  <p id="2x5p"></p>
  <h3 id="Md8F">Несколько выводов, которые оставлю здесь для себя и тех, кто столкнётся с похожим:</h3>
  <ul id="7Qvb">
    <li id="jq8d">Если первый фикс помог - не спешите радоваться. Несколько независимых причин могут маскировать друг друга. Смотрите на абсолютное количество ошибок, не только на тренд</li>
    <li id="3dpT">Включите ALB access logs сразу. target_status_code: null и target_processing_time вместе сразу говорят где именно рвётся цепочка - не тратьте время на угадывание</li>
    <li id="Qsaj">preStop: sleep N - обязательный минимум для любого сервиса за ALB при роллинг-деплоях (на самом деле не всегда, но пусть будет обязательным для статьи)</li>
    <li id="GHVS">CPU-лимит в Kubernetes - это не просто &quot;ограничение&quot;. Для однопоточных рантаймов (Node.js, Python GIL) троттлинг убивает не производительность, а живость сервиса целиком</li>
    <li id="dkil">Дефолтный http.Server в Node.js не делает graceful shutdown - надо писать руками</li>
    <li id="ow3p">Написать SIGTERM-обработчик недостаточно - надо убедиться что процесс вообще получает сигнал. CMD [&quot;yarn&quot;, &quot;start&quot;] в Dockerfile делает yarn PID 1, и Yarn Classic сигналы не пробрасывает. Запускайте node напрямую</li>
    <li id="prA2">460 от ALB - это не &quot;клиент сам ушёл&quot;. Это часто симптом того, что сервер рвёт keep-alive соединения без предупреждения</li>
    <li id="gbBp">server.close() в Node.js не закрывает idle keep-alive соединения - без closeIdleConnections() они будут висеть до SIGKILL и давать 460 в самом конце</li>
  </ul>
  <p id="bZgc"></p>
  <h3 id="zj5D">Ссылки</h3>
  <p id="fKlA">Tuning HTTP Keep-Alive in Node.js (про keepAliveTimeout, headersTimeout и откуда берутся 502 при Node.js за ALB)</p>
  <ul id="PukK">
    <li id="VgB9"><a href="https://connectreport.com/blog/tuning-http-keep-alive-in-node-js" target="_blank">https://connectreport.com/blog/tuning-http-keep-alive-in-node-js</a> </li>
  </ul>
  <p id="vAOT">AWS ALB access logs - список всех полей и как включить</p>
  <ul id="nuzC">
    <li id="mnnh"><a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-access-logs.html" target="_blank">https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-access-logs.html</a> </li>
  </ul>
  <p id="MQfm">AWS ALB - настройка idle timeout и других атрибутов балансера</p>
  <ul id="NnWa">
    <li id="m9dj"><a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-attributes.html" target="_blank">https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-attributes.html</a> </li>
  </ul>
  <p id="pPTs">Express.js - graceful shutdown и работа с http.Server</p>
  <ul id="LOwJ">
    <li id="JOk4"><a href="https://expressjs.com/en/advanced/healthcheck-graceful-shutdown.html" target="_blank">https://expressjs.com/en/advanced/healthcheck-graceful-shutdown.html</a> </li>
  </ul>
  <p id="bPoF">Kubernetes - preStop hooks и жизненный цикл пода</p>
  <ul id="BH1d">
    <li id="P7q9"><a href="https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-hooks" target="_blank">https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-hooks</a> </li>
  </ul>
  <p id="CWN5">Kubernetes KEP-3960 - нативный sleep в preStop (PodLifecycleSleepAction, GA в 1.34)</p>
  <ul id="jnhS">
    <li id="TLkg"><a href="https://kep.k8s.io/3960" target="_blank">https://kep.k8s.io/3960</a></li>
  </ul>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@kruchkov_alexandr/mwVCBuS1y6T</guid><link>https://teletype.in/@kruchkov_alexandr/mwVCBuS1y6T?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=kruchkov_alexandr</link><comments>https://teletype.in/@kruchkov_alexandr/mwVCBuS1y6T?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=kruchkov_alexandr#comments</comments><dc:creator>kruchkov_alexandr</dc:creator><title>Просто алерт. Просто Арго.</title><pubDate>Tue, 17 Mar 2026 09:27:02 GMT</pubDate><description><![CDATA[Прилетает алёрт: HPA maxed out.]]></description><content:encoded><![CDATA[
  <p id="fgfn">Прилетает алёрт: <u>HPA maxed out.</u></p>
  <pre id="qSY5">HPA: keda-hpa-vmagent-scaler
Cluster: stg-**-uswest1
Current value: 10 (max)</pre>
  <p id="hsHT">Сперва я вообще задумался - а нахрена этот алерт? Что он мне дает? Ну уперлось в максимум, и что? Все остальное работает ок, никаких других алертов.</p>
  <p id="tR0R">Спросил у умных людей, умные люди дали умные советы, что может быть неверные триггеры трешхолда, может быть сервис в максимуме и скоро будут ошибки. Ладно, аргумент. </p>
  <p id="5S3a">Ну ок, пошёл смотреть.</p>
  <p id="aNr1"><br />Проблема первая: </p>
  <ul id="SRvp">
    <li id="h9rO">vmagent жрёт памяти больше, чем ему отведено</li>
  </ul>
  <p id="DlpZ">Первым делом смотрю что там с HPA:</p>
  <section style="background-color:hsl(hsl(0, 0%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="nABD">NAME                      REFERENCE        TARGETS                           MIN   MAX   REPLICAS<br />keda-hpa-vmagent-scaler   VMAgent/...      5694m/40 (avg), memory: 50%/40%   2     10    10</p>
  </section>
  <p id="xnPr">Два триггера. </p>
  <ul id="h8Wd">
    <li id="eHBO">Один prometheus-based - 5.7/40, всё хорошо. </li>
    <li id="lhcD">Второй - memory: 50%/40%.<br />Вот он виновник.</li>
  </ul>
  <p id="voM3">Смотрю дальше:</p>
  <ul id="OUPk">
    <li id="mZLY">Лимит на каждый pod: 256Mi</li>
    <li id="D7Pt">Фактическое потребление: ~130 MiB на pod</li>
  </ul>
  <p id="ubbX">130/256 = 50%. Цель триггера - 40%, то есть 102 MiB.</p>
  <p id="DQW1">Это физически недостижимо - <code>vmagent</code> столько и держит в памяти просто чтобы работать, независимо от нагрузки. Горизонтальный скейлинг тут не поможет: добавишь реплик, каждая всё равно будет жрать те же 130 MiB.</p>
  <p id="bHQH">Решение простое - поднять лимит. 384Mi &gt; утилизация падает до 34%, HPA успокоится.</p>
  <p id="r2Qi">Правлю values в репозитории с ArgoCD-приложениями для кластера stg-*-uswest1:</p>
  <pre id="zLvy">resources:
  limits:
    memory: 384Mi
  requests:
    memory: 384Mi</pre>
  <p id="dfub">Коммичу, засинкал ArgoCD.</p>
  <p id="4wiN">И вот тут началось.</p>
  <p id="ellz"><br />Проблема вторая: </p>
  <ul id="pv9t">
    <li id="tK1w">ArgoCD и KEDA устроили драку</li>
  </ul>
  <p id="IMUo">После синка:</p>
  <pre id="s79w">Operation cannot be fulfilled on scaledobjects.keda.sh &quot;vmagent-scaler&quot;:
the object has been modified; please apply your changes to the latest version
and try again. Retrying attempt #1</pre>
  <p id="FYba">Классический 409. ArgoCD читает объект, хочет запатчить - а за это время KEDA уже успел его обновить.<br />Проверяю как часто это происходит:</p>
  <pre id="6NgM">kubectl -n vm get scaledobject vmagent-scaler -o jsonpath=&#x27;{.metadata.resourceVersion}&#x27;
sleep 5
kubectl -n vm get scaledobject vmagent-scaler -o jsonpath=&#x27;{.metadata.resourceVersion}&#x27;

# 157227839 → 157227898 за 5 секунд</pre>
  <p id="UDql">KEDA пишет в <code>ScaledObject</code> каждые 1-2 секунды. Статусы, условия, метрики - всё туда.<br />Ретрай через 10, 20, 40 секунд не помогал - KEDA всегда успевал раньше.</p>
  <p id="y1vk"><br />Попытка 1: </p>
  <ul id="S5eV">
    <li id="ZYFm"><code>ignoreDifferences</code></li>
  </ul>
  <p id="Cwkj">В конфиге ApplicationSet уже был блок:</p>
  <pre id="BgSk">ignoreDifferences:
  - group: keda.sh
    kind: ScaledObject
    managedFieldsManagers:
      - keda-operator
      - keda-metrics-adapter</pre>
  <p id="FBER">Но не работает. Пошёл смотреть кто реально владеет полями в объекте:</p>
  <pre id="cI4o">kubectl -n vm get scaledobject vmagent-scaler --show-managed-fields -o json

manager: keda               | op: Update
manager: argocd-controller  | op: Apply</pre>
  <p id="6GTS">А я что написал? keda-operator. А реальное имя - просто keda. </p>
  <p id="cwbo">Добавил keda в список.</p>
  <p id="pYLR">Не помогло.</p>
  <p id="NAUm">Дело в том, что <code>ignoreDifferences</code> влияет только на то, что ArgoCD показывает в дифе.<br />На сам apply - никак не влияет. ArgoCD всё равно патчит объект при синке.<br />Это я понял позже. </p>
  <p id="73Xh">Изрядно помучавшись с другими подобными попытками я снова пришел к умным людям, которые снова дали умные советы, в том числе сервер сайд апплай.</p>
  <p id="KdZ7">Почитал документацию, вроде красиво, ок.</p>
  <p id="c7Iz"><br />Попытка 2: </p>
  <ul id="SO1a">
    <li id="azG3">включить <code>ServerSideApply</code></li>
  </ul>
  <p id="RSOL">Логика была такая: с SSA ArgoCD перестаёт посылать resourceVersion в патче, значит 409 уйдёт.</p>
  <p id="kccP">Добавил <code>ServerSideApply=true</code> в syncOptions для vm-приложений.</p>
  <p id="pQxh">Тут выяснился интересный нюанс. В ApplicationSet у нас базовый шаблон уже имел ServerSideApply=true:</p>
  <pre id="0g98">syncPolicy:
  syncOptions:
    - ServerSideApply=true</pre>
  <p id="suii">Но в templatePatch для vm-секции был такой кусок:</p>
  <pre id="QLUq">syncPolicy:
  syncOptions:
    - RespectIgnoreDifferences=true</pre>
  <p id="vzRr">И он молча перезаписывал базовый список вместо того чтобы добавить к нему.<br />Итого vm-приложения жили без SSA всё это время, хотя казалось что SSA включён. 🤡</p>
  <p id="aeaw">Добавил ServerSideApply=true явно в vm-секцию. Применил. Синканул.</p>
  <p id="sMed">Ошибка изменилась:</p>
  <pre id="TtYh">Please review the fields above--they currently have other managers.
Please re-run the apply command with the --force-conflicts flag.</pre>
  <p id="eBR5">Хм. Это уже не 409 от гонки - это SSA ownership conflict.<br />Стало хуже. 🙃</p>
  <ul id="FY24">
    <li id="eSsg">До SSA: транзиентная 409, иногда сама проходила с третьей попытки.</li>
    <li id="FBJ4">После SSA: постоянный конфликт владения полями, не проходит никогда.</li>
  </ul>
  <p id="6JIr"><br />Попытка 3: </p>
  <ul id="iKjM">
    <li id="jGgy">найти конкретные поля-конфликтеры</li>
  </ul>
  <p id="Ylgz">Смотрю что именно KEDA записывает в managedFields:</p>
  <pre id="HG1Y">kubectl get &lt;resource&gt; --show-managed-fields -o json \
  | jq -r &#x27;.metadata.managedFields[].manager&#x27;</pre>
  <p id="xvUE">KEDA через CSA (client-side apply) владеет spec.advanced.scalingModifiers.<br />ArgoCD через SSA тоже хочет применить spec.advanced (оно есть в Helm-темплейте).</p>
  <p id="XSCa"><br />Конфликт.</p>
  <p id="Qz86">Добавил jsonPointers для этого поля:</p>
  <pre id="wkXV">jsonPointers:
  - /metadata/resourceVersion
  - /metadata/finalizers
  - /spec/advanced/scalingModifiers
  - /status</pre>
  <p id="nRrx">Не помогло.</p>
  <p id="l4SS">Потому что ignoreDifferences + RespectIgnoreDifferences=true - это &quot;не синкай ресурс если разница ТОЛЬКО в этих полях&quot;. Но если ресурс синкается по другой причине (а он синкался, потому что менялись лимиты памяти в VMAgent) - ArgoCD применяет объект целиком. </p>
  <p id="FIZZ">Включая все поля. </p>
  <p id="fGQH">Включая конфликтные. 🤡</p>
  <p id="Gs6D"><br />Попытка 4: </p>
  <ul id="121n">
    <li id="D1i7"><code>Force=true</code> аннотация</li>
  </ul>
  <p id="5M1I">Думаю, ну раз нужен <code>--force-conflicts</code>, может есть способ сказать ArgoCD &quot;применяй этот ресурс с --force-conflicts&quot;?</p>
  <p id="hrd4">Добавил в Helm-темплейт ScaledObject:</p>
  <pre id="aok3">annotations:
  argocd.argoproj.io/sync-options: Force=true</pre>
  <p id="ERjU">Тут я вовремя остановился и проверил что это вообще делает.<br />Force=true в ArgoCD - это delete + recreate ресурса при каждом синке.<br />Не --force-conflicts для SSA. Это вообще другое и довольно опасное.</p>
  <p id="1Psj">Убрал, не применял.</p>
  <h2 id="UgF3"><br />Что реально сработало</h2>
  <p id="j3nk">Пока я всё это ковырял, читал документацию, смотрел менеджед поля и логи, понял в чём корень.</p>
  <p id="CTs0">Проблема - смешанный <code>ownership</code>: KEDA пишет в ScaledObject через <code><u>CSA</u></code> (старый client-side apply), ArgoCD пытается применить через <code><u>SSA</u></code>. Когда два разных механизма клеймят одно поле - кубернетис говорит &quot;разберитесь между собой&quot;.</p>
  <p id="18Tw">И ignoreDifferences тут не поможет никак - он только про вычисление диффа, не про применение.</p>
  <p id="iO4U">Самое простое решение - удалить ScaledObject и дать ArgoCD пересоздать его с нуля через SSA.</p>
  <p id="8uQJ">После пересоздания:</p>
  <ul id="CdLr">
    <li id="csSp">ArgoCD - единственный SSA-owner всех полей в объекте</li>
    <li id="TXAr">KEDA потом пишет scalingModifiers через CSA - это его поле, арго его не трогает</li>
    <li id="cxoa">Поля не пересекаются &gt; конфликта нет</li>
  </ul>
  <p id="JJnR">Похер, это стейдж.</p>
  <pre id="YbW7">kubectl -n vm delete scaledobject vmagent-scaler</pre>
  <p id="RDWS">ArgoCD пересоздал. Синк прошёл с первого раза. Всё зелёное. 🎉</p>
  <p id="PS6y">Следом HPA отскейлился обратно - vmagent с новым лимитом 384Mi держит ~34% утилизации, ниже порога 40%. </p>
  <p id="jI5N">Через 10 минут (stabilizationWindowSeconds: 600 на scaleDown) реплики упали с 10 до 2.</p>
  <h2 id="8HWt"><br />Что поправили в итоге</h2>
  <p id="TtGN">В Terraform-модулях - ApplicationSet для всех mt-кластеров:</p>
  <ul id="nANk">
    <li id="6n5b">Добавили <code>ServerSideApply=true</code> явно в vm-секцию <code>templatePatch</code> (чтобы он не терялся при override)</li>
    <li id="jvAq">Добавили keda в managedFieldsManagers (было только keda-operator и keda-metrics-adapter, реального имени менеджера не было)</li>
    <li id="2DS2">Добавили <code>/spec/advanced/scalingModifiers</code> и <code>/metadata/finalizers</code> в jsonPointers - теперь ArgoCD корректно игнорирует эти поля при диффе и не показывает ложные OutOfSync</li>
  </ul>
  <p id="nrSC">В репозитории с ArgoCD-приложениями - values для stg-mt-uswest1:</p>
  <ul id="O4v7">
    <li id="6lCq">Лимит vmagent: 256Mi → 384Mi</li>
  </ul>
  <h2 id="ZuBr"><br />Итоги</h2>
  <ul id="ksQN">
    <li id="GObQ">ignoreDifferences - только про отображение диффа. Не про apply. Всегда.</li>
    <li id="atUO">CSA + SSA на одном объекте = смешанный ownership = проблемы. Лечится пересозданием.</li>
    <li id="0XyC">Имена менеджеров надо проверять в реальном объекте, не угадывать:</li>
  </ul>
  <pre id="yUzj">kubectl get &lt;resource&gt; --show-managed-fields -o json \
  | jq -r &#x27;.metadata.managedFields[].manager&#x27;</pre>
  <ul id="e9IX">
    <li id="i7xM"><code>Force=true</code> в <code>ArgoCD != --force-conflicts</code> в kubectl <u>SSA</u>. </li>
    <ul id="FmNr">
      <li id="ybd3">Первое - delete+recreate</li>
      <li id="L1dE">второе - принудительное взятие ownership поля. </li>
    </ul>
  </ul>
  <p id="7zN6">Разные вещи.</p>
  <ul id="Ck9T">
    <li id="AIGX">templatePatch в ApplicationSet переписывает поля целиком, а не мержит. Если в базовом шаблоне есть syncOptions: [ServerSideApply=true], а в templatePatch ты пишешь syncOptions: [RespectIgnoreDifferences=true] - SSA пропадает молча.</li>
    <li id="rDgo">Иногда самый быстрый путь - удалить объект и дать системе пересоздать его в правильном состоянии.</li>
  </ul>
  <p id="hiLe"></p>
  <p id="agFs">Несколько часов потратил на то, что решилось одной командой <code>kubectl delete</code>.</p>
  <p id="EkGu">Просто алерт. Просто Арго. Просто пять минут.</p>
  <p id="48sJ">Классика.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@kruchkov_alexandr/--Mo_yGq58L</guid><link>https://teletype.in/@kruchkov_alexandr/--Mo_yGq58L?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=kruchkov_alexandr</link><comments>https://teletype.in/@kruchkov_alexandr/--Mo_yGq58L?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=kruchkov_alexandr#comments</comments><dc:creator>kruchkov_alexandr</dc:creator><title>Нули</title><pubDate>Thu, 05 Mar 2026 17:34:50 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/a7/0d/a70d5f75-c614-4fc5-8081-7a4a693d2926.png"></media:content><description><![CDATA[<img src="https://img2.teletype.in/files/91/8b/918b5a3f-3c33-41ff-a8a8-67d15e7429a2.png"></img>#бытовое #troubleshooting #одинденьизжизни]]></description><content:encoded><![CDATA[
  <figure id="FlZi" class="m_original">
    <img src="https://img2.teletype.in/files/91/8b/918b5a3f-3c33-41ff-a8a8-67d15e7429a2.png" width="768" />
  </figure>
  <p id="zQMn">На планшете для работы умер кулер. Приплыли.</p>
  <p id="kq50"></p>
  <p id="jLmf">Ну, не совсем всё умерло - он включается, нагревается и троттлит 100% времени.<br />Windows-планшет, на котором жила вся моя рабочая среда: куб контексты, профили AWS и Azure, скрипты, IDEs. Да всё. Всё было там.</p>
  <p id="Ocmn">Надо быстро восстановить рабочее место.</p>
  <p id="mKKz">Причём в <u>изолированное</u> окружение, чтобы ничего, связанного с работой не перемешалось с личным и не было лишних утечек.<br />Поднял виртуальную машину с Windows на Mac (да, я привык к Windows-среде, не осуждайте 🙃), начал по памяти восстанавливать инструменты. </p>
  <p id="eu1J"><br />Благо есть <code>asdf</code> с <code>.tool-versions</code> - всё задокументировано, стоишь раз за разом одни и те же версии. Скопировал файл, запустил инсталляцию.</p>
  <p id="rhTm">Поставилось автоматически:</p>
  <ul id="TrEq">
    <li id="aQkp">kubectl, helm, helmfile, terragrunt, terraform</li>
    <li id="0sci">awscli, azure-cli, kubelogin</li>
    <li id="O9a9">jq, yq, k9s, kubectx</li>
  </ul>
  <p id="TwCp">Дохера всего, около 45 утилит.</p>
  <p id="S6Qv">Дальше надо запустить скрипты - клонировать весь GitLab компании разом, настроить контексты кластеров всех облаков и профили облаков. Всё это у меня было. Всё это я написал раньше. Надо просто запустить.</p>
  <p id="wfgF">Запускаю скрипт клонирования GitLab.<br />Завис. Минута. Две. Пять.<br />Окей, Ctrl+C, думаю. Что-то с сетью? Или токен протух?</p>
  <p id="7SQJ"><br />Иду в GitLab UI - токен живой. Проверяю вручную:</p>
  <pre id="FiHp">curl -s -H &quot;PRIVATE-TOKEN: $TOKEN&quot; &quot;https://gl.company.com/api/v4/user&quot;</pre>
  <p id="ftUw">Всё нормально, вернул мой профиль, HTTP 200. Хорошо.<br />Значит не токен.</p>
  <p id="tMt6">Меняю токен на новый - на всякий случай. Запускаю скрипт. Снова завис.</p>
  <p id="KAEq">Прошу нейронку помочь. <br />Та начинает советовать добавить таймаут в curl, добавить проверки пагинации, переписать функции... <br />Делаю всё это. Всё равно зависает или выдаёт нули:<br />Хотя вручную curl отдаёт нормальные данные.<br />Ни один совет нейронки не помог ни в чем.</p>
  <p id="YDkb">Добавляю отладочный вывод. <br />Оборачиваю запрос в файл, читаю из файла, убираю BOM, убираю нулевые байты, убираю CR... <br />Нули.</p>
  <p id="nuiM">Пишу отдельный дебаг-скрипт с пошаговыми запросами. <br />Каждый шаг - отдельный curl, каждый ответ - в переменную, потом проверяем <br /><code>jq -e &#x27;type == &quot;array&quot;&#x27;...</code></p>
  <p id="jdTh">И тут вижу странное:<br />API error на /groups page=1:</p>
  <pre id="mzJX">[{&quot;id&quot;:10,&quot;web_url&quot;:&quot;https://gl.company.com/groups/all&quot;,&quot;name&quot;:&quot;All&quot;...</pre>
  <p id="W5fE"><strong>Что.</strong></p>
  <p id="aWgf">Стоп. Там же чётко написано <code>[{&quot;id&quot;:10...</code> - это массив. <br />Это валидный, сука, JSON. Ты чо, пёс.<br />Но проверка говорит &quot;не массив&quot;.<br />Проверяю сам:</p>
  <pre id="fG33">echo &#x27;[]&#x27; | jq -e &#x27;type == &quot;array&quot;&#x27;; echo &quot;exit: $?&quot;
exit: 1</pre>
  <p id="wAHb"><strong>Чтоооо.</strong></p>
  <pre id="Wy6F">echo &#x27;[1,2,3]&#x27; | jq -e &#x27;type == &quot;array&quot;&#x27;; echo &quot;exit: $?&quot;
exit: 1</pre>
  <p id="62HS"><strong>ЧТО.</strong></p>
  <p id="1Wen">jq возвращает 1 для валидного массива. На вопрос &quot;это массив?&quot; jq отвечает &quot;нет&quot;.</p>
  <p id="sNUj">Ладно, решаю - пофиг на GitLab, пофиг на jq, склоню вручную потом. <br />Сначала сделаю рабочую среду.</p>
  <p id="p3mH">Иду настраивать kubectl-контексты. Запускаю скрипт - не работает.<br />Иду в AWS, пробую aws eks update-kubeconfig - зависает.<br />Пробую вручную aws sts get-caller-identity - зависает на несколько секунд, потом отрабатывает.<br />Что-то медленное и странное.</p>
  <p id="pGSA">Пробую aws s3 ls - работает, но медленно.<br />Иду в Azure - az account list отрабатывает, но вывод странный. Где-то что-то парсится не так.</p>
  <p id="9OFe">Начинаю замечать паттерн: </p>
  <section style="background-color:hsl(hsl(55,  86%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="9BCI">всё, что связано с обработкой JSON в CLI-инструментах, либо зависает, либо даёт неверный результат. awscli внутри гоняет Python и boto3, там свой парсер. <br />jq - отдельный бинарник.</p>
  </section>
  <p id="YZu8"></p>
  <p id="gTVA">Стоп. Какого лешего тут происходит.</p>
  <p id="Vp3E">Как мне может ВСЁ поломать всего-лишь одна утилита?????</p>
  <p id="tRo1">Пойдём смотреть тебя.</p>
  <pre id="XAIG">which jq
/home/alexk/.asdf/shims/jq
file /home/alexk/.asdf/shims/jq
/home/alexk/.asdf/shims/jq: Bourne-Again shell script, 
ASCII text executable</pre>
  <p id="RaDS"><br />Шим asdf. Смотрю куда он ведёт:</p>
  <pre id="iRUE">ls /home/alexk/.asdf/installs/jq/
1.8.1
file /home/alexk/.asdf/installs/jq/1.8.1/bin/jq
ELF 64-bit LSB executable, x86-64</pre>
  <p id="Vd01">От сука.</p>
  <p id="Jtsc"><br />А теперь проверяем ещё раз для наглядности:</p>
  <pre id="zZEU">uname -m
aarch64</pre>
  <p id="661N"><br />Вот оно.<br />Система - ARM64. Бинарник jq - x86-64.</p>
  <section style="background-color:hsl(hsl(55,  86%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="nZ6P">WSL2 на Windows ARM (в моём случае - Windows виртуалка на Mac с чипом Apple Silicon, WSL2 внутри неё) умеет запускать x86-64 бинарники через Microsoft Prism - встроенный эмулятор. <br />Запускает. Но нестабильно. Базовые операции типа <code>jq &#x27;.field&#x27; или jq &#x27;.[]&#x27;</code> работают. <br />А вот более сложные выражения, типа <code>jq -e &#x27;type == &quot;array&quot;&#x27;</code> или даже <code>jq --version</code> - падают или зависают.</p>
  </section>
  <p id="MZHq"></p>
  <p id="0Dzx">Именно поэтому:</p>
  <ul id="Mgsk">
    <li id="SjOK">скрипт клонирования GitLab зависал на первой же функции, которая проверяла тип ответа через jq</li>
    <li id="PPk5">echo <code>&#x27;[]&#x27; | jq -e &#x27;type == &quot;array&quot;&#x27;</code> возвращал 1 вместо 0</li>
    <li id="jVuK">счётчики были нулями - jq тихо ломался при подсчёте длины массива</li>
    <li id="UOiF">всё, что просто парсило JSON вручную через awscli/azure-cli, работало через собственные парсеры</li>
  </ul>
  <p id="efoI"><br />Короче все скрипты подготовки среды на новом рабочем железе.</p>
  <p id="gesr">Понятно что сломалось. <br />Непонятно почему asdf поставил неправильный бинарник.</p>
  <p id="BJjO">Лезу в плагин:</p>
  <pre id="tIF1">cat ~/.asdf/plugins/jq/bin/download</pre>
  <p id="BUP9"><br />Нахожу функцию определения архитектуры:</p>
  <pre id="USrs">get_arch(){
  declare arch=&quot;$(uname -m)&quot;
  if [ &quot;$arch&quot; == &#x27;x86_64&#x27; ]; then
    echo &#x27;64&#x27;
  elif [ &quot;$arch&quot; == &#x27;aarch64&#x27; ]; then
    echo &#x27;64&#x27;     # &lt;--- вот оно
  elif [ &quot;$arch&quot; == &#x27;arm64&#x27; ]; then
    echo &#x27;64&#x27;     # &lt;--- и вот
  elif [ &quot;$arch&quot; == &#x27;i386&#x27; ]; then
    echo &#x27;32&#x27;
  ...
}</pre>
  <p id="BwxY"><br />И чуть ниже - как формируется имя файла для скачивания:</p>
  <pre id="Ranb">guessed_file=&quot;jq-linux$arch&quot;</pre>
  <p id="sNdj"><br />То есть: aarch64 -&gt; get_arch() возвращает &#x27;64&#x27; -&gt; скачивается jq-linux64 - это x86-64 бинарник.</p>
  <p id="ji1X">Самое смешное - в том же файле есть функция guess_download_url(), которая это правильно обрабатывает:</p>
  <pre id="OGff">guess_download_url() {
  ...
  if [ &quot;$arch&quot; == &#x27;aarch64&#x27; ]; then
    arch=&quot;arm64&quot;
  fi
  ...
}</pre>
  <p id="21E2"></p>
  <p id="Mr9c">Правильная логика есть. Но эта функция - мёртвый код. <br />Нигде не вызывается. Кто-то написал, не подключил и забыл. <br />А в <code>download()</code> используется старая <code>get_arch()</code>, которая для любой 64-битной архитектуры, включая ARM, выдаёт одинаковое &#x27;64&#x27;.</p>
  <p id="ZpJI">А на GitHub Releases у jq 1.7.1+ есть отдельный jq-linux-arm64. <br />Он существует. <br />Просто плагин про него не знает.</p>
  <p id="fPfd"><br />Правлю плагин (PR с фиксом позже сделаю в плагин):</p>
  <p id="j3Bb">было:</p>
  <pre id="z7ge">elif [ &quot;$arch&quot; == &#x27;aarch64&#x27; ]; then
    echo &#x27;64&#x27;
elif [ &quot;$arch&quot; == &#x27;arm64&#x27; ]; then
    echo &#x27;64&#x27;</pre>
  <p id="Hp39">стало:</p>
  <pre id="FV23">elif [ &quot;$arch&quot; == &#x27;aarch64&#x27; ]; then
    echo &#x27;arm64&#x27;
elif [ &quot;$arch&quot; == &#x27;arm64&#x27; ]; then
    echo &#x27;arm64&#x27;</pre>
  <p id="azFW"></p>
  <p id="KFFT">И имя файла:</p>
  <p id="pV5N">было:</p>
  <pre id="oJfm">guessed_file=&quot;jq-linux$arch&quot;</pre>
  <p id="2ecH">стало:</p>
  <pre id="P29B">case &quot;$arch&quot; in
  64)    guessed_file=&quot;jq-linux64&quot; ;;
  32)    guessed_file=&quot;jq-linux32&quot; ;;
  arm64) guessed_file=&quot;jq-linux-arm64&quot; ;;
  *)     guessed_file=&quot;jq-linux-$arch&quot; ;;
esac</pre>
  <p id="4jAd"><br />Переустанавливаю:</p>
  <pre id="GwPo">asdf uninstall jq 1.8.1
asdf install jq 1.7.1

file ~/.asdf/installs/jq/1.7.1/bin/jq
ELF 64-bit LSB executable, ARM aarch64 ✅

echo &#x27;[]&#x27; | jq -e &#x27;type == &quot;array&quot;&#x27;; echo &quot;exit: $?&quot;
exit: 0 ✅</pre>
  <p id="lG0s"></p>
  <p id="wn1g">Запускаю скрипт клонирования и контексты всех куберентисов.<br />Всё работает. Красота.</p>
  <p id="gWCV"><br /><u>Итоги</u><br />- планшет умер, виртуалка поднялась, среда восстановлена. Полдня потрачено.<br />- из них часа два - на отладку того, что <u>казалось</u> сломанным скриптом или токеном.<br />- причина: jq x86-64 на aarch64-системе работает через эмуляцию, частично. Простые операции - норм. Чуть сложнее - всё, привет нулям и зависаниям.<br />- баг в asdf-плагине для jq: get_arch() возвращает 64 для любой 64-битной архитектуры, ARM в том числе. В итоге всегда скачивается jq-linux64 (x86-64).<br />- правильная логика в том же файле есть - в мёртвой функции guess_download_url(), которая никогда не вызывается.</p>
  <p id="yKed"></p>
  <p id="aseb">Нули.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@kruchkov_alexandr/wWa-INvQvJH</guid><link>https://teletype.in/@kruchkov_alexandr/wWa-INvQvJH?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=kruchkov_alexandr</link><comments>https://teletype.in/@kruchkov_alexandr/wWa-INvQvJH?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=kruchkov_alexandr#comments</comments><dc:creator>kruchkov_alexandr</dc:creator><title>Когда kubectl врёт</title><pubDate>Wed, 11 Feb 2026 18:07:58 GMT</pubDate><description><![CDATA[<img src="https://img1.teletype.in/files/c4/34/c43441cf-cf71-463d-ac0d-d41be3a40d1d.png"></img>Сегодня мне для нашего кубернетис оператора надо было поменять Namespaced scope на Cluster scope у одного CRD.]]></description><content:encoded><![CDATA[
  <p id="qToV"><br />Сегодня мне для нашего кубернетис оператора надо было поменять <code>Namespaced scope</code> на <code>Cluster scope</code> у одного CRD.</p>
  <p id="zxO3"><br />Казалось бы рутинная задача: </p>
  <ul id="XJgZ">
    <li id="XfFz">удалить старый CRD</li>
    <li id="meDE">накатить новый с scope: Cluster (через ArgoCD сменой версии)</li>
    <li id="cvqj">обновить манифесты custom resources, чтобы не было namespace в метаданных.</li>
  </ul>
  <p id="OUIE">Поднял локальный kind-кластер, протестировал  всё летает. <br />Накатил на stage-кластер  готово, CRD установился, статус <code>Established</code>.<br />Применяю тестовый манифест:</p>
  <pre id="thOb">kubectl apply -f test.yaml</pre>
  <p id="wvNL">И тут...</p>
  <pre id="Qk7y">Error from server (NotFound): error when creating &quot;test.yaml&quot;: 
the server could not find the requested resource 
(post myresources.operator.example.com)</pre>
  <figure id="lHEI" class="m_original">
    <img src="https://img1.teletype.in/files/c4/34/c43441cf-cf71-463d-ac0d-d41be3a40d1d.png" width="480" />
  </figure>
  <p id="0k8U"></p>
  <h3 id="e2ly">Поехали дебажить</h3>
  <p id="Nt1U">CRD в кластере есть:</p>
  <pre id="hrFv">kubectl get crd myresources.operator.example.com
NAME                                 CREATED AT
myresources.operator.example.com     2026-02-11T15:23:50Z</pre>
  <p id="BX53">Статус <code>Established</code>:</p>
  <pre id="cXIP">kubectl get crd myresources.operator.example.com -o 
jsonpath=&#x27;{.status.conditions[?(@.type==&quot;Established&quot;)]}&#x27;
{&quot;lastTransitionTime&quot;:&quot;2026-02-11T15:23:50Z&quot;,
&quot;message&quot;:&quot;the initial names have been accepted&quot;,
&quot;reason&quot;:&quot;InitialNamesAccepted&quot;,
&quot;status&quot;:&quot;True&quot;,
&quot;type&quot;:&quot;Established&quot;}</pre>
  <p id="HWtZ"><code>Scope </code>правильный:</p>
  <pre id="SoLQ">kubectl get crd myresources.operator.example.com 
-o jsonpath=&#x27;{.spec.scope}&#x27;
Cluster</pre>
  <p id="aNbW">List работает:</p>
  <pre id="rtYW">kubectl get --raw /apis/operator.example.com/v1alpha1/myresources
{&quot;kind&quot;:&quot;MyResourceList&quot;,&quot;apiVersion&quot;:&quot;operator.example.com/v1alpha1&quot;,
&quot;items&quot;:[]}</pre>
  <p id="pvyS">А apply нет. 404. Не найдено. Бред.</p>
  <p id="Mdqh"></p>
  <h3 id="tuAS">Начинаем копать</h3>
  <p id="Fy8B">Первая мысль &quot;<em>может API ещё не зарегистрировался до конца?</em>&quot;<br />Жду минуту. Пять минут. Десять минут.<br />Та же херня.</p>
  <p id="AtPt">Вторая мысль &quot;<em>может права? RBAC?</em>&quot;<br />Проверяю:</p>
  <pre id="Q980">kubectl auth can-i create myresources.operator.example.com
yes</pre>
  <p id="LVvT"><br />Права есть. API отвечает. CRD Established. </p>
  <p id="yK0j">А apply возвращает 404. Бред.</p>
  <p id="pxfK">Ладно, смотрим что kubectl вообще пытается сделать.<br />Включаю verbose режим:</p>
  <pre id="LqnI">kubectl create -f test.yaml -v=9 2&gt;&amp;1 | grep -E &quot;POST |Request Body|namespaces/&quot;</pre>
  <p id="mjJK">И вот тут я вижу ЭТО:</p>
  <pre id="Xw3k">Request Body: {&quot;apiVersion&quot;:&quot;operator.example.com/v1alpha1&quot;,&quot;kind&quot;:&quot;MyResource&quot;,&quot;metadata&quot;:{&quot;name&quot;:&quot;my-test-resource&quot;,&quot;namespace&quot;:&quot;my-namespace&quot;}, ...}

curl -v -XPOST ... &#x27;https://.../apis/operator.example.com/v1alpha1/namespaces/my-namespace/myresources?...&#x27;

POST https://.../apis/operator.example.com/v1alpha1/namespaces/my-namespace/myresources 404 Not Found</pre>
  <p id="qfWv">Стоп.</p>
  <p id="wVll"><code>kubectl </code>пытается создать ресурс по <code>namespaced </code>URL: </p>
  <pre id="2Sq1">.../namespaces/my-namespace/myresources.</pre>
  <section style="background-color:hsl(hsl(55,  86%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="GlHs">Но ресурс-то у меня теперь <code>cluster-scoped</code> !!!!!!!</p>
  </section>
  <p id="0DKy">Для него URL <code>должен </code>быть </p>
  <pre id="1Bxs">.../apis/operator.example.com/v1alpha1/myresources (без /namespaces/).</pre>
  <p id="HBPL">Более того, <code>kubectl </code>сам подставляет namespace в тело запроса, хотя в моём манифесте его нет!</p>
  <p id="0Wnf"></p>
  <h3 id="u1pe">Откуда namespace в запросе?</h3>
  <p id="7KAf">Проверяю текущий контекст:</p>
  <pre id="DIrL">kubectl config get-contexts
CURRENT   NAME                           NAMESPACE
*         my-staging-cluster-context     my-namespace</pre>
  <p id="8y4R">Ага. У меня в контексте задан default namespace my-namespace.</p>
  <p id="t7IX">kubectl видит:</p>
  <ul id="Bo7v">
    <li id="2csd">в контексте есть namespace</li>
    <li id="uVFo">в манифесте <code>metadata.namespace</code> не указан</li>
    <li id="YxWF">думает: &quot;<em>ресурс же namespaced, надо подставить namespace из контекста!</em>&quot;</li>
    <li id="WhP4">подставляет <code>namespace </code>и строит URL для <code>namespaced </code>ресурса</li>
  </ul>
  <p id="2suw">Но откуда kubectl взял, что ресурс <code>namespaced</code>? </p>
  <p id="gtOC">Хер ли ты такой инициативный-то?</p>
  <p id="nvB2">Пришлось углубиться в документацию, стаковерфлоу и немного потыкать бесплатный джемини...</p>
  <p id="esix"></p>
  <h3 id="sGmf">Discovery cache  вот где собака зарыта </h3>
  <blockquote id="cCXo">я уже дед, если так говорю? да?😢 </blockquote>
  <p id="wgiY">kubectl делает discovery при первом запросе к новой API-группе  запрашивает список всех ресурсов этой группы и их параметры:</p>
  <ul id="fcBV">
    <li id="CHWN">какие есть kinds</li>
    <li id="ZdPW">какие shortNames (например, po для pods, svc для service)</li>
    <li id="iFFd">namespaced или cluster-scoped</li>
    <li id="XHMi">какие verbs доступны (create, get, list, etc.)</li>
  </ul>
  <p id="kAaF">Эта информация сохраняется в <code>кэше</code>:</p>
  <pre id="jUUL">~/.kube/cache/discovery/&lt;server-host&gt;/v1/serverresources.json</pre>
  <p id="KnYR">Я даже не знал о его существовании.</p>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="WLcZ">Кэш позволяет kubectl не дёргать API-сервер при каждом kubectl get po, а сразу знать, что po = pods.</p>
  </section>
  <p id="LvpY">Проверяю discovery для моего ресурса:</p>
  <pre id="iGTT">kubectl get --raw /apis/operator.example.com/v1alpha1</pre>
  <p id="2wo4">Вывод:</p>
  <pre id="Gr8z">{
  &quot;resources&quot;: [
    {
      &quot;name&quot;: &quot;myresources&quot;,
      &quot;singularName&quot;: &quot;myresource&quot;,
      &quot;namespaced&quot;: false,
      &quot;kind&quot;: &quot;MyResource&quot;,
      &quot;verbs&quot;: [&quot;delete&quot;,&quot;deletecollection&quot;,&quot;get&quot;,&quot;list&quot;,&quot;patch&quot;,&quot;create&quot;,&quot;update&quot;,&quot;watch&quot;]
    }
  ]
}</pre>
  <p id="iRo8">API-сервер правильно отвечает: </p>
  <pre id="Ef58">&quot;namespaced&quot;: false.</pre>
  <p id="7c8S">Значит проблема в кэше kubectl. </p>
  <p id="1Hxt">Он закэшировал старую версию CRD (когда тот был namespaced), и теперь считает, что ресурс всё ещё namespaced!</p>
  <p id="KDCH"></p>
  <h3 id="5u94">TTL кэша discovery</h3>
  <p id="HAgf">До Kubernetes v1.22 (август 2021) TTL кэша discovery в kubectl составлял 10 минут.<br />Многие до сих пор думают, что достаточно подождать 10 минут, и кэш обновится сам. Хуй вам. </p>
  <p id="8YG9">С этого коммита</p>
  <ul id="sSsS">
    <li id="lUAt"><a href="https://github.com/kubernetes/kubernetes/commit/94f7f922054d0aa4aa07d572a940ec0dda842646" target="_blank">https://github.com/kubernetes/kubernetes/commit/94f7f922054d0aa4aa07d572a940ec0dda842646</a> </li>
  </ul>
  <p id="virs">(PR #107141, merged в августе 2021) </p>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="ThVT">дефолтное значение TTL увеличили до 6 часов:</p>
  </section>
  <pre id="VNnX">return diskcached.NewCachedDiscoveryClientForConfig(
    config, 
    discoveryCacheDir, 
    httpCacheDir, 
    time.Duration(6*time.Hour)  // было 10*time.Minute
)</pre>
  <p id="NrfD"><br />Изменение вступило в силу:</p>
  <ul id="huTs">
    <li id="mRuL">для kubectl  с Kubernetes v1.22 (август 2021)</li>
    <li id="RnX7">для всех client-go клиентов  с Kubernetes v1.25</li>
  </ul>
  <p id="MuAy">Причина: </p>
  <ul id="l8XN">
    <li id="dqCc">на больших кластерах с сотнями CRD каждый discovery-запрос может включать сотни GET запросов для всех group-versions. Это вызывало client-side rate limiting и тормозило kubectl.</li>
  </ul>
  <p id="CBfH">Увеличение TTL снизило частоту discovery, но создало новую проблему: <br />устаревший кэш может жить до 6 часов, и ждать не имеет смысла.</p>
  <p id="8DBr"></p>
  <h3 id="HUhP">Решение 1: Применить с пустым cache-dir</h3>
  <p id="VwhD">Самый быстрый способ  заставить kubectl не использовать старый кэш:</p>
  <pre id="9G1A">kubectl apply -f test.yaml --cache-dir=/tmp/kubectl-cache-fresh</pre>
  <p id="GGRb">kubectl создаст свежий кэш в /tmp/kubectl-cache-fresh, сделает discovery заново, увидит, что ресурс cluster-scoped, и применит манифест корректно:</p>
  <pre id="2TWC">myresource.operator.example.com/my-test-resource created</pre>
  <p id="krck">После первого успешного apply новый кэш уже содержит правильную информацию. <br />Дальше можно работать без --cache-dir.</p>
  <p id="UQ0V"></p>
  <h3 id="ttdZ">Решение 2: Сбросить namespace в контексте</h3>
  <p id="CJ7R">Если не хочется трогать кэш, можно убрать default namespace из контекста:</p>
  <pre id="MgEB">kubectl config set-context --current --namespace=&quot;&quot;
kubectl apply -f test.yaml</pre>
  <p id="OxHe">Без namespace в контексте kubectl не будет подставлять его автоматически, и запрос пойдёт на cluster-scoped URL.</p>
  <p id="nHjY">После применения можно вернуть namespace:</p>
  <pre id="coVQ">kubectl config set-context --current --namespace=&quot;my-namespace&quot;</pre>
  <p id="VHmh"></p>
  <h3 id="mJte"><br />Решение 3: Очистить кэш вручную</h3>
  <p id="dWYL">Удалить старый кэш для данного API-сервера:</p>
  <pre id="oHM0"># Посмотреть, где кэш
ls ~/.kube/cache/discovery/

# Удалить кэш для нужного хоста (подставьте свой каталог, я вообще всё дропнул))
rm -rf ~/.kube/cache/discovery/&lt;host-from-above&gt;
rm -rf ~/.kube/cache/http/&lt;same-host&gt;

kubectl apply -f test.yaml</pre>
  <p id="tjFf">kubectl при следующем запросе сделает discovery заново.</p>
  <p id="Q2nc"></p>
  <h3 id="1ejH">Решение 4: Обновить discovery через api-resources</h3>
  <p id="aR8I">Официальный способ форсировать обновление discovery cache без удаления файлов:</p>
  <pre id="ttpW">kubectl api-resources --api-group=operator.example.com
kubectl apply -f test.yaml</pre>
  <p id="pcwd">Команда kubectl api-resources принудительно обновляет discovery cache для указанной API-группы.<br />Это безопаснее, чем удаление файлов кэша вручную, и работает для конкретной группы, не затрагивая остальные.</p>
  <p id="MzuD"></p>
  <p id="QsGW"></p>
  <h2 id="VhzC">Как работает discovery cache **. </h2>
  <blockquote id="n08I">Материал со звёздочками для самых любознательных. </blockquote>
  <p id="Qe1f">Разберём подробнее, чтобы понять, почему эта проблема вообще возникает.</p>
  <h3 id="tyUX">Архитектура кэша</h3>
  <p id="sZ7m">kubectl хранит кэш в двух местах:</p>
  <ul id="nMBl">
    <li id="mMUH">Discovery cache</li>
  </ul>
  <pre id="ZT0j">~/.kube/cache/discovery/&lt;server-host&gt;/</pre>
  <ul id="Oeaq">
    <ul id="hnOe">
      <li id="L2ab">файлы serverresources.json для каждой group-version</li>
      <li id="RgjJ">содержат информацию о ресурсах: kinds, shortNames, verbs, scope</li>
    </ul>
    <li id="qa70">HTTP cache</li>
  </ul>
  <pre id="RtRd"> ~/.kube/cache/http/&lt;server-host&gt;/</pre>
  <ul id="4IAp">
    <ul id="JyM4">
      <li id="eZe0">кэш HTTP-запросов к API-серверу</li>
    </ul>
  </ul>
  <h3 id="V0pk"></h3>
  <p id="mFSZ">Процесс работы</p>
  <ul id="Wcyz">
    <li id="YCCk">Пользователь запускает</li>
  </ul>
  <pre id="AQ70">kubectl apply -f test.yaml</pre>
  <ul id="RnGT">
    <li id="FvPI">kubectl читает манифест, видит </li>
  </ul>
  <pre id="72eG">kind: MyResource</pre>
  <ul id="jNJZ">
    <li id="HXt7">Проверяет локальный кэш:</li>
    <ul id="AjqM">
      <li id="f1fi">есть ли файл </li>
    </ul>
  </ul>
  <pre id="Vrx9">~/.kube/cache/discovery/&lt;host&gt;/operator.example.com/v1alpha1/serverresources.json?</pre>
  <ul id="6TtT">
    <ul id="kYFo">
      <li id="KpXa">валиден ли этот файл (не истёк ли TTL)?</li>
    </ul>
    <li id="O47C">Если кэш валиден, kubectl использует закэшированную информацию</li>
    <li id="ygqk">Если кэш устарел или отсутствует, делает GET запрос к API-серверу:</li>
  </ul>
  <pre id="1Ujf">   GET /apis/operator.example.com/v1alpha1</pre>
  <ul id="xzzP">
    <li id="ci62">Сохраняет результат в кэш с TTL = 6 часов</li>
  </ul>
  <p id="yejD"></p>
  <h3 id="Ffvx">Проблема при смене scope</h3>
  <p id="7Fvl">Когда CRD меняет scope с Namespaced на Cluster:</p>
  <ul id="4ruN">
    <li id="omTt">Старый кэш содержит</li>
  </ul>
  <pre id="hGwR"> &quot;namespaced&quot;: true</pre>
  <ul id="c0wI">
    <li id="vxnX">Новый API отдаёт </li>
  </ul>
  <pre id="DaGz">&quot;namespaced&quot;: false</pre>
  <ul id="bF7I">
    <li id="BdZ7">но kubectl использует <strong>старый кэш</strong> (TTL ещё не протух)</li>
    <li id="vakC">kubectl строит URL для namespaced ресурса: </li>
  </ul>
  <pre id="vlR5">.../namespaces/&lt;ns&gt;/myresources</pre>
  <ul id="jpa2">
    <li id="Rywc">Для cluster-scoped ресурса такой endpoint не существует и он выплёвывает </li>
  </ul>
  <pre id="Fyqz">404</pre>
  <p id="d36a"></p>
  <h3 id="3Hwe">Почему kubectl подставляет namespace</h3>
  <p id="5icf">Когда kubectl видит:</p>
  <ul id="iqvB">
    <li id="SIYe">ресурс namespaced (по кэшу)</li>
    <li id="8t3i">в манифесте нет metadata.namespace</li>
    <li id="uzg5">в текущем контексте задан default namespace</li>
  </ul>
  <p id="Aifv">Он делает вывод: &quot;<em>пользователь забыл указать namespace, подставлю из контекста</em>&quot;.</p>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="9BVl">Для cluster-scoped ресурсов это некорректное поведение, но kubectl этого не знает, потому что кэш врёт.</p>
  </section>
  <p id="kLFn"></p>
  <h2 id="pB7x">Discovery cache и масштабирование</h2>
  <p id="F46V">Эта проблема  часть более широкой темы: discovery cache как ограничение расширяемости Kubernetes.</p>
  <p id="4nPe"></p>
  <h3 id="vLCr">Проблема с большим количеством CRD</h3>
  <p id="n48y">На кластерах с сотнями CRD (например, с Crossplane, GCP Config Connector, Azure Service Operator) discovery превращается в бутылочное горлышко:</p>
  <ul id="Dyuv">
    <li id="ELTI">kubectl запускает discovery для всех, сука, group-versions в кластере</li>
    <li id="BGHG">Если у вас 500 CRD, это 500+ GET-запросов</li>
    <li id="DstN">Каждый kubectl get pods может инициировать discovery (если кэш устарел)</li>
    <li id="OXsq">Это вызывает client-side rate limiting:</li>
  </ul>
  <pre id="izpV">   Waited for 1.140352693s due to client-side throttling, not priority and fairness</pre>
  <p id="7qEw"></p>
  <p id="lOZt">Увеличение TTL решило одну проблему (частые discovery-запросы), но создало другую (долгоживущий устаревший кэш).</p>
  <p id="uPhn"></p>
  <h3 id="AjtM">Будущие улучшения</h3>
  <p id="w6ru">Обсуждаются следующие подходы:</p>
  <ul id="l6o8">
    <li id="m8k4">Инкрементальный discovery обновлять только изменившиеся group-versions</li>
    <li id="h7qn">server-side filtering  позволить клиенту запрашивать discovery только для нужных kinds</li>
    <li id="DlRd">Configurable TTL сделать TTL настраиваемым через флаг</li>
  </ul>
  <p id="haRI">Подробнее: </p>
  <ul id="TzLB">
    <li id="D1Tz"><a href="https://github.com/kubernetes/kubernetes/issues/107077" target="_blank">https://github.com/kubernetes/kubernetes/issues/107077</a></li>
  </ul>
  <p id="aiaF"></p>
  <h3 id="0b6l">Выводы  и практические советы</h3>
  <ul id="0jjX">
    <li id="uyXD">При смене scope CRD сразу очищайте кэш, используйте --cache-dir или запустите kubectl api-resources</li>
    <li id="Oumn">На CI/CD не используйте общий кэш между пайплайнами или очищайте перед каждым запуском</li>
    <li id="268D">При траблшутинге kubectl первым делом включайте -v=9 и смотрите на URL запросов</li>
    <li id="rZvp">Если видите &quot;<em>client-side throttlin</em>g&quot;  проблема в discovery, не в API-сервере</li>
    <li id="itgo">Не ждите 10 минут, это устаревшая информация (до Kubernetes v1.22), сейчас TTL = 6 часов</li>
    <li id="01Tp">Официальный способ обновить кэш (форсирует re-discovery)</li>
  </ul>
  <pre id="MRWG"> kubectl api-resources --api-group=&lt;ваша-группа&gt; </pre>
  <p id="yD2u"></p>
  <h3 id="igOm">Альтернатива для операторов</h3>
  <p id="aMe5">Если пишете оператор, который программно работает с cluster-scoped ресурсами:</p>
  <ul id="Knkc">
    <li id="KBJM">используйте <code>typed clients</code> вместо <code>dynamic clients</code></li>
    <li id="13GF">если нужен dynamic client, явно указывайте scope через REST mapper</li>
    <li id="0S7M">не полагайтесь на автоматическую подстановку namespace</li>
  </ul>
  <p id="fB8x">Пример:</p>
  <pre id="noDU">import (
    &quot;k8s.io/apimachinery/pkg/apis/meta/v1/unstructured&quot;
    &quot;k8s.io/apimachinery/pkg/runtime/schema&quot;
)

gvr := schema.GroupVersionResource{
    Group:    &quot;operator.example.com&quot;,
    Version:  &quot;v1alpha1&quot;,
    Resource: &quot;myresources&quot;,
}

// Cluster-scoped resource  без namespace
obj, err := dynamicClient.Resource(gvr).Create(ctx, &amp;unstructured.Unstructured{
    Object: map[string]interface{}{
        &quot;apiVersion&quot;: &quot;operator.example.com/v1alpha1&quot;,
        &quot;kind&quot;:       &quot;MyResource&quot;,
        &quot;metadata&quot;: map[string]interface{}{
            &quot;name&quot;: &quot;my-instance&quot;,
            // НЕТ namespace!
        },
    },
}, metav1.CreateOptions{})

</pre>
  <p id="ruCu"></p>
  <h3 id="yGRK">Ссылки</h3>
  <ul id="rc2J">
    <li id="KtMa"><a href="https://serverfault.com/questions/1049006/what-does-kubectl-store-in-the-cache" target="_blank">https://serverfault.com/questions/1049006/what-does-kubectl-store-in-the-cache</a> </li>
    <li id="OhJO"><a href="https://jonnylangefeld.com/blog/the-kubernetes-discovery-cache-blessing-and-curse" target="_blank">https://jonnylangefeld.com/blog/the-kubernetes-discovery-cache-blessing-and-curse</a> </li>
    <li id="DnBx"><a href="https://github.com/kubernetes/kubernetes/pull/107141" target="_blank">https://github.com/kubernetes/kubernetes/pull/107141</a> </li>
    <li id="GaXU"><a href="https://github.com/kubernetes/kubernetes/issues/107077" target="_blank">https://github.com/kubernetes/kubernetes/issues/107077</a> </li>
    <li id="2l7q"><a href="https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/config_flags.go" target="_blank">https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/config_flags.go</a></li>
  </ul>

]]></content:encoded></item></channel></rss>