SenseiTeoría desde cero0/21

3 · La computadora por dentroLección 4 de 5

El sistema operativo: Linux, archivos y permisos

The operating system: Linux, files and permissions

Qué hace el kernel por cada programa, cómo se ordenan los archivos y quién puede tocarlos, por qué una imagen hecha en una Mac no corre en un servidor y qué es de verdad un contenedor: un proceso con lentes y con límites.

⏱ 59 min lo esencial · 79 min todo12 conceptos nuevos2 calculadoras10 ejerciciosContenedor
Secciones14
  1. 01El sistema operativo y el kernel
  2. 02Linux y sus distribuciones
  3. 03El sistema de archivos: un árbol desde /
  4. 04Permisos y root
  5. 05Montar: discos, volúmenes y solo lectura
  6. 06Arquitectura de CPU: x86-64 y ARM64
  7. 07Qué es de verdad un contenedor
  8. 08Los límites por dentro: cuotas de CPU y memoria extra
  9. 09Errores comunes
  10. 10En la vida real extra
  11. 11Ejercicios
  12. 12Lo que sigue
  13. 13Palabras que vas a oír
  14. 14Chequeo
La pregunta

¿Qué es Linux, qué hace el kernel por cada programa y qué es de verdad un contenedor?

Cuando Ana sube la foto de sus tenis, la atienden dos programas de C01: el gateway, que la decodifica, y Triton, que corre el modelo. En la lección «Programa, proceso e hilo» viste que cada uno es un proceso: un programa que está corriendo. Los dos viven en máquinas con Linux en la nube, y antes de la primera foto esas máquinas ya respondieron muchas preguntas. ¿Dónde está el archivo del modelo? ¿Quién lo puede leer? ¿Cuánta memoria puede usar el gateway antes de que lo maten? ¿Para qué CPU se compiló cada programa?

Todas las responde el sistema operativo. En la Ronda, M05 lo dice en una frase: «Un contenedor no es una VM: todos los contenedores del nodo comparten el kernel del host». Al terminar esta lección vas a entender cada palabra.

01

El sistema operativo y el kernel

Esencial

The operating system and the kernel

Mientras lees esto, tu computadora corre cientos de procesos: el navegador, la música, los avisos del correo. Todos comparten unos cuantos núcleos de CPU, una sola memoria y un solo disco. Alguien tiene que repartir. Ese alguien es el sistema operativo (operating system, OS): Linux, Windows, macOS o Android.

Hace tres trabajos:

  • Reparte. Le presta la CPU a cada proceso por turnos de milisegundos (cada cambio de turno es un cambio de contexto) y le da a cada uno su pedazo de memoria.
  • Esconde los detalles. Triton pide «abre /models/classifier/1/model.plan», el archivo de su modelo, sin saber si está en el disco de su máquina, en uno de otra máquina que le llega por la red o en memoria.
  • Protege. Un proceso no puede, sin permiso, leer la memoria de otro ni abrir un archivo que no le toca.

El kernel y el espacio de usuario

El corazón del sistema operativo es el kernel (en español, «núcleo», aunque no tiene nada que ver con los núcleos de la CPU): el único programa que toca el hardware directamente. Todo lo demás vive en el espacio de usuario (user space): tus programas, Python, las bibliotecas que usan y los comandos como ls.

La separación la hace cumplir la propia CPU, que tiene dos modos. En modo kernel puede todo: cualquier dirección de memoria, cualquier dispositivo. En modo usuario, no. Tu código de Python corre en modo usuario. Si intentara leer memoria que no es suya o hablarle directo al disco, la CPU no lo dejaría: le pasaría el control al kernel, y el kernel mataría el proceso con la señal SIGSEGV. Es el famoso Segmentation fault.

Las llamadas al sistema: la única puerta

Si un programa no puede tocar el hardware, ¿cómo lee un archivo? Se lo pide al kernel con una llamada al sistema (system call, o syscall). La CPU pasa a modo kernel, el kernel revisa que la petición se pueda (¿existe el archivo?, ¿tienes permiso?), hace el trabajo y regresa a modo usuario con el resultado.

Linux tiene más de 300. Estas son las que vas a oír:

SyscallQué le pides al kernel
openat, read, write, closeAbrir un archivo (openat es la versión actual de open), leerlo, escribirlo y cerrarlo
mmapVer un archivo como si fuera memoria (el mmap de la lección de memoria): así se pueden cargar los pesos en safetensors, un formato de archivo para pesos
clone, execveCrear un proceso nuevo, copia del que lo pide (clone), y cambiar su programa por otro (execve): así arranca cada comando que escribes
killMandarle una señal a otro proceso
socket, connect, acceptHablar por la red
exitTerminar, con un código de salida

Hasta un print("hola") termina en una syscall: write(1, "hola\n", 5), «escribe estos 5 bytes (las 4 letras y \n, el salto de línea) en el archivo número 1». El kernel numera los archivos que tiene abiertos cada proceso, y a cada número le dice descriptor; el 1 es la salida del programa, lo que ves en la terminal o en los logs. Python no hace la syscall solo: se la encarga a la biblioteca de C, unas funciones para pedirle cosas al kernel que usan casi todos los programas (la sección que sigue la presenta).

El diagrama sigue a Triton cuando abre su engine, el modelo de C01 ya convertido por TensorRT para la T4.

Triton abre su engine: una llamada al sistema, del programa al disco · Fíjate en que Triton nunca toca el disco: su petición cruza al kernel por una sola puerta, la syscall. Usa «Recorrer» para seguir la lectura del engine y dos maneras de fallar.
  • Triton · quiere leer su engine
    El servidor de modelos de C01 arranca y necesita su engine: /models/classifier/1/model.plan. Corre en modo usuario, así que no puede leer el disco por su cuenta: tiene que pedirlo.
  • glibc · la biblioteca de C
    Triton llama a la función open() de la biblioteca de C. Por dentro, glibc arma la llamada al sistema openat con la ruta y «solo lectura», y ejecuta la instrucción que cambia la CPU a modo kernel.
  • Entrada al kernel · la CPU pasa a modo kernel
    La syscall llega al kernel. La CPU ya está en modo kernel: desde aquí se puede tocar todo. Al terminar, el kernel regresa un número y la CPU vuelve a modo usuario. Si todo salió bien, ese número es el descriptor del archivo abierto, con el que el programa lo va a leer; si no, un error.
  • Sistema de archivos · recorre la ruta
    El sistema de archivos recorre la ruta carpeta por carpeta: /, models, classifier, 1 y por fin model.plan. Sabe en qué bloques del disco están sus bytes. Si una carpeta no existe, responde con el error 2: «No such file or directory».
  • ¿Tiene permiso? · el UID contra rwx
    En cada paso, el kernel revisa los permisos del UID de Triton: x en cada carpeta del camino y r en el archivo. Si falta uno, responde con el error 13, «Permission denied», sin leer ni un byte.
  • Driver del disco · le habla al NVMe
    El driver del disco es la parte del kernel que sabe darle órdenes a ese tipo de disco. Pide los bloques del engine. Si se leyeron hace poco, el kernel ya los tiene en la RAM, en su page cache, y ni siquiera va al disco.
  • Disco NVMe · bloques de bytes
    El disco solo sabe de bloques numerados. Los nombres, las carpetas y los permisos existen gracias al sistema de archivos que el kernel escribió encima.

Cuando el kernel dice que no, regresa un número de error (errno) con un significado fijo. Python lo convierte en una excepción que trae el número y el mensaje. Estos números son los mismos en Linux y en macOS:

errnoMensajeQué pasóEn Python
2No such file or directoryLa ruta no existeFileNotFoundError
13Permission deniedNo tienes permisoPermissionError
30Read-only file systemEsa parte del árbol de archivos solo se puede leer (sección 5)OSError
28No space left on deviceEl disco se llenóOSError
8Exec format errorEl programa no es para esta CPUOSError

Los vas a leer en los logs una y otra vez. Esta lección explica de dónde sale cada uno.

Para ir más hondoCuántas syscalls hace un print

Cada syscall cuesta poco: de décimas de microsegundo a unos microsegundos, según cuál y según la máquina. Pero un millón pueden sumar segundos. Por eso Python no hace una syscall por cada write que le pides: junta lo que escribes en un buffer, un pedazo de memoria, y lo manda al kernel cuando se llena. El código cuenta las llamadas reales con una clase que se monta sobre la capa más baja de Python, la que habla con el kernel:

python
import io, os, tempfile

class Contador(io.FileIO):         # FileIO es la capa más baja: cada write es una syscall
    llamadas = 0
    def write(self, datos):
        Contador.llamadas += 1
        return super().write(datos)

ruta = os.path.join(tempfile.mkdtemp(), "log.txt")
for buffer in (0, 8192):
    Contador.llamadas = 0
    crudo = Contador(ruta, "w")
    f = crudo if buffer == 0 else io.BufferedWriter(crudo, buffer_size=buffer)
    for i in range(1000):
        f.write(b"foto procesada\n")       # 15 bytes por línea
    f.close()
    print(f"buffer de {buffer:>5} bytes: {Contador.llamadas:>5} llamadas a write para {os.path.getsize(ruta):,} bytes")
Salida
buffer de     0 bytes:  1000 llamadas a write para 15,000 bytes
buffer de  8192 bytes:     2 llamadas a write para 15,000 bytes

El buffer tiene un costo: lo que está ahí todavía no llegó al kernel. Si el proceso muere de golpe, se pierde. Por eso el gateway de C01 arranca con PYTHONUNBUFFERED=1, una variable de entorno (un ajuste que el proceso recibe al arrancar) que le pide a Python no usar buffer en lo que imprime: cada línea de log sale al instante, con más syscalls, pero sin perder las últimas líneas cuando el proceso cae.

02

Linux y sus distribuciones

Esencial

Linux and its distributions

En 1991, Linus Torvalds, un estudiante de 21 años en Helsinki, anunció en un foro de internet que escribía un sistema operativo «solo como pasatiempo». Lo publicó con su código abierto: cualquiera podía leerlo, cambiarlo y usarlo gratis. Hoy Linux corre en la mayoría de los servidores de internet (el conteo de W3Techs lo encuentra en ≈ 63 % de los sitios web cuyo sistema operativo se conoce, en 2026), en las 500 supercomputadoras más rápidas del mundo (en todas, desde 2017), en cada teléfono Android y en los Jetson, las computadoras chicas con GPU de NVIDIA.

En sentido estricto, Linux es solo el kernel. Lo que se instala es una distribución (distro): el kernel más un espacio de usuario completo. Ese espacio trae la biblioteca de C, la shell (el programa que lee los comandos que escribes en la terminal, la ventana de texto de los programadores), los comandos como ls (lista una carpeta) y cp (copia archivos) y un gestor de paquetes, el programa que instala más programas, como apt en Ubuntu y Debian. Hay cientos. Estas son las que vas a ver en ML:

DistribuciónDónde la vas a ver
UbuntuLaptops, servidores y la imagen de Triton de C01 (Ubuntu 22.04, la versión de abril de 2022)
DebianLa base de las imágenes python:3.13-slim, como la del gateway de C01
AlpineImágenes diminutas; usa otra biblioteca de C, musl, y para ML no sirve
Amazon Linux 2023Los nodos con GPU de C01, que arrancan con la AMI acelerada de EKS
BottlerocketUn sistema mínimo de AWS hecho solo para correr contenedores
Jetson Linux (L4T)Los Jetson; viene en JetPack, el software de NVIDIA para esos equipos, y en JetPack 6.2 está basado en Ubuntu 22.04

Unas palabras de la tabla. Una imagen es el paquete de archivos del que nace un contenedor. Un nodo es cada máquina donde Kubernetes, el orquestador de C01, corre contenedores. Y una AMI (Amazon Machine Image) es la imagen de disco con la que arranca una máquina de AWS; la de EKS, el Kubernetes de AWS, ya trae lo necesario para la GPU.

La biblioteca de C: la que hace las syscalls

Casi ningún programa hace syscalls a mano. Usa la biblioteca de C (C library; está escrita en C, el lenguaje de Linux), que trae funciones como open() y, por dentro, hace las syscalls. La de casi todas las distribuciones es glibc; Alpine usa musl, más chica. Hacen lo mismo, pero un programa compilado contra una no siempre corre con la otra. Por eso Alpine no sirve para ML: torch y buena parte de las librerías de ML publican sus versiones compiladas para glibc y no para musl (M05).

