Tema representativo de entrevista

Entrevista de diseño de sistemas: ¿Cómo diseñarías un controlador de dispositivo de bloques recuperable en el espacio de usuario?

Diseño de sistemasDifícil
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

Diseña un sistema que ubique la lógica del dispositivo de bloques virtual en el espacio de usuario. Debe soportar E/S de alta concurrencia, recuperación ante fallas del proceso en el espacio de usuario, aislamiento de permisos y observabilidad. Explica el límite del protocolo entre el plano de control y el de datos.

Planteamiento y contexto

Diseña un sistema que ubique la lógica del dispositivo de bloques virtual en el espacio de usuario: el kernel expone un dispositivo de bloques mientras que un servicio en el espacio de usuario maneja el bucle (loop), el almacenamiento de bloques remoto o el mapeo qcow2. Debe soportar E/S de alta concurrencia, recuperación ante caídas de procesos en el espacio de usuario, aislamiento de permisos y observabilidad.

Linux ublk separa este framework en un plano de control y un plano de datos: /dev/ublk-control administra los dispositivos, /dev/ublkb* transporta la E/S de bloques y el servicio en el espacio de usuario obtiene las solicitudes y confirma los resultados a través del passthrough de io_uring. La entrevista evalúa los ciclos de vida de las solicitudes, la semántica de fallos y los límites de seguridad en lugar de simplemente trasladar la E/S de archivos a un proceso.

Qué evalúa el entrevistador

Demuestra los límites entre el control, la capa de bloques del kernel, io_uring y el backend en el espacio de usuario. Explica la relación uno a uno entre colas, etiquetas (tags), búferes y finalizaciones; elige entre modos por E/S individual (per-I/O) y por lotes (batch); define la semántica de reencolado (requeue), fallo y reproducción (replay) cuando el servidor finaliza; y gestiona zero-copy, privilegios, aislamiento de contenedores y métricas.

Preguntas para aclarar primero

Carga de trabajo y backend

Confirma la proporción de lectura/escritura, los tamaños de E/S, el número de colas, los objetivos de latencia, si el backend es un archivo local, un NBD remoto o un formato de copia en escritura (copy-on-write), y si se requieren garantías de ordenamiento.

Fallas y seguridad de los datos

Confirma la semántica para una caída del espacio de usuario, partición de red, escritura corta en el backend, escritura duplicada y eliminación de dispositivos. Establece si la reproducción (replay) puede tolerar escrituras dobles.

Permisos y despliegue

Confirma quién puede crear dispositivos, quién puede leer /dev/ublkc*, si el servicio se ejecuta en un contenedor, qué privilegios de zero-copy están permitidos y cómo se aíslan los inquilinos (tenants).

Una respuesta de 30 segundos

“Separo los planos de control y de datos. El control negocia las colas, la profundidad y las características antes de iniciar el dispositivo; los datos obtienen solicitudes de io_uring por cola y etiqueta y confirman los resultados. Cada solicitud tiene un propietario, un estado y un tiempo de espera. Cuando el servidor en el espacio de usuario finaliza, pongo el dispositivo en reposo (quiesce) y elijo reencolar, fallar o reproducir según las garantías del backend. La copia es la opción predeterminada; zero-copy se limita a backends confiables y autorizados. Las métricas cubren la profundidad de la cola, la latencia de finalización, los reintentos, los descartes y el tiempo de recuperación, con pruebas de consistencia para la eliminación y la recuperación.”

Respuesta detallada paso a paso

Paso 1: Particionar el plano de control

Expón comandos para agregar, establecer/obtener parámetros, iniciar, detener y eliminar un dispositivo. Al agregar uno se negocian nr_hw_queues, queue_depth y el tamaño máximo del búfer de E/S; los parámetros se congelan antes del inicio, tras lo cual se expone /dev/ublkb*. El servicio en el espacio de usuario almacena los ID de los dispositivos y la información específica del backend.

Paso 2: Diseñar las solicitudes del plano de datos

La capa de bloques asigna una etiqueta única por cola y el servicio en el espacio de usuario asocia las solicitudes mediante (queue, tag). Un área mapeada fija describe el desplazamiento (offset), la longitud, la operación y los flags. El servicio recibe notificaciones a través del passthrough de io_uring y devuelve el estado y los bytes completados al kernel.

Paso 3: Elegir el modo por E/S individual o por lotes

Los comandos tradicionales por E/S son fáciles de razonar, con un demonio como propietario de cada etiqueta. El modo por lotes prepara y confirma múltiples solicitudes por cola, reduce la sobrecarga de llamadas al sistema (syscalls) y permite que las tareas compartan el trabajo dinámicamente. No mezcles los conjuntos de comandos durante la migración; compara la latencia de cola (tail latency), la CPU y el balanceo de carga bajo presión.

Paso 4: Definir la máquina de estados de recuperación

Utiliza estados como running, quiescing, recovering y failed. Al salir el servidor, detén el despacho de nuevas E/S, espera o marca las solicitudes en curso y emite START_USER_RECOVERY. REISSUE se adapta a backends que toleran escrituras duplicadas; FAIL_IO hace que las solicitudes en curso y futuras fallen explícitamente en lugar de simular un éxito.

