¿Cómo logra Go un reinicio en caliente?

Autor: zhijiezhang, ingenieros de desarrollo de antecedentes de Tencent PCG

Recientemente, descubrí un problema relacionado con el reinicio en caliente al optimizar el marco trpc de la empresa. Después de la optimización, también resumí y resolví, e hice una revisión simple de cómo lograr el reinicio en caliente en marcha.

1. ¿Qué es un reinicio en caliente?

El reinicio en caliente (Hot Restart) es un medio para garantizar la disponibilidad del servicio. Permite que la conexión establecida no se interrumpa durante el reinicio del servicio, el antiguo proceso de servicio ya no acepta nuevas solicitudes de conexión y la nueva solicitud de conexión se aceptará en el nuevo proceso de servicio. Para la conexión que se ha establecido en el proceso de servicio original, también puede configurarla para leer y esperar a que la solicitud en la conexión se procese sin problemas y la conexión esté inactiva antes de salir. De esta manera, se puede garantizar que la conexión establecida no se interrumpa, la transacción (solicitud, procesamiento, respuesta) en la conexión se puede completar con normalidad y el nuevo proceso de servicio también puede aceptar la conexión y procesar la solicitud en la conexión normalmente. Por supuesto, la salida sin problemas del proceso durante el reinicio en caliente implica no solo la transacción conectada, sino también el servicio de mensajes y las transacciones personalizadas que requieren atención.

Esta es una descripción aproximada del reinicio en caliente según tengo entendido. ¿Es necesario un reinicio en caliente ahora? Mi entendimiento es mirar la escena.

Tome el desarrollo en segundo plano como ejemplo. Si la plataforma de operación y mantenimiento tiene la capacidad de iniciar automáticamente el tráfico cuando el servicio se actualiza o reinicia, y agregar automáticamente el tráfico cuando el servicio está listo, si el QPS del servicio y el tiempo de procesamiento de la solicitud se pueden estimar razonablemente, entonces configure un valor razonable El tiempo de espera antes de detenerse puede lograr un efecto similar al de un reinicio en caliente. En este caso, no es necesario admitir el reinicio en caliente en el servicio en segundo plano. Sin embargo, si desarrollamos un marco de microservicio, no podemos hacer tales suposiciones sobre la plataforma y el entorno de implementación futuros. También es posible que el usuario solo implemente en una o dos máquinas físicas sin otras instalaciones de equilibrio de carga, pero no queremos que nos afecte el reinicio. Interferencia, es necesario un reinicio en caliente. Por supuesto, existen algunos escenarios más complejos y exigentes que también requieren capacidades de reinicio en caliente.

El reinicio en caliente es un medio más importante para garantizar la calidad del servicio, y vale la pena entenderlo. Esta es también la intención original de este artículo.

2. ¿Cómo lograr un reinicio en caliente?

De hecho, aquí no se puede generalizar cómo lograr un reinicio en caliente, sino que se debe combinar con escenarios reales (como el modelo de programación de servicios, los requisitos de disponibilidad, etc.). La idea aproximada de la realización puede desecharse primero.

Generalmente, para lograr un reinicio en caliente, se deben incluir aproximadamente los siguientes pasos:

  • Primero, deje que el proceso anterior, llamado proceso padre aquí, primero bifurque un proceso hijo para reemplazarlo;

  • Luego, una vez que el proceso hijo esté listo, notifique al proceso padre, acepte la nueva solicitud de conexión normalmente y procese la solicitud recibida en la conexión;

  • Luego, una vez que el proceso principal ha procesado la solicitud en la conexión establecida y la conexión está inactiva, se cierra sin problemas.

Suena simple ...

2.1. Conozca la bifurcación

Todos conocen la fork()llamada al sistema, el proceso padre que llama a la bifurcación creará una copia del proceso, y el código también puede distinguir si es un proceso hijo o un proceso padre por si el valor de retorno de la bifurcación es 0.

int main(char **argv, int argc) {
    pid_t pid = fork();
    if (pid == 0) {
        printf("i am child process");
    } else {
        printf("i am parent process, i have a child process named %d", pid);
    }
}

