diff --git a/.context/docs/README.md b/.context/docs/README.md index e73bc93..2dbcb81 100644 --- a/.context/docs/README.md +++ b/.context/docs/README.md @@ -5,6 +5,8 @@ Welcome to the repository knowledge base. Start with the project overview, then **Blog Posts Reference** The following blog posts have been added to the documentation index: +- [Understanding P99 and Tail Latency](../../content/en/blog/p99-tail-latency/index.md) (en) — Why the mean lies and how to eliminate tail latency bottlenecks in production +- [P99 e Latência de Cauda (Tail Latency)](../../content/pt-br/blog/p99-tail-latency/index.md) (pt-br) — Por que a média engana e como mitigar gargalos de latência de cauda em produção - [Understanding RFC 10008: The HTTP QUERY Method](../../content/en/blog/rfc-10008-http-query.md) (en) — Standardizing HTTP QUERY as a safe and idempotent method with a body payload - [Entendendo a RFC 10008: O Método HTTP QUERY](../../content/pt-br/blog/rfc-10008-http-query.md) (pt-br) — Padronizando o método HTTP QUERY como uma alternativa segura e idempotente com payload no corpo - [Google SRE Principles: Evolution from Traditional Operations to Site Reliability Engineering](../../content/en/blog/google-sre-principles.md) (en) — Comprehensive overview of Google SRE principles from the SRE Book @@ -47,6 +49,8 @@ The following blog posts have been added to the documentation index: | Tooling & Productivity Guide | `tooling.md` | CLI scripts, IDE configs, automation workflows | ## Recent Blog Posts +- [Understanding P99 and Tail Latency](../../content/en/blog/p99-tail-latency/index.md) (en) — Why the mean lies and how to eliminate tail latency bottlenecks in production +- [P99 e Latência de Cauda (Tail Latency)](../../content/pt-br/blog/p99-tail-latency/index.md) (pt-br) — Por que a média engana e como mitigar gargalos de latência de cauda em produção - [Understanding RFC 10008: The HTTP QUERY Method](../../content/en/blog/rfc-10008-http-query.md) (en) — Standardizing HTTP QUERY as a safe and idempotent method with a body payload - [Entendendo a RFC 10008: O Método HTTP QUERY](../../content/pt-br/blog/rfc-10008-http-query.md) (pt-br) — Padronizando o método HTTP QUERY como uma alternativa segura e idempotente com payload no corpo - [Understanding Site Reliability Engineering: Google's SRE Philosophy and Practices](../../content/en/blog/understanding-sre-google-sre-philosophy-practices.md) (en) — Deep dive into Google SRE principles: error budgets, progressive rollouts, and the engineering-first culture diff --git a/config/_default/languages.en.toml b/config/_default/languages.en.toml index 087b9e2..da29f9f 100644 --- a/config/_default/languages.en.toml +++ b/config/_default/languages.en.toml @@ -13,6 +13,9 @@ title = "EF-TECH" description = "Cloud computing solutions" copyright = "EF-TECH" + [params.homepage] + showMoreLinkDest = "/en/blog/" + [params.author] name = "EF-TECH" image = "img/logo.png" diff --git a/config/_default/languages.pt-br.toml b/config/_default/languages.pt-br.toml index 0776f12..6b2700b 100644 --- a/config/_default/languages.pt-br.toml +++ b/config/_default/languages.pt-br.toml @@ -13,6 +13,9 @@ title = "EF-TECH" description = "Soluções em cloud computing" copyright = "EF-TECH" + [params.homepage] + showMoreLinkDest = "/pt-br/blog/" + [params.author] name = "EF-TECH" image = "img/logo.png" diff --git a/config/_default/params.toml b/config/_default/params.toml index f558e26..ac1f1bf 100644 --- a/config/_default/params.toml +++ b/config/_default/params.toml @@ -61,7 +61,7 @@ forgejoDefaultServer = "https://v11.next.forgejo.org" showRecent = true showRecentItems = 5 showMoreLink = true - showMoreLinkDest = "/blog/" + # showMoreLinkDest is configured per-language in languages..toml cardView = false cardViewScreenWidth = false layoutBackgroundBlur = false # only used when layout equals background diff --git a/content/en/blog/p99-tail-latency/cover.png b/content/en/blog/p99-tail-latency/cover.png new file mode 100644 index 0000000..776bf14 Binary files /dev/null and b/content/en/blog/p99-tail-latency/cover.png differ diff --git a/content/en/blog/p99-tail-latency/index.md b/content/en/blog/p99-tail-latency/index.md new file mode 100644 index 0000000..676cbd5 --- /dev/null +++ b/content/en/blog/p99-tail-latency/index.md @@ -0,0 +1,299 @@ +--- +title: "Understanding P99 and Tail Latency: Why the Mean Lies and How to Eliminate Bottlenecks in Production" +description: "Learn what P99 is, how latency percentiles work (P50 to P99.9), why arithmetic mean hides critical issues in distributed systems, and how to identify and mitigate tail latency in production." +summary: "P99 (99th percentile) is one of the most critical metrics in Software Engineering, SRE, and Observability. Discover why averages mask outliers, how the fan-out effect in microservices amplifies slowness, and what architectural strategies — from aggressive timeouts to hedged requests and distributed tracing — solve tail latency." +date: 2026-08-22 +draft: false +tags: ["sre", "observability", "performance", "distributed-systems", "prometheus", "opentelemetry", "devops"] +categories: ["engineering"] +featureimage: "cover.png" +featureimagecaption: "P99 and Tail Latency — Percentile Metrics and Observability in Distributed Systems" +--- + +In large-scale systems and modern microservice architectures, one of the most common engineering pitfalls is relying on the **arithmetic mean (average)** to evaluate application health and response times. + +Stating that *"our API average response time is 60ms"* may sound comforting in executive status updates. However, lurking behind that seemingly pristine number, hundreds or thousands of users might be suffering from 3-to-10-second freezes, abandoning checkout carts, or triggering cascading timeouts across your backend. + +This is where **percentiles** come in — specifically **P99 (the 99th Percentile)** and the phenomenon known as **Tail Latency**. + +In this in-depth guide, we will explore the foundational statistical concepts, demystify why averages are misleading, demonstrate how microservice fan-out amplifies tail latency, and analyze root causes and battle-tested mitigation strategies used by leading SRE and performance engineering teams. + +![P99 and Tail Latency](cover.png) + +--- + +## 1. What Is a Percentile and How Does It Work? + +A **percentile** (or quantile) is a statistical measure indicating the value below which a given percentage of observations in a dataset falls. + +To calculate response time percentiles: +1. Collect all latency measurements over a specific time window (e.g., the last 5 minutes). +2. Sort all samples in ascending order from fastest to slowest. +3. Locate the value corresponding to the desired percentile rank. + +``` +Sorted samples (in ms): [ 12, 15, 18, 22, 25, ..., 180, 240, 890, 2400 ] + ▲ ▲ ▲ ▲ + P50 P90 P95 P99 +``` + +### Key Percentile Milestones in System Monitoring + +| Metric | Name | Technical Meaning | Real-World Representation | +| :--- | :--- | :--- | :--- | +| **P50** | Median | 50% of requests were faster than this value; 50% were slower. | The experience of the **typical/average user**. | +| **P90** | 90th Percentile | 90% of requests were faster; 10% were slower. | The threshold where degradation begins. | +| **P95** | 95th Percentile | 95% of requests were faster; 5% were slower. | Standard baseline for **first-tier alerting** and intermediate SLOs. | +| **P99** | 99th Percentile | 99% of requests were faster; only **1%** was slower. | **The standard tail latency metric**. Measures near-worst-case experience. | +| **P99.9** | "Three Nines" | 99.9% of requests were faster; 1 in 1,000 was slower. | Critical metric for financial transactions and high-density infrastructure. | + +--- + +## 2. What Is P99 and Why Measure Latency With It? + +**P99** represents the response time threshold that covers 99% of all requests processed by a service. It isolates and highlights the **slowest 1% of traffic** — the requests that encountered severe friction, resource contention, or transient blocking. + +### Why Is P99 the Industry Standard? + +1. **Realistic Worst-Case Assessment:** Measuring the absolute maximum response time (P100 or *Max Latency*) is often too noisy, as a single dropped packet from a poor mobile client connection would skew the dashboard. P99 filters out extraneous client-side network blips while faithfully revealing infrastructure bottlenecks. +2. **Standard for SLAs and SLOs:** Service Level Agreements (SLAs) and Service Level Objectives (SLOs) at tech leaders (Google, Amazon, Netflix) are rarely based on averages; they are built around P95, P99, and P99.9. +3. **Early Warning for Saturation:** When a database or connection pool starts to saturate, P50 often stays flat for minutes, while P99 immediately spikes. + +--- + +## 3. What Is Tail Latency? + +In statistics, the distribution of response times in computing systems **does not follow a symmetrical normal (Gaussian) bell curve**. Instead, it follows a **heavy-tailed / log-normal distribution**, featuring a dense cluster of fast responses on the left and a long, drawn-out "tail" of slow requests extending far to the right. + +``` +Frequency + │ ██ + │ ████ + │ ██████ + │ ███████ + │ █████████ + │ ███████████ + │ █████████████ + │ ███████████████ ← P50 (e.g. 45ms) + │ ███████████████████ + │ ███████████████████████ ← P90 (120ms) + │ █████████████████████████████ ← P95 (190ms) + │ ████████████████████████████████████████████████████████ ← P99 (890ms) + └─────────────────────────────────────────────────────────────► Time (ms) + └───────────────┘ + TAIL LATENCY +``` + +That region on the far right is the **tail latency**. Even if it accounts for only 1% of requests, that 1% has an outsized impact on user retention, revenue, and system stability. + +--- + +## 4. Mean vs. Percentiles: Why Averages Lie + +Consider a concrete production scenario with 100 requests handled by a payment API: + +- **99 requests** completed in exactly **20 ms**. +- **1 request** hit a temporary database lock and took **10,000 ms (10 seconds)**. + +Let's compare the metrics: + +$$\text{Arithmetic Mean} = \frac{(99 \times 20) + (1 \times 10000)}{100} = \frac{1980 + 10000}{100} = 119.8\text{ ms}$$ + +- **Mean (Average):** `119.8 ms` (Looks harmless on a high-level dashboard). +- **P50 (Median):** `20 ms` (Lightning fast). +- **P99:** `10,000 ms` (Catastrophic — client timeout!). + +### Why Averages Fail in Distributed Systems: + +1. **Averages Dilute Outliers:** A few catastrophic requests are masked by the overwhelming volume of fast requests. +2. **Averages Represent No Real User:** In the scenario above, not a single user experienced ~120ms. 99 users got instant 20ms responses, and 1 user suffered a 10-second freeze. +3. **Averages Are Non-Composable:** Calculating the average of averages across 10 server nodes produces mathematically invalid results that hide unhealthy instances. + +--- + +## 5. User Experience and the Fan-Out Amplification Effect + +### 1% at Scale Means Millions of Frustrated Users + +If your service processes **10 million requests per day**: +- **1% (P99)** represents **100,000 slow requests every day**. +- If a typical user triggers 5 API requests per session, up to **20,000 individual users daily** encounter sluggish interactions, abandoned carts, or failed checkouts. + +### The Mathematics of Microservices (*The Tail at Scale*) + +In Google's seminal paper *"The Tail at Scale"* (Jeffrey Dean and Luiz André Barroso), the authors demonstrated how distributed systems amplify tail latency through the *fan-out* pattern. + +Suppose rendering a user's homepage requires the API gateway to issue concurrent requests to **20 backend microservices** (pricing, catalog, recommendations, inventory, user profile, notifications, etc.): + +The overall user request completes only when the **slowest** backend service finishes. + +If each microservice has a $99\%$ probability of responding within its target latency (meaning a $1\%$ chance of hitting its P99): + +$$P(\text{Slow page}) = 1 - (0.99)^{20} = 1 - 0.8179 \approx 18.2\%$$ + +Even though every single microservice is 99% healthy, **nearly 1 in every 5 users (18.2%)** will experience tail latency on the frontend! + +If the call graph expands to 100 backend services: + +$$P(\text{Slow page}) = 1 - (0.99)^{100} = 1 - 0.3660 \approx 63.4\%$$ + +More than **63% of all user requests** will be slowed down by the tail latency of at least one downstream service. + +``` + ┌───► [Catalog Service] (P99: 1%) ──┐ + ├───► [Pricing Service] (P99: 1%) ──┤ + ├───► [Inventory Service] (P99: 1%) ──┤ +[Client] ┼───► [Promotions Service] (P99: 1%) ──┼──► Final Response = Max(All) + ├───► [Reviews Service] (P99: 1%) ──┤ Chance of hitting P99: + ├───► [...] ──┤ 1 - (0.99)^20 ≈ 18.2%! + └───► [Recommendation Svc] (P99: 1%) ──┘ +``` + +--- + +## 6. Root Causes of Tail Latency (Where Are the Bottlenecks?) + +Diagnosing high P99 requires understanding the underlying hardware and software triggers: + +### 1. Garbage Collection (GC Stop-The-World) Pauses +In managed runtime environments (Java/JVM, Go, Node.js, .NET), full garbage collection cycles can freeze application threads for tens or hundreds of milliseconds while sweeping heap memory. + +### 2. Cold Starts and Auto-Scaling Spikes +In serverless environments (AWS Lambda, Cloudflare Workers, Google Cloud Run) or newly spun Kubernetes pods, runtime bootstrapping, dependency injection, and JIT compilation cause severe latency spikes for initial requests. + +### 3. Lock Contention and Connection Pool Starvation +When concurrent threads compete for shared resources (such as database row locks, mutexes, or HTTP client pools), 99 threads acquire locks instantly, but the 100th thread gets queued behind a blocking transaction. + +### 4. Unindexed Database Queries and Disk Spills +SQL execution plans performing full table scans or disk-based temporary sorting degrade sharply under concurrent write load or growing dataset sizes. + +### 5. CPU Throttling (cgroups CFS in Kubernetes) +Under Kubernetes, overly restrictive `resources.limits` settings trigger the Linux Completely Fair Scheduler (CFS) quota throttling, freezing container CPU cycles mid-execution even when node CPU is plenty available. + +### 6. Noisy Neighbors and Shared I/O +In multi-tenant cloud virtual machines, noisy neighbors saturating network bandwidth or shared block storage (e.g., EBS IOPS limits) cause non-deterministic latency spikes. + +--- + +## 7. How to Monitor P99 in Practice + +### Querying Percentiles with Prometheus and PromQL + +Prometheus uses **Histograms** to compute percentiles efficiently without storing raw sample values in memory. + +The `histogram_quantile()` function interpolates across duration buckets: + +```promql +# P99 latency of successful HTTP requests over 5 minutes by route +histogram_quantile( + 0.99, + sum(rate(http_request_duration_seconds_bucket{status=~"2.."}[5m])) by (le, path) +) +``` + +```promql +# Comparing P50, P95, and P99 simultaneously for a service +histogram_quantile(0.50, sum(rate(http_request_duration_seconds_bucket[5m])) by (le)) # Median (P50) +histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le)) # Alert threshold (P95) +histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le)) # Tail latency (P99) +``` + +> **Crucial Observability Rule:** Never calculate the average of pre-computed percentiles (e.g., `avg(p99_metric)`). Percentiles **are not mathematically composable**. You must always aggregate the raw bucket counts with `sum()` before passing them to `histogram_quantile()`. + +### Distributed Tracing with OpenTelemetry and Jaeger/Tempo + +Metrics alert you **that** P99 is degraded; Distributed Tracing tells you **why** it happened. + +With OpenTelemetry, every request carries a unique `TraceID`. Filtering traces that exceed your P99 threshold in tools like Grafana Tempo or Jaeger reveals the exact blocking span: + +``` +[Trace: a8f4b1] HTTP GET /api/v1/checkout ─────────────────────── Total: 1,240ms +├── [Span] Auth Middleware ─────────────────────── 12ms +├── [Span] SQL: SELECT user_profile ────────────── 8ms +├── [Span] HTTP POST https://api.payment.com ───── 1,180ms ⚠️ (P99 BOTTLENECK) +└── [Span] Emit Event to Kafka ─────────────────── 15ms +``` + +--- + +## 8. Architectural Strategies to Eliminate Tail Latency + +To tame P99 in high-throughput production systems, apply these proven engineering techniques: + +### 1. Defensive Timeouts and Deadline Propagation +Never execute an outbound network call without an explicit timeout. Propagate request deadlines (via gRPC metadata or HTTP `X-Request-Deadline` headers): if a request has a 500ms budget and 400ms have elapsed, downstream services should immediately abort rather than wasting CPU on a request that will ultimately time out. + +```go +// Go Example: Context with strict timeout +ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond) +defer cancel() + +req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://inventory-service/stock", nil) +if err != nil { + return err +} +``` + +### 2. Hedged Requests +Pioneered by Google: issue a primary request to replica A. If replica A has not responded within the expected P95 latency (e.g., after 40ms), spawn a second concurrent request to replica B and take the result of whichever finishes first. + +This neutralizes transiently stalled nodes without doubling system load for the vast majority of traffic. + +### 3. Circuit Breakers +Use circuit breakers (Envoy, Istio, Resilience4j, Sony/gobreaker) to quickly fast-fail calls to degraded downstream dependencies, returning fallback data instead of queuing requests into oblivion. + +```yaml +# Envoy / Istio DestinationRule: Circuit Breaking & Outlier Detection +apiVersion: networking.istio.io/v1alpha3 +kind: DestinationRule +metadata: + name: payment-service-circuit-breaker +spec: + host: payment-service + trafficPolicy: + connectionPool: + tcp: + maxConnections: 100 + http: + http1MaxPendingRequests: 10 + maxRequestsPerConnection: 10 + outlierDetection: + consecutive5xxErrors: 3 + interval: 10s + baseEjectionTime: 30s + maxEjectionPercent: 50 +``` + +### 4. Garbage Collection Tuning +- In Java, upgrade to modern low-latency concurrent collectors like **ZGC** (`-XX:+UseZGC`) or **Shenandoah**, which maintain sub-millisecond pause times even across hundreds of gigabytes of heap memory. +- In Go, configure the `GOMEMLIMIT` environment variable to stabilize GC behavior under burst memory allocation. + +### 5. Multi-Tier Caching and Connection Warmup +- Keep HTTP/TCP connection pools pre-warmed with active keep-alives to eliminate TLS handshake overhead from the P99 critical path. +- Utilize in-process local caches for high-frequency lookup data to avoid round trips to Redis or databases. + +### 6. Adaptive Concurrency Limits and Graceful Load Shedding +Instead of letting unbounded request queues build up, implement adaptive concurrency limits based on queuing theory (TCP Vegas / Little's Law). When P99 latency starts climbing, the service sheds non-critical traffic early (HTTP 429/503) to ensure high-priority requests complete within SLO. + +--- + +## Comparison Summary: Performance Metrics + +| Dimension | Arithmetic Mean | Median (P50) | 99th Percentile (P99) | +| :--- | :--- | :--- | :--- | +| **Outlier Sensitivity** | High (distorts the metric) | Zero (ignores extremes) | **Optimal** (isolates the tail) | +| **Ideal Usage** | Cost & aggregate volume | Typical user baseline | **SLA/SLO Assurance & Stability** | +| **Microservice Impact** | Conceals failures | Conceals degradation | **Predicts real user experience** | +| **Actionability** | Low | Medium | **High (pinpoints contention & bottlenecks)** | + +--- + +## Conclusion + +The reliability of a distributed system is not measured by its speed when conditions are perfect, but by its predictability and resilience when components degrade. + +Relying on the arithmetic mean is flying blind with a broken altimeter. By placing **P99** and tail latency analysis at the center of your observability dashboards and SLOs, you gain direct visibility into real user friction, catch resource bottlenecks before they trigger outages, and build systems that scale gracefully. + +--- + +At **EF-TECH**, we specialize in SRE, Observability, Kubernetes, and high-performance cloud architecture. We help engineering teams instrument advanced metrics, establish actionable SLOs, and eliminate tail latency bottlenecks across critical production systems. [Contact us](/en/contato/) to discuss how we can elevate your infrastructure. For more technical guides, explore our [blog](/en/blog/). diff --git a/content/pt-br/blog/p99-tail-latency/cover.png b/content/pt-br/blog/p99-tail-latency/cover.png new file mode 100644 index 0000000..776bf14 Binary files /dev/null and b/content/pt-br/blog/p99-tail-latency/cover.png differ diff --git a/content/pt-br/blog/p99-tail-latency/index.md b/content/pt-br/blog/p99-tail-latency/index.md new file mode 100644 index 0000000..1939deb --- /dev/null +++ b/content/pt-br/blog/p99-tail-latency/index.md @@ -0,0 +1,299 @@ +--- +title: "P99 e Latência de Cauda (Tail Latency): Por Que a Média Engana e Como Mitigar Gargalos em Produção" +description: "Entenda o que é P99, percentis de latência (P50 a P99.9), por que a média aritmética esconde problemas graves em sistemas distribuídos e como identificar e mitigar a latência de cauda na prática." +summary: "O P99 (percentil 99) é uma das métricas mais críticas para Engenharia de Software, SRE e Observabilidade. Descubra por que a média esconde outliers, como o efeito fan-out em microsserviços amplifica a lentidão e quais estratégias arquiteturais — de timeouts agressivos a hedged requests e tracing distribuído — resolvem a latência de cauda." +date: 2026-08-22 +draft: false +tags: ["sre", "observabilidade", "performance", "distribuídos", "prometheus", "opentelemetry", "devops"] +categories: ["engenharia"] +featureimage: "cover.png" +featureimagecaption: "P99 e Latência de Cauda — Métricas de Percentil e Observabilidade em Sistemas Distribuídos" +--- + +Em sistemas de alta escala e arquiteturas modernas baseadas em microsserviços, um dos erros mais comuns de engenharia é confiar na **média aritmética** para avaliar a saúde e o tempo de resposta das aplicações. + +Dizer que *"o tempo médio de resposta da API é de 60ms"* pode parecer reconfortante em reuniões executivas. No entanto, por trás desse número aparentemente saudável, centenas ou milhares de usuários podem estar enfrentando travamentos de 3 a 10 segundos, abandonando carrinhos de compras ou acionando timeouts em cascata. + +É aqui que entram os **percentis**, em especial o **P99 (Percentil 99)** e o fenômeno conhecido como **latência de cauda (*Tail Latency*)**. + +Neste artigo aprofundado, vamos explorar os conceitos estatísticos essenciais, desmistificar por que a média engana, demonstrar o efeito multiplicador da latência em microsserviços e analisar as causas raízes e estratégias práticas de mitigação utilizadas por equipes de SRE e Engenharia de Performance. + +![P99 e Latência de Cauda](cover.png) + +--- + +## 1. O Que É um Percentil e Como Funciona? + +Um **percentil** (ou quantil) é uma medida estatística que indica o valor abaixo do qual uma determinada porcentagem de observações em um grupo de dados se encontra. + +Para calcular percentis de tempo de resposta: +1. Coletamos todas as amostras de latência de um intervalo (por exemplo, os últimos 5 minutos). +2. Ordenamos todas as medições da mais rápida para a mais lenta. +3. Encontramos o valor na posição percentual desejada. + +``` +Amostras ordenadas (em ms): [ 12, 15, 18, 22, 25, ..., 180, 240, 890, 2400 ] + ▲ ▲ ▲ ▲ + P50 P90 P95 P99 +``` + +### Os Marcos Mais Importantes no Monitoramento + +| Métrica | Nome | Significado Técnico | O Que Representa na Prática | +| :--- | :--- | :--- | :--- | +| **P50** | Mediana | 50% das requisições foram mais rápidas que esse valor; 50% foram mais lentas. | A experiência do **usuário típico/comum**. | +| **P90** | 90º Percentil | 90% das requisições foram mais rápidas; 10% foram mais lentas. | O início da degradação de performance. | +| **P95** | 95º Percentil | 95% das requisições foram mais rápidas; 5% mais lentas. | Padrão comum para **alertas de primeiro nível** e SLOs intermediários. | +| **P99** | 99º Percentil | 99% das requisições foram mais rápidas; apenas **1%** foi mais lenta. | **A latência de cauda padrão**. Mede o pior caso quase-extremo. | +| **P99.9** | "Three Nines" | 99,9% das requisições foram mais rápidas; 1 em cada 1.000 foi mais lenta. | Latência crítica em serviços financeiros e infraestrutura de alta densidade. | + +--- + +## 2. O Que É o P99 e Por Que Medi-lo em Latência? + +O **P99** representa o tempo de resposta que abrange 99% de todas as requisições realizadas a um serviço. Ele isola e expõe o **1% superior de lentidão** — os casos que sofreram atrito grave, contenção de recursos ou bloqueios transitórios. + +### Por Que o P99 É a Métrica Padrão da Indústria? + +1. **Foco no Pior Cenário Realista:** Medir o tempo máximo absoluto (P100 ou *Max Latency*) pode ser volátil demais, pois uma única requisição corrompida por uma falha de conexão de rede de um cliente mobile distorceria o gráfico. O P99 filtra anomalias espúrias de rede do cliente, mas mantém visíveis os gargalos reais da sua infraestrutura. +2. **Definição de SLAs e SLOs:** Contratos de Nível de Serviço (SLAs) e Objetivos de Nível de Serviço (SLOs) de empresas de classe mundial (Google, Amazon, Netflix) raramente são definidos sobre médias; são construídos sobre P95, P99 e P99.9. +3. **Detecção Precoce de Saturação:** Quando um banco de dados ou pool de conexões começa a saturar, o P50 costuma permanecer inalterado durante vários minutos, enquanto o P99 dispara imediatamente. + +--- + +## 3. O Que É Latência de Cauda (*Tail Latency*)? + +Em estatística, a distribuição dos tempos de resposta de sistemas computacionais **não segue uma curva normal (Gaussiana)** simétrica. Ela segue uma distribuição de cauda pesada (*heavy-tailed / log-normal*), com uma concentração imensa de respostas rápidas à esquerda e uma longa "cauda" de requisições lentas estendendo-se para a direita. + +``` +Frequência + │ ██ + │ ████ + │ ██████ + │ ███████ + │ █████████ + │ ███████████ + │ █████████████ + │ ███████████████ ← P50 (ex: 45ms) + │ ███████████████████ + │ ███████████████████████ ← P90 (120ms) + │ █████████████████████████████ ← P95 (190ms) + │ ████████████████████████████████████████████████████████ ← P99 (890ms) + └─────────────────────────────────────────────────────────────► Tempo (ms) + └───────────────┘ + LATÊNCIA DE CAUDA (TAIL) +``` + +Essa região à direita é a **latência de cauda**. Mesmo que represente apenas 1% do total de requisições, esse 1% tem consequências desproporcionais para a estabilidade e para o negócio. + +--- + +## 4. Média vs. Percentis: Por Que a Média Aritmética Engana? + +Considere um cenário real com 100 requisições atendidas por uma API de pagamentos: + +- **99 requisições** responderam em exatamente **20 ms**. +- **1 requisição** sofreu um deadlock temporário no banco e levou **10.000 ms (10 segundos)**. + +Vamos comparar as métricas: + +$$\text{Média Aritmética} = \frac{(99 \times 20) + (1 \times 10000)}{100} = \frac{1980 + 10000}{100} = 119{,}8\text{ ms}$$ + +- **Média:** `119.8 ms` (Parece aceitável em um painel sem contexto). +- **P50 (Mediana):** `20 ms` (Excelente). +- **P99:** `10.000 ms` (Inaceitável — timeout do cliente!). + +### Por Que a Média Falha? + +1. **A Média Dilui Desastres:** Poucas requisições com latência catastrófica são "engolidas" pelo grande volume de requisições rápidas. +2. **A Média Não Representa Nenhum Usuário Real:** No exemplo acima, nenhum usuário experimentou ~120ms. 99 usuários tiveram uma resposta instantânea de 20ms, e 1 usuário enfrentou uma tela travada por 10 segundos. +3. **Não É Comutativa em Agregações:** Se você calcular a média das médias de 10 servidores, o resultado estatístico perde totalmente o sentido e esconde nós com problemas graves. + +--- + +## 5. O Impacto na Experiência do Usuário e o Efeito *Fan-out* + +### 1% em Escala São Milhares de Usuários Reais + +Se sua plataforma processa **10 milhões de requisições por dia**: +- **1% (P99)** equivale a **100.000 requisições lentas por dia**. +- Se cada usuário faz em média 5 requisições por sessão, até **20.000 clientes diários** terão uma experiência frustrante, com carrinhos abandonados e desistência de compra. + +### A Matemática Cruel dos Microsserviços (*The Tail at Scale*) + +No clássico artigo do Google *"The Tail at Scale"* (Jeffrey Dean e Luiz André Barroso), os autores demonstraram como arquiteturas distribuídas amplificam a latência de cauda através do padrão de *fan-out*. + +Imagine que, para renderizar a página inicial da sua aplicação, o API Gateway precisa fazer chamadas paralelas para **20 microsserviços** (preço, catálogo, recomendação, estoque, perfil, notificações, etc.): + +A requisição do usuário só termina quando o serviço **mais lento** responder. + +Se cada microsserviço individual tem uma probabilidade de $99\%$ de responder dentro do seu SLA (ou seja, $1\%$ de chance de cair no P99): + +$$P(\text{Página lenta}) = 1 - (0{,}99)^{20} = 1 - 0{,}8179 \approx 18{,}2\%$$ + +Ou seja: mesmo que cada microsserviço atinja individualmente 99% de conformidade, **quase 1 em cada 5 usuários finais (18,2%)** será atingido pela latência de cauda! + +Se o número de chamadas paralelas subir para 100 serviços: + +$$P(\text{Página lenta}) = 1 - (0{,}99)^{100} = 1 - 0{,}3660 \approx 63{,}4\%$$ + +Mais de **63% das requisições dos usuários** experimentarão a latência do P99. + +``` + ┌───► [Serviço Catálogo] (P99: 1%) ──┐ + ├───► [Serviço Preços] (P99: 1%) ──┤ + ├───► [Serviço Estoque] (P99: 1%) ──┤ +[Cliente] ┼───► [Serviço Promoção] (P99: 1%) ──┼──► Resposta Final = Max(Todos) + ├───► [Serviço Avaliação](P99: 1%) ──┤ Chance de sofrer P99: + ├───► [...] ──┤ 1 - (0.99)^20 ≈ 18.2%! + └───► [Serviço Recomenda](P99: 1%) ──┘ +``` + +--- + +## 6. Principais Causas da Latência de Cauda (Onde Estão os Gargalos?) + +Para combater o P99, é fundamental entender o que causa picos de latência isolados: + +### 1. Pausas de *Garbage Collection* (GC Stop-The-World) +Em linguagens com gerenciamento automático de memória (Java/JVM, Go, Node.js, C#), coletas completas de lixo (*Full GC*) podem congelar a execução de threads por dezenas ou centenas de milissegundos. + +### 2. *Cold Starts* e Escalonamento +Em ambientes serverless (AWS Lambda, Cloudflare Workers, Google Cloud Run) ou pods Kubernetes que acabaram de subir, a inicialização de runtimes, injeção de dependências e compilação JIT criam picos brutais de latência para a primeira fatia de requisições. + +### 3. Contenção de *Locks* e Esgotamento de *Connection Pools* +Quando múltiplas threads disputam um recurso compartilhado (ex: lock de linha em banco de dados ou pool de conexões HTTP/DB), 99 threads passam direto, mas a centésima fica enfileirada aguardando uma conexão livre. + +### 4. Consultas a Bancos de Dados sem Índices Apropriados +Planos de execução que realizam *Full Table Scan* ou *Temporary Table on Disk* degradam dramaticamente quando o volume de dados cresce ou quando ocorrem atualizações concorrentes. + +### 5. Estrangulamento de CPU (*CPU Throttling* via cgroups) +No Kubernetes, limites de CPU mal dimensionados no `resources.limits` ativam o mecanismo de CFS (*Completely Fair Scheduler*) do Linux, congelando os ciclos de CPU do container no meio do processamento da requisição. + +### 6. *Noisy Neighbors* e I/O de Disco Compartilhado +Em nuvens públicas, instâncias vizinhas consumindo banda de rede ou operações de IOPS em discos compartilhados (ex: EBS) provocam lentidão esporádica não determinística. + +--- + +## 7. Como Monitorar o P99 na Prática + +### Consultando Percentis com Prometheus e PromQL + +O Prometheus utiliza **Histogramas** para calcular percentis sem a necessidade de reprocessar amostras individuais em memória. + +A função `histogram_quantile()` interpola os *buckets* coletados: + +```promql +# Latência P99 das requisições HTTP nos últimos 5 minutos por rota +histogram_quantile( + 0.99, + sum(rate(http_request_duration_seconds_bucket{status=~"2.."}[5m])) by (le, path) +) +``` + +```promql +# Comparativo simultâneo de P50, P95 e P99 para um serviço +histogram_quantile(0.50, sum(rate(http_request_duration_seconds_bucket[5m])) by (le)) # Mediana +histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le)) # Alerta +histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le)) # Cauda +``` + +> **Aviso Crítico de Observabilidade:** Nunca tente calcular o P99 tirando a média de percentis calculados previamente (ex: `avg(p99_metric)`). Percentis **não são aditivos**. A única forma correta é agregar os buckets brutos (`http_request_duration_seconds_bucket`) com `sum()` antes de aplicar a função `histogram_quantile()`. + +### Tracing Distribuído com OpenTelemetry e Jaeger/Tempo + +Métricas dizem **que** o P99 está alto; o Tracing Distribuído diz **por que** ele está alto. + +Com OpenTelemetry, cada requisição carrega um `TraceID`. Ao inspecionar os traces que ultrapassaram o limiar do P99 no Grafana Tempo ou Jaeger, você identifica visualmente qual *span* específico (ex: query SQL ou chamada gRPC externa) reteve a requisição: + +``` +[Trace: a8f4b1] HTTP GET /api/v1/checkout ─────────────────────── Total: 1.240ms +├── [Span] Auth Middleware ─────────────────────── 12ms +├── [Span] SQL: SELECT user_profile ────────────── 8ms +├── [Span] HTTP POST https://api.pagamento.com ─── 1.180ms ⚠️ (GARGALO P99) +└── [Span] Emit Event to Kafka ─────────────────── 15ms +``` + +--- + +## 8. Estratégias Arquiteturais para Mitigar a Latência de Cauda + +Para controlar o P99 em ambientes de produção de alta performance, adote as seguintes práticas recomendadas: + +### 1. Timeouts Defensivos e Propagação de Deadlines +Nunca faça uma chamada de rede sem timeout explícito. Utilize propagação de deadlines (como no gRPC ou via headers HTTP `X-Request-Deadline`): se a requisição original do usuário tem timeout de 500ms e 400ms já se passaram, os serviços downstream devem abortar imediatamente o processamento em vez de gastar recursos em vão. + +```go +// Exemplo em Go: Context com Timeout estrito +ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond) +defer cancel() + +req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://inventory-service/stock", nil) +if err != nil { + return err +} +``` + +### 2. Requisições Especulativas (*Hedged Requests*) +Técnica popularizada pelo Google: envie uma requisição primária para a réplica A. Se a réplica A não responder dentro do tempo esperado do P95 (por exemplo, após 50ms), dispare uma segunda requisição concorrente para a réplica B e use o resultado da que responder primeiro. + +Isso elimina o impacto de nós temporariamente degradados sem duplicar a carga na maioria das vezes. + +### 3. Circuit Breakers +Utilize Circuit Breakers (Envoy, Istio, Resilience4j, Sony/gobreaker) para interromper o envio de tráfego para dependências degradadas, retornando fallbacks rápidos em vez de enfileirar requisições até o timeout. + +```yaml +# Exemplo no Envoy / Istio: Circuit Breaking e Outlier Detection +apiVersion: networking.istio.io/v1alpha3 +kind: DestinationRule +metadata: + name: payment-service-circuit-breaker +spec: + host: payment-service + trafficPolicy: + connectionPool: + tcp: + maxConnections: 100 + http: + http1MaxPendingRequests: 10 + maxRequestsPerConnection: 10 + outlierDetection: + consecutive5xxErrors: 3 + interval: 10s + baseEjectionTime: 30s + maxEjectionPercent: 50 +``` + +### 4. Tuning de Garbage Collection +- No Java, migre para coletores de lixo concorrentes de ultrabaixa latência, como **ZGC** (`-XX:+UseZGC`) ou **Shenandoah**, que garantem pausas de GC sub-milissegundos mesmo com heaps de centenas de gigabytes. +- Em Go, utilize a variável `GOMEMLIMIT` (introduzida no Go 1.19) para evitar picos de GC sob pressão de memória. + +### 5. Caching Multi-camadas e Warmup de Conexões +- Mantenha pools de conexões HTTP/TCP aquecidos (*keep-alive* ativo) para evitar o handshake TLS no caminho crítico do P99. +- Implemente cache local em memória (in-process) para dados altamente consultados, reduzindo viagens de rede ao Redis ou banco de dados. + +### 6. Concurrency Limits e Shedding de Carga Gracioso +Em vez de permitir filas infinitas que degradam a latência de todos os usuários, adote algoritmos de *Adaptive Concurrency Limits* (como Little's Law / TCP Vegas). Quando o sistema detecta que a latência P99 começou a subir, ele descarta graciosamente requisições de baixa prioridade (HTTP 429 / 503) para preservar a estabilidade das requisições prioritárias. + +--- + +## Resumo Comparativo: Métricas de Performance + +| Dimensão | Média Aritmética | Mediana (P50) | Percentil 99 (P99) | +| :--- | :--- | :--- | :--- | +| **Sensibilidade a Outliers** | Alta (distorce o resultado) | Nula (ignora extremos) | **Excelente** (isola e foca na cauda) | +| **Uso Ideal** | Custos e volume agregado | Comportamento típico | **Garantia de SLA/SLO e Estabilidade** | +| **Impacto em Microsserviços** | Oculta falhas | Oculta degradação | **Prediz a experiência real do usuário** | +| **Acionabilidade** | Baixa | Média | **Alta (indica contenção e gargalos)** | + +--- + +## Conclusão + +A confiabilidade de um sistema distribuído não é medida pela sua velocidade quando tudo funciona perfeitamente, mas pela sua resiliência e previsibilidade nos momentos de estresse. + +Confiar na média aritmética é navegar com uma bússola descalibrada. Ao adotar o **P99** como métrica central nos seus painéis de observabilidade e SLOs, você passa a enxergar as reais dores dos seus usuários, identifica gargalos antes que virem incidentes de indisponibilidade e projeta sistemas verdadeiramente resilientes em escala. + +--- + +Na **EF-TECH**, somos especialistas em SRE, Observabilidade, Kubernetes e arquiteturas de alta performance em nuvem. Ajudamos sua equipe a instrumentar métricas avançadas, configurar SLOs realistas e eliminar gargalos de latência em sistemas de missão crítica. [Entre em contato conosco](/pt-br/contato/) e descubra como otimizar sua infraestrutura. Para mais artigos técnicos como este, explore o nosso [blog](/pt-br/blog/). diff --git a/static/img/blog/p99-tail-latency/cover.png b/static/img/blog/p99-tail-latency/cover.png new file mode 100644 index 0000000..776bf14 Binary files /dev/null and b/static/img/blog/p99-tail-latency/cover.png differ diff --git a/static/img/blog/p99-tail-latency/generate_cover.py b/static/img/blog/p99-tail-latency/generate_cover.py new file mode 100644 index 0000000..5a8c513 --- /dev/null +++ b/static/img/blog/p99-tail-latency/generate_cover.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +""" +Generate the featured image (1200x630px PNG) for the P99 & Tail Latency blog post. +Requires: pip install pillow +""" + +import os +import sys +import shutil + +try: + from PIL import Image, ImageDraw, ImageFont +except ImportError: + print("Pillow is required. Install it with: pip install pillow") + sys.exit(1) + +WIDTH, HEIGHT = 1200, 630 + + +def find_font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + """Try to find a suitable monospace or sans font on the system.""" + candidates = [] + if bold: + candidates = [ + "/System/Library/Fonts/Supplemental/SFNSMono-Bold.otf", + "/System/Library/Fonts/SFNS-Bold.otf", + "/Library/Fonts/Arial Bold.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf", + "/usr/share/fonts/truetype/ubuntu/UbuntuMono-B.ttf", + "/usr/share/fonts/truetype/liberation/LiberationMono-Bold.ttf", + ] + candidates += [ + "/System/Library/Fonts/Supplemental/SFNSMono-Regular.otf", + "/System/Library/Fonts/SFNS.ttf", + "/Library/Fonts/Arial.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", + "/usr/share/fonts/truetype/ubuntu/UbuntuMono-R.ttf", + "/usr/share/fonts/truetype/liberation/LiberationMono.ttf", + ] + for path in candidates: + if os.path.exists(path): + return ImageFont.truetype(path, size) + return ImageFont.load_default() + + +def draw_card(draw: ImageDraw, x: int, y: int, w: int, h: int, title: str = "") -> None: + """Draw a modern dark card with title bar.""" + # Shadow + draw.rounded_rectangle( + [x + 4, y + 4, x + w + 4, y + h + 4], + radius=12, fill=(0, 0, 0, 80) + ) + # Body + draw.rounded_rectangle( + [x, y, x + w, y + h], radius=10, fill=(18, 24, 38) + ) + # Header bar + draw.rounded_rectangle( + [x, y, x + w, y + 38], radius=10, fill=(28, 36, 56) + ) + draw.rectangle([x, y + 28, x + w, y + 38], fill=(28, 36, 56)) + + # Traffic light dots + for cx, color in [(x + 18, (255, 95, 87)), (x + 40, (255, 189, 46)), (x + 62, (39, 201, 63))]: + draw.ellipse([cx, y + 13, cx + 12, y + 25], fill=color) + + if title: + font_title = find_font(13, bold=True) + draw.text((x + w // 2, y + 19), title, fill=(160, 175, 205), font=font_title, anchor="mm") + + +def create_featured_image(output_path: str) -> None: + """Create the 1200x630 OG image.""" + img = Image.new("RGBA", (WIDTH, HEIGHT), (11, 15, 25, 255)) + draw = ImageDraw.Draw(img) + + # Background gradient + top_color = (11, 15, 25) + bottom_color = (22, 32, 54) + for i in range(HEIGHT): + t = i / HEIGHT + r = int(top_color[0] + (bottom_color[0] - top_color[0]) * t) + g = int(top_color[1] + (bottom_color[1] - top_color[1]) * t) + b = int(top_color[2] + (bottom_color[2] - top_color[2]) * t) + draw.line([(0, i), (WIDTH, i)], fill=(r, g, b, 255)) + + # Grid dots + for x in range(0, WIDTH, 28): + for y in range(0, HEIGHT, 28): + draw.point((x, y), fill=(60, 80, 120, 50)) + + # Left Card: PromQL Query & Distribution Metrics + cw1, ch1 = 540, 360 + cx1, cy1 = 45, 35 + draw_card(draw, cx1, cy1, cw1, ch1, "prometheus-tail-latency — promql") + + font_code_bold = find_font(13, bold=True) + font_code = find_font(13) + font_val = find_font(13, bold=True) + + # PromQL code + py = cy1 + 52 + draw.text((cx1 + 20, py), "# PromQL: Cálculo de Latência P99", fill=(100, 120, 150), font=font_code) + py += 22 + draw.text((cx1 + 20, py), "histogram_quantile(0.99,", fill=(130, 180, 255), font=font_code_bold) + py += 20 + draw.text((cx1 + 40, py), "sum(rate(http_duration_bucket[5m]))", fill=(240, 240, 250), font=font_code) + py += 20 + draw.text((cx1 + 40, py), "by (le)", fill=(240, 240, 250), font=font_code) + py += 20 + draw.text((cx1 + 20, py), ")", fill=(130, 180, 255), font=font_code_bold) + py += 30 + + # Separator line + draw.line([(cx1 + 20, py), (cx1 + cw1 - 20, py)], fill=(40, 50, 75), width=1) + py += 15 + + # Visual Percentiles Bars + percentiles = [ + ("P50 (Mediana)", "45 ms", 0.15, (52, 199, 89)), # Green + ("P90", "120 ms", 0.35, (50, 173, 230)), # Blue + ("P95", "210 ms", 0.50, (255, 204, 0)), # Yellow + ("P99 (Tail)", "890 ms", 0.85, (255, 149, 0)), # Orange + ("P99.9 (Max)", "2.4 s", 1.00, (255, 59, 48)), # Red + ] + + for label, val, ratio, bar_color in percentiles: + draw.text((cx1 + 20, py), label, fill=(200, 210, 230), font=font_code) + draw.text((cx1 + 175, py), val, fill=bar_color, font=font_val) + + # Bar track + bx = cx1 + 245 + bw = 265 + bh = 12 + draw.rounded_rectangle([bx, py + 2, bx + bw, py + 2 + bh], radius=6, fill=(35, 45, 68)) + # Bar fill + fill_w = max(12, int(bw * ratio)) + draw.rounded_rectangle([bx, py + 2, bx + fill_w, py + 2 + bh], radius=6, fill=bar_color) + + py += 26 + + # Right Card: The Average Trap vs Reality + cw2, ch2 = 540, 360 + cx2, cy2 = 615, 35 + draw_card(draw, cx2, cy2, cw2, ch2, "por que a media engana.log") + + py2 = cy2 + 55 + font_bold_lg = find_font(16, bold=True) + font_body = find_font(14) + + draw.text((cx2 + 25, py2), "⚠️ A Armadilha da Média Aritmética", fill=(255, 204, 0), font=font_bold_lg) + py2 += 30 + + stats_lines = [ + ("Cenário:", "10.000 requisições / segundo"), + ("Média Geral:", "65 ms (Parece perfeito!)"), + ("P50 (50%):", "40 ms (Excelente experiência)"), + ("P99 (1% mais lento):", "890 ms (Experiência degradada)"), + ("Impacto em escala:", "100 req/s sofrem lentidão severa"), + ] + + for label, text in stats_lines: + draw.text((cx2 + 25, py2), label, fill=(140, 160, 195), font=font_body) + highlight = (255, 100, 100) if "P99" in label or "Impacto" in label else (230, 235, 250) + draw.text((cx2 + 185, py2), text, fill=highlight, font=font_code_bold) + py2 += 26 + + py2 += 12 + # Alert box inside right card + draw.rounded_rectangle([cx2 + 20, py2, cx2 + cw2 - 20, py2 + 70], radius=8, fill=(32, 42, 65)) + draw.text((cx2 + 35, py2 + 14), "💡 Em arquitetura de microsserviços:", fill=(100, 210, 255), font=font_code_bold) + draw.text((cx2 + 35, py2 + 38), "1 chamada do usuário toca 20+ serviços.", fill=(210, 220, 240), font=font_body) + draw.text((cx2 + 35, py2 + 54), "A probabilidade de atingir o P99 salta para ~18%!", fill=(255, 180, 100), font=font_code) + + # Big Title Section at the Bottom + title_font = find_font(38, bold=True) + draw.text((WIDTH // 2, 440), "P99 e Latência de Cauda (Tail Latency)", + fill=(255, 255, 255), font=title_font, anchor="mm") + + subtitle_font = find_font(20, bold=True) + draw.text((WIDTH // 2, 485), "Por que a média engana e como dominar a performance em sistemas distribuídos", + fill=(94, 180, 255), font=subtitle_font, anchor="mm") + + badge_font = find_font(14) + draw.text((WIDTH // 2, 525), "SRE · Observabilidade · Percentis · SLA/SLO · Microsserviços", + fill=(145, 165, 200), font=badge_font, anchor="mm") + + # EF-TECH branding + brand_font = find_font(15, bold=True) + draw.text((WIDTH - 45, HEIGHT - 25), "EF-TECH", fill=(120, 140, 180), font=brand_font, anchor="rs") + + # Save PNG + final = Image.new("RGB", (WIDTH, HEIGHT), (11, 15, 25)) + final.paste(img, mask=img.split()[3]) + final.save(output_path, "PNG") + print(f"Image saved to {output_path} ({WIDTH}x{HEIGHT})") + + +if __name__ == "__main__": + output = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cover.png") + create_featured_image(output) + + # Also copy to content directories + root_dir = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../../../")) + pt_dir = os.path.join(root_dir, "content/pt-br/blog/p99-tail-latency") + en_dir = os.path.join(root_dir, "content/en/blog/p99-tail-latency") + + os.makedirs(pt_dir, exist_ok=True) + os.makedirs(en_dir, exist_ok=True) + + shutil.copyfile(output, os.path.join(pt_dir, "cover.png")) + shutil.copyfile(output, os.path.join(en_dir, "cover.png")) + print("Copied cover.png to pt-br and en content folders.")