Reformulando como o Helium armazena e consulta dados de rede

Migrando de protobufs compactados com gzip para o Apache Iceberg

Estamos migrando os dados da rede Helium de arquivos protobuf compactados com gzip no S3 para tabelas Apache Iceberg, consultadas com Trino por meio de um catálogo REST do Iceberg, com o Cloudflare R2 por baixo. O processo está em andamento e ainda não foi concluído: algumas tabelas já foram totalmente migradas, enquanto outras continuam sendo gravadas em ambos os formatos enquanto validamos o novo caminho em relação ao antigo. A transição definitiva está prevista para 1º de novembro de 2026. Uma observação de escopo preliminar: isso se aplica exclusivamente à rede Mobile. A rede IoT executa seus próprios oráculos e suas próprias tabelas, e nada do que está descrito abaixo se aplica a ela.

O design antigo era otimizado para a escrita: arquivos em um bucket são baratos, simples e imutáveis. Mas consultá-los era o problema.

O pipeline antigo

Durante a maior parte da existência da rede Helium Mobile, a resposta para "onde esses dados são armazenados?" era: em um arquivo protobuf compactado com gzip no S3.

Um serviço de oráculo — ingestão, o verificador de pacotes, o verificador móvel — recebia uma mensagem protobuf, a serializava e a anexava a um fluxo de quadros delimitados por comprimento (length-delimited frames). Esse fluxo era compactado com gzip conforme era gravado, sofria rotação periódica por temporizador ou ao atingir 50 MB, e era enviado para o S3 com uma chave semelhante a data_transfer_session_ingest_report.1782427072597.gz. O timestamp em milissegundos no nome do arquivo era o único índice existente.

Esse design tem méritos reais. É barato: o armazenamento de objetos é o meio mais econômico de armazenamento durável que se pode comprar, e o protobuf compactado com gzip é compacto. É simples: sem registro de esquema, sem coordenador, sem cluster para manter ativo — um escritor capaz de executar um PUT em um bucket constitui uma implementação completa. É imutável por construção, o que é fundamental quando esses arquivos representam a trilha de auditoria por trás das emissões de tokens on-chain. E como os timestamps têm largura fixa, as chaves são ordenadas cronologicamente, tornando "tudo a partir deste ponto" uma única chamada ListObjectsV2 com StartAfter.

O problema

Quatro fatores tornavam esses dados difíceis de operar no dia a dia, fazendo com que ninguém conseguisse obter uma visão completa da rede sem antes escrever um programa dedicado.

  • Não era possível consultá-los diretamente. Ler significava reproduzir (replay): listar as chaves após um watermark, baixar cada objeto, descompactar o gzip, decodificar quadro a quadro e ignorar o que já havia sido processado. Cada consumidor mantinha sua própria cópia dessa máquina de estados e seu próprio cursor persistente.

  • Uma visão completa exigia o download de tudo. Não há pushdown de predicados em um blob gzippado. Qualquer agregação, join ou filtro sobre um mês de tráfego exigia baixar e decodificar um mês inteiro de arquivos — razão pela qual o verificador móvel mantinha sua própria cópia no Postgres do que o cálculo de recompensas demandava: uma sombra parcial e selecionada manualmente do lake, inútil para qualquer pergunta que não tivesse sido prevista com antecedência.

  • A leitura tinha um custo financeiro real. O S3 cobra pelo tráfego de saída (egress), e um replay é uma releitura completa. Quanto mais se demandava dos dados, mais caro ficava — um incentivo adverso para quem tentava analisar e compreender a rede.

  • O esquema residia no código. As definições de protobuf eram o próprio esquema, portanto nada em um arquivo .gz em um bucket indicava o que havia dentro dele. Eram necessários os tipos gerados correspondentes, o decodificador correto e o conhecimento sobre qual mensagem um determinado prefixo continha. Um analista usando um cliente SQL simplesmente não tinha como acessar os dados.

