Эта статья посвящена проблемам с 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 секунд.
Чувствуя, что решение проблемы близко, собираю тест под: Описание тестового модуля 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, занимающий 99% времени запроса — те самые 5 секунд. Проблема может возникать каждые два запроса или не проявляться в течение нескольких минут.
Диагностика
Еще раз заходим в Интернет. Окей, Google — DNS разрешает кубернеты за 5 секунд. Google мгновенно возвращает дюжину статей и сообщений в блогах, большинство из которых ссылаются на одну из двух публикаций:- Причина необъяснимых тайм-аутов соединения в Kubernetes/Docker
- DNS-запросы в течение 5–15 секунд в Kubernetes
Из них можно узнать, что проблема в следующем:
- Она существует уже довольно давно (самое раннее упоминание, которое я смог найти, было в 2017 году, но проблема, вероятно, существовала почти всегда) и до сих пор не имеет стабильно работающего решения.
- Это проявляется во внезапном увеличении времени поиска DNS. В некоторых случаях задержка может достигать 10-15 секунд.
- Происходит, если один VIP обращается к нескольким DNS-серверам, скрытым за DNAT.
- Не зависит от используемого сетевого плагина.
Однако в случае с Kubernetes DNS условия гонки продолжают возникать.
При использовании glibc (образы Ubuntu, Debian и т. д.) это работает следующим образом:
- glibc использует один и тот же сокет UDP для параллельных запросов (A и AAAA).
Поскольку UDP является протоколом без установления соединения, вызов Connect(2) не отправляет никаких пакетов, поэтому в таблице conntrack не создается никакой записи.
- DNS-сервер в Kubernetes доступен через VIP с использованием правил DNAT в iptables.
- Во время трансляции DNAT ядро вызывает перехватчики netfilter в следующем порядке:
а.
nf_conntrack_in: создает хэш-объект conntrack и добавляет его в список неподтвержденных записей.
б.
nf_nat_ipv4_fn: транслирует и обновляет кортеж conntrack. в.
nf_conntrack_confirm: подтверждает запись, добавляет ее в таблицу.
- Два параллельных запроса UDP конкурируют за подтверждение входа.
Кроме того, они используют разные экземпляры DNS-серверов.
Один из них выигрывает гонку, другой проигрывает. Из-за этого увеличивается счётчик Insert_failed, запрос теряется и таймауты.
Решение
Готового универсального решения до сих пор нет, но есть несколько возможных обходных путей:- Используйте один DNS-сервер для каждого узла и укажите его в качестве сервера имен.
Может быть использован Локальный DNS-кеш
- При использовании сетевого плагина Weave с помощью tc(8) добавить искусственную микрозадержку ко всем DNS-запросам AAAA
- Добавьте флаг повторного открытия по одному запросу в 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
-
Почему Ваш Email-Маркетинг Неэффективен?
19 Oct, 24 -
Конференция Думп-2014: Секция «Тестирование»
19 Oct, 24