Reestructurando la forma en que Helium almacena y consulta los datos de la red

Migración de protobufs comprimidos con gzip a Apache Iceberg

Estamos migrando los datos de la red de Helium desde archivos protobuf comprimidos con gzip en S3 hacia tablas de Apache Iceberg, consultadas con Trino a través de un catálogo REST de Iceberg, con Cloudflare R2 como base de almacenamiento. Este proceso está en marcha en lugar de estar terminado: algunas tablas ya migraron por completo, otras se siguen escribiendo de ambas formas mientras validamos la nueva ruta contra la anterior. El cambio definitivo se completará el 1 de noviembre de 2026. Una nota aclaratoria sobre el alcance antes de continuar: esto corresponde únicamente a la red Mobile. IoT opera sus propios oráculos y sus propias tablas, y nada de lo que se describe a continuación les aplica.

El diseño anterior estaba optimizado para la escritura: los archivos en un bucket son económicos, sencillos e inmutables. Sin embargo, consultarlos era el gran problema.

La arquitectura anterior

Durante la mayor parte de la existencia de la red Helium Mobile, la respuesta a "¿a dónde van estos datos?" era: a un archivo protobuf comprimido con gzip en S3.

Un servicio de oráculo (ingest, el verificador de paquetes, el verificador de mobile) tomaba un mensaje de protobuf, lo serializaba y lo agregaba a un flujo de tramas delimitado por longitud. Ese flujo se comprimía con gzip a medida que se escribía, se rotaba mediante un temporizador o al alcanzar los 50 MB, y se cargaba en S3 bajo una clave como data_transfer_session_ingest_report.1782427072597.gz. El timestamp en milisegundos en el nombre del archivo era el único índice existente.

Ese diseño tiene grandes virtudes. Es económico: el almacenamiento de objetos es la opción duradera menos costosa que se puede adquirir, y el protobuf comprimido con gzip es compacto. Es simple: sin registro de esquemas, sin coordinador, sin clústeres que mantener activos; un escritor con permisos de PUT en un bucket es una implementación completa. Es inmutable por diseño, lo cual es fundamental cuando esos archivos constituyen la pista de auditoría detrás de la emisión de tokens en la blockchain. Y como los timestamps tienen una longitud fija, las claves se ordenan cronológicamente, por lo que "todo lo que ocurra después de este punto" se resuelve con una sola llamada ListObjectsV2 usando StartAfter.

El problema

Cuatro factores dificultaban el uso de esos datos, y en conjunto implicaban que nadie pudiera obtener una visión completa de la red sin antes tener que escribir un programa.

  • No era posible consultarlos directamente. Leer significaba procesar secuencialmente: listar claves a partir de una marca de tiempo, descargar cada objeto, descomprimirlo con gunzip, decodificar trama por trama y omitir lo que ya se había procesado. Cada consumidor debía implementar su propia copia de esa máquina de estados y su propio cursor persistente.

  • Una vista completa requería descargar todo. No existe la optimización de pushdown de predicados en un blob comprimido con gzip. Cualquier agregación, combinación (join) o filtrado sobre un mes de tráfico requería descargar y decodificar un mes entero de archivos; razón por la cual el verificador de mobile mantenía su propia copia en Postgres de lo que requería el cálculo de recompensas: un reflejo parcial y seleccionado a mano del data lake, inútil para responder cualquier pregunta que no se hubiera previsto.

  • La lectura generaba costos reales. S3 cobra por transferencia de salida (egress), y volver a procesar implica una lectura completa. Cuanto más se consultaban los datos, mayor era el costo; un incentivo negativo para quienes intentaban entender el comportamiento de la red.

  • El esquema residía únicamente en el código. Las definiciones de protobuf eran el esquema, por lo que nada en un archivo .gz dentro de un bucket indicaba su contenido. Se necesitaban los tipos generados correspondientes, el decodificador correcto y saber qué mensaje contenía un prefijo determinado. Un analista con un cliente SQL no tenía forma alguna de acceder.

