<?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>@monitorim_it</title><generator>teletype.in</generator><description><![CDATA[@monitorim_it]]></description><image><url>https://teletype.in/files/d4/9c/d49cb702-f11e-4ab9-bc20-6ffaaa1f2d80.jpeg</url><title>@monitorim_it</title><link>https://teletype.in/@monitorim_it</link></image><link>https://teletype.in/@monitorim_it?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/monitorim_it?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/monitorim_it?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Fri, 17 Jul 2026 05:31:02 GMT</pubDate><lastBuildDate>Fri, 17 Jul 2026 05:31:02 GMT</lastBuildDate><item><guid isPermaLink="true">https://teletype.in/@monitorim_it/collect-docker-logs-with-opentelemetry</guid><link>https://teletype.in/@monitorim_it/collect-docker-logs-with-opentelemetry?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it</link><comments>https://teletype.in/@monitorim_it/collect-docker-logs-with-opentelemetry?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it#comments</comments><dc:creator>monitorim_it</dc:creator><title>Сбор логов Docker-контейнеров с помощью OpenTelemetry</title><pubDate>Fri, 10 Jul 2026 13:31:29 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/65/b6/65b6fa1b-4d8e-4e2e-ae8a-7a778bcfad10.png"></media:content><description><![CDATA[<img src="https://www.dash0.com/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Frdn92ihu%2Fproduction%2F2fe8cf6b90b93cc218b8ad15e92861c69dd99ee7-1672x941.png%3Fw%3D1200&amp;w=3840&amp;q=100"></img>Перевод оригинальной статьи Collecting Docker Container Logs with OpenTelemetry.]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="u7ME">Перевод оригинальной статьи <a href="https://www.dash0.com/guides/docker-logs-opentelemetry" target="_blank">Collecting Docker Container Logs with OpenTelemetry</a>.</p>
  </section>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="Jiue">Перевод сделан специально для телеграм-канала <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ.</a> Подписывайтесь! Там еще больше полезных постов о мониторинге.</p>
  </section>
  <p id="TKyy">Когда в Docker-контейнерах что-то выходит из строя, команда <code>docker logs</code> позволяет довольно быстро приблизиться к поиску причины. Однако её ограничения становятся очевидными, когда количество хостов превышает число тех, к которым можно подключиться по SSH, контейнеры уже завершили работу, а поиск по логам разных сервисов требует ручного объединения вывода.</p>
  <p id="85jc">Передача логов с каждого хоста через конвейер OpenTelemetry решает эту проблему, однако процесс настройки не является очевидным. Получатель <code>filelog</code> кажется подходящим инструментом, пока вы не столкнётесь с ограничениями доступа к файловой системе, а альтернативные подходы зачастую оказываются значительно проще, чем кажется на первый взгляд.</p>
  <p id="XZNI">В этом руководстве показано, как на практике реализовать прием логов Docker, а также как построить полностью работоспособный конвейер от начала до конца.</p>
  <h2 id="how-docker-captures-and-stores-logs">Как Docker собирает и хранит логи</h2>
  <p id="CLMA">По умолчанию <a href="https://www.dash0.com/guides/mastering-docker-logs" target="_blank">Docker перехватывает все</a>, что контейнер записывает в <code>stdout</code> и <code>stderr</code>, после чего передаёт эти данные <strong>драйверу логирования</strong>. Драйвер, используемый по умолчанию — <code>json-file</code>, — записывает эти потоки в файл на хосте.</p>
  <pre id="RZt0" data-lang="bash">/var/lib/docker/containers/&lt;container-id&gt;/&lt;container-id&gt;-json.log</pre>
  <p id="KmhN">Этот файл содержит по одному объекту JSON для каждой строки лога, включая текст сообщения, поток (<code>stdout</code> или <code>stderr</code>) и временную метку.</p>
  <pre id="iXCF" data-lang="bash">{
  &quot;log&quot;: &quot;Starting cloud controller for cluster desktop&quot;,
  &quot;stream&quot;: &quot;stderr&quot;,
  &quot;time&quot;: &quot;2026-04-18T14:28:28.812549652Z&quot;
}</pre>
  <p id="PXHY">Проблема заключается в том, что эти файлы связаны с жизненным циклом контейнера. Когда контейнер удаляется, Docker удаляет вместе с ним и файл логов. Такое поведение является штатным для эфемерных сред, однако это означает, что нельзя полагаться исключительно на локальные файлы как на единственный источник достоверных данных.</p>
  <p id="LjKp">Более эффективный подход заключается в запуске OpenTelemetry Collector на каждом хосте и настройке Docker таким образом, чтобы он передавал логи непосредственно в Collector в момент их записи контейнерами, полностью исключая необходимость доступа к файлам. Затем Collector пересылает эти логи в централизованную систему хранения, где можно выполнять поиск и корреляцию данных по всей инфраструктуре. Остальная часть этого руководства посвящена настройке именно такого решения.</p>
  <h2 id="why-scraping-docker-log-files-is-often-the-wrong-choice">Почему чтение файлов логов Docker зачастую является не лучшим выбором</h2>
  <p id="4ZyZ">Когда специалисты впервые интегрируют логи Docker в OpenTelemetry, наиболее очевидным решением кажется <a href="https://www.dash0.com/guides/opentelemetry-filelog-receiver" target="_blank">настройка получателя filelog</a> на каталог <code>/var/lib/docker/containers/</code> с последующим разбором файлов. Такой подход действительно работает, однако нередко сопровождается дополнительными сложностями.</p>
  <p id="EzqQ">JSON-оболочка, которую Docker добавляет вокруг каждой строки лога, не полностью соответствует исходному сообщению, записанному приложением. Получателю filelog требуется конвейер операторов, который извлечёт внешний JSON, получит содержимое поля <code>log</code>, затем, если приложение также пишет логи в формате JSON, разберёт внутренний JSON и сформирует корректную запись лога OpenTelemetry.</p>
  <p id="QxTh">Кроме того, необходимо предоставить Collector доступ к сокету Docker или каталогу с логами контейнеров на каждом хосте. В средах, где такой доступ ограничен, либо при использовании управляемых сервисов контейнеризации, не предоставляющих доступ к файловой системе хоста, чтение файлов логов становится полностью невозможным.</p>
  <h2 id="an-easier-approach-the-fluentd-logging-driver">Более простой подход: драйвер логирования Fluentd</h2>
  <p id="OOyM">Docker поддерживает драйвер логирования <code>fluentd</code>, который отправляет логи непосредственно на совместимую с Fluentd конечную точку сразу после их записи контейнером, вместо того чтобы сначала сохранять их в файлы и затем считывать.</p>
  <p id="ZgS6"><a href="https://www.dash0.com/guides/opentelemetry-fluent-forward-receiver" target="_blank">Получатель <code>fluentforward</code></a> в OpenTelemetry Collector использует тот же протокол, поэтому можно полностью отказаться от использования самого Fluentd и принимать события непосредственно в Collector.</p>
  <p id="ZciN">Схема выглядит следующим образом:</p>
  <figure id="qWBp" class="m_custom">
    <img src="https://www.dash0.com/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Frdn92ihu%2Fproduction%2F2fe8cf6b90b93cc218b8ad15e92861c69dd99ee7-1672x941.png%3Fw%3D1200&w=3840&q=100" width="691.3623804463336" />
  </figure>
  <p id="3T5F">Логи передаются через сокет сразу после их создания, поэтому отсутствует необходимость в чтении файлов, а также не требуется настраивать конвейер операторов для удаления JSON-оболочки, добавляемой Docker.</p>
  <h2 id="configuring-the-docker-logging-driver">Настройка драйвера логирования Docker</h2>
  <p id="Vyuz">Сначала необходимо настроить Docker на использование <a href="https://docs.docker.com/engine/logging/drivers/fluentd/#fluentd-buffer-limit" target="_blank">драйвера логирования fluentd.</a> Это можно сделать глобально в файле <code>/etc/docker/daemon.json</code>, чтобы настройка применялась ко всем контейнерам данного хоста.</p>
  <pre id="gp2n" data-lang="yaml">{
  &quot;log-driver&quot;: &quot;fluentd&quot;,
  &quot;log-opts&quot;: {
    &quot;fluentd-address&quot;: &quot;localhost:24224&quot;,
    &quot;fluentd-async&quot;: &quot;true&quot;,
    &quot;fluentd-sub-second-precision&quot;: &quot;true&quot;,
    &quot;tag&quot;: &quot;docker.{{.Name}}&quot;
  }
}</pre>
  <p id="4PoH">Параметр <code>fluentd-async</code>играет здесь важную роль. Без него Docker блокирует запись логов контейнером до тех пор, пока Collector не подтвердит получение каждого сообщения. Если Collector окажется недоступен, контейнер немедленно прекратит работу.</p>
  <p id="fOiZ">При использовании асинхронного режима Docker буферизует сообщения локально и повторяет попытки их отправки в фоновом режиме. Однако если Collector так и не станет доступен, Docker сохранит не более <code>fluentd-buffer-limit</code> сообщений (по умолчанию 1 048 576), после чего начнёт молча отбрасывать новые записи.</p>
  <p id="42Zp">Без параметра <code>fluentd-sub-second-precision</code> временные метки будут округляться до целых секунд. Это делает ненадёжным порядок логов и их корреляцию в сервисах, которые записывают более нескольких строк в секунду.</p>
  <p id="Uozl">После изменения файла <code>daemon.json</code> необходимо перезапустить демон Docker. Следует учитывать, что новые настройки будут применены только к вновь созданным контейнерам, поэтому существующие контейнеры потребуется пересоздать:</p>
  <pre id="S30M" data-lang="bash">sudo systemctl restart docker</pre>
  <p id="Cakd">Теперь, когда Docker начнёт пересылать логи контейнеров на порт 24224, потребуется экземпляр Collector, прослушивающий этот порт и принимающий поступающие данные.</p>
  <h3 id="per-service-configuration-in-docker-compose">Конфигурация для каждого сервиса в Docker Compose</h3>
  <p id="WIyP">Если вы не можете изменить конфигурацию демона Docker напрямую или если для отдельных сервисов требуются разные параметры, драйвер логирования можно настроить отдельно для каждого сервиса в файле Compose:</p>
  <pre id="hx0z" data-lang="yaml"># docker-compose.yml
services:
  api:
    image: my-api:latest
    logging:
      driver: fluentd
      options:
        fluentd-address: localhost:24224
        fluentd-async: &quot;true&quot;
        fluentd-sub-second-precision: &quot;true&quot;
        tag: docker.{{.Name}}

  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    volumes:
      - ./otelcol.yaml:/etc/otelcol-contrib/config.yaml
    ports:
      - 24224:24224</pre>
  <p id="klqN">При использовании Docker Compose необходимо указывать <code>localhost</code>, а не имя сервиса (<code>otel-collector</code>) для разрешения адреса Fluentd, поскольку этот адрес разрешается демоном Docker на хосте, где имена сервисов Compose отсутствуют в DNS.</p>
  <p id="1pCy">Чтобы избежать повторения одинаковой конфигурации для нескольких сервисов, можно использовать YAML-якорь:</p>
  <pre id="4HsW" data-lang="yaml"># docker-compose.yml
x-logging: &amp;default-logging
  driver: fluentd
  options:
    fluentd-address: localhost:24224
    fluentd-async: &quot;true&quot;
    fluentd-sub-second-precision: &quot;true&quot;
    tag: docker.{{.Name}}

services:
  api:
    image: my-api:latest
    logging: *default-logging

  worker:
    image: my-worker:latest
    logging: *default-logging

  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    volumes:
      - ./otelcol.yaml:/etc/otelcol-contrib/config.yaml
    ports:
      - 24224:24224</pre>
  <h2 id="keeping-async-delivery-reliable">Обеспечение надежной асинхронной доставки</h2>
  <p id="K7cF">Асинхронный режим решает проблему обратного давления (back-pressure), однако создаёт другую проблему: если буфер в памяти заполняется быстрее, чем Collector успевает обработать накопленные данные, Docker начинает отбрасывать сообщения логов. По умолчанию буфер рассчитан на 1 048 576 событий, чего достаточно для большинства сервисов, однако при обнаружении потерь логов его размер можно увеличить:</p>
  <pre id="6PFY" data-lang="bash">{
  &quot;log-driver&quot;: &quot;fluentd&quot;,
  &quot;log-opts&quot;: {
    &quot;fluentd-async&quot;: &quot;true&quot;,
    &quot;fluentd-buffer-limit&quot;: &quot;2097152&quot;
  }
}</pre>
  <p id="TxzJ">По умолчанию Docker повторяет попытки доставки неограниченное количество раз, если Collector недоступен. Также можно настроить интервал ожидания между повторными попытками с помощью параметра <code>fluentd-retry-wait</code>, значение которого по умолчанию составляет одну секунду.</p>
  <pre id="29vT" data-lang="bash">{
  &quot;log-opts&quot;: {
    &quot;fluentd-async&quot;: &quot;true&quot;,
    &quot;fluentd-retry-wait&quot;: &quot;2s&quot;
  }
}</pre>
  <p id="nVbp">Если Collector работает на том же хосте, что и Docker, а именно такая схема используется чаще всего, вероятность того, что он окажется настолько медленным, чтобы вызвать переполнение буфера, крайне мала.</p>
  <h2 id="setting-up-the-opentelemetry-collector">Настройка OpenTelemetry Collector</h2>
  <p id="oCBZ">Вам потребуется использовать образ<code> otel/opentelemetry-collector-contrib</code>, поскольку получатель <code>fluent_forward</code> отсутствует в базовой поставке OpenTelemetry Collector.</p>
  <pre id="cuuN" data-lang="bash">docker pull otel/opentelemetry-collector-contrib:0.154.0</pre>
  <p id="Vhin">Вот минимальная конфигурация, настраивающая получатель <code>fluent_forward</code> для приема логов Docker:</p>
  <pre id="DEix" data-lang="yaml"># otelcol.yaml
receivers:
  fluent_forward:
    endpoint: 0.0.0.0:24224

processors:
  resourcedetection/system:
    detectors: [system]
    system:
      hostname_sources: [os]

exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    logs:
      receivers: [fluent_forward]
      processors: [resourcedetection/system]
      exporters: [debug]</pre>
  <p id="MW7R">Процессор <code>resourcedetection/system</code> автоматически добавляет имя хоста, что особенно полезно при корреляции логов, поступающих с нескольких хостов.</p>
  <p id="Pe6k">Добавьте Collector в качестве сервиса в файл Compose:</p>
  <pre id="RnpG" data-lang="yaml"># docker-compose.yml
services:
  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.154.0
    volumes:
      - ./otelcol.yaml:/etc/otelcol-contrib/config.yaml
    ports:
      - 24224:24224</pre>
  <p id="Wr7m">После перезапуска сервисов можно проверить логи Collector и убедиться, что логи Docker-контейнеров успешно поступают:</p>
  <pre id="kK72" data-lang="bash">docker compose logs otel-collector -f --no-log-prefix</pre>
  <p id="sEpp">Вы должны увидеть записи логов, поступающие от ваших контейнеров, где поле <code>Body</code> содержит исходную строку лога, а получатель <code>fluent_forward</code> автоматически добавляет такие атрибуты, как <code>container_name</code>, <code>container_id</code> и <code>source</code>:</p>
  <pre id="Rj6D" data-lang="bash">2026-06-08T07:43:51.763Z        info    ResourceLog #0
Resource SchemaURL: https://opentelemetry.io/schemas/1.40.0
Resource attributes:
     -&gt; host.name: Str(4e940d4722ea)
     -&gt; os.type: Str(linux)
ScopeLogs #0
ScopeLogs SchemaURL:
InstrumentationScope
LogRecord #0
ObservedTimestamp: 1970-01-01 00:00:00 +0000 UTC
Timestamp: 2026-06-08 07:43:51 +0000 UTC
SeverityText:
SeverityNumber: Unspecified(0)
Body: Str({&quot;timestamp&quot;: &quot;2026-06-08T07:43:51+00:00&quot;,&quot;level&quot;: &quot;info&quot;,&quot;pid&quot;: &quot;29&quot;,&quot;client.address&quot;: &quot;172.22.0.1&quot;,&quot;client.port&quot;: &quot;34230&quot;,&quot;url.path&quot;: &quot;/index.html&quot;,&quot;url.query&quot;: &quot;&quot;,&quot;network.protocol.name&quot;: &quot;HTTP/1.1&quot;,&quot;server.address&quot;: &quot;localhost&quot;,&quot;server.port&quot;: &quot;80&quot;,&quot;user_agent.original&quot;: &quot;curl/8.5.0&quot;,&quot;http.request.method&quot;: &quot;GET&quot;,&quot;http.request.header.referer&quot;: &quot;&quot;,&quot;http.response.status_code&quot;: 200,&quot;http.response.body.size&quot;: 615,&quot;http.server.request.duration&quot;: 0.000,&quot;trace_id&quot;: &quot;cdc134217e5afb428b188b4271c3305f&quot;,&quot;span_id&quot;: &quot;930898f94fe03e5b&quot;,&quot;parent_sampled&quot;: 0})
Attributes:
     -&gt; fluent.tag: Str(docker.nginx-server)
     -&gt; source: Str(stdout)
     -&gt; container_id: Str(18d07bfa79470448896c0f2fcbd0aa076dd0082fc8642f1c6339e6174c4b5bc6)
     -&gt; container_name: Str(/nginx-server)
Trace ID:
Span ID:
Flags: 0</pre>
  <p id="XQww">Начиная с этого момента можно приступать к дальнейшей настройке конвейера, чтобы привести эти логи в большее соответствие с <a href="https://www.dash0.com/knowledge/opentelemetry-logging-explained#understanding-the-opentelemetry-logging-specification" target="_blank">моделью данных логов OpenTelemetry</a>.</p>
  <p id="BwZ1">Например, атрибуты <code>container_name</code> и <code>container_id</code> в настоящее время находятся среди атрибутов записи лога, однако они описывают источник логов, а не содержимое конкретной записи. Поэтому их следует перенести в атрибуты ресурса. Для этого можно использовать <a href="https://www.dash0.com/guides/opentelemetry-resource-processor" target="_blank">процессор resource</a> или <a href="https://www.dash0.com/guides/opentelemetry-transform-processor" target="_blank">процессор transform</a>.</p>
  <p id="EEWO">Поле <code>Body</code> также содержит необработанную строку JSON, включающую данные, которые должны располагаться в других элементах модели данных OpenTelemetry: уровень критичности, временную метку, контекст трассировки, а также такие атрибуты, как <code>http.response.status_code</code> и <code>http.request.method</code>. Разбор этого JSON с помощью процессора transform и перенос каждого поля в соответствующее место модели данных OpenTelemetry делает эти данные доступными для поиска и анализа, вместо того чтобы они оставались скрытыми внутри строки.</p>
  <p id="nFDA">После того как вы убедитесь, что логи успешно поступают и записи полностью соответствуют модели данных OpenTelemetry, <a href="https://www.dash0.com/guides/opentelemetry-debug-exporter" target="_blank">экспортёр debug</a> можно заменить на экспортёр, который будет передавать логи в используемую вами платформу наблюдаемости.</p>
  <h2 id="final-thoughts">Заключительные мысли</h2>
  <p id="ur9t">Построенный в этом руководстве конвейер намеренно остается минималистичным. Один получатель, несколько процессоров и один экспортёр. Этого достаточно, чтобы передавать логи с хоста в систему хранения, однако возможности дальнейшего развития значительно шире: можно разбирать JSON в поле Body, переносить поля в соответствующие атрибуты OpenTelemetry, <a href="https://www.dash0.com/guides/opentelemetry-filter-processor" target="_blank">отфильтровывать лишние данные</a> до их записи в хранилище или направлять логи различных сервисов в разные наборы данных.</p>
  <p id="1Wui"><a href="https://www.dash0.com/guides/opentelemetry-collector" target="_blank">Руководство по OpenTelemetry Collector</a> — хороший следующий шаг, если вы хотите научиться строить более сложные конвейеры, а <a href="https://www.dash0.com/guides/opentelemetry-fluent-forward-receiver" target="_blank">руководство по Fluent Forward Receiver</a> более подробно описывает работу получателя.</p>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="wavr">Подписывайтесь на телеграм-канал <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ</a>, там еще больше полезной информации о мониторинге!</p>
  </section>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@monitorim_it/memory-leak-hunting-for-dotnet</guid><link>https://teletype.in/@monitorim_it/memory-leak-hunting-for-dotnet?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it</link><comments>https://teletype.in/@monitorim_it/memory-leak-hunting-for-dotnet?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it#comments</comments><dc:creator>monitorim_it</dc:creator><title>Три недели в окопах: охота за утечкой 4 ГБ нативной памяти, которую .NET не мог увидеть</title><pubDate>Fri, 10 Jul 2026 13:00:23 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/7c/30/7c30ff5a-dc16-4935-aea2-12df569e8c6d.png"></media:content><description><![CDATA[<img src="https://miro.medium.com/v2/resize:fit:875/1*RcTV6F91WkKvnk04u0EBAw.png"></img>Перевод оригинальной статьи Three Weeks in the Trenches: Hunting a 4GB Native Memory Leak That .NET Couldn’t See.]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="id5r">Перевод оригинальной статьи <a href="https://medium.com/@kalyanjv/three-weeks-in-the-trenches-hunting-a-4gb-native-memory-leak-that-net-couldnt-see-0713935776d0" target="_blank">Three Weeks in the Trenches: Hunting a 4GB Native Memory Leak That .NET Couldn’t See</a>.</p>
  </section>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="DsdV">Перевод сделан специально для телеграм-канала <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ.</a> Подписывайтесь! Там еще больше полезных постов о мониторинге.</p>
  </section>
  <blockquote id="f2Mt"><strong>Кратко (TL;DR):</strong><em>Наши поды ASP.NET Core в Kubernetes завершались по OOM-kill каждые несколько часов во время предпускового пилотного запуска. Heap был практически пустой — 95% памяти занимала нативная память, невидимая для любых средств диагностики .NET. Причиной стали нестабильные мобильные соединения, создававшие тысячи «зомби»-подключений SignalR. Каждое такое подключение содержало собственные накладные расходы в нативной памяти среды выполнения — состояние подключения, структуры нативного взаимодействия (native interop) и многочисленные небольшие выделения памяти через <code>malloc()</code>, которые среда выполнения выполняет для каждого подключения. Поскольку размеры этих выделений были меньше порога 128 КБ для <code>mmap()</code> в glibc, они попадали во внутренний heap аллокатора, вызывая фрагментацию heap в десятках потоках. Освобождённая память никогда не возвращалась операционной системе. Дополнительный невидимый слой памяти составляли буферы отправки TCP ядра для уже несуществующих сокетов. Для устранения проблемы потребовалось внести четыре изменения на четырёх уровнях: сократить тайм-ауты SignalR (с 5 минут до 10 секунд); уменьшить тайм-аут бездействия ALB; устранить дублирование потоков данных; заменить стандартный аллокатор glibc на <code>jemalloc</code> с помощью <code>LD_PRELOAD</code>. После этого использование памяти подами стабилизировалось на уровне менее 1 ГБ. С тех пор не произошло ни одного OOM-kill.</em></blockquote>
  <h2 id="aaec">День 0</h2>
  <p id="39cb">Всё началось с уведомления поздним утром. Один из наших production-подов был завершён из-за нехватки памяти (OOM). Ничего необычного для системы под нагрузкой — за исключением того, что это продолжало происходить снова и снова. В рабочее время — примерно с 9:00 до 21:00, когда наши пользователи были активны, — поды разрастались до 4 ГБ и завершались. Kubernetes перезапускал их, наши мобильные клиенты автоматически переподключались через три секунды, и цикл начинался снова.</p>
  <p id="3b84">Я являюсь основателем и техническим директором <a href="https://www.basichomeloan.com/" target="_blank">BasicHomeLoan</a>. Система, о которой идёт речь, обрабатывает запросы на управление лидами в режиме реального времени — каждое взаимодействие с клиентом, журнал звонков, сообщение WhatsApp и обновление статуса заявки на ипотечный кредит передаются в режиме реального времени назначенному сотруднику отдела обработки заявок и его руководству через SignalR. Цель заключается в том, чтобы к моменту звонка клиента все сотрудники команды сопровождения, которым необходимо об этом знать, уже владели этой информацией. Возможность видеть происходящее в режиме реального времени во всей организации позволяет быстрее реагировать на обращения клиентов и повышает их удовлетворённость. Это был предпусковой пилотный запуск. Около 200 подключённых устройств, наша команда сопровождения тестировала систему перед полномасштабным внедрением. Поскольку это был контролируемый пилотный запуск, мы могли сопоставить IP-адреса клиентов и сетевые обращения с конкретными устройствами. Именно так мы обнаружили любопытную деталь: примерно половина нестабильных подключений поступала вовсе не из какого-то удалённого сельского района — они поступали из подвала и с первого этажа нашего собственного офиса, где мобильная связь была очень плохой. После того как мы выявили эту закономерность, мы специально разместили тестировщиков и разработчиков в подвале с их телефонами, чтобы намеренно воспроизводить обрывы соединения, одновременно наблюдая за серверными метриками в режиме реального времени.</p>
  <p id="b5ba">Архитектура была следующей: когда изменялась заявка на ипотеку — поступало новое сообщение WhatsApp, обновлялся статус или добавлялась запись о звонке, — мы считывали полную сущность заявки и связанные с ней таблицы, а затем отправляли её всем клиентам, которым необходимо было видеть эти данные. Поскольку приложение работало в нескольких подах Kubernetes, SignalR использовал Redis backplane — каждое широковещательное сообщение проходило через Redis, чтобы все поды могли доставить его своим подключённым клиентам независимо от того, какой под обработал исходный запрос. Позже станет понятно, почему это оказалось важным.</p>
  <p id="0407">Использование памяти пода говорило само за себя: в рабочее время что-то потребляло 4 ГБ памяти. Чтобы выяснить, что именно, понадобилось три недели — и до тех пор, пока проблема не была устранена, мы не могли запустить систему.</p>
  <figure id="ZgbJ" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*RcTV6F91WkKvnk04u0EBAw.png" width="700" />
  </figure>
  <h2 id="2e1d">Неделя 1: Очевидный подозреваемый</h2>
  <p id="faa2">Первое, что приходит в голову любому .NET-разработчику, столкнувшемуся с проблемой нехватки памяти, — это проверить управляемый heap. Я снял полный дамп памяти с пода, который потреблял 3,7 ГБ, и открыл его с помощью <code>dotnet-dump</code>.</p>
  <pre id="AVHO" data-lang="bash"># The managed heap breakdown
eeheap -gc</pre>
  <pre id="4a5h" data-lang="bash"># Result:
