06 de jul. de 2025·8 min de leitura

Profile de memória em Go para picos de tráfego: walkthrough do pprof

Profiling de memória em Go ajuda a lidar com picos de tráfego. Um walkthrough prático com pprof para identificar pontos quentes de alocação em JSON, scanneamento de BD e middleware.

Profile de memória em Go para picos de tráfego: walkthrough do pprof

O que picos súbitos de tráfego fazem à memória de um serviço Go

Um “pico de memória” em produção raramente significa que um único número subiu e pronto. Você pode ver o RSS (memória do processo) subir rápido enquanto o heap do Go mal se move, ou o heap crescer e cair em ondas agudas conforme o GC roda. Ao mesmo tempo, a latência costuma piorar porque o runtime passa mais tempo limpando.

Padrões comuns nas métricas:

  • RSS sobe mais rápido do que o esperado e às vezes não cai totalmente depois do pico
  • Heap in-use sobe, depois cai em ciclos agudos conforme o GC roda mais frequentemente
  • Taxa de alocação salta (bytes alocados por segundo)
  • Tempo de pause do GC e CPU usada pelo GC aumentam, mesmo se cada pausa for curta
  • Latência das requisições sobe e a latência nas caudas fica ruidosa

Picos de tráfego ampliam alocações por requisição porque desperdício “pequeno” escala linearmente com a carga. Se uma requisição aloca 50 KB a mais (buffers temporários de JSON, objetos por linha escaneada, dados de contexto de middleware), então a 2.000 requisições por segundo você está alimentando o alocador com cerca de 100 MB por segundo. Go aguenta muito, mas o GC ainda precisa rastrear e liberar esses objetos de vida curta. Quando a alocação supera a limpeza, a meta do heap cresce, o RSS segue, e você pode bater nos limites de memória.

Os sintomas são familiares: OOMs do orquestrador, saltos de latência, mais tempo gasto no GC e um serviço que parece “ocupado” mesmo com CPU livre. Também dá pra ter thrash de GC: o serviço fica no ar mas fica alocando e coletando sem parar, reduzindo a taxa de transferência justamente quando mais precisa.

pprof ajuda a responder uma pergunta rápido: quais caminhos de código alocam mais e essas alocações são necessárias? Um heap profile mostra o que está retido agora. Vistas focadas em alocação (como alloc_space) mostram o que está sendo criado e descartado.

O que o pprof não vai fazer é explicar cada byte do RSS. RSS inclui mais que o heap do Go (stacks, metadados do runtime, mapeamentos do SO, alocações cgo, fragmentação). pprof é melhor para apontar pontos quentes de alocação no seu código Go, não para provar um total exato de memória do contêiner.

Configurar o pprof com segurança (passo a passo)

pprof é mais fácil de usar como endpoints HTTP, mas esses endpoints podem revelar muita coisa sobre o seu serviço. Trate-os como um recurso de administração, não como uma API pública.

1) Adicione endpoints pprof

Em Go, a configuração mais simples é rodar pprof em um servidor admin separado. Isso mantém as rotas de profiling longe do seu router e middleware principais.

package main

import (
	"log"
	"net/http"
	_ "net/http/pprof"
)

func main() {
	go func() {
		// Admin only: bind to localhost
		log.Println(http.ListenAndServe("127.0.0.1:6060", nil))
	}()

	// Your main server starts here...
	// http.ListenAndServe(":8080", appHandler)
	select {}
}

Se você não pode abrir uma segunda porta, pode montar as rotas do pprof no servidor principal, mas é mais fácil expô-las acidentalmente. Uma porta admin separada é o padrão mais seguro.

2) Restringa antes de deployar

Comece com controles que são difíceis de errar. Fazer bind em localhost significa que os endpoints não são alcançáveis pela internet, a menos que alguém também exponha essa porta.

Uma checklist rápida:

  • Rode pprof em uma porta admin, não na porta principal voltada ao usuário
  • Faça bind em 127.0.0.1 (ou em uma interface privada) em produção
  • Adicione uma allowlist na borda de rede (VPN, bastião ou sub-rede interna)
  • Exija auth se sua borda puder aplicar isso (basic auth ou token)
  • Verifique que você pode buscar os perfis que realmente vai usar: heap, allocs, goroutine

3) Build e rollout seguros