Es posible que algunos desarrolladores no conozcan el principio de implementación de la bifurcación, o por qué el valor de retorno de la bifurcación es diferente en el proceso padre-hijo, o cómo hacer que el valor de retorno del proceso padre-hijo sea diferente ... Comprender esto requiere un poco de acumulación de conocimientos.

2.2. Valor devuelto

En un breve resumen, ABI define algunas especificaciones para llamadas a funciones, cómo pasar parámetros, cómo devolver valores, etc. Tomando x86 como ejemplo, si el valor de retorno es lo que puede contener el registro rax, generalmente se devuelve a través del registro rax.

¿Qué pasa si el ancho de bits del registro rax no puede acomodar el valor de retorno? También es simple, el compilador insertará algunas instrucciones para completar estas misteriosas operaciones, las instrucciones específicas están relacionadas con la implementación del compilador del lenguaje.

  • En el lenguaje c, la dirección del valor de retorno puede pasarse a rdi u otros registros Dentro de la función llamada, el valor de retorno se escribe en el área de memoria referida por rdi a través de múltiples instrucciones;

  • En el lenguaje c, también es posible usar múltiples registros rax, rdx ... para almacenar temporalmente el resultado devuelto en la función llamada, y luego asignar los valores de múltiples registros a la variable cuando la función regresa;

  • También puede regresar a través de la memoria de pila como golang;

2.3.valor de retorno de la horquilla

El valor de retorno de la llamada al sistema de la bifurcación es un poco especial. En el proceso padre y en el proceso hijo, el valor de retorno de esta función es diferente. ¿Cómo hacerlo?

Cuando Lenovo llama a la bifurcación desde el proceso principal, ¿qué necesita hacer el kernel del sistema operativo? Asignar bloques de control de procesos, asignar pids, asignar espacio de memoria ... Debe haber muchas cosas, aquí preste atención a la información de contexto de hardware del proceso, estas son muy importantes, cuando el proceso es seleccionado por el algoritmo de programación para la programación, es necesario restaurar la información de contexto de hardware de.

Cuando Linux se bifurca, hará ciertas modificaciones en el contexto de hardware del proceso hijo. Solo te dejo que el pid después de la bifurcación sea 0. ¿Qué debo hacer? Como se mencionó en la sección 2.2 anterior, para esos enteros pequeños, el registro rax es más que suficiente.Cuando la bifurcación regresa, el pid asignado por el sistema operativo se coloca en el registro rax.

Luego, para el proceso hijo, solo necesito borrar su registro rax de contexto de hardware a 0 cuando se bifurca, y luego esperar a que otras configuraciones estén bien, luego cambiar su estado de estado de espera ininterrumpible a estado ejecutable, y esperar a que sea Cuando el programador está programando, primero restaurará su información de contexto de hardware, incluyendo PC, rax, etc., de modo que después de que la bifurcación regrese, el valor medio de rax sea 0 y el valor final asignado a pid sea 0.

Por lo tanto, es posible distinguir si el proceso actual es un proceso padre o un proceso hijo mediante esta forma de juzgar "si pid es igual a 0".

2.4. Limitaciones

Mucha gente sabe que la bifurcación puede crear una copia de un proceso y continuar ejecutándolo, y se pueden ejecutar diferentes lógicas de bifurcación de acuerdo con el valor de retorno de la bifurcación. Si el proceso es de varios subprocesos, ¿llamar a fork en un subproceso copiará todo el proceso?

Fork solo puede crear una copia del hilo que llama a la función. Para otros hilos en ejecución en el proceso, no se procesará fork. Esto significa que para los programas multiproceso, no es factible esperar crear una copia completa del proceso a través de la bifurcación.

Como mencionamos anteriormente, la bifurcación es una parte importante para realizar el reinicio en caliente. La limitación de la bifurcación aquí restringe la implementación del reinicio en caliente bajo diferentes modelos de programación de servicios. Entonces decimos que los problemas específicos se analizan en detalle y que diferentes implementaciones pueden usarse en diferentes modelos de programación.

3. Modelo de un solo proceso de un solo hilo

