Escalar más allá de un host¶
El camino que se entrega es un host con Docker Compose. Esta página es para el paso siguiente: un Postgres gestionado, varios hosts de API, workers propios o varios despliegues uno al lado de otro. Dice qué necesita cada parte cuando se mueve y qué no hace todavía la plataforma, para que un plan se construya sobre lo que funciona.
Primero, medir. Un host del tamaño recomendado soporta un equipo de unas decenas de personas, y la mayor parte de la lentitud en un host pequeño es CPU steal o un pool demasiado pequeño, no una réplica que falta. La batería de carga ejecuta la misma carga contra cualquier despliegue.
Qué guarda estado y qué no¶
| Parte | Qué guarda | Cómo crece |
|---|---|---|
API (app) |
Nada propio. Las sesiones, la presencia, los turnos de chat en curso y el feed de actualizaciones en vivo están en Postgres y Redis | Más workers de uvicorn, luego más hosts detrás de un balanceador |
| Worker | Nada. Los jobs, los leases y las programaciones son filas en Postgres | Más procesos, en cualquier host, con los pools repartidos entre ellos |
Consola (frontend) |
El branding que guarda en caché cinco minutos | Más instancias |
| Postgres | Todo lo duradero: organizaciones, agentes, conversaciones, vectores, la cola de jobs | Una instancia mayor, o una gestionada |
| Redis (Valkey) | Una caché y sus canales pub/sub, reconstruidos tras un reinicio | Una instancia que comparten todos los hosts |
| Archivos subidos | Un volumen en el host, o un bucket | Un bucket en cuanto más de un host los lee o los escribe |
| Servicio de sandboxes | Los sandboxes en los que los agentes ejecutan comandos | Su propio host, ver Espacios de trabajo de agentes |
Una base de datos por despliegue¶
Todas las organizaciones de un despliegue comparten una base de datos de
Postgres, y cada consulta está acotada a la organización. Los vectores de cada
base de conocimiento también están en ella, mediante pgvector, igual que la
cola de jobs. POSTGRES_DB nombra la única base de datos que usa un
despliegue: no hay una base de datos vectorial aparte, ni réplica de lectura,
ni sharding por organización.
Varias bases de datos significan, por tanto, varios despliegues. Es la
respuesta para datos que deben quedarse en una región, para una unidad de
negocio con sus propios administradores y para un tenant cuya carga no debe
alcanzar a los demás. Cada despliegue tiene sus propios nombres de host,
SECRET_KEY y VAULT_MASTER_KEY, y en tiempo de ejecución no comparten nada.
Pueden compartir infraestructura. Dos despliegues en un mismo servidor de
Postgres necesitan sus propias bases de datos (POSTGRES_DB), y en un mismo
Redis sus propios números de REDIS_DB, para que los buckets de límites y las
respuestas en caché no se mezclen. Un bucket también es de un solo despliegue.
Un agente pasa de uno a otro como su spec: se exporta como YAML en uno
(GET /agents/{id}/spec.yaml) y se importa en un agente del otro.
Un Postgres externo¶
Un Postgres gestionado o propio funciona cuando tiene esto:
- PostgreSQL 16 con pgvector. Los servicios gestionados lo ofrecen como la
extensión
vector. La plataforma la crea la primera vez que se escribe en una colección, así que el rol que se conecta necesitaCREATEsobre la base de datos, o un administrador la crea antes. Lo mismo vale parapg_stat_statements, que una migración añade donde puede y omite con un aviso donde no. - Una conexión cifrada a una base de datos en otro host. Ver
Conexiones cifradas: el override
tiene que llegar a
app,migrateyworker, porque los archivos de compose fijan el host en cada uno. - Las migraciones se ejecutan una vez. El servicio
migratelas aplica antes de que arranque la API. Con varios hosts, se ejecuta desde un despliegue y no desde cada uno.
Tras una conmutación por error del Postgres gestionado, la primera petición de cada proceso que se encuentra con una conexión caída falla, y el pool descarta entonces todas las conexiones más antiguas que ella. Los motores no comprueban una conexión antes de usarla, porque eso costaría un viaje de ida y vuelta en cada entrega del pool.
Contar las conexiones¶
Primero, dimensionar max_connections, o Postgres rechaza conexiones con "too
many clients" antes de que un pool pueda encolar o descartar la carga.
Cada proceso de la API abre dos pools de DB_POOL_SIZE + DB_MAX_OVERFLOW, 15
cada uno por defecto: uno para las peticiones y otro para las búsquedas
vectoriales, que se ejecutan dentro de una petición que ya tiene una conexión.
Un proceso worker abre los mismos dos, más una conexión de corta duración por
cada job que ejecuta. Cada proceso hijo de ingesta es un proceso propio, y el
job que ejecuta escribe sus vectores con un motor propio con el pool por
defecto de 15, que se cierra al terminar el job. Las búsquedas de conocimiento
de un agente se conectan una vez por búsqueda, sin pool.
max_connections ≥ hosts de API × UVICORN_WORKERS × 30
+ procesos worker × 30
+ hijos de ingesta × 15
+ margen para migraciones y la sesión de un administrador
Dos hosts de API con cuatro workers de uvicorn cada uno y dos workers con un hijo de ingesta cada uno suman 330, así que 400 es un valor sensato. Son límites superiores: un despliegue en reposo mantiene muchas menos. El archivo de compose de producción fija 200 para un host.
PgBouncer¶
Solo en modo sesión. El pooling por transacción rompe dos cosas de las que
depende la plataforma: los prepared statements de asyncpg, que ningún ajuste de
aquí desactiva, y el advisory lock a nivel de sesión que impide que una fuente
de sincronización se ejecute dos veces a la vez. El modo sesión limita cuántas
conexiones de servidor existen, pero ahorra poco más, así que dimensionar los
pools frente a max_connections suele ser la mejor palanca.
Más hosts de API¶
Delante va un balanceador que deje pasar WebSockets: las actualizaciones en vivo
y el chat de la consola usan /api/v1/ws/*. No hacen falta sesiones fijas.
Quien vuelve a conectar su socket a otro host sigue un turno en curso a través
de Redis, y la presencia y los límites también se guardan allí.
Cada host necesita:
- Los mismos secretos.
SECRET_KEYfirma tokens que cualquier host debe verificar, yVAULT_MASTER_KEYdescifra cada credencial guardada. - El bucket. Con
FILE_STORAGE_BACKEND=local, un archivo subido a través de un host no está en el otro, y el worker que lo analiza puede no estar en ninguno de los dos. Usas3(ver Archivos subidos). - El mismo Redis, como una instancia o un endpoint delante de una primaria. El cliente no entiende de clústeres, así que Redis Cluster no está soportado.
RATE_LIMIT_TRUST_FORWARDED_FORen cuanto el balanceador sea lo único que llega a los hosts, o todos los visitantes compartirán el bucket del balanceador.
Un turno de chat se ejecuta en el proceso de la API que lo aceptó, no en un
worker, así que los hosts de API soportan la carga del chat y UVICORN_WORKERS
se dimensiona para ella como en un solo host.
Varias instancias de la consola no son un problema. El branding que guardan en caché es de cada instancia: el cambio de un administrador se ve al momento en la instancia que lo guardó y en las demás en menos de cinco minutos.
Más workers¶
Ejecuta tantos procesos worker como necesite el trabajo, en cualquier host.
Reclaman jobs con SKIP LOCKED, así que nunca dos ejecutan el mismo, y
cualquiera de ellos dispara una programación que toca. Un worker que muere
devuelve sus jobs en menos de WORKER_LEASE_SECONDS.
Por qué la cola son filas en Postgres, medida frente a otros nueve motores, se
explica en el benchmark de jobs en segundo plano.
Separa los pools cuando el análisis de documentos compite con las ejecuciones
de agentes. WORKER_POOLS=agent,system en un grupo de procesos y
WORKER_POOLS=ingest en hosts con memoria de sobra evitan que una subida
grande retrase una rutina. La sonda de salud y agenticos cmd doctor avisan de
un pool que ningún worker vivo atiende. Los ajustes de
Trabajo en segundo plano dimensionan cada
proceso.
Redis¶
Todos los hosts de API y los workers usan el mismo Redis: transporta entre ellos el feed de actualizaciones en vivo, la presencia, los turnos en curso y los límites. Es una caché, así que un reinicio no pierde nada duradero. Las actualizaciones en vivo se reconectan, y quien sigue un turno en curso ve la respuesta en la transcripción cuando llega, en lugar de fotograma a fotograma.
Medir antes de añadir hosts¶
agenticos cmd doctordice a qué Postgres y a qué Redis llegó cada conexión, si iba cifrada y si los workers están vivos.pg_stat_statementsmuestra qué sentencias cuestan más a la base de datos.- Logfire, con
LOGFIRE_ENVIRONMENTfijado por despliegue, separa las trazas de un despliegue de las de otro (ver Observabilidad). - La batería de carga, ejecutada antes y después de un cambio, dice si ayudó.
Lo que todavía no hace¶
- Réplicas de lectura, o una base de datos vectorial propia.
- Sharding de organizaciones entre bases de datos dentro de un despliegue.
- PgBouncer en modo transacción, o Redis Cluster.
- Manifiestos de Kubernetes. Las imágenes son las mismas que ejecutan los archivos de compose, y cada servicio lleva su propio health check, así que un clúster las ejecuta con el entorno que describe esta página.