Mantenha a mudança pequena: adicione pprof, envie e confirme que ele é alcançável apenas de onde você espera. Se tiver staging, teste lá primeiro simulando carga e capturando um heap e um allocs profile.

Em produção, faça rollout gradual (uma instância ou uma pequena fatia do tráfego). Se pprof estiver mal configurado, o raio de ação fica pequeno enquanto você corrige.

Capturar os perfis certos durante um pico

Durante um pico, um único snapshot raramente é suficiente. Capture uma pequena linha do tempo: alguns minutos antes do pico (baseline), durante o pico (impacto) e alguns minutos depois (recuperação). Isso torna mais fácil separar mudanças reais de alocação do comportamento normal de warm-up.

Se você consegue reproduzir o pico com carga controlada, combine com a produção o mais fielmente possível: mix de requisições, tamanhos de payload e concorrência. Um pico de requisições pequenas se comporta muito diferente de um pico de grandes respostas JSON.

Pegue tanto um heap profile quanto um profile focado em alocações. Eles respondem perguntas diferentes:

  • Heap (inuse) mostra o que está vivo e segurando memória agora
  • Alocações (alloc_space ou alloc_objects) mostram o que está sendo alocado intensamente, mesmo que seja liberado rápido

Um padrão prático de captura: pegue um heap profile, depois um allocation profile, e repita 30 a 60 segundos depois. Dois pontos durante o pico ajudam a ver se um caminho suspeito é estável ou está acelerando.

# examples: adjust host/port and timing to your setup
curl -o heap_during.pprof "http://127.0.0.1:6060/debug/pprof/heap"
curl -o allocs_30s.pprof "http://127.0.0.1:6060/debug/pprof/allocs?seconds=30"

Paralelamente aos arquivos pprof, registre algumas estatísticas de runtime para explicar o que o GC estava fazendo ao mesmo tempo. Tamanho do heap, número de GCs e tempo de pause geralmente bastam. Mesmo uma linha curta de log em cada momento de captura ajuda a correlacionar “as alocações subiram” com “o GC começou a rodar constantemente”.

Mantenha notas do incidente enquanto avança: versão da build (commit/tag), versão do Go, flags importantes, mudanças de configuração e que tráfego estava ocorrendo (endpoints, tenants, tamanhos de payload). Esses detalhes muitas vezes importam quando você compara perfis e percebe que o mix de requisições mudou.

Como ler perfis de heap e alocações

Um heap profile responde perguntas diferentes dependendo da vista.

Inuse space mostra o que ainda está em memória no momento da captura. Use para leaks, caches de longa duração ou requisições que deixam objetos para trás.

Alloc space (alocações totais) mostra o que foi alocado ao longo do tempo, mesmo que tenha sido liberado rápido. Use quando picos causam muito trabalho do GC, saltos de latência ou OOMs por churn.

A amostragem importa. Go não registra toda alocação. Ele amostra alocações (controlado por runtime.MemProfileRate), então alocações pequenas e frequentes podem ser subrepresentadas e os números são estimativas. Ainda assim, os maiores culpados costumam se destacar, especialmente em condições de pico. Procure tendências e os principais contribuintes, não contabilidade perfeita.

As vistas pprof mais úteis:

  • top: leitura rápida de quem domina inuse ou alloc (verifique flat e cumulative)
  • list : fontes de alocação no nível de linha dentro de uma função quente
  • graph: caminhos de chamada que explicam como você chegou lá

Diffs é onde fica prático. Compare um profile baseline (tráfego normal) com um profile do pico para destacar o que mudou, em vez de perseguir ruídos de fundo.

Valide achados com uma pequena mudança antes de um grande refactor:

  • Reutilize um buffer (ou adicione um pequeno sync.Pool) no caminho quente
  • Corte a criação de objetos por requisição (por exemplo, evite construir maps intermediários para JSON)
  • Re-perfilar sob a mesma carga e confirmar que o diff encolheu onde você esperava

Se os números mudarem do jeito certo, você encontrou a causa real, não só um relatório assustador.

Encontrar pontos quentes de alocação em encoding JSON

One stack for full apps
Create web and mobile apps alongside your backend, with one place to manage logic and data.
Build App

Durante picos, trabalho com JSON pode virar uma conta de memória importante porque roda em cada requisição. Pontos quentes de JSON costumam aparecer como muitas pequenas alocações que forçam mais o GC.