GC Heap Size:    ~207 MB</pre>
  <p id="fe8c">207 МБ из 3,7 ГБ. Управляемый heap использовал <strong>пять процентов</strong> от общего объема памяти.</p>
  <p id="16e6">Тем не менее я выполнил <code>dumpheap -stat</code> и действительно обнаружил проблему: наш интерцептор кэша второго уровня EF Core был настроен так, чтобы по умолчанию кэшировать все запросы, включая запросы потоковой передачи, которые выполнялись каждые две секунды. 172 000 объектов <code>EFTableRow</code>. 31 миллион строк. Я добавил <code>.NotCached()</code> ко всем запросам в нашем уведомителе потоковой передачи, повторно развернул приложение и начал наблюдать за метриками.</p>
  <p id="86f4">Объём управляемого heap уменьшился. RSS практически не изменился.</p>
  <p id="4133">Это стало причиной того, что расследование стало таким изнурительным. Мы работали всю ночь, развёртывая исправления и проводя нагрузочные тесты — бессонная ночь за бессонной ночью. И каждый раз ночные тесты проходили успешно. Использование памяти оставалось стабильным, поды работали без сбоев. Мы наконец ложились спать, уверенные, что решили проблему. Но наступало 9 утра, 200 реальных пользователей из нашей команды сопровождения подключались со своих телефонов через нестабильную мобильную сеть, и уже через несколько часов поды снова начинали завершаться. Проблема проявлялась только при реальном мобильном трафике с реальных устройств, работавших в нестабильных сетях, — то, что невозможно было воспроизвести ни одним синтетическим ночным тестом.</p>
  <p id="1aa4">Я принудительно запустил полную сборку мусора. Ничего. Вызвал <code>GC.Collect(2, GCCollectionMode.Aggressive, true, true)</code>. Ничего. Память по-прежнему оставалась занятой, продолжая неуклонно расти, совершенно не реагируя на сборщик мусора.</p>
  <blockquote id="OtwF"><strong>Осознание<br /></strong>Наша точка проверки работоспособности это подтвердила. Объем нативной памяти составлял <strong>2577 МБ</strong> при бюджете в 656 МБ — <strong>почти в 4 раза больше,</strong> чем должно было быть. Объем управляемого heap составлял 145 МБ. Сборщику мусора было нечего освобождать, потому что 95% памяти вообще не относились к управляемой памяти.</blockquote>
  <h2 id="5522">Небольшое отступление: что такое «нативная» память?</h2>
  <p id="623f">Если вы всю свою карьеру писали на C#, возможно, вам никогда не приходилось об этом задумываться. В .NET ваши объекты находятся в <strong>управляемом heap</strong> — области памяти, которой управляет сборщик мусора (GC). Когда вы создаёте объект, GC выделяет для него память. Когда на объект больше никто не ссылается, GC освобождает эту память. Именно с этим работают <code>dumpheap</code>, <code>gcroot</code> и <code>GC.Collect()</code>.</p>
  <p id="c288">Но приложение .NET не существует само по себе. Оно работает поверх среды выполнения, а та, в свою очередь, работает поверх операционной системы. И операционная система, среда выполнения и многие библиотеки, от которых зависит ваше приложение, выделяют память за пределами управляемого heap. Это и есть <strong>нативная память</strong> — память, выделяемая непосредственно у операционной системы с помощью функций уровня C, таких как <code>malloc()</code> и <code>free()</code>. Сборщик мусора .NET не может её видеть, отслеживать или освобождать. К ней относятся, например, буферы подключений к базе данных (MySQL считывает данные из сети в нативные буферы), внутренние структуры среды выполнения (состояние JIT-компилятора, стеки потоков) и — что было особенно важно в нашем случае — <strong>накладные расходы среды выполнения на каждое подключение</strong>. Каждое WebSocket-подключение приводит к выделению нативной памяти, пока среда выполнения .NET управляет его жизненным циклом: создаются структуры взаимодействия с нативным кодом, данные отслеживания подключения и множество небольших внутренних выделений памяти через <code>malloc()</code>, которые накапливаются при сотнях одновременных подключений. По отдельности они невелики, но в совокупности становятся значительными — и ни одно из этих выделений не видно сборщику мусора.</p>
  <p id="87ac">В Linux существует параметр <strong>RSS</strong> — Resident Set Size.Это общий объём физической памяти, который процесс фактически использует, согласно данным операционной системы. Он включает управляемую кучу, все нативные выделения памяти, стеки потоков и файлы, отображённые в память. Но Kubernetes ориентируется не только на RSS — он использует <strong>учёт памяти cgroup</strong>, который включает RSS и память ядра, потребляемую от имени процесса, например буферы TCP-сокетов. Когда Kubernetes принимает решение завершить под из-за нехватки памяти (OOM), он ориентируется именно на этот совокупный показатель, а не на размер управляемой кучи. Когда на панели мониторинга вы видите, что под потребляет 4 ГБ памяти, это и есть память cgroup.</p>
  <p id="8271">Разница между управляемой кучей и RSS — это объём вашей нативной памяти. В нормально работающем приложении .NET нативная память может составлять 200–400 МБ — накладные расходы среды выполнения, пулы соединений, состояние JIT-компилятора. В нашем случае она составляла <strong>3,5 ГБ</strong>.</p>
  <figure id="ToOg" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*bsn424Z9jAMvk8WE38ynqg.png" width="700" />
  </figure>
  <p id="846a">Именно здесь отладка .NET разделяется на два мира. Всё, что я делал до этого момента — <code>dumpheap</code>, <code>gcroot</code>, принудительный запуск GC, — работает только с управляемой кучей. Память, которая поглощала наши поды, была нативной памятью и находилась ниже уровня среды выполнения .NET — в том слое, о котором большинству C#-разработчиков никогда не приходится задумываться.</p>
  <p id="36ef">Я переключился на LLDB — единственный отладчик, который способен видеть и управляемую, и нативную память в дампе Linux. И начал изучать, как на самом деле работает память в Linux. В частности, то, что называется <strong>glibc</strong>.</p>
  <h2 id="2e69">Неделя 2: Проблема зомби</h2>
  <p id="6aa9">Анализ дампа ни к чему определённому не привёл. Ни одно отдельное выделение нативной памяти не было достаточно большим, чтобы объяснить 3,5 ГБ. Это была не классическая утечка памяти — не забытый вызов <code>malloc()</code> без соответствующего <code>free()</code>. Каждому выделению памяти соответствовало её освобождение. Память освобождалась, но не возвращалась.</p>
  <p id="afb7">Чтобы понять причину, мне нужно было разобраться, что на самом деле происходит по сети. Вот наша архитектура:</p>
  <figure id="q3Wy" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*FPUTrmoN6RfKCZQuNso_Hg.png" width="700" />
  </figure>
  <p id="9659">Каждое изменение заявки приводило к полному чтению данных из базы и широковещательной отправке всем подключённым клиентам. Клиенты использовали SignalR только для получения данных — все изменения выполнялись через REST API. И именно здесь скрывалась проблема: в нашей конфигурации SignalR параметр <code>ClientTimeoutInterval</code> был установлен на <strong>пять минут</strong>.</p>
  <p id="3223">Пять минут. На мобильных устройствах с нестабильной сотовой связью. При автоматическом переподключении каждые три секунды.</p>
  <p id="6cec">Представьте, что происходит, когда мобильное устройство въезжает в тоннель, переключается с Wi-Fi на мобильную сеть или просто попадает в зону отсутствия сигнала. Сетевое соединение молча разрывается — без корректного завершения, без уведомления сервера, вообще без каких-либо сигналов. Сервер не знает, что клиент исчез. Он продолжает считать соединение активным и продолжает отправлять обновления сущностей в пустоту ещё <em>в течение пяти минут</em>, пока не срабатывает тайм-аут KeepAlive и не завершает это «зомби»-подключение.</p>
  <p id="156c">Тем временем клиент уже переподключился. Через три секунды после потери сигнала он устанавливает <strong>новое</strong> соединение. Сервер теперь отправляет данные обоим соединениям — новому активному и старому «зомби-соединению». Через три секунды, если сеть снова произойдёт сбой, появится ещё одно «зомби-соединение». Каждое из них будет активно до пяти минут.</p>
  <h2 id="db79">Математика зомби</h2>
  <pre id="vAcL" data-lang="bash"># With 100 unstable clients:
KeepAlive timeout:    300 seconds (5 minutes)
Client retry:         3 seconds (linear)
Silent drop rate:     ~20-30% of disconnects</pre>
  <pre id="quvH" data-lang="bash"># Worst case per unstable client:
300s timeout / 3s retry = up to 100 zombies stacking up
                          before the first one even times out
# Conservative estimate across fleet:
100 clients × ~20-30 zombies at peak = 2,000-3,000 zombie connections</pre>
  <p id="f9bc">Каждое зомби-подключение удерживало нативную память, недоступную сборщику мусора: структуры среды выполнения, связанные с каждым подключением (состояние взаимодействия с нативным кодом, метаданные отслеживания подключения), а также буферы отправки TCP ядра, которые постепенно заполнялись обновлениями сущностей и никогда не получали подтверждения доставки. Объём нативной памяти на одно подключение был небольшим, но каждое «зомби» существовало до пяти минут, и в течение этого времени его выделения памяти действовали как своеобразные «якоря» во внутренней куче аллокатора, не позволяя возвращать окружающую их память операционной системе. Буферы транспорта на стороне управляемой памяти (System.IO.Pipelines в Kestrel) использовали пул и могли повторно использоваться, а вот нативные накладные расходы на каждое подключение — нет. Всё это было невидимо для <code>dotnet-dump</code>.</p>
  <h2 id="df14">Множитель Redis backplane</h2>
  <p id="19a5">Именно здесь работа в Kubernetes добавила ещё один нюанс. Поскольку мы использовали несколько подов, SignalR применял Redis backplane для синхронизации сообщений между ними. Когда один под выполнял широковещательную отправку обновления сущности, он публиковал сообщение в Redis, после чего каждый под получал его и доставлял своим локально подключённым клиентам — включая свои «зомби»-подключения.</p>
  <p id="a624">Это означало, что одно изменение сущности затрагивало не только «зомби» на одном поде. Оно проходило через Redis и достигало «зомби» на каждом поде. Каждый под поддерживал собственный набор «зомби»-подключений, каждое из которых имело собственные накладные расходы на нативную память. Кроме того, сам Redis backplane создавал дополнительную нагрузку. StackExchange.Redis использует управляемые буферы для операций чтения и записи, однако увеличение объёма публикаций и подписок (pub/sub), вызванное рассылкой сообщений «зомби»-подключениям, приводило к ещё более интенсивным выделениям памяти через нативный аллокатор при обработке каждого сообщения средой выполнения.</p>
  <p id="2f3a">При работе трёх подов мы имели дело уже не с одной армией «зомби», а с тремя, и все они получали одни и те же сообщения из потока Redis.</p>
  <blockquote id="Ab18"><strong>Усугубляющий фактор<br /></strong>Каждое изменение сущности отправляло новые данные в буферы отправки TCP ядра каждого зомби-подключения и продолжало удерживать его нативное состояние подключения во внутренней куче аллокатора. «Зомби», существовавший пять минут, за это время накапливал в своём буфере отправки ядра все широковещательные сообщения. Поскольку сущности изменялись каждые несколько секунд, объём памяти ядра, приходившийся на каждое «зомби», непрерывно рос. И чем дольше существовало «зомби», тем сильнее обычные выделения памяти перемешивались вокруг удерживаемых им нативных структур.</blockquote>
  <h2 id="7eb6">Ловушка фрагментации</h2>
  <p id="105d">Обнаружение «зомби» объяснило, куда уходила память. Но это не объяснило, почему она никогда не возвращалась, даже после периодов затишья, когда все «зомби» были удалены. Чтобы разобраться, мне пришлось спуститься ещё на один уровень — ниже .NET, ниже Kestrel, к самому аллокатору памяти Linux.</p>
  <h2 id="a226">Что такое glibc и почему разработчику на C# это должно быть интересно?</h2>
  <p id="8862">Когда ваше .NET-приложение работает под Linux, оно не взаимодействует с операционной системой напрямую. Между средой выполнения .NET и ядром Linux находится базовая библиотека под названием <strong>glibc</strong> (библиотека GNU C). Вы, вероятно, никогда об этом не задумывались, но она присутствует в каждом стандартном образе контейнера Docker для .NET, незаметно обрабатывая низкоуровневые процессы: файловый ввод-вывод, многопоточность, работу с сетью и — что наиболее важно в данном случае — <strong>выделение памяти</strong>.</p>
  <p id="1934">Каждый раз, когда чему-либо внутри процесса требуется память, не управляемая сборщиком мусора .NET, в конечном итоге вызывается <code>malloc()</code>, обработкой которого занимается glibc. Когда эта память больше не нужна, <code>free()</code> возвращает её glibc. Драйвер базы данных, внутренние структуры среды выполнения .NET, слой взаимодействия Kestrel с нативным кодом — все они используют glibc для работы с нативной памятью. Можно представить glibc как арендодателя памяти для всего, что не находится в управляемой куче.</p>
  <p id="2c9e">Специализированный аллокатор памяти внутри glibc называется <strong>ptmalloc2</strong>. Он был разработан в начале 2000-х годов как универсальный аллокатор и основывается на определённых предположениях о том, как приложения используют память. Эти предположения вполне разумны для короткоживущих программ, но перестают работать в случае современного сервера ASP.NET Core, который обслуживает сотни WebSocket-подключений внутри контейнера, никогда не перезапускающегося.</p>
  <h2 id="7db8">Порог, который нас погубил</h2>
  <p id="5a0f">Каждое выделение нативной памяти в процессе — взаимодействие среды выполнения с нативным кодом, временные объекты сериализации JSON, операции драйвера базы данных, управление жизненным циклом подключений — проходит через <code>malloc()</code>. Все эти выделения невелики — каждое значительно меньше 128 КБ. И именно в этом заключается проблема.</p>
  <p id="8dd0">У функции ptmalloc2 есть внутренний порог — по умолчанию 128 КБ — который определяет <em>способ</em> выделения памяти. Это самая важная деталь во всей этой истории:</p>
  <p id="41a1"><strong>Выделения памяти свыше 128 КБ</strong> получают от операционной системы собственный выделенный блок памяти (с помощью механизма <code>mmap()</code>). Представьте, что вы арендуете отдельное хранилище. Когда оно больше не нужно, вы возвращаете ключи, и операционная система сразу освобождает эту память. Всё просто и без остатка.</p>
  <p id="7a50"><strong>Выделенные объемы памяти менее 128 КБ</strong> аллокатора — в растущие области памяти, которыми аллокатор управляет самостоятельно (основная область расширяется через <code>brk()</code>, а дополнительные области потоков выделяют собственные области с помощью <code>mmap()</code>). Представьте себе место на общем хранилище. Когда вы вызываете <code>free()</code>, аллокатор лишь помечает этот участок памяти как доступный для повторного использования. Но здесь есть критически важный момент: <strong>ОС не получает эту память обратно</strong>. С точки зрения операционной системы процесс по-прежнему её использует. Она продолжает учитываться в RSS. Kubernetes тоже продолжает её учитывать.</p>
  <figure id="d3Sc" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*paakMa5v2D0OxPXDdgU0Zg.png" width="700" />
  </figure>
  <p id="6a77">Вот ключевая мысль: сами выделения памяти, связанные с «зомби»-подключениями, были недостаточно велики, чтобы заполнить 3,5 ГБ. Несколько килобайт нативных накладных расходов на подключение, умноженные на 3 000 «зомби», — это всего около 10–20 МБ реально используемых данных. Но эти выделения существовали по пять минут каждое и были разбросаны по всей куче. За это время все остальные выделения нативной памяти в процессе — сериализация JSON, чтение из базы данных, создание и завершение подключений исправных клиентов — попадали в те же самые области, чередуясь с памятью «зомби». Когда «зомби» наконец завершался, освободившиеся участки оставляли после себя «дыры», фрагментировавшие кучу. Содержимым этих 3,5 ГБ были вовсе не «зомби». Они были теми самыми «якорями», которые не позволяли куче когда-либо снова уменьшиться.</p>
  <p id="6d8f">Несколько слов о том, что мы могли и чего не могли наблюдать: во время первоначального инцидента у нас не было включено трассирование <code>malloc()</code> — в условиях реальной производственной среды невозможно инструментировать внутренние механизмы libc. Точный профиль распределения нативных ресурсов был восстановлен на основе того, что мы смогли измерить (RSS, размер управляемой кучи, количество подключений, память cgroup) и что мы смогли проверить (jemalloc устранил удержание памяти, тем самым доказав, что причиной была фрагментация в ptmalloc2). Конкретное взаимодействие между внутренними вызовами <code>malloc()</code> среды выполнения и жизненным циклом «зомби»-подключений — это наша наиболее вероятная модель того, почему всё происходило именно так. Она была построена на основе наблюдавшихся симптомов, внесённых исправлений и тех частей стека, которые были нам доступны. Когда вы отлаживаете проблему, проходящую через уровни, которыми не управляете, — среду выполнения .NET, Kestrel, glibc, ядро Linux, — именно так и выглядит реальность системной разработки.</p>
  <h2 id="f08e">Арены: почему потоки ухудшают ситуацию</h2>
  <p id="fd6b">В ptmalloc2 есть ещё одно архитектурное решение, которое усугубило нашу проблему. Чтобы уменьшить конкуренцию, когда нескольким потокам одновременно требуется память, он выделяет каждому потоку свой собственный отдельный пул памяти — так называемую <strong>арену</strong>. В системе с 8 ядрами ЦП он может создать до 64 арен. Идея вполне разумна: потоки не блокируют друг друга при выделении памяти.</p>
  <p id="25f1">Проблема заключается в том, что арены не могут совместно использовать свободную память. Представьте их как отдельные склады. Если на складе A есть свободные полки, а склад B переполнен, склад B не может воспользоваться свободным местом склада A — ему приходится расширяться. Kestrel использует пул потоков, а подключения в течение своего жизненного цикла могут обслуживаться разными потоками. «Зомби»-подключение могло получить свои нативные структуры в арене 1, затем обслуживаться потоком, использующим арену 3, а после завершения оставить «дыры» сразу в обеих аренах, которыми ни одна из них не могла поделиться с другой.</p>
  <p id="fa4a">Схема выделения памяти выглядела следующим образом:</p>
  <pre id="VmaI" data-lang="bash"># Thread pool thread 1 → arena 1
Handles connection A, native allocations: interop + state + metadata
Handles connection B, native allocations: interop + state + metadata</pre>
  <pre id="pn6M" data-lang="bash"># Connection A goes zombie — lives for minutes, pinning its allocations
# Meanwhile, normal churn (serialization, DB, HTTP) fills around it
Arena 1:  [churn] [A: pinned] [churn] [B: alive] [churn]
# Connection A finally times out and is freed
Arena 1:  [churn] [hole] [churn] [B] [churn]
# Repeat thousands of times across dozens of arenas
# → Heap only grows. RSS never returns.</pre>
  <p id="cbda">Жизненный цикл «зомби-подключений» был наихудшим из возможных вариантов для этого аллокатора. Долгое существование «зомби» (до пяти минут) приводило к тому, что их память глубоко перемешивалась с памятью исправных подключений. Когда «зомби» наконец умирал, освобожденная память представляла собой швейцарский сыр — разбросанные дыры по множеству областей, которые можно было повторно использовать для запросов того же или меньшего размера, но которые никогда нельзя было вернуть в операционную систему.</p>
  <p id="zkTa">И есть ещё одна деталь, окончательно замыкающая эту ловушку. Куча может уменьшаться только сверху. Представьте стопку коробок — убрать можно только верхние. Если верхняя коробка всё ещё используется, все коробки под ней оказываются заблокированными, даже если они пусты. При сотнях пересекающихся по времени жизненных циклов подключений в десятках арен почти всегда оставалось что-то ещё живое рядом с вершиной каждой кучи арены. Вся область ниже — потенциально сотни мегабайт свободных «дыр» — оставалась заблокированной и продолжала учитываться операционной системой и Kubernetes как «используемая память».</p>
  <p id="JyTa">Именно поэтому всё выглядело как утечка памяти, хотя с технической точки зрения каждому вызову <code>malloc()</code> соответствовал вызов <code>free()</code>. С точки зрения приложения память была освобождена. Но операционная система так её обратно и не получила.</p>
  <h2 id="4bb6">Ещё одним уровнем ниже</h2>
  <p id="309c">Фрагментация аллокатора объясняла, почему освобождённая память не возвращалась операционной системе. Но существовал ещё один уровень накопления нативной памяти, который был ещё менее заметен: <strong>буферы отправки TCP</strong>, управляемые ядром Linux.</p>
  <p id="c244">Когда приложение отправляет данные через сетевой сокет, они не попадают сразу в сеть. По пути они проходят через несколько буферов. В нашем случае сериализованные данные сущности проходили из .NET в буферы канала ввода-вывода Kestrel, а затем попадали в буфер, которым для каждого TCP-сокета управляет ядро Linux. Именно ядро отвечает за фактическую передачу байтов по сети, повторную отправку потерянных пакетов и ожидание подтверждений от другой стороны.</p>
  <p id="ff05">Когда мы отправляли обновления сущностей «зомби»-подключению, ядро добросовестно пыталось их доставить. Но подтверждения не поступало — клиент уже исчез. Протокол TCP не сдаётся сразу. Он повторно передаёт данные, используя механизм экспоненциальной задержки, каждый раз увеличивая интервал между попытками вдвое. В течение всего этого времени ядро продолжает удерживать данные, поскольку они могут понадобиться для повторной отправки.</p>
  <p id="524e">Эти буферы находятся в <strong>пространстве ядра</strong> — памяти, которой управляет сама операционная система и которая полностью находится за пределами адресного пространства процесса. Ни один инструмент диагностики на уровне приложения не может их увидеть. Их не видит <code>dotnet-dump</code>. Их не видит LLDB. Их не видит <code>pmap</code> — их вообще нет в виртуальном адресном пространстве процесса. Но они учитываются системой учёта памяти cgroup ядра. Их видит Kubernetes. Их видит OOM-killer.</p>
  <p id="8836">При пятиминутном тайм-ауте KeepAlive ядро удерживало буфер отправки каждого «зомби»-подключения на протяжении всего этого времени. Размер таких буферов обычно составляет от 64 КБ до 4 МБ на сокет — в зависимости от объёма данных в очереди и настроек системы.</p>
  <pre id="2xt9" data-lang="bash"># Estimated kernel memory from zombies:
2,000 zombie connections × ~1MB TCP send buffer = ~2GB in kernel space
# Completely invisible to any application-level diagnostic</pre>
  <h2 id="a68d">Почему Kubernetes усугубил ситуацию</h2>
  <p id="c8ea">В Kubernetes каждый под работает внутри изолированной среды (<strong>cgroup</strong>), которую ядро Linux использует для учёта и ограничения потребления ресурсов контейнером. Метрика памяти, за которой следит Kubernetes, включает всё: управляемую кучу, фрагментированную нативную кучу, удерживаемую аллокатором, и все буферы TCP ядра, принадлежащие «зомби»-сокетам. Она не различает «память, которую код действительно использует» и «память, которую аллокатор удерживает, но не использует».</p>
  <p id="ed1e">Мы используем <strong>Horizontal Pod Autoscaler (HPA)</strong> — механизм Kubernetes, который автоматически увеличивает или уменьшает количество реплик подов на основании метрик использования ресурсов. Когда поды начинали потреблять слишком много памяти, HPA масштабировал систему: больше подов, больше вычислительных ресурсов, выше стоимость. Но здесь возникла проблема: каждый новый под, присоединившийся к кластеру, сразу начинал получать широковещательные сообщения через Redis backplane и принимать повторные подключения мобильных клиентов. Уже через несколько минут у нового пода появлялась собственная армия «зомби». HPA масштабировал систему, пытаясь решить проблему памяти, но каждый новый под только усугублял эту проблему.</p>
  <p id="be09">Но когда нагрузка снижалась, RSS подов так и не уменьшался. Управляемая куча сокращалась — сборщик мусора выполнял свою работу. Буферы TCP ядра освобождались после закрытия «зомби»-сокетов. Но фрагментированная нативная куча, которую продолжал удерживать аллокатор, сохраняла высокий RSS. Память, которая действительно использовалась во время пиковых нагрузок, оставляла после себя множество пустых участков, которые операционная система уже не могла вернуть себе. HPA видел, что поды по-прежнему потребляют значительно больше памяти, чем необходимые им 600 МБ, и больше не уменьшал количество реплик.</p>
  <p id="f805">Мы платили за призрачную память — пустые фрагменты арен, которые операционная система не могла освободить, сборщик мусора не мог затронуть, а Kubernetes не мог отличить от реально используемой памяти. Поды, которым было достаточно 600 МБ, оставались раздутыми из-за фрагментированной нативной кучи, а HPA исправно продолжал поддерживать их работу.</p>
  <blockquote id="GlRQ"><strong>Полный каскад<br /></strong>Нестабильные мобильные соединения (в нашем собственном офисном подвале) → «зомби»-подключения SignalR, существующие несколько минут → Redis backplane рассылает обновления заявок «зомби»-подключениям на каждом поде → нативные выделения памяти для каждого подключения попадают в общую кучу вместо выделения отдельных областей памяти → фрагментация аллокатора между потоками пула → буферы отправки TCP ядра для «мёртвых» сокетов → объём памяти cgroup вырастает до 4 ГБ → завершение пода из-за нехватки памяти (OOM) → перезапуск пода → клиенты автоматически переподключаются через три секунды → HPA масштабирует систему → новые поды получают ту же самую армию «зомби» → цикл повторяется.</blockquote>
  <h2 id="0f54">Неделя 3: Исправление каждого слоя</h2>
  <p id="25d5">Единого решения не существовало. Проблема существовала на каждом уровне, и для каждого уровня требовалось своё собственное решение.</p>
  <h2 id="b1f5">Уровень 1: Быстрее избавляться от «зомби»</h2>
  <p id="993a">Наиболее значимым изменением стало сокращение тайм-аута SignalR с пяти минут до десяти секунд. Только это изменение уменьшило максимально возможное время существования «зомби»-подключения на 97%.</p>
  <figure id="rofA" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*IQK6NWU8EQC7kNBWBkcWiA.png" width="700" />
  </figure>
  <pre id="8fcF" data-lang="bash">var hubConfig = services.AddSignalR(options =&gt;
{
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(10);
    options.KeepAliveInterval = TimeSpan.FromSeconds(7);
    options.MaximumReceiveMessageSize = 32 * 1024;
    options.HandshakeTimeout = TimeSpan.FromSeconds(15);
    options.StreamBufferCapacity = 10;
    options.MaximumParallelInvocationsPerClient = 1;
    options.StatefulReconnectBufferSize = 0;
});</pre>
  <p id="88a8">Наши прежние значения не были случайными. Мы установили <code>ClientTimeoutInterval</code> равным пяти минутам, чтобы быть более терпимыми к мобильным клиентам, работающим через нестабильные сотовые сети. В документации Microsoft рекомендуется оставлять достаточный запас времени для получения ping-пакетов в сетях с высокой задержкой, и мы решили выбрать более безопасный вариант. Параметр <code>MaximumReceiveMessageSize</code> был увеличен до 128 КБ, чтобы иметь запас для команд, отправляемых клиентом в Hub (<code>JoinGroup</code>, <code>GetData</code> и аналогичных запросов на получение данных), хотя их размер редко превышал несколько сотен байт. На первый взгляд оба решения выглядели вполне разумными. Однако в нестабильных мобильных сетях с «тихими» разрывами соединения такой тайм-аут оказался катастрофическим, а увеличенный буфер приёма означал, что входной канал каждого «зомби»-подключения резервировал значительно больше памяти, чем ему когда-либо требовалось.</p>
  <p id="6a72">Параметр <code>StatefulReconnectBufferSize</code> тоже заслуживает отдельного пояснения. Эта возможность появилась в .NET 8 и хранит буфер для каждого подключения, чтобы клиент мог восстановить соединение после кратковременного разрыва, не потеряв сообщения. Звучит идеально для нестабильных соединений. Но 60% наших отключений происходили из-за переключения между сетями, при котором создаётся совершенно новое TCP-соединение. В таких случаях этот буфер никак не помогал. Мы расходовали примерно 10–17 МБ памяти на функцию, которая не приносила никакой пользы в нашей рабочей нагрузке.</p>
  <h2 id="9254">Уровень 2: Уменьшить масштаб последствий</h2>
  <p id="0a49">В нашем ALB (Application Load Balancer) тайм-аут бездействия составлял 3 600 секунд — один час. Он был увеличен по сравнению со стандартным значением AWS, равным 60 секундам, чтобы балансировщик нагрузки не закрывал долгоживущие WebSocket-подключения. Неактивные HTTP-соединения оставались открытыми в течение часа, удерживая нативную память. Мы вернули тайм-аут к 60 секундам и установили для Kestrel значение <code>KeepAliveTimeout</code>, равное 70 секундам (всегда немного больше тайм-аута ALB, чтобы избежать ошибок 502 Bad Gateway).</p>
  <pre id="Bt4w" data-lang="bash"># Kestrel (defaults: KeepAliveTimeout=130s, MaxConcurrentConnections=unlimited,
#          MaxConcurrentUpgradedConnections=unlimited)
options.Limits.KeepAliveTimeout = TimeSpan.FromSeconds(70);
options.Limits.MaxConcurrentConnections = 550;
options.Limits.MaxConcurrentUpgradedConnections = 350;</pre>
  <pre id="IfeB" data-lang="bash"># ALB (default idle timeout is 60s; ours was set to 3600s for WebSocket longevity)