Por lo tanto, cualquier nueva pregunta —como cuántos datos transfirió un operador el martes pasado, qué hotspots emiten señales de vida (heartbeat) pero no transmiten tráfico, o cómo se distribuyen los rechazos por región— comenzaba como un ticket de soporte, requería desarrollar un programa en Rust y terminaba en una tabla que alguien debía mantener.

Mientras tanto, el volumen de datos seguía creciendo, al igual que el número de personas que necesitaban analizarlos: ingeniería de recompensas, desarrollo de negocio, alianzas con operadores y soporte. Ninguno de ellos quería programar un recolector de archivos. Lo que necesitaban era un SELECT.

flowchart LR

    ORC[Oracles<br/>ingest, verifiers]

    S3[(S3 bucket<br/>gzipped protobuf)]

    POLL[File poller<br/>list, download, decode]

    PG[(Postgres<br/>partial copy)]

    OUT[Rewarder]

    AN[Analyst]

    ORC -->|PUT .gz| S3

    S3 --> POLL

    POLL -->|INSERT| PG

    PG --> OUT

    AN -.->|no way in| S3

Todo lo que está después del bucket son mecanismos de procesamiento secuencial, y Postgres solo almacena las columnas que algún consumidor decidió conservar. La línea punteada representa el problema principal: no existe un camino directo entre una consulta y los datos sin tener que escribir código primero.

Esa brecha es justamente lo que esta migración busca resolver.

Solución

La nueva arquitectura consta de tres componentes, y lo fundamental es que se mantienen modulares e independientes.

  • Apache Iceberg es el formato de tabla. Los datos siguen almacenándose como archivos inmutables en el almacenamiento de objetos —ahora en Parquet en lugar de protobuf con gzip— pero junto a ellos se guardan metadatos que describen las columnas, la partición y a qué instantánea (snapshot) confirmada pertenece cada archivo.

  • Un catálogo REST de Iceberg coordina el acceso a estas tablas. Es el servicio que responde qué tablas existen en el espacio de nombres de hotspots y dónde residen los metadatos actuales de heartbeats. Tanto los escritores como los lectores interactúan con él, asegurando la consistencia de los datos.

  • Trino es el motor de consultas. Lee el catálogo, planifica la ejecución sobre los archivos Parquet y expone una interfaz SQL estándar para cualquier herramienta compatible con conexiones JDBC.

Ninguno de estos componentes se adueña de los datos. Los datos siguen siendo archivos almacenados en un bucket, una condición que nunca estuvo a discusión ya que es lo que permite que el proceso de recompensas sea auditable. El cambio radica en que ahora los archivos se autodescriben.

flowchart LR

    ORC[Oracles<br/>ingest, verifiers]

    OBJ[(Object storage<br/>Parquet)]

    CAT[Iceberg REST catalog]

    TR[Trino]

    OUT[Services, analysts<br/>dashboards]

    ORC --> OBJ

    ORC -.->|commit snapshot| CAT

    OBJ --> TR

    CAT -.-> TR

    TR --> OUT

Las líneas continuas representan el flujo de datos; las líneas punteadas, las confirmaciones de metadatos. Una vez que una tabla se migra, la infraestructura de reprocesamiento anterior desaparece: el verificador de mobile accede a sus datos de entrada de la misma manera que lo hace un analista, mediante SQL, y todos leen las mismas tablas confirmadas por los escritores.

Por qué Iceberg

Lo que los datos requerían era comportarse como una tabla: algo sobre lo que un motor de consultas pudiera planificar ejecuciones, y donde cualquier persona externa al equipo pudiera dirigir una consulta sin necesidad de recibir un decodificador previamente.

