Prompt y contexto aplicable
Un servicio HTTP de Linux se ejecuta en un contenedor de Kubernetes. Durante una actualización continua, recibe SIGTERM con un período de gracia de 30 segundos mientras hay 200 solicitudes en curso, dos workers secundarios en ejecución y un consumidor de tareas en segundo plano activo. Diseña el protocolo de apagado desde la entrega de la señal hasta la salida del proceso. Explica el comportamiento de SIGTERM y SIGKILL, la seguridad del manejador de señales (signal-handler safety), el drenado de tráfico, el manejo de tareas y procesos secundarios, los límites de tiempo (deadlines) y la verificación.
Los 30 segundos y las 200 solicitudes son entradas del escenario de entrevista. Kubernetes utiliza comúnmente un período de gracia de terminación de Pods predeterminado de 30 segundos, pero los valores de producción deben provenir de la duración medida de las solicitudes, el tiempo de limpieza, la semántica de la carga de trabajo y los requisitos de disponibilidad. En este escenario, utiliza un deadline interno de drenado de 25 segundos y reserva cinco segundos para la limpieza final y la variación de programación (scheduling variance). Esa división es una decisión de ingeniería, no una garantía de la plataforma.
El problema central es un protocolo de ciclo de vida de procesos de Linux. Kubernetes y HTTP proporcionan el contexto operativo. Una respuesta sólida sigue la señal desde el kernel hasta el proceso de destino, la transforma en una transición de estado segura, controla la admisión y el trabajo en curso, y demuestra que la terminación se mantiene dentro del plazo límite.
Qué evalúa el entrevistador
Primero, el entrevistador comprueba la semántica de las señales. Un proceso puede capturar, bloquear o ignorar SIGTERM; su acción por defecto es la terminación. Una vez que una aplicación la captura, debe terminar por sí misma eventualmente. SIGKILL no se puede capturar, bloquear ni ignorar, por lo que no ofrece ninguna oportunidad de limpieza. Las señales estándar repetidas no son una cola duradera: múltiples instancias pendientes de la misma señal estándar pueden fusionarse (coalesce).
Segundo, el entrevistador comprueba si la señal llega al servicio. En un contenedor, un entry point en forma de shell puede dejar a /bin/sh -c como PID 1 e impedir que el ejecutable reciba el SIGTERM esperado. El servicio normalmente debe ser el entry point en forma exec, o un wrapper debe finalizar con exec. Un servicio que crea procesos secundarios también debe reenviar la terminación y recolectarlos (reap), o ejecutarse con un init ligero apropiado cuando no pueda realizar las tareas de PID 1 por sí mismo.
Tercero, el entrevistador busca un límite seguro para señales asíncronas (async-signal-safe boundary). Un manejador de señales en C puro interrumpe la ejecución normal en una instrucción arbitraria. Llamar a printf, asignar memoria, adquirir un mutex, cerrar un grafo de objetos de la aplicación o vaciar una biblioteca cliente puede causar interbloqueos (deadlocks) o corromper el estado. El manejador solo debe publicar una notificación mínima utilizando operaciones signal-safe; la ruta de control normal es la dueña de la limpieza.
Cuarto, el entrevistador evalúa el orden de apagado. El servicio debe pasar a estado no listo (unready) y dejar de aceptar nuevo trabajo, para luego drenar o cancelar el trabajo admitido bajo un plazo límite. Debe detener la ingesta de tareas sin perder la posesión del arrendamiento (lease), mantener abiertas las dependencias compartidas hasta que quienes las usan terminen, finalizar y recolectar los procesos secundarios, realizar vaciados de telemetría acotados en el tiempo y salir antes de que el orquestador escale a SIGKILL.
Finalmente, el entrevistador busca pruebas operativas. Una prueba unitaria del manejador por sí sola no puede probar la disposición de PIDs del contenedor, la eliminación de endpoints, el comportamiento de las conexiones, la reentrega de tareas, la recolección de procesos secundarios o el cumplimiento del período de gracia. La respuesta debe incluir pruebas a nivel de contenedor y a nivel de despliegue con criterios de éxito observables.
Preguntas para clarificar antes de responder
- ¿Quién envía la señal y a qué PID? Confirma el runtime de contenedores, la señal de parada configurada, el entry point, los wrappers y si la aplicación es el PID 1.
- ¿Qué consume los 30 segundos? Un hook
preStopde Kubernetes se ejecuta dentro del mismo período de gracia de terminación. Su duración reduce el tiempo restante para el drenado de la aplicación. - ¿Qué significa "en curso" (in flight)? Separa las solicitudes aceptadas, las conexiones keep-alive sin solicitudes, las respuestas en streaming, las conexiones actualizadas (upgraded) y el trabajo en cola de la aplicación. Necesitan diferentes políticas de finalización.
- ¿Puede el servicio rechazar nuevo trabajo de inmediato? Identifica el readiness probe, el balanceador de carga, el listener, la malla de servicios (service mesh) y cualquier emisor directo. La propagación del estado de no preparación no es instantánea.
- ¿Cuáles son los contratos de duración de solicitudes y de reintentos? Una lectura idempotente corta puede completarse; una carga larga o una escritura con efectos secundarios puede necesitar cancelación, traspaso o una clave de idempotencia.
- ¿Cómo se gestiona la propiedad de las tareas en segundo plano? Clarifica el momento de confirmación (acknowledgement), el tiempo de visibilidad o lease, el comportamiento de heartbeat, la reentrega y la idempotencia. "Detener el worker" no es seguro sin este contrato.
- ¿Quién es el dueño de los workers secundarios? Determina si el padre puede enviar señales a un grupo de procesos, si los secundarios tienen su propio protocolo de apagado y quién llama a
waitpid. - ¿Qué runtime maneja las señales? Un hilo dedicado a
sigwait, un callback del bucle de eventos y un manejador en C puro tienen diferentes límites de seguridad. Declara el comportamiento real del runtime. - ¿Qué resultado define el éxito? Establece límites para nuevas admisiones, trabajo completado y cancelado, efectos secundarios duplicados, procesos zombi, tiempo de salida y tasa de terminación forzada.
Estructura de respuesta en 30 segundos
"Verifico que un entry point en forma exec entregue SIGTERM a la aplicación. El manejador directo solo despierta la ruta de control normal. Esa ruta falla el readiness probe, detiene la admisión de solicitudes y tareas, y drena el trabajo aceptado con un deadline interno de 25 segundos. Luego cancela de forma segura el trabajo restante, finaliza y recolecta ambos procesos secundarios, realiza vaciados finales acotados y sale antes de que finalice el período de gracia de 30 segundos. SIGKILL no puede ejecutar código de limpieza. Pruebo el contenedor compilado bajo solicitudes y tareas concurrentes, verificando el corte de admisión, el comportamiento de reintentos, la recolección de procesos secundarios y el tiempo de salida."
Esta estructura establece el flujo de control. La respuesta detallada también debe cubrir la entrega de señales multihilo, las llamadas al sistema interrumpidas, las solicitudes de apagado repetidas y la diferencia entre la eliminación del endpoint y la admisión en el listener.
Análisis detallado paso a paso
Comienza con la ruta de entrega. Utiliza la forma ejecutable de ENTRYPOINT o CMD para que la aplicación reciba directamente la señal de parada del runtime. Si se necesita un wrapper para la configuración previa, finalízalo con exec "$@". Inspecciona el contenedor en ejecución en lugar de confiar ciegamente en el Dockerfile: verifica el PID 1, su línea de comandos, las relaciones padre-hijo y la señal de parada configurada. Una prueba de despliegue que envíe SIGTERM al contenedor es la comprobación decisiva.
Inicializa el mecanismo de despertar antes de instalar el manejador, e instala el manejador antes de anunciar que el servicio está listo (readiness). Un esquema al estilo de C puede usar un self-pipe no bloqueante:
static volatile sig_atomic_t stop_requested = 0;
static int wake_fd; /* initialized as nonblocking before sigaction */
static void on_term(int signo) {
int saved_errno = errno;
stop_requested = 1;
const unsigned char byte = 1;
(void)write(wake_fd, &byte, sizeof byte);
errno = saved_errno;
}write es seguro para señales asíncronas (async-signal-safe). El descriptor no bloqueante evita que el manejador espere si las notificaciones ya llenan la tubería; la bandera preserva el estado incluso si la escritura de despertar no puede añadir otro byte. El manejador no escribe logs, no asigna memoria, no bloquea mutexes, no espera por procesos secundarios ni llama a clientes de la aplicación. El bucle de eventos ordinario drena la tubería y avanza una máquina de estados de apagado idempotente.
Una alternativa para un servicio multihilo es bloquear las señales de terminación antes de crear los hilos de trabajo, y luego dejar que un único hilo dedicado llame a sigwait o use signalfd en Linux. La disposición de la señal es a nivel de proceso, mientras que cada hilo tiene su propia máscara de señales. Una señal dirigida al proceso puede ser entregada a cualquier hilo que no la bloquee. El manejo síncrono centralizado de señales elimina el manejador asíncrono del código de la aplicación, siempre que las máscaras de señales se establezcan de manera consistente antes de que los hilos inicien.
No dependas únicamente de una llamada al sistema interrumpida para despertar al servicio. Según la interfaz y SA_RESTART, una llamada bloqueante puede reanudarse automáticamente o devolver EINTR. El self-pipe, el descriptor de eventos, el canal de señales del runtime o el hilo de señales dedicado crean una ruta de despertar explícita. Cada espera bloqueante durante el apagado también debe tener un deadline.
Conduce el servicio a través de estados explícitos:
RUNNING
--SIGTERM--> QUIESCING
--admission closed--> DRAINING
--work finished or 25 s reached--> FINALIZING
--children reaped and bounded flush complete--> EXITEDLa transición desde RUNNING debe ser atómica e idempotente. El primer SIGTERM registra la hora de inicio y el deadline. Un segundo SIGTERM no debe iniciar otro grafo de limpieza ni cerrar el mismo recurso dos veces. El equipo puede elegir si solo registra una repetición o si acorta el tiempo de drenado, pero el comportamiento debe estar documentado y probado.
En QUIESCING, haz que el readiness falle y detén de inmediato la nueva admisión de la aplicación. Kubernetes marca un endpoint en terminación como no listo, pero la propagación en el plano de control y en los proxies toma tiempo. Cierra el socket de escucha, deshabilita los accepts o haz que la capa de admisión devuelva una respuesta con indicación de reintento para las solicitudes que no han cruzado el límite de trabajo aceptado. Las conexiones aceptadas existentes pueden permanecer abiertas para drenarse. Maneja HTTP keep-alive explícitamente para que una conexión antigua e inactiva no pueda enviar nuevas solicitudes ilimitadas después de iniciado el apagado.
Detén al consumidor en segundo plano para que no obtenga nuevas tareas en el mismo límite. Para una tarea ya arrendada, continúa solo si puede finalizar de forma segura antes del deadline. De lo contrario, detén los heartbeats o libera/rechaza (nack) el lease según el contrato de la cola para que otro worker pueda reintentarla. La confirmación (acknowledgement) debe realizarse después de la finalización persistente. Los efectos secundarios necesitan claves de idempotencia o transiciones de estado transaccionales, ya que puede ocurrir una terminación forzada después de una escritura externa y antes de la confirmación.
En DRAINING, rastrea el trabajo admitido con un contador o registro. Permite que las solicitudes terminen mientras sus dependencias sigan disponibles. No cierres el pool de conexiones a la base de datos, el cliente de caché ni el exportador de telemetría mientras los manejadores de solicitudes aún los estén utilizando. Al llegar al deadline interno de 25 segundos, cancela el trabajo restante según el protocolo: detén el streaming, propaga la cancelación, devuelve una respuesta definida cuando sea posible y deja el estado reintentable consistente. Reserva los cinco segundos restantes para callbacks de cancelación, recolección de procesos secundarios, escrituras de estado final y variación de programación del runtime.
Para los dos workers secundarios, detén su entrada primero, envía su señal de terminación acordada y espera con un deadline. Si el padre posee un grupo de procesos dedicado, puede enviar la señal a ese grupo evitando procesos no relacionados. Recolecta cada hijo terminado con waitpid para que no queden zombis. Un init ligero puede encargarse del reenvío de señales y de la recolección para un contenedor cuya aplicación no pueda hacerlo, pero no define la semántica de tareas o solicitudes de la aplicación.
En FINALIZING, emite las métricas finales de apagado y vacía logs o trazas bajo un presupuesto de tiempo estricto. La observabilidad ayuda a explicar las salidas forzadas, pero un backend de telemetría no disponible nunca debe consumir todo el período de gracia. Cierra los recursos restantes en orden de dependencia y devuelve un código de salida cero para un apagado ordenado exitoso. Si el proceso no cumple con el deadline de la plataforma, Kubernetes finalmente solicitará al runtime que envíe SIGKILL; ningún manejador, bloque diferido (deferred block) o hook de apagado se ejecutará después de ese punto.
Usa las señales como notificaciones de control, no como mensajes de trabajo. Las señales estándar pueden fusionarse, contienen poca carga útil y pueden llegar en ubicaciones de código inesperadas. Coloca el trabajo, los reintentos y la propiedad persistente en colas o almacenes de estado. La señal solo inicia o escala la transición del ciclo de vida local.
La verificación debe ejercitar el mismo límite utilizado en producción:
- Compila la imagen real, inspecciona el PID 1, inicia el servicio y envía
SIGTERMal contenedor en lugar de invocar un endpoint de apagado interno. - Mantén 200 solicitudes de duración mixta, incluyendo trabajo que termine dentro de los 25 segundos y trabajo que deba cancelarse. Asegura que ninguna solicitud sea admitida después del corte y que cada solicitud admitida tenga un resultado final registrado.
- Confirma que el readiness pase a false y que el Pod antiguo no reciba nuevo tráfico de despliegue tras la propagación del endpoint. Prueba también una conexión directa para que la admisión en el listener se verifique de forma independiente.
- Ejecuta tareas en segundo plano arrendadas durante la terminación. Verifica que el trabajo completado se confirme una sola vez, que el trabajo no finalizado sea elegible para reintento y que la entrega duplicada no duplique el efecto de negocio.
- Verifica que ambos workers secundarios reciban la terminación, salgan dentro de su límite de tiempo y sean recolectados. Inspecciona la tabla de procesos en busca de zombis.
- Envía un segundo
SIGTERMy demuestra que la limpieza sigue siendo idempotente. Por separado, envíaSIGKILLpara demostrar que no se asume ninguna limpieza y que los contratos de recuperación aún protegen el trabajo duradero. - Registra
shutdown_started, el estado de admisión, el conteo de trabajo en curso, los incumplimientos del deadline de drenado, el estado de los procesos secundarios, el tiempo de salida y las terminaciones forzadas. Falla la prueba si la salida ordenada alcanza los 30 segundos.
Respuesta de muestra de alta calidad
"Comenzaría en el límite del contenedor. Usaría un entry point en forma exec e inspeccionaría la imagen en tiempo de ejecución para verificar que el servicio HTTP sea el PID 1 o se encuentre detrás de un init que reenvíe señales. Un wrapper en shell terminaría con exec, de modo que SIGTERM no se detenga en el shell.
Antes de que el readiness sea true, el servicio instalaría su ruta de señales. En un manejador de C puro, solo establecería una bandera sig_atomic_t y escribiría en un self-pipe no bloqueante. Los registros de logs, mutexes, asignación de memoria, llamadas a bases de datos y esperas de procesos secundarios quedan fuera de ese manejador. En una implementación multihilo, preferiría bloquear las señales de terminación antes de crear workers y consumirlas desde un único hilo con sigwait. Cualquiera de los dos diseños despierta el bucle de control normal explícitamente en lugar de depender de EINTR.
El primer SIGTERM mueve atómicamente el servicio de running a quiescing y fija un deadline interno 25 segundos después. El servicio falla inmediatamente el readiness, cierra o deshabilita nuevas admisiones, evita que las conexiones keep-alive inicien más solicitudes y deja de obtener tareas en segundo plano. La eliminación de readiness y el cierre de admisión son necesarios porque la propagación de endpoints es asíncrona.
Las 200 solicitudes aceptadas pueden continuar mientras sus clientes de base de datos y caché permanezcan abiertos. Las rastreo directamente. Las solicitudes que se completan antes de los 25 segundos responden normalmente. En el deadline interno, cancelo el resto mediante el protocolo de la aplicación y preservo el estado reintentable. Para el consumidor de tareas, confirmo solo la finalización persistente; las tareas arrendadas no terminadas se liberan o se dejan expirar según el contrato de la cola, y sus efectos secundarios utilizan claves de idempotencia.
Luego finalizo los dos workers secundarios a través de su ruta de señal definida y los recolecto con una espera acotada. Solo después de que las solicitudes y los procesos secundarios hayan terminado, cierro los clientes compartidos. Los logs y trazas reciben un presupuesto de vaciado pequeño y acotado. Una ruta exitosa sale con código cero antes de los 30 segundos. Si se sobrepasa ese deadline, SIGKILL puede terminar el proceso y ningún código de limpieza se ejecutará, por lo que la corrección del estado duradero no puede depender del hook final.
Para la verificación, ejecuto el contenedor compilado con 200 solicitudes concurrentes de duración mixta y tareas arrendadas activas, y luego le envío SIGTERM. Verifico que el proceso real reciba la señal, el readiness cambie, ningún trabajo nuevo cruce el límite de admisión, el trabajo admitido se complete o se cancele explícitamente a los 25 segundos, las tareas inconclusas puedan reintentarse sin efectos duplicados, ambos procesos secundarios sean recolectados y el proceso termine antes de los 30 segundos. También pruebo un SIGTERM repetido para verificar la idempotencia y SIGKILL para el comportamiento de recuperación. El panel de despliegue debe exponer la duración del apagado, el trabajo en curso, los incumplimientos de deadline y las salidas forzadas."
Errores comunes
- Hacer la limpieza dentro del manejador de señales puro → La señal puede interrumpir el código mientras un lock de biblioteca o el estado del asignador están activos → Publica una notificación mínima signal-safe y realiza la limpieza en la ruta de control normal.
- Asumir que la aplicación recibe
SIGTERM→ Un entry point en forma de shell puede mantener al shell como PID 1 → Usa la forma exec o un wrapper que termine enexec, y luego prueba el contenedor compilado. - Tratar el fallo de readiness como el cierre de la admisión → Las actualizaciones de endpoints toman tiempo y las conexiones directas o existentes aún pueden enviar trabajo → Falla el readiness y aplica un corte de admisión/listener en la aplicación.
- Cerrar clientes compartidos primero → Los manejadores en curso pueden fallar después de la admisión a pesar de haber tenido tiempo para terminar → Drena a los usuarios antes de cerrar los recursos que necesitan.
- Detener un consumidor de tareas sin verificar los leases → El trabajo puede quedar invisible, confirmarse demasiado pronto o repetir efectos secundarios → Cumple explícitamente los contratos de confirmación, lease, reintento e idempotencia.
- Esperar indefinidamente un drenado perfecto → El orquestador eventualmente enviará
SIGKILLy eliminará toda oportunidad de limpieza → Usa un deadline interno con tiempo reservado para la finalización. - Olvidar la propiedad de los procesos secundarios → Los secundarios pueden sobrevivir brevemente al padre o convertirse en zombis si no se recolectan → Reenvía la terminación deliberadamente y usa un bucle
waitpidacotado. - Iniciar la limpieza dos veces ante señales repetidas → Operaciones duplicadas de cierre y vaciado pueden competir o fallar → Haz que la transición de estado sea atómica y la limpieza idempotente.
- Usar señales estándar como una cola de comandos → Señales estándar pendientes idénticas pueden fusionarse y no transportan información de propiedad persistente → Almacena trabajo y reintentos en una cola; usa la señal solo para el control del ciclo de vida.
- Probar únicamente un método de apagado interno → Omite la disposición de PIDs, la entrega de señales en tiempo de ejecución y el comportamiento de la orquestación → Envía señales reales a la imagen de producción bajo una carga de trabajo concurrente realista.
Preguntas de seguimiento y respuestas
Pregunta de seguimiento 1: ¿Por qué el manejador de señales no puede llamar a la función de apagado normal?
El manejador puede interrumpir el programa mientras otro hilo o el mismo hilo interrumpido mantiene locks del asignador de memoria, de stdio, de logging o de la aplicación. La mayoría de las funciones de limpieza de aplicaciones no son seguras para señales asíncronas (async-signal-safe). Llamarlas puede causar interbloqueos o corromper el estado interno. El manejador debe establecer una bandera y usar una operación de despertar segura para señales; el bucle de eventos o un hilo de señales dedicado invoca el código de apagado normal después.
Pregunta de seguimiento 2: ¿Cómo se comportan las señales en un proceso multihilo?
La disposición de la señal es compartida por todo el proceso, pero cada hilo tiene su propia máscara de señales. Una señal dirigida al proceso puede entregarse a cualquier hilo elegible que no la tenga bloqueada. Un patrón robusto bloquea las señales de terminación antes de que se creen los workers y permite que un solo hilo las reciba sincrónicamente con sigwait o signalfd. Otro utiliza un manejador mínimo a nivel de proceso que solo publica una notificación segura. Las máscaras mixtas e inconsistentes dificultan el razonamiento sobre el comportamiento.
Pregunta de seguimiento 3: ¿Cuál es la diferencia práctica entre SIGTERM y SIGKILL aquí?
SIGTERM solicita la terminación y le da a la aplicación la oportunidad de ejecutar su protocolo porque se puede capturar. Su acción predeterminada sigue siendo terminar el proceso. SIGKILL es una terminación forzada por el kernel: no se puede capturar, bloquear ni ignorar, y no se ejecuta ninguna limpieza. El período de gracia solo es valioso si la ruta de SIGTERM es alcanzable, segura y está acotada en el tiempo.
Pregunta de seguimiento 4: ¿Por qué fallar el readiness y además cerrar la admisión?
Los cambios en readiness indican a Kubernetes y a sus proxies que dejen de enrutar tráfico, pero las actualizaciones de endpoints y el drenado de conexiones son asíncronos. Las conexiones keep-alive existentes o directas aún pueden llegar al proceso. El corte de la aplicación define el punto exacto después del cual no puede entrar nuevo trabajo, mientras que readiness retira al Pod del enrutamiento normal. La verificación debe observar ambas capas.
Pregunta de seguimiento 5: ¿Qué debería suceder con una tarea que está a medio completar?
Utiliza el contrato de propiedad de la tarea. Continúa solo si puede terminar de manera segura antes del deadline interno. De lo contrario, detén o libera su lease para que pueda reintentarse, y evita confirmarla antes de su finalización duradera. Los efectos secundarios externos requieren una clave de idempotencia o una transición de estado transaccional, ya que la terminación puede ocurrir entre el efecto secundario y la confirmación.
Pregunta de seguimiento 6: ¿Qué cambia cuando existe un hook preStop?
El hook consume el mismo período de gracia de terminación del Pod. Mide su duración en el peor de los casos y réstala del presupuesto de la aplicación. Mantén el hook acotado y evita duplicar la limpieza de la aplicación en dos rutas que compitan entre sí. La aplicación aún debe manejar SIGTERM, ya que los hooks pueden fallar y los procesos pueden recibir señales fuera de un despliegue normal.
Pregunta de seguimiento 7: ¿Cómo investigarías terminaciones forzadas en producción?
Correlaciona el motivo de terminación del Pod y las marcas de tiempo con la hora de inicio de apagado de la aplicación, el recuento de solicitudes en curso, los leases de tareas, el estado de los procesos secundarios y el último estado de apagado completado. Separa la falta de entrega de señales de un drenado lento, un proceso secundario bloqueado o un vaciado final congelado. Monitorea la duración del apagado ordenado y la tasa de salidas forzadas por versión para que cualquier regresión aparezca durante un despliegue canary.