¿Cómo lidiar con las fugas de recursos de Dockerd? Ver la esencia del problema a través del fenómeno.

Inserte la descripción de la imagen aquí

1. Fenómeno

Alarma de clúster k8s en línea, la tasa de utilización de fd del host supera el 80%, inicie sesión para verificar el uso de la memoria de dockerd 26G

2. Ideas para solucionar problemas

Dado que hemos encontrado muchas fugas de recursos de Dockerd antes, veamos si es causado por una causa conocida.

3. ¿Quién es el opuesto de fd?

Ejecute ss -anp | grep dockerd, y el resultado es como se muestra en la siguiente figura. Puede ver que el problema es diferente al problema encontrado antes. La octava columna muestra 0, que no es consistente con la situación encontrada antes, y no se puede encontrar al par.

Inserte la descripción de la imagen aquí

4. ¿Por qué se pierde la memoria?

Para usar pprof para analizar la ubicación de la fuga de memoria, primero abra el modo de depuración para dockerd, debe modificar el archivo de servicio, agregue las siguientes dos oraciones

ExecReload=/bin/kill -s HUP $MAINPID
KillMode=process

Al mismo tiempo, agregue la configuración de "debug": true en el archivo /etc/docker/daemon.json. Después de la modificación, ejecute systemctl daemon-reload para volver a cargar la configuración del servicio docker, luego ejecute systemctl reload docker, vuelva a cargar el configuración de la ventana acoplable y active el modo de depuración

Por defecto, dockerd usa uds para no brindar ningún servicio. Para facilitar nuestra depuración, podemos usar socat para reenviar el puerto a docker, de la siguiente manera: sudo socat -d -d TCP-LISTEN: 8080, fork, bind = 0.0.0.0 UNIX: / var / run / docker.sock, lo que significa que el exterior puede llamar a la API de Docker accediendo al puerto 8080 del host, hasta ahora todo está listo

Ejecute la herramienta go pprof http: // ip: 8080 / debug / pprof / heap localmente para ver el uso de memoria, como se muestra en la siguiente figura

Inserte la descripción de la imagen aquí

Puedes ver que las áreas ocupadas están en el bufio NewWriterSize y NewReaderSize que vienen con golang. Cada llamada http estará aquí. También veo lo que está mal.

5. ¿Goroutine también se filtró?

Ubicación de la fuga

Todavía no es posible conocer la ubicación específica del problema a través de la memoria, el problema no es grande, luego mire la situación de goroutine, visite directamente http: // ip: 8080 / debug / pprof / goroutine? Debug = 1 en el navegador, como se muestra a continuación

Inserte la descripción de la imagen aquí

Hay un total de 1,572,822 gorutinas, con dos cabezudos cada uno que representan 786,212 cada uno. Cuando vea esto, básicamente puede ir al código fuente a lo largo del número de líneas en el archivo. Aquí usamos la versión 18.09.2 de la ventana acoplable para cambiar el código fuente a la versión correspondiente. Al verificar el código fuente, puede saber las razones de la fuga de estos dos tipos de goroutines. El flujo de procesamiento relacionado de dockerd y containerd es el siguiente

Inserte la descripción de la imagen aquí

De acuerdo con la imagen de arriba, la fuga de goroutine es causada por el cierre del canal de espera en la última matanza de docker anterior. Se iniciará otra goroutine durante la espera. Cada matanza de docker provocará la fuga de las dos goroutines. El código correspondiente es el siguiente