Sinais de alerta no pprof

Se o heap ou a vista de alocação apontarem para encoding/json, olhe com atenção o que você passa para ele. Esses padrões costumam inflar alocações:

  • Usar map[string]any (ou []any) para respostas em vez de structs tipadas
  • Marshaling do mesmo objeto várias vezes (por exemplo, para log e para resposta)
  • Pretty printing com json.MarshalIndent em produção
  • Construir JSON através de strings temporárias (fmt.Sprintf, concatenação de strings) antes de marshalar
  • Converter grandes []byte para string (ou vice-versa) só para casar com uma API

json.Marshal sempre aloca um novo []byte para toda a saída. json.NewEncoder(w).Encode(v) normalmente evita esse grande buffer porque escreve em um io.Writer, mas ainda pode alocar internamente, especialmente se v estiver cheio de any, maps ou estruturas cheias de ponteiros.

Correções rápidas e experimentos rápidos

Comece com structs tipadas para a forma da sua resposta. Elas reduzem trabalho de reflexão e evitam boxing por interface por campo.

Depois, remova temporários por requisição evitáveis: reutilize bytes.Buffer via sync.Pool (com cuidado), não indente em produção e não remarshal só para logs.

Pequenos experimentos que confirmam que JSON é o culpado:

  • Substitua map[string]any por um struct em um endpoint quente e compare perfis
  • Troque Marshal por Encoder escrevendo direto para a resposta
  • Remova MarshalIndent ou formatações de debug em produção e re-perfilar sob a mesma carga
  • Pule encoding JSON para respostas em cache e meça a queda

Encontrar pontos quentes em scanneamento de consultas

Quando a memória salta durante um pico, leituras do banco são uma surpresa comum. É fácil focar no tempo de SQL, mas o passo de scan pode alocar muito por linha, especialmente quando você escaneia em tipos flexíveis.

Ofensores comuns:

  • Scannear em interface{} (ou map[string]any) e deixar o driver decidir os tipos
  • Converter []byte para string para cada campo
  • Usar wrappers nulos (sql.NullString, sql.NullInt64) em grandes conjuntos de resultados
  • Puxar colunas grandes de texto/blob que nem sempre precisa

Um padrão que queima memória silenciosamente é escanear dados de linha em variáveis temporárias e depois copiar para uma struct real (ou construir um map por linha). Se você puder scannear direto em uma struct com campos concretos, evita alocações extras e verificações de tipo.

Tamanho de batch e paginação mudam sua forma de memória. Buscar 10.000 linhas em um slice aloca para o crescimento do slice e para cada linha, tudo de uma vez. Se o handler só precisa de uma página, empurre isso para a query e mantenha o page size estável. Se precisar processar muitas linhas, faça streaming e agregue resumos pequenos em vez de armazenar cada linha.

Campos de texto grandes precisam de cuidado. Muitos drivers retornam texto como []byte. Converter isso para string copia os dados, então fazer isso para cada linha pode explodir alocações. Se você só precisa do valor às vezes, atrase a conversão ou scanneie menos colunas para esse endpoint.

Para confirmar se o driver ou seu código está alocando mais, veja o que domina seus perfis:

  • Se frames apontam para seu código de mapeamento, foque em alvos de scan e conversões
  • Se frames apontam para database/sql ou o driver, reduza linhas e colunas primeiro, depois considere opções específicas do driver
  • Confira tanto alloc_space quanto alloc_objects; muitas alocações pequenas podem ser piores que algumas grandes

Exemplo: um endpoint “list orders” faz SELECT * em []map[string]any. Durante um pico, cada requisição constrói milhares de pequenos maps e strings. Mudar a query para selecionar só colunas necessárias e scannear em []Order{ID int64, Status string, TotalCents int64} costuma reduzir alocações imediatamente. A mesma ideia vale se você estiver profileando um backend Go gerado pelo AppMaster: o ponto quente geralmente está em como você forma e scanneia os dados, não no banco em si.

Padrões de middleware que alocam por requisição sem perceber

Cut churn at the source
Model data, APIs, and logic without hand-writing boilerplate that adds allocations.
Start Building

Middleware parece barato porque é “só um wrapper”, mas roda em cada requisição. Durante um pico, pequenas alocações por requisição somam rápido e aparecem como aumento da taxa de alocação.

