{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration","url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration","title":"Linux io_uring: Arquitectura de I/O Asíncrono e Integración con Node.js","description":"Análisis a nivel de sistemas de io_uring, colas de submission/completion, buffers registrados, archivos fijos, polling kernel-side y benchmarks comparando io_uring contra epoll e I/O sincrónico en diversas cargas de trabajo.","language":"es","datePublished":"2026-01-11","dateModified":null,"contentFormat":"text/markdown","content":"Systems Programming\n\n# Linux io\\_uring: Arquitectura de I/O Asíncrono e Integración con Node.js\n\n2026-01-117 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/linux-io-uring-async-io-nodejs-integration>)\n\n## Arquitectura: Ring Buffers en Memoria Compartida\n\nio\\_uring usa un par de ring buffers lock-free en memoria compartida entre userspace y el kernel:\n\n1. **Submission Queue (SQ)**: Vos escribirs requests de I/O aquí. El kernel los lee.\n2. **Completion Queue (CQ)**: El kernel escribe resultados de completion aquí. Vos los leés.\n\nLa clave: ambos rings están en regiones memory-mapped accesibles tanto por userspace como por el kernel. Escribir una submission es una escritura en memoria, no una syscall. Leer una completion es una lectura de memoria, no una syscall. Puede batchear docenas de operaciones de I/O, después hacer una sola syscall ` io_uring_enter() ` para submittearlas todas, o, con polling kernel-side, saltarte la syscall completamente.\n\n## Configurando io\\_uring en C\n\n```c\n#include <liburing.h>\n#include <stdio.h>\n#include <fcntl.h>\n#include <string.h>\n#include <stdlib.h>\n\n#define QUEUE_DEPTH 64\n#define BLOCK_SIZE 4096\n\nstruct io_data {\n int fd;\n off_t offset;\n size_t length;\n struct iovec iov;\n};\n\nint main(int argc, char *argv[]) {\n struct io_uring ring;\n int ret;\n\n // Inicializar io_uring con queue depth de 64\n ret = io_uring_queue_init(QUEUE_DEPTH, &ring, 0);\n if (ret < 0) {\n fprintf(stderr, \"io_uring_queue_init: %s\\n\", strerror(-ret));\n return 1;\n }\n\n int fd = open(argv[1], O_RDONLY | O_DIRECT);\n if (fd < 0) {\n perror(\"open\");\n return 1;\n }\n\n struct stat st;\n fstat(fd, &st);\n off_t file_size = st.st_size;\n off_t offset = 0;\n\n int pending = 0;\n while (offset < file_size) {\n struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);\n if (!sqe) {\n // La SQ está llena, submitteamos lo que tenemos y esperamos\n io_uring_submit(&ring);\n\n struct io_uring_cqe *cqe;\n io_uring_wait_cqe(&ring, &cqe);\n struct io_data *data = io_uring_cqe_get_data(cqe);\n\n if (cqe->res > 0) {\n process_block(data->iov.iov_base, cqe->res);\n }\n\n free(data->iov.iov_base);\n free(data);\n io_uring_cqe_seen(&ring, cqe);\n pending--;\n\n sqe = io_uring_get_sqe(&ring);\n }\n\n struct io_data *data = malloc(sizeof(*data));\n posix_memalign(&data->iov.iov_base, BLOCK_SIZE, BLOCK_SIZE);\n data->iov.iov_len = BLOCK_SIZE;\n data->fd = fd;\n data->offset = offset;\n\n io_uring_prep_readv(sqe, fd, &data->iov, 1, offset);\n io_uring_sqe_set_data(sqe, data);\n\n offset += BLOCK_SIZE;\n pending++;\n }\n\n io_uring_submit(&ring);\n\n while (pending > 0) {\n struct io_uring_cqe *cqe;\n io_uring_wait_cqe(&ring, &cqe);\n struct io_data *data = io_uring_cqe_get_data(cqe);\n\n if (cqe->res > 0) {\n process_block(data->iov.iov_base, cqe->res);\n }\n\n free(data->iov.iov_base);\n free(data);\n io_uring_cqe_seen(&ring, cqe);\n pending--;\n }\n\n io_uring_queue_exit(&ring);\n close(fd);\n return 0;\n}\n```\n\nEl patrón es el siguiente: se llena la submission queue con tantas operaciones como sea posible, se envían en un batch, y luego se drenan los completions a medida que llegan. El kernel las procesa en paralelo, potencialmente reordenando para scheduling óptimo de I/O.\n\n## Buffers Registrados y Archivos Fijos\n\nPara throughput máximo, se desea eliminar *todo* el overhead por-I/O:\n\n```c\n// Pre-registrar buffers, el kernel pinea las páginas, salteando copy_from_user\nstruct iovec buffers[NUM_BUFFERS];\nfor (int i = 0; i < NUM_BUFFERS; i++) {\n posix_memalign(&buffers[i].iov_base, 4096, BUFFER_SIZE);\n buffers[i].iov_len = BUFFER_SIZE;\n}\nio_uring_register_buffers(&ring, buffers, NUM_BUFFERS);\n\n// Pre-registrar file descriptors, el kernel los mantiene abiertos\nint fds[NUM_FILES] = { fd1, fd2, fd3 };\nio_uring_register_files(&ring, fds, NUM_FILES);\n\n// Ahora usamos los índices registrados en vez de fds/buffers crudos\nstruct io_uring_sqe *sqe = io_uring_get_sqe(&ring);\nio_uring_prep_read_fixed(sqe, 0, buffers[0].iov_base, BUFFER_SIZE, 0, 0);\nsqe->flags |= IOSQE_FIXED_FILE;\n```\n\nLo que ahorra por operación:\n\n- **Registro de buffers**: Elimina ` copy_from_user() `, el kernel ya tiene un mapeo directo a las páginas de tu buffer. En un read de 4KB, ahorra ~200ns.\n- **Archivos fijos**: Elimina ` fget() `/` fput() `, el reference counting atómico en el file descriptor. ~100ns.\n\n300ns por operación no suena a mucho hasta que se está haciendo 500K IOPS, y ahí son 150ms de tiempo de CPU por segundo.\n\n## Polling Kernel-Side: I/O sin Syscalls\n\nLa forma definitiva de io\\_uring: el kernel pollea la submission queue por vos.\n\n```c\nstruct io_uring_params params = {\n .flags = IORING_SETUP_SQPOLL,\n .sq_thread_idle = 2000,\n};\n\nio_uring_queue_init_params(QUEUE_DEPTH, &ring, &params);\n\n// Ahora solo escribimos en la SQ, el kernel thread lo levanta\n// ¡No hace falta io_uring_submit()!\nstruct io_uring_sqe *sqe = io_uring_get_sqe(&ring);\nio_uring_prep_read(sqe, fd, buf, size, offset);\n\n// Chequear CQ por completions, tampoco syscall\nstruct io_uring_cqe *cqe;\nunsigned head;\nio_uring_for_each_cqe(&ring, head, cqe) {\n // Procesar completion\n io_uring_cqe_seen(&ring, cqe);\n}\n```\n\nCon ` SQPOLL `, todo el path de submission de I/O es libre de syscalls. Escribirs en una región memory-mapped y el kernel thread lo procesa. La aplicación y el kernel se comunican puramente a través de memoria compartida.\n\n## Benchmarks: io\\_uring vs epoll vs sync I/O\n\nEstos benchmarks se corrieron en un c5d.2xlarge (8 vCPU, NVMe local) con kernel 6.6 y fio:\n\n### Lecturas Random de 4K (Queue Depth 64)\n\n```\nEngine IOPS Latency (p99) CPU Usage\n─────────────────────────────────────────────────────────\nsync (1 thread) 85,000 11.7 μs 24%\nlibaio 310,000 8.2 μs 31%\nio_uring 620,000 4.1 μs 28%\nio_uring+sqpoll 680,000 3.8 μs 35%*\nio_uring+reg_buf 710,000 3.6 μs 26%\n```\n\nio\\_uring con buffers registrados pega 710K IOPS, 8.3x más que sync I/O. La latencia p99 es 3.2x menor. Y el uso de CPU es *menor* que ` libaio ` porque menos syscalls significan menos overhead de transición al kernel.\n\n## io\\_uring y Node.js: El Estado Actual\n\nPara ser claros: Node.js no usa io\\_uring en su event loop principal. libuv sigue usando epoll en Linux. Sin embargo, es posible usar io\\_uring desde Node.js vía addons nativos:\n\n```javascript\nimport { IoUring } from 'io-uring';\nimport { open } from 'fs/promises';\n\nconst ring = new IoUring(256);\n\nasync function readFileWithUring(path) {\n const fileHandle = await open(path, 'r');\n const fd = fileHandle.fd;\n const buffers = [];\n\n const stats = await fileHandle.stat();\n let offset = 0;\n\n while (offset < stats.size) {\n const buf = Buffer.allocUnsafe(4096);\n ring.prep_read(fd, buf, buf.length, offset);\n buffers.push(buf);\n offset += 4096;\n }\n\n ring.submit();\n const results = await ring.waitCompletions(buffers.length);\n\n await fileHandle.close();\n return Buffer.concat(buffers.slice(0, results.length));\n}\n```\n\nLo más interesante es lo que pasa *adentro* de libuv de Node.js. El thread pool que maneja file I/O actualmente usa llamadas bloqueantes ` read() `/` write() ` en worker threads. Si libuv reemplazara eso con submissions de io\\_uring, el thread pool podría manejar muchas más operaciones de archivo concurrentes sin agregar threads.\n\nUn prototipo demostró throughput de file I/O mejorando 3.2x en una máquina de 32 cores al usar io\\_uring en vez del thread pool para ` fs.readFile ` sobre NVMe:\n\n```\n// Benchmark: leyendo 10,000 archivos de 4KB\n// Node.js fs.readFile (libuv thread pool): 42,000 archivos/seg\n// io_uring addon nativo: 134,000 archivos/seg\n// io_uring con buffers registrados: 158,000 archivos/seg\n```\n\n## Operaciones Encadenadas\n\nio\\_uring soporta linking de operaciones, creando cadenas donde cada operación arranca solo después de que la anterior completa:\n\n```c\n// Cadena: abrir archivo -> leer -> cerrar\n// Las tres submitteadas de una, ejecutadas en orden\n\nstruct io_uring_sqe *sqe;\n\nsqe = io_uring_get_sqe(&ring);\nio_uring_prep_openat(sqe, AT_FDCWD, \"/data/file.dat\", O_RDONLY, 0);\nsqe->flags |= IOSQE_IO_LINK;\n\nsqe = io_uring_get_sqe(&ring);\nio_uring_prep_read(sqe, 0, buf, size, 0);\nsqe->flags |= IOSQE_IO_LINK;\n\nsqe = io_uring_get_sqe(&ring);\nio_uring_prep_close(sqe, 0);\n\nio_uring_submit(&ring);\n```\n\nLa cadena entera se submittea de un saque, y el kernel la ejecuta sin ninguna intervención de userspace entre pasos.\n\n## Consideraciones de Seguridad\n\nio\\_uring tuvo una historia de seguridad complicada. La superficie de ataque es grande y se encontraron varias vulnerabilidades de escalación de privilegios en 2021-2023. Como resultado:\n\n- Docker deshabilita io\\_uring por defecto en su perfil seccomp\n- Google lo deshabilitó en ChromeOS y Android\n- Varias distros Linux agregaron parámetros del kernel para deshabilitarlo\n\nA partir del kernel 6.1+, el modelo de seguridad mejoró significativamente.\n\n## Cuándo Realmente Usar Esto\n\nio\\_uring no es para toda aplicación. El costo de complejidad es real.\n\nUtilizar io\\_uring cuando:\n\n- Estás I/O-bound y el overhead de syscalls es medible en tus profiles\n- Necesitás saturar NVMe o dispositivos de red de alta velocidad\n- Estás construyendo storage engines, proxies, o sistemas de base de datos\n\nQuedate con epoll/libuv cuando:\n\n- Tu bottleneck es lógica de aplicación, no dispatch de I/O\n- Estás corriendo en contenedores donde io\\_uring puede estar deshabilitado\n- Tu equipo no tiene expertise en systems programming\n\nLa realidad es que para la mayoría de las aplicaciones web Node.js, el event loop no es el bottleneck, son las queries a la base de datos, las llamadas a APIs externas, y la serialización JSON. Cambiar de epoll a io\\_uring no va a ayudar si tu latencia p99 está dominada por una query Postgres de 200ms. Pero para esa clase de aplicaciones donde el overhead de dispatch de I/O importa, servidores de archivos de alto throughput, storage gateways, bases de datos custom, io\\_uring es transformacional. No es mejora incremental. Es una arquitectura diferente.\n\n## Preguntas frecuentes\n\n### ¿Está io\\_uring listo para producción?\n\nSí, con salvedades. Lo usan en producción Ceph, RocksDB y varios stacks de almacenamiento de cloud providers. La superficie de seguridad fue controversial, se deshabilitó en varios runtimes de contenedores y Android. A partir del kernel 6.1+, el modelo de seguridad mejoró significativamente.\n\n### ¿Node.js usa io\\_uring?\n\nNo en el event loop principal. libuv todavía usa epoll en Linux. Sin embargo, existen bindings experimentales y el equipo de libuv exploró integración de io\\_uring. Para workloads específicos como file I/O, es posible usar io\\_uring directamente vía addons nativos.\n\n### ¿io\\_uring va a reemplazar a epoll?\n\nPara código nuevo, io\\_uring es superior en casi toda dimensión. Pero epoll no se va a ningún lado, es más simple, bien entendido, y suficiente para muchos workloads. io\\_uring importa más para servidores I/O-bound de alto throughput donde el overhead de syscalls es medible.\n\n## Tips\n\n1. **Tip:** Utilizar ` IORING_SETUP_SQPOLL ` solo para workloads de alto throughput sostenido. El thread de polling del kernel quema un core de CPU continuamente, para tráfico a ráfagas, el overhead supera al ahorro de syscalls.\n2. **Tip:** Registrar los buffers con ` io_uring_register_buffers() ` antes de entrar al hot loop. El kernel fija las páginas y omite el ` copy_from_user ` en cada I/O, solo esto proporciona 15-20% de mejora en throughput en lecturas grandes.\n3. **Tip:** Al realizar benchmarks de io\\_uring vs epoll, deshabilitar las mitigaciones ` CONFIG_RETPOLINE ` o el delta de overhead de syscalls estará inflado. Los servidores de producción reales tienen mitigaciones habilitadas, por lo que conviene medir con ellas activas para obtener números realistas.\n4. **Tip:** Configurar ` IORING_SETUP_COOP_TASKRUN ` en kernel 5.19+ para evitar la interrupción inter-procesador (IPI) en completion. Esto importa enormemente en sistemas NUMA multi-socket.\n\n- io\\_uring\n- linux\n- async-io\n- nodejs\n- systems-programming\n- kernel\n- performance\n- epoll\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"Linux io_uring: Arquitectura de I/O Asíncrono e Integración con Node.js","url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#title"},{"level":2,"text":"Arquitectura: Ring Buffers en Memoria Compartida","url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#arquitectura-ring-buffers-en-memoria-compartida"},{"level":2,"text":"Configurando io_uring en C","url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#configurando-iouring-en-c"},{"level":2,"text":"Buffers Registrados y Archivos Fijos","url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#buffers-registrados-y-archivos-fijos"},{"level":2,"text":"Polling Kernel-Side: I/O sin Syscalls","url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#polling-kernel-side-io-sin-syscalls"},{"level":2,"text":"Benchmarks: io_uring vs epoll vs sync I/O","url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#benchmarks-iouring-vs-epoll-vs-sync-io"},{"level":3,"text":"Lecturas Random de 4K (Queue Depth 64)","url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#lecturas-random-de-4k-queue-depth-64"},{"level":2,"text":"io_uring y Node.js: El Estado Actual","url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#iouring-y-nodejs-el-estado-actual"},{"level":2,"text":"Operaciones Encadenadas","url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#operaciones-encadenadas"},{"level":2,"text":"Consideraciones de Seguridad","url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#consideraciones-de-seguridad"},{"level":2,"text":"Cuándo Realmente Usar Esto","url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#cundo-realmente-usar-esto"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#questions"},{"level":3,"text":"¿Está io_uring listo para producción?","url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#question-1"},{"level":3,"text":"¿Node.js usa io_uring?","url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#question-2"},{"level":3,"text":"¿io_uring va a reemplazar a epoll?","url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#article-tips"}],"sections":[{"heading":"Linux io_uring: Arquitectura de I/O Asíncrono e Integración con Node.js","level":1,"url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration","content":"Systems Programming"},{"heading":"Linux io_uring: Arquitectura de I/O Asíncrono e Integración con Node.js","level":1,"url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#title","content":"2026-01-117 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/linux-io-uring-async-io-nodejs-integration>)"},{"heading":"Arquitectura: Ring Buffers en Memoria Compartida","level":2,"url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#arquitectura-ring-buffers-en-memoria-compartida","content":"io\\_uring usa un par de ring buffers lock-free en memoria compartida entre userspace y el kernel:\n\n1. **Submission Queue (SQ)**: Vos escribirs requests de I/O aquí. El kernel los lee.\n2. **Completion Queue (CQ)**: El kernel escribe resultados de completion aquí. Vos los leés.\n\nLa clave: ambos rings están en regiones memory-mapped accesibles tanto por userspace como por el kernel. Escribir una submission es una escritura en memoria, no una syscall. Leer una completion es una lectura de memoria, no una syscall. Puede batchear docenas de operaciones de I/O, después hacer una sola syscall ` io_uring_enter() ` para submittearlas todas, o, con polling kernel-side, saltarte la syscall completamente."},{"heading":"Configurando io_uring en C","level":2,"url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#configurando-iouring-en-c","content":"```c\n#include <liburing.h>\n#include <stdio.h>\n#include <fcntl.h>\n#include <string.h>\n#include <stdlib.h>\n\n#define QUEUE_DEPTH 64\n#define BLOCK_SIZE 4096\n\nstruct io_data {\n int fd;\n off_t offset;\n size_t length;\n struct iovec iov;\n};\n\nint main(int argc, char *argv[]) {\n struct io_uring ring;\n int ret;\n\n // Inicializar io_uring con queue depth de 64\n ret = io_uring_queue_init(QUEUE_DEPTH, &ring, 0);\n if (ret < 0) {\n fprintf(stderr, \"io_uring_queue_init: %s\\n\", strerror(-ret));\n return 1;\n }\n\n int fd = open(argv[1], O_RDONLY | O_DIRECT);\n if (fd < 0) {\n perror(\"open\");\n return 1;\n }\n\n struct stat st;\n fstat(fd, &st);\n off_t file_size = st.st_size;\n off_t offset = 0;\n\n int pending = 0;\n while (offset < file_size) {\n struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);\n if (!sqe) {\n // La SQ está llena, submitteamos lo que tenemos y esperamos\n io_uring_submit(&ring);\n\n struct io_uring_cqe *cqe;\n io_uring_wait_cqe(&ring, &cqe);\n struct io_data *data = io_uring_cqe_get_data(cqe);\n\n if (cqe->res > 0) {\n process_block(data->iov.iov_base, cqe->res);\n }\n\n free(data->iov.iov_base);\n free(data);\n io_uring_cqe_seen(&ring, cqe);\n pending--;\n\n sqe = io_uring_get_sqe(&ring);\n }\n\n struct io_data *data = malloc(sizeof(*data));\n posix_memalign(&data->iov.iov_base, BLOCK_SIZE, BLOCK_SIZE);\n data->iov.iov_len = BLOCK_SIZE;\n data->fd = fd;\n data->offset = offset;\n\n io_uring_prep_readv(sqe, fd, &data->iov, 1, offset);\n io_uring_sqe_set_data(sqe, data);\n\n offset += BLOCK_SIZE;\n pending++;\n }\n\n io_uring_submit(&ring);\n\n while (pending > 0) {\n struct io_uring_cqe *cqe;\n io_uring_wait_cqe(&ring, &cqe);\n struct io_data *data = io_uring_cqe_get_data(cqe);\n\n if (cqe->res > 0) {\n process_block(data->iov.iov_base, cqe->res);\n }\n\n free(data->iov.iov_base);\n free(data);\n io_uring_cqe_seen(&ring, cqe);\n pending--;\n }\n\n io_uring_queue_exit(&ring);\n close(fd);\n return 0;\n}\n```\n\nEl patrón es el siguiente: se llena la submission queue con tantas operaciones como sea posible, se envían en un batch, y luego se drenan los completions a medida que llegan. El kernel las procesa en paralelo, potencialmente reordenando para scheduling óptimo de I/O."},{"heading":"Buffers Registrados y Archivos Fijos","level":2,"url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#buffers-registrados-y-archivos-fijos","content":"Para throughput máximo, se desea eliminar *todo* el overhead por-I/O:\n\n```c\n// Pre-registrar buffers, el kernel pinea las páginas, salteando copy_from_user\nstruct iovec buffers[NUM_BUFFERS];\nfor (int i = 0; i < NUM_BUFFERS; i++) {\n posix_memalign(&buffers[i].iov_base, 4096, BUFFER_SIZE);\n buffers[i].iov_len = BUFFER_SIZE;\n}\nio_uring_register_buffers(&ring, buffers, NUM_BUFFERS);\n\n// Pre-registrar file descriptors, el kernel los mantiene abiertos\nint fds[NUM_FILES] = { fd1, fd2, fd3 };\nio_uring_register_files(&ring, fds, NUM_FILES);\n\n// Ahora usamos los índices registrados en vez de fds/buffers crudos\nstruct io_uring_sqe *sqe = io_uring_get_sqe(&ring);\nio_uring_prep_read_fixed(sqe, 0, buffers[0].iov_base, BUFFER_SIZE, 0, 0);\nsqe->flags |= IOSQE_FIXED_FILE;\n```\n\nLo que ahorra por operación:\n\n- **Registro de buffers**: Elimina ` copy_from_user() `, el kernel ya tiene un mapeo directo a las páginas de tu buffer. En un read de 4KB, ahorra ~200ns.\n- **Archivos fijos**: Elimina ` fget() `/` fput() `, el reference counting atómico en el file descriptor. ~100ns.\n\n300ns por operación no suena a mucho hasta que se está haciendo 500K IOPS, y ahí son 150ms de tiempo de CPU por segundo."},{"heading":"Polling Kernel-Side: I/O sin Syscalls","level":2,"url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#polling-kernel-side-io-sin-syscalls","content":"La forma definitiva de io\\_uring: el kernel pollea la submission queue por vos.\n\n```c\nstruct io_uring_params params = {\n .flags = IORING_SETUP_SQPOLL,\n .sq_thread_idle = 2000,\n};\n\nio_uring_queue_init_params(QUEUE_DEPTH, &ring, &params);\n\n// Ahora solo escribimos en la SQ, el kernel thread lo levanta\n// ¡No hace falta io_uring_submit()!\nstruct io_uring_sqe *sqe = io_uring_get_sqe(&ring);\nio_uring_prep_read(sqe, fd, buf, size, offset);\n\n// Chequear CQ por completions, tampoco syscall\nstruct io_uring_cqe *cqe;\nunsigned head;\nio_uring_for_each_cqe(&ring, head, cqe) {\n // Procesar completion\n io_uring_cqe_seen(&ring, cqe);\n}\n```\n\nCon ` SQPOLL `, todo el path de submission de I/O es libre de syscalls. Escribirs en una región memory-mapped y el kernel thread lo procesa. La aplicación y el kernel se comunican puramente a través de memoria compartida."},{"heading":"Benchmarks: io_uring vs epoll vs sync I/O","level":2,"url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#benchmarks-iouring-vs-epoll-vs-sync-io","content":"Estos benchmarks se corrieron en un c5d.2xlarge (8 vCPU, NVMe local) con kernel 6.6 y fio:"},{"heading":"Lecturas Random de 4K (Queue Depth 64)","level":3,"url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#lecturas-random-de-4k-queue-depth-64","content":"```\nEngine IOPS Latency (p99) CPU Usage\n─────────────────────────────────────────────────────────\nsync (1 thread) 85,000 11.7 μs 24%\nlibaio 310,000 8.2 μs 31%\nio_uring 620,000 4.1 μs 28%\nio_uring+sqpoll 680,000 3.8 μs 35%*\nio_uring+reg_buf 710,000 3.6 μs 26%\n```\n\nio\\_uring con buffers registrados pega 710K IOPS, 8.3x más que sync I/O. La latencia p99 es 3.2x menor. Y el uso de CPU es *menor* que ` libaio ` porque menos syscalls significan menos overhead de transición al kernel."},{"heading":"io_uring y Node.js: El Estado Actual","level":2,"url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#iouring-y-nodejs-el-estado-actual","content":"Para ser claros: Node.js no usa io\\_uring en su event loop principal. libuv sigue usando epoll en Linux. Sin embargo, es posible usar io\\_uring desde Node.js vía addons nativos:\n\n```javascript\nimport { IoUring } from 'io-uring';\nimport { open } from 'fs/promises';\n\nconst ring = new IoUring(256);\n\nasync function readFileWithUring(path) {\n const fileHandle = await open(path, 'r');\n const fd = fileHandle.fd;\n const buffers = [];\n\n const stats = await fileHandle.stat();\n let offset = 0;\n\n while (offset < stats.size) {\n const buf = Buffer.allocUnsafe(4096);\n ring.prep_read(fd, buf, buf.length, offset);\n buffers.push(buf);\n offset += 4096;\n }\n\n ring.submit();\n const results = await ring.waitCompletions(buffers.length);\n\n await fileHandle.close();\n return Buffer.concat(buffers.slice(0, results.length));\n}\n```\n\nLo más interesante es lo que pasa *adentro* de libuv de Node.js. El thread pool que maneja file I/O actualmente usa llamadas bloqueantes ` read() `/` write() ` en worker threads. Si libuv reemplazara eso con submissions de io\\_uring, el thread pool podría manejar muchas más operaciones de archivo concurrentes sin agregar threads.\n\nUn prototipo demostró throughput de file I/O mejorando 3.2x en una máquina de 32 cores al usar io\\_uring en vez del thread pool para ` fs.readFile ` sobre NVMe:\n\n```\n// Benchmark: leyendo 10,000 archivos de 4KB\n// Node.js fs.readFile (libuv thread pool): 42,000 archivos/seg\n// io_uring addon nativo: 134,000 archivos/seg\n// io_uring con buffers registrados: 158,000 archivos/seg\n```"},{"heading":"Operaciones Encadenadas","level":2,"url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#operaciones-encadenadas","content":"io\\_uring soporta linking de operaciones, creando cadenas donde cada operación arranca solo después de que la anterior completa:\n\n```c\n// Cadena: abrir archivo -> leer -> cerrar\n// Las tres submitteadas de una, ejecutadas en orden\n\nstruct io_uring_sqe *sqe;\n\nsqe = io_uring_get_sqe(&ring);\nio_uring_prep_openat(sqe, AT_FDCWD, \"/data/file.dat\", O_RDONLY, 0);\nsqe->flags |= IOSQE_IO_LINK;\n\nsqe = io_uring_get_sqe(&ring);\nio_uring_prep_read(sqe, 0, buf, size, 0);\nsqe->flags |= IOSQE_IO_LINK;\n\nsqe = io_uring_get_sqe(&ring);\nio_uring_prep_close(sqe, 0);\n\nio_uring_submit(&ring);\n```\n\nLa cadena entera se submittea de un saque, y el kernel la ejecuta sin ninguna intervención de userspace entre pasos."},{"heading":"Consideraciones de Seguridad","level":2,"url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#consideraciones-de-seguridad","content":"io\\_uring tuvo una historia de seguridad complicada. La superficie de ataque es grande y se encontraron varias vulnerabilidades de escalación de privilegios en 2021-2023. Como resultado:\n\n- Docker deshabilita io\\_uring por defecto en su perfil seccomp\n- Google lo deshabilitó en ChromeOS y Android\n- Varias distros Linux agregaron parámetros del kernel para deshabilitarlo\n\nA partir del kernel 6.1+, el modelo de seguridad mejoró significativamente."},{"heading":"Cuándo Realmente Usar Esto","level":2,"url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#cundo-realmente-usar-esto","content":"io\\_uring no es para toda aplicación. El costo de complejidad es real.\n\nUtilizar io\\_uring cuando:\n\n- Estás I/O-bound y el overhead de syscalls es medible en tus profiles\n- Necesitás saturar NVMe o dispositivos de red de alta velocidad\n- Estás construyendo storage engines, proxies, o sistemas de base de datos\n\nQuedate con epoll/libuv cuando:\n\n- Tu bottleneck es lógica de aplicación, no dispatch de I/O\n- Estás corriendo en contenedores donde io\\_uring puede estar deshabilitado\n- Tu equipo no tiene expertise en systems programming\n\nLa realidad es que para la mayoría de las aplicaciones web Node.js, el event loop no es el bottleneck, son las queries a la base de datos, las llamadas a APIs externas, y la serialización JSON. Cambiar de epoll a io\\_uring no va a ayudar si tu latencia p99 está dominada por una query Postgres de 200ms. Pero para esa clase de aplicaciones donde el overhead de dispatch de I/O importa, servidores de archivos de alto throughput, storage gateways, bases de datos custom, io\\_uring es transformacional. No es mejora incremental. Es una arquitectura diferente."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#questions","content":""},{"heading":"¿Está io_uring listo para producción?","level":3,"url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#question-1","content":"Sí, con salvedades. Lo usan en producción Ceph, RocksDB y varios stacks de almacenamiento de cloud providers. La superficie de seguridad fue controversial, se deshabilitó en varios runtimes de contenedores y Android. A partir del kernel 6.1+, el modelo de seguridad mejoró significativamente."},{"heading":"¿Node.js usa io_uring?","level":3,"url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#question-2","content":"No en el event loop principal. libuv todavía usa epoll en Linux. Sin embargo, existen bindings experimentales y el equipo de libuv exploró integración de io\\_uring. Para workloads específicos como file I/O, es posible usar io\\_uring directamente vía addons nativos."},{"heading":"¿io_uring va a reemplazar a epoll?","level":3,"url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#question-3","content":"Para código nuevo, io\\_uring es superior en casi toda dimensión. Pero epoll no se va a ningún lado, es más simple, bien entendido, y suficiente para muchos workloads. io\\_uring importa más para servidores I/O-bound de alto throughput donde el overhead de syscalls es medible."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#article-tips","content":"1. **Tip:** Utilizar ` IORING_SETUP_SQPOLL ` solo para workloads de alto throughput sostenido. El thread de polling del kernel quema un core de CPU continuamente, para tráfico a ráfagas, el overhead supera al ahorro de syscalls.\n2. **Tip:** Registrar los buffers con ` io_uring_register_buffers() ` antes de entrar al hot loop. El kernel fija las páginas y omite el ` copy_from_user ` en cada I/O, solo esto proporciona 15-20% de mejora en throughput en lecturas grandes.\n3. **Tip:** Al realizar benchmarks de io\\_uring vs epoll, deshabilitar las mitigaciones ` CONFIG_RETPOLINE ` o el delta de overhead de syscalls estará inflado. Los servidores de producción reales tienen mitigaciones habilitadas, por lo que conviene medir con ellas activas para obtener números realistas.\n4. **Tip:** Configurar ` IORING_SETUP_COOP_TASKRUN ` en kernel 5.19+ para evitar la interrupción inter-procesador (IPI) en completion. Esto importa enormemente en sistemas NUMA multi-socket.\n\n- io\\_uring\n- linux\n- async-io\n- nodejs\n- systems-programming\n- kernel\n- performance\n- epoll\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/linux-io-uring-async-io-nodejs-integration"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration"},"headline":"Linux io_uring: Arquitectura de I/O Asíncrono e Integración con Node.js","description":"Análisis a nivel de sistemas de io_uring, colas de submission/completion, buffers registrados, archivos fijos, polling kernel-side y benchmarks comparando io_uring contra epoll e I/O sincrónico en diversas cargas de trabajo.","datePublished":"2026-01-11","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Systems Programming","keywords":"io_uring, linux, async-io, nodejs, systems-programming, kernel, performance, epoll","wordCount":1296,"timeRequired":"PT7M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/linux-io-uring-async-io-nodejs-integration"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"Linux io_uring: Arquitectura de I/O Asíncrono e Integración con Node.js","item":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿Está io_uring listo para producción?","acceptedAnswer":{"@type":"Answer","text":"Sí, con salvedades. Lo usan en producción Ceph, RocksDB y varios stacks de almacenamiento de cloud providers. La superficie de seguridad fue controversial, se deshabilitó en varios runtimes de contenedores y Android. A partir del kernel 6.1+, el modelo de seguridad mejoró significativamente."}},{"@type":"Question","name":"¿Node.js usa io_uring?","acceptedAnswer":{"@type":"Answer","text":"No en el event loop principal. libuv todavía usa epoll en Linux. Sin embargo, existen bindings experimentales y el equipo de libuv exploró integración de io_uring. Para workloads específicos como file I/O, es posible usar io_uring directamente vía addons nativos."}},{"@type":"Question","name":"¿io_uring va a reemplazar a epoll?","acceptedAnswer":{"@type":"Answer","text":"Para código nuevo, io_uring es superior en casi toda dimensión. Pero epoll no se va a ningún lado, es más simple, bien entendido, y suficiente para muchos workloads. io_uring importa más para servidores I/O-bound de alto throughput donde el overhead de syscalls es medible."}}]}]}],"contentHash":"c936293c978b79c9cde092bdfb1c05cb5fd86aa630c9f1c90fbc0c45a29042a8","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/linux-io-uring-async-io-nodejs-integration","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":62827,"contentBytes":12224}}