Connection idle timeout: 60
HTTP client keepalive:   65</pre>
  <h2 id="a243">Уровень 3: Исправить конвейер потоковой передачи</h2>
  <p id="h3jm">Наш <code>SystemWaStreamNotifier</code> обрабатывал изменения сущностей каждые две секунды с помощью очереди, управляемой таймером. Но без устранения дубликатов одна и та же сущность могла неоднократно попадать в очередь и обрабатываться повторно при быстром поступлении изменений. Больше обработки означало больше чтений из базы данных, больше сериализации и больше данных, отправляемых в буферы «зомби»-подключений.</p>
  <p id="knoI">Мы добавили глобальное устранение дубликатов с помощью <code>ConcurrentDictionary</code>. Если сущность <code>App123</code> уже ожидала обработки, последующие обновления лишь обновляли отметку времени — новый элемент в очередь не добавлялся. Во время пиковых нагрузок количество операций обработки сократилось на 60–80%.</p>
  <h2 id="2c27">Уровень 4: Заменить аллокатор</h2>
  <p id="cbb1">Все перечисленные выше изменения уменьшили <em>скорость</em> накопления нативной памяти. Но фрагментация аллокатора glibc является следствием самой архитектуры — она заложена в принципах работы <code>ptmalloc2</code>. Даже при сокращении времени жизни «зомби»-подключений куча всё равно продолжала бы фрагментироваться с течением дней и недель. Просто медленнее.</p>
  <p id="a839">Окончательным решением проблемы фрагментации стала полная замена стандартного аллокатора glibc. Для этого потребовалась всего одна строка в нашем Dockerfile:</p>
  <pre id="OI1l" data-lang="bash">FROM mcr.microsoft.com/dotnet/aspnet:8.0</pre>
  <pre id="lpnw" data-lang="bash">RUN apt-get update &amp;&amp; \
    apt-get install -y libjemalloc2 &amp;&amp; \
    rm -rf /var/lib/apt/lists/*
ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2</pre>
  <p id="7c44"><code>LD_PRELOAD</code> — это переменная окружения Linux, которая указывает системе загрузить разделяемую библиотеку раньше всех остальных. Если указать в ней библиотеку jemalloc, каждый вызов <code>malloc()</code> и <code>free()</code> во всём процессе — от среды выполнения .NET до драйвера MySQL и любых других нативных библиотек — будет выполняться через jemalloc вместо стандартного аллокатора glibc. Никаких изменений в коде. Никакой повторной компиляции. Само приложение даже не знает, что использует другой аллокатор.</p>
  <p id="db6c"><a href="https://github.com/jemalloc/jemalloc" target="_blank">jemalloc</a> был создан именно для такого типа нагрузки — долгоживущих серверов с большим количеством одновременных выделений памяти разного размера, выполняемых из множества потоков. Именно его используют <a href="https://github.com/redis/redis/blob/unstable/deps/README.md" target="_blank">Redis</a>, <a href="https://engineering.fb.com/2011/01/03/core-infra/scalable-memory-allocation-using-jemalloc/" target="_blank">Meta</a> и <a href="https://blog.mozilla.org/nnethercote/category/memory-allocation/" target="_blank">Firefox</a> в продакшене. Вместо отдельных арен для каждого потока с фрагментированными списками свободных блоков jemalloc организует память по классам размеров (size-class bins), которые можно представить как заранее отсортированные полки для объектов разных размеров, и активно сообщает операционной системе о возможности освободить неиспользуемые страницы памяти. Для этого используется системный вызов <code>madvise</code>, который фактически говорит ядру: «Эта страница памяти мне больше не нужна — можешь забрать её обратно, но оставь резервирование адресного пространства на случай, если она снова понадобится». В результате освобождённая память действительно приводит к уменьшению RSS.</p>
  <figure id="3eve" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*v2S5x-aP6jqUn32yGnMrrg.png" width="700" />
  </figure>
  <blockquote id="E22n"><strong>Несколько слов о компромиссах при использовании jemalloc<br /></strong>jemalloc — не волшебная палочка. Его агрессивная очистка страниц памяти требует немного больше процессорного времени, чем аллокатор glibc, — ядру приходится выполнять больше работы по освобождению и повторному отображению страниц памяти. Некоторые средства мониторинга могут показывать непривычную картину использования памяти, поскольку внутренний механизм учёта jemalloc отличается от того, чего ожидают эти инструменты. Кроме того, это ещё одна зависимость: её необходимо установить в Docker-образ и своевременно обновлять. Следует также отметить, что темпы развития jemalloc в последние годы снизились после того, как Meta сократила объём инвестиций в проект. Тем не менее стабильные ветки продолжают получать исправления, а jemalloc по-прежнему остаётся аллокатором по умолчанию в FreeBSD. Для нашей рабочей нагрузки компромисс был очевиден — мы расходовали гигабайты оперативной памяти ради экономии нескольких процессорных циклов на управлении страницами памяти. Но прежде чем принимать такое решение, всегда следует проводить тестирование на собственной рабочей нагрузке.</blockquote>
  <h2 id="c441">Почему не другой аллокатор?</h2>
  <p id="a3e3">jemalloc был не единственным вариантом. Мы рассматривали две альтернативы, но в итоге отказались от них.</p>
  <p id="f7bb"><a href="https://github.com/gperftools/gperftools" target="_blank"><strong>gperftools tcmalloc</strong></a> (Google) — оптимизирован для быстрого выделения небольших объектов за счёт использования кэшей потоков. Хорошо показывает себя при умеренном количестве потоков, однако его центральные списки свободных блоков и общий пул страниц не разделены между потоками, что может стать узким местом при интенсивном выделении памяти из разных потоков. Это не соответствовало профилю нашей системы — сервера ASP.NET Core с активным использованием пула потоков и высокой интенсивностью создания и завершения подключений.</p>
  <p id="3a42"><a href="https://github.com/microsoft/mimalloc" target="_blank"><strong>mimalloc</strong></a> (Microsoft Research) — компактный высокопроизводительный аллокатор с превосходными результатами в тестах производительности. Однако в архитектуре версии 2 используются отдельные пулы памяти для каждого потока, и память намеренно не передаётся между потоками — память, освобождённая одним потоком, остаётся зарезервированной именно за ним. Для пула потоков с постоянно меняющейся нагрузкой это хорошо известный путь к накоплению памяти. Отличный выбор, если важна максимальная скорость выделения памяти, но неподходящий вариант для наших контейнеров с жёсткими ограничениями по памяти.</p>
  <p id="0e72"><strong>jemalloc</strong> оказался лучшим выбором благодаря своей зрелости и соответствию нашей задаче. Он появился в 2005 году как аллокатор libc для FreeBSD, что делает его самым старым из рассматриваемых альтернатив, проверенных в реальных условиях эксплуатации. С тех пор он прошёл испытание экстремальными нагрузками в Redis, Meta и Firefox. Окончательно выбор определили особенности его архитектуры: множество арен с возможностью совместного использования памяти между потоками, классы размеров, минимизирующие фрагментацию, и активный возврат неиспользуемых страниц операционной системе с помощью <code>madvise</code>. Для долгоживущего сервера с высокой интенсивностью создания и завершения подключений, сообщениями переменного размера и жёсткими ограничениями памяти контейнеров именно такое сочетание возможностей оказалось тем, что нам было нужно.</p>
  <h2 id="51ac">После</h2>
  <figure id="Ism4" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*CThP1lly7gTx-VBkdzUPdw.png" width="700" />
  </figure>
  <p id="c1c4">Использование памяти подами стабилизировалось на уровне менее 1 ГБ. Завершения из-за нехватки памяти (OOM) прекратились. HPA впервые начал уменьшать количество реплик. Перезапуски в середине рабочего дня прекратились.</p>
  <p id="cedd">Напомню — это был предпусковой пилотный запуск. 200 устройств, половина из которых принадлежала нашим собственным специалистам по сопровождению, пытавшимся работать при плохом сигнале в подвале офиса. Три недели мы не могли выйти в production, потому что эта проблема приводила к завершению каждого пода, который мы запускали. Через неделю после внедрения исправлений мы выполнили полноценный запуск системы. Система, которая не могла выдержать нагрузку от 200 пользователей во время пилотного запуска, теперь работает в промышленной эксплуатации без единого завершения из-за нехватки памяти (OOM).</p>
  <p id="cd68">Три недели мы каждую ночь развёртывали исправления, а днём наблюдали, как они не дают результата, — и всё из-за ошибки, которую не мог обнаружить ни один инструмент диагностики .NET. Но за эти три недели я узнал о том, как программное обеспечение на самом деле работает в Linux, больше, чем за несколько предыдущих лет разработки на C#.</p>
  <h2 id="cec1">Почему production ни разу не остановился</h2>
  <p id="c537">Три недели поды завершались из-за нехватки памяти каждые несколько часов, и при этом не произошло ни одного сбоя, заметного пользователям. Это было не везение, а результат трёх уровней защиты, которые мы реализовали ещё до того, как поняли первопричину проблемы: <strong>readiness probe</strong>, исключавшей под из балансировщика нагрузки при достижении 80% использования памяти (при этом существующие подключения продолжали обслуживаться), <strong>liveness probe</strong>, принудительно перезапускавшей под, если использование памяти слишком долго оставалось выше 93%, и <strong>SmartMemoryTrimService</strong>, который агрессивно запускал <code>GC.Collect()</code> с уплотнением Large Object Heap (LOH), чтобы удерживать управляемую кучу под контролем. Ключевой вывод заключался в следующем: к тому моменту, когда под приближался к завершению из-за нехватки памяти (OOM), он уже был исключён из production-трафика и обслуживал только «зомби»-подключения. Поэтому даже если происходило завершение из-за OOM, пользователи этого не замечали.</p>
  <p id="d605">Побочным эффектом стали затраты. Поды, которые были работоспособны, но не готовы к работе, поддерживали максимальное количество реплик HPA. Мы платили за стабильность инфраструктурными затратами — именно тот компромисс, который необходим, пока вы ищете первопричину проблемы.</p>
  <h2 id="f534">Чему меня научили три недели без сна</h2>
  <p id="e684"><strong>Большинству .NET-разработчиков никогда не приходится задумываться о нативной памяти.</strong> Управляемая куча, сборщик мусора, <code>IDisposable</code> — все эти абстракции настолько хорошо работают, что можно построить целую карьеру, так и не узнав, как операционная система выделяет память под средой выполнения. До тех пор, пока ваше приложение не начинает работать в контейнерах Linux с WebSocket-подключениями от мобильных устройств, находящихся в нестабильных сетях, — и тогда оказывается, что эта абстракция имеет свои пределы.</p>
  <p id="b320"><strong><code>dotnet-dump</code> видит только половину картины.</strong> Если в Linux растёт RSS, а размер управляемой кучи остаётся неизменным, значит проблема находится в нативной памяти, и <code>dotnet-dump</code> её не увидит. Для анализа дампов потребуется LLDB, а для просмотра областей памяти работающего процесса — <code>pmap</code>. Быстрый способ диагностики — запустить <code>dotnet-counters</code> и следить за двумя показателями: GC Heap Size и Working Set. Если Working Set продолжает расти, а GC Heap Size остаётся стабильным, значит проблема находится в нативной памяти. Инструменты .NET будут утверждать, что всё в порядке, в то время как Kubernetes уже готовится завершить ваш под из-за нехватки памяти.</p>
  <p id="7569"><strong>Стандартный аллокатор памяти Linux не рассчитан на подобную рабочую нагрузку.</strong> ptmalloc2 из glibc — это универсальный аллокатор, разработанный в эпоху, когда ещё не существовало контейнеров, WebSocket и пулов потоков, обслуживающих сотни одновременных подключений в долгоживущих процессах. Он исходит из того, что большинство приложений работают недолго, характер использования памяти достаточно предсказуем, а куча может свободно расти благодаря практически неограниченной виртуальной памяти. В контейнере с лимитом памяти 4 ГБ, внутри которого работает сервер, никогда не перезапускающийся, все эти предположения перестают быть верными. Если вы запускаете долгоживущий сервис ASP.NET Core в Linux, добавление <code>LD_PRELOAD=libjemalloc.so</code> в Dockerfile должно стать такой же привычной практикой, как установка <code>DOTNET_gcServer=1</code>. И это касается не только .NET — Rust, Node.js, Python и любой другой язык, использующий <code>malloc()</code>, в той или иной степени может столкнуться с той же проблемой в glibc.</p>
  <p id="f881"><strong>Значения тайм-аутов SignalR по умолчанию рассчитаны на стабильные сети, а их увеличение ради лучшей поддержки мобильных клиентов только усугубляет ситуацию.</strong> Мы установили <code>ClientTimeoutInterval</code> равным пяти минутам, полагая, что это поможет клиентам с нестабильным соединением. Вместо этого мы создали целую армию «зомби». Если ваши клиенты работают через мобильные сети, уменьшайте это значение, а не увеличивайте. Устанавливайте <code>ClientTimeoutInterval</code> максимально агрессивно. Ваши клиенты уже умеют автоматически переподключаться.</p>
  <p id="599c"><strong>Граница в 128 КБ имеет значение.</strong> В Linux стандартный аллокатор совершенно по-разному обрабатывает выделения памяти меньше и больше 128 КБ. В долгоживущем сервере все нативные выделения памяти — взаимодействие среды выполнения с нативным кодом, сериализация, управление подключениями — оказываются ниже этого порога и попадают в общую кучу, которая никогда не уменьшается. Долгоживущие подключения становятся источниками фрагментации, не позволяющими уменьшить размер кучи даже после закрытия этих подключений.</p>
  <p id="fc6b"><strong>Искусственный интеллект — это инструмент, многократно повышающий эффективность работы, а не замена инженерному мышлению.</strong> Я активно использовал <a href="https://claude.ai/" target="_blank">Claude</a> — как помощника для исследований, а не как непререкаемый источник истины. Он помог мне разобраться в механизме арен ptmalloc2, оценить объём памяти, занимаемой «зомби»-подключениями, проанализировать компромиссы при выборе настроек буферов и обратить внимание на альтернативные аллокаторы, о которых я раньше не задумывался. Но он ошибался не реже, чем был прав. Я тратил столько же времени на проверку его гипотез, сколько и на их использование: уточнял оценки памяти, опровергал предположения о работе буферов и заставлял его согласовывать выводы с тем, что показывали реальные метрики production. Направление задавало понимание системы. Скорость обеспечивал помощник, который в рамках одной беседы мог переключаться от внутреннего устройства ядра Linux к особенностям работы SignalR.</p>
  <blockquote id="s5o1"><strong><em>«Самые сложные ошибки находятся не в вашем коде. Они скрываются в предположениях, которые платформа делает о том, как этот код должен работать.»</em></strong></blockquote>
  <h2 id="f1ac">Хронология</h2>
  <figure id="q7AT" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*xlrAnpo4aBezkgQtS9zPKA.png" width="700" />
  </figure>
  <p id="e489">Если вы используете ASP.NET Core в контейнерах Linux с подключениями в режиме реального времени, проверьте объём нативной памяти. Запустите <code>dotnet-counters</code> и сравните GC Heap Size с Working Set. Если Working Set продолжает расти, а GC Heap Size остаётся стабильным, проблема находится в нативной памяти — за пределами области, которую контролирует сборщик мусора. Путь к её устранению начинается с понимания того, где на самом деле находится память вашего приложения.</p>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="NgyA">Подписывайтесь на телеграм-канал <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ</a>, там еще больше полезной информации о мониторинге!</p>
  </section>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@monitorim_it/how-to-otel-filelog-receiver</guid><link>https://teletype.in/@monitorim_it/how-to-otel-filelog-receiver?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it</link><comments>https://teletype.in/@monitorim_it/how-to-otel-filelog-receiver?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it#comments</comments><dc:creator>monitorim_it</dc:creator><title>OpenTelemetry Filelog Receiver: руководство по приему лог-файлов</title><pubDate>Thu, 09 Jul 2026 11:46:35 GMT</pubDate><media:content medium="image" url="https://img1.teletype.in/files/46/3d/463dc543-9f8e-4ec7-aae6-5932d69d4c25.png"></media:content><description><![CDATA[<img src="https://www.dash0.com/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Frdn92ihu%2Fproduction%2F621da0a29da0aeffa2ab20055f4cd6317c24e7a7-2126x892.png%3Fw%3D1200&amp;w=3840&amp;q=100"></img>Перевод оригинальной статьи OpenTelemetry Filelog Receiver: A Guide to Ingesting Log Files.]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="yOAC">Перевод оригинальной статьи <a href="https://www.dash0.com/guides/opentelemetry-filelog-receiver" target="_blank">OpenTelemetry Filelog Receiver: A Guide to Ingesting Log Files</a>.</p>
  </section>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="IFns">Перевод сделан специально для телеграм-канала <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ.</a> Подписывайтесь! Там еще больше полезных постов о мониторинге.</p>
  </section>
  <p id="wehG">Даже в эпоху облачных приложений и распределенного трейсинга обычные файлы логов остаются одним из самых ценных источников достоверной информации в любой системе. От корпоративных приложений и пакетных заданий до <a href="https://www.dash0.com/guides/nginx-logs" target="_blank">NGINX</a>, баз данных и локальной инфраструктуры — критически важная диагностическая информация по-прежнему записывается на диск.</p>
  <p id="fmux"><a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/filelogreceiver" target="_blank">Получатель filelog в OpenTelemetry Collector</a> позволяет интегрировать эти логи в <a href="https://www.dash0.com/guides/opentelemetry-collector" target="_blank">современный конвейер наблюдаемости</a>. Он непрерывно отслеживает файлы, анализирует их содержимое и преобразует необработанный текст в <a href="https://www.dash0.com/knowledge/opentelemetry-logging-explained" target="_blank">структурированные записи журналов OpenTelemetry</a>.</p>
  <p id="aenU">В этой статье показано, как использовать эти возможности на практике, от базового чтения файла до построения готового к эксплуатации конвейера, который корректно обрабатывает ротацию логов, восстанавливает работу после перезапуска и не теряет ни одной строки.</p>
  <p id="RnWP">Прежде чем углубляться: если вы контролируете приложения, записывающие эти логи, <strong>наилучшим решением будет настроить их на <a href="https://www.dash0.com/guides/structured-logging-for-modern-applications" target="_blank">вывод структурированных JSON-логов,</a></strong> размещённых в одной строке. Чтобы получатель <code>filelog</code> в этом случае стал простым и удобным уровнем приема данных.</p>
  <p id="nmK5">Значительная часть этой статьи посвящена ситуациям, когда такой вариант невозможен. В подобных случаях получатель <code>filelog</code> предоставляет мощные инструменты для анализа, структурирования и обогащения любых получаемых логов, превращая их в полноценные данные наблюдаемости.</p>
  <p id="OGvD">Начнём!</p>
  <h2 id="how-the-filelog-receiver-works">Как работает получатель filelog</h2>
  <figure id="OXir" class="m_custom">
    <img src="https://www.dash0.com/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Frdn92ihu%2Fproduction%2F621da0a29da0aeffa2ab20055f4cd6317c24e7a7-2126x892.png%3Fw%3D1200&w=3840&q=100" width="700.8542600896862" />
  </figure>
  <p id="sTMO">Прежде чем перейти к деталям конфигурации, полезно представить, как получатель обрабатывает файл логов на протяжении всего своего жизненного цикла. Можно представить это как простой повторяющийся четырехэтапный цикл:</p>
  <ol id="qvo3">
    <li id="WAFi"><strong>Обнаружение (Discover):</strong> получатель через заданные интервалы времени сканирует файловую систему, используя настроенные шаблоны <code>include</code> and <code>exclude</code>, чтобы определить, какие файлы логов необходимо отслеживать.</li>
    <li id="vnd9"><strong>Чтение (Read):</strong> после обнаружения файла получатель открывает его и начинает отслеживать появление новых строк. Параметр <code>start_at</code> определяет, следует ли начинать чтение с <code>beginning</code> или отслеживать только новые данные, начиная с <code>end</code>.</li>
    <li id="cxFj"><strong>Разбор (Parse):</strong> каждая строка (или блок строк, если используется разбор многострочных записей) проходит через последовательность операторов <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/pkg/stanza/docs/operators/README.md" target="_blank">Stanza</a>(если они настроены). Эти операторы разбирают необработанный текст, извлекают ключевые атрибуты, назначают временные метки и уровни критичности, а затем формируют структурированные данные логов.</li>
    <li id="UO2k"><strong>Передача (Emit)</strong> в завершение структурированные записи логов передаются в конвейер OpenTelemetry Collector, где они могут быть <a href="https://www.dash0.com/guides/opentelemetry-filter-processor" target="_blank">отфильтрованы</a>, дополнительно преобразованы или экспортированы в серверную часть.</li>
  </ol>
  <p id="MW0Q">Цикл <code>Discover -&gt; Read -&gt; Parse -&gt; Emit</code> лежит в основе всего сбора данных.</p>
  <h2 id="quick-start-tailing-a-log-file">Быстрый старт: отслеживание файла логов</h2>
  <p id="66Nq">Одним из наиболее распространённых сценариев является ситуация, когда приложение уже записывает логи в формате JSON в файл. Например, представим сервис, который записывает JSON-логи в файл <code>/var/log/myapp/app.log</code>:</p>
  <p id="GVDo">json</p>
  <pre id="Vu7S" data-lang="bash">{&quot;time&quot;:&quot;2025-09-28 20:15:12&quot;,&quot;level&quot;:&quot;INFO&quot;,&quot;message&quot;:&quot;User logged in successfully&quot;,&quot;user_id&quot;:&quot;u-123&quot;,&quot;source_ip&quot;:&quot;192.168.1.100&quot;}
{&quot;time&quot;:&quot;2025-09-28 20:15:45&quot;,&quot;level&quot;:&quot;WARN&quot;,&quot;message&quot;:&quot;Password nearing expiration&quot;,&quot;user_id&quot;:&quot;u-123&quot;}</pre>
  <p id="8wWS">Ниже приведён минимальный пример настройки получателя<code>filelog</code>, который считывает такие логи и передаёт их в конвейер OpenTelemetry.</p>
  <pre id="oSDp" data-lang="yaml"># otelcol.yaml
receivers:
  filelog:
    # 1. DISCOVER all .log files in /var/log/myapp/
    include: [/var/log/myapp/*.log]
    # 2. READ from the beginning of new files
    start_at: beginning
    # 3. PARSE using the json_parser operator
    operators:
      - type: json_parser
        # Tell the parser where to find the timestamp and how it&#x27;s formatted
        timestamp:
          parse_from: attributes.time
          layout: &quot;%Y-%m-%d %H:%M:%S&quot;
        # Tell the parser which field contains the severity
        severity:
          parse_from: attributes.level

exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    logs:
      receivers: [filelog]
      exporters: [debug]</pre>
  <p id="q8Ys">Ниже приведено объяснение приведённой выше конфигурации.</p>
  <ul id="lRVE">
    <li id="SohP"><code>include</code>: указывает получателю все файлы <code>.log</code> в каталоге <code>/var/log/myapp/</code>.</li>
    <li id="GvsL"><code>start_at: beginning</code>: гарантирует, что при первом обнаружении файла получатель обработает его полностью. По умолчанию используется значение <code>end</code>, при котором будут считываться только новые строки, записанные после запуска Collector.</li>
    <li id="F9E4"><code>operators:</code> в данном случае он всего один: <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/pkg/stanza/docs/operators/json_parser.md" target="_blank">json_parser.</a>  Его задача заключается в том, чтобы интерпретировать каждую строку лога как JSON, а затем перенести выбранные поля в основную метаинформацию записи лога.</li>
    <li id="6awj"><code><strong>timestamp</strong> и <strong>severity</strong></code><strong>: </strong>внутри <code>json_parser</code> из JSON извлекаются поля <code>time</code> и <code>level</code> и преобразуются в поля верхнего уровня OpenTelemetry <code>Timestamp</code> и <code>Severity</code> для каждой записи лога.</li>
  </ul>
  <p id="Hrqn">При использовании экспортёра <a href="https://www.dash0.com/guides/opentelemetry-debug-exporter" target="_blank">debug</a> вы увидите разобранный и структурированный результат. Вместо простого необработанного JSON каждое поле теперь будет корректно представлено внутри записи лога:</p>
  <pre id="2bnL" data-lang="bash">LogRecord #0
ObservedTimestamp: 2025-09-28 20:48:36.728437503 +0000 UTC
Timestamp: 2025-09-28 20:15:12 +0000 UTC
SeverityText: INFO
SeverityNumber: Info(9)
Body: Str({&quot;time&quot;:&quot;2025-09-28 20:15:12&quot;,&quot;level&quot;:&quot;INFO&quot;,&quot;message&quot;:&quot;User logged in successfully&quot;,&quot;user_id&quot;:&quot;u-123&quot;,&quot;source_ip&quot;:&quot;192.168.1.100&quot;})
Attributes:
     -&gt; user_id: Str(u-123)
     -&gt; source_ip: Str(192.168.1.100)
     -&gt; log.file.name: Str(myapp.log)
     -&gt; time: Str(2025-09-28 20:15:12)
     -&gt; level: Str(INFO)
     -&gt; message: Str(User logged in successfully)
Trace ID:
Span ID:
Flags: 0</pre>
  <p id="mtmQ">Теперь необработанные JSON-логи преобразованы в унифицированный формат данных логов OpenTelemetry, что обеспечивает единообразную основу для наблюдаемости в различных системах.</p>
  <p id="EL4m">Атрибут <code>log.file.name</code> по умолчанию автоматически добавляется получателем. Кроме того, можно включить параметр <code>include_file_path</code>, чтобы также сохранять полный путь к файлу.</p>
  <pre id="M03U" data-lang="yaml"># otelcol.yaml
receivers:
  filelog:
    include: [/var/log/myapp/*.log]
    include_file_path: true</pre>
  <p id="aEAK">Это позволяет легко фильтровать или выполнять поиск логов по их точному пути.</p>
  <pre id="iyK6" data-lang="bash">Attributes:
     -&gt; log.file.path: Str(/var/log/myapp/app.log)
     -&gt; log.file.name: Str(app.log)</pre>
  <p id="Vrfd">Дополнительные возможности обогащения данных описаны в <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/receiver/filelogreceiver/README.md" target="_blank">официальной документации OpenTelemetry Filelog Receiver.</a></p>
  <h2 id="filtering-and-managing-log-files">Фильтрация и управление файлами логов</h2>
  <p id="KpAf">Наиболее важным шагом в настройке настройке получателя <code>filelog</code> является указание того, какие файлы необходимо отслеживать. Это настраивается с помощью шаблонов <code>include</code> и <code>exclude</code>.</p>
  <p id="d4Wo">Сначала получатель использует шаблоны <strong><code>include</code></strong> для формирования списка всех потенциальных файлов, после чего применяет шаблоны <strong><code>exclude</code></strong>, исключая из этого списка нежелательные файлы.</p>
  <p id="N1Ib">Пример:</p>
  <pre id="b1Ag" data-lang="yaml"># otelcol.yaml
receivers:
  filelog:
    include: [/var/log/apps/**/*.log]
    exclude:
      - /var/log/apps/**/debug.log
      - /var/log/apps/**/*.tmp</pre>
  <p id="zIE4">В данном случае получатель будет собирать все файлы <code>.log</code> в каталоге <code>/var/log/apps/</code>, включая все вложенные каталоги, но пропустит любой файл с именем <code>debug.log</code>, а также любой файл с расширением <code>.tmp</code>.</p>
  <h3 id="excluding-files-by-modification-age">Исключение файлов по времени последнего изменения</h3>
  <p id="54Fc">Если каталог логов содержит большое количество уже существующих файлов, можно указать получателю игнорировать файлы, которые не изменялись в течение заданного периода времени, используя параметр <code>exclude_older_than</code>.</p>
  <pre id="Id6G" data-lang="yaml"># otelcol.yaml