Assim, qualquer nova pergunta — quanto tráfego uma operadora movimentou na última terça-feira, quais hotspots enviam heartbeats mas não trafegam dados, qual o padrão de rejeições por região — começava como um chamado, virava um programa em Rust e terminava como uma tabela que alguém precisava manter.

Enquanto isso, o volume de dados continuava crescendo, assim como o número de pessoas que precisavam analisá-los: engenharia de recompensas, desenvolvimento de negócios, parcerias com operadoras, suporte. Nenhum deles queria programar um poller de arquivos. Eles queriam executar 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

Tudo a jusante do bucket é infraestrutura de replay, e o Postgres armazena apenas as colunas que algum consumidor decidiu previamente guardar. A linha pontilhada resume todo o problema: não há caminho entre a pergunta e os dados sem escrever um programa antes.

Eliminar essa lacuna foi o objetivo desta migração.

Solução

A nova stack é composta por três partes, sendo fundamental que permaneçam desacopladas.

  • Apache Iceberg é o formato de tabela. Os dados continuam sendo gravados como arquivos imutáveis no armazenamento de objetos — agora em Parquet em vez de protobuf compactado com gzip —, mas acompanhados de metadados que descrevem as colunas, o particionamento e quais arquivos pertencem a cada snapshot confirmado.

  • Um catálogo REST do Iceberg atua à frente dessas tabelas. É o serviço responsável por responder quais tabelas existem no namespace hotspots e onde residem os metadados atuais de heartbeats. Tanto escritores quanto leitores comunicam-se com ele, garantindo a consistência entre ambos.

  • O Trino é o motor de consulta. Ele lê o catálogo, planeja a execução sobre os arquivos Parquet e disponibiliza interface SQL para qualquer aplicação capaz de abrir uma conexão JDBC.

Nenhum deles detém a posse exclusiva dos dados. Os dados continuam sendo arquivos em um bucket, um requisito inegociável para garantir a auditabilidade do pipeline de recompensas. A mudança é que agora os próprios arquivos são autodescritivos.

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

Linhas contínuas transportam dados; linhas pontilhadas transportam commits de metadados. Onde uma tabela já foi migrada, a estrutura de replay é eliminada: o verificador móvel acessa suas próprias entradas da mesma forma que um analista, via SQL, e todos consultam as mesmas tabelas confirmadas pelos escritores.

Por que o Iceberg

O que os dados precisavam era se comportar como uma tabela — algo sobre o qual um motor de consulta possa elaborar um plano de execução, e sobre o qual uma pessoa de fora da equipe possa apontar uma query sem precisar receber um decodificador primeiro.

O Iceberg fornece isso. Os arquivos no bucket deixam de ser uma pilha desordenada e passam a ser uma tabela que declara o que contém: quais arquivos fazem parte dela neste momento, quais são as colunas e como ela está particionada. Ao apontar um motor compatível com Iceberg para o catálogo, ele descobre o que escanear e o que ignorar, sem necessidade de instruções prévias da nossa parte.

O Iceberg é um formato de tabela aberto para tabelas analíticas massivas. Três de suas propriedades centrais foram determinantes aqui.

O esquema é um artefato declarado, não um efeito colateral. Uma tabela é definida uma única vez na base de código — campos, nulabilidade, particionamento — e essa definição é o que a cria no catálogo. Assim, uma consulta sabe que uma coluna nunca é nula e não requer COALESCE, e um filtro na coluna de partição elimina arquivos (pruning) em vez de varrer a tabela inteira. Ambos os fatos agora podem ser interpretados por ferramentas automatizadas, não apenas por quem conhece o código-fonte. E como o Iceberg rastreia colunas por ID e não por posição, adicionar, renomear ou reordenar colunas é apenas uma alteração de metadados: sem reescrita de tabela e sem dados-fantasma ressurgindo de arquivos antigos.