El modelo de un solo proceso de un solo subproceso puede ser considerado obsoleto por muchas personas y no se puede utilizar en el entorno de producción, ¿de verdad? Más fuerte que redis, no solo de un solo hilo. No es inútil enfatizar que el modelo de un solo subproceso no es inútil, ok, retírelo y ahora céntrese en cómo el modelo de un solo subproceso del proceso único puede lograr un reinicio en caliente.

Proceso único e hilo único, es más fácil realizar el reinicio en caliente:

  • Puede crear un proceso hijo con una bifurcación,

  • El proceso hijo puede heredar los recursos del proceso padre, como los descriptores de archivos abiertos, incluidos listenfd y connfd del proceso padre,

  • El proceso padre puede optar por cerrar listenfd, y la tarea posterior de aceptar la conexión se entregará al proceso hijo para que la complete.

  • El proceso padre puede incluso cerrar connfd, lo que permite al proceso hijo procesar solicitudes de conexión, devolver paquetes, etc., y también puede procesar solicitudes en conexiones establecidas por sí mismo;

  • El proceso padre elige salir en un momento apropiado y el proceso hijo comienza a convertirse en el pilar.

Las ideas centrales son estas, pero cuando se trata de materialización, hay muchas formas:

  • Puede elegir el método de la bifurcación para permitir que el proceso hijo obtenga el listenfd original, connfd,

  • También puede elegir la forma de socket de unixdomain, el proceso padre enviará listenfd, connfd al proceso hijo.

Algunos estudiantes pueden pensar, ¿no puedo aprobar estos fd?

  • Por ejemplo, cuando abro el puerto de reutilización, el proceso padre procesa directamente la solicitud en la conexión establecida connfd y luego la cierra. El proceso de reutilización.Listen en el proceso hijo crea directamente un nuevo listenfd.

¡Si tambien! Pero algunas cuestiones deben considerarse de antemano:

  • Aunque reuseport permite que varios procesos escuchen en el mismo puerto varias veces, parece cumplir con los requisitos, pero debe saber que siempre que el euid sea el mismo, ¡puede escuchar en este puerto! ¡No es seguro!

  • La implementación de reuseport está relacionada con la plataforma. Si escuchas la misma dirección + puerto varias veces en la plataforma Linux, varias capas inferiores de listenfd pueden compartir la misma cola de conexión, y el kernel puede lograr el equilibrio de carga, ¡pero no en la plataforma darwin!

Por supuesto, los problemas mencionados aquí ciertamente existen bajo el modelo de subprocesos múltiples.

4. Modelo multiproceso de un solo proceso

Los problemas antes mencionados también aparecen en el modelo multiproceso:

  • ¡Fork solo puede copiar el hilo de llamada, no todo el proceso!

  • Múltiples fd obtenidos por reuseport en la misma dirección + puerto escuchan varias veces, diferentes plataformas tienen diferentes rendimientos y es posible que no puedan lograr un equilibrio de carga al aceptar conexiones.

  • En el caso de no reutilización, ¡la escucha repetida fallará!

  • No pase fd, vuelva a escuchar directamente a través de reuseport para obtener listenfd, no es seguro, diferentes instancias de procesos de servicio pueden escuchar en el mismo puerto, gg!

  • La lógica del proceso padre sale sin problemas, cierre listenfd, espere el final del procesamiento de la solicitud en connfd, cierre connfd, después de que todo esté en orden, el proceso padre sale y el proceso hijo toma la iniciativa.

5. Otros modelos de roscado

Básicamente, otros hilos no pueden evitar la implementación o combinación de los 3 y 4 anteriores, y los problemas correspondientes son similares, por lo que no los repetiré.

6. Ir a lograr un reinicio en caliente: tiempo de activación

Necesito elegir un momento para activar un reinicio en caliente. ¿Cuándo debería activarse? El sistema operativo proporciona un mecanismo de señal que permite al proceso realizar un procesamiento de señal personalizado.

Matar un proceso generalmente kill -9envía una señal SIGKILL al proceso. Esta señal no se permite capturar, y SIGABORT tampoco se permite capturar. Esto permite que el propietario del proceso o un usuario con altos privilegios controle la vida y muerte del proceso y logre mejores efectos de gestión.