Iceberg ofrece exactamente eso. Los archivos en el bucket dejan de ser una pila desordenada y pasan a ser una tabla que declara su estructura: qué archivos forman parte de ella actualmente, cuáles son las columnas y cómo está particionada. Al apuntar un motor compatible con Iceberg al catálogo, este determina qué escanear y qué omitir, de forma totalmente autónoma.

Iceberg es un formato abierto de tablas diseñado para analítica de datos a gran escala. Tres de sus principales características clave fueron las que resolvieron nuestro problema.

El esquema es un recurso declarado, no un efecto secundario. Una tabla se define una sola vez en el código fuente —campos, nulidad, particionado— y esa definición es la que la crea en el catálogo. Así, una consulta detecta si una columna nunca es nula y no requiere un COALESCE, y un filtro en la columna de partición descarta archivos en lugar de escanear la tabla completa. Ambos aspectos ahora son legibles para las herramientas, no solo para quien conozca el código. Además, debido a que Iceberg rastrea las columnas por ID y no por posición, agregar, renombrar o reordenar una de ellas es solo un cambio de metadatos: sin reescrituras de tablas ni reaparición de datos obsoletos desde archivos antiguos.

Confirmaciones atómicas y múltiples escritores concurrentes. Una confirmación (commit) consiste en un intercambio del puntero de metadatos, no en un directorio de archivos que todos asumen que no deben modificar; por ende, un lector nunca verá datos a medias y múltiples motores pueden escribir en la misma tabla de forma concurrente. En el diseño anterior, un consumidor podía listar un objeto que aún se estaba cargando, y las soluciones para esto eran heurísticas: esperar, retrasar la marca de tiempo o reintentar.

Particionado oculto. Iceberg calcula automáticamente los valores de partición a partir de la columna indicada y omite los archivos que la consulta no necesita. Quien escribe el SQL no requiere conocer la estructura física de almacenamiento ni acordarse de agregar predicados adicionales para optimizar la consulta; además, la estructura puede modificarse a futuro sin tener que reescribir el historial ni romper las consultas ya desarrolladas.

Nada de esto es un desarrollo propietario. Es el propósito de Apache Iceberg; el trabajo de nuestro lado consistió en implementar la escritura y la lectura en él.

Esto es lo que hace posible la siguiente fase. Las tablas residen en R2, que no cobra costos de salida (egress), por lo que consultarlas no tiene costo para el usuario ni para nosotros. Cualquiera puede desplegar su propio Trino, apuntarlo al catálogo y consultar la red de forma directa: el tráfico transferido por un hotspot, las recompensas pagadas, o qué registros fueron rechazados y el motivo. Sin necesidad de desarrollar APIs, mantener pipelines de exportación ni recibir facturas por consultas de terceros. Para una red cuyas recompensas deben ser auditables, esta es la diferencia real entre publicar una cifra y publicar los datos que la respaldan.

Lo que Iceberg no hace de forma autónoma es su propio mantenimiento.

Compactación

Los escritores de streaming generan archivos pequeños. Cada confirmación escribe lo acumulado en la ventana del último lote, por lo que una tabla que recibe escrituras continuas termina el día con miles de objetos de unos cuantos megabytes en lugar de unos pocos archivos de gran tamaño.

Esto ralentiza y encarece las consultas. El motor debe abrir cada archivo que no pueda descartar y leer sus metadatos finales, por lo que el costo de la consulta escala según el número de archivos y no por el volumen real de datos. Además, en el almacenamiento de objetos, cada apertura representa una petición de red. Los metadatos crecen con la misma velocidad: cada instantánea (snapshot) conserva su manifiesto y los archivos antiguos permanecen allí hasta que se eliminen explícitamente.

La compactación es la solución, e Iceberg cuenta con soporte nativo para ello. El comando OPTIMIZE reorganiza los archivos pequeños de una partición en un menor número de archivos grandes, EXPIRE_SNAPSHOTS depura el historial que supera la ventana de retención y REMOVE_ORPHAN_FILES limpia los archivos residuales de las reescrituras. Ejecutamos estos tres procesos de forma programada por tabla: identificamos las tablas mediante el catálogo, seleccionamos las particiones con suficientes archivos pequeños para que valga la pena reescribirlos y dejamos intactas las particiones que aún reciben escrituras activas hasta que se estabilicen.