Commits atômicos e múltiplos escritores concorrentes. Um commit é uma simples troca de ponteiro de metadados, não um diretório de arquivos que todos combinam não alterar; assim, um leitor nunca vê lotes parciais e vários motores podem gravar na mesma tabela simultaneamente. No design anterior, um consumidor podia listar um objeto ainda em upload, e todas as mitigações eram heurísticas — esperar um pouco, retroceder o watermark, tentar novamente.

Particionamento oculto. O Iceberg deriva os valores de partição automaticamente da coluna configurada e descarta os arquivos irrelevantes para a consulta. Quem escreve SQL não precisa conhecer o layout físico nem se lembrar de incluir predicados extras para acionar o pruning — e o particionamento pode ser alterado no futuro sem reescrever o histórico ou quebrar consultas existentes.

Nada disso é proprietário. Essa é a função do Apache Iceberg; nosso trabalho consistiu em implementar a gravação e a leitura sobre ele.

Isso viabiliza a próxima etapa. As tabelas ficam no R2, que não cobra egress, de modo que a leitura não gera custos por consulta para o usuário nem para nós. Qualquer pessoa pode subir uma instância do Trino, apontá-la para o catálogo e consultar a rede diretamente — volume de tráfego de um hotspot, pagamentos efetuados pelo módulo de recompensas, quais registros foram rejeitados e por quê. Sem APIs para construir, sem pipelines de exportação para manter e sem custos de egress decorrentes do tráfego de terceiros. Para uma rede cujos pagamentos devem ser verificáveis, essa é a diferença entre publicar um número e disponibilizar os dados que o fundamentam.

O que o Iceberg não faz é a sua própria manutenção.

Compactação

Escritores de streaming geram arquivos pequenos. Cada commit grava o acumulado da última janela de lote, fazendo com que uma tabela sob escrita contínua termine o dia com milhares de objetos de poucos megabytes em vez de alguns arquivos grandes.

Consultar isso é lento e oneroso. O motor precisa abrir cada arquivo não descartado e ler o rodapé de cada um; com isso, o custo da consulta passa a acompanhar a quantidade de arquivos e não o volume de dados — e, no armazenamento de objetos, cada operação de abertura representa um round trip de rede. Os metadados crescem no mesmo ritmo: cada snapshot mantém seu manifesto, e arquivos antigos permanecem no local até serem explicitamente removidos.

A compactação é a solução, e o Iceberg oferece suporte nativo a ela. O OPTIMIZE reescreve os arquivos pequenos de uma partição em menos arquivos maiores, o EXPIRE_SNAPSHOTS descarta históricos fora da janela de retenção, e o REMOVE_ORPHAN_FILES limpa os resíduos deixados pelas reescritas. Executamos os três comandos periodicamente por tabela: descobrimos as tabelas no catálogo, selecionamos as partições com arquivos pequenos suficientes para justificar a reescrita e mantemos intactas as partições ainda sob escrita ativa até que estabilizem.

Estrutura dos dados

Três namespaces, cada um refletindo uma etapa do pipeline de produção.

Tabela

Representação da linha

Particionado por

data_transfer.sessions

Sessão de offload conforme ingerida: pubkey, operadora, bytes enviados/baixados/recompensáveis, pagador

day(report_received_timestamp)

data_transfer.burned_sessions

Consumo (burn) de DC de um hotspot: rewardable_bytes, num_dcs, o multiplier do HIP-150, horário do burn

day(burn_timestamp)

data_transfer.multiplier_ticket_history

Histórico de tickets de multiplicador do HIP-150 recebidos, aceitos ou rejeitados; append-only

bucket(hotspot_pubkey, 4)

hotspots.heartbeats

Heartbeat validado: pubkey, localização, multiplicador de confiança de localização, origem da localização

day(received_timestamp)

hotspots.speedtests

Resultado individual de speedtest

day(received_timestamp)

hotspots.speedtest_avgs

Média móvel de speedtest de um hotspot

day(timestamp)

hotspots.bans

Banimento aplicado a um hotspot

day(received_timestamp)

rewards.data_transfer

Recompensa de um hotspot no período: dc_transfer_reward, rewardable_bytes, o price utilizado