Kill también se puede utilizar para enviar otras señales al proceso, como enviar SIGUSR1, SIGUSR2, SIGINT, etc. El proceso puede recibir estas señales y tratarlas en consecuencia. Aquí puede elegir SIGUSR1 o SIGUSR2 para notificar el proceso de reinicio en caliente.

go func() {
    ch := make(chan os.Signal, 1)
    signal.Notify(ch, os.SIGUSR2)
    <- ch

    //接下来就可以做热重启相关的逻辑了
    ...
}()

7. Cómo juzgar un reinicio en caliente

Una vez que se reinicia el programa go, toda la información del estado del tiempo de ejecución es nueva, entonces, ¿cómo puedo saber si soy un proceso secundario o si quiero realizar una lógica de reinicio en caliente? El proceso padre puede establecer la variable de entorno cuando se inicializa el proceso hijo, como agregar HOT_RESTART = 1.

Esto requiere que el código primero verifique si la variable de entorno HOT_RESTART es 1 en el lugar apropiado, si es verdadero, luego ejecute la lógica de reinicio en caliente; de ​​lo contrario, ejecute una nueva lógica de inicio.

8. ForkExec

Si el proceso actual quiere ejecutar la lógica de reinicio en caliente después de recibir la señal SIGUSR2, entonces bien, debe ejecutar syscall.ForkExec (...) para crear un proceso hijo. Tenga en cuenta que go es diferente de cc ++, que se basa en múltiples subprocesos para programar el protocolo. Cheng es, naturalmente, un programa de subprocesos múltiples, pero no utilizó la biblioteca de subprocesos NPTL para crearlo, sino a través de la llamada al sistema de clonación.

Como se mencionó anteriormente, si simplemente realiza un fork, solo puede copiar el hilo que llama a la función fork, y no puede hacer nada con otros hilos en el proceso. Por lo tanto, para un programa natural de múltiples subprocesos como go, debe reiniciarlo desde el principio y ejecutarlo nuevamente. Entonces, la función proporcionada por la biblioteca estándar de go es syscall.ForkExec en lugar de syscall.Fork.

9. Vaya a lograr un reinicio en caliente: pase listenfd

Hay varias formas de pasar fd en go. Cuando el proceso padre bifurca el proceso hijo, pase fd o páselo a través del socket de dominio Unix más tarde. Cabe señalar que lo que pasamos es en realidad una descripción de archivo, no un descriptor de archivo.

Se adjunta un diagrama de la relación entre el descriptor de archivo, la descripción de archivo y el inodo en sistemas similares a Unix:

Fd se asigna de pequeño a grande. El fd en el proceso padre es 10, y puede que no sea 10 después de pasarlo al proceso hijo. Entonces, ¿es predecible el fd pasado al proceso hijo? Se puede predecir, pero no se recomienda. Así que proporciono dos formas de lograrlo.

9.1 ForkExec + ProcAttr {Archivos: [] uintptr {}}

Pasar un listenfd es muy simple, si es de tipo net.Listener, utilice tcpln := ln.(*net.TCPListener); file, _ := tcpln.File(); fd := file.FD()para obtener el fd correspondiente a la descripción del archivo subyacente del oyente.

Cabe señalar que el fd aquí no es el fd inicial correspondiente a la descripción del archivo subyacente, sino un fd copiado por dup2 (asignado cuando se llama a tcpln.File ()), por lo que el recuento de referencias de la descripción del archivo subyacente será +1. Si desea cerrar el conector de escucha a través de ln.Close (), lo siento, no puede cerrarlo. Aquí debe ejecutar file.Close () para cerrar el fd recién creado, hacer el recuento de referencia de descripción de archivo correspondiente -1, y asegurarse de que el recuento de referencia sea 0 cuando se cierra, antes de que pueda cerrarse normalmente.