Middleware de logging é fonte comum: formatar strings, construir maps de campos ou copiar headers para uma saída mais bonita. Helpers de request ID podem alocar ao gerar um ID, convertê-lo para string e então anexá-lo ao contexto. Até context.WithValue pode alocar se você armazenar novos objetos (ou novas strings) em cada requisição.

Compressão e manipulação do body são outros culpados frequentes. Se o middleware lê todo o body para “espiar” ou validar, você pode acabar com um buffer grande por requisição. Middleware gzip pode alocar muito se criar novos readers e writers toda vez em vez de reutilizar buffers.

Camadas de auth e sessão são similares. Se cada requisição faz parse de tokens, decodifica cookies em base64 ou carrega blobs de sessão em structs frescas, você tem churn constante mesmo quando o handler faz pouco.

Tracing e métricas podem alocar mais do que o esperado quando labels são construídos dinamicamente. Concatenar nomes de rotas, user agents ou tenant IDs em novas strings por requisição é um custo escondido clássico.

Padrões que frequentemente aparecem como “morte por mil cortes”:

  • Montar linhas de log com fmt.Sprintf e novos map[string]any por requisição
  • Copiar headers em novos maps ou slices para log ou assinatura
  • Alocar novos buffers e readers/writers gzip em vez de fazer pooling
  • Criar labels de métricas de alta cardinalidade (muitas strings únicas)
  • Armazenar novas structs no contexto a cada requisição

Para isolar custo de middleware, compare dois perfis: um com a cadeia completa ativada e outro com middleware temporariamente desabilitado ou substituído por no-op. Um teste simples é um endpoint de health que deveria ser quase livre de alocações. Se /health aloca muito durante um pico, o handler não é o problema.

Se você gera backends Go com o AppMaster, a mesma regra vale: mantenha recursos transversais (logging, auth, tracing) mensuráveis e trate alocações por requisição como um orçamento a auditar.

Correções que normalmente valem a pena rapidamente

Offload work with internal apps
Create internal tools that reduce pressure on your core API during traffic spikes.
Try AppMaster

Uma vez que você tenha vistas de heap e allocs do pprof, priorize mudanças que reduzam alocações por requisição. O objetivo não é truques sofisticados, é fazer o caminho quente criar menos objetos de vida curta, especialmente sob carga.

Comece com ganhos seguros e pouco glamourosos

Se tamanhos são previsíveis, pré-alocar. Se um endpoint normalmente retorna cerca de 200 itens, crie o slice com capacidade 200 para que ele não cresça e copie várias vezes.

Evite construir strings em caminhos quentes. fmt.Sprintf é conveniente, mas costuma alocar. Para logging, prefira campos estruturados e reutilize um pequeno buffer quando fizer sentido.

Se você gera respostas JSON grandes, considere fazer streaming em vez de construir um enorme []byte ou string na memória. Um padrão comum de pico é: a requisição chega, você lê um body grande, monta uma resposta grande, a memória sobe até o GC alcançar tudo.

Mudanças rápidas que tipicamente aparecem claramente em perfis antes/depois:

  • Pré-alocar slices e maps quando souber a faixa de tamanho
  • Substituir formatações heavy de fmt no tratamento de requisições por alternativas mais baratas
  • Streamar grandes respostas JSON (encode direto para o response writer)
  • Usar sync.Pool para objetos reutilizáveis e de mesma forma (buffers, encoders) e devolvê-los consistentemente
  • Definir limites de requisição (tamanho do body, payload, page size) para limitar piores casos

Use sync.Pool com cuidado

sync.Pool ajuda quando você aloca repetidamente a mesma coisa, como um bytes.Buffer por requisição. Também pode atrapalhar se você poolar objetos com tamanhos imprevisíveis ou esquecer de resetá-los, o que mantém grandes arrays backing vivos.

Meça antes e depois usando a mesma carga:

  • Capture um allocs profile durante a janela de pico
  • Aplique uma mudança por vez
  • Reproduza o mesmo mix de requisições e compare allocs/op totais
  • Observe latência nas caudas, não só memória

Se você gera backends Go pelo AppMaster, essas correções ainda se aplicam ao código customizado ao redor de handlers, integrações e middleware. É aí que alocações induzidas por picos costumam se esconder.