text
on_server_exit:
  quiesce_device()
  if policy == REISSUE:
    requeue_inflight()
  else:
    fail_inflight_and_future_io()
  wait_new_server_ready()
  end_user_recovery()

Paso 5: Manejar búferes y zero-copy

La ruta ordinaria utiliza búferes preasignados en el espacio de usuario y copias del kernel, lo que proporciona límites más simples. Zero-copy requiere búferes fijos registrados, alineación de segmentos en el backend y un servicio confiable que complete los datos de READ y reporte los recuentos de bytes correctamente. Un error puede exponer búferes no inicializados del kernel, por lo que se deben restringir los privilegios y auditar el tiempo de vida.

Paso 6: Construir aislamiento de permisos y contenedores

Separa los comandos de control privilegiados del acceso a los dispositivos. Con dispositivos no privilegiados, el kernel aún verifica la propiedad del dispositivo de caracteres relevante; un contenedor solo debería ver sus propios nodos de dispositivo. El backend en el espacio de usuario no debe recibir permisos de archivos, red o KMS más allá de su dispositivo de destino.

Paso 7: Verificar rendimiento y corrección

Mide IOPS, latencia p50/p99, profundidad de cola, CPU, bytes copiados y tiempo de recuperación con fio o cargas de trabajo similares a las de producción. Inyecta caídas del servidor, tiempos de espera del backend, escrituras cortas, eliminación de dispositivos y envíos duplicados. Verifica que cada solicitud se complete una vez o falle explícitamente según la política; prueba los límites y los checksums en modos de copia y zero-copy.

Respuesta modelo

Dividiría un sistema similar a ublk en un plano de control, la capa de bloques del kernel, el plano de datos de io_uring y el backend en el espacio de usuario. El control negocia colas y búferes antes del inicio; los datos rastrean las solicitudes mediante (queue, tag) y validan el estado y los recuentos de bytes al completarse. La caída de un servidor activa la puesta en reposo y la recuperación: se reproduce solo para un backend que pueda tolerarlo, y falla en caso contrario. La copia es la opción predeterminada; zero-copy se limita a servicios confiables, alineados y autorizados. El lanzamiento requiere pruebas de latencia de cola, inyección de fallas, aislamiento de permisos y consistencia en la eliminación.

Errores comunes

  • Error: Permitir que el servidor en el espacio de usuario cierre el dispositivo directamente. → Por qué falla: Las solicitudes en curso y el estado de la cola del kernel permanecen indefinidos. → Solución: Detén el despacho, pon en reposo y luego aplica una política de recuperación.
  • Error: Habilitar zero-copy para todos los backends. → Por qué falla: Aumentan los riesgos del tiempo de vida del búfer, los privilegios y los datos no inicializados. → Solución: Copia por defecto y condiciona zero-copy a la capacidad y la auditoría.
  • Error: Proteger todas las colas con un único bloqueo global. → Por qué falla: La concurrencia de múltiples colas se serializa. → Solución: Fragmenta (shard) el estado por cola/etiqueta y mide la contención.
  • Error: Medir únicamente el rendimiento de E/S en estado óptimo. → Por qué falla: Las caídas, las escrituras cortas y las escrituras duplicadas determinan la consistencia. → Solución: Incluye la máquina de estados de recuperación y la inyección de fallas en las pruebas de aceptación.

Preguntas de seguimiento y respuestas

¿Cuándo deberías elegir E/S por lotes?

Elige por lotes cuando la sobrecarga de llamadas al sistema y notificaciones predomine, las colas tengan suficiente concurrencia y las tareas puedan compartir el trabajo dinámicamente. El modo tradicional es más fácil de depurar cuando importa la propiedad por E/S o la concurrencia es baja.

¿Cómo evita REISSUE la corrupción?

Habilítalo únicamente para backends idempotentes o con detección de duplicados, utilizando identificadores de solicitud, versiones de escritura o deduplicación de registros. Si no se puede demostrar la idempotencia, hazlo fallar y deja que la capa superior se recupere.

¿Cómo se limita el impacto de seguridad de zero-copy?

Vincula el registro y desregistro de búferes y los permisos de dispositivos a un solo servicio confiable; valida la dirección, la longitud, la alineación y los bytes completados; y prohíbe mapeos editables compartidos entre inquilinos.

¿Cómo se preserva la consistencia durante la eliminación de un dispositivo?

Detén las nuevas solicitudes, espera a que las solicitudes enviadas se completen o fallen, confirma que la cola del espacio de usuario esté vacía y luego libera los nodos de dispositivo y los mapeos. Registra los tiempos de espera como fallas rastreables en lugar de descartarlos silenciosamente.

Fuentes públicas

Preguntas relacionadas

Herramienta de entrevista relacionada

Usa Resolver para una respuesta de diseño de sistemas

Aclara primero los requisitos y luego avanza a través de la escala, la arquitectura, la elección de componentes y las compensaciones (trade-offs).

Ver la herramienta