Imagine que si queremos lograr un reinicio en caliente, debemos esperar a que se procese la solicitud recibida en la conexión antes de poder salir del proceso, pero durante este período el proceso principal ya no puede recibir nuevas solicitudes de conexión. Si el oyente no se puede cerrar normalmente aquí, entonces nuestro objetivo No se puede lograr. Por lo tanto, el manejo de fd desde dup debería ser más cuidadoso aquí, no lo olvide.

Bien, hablemos de syscall.ProcAttr {Archivos: [] uintptr {}}, aquí está el fd en el proceso padre que se debe pasar, por ejemplo, para pasar stdin, stdout, stderr al proceso hijo, debe transferir estos Inserte fd en os.Stdin.FD (), os.Stdout.FD (), os.Stderr.FD (). Si desea pasar el listenfd en este momento, debe insertar el file.FD()fd devuelto anteriormente .

Después de recibir estos fd en el proceso hijo, el sistema similar a Unix generalmente asignará fd en orden creciente de 0, 1, 2, 3, entonces el fd pasado es predecible, excepto para stdin, stdout, stderr Pase dos listenfd más, entonces se puede predecir que el fd de estos dos debería ser 3, 4. Esto generalmente se hace en un sistema similar a Unix. El proceso hijo puede comenzar a contar desde 3 de acuerdo con el número de fd pasados ​​(por ejemplo, pasados ​​al proceso hijo FD_NUM = 2 a través de variables de entorno). Oh, estos dos fd deberían ser 3, 4.

Los procesos padre e hijo pueden organizar la secuencia del listenfd pasado a través de un orden acordado para facilitar el procesamiento del proceso hijo de acuerdo con el mismo acuerdo. Por supuesto, también puede usar fd para reconstruir el oyente para determinar la red de oyente correspondiente + dirección para distinguir al oyente A qué servicio lógico corresponde. ¡Todo es posible!

Cabe señalar que el fd devuelto por file.FD () no es de bloqueo, lo que afectará a la descripción del archivo subyacente. Antes de reconstruir el oyente, primero configúrelo en nonblock, syscall.SetNonBlock (fd), y luego file, _ := os.NewFile(fd); tcplistener := net.FileListener(file), o sí udpconn := net.PacketConn(file), puede obtenerlo Las direcciones de escucha de tcplistener y udpconn están asociadas con sus correspondientes servicios lógicos.

Como se mencionó anteriormente, file.FD () establecerá la descripción del archivo subyacente en modo de bloqueo. Agregaré aquí que net.FileListener (f), net.PacketConn (f) llamará a newFileFd () -> dupSocket () internamente. Varias funciones restablecen internamente la descripción del archivo correspondiente a fd a no bloqueo. La descripción del archivo correspondiente al oyente se comparte en los procesos padre e hijo, por lo que no es necesario mostrarlo como sin bloqueo.

Algunos marcos de microservicios admiten la agrupación de servicios lógica de servicios. La especificación de Google PB también admite varias definiciones de servicios. Esto también es compatible con los marcos Goat y Trpc de Tencent.

Por supuesto, aquí no escribiré una demostración completa que contenga todas las descripciones anteriores para todos. Esto es un poco de espacio. Aquí hay solo una versión condensada del ejemplo. Otros lectores pueden codificar y probar por sí mismos si están interesados. Es importante saber que es demasiado superficial en el papel, por lo que aún tienes que practicar más.

package main

import (
 "fmt"
 "io/ioutil"
 "log"
 "net"
 "os"
 "strconv"
 "sync"
 "syscall"
 "time"
)

const envRestart = "RESTART"
const envListenFD = "LISTENFD"