Por qué una imagen de Ubuntu corre en un nodo de Amazon Linux

Linux tiene una regla que Torvalds defiende sin concesiones: un cambio en el kernel no puede romper a los programas que ya funcionan. Cada syscall se pide con un número fijo, y las de hace veinte años siguen ahí, con el mismo número y el mismo comportamiento. Gracias a eso, el espacio de usuario de Ubuntu 22.04 que trae la imagen de Triton habla sin problema con el kernel de Amazon Linux 2023 del nodo. Esa regla es la base de que los contenedores funcionen.

¿Y tu Mac?

macOS no es Linux: tiene su propio kernel, igual que Windows. Un contenedor de Linux necesita un kernel Linux, así que Docker Desktop, la app de Docker para Mac y Windows, corre en tu Mac una pequeña máquina virtual con Linux, escondida: una computadora simulada por software, con su propio sistema operativo (la sección 7 la explica). En Windows usa WSL 2, que es la misma idea. Cuando haces docker run en la Mac, el contenedor corre dentro de esa máquina virtual.

03

El sistema de archivos: un árbol desde /

Esencial

The file system: a tree from /

Un disco, por dentro, es una fila enorme de bloques de bytes numerados: en «Memoria, caché y disco» viste que son de 512 bytes o de 4 KiB. No tiene nombres ni carpetas. El sistema de archivos (file system) es el índice que el sistema operativo escribe en el mismo disco: qué bloques forman cada archivo, cómo se llama, cuánto mide, quién es su dueño y quién puede leerlo. En Linux los más comunes se llaman ext4 y XFS; en la Mac, APFS; en Windows, NTFS.

Un solo árbol

En Windows, cada disco tiene su letra: C:, D:. En Linux no: todo cuelga de un solo árbol que empieza en la raíz, que se escribe /. Un disco nuevo no aparece como «D:», sino como una carpeta más del árbol (cómo se pega, en la sección de montaje).

La dirección de un archivo en el árbol es su ruta (path): las carpetas desde la raíz, separadas por /. El engine de C01 vive en /models/classifier/1/model.plan: en la carpeta models de la raíz, dentro de classifier, dentro de 1.

Rutas absolutas y relativas

  • Una ruta absoluta empieza con /. Es el camino completo desde la raíz y significa lo mismo desde cualquier lugar.
  • Una ruta relativa no empieza con /. Se lee desde la carpeta de trabajo (working directory) del proceso: la carpeta donde arrancó, si nadie la cambió después, y la que muestra el comando pwd.

Cada proceso tiene la suya y la hereda de quien lo lanzó. El comando cd cambia la de la shell (con la syscall chdir), y los programas que lances desde ahí arrancan en esa carpeta. En una imagen, la fija la línea WORKDIR de su Dockerfile, la receta con la que se arma: el gateway de C01 arranca en /srv.

Dos nombres especiales sirven para moverse: . es la carpeta donde estás y .. es la de arriba. Parado en /opt/tritonserver, la ruta ../../models/classifier/1/model.plan sube a /opt, sube a / y baja hasta el engine. Hay un tercero, ~, tu carpeta personal, pero ese lo traduce la shell y no el kernel: en Python, open("~/x") no lo entiende.

El árbol de archivos del contenedor de Triton en C01. Todo empieza en la raíz, que se escribe con una barra. De ella cuelgan etc, con la configuración; dev, con los dispositivos como nvidia0 y shm; proc, con los procesos que inventa el kernel; tmp, con los temporales; usr, con los programas y las bibliotecas; opt, que contiene tritonserver, la carpeta de trabajo; y models, que contiene classifier, que contiene 1, que contiene model.plan, el engine. La ruta absoluta del engine es /models/classifier/1/model.plan y vale desde cualquier lugar. Parado en /opt/tritonserver, la ruta relativa es ../../models/classifier/1/model.plan: el primer punto punto sube a opt, el segundo sube a la raíz, y de ahí baja por el mismo camino que la absoluta. EL ÁRBOL DEL CONTENEDOR DE TRITON / la raíz · hasta aquí sube el segundo «..» etc configuración: passwd, hosts dev dispositivos: nvidia0, shm proc procesos, que inventa el kernel tmp temporales usr programas y bibliotecas opt hasta aquí sube el primer «..» tritonserver carpeta de trabajo models classifier 1 model.plan el engine Absoluta, desde cualquier lugar: /models/classifier/1/model.plan Relativa, parado en /opt/tritonserver: ../../models/classifier/1/model.plan
El árbol del contenedor de Triton en C01. Sigue las dos rutas al engine: la absoluta empieza en / y baja; la relativa, para un proceso parado en /opt/tritonserver, sube dos veces con .. hasta la raíz y después baja por el mismo camino.

Una diferencia que muerde: en Linux, Model.plan y model.plan son dos archivos distintos. En una Mac con la configuración de fábrica, o en Windows, son el mismo, porque su disco no distingue mayúsculas de minúsculas. Un programa que abre Model.plan cuando el archivo se llama model.plan funciona en tu Mac y falla en el contenedor.

Para ir más hondoPruébalo en Python

El código resuelve rutas como lo hace el kernel, para un proceso parado en /opt/tritonserver, y revisa si tu disco distingue mayúsculas:

python
import os, posixpath, tempfile
from pathlib import PurePosixPath

aqui = PurePosixPath("/opt/tritonserver")         # la carpeta de trabajo del proceso
rutas = ("bin/tritonserver", "../../models/classifier/1/model.plan",
         "./config/../labels.json", "/etc/hosts")
for ruta in rutas:
    p = PurePosixPath(ruta)
    completa = p if p.is_absolute() else aqui / p  # una relativa se pega a la carpeta de trabajo
    limpia = posixpath.normpath(completa)          # resuelve los . y los ..
    print(f"{ruta} → {limpia} ({'absoluta' if p.is_absolute() else 'relativa'})")

with tempfile.TemporaryDirectory() as carpeta:     # ¿este disco distingue mayúsculas?
    open(os.path.join(carpeta, "Model.plan"), "w").close()
    print("Creé Model.plan. ¿Existe model.plan?", os.path.exists(os.path.join(carpeta, "model.plan")))
Salida
bin/tritonserver → /opt/tritonserver/bin/tritonserver (relativa)
../../models/classifier/1/model.plan → /models/classifier/1/model.plan (relativa)
./config/../labels.json → /opt/tritonserver/labels.json (relativa)
/etc/hosts → /etc/hosts (absoluta)
Creé Model.plan. ¿Existe model.plan? True

La última línea depende de dónde lo corras: en una Mac imprime True; en Linux, False.

Las carpetas que importan

CarpetaQué guarda
/etcLa configuración del sistema: /etc/passwd (los usuarios), /etc/hosts (nombres de máquinas)
/usrLos programas y las bibliotecas instaladas: /usr/bin/python3, /usr/lib
/optProgramas que se instalan aparte: /opt/tritonserver, o el entorno de Python del gateway (su propia carpeta de librerías) en /opt/venv
/home, /rootLa carpeta personal de cada usuario, y la de root, el superusuario (sección 4)
/varLo que cambia mientras la máquina corre: los logs (/var/log), las imágenes de Docker (/var/lib/docker)
/tmpTemporales: cualquiera escribe y se puede borrar al reiniciar
/procNo está en ningún disco: el kernel lo inventa al vuelo para mostrar los procesos (/proc/1/status es el estado del PID 1) y la máquina (/proc/meminfo)
/sysLo mismo, para los dispositivos y los límites de cada contenedor (sección 7): /sys/fs/cgroup
/devLos dispositivos, como archivos: el disco (/dev/nvme0n1), la GPU (/dev/nvidia0) y la memoria compartida (/dev/shm), donde varios procesos se pasan datos sin copiarlos

Casi todo es un archivo

Lo de /proc y /dev es una idea central de Linux: casi todo se ve como un archivo. La GPU es /dev/nvidia0, la memoria libre se lee en /proc/meminfo y el límite de memoria de un contenedor, en /sys/fs/cgroup/memory.max. A todos se les aplican las mismas syscalls: se abren con openat y se usan con read y write o, para darle órdenes a un dispositivo, con ioctl. Por eso, para darle la GPU a un contenedor, el NVIDIA Container Toolkit de «CPU y GPU» le pone en su árbol (le monta, como verás en la sección 5), entre otras cosas, tres archivos: /dev/nvidia0, /dev/nvidiactl y /dev/nvidia-uvm (M05).

Archivos ocultos

Los nombres que empiezan con punto son ocultos: el comando ls no los muestra si no le pides ls -a. Vas a ver tres muy seguido: .env (ajustes del programa, a veces con contraseñas), .git (la historia del código, la que guarda Git) y .dockerignore (la lista de lo que Docker no debe copiar a la imagen).

Para ir más hondoAtajos: los enlaces simbólicos

Un enlace simbólico (symlink) es un archivo que solo guarda otra ruta: abrirlo es abrir el archivo de esa ruta. ls -l lo marca con una l al principio y una flecha hacia su destino. En casi todas las distribuciones de hoy, /bin es un enlace a /usr/bin.

El python de un entorno virtual (una carpeta con su propio juego de librerías, como el /opt/venv del gateway) también es un enlace al Python del sistema: en el gateway, ls -l muestra python -> /usr/local/bin/python. Por eso el Dockerfile del gateway puede armar el entorno en una primera etapa, con las herramientas para instalar, y copiar solo esa carpeta a la imagen final: como las dos etapas parten de la misma imagen de Python, el enlace encuentra a Python en la misma ruta. Si el destino no existe, el enlace queda roto, y abrirlo da «No such file or directory» aunque ls lo muestre.

04

Permisos y root

Esencial

Permissions and root

Linux, como Unix (el sistema de los años setenta en que se inspiró), se hizo para que muchas personas usaran la misma máquina a la vez, así que cada proceso corre como alguien. Ese alguien es un usuario, y el kernel no conoce su nombre: conoce un número, el UID (user id). El archivo /etc/passwd traduce nombres a números. Cada usuario pertenece además a uno o más grupos, cada uno con su número, el GID (group id).

Cada archivo guarda sus permisos (permissions): un dueño (un UID), un grupo (un GID) y 9 bits que dicen qué se puede hacer. Hay tres permisos:

  • r (read): leer el archivo.
  • w (write): cambiarlo.
  • x (execute): correrlo como programa.

Y tres juegos de permisos: uno para el dueño, otro para su grupo y otro para los demás. Así se ven con ls -l, el comando que lista una carpeta con detalle:

text
-rwxr-x---  1 app  app  2048 Nov  5 10:12 arrancar.sh

Después de los permisos vienen el número de enlaces (por ahora, ignóralo), el dueño (app), el grupo (app), el tamaño en bytes, la fecha del último cambio y el nombre.

Los diez caracteres de permisos que muestra ls -l para arrancar.sh: guion, r, w, x, r, guion, x, guion, guion, guion. El primero es el tipo: un guion quiere decir archivo. Los siguientes nueve son tres juegos de tres: el dueño tiene r, w y x; el grupo tiene r y x, sin w; los demás no tienen nada. Cada letra es un bit prendido y cada posición vale 4, 2 y 1. El dueño suma 4 más 2 más 1, que da 7; el grupo, 4 más 0 más 1, que da 5; los demás, 0. Por eso estos permisos se escriben 750 y se ponen con chmod 750 arrancar.sh. Abajo, una tabla: en un archivo, r es leerlo, w es cambiarlo y x es correrlo; en una carpeta, r es ver los nombres, w es crear y borrar adentro, y x es entrar. LOS PERMISOS DE ARRANCAR.SH - r w x r - x - - - tipo dueño grupo los demás BITS 1 1 1 1 0 1 0 0 0 VALE 4 2 1 4 2 1 4 2 1 4 + 2 + 1 4 + 0 + 1 0 + 0 + 0 7 5 0 chmod 750 arrancar.sh EN UN ARCHIVO EN UNA CARPETA r leerlo ver los nombres w cambiarlo crear y borrar x correrlo entrar
Los primeros 10 caracteres de ls -l. El primero dice qué es (un guion es un archivo; una d, una carpeta). Después vienen tres juegos de rwx: dueño, grupo y los demás. Cada juego son 3 bits con valores 4, 2 y 1, y su suma es un dígito: 7, 5 y 0.

