Длинное Разрешение Dns В Kubernetes

Эта статья посвящена проблемам с DNS в Kubernetes, с которыми столкнулась наша команда.

Как оказывается, иногда проблема лежит гораздо глубже, чем кажется на первый взгляд.



Введение

Всегда наступает момент, когда обстоятельства вмешиваются в уже налаженную работу, заставляя нас что-то менять.

Поэтому наша небольшая команда была вынуждена перенести все используемые приложения в Kubernetes. Причин было много, объективных и не очень, но история не об этом.

Поскольку до этого никто активно не использовал Kubernetes, кластер несколько раз пересоздавался, из-за чего у нас не было времени оценить качество переданных приложений.

И вот, после четвертого переноса, когда все основные части уже заполнены, все контейнеры собраны и написаны все развертывания, можно проанализировать проделанную работу и, наконец, перейти к другим задачам.

11 часов, начало рабочего дня.

В системе мониторинга отсутствуют некоторые сообщения от одного из приложений.



Диагностика

Приложение было недавно перенесено в кластер и представляло собой простой рабочий, который раз в несколько минут лазил в базу, проверял ее на наличие изменений и, если они были, отправлял сообщение в шину.

Перед началом проверки и после ее завершения программа записывает сообщение в журнал.

Никакого параллелизма, никакой многозадачности, только один модуль с одним контейнером внутри.

При ближайшем рассмотрении выяснилось, что логи попадают в консоль, но в Elastic их уже нет. Немного о системе мониторинга: кластер Elasticsearch с тремя узлами, развернутый в том же Kubernetes, отображает с помощью Kibana. Логи отправлялись в Elastic с помощью пакета Serilog.Sinks.Elasticsearch, который отправлял запросы через обратный прокси на базе Nginx (изначально все приложения писались с расчетом на то, что Elasticsearch не будет использовать авторизацию, поэтому пришлось проявить креативность).

Проверка логов Nginx показала, что иногда запросы от приложения вообще отсутствуют. Быстрый поиск в Интернете не обнаружил подобных проблем среди пользователей Serilog, но сообщил мне, что в Serilog есть собственный журнал для регистрации внутренних ошибок.

Недолго думая подключаю его к самому Serilog:

  
  
  
  
   

Serilog.Debugging.SelfLog.Enable(msg => { Serilog.Log.Logger.Error($"Serilog self log: {msg}"); });

Делаю новую сборку, запускаю релиз, проверяю логи приложения.

В Кибане.



> Caught exception while preforming bulk operation to Elasticsearch: Elasticsearch.Net.ElasticsearchClientException: Maximum timeout reached while retrying request. Call: Status code unknown from: POST /_bulk

Сообщения логгера Elasticsearch о том, что он не может отправлять сообщения в Elasticsearch, вызывают у меня легкую истерику.

Но становится ясно, что HttpClient логгера периодически истекает по тайм-ауту при попытке отправить сообщение.

Не всегда, но достаточно часто, чтобы просто закрыть глаза на проблему.

В этот момент возникает стойкое ощущение, что проблема лежит гораздо глубже, чем казалось изначально.

В курилке обсуждаю варианты с коллегой.

Коллега вспоминает, что мы до сих пор не перенесли в Kubernetes ни один API, потому что запросы к нему иногда начинали занимать 5–10 секунд вместо 100–150 мс на местах.

Логика его работы как раз и подразумевала доступ к сторонним сервисам по HTTP. Тогда все списали на географическую удаленность кластера от этих самых сторонних сервисов и перенос отложили до лучших времен.

Начинает вырисовываться картина — периодически, но не всегда, при выполнении HTTP-запроса из контейнера запрос начинает идти неприлично долго.

Чтобы убедиться, что проблема не привязана к конкретному приложению или фреймворку, в первом попавшемся контейнере я запускаю bash-скрипт, который в цикле опрашивает google.com и отображает время запроса:

while true; do curl -w "%{time_total}\n" -o /dev/null -s " https://google.com/ "; sleep 1; done

Догадка оказалась верной - спорадически начинают появляться те самые задержки в 5 секунд.

Длинное разрешение DNS в Kubernetes

Чувствуя, что решение проблемы близко, собираю тест под: Описание тестового модуля

apiVersion: v1 kind: ConfigMap metadata: name: ubuntu-test-scripts data: loop.sh: |- apt-get update; apt-get install -y curl; while true; do echo $(date)'\n'; curl -w "@/etc/script/curl.txt" -o /dev/null -s " https://google.com/ "; sleep 1; done curl.txt: |- lookup: %{time_namelookup}\n connect: %{time_connect}\n appconnect: %{time_appconnect}\n pretransfer: %{time_pretransfer}\n redirect: %{time_redirect}\n starttransfer: %{time_starttransfer}\n total: %{time_total}\n --- apiVersion: v1 kind: Pod metadata: name: ubuntu-test labels: app: ubuntu-test spec: containers: - name: test image: ubuntu args: [/bin/sh, /etc/script/loop.sh] volumeMounts: - name: config mountPath: /etc/script readOnly: true volumes: - name: config configMap: defaultMode: 0755 name: ubuntu-test-scripts