func main() {

 v := os.Getenv(envRestart)

 if v != "1" {

  ln, err := net.Listen("tcp", "localhost:8888")
  if err != nil {
   panic(err)
  }

  wg := sync.WaitGroup{}
  wg.Add(1)
  go func() {
   defer wg.Done()
   for {
    ln.Accept()
   }
  }()

  tcpln := ln.(*net.TCPListener)
  f, err := tcpln.File()
  if err != nil {
   panic(err)
  }

  os.Setenv(envRestart, "1")
  os.Setenv(envListenFD, fmt.Sprintf("%d", f.Fd()))

  _, err = syscall.ForkExec(os.Args[0], os.Args, &syscall.ProcAttr{
   Env:   os.Environ(),
   Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), f.Fd()},
   Sys:   nil,
  })
  if err != nil {
   panic(err)
  }
  log.Print("parent pid:", os.Getpid(), ", pass fd:", f.Fd())
  f.Close()
  wg.Wait()

 } else {

  v := os.Getenv(envListenFD)
  fd, err := strconv.ParseInt(v, 10, 64)
  if err != nil {
   panic(err)
  }
  log.Print("child pid:", os.Getpid(), ", recv fd:", fd)

  // case1: 理解上面提及的file descriptor、file description的关系
  // 这里子进程继承了父进程中传递过来的一些fd,但是fd数值与父进程中可能是不同的
  // 取消注释来测试...
  //ff := os.NewFile(uintptr(fd), "")
  //if ff != nil {
  // _, err := ff.Stat()
  // if err != nil {
  //  log.Println(err)
  // }
  //}

  // case2: 假定父进程中共享了fd 0\1\2\listenfd给子进程,那再子进程中可以预测到listenfd=3
  ff := os.NewFile(uintptr(3), "")
  fmt.Println("fd:", ff.Fd())
  if ff != nil {
   _, err := ff.Stat()
   if err != nil {
    panic(err)
   }

   // 这里pause, 运行命令lsof -P -p $pid,检查下有没有listenfd传过来,除了0,1,2,应该有看到3
   // ctrl+d to continue
   ioutil.ReadAll(os.Stdin)

   fmt.Println("....")
   _, err = net.FileListener(ff)
   if err != nil {
    panic(err)
   }

   // 这里pause, 运行命令lsof -P -p $pid, 会发现有两个listenfd,
   // 因为前面调用了ff.FD() dup2了一个,如果这里不显示关闭,listener将无法关闭
   ff.Close()

   time.Sleep(time.Minute)
  }

  time.Sleep(time.Minute)
 }
}

Aquí hay un código simple que explica aproximadamente cómo usar ProcAttr para pasar listenfd. Aquí hay una pregunta: ¿Qué sucede si el fd pasado en el proceso padre posterior se modifica, por ejemplo, el fd de stdin, stdout y stderr no se pasa? ¿El servidor comienza a predecir que debería comenzar a numerar desde 0? Podemos notificar al proceso hijo a través de variables de entorno, por ejemplo, de qué número es listenfd el fd pasado, hay varios listenfd, por lo que esto también se puede lograr.

Esta implementación puede ser multiplataforma.

Si está interesado, puede consultar esta gracia de implementación proporcionada por Facebook .

9.2 socket de dominio Unix + cmsg

Otra forma de pensar es pasarlo a través del socket de dominio Unix + cmsg.Cuando el proceso padre comienza, todavía usa ForkExec para crear el proceso hijo, pero no pasa el listenfd a través de ProcAttr.

Antes de crear el proceso hijo, el proceso padre crea un socket de dominio Unix y escucha. Una vez iniciado el proceso hijo, se establece una conexión a este socket de dominio Unix. El proceso padre comienza a enviar listenfd al proceso hijo a través de cmsg, y la forma de obtener fd es la misma. 9.1 es el mismo, el problema de cierre de fd que debe tenerse en cuenta también se maneja de la misma manera.

El proceso hijo se conecta al socket de dominio Unix y comienza a recibir cmsg. Cuando el kernel ayuda al proceso hijo a recibir mensajes, encuentra que hay un fd del proceso padre. El kernel busca la descripción del archivo correspondiente y asigna un fd al proceso hijo para establecer un mapeo entre los dos relación. Luego, al regresar al proceso hijo, el proceso hijo obtiene el fd correspondiente a la descripción del archivo. Puede obtener el archivo a través de os.NewFile (fd), y luego puede obtener tcplistener o udpconn a través de net.FileListener o net.PacketConn.

El resto de las acciones de obtener la dirección de escucha y asociar el servicio lógico son las mismas que se describen en el resumen 9.1.

Aquí también proporciono una versión simplificada ejecutable de la demostración para que todos la comprendan y prueben.