El kernel revisa un solo juego. Si el proceso es el dueño, el del dueño; si no, pero está en el grupo, el del grupo; si no, el de los demás.

En una carpeta, x es «entrar»

En una carpeta, las letras cambian de sentido: r deja ver los nombres de adentro, w deja crear, borrar y renombrar archivos, y x deja entrar y pasar a través de ella. Para abrir /models/classifier/1/model.plan, el proceso necesita x en /, en /models, en /models/classifier y en /models/classifier/1, y r en el archivo. Si a una sola carpeta del camino le falta x, la respuesta es «Permission denied», aunque el archivo diga que cualquiera lo puede leer.

Los permisos en octal

Cada juego son 3 bits, y 3 bits son un número del 0 al 7: es el binario de la lección de números, con posiciones que valen 4, 2 y 1. Por eso r vale 4, w vale 2 y x vale 1, y cada juego se escribe con un solo dígito. Con tres dígitos (dueño, grupo y demás) se dice todo; a esa forma se le llama octal, base 8, porque cada dígito va del 0 al 7.

rwxr-x--- en octalLa cuenta
Dueñorwx: los tres bits prendidos
4 + 2 + 1
7
Grupor-x: leer y correr, sin cambiar
4 + 0 + 1
5
Los demás---: nada
0 + 0 + 0
0
Resultado
rwxr-x--- es 750. Para ponerlo: chmod 750 arrancar.sh.
OctalLetrasPara qué
644rw-r--r--Un archivo normal: tú lo cambias, los demás lo leen
755rwxr-xr-xUn programa o una carpeta: todos lo usan, solo tú lo cambias
600rw-------Un secreto, como la llave privada de ssh (el archivo que prueba quién eres al entrar a otra máquina): ssh no la usa si otros pueden leerla
777rwxrwxrwxCualquiera hace cualquier cosa: casi siempre es un error

Se cambian con dos comandos. chmod cambia los bits: chmod 640 config.yaml, o chmod +x arrancar.sh para agregar la x. chown cambia el dueño y el grupo (chown app:app archivo), y eso solo lo puede hacer root, el superusuario (en un momento). En Python son os.chmod y os.chown. En el código, 0o marca un número en octal y 0b, uno en binario:

python
import os, stat, tempfile

def letras(modo):                      # 0o750 → rwxr-x---: cada dígito son 3 bits
    texto = ""
    for corrimiento in (6, 3, 0):      # dueño, grupo y los demás
        d = (modo >> corrimiento) & 0b111
        texto += ("r" if d & 4 else "-") + ("w" if d & 2 else "-") + ("x" if d & 1 else "-")
    return texto

for modo in (0o755, 0o644, 0o750, 0o600):
    b = f"{modo:09b}"
    print(f"{modo:o} = {b[:3]} {b[3:6]} {b[6:]} = {letras(modo)}")

with tempfile.TemporaryDirectory() as carpeta:
    for nombre, modo in (("main.py", 0o644), ("arrancar.sh", 0o755), ("token.txt", 0o000)):
        ruta = os.path.join(carpeta, nombre)
        with open(ruta, "w") as f:
            f.write("hola\n")
        os.chmod(ruta, modo)                   # la syscall chmod
        print(stat.filemode(os.stat(ruta).st_mode), nombre)
    try:
        open(os.path.join(carpeta, "token.txt")).read()
    except PermissionError as e:
        print("leer token.txt:", e.strerror, f"(errno {e.errno})")
Salida
755 = 111 101 101 = rwxr-xr-x
644 = 110 100 100 = rw-r--r--
750 = 111 101 000 = rwxr-x---
600 = 110 000 000 = rw-------
-rw-r--r-- main.py
-rwxr-xr-x arrancar.sh
---------- token.txt
leer token.txt: Permission denied (errno 13)

En Linux y en macOS sale igual, salvo como root: root lee el archivo aunque sus permisos digan ---------. Es lo que sigue.

Root: el usuario que se salta las reglas

El usuario con UID 0 se llama root («raíz», aunque no tiene que ver con la carpeta raíz, /): el superusuario. Para él, el kernel se salta casi todas las revisiones: lee y cambia cualquier archivo, mata cualquier proceso, monta discos y carga drivers. Para usar ese poder en un solo comando se escribe sudo antes: en un Jetson, cambiar el modo de energía es sudo nvpmodel, porque toca el hardware (M04).

¿Por qué no correr todo como root? Porque los programas tienen errores. Si alguien encuentra uno que le deje ejecutar su propio código dentro de tu proceso, obtiene exactamente los permisos de tu proceso. El gateway de C01 es justo ese riesgo: decodifica fotos que manda cualquiera, y una foto armada para explotar (aprovechar) un error de Pillow, la librería que lee las imágenes, podría tomar el control. La regla se llama mínimo privilegio: cada proceso con lo que necesita y nada más.

En un contenedor pesa más de lo que parece. Si la imagen no dice otra cosa, el proceso corre como root, y ese UID 0 es el mismo root del nodo, solo que encerrado y con menos poderes. (Linux puede darle al contenedor su propia numeración de usuarios, con un root que no es el del nodo, pero en Kubernetes eso es estable apenas desde la versión 1.36, de 2026, y hay que pedirlo para cada pod, la unidad con la que Kubernetes corre contenedores.) M05 lo resume: quien tome el control del proceso es root dentro del contenedor, «un paso menos para llegar al nodo».

C01 se defiende en dos lugares:

  • En la imagen. Su Dockerfile da de alta un usuario sin privilegios con useradd --uid 10001 y lo usa con USER 10001. El código se copia con dueño root y solo lectura para ese usuario: el proceso ni siquiera puede modificarse a sí mismo.
  • En Kubernetes. Con runAsNonRoot: true, Kubernetes se niega a arrancar el contenedor si fuera a correr como root. Para revisarlo necesita un número: no abre el /etc/passwd de la imagen, así que con USER app no sabría si app es el 0 y se negaría a arrancar. El archivo de Kubernetes del gateway, su manifiesto, fija además runAsUser: 10001.
05

Montar: discos, volúmenes y solo lectura

Esencial

Mounting: disks, volumes and read-only

Si todo cuelga de un solo árbol, ¿dónde aparece un disco nuevo? Donde tú digas. Montar (mount) es pegar un sistema de archivos completo en una carpeta del árbol, el punto de montaje. Desde ese momento, lo que hay en esa carpeta vive en el otro sistema. Ya lo has visto pasar: cuando conectas una memoria USB a una Mac, aparece como una carpeta dentro de /Volumes, porque la Mac la montó ahí.

Un ejemplo de servidor: llega un disco SSD nuevo, de tipo NVMe (la conexión de los SSD más rápidos), que aparece como /dev/nvme1n1. Se formatea con ext4 (formatear es escribirle un sistema de archivos vacío) y se monta en /data. Ahora /data/pesos/model.safetensors está en el disco nuevo, y /data es solo la puerta. El comando df -h lista lo montado, con el espacio que queda en cada uno.

Lo que se monta no siempre es un disco:

  • Un disco de red, como el EFS (Elastic File System) de AWS: los bytes viajan por la red, pero el programa abre los archivos como si fueran locales.
  • Un pedazo de memoria (tmpfs): /dev/shm vive en la RAM, así que lo que escribes ahí ocupa memoria, no disco.
  • Un sistema inventado: /proc y /sys, que el kernel arma al vuelo.
  • Otra carpeta (bind mount): la misma carpeta, vista desde otro lugar del árbol. Así le pasa Docker una carpeta de tu máquina a un contenedor.

Solo lectura

Cada montaje es de lectura y escritura (rw) o de solo lectura (ro). En uno de solo lectura, el kernel rechaza cualquier escritura con el error 30, «Read-only file system». Suena a limitación, pero protege: ni un error del programa ni un atacante pueden cambiar lo que está ahí, y muchos procesos comparten la misma copia sin pisarse. M05 lo propone para los pesos de un modelo grande: bajarlos una vez a un disco compartido que muchos pods montan en solo lectura. Kubernetes puede montar así hasta la raíz del contenedor, con readOnlyRootFilesystem: true, y el gateway de C01 lo hace: no necesita escribir nada. Solo le deja escribible /tmp, una carpeta vacía de 64 MiB montada aparte (un emptyDir, en la tabla de abajo), por si alguna librería necesita temporales.

El árbol de un contenedor, armado con montajes

El árbol propio de un contenedor se arma montando:

  1. La imagen, como raíz. Una imagen es una pila de capas de archivos de solo lectura: cada paso del Dockerfile que cambia archivos agrega una capa con lo que cambió. Linux las apila con un sistema de archivos hecho para eso, overlayfs, y el contenedor las ve como un solo /. Encima va una capa vacía de lectura y escritura, la del contenedor, que se borra con él.
  2. Lo que llega de afuera, en su carpeta. A lo que se monta así, desde fuera de la imagen, se le llama volumen (volume). En Docker, docker run -v /pesos:/models:ro monta la carpeta /pesos de la máquina en /models, en solo lectura. En Kubernetes son dos piezas: volumes dice qué se monta y volumeMounts dice dónde, con readOnly: true para solo lectura.
Qué se montaPara quéEn la Ronda
Un Secret o un ConfigMap (dos objetos de Kubernetes)Contraseñas y configuración, como archivosM15 prefiere un secreto montado como archivo a uno guardado en la imagen
emptyDirUna carpeta vacía que dura lo que dura el pod, en el disco del nodo o en memoriaC10, el caso del LLM, copia ahí sus 70.6 GB de pesos, sobre el disco NVMe del nodo
/dev/shm en memoriaMemoria compartida entre procesosDocker trae 64 MiB; C01 le da 1 GiB a Triton y C10, 16 GiB a vLLM, el programa que sirve su LLM
Un disco persistenteDatos que sobreviven al podCasi nunca en serving: el modelo no cambia

Lo importante: lo que un contenedor escribe fuera de un volumen se pierde cuando el contenedor se reemplaza, y en Kubernetes cada reinicio crea un contenedor nuevo, con su capa escribible vacía. Un log o un modelo descargado en la capa del contenedor no sobrevive. Ese error está en «Errores comunes».

06

Arquitectura de CPU: x86-64 y ARM64

Esencial

CPU architecture: x86-64 and ARM64

Una CPU solo entiende un idioma: su juego de instrucciones (instruction set), la lista de operaciones que sabe hacer (sumar, comparar, saltar a otra parte del programa, leer memoria) con el código en bytes de cada una. Un programa compilado es una fila de esas instrucciones. La arquitectura de la CPU dice qué idioma habla.

Dos familias dominan:

ArquitecturaOtros nombresDónde está
x86-64amd64, x64La mayoría de los servidores de la nube (como las g4dn de Triton y las c6i del gateway en C01), las PC con Windows y las Mac con chip Intel
ARM64aarch64, arm64Las Mac con Apple Silicon, los chips M de Apple (desde 2020), los Graviton de AWS (más de la mitad de la CPU nueva que instala AWS cada año desde 2023, según AWS), los Jetson y casi todos los teléfonos

x86-64 lo diseñó AMD como la versión de 64 bits del x86 de Intel (una CPU de 64 bits maneja números y direcciones de memoria de 64 bits); por eso también se llama amd64, aunque hoy lo usan las dos. El mismo programa, compilado para cada familia, queda escrito en bytes distintos. En la figura, cada byte va en hexadecimal: dos símbolos, del 0 al 9 y de la A (10) a la F (15).

Arriba, la misma orden, regresa de la función, en los dos idiomas: en x86-64 es un solo byte, C3; en ARM64 son cuatro bytes, C0, 03, 5F y D6. Una CPU x86-64 no entiende los bytes de ARM64. Abajo, la cabecera de un ejecutable de Linux, que el kernel lee antes de correrlo. Los bytes 0 a 3 son 7F, 45, 4C y 46: el 7F y las letras E, L, F, que dicen «soy un programa». Los bytes 18 y 19 son 3E y 00, el número 62, que quiere decir que se compiló para x86-64; para ARM64 serían 183. Si no es para su CPU, el kernel responde exec format error. «REGRESA DE LA FUNCIÓN», EN DOS IDIOMAS x86-64 C3 1 byte ARM64 C0 03 5F D6 4 bytes Una CPU x86-64 no entiende los 4 bytes de ARM64. Por eso el kernel lee antes la cabecera: LA CABECERA DE UN EJECUTABLE DE LINUX 0 1 2 3 18 19 7F 45 4C 46 · · · 3E 00 7F y las letras E, L, F: «soy un programa» la CPU: 3E 00 = 62, x86-64 (ARM64 sería 183) Si no es su CPU: «exec format error».
Arriba, la misma orden, «regresa de esta función», en los dos idiomas: un byte en x86-64 y cuatro en ARM64. Abajo, la cabecera (los primeros bytes) de un ejecutable de Linux, que el kernel lee antes de correrlo: los bytes 18 y 19 dicen para qué CPU se compiló.

