Pular para o conteúdo

Desafios de entrevista em Go · parte 11 de 11

Datadog - High-Throughput Metric Aggregator Daemon

avançadoPublicada em · 4 min de leitura

Tamanho da fonte:
Largura da coluna:

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

  1. 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.
  2. 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

  1. Troca atômica de ponteiros (atomic.Pointer): O uso de atomic.Pointer ou atomic.SwapPointer garante 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 chamando Ingest() comecem a escrever no novo balde imediatamente.
  2. Reciclagem de estruturas com sync.Pool: Reutilizar os mapas de métricas já alocados em vez de recriar make(map[string]int64) a cada 10 segundos reduz o churn de memória e evita que o Garbage Collector cause pausas indesejadas no agente.
  3. Benchmarks de micro-alocação (b.ReportAllocs()): Os avaliadores executam testes de estresse com testing.B simulando 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.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?