Articles

Depurando war story: el misterio de NXDOMAIN

La siguiente entrada de blog describe una aventura de depuración en el clúster basado en Mesos de Cloudflare. Este clúster interno se utiliza principalmente para procesar información de archivos de registro para que los clientes de Cloudflare tengan análisis y para nuestros sistemas que detectan y responden a los ataques.

El problema encontrado no tuvo ningún efecto en nuestros clientes, pero hizo que los ingenieros se rascaran la cabeza…

El problema

En algún punto de nuestro clúster empezamos a ver errores como este (un dominio NX para un dominio existente en nuestro DNS interno):

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

Esto parecía muy extraño, ya que el dominio existía de hecho. ¡Era uno de nuestros dominios internos! Los ingenieros habían mencionado que habían visto este comportamiento, así que decidimos investigar más a fondo. Las consultas que desencadenaron este error fueron variadas y variaron desde registros SRV dinámicos administrados por mesos-dns hasta dominios externos buscados desde el interior del clúster.

Nuestro primer intento ingenuo fue ejecutar lo siguiente en un bucle:

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

Ejecutar esto durante un tiempo en un servidor no reprodujo el problema: todas las búsquedas se realizaron correctamente. Luego tomamos nuestros registros de servicio por un día e hicimos un grep para» no hay tal host » y mensajes similares. Los errores ocurrían esporádicamente. Hubo horas entre errores y ningún patrón obvio que pudiera llevarnos a ninguna conclusión. Nuestra investigación descartó la posibilidad de que el error estuviera en Go, que usamos para muchos de nuestros servicios, ya que los errores también provenían de servicios Java.

En el agujero del conejo

Solíamos ejecutar Sin vincular en una sola IP en algunas máquinas para nuestro solucionador de DNS de clúster. BGP es responsable de anunciar las rutas internas de las máquinas al enrutador. Decidimos intentar encontrar un patrón enviando muchas solicitudes de diferentes máquinas y registrando errores. Así es como se veía nuestro programa de pruebas de carga al principio:

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)}}

Ejecutamos net.LookupHost en un bucle con pequeñas pausas y errores de registro; eso es todo. Empaquetar esto en un contenedor Docker y correr en Marathon fue una opción obvia para nosotros, ya que así es como ejecutamos otros servicios de todos modos. Los troncos se envían a Kafka y luego a Kibana, donde podemos analizarlos. Ejecutar este programa en 65 máquinas que realizan búsquedas cada 50 ms muestra la siguiente distribución de errores (de alto a bajo) entre hosts:

No vimos una correlación fuerte con racks o máquinas específicas. Los errores ocurrieron en muchos hosts, pero no en todos ellos y en diferentes ventanas de tiempo, los errores ocurren en diferentes máquinas. Poner el tiempo en el eje X y el número de errores en el eje Y mostró lo siguiente:

Para ver si algún recursivo DNS en particular se había vuelto loco, detuvimos todos los generadores de carga en máquinas normales e iniciamos la herramienta de generación de carga en los propios recursivos. No hubo errores en unas pocas horas, lo que sugiere que Sin consolidar es perfectamente saludable.

Empezamos a sospechar que la pérdida de paquetes era el problema, pero ¿por qué «no ocurriría tal host»? Solo debería ocurrir cuando hay un error NXDOMAIN en una respuesta DNS, pero nuestra teoría era que las respuestas no regresaban en absoluto.

La falta

Para probar la hipótesis de que la pérdida de paquetes puede conducir a un error de «no hay tal host», primero intentamos bloquear el tráfico saliente en el puerto 53:

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

En este caso, dig y herramientas similares solo tienen tiempo de espera, pero no devuelven «no hay tal host»:

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

Go es un poco más inteligente y te dice más sobre lo que está pasando, pero tampoco devuelve «no hay tal anfitrión» :

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

Dado que el kernel de Linux le dice al remitente que dejó caer paquetes, tuvimos que apuntar el servidor de nombres a algún agujero negro en la red que no hace nada con los paquetes para imitar la pérdida de paquetes. Sin embargo, no hubo suerte:

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 seguir culpando a la red, tuvimos que apoyar nuestras suposiciones de alguna manera, así que agregamos información de tiempo a nuestras búsquedas:

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 honestos, comenzamos con errores de tiempo y agregamos tiempo de éxito más tarde. Los errores sucedían después de los 10, comparativamente muchas respuestas exitosas llegaban después de los 5. Parece una pérdida de paquetes, pero aún no nos dice por qué «no ocurre tal host».