Estructura de datos

Tres espacios de nombres, cada uno de los cuales refleja una etapa del flujo que lo genera.

Tabla

Definición de fila

Particionada por

data_transfer.sessions

Sesión de descarga procesada: clave pública, operador, bytes de subida/bajada/recompensables, pagador

day(report_received_timestamp)

data_transfer.burned_sessions

Consumo de Data Credits (DC) de un hotspot: rewardable_bytes, num_dcs, el multiplier de HIP-150 y la hora de consumo

day(burn_timestamp)

data_transfer.multiplier_ticket_history

Historial de tickets de multiplicador HIP-150 recibidos, aceptados o rechazados; solo lectura para inserción

bucket(hotspot_pubkey, 4)

hotspots.heartbeats

Señal de vida (heartbeat) validada: clave pública, posición, multiplicador de confianza de ubicación, origen de ubicación

day(received_timestamp)

hotspots.speedtests

Resultado de una prueba de velocidad (speedtest)

day(received_timestamp)

hotspots.speedtest_avgs

Promedio móvil de pruebas de velocidad de un hotspot

day(timestamp)

hotspots.bans

Bloqueo aplicado a un hotspot

day(received_timestamp)

rewards.data_transfer

Pago a un hotspot por período: dc_transfer_reward, rewardable_bytes y el price aplicado

day(start_period)

rewards.service_provider

Pago a un proveedor de servicios por período

day(start_period)

rewards.unallocated

Emisiones no asignadas a ninguna entidad en un período

day(start_period)

Es indispensable filtrar siempre por la columna de partición. Este predicado permite a Iceberg descartar archivos; si se omite, la consulta leerá la tabla completa, sin importar qué tan delimitado parezca el resultado:

SELECT hotspot_pubkey, count(*)

FROM mobile.hotspots.heartbeats

WHERE received_timestamp >= TIMESTAMP '2026-09-21 00:00:00 UTC'

  AND received_timestamp <  TIMESTAMP '2026-09-22 00:00:00 UTC'

GROUP BY hotspot_pubkey

Debe ser obligatoriamente la columna de partición; filtrar por cualquier otra marca de tiempo en la misma fila no optimizará el escaneo, incluso si se mueven en paralelo. La excepción a esta regla es multiplier_ticket_history, que está segmentada por buckets de hotspot_pubkey, por lo que un filtro sobre la clave pública optimizará la consulta, mientras que un rango de fechas no lo hará.

Para consultar el esquema en tiempo real, consulte directamente al catálogo en lugar de este artículo: ejecute SHOW TABLES IN mobile.hotspots, DESCRIBE mobile.hotspots.heartbeats, o realice una consulta a mobile.information_schema.columns.

Conexión al servicio

El catálogo utiliza un punto de enlace (endpoint) REST estándar de Iceberg, por lo que cualquier cliente compatible con Iceberg puede consultar estas tablas. Todo cliente requiere la misma configuración inicial: el catálogo se encuentra en https://catalog.mobile.helium.io/api/catalog con el almacén de datos (warehouse) mobile, y el almacenamiento subyacente es de acceso anónimo. Estos valores son reales y están activos, no son ejemplos.

Nosotros utilizamos Trino, por lo que es la configuración que podemos garantizar. Para otras herramientas, los parámetros son homólogos según su propia sintaxis.

Trino

La documentación oficial de Trino es la referencia para esto: consulte Deploying Trino para la instalación del servidor, y la página del conector de Iceberg para conocer en detalle todas las propiedades descritas a continuación y otras opciones avanzadas.

# etc/catalog/mobile.properties

connector.name=iceberg