«exec format error»

Antes de correr un programa, el kernel lee su cabecera (header), los primeros bytes del archivo. Un ejecutable de Linux empieza con 4 bytes fijos, 7F 45 4C 46: una marca y las letras E, L y F, el nombre del formato de los ejecutables de Linux. En los bytes 18 y 19 trae un número que dice para qué CPU se compiló: 62 si es x86-64, 183 si es ARM64. Si no coincide con la CPU de la máquina, el kernel se niega con el error 8: exec format error. En un contenedor, el log dice algo como exec /usr/local/bin/python: exec format error, y el contenedor muere al arrancar. Un script de arranque (un programa escrito como texto) da el mismo error si le falta su primera línea, la que empieza con #! y dice qué intérprete lo corre.

Para ir más hondoLos bytes de la cabecera, uno por uno

En hexadecimal, el primer símbolo de un byte vale 16 veces su número y el segundo, su número: C3 es 12 × 16 + 3 = 195. Cada letra se guarda como un número (la E es el 69, que es 4 × 16 + 5, o 45), y por eso Python escribe los 4 bytes del principio como \x7fELF. El número de la CPU ocupa dos bytes y va al revés de como lo escribirías: primero el byte que vale menos.

Los bytes 18 y 19, en decimalLa cuenta
Byte 18 en x86-643E; la E vale 14
3 × 16 + 14
62
Byte 1900; el segundo byte vale 256 veces su número
0 × 256
0
Número de la CPU
62 + 0
62: x86-64
Byte 18 en ARM64B7, con 00 en el byte 19; la B vale 11
11 × 16 + 7
183: ARM64
Resultado
x86-64 trae 3E 00 (62) y ARM64, B7 00 (183). El kernel compara ese número con el de su CPU.

Este código arma los primeros 20 bytes de un ejecutable para cada CPU y hace la misma revisión que el kernel, también con scripts, con y sin su línea #!:

python
import errno, os, struct

def compilado(cpu):                    # los primeros 20 bytes de un ejecutable de Linux
    return b"\x7fELF" + bytes([2, 1, 1]) + bytes(9) + struct.pack("<HH", 3, cpu)

def exec_en(cpu_del_nodo, archivo):    # lo que revisa el kernel antes de correr un archivo
    if archivo[:2] == b"#!":           # un script: corre el intérprete de su primera línea
        return "corre con " + archivo[2:].split(b"\n")[0].decode()
    if archivo[:4] == b"\x7fELF":      # un programa compilado: ¿para esta CPU?
        (cpu,) = struct.unpack_from("<H", archivo, 18)
        if cpu == cpu_del_nodo:
            return "corre"
    return os.strerror(errno.ENOEXEC)

for nombre, archivo in (("ELF para x86-64", compilado(62)), ("ELF para ARM64", compilado(183)),
                        ("script con #!", b"#!/usr/bin/python3\nprint('hola')"), ("script sin #!", b"print('hola')")):
    print(f"{nombre:<16} → en un nodo x86-64: {exec_en(62, archivo)}")
Salida
ELF para x86-64  → en un nodo x86-64: corre
ELF para ARM64   → en un nodo x86-64: Exec format error
script con #!    → en un nodo x86-64: corre con /usr/bin/python3
script sin #!    → en un nodo x86-64: Exec format error

Tu Mac guarda a veces dos idiomas en un solo archivo: el comando file, que dice qué tipo de archivo es uno, responde para /bin/ls que es un universal binary con dos arquitecturas adentro, x86_64 y arm64e (una variante de ARM64 que usa Apple).

El código de Python no depende de la CPU; sus librerías, sí

Tu archivo .py no depende de la CPU: lo lee el intérprete. Pero el intérprete mismo (python3) es un programa compilado, y también las partes rápidas de torch, NumPy o Pillow, escritas en C y C++. Por eso pip, el programa que instala librerías de Python, baja una versión distinta para cada plataforma: cada combinación de sistema operativo y arquitectura. De los 37 paquetes del gateway de C01, los que traen partes compiladas (NumPy, Pillow y otros) se instalan en versión manylinux x86_64, para Linux en x86-64; en ARM64 serían las aarch64. Los de puro Python sirven en cualquier CPU.

Las imágenes tienen plataforma

Una imagen de Docker se construye para una plataforma: linux/amd64 o linux/arm64. La trampa clásica, en M05: construyes en una Mac con Apple Silicon, la imagen sale linux/arm64, la subes al registro (el almacén de imágenes de donde los nodos las bajan) y los nodos de C01, que son x86-64, no la pueden correr. Hay dos salidas:

  • Construir para la plataforma de los nodos: en CI (las máquinas que construyen y prueban tu código cada vez que lo subes), con --platform linux/amd64.
  • Una imagen multi-arquitectura: un índice que, bajo un solo tag (el nombre de una versión de la imagen, como 3f9c2ab), apunta a una imagen por plataforma. Cada máquina baja la suya. Si el índice no trae la de tu máquina, la descarga falla con «no match for platform».

Docker Desktop puede correr imágenes amd64 en una Mac con Apple Silicon traduciendo sus instrucciones al vuelo (emulación, con Rosetta o con QEMU), pero más lento: sirve para probar, no para servir.

07

Qué es de verdad un contenedor

Esencial

What a container really is

En la lección «El camino completo», un contenedor era un paquete con tu programa y todo lo que necesita, para que corra igual en cualquier máquina. Sigue siendo cierto. Ahora puedes ver qué es para el kernel: un proceso normal, como cualquier otro del nodo, al que el kernel le hace tres cosas.

  1. 1

    Le pone lentes: los namespaces

    Un namespace (espacio de nombres) decide qué ve el proceso, como unos lentes que solo dejan ver una parte; solo que el proceso no se los puede quitar. Con el de procesos, ve una lista que empieza en él: adentro es el PID 1, aunque en el nodo tenga otro, digamos el 48,213. Con el de red, tiene su propia dirección en la red y sus propios puertos, las puertas numeradas por donde un programa recibe conexiones (el gateway escucha en el 8080; la fase 5 abre la red). Con el de montaje, su propio árbol, que empieza en la imagen. Hay otros para el nombre de la máquina, los usuarios, el reloj, la comunicación entre procesos y el propio árbol de cgroups: ocho en total.

  2. 2

    Le pone límites: los cgroups

    Un cgroup (control group) decide cuánto puede usar: cuánta memoria, cuánta CPU, cuántos procesos. Los limits de Kubernetes (el tope que el manifiesto le pone a cada contenedor) y el --memory de Docker se vuelven números del cgroup.

  3. 3

    Le da su árbol: los montajes

    Las capas de la imagen como su /, la capa escribible encima y los volúmenes en sus carpetas, como viste en la sección de montaje.

Además, le quita poderes. El poder de root está partido en unas 40 capacidades (capabilities), como cambiar el dueño de un archivo o abrir los puertos menores que 1024, y un contenedor arranca solo con algunas. Y un filtro de syscalls, seccomp, le bloquea las más peligrosas: la lista que Docker trae por omisión, su perfil, bloquea unas 44 de las más de 300.

Docker, containerd (el programa que arranca los contenedores en cada nodo) y Kubernetes no tienen magia propia: le piden todo esto al kernel con syscalls. Por eso, desde el nodo, el comando ps, que lista los procesos, muestra a Triton como uno más.

Un kernel para todos

Todos los contenedores de un nodo comparten el mismo kernel: el del nodo, al que la Ronda llama el host (la máquina anfitriona). Es la frase de M05 con la que empezó la lección, y tiene consecuencias:

  • La imagen no trae kernel. Trae el espacio de usuario de una distribución. Triton ve Ubuntu 22.04, pero le habla al kernel de Amazon Linux 2023 de su nodo, el mismo que usa el contenedor de al lado: el device plugin de NVIDIA, el programa que le anuncia a Kubernetes la GPU del nodo. El gateway, con su Debian, vive en otras máquinas, sin GPU, y le habla al kernel de la suya.
  • El driver de la GPU lo pone el nodo. El driver de NVIDIA tiene una parte que vive dentro del kernel (el módulo nvidia.ko: una pieza que se le agrega al kernel ya encendido, hecha para esa versión del kernel), así que no puede ir en la imagen. La imagen trae las librerías de CUDA; el nodo, el driver.
  • Aísla menos que una máquina virtual, que viene en un momento. Si alguien encontrara un error del kernel desde un contenedor, tendría el nodo entero, con todos sus contenedores.
Un nodo con GPU de C01 por dentro: dos contenedores, un solo kernel · Fíjate en la banda del medio: los dos contenedores, cada uno con su propia vista, terminan en el mismo kernel. Usa «Recorrer» para arrancar Triton, ver qué pasa cuando se pasa de memoria y seguir quién pone el kernel.
  • Device plugin · NVIDIA · uno por nodo
    El programa de NVIDIA que corre en cada nodo con GPU. Le pregunta al driver cuántas GPU hay y se lo anuncia a Kubernetes: «este nodo tiene nvidia.com/gpu: 1». Sin él, Kubernetes no sabría dónde poner a Triton. Trae su propia imagen, con su propio espacio de usuario.
  • Triton · Ubuntu 22.04 · UID 1000
    El servidor de modelos. Dentro ve Ubuntu 22.04, con las librerías de CUDA y el engine horneado en /models, y corre como el UID 1000, sin root. Ve la GPU porque el NVIDIA Container Toolkit le montó /dev/nvidia0 y las librerías del driver al arrancar.
  • cgroups · uno por contenedor
    Un cgroup por contenedor. El de Triton tiene un tope de memoria de 6 GiB, que cuenta también su /dev/shm, porque vive en la RAM. Si un grupo pasa de su tope, el kernel mata procesos de ese grupo y de ningún otro.
  • Namespaces · qué ve cada uno
    Cada contenedor tiene sus namespaces: su lista de procesos (adentro, Triton es el PID 1), su red y su árbol de archivos. Por eso el device plugin no ve los archivos ni los procesos de Triton, y Triton no ve los suyos.
  • Driver NVIDIA · nvidia.ko
    El módulo del driver de la GPU vive dentro del kernel del nodo; lo instaló la AMI acelerada de EKS. Los dos contenedores llegan a la misma T4 a través de él: Triton, para correr el modelo; el device plugin, para contarla. Por eso no viaja en ninguna imagen.
  • CPU y RAM · 4 vCPU · 16 GiB
    La CPU x86-64 y la RAM de la g4dn.xlarge: 4 vCPU y 16 GiB. El kernel las reparte entre los contenedores. Ojo: cada contenedor cuenta los 4 vCPU del nodo, aunque su cgroup le dé solo una parte.
  • Disco · capas de las imágenes
    Las imágenes bajadas viven en el disco del nodo, capa por capa. Al arrancar un contenedor, el kernel monta sus capas como su /, con una capa escribible encima que se borra con él.
  • T4 · 16 GB · /dev/nvidia0
    La GPU del nodo. Para el kernel es un dispositivo; para el contenedor de Triton, el archivo /dev/nvidia0. Kubernetes se la da entera a Triton.

La máquina virtual: un kernel por máquina

Una máquina virtual (VM, virtual machine) es otra idea. Un programa llamado hipervisor (hypervisor) parte una computadora física en varias computadoras simuladas, cada una con sus CPU, su memoria y su disco virtuales. En cada VM arranca un sistema operativo completo, con su propio kernel.