Dado que ahora estábamos en un lugar en el que sabíamos qué hosts tenían más probabilidades de verse afectados por esto, ejecutamos los siguientes dos comandos en paralelo en dos sesiones screen:

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

El objetivo era obtener un volcado de red con resoluciones fallidas. Allí, vimos las siguientes consultas:

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

Dos consultas expiran sin respuesta, pero la tercera tiene suerte y tiene éxito. Naturalmente, no tenemos cloudflare.com en nuestro dominio interno, por lo que Unbound da legítimamente NXDOMAIN en respuesta, 10s después de que se inició la búsqueda.

Bingo

Veamos /etc / resolv.conf para entender más:

nameserver 10.1.0.9search in.our.internal.domain

Usar la palabra clave de búsqueda nos permite usar nombres de host cortos en lugar de FQDN, haciendo que myhostsea transparente y equivalente a myhost.in.our.internal.domain.

Para el solucionador DNS significa lo siguiente: para cualquier consulta DNS, pregunte al servidor de nombres 10.1.0.9, si esto falla, agregue .in.our.internal.domain a la consulta y vuelva a intentarlo. No importa qué fallo se produzca en la consulta DNS original. Normalmente es NXDOMAIN, pero en nuestro caso es un tiempo de espera de lectura debido a la pérdida de paquetes.

Los siguientes eventos parecían tener que ocurrir para que apareciera un error de «no hay tal host»:

  1. La solicitud DNS original tiene que perderse
  2. El reintento que se envía después de 5 segundos tiene que perderse
  3. La consulta posterior para el dominio interno (causada por la opción search) tiene que tener éxito y devolver NXDOMAIN

Por otro lado, para observar una consulta DNS con tiempo de espera en lugar de NXDOMAIN, tiene que perder cuatro los paquetes se envían 5 segundos uno tras otro (2 para la consulta original y 2 para la versión interna de su dominio), lo que es una probabilidad mucho menor. De hecho, solo vimos un NXDOMAIN después de 15s una vez y nunca vimos un error después de 20s.

Para validar esa suposición, creamos un servidor DNS de prueba de concepto que elimina todas las solicitudes de cloudflare.com, pero envía un dominio NX para dominios 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, encontramos lo que estaba sucediendo y teníamos una forma de replicar de manera confiable ese comportamiento.

Soluciones

Pensemos en cómo podemos mejorar a nuestro cliente para manejar mejor estos problemas de red transitorios, haciéndolo más resistente. La página de manual de resolv.conf le indica que tiene dos botones: las opciones timeout y retries. Los valores predeterminados son 5 y 2, respectivamente.

A menos que mantenga su servidor DNS muy ocupado, es muy poco probable que le tome más de 1 segundo responder. De hecho, si tiene un dispositivo de red en la Luna, puede esperar que responda en 3 segundos. Si su servidor de nombres vive en el siguiente rack y es accesible a través de una red de alta velocidad, puede asumir con seguridad que si no hay respuesta después de 1 segundo, su servidor DNS no recibió su consulta. Si quieres tener errores menos extraños de «no hay tal dominio» que te hagan rascarte la cabeza, también podrías aumentar los reintentos. Cuantas más veces reintente con pérdida transitoria de paquetes, menos posibilidades de fallo. Cuanto más a menudo vuelva a intentarlo, mayores serán las posibilidades de terminar más rápido.

Imagine que tiene una pérdida de paquetes verdaderamente aleatoria del 1%.

  • 2 reintentos, tiempo de espera de 5s: máximo 10 segundos de espera antes del error, 0,001% de probabilidad de error
  • 5 reintentos, tiempo de espera de 1s: máximo 5 segundos de espera antes del error, 0,000001% de probabilidad de error

En la vida real, la distribución sería diferente debido al hecho de que la pérdida de paquetes no es aleatoria, pero puede esperar mucho menos para que DNS responda con este tipo de cambio.

Como usted sabe, muchas bibliotecas de sistemas que proporcionan resolución de DNS como glibc, nscd, systemd-resolved no están endurecidas para manejar estar en Internet o en un entorno con pérdidas de paquetes. Nos hemos enfrentado al desafío de crear un entorno de resolución de DNS confiable y rápido varias veces a medida que hemos crecido, solo para descubrir más tarde que la solución no es perfecta.

A usted

Dado lo que ha leído en este artículo sobre pérdida de paquetes y DNS dividido / espacio de nombres privado, ¿cómo diseñaría una configuración de resolución rápida y confiable? ¿Qué software usarías y por qué? ¿Qué cambios de ajuste de la configuración estándar usarías?