Avahi demorando el arranque

Desde el primer día que monté mi actual PC y le instalé Debian he tenido demoras en el arranque del sistema. En concreto, el proceso se quedaba clavado durante unos segundos cuando (supuestamente) estaba configurando la red, mientras la pantalla de arranque mostraba:

[ ok ] Mounting local filesystems... done.
[ ok ] Activating swapfile swap... done.
[ ok ] Cleaning up temporary files....
[ ok ] Setting kernel variables ... done.
[....] Configuring network interfaces...

Si utilizaba DHCP (la IP dinámica era otorgada por el router), ese tiempo de espera aparecía intermitentemente en algunos de los arranques junto con un extraño mensaje de asignación de IP del cliente DHCP. Si empleaba en cambio una IP estática, asignada manualmente por mí, el retraso aparecía siempre.

Durante mucho tiempo he creido que el origen del problema era el propio driver de la tarjeta de red Intel e1000e, ya que en los logs del kernel aparecían un par de líneas extrañas, "IPv6: ADDRCONF(NETDEV_UP): eth0: link is not ready" y más tarde "IPv6: ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready" que me tuvieron despistado. Esta familia de tarjetas había tenido algún bug serio, así que los bug tracking systems estaban llenos de referencias a logs con estos mensajes, lo que complicaba la búsqueda. Al final encontré en el README del driver de Intel, casi al final de la sección de "Known Issues", el siguiente fragmento:

> Link may take longer than expected
> ----------------------------------
> 
> 
> With some Phy and switch combinations, link can take longer than expected.
> This can be an issue on Linux distributions that timeout when checking for
> link prior to acquiring a DHCP address; however there is usually a way to work
> around this (for example, set LINKDELAY in the interface configuration on
> RHEL).

También estuve usando durante un mes bootchart2 para obtener gráficas de tiempos del proceso de arranque. Los arranques sin problemas de retraso rondaban los 15-16 segundos, mientras que los que tenían retraso variaban desde los 20 a los 27 segundos, alcanzando en un caso excepcional ¡los 59 segundos! De todo este montón de información lo que me llamó la atención era un proceso llamado dhclient-script, que aparecía "alargado" durante segundos en los arranques con retraso, como por ejemplo en éste:

Detalle de una gráfica de bootchart2

Tanto el comentario del README como los resultados de bootchart2 me llevaron a pensar que era un problema exclusivamente de la tarjeta de red, y por lo tanto que sólo la propia gente de Intel podía resolver en el código del driver. Actualizaba el kernel cada vez que cambiaban algo en el driver, esperando a que alguna vez "sonara la flauta". Y probablemente algo así ha pasado, porque si miro el último kernel compilado, los mensajes sobre el link not ready/ready siguen apareciendo en el kernel, sólo que actualmente el tiempo que transcurre entre ambos mensajes es de aproximadamente 1,5 segundos:

[   10.034797] e1000e 0000:00:19.0: irq 41 for MSI/MSI-X
[   10.138513] e1000e 0000:00:19.0: irq 41 for MSI/MSI-X
[   10.138633] IPv6: ADDRCONF(NETDEV_UP): eth0: link is not ready
[   11.658786] e1000e: eth0 NIC Link is Up 100 Mbps Full Duplex, Flow Control: Rx/Tx
[   11.658790] e1000e 0000:00:19.0 eth0: 10/100 speed: disabling TSO
[   11.658821] IPv6: ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready

Pero el problema del retraso persistía, aunque la situación había mejorado lo suficiente como para poder usar una IP estática con un tiempo de arranque asumible.

Más adelante, en otra tanda de investigaciones, encontré que desactivando los scripts que se llaman en un ifup no había retrasos, lo que me llevó a descubrir que la demora se producía concretamente dentro del script /usr/lib/avahi/avahi-daemon-check-dns.sh. Es más, a base de hacer debugging del script fuí capaz de acotar el lugar donde se producía exactamente:

  OUT=`LC_ALL=C host -t soa local. 2>&1`
  if [ $? -eq 0 ] ; then
    if echo "$OUT" | egrep -vq 'has no|not found'; then
      return 0
    fi
  else 
    # Checking the dns servers failed. Assuming no .local unicast dns, but
    # remove the nameserver cache so we recheck the next time we re triggered
    rm -f ${NS_CACHE}
  fi
  return 1