Contenedores contra máquinas virtuales, como pilas. Arriba, los tres contenedores del docker compose de C01, que para probar corren juntos en una sola máquina con GPU: el gateway con Debian y Python 3.13, Triton con Ubuntu 22.04, CUDA y el engine, y Prometheus, el programa que junta las métricas, con BusyBox, un espacio de usuario mínimo, y su binario. Cada uno trae solo su espacio de usuario, y los tres se apoyan en un solo kernel Linux, el de la máquina, que está sobre el hardware: CPU, RAM, GPU y disco. Abajo, dos máquinas virtuales: cada una tiene tu servicio, su sistema completo y su propio kernel, y las dos se apoyan en un hipervisor (en AWS, Nitro), que reparte el hardware. CONTENEDORES: UN KERNEL PARA TODOS gateway Triton Prometheus Debian Python 3.13 Ubuntu 22.04 CUDA, engine BusyBox y su binario cada uno trae su espacio de usuario, sin kernel kernel Linux de la máquina: uno solo hardware: CPU, RAM, GPU, disco MÁQUINAS VIRTUALES: UN KERNEL POR VM tu servicio tu servicio su sistema completo su sistema completo su propio kernel su propio kernel hipervisor (en AWS, Nitro) hardware: CPU, RAM, GPU, disco
Arriba, contenedores: los del docker compose de C01, el archivo que, para probar, levanta juntos en una sola máquina con GPU el gateway, Triton y Prometheus, el programa que junta las métricas. Hay un solo kernel, el de la máquina, y cada contenedor trae solo su espacio de usuario: Debian, Ubuntu o BusyBox, uno mínimo. Abajo, máquinas virtuales: el hipervisor reparte el hardware y cada VM arranca su propio kernel y su sistema completo.
ContenedorMáquina virtual
Qué esUn proceso con namespaces y cgroupsUna computadora simulada por un hipervisor
KernelEl del nodo, compartidoUno propio en cada VM
Qué trae su imagenUn espacio de usuario y tu programaUn sistema operativo completo y tu programa
Arranca enMenos de un segundo si la imagen ya está en el nodo, más lo que tarde su programaDe segundos a minutos: arranca un sistema entero
Memoria de másCasi nadaLa de su kernel y su sistema
Qué aíslaLo que ve y cuánto usaTambién el kernel: hay que romper el hipervisor

Las dos se usan juntas. Cada máquina que rentas en EC2, el servicio de AWS que renta computadoras (le dice instancia), es una VM, como la g4dn.xlarge de C01: el hipervisor de AWS, Nitro, parte un servidor físico entre clientes, y la T4 se le entrega a la VM directo. Dentro de la VM corre Linux, y dentro de Linux, los contenedores. La VM separa a un cliente de AWS de otro; los contenedores, los servicios de un mismo equipo.

El límite de memoria vive en el cgroup

En «Programa, proceso e hilo» hiciste la cuenta del gateway de C01 en su peor momento: ≈ 80 MB de Python, dos PNG gigantes de ≈ 320 MB decodificándose a la vez y 160 MB de requests en vuelo suman 880 MB, el 82 % de su límite de 1 GiB. Ese límite es un número del cgroup del contenedor, en bytes: 1 GiB = 2³⁰ = 1,073,741,824.

Si los procesos del grupo lo pasan, no hay negociación: el kernel mata al proceso con SIGKILL, y Kubernetes lo reporta como OOMKilled, con código de salida 137 (128 + 9): el OOM de esa lección. Y mata solo ahí: los demás contenedores, cada uno en su cgroup, ni se enteran, aunque a la máquina le sobre RAM. La CPU, en cambio, no mata a nadie: un contenedor con límite de CPU que se acaba su parte se queda congelado unos milisegundos (throttling). La sección extra que sigue abre los dos límites con números.

08

Los límites por dentro: cuotas de CPU y memoria

Extra

Limits up close: CPU quotas and memory

Un cgroup, como casi todo en Linux, se ve como archivos. Cada contenedor tiene su carpeta en /sys/fs/cgroup, y Kubernetes escribe ahí sus limits. Así se ven desde adentro de un contenedor en Linux, con cat, el comando que imprime lo que tiene un archivo. Es una ilustración: en tu Mac esos archivos no existen, y la ventana y el congelamiento se explican en un momento.

bash
cat /sys/fs/cgroup/memory.max      # 1073741824: el límite de 1Gi, en bytes
cat /sys/fs/cgroup/memory.current  # lo que usa ahora, en bytes
cat /sys/fs/cgroup/cpu.max         # «max 100000»: sin límite de CPU; ventana de 100,000 µs
cat /sys/fs/cgroup/cpu.stat        # nr_throttled: cuántas ventanas lo congelaron

Lo que se reserva y lo que se corta

El manifiesto de cada pod le da a cada contenedor dos números por recurso, en sus partes requests y limits. Lo que va en requests es lo que se reserva: Kubernetes solo pone el pod en un nodo donde quede eso libre. Lo que va en limits es el techo, y termina en el cgroup como memory.max y cpu.max. La reserva de CPU también llega al cgroup, como un peso (cpu.weight) que decide cómo se reparte la CPU cuando no alcanza para todos. En C01, el gateway reserva 1 CPU y 1 GiB, con un techo de memoria de 1 GiB y ninguno de CPU. Cada Triton reserva 2 CPU, 6 GiB y una GPU entera, también con el techo de memoria igual a lo que reserva (M06). Al pasarse del techo, la memoria y la CPU reaccionan muy distinto.

La memoria: un tope duro

memory.max es un tope. Cuando los procesos del grupo llegan a él, el kernel primero intenta liberar lo que no es indispensable, como las copias de archivos que ya leyó (su page cache). Si no alcanza, el OOM killer (out of memory, sin memoria) mata con SIGKILL a un proceso del grupo; en Kubernetes, desde la versión 1.28, a todos los procesos del contenedor de un jalón. No hay excepción que atrapar ni mensaje de despedida: el proceso desaparece. Por eso C01 pone el límite de memoria igual a lo que reserva: si se pasa, el resultado es un OOMKilled claro de ese contenedor, en vez de quitarle al resto del nodo memoria que nadie le reservó (M06).

¿Cabe el gateway en su límite de memoria?Calculadora

Si los procesos de un cgroup pasan de su límite de memoria, el kernel mata uno. Arranca con el gateway de C01 en el peor caso, como en «Programa, proceso e hilo»: ≈ 80 MB de Python, 2 decodificaciones a la vez de ≈ 320 MB (un PNG de 40 megapíxeles) y 16 requests en vuelo de hasta 10 MB.

Memoria en el peor caso880 MB
Límite del cgroup1 GiB = 2³⁰ bytes ≈ 1.07 GB1.07 GB
Parte del límite que usamás de 100 %: OOMKilled, código 13782.0 %
Margennegativo: el kernel mata el proceso194 MB

Con 3 decodificaciones a la vez son 1.2 GB, que no caben en 1 GiB; con un límite de 1.5 GiB quedan en 75 %. Con 4 son 1.52 GB: por eso la Ronda dice que con 4 el límite sube a 2 GiB. La RAM libre del nodo no cuenta: el tope es del contenedor.

Es la cuenta del gateway de «Programa, proceso e hilo», con el límite tal como lo guarda el cgroup. Sube las decodificaciones a 3 y mira cómo el margen se vuelve negativo; después sube el límite hasta que quepan con margen.

La CPU: una cuota por ventana

La CPU no tiene tope duro: tiene cuota. Un «ms de CPU» es un milisegundo de trabajo de un núcleo: 4 hilos que corren 1 ms en 4 núcleos gastan 4 ms de CPU. Con un límite de 1 CPU, el grupo puede usar 100 ms de CPU en cada ventana de 100 ms, sumando todos sus hilos. Con 500m (500 milésimas de CPU, medio CPU), 50 ms por ventana. Si el grupo se acaba la cuota antes de que termine la ventana, el kernel lo congela hasta la siguiente. Eso es el throttling: el proceso no muere, se vuelve lento a ráfagas.

M06 lo cuenta con el gateway (si leíste la sección extra de «Programa, proceso e hilo», ya viste esta cuenta; aquí va con el mecanismo completo):

Cuatro fotos a la vez con un límite de 1 CPULa cuenta
Cuota por ventanalímite de 1 CPU, ventana de 100 ms
1 × 100 ms
100 ms de CPU
CPU que piden las 4 fotos40 ms de CPU cada una, un hilo por foto
4 × 40 ms
160 ms de CPU
Cuándo se acaba la cuota4 hilos gastan 4 ms de CPU por cada ms de reloj
100 ÷ 4
a los 25 ms
Congelado
del ms 25 al 100
75 ms sin correr
Lo que faltaa cada foto le faltan 40 − 25 = 15 ms de CPU
4 × 15 ms ÷ 4 hilos
15 ms más
Resultado
Cada foto tarda 100 + 15 = 115 ms en vez de 40. Y una gráfica que promedia el uso de cada minuto ve poca CPU, porque las ráfagas duran milisegundos. Por eso C01 no le pone límite de CPU al gateway ni a Triton: lo que reservan, con su peso en el cgroup, ya les garantiza su parte.

El mismo cálculo, para más fotos a la vez. Supone que llegan justo al empezar una ventana y que el nodo tiene núcleos de sobra:

python
def tarda(fotos, ms_de_cpu, limite, ventana=100):
    cuota = limite * ventana          # ms de CPU que el cgroup deja usar en cada ventana
    falta = fotos * ms_de_cpu         # ms de CPU que piden entre todas, un hilo por foto
    reloj = 0
    while falta > cuota:              # se acaba la cuota: congeladas hasta la siguiente ventana
        falta -= cuota
        reloj += ventana
    return reloj + falta / fotos      # en la última ventana, los hilos corren a la vez

for fotos in (1, 2, 4, 8):
    t = tarda(fotos, 40, limite=1)
    print(f"{fotos} a la vez, límite de 1 CPU: {t:g} ms cada una ({t / 40:.1f} veces lo normal)")
print(f"8 a la vez, límite de 4 CPU: {tarda(8, 40, limite=4):g} ms cada una")
Salida
1 a la vez, límite de 1 CPU: 40 ms cada una (1.0 veces lo normal)
2 a la vez, límite de 1 CPU: 40 ms cada una (1.0 veces lo normal)
4 a la vez, límite de 1 CPU: 115 ms cada una (2.9 veces lo normal)
8 a la vez, límite de 1 CPU: 302.5 ms cada una (7.6 veces lo normal)
8 a la vez, límite de 4 CPU: 40 ms cada una
El límite de CPU: cuota y throttlingCalculadora

Con un límite de CPU, el cgroup da una cuota: tantos ms de CPU por cada ventana de 100 ms, sumando todos los hilos. Arranca con el ejemplo de M06: 4 fotos a la vez, 40 ms de CPU cada una y un límite de 1 CPU.

Cuota por ventana de 100 msde CPU, sumando todos los hilos100 ms
CPU que piden entre todas160 ms
Ventanas enteras en que se congelan1
Lo que tarda cada foto115.0 ms
Veces lo que tardaría sin límite1.0× = el límite alcanza2.9×

Supone que las fotos llegan justo al empezar una ventana y que el nodo tiene un núcleo libre para cada hilo. Si llegan a media ventana, puede ir mejor o peor: depende de cuánta cuota quede.

Prueba 8 fotos con límite de 1 y luego sube el límite hasta que cada foto vuelva a tardar 40 ms.

Resuélvelo con lápiz y compruébalo con la calculadora:

  1. 1Medio

    Un pod tiene un límite de 2 CPU. Llegan 8 fotos a la vez, cada una con 40 ms de CPU y su propio hilo. ¿Cuánto tarda cada una? ¿Y sin límite, con 8 núcleos libres?

    Ver la solución paso a paso
    • Cuota: 2 × 100 = 200 ms de CPU por ventana. Piden 8 × 40 = 320 ms.
    • 8 hilos gastan 8 ms de CPU por ms: la cuota se acaba a los 200 ÷ 8 = 25 ms y quedan 120 ms de CPU.
    • Congeladas hasta el ms 100; luego 120 ÷ 8 = 15 ms más. 115 ms cada una.
    • Sin límite: 40 ms. Con un límite de 4 CPU, la cuota de 400 ms alcanza y también serían 40.

El contenedor no sabe cuántas CPU tiene