// Kill forcefully terminates a container.
func (daemon *Daemon) Kill(container *containerpkg.Container) error {
    
    
   if !container.IsRunning() {
    
    
      return errNotRunning(container.ID)
   }
   // 1. Send SIGKILL
   if err := daemon.killPossiblyDeadProcess(container, int(syscall.SIGKILL)); err != nil {
    
    
      // While normally we might "return err" here we're not going to
      // because if we can't stop the container by this point then
      // it's probably because it's already stopped. Meaning, between
      // the time of the IsRunning() call above and now it stopped.
      // Also, since the err return will be environment specific we can't
      // look for any particular (common) error that would indicate
      // that the process is already dead vs something else going wrong.
      // So, instead we'll give it up to 2 more seconds to complete and if
      // by that time the container is still running, then the error
      // we got is probably valid and so we return it to the caller.
      if isErrNoSuchProcess(err) {
    
    
         return nil
      }
      ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
      defer cancel()
      if status := <-container.Wait(ctx, containerpkg.WaitConditionNotRunning); status.Err() != nil {
    
    
         return err
      }
   }
   // 2. Wait for the process to die, in last resort, try to kill the process directly
   if err := killProcessDirectly(container); err != nil {
    
    
      if isErrNoSuchProcess(err) {
    
    
         return nil
      }
      return err
   }
   // Wait for exit with no timeout.
   // Ignore returned status.
   <-container.Wait(context.Background(), containerpkg.WaitConditionNotRunning)
   return nil
}
// Wait waits until the container is in a certain state indicated by the given
// condition. A context must be used for cancelling the request, controlling
// timeouts, and avoiding goroutine leaks. Wait must be called without holding
// the state lock. Returns a channel from which the caller will receive the
// result. If the container exited on its own, the result's Err() method will
// be nil and its ExitCode() method will return the container's exit code,
// otherwise, the results Err() method will return an error indicating why the
// wait operation failed.
func (s *State) Wait(ctx context.Context, condition WaitCondition) <-chan StateStatus {
    
    
   s.Lock()
   defer s.Unlock()
   if condition == WaitConditionNotRunning && !s.Running {
    
    
      // Buffer so we can put it in the channel now.
      resultC := make(chan StateStatus, 1)
      // Send the current status.
      resultC <- StateStatus{
    
    
         exitCode: s.ExitCode(),
         err:      s.Err(),
      }
      return resultC
   }
   // If we are waiting only for removal, the waitStop channel should
   // remain nil and block forever.
   var waitStop chan struct{
    
    }
   if condition < WaitConditionRemoved {
    
    
      waitStop = s.waitStop
   }
   // Always wait for removal, just in case the container gets removed
   // while it is still in a "created" state, in which case it is never
   // actually stopped.
   waitRemove := s.waitRemove
   resultC := make(chan StateStatus)
   go func() {
    
    
      select {
    
    
      case <-ctx.Done():
         // Context timeout or cancellation.
         resultC <- StateStatus{
    
    
            exitCode: -1,
            err:      ctx.Err(),
         }
         return
      case <-waitStop:
      case <-waitRemove:
      }
      s.Lock()
      result := StateStatus{
    
    
         exitCode: s.ExitCode(),
         err:      s.Err(),
      }
      s.Unlock()
      resultC <- result
   }()
   return resultC
}

En comparación con la imagen de la goroutine, las dos gorutinas fueron al último contenedor. Espera de matar y la selección de Esperar. Es precisamente porque la selección del método Esperar no ha regresado, el resultado C no tiene datos y el exterior no puede ser devuelto desde el contenedor. Espere. Los datos se leen en chan, lo que hace que se bloqueen dos goroutines para cada llamada de parada de docker.

¿Por qué fuga?

¿Por qué select nunca regresa? Puede ver que select está esperando tres canales, y cualquiera de ellos tiene datos o está cerrado volverá

ctx.Done (): No regresa porque el context.Background () se pasó la última vez que se llamó a Wait. En realidad, así es como dockerd maneja las solicitudes. Dado que el cliente quiere eliminar el contenedor, espero a que se elimine el contenedor, y cuándo eliminarlo y cuándo salir. Mientras no se elimine el contenedor, siempre habrá un goroutine esperando.
waitStop y waitRemove: No regresa porque no recibió la señal de salida de la tarea enviada por containerd. Puede ver la imagen de arriba. Chan se cerrará solo después de que se reciba la salida de la tarea.
¿Por qué no recibí el evento de salida de la tarea?

