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.
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.