Un detalle que muerde: dentro de un contenedor, os.cpu_count() de Python, y muchas librerías, cuentan los núcleos del nodo, no la cuota del cgroup. En un nodo de 64 núcleos con un límite de 4 CPU, una librería que arma un hilo por núcleo lanza 64 hilos a pelear por 4 CPU de cuota: throttling y un p99 peor. M08 lo pregunta con ONNX Runtime, un programa para correr modelos que se usa mucho en CPU, y la respuesta es fijar el número de hilos a mano, según el límite del pod.

09

Errores comunes

Esencial

Common mistakes

1. Construir en la Mac y desplegar en x86-64

La imagen sale linux/arm64, y en los nodos x86-64 el contenedor muere con exec format error, o la descarga falla con «no match for platform».

Cómo evitarlo: construir en CI con --platform linux/amd64, o multi-arquitectura a propósito. Para revisar una imagen: docker image inspect --format '{{.Architecture}}' <imagen>.

2. Arreglar «Permission denied» con 777 o con root

Funciona, pero abre todo: cualquiera puede cambiar el archivo, o el proceso puede hacer cualquier cosa.

Cómo evitarlo: buscar qué falta de verdad (¿la x de una carpeta del camino?, ¿el dueño?) con ls -l y con id, que dice tu UID y tus grupos, y dar justo eso: chown al usuario de la app y 644 o 755. Y el servicio, con un USER numérico.

3. Rutas relativas que dependen de dónde arrancas

open("models/model.plan") funciona desde la carpeta de tu proyecto y falla en el contenedor, que arranca en otra carpeta: FileNotFoundError.

Cómo evitarlo: rutas absolutas que vienen de la configuración (MODEL_PATH=/models/classifier/1/model.plan), o relativas al propio archivo de código con Path(__file__).parent: en Python, __file__ es la ruta del archivo .py que está corriendo, así que no depende de dónde arrancaste.

4. Mayúsculas que tu Mac perdona

Model.plan y model.plan son el mismo archivo en tu Mac y dos distintos en Linux.

Cómo evitarlo: nombres en minúsculas, y las pruebas de CI dentro del contenedor de Linux, no solo en tu laptop.

5. Escribir en el contenedor como si fuera un disco

Logs, cachés o modelos descargados en la capa escribible se pierden cuando Kubernetes reinicia el contenedor, llenan el disco del nodo y, con readOnlyRootFilesystem, fallan con «Read-only file system».

Cómo evitarlo: los logs, a la salida del programa (stdout), donde los recoge el sistema de logs; lo que debe durar, a un volumen o a S3 (el almacén de archivos de AWS); y un emptyDir montado en /tmp si la app necesita temporales.

6. Creer que el contenedor ve sus CPU

os.cpu_count() cuenta los núcleos del nodo. Una librería que lanza un hilo por núcleo dentro de un límite de 4 CPU se congela a ráfagas.

Cómo evitarlo: fijar los hilos según el límite (torch.set_num_threads, intra_op_num_threads en ONNX Runtime) y vigilar el throttling con dos métricas que junta Prometheus: container_cpu_cfs_throttled_periods_total entre container_cpu_cfs_periods_total es la fracción de ventanas de 100 ms en que el cgroup lo congeló (sección 8).

7. Un secreto en una capa de la imagen

Copiar el archivo de secretos a la imagen (COPY .env) lo deja guardado en una capa, y pasarle al build un token (una contraseña para programas, como el HF_TOKEN que deja bajar modelos de Hugging Face) como argumento (ARG HF_TOKEN) lo deja en el historial de la imagen. Borrarlo en una línea posterior del Dockerfile no lo quita: la capa de antes sigue ahí, y cualquiera que pueda bajar la imagen lo lee (M15).

Cómo evitarlo: que el secreto nunca entre a la imagen (.env en .dockerignore; si el build lo necesita, RUN --mount=type=secret, que no deja rastro en ninguna capa) y que en producción llegue montado como archivo, o que no haga falta porque el pod tiene su propia identidad en la nube, con permisos y sin contraseña.

10

En la vida real

Extra

In real life

C01: los nodos de punta a punta

Triton corre en una g4dn.xlarge: una VM sobre Nitro, con una CPU x86-64 (un Intel Xeon), 4 vCPU, 16 GiB de RAM y una T4 que se le entrega directo. Arranca con Amazon Linux 2023 y la AMI acelerada de EKS, que ya trae el driver de NVIDIA y el Container Toolkit (M06). El gateway vive aparte, en máquinas sin GPU (dos c6i.xlarge), también x86-64. Las dos imágenes son linux/amd64:

  • El gateway: Debian con Python 3.13, UID 10001, raíz de solo lectura (readOnlyRootFilesystem: true) con /tmp en un emptyDir, sin ninguna capacidad de root (drop: ["ALL"]), el filtro seccomp por omisión (Docker lo aplica siempre; Kubernetes, solo si el pod lo pide con seccompProfile: RuntimeDefault, como aquí), 1 GiB de memoria y sin límite de CPU. Escucha en el puerto 8080, que no pide ningún poder: en Linux, por omisión, abrir un puerto menor que 1024, como el 80, es uno de los poderes de root (Docker, desde 2020, y containerd, desde su versión 2.0, lo permiten dentro de sus contenedores).
  • Triton: Ubuntu 22.04, con el engine horneado (copiado dentro de la imagen) en /models/classifier/1/model.plan, /dev/nvidia0 montado por el toolkit y 6 GiB de memoria. La imagen de NVIDIA corre como root si nadie dice otra cosa, así que el manifiesto de Kubernetes de C01 la arranca como el UID 1000, el usuario triton-server que la imagen ya trae, con runAsNonRoot: true y sin capacidades de root.

C01: ¿y si fuera ARM64?

AWS ofrece la misma T4 en una máquina ARM64 más barata, la g5g.xlarge, con un Graviton2; a su GPU la Ronda la llama T4G. La Ronda la pone junto a la g4dn.xlarge que eligió C01. Un mes son 730 horas (24 × 365 ÷ 12):

Dos T4 al mes: x86-64 contra ARM64La cuenta
Dos g4dn.xlargex86-64 · 16 GiB de RAM
2 × US$0.526/h × 730 h
≈ US$768
Dos g5g.xlargeARM64 (Graviton2) · la misma T4 · 8 GiB de RAM
2 × US$0.42/h × 730 h
≈ US$613
Ahorro155 ÷ 768 ≈ 20 %
768 − 613
≈ US$155 al mes
Red sostenida de cada g5gel dato de AWS que usa la Ronda
—
≈ 1.25 Gb/s
Lo que recibe cada nodo en el pico500 fotos/s, cada una un tensor de 602 KB, repartidas entre dos
500 × 602 KB × 8 bits ÷ 2
≈ 1.2 Gb/s
Resultado
Ahorra 20 %, pero cada nodo quedaría en el límite de su red, con la mitad de RAM, y habría que reconstruir la imagen de Triton para arm64 y el engine en la T4G (y la del gateway, si sus máquinas también se mudaran a Graviton). C01 se quedó en x86-64.

C10: un LLM de 70.6 GB

C10 sirve con vLLM un modelo de ≈ 70,600 millones de parámetros en FP8, un byte por parámetro: 70.6 GB. Casi todo su arranque es sistema operativo. Un contenedor que corre y termina antes que vLLM copia los pesos de S3 al disco NVMe del nodo, en un emptyDir, y revisa cada archivo contra su huella (sha256), un número que cambia si cambia un solo byte. /dev/shm va en memoria con 16 GiB, que cuentan contra el límite del contenedor: vLLM corre un proceso por GPU, y esos procesos se coordinan por memoria compartida. Los pesos van en safetensors, que no ejecuta código al cargarse (un pickle, el formato que PyTorch usa si no le pides otro, sí puede) y se puede leer con mmap: el kernel trae del disco cada pedazo cuando se lee.

C02: una caja con GPU en cada centro de distribución

En C02, cada centro tiene su propia caja con una GPU L4 de NVIDIA que vigila sus cámaras. La contraseña de las cámaras nunca está en el código: el forwarder, el programa que le pide a cada cámara sus fotos y arma los eventos, la lee de un secreto montado como archivo. Y el archivo de configuración de DeepStream (el programa de NVIDIA que procesa el video), que solo acepta la contraseña escrita dentro de la dirección de la cámara, se genera en la caja al instalarla, con permisos 0600 (el 0 de adelante solo avisa que el número va en octal): solo su dueño lo puede leer. Antes de activar una imagen nueva, la caja revisa su firma, una marca que solo el equipo puede producir con su llave privada: nada que no salió del equipo llega a correr ahí.

C03: un Jetson en cada línea de producción

En la planta de C03, cada línea tiene un Jetson Orin NX: ARM64 con Jetson Linux (L4T) sobre Ubuntu 22.04 en JetPack 6.2. Sus imágenes son linux/arm64 y, regla de M04, usan la misma versión de L4T que el equipo. El engine se construye en un Orin NX igual a los de la línea, con el mismo JetPack, y el modo de energía se fija con sudo nvpmodel. Actualizar el JetPack de la flota se hace a distancia (OTA, over the air), obliga a reconstruir los engines y lleva un plan para regresar a la versión anterior (M04).

Fuera de ML

  • Android usa el kernel Linux y le da a cada app su propio UID. Los permisos de esta lección, los de dueño, grupo y demás, son los que impiden que una app lea los archivos de otra.
  • AWS Lambda, el servicio que corre una función tuya cada vez que llega una petición, sin que tengas un servidor, corre cada función en una microVM de Firecracker, una VM mínima que AWS dice que arranca en ≈ 125 ms con menos de 5 MiB de memoria de más. Como comparte servidores con código de miles de clientes, un contenedor no le basta: quiere el aislamiento de una VM.
  • GitLab, enero de 2017. Un ingeniero que intentaba arreglar una réplica de la base de datos (una copia que se mantiene al día) borró por error, en el servidor equivocado, la carpeta de datos de la base principal: ≈ 300 GB. Los respaldos tampoco servían, y se perdieron unas 6 horas de datos de los usuarios. En Linux, rm, el comando que borra, no pregunta ni manda nada a una papelera: lo que tus permisos dejan borrar, se borra. Por eso importa que cada quien tenga solo los permisos que necesita.
  • CrowdStrike, julio de 2024. Una actualización defectuosa de un programa de seguridad que corre dentro del kernel de Windows tumbó ≈ 8.5 millones de computadoras, con aerolíneas, bancos y hospitales detenidos. Un error en el espacio de usuario mata un programa; un error en el kernel tumba la máquina.
11

Ejercicios

Esencial

Exercises