El problema se aclara gradualmente, pero es necesario investigar más a fondo por qué no se recibe el evento de salida de la tarea. Hay dos posibilidades

Enviado pero confiscado y recibido: Lo primero que me viene a la mente es un problema encontrado por Tencent. También en la versión 18 de la ventana acoplable, la goroutine de processEvent salió de forma anormal, lo que resultó en la imposibilidad de recibir la señal enviada por containerd, consulte hasta aquí [1]
Sin envío
Primero vea si se ha recibido, o el contenido de la goroutine. Como se muestra en la siguiente figura, puede ver que existen tanto goroutine: processEventStream para procesar eventos como goroutine: Subscribe para recibir eventos. se puede descartar la primera posibilidad

Inserte la descripción de la imagen aquí

Luego, observe la segunda posibilidad, no se emitió ningún evento de salida de tarea. Luego del análisis anterior, se sabe que hay una fuga de goroutine y es causada por docker stop, por lo que es seguro que kubelet ha iniciado una solicitud para eliminar el contenedor, y lo ha estado intentando, de lo contrario no se filtrará todo el tiempo. El único problema que queda es averiguar qué contenedor se elimina continuamente y por qué no se puede eliminar. De hecho, en este momento, es posible que haya pensado que existe una alta probabilidad de que haya un proceso D en el contenedor, por lo que el proceso del contenedor no puede salir normalmente incluso si se envía la señal Kill. El siguiente paso es verificar esta conjetura. Primero, averigüe qué contenedor tiene el problema. Primero mire el registro de Kubelet y el registro de la ventana acoplable, de la siguiente manera

Inserte la descripción de la imagen aquí
Inserte la descripción de la imagen aquí

Buen chico, no se puede eliminar más de un contenedor. Se verifica que el contenedor se borra constantemente, pero no se puede borrar. A continuación, veamos si hay un proceso D, de la siguiente manera

Inserte la descripción de la imagen aquí
Inserte la descripción de la imagen aquí
Inserte la descripción de la imagen aquí

Es cierto que hay procesos D en el contenedor, puede ir al host para ver, ps aux | awk '$ 8 = "D"', hay muchos procesos D.

para resumir

Para garantizar la coherencia final, Kubelet seguirá intentando eliminar cuando encuentre que hay contenedores en el host que no deberían existir. Cada vez que se elimine, se llamará a la API de docker stop para establecer una conexión de uds con dockerd. .dockerd se iniciará cuando se elimine el contenedor. Una goroutine llama a containerd en forma de rpc para eliminar el contenedor y espera a que se complete la eliminación final antes de regresar. Durante el proceso de espera, se crea otra goroutine para obtener el resultado. , cuando containerd llama a runc para ejecutar la eliminación, no se puede eliminar debido al proceso D en el contenedor. El contenedor no emite una señal de salida de tarea y las dos goroutines relacionadas de dockerd no se cerrarán. Todo el proceso se repite continuamente, lo que eventualmente conduce a la fuga de fd, memoria y goroutine paso a paso, y el sistema gradualmente se vuelve inutilizable.

Pensando en retrospectiva, en realidad no hay ningún problema con el procesamiento de kubelet en sí. El kubelet es para garantizar la coherencia. Es necesario eliminar los contenedores que no deberían existir hasta que el contenedor se elimine por completo. El tiempo de espera se establece cada vez que se llama a la API de Docker. . La lógica de dockerd está abierta a discusión, al menos se pueden hacer algunas mejoras, porque el cliente solicita el tiempo de espera, y el backend de dockerd realizará la operación de eliminación del contenedor después de recibir el evento de salida de la tarea, incluso si no hay una solicitud de parada de docker actualmente. Por lo tanto, puede considerar eliminar la última llamada a la función Wait pasada a context.Background (), y puede salir directamente después de que Wait con el tiempo de espera actual regrese, para no causar pérdida de recursos.

Inserte la descripción de la imagen aquí

Supongo que te gusta

Origin blog.csdn.net/liuxingjiaoyu/article/details/112259524
Recomendado
Clasificación