El culpable era el comando host -t soa local. Y además averigüé, mostrando la hora antes y despues, que el retraso era siempre de 10 segundos. Tenía toda la pinta de que el comando host estaba esperando una respuesta hasta que sus timeouts se agotaban. Sin embargo, el problema parecía limitarse al arranque, ya que invocando host -t soa local en la línea de comandos el tiempo de espera era inapreciable.

Ahora que conocía el problema, no me fue difícil encontrar que ya existía un bug abierto con el problema en el BTS de Debian: Bug #559927 (lo cual era un alivio, porque significaba que no era el único pringao con el bug).

Había una solución sencilla para evitar el retraso, que era eliminar el comando en cuestión. Pero todavía no sabía qué hacía ese comando y el script en general, así que ignoraba cuáles eran las repercusiones que ello podía acarrear. No ha sido hasta hace unos días que me he puesto otra vez con este bug y he conseguido resolverlo defintivamente.

Avahi

Avahi es una implementación libre de la especificación Zeroconf de Apple, y si quieres saber qué significa eso mejor que mires los enlaces a Wikipedia que he puesto. El caso es que Avahi se usa para descubrir recursos en la red local como impresoras y unidades de red, entre otras cosas, por lo que CUPS (el subsistema de impresión de Linux que también lo es de Mac OS X) lo emplea. Yo, como la mayoría del mundo, tengo una impresora local conectada por un puerto USB, así que todo esto del descubrimiento de impresoras en la red en realidad me sobra. Sin embargo los que definen las dependencias en Debian no piensan lo mismo y Avahi es una dependencia Recommends: de CUPS. Si tienes una impresora y por lo tanto CUPS, es casi seguro que tienes instalado Avahi.

Pero hay un problema: Avahi se basa en multicast DNS (mDNS), una forma de DNS en el que se interroga a través de IPs multicast a toda la red local, buscando otros equipos con Avahi/Zeroconf. Zeroconf está definido bajo una zona DNS especial cuyo dominio es .local (por ejemplo miequipo.local o mordor.local). Y aunque .local está reservado, hay gente que no lo sabe y que lo usa como parte de su FQDN para su equipo o su red local, poniéndolo en su /etc/host o lo que es peor, como una zona en su servidor DNS. Y cuando esto sucede hay problemas, porque ahora los nombres de dominio *.local puede resolverse tanto a través de multicast DNS como de unicast DNS.

Para evitar esta situación, en el paquete Debian escribieron un script que comprueba precisamente esto: si existe (se puede resolver) un dominio .local en el DNS actual. Si existe, Avahi no arrancará, y se avisa de ello al administrador del equipo para que tome las medidas oportunas. Si no se resuelve, Avahi puede lanzarse con la seguridad de que no habrá problemas con la resolución de .local a través del mDNS. Para hacer esta comprobación, lo que hace el script es literalmente llamar al DNS usando host. Huelga decir que este script es, naturalmente, el mencionado avahi-daemon-check-dns.sh.

No contentos con esto, y debido a algún bug abierto, los mantenedores de Avahi decidieron que era buena idea hacer la comprobación de .local cada vez que se levantara o reiniciara un interfaz de red (por ejemplo cada vez que te conectes a una WiFi y te asigne nuevos DNS via DHCP). Y para ello lo que hicieron es naturalmente enganchar el script avahi-daemon-check-dns.sh a la infraestructura de dispositivos de red de Debian, que son los scripts de ifupdown.

¿Por qué no funciona esto? En realidad sí funciona... si no tienes un DNS local, como es mi caso. Y es que bind9 (o el servidor de DNS que utilices) no está en marcha hasta mucho más tarde en el proceso de arranque. Por eso el host -t soa local funciona perfectamente desde la línea de comandos, pero falla miserablemente cuando es invocado desde avahi-daemon-check-dns.sh cuando ifup levanta un interfaz en el arranque del sistema. O también en ifdown en el shutdown del mismo, cuando el servidor de DNS ya está muerto. Y es que no lo he dicho, pero el retraso también existía en el apagado (aunque normalmente es de esas cosas en las que no te sueles fijar mucho o darle importancia).

Aplicando el siguiente parche a /usr/lib/avahi/avahi-daemon-check-dns.sh se puede ver el tiempo que transcurre en la llamada a host:

