Datos y estado¶
rendimiento guarda el estado en tres lugares, a propósito. Saber cuál es cuál contesta casi todas las preguntas de "¿de dónde sale X?".
flowchart LR
subgraph git[Git: lo que usted quiere]
spec[rendimiento.yaml<br/>en el repositorio de cada aplicación]
defs[addons/*.yaml<br/>en p0dxD/gitops]
end
subgraph k8s[Kubernetes: lo que debe ejecutarse ahora]
app[objetos App<br/>especificación + imágenes publicadas]
addon[objetos Addon]
secrets[Secrets]
end
subgraph pg[Postgres: lo que pasó]
runs[ejecuciones, pasos, registros]
rel[versiones]
apps[aplicaciones]
sess[sesiones]
adds[ajustes y ejecuciones de complementos]
end
spec -- "envío → ejecución → versión" --> rel --> app
defs -- sincronización --> addon
| Pregunta | La respuesta vive en | Por qué |
|---|---|---|
| ¿Cómo se construye y se ejecuta esta aplicación? | rendimiento.yaml (git) |
Se revisa en solicitudes de incorporación, tiene versiones y se despliega al integrarse. |
| ¿Qué imágenes se están ejecutando ahorita? | el objeto App (spec.images, spec.release) |
El controlador lo necesita; kubectl get apps.rendimiento.ai lo muestra; el clúster sigue funcionando si la base de datos se cae. |
| ¿Qué programas del clúster están instalados, y cómo? | addons/*.yaml (git) → objetos Addon |
Las mismas razones. |
| ¿Qué pasó (ejecuciones, registros, versiones, quién inició sesión)? | Postgres | Historial relacional, que casi solo crece y se puede consultar. |
| Contraseñas y tokens | Secrets de Kubernetes | Nunca en git en texto plano, nunca en Postgres. |
Las consecuencias son útiles: perder la base de datos pierde el historial pero no lo que se está ejecutando; borrar un objeto App se recupera con la siguiente versión; git siempre dice lo que se quería.
La base de datos¶
Postgres 17, una sola instancia (rendimiento-db), a la que se llega con pgx (el controlador estándar de alto rendimiento para Go) a través de internal/store. No hay mapeador de objetos: las consultas son SQL simple junto al código que las usa, lo que las mantiene legibles y explícitas.
Esquema¶
erDiagram
apps ||--o{ runs : ""
runs ||--|{ steps : ""
apps ||--o{ releases : ""
runs ||--o| releases : ""
apps ||--o{ app_addons : ""
addons ||--o{ addon_runs : ""
apps ||--o{ probes : ""
apps ||--o{ probe_hourly : ""
apps ||--o{ incidents : ""
apps {
bigint id PK
text name UK
text repo
bigint installation_id
text default_branch
jsonb spec
}
runs {
bigint id PK
bigint app_id FK
text sha
text branch
text event
bool deploy
text status
int attempts
bigint check_run
}
steps {
bigint run_id PK
text step_id PK
text kind
text status
text digest
text log
}
releases {
bigint id PK
bigint app_id FK
bigint number
jsonb images
jsonb spec
bigint rollback_of
}
sessions {
text token_hash PK
text login
timestamptz expires_at
}
addons {
text name PK
bool enabled
jsonb settings
timestamptz last_scheduled
}
addon_runs {
bigint id PK
text addon
text status
jsonb results
text log
}
app_addons {
bigint app_id PK
text addon PK
bool enabled
}
probes {
bigint app_id FK
text service
text kind
timestamptz at
bool ok
int latency_ms
}
probe_hourly {
bigint app_id PK
text service PK
text kind PK
timestamptz hour PK
int total
int ok
int p50_ms
int p95_ms
}
incidents {
bigint id PK
bigint app_id FK
text service
timestamptz started_at
timestamptz ended_at
}
Los nombres de las tablas y las columnas se quedan en inglés porque son los del código.
Migraciones¶
Las migraciones son archivos de SQL simple en internal/store/migrations/, incrustados en el programa y aplicados en el orden de sus nombres al arrancar (Store.Migrate), cada uno una sola vez, anotados en schema_migrations, con la tabla bloqueada para que dos procesos nunca apliquen la misma. Para cambiar el esquema, agregue el siguiente archivo numerado; nunca edite uno que ya se ejecutó.
-- Add-ons are optional platform features (Renovate first). Their settings
-- live here until add-on settings move to a git repo.
CREATE TABLE addons (
name TEXT PRIMARY KEY,
enabled BOOLEAN NOT NULL DEFAULT false,
settings JSONB NOT NULL DEFAULT '{}',
last_scheduled TIMESTAMPTZ,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Per-app switches, e.g. Renovate on for this app's repo.
CREATE TABLE app_addons (
app_id BIGINT NOT NULL REFERENCES apps(id) ON DELETE CASCADE,
addon TEXT NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (app_id, addon)
);
CREATE TABLE addon_runs (
id BIGSERIAL PRIMARY KEY,
addon TEXT NOT NULL,
trigger TEXT NOT NULL,
status TEXT NOT NULL,
message TEXT NOT NULL DEFAULT '',
repos JSONB NOT NULL DEFAULT '[]',
results JSONB NOT NULL DEFAULT '{}',
log TEXT NOT NULL DEFAULT '',
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
finished_at TIMESTAMPTZ
);
CREATE INDEX addon_runs_recent ON addon_runs (addon, id DESC);
La cola de ejecuciones¶
Las ejecuciones de integración continua son una cola en Postgres, sin necesidad de un intermediario de mensajes:
// ClaimRun takes the oldest queued run and marks it running. It returns
// ErrNotFound when the queue is empty. Safe across replicas.
func (s *Store) ClaimRun(ctx context.Context) (*Run, error) {
return scanRun(s.pool.QueryRow(ctx, `
UPDATE runs SET status = 'running', started_at = now()
WHERE id = (SELECT id FROM runs WHERE status = 'queued' ORDER BY id FOR UPDATE SKIP LOCKED LIMIT 1)
RETURNING `+runCols))
}
FOR UPDATE SKIP LOCKED deja que cualquier número de trabajadores tome de la cola al mismo tiempo: cada uno aparta una fila distinta y ninguno espera a otro. Es el patrón estándar para colas de trabajo en Postgres.
Datos de disponibilidad¶
Las comprobaciones de disponibilidad escriben una fila por comprobación en probes (insertadas en bloque con COPY), que se guardan 7 días. Cada 5 minutos, RollupProbes vuelve a calcular las últimas 3 horas de resúmenes por hora en probe_hourly (total, exitosas, tiempo de respuesta p50 y p95), que se guardan 400 días. La gráfica de 24 horas lee las comprobaciones sueltas; las de 7 y 30 días leen los resúmenes. Cada servicio comprobado una vez por minuto son 1,440 filas al día, así que una docena de aplicaciones se queda muy por debajo de un millón de filas sueltas.
Registros¶
Mientras una ejecución está en curso, el registro de cada paso es una columna de texto que crece por pedazos (right(log || chunk, 1 MiB)). Pasando 1 MiB se tira el principio y se conserva el final, donde están los errores. Los registros de las ejecuciones de complementos se guardan igual, recortados por en medio pasando 512 KiB.
El archivo de registros¶
Cuando una ejecución lleva 2 minutos terminada, los registros de sus pasos se mudan al almacenamiento de objetos. El barrendero logarchive.Archive corre cada 5 minutos en el líder:
flowchart LR
pg[(steps.log<br/>Postgres)] -- ejecución terminada --> sweep[barrendero<br/>cada 5 min]
sweep -- gzip --> minio[(almacenamiento S3, Garage<br/>rendimiento-logs/runs/app/run/step.log.gz)]
sweep -- "log = '', log_ref = llave" --> pg
ui[página de la ejecución] -- GET …/log --> api[API]
api -- con log_ref --> minio
minio -. regla de ciclo de vida .-> gone[borrado después de<br/>LOG_RETENTION_DAYS]
- Comprimidos: los registros se comprimen con gzip al nivel más alto; los registros de construcción se encogen unas 10 a 20 veces.
- Seguros: la columna se vacía solo después de que la subida tuvo éxito, y solo si el registro no cambió mientras tanto. Un barrido que falla (almacenamiento caído) se reintenta en el siguiente, y no se pierde nada.
- Transparentes: la interfaz le pide el registro a la API como antes. La API lo lee del almacenamiento de objetos cuando hay
log_ref, y lo dice claramente cuando el registro ya caducó o no se puede llegar al archivo. - Con rotación: al arrancar, la plataforma pone una regla de ciclo de vida en la cubeta, borrar los objetos bajo
runs/después deLOG_RETENTION_DAYS(365). El almacenamiento es quien borra, así que el espacio se mantiene acotado sin ninguna tarea que correr. - Privilegio mínimo: la plataforma usa su propia llave,
rendimiento-logs, que solo puede usar esta cubeta.
La comprobación Archivo de registros de la página Entorno muestra cuántos registros están archivados, su tamaño, cuántos esperan y la retención.
Objetos de Kubernetes¶
Dos recursos personalizados, definidos en Go en api/v1alpha1 y convertidos en CRD por controller-gen (referencia):
App(apps.rendimiento.ai, de alcance de clúster):specson los servicios y las tareas programadas de la aplicación (los mismos tipos de Go querendimiento.yaml),images,release,suspend,adopt,sharedNamespacey los ajustes de las necesidades;statuses la fase, un mensaje y la salud de cada servicio.Addon(addons.rendimiento.ai, de alcance de clúster, nombre cortoradd): el origen, los valores, las compuertas y los interruptores enspec; la fase, la vista previa, el inventario, los ganchos y la huella enstatus.
Los dos son de alcance de clúster porque una aplicación es dueña de su espacio de nombres y un complemento abarca varios espacios de nombres.
Vuelva a generar los CRD cuando cambie los tipos
El servidor de la API tira sin avisar los campos que no están en el esquema del CRD. Después de cambiar api/v1alpha1 o internal/spec, ejecute make generate (los objetivos de pruebas lo hacen por usted) y aplique el CRD nuevo.