Fundamentos de Programação/03 - Sistemas Reais/Semana 10 - Rede e HTTP6 min
Ciclo de uma Requisição
- Enumere tudo entre digitar a URL e ver o pixel: DNS, TCP, TLS, requisição, resposta, render.
- Em qual dessas etapas costuma morar a latência que o usuário sente?
- Quantos round-trips uma conexão HTTPS nova custa antes do primeiro byte útil?
- O que keep-alive e connection pooling economizam exatamente?
Conceito
Entre digitar a URL e ver o pixel, a sequência completa:
| # | Etapa | Custo típico |
|---|---|---|
| 1 | parse da URL — esquema, host, porta, caminho | ~0 |
| 2 | DNS — nome → IP (com cache em vários níveis) | 0 (cache) a 100 ms |
| 3 | TCP handshake — SYN, SYN-ACK, ACK | 1 RTT |
| 4 | TLS handshake — negociação, certificado, chaves | 1 RTT (1.3) ou 2 (1.2) |
| 5 | envio da requisição | 0,5 RTT |
| 6 | trabalho do servidor — sua aplicação e o banco | variável |
| 7 | resposta trafega | proporcional ao tamanho |
| 8 | parse e render no cliente | variável |
Quase sempre nos round trips, não no seu código.
Uma conexão HTTPS nova custa, antes do primeiro byte útil: DNS + TCP (1 RTT) + TLS (1 RTT em 1.3) ≈ 3 idas e voltas.
Com 50 ms de RTT — que é um valor normal para intercontinental — são 150 ms gastos antes de o servidor ver a requisição. Nenhuma otimização de query resolve isso.
O que keep-alive e connection pooling economizam: exatamente as etapas 2, 3 e 4. Reusar uma conexão elimina DNS, TCP e TLS — a requisição seguinte começa direto na etapa 5. É a otimização de rede de maior retorno, e é gratuita.
HTTP/2 multiplexa vários fluxos na mesma conexão TCP, eliminando o head-of-line blocking no nível HTTP (e o hack de abrir 6 conexões por domínio). HTTP/3 (sobre QUIC, que é UDP) elimina o blocking também no transporte, e funde o handshake TLS com o de conexão — 1 RTT total, ou 0 na retomada.
Na prática
Medido com net/http/httptrace contra um servidor TLS local (RTT ≈ 0, então os números
isolam o custo de CPU de cada fase, não a rede):
TLS: versao=TLS 1.3 cifra=TLS_AES_128_GCM_SHA256
fases da requisicao:
TCP connect: 223µs
TLS handshake: 3.889ms <- 17x o custo do TCP connect
espera do servidor: 20.833ms
total: 25.078ms
segunda requisicao (keep-alive): 20.51ms <- economizou ~4.1ms
Duas leituras:
- O handshake TLS custa 17× o TCP connect mesmo em loopback, porque é criptografia de chave pública (3. TLS). Em rede real, somam-se os RTTs.
- Keep-alive economizou exatamente o setup (25,1 ms → 20,5 ms). Os 20,5 ms restantes são o trabalho simulado do servidor — irredutível por rede.
Em Go
httptrace te dá gancho para cada fase, e é a ferramenta certa quando alguém diz "a API está
lenta" e você precisa saber onde:
trace := &httptrace.ClientTrace{
DNSDone: func(httptrace.DNSDoneInfo) { /* ... */ },
ConnectDone: func(net, addr string, err error) { /* ... */ },
TLSHandshakeDone: func(tls.ConnectionState, error) { /* ... */ },
GotFirstResponseByte: func() { /* TTFB */ },
GotConn: func(i httptrace.GotConnInfo) { /* i.Reused! */ },
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
GotConnInfo.Reused responde diretamente "essa requisição reusou conexão?" — é como você
prova que o pool está funcionando.
http.Client que mais custam em produção Go1. http.Client sem timeout nunca desiste. O zero value não tem prazo algum:
resp, err := http.Get(url) // pode pendurar para sempre
Um servidor que aceita a conexão e não responde segura sua goroutine e sua conexão indefinidamente. Sempre:
cli := &http.Client{Timeout: 10 * time.Second}
2. MaxIdleConnsPerHost é 2 por padrão. Com concorrência alta contra um único host,
o pool guarda só duas conexões ociosas e descarta as demais — então você paga
TCP+TLS de novo constantemente. Em serviço que fala com outro serviço, isto é o gargalo
escondido mais comum:
t := http.DefaultTransport.(*http.Transport).Clone()
t.MaxIdleConns = 100
t.MaxIdleConnsPerHost = 100
t.IdleConnTimeout = 90 * time.Second
cli := &http.Client{Transport: t, Timeout: 10 * time.Second}
E a regra que devolve a conexão ao pool: você precisa ler o corpo até o fim e fechá-lo. Fechar sem drenar descarta a conexão:
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body) // drena para permitir reuso
Reuse o http.Client. Criar um por requisição cria um Transport por requisição, e
portanto um pool novo e vazio — anulando o keep-alive inteiro. Um cliente por dependência,
guardado em campo de struct.
Respostas às perguntas-guia
1. Enumere tudo entre digitar a URL e ver o pixel.
Parse da URL → DNS → TCP handshake → TLS handshake → requisição → trabalho do servidor → resposta → parse/render. A tabela acima com os custos.
2. Em qual dessas etapas costuma morar a latência que o usuário sente?
Nos round trips de estabelecimento de conexão, e no trabalho do servidor quando há 7. Problema N+1 no banco. Raramente na transferência.
Em Go: httptrace distingue as duas causas — se GotFirstResponseByte demora e o
handshake foi rápido, o problema é o servidor.
3. Quantos round-trips uma conexão HTTPS nova custa antes do primeiro byte útil?
DNS (0 se em cache) + TCP (1) + TLS (1 em TLS 1.3, 2 em 1.2) + envio (0,5) ≈ 3 idas e voltas. Com RTT de 50 ms, 150 ms antes de o servidor ver a requisição.
4. O que keep-alive e connection pooling economizam exatamente?
DNS, TCP handshake e TLS handshake. Medido: 4,1 ms em loopback; em rede real, 2 a 3 RTTs.
Em Go: e só funciona se você reusar o http.Client, drenar o corpo e ajustar
MaxIdleConnsPerHost.
Trade-offs
Do conceito:
- Keep-alive compra RTTs e cobra conexões abertas (recursos no servidor e no balanceador).
- HTTP/2 compra multiplexação e cobra que uma perda de pacote afete todos os fluxos daquela conexão TCP — problema que o HTTP/3 resolve.
- Pool grande compra latência e cobra memória e limite de conexões do backend.
Em Go:
http.DefaultClientcompra conveniência e não tem timeout: use só em script.- Pool ajustado compra throughput e cobra conexões ociosas.
httptracecusta algumas linhas e é a única forma de responder "onde está a latência" sem adivinhar.
Exemplo prático
package main
import (
"crypto/tls"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/http/httptrace"
"time"
)
func main() {
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(20 * time.Millisecond) // trabalho simulado do servidor
fmt.Fprint(w, "ok")
}))
defer srv.Close()
cliente := srv.Client() // confia no certificado do servidor de teste
cliente.Timeout = 5 * time.Second
medir := func(rotulo string) {
var t0, tConn, tTLS, tFirst time.Time
var reusou bool
t0 = time.Now()
trace := &httptrace.ClientTrace{
GotConn: func(i httptrace.GotConnInfo) { reusou = i.Reused },
ConnectDone: func(_, _ string, _ error) { tConn = time.Now() },
TLSHandshakeDone: func(cs tls.ConnectionState, _ error) { tTLS = time.Now() },
GotFirstResponseByte: func() { tFirst = time.Now() },
}
req, _ := http.NewRequest("GET", srv.URL, nil)
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
resp, err := cliente.Do(req)
if err != nil {
fmt.Println("erro:", err)
return
}
io.Copy(io.Discard, resp.Body) // DRENA: sem isto a conexão não volta ao pool
resp.Body.Close()
fmt.Printf("%-22s total=%-11v reusou_conexao=%v\n",
rotulo, time.Since(t0).Round(time.Microsecond), reusou)
if !reusou {
fmt.Printf(" TCP=%v TLS=%v espera_servidor=%v\n",
tConn.Sub(t0).Round(time.Microsecond),
tTLS.Sub(tConn).Round(time.Microsecond),
tFirst.Sub(tTLS).Round(time.Microsecond))
}
}
medir("1a (conexão nova)")
medir("2a (keep-alive)")
medir("3a (keep-alive)")
// o erro: cliente novo por requisição => pool novo e vazio => sem keep-alive
fmt.Println("\ncliente NOVO por requisição (anula o pool):")
for i := 1; i <= 2; i++ {
t := srv.Client().Transport
c := &http.Client{Transport: t, Timeout: 5 * time.Second}
t0 := time.Now()
var reusou bool
req, _ := http.NewRequest("GET", srv.URL, nil)
req = req.WithContext(httptrace.WithClientTrace(req.Context(),
&httptrace.ClientTrace{GotConn: func(i httptrace.GotConnInfo) { reusou = i.Reused }}))
r, err := c.Do(req)
if err == nil {
io.Copy(io.Discard, r.Body)
r.Body.Close()
}
fmt.Printf(" req %d: total=%-11v reusou=%v\n", i,
time.Since(t0).Round(time.Microsecond), reusou)
}
}
Relacionado
- 3. TLS — a fase mais cara do estabelecimento
- 1. Modelo Cliente-Servidor — quem inicia o ciclo
- 7. Problema N+1 — semana 9, quando a latência está no servidor e é multiplicada
- 6. Headers HTTP — cache evita o ciclo inteiro
Parte de Semana 10 - Rede e HTTP · 00 - MOC Fundamentos de Programação