day(start_period)

rewards.service_provider

Recompensa de um provedor de serviço no período

day(start_period)

rewards.unallocated

Emissões não alocadas a nenhuma entidade no período

day(start_period)

Filtre sempre pela coluna de partição. Esse predicado é o que viabiliza o pruning de arquivos pelo Iceberg; sem ele, a consulta lê a tabela inteira, por mais refinado que seja o resultado final:

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

A filtragem precisa ser aplicada na coluna de partição em si — filtrar por outro timestamp na mesma linha não descarta arquivos, mesmo que ambos variem em conjunto. A exceção a essa regra é multiplier_ticket_history: particionada via bucket por hotspot_pubkey, de modo que o pruning é ativado por um filtro na pubkey, e não por intervalo de datas.

Para consultar o esquema atualizado, consulte diretamente o catálogo em vez deste artigo: execute SHOW TABLES IN mobile.hotspots, DESCRIBE mobile.hotspots.heartbeats, ou consulte mobile.information_schema.columns.

Como se conectar

O catálogo expõe um endpoint REST padrão do Iceberg, tornando as tabelas legíveis por qualquer cliente compatível com o formato. Dois parâmetros são essenciais para a conexão: o catálogo fica em https://catalog.mobile.helium.io/api/catalog sob o warehouse mobile, e o armazenamento subjacente possui acesso anônimo configurado. Esses valores estão em produção e funcionam conforme especificado.

Como utilizamos o Trino, podemos atestar diretamente a respectiva configuração. Os demais exemplos empregam os mesmos valores na sintaxe própria de cada ferramenta.

Trino

A documentação oficial do Trino serve como referência principal — consulte Deploying Trino para a instalação do servidor e a página do conector Iceberg para consultar os parâmetros listados abaixo e outras configurações disponíveis.

# 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

A alternativa mais leve, dispensando a necessidade de clusters dedicados. Declare o catálogo em ~/.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

Ou ignore o arquivo de configuração e passe as propriedades inline no 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()

Observe o padrão de nomenclatura: o PyIceberg faz o lookup de hotspots.heartbeats dentro do catálogo já carregado, enquanto o Trino exige o prefixo do catálogo — mobile.hotspots.heartbeats.

Spark

Utilize os mesmos valores como parâmetros spark.sql.catalog.* em spark-defaults.conf ou via flags --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

Em seguida, execute spark.sql("SELECT * FROM mobile.hotspots.heartbeats LIMIT 10"). É obrigatório incluir iceberg-spark-runtime e iceberg-aws-bundle no classpath; sem o bundle da AWS, o catálogo é resolvido, mas as leituras falham na camada de armazenamento.

Alguns detalhes essenciais para evitar problemas de configuração em qualquer cliente:

  • Não há credenciais secretas. iceberg.rest-catalog.security=NONE indica ausência de autenticação no catálogo, e as credenciais de armazenamento consistem na string literal anonymous. Não há tokens para solicitar, rotacionar ou omitir do controle de versão.

  • warehouse não é o nome do bucket. Trata-se do identificador do warehouse registrado no catálogo REST. Configurar esse valor incorretamente resulta em um catálogo que conecta sem erros, mas lista zero tabelas.

  • Credenciais vendidas (vended credentials) estão desativadas. O catálogo não fornece credenciais de acesso específicas por tabela; portanto, o Trino utiliza as propriedades s3.* configuradas acima para todas as tabelas acessadas.

  • Múltiplos catálogos em um único motor. Cada arquivo .properties adicional configura um novo catálogo, permitindo ao Trino realizar joins entre eles — possibilitando cruzar essas tabelas com bases locais em uma única instrução SQL.

  • O catálogo possui limitação de taxa (rate limiting). Acesso público não significa chamadas ilimitadas. Requisições ao catálogo sofrem throttling; clientes que realizarem excesso de chamadas serão temporariamente bloqueados.

O que isso viabiliza

