Desafios de entrevista em Go · parte 11 de 11
Datadog - High-Throughput Metric Aggregator Daemon
avançadoPublicada em · 4 min de leitura
Dados da empresa e da vaga
- Empresa: Datadog, Inc.
- Sede: Nova York, EUA (com polos em Paris, Madri, Boston, Tóquio e contratação remota internacional)
- Perfil da vaga: Software Engineer (Core Agent & Ingestion Pipelines em Go)
- Modelo de trabalho: Remoto internacional ou híbrido
- Faixa salarial estimada da vaga:
- Sênior nos EUA: $175.000 a $235.000 (Base) / $280.000 a $400.000+ USD/ano (Total Comp com ações na NASDAQ: DDOG)
- Sênior Internacional Remoto (LATAM/Europa): $85.000 a $130.000 USD/ano (B2B / EOR)
- Stack principal do time: Go, C/C++, eBPF, Docker, Kubernetes, Linux Internals, OpenTelemetry, gRPC, Protobuf
Contexto do desafio técnico
O Datadog Agent é o software de monitoramento de infraestrutura de código aberto escrito predominantemente em Go que roda como daemon de segundo plano em milhões de servidores bare-metal e contêineres Kubernetes em todo o mundo.
Em processos seletivos para o time do Core Agent da Datadog, os avaliadores exigem domínio absoluto sobre alocação zero de memória no caminho quente (0 allocs/op), redução de pausas do Garbage Collector, operações atômicas em nível de processador (sync/atomic) e estruturas de dados sem contenção de travas.
Este desafio técnico exige que o candidato projete e implemente um Daemon Agregador de Métricas de Alta Vazão em Go, capaz de absorver centenas de milhares de medições por segundo de múltiplas goroutines concorrentes, agregá-las em janelas temporais fixas (ex.: 10 segundos) e descarregar o lote consolidado via rede sem bloquear a ingestão em tempo real.
Requisitos de arquitetura e funcionamento
1. Tipos de métricas suportadas
- Counter (Contador): Representa eventos cumulativos (ex.: contagem de requisições HTTP ou pacotes de rede). A agregação durante a janela temporal é a soma de todos os valores incrementados.
- Gauge (Medidor): Representa um valor instantâneo (ex.: uso percentual de memória RAM ou temperatura da CPU). A agregação na janela temporal é o último valor registrado ou a média.
2. O caminho crítico de ingestão (Ingest)
- A função de envio de métrica deve ser extremamente veloz ($< 50 \text{ nanossegundos}$).
- Proibido travar globalmente: Não é permitido proteger o dicionário de métricas inteiro com um único mutex, pois centenas de goroutines emitindo métricas simultaneamente causariam contenção catastrófica de CPU.
- Minimização de alocações no Heap: O caminho crítico não deve alocar strings ou structs dinamicamente na memória livre a cada medição.
3. Agregação em Janela Temporal e Descarregamento (Flush)
- A cada ciclo de 10 segundos (governado por um
time.Ticker), o agregador deve isolar as métricas coletadas na janela anterior e preparar o envio em lote (batch flush). - Sem perda de dados durante o envio: Enquanto o lote da janela anterior está sendo serializado e enviado pela rede, as novas métricas que continuam chegando devem ser imediatamente gravadas em uma nova janela ativa, sem que ocorra bloqueio de escrita.
Exemplo de arquitetura com troca atômica de buffers em Go
package aggregator
import (
"context"
"fmt"
"sync"
"sync/atomic"
"time"
)
type MetricType int
const (
TypeCounter MetricType = iota
TypeGauge
)
type MetricSample struct {
Name string
Type MetricType
Value float64
}
type WindowBucket struct {
mu sync.Mutex
counters map[string]int64
gauges map[string]float64
}
func newWindowBucket() *WindowBucket {
return &WindowBucket{
counters: make(map[string]int64),
gauges: make(map[string]float64),
}
}
type MetricAggregator struct {
activeBucket atomic.Pointer[WindowBucket]
flushChan chan *WindowBucket
pool sync.Pool
}
func NewMetricAggregator(ctx context.Context, flushInterval time.Duration) *MetricAggregator {
agg := &MetricAggregator{
flushChan: make(chan *WindowBucket, 10),
pool: sync.Pool{
New: func() any {
return newWindowBucket()
},
},
}
initialBucket := agg.pool.Get().(*WindowBucket)
agg.activeBucket.Store(initialBucket)
go agg.startFlusher(ctx, flushInterval)
go agg.networkSender(ctx)
return agg
}
// Ingest é o método de alta frequência chamado pelas goroutines do sistema
func (a *MetricAggregator) Ingest(sample MetricSample) {
bucket := a.activeBucket.Load()
bucket.mu.Lock()
defer bucket.mu.Unlock()
if sample.Type == TypeCounter {
bucket.counters[sample.Name] += int64(sample.Value)
} else if sample.Type == TypeGauge {
bucket.gauges[sample.Name] = sample.Value
}
}
// startFlusher faz a rotação atômica dos baldes a cada intervalo de tempo
func (a *MetricAggregator) startFlusher(ctx context.Context, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
// 1. Obtém um novo balde limpo da pool de reciclagem
newBucket := a.pool.Get().(*WindowBucket)
// 2. Troca atômica do balde ativo (zero downtime e zero bloqueio)
oldBucket := a.activeBucket.Swap(newBucket)
// 3. Envia o balde antigo para processamento e envio em segundo plano
a.flushChan <- oldBucket
}
}
}
// networkSender simula a serialização e transmissão pela rede sem travar a ingestão
func (a *MetricAggregator) networkSender(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case bucket := <-a.flushChan:
a.sendBatch(bucket)
// Limpa as estruturas e devolve à pool de reciclagem
bucket.mu.Lock()
clear(bucket.counters)
clear(bucket.gauges)
bucket.mu.Unlock()
a.pool.Put(bucket)
}
}
}
func (a *MetricAggregator) sendBatch(bucket *WindowBucket) {
bucket.mu.Lock()
defer bucket.mu.Unlock()
// Simula envio em lote de métricas consolidadas
fmt.Printf("[Flush] Enviando lote com %d contadores e %d medidores.\n",
len(bucket.counters), len(bucket.gauges))
}
O que os avaliadores da Datadog analisam
- Troca atômica de ponteiros (
atomic.Pointer): O uso deatomic.Pointerouatomic.SwapPointergarante que a substituição do balde no momento do flush ocorra em uma única instrução de processador, garantindo que as goroutines que estão chamandoIngest()comecem a escrever no novo balde imediatamente. - Reciclagem de estruturas com
sync.Pool: Reutilizar os mapas de métricas já alocados em vez de recriarmake(map[string]int64)a cada 10 segundos reduz o churn de memória e evita que o Garbage Collector cause pausas indesejadas no agente. - Benchmarks de micro-alocação (
b.ReportAllocs()): Os avaliadores executam testes de estresse comtesting.Bsimulando 50 goroutines inserindo milhões de métricas simultâneas para comprovar que o sistema opera sem travamentos e com uso previsível de memória.