Суть пода в том, чтобы раз в секунду в бесконечном цикле делать запрос к google.com с помощью Curl. Результаты запроса нас не интересуют, поэтому мы перенаправляем их в /dev/null и выводим подробные метрики из curl на консоль.

В качестве адреса можно указать что угодно, включая сервисы, развернутые в самом кластере.

Главное условие — оно должно быть указано как доменное имя и разрешено для использования в DNS-кластере (почему именно так, ниже).

Конфигурация написана для Kubernetes 1.14; другие версии могут потребовать незначительных изменений.

Запустим его и посмотрим его логи:

Длинное разрешение DNS в Kubernetes

Из логов видно, что основная проблема — долгий поиск DNS, занимающий 99% времени запроса — те самые 5 секунд. Проблема может возникать каждые два запроса или не проявляться в течение нескольких минут.

Диагностика

Еще раз заходим в Интернет. Окей, Google — DNS разрешает кубернеты за 5 секунд. Google мгновенно возвращает дюжину статей и сообщений в блогах, большинство из которых ссылаются на одну из двух публикаций: Вы даже можете найти открытая проблема в репозитории weave .

Из них можно узнать, что проблема в следующем:

  1. Она существует уже довольно давно (самое раннее упоминание, которое я смог найти, было в 2017 году, но проблема, вероятно, существовала почти всегда) и до сих пор не имеет стабильно работающего решения.

  2. Это проявляется во внезапном увеличении времени поиска DNS. В некоторых случаях задержка может достигать 10-15 секунд.
  3. Происходит, если один VIP обращается к нескольким DNS-серверам, скрытым за DNAT.
  4. Не зависит от используемого сетевого плагина.

Корень проблемы — возникновение состояний гонки в механизмах отслеживания Linux при использовании SNAT и DNAT. Возможность этой проблемы Тобиас Клаусманн обдуманный еще в 2009 году был представлен Linux 5.0. два пластырь , уменьшая вероятность этого события.

Однако в случае с Kubernetes DNS условия гонки продолжают возникать.

При использовании glibc (образы Ubuntu, Debian и т. д.) это работает следующим образом:

  1. glibc использует один и тот же сокет UDP для параллельных запросов (A и AAAA).

    Поскольку UDP является протоколом без установления соединения, вызов Connect(2) не отправляет никаких пакетов, поэтому в таблице conntrack не создается никакой записи.

  2. DNS-сервер в Kubernetes доступен через VIP с использованием правил DNAT в iptables.
  3. Во время трансляции DNAT ядро вызывает перехватчики netfilter в следующем порядке: а.

    nf_conntrack_in: создает хэш-объект conntrack и добавляет его в список неподтвержденных записей.

    б.

    nf_nat_ipv4_fn: транслирует и обновляет кортеж conntrack. в.

    nf_conntrack_confirm: подтверждает запись, добавляет ее в таблицу.

  4. Два параллельных запроса UDP конкурируют за подтверждение входа.

    Кроме того, они используют разные экземпляры DNS-серверов.

    Один из них выигрывает гонку, другой проигрывает. Из-за этого увеличивается счётчик Insert_failed, запрос теряется и таймауты.



Решение

Готового универсального решения до сих пор нет, но есть несколько возможных обходных путей:
  1. Используйте один DNS-сервер для каждого узла и укажите его в качестве сервера имен.

    Может быть использован Локальный DNS-кеш

  2. При использовании сетевого плагина Weave с помощью tc(8) добавить искусственную микрозадержку ко всем DNS-запросам AAAA
  3. Добавьте флаг повторного открытия по одному запросу в resolv.conf.
В нашем случае был выбран третий вариант, так как он наиболее прост в реализации и не требует глубоких вмешательств в стандартные настройки кластера.

Начиная с Kubernetes версии 1.9 в конфигурации пода появился блок dnsConfig, отвечающий за генерацию файла resolv.conf. Добавив следующий блок в описание модуля, мы заставим glibc использовать разные сокеты для запросов A и AAAA, тем самым избегая состояния гонки.



spec: dnsConfig: options: - name: single-request-reopen

К сожалению, именно у этого метода есть одно ограничение — если приложение не использует glibc (например, это образ на основе alpine, использующий musl), то файл resolv.conf будет игнорироваться.

P.S. В нашем случае, чтобы автоматизировать этот процесс, мы написали простой мутирующий вебхук, который автоматически устанавливает этот раздел конфигурации для всех новых подов в кластере.

К сожалению, не могу предоставить код. Теги: #linux #Kubernetes #docker #DevOps #настройка Linux

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.