package main

import (
 "fmt"
 "io/ioutil"
 "log"
 "net"
 "os"
 "strconv"
 "sync"
 "syscall"
 "time"

 passfd "github.com/ftrvxmtrx/fd"
)

const envRestart = "RESTART"
const envListenFD = "LISTENFD"
const unixsockname = "/tmp/xxxxxxxxxxxxxxxxx.sock"

func main() {

 v := os.Getenv(envRestart)

 if v != "1" {

  ln, err := net.Listen("tcp", "localhost:8888")
  if err != nil {
   panic(err)
  }

  wg := sync.WaitGroup{}
  wg.Add(1)
  go func() {
   defer wg.Done()
   for {
    ln.Accept()
   }
  }()

  tcpln := ln.(*net.TCPListener)
  f, err := tcpln.File()
  if err != nil {
   panic(err)
  }

  os.Setenv(envRestart, "1")
  os.Setenv(envListenFD, fmt.Sprintf("%d", f.Fd()))

  _, err = syscall.ForkExec(os.Args[0], os.Args, &syscall.ProcAttr{
   Env:   os.Environ(),
   Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), /*f.Fd()*/}, // comment this when test unixsock
   Sys:   nil,
  })
  if err != nil {
   panic(err)
  }
  log.Print("parent pid:", os.Getpid(), ", pass fd:", f.Fd())

  os.Remove(unixsockname)
  unix, err := net.Listen("unix", unixsockname)
  if err != nil {
   panic(err)
  }
  unixconn, err := unix.Accept()
  if err != nil {
   panic(err)
  }
  err = passfd.Put(unixconn.(*net.UnixConn), f)
  if err != nil {
   panic(err)
  }

  f.Close()
  wg.Wait()

 } else {

  v := os.Getenv(envListenFD)
  fd, err := strconv.ParseInt(v, 10, 64)
  if err != nil {
   panic(err)
  }
  log.Print("child pid:", os.Getpid(), ", recv fd:", fd)

  // case1: 有同学认为以通过环境变量传fd,通过环境变量肯定是不行的,fd根本不对应子进程中的fd
  //ff := os.NewFile(uintptr(fd), "")
  //if ff != nil {
  // _, err := ff.Stat()
  // if err != nil {
  //  log.Println(err)
  // }
  //}

  // case2: 如果只有一个listenfd的情况下,那如果fork子进程时保证只传0\1\2\listenfd,那子进程中listenfd一定是3
  //ff := os.NewFile(uintptr(3), "")
  //if ff != nil {
  // _, err := ff.Stat()
  // if err != nil {
  //  panic(err)
  // }
  // // pause, ctrl+d to continue
  // ioutil.ReadAll(os.Stdin)
  // fmt.Println("....")
  // _, err = net.FileListener(ff) //会dup一个fd出来,有多个listener
  // if err != nil {
  //  panic(err)
  // }
  // // lsof -P -p $pid, 会发现有两个listenfd
  // time.Sleep(time.Minute)
  //}
  // 这里我们暂停下,方便运行系统命令来查看进程当前的一些状态
  // run: lsof -P -p $pid,检查下listenfd情况

  ioutil.ReadAll(os.Stdin)
  fmt.Println(".....")

  unixconn, err := net.Dial("unix", unixsockname)
  if err != nil {
   panic(err)
  }

  files, err := passfd.Get(unixconn.(*net.UnixConn), 1, nil)
  if err != nil {
   panic(err)
  }

  // 这里再运行命令:lsof -P -p $pid再检查下listenfd情况

  f := files[0]
  f.Stat()

  time.Sleep(time.Minute)
 }
}

Esta implementación está limitada a sistemas similares a Unix.

Si hay una situación de servicio mixto, debe considerar el nombre de archivo del socket de dominio Unix utilizado para evitar problemas causados ​​por el mismo nombre. Puede considerar usar "nombre de proceso.pid" como el nombre del socket de dominio Unix y usar variables de entorno para cambiar Se pasa al proceso hijo.

10. Ir a lograr un reinicio en caliente: cómo reconstruir el oyente a través de listenfd

