Слабые тесты в вашем CI — это кошмар.
Вы не можете сказать, что ваш новый код кто-то сломал или просто эти тесты снова не работают. Поэтому в любое время, когда мы видим странные случайные сбои в CI для нашего проекта с открытым исходным кодом, Adapt, мы пытаемся найти виновника как можно скорее. Это история о том, как мы обнаружили, что (случайно) наводнили наш DNS-сервер трафиком и как мы использовали DNS-кеш в Docker для решения этой проблемы.
Фон
Один из проектов с открытым исходным кодом, над которым я работаю, AdaptJS может развертывать приложения в нескольких облаках и технологиях, поэтому существует множество системных тестов и сквозных тестов с Docker, Kubernetes, AWS, Google Cloud и другими подобными технологиями.
Мы интенсивно используем Docker в наших тестах, поэтому в итоге мы создали множество недолговечных контейнеров, которые запускаются, выполняют некоторую работу, например, создают или устанавливают приложение, а затем удаляются. И по мере того, как мы добавляли все больше и больше этих тестов, мы начали видеть, что ранее стабильные системные тесты случайно проваливались в CI.
Симптом: время ожидания теста
Первые симптомы, которые мы увидели, были тайм-ауты теста У нас достаточно короткие тайм-ауты во многих наших сквозных тестах, поэтому мы можем определить, вдруг ли новый код заставляет конечных пользователей работать дольше. Но теперь тест, который обычно занимает полсекунды, иногда занимает 5,5 секунд.
Дополнительные 5 секунд были отличной подсказкой — 5 секунд звучали так, будто это могло быть какое-то время. Вооружившись этой догадкой, мы оглянулись назад на все кажущиеся случайными неудачи тестов и нашли общий поток: все они были тестами, которые инициировали сетевые запросы. Мы также заметили несколько тестов, которые потерпели неудачу еще дольше … всегда с шагом 5 секунд.
Здесь не было слишком много сетевых протоколов, которые могли бы быть задействованы, поэтому некоторое быстрое поиск в Google указало нам правильное направление. Тайм-аут по умолчанию для запросов DNS-сервера в Linux составляет 5 секунд .
Чтобы увидеть, что происходит с DNS, мы нашли, пожалуй, самый важный инструмент для отладки сетевых проблем в Linux: tcpdump . (Или, если вы предпочитаете версию с графическим интерфейсом, wireshark также хорош .) Мы запустили tcpdump на хост-системе (экземпляр Amazon Workspaces Linux) и использовали фильтр для просмотра трафика DNS:
1 2 3 4 5 |
$ tcpdump -n -i eth1 port 53 11:35:59.474735 IP 172.16.0.131.54264 > 172.16.0.119.domain: 64859+ AAAA? registry-1.docker.io. (38) 11:35:59.474854 IP 172.16.0.131.49631 > 172.16.0.119.domain: 43524+ A? registry-1.docker.io. (38) 11:35:59.476871 IP 172.16.0.119.domain > 172.16.0.131.49631: 43524 8/0/1 A 34.197.189.129, A 34.199.40.84, A 34.199.77.19, A 34.201.196.144, A 34.228.211.243, A 34.232.31.24, A 52.2.186.244, A 52.55.198.220 (177) 11:35:59.476957 IP 172.16.0.119.domain > 172.16.0.131.54264: 64859 0/1/1 (133) |
Первое, что мы заметили, было то, что мы генерировали огромный поток DNS-запросов к DNS-серверу AWS по умолчанию для нашего VPC. Выглядело, как будто все эти недолговечные контейнеры имели тенденцию делать кучу DNS-запросов при запуске по разным причинам. Далее мы заметили, что некоторые из этих DNS-запросов просто остались без ответа.
Общие DNS-серверы довольно часто применяют ограничения скорости, чтобы один пользователь не мог снизить производительность для всех остальных. Здесь мы подозревали, что DNS-серверы AWS делают именно это. Мы не смогли найти способ подтвердить, действительно ли мы достигли ограничения скорости AWS, но нам показалось разумным не делать DoS нашему DNS-серверу.
Решение: кеш Docker DNS, использующий dnsmasq
Чтобы изолировать DNS-трафик внутри хоста, нам нужен был локальный DNS-сервер, который выполнял бы функции кеша. Отличным выбором для такого кеша является dnsmasq . Он надежен, широко используется и очень прост в настройке. И поскольку все наши тесты выполняются внутри контейнеров Docker, имеет смысл запускать DNS-сервер и в Docker.
Основная идея довольно проста: запустить контейнер dnsmasq в качестве DNS-кэша в сети хоста Docker, а затем запустить наши тестовые контейнеры с —dns опция, указывающая на IP-адрес контейнера кэша.
Вот dns_cache скрипт, который запускает контейнер кеша DNS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
#!/usr/bin/env bash : "${IMAGE:=andyshinn/dnsmasq:2.76}" : "${NAME:=dnsmasq}" : "${ADAPT_DNS_IP_FILE:=/tmp/adapt_dns_ip}" # Get IP address for an interface, as visible from inside a container # connected to the host network interfaceIP() { # Run a container and get ifconfig output from inside # We need the ifconfig that will be visible from inside the dnsmaq # container docker run --rm --net=host busybox ifconfig "$1" 2>/dev/null | \ awk '/inet /{print(gensub(/^.*inet (addr:)?([0-9.]+)\s.*$/, "\\2", 1))}' } if docker inspect --type container "${NAME}" >& /dev/null ; then if [ -f "${ADAPT_DNS_IP_FILE}" ]; then # dnsmasq is already started cat "${ADAPT_DNS_IP_FILE}" exit 0 else echo DNS cache container running but file ${ADAPT_DNS_IP_FILE} does not exist. >&2 exit 1 fi fi # We only support attaching to the default (host) bridge named "bridge". DOCKER_HOST_NETWORK=bridge # Confirm that "bridge" is the default bridge IS_DEFAULT=$(docker network inspect "${DOCKER_HOST_NETWORK}" --format '{{(index .Options "com.docker.network.bridge.default_bridge")}}') if [ "${IS_DEFAULT}" != "true" ]; then echo Cannot start DNS cache. The Docker network named \"${DOCKER_HOST_NETWORK}\" does not exist or is not the default bridge. >&2 exit 1 fi # Get the Linux interface name for the bridge, typically "docker0" INTF_NAME=$(docker network inspect "${DOCKER_HOST_NETWORK}" --format '{{(index .Options "com.docker.network.bridge.name")}}') if [ -z "${INTF_NAME}" ]; then echo Cannot start DNS cache. Unable to determine default bridge interface name. >&2 exit 1 fi # Get the IP address of the bridge interface. This is the address that # dnsmasq will listen on and other containers will send DNS requests to. IP_ADDR=$(interfaceIP "${INTF_NAME}") if [ -z "${IP_ADDR}" ]; then echo Cannot start DNS cache. Docker bridge interface ${INTF_NAME} does not exist. >&2 exit 1 fi # Run the dnsmasq container. The hosts's /etc/resolv.conf configuration will # be used by dnsmasq to resolve requests. docker run --rm -d --cap-add=NET_ADMIN --name "${NAME}" --net=host -v/etc/resolv.conf:/etc/resolv.conf "${IMAGE}" --bind-interfaces --listen-address="${IP_ADDR}" --log-facility=- > /dev/null if [ $? -ne 0 ]; then echo Cannot start DNS cache. Docker run failed. exit 1 fi # Remember what IP address to use as DNS server, then output it. echo ${IP_ADDR} > "${ADAPT_DNS_IP_FILE}" echo ${IP_ADDR} |
Помимо запуска контейнера (если он еще не запущен), скрипт выводит IP-адрес контейнера кеша. Мы будем использовать это в командной строке любых других контейнеров, которые мы запускаем. Сценарий также гарантирует, что dnsmasq прослушивает только DNS-запросы в Docker (на интерфейсе моста Docker), так что есть небольшая дополнительная работа для определения IP-адреса для прослушивания.
Вот пример того, как запустить кэш DNS, запоминая IP-адрес в переменной DNS_IP а затем запустить другой контейнер, который будет использовать кэш.
1 2 |
$ DNS_IP=$(dns_cache) $ docker run --dns ${DNS_IP} --rm busybox ping -c1 adaptjs.org |
Проверка работоспособности кеша
После того, как мы начали использовать кеш в нашем тестировании, количество DNS-запросов, отправленных хост-системой на DNS-сервер AWS, сократилось до небольшого уровня. Мы также подтвердили, что кэш работает правильно, проверив статистику dnsmasq. Отправка SIGUSR1 dnsmasq заставляет его печатать статистику в свой журнал :
1 2 3 4 5 6 7 8 |
$ docker kill -s USR1 dnsmasq $ docker logs dnsmasq dnsmasq[1]: cache size 150, 1085/4664 cache insertions re-used unexpired cache entries. dnsmasq[1]: queries forwarded 1712, queries answered locally 3940 dnsmasq[1]: queries for authoritative zones 0 dnsmasq[1]: server 172.16.0.119#53: queries sent 1172, retried or failed 0 dnsmasq[1]: server 172.16.1.65#53: queries sent 252, retried or failed 0 dnsmasq[1]: server 172.16.0.2#53: queries sent 608, retried or failed 0 |
И самое главное, мы увидели резкое сокращение времени ожидания системных тестов, и наши тесты CI стабилизировались.
Эта проблема заняла некоторое время, чтобы выследить. Но поддержание здорового состояния КИ чрезвычайно важно. Если у вас слишком много случайных неудачных тестов, разработчики склонны игнорировать результаты CI и выдвигать потенциально испорченный код.
Таким образом, несмотря на то, что отслеживание этих сбоев занимало много времени, учитывая простоту исправления, оно определенно стоило вложений.