Resuélvelos con lápiz antes de abrir la solución. Hay uno más, de la cuota de CPU, en la sección extra «Los límites por dentro», junto a su calculadora.

  1. 1Fácil

    Un proceso está parado en /opt/tritonserver. (a) ¿A qué ruta absoluta lleva ../../models/classifier/1/model.plan? (b) ¿Y bin/tritonserver? (c) De models/x, /models/x y ./models/x, ¿cuál es absoluta?

    Ver la solución paso a paso
    • (a) El primer .. sube a /opt y el segundo, a /. Desde ahí baja: /models/classifier/1/model.plan.
    • (b) Es relativa, así que se pega a la carpeta de trabajo: /opt/tritonserver/bin/tritonserver.
    • (c) Solo /models/x, la que empieza con /. Las otras dos son la misma ruta relativa: /opt/tritonserver/models/x.
  2. 2Fácil

    (a) Escribe rw-r----- en octal. (b) Escribe 755 en letras. (c) ¿Quién puede leer un archivo con permisos 600?

    Ver la solución paso a paso
    • (a) rw- = 4 + 2 + 0 = 6; r-- = 4; --- = 0. 640.
    • (b) 7 = rwx y 5 = r-x: rwxr-xr-x.
    • (c) Solo su dueño, y root, que se salta los permisos.
  3. 3Fácil

    ¿Qué pasó en cada caso? (a) PermissionError: [Errno 13] Permission denied: '/models/classifier/1/model.plan'. (b) FileNotFoundError: [Errno 2] No such file or directory: 'models/model.plan'. (c) OSError: [Errno 30] Read-only file system: '/srv/app/cache.json'. (d) exec /usr/local/bin/python: exec format error.

    Ver la solución paso a paso
    • (a) El kernel revisó los permisos y el UID del proceso no puede leer el archivo, o no puede entrar a alguna carpeta del camino.
    • (b) La ruta no existe. Como es relativa, lo más probable es que el proceso arrancó en otra carpeta.
    • (c) Esa carpeta está montada en solo lectura, por ejemplo con readOnlyRootFilesystem.
    • (d) El programa es de otra arquitectura (una imagen arm64 en un nodo x86-64), o es un script sin la línea #!.
  4. 4Fácil

    ¿Qué viaja dentro de la imagen de Triton y qué pone el nodo? Python 3.10, glibc, el kernel, nvidia.ko, las librerías de CUDA, la T4 y el engine.

    Ver la solución paso a paso
    • En la imagen: Python 3.10 y glibc (son el espacio de usuario de Ubuntu 22.04), las librerías de CUDA y el engine, que va horneado.
    • Del nodo: el kernel, nvidia.ko (el módulo del driver vive en el kernel) y la T4. Las librerías del driver también son del nodo: el toolkit las monta al arrancar.
  5. 5Medio

    El engine tiene permisos 644 y dueño root. La carpeta /models/classifier/1 tiene 700 y dueño root. Triton corre como el UID 1000. ¿Puede leer el engine? ¿Cómo lo arreglas?

    Ver la solución paso a paso
    • Para llegar al archivo, el kernel pide x en cada carpeta del camino. La carpeta tiene 700 (rwx------): el UID 1000 cae en «los demás», que tienen 0. No puede entrar y recibe «Permission denied», aunque el archivo diga r para todos.
    • Arreglo: chmod 755 /models/classifier/1, o hacer dueño de la carpeta al UID 1000. Nunca 777: Triton solo necesita leer.
  6. 6Medio

    Dentro del contenedor de Triton, ps muestra a tritonserver como el PID 1. Desde el nodo, el mismo programa aparece como el PID 48,213. ¿Son dos procesos? ¿Qué pasa si desde el nodo le mandas kill -9 48213?

    Ver la solución paso a paso
    • Es uno solo, con dos números: el namespace de procesos le da su propia numeración, que adentro empieza en 1.
    • kill -9 manda SIGKILL: el proceso muere, el contenedor termina con código 137 (128 + 9) y Kubernetes lo vuelve a arrancar.
    • Al revés no se puede: desde adentro, el contenedor ni siquiera ve los procesos del nodo.
  7. 7Medio

    Un servidor de modelos tiene tres cosas en su contenedor: (a) su log, en /var/log/app.log, en la capa escribible; (b) los pesos, en un volumen montado en /models con readOnly: true; (c) 512 MiB de datos que sus procesos se pasan entre sí, en /dev/shm. Kubernetes reinicia el contenedor: ¿qué pasa con (a) y con (b)? ¿Y si el programa intenta escribir en /models? ¿Contra qué cuentan los 512 MiB de (c)?

    Ver la solución paso a paso
    • (a) Se pierde: cada reinicio crea un contenedor nuevo, con la capa escribible vacía. Por eso los logs van a stdout.
    • (b) Sigue ahí. El volumen no es parte del contenedor: se vuelve a montar en el nuevo. Escribir en /models falla con el error 30, «Read-only file system»: el kernel rechaza la escritura y nadie puede corromper los pesos.
    • (c) /dev/shm vive en la RAM, así que sus 512 MiB cuentan contra el límite de memoria del contenedor, como el de Triton y el de C10. Si el proceso ya usa 5.7 GiB y el límite es de 6 GiB, esos 0.5 GiB lo pasan del tope (5.7 + 0.5 = 6.2): OOMKilled, código 137.
  8. 8Reto

    Construyes la imagen del gateway en tu MacBook con un chip M3 y la subes. En los nodos de C01, el pod muere al arrancar. (a) ¿Qué pasó y qué dice el log? (b) Da dos arreglos. (c) Si C01 se mudara a máquinas con Graviton, ¿qué cambiaría?

    Ver la solución paso a paso
    • (a) Docker construyó para la plataforma de tu Mac: linux/arm64. Los nodos de C01, las c6i del gateway y las g4dn de Triton, son x86-64. Al arrancar, el kernel lee la cabecera de python, ve el 183 de ARM64 en vez del 62 y se niega: exec format error. Si el registro guardó un índice sin amd64, ni siquiera baja: «no match for platform».
    • (b) Construir en CI con --platform linux/amd64. O una imagen multi-arquitectura con las dos plataformas bajo el mismo tag. Y revisar con docker image inspect antes de desplegar.
    • (c) En nodos ARM64 esa imagen sí correría: el gateway en máquinas de CPU con Graviton y Triton en las g5g, las máquinas ARM64 de AWS con la misma T4 (la T4G). Pero harían falta una imagen arm64 de Triton, un engine construido en la T4G y las librerías del gateway en aarch64, y aceptar en las g5g la mitad de RAM y una red justa para el pico («En la vida real» trae la cuenta).
  9. 9Reto

    El gateway de C01 lleva USER 10001 en su Dockerfile y esto en Kubernetes: runAsNonRoot: true, readOnlyRootFilesystem: true y capabilities: drop: ["ALL"]. Explica qué protege cada línea. Un día alguien agrega código que guarda un caché (una copia de resultados, para no recalcularlos) en /srv/app/cache.json: ¿qué pasa y cómo se arregla sin quitar nada?

    Ver la solución paso a paso
    • USER 10001: el proceso corre como un usuario sin privilegios. Si una foto maliciosa explota un error de Pillow, el atacante tiene los permisos del UID 10001, no los de root.
    • runAsNonRoot: true: Kubernetes se niega a arrancar el contenedor si fuera a correr como root; con el UID numérico lo puede verificar.
    • readOnlyRootFilesystem: true: la raíz se monta en solo lectura. Nadie puede cambiar el código ni dejar herramientas instaladas.
    • drop: ["ALL"]: aunque algo corriera como root, no tendría sus poderes especiales. El gateway no necesita ninguno: escucha en el 8080.
    • El caché falla con «Read-only file system» (errno 30): /srv/app es parte de la raíz, que está en solo lectura. Arreglo: escribirlo en /tmp, el emptyDir de 64 MiB que C01 ya monta (escribible, y se borra con el pod), o montar otro emptyDir solo para el caché. Lo demás sigue en solo lectura.
12

Lo que sigue

Esencial

What comes next

En la Ronda:

  • M05 · Contenedores es esta lección aplicada: qué viaja en la imagen y qué no (el kernel y el driver son del nodo), el usuario no root con UID numérico, la trampa de construir en la Mac y las capas de solo lectura con una escribible encima.
  • M06 · Orquestación y escalado convierte lo que reserva y lo que limita cada pod en números del cgroup (la cuota de 100 ms, el OOMKilled con código 137) y usa los volúmenes, los Secrets montados como archivos y las AMI de los nodos con GPU.
  • M15 · Seguridad y confiabilidad usa el mínimo privilegio en cada componente, los secretos que no van en una capa de la imagen y los pesos en safetensors en vez de pickles.
  • También: M04 (Jetson con ARM64, L4T y sudo nvpmodel) y M08 (los hilos de ONNX Runtime contra el límite del pod).

En el Dojo: la estación 7, «Empaquetado (Docker)», ya está abierta. Ahí el model server corre como USER 10001:10001, te topas con la diferencia entre la imagen arm64 de tu Mac y la amd64 de un servidor, y montas el modelo en un volumen de solo lectura (:ro). La estación 9, «Kubernetes», todavía no abre.

En Sensei: la siguiente lección, «Hacer varias cosas a la vez: async, event loop y GIL», empieza donde terminan las syscalls: qué hace un programa mientras espera a que el kernel le traiga un archivo o una respuesta de la red, sin quedarse bloqueado. La fase 5, con «Cómo viaja un mensaje: IP, puertos y DNS», abre la red. Más adelante, «La terminal y la shell» practica ls, cd y chmod con las manos, y «Empaquetado con Docker» arma las imágenes de C01 capa por capa.

13

Palabras que vas a oír

Esencial

Words you will hear

Estos son los conceptos que presentó esta lección. Cada uno tiene su tarjeta con la definición completa.

Sistema operativooperating system · OSEl programa que administra la computadora: reparte la CPU, la memoria y el disco entre los demás programas, guarda los archivos y no deja que un programa se meta con otro. Linux, Windows, macOS y Android son sistemas operativos.Más

Tiene dos partes. El kernel, el núcleo que controla el hardware y corre con todos los privilegios, y el espacio de usuario (user space): la biblioteca de C (como glibc), la shell, los comandos como ls y los programas. Un programa nunca toca el hardware directo: se lo pide al kernel con una llamada al sistema. En los servidores, el sistema operativo casi siempre es Linux. Una imagen de contenedor trae el espacio de usuario de una distribución, pero usa el kernel de la máquina donde corre.

Ejemplo. En un nodo con GPU de C01 corre Amazon Linux 2023. Su kernel reparte 4 vCPU y 16 GiB de RAM entre Triton y los programas de Kubernetes de esa máquina, y es el que deja o no a cada uno leer cada archivo.

Ver en el glosario →

Kernelkernel · núcleo del sistema operativoEl núcleo del sistema operativo: el único programa que toca el hardware directamente. Reparte la CPU y la memoria entre los programas que están corriendo (los procesos), maneja los discos y la red, y revisa los permisos de todo lo que le piden.Más

La CPU tiene dos modos: en modo kernel se puede todo; en modo usuario no se puede tocar el hardware ni la memoria de otro proceso. El kernel corre en modo kernel y los programas, en modo usuario: para cualquier cosa del mundo exterior le hacen una llamada al sistema. También crea los procesos, los aísla con namespaces y les pone límites con cgroups. Los drivers son partes del kernel que saben hablar con un dispositivo. Ojo: un kernel de GPU es otra cosa, un programita que corre en la GPU, y el kernel de un notebook de Jupyter es el proceso que corre tus celdas.

Analogía. El personal de una biblioteca de estantería cerrada: los lectores no entran a los estantes; piden el libro en el mostrador, alguien revisa su credencial y se los trae. Se rompe en que el kernel también le quita la silla a un lector cuando le toca a otro: reparte el tiempo de la CPU sin que nadie se lo pida.

Ejemplo. El contenedor de Triton de C01 usa el kernel de su nodo, el mismo que todos los contenedores de esa máquina. Por eso el driver de la T4 (el módulo nvidia.ko) se instala en el nodo y no viaja en la imagen.

Ver en el glosario →

Llamada al sistemasystem call · syscallLa forma en que un programa le pide algo al kernel, el núcleo del sistema operativo: abrir un archivo, leerlo, mandar algo por la red, crear un proceso, pedir memoria. Es la única puerta entre un programa y el hardware.Más

Al hacer una, la CPU pasa a modo kernel; el kernel revisa que se pueda (que exista el archivo, que haya permisos), hace el trabajo y regresa el resultado o un número de error (errno): 2 es «No such file or directory»; 13, «Permission denied»; 8, «Exec format error». Linux tiene más de 300, como openat, read, write, mmap, clone, execve y kill. Casi nadie las hace a mano: las hace la biblioteca de C por dentro de funciones como open(). Cada una cuesta poco, de décimas de microsegundo a unos microsegundos, pero millones suman: por eso las bibliotecas juntan las escrituras en un buffer. Que Linux no cambie sus syscalls es lo que deja correr un contenedor con Ubuntu sobre el kernel de otra distribución.

Ejemplo. Un print('hola') de Python termina en write(1, 'hola\n', 5): escribe 5 bytes en el archivo número 1, la salida del programa. Escribir 1,000 líneas sin buffer son 1,000 syscalls; con un buffer de 8 KiB, son 2.

Ver en el glosario →

LinuxLinuxEl núcleo (kernel) de sistema operativo, libre y de código abierto, que corre en la mayoría de los servidores del mundo, en los teléfonos Android y en equipos chicos de NVIDIA como los Jetson. Lo empezó Linus Torvalds en 1991.Más

En sentido estricto, Linux es solo el kernel. Lo que se instala es una distribución: el kernel más el espacio de usuario (la biblioteca de C, la shell, los comandos y un gestor de paquetes). Ubuntu, Debian, Alpine, Amazon Linux 2023, Bottlerocket y Jetson Linux son distribuciones. Los contenedores son una función del kernel Linux: en una Mac o en Windows, Docker corre una máquina virtual con Linux, escondida. Linux tiene una regla: un cambio del kernel no puede romper a los programas que ya funcionan. Gracias a ella, una imagen de Ubuntu 22.04 corre en un nodo con Amazon Linux.