iceberg.catalog.type=rest

iceberg.rest-catalog.uri=https://catalog.mobile.helium.io/api/catalog

iceberg.rest-catalog.security=NONE

iceberg.rest-catalog.warehouse=mobile

iceberg.rest-catalog.vended-credentials-enabled=false

iceberg.security=READ_ONLY

fs.native-s3.enabled=true

s3.region=us-east-1

s3.endpoint=https://helium.io

s3.path-style-access=false

s3.aws-access-key=anonymous

s3.aws-secret-key=anonymous

PyIceberg

La opción más ligera y que no requiere de un clúster activo. Defina el catálogo en el archivo ~/.pyiceberg.yaml:

catalog:

  mobile:

    type: rest

    uri: https://catalog.mobile.helium.io/api/catalog

    warehouse: mobile

    s3.endpoint: https://helium.io

    s3.region: us-east-1

    s3.path-style-access: false

    s3.access-key-id: anonymous

    s3.secret-access-key: anonymous

O bien, defina los parámetros directamente en el código:

from pyiceberg.catalog import load_catalog

catalog = load_catalog("mobile", **{

    "type": "rest",

    "uri": "https://catalog.mobile.helium.io/api/catalog",

    "warehouse": "mobile",

    "s3.endpoint": "https://helium.io",

    "s3.region": "us-east-1",

    "s3.path-style-access": "false",

    "s3.access-key-id": "anonymous",

    "s3.secret-access-key": "anonymous",

})

table = catalog.load_table("hotspots.heartbeats")

df = table.scan(limit=10).to_arrow()

Considere la nomenclatura de las tablas: PyIceberg direcciona hotspots.heartbeats dentro del catálogo previamente cargado, mientras que Trino requiere el prefijo del nombre del catálogo: mobile.hotspots.heartbeats.

Spark

Defina las propiedades equivalentes como parámetros spark.sql.catalog.* en su archivo spark-defaults.conf o como flags del tipo --conf:

spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions

spark.sql.catalog.mobile=org.apache.iceberg.spark.SparkCatalog

spark.sql.catalog.mobile.type=rest

spark.sql.catalog.mobile.uri=https://catalog.mobile.helium.io/api/catalog

spark.sql.catalog.mobile.warehouse=mobile

spark.sql.catalog.mobile.io-impl=org.apache.iceberg.aws.s3.S3FileIO

spark.sql.catalog.mobile.s3.endpoint=https://helium.io

spark.sql.catalog.mobile.s3.region=us-east-1

spark.sql.catalog.mobile.s3.path-style-access=false

spark.sql.catalog.mobile.s3.access-key-id=anonymous

spark.sql.catalog.mobile.s3.secret-access-key=anonymous

Posteriormente ejecute spark.sql("SELECT * FROM mobile.hotspots.heartbeats LIMIT 10"). Asegúrese de incluir las dependencias iceberg-spark-runtime e iceberg-aws-bundle en el classpath; sin el paquete de AWS, el catálogo resolverá las consultas pero las lecturas fallarán en la capa de almacenamiento.

Algunas consideraciones clave para evitar inconvenientes durante la implementación, independientemente del cliente de su elección:

  • Ningún dato de conexión es confidencial o privado. iceberg.rest-catalog.security=NONE establece que el catálogo no requiere autenticación y las credenciales de almacenamiento son literalmente la cadena de texto anonymous. No hay tokens que solicitar, rotar ni omitir en sus sistemas de control de versiones.

  • warehouse no corresponde al nombre del bucket. Define el nombre del almacén tal como lo identifica el catálogo REST. Definir este parámetro de forma errónea permitirá una conexión exitosa pero no listará ninguna tabla.

  • Las credenciales delegadas (vended credentials) están desactivadas. El catálogo no provee credenciales de almacenamiento por tabla, por lo que Trino utiliza los parámetros s3.* definidos arriba para cada tabla con la que interactúa.

  • Múltiples catálogos en un solo motor. Agregar un segundo archivo .properties representa un segundo catálogo, y Trino permite realizar consultas combinadas (joins) entre ambos. De esta forma, puede integrar este catálogo con sus datos internos y consultar ambos en una sola sentencia SQL.

  • El catálogo cuenta con límite de peticiones (rate-limiting). Que el acceso sea abierto no significa que sea ilimitado. Las solicitudes están controladas, por lo que peticiones masivas y concurrentes recibirán respuestas de rechazo.

