HimeraSearchDB
Carding_EbayThief
triada
CrackerTuch
JustinSun

НОВОСТИ Отладка высоконагруженных Golang-приложений или как мы искали проблему в Kubernetes, которой не было

BDFpromo
Оффлайн

BDFpromo

.
.
Регистрация
23.09.18
Сообщения
12.347
Реакции
176
Репутация
0
В современном мире Kubernetes-облаков, так или иначе, приходится сталкиваться с ошибками в программном обеспечении, которые допустил не ты и не твой коллега, но решать их придется тебе. Данная статья, возможно, поможет новичку в мире Golang и Kubernetes понять некоторые способы отладки своего и чужого софта.

okxxpinnrmycv6thscy9dusxm5u.jpeg


Меня зовут Виктор Ягофаров, я занимаюсь развитием Kubernetes-облака в компании ДомКлик, и сегодня хочу рассказать о том как мы решили проблему с одним из ключевых компонентов нашего production k8s (Kubernetes) кластера.

В нашем боевом кластере (на момент написания статьи):
— запущено 1890 pod'ов и 577 сервисов (количество реальных микросервисов тоже в районе этой цифры)
Ingress-контроллеры обслуживают около 6k RPS и примерно столько же идёт мимо Ingress сразу в hostPort.



Проблема

Несколько месяцев назад наши pod'ы начали испытывать проблему с разрешением DNS-имён. Дело в том, что DNS работает, в основном, по UDP, а в ядре Linux есть с conntrack и UDP. DNAT при обращении на сервисные адреса k8s Service только усугубляет проблему с . Стоит добавить, что в нашем кластере на момент проблемы было около 40k RPS в сторону DNS-серверов, CoreDNS.

57ce4989783892aee49e651ad7d564b6.jpg


Было принято решение использовать специально созданный сообществом локальный кэширующий DNS-сервер (nodelocaldns) на каждой worker-ноде кластера, который всё еще находится в beta и призван решить . Если вкратце: избавляемся от UDP при коннекте к кластерному DNS, убираем NAT, добавляем дополнительный слой кэширования.

В первую итерацию внедрения nodelocaldns мы использовали версию 1.15.4 (не путать с версией куба), которая шла в комплекте с «kubernetes-инсталлятором» Kubespray – речь идёт о нашем форке форка от компании Southbridge.

Почти сразу же после внедрения начались проблемы: текла память, и происходил рестарт подов по memory limits (OOM-Kill). На момент рестарта такого пода терялся весь трафик на хосте, так как во всех подах /etc/resolv.conf указывал именно на IP-адрес nodelocaldns.

Эта ситуация решительно всех не устраивала, и наша команда OPS предприняла ряд мер, чтобы ее устранить.

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

Ищем решение

Итак, поехали!

На dev кластер была выкачена версия 1.15.7, которая уже считается beta, а не alpha как 1.15.4, но на деве нет такого трафика в DNS (40k RPS). Печально.

По ходу дела мы отвязали nodelocaldns от Kubespray и написали специальный для более удобной выкатки. Заодно написали playbook для Kubespray, который позволяет менять настройки kubelet, не переваривая весь стейт кластера по часу; причем, делать это можно точечно (проверяя сначала на небольшом количестве нод).

Далее, мы выкатили версию nodelocaldns 1.15.7 на прод. Ситуация, увы, повторилась. Память текла.

В официальном была версия с тэгом 1.15.8, но я почему-то не смог сделать docker pull на эту версию и посчитал, что раз еще не собрали официальный Docker-образ – значит версию эту использовать не стоит. Это важный момент, и мы к нему еще вернемся.

Отладка: этап 1

Я долго не мог понять, как вообще собрать свою версию nodelocaldns в принципе, так как Makefile из репы валился с непонятными ошибками изнутри докер-образа, а я не очень понимал, как хитро собрать Go-проект с govendor, разложенным странным образом по директориям сразу для нескольких разных вариантов DNS-серверов. Всё дело в том, что Go я начинал изучать, когда уже появилось .

С проблемой мне очень помог справиться Павел Селиванов , за что ему огромное спасибо. Удалось собрать свою версию.

Далее мы прикрутили профайлер pprof, протестировали сборку на деве и выкатили в прод.

Коллега из команды Chat очень помог разобраться с профилированием так, чтобы можно было удобно цепляться через URL CLI утилитой pprof и изучать память и треды процесса с помощью интерактивных меню в браузере, за что ему тоже огромное спасибо.

На первый взгляд, исходя из вывода профайлера, у процесса было всё хорошо — бОльшая часть памяти выделялась на стеке и, вроде бы, использовалась Go-рутинами постоянно.

Но в какой-то момент стало понятно, что у «плохих» подов nodelocaldns было активно слишком много тредов по сравнению со «здоровыми» экземплярами. И треды никуда не девались, а продолжали висеть в памяти. В этот момент подтвердилась догадка Павла Селиванова о том, что «текут треды».

abcafe2a1a13db47c51bbafbd0c78d9d.jpg


Отладка: этап 2

Стало интересно, почему это происходит (текут треды), и начался следующий этап изучения процесса nodelocaldns.

Статический анализатор кода показал, что есть некие проблемы как раз на этапе создания тредов в , которая используется в nodelocaldns (её инклудит CoreDNS, который инклудится nodelocaldns'ом). Как я понял, в некоторых местах передаются не указатели на структуру, а копия их значений.

Было решено сделать coredump «плохого» процесса с помощью утилиты gcore и посмотреть что там внутри.

Потыкав в coredump с помощью gdb-подобного инструмента dlv я осознал его мощь, но понял, что причину искать таким образом буду очень долго. Поэтому, я загрузил coredump в IDE Goland и проанализировал состояние памяти процесса.

Отладка: этап 3

Было очень интересно изучать структуры программы, видя код, который их создаёт. Минут за 10 стало понятно, что множество go-рутин создают некую структуру для TCP- соединений, помечает их как false и никогда не удаляет (помним про 40k RPS?).

f7c83f6f00e3cddb93f611b0788001ca.jpg

9f77ee8eebd04646549bc22b01cd0747.jpg


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

Также, из coredump по IP-адресам в этих структурах стал известен виновник такого количества RPS (спасибо, что помогли найти узкое место в нашем кластере :).

Решение

Во время борьбы с этой проблемой я обнаружил с помощью коллег из сообщества Kubernetes, что официальный Docker-образ nodelocaldns 1.15.8 всё же существует (а у меня на самом деле кривые руки и я как-то неправильно сделал docker pull, либо WIFI шалил в момент pull'а).

В данной версии сильно «апнуты» версии библиотек, которые он использует: конкретно «апнулась» примерно аж на 20 версий вверх!

Мало того, в новой версии уже есть поддержка профилирования через pprof и включается через Configmap, ничего пересобирать не нужно.

Была выкачена новая версия сначала в dev, а потом в прод.
Ииии… Победа!
Процесс стал возвращать свою память в систему и проблемы прекратились.

На графике ниже можно видеть картину: «DNS курильщика vs. DNS здорового человека».

1c71a0cb503ca04b4dc6aab77100c73a.jpg


Выводы

Вывод здесь простой: перепроверяй что делаешь по несколько раз и не гнушайся помощью сообщества. В итоге, мы потратили на проблему на несколько дней времени больше, чем могли бы, зато получили отказоустойчивую работу DNS в контейнерах. Спасибо, что дочитали до этого момента :)

Полезные ссылки:

1.
2.
3.
 
Сверху Снизу