Erros comuns com pprof e falsos alarmes

A maneira mais rápida de perder um dia é otimizar a coisa errada. Se o serviço está lento, comece com CPU. Se está sendo morto por OOM, comece pelo heap. Se sobrevive mas o GC está rodando sem parar, olhe a taxa de alocação e o comportamento do GC.

Outra armadilha é olhar só o “top” e achar que está resolvido. “Top” esconde contexto. Sempre inspecione stacks de chamada (ou um flame graph) para ver quem chamou o alocador. A correção costuma estar uma ou duas frames acima da função quente.

Também fique atento à confusão entre inuse e churn. Uma requisição pode alocar 5 MB de objetos de vida curta, disparar GC extra e terminar com só 200 KB inuse. Se você olhar só o inuse, perde o churn. Se olhar só as alocações totais, pode otimizar algo que nunca fica residente e não importa para risco de OOM.

Checagens rápidas antes de mudar código:

  • Confirme a vista certa: heap inuse para retenção, alloc_space/alloc_objects para churn
  • Compare stacks, não só nomes de função (encoding/json costuma ser sintoma)
  • Reproduza tráfego realisticamente: mesmos endpoints, tamanhos de payload, headers, concorrência
  • Capture baseline e profile do pico, então diff-os

Testes de carga irreais causam falsos alarmes. Se seu teste envia bodies tiny mas a produção envia payloads de 200 KB, você vai otimizar o caminho errado. Se seu teste retorna uma linha, nunca verá o comportamento de scan que aparece com 500 linhas.

Não persiga ruído. Se uma função aparece só no profile do pico (não no baseline), é um forte indício. Se aparece em ambos no mesmo nível, pode ser trabalho de fundo normal.

Um walkthrough realista de incidente

Keep code clean as you iterate
Regenerate clean Go code when requirements change, without leaving old memory-heavy paths behind.
Generate Code

Uma promoção na segunda de manhã sai e sua API Go começa a receber 8x o tráfego normal. O primeiro sintoma não é um crash. RSS sobe, o GC fica mais ativo e a latência p95 dispara. O endpoint mais quente é GET /api/orders porque o app móvel atualiza essa rota em cada abertura de tela.

Você pega dois snapshots: um em um momento calmo (baseline) e outro durante o pico. Capture o mesmo tipo de heap profile em ambos para manter a comparação justa.

O fluxo que funciona no momento:

  • Pegue um heap profile baseline e anote RPS, RSS e p95 atuais
  • Durante o pico, pegue outro heap profile mais um allocation profile na mesma janela de 1 a 2 minutos
  • Compare os maiores alocadores entre os dois e foque no que mais cresceu
  • Caminhe da maior função até seus callers até chegar no handler
  • Faça uma pequena mudança, redeploy em uma única instância e re-profile

Neste caso, o profile do pico mostrou que a maioria das novas alocações vinha do encoding JSON. O handler montava map[string]any por linha e chamava json.Marshal em um slice de maps. Cada requisição criava muitas strings de vida curta e valores boxed em interface.

A correção mais segura e pequena foi parar de construir maps. Scannear linhas do banco direto em uma struct tipada e codificar esse slice. Nada mais mudou: mesmos campos, mesmo formato de resposta, mesmos códigos de status. Depois de rodar a mudança em uma instância, as alocações no caminho JSON caíram, o tempo do GC diminuiu e a latência estabilizou.

Só então você faz rollout gradual acompanhando memória, GC e taxas de erro. Se você gera serviços numa plataforma low-code como AppMaster, isso lembra de manter modelos de resposta tipados e consistentes para evitar custos de alocação escondidos.

Próximos passos para evitar o próximo pico de memória

Depois de estabilizar um pico, torne o próximo chato. Trate profiling como um exercício repetível.

Escreva um runbook curto que sua equipe possa seguir cansada. Deve dizer o que capturar, quando capturar e como comparar com um baseline conhecido. Seja prático: comandos exatos, onde guardar perfis e o que é “normal” para seus principais alocadores.

Adicione monitoramento leve para pressão de alocação antes de chegar a OOM: tamanho do heap, ciclos de GC por segundo e bytes alocados por requisição. Detectar “alocações por requisição subiram 30% semana a semana” muitas vezes é mais útil do que esperar por um alarme rígido de memória.