Las posibilidades de esta migración

El objetivo final nunca fue únicamente implementar Iceberg. Se buscaba hacer que los datos de la red fueran fáciles y económicos de consultar. La información de la red ahora es accesible a través de SQL estándar sobre una conexión JDBC, compatible con cualquier herramienta de Business Intelligence o notebooks que utilice actualmente. El único costo operativo de realizar consultas es mantener activa su instancia de Trino; las consultas de lectura no generan costos adicionales. Esta facilidad de integración permite desarrollar aplicaciones, tableros de control o estadísticas públicas a partir de los datos de la red en cuestión de horas en lugar de requerir un proyecto complejo.

Determinar cuántos datos transfirió cada operador el mes pasado, qué hotspots registran actividad pero no transmiten tráfico, cuáles fueron las ganancias detalladas de un nodo o qué sitios aprobados no han transmitido datos se resuelve ahora con una consulta SQL a tablas públicas en las que cualquiera puede configurar su propio motor analítico.

Artículos que te podrían interesar

Artículos que te podrían interesar

Reestructurando la forma en que Helium almacena y consulta los datos de la red

Reestructurando la forma en que Helium almacena y consulta los datos de la red

Migración de protobufs comprimidos con gzip a Apache Iceberg

Migración de protobufs comprimidos con gzip a Apache Iceberg

La inversión de miles de millones de dólares en cobertura satelital sigue dejando rezagada a la conectividad en interiores

La inversión de miles de millones de dólares en cobertura satelital sigue dejando rezagada a la conectividad en interiores

SpaceX y los operadores nacionales están invirtiendo miles de millones para eliminar las zonas sin cobertura en exteriores, pero la brecha de conectividad que más frustra a los clientes se encuentra detrás de las paredes.

SpaceX y los operadores nacionales están invirtiendo miles de millones para eliminar las zonas sin cobertura en exteriores, pero la brecha de conectividad que más frustra a los clientes se encuentra detrás de las paredes.

Wakefern Grocery Stores Spent 80 Years Sharing Infrastructure. Now That Includes the Wi-Fi.

Wakefern Grocery Stores Spent 80 Years Sharing Infrastructure. Now That Includes the Wi-Fi.

The co-op that turned shared warehouses into a $20.7 billion business is now turning store Wi-Fi from a line item into a revenue stream.

The co-op that turned shared warehouses into a $20.7 billion business is now turning store Wi-Fi from a line item into a revenue stream.

The Handoff: Alabama’s Bryant-Denny Stadium Wi-Fi Now Carries Game-Day Carrier Traffic

The Handoff: Alabama’s Bryant-Denny Stadium Wi-Fi Now Carries Game-Day Carrier Traffic

The Crimson Tide brings 100,000 fans and a surge of cellular demand. Through Wi-Fi offload, the stadium's own network now shoulders part of the load.

The Crimson Tide brings 100,000 fans and a surge of cellular demand. Through Wi-Fi offload, the stadium's own network now shoulders part of the load.

Español
© 2026 Nova Labs, Inc., que opera bajo el nombre comercial de Helium. Todos los derechos reservados.
Español
© 2026 Nova Labs, Inc., que opera bajo el nombre comercial de Helium. Todos los derechos reservados.
Español
© 2026 Nova Labs, Inc., que opera bajo el nombre comercial de Helium. Todos los derechos reservados.