Como se mencionó anteriormente, cuando obtengo fd, no sé si corresponde al oyente tcp o udpconn. ¿Qué debo hacer? Pruébelo todo.

file, err := os.NewFile(fd)
// check error

tcpln, err := net.FileListener(file)
// check error

udpconn, err := net.PacketConn(file)
// check error

11. Ir a lograr un reinicio en caliente: el proceso principal se cierra sin problemas

¿Cómo sale sin problemas el proceso padre? Esto depende de la lógica del proceso padre para que se detenga sin problemas.

11.1. Procesamiento de la solicitud en la conexión establecida

Puedes partir de estos dos aspectos:

  • apagado leído, ya no acepta nuevas solicitudes, el par percibirá fallas cuando continúe escribiendo datos;

  • Continúe procesando las solicitudes que se han recibido normalmente en la conexión. Una vez completado el procesamiento, devuelva el paquete y cierre la conexión;

También se puede considerar que en lugar de cerrar el lector, espere hasta que la conexión esté inactiva por un período de tiempo antes de cerrar. Si se cierra lo antes posible está más en línea con los requisitos, debe combinarse con la escena y los requisitos.

Si los requisitos de disponibilidad son más estrictos, es posible que también deba considerar pasar los datos de búfer leídos y escritos en connfd y connfd al proceso hijo para su procesamiento.

11.2. Servicio de mensajes

  • Confirme si el consumo de mensajes y el mecanismo de confirmación de su servicio son razonables

  • No más mensajes nuevos

  • Salir después de procesar el mensaje recibido

11.3. Personalizar la tarea de limpieza AtSalir

Algunas tareas tendrán algunas tareas personalizadas y esperan que el proceso se pueda ejecutar antes de salir. Esto puede proporcionar una función de registro similar a AtSalir, permitiendo que el proceso ejecute la lógica de limpieza definida por el negocio antes de salir.

Ya sea que se trate de un reinicio suave u otras salidas normales, existe una cierta demanda de este soporte.

12. Otro

En algunos escenarios, también es deseable pasar connfd, incluidos los datos de lectura y escritura correspondientes en connfd.

Por ejemplo, en el escenario de la reutilización de la conexión, el cliente puede enviar varias solicitudes a través de la misma conexión. Si el servidor realiza una operación de reinicio en caliente en algún punto intermedio, si el servidor se conecta directamente para leer y se cierra, la transmisión de datos posterior del cliente fallará. Si la conexión se cierra al final, es posible que la solicitud recibida anteriormente no responda normalmente. En este caso, puede considerar que el servidor continúe procesando la solicitud de conexión, esperar hasta que la conexión esté inactiva y luego cerrarla. ¿Estará siempre inactivo? posible.

De hecho, el servidor no puede predecir si el cliente adoptará el modo de multiplexación de conexión. Sería mejor elegir un método de procesamiento más confiable. Si los requisitos de la escena son más exigentes, no se espera que la capa superior intente resolverlo. Se puede considerar que esto pasa connfd y los datos de búfer leídos y escritos en connfd al proceso secundario, y entregárselos al proceso secundario para su procesamiento. En este momento, hay más puntos a los que prestar atención y el procesamiento es más complicado. Si está interesado, puede consultar la implementación de mosn .

13. Resumen

Como una forma de garantizar el reinicio y la actualización sin problemas de los servicios, el reinicio en caliente sigue siendo muy valioso en la actualidad. Este artículo describe algunas ideas generales para implementar un reinicio en caliente y describe cómo implementarlo en el servicio Go paso a paso a través de una demostración. Aunque no he proporcionado un ejemplo completo de reinicio en caliente para todos, creo que debería poder implementarlo usted mismo después de leerlo.

Debido al nivel limitado del autor, es inevitable que haya omisiones en la descripción. Corrígeme.

Articulo de referencia

  1. Programación avanzada Unix: comunicación entre procesos, Steven Richards

  2. proceso de inicio de mosn: https://mosn.io/blog/code/mosn-startup/

Supongo que te gusta

Origin blog.csdn.net/Tencent_TEG/article/details/108505187
Recomendado
Clasificación