57a58,59
>   DEBUG_HOST_CMD_TIME=`LC_ALL=C date +%T`
>   echo "\n[$DEBUG_HOST_CMD_TIME] Before calling host -t soa local."
59a62,63
>     DEBUG_HOST_CMD_TIME=`LC_ALL=C date +%T`
>     echo "[$DEBUG_HOST_CMD_TIME] After calling host -t soa local."
63a68,69
>     DEBUG_HOST_CMD_TIME=`LC_ALL=C date +%T`
>     echo "[$DEBUG_HOST_CMD_TIME] After calling host -t soa local."

Para poder hacer logging de las líneas generadas con ese parche he tenido que emplear la utilidad bootlogd. bootlogd se encarga de guardar todo lo que se imprime en la consola al inicio del proceso de arranque, para posteriormente volcarlo en un fichero de log /var/log/boot. Es la única forma de hacerlo, ya que esta parte inicial del arranque sucede antes de que syslogd esté en marcha, cuando ni siquiera está montado el root filesystem.

Sun Jun 22 09:27:11 2014: [....] Setting kernel variables ... [ok] done.
Sun Jun 22 09:27:14 2014: [....] Configuring network interfaces...
Sun Jun 22 09:27:14 2014: [09:27:12] Before calling host -t soa local.
Sun Jun 22 09:27:22 2014: [09:27:22] After calling host -t soa local.
Sun Jun 22 09:27:22 2014:  [ok] done.
Sun Jun 22 09:27:22 2014: [....] Starting rpcbind daemon... [ok] .
Sun Jun 22 09:27:22 2014: [....] Starting NFS common utilities: statd [ok] .
...

Los 10 segundos se repiten siempre que empleo el DNS local (nameserver 127.0.0.1 en /etc/resolv.conf). Cuando empleo un DNS externo, por ejemplo de mi ISP, el tiempo de host es de 2 segundos, aunque eso dependerá de lo que tarde ese servidor DNS en enviar la respuesta (con un máximo de 10 segundos, claro, que es lo más que espera host una respuesta del DNS).

Ahora que sabía exactamente qué estaba pasando, podía resolver el problema. De hecho, el propio paquete Avahi proporciona una solución: se puede desactivar la comprobación del dominio .local poniendo a 0 el valor de AVAHI_DAEMON_DETECT_LOCAL en /etc/default/avahi-daemon. Sin embargo, yo he optado por una solución más drástica, que es desinstalar directamente Avahi. Así me evito futuros problemas y bugs con un software que no necesito ni uso, amén de ahorrarme dos demonios innecesarios corriendo en el sistema.

Los resultados de bootchart2 son los siguientes: antes de eliminar el retraso un arranque tardaba entre 25 y 26 segundos, mientras que después, desactivando la resolución de .local, tarda de 15 a 16 segundos y desinstalando completamente Avahi también de 15 a 16. Es un poco contraintuitivo que tarde lo mismo teniendo que lanzar 2 procesos más que sin hacerlo, pero he hecho 4 arranques de cada configuración para cerciorarme y eso es lo que he obtenido.

Para verlo gráficamente, aquí tenéis un informe completo de bootchart2 de cada uno de los casos:

(Nota: las gráficas de bootchart2 no salen todas iguales, ya que el arranque en paralelo es no determinista, y además me parece que la forma de medir de bootchart2 tampoco es perfecta; lo que he hecho ha sido escoger la que me ha parecido más representativa de cada una de las cuatro realizadas para cada caso).

Desinstalando Avahi en Debian

Dependencias de Avahi

Si se observa el mapa de dependencias (limitado) de Avahi en la imagen de arriba, se puede ver que la única manera de desinstalar Avahi sin desinstalar CUPS es "cortar" por las recomendaciones (flechas discontínuas), es decir, por los paquetes marcados en rojo avahi-daemon y cups-browsed (lo que también desinstalaría por dependencias libnss-mdns). Esto permitirá desinstalar además las bibliotecas marcadas en violeta, si es que apt no lo hace automáticamente. Sin embargo, no es posible desinstalar todas las bibliotecas Avahi, ya que CUPS (y puede que otros paquetes que tengas instalados) dependen directamente del paquete de la biblioteca cliente y de la parte común. Cualquier intento de quitar estos paquetes puede llevarse por delante muchos otros que probablemente no quieres quitar, así que mejor dejarlo ahí.

:wq

blogroll

social