Versiones y actualizaciones
DeviceChain se distribuye como un conjunto de imágenes de contenedor precompiladas y versionadas, más un chart de Helm. No necesita compilar nada para ejecutarlo: descargue una versión publicada, instale el chart y actualice in situ sin tiempo de inactividad.
Hasta ahora, cuatro puntos de la historia pueden exigir recrear la instancia en lugar de actualizarla:
- La
v0.9.0reemplazó la cadena de migraciones de cada servicio por una única línea base congelada, por lo que una base de datosv0.8.xfalla conalready existsal encontrarla. Consulte La compactación de la línea base de la v0.9.0. - La
v0.10.0cambió la clave primaria de las tablas de eventos para corregir un defecto que descartaba telemetría de forma silenciosa. Consulte El cambio de clave de eventos de la v0.10.0. - Cualquier instancia creada por la
v0.16.0o una versión anterior, que no registró ninguna declaración de lo que es la instancia: el registro que una actualización lee ahora para saber qué desplegar. Consulte Instancias creadas por la v0.16.0 y anteriores. - La
v0.19.0, para una instancia cuyo almacén de eventos guarda más de 4 000 000 de filas aún sin comprimir, o una tabla de más de 500 fragmentos cuyos eventos más antiguos no se pueden eliminar: su primer arranque reconstruye las claves del almacén de eventos sobre esas filas y se niega por encima de esos límites. Consulte Comprueba el número de filas.
Si está en cualquiera de ellos, lea la sección correspondiente más abajo antes de hacer nada más.
La v0.12.0 se actualiza en el sitio, pero cambia el tema en el que un dispositivo responde
a un comando, mueve un permiso y cambia varias cosas cuya forma se mantuvo igual. Un
La actualización informará éxito en cualquier caso. Lea
v0.12.0: una actualización que cambia contratos antes de empezar.
Esto se aplica a cualquier actualización que cruce la v0.12.0, no solo a la que se
detiene ahí: pasar de la v0.11.0 directamente a un parche posterior no omite esos cambios.
Modelo de versionado
Cada versión es una única etiqueta git de versión semántica (vX.Y.Z). Ese único número cubre
todo en conjunto: cada imagen de servicio, el operador, el chart de Helm y la CLI
dcctl se publican todos con la misma versión. No hay desfase de versión por servicio
del que preocuparse: un despliegue es un único número coherente.
Dos comandos mueven todo ello, y cuál mueve qué se decide por el tiempo de vida de cada
cosa. El operador es un solo controlador por clúster, compartido por todas las instancias que
haya en él, así que dcctl install lo mueve junto con el resto de los requisitos previos del
clúster. El documento de configuración, la versión desplegada del chart y las imágenes de los
servicios pertenecen a una instancia, así que dcctl upgrade mueve esos, instancia por
instancia. Consulte Actualizaciones sin tiempo de inactividad para el
procedimiento.
- Las versiones estables son
vX.Y.Z(por ejemplo,v1.2.0). La etiqueta:latestsigue a la versión estable más reciente. - Las versiones preliminares son
vX.Y.Z-rc.N(por ejemplo,v1.2.0-rc.1). Estas nunca mueven:latest.
Estabilidad previa a 1.0
Hasta la v1.0.0, cualquier versión —incluida una versión de parche— puede cambiar APIs, esquemas o comportamiento sin una capa de compatibilidad. Esto es deliberado: mientras el modelo de datos aún se está asentando, preferimos un cambio limpio antes que cargar con una capa de compatibilidad que tendríamos que mantener para siempre.
Cada cambio disruptivo se indica al principio de las notas de esa versión. Léalas antes de actualizar. Son la lista autorizada; el número de versión por sí solo no le indica si una versión es segura para su despliegue.
En concreto, antes de la v1.0.0 debe esperar que una versión pueda:
- endurecer la validación, de modo que una solicitud que antes tenía éxito ahora sea rechazada, por lo general porque se estaba aceptando silenciosamente o descartando silenciosamente
- cambiar o eliminar un campo GraphQL, en lugar de marcarlo obsoleto durante un ciclo
- alterar el esquema de la base de datos de formas que una reversión no deshará
- reemplazar por completo la línea base de migraciones, lo que elimina por entero la ruta de
actualización en lugar de limitarse a hacerla unidireccional. Cuando eso ocurre, las notas de la versión lo
indican al principio, y la única vía es recrear la instancia. La
v0.9.0y lav0.10.0son versiones de este tipo - dejar de admitir la actualización desde una instancia más antigua por un motivo que no
tiene nada que ver con el esquema: la versión posterior a la
v0.16.0lee un registro de lo que es una instancia que las versiones anteriores nunca escribieron, y se niega en lugar de inventárselo - negarse a una actualización in situ por encima de un límite de tamaño de los datos: la
v0.19.0rechaza un almacén de eventos con más de 4 000 000 de filas aún sin comprimir, o una tabla de más de 500 fragmentos, y lo indica al principio de sus notas
La propiedad de "actualizar in situ sin tiempo de inactividad" descrita arriba describe la mecánica de una actualización progresiva. No es una promesa de que sus llamadas a la API existentes conserven el mismo significado a través de un incremento de versión previo a 1.0.
Una vez que se publique la v1.0.0, esta sección se reemplaza por una promesa de compatibilidad de versionado semántico normal: cambios disruptivos solo en una versión mayor.
Debido a que las versiones son frecuentes antes de la disponibilidad general (GA), la versión menor marca un hito (una funcionalidad o subsistema significativo que se lanza) y la versión de parche lleva el ritmo continuo de correcciones y endurecimiento. Una versión de parche no es automáticamente una actualización de bajo riesgo durante este período; nuevamente, las notas de la versión son las que se lo indican.
Imágenes
Las imágenes se publican en el Registro de Contenedores de GitHub público bajo
ghcr.io/devicechain-io, por ejemplo, ghcr.io/devicechain-io/device-management. Son
multiarquitectura (linux/amd64 y linux/arm64) y se construyen sobre una base distroless sin
privilegios de root, por lo que se ejecutan como un usuario sin privilegios, sin shell y con una superficie de ataque mínima.
Debido a que el registro es público, no se requieren credenciales para descargar las imágenes publicadas.
Instalación de una versión específica
Fije la etiqueta de imagen a la versión que desea:
DC_ROOT_KEY, más abajo, es la clave raíz del almacén de secretos de la instancia:
la requieren todos los perfiles, se genera una sola vez con openssl rand -base64 32 y se
pasa sin cambios en cada instalación y actualización. Consulte
Desplegando con Helm para saber por qué.
Sustituya <version> por una etiqueta realmente publicada —la
página de versiones las lista, y un
valor no publicado falla al descargar la imagen, no en el momento de la instalación—.
helm install dc deploy/helm/devicechain \
--set instance.id=devicechain \
--set instance.config.infrastructure.secrets.rootKey="$DC_ROOT_KEY" \
--set image.tag=<version>
El chart de Helm en sí también se publica como un artefacto OCI, por lo que puede instalarlo sin una
copia local del repositorio. La versión del chart es la versión publicada sin la v inicial
—--version 0.16.0 instala la versión v0.16.0—, así que no hay un número aparte que buscar;
helm show chart oci://ghcr.io/devicechain-io/charts/devicechain imprime la última, y
--version rechaza cualquier valor que nunca se haya publicado:
helm install dc oci://ghcr.io/devicechain-io/charts/devicechain \
--version <chart-version> \
--set instance.id=devicechain \
--set instance.config.infrastructure.secrets.rootKey="$DC_ROOT_KEY" \
--set image.tag=<version>
El chart también está publicado en Artifact Hub, que muestra cada versión publicada junto con sus valores predeterminados y sus plantillas renderizadas.
Actualizar una instalación hecha solo con el chart
Una instancia instalada con helm install en lugar de dcctl bootstrap se actualiza con
helm upgrade, y conserva una trampa que la ruta de dcctl no tiene.
dcctl upgrade no se aplica a ella. Ese comando vuelve a leer del clúster la declaración de
una instancia y su documento de configuración, y una instalación hecha solo con el chart no
tiene ninguno de los dos.
El release de abajo se llama dc porque ese es el nombre que eligió el helm install de más
arriba. Una instancia instalada con dcctl bootstrap lleva un release con el nombre de la
instancia —devicechain se instala como dc-devicechain—, así que cualquier comando helm
dirigido a una de esas necesita ese nombre en su lugar.
helm get values dc -n default -o yaml > dc-values.yaml
helm upgrade dc deploy/helm/devicechain \
-n default \
-f dc-values.yaml \
--set image.tag=<new-version>
rm dc-values.yaml # este archivo contiene los secretos de su instancia
--set image.tag=… por sí solo no funcionaráLa regla de Helm es la trampa. Una actualización que no pasa ningún valor reutiliza los que
ya están en la versión desplegada. Pero en cuanto pasa cualquier valor —incluido el único
--set que cambia la versión, que es justamente el objetivo de una actualización— Helm parte
de los valores predeterminados del chart y todo lo que usted fijó al instalar desaparece. Eso
incluye la clave raíz de la instancia, sin la cual los secretos almacenados de una instancia en
funcionamiento no se pueden leer.
Cuando eso ocurre no se corrompe nada, porque el chart se niega a renderizar sin la clave raíz:
Error: UPGRADE FAILED: execution error at (devicechain/templates/instance-config.yaml:21:4): instance.config.infrastructure.secrets.rootKey is required: every instance seals its token-signing key under it, along with any integration credentials it stores, and user-management cannot start without it. Set it to a base64 256-bit key (openssl rand -base64 32); dcctl bootstrap mints one automatically.
--reuse-values también funciona, pero conserva en silencio entradas obsoletas cuando los
valores predeterminados del chart cambian entre versiones, así que es preferible volcar los
valores y pasarlos con -f, donde puede verlos.
Una instalación que monta su configuración de instancia desde un Secret que el chart no
escribe —fijado con instance.existingSecret, el patrón que producen External Secrets y
sealed-secrets— tiene ahora que cumplir cuatro condiciones, y helm upgrade hace fallar el
renderizado en lugar de continuar cuando alguna no se cumple. Cuando eso ocurre, nada cambia
en la release; el rechazo es todo el efecto.
-
El Secret debe llamarse
dci-<instance.id>-config, en el namespace de la instancia. Cualquier otro nombre se rechaza, porquedcctlvuelve a leer la configuración exactamente por ese nombre para decidir si una instancia ya existe, y trata un Secret ausente como una instalación nueva: una nueva ejecución acuñaría una nueva clave raíz y nuevas credenciales de base de datos y de broker sobre una instancia en funcionamiento. Si su Secret está bajo otro nombre, créelo de nuevo bajo este (eltarget.namede External Secrets, elmetadata.namede un sealed secret) antes de actualizar. -
instance.existingSecretChecksumes obligatorio: el sha256 del documento bajo la claveinstancedel Secret, como 64 caracteres hexadecimales en minúsculas. El chart no puede leer su Secret, así que esto es lo que reinicia los pods cuando la configuración cambia; sin él, una credencial rotada se aplicaría sin problemas, no reiniciaría nada e informaría éxito. Vuelva a calcularlo cada vez que el documento cambie:kubectl get secret dci-<instance.id>-config -n dci-<instance.id> \-o jsonpath='{.data.instance}' | base64 -d | sha256sum | cut -d' ' -f1 -
networkPolicy.externalConfigPortses obligatorio mientrasnetworkPolicy.enabledesté activado, con las clavesnatsyrdbrepitiendo los puertos del broker y de la base de datos que nombra su documento. De lo contrario el chart los tomaría de sus propios valores predeterminados, y un puerto que no coincida bloquea en silencio la salida de los propios servicios, lo que se presenta como una caída del broker o de la base de datos. -
metrics.natsBrokerHostes obligatorio mientrasmetrics.natsPodMonitoresté activado: el nombre de host del broker tal como lo nombra su documento (el nombre corto del Service para un broker en el namespace de esta instancia,<service>.<namespace>para uno en otro lugar). Decide qué namespace vigila el PodMonitor; un valor predeterminado que no coincida no monitoriza nada. O bien fijemetrics.natsPodMonitor=false.
Cada error de helm nombra el valor que necesita y por qué. Las dos transformaciones que el
chart aplica normalmente al escribir el documento —el bloque infrastructure.shutdown y la
eliminación de infrastructure.aiInference cuando esa área no está desplegada— siguen
correspondiéndole a usted reproducirlas, como hasta ahora.
Actualizaciones sin tiempo de inactividad
Actualizar consiste en dos comandos —uno para el clúster y luego uno por cada instancia
que haya en él—, y el chart y los servicios están diseñados para hacer avanzar a los clientes sin
perder tráfico. Hay cinco excepciones, todas documentadas más abajo: la transición a la
ingesta duradera, que sigue siendo una actualización corriente pero tiene un efecto secundario
visible; la v0.9.0, la v0.10.0 y cualquier instancia creada por la v0.16.0 o una
versión anterior, a las que no se puede actualizar en absoluto; y la v0.19.0 para una
instancia cuyo almacén de eventos guarda más de 4 000 000 de filas aún sin comprimir, o una
tabla de más de 500 fragmentos cuyos eventos más antiguos no se pueden eliminar
(Comprueba el número de filas). Consulte las notas de la
versión a la que va a migrar antes de ejecutarlo:
dcctl install local --version <new-version>
dcctl upgrade local devicechain --version <new-version>
Una versión de DeviceChain es un único número que abarca las imágenes de los servicios, el
chart, el operador y dcctl. Los dos comandos se lo reparten según a qué pertenece cada cosa:
dcctl install mueve el clúster. El operador —su namespace, sus CRD, su RBAC y su
controlador— se aplica a partir de manifiestos incrustados en dcctl. No forma parte del
chart de Helm, así que nada dentro del chart puede alcanzarlo. Se aplica el flujo renderizado
completo y no solo la imagen del controlador, porque los CRD van en él: un esquema que se
quedara en la versión con la que se arrancó la instancia descartaría en silencio cualquier
campo que añadiera una versión posterior. Este comando mueve además el resto de los requisitos
previos compartidos del clúster; consulte
Instalar el clúster.
dcctl upgrade mueve una instancia, y no mueve nada de lo compartido:
- la infraestructura de la instancia: su bróker de mensajería (NATS) y su almacén de eventos, aplicados desde la configuración de OpenTofu que trae esta versión, de modo que un cambio en cualquiera de los dos llega a las instancias existentes y no solo a las nuevas. Consulte Qué aplica una actualización a la infraestructura;
- el documento de configuración del que cada servicio lee sus credenciales y sus
endpoints, recompuesto a partir del chart de esta versión y escrito por
dcctl, que es su dueño; - la versión desplegada de Helm que ejecuta los servicios, que los hace avanzar a las imágenes nuevas y espera a que cada área termine.
El orden importa, y la actualización lo comprueba. El operador es lo que define la
declaración de la instancia, así que tiene que estar en la versión nueva antes de mover una
instancia a ella. dcctl upgrade lee el operador que lleva el clúster y rechaza una
instancia cuyo clúster no tenga operador alguno, o cuyo operador sea identificablemente el de
otra versión, nombrando el comando de instalación que hay que ejecutar primero. No aplica el
operador él mismo: en un clúster con varias instancias eso movería el controlador de todas
las demás como efecto secundario de actualizar una, en silencio.
Hay un caso que deja pasar con un aviso en lugar de rechazarlo. dcctl install deja
constancia de qué versión instaló las definiciones; un operador puesto en el clúster a
mano no lleva esa constancia, y dcctl no puede distinguir una instalación manual
deliberada de una que un dcctl más antiguo sobrescribió. En lugar de pasar por encima de
una decisión que no puede ver, imprime una nota con el comando de instalación y continúa. Si
usted no instaló el operador a mano, tome esa nota como el rechazo que habría sido y ejecute
dcctl install antes de seguir.
Ejecute cualquiera de los dos con --dry-run primero si quiere ver qué movería. dcctl upgrade toma el clúster de destino del propio registro de la instancia en lugar de
adivinarlo, y dice cuál es.
dcctl upgrade conserva aquello sobre lo que la instancia está funcionando: las contraseñas
propietarias de las bases de datos, la autoridad y los inicios de sesión del bróker, el secreto
entre servicios, la clave raíz del almacén de secretos y —cuando el clúster los ejecuta— la
contraseña de administrador del panel de monitoreo y la credencial del almacén de objetos de
respaldo interno del clúster. Un cambio de versión no puede convertirse en un cambio de
credenciales.
Esto está verificado, no solo afirmado. Se comprobó la actualización de una instancia en funcionamiento comparando un resumen criptográfico (digest) de cada una de esas credenciales antes y después, y lo único que había cambiado era la etiqueta de imagen: en todos los servicios, en la consola y en el operador.
Como las conserva por diseño, no rota nada. Si necesita cambiar una credencial, una actualización no lo hará — y para varias de ellas hoy no existe un procedimiento admitido.
El perfil, la topología y las áreas funcionales habilitadas provienen de la propia declaración
de la instancia —lo que dcctl bootstrap registró en el clúster—, no de flags escritos aquí.
Cambiar lo que una instancia es es una pregunta distinta con respuestas distintas: subir el
número de réplicas, por ejemplo, no vuelve a replicar los streams de mensajería que se crearon
con el número anterior.
Una cosa queda además deliberadamente fuera de este comando: no toca los datos de las bases de datos más allá de dejar que los servicios ejecuten sus propias migraciones, y nunca cambia el tamaño de un volumen.
Qué aplica una actualización a la infraestructura
Antes de mover los servicios, dcctl upgrade aplica la configuración de OpenTofu de la
instancia, la misma que aplicó dcctl bootstrap al crearla. Esa configuración contiene los
servidores NATS de la instancia y su almacén de eventos: la imagen, los ajustes y la ubicación
de la base de datos, y su configuración de respaldo y archivado. La actualización planifica
primero y muestra lo que va a cambiar; con --dry-run muestra el plan y se detiene. Necesita
tofu en el PATH, como un bootstrap, y preparar la configuración consulta el registro de
proveedores (o su réplica) por sus proveedores; una ejecución en seco también lo hace, en el
directorio de trabajo local en el que planifica.
Conserva aquello con lo que funciona la instancia. Conserva todas las credenciales, como se indica arriba. El volumen de JetStream y el del almacén de eventos conservan su tamaño. Si esta versión crea otro tamaño para las instancias nuevas, la actualización lo dice, y Volumen del almacén de eventos muestra cómo ampliar uno usted mismo. El almacén de eventos conserva la ruta bajo la que archiva, y un almacén restaurado conserva el archivo desde el que se restauró.
Se niega, sin cambiar nada, cuando:
- Esta máquina no tiene el estado de la instancia. El estado de la configuración vive en
~/.devicechain/instances/<instancia>/en la máquina que la creó. Ejecute allí la actualización, o copie antes ese directorio. Contiene credenciales, así que manténgalo privado (modo0700). - El plan borraría o reemplazaría algo que gestiona la configuración, como el bróker o el almacén de eventos. El rechazo dice qué. Las notas de una versión así explican cómo pasar a ella. Esto juzga los recursos que gestiona la configuración; un objeto que desaparece dentro de uno de ellos se muestra como un cambio de ese recurso.
- El plan acortaría la ventana de recuperación del almacén de eventos o dejaría de declarar un lector de analítica que nada de lo que lee esta actualización declara. Son valores puestos a mano, y aplicar sin ellos podaría respaldos o dejaría sin gestionar la contraseña de un lector. Declárelos como se describe abajo y vuelva a ejecutar la actualización.
- El plan desactivaría los respaldos del almacén de eventos. Ninguna declaración lo deja
pasar:
dcctldecide si los respaldos están activos según cómo se instaló el clúster.--skip-infrastructuremueve los servicios y deja el almacén de eventos como está. - El bróker o el almacén de eventos no está sano antes de empezar, por ejemplo un servidor que no está listo o un almacén a mitad de una conmutación por error. Recupérelo primero. La única excepción es un bróker cuyo último despliegue gradual no terminó, con algunos servidores ya en los ajustes nuevos y uno de ellos sin estar listo. Solo una aplicación puede cambiar los ajustes hacia los que avanza, así que la actualización aplica sobre él, lo indica junto al plan y espera a que los ajustes nuevos terminen de desplegarse. Un bróker al que le falta un servidor con los ajustes que ya ejecutan todos, por ejemplo tras perder un nodo, se sigue rechazando.
- El plan cambia la imagen del almacén de eventos y sus ajustes a la vez en un almacén de más de una instancia, algo que rechaza el operador de la base de datos. El rechazo indica el orden en que aplicarlos.
--skip-infrastructure mueve solo los servicios, como hacían las actualizaciones anteriores a
esta versión, y al terminar dice lo que dejó sin aplicar.
Valores que usted mismo dio a la configuración. Una actualización aplica la configuración con
los valores de dcctl, como hace un bootstrap. Un valor se conserva cuando está declarado en un
archivo terraform.tfvars o *.auto.tfvars junto al estado de la instancia, en
~/.devicechain/instances/<instancia>/infra/instance/, o en una variable de entorno
TF_VAR_<nombre>: OpenTofu lee ambos en cada aplicación que ejecuta dcctl. Un valor pasado a
mano con -var no se conserva. Entre los valores que se indica fijar así están
backup_retention_tsdb, timescale_analytics_readers (consulte
Acceso SQL y BI) y los del bróker nats_cpu_request,
nats_memory_request y nats_memory_limit. Una solicitud o un límite del bróker que la
actualización bajaría se muestra como aviso junto al plan.
Esto solo funciona para una variable que dcctl no pasa por sí mismo, porque OpenTofu deja que un
-var en la línea de órdenes prevalezca sobre esos archivos y sobre TF_VAR_. dcctl pasa, en
cada aplicación:
- en una instancia instalada con
--compact, las solicitudes del brókernats_cpu_requestynats_memory_request, y los tamaños de sus volúmenes. Fijar esas solicitudes enterraform.tfvarsno tiene efecto en una instancia compacta.nats_memory_limitno está entre ellas. - según cómo se instaló el clúster, si los respaldos de base de datos están activos
(
enable_database_backups), la clase de instantánea (backup_snapshot_class) y la ubicación de las bases de datos en los nodos. Cámbielos condcctl install, no en ese archivo.
Los avisos y rechazos de la actualización indican en qué caso está cada valor.
Lo que ve una instancia en marcha:
- Los servidores NATS se reinician cuando cambian sus ajustes. Con
--hase reinician de uno en uno; cada uno está Ready antes de que se detenga el siguiente. Sin--hase reinicia el único servidor y el bróker no está disponible mientras tanto, normalmente alrededor de un minuto. Los servicios y los dispositivos se reconectan solos; las publicaciones hechas entretanto se rechazan. Si el nodo no tiene sitio para las nuevas solicitudes del servidor, este queda enPending: con--halos otros dos siguen atendiendo, pero sin--hael bróker sigue caído hasta que haya sitio, y la actualización falla tras hasta 15 minutos diciéndolo. - El almacén de eventos se reinicia cuando cambian sus pods, por ejemplo por una imagen o una
ubicación nuevas. Un cambio solo de ajustes se recarga sin reinicio. Con
--hase reinician primero las réplicas en espera y después el rol primario pasa a una de ellas. Sin--hase reinicia la única instancia, y event-management no puede almacenar eventos hasta que vuelva. Esos eventos esperan en el stream de ingesta y se reintentan, y uno solo se abandona tras unos cuatro minutos de reintentos fallidos. - Los dos pueden reiniciarse a la vez. Se aplican juntos, y la actualización espera a que ambos estén sanos antes de mover los servicios.
Una versión que cambie la imagen del almacén de eventos llega ahora por este paso a las instancias existentes: sus instancias se reinician con ella. Las notas de una versión así dicen qué más necesita.
Qué más comprueba una actualización
Dos cosas viajan con ella, porque un cambio de versión es lo que le ocurre de forma fiable a una instancia en funcionamiento, y un calendario no.
El certificado del bróker. El bróker de mensajería sirve un certificado válido durante un
año, emitido por una autoridad que dcctl acuña en el arranque inicial y conserva en el
clúster. Una actualización vuelve a emitir ese certificado cuando está dentro de sus últimos 30
días, o cuando ya no cubre todos los nombres por los que los bróker se llaman entre sí —que es
lo que le hace escalar una instancia a un certificado que, por lo demás, sigue holgadamente en
vigor. La reemisión se hace bajo la misma autoridad, así que nada tiene que volver a
confiar en nada, y el bróker se reinicia para que sirva de verdad el certificado nuevo en lugar
de conservar el antiguo hasta que algo ajeno lo reinicie. Fuera de esas condiciones la
comprobación se ejecuta y no hace nada.
Una instancia arrancada antes de que dcctl conservara esa autoridad no puede tener su
certificado reemitido en sitio. La actualización lo indica y continúa en lugar de fallar;
recrear la instancia es lo que acuña una autoridad y un certificado nuevos.
El depósito (escrow) de la clave raíz. Cada actualización comprueba que el artefacto de depósito de esta instancia siga protegiendo la clave sobre la que la instancia está realmente funcionando. Esa comprobación no necesita contraseña: el artefacto registra una huella de la clave que protege, así que compararla con la que está en uso no abre nada.
| Qué encuentra | Qué hace |
|---|---|
| El artefacto protege la clave en uso | Lo indica y lo deja intacto |
| El artefacto protege una clave distinta | Avisa con claridad. Lo más habitual es que pertenezca a una instancia anterior con el mismo nombre, y restaurar desde él recuperaría un clúster incapaz de leer sus propios secretos |
| No hay artefacto | Escribe uno, si usted pasó --escrow-passphrase-file (o fijó DCCTL_ESCROW_PASSPHRASE). Si no, avisa de que la única copia de la clave raíz está dentro del clúster |
Así es como una instancia creada al principio con --no-escrow obtiene un depósito más tarde.
Ninguno de esos desenlaces hace fallar la actualización: un problema de depósito trata de un
desastre futuro y la actualización que tiene delante trata de la instancia en funcionamiento, y
un operador que no puede actualizar rodeará la comprobación en lugar de arreglarla.
helm upgradeEl procedimiento era: volcar a un archivo los valores de la versión desplegada actual con helm get values, volver a pasarlos con -f junto a la nueva etiqueta de imagen, borrar el archivo
porque contenía sus secretos, y después mover el operador por separado.
Ese baile existía únicamente porque la versión desplegada de Helm era donde vivían las
credenciales generadas de la instancia, y Helm parte de los valores predeterminados del chart
en cuanto se le pasa cualquier valor —de modo que una actualización que no las trasladara a
mano las perdía. Ahora dcctl es el dueño del documento de configuración, la versión desplegada
ya no contiene esas credenciales, y el paso que le decía que escribiera sus secretos en un
archivo simplemente desaparece.
El hueco que aquella forma dejaba abierto era una actualización que se detenía tras la mitad
de helm: dejaba los servicios nuevos ejecutándose contra el controlador con el que se arrancó
la instancia por primera vez, indefinidamente y sin ningún error que lo indicara. Eso es lo que
cierra el rechazo descrito más arriba: los dos comandos siguen siendo dos, porque el operador
pertenece al clúster y la versión desplegada pertenece a la instancia, pero dcctl upgrade
lee ahora qué operador lleva el clúster y lo dice: rechaza cuando puede distinguirlos, y avisa
cuando no puede.
Lo que hace que el despliegue sea seguro:
- Aumentar antes de terminar. Los Deployments usan por defecto una estrategia
RollingUpdateconmaxUnavailable: 0ymaxSurge: 1, de modo que un pod nuevo debe pasar su sonda de disponibilidad/readyzantes de que se elimine un pod antiguo, y la capacidad nunca disminuye durante el despliegue. Cuatro áreas se distribuyen en cambio constrategy: Recreatey una sola réplica, porque solo uno de sus pods puede servir a la vez:event-processing(el motor de reglas es un escritor único),mcp(la sesión de un cliente vive en el pod que la abrió),sparkplug-ingest(un Sparkplug Host por pod) ylwm2m-ingest(un socket CoAP/UDP por pod). En ellas, todos los pods antiguos se detienen antes de que arranque el nuevo, así que un despliegue tiene una breve brecha por diseño; y el chart rechazaRecreatecon más de una réplica. - Apagado ordenado / drenaje de conexiones. Cuando se le pide a un pod que termine, primero
informa "no listo" (de modo que el Service deje de enrutarle nuevas solicitudes), espera una breve
ventana de drenaje para que ese cambio se propague, y solo entonces termina el trabajo en curso y
se apaga. Configure la ventana con
shutdownDrainSeconds(por defecto5), mantenida de forma segura por debajo determinationGracePeriodSeconds(por defecto30). Ambos forman un único presupuesto y los servicios lo verifican: el drenaje puede ocupar como máximo la mitad del período de gracia, porque la ventana solo espera — terminar las solicitudes en curso, drenar los consumidores del bróker y cerrar el pool de base de datos ocurren después de ella, y el kubelet envía SIGKILL cuando el período de gracia expira, haya terminado eso o no. Una ventana mayor se rechaza al arrancar el servicio (y endcctl bootstrap, antes de instalar nada), en lugar de descubrirse cuando un pod ya se está apagando. PongashutdownDrainSeconds: 0para omitir el drenaje por completo, lo que encaja con una ejecución de una sola instancia sin ningún Service del que retirarse. - Migraciones de esquema coordinadas. Los servicios ejecutan migraciones de base de datos bajo un bloqueo a nivel de base de datos, de modo que cuando varias réplicas se inician a la vez, exactamente una aplica las migraciones y el resto espera; sin condiciones de carrera, sin DDL duplicado.
Para lograr un verdadero cero tiempo de inactividad, ejecute replicas: 2 (o más) para cada área que
pueda servir desde más de un pod, de modo que el despliegue siempre tenga un pod activo sirviendo tráfico.
Una sola réplica igualmente tiene una breve brecha mientras se reemplaza su único pod.
Configúrelo globalmente con --set replicas=2, o por área bajo
functionalAreas.<area>.replicas. Las cuatro áreas de un solo pod mencionadas arriba son la excepción:
mcp, sparkplug-ingest y lwm2m-ingest rechazan más de una réplica con cualquier estrategia, y
event-processing acepta una segunda réplica solo como reserva en caliente, con strategy: RollingUpdate
configurada junto a ella; en cualquier otro caso el renderizado falla y explica por qué. Un
PodDisruptionBudget se genera automáticamente para cualquier área con más de una réplica, de modo que
los drenajes de nodo no puedan expulsar a todas las réplicas a la vez.
Los techos de tasa los aplica cada réplica por separado, de modo que dos réplicas de event-sources,
outbound-connectors o ai-inference pueden admitir hasta el doble del techo de un inquilino. Consulte
Gobernanza.
La compactación de la línea base de la v0.9.0
La v0.9.0 es la primera de las dos versiones a las que no se puede llegar actualizando en sitio
(la otra es la v0.10.0).
Antes de ella, el esquema de cada servicio se construía mediante una cadena de migraciones aplicadas en orden.
La v0.9.0 reemplaza todas esas cadenas por una única línea base congelada: una migración por servicio que
crea el esquema completo tal y como está. Una base de datos creada por v0.8.x ya aplicó la cadena antigua, así
que cuando se encuentra con la línea base intenta crear tablas que ya existen y falla con already exists. El
fallo es evidente y ocurre en el arranque; no corrompe nada.
No hay ruta de migración y, antes de la v1.0.0, no la habrá. Mantener una capa de compatibilidad para una forma
de esquema que todavía se está asentando es precisamente el coste que este proyecto ha decidido no asumir
mientras todas las instalaciones siguen siendo tempranas.
Para pasar a la v0.9.0, recree la instancia:
# Exporte antes lo que necesite: esto descarta las bases de datos.
dcctl destroy local devicechain --without-state # elimina la instancia; el clúster antiguo va después
kind delete cluster --name devicechain # y el clúster que preparó la versión anterior
dcctl install local # prepara un clúster nuevo
dcctl bootstrap local devicechain
Con el dcctl actual se recrea también el clúster, no solo la instancia: consulte
por qué.
La protección de destrucción protege las bases de datos frente a una operación normal de
helm, no frente a un dcctl destroy deliberado. Si la instancia contiene telemetría, definiciones de
dispositivos o paneles que le importan, expórtelos antes de empezar. No existe una ruta in situ que los
conserve a través de esta versión.
Normalmente, un cambio de esquema añade una nueva migración sobre la línea base, lo que es una actualización in situ corriente. Esa es la regla, y se cumple en casi todas las versiones.
Decía que la compactación describía «una única versión, no una nueva política». Después, la
v0.10.0 también necesitó recrear la instancia, por un motivo distinto. La versión honesta de la
regla es: añadir migraciones es lo normal y, antes de la v1.0.0, una versión todavía puede exigir
recrear la instancia cuando un defecto no se puede corregir de otra forma. Toda versión que lo
exija lo indicará en sus notas y aquí. Consulte ambas antes de actualizar, en lugar de deducirlo
del número de versión. La v0.19.0 es la más reciente, por el tamaño de los datos y no por su
forma: una instancia cuyo almacén de eventos guarda más de 4 000 000 de filas aún sin comprimir,
o una tabla de más de 500 fragmentos cuyos eventos más antiguos no se pueden eliminar
(Comprueba el número de filas).
El cambio de clave de eventos de la v0.10.0
La v0.10.0 es la segunda versión a la que no se puede llegar actualizando en sitio, por un motivo
distinto al de la compactación.
Un evento se identificaba por la combinación de su inquilino, dispositivo, tipo y marca de tiempo. Esa combinación no es única: un dispositivo que muestrea dos sensores y publica cada uno como su propio mensaje bajo una misma marca de tiempo produce dos eventos realmente distintos que a la base de datos le parecen idénticos. El segundo se descartaba de forma silenciosa: sus lecturas quedaban guardadas contra el registro del primero y, una vez descartado, ya nunca podía reconocerse como repetido, de modo que cada reintento posterior de ese mensaje añadía otra copia de sus lecturas.
Cualquier dispositivo que marque el tiempo en segundos enteros podía provocarlo emitiendo dos veces en un mismo segundo, y el SDK de .NET publicado lo hacía así hasta esta versión.
La v0.10.0 otorga a cada evento, a cada lectura y a cada registro de relación una identidad
derivada de su propio contenido, y convierte esa identidad en la clave. Almacenar la telemetría
correctamente implica cambiar la clave primaria de las tablas más grandes del sistema, y esas tablas
están comprimidas: un motor de base de datos no altera una clave sobre datos comprimidos in situ. No
existe ninguna ruta de actualización que conserve las filas existentes.
Para migrar a la v0.10.0, recree la instancia:
# Exporte antes lo que necesite: esto descarta las bases de datos.
dcctl destroy local devicechain --without-state # elimina la instancia; el clúster antiguo va después
kind delete cluster --name devicechain # y el clúster que preparó la versión anterior
dcctl install local # prepara un clúster nuevo
dcctl bootstrap local devicechain
Con el dcctl actual se recrea también el clúster, no solo la instancia: consulte
por qué.
Se aplica la misma advertencia que más arriba: recrear la instancia descarta su telemetría, las definiciones de dispositivos y los paneles. Exporte lo que necesite antes de empezar.
La acompañan dos cambios en cómo la API informa del tiempo, y ninguno requiere acción alguna:
- Las marcas de tiempo se devuelven ahora con la precisión con la que se registraron; antes se redondeaban hacia abajo al segundo entero al salir, por lo que dos lecturas separadas por 200 milisegundos volvían pareciendo simultáneas. Las marcas de tiempo de segundo entero no cambian.
- Las peticiones que usan el valor
updatedAtde un registro para evitar sobrescribir la edición de otra persona se comprueban ahora con esa misma precisión. Antes, dos ediciones dentro de un mismo segundo podían superar ambas la comprobación, y la posterior sobrescribía en silencio un cambio que nunca había visto.
v0.11.0: de nuevo una actualización normal
La v0.11.0 es la primera versión desde la v0.8.5 a la que se puede llegar en sitio.
Su cambio de esquema añade tres migraciones en lugar de reemplazar una línea base, por lo que
una base de datos v0.10.0 existente se traslada con sus filas intactas en vez de tener que
recrearse.
Lo que llega a la base de datos:
- dos tablas nuevas que registran el progreso y el historial de la eliminación de un inquilino, y
- dos columnas en la tabla de inquilinos que siguen su estado de ciclo de vida.
Todos los inquilinos que ya existen quedan en el estado activo normal al añadirse la columna, así que nada cambia en una instancia en funcionamiento hasta que elimine realmente un inquilino.
Antes de la publicación se ejecutaron dos comprobaciones, y conviene mantenerlas separadas porque midieron cosas distintas.
Las migraciones, contra una base de datos con datos dentro. Se construyó un esquema v0.10.0,
se llenó con filas representativas y se trasladó hacia adelante. Todas esas filas llegaron
idénticas byte a byte, y el esquema resultante es idéntico al de una instalación nueva de
v0.11.0, para todas las áreas funcionales, no solo para la que cambió.
La actualización en sí, sobre una instancia en funcionamiento. Se construyó una instancia
v0.10.0 a partir de las imágenes v0.10.0 publicadas, se le dieron inquilinos e identidades
reales y luego se actualizó con el comando de arriba. Todos los servicios se desplegaron, el
recuento de filas de las 67 tablas no varió salvo por las nuevas entradas de migración y los
registros de auditoría que escribieron, el inicio de sesión siguió funcionando para una cuenta
creada bajo v0.10.0, y la nueva API de eliminación de inquilinos respondió en la instancia
actualizada.
Cuatro límites, indicados con claridad:
- Solo se verificaron las bases de datos. El estado del bróker (JetStream), el almacenamiento de objetos y el estado clave-valor no están cubiertos por ninguna de las dos comprobaciones.
- Solo PostgreSQL 16. Las instalaciones nuevas se verifican en ambas versiones principales admitidas; la ruta de actualización en sí se midió en la 16.
- La comparación fila a fila proviene de la primera comprobación, no de la segunda. La instancia en funcionamiento se verificó por recuentos de filas, que no detectarían una fila modificada en el sitio en lugar de eliminada.
- La consola web se dejó con su imagen
v0.10.0durante la segunda comprobación, así que la consolav0.11.0no se ejercitó contra una instancia actualizada.
v0.12.0: una actualización que cambia contratos
Se puede llegar a la v0.12.0 en sitio. Su cambio de esquema añade
migraciones en lugar de reemplazar una línea base, así que una base de datos v0.11.0
existente se conserva con sus filas intactas, y esto se midió sobre una instancia en
ejecución en lugar de razonarse.
Lo que sí cambia son los contratos: el tema MQTT en el que un dispositivo responde a un comando, unas cuantas operaciones GraphQL y el significado de varias cosas cuya forma no cambió en absoluto. Nada de eso se ve en una actualización que informa éxito, así que lea esta sección antes de ejecutarlo.
Haga esto antes de actualizar
1. Actualice todo dispositivo que responda a comandos. El tema en el que un dispositivo publica la respuesta a un comando ahora está acotado a ese dispositivo:
# antes
{instanceId}/{tenant}/command-responses
# ahora
{instanceId}/{tenant}/command-responses/{deviceToken}
Las credenciales que se emiten a un dispositivo ya no permiten el tema anterior, así que a un dispositivo sin actualizar se le rechazarán las respuestas en el broker: seguirá recibiendo los comandos y actuando sobre ellos, pero la plataforma nunca registrará que lo hizo, y todos acabarán leyéndose como caducados por tiempo.
El motivo del cambio es que el tema anterior permitía que cualquier dispositivo del inquilino publicara una respuesta nombrando cualquier comando, incluido uno emitido a otro dispositivo. Nada en la respuesta decía quién la enviaba, así que nada podía distinguirlo. El token del dispositivo forma ahora parte del tema, que forma parte de lo que el broker firma, de modo que un dispositivo solo puede responder por sí mismo.
Actualice primero los dispositivos si puede. Las respuestas enviadas en el tema anterior durante la transición se rechazan, no se encolan, y el pequeño número de respuestas en vuelo justo en el momento de la actualización se descarta en lugar de entregarse.
2. Renombre una fuente de eventos cuyo id sea exactamente lwm2m. Ese es el valor bajo
el que el servicio LwM2M archiva la presencia de sus propios dispositivos, y los registros
de presencia se comparan por igualdad exacta, así que su fuente y ese servicio se
sobrescriben mutuamente las filas. event-sources ahora se niega a arrancar con ese id, lo
que detiene toda la ingesta de la instancia.
Un id que solo se lee como un transporte, como sparkplug:plant-a o lwm2m:site-a, ahora
arranca con una advertencia en lugar de negarse. Renómbrelos cuando le venga bien. En ambos
casos tenga en cuenta la trampa al renombrar: la presencia ya registrada bajo el id anterior
no se traslada, y nada la rellena después.
3. Compruebe quién lee el historial de ubicaciones. Las consultas que devuelven
posiciones de dispositivos ahora requieren el permiso location:read en lugar de
event:read. Ese permiso no está en la base de solo lectura que recibe un visor, así que
una cuenta que podía leer el historial de posiciones en la v0.11.0 no puede en la
v0.12.0. Concédalo explícitamente a los roles que lo necesiten.
Ese mismo permiso pasa a controlar además la vista previa de una regla que comprueba la
contención en una geocerca. Una vista previa de ese tipo devuelve, por dispositivo, cuándo
entró en una región y cuándo salió — una lectura de posición, se pida como se pida — así que
previewRule requiere location:read además del device:read que toma toda vista previa. A
un autor de reglas que en la v0.11.0 podía previsualizar cualquier borrador se le rechazan
los borradores con contención hasta que se le conceda ese permiso. Las vistas previas que no
comprueban contención no se ven afectadas.
Y compruebe quién lo lee a través de un asistente de IA. El servidor MCP incorporó una
herramienta query_locations que devuelve las posiciones reportadas de un dispositivo, y llegar
a ella exige dos concesiones que se mantienen separadas de forma deliberada. La autorización
del agente debe incluir un alcance OAuth nuevo, location, y la persona que lo autorizó debe
tener un rol que conceda location:read. Ninguna de las dos basta por sí sola: el alcance es un
techo sobre lo que un token puede portar, no una concesión de nada.
Un agente autorizado solo con read-only no puede leer posiciones, por mucho que tenga su
usuario. Ese es justamente el sentido de un alcance aparte en lugar de un read-only más
amplio: la pantalla de consentimiento le muestra a la persona la cadena de alcance en crudo, así
que meter la posición dentro de read-only habría significado una autorización idéntica antes y
después que ahora incluye dónde han estado los dispositivos — y, con bastante frecuencia, dónde
han estado las personas que los llevan. Mantenerlo separado hace que conceder observabilidad a
un agente no sea el mismo acto que concederle el historial de ubicaciones, y permite a un
usuario permitir lo uno reteniendo lo otro.
Un cliente MCP que ya tenga registrado seguirá funcionando y seguirá recibiendo un rechazo en
las posiciones hasta que su petición de autorización pida read-only location y el usuario lo
vuelva a autorizar. La base del visor no cambia: location:read sigue sin ser algo que un
miembro reciba de forma predeterminada. Vea Acceso de IA (MCP).
4. Busque estas operaciones GraphQL en cualquier cosa que haya escrito contra la API:
| Operación | Qué cambió |
|---|---|
createCommand | Devuelve CreateCommandResult! en lugar de Command!. El comando está ahora bajo un campo command, junto a un campo rejection que explica un rechazo. |
updateDeviceType | Su argumento request es ahora un DeviceTypeUpdateRequest! obligatorio, y con él cambió la semántica: esto es una actualización parcial. Un campo omitido ahora CONSERVA su valor almacenado en lugar de borrarlo, y un null explícito lo limpia. Así que un cliente que limpiaba un campo dejándolo fuera ahora tiene que enviarle null — y, en el otro sentido, renombrar un tipo ya no desvincula el perfil a través del cual sus dispositivos resuelven sus capacidades. Un cliente escrito contra el comportamiento anterior de registro completo — uno que lee el tipo y devuelve todos los campos — sigue funcionando y sigue escribiendo lo que envía. token también ha desaparecido de la entrada, así que una actualización ya no puede mover el token de un tipo. Los campos no reconocidos dentro de la solicitud se rechazan en lugar de ignorarse. |
assertedActiveDeviceStates | Sustituida por assertedDeviceStates, que toma activeOnly y pagina mediante afterId y pageSize. |
deviceCredentials, deviceCredentialsById, deviceCredentialsByToken | Ahora requieren device:write. Para un tipo de credencial el identificador legible es el token portador, así que device:read — que tiene todo miembro habilitado — bastaba para abrir una sesión en el broker como cualquier dispositivo del inquilino. |
locationEvents | Ahora requiere location:read, como arriba. |
geoFenceSetSnapshot, currentGeoFenceSet | Su campo fences ahora está paginado: toma un argumento pagination obligatorio y devuelve results junto a un registro pagination, en lugar de una lista simple. Lea páginas hasta que pageEnd alcance totalRecords. Un conjunto de geocercas en los límites documentados es mayor de lo que puede transportar una sola respuesta, así que la forma de lista no podía devolverse en absoluto para los inquilinos con más probabilidad de pedirla. |
Cualquier consulta ...ById(ids: []) | Una lista de ids vacía ahora no devuelve nada. Antes devolvía la tabla entera, sin paginar. |
5. Deje de rellenar los ids con ceros a la izquierda. Un argumento id se interpreta
ahora como un número decimal y nada más. Antes se interpretaba deduciendo la base del propio
literal, así que un "017" rellenado con ceros — exactamente lo que envía un cliente que
formatea los ids a un ancho fijo — se leía como octal y resolvía a la fila 15: la entidad
equivocada, devuelta con éxito y sin ningún error que lo delatara. "0x2", "0b101" y "1_0"
se aceptaban de la misma manera. Las cuatro formas se rechazan ahora de plano. Envíe "17".
6. Cuente con que todos los pods de servicio se reinicien, una vez. El documento de
configuración de instancia que se entrega a los servicios tiene ahora eliminada la coordenada
de cualquier área funcional que este despliegue no habilitó. En un despliegue sin
ai-inference — es decir, todos los perfiles salvo full — eso cambia los bytes del documento
y por tanto la anotación de suma de verificación que reinicia los pods, así que el
la actualización reinicia todos los servicios y no solo aquellos cuya imagen se movió. Es una
actualización progresiva normal y no requiere nada de usted; figura aquí para que un reinicio
completo no se lea como un síntoma.
El motivo de eliminar la coordenada es que un nombre de host de un servicio que nadie desplegó era peor que ningún nombre de host: la superficie de autoría de reglas construía su puerta de lenguaje natural «Describe» contra él, fallaba al resolver el nombre e informaba de que el inquilino no había consentido el enrutamiento externo de IA — culpando a un ajuste del inquilino por un servicio que el operador nunca instaló. Ahora dice que la función no está habilitada en este despliegue, que es la verdad.
Cambios sin cambio de firma
Estos son los que un cliente no puede detectar mirando el esquema.
Actualizar un perfil de dispositivo borra su declaración de ubicación. Un perfil puede
ahora declarar que sus dispositivos informan de su posición, y updateDeviceProfile
reemplaza el perfil completo. Un cliente escrito contra la v0.11.0 no envía el campo
nuevo, así que actualizar un perfil por cualquier motivo — renombrarlo, editar su
descripción — deja de declarar la posición para todos los dispositivos que lo usan, en
silencio. El único síntoma es que las superficies de mapa se quedan vacías. Envíe el campo,
o vuelva a establecer la declaración después de cualquier actualización hecha desde un
cliente antiguo.
updateDeviceProfile ha pasado desde entonces a ser una actualización
parcial: una petición que no
dice nada sobre la declaración ahora la deja como está, y limpiarla requiere un null explícito.
El consejo de arriba es lo que hay que hacer en una instancia v0.12.x o v0.13.x; en una actual
no hay nada que arrastrar. Renombrar un perfil es una mutación
propia.
Las reglas de detección con ventana ya no cuentan lecturas acumuladas de fuera de su
ventana. Las reglas de repetición, de agregado deslizante y de correlación incorporaban una
lectura de cualquier punto del pasado, lo que permitía que una regla que dice «tres lecturas
en diez segundos» se disparara con lecturas separadas por una hora — el detonante habitual
era un dispositivo subiendo su búfer acumulado. Ahora esas reglas descartan una lectura que
llega después de que haya pasado la ventana a la que pertenecía, igual que ya hacían los
agregados de ventana fija y las reglas de sesión. Espere menos alarmas de esos tipos de
regla en cualquier flota que suba por lotes, y consulte detect_late_samples_total para ver
cuánto se está descartando. Las lecturas se almacenan y grafican exactamente igual que antes;
esto afecta solo a la detección. Véase ejecutar el motor de
detección.
La geometría de una geocerca se valida de forma más estricta y se almacena tal como queda escrita, no tal como se envía. Tres cambios, todos en el momento de crear o actualizar una geocerca:
- Una posición debe ser exactamente
[longitud, latitud]. Antes se aceptaba e ignoraba una tercera ordenada o posteriores. - El documento de geometría solo puede llevar las claves que la plataforma lee —
kindygeometryen el nivel superior,typeycoordinatesdentro. Cualquier otra clave antes se almacenaba y nunca se consultaba. - Las coordenadas se reescriben en notación decimal simple antes de almacenarse. Una
coordenada enviada como
1e-300se devuelve como su expansión decimal completa. No se redondea ningún valor y ninguna geocerca cambia de forma, pero un documento leído de vuelta no es idéntico byte a byte al enviado.
Una geocerca también se rechaza ahora si su forma almacenada supera 32 KiB. Eso es aproximadamente el doble del tamaño de una geocerca que use todos los vértices que la plataforma permite, así que la geometría ordinaria no se ve afectada; lo que se rechaza es un documento cuyo tamaño proviene de la notación y no de la forma. La consola siempre ha escrito las posiciones en la forma aceptada, así que las geocercas dibujadas en la consola no se ven afectadas. Las geocercas ya almacenadas no se reescriben y siguen funcionando, pero una que incumpla alguna regla anterior será rechazada la próxima vez que se guarde.
Cancelar un comando registra CANCELLED. Antes registraba EXPIRED, que compartía con
un comando que simplemente agotó su tiempo. Si se bifurca sobre EXPIRED para detectar su
propia cancelación, ya no estará ahí.
Los comandos pueden quedarse ahora en HELD o PARKED. Un comando dirigido a un
dispositivo que la plataforma sabe ausente se retiene en lugar de publicarse, y uno que se
despachó a un dispositivo que resultó inalcanzable se aparca. Ambos están esperando, no
terminados, y ambos son nuevos: el código que trate cualquier cosa distinta de QUEUED o
SENT como terminal se equivocará. El conjunto completo es ahora QUEUED, HELD, SENT,
PARKED, SUCCESSFUL, FAILED, TIMEOUT, EXPIRED, CANCELLED.
Una lectura se almacena en el instante en que se tomó. Cuando un mensaje transporta muchas muestras, cada una con su propia marca de tiempo — toda carga de Sparkplug y LwM2M lo hace, y también cualquier dispositivo que almacene mientras está sin conexión — esas muestras se almacenaban en el instante en que llegaba el mensaje. Ahora se almacenan en el suyo propio. Un dispositivo que sube una hora de lecturas almacenadas las escribe a lo largo de esa hora en lugar de en el momento de la subida, de modo que el historial, las gráficas, la retención y la detección las ven donde realmente corresponden.
Una fuente de presencia que deja de ejecutarse ahora devuelve sus dispositivos. Un dispositivo
marcado como ASSERTED conservaba indefinidamente la presencia que tuviera por última vez: el
barrido de inactividad omite los dispositivos afirmados y un evento de datos no puede cambiarlos, así
que un dispositivo que estaba conectado cuando su fuente desapareció figuraba conectado para siempre,
y uno que estaba fuera de línea tenía sus comandos retenidos para siempre. La presencia MQTT afirmada
por el broker ahora libera los dispositivos que afirmó cuando se la desactiva deliberadamente, o
cuando falta su credencial de cuenta de sistema de NATS, devolviéndolos a INFERRED sin afirmar nada
sobre la conectividad. En una instancia donde eso aplique, espere un evento de cambio de estado por
dispositivo, a ritmo pausado, contado bajo presence_events_total{state="demoted"}, y espere que
esos dispositivos vuelvan a quedar bajo el barrido de inactividad de diez minutos. Sparkplug y LwM2M
no tienen liberación automática: dcctl presence demote y la nueva mutación demoteAssertedPresence
de device-state lo hacen a mano, para cualquier fuente. La mutación necesita un permiso nuevo,
state:demote, que ningún rol tiene de forma predeterminada. Un medidor nuevo,
presence_tap_off{reason}, informa de si la presencia afirmada por el broker está funcionando
siquiera — algo que nada reportaba antes, porque desde fuera una flota en silencio y una toma que
nunca arrancó son idénticas. Vea Devolver un dispositivo a presencia
inferida.
Una lectura reentregada ya no duplica sus filas. La identidad de un evento de medición se deriva de un resumen de su propio contenido, y esa identidad es lo que hace inofensiva una reentrega. Para una lectura que lleva más de una métrica sobre un transporte JSON, el resumen se calculaba sobre un orden que inventaba la plataforma y no sobre el que envió el dispositivo, así que la misma lectura resolvía a una identidad distinta aproximadamente cuatro de cada cinco veces. Cuando la plataforma reentregaba uno de esos mensajes — cosa que hace de forma rutinaria, ante una publicación sin confirmar o un fallo de escritura transitorio — el duplicado no se reconocía: las filas de medición se escribían una segunda vez y los resúmenes horarios las contaban dos veces. Las lecturas de una sola métrica, y las que llegan por Sparkplug o LwM2M, nunca se vieron afectadas. La corrección es solo hacia adelante: los duplicados ya escritos antes de la actualización se quedan donde están, y sus resúmenes siguen inflados. Si tiene gráficas que se veían demasiado altas en dispositivos de varias métricas, este es el motivo, y a partir de la actualización se leerán correctamente.
Toda lista paginada devuelve ahora las filas en un orden declarado. De los 37 puntos finales de lista de la plataforma, 31 no nombraban ningún orden, lo que deja a una lectura paginada libre de entregar la misma fila en dos páginas y no mostrar nunca otra — un defecto real que ya se había reportado dos veces como una pantalla que se reordenaba bajo un operador. Cada lista ordena ahora por una clave total y sin ambigüedad. Si tiene código que dependía del orden incidental que una consulta concreta devolvía por casualidad, verá ahora uno estable, que puede no ser el mismo. Un orden se eligió deliberadamente en lugar de mecánicamente: las credenciales de dispositivo se listan con la de mayor margen restante primero, porque una lectura sin acotar de ellas alimenta la reutilización de credenciales, y ordenar por id habría devuelto la credencial más próxima a caducar.
Un comando respondido en texto plano registra ahora su respuesta. Un dispositivo que
respondía a un comando con algo que no es JSON — acknowledged, una palabra de estado suelta —
hacía fallar la escritura con un error de tipo de la base de datos y dejaba el comando en SENT,
reintentando la misma escritura condenada una vez por minuto durante toda la vida de la fila. El
comando acababa caducando por tiempo contra un dispositivo que lo había respondido
correctamente. Una respuesta así se almacena ahora, sin pérdida, como una cadena JSON. Los
valores que suministra un cliente de la API no cambian: esos deben seguir siendo JSON válido,
porque a un cliente que envía JSON mal formado hay que decírselo.
Los comandos a dispositivos Sparkplug fallan ahora de inmediato en lugar de perderse. La
plataforma no tiene ruta de comandos hacia un dispositivo Sparkplug — esos nodos viven en tu
propia infraestructura MQTT y nada tiende un puente entre ambas — y la comprobación que debía
rechazar uno de esos comandos se comparaba contra un valor que ningún dispositivo lleva jamás,
así que no coincidía con nada y todos esos comandos se aceptaban y luego se iban en silencio a
ninguna parte. Ahora se registran FAILED de inmediato con ese motivo, y se cuentan bajo
command_delivery_undeliverable_total. Espere que los comandos que antes se quedaban hasta su
TTL y registraban TIMEOUT aparezcan ahora como fallos inmediatos. Vea
Comandos.
Un comando cuyo rastro perdió la plataforma se rearma en lugar de culpar al dispositivo. Un
comando podía llegar a SENT y luego no ser alcanzable por nada — el pod que lo publicó muere
antes de registrar el desenlace — y SENT no tenía más salida que el TTL, que registraba
TIMEOUT contra un dispositivo al que nunca se le envió nada. Una pasada en segundo plano
encuentra ahora esos casos y los rearma a PARKED, de modo que se entregan en el siguiente
despertar del dispositivo. command_delivery_stranded_recovered_total lleva una etiqueta
{disposition} que dice dónde acabó cada uno. Esto aplica solo a dispositivos LwM2M: en MQTT
plano, un comando que parece no haber llegado a nada no se distingue de uno que llegó y cuya
respuesta se perdió, así que ahí el comportamiento no cambia y
command_delivery_stranded_skipped_total{reason="transport"} mostrará un ritmo constante que no
es un fallo. Vea cuando la plataforma pierde el rastro de un
comando.
Una acción de regla que la plataforma nunca podrá entregar se descarta en lugar de
reintentarse. Cuando una acción REACT se rechaza por un motivo que ningún reintento puede
cambiar — un sendCommand dirigido a un dispositivo que ya no existe, o a un comando fuera del
vocabulario publicado de ese dispositivo — antes se reintentaba hasta el límite de reentregas y
luego se contaba como envenenada, lo que ponía un error de autoría en el mismo estante que un
fallo de infraestructura. Ahora se descarta al primer rechazo de ese tipo y se cuenta bajo
react_actions_permanently_rejected_total, etiquetada por tipo de acción. Un ritmo sostenido en
ese contador significa que una regla apunta a algo que sus dispositivos no pueden aceptar; el
contador de envenenadas que antes inflaba significa ahora lo que dice.
Una respuesta truncada entre servicios se cuenta. Los servicios leen las respuestas de los
demás hasta un tope fijo de 1 MiB, y una respuesta mayor se cortaba en silencio. Ahora la cuenta
devicechain_svcclient_responses_truncated_total, etiquetada por par. La lectura debería ser
plana en cero; una distinta de cero significa que algún servicio está actuando sobre una
respuesta parcial, algo que conviene saber antes de que el síntoma llegue a una pantalla.
Entradas que antes se aceptaban y ahora no
- Una política de notificación que lleve
deviceTypeToken. Acotar una política a un tipo de dispositivo no está implementado; la escritura antes tenía éxito y luego no entregaba nada. - Una regla de notificación cuya
severityno sea uno de los niveles en mayúsculas o*. Una severidad en minúsculas antes se escribía, se releía sin cambios y nunca coincidía con ninguna alarma. - Un
occurredTimede0001-01-01T00:00:00Z. Es una marca de tiempo válida, y la plataforma la reserva para significar que no se informó ninguna hora. - Un encolado que llevaría a un inquilino por encima de su techo de comandos retenidos. Los
comandos retenidos para un dispositivo ausente se acumulan sin freno natural — la acumulación
de una flota dormida puede quedarse días — y antes nada la acotaba. El límite se resuelve desde
la anulación propia del inquilino, si no la de su nivel, si no un valor predeterminado de
plataforma de 10 000, y no hay ningún valor, en ningún nivel, que signifique ilimitado. El
rechazo lleva el código
HELD_CEILING_EXCEEDEDy es el único temporal que produce la puerta de encolado: se libera a medida que esos dispositivos vuelven. Un cliente que trate todo rechazo como permanente debería tratarlo como caso aparte. Vea cuánta acumulación puede retener un inquilino. - Un encolado que llevaría a un inquilino por encima de la parte de ese techo reservada para la
entrega. Una parte del límite — el 20 % de forma predeterminada — se guarda para la entrega de
comandos de la propia plataforma, de modo que una sola escritura de flota no pueda consumirlo
todo y dejar rechazado cada
sendCommandautomatizado de ese inquilino hasta que la acumulación drene. Todo lo que emite comandos en su nombre queda acotado por el resto: la consola, los SDK,dcctly sus propias integraciones por igual. La consecuencia práctica es que un lote grande que antes se habría admitido entero puede ahora rechazarse en parte; cuando el lote pudo desplegarse parcialmente, su registro dice qué dispositivos no cupieron. Vea una parte del techo está reservada para la entrega.
Arranque inicial y la CLI
Estos llegan a una instancia a través de dcctl bootstrap y de la aplicación de la
infraestructura, no de la versión desplegada, así que ninguno se materializa durante la actualización
descrita arriba. Figuran aquí porque cada uno es un cambio en lo que sale mal.
Un cambio en la configuración del bróker reinicia ahora el bróker. nats-server no puede
recargar en caliente su bloque de callout de autorización ni sus límites de JetStream, y su
negativa es total: abandona la recarga entera, incluido todo cambio no relacionado que viajara
en la misma aplicación. Visto desde fuera eso era la peor clase de nada: la aplicación informaba
éxito, el ConfigMap mostraba los valores nuevos, y el bróker en ejecución seguía con la
configuración con la que arrancó, con la única evidencia en una línea dentro del propio registro
del bróker. Los servicios fallaban entonces al autenticarse contra un ConfigMap que demostraba
que sus credenciales eran correctas. El StatefulSet del bróker lleva ahora en su plantilla de pod
un hash de su configuración renderizada, de modo que el servidor siempre arranca con el archivo
que se le dio. El coste es que los cambios de configuración del bróker reinician ahora esos pods,
donde antes solo lo hacía un cambio de chart o de imagen: presupueste unos 50–70 segundos por
pod, lo que en un bróker de un solo servidor es una interrupción total breve y en tres es un
reinicio continuo.
Las versiones de los charts de terceros están fijadas. ingress-nginx y cert-manager se
instalaban con lo último que hubiera publicado su repositorio, lo que convertía al repositorio de
charts en una dependencia de la planificación además de la aplicación: cuando su host de
artefactos devolvió un 503, el plan falló con un error que no nombraba ni el chart ni la red, y
costó dos arranques fallidos hasta dar con la causa. Quedan fijados en 4.15.1 y v1.21.1
respectivamente — las versiones que ejecuta el clúster con el que se hicieron las pruebas. Si
contaba con recoger una más nueva automáticamente, ahora la actualiza deliberadamente.
Un dcctl compilado por usted tiene ahora una etiqueta de imagen predeterminada utilizable.
make -C backend/cli build producía un binario cuya etiqueta de imagen predeterminada salía del
archivo VERSION del repositorio — un valor que ninguna versión establece y bajo el que nunca se
publicó ninguna imagen. Todas las cargas de trabajo acababan en ImagePullBackOff, varios minutos
dentro de un arranque que había informado progreso sano todo el camino. Un dcctl compilado
localmente usa ahora dev de forma predeterminada, que el guardián de versiones no publicadas
reconoce y rechaza pronto con un mensaje legible, en vez de tarde con uno que no lo es. Un dcctl
publicado nunca estuvo afectado: su etiqueta viene de la propia versión.
Configuración
Una clave se movió. maxEventFutureSkewSeconds limitaba cuánto puede adelantarse una marca
de tiempo informada por el dispositivo respecto al reloj de la plataforma; era un ajuste de
event-processing y ahora es de device-management, porque la hora del evento se decide
ahora en un único lugar, tanto para la detección en vivo como para la reproducción.
Una configuración que siga estableciéndola bajo event-processing arranca con
normalidad y registra una advertencia que nombra la nueva ubicación. El valor anterior no
se aplica: establézcalo bajo device-management si lo había cambiado respecto al valor
predeterminado de 300 segundos.
No se eliminó nada de los valores del chart, así que un archivo de valores v0.11.0 se
aplica sin cambios.
El límite se movió, así que mientras dura este despliegue ninguna de las dos partes lo está
aplicando. En v0.11.0 solo el motor de detección limitaba una hora informada por el
dispositivo; en v0.12.0 solo lo hace la resolución de eventos. Los dos servicios se
despliegan como Deployments independientes, así que existe una ventana en la que un
event-processing de v0.11.0 ya ha sido sustituido mientras un device-management de
v0.11.0 sigue publicando, y un evento que cruce en esa ventana no lo comprueba ninguno.
Lo que cuesta si llega uno con una marca de tiempo desmesuradamente futura: la detección mantiene una única frontera temporal para toda la instancia, así que ese único evento la adelanta y todos los temporizadores pendientes de todos los inquilinos se disparan a la vez. Recuperarse implica reiniciar la instantánea del motor.
Es un límite de una sola actualización, no una debilidad permanente: una vez que ambos
servicios están en v0.12.0 concuerdan de forma definitiva, y una instancia que se destruye
y se recrea nunca queda expuesta. Si va a actualizar en caliente con dispositivos enviando
datos, detenga el tráfico de dispositivos durante el despliegue, o prepárese para reiniciar
la instantánea de detección después.
Un servicio que rechaza su propia configuración sale ahora con un estado distinto de cero.
Antes registraba «refusing to start» y terminaba con estado 0, así que el pod informaba
Completed — exactamente lo que informa un apagado ordenado, e indistinguible de uno a simple
vista. Esos pods entrarán ahora en CrashLoopBackOff. No ha cambiado nada sobre qué
configuraciones se rechazan; lo que cambió es que el rechazo se ve ahora en kubectl get pods,
en un contador de reinicios y para cualquier cosa que alerte sobre ellos. Un servicio que no
consigue apagarse limpiamente se informa igual, por el mismo motivo. Si tiene una alerta que
trata un pod de servicio en Completed como benigno, esta es la versión en la que el fallo
subyacente empieza a llegarle.
v0.12.1: un parche, nada que hacer
La v0.12.1 es una actualización en sitio corriente desde la v0.12.0. No añade ninguna migración, por
lo que la base de datos queda intacta, y no cambia ninguna API, tema, permiso ni clave de
configuración: todo lo que describe la sección de la v0.12.0 anterior sigue siendo exactamente
lo que usted está ejecutando.
Vale la pena conocer dos correcciones:
- Los colores de estado de la consola web ahora cumplen el contraste WCAG AA en los temas
claro y oscuro. Las insignias
pendingyonlinefallaban en ambos temas, y el texto de error fallaba en el tema oscuro. Lo que los colores significan no ha cambiado, pero las insignias rellenas son visiblemente más oscuras, porque esa es la única forma de que las letras blancas sobre ellas resulten legibles. - El monitor de inactividad ya no lee en memoria todos los dispositivos de todos los inquilinos en cada pasada, ni emite una ida y vuelta a la base de datos por cada dispositivo que marca; decide y escribe en una sola sentencia. Los dispositivos pasan a inactivos según exactamente el mismo calendario que antes —esto es un cambio de coste, no de comportamiento— y se nota sobre todo en flotas grandes y en los momentos justo después de que una fuente de presencia devuelva sus dispositivos.
v0.13.0 — los límites de geocercas pasan a formar parte de su plan
v0.13.0 es una actualización en sitio corriente, y no cambia ningún tema, permiso ni clave de
configuración.
Sí cambia la base de datos, de forma aditiva: crea una tabla para las formas de las geocercas, añade tres columnas anulables al registro del inquilino y reescribe una sola vez, en su sitio, el historial de geocercas almacenado para adaptarlo al nuevo formato. No se elimina nada y no hay que recrear nada.
Ese último paso es el que conviene conocer si ya utiliza geocercas. A partir de v0.13.0 la
forma de una geocerca se almacena una sola vez y se referencia por su contenido, en lugar de
copiarse en cada versión de su conjunto de geocercas, y la actualización reescribe el historial
que ya tiene para que lo referencie del mismo modo. Sus geocercas y su historial se conservan sin
cambios; lo que cambia es cómo se almacenan. El paso se puede volver a ejecutar sin riesgo y no
hace nada en una instancia que ya lo haya aplicado.
Lo que cambia es que los dos límites de geocercas que antes eran fijos para todos — 512 posiciones en una geocerca, 100 geocercas por inquilino — ahora son ajustes de su plan, junto a un tercero: un límite sobre el total de posiciones de todo su conjunto de geocercas. Los tres conservan sus valores anteriores de forma predeterminada, así que un inquilino al que nunca se le hayan cambiado queda medido exactamente donde estaba y no tiene que hacer nada.
Dos cosas que conviene saber antes de actualizar:
- El límite del conjunto completo es nuevo, y su valor predeterminado es el que los otros dos ya implicaban: 51.200 posiciones, que son 100 geocercas de 512. Así que un inquilino que use geocercas exactamente hasta los límites documentados queda en el nuevo límite, nunca por encima. El total cuenta formas distintas, así que dos geocercas dibujadas de forma idéntica cuestan una.
- Un cambio solo se rechaza cuando hace un número mayor. Si más adelante un operador baja uno de sus límites por debajo de lo que ya tiene, conserva todas sus geocercas. Editar el nombre o la descripción de una geocerca, y eliminar una geocerca, siempre siguen funcionando: la comprobación es sobre el crecimiento, no sobre el tamaño. Esto es lo que evita que un cambio de plan deje varadas geocercas que eran válidas cuando se dibujaron. Hacer una geocerca más pequeña casi siempre funciona también; la excepción es que el total del conjunto cuenta formas distintas, así que editar una de varias geocercas dibujadas igual la separa del resto y puede subir el total aunque esa geocerca se haya encogido.
Una consecuencia que hay que prever: como eliminar una geocerca baja el total almacenado, un inquilino que esté por encima de un límite y elimine una geocerca no podrá volver a crearla. Para mover una geocerca a otro token, cree primero la nueva y elimine después la antigua, lo que requiere un hueco libre de geocerca durante el momento en que ambas existen.
Los operadores que empaqueten planes deben saber que estos tienen topes reales, porque no todos se
gastan solo en el inquilino: el total del conjunto es una porción de una caché de geometría de la
que tiran todos los inquilinos de la instancia, y el número de geocercas acota un anuncio que tiene
que caber en un solo mensaje del broker. Los rechazos
nombran tanto el número como el ajuste que hay que subir, y una métrica
geofence_cap_refusals_total los cuenta según qué límite rechazó.
v0.14.0 — los paquetes contra los que compila
v0.14.0 es una actualización en sitio corriente desde v0.13.x. No añade ninguna migración, así que la base
de datos queda intacta, y no cambia ninguna API, tema, permiso ni clave de configuración. Si
solo ejecuta la plataforma, no hay nada que hacer.
Lo que cambió está a su alrededor: los artefactos contra los que compila y la CLI con la que la ejecuta.
El runtime web está publicado. @devicechain/client, @devicechain/dashboards,
@devicechain/widgets y @devicechain/brand están en npm, así que embeber un panel o un widget en
su propia aplicación es una instalación y ya no una compilación contra nuestro árbol de fuentes.
Los cuatro se publican juntos en una misma versión y están fijados entre sí. Ver
Paquetes de npm para la línea de instalación y la política de
dist-tags.
Si estaba compilando nuestros widgets desde el árbol de fuentes, hay un cambio que le
corresponde hacer. maplibre-gl ahora es una dependencia peer de @devicechain/widgets: su
aplicación proporciona la biblioteca, la URL de su worker y su hoja de estilos, en lugar de que el
paquete de widgets las decida por usted. Eso es lo que hace que el paquete funcione bajo un
empaquetador que no controlamos — pero significa que un widget de mapa sin cableado del anfitrión
por encima ahora muestra un aviso explícito en lugar de un lienzo en blanco, que es el síntoma que
debe esperar si actualiza sin hacerlo. El cableado es corto y está descrito en
Renderizar un mapa. En el servidor no cambia nada.
El SDK cliente para .NET y Unity está publicado en nuget.org como DeviceChain.Sdk.
dcctl ya puede decirle qué ha arrancado, y apagarlo todo.
# cada instancia, el clúster en el que vive y si ese clúster sigue existiendo
dcctl instances list
# destruirlas todas
dcctl destroy --all
🔴 Esto cierra un defecto sobre el que conviene actuar, no solo conocerlo. Hasta ahora nada
registraba en qué clúster se había arrancado una instancia: se derivaba del nombre de la instancia
al crearla y se volvía a derivar al destruirla. Esa derivación es incorrecta para cualquier
instancia arrancada con --kube-context, y el fallo era silencioso en la peor dirección: dcctl destroy pedía al proveedor que borrara un clúster que no existía, lo cual tiene éxito sin decir
nada, eliminaba el estado local e informaba de que la instancia había sido destruida mientras su
clúster real seguía funcionando. Si alguna vez arrancó con --kube-context y después destruyó
esa instancia, es probable que su clúster siga en pie. dcctl instances list no puede
mostrárselas — la destrucción eliminó el registro local, que es justamente el problema — así que
pregunte directamente al proveedor (para el proveedor local, kind get clusters) y borre lo que
reconozca.
A partir de esta versión el clúster se anota al arrancar y se relee al destruir, y la línea final dice cuál de tres cosas ocurrió: se borró el clúster registrado, el clúster ya no estaba y solo se limpió el estado local, o el registro no era fiable y no se tocó nada. Ninguna de ellas es la frase antigua impresa sobre un clúster que sigue funcionando.
Las instancias creadas antes de esta versión no tienen ese registro y aparecen como no record — destroy will guess the cluster. La destrucción sigue funcionando sobre ellas recurriendo a la
derivación antigua, así que la advertencia anterior sigue aplicando a ellas y solo a ellas.
dcctl destroy ya no elimina clústeresEn las versiones actuales dcctl destroy elimina solo una instancia —su release de Helm, su broker
NATS y su almacén de eventos, su base de datos y su login, su namespace y su estado local— y nunca elimina un clúster ni los requisitos
previos que dejó dcctl install. Por tanto, dcctl destroy --all elimina todas las instancias y
deja todos los clústeres en marcha. Para eliminar un clúster local, usa
kind delete cluster --name <name>. Consulta Eliminar una instancia.
v0.15.0 — las actualizaciones dejan de borrar lo que no envió
v0.15.0 es una actualización en sitio corriente desde v0.14.x. Las migraciones nuevas se
ejecutan solas al arrancar los servicios, no hay nada que recrear y ningún dato debe moverse a
mano.
Los cambios incompatibles están en la API y en el acceso de red saliente, no en la
actualización en sí. Si administra la plataforma y la maneja desde la consola, aquí no hay nada
que deba hacer. Las secciones siguientes son para quienes llaman a la API directamente, quienes
envían notificaciones a través de algo dentro de su propia red, quienes ejecutan el servidor MCP,
o quienes han personalizado la configuración de event-sources.
Las actualizaciones ya no reemplazan el registro completo
Este es el cambio que afecta a más gente, y es la razón por la que esta versión está marcada como incompatible.
Antes, una actualización reemplazaba el registro: cualquier campo que omitiera se borraba.
Ahora un campo que no menciona se deja exactamente como estaba, y borrar un valor requiere un
null explícito.
La petición en sí tiene una forma nueva que ya no lleva el nombre del propio registro —
actualizar y renombrar son operaciones distintas, y ahora existen mutaciones rename… dedicadas
para los cuatro tipos que lo necesitan. Por tanto, una aplicación que llame a la API directamente
debe quitar el nombre de sus peticiones de actualización y regenerar su código cliente.
Una petición con la forma antigua se rechaza de plano, con un error que nombra el campo que ya no se acepta. No se aplica a medias y no falla en silencio: se entera en la primera llamada, y no a partir de un registro que ha perdido la mitad de su contenido.
Una aplicación que borraba un valor omitiendo el campo ahora conserva el valor anterior. Nada
da error; la actualización simplemente hace menos de lo que hacía. Si su código se apoya en la
omisión para borrar un campo, envíe un null explícito en su lugar.
Tenga en cuenta que no todos los campos aceptan null: algunos son obligatorios y lo rechazan con
un error específico. Son campos que nunca podrían borrarse legítimamente.
Si construye una petición de actualización enlazando una variable distinta por campo, una
variable que no suministre llega como null explícito en lugar de como campo ausente — y null
explícito significa borra esto. En el campo rules de una política de notificación eso vacía
todo el conjunto de reglas y devuelve éxito. Enlace el objeto de petición completo como una sola
variable, o incluya únicamente los campos que realmente quiere cambiar.
El id de un evento almacenado ha cambiado
El id de un evento es ahora el identificador propio del evento, en lugar de un valor compuesto a
partir del token del dispositivo, el tipo de evento y la marca de tiempo. Cualquier id que haya
guardado de una versión anterior ya no coincidirá con nada.
La forma anterior tampoco era única: un dispositivo que reportaba dos medidas en el mismo instante producía el mismo id para ambas, de modo que cualquier cliente con una caché normalizada basada en él estaba fusionando esas lecturas en una sola sin avisar. Si guardó ids, vuelva a leerlos; si los usaba como clave, esto es tanto una corrección como una ruptura.
Las conexiones salientes a direcciones privadas ahora se rechazan
Los webhooks de notificación, los relés SMTP y las llamadas HTTP de los conectores ya no pueden alcanzar direcciones de loopback, privadas, de NAT de operador, de enlace local ni de metadatos de nube. La comprobación ocurre en el momento de conectar, y un rechazo es definitivo: no se reintenta.
Esto está activo por omisión y no hay ningún interruptor para desactivarlo.
Este es el fallo con más probabilidad de sorprenderle, porque nada en él se parece a un cambio de política de red: las notificaciones simplemente dejan de llegar, y el fallo queda registrado como permanente en lugar de pendiente. Autorice las direcciones concretas que utiliza:
instance:
config:
infrastructure:
egress:
allowedDestinations:
- 10.96.0.25/32 # el relé SMTP dentro del clúster
Indique cada destino como su propio /32. Los destinos en la internet pública no se ven afectados
y no necesitan entrada.
Si ejecuta el servidor MCP
Dos cambios requieren acción, y uno de ellos impide que el servicio arranque:
- Una URL de recurso con una barra final ahora se rechaza al arrancar. Un identificador se compara de forma exacta, así que una barra final significaba que los tokens quedaban ligados a una dirección que nunca terminaba de coincidir. Antes se aceptaba y luego fallaba de forma silenciosa; ahora falla de forma visible al arrancar. Quite la barra.
- Los metadatos de recurso protegido han cambiado de ubicación, a la que define la
especificación, con el segmento well-known entre el host y la ruta. El chart los enruta por
usted. Si usted mismo termina el ingress, añada una ruta para el prefijo
/.well-known/que no reescriba la ruta.
Se han eliminado dos claves de configuración, y se comportan de forma distinta
debug, dentro de una entrada deeventSources. La configuración se valida de forma estricta, así que dejarla impide queevent-sourcesarranque, con un error que nombra el campo. Elimínela.inboundEventBatchingy susmaxBatchSize/batchTimeoutMs. Esta queda retirada, no rechazada: se descarta al cargar la configuración con una advertencia, así que el servicio arranca con normalidad. Elimínela cuando le venga bien.
La diferencia no es arbitraria: una clave retirada es una que todavía podemos reconocer por su nombre, de modo que podemos descartarla por usted. Una clave anidada dentro de una entrada de una lista no lo es, y por eso la primera tiene que detener el servicio.
También en esta versión
Los comandos se despachan ahora en el momento en que se encolan, en lugar de esperar al siguiente barrido, y el intervalo de ese barrido es configurable si quiere cambiar cada cuánto se ejecuta la red de seguridad. Los mensajes no entregados (dead letters) se pueden leer y consultar, no solo contar. Hay una vista de informes a la que puede apuntar una herramienta de BI. Los activos incorporan jerarquía padre/hijo y un contrato de propiedades documentado, los dispositivos incorporan una operación de sustitución, las alarmas incorporan reconocimiento masivo, y un inquilino puede elegir el idioma con el que abre su consola.
Los paquetes npm publicados y el SDK para .NET/Unity no llevan cambios de código en esta versión. Ahora bien, si su propio código envía mutaciones de actualización a través de ellos, ese código sí tendrá que regenerarlo usted.
v0.16.0 — los dispositivos deben nombrar el despacho que responden
v0.16.0 es una actualización en sitio corriente desde v0.15.x. Una migración nueva se
ejecuta sola al arrancar command-delivery: añade una columna con un valor por defecto, rellena
las filas existentes en la misma sentencia y no requiere nada de usted.
Hay una comprobación previa que conviene hacer antes de actualizar y un cambio incompatible que afecta a los dispositivos, no a quienes llaman a la API. Más allá de eso, esta versión trata sobre todo de servicios que se niegan a arrancar ante configuraciones que antes se aceptaban y se ignoraban en silencio — lo cual es más seguro, y también puede detener un pod que llevaba meses funcionando sin problemas.
Antes de actualizar: revise si sus puertos de escucha colisionan
event-sources ejecuta más de un servidor HTTP en un mismo proceso — GraphQL en su propio puerto,
más cada fuente de eventos HTTP que tenga configurada. Hasta ahora, que dos de ellos coincidieran
en el mismo puerto mataba un transporte de ingesta en silencio: el servidor perdedor moría
dentro de una gorutina y nunca se volvía a mencionar.
Ahora los enlaces son síncronos y un fallo es fatal, de modo que una colisión que llevaba meses rota hace que el despliegue entre en ciclo de reinicios. Ese es el comportamiento correcto y es también el cambio que con más probabilidad le sorprenderá, porque hoy nada le avisa de ello.
Compare el port de cada fuente con los demás y con el puerto de GraphQL, y compruebe que la
entrada extraPorts del chart coincide con el port de cada fuente. Un port: "0" de cara al
dispositivo también se rechaza ahora.
Todo dispositivo que responda a un comando debe devolver el nonce del despacho
Este es el único cambio incompatible de protocolo de la versión, y la población a la que afecta son los dispositivos construidos fuera de este repositorio — firmware, pasarelas, cualquier cosa que hable directamente el protocolo de comandos.
El sobre de entrega lleva un dispatchNonce. Un dispositivo que responda a ese comando debe ahora
devolver el mismo valor en el sobre de su respuesta. Una respuesta que lo omita, o que nombre un
despacho que el comando ya ha abandonado, se rechaza y se registra como carta muerta en lugar
de resolver el comando.
El motivo es un defecto real, no una cuestión de orden: el mismo comando puede publicarse legítimamente más de una vez — devuelto a la cola y despachado de nuevo — y sin un nonce no hay forma de saber a qué despacho pertenece una respuesta. Una respuesta a un despacho ya superado estaba resolviendo el más reciente con el resultado del anterior.
La plataforma no puede enumerar dispositivos construidos en otro lugar, así que los cuenta por usted. Después de actualizar, vigile:
devicechain_commanddelivery_command_delivery_responses_without_nonce_total— respuestas que no nombraron ningún despacho. Sobre todo dispositivos sin actualizar, y en una flota donde todos hablan el contrato actual debería ser cero. Cuenta cualquier respuesta sin nonce, así que una respuesta duplicada o tardía a un comando ya resuelto también acaba aquí.devicechain_commanddelivery_command_delivery_responses_stale_nonce_total— respuestas que nombran un despacho que el comando ya abandonó. La lectura habitual es que se están publicando comandos más de una vez, que es el defecto que este cambio corrige; un dispositivo que reenvía una entrada antigua de su bandeja de salida también lo produce.
(El command_delivery duplicado no es una errata: la serie lleva como prefijos el espacio de
nombres de la plataforma y el área funcional, así que el nombre de arriba es el que se pega en una
consulta.)
Una respuesta rechazada no se descarta. Se escribe como carta muerta, porque el informe del dispositivo sobre lo que hizo no existe en ningún otro sitio — así puede consultar esas respuestas mientras resuelve el primer contador.
Si sus dispositivos usan el SDK de .NET/Unity, actualizar el SDK es toda la solución: lleva el nonce por usted en ambos sentidos. El adaptador de bajada LwM2M y el simulador de dispositivos se actualizaron en el mismo cambio. El agente de borde no se ve afectado: envía telemetría y no recibe comandos.
Junto a ellos llega un contador relacionado:
devicechain_commanddelivery_command_delivery_responses_not_answerable_total cuenta una respuesta
que nombró el despacho en el que está su comando y aun así no pudo resolverlo. Ningún estado de
comando del vocabulario actual produce eso, así que debería marcar cero; existe para detectar
que se añada más adelante un estado sin que nadie haya decidido si una respuesta puede resolverlo.
Ahora los servicios se niegan a arrancar ante cosas que antes aceptaban
Cada uno de estos es una corrección que falla de forma cerrada, y cada uno puede detener un pod que antes funcionaba:
| Qué | La condición que ahora se rechaza |
|---|---|
| Almacén de secretos | una clave raíz de instancia bien formada pero incorrecta — antes arrancaba y fallaba en el primer secreto que se le pedía |
| Configuración de instancia | una clave mal escrita — antes se descartaba en silencio y se aplicaba el valor por defecto |
| Configuración de instancia | DC_SHUTDOWN_DRAIN_SECONDS todavía definida — la variable de entorno ya no existe; el valor es infrastructure.shutdown.drainSeconds |
| Configuración de instancia | una ventana de drenaje mayor que la mitad del terminationGracePeriodSeconds del pod |
| Puertos de escucha | dos servidores en un mismo puerto, o un port: "0" de cara al dispositivo |
| Cualquier servidor HTTP | un puerto ya en uso — antes se registraba desde dentro de una gorutina mientras el servicio informaba de un arranque correcto |
La de la clave mal escrita merece un momento. Una errata en maxSubscriptionMessageBytes redujo a
la mitad el techo real de trama sin registrar nada — la clave se descartaba y se aplicaba el valor
por defecto, lo que se parece exactamente a una configuración que funciona. La decodificación
estricta hace que se entere al arrancar.
Métricas: once series nuevas, ninguna renombrada ni eliminada
Todas las series que existían en v0.15.0 conservan su nombre exacto — nada se renombró y nada desapareció, ni siquiera con el cambio que dio a cada servicio su propio registro de métricas.
Lo nuevo: cinco contadores en command-delivery (los dos del nonce de arriba, más despachos
agotados, respuestas que el estado del comando no pudo aceptar, y respuestas perdidas porque no se
pudo escribir una carta muerta), dos contadores de cartas muertas de alarma en device-management,
un contador de cierre temprano en event-sources, is_serving en lwm2m-ingest, y los dos
contadores de renacimiento de Sparkplug de más abajo.
is_leader cambia de significado en lwm2m-ingest, y la documentación recomienda alertar sobre élEl indicador se levanta ahora cuando la réplica adquiere el arrendamiento, en lugar de después
de terminar de construir su mandato. Construir un mandato tarda hasta 30 segundos por cada
inquilino asignado, así que una réplica que acababa de ganar una conmutación por error informaba
antes is_leader=0 durante hasta 30 segundos por inquilino mientras sí tenía el arrendamiento.
Si sigue la alerta sum(devicechain_lwm2mingest_is_leader) != 1 que recomienda la guía de
despliegue, esa ventana de falsa ausencia de líder desaparece. El nuevo indicador
devicechain_lwm2mingest_is_serving es lo que ahora distingue «líder, todavía construyendo su
mandato» de «líder y sirviendo» — el estado que un solo indicador no podía expresar. is_leader == 1
con is_serving == 0 de forma sostenida es un líder atascado en su construcción. Tenga en cuenta
que el chart no incluye ninguna regla de alerta para ninguno de los dos: esto es orientación para
que la escriba usted, no una regla que hereda.
sparkplug-ingest añade rebirth_enqueued_total y rebirth_dropped_total. Una cola de
renacimiento saturada era antes indistinguible de una inactiva en todas las series que el servicio
exportaba, porque rebirth_requests_total cuenta publicaciones exitosas — así que la saturación
hacía que dejara de subir. Lea el par nuevo en conjunto: descartes que suben mientras las
solicitudes se mantienen en un techo es un abanico de salida que supera a un publicador sano;
descartes que suben mientras las solicitudes siguen planas apunta a la conexión con el broker.
Otros comportamientos que conviene conocer
- Un comando que la plataforma no puede publicar ahora falla. Antes alternaba entre en cola y
enviado en cada barrido hasta que su TTL vencía días después, y entonces registraba un tiempo de
espera agotado — lo que dice que un dispositivo no respondió, cuando nunca se había despachado
nada. Ahora se detiene en un límite (20 intentos por defecto, unos diez minutos con la cadencia
de barrido por defecto de 30 segundos) y registra el fallo nombrando a la plataforma. El límite
puede cambiarlo en
functionalAreas.command-delivery.config.maxDispatchFailures. - El
reasonde una carta muerta por un destino de conector bloqueado es ahoraunprocessableen lugar deexhausted. Actualice cualquier alerta o consulta guardada que use el valor anterior; los registros existentes se leen igual que antes. - Un cambio de estado de alarma que no se pudo publicar ahora se registra como carta muerta y se
cuenta, y
alarm_event_dead_letter_lost_totalse suma a la alertaDeadLetterWriteLost. - Un mensaje entrante que no se puede decodificar ya no se archiva entero. El registro apunta al original por asunto y número de secuencia del flujo.
- Las suscripciones GraphQL se cierran limpiamente al apagar con una trama
1001, y una trama entrante tiene ahora un tope —infrastructure.graphql.maxSubscriptionMessageBytes, 4 MiB por defecto. Es el único valor nuevo del chart en esta versión, y tiene valor por defecto. - El refresco de gobernanza tiene un límite de ritmo — 50 consultas/s con una ráfaga de 100, por dimensión gobernada, y una caché negativa de 10 segundos tras una consulta fallida. Un resolutor mantiene al día a unos 3000 inquilinos sin llegar nunca al límite. Por encima, un inquilino ya resuelto sigue sirviendo su último valor conocido en lugar de caer al valor por defecto de la plataforma; solo un inquilino que nunca se ha resuelto recibe el valor por defecto.
- Aparte, un techo de ingesta por defecto de la plataforma de
0se eleva ahora a 100 mensajes/s con una ráfaga de 200, en lugar de no admitir nada. Es un eje distinto del ritmo de consultas anterior. - El apagado tiene un límite derivado del periodo de gracia, menos la ventana de drenaje y un margen de dos segundos, y respeta la cancelación en todo momento. Los bucles de lectura de los consumidores aplican espera creciente y luego hacen fallar el proceso, en lugar de girar en vacío o reintentar indefinidamente.
- Dos detalles del chart con los que es fácil tropezar.
instance.config.infrastructure.metrics.httpPortestá retirada — un documento que aún la lleve registra un aviso nombrando la clave y arranca con normalidad, y el chart ya no la escribe. Yinstance.config.infrastructure.shutdownla escribe ahora el chart por usted a partir deshutdownDrainSecondsyterminationGracePeriodSecondsde nivel superior; definir ese bloque a mano hace quehelmfalle al renderizar, en lugar de discrepar en silencio con la especificación del pod. Si aporta la configuración de instancia medianteinstance.existingSecret, ese bloque le corresponde añadirlo a usted. - Un pod que se termina a sí mismo libera su arrendamiento de liderazgo al salir. En
lwm2m-ingestse han corregido las dos rutas que salían mientras aún lo tenían. La espera de 30 segundos antes de que un sustituto pueda tomar el relevo es ahora lo que sigue a una pérdida abrupta — un fallo de nodo, unkill -9— no lo que sigue a un pod que decide detenerse.
Los paquetes publicados
El SDK de .NET/Unity incorpora el cambio del nonce de comando descrito arriba; actualizarlo es la forma de que un dispositivo construido sobre él siga respondiendo a comandos.
@devicechain/client, @devicechain/dashboards, @devicechain/widgets y @devicechain/brand no
tienen cambios de código en esta versión. Una cosa que conviene saber si instala
@devicechain/widgets por su cuenta: su rango de dependencia par maplibre-gl pasa de ^6.6.0
a ^6.7.0. Si fija maplibre-gl en 6.6.x verá un aviso de dependencia par no satisfecha, o un
fallo de instalación con un gestor de paquetes que las exija estrictamente. Nada más cambió en los
paquetes.
v0.17.0 — las instancias dejan de ser dueñas del clúster en el que se ejecutan
🔴 No hay actualización en sitio a la v0.17.0. Toda instancia creada por la v0.16.0 o una
versión anterior hay que destruirla y volver a crearla: dcctl upgrade se niega e imprime la receta
en lugar de hacer algo a medias. La siguiente sección es la que hay que
leer, e indica qué exportar antes: no existe ningún camino que conserve su telemetría, sus
definiciones de dispositivo ni sus paneles a través de esta versión.
Lo que sigue es lo que cambia para usted una vez que esté en ella.
dcctl bootstrap son ahora dos comandos
dcctl install <provider> prepara un clúster, una sola vez. dcctl bootstrap <provider> <instance> crea una instancia sobre un clúster ya preparado, tantas veces como instancias
quiera.
dcctl install local
dcctl bootstrap local my-instance
Todo lo que dimensiona o da forma al clúster se trasladó a install y queda registrado ahí, así que
--ha, --compact, --no-monitoring, --no-cnpg y --max-connections se fijan una vez y
todas las instancias del clúster los siguen. Un bootstrap ya no tiene banderas para ellos. dcctl bootstrap se niega en un clúster donde install no ha terminado, y la negativa nombra el comando
que hay que ejecutar.
dcctl destroy desmonta ahora la infraestructura propia de una instancia y deja el clúster en
pie. --keep-cluster desapareció, porque describe lo que destroy hace siempre.
Una actualización futura son dos comandos, y el primero es el del clúster
El operador se movió con la separación. Es un solo controlador por clúster, compartido por todas
las instancias que haya en él, así que dcctl install es lo que lo coloca y lo que lo mueve:
dcctl install local --version <new-version>
dcctl upgrade local <instance> --version <new-version>
dcctl upgrade ya no aplica el operador. Lee el que lleva el clúster y rechaza una instancia
cuyo clúster no tenga operador, o tenga uno identificablemente de otra versión, nombrando el comando
de instalación que hay que ejecutar primero. La razón importa si ejecuta varias instancias en un
clúster: una actualización que aplicara el operador ella misma lo movía para todas las instancias
del clúster, en silencio, como efecto secundario de actualizar una.
Hay un caso que deja pasar con un aviso. Un operador que usted instaló a mano no lleva constancia
de qué versión lo puso, y dcctl no puede distinguir eso de un operador que un dcctl más antiguo
sobrescribió, así que imprime una nota con el comando de instalación y continúa. Si usted no lo
instaló a mano, tome esa nota como el rechazo que habría sido.
Un clúster aloja ahora tantas instancias como usted cree
Cada instancia recibe un namespace propio —dci-<instance>, no el id de la instancia a secas—
con su propio bróker, su propio almacén de eventos y su propio login y base de datos en el almacén
relacional compartido. Dos cosas del clúster siguen pudiendo pertenecer a una sola instancia, y un
bootstrap se niega en lugar de colisionar: el host de ingress y el NodePort MQTT local.
El prefijo es la razón de que el namespace no sea simplemente el id de su instancia: una instancia ya no puede recibir un nombre que colisione con un namespace que usa el propio clúster.
Si concede a dcctl un RBAC explícito
En un clúster que administra otra persona, dcctl necesita verbos que antes no necesitaba: list,
patch y delete sobre instances.core.devicechain.io junto a get, create y update, además
de list sobre secrets en dc-system. Cada bootstrap y cada actualización preguntan ahora al
clúster qué instancias aloja ya y qué han reclamado, y esa pregunta es un list. Una cuenta que solo
tenga el conjunto documentado hasta ahora se rechaza a mitad de un bootstrap.
Dos cosas que se movieron y una que desapareció
- El inicio de sesión único de Grafana a través de DeviceChain desapareció. A Grafana se llega
haciendo port-forward de su Service —no hay ruta de ingress— y se entra con una credencial de
administración por clúster que está en el Secret
dc-grafana-admin. Consulte Observabilidad. - El estado local de
dcctlse movió. Los registros por instancia están bajo~/.devicechain/instances/<instance>/, y un directorio nuevo por clúster,~/.devicechain/clusters/<cluster-uid>/, guarda el estado de infraestructura del propio clúster. Ese directorio se indexa por la identidad del clúster y no por su nombre, y es la única copia de ese estado: ningún respaldo lo contiene. Un clúster instalado solo puede reinstalarse desde la máquina que lo guarda. Consulte Instalar el clúster. - Un bootstrap pregunta al almacén relacional qué contiene ya antes de acuñar una clave raíz. Una base de datos que esté ahí con el nombre de la instancia solo puede haber sobrevivido al clúster que la creó, así que el bootstrap se detiene en lugar de acuñar una clave que no podría descifrar las filas que ya están ahí.
Instancias creadas por la v0.16.0 y anteriores
Una instancia arrancada por la v0.16.0, o por cualquier versión anterior, no se puede
actualizar a la versión posterior a la v0.16.0. dcctl upgrade se niega en lugar de
intentarlo.
dcctl bootstrap registra ahora una declaración: un objeto con ámbito de clúster que dice
lo que la instancia es — su perfil, su topología, cómo está expuesta y qué áreas funcionales
ejecuta. dcctl upgrade lee esa declaración para saber qué desplegar, que es lo que le
permite mover una versión sin tener que indicarle de nuevo la forma de la instancia. Las versiones hasta la v0.16.0 incluida no escribieron ese registro, así que no
hay nada que la actualización pueda leer.
Y lo dice, en lugar de tratar su instancia como un nombre que no existe:
instance "devicechain" IS in this cluster — named by the DeviceChain Helm releases in this
cluster — and it carries no declaration, so it was built by a release older than the one
that began recording them.
No hay capa de compatibilidad, y antes de la v1.0.0 no la habrá. Aquello con lo que se
configuró la instancia antigua nunca se escribió en una forma que esta versión pueda leer, de
modo que una declaración inventada a posteriori sería una conjetura aplicada sobre una
instancia en funcionamiento.
Para migrar a esta versión, recree la instancia, y también el clúster que la aloja. Las
versiones que construyeron estas instancias no tenían dcctl install: instalaban los
requisitos previos compartidos del clúster como parte de la instancia, y no registraban
ninguna instalación. Por eso dcctl destroy se niega a ejecutar tofu destroy sobre el
estado que escribieron esas versiones y necesita --without-state, lo que aun así deja atrás esos requisitos previos;
dcctl bootstrap rechaza un clúster sin registro de instalación y dcctl install chocaría
con lo que dejó la versión anterior. Parta de un clúster nuevo entre medias:
# Exporte antes lo que necesite: esto descarta las bases de datos.
dcctl destroy local devicechain --without-state # elimina la instancia; el clúster antiguo va después
kind delete cluster --name devicechain # y el clúster que preparó la versión anterior
dcctl install local # prepara un clúster nuevo
dcctl bootstrap local devicechain
Para un clúster al que se llega con --kube-context, que dcctl nunca elimina, pase también
ese --kube-context en la línea de destroy —el motivo está en la nota siguiente—; después
borre y recree el clúster con lo que lo creó, y pase el mismo --kube-context a install y
bootstrap. dcctl upgrade imprime esta misma receta cuando se niega.
dcctl de esta versión no lee el estado local de la versión anteriordcctl guarda ahora lo que sabe de una instancia en ~/.devicechain/instances/<instance>/.
Las versiones hasta la v0.16.0 incluida lo guardaban un nivel más arriba, en
~/.devicechain/<instance>/, y el nuevo dcctl no lee, no lista ni elimina ese
directorio: deliberadamente no hay migración, porque dcctl no puede distinguir un
directorio de instancia antiguo de uno que usted haya creado por su cuenta. De ello se siguen
tres cosas para la receta anterior:
-
dcctl instances listno muestra ninguna de sus instancias antiguas. En una máquina que solo tenga instancias construidas por lav0.16.0o una versión anterior imprimeNo DeviceChain instances on this machine (nothing under ~/.devicechain/instances).No se ha perdido nada; las instancias siguen en sus clústeres, yhelm list -Asigue mostrando sus releases. -
destroyadivina el clúster. El registro de clúster que escribió la versión anterior está en el directorio que el nuevodcctlno lee, así quedestroyimprimeNo record of which cluster instance "<instance>" lives in — GUESSING cluster … from its namey lo deriva del nombre de la instancia. Esa conjetura es correcta para una instancia local con el nombre que usa la receta, y errónea para una arrancada con--kube-context, que es el motivo por el que--kube-contextva también en la línea dedestroy.--without-statesigue siendo necesario: el estado de infraestructura también está en el directorio en el quedcctlya no mira. -
El directorio antiguo se queda en disco.
destroyelimina~/.devicechain/instances/<instance>/—que para una instancia así no contiene nada— y deja~/.devicechain/<instance>/donde estaba, con suinfra/terraform.tfstate(la contraseña del superusuario de la base de datos y la clave privada TLS del broker, en claro) y subroker-credentials.json. Una vez que la instancia haya desaparecido, elimínelo usted mismo:rm -rf ~/.devicechain/<instance>No toque
~/.devicechain/escrow/. El artefacto de depósito de la clave raíz siempre ha vivido ahí, fuera de cualquier directorio de instancia, y sigue abriendo las copias de seguridad de la base de datos de esa instancia; consulte Recuperación ante desastres.
La protección de destrucción protege las bases de datos frente a una
operación corriente de helm, no frente a un dcctl destroy deliberado. Si la instancia
contiene telemetría, definiciones de dispositivos o paneles que le importan, vuélquelos antes
de empezar.
dcctl upgrade lee la instancia antes de escribir nada, así que el rechazo ocurre antes del
primer cambio: la versión de Helm se queda en la revisión en la que estaba, el operador sigue
ejecutando la imagen que ejecutaba y todas las filas siguen donde estaban. Ejecutarlo para ver
qué dice no cuesta nada. Ambas mitades —el rechazo y que la instancia quede intacta después—
se comprueban contra un clúster real en cada versión.
Una vez que esté en una versión que registra una declaración, las actualizaciones in situ
corrientes se reanudan. dcctl instances list muestra qué hay declarado y en qué clúster.
v0.18.0 — lo que fallaba en silencio ahora lo dice, y la ingesta sigue el ritmo de su techo
v0.18.0 es una actualización en el sitio desde v0.17.0: dcctl install para el clúster y
después dcctl upgrade para cada instancia que contiene, como se describe para
v0.17.0. Una instancia creada con v0.16.0 o anterior sigue teniendo que
destruirse y crearse de nuevo.
Buena parte de lo que cambia hace visible un fallo que antes pasaba desapercibido: un mensaje perdido ahora se cuenta o se registra como mensaje no entregado, un servicio que no puede hacer su trabajo se reinicia en lugar de seguir listo, y un ajuste que la plataforma no puede respetar detiene el servicio al arrancar. Además, los eventos se persisten y se fusionan en el estado en vivo por lotes, y se resuelven varios a la vez, de modo que una instalación por defecto sigue el ritmo de los 1000 mensajes por segundo que se permiten a cada inquilino.
Quién tiene que hacer algo:
- Todos: cada usuario vuelve a iniciar sesión una vez, y los clientes OAuth, incluidos los asistentes de IA conectados por MCP, vuelven a autorizarse. No hay nada que preparar para ello.
- Toda instancia creada antes de esta versión: revise la contraseña del superusuario después de actualizar. La actualización no la cambia, y puede seguir siendo el antiguo valor por defecto publicado.
- Quien tenga canales de notificación webhook: cada uno necesita una clave
authantes de la actualización, o deja de entregar. - Quien fije en sus valores una clave que esta versión rechaza, fije
resourcespara un solo servicio, haya dimensionado por su cuenta el volumen de JetStream, restrinja de qué registros pueden descargar los nodos o permita que los conectores de los inquilinos lleguen a direcciones privadas: vea Antes de actualizar. - Quien llame a la API de GraphQL desde su propio código o scripts: lea API y
errores. El campo
messagede las alarmas desaparece, varios rechazos llevan ahora un código, y el servidor rechaza algunas peticiones que antes aceptaba. - Quien vigile el recuento de reinicios de los pods o busque en los registros de los servicios:
un servicio cuya conexión con el broker se cierra definitivamente, o cuyas lecturas de mensajes
fallan durante dos minutos, ahora se reinicia en lugar de seguir listo
(Mensajería), y los servicios registran ahora en
info, así que desaparecen líneas de depuración que quizá buscaba (detalles). - Quien enrute o silencie alertas por su nombre: una alerta cambia de nombre y se añaden varias; vea Alertas añadidas y renombradas.
Todo lo demás está en Qué cambia, por área. Cada apartado indica si requiere algo de usted; la mayoría no.
Antes de actualizar
Hágalo en este orden. Cada punto enlaza con el apartado que tiene los detalles.
-
Dé a cada canal de notificación webhook una clave
auth. Un canal guardado sin ella deja de entregar con la actualización, incluido uno que hoy funciona. La versión actual acepta la clave y la ignora, así que añadirla antes no deja ninguna interrupción. En cada inquilino, localice los canales webhook cuyoconfigno tieneauth:query {notificationChannels(criteria: {pageNumber: 1, pageSize: 100, channelType: "webhook"}) {results { token config hasSecret enabled }pagination { totalRecords }}}Si
totalRecordses mayor que 100, lea también el siguientepageNumber. Qué valor usar está en Los canales de notificación webhook deben indicar cómo se autentican. -
Elimine o cambie la configuración que los servicios nuevos rechazan. Cada una de estas impide que un servicio arranque, y el error nombra el ajuste.
dispatchBacklogenfunctionalAreas.outbound-connectors.config: elimínelo (detalles).auth.superuserPasswordenfunctionalAreas.user-management.config: elimínelo (detalles).checkpointIntervalSecondspor encima de 30 paraevent-processing: redúzcalo (detalles).- Un pool de conexiones igual o menor que un nuevo número de trabajadores.
event-managementrechaza untsdbConfiguration.maxOpenConnectionsde 5 o menos, ydevice-stateunrdbConfiguration.maxOpenConnectionsde 5 o menos, salvo que también fijepersistence.writers(event-management) oprojection.writers(device-state) por debajo (detalles).device-managementrechaza unrdbConfiguration.maxOpenConnectionsde 10 o menos salvo que fijeresolution.workerspor debajo (detalles).
-
Una instalación solo con el chart de los perfiles
telemetryoingest-only: fije una clave raíz de la instancia. El chart ahora se niega a renderizar cualquier perfil sin ella. Una instancia creada condcctl bootstrapya tiene una (detalles). -
Si sus valores fijan
resourcespara un solo servicio, revise los pods renderizados. Losresourcesde un servicio se fusionan ahora clave a clave sobre losresourcesde nivel superior, en lugar de sustituirlos, ydevice-managementyevent-managementreciben un límite de CPU propio de 2 núcleos. Por tanto, un límite de CPU de nivel superior por encima de 2 núcleos baja esos dos servicios a 2 núcleos, y unResourceQuotao unLimitRangede namespace que haya añadido puede rechazarlos (detalles). -
Si dimensionó por su cuenta el volumen de JetStream, compruebe su espacio libre. La reserva crece 128 MiB por los recuentos de inicio de sesión, 128 MiB por los recuentos de conexiones MQTT y 64 MiB por el nuevo bucket de caché de device-management hasta que borre los dos que sustituye (16, 16 y 4 MiB en el preajuste compacto). Si no hay espacio suficiente, device-management no arranca (detalles). Una instancia instalada con
--compactconserva su volumen de JetStream de 2Gi:dcctl upgradeno vuelve a aplicar la infraestructura de una instancia, así que el volumen no se redimensiona, y los buckets nuevos siguen cabiendo en él. Solo una instancia creada por eldcctl bootstrapde esta versión recibe el nuevo volumen de 3Gi del preajuste compacto. No borre el StatefulSetdc-natspara redimensionarlo: ningún comando de dcctl lo vuelve a crear en una instancia existente (detalles). -
Asegúrese de que sus nodos pueden descargar de
cgr.dev. El almacén de respaldos dentro del clúster usa ahoracgr.dev/chainguard/minio, fijado por digest; permítalo en sus reglas de salida o replíquelo con ese digest (detalles). Si ejecutadcctldonde no se puede alcanzar el registro de proveedores de OpenTofu, haga que él o un espejo de proveedores sea alcanzable: cadadcctl install,dcctl bootstrapydcctl destroyle pide ahora las versiones fijadas de los proveedores (detalles). -
Si un conector de un inquilino publica en una dirección privada, permítala. Los conectores MQTT, Kafka, SNS y SQS ya no pueden llegar a direcciones de loopback, privadas, de NAT de operador, de enlace local o de metadatos de la nube, incluidos los brokers de Amazon MSK, Amazon MQ y SNS o SQS a través de un endpoint de interfaz con DNS privado. Permita cada dirección como su propio
/32eninstance.config.infrastructure.egress.allowedDestinations. Revise a la vez los esquemas de las URL MQTT y las ACL de Kafka por id de cliente (detalles). -
Fije
outboundMessagesPerSecondyoutboundBurstcon el mismo valor paraevent-processingyoutbound-connectors. Si no, el nuevo avisoConnectorDispatchRateLimitedse dispara cada vez que un inquilino medido con el valor por defecto de la plataforma envía más rápido que el menor de los dos (detalles). -
Actualice sus propios clientes de GraphQL. Quite
messagede las selecciones de alarmas y actualice@devicechain/dashboardsy@devicechain/widgetsjunto con la plataforma (detalles). Llame atenantDeletionscon su nuevo argumento de criterios (detalles). Los demás cambios de la API, enumerados en API y errores, solo requieren atención si su código depende del comportamiento anterior. -
Corrija las condiciones CEL que son verdaderas para todo dispositivo sin un atributo. Una condición de umbral o de duración como
!("tempLimit" in attr) || m["temp"] > attr["tempLimit"]deja de ejecutarse con la actualización. La forma corregida también es válida en la versión actual (detalles). -
Revise las ACL de su broker si ejecuta una fuente MQTT sobre su propio broker o una fuente Sparkplug. Una suscripción que el broker rechaza detiene ahora todo el servicio
event-sourcesen el caso de una fuente MQTT externa (detalles), y mantiene fuera de línea una fuente Sparkplug (detalles). -
Actualice las rutas de alertas, los silencios y los paneles que nombran lo que cambió.
EventProcessingStreamNearFullpasa a llamarseJetStreamStreamNearFull(detalles).JetStreamLeaseBucketNotReplicatedtiene un resumen nuevo (detalles). Las tres alertas de mensajes no entregados pasan a su propio grupo de reglas (detalles). Cinco series de pérdidas de mensajes no entregados se sustituyen por un nombre por servicio (detalles).
Durante la actualización
- Todos cierran sesión una vez. Las sesiones de la consola, del panel y de los SDK terminan en su siguiente renovación, en 15 minutos salvo que haya cambiado la duración del token de acceso, y todos los tokens de acceso y de actualización emitidos antes de la actualización dejan de funcionar cuando termina el despliegue. Los clientes OAuth, incluidos los clientes MCP, tienen que volver a autorizarse (detalles).
- Durante unos segundos, las peticiones pueden rechazarse con
401 invalid or expired token, y un inicio de sesión coninvalid or expired token, incluso para un token emitido momentos antes. Dura hasta que se detiene el último pod de user-management de la versión anterior. Después, volver a iniciar sesión y reintentar funciona (detalles). - Varias cosas se reinician una vez.
device-managementyevent-managementse reinician pod a pod por sus nuevos límites de CPU (detalles).dcctl installreinicia las instancias del almacén relacional, con una breve interrupción de escritura con--hay una más larga con una sola instancia (detalles), y reinicia el almacén de objetos de respaldos con su nueva imagen, lo que pausa el archivado mientras se descarga (detalles). - Las alarmas pueden retrasarse o perderse si el despliegue se atasca. Un pod de
device-managementde la versión anterior no puede guardar una alarma nueva; la alarma se reintenta aproximadamente una vez por minuto y normalmente la guarda un pod actualizado. Mantenga el despliegue corto. Una pestaña de la consola abierta antes de la actualización recibe un error en las listas de alarmas hasta que se recarga (detalles). - Los comandos LwM2M emitidos a mitad del despliegue pueden retrasarse varios minutos, mientras
lwm2m-ingestycommand-deliveryestán en versiones distintas (detalles). - Publicar o revertir un perfil, o editar una geocerca, puede tardar hasta un TTL de caché en
llegar a todas las réplicas de
device-managementmientras conviven las dos versiones (detalles). - Algunas cosas pueden registrarse dos veces. Un abandono puede registrarse como mensaje no
entregado una vez por un pod de la versión anterior y otra por el aviso del broker
(detalles). Un evento sin
occurredTimeque seguía sin confirmar puede guardarse dos veces (detalles). - Una alerta de mensajes no entregados que estaba pendiente o activa vuelve a empezar cuando su regla pasa al grupo nuevo (detalles).
Después de actualizar
Nada de esto es necesario para que la plataforma funcione. Cada punto limpia algo que la actualización deja atrás.
-
Revise la contraseña del superusuario. En una instancia sin contraseña generada, la actualización imprime un aviso al terminar. Si nunca cambió la contraseña de ese superusuario, sigue siendo
devicechain: inicie sesión y cámbiela (detalles). -
Borre los dos buckets de caché que
device-managementya no usa. Cada uno conserva su reserva hasta que lo borra. Con la CLInatsy un inicio de sesión que pueda administrar JetStream en la cuenta de la plataforma:nats stream rm KV_<instance>_device-management_metric-defs-by-typenats stream rm KV_<instance>_device-management_profile-scope-by-type(detalles)
-
Dé a un almacén de eventos anterior los nuevos ajustes de parada.
dcctl upgradeno vuelve a aplicar las bases de datos de una instancia, así que una instancia creada antes de esta versión conserva los antiguos en su almacén de eventos hasta que lo parchea (detalles). -
Despeje las alarmas que levantó una condición CEL que ahora se rechaza. La regla ya no se ejecuta, así que nada las resuelve (detalles).
-
Busque alarmas que no se levantaron durante el despliegue con
dcctl dead-letters list --kind detection-action --source device-management(detalles). -
Vuelva a enviar toda contraseña de dispositivo que deba empezar o terminar con un espacio. Los valores guardados antes de esta versión se guardaron recortados (detalles).
-
Dé un secreto real a un perfil de aprovisionamiento guardado con un secreto vacío con
updateProvisioningProfile(detalles). -
Busque niveles de alerta guardados mayores que 2147483647, que ahora convierten en error el listado de alertas que los incluye (detalles).
-
Siga protegiendo las copias de seguridad tomadas antes de la actualización. Contienen las claves de firma de tokens que esta actualización retira (detalles).
Qué cambia
El resto de esta sección describe los cambios que un operador o quien llama a la API puede notar, agrupados por área.
Ingesta y resolución
La ingesta HTTP tiene su propia asignación
Las peticiones de ingesta HTTP se miden ahora contra una asignación por inquilino propia. Antes de esta versión consumían la misma asignación que el tráfico MQTT, NATS y de presencia del broker del inquilino. HTTP toma el inquilino de la ruta de la petición y comprueba la credencial del dispositivo solo después de admitir la petición, así que cualquiera que pudiera llegar al puerto 8081 y conociera el nombre de un inquilino podía agotar esa asignación y hacer que se descartara la telemetría MQTT del inquilino, incluidos mensajes que el broker ya había confirmado al dispositivo. Ahora quien haga eso solo puede agotar la asignación HTTP del inquilino.
Esto cambia lo que significa el techo de ingesta de un nivel. En cada réplica de event-sources el
techo ya se aplicaba por separado al tráfico en vivo de los dispositivos y a un atraso que se drena
tras una caída, y HTTP es ahora una tercera asignación junto a ellas. En el peor caso, un inquilino puede
ser admitido hasta al triple de su techo por réplica (Cuándo la ingesta puede admitir a un
inquilino por encima de su techo). Si dimensiona
los techos de los niveles para facturar o por capacidad, téngalo en cuenta.
Exponga el puerto 8081 solo detrás de controles de red, como una NetworkPolicy o un ingress que autentique a quien llama. El chart no lo enruta por su ingress, pero por defecto cualquier pod del clúster puede llegar a él.
Los nombres de inquilino inventados en la ingesta HTTP ya no hacen crecer la memoria, y dos alertas nuevas
El endpoint de ingesta HTTP creaba una asignación de frecuencia separada para cada nombre de
inquilino que llegaba en la ruta de una petición, estuviera confirmado o no, así que un flujo de
nombres inventados podía hacer crecer la memoria de event-sources sin límite. Los nombres que el
plano de control no ha confirmado comparten ahora un conjunto fijo de 1024 asignaciones y, pasado
ese conjunto, una única asignación compartida con el valor por defecto de la plataforma. Los
inquilinos que llegan por MQTT, NATS o LwM2M no se ven afectados (Nombres de inquilino que no se
pueden confirmar).
Se añaden dos avisos:
RateLimiterOverflowInUsese dispara cuando la asignación compartida está en uso.TenantsMeteredAtPlatformDefaultse dispara cuando un servicio lleva 15 minutos sin poder leer los techos de los inquilinos desde user-management y los está midiendo con su valor por defecto de la plataforma (Antes de conocer el techo de un inquilino).
Cada servicio exporta ahora
…_governance_unresolved_admissions_total{dimension,cause}, y event-sources exporta además
…_ratelimit_overflow_admissions_total.
Además, la asignación de un inquilino se mide ahora con un único reloj que nunca retrocede. Un servicio que vaciaba un atraso según la hora de envío de cada mensaje podía admitir antes más que el techo cuando esas horas retrocedían (un cambio de líder del broker entre servidores cuyos relojes no coinciden) o cuando el techo de un inquilino cambiaba a mitad del vaciado; esos mensajes se cargan ahora en la última hora que la asignación ya ha visto, lo que puede descartar algo más pero nunca admite más.
processedTime ahora significa cuándo la plataforma recibió un evento
El processedTime de un evento (en GraphQL, y la columna processed_time de la vista analítica
events) es ahora la hora en que la plataforma recibió el evento. Para MQTT en el broker de la
plataforma, es el momento en que el broker guardó el mensaje. Antes de esta versión era el momento
en que event-sources lo decodificaba. Normalmente los dos difieren en milisegundos, pero tras una
caída de event-sources diferían en toda la duración de la caída.
Un evento enviado sin occurredTime también se fecha ahora cuando se recibió, así que las
lecturas que esperaron en la plataforma durante una caída conservan la hora en que llegaron y no
la hora en que se procesaron. De ahí se sigue:
- Las reglas de detección con ventana pueden perder esas lecturas tras una caída. Las lecturas
que llegan por LwM2M o Sparkplug mantienen la frontera del motor de detección en la hora actual
mientras
event-sourcesestá caído. Cuandoevent-sourcesse pone al día, las lecturas a las que asigna una hora de antes de la caída llegan tarde a las reglas de repetición, de agregado deslizante y de correlación: se almacenan y se grafican, pero no cuentan en esas ventanas, ydetect_late_samples_totalsube. Las lecturas que llevan su propiooccurredTimesiempre se han comportado así. Vea qué significa «cuándo» para el motor de detección. - Los eventos decodificados a ambos lados de la actualización pueden guardarse dos veces. El id
de un evento se deriva de su contenido, incluido su
occurredTime. Un mensaje sinoccurredTimeque se guardó antes de la actualización y se entrega de nuevo después recibe ahora otra hora, y por tanto otro id. Solo puede ocurrir con mensajes que seguían esperando confirmación mientras se desplegaba la actualización. - Las filas escritas antes de la actualización no cambian.
- Los agregados de mediciones colocan un atraso en los intervalos de cuando se recibió. Los agregados se refrescan 30 días hacia atrás, así que nada de una caída más corta queda fuera.
- Las acciones de salida se miden según cuándo su telemetría llegó a la plataforma. Para la
telemetría que esperó durante una caída de
event-sources, eso es ahora cuándo llegó y no cuándo se decodificó. Para un inquilino cuyo único tráfico esperó en el broker, la puesta al día ya no se cobra como una sola ráfaga. Un inquilino que además envió telemetría por LwM2M o Sparkplug durante la caída ya llevó su medidor de salida al presente, así que su atraso se sigue cobrando junto, como antes.
Los eventos que los dispositivos envían por MQTT se reenvían varios a la vez
No hay que hacer nada en la actualización.
event-sourcesmantiene hasta 128 publicaciones en inbound-events esperando al bróker a la vez para los eventos que los dispositivos envían por MQTT al bróker de la plataforma, en lugar de cinco como máximo. El mensaje de un dispositivo se sigue confirmando solo después de que el evento que llevaba se haya guardado, y uno cuya publicación falla se sigue dejando para volver a entregarse. Los eventos enviados por HTTP, y a través de un bróker MQTT externo que usted haya configurado, se publican como antes.- Cuando publicar en inbound-events sigue fallando,
event-sourcesfrena igual quedevice-management: tras una publicación fallida espera medio segundo, duplicándolo hasta dos segundos, y hasta que una publicación tiene éxito envía una cada vez. - Pueden volver a entregarse más mensajes de dispositivos cuando
event-sourcesse detiene de golpe: hasta los 128 que esperaban al bróker, además de los que ya retenía antes. Cada uno lleva el mismo identificador de detección de duplicados que antes, así que un evento que ya se guardó no se guarda dos veces. - Un mensaje de dispositivo que falló en todas sus entregas se sigue enviando a failed-decode, como mucho cuatro a la vez. Uno que llega mientras se envían cuatro se deja para que el bróker lo termine, y se registra como mensaje no entregado en lugar de en failed-decode.
- Los eventos de un dispositivo pueden llegar a inbound-events ligeramente desordenados, como ya podían: cinco decodificadores procesan a la vez los mensajes capturados, y cada réplica publica.
devicechain_eventsources_jetstream_publish_duration_secondsañade una seriemode="pipelined"parasuffix="inbound-events". Observabilidad describe los modos.
Una fuente MQTT sobre su propio broker vuelve a suscribirse tras una reconexión
Una fuente de eventos MQTT que lee de un broker que usted gestiona se suscribía una sola vez, al arrancar. El cliente se reconecta por su cuenta con una sesión limpia, así que tras un reinicio del broker o un corte de red el broker no tenía ninguna suscripción para ella: la fuente seguía conectada, no informaba de ningún problema y no ingería nada hasta que se reiniciaba el pod. Ahora vuelve a suscribirse en cada conexión. La fuente de pasarela propia de la instalación por defecto lee del flujo de la plataforma y no estaba afectada.
- Un broker que rechaza la suscripción tras una reconexión detiene ahora todo el servicio
event-sources, y también uno que nunca la confirma. Kubernetes reinicia el pod, y un broker que sigue rechazándola al arrancar lo detiene de nuevo, como ya ocurría. Por tanto, un cambio de ACL en su broker se ve como un bucle de fallos, y la ingesta HTTP y la del broker de la plataforma caen con él. Compruebe antes de actualizar que la credencial de la fuente puede seguir suscribiéndose a sus temas. Vea Matriz de transportes. - Una conexión que se cae mientras se envía la suscripción se deja a la reconexión del cliente.
- Cuando la presencia del broker no puede recuperarse,
event-sourcesse detiene ahora de forma ordenada. Antes salía de inmediato, sin retirar su disponibilidad, sin detener cada fuente, el servidor GraphQL ni vaciar la conexión con el broker. Ahora hace todo eso antes de salir, y el pod se reinicia como antes (Presencia de dispositivos).
Una fuente Sparkplug con un grupo rechazado sigue fuera de línea
Si el broker acepta la conexión de una fuente Sparkplug pero rechaza su suscripción a cualquiera de los grupos de la fuente (lo más habitual, porque la credencial de la fuente no puede leer ese grupo), la fuente ya no se anuncia en línea. No ingiere ninguno de sus grupos, se desconecta y reintenta con una espera creciente de hasta 30 segundos hasta que se conceden todos los grupos. Por tanto, un solo grupo rechazado detiene toda esa fuente hasta que se corrija la ACL del broker. Ocurre lo mismo si el broker no confirma el propio anuncio en línea. Antes de desconectarse, la fuente publica su estado fuera de línea, para que un anuncio en línea que el broker guardó sin confirmarlo no se quede vigente.
Antes, la fuente se anunciaba en línea con el grupo ausente. Los nodos edge de ese grupo volcaban
entonces sus datos almacenados en una suscripción que no existía, y la fuente marcaba después sus
dispositivos como desconectados por quedarse en silencio. Un contador nuevo,
devicechain_sparkplugingest_subscribe_failures_total, cuenta las sesiones abandonadas: alerte
ante cualquier aumento. La línea de log nombra el grupo rechazado. Si vigila el estado del host
Sparkplug, una fuente con un grupo rechazado aparece ahora fuera de línea en lugar de en línea.
Servicios edge tiene los detalles.
Los eventos resueltos se publican varios a la vez
No hay que hacer nada en la actualización.
device-managementmantiene hasta 128 publicaciones de eventos resueltos esperando al bróker a la vez, en lugar de esperar a cada una antes de enviar la siguiente, así que la resolución de un pod ya no queda limitada a un viaje de ida y vuelta de publicación cada vez. Un evento de entrada se sigue confirmando solo después de que se hayan almacenado todos los eventos resueltos que produjo, y uno cuya publicación falla se sigue dejando para su reentrega.- Un evento resuelto se almacena una sola vez cuando su evento de entrada se reentrega. Cada publicación resuelta lleva un identificador de detección de duplicados derivado de su evento de entrada, así que cuando una publicación se almacenó pero se perdió su confirmación, la copia publicada en la reentrega la descarta el bróker en lugar de almacenarla dos veces. El bróker conserva cada identificador durante dos minutos, lo que cuesta memoria de NATS por cada evento resuelto publicado en ese intervalo, unos 100 bytes cada uno según la medición en proceso.
- Un evento fallido se confirma solo después de que su registro se almacene en el flujo failed-events. Antes, el evento de entrada se confirmaba cuando su registro se entregaba para publicarlo, así que un registro que no se llegaba a publicar se perdía. Ahora el evento de entrada se reentrega o, en su última entrega, se registra como mensaje no entregado. El registro lleva el mismo tipo de identificador de detección de duplicados, así que una reentrega no registra el fallo dos veces.
- Cuando publicar en resolved-events sigue fallando,
device-managementva más despacio en lugar de hacer fallar toda su cola de entrada a máxima velocidad: tras una publicación fallida espera medio segundo, duplicándose hasta dos segundos, y hasta que una publicación tiene éxito envía una cada vez. Las publicaciones que fallaron juntas, como todas las que están en curso cuando se cae la conexión con el bróker, comparten una sola espera. - Los eventos resueltos de un dispositivo pueden llegar al flujo ligeramente desordenados, y ya
podían antes: cada réplica publica, y una actualización progresiva ejecuta dos pods a la vez. El
ajuste
watermarkLatenessSecondsde la detección lo tolera. Un evento cuya publicación falló se vuelve a publicar al menos 60 segundos después, más allá del valor por defecto. - Un histograma nuevo,
devicechain_<area>_jetstream_publish_duration_seconds{suffix, mode}, mide cada publicación en JetStream. Observabilidad lo describe.
device-management resuelve más eventos a la vez
device-management resolvía los eventos entrantes con cinco resolvedores, un número fijado en el
código. Resolver un evento es sobre todo esperar: a que la base de datos autentique su credencial y
después a que el almacén clave-valor del bróker de mensajes devuelva su perfil y sus relaciones, una
consulta tras otra. Así que cinco resolvedores limitaban cuántos eventos por segundo podía resolver
un pod mientras la mayor parte de su CPU seguía libre. En un clúster de pruebas el pod resolvía unos
1600 eventos por segundo con 1,5 de sus 4 núcleos, y los eventos por encima de ese ritmo esperaban
en el pod, unos 140 a la vez, y después en el flujo, con la detección quedándose atrás con ellos.
Ahora ejecuta 10 por defecto, y el número es configurable. Medido dentro del proceso contra un
bróker de tres servidores, con cada consulta tardando 750 µs, 5 resolvedores resolvieron unos 1500
eventos por segundo y 10 unos 2900. Consulte Resolución de eventos.
- El ajuste nuevo es
resolution.workers(por defecto10). Debe ser menor que el pool de conexiones del servicio (rdbConfiguration.maxOpenConnections, 20 si no se indica). Un valor fuera de rango impide que el servicio arranque, y el error nombra el ajuste. El valor por defecto solo se rechaza si fijamaxOpenConnectionsparadevice-managementen 10 o menos: fijeresolution.workerspor debajo antes de actualizar. - device-management ocupa más conexiones a la base de datos mientras resuelve eventos. Cada
resolvedor ocupa una mientras autentica la credencial de un evento, lo que con la autenticación de
dispositivos
requiredpor defecto ocurre con cada evento. Con todos los resolvedores ocupados son ahora hasta 10 conexiones en lugar de 5, del pool que también usan la API GraphQL, las comprobaciones de conexión MQTT y el consumidor que aplica las activaciones y resoluciones de alarmas. Si fijamaxOpenConnectionspor debajo de 20, compruebe que lo que queda les basta. Se permite más de la mitad del pool, y se registra al arrancar. - Un pod en su límite de CPU no gana nada con más resolvedores. Esto solo sube el ritmo donde al pod le sobra CPU.
- Una métrica nueva,
resolve_workers, indica cuántos resolvedores ejecuta el pod, yresolve_inflightpuede llegar ahora a 10.resolve_inflightmantenido enresolve_workerssignifica que los eventos llegan más rápido de lo que el pod los resuelve. - Para volver a una versión anterior: un
device-managementanterior rechaza una configuración que fijaresolution.workers, como rechaza cualquier ajuste que no conoce. Quite el ajuste antes de volver atrás.
device-management usa un solo bucket de caché por tipo de dispositivo en lugar de dos
Las definiciones de métricas y el alcance de reglas de un tipo de dispositivo que se guardan en
caché son ahora un único bucket clave-valor, <instance>_device-management_profile-resolution-by-type,
de modo que cada evento de medición lee el perfil publicado de su tipo de dispositivo una vez en
lugar de tres, y un evento ya no puede validarse contra una versión del perfil y etiquetarse con
otra. No hay nada que hacer salvo que dimensione usted mismo el volumen de JetStream o gestione los
buckets a mano.
- La actualización añade un bucket de caché a la reserva de JetStream (64 MiB por defecto, 4 MiB en el preajuste compacto). device-management crea el bucket nuevo al arrancar, y los dos que reemplaza conservan su reserva hasta que los borre (vea el punto siguiente). Si el volumen de JetStream tiene menos espacio libre que un bucket de caché, la creación del bucket nuevo falla por falta de almacenamiento y device-management no arranca. Una vez borrados los dos buckets antiguos, la reserva queda un bucket de caché por debajo de la de antes de la actualización, que es también lo que reserva una instalación nueva.
- Una instancia actualizada conserva los dos buckets que este reemplaza:
<instance>_device-management_metric-defs-by-typey<instance>_device-management_profile-scope-by-type. Nada escribe en ellos tras la actualización, y sus entradas caducan dentro del TTL de caché con el que se crearon los buckets (60 segundos salvo que se cambiarametricDefCacheTtlSecondsantes de esta actualización), pero cada uno sigue reservando su techo hasta que lo borre. Borrarlos requiere la CLInatscon un inicio de sesión que pueda administrar JetStream en la cuenta de la plataforma;dcctlno tiene un comando para ello:nats stream rm KV_<instance>_device-management_metric-defs-by-typeynats stream rm KV_<instance>_device-management_profile-scope-by-type. Dejarlos solo cuesta esa reserva. Borrar un inquilino los sigue limpiando durante una versión más. - Mientras la actualización está en curso, un perfil publicado o revertido, o una edición de geocercas, puede tardar hasta un TTL de caché en llegar a todas las réplicas de device-management: una réplica de la versión anterior limpia solo los buckets antiguos, y una de esta solo el bucket nuevo. Si una edición de geocercas se pierde así, los eventos de ubicación se marcan con el conjunto de geocercas anterior durante hasta ese TTL.
device-management sigue resolviendo eventos cuando un servidor NATS se cae de la red
No hay nada que hacer en la actualización.
- Una búsqueda en una de las cachés de clave-valor de
device-managementespera como máximo medio segundo, en lugar de los cinco segundos que se permiten a una petición NATS. Cuando un servidor NATS se caía de la red sin cerrar sus conexiones, parte de estas búsquedas se le enviaba y cada una esperaba los cinco segundos completos, así que la resolución de eventos bajaba a unos pocos eventos por segundo durante cerca de un minuto, sin registrar nada. - Una caché que agota el tiempo, o por la que no responde ningún servidor, se omite durante
cinco segundos, y sus búsquedas van a la base de datos.
device-managementregistra una advertencia cuando eso empieza y una línea cuando la caché vuelve a responder. Consulte Cachés que dejan de responder para las cuatro métricas nuevas. - Eliminar una entrada de la caché tras un cambio nunca se omite, y una eliminación que falla
ahora se registra (
A key-value cache eviction failed); antes, un fallo pasaba en silencio. - Resolver un evento que tarda más de cinco segundos ahora se registra como advertencia
(
Event resolution is slow), como máximo una vez cada 30 segundos.
Detección
Una condición de umbral o de duración que es verdadera para todo dispositivo sin un atributo ahora se rechaza
Reglas de detección. Una condición de umbral o de duración escrita en CEL que sería verdadera en
todos los eventos de todos los dispositivos a los que les faltan los atributos que lee, sea cual sea
el contenido del evento, ahora se rechaza al publicar el perfil. La forma habitual es una prueba de
presencia negada unida con ||, por ejemplo
!("tempLimit" in attr) || m["temp"] > attr["tempLimit"], o una prueba de presencia negada sola.
Una regla así levantaba una alarma para cada dispositivo sin el atributo, informara lo que
informara, y la mantenía levantada mientras le faltara el atributo. Eso incluye los dispositivos
cuyo atributo se estableció con algo que no es un número o con alcance CLIENT, no solo los que
nunca lo establecieron.
Qué verá:
- Publicar un perfil que contenga una regla así falla, y el error nombra la condición y el motivo.
- Una regla con esta forma publicada antes de la actualización, incluida una publicada mientras la
actualización se está desplegando, deja de ejecutarse con la actualización. El estado de la
regla la muestra como
COMPILE_ERRORcon el mismo motivo, y el registro deevent-processinganota una línea que empieza porPublished detection rule failed to compile; skipping. - Las alarmas que esa regla ya había levantado siguen activas hasta que las limpie. La regla ya no se ejecuta, así que nada las resuelve.
- Revertir un perfil a una versión publicada antes de la actualización trae de vuelta esa regla como
COMPILE_ERROR. Publique una versión corregida en su lugar.
No se ven afectados los umbrales dinámicos creados en el formulario o en el lienzo; las condiciones
CEL que siguen evaluando el evento, como !("tempLimit" in attr) && m["temp"] > 80.0; ni una
condición usada como filtro en una regla de repetición, de tasa de cambio, de agregado en ventana o
de correlación de área, como !("maint" in attr).
Para corregir una regla rechazada, compruebe el atributo en positivo o escriba el respaldo como su
propia comparación
("temp" in m && ("tempLimit" in attr ? m["temp"] > attr["tempLimit"] : m["temp"] > 80.0)) y vuelva a
publicar el perfil. Consulte
Umbrales dinámicos en una expresión CEL.
También se corrige la documentación de la previsualización. La previsualización no resuelve atributos de dispositivo, así que un respaldo CEL se previsualiza con su respaldo en todos los dispositivos; no se previsualiza como que nunca se dispara.
Las reglas de duración colocan las lecturas tardías según su propia hora
Una regla de duración («temperatura por encima de 80 durante 10 minutos») ahora coloca cada lectura según la hora en que se tomó, no según el orden en que llegó, y descarta una lectura que cumple su condición pero queda más atrás de la frontera del motor de detección que el tiempo de sostenimiento de la regla. Antes de esta versión, una lectura tardía se aplicaba como si fuera la más reciente:
- Una lectura tardía que no cumplía la condición cancelaba un sostenimiento que lecturas más recientes seguían respaldando. Un dispositivo que sube lecturas acumuladas podía retrasar una alarma de duración hasta un sostenimiento completo cada vez, o impedir que se elevara mientras la condición se cumplía todo el tiempo. Ahora se ignora una lectura tardía anterior a la racha, y una que muestra que la condición se interrumpió a mitad reinicia la racha desde la lectura más reciente que la cumplía.
- Una lectura tardía que cumplía la condición podía abrir una racha por encima de una interrupción que el motor ya había visto, y elevar una alarma que las lecturas no respaldaban. Ahora se ignora.
- Una lectura de mucho antes de la frontera podía abrir una racha cuyo sostenimiento ya había
pasado, y elevar la alarma con el siguiente evento. Ahora se descarta y se cuenta en
detect_late_samples_total, cuya descripción ahora nombra las reglas de duración junto a los tipos deslizantes.
Una lectura que no cumple la condición sigue terminando una alarma de duración elevada por tarde que
llegue, salvo que sea anterior a la alarma: una lectura tardía de antes de que se elevara la alarma,
que llega después, no la retira, y se cuenta en detect_late_samples_total cuando se tomó dentro de
la racha que elevó la alarma (una anterior a toda la racha se ignora). Como ahora las lecturas
se colocan donde corresponde, una alarma de duración puede elevarse más pronto o más tarde que antes, según
el orden en que llegaron sus lecturas; ya no se eleva sobre una racha que las lecturas muestran
interrumpida. Como hasta ahora, un episodio solo es seguro que eleve la alarma si dura su tiempo de
sostenimiento más la tolerancia de retraso. Consulte qué significa «cuándo» para el motor de
detección.
Como la frontera es compartida por toda la instancia, un dispositivo cuyas marcas de tiempo van sistemáticamente por detrás del resto de la flota más que el tiempo de sostenimiento de una regla de duración más la tolerancia de retraso, por un reloj lento o un camino lento, nunca eleva esa regla: cada lectura suya que cumple la condición se descarta y se cuenta como tardía. Antes de esta versión esas lecturas se aplicaban.
La previsualización del lienzo se ejecuta sin tolerancia de retraso, así que descarta una lectura tardía sin margen. Ahora indica cuántas lecturas apartó por tardías, para las reglas de duración y los tipos deslizantes.
Lo que cuesta. Para distinguir una lectura tardía de una interrupción, una regla de duración ahora guarda un pequeño registro y un temporizador de caducidad por cada dispositivo que envía la métrica de la regla sin cumplir su condición, hasta un tiempo de sostenimiento después de la lectura más reciente de ese tipo. Antes, un dispositivo así no ocupaba nada. Para un dispositivo que reporta al menos una vez por sostenimiento, el registro es por tanto permanente mientras siga reportando:
- Dos claves vivas por dispositivo y regla de duración, que cuentan en el techo de claves vivas
por inquilino, que se mide y no se impone. Un inquilino con 100 000 dispositivos que reportan bajo
cinco reglas de duración alcanza solo con esto el
maxLiveKeysPerTenantpor defecto de 1 000 000, y entonces se dispara el avisoDetectTenantOverStateBudget. SubamaxLiveKeysPerTenantsi esa es su flota. - Unos 290 bytes de punto de control de detección por dispositivo y regla de duración
(
detect_snapshot_bytes), medidos en proceso con un identificador de regla de 26 caracteres y tokens de dispositivo de 19 caracteres. Crece con la longitud de ambos. - La frontera avanza en cada intervalo de inactividad. Esos temporizadores de caducidad son trabajo pendiente, así que una instancia con una regla de duración y algún dispositivo que reporta su métrica avanza la frontera y guarda un punto de control con el flujo en calma, en lugar de quedarse en reposo.
Un dispositivo con la alarma elevada también conserva su racha hasta que la condición cesa, cuando antes la soltaba al elevarse la alarma.
Un punto de control escrito antes de la actualización se restaura sin cambios, incluida cualquier alarma de duración elevada en ese momento. Volver después a la versión anterior no convierte los registros nuevos en alarmas: solo lee del punto de control las rachas abiertas e ignora el resto. No hay que hacer nada en la actualización.
Una acción que falla ya no detiene las demás acciones de la regla
No hay que hacer nada en la actualización. Lea esto si alguna regla tiene más de una acción.
Antes de esta versión, las acciones de una regla se ejecutaban en el orden en que estaban listadas, y la primera que fallaba detenía al resto. En cada reintento las acciones anteriores se volvían a ejecutar y las posteriores no llegaban a ejecutarse nunca, así que un comando que no se podía encolar durante unos minutos (por ejemplo, porque el inquilino estaba en su límite de comandos retenidos) hacía que nunca se levantara una alarma listada después de él. Ahora cada acción se intenta en cada entrega, pase lo que pase con las demás.
- Una regla ya no necesita listar primero su acción más importante. Las reglas que se reordenaron para sortear esto pueden quedarse como están.
- Una regla que dependía del comportamiento anterior para ejecutar una acción solo si otra anterior tenía éxito deja de tenerlo: cada acción se ejecuta con independencia de las demás.
- Un reintento del motor de detección ya no envía por segunda vez un webhook ni una publicación a un conector, dentro de unos diez minutos desde el primer intento. Las actualizaciones de alarma y las solicitudes a conectores que se vuelven a enviar en ese plazo las reconoce el bus de mensajes y se almacenan una sola vez. Los dos flujos afectados se reconfiguran automáticamente al arrancar la nueva versión; recordar cada solicitud durante diez minutos cuesta memoria de NATS en proporción a la frecuencia con que se disparan las reglas. Pasado ese plazo, y cuando el propio servicio de conectores reintenta una llamada, una solicitud todavía puede llegar dos veces a su destino.
- Mientras una de las acciones de una detección sigue fallando, cada reintento vuelve a cobrar sus acciones de webhook y de conector contra la tasa de salida del inquilino, aunque la solicitud reenviada se almacene una sola vez. Un fallo sostenido, como un inquilino en su límite de comandos retenidos, puede provocar así descartes en las demás reglas del inquilino.
- Una detección que acaba en la cola de mensajes no entregados tras sus reintentos ahora nombra,
en el detalle del mensaje, cada acción que falló en el último intento, por tipo y clave de
idempotencia, como
sendCommand/failed/<clave>(yhttpCall/shed/<clave>para una que la tasa de salida rechazó), la misma forma que usan los mensajes de descarte. El resumen y la descripción de la alertaReactPoisonDroppingse reformulan en consecuencia. - El motor de detección ahora obtiene del bus de mensajes una detección cada vez, así que una detección nunca queda esperando detrás de otra lenta el tiempo suficiente para entregarse dos veces, y un intento contra un servicio que no responde termina cuando termina su entrega en lugar de prolongarse. Cada acción recibe su parte de ese tiempo, así que los comandos a un servicio que no responde no pueden agotarlo antes de que se active una alarma listada detrás de ellos.
Las acciones de salida ya no se pierden cuando el motor de detección se pone al día
Tras un reinicio, un despliegue o una conmutación por error, el motor de detección procesa la telemetría que llegó mientras estaba caído. Las acciones de webhook y de conector de salida de ese atraso se cobraban antes contra la tasa de salida del inquilino como si hubieran ocurrido todas a la vez, así que la mayoría se descartaban y solo quedaba una métrica como registro. Ahora se miden, tanto donde se desencadenan como en el servicio de conectores, según el momento en que la telemetría llegó a la plataforma. Un inquilino dentro de su límite no pierde nada en una puesta al día ni se ve frenado por ella.
Una acción que sigue por encima del límite se registra como mensaje no entregado con motivo shed y
tipo detection-action, hasta aproximadamente un mensaje por segundo por inquilino (60 de golpe) y
diez por segundo en total. Por encima de eso, las acciones se cuentan y se resumen en un mensaje por
inquilino y minuto. Cuatro ajustes de event-processing regulan el presupuesto:
shedLetterPerSecond, shedLetterBurst, shedLetterGlobalPerSecond y shedLetterGlobalBurst. Se
añaden dos avisos: ReactShedLettersOverBudget y RateMeteringClockFallback.
Las detecciones que el motor vuelve a publicar tras un reinicio las reconoce ahora el bus de mensajes
y se almacenan una sola vez dentro de una ventana de 30 minutos, así que los suscriptores del flujo de
eventos derivados ven menos duplicados. Cada evento derivado lleva ahora un campo triggeredAt. Si
filtra los mensajes no entregados por motivo, espere mensajes shed de tipo detection-action. No
hace falta hacer nada en la actualización.
Con una reserva en caliente, solo la réplica que detecta despacha acciones
En un despliegue de event-processing con una reserva en caliente, la reserva se quedaba con una
parte de las acciones de detección (comandos, alarmas y llamadas a conectores) y cargaba las llamadas
a conectores contra su propia copia del techo de salida de cada inquilino, de modo que un inquilino
podía llegar hasta el doble de ese techo. Ahora las acciones solo las despacha la réplica que tiene la
partición de detección. Cuando la partición cambia de réplica, ambas pueden despachar durante unos
cinco segundos como máximo, y una llamada a un conector hecha dos veces en ese intervalo llega dos
veces a su destino.
Con una réplica (el valor por defecto) nada cambia, salvo cuando el pod se detiene sin un apagado ordenado (una caída o una terminación por falta de memoria). Las acciones pendientes de despachar se reanudan entonces cuando el reemplazo toma la partición, hasta unos 35 segundos después, en lugar de en cuanto arranca el reemplazo. La detección se reanuda tras una espera de traspaso adicional y la reproducción, igual que antes de esta versión.
Los techos de tasa de event-sources, outbound-connectors y ai-inference los aplica cada réplica
por separado. Esto ahora está documentado en Gobernanza, y
importa si ejecuta más de una réplica de esos servicios.
checkpointIntervalSeconds tiene un máximo de 30, y algunos fallos silenciosos ahora avisan
event-processing ahora se niega a arrancar cuando checkpointIntervalSeconds es mayor que 30. El
motor de detección confirma su entrada solo cuando guarda un punto de control, así que un intervalo
cercano o superior a la ventana de confirmación de 60 segundos del bróker retenía los mensajes de
un flujo tranquilo hasta que el bróker los volvía a entregar y, tras cinco ventanas, los contaba
como entregas agotadas, lo que dispara ReplayCoveredDeliveriesExhausted con un motor sano. 30
deja margen para el propio punto de control. El límite es un rechazo al
arrancar y no una comprobación del chart, así que helm upgrade con un valor mayor tiene éxito y
después el pod no arranca. Si sus valores lo fijan más alto, redúzcalo antes de actualizar. El valor
predeterminado (10) no se ve afectado.
Algunos fallos que eran silenciosos con el nivel de registro predeterminado ahora registran un aviso:
- un muestreo fallido del tamaño y la replicación de un flujo o de un bucket KV, o de la pérdida sin leer de un consumidor;
- un muestreo fallido del retraso del consumidor del motor de detección;
- una autenticación de dispositivo en el bróker que falló por un motivo distinto de la propia credencial del dispositivo: el almacén de credenciales falla, o una credencial almacenada no tiene secreto y por tanto nunca puede autenticar.
Un dispositivo que presenta una credencial incorrecta, desconocida, caducada o revocada sigue registrándose solo en depuración. Durante una caída del bróker, los avisos de muestreo se repiten en cada pasada, aproximadamente cada 30 segundos por flujo; un muestreo interrumpido por un apagado se queda en depuración. Durante una caída del almacén de credenciales (la base de datos), el aviso de autenticación se repite una vez por cada intento de conexión de un dispositivo, así que una flota que se reconecta durante la caída registra un aviso por intento.
Los cambios perdidos de reglas, dispositivos y atributos se reparan solos
Publicar o revertir un perfil de dispositivo, crear un dispositivo o cambiar su tipo, apuntar un tipo de dispositivo a otro perfil, y establecer un atributo de umbral avisan al motor de detección una sola vez. Antes de esta versión un aviso perdido nunca se reintentaba: una versión de perfil recién publicada no ejecutaba ninguna regla, un dispositivo que nunca informaba no se vigilaba por silencio y un umbral dinámico conservaba su valor anterior, sin nada marcado como fallido y sin alerta. El remedio documentado era volver a publicar el perfil.
event-processing compara ahora su copia de las reglas publicadas, las versiones activas de perfil,
los dispositivos y los atributos de umbral de cada inquilino con device-management cuando asume la
detección y cada cinco minutos después, y corrige lo que difiera. Un cambio perdido se repara
en unos siete minutos como máximo, y un dispositivo o atributo eliminado en unos diez. Ya no hace falta
volver a publicar tras una interrupción del broker.
Lo que verá:
- La actualización añade dos columnas a la base de datos de
device-management: cuándo pasó a estar activa la versión activa de cada perfil, y cuándo empezó la pertenencia de cada dispositivo a su perfil actual. Ambas admiten nulos y se rellenan con el uso, así que la migración es rápida con cualquier tamaño de flota y una réplica de la versión anterior sigue funcionando durante el despliegue. Un perfil cuya versión activa se eligió antes de la actualización se trata como activo desde que se publicó esa versión o, si se revirtió a ella, desde justo después de publicarse la versión más reciente. - Tres avisos nuevos:
DeviceFactPublishFailing(las notificaciones no se están enviando),DetectFactsRepaired(el motor corrigió algo de lo que no se le había avisado) yDetectFactReconcileFailing(la propia comparación está fallando).DetectFactsRepairedcuenta solo una pérdida real —un cambio cuyo aviso simplemente sigue en camino se deja para la siguiente comparación—, así que tras la actualización solo se dispara si de verdad se habían perdido avisos antes. device-managementexportafact_publish_failures_total, yevent-processingexportadetect_fact_reconcile_repairs_totalydetect_fact_reconcile_failures_total.- Una reversión lleva ahora el momento en que se hizo tal como lo guarda
device-management. Cada publicación o reversión de un perfil queda marcada después de la anterior, aunque las réplicas que las hicieron tengan relojes que no coinciden. La excepción son dos cambios del mismo perfil hechos en el mismo instante: cualquiera de los dos puede quedarse con el momento anterior, y el motor de detección acaba igualmente en la versión que guardadevice-management. event-processingllama ahora también auser-managementpara listar los inquilinos y adevice-managementpara leer reglas, dispositivos y atributos, con el mismo secreto de servicio que ya usa para las geocercas. Si el secreto de servicio o alguna de las dos direcciones no está configurado, la comparación queda desactivada y el servicio registra un aviso al arrancar, como ocurre con la evaluación de geocercas.
El lienzo de automatización permite crear reglas de Conectividad y no guarda sobre una regla que no puede mostrar por completo
No hace falta hacer nada en la actualización.
- El lienzo tiene un nodo de Conectividad, así que una regla de «el dispositivo se desconectó» puede construirse allí además de en el generador de formularios.
- Al abrir una regla existente, el lienzo comprueba que guardar conservaría la regla completa. Antes, una regla de un tipo que el lienzo no podía mostrar se abría como un lienzo vacío sin ninguna explicación, y una regla con un campo o un tipo de acción que el lienzo no modela se abría sin él. Guardar desde el lienzo reemplazaba entonces la regla almacenada por la reducida. Ahora el lienzo explica lo que no puede mostrar y desactiva el guardado para esa regla; edítela a través de la API.
- Una regla construida en el lienzo cuya definición se modificó a través de la API se vuelve a disponer a partir de esa definición cuando el lienzo la abre, en lugar de a partir de su disposición guardada anterior, así que guardar desde el lienzo ya no deshace el cambio. El lienzo indica cuándo lo ha hecho. La excepción es un lienzo guardado que ya no compila, que no puede compararse con la regla: se abre como estaba, y su nota advierte que guardarlo deshace cualquier cambio de ese tipo.
- El lienzo conserva una plantilla de clave de alarma definida a través de la API. La muestra en modo de solo lectura y la guarda sin cambios.
- Guardar desde el lienzo ya no borra el nombre ni la descripción de una regla cuando su definición no los contiene. Solo se envían cuando los edita en el lienzo.
Comandos
Las respuestas a comandos que no se pudieron registrar vuelven a registrarse como mensajes no entregados
En v0.16.0 y v0.17.0, command-delivery no podía escribir ni un solo mensaje no entregado. Cada
uno que intentaba se rechazaba antes de escribirse, se contaba como perdido, y la respuesta del
dispositivo desaparecía. Por eso DeadLetterWriteLost podía dispararse con el bróker en buen
estado. Ese caso está corregido, y cambia lo que ocurre con los comandos afectados:
- Una respuesta que no se pudo registrar tras todos los intentos aparece como mensaje no entregado,
y su comando pasa ahora a
FAILED, con un error que dice que el dispositivo respondió y la respuesta se perdió. Antes, ese comando seguía en curso hasta que otra cosa lo resolvía. - Una respuesta que no nombraba ningún despacho, o nombraba uno que su comando ya había dejado atrás, aparece como mensaje no entregado y no resuelve nada. El comando queda como estaba.
Los comandos LwM2M se confirman inmediatamente antes de llegar al dispositivo
Un comando entregado a un dispositivo LwM2M ahora se confirma con command-delivery inmediatamente antes de que el adaptador lo lleve a cabo. Una entrega que la plataforma ya rearmó o reenvió se descarta en lugar de llegar al dispositivo una segunda vez. Una entrega así puede aparecer tarde tras una caída o una conmutación por error, y antes de esta versión podía actuar sobre un dispositivo al que una entrega posterior ya había hecho actuar. Si no se puede contactar con command-delivery, los comandos LwM2M esperan y se reintentan. Nunca se envían sin confirmar.
Lo que cambia y puede ver:
- Cancelar un lote ahora también detiene los comandos LwM2M que se publicaron pero aún no habían
llegado a su dispositivo. Registran
CANCELLED. El resultado de la cancelación todavía los cuenta como ya enviados, porque eso eran cuando se ejecutó la cancelación. - Para un comando LwM2M,
sentTimeahora registra cuándo se le envió realmente al dispositivo. Puede ser posterior a cuando el comando se publicó por primera vez. lwm2m-ingestse niega a arrancar sininfrastructure.commandDeliverysiempre que tenga identidades de dispositivo que atender. El chart siempre lo define, así que solo afecta a una configuración hecha a mano.- Métricas.
lwm2m-ingestañadedevicechain_lwm2mingest_commands_stale_dispatch_total(entregas descartadas porque la plataforma ya había avanzado: una actuación duplicada evitada, no un fallo) ydevicechain_lwm2mingest_command_live_claim_errors_total(comandos no llevados a cabo porque command-delivery no pudo confirmarlos). Se eliminadevicechain_lwm2mingest_command_drain_dedup_total. - Durante la propia actualización, un
lwm2m-ingestnuevo no puede confirmar comandos con un command-delivery que todavía está en la versión anterior. Un comando LwM2M emitido en esa ventana puede retrasarse varios minutos. Si agota sus reintentos antes de que ambos servicios estén actualizados, se rearma y se entrega en el siguiente despertar del dispositivo.
No hay que hacer nada durante la actualización.
Un dispositivo LwM2M lento ya no retrasa los comandos de los demás
Cuando un dispositivo LwM2M tarda en responder y sus comandos se acumulan, los siguientes comandos para él ahora se apartan en command-delivery y se entregan en orden momentos después. Antes de esta versión detenían todo el adaptador: los comandos de todos los demás dispositivos LwM2M esperaban detrás del lento. Los comandos para un dispositivo sin conexión se apartan de la misma manera, sin ocupar el espacio que necesitan los dispositivos conectados.
Lo que cambia y puede ver:
- Un dispositivo conectado recibe el resto de una acumulación larga sin reconectarse. Sus comandos pendientes se entregaban de 32 en 32 por despertar, y el resto esperaba a que el dispositivo despertara de nuevo, cosa que un dispositivo que permanece conectado nunca hace. Ahora se entregan unos pocos cada vez, alternando con los comandos de otros dispositivos, hasta vaciar la acumulación.
- Los comandos de un dispositivo siguen llegando en el orden en que se enviaron. Esto ahora
también se cumple cuando un comando no pudo confirmarse con command-delivery y se reintenta: los
comandos posteriores lo esperan. Dos casos todavía pueden entregar un comando después de los que
se enviaron detrás de él, y ambos requieren que command-delivery esté fallando. Uno es una
interrupción que dura más que los reintentos del comando. El otro es una confirmación que
command-delivery registró pero cuya respuesta nunca llegó a
lwm2m-ingest, por ejemplo porque la petición agotó su tiempo de espera. En ambos casos el comando queda enviado pero sin ejecutar hasta que la plataforma lo detecta como varado, y entonces se entrega en la siguiente conexión del dispositivo, después de los comandos posteriores, o caduca. - Tras una conmutación por error, los comandos enviados mientras los dispositivos se reconectan se entregan un momento después. Los comandos pendientes de cada dispositivo se entregan antes que cualquiera nuevo, así que durante ese breve intervalo los nuevos también se apartan. Es de esperar un breve aumento del tráfico hacia command-delivery tras una conmutación por error.
- Un comando apartado de esta forma muestra el estado
PARKED, aunque su dispositivo esté conectado. Antes de esta versiónPARKEDsolo significaba que el dispositivo no tenía conexión activa. Vea Comandos. - Métricas.
lwm2m-ingestañadedevicechain_lwm2mingest_commands_overflow_parked_total, con una etiquetareason(full,offline,bind,unconfirmed),devicechain_lwm2mingest_command_overflow_blocked_total(el adaptador esperó porque command-delivery estaba lento) ydevicechain_lwm2mingest_command_drain_turns_total. Se eliminadevicechain_lwm2mingest_command_drain_dropped_total: la petición de un dispositivo de sus comandos pendientes ya no se descarta cuando el adaptador está ocupado.
No hay que hacer nada durante la actualización.
Conectores y notificaciones
Los conectores ya no pueden llegar a direcciones privadas, y el servicio de conectores tiene clientes nuevos
Los conectores de MQTT, Kafka, SNS y SQS reciben ahora la misma comprobación en el momento de
conectar que ya tenían los webhooks y los relés de correo. Se rechaza un destino que resuelve a una
dirección de loopback, privada, NAT de operador (carrier-grade NAT), de enlace local o de metadatos
de nube. El rechazo es definitivo: el despacho acaba en la cola de mensajes no entregados como
blocked y no se reintenta. En Kafka esto cubre cada bróker que anuncia el clúster, no solo las
direcciones que usted configuró. La comprobación se hace en el propio servicio, así que ya no
depende de networkPolicy.enabled ni de que su clúster la aplique.
Qué revisar antes de actualizar:
-
Los destinos en direcciones privadas dejan de recibir. Esto incluye:
- un bróker dentro del clúster o en una red emparejada;
- los brókers de Amazon MSK, que son privados por defecto;
- Amazon MQ usado para MQTT;
- SNS y SQS alcanzados a través de un endpoint de interfaz de VPC con DNS privado activado.
Con DNS privado, incluso los nombres predeterminados
sns.<región>.amazonaws.com/sqs.<región>.amazonaws.comresuelven a direcciones privadas, así que también se ven afectados los conectores sin endpoint personalizado.
Permita cada dirección como su propio
/32eninstance.config.infrastructure.egress.allowedDestinations. Un endpoint de interfaz tiene una dirección por zona de disponibilidad, y cada una necesita su propia entrada. Una autorización se aplica a todos los inquilinos y a todas las rutas de conectores y webhooks, no solo a la que usted tiene en mente. -
Las URL de MQTT deben usar
tcp://,mqtt://,ssl://,tls://,mqtts://,ws://owss://, con un puerto explícito y un bróker por entrada.tcps://,mqtt+ssl://yunix://ya no se aceptan. Se rechazan al guardar un conector, y un conector almacenado que use uno acaba en la cola de mensajes no entregados comoinvalidcuando se dispara. Las direcciones de Kafka deben serhost:puerto. -
Las variables de entorno de proxy (
HTTPS_PROXY,ALL_PROXY) ya no las usan los conectores, y tampoco las variablesAWS_*ni los archivos de configuración de AWS del pod. -
El cliente de Kafka cambió.
- El identificador de cliente predeterminado es ahora
devicechain(antes erabento). FijeclientIden el conector si sus brókers aplican ACL o cuotas por identificador de cliente. - Los registros sin clave se reparten ahora con particionado pegajoso (sticky partitioning).
- La versión del protocolo se negocia con el bróker en lugar de ser fija.
- La entrega no cambia: confirmación del líder, sin productor idempotente.
- El identificador de cliente predeterminado es ahora
-
Los mensajes de SQS se envían de uno en uno (
SendMessage), no por lotes. El permiso de IAM es el mismo,sqs:SendMessage.
Dos cosas mejoran como efecto secundario. Un bróker de Kafka que está inaccesible brevemente ahora
se reintenta en lugar de acabar en la cola de mensajes no entregados como invalid. Y el binario
del servicio de conectores, que es la mayor parte de su imagen, ocupa alrededor de un tercio de lo
que ocupaba.
El servicio de conectores ya no acepta dispatchBacklog
Si sus valores fijan dispatchBacklog en functionalAreas.outbound-connectors.config, elimínelo
antes de actualizar. El servicio se niega a arrancar con él, y el error nombra la clave.
El ajuste dimensionaba un búfer entre el lector del servicio y sus trabajadores de envío, y ese búfer
ya no existe. El lector ahora solo obtiene tantos envíos como trabajadores libres hay para empezarlos
(maxConcurrentSends), así que un envío ya no espera dentro del proceso mientras corre la ventana de
confirmación del broker. El servicio de notificaciones lee las alarmas de la misma forma, una por
despachador.
Esto corrige un duplicado. Antes, una ráfaga de alarmas o de envíos de conectores en cola detrás de un canal lento podía quedarse en el servicio más tiempo que esa ventana. El broker entonces volvía a entregar los mismos mensajes mientras las primeras copias seguían esperando, y se enviaban ambas copias: una segunda notificación por una sola alarma, o una segunda llamada al mismo webhook. Además, ahora cada envío se corta con margen antes de que la ventana se cierre.
Una alerta nueva, ReaderHeldMessagePastAckWait, se dispara si alguno de los dos servicios aún
retiene un mensaje más allá de la ventana. Mensajes retenidos más allá de su ventana de
confirmación explica qué significa cada caso.
Los canales de notificación webhook deben indicar cómo se autentican
La configuración de un canal webhook necesita ahora una clave auth: none, bearer o header.
Antes, el canal enviaba Authorization: Bearer <secret> cuando había un secreto guardado y
ninguna credencial cuando no lo había, así que un canal cuyo secreto faltaba o se había borrado
seguía publicando sin autenticación, y nada lo indicaba. Configuración de canales de
notificación tiene los detalles.
- Un canal webhook guardado antes de esta versión no tiene
authy deja de entregar con la actualización, incluido uno que hoy funciona. Cada entrega a él se rechaza en el primer intento y no se reintenta, para cada alarma y cada escalado que se dirija a él, y la alarma no se vuelve a entregar para ese canal. El servicio de notificaciones registra el inquilino, el token del canal y el motivo, y lo cuenta endevicechain_notificationmanagement_deliveries_refused_total{reason="credential"}. - Localice los canales que hay que corregir antes de actualizar. En cada inquilino, ejecute
notificationChannels(criteria: {pageNumber: 1, pageSize: 100, channelType: "webhook"}) { results { token config hasSecret enabled } pagination { totalRecords } }y busque unconfigsinauth. SitotalRecordses mayor que 100, repita con el siguientepageNumberhasta haberlos leído todos. - Añada
authantes de actualizar. La versión actual acepta la clave y la ignora, así que no hay interrupción. Usebearerpara un canal con secreto (hasSecret: true) ynonepara uno sin secreto que no debe tenerlo, como un webhook entrante de Slack. Un canal que fijabaauthHeadernecesitaheader. Uno que solo fijabaauthScheme(por ejemploToken) necesita"auth":"header","authHeader":"Authorization". Conbeareronone, quiteauthHeaderyauthScheme: esta versión los rechaza ahí en lugar de ignorarlos. Connone, envíesecret: nullen la misma actualización si el canal tiene secreto. - Un canal cuyo
authy secreto no concuerdan se rechaza al guardarlo. Esto incluyebeareroheadersin secreto,nonecon secreto, y borrar el secreto de un canalbeareroheader. Rotar el secreto de un canal que aún no tieneauthtambién se rechaza, hasta que se añadaauthen la misma solicitud. Una actualización que solo renombra, describe o desactiva un canal no se comprueba, así que puede desactivar un canal roto sin arreglarlo antes; activarlo sí se comprueba. - Un canal SMTP con usuario y sin secreto se rechaza ahora antes de que la plataforma conecte con el servidor de correo, y no se reintenta. Antes conectaba, desistía sin autenticarse y reintentaba en cada intento.
- Una acción
httpCallcuyo manejador de secreto no tiene ningún secreto guardado se registra una vez en la cola de mensajes no entregados con el resultadoinvalidy no se reintenta. Antes se reintentaba hasta el límite de reentregas y se registraba como agotada, así que un secreto guardado durante ese intervalo aún podía dejar pasar la llamada. Nada vuelve a reproducir un mensaje no entregado, así que la llamada de ese disparo no se hace; corrija el manejador de secreto de la acción para que los disparos posteriores se autentiquen. La llamada nunca se envía sin su credencial.
Se puede rechazar el guardado de una política de notificación hecho desde una copia obsoleta
updateNotificationPolicy acepta un expectedUpdatedAt opcional. Envíe el updatedAt que leyó
por última vez, o el que devolvió la actualización anterior, y un guardado hecho desde una copia
obsoleta se rechaza, sin escribir nada, si alguien ha cambiado la política desde entonces, sus
reglas incluidas. El rechazo dice notification policy was modified by another writer; reload and try again y no lleva extensions.code. La respuesta de la actualización vuelve a leer ahora
updatedAt de la base de datos, para que pueda enviarse como la siguiente precondición. Si omite el
argumento, gana la última escritura, como antes. La consola no tiene editor de políticas de
notificación; solo quien llame a la API puede enviarlo. Vea Qué
mutaciones son actualizaciones parciales.
Persistencia y estado
Los eventos se persisten por lotes
event-management confirma ahora juntos, en una sola transacción, los eventos que esperan a un
escritor, en lugar de usar una transacción por evento. En un almacén de eventos replicado cada
confirmación espera a una réplica, y era esa espera, no el trabajo de la base de datos, lo que
limitaba la velocidad a la que se almacenaban los eventos. Un lote la paga una sola vez. No cambia
qué eventos se almacenan, y un evento se sigue reconociendo solo después de haberse almacenado. Un
evento rechazado se vuelve a escribir por separado, así que se reintenta o se notifica exactamente
como antes, y el resto de su lote se confirma sin él.
Hay tres ajustes nuevos, todos opcionales: persistence.writers (por defecto 5, el número que
antes era fijo), persistence.maxBatch (por defecto 32; 1 vuelve a una transacción por evento)
y persistence.lingerMillis (por defecto 0). device-state incorpora projection.writers (por
defecto 5). Consulte Persistencia de eventos.
- El número de escritores debe ser menor que el pool de conexiones del servicio. Un valor fuera
de rango impide que el servicio arranque, y el error nombra el ajuste. El valor por defecto de 5
solo se rechaza si ha fijado
tsdbConfiguration.maxOpenConnectionsparaevent-management, ordbConfiguration.maxOpenConnectionsparadevice-state, en 5 o menos. Antes de actualizar, fije el número de escritores por debajo del pool, o aumente el pool como máximo hasta su valor por defecto de 20: los límites de conexiones a la base de datos de la plataforma están dimensionados para ese valor. device-statetambién fusiona por lotes. Consulte El estado en vivo se fusiona por lotes.- Si representa las métricas de persistencia:
persist_inflightpuede superar ahora el número de escritores, porque cuenta los eventos que esperan a que su lote se confirme, ypersist_duration_secondsincluye ahora esa espera. Hay dos métricas nuevas,persist_batch_sizeypersist_batch_fallbacks_total.
El estado en vivo se fusiona por lotes, y un consumidor que se queda atrás genera un aviso
device-state fusionaba cada evento en el estado en vivo de un dispositivo (conectividad,
actividad, últimas lecturas y última posición) en dos transacciones propias. En una base de datos
replicada cada confirmación espera a la réplica, así que el estado en vivo se quedaba atrás siempre
que los eventos llegaban más rápido de lo que podía confirmarlos de uno en uno, y tras un ritmo alto
y sostenido podía ir más de una hora por detrás de los eventos almacenados sin que nada lo
indicara. Ahora fusiona en una sola transacción los eventos que esperan a un escritor, igual que
event-management los persiste. Medido dentro del proceso contra TimescaleDB con una réplica
síncrona, cinco escritores fusionaron unos 3500 eventos por segundo, frente a unos 85 antes.
- Lo que acaba guardando el estado en vivo de un dispositivo no cambia, con una excepción. Una lectura o una posición sustituye a la almacenada solo si es estrictamente más reciente, y las horas se comparan ahora tal como las guarda la base de datos, al microsegundo. Antes, una lectura que llegaba dentro del mismo microsegundo que la almacenada podía sustituirla aunque fuera más antigua. Ahora se conserva la que se almacenó primero.
- Un evento se sigue reconociendo solo después de haberse almacenado. Si se rechaza la parte de un lote que corresponde a un inquilino, los eventos de ese inquilino se vuelven a fusionar de uno en uno, así que solo un evento que es rechazado por sí mismo se reintenta o se descarta como antes, y los eventos de los demás inquilinos se confirman juntos.
- Hay dos ajustes nuevos, ambos opcionales:
projection.maxBatch(por defecto32;1vuelve a un evento cada vez) yprojection.lingerMillis(por defecto0), con los mismos rangos que los deevent-management. Consulte Estado en vivo de los dispositivos. - Si representa las métricas de
device-state:state_inflightpuede superar ahora el número de escritores, porque cuenta los eventos que esperan a que su lote se confirme, ystate_duration_secondsincluye ahora esa espera. Hay dos métricas nuevas,state_batch_sizeystate_batch_fallbacks_total. - Cada servicio informa ahora de cuántos mensajes esperan a cada consumidor que lee, como
jetstream_consumer_pending_messagesyjetstream_consumer_ack_pending_messages. Un aviso nuevo,JetStreamDurableFallingBehind, salta cuando un consumidor ha tenido más de 10000 mensajes esperándole durante 15 minutos. El consumidor de detección deevent-processingqueda a cargo deDetectConsumerBacklogHigh, que no cambia. Consulte Un consumidor que se queda atrás.
Bases de datos y conmutación
La primaria de una base de datos conmuta en segundos
Eliminar el pod de la primaria de una base de datos, drenar su nodo o desplegar un cambio en ella
retenía la conmutación por error durante tres minutos: la primaria esperaba a que todos los
clientes se desconectaran, y los servicios de la plataforma nunca lo hacen. Ahora concede cinco
segundos a los clientes y después se detiene, de modo que con --ha una réplica en espera toma
el relevo en bastante menos de un minuto. Vea
Cuando se detiene la primaria de una base de datos.
- Una base de datos de varias instancias renueva ahora su primaria mediante un traspaso. Las versiones anteriores reiniciaban la primaria en su sitio y la esperaban, sin promover ninguna réplica, lo que convertía cada uno de esos despliegues en una interrupción completa de las escrituras.
- Una instancia de base de datos que se está deteniendo se detiene de forma forzosa a los dos
minutos, en lugar de a los treinta. Un pod de base de datos que podía quedarse en
Terminatinghasta media hora después de haberse detenido su base de datos, dejando al clúster con una réplica de menos, ahora se elimina en dos minutos como máximo. - Si el almacén de copias de seguridad es inaccesible cuando se detiene una instancia, esta ya no espera hasta treinta minutos a que se archive su último registro de escritura anticipada: espera como máximo dos. Los datos confirmados no se ven afectados, pero el archivo puede tener un hueco, como se describe en la página enlazada arriba.
Las instancias del almacén relacional se reinician una vez cuando dcctl install actualiza el
clúster. Con --ha primero se reinician las réplicas en espera y después el papel de primaria
se traspasa a una de ellas, lo que supone una breve interrupción de las escrituras. Una
instalación de una sola instancia reinicia su única instancia en su sitio, y el almacén
relacional no está disponible hasta que termina el reinicio. Las escrituras de ese intervalo se
reintentan.
El almacén de eventos de una instancia existente conserva la configuración anterior.
dcctl upgrade no vuelve a aplicar las bases de datos de una instancia, así que solo las
instancias creadas con esta versión reciben la nueva configuración en su almacén de eventos.
Para dar la misma configuración al almacén de eventos de una instancia existente, aplique un
parche a su clúster de base de datos con UNO de estos comandos. Esto reinicia sus instancias
una vez, como se describe arriba:
# con --ha: el ajuste de traspaso va en el MISMO parche que los tiempos
kubectl -n dci-<instance> patch cluster dc-tsdb --type merge \
-p '{"spec":{"primaryUpdateMethod":"switchover","smartShutdownTimeout":5,"stopDelay":120,"switchoverDelay":120}}'
# una instalación de una sola instancia
kubectl -n dci-<instance> patch cluster dc-tsdb --type merge \
-p '{"spec":{"smartShutdownTimeout":5,"stopDelay":120,"switchoverDelay":120}}'
Con --ha, no divida el primer comando en dos parches aplicando primero los tiempos. Cambiar
stopDelay inicia el reinicio de inmediato y, si el ajuste de traspaso aún no está aplicado, la
primaria se reinicia en su sitio sin promover ninguna réplica, que es la interrupción larga que
esta versión elimina.
Con --ha, un cambio de imagen de la base de datos y un cambio de parámetros de la base de
datos deben aplicarse por separado. Con el ajuste de traspaso aplicado, el operador de la base
de datos rechaza una actualización que cambia a la vez la imagen de la base de datos y cualquier
parámetro de la base de datos. Por tanto, se rechaza una sola ejecución de dcctl install que
pase a una versión con una nueva imagen de base de datos y cambie además --max-connections.
Ejecute primero dcctl install sin el cambio de --max-connections y después otra vez con él.
El límite de dos minutos pertenece a cada pod de base de datos, así que llega con el reinicio que
sustituye el pod, y los pods que se sustituyen todavía tienen treinta minutos. Si un pod de base
de datos se queda en Terminating más de dos minutos durante ese reinicio, y su registro muestra
failed waiting for all runnables to end within grace period of 30s, su base de datos ya se ha
detenido y el pod no va a terminar por sí solo. Elimínelo, y el operador lo vuelve a crear:
# los pods del almacén relacional son dc-rdb-<n> en dc-system,
# los del almacén de eventos son dc-tsdb-<n> en dci-<instance>
kubectl -n dc-system delete pod dc-rdb-1 --grace-period=0 --force
Hágalo solo cuando el nodo del pod esté Ready y haya leído esa línea en su registro. Un pod que
está en Terminating porque su nodo es inaccesible es un caso distinto: su base de datos puede
seguir en ejecución, y no debe eliminarse a la fuerza. Consulte
perder un nodo.
Una transacción de base de datos inactiva durante un minuto se termina
No hay que hacer nada en la actualización.
Cada servicio pide ahora a la base de datos que termine cualquiera de sus transacciones que lleve 60
segundos inactiva. Esto cubre un pod que se congela o pierde la red en mitad de una transacción.
Antes, esa transacción seguía abierta, con sus bloqueos, hasta que se detectaba que la conexión
estaba muerta, lo que con la configuración predeterminada del sistema operativo puede tardar horas,
y aún podía confirmarse al volver la red. Eso podía escribir datos de un inquilino cuya eliminación
ya se había dado por completada. Cuando se alcanza el límite, la base de datos registra
terminating connection due to idle-in-transaction timeout y revierte la transacción. La solicitud
a la que pertenecía falla con un error, y el trabajo que un servicio toma de un flujo se le vuelve a
entregar. Vea Rechazo de escrituras en la base de datos.
Qué ocurre al perder un nodo y al recuperarlo queda documentado
El comportamiento no cambia. Con --ha, perder un nodo describe
ahora, en orden, lo que ve un operador: con qué rapidez se recuperan el broker, los servicios y
las bases de datos, que el procesamiento de eventos puede detenerse durante aproximadamente un
minuto, por qué
los pods desalojados del nodo perdido se quedan en Terminating y no deben
eliminarse a la fuerza mientras el nodo sea inaccesible, y que la vuelta de un nodo es en sí
misma una interrupción breve. Un servidor del broker que quedó aislado vuelve tras haber
celebrado elecciones por su cuenta, y los demás servidores vuelven a elegir a sus líderes cuando
se reincorpora: cuente con unos segundos de "temporalmente no disponible" en JetStream alrededor
de 45 segundos después de que vuelva el nodo. No se pierde nada. Planifique la vuelta de un nodo
como planifica su pérdida.
El desalojo a los 30 segundos de los pods de los servicios cuando se pierde un nodo no cambia y es deliberado, y ahora se comprueba en cada pod que renderiza el chart, incluida la consola.
Mensajería
Un servicio cuya conexión con el broker se cierra definitivamente ahora se reinicia
El pod de cada servicio falla ahora su comprobación de actividad (/healthz), y con ella la de
disponibilidad, cuando su conexión con el broker de mensajería se cierra definitivamente sin que el
servicio lo haya pedido: por ejemplo, cuando el broker deja de aceptar una credencial que se revocó
o se rotó bajo el pod en ejecución, o envía un error que el cliente no sabe interpretar. Kubernetes
reinicia el pod, y el pod nuevo vuelve a leer su credencial. Antes de esta versión el pod seguía
activo y listo, registraba una línea de error y no hacía nada más. Un cierre que el propio servicio
pidió, como en una parada ordenada, nunca lo activa.
- Un cambio de credencial puede verse como reinicios. Si el broker corta la conexión de un pod antes de que el propio pod se sustituya, por ejemplo durante una rotación de credenciales, ese pod se reinicia una vez.
- Una parada ordenada espera ahora a que se vacíe la conexión con el broker, dentro del presupuesto de terminación del pod, de modo que los mensajes ya recibidos se procesan antes de que el pod salga. Una parada puede tardar más que antes, hasta ese presupuesto.
event-sources tiene además una conexión propia para la presencia del broker, y ahora ocurre lo
mismo con ella. Por esa conexión lee del broker los eventos de conexión MQTT, autenticada con la
credencial de la cuenta de sistema. Si el broker la cerraba definitivamente, el pod seguía activo y
listo con la presencia del broker congelada en silencio: no se afirmaba ni se liberaba nada, y nada
lo reiniciaba.
Ahora el pod falla su comprobación de actividad (liveness) y Kubernetes lo reinicia. Una credencial
rechazada llega a todas las réplicas a la vez, así que se reinician todos los pods de
event-sources, y la ingesta HTTP no está disponible mientras tanto; la telemetría MQTT la guarda
el broker y se procesa cuando vuelven. Si los pods reiniciados siguen sin poder iniciar sesión, la
presencia del broker se apaga con el motivo broker_unreachable y los dispositivos afirmados vuelven
a la presencia inferida, como cuando no se puede alcanzar el broker al arrancar (Presencia de
dispositivos).
Los bucles de lectura que siguen fallando ahora reinician su servicio
Un bucle que lee mensajes de un flujo reintentaba una lectura fallida una vez por segundo mientras siguiera fallando, mientras su pod se declaraba listo y no consumía nada. Cada uno de esos bucles reintenta ahora con una pausa creciente, de hasta 5 segundos, y cuando las lecturas llevan dos minutos fallando sin interrupción termina el proceso con un código de salida distinto de cero, de modo que Kubernetes reinicia el pod. Una sola lectura correcta vuelve a empezar los dos minutos, que bastan para superar una conmutación o un reinicio del broker. El reinicio vuelve a conectarse al broker, vuelve a crear el consumidor y vuelve a leer la credencial montada.
Los bucles que lo incorporan en esta versión son:
- el despacho de conectores de
outbound-connectors; - el almacén de mensajes no entregados de
user-management; - la reescritura de mensajes no entregados de
command-delivery; - siete en
event-processing: eventos resueltos, cambios de reglas y de atributos, la lista de dispositivos y sus eliminaciones, los conjuntos de geocercas y el despachador de acciones; - el despachador de comandos de
lwm2m-ingest.
Lo que verá es un recuento de reinicios donde antes había un pod listo que no consumía nada.
Un consumidor que se queda atrás de un flujo lleno ahora genera una alerta
Un flujo (stream) de JetStream lleno descarta sus mensajes más antiguos. Antes de esta versión, un consumidor que aún no había leído esos mensajes los perdía sin que ninguna métrica ni alerta lo indicara. Ahora cada servicio lo mide para cada consumidor duradero que lee y exporta dos series nuevas:
devicechain_<area>_jetstream_consumer_unread_skipped_total{stream, durable}: mensajes que el consumidor pasó por encima sin leerlos.devicechain_<area>_jetstream_consumer_unread_gap_messages{stream, durable}: mensajes descartados por delante de un consumidor que ha dejado de leer (uno que no ha recibido ningún mensaje desde la muestra anterior; un consumidor que lee pero va atrasado vale 0 aquí).
Dos alertas críticas nuevas las leen: JetStreamDurableLostUnread y
JetStreamDurableStalledBehindStream. Eliminar un inquilino puede disparar la primera: la
eliminación
borra los mensajes de ese inquilino, incluidos los que un consumidor aún no había alcanzado.
Consulte
Mensajes que un consumidor nunca leyó para saber qué significa cada
alerta y qué hacer.
La advertencia de flujo casi lleno cambia de nombre, de EventProcessingStreamNearFull a
JetStreamStreamNearFull, y ahora cubre los flujos de todos los servicios, no solo los de
event-processing. El umbral (80% del límite de bytes durante 10 minutos) no cambia. Si una ruta o un
silencio de Alertmanager nombra la alerta antigua, cámbielo al nombre nuevo.
Los mensajes abandonados en su último intento ahora se registran
Hasta ahora, solo llegaba a la lista de mensajes no entregados un mensaje que un servicio abandonaba después de procesarlo. Un mensaje cuyos cinco intentos de entrega se agotaban sin ningún resultado (un pod detenido a mitad del procesamiento, o un manejador que se pasó de su ventana de confirmación) no dejaba rastro, porque nada llegaba al código que escribe el registro. El broker sí lo advierte, y ahora cada servicio también registra esos casos a partir del propio aviso del broker.
Qué cambia para usted:
- Tres tipos nuevos,
event,commandycontrol-fact, para los mensajes de los streams de eventos de dispositivos, de comandos y del plano de control. El tipo de un registro queda fijado ahora por el stream por el que llegó el mensaje.dcctl dead-letters list --kindlos ofrece todos. - Un motivo nuevo,
no-outcome. Nunca liquida un comando: el último intento pudo haber hecho su trabajo y perder solo su acuse de recibo. En los streams de mucho volumen (eventos de dispositivos, comandos y acciones de detección) el registro no lleva copia del mensaje; su detalle indica dónde está el original hasta que el stream lo descarte por antigüedad. Lo mismo vale para un mensaje demasiado grande para copiarse, y el registro de una petición de conector apunta al stream propio de mensajes no entregados del servicio de conectores, que guarda la petición completa. - Un stream nuevo,
max-deliveries, que crea cada servicio que lee de un stream. Reserva 8 MiB con el dimensionamiento por defecto y cabe en el volumen de JetStream existente; no hace falta redimensionar nada. Está vacío en régimen normal, y una alerta nueva,MaxDeliveryRecordsWaiting, se dispara si hay avisos esperando sin registrar (Mensajes que agotaron sus intentos de entrega). - Los abandonos del motor de detección se cuentan, no se registran.
event-processingleeresolved-eventsdesde su propio punto de control guardado y vuelve a leer el stream tras un reinicio, así que un evento que agotó ahí sus intentos no se ha perdido. Cuando el motor no puede guardar su punto de control durante más tiempo del que el broker sigue reentregando (normalmente, una caída de la base de datos), todos los eventos de ese intervalo agotan sus intentos, y un registro por cada uno informaría de pérdidas que no ocurrieron. En su lugar se cuentan conoutcome="replay-covered", y una alerta nueva de nivel warning,ReplayCoveredDeliveriesExhausted, informa de ellos. Los demás servicios que leenresolved-eventssiguen registrando los suyos. - El stream de mensajes no entregados gana una ventana de duplicados de 30 minutos, aplicada en el sitio durante la actualización, y también el stream propio de mensajes no entregados del servicio de conectores. Es lo que hace que un abandono registrado a la vez por un servicio y por el aviso del broker quede una sola vez.
DeadLetterWriteLosttiene una tercera causa: un mensaje de esa cola en el que el almacén de mensajes no entregados o la reescritura de comandos agotó sus intentos, y que ahora puede caducar en el stream sin haberse almacenado (su último intento pudo almacenarlo y perder solo el acuse de recibo).
Durante la actualización escalonada, un abandono puede registrarse dos veces: una por un pod de la versión anterior y otra a partir del aviso del broker. Es el mismo fallo; no se perdió nada.
Los contadores de pérdidas de mensajes no entregados son una métrica por servicio
Un servicio que abandona un mensaje y luego no puede registrarlo como mensaje no entregado cuenta
ahora esa pérdida en dead_letter_lost_total bajo su propio subsistema, con el mismo nombre en
todos los servicios. Estas cinco series desaparecen:
devicechain_eventprocessing_react_events_dead_letter_lost_totaldevicechain_notificationmanagement_notifications_dead_letter_lost_totaldevicechain_commanddelivery_command_delivery_responses_dead_letter_lost_totaldevicechain_devicemanagement_raise_alarm_dead_letter_lost_totaldevicechain_devicemanagement_alarm_event_dead_letter_lost_total
Estas cinco las reemplazan:
devicechain_eventprocessing_dead_letter_lost_totaldevicechain_notificationmanagement_dead_letter_lost_totaldevicechain_commanddelivery_dead_letter_lost_totaldevicechain_devicemanagement_dead_letter_lost_total, un único contador para las dos rutas de device-managementdevicechain_outboundconnectors_dead_letter_lost_total, que es nueva. Un envío de conector saliente cuya copia no se pudo escribir en su entrega final solo se contaba antes comoconnector_dispatch_total{outcome="dead_write_failed"}, que ninguna alerta leía. Se sigue contando ahí, y ahora también aquí.
La alerta DeadLetterWriteLost las selecciona por nombre en lugar de enumerarlas, así que ahora
cubre también los conectores salientes. Si sus propios paneles o reglas nombran las series
antiguas, cámbielos. El selector {__name__=~"devicechain_[a-z0-9]+_dead_letter_lost_total"}
cubre todos los servicios. Use [a-z0-9]+ y no .+: mientras avanza la actualización, los pods
que aún no se han reemplazado siguen exportando los dos nombres antiguos de device-management, y
.+ coincide con ambos.
El contador cuenta también un mensaje no entregado que el servicio rechazó por estar mal
formado, lo que es un defecto de ese servicio y no un problema del bróker. La línea de error LOST del pod indica cuál
de los dos casos ocurrió.
Seguridad e inicio de sesión
Restablecer una contraseña termina las sesiones, y todos vuelven a iniciar sesión una vez
Dos cambios terminan todas las sesiones con la actualización.
Cada usuario tiene ahora un valor de sesión, y todo token que se puede canjear por otro nuevo lo lleva: los tokens de actualización, el token de inicio de sesión que la consola guarda antes de elegir un inquilino y los códigos de autorización de OAuth. Restablecer la contraseña de un usuario, desactivarlo o eliminarlo cambia ese valor, y un token que lleva el anterior se rechaza. Antes de esta versión, restablecer una contraseña dejaba funcionando todos los tokens de actualización ya emitidos, de modo que uno robado seguía renovándose mientras se usara.
La clave que firma todos los tokens de acceso y de actualización se sella ahora bajo la clave raíz de la instancia. Antes se guardaba sin cifrar en la base de datos de user-management, así que cualquiera que pudiera leer esa base de datos, una copia de seguridad de ella o su archivo del registro de escritura anticipada podía firmar tokens que todos los servicios aceptan. Ahora se sella como cualquier otra credencial almacenada, y solo la clave en uso tiene mitad privada. Cuando una clave se retira por rotación, su mitad privada se borra y solo se conserva la pública, de modo que los tokens que firmó siguen verificándose hasta que caducan. La actualización borra todas las claves de firma que tenía la instancia, en lugar de sellarlas, porque cada una ya ha estado sin cifrar en todas las copias de seguridad tomadas hasta ahora. user-management genera una clave nueva al arrancar.
Lo que verá en la actualización:
- Todos los usuarios vuelven a iniciar sesión. Los tokens emitidos antes de la actualización no llevan valor de sesión, así que no se pueden renovar, y cuando termina el despliegue tampoco validan. Las sesiones de la consola, del panel y de los SDK terminan en su siguiente renovación, dentro de los 15 minutos posteriores a la actualización salvo que haya cambiado la duración del token de acceso. También terminan las sesiones en una aplicación de paneles embebida. Una consola que en ese momento esté en el selector de inquilinos o en las páginas de administración puede mostrar un error al elegir un inquilino, en lugar de volver a la página de inicio de sesión. Cerrar sesión y volver a entrar lo resuelve.
- Los clientes OAuth, incluidos los asistentes de IA conectados por MCP, deben autorizarse de
nuevo. Sus tokens de actualización se rechazan con
invalid_grant. - La clave antigua es de confianza hasta que termina el despliegue. Hasta que se detiene el
último pod antiguo de user-management, sigue firmando tokens con la clave antigua y publicándola
para que los demás servicios verifiquen con ella.
helm upgradeydcctl upgradesustituyen todos los pods, así que la clave antigua deja de ser de confianza cuando terminan. Mientras ese pod antiguo se detiene, una petición hecha en los primeros segundos del despliegue puede rechazarse con401 invalid or expired token, y un inicio de sesión coninvalid or expired token, incluso para un token emitido momentos antes; cuando el pod antiguo de user-management se ha detenido, volver a iniciar sesión y reintentar funciona. - Un usuario creado por un servicio que aún no se había reemplazado mientras avanzaba la actualización no tiene valor de sesión y no puede iniciar sesión. El inicio de sesión falla como lo haría con una contraseña incorrecta, y el registro de user-management nombra al usuario y la causa. Que un administrador restablezca la contraseña de ese usuario lo corrige.
Lo que cambia a partir de entonces:
- Restablecer la contraseña de un usuario, desactivarlo o eliminarlo termina todas sus sesiones. Sus tokens de actualización dejan de funcionar en el siguiente uso, y un token de inicio de sesión o un código de autorización emitido antes del cambio ya no se puede canjear por una sesión nueva. Reactivar un usuario desactivado no recupera las sesiones anteriores.
- Los tokens ya emitidos para uso directo no se revocan. Un token de acceso, y el token de inicio de sesión en la API de administración, siguen funcionando hasta que caducan: 15 minutos, salvo que haya cambiado la duración del token de acceso. Eso incluye el token de inicio de sesión de un administrador.
- Eliminar un usuario y crear otro con el mismo correo empieza de cero. El usuario nuevo no hereda ninguna sesión que conservara el anterior.
- Cambiar los roles o las membresías de un usuario no cierra su sesión. Surte efecto en su siguiente renovación, como antes.
Las copias de seguridad y el registro de escritura anticipada archivado antes de la actualización siguen conteniendo las claves antiguas. Esas claves ya no son de confianza en ninguna parte una vez terminado el despliegue, pero conviene seguir protegiendo esos archivos como las credenciales que fueron.
Una nueva clave de firma se acepta en aproximadamente un segundo
Cuando user-management empieza a firmar tokens con una clave nueva, como ocurre en esta
actualización, cada uno de los demás servicios conoce esa clave la primera vez que ve un token
firmado con ella, volviendo a obtener el conjunto de claves que publica user-management. Las
versiones anteriores permitían a cada pod de servicio esa obtención como mucho una vez cada 30
segundos. Si respondía un pod de user-management que aún publicaba el conjunto de claves anterior,
como hace el pod antiguo mientras una actualización progresiva está en curso, ese servicio
rechazaba todos los tokens firmados con la clave nueva como invalid or expired token hasta que
pasaban los 30 segundos. Un inicio de sesión justo después de una actualización podía fallar así.
- Un servicio vuelve ahora a obtener las claves en cuanto pasa un segundo desde que terminó la obtención anterior. Las solicitudes que llegan mientras una obtención está en curso esperan a que termine en lugar de rechazarse.
- Los tokens que nombran una clave que user-management nunca publicó le siguen costando como mucho una obtención por segundo desde cada pod de servicio.
- Una obtención que no encuentra la clave ahora se registra. El servicio registra
JWKS refetched on an unknown kid, and the fetched set does not hold it.con el identificador de la clave, lo que distingue un token rechazado durante un cambio de clave de un token que simplemente no es válido.
No hay nada que configurar.
La clave raíz de la instancia es ahora obligatoria en todos los perfiles
Antes, la clave raíz solo la necesitaban los perfiles que almacenan credenciales de integración, así
que el chart permitía renderizar los perfiles telemetry e ingest-only sin ella. Ahora toda
instancia sella su clave de firma con la clave raíz, así que el chart falla el renderizado en
cualquier perfil cuando no hay clave. Una instancia construida con dcctl bootstrap ya tiene una y
no necesita nada. Una instalación telemetry o ingest-only hecha solo con el chart necesita una
clave antes de poder actualizarse. Genérela con openssl rand -base64 32, pásela como
instance.config.infrastructure.secrets.rootKey y consérvela: hay que pasar el mismo valor en
cada actualización posterior.
La clave raíz decide ahora también si alguien puede iniciar sesión. Con una clave equivocada o perdida, user-management se niega a arrancar, igual que todo servicio que almacena credenciales de integración. Los demás servicios se quedan entonces sin estar listos, porque no pueden obtener las claves que necesitan para validar un token. Cae toda la API, no solo las integraciones. Consulte Recuperación ante desastres para saber qué hacer.
Borrar una credencial almacenada ahora también la elimina por completo de la base de datos. Antes, la fila sellada se quedaba en la tabla, marcada como borrada. Las credenciales borradas antes de esta versión se quedan así: no se pueden usar, pero sus filas selladas siguen en la tabla y en las copias de seguridad.
El superusuario ya no tiene contraseña por defecto
Las versiones anteriores creaban el superusuario de cada instancia, superuser@devicechain.local,
con la misma contraseña publicada, devicechain. Ahora cada instancia nueva recibe una contraseña
generada para ella: dcctl bootstrap la imprime una sola vez y la guarda en el Secret
dci-<instance>-superuser del namespace de la instancia. Ya no hay valor por defecto. Si
user-management arranca con la tabla de identidades vacía y sin contraseña en ese Secret, se niega a
crear el superusuario.
La actualización no cambia la contraseña de un superusuario existente. No puede saber si usted
la cambió, así que no la toca, e imprime un aviso al terminar para cualquier instancia que no tenga
contraseña generada. Si nunca cambió la contraseña en una instancia así, sigue siendo devicechain.
Inicie sesión y cámbiela, o vuelva a crear la instancia para que se le genere una.
dcctl sim y las herramientas de simulacro tampoco dan ya por supuesta la contraseña antigua.
dcctl sim lee la generada del Secret de la instancia, y acepta --admin-password o
$DC_ADMIN_PASSWORD para una instancia que no lo tiene.
Si sus valores fijan auth.superuserPassword para user-management
(functionalAreas.user-management.config.auth.superuserPassword), elimínelo antes de actualizar.
user-management se niega ahora a arrancar con él, y el error nombra la clave. La contraseña inicial
solo llega del Secret dci-<instance>-superuser, o del que nombra instance.superuserSecret.
El inicio de sesión tiene límite de frecuencia, y las solicitudes de GraphQL llevan menos campos
No hay que hacer nada en la actualización salvo que su propio código o sus scripts hagan alguna de
las cosas siguientes. La consola, la aplicación de paneles, los SDK y dcctl ya respetan todos los
límites.
- Una operación puede seleccionar como mucho 5 campos de primer nivel en una mutación y 20 en una
consulta. Los alias cuentan, y también los campos a los que se llega a través de fragmentos. Una
solicitud que supera el límite no ejecuta nada y recibe un error con el código
TOO_MANY_ROOT_FIELDS. Divida esa solicitud, o aumenteDC_GRAPHQL_MAX_MUTATION_ROOT_FIELDS/DC_GRAPHQL_MAX_QUERY_ROOT_FIELDSpara ese servicio. - En una solicitud se puede comprobar una contraseña. Otro
loginen la misma solicitud no se evalúa y recibe el códigoTOO_MANY_CREDENTIAL_CHECKS. Inicie sesión una vez por solicitud. - Los inicios de sesión fallidos repetidos sobre una dirección de correo se ralentizan. Tras cinco
fallos seguidos, el siguiente intento sobre esa dirección espera 1 segundo, duplicándose hasta 5
minutos. Un intento hecho durante la espera recibe el código
THROTTLEDconretryAfterSeconds, no «credenciales no válidas». Un inicio de sesión que el servidor no puede contar recibeUNAVAILABLE. Si su código inicia sesión, trate ambos como errores propios y no como una contraseña incorrecta. Los secretos de cliente OAuth no se ralentizan. - La reserva de JetStream crece en 128 MiB (16 MiB en el preajuste compacto), por el bucket que guarda los recuentos de inicio de sesión. En el preajuste compacto los buckets de caché bajan de 8 a 4 MiB cada uno para hacer sitio. Si dimensionó usted mismo el volumen de JetStream cerca de la reserva, compruebe que tiene espacio.
- Una alerta nueva,
CredentialAttemptStoreFull, se dispara si ese bucket se llena. El inicio de sesión sigue funcionando cuando está lleno, pero los fallos repetidos ya no se ralentizan. Espera entre intentos de inicio de sesión explica qué hacer.
Límites de las solicitudes y espera entre intentos de inicio de sesión tienen los detalles.
Una renovación de sesión sobrevive a una caída breve
Renovar una sesión gastaba antes el token de actualización antes de volver a
comprobar la sesión. Un error de la base de datos o del broker durante esa comprobación terminaba
entonces la sesión: la renovación fallaba como «invalid or expired token» y el token ya no se
podía usar. Ahora la comprobación va primero. Un error del almacén deja el token válido y devuelve
un error que se puede reintentar («the session could not be refreshed right now; try again»), y el
endpoint de tokens de OAuth devuelve server_error sin el texto del error subyacente. Una renovación
rechazada porque la sesión terminó, la membresía se eliminó o se desactivó, o el inquilino niega el
acceso sigue gastando el token.
Solo se beneficia un cliente que reintenta con el mismo token de actualización. Un cliente OAuth,
como
un agente de IA que se conecta por MCP, recibe ahora server_error en lugar de invalid_grant
durante esa caída, así que puede reintentar en lugar de pedir al usuario que lo autorice de nuevo. La
biblioteca cliente de Go que usan el simulador, las pruebas de carga y dcctl ya no pierde su token
de renovación por la caída, pero sigue recurriendo a un inicio de sesión con contraseña cuando falla
una renovación, como antes. La consola sigue cerrando la sesión del usuario ante cualquier fallo de
renovación. No hay que hacer nada en la actualización.
Las conexiones MQTT fallidas repetidas con contraseña se ralentizan
No hay que hacer nada salvo que un dispositivo se conecte en bucle con una contraseña MQTT incorrecta, o que haya dimensionado por su cuenta el volumen de JetStream. Las conexiones fallidas repetidas se ralentizan tiene los detalles.
- Tras 10 conexiones fallidas seguidas para un mismo usuario MQTT, el siguiente intento espera 1 segundo, duplicándose hasta 30 segundos. Una conexión hecha durante la espera se rechaza como una contraseña incorrecta, aunque la contraseña sea correcta. Una conexión correcta pone la cuenta a cero. Las conexiones con token de acceso y las credenciales en el cuerpo de los eventos no se ven afectadas.
- Alguien que conozca el usuario MQTT de un dispositivo puede impedir que ese dispositivo se reconecte mientras siga enviando contraseñas incorrectas para él. Los dispositivos que ya están conectados no se ven afectados hasta que se reconectan.
- Las conexiones con contraseña necesitan ahora JetStream. Si no se puede acceder al almacén que guarda los recuentos, las conexiones con contraseña se rechazan, también brevemente mientras cambia el líder de JetStream de ese almacén, por ejemplo mientras se reinicia un nodo de NATS.
- Durante una caída de la base de datos, un dispositivo cuyas conexiones con contraseña siguen fallando se ralentiza del mismo modo, así que tras sus primeros 10 intentos sus conexiones rechazadas se registran en depuración en lugar de como un aviso cada una. Un almacén de recuentos inaccesible se registra como un aviso por minuto.
- La reserva de JetStream crece 128 MiB (16 MiB en el preajuste compacto) para el bucket nuevo que guarda los recuentos.
- El volumen de JetStream del preajuste compacto crece de 2Gi a 3Gi para las instancias creadas
por esta versión: el almacén crece de 1 GiB a 2 GiB. Una instancia compacta existente conserva
su volumen de 2Gi, porque
dcctl upgradeno vuelve a aplicar la infraestructura, y el bucket nuevo sigue cabiendo en él. - Una alerta nueva,
DeviceCredentialAttemptStoreFull(aviso), se dispara cuando ese bucket se llena. Las conexiones siguen funcionando, pero sin la ralentización. Una ola de reconexiones muy grande puede llenarlo igual que un ataque.
Los valores de las credenciales se guardan exactamente como se envían
- El
credentialValuede una credencial de dispositivo ya no se recorta. Las versiones anteriores quitaban los espacios iniciales y finales al guardar el valor, pero comparaban sin recortar la contraseña que presentaba el dispositivo, así que una contraseña MQTT que empezaba o terminaba con un espacio nunca podía autenticarse. Ahora el valor se guarda exactamente como se envía. Solo un valor vacío, o unnullexplícito al actualizar, no guarda ninguna contraseña. - También cambia la dirección contraria. Un valor pegado con un salto de línea o un espacio al final se guardaba sin él, así que se aceptaba a un dispositivo que presentaba la contraseña sin él. Ahora se guarda con él, y ese dispositivo se rechaza hasta que se vuelva a enviar el valor sin el salto de línea.
- La actualización no cambia los valores guardados antes de esta versión. Se guardaron
recortados, y los espacios no se pueden recuperar. Si la contraseña configurada en un
dispositivo lleva espacios alrededor, vuelva a enviar el valor con
updateDeviceCredential. - Un nivel sin color ahora devuelve
color: nullen la API de administración, donde antes devolvía"". Enviar""onulllo sigue limpiando, y la actualización convierte a null todos los colores vacíos guardados. Los espacios alrededor se recortan ahora antes de comprobar el color, así que" amber "se acepta comoamberdonde antes se rechazaba. La consola no necesita nada; el código propio que comparecolorcon""debe comprobar null. firstNameylastNamese recortan como el resto del texto visible, encreateIdentityy enupdateProfile, y un nombre limpiado se guarda como null. Las lecturas ya devolvíannullpara un nombre vacío, y lo siguen haciendo: la actualización convierte a null todos los nombres vacíos guardados. Hace lo mismo con elendpointvacío de un proveedor de IA, que también se leía ya comonull. Un nombre que una versión anterior guardó con espacios alrededor los conserva hasta que una actualización nombra ese campo, que entonces lo recorta.- Un número guardado demasiado grande para un
Intde GraphQL es ahora un error en lugar de un número equivocado. Los camposthrottleSeconds,escalateAfterSecondsymaxEscalationsde una política de notificación, y las anulaciones de ráfaga, prioridad de descarte, comandos retenidos y geocercas de un inquilino en la API de administración, convertían ese valor en uno negativo. Los valores escritos a través de la API siempre caben, así que esto afecta solo a un valor escrito en la base de datos por otra vía. Una actualización de una política de notificación que no nombra uno de esos tres campos tampoco lo reescribe ya.
Ya no se puede crear un perfil de aprovisionamiento con un secreto en blanco
createProvisioningProfile rechaza ahora un provisionKey o un provisionSecret vacío o formado
solo por espacios. Un perfil con un secreto vacío coincidiría con un dispositivo que también enviara
un secreto vacío. Ninguna instancia quedó expuesta a ello, porque la vía por la que un dispositivo
presentaría un secreto de aprovisionamiento todavía no se ha publicado; la comprobación cierra el
hueco antes de que lo haga. updateProvisioningProfile ya rechazaba un valor en blanco. Un
perfil que ya esté guardado con un secreto vacío no coincide ahora con ningún secreto presentado,
vacío o no: asígnele uno real con updateProvisioningProfile.
API y errores
Los WebSocket de GraphQL solo llevan suscripciones y se cierran cuando caduca su token
El WebSocket que un servicio acepta en su endpoint GraphQL cambia de tres maneras:
- Solo ejecuta suscripciones. Una consulta o mutación enviada por él se rechaza con un error que indica usar HTTP, y no se ejecuta nada. Antes se ejecutaban ambas, con el token que la conexión presentó al abrirse. Envíe las consultas y mutaciones como peticiones HTTP.
- Se cierra con el código
4401cuando caduca el token de acceso con el que se autenticó. Antes, una conexión seguía abierta, y sus suscripciones seguían emitiendo, mientras el cliente respondiera a los pings. Para mantener un flujo, abra una conexión nueva con un token nuevo y vuelva a suscribirse.@devicechain/clientlo hace por usted. Cuando una conexión que había establecido se cierra con4401, se reconecta una vez con un token recién obtenido, vuelve a suscribirse e informa de la reconexión a su receptor comoconnected(true).- El SDK de .NET lanza el cierre desde
SubscribeAsynccomo una excepción que indica el código. Vuelva a suscribirse para continuar; la conexión nueva toma un token nuevo de la sesión. - El visor de paneles independiente no renueva su token, así que sus widgets en vivo se detienen cuando el token caduca. Vuelva a iniciar sesión.
- Un servicio sin suscripciones ya no acepta ningún WebSocket. La petición de upgrade se rechaza con HTTP 400. Antes, la conexión se abría y cada operación enviada por ella fallaba.
Los documentos de GraphQL deben usar los comentarios y las cadenas propios de GraphQL
No hay que hacer nada en la actualización salvo que su propio código o sus scripts escriban
documentos de GraphQL a mano. La consola, la aplicación de paneles, los SDK, dcctl y el servidor
MCP nunca envían nada de lo siguiente.
- Un documento escrito con comentarios
//o/* */, cadenas entre comillas invertidas o caracteres entre comillas simples ahora se rechaza con un error de sintaxis, y no se ejecuta nada de él. Las versiones anteriores los aceptaban, aunque no son GraphQL. Use#para los comentarios y"para las cadenas. - También se rechaza una cadena de bloque cuyo
"""de cierre sigue directamente a una barra invertida, como en\""". Ese escape es GraphQL válido, pero las versiones anteriores nunca lo leyeron como lo define la especificación: cerraban la cadena en esas tres comillas y leían el resto del documento a partir de ahí. Envíe ese texto en una variable. - Se rechaza una cadena seguida directamente de una comilla, como
"x""y". GraphQL lo lee como dos cadenas contiguas, que nunca son un valor válido; las versiones anteriores lo leían en cambio como el comienzo de una cadena de bloque. Solo"""abre una cadena de bloque, y la cadena vacía""no se ve afectada. - Por un WebSocket de GraphQL, una suscripción que el servidor no puede leer recibe ahora el error de sintaxis, no el mensaje que dice que solo se aceptan suscripciones. Un documento que nombra una operación que no contiene también recibe su propio error. Ambos son errores solo de esa operación; la conexión sigue abierta.
Límites de las solicitudes tiene los detalles.
Se elimina el campo message de las alarmas
Las alarmas tenían un campo message que nada rellenaba nunca: siempre era nulo. Se elimina en
todos los sitios donde aparecía:
- GraphQL: desaparecen
Alarm.messageyAlarmEvent.message(la suscripciónalarmStream). Una consulta o suscripción que siga seleccionandomessageahora se rechaza conCannot query field "message". Quítelo de sus propios documentos antes de actualizar. @devicechain/dashboardsy@devicechain/widgets:AlarmRowya no tienemessage, así que el código que leeAlarmRow.messageya no compila. El widget de tabla de alarmas ya no muestra una descripción emergente sobre la clave de la alarma, y la vista previa del editor de paneles ya no muestra mensajes de alarma inventados. Las versiones de estos paquetes anteriores a esta versión siguen seleccionandomessage, así que el servidor actualizado rechaza su lista de alarmas y los widgets de alarmas dejan de cargar: actualice los paquetes junto con la plataforma.- Notificaciones: los correos de alarma ya no tienen una línea
Messagey las cargas de los webhooks ya no tienen una clavemessage. Ninguna de las dos apareció nunca, porque el valor siempre estaba vacío. - MCP:
list_alarmsyget_alarmya no devuelvenmessage. - Base de datos: la columna vacía
messagese elimina de la tabla de alarmas cuando arranca device-management.
Mientras se despliega la actualización:
- Una pestaña de la consola abierta antes de la actualización, y un pod de la consola, de paneles o de MCP que siga en la versión anterior, dan un error en las listas de alarmas hasta que se recarga la pestaña o se sustituye el pod.
- Un pod de
device-managementque siga en la versión anterior no puede guardar una alarma nueva. La alarma se reintenta aproximadamente una vez por minuto y normalmente la guarda un pod ya actualizado. Si los pods de la versión anterior siguen en marcha más de unos cinco minutos, por ejemplo porque un pod nuevo nunca llega a estar listo, la alarma se abandona y se registra como mensaje no entregado, y solo se genera cuando su condición desaparece y vuelve a darse. Mantenga corto el despliegue y, después, revisedcctl dead-letters list --kind detection-action --source device-managementpara ver las alarmas que no se generaron. - Las listas de alarmas, y los reconocimientos y borrados de alarmas, que atiende un pod de
device-managementque siga en la versión anterior pueden fallar una vez por conexión a la base de datos. Repetir la petición funciona.
Un duplicado responde ahora con el código CONFLICT
No hay que hacer nada salvo que su propio código o sus scripts reconozcan un duplicado leyendo el mensaje de error. Un valor que debe ser único tiene los detalles.
- Una creación, actualización o renombrado que repite un valor que debe ser único lleva ahora
extensions.codeigual aCONFLICT. Decida por el código. Significa que la escritura chocó con un valor único, que no siempre es uno que usted envió: dos publicaciones del mismo registro que compiten por el siguiente número de versión también chocan, y entonces un reintento funciona. - Se sustituye el texto propio de la base de datos. Donde un mensaje terminaba en
duplicate key value violates unique constraint "…" (SQLSTATE 23505)oUNIQUE constraint failed: …, ahora termina enthe request conflicts with an existing record: a value that must be unique is already in use, que no nombra ningún índice ni columna de la base de datos. - Los rechazos que ya tenían su propia frase la conservan y ganan el código: renombrar a un token que ya está en uso, añadir una segunda membresía en el mismo inquilino y declarar una clave de comando que el perfil ya tiene.
dcctl sim createreconoce por el código un inquilino, una identidad o una membresía que ya existen, así que volver a ejecutarlo con el mismo nombre termina. Antes, una nueva ejecución se detenía en el paso de la membresía. Use undcctlde esta versión con una instancia de esta versión: una instancia anterior no envía el código, y estedcctlinforma entonces del duplicado como un error.- Estos no son duplicados y no llevan
CONFLICT: crear un inquilino con el token reservado de un inquilino eliminado, y un guardado rechazado porque el registro cambió desde que se leyó.
Las referencias y los valores rechazados responden con un código
No hay que hacer nada salvo que su propio código o sus scripts reconozcan estos rechazos leyendo el mensaje de error. Una referencia o un valor que se rechaza tiene los detalles.
- Una eliminación rechazada porque otros registros todavía se refieren al registro lleva ahora
extensions.codeigual aREFERENCE_VIOLATION: un perfil de dispositivo, un tipo de dispositivo u otro tipo todavía en uso, un grupo de entidades al que una regla de detección todavía se limita, un nivel de inquilino en el que todavía hay inquilinos, un inquilino que todavía tiene membresías, un proveedor de IA que todavía tiene concesiones y un canal de notificación que una regla de política todavía nombra. El mensaje no cambia. - Se sustituye el texto propio de la base de datos. Donde un mensaje terminaba en
violates foreign key constraint "…" (SQLSTATE 23503), ahora termina enthe request refers to a record that does not exist, or removes one that other records still refer toy llevaREFERENCE_VIOLATION. Un valor que la base de datos rechaza por cualquier otro motivo de integridad, como un valor obligatorio que falta, se responde igual conthe request contains a value this record does not alloweINVALID_VALUE. Ninguna de las dos frases nombra una tabla, una columna ni una restricción, ni repite un valor que usted envió. - Ninguno de los dos es
CONFLICT, así que nidcctlni el código que trataCONFLICTcomo «ya existe» continúan como si nada. Un rechazo en el que intervienen a la vez un duplicado y uno de estos lleva ahora el código nuevo en lugar deCONFLICT. - El servidor registra cada uno de estos rechazos de la base de datos como una advertencia que nombra la restricción, la tabla y la columna, porque normalmente responde antes la propia comprobación del servicio. El detalle, que puede repetir los valores enviados, no se registra.
Los niveles de alerta mayores que 2147483647 se rechazan, y cuatro números más ya no se desbordan
El level de una alerta se aceptaba hasta 4294967295, pero se sirve como un Int de GraphQL, que
llega hasta 2147483647, así que un nivel mayor se leía como un número negativo. Cuatro campos
tenían el mismo fallo: el número guardado se recortaba a 32 bits al salir, así que un valor mayor
que 2147483647 se convertía en un número negativo verosímil. Cada uno es ahora un error, como los
campos enumerados en Los valores de las credenciales se guardan exactamente como se
envían.
- Una alerta con un
levelmayor que 2147483647 se rechaza ahora al llegar. El mensaje del dispositivo se rechaza como datos erróneos, igual que una alerta sintype: HTTP responde400y una publicación MQTT se registra como mensaje no entregado. No hay nada que cambiar salvo que un dispositivo envíe esos niveles. - Una alerta ya guardada con un nivel así convierte en error el listado de alertas que la
incluye.
levelno puede ser nulo, así que toda la respuesta dealertEventses nula con el error, en lugar de faltar una sola alerta. Esto se aplica a las filas guardadas antes de la actualización y a las que se guarden mientras sigan en ejecución pods deevent-sourcesde la versión anterior. La fila permanece hasta que la retención la elimina. Para saber si tiene alguna, ejecute esto en la base de datos de event-management:SELECT tenant_id, device_token, count(*) FROM "event-management".alert_events WHERE level > 2147483647 GROUP BY 1, 2; - El
classifierde una última medición es el id de la definición de métrica a la que se asoció la lectura. Cuando esos ids superan 2147483647,classifieres un error solo en ese campo: se lee como nulo y elvalue, launity la hora de la lectura se siguen devolviendo. La consola y las herramientas MCP no leen este campo. (El mismo id es una cadena en los eventos de medición guardados, donde no tiene límite). - El
inactivityTimeoutdel estado de un dispositivo y ellogoMaxHeightde la marca de un inquilino solo se escriben con valores que caben, así que esto afecta solo a un valor escrito en la base de datos por otra vía. UninactivityTimeoutdemasiado grande convierte en error todo el listado de estados de dispositivo que lo selecciona. UnlogoMaxHeightdemasiado grande hace que falle la petición de la consola para el inquilino, así que la consola ya no puede cargar la marca, la configuración regional predeterminada ni la configuración de mapa de ese inquilino, y puede que el editor de marca no se abra. Para resolverlo, vuelva a fijar la altura del logotipo consetTenantBrandinga través de la API, enviando también el título y los colores: esa mutación los reemplaza juntos, así que uno que se omita se borra.
tenantDeletions en la API de administración está paginada
tenantDeletions en /api/user-management/admin/graphql recibe ahora un argumento de criterios y
devuelve una página, como auditEvents y deadLetters en la API de administración. Una llamada con
la forma anterior se rechaza.
# antes
tenantDeletions(completed: false, limit: 50, offset: 0) { token epoch completedAt }
# ahora
tenantDeletions(criteria: {pageNumber: 1, pageSize: 50, completed: false}) {
results { token epoch completedAt }
pagination { totalRecords }
}
pageNumber y pageSize son obligatorios. Un tamaño de página menor que 1 lee 100 registros y uno
mayor que 1000 lee 1000, como en las demás listas de la plataforma. Antes, omitir limit leía todo
el historial de eliminaciones. completed sigue siendo opcional. Vea Eliminación de
inquilinos.
Las entradas de auditoría nombran la fila que cambiaron
Veinte mutaciones auditadas escribían entradas en el diario de auditoría con la clave y la etiqueta de fila vacías: cambios de versión de perfiles, grupos y tipos de activo, reclamaciones de dispositivos, paneles, conectores, proveedores de inferencia y cambios de estado de comandos. Ahora registran la clave primaria de la fila afectada, y once de ellas también su etiqueta; los cambios de estado de comandos registran solo la clave. Las entradas escritas antes de la actualización se quedan como estaban. No hay que hacer nada.
Observabilidad
Alertas añadidas y renombradas
Si enruta o silencia alertas por su nombre, estas son las alertas nuevas del chart y la que cambia de nombre. Cada una enlaza con el apartado que la explica.
| Alerta | Gravedad | Qué indica | Detalles |
|---|---|---|---|
JetStreamStreamNearFull | warning | Antes EventProcessingStreamNearFull; ahora cubre los flujos de todos los servicios | detalles |
JetStreamDurableLostUnread | critical | Un consumidor perdió mensajes que no había leído | detalles |
JetStreamDurableStalledBehindStream | critical | Un consumidor detenido está perdiendo mensajes por delante de él | detalles |
JetStreamDurableFallingBehind | warning | Un consumidor lleva 15 minutos con más de 10000 mensajes esperando | detalles |
JetStreamReplicationUnobserved | warning | Un pod no puede leer el estado de replicación de un flujo | detalles |
ReaderHeldMessagePastAckWait | warning | Un mensaje se retuvo más allá de su ventana de confirmación y se volvió a entregar | detalles |
MaxDeliveryRecordsWaiting | warning | No se están registrando los avisos de mensajes que agotaron sus intentos | detalles |
ReplayCoveredDeliveriesExhausted | warning | El punto de control del motor de detección lleva demasiado tiempo sin guardarse | detalles |
RateLimiterOverflowInUse | warning | La ingesta HTTP comparte una sola asignación entre nombres de inquilino no confirmados | detalles |
TenantsMeteredAtPlatformDefault | warning | Un servicio no puede leer los techos de los inquilinos y los mide con el valor por defecto | detalles |
ReactShedLettersOverBudget | warning | Las acciones descartadas se resumen en lugar de registrarse una a una | detalles |
RateMeteringClockFallback | warning | Las acciones de salida se miden sin su hora de disparo | detalles |
ConnectorDispatchRateLimited | warning | El servicio de conectores descarta envíos que el motor de detección admitió | detalles |
CredentialAttemptStoreFull | critical | El bucket que cuenta los inicios de sesión fallidos está lleno | detalles |
DeviceCredentialAttemptStoreFull | warning | El bucket que cuenta las conexiones MQTT fallidas con contraseña está lleno | detalles |
DeviceFactPublishFailing | warning | Fallan las notificaciones de cambios de reglas, dispositivos y atributos | detalles |
DetectFactsRepaired | warning | El motor de detección corrigió un cambio del que no se le había avisado | detalles |
DetectFactReconcileFailing | warning | Falla la comparación del motor de detección con device-management | detalles |
TenantPurgeStalled | warning | Una eliminación de inquilino no avanza | detalles |
TenantPurgeVisibilityLost | warning | Nada informa de si las eliminaciones de inquilinos avanzan | detalles |
ReactPoisonDropping, DeadLetterStoreLosing y DeadLetterWriteLost conservan su nombre y pasan
a su propio grupo de reglas (detalles).
Los servicios registran en info por defecto, y el nivel se puede fijar
Todos los servicios registraban en debug, se quisiera o no. Ahora registran en info por
defecto, y una clave nueva de la configuración de la instancia, infrastructure.logging.level, fija
el nivel para toda la instancia. Vea El nivel de registro.
- Acepta exactamente
trace,debug,info,warnoerror, en minúsculas. Cualquier otro valor, incluidosINFOo un número, impide que los servicios arranquen, y el registro nombra la clave y los valores aceptados. - Con el nivel por defecto desaparecen líneas que quizá buscaba: las líneas por mensaje de la
ruta de ingesta, las confirmaciones de lectura y escritura del broker y diagnósticos similares.
Fije
debugpara volver a verlas.debugytraceescriben una línea por cada mensaje de dispositivo, así que sirven para diagnosticar un problema, no para funcionar. - El documento de configuración de un servicio ya no se escribe en el registro al arrancar. En
su lugar, el servicio registra
config_sha256, los primeros 16 caracteres hexadecimales del SHA-256 del documento. - Una instancia instalada con
dcctl bootstrapfunciona eninfo, ydcctltodavía no tiene ninguna opción para cambiar el nivel. Una instalación con el chart lo fija con sus demás valores, por ejemplo--set instance.config.infrastructure.logging.level=debug. Coninstance.existingSecret, añádalo al documento que proporciona y actualiceinstance.existingSecretChecksum.
Los mensajes de la base de datos son líneas de registro estructuradas, y una consulta que no encuentra nada ya no se registra como un fallo
Las versiones anteriores imprimían los mensajes de la base de datos en un formato propio: texto de
varias líneas y en color, escrito fuera del registro JSON del servicio, sin los campos instance
ni area, fuera cual fuera el valor de infrastructure.logging.level. Cada servicio que escribe
datos de inquilinos imprimía uno de esos bloques, con el texto record not found, por cada
transacción de base de datos que los escribía; en event-management era uno por cada evento
guardado. Ese bloque era la respuesta normal a una comprobación previa a cada escritura, no un
error, y con un ritmo de eventos alto ocupaba la mayor parte del registro y ocultaba fallos reales.
- Los mensajes de la base de datos son ahora líneas de registro JSON, en
errorcuando una sentencia falló (database statement failed) y enwarncuando tardó más de 200 ms (slow database statement), con los campossql,rows,elapsed_msycaller. Una consulta que no encuentra filas ya no se registra como un fallo. Eso incluye la comprobación previa a cada escritura. - El campo
sqlya no muestra los valores con los que se envió una sentencia. Muestra sus marcadores ($1,$2, …). Las versiones anteriores incluían los valores, también en la línea de una escritura fallida. El mensaje de error de la propia base de datos se sigue registrando tal cual, y algunos citan el valor que rechazaron. sqlDebugsigue ahora el nivel de registro. Sus líneas por sentencia se escriben eninfo, así que una instancia con nivelwarnoerrorya no las muestra.- Un servicio que no puede alcanzar su propia base de datos al arrancar registra cada intento
fallido en
error, comofailed to initialize database, got error …. Las versiones anteriores lo imprimían como texto fuera del registro JSON.
Si buscaba el texto anterior (por ejemplo record not found o SLOW SQL), busque en su lugar el
campo message. No hay nada que configurar.
Dos avisos nuevos: un flujo que no se puede leer, y descartes de conectores que el motor de detección admitió
JetStreamReplicationUnobserved(warning, grupojetstream-replication) se dispara cuando un pod en ejecución lleva 15 minutos sin poder leer el estado de replicación de un flujo que antes sí podía leer. Hasta ahora, un servicio que no podía leer un flujo dejaba de informar de él, y los demás avisos de replicación se quedaban en silencio para ese flujo en lugar de indicarlo. También se dispara para todos los flujos en todos los pods durante una caída del bróker, y es deliberado: nada más en el chart informa de una. Se resuelve seis horas después de la última lectura del flujo, tanto si se puede volver a leer como si no. Consulte Replicación.ConnectorDispatchRateLimited(warning, grupogovernance) se dispara cuando outbound-connectors lleva 15 minutos descartando envíos por superar la tasa de salida de su inquilino. El motor de detección ya descarta las acciones que superan la cuota antes de enviarlas, así que esto significa que los dos servicios no coinciden en el techo (lo más habitual es que sus valores por defecto de la plataforma sean distintos) o que se reintentan envíos que fallan y se vuelven a medir. Antes de actualizar, compruebe queoutboundMessagesPerSecondyoutboundBursttienen el mismo valor para event-processing y outbound-connectors. Si no, se dispara cada vez que un inquilino medido con el valor por defecto de la plataforma envía más rápido que el menor de los dos. Consulte Inquilinos medidos con el valor por defecto de la plataforma.JetStreamLeaseBucketNotReplicatedtiene un resumen nuevo, "The partition-lease bucket is not replicated". Su nombre, etiquetas y severidad no cambian. Actualice las rutas o los silencios que filtren por el texto del resumen anterior.
Las alertas de mensajes no entregados pasan a su propio grupo de reglas
Las tres alertas de mensajes no entregados, ReactPoisonDropping, DeadLetterStoreLosing y
DeadLetterWriteLost, se distribuyen ahora en su propio PrometheusRule dead-letter (grupo
devicechain.dead-letter) en lugar de en event-processing. Sus nombres, etiquetas, severidades,
umbrales y descripciones no cambian, así que las rutas y los silencios que filtran por el nombre de
la alerta o por sus etiquetas siguen funcionando.
- Una alerta pendiente o disparada durante la actualización vuelve a empezar. Prometheus ve una
regla nueva, así que la alerta se resuelve y vuelve cuando ha pasado su espera
for(5 o 10 minutos), si la condición se mantiene. - Si selecciona objetos PrometheusRule por nombre, o busca grupos de reglas en la interfaz de
Prometheus, añada
dead-letter. DeadLetterStoreLosingyDeadLetterWriteLostya no terminan enor vector(0). Una expresión que no devuelve nada y una que devuelve una comparación falsa dejan la alerta en el mismo estado, así que la cláusula no cambiaba nada. Ambas alertas se disparan y se resuelven exactamente igual que antes.
Dos avisos para una eliminación de inquilino que no avanza
Una eliminación de inquilino que no puede terminar deja sanas las métricas de pasada del propio
coordinador, así que el chart genera ahora alertas directamente sobre ella, en un grupo de reglas
nuevo, devicechain.tenant-purge (PrometheusRule tenant-purge):
TenantPurgeStalled(warning) se dispara cuando la eliminación abierta más antigua lleva abierta más del doble de la retención de identificador configurada (24 horas con la retención por defecto) durante 15 minutos.TenantPurgeVisibilityLost(warning) se dispara cuando los indicadores de eliminación de user-management llevan 15 minutos sin recogerse, de modo que la primera alerta no podría dispararse.
user-management exporta los dos indicadores que leen,
devicechain_usermanagement_tenant_purge_in_flight y
devicechain_usermanagement_tenant_purge_oldest_age_seconds. Un tokenHoldSeconds de 0 se lee
como el valor por defecto, igual que lo lee el servicio. Vea Cómo te
enteras.
Las tareas de mantenimiento informan de cada pasada, y los barridos ya no se ejecutan con un intervalo exacto
Diez tareas de mantenimiento, los barridos, reconciliadores y planificadores que se ejecutan con un
temporizador, exportan ahora tres series cada una: devicechain_<area>_<task>_passes_total{outcome},
…_pass_duration_seconds y …_last_success_timestamp_seconds. El resultado es complete,
partial, failed, skipped o cancelled. skipped (otra réplica tiene el bloqueo de la tarea)
y cancelled (el servicio se estaba deteniendo) no son fallos, y una pasada omitida no mueve la
hora del último éxito. Pasadas de mantenimiento enumera las
tareas.
Cinco de ellas, el barrido de mensajes no entregados y el coordinador de eliminación de inquilinos de user-management, el barrido de retención y el planificador de escalados de notification-management, y el barrido de anclas de event-management, empiezan ahora cada pasada en un momento aleatorio dentro de un 10 % a cada lado de su intervalo, en lugar de exactamente en él, de modo que las réplicas que arrancaron juntas ya no llegan a la base de datos en el mismo instante. No hay nada que configurar.
Despliegue y dimensionamiento
device-management y event-management pueden usar más CPU
device-management y event-management pueden usar ahora hasta 2 núcleos de CPU cada uno; los
demás servicios de backend mantienen el límite de 500m. Con 500m, device-management resolvía como
máximo unos 720 eventos por segundo en un clúster de pruebas de cuatro nodos, por debajo de los 1000
mensajes por segundo que cada inquilino tiene permitidos por defecto, así que un inquilino que
enviaba a ese ritmo acumulaba un retraso que crecía mientras siguiera enviando. event-management
necesita alrededor de medio núcleo a ese ritmo, todo su límite anterior, así que habría sido el
siguiente límite. Consulte Dimensionamiento de los servicios.
La actualización reinicia esos dos servicios una vez, pod a pod. Las solicitudes no cambian, así que
ningún pod necesita más espacio en un nodo, y las instancias --compact conservan sus solicitudes
reducidas.
- El bloque
functionalAreas.<servicio>.resourcesde un servicio se combina ahora clave a clave sobre losresourcesde nivel superior, en lugar de sustituirlos. Elfrontend.resourcesde la consola no cambia. Revise los pods generados antes de actualizar si sus valores definenresourcespara un solo servicio:- Si definía solo
requestspara un servicio, no tenía límites y ahora recibe los de nivel superior. Para que un servicio siga sin límite, quite ese límite de losresourcesde nivel superior y defínalo en los servicios que lo necesiten. - Si definía solo
limitspara un servicio, Kubernetes le daba solicitudes iguales a esos límites; ahora recibe las solicitudes de nivel superior (100m y 128Mi, o 25m y 64Mi con--compact), que reservan menos en el nodo y pueden cambiar la clase QoS del pod. Defina lasrequestsde ese servicio para conservar su reserva. - Si su bloque para
device-managementoevent-managementno definelimits.cpu, ese servicio recibe ahora el nuevo límite de 2 núcleos. - Una solicitud por encima del límite combinado se rechaza al generar el chart, indicando el
servicio; por ejemplo, un servicio que define solo
requests.memory: 512Mifrente al límite de 256Mi de nivel superior. Defina también su límite. - Se rechaza cualquier clave distinta de
requests,limitsoclaimsbajo losresourcesde un servicio.
- Si definía solo
- Un
resources.limits.cpude nivel superior por encima de 2 núcleos ya no llega adevice-managementni aevent-management, porque su nuevo límite de 2 núcleos es propio y la clave propia de un servicio prevalece sobre la de nivel superior. Si sus valores suben solo el límite de CPU de nivel superior, por ejemplo a 4, la actualización BAJA estos dos servicios a 2 núcleos. DefinafunctionalAreas.device-management.resources.limits.cpuyfunctionalAreas.event-management.resources.limits.cpupara conservar lo que tenían. Por la misma razón, unresources.requests.cpude nivel superior por encima de 2 núcleos, que antes se generaba, ahora se rechaza, indicando como límite superado el propio del servicio; suba también ese límite. - Un
ResourceQuotade espacio de nombres sobrelimits.cpu, o unLimitRangecon unmaxde CPU, puede rechazar los dos pods ahora que sus límites son más altos. Nada de lo que instala DeviceChain crea ninguno de los dos; revise los que haya añadido. - Para conservar los límites anteriores, defina
functionalAreas.device-management.resources.limits.cpuyfunctionalAreas.event-management.resources.limits.cpuen500m. Eso restaura el techo anterior de unos 720 eventos por segundo.
El almacén de respaldos dentro del clúster se descarga de una imagen mantenida
El almacén de objetos que dcctl install ejecuta en dc-system para guardar los respaldos de las
bases de datos descargaba su imagen de MinIO de quay.io/minio/minio. Esas imágenes ya no se
publican: el registro rechaza una descarga anónima, de modo que en una máquina que no tuviera ya la
imagen en caché, dcctl install se detenía con el almacén en ImagePullBackOff. Los clústeres
que ya tenían la imagen en caché siguieron funcionando, pero solo mientras el pod del almacén
permaneciera en un nodo que la tuviera: un pod trasladado a otro nodo —por un drenaje, un desalojo
o un nodo sustituido— no podía arrancar, y el archivado se detenía hasta que pudiera. Actualizar
elimina esa exposición.
Esta versión descarga cgr.dev/chainguard/minio, fijada por digest: una compilación de una
bifurcación mantenida del mismo servidor MinIO, que sigue bajo licencia AGPL-3.0. Lee tal cual los
datos que escribió el servidor anterior.
- Antes de actualizar, asegúrese de que sus nodos pueden descargar de
cgr.dev: permítalo en sus reglas de salida, o repliquecgr.dev/chainguard/miniocon el digest que fija esta versión. Si la descarga falla, el almacén sigue caído y las bases de datos conservan localmente su registro de escritura anticipada hasta que vuelva. - Una instalación nueva vuelve a funcionar con el destino de respaldo predeterminado.
- En un clúster existente,
dcctl installreinicia una vez el pod del almacén con la nueva imagen. Los respaldos y el registro de escritura anticipada guardados se conservan. Mientras el pod se reinicia, el archivado se detiene y las bases de datos retienen el registro localmente, durante el tiempo que la nueva imagen tarde en descargarse y arrancar. - Esto no se deshace instalando una versión anterior. El
dcctl installde una versión anterior detendría el almacén y después no podría descargar la imagen que indica, y el archivado se detendría hasta volver a ejecutar eldcctl installde esta versión. - Si
dcctl installfalló por esto en una versión anterior, vuelva a ejecutar eldcctl installde esta versión: sustituye el almacén que dejó aquel intento (consulte Undcctl installfallido puede volver a ejecutarse).
No hay nada que configurar. Si dirige los respaldos a su propio almacén de objetos con
--backup-credentials-file, no cambia nada para usted.
Un dcctl install fallido puede volver a ejecutarse
Si dcctl install fallaba mientras arrancaba el almacén de objetos de los respaldos, por ejemplo
porque no se podía descargar su imagen, o se interrumpía en ese punto, todas las ejecuciones
posteriores fallaban también, antes de cambiar nada, con Unexpected Identity Change sobre
kubernetes_deployment_v1. Ocurría con Terraform 1.12 o posterior, y la única salida era borrar el
clúster y su directorio en ~/.devicechain/clusters/. Ahora una nueva ejecución elimina el almacén
a medio crear, lo crea de nuevo y espera a que esté listo. Un clúster que ya estaba bloqueado así se
recupera del mismo modo, sin ningún paso manual, una vez corregida la causa: ejecute de nuevo el
mismo dcctl install. Si la causa sigue ahí, la nueva ejecución falla del mismo modo en lugar de
dar el clúster por instalado.
dcctl installcomprueba ahora que el almacén de objetos de los respaldos ha terminado su despliegue antes de dar el clúster por instalado. Un cambio en el almacén que agotaba su tiempo, como una imagen nueva que no se podía descargar, lo aceptaba la ejecución siguiente, porque el cambio fallido ya había quedado registrado. Esa ejecución espera ahora hasta cinco minutos al almacén y después falla indicándolo.- El proveedor de Kubernetes pasa de 2.38.0 a 3.2.1. La corrección está en esa versión. El primer comando de dcctl que aplica o destruye infraestructura en cada clúster lo descarga. No cambia nada en el clúster.
- Los proveedores de infraestructura quedan fijados a versiones exactas (Kubernetes 3.2.1,
Helm 2.17.0), y dcctl lleva el
.terraform.lock.hclde cada raíz a ellas en cada ejecución. Antes, cada clúster conservaba las versiones que resolvió su primera instalación. Cada ejecución que aplica o destruye infraestructura, incluidadcctl destroy, consulta ahora esas versiones al registro de proveedores, así que el registro, o un espejo de proveedores que haya configurado, debe ser accesible. El proveedor TLS, que no se usaba, ya no se declara. - Un dcctl anterior no puede operar un clúster sobre el que se ha ejecutado esta versión. Su
initrechaza el archivo de bloqueo, que ahora indica versiones de proveedor que su configuración no admite. - Si ejecuta la configuración de OpenTofu directamente en lugar de a través de dcctl, ejecute
tofu init -upgradeuna vez en cada raíz: uninitnormal rechaza un archivo de bloqueo con las versiones anteriores.
Los archivos de la versión llevan una procedencia de compilación firmada
Desde v0.18.0, cada versión publica devicechain_<version>_provenance.sigstore.json (la versión
sin su v, p. ej. devicechain_0.18.0_provenance.sigstore.json) junto a los
archivos de dcctl y dc-edge-agent: un registro firmado de la ejecución del flujo de trabajo que
los compiló. Para comprobar un archivo que ha descargado:
gh attestation verify <archive> --repo devicechain-io/devicechain
# o sin conexión, contra el paquete de la versión
gh attestation verify <archive> --repo devicechain-io/devicechain \
--bundle devicechain_0.18.0_provenance.sigstore.json
No hay que hacer nada.
v0.21.1: un parche, nada que hacer
v0.21.1 es una actualización directa desde v0.21.0: dcctl install para el clúster y después
dcctl upgrade para cada instancia, como en la v0.21.0. No añade ninguna
migración, por lo que la base de datos queda intacta, y no cambia ninguna API, tema, permiso,
valor del chart ni clave de configuración. Todo lo que describe la sección de la v0.21.0 que sigue
es exactamente lo que estás ejecutando. Si vienes de v0.20.0 o una versión anterior, sigue
primero las notas de la v0.21.0; se aplican sin cambios.
Cuatro correcciones:
- Leer de la base de datos vuelve a costar lo que costaba antes de la v0.21.0. La v0.21.0 fue
la primera versión compilada con la cadena de herramientas Go 1.27, y con ella el controlador de
PostgreSQL hacía una inspección de tipos adicional por cada columna de cada fila que leía. Los
servicios que leen mucho, device-state sobre todo, gastaban por ello bastante más CPU en la
ingesta. Las filas vuelven a leerse como en
v0.20.0, y el rendimiento de ingesta de device-state queda restablecido. En una prueba de rendimiento centrada en lecturas, la misma lectura pasó de 85–95 ms a 31–35 ms. - La vista Rule Health cuenta los disparos en PostgreSQL. Hasta ahora mostraba cero disparos y ninguna hora de último disparo para todas las reglas, porque PostgreSQL rechazaba la sentencia que registra un disparo. La detección no se vio afectada: las reglas generaron y cerraron alarmas con normalidad en todo momento. Los recuentos empiezan con la actualización; los disparos anteriores nunca se registraron.
- Crear una identidad con
enabled: falsecrea una identidad deshabilitada. Antes se guardaba habilitada. La misma corrección cubre los demás registros que llevan un indicador de habilitación. Una identidad creada deshabilitada conv0.21.0o una versión anterior sigue habilitada: revisa las que quisiste crear deshabilitadas y deshabilítalas. - Detener la fuente de eventos HTTP espera a que sus conexiones terminen de cerrarse, de modo que una fuente detenida ya no actualiza sus métricas después de haberse detenido.
v0.21.0 — un dispositivo puede quedar excluido por completo, y las actualizaciones se firman
v0.21.0 es una actualización en su sitio desde v0.20.0: dcctl install para el clúster y
después dcctl upgrade para cada instancia, como se describe para la v0.17.0. Si
vienes de v0.19.0 o una versión anterior, lee también las notas de v0.20.0: sus
recuentos de filas y de fragmentos y sus pasos se aplican también a esta actualización. Una instancia
creada por la v0.16.0 o una versión anterior sigue teniendo que
destruirse y crearse de nuevo.
La mayor parte de esta versión es capacidad nueva que no cambia nada hasta que la usas: un dispositivo puede deshabilitarse por completo, los artefactos de actualización pueden firmarse, una flota puede importarse desde un archivo CSV, los ajustes preestablecidos de alertas y una comprobación de notificación se suman al formulario de reglas de detección, y una conexión de inicio de sesión único puede confiar en una autoridad de certificación privada. Lo que cambia para alguien: un dispositivo creado para entender solo asignaciones de actualización sin firmar rechaza una firmada, un artefacto solo puede asignarse cuando lo ha firmado un firmante activo, se rechaza un Factory Reset de LwM2M en bruto, un dispositivo deshabilitado queda excluido de la configuración, las actualizaciones y el registro LwM2M, con sus comandos en cola retenidos, y los archivos compilados de la consola pasan a una ruta nueva. Todos los binarios se compilan ahora con Go 1.27.2.
Antes de actualizar
- Dispositivos que reciben actualizaciones por aire: despliega primero un actualizador que
verifique firmas. Una asignación puede llevar ahora
trust: "SIGNED"con una firma, un id de clave y un algoritmo, y un dispositivo que solo entiendeUNVERIFIEDla rechaza. Nada firma una asignación hasta que un inquilino registra un firmante y firma un artefacto, así que despliega el actualizador verificador antes de hacerlo. Consulta Las asignaciones de actualización pueden firmarse. - Decide cómo se confía en los artefactos de actualización. Con
updates.allowUnverifiedArtifactsenfalse, solo puede asignarse un artefacto firmado por un firmante activo de su inquilino. Si activas la opción para asignar artefactos sin firmar, sigue aplicándose. Consulta Las asignaciones de actualización pueden firmarse. - Deja de enviar un Factory Reset de LwM2M en bruto. Un comando que ejecuta
/3/0/5se rechaza al encolarse, también desde una regla o un lote. Consulta Se rechaza un Factory Reset de LwM2M en bruto. - Comprueba qué depende de la ruta estática de la consola. Sus archivos compilados se sirven
ahora desde
/_app/en lugar de/assets/. Consulta Los archivos compilados de la consola cambian de ruta.
Durante la actualización
- Actualiza
device-managementycommand-deliverya la vez.command-deliveryconserva su propio registro de qué dispositivos están deshabilitados y lo rellena desdedevice-management. Los comandos de un inquilino no se despachan hasta la primera sincronización correcta de ese inquilino, así que un pod nuevo decommand-deliveryque aún no puede llegar a un pod dedevice-managementcon la nueva consulta los retiene. No se pierden, y salen en cuanto la sincronización tiene éxito. - Cuatro áreas aplican una migración en su primer arranque:
device-managementañade a la tabla de dispositivos una columnadisabled_atanulable y un índice parcial,command-deliverycrea dos tablas pequeñas,update-managementcrea el registro de firmantes yuser-managementañade una columna de paquete de CA a los proveedores de identidad. Cada una puede volver a ejecutarse por separado, ninguna reescribe una fila existente y ninguna requiere recrear. Consulta Tablas y columnas nuevas. update-managementempieza a leer el censo de dispositivos solo a partir de hechos nuevos, así que no necesita ponerse al día ni ningún paso del operador.
Volver a v0.20.0 tras la actualización conserva las columnas y tablas nuevas, que v0.20.0
ignora. No conoce los firmantes, así que allowUnverifiedArtifacts por sí solo decide lo que puede
asignar.
Después de actualizar
- Recarga la consola una vez. Un navegador que conserva el
index.htmlanterior pide las rutas de archivo antiguas hasta que se recarga.index.htmlse sirve conno-cache, así que basta una navegación. - Da a los roles personalizados la nueva autoridad si gestionan firmantes. Los administradores de
inquilino tienen
artifact:trust; un rol que hayas definido no. Consulta Las asignaciones de actualización pueden firmarse. - No se requiere nada más. Deshabilitar un dispositivo, firmar, importar, los ajustes preestablecidos y el paquete de CA empiezan todos sin usarse.
Qué cambia
Un dispositivo deshabilitado queda excluido por completo, y sus comandos en cola se retienen
Un dispositivo puede deshabilitarse por completo en lugar de buscar y revocar cada credencial, con
updateDevice(enabled: false) o la acción Deshabilitar de la página del dispositivo. Mientras está
deshabilitado:
- sus eventos se rechazan en la resolución, sus conexiones al broker se rechazan y los comandos
nuevos para él se rechazan con
DEVICE_DISABLED; - se rechazan su sincronización de configuración y sus informes de estado, así como sus informes de
progreso de actualización y de inventario, sus concesiones de descarga y sus descargas, y un
enlace de descarga emitido antes de deshabilitarlo responde 403;
assignArtifactlo rechaza conDEVICE_DISABLED; - un registro LwM2M responde 4.03 Forbidden, y un dispositivo registrado se da por terminado en su siguiente Update para que se registre de nuevo y sea rechazado;
- sus reglas de ausencia se desarman, de modo que no se genera ni se mantiene para él ninguna alarma de «dispositivo en silencio»;
- los comandos ya en cola quedan retenidos: ni se envían ni se cancelan. Un comando retenido
sigue caducando por su propio TTL. Habilitar el dispositivo los libera, los más antiguos primero, y
eliminar un dispositivo deshabilitado los cancela.
deviceCommandHoldycancelDeviceCommandslos muestran y los borran.
Habilitar el dispositivo envía una vez su última configuración y vuelve a ofrecer sus asignaciones de actualización vigentes. Una sesión ya abierta sigue abierta hasta la siguiente comprobación de la conexión. Consulta Deshabilitar un dispositivo.
Si hoy deshabilitas dispositivos y cuentas con que sus comandos en cola salgan, ahora esperan.
Las asignaciones de actualización pueden firmarse
Un inquilino puede registrar las claves públicas en las que confía para firmar artefactos de
actualización: ECDSA P-256 o Ed25519, como un único PEM PUBLIC KEY. Una firma sobre el resumen
SHA-256 de un artefacto se verifica al añadirla, y un artefacto cuenta como firmado mientras al
menos un firmante activo del mismo inquilino lo haya firmado. Revocar un firmante, que es terminal,
devuelve sus artefactos a sin verificar sin tocarlos. Se rechaza cualquier cosa que contenga una
clave privada.
Qué cambia con esto:
- La nueva autoridad
artifact:trustregistra y revoca firmantes. La tienen los administradores de inquilino y ningún rol que hayas definido;artifact:writeañade una firma. - Asignar requiere un artefacto firmado, salvo que
updates.allowUnverifiedArtifactsseatrue. Esa opción no cambia y sigue siendofalsepor defecto; lo que cambia es que un artefacto firmado ya no la necesita. - El contrato del dispositivo crece. Un
update.assignmentpara un artefacto firmado llevatrust: "SIGNED", unasignature(base64 estándar), unkeyId(el SHA-256 en hexadecimal minúsculo de la clave pública codificada en DER) y unalgorithm(ECDSA_P256_SHA256oED25519); ambos firman el resumen en bruto de 32 bytes."v":1no cambia y una asignación sin firmar es idéntica byte a byte a la de antes. Un dispositivo que solo entiendeUNVERIFIEDrechaza una asignaciónSIGNED, lo que falla de forma cerrada. Un dispositivo informaUNKNOWN_SIGNERoSIGNATURE_INVALIDbajoFAILEDen la etapa de descarga completada. - Una firma no ata la versión. Firmar el resumen dice quién produjo los bytes, no de qué versión son; consulta Actualizaciones por aire.
Cuando se asigna un artefacto, la firma más antigua de un firmante activo se fija a la
asignación (UpdateAssignment.signedBy nombra al firmante), y esa firma es la que recibe el
dispositivo. De ello se derivan dos cosas:
- Un dispositivo debe tener la clave de cada firmante que pueda fijarse. Pon la clave de un firmante nuevo en tus dispositivos antes de revocar el anterior. Revocar un firmante después de fijar la firma no cambia un mensaje ya enviado.
- Desactivar
allowUnverifiedArtifactsdetiene el envío de asignaciones sin firma, incluido un reenvío cuando un dispositivo sincroniza. Una asignación hecha env0.20.0no tiene firma, así que con el ajuste desactivado su dispositivo conserva lo que tiene hasta que asignes un artefacto firmado. Una asignación cuya firma fijada no se puede leer no se envía; nunca se envía comoUNVERIFIED.
Para registrar un firmante y firmar un artefacto, sigue Firmar actualizaciones.
Se rechaza un Factory Reset de LwM2M en bruto
Un lwm2m.execute sobre el recurso 5 del objeto Device (/3/0/5) se rechaza al encolarse con
COMMAND_DESTRUCTIVE_REFUSED, tanto para un comando individual como para un lote y para un comando
enviado por una regla de detección, en cualquier grafía de la ruta, y de nuevo antes de que llegue al
dispositivo. Las reglas de detección tratan el código como permanente y no reintentan. Reboot
(/3/0/4), los demás execute y las lecturas no se ven afectados. Está prevista una operación con
nombre, confirmada y para un solo dispositivo, para Factory Reset; hasta entonces la ruta en bruto
permanece cerrada. El ejecutor también rechaza cualquier carga de lectura, escritura o ejecución
cuya clave de ruta aparezca más de una vez.
Los lotes pueden reintentarse por resultado, y una petición LwM2M sin respuesta es un TIMEOUT
Cada comando que termina lleva ahora un outcome almacenado, escrito por la operación que lo terminó:
SUCCEEDED, NOT_DELIVERED (la plataforma puede demostrar que nunca llegó al dispositivo), REJECTED
(el dispositivo dijo que no lo hizo) o UNKNOWN (puede haber llegado al dispositivo y haber actuado). Una
migración añade la columna y la rellena para los comandos ya terminados: SUCCESSFUL pasa a SUCCEEDED,
EXPIRED y CANCELLED pasan a NOT_DELIVERED, y cualquier otro comando terminado, incluido uno que
falló o agotó el tiempo, pasa a UNKNOWN. Los commands pueden buscarse por outcomes.
retryCommandBatch emite un lote nuevo, enlazado en ambos sentidos con el original (retryOf, retries),
para los dispositivos que indiques y que no tuvieron éxito. El comando se copia del original, nunca se
reescribe. Los dispositivos con resultado UNKNOWN se rechazan con RETRY_UNKNOWN_NOT_ACKNOWLEDGED salvo
que la petición fije includeUnknown, de modo que un comando anterior a la actualización que falló o
agotó el tiempo necesita ese indicador para reintentarse. Un dispositivo que ya tiene un comando vivo o
con éxito en cualquier lote de la familia del reintento se rechaza, para que no se envíe un comando dos
veces porque haya dos reintentos abiertos. Requiere command:write. La página del lote en la consola
tiene una acción Reintentar…. Consulta
Comandar una flota.
Cambio de comportamiento. Una petición LwM2M que no recibe respuesta del dispositivo se notifica
ahora como TIMEOUT, igual que en los demás transportes. Si lees el estado de un comando LwM2M para
decidir qué hacer a continuación, espera TIMEOUT ahí.
Los comandos disruptivos pueden esperar a una ventana de mantenimiento
A un tipo de dispositivo se le puede asignar una ventana de mantenimiento semanal recurrente: una zona
horaria y de 1 a 56 franjas semanales, que se gestionan en la API de device-management (device:write).
Cada definición de comando gana un impact de NORMAL, DISRUPTIVE o DESTRUCTIVE; una definición que ya
existe se lee como NORMAL, y lwm2m.reboot se crea ahora como DISRUPTIVE. Nada se retiene hasta que se
asigna una ventana a un tipo de dispositivo y un comando de ese tipo es DISRUPTIVE o DESTRUCTIVE.
Entonces, para un dispositivo de ese tipo, un comando así encolado mientras la ventana está cerrada se acepta
y espera, aún QUEUED, hasta la siguiente apertura. Uno encolado con la ventana abierta queda ligado a esa
apertura: si no se ha enviado cuando la ventana se cierra, espera a la siguiente en lugar de salir cuando el
dispositivo vuelva. Esto vale para un comando suelto, un lote y un comando enviado por una regla de detección,
y en cada ruta que envía un comando o confirma uno en vuelo. Un comando cuyo expiresAt cae antes de la
siguiente apertura se rechaza con WINDOW_AFTER_EXPIRY, en un lote como rechazo por dispositivo. Un lote
informa de cuántos de sus comandos están en waiting.
Dos comportamientos que conviene conocer. Si un dispositivo confirma un comando en vuelo después de que la ventana se haya cerrado, la confirmación se rechaza; el comando se aparca y pasa a la siguiente apertura. Y desasignar una ventana de un tipo de dispositivo no libera los comandos ya marcados con ella.
Para configurar una, consulta Ventanas de mantenimiento.
Los archivos compilados de la consola pasan a /_app/
Un enlace a la página de un recurso (/assets/<token>) devolvía 404 al cargar o refrescar, porque el
JavaScript y el CSS compilados de la consola estaban bajo /assets/ y el servidor web respondía 404
a todo lo que allí no tenía. Los archivos compilados están ahora bajo /_app/, con la misma caché
inmutable de un año, y la página del recurso carga desde un enlace directo. La aplicación de paneles
bajo /dash no cambia. Si usas un proxy, una caché o una lista de permitidos para las rutas de la
consola, permite /_app/.
Un registro inexistente se responde con NOT_FOUND
Una petición que nombra un rol, nivel de inquilino, identidad, membresía, inquilino, cliente OAuth,
proveedor de identidad o solicitud de vínculo de SSO que no existe, o un dispositivo que claimDevice
no encuentra, responde ahora con extensions.code NOT_FOUND, como dice la
referencia de GraphQL. Antes solo llevaba un mensaje como
role not found y ningún código. Los mensajes no cambian, así que un cliente que comparaba el
mensaje sigue funcionando; un cliente que trataba la ausencia de código como «otro error» debe
manejar el código.
Go 1.27.2 acepta horas de evento escritas con escapes JSON
Todos los binarios se compilan con Go 1.27.2. Un dispositivo que escribía la hora de un evento con
escapes JSON en la cadena, como "2026-10-13T12:00:00Z" para ...Z, veía su evento enviado a la
cola de mensajes fallidos al decodificarlo con Go 1.26. Es JSON correcto, y ahora se acepta y se
guarda. No hay que hacer nada.
Tablas y columnas nuevas
| Área | Cambio |
|---|---|
device-management | devices.disabled_at timestamptz (nulo significa habilitado) y el índice parcial idx_devices_disabled; la tabla maintenance_windows, device_types.maintenance_window_id y command_definitions.impact |
command-delivery | device_gates y device_gate_syncs, con sus índices únicos y parciales; commands.outcome (rellenado para los comandos terminados), command_batches.retry_of y un índice parcial sobre él; commands.dispatch_after, dispatch_until y window_token con un índice parcial, y command_batches.waiting |
update-management | artifact_signers y artifact_signatures (solo claves públicas y firmas); la confianza de un artefacto se deriva ahora de ellas, y la antigua columna de confianza guardada se conserva, sin escribirse más, de modo que una actualización gradual es segura; cada asignación gana además un signature_id nulo, la firma a la que está fijada |
user-management | ca_bundle en los proveedores de identidad |
El inicio de sesión único puede confiar en una autoridad de certificación privada
Una conexión de proveedor de identidad puede llevar su propio paquete de CA, fijado al crearla o con
setIdpCaBundle (una cadena vacía lo borra; ambos necesitan user:write en la API de administración
de la instancia). Las llamadas a ese proveedor confían en las raíces del sistema más el paquete, solo
para esa conexión, y siguen pasando por la protección de salida. El descubrimiento debe tener éxito
con el paquete nuevo antes de guardarlo. Consulta
Configurar el inicio de sesión único. No cambia nada para una
conexión sin paquete.
Otro comportamiento que conviene conocer
- Importación de flota.
importDevicesadmite hasta 1000 filas y responde cada una comoCREATED,EXISTS,CONFLICT,INVALIDoREFUSED; solo crea, así que ejecutar un archivo de nuevo es seguro, y una ejecución en seco no escribe nada. Necesitadevice:write. - Configuración de dispositivo.
restoreDeviceConfigurationycopyDeviceConfigurationvuelven a aplicar una revisión anterior o la configuración de otro dispositivo mediante los atributos compartidos declarados del dispositivo, generando una revisión nueva. Necesitandevice:writey añaden los códigosCONFIGURATION_KEY_UNDECLAREDyCONFIGURATION_CONFLICT. - Exportación de Thing Model. Una versión publicada de un perfil está disponible como Thing Model
del W3C Web of Things mediante
DeviceProfileVersion.thingModel. - LwM2M.
lwm2m.rebootylwm2m.readDeviceInfoson operaciones con nombre, y el fabricante, el modelo, el número de serie, la versión de firmware y el nivel de batería de un dispositivo se leen cuando se registra (como máximo una vez por hora) y se conservan como atributos de cliente. - Reglas de detección. El formulario de la consola ofrece ajustes preestablecidos para un dispositivo en silencio, un valor mantenido por encima de un límite y una batería baja, y muestra si la severidad de la regla llega a alguien. Una alarma de ausencia se borra cuando su dispositivo se deshabilita, se elimina o cambia de tipo.
- Mensajería. Dos réplicas que migran a la vez la retención de un flujo ya no hacen que una se niegue a arrancar.
- Reintento de lotes.
retryCommandBatch, eloutcomede cada comando terminado y la búsqueda poroutcomesse describen en Los lotes pueden reintentarse por resultado. - Ventanas de mantenimiento. Las ventanas,
impacty la retención se describen en Los comandos disruptivos pueden esperar a una ventana de mantenimiento.
v0.20.0 — inicia sesión con tu propio proveedor de identidad; los widgets se distribuyen como paquetes
v0.20.0 es una actualización en su sitio desde v0.19.0: dcctl install para el clúster y
después dcctl upgrade para cada instancia, como se describe para la v0.17.0. Si
vienes de v0.18.0, lee también las notas de v0.19.0: sus recuentos de filas y
de fragmentos y sus pasos se aplican también a esta actualización. Una instancia creada por la
v0.16.0 o una versión anterior sigue teniendo que destruirse y crearse de nuevo.
La mayor parte de esta versión es capacidad nueva que no cambia nada hasta que la usas: las personas
pueden iniciar sesión mediante tu propio proveedor de identidad OpenID Connect, los paneles pueden
mostrar widgets de tus propios paquetes, los dispositivos pueden recibir configuración y las
actualizaciones por aire llegan como una primera entrega. Lo que cambia para todos: cada usuario
inicia sesión de nuevo una vez, los techos de tasa se renombran según lo que cuentan, se rechazan
las credenciales de dispositivo X.509, la contraseña de un dispositivo se conserva solo como un
resumen irreversible, los paneles publicados antes de la actualización tienen que publicarse de
nuevo y muchas entradas de la API pasan a tener un límite. Los servicios que procesan cada evento
se ejecutan con un recolector de basura ajustado, y device-management conserva sus búsquedas
durante minutos en lugar de segundos.
Antes de actualizar
- Renombra las claves de los techos de tasa en cualquier
configde servicio que fijes:ingestRateLimit.messagesPerSecondpasa a seringestRateLimit.readingsPerSecond(event-sources,lwm2m-ingest,sparkplug-ingest) youtboundMessagesPerSecondpasa a seroutboundCallsPerSecond(event-processing,outbound-connectors). Un servicio que aún encuentra una clave anterior se niega a arrancar. Los scripts que leen o fijan techos de inquilinos mediante la API también necesitan los nuevos nombres de campo. Consulta Los techos de tasa se renombran. - Saca los dispositivos de las credenciales X.509. Una credencial de tipo
X509_CERTIFICATEdeja de autenticar con la actualización. Da antes a cada uno de esos dispositivos una credencialACCESS_TOKENoMQTT_BASIC. Consulta Ya no se aceptan credenciales de dispositivo X.509. - En un almacén de objetos S3, concede a la credencial
s3:ListBucketsobre el bucket, o el borrado de un inquilino eliminado no termina. Consulta Eliminar un inquilino ahora borra todo lo guardado bajo su prefijo del almacén de objetos. - En un almacén de eventos grande, plantéate construir antes dos índices. El primer arranque de
event-managementconstruye dos índices sobre los eventos base aún sin comprimir, y se niega, sin cambiar nada, cuando hay más de 4 000 000 de filas que indexar o una tabla tiene más de 500 fragmentos. Ambas secciones dan las sentencias que construyen el índice a mano, antes o después de la actualización: La lista de eventos de un dispositivo lee solo las filas de su inquilino y Dos dispositivos pueden enviar el mismoaltIden el mismo instante. - Si fijas
gogcogoMemLimitPercenten el nivel superior de los valores del chart, los cinco servicios que procesan cada evento fijan ahora los suyos, que prevalecen. Consulta Los servicios de la ruta de eventos se ejecutan con un recolector de basura ajustado. - Comprueba la memoria de
device-managementpara una flota grande. Sus búsquedas se conservan ahora durante 5 minutos, y cada réplica necesita sitio para toda la flota activa. Consultadevice-managementconserva sus búsquedas durante minutos. - Detén cualquier otra cosa que lea el flujo
device-events-capture, comonats stream view. Ahora solo admite el consumidor que poseeevent-sources. Consulta El flujo de entrada de dispositivos elimina cada mensaje una vez reenviado. - Vuelve a crear toda identidad cuya dirección de correo tenga un dominio no ASCII
(
user@bücher.example), o no sea unlocal@dominioválido. Tras la actualización no puede iniciar sesión hasta que un administrador la vuelva a crear. Elimínala, créala de nuevo con el dominio en su formaxn--(user@xn--bcher-kva.example) y restaura sus membresías. Consulta El dominio de un correo ahora tiene una sola grafía. - Revisa tus clientes de la API y los paquetes con los que compilas frente a Peticiones que ahora se rechazan, Paneles: publicar pone en vigor una versión y Los paquetes publicados.
Durante la actualización
- Las contraseñas de dispositivo se convierten. El primer arranque del nuevo
device-managementsustituye cada contraseñaMQTT_BASICguardada por un resumen irreversible. Hasta que todos los pods dedevice-managementejecutan la nueva versión, un pod anterior no encuentra contraseña para una credencial convertida, así que rechaza las conexiones MQTT de ese dispositivo y las comprobaciones de contraseña de sus eventos; el dispositivo reintenta y llega a un pod nuevo. Una contraseña establecida en cualquiera de las dos versiones durante el despliegue es la que perdura. - Actualiza
event-sourcesydevice-managementa la vez. Mientras ejecutan versiones distintas, se pueden rechazar los eventos que llevan una contraseña de dispositivo o un token de acceso en el cuerpo. Consulta Eventos que llevan una contraseña de dispositivo o un token de acceso en el cuerpo. - Evita modificar inquilinos o niveles hasta que termine el despliegue. Un pod de ingesta o de
salida anterior mide a todos los inquilinos con el valor predeterminado de la plataforma, y un pod
de
user-managementanterior hace fallar las lecturas y escrituras de inquilinos que atiende, hasta que se sustituye. Consulta Los techos de tasa se renombran. event-managementconstruye dos índices en su primer arranque; las escrituras de eventos base esperan mientras se construye cada uno, y las lecturas no.event-sourcesreenvía lo que contiene el flujo de entrada de dispositivos y después lo crea de nuevo. Si el flujo no queda vacío en cinco minutos,event-sourcesse detiene con un error y no elimina nada.- La detección pasa a un nuevo consumidor de los eventos resueltos. Consulta La detección lee sus eventos mediante un nuevo consumidor.
Volver a v0.19.0 tras la actualización deja sin poder autenticarse a todos los dispositivos que
usan una contraseña MQTT_BASIC, porque v0.19.0 no sabe leer un resumen; si lo haces, establece
contraseñas nuevas y cárgalas en esos dispositivos. Los cambios de índices y de flujo anteriores
indican cada uno lo que exige volver atrás.
Después de actualizar
- Todos inician sesión de nuevo una vez, y los clientes OAuth, como los agentes MCP, se autorizan de nuevo. Consulta Cerrar sesión ahora termina la sesión en el servidor.
- Publica de nuevo cada panel que deban ver los espectadores. Hasta entonces, a los espectadores se les indica que no está publicado. Consulta Paneles: publicar pone en vigor una versión.
- Pueden dispararse alertas nuevas.
SafetyGateDisabledse dispara por una compuerta de seguridad opcional sin la que se ejecuta un servicio. Las demás se enumeran en Alertas nuevas. - Una instancia creada con bootstrap antes de esta versión no tiene almacén de objetos, y la
actualización no le añade uno. Los logotipos de inquilino y los artefactos de actualización lo
necesitan. Consulta
dcctl bootstrappuede dar a una instancia un almacén de objetos.
Qué cambia
Cerrar sesión ahora termina la sesión en el servidor, y todos inician sesión de nuevo una vez
Cerrar sesión en la consola o en la aplicación de paneles incrustada solo borraba el navegador: el token de actualización seguía siendo válido hasta 7 días, y el token de inicio de sesión que conserva la consola aún podía iniciar una sesión nueva durante hasta 15 minutos. Una nueva mutación logout termina el inicio de sesión en el servidor. Desde ese momento se rechazan los intercambios de ese inicio de sesión: sus tokens de actualización ya no se renuevan, en ningún inquilino, su token de inicio de sesión ya no puede elegir un inquilino, y una concesión OAuth autorizada durante ese inicio de sesión ya no se renueva. La página de autorización de MCP inicia sesión por su cuenta, así que cerrar sesión en la consola no termina una concesión hecha allí; termina cuando caduca ese inicio de sesión (7 días por defecto). Los demás inicios de sesión de la misma cuenta no se ven afectados. Los tokens de acceso y los tokens de inicio de sesión ya emitidos, también en la API de administración, no se pueden revocar y siguen siendo válidos hasta que caducan, en un plazo de 15 minutos. Si no se puede contactar con el servidor al cerrar sesión, la aplicación cierra la sesión localmente igualmente e informa de que no se pudo terminar la sesión del servidor.
Cada inicio de sesión es ahora un registro que dura lo mismo que un token de actualización (7 días por defecto), de modo que una sesión que se renueva continuamente termina 7 días después del inicio de sesión y el usuario vuelve a iniciar sesión. Eso incluye una concesión OAuth, como la de un agente MCP: termina 7 días después del inicio de sesión en la página de autorización, y el cliente se autoriza de nuevo.
Los tokens emitidos antes de la actualización no tienen registro de inicio de sesión y se rechazan, por lo que todos inician sesión de nuevo una vez: las sesiones de la consola, de la aplicación de paneles, del SDK y de OAuth terminan en su siguiente renovación, dentro de los 15 minutos posteriores a la actualización salvo que hayas cambiado la vida del token de acceso. Los clientes OAuth, como los agentes MCP, se autorizan de nuevo. No hay que hacer nada más.
Los eventos que llevan una contraseña de dispositivo o un token de acceso en el cuerpo se ven afectados mientras conviven versiones
Desde esta versión, event-sources y device-management se pasan en otra forma la contraseña de
dispositivo o el token de acceso que llega en el cuerpo de un evento, y cada uno necesita la versión
nueva para leer la del otro. Mientras conviven versiones antiguas y nuevas de ambos durante una
actualización progresiva:
- Los eventos que llevan en el cuerpo una contraseña de dispositivo (
MQTT_BASIC), por HTTP o MQTT, se rechazan como no autenticados y no se reproducen. - Los eventos que llevan en el cuerpo un token de acceso (
ACCESS_TOKEN) se rechazan condeviceAuthMode: required. CondeviceAuthMode: optionalse aceptan con el token de dispositivo que nombran, sin comprobar su credencial. - Las conexiones MQTT no se ven afectadas.
Actualiza event-sources y device-management a la vez, o acepta la breve pérdida de esos eventos.
La contraseña de un dispositivo MQTT_BASIC se muestra una vez y no puede volver a mostrarse
La plataforma conserva ahora solo un resumen irreversible con clave de la contraseña de un dispositivo MQTT_BASIC, no la contraseña. La API nunca devolvía la contraseña en una lectura, así que ningún resultado de consulta cambia. Lo que cambia es que nadie puede recuperarla tampoco de la plataforma: la consola muestra una contraseña una sola vez, justo después de agregar la credencial o de establecer una nueva, con un botón de copiar, y después solo indica que hay una contraseña establecida. La fila de la credencial incorpora la acción Establecer nueva contraseña, y el formulario ofrece Generar contraseña segura.
Las contraseñas existentes se convierten en resúmenes la primera vez que device-management arranca con la nueva versión, y los dispositivos siguen autenticándose con la contraseña que ya usan. Una contraseña que no anotaste no puede mostrarse después; establece una nueva y cárgala en el dispositivo. Consulta Contraseñas que se muestran una vez y su rotación.
No hay nada que hacer.
La lista de eventos de un dispositivo lee solo las filas de su inquilino
El índice que sirve la lista de eventos de un dispositivo empieza ahora por el inquilino. En
v0.19.0, el total de la lista, y la lista de un dispositivo filtrada por tipo de evento, recorrían
todas las filas aún sin comprimir con el token de ese dispositivo, incluidas las de un dispositivo con
el mismo token en otro inquilino, así que un gateway-1 con mucho tráfico en un inquilino ralentizaba
la lista de gateway-1 en otro. Ahora recorren solo las filas del propio dispositivo, y una consulta
SQL o de BI sobre los eventos de un dispositivo gana lo mismo. Cada evento base almacenado sigue
actualizando el mismo número de índices: el nuevo sustituye al anterior, y es algo más ancho porque
incluye el inquilino.
-
Durante la actualización. El primer arranque del nuevo
event-managementconstruye el índice sobre los eventos base aún sin comprimir (por defecto, aproximadamente la última semana) y después elimina el índice al que sustituye. Mientras construye, las escrituras de eventos base esperan y las lecturas no; la construcción está acotada a 40 segundos una vez bloqueada la tabla. Se niega, sin cambiar nada, cuando hay más de 4 000 000 de filas que indexar o la tabla tiene más de 500 fragmentos, y se detiene de forma definitiva (hasta que el índice exista) si la construcción agota su tiempo. Ninguno de estos casos obliga a recrear la instancia: el error incluye las sentencias que construyen el índice a mano. Mientras tanto, elevent-managementanterior sigue almacenando eventos. -
Para construirlo a mano, en un momento tranquilo. Ejecuta esto en el primario del almacén de eventos y reinicia
event-management, que encuentra el índice y solo elimina el que sustituye. La primera sentencia elimina un índice inacabado del mismo nombre que deja una construcción interrumpida, y no hace nada si no lo hay:DROP INDEX IF EXISTS "event-management".idx_events_tenant_device_time;CREATE INDEX idx_events_tenant_device_timeON "event-management".events (tenant_id, device_token, occurred_time DESC)WITH (timescaledb.transaction_per_chunk);La construcción avanza fragmento a fragmento, así que una escritura espera solo mientras se indexa el fragmento al que va dirigida: unos 3 microsegundos por cada evento base de ese fragmento aún sin comprimir. Mientras se indexa un fragmento, el
event-managementanterior y el nuevo esperan para almacenar eventos en él, y un evento retenido más allá de la espera de confirmación del broker, de 60 segundos, se entrega de nuevo, lo cual es seguro. En una instancia con mucho tráfico, elige un momento tranquilo. -
Para evitar el rechazo, construye el índice antes de actualizar. En una instancia
v0.19.0con más de4000000eventos base sin comprimir, ejecuta primero las dos sentencias anteriores;v0.19.0funciona con ellas, y la actualización entonces solo elimina el índice anterior. Para contar los eventos base, usa la consulta de Comprueba el número de filas conhypertable_name IN ('events'). -
Volver a
v0.19.0conserva el nuevo índice, yv0.19.0funciona con él.
Dos dispositivos pueden enviar el mismo altId en el mismo instante y se guardan ambos eventos
El altId de un evento es el identificador de mensaje del propio emisor, y el lado de escritura lo
usaba, junto con el occurredTime del evento, para descartar una copia reentregada. Esa clave era
por inquilino, así que cuando dos dispositivos del mismo inquilino enviaban el mismo altId en el
mismo instante, el segundo evento se tomaba por una reentrega del primero y se descartaba sin aviso.
Ahora la clave incluye el dispositivo: se guardan ambos eventos, y un dispositivo que vuelve a
enviar el mismo altId en el mismo instante sigue siendo deduplicado. Si dependía de que el altId
de un dispositivo suprimiera el de otro, eso ya no ocurre; haga usted mismo que el altId sea único
entre sus dispositivos si lo necesita.
-
En la actualización. El primer arranque del nuevo
event-managementconstruye el nuevo índice único sobre los eventos base que aún no están comprimidos y después elimina el índice por inquilino que reemplaza; no hay ningún momento sin una protección de deduplicación. Mientras se construye, las escrituras de eventos base esperan y las lecturas no; la construcción está acotada a 40 segundos una vez bloqueada la tabla. Se rehúsa, sin cambiar nada, cuando hay más de 4.000.000 de filas por indexar o la tabla tiene más de 500 fragmentos, y un arranque que no consigue bloquear la tabla o terminar a tiempo no cambia nada y el siguiente arranque lo intenta de nuevo. Nada de esto requiere recrear: el error incluye las sentencias que construyen el índice a mano. Elevent-managementanterior sigue guardando eventos mientras tanto. -
Para construirlo a mano, en un momento tranquilo. Ejecute esto en el primario del almacén de eventos y reinicie
event-management, que encuentra el índice y solo elimina el que reemplaza. La primera sentencia elimina un índice inacabado del mismo nombre que deja una construcción interrumpida, y no hace nada si no hay ninguno:DROP INDEX IF EXISTS "event-management".idx_events_tenant_device_alt_id;CREATE UNIQUE INDEX idx_events_tenant_device_alt_idON "event-management".events (tenant_id, device_token, alt_id, occurred_time)WHERE alt_id IS NOT NULL;Un índice único se construye de una sola vez, de modo que las escrituras de eventos base esperan durante toda la construcción y las lecturas no. Un evento retenido más que la espera de confirmación de 60 segundos del broker se entrega de nuevo, lo cual es seguro. En una instancia ocupada, elija un momento tranquilo.
-
Para evitar el rechazo, construya el índice antes de actualizar. En una instancia
v0.19.0con más de4000000eventos base sin comprimir, ejecute primero las dos sentencias anteriores;v0.19.0funciona con ellas y la actualización solo elimina el índice antiguo. Para contar los eventos base, use la consulta de Comprobar el número de filas conhypertable_name IN ('events'). -
Volver a
v0.19.0conserva el nuevo índice yv0.19.0funciona con él: los eventos reentregados siguen deduplicándose, pero esa versión sigue comprobando por inquilino, así que vuelve a descartar el evento del segundo dispositivo.
El flujo de entrada de dispositivos elimina cada mensaje una vez reenviado
El flujo que guarda cada publicación de un dispositivo hasta que event-sources la reenvía
(device-events-capture) ahora elimina un mensaje cuando event-sources lo confirma. En v0.19.0
conservaba cada mensaje hasta siete días después de reenviarlo. Una publicación que aún no se ha
reenviado se conserva exactamente como antes, así que una caída de event-sources sigue sin perder
nada.
- En la actualización. El primer arranque del nuevo
event-sourcesreenvía lo que el flujo existente aún contiene, espera a que quede vacío y entonces elimina el flujo y lo crea de nuevo con la nueva retención. Los números de secuencia saltan por delante de los del flujo anterior, así que ninguno se reutiliza. Una publicación de dispositivo que llegue en el instante entre la comprobación de vacío y la recreación puede perderse después de que el broker la haya confirmado. Si el flujo no queda vacío en cinco minutos,event-sourcesse detiene con un error que nombra el flujo y no elimina nada; vuelva a iniciarlo cuando pueda reenviar. - Nada más puede leer este flujo. Admite un solo consumidor, el que posee
event-sources, así quenats stream viewy cualquier otro consumidor sobredevice-events-capturedejan de funcionar. Para ver lo que envían los dispositivos, lea el flujoinbound-events, que contiene los eventos decodificados. - Volver a
v0.19.0exige eliminar antes el flujo, conevent-sourcesdetenido, porquev0.19.0se niega a arrancar contra un flujo con la nueva retención; lo crea de nuevo en su siguiente arranque. Lo que aún no se haya reenviado cuando lo elimine se pierde.
Un evento entrante cuyo registro de fallo no se puede codificar ya no se guarda como un registro vacío
Cuando device-management no podía codificar el registro de un evento entrante que había fallado,
guardaba de todos modos un registro vacío y confirmaba el evento. El registro se leía como un fallo
sin motivo ni texto, y el fallo real no quedaba registrado en ningún sitio. Ahora no publica nada
para ese evento, lo confirma y cuenta la pérdida en dead_letter_lost_total, por lo que se dispara
DeadLetterWriteLost y la línea LOST del registro del pod indica de qué evento se trata. Ningún
evento que pueda enviar un dispositivo llega hoy hasta ahí; es una ruta de defecto, y la alerta es
la forma de enterarse de uno. Por eso DeadLetterWriteLost tiene una cuarta causa, indicada en
la tabla de alertas.
No hay que hacer nada.
Un servicio que no alcanza el broker de mensajería al arrancar se detiene al cabo de un minuto
Cada servicio espera al broker de mensajería (NATS) al arrancar. En v0.19.0 esperaba 30 segundos
y luego arrancaba de todos modos. Los servicios que crean un bucket al arrancar se detenían entonces
con un error que culpaba a la versión del broker (key-value requires at least server version 2.6.2), y los demás se declaraban listos sin poder enviar ni recibir nada. Ahora un servicio espera
hasta un minuto y, si el broker no ha respondido para entonces, se detiene con un error que indica
la dirección del broker y el último error de conexión, y Kubernetes lo reinicia. Un broker que
arranca dentro de ese minuto se trata igual que antes.
- Un pod que se reinicia con
the NATS broker was not reachable before the startup connection wait ran outno pudo conectarse al broker en la dirección de la configuración de la instancia. Compruebe que los pods del broker están en ejecución y listos, y lea el error de conexión al final del mensaje: un nombre que no se resuelve, una conexión rechazada y un certificado o credencial rechazados apuntan a sitios distintos. - Un broker ausente durante más tiempo cuesta reinicios, no un servicio que se queda caído. Cada reinicio vuelve a intentarlo, con la espera entre reinicios habitual de Kubernetes, y el servicio arranca en cuanto el broker responde.
No hay que hacer nada.
dcctl install --dry-run aplica los rechazos de reinstalación que aplica la instalación
En un clúster que responde, una ejecución en seco ahora lee el registro de instalación, pregunta
qué instancias se ejecutan en el clúster cuando cambian los ajustes, y falla con el mismo mensaje que la instalación cuando
esta se rechazaría por lo que el clúster ya contiene: una reinstalación desde una máquina sin
estado del clúster, ajustes cambiados mientras hay instancias en ejecución, o un
--backup-snapshot-class que no puede funcionar. Antes imprimía un plan y salía con 0. Una
ejecución en seco contra un clúster que aún no existe, o al que no llega, sigue imprimiendo su plan
e indica que esas comprobaciones no se hicieron. Una ejecución en seco no comprueba todo lo que la
instalación rechaza; por ejemplo, un clúster cuyo almacén relacional fue creado por una versión
anterior se sigue rechazando solo cuando la instalación se ejecuta.
No hay nada que hacer.
dcctl install y dcctl bootstrap esperan a que se hayan unido todas las instancias de la base de datos
Antes, dcctl install volvía en cuanto el primario de la base de datos relacional aceptaba una
conexión, y dcctl bootstrap solo esperaba a los servicios, de modo que cualquiera de los dos
podía informar de éxito, y bootstrap imprimir que ambas bases de datos estaban replicadas, mientras
una réplica aún se estaba creando. Ahora install se registra, libera el bloqueo del clúster y espera
hasta 15 minutos a que se hayan unido todas las instancias de la base de datos relacional. Bootstrap
espera a los servicios y después, hasta 15 minutos, a todas las instancias del almacén de eventos de
la instancia. Si alguna no se ha unido a tiempo, el comando termina con un error que nombra la base
de datos y cuántas de sus instancias están listas. Ejecute de nuevo el mismo comando para seguir
esperando (en install, eso vuelve a aplicar los requisitos previos y rechaza los bootstraps mientras
se ejecuta, así que si solo quiere seguir la base de datos obsérvela con kubectl); un bootstrap
que terminó así no se deshace, y la instalación ya está registrada, así que mientras tanto se
pueden hacer bootstrap de instancias. Una instalación sana añade unos segundos.
No hace falta nada más.
Los techos de tasa se renombran según lo que cuentan
El techo de ingesta cuenta lecturas desde hace tiempo, y el techo de salida cuenta llamadas de conector, pero ambos seguían nombrados por mensajes. Se renombran en todas partes, sin conservar la grafía anterior:
| Antes | Ahora |
|---|---|
ingestRateLimit.messagesPerSecond (configuración de event-sources, lwm2m-ingest, sparkplug-ingest) | ingestRateLimit.readingsPerSecond |
outboundMessagesPerSecond (configuración de event-processing, outbound-connectors) | outboundCallsPerSecond |
ingestMessagesPerSecond (campo GraphQL, clave de configuración de nivel) | ingestReadingsPerSecond |
outboundMessagesPerSecond (campo GraphQL, clave de configuración de nivel) | outboundCallsPerSecond |
- Antes de actualizar, renombre las claves de configuración de los servicios. Si sus valores
fijan alguna de las claves anteriores en el
configde un servicio, cámbiela por el nombre nuevo. Un servicio que aún encuentra una clave anterior se niega a arrancar, y su error indica la clave que debe usar. No recurre al valor predeterminado de la plataforma, porque eso sustituiría en silencio el techo que usted fijó. - Clientes de la API. Se renombran los campos de
tenantGovernance, del tipo de inquilino de la API de administración y de sus entradas de creación y actualización. Una petición que selecciona o envía un nombre anterior ahora falla, así que actualice cualquier script o integración que gestione techos de inquilinos. Una configuración de nivel que usa una clave anterior se rechaza, y su error enumera las claves aceptadas. - Los datos guardados se convierten solos. La actualización renombra las columnas de sobrescritura del inquilino y cambia las claves de cada configuración de nivel guardada, incluidos los niveles de serie y los eliminados, con los mismos valores.
- Durante el despliegue. Hasta que todos los pods ejecutan la nueva versión, un pod anterior y
un
user-managementnuevo no se entienden. Un pod de ingesta o de salida anterior pide los campos anteriores detenantGovernance, la petición falla y ese pod mide a todos los inquilinos con el valor predeterminado de la plataforma;TenantsMeteredAtPlatformDefaultpuede dispararse por él. Un pod deuser-managementanterior sigue usando los nombres de columna anteriores una vez que el pod nuevo los ha renombrado, así que las lecturas y escrituras de inquilinos que atiende fallan hasta que se sustituye. Ambas cosas terminan al acabar el despliegue; evite modificar inquilinos o niveles hasta entonces.
Eliminar un inquilino ahora borra todo lo guardado bajo su prefijo del almacén de objetos
El borrado de un inquilino eliminado solo quitaba los objetos que nombraba una fila de user-management. Ahora también quita todos los objetos bajo el prefijo de ese inquilino en el almacén de objetos, sea cual sea el servicio que los guardó, de modo que los artefactos de actualización y cualquier objeto cuya referencia se perdió se borran con el inquilino.
- En el backend S3, la credencial ahora necesita
s3:ListBucketsobre el bucket, además des3:GetObject,s3:PutObjectys3:DeleteObject. Sin ello, el paso de objetos del borrado de un inquilino falla y se reintenta en cada pasada, así que el borrado no termina. Concédelo antes de actualizar. El backend filesystem no necesita nada.
El token de un inquilino eliminado se retiene hasta que todo lo emitido para él haya caducado, y un inquilino nuevo con ese token empieza limpio
El token de un inquilino eliminado se liberaba un tiempo fijo después de empezar la eliminación. La
retención (tokenHoldSeconds, 12 horas por defecto) ahora se cuenta desde que el último sistema de
almacenamiento terminó de borrar los datos del inquilino, más un minuto de margen, de modo que una
credencial emitida para el inquilino eliminado haya caducado antes de que el token pueda reutilizarse.
Por eso una eliminación retiene su token algo más que antes. Tres cambios lo respaldan:
- No se emite ninguna credencial de dispositivo una vez iniciado el borrado. Desde ese momento se rechaza la conexión de un dispositivo de ese inquilino.
- Los servicios LwM2M y Sparkplug olvidan una búsqueda de dispositivo en caché después de una hora, y un registro LwM2M de un inquilino eliminado se termina en lugar de prorrogarse.
- Se rechaza un token de actualización emitido en un segundo anterior a la creación del inquilino, para cualquier llamante. Cuando se elimina un inquilino y se crea otro con el mismo token, quienes tenían la sesión iniciada en el primero deben volver a iniciar sesión.
Las entradas de auditoría conservadas de un inquilino eliminado ahora se archivan bajo la eliminación y
no bajo el token del inquilino, así que el diario del inquilino nuevo empieza vacío. El tipo
TenantDeletion de la API de administración incorpora retainedAuditTenant, que es el filtro tenant
de auditEvents que lee esas entradas; en la consola, Administración → Auditoría muestra cada
eliminación completada como <token> (eliminado el <fecha>) en su filtro de inquilino. No hay nada que
hacer. Consulta Eliminación de un inquilino.
Un registro que no existe se responde con NOT_FOUND
Una petición que nombra un registro que no existe llegaba al cliente como un record not found sin
extensions.code en la mayoría de los servicios. Ahora lleva extensions.code con el valor
NOT_FOUND, y el mensaje no cambia. Un rechazo que ya tenía su propio código para un registro
inexistente, como ARTIFACT_NOT_FOUND, lo conserva. En esta versión las API de user-management todavía
responden algunos casos (un inquilino, un rol, una identidad o una pertenencia que no existe) con un
mensaje como tenant not found y sin código. Un cliente que distinguía un registro
inexistente de otros fallos leyendo el mensaje puede pasar a usar el código. Consulta la
referencia de GraphQL.
El inicio de sesión único es nuevo y no te pide nada
Las personas ahora pueden iniciar sesión mediante un proveedor de identidad externo OpenID Connect, y un inquilino puede exigirlo. Nada se activa hasta que un superusuario crea una conexión, así que una instancia actualizada se comporta como antes. No hay nada que migrar y el inicio de sesión con contraseña sigue funcionando. El nuevo inicio de sesión que provoca el cierre de sesión en el servidor ya está descrito.
- Configurar un proveedor se describe en Configurar el inicio de sesión único, y su comportamiento en Inicio de sesión único.
- La pantalla de inicio de sesión empieza solo con la dirección de correo. A una dirección cuyo dominio tiene una conexión habilitada y aprobada se le ofrece Continuar con el inicio de sesión único, con Usar una contraseña al lado; cualquier otra dirección pasa al campo de contraseña.
beginSsollamado con un correo respondenullpara un dominio sin conexión y para uno cuya conexión está deshabilitada o pendiente de aprobación. Una conexión indicada por token sigue rechazando con «single sign-on is not available». El inicio de sesión con contraseña nunca se elimina, así que una cuenta que solo tiene contraseña no queda aislada. - Cada persona vincula su propia cuenta. Quien inició sesión con contraseña puede vincular una cuenta de inicio de sesión único desde el menú de usuario.
beginSsoLinkno recibe un token de conexión: el servidor usa la conexión para la que un superusuario verificó el dominio del correo de la persona. Cada rechazo lleva un código (SSO_LINK_SESSION_REQUIRED,SSO_LINK_PASSWORD_REQUIRED,SSO_LINK_NOT_ALLOWED,SSO_LINK_NO_CONNECTION,SSO_LINK_ALREADY_LINKED), y un rechazo en el retorno desde el proveedor indica qué comprobación falló. - Una conexión también puede cerrar la sesión en el proveedor de identidad. Está desactivado hasta que un administrador lo activa, porque cerrar sesión en el proveedor termina la sesión allí para todas las aplicaciones que atiende. La actualización añade el ajuste (
providerSignOut, desactivado por defecto) a cada conexión; nada cambia hasta que se establece.logoutacepta unreturnToopcional (CONSOLEoDASHBOARD) y respondeSignOut { providerSignOutUrl }, que esnullcuando no queda nada más por hacer. Las direcciones que hay que registrar en el proveedor están en la conexión (signOutReturnUrls). Consulta Cerrar sesión en el proveedor. - Terminar una sesión no recupera los tokens ya emitidos. Tras un cierre de sesión, una revocación, una desvinculación o una conexión deshabilitada, los tokens de acceso y de inicio de sesión ya emitidos siguen funcionando hasta que caducan, 15 minutos por defecto.
El inicio de sesión único necesita la dirección pública de la instancia, así que el chart ahora la deriva
Iniciar sesión mediante un proveedor de identidad externo OpenID Connect, y el servidor de
autorización OAuth que usan los clientes MCP, necesitan conocer la dirección en la que se accede a
esta instancia, porque un inicio de sesión tiene que volver a algún sitio. El auth.issuerUrl de
user-management estaba vacío salvo que lo establecieras. El chart ahora lo deriva del ingress, como
https://<host>/api/user-management, cuando la instancia se sirve por https o en un host de
loopback (localhost, 127.0.0.1, ::1), y tu propio valor siempre prevalece. De ello se sigue:
- El servidor de autorización OAuth se activa en esas instancias. En una instalación por defecto
los clientes MCP no podían obtener un token, porque estaba desactivado hasta que establecías este
valor. Para mantenerlo desactivado, y con él el inicio de sesión único, establece
functionalAreas.user-management.config.auth.issuerUrlen"". - Los tokens emitidos tras la actualización llevan esa dirección como emisor, en lugar del identificador por instancia. Los validadores de tokens de los servicios no comprueban ni el emisor ni la audiencia, así que los tokens emitidos antes de la actualización siguen funcionando hasta que caducan. No hay nada que hacer.
- Una instancia solo http en un nombre de host real no recibe emisor, porque user-management
rechaza un emisor http en cualquier host que no sea de loopback. El servidor de autorización y el
inicio de sesión único siguen desactivados, y
helm installydcctl bootstraplo indican. Sirve la instancia por https para activarlos.
Alertas del inicio de sesión único
Tres alertas informan de los fallos del inicio de sesión único. Si enrutas o silencias alertas por nombre, estas son nuevas.
| Alerta | Severidad | Qué informa | Qué hacer |
|---|---|---|---|
SsoIdpUnreachable | warning | Durante diez minutos, user-management no ha podido alcanzar el endpoint de tokens ni las claves de firma de un proveedor de identidad configurado, así que se rechaza a quienes inician sesión a través de él. El inicio de sesión con contraseña no se ve afectado. | Comprueba que el proveedor está activo y que su dirección se resuelve desde el clúster, que la lista de destinos permitidos de salida (instance.config.infrastructure.egress) permite el rango de un proveedor local y, si el proveedor movió un endpoint, actualiza y aprueba la instantánea de descubrimiento de la conexión. |
SsoUnverifiedIdentityRefused | warning | Se rechazó un inicio de sesión aunque el proveedor lo avalaba, con el resultado en la etiqueta outcome: refused_unverified_email, refused_unverified_domain, refused_required_claim o refused_system_role_identity. No se inició sesión a nadie. | Suele ser una conexión mal configurada o alguien que la está sondeando. Busca en el diario de auditoría la dirección que afirmó el proveedor, y en el registro de inicios de sesión del propio proveedor. |
SsoStateStoreFull | critical | El almacén que guarda los inicios de sesión comenzados está lleno, así que user-management se niega a comenzar cualquier inicio de sesión único con cualquier proveedor hasta que las entradas caduquen, diez minutos después. El inicio de sesión con contraseña no se ve afectado. | Casi seguro que alguien llama en bucle al inicio público de sesión: localiza el tráfico en los registros del ingress y bloquéalo aguas arriba. Si el volumen es legítimo, aumenta instance.config.infrastructure.nats.kvStateMaxBytes. |
El dominio de un correo ahora tiene una sola grafía, y una dirección almacenada con dominio no ASCII deja de coincidir
Una dirección de correo ahora se almacena, se busca y se compara en una única forma canónica: la parte local recortada y en minúsculas como antes, y el dominio convertido a ASCII en minúsculas (la forma xn-- para un nombre internacionalizado) y sin punto final. Por tanto, user@bücher.example y user@xn--bcher-kva.example son la misma identidad en el inicio de sesión con contraseña, en el inicio de sesión único y en una solicitud de vinculación, y el dominio que reclama una conexión se compara en la misma forma. Se rechaza una dirección que no sea exactamente local@dominio, o cuyo dominio no sea un nombre de host válido.
- Una dirección con dominio ASCII no cambia. No tienes que hacer nada.
- Una identidad almacenada con un dominio no ASCII (
user@bücher.example) conserva la forma antigua, y se rechaza hasta que un administrador la vuelva a crear. No puede iniciar sesión con contraseña ni mediante inicio de sesión único, y una sesión que ya tenga abierta termina en su siguiente intercambio; al seleccionar un tenant o renovar se informa entonces el códigoIDENTITY_EMAIL_NOT_CANONICAL. Las acciones de administración siguen encontrándola por la dirección tal como está almacenada, así que puedes deshabilitarla, terminar sus sesiones, desvincularla y eliminarla. Para restaurar el acceso, desvincula y elimina la identidad antigua, vuelve a crearla (con cualquiera de las dos grafías del dominio: se almacena en la formaxn--) y restaura sus membresías y vínculos. Hacerlo antes de actualizar, con el dominio en su formaxn--, evita la interrupción: consulta Antes de actualizar. Lo mismo vale para una identidad cuya dirección almacenada no sea unlocal@dominioválido.
Ya no se aceptan credenciales de dispositivo X.509
El tipo de credencial X509_CERTIFICATE no se acepta hasta que se admita la verificación de
certificados. Crear una credencial de ese tipo, cambiar una a ese tipo y sustituir un dispositivo
por uno que la tenga se rechazan con UNSUPPORTED, y no se escribe nada. La consola ya no lo
ofrece. Una credencial de ese tipo guardada antes de la actualización deja de autenticar, y nada la
convierte: antes de actualizar, da a cada uno de esos dispositivos una credencial ACCESS_TOKEN o
MQTT_BASIC. Consulta Credenciales de dispositivo.
Una contraseña MQTT_BASIC debe tener al menos 16 bytes
Crear una credencial MQTT_BASIC, o establecer su contraseña, rechaza ahora una contraseña de menos
de 16 bytes, o de más de 4096, contados tal como se envían. Una contraseña corta se rechaza con
INVALID_VALUE. La contraseña de un dispositivo se aprovisiona en lugar de recordarse, así que el
mínimo está pensado para un secreto generado; Generar contraseña segura en la consola genera
uno. Las contraseñas más cortas existentes siguen funcionando hasta que se establecen de nuevo.
Cambiar una credencial a MQTT_BASIC debe establecer su contraseña en la misma petición.
Paneles: publicar pone en vigor una versión
Publicar un panel hace ahora que la versión que crea sea la que se sirve a los espectadores, y
activateDashboardVersion vuelve a poner en vigor una versión anterior; no hay forma de despublicar.
Los espectadores leen la versión en vigor con publishedDashboard, y /dash la abre por su token.
El borrador es para los autores del panel.
- Los paneles publicados antes de la actualización no están en vigor. Nada registraba qué
versión servían, así que a los espectadores se les indica que el panel no está publicado
(
NOT_PUBLISHED) hasta que un autor lo publica de nuevo. publishDashboarddevuelve unDashboardPublication, con la nuevaversiony eldashboard.- Leer la
definitionde un panel, o el cuerpo de una versión condashboardVersion, necesitadashboard:write. Quien solo tienedashboard:readleepublishedDashboard. - La búsqueda
dashboardsdevuelveDashboardSummary, que no tienedefinition. dashboardVersionsse pagina, de la más reciente a la más antigua: 100 versiones salvo que paseslimit(como máximo 1000) yoffset, y sin el cuerpo de cada versión.rollbackDashboardacepta unexpectedUpdatedAtopcional y rechaza una reversión obsoleta.- El filtro de nombre de la búsqueda
dashboardstrata%,_y\como sí mismos, no como comodines. - Modificar un panel de un inquilino que se está eliminando se rechaza con
TENANT_DELETED; eliminarlo no. - La consola no publica una versión mientras un widget de ella esté sin resolver. Consulta Publicar mientras un widget no está resuelto.
La agregación de mediciones necesita una hora de inicio, y está acotada
bucketedMeasurements exige ahora startTime, y sin él se rechaza con INVALID_VALUE; endTime
es por defecto el momento actual. Una lectura se rechaza con LIMIT_EXCEEDED, en lugar de
truncarse, cuando produciría más de 10 000 intervalos, o cuando sus intervalos multiplicados por sus
nombres de medición superan 50 000. Sin un filtro name, los nombres contados son los nombres
distintos que coinciden con la lectura. La herramienta MCP aggregate_measurements tiene la misma
exigencia y los mismos límites.
Peticiones que ahora se rechazan
Entradas que se aceptaban de cualquier tamaño se rechazan ahora por encima de estos límites, con
LIMIT_EXCEEDED salvo que se indique otro código. Un valor exactamente en el límite se acepta. La
referencia de GraphQL los enumera.
| Qué | Límite |
|---|---|
Claves en una búsqueda por token, id o id externo, en todos los servicios, y los tokens de un removeEntityRelationships | 1000 |
| Una entrada JSON: metadatos, propiedades, configuración, una carga útil, destinatarios y similares | 64 KiB |
| La carga útil o los metadatos de un comando, al ponerlo en cola o en lote | 64 KiB (PAYLOAD_TOO_LARGE, METADATA_TOO_LARGE) |
| El nombre de un comando | 128 bytes (COMMAND_NAME_TOO_LONG) |
| Una clave de atributo, y un valor de atributo | 128 bytes, y 64 KiB |
| La definición de una regla de detección, y su grafo de autoría | 256 KiB cada uno |
| Relaciones rastreadas de un dispositivo | 256 |
| Definiciones de métricas, definiciones de comandos y reglas de detección de un perfil | 1000 de cada una |
Relaciones creadas por un createEntityRelationships | 1000 |
Versiones devueltas por deviceProfileVersions, entityGroupVersions, assetTypeVersions y connectorVersions | las 1000 más recientes, salvo que se paginen con sus nuevos limit y offset |
| Destinatarios de una regla de notificación | 100 |
Los redirectUris de un cliente OAuth, y sus scopes | 16 entradas cada uno, de como máximo 2048 y 64 caracteres |
Niveles de inquilino, y los tokens de un reorderTenantTiers | 100 |
| Una contraseña establecida mediante la API de administración | 72 bytes |
| Roles, autoridades o tokens de rol en una petición de administración | 100 |
| Ejecutar una consulta o mutación GraphQL | 60 segundos |
| Recibir el cuerpo de una petición, en GraphQL y en los endpoints de OAuth y MCP | 30 segundos; a un cuerpo detenido se responde 400 y se cierra la conexión |
También se rechazan tres valores al crear o actualizar un registro, que antes se guardaban y fallaban más tarde o no hacían nada:
- un conector de tipo
gcp_pubsub, que aún no tiene cliente de entrega (UNSUPPORTED); uno guardado antes sigue enviándose a mensajes fallidos cuando se despacha; paramsde un proveedor de IA que fijen algo (UNSUPPORTED), ya que ningún cliente de proveedor los aplica; se aceptan vacío, null y{};- las cabeceras personalizadas de un canal de notificación webhook que la entrega rechazaría: un
nombre no válido, caracteres de control en un valor, una cabecera reservada
AuthorizationoX-DC-*, o un duplicado.
Los servicios de la ruta de eventos se ejecutan con un recolector de basura ajustado
event-sources, event-management, device-management, device-state y event-processing se
ejecutan ahora con GOGC=400 y un límite de memoria blando (GOMEMLIMIT) al 75% de su límite de
memoria, y el límite de memoria de event-processing es de al menos 384Mi. En Google Kubernetes
Engine a 6000 eventos por segundo, frente a una ejecución de control en la misma sesión, la CPU por
evento bajó un 18% en device-management y event-management, un 15% en device-state, un 4% en
event-sources y un 5% en event-processing.
Son valores del chart, functionalAreas.<area>.gogc y functionalAreas.<area>.goMemLimitPercent,
y un nuevo gogc de nivel superior (por defecto 0, desactivado) junto al goMemLimitPercent de
nivel superior ya existente. El valor propio de un área prevalece sobre el de nivel superior, así
que un 0 de nivel superior ya no desactiva el límite blando para estos cinco: fija el valor propio
del área en 0 para desactivar cualquiera de los dos en esa área.
device-management conserva sus búsquedas durante minutos, y un cambio llega a todas las réplicas
device-management busca la credencial, el dispositivo, las relaciones rastreadas, el perfil y las
pertenencias a grupos de cada evento, y los conservaba en la memoria de cada réplica durante 5
segundos, así que un dispositivo que informaba con menos frecuencia se buscaba de nuevo cada vez.
Ahora se conservan durante 5 minutos por defecto:
Clave de configuración de device-management | Valor por defecto | Rango |
|---|---|---|
inMemoryCache.ttlSeconds (nueva) | 300 | 1-3600 |
inMemoryCache.credentialCacheMiB (nueva; era un valor fijo de 16 MiB) | 32 | 1-256 |
deviceCacheTtlSeconds, relationshipCacheTtlSeconds, metricDefCacheTtlSeconds, membershipCacheTtlSeconds (eran 60) | 300 | 1-3600 |
Un valor fuera de su rango impide que el servicio arranque. Un cambio descarta ahora la copia guardada en todas las réplicas, no solo en la que lo hizo; una réplica cuya conexión con el bróker se restablece descarta todo lo que conserva, y el borrado de un inquilino eliminado descarta las copias de ese inquilino en todas las réplicas. Actualizar un dispositivo, incluido cambiar su tipo, y crear una relación, en una llamada o en varias, al canjear o reabrir una reclamación, o al cambiar un tipo de relación, no descartaban las copias que modificaban, y ahora sí lo hacen.
- Dimensiona cada réplica para toda la flota activa, no para la flota dividida entre las
réplicas: cada réplica ve casi todos los dispositivos. Para 60 000 dispositivos con una relación
rastreada cada uno, eso son unos 80 para
inMemoryCache.credentialCacheMiB, 64 parainMemoryCache.perDeviceCacheMiBy un límite de memoria de 512Mi. Un límite menor que la flota hace que la caché no responda casi nada. - Una revocación está acotada por los mismos 5 minutos. Una réplica que pierde el mensaje que le indica descartar una credencial revocada, sin perder su conexión con el bróker, puede seguir autenticando los eventos de esa credencial hasta que caduque su copia. Las conexiones siempre se comprueban contra la base de datos. Consulta Cuánto tarda en aplicarse una revocación.
Los lectores usan la lectura anticipada por defecto
Un servicio que lee un stream pide ahora su siguiente lote mientras entrega el actual, y pide 128
mensajes cada vez en lugar de 64. Según lo medido en Google Kubernetes Engine, esto mantuvo la cola
pendiente del motor de detección bajo carga intensa en unos cientos de mensajes en lugar de decenas
de miles; el techo de rendimiento y la CPU por evento de cada servicio fueron los mismos con ello
desactivado. Fija instance.config.infrastructure.nats.fetch.ahead: false para desactivarlo.
Consulta Lectura anticipada.
La detección lee sus eventos mediante un nuevo consumidor
event-processing confirma ahora los eventos resueltos una vez por punto de control, mediante un
nuevo consumidor duradero cuyo nombre termina en _ackfloor
(<instance>_event-processing_resolved-events_ackfloor). En su primer arranque, el nuevo consumidor
empieza justo después del último evento que cubre el estado guardado, y el primer líder de la nueva
versión elimina el consumidor anterior. No hay nada que migrar. Un panel o una alerta propios que
seleccionen el consumidor de eventos resueltos de event-processing por su etiqueta durable
necesitan el nuevo nombre; las alertas incluidas coinciden con ambos. Volver a v0.19.0 hace que la
detección lea una vez más los eventos retenidos del stream, y omite los que ya ha visto.
La detección en vivo también vuelve a leer un tramo de eventos que no se le entregó, en lugar de
saltárselo, y dos alertas lo notifican (DetectLiveGapFillFailing, DetectLiveDeliveriesRecovered).
Un bucle de detección que deja de avanzar dispara DetectLoopStalled, y cada punto de control se
ejecuta ahora con un plazo cuyo mínimo es el nuevo checkpointTimeoutSeconds (por defecto 10, como
máximo 120).
El tiempo de espera del borrador con IA
El techo por defecto de una llamada de ai-inference a un proveedor de modelos baja de 60 a 40
segundos (inferenceTimeoutMs, como máximo 120000). Redactar una regla a partir de una descripción
espera ahora ese techo, con un presupuesto total de 50 segundos, y un modelo que no responde a
tiempo se notifica como tal y no como un proveedor no disponible. Si dependías del valor por
defecto de 60 segundos, fija inferenceTimeoutMs tú mismo.
dcctl bootstrap puede dar a una instancia un almacén de objetos
Los logotipos de inquilino y los artefactos de actualización se guardan en un almacén de objetos, y
una instancia creada por dcctl no tenía ninguno, así que ambos se rechazaban con
OBJECT_STORE_NOT_CONFIGURED. dcctl bootstrap acepta ahora
--object-store none|filesystem|s3, con opciones para el volumen o el bucket. Sin la opción, una
instancia en un clúster que crea dcctl recibe un almacén filesystem de 5Gi, y una en un clúster
indicado con --kube-context no recibe ninguno. La elección queda registrada y dcctl upgrade la
conserva, y rechaza un cambio que dejaría huérfanos los objetos guardados. dcctl upgrade no
añade un almacén a una instancia creada sin él: antes de v1.0.0, destrúyela y vuelve a crearla
con bootstrap con la opción. Consulta El almacén de objetos.
Los dispositivos pueden recibir configuración
Una versión de perfil de dispositivo declara cuáles de los atributos compartidos de sus dispositivos
puede ver un dispositivo, en la pestaña Configuración del editor de perfiles o con
setDeviceProfileConfigurationDeclaration. Escribir un atributo compartido declarado crea una
revisión de configuración numerada, que se envía al dispositivo por su propio topic de
configuración; el dispositivo informa RECEIVED, APPLIED o REJECTED, y pide la revisión actual
con un sync, que es también como le llega un perfil recién publicado. La página del dispositivo en
la consola muestra la declaración, la revisión actual y lo último que informó el dispositivo, y
deviceConfiguration y deviceConfigurationRevisions los leen. Escribir un atributo declarado se
rechaza con CONFIGURATION_TOO_LARGE cuando el documento superaría 64 KiB, y con
CONFIGURATION_VALUE_TYPE_MISMATCH cuando el valor no encaja con el tipo declarado. Por ahora, solo
dispositivos MQTT. Consulta
Topics de entrega de configuración.
Actualizaciones por aire: una primera entrega
El nuevo servicio update-management mantiene un catálogo por inquilino de imágenes de firmware,
software y configuración, con una subida comprobada contra su tamaño declarado y su resumen
SHA-256. Un artefacto se asigna a un dispositivo cada vez; al dispositivo se le avisa por su topic
de configuración, descarga la imagen con una concesión de corta duración que puede reanudar, y cada
intento se sigue hasta la versión en ejecución. Es opcional: se ejecuta en el perfil full, o con
--enable-area update-management. Necesita un almacén de objetos, y:
- No se puede asignar ningún artefacto hasta que fijes
updates.allowUnverifiedArtifacts: true, porque nada autentica aún quién produjo un artefacto. - Los dispositivos no reciben concesión de descarga hasta que se fija
updates.downloadBaseUrl. - Mantén la cadena de consulta de la ruta de descarga fuera de los registros de acceso. El chart la sirve en un ingress propio con el registro de acceso desactivado; detrás de otro proxy, haz lo mismo.
- Aún no hay despliegues a toda la flota, y solo se pueden actualizar dispositivos MQTT.
Los paquetes publicados
@devicechain/widget-sdkes nuevo. Define un widget (defineWidget) y construye un registro de ellos (createWidgetRegistry); consulta Escribir un widget personalizado.@devicechain/widgetslo declara como dependencia par, así que instálalo junto a@devicechain/widgets.@devicechain/widgetsya no exportaWIDGET_REGISTRY,ALARM_WIDGET_REGISTRY,CONTROL_WIDGET_REGISTRY,SELECTION_WIDGET_REGISTRYniLOCATION_WIDGET_REGISTRY. Busca un widget mediante un registro, como el que construyecreateBuiltinWidgetRegistry.echartses una dependencia par de@devicechain/widgets, no una dependencia: añadeecharts(^6.0.0) a tu aplicación. Las utilidades de gráficos están en@devicechain/widget-sdk/echarts.- Suben los rangos de las dependencias par:
reactyreact-doma^19.3.0,maplibre-gla^6.11.2. - Se eliminan
DashboardHubConfig.bindingsyDashboardHub.setBindingsde@devicechain/dashboards. Pasa los enlaces aDashboardRenderery usauseDashboardHub.fetchWidgetHistoryywidgetSubjectLabelaceptan un widget cuya fuente de datos ya está resuelta. parseDashboardDefinition(raw, { catalog })acepta el catálogo de widgets contra el que comprobar. Un widget de un tipo desconocido, o uno que el catálogo no sabe leer, se conserva como marcador (UnknownWidgetInstance, parte de la uniónWidgetInstance) y se vuelve a escribir exactamente como estaba guardado, donde antes hacía fallar toda la definición. Se rechaza una fuente de datos guardada de tipounbound.- El SDK de .NET abre las suscripciones por sí mismo y ya no pasa por el proxy HTTP del sistema.
Un dispositivo puede optar por procesar comandos de forma concurrente (
MaxConcurrentCommands), cada comando lleva su orden de llegada (DeviceCommand.Sequence), y una respuesta terminada durante una reconexión se envía después de ella en lugar de descartarse.
Alertas nuevas
Si enrutas o silencias alertas por nombre, estas son nuevas, además de las alertas del inicio de sesión único:
| Alerta | Qué informa |
|---|---|
SafetyGateDisabled | Un servicio se ejecuta sin una de sus compuertas de seguridad opcionales: la compuerta del ciclo de vida del inquilino, la presencia de comandos o la validación de las reglas de detección al publicar. |
DetectLoopStalled | El líder de detección lleva minutos sin avanzar. |
DetectLiveGapFillFailing, DetectLiveDeliveriesRecovered | La detección en vivo no pudo volver a leer, o sí volvió a leer, eventos que no se le entregaron. |
SparkplugIngestLeaderCountNotOne, Lwm2mIngestLeaderCountNotOne, Lwm2mIngestLeaderNotServing | Un servicio de ingesta perimetral no tiene exactamente un líder, o su líder no está sirviendo. Un sparkplug-ingest sin fuentes no tiene líder y la dispara. |
SparkplugIngestDroppingData, SparkplugIngestSubscriptionRefused, SparkplugIngestBrokerUnreachable, Lwm2mIngestDroppingNotifications | La ingesta Sparkplug o LwM2M está perdiendo datos o no puede alcanzar su bróker. |
CNPGCollectorAbsent | Prometheus ha dejado de recoger las métricas de una base de datos. |
dcctl install también deja de recoger métricas, y de alertar, de los componentes del plano de
control que no ejecuta un clúster gestionado como Google Kubernetes Engine, así que sus alertas
...Down ya no se disparan allí.
Otros comportamientos que conviene conocer
- Sparkplug. Un nacimiento de nodo que sustituye a una sesión en línea marca ahora como desconectados los dispositivos hijos de la sesión anterior; antes seguían en línea.
- Notificaciones. Un evento de alarma de un tipo que
notification-managementno conoce se reintenta y después se envía a mensajes fallidos, en lugar de descartarse. - Los mensajes fallidos y las decodificaciones fallidas conservan una carga útil con sus campos
de credencial sustituidos por
[redacted]. Un cuerpo que no es JSON se conserva como su longitud y su SHA-256. - Las respuestas a comandos pueden llevar cualquier valor JSON como
payload, no solo una cadena. - Los identificadores de correlación que genera la plataforma tienen 22 caracteres en lugar de un UUID de 36; trátalos como cadenas opacas.
- Las descripciones de GraphQL provienen solo de las cadenas
"""de los esquemas, y ahora cada elemento tiene una; la introspección ya no muestra comentarios#. dcctl installydcctl bootstrapcon--kube-contextya no necesitan Docker nikind, y comprueban la versión de Kubernetes del contexto al que se dirigen. Un contexto indicado que falta en el kubeconfig hace fallar la comprobación previa.- La detección puede dividir su estado en
detectShards(1-64, por defecto 1). Las particiones siguen ejecutándose una tras otra, así que más de una aún no acelera la detección.
v0.19.0 — dimensionada a partir de lo medido a 6000 eventos por segundo; un stream lleno rechaza
v0.19.0 es una actualización en su sitio desde v0.18.0: dcctl install para el clúster y
después dcctl upgrade para cada instancia, como se describe para la v0.17.0.
Cuenta tu almacén de eventos antes de ejecutar el dcctl install de esta versión. Una
instancia cuyo almacén de eventos guarda más de
4 000 000 de filas aún sin comprimir no puede recibirla en su sitio y hay que recrearla, lo que
descarta sus datos, y la mayoría de las instancias con tráfico real superan ese límite
(Comprueba el número de filas). También se rechaza una tabla con más de 500
fragmentos, salvo que antes elimines sus eventos más antiguos
(Cuenta los fragmentos). Una instancia creada por la v0.16.0 o una versión
anterior sigue teniendo que destruirse y crearse de nuevo.
La mayor parte de esta versión trata de seguir el ritmo. Los servicios que procesan cada evento
solicitan ahora la CPU que se midió que usan, se reparten entre los nodos, almacenan y fusionan los
eventos en menos sentencias y responden desde memoria las búsquedas repetidas; con --ha sin
--compact, event-management se ejecuta en dos pods. En Google Kubernetes Engine, una instalación HA
predeterminada aceptó 6000 eventos por segundo durante 10 minutos, dos veces, y almacenó
exactamente una vez cada evento aceptado, comprobado evento por evento (Rendimiento). Dos caminos de fallo dejan
de ser silenciosos: un stream de ingesta lleno rechaza ahora los eventos nuevos en lugar de
descartar los no leídos, y las alertas de respaldo avisan antes de que el archivado pueda detener
una base de datos. Y dcctl upgrade aplica ahora los ajustes del bróker de mensajería y del almacén
de eventos de una instancia, algo que antes no hacía nunca, así que una instancia actualizada recibe
el mismo bróker y el mismo almacén de eventos que una nueva.
Quién tiene que hacer algo:
- Toda instancia con tráfico real: cuenta las filas sin comprimir y los fragmentos del almacén de eventos antes de actualizar. Por encima de 4 000 000 de filas hay que recrear la instancia, y recrearla descarta sus datos (Comprueba el número de filas).
- Todo clúster: vuelve a ejecutar
dcctl installcon esta versión antes de cualquierbootstrap,upgradeodestroy. Estedcctlrechaza un registro de instalación escrito por uno anterior. Actualiza también todas las copias dedcctlque uses: una anterior puede dejar marcada como sin terminar una instancia creada por esta versión, y undcctl bootstrapsin más se ejecutaría entonces sobre ella (Terminar un arranque inicial fallido). - Toda instancia instalada sin
--compact: los nodos necesitan sitio para unas solicitudes de CPU mayores y para las primeras solicitudes del bróker de mensajería (Haz sitio). - Quien tenga dispositivos que envían más de 256 lecturas en un mensaje: esos mensajes se rechazan ahora (256 lecturas).
- Todo inquilino que envíe más de 1000 lecturas por segundo, incluida una flota que pone varias
lecturas en un mensaje, y todo inquilino LwM2M: el techo de ingesta cuenta ahora lecturas, y
las que lo superan se rechazan con
429por HTTP y se descartan, sin avisar al dispositivo, en todos los demás transportes (Lecturas, no mensajes). - Quien tenga dispositivos que envían lecturas fechadas más de 366 días atrás, o que informan horas cercanas a 1970 porque no tienen el reloj configurado: esas lecturas se rechazan ahora (Lecturas antiguas).
- Quien haya configurado
backup_retention,tsdbConfiguration.maxOpenConnections,rdbConfiguration.maxOpenConnections,maxReadingsPerMessageo los límites de conexión de los lectores de analítica: consulta Antes de actualizar. - Quien dependa de que revocar una credencial de dispositivo surta efecto al instante: una
revocación puede tardar ahora hasta cinco segundos en una réplica de
device-management(Comprobación de credenciales). - Quien llame a la API de ingesta HTTP: reintenta ante un
503conRetry-After(Contrapresión). - Quien tenga una fuente de eventos que lee de su propio bróker MQTT: la fuente se conecta con un identificador de cliente nuevo, que el bróker tiene que permitir, y puede leerse dos veces mientras avanza la actualización (Tu propio bróker MQTT).
- Quien enrute o silencie alertas por nombre o por severidad: se añaden alertas, y
JetStreamStreamNearFullpasa ainfo(Alertas). - Quien instale el chart con sus propios valores: varios valores de recursos de nivel superior ya no llegan a los servicios de la ruta de eventos (Tus propios valores del chart).
Todo lo demás está en Qué cambia, por áreas.
Antes de actualizar
Hazlo en este orden.
1. Comprueba el número de filas
El primer arranque del nuevo event-management reconstruye las claves del almacén de eventos sobre
todas las filas de cinco tablas que aún no están comprimidas: por defecto, más o menos la última
semana. Una instancia con más de 4 000 000 de esas filas no se puede actualizar en su sitio. Un
evento base es una fila, más una por cada lectura, ubicación o alerta que lleva, más una por cada
anclaje de relación. Con una lectura y un anclaje por evento, 4 millones de filas son una semana a
una media de unos 2 eventos por segundo, así que la mayoría de las instancias con tráfico real
superan el límite.
Abre psql en el almacén de eventos (el espacio de nombres es dci- más el id de la instancia; la
base de datos es el id de la instancia solo). El recuento solo lee, así que sirve cualquiera de los
pods del almacén; si dc-tsdb-1 no existe, kubectl -n dci-<instance-id> get pods lista los demás:
kubectl -n dci-<instance-id> exec -it dc-tsdb-1 -c postgres -- psql -U postgres -d <instance-id>
Después ejecuta el recuento que hace la actualización. Lee todas las filas sin comprimir, así que en un almacén grande tarda un rato:
DO $$
DECLARE c record; n bigint := 0; k bigint;
BEGIN
FOR c IN SELECT format('%I.%I', chunk_schema, chunk_name) AS chunk
FROM timescaledb_information.chunks
WHERE hypertable_schema = 'event-management'
AND hypertable_name IN ('events', 'measurement_events', 'location_events',
'alert_events', 'event_anchors')
LOOP
EXECUTE 'SELECT count(*) FROM ONLY ' || c.chunk INTO k;
n := n + k;
END LOOP;
RAISE NOTICE 'rows to rebuild: %', n;
END $$;
Si imprime más de 4000000, no actualices esta instancia en su sitio: saca de ella lo que
necesites y después recréala con dcctl destroy y dcctl bootstrap en la nueva versión. Cuenta
mientras el clúster sigue en la v0.18.0, antes del dcctl install de esta versión, para que una
instancia por encima del límite aún se pueda leer y destruir con el dcctl que la creó.
dcctl destroy elimina las bases de datos de la instancia: todos los inquilinos, dispositivos,
definiciones de dispositivo, paneles y usuarios, y todos sus eventos. DeviceChain no tiene ninguna
herramienta de exportación. Saca lo que necesites antes de destruirla. Los eventos, lecturas,
ubicaciones y alertas de un inquilino se pueden leer mediante el
acceso por SQL y BI. pg_dump de PostgreSQL puede copiar las dos
bases de datos de la instancia, cada una con el id de la instancia como nombre: una en el almacén de
eventos, en el espacio de nombres de la propia instancia, y otra en la base de datos relacional
compartida (dc-rdb, en dc-system), que guarda los datos de todas las instancias, así que vuelca
solo esa base de datos. Escribe el volcado en tu propia máquina, por ejemplo
kubectl -n dc-system exec dc-rdb-1 -c postgres -- pg_dump -U postgres -Fc <instance-id> > rdb.dump,
y lo mismo con -n dci-<instance-id> y dc-tsdb-1 para el almacén de eventos. Los secretos del
volcado siguen sellados con la clave raíz de la instancia. Un volcado es una copia para leer, o
para cargar en una base de datos tuya: ningún dcctl lo carga en la instancia nueva.
Conserva los respaldos, para poder volver atrás. Destruye la instancia con el dcctl de la
v0.18.0 antes de ejecutar el dcctl install de esta versión: deja en su sitio los respaldos
internos. Después de la instalación, usa el dcctl destroy --keep-backups de esta versión; sin esa
opción borra el archivo del almacén de eventos de la instancia. En ambos casos, lo que se conserva
es el archivo del almacén de eventos, junto al archivo relacional del clúster, que destroy no
borra nunca. En el mismo clúster, el archivo del almacén de eventos solo devuelve el historial de
eventos de la instancia, bajo un plano de control vacío, sin inquilinos, dispositivos, usuarios ni
secretos. Recuperar la instancia entera en la v0.18.0 significa recuperarla en un clúster nuevo, a
partir del archivo relacional en un momento anterior al destroy (--restore-rdb-at), lo que
rebobina todas las instancias de ese clúster, con el respaldo en custodia de la clave raíz de la
instancia y el archivo de su almacén de eventos, como describe
Recuperar una instancia. Restaurar el archivo del almacén de
eventos en la v0.19.0 no es un camino hacia delante: devuelve las mismas filas, y la
actualización las vuelve a rechazar.
El archivo conservado del almacén de eventos guarda los eventos de todos los inquilinos que tuvo la instancia, incluidos los inquilinos eliminados dentro de su ventana de recuperación, y nada lo poda una vez que la instancia ya no existe. Elimínalo cuando ya no lo necesites, como muestra Qué pasa con los respaldos de la instancia.
Antes de ejecutar dcctl bootstrap con el mismo nombre, aparta el artefacto de custodia de la clave
raíz de la instancia (~/.devicechain/escrow/<instance>-rootkey.escrow) y consérvalo: bootstrap no
lo sobrescribe, y es la única clave de los respaldos relacionales tomados antes del destroy.
No se ha probado ningún camino en su sitio para una instancia por encima del límite.
2. Cuenta los fragmentos
La reconstrucción, y la eliminación de índices, también rechazan una
tabla con más de 500 fragmentos. Cada una bloquea todos los fragmentos de la tabla que cambia, y
500 es la mitad de lo máximo que esta versión ha medido que se reconstruye dentro de un arranque, lo
que deja margen para un almacenamiento más lento. Por encima, event-management se niega antes de
bloquear nada, en lugar de arriesgarse a una reconstrucción que no puede terminar y que después
bloquea todos los arranques siguientes. Un fragmento cubre un intervalo de fragmento de una tabla:
un día por defecto (lifecycle.chunkIntervalHours), así que con el valor por defecto 500 fragmentos
son un año y cuatro meses de historial, aproximadamente, con la retención desactivada. Un intervalo
más corto llega antes a 500, y también un dispositivo que envió lecturas fechadas muy en el pasado.
Cuéntalos, en cualquiera de los pods del almacén de eventos:
SELECT hypertable_name, count(*) AS chunks, min(range_start) AS oldest, max(range_end) AS newest
FROM timescaledb_information.chunks WHERE hypertable_schema = 'event-management'
GROUP BY 1 ORDER BY 1;
Si una tabla tiene más de 500 y puedes prescindir de sus eventos más antiguos, elimínalos antes de actualizar. Esto borra para siempre los eventos de todos los inquilinos en esos fragmentos.
- Detén lo que los envió. Si un dispositivo sigue enviando lecturas fechadas muy en el pasado, revoca antes su credencial: la versión anterior las acepta, y volverían a crear los fragmentos.
- Elige una fecha de corte. Los fragmentos que terminan en ella o antes se eliminan enteros; el fragmento que la abarca se conserva. La fecha en que se instaló la instancia elimina solo eventos fechados antes de que la instancia existiera. La misma fecha de corte se aplica a las seis tablas de eventos, para que ningún evento quede sin sus lecturas.
- Ejecuta la eliminación en el primario del almacén de eventos.
kubectl -n dci-<instance-id> get pods -l cnpg.io/instanceRole=primarylista los primarios; usa el que empieza pordc-tsdb-, abrepsqlen él como arriba y ejecuta la sentencia de abajo con tu fecha de corte en lugar de2024-01-01. Ejecútala sola, no dentro deBEGIN: elimina como mucho 100 fragmentos de una tabla cada vez y confirma tras cada lote, así que la base de datos nunca tiene que bloquearlos todos a la vez.
DO $$
DECLARE
cutoff constant timestamptz := TIMESTAMPTZ '2024-01-01';
tbl text;
step timestamptz;
BEGIN
FOREACH tbl IN ARRAY ARRAY['events', 'measurement_events', 'location_events',
'alert_events', 'event_anchors', 'state_change_events'] LOOP
LOOP
SELECT max(range_end) INTO step FROM (
SELECT range_end FROM timescaledb_information.chunks
WHERE hypertable_schema = 'event-management' AND hypertable_name = tbl
AND range_end <= cutoff
ORDER BY range_end LIMIT 100) oldest;
EXIT WHEN step IS NULL;
PERFORM drop_chunks(format('%I.%I', 'event-management', tbl)::regclass, older_than => step);
COMMIT;
END LOOP;
END LOOP;
END $$;
Después vuelve a contar los fragmentos y actualiza. Si no puedes prescindir de esos eventos, recrea la instancia como en Comprueba el número de filas.
3. Ejecuta la actualización donde se creó la instancia
dcctl upgrade aplica ahora la configuración de OpenTofu de la instancia, así que necesita su
estado: ejecútala en la máquina que creó la instancia, o copia antes allí
~/.devicechain/instances/<instancia>/ y mantenlo privado (modo 0700), porque contiene
credenciales. También necesita tofu en el PATH, con OpenTofu 1.9 o posterior (o Terraform 1.9 o
posterior). Sin el estado, la actualización se niega y no cambia nada. --skip-infrastructure
mueve solo los servicios, como antes, y dice lo que dejó sin aplicar.
4. Haz sitio
Una vez actualizada, una instancia instalada sin --compact solicita más CPU, y la actualización
progresiva necesita aún más mientras los pods antiguos y los nuevos se ejecutan a la vez:
| CPU solicitada de más una vez actualizada | CPU libre que la actualización progresiva de los servicios necesita a la vez | |
|---|---|---|
--ha | 4,45 para los servicios, más 500m y 768Mi de memoria en cada uno de los tres nodos que ejecutan un servidor NATS | 4,85, además de la de los servidores NATS: 4,55 para los pods que arrancan junto a los antiguos (device-management, los dos pods de event-management, device-state y event-sources) y 0,3 para event-processing, cuyo pod nuevo arranca después de que se detenga el antiguo |
sin --ha | 3,55 para los servicios, más 500m y 768Mi para el único servidor NATS | 3,95 (3,65 y 0,3), además de la del servidor NATS |
--compact | nada para los servicios; 25m y 64Mi en cada nodo que ejecuta un servidor NATS | 0,1, como antes (25m por cada pod que arranca junto al antiguo), además de la de los servidores NATS |
Estas cifras son para una instancia que ejecuta los cinco servicios. Las nuevas solicitudes de los
servicios son device-management 800m, event-management 900m (dos pods con --ha, lo que añade
también los 128Mi de memoria del segundo pod), device-state 950m, event-sources 1 núcleo y
event-processing 400m; en v0.18.0 cada uno solicitaba 100m. Cada pod nuevo necesita su solicitud
entera libre en un solo nodo. Cada servidor NATS solicita ahora 500m de CPU y 768Mi de memoria y
tiene un límite de 2Gi de memoria; antes no solicitaba nada. Compara la CPU y la memoria asignables
libres de los nodos (kubectl describe nodes, "Allocated resources") con
Dimensionamiento de los servicios. Un clúster kind en un portátil
es el que más probablemente no las tenga.
Un pod que no se puede colocar queda en Pending, y la actualización falla tras esperar (hasta 15
minutos para el bróker), con la instancia actualizada a medias: los servicios cuyos pods nuevos
arrancaron están en la nueva versión, y el resto sigue en la antigua. Un servidor NATS que queda en
Pending sin --ha deja el bróker caído hasta que haya sitio. Haz sitio y vuelve a ejecutar
dcctl upgrade para terminar. Una instancia creada con dcctl no tiene forma de conservar las
solicitudes antiguas; el remedio es la capacidad, o volver a crearla con --compact. Las instancias
instaladas con --compact conservan sus solicitudes de 25m y 64Mi, y sus servidores NATS solicitan
lo mismo.
Un ResourceQuota o LimitRange en el espacio de nombres de la instancia puede rechazar las
solicitudes mayores, los límites de 2 núcleos de event-sources y device-state, o el límite de 1
núcleo de event-processing.
5. Comprueba si hay una cuota sobre la ubicación entre espacios de nombres
Los pods de base de datos llevan ahora una preferencia de ubicación que tiene en cuenta otros
espacios de nombres (Primarias de las bases de datos). Un ResourceQuota
con el ámbito CrossNamespacePodAffinity rechaza esos pods, sea preferencia o no: las instancias
reiniciadas de la base de datos relacional en dc-system, y el almacén de eventos de cada instancia
en su propio espacio de nombres. Nada de lo que instala DeviceChain crea uno. Para listar los que
existan:
kubectl get resourcequota -A \
-o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,SCOPES:.spec.scopeSelector
Una lista vacía no lo zanja. La configuración de admisión de cuotas del servidor de API puede
nombrar CrossNamespacePodAffinity en limitedResources, y entonces esos pods se rechazan en todo
espacio de nombres que no tenga una cuota con ese ámbito que los admita. Esa configuración vive
en el plano de control, así que pregunta a quien administre el clúster; si está activa, da a
dc-system y al espacio de nombres de cada instancia una cuota con ese ámbito antes de actualizar.
Un pod del almacén de eventos rechazado deja el almacén con una instancia menos o, sin --ha, caído.
6. Comprueba los ajustes que ahora impiden que un servicio arranque
- Si fijaste
tsdbConfiguration.maxOpenConnectionsdeevent-management, ordbConfiguration.maxOpenConnectionsdedevice-state, en10o menos y no fijastepersistence.writersniprojection.writers, el pod nuevo no arranca, y nombra el ajuste y el tamaño del pool. Fija los escritores por debajo de tu pool (el valor por defecto anterior era5), o quita el ajuste del pool para usar el valor por defecto de 20. Los pools de 11 a 19 arrancan y registran que más de la mitad del pool se da a los escritores. - Lectores de analítica (instancias
--ha): sus valores deconnection_limitpueden sumar ahora como máximo 17 de las conexiones del almacén de eventos, no 57, porque la plataforma reserva 80 para dos pods deevent-managementen lugar de 40. Por encima de 17 la actualización se detiene antes de cambiar nada. Si subistetimescale_analytics_reserved_connectionspara ejecutar por tu cuenta un segundo pod deevent-management, devuélvelo a 40: ahora es la reserva de cada pod. Consulta Límite de conexiones. maxReadingsPerMessagequeda retirado: el servicio lo ignora con un aviso. Consulta 256 lecturas para localizar los dispositivos que rechazaría.
7. Comprueba lo que envían tus dispositivos
- Más de 256 lecturas en un mensaje se rechazan. Localiza esos dispositivos como indica 256 lecturas.
- Más de 1000 lecturas por segundo de un inquilino superan ahora el techo predeterminado, que
cuenta lecturas en lugar de mensajes. Localiza esos inquilinos y eleva primero la tasa de ingesta
de su nivel, como indica Lecturas, no mensajes. Las lecturas por encima
del techo se descartan, no se retrasan: HTTP responde
429, los dispositivos MQTT, LwM2M y Sparkplug no reciben aviso, y un mensaje MQTT descartado así ya estaba confirmado. - Las lecturas fechadas más de 366 días antes de llegar se rechazan. Un dispositivo sin reloj
debe omitir
occurredTime. Los mensajes que ya esperan en la plataforma se juzgan según la hora en que llegaron; el almacén local de un agente de borde no tiene límite de antigüedad propio (Lecturas antiguas).
8. Cambia el nombre de backup_retention
Si lo configuraste: backup_retention_rdb en la configuración del clúster, backup_retention_tsdb
en la de la instancia. Un -var backup_retention=… se rechaza; una línea en un .tfvars solo
produce un aviso, y se usa el valor predeterminado en lugar del tuyo; una variable
TF_VAR_backup_retention se ignora sin ningún aviso.
9. Comprueba el margen del almacén de respaldos
La base de datos relacional conserva ahora 30 días de respaldos en lugar de 7 (Ventanas de recuperación), y un almacén existente conserva su tamaño, así que un almacén creado antes de esta versión sigue teniendo 20 GiB salvo que lo hayas ampliado. Amplíalo primero si tiene poco margen, como muestra Tamaño del almacén de objetos de respaldo.
10. Conserva los valores de OpenTofu que fijaste tú mismo
La actualización conserva un valor declarado en un terraform.tfvars junto al estado de la
instancia (~/.devicechain/instances/<instancia>/infra/instance/) o en TF_VAR_<nombre>, y no uno
que pasaste con -var. Se niega antes que acortar la ventana de recuperación del almacén de eventos
o dejar de declarar un lector de analítica, y avisa de una solicitud o un límite del bróker que
bajaría. Pasa esos valores a ese archivo primero. Consulta
Valores que usted mismo dio a la configuración.
11. Si sustituiste la imagen de base de datos del almacén de eventos
Comprueba que puede usar lz4 para la
compresión del registro de escritura anticipada; una instancia que se reinicie en una
imagen que no puede puede no arrancar. Actualiza solo si cada línea imprime t:
pods=$(kubectl -n dci-<instance> get pods -l cnpg.io/cluster=dc-tsdb,cnpg.io/podRole=instance -o name)
for p in $pods; do
kubectl -n dci-<instance> exec "$p" -c postgres -- psql -U postgres -tAc \
"SELECT 'lz4' = ANY (enumvals) FROM pg_settings WHERE name = 'wal_compression'"
done
12. Si una fuente de eventos lee de tu propio bróker MQTT
Cada fuente así se conecta ahora como devicechain:<instancia>:<fuente>:<pod> en lugar de
devicechain (Su propio identificador de cliente). Si la ACL, la
lista de permitidos o la regla de identificadores de tu bróker nombra devicechain, permite los
identificadores que empiezan por devicechain:. El identificador además supera los 23 caracteres,
el límite más corto que MQTT exige aceptar a un bróker. Un bróker que rechaza el nuevo
identificador hace que el pod nuevo no arranque, con el motivo del bróker en su registro, y la
actualización se detiene con los pods viejos aún en marcha y leyendo la fuente como antes.
Para no almacenar mensajes dos veces mientras avanza la actualización (consulta Durante la actualización), prevé pausar a los publicadores de tu bróker mientras dura el despliegue.
Durante la actualización
dcctl install da a la base de datos relacional compartida una ventana de recuperación de 30
días, zstd para su registro archivado y la etiqueta de ubicación, y reinicia una vez sus instancias:
con --ha, primero las réplicas en espera y después un traspaso a una de ellas, en otro nodo. Una
base de datos relacional de una sola instancia no está disponible hasta que termina de reiniciarse;
las escrituras hechas mientras tanto se reintentan.
dcctl upgrade aplica después los servidores NATS y el almacén de eventos de la instancia antes
de mover los servicios (detalles):
- Los servidores NATS se reinician por sus nuevas solicitudes y su límite, de uno en uno con
--ha. Sin--hael bróker no está disponible mientras se reinicia su único servidor, normalmente alrededor de un minuto. Con--ha, la ingesta HTTP puede responder503brevemente mientras se sustituye un servidor; reinténtalo como con cualquier503(calidad de servicio). - Sin
--ha, la detección se detiene mientras se reinicia el bróker, durante ese minuto más hasta 25 segundos, y después reproduce el flujo desde su último punto de control (La partición de la detección). - En una instancia creada con
v0.18.0, las instancias del almacén de eventos pueden reiniciarse una vez por la preferencia de ubicación: primero las réplicas en espera y después un cambio de primaria con--ha. Sin--ha, los eventos no se almacenan hasta que vuelve la única instancia, y mientras tanto esperan en el stream de ingesta. Su compresión del registro de escritura anticipada y el archivado con zstd no necesitan reinicio. - El bróker y el almacén de eventos pueden reiniciarse a la vez; la actualización espera a ambos.
Mientras se despliega event-sources, una fuente sobre tu propio bróker MQTT puede leerse dos
veces. Hasta que se detiene el último pod de la versión anterior, ese pod sigue leyendo la fuente
con el identificador viejo, junto al pod nuevo que tiene la fuente, y cada uno almacena lo que
recibe. Los mensajes que llegan en esa ventana pueden almacenarse dos veces. Ocurre una sola vez, y
lo mismo pasa al volver a la versión anterior; pausar a los publicadores de tu bróker mientras dura
el despliegue lo evita. Los arrendamientos que deja una versión revertida caducan solos en 30
segundos.
El nuevo event-management, en su primer arranque, elimina dieciséis índices y reconstruye las
claves del almacén de eventos, tabla a tabla
(Las claves del almacén de eventos). Mientras trabaja en una tabla, las
lecturas y escrituras de esa tabla esperan: unos segundos en el caso habitual, y como máximo 45
segundos por tabla para la reconstrucción de la clave. El event-management anterior sigue
recibiendo y almacenando eventos; un lote retenido el tiempo suficiente se entrega de nuevo y se
almacena una sola vez. Con --ha, los pods se
renuevan de uno en uno, y el primer pod nuevo en arrancar hace el trabajo antes de estar listo. Un
segundo pod nuevo que arranque mientras ese trabajo sigue en curso lo espera y puede ser reiniciado
una vez por su comprobación de arranque; el que arranca después no tiene nada que esperar. Ambos
casos son lo esperado. Si el trabajo se detiene (una tabla ocupada, el minuto agotado, demasiadas
filas, más de 500 fragmentos, una tabla que tarda más de 40 segundos), event-management se detiene
con un error que nombra la tabla y qué hacer, todos los demás servicios ejecutan la nueva versión, y
el event-management anterior sigue almacenando eventos. Consulta
Si el cambio del almacén de eventos se detiene.
Si el cambio del almacén de eventos se detiene
- Demasiadas filas:
event-managementse detiene antes de cambiar nada, y explica el motivo.dcctl upgradeinforma de que no terminó de desplegarse; recrea la instancia como en Comprueba el número de filas. - Una tabla con más de 500 fragmentos:
event-managementse detiene antes de bloquear nada; su registro nombra la tabla, su número de fragmentos y la consulta de Cuenta los fragmentos. Ese arranque no cambia ni marca nada (su registro lista lo que ya hicieron arranques anteriores): una vez eliminados los fragmentos, el siguiente arranque sigue adelante. - Una tabla ocupada, o el minuto agotado: cada intento de bloqueo desiste como máximo a los 5
segundos, y una tabla ocupada se reintenta cada 2 segundos durante un minuto como máximo.
event-managementse detiene entonces con un error que nombra la tabla, lista lo que ya está hecho e incluye una consulta que lista las sesiones que la retienen; continúa desde ahí al reiniciarse. Si se sigue deteniendo, busca consultas SQL o de BI de larga duración, el borrado de un inquilino o el trabajo de compresión o de retención del almacén. - La reconstrucción de la clave de una tabla tarda más de 40 segundos una vez bloqueada: se
deshace, la tabla conserva su clave anterior, y
event-managementse detiene de inmediato en cada arranque posterior sin tocarla, así que la ingesta no se atasca en cada reinicio. Recrea la instancia o, para intentarlo una vez más (por ejemplo, en un almacenamiento más rápido), ejecuta la sentenciaCOMMENT ON INDEXque da el error. Si la tabla guarda eventos fechados muy en el pasado, eliminarlos como en Cuenta los fragmentos acorta la reconstrucción; después ejecuta la sentenciaCOMMENT ON INDEX. - Sin espacio para bloqueos: eliminar un índice o reconstruir una clave bloquea cada fragmento de su tabla; el error lo indica y nombra el ajuste que hay que aumentar.
Después de actualizar
- Comprueba dónde están las dos primarias de las bases de datos. Los traspasos de arriba pueden dejar las dos en un mismo nodo. Dónde se ejecutan las primarias de las bases de datos explica cómo comprobarlo y cómo mover una.
- Vigila el almacén de respaldos durante tres semanas. Nada sale de la nueva ventana de 30 días de la base de datos relacional hasta que tiene 30 días, así que su parte del almacén crece durante unas tres semanas, hacia unas cuatro veces el registro que conserva hoy más unos 23 respaldos base nocturnos más, y después se estabiliza.
- Vigila las lecturas rechazadas en el techo de cada inquilino:
devicechain_eventsources_total_readings_rate_limited,devicechain_lwm2mingest_ingest_samples_shed_totalydevicechain_sparkplugingest_ingest_samples_shed_total. Un inquilino que descarta ahí está enviando más lecturas de las que permite su nivel. Eleva la tasa de ingesta de su nivel, o haz que sus dispositivos envíen menos. - Una instancia existente conserva los tamaños de sus volúmenes. La actualización indica que esta versión crea un almacén de eventos de 32Gi; para ampliar uno de 8Gi, consulta Volumen del almacén de eventos. Un almacén de respaldos existente también conserva su tamaño.
Volver a v0.18.0 no necesita ningún cambio de datos: guarda y lee eventos con las claves nuevas y
sin los índices eliminados, y devuelve event-management a un pod y el techo de ingesta a contar
mensajes. Una lectura es más lenta allí: v0.18.0 lista los eventos registrados contra un anclaje de
relación recorriendo los eventos del inquilino en el rango de tiempo pedido, comprimidos incluidos.
Una configuración de device-management que fije inMemoryCache.perDeviceCacheEntries o
inMemoryCache.perDeviceCacheMiB tiene que quitarlos antes: v0.18.0 no arranca con ninguno de los
dos.
Qué cambia
Ingesta
Un stream de ingesta lleno rechaza eventos nuevos en lugar de descartar los no leídos
Cuando un consumidor se atrasaba tanto que su cola sin leer llenaba inbound-events o
resolved-events, el stream descartaba sus eventos más antiguos para hacer sitio, y eran eventos
que nadie había procesado todavía. Al dispositivo ya se le había dicho que se aceptaron.
Ahora, cuando la cola sin leer de device-management en inbound-events, o la de
event-management en resolved-events, alcanza el 90% de lo que cabe en el stream, la plataforma
deja de aceptar eventos nuevos hasta que la cola baja del 80%. Los dos streams conservan su semana
de eventos ya procesados, y ese historial no cuenta para el 90%: solo cuentan los eventos sin
leer. Un stream lleno también rechaza antes de descartar ese historial tan deprisa que alcance
eventos sin leer; consulta
Un stream lleno también rechaza antes de que los eventos grandes expulsen a los no leídos.
- La ingesta HTTP responde
503conRetry-After: 10mientras rechaza. Reintenta ante un503. Un503sinRetry-Aftersigue significando que la publicación en sí falló. Una solicitud por encima del techo de su propio inquilino recibe antes un429; solo una que esté dentro de él puede recibir el503(Lecturas, no mensajes). - Los dispositivos MQTT ya recibieron el acuse del bróker. Sus mensajes esperan en el stream de captura hasta que se reanuda la ingesta.
- Las lecturas de Sparkplug y LwM2M, y los mensajes de un bróker MQTT externo, se descartan y se cuentan, porque esos protocolos no dan a la plataforma forma de hacer que el dispositivo reintente. Las transiciones de conexión y desconexión se siguen aceptando. La compuerta de contrapresión no limita cuántas admite (el techo del inquilino sigue aplicándose a la toma del broker), así que una flota que se reconecta en bucle aún puede llevar el stream a su techo, donde descarta sus eventos más antiguos como antes.
- El rechazo afecta a todos los inquilinos, porque los streams son compartidos. Un
device-stateo unevent-processinglentos no lo provocan. - El simulador y el arnés de carga cuentan un
503conRetry-Aftercomo rechazo, no como fallo. Consulta la entrada sobre los informes de las pruebas de carga para ver cómo lo informan.
Consulta Contrapresión en la ruta de ingesta. Ningún stream se reconfigura, y un servicio que siga en la versión anterior conserva su comportamiento anterior hasta que se actualice.
Un stream lleno también rechaza antes de que los eventos grandes expulsen a los no leídos
El rechazo de arriba valoraba cada evento sin leer al tamaño medio del stream, porque el bróker
informa de los bytes totales de un stream y no de los de los eventos que un consumidor no ha leído.
En un stream lleno eso es la parte sin leer del NÚMERO de eventos. Una ráfaga de eventos grandes
sobre un historial de eventos pequeños ya procesados podía, por tanto, llenar inbound-events o
resolved-events mientras la cola sin leer marcaba más o menos la mitad del límite, y el stream
descartaba entonces eventos que nadie había procesado. Medir después de cada publicación no lo
habría evitado.
- El stream también rechaza cuando se le agota el historial procesado. Un stream lleno descarta primero sus eventos más antiguos, así que los eventos ya procesados que aún conserva por delante de los no leídos son lo que se agota antes de que se pierda uno sin leer. Los servicios que escriben en el stream rechazan ahora eventos nuevos cuando ese historial se descartaría entero en 30 segundos al ritmo al que el stream lo está descartando, y vuelven a aceptarlos cuando duraría un minuto, o el consumidor lo ha leído todo. Las dos magnitudes se calculan a partir de números de secuencia que informa el bróker, así que la regla no depende del tamaño de los eventos. Un stream lleno cuyo consumidor mantiene el ritmo no rechaza nada. Eliminar un inquilino tampoco hace por sí solo que un stream rechace, salvo que sus eventos sean los más antiguos que conserva un stream lleno: entonces el stream puede rechazar, sin perder nada, hasta que el consumidor se ponga al día. Consulta Contrapresión en la ruta de ingesta para ese caso y para un stream lleno que contiene menos de un minuto de su propio tráfico.
- Un evento que el consumidor recibió pero no ha confirmado cuenta como no leído, también uno que espera a entregarse de nuevo tras un fallo. Mientras un evento así está cerca del principio de un stream lleno, el stream puede rechazar eventos nuevos hasta que se confirme o se abandone.
- Cada servicio mide también el stream en cuanto él mismo ha escrito alrededor de una milésima parte del límite del stream desde su última medición, como mucho cada 100 ms, además de cada 5 segundos.
- Una serie nueva,
devicechain_<area>_jetstream_backpressure_history_runway_seconds{stream, durable}, muestra cuánto duraría ese historial.JetStreamIngestBackpressureEngagedtambién se dispara por este rechazo, y su descripción explica ahora cómo distinguir las dos causas. - Aún se puede perder un evento sin leer cuando los servicios que escriben en un stream gastan, entre dos de sus mediciones, más historial procesado del que se vio descartar al stream en los 30 segundos anteriores, o cuando un stream llega a su límite por primera vez casi sin historial procesado.
Ningún stream se reconfigura y no se añade ningún ajuste.
El techo de ingesta de un inquilino cuenta lecturas, y HTTP lo comprueba antes de la compuerta de contrapresión compartida
El techo de ingesta por inquilino cuenta ahora lecturas en todos los transportes de dispositivo. Una lectura es un valor almacenado: una clave de una entrada de medición, una ubicación o una alerta. Antes, HTTP y MQTT cobraban una unidad por mensaje, llevara las lecturas que llevara. Un inquilino con el valor por defecto de 1000 por segundo podía enviar hasta 256 000 lecturas por segundo, mucho más de lo que una instalación puede almacenar, y la compuerta de contrapresión acababa rechazando los eventos de todos los inquilinos.
- HTTP y MQTT: un mensaje se cobra una vez antes de decodificarlo, como antes, y después una vez
por cada lectura que lleva. En HTTP, un inquilino por encima de su techo recibe
429. En MQTT, el mensaje se confirma y se descarta, como ya ocurría con un mensaje por encima del techo. Ese mensaje se cuenta en el nuevodevicechain_eventsources_total_msg_reading_limitedy sus lecturas en el nuevodevicechain_eventsources_total_readings_rate_limited. Se recibió, así que también se cuenta endevicechain_eventsources_total_inbound_messages, y no endevicechain_eventsources_total_msg_rate_limited, que sigue contando solo los mensajes descartados antes de decodificar. - HTTP comprueba primero el techo del propio inquilino. Una solicitud por encima del techo de su
inquilino recibe
429aunque la plataforma esté rechazando eventos. Solo una solicitud dentro del techo de su inquilino puede recibir el503de contrapresión. El cuerpo de la solicitud se lee y se decodifica ahora antes de comprobar la contrapresión, y una solicitud rechazada por contrapresión se cuenta ahora como recibida. - Los brokers MQTT externos también comprueban el techo del inquilino antes de la compuerta de
contrapresión, así que un mensaje por encima del techo de su inquilino mientras la plataforma
rechaza se cuenta en
devicechain_eventsources_total_msg_rate_limiteden lugar de endevicechain_eventsources_total_msg_backpressured. - LwM2M ya cobraba lecturas, a 25 veces el techo. Ahora las cobra al propio techo, así que el presupuesto de lecturas LwM2M de un inquilino es 25 veces menor que antes.
- Sparkplug tiene ahora un techo de ingesta por inquilino, contado en lecturas:
ingestRateLimit.messagesPerSecond(por defecto 1000) eingestRateLimit.burst(por defecto 2000). Las lecturas de cada mensaje DATA que lo superan se descartan y se cuentan endevicechain_sparkplugingest_ingest_samples_shed_total. Los nacimientos, las muertes y el ritmo de mensajes no se miden. Consulta Qué acota la ingesta de Sparkplug. - El valor por defecto sigue siendo 1000 por segundo con una ráfaga de 2000, ahora en lecturas. Una flota que envía una lectura por mensaje no nota ningún cambio. Una flota que agrupa varias lecturas en un mensaje queda limitada a 1000 lecturas por segundo por inquilino. Si ese tráfico es legítimo, eleva la tasa de ingesta del nivel del inquilino. La consola muestra ahora la tasa de ingesta en lecturas por segundo.
Con el valor por defecto, los dispositivos de un inquilino se admiten como máximo a 5000 lecturas
por segundo, incluso con un atraso vaciándose y ambos servicios de borde en marcha, lo que queda por
debajo de las 6000 por segundo que se midió que almacena una instalación de alta disponibilidad por
defecto, con event-management en dos pods, en el clúster descrito en
qué permite el valor por defecto. En un clúster más
pequeño, o con más de una réplica de event-sources, baja el valor por defecto.
Para localizar antes de actualizar los inquilinos que envían más de 1000 lecturas por segundo:
- Para dispositivos JSON por MQTT y HTTP, cuenta las mediciones almacenadas por segundo del inquilino durante una hora de mucha actividad.
- Para LwM2M, compara la tasa actual de lecturas del inquilino con su techo, no con 25 veces su techo.
- Para Sparkplug,
rate(devicechain_sparkplugingest_measurements_emitted_total[5m])son las lecturas de los inquilinos de las fuentes de ese pod, ya que cada fuente pertenece a un inquilino. - Un agente de borde vuelve a publicar las lecturas que tenía en búfer tan rápido como puede tras una reconexión. Un sitio que agrupa varias lecturas en un mensaje puede superar el techo de su inquilino mientras se pone al día.
Volver a la versión anterior restaura el cobro por mensaje. No cambian datos almacenados ni el esquema, y no se renombra ninguna clave de configuración.
Un evento lleva como máximo 256 lecturas, y las pasarelas dividen los mensajes mayores
Un evento lleva ahora como máximo 256 lecturas, en todos los transportes, y el límite no es configurable. Una lectura es un valor de métrica de una medición, o una entrada de ubicación o de alerta. Antes, el evento JSON de dispositivo en HTTP y MQTT admitía hasta 1000 por defecto, y un operador podía subir o bajar ese valor.
Para localizar antes de actualizar los dispositivos afectados, establece
maxReadingsPerMessage: 256 en la configuración de event-sources de v0.18.0 y vigila
total_msg_too_many_readings: cada mensaje que cuente es uno que esta versión rechaza. Cambia esos
dispositivos para que envíen como máximo 256 lecturas por mensaje.
- HTTP y MQTT: un mensaje con más de 256 lecturas se rechaza entero, nunca se recorta. HTTP
responde
400, indicando el número de lecturas y el límite. En MQTT no se avisa al dispositivo, porque el bróker confirma antes de decodificar; el rechazo se cuenta entotal_msg_too_many_readingsy el mensaje va al stream de decodificación fallida. Los mensajes capturados antes de la actualización y decodificados después, incluidos los que sigan en la cola de un agente de borde, se evalúan con el límite nuevo. maxReadingsPerMessagequeda retirado. El servicio arranca, registra un aviso y lo ignora; un valor por debajo de 256 tampoco se respeta ya. Elimina la clave.- Sparkplug B: un mensaje con más de 256 valores de métrica se convertía en un solo evento. Ahora se convierte en eventos consecutivos de como máximo 256, cada valor con su propia marca de tiempo. Las consultas que cuentan eventos verán más; las lecturas almacenadas son las mismas. Las reglas ven cada evento por separado, así que una regla de tiempo de retención o de ausencia puede dispararse ahora entre dos eventos de un mismo mensaje ancho.
- LwM2M: un Notify con más de 256 valores numéricos conservaba los 256 primeros y descartaba el
resto. Ahora se almacena como varios eventos, y el presupuesto de muestras del inquilino se cobra
evento a evento; los eventos que no puede admitir se cuentan en
ingest_samples_shed_total. Se eliminanotify_samples_truncated_total: quítala de cualquier panel o alerta.
Se almacena un evento que ya estaba en cola con miles de lecturas
Un evento con más lecturas de las que caben en una sola sentencia de la base de datos (más de
unas 5950 mediciones, 5450 ubicaciones o 6550 alertas, o 9350 anclajes de relación) no se podía
almacenar nunca. El controlador de la base de datos rechazaba la sentencia, event-management
reintentaba el evento hasta agotar sus entregas y después lo registraba en el stream
failed-events como un fallo posterior, no como un problema del propio evento. Antes del
límite de 256 lecturas, un evento así podía venir de un mensaje Sparkplug
con miles de métricas, o de un transporte JSON cuyo maxReadingsPerMessage se hubiera subido por
encima de su valor predeterminado de 1000, y puede que alguno siga esperando en un stream al
actualizar. Ahora el almacén de eventos escribe un evento grande en tantas sentencias como necesite,
dentro de la misma transacción, así que se almacena entero o no se almacena, como cualquier otro, y
una reentrega suya no añade nada. El mismo camino de reintentos y fallo posterior lo seguía un
evento de cambio de estado cuyo identificador de sesión es demasiado grande para la columna de 64
bits con signo de la base de datos; ese evento se registra ahora como inválido en su primera
entrega.
Se rechaza una lectura fechada más de 366 días antes de llegar
El occurredTime de una lectura, en el sobre o en cualquier entrada, puede ser ahora como mucho 366
días anterior al momento en que la plataforma la recibió. Uno más antiguo se rechaza, nunca se mueve
a una hora posterior:
- HTTP y MQTT: se rechaza el mensaje entero, como en cualquier otro rechazo de marca de tiempo.
HTTP responde
400, y una publicación MQTT se envía a la cola de mensajes fallidos. Ambos se cuentan entotal_msg_invalid_event_timeenevent-sources. - Sparkplug y LwM2M: solo se descartan las lecturas demasiado antiguas, y el resto del mensaje
se almacena. El mensaje de una pasarela lo compone la pasarela, no lo envía un dispositivo como una
unidad, y la marca de tiempo de una métrica de Sparkplug es cuándo cambió su valor por última vez,
así que un valor sin cambios durante más de 366 días se descarta de cada nacimiento hasta que
cambie. Un nodo sin reloj configurado, que informa horas cercanas a 1970, ve descartadas todas sus
lecturas. Se cuentan en
samples_too_old_dropped_totalensparkplug-ingesty entelemetry_too_old_dropped_totalenlwm2m-ingest, y el servicio registra un aviso que nombra el dispositivo. - Cualquier otro evento fechado tan atrás se envía a la cola de mensajes fallidos con el motivo
Invaliden su primera entrega, y se cuenta enresolve_event_time_too_old_totalendevice-management.
Antes se aceptaba cualquier hora pasada, y el almacén de eventos guarda una partición (fragmento) por intervalo de fragmento, un día por defecto. Un dispositivo podía crear una partición por cada día hasta el año 1, y toda operación posterior que recorre cada partición, como una actualización que reconstruye una clave, se volvía más lenta con ellas. Cuenta los fragmentos explica qué hace la actualización de esta versión con una tabla que ya tiene demasiados.
- Un dispositivo sin reloj debe omitir
occurredTime; la plataforma fecha entonces la lectura al llegar. La época,1970-01-01T00:00:00Z, se rechaza. - Los mensajes que ya esperan en la plataforma al actualizar se juzgan según la hora en que llegaron, así que la nueva versión rechaza uno fechado más de 366 días antes de esa hora.
- El almacén local del agente de borde no tiene límite de antigüedad propio; consulta el almacén local.
- El límite es fijo. No hay ningún ajuste para él.
Una fuente MQTT sobre tu propio bróker se conecta con su propio identificador de cliente, desde un solo pod a la vez
Una fuente de eventos que lee de un bróker MQTT que tú operas se conectaba con el identificador de
cliente devicechain, fuera cual fuera la instancia, la fuente o el pod. Un bróker mantiene una
sesión por identificador de cliente y corta la conexión más antigua cuando llega una segunda. Así
que, cada vez que dos conexiones usaban ese identificador, cada una le quitaba la sesión a la otra
en bucle, y los mensajes que llegaban mientras cualquiera de las dos se reconectaba se perdían sin
contarse. Ocurría con dos pods de event-sources, en cada actualización progresiva (el pod nuevo
arranca antes de que se detenga el viejo), con dos fuentes así sobre un mismo bróker y con dos
instancias que leían un mismo bróker. La fuente de pasarela de la instalación por defecto lee el
flujo de la plataforma y no se veía afectada.
Ahora cada fuente se conecta como devicechain:<instancia>:<fuente>:<pod>, y solo un pod lee una
fuente dada a la vez. Los pods acuerdan cuál mediante un arrendamiento guardado en el bróker de
mensajería de la plataforma. Los demás no conectan nada para esa fuente hasta que se libera.
Consulta la Matriz de transportes. Lo que
hay que permitir antes en tu bróker está en el paso 12, y la única doble
lectura durante el despliegue, en Durante la actualización.
- Un pod que se detiene cede la fuente a otro pod en unos dos segundos. Tras una pérdida abrupta
(un fallo de nodo, un
SIGKILL, una terminación por falta de memoria), la fuente queda sin leer unos 30 segundos más lo que tarde en reconectarse. La fuente sigue siendo como mucho una vez: tu bróker no conserva lo que entrega en ninguno de esos huecos. - Un pod que toma una fuente y no puede llegar a tu bróker la libera y vuelve a intentarlo cada 15 segundos, en lugar de detener el servicio. Un bróker que rechaza la suscripción sigue deteniendo el servicio.
- Ejecutar más pods de
event-sourcesya no significa más lectores de una fuente externa. Antes tampoco añadía capacidad, porque los pods se quitaban una misma sesión. Ahora añade pods en espera. - Nuevo indicador:
devicechain_eventsources_external_mqtt_owner{source}, que vale 1 en el pod que lee la fuente y 0 en los demás, y un nuevo contador,devicechain_eventsources_total_msg_not_owner{source}, para los mensajes que un pod descarta porque acaba de perder la fuente. Nueva alerta:ExternalMqttSourceNotReadByOnePod(aviso), cuando una fuente lleva dos minutos sin que la lea ningún pod, o leída por más de uno. Consulta Una fuente MQTT externa que nadie lee.
Resolución
device-management responde las búsquedas repetidas desde memoria
- Cada réplica de
device-managementguarda en memoria lo que leyó de sus cachés de clave-valor durante hasta cinco segundos, y responde desde ahí las búsquedas repetidas del mismo dispositivo, tipo de dispositivo o inquilino. Un tiempo de vida de caché menor de cinco segundos también lo acorta. - Un cambio puede tardar hasta cinco segundos más en llegar a los eventos que resuelven las demás réplicas. Un dispositivo borrado, o vuelto a crear con el mismo token, puede seguir resolviéndose a través de su registro anterior en otra réplica durante esos segundos, y una regla cuyo alcance de grupo acaba de cambiar puede evaluarse allí con el alcance anterior. Un flanco de alarma para un dispositivo recién borrado se sigue descartando de inmediato en todas las réplicas.
- Hasta 131 072 entradas o 24 MiB por réplica para cada una de las tres cachés que se guardan
por dispositivo (un dispositivo por token, sus relaciones seguidas, sus pertenencias a grupos);
4096 entradas o 4 MiB para las cachés por tipo de dispositivo y por inquilino.
inMemoryCache.perDeviceCacheEntrieseinMemoryCache.perDeviceCacheMiBcambian el límite; sube con ellos el límite de memoria del servicio. - Un dispositivo que informa con menos frecuencia que cada cinco segundos no se responde desde memoria, por grande que sea la caché.
- Las búsquedas de perfil, relaciones y alcance de grupos de un evento se hacen a la vez, y
también sus búsquedas de pertenencia a grupos, así que un evento que no encuentra nada en memoria
espera una sola ida y vuelta a NATS en lugar de tres. La base de datos se sigue leyendo una
consulta tras otra, así que
resolution.workerssigue contando las conexiones como antes. - Métricas nuevas cuentan las búsquedas respondidas desde memoria, las entradas descartadas, el
tamaño y los límites (
kv_cache_local_max_entries,kv_cache_local_max_bytes);kv_cache_request_duration_seconds{op="get"}cuenta ahora solo las búsquedas que la memoria no pudo responder. Consulta Cachés que dejan de responder.
device-management comprueba desde memoria una credencial de dispositivo repetida
- Cada réplica guarda en memoria, durante hasta cinco segundos, una credencial de dispositivo que
acaba de verificar, y comprueba los siguientes eventos del dispositivo contra esa copia en lugar
de leer la base de datos. La copia se comprueba exactamente igual que la credencial almacenada: la
contraseña de
MQTT_BASICse sigue comparando en cada evento, y una expiración sigue surtiendo efecto en su momento. Una credencial que no se pudo verificar nunca se guarda. - Una revocación puede tardar ahora hasta cinco segundos en una réplica que no recibió el aviso. Deshabilitar, borrar, reasignar o cambiar de cualquier otra forma una credencial, y reemplazar, editar o borrar su dispositivo, descartan la copia en la réplica que hace el cambio antes de responder, y avisan a las demás réplicas para que descarten la suya. Si ese aviso se pierde (una réplica que se reconecta a NATS, o réplicas que aún ejecutan la versión anterior durante la actualización), la copia de esa réplica caduca en cinco segundos como máximo. Hasta ahora una revocación surtía efecto en el siguiente evento en todas las réplicas. Consulta Cuánto tarda en aplicarse una revocación.
- Las conexiones MQTT siguen leyendo la base de datos cada vez, así que una credencial revocada no puede abrir una conexión nueva en ninguna réplica.
- Como máximo 65 536 credenciales o 16 MiB por réplica, un límite fijo; con los valores por defecto las cachés en memoria del servicio guardan como máximo 96 MiB en lugar de 80. Métricas nuevas cuentan las comprobaciones respondidas desde la copia, las credenciales descartadas, su tamaño y los avisos de descarte enviados y recibidos.
Almacenamiento
Las claves del almacén de eventos empiezan por el tiempo, y desaparecen dieciséis índices
Las claves que impiden almacenar un evento dos veces empiezan ahora por el inquilino y el instante
del evento, en lugar del inquilino y un resumen criptográfico del evento, así que la clave de un
evento nuevo queda junto a la anterior y la base de datos reescribe muchas menos páginas de índice.
Se eliminan dieciséis índices: doce que repetían otro índice o que ninguna consulta leía, y los
índices por inquilino y tiempo de los eventos base, las mediciones, las ubicaciones y las alertas,
a los que ahora responden las claves nuevas. Cada fila almacenada actualiza menos índices que en
v0.18.0: una fila de evento base, dos en lugar de cinco (tres en lugar de seis con un id
alternativo); una fila de medición, tres en lugar de cinco; una fila de ubicación o de alerta, uno
en lugar de cuatro; una fila de anclaje de relación, dos en lugar de cuatro; y una fila de cambio de
presencia, uno en lugar de cuatro. Un evento de medición con una lectura y sin anclajes actualiza
cinco índices en lugar de diez.
Si event-management se negó a arrancar porque una tabla tiene más de 500 fragmentos, el
procedimiento de drop_chunks por lotes al que remite su registro está en
Cuenta los fragmentos.
- Los eventos se siguen almacenando una sola vez, y toda lectura que sirve
event-managementsigue usando un índice. - La lista de eventos de un dispositivo trabaja más sobre los datos recientes. El total que acompaña a la lista, y una lista filtrada por tipo de evento, recorren ahora todas las filas del dispositivo que aún no están comprimidas (la última semana, por defecto), incluidas las de un dispositivo con el mismo token en otro inquilino. Cuando el almacén tiene pocos tokens de dispositivo distintos (en nuestras pruebas, 40 en lugar de 100), la primera página de un dispositivo que lleva un tiempo en silencio mientras los demás siguen enviando puede tardar bastante más.
- Acceso por SQL y BI. Una consulta sobre los eventos, lecturas, ubicaciones, alertas o anclajes
de un inquilino que filtra solo por tiempo usa un índice. Una consulta solo por tiempo sobre
analytics.state_change_eventslee todas las filas de tu inquilino en cada fragmento sin comprimir que toca; añadedevice_tokenpara usar un índice. Para uniranalytics.event_anchorsconanalytics.events, une porevent_idy poroccurred_time. Consulta Notas prácticas.
Los lotes se escriben en menos sentencias
event-managementescribe cada lote con una sentencia por tabla para cada inquilino que contiene, dentro de la misma transacción, en lugar de varias por evento. Lo que se almacena no cambia, un evento se sigue reconociendo solo después de que su lote se confirme, y un evento reentregado no añade nada. Cuando la base de datos rechaza una fila de una sentencia que lleva varios eventos, el lote se vuelve a escribir evento a evento para encontrarlo;persist_batch_fallbacks_totallo cuenta. Los eventos de conexión y desconexión se siguen escribiendo uno a uno. Consulta Persistencia de eventos.device-stateescribe el estado de todos los dispositivos que ya tienen uno en una sola sentencia por cada inquilino del lote. Un dispositivo que aparece por primera vez se sigue creando por separado, yupdatedAtsigue avanzando cada vez que se escribe su estado.- Cada conexión del pool a la base de datos sigue abierta entre usos, hasta el tamaño del pool;
antes solo lo hacía la mitad, y un servicio que usaba más de la mitad se reconectaba en cada
consulta. Una conexión se sigue cerrando una hora después de abrirse. La base de datos puede
mostrar más conexiones inactivas de cada servicio después de un periodo de mucha carga; con la
configuración predeterminada lo cubre el
presupuesto de conexiones, pero si ejecutas un servicio con
replicaspor encima de 1 o subiste sumaxOpenConnections, comprueba el límite de conexiones de la instancia.maxIdleConnectionsse sigue respetando cuando se fija.
Nuevos valores por defecto del flujo de eventos
| Ajuste | v0.18.0 | v0.19.0 |
|---|---|---|
event-management persistence.writers | 5 | 10 |
event-management persistence.maxBatch | 32 | 64 |
event-management persistence.lingerMillis | 0 | 10 |
device-state projection.writers | 5 | 10 |
Límite de CPU de event-processing | 500m | 1 núcleo |
Pods de event-management con --ha (no con --compact --ha) | 1 | 2 |
Una instalación que fija alguno de estos valores conserva el suyo; persistence.lingerMillis: 0
sigue significando sin espera. Un escritor que encuentra menos eventos que un lote completo espera
ahora hasta 10 milisegundos a que lleguen más, así que por debajo de unos pocos cientos de eventos
por segundo por pod cada evento se confirma hasta 10 milisegundos más tarde que antes y
persist_duration_seconds sube más o menos eso; con cola acumulada no cambia nada. En la prueba de
rendimiento de la versión a 3000 eventos por segundo, las transacciones por evento almacenado
bajaron un 37% (de 0,126 a 0,080), el tiempo mediano para almacenar un evento subió unos 5
milisegundos y el 1% más lento bajó un 18%. Esa comparación también cambió las claves del almacén
de eventos y la compresión de su archivo, las solicitudes de CPU de los servicios y su colocación, los escritores de device-state y el límite de CPU de la detección, y las solicitudes y el límite de memoria de los servidores NATS, así que no aísla la espera. En una ejecución de cinco
minutos con 6800 eventos por segundo ofrecidos, el camino que confirma un evento cada vez se llevó
el 4,8% de la CPU de event-management, con 0,050 transacciones por evento, frente al 24% en la
prueba anterior, con la compilación previa a la espera y en otros nodos de servicios. Las
instalaciones con --compact reciben los mismos valores salvo el segundo pod; sus solicitudes no
cambian.
Dos pods de event-management con --ha. En un grupo de servicios de tres nodos, uno de ellos
ejecuta también el servidor NATS que lidera el stream de eventos entrantes. En la prueba de
rendimiento de la versión, el planificador puso allí el único pod de event-management junto con
device-state; ese nodo funcionó al 94-95% de CPU y, con un solo pod, el almacenamiento bajó a 6592
eventos por segundo con 6800 ofrecidos, la primera etapa en quedarse atrás. Un segundo pod permite
almacenar usando la CPU de otro nodo. La cifra de Rendimiento se midió con dos pods. Los pods prefieren nodos distintos, pero es una preferencia, no
una garantía. Cada uno llena sus propios lotes, así que el almacén de eventos confirma más o menos
el doble de transacciones por evento; su nodo se mantuvo por debajo del 70% de CPU. Si instalas el
chart por tu cuenta, su valor predeterminado sigue siendo una réplica: establece
functionalAreas.event-management.replicas: 2 y, en un almacén de eventos creado con el OpenTofu
de este repositorio, establece event_management_replicas = 2, que reserva las conexiones del
segundo pod.
Dimensionamiento y ubicación
Los servicios de la ruta de eventos solicitan lo que usan y se reparten entre los nodos
Los cinco servicios que procesan cada evento solicitan la CPU que se midió que usan a 6000 eventos
por segundo (los valores están en Haz sitio); antes, con 100m cada uno, solicitaban
mucho menos que eso, y el planificador, que ubica los pods según sus solicitudes, juntaba a los más
ocupados. event-sources y device-state pueden usar ahora hasta 2 núcleos, como
device-management y event-management. Los cinco prefieren nodos distintos (con tres nodos,
normalmente no más de dos por nodo); functionalAreas.<servicio>.eventPathSpread: false lo
desactiva para un servicio. device-management, event-management y event-sources prefieren
además un nodo que no ejecute el primario del almacén de eventos. Una preferencia solo se aplica
cuando se planifica un pod: tras una conmutación por error, un pod que ya se ejecuta en el nodo del
nuevo primario se queda hasta que se vuelva a planificar.
El almacén de eventos de una instancia nueva recibe un volumen de 32Gi en lugar de 8Gi. Sus
servidores NATS solicitan 500m de CPU y 768Mi de memoria y tienen un límite de 2Gi; antes no
solicitaban nada, lo que los convertía en los primeros pods desalojados cuando un nodo se quedaba
sin memoria. El límite de 2Gi se dimensionó con ingesta sostenida; no se midió un servidor
poniéndose al día tras un reinicio, así que si uno queda OOMKilled durante la actualización, sube
nats_memory_limit en el terraform.tfvars de la instancia y vuelve a ejecutar dcctl upgrade.
Consulta Dimensionamiento de los servicios y
El intermediario de mensajes.
Un servicio que ejecuta más de un pod mantiene sus pods en nodos distintos
Con más de una réplica, cada uno de los cinco servicios de la ruta de eventos (device-management,
event-management, device-state, event-sources y event-processing) prefiere un nodo que no
ejecute ya otro de sus propios pods. Un servicio con el reparto de la ruta de
eventos deja de recibir el reparto predeterminado del clúster, y esto le
devuelve la mitad que mantiene las réplicas de un servicio en nodos distintos; la otra mitad, zonas
distintas, no se devuelve. Es una preferencia que el planificador pondera junto con las demás del
servicio (menos servicios de la ruta de eventos por nodo y, para device-management,
event-sources y event-management, sin la primaria del almacén de eventos), no una garantía.
- Con
--ha, se aplica a los dos pods deevent-management. - Si instalas el chart por tu cuenta, se aplica a cualquiera de los cinco que ejecutes con
replicaspor encima de uno, y figura en la antiafinidad del pod, junto a la preferencia por evitar la primaria del almacén de eventos en los tres servicios que la tienen. Un servicio coneventPathSpread: falseconserva en su lugar el reparto predeterminado del clúster, que también prefiere zonas distintas, y no recibe esta preferencia.
Tus propios valores del chart
Si instalas el chart tú mismo:
- Un
resources.requests.cpude nivel superior ya no llega adevice-management,event-management,device-state,event-sourcesnievent-processing: su solicitud medida prevalece. Define las suyas enfunctionalAreas.<servicio>.resources.requests, o estableceuseMeasuredRequests: falsepara que las solicitudes de nivel superior vuelvan a aplicarse a todos los servicios. ConuseMeasuredRequests: false, una solicitud de nivel superior por encima de 1 núcleo hace fallar ahora la generación paraevent-processing, porque supera el límite propio de ese servicio. - Un
resources.limits.cpude nivel superior por encima de 2 núcleos baja ahoraevent-sourcesydevice-statea 2, y ya no llega aevent-processing, que tiene siempre 1 núcleo salvo que se defina enfunctionalAreas.event-processing.resources.limits. - Un límite de CPU propio de un servicio por debajo de su solicitud medida se rechaza ahora,
nombrando
measuredRequests. - Los valores de nivel superior del chart y el bloque de cada servicio en
functionalAreasrechazan ahora una clave que el chart no lee, así que una clave mal escrita hace fallar la generación. eventPathSpreadestá activado en los cinco servicios; activarlo en otro servicio lo reparte junto con ellos.- Un servicio que ejecutes con
replicaspor encima de uno prefiere nodos que no ejecuten otro de sus propios pods (Pods propios separados).
Las primarias de las bases de datos prefieren nodos distintos
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, el nodo que ejecutaba las dos primarias estaba al 94-98% de CPU mientras los otros dos estaban al 45-51%. Es una preferencia: un clúster más pequeño sigue planificando todas las instancias de base de datos. Actúa cuando se planifica un pod de base de datos, así que una conmutación por error o un traspaso todavía pueden dejar las dos primarias en un mismo nodo; consulta Dónde se ejecutan las primarias de las bases de datos. Una cuota que limita la ubicación entre espacios de nombres puede rechazar estos pods: consulta el paso 5.
Las bases de datos pueden ejecutarse en los nodos que elijas
dcctl install admite --database-node-selector y --database-toleration. Colocan el almacén
relacional compartido y el almacén de eventos de cada instancia en nodos con una etiqueta, incluidos
nodos con un taint que mantiene fuera otras cargas. dcctl bootstrap no tiene esos flags: cada
instancia sigue a la instalación. Antes de instalar nada, install rechaza una ubicación con menos
nodos utilizables que instancias tiene una base de datos, y cada bootstrap vuelve a comprobarlo.
NATS, los servicios y el almacén de objetos de respaldo no se colocan. Añadir una ubicación a un
clúster que ya ejecuta instancias se rechaza; sin ninguna en marcha, mueve el almacén relacional,
cuyos volúmenes quizá no puedan seguirlo. Consulta
Ubicación de las bases de datos.
Respaldos
dcctl destroy elimina los respaldos internos de una instancia
Cuando el espacio de nombres de la instancia ya no existe, destroy borra todo lo que hay bajo la
ruta a la que archivaba su almacén de eventos en el almacén de objetos propio del clúster, y
comprueba que la ruta ha quedado vacía. Lee y muestra esa ruta antes de cambiar nada. Los respaldos
en un almacén de objetos que tú proporcionaste no se borran nunca: destroy indica dónde están. Pasa
--keep-backups para conservar también los respaldos internos, y pásalo sin falta antes de
reconstruir una instancia a partir de sus propios respaldos con --restore-tsdb-from, porque un
destroy sin esa opción borra el archivo que lee esa restauración. Si no se puede acceder al almacén,
destroy termina igualmente e indica qué dejó. Un archivo que destroy no escribió en esta ejecución
(uno que dejó una versión anterior, o uno conservado con --keep-backups y del que después se
restauró) se lista, no se elimina;
Qué pasa con los respaldos de la instancia explica cómo
eliminarlo. Destroy ya no muestra la lista Changes to Outputs de OpenTofu, cuyos valores eran
los predeterminados de la configuración y no los de la instancia.
Los respaldos que dejan de enviarse avisan antes
PostgresWALArchiveBacklog se dispara cuando una base de datos retiene registro de escritura
anticipada sin enviar, también cuando el archivador es lento o está bloqueado.
BackupDestinationFillingFast y DatabaseVolumeFillingFast se disparan según lo rápido que se
llena el almacén de respaldos o un volumen del almacén de eventos. Consulta
Respaldos que dejan de enviarse.
El almacén de respaldos se dimensiona para que el almacén de eventos se llene primero
El almacén de respaldos interno de un clúster nuevo tiene 160 GiB en lugar de 20 GiB, y
20 GiB en lugar de 8 GiB con --compact cuando se mantiene TLS. Con 20 GiB, una ingesta
sostenida llenaba el almacén tras entre 12 y 16 millones de eventos, mucho antes que un almacén de
eventos de 32 GiB; entonces el archivado se detenía y el primario del almacén de eventos llenaba su
propio volumen. El dimensionamiento supone hasta unos 1,9 KB de archivo por evento para las dos
bases de datos, medidos antes de que esta versión comprimiera el registro de escritura anticipada,
cambiara las claves del almacén de eventos y archivara con zstd, así que peca por exceso: en la
prueba de rendimiento de la versión, el archivo del almacén de eventos ocupó por sí solo unos
0,47 KB por evento almacenado. Un clúster existente conserva el tamaño de su almacén: el volumen
se dimensiona solo al crearlo, y backup_object_store_storage en un almacén existente no hace nada.
Para dar el nuevo tamaño a un clúster existente, amplía el volumen en una StorageClass que lo
permita; consulta Tamaño del almacén de objetos de respaldo.
Cada base de datos conserva su propia ventana de recuperación: 30 días para los datos de núcleo, 7 para los de eventos
backup_retention se sustituye por backup_retention_rdb en la configuración del clúster (por
defecto 30d) y backup_retention_tsdb en la de la instancia (por defecto 7d). La base de datos
relacional, que guarda inquilinos, usuarios, dispositivos, reglas, secretos y el último estado
conocido, se puede recuperar ahora a cualquier punto de los últimos 30 días en lugar de 7, y los
datos de núcleo de un inquilino eliminado siguen siendo restaurables durante 30 días
(Qué se conserva deliberadamente). Una ventana es un número entero
y d, w o m; cualquier otra forma se rechaza antes de la aplicación. Cuando una ventana de
retención limita el almacén de eventos, el ritmo de ingesta sostenido que llena un almacén de 20 GiB
baja de unos 19 a unos 13 eventos por segundo, y el del almacén de 160 GiB de unos 150 a unos 100.
Esos ritmos salen del coste por evento medido antes de la compresión del registro, las claves nuevas
y zstd de esta versión, así que en ese aspecto pecan por defecto. También usan la parte del registro
de la base de datos relacional medida una sola vez, aproximadamente un 14 % para una flota pequeña
que informa con frecuencia; para flotas más grandes o más lentas probablemente es mayor, y si el
registro relacional fuera todo, la cifra de 160 GiB sería de unos 35. Para conservar 7 días,
establece backup_retention_rdb = "7d" si aplicas tú mismo la configuración de OpenTofu;
dcctl install no tiene ninguna opción para ello. Consulta
Ventanas de recuperación.
Los respaldos base de las bases de datos pueden ser 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 en lugar de una copia completa en el almacén de respaldos. No cambia nada si
no pasas la opción. Consulta
Respaldos base como instantáneas de volumen.
- El archivado del registro no cambia, y un respaldo base completo sigue yendo al almacén de respaldos cada domingo a las 04:00. Toda restauración lee el almacén, no las instantáneas, así que puede reproducir hasta una semana de registro.
- Las instantáneas se guardan en tu proveedor de nube y sobreviven al clúster. Respaldos base como instantáneas de volumen explica qué comprobar antes y después de borrar una.
- La clase tiene que existir, tener
deletionPolicy: Deletey pertenecer al controlador que aprovisiona los volúmenes de las bases de datos; install y cada bootstrap comprueban las tres cosas primero. GKE y AKS incluyen un controlador de instantáneas; en EKS, instala antes el complemento del controlador de instantáneas. - El operador de DeviceChain poda las instantáneas antiguas cada diez minutos, y conserva todas las
que están dentro de la ventana de recuperación y la más reciente anterior a ella. Su ClusterRole
gana, en todos los espacios de nombres:
getsobre namespaces;get,listydeletesobre los Backups de CloudNativePG;get,listypatchsobre los ScheduledBackups de CloudNativePG; ycreateypatchsobre los Events deevents.k8s.io. Solo actúa en espacios de nombres que creó DeviceChain, sobre las programaciones que genera su propia configuración, y registra cada pasada en la anotacióndevicechain.io/snapshot-retention-checked-at. - El almacén de respaldos guarda hasta una semana más de registro de cada base de datos, así que se llena antes: con las ventanas y el almacén predeterminados, a unos 60 eventos por segundo de ingesta sostenida en lugar de unos 100 (de la misma medición, anterior a la compresión, así que conservadora).
PostgresNoRecentSnapshotBackup,DatabaseSnapshotPruningStalledyDatabaseSnapshotBackupsUnobservedson nuevas, y en un clúster asíPostgresNoRecentBaseBackupespera 8,5 días en lugar de 36 horas.- El ajuste pertenece al clúster; cambiarlo se rechaza mientras haya instancias en él.
El registro de escritura anticipada se comprime, y se archiva con zstd
Los almacenes de eventos nuevos y los actualizados comprimen las imágenes de página de su registro
de escritura anticipada (wal_compression = lz4), algo que se recarga sin reinicio. Las dos bases de
datos archivan ahora su registro con zstd en lugar de gzip; los respaldos base siguen en gzip. Las
restauraciones y la caducidad de los respaldos leen ambos formatos, según el nombre de cada segmento
(.gz o .zst), así que un archivo que cambia de compresión a mitad se restaura como antes; la
prueba de actualización de la versión restauró un archivo así y recuperó todos los eventos. En la
prueba de rendimiento de la versión a 3000 eventos por segundo, el cambio de claves de esta versión
y el archivado con zstd redujeron juntos un 22% el registro por evento almacenado del almacén de
eventos y un 53% la CPU de su archivador, frente a la compilación de desarrollo anterior a ellos.
Consulta Volumen del almacén de eventos.
Operaciones
dcctl upgrade aplica los ajustes del bróker de mensajería y del almacén de eventos de una instancia
dcctl upgrade aplica ahora los servidores NATS y el almacén de eventos de la instancia desde la
configuración de OpenTofu que trae esta versión, antes de mover los servicios. Hasta ahora solo los
aplicaba dcctl bootstrap, así que una instancia actualizada conservaba sus ajustes antiguos del
bróker y del almacén de eventos sin decirlo. Esta actualización da a una instancia creada con
v0.18.0 las solicitudes y el límite de NATS, el registro de escritura anticipada comprimido, el
archivado con zstd y la preferencia de ubicación de las primarias; en una instancia así, las
instancias del almacén de eventos pueden reiniciarse una vez por la preferencia de ubicación. Los
tamaños de los volúmenes se conservan. Consulta
Qué aplica una actualización a la infraestructura. La configuración de
OpenTofu de la instancia declara ahora que necesita OpenTofu o Terraform 1.9 o posterior.
Volver a ejecutar dcctl bootstrap termina una instancia cuyo primer arranque inicial falló al final
Un arranque inicial que fallaba después de escribir el documento de configuración de la
instancia (mientras instalaba el chart, o mientras esperaba a que los servicios estuvieran
listos) dejaba una instancia sobre la que dcctl bootstrap se negaba a volver a ejecutarse,
como si ya estuviera en marcha.
Ahora, cuando dcctl bootstrap declara una instancia que está construyendo, deja anotado en la
declaración que el primer arranque inicial no ha terminado (la anotación
core.devicechain.io/bootstrap-unfinished). Un arranque inicial o una actualización que termina
con éxito la retira, en la misma escritura que registra la instancia como Ready. Hasta
entonces, volver a ejecutar el mismo dcctl bootstrap termina la instancia. Reutiliza la clave
raíz, las credenciales del bróker y las contraseñas de las bases de datos que dejó en el clúster
la ejecución anterior, y muestra la contraseña generada del superusuario, a la que la ejecución
fallida nunca llegó. Una vez retirada la anotación, una nueva ejecución se rechaza como antes.
- Si una ejecución termina con éxito pero no puede retirar la anotación (la escritura se rechaza, o el bloqueo del clúster se reclamó antes), sale con un error que indica que vuelvas a ejecutar el mismo comando, en lugar de informar de un éxito sobre una instancia sobre la que un arranque inicial posterior se ejecutaría.
- Una instancia cuyo primer arranque inicial empezó con una versión anterior no tiene esa
anotación, así que se trata como en marcha, igual que antes; la negativa nombra
dcctl upgradeydcctl destroy. Consulta Terminar un bootstrap que falló a mitad de camino. - Actualiza todas las copias de
dcctlque uses antes de ejecutarlas sobre una instancia construida con esta versión. Undcctlanterior no retira la anotación, así que una actualización posterior con él que falle puede dejarla en vigor sobre una instancia en marcha, y undcctl bootstrapsin más se ejecutaría entonces sobre esa instancia.
La detección reintenta liberar su partición cuando el bróker no responde
Cuando el pod del motor de detección se detiene, libera su partición para que el siguiente pod empiece a detectar enseguida. Hasta ahora, si el bróker no respondía a esa liberación, por ejemplo porque un servidor del bróker se estaba reiniciando en ese momento, el pod se rendía tras un intento. El siguiente pod esperaba entonces a que la partición expirara, hasta 30 segundos, y un periodo de traspaso adicional de 20 segundos antes de detectar nada. Ahora el pod reintenta la liberación hasta que el bróker responde, mientras se lo permita su tiempo de apagado, y reserva lo suficiente para un tiempo de espera más del bróker y para terminar de detenerse. Una renovación o una liberación cuya respuesta se perdió mientras un servidor del bróker se reiniciaba tampoco le cuesta ya la partición al motor. El reintento lo hace el pod que se detiene, así que tiene efecto a partir de la actualización siguiente a la que instala esta versión: durante esa actualización, el pod que se sustituye todavía ejecuta la versión anterior y hace un solo intento.
Lo que todavía pausa la detección:
- Sin
--ha, una actualización que reinicia el bróker detiene la detección durante todo el reinicio, normalmente alrededor de un minuto (consulta la entrada del bróker y del almacén de eventos). El motor no puede conservar su partición durante una interrupción del bróker de más de 30 segundos, y nada registra que se detuvo limpiamente, así que, cuando el bróker vuelve, espera el periodo de traspaso de 20 segundos, más hasta 5 segundos antes de reintentar, y después reproduce el flujo desde su último punto de control, como tras cualquier reinicio. El despliegue gradual de los servicios que sigue mueve luego la partición al pod nuevo, que reproduce el flujo una vez más pero no espera. - Un pod que se detiene mientras el bróker sigue inaccesible más allá de su apagado todavía deja que su partición expire, y el siguiente pod espera hasta 30 segundos más el periodo de traspaso.
Con --ha el bróker sigue disponible mientras sus servidores se reinician de uno en uno. El motor
solo pierde su partición si sus renovaciones fallan durante 30 segundos.
Alertas
-
Nuevas:
JetStreamUnreadBacklogNearFull(warning) yJetStreamIngestBackpressureEngaged(critical) para la contrapresión, que también se dispara por el rechazo por historial;JetStreamDurableUnreadNearFull(warning), cuando un consumidor lleva 5 minutos sin haber leído más del 80% de lo que cabe en su stream; las tres alertas de respaldo; las tres alertas de instantáneas;ExternalMqttSourceNotReadByOnePod(warning) para una fuente MQTT sobre tu propio bróker;BrokerConnectionDiedSilently(warning) para una conexión con el bróker que murió sin cerrarse; yInstanceContainerRestarted(warning) para un contenedor que Kubernetes reinició. -
Cambia:
JetStreamStreamNearFullpasa ainfo, solo se dispara para streams que guardan registros para un operador (failed-decode,failed-events,connector-dispatch.dead,max-deliveries, ydead-lettersmientrasuser-managementno informe de que lo lee), y tiene en cuenta el límite de mensajes de un stream además del de bytes. La configuración predeterminada de Alertmanager de kube-prometheus-stack no entrega las alertasinfo. Consulta Mensajes que un consumidor nunca leyó. -
Series nuevas:
devicechain_<area>_jetstream_consumer_unread_ratio{stream, durable}ydevicechain_<area>_jetstream_stream_sink{stream}, más las que nombran los apartados de arriba, entre ellas, con sus nombres completos:devicechain_<area>_jetstream_backpressure_history_runway_seconds{stream, durable}(el rechazo por historial);devicechain_eventsources_total_msg_reading_limitedydevicechain_eventsources_total_readings_rate_limited(Lecturas, no mensajes);devicechain_sparkplugingest_samples_too_old_dropped_total,devicechain_lwm2mingest_telemetry_too_old_dropped_totalydevicechain_devicemanagement_resolve_event_time_too_old_total(Lecturas antiguas);devicechain_eventsources_external_mqtt_owner{source}ydevicechain_eventsources_total_msg_not_owner{source}(Identificador de cliente MQTT);devicechain_<area>_nats_connection_dead_total{detected_by}ydevicechain_<area>_nats_connection_dead_last_timestamp_seconds(Un servidor del bróker que dejó de responder).
Se elimina:
devicechain_lwm2mingest_notify_samples_truncated_total.
Cualquier servicio puede servir perfiles del runtime de Go, desactivado por defecto
functionalAreas.<service>.profiler.enabled: true sirve perfiles de CPU, memoria, asignaciones,
goroutines y traza de ejecución de ese servicio, en un listener propio en la dirección de loopback
del pod, al que se accede con kubectl port-forward. Nunca es un puerto del contenedor, un puerto
del Service ni una ruta del ingress. Solo se reinician los pods de ese servicio. Consulta
Perfilar un servicio.
Una configuración de Google Kubernetes Engine (nueva)
deploy/gke es nuevo: una configuración de OpenTofu que crea un clúster de GKE para DeviceChain,
con un grupo database con taint de tres nodos de 4 vCPU y 16 GB y un grupo services de tres
nodos de 4 vCPU y 8 GB, todos arrancando desde un disco persistente estándar. Instala con
--database-node-selector y --database-toleration para colocar las bases de datos en el grupo
database. Una instalación --ha predeterminada con una instancia cabe en la cuota de SSD de un
proyecto nuevo de Google Cloud; para más instancias, o para un clúster creado de otra forma en
cualquier nube, comprueba antes la cuota de disco: Prerrequisitos
indica los tamaños de los volúmenes y la guía de deploy/gke, la cuota que hay que solicitar.
Los informes de las pruebas de carga cuentan los eventos rechazados
El simulador y el arnés de las pruebas de carga cuentan ahora por separado las dos formas en que la ingesta rechaza un evento:
shedes un429, rechazado en el techo de ingesta del inquilino.backpressuredes un503conRetry-After, rechazado porque la plataforma va retrasada. Este rechazo afecta a todos los inquilinos.
Un 503 sin Retry-After se sigue contando como failed. Dónde aparecen los recuentos:
- El
GET /statusdel simulador muestraemitted,shed,backpressuredyfailed. - La sección
drivede los informes L1, de monitorización, de detección, de comandos, de presencia y de lotes muestraaccepted,shed,backpressuredyfailed. - El informe de contención muestra los cuatro para cada inquilino de prueba.
- El informe de autocomprobación sigue mostrando solo
accepted.
Qué cambia:
- Antes, la mayoría de los informes de las pruebas de carga mostraban siempre
shed 0, por muchos eventos que rechazara la ingesta. Ahora muestran el recuento real. - El
stats.sheddel simulador cuenta ahora solo los429. Antes incluía los503. - La prueba de contención falla una ejecución en la que la contrapresión rechazó al inquilino que nunca se descarta, o a cualquiera de los dos inquilinos cuando no hay un suelo de contención. Esa ejecución no dice nada sobre la prioridad de descarte. Antes, la prueba la informaba como un descarte del inquilino que nunca se descarta. Con un suelo, también podía aprobar una ejecución en la que el suelo no rechazó nada.
No hay nada que hacer al actualizar.
Los servicios detectan en 40 segundos un servidor del bróker que dejó de responder
Cuando la máquina de un servidor del bróker se detenía de golpe (un nodo reiniciado o que perdió la
alimentación), los servicios conectados a ese servidor podían tardar hasta seis minutos en darse
cuenta, porque nada cerraba sus conexiones. Si uno de ellos era event-sources, la ingesta HTTP se
detenía durante ese tiempo, y si la máquina volvía pronto Kubernetes no registraba nada. En las
pruebas, un reinicio forzado del nodo que tenía la conexión de event-sources con el bróker detuvo
la ingesta durante unos cinco minutos y medio.
- Ahora cada servicio envía un ping a su servidor del bróker cada 10 segundos y da la conexión por
perdida tras tres intervalos sin respuesta, o cuando una escritura lleva 10 segundos sin avanzar,
así que una conexión muerta se detecta en 40 segundos como mucho; después el servicio se reconecta
a un servidor que responde. La segunda conexión con el bróker que mantiene
event-sources, para la presencia de los dispositivos, tiene el mismo límite. - Nuevo contador
devicechain_<area>_nats_connection_dead_total{detected_by}y nuevo indicadordevicechain_<area>_nats_connection_dead_last_timestamp_seconds, y una nueva alerta,BrokerConnectionDiedSilently(warning). Consulta Una conexión con el broker que murió en silencio. - El procedimiento de
JetStreamIngestBackpressureEngagedcubre ahora un servicio que escribe y no llega al bróker, y la descripción de la pérdida de un nodo indica cuánto tarda en reconectarse un servicio conectado al servidor perdido.
No hay nada que hacer al actualizar.
Una perturbación del bróker minutos después de otra ya no reinicia servicios, y un reinicio dispara una alerta
Un servicio termina por sí mismo, para que Kubernetes lo reinicie, cuando uno de sus bucles de
lectura de mensajes sigue fallando (consulta
Los bucles de lectura que siguen fallando ahora reinician su servicio). Un
bucle sobre un flujo tranquilo, que puede pasar mucho tiempo sin recibir un mensaje, contaba esos
dos minutos desde su primer fallo y solo volvía a empezar cuando llegaba un mensaje. Dos
perturbaciones del bróker separadas por minutos u horas, como el reinicio de un servidor del bróker
y un cambio posterior del líder de un consumidor, contaban por tanto como un único fallo de más de
dos minutos, y el servicio terminaba en la segunda. Detener a la vez dos de los tres servidores del
bróker reiniciaba así varios servicios juntos, entre ellos event-sources, lo que alargaba el
corte de la ingesta.
- Ahora un bucle vuelve a empezar la cuenta siempre que el bróker le haya respondido desde su último fallo: una entrega, la comprobación que hace un bucle tranquilo cada pocos segundos de que su consumidor sigue existiendo, una nueva vinculación a su consumidor o una reconexión. Un bucle cuyas lecturas siguen fallando durante dos minutos sin ninguna respuesta del bróker entre medias sigue terminando el proceso, como antes.
- Una caída del bróker de más de dos minutos ya no reinicia todos los servicios en el momento en que el bróker vuelve. La reconexión cuenta como respuesta del bróker.
- Una lectura vacía no cuenta como respuesta, porque el cliente informa de una incluso mientras está desconectado. Un segundo fallo pocos segundos después de que vuelva el bróker todavía puede contarse junto con el primero.
- Un fallo que se repite mientras el bróker responde entre medias, como un líder de consumidor que cambia cada minuto, se reintenta y se registra cada vez, pero ya no reinicia el servicio.
- Nueva alerta:
InstanceContainerRestarted(aviso), por cada contenedor del namespace de la instancia que Kubernetes haya reiniciado en los últimos 15 minutos. Hasta ahora nada informaba de un reinicio aislado:KubePodCrashLooping, de la pila de monitoreo, solo se dispara para un pod que sigue reiniciándose a los 15 minutos. La alerta lee kube-state-metrics, que instala la pila de monitoreo incluida. Consulta Un contenedor que se reinició.
No hay nada que hacer al actualizar.
Los informes de las pruebas de carga comprueban cada evento, no solo los totales
Las pruebas de carga L1 y de contención comprueban ahora cada evento que enviaron, no solo que el número de eventos almacenados sea igual al de aceptados. Unos totales iguales pueden ocultar un evento perdido compensado por un duplicado o por un evento que no debería estar.
Cada evento se identifica por su dispositivo y por el occurredTime con el que lo envió el
simulador, que ahora lo escribe al microsegundo, la precisión que guarda el almacén de eventos. Tras
el intervalo de asentamiento de la ejecución, el arnés vuelve a leer los eventos almacenados de cada
dispositivo con la consulta events del inquilino y comprueba que:
- cada evento aceptado está almacenado exactamente una vez;
- ningún evento rechazado está almacenado: un
429, un503conRetry-Afteru otro4xx; - un evento de resultado desconocido está almacenado como mucho una vez: un tiempo de espera
agotado o una conexión cortada, un
503sinRetry-Afteru otro5xx; - no hay nada más almacenado en la ventana de la ejecución.
Dónde aparece:
- El informe L1 tiene una sección
identityconmissing,duplicateKeys,unexpected,refusedStored,ambiguousStoredyambiguousAbsent, hasta diez eventos de ejemplo de cada uno, cuánto tiempo después de la ejecución miró la comprobación por última vez (observedUntilAfterDriveSeconds) y un veredictoreconciled. Una nueva comprobación,ingest-identity, falla la ejecución cuando alguno de los tres primeros no es cero, o cuando las pruebas no permiten decidir, por ejemplo porque seguían llegando eventos mientras se leían. Una petición de relectura que falla se reintenta dos veces, con dos segundos entre intentos; si sigue fallando,ingest-identityfalla como no concluyente con el error anotado en el informe, y el informe se escribe igualmente. Un informe sin esta sección ya no se aprueba. - El informe de contención tiene la misma sección para cada inquilino de prueba, comprobada como
gold-identityyshed-identity. - La autocomprobación ahora también elimina un evento almacenado e inserta un duplicado de otro, de modo que los totales vuelven a coincidir, y solo se aprueba si la comprobación de los totales sigue aprobándose y la comprobación de identidad nombra los dos eventos.
- La comprobación
ingest-completenesssigue comparando los totales, y su mensaje dice ahora que es lo único que compara.
Una copia reentregada de un evento se almacena al menos un minuto después de la primera: el broker
reenvía un evento 60 segundos después de una entrega cuyo acuse de recibo se perdió, la copia aún
tiene que procesarse, y una copia cuyo propio acuse se pierde se reenvía de nuevo. Un minuto es, por
tanto, lo mínimo que puede tardar una copia, no lo máximo. Una ejecución cuyo resultado se vaya a
publicar debe asentarse con margen por encima de ese tiempo, por ejemplo --quiesce-settle 90s.
El simulador no envía altId. Un altId haría que el almacén de eventos descartara una segunda
copia antes de almacenarla, lo que ocultaría los duplicados que busca esta comprobación y cambiaría
el trabajo que se mide.
No hay nada que hacer al actualizar.
Rendimiento
En Google Kubernetes Engine, en tres nodos de base de datos de 4 vCPU y 16 GB y tres nodos de servicios de 4 vCPU y 8 GB, una instalación HA predeterminada aceptó 6000 eventos por segundo durante 10 minutos, dos veces, y almacenó exactamente una vez cada evento aceptado. Se comprobó evento por evento: 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 durante esos 10 minutos, y la cola se había vaciado en los 3 segundos siguientes al fin de la carga. La detección, que esa comprobación no cubre, siguió el ritmo a 6000 (cola máxima por debajo de 1000) y se quedó atrás a partir de 7600 ofrecidos. No se afirma ningún ritmo sostenido por encima de 6000. Consulta Rendimiento medido.
La transición única a la ingesta duradera
La versión que introduce la ingesta MQTT duradera cambia la forma en que event-sources recibe
la telemetría de dispositivos: en lugar de suscribirse al broker como cliente MQTT, consume un
flujo de captura duradero que el broker escribe antes de confirmar la recepción al dispositivo. Esto
es lo que evita que se pierda telemetría cuando event-sources está caído.
Cruzar esa versión una vez es una actualización en sitio corriente, pero espere una breve ventana de telemetría duplicada y planifique para ello:
- Durante el despliegue, el pod saliente sigue ingiriendo por MQTT mientras el pod entrante ya ha comenzado a consumir el flujo de captura, de modo que los mensajes publicados en ese solapamiento son ingeridos por ambos. La ventana está acotada por cuánto tiempo coexisten los dos pods: el arranque del pod entrante más el drenaje del pod saliente.
- Los eventos que llevan tanto un
altIdcomo unaoccurredTimeprovista por el dispositivo no se ven afectados: la clave de deduplicación del lado de escritura es(tenant, altId, occurredTime), de modo que esos duplicados se colapsan. Un evento con unaltIdpero sinoccurredTimeno se colapsa: el decodificador estampa la hora actual cuando el dispositivo omite una, y las dos copias se decodifican en pods diferentes en instantes diferentes, por lo que obtienen marcas de tiempo diferentes y terminan como dos filas. La telemetría sinaltIdno se deduplica en absoluto. - El solapamiento se prefiere deliberadamente. El orden alternativo —detener el pod antiguo antes de que exista el flujo de captura— pierde cada mensaje que el broker confirma en la brecha, y esa pérdida es silenciosa: al dispositivo se le informa que el mensaje fue aceptado y nunca se almacena. Una lectura duplicada es visible y corregible; una faltante no es ninguna de las dos cosas.
event-sources como Recreatestrategy: Recreate en event-sources produce exactamente el orden con pérdida descrito arriba, porque
termina el pod antiguo antes de que el nuevo cree el flujo de captura. El chart se niega
a renderizar esta configuración en lugar de dejar que descarte telemetría silenciosamente. event-sources
no es un servicio de escritor único y no gana nada con Recreate; una vez realizada la transición puede ejecutar
múltiples réplicas, algo que la ruta de cliente MQTT que reemplaza no podía hacer.
Durabilidad de los datos
La capa de base de datos es intencionalmente independiente del ciclo de vida de la aplicación. Ambas bases de datos se aprovisionan como infraestructura separada con una protección contra destrucción, de modo que actualizar, reinstalar o desinstalar la aplicación nunca las toca. Ese es el caso habitual y es seguro.
La protección resguarda cada base de datos mientras está dentro de la configuración de infraestructura. No resguarda una que se haya sacado fuera de ella: un recurso eliminado de la configuración deja de estar cubierto por las reglas que esa configuración declara, y el plan de eliminación se ejecutará con éxito. Los clústeres de base de datos además son dueños de sus volúmenes, así que eliminar uno se lleva sus datos consigo en lugar de dejar un volumen desasociado.
No edites la base de datos para sacarla de la configuración de infraestructura como forma de reemplazarla.
Actualizar una instancia creada antes de que las bases de datos pasaran al operador es el
único caso en que esto aparece, y se rechaza en tiempo de planificación en lugar de dejarse
al azar. Vuelca primero ambas bases de datos y vuelve a ejecutar dcctl install con
--allow-legacy-db-removal para la base de datos relacional, y el arranque con él para el
almacén de eventos, lo que afirma que te has ocupado de los datos y no verifica nada.
Para una instancia local, recrearla es más simple y descarta los datos de forma
deliberada: destrúyela, recrea el clúster y después instala y arranca, como se describe en
Instancias creadas por la v0.16.0 y anteriores.
Esto es durabilidad de los volúmenes en ejecución; no es un sustituto de las copias de seguridad programadas y la recuperación a un punto en el tiempo, que se aprovisionan con la infraestructura de producción. Consulte Despliegue y operador para saber cómo se separan las capas de infraestructura y aplicación.