Articles

Debugging war story: The mystery of nxdomain

The following blog post describes a debugging adventure on Cloudflare’s Mesos-based cluster. Este cluster interno é usado principalmente para processar informações de arquivo de log para que os clientes Cloudflare têm análises, e para os nossos sistemas que detectam e respondem a ataques.

O problema encontrado não teve qualquer efeito em nossos clientes, mas teve engenheiros coçando suas cabeças…

O Problema

Em algum ponto em um dos nossos cluster, começamos a ver os erros como esse (um NXDOMAIN para um domínio existente em nosso DNS interno):

lookup some.existing.internal.host on 10.1.0.9:53: no such host

Isso parecia muito estranho, uma vez que o domínio existe realmente. Era um dos nossos domínios internos! Os engenheiros tinham mencionado que tinham visto este comportamento, então decidimos investigar mais profundamente. Consultas que desencadearam este erro foram variadas e variaram de registros SRV dinâmicos gerenciados por mesos-dns para domínios externos olhados de dentro do cluster.

a Nossa primeira ingênua tentativa foi execute o seguinte em um loop:

while true; do dig some.existing.internal.host > /tmp/dig.txt || break; done

rodando por um tempo em um servidor não reproduzir o problema: todas as pesquisas foram bem-sucedidas. Então nós pegamos nossos registros de serviço por um dia e fizemos um grep para “nenhum hospedeiro” e mensagens semelhantes. Erros aconteciam esporadicamente. Havia horas entre erros e nenhum padrão óbvio que nos pudesse levar a qualquer conclusão. Nossa investigação descartou a possibilidade de que o erro estava em Go, que usamos para muitos de nossos serviços, já que os erros estavam vindo de Serviços Java também.

na Toca do coelho

usávamos correr sem ligação num único IP através de algumas máquinas para a resolução do nosso conjunto de DNS. BGP é então responsável por anunciar rotas internas das máquinas para o roteador. Decidimos tentar encontrar um padrão enviando muitos pedidos de diferentes máquinas e erros de gravação. Aqui está o que nosso Programa de teste de carga parecia no início:

package mainimport ("flag""fmt""net""os""time")func main() {n := flag.String("n", "", "domain to look up")p := flag.Duration("p", time.Millisecond*10, "pause between lookups")flag.Parse()if *n == "" {flag.PrintDefaults()os.Exit(1)}for {_, err := net.LookupHost(*n)if err != nil {fmt.Println("error:", err)}time.Sleep(*p)}}

executamosnet.LookupHost em um loop com pequenas pausas e erros de log; é isso. Empacotar isso em um recipiente do Docker e correr em Marathon foi uma escolha óbvia para nós, uma vez que é assim que nós executamos outros serviços de qualquer maneira. Os registos são enviados para Kafka e depois para Kibana, onde podemos analisá-los. Executando este programa em 65 máquinas que fazem pesquisas a cada 50m mostra a seguinte distribuição de erros (de alto a baixo) em máquinas:

não vimos nenhuma forte correlação com racks ou máquinas específicas. Erros aconteceram em muitas máquinas, mas não em todas elas e em diferentes janelas de tempo erros acontecem em máquinas diferentes. Colocar o tempo no eixo X e o número de erros no eixo Y mostraram o seguinte:

Para ver se algum particular DNS recursor tinha ido loucos, nós paramos de todos os geradores de carga em regular máquinas e começou a ferramenta de geração de carga no recursors-se. Não houve erros em poucas horas, o que sugeriu que o Não consolidado era perfeitamente saudável.

começamos a suspeitar que a perda de pacotes era o problema, mas por que “tal host” não ocorreria? Isso só deve acontecer quando um erro NXDOMAIN está em uma resposta DNS, mas nossa teoria era que as respostas não voltaram de todo.

A Falta

Para testar a hipótese de que a perda de pacotes pode levar a um “host não” erro”, primeiro, tentou bloquear o tráfego de saída na porta 53:

sudo iptables -A OUTPUT -p udp --dport 53 -j DROP

neste caso, cavar e ferramentas semelhantes, apenas o tempo, mas não retorno “nenhum host”:

; <<>> DiG 9.9.5-9+deb8u3-Debian <<>> cloudflare.com;; global options: +cmd;; connection timed out; no servers could be reached

Ir é um pouco mais esperto e diz mais sobre o que está acontecendo, mas não retorno “nenhum host” ou:

error: lookup cloudflare.com on 10.1.0.9:53: write udp 10.1.14.20:47442->10.1.0.9:53: write: operation not permitted

Uma vez que o kernel Linux diz ao remetente que ele deixou cair pacotes, tivemos que apontar o servidor de nomes para algum buraco negro na rede que não faz nada com pacotes para imitar a perda de pacotes. Ainda sem sorte:

error: lookup cloudflare.com on 10.1.2.9:53: read udp 10.1.14.20:39046->10.1.2.9:53: i/o timeout

Para continuar culpando a rede que tinha para apoiar os nossos pressupostos, de alguma forma, então nós adicionamos informações de temporização para nossas pesquisas:

s := time.Now()_, err := net.LookupHost(*n)e := time.Now().Sub(s).Seconds()if err != nil { log.Printf("error after %.4fs: %s", e, err)} else if e > 1 { log.Printf("success after %.4fs", e)}

