Arranque inicial de una instancia
Un despliegue de DeviceChain se construye con dos comandos. dcctl install prepara un
clúster una sola vez. Después, dcctl bootstrap levanta en él una instancia completa de
DeviceChain —su infraestructura y todas las cargas de trabajo de servicio— tantas veces como
instancias quieras:
dcctl install local
dcctl bootstrap local my-instance
dcctl lleva su propio contenido. La configuración de infraestructura de OpenTofu, el chart
de Helm y los manifiestos del operador están todos incrustados en él, así que nunca necesitas
un checkout del árbol de código fuente ni git para desplegar.
Lo que no lleva son sus propias herramientas. install y bootstrap ejecutan kubectl,
helm y tofu (o terraform) como binarios en tu PATH, y además docker y kind cuando
dcctl gestiona el propio clúster (el proveedor local) y docker cuando construye imágenes
(--build). Ambos comandos comprueban que estén las que necesita la ejecución antes de empezar
y se detienen si falta alguna, así que instálalas primero. La lista completa está en
Prerrequisitos.
DeviceChain está en fase previa al lanzamiento. dcctl install local y dcctl bootstrap local
están implementados y validados de extremo a extremo en Kubernetes local (kind).
dcctl install local crea el clúster de kind por ti si no existe ninguno, y te pregunta antes
salvo que pases --yes. El proveedor gcp es una mejora planificada.
Instalar el clúster
dcctl install <provider> prepara un clúster para alojar instancias de DeviceChain. Instala
los requisitos previos que comparten todas las instancias del clúster.
En el namespace dc-k8s-system instala el operador de DeviceChain y las dos definiciones
de recurso personalizado que reconcilia, Instance e InstanceConfiguration. Son lo que hace
que un clúster pueda alojar una instancia: dcctl bootstrap declara un Instance, y no puede
declarar uno en un clúster donde la definición no existe. Consulta
el operador.
En el namespace dc-system instala:
- la base de datos relacional (
dc-rdb), que contiene una base de datos por instancia; - el almacén de objetos al que se archivan los respaldos de base de datos.
Cada uno en un namespace propio, instala:
- el operador CloudNativePG (
cnpg-system); - cert-manager (
cert-manager); - la monitorización (Prometheus y Grafana, en
monitoring—consulta Observabilidad); - el controlador de ingress (
ingress-nginx).
El operador es un solo controlador por clúster, compartido por todas las instancias que haya en él. Por eso instalarlo es tarea del comando del clúster y no de cada instancia, y por eso mover un clúster a una versión nueva empieza aquí: consulta Versiones y actualizaciones.
install también crea la identidad de base de datos base con la que se crea el login de base
de datos propio de cada instancia, y registra la instalación en el clúster.
En qué clúster instala:
local—--cluster <name>(por defectodevicechain) nombra un clúster de kind. Si no existe ninguno con ese nombre,installlo crea, preguntando antes salvo que pases--yes. Si existe, lo reutiliza.- Cualquier proveedor —
--kube-context <ctx>instala en un clúster que ya existe.dcctlnunca crea ni elimina un clúster al que se llega de este modo.
Volver a ejecutar install
Volver a ejecutar install contra el mismo clúster lo hace converger: lo que ya está en su
sitio se deja como está, y lo que falta se añade. Eso incluye el volumen del almacén de objetos de
respaldo, que conserva el tamaño que tiene; consulta
Tamaño del almacén de objetos de respaldo.
Cambiar sus ajustes —--ha, --compact, la monitorización, los respaldos,
--backup-snapshot-class, la ubicación de las bases de datos— se rechaza mientras exista alguna instancia en el clúster. Cada instancia se construyó con los ajustes
vigentes cuando se arrancó, y ninguna se reconstruye cuando cambian. Eso incluye el archivo
externo: un --backup-credentials-file que nombre otro endpoint u otro bucket del almacén de
eventos también se rechaza, porque el almacén de eventos de cada instancia sigue archivando en
el que tenía cuando se construyó.
Hay una excepción: una nueva ejecución puede aumentar --max-connections mientras hay
instancias en marcha (consulta el presupuesto de conexiones). Reducirlo
se rechaza como cualquier otro cambio. Una nueva ejecución que no pase --max-connections
conserva el presupuesto que ya tiene el clúster.
Una ejecución que falló a mitad se repara de la misma forma. Cuando hayas corregido la
causa, vuelve a ejecutar el mismo dcctl install. Si el almacén de objetos de los respaldos no
pudo arrancar la primera vez, por ejemplo porque no se pudo descargar su imagen, la nueva
ejecución lo elimina, lo crea de nuevo y espera a que esté listo, igual que la primera. Si la
causa sigue ahí, la nueva ejecución falla del mismo modo. Antes de dar el clúster por instalado,
install comprueba además que el almacén de objetos de los respaldos ha terminado su despliegue,
así que un almacén que quedó sin estar listo tras un cambio fallido anterior se notifica en
lugar de pasarse por alto.
install también espera, hasta 15 minutos, a que se hayan unido todas las instancias de la base de
datos relacional, y termina con un error que nombra la base de datos y cuántas de sus instancias
están listas si alguna no lo ha hecho. Para entonces la instalación ya está registrada y el bloqueo
del clúster liberado, así que se pueden hacer bootstrap de instancias mientras sigue esperando. Para
seguir la base de datos, obsérvela con kubectl; ejecutar de nuevo el mismo dcctl install también
sigue esperando, pero vuelve a aplicar los requisitos previos y rechaza los bootstraps mientras se
ejecuta.
Dónde guarda install su estado
Los requisitos previos se aplican con OpenTofu, y su estado vive en la máquina que ejecutó
dcctl install, en ~/.devicechain/clusters/<cluster-uid>/infra.
El directorio se indexa por la identidad del clúster —el UID de su namespace kube-system— y
no por su nombre. Un clúster de kind borrado y vuelto a crear lleva el mismo nombre de contexto
sin contener ninguno de los recursos que describe el estado antiguo. El cluster.json que hay
junto a infra/ registra el nombre del clúster y el kube-context por los que tú lo conoces,
de modo que puedes emparejar un directorio con su clúster:
cat ~/.devicechain/clusters/*/cluster.json
kubectl --context <kube-context> get namespace kube-system -o jsonpath='{.metadata.uid}'
Los proveedores de OpenTofu están fijados a versiones exactas, y cada ejecución, incluida
dcctl destroy, lleva el .terraform.lock.hcl del directorio a las versiones que fija este
dcctl. Por eso cada ejecución consulta al registro de proveedores qué versiones existen, así
que el registro, o un espejo de proveedores que hayas configurado, debe ser accesible.
Una nueva ejecución trabaja a partir de ese estado, así que un clúster ya instalado solo
puede reinstalarse desde la máquina que tiene su directorio. Ejecuta dcctl install contra
él desde otra máquina y se rechaza antes de aplicar nada:
cluster ... is already installed (by dcctl <version>, <time>), but this machine holds no state
for it under ~/.devicechain/clusters/<cluster-uid>. It was installed from another machine, and
re-applying from empty state would try to create every prerequisite again. Run `dcctl install`
from the machine that installed it
Aplicar desde un estado vacío planificaría todos los requisitos previos como nuevos y fallaría
a medias con nombres ya en uso, así que la negativa es el resultado seguro. Pero convierte ese
directorio en precondición de toda nueva ejecución descrita en esta página, incluida la que
aumenta --max-connections.
El directorio es la única copia del estado de los requisitos previos del clúster, y ningún
respaldo de DeviceChain lo contiene. Consérvalo en la máquina desde la que instalas. Si otra
máquina va a tomar el relevo, copia antes a ella el directorio
~/.devicechain/clusters/<cluster-uid>/ completo. Contiene el estado de la raíz del clúster,
credenciales incluidas, así que trátalo como tratas el directorio del depósito (escrow).
TLS, flags e instalación en un portátil
--no-tls en dcctl install solo tiene sentido junto con --compact, donde descarta
cert-manager. Sin --compact se rechaza. Para servir una instancia por HTTP simple, pasa
--no-tls a dcctl bootstrap.
Los flags se enumeran en Flags de instalación. En un portátil:
dcctl install local --dev
dcctl bootstrap local devicechain --dev
dcctl bootstrap se niega en un clúster donde dcctl install no ha terminado, y la negativa
nombra el comando de instalación que hay que ejecutar. dcctl upgrade se niega en el mismo
caso.
Eliminar un clúster
Todavía no hay ningún comando que desinstale los requisitos previos. dcctl destroy
elimina una instancia y los deja en su sitio. Para eliminar un clúster local que creó
dcctl install, bórralo con kind:
kind delete cluster --name devicechain
docker rm -f kind-registry # el registro de imágenes local, si usaste --build
kind no sabe nada de ~/.devicechain/clusters/, así que borrar el clúster de este modo deja
atrás su directorio. El siguiente dcctl destroy de una instancia que estaba en ese clúster
descubre que el clúster ya no existe y elimina el directorio junto con el estado local propio
de la instancia —consulta Eliminar una instancia. O elimínalo a mano, una vez que
su cluster.json haya confirmado a qué clúster pertenecía.
En un clúster que conservas, como un clúster gestionado o cualquier clúster que no haya creado
dcctl install, los requisitos previos permanecen después de eliminar la última instancia. Quitarlos
a mano no está admitido: incluyen la base de datos relacional compartida y sus copias de seguridad, y el almacén de
objetos de copias del propio clúster si lo usas, junto con las copias de instancias que una
eliminación conservó o no pudo borrar (consulta Qué pasa con los respaldos de la instancia). Conserva el directorio del
clúster en ~/.devicechain/clusters/<cluster-uid>/ mientras sigan ahí, porque todo dcctl install
posterior en el clúster trabaja a partir de él (consulta
Dónde guarda install su estado).
El presupuesto de conexiones
La base de datos relacional tiene un número fijo de conexiones, fijado por --max-connections
(por defecto 600). Cada instancia reserva un límite de conexiones en su login de base de
datos, dimensionado a partir de las áreas que habilita. dcctl bootstrap rechaza una
instancia cuya reserva no cabe en lo que queda.
Esa comprobación se hace antes de escribir nada de la instancia —ni namespace, ni base de datos, ni login—, así que un arranque rechazado no deja nada que limpiar. Un clúster pensado para alojar muchas instancias, o instancias con muchas áreas habilitadas, necesita un presupuesto mayor.
Cada servicio mantiene abiertas entre usos todas las conexiones que ha abierto su pool, hasta el
tamaño del pool (20 si no se fija maxOpenConnections), y cierra cada una una hora después de
abrirla. La reserva permite a cada área un pod con un pool completo del tamaño predeterminado, más
otro pod durante un despliegue, así que con los valores predeterminados todos los pools caben en ella. Un
servicio con replicas por encima de 1, o con un maxOpenConnections mayor, puede mantener más
que eso después de un periodo de mucha carga, así que comprueba esos ajustes frente al límite de
conexiones de la instancia.
El presupuesto es el único ajuste de instalación que puede cambiar con instancias en marcha, y
solo hacia arriba: vuelve a ejecutar dcctl install con un --max-connections mayor. No sale
gratis. Cambiar el límite de conexiones de la base de datos reinicia sus instancias de base de
datos una a una. En un clúster instalado sin --ha solo hay una, así que todas las
instancias del clúster pierden brevemente su base de datos mientras se reinicia. Hazlo en un
momento tranquilo.
dcctl upgrade se admite contra el mismo presupuesto. Una versión puede cambiar lo que
necesitan las áreas relacionales de una instancia, así que la actualización compara la
necesidad de esta versión con el límite que ya tiene el login de la instancia, antes de
escribir nada. Una necesidad que no ha cambiado no se vuelve a admitir. Un aumento que el
presupuesto no puede admitir se rechaza sin haber movido nada:
this release needs instance "my-instance"'s database login to hold <n> connections, up from <m>,
and nothing has been changed: ... Destroy an instance, or raise the budget by re-running
`dcctl install` with a larger --max-connections
El remedio es el de arriba, y reinicia la base de datos. En un clúster cuyo presupuesto está casi agotado, auméntalo en un momento tranquilo antes de la ventana de actualización, en lugar de descubrir la necesidad dentro de ella.
Un aumento que cabe se aplica antes de que los servicios se desplieguen. Si una versión
necesita menos, el login se recorta solo cuando todos los servicios están listos en la nueva
versión. Un recorte que falla no hace fallar la actualización, porque los servicios ya están en
marcha. Se imprime como una advertencia que termina en re-run `dcctl upgrade` to finish it,
y hasta que lo hagas el login retiene más presupuesto del que necesita.
dcctl upgrade --dry-run informa de la comprobación que haría sin iniciar sesión en el
almacén.
Qué hace bootstrap
El arranque inicial se ejecuta como una canalización (pipeline) ordenada que construye una instancia, y te indica qué paso falló si alguno lo hace.
Es un verbo de creación. Todas las credenciales de la instancia se acuñan aquí —las
contraseñas de las bases de datos, la autoridad y los logins del broker, el secreto entre
servicios, la clave raíz del almacén de secretos— porque ninguna de ellas existe todavía.
Apúntalo a una instancia que ya esté en marcha y se detiene en el paso 3, antes de tocar la
infraestructura o el chart. Nombra el comando que sí mueve una instancia viva:
dcctl upgrade, descrito en
Versiones y actualizaciones.
Una ejecución que falló a mitad de camino es un caso distinto, y volver a ejecutarla sigue
siendo la forma de repararla. El paso 3 solo rechaza una instancia viva: una cuyo
documento de configuración (escrito en el paso 8) existe y cuyo primer arranque inicial
ha terminado. Cuando el paso 5 declara la instancia, deja anotado en la declaración que su
primer arranque inicial no ha terminado, y solo una ejecución que acaba con éxito retira esa
anotación. Hasta entonces, un fallo en cualquier paso, incluso dentro de la instalación de
Helm o mientras se espera a que todo esté listo, deja una instancia a medio construir. Vuelve
a ejecutar el mismo dcctl bootstrap, desde la misma máquina, para terminarla.
Una instancia cuyo primer arranque inicial empezó con una versión anterior de dcctl no
lleva esa anotación. Si ese arranque falló después de empezar el paso 8, el paso 3 no puede
distinguirla de una instancia en marcha y la rechaza. La negativa nombra las dos salidas:
dcctl upgrade, que ejecuta sobre la instancia la misma instalación de Helm y la misma espera
a que todo esté listo, o dcctl destroy seguido de un arranque inicial nuevo.
La base de datos relacional pasó de ser un StatefulSet a un clúster de CloudNativePG, y no existe una actualización en sitio. En una instancia creada antes de ese cambio, el arranque inicial se niega y te indica cómo volcar los datos o descartarlos deliberadamente. Consulta la excepción de la base de datos heredada.
La excepción de la base de datos heredada
El directorio de datos de un StatefulSet no puede ser adoptado por el operador, y por eso no existe una actualización en sitio. La negativa es justamente el objetivo: sin ella, la base de datos antigua se eliminaría y una nueva, vacía, ocuparía el mismo nombre de host, dejando una instancia que parece perfectamente sana y no tiene ningún dato.
Esta es la única razón documentada para ejecutar dcctl bootstrap contra una instancia que ya
está viva, así que --allow-legacy-db-removal queda exceptuado tanto de la negativa del paso 3
como de esta. Nada más lo está.
El flag se divide por la misma línea que las bases de datos. La base de datos relacional
pertenece al clúster, así que la cubre dcctl install --allow-legacy-db-removal. El almacén de
eventos pertenece a la instancia, así que lo cubre dcctl bootstrap --allow-legacy-db-removal.
Varias instancias en un mismo clúster
Un clúster puede alojar más de una instancia de DeviceChain. Los servicios de cada instancia,
su broker (NATS), su almacén de eventos (TimescaleDB) y sus credenciales viven en un namespace
propio, que lleva el nombre de la instancia detrás del prefijo dci-: la instancia
devicechain se ejecuta en el namespace dci-devicechain.
El prefijo impide que el namespace de una instancia choque alguna vez con uno de los que usa
el propio clúster. monitoring, cert-manager, ingress-nginx y los demás quedan fuera de
alcance por construcción, así que ningún id de instancia puede ocupar uno de ellos.
Cada instancia se conecta a la base de datos relacional compartida con un login propio que es dueño de exactamente una base de datos, de modo que ninguna instancia puede llegar a los datos de otra.
Lo que comparten las instancias son los requisitos previos del clúster: el operador de
DeviceChain y sus definiciones de recurso personalizado, el controlador de ingress,
cert-manager, el operador CloudNativePG, la monitorización, la base de datos relacional y el
almacén de objetos de los respaldos. dcctl install los instala una vez. Cada
arranque inicial los reutiliza y sigue los ajustes con los que se instaló el clúster: alta
disponibilidad, tamaño compacto, monitorización y respaldos.
Dos cosas de un clúster solo pueden pertenecer a una instancia, y el arranque inicial se ocupa de ambas:
- El host del ingress. Un controlador de ingress al que se le dan dos instancias en un
mismo host solo sirve una de ellas, en silencio. Un arranque inicial cuyo host ya sirve otra
instancia se rechaza. Dale a cada instancia su propio
--host(en un clúster local, por ejemplo--host beta.localhost). - El puerto MQTT local. En un clúster local, el puerto 1883 de tu máquina llega solo al broker de la primera instancia. Los brokers de las instancias posteriores son alcanzables desde dentro del clúster, y el arranque inicial lo indica cuando ocurre.
El namespace de la instancia
El arranque inicial comprueba el namespace de la instancia en el mismo punto. Una instancia es
dueña de dci-<id>: dcctl escribe ahí su clave raíz, el par de claves TLS de su broker y todas
sus credenciales de base de datos, y dcctl destroy elimina el namespace entero. Por eso un
dci-<id> que ya existe y no lleva la etiqueta devicechain.io/instance=<id> se rechaza antes
de escribir nada de eso.
Nada salvo dcctl crea namespaces bajo dci-. La causa probable es un dcctl destroy anterior
de esta misma instancia que no terminó. La negativa lo dice y nombra el comando para
terminarlo, dcctl destroy <provider> <id>. Un namespace que aún se está eliminando también se
rechaza, hasta que desaparece.
Si el namespace lo creaste tú a propósito —para llevar una cuota, una política o un RBAC propios—, entrégaselo a la instancia y vuelve a ejecutar el arranque inicial:
kubectl label namespace dci-<id> devicechain.io/instance=<id>
Nombres de instancia
Los nombres de instancia son letras minúsculas, dígitos y -, de 50 caracteres como máximo.
El nombre es, tal cual, la base de datos de la instancia y el login de esa base de datos.
También es la cola de otros dos nombres: el namespace es dci- más el nombre, y la release de
Helm es dc- más el nombre. El límite de 50 caracteres sale de esa release: Helm limita el
nombre de una release a 53 caracteres, así que al nombre en sí le quedan 50.
Una instancia construida antes de que cada instancia tuviera su propio namespace ejecuta su
broker y su almacén de eventos en el namespace compartido dc-system, y no se pueden mover en
sitio. El arranque inicial rechaza una instancia así e indica que hay que destruirla y volver a
arrancarla.
Los pasos del arranque inicial
Estos son los pasos que la ejecución va imprimiendo ([8/10] Install instance (Helm)), de modo
que un fallo nombra un paso que puedes encontrar aquí:
- Ensure local registry (asegurar el registro local) — solo en la ruta de desarrollo
--build: aprovisiona un registro local y compila todas las imágenes en él. En la ruta de imágenes publicadas no hace nada y lo indica. Va primero porque el chart que se instala más adelante nombra esas imágenes, y en la ruta--buildeste es el paso que las produce. - Claim the cluster (reclamar el clúster) — crea el namespace del operador y toma el
bloqueo del clúster, antes de aplicar nada. Mientras está tomado, un segundo
dcctl bootstrapcontra el mismo clúster se rechaza en lugar de aplicarse silenciosamente por encima de este. Consulta El bloqueo del clúster, que también explica qué hacer cuando resulta que el clúster está reclamado por otra persona. - Refuse a rebuild (rechazar una reconstrucción) — pregunta al clúster si esta instancia ya está viva y se detiene si lo está. Su posición es deliberada por ambos lados. Va después del bloqueo, porque un arranque inicial concurrente es justamente lo que dejaría obsoleta la respuesta entre leerla y actuar sobre ella. Va antes de que se aplique nada de la instancia, porque todos los pasos que vienen debajo escriben en un clúster que puede estar ejecutando ya la instancia sobre la que escribirían. Una ejecución en seco dice qué rechazaría una ejecución real, en lugar de ocultarlo. Reconoce una instancia viva por su documento de configuración, salvo que la declaración de la instancia indique que su primer arranque inicial nunca terminó. En ese caso deja pasar la ejecución para terminar la instancia. Si no puede leer la declaración, se detiene en lugar de suponer.
- Check what other instances hold (comprobar lo que tienen otras instancias) — pregunta
al clúster qué host de ingress y qué puerto MQTT local tienen ya otras instancias, y se
detiene si el host de esta instancia es uno de ellos. También mira el namespace en el que
está a punto de construirse esta instancia,
dci-<id>, y se detiene si existe y no es de esta instancia, o si aún se está eliminando. El paso se ejecuta antes de escribir nada, así que una negativa no deja nada detrás. Un puerto MQTT local que tiene otra instancia no detiene la ejecución, y se indica. Una ejecución en seco dice qué rechazaría una ejecución real. Consulta Varias instancias en un mismo clúster, incluida la etiqueta que entrega a la instancia un namespace que creaste tú. - Declare the instance (declarar la instancia) — escribe la declaración de la
instancia en el clúster: el proveedor y el clúster al que pertenece, el perfil, la versión
de imagen y si sus bases de datos se están recuperando desde un archivo. Acto seguido se
vuelve a leer, y todos los pasos siguientes trabajan con lo que se leyó y no con los flags
que lo produjeron. El registro de lo que es esta instancia está en el clúster, no en tu
portátil. Consulta
la declaración de la instancia.
Cuando la ejecución está construyendo la instancia (el paso 3 no encontró documento de
configuración, o encontró que el primer arranque inicial nunca terminó), la declaración
lleva además la anotación
core.devicechain.io/bootstrap-unfinished: "true". Una ejecución que acaba con éxito la retira, en la misma escritura que registra la instancia comoReady. - Render configuration (renderizar la configuración) — resuelve el id de la instancia, el namespace, el perfil y todas las credenciales generadas: el material de autenticación del broker (la contraseña de servicio compartida y la clave del emisor del callout), la autoridad certificadora que firma el propio certificado TLS del broker, el secreto de autenticación entre servicios y la clave raíz del almacén de secretos. Todas se acuñan aquí, porque el paso 3 ha establecido que no hay ninguna instancia viva de la que tomarlas. Terminar una instancia a medio construir es la excepción: ahí el paso vuelve a leer lo que una ejecución anterior ya dejó en el clúster en lugar de generar un segundo juego, también cuando esa ejecución llegó a escribir el documento de configuración. La autoridad certificadora del broker es lo único que cada ejecución emite de nuevo. El paso también registra las credenciales del broker en la máquina desde la que lo ejecutas, antes de configurar el broker con ellas. El broker se configura antes que la instancia, y sus credenciales ya no se pueden recuperar del clúster una vez están en él, así que esto es lo que te permite retomar una ejecución interrumpida con solo volver a ejecutarla. Además, la clave raíz se deposita en un archivo cifrado que tú conservas; consulta Recuperación ante desastres.
- Apply infrastructure (aplicar la infraestructura) — ejecuta
tofu applysobre la configuración de OpenTofu incrustada vía terraform-exec, solo para esta instancia: su propio broker (NATS) y almacén de eventos (TimescaleDB) en su namespace, con el estado guardado en~/.devicechain/instances/<instance>/infra. El paso también crea el login y la base de datos propios de la instancia en la base de datos relacional compartida. Los requisitos previos compartidos del clúster no se aplican aquí; los dejó en su sitiodcctl install. Las ejecuciones posteriores son incrementales. - Install instance (Helm) (instalar la instancia) — escribe el documento de configuración de la instancia, del que cada servicio lee sus credenciales y sus endpoints. Después despliega el chart de Helm vía el SDK de Helm para Go, bloqueando hasta que las cargas de trabajo estén listas. El paso 3 busca ese documento en cualquier ejecución posterior. La instancia cuenta como viva cuando, además, una ejecución ha terminado con éxito.
- Wait for readiness (esperar a que todo esté listo) — sondea el Deployment de cada área
habilitada hasta que haya terminado de desplegarse sobre la configuración que ha producido
esta ejecución. Es una puerta de confirmación explícita, en lugar de confiar en la espera
del propio paso de Helm. Que haya réplicas disponibles no basta: cuando se están
sustituyendo pods, eso ya es cierto de los que están de salida. Así que el paso espera
además a que se observe la nueva plantilla, a que todas las réplicas se hayan recreado sobre
ella y a que no quede ninguna réplica antigua en ejecución.
dcctl upgradeusa la misma puerta por la misma razón. Después espera, hasta 15 minutos, a que se hayan unido todas las instancias del almacén de eventos de la instancia, porque los servicios están listos solo en el primario. Si alguna no lo ha hecho, el paso falla con un error que nombra la base de datos y cuántas de sus instancias están listas; nada se deshace, y ejecutar de nuevo el mismodcctl bootstraptermina la instancia. - Report access info (informar de los datos de acceso) — imprime el namespace, el correo del superusuario y dónde se guarda su contraseña (y la propia contraseña, una sola vez: en la ejecución que la generó, o en la que termina un arranque inicial que falló antes de mostrarla), y cómo llegar a la instancia.
Ctrl+C detiene una ejecución de forma limpiaUna ejecución interrumpida detiene la herramienta de infraestructura con elegancia —termina lo
que está haciendo y escribe su estado— y devuelve el bloqueo del clúster, así que basta con
volver a ejecutarla. Un segundo Ctrl+C sale de inmediato y renuncia a ambas cosas. Consulta
Interrumpir una ejecución.
Si la ejecución ya había llegado al paso 8, volver a ejecutarla también la termina: el paso 3
deja pasar una nueva ejecución hasta que un arranque inicial de la instancia haya terminado con
éxito. Un segundo Ctrl+C durante el paso 8 puede dejar la release de Helm de la instancia
marcada como todavía en curso. Entonces la nueva ejecución se detiene en el paso 8 con
another operation (install/upgrade/rollback) is in progress, y dcctl no lo resuelve por ti.
Para recuperarte, usa dcctl destroy y vuelve a hacer el arranque inicial.
Dado que los artefactos incrustados son los mismos que distribuye la plataforma, una instancia arrancada de este modo ejercita el despliegue real. No puede desviarse de un despliegue de producción.
El destino de respaldo predeterminado
Los respaldos de base de datos necesitan un destino. De forma predeterminada, ese destino es un
MinIO de una sola réplica en el namespace dc-system, instalado por dcctl install. Así,
una instalación estándar produce instancias cuyo log de escritura anticipada (WAL) se archiva
de verdad, en lugar de instancias que llevan un plugin de respaldo sin ningún sitio donde
escribir.
MinIO se distribuye bajo licencia AGPL-3.0. La edición comunitaria de MinIO se archivó en abril de
2026 y sus propias imágenes ya no se publican, así que DeviceChain ejecuta una compilación de una
bifurcación mantenida (cgr.dev/chainguard/minio), fijada por digest, que tus nodos descargan de
cgr.dev. Una compilación con parches llega a tu clúster solo cuando una versión de DeviceChain
mueve esa fijación. Para evitar tanto la licencia como esa dependencia, apunta los respaldos a un
almacenamiento fuera del clúster.
Ninguna de las dos cosas afecta a la licencia Apache-2.0 de la propia DeviceChain. La imagen se referencia, nunca se compila, modifica ni redistribuye, y la plataforma se comunica con ella mediante la API HTTP de S3. Pero el componente sí se ejecuta en tu clúster, y muchas organizaciones no permiten software AGPL con independencia de cómo se use.
El almacenamiento fuera del clúster es la configuración de producción recomendada de todos
modos, por un motivo que nada tiene que ver con las licencias: un bucket dentro del clúster
comparte su dominio de fallo, así que no puede constituir recuperación ante desastres. Pasa
--backup-credentials-file a dcctl install para nombrar un almacén de objetos que ya tengas.
Consulta Recuperación ante desastres y el backup_destination de la
configuración de OpenTofu.
Ventanas de recuperación
Cada base de datos conserva su propia ventana de recuperación: el intervalo de tiempo dentro del
cual se puede restaurar a cualquier punto. La base de datos relacional, que guarda inquilinos,
usuarios, dispositivos, reglas, secretos y el último estado conocido de cada dispositivo, conserva
30 días; se ajusta con backup_retention_rdb en la configuración de OpenTofu del clúster. El
almacén de eventos de cada instancia conserva 7 días; se ajusta con backup_retention_tsdb en
un terraform.tfvars junto al estado de OpenTofu de la instancia
(~/.devicechain/instances/<instancia>/infra/instance/), donde lo lee cada aplicación de la
instancia que hace dcctl, también la de una actualización. La base de datos relacional tiene la ventana más larga porque sin
ella no se puede reconstruir una instancia, y los errores de los que se restaura, como una
migración defectuosa o un borrado por error, se descubren a menudo días después. El historial de
eventos es voluminoso, tiene su propio ciclo de vida de los datos, y
su log es lo que llena el almacén de respaldos.
Una ventana es un número entero y una unidad: d para días, w para semanas o m para meses
(no minutos). Cualquier otra forma se rechaza antes de empezar la aplicación. Una ventana vacía
conserva todos los respaldos. Alargar una ventana guarda más en el almacén de respaldos: consulta
más abajo.
Tamaño del almacén de objetos de respaldo
El almacén predeterminado tiene 160 GiB, dimensionado a partir del almacén de eventos predeterminado para que, con una ingesta sostenida, se llene primero el volumen del almacén de eventos. Mientras una instancia está tranquila, el log de escritura anticipada que archiva no cuesta casi nada. Con ingesta sostenida, el log crece al ritmo de las escrituras: medido en Google Kubernetes Engine, hasta unos 1,9 KB por evento ingerido para las dos bases de datos juntas, frente a aproximadamente 1 KB por evento de datos almacenados. (Esta página daba antes aproximadamente 1 KB por evento para el archivo; esa cifra era baja). 160 GiB guardan el archivo de un almacén de eventos de 32 GiB lleno, más un respaldo base completo de cada base de datos, con más de un tercio del almacén todavía libre. Así, las alertas del propio almacén de respaldos no se disparan, y las alertas del almacén de eventos nombran la causa.
Eso se cumple para una instancia cuyo almacén de eventos se llena en aproximadamente un día. No se cumple:
- con varias instancias que ingieren de forma continua. El almacén pertenece al clúster, y
cada instancia añade su propio archivo. Añade unos 160 GiB por cada una de esas instancias, o
envía los respaldos a un almacén de objetos que gestiones tú (
--backup-credentials-file), que es de todos modos la configuración de producción recomendada. - cuando el almacén de eventos tarda más de aproximadamente un día en llenarse. El almacén guarda un respaldo base completo de cada base de datos por cada día de la ventana de recuperación de esa base de datos, más el log archivado desde entonces. Con las ventanas predeterminadas son 32 copias comprimidas de la base de datos relacional y 9 del almacén de eventos, y tienen que caber junto al log: 32 veces el tamaño comprimido de la base de datos relacional más 9 veces el del almacén de eventos tiene que quedar por debajo de 160 GiB. La base de datos relacional ocupa normalmente megabytes, así que en la práctica el almacén de eventos tiene que comprimirse bastante por debajo de unos 17 GiB.
- cuando una ventana de retención (
retentionDays) limita los datos almacenados de una instancia. Su almacén de eventos no se llena nunca, pero sigue enviando aquí su log: una semana del almacén de eventos y 30 días de la base de datos relacional, cuyo log crece con la ingesta porque registra el último estado conocido de cada dispositivo. En la única medición realizada, de una flota pequeña que informa con frecuencia, los respaldos de la base de datos relacional eran aproximadamente un 14 % del contenido del almacén. Con esa proporción, unos 100 eventos por segundo sostenidos llenan el almacén predeterminado antes de contar ningún respaldo base. La proporción no está medida para flotas más grandes o más lentas, donde probablemente es mayor: si el log de la base de datos relacional fuera todo el archivo, la cifra sería de unos 35 eventos por segundo. - cuando amplías el almacén de eventos. Cada GiB que añades al volumen del almacén de eventos necesita unos cinco GiB más aquí.
En esos casos, el aviso son las alertas descritas en
Respaldos que dejan de enviarse:
BackupDestinationAlmostFull al 85 % de ocupación y BackupDestinationFillingFast con un ritmo
pronunciado, y PostgresWALArchivingFailing cuando el archivado ya se ha detenido. Un respaldo
base llega de golpe, así que un almacén que ya pasa del 85 % puede llenarse con el siguiente
respaldo nocturno: actúa con la primera alerta. Cuando el almacén está lleno, el archivado se
detiene para todas las instancias del clúster, y cada base de datos conserva el log sin enviar en
su propio volumen hasta que también se llena y la base de datos se detiene.
Cada base de datos comprime con zstd el log que archiva. La cifra de 1,9 KB, y los 100 y 35 eventos por segundo calculados a partir de ella más arriba, se midieron antes de que esta versión comprimiera el log del almacén de eventos, le diera claves encabezadas por el tiempo y archivara con zstd, cuando el archivo se comprimía con gzip, así que pecan por exceso: en la prueba de rendimiento de la versión, el archivo del almacén de eventos ocupó unos 0,47 KB por evento almacenado. La parte de la base de datos relacional no se volvió a medir. Sobre el mismo log, la salida de zstd fue entre un 2 % y un 16 % menor que la de gzip, así que el cambio no hace crecer el archivo. Un segmento cerrado antes de tiempo en una base de datos tranquila ocupa unos 16 KiB, o unos 32 KiB en una base de datos que sigue archivando con gzip.
El tamaño del almacén se fija cuando dcctl install lo crea por primera vez. Volver a ejecutar
install, también como primer paso de una actualización, conserva el tamaño que tiene el almacén, y
lo mismo hace un tofu apply directo de la configuración de OpenTofu. Para ampliarlo, con una
StorageClass que permita la expansión de volúmenes:
kubectl -n dc-system patch pvc dc-object-store-data \
-p '{"spec":{"resources":{"requests":{"storage":"320Gi"}}}}'
En kind, el volumen no está limitado a su tamaño: usa lo que tenga el disco del host.
Acortar una ventana de recuperación (backup_retention_tsdb para el almacén de eventos de una
instancia, backup_retention_rdb para la base de datos relacional) no resuelve la ingesta: guarda
menos historial pero sigue guardando un día completo de log, y renuncia a alcance de recuperación
para ganar espacio. Las alertas descritas en
Respaldos que dejan de enviarse avisan antes de que se
llene el almacén o un volumen de base de datos.
Respaldos base como instantáneas de volumen
En un clúster cuyo controlador de almacenamiento toma instantáneas de volumen CSI, dcctl install --backup-snapshot-class <class> toma el respaldo base diario de cada base de datos como una
instantánea de volumen de sus discos, en lugar de una copia completa en el almacén de respaldos.
Sin la opción no cambia nada.
- Lo que no cambia. Cada base de datos sigue archivando su log de escritura anticipada en el almacén de respaldos, de forma continua. Un respaldo base completo sigue yendo al almacén de respaldos una vez por semana (el domingo a las 04:00): el almacén solo poda el log antiguo en relación con los respaldos base que guarda, así que sin ninguno conservaría cada segmento hasta llenarse, y toda restauración lee el almacén.
- La clase. Tiene que existir, tener
deletionPolicy: Deletey pertenecer al controlador de almacenamiento que aprovisiona los volúmenes de las bases de datos (normalmente el de la StorageClass predeterminada).dcctl installcomprueba las tres cosas antes de cambiar nada, y lo mismo hace cadadcctl bootstrap, así que una clase borrada después de la instalación se detecta antes de construir una instancia. El clúster necesita un controlador de instantáneas CSI: Google Kubernetes Engine y Azure AKS lo incluyen con sus controladores de disco; en Amazon EKS, instala antes el complemento del controlador de instantáneas. - Retención. CloudNativePG no borra las instantáneas antiguas. Lo hace el operador de
DeviceChain, cada diez minutos: conserva todas las instantáneas dentro de la
ventana de recuperación de la base de datos y la más reciente anterior a
ella, y borra el resto, lo que borra también la copia del proveedor.
DatabaseSnapshotPruningStalledse dispara cuando deja de hacerlo. Cada pasada registra su hora en la programación, en la anotacióndevicechain.io/snapshot-retention-checked-at. - Lo que no hace. Ninguna restauración lee una instantánea. Una restauración
(
--restore-rdb-from,--restore-tsdb-from) lee el almacén de respaldos: el respaldo base semanal más reciente y el log desde entonces, así que puede reproducir hasta una semana de log. Las instantáneas de una instancia se borran con su namespace cuando se destruye, y las de la base de datos relacional cuando se borra el namespacedc-system. Las instantáneas tomadas en tu proveedor de nube sobreviven al clúster, y borrar la copia del proveedor puede terminar después de que el namespace haya desaparecido. Si el clúster se borra antes, se quedan en el proveedor con el contenido de las bases de datos, incluidos los datos que ha eliminado el borrado de un inquilino, hasta que las borres allí; ya nada las poda. Antes de borrar el clúster, comprueba quekubectl get volumesnapshotcontentno muestra ninguna cuyo namespace de instantánea (la columnaVOLUMESNAPSHOTNAMESPACE) seadc-systemo el de una de tus instancias; después, revisa la lista de instantáneas de tu proveedor. La guía de Google Kubernetes Engine da los comandos para GKE. - Lo que hace con el almacén de respaldos. El almacén guarda ahora hasta una semana más de log de cada base de datos: el log hasta el respaldo base semanal más reciente anterior a cada ventana. Guarda menos copias completas, pero donde el log es la mayor parte de lo que guarda, se llena antes. Con las ventanas y el almacén predeterminados, y la proporción relacional medida más arriba, las copias base como instantáneas de volumen llenan el almacén a unos 60 eventos por segundo sostenidos, no a unos 100; y a unos 29 si todo fuera registro de la base de datos relacional. Las alertas de arriba son el aviso, igual que sin instantáneas. No cambia el tamaño predeterminado del almacén: la regla de dimensionamiento de más arriba cuenta un respaldo base completo de cada base de datos y el log de cada evento que guarda un almacén de eventos lleno, sea cual sea la programación de los respaldos base, y un respaldo base completo sigue llegando al almacén al crearse cada base de datos y luego cada semana, así que la regla da los mismos 160 GiB con o sin instantáneas.
- Pertenece al clúster. Todas las instancias lo siguen, y cambiarlo se rechaza mientras haya instancias en el clúster, como los demás ajustes de install.
Las alertas de las instantáneas se describen en Respaldos que dejan de enviarse.
Prerrequisitos
- Un clúster de Kubernetes, versión 1.29 o más reciente, y un kube-context que apunte a él.
El mínimo proviene de los charts de CloudNativePG, que se niegan a instalarse por debajo de
esa versión.
dcctl preflightlo verifica por adelantado, porque de lo contrario el fallo aparece a mitad de un arranque que ya ha escrito tu archivo de depósito (escrow) de la clave raíz. Para el proveedorlocalesto es un clúster de kind, quedcctl install localcrea por ti (--cluster <name>, por defectodevicechain). Pasa--kube-context <name>para usar en su lugar un clúster que ya tengas (kind / minikube / k3d / docker-desktop). - Disco para los volúmenes persistentes, en la StorageClass predeterminada del clúster. En un
clúster que no es local (ni kind, ni minikube, ni k3d, ni docker-desktop, ni rancher-desktop), con
los ajustes de install predeterminados (sin
--compact,--no-cnpg,--no-monitoringni--backup-credentials-file),dcctl install --hareclama 204 GiB para el clúster: tres volúmenes de la base de datos relacional, el almacén de respaldos (160 GiB) y el Prometheus de la pila de monitorización. Cada instancia reclama 144 GiB más, tres volúmenes del almacén de eventos y tres del broker de mensajes: 348 GiB para un clúster con una instancia. Sin--ha, el clúster reclama 188 GiB y cada instancia 48 GiB. Cada instancia adicional que ingiere de forma continua obliga además a ampliar el almacén de respaldos en unos 160 GiB, como explica Tamaño del almacén. En un proveedor de nube, comprueba antes la cuota de disco. Un proyecto nuevo de Google Cloud permite 500 GB de SSD por región, y cuenta cada GiB de volumen como un GB; las dos clases de disco de Google Kubernetes Engine cuentan para ella. Los volúmenes de una instancia quedan por debajo, y una instalación--hapredeterminada con una instancia cabe en el clúster que crea la guía de Google Kubernetes Engine porque sus nodos arrancan desde discos estándar, que cuentan para otra cuota. Unos discos de arranque equilibrados o SSD también contarían para la cuota de SSD. La guía indica la cuota que hay que solicitar para más instancias. En un clúster local la pila de monitorización no guarda ningún volumen, y en kind los tamaños no se aplican. - CPU y memoria para las solicitudes. Sin
--compact, los cinco servicios que procesan cada evento solicitan unas 4 CPU entre todos, unas 5 con--ha, dondeevent-managementse ejecuta en dos pods, y cada servidor NATS (tres con--ha) solicita 500m de CPU y 768Mi de memoria, además de los demás servicios, las bases de datos y los componentes propios del clúster. Un pod que no cabe se queda enPending, y la instalación espera hasta agotar su tiempo límite. Consulta cada cifra en Dimensionamiento de los servicios. En un clúster que no puede dedicarles eso, como un portátil, usa--compact. - OpenTofu 1.9 o posterior (el binario
tofu;terraform1.9 o posterior también funciona) en tuPATH.dcctllo ejecuta para aprovisionar infraestructura. Instálalo desde opentofu.org. Ejecutadcctl preflight localpara comprobar que está y el resto de tu entorno de antemano. La comprobación previa no mira su versión; una anterior falla al cargar la configuración de la infraestructura. kubectlyhelmen tuPATH, ydockerykindpara el proveedorlocal. La comprobación previa falla si falta alguno de los que necesita la ejecución. Docker solo hace falta para un clúster local de kind o para--build: una instalación en un clúster que nombras con--kube-context, a partir de imágenes publicadas, no lo comprueba ni lo usa. Donde hace falta, debería ser un motor de Docker nativo y no Docker Desktop, y su daemon tiene que ser accesible.ko, solo para compilar imágenes desde el código fuente con--build. La comprobación previa avisa, en lugar de fallar, cuando falta.
Origen de las imágenes
Por defecto, el arranque inicial despliega las imágenes publicadas desde
ghcr.io/devicechain-io, sin nada que compilar:
dcctl bootstrap local my-instance
Si trabajas desde un checkout del código fuente, puedes compilar las imágenes desde el código y
desplegar esas en su lugar con --build. Compila cada servicio y el operador con
ko, además de la consola web con docker build, en un registro local, y
despliega por referencia:
# desde un checkout de código fuente; requiere Docker + ko
dcctl bootstrap local my-instance --build
La única diferencia entre ambos caminos es el registro desde el que los pods extraen las imágenes. La canalización, el chart y el operador son idénticos.
Flags de bootstrap
| Flag | Propósito |
|---|---|
--cluster <name> | Proveedor local: el clúster de kind en el que crear la instancia (por defecto devicechain). Debe estar ya instalado; el arranque inicial nunca crea un clúster. |
--kube-context <name> | Apunta en su lugar a un clúster instalado a través de este kube-context. |
--profile <profile> | Perfil de área funcional: default (el sistema estándar, usado cuando se omite), full (todo: añade inferencia de IA, conectores salientes, MCP, ingesta de Sparkplug B, ingesta de LwM2M y gestión de actualizaciones), telemetry o ingest-only. |
--build | Compila las imágenes desde el código fuente en un registro local (ruta para desarrolladores; necesita el árbol de código fuente + Docker + ko). |
--registry / --version | Sobrescribe el registro/etiqueta de imagen (por defecto: ghcr.io/devicechain-io publicado, o localhost:5000 + dev con --build). |
--host <name> | Host de ingress en el que exponer la instancia (por defecto devicechain.local). Usa localhost en un clúster local para llegar a la consola sin editar /etc/hosts. |
--no-tls | Sirve HTTP simple en lugar de un certificado autofirmado. Con --host localhost, un http://localhost/ sin configuración adicional (sin advertencia de certificado). En un clúster instalado sin cert-manager está activado por defecto, y --no-tls=false se rechaza: no hay nada que emita el certificado. |
--object-store <none|filesystem|s3> | Dónde guarda la instancia los logotipos de los tenants y los artefactos de actualización. Por defecto, un pequeño volumen de sistema de archivos en un clúster creado por dcctl y ninguno en uno al que lo apuntaste con --kube-context. Se ajusta con los flags --object-store-*; consulta el almacén de objetos. |
--dry-run | Imprime lo que haría cada paso sin cambiar nada. Una ejecución en seco no toma el bloqueo del clúster; sí informa de si otro operador está reteniendo el clúster. |
--skip-preflight | Omite las comprobaciones de entorno. |
--escrow-passphrase-file <path> | Lee la frase de paso del depósito de la clave raíz desde un archivo en lugar de pedirla. Consulta más abajo. |
--escrow-file <path> | Escribe el artefacto de depósito en otro sitio distinto de ~/.devicechain/escrow/. |
--no-escrow | No deposita la clave raíz. Solo para instancias desechables; implícito con --dev. A una instancia creada así se le puede dar un depósito más tarde —consulta la reconciliación del depósito. |
--restore-root-key <path> | Recuperación ante desastres: siembra la clave raíz de esta instancia desde un artefacto de depósito en lugar de acuñar una. Solo se acepta cuando hay algo que esa clave pueda abrir: una base de datos que el almacén relacional ya contiene para esta instancia (consulta Recuperar una instancia), o esta misma instancia a medio construir por una ejecución anterior que se está terminando. Tras dcctl destroy no se da ninguna de las dos cosas y el flag se rechaza: la siguiente instancia con ese nombre acuña una clave propia, así que arranca sin el flag y aparta antes el artefacto antiguo, porque el arranque inicial no sobrescribe ninguno. dcctl secrets escrow show <path> te dice para qué instancia se escribió un artefacto. |
El depósito de la clave raíz
El arranque inicial escribe una copia cifrada de la clave raíz del almacén de secretos de
la instancia en ~/.devicechain/escrow/<instance>-rootkey.escrow, sellada con una frase de
paso que tú eliges. Te pide esa frase de paso, o la toma de --escrow-passphrase-file o de
DCCTL_ESCROW_PASSPHRASE.
El depósito está activado por defecto. Una ejecución no interactiva sin frase de paso falla en lugar de continuar sin ella:
# automatización
DCCTL_ESCROW_PASSPHRASE="$(pass show devicechain/prod-escrow)" \
dcctl bootstrap local prod --yes
# una instancia desechable
dcctl bootstrap local scratch --dev
La clave raíz cifra todos los secretos que guarda la instancia. Vive solo en el etcd del clúster, y ningún respaldo de DeviceChain contiene etcd. Sin este archivo, un respaldo de base de datos restaurado en un clúster nuevo rehidrata secretos que nada puede descifrar. Lee Recuperación ante desastres antes de necesitarlo.
Las áreas que guardan secretos se niegan a arrancar en lugar de servir credenciales que no pueden abrir. El servicio user-management es una de ellas: sella la clave de firma de tokens, así que nadie puede iniciar sesión. Te enteras de inmediato, y para entonces ya no hay nada que hacer. Recuperación ante desastres explica el procedimiento completo.
A una instancia que no tiene depósito —creada con --no-escrow o con --dev— se le puede dar
uno más tarde sin reconstruirla. dcctl upgrade escribe el artefacto que falta cuando le pasas
una frase de paso, y comprueba uno existente cada vez que se ejecuta. Consulta
la reconciliación del depósito.
El almacén de objetos
Los logotipos de marca de los tenants y los artefactos de actualización viven en un almacén de
objetos, no en la base de datos. Sin uno, declarar un artefacto de actualización se rechaza con
OBJECT_STORE_NOT_CONFIGURED, y una subida de logotipo se responde con 503. El almacén es configuración fija: se elige al hacer bootstrap,
se escribe en la configuración montada de la instancia y se adopta reiniciando los pods. No es un
ajuste en tiempo de ejecución.
| Modo | Qué obtienes |
|---|---|
--object-store filesystem | Un volumen que crea el chart y que monta en los servicios que usan el almacén. Ajusta su tamaño con --object-store-size (por defecto 5Gi), elige la clase con --object-store-storage-class (por defecto, la clase predeterminada del clúster) y usa --object-store-access-mode ReadWriteMany cuando más de un pod deba compartirlo (requiere una clase de almacenamiento que lo admita). |
--object-store s3 | Un bucket en AWS S3 o en un servicio compatible con S3 como MinIO. Indica --object-store-s3-bucket y --object-store-s3-region o --object-store-s3-endpoint (con --object-store-s3-path-style para MinIO). |
--object-store none | Sin almacén. |
Si omites --object-store, un clúster que dcctl creó él mismo (el clúster kind local) recibe un
almacén de sistema de archivos de 5Gi, de modo que los logotipos y los artefactos funcionan desde
el primer momento. Un clúster al que apuntaste dcctl con --kube-context no recibe ninguno hasta que
elijas: si existe un volumen de la clase adecuada, o si prefieres un bucket, lo decides tú.
Los flags que no pueden aplicarse se rechazan en lugar de ignorarse. Un bucket junto con
--object-store filesystem, un tamaño de volumen junto con s3 o cualquier detalle junto con
none falla antes de crear nada.
Credenciales de S3
Una clave de acceso nunca es un flag, nunca es un campo del registro de la instancia y nunca se escribe en la configuración de la instancia. En su lugar, apunta dcctl a un Secret de Kubernetes que ya exista:
kubectl -n storage create secret generic minio-keys \
--from-literal=AWS_ACCESS_KEY_ID=... --from-literal=AWS_SECRET_ACCESS_KEY=...
dcctl bootstrap local prod --object-store s3 \
--object-store-s3-bucket dc-objects --object-store-s3-endpoint https://minio.example.com:9000 \
--object-store-s3-path-style --object-store-s3-credentials-secret storage/minio-keys
El valor es <namespace>/<nombre>. Antes de crear nada, dcctl comprueba que el Secret existe y
contiene ambas claves; después copia esas dos claves (y nada más) a un Secret propio en el namespace
de la instancia, que los servicios leen mediante las variables de entorno habituales del SDK de AWS.
Se elimina un único salto de línea final de cada valor (una clave escrita con kubectl create secret --from-file lo lleva); cualquier otro espacio en blanco o carácter de control se rechaza. Ningún
rechazo imprime un valor.
Para rotar una clave, actualiza tu Secret y ejecuta dcctl upgrade. El upgrade copia los valores
nuevos y estampa un resumen (digest) de ellos en los pods que los leen, de modo que los pods se
reinician con la clave nueva aunque la versión de la release no haya cambiado.
Para ello dcctl necesita, con las credenciales de tu kubeconfig, permiso get sobre Secrets en el
namespace donde vive tu Secret, y create y update sobre Secrets en el namespace propio de la
instancia (dci-<instancia>).
En un clúster creado por dcctl (el clúster kind local) los pods no tienen ninguna identidad en la nube
a la que recurrir, así que --object-store s3 sin un Secret de credenciales se rechaza. En un
clúster en la nube puedes omitir el flag y dar a los pods una identidad: un perfil de instancia, o una
anotación de IAM para service accounts mediante serviceAccount.annotations del chart. dcctl no
establece esa anotación por ti.
Conservar el almacén
La elección queda registrada en la declaración de la instancia, de modo que dcctl upgrade y un
segundo operador reconstruyen exactamente el almacén con el que se creó la instancia. La declaración
contiene el nombre del Secret, nunca su contenido. Una nueva ejecución de dcctl bootstrap sobre una
instancia ya existente (terminar un bootstrap interrumpido, reintentar una restauración) que no indique
ningún flag --object-store* conserva el almacén declarado en lugar de aplicar encima los valores por
defecto.
Se rechazan, tanto por dcctl como por el propio recurso Instance, los cambios que dejarían huérfano o perderían lo que contiene el almacén: eliminarlo, pasar de sistema de archivos a S3 o a la inversa, reducir un volumen, cambiar su clase de almacenamiento o modo de acceso, y cambiar un bucket, un endpoint o una región. Se permite ampliar un volumen, cambiar el Secret que guarda las credenciales y cambiar el estilo de ruta.
Una instancia creada sin almacén no lo adquiere con dcctl upgrade, que no tiene flags de almacén.
Para dárselo, destrúyela y haz bootstrap de nuevo con el flag.
En un clúster creado por dcctl, los pods del almacén se programan juntos en un único nodo, porque un
volumen ReadWriteOnce se acopla a un solo nodo. En cualquier otro clúster, dcctl rechaza antes de crear
nada un conjunto de áreas que montaría un almacén ReadWriteOnce desde más de un pod; usa
--object-store-access-mode ReadWriteMany con una clase de almacenamiento que lo admita, o S3.
El chart marca el volumen del almacén con helm.sh/resource-policy: keep, de modo que un cambio
posterior de los valores no puede hacer que Helm borre los logotipos y artefactos almacenados.
dcctl destroy elimina el namespace de la instancia y con él el volumen. Para borrar un volumen
deliberadamente conservando la instancia, ejecuta
kubectl -n dci-<instancia> delete pvc dci-<instancia>-blob.
Flags de instalación
Estos son flags de dcctl install. Describen el clúster, y toda instancia
arrancada en él los sigue. Ninguno es un flag de dcctl bootstrap.
| Flag | Propósito |
|---|---|
--cluster <name> | Proveedor local: el clúster de kind en el que instalar (por defecto devicechain), que se crea si no existe. |
--kube-context <name> | Instala en el clúster existente al que apunta este kube-context. dcctl nunca lo crea ni lo elimina. |
--compact | Preajuste de huella pequeña; consulta más abajo. |
--ha | Alta disponibilidad; consulta más abajo. Requiere al menos 3 nodos planificables. |
--no-tls | Con --compact: no instala cert-manager y, por tanto, tampoco respaldos de base de datos. --compact --no-tls=false conserva ambos. Se rechaza sin --compact: usa dcctl bootstrap --no-tls para servir una instancia por HTTP simple. |
--no-monitoring | Omite la pila de monitorización (Prometheus y Grafana). |
--no-cnpg | Omite el operador CloudNativePG y el plugin de respaldo de base de datos. Para un clúster que ya ejecuta CloudNativePG: Helm no puede adoptar objetos creados por otro instalador, así que sin este flag la instalación falla. |
--backup-credentials-file <path> | Envía los respaldos de base de datos a un almacén de objetos que ya tengas, descrito por un archivo JSON, en lugar del que hay dentro del clúster. Consulta Recuperación ante desastres. |
--backup-snapshot-class <class> | Toma el respaldo base diario de cada base de datos como una instantánea de volumen CSI con esta VolumeSnapshotClass, en lugar de una copia completa en el almacén de respaldos; una copia completa sigue yendo al almacén cada semana, y las restauraciones siguen leyendo el almacén. La clase tiene que existir, usar deletionPolicy: Delete y pertenecer al controlador que aprovisiona los volúmenes de las bases de datos. Se rechaza con --no-cnpg o --compact --no-tls, que no dejan respaldos. Consulta Respaldos base como instantáneas de volumen. |
--database-node-selector <clave>=<valor> | Ejecuta las bases de datos solo en nodos con esta etiqueta (repetible). Se aplica al almacén relacional compartido y al almacén de eventos de cada instancia arrancada en el clúster. Consulta Ubicación de las bases de datos. |
--database-toleration <clave>[=<valor>][:<efecto>] | Permite que las bases de datos se ejecuten en nodos con este taint, escrito como lo escribe kubectl taint (repetible). Necesita --database-node-selector. |
--restore-rdb-from <archive> | Recuperación ante desastres: recupera el almacén relacional compartido desde esta ruta de archivo dentro del bucket de respaldos (dc-rdb para un almacén que nunca se ha restaurado) en lugar de inicializar uno vacío. Solo surte efecto cuando el almacén se crea —contra un clúster cuyo almacén ya existe no mueve ningún dato—, así que es una palanca de reconstrucción, no de reparación. Necesita el plugin de respaldo, así que se rechaza en un clúster instalado con --no-cnpg o con --compact --no-tls. Consulta Recuperar una instancia. |
--restore-rdb-at <timestamp> | Detiene esa recuperación en un instante en lugar de reproducir todo el archivo, para datos destruidos correctamente, por una migración defectuosa o un borrado por error; elige un momento estrictamente anterior al daño. Necesita --restore-rdb-from, y una marca de tiempo RFC 3339 con desfase explícito (2026-07-27T13:59:00Z): sin él, PostgreSQL la interpreta en la zona horaria del propio servidor en recuperación y se detiene en un momento distinto del que nombraste. |
--max-connections <n> | El presupuesto de conexiones de la base de datos relacional (por defecto 600 en una primera instalación; una nueva ejecución sin él conserva el presupuesto actual); consulta el presupuesto de conexiones. Puede aumentarse, pero no reducirse, con instancias en marcha. |
--allow-legacy-db-removal | La mitad relacional de la excepción única descrita en Qué hace bootstrap. |
--dry-run | Imprime lo que haría cada paso sin cambiar nada. Una ejecución en seco no crea ningún clúster, así que las comprobaciones que necesitan leer uno —en particular la de capacidad de nodos de --ha— informan de lo que no pudieron ver en lugar de hacer fallar el ensayo. Lo que sí llegan a ver sigue siendo fatal: un clúster que responde y no puede alojar --ha también hace fallar una ejecución en seco. En un clúster que responde, una ejecución en seco también aplica los rechazos de una reinstalación —una instalación desde una máquina sin el estado del clúster, ajustes cambiados bajo instancias en ejecución, un --backup-snapshot-class inutilizable— y falla con el mismo mensaje que daría la instalación. En un clúster al que no llega, dice que no los comprobó. |
--yes | No pregunta antes de crear un clúster de kind. |
--skip-preflight | Omite las comprobaciones de entorno. |
--dev | Comodidad local para un clúster en un portátil; implica --build --yes. |
--compact
--compact es un preajuste para clústeres pequeños, que se elige al instalar. Compone palancas
que ya existen en lugar de añadir un eje de ajuste propio:
- techos por stream más bajos de JetStream y KV, y los volúmenes más pequeños que eso permite (3Gi JetStream, 2Gi Postgres relacional, 4Gi TimescaleDB, y un almacén de objetos de respaldo de 20Gi cuando se mantiene TLS);
- solicitudes (requests) de programación más bajas (25m / 64Mi) para cada servicio y cada
servidor NATS, para que los pods quepan en un nodo pequeño. Los límites quedan intactos: bajar el límite de memoria convierte la
presión en OOMKills y bajar el límite de CPU produce throttling, y ninguna de las dos cosas
reduce nada.
device-management,event-management,event-sourcesydevice-stateconservan sus límites de CPU más altos, y las solicitudes por servicio que usa una instalación predeterminada se desactivan para que las más bajas se apliquen a todos los servicios; consulta Dimensionamiento de los servicios; - sin la pila de monitorización, el mayor consumidor individual;
- sin cert-manager, ya que con TLS desactivado nada necesita que se emita un certificado (mantener TLS conserva también cert-manager; consulta más abajo), y en consecuencia sin el plugin de respaldo de base de datos.
No cambia qué servicios se ejecutan. Eso sigue en el --profile de cada instancia, donde
queda nombrado y visible. Un perfil más grande que default —hoy solo full— se rechaza en
un clúster compacto. Las cifras compactas publicadas se miden sobre default, así que no
describirían una instancia que ejecuta seis servicios más (inferencia de IA, conectores
salientes, MCP, ingesta de Sparkplug B, ingesta de LwM2M y gestión de actualizaciones). Los perfiles más pequeños
(telemetry, ingest-only) sí se aceptan.
Puedes conservar tanto TLS como la monitorización. Un --no-tls=false o
--no-monitoring=false explícito en dcctl install se respeta, y el resto de las palancas
compactas siguen aplicándose. Mantener TLS también conserva cert-manager, que es lo que emite
el certificado. En un clúster instalado sin cert-manager, toda instancia se sirve sin TLS:
dcctl bootstrap activa --no-tls por defecto y rechaza --no-tls=false.
--compact --no-tls descarta el plugin de respaldoEl plugin Barman Cloud emite sus propios certificados a través de cert-manager, así que
descartar cert-manager descarta también el plugin. Volver a activar TLS (--no-tls=false)
restablece ambos. Hacen falta ambos flags de instalación: dcctl install --no-tls sin
--compact se rechaza, y --no-tls en dcctl bootstrap, como en el ejemplo de URL local más
abajo, solo cambia cómo se sirve esa instancia.
El operador CloudNativePG en sí se instala en todo clúster, incluido el compacto: un Deployment que solicita 100m/128Mi, más sus CRDs. Es un costo de huella que el modo compacto no evita, y es deliberado. El respaldo no es una función de alta disponibilidad, así que la capa de almacenamiento tiene una sola forma en todas partes. Ambas bases de datos se ejecutan ahora sobre el operador: tanto el almacén relacional como el de eventos.
El volumen de JetStream se deriva: los techos por stream se reservan por adelantado, así que
el volumen se dimensiona para contener su suma. Los dos volúmenes de base de datos no. Nada
poda las tablas de comandos o de alarmas, y retentionDays es 0 por defecto, es decir,
conservar los datos para siempre. En una instancia compacta pensada para ejecutarse
indefinidamente, establece una ventana de retención en lugar de confiar en el tamaño del
volumen.
Bajar un techo por debajo de lo que un stream o bucket de KV ya contiene tiene éxito en
silencio, no trunca nada y rechaza escrituras hasta que los datos envejecen y se purgan. Por
eso dcctl install se niega a cambiar sus ajustes mientras exista alguna instancia en el
clúster: --compact se decide una vez, antes de que haya nada ejecutándose bajo él.
dcctl bootstrap local my-instance --build --host localhost --no-tls expone la consola en
http://localhost/, sin entrada en el archivo hosts y sin advertencia de certificado.
--ha
--ha se elige al instalar, y lo sigue toda instancia del clúster. Cada instancia ejecuta su
broker de mensajería como un clúster RAFT de 3 nodos, un servidor por nodo, con cada stream
de JetStream y cada bucket KV replicados a lo largo de él. La instancia sobrevive entonces a
la pérdida de cualquier nodo sin perder mensajes, sesiones de dispositivo ni estado en vivo.
dcctl install local --ha
dcctl bootstrap local my-instance
Ese único ajuste establece ambas mitades, y ese es justamente su propósito. El tamaño del broker es infraestructura (OpenTofu); el factor de réplica por stream es configuración de la instancia (Helm). Viven en herramientas distintas, ninguna de las cuales puede ver a la otra. Elevar solo la primera es el modo de fallo que este flag existe para evitar: un clúster de tres nodos cuyos streams siguen siendo de una sola réplica cuesta el triple de cómputo, informa de tres pares sanos y no sobrevive a nada.
Tres servidores confirman por mayoría, de modo que dos siguen siendo quórum y uno no. Perder un segundo nodo —incluido perder uno por una actualización continua de nodos mientras otro ya está caído— detiene las escrituras hasta que un nodo regrese. Planifica el mantenimiento de un nodo a la vez. Sobrevivir a dos pérdidas simultáneas requiere un clúster de 5 servidores, que hoy no es una topología soportada.
Tres nodos planificables, no tres nodos. Los servidores llevan una restricción dura de
antiafinidad. Si el clúster no puede colocar uno por nodo, el excedente queda en Pending en
lugar de duplicarse, porque réplicas colocadas en el mismo nodo costarían lo que cuesta la
replicación sin proteger de nada. dcctl cuenta los nodos planificables y rechaza la
operación antes de aprovisionar nada. En un clúster kind local esto significa tres workers:
kind solo elimina el taint del plano de control en un clúster de un solo nodo, de modo que un
plano de control más dos workers es un clúster de tres nodos con dos nodos utilizables.
--ha también ejecuta event-management en dos pods. Este servicio almacena cada evento. En
un grupo de tres nodos de servicios, uno de los nodos ejecuta además el servidor NATS que lidera el
stream de eventos entrantes, que usa más CPU que cualquier servicio. En las pruebas, el planificador
puso allí el único pod de event-management junto con device-state. Ese nodo funcionó al 94-95%
de CPU y, con 6800 eventos por segundo ofrecidos y un solo pod, el almacenamiento bajó a 6592 por
segundo durante tres minutos, la primera etapa en quedarse atrás. Un segundo pod permite almacenar
usando la CPU de otro nodo. Los dos pods
prefieren nodos distintos, pero es una preferencia, no una garantía. Cada pod llena sus propios
lotes, así que un lote contiene más o menos la mitad de eventos y el almacén de eventos confirma más
o menos el doble de transacciones por evento; el nodo del almacén de eventos se mantuvo por debajo
del 70% de CPU. Los 6000 eventos por segundo de la última fila de Rendimiento medido se midieron con dos pods. Dos pods solicitan el doble de CPU, 1,8 núcleos entre los dos. Cada pod
tiene además sus propias conexiones al almacén de eventos, así que estas instancias reservan 80 de
sus conexiones para la plataforma en lugar de 40 (consulta
Límite de conexiones). Con --compact --ha, y sin
--ha, event-management se ejecuta en un pod.
Bases de datos con --ha
--ha también ejecuta la base de datos relacional como tres instancias con replicación
síncrona, detrás del mismo nombre de host dc-postgresql que los clientes ya usan. El operador
mantiene ese nombre de host y lo mueve para que siga a la primaria a través de una conmutación
por error, de modo que no cambia la configuración de ningún servicio.
La replicación síncrona es lo que obliga a tres instancias en lugar de dos. Una réplica en espera debe confirmar cada commit, así que con solo dos instancias la pérdida de cualquiera de ellas detiene todas las escrituras: peor disponibilidad que un solo nodo, a cambio de mayor durabilidad. Una tercera instancia permite perder una réplica en espera sin que el clúster se quede sin su réplica confirmadora.
El almacén de eventos también se replica en tres instancias, con una diferencia deliberada: no retiene una escritura a la espera de una réplica. Si no hay ninguna disponible, vuelve a la replicación asíncrona y se pone al día cuando reaparece una. Esa concesión es la correcta para este almacén y la equivocada para el otro. Los eventos ya se conservan de forma duradera aguas arriba en la capa de mensajería hasta que se persisten, así que las escrituras de una conmutación por error pueden reproducirse. El registro de auditoría del almacén relacional no tiene nada así aguas arriba, y por eso ese almacén se detiene. El costo es que el punto de recuperación del almacén de eventos queda acotado por el retraso de replicación en lugar de ser cero.
--ha cambia el número de réplicas de un servicio, el de event-management (dos pods; consulta
más arriba). Todos los demás servicios se quedan en una, y nada de esto sobrevive por sí solo a la
pérdida de un nodo. La replicación es lo que hace posible la recuperación, no lo que la
ejecuta.
Esto se aplica al almacén relacional, que es el que se detiene. Cuando no hay ninguna réplica
en espera disponible, una escritura no falla: espera, y la fila ya se ha confirmado
localmente. Un cliente que se rinda y reintente escribirá dos veces salvo que la operación sea
idempotente. statement_timeout no acota esa espera, porque la espera ocurre después del
commit y no durante la sentencia.
Cuando se detiene la primaria de una base de datos
Una instancia de base de datos se detiene cuando se elimina su pod, cuando se drena su nodo y cuando se despliega un cambio en su configuración. La instancia primero escribe un checkpoint, después deja de aceptar conexiones nuevas y concede cinco segundos a los clientes conectados para que se desconecten. Los servicios de la plataforma mantienen abiertas sus conexiones a la base de datos mientras se ejecutan, así que esperar más solo retrasaría lo que viene después. Pasados esos cinco segundos, la instancia cierra todas las conexiones abiertas y se detiene. Las escrituras que estaban en curso fallan, y los servicios las reintentan.
Con --ha, se promueve una réplica en espera en cuanto la antigua primaria se ha detenido, y
dc-postgresql o dc-timescaledb-single pasa a apuntar a ella. En las pruebas, la nueva
primaria aceptaba escrituras entre 20 y 25 segundos después de que la antigua cerrara sus
conexiones, aproximadamente medio minuto después de eliminar el pod. En una base de datos que
tiene esta configuración, desplegar un cambio de configuración no reinicia la primaria en su
sitio: primero se reinician las réplicas en espera, después el papel de primaria se traspasa a
una réplica al día, y la antigua primaria se reinicia como réplica. En las pruebas, ese
traspaso interrumpió las escrituras durante unos diez segundos. Un almacén de eventos creado
antes de que existiera esta configuración mantiene el reinicio en su sitio hasta que
dcctl upgrade aplica la infraestructura de su instancia
(Qué aplica una actualización a la infraestructura),
o hasta que se le aplica el parche descrito en las
notas de la versión.
Una instalación de una sola instancia no tiene ninguna réplica que promover. Su base de datos no está disponible hasta que la instancia se ha reiniciado, y las escrituras esperan. En las pruebas, con una base de datos pequeña, las escrituras se reanudaron unos 15 segundos después de detenerse; un reinicio con más registro de escritura anticipada que reproducir tarda más.
En ambos casos, la capa de mensajería conserva los eventos hasta que se almacenan. Cada uno se entrega hasta cinco veces, con un minuto entre entregas, antes de abandonarlo y registrarlo como no entregado. Por eso una interrupción de la base de datos de menos de unos cuatro minutos no deja ningún evento sin entregar.
Una instancia que se está deteniendo dispone como máximo de dos minutos en total. Si para
entonces no se ha detenido, se detiene de forma forzosa y se elimina su pod. El motivo más
probable es que siga intentando copiar su último registro de escritura anticipada (WAL) a un
almacén de copias de seguridad inaccesible. Los datos confirmados siguen donde se
confirmaron, pero puede faltar parte del archivo de copias de seguridad: puede que una
restauración a un momento concreto no alcance un instante dentro de ese hueco, y la alerta
PostgresWALArchivingFailing ya está activa. Las restauraciones a momentos posteriores a la
siguiente copia base no se ven afectadas. Una copia base que esté en curso cuando se detiene la
primaria se abandona, y la siguiente copia programada se ejecuta con normalidad.
Los mismos dos minutos cubren también un problema conocido del operador de base de datos. Una
instancia puede apagar PostgreSQL correctamente y después no terminar: su registro acaba con
failed waiting for all runnables to end within grace period of 30s, y su pod se queda en
Terminating aunque la base de datos ya se ha detenido. Los datos no tienen ningún problema, y
el pod se elimina al cumplirse los dos minutos. Un pod que aún no tiene este límite (uno creado
antes de que se introdujera, o cualquier instancia de un almacén de eventos creado antes de que
existiera ese límite y que aún no haya recibido ni una actualización ni el parche que describen
las notas de la versión) sigue
teniendo treinta minutos; las
notas de la versión explican
cómo reconocer ese caso y resolverlo.
Con --ha, una primaria que se está degradando (por un traspaso, o porque está fallando)
también se detiene de forma abrupta si no se ha apagado en dos minutos. En el almacén relacional
eso no pierde nada, porque cada commit se retiene hasta que una réplica lo tiene. El almacén de
eventos no espera a una réplica cuando no hay ninguna disponible, así que un commit hecho
mientras no había ninguna réplica conectada solo existe en su primaria, y se pierde si esa
primaria se sustituye antes de que una réplica se ponga al día. Es la concesión sobre el punto
de recuperación descrita más arriba, y una parada abrupta es una vía más para llegar a ella.
Cómo verificarlo
Una afirmación de alta disponibilidad vale solo lo que el broker realmente sostiene, así que compruébalo ahí y no en la configuración renderizada:
dcctl ha verify --instance my-instance
Esto lee el broker en vivo. Verifica que cada stream, bucket KV y consumidor durable lleva el factor de réplica declarado con todos los pares al día, y que los tres servidores están en tres nodos distintos. Termina con código distinto de cero si algo se queda corto, e imprime qué examinó para que un resultado correcto sobre un conjunto vacío no se confunda con un éxito real.
Perder un nodo y recuperarlo
--ha sobrevive a la pérdida de un nodo. Así se ve desde fuera, en el orden en que ocurre:
- El broker elige nuevos líderes en segundos. Los streams cuyo líder estaba en el nodo
perdido eligen uno nuevo, y en las pruebas las escrituras confirmadas se reanudaron en unos
diez segundos para los servicios cuya propia conexión con el broker iba a un servidor
superviviente. Las publicaciones en curso en ese momento fallan, y un dispositivo que publica
por HTTP puede recibir algunas respuestas
503y debe reintentar. Un503sin cabeceraRetry-Aftersignifica que la publicación falló y puede que el evento se haya almacenado igualmente, así que un reintento lo almacena dos veces salvo que llevealtIdyoccurredTime(consulta calidad de servicio). Las conexiones nuevas a través del servicio del broker pueden seguir fallando de forma intermitente durante unos 45 segundos, hasta que el servidor de ese nodo vuelve o Kubernetes da el nodo por perdido y deja de dirigir tráfico a él. - Un servicio conectado al servidor perdido se reconecta en un minuto, aproximadamente. Una
máquina que se detiene de golpe no cierra nada, así que un servicio cuya propia conexión con el
broker iba al servidor de ese nodo solo se entera cuando ese servidor deja de responder. Cada
servicio envía un ping a su servidor cada 10 segundos y da la conexión por perdida tras tres
intervalos sin respuesta (30 segundos), o en cuanto una escritura lleva 10 segundos sin avanzar;
entre las dos cosas detectan una conexión muerta en 40 segundos como mucho. Después el servicio
se reconecta, lo que lleva unos segundos, o más mientras el servicio del broker siga dirigiendo
algunas conexiones nuevas al servidor perdido (consulta el punto anterior). Hasta entonces no
puede publicar ni recibir. Si es
event-sources, la ingesta HTTP se rechaza o agota su tiempo mientras tanto, así que los dispositivos que publican por HTTP deben reintentar ante respuestas503y tiempos agotados. Una petición que agotó su tiempo también puede haberse almacenado, así que la misma regla dealtIdyoccurredTimese aplica a su reintento. - Puede que Kubernetes no informe de nada. Una máquina que se reinicia dentro del periodo de
gracia del nodo (entre 40 y 50 segundos, aproximadamente) nunca se marca como
NotReady, así que ningún evento de nodo ni alerta de Kubernetes registra la pérdida. Si las alertas del chart están instaladas,BrokerConnectionDiedSilentlysí lo hace; consulta Una conexión con el broker que murió en silencio. - El procesamiento de eventos puede detenerse durante un minuto, aproximadamente. Si el servidor del broker del nodo perdido era el líder del stream de eventos entrantes, los eventos de los dispositivos se siguen aceptando, pero su resolución puede detenerse durante un minuto, sin que se notifique ningún error, antes de reanudarse y procesar lo acumulado. No se pierde nada; las alarmas y los eventos almacenados de ese minuto llegan con retraso.
- Los pods de los servicios se mueven al cabo de un minuto y cuarto, aproximadamente.
Kubernetes tarda primero entre 40 y 50 segundos en dar el nodo por perdido. Después desaloja
cada pod de servicio de ese nodo al cabo de
nodeLossTolerationSeconds(30 por defecto;nullrestablece los 300 del propio Kubernetes) y lo arranca en otro nodo. Con una réplica por servicio, que es lo predeterminado, un servicio cuyo pod estaba en el nodo perdido no está disponible hasta entonces. Con--ha,event-managementse ejecuta en dos pods y, cuando están en nodos distintos (el planificador lo prefiere pero no lo garantiza), el otro sigue almacenando eventos mientras tanto. Las instancias de base de datos y el operador de base de datos usan los mismos 30 segundos. Los servidores del broker no: Kubernetes no los recrea en otro nodo mientras no pueda confirmar que el anterior se ha detenido, así que un plazo más corto no aportaría nada. - Una primaria de base de datos que estaba en el nodo perdido conmuta. Se promueve una
réplica en cuanto el operador de base de datos ve que la primaria es inaccesible, y
dc-postgresqlodc-timescaledb-singlepasa a apuntar a ella. Tarda más que detener una primaria, porque nada avisa al operador de que la primaria ha desaparecido; en las pruebas, con el propio operador en un nodo superviviente, la nueva primaria aceptaba escrituras unos dos minutos después de perder el nodo. Mientras tanto, los eventos esperan en la capa de mensajería. - Los pods desalojados del nodo perdido aparecen en
Terminatinghasta que vuelve. Kubernetes no puede confirmar que se han detenido, así que los deja ahí. No elimines a la fuerza un pod cuyo nodo es inaccesible: en el caso de una instancia de base de datos o de un servidor del broker, eso permite que arranque un sustituto mientras el original puede seguir ejecutándose al otro lado del fallo. Si la máquina no va a volver, confirma que está apagada y elimina después su objeto Node; Kubernetes elimina entonces sus pods. Una instancia de base de datos cuyo nodo vuelve se reincorpora como réplica. - Recuperar el nodo también es una interrupción breve. Un servidor del broker que quedó aislado ha seguido celebrando elecciones por su cuenta y, al reincorporarse, los streams y consumidores de los demás servidores vuelven a elegir a sus líderes. Cuenta con que JetStream responda "temporalmente no disponible" durante unos segundos, unos 45 segundos después de que vuelva el nodo, con que la ingesta HTTP rechace algunas publicaciones en ese intervalo, y con que algunos eventos que ya estaban en curso se procesen hasta un minuto tarde. No se pierde nada, y los servicios se vuelven a enlazar solos.
Planifica la vuelta de un nodo como planificas su pérdida, y no retires un segundo nodo hasta
que dcctl ha verify vuelva a pasar.
Mantenimiento de un nodo de base de datos
Con --ha, cada base de datos ejecuta tres instancias y nunca dos en el mismo nodo. En un clúster
con exactamente tres nodos que las bases de datos pueden usar (tres nodos de base de datos, si los
reservaste para ellas), cada uno de esos nodos ejecuta por tanto una
instancia de cada base de datos. Lo que sigue describe acordonar uno de ellos, hacer el trabajo y
desacordonar el mismo nodo, como en un parche del sistema operativo o un reinicio:
- La primaria que esté en el nodo se conmuta a una réplica en espera. En cuanto el nodo se acordona, el operador de bases de datos empieza a conmutar la primaria a una réplica en espera de otro nodo, y el drenado puede desalojar la primaria antigua mientras eso ocurre. Las escrituras en esa base de datos se detienen durante la conmutación (consulta Cuando se detiene la primaria de una base de datos), y los servicios las reintentan. En las pruebas, una primaria nueva estuvo en funcionamiento unos diez segundos después del acordonamiento.
- Las instancias desalojadas esperan a que vuelva el nodo. Cada uno de los demás nodos ya
ejecuta una instancia de la misma base de datos, así que la instancia desalojada de cada base de
datos queda en
Pendinghasta que el nodo se desacordona. Todas las bases de datos funcionan con dos de sus tres instancias mientras el nodo esté fuera. Es lo esperado, y no hay nada que corregir. - No dejes que ninguna base de datos pierda otra instancia mientras tanto. Si el almacén
relacional pierde su réplica en espera restante, todas las escrituras en él esperan (consulta
Bases de datos con
--ha). Si el almacén de eventos de una instancia pierde su réplica en espera, sigue funcionando sin replicación, y su punto de recuperación queda entonces acotado por el retraso de replicación. Mantén el mantenimiento corto y no retires nada más hasta que el nodo vuelva. - Desacordona el nodo cuando termine el trabajo. Las instancias que esperaban vuelven a arrancar en él y se reincorporan como réplicas en espera. En las pruebas, tras un nodo que había estado fuera menos de un minuto, todas las bases de datos volvieron a tener tres instancias listas en menos de 45 segundos tras el desacordonamiento. Un nodo que se sustituye en lugar de devolverse, como hace la actualización de un grupo de nodos gestionado, no se probó: una instancia en espera solo puede arrancar donde su volumen pueda seguirla.
Si las bases de datos comparten nodos con el resto de la instancia (no configuraste la
ubicación de las bases de datos), drenar un nodo también retira un servidor
del bróker y los pods de servicio que hay en él, y el servidor del bróker también queda en
Pending hasta que se desacordona. Trátalo como perder un nodo, pero planificado:
un nodo cada vez.
Antes de drenar el siguiente nodo, comprueba que cada base de datos ha vuelto a su plena capacidad:
kubectl get clusters.postgresql.cnpg.io -A
READY debe mostrar 3 en todas las bases de datos. Si había un servidor del bróker en el nodo,
ejecuta también dcctl ha verify para la instancia, como tras perder un nodo.
Mientras un nodo que la instancia necesita está acordonado, se rechazan tanto volver a ejecutar
dcctl install como un nuevo dcctl bootstrap, porque un nodo acordonado no cuenta entre los
nodos que comprueban (consulta Ubicación de las bases de datos); termina
antes el mantenimiento.
Sin --ha, cada base de datos tiene una sola instancia y ninguna réplica en espera a la que
conmutar. Nada impide drenar su nodo: la instancia se detiene, y la base de datos no está
disponible hasta que la instancia pueda arrancar de nuevo. Si su volumen está ligado a ese nodo,
como en un clúster kind local, eso no ocurre hasta que el nodo se desacordona.
Dónde se ejecutan las primarias de las bases de datos
Cada base de datos prefiere un nodo que no ejecute la primaria de otra base de datos de DeviceChain. En las pruebas, con tres nodos de 8 vCPU y las primarias relacional y del almacén de eventos en el mismo nodo, ese nodo funcionó al 94-98 % de CPU mientras los otros dos estaban al 45-51 %.
- Es una preferencia, no un requisito. Un clúster con menos nodos sigue planificando todas
las instancias de base de datos. La única excepción es un
ResourceQuotacon el ámbitoCrossNamespacePodAffinity: rechaza los pods cuya ubicación tiene en cuenta otros espacios de nombres, sea preferencia o no, así que rechaza estos pods de base de datos en un espacio de nombres donde lo prohíba. La configuración de admisión de cuotas del servidor de API puede imponer el mismo límite a todo espacio de nombres sin una cuota que lo admita; consulta las notas de versión. - Se aplica cuando se planifica un pod de base de datos. Con
--haen tres nodos, cada nodo ya ejecuta una instancia de cada base de datos, así que en la práctica la preferencia decide una sola cosa: cuando se crea el almacén de eventos de una instancia, su primera primaria va a un nodo que no ejecuta la primaria relacional. Una conmutación por error, un traspaso, o el traspaso con el que termina una actualización progresiva de una base de datos, todavía pueden dejar las dos primarias en un mismo nodo, y la preferencia no las vuelve a separar. - Un almacén de eventos creado antes de que existiera esta preferencia la adquiere cuando se
actualiza su instancia. Hasta entonces sus pods no llevan la etiqueta que buscan las demás
bases de datos ni la preferencia.
dcctl upgradeaplica el almacén de eventos de la instancia (Qué aplica una actualización a la infraestructura) y añade ambas. La etiqueta llega a los pods en ejecución sin reiniciarlos. La preferencia cambia la especificación de los pods, así que las instancias del almacén se reinician una vez por ella: con--ha, primero las réplicas en espera y después un traspaso a una de ellas; sin--ha, la única instancia se reinicia y los eventos esperan en el stream de ingesta hasta que vuelve. Una actualización con--skip-infrastructuredeja el almacén de eventos sin ninguna de las dos hasta que una actualización sin esa opción lo aplique. Adquirir la preferencia no mueve una primaria: con--ha, ese reinicio termina con un traspaso a una réplica en espera que ya está ubicada, así que las primarias pueden seguir compartiendo un nodo después. Compruébalo y, si es así, mueve una como se indica a continuación.
Para ver dónde están las primarias:
kubectl get pods -A -l cnpg.io/instanceRole=primary -o wide
Si dos de ellas comparten nodo, traspasa la primaria de una base de datos a una réplica en espera
de otro nodo. Con el plugin de kubectl de CloudNativePG:
kubectl cnpg promote dc-rdb dc-rdb-2 -n dc-system
o, sin el plugin:
kubectl -n dc-system patch cluster dc-rdb --subresource=status --type=merge \
-p '{"status":{"targetPrimary":"dc-rdb-2"}}'
Elige una réplica en un nodo que, según el primer comando, no tenga ninguna primaria. Un traspaso interrumpe brevemente las escrituras de la base de datos (consulta Cuando se detiene la primaria de una base de datos), y los servicios las reintentan. Una instalación de una sola instancia no tiene ninguna réplica a la que traspasar.
Ubicación de las bases de datos
Por defecto, las bases de datos se ejecutan en los nodos que elija Kubernetes. Si tu clúster tiene
nodos reservados para bases de datos, dcctl install puede colocarlas en ellos:
dcctl install local --kube-context "$CTX" --ha \
--database-node-selector example.com/role=database \
--database-toleration dedicated=database:NoSchedule
--database-node-selector clave=valorejecuta las bases de datos solo en los nodos que llevan la etiqueta. Repítelo para exigir varias etiquetas.--database-tolerationles permite entrar en nodos con un taint, escrito como lo escribekubectl taint:dedicated=database:NoScheduletolera ese taint con ese valor, ydedicated:NoSchedulelo tolera con cualquier valor. Necesita--database-node-selector, porque una tolerancia solo permite un nodo; no lo elige.
Una etiqueta por sí sola limita dónde se ejecutan las bases de datos, pero no impide que otras cargas se ejecuten en esos nodos. Solo un taint lo impide, y por eso un grupo de nodos reservado para bases de datos suele llevar ambos.
Qué coloca. El almacén relacional compartido y el almacén de eventos de cada instancia arrancada
en el clúster. dcctl bootstrap no tiene flags de ubicación: sigue a la instalación. NATS, el
almacén de objetos de respaldo, la pila de monitorización y los servicios no se colocan. NATS no
declara tolerancias, así que en un clúster cuyos nodos de bases de datos tienen un taint se ejecuta
en los demás nodos. Es deliberado: un servidor NATS usa más de un núcleo de CPU bajo carga, y con
--ha sus tres servidores se ejecutan en tres nodos distintos, de modo que en un grupo de tres nodos
de bases de datos uno de ellos compartiría nodo con la primaria del almacén de eventos, el pod de base
de datos más cargado.
Qué se comprueba.
- Antes de instalar nada,
installrechaza una ubicación que los nodos no pueden aceptar: cada base de datos ejecuta una instancia por nodo, así que necesita tantos nodos utilizables como instancias tiene (tres con--ha, una en otro caso). Un nodo es utilizable cuando lleva todas las etiquetas, no está acordonado y cada taintNoScheduleoNoExecuteque tiene está tolerado. El mensaje enumera los nodos que encontró y qué dejó fuera a cada uno.bootstrapvuelve a comprobarlo para cada instancia, porque los nodos pueden vaciarse, reetiquetarse o recibir taints después de la instalación. - El apply comprueba el mismo recuento contra el número de instancias que ejecuta realmente cada base de datos, y lo rechaza antes de crear o cambiar esa base de datos.
- Un nodo acordonado por mantenimiento, o que no está listo, no cuenta. Mientras una actualización de
nodos tiene fuera uno de los nodos de bases de datos, se rechaza volver a ejecutar
installo unbootstrapnuevo; termina la actualización y vuelve a ejecutarlo. Lo que hacen las bases de datos mientras el nodo está fuera se explica en Mantenimiento de un nodo de base de datos. - Las primarias de las dos bases de datos siguen prefiriendo nodos distintos, entre los nodos que elegiste. Si la selección es un solo nodo, lo comparten.
Para ver dónde se ejecutan las bases de datos:
kubectl get pods -A -l cnpg.io/cluster -o wide
Cambiarla. La ubicación es un ajuste de la instalación, así que cambiarla se rechaza mientras
alguna instancia se ejecute en el clúster, como los demás ajustes (consulta
Volver a ejecutar install). Eso incluye volver a ejecutar install sin los
flags en un clúster instalado con ellos. Elígela en la primera instalación: los volúmenes de una base
de datos se quedan donde se crearon, y un almacenamiento ligado a un nodo o a una zona no puede seguir
a una instancia a un nodo en otro lugar.
Sin ninguna instancia en marcha, nada rechaza el cambio, y aun así puede dejar fuera de servicio la
base de datos relacional. La base relacional sobrevive a todas las instancias, así que volver a
ejecutar con otra ubicación mueve sus pods a los nodos recién seleccionados. Si sus volúmenes están
ligados a un nodo (almacenamiento local-path, como en kind) o a una zona fuera de la nueva selección,
esos pods se quedan en Pending y la base no vuelve. dcctl cuenta los nodos que coinciden; no
comprueba dónde están los volúmenes existentes. Cambia la ubicación solo donde el almacenamiento de la
base pueda seguirla, o donde puedas permitirte recrear la base.
Dimensionamiento de los servicios
Cada servicio de backend solicita 128Mi de memoria y tiene un límite de 256Mi, salvo
event-processing, cuyo límite es 384Mi (un mínimo: un resources.limits.memory superior de nivel superior, o un límite propio de ese servicio, se usa tal cual). La CPU se dimensiona por servicio a partir de mediciones:
| Servicio | Solicitud de CPU | Límite de CPU |
|---|---|---|
device-management | 800m | 2 núcleos |
event-management | 900m | 2 núcleos |
device-state | 950m | 2 núcleos |
event-sources | 1 núcleo | 2 núcleos |
event-processing | 400m | 1 núcleo |
| cualquier otro servicio de backend | 100m | 500m |
Cada servidor NATS solicita 500m de CPU y 768Mi de memoria, con un límite de 2Gi de memoria y sin límite de CPU; consulta El intermediario de mensajes.
Con --compact, cada servicio de backend y cada servidor NATS solicita en cambio
25m y 64Mi, y los límites quedan como arriba. La consola se dimensiona por separado.
Los cinco servicios de la tabla anterior que tienen un límite de CPU propio ejecutan además el
recolector de basura de Go con GOGC=400 y un límite blando de memoria (GOMEMLIMIT) al 75 % del
límite de memoria del propio servicio. A 6.000 eventos por segundo esto redujo entre un 4 % y un
18 % la CPU que cuesta cada evento (un 18 % en device-management y event-management), porque el
recolector se ejecutó entre cuatro y trece veces menos. El límite blando es lo que mantiene el
montón más grande dentro del contenedor: hace que el recolector trabaje más en lugar de dejar que
el montón supere el límite. Por eso event-processing tiene 384Mi, ya que su conjunto de trabajo
llegó a 174 MiB de 256Mi en esa prueba. Ponga functionalAreas.<servicio>.gogc y
functionalAreas.<servicio>.goMemLimitPercent a 0 para desactivar cualquiera de los dos en un
servicio.
Los cuatro primeros servicios hacen el trabajo por evento: recibir, resolver y almacenar cada
evento, y fusionarlo en el estado en vivo de cada dispositivo. event-processing ejecuta la
detección sobre cada evento; su límite es el doble de lo que se midió que usaba (consulta más
abajo). Los límites de los cuatro primeros servicios
están dimensionados para el tráfico en vivo de los dispositivos al techo de ingesta predeterminado de un inquilino, 1000
lecturas por segundo con una lectura por mensaje, y para unos 4000 eventos por segundo, el ritmo
que sostenía una instalación predeterminada antes de que se aumentaran los valores de persistencia
de event-management (consulta Rendimiento medido).
-
Las solicitudes son lo que cada servicio usó a 6000 eventos por segundo. Una solicitud es la CPU que el planificador reserva para un pod en su nodo, y nada más: decide dónde va el pod. Cada una de las de arriba es lo que ese servicio usó, medido, a 6000 eventos por segundo, redondeado hacia arriba: el ritmo que sostuvo una instalación
--hapredeterminada, conevent-managementen dos pods, en un clúster de tres nodos de base de datos y tres nodos de servicios de 4 vCPU, con la resolución, el almacenamiento y el estado en vivo de los dispositivos siguiendo el ritmo (consulta Rendimiento medido).event-processinges la excepción: su solicitud es lo que usó con su límite de 1 núcleo a 6800 eventos por segundo ofrecidos. Con esa solicitud y ese límite, en la versión candidata, la detección siguió el ritmo a 6000 (cola máxima por debajo de 1000) y se quedó atrás a partir de 7600 ofrecidos; queda fuera de la comprobación de almacenamiento exactamente una vez de Rendimiento medido. Con las solicitudes menores que tenían antes (las de la fila sustituida de Rendimiento medido), estos servicios solicitaban entre el 15% y el 66% de lo que usaban a ese ritmo, y el planificador juntaba a los más ocupados: a 6000 eventos por segundo dos nodos de servicios funcionaban a un 80% de CPU mientras el tercero estaba al 55%. La memoria se queda en 128Mi: ningún servicio de la ruta de eventos usó más de 51Mi en ninguna muestra, hasta 9200 eventos por segundo. Una solicitud es por pod, así que un servicio escalado a dos réplicas solicita el doble para el mismo tráfico. Con--ha,event-managementse ejecuta en dos pods, así que solicita 1,8 núcleos; consulta--ha. Cada pod conserva la cifra de un solo pod, medida con un pod haciendo todo el trabajo, así que ahí reserva más de lo que usan los dos. -
Los límites no reservan nada. Kubernetes planifica un pod según sus solicitudes, así que los límites más altos no necesitan espacio adicional en un nodo. Solo permiten que un servicio ocupado use la CPU que el nodo tiene libre.
--compactreduce todas las solicitudes y no toca los límites. -
Bajar un límite de CPU recorta el rendimiento; no ahorra capacidad. Con 500m, en un clúster kind
--hade cuatro nodos,device-managementresolvía como máximo unos 720 eventos por segundo, frenado por su límite en casi todos los periodos de planificación, y cada evento por encima de eso esperaba en el stream de entrada. Medido allí sin un límite que lo frenara, usaba hasta 0,96 milinúcleos de CPU por evento yevent-managementhasta 0,53 por evento almacenado, así que el techo predeterminado necesita alrededor de un núcleo y de medio núcleo; cada límite es el doble, porque un límite de CPU se aplica en periodos cortos y un servicio ocupado lo alcanza en ráfagas mucho antes que su media. En un clúster en la nube de tres nodos, con 500m,event-sourcesquedaba frenado por su límite en el 84% de los periodos de planificación a 4000 eventos por segundo, y sus respuestas más lentas limitaron el ritmo al que podían enviar los dispositivos a unos 4300 eventos por segundo;device-statefusionaba el estado en vivo a no más de unos 2300 eventos por segundo, así que la vista en vivo de los dispositivos se retrasaba minutos. Medidos sin un límite que los frenara, usan 0,14 y 0,37 milinúcleos por evento, así que a 4000 eventos por segundo necesitan alrededor de medio núcleo y de núcleo y medio. -
La detección dispone de hasta un núcleo.
event-processingcomprueba cada evento contra las reglas de detección, en una sola partición. Con su límite anterior de 500m, en un clúster en la nube de tres nodos, su límite lo frenó en un 5% de los periodos de planificación a 6000 eventos por segundo y se quedó atrás: su cola llegó a unos 41 000 y 93 000 eventos en dos ejecuciones de 10 minutos. Con el límite de 1 núcleo y la solicitud de 400m, en la versión candidata, siguió el ritmo a 6000 eventos por segundo (cola máxima por debajo de 1000) y se quedó atrás a partir de 7600 ofrecidos, sin que su límite lo frenara. La detección queda fuera de la comprobación de almacenamiento exactamente una vez de Rendimiento medido. Su límite es de 1 núcleo. -
Los servicios más ocupados evitan el primario del almacén de eventos.
device-management,event-managementyevent-sourcesprefieren un nodo que no ejecute el primario del almacén de eventos de la instancia (en las instalaciones que usan CloudNativePG, la opción predeterminada), que es el proceso individual más ocupado de una instalación. Es una preferencia, no un requisito: en un clúster con menos nodos que servicios ocupados se siguen planificando. Solo se aplica cuando se planifica un pod, así que una conmutación por error de la base de datos no mueve los pods en ejecución. Con la configuración ajustada de abajo, sacarevent-managementde ese nodo elevó el ritmo sostenido de unos 4750 a unos 5600 eventos por segundo; su efecto con la configuración predeterminada no se ha medido. Para desactivarlo en un servicio, establecefunctionalAreas.<servicio>.avoidEventStorePrimary: false. -
Los cinco servicios de la ruta de eventos se reparten entre los nodos.
device-management,event-management,device-state,event-sourcesyevent-processingprefieren un nodo que ejecute menos de los cinco, así que con tres nodos suelen ejecutarse no más de dos por nodo. Es una preferencia que el planificador pondera junto con otras, no una garantía: con menos nodos se siguen planificando, y un nodo con más CPU libre puede ganar. Solo se aplica cuando se planifica un pod, así que los pods en ejecución no se mueven. Qué servidor NATS lidera un stream lo decide NATS, así que un servicio puede seguir compartiendo nodo con el servidor más ocupado. Un servicio con un reparto propio deja de recibir el reparto predeterminado del clúster, que coloca las réplicas de un servicio en nodos y zonas distintos; con una réplica, lo predeterminado, eso no cambia nada. Con más de una réplica, los pods del propio servicio siguen prefiriendo nodos distintos, mediante una preferencia por evitar los nodos que ya ejecutan uno de ellos, pero ya no zonas distintas. Con--ha,event-managementse ejecuta en dos pods, así que esto se le aplica. Para desactivarlo en un servicio, establecefunctionalAreas.<servicio>.eventPathSpread: false. -
Las primarias de las bases de datos prefieren nodos distintos. En las pruebas, un nodo que ejecutaba a la vez la primaria relacional y la del almacén de eventos funcionó al 94-98 % de CPU mientras los demás estaban a la mitad, aproximadamente. Consulta Dónde se ejecutan las primarias de las bases de datos, incluido cómo comprobarlo después de una conmutación por error o de una actualización.
-
Más tráfico necesita más. Varios inquilinos enviando cada uno a su techo, mensajes con muchas lecturas, o un inquilino admitido por encima de su techo mientras se pone al día necesitan más que esto. Añade
replicasadevice-management, que escala horizontalmente (más réplicas reordenan un poco más los eventos de un mismo dispositivo; consultawatermarkLatenessSecondsen la configuración del motor de detección), o aumenta el límite de un servicio:functionalAreas:device-management:resources:limits:cpu: "4"La CPU y la memoria de un servicio vienen de tres sitios, y cada uno prevalece sobre el anterior, clave a clave: los
resourcesde nivel superior, después la solicitud de CPU medida del servicio (functionalAreas.<servicio>.measuredRequests), y después losfunctionalAreas.<servicio>.resourcespropios del servicio. Así que indica solo lo que cambia. Para garantizar la CPU cuando el nodo está saturado, aumenta también la solicitud en losresourcespropios del servicio. Una solicitud por encima del límite de su servicio se rechaza al generar el chart, y el rechazo indica de dónde vino cada valor.Los límites y las solicitudes de CPU propios de los servicios de la ruta de eventos se definen de la misma forma, así que los
resourcesde nivel superior no los sustituyen: un límite de nivel superior de 4 núcleos da 4 núcleos a los demás servicios de backend y deja a los cinco en sus propios límites (2 núcleos, y 1 paraevent-processing), y unrequests.cpude nivel superior no llega a ninguno de los cinco. Define los suyos enfunctionalAreas, como arriba. En una instalación solo con el chart,useMeasuredRequests: falsedesactiva las solicitudes medidas, de modo que las solicitudes de nivel superior se aplican a todos los servicios; es lo que hace--compact.
La métrica que muestra un servicio frenado por su límite es
container_cpu_cfs_throttled_periods_total de su contenedor.
El intermediario de mensajes
Cada servidor NATS solicita 500m de CPU y 768Mi de memoria, y tiene un límite de 2Gi de memoria. Su
runtime de Go recibe un límite de memoria blando del 80% de ese valor (GOMEMLIMIT), así que
recolecta con más intensidad antes de que el kernel lo detenga. Antes, los servidores no
solicitaban ni tenían límite de nada, lo que los ponía los primeros en la cola de desalojo cuando
un nodo se quedaba sin memoria.
- La memoria se dimensiona a partir de mediciones. En la prueba de rendimiento de la versión,
la memoria residente de ningún servidor pasó de unos 830 MiB (conjunto de trabajo de unos
1,3 GiB) en ninguna ejecución hasta 9200 eventos por segundo ofrecidos, por debajo del límite de
2Gi, y ninguno se detuvo por memoria. La solicitud de 768Mi queda por debajo de ese pico: decide
dónde se coloca un servidor, y lo que lo detiene es el límite. Esas ejecuciones fueron de ingesta constante:
no se midió un servidor que vuelve a unirse a su clúster ni que se pone al día con una cola
grande tras perder un nodo. Si alguna vez un servidor se detiene por quedarse sin memoria
(
OOMKilledenkubectl describe pod), aumenta su límite. - La CPU se solicita por debajo de lo que un servidor usa con carga: unos 4,4 núcleos entre
los tres servidores a 6000 eventos por segundo, de 1,6 a 1,8 de ellos en el servidor que lidera
el stream de eventos entrantes. Con
--haen tres nodos cada nodo ejecuta exactamente un servidor, así que la solicitud no puede cambiar dónde se ejecuta. Lo que hace es mantener los servidores fuera de la clase que se desaloja primero y darles una parte de la CPU de un nodo ocupado. Solicitar todo su uso quitaría más de 4 núcleos a un clúster de tres nodos sin mover nada. Sin--ha, un único servidor lleva todos los eventos y su uso no se midió; 500m se queda corto. No hay límite de CPU: todos los eventos pasan por el intermediario, y un límite frenaría a todos los servicios a la vez. - Cada nodo necesita espacio para un servidor. Con
--halos servidores deben ejecutarse en tres nodos distintos. Si en uno de los tres no caben 500m y 768Mi más, ese servidor se queda enPending, y el bootstrap espera hasta 15 minutos antes de fallar.kubectl get pods -n <namespace de la instancia>muestra el servidor enPending, ykubectl describe podsobre él indica por qué. - Las instancias existentes los reciben al actualizarse.
dcctl upgradeaplica el bróker de una instancia, así que una instancia creada con una versión anterior recibe estas solicitudes y este límite al actualizarse a esta, y sus servidores se reinician para tomarlos: de uno en uno con--ha. Consulta Qué aplica una actualización a la infraestructura. - Para cambiarlos, establece
nats_cpu_request,nats_memory_requestonats_memory_limiten unterraform.tfvarsjunto al estado de OpenTofu de la instancia (~/.devicechain/instances/<instancia>/infra/instance/), que lee cada aplicación de la instancia que hacedcctl, también la de una actualización. La memoria admiteMioGi. En una instancia instalada con--compact,dcctlpasa las dos solicitudes por sí mismo en cada aplicación, lo que prevalece sobre el archivo, así que allí solonats_memory_limitse puede cambiar de esta forma.
Rendimiento medido
| Versión | Clúster | Configuración | Ritmo sostenido | Resultado |
|---|---|---|---|---|
| v0.18.0 | Google Kubernetes Engine, 3 × n2-standard-8 (8 vCPU cada uno), discos persistentes SSD, --ha | los valores predeterminados de v0.18.0, anteriores al dimensionamiento de arriba | unos 3800 eventos/s | Dos ejecuciones de 10 minutos a 4000 eventos/s ofrecidos almacenaron cada una 2 399 000 eventos, tantos como se aceptaron (se compararon los totales, no cada evento). La etapa más lenta mantuvo el 96,5% del ritmo ofrecido en una ejecución (la resolución, 3858 por segundo) y el 98% en la otra. El estado en vivo de los dispositivos solo siguió el ritmo hasta unos 2300 eventos por segundo, con el límite de 500m que tenía entonces device-state. |
| v0.18.0 | el mismo | ajustada: ver abajo | unos 5600 eventos/s | Ejecuciones de 180 segundos. A 5600 ofrecidos, cada etapa mantuvo al menos el 98,9% del ritmo ofrecido, la cola se vació en 3 segundos y el número de eventos almacenados fue igual al de aceptados (se compararon los totales, no cada evento). El estado en vivo siguió el ritmo. |
| después de v0.18.0, antes de sus valores de persistencia | el mismo | el dimensionamiento de arriba, con event-management en persistence.writers: 5 y persistence.maxBatch: 32 | unos 3900 eventos/s | Dos ejecuciones de 10 minutos a 4000 eventos/s ofrecidos almacenaron cada una 2 400 000 eventos, tantos como se aceptaron (se compararon los totales, no cada evento). El almacenamiento fue la etapa más lenta, con el 96,7% y el 95,7% del ritmo ofrecido. El estado en vivo de los dispositivos se mantuvo a unos 40 segundos. |
| después de v0.18.0 | el mismo | ajustada: event-management con persistence.writers: 10 y persistence.maxBatch: 64; ver abajo | unos 6000 eventos/s | Ejecuciones de 180 segundos. A 6000 ofrecidos, cada etapa mantuvo al menos el 98% del ritmo ofrecido, la cola se vació en 5 segundos y el número de eventos almacenados fue igual al de aceptados (se compararon los totales, no cada evento). Mantenido 5 minutos a 6000, el almacenamiento conservó el 96%, así que la cifra sostenida es de unos 5800 a 6000. |
| después de v0.18.0 | Google Kubernetes Engine, 3 × n2-standard-4 nodos de base de datos (con taint, solo bases de datos) y 3 × n2-highcpu-4 nodos de servicios, discos persistentes SSD, --ha | los valores predeterminados anteriores a las solicitudes de arriba (device-management 500m, event-management y device-state 400m, event-sources 150m, event-processing 100m), con los valores de persistencia actuales de event-management | unos 6000 eventos/s | Sustituida por la fila siguiente, medida con las solicitudes de arriba y los tamaños de volumen predeterminados. Dos ejecuciones de 10 minutos a 6000 eventos/s ofrecidos almacenaron cada una 3 600 000 eventos, tantos como se aceptaron (se compararon los totales, no cada evento). La resolución, el almacenamiento y el estado en vivo de los dispositivos mantuvieron cada uno el 99,7% del ritmo ofrecido o más, y la cola se vació en 10 segundos. La detección (event-processing) se quedó atrás: su cola llegó a unos 93 000 eventos. Los volúmenes del almacén de eventos y de las copias de seguridad eran más pequeños que los predeterminados. Las solicitudes de arriba se dimensionaron a partir de estas ejecuciones. |
| versión candidata v0.19.0 | Google Kubernetes Engine (us-east4-b), 3 × n2-standard-4 nodos de base de datos (16 GB; con taint, solo bases de datos) y 3 × n2-custom-4-8192 nodos de servicios (4 vCPU, 8 GB), discos de arranque persistentes estándar, --ha, los tamaños de volumen predeterminados | los valores predeterminados de arriba, con event-management en dos pods (el valor predeterminado de --ha) y la generación de perfiles de Go activada pero inactiva | 6000 eventos/s | Dos ejecuciones de 10 minutos a 6000 eventos/s ofrecidos aceptaron cada una 5997 eventos/s y almacenaron exactamente una vez los 3 600 000 eventos aceptados. Se comprobó evento por evento, por el dispositivo y el instante de ocurrencia de cada uno: ninguno faltante, ninguno almacenado dos veces, ninguno inesperado. La resolución, el almacenamiento y el estado en vivo de los dispositivos siguieron cada uno el ritmo, y la cola se vació en unos 2 segundos. La detección (event-processing), que esa comprobación no cubre, siguió el ritmo con una cola máxima por debajo de 1000 eventos, y se quedó atrás a partir de 7600 ofrecidos. |
Las filas de v0.18.0 se midieron en v0.18.0, la última fila en la versión candidata v0.19.0, y las
demás en la versión de desarrollo entre ambas, todas con el generador de carga en un nodo aparte y un almacén de eventos replicado
(--ha). Con la configuración predeterminada de v0.18.0, lo que frenó el ritmo fue el grupo de
resolutores de device-management y, más allá de él, los límites de CPU de event-sources y
device-state que el dimensionamiento de arriba aumenta. Con ellos aumentados, el límite pasó a
ser el almacenamiento de eventos: 5 escritores que confirmaban hasta 32 eventos cada uno llenaban
todos los lotes desde 4400 eventos por segundo, y cada confirmación tardaba unos 38 milisegundos,
así que el almacenamiento se detenía cerca de 4200 por segundo. Por eso event-management usa
ahora por defecto 10 escritores y lotes de hasta 64 (ver Persistencia de
eventos).
La fila ajustada de v0.18.0 usó device-management con 2 réplicas, resolution.workers: 32 y
rdbConfiguration.maxOpenConnections: 48; event-sources con 2 réplicas; event-management con
persistence.writers: 10 y persistence.maxBatch: 64, en un nodo sin el primario del almacén de
eventos; device-state con projection.writers: 5, projection.maxBatch: 64 y
projection.lingerMillis: 25; límites de CPU de 4 núcleos (2 para event-processing); y límites
de memoria de 1Gi. La fila ajustada posterior dejó device-management con sus valores por defecto
y una réplica, y por lo demás usó los mismos ajustes de event-management y device-state, con
límites de CPU de 4 núcleos y de memoria de 1Gi para device-management, event-sources,
event-management y device-state. En ella, event-management usó como mucho unos 1,7 núcleos; no
se midió con su límite por defecto de 2. Sus lotes quedaron por debajo de 32 de media en todas las
ejecuciones, así que la medición no muestra que un lote de 64 ayude más que uno de 32. Los lotes
fueron de unos 21 eventos de media a 6000 por segundo; por encima, el almacenamiento dejó de crecer
con lotes de 28 a 30 eventos de media, por debajo del límite, mientras dos de los tres nodos, uno
de ellos el del almacén de eventos, estaban al 86-95% de CPU. No se aisló cuál de esas dos cosas
limitó el ritmo, pero para más rendimiento en ese clúster de tres nodos hacen falta más nodos antes
que más ajustes por servicio. Una instalación predeterminada con los nuevos valores de persistencia
se midió en los clústeres divididos de las dos últimas filas, donde sostuvo 6000 eventos por
segundo con event-management en dos pods en la última fila; no se ha medido en el clúster de tres nodos de las
demás filas.
Volumen del almacén de eventos
Cada instancia del almacén de eventos tiene un volumen de 32Gi, así que tres con --ha (4Gi cada
uno con --compact). Una medición almacenada ocupa alrededor de 1,05 KB, y el registro de
escritura anticipada de la base de datos ocupa alrededor de 1,1 GB más mientras las copias de
seguridad le siguen el ritmo, así que 32Gi contienen unos 27 millones de eventos: unas siete
horas de un inquilino que envía a su techo predeterminado completo.
El almacén de eventos comprime las imágenes de página de su registro de escritura anticipada
(wal_compression = lz4). Tras cada punto de control, el primer cambio en una página escribe la
página entera en el registro, y en este almacén la mayoría de esas páginas son páginas de índice.
En una comparación en una versión de desarrollo posterior a v0.18.0, en un clúster de la misma
forma que el de Rendimiento medido, con un ajuste distinto del de las filas
de esa tabla, a 5200 eventos por segundo ofrecidos, la compresión redujo el registro escrito por
evento almacenado de unos 3,0 KB a unos 1,7 KB, y los puntos de control forzados por el tamaño del
registro bajaron en una proporción parecida (de 5,8 a 3,2 por millón de eventos almacenados). No
cambia lo que contiene este volumen: el registro sigue ocupando alrededor de 1,1 GB mientras las
copias de seguridad le siguen el ritmo, así que la cifra anterior se mantiene. El
tamaño del almacén de respaldos se midió antes de esta compresión, de las
claves encabezadas por el tiempo y del archivado con zstd, así que peca por exceso: en la prueba de
rendimiento de la versión, el archivo del almacén de eventos ocupó unos 0,47 KB por evento
almacenado, frente a los 1,9 KB que ese dimensionamiento supone para las dos bases de datos; la
parte de la base de datos relacional no se volvió a medir. El almacén relacional no comprime su registro.
Con el almacén de respaldos predeterminado y una sola instancia, este
volumen es lo primero que se llena con una ingesta sostenida, y DatabaseVolumeFillingFast avisa
antes. Si se llena primero el almacén de respaldos (varias instancias en él, un almacén más
pequeño o un almacén de eventos ampliado por encima de su tamaño predeterminado), el archivado se
detiene, y el registro que la base de datos no puede enviar se acumula en este volumen hasta que
se llena y la base de datos se detiene; las alertas avisan
antes de ambas cosas. Una ventana de retención
(retentionDays en la configuración lifecycle de event-management) limita los datos
almacenados en una instancia que deba funcionar indefinidamente, pero no el archivo.
El tamaño se fija al crear una instancia. dcctl upgrade aplica el resto de la configuración del
almacén de eventos y conserva su volumen con el tamaño que tiene. Para ampliarlo, con una
StorageClass que permita la expansión de volúmenes:
kubectl -n dci-<instance> patch clusters.postgresql.cnpg.io dc-tsdb --type merge \
-p '{"spec":{"storage":{"size":"64Gi"}}}'
Ampliar este volumen mueve el punto en que el almacén de respaldos se llena primero. Amplía el almacén de respaldos unas cinco veces lo que añadas aquí; consulta Tamaño del almacén de objetos de respaldo.
Después del arranque inicial
El comando imprime el namespace, la credencial de superusuario y cómo llegar a la
instancia a través del ingress del clúster. El superusuario es superuser@devicechain.local,
y no hay contraseña por defecto. El arranque genera una para la instancia, la guarda en el
Secret dci-<instance>-superuser del namespace de la instancia y la imprime una sola vez, al
final de la ejecución que la generó (o de la que termina un arranque inicial que falló antes de
imprimirla). Para volver a leerla:
kubectl -n dci-my-instance get secret dci-my-instance-superuser -o jsonpath='{.data.password}' | base64 -d
La contraseña del superusuario
El servicio user-management lee esa contraseña una sola vez: para crear el superusuario la primera vez que arranca con la tabla de identidades vacía. A partir de ahí, el Secret es un registro de la contraseña que el superusuario recibió al principio. Cambiar la contraseña en la consola no lo actualiza. Cuando un arranque recupera una instancia y sus identidades vuelven con ella, el superusuario restaurado conserva la contraseña que tenía. El informe entonces no imprime el valor del Secret, e indica que puede no ser la contraseña del superusuario.
Las instancias arrancadas con una versión anterior no tienen ese Secret. Su superusuario se
creó con la contraseña por defecto que publicaban esas versiones. Ni dcctl upgrade ni una
nueva ejecución del arranque sobre la instancia en marcha (una restauración, o
--allow-legacy-db-removal) la cambian ni generan un Secret para ella, y ambos lo indican al
terminar. Si esa contraseña no se ha cambiado desde entonces, cámbiala en la consola.
Una instalación hecha solo con Helm, sin dcctl, debe crear ese Secret por su cuenta (clave
password) antes de que user-management arranque por primera vez, o nombrar otro con el valor
del chart instance.superuserSecret. Sin él, user-management se niega a crear el superusuario.
Iniciar sesión en la consola
La instancia incluye la consola web. El ingress la sirve en la raíz del host
(https://<host>/) y enruta https://<host>/api/<area>/graphql a cada servicio de área
funcional. Abre la consola en un navegador e inicia sesión con el correo electrónico y la
contraseña del superusuario.
Una instancia recién creada no tiene tenants, así que aterrizas en la consola de
administración (/admin) para crear tu primer tenant y asignar membresías. Cambia a un tenant
para llegar a la consola del tenant. (Para una instancia headless/solo de ingesta, despliega
con la consola deshabilitada; consulta el valor frontend.enabled del chart.)
Para inspeccionar la instancia en ejecución:
kubectl --context <kube-context> get pods -n dci-my-instance
Ejecutar una simulación
Para explorar la consola con una flota en movimiento en lugar de una vacía, ejecuta una
simulación. sim create acuña una identidad y un tenant acotados en la instancia y escribe
el archivo de handshake que el proceso dc-simulator lee al arrancar:
dcctl sim create demo --instance my-instance --server localhost
El simulador inyecta entonces telemetría y alarmas por el mismo canal de dispositivo que usa el hardware real; consulta Probarlo con datos simulados.
Eliminar una instancia
dcctl destroy local my-instance
dcctl destroy elimina solo esa instancia, en este orden:
- Su release de Helm. El namespace de la instancia pertenece a esa release, así que desinstalarla elimina el namespace y todo lo que contiene, incluidos su broker NATS y su almacén de eventos.
- Su estado de infraestructura, mediante
tofu destroy, que elimina lo que ese estado aún contenga. Destroy muestra el plan de OpenTofu de lo que elimina, sin la lista de valores de salida del plan: un destroy calcula algunos de esos valores con los valores predeterminados de la configuración en lugar de con los ajustes de la instancia, así que la lista no describiría la instancia. - Su base de datos y su login de base de datos en la base de datos relacional compartida.
- Su namespace, si sigue ahí. Un arranque que se detuvo antes de que Helm instalara nada deja un namespace sin release que desinstalar. Destroy espera a ver desaparecer el namespace por completo.
- Los respaldos de su almacén de eventos, cuando están en el almacén de objetos propio del clúster (el destino predeterminado): se borra todo lo que hay bajo la ruta a la que archivaba el almacén de eventos de la instancia —su archivo del log de escritura anticipada y sus respaldos base—, y destroy comprueba después que la ruta ha quedado vacía.
- Comprueba que lo que eliminó ya no existe y, solo entonces, elimina su estado local en
~/.devicechain/instances/<instance>/.
El artefacto de depósito de la clave raíz se conserva; consulta Recuperación ante desastres.
Qué pasa con los respaldos de la instancia
Destroy lee a qué ruta archiva el almacén de eventos de la instancia antes de cambiar nada, la muestra, y solo elimina esa ruta cuando el namespace de la instancia ya no existe, de modo que nada sigue escribiendo en ella. Elimina esa única ruta y nada más: ni los respaldos de la base de datos relacional compartida, ni los de otra instancia, ni un archivo anterior que haya quedado con el mismo nombre de instancia.
- Los respaldos en un almacén de objetos que tú proporcionaste
(
dcctl install --backup-credentials-file) no se borran nunca. Son la copia que sobrevive al clúster. Destroy indica dónde están; bórralos tú cuando ya no los necesites. --keep-backupsconserva también los respaldos del almacén interno. Úsalo cuando vayas a reconstruir la instancia a partir de ellos condcctl bootstrap --restore-tsdb-fromen el mismo clúster: un destroy sin esa opción borra precisamente el archivo que lee esa restauración.dcctl destroy --allacepta--keep-backupsy lo aplica a todas las instancias.- Los respaldos base como instantáneas de volumen (
dcctl install --backup-snapshot-class) del almacén de eventos de la instancia están en su namespace, y se borran con él, con--keep-backupso sin él. Ninguna restauración los lee:--restore-tsdb-fromlee el almacén de respaldos, que--keep-backupsconserva. - Si no se puede acceder al almacén de objetos, o destroy no puede saber qué ruta es la de la instancia, el destroy termina igualmente. Indica qué dejó, y su línea final no da la instancia por destruida del todo.
Un destroy que se interrumpe después de que el almacén de eventos haya desaparecido, incluido uno interrumpido mientras elimina los respaldos, recuerda la ruta que leyó, así que ejecutarlo de nuevo sigue eliminándolos.
Las versiones anteriores a esta dejaban los respaldos de una instancia destruida en el almacén
interno, donde nada los elimina nunca. Después de eliminar los respaldos de la propia instancia,
destroy enumera las rutas del bucket que parecen archivos anteriores del mismo nombre de
instancia, y las deja. No enumera nada cuando deja en su sitio los respaldos de la propia
instancia: con --keep-backups, en un almacén externo, o cuando no pudo acceder al almacén o
borrar en él. Para encontrar y eliminar esos archivos a
mano, accede al almacén mediante un port-forward y usa cualquier cliente S3, por ejemplo la CLI
de AWS:
kubectl -n dc-system port-forward svc/dc-object-store 9000:9000 &
export AWS_ACCESS_KEY_ID="$(kubectl -n dc-system get secret dc-object-store-credentials \
-o jsonpath='{.data.MINIO_ROOT_USER}' | base64 -d)"
export AWS_SECRET_ACCESS_KEY="$(kubectl -n dc-system get secret dc-object-store-credentials \
-o jsonpath='{.data.MINIO_ROOT_PASSWORD}' | base64 -d)"
aws s3 ls s3://devicechain-tsdb/ --endpoint-url http://127.0.0.1:9000
aws s3 rm --recursive s3://devicechain-tsdb/<path>/ --endpoint-url http://127.0.0.1:9000
Cada instancia en marcha archiva bajo la ruta que nombra su almacén de eventos; puedes verla con
kubectl -n dci-<instance> get clusters.postgresql.cnpg.io dc-tsdb -o yaml (el serverName
bajo spec.plugins). Elimina solo rutas que no use ninguna instancia en marcha y de las que nadie
vaya a restaurar.
Si un paso falla o se interrumpe —incluido un namespace que sigue terminando cuando se agota la espera—, destroy termina con un error y conserva el estado local. Ejecutar de nuevo el mismo comando continúa donde se detuvo.
Si la instancia sigue en marcha pero falta su estado local de infraestructura —porque se
perdió, o porque la instancia se arrancó desde otra máquina—, destroy se niega, porque sin
ese estado no puede ejecutar la destrucción de la infraestructura. --without-state elimina la
instancia de todos modos, por su release de Helm, su base de datos y su login, y su namespace,
e informa de que se omitió la destrucción de la infraestructura. Una instancia cuyo estado
local es anterior a la división de la infraestructura en una parte de clúster y otra de
instancia se rechaza del mismo modo, antes de cualquier cambio, y también necesita
--without-state. dcctl destroy --all acepta --without-state.
Destroy nunca elimina el clúster ni los requisitos previos que dejó dcctl install, así que el
siguiente dcctl bootstrap en el clúster no necesita instalar antes. Destruir una instancia y
volver a arrancarla con el mismo nombre es como se recrea una instancia. Una instancia
construida por una versión anterior, de antes de que existiera dcctl install, es la
excepción: también hay que recrear su clúster; consulta
Versiones y actualizaciones.
Si el propio clúster de kind ya no existe —borrado con kind delete cluster—, destroy no tiene
nada que desinstalar y lo dice, limpiando solo el estado local. Eso es el directorio de la
instancia y el directorio propio del clúster en ~/.devicechain/clusters/<cluster-uid>/, que
creó dcctl install y que kind delete cluster dejó atrás (consulta
Instalar el clúster). Mientras lo hace, imprime:
removing the gone cluster's local state (~/.devicechain/clusters/<cluster-uid>)
Todavía no hay ningún comando de desinstalación. Para eliminar un clúster local que creó
dcctl install, usa kind directamente, como se muestra en Instalar el clúster. En un clúster que
conservas, consulta Eliminar un clúster.