Cgo-резолвер vs Go-резолвер
Начнём немного издалека. Будет здорово, если вы освежите знания о том, как работает DNS Lookup в Linux. В данной статье я будут рассматривать версию Go 1.16.В Go можно использовать две реализации резолверов: Go и Cgo.
Cgo использует системный резолвер, а Go — резолвер, написанный на Go. Казалось бы, они должны работать одинаково, но на самом деле есть различия. И какой включится по умолчанию, не очень очевидно, даже несмотря на документацию и код.
Различия в работе резолверов как раз и определяют, какое количество запросов будет сгенерировано в том или ином случае. Моей целью было выяснить, в каком случае стоит запустить один резолвер, а в каком — другой, и стараться учитывать это на старте проектов.
Итак, первым делом нам нужно понять, какой резолвер будет использоваться в конкретном случае. Это сильно зависит от среды, в которой запускается приложение, а иногда даже от имени хоста. Я использую преимущественно macOS и Linux, поэтому буду рассматривать только их.
В основном я сталкивался с такими внешними факторами, влияющими на выбор резолвера, как CGO_ENABLED и опции, используемые в resolv.conf и nsswitch.conf. Но не только с ними.
CGO_ENABLED
Tables | CGO_ENABLED = 0 | CGO_ENABLED = 1 |
Linux | go | it depends =) |
MacOs | go | cgo |
resolv.conf
Будет включаться Cgo-реализация, если в resolv.conf есть опции, кроме следующих:- ndots
- timeout
- attempts
- rotate
- single-requests
- single-requests-reopen
- use-vc, usevc, tcp
Тут включится Go-резолвер:
$ cat /etc/resolv.conf
nameserver 127.0.0.53
А тут уже будет работать Cgo-резолвер (это пример с десктопной Ubuntu):
$ cat /etc/resolv.conf
nameserver 127.0.0.53
options edns0 trust-ad
nsswitch.conf
Что касается nsswitch.conf, то, если файл отсутствует, включается Go-резолвер.С таким nsswitch включается реализация на Go (это файл в контейнере ubuntu:focal):
passwd: compat
group: compat
shadow: compat
gshadow: files
hosts: files dns
networks: files
protocols: db files
services: db files
ethers: db files
rpc: db files
netgroup: nis
У меня на десктопной Ubuntu было вот такое: hosts: files dns mymachines. С такой конфигурацией включается Cgo-резолвер. Код проверок опций в nsswitch.
Принудительное включение резолвера
Можно с помощью env-переменной GODEBUG принудительно включать нужный резолвер:GODEBUG=netdns=cgo
GODEBUG=netdns=go
На самом деле, есть ещё много вещей, которые оказывают влияние на выбор резолвера. Но я в повседневной практике с ними не сталкивался. Если интересно, можно изучить код.
Проверка какой резолвер включается
Напишем простую программу, которая сделает GET-запрос:req, err := http.NewRequest("GET", url, nil)
if err != nil {
panic(err)
}
_, err = http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
Запускаем с дебагом DNS в системе, где по умолчанию выбирается Cgo. Выглядит логично:
Добавляем резолв ещё одного домена: my.local.
Принудительно указываем Go-резолвер. Пока ещё логично:
Теперь запускаем программу в системе, где Go-резолвер будет выбираться по умолчанию:
WAT?! Почему-то для домена my.local включается реализация Cgo-резолвера, хотя для golang.org выбрался Go-резолвер. В коде Go-резолвера есть условие для доменов .local. Поэтому я рекомендую делать реальные запросы и сниффать DNS-запросы через tcpdump -i any -nnn port 53.
Подводные камни
Я нашёл примеры нескольких ситуаций, которые влияют на количество запросов. Это IPv6, опция ndots в resolv.conf и резолвинг localhost.IPv6
В Go-резолвере всегда делается два запроса в DNS: A и AAAA (даже если вы не используете IPv6). Если у вас нет опции single-request в resolv.conf, они выполняются параллельно, но всё же это дополнительные запросы.Чтобы решить эту проблему, можно использовать кастомный DialContext. Внутри исходников http.Transport зашито использование TCP, а нам надо пробросить значение tcp4:
Посмотрим, есть ли сейчас АААА-запросы в DNS. При использовании Cgo-резолвера дополнительного запроса нет:
А вот при использовании Go-резолвера мы видим запрос для получения адреса IPv6:
В Go 1.17 это исправили:
В итоге получается такая ситуация с IPv6:
resolver | 1.16 | 1.17 |
cgo | 2 | 2 |
go | 2 | 2 |
cgo custom transport | 1 | 1 |
go custom transport | 2 | 1 |
ndots
Эта тема особенно актуальна для тех, кто использует Kubernetes. По умолчанию в подах Kubernetes будет примерно такой resolv.conf:Можно увидеть много запросов:ndots:n Sets a threshold for the number of dots which must appear in a name given to res_query(3) (see resolver(3)) before an initial absolute query will be made. The default for n is 1, meaning that if there are any dots in a name, the name will be tried first as an absolute name before any search list elements are appended to it. The value for this option is silently capped to 15.
В итоге делается восемь запросов — по два запроса (A и AAAA) на каждый из этих доменов:
- golang.org.ns.svc.cluster.local
- golang.org.svc.cluster.local
- golang.org.cluster.local
- golang.org
Лечится это несколькими способами. Можно использовать полный FQDN с точкой в конце. А можно добавить настройку в поды. Но в обоих случаях вам нужно использовать полные домены при обращении к другим сервисам внутри Kubernetes.
dnsConfig:
options:
- name: ndots
value: "1"
localhost
В Kubernetes могут осуществляться вызовы localhost. Обычно это происходит, когда используются сайдкары для сетевых вызовов. По сути, это прокси, которое работает в соседнем контейнере в поде, но в том же сетевом неймспейсе.Смотрим на системе, в которой по умолчанию включается Cgo-резолвер и отсутствует файл /etc/nsswitch.conf (привет, Alpine). Запрашивается DNS, так как системный резолвер не знает, где сначала смотреть домен, и сразу делает запрос.
Принудительно включаем Go-резолвер. Запросов нет:
Теперь попробуем запустить программу на системе, где Go-резолвер включается по умолчанию:
WAT?!
В версии 1.16 это исправили.
В версии Go до 1.16
hostLookupOrder(localhost) = dns,files
Если файл такой, то резолвер будет сначала искать совпадения в /etc/hosts, даже в приведённых выше примерах, и не будет лишних запросов:В версии Go 1.16
hostLookupOrder(localhost) = files,dns
$ cat /etc/nsswitch.conf
hosts: files dns
А если такой, то всегда сначала будет делаться запрос в DNS:
$ cat /etc/nsswitch.conf
hosts: dns files
В моей практике был забавный случай с localhost: он резолвился в адрес.
Как говорится, happy debugging, suckers!
Баги ядра
Ещё я натыкался на баги в ядре. Были race conditions в DNAT Conntrack. Возникали они, когда отправлялись одновременно два UDP-пакета через один сокет из разных тредов. Это поправлено в версии 4.19 и совсем исправлено — в 5.0.Итоги
Если использовать образ Ubuntu Focal, то будет использоваться Go-резолвер, а если Alpine 3.13 — то Cgo-резолвер. На macOS будет использоваться Cgo-резолвер. Как мне кажется, сейчас большинство программ на Go запускается в Kubernetes, поэтому quick win будет исправление ndots. Это позволит в четыре раза уменьшить количество DNS-запросов.Если же использовать дополнительный код для исключения IPv6 адресов, то можно ещё в два раза уменьшить количество DNS-запросов. В результате мы можем добиться сокращения запросов в целых восемь раз. При этом проверять, какие запросы отправляются в DNS, лучше через tcpdump.
Ну и последнее: не стесняйтесь копаться в кишочках используемых технологий — вы можете открыть для себя много нового и серьёзно улучшить производительность ваших приложений.
Полезные ссылки:
- Реализация IPv4 only запросов в Go-резолвере
- Исправление бага с localhost в Go 1.16
- src/net/conf.go
- src/net/dnsclient_unix.go
- man resolv.conf
- Anatomy of a Linux DNS Lookup (en)
- Tracing Linux Hostname Resolution (en)
- Resolve IP addresses in Linux (ru)
- Racy conntrack and DNS lookup timeouts (en)
- 5 – 15s DNS lookups on Kubernetes (en)
- DNS Resolution in Go and Cgo (en)
Как в восемь раз уменьшить количество DNS-запросов в Go
Привет, Хабр. Меня зовут Рустам. Я работаю в Ozon: админю Kubernetes и пишу на Go. У нас очень много сервисов на Go — их количество исчисляется тысячами. Запускаются они внутри кластеров Kubernetes. А...
habr.com