O foco nunca foi a adoção do Iceberg em si, mas sim tornar a consulta aos dados da rede simples e financeiramente acessível. Os dados da rede agora podem ser consultados por vias convencionais — SQL sobre JDBC a partir de qualquer ferramenta de BI ou notebook. O único custo associado é o de execução da sua própria instância do Trino; as operações de leitura em si não geram cobrança de saída. Essa redução de fricção permite que a criação de aplicações, dashboards ou painéis de métricas públicas sobre a rede torne-se viável em um único final de semana.

O volume transferido por cada operadora no último mês, os hotspots ativos que não processam tráfego, o cálculo detalhado de recompensas auferidas por um hotspot ou os locais aprovados sem tráfego registrado — cada um desses cenários agora se resolve com uma única consulta sobre tabelas abertas para qualquer motor de dados compatível.

Entre em Contato

Entre em Contato

Fale com a equipe da Helium.

Fale com a equipe da Helium.

Tem alguma dúvida sobre esta história, a cobertura na sua região ou como trabalhar com a Helium? Envie-nos uma mensagem e a pessoa responsável entrará em contato com você.

Tem alguma dúvida sobre esta história, a cobertura na sua região ou como trabalhar com a Helium? Envie-nos uma mensagem e a pessoa responsável entrará em contato com você.

* Required

Artigos Relacionados

Artigos Relacionados

Sinal Máximo, Sem Problemas: Dividir a Capacidade Entre Sistemas Sem Fio Aumenta a Confiabilidade

Sinal Máximo, Sem Problemas: Dividir a Capacidade Entre Sistemas Sem Fio Aumenta a Confiabilidade

Um DAS legado não é capaz de suportar toda a demanda. Direcione a voz para a rede celular e o tráfego de dados para o Wi-Fi, permitindo que cada rede opere no seu melhor potencial.

Um DAS legado não é capaz de suportar toda a demanda. Direcione a voz para a rede celular e o tráfego de dados para o Wi-Fi, permitindo que cada rede opere no seu melhor potencial.

Helium Plus dá início a uma sequência de vitórias no Hialeah Park Casino

Helium Plus dá início a uma sequência de vitórias no Hialeah Park Casino

Três semanas após a entrada em operação, a rede Wi-Fi do próprio cassino atendeu a mais de 1.100 assinantes de operadoras em um único dia.

Três semanas após a entrada em operação, a rede Wi-Fi do próprio cassino atendeu a mais de 1.100 assinantes de operadoras em um único dia.

Conectando o Vale: em Ellicott City, a melhoria na cobertura celular começa na Main Street

Conectando o Vale: em Ellicott City, a melhoria na cobertura celular começa na Main Street

A Corelogix planeja conectar o centro histórico por meio da rede Helium, utilizando conexões limitadas de backhaul para atender aos edifícios vizinhos.

A Corelogix planeja conectar o centro histórico por meio da rede Helium, utilizando conexões limitadas de backhaul para atender aos edifícios vizinhos.

Sinal Máximo, Zero Orçamento: O Upgrade de DAS Que Ninguém Quer Financiar

Sinal Máximo, Zero Orçamento: O Upgrade de DAS Que Ninguém Quer Financiar

Os sistemas de antenas distribuídas (DAS) da era LTE estão se tornando obsoletos, uma modernização para o 5G custa milhões e a dúvida que paralisa locais e empreendimentos no mundo todo é a mesma: quem vai pagar essa conta?

Os sistemas de antenas distribuídas (DAS) da era LTE estão se tornando obsoletos, uma modernização para o 5G custa milhões e a dúvida que paralisa locais e empreendimentos no mundo todo é a mesma: quem vai pagar essa conta?

Portuguese
© 2026 Nova Labs, Inc., operando como Helium. Todos os direitos reservados.
Portuguese
© 2026 Nova Labs, Inc., operando como Helium. Todos os direitos reservados.
Portuguese
© 2026 Nova Labs, Inc., operando como Helium. Todos os direitos reservados.