receivers:
  filelog:
    include: [/var/log/myapp/*.log]
    exclude_older_than: 24h
    start_at: beginning</pre>
  <p id="9Yvo">В этом примере, даже если файл <code>app-2025-07-15.log</code> соответствует шаблону, он будет пропущен, если не изменялся в течение последних 24 часов.</p>
  <h2 id="parsing-unstructured-text-with-regular-expressions">Разбор неструктурированного текста с помощью регулярных выражений</h2>
  <p id="KVFS">Большинство инфраструктурных логов не записываются в аккуратном формате JSON. Гораздо чаще они представляют собой обычный текст, соответствующий некоторому шаблону, например логи доступа веб-сервера, логи запросов к базе данных или сообщения операционной системы. Такие логи легко читаются человеком, но их сложно анализировать автоматически, пока им не будет придана структура.</p>
  <p id="1cz6">Для решения этой задачи Collector предоставляет оператор <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/pkg/stanza/docs/operators/regex_parser.md" target="_blank">regex_parser. </a>Используя регулярные выражения с именованными группами захвата, вы можете разбить исходную строку лога на осмысленные поля и преобразовать их в структурированные атрибуты.</p>
  <p id="am3f">Например, рассмотрим <a href="https://www.dash0.com/guides/nginx-logs" target="_blank">лог доступа NGINX</a> в в формате <a href="https://en.wikipedia.org/wiki/Common_Log_Format" target="_blank">Common Log Format.</a></p>
  <pre id="WTGs" data-lang="bash">127.0.0.1 - - [28/Sep/2025:20:30:00 +0000] &quot;GET /api/v1/users HTTP/1.1&quot; 200 512
127.0.0.1 - - [28/Sep/2025:20:30:05 +0000] &quot;POST /api/v1/login HTTP/1.1&quot; 401 128</pre>
  <p id="v02g">Для преобразования таких записей в структурированные атрибуты можно настроить <code>regex_parser</code> следующим образом.</p>
  <pre id="zrdF" data-lang="yaml"># otelcol.yaml
receivers:
  filelog:
    include: [/var/log/nginx/access.log]
    start_at: beginning
    operators:
      - type: regex_parser
        # Use named capture groups to extract data
        regex:
          &#x27;^(?P&lt;client_ip&gt;[^ ]+) - - \[(?P&lt;timestamp&gt;[^\]]+)\]
          &quot;(?P&lt;http_method&gt;[A-Z]+) (?P&lt;http_path&gt;[^ &quot;]+)[^&quot;]*&quot;
          (?P&lt;status_code&gt;\d{3}) (?P&lt;response_size&gt;\d+)$&#x27;
        # Parse the extracted timestamp
        timestamp:
          parse_from: attributes.timestamp
          layout: &quot;%d/%b/%Y:%H:%M:%S %z&quot;
        # Map status codes to severities
        severity:
          parse_from: attributes.status_code
          mapping:
            info:
              - min: 200
                max: 399
            warn: 4xx
            error: 5xx</pre>
  <p id="xBFO">В основе данной конфигурации лежит регулярное выражение <code>regex</code> с именованными группами захвата. Каждая группа выделяет определённую часть строки, чтобы анализатор мог преобразовать её в атрибут. Группа <code>client_ip</code> извлекает IP-адрес клиента, <code>timestamp</code> — временную метку в квадратных скобках, <code>http_method</code> и <code>http_path</code> — HTTP-метод и путь запроса, <code>status_code</code> — трёхзначный код ответа, а <code>response_size</code> — размер ответа в байтах.</p>
  <p id="FXk0">После создания этих атрибутов параметр <code>timestamp</code> преобразует строковое значение <code>timestamp</code> в корректное значение даты и времени, а блок <code>severity</code> преобразует коды состояния в соответствующие уровни критичности с помощью явного сопоставления (<code>mapping</code>): ответы <code>2xx</code> и <code>3xx</code> становятся уровнем <code>INFO</code>, ответы <code>4xx</code> — уровнем <code>WARN</code>, а ответы <code>5xx</code> — уровнем <code>ERROR</code>.</p>
  <p id="oTiV">После приема логов доступа с такой конфигурацией вы получите структурированную запись лога, содержащую все важные данные, извлечённые в виде атрибутов.</p>
  <pre id="BDH6" data-lang="bash">LogRecord #0
ObservedTimestamp: 2025-09-28 21:17:42.31729069 +0000 UTC
Timestamp: 2025-09-28 20:30:00 +0000 UTC
SeverityText: 200
SeverityNumber: Info(9)
Body: Str(127.0.0.1 - - [28/Sep/2025:20:30:00 +0000] &quot;GET /api/v1/users HTTP/1.1&quot; 200 512)
Attributes:
     -&gt; status_code: Str(200)
     -&gt; response_size: Str(512)
     -&gt; log.file.name: Str(myapp.log)
     -&gt; client_ip: Str(127.0.0.1)
     -&gt; timestamp: Str(28/Sep/2025:20:30:00 +0000)
     -&gt; http_method: Str(GET)
     -&gt; http_path: Str(/api/v1/users)
Trace ID:
Span ID:
Flags: 0</pre>
  <p id="cprl">Используя одно регулярное выражение и несколько этапов разбора, обычный лог доступа NGINX преобразуется в структурированные данные OpenTelemetry. Следующим логичным шагом является приведение полученных атрибутов в соответствие с <a href="https://opentelemetry.io/docs/specs/semconv/http/" target="_blank">семантическими соглашениями HTTP</a> с помощью процессоров <a href="https://www.dash0.com/guides/opentelemetry-attributes-processor" target="_blank">attributes</a> или <a href="https://www.dash0.com/guides/opentelemetry-transform-processor" target="_blank">transform</a>.</p>
  <h2 id="handling-multiple-log-formats">Обработка нескольких форматов логов</h2>
  <p id="Zfaj">Файлы логов редко имеют единый формат. Например, может потребоваться одновременно принимать <a href="https://www.dash0.com/guides/nginx-logs" target="_blank">логи NGINX</a>, <a href="https://www.dash0.com/guides/postgresql-logs" target="_blank">логи PostgreSQL</a> и приложения, каждый из которых использует собственный формат.</p>
  <p id="DvGz">Наиболее правильный подход — определить отдельный получатель <code>filelog</code> для каждого типа файлов. Каждый получатель использует собственные правила разбора и работает независимо, что делает конфигурацию более понятной и значительно упрощает её отладку.</p>
  <p id="LDzC">Это лучший вариант, когда форматы логов полностью различаются и не имеют ничего общего.</p>
  <pre id="Chcv" data-lang="bash"># otelcol.yaml
receivers:
  # NGINX access logs
  filelog/nginx_access:
    include: [/var/log/nginx/access.log]
    operators:
      - type: regex_parser
        # ... NGINX access log parsing rules

  # NGINX error logs
  filelog/nginx_error:
    include: [/var/log/nginx/error.log]
    operators:
      - type: regex_parser
        # ... NGINX error log parsing rules</pre>
  <p id="svQ9">Однако иногда различия встречаются внутри одного и того же файла.</p>
  <p id="vaWf">Например, большинство строк могут содержать только простые сообщения, тогда как некоторые дополнительно содержат поле <code>trace_id</code>.</p>
  <pre id="h9ol" data-lang="bash">INFO: Application started successfully.
DEBUG: Processing request for trace_id=12345</pre>
  <p id="jgVc">Вместо создания одного большого регулярного выражения, охватывающего все возможные варианты, можно использовать условные операторы с параметром <code>if</code>.</p>
  <pre id="C2yo" data-lang="yaml"># otelcol.yaml
receivers:
  filelog:
    include: [/var/log/app.log]
    operators:
      # Parse the basic structure of every line
      - type: regex_parser
        id: base_parser # a unique ID is required when multiple operators of the same type is being used
        regex: &#x27;^(?P&lt;severity&gt;\w+): (?P&lt;message&gt;.*)$&#x27;

      # Only run this parser when &quot;trace_id&quot; appears
      - type: regex_parser
        id: trace_parser
        if: &#x27;attributes[&quot;message&quot;] matches &quot;trace_id&quot;&#x27;
        parse_from: attributes.message
        regex: &#x27;.*trace_id=(?P&lt;trace_id&gt;\w+).*&#x27;</pre>
  <p id="mThD">Вот что происходит:</p>
  <ul id="V81R">
    <li id="KJnb">Первый анализатор выполняется для каждой строки лога и извлекает поля <code>severity</code> и <code>message</code>.</li>
    <li id="y8Y7">Второй анализатор выполняется только в том случае, если сообщение содержит <code>trace_id</code>, дополняя запись лога этим дополнительным полем.</li>
  </ul>
  <p id="RMzX">Сочетая эти два подхода — использование нескольких получателей для различных форматов и условный разбор для небольших различий внутри одного формата — можно обрабатывать практически любые виды логов, создаваемых вашими системами, не превращая конфигурацию в сложную и трудно поддерживаемую конструкцию.</p>
  <h2 id="handling-stack-traces-and-multiline-logs">Обработка стек-трейсов и многострочных логов</h2>
  <p id="kFUB">Не каждая запись лога помещается в одну строку. Классическим примером является стек-трейс.</p>
  <pre id="NwKf" data-lang="bash">2025-09-28 21:05:42 [ERROR] Unhandled exception: Cannot read property &#x27;foo&#x27; of undefined
TypeError: Cannot read property &#x27;foo&#x27; of undefined
    at Object.&lt;anonymous&gt; (/usr/src/app/index.js:15:18)
    at Module._compile (node:internal/modules/cjs/loader:1254:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
    at Module.load (node:internal/modules/cjs/loader:1117:32)
    at Module._load (node:internal/modules/cjs/loader:958:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:17:47</pre>
  <p id="OgvN">Если отправить такой лог напрямую в Collector, получатель filelog будет рассматривать каждую строку как отдельную запись лога. Это не соответствует ожидаемому поведению, поскольку сообщение об ошибке и каждый фрейм стек-трейса относятся к одной записи.</p>
  <p id="aK12">Если вы контролируете источник логов, лучшим решением будет настроить его таким образом, чтобы исключения и трассировки стека сериализовались в одно строковое поле JSON (например, <code>exception.stacktrace</code>), а не записывались в несколько строк.</p>
  <pre id="l0A1" data-lang="bash">{
  &quot;time&quot;: &quot;2025-09-28 21:05:42&quot;,
  &quot;level&quot;: &quot;ERROR&quot;,
  &quot;message&quot;: &quot;Unhandled exception: Cannot read property &#x27;foo&#x27; of undefined&quot;,
  &quot;exception.stacktrace&quot;: &quot;TypeError: Cannot read property &#x27;foo&#x27; of undefined\n    at Object.&lt;anonymous&gt; (/usr/src/app/index.js:15:18)\n    at Module._compile (node:internal/modules/cjs/loader:1254:14)\n    at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)\n    at Module.load (node:internal/modules/cjs/loader:1117:32)\n    at Module._load (node:internal/modules/cjs/loader:958:12)\n    at Function.executeUserEntryPoint [as runMain] (node:internal/main/run_main:81:12)\n    at node:internal/main/run_main_module:17:47&quot;
}</pre>
  <p id="DOFO">Но если изменить формат логов невозможно, можно использовать приведённую ниже конфигурацию <code>multiline</code>, которая указывает получателю, как объединять несколько строк в одну запись <code>LogRecord</code>.</p>
  <pre id="bANr" data-lang="yaml"># otelcol.yaml
receivers:
  filelog:
    include: [/var/log/myapp/*.log]
    start_at: beginning

    multiline:
      # New entry starts when a line begins with &quot;YYYY-MM-DD HH:MM:SS&quot;
      line_start_pattern: ^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}

    operators:
      - type: regex_parser
        regex: (?P&lt;timestamp&gt;\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[(?P&lt;severity&gt;[A-Za-z]+)\]\s+(?P&lt;message&gt;.+)

        timestamp:
          parse_from: attributes.timestamp
          layout: &quot;%Y-%m-%d %H:%M:%S&quot;

        severity:
          parse_from: attributes.severity</pre>
  <p id="cvmB">В данном случае параметр <code>line_start_pattern</code> служит точкой привязки. Новая запись лога начинается только тогда, когда строка начинается с даты в формате <code>YYYY-MM-DD HH:MM:SS</code>, а любая строка, не соответствующая этому шаблону, присоединяется к предыдущей записи.</p>
  <p id="1B8C">В результате вся трассировка стека — от сообщения об ошибке до каждой строки <code>at ...</code> — будет сохранена как одна структурированная запись лога. Это сохраняет полный контекст события и значительно упрощает анализ и диагностику ошибок.</p>
  <pre id="zlcz" data-lang="bash">LogRecord #0
ObservedTimestamp: 2025-10-07 12:04:26.963143642 +0000 UTC
Timestamp: 2025-09-28 21:05:42 +0000 UTC
SeverityText: ERROR
SeverityNumber: Error(17)
Body: Str(2025-09-28 21:05:42 [ERROR] Unhandled exception: Cannot read property &#x27;foo&#x27; of undefined
TypeError: Cannot read property &#x27;foo&#x27; of undefined
    at Object.&lt;anonymous&gt; (/usr/src/app/index.js:15:18)
    at Module._compile (node:internal/modules/cjs/loader:1254:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
    at Module.load (node:internal/modules/cjs/loader:1117:32)
    at Module._load (node:internal/modules/cjs/loader:958:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:17:47)
Attributes:
     -&gt; log.file.name: Str(/var/log/myapp/app.log)
     -&gt; message: Str(Unhandled exception: Cannot read property &#x27;foo&#x27; of undefined)
     -&gt; timestamp: Str(2025-09-28 21:05:42)
     -&gt; severity: Str(ERROR)
Trace ID:
Span ID:
Flags: 0</pre>
  <h2 id="parsing-metadata-from-file-headers">Разбор метаданных из заголовков файлов</h2>
  <p id="jIPG">Некоторые файлы логов содержат не только записи логов. Они начинаются с заголовка, содержащего важные метаданные обо всём файле. Без этого контекста отдельные записи логов могут быть трудны для интерпретации.</p>
  <p id="4cnp">Такой подход часто используется в пакетных заданиях и процессах экспорта. Например, при ночном запуске процесса выставления счетов может создаваться отдельный файл логов для каждого выполнения. В начале такого файла можно увидеть примерно следующее.</p>
  <pre id="xo66" data-lang="bash"># Job-ID: job-d8e8fca2
# Job-Type: nightly-billing-run
# Executed-By: scheduler-prod-1
# Records-To-Process: 1500
2025-10-08T08:20:00Z INFO: Starting billing run.
2025-10-08T08:21:15Z INFO: Processed account #1.
2025-10-08T08:21:16Z WARN: Account #2 has a negative balance.
. . .</pre>
  <p id="JJ6n">Первые строки содержат информацию о том, какое именно задание сформировало последующие логи. Если проигнорировать эти строки, будет потерян важный контекст. Возможность <code>header</code> решает эту задачу, извлекая метаданные из начала файла и добавляя их ко всем последующим записям логов.</p>
  <p id="VGgF">Для этого определяется небольшой отдельный конвейер, который выполняется только для начального блока строк файла. Необходимо указать регулярное выражение, определяющее, какие строки относятся к заголовку. Затем <code>metadata_operators</code> разбирают эти строки и преобразуют их в атрибуты, которые автоматически добавляются к каждой последующей записи лога.</p>
  <p id="rNTk">Для использования этой функции необходимо выполнить три действия:</p>
  <ol id="98or">
    <li id="EYdP">Включить feature gate <code>filelog.allowHeaderMetadataParsing</code>:</li>
  </ol>
  <pre id="on6K" data-lang="yaml"># docker-compose.yml
services:
  otelcol:
    command:
      [
        --config=/etc/otelcol-contrib/config.yaml,
        --feature-gates=filelog.allowHeaderMetadataParsing,
      ]</pre>
  <ol id="Ukz3">
    <li id="fvZY">Установить параметр <code>start_at: beginning</code>, поскольку заголовок должен считываться с начала файла.</li>
    <li id="pfiG">Настроить как правила <code>header</code>, так и основной конвейер <code>operators</code>.</li>
  </ol>
  <p id="426w">Ниже приведена конфигурация для разбора заголовков из приведённого выше примера файла логов.</p>
  <pre id="ZZp8" data-lang="yaml"># otelcol.yaml
receivers:
  filelog:
    include: [/var/log/jobs/*.log]
    start_at: beginning # required
    header:
      pattern: ^#
      metadata_operators:
        - type: key_value_parser
          delimiter: &quot;: &quot;
          pair_delimiter: &quot;# &quot;</pre>
  <p id="eowy">В данной конфигурации происходит следующее.</p>
  <ul id="z7Wd">
    <li id="VyM3">Параметр <code>pattern: ^#</code>: указывает, что любая строка, начинающаяся с символа <code>#</code>, относится к заголовку. Затем эти строки передаются в конвейер <code>metadata_operators</code>.</li>
    <li id="TzVE">Оператор <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/pkg/stanza/docs/operators/key_value_parser.md" target="_blank">key_value_parser</a> разделяет каждую строку заголовка на ключ и значение, используя символ <code>:</code> в качестве разделителя, а <code>#</code> обозначает начало новой пары «ключ-значение».</li>
  </ul>
  <p id="y3aH">В результате каждая последующая запись лога получает следующие атрибуты:</p>
  <pre id="SQ6k" data-lang="bash">Attributes:
     -&gt; Job-ID: Str(job-d8e8fca2)
     -&gt; Job-Type: Str(nightly-billing-run)
     -&gt; Executed-By: Str(scheduler-prod-1)
     -&gt; Records-To-Process: Str(1500)</pre>
  <p id="EiO6">Как видно, атрибут <code>Job-ID</code> и другие поля заголовка теперь прикрепляются к записи лога, предоставляя ценный контекст, который в противном случае был бы потерян.</p>
  <p id="Onds">После этого их можно обработать дополнительно, преобразовав поля заголовка в <a href="https://www.dash0.com/knowledge/what-are-opentelemetry-resources" target="_blank">атрибуты ресурса</a> и приведя их в соответствие с <a href="https://www.dash0.com/knowledge/otel-semantic-conventions-explainer" target="_blank">семантическими соглашениями OpenTelemetry</a>.</p>
  <h2 id="how-to-avoid-lost-or-duplicate-logs">Как избежать потери или дублирования логов</h2>
  <p id="ASic">При перезапуске Collector процесс приема логов может легко нарушиться, если не сохраняется его состояние. В этом случае существует риск либо повторного приема уже обработанных данных, либо пропуска новых логов. Если используется параметр <code>start_at: beginning</code>, получатель повторно прочитает все файлы логов, что приведёт к массовому дублированию данных. При использовании <code>start_at: end</code> могут быть пропущены записи, появившиеся во время недоступности Collector.</p>
  <p id="8ZG5">Решением этой проблемы является использование контрольных точек (<code>checkpointing</code>). Настроив расширение <code>storage</code>, вы указываете получателю <code>filelog</code> сохранять свою позицию в каждом файле (смещение последней прочитанной записи) на диск и после перезапуска продолжать чтение именно с этого места.</p>
  <p id="FZTo">Наиболее распространённым способом является использование расширения <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/extension/storage/filestorage" target="_blank">расширения file_storage.</a></p>
  <pre id="Yf04" data-lang="yaml"># otelcol.yaml
extensions:
  file_storage:
    directory: /var/otelcol/storage

receivers:
  filelog:
    include: [/var/log/myapp/*.log]
    start_at: beginning
    # Link the receiver to the storage extension
    storage: file_storage

# ... processors, exporters

service:
  # The extension must be enabled in the service section
  extensions: [file_storage]
  pipelines:
    logs:
      receivers: [filelog]
      # ...</pre>
  <p id="H0JH">После включения расширения <code>storage</code> получатель будет выполнять следующие действия.</p>
  <ol id="t4bP">
    <li id="IZGb">При запуске проверять каталог <code>/var/otelcol/storage</code> на наличие сохранённых смещений.</li>
    <li id="cnoM">Продолжать чтение каждого отслеживаемого файла с ранее сохранённого смещения, гарантируя отсутствие потери и дублирования данных.</li>
    <li id="xyqN">Периодически обновлять информацию о своём текущем положении в хранилище.</li>
  </ol>
  <p id="7nuT">Использование контрольных точек обеспечивает устойчивость процесса сбора логов к перезапускам, обновлениям и даже аварийным завершениям работы. Это является одной из важнейших рекомендаций при построении надёжного конвейера приема логов.</p>
  <h3 id="handling-log-delivery-failures-gracefully">Корректная обработка ошибок доставки логов</h3>
  <p id="iH8w">Использование контрольных точек совместно с расширением storage защищает данные при перезапуске Collector, однако существует ещё один распространённый сценарий сбоя: получатель успешно считывает пакет логов, но не может передать его следующему компоненту конвейера.</p>
  <p id="HEBS">Это может произойти, если экспортёр не может установить соединение со своей конечной точкой или если процессор <a href="https://www.dash0.com/guides/opentelemetry-memory-limiter-processor" target="_blank">memory_limiter</a> отклоняет данные. По умолчанию получатель просто отбросит этот пакет логов и перейдёт к следующему, что приведёт к незаметной потере данных.</p>
  <p id="cnjC">Чтобы избежать этого, получатель имеет встроенный механизм повторной отправки неудачно переданных пакетов. Если включить параметр <code>retry_on_failure</code>, получатель приостанавливает работу, ожидает заданный интервал времени и повторяет попытку отправить тот же самый пакет логов. Этот процесс повторяется с использованием экспоненциального увеличения интервала ожидания (<a href="https://en.wikipedia.org/wiki/Exponential_backoff" target="_blank">exponential backoff</a>) до тех пор, пока пакет не будет успешно отправлен либо пока не будет достигнуто значение <code>max_elapsed_time</code>.</p>
  <pre id="I0xF" data-lang="yaml"># otelcol.yaml
receivers:
  filelog:
    retry_on_failure:
      enabled: true
      # Wait 5 seconds after the first failure before the first retry.
      initial_interval: 5s
      # The longest the receiver will wait between retries is 30 seconds.
      max_interval: 30s
      # Give up trying to send a batch after 10 minutes.
      max_elapsed_time: 10m</pre>
  <p id="SwNZ">Сочетая механизм контрольных точек с надёжной политикой повторных попыток, можно построить отказоустойчивый конвейер приема логов из файлов, способный пережить как перезапуски Collector, так и временные сбои или ограничения производительности последующих компонентов.</p>
  <h3 id="deleting-log-files-after-processing">Удаление файлов логов после обработки</h3>
  <p id="G1s1">Некоторые сценарии предполагают однократную обработку файла с последующим его удалением для экономии дискового пространства и предотвращения повторной обработки. Для этого можно использовать параметр <code>delete_after_read</code>, который требует установки <code>start_at: beginning</code>.</p>
  <pre id="222d" data-lang="yaml"># otelcol.yaml
receivers:
  filelog:
    include: [/var/log/archives/*.gz]
    start_at: beginning
    delete_after_read: true</pre>
  <p id="7pIB">Чтобы эта возможность работала, необходимо также включить feature gate <code>filelog.allowFileDeletion</code>.</p>
  <pre id="Mv8j" data-lang="yaml"># docker-compose.yml
services:
  otelcol:
    command:
      [
        --config=/etc/otelcol-contrib/config.yaml,
        --feature-gates=filelog.allowFileDeletion,
      ]</pre>
  <p id="EUA0">Наконец, убедитесь, что файлы могут быть удалены и что служба Collector обладает достаточными правами для их удаления. Если прав недостаточно, в логах появится запись&quot;could not delete&quot;:</p>
  <pre id="7nr8" data-lang="bash">2025-10-08T06:42:03.973Z        error   reader/reader.go:278    could not delete        {&quot;resource&quot;: {&quot;service.instance.id&quot;: &quot;7c0daf0e-e625-4da8-9577-072606dce057&quot;, &quot;service.name&quot;: &quot;otelcol-contrib&quot;, &quot;service.version&quot;: &quot;0.136.0&quot;}, &quot;otelcol.component.id&quot;: &quot;filelog&quot;, &quot;otelcol.component.kind&quot;: &quot;receiver&quot;, &quot;otelcol.signal&quot;: &quot;logs&quot;, &quot;component&quot;: &quot;fileconsumer&quot;, &quot;path&quot;: &quot;/var/log/myapp/app.log&quot;, &quot;filename&quot;: &quot;/var/log/myapp/app.log&quot;}</pre>
  <p id="ZN7t">Будьте осторожны при включении этой настройки, поскольку при его включении файлы безвозвратно удаляются с диска.</p>
  <h2 id="handling-log-rotation-seamlessly">Корректная обработка ротации логов</h2>
  <p id="FJzb">Файлы логов не растут бесконечно. Рано или поздно <a href="https://www.dash0.com/guides/log-rotation-linux-logrotate" target="_blank">выполняется их ротация</a> (по крайней мере, так должно быть). Получатель <code>filelog</code> способен автоматически обрабатывать распространённые схемы ротации, например переименование <code>app.log</code> в <code>app.log.1</code>, без потери данных.</p>
  <p id="xhv3">Вместо того чтобы полагаться исключительно на имя файла, получатель отслеживает каждый файл с помощью уникального отпечатка (fingerprint), вычисленного на основе первых нескольких килобайт его содержимого. Когда происходит ротация, получатель определяет, что исходный файл был переименован, завершает его чтение, после чего начинает работу с новым файлом <code>app.log</code> с самого начала.</p>
  <p id="ANwY">Такое поведение не требует дополнительной настройки и работает сразу после установки, обеспечивая надёжный прием логов даже в средах с частой ротацией файлов.</p>
  <h3 id="reading-compressed-files">Чтение сжатых файлов</h3>
  <p id="Bm20">Многие инструменты ротации логов сжимают старые файлы для экономии дискового пространства, создавая файлы вида <code>access.log.1.gz</code>. Получатель <code>filelog</code> способен работать с такими файлами, автоматически распаковывая их в процессе чтения.</p>
  <p id="H7Fv">Для этого используется параметр <code>compression</code>. Он сообщает получателю, что некоторые или все обнаруженные файлы могут быть сжаты и перед разбором должны быть распакованы.</p>
  <p id="DMtV">Для параметра <code>compression</code> доступны два основных значения:</p>
  <ul id="AP4F">
    <li id="DwEJ"><code>gzip</code>: считать все найденные файлы сжатыми в формате gzip.</li>
    <li id="fzUB"><code>auto</code>: автоматически определять наличие сжатия по расширению файла (в настоящее время поддерживается расширение <code>.gz</code>). Этот вариант является наиболее предпочтительным, если каталог содержит одновременно активные несжатые логи и старые сжатые файлы.</li>
  </ul>
  <p id="If8k">Например, если каталог содержит файлы <code>app.log</code> (активный) и <code>app.log.1.gz</code> (ротированный и сжатый), получатель можно настроить следующим образом.</p>
  <pre id="nzi7" data-lang="yaml"># otelcol.yaml
receivers:
  filelog:
    include: [/var/log/myapp/*]
    start_at: beginning
    # Automatically detect and decompress .gz files
    compression: auto
    operators:
      - type: regex_parser
        # ... your parsing rules</pre>
  <p id="CCeg">При работе со сжатыми логами следует помнить о двух основных вещах.</p>
  <p id="ODEX">Во-первых, получатель предполагает, что сжатые файлы могут увеличиваться только путём добавления новых данных в конец. Если файл полностью переписывается, например исходное содержимое заново архивируется вместе с новыми строками, получатель может обработать его некорректно.</p>
  <p id="XiDM">Во-вторых, необходимо учитывать особенности формирования хэш-сумм файлов. По умолчанию получатель идентифицирует файлы по их сжатому содержимому. В большинстве случаев этого достаточно, однако при переименовании или перемещении файлов это может привести к неоднозначности. Для повышения надёжности идентификации можно включить feature gate <code>filelog.decompressFingerprint</code>. В этом случае отпечаток будет вычисляться по уже распакованному содержимому файла:</p>
  <pre id="l2PI" data-lang="yaml"># docker-compose.yml
services:
  otelcol:
    command:
      [
        --config=/etc/otelcol-contrib/config.yaml,
        --feature-gates=filelog.decompressFingerprint,
      ]</pre>
  <p id="izMZ">Одно важное замечание: если включить эту возможность в уже существующей системе, отпечатки файлов изменятся. Это означает, что ранее обработанные сжатые файлы могут быть повторно приняты получателем.</p>
  <h2 id="performance-tuning-for-high-volume-environments">Настройка производительности для сред с высокой нагрузкой</h2>
  <p id="0ckl">Настройки получателя <code>filelog</code> в OTel Collector по умолчанию оптимизированы для общего использования, однако в производственных средах с сотнями файлов логов или очень высокой интенсивностью поступления данных, скорее всего, потребуется дополнительная настройка производительности.</p>
  <p id="vm2g">По умолчанию получатель пытается одновременно читать все найденные файлы. В системе, создающей тысячи файлов, это может привести к чрезмерной загрузке процессора и быстрому достижению лимита открытых файловых дескрипторов.</p>
  <p id="j4rD">Параметр <code>max_concurrent_files</code> ограничивает количество файлов, читаемых одновременно. По умолчанию его значение равно <code>1024</code>, однако уменьшение этого значения позволяет предотвратить чрезмерную нагрузку на систему.</p>
  <p id="77SC">Другим важным параметром является <code>poll_interval</code>, который определяет, как часто получатель проверяет наличие новых файлов и новых строк логов. По умолчанию используется значение 200ms, благодаря чему новые логи появляются практически мгновенно, однако при этом возрастает нагрузка на процессор, поскольку файловая система сканируется чаще.</p>
  <p id="8TAR">Для менее критичных логов или сред с ограниченными ресурсами увеличение этого значения до <code>1s</code> или даже 5s может стать хорошим компромиссом, поскольку уменьшит нагрузку, связанную с периодическим опросом файловой системы, практически без заметного влияния на наблюдаемость в большинстве сценариев.</p>
  <p id="0LYQ">Наконец, защита от чрезмерно больших записей логов обеспечивается параметром <code>max_log_size</code>. Он определяет максимально допустимый размер записи лога. Если запись превышает этот размер, она будет усечена. По умолчанию используется значение 1MiB, которое подходит для большинства рабочих нагрузок.</p>
  <pre id="lweE" data-lang="yaml"># otelcol.yaml
receivers:
  filelog/k8s_pods:
    include: [/var/log/pods/*/*/*.log]
    max_concurrent_files: 200
    poll_interval: 1s
    max_log_size: 2MiB</pre>
  <h2 id="enforcing-log-file-order">Обеспечение порядка обработки файлов логов</h2>
  <p id="D9aP">В большинстве случаев порядок приема файлов логов не имеет значения. Однако некоторые системы формируют логи в виде последовательности файлов, для которых порядок обработки является критически важным.</p>
  <p id="PmUN">По умолчанию получатель <code>filelog</code> читает все соответствующие шаблону файлы одновременно, поэтому их обработка может происходить не по порядку. Параметр <code>ordering_criteria</code> позволяет принудительно задать строгий порядок чтения файлов.</p>
  <p id="rpVT">Например, имеется следующий набор файлов логов:</p>
  <pre id="gw7R" data-lang="bash">batch-run-001.log
batch-run-002.log
batch-run-003.log</pre>
  <pre id="ulr8" data-lang="yaml"># otelcol.yaml
receivers:
  filelog/batch_logs:
    include: [/var/log/batch-runs/batch-run-*.log]
    start_at: beginning
    ordering_criteria:
      top_n: 1
      # Extract the sequence number from the filename
      regex: batch-run-(?P&lt;seq_num&gt;\d+)\.log
      # Sort files by the sequence number as a number, not a string
      sort_by:
        - regex_key: seq_num
          sort_type: numeric
          ascending: true</pre>
  <p id="TO2M">При такой конфигурации получатель обнаружит все файлы, соответствующие шаблону <code>batch-run-*.log</code>, извлечёт номер последовательности из имени каждого файла и отсортирует файлы по возрастанию этого номера.</p>
  <p id="3nNP">Параметр <code>top_n</code> определяет, сколько файлов будет отслеживаться после применения правил сортировки. При значении <code>top_n: 1</code> будет отслеживаться и приниматься только первый файл (<code>batch-run-001.log</code>).</p>
  <h2 id="filelog-receiver-tips-and-best-practices">Советы и рекомендации по использованию Filelog Receiver</h2>
  <p id="PAmD">При поиске и устранении неисправностей получателя <code>filelog</code> некоторые проблемы возникают особенно часто. Ниже приведены рекомендации по их быстрой диагностике и устранению.</p>
  <h3 id="log-files-are-not-being-watched">Файлы логов не отслеживаются</h3>
  <p id="fwAI">Когда Collector начинает отслеживать файл логов, в его логах появляется запись следующего вида:</p>
  <pre id="QC56" data-lang="bash">2025-10-09T08:47:05.574Z        info    fileconsumer/file.go:261        Started watching file   {...}</pre>
  <p id="O5Im">Если такое сообщение отсутствует либо вместо него появляется запись, приведённая ниже, это означает, что получатель ещё не обнаружил ни одного подходящего файла.</p>
  <pre id="s18x" data-lang="bash">2025-10-09T09:25:20.280Z        warn    fileconsumer/file.go:49 finding files   {..., &quot;error&quot;: &quot;no files match the configured criteria&quot;}</pre>
  <p id="HPix">В первую очередь необходимо проверить параметры <code>include</code>, <code>exclude</code>и <code>exclude_older_than</code>, чтобы убедиться, что заданные шаблоны действительно соответствуют ожидаемым файлам.</p>
  <p id="5XoC">Затем следует убедиться, что процесс Collector обладает правами доступа как к самим файлам, так и к каталогам, в которых они расположены. Отсутствие прав доступа к каталогам является одной из наиболее распространённых причин, по которым файлы не обнаруживаются и не начинают отслеживаться.</p>
  <h3 id="files-are-watched-but-no-log-lines-are-read">Файлы отслеживаются, но строки логов не считываются</h3>
  <p id="BXrx">Если сообщения &quot;Started watching file&quot; присутствуют, но логи не поступают, наиболее вероятной причиной является параметр <code>start_at</code>. По умолчанию используется значение <code>end</code>, которое указывает получателю начинать чтение только с новых строк, добавленных после запуска Collector.</p>
  <p id="r5qK">Если во время тестирования используется уже существующий файл, в который больше ничего не записывается, никаких логов принято не будет. Чтобы прочитать файл полностью с самого начала, необходимо установить параметр <code>start_at</code> в значение <code>beginning</code>.</p>
  <pre id="Igee" data-lang="yaml"># otelcol.yaml
receivers:
  filelog:
    start_at: beginning</pre>
  <p id="zoSo">Это гарантирует, что при первом обнаружении файла получатель обработает всё его существующее содержимое.</p>
  <h3 id="regular-expression-doesnt-match-log-lines">Регулярное выражение не соответствует строкам лога</h3>
  <p id="NfBZ">Если логи не разбираются должным образом, чаще всего проблема связана с регулярным выражением. В этом случае Collector обычно записывает следующую ошибку:</p>
  <pre id="eLWG" data-lang="bash">2025-10-09T09:32:14.949Z        error   helper/transformer.go:154       Failed to process entry {&quot;resource&quot;: {&quot;service.instance.id&quot;: &quot;f8ec2efd-16e9-44ad-9ed2-9f406e46719f&quot;, &quot;service.name&quot;: &quot;otelcol-contrib&quot;, &quot;service.version&quot;: &quot;0.136.0&quot;}, &quot;otelcol.component.id&quot;: &quot;filelog&quot;, &quot;otelcol.component.kind&quot;: &quot;receiver&quot;, &quot;otelcol.signal&quot;: &quot;logs&quot;, &quot;operator_id&quot;: &quot;regex_parser&quot;, &quot;operator_type&quot;: &quot;regex_parser&quot;, &quot;error&quot;: &quot;regex pattern does not match&quot;, &quot;action&quot;: &quot;send&quot;, &quot;entry.timestamp&quot;: &quot;0001-01-01T00:00:00.000Z&quot;, &quot;log.file.name&quot;: &quot;batch-run-001.log&quot;}</pre>
  <p id="VFC9">Перед внесением изменений в конфигурацию Collector рекомендуется проверить регулярное выражение с помощью такого инструмента, как <strong><a href="https://regex101.com/" target="_blank">Regex101</a></strong>. При этом необходимо выбрать режим <strong>Golang</strong>, чтобы поведение регулярного выражения соответствовало механизму регулярных выражений Collector.</p>
  <figure id="BbCJ" class="m_custom">
    <img src="https://www.dash0.com/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Frdn92ihu%2Fproduction%2Fa8b5fe54e1103e35a5b710a73460372a6c378026-2659x1452.png%3Fw%3D1200&w=3840&q=100" width="711.051652892562" />
  </figure>
  <p id="0X3D">Если данная ошибка не появляется, но регулярное выражение по-прежнему не работает, следует проверить, не установлен ли <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/pkg/stanza/docs/types/on_error.md" target="_blank">параметр <code>on_error</code></a> в один из режимов <code>_quiet</code>. Эти значения подавляют сообщения об ошибках операторов, если уровень логирования Collector не установлен в значение <code>DEBUG</code>.</p>
  <p id="dvEE">Наиболее распространёнными причинами несоответствия регулярного выражения являются невидимые пробелы или символы табуляции, отсутствие якорей (<code>^</code> или <code>$</code>), неправильное экранирование символов либо небольшие различия между фактическим форматом лога и шаблоном регулярного выражения. Прежде чем искать более сложные причины, следует внимательно проверить именно эти моменты.</p>
  <h3 id="logs-are-duplicated-after-restart">После перезапуска появляются дублирующиеся логи</h3>
  <p id="hnFJ">Если после перезапуска Collector появляются дублирующиеся записи логов, это обычно означает, что получатель не запоминает место, на котором остановился. Для устранения этой проблемы необходимо включить расширение <code>storage</code>, чтобы получатель <code>filelog</code> мог сохранять контрольные точки своего положения в каждом файле.</p>
  <p id="aJlh">Это позволит получателю продолжить чтение точно с того места, где оно было остановлено, предотвращая как потерю данных, так и их дублирование. Без использования расширения storage после каждого перезапуска получатель будет заново считывать все файлы целиком.</p>
  <h2 id="final-thoughts">Заключительные мысли</h2>
  <p id="seHw">Получатель <code>filelog</code> в OpenTelemetry является важным связующим звеном между традиционным ведением логов в файлы (зачастую содержащие неструктурированные данные) и современным миром структурированной наблюдаемости.</p>
  <p id="GSJR">Освоив его основные принципы — обнаружение файлов, разбор данных с помощью операторов и использование контрольных точек — вы сможете построить надёжный конвейер приема логов для любого сервиса, записывающего логи в файл.</p>
  <p id="tnIi">После преобразования необработанных текстовых логов в хорошо структурированные данные OpenTelemetry становятся доступны все возможности экосистемы наблюдаемости. Вы сможете обогащать, фильтровать и маршрутизировать их в любую систему, поддерживающую протокол <a href="https://www.dash0.com/knowledge/opentelemetry-protocol-otlp" target="_blank">OTLP</a>.</p>
  <figure id="n5iL" class="m_custom">
    <img src="https://www.dash0.com/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Frdn92ihu%2Fproduction%2F33cbc92813eb554a7eb0a7bf281c109a4147a259-3382x1929.png%3Fw%3D1200&w=3840&q=100" width="695.0668740279938" />
  </figure>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="ewav">Подписывайтесь на телеграм-канал <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ</a>, там еще больше полезной информации о мониторинге!</p>
  </section>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@monitorim_it/airbnb-observability-migration</guid><link>https://teletype.in/@monitorim_it/airbnb-observability-migration?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it</link><comments>https://teletype.in/@monitorim_it/airbnb-observability-migration?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it#comments</comments><dc:creator>monitorim_it</dc:creator><title>От вендоров к авангарду: ценный опыт Airbnb в области обеспечения наблюдаемости</title><pubDate>Thu, 09 Jul 2026 11:09:18 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/74/aa/74aaa83f-054e-4240-abeb-4b5ce02c0424.png"></media:content><description><![CDATA[<img src="https://miro.medium.com/v2/resize:fit:875/1*-SS6XenH2zP8ON5LtPEcEA.png"></img>Это перевод оригинальной статьи From vendors to vanguard: Airbnb’s hard-won lessons in observability ownership.]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="Bsg2">Это перевод оригинальной статьи <a href="https://medium.com/airbnb-engineering/from-vendors-to-vanguard-airbnbs-hard-won-lessons-in-observability-ownership-3811bf6c1ac3" target="_blank">From vendors to vanguard: Airbnb’s hard-won lessons in observability ownership</a>.</p>
  </section>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="XUoD">Перевод сделан специально для телеграм-канала <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ.</a> Подписывайтесь! Там еще больше полезных постов о мониторинге.</p>
  </section>
  <p id="cc9d"><em>Как сложная, масштабная миграция на внутреннюю платформу наблюдаемости привела к более совершенным инструментам, согласованным данным и фундаментальной перестройке опыта разработчиков.</em></p>
  <p id="4858">Наблюдаемость — функция, обеспечивающая видимость производительности и надежности приложений с использованием метрик, логов и трассировок — является одним из наиболее важных инструментов группы инфраструктуры в любой компании. Без надежной, экономически эффективной и удобной платформы наблюдаемости вы ограничиваете способность организации предоставлять инженерам возможность оценивать, поддерживать и улучшать надежность своих приложений. </p>
  <p id="8ce5">Как и многие другие компании, Airbnb изначально передала свои потребности в наблюдаемости внешним вендорам. Но по мере развития компании наши потребности стали отличаться от типичных стимулов вендоров. Вендоры взимают плату за объем обрабатываемых данных, поэтому затраты Airbnb росли, но большее количество данных не приводит автоматически к более быстрому получению аналитических данных или сокращению среднего времени обнаружения (MTTD) или устранения (MTTR). Кроме того, из-за отсутствия обратной связи в процессе использования данных наблюдаемости, наша способность улучшать рабочие процессы мониторинга наших клиентов или оптимизировать затраты на наблюдаемость была существенно ограничена.</p>
  <p id="1975">Для достижения поставленных целей Airbnb приступила к сложной миграции, полностью перестроив всю свою инфраструктуру метрик. Этот непростой процесс включал замену наших систем инструментирования, сбора, хранения и визуализации данных в процессе перехода от сторонней платформы мониторинга, управляемой вендором, к собственному решению, построенному на основе технологии с открытым исходным кодом на базе Prometheus.</p>
  <p id="6561">Этот переход позволил нам получить полный контроль над сбором, хранением и запросами метрик. В рамках этого проекта мы успешно использовали наши инструменты автоматизации для миграции огромного объема данных через 1 000 сервисов: 300 миллионов временных рядов, 3 100 дашбордов и более 300 000 алертов. Хотя проект такого масштаба не реализуется без серьезных оснований, ограничения на то, что мы могли делать с нашими собственными данными, практически не оставили нам альтернативы.</p>
  <p id="b954">Хотя универсального руководства по миграции не существует, мы извлекли некоторые уроки из этой конкретной миграции, которые, как мы считаем, будут полезны другим. Сначала мы опишем упрощенный, но в конечном итоге ошибочный подход к миграции, а затем подробно рассмотрим успешную стратегию, которую мы в итоге применили. Наши планы по этой миграции, начатые пять лет назад, значительно изменились по мере их реализации. Этот путь предоставил нам ценные, с трудом полученные уроки, дающие четкое представление о лучших практиках и подводных камнях при выполнении масштабной миграции.</p>
  <h2 id="7fbf">Миграция v1</h2>
  <h3 id="0825">Восхождение на Эверест в первый день</h3>
  <p id="66ab">При масштабной миграции общая стратегия заключается в том, чтобы в первую очередь сосредоточиться на самом крупном и значимом сервисе. Решение самой сложной задачи на начальном этапе позволяет проектной команде продемонстрировать немедленный и существенный прогресс, укрепить уверенность в новой инфраструктуре и доказать жизнеспособность стратегии миграции всей команде и заинтересованным сторонам. Эта очевидная победа задает тон всему дальнейшему развертыванию.</p>
  <p id="e9a2">С учетом этого подхода на первый взгляд имеет смысл выбрать сложный сервис — например, такой, который имеет разреженные данные или генерирует метрики способом, нестандартным для новой системы — и сделать его первым кандидатом для миграции на новую систему. Это позволило бы выявить различия между двумя системами и дать возможность разработать решения, имитирующие поведение старой системы, гарантируя, что дашборды и алерты будут выглядеть одинаково.</p>
  <p id="8849">При следовании этой стратегии команда потратит много времени на запуск первого сервиса. При этом возникают ложные срабатывания, несоответствия дашбордов между системами и необходимость масштабного обучения пользователей — и все это лишь для того, чтобы доказать всей компании, что миграция в целом оправдана.</p>
  <h3 id="58ec">Тот же багаж, но воздух разреженный.</h3>
  <p id="c8b7">При миграции систем плавный переход для ваших пользователей имеет первостепенное значение, поскольку для них и так достаточно изменений. Даже если устаревшая система привела к некорректным или неточным запросам, которые можно исправить, важно подходить к исправлению стратегически. Например, не стоит пытаться одновременно исправить все проблемы в существующих шаблонах дашбордов и алертов.</p>
  <p id="f619">Вместо этого следует сначала полностью перенести систему. После того как все пользователи будут переведены и централизованы, можно будет заняться улучшениями дашбордов и алертов во втором этапе изменений. Пользователи хотят в первую очередь завершить миграцию. Если переход на новую систему уже создает сложности, не следует усугублять ситуацию дополнительными изменениями функциональности их запросов.</p>
  <h3 id="172a">Документация объясняет все — для тех, кто готов ее изучать</h3>
  <p id="2f05">Для облегчения внедрения новой системы клиентами, которая, вероятно, будет включать в себя масштабные изменения, крайне важно предоставить исчерпывающую документацию по новому языку запросов и интерфейсам. Это должно сочетаться с обширным обучением для обеспечения компетентности пользователей.</p>
  <p id="b49d">Даже для редких пользователей знание языка запросов наблюдаемости важно — аналогично любому языку программирования. Эти знания, подкрепленные документацией и обучающими материалами, особенно ценны для быстрой диагностики проблем во время инцидентов.</p>
  <h2 id="9827">Миграция v2</h2>
  <h3 id="0ddf">Начинайте с достижимой цели.</h3>
  <p id="44af">Выбор первоначального сервиса для миграции, который тесно связан с вашей целевой системой, может устранить большую часть отвлекающих факторов и лишней информации, обычно сопровождающих сложную миграцию. Когда ваша первоначальная цель — доказать (вашей команде и компании в целом), что миграция оправдана (с точки зрения снижения затрат, расширения возможностей и т. д.), крайне важно сосредоточиться на высокоэффективном сервисе.</p>
  <p id="2091">Это позволит вашей команде:</p>
  <ul id="Ze1H">
    <li id="ce90">Проверить, что система хранения выдерживает необходимую нагрузку, и наблюдать ее поведение во время инцидентов</li>
    <li id="3574">Предоставить инструменты для переноса существующих дашбордов и алертов в новую систему.</li>
    <li id="041b">Инвестировать в документацию, обучение и поддержку пользователей.</li>
    <li id="f986">Предложить новый инструмент визуализации небольшой группе пользователей, дав им время адаптироваться к новому интерфейсу, пока ваша команда собирает обратную связь до масштабного развертывания.</li>
  </ul>
  <p id="6fef">Успешная миграция такого менее сложного сервиса достигает двух целей: демонстрирует техническую и операционную реализуемость проекта и помогает подтвердить целесообразность стратегии миграции для всей компании.</p>
  <p id="8727">Смена вендора часто требует нового инструмента визуализации, поэтому это также хорошая возможность выявить любые различия в пользовательском интерфейсе на раннем этапе. Обратная связь, собранная вашей командой от этой первой группы пользователей, окажется бесценной при внедрении нового инструментария в остальной части организации.</p>
  <h3 id="dc34">Мигрируйте смысл запроса</h3>
  <p id="69d7">В любой долго существующей системе со временем накапливается большое разнообразие способов измерения задержек, ошибок и других сигналов. В демократизированной среде наблюдаемости это практически неизбежно: некоторые метрики будут вычисляться некорректно — например, усреднение вместо p95 или суммирование общей задержки. Пользователи могут легко создавать некорректные запросы, и без ограничений их результаты начинают восприниматься как истина.</p>
  <p id="cf0c">Миграция позволяет вашей команде снова включиться в процесс. Это возможность исправить эти шаблоны — не путем их удаления или сброса, а путем понимания первоначального замысла и сопоставления его с предпочтительным, стандартизированным запросом.</p>
  <figure id="JIkD" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*-SS6XenH2zP8ON5LtPEcEA.png" width="700" />
  </figure>
  <p id="41e3">Учитывая это, мы адаптировали нашу систему перевода, чтобы она фокусировалась на намерениях, а не на прямом переводе запросов. Например, если запрос включал вычисление p95, мы игнорировали любые дополнительные агрегации и вместо этого возвращали канонический запрос гистограммы для этой метрики.</p>
  <p id="a4ff">На практике это потребовало внедрения специального механизма метаданных. В Prometheus типы метрик часто определяются на основе соглашений об именовании (например, суффикс _total указывает на счетчик). Но поскольку мы решили сохранить существующие имена метрик, чтобы избежать расхождений между кодом и наблюдаемыми данными, одного лишь именования оказалось недостаточно.</p>
  <p id="a73d">Для решения этой проблемы мы встроили механизм метаданных непосредственно в слой перевода. Он периодически сканирует все известные метрики и использует внутреннюю метку ( <em>otel_metric_type</em> ) для построения и поддержания надежного сопоставления метрики с типом.</p>
  <p id="0d8f">Благодаря этому изменению мы получили значительно большую уверенность в том, что команды имеют точную и согласованную видимость производительности своих приложений.</p>
  <h3 id="083a">Освоение нового языка запросов может быть непростой задачей.</h3>
  <p id="7d3b">Вполне понятно, что пользователям потребуется время, чтобы перестроить привычки, сформированные в старой системе. В некоторых случаях новая система может быть менее удобной для составления запросов.</p>
  <p id="44c7">В процессе миграции мы решили проблему незнания пользователями нового языка запросов, используя наши инструменты перевода, которые могли сопоставлять заданный запрос с новой системой, помогая запустить самостоятельную миграцию. Однако после миграции мы не хотели продолжать предоставлять промежуточный вариант между старым и новым языком, поскольку это замедлило бы или помешало бы формированию новых навыков.</p>
  <p id="5e7a">Используя PromQL в качестве языка запросов для нашей новой системы, мы получаем преимущества зрелой экосистемы и хорошо документированного языка запросов, понятного как людям, так и современным LLM.</p>
  <p id="8382">Мы дополнительно снизили порог входа, объединив это с внутренними навыками в области инструментов искусственного интеллекта, которые предоставляют обширные семантические метаданные о каждой метрике — например, является ли она счетчиком или гистограммой, или в каких единицах она представлена. Вместе эти возможности позволяют агентам генерировать корректный PromQL с гораздо меньшими усилиями вручную, что позволяет разработчикам быстрее осваивать необходимые знания и эффективнее исследовать метрики. Это стало критически важным для отладки инцидентов.</p>
  <figure id="llDz" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*PuJrwIdLl83tZJw7Z8LVBw.png" width="700" />
  </figure>
  <p id="c145">В результате пользователи теперь могут выполнять типовые задачи, такие как диагностика инцидентов или создание дашбордов, за считанные минуты, что значительно сокращает время, необходимое для выполнения этих задач ранее.</p>
  <h3 id="70d8">Сейчас самое время исправить устаревшие паттерны</h3>
  <p id="f938">Со временем старые системы могут прийти в упадок, а ограничения функциональности могут снизить производительность. Таким образом, преимущество любой масштабной миграции заключается в возможности остановиться, проанализировать, как используется ваш продукт, и выявить дополнительные области для улучшения.</p>
  <p id="f818">Хотя поначалу мы опасались внедрения новых концепций, по мере того, как наши первые пользователи начали использовать новую систему, нам стало ясно, что существующая система оповещений нуждается в замене. Мы решили продолжить работу команды, Reliability Experience, над улучшением системы создания алертов и продвигать их разработку как предпочтительный инструмент для создания оповещений.</p>
  <p id="1b5d">Новый подход заменил устаревшую систему на новый интерфейс создания оповещений, который рассматривает каждое оповещение как этап разработки, а не как слабо документированный конфигурационный файл. Оповещения создаются как код, с автозаполнением и подсказками в стиле конструктора для написания запросов, встроенным тестированием для понимания того, когда оповещение сработало бы в прошлом, и сравнением изменений для наглядной оценки их влияния — и всё это до развертывания.</p>
  <p id="bdd5">Это стало отклонением от нашей первоначальной стратегии, предполагавшей сохранение дашбордов и алертов без изменений после первоначальной масштабной миграции. Однако мы обнаружили, что успех миграции значительно повысился благодаря улучшенной системе оповещений. Это изменение помогло клиентам осознать неотъемлемую ценность миграции, выходящую за рамки простого контроля и снижения затрат, изменив восприятие проекта. Кроме того, централизованный характер новых оповещений упростил процесс, сократив общие затраты на миграцию. Централизованный подход сократил общий объем работы при миграции.</p>
  <h2 id="d74e">Заключение</h2>
  <p id="32e3">В конечном итоге эта миграция стала чем-то гораздо большим, чем просто смена системы хранения. То, что начиналось как попытка «просто перенести систему», переросло в переосмысление нашего подхода к наблюдаемости, управлению и удобству работы разработчиков.</p>
  <p id="0ddb">Благодаря контролю над полным жизненным циклом наших метрик, от генерации до запросов, мы существенно повысили уровень надежности. Мы перешли от хрупких ручных запросов к более осмысленным паттернам, лучшим инструментам и более надежным оповещениям и дашбордам. Инвестиции в метаданные, инструменты искусственного интеллекта и современные рабочие процессы разработки окупились не только с точки зрения корректности, но и с точки зрения скорости, с которой инженеры могут исследовать возможности и реагировать на инциденты.</p>
  <p id="f0e8">Не менее важно, что эта миграция изменила наше взаимодействие с внутренними пользователями. Вместо передачи ключевых аспектов наблюдаемости внешним вендорам мы были вынуждены глубоко погрузиться в то, как команды действительно используют метрики под реальным операционным давлением. Этот контур обратной связи оказался чрезвычайно ценным и продолжает определять направления наших дальнейших инвестиций.</p>
  <p id="27ae">Автоматизация сыграла ключевую роль в обеспечении возможности этой миграции, но мы поняли, что одной автоматизации недостаточно. Слепое копирование существующих шаблонов (особенно ошибочных) просто переносит технический долг в новую систему. Наиболее значимые результаты были достигнуты тогда, когда мы осознанно отказывались от совместимости в пользу более качественных решений по умолчанию, даже если это вызывало краткосрочные сложности.</p>
  <p id="d96a">В итоге эта работа сформировала воспроизводимый подход. Для будущих миграций мы четко понимаем, чем именно хотим владеть: опытом и интерфейсами, с помощью которых инженеры пишут, читают и осмысляют свои данные. Движки хранения и backend-системы могут меняться, но ответственность за то, чтобы наблюдаемость оставалась удобной, надежной и развиваемой, должна оставаться в наших руках.</p>
  <p id="5266">Даже для команд, которые не стоят перед необходимостью срочной миграции, уже сейчас есть шаги, позволяющие снизить будущие сложности. Наиболее важный из них — это контроль над слоем взаимодействия: frontend, инструменты разработки и workflow, которые инженеры используют для исследования и применения данных наблюдаемости. В ходе нашей миграции нам пришлось не только сменить бэкенд-систему, но и перевести все команды на новый инструмент визуализации и новый фреймворк алертов, что существенно увеличило стоимость перехода. Если бы эти точки взаимодействия уже находились под нашим контролем, переход бэкенд был бы значительно проще и более постепенным.</p>
  <p id="9fc2">Несмотря на различия в ограничениях разных организаций, мы надеемся, что эти выводы помогут другим рассматривать масштабные миграции не как вынужденную нагрузку, а как редкую возможность существенно улучшить системы и, как следствие, уровень наблюдаемости.</p>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="8bKg">Подписывайтесь на телеграм-канал <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ</a>, там еще больше полезной информации о мониторинге!</p>
  </section>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@monitorim_it/be-ready-for-k8s-monitoring</guid><link>https://teletype.in/@monitorim_it/be-ready-for-k8s-monitoring?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it</link><comments>https://teletype.in/@monitorim_it/be-ready-for-k8s-monitoring?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it#comments</comments><dc:creator>monitorim_it</dc:creator><title>Вы не готовы к мониторингу Kubernetes, пока не поймете эти концепции</title><pubDate>Thu, 09 Jul 2026 11:04:36 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/e2/b9/e2b94451-2daf-4bd7-b021-ce15251664f8.png"></media:content><description><![CDATA[<img src="https://miro.medium.com/v2/resize:fit:875/0*plHi3DnjJKSSgqaa.png"></img>Это перевод оригинальной статьи You’re Not Ready for Kubernetes Observability Until You Understand These Concepts.]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="yuqC">Это перевод оригинальной статьи <a href="https://medium.com/@akhilesh-mishra/youre-not-ready-for-kubernetes-observability-until-you-understand-these-concepts-5acc6390cfa5" target="_blank">You’re Not Ready for Kubernetes Observability Until You Understand These Concepts</a>.</p>
  </section>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="2fK5">Перевод сделан специально для телеграм-канала <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ.</a> Подписывайтесь! Там еще больше полезных постов о мониторинге.</p>
  </section>
  <h3 id="2308">Реальная история обеспечения наблюдаемости в Kubernetes (которая действительно работает в продакшене)</h3>
  <p id="8b62"><strong>Большинство людей начинают работать с Prometheus и Grafana, не понимая, какие задачи они решают. Вот что вам нужно знать в первую очередь.</strong></p>
  <p id="62dd">Данная реализация взята из моего недавнего курса по DevOps, где мы создали реальную систему мониторинга для микросервисов, работающих на EKS.</p>
  <p id="ebd0">Это не базовая версия из учебного пособия, а полная боевая архитектура со всеми решениями и компромиссами, которые с ней связаны.</p>
  <figure id="Ex4G" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/0*plHi3DnjJKSSgqaa.png" width="700" />
  </figure>
  <p id="bb6d">Позвольте мне пошагово объяснить вам основные понятия, закономерности и причины, по которым всё работает именно так.</p>
  <h2 id="05a0">Две разные проблемы, два разных решения</h2>
  <p id="195f">Когда вы начинаете работать с мониторингом Kubernetes, первое, что вам нужно понять, это то, что вы решаете две совершенно разные задачи.</p>
  <p id="118d">Логи событий показывают, что произошло. Кто-то отправил запрос. Произошла ошибка. Не удалось установить соединение с базой данных. Это события, истории, которые ваше приложение рассказывает во время работы.</p>
  <p id="d340">Метрики показывают, насколько хорошо всё работает. Ваша задержка составляет 200 миллисекунд. Загрузка CPU — 75%. За последнюю минуту было обработано 1000 запросов. Эти показатели отражают состояние и производительность системы.</p>
  <p id="468c">Это принципиально разные типы данных. Для их сбора требуются разные методы, разные решения для хранения и разные шаблоны запросов. Попытка использовать один и тот же инструмент для обоих случаев приводит к путанице.</p>
  <h2 id="e25a">Как на самом деле работает сбор логов</h2>
  <p id="52df">Ваше приложение работает в поде. Оно записывает логи куда-то. Ваша задача — забрать эти логи и сохранить их в месте, где вы действительно сможете их использовать.</p>
  <p id="638d">Существует два основных сценария, зависящих от типа выполняемой рабочей нагрузки.</p>
  <h2 id="931f">Простая модель для управляемых вычислительных ресурсов</h2>
  <p id="6e04">Если вы используете, например, AWS Fargate, где вычислительный уровень управляется автоматически, сбор логов не представляет сложности.</p>
  <p id="SrYq">Вы устанавливаете аддон в кластер. Это может быть CloudWatch Observability add-on. Может быть OpenTelemetry или Fluentd. Эти аддоны разворачивают коллекторы, которые автоматически обнаруживают ваши pod’ы и начинают забирать логи.</p>
  <p id="0671">Ключевое слово — «pull» (запрос). Сборщики подключаются к логам вашего пода и передают их в пункт назначения. Обычно первым делом используется CloudWatch.</p>
  <p id="62e2">Вашему приложению ничего из этого знать не нужно. Оно просто записывает данные в стандартный вывод и стандартный поток ошибок, как обычно. Всё остальное обрабатывает коллектор.</p>
  <h2 id="6b47">Паттерн-sidecar для сложных приложений</h2>
  <p id="5951">Для рабочих нагрузок, которым требуется постоянное хранилище или есть специфические требования к логированию, используется так называемый sidecar-паттерн.</p>
  <p id="ba4b">В вашем поде работают два контейнера вместо одного: основной контейнер приложения и контейнер для ведения логов. Оба контейнера монтируют один и тот же том.</p>
  <h2 id="03b6">Вот как это происходит</h2>
  <ul id="lznd">
    <li id="402e">Ваше приложение записывает логи в файл на общем томе.</li>
    <li id="bfaa">В контейнере-сайдкаре работает что-то вроде Fluentd, которое постоянно считывает этот лог-файл и пересылает его содержимое в пункт назначения.</li>
  </ul>
  <p id="e294">Мы делаем это таким образом, чтобы обеспечить разделение ответственности. Код вашего приложения остаётся простым. Он просто записывает данные в файл. Вся сложность форматирования, батчинга, повторных попыток при неудачной отправке и маршрутизации в разные назначения находится в sidecar’е.</p>
  <p id="aed6">Если вам нужно изменить место хранения логов или их формат, вы изменяете конфигурацию sidecar-контейнера. Код вашего приложения при этом остаётся неизменным.</p>
  <h2 id="c10e">Почему одного CloudWatch недостаточно при масштабировании</h2>
  <p id="669e">Большинство начинают с отправки всех данных в CloudWatch. Он интегрирован с AWS. Он надежен. Он хорошо подходит для небольших систем.</p>
  <p id="5361">Однако при генерации значительного объема логов у CloudWatch есть ограничения.</p>
  <ul id="JUyS">
    <li id="45ab">Выполнение запросов замедляется.</li>
    <li id="8bd9">Когда у вас терабайты логов, распределенных по нескольким группам, поиск конкретных ошибок занимает слишком много времени.</li>
    <li id="3902">Интерфейс запросов не предназначен для сложных поисков по огромным массивам данных.</li>
    <li id="94db">Затраты накапливаются. CloudWatch взимает плату за прием данных, хранение и запросы. В больших масштабах эти цифры становятся неприятными.</li>
    <li id="6a09">Возможности интеграции ограничены. Если вы хотите использовать передовые аналитические инструменты или машинное обучение для обработки логов, извлечение данных из CloudWatch будет проблематичным.</li>
  </ul>
  <p id="b506">Именно поэтому в production-конфигурациях добавляют дополнительные слои.</p>
  <h2 id="4b76">Паттерн корпоративного пайплайна логов</h2>
  <p id="3990">В нашей bootcamp-конфигурации логи проходят через несколько этапов. Каждый этап выполняет свою конкретную задачу.</p>
  <p id="8b9d">CloudWatch по-прежнему является точкой входа. Он надежен, и именно через него ваши сервисы AWS ожидают отправлять данные. Но это только первый этап.</p>
  <p id="004d">Из CloudWatch логи передаются в функцию Lambda. Это необязательный, но полезный механизм. Lambda стандартизирует форматы логов, добавляет метаданные и приводит в порядок временные метки. Разные сервисы логируют по-разному. Этот этап делает всё единообразным.</p>
  <p id="c5b5">Далее следует Kinesis Firehose. Это ваш стриминговый буфер. Вместо записи тысяч отдельных лог-записей в конечное хранилище, Kinesis объединяет их в батчи.</p>
  <p id="575d">Мы настроили буфер на 20 минут. Он собирает логи, а затем сбрасывает всё одним эффективным батчем.</p>
  <p id="de03">Буферизация необходима, потому что конечный пункт назначения не может обрабатывать тысячи отдельных операций записи в секунду. Пакетная обработка снижает нагрузку, уменьшает затраты и фактически повышает надежность.</p>
  <p id="835d">Конечная цель — OpenSearch. Именно здесь по-настоящему проявляется возможность мониторинга в масштабах.</p>
  <h2 id="1152">Что отличает OpenSearch</h2>
  <p id="3037">OpenSearch создан для одной цели: быстрого полнотекстового поиска по огромным массивам данных.</p>
  <p id="1e8d">Когда у вас петабайты логов и вам нужно найти каждое вхождение конкретной ошибки в сотнях сервисов, OpenSearch справится с этим за секунды. Когда нужно агрегировать частоту ошибок по сервисам и времени. Когда нужно выполнять сложные запросы, которые в CloudWatch просто завершились бы по таймауту. С этим справляется OpenSearch.</p>
  <p id="2483">Это обходится дороже, чем просто использование CloudWatch. Но когда нескольким командам необходимо одновременно запрашивать логи, когда важна скорость и когда требуется анализ в реальном времени, затраты оправданы.</p>
  <p id="3467">В нашей системе мы храним данные за 7 дней в OpenSearch. Это актуальные данные для активного расследования и мониторинга.</p>
  <p id="3a9b">Все данные также резервно копируются в S3 через Kinesis Firehose. S3 — это недорогое хранилище для долговременного хранения. Мы храним там логи за несколько лет в целях соблюдения нормативных требований. Мы нечасто обращаемся к S3, но если нам нужны исторические данные для расследования, они там есть.</p>
  <p id="51d8">Таким образом, стратегия хранения данных такова: 7 дней в OpenSearch для быстрых запросов, 30 дней в CloudWatch в качестве промежуточного варианта, и годы в S3 для соответствия требованиям.</p>
  <p id="2b3c">Каждый этап имеет разные характеристики стоимости и производительности. Вы выбираете, исходя из своих потребностей.</p>
  <h2 id="1571">Чем отличается сбор метрик</h2>
  <p id="d1f9">Пока логи проходят через этот пайплайн, метрики работают в совершенно другой системе.</p>
  <p id="6bb9">Принципиальное различие заключается в структуре данных. Логи — это неструктурированный текст. Метрики — это числа с подписями. Логи рассказывают истории. Метрики измеряют показатели.</p>
  <p id="286c">Prometheus — это стандарт для метрик Kubernetes. Он использует pull-модель, что выглядит непривычно для большинства людей.</p>
  <p id="8538">Вместо того чтобы ваше приложение отправляло метрики на центральный сервер, Prometheus по расписанию собирает данные с конечных точек. Каждые N секунд Prometheus обращается к конечной точке метрик вашего приложения и получает последние значения.</p>
  <p id="0dde">Почему используется модель «pull» вместо «push»? Это обеспечивает контроль и надежность. Prometheus контролирует расписание сбора данных. Если ваше приложение аварийно завершает работу, Prometheus узнает об этом немедленно, поскольку сбор данных завершается неудачей. При использовании модели «push» аварийно завершившееся приложение просто перестает отправлять данные, и вы можете этого не заметить.</p>
  <h2 id="050c">Что на самом деле делает Prometheus </h2>
  <p id="cf06">Люди часто путают, что такое Prometheus. Это не система долгосрочного хранения. Это база данных временных рядов, оптимизированная под свежие данные и быстрые запросы.</p>
  <p id="0cb2">Prometheus хранит метрики в оперативной памяти с использованием дискового кэширования. Вы можете настроить срок хранения, обычно 15 дней. После этого данные устаревают. Если вам требуется более длительный срок хранения, вы можете использовать расширения, такие как Thanos, которые архивируют данные в объектное хранилище.</p>
  <p id="bfe0">Prometheus также ничего не визуализирует. Это не его задача. Он собирает, хранит и позволяет запрашивать метрики. Визуализация — это задача Grafana.</p>
  <h2 id="85ee">Как приложения предоставляют доступ к метрикам</h2>
  <p id="b98e">Для того чтобы Prometheus мог собирать метрики из вашего приложения, ваше приложение должно предоставлять конечную точку для сбора метрик.</p>
  <p id="3a0e">Для каждого основного языка программирования существуют клиентские библиотеки Prometheus. В Python есть prometheus_client. В Go есть официальный client_golang. В Java есть несколько вариантов.</p>
  <p id="45c4">Схема всегда одна и та же. Импортируйте библиотеку. Определите свои метрики в виде счетчиков, индикаторов или гистограмм. Увеличивайте или устанавливайте значения в своем коде. Запускайте HTTP-сервер, который отдаёт эти метрики.</p>
  <p id="5cb1">В результате у приложения появляется эндпоинт вроде <code>/metrics</code>, который возвращает текущие значения в формате Prometheus. Prometheus опрашивает этот эндпоинт и сохраняет данные.</p>
  <p id="43ea">Ключевая идея заключается в том, что разработчики используют инструменты мониторинга для своего кода. Они сами решают, что стоит измерять. Количество запросов. Частота ошибок. Время ответа. Длина очереди. Размеры пулов подключений к базе данных. Всё, что важно для понимания работы приложения.</p>
  <h2 id="9e49">ServiceMonitor сообщает Prometheus, что нужно опрашивать</h2>
  <p id="4d18">Prometheus должен знать, какие эндпоинты опрашивать и как часто. В Kubernetes это описывается с помощью ресурсов ServiceMonitor.</p>
  <p id="2499">В ServiceMonitor указано: «Найти все поды с этими метками, собирать их метрики с порта каждые 30 секунд по этому пути».</p>
  <p id="28af">Для каждого приложения создается отдельный ServiceMonitor. Prometheus отслеживает эти ресурсы и автоматически настраивает сбор данных на их основе.</p>
  <p id="e3e1">Это динамический процесс. Когда запускаются новые поды с совпадающими метками, Prometheus автоматически начинает их опрашивать. Когда pod’ы завершают работу, Prometheus перестаёт это делать. Ручная настройка не требуется.</p>
  <p id="d73e">30 секунд — это стандартный интервал сбора данных. Более частый интервал обеспечивает лучшее разрешение, но потребляет больше ресурсов. Менее частый интервал снижает нагрузку, но вы можете пропустить кратковременные всплески метрик.</p>
  <h2 id="1fca">Что на самом деле делает Grafana</h2>
  <p id="27e3">Grafana — это всего лишь инструмент визуализации. Он ничего не собирает и ничего не хранит. Он подключается к источникам данных и создает красивые графики.</p>
  <p id="c8a2">При установке kube-prometheus-stack с помощью Helm, Grafana поставляется в комплекте и предварительно настроена с Prometheus в качестве источника данных. Но вы можете добавить и другие источники данных: CloudWatch для метрик AWS, OpenSearch для визуализации на основе логов, а также любую базу данных, поддерживаемую Grafana.</p>
  <p id="fc74">Главное преимущество Grafana заключается в возможности объединять данные из разных источников в одном дашборде. Вы можете показать метрики приложения из Prometheus, производительность базы данных из CloudWatch и тренды ошибок из логов OpenSearch — всё на одном экране.</p>
  <h2 id="6ef2">Использование готовых дашбордов вместо создания с нуля</h2>
  <p id="7abe">Вот что значительно сэкономит время. На сайте grafana.com/grafana/dashboards доступны тысячи готовых панелей мониторинга.</p>
  <p id="4647">Нужен мониторинг кластера Kubernetes? Найдите подходящий дашборд. Скопируйте его ID. Импортируйте в Grafana. Готово. У вас профессиональный мониторинг без написания ни одного запроса.</p>
  <p id="893f">Нужно отслеживать состояние вашей базы данных? Импортируйте панель мониторинга для вашего типа базы данных. Нужны метрики балансировщика нагрузки? Импортируйте соответствующую панель мониторинга.</p>
  <p id="4471">Каждая панель мониторинга уже настроена с необходимыми запросами, визуализациями и пороговыми значениями. Вам нужно лишь подключить её к источнику данных, и всё заработает.</p>
  <p id="5909">По мере освоения этих инструментов вы сможете настраивать их или создавать собственные. Но начинать с уже существующих панелей мониторинга — разумное решение. Вы научитесь понимать, как выглядит качественный мониторинг, прежде чем пытаться создать его самостоятельно.</p>
  <h2 id="e0b9">Создание кастомных дашбордов по мере необходимости</h2>
  <p id="6894">Иногда в вашем приложении есть специфические метрики, которые не охватываются готовыми панелями мониторинга. В таких случаях вам нужно создавать собственные панели.</p>
  <p id="7df2">В Grafana есть конструктор запросов, но понимание базового языка запросов сильно помогает. Для источников данных Prometheus это PromQL.</p>
  <p id="d81a">Простой PromQL-запрос <code>http_requests_total</code> может отображать общее количество запросов. Вы можете фильтровать запросы по меткам, например, <code>http_requests_total{service=”catalog”}</code> только для сервиса catalog. Вы можете рассчитать скорость запросов с помощью <code>rate(http_requests_total[5m])</code>, чтобы показать количество запросов в секунду за 5 минут.</p>
  <p id="0dfc">Кривая обучения существует. Но документация хорошая, и вы учитесь на практике. Начните с простых запросов. Добавьте фильтры. Добавьте агрегации. Постепенно усложняйте запросы по мере понимания.</p>
  <h2 id="de5d">Концепция соглашений об уровне обслуживания (SLA) и почему они важны</h2>
  <p id="430b">Именно здесь наблюдаемость соприкасается с реальностью бизнеса.</p>
  <p id="b1e7">Соглашение об уровне обслуживания (SLA) — это обещание вашим клиентам относительно качества предоставляемых услуг. Возможно, вы обещаете 99,1% времени безотказной работы. Возможно, вы обещаете время ответа менее 500 миллисекунд для 95% запросов.</p>
  <p id="6db0">Это не просто цифры. Это договорные обязательства. Нарушение этих обязательств может повлечь за собой финансовые санкции или уход клиентов.</p>
  <p id="2a15">Позвольте мне показать вам, что на самом деле означает 99,1% времени безотказной работы. В одном месяце примерно 2,5 миллиона секунд. 99,1% времени безотказной работы означает, что допустимое время простоя составляет 0,9%. Это примерно 64 часа в месяц.</p>
  <p id="3668">Это ваш бюджет простоя. Ваша система мониторинга отслеживает, укладываетесь ли вы в бюджет. Если вы расходуете его слишком быстро, вам нужно исправить ситуацию до того, как нарушится соглашение об уровне обслуживания (SLA).</p>
  <p id="1bfd">Такие показатели, как время безотказной работы, процентные доли задержки и частота ошибок, напрямую связаны с этими бизнес-обязательствами. Именно поэтому мониторинг этих показателей важен. Это не просто технический интерес. Это выполнение обещаний, данных клиентам.</p>
  <h2 id="7004">Подводя итог</h2>
  <p id="17e3">Полная картина наблюдаемости включает в себя поток логов через один конвейер и поток метрик через другой.</p>
  <p id="4fd0">Логи передаются из подов к коллекторам, а затем в CloudWatch, опционально проходят через Lambda для форматирования, затем через Kinesis для буферизации, попадают в OpenSearch для анализа и в S3 для резервного хранения.</p>
  <p id="80ca">Метрики собираются Prometheus с конечных точек приложений, определенных ServiceMonitors. Prometheus сохраняет актуальные данные. Grafana визуализирует их вместе с данными из других источников.</p>
  <p id="3b53">Обе системы работают параллельно. Обе критически важны. Невозможно эффективно устранять неполадки, опираясь только на логи или только на метрики. Необходимы обе точки зрения.</p>
  <p id="5310">Когда что-то ломается, логи показывают, что пошло не так. Метрики показывают, когда это началось, насколько серьезной была проблема и когда она была решена.</p>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="jbHi">Подписывайтесь на телеграм-канал <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ</a>, там еще больше полезной информации о мониторинге!</p>
  </section>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@monitorim_it/vector-metrics-prometheus</guid><link>https://teletype.in/@monitorim_it/vector-metrics-prometheus?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it</link><comments>https://teletype.in/@monitorim_it/vector-metrics-prometheus?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it#comments</comments><dc:creator>monitorim_it</dc:creator><title>Пошаговое руководство по настройке безопасности конвейеров наблюдаемости с помощью Vector от Datadog</title><pubDate>Wed, 08 Jul 2026 06:29:00 GMT</pubDate><media:content medium="image" url="https://img1.teletype.in/files/0b/4b/0b4b7bc1-cba6-4eb8-a181-af690a27ab3a.png"></media:content><description><![CDATA[<img src="https://miro.medium.com/v2/resize:fit:875/1*4--uIcHxlL1vkMTBjHb51A.png"></img>Перевод оригинальной статьи A Step-by-Step Guide to Securing Observability Pipelines Using Vector by Datadog.]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="d228">Перевод оригинальной статьи <a href="https://medium.com/doubleverify-engineering/a-step-by-step-guide-to-securing-observability-pipelines-using-vector-by-datadog-75417454e532" target="_blank">A Step-by-Step Guide to Securing Observability Pipelines Using Vector by Datadog</a>.</p>
  </section>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="HyOb">Перевод сделан специально для телеграм-канала <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ.</a> Подписывайтесь! Там еще больше полезных постов о мониторинге.</p>
  </section>
  <p id="673f">В сложном мире современных распределенных программных систем наблюдаемость является ключевым компонентом, позволяющим обеспечить высокую производительность и надёжность.</p>
  <p id="3b78">Независимо от того, являетесь ли вы начинающим специалистом или опытным инженером, который ищет рекомендации по построению конвейеров наблюдаемости, эта статья будет полезна для любого уровня подготовки.</p>
  <h2 id="5124"><strong>Почему Vector?</strong></h2>
  <p id="0132">Начнём с нашего сценария использования. В DV мы часто упаковываем и поставляем приложения нашим партнёрам, которые затем устанавливают их в своей инфраструктуре. Чтобы обеспечить надёжность, нам необходимо собирать данные о производительности этих приложений.</p>
  <p id="0bf9">Поскольку инфраструктура наших партнеров часто остается для нас &quot;черным ящиком&quot;, мы пришли к выводу, что нам необходим инструмент, отвечающий следующим требованиям:</p>
  <ol id="wXcx">
    <li id="7981">Мы могли бы включить его в наше приложение для сбора необходимых данных о производительности.</li>
    <li id="ffd9">Процесс сбора данных оказывает минимальное воздействие на систему как с точки зрения использования памяти, так и загрузки процессора.</li>
    <li id="585e">Возможность безопасно передавать собранные данные в наши централизованные системы мониторинга.</li>
  </ol>
  <p id="6950">Несмотря на наличие множества решений для корпоративного мониторинга и инструментов с открытым исходным кодом, <strong>Vector от Datadog</strong> выделился среди прочих благодаря своей лёгкости и высокой производительности.</p>
  <p id="c6dd">Vector — инструмент с открытым исходным кодом для построения конвейеров наблюдаемости. Он настраивается для сбора, преобразования и маршрутизации логов, метрик и трассировок в любую поддерживаемую систему назначения из списка <a href="https://vector.dev/docs/reference/configuration/sinks/" target="_blank">поддерживаемых Datadog получателей</a>.</p>
  <p id="fb61">В этой статье я покажу, как собирать метрики Prometheus из приложения, работающего в кластере Kubernetes, и передавать их в экземпляр Prometheus, размещённый на виртуальной машине в другом географическом регионе, используя Prometheus Remote Write.</p>
  <h2 id="5639"><strong>Предварительные требования</strong></h2>
  <p id="93d8">Для понимания этой статьи вам потребуется:</p>
  <ol id="XqVW">
    <li id="ba4e">Практические навыки работы с Kubernetes и использования Kubectl.</li>
    <li id="2f8c">Практические навыки установки и настройки Helm-чартов.</li>
    <li id="6cf2">Запущенный кластер Kubernetes как минимум с одним узлом (в примере используется кластер GKE).</li>
    <li id="8ea7">Запущенный экземпляр Prometheus с включённой поддержкой Remote Write (в примере Prometheus установлен на виртуальной машине в GCP).</li>
    <li id="52de">Все необходимые сетевые коммуникации между Vector и Prometheus.</li>
    <li id="8f38">Сертификаты mTLS, подписанные доверенным центром сертификации (для этой демонстрации используются самоподписанные сертификаты).</li>
  </ol>
  <h2 id="31dc"><strong>Что такое mTLS?</strong></h2>
  <p id="GTna">Mutual TLS, или сокращенно mTLS, — это механизм взаимной аутентификации. mTLS гарантирует, что обе стороны сетевого соединения действительно являются теми, за кого себя выдают, проверяя наличие корректного закрытого ключа у каждой из сторон. Дополнительная проверка осуществляется на основе информации, содержащейся в соответствующих TLS-сертификатах.</p>
  <p id="9fd6">mTLS часто используется в рамках концепции безопасности «нулевого доверия» для проверки пользователей, устройств и серверов организации.</p>
  <h3 id="f2f5"><strong>Архитектурная схема</strong></h3>
  <figure id="IEaQ" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*4--uIcHxlL1vkMTBjHb51A.png" width="700" />
  </figure>
  <h2 id="45cf"><strong>Далее — самая интересная часть: внедрение!</strong></h2>
  <h3 id="c703"><strong>Шаг 1: Создание самоподписанных mTLS-сертификатов</strong></h3>
  <p id="ebd2">Сначала мы создаём центр сертификации (ЦС), которому доверяют как клиент, так и сервер.</p>
  <pre id="F1c0" data-lang="bash">openssl req \
 -new \
 -x509 \
 -nodes \
 -days 30 \
 -subj &#x27;/CN=my-ca.my-domain.com&#x27; \
 -keyout ca.key \
 -out ca.crt</pre>
  <p id="abdc">Эта команда выводит два файла, ca.key и ca.crt, оба в формате PEM.</p>
  <p id="32c7">Далее мы создаём ключ сервера, а затем сертификат.</p>
  <pre id="ngGk" data-lang="bash">openssl genrsa -out server.key 4096</pre>
  <p id="e331">Теперь давайте создадим запрос на подписание сертификата (CSR), который будет подписан нашим центром сертификации:</p>
  <pre id="eUiq" data-lang="bash">openssl req \
 -new \
 -key server.key \
 -subj &#x27;/CN=prometheus.my-domain.com&#x27; \
 -out server.csr</pre>
  <p id="8a85">Используя запрос на подписание сертификата (CSR), давайте создадим сертификат сервера:</p>
  <pre id="Ezs0" data-lang="bash">openssl x509 \
 -req \
 -in server.csr \
 -CA ca.crt \
 -CAkey ca.key \
 -CAcreateserial \
 -days 30 \
 -out server.crt</pre>
  <p id="c9b7">Обратите внимание на следующий вывод команды, а также на созданный PEM-файл server.crt:</p>
  <pre id="eawO" data-lang="bash">Certificate request self-signature ok
subject=CN=prometheus.my-domain.com</pre>
  <p id="2e3e">Теперь повторим этот процесс для создания клиентского сертификата и закрытого ключа. Как и ранее, начнём с генерации ключа.</p>
  <pre id="Bjic" data-lang="bash">openssl genrsa -out client.key 4096</pre>
  <p id="8714">Теперь создадим запрос на подпись сертификата (CSR), который также будет подписан нашим центром сертификации.</p>
  <pre id="gv1H" data-lang="bash">openssl req \
 -new \
 -key client.key \
 -subj &#x27;/CN=vector.my-domain.com&#x27; \
 -out client.csr</pre>
  <p id="91bc">Используя запрос на подписание сертификата (CSR), создайте клиентский сертификат:</p>
  <pre id="PEz4" data-lang="bash">openssl x509 \
 -req \
 -in client.csr \
 -CA ca.crt \
 -CAkey ca.key \
 -CAcreateserial \
 -days 30 \
 -out client.crt</pre>
  <p id="5b8e">Обратите внимание на приведенный ниже вывод, а также на PEM-файл client.crt:</p>
  <pre id="myki" data-lang="bash">Certificate request self-signature ok
subject=CN=vector.my-domain.com</pre>
  <p id="0460">Теперь в текущей рабочей директории должны находиться следующие файлы:</p>
  <pre id="OeBQ" data-lang="bash">ca.crt
ca.key
ca.srl
client.crt
client.csr
client.key
server.crt
server.csr
server.key</pre>
  <p id="99c6">Мы будем работать с файлами <strong><em>ca.crt, ca.key, client.crt, client.key, server.crt</em></strong> и <strong><em>server.key</em></strong>. Переместите их в такое место, где Vector сможет получить доступ к клиентским сертификатам, а Prometheus — к серверным.</p>
  <p id="64f3">Можете смело удалять <strong><em>файлы ca.srl, client.csr</em></strong> и <strong><em>server.csr</em></strong> — они нам больше не нужны.</p>
  <h3 id="b39c"><strong>Шаг 2: Создание Namespace для Vector и Kubernetes Secret с сертификатами mTLS</strong></h3>
  <p id="5526">Создайте своё пространство имён:</p>
  <pre id="dmfX" data-lang="bash">kubectl create namespace vector</pre>
  <p id="81b4">Затем создайте Kubernetes Secret с сертификатами:</p>
  <pre id="QjlB" data-lang="bash">kubectl create secret generic vector-certs \
--namespace vector \
--from-file=ca.crt=ca.crt \
--from-file=tls.crt=client.crt \
--from-file=tls.key=client.key</pre>
  <h3 id="632c"><strong>Шаг 3: Установка и настройка Vector с помощью Helm</strong></h3>
  <p id="fa03">Добавьте репозиторий Helm:</p>
  <pre id="F3VJ" data-lang="bash">helm repo add vector https://helm.vector.dev
helm repo update</pre>
  <p id="ec1e"><strong>Подготовьте пользовательскую конфигурацию для Vector.</strong></p>
  <p id="1471">Давайте создадим пользовательский файл и определим в нём наши конвейеры наблюдаемости.</p>
  <p id="bc67">В приведённом ниже примере Vector настроен на получение метрик с конечной точки <a href="http://app.my-namespace.svc.cluster.local:8384/metrics" target="_blank">http://app.my-namespace.svc.cluster.local:8384/metrics</a> в секции source, а затем на передачу данных на конечную точку <a href="https://prometheus.my-domain.com:9090/api/v1/write" target="_blank">https://prometheus.my-domain.com:9090/api/v1/write</a> в секции sink (получателя) с использованием TLS.</p>
  <pre id="h9hF" data-lang="bash">## my-vector-custom-configs.yaml
data_dir: /var/lib/vector
sources:
  app_scrape:
    type: prometheus_scrape
    endpoints:
    - &quot;http://app.my-namespace.svc.cluster.local:8384/metrics&quot; ## Enpoint that exposes application performance metrics
    scrape_interval_secs: 15
sinks:
  prometheus_remotewrite:
    type: prometheus_remote_write
    inputs:
    - app_scrape
    endpoint: https://prometheus.my-domain.com:9090/api/v1/write ## Endpoint that receives p8s metrics
    healthcheck:
      enabled: true
    tls:
      ca_file: &quot;/run/secrets/tls/ca.crt&quot;
      crt_file: &quot;/run/secrets/tls/client.crt&quot;
      key_file: &quot;/run/secrets/tls/client.key&quot;</pre>
  <p id="2d1d">Теперь давайте создадим Kubernetes ConfigMap, используя указанный выше файл, и подключим его к поду Vector. Вам также необходимо подключить ранее созданный Secret, чтобы Vector мог использовать TLS-сертификаты при передаче данных на Prometheus Remote Write.</p>
  <p id="149e"><strong>Создайте ConfigMap для Kubernetes:</strong></p>
  <pre id="PqUD" data-lang="bash">kubectl create configmap vector-custom-configs \
--namespace vector \
--from-file=vector.yaml=my-vector-custom-configs.yaml</pre>
  <p id="a4b1">Подготовьте пользовательский файл values, который подключит и ConfigMap, и Secret к поду Vector:</p>
  <pre id="jzBs" data-lang="bash">## my-vector-custom-values.yaml
args:
  - --config
  - &quot;/opt/vector/vector.yaml&quot;

extraVolumeMounts:
  - name: secret
    mountPath: /run/secrets/tls/
  - name: custom-configs
    mountPath: /opt/vector
    readOnly: true

extraVolumes:
  - name: secret
    secret:
      secretName: &quot;vector-certs&quot;
  - name: custom-configs
    configMap:
      name: &quot;vector-custom-configs&quot;</pre>
  <p id="f517"><strong>Установите Helm Chart Vector.</strong></p>
  <p id="92e5">По умолчанию Vector запускается как StatefulSet в роли Aggregator. В качестве альтернативы он может работать как Deployment в роли Stateless-Aggregator либо как DaemonSet в роли Agent. Эти свойства можно переопределить в файле custom-values.yaml по мере необходимости. Для получения дополнительной информации можно также обратиться к файлу <a href="https://github.com/vectordotdev/helm-charts/blob/develop/charts/vector/values.yaml" target="_blank">default-values.yaml .</a></p>
  <pre id="yhnS" data-lang="bash">helm upgrade --install my-vector-release vector/vector \
--namespace vector \
-f my-vector-custom-values.yaml</pre>
  <p id="cbc8">После успешной установки в терминале вы должны увидеть вывод Helm со статусом «STATUS: deployed» и шаблоном запуска Vector.</p>
  <p id="9a75">Если настройка прошла успешно, вы увидите, что поды Vector StatefulSet находятся в состоянии Running без сообщений об ошибках в логах.</p>
  <h2 id="9716"><strong>Наблюдаемость: ключ к эффективному управлению данными.</strong></h2>
  <p id="06ac">Современные распределенные программные системы сложны, поэтому их наблюдаемость становится ключевым фактором для достижения реальной производительности и надежности.</p>
  <p id="2e48">Vector обладает мощными и адаптируемыми возможностями сбора данных, что делает его незаменимым решением для построения надежных конвейеров мониторинга. Однако, помимо сбора метрик Prometheus, Vector может построить несколько конвейеров для сбора логов и трассировок (traces), которые затем могут быть отправлены в другие системы мониторинга, такие как Loki, Splunk, Datadog или Elastic.</p>
  <p id="4361">Надеюсь, было полезно. Спасибо за прочтение!</p>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="08kK">Подписывайтесь на телеграм-канал <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ</a>, там еще больше полезной информации о мониторинге!</p>
  </section>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@monitorim_it/grafana-alloy</guid><link>https://teletype.in/@monitorim_it/grafana-alloy?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it</link><comments>https://teletype.in/@monitorim_it/grafana-alloy?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it#comments</comments><dc:creator>monitorim_it</dc:creator><title>Прекратите запускать по 5 агентов на каждом сервере — используйте Grafana Alloy</title><pubDate>Tue, 07 Jul 2026 07:48:16 GMT</pubDate><tt:hashtag>devops</tt:hashtag><tt:hashtag>мониторинг</tt:hashtag><tt:hashtag>grafana</tt:hashtag><tt:hashtag>prometheus</tt:hashtag><tt:hashtag>наблюдаемость</tt:hashtag><description><![CDATA[Это перевод оригинальной статьи Stop Running 5 Agents Per Server — Use Grafana Alloy Instead.]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="89F9">Это перевод оригинальной статьи <a href="https://blog.devops.dev/stop-running-5-agents-per-server-use-grafana-alloy-instead-8ecd7b147950" target="_blank">Stop Running 5 Agents Per Server — Use Grafana Alloy Instead</a>.</p>
  </section>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="AE5z">Перевод сделан специально для телеграм-канала <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ.</a> Подписывайтесь! Там еще больше полезных постов о мониторинге.</p>
  </section>
  <h3 id="8d81"><em>Как я заменил Prometheus + Node Exporter + Promtail + MySQL Exporter одним бинарным файлом</em></h3>
  <p id="3d8e">Я управлял более чем 10 серверами с классическим стеком мониторинга: Prometheus для сбора метрик, Node Exporter на каждом хосте, Promtail для отправки логов, а также MySQL Exporter, Apache Exporter и другими.</p>
  <p id="4266"><strong>Это означает 4–5 агентов на сервер.</strong> Каждому требуется настройка, обновления и устранение неполадок.</p>
  <p id="42ab">Затем я открыл для себя <strong>Grafana Alloy</strong>.</p>
  <p id="2d43">Один бинарный файл. Один конфигурационный файл. Метрики, логи и трассировки — всё в одном месте.</p>
  <p id="a8fe">Так я это настроил.</p>
  <h2 id="1d24">Что такое Grafana Alloy?</h2>
  <p id="5d5f">Alloy — это унифицированный сборщик телеметрии Grafana (ранее Grafana Agent). Он заменяет:</p>
  <ul id="1Af5">
    <li id="0499">✅ Prometheus (сбор метрик)</li>
    <li id="2eb1">✅ Node Exporter</li>
    <li id="7af8">✅ MySQL Exporter</li>
    <li id="0d6e">✅ Apache Exporter</li>
    <li id="8557">✅ Promtail</li>
    <li id="9b84">✅ OpenTelemetry Collector</li>
  </ul>
  <p id="ef6c"><strong>Один агент для управления всем стеком.</strong></p>
  <h2 id="c3e1">Архитектура</h2>
  <pre id="xvLC" data-lang="bash">┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│ MySQL Server │    │  Web Server  │    │ Docker Host  │
│    Alloy     │    │    Alloy     │    │    Alloy     │
└──────┬───────┘    └──────┬───────┘    └──────┬───────┘
       │                   │                   │
       └───────────────────┼───────────────────┘
                           ▼
              ┌────────────────────────┐
              │   Monitoring Server    │
              │  Prometheus + Loki +   │
              │       Grafana          │
              └────────────────────────┘</pre>
  <p id="9ad7">Рассмотрим, как это настроить.</p>
  <h2 id="3cc8">Часть 1: Сервер мониторинга</h2>
  <p id="7dea">Я использую Docker Compose. Простой и портативный способ.</p>
  <h2 id="72ae">Создайте структуру каталогов.</h2>
  <pre id="wnXQ" data-lang="bash">mkdir -p /opt/monitoring/{prometheus,loki,grafana/provisioning/datasources}
cd /opt/monitoring</pre>
  <h2 id="bc4c">docker-compose.yml</h2>
  <pre id="UtT2" data-lang="bash">version: &quot;3.8&quot;</pre>
  <pre id="VgPs" data-lang="bash">services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - &#x27;--config.file=/etc/prometheus/prometheus.yml&#x27;
      - &#x27;--storage.tsdb.retention.time=30d&#x27;
      - &#x27;--web.enable-remote-write-receiver&#x27;
    ports:
      - &quot;9090:9090&quot;
    restart: unless-stopped
  grafana:
    image: grafana/grafana:latest
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=changeme
      - GF_USERS_ALLOW_SIGN_UP=false
    ports:
      - &quot;3000:3000&quot;
    restart: unless-stopped
  loki:
    image: grafana/loki:latest
    volumes:
      - ./loki/loki-config.yml:/etc/loki/local-config.yaml
      - loki_data:/loki
    command: -config.file=/etc/loki/local-config.yaml
    ports:
      - &quot;3100:3100&quot;
    restart: unless-stopped
volumes:
  prometheus_data:
  grafana_data:
  loki_data:</pre>
  <h2 id="eaff">prometheus/prometheus.yml</h2>
  <pre id="dAqm" data-lang="bash">global:
  scrape_interval: 15s</pre>
  <pre id="khtW" data-lang="bash">scrape_configs:
  - job_name: &#x27;prometheus&#x27;
    static_configs:
      - targets: [&#x27;localhost:9090&#x27;]</pre>
  <p id="88c7">Ключевая настройка: <code>--web.enable-remote-write-receiver</code> позволяет Alloy напрямую передавать метрики в Prometheus.</p>
  <h2 id="75b2">loki/loki-config.yml</h2>
  <pre id="qgvN" data-lang="bash">auth_enabled: false</pre>
  <pre id="G5l9" data-lang="bash">server:
  http_listen_port: 3100
common:
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory
schema_config:
  configs:
    - from: 2020-10-24
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h
limits_config:
  retention_period: 30d
compactor:
  working_directory: /loki/compactor
  retention_enabled: true
  delete_request_store: filesystem</pre>
  <p id="deee">⚠️ <strong>Важно:</strong> строка <code>delete_request_store: filesystem</code> обязательна, когда включено хранение (retention). Без неё Loki не запустится.</p>
  <h2 id="f4a3">grafana/provisioning/datasources/datasources.yml</h2>
  <pre id="quHE" data-lang="bash">apiVersion: 1</pre>
  <pre id="DjpL" data-lang="bash">datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
  - name: Loki
    type: loki
    access: proxy
    url: http://loki:3100</pre>
  <h2 id="5f75">Запускаем</h2>
  <pre id="R97O" data-lang="bash">docker compose up -d</pre>
  <p id="2573">Проверка:</p>
  <ul id="HS8Q">
    <li id="f34e">Grafana: <code>http://YOUR_IP:3000</code> (admin/changeme)</li>
    <li id="d677">Prometheus: <code><a href="http://your_ip:9090/" target="_blank">http://YOUR_IP:9090</a></code></li>
    <li id="fd81">Loki: <a href="http://YOUR_IP:3100/ready" target="_blank">http://YOUR_IP:3100/ready</a></li>
  </ul>
  <h2 id="bd3e">Часть 2: Мониторинг сервера MySQL</h2>
  <p id="6882">Теперь давайте установим Alloy на сервер базы данных MySQL.</p>
  <h2 id="1eb8">Установка Alloy</h2>
  <pre id="r7AI" data-lang="bash">curl -fsSL https://apt.grafana.com/gpg.key | gpg --dearmor -o /usr/share/keyrings/grafana.gpg
echo &quot;deb [signed-by=/usr/share/keyrings/grafana.gpg] https://apt.grafana.com stable main&quot; | tee /etc/apt/sources.list.d/grafana.list
apt update &amp;&amp; apt install alloy -y</pre>
  <h2 id="d7bf">Создаём пользователя мониторинга MySQL</h2>
  <pre id="re4Z" data-lang="bash">CREATE USER &#x27;alloy&#x27;@&#x27;localhost&#x27; IDENTIFIED BY &#x27;secure_password&#x27;;
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO &#x27;alloy&#x27;@&#x27;localhost&#x27;;
FLUSH PRIVILEGES;</pre>
  <h2 id="ba4e">Конфигурация Alloy</h2>
  <p id="9856">Создайте <code>/etc/alloy/config.alloy</code>:</p>
  <pre id="9N4J" data-lang="bash">// Node metrics (replaces node_exporter)
prometheus.exporter.unix &quot;node&quot; { }</pre>
  <pre id="VcFJ" data-lang="bash">prometheus.scrape &quot;node&quot; {
  targets    = prometheus.exporter.unix.node.targets
  forward_to = [prometheus.relabel.add_labels.receiver]
}
// MySQL metrics (replaces mysqld_exporter)
prometheus.exporter.mysql &quot;db&quot; {
  data_source_name = &quot;alloy:secure_password@(localhost:3306)/&quot;
}
prometheus.scrape &quot;mysql&quot; {
  targets    = prometheus.exporter.mysql.db.targets
  forward_to = [prometheus.relabel.add_labels.receiver]
}
// Add server label to all metrics
prometheus.relabel &quot;add_labels&quot; {
  rule {
    action       = &quot;replace&quot;
    target_label = &quot;server&quot;
    replacement  = &quot;mysql-01&quot;
  }
  forward_to = [prometheus.remote_write.default.receiver]
}
// Ship to Prometheus
prometheus.remote_write &quot;default&quot; {
  endpoint {
    url = &quot;http://192.168.0.23:9090/api/v1/write&quot;
  }
}
// Ship logs to Loki
loki.source.file &quot;logs&quot; {
  targets = [
    {__path__ = &quot;/var/log/mysql/error.log&quot;, job = &quot;mysql-error&quot;, server = &quot;mysql-01&quot;},
    {__path__ = &quot;/var/log/syslog&quot;, job = &quot;syslog&quot;, server = &quot;mysql-01&quot;},
  ]
  forward_to = [loki.write.default.receiver]
}
loki.write &quot;default&quot; {
  endpoint {
    url = &quot;http://192.168.0.23:3100/loki/api/v1/push&quot;
  }
}</pre>
  <h2 id="0a09">Исправьте права доступа и запустите.</h2>
  <pre id="7Do8" data-lang="bash">usermod -aG adm alloy
usermod -aG mysql alloy
systemctl enable alloy
systemctl start alloy</pre>
  <h2 id="9181">Убедитесь, что всё работает.</h2>
  <pre id="XBoI" data-lang="bash">curl -s &quot;http://192.168.0.23:9090/api/v1/query?query=mysql_up&quot; | jq .</pre>
  <p id="2a2d">В результате вы должны увидеть сервер <code>mysql-01</code> .</p>
  <h2 id="71c8">Часть 3: Мониторинг веб-сервера Apache</h2>
  <p id="a8bd">Схема та же — установить Alloy, настроить экспортеры.</p>
  <h2 id="38d3">Включаем статус Apache</h2>
  <pre id="FHDv" data-lang="bash">a2enmod status</pre>
  <p id="b86b">Создайте <code>/etc/apache2/sites-available/000-localhost-status.conf</code>:</p>
  <pre id="GCHq" data-lang="bash">&lt;VirtualHost 127.0.0.1:80&gt;
    ServerName 127.0.0.1
    
    &lt;Location &quot;/server-status&quot;&gt;
        SetHandler server-status
        Require local
    &lt;/Location&gt;
&lt;/VirtualHost&gt;</pre>
  <pre id="FSM2" data-lang="bash">a2ensite 000-localhost-status
systemctl restart apache2</pre>
  <p id="a7f2"><strong>Полезный совет: </strong>если используется Laravel или другой фреймворк, перехватывающий маршруты, создайте отдельный vhost. Префикс 000- гарантирует, что он загрузится первым.</p>
  <h2 id="fcb6">Настройка Alloy</h2>
  <pre id="HtcQ" data-lang="bash">prometheus.exporter.unix &quot;node&quot; { }</pre>
  <pre id="MFRK" data-lang="bash">prometheus.scrape &quot;node&quot; {
  targets    = prometheus.exporter.unix.node.targets
  forward_to = [prometheus.relabel.add_labels.receiver]
}
prometheus.exporter.apache &quot;web&quot; {
  scrape_uri = &quot;http://127.0.0.1/server-status?auto&quot;
}
prometheus.scrape &quot;apache&quot; {
  targets    = prometheus.exporter.apache.web.targets
  forward_to = [prometheus.relabel.add_labels.receiver]
}
prometheus.relabel &quot;add_labels&quot; {
  rule {
    action       = &quot;replace&quot;
    target_label = &quot;server&quot;
    replacement  = &quot;web-server&quot;
  }
  forward_to = [prometheus.remote_write.default.receiver]
}
prometheus.remote_write &quot;default&quot; {
  endpoint {
    url = &quot;http://192.168.0.23:9090/api/v1/write&quot;
  }
}
loki.source.file &quot;apache_logs&quot; {
  targets = [
    {__path__ = &quot;/var/log/apache2/access.log&quot;, job = &quot;apache-access&quot;, server = &quot;web-server&quot;},
    {__path__ = &quot;/var/log/apache2/error.log&quot;, job = &quot;apache-error&quot;, server = &quot;web-server&quot;},
  ]
  forward_to = [loki.write.default.receiver]
}
loki.write &quot;default&quot; {
  endpoint {
    url = &quot;http://192.168.0.23:3100/loki/api/v1/push&quot;
  }
}</pre>
  <h2 id="b616">Часть 4: Мониторинг контейнеров Docker</h2>
  <p id="b29f">Для хостов Docker используйте cAdvisor для сбора метрик контейнеров.</p>
  <h2 id="837a">Запустите cAdvisor</h2>
  <pre id="Vp5n" data-lang="bash">docker run -d \
  --name=cadvisor \
  --restart=unless-stopped \
  --privileged \
  -p 8081:8080 \
  -v /:/rootfs:ro \
  -v /var/run:/var/run:ro \
  -v /sys:/sys:ro \
  -v /var/lib/docker/:/var/lib/docker:ro \
  gcr.io/cadvisor/cadvisor:latest</pre>
  <h2 id="d892">Настройте Alloy для сбора данных с cAdvisor.</h2>
  <pre id="IbCf" data-lang="bash">prometheus.exporter.unix &quot;node&quot; { }</pre>
  <pre id="61eN" data-lang="bash">prometheus.scrape &quot;node&quot; {
  targets    = prometheus.exporter.unix.node.targets
  forward_to = [prometheus.relabel.add_labels.receiver]
}
prometheus.scrape &quot;cadvisor&quot; {
  targets = [
    {&quot;__address__&quot; = &quot;localhost:8081&quot;, &quot;job&quot; = &quot;cadvisor&quot;},
  ]
  forward_to = [prometheus.relabel.add_labels.receiver]
}
prometheus.relabel &quot;add_labels&quot; {
  rule {
    action       = &quot;replace&quot;
    target_label = &quot;server&quot;
    replacement  = &quot;docker-host&quot;
  }
  forward_to = [prometheus.remote_write.default.receiver]
}
prometheus.remote_write &quot;default&quot; {
  endpoint {
    url = &quot;http://192.168.0.23:9090/api/v1/write&quot;
  }
}
// Collect container logs directly
discovery.docker &quot;containers&quot; {
  host = &quot;unix:///var/run/docker.sock&quot;
}
loki.source.docker &quot;containers&quot; {
  host       = &quot;unix:///var/run/docker.sock&quot;
  targets    = discovery.docker.containers.targets
  labels     = {server = &quot;docker-host&quot;, job = &quot;docker&quot;}
  forward_to = [loki.write.default.receiver]
}
loki.write &quot;default&quot; {
  endpoint {
    url = &quot;http://192.168.0.23:3100/loki/api/v1/push&quot;
  }
}</pre>
  <p id="f79d">Не забудьте:</p>
  <pre id="kf9D" data-lang="bash">usermod -aG docker alloy
systemctl restart alloy</pre>
  <h2 id="2212">Советы по дашбордам Grafana</h2>
  <h2 id="b32d">Добавьте выпадающий список серверов</h2>
  <ol id="Ek5G">
    <li id="e7c2">Настройки дашборда → Переменные → Создать</li>
    <li id="6f2c">Имя: <code>server</code></li>
    <li id="7a3b">Тип: Запрос</li>
    <li id="86a7">Запрос: <code>label_values(up, server)</code></li>
    <li id="fb10">Включите Multi-value и Include All</li>
  </ol>
  <p id="d671">Теперь используйте <code>{server=~&quot;$server&quot;}</code> в своих запросах:</p>
  <pre id="C7UQ" data-lang="bash">rate(mysql_global_status_queries{server=~&quot;$server&quot;}[5m])</pre>
  <h2 id="1f1f">Ключевые метрики для мониторинга</h2>
  <p id="4e8c"><strong>MySQL:</strong></p>
  <ul id="GTln">
    <li id="567e"><code>mysql_up</code> — доступен ли сервер </li>
    <li id="97a0"><code>mysql_global_status_threads_connected</code> — количество активных подключений</li>
    <li id="dfdc"><code>rate(mysql_global_status_slow_queries[5m])</code> — скорость медленных запросов</li>
  </ul>
  <p id="14aa"><strong>Apache:</strong></p>
  <ul id="Cvvn">
    <li id="0c6b"><code>apache_up</code> — доступен?</li>
    <li id="d31b"><code>apache_workers{state=&quot;busy&quot;}</code> — активные воркеры</li>
    <li id="7c86"><code>rate(apache_accesses_total[5m])</code> — количество запросов в секунду</li>
  </ul>
  <p id="9ff0"><strong>Host:</strong></p>
  <ul id="X3a8">
    <li id="b9d3"><code>100 - (avg(rate(node_cpu_seconds_total{mode=&quot;idle&quot;}[5m])) * 100)</code> — загрузка CPU %</li>
    <li id="0900"><code>node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes</code> — используемая оперативная память (RAM)</li>
  </ul>
  <h2 id="5c62">Распространённые проблемы</h2>
  <h2 id="4d2f">«Компонент не существует»</h2>
  <p id="c636">В Alloy нет всех экспортеров. Например, <code>prometheus.exporter.php_fpm</code> просто не существует.</p>
  <p id="ab1e"><strong>Решение:</strong> Запустить автономный экспортер и собирайте метрики с него, или предоставьте доступ к метрикам через HTTP.</p>
  <h2 id="5dbf">Конфигурация не загружается</h2>
  <pre id="pwJl" data-lang="bash">alloy fmt /etc/alloy/config.alloy  # Check syntax
alloy run /etc/alloy/config.alloy  # See actual error</pre>
  <h2 id="e441">Нет доступа к логам</h2>
  <pre id="zPo9" data-lang="bash">usermod -aG adm alloy
usermod -aG www-data alloy  # for Apache logs
systemctl restart alloy</pre>
  <h2 id="290a">Результат</h2>
  <p id="c210"><strong>До:</strong> 5 агентов на сервер, 5 конфигураций для поддержки, 5 потенциальных точек отказа.</p>
  <p id="be95"><strong>После:</strong> 1 агент, 1 конфигурация, единая ситема наблюдаемости.</p>
  <p id="b2c4">Теперь мой стек мониторинга:</p>
  <ul id="hSUV">
    <li id="626b">Проще разворачивать (один бинарник)</li>
    <li id="8c73">Проще настраивать (один конфигурационный файл River)</li>
    <li id="1413">Проще диагностировать проблемы (всё в одном месте)</li>
  </ul>
  <p id="cbb2">Попробуйте Alloy. В будущем вы скажете себе спасибо.</p>
  <h2 id="025c">Ресурсы</h2>
  <ul id="xA3z">
    <li id="ed80"><a href="https://grafana.com/docs/alloy/latest/" target="_blank">Документация Grafana Alloy</a></li>
    <li id="56bc"><a href="https://grafana.com/docs/alloy/latest/concepts/configuration-syntax/" target="_blank">Язык конфигурации River</a></li>
    <li id="8b44"><a href="https://grafana.com/docs/alloy/latest/reference/components/" target="_blank">Встроенный список экспортеров</a></li>
  </ul>
  <tt-tags id="PTPC">
    <tt-tag name="devops">#devops</tt-tag>
    <tt-tag name="мониторинг">#мониторинг</tt-tag>
    <tt-tag name="grafana">#grafana</tt-tag>
    <tt-tag name="prometheus">#prometheus</tt-tag>
    <tt-tag name="наблюдаемость">#наблюдаемость</tt-tag>
  </tt-tags>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="EZFS">Подписывайтесь на телеграм-канал <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ</a>, там еще больше полезной информации о мониторинге!</p>
  </section>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@monitorim_it/building-a-production-grade-observability-platform</guid><link>https://teletype.in/@monitorim_it/building-a-production-grade-observability-platform?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it</link><comments>https://teletype.in/@monitorim_it/building-a-production-grade-observability-platform?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it#comments</comments><dc:creator>monitorim_it</dc:creator><title>Создание платформы наблюдаемости с помощью SigNoz, ClickHouse и OpenTelemetry — Часть 1</title><pubDate>Fri, 27 Mar 2026 08:43:07 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/39/f4/39f4560e-7390-4d1f-9004-a722cee25523.png"></media:content><description><![CDATA[<img src="https://miro.medium.com/v2/resize:fit:1926/1*8QBI-1qSvN1QYGlP3IxT4g.png"></img>Перевод оригинальной статьи Building a Production-Grade Observability Platform with SigNoz, ClickHouse, and OpenTelemetry — Part 1]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="d228">Перевод оригинальной статьи <a href="https://medium.com/@ShiveeGupta/building-a-production-grade-observability-platform-with-signoz-clickhouse-and-opentelemetry-d7f09a5250f5" target="_blank">Building a Production-Grade Observability Platform with SigNoz, ClickHouse, and OpenTelemetry — Part 1</a></p>
  </section>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="HyOb">Перевод сделан специально для телеграм-канала <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ.</a> Подписывайтесь! Там еще больше полезных постов о мониторинге.</p>
  </section>
  <p id="pAKi"><em>Уроки, полученные в ходе нашей внутренней настройки наблюдения, и то, что мы узнали помимо документации и долгих ночей настройки кластера.</em></p>
  <h2 id="2094">1. Обзор</h2>
  <p id="6262">Мы поставили перед собой задачу создать собственную платформу на базе <strong>SigNoz</strong>, <strong>ClickHouse</strong> и <strong>OpenTelemetry (OTEL)</strong>, способную обрабатывать метрики и трассировки в различных средах в производственных масштабах. Мы постоянно запускаем тысячи экземпляров EC2 и хранилищ данных, генерирующих миллионы метрик и событий трассировки. Кроме того, это помогает нам экономить миллионы долларов на счетах других коммерческих решений, которые мы использовали раньше. 💸</p>
  <h2 id="9ff4">О чем этот блог (и серия)</h2>
  <p id="8c5b">В этой публикации мы рассмотрим <strong>архитектуру и ключевые системные выводы,</strong> сделанные в ходе масштабного запуска — от проектирования до наблюдения за стеком наблюдения!<br />Мы выйдем за рамки обучающих материалов и рассмотрим <em>операционные реалии</em> высокопроизводительных систем наблюдения.</p>
  <p id="b799">В следующих частях мы подробно рассмотрим:</p>
  <p id="4fda"><strong>Единый двоичный пользовательский интерфейс SigNoz и схемы *MergeTree</strong></p>
  <ul id="BwR0">
    <li id="eae2">Различные компонент пользовательского интерфейса Signoz и их практические последствия при настройке продакшна.</li>
    <li id="e632">*Схемы MergeTree для хранения метрик и трассировок в ClickHouse и то, как они используют “fingerprints” для оптимизации хранения.</li>
  </ul>
  <p id="8bdd"><strong>Внутреннее устройство ClickHouse для телеметрических нагрузок</strong></p>
  <ul id="jNqv">
    <li id="8d5a">Как ведут себя репликация, очереди слияний и компрессия в масштабах петабайт.</li>
    <li id="b9ab">Почему решения по ключам сортировки могут как ускорить, так и полностью разрушить латентность запросов.</li>
    <li id="20ca">Как несоответствие форм вставки тихо убивает пропускную способность.</li>
    <li id="792a">Реальное влияние пакетной обработки, ограничений памяти и асинхронных вставок через коллектора.</li>
  </ul>
  <p id="41c4"><em>Давайте же приступим!</em></p>
  <h2 id="9732">Высокоуровневая архитектура</h2>
  <figure id="unC1" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:1926/1*8QBI-1qSvN1QYGlP3IxT4g.png" width="1541" />
    <figcaption>Корпоративный конвейер OTEL для метрик и трассировок Cloudwatch и Host</figcaption>
  </figure>
  <h2 id="96bc">Проектирование высокого уровня (HLD)</h2>
  <p id="026e">Вот высокоуровневая архитектура конвейера наблюдаемости, который мы настроили с использованием OpenTelemetry (OTEL), Kafka и SigNoz. Цель была проста: создать масштабируемый, независимый от вендора конвейер телеметрии, который мог бы обрабатывать метрики и трассировки десятков сервисов, не ограничиваясь одним проприетарным решением.</p>
  <p id="91cc">Всё начинается с источника — наших приложений и систем данных, таких как PostgreSQL, Spark и различных микросервисов. Они генерируют телеметрию (метрики и трассировки), которые мы собираем с помощью OTEL-коллекторов и SDK. Коллекторы отвечают за пакетирование, сэмплинг и преобразование данных в единый формат OTEL перед их дальнейшей отправкой.</p>
  <p id="f7d4">Для мониторинга метрик собственных сервисов AWS мы передаем <strong>метрики CloudWatch</strong> через <strong>Firehose</strong>. На пути потока находится <strong>лямбда-функция для обогащения тегами</strong>, добавляя полезные метаданные, такие как имена сервисов, окружения или кастомные теги, которые значительно упрощают поиск данных в будущем. Далее обогащенные данные резервируются в <strong>S3</strong> и направляются на уровень коллектора OTEL через балансировщик нагрузки.</p>
  <blockquote id="R0tG">Совет 🚀<em>В Firehose мы использовали размер буфера 0,2 МБ и интервал между буферами 60 секунд. Это даёт нам около 200 одновременных вызовов лямбда. Увеличение размера буфера может уменьшить количество лямбда  вызовов. Однако необходимо учитывать размер сообщения после обогащения тега. Чем больше буфер, тем больше пакет после обогащения, и вы можете столкнуться с ошибками <strong>«Message size too large»</strong> в логах Firehose. Настройте значение для оптимизации затрат. <br /><br />Лямбда для обогащения метрик тегами: мы использовали <a href="https://github.com/coralogix/cloudwatch-metric-streams-lambda-transformation?tab=readme-ov-file" target="_blank"><strong>модификацию этой лямбды</strong></a> для OTLPv1.0. Она использует <a href="https://docs.aws.amazon.com/resourcegroupstagging/latest/APIReference/API_GetResources.html" target="_blank">ResourceGroupsTaggingAPI</a> для обогащения метрик тегами в потоке. <br /><br />Используйте <strong>sending_queue</strong> и <strong>retry_on_failure</strong> в конфигурации OTEL collector. При экспорте в Kafka, в случае сбоя, коллектор будет хранить сообщения в памяти в зависимости от установленного лимита и пытаться отправить их повторно. Если очередь переполнена, начнется потеря сообщений. Слишком длинная очередь может привести к исчерпанию памяти экземпляра, вплоть до полной потери ответа. Поэтому лимит очереди критически важен.</em></blockquote>
  <p id="7a4e">Все эти телеметрические данные — как метрики, так и трассировки — поступают в <strong>Kafka</strong>, которая служит основой нашего конвейера наблюдения. У нас есть отдельные топики Kafka для метрик и трассировок, поддерживаемые <strong>шестью брокерами, с фактором репликации 3 и сроком хранения 3 дня</strong>. Это обеспечивает надёжный, отказоустойчивый буфер, способный справляться с пиковыми нагрузками без потери данных.</p>
  <p id="d7e4"><strong>Из Kafka данные получают коллекторы SigNoz</strong> и записывают их в <strong>ClickHouse</strong>, нашу основную базу данных временных рядов. Кластер ClickHouse состоит из <strong>3 шардов и 3 реплик</strong>, координируемых <strong>ZooKeeper</strong> для обеспечения согласованности и отказоустойчивости. Для управления расходами старые данные (более 90 дней) автоматически выгружаются в <strong>S3</strong>, что обеспечивает долгосрочное хранение без избыточных расходов на локальное хранилище.</p>
  <blockquote id="GMH5">Совет 🚀</blockquote>
  <blockquote id="2kKN"><em>- Убедитесь, что брокеры размещены в разных зонах доступности для высокой отказоустойчивости. У нас есть 6 брокеров с коэффициентом репликации 3 и сроком хранения 3 дня.</em></blockquote>
  <blockquote id="vlQh"><em>– Нам пришлось обновить max_message_bytes, поскольку батчи OTEL-коллектора превышали стандартный размер в 1 МБ. Настройте это значение в соответствии с вашей пропускной способностью.</em></blockquote>
  <blockquote id="J50p"><em>- Конфигурация <a href="https://github.com/open-telemetry/opentelemetry-collector/tree/main/processor/memorylimiterprocessor" target="_blank"><strong>memory_limiter</strong></a> на коллекторе Signoz OTEL оказалась настоящим спасением! Она ограничивает потребление памяти заданным порогом и оказывает обратное давление на приемник Kafka при превышении лимитов памяти экземпляра, защищая экземпляр от выполнения OOM.</em></blockquote>
  <blockquote id="ErMT"><em>- Мы используем 8xlarge-инстанс для каждого узла ClickHouse с <strong>SSD-накопителем на 250 ГБ</strong>. Мы будем экспериментировать с уменьшением размера экземпляра после бенчмаркинга с использованием трёхмесячных метрик. В настоящее время загрузка ЦП составляет около 8%.</em></blockquote>
  <p id="0ee5">Наконец, все данные выводятся на <strong>сервер SigNoz</strong>, предоставляющий единую интерфейс-панель для запросов и визуализации как метрик, так и трассировок. Именно здесь команды непосредственно взаимодействуют с данными — устраняют скачки задержек, анализируют тенденции и находят узкие места в производительности.</p>
  <p id="1e8b">Короче говоря, эта архитектура обеспечивает чёткое разделение приёма, обработки и хранения данных. Она устойчива к пиковым нагрузкам, достаточно модульна для развития со временем и даёт нам полный контроль над тем, как данные наблюдения проходят через наш стек, — без зависимости от непрозрачного (&quot;чёрного ящика&quot;) SaaS-решения.</p>
  <h2 id="3480">Масштаб и производительность</h2>
  <ul id="iSeo">
    <li id="905b">Производственная инфраструктура, находящаяся под наблюдением, включает в себя более 10 тыс. экземпляров EC2 и хранилищ данных, работающих на пике производительности, генерирующих метрики и трассировки.</li>
    <li id="3272">Скорость приема данных: ~200 Мбит/с (Cloudwatch + метрики хоста + трассировки)</li>
    <li id="46a5">~100 млн уникальных метрик на пике</li>
    <li id="2b8f">~1 млн строк/сек реплицируется на все 9 узлов ClickHouse</li>
    <li id="ab31">Время вставки запросов в ClickHouse: 25 мкс.</li>
    <li id="2d0e">357 ТБ загруженных спанов в месяц.</li>
  </ul>
  <h2 id="c195">2. Почему OpenTelemetry (OTEL), а не Prometheus</h2>
  <p id="24e2">Prometheus многие годы был стандартным выбором для наблюдаемости, но когда нам понадобилась телеметрия «сквозных сигналов» (метрики + трассировки + логи), его ограничения стали очевидными. Подробнее об этих различиях читайте <a href="https://signoz.io/blog/opentelemetry-vs-prometheus/" target="_blank">в этой статье</a>.</p>
  <figure id="6PvZ" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*xaLGZpYOzprPHy073ScTIQ.png" width="700" />
    <figcaption>Prometheus против OpenTelemetry</figcaption>
  </figure>
  <p id="48d5"><strong>Точки принятия решений:</strong></p>
  <ul id="0TcQ">
    <li id="abb3"><em>Модель pull</em> в Prometheus отлично подходит для простых кластеров, но плохо масштабируется для распределенных рабочих нагрузок с высокой мощностью.</li>
    <li id="4a4f"><em>Конструкция</em> OTEL на основе push-уведомлений и настраиваемые процессоры обеспечили нам более точный контроль и отказоустойчивость.</li>
    <li id="3bbc">Использование OTEL означало, что при необходимости мы могли экспортировать данные в несколько бэкэндов (например, ClickHouse + S3 + kafka).</li>
  </ul>
  <h2 id="3956">Почему SigNoz</h2>
  <ul id="IGVJ">
    <li id="60d5">Тесно интегрируется с OTEL и ClickHouse.</li>
    <li id="60dd">По умолчанию он предоставляет UI для трассировки, дашборды для метрик и систему алёртинга.</li>
    <li id="eca1">В отличие от SaaS-инструментов, он имеет открытый исходный код и является расширяемым — вы можете модифицировать экспортёр, менять схему ClickHouse и напрямую анализировать работу запросов.</li>
    <li id="8cdb">У него очень активная поддержка сообщества. </li>
  </ul>
  <blockquote id="vF4D">В последующих публикациях этой серии я поделюсь более подробной информацией о сервере SigNoz и архитектуре схемы, а также о том, как они себя покажут на платформе промышленного масштаба.</blockquote>
  <figure id="UpJO" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:1250/1*LPPNca0jHlDXlKBm-rv9yg.png" width="1000" />
    <figcaption>Signoz поддерживает PromQL для создания панелей мониторинга и мониторов</figcaption>
  </figure>
  <h2 id="632f">3. Наблюдение за стеком наблюдаемости</h2>
  <p id="0bc5">Наблюдаемость не будет полной, если вы не можете <em>наблюдать за самим стеком наблюдаемости</em>.<br />Наш конвейер телеметрии оснащен <strong>Prometheus и коллекторами OTEL</strong>, но с одной особенностью: мы создали <strong>устойчивый уровень сбора метрик с буферизацией через Kafka</strong>, который устраняет необходимость в статических scrape-таргетов Prometheus и допускает простои сервера Prometheus.</p>
  <p id="2cc0"><em>PS: Мы решили не настраивать отдельный конвейер Signoz + CH для этого, поскольку настройка еще одного кластера CH + ансамбля Zookeeper потребовала бы слишком больших затрат на обслуживание ради наблюдаемости самого конвейера наблюдаемости, который работает на фиксированных инстансах.</em></p>
  <h3 id="d5e1">3.1 Мотивация дизайна</h3>
  <p id="1e73">По мере масштабирования наших кластеров OTEL и ClickHouse поддержка статических конфигураций сбора данных Prometheus для каждого коллектора, брокера или узла ClickHouse становилась неуправляемой.</p>
  <p id="a5da">Мы хотели:</p>
  <ul id="TphC">
    <li id="b34a"><strong>Динамическое автообнаружение</strong> — не требуется ручное изменение конфигурации при масштабировании экземпляров.</li>
    <li id="2212"><strong>Отказоустойчивость</strong> — метрические данные должны выдерживать кратковременные сбои Prometheus.</li>
    <li id="184f"><strong>Единый путь приема данных</strong> — объединение телеметрии на уровне инфраструктуры и приложения в один поток.</li>
  </ul>
  <p id="6ab0">Поэтому вместо того, чтобы позволить Prometheus напрямую опрашивать каждый endpoint, мы <strong>перевернули архитектуру</strong>: Prometheus получает метрики всего из одного OTEL-коллектора, который читает метрики из Kafka, куда отправляют их все компоненты конвейера<strong>.</strong></p>
  <h3 id="e36f">3.2 Примеры конфигураций коллектора</h3>
  <h3 id="6da3">На каждом хосте (узлы Kafka, ClickHouse, OTEL Collector):</h3>
  <p id="198d">Экспортирует метрики Prometheus в Kafka.</p>
  <pre id="HYlF" data-lang="bash">receivers:
  prometheus:
      config:
        global:
          scrape_interval: 30s
        scrape_configs:
          - job_name: otel-collector-binary
            static_configs:
              - targets: [&#x27;localhost:8888&#x27;] # OTEL internal metrics
  hostmetrics:
      collection_interval: 30s
      scrapers:
        cpu: {}
        load: {}
        memory: {}
        disk: {}
        filesystem: {}
        network: {}
processors:
  resourcedetection/aws:
      detectors: [ec2]
      timeout: 2s
      override: true
      ec2:
        tags:
          - aws:autoscaling:groupName
          - service_name
          - environment_name
          - cluster
          - provisioned-by-user
          - resource_type
exporters:
  kafka:
    brokers: [&quot;observibility-kafka.prod.local:&lt;port&gt;&quot;]
    topic: &quot;pipeline-metrics&quot;</pre>
  <h3 id="d78c">Центральный коллектор (обращённый к Prometheus):</h3>
  <p id="1cb2">Использует данные из Kafka, повторно выставляет метрики для Prometheus.</p>
  <pre id="11OS" data-lang="bash">receivers:
  kafka:
    brokers: [&quot;kafka-1:9092&quot;, &quot;kafka-2:9092&quot;]
    topic: &quot;pipeline-metrics&quot;

exporters:
  prometheus:
    endpoint: &quot;0.0.0.0:9464&quot;

service:
  pipelines:
    metrics:
      receivers: [kafka]
      exporters: [prometheus]</pre>
  <h3 id="348c">Конфигурация Прометея</h3>
  <p id="d7c6">Prometheus требуется только один таргет:</p>
  <pre id="Fxom" data-lang="bash">scrape_configs:
  - job_name: &#x27;otel-metrics-aggregator&#x27;
    static_configs:
      - targets: [&#x27;otel-central-collector:9464&#x27;]</pre>
  <p id="2a84">Это делает настройку <strong>полностью независимой от топологии</strong> — можно масштабировать Kafka, ClickHouse или OTEL-коллекторы вверх/вниз, не меняя YAML-конфигурацию Prometheus.</p>
  <h3 id="910f">3.3 Что мы отслеживаем</h3>
  <figure id="vJTG" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*NiDfMiTve_Fnk4jJbOUy0A.png" width="700" />
    <figcaption>Из множества показателей у нас есть лишь немногие оповещения и панели мониторинга для мониторинга конвейера.</figcaption>
  </figure>
  <figure id="QJes" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:1250/1*CEYx5VQ2V2rM3RCw8McE8Q.png" width="1000" />
    <figcaption>Просмотр показателей приемника OTEL в Grafana.</figcaption>
  </figure>
  <h2 id="cadf">4. Инструменты для отслеживания</h2>
  <p id="3047">Масштабное отслеживание требует тщательной координации между агентом OTEL и коллекторами, а также использования специальных инструментов для работы с устаревшим кодом. Вот обзор того, как мы справляемся с этим на нашей платформе.</p>
  <h3 id="e589">Обзор агента + коллектора</h3>
  <ul id="bOSq">
    <li id="c485"><strong>Агент OTEL</strong>: запускается вместе с приложениями (sidecar или in-process), перехватывая входящие и исходящие запросы для создания интервалов. Для HTTP, gRPC и поддерживаемых фреймворков агент автоматически инструментирует запросы.</li>
    <li id="4089"><strong>Сборщик OTEL</strong>: получает данные от агентов, выполняет батчинг, семплинг и обогащение, а также экспортирует их в Kafka для обеспечения устойчивости. Коллектор SigNoz из Kafka записывает данные в ClickHouse для хранения и визуализации.</li>
  </ul>
  <p id="f21c">Такое разделение гарантирует, что агенты остаются легкими, в то время как коллекторы выполняют тяжелую работу, такую ​​как батчинг, backpressure и  экспорт в больших масштабах.</p>
  <h3 id="9625">Кастомное инструментирование для устаревших систем</h3>
  <p id="264e">Некоторые из наших сервисов используют старые версии фреймворков и библиотек, которые не инструментируются автоматически OTEL:</p>
  <ul id="rL4c">
    <li id="e77b"><strong>Vert.x 3.9</strong>: Требуется специальный инструментарий для корректного захвата асинхронных циклов событий.</li>
    <li id="141b"><strong>Клиенты SQL</strong> (старые версии JDBC) и <strong>клиенты Redis</strong>: мы написали небольшие оболочки OTEL для захвата областей запросов/команд без изменения существующей бизнес-логики.</li>
  </ul>
  <p id="635d">Эти специальные инструменты имели решающее значение для поддержания сквозной видимости трассировки для критически важных устаревших потоков.</p>
  <h3 id="3448">Семплинг</h3>
  <p id="cac8">Мы применяем <strong>вероятностный семплинг 1%</strong> для трейсов:</p>
  <ul id="GpZZ">
    <li id="578a">Это означает, что только 1% всех запросов фиксируется как трейсы.</li>
    <li id="1a52">Это снижает нагрузку на систему, при этом позволяя проводить статистически значимый анализ ошибок и скачков задержки.</li>
    <li id="98d5">Даже при таком семплинге большинство ошибок фиксируются, так как спаны с ошибками чаще встречаются в запросах с высокой задержкой или сбоев.</li>
  </ul>
  <blockquote id="FaOI"><em>Полезный совет: <br />если уровень ошибок у вас крайне низок, рассмотрите <strong>tail-based sampling</strong> (сбор всех трейсов с ошибками) в сочетании с вероятностной выборкой, чтобы не пропустить критические события сбоя.</em></blockquote>
  <h3 id="c537">Метрики охвата</h3>
  <ul id="76YY">
    <li id="4c55">Все метрики интервала (задержка, количество ошибок, длительность) хранятся в ClickHouse.</li>
    <li id="9549">Это позволяет строить дашборды без необходимости повторной обработки необработанных трассировок.</li>
  </ul>
  <pre id="FWwy" data-lang="bash"># Sample signoz config for span metrics
signozspanmetrics/delta:
    aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA
    dimensions:
    - default: default
      name: service.namespace
    - default: default
      name: deployment.environment
    - name: signoz.collector.id
    dimensions_cache_size: 100000
    latency_histogram_buckets:
    - 100us
    - 1ms
    - 2ms
    - 6ms
    - 10ms
    - 50ms
    - 100ms
    - 250ms
    - 500ms
    - 1000ms
    - 1400ms
    - 2000ms
    - 5s
    - 10s
    - 20s
    - 40s
    - 60s
    metrics_exporter: signozclickhousemetrics</pre>
  <h3 id="43f3">Вспомогательные библиотеки OTEL</h3>
  <p id="8d5b">Для поддерживаемых библиотек OTEL предлагает богатый набор инструментов в <a href="https://github.com/open-telemetry/opentelemetry-java-contrib" target="_blank"><strong>opentelemetry-contrib</strong></a>. Он включает в себя автоматический инструментарий для баз данных, систем обмена сообщениями, веб-фреймворков и многого другого. Использование этих инструментов снижает потребность в специальных оболочках и обеспечивает совместимость с будущими версиями.</p>
  <h2 id="706c">5. Итоги и что дальше</h2>
  <p id="4973">В этой части мы рассмотрели общие принципы проектирования и эксплуатации платформы наблюдения промышленного уровня с использованием SigNoz, ClickHouse и OpenTelemetry. Ключевые выводы:</p>
  <ul id="N7Kw">
    <li id="b62e"><strong>Масштабируемая архитектура:</strong> разделение приема, обработки и хранения позволяет системе обрабатывать миллионы показателей и трассировок в секунду, оставаясь при этом устойчивой к скачкам нагрузки.</li>
    <li id="1174"><strong>Практические уроки настройки:</strong> такие конфигурации, как ограничения памяти коллектора OTEL, пакетирование Kafka и размеры буфера Lambda, имеют решающее значение для надежности и оптимизации затрат.</li>
    <li id="7622"><strong>Самонаблюдаемость:</strong> Инструментирование самого конвейера наблюдаемости обеспечивает динамический мониторинг метрик Prometheus и обратного давления, без необходимости управлять сотнями статических конфигураций сбора данных.</li>
    <li id="544d"><strong>Трейс-инструментирование:</strong> правильная настройка агента и коллектора, а также специализированный инструментарий для устаревших библиотек обеспечивают полноту распределённых трейсов, в то время как вероятностная выборка позволяет контролировать затраты на хранение и прием данных.</li>
  </ul>
  <p id="3fb4">Эта статья посвящена вопросам <strong>«почему»</strong> и <strong>«как» на архитектурном уровне</strong>. Мы надеемся, что она даст вам конкретные идеи и практические рекомендации по созданию и настройке собственного телеметрического стека.</p>
  <p id="1432"><strong>Что дальше (часть 2):</strong><br />В следующей публикации мы подробно рассмотрим <strong>сервер SigNoz, настройки коллектора и схемы ClickHouse</strong>. Вы получите подробную информацию о:</p>
  <ul id="ueY7">
    <li id="2749">Как коллекторы обрабатывают пакетирование, повторные попытки и обратное давление.</li>
    <li id="2d82">Шаблоны проектирования схемы Signoz <strong>*MergeTree</strong> для метрик и диапазонов с высокой кардинальностью, а также то, как она использует отпечатки для оптимизации хранения.</li>
    <li id="401c">Однобинарный UI-сервер SigNoz с компонентами alert-manager и query-builder.</li>
  </ul>
  <p id="e696">Оставайтесь с нами, если хотите узнать <strong>подробности, выходящие за рамки документации</strong>, — то, что делает платформу наблюдения за производством по-настоящему надежной и производительной. 👋</p>
  <p id="2adP">2 часть статьи на момент публикации перевода так и не вышлаю (примечание переводчика).</p>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="3HcO">Подписывайтесь на телеграм-канал <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ</a>, там еще больше полезной информации о мониторинге!</p>
  </section>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@monitorim_it/scaling-prometheus-metrics-with-grafana-mimir-step</guid><link>https://teletype.in/@monitorim_it/scaling-prometheus-metrics-with-grafana-mimir-step?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it</link><comments>https://teletype.in/@monitorim_it/scaling-prometheus-metrics-with-grafana-mimir-step?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it#comments</comments><dc:creator>monitorim_it</dc:creator><title>Масштабирование метрик Prometheus с помощью Grafana Mimir: пошаговая настройка</title><pubDate>Sun, 22 Mar 2026 13:51:54 GMT</pubDate><media:content medium="image" url="https://img1.teletype.in/files/46/bf/46bff762-4961-45a6-a0b4-2806a3f20227.png"></media:content><description><![CDATA[<img src="https://miro.medium.com/v2/resize:fit:875/1*YxSxgKTTmIWBnHnlUh4hVg.png"></img>Это перевод оригинальной статьи Scaling Prometheus Metrics with Grafana Mimir: Step-by-Step Setup and Demo.]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="k3gk">Это перевод оригинальной статьи <a href="https://medium.com/@sanikakotgire2003/scaling-prometheus-metrics-with-grafana-mimir-step-by-step-setup-and-demo-d6750e9693d2" target="_blank">Scaling Prometheus Metrics with Grafana Mimir: Step-by-Step Setup and Demo</a>.</p>
  </section>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="x7Gb">Перевод сделан специально для телеграм-канала <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ.</a> Подписывайтесь! Там еще больше полезных постов о мониторинге.</p>
  </section>
  <p id="3bed">Prometheus отлично подходит для мониторинга, но при масштабировании возникают реальные проблемы, такие как высокая кардинальность, долговременное хранение и высокая доступность. Вот тут-то и приходит на помощь Grafana Mimir.</p>
  <p id="26f9">На недавнем митапе <a href="https://www.linkedin.com/posts/sanika-kotgire-69b361234_grafana-mimir-prometheus-activity-7347309868296409088-jqn4?utm_source=social_share_send&utm_medium=android_app&rcm=ACoAADpt1JcBAf1J4LD6F6afuMIvFxaOrMpCOZI&utm_campaign=copy_link" target="_blank"><strong>Grafana &amp; Friends x Kubernetes </strong></a>я выступил с докладом и живой демонстрацией того, как Grafana Mimir может масштабировать метрики Prometheus.</p>
  <p id="faa3">В этой статье вы шаг за шагом узнаете обо всем, что я показал в демонстрации.</p>
  <figure id="pRaJ" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*YxSxgKTTmIWBnHnlUh4hVg.png" width="700" />
    <figcaption><strong>Настройка панели мониторинга Grafana Mimir в качестве источника данных TSDB.</strong></figcaption>
  </figure>
  <h2 id="11b4">Что мы рассмотрим</h2>
  <blockquote id="oJt9">Ограничения Prometheus<br />Введение в Grafana Mimir<br />Архитектура и компоненты Mimir<br />Конфигурация remote write из Prometheus → Mimir<br />Настройка Grafana → Mimir в качестве источника данных<br />Визуализация метрик + изучение административного интерфейса</blockquote>
  <h2 id="a59f">Почему именно Grafana Mimir?</h2>
  <p id="61cc">Prometheus — мощная система сбора метрик, но у неё есть следующие проблемы:</p>
  <ul id="swhd">
    <li id="55e7">Отсутствует встроенная кластеризация или высокая доступность.</li>
    <li id="1f43">Только локальное хранилище</li>
    <li id="f465">Интенсивное использование памяти при работе с данными высокой кардинальности.</li>
  </ul>
  <p id="a94b">Grafana Mimir — это <strong>высокомасштабируемый backend для долгосрочного хранения метрик Prometheus</strong>, который решает все эти проблемы.</p>
  <h2 id="4d28">Архитектура Mimir Architecture (краткий обзор)</h2>
  <figure id="TFEB" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:778/1*unJccEN94Vm68iFj74_okw.png" width="622" />
  </figure>
  <p id="bb30">Mimir подразделяется компоненты:</p>
  <ul id="TCRX">
    <li id="H0QY"><strong>Distributor</strong> — принимает remote write от Prometheus</li>
    <li id="jOg9"><strong>Ingester</strong> — временно хранит данные</li>
    <li id="pPJZ"><strong>Compactor</strong> — компактизирует блоки</li>
    <li id="nFEz"><strong>Querier</strong> — обрабатывает запросы</li>
    <li id="dLii"><strong>Store-gateway</strong> — извлекает блоки из объектного хранилища.</li>
  </ul>
  <p id="edae">Это делает Mimir горизонтально масштабируемым и совместимым с HA.</p>
  <h2 id="6a05">Пошаговая настройка</h2>
  <h3 id="3167">Запуск экземпляра EC2 (виртуальной машины)</h3>
  <p id="563b">Перед установкой чего-либо я запустил экземпляр Amazon EC2, который использовал в качестве демонстрационной среды.</p>
  <figure id="UDkV" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*3KCtsAXNMEiDV6d94p-VDQ.png" width="700" />
  </figure>
  <h3 id="ff07">Настройка Prometheus</h3>
  <figure id="N3XC" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*UVrNThpy1q3L8mgUnFpj1A.png" width="700" />
    <figcaption><strong>Статус Mimir TSDB</strong></figcaption>
  </figure>
  <figure id="k6Tg" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*sC9SYWEQqIKiOvWPvP3mjw.png" width="700" />
    <figcaption><strong>Prometheus Targets</strong></figcaption>
  </figure>
  <figure id="cYVd" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*VczPiFMuF6-cpLwYwGUlYQ.png" width="700" />
    <figcaption><strong>Собранные метрики через Grafana Mimir</strong></figcaption>
  </figure>
  <p id="7c6a">Скачайте и распакуйте Prometheus:</p>
  <pre id="0TeJ" data-lang="bash">wget https://github.com/prometheus/prometheus/releases/download/v2.52.0/prometheus-2.52.0.linux-amd64.tar.gz
tar -xvzf prometheus-2.52.0.linux-amd64.tar.gz
cd prometheus-2.52.0.linux-amd64</pre>
  <p id="59eb">Отредактируйте файл prometheus.yml, чтобы включить удаленную запись в Grafana Mimir:</p>
  <p id="7fd9">Добавьте удаленную запись</p>
  <blockquote id="8tD4">remote_write:<br />- url: <a href="http://localhost:9009/api/v1/push" target="_blank">http://localhost:9009/api/v1/push</a></blockquote>
  <figure id="09Z2" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*zk05SQ8i4RuPupP4lhlpZQ.png" width="700" />
    <figcaption><strong>Файл конфигурации Prometheus</strong></figcaption>
  </figure>
  <h3 id="d182">Настройка Grafana Mimir</h3>
  <p id="621d">Скачайте Mimir:</p>
  <pre id="ky9l" data-lang="bash">wget https://github.com/grafana/mimir/releases/download/v2.12.0/mimir-linux-amd64.zip
unzip mimir-linux-amd64.zip
chmod +x mimir</pre>
  <p id="a254">Создайте конфигурационный файл <code>demo.yaml</code> (базовая настройка для одного процесса):</p>
  <pre id="yijR" data-lang="bash"># Do not use this configuration in production.
# It is for demonstration purposes only.
multitenancy_enabled: false

blocks_storage:
  backend: filesystem
  bucket_store:
    sync_dir: /tmp/mimir/tsdb-sync
  filesystem:
    dir: /tmp/mimir/data/tsdb
  tsdb:
    dir: /tmp/mimir/tsdb

compactor:
  data_dir: /tmp/mimir/compactor
  sharding_ring:
    kvstore:
      store: memberlist

distributor:
  ring:
    instance_addr: 127.0.0.1
    kvstore:
      store: memberlist

ingester:
  ring:
    instance_addr: 127.0.0.1
    kvstore:
      store: memberlist
    replication_factor: 1

ruler_storage:
  backend: filesystem
  filesystem:
    dir: /tmp/mimir/rules

server:
  http_listen_port: 9009
  log_level: error

store_gateway:
  sharding_ring:
    replication_factor: 1</pre>
  <figure id="oYlh" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*L2njPFcv6bbIq_5E042qnA.png" width="700" />
    <figcaption><strong>Конфигурация Mimir</strong></figcaption>
  </figure>
  <h3 id="6798">Настройка Grafana</h3>
  <p id="2ecc">Запуск Grafana:</p>
  <pre id="u51z" data-lang="bash">sudo systemctl start grafana-server</pre>
  <p id="5a9f">Подключите Mimir к Grafana</p>
  <figure id="3ah0" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*tMyl7fu0ia12zg3NPwzpkA.png" width="700" />
    <figcaption><strong>Настройка Mimir в качестве источника данных</strong></figcaption>
  </figure>
  <ul id="xtUv">
    <li id="a0e8">Войдите в Grafana по адресу <code>http://&lt;ip&gt;:3000</code>(по умолчанию: admin/admin)</li>
    <li id="6bf6">Перейдите в <strong>Settings → Data Sources → Add Data Source</strong></li>
    <li id="d048">Выберите <strong>Prometheus</strong></li>
  </ul>
  <p id="d530">Теперь Grafana подключена к Mimir!</p>
  <h3 id="8ca8">Обзор административного интерфейса Mimir</h3>
  <figure id="hTGP" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*Le8XZxYiZq6-8eW07J52sQ.png" width="700" />
    <figcaption><strong>Административный интерфейс Grafana Mimir</strong></figcaption>
  </figure>
  <figure id="9kIP" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*_vR6HQyz3SFRdV2VOV02Kw.png" width="700" />
    <figcaption><strong>Статус сервисов</strong></figcaption>
  </figure>
  <figure id="bMch" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*ttI1hL04H9dhZleSMamvXQ.png" width="700" />
    <figcaption><strong>Статус Hash Ring</strong></figcaption>
  </figure>
  <figure id="hMyT" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:808/1*KtC2l2auW3zdhKVg7utzCA.png" width="646" />
    <figcaption><strong>Статус Ingester</strong></figcaption>
  </figure>
  <figure id="cRz3" class="m_custom">
    <img src="https://miro.medium.com/v2/resize:fit:875/1*pqIQ-YLdvc9Bve4B8MnueA.png" width="700" />
    <figcaption><strong>Grafana Mimir Memberlist — пары ключ-значение</strong></figcaption>
  </figure>
  <p id="1e7b">Эта конфигурация демонстрирует, насколько легко масштабировать Prometheus с помощью Grafana Mimir — без изменения способа сбора метрик.</p>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="YT9G">Подписывайтесь на телеграм-канал <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ</a>, там еще больше полезной информации о мониторинге!</p>
  </section>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@monitorim_it/what-kubernetes-logs-wont-tell-you-during-an-incid</guid><link>https://teletype.in/@monitorim_it/what-kubernetes-logs-wont-tell-you-during-an-incid?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it</link><comments>https://teletype.in/@monitorim_it/what-kubernetes-logs-wont-tell-you-during-an-incid?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=monitorim_it#comments</comments><dc:creator>monitorim_it</dc:creator><title>О чём логи Kubernetes не расскажут вам во время инцидента</title><pubDate>Sat, 21 Mar 2026 07:31:48 GMT</pubDate><description><![CDATA[Это перевод оригинальной статьи What Kubernetes Logs Won’t Tell You During an Incident.]]></description><content:encoded><![CDATA[
  <section style="background-color:hsl(hsl(24,  24%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="yuqC">Это перевод оригинальной статьи <a href="https://naushiljain.medium.com/what-kubernetes-logs-wont-tell-you-during-an-incident-d2f427197c80" target="_blank">What Kubernetes Logs Won’t Tell You During an Incident</a>.</p>
  </section>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="2fK5">Перевод сделан специально для телеграм-канала <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ.</a> Подписывайтесь! Там еще больше полезных постов о мониторинге.</p>
  </section>
  <p id="4295">Если вы достаточно долго используете Kubernetes в продакшене, то уже знаете этот ритуал:</p>
  <ol id="7luH">
    <li id="JZQS">Срабатывает алерт.</li>
    <li id="6m0b">Вы открываете логи.</li>
    <li id="jzlR">Они выглядят нормально.</li>
    <li id="mUGN">А продакшен всё ещё «горит».</li>
  </ol>
  <p id="7dfb">Это не плохая наблюдаемость. Это <strong>неоправданное доверие</strong>.</p>
  <p id="2212">Логи Kubernetes отлично показывают, <em>что, по мнению вашего приложения, произошло</em>. Но они совершенно не помогают понять, <strong>почему система ведет себя именно так</strong>. Именно в этот промежуток времени тратится большая часть времени впустую во время инцидентов.</p>
  <p id="9d40">Вот что обычно не показывают логи — и на что следует обратить внимание вместо них.</p>
  <h2 id="5142">1. В логах не отображается информация о нехватке ресурсов (пока не станет слишком поздно).</h2>
  <p id="a459">В ваших логах указано:</p>
  <blockquote id="ymlq">«Request processed successfully»</blockquote>
  <p id="8f81">Ваши пользователи говорят:</p>
  <blockquote id="m4Cf">«Приложение работает ужасно медленно».</blockquote>
  <p id="ad71">Чего вам не расскажут логи:</p>
  <ul id="ERMn">
    <li id="9eca">Ваши поды ограничены по CPU</li>
    <li id="71da">Ваши контейнеры технически &quot;работают&quot;.</li>
    <li id="226f">Ваши запросы находятся в очереди, а не завершаются с ошибкой.</li>
  </ul>
  <p id="e3ca">Ограничение использования CPU не приводит к сбоям в работе подов. Оно <strong>незаметно увеличивает задержку</strong>. Kubernetes будет спокойно поддерживать работоспособность вашего контейнера, получая при этом 20 мс ресурсов CPU каждые 100 мс.</p>
  <p id="22f6">К тому моменту, когда в логах появляются сообщения о таймаутах, ущерб уже нанесен.</p>
  <p id="6119"><strong>Что действительно помогает</strong></p>
  <ul id="c8xZ">
    <li id="068f">Метрики CPU контейнера и его ограничение</li>
    <li id="a28d">Нагрузка CPU на узле</li>
    <li id="c559">Процентили задержки запроса (не средние значения)</li>
  </ul>
  <p id="fc03">С трудом выработанное правило: если задержка высокая, а логи чисты, <strong>в первую очередь следует подозревать процессор</strong>.</p>
  <h2 id="c368">2. Логи не объясняют, почему поды перезапускаются</h2>
  <p id="7899">Вы видите:</p>
  <blockquote id="XPlz">«Container terminated with exit code 137»</blockquote>
  <p id="0919">Отлично. Это ничего полезного вам не скажет.</p>
  <p id="8d63">Логи не скажут:</p>
  <ul id="2uuj">
    <li id="3388">Был ли pod убит (OOM-killed) из-за нехватки ресурсов на узле</li>
    <li id="f2f9">Выселил ли его kubelet по собственной инициативе</li>
    <li id="88a8">Был ли ограничен другим нагрузкой другого процесса</li>
  </ul>
  <p id="396e">Логи контейнера обрываются внезапно — потому что контейнер так и не успел зафиксировать собственную гибель.</p>
  <p id="e4f5"><strong>Что действительно помогает</strong></p>
  <ul id="LIG0">
    <li id="7e03">Последнее состояние pod (OOMKilled или Evicted)</li>
    <li id="8be2">События, связанные с нехваткой памяти узла</li>
    <li id="2a79">Какие <em>ещё</em> pod одновременно вызвали всплеск потребления памяти?</li>
  </ul>
  <p id="a002">Моя давняя ошибка, допущенная на позднем этапе разработки: я<br />пытался исправить ошибки в приложении, когда реальная проблема заключалась <strong>в конфликтах за память на уровне узлов</strong>.</p>
  <h2 id="ac5e">3. Логи не показывают решения планировщика (scheduler)</h2>
  <p id="6e7f">Деплой «застрял».</p>
  <p id="2177">В логах отображается следующее:</p>
  <blockquote id="zY5n">«Scaled to 10 replicas»</blockquote>
  <p id="a70e">Но реально работает только 6 подов.</p>
  <p id="f0ae">В логах вы не узнаете:</p>
  <ul id="2jUC">
    <li id="3f14">Почему планировщик не может разместить оставшиеся pod</li>
    <li id="a50f">Какие ограничения препятствуют планированию?</li>
    <li id="179b">Провал упаковки в контейнеры произошел незаметно.</li>
  </ul>
  <p id="68d2">Планировщик не записывает в логи причины, по которым он отклонил узлы, так чтобы это было видно в логах приложения.</p>
  <p id="5d4d"><strong>Что действительно помогает</strong></p>
  <ul id="rJFW">
    <li id="3bd7">События планирования подов</li>
    <li id="d384">Доступные vs запрошенные ресурсы узла</li>
    <li id="18ff">Конфликты affinity и taint</li>
  </ul>
  <p id="0291">Суровая правда: большинство «ошибок Kubernetes» во время инцидентов связаны с <strong>математическими ошибками планировщика</strong>.</p>
  <h2 id="6f3a">4. Логи не фиксируют ухудшение качества сети.</h2>
  <p id="fa8a">Логи говорят:</p>
  <blockquote id="3MV9">«Request sent»</blockquote>
  <p id="f4c3">Они не говорят:</p>
  <ul id="6GSj">
    <li id="85ef">Разрешение DNS-запроса заняло 800 мс.</li>
    <li id="c8e1">Между узлами резко возросла потеря пакетов.</li>
    <li id="a106">Правила kube-proxy вышли из строя</li>
  </ul>
  <p id="87b7">С точки зрения приложения, всё работает нормально. С точки зрения пользователя, всё тормозит.</p>
  <p id="2720">Проблемы в сети сначала приводят к ухудшению качества, <em>а затем</em> к поломке.</p>
  <p id="01e9"><strong>Что действительно помогает</strong></p>
  <ul id="KiP3">
    <li id="6a39">метрики задержки DNS</li>
    <li id="58f3">Потеря пакетов между узлами</li>
    <li id="91ae">Частота повторных попыток подключения</li>
  </ul>
  <p id="03f6">Совет от опытного пользователя: если работа <em>нескольких сервисов</em><br />тормозит, перестаньте читать логи и <strong>начните проверять DNS и сеть</strong>.</p>
  <h2 id="f07d">5. Логи обманывают во время Rolling Updates</h2>
  <p id="6864">Во время rollout’ов логи могут вводить в заблуждение.</p>
  <p id="b15c">Вы увидите:</p>
  <blockquote id="9PJ2">«Pod started successfully»</blockquote>
  <p id="2810">Чего вам не расскажут логи:</p>
  <ul id="1urM">
    <li id="fa68">Проверки на готовность были пройдены слишком рано.</li>
    <li id="8013">Трафик попал в pod до того, как кеши прогрелись.</li>
    <li id="f4d6">Старые pod освобождались слишком медленно (или вообще не освобождались)</li>
  </ul>
  <p id="1f71">Приложение считает, что готово. Система — нет.</p>
  <p id="8c64"><strong>Что действительно помогает</strong></p>
  <ul id="DSCl">
    <li id="5dcd">Реальные показатели успешности трафика во время rollout</li>
    <li id="461a">Задержка готовности против фактической готовности</li>
    <li id="d177">Поведение балансировщика нагрузки при исчерпании ресурсов соединения</li>
  </ul>
  <p id="4fe2">Урок усвоен на горьком опыте: зелёное развертывание — это не то же самое, что <strong>безопасное внедрение</strong>.</p>
  <h2 id="ccee">6. Логи не показывают проблемы control plane</h2>
  <p id="d2fb">В логах вашего приложения мало информации.<br />Ваш кластер работает медленно.</p>
  <p id="7985">В логах вы не узнаете что:</p>
  <ul id="Mx7z">
    <li id="2e66">API-сервер ограничивает количество запросов.</li>
    <li id="ee67">Контроллеры отстают</li>
    <li id="b071">Watch-события задерживаются</li>
  </ul>
  <p id="dc5f">С точки зрения рабочей нагрузки, <em>проблема заключается в</em> Kubernetes — но Kubernetes не сообщает об этом вашему приложению.</p>
  <p id="ab25"><strong>Что действительно помогает</strong></p>
  <ul id="3pof">
    <li id="99e7">Задержка API-сервера</li>
    <li id="6d73">Метрики ограничения запросов</li>
    <li id="a900">Задержка reconcile контроллеров</li>
  </ul>
  <p id="c4e5">Если kubectl работает медленно во время инцидента — это сигнал, а не просто неудобство.</p>
  <h2 id="f9cd">7. Логи не показывают того, чего не произошло</h2>
  <p id="b418">Это самая опасная вещь.</p>
  <p id="8478">В логах показано, что произошло.<br />В них не показано:</p>
  <ul id="haxj">
    <li id="8cdf">Запросы, которые так и не дошли до pod</li>
    <li id="5eb5">Pod, которые так и не получили трафика</li>
    <li id="a2d7">Джобы, которые не запустились</li>
  </ul>
  <p id="edb8">Отсутствие логов редко рассматривают как данные — но во время инцидентов это часто главный сигнал.</p>
  <p id="534e"><strong>Что действительно помогает</strong></p>
  <ul id="Nk4x">
    <li id="3da0">Метрики трафика vs ожидаемый объём</li>
    <li id="f695">Коэффициенты отброса запросов upstream</li>
    <li id="48f3">Пропуски событий control plane</li>
  </ul>
  <p id="e0d1"><strong>Апгрейд инстинкта старшего инженера:</strong> когда логи пусты, спрашивай: что должно было залогироваться, но не залогировалось?</p>
  <h2 id="4fac">Главный вывод: логи — это запаздывающий сигнал</h2>
  <p id="ab3c">Логи показывают симптомы, а не причины.</p>
  <p id="5fdd">К тому моменту, когда логи «кричат»:</p>
  <ul id="0C4z">
    <li id="c526">Задержка уже плохая.</li>
    <li id="cb65">Пользователи уже это заметили</li>
    <li id="03c2">Инцидент уже идёт</li>
  </ul>
  <p id="334f">Опытные инженеры не перестают использовать логи — они <strong>перестают полагаться только лишь на них</strong>.</p>
  <h2 id="e3a0">Что я теперь проверяю до логов?</h2>
  <p id="356e">В каждом инциденте я неукоснительно следую одному и тому же списку правил:</p>
  <ul id="EMdT">
    <li id="0166">CPU и память на узлах</li>
    <li id="9273">События планирования подов</li>
    <li id="227c">Сигналы троттлинга и eviction-сигналы</li>
    <li id="b524">DNS и задержка сети</li>
    <li id="548a">Состояние API-сервера</li>
    <li id="bcf2">Несоответствие между трафиком и пропускной способностью</li>
  </ul>
  <p id="d31b">Только <em>после этого</em> я читаю логи — чтобы подтвердить, а не чтобы обнаружить.</p>
  <h2 id="1bd5">Финал</h2>
  <p id="239f">Если ваше реагирование на инцидент начинается и заканчивается логами, значит, отладка выполняется <strong>слишком поздно</strong>.</p>
  <p id="b132">Инциденты в Kubernetes происходят в пространстве <strong>между компонентами</strong> — scheduler, узлы, сеть, control plane — и логи никогда не предназначались для того, чтобы рассказывать эту историю.</p>
  <p id="8de8">Логи не лгут. Они просто не рассказывают всей правды.</p>
  <p id="865e">А в продакшене половина правды — это причина, почему простои длятся дольше, чем должны.</p>
  <section style="background-color:hsl(hsl(34,  84%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="Mm8U">Подписывайтесь на телеграм-канал <a href="https://t.me/monitorim_it" target="_blank">Мониторим ИТ</a>, там еще больше полезной информации о мониторинге!</p>
  </section>

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