Para ser honesto, nós começamos por erros de tempo e adicionado de sucesso de tempo mais tarde. Erros estavam acontecendo depois de 10s, comparativamente muitas respostas bem sucedidas estavam vindo depois de 5s. Ele parece perda de pacotes, mas ainda não nos diz Por que “nenhum desses hospedeiros” acontece.

Desde já estávamos em um lugar quando sabíamos que hospeda eram mais propensos a ser afetados por isso, nós corremos os dois comandos a seguir em paralelo em dois screen sessões:

while true; do dig cloudflare.com > /tmp/dig.log || break; done; date; sudo killall tcpdumpsudo tcpdump -w /state/wtf.cap port 53

O objetivo era obter um despejo de rede com falha resolve. Lá, vimos as seguintes consultas:

00.00s A cloudflare.com05.00s A cloudflare.com10.00s A cloudflare.com.in.our.internal.domain

duas consultas saem sem qualquer resposta, mas a terceira tem sorte e tem sucesso. Naturalmente, não temos a cloudflare.com em nosso domínio interno, então livre por direito dá NXDOMAIN em resposta, 10s após a consulta foi iniciada.

Bingo

vamos olhar para /etc / resolv.conf to understand more:

nameserver 10.1.0.9search in.our.internal.domain

Using the search keyword allows us to use short hostnames instead of FQDN, making myhost transparently equivalent to myhost.in.our.internal.domain.

para a resolução do DNS significa o seguinte: para qualquer consulta do DNS pergunte ao servidor de nomes 10.1.0.9, se isso falhar, adicione .in.our.internal.domain à consulta e repetição. Não importa que falha ocorra para a consulta DNS original. Normalmente é NXDOMAIN, mas no nosso caso é um tempo limite de leitura devido à perda de pacotes.

os seguintes eventos parecem ter de ocorrer para que um erro de” não tal máquina ” apareça:

  1. original solicitação de DNS tem de ser perdido
  2. A repetir que é enviado após 5 segundos para ser perdido
  3. consulta subsequente para o domínio interno (causado por search opção) tem de ter sucesso e retorno NXDOMAIN

por outro lado, para observar o tempo limite de consulta de DNS em vez de NXDOMAIN, você tem a perder quatro pacotes enviados de 5 segundos, um após o outro (2 para a consulta original e 2 para a versão interna do seu domínio), que é muito menor probabilidade. Na verdade, só vimos um NXDOMAIN depois dos 15 anos uma vez e nunca vimos um erro depois dos 20.

Para validar essa suposição, construímos uma prova-de-conceito de servidor de DNS que cai todos os pedidos para cloudflare.com, mas envia um NXDOMAIN de domínios existentes:

package mainimport ("github.com/miekg/dns""log")func main() {server := &dns.Server{Addr: ":53", Net: "udp"}dns.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) {m := &dns.Msg{}m.SetReply(r)for _, q := range r.Question {log.Printf("checking %s", q.Name)if q.Name == "cloudflare.com." {log.Printf("ignoring %s", q.Name)// just ignorereturn}}w.WriteMsg(m)})log.Printf("listening..")if err := server.ListenAndServe(); err != nil {log.Fatalf("error listening: %s", err)}}

Finalmente, encontrou o que estava acontecendo e tinha uma maneira de replicar de forma confiável, que o comportamento.

soluções

vamos pensar sobre como podemos melhorar o nosso cliente para lidar melhor com estes problemas de rede transitórios, tornando-o mais resistente. A página do homem para resolver.conf lhe diz que você tem dois botões: o timeout e retries opções. Os valores padrão são 5 e 2, respectivamente.

a menos que mantenha o seu servidor DNS muito ocupado, é muito improvável que ele levaria mais de 1 segundo para responder. Na verdade, se você tiver um dispositivo de rede Na Lua, você pode esperar que ele responda em 3 segundos. Se o servidor de nomes viver no próximo rack e estiver acessível através de uma rede de alta velocidade, você pode assumir com segurança que se não Houver resposta após 1 segundo, o seu servidor de DNS não recebeu a sua consulta. Se você quiser ter menos estranho “não tal domínio” erros que fazem você coçar sua cabeça, você pode muito bem aumentar as repetições. Quanto mais vezes você repetir com perda transitória de pacotes, menos chance de fracasso. Quanto mais vezes você repetir, maiores chances de terminar mais rápido.

Imagine que você tem realmente perda aleatória de 1% do pacote.

  • 2 tentativas, de 5 anos de tempo limite: máximo de 10 segundos de espera antes de erro de 0,001% de chance de falha
  • 5 tentativas, 1s tempo limite: máximo de 5 anos de espera antes de erro, 0.000001% de chance de falha

Na vida real, a distribuição poderia ser diferente, devido ao fato de que a perda de pacotes não é aleatória, mas você pode esperar e muito menos para o DNS para responder com este tipo de alteração.

Como você sabe, muitas bibliotecas de sistemas que fornecem resolução DNS como glibc, nscd, systemd-resolved não são endurecidas para lidar com estar na internet ou em um ambiente com Perdas de pacotes. Temos enfrentado o desafio de criar um ambiente de resolução DNS confiável e rápido várias vezes como temos crescido, só para mais tarde descobrir que a solução não é perfeita.

para si

dado o que leu neste artigo sobre perda de pacotes e split-DNS/private-namespace, como é que conceberia uma configuração de resolução rápida e fiável? Que software você usaria e por quê? Que alterações de afinação da configuração padrão você usaria?