Ejemplo. La imagen de Triton de C01 trae Ubuntu 22.04 con Python 3.10; la del gateway, Debian con Python 3.13. Ninguna trae kernel: Triton usa el de Amazon Linux 2023 de su nodo con GPU, y el gateway, el de su máquina sin GPU.

Ver en el glosario →

Sistema de archivosfile system · filesystemLa forma en que el sistema operativo organiza los bytes de un disco en archivos y carpetas, con nombre, tamaño, dueño y permisos. En Linux todo cuelga de un solo árbol que empieza en la raíz, /.Más

En el disco solo hay bloques de bytes; el sistema de archivos lleva el índice de qué bloques son de qué archivo. Los de Linux más comunes son ext4 y XFS; los de Mac y Windows, APFS y NTFS. Otros discos o memorias se agregan al árbol con un montaje, y hay sistemas de archivos que no están en ningún disco: /proc y /sys los inventa el kernel para mostrar procesos y límites, y /dev/shm vive en la RAM. En Linux casi todo es un archivo: la GPU es /dev/nvidia0. Y los nombres distinguen mayúsculas: Model.plan y model.plan son dos archivos, aunque en una Mac sean el mismo.

Ejemplo. El árbol del contenedor de Triton en C01: el engine está en /models/classifier/1/model.plan, la configuración del sistema en /etc y la GPU aparece como /dev/nvidia0.

Ver en el glosario →

Rutapath · file pathLa dirección de un archivo en el árbol, con las carpetas separadas por /. Es absoluta si empieza en la raíz, como /models/classifier/1/model.plan, y relativa si se lee desde la carpeta de trabajo del programa (casi siempre, la carpeta donde arrancó), como models/model.plan.Más

Una ruta relativa depende de la carpeta de trabajo del proceso (working directory, la que muestra pwd y fija el WORKDIR de un Dockerfile): el mismo open('models/model.plan') funciona si arrancas en la carpeta correcta y falla con «No such file or directory» si no. . es la carpeta actual y .. la de arriba. ~ es la carpeta personal, pero la traduce la shell, no el kernel. Windows separa con \ en vez de /; en Python, pathlib arma rutas sin preocuparse por eso.

Ejemplo. Parado en /opt/tritonserver, la ruta relativa ../../models/classifier/1/model.plan sube dos carpetas, hasta /, y baja al engine: llega a la misma /models/classifier/1/model.plan.

Ver en el glosario →

Permisosfile permissions · rwx · chmodLo que cada usuario puede hacer con un archivo: leerlo (r), escribirlo (w) o ejecutarlo (x), en tres juegos: para el dueño, para su grupo y para los demás. Se escriben en letras (rwxr-x---) o en octal (750).Más

Cada proceso corre como un usuario, que el kernel conoce por un número, el UID, y que pertenece a grupos (GID). Cada archivo tiene un dueño, un grupo y 9 bits de permiso. Cada juego de 3 bits es un número del 0 al 7 (r = 4, w = 2, x = 1): por eso rwxr-x--- es 750. En una carpeta, r deja ver los nombres, w crear y borrar adentro, y x entrar: para leer un archivo hace falta x en todas las carpetas de su ruta. Se cambian con chmod (los bits) y chown (el dueño). Cuando no alcanzan, el kernel responde «Permission denied» (errno 13). Root se los salta.

Ejemplo. 644 (rw-r--r--) para el código, 755 (rwxr-xr-x) para un programa o una carpeta, 600 (rw-------) para una llave privada. En C02, la configuración con la contraseña de las cámaras se guarda con 0600. Nunca 777: cualquiera podría cambiarlo.

Ver en el glosario →

Rootroot · superuser · UID 0El superusuario de Linux, el usuario número 0: con él, el kernel se salta casi todas las revisiones de permisos. Puede leer y cambiar cualquier archivo, matar cualquier proceso, montar discos y cargar drivers.Más

Para usar sus poderes en un solo comando se escribe sudo antes. Lo peligroso es dejar un servicio corriendo como root: si alguien explota un error del programa, obtiene exactamente sus permisos. En un contenedor, el proceso corre como root si la imagen no dice otra cosa, y ese UID 0 es el mismo root del nodo, solo que encerrado y con menos poderes: si escapa, es root de la máquina. Por eso la regla es el mínimo privilegio: USER con un UID numérico en la imagen y runAsNonRoot en Kubernetes. Sus poderes están partidos en unas 40 capabilities que se pueden quitar.

Ejemplo. El gateway de C01 corre como el UID 10001 (USER 10001 y runAsUser: 10001). Es el que decodifica fotos que manda cualquiera: si una foto maliciosa explotara un error de Pillow, el atacante no sería root.

Ver en el glosario →

Montajemount · volumePegar un sistema de archivos (un disco, una carpeta de red, un pedazo de memoria) en una carpeta del árbol: desde ahí, lo que hay en esa carpeta vive en el otro sistema. Puede ser de lectura y escritura o de solo lectura.Más

Así entra todo al árbol que empieza en /: un disco nuevo se monta en /data, un disco de red como el EFS de AWS llega por la red, /dev/shm vive en la RAM y /proc lo inventa el kernel. En un contenedor, la raíz es la imagen (capas de solo lectura y una capa escribible encima, que se borra con el contenedor), y lo demás llega montado: en Docker con -v /en/el/nodo:/models:ro, en Kubernetes con volumes y volumeMounts. Montado en solo lectura, cualquier escritura falla con «Read-only file system» (errno 30): los pesos no se pueden corromper y muchos pods comparten una copia.

Ejemplo. C10 copia sus 70.6 GB de pesos de S3 al disco NVMe del nodo, en un volumen del pod, y monta /dev/shm en memoria con 16 GiB porque los 64 MiB que trae un contenedor no alcanzan.

Ver en el glosario →

Arquitectura de CPUCPU architecture · instruction set · x86-64 · ARM64El idioma de instrucciones que entiende una CPU. Las dos que vas a ver son x86-64 (también amd64: Intel y AMD, la mayoría de los servidores) y ARM64 (también aarch64: las Mac con Apple Silicon, los Graviton de AWS, los Jetson y los teléfonos). Un programa compilado para una no corre en la otra.Más

Un binario ejecutable guarda instrucciones en el idioma de una CPU: la instrucción «regresar» es el byte C3 en x86-64 y los 4 bytes C0 03 5F D6 en ARM64. El kernel revisa la cabecera antes de ejecutar y, si es de otra arquitectura, falla con «exec format error» (errno 8). El código de Python no depende de la CPU, pero el intérprete y las librerías con partes compiladas (torch, NumPy, Pillow) sí: pip baja una versión para cada una. Las imágenes de Docker tienen plataforma (linux/amd64, linux/arm64); una imagen multi-arquitectura es un índice con una por plataforma bajo el mismo tag.

Ejemplo. Si construyes la imagen del gateway en una Mac con Apple Silicon, sale linux/arm64; en los nodos de C01, que son x86-64, el contenedor muere con «exec format error». Se arregla construyendo en CI con --platform linux/amd64.

Ver en el glosario →

Contenedor contra máquina virtualcontainer vs. virtual machine · VM · hypervisorUn contenedor es un programa corriendo (un proceso) al que el kernel, el núcleo del sistema operativo, le enseña solo su pedazo del sistema (namespaces) y le pone límites (cgroups): todos los de una máquina comparten un kernel. Una máquina virtual simula una computadora completa, con su propio kernel, encima de un hipervisor, el programa que parte una computadora física en varias.Más

Los namespaces deciden qué ve el proceso: su propia lista de procesos (adentro es el PID 1), su red, su árbol de archivos, su nombre de máquina. Los cgroups deciden cuánto usa. Como el kernel es compartido, la imagen no trae kernel ni driver, arranca en menos de un segundo, más lo que tarde el proceso, y casi no gasta memoria de más; pero un error del kernel afecta a todos los contenedores del nodo. Una VM paga su propio kernel y su sistema completo (más memoria y un arranque de segundos a minutos) a cambio de aislar más: un atacante tendría que romper también el hipervisor. En la nube se usan las dos: una instancia de EC2 es una VM, y adentro corren los contenedores.

Analogía. Departamentos en un mismo edificio contra casas separadas: los departamentos comparten cimientos, tubería y luz (el kernel); cada casa tiene los suyos. Se rompe en que un contenedor ni siquiera tiene paredes propias: es un proceso al que el kernel le oculta a los vecinos.

Ejemplo. En C01, una g4dn.xlarge (una VM sobre el hipervisor Nitro de AWS) corre el contenedor de Triton, con Ubuntu 22.04, sobre el kernel de Amazon Linux 2023 de esa VM, junto al device plugin de NVIDIA.

Ver en el glosario →

cgroupcontrol group · cgroupUna función de Linux que junta procesos (programas corriendo) en un grupo y le pone límites: cuánta memoria, cuánta CPU, cuántos procesos. Es lo que hace cumplir los limits de Kubernetes y el --memory de Docker.Más

Se ve como una carpeta en /sys/fs/cgroup, con archivos como memory.max (bytes) y cpu.max (cuota y ventana). La memoria es un tope duro: si el grupo se pasa, el OOM killer del kernel mata con SIGKILL a un proceso del grupo (en Kubernetes, a todo el contenedor), y sale con código 137 (oom). La CPU es una cuota: con un límite de 1 CPU, el grupo puede usar 100 ms de CPU en cada ventana de 100 ms, sumando todos sus hilos; si se la acaba, se congela hasta la siguiente ventana (throttling): no muere, se vuelve lento a ráfagas. Ojo: os.cpu_count() de Python cuenta los núcleos del nodo, no la cuota del cgroup.

Ejemplo. El gateway de C01 tiene un límite de memoria de 1 GiB y ninguno de CPU. Con 4 fotos de 40 ms de CPU a la vez y un límite de 1 CPU, cada foto tardaría 115 ms en vez de 40.

Ver en el glosario →

14

Chequeo

Esencial

Check

Intenta responder en tu cabeza antes de abrir cada respuesta.

  1. 1¿Qué diferencia hay entre el kernel y el sistema operativo?Ver respuesta

    El kernel es el núcleo: el único programa que toca el hardware, reparte la CPU y la memoria y revisa los permisos. El sistema operativo es el kernel más el espacio de usuario: la biblioteca de C, la shell y los comandos. Una imagen trae solo el espacio de usuario; el kernel lo pone el nodo.

  2. 2¿Qué es una syscall y por qué existe?Ver respuesta

    La forma en que un programa le pide algo al kernel: abrir un archivo, usar la red, crear un proceso. Existe porque la CPU no deja que un programa en modo usuario toque el hardware ni la memoria de otros: lo pide, y el kernel revisa antes.

  3. 3Parado en /opt/tritonserver, ¿qué diferencia hay entre ../../models y /models?Ver respuesta

    Las dos llevan a /models. La primera es relativa: sube dos veces desde la carpeta de trabajo, así que depende de dónde estás. La segunda es absoluta y significa lo mismo desde cualquier lugar.

  4. 4¿Por qué no correr un servicio como root dentro de un contenedor?Ver respuesta

    Porque quien explote un error del servicio obtiene sus permisos, y ese UID 0 es el mismo root del nodo: queda a un paso de la máquina. Se usa un USER numérico y runAsNonRoot.

  5. 5¿Qué es de verdad un contenedor y en qué se diferencia de una VM?Ver respuesta

    Un proceso normal del nodo con namespaces (qué ve), cgroups (cuánto usa) y su propio árbol montado desde la imagen; todos comparten el kernel del nodo. Una VM simula una computadora completa sobre un hipervisor y arranca su propio kernel: aísla más, pero pesa y tarda más.

  6. 6¿Qué significa «exec format error» en un contenedor que no arranca?Ver respuesta

    Que el kernel leyó la cabecera del programa y no es para su CPU: casi siempre, una imagen arm64 hecha en una Mac en un nodo x86-64. También pasa con un script sin la línea #!.

Sigue · 20Hacer varias cosas a la vez: async, event loop y GILLa computadora por dentro Léelo en la RondaM05 · ContenedoresLo que acabas de leer, con el ojo del entrevistador. ← 18 · CPU y GPU