Empurre checagens para mais cedo com um pequeno teste de carga no CI em um endpoint representativo. Pequenas mudanças na resposta podem dobrar alocações se acionarem cópias extras, e é melhor descobrir isso antes do tráfego de produção.

Se você roda um backend gerado, exporte o código-fonte e profile da mesma forma. Código gerado ainda é código Go, e pprof vai apontar funções e linhas reais.

Se seus requisitos mudam com frequência, AppMaster (appmaster.io) pode ser uma forma prática de reconstruir e regenerar backends Go limpos conforme a app evolui, e então profile o código exportado sob carga realista antes de entregar.

FAQ

Por que um pico repentino de tráfego faz a memória disparar mesmo que meu código não tenha mudado?

Um pico normalmente aumenta a taxa de alocações mais do que você espera. Mesmo objetos temporários pequenos por requisição somam linearmente com o RPS, forçando o GC a rodar mais e podendo elevar o RSS mesmo que o heap “vivo” não esteja grande.

Por que o RSS cresce enquanto o heap do Go parece estável?

As métricas de heap acompanham a memória gerida pelo Go, mas o RSS inclui mais coisas: stacks de goroutines, metadados do runtime, mapeamentos do SO, fragmentação e alocações fora do heap (incluindo algum uso de cgo). É normal que RSS e heap se movam de forma diferente durante picos; use pprof para localizar pontos quentes de alocação em vez de tentar “bater” exatamente o RSS.

Devo olhar heap ou alloc profiles primeiro durante um pico?

Comece com um heap profile quando suspeitar de retenção (algo ficando vivo) e com um profile focado em alocações (como allocs/alloc_space) quando suspeitar de churn (muitos objetos de vida curta). Em picos de tráfego, churn costuma ser o problema real porque ele consome CPU do GC e aumenta a latência nas caudas.

Qual é a forma mais segura de expor pprof em produção?

A forma mais simples e segura é rodar pprof em um servidor admin separado ligado a 127.0.0.1 e deixá-lo acessível apenas internamente. Trate pprof como uma interface de administração porque ele expõe detalhes internos do serviço.

Quantos perfis devo capturar e quando?

Capture uma pequena linha do tempo: um profile alguns minutos antes do pico (baseline), um durante o pico (impacto) e outro depois (recuperação). Isso facilita ver o que mudou em vez de perseguir alocações normais de fundo.

Qual é a diferença entre inuse e alloc_space no pprof?

Use inuse para encontrar o que está realmente retido no momento da captura, e alloc_space (ou alloc_objects) para ver o que está sendo criado intensamente. Um erro comum é olhar só o inuse e perder o churn que causa thrash do GC e picos de latência.

Quais são as maneiras mais rápidas de reduzir alocações relacionadas a JSON?

Se encoding/json dominar as alocações, o culpado usual é a forma dos dados, não o pacote em si. Substituir map[string]any por structs tipadas, evitar json.MarshalIndent e não construir JSON via strings temporárias costuma reduzir alocações imediatamente.

Por que o escaneamento de consultas de banco de dados pode explodir a memória durante picos?

Escanear linhas em alvos flexíveis como interface{} ou map[string]any, converter []byte para string para muitos campos e buscar muitas linhas/colunas pode alocar muito por requisição. Selecionar só as colunas necessárias, paginar resultados e escanear direto em campos concretos de struct são correções de alto impacto.

Quais padrões de middleware normalmente causam alocações do tipo “death by a thousand cuts”?

Middleware roda em todas as requisições, então pequenas alocações viram um problema sob carga. Logs que montam novas strings, tracing com labels de alta cardinalidade, geração de request ID, leitores/escritores gzip criados por requisição e valores de contexto que armazenam novos objetos aparecem como churn constante nas profiles.

Posso usar esse workflow de pprof em backends Go gerados pelo AppMaster?

Sim. A mesma abordagem guiada por perfil vale para qualquer código Go, gerado ou escrito à mão. Se você exportar o código gerado, pode rodar pprof, localizar as chamadas que alocam e ajustar modelos, handlers e lógica transversal para reduzir alocações por requisição antes do próximo pico.

Fácil de começar
Criar algo espantoso

Experimente o AppMaster com plano gratuito.
Quando estiver pronto, você poderá escolher a assinatura adequada.

Comece