capa-post-prevendo-gargalos-servidor

Prevendo gargalos no seu servidor com filas M/M/1

  • Autor do post:
  • Categoria do post:Python
  • Última modificação do post:14/03/2026
  • Comentários do post:0 comentário

Sua aplicação está em produção, os usuários começam a reclamar de lentidão e ninguém consegue explicar por que o servidor de repente entra em colapso nos horários de pico. Neste artigo, você vai entender como um conceito da Teoria da Filas é capaz de solucionar esse problema e como implementá-lo em Python para extrair métricas valiosas sobre o comportamento do servidor antes que os problemas aconteçam.

Sistema de filas

Todo servidor web, banco de dados, fila de mensagens ou qualquer serviço que processa requisições pode ser abstraído como um sistema de filas: chegam pedidos, eles esperam por um recurso disponível, são atendidos e saem.

A Teoria das Filas é o ramo da matemática e da pesquisa operacional que estuda exatamente esses sistemas. Ela permite responder perguntas como:

  • Qual é o tempo médio que uma requisição espera na fila?
  • Quantas requisições estarão acumuladas no servidor em média?
  • Qual é a probabilidade de o servidor estar ocioso?
  • A partir de qual taxa de chegada meu servidor entra em colapso?

Essas perguntas têm respostas analíticas, isto é, você não precisa simular milhões de requisições para obtê-las. Com alguns parâmetros do seu servidor, as fórmulas da teoria das filas entregam as respostas diretamente. O modelo mais simples e amplamente utilizado é o M/M/1, que serve como ponto de partida para a análise de praticamente qualquer servidor.

O que é uma fila M/M/1 ?

Uma fila M/M/1 é um modelo matemático da teoria das filas usado para analisar sistemas onde clientes chegam, esperam em uma fila e são atendidos por um único servidor. Esse modelo é muito usado em áreas como redes de computadores, sistemas operacionais, telecomunicações e atendimento ao cliente para prever tempo de espera, tamanho da fila e utilização do servidor.

A notação M/M/1 vem da Notação de Kendall, um sistema padronizado para descrever filas com três campos A/B/c, onde:

  • A – Distribuição dos intervalos entre chegadas
  • B – Distribuição dos tempos de serviço
  • c – Número de servidores

Com base nessa notação a fila M/M/1 segue uma estrutura semelhante, veja:

  • Primeiro M: Significa que as chegadas seguem um processo de Poisson, ou seja, os intervalos entre chegadas seguem uma distribuição Exponencial. Na prática, a probabilidade de uma nova chegada não depende de quando foi a última.
  • Segundo M: Significa que os tempos de atendimento também seguem uma distribuição exponencial.
  • 1: Significa que existe apenas um servidor.

Parâmetros fundamentais

O modelo M/M/1 é definido por apenas dois parâmetros: λ que indica o número médio de requisições por unidade de tempo e μ que indica o número médio de requisições atendidas por unidade de tempo. A partir desses dois parâmetros deriva-se o parâmetro mais importante desse sistema.

$$ \rho = \frac{\lambda}{\mu} $$

ρ é chamado de intensidade de tráfego ou fator de utilização. Ele representa a fração do tempo em que o servidor está ocupado. A intensidade de tráfego nos diz algo muito importante, o servidor só está estável se ρ < 1. Quando λ ≥ μ, o servidor entra em colapso e a fila de requisições tende ao infinito.

Certifique-se sempre de que λ e μ estejam na mesma unidade de tempo. Se você medir a chegada em requisições por segundo, a taxa de serviço também deve ser calculada em requisições por segundo.

Métricas do servidor

Com o ρ calculado, toda a dinâmica do servidor pode ser derivada analiticamente. Abaixo é listado várias informações que podemos pegar a partir de ρ.

Número médio de requisições no servidor

Quantidade média de requisições no servidor, isto é, em espera + sendo atendidas.

$$ L = \frac{\rho}{1 – \rho} $$

Número médio de requisições na fila

Quantidade média de requisições aguardando na fila para serem processadas pelo servidor.

$$ L_q = \frac{\rho^2}{1 – \rho} $$

Tempo médio

Podemos calcular o tempo médio em dois momentos diferentes. O primeiro momento é quando a requisição entra no servidor, então é computabilizado o tempo de espera e o tempo de processamento. Chamamos isso de tempo médio no servidor o qual é dado pela fórmula abaixo.

$$ W = \frac{1}{\mu – \lambda} $$

O segundo momento que podemos calcular o tempo médio é durante a espera na fila de requisições para serem atendidas. Sua fórmula é apresentada abaixo.

$$ W_q = \frac{\lambda}{\mu(\mu – \lambda)} $$

Note que, essas serão as únicas informações que não dependem necessariamente de ρ.

Probabilidade do servidor não estar processando nada

Para calcular a probabilidade de não haver nenhuma requisição no servidor usamos:

$$ P_0 = 1 – \rho $$

Probabilidade de haver N requisições

Essa fórmula calcula a probabilidade de haver n requisições no servidor, seja em espera ou sendo processado por ele. Lembre-se que n é a quantidade de requisições.

$$ P_n = (1 – \rho) \cdot \rho^n $$

Probabilidade de haver mais de n requisições

Essa função é muito útil quando queremos estimar a probabilidade da fila ultrapassar um determinado tamanho.

$$ P(N>n) = \rho^{n+1} $$

Implementando as métricas usando Python

Agora vamos implementar cada métrica como uma função independente, documentada e explicada em detalhes. Essa abordagem facilita o entendimento de cada cálculo individualmente e torna o código mais didático.

O código será implementado na ferramenta Google Colab, então para iniciarmos, acesse a plataforma e clique em “Novo Notebook”. A tela que aparecerá é semelhante a da imagem abaixo.

Se você chegou nessa tela, então o próximo passo é a implementação das funções que calculam as métricas que vimos acima.

Verificando a estabilidade do servidor

Antes de qualquer cálculo, precisamos garantir que o sistema é estável. Como vimos, isso só é verdade quando ρ < 1. Todas as funções a seguir dependem dessa condição.

Python
    import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

def calcular_utilizacao(lamb: float, mu: float):
    if lamb <= 0 or mu <= 0:
        raise ValueError("λ e μ devem ser valores positivos.")

    rho = lamb / mu

    if rho >= 1:
        print(f"SISTEMA INSTÁVEL: ρ = {rho:.4f}. A fila cresce sem limite.")
    else:
        print(f"Sistema estável: ρ = {rho:.4f} ({rho * 100:.2f}% de utilização)")

    return rho
    
  

Calculando a média de requisições no servidor

Python
    def calcular_L(rho: float):
    if rho >= 1:
        return float('inf')

    return rho / (1 - rho)
    
  

Calculando o número médio de requisições na fila

Python
    def calcular_Lq(rho: float):
    if rho >= 1:
        return float('inf')

    return (rho ** 2) / (1 - rho)
    
  

Calculando o tempo médio no servidor

Python
    def calcular_W(lamb: float, mu: float):
    if lamb >= mu:
        return float('inf')

    return 1 / (mu - lamb)
    
  

Calculando o tempo médio na fila

Python
    def calcular_Wq(lamb: float, mu: float):
    if lamb >= mu:
        return float('inf')

    return lamb / (mu * (mu - lamb))
    
  

Probabilidade do servidor estar vazio

Python
    def calcular_P0(rho: float):
    if rho >= 1:
        return 0.0

    return 1 - rho
    
  

Probabilidade de exatamente N requisições no servidor

Python
    def calcular_Pn(rho: float, n: int):
    
    if n < 0:
        raise ValueError("n deve ser um inteiro não-negativo.")
    if rho >= 1:
        return 0.0

    return (1 - rho) * (rho ** n)
    
  

Probabilidade de mais de N requisições no servidor

Python
    def calcular_prob_maior_que_n(rho: float, n: int):
    if n < 0:
        raise ValueError("n deve ser um inteiro não-negativo.")
    if rho >= 1:
        return 1.0

    return rho ** (n + 1)
    
  

Indicadores de dimensionamento e performance

Até aqui, todas as métricas que calculamos respondem perguntas sobre o estado médio do sistema. São médias, e médias escondem variação. Um W de 50ms não significa que toda requisição responde em 50ms, algumas respondem em 10ms, outras em 200ms.

É exatamente aí que as funções a seguir entram. Elas transformam os valores calculados anteriormente em respostas práticas e operacionais, indo além da média para responder perguntas como:

  • Qual é o pior caso que 99% dos usuários enfrentam?
  • A partir de qual carga meu servidor se torna arriscado?
  • O que acontece com todas as métricas se a carga subir 20% amanhã?

Cada função abaixo consome os resultados das funções anteriores e os transforma em inteligência aplicada.

Percentil do tempo no sistema

O tempo que uma requisição passa no servidor em uma fila M/M/1 segue uma distribuição exponencial com taxa (μ – λ). Isso não é coincidência, é uma consequência direta do tempo de processamento e os intervalos de chegada também serem exponenciais. A média dessa distribuição é exatamente 1/(μ – λ), que é o nosso W. Ou seja, o percentil de tempo e W descrevem a mesma distribuição: W é sua média, e o percentil é um ponto específico de sua cauda. Sabendo disso, dizemos que o tempo no servidor segue uma distribuição exponencial cuja sua função de distribuição acumulada é apresentada logo abaixo.

$$ F(t) = P(T ≤ t) = 1 – e^{-(μ-λ)\cdot t} $$

Como queremos calcular o tempo em função de ρ, precisamos manipular a equação para isolar o t, assim teremos:

$$ t = -\ln\frac{(1 – p)}{(μ – λ)} $$

Traduzindo essa equação para um código em Python temos

Python
    def calcular_percentil_tempo(lamb: float, mu: float, percentil: float):
    if not (0 < percentil < 1):
        raise ValueError("Percentil deve estar entre 0 e 1 (ex: 0.95).")
    if lamb >= mu:
        return float('inf')

    taxa_saida = mu - lamb
    return -np.log(1 - percentil) / taxa_saida
    
  

Mas por que isso é importante ? Porque são usados para encontrar o melhor desempenho do servidor. Saber que W = 50ms é insuficiente para garantir ao cliente que “99% das requisições respondem em até 200ms”. Esta função transforma W em uma família de garantias concretas: P50, P90, P95, P99.

Capacidade máxima recomendada

Sabemos pela análise de L e Lq que o comportamento do sistema é altamente não-linear próximo de ρ = 1: um aumento de 10% na carga pode dobrar a fila. Mas onde exatamente fica o limite seguro de operação?

Esta função inverte a lógica de calcular_utilizacao. Em vez de calcular ρ a partir de λ e μ, ela parte de um ρ máximo aceitável e devolve o λ máximo correspondente. É a ponte entre a análise matemática e uma regra operacional concreta: “nunca deixe λ ultrapassar este valor”.

Python
    def calcular_capacidade_maxima(mu: float, margem: float):
    if not (0 < margem < 1):
        raise ValueError("A margem deve estar entre 0 e 1.")

    return mu * margem
    
  

Isso é importante porque transforma a análise em uma ação concreta. Após calcular ρ, L, Lq e o percentis, o engenheiro precisa saber: "a partir de qual número eu devo escalar?". Esta função responde isso diretamente.

Análise de sensibilidade

Todas as funções anteriores analisam o sistema em um ponto fixo: λ = 80, μ = 100. Mas sistemas reais têm tráfego variável. O que acontece nas métricas se λ aumentar 10% numa Black Friday? E se cair 30% de madrugada?

Esta função responde isso sistematicamente. Ela reutiliza internamente toda a cadeia de cálculo, isto é, ρ, L, Lq, W, Wq para múltiplos valores de λ, gerando uma tabela comparativa. É como chamar as cinco primeiras funções em loop, para cada cenário possível.

Python
    def calcular_sensibilidade(
    lamb: float,
    mu: float,
    variacoes: list = [-0.30, -0.20, -0.10, 0, 0.10, 0.20, 0.30]
):
    resultados = []

    for v in variacoes:
        lamb_novo = lamb * (1 + v)
        rho_novo  = lamb_novo / mu
        estavel   = rho_novo < 1

        if estavel:
            L   = rho_novo / (1 - rho_novo)
            Lq  = (rho_novo ** 2) / (1 - rho_novo)
            W   = 1 / (mu - lamb_novo)
            Wq  = lamb_novo / (mu * (mu - lamb_novo))
        else:
            L = Lq = W = Wq = float('inf')

        resultados.append({
            "Variação λ": f"{v*100:+.0f}%",
            "λ":          round(lamb_novo, 2),
            "ρ":          f"{rho_novo*100:.1f}%",
            "Estável":    "SIM" if estavel else "NAO",
            "L":          round(L,  2) if estavel else "∞",
            "Lq":         round(Lq, 2) if estavel else "∞",
            "W (s)":      round(W,  5) if estavel else "∞",
            "Wq (s)":     round(Wq, 5) if estavel else "∞",
        })

    return pd.DataFrame(resultados)
  

Quando estamos medindo as métricas de um servidor, devemos ser capaz de questionar "e se?". Esta função entrega o planejamento de capacidade: uma tabela que podemos consultar antes de um momento de estresse do servidor ou antes de um evento de alto tráfego. Ela também torna visível o ponto exato de colapso do sistema.

Criando um dashboard visual

Temos agora um conjunto rico de métricas numéricas. O problema é que números em tabela são difíceis de apresentar para usuários e de usar para comunicar risco de forma imediata. O dashboard resolve isso: ele traduz todas as métricas anteriores em gráficos, tornando os padrões e riscos visualmente óbvios. O dashboard implementará quatro gráficos:

  1. Mostra qual estado mais provável que o servidor se encontra
  2. Mostra o risco da fila longa
  3. Expõe e não-linearidade da chegada de requisições no servidor
  4. Mostra onde a latência explode
Python
    def plotar_dashboard(lamb: float, mu: float, rho: float, n_max: int = 20):
 
    L  = rho / (1 - rho)
    W  = 1 / (mu - lamb)
    ns = np.arange(0, n_max + 1)

    lamb_range = np.linspace(0.01 * mu, 0.99 * mu, 300)
    rho_range  = lamb_range / mu

    cor_azul     = "#2E86AB"
    cor_laranja  = "#F4A261"
    cor_vermelho = "#E84855"

    fig = plt.figure(figsize=(16, 12))
    fig.suptitle(
        f"Dashboard — Fila M/M/1  |  λ={lamb}, μ={mu}, ρ={rho:.0%}",
        fontsize=14, fontweight="bold", y=0.98
    )
    gs = gridspec.GridSpec(2, 2, figure=fig, hspace=0.38, wspace=0.32)

    ax1 = fig.add_subplot(gs[0, 0])
    probs = [(1 - rho) * (rho ** n) for n in ns]
    ax1.bar(ns, probs, color=cor_azul, alpha=0.8, edgecolor="white")
    ax1.axvline(L, color=cor_vermelho, linestyle="--",
                linewidth=2, label=f"L = {L:.2f}")
    ax1.set_title("Distribuição P(N = n)", fontweight="bold")
    ax1.set_xlabel("Requisições no sistema (n)")
    ax1.set_ylabel("Probabilidade")
    ax1.legend()
    ax1.grid(axis="y", alpha=0.3)

    ax2 = fig.add_subplot(gs[0, 1])
    probs_gt = [rho ** (n + 1) for n in ns]
    ax2.plot(ns, probs_gt, color=cor_laranja, linewidth=2.5,
             marker="o", markersize=5, label="P(N > n)")
    ax2.axhline(0.05, color=cor_vermelho, linestyle="--",
                linewidth=1.5, label="Risco de 5%")
    ax2.set_title("Probabilidade Acumulada P(N > n)", fontweight="bold")
    ax2.set_xlabel("Requisições no sistema (n)")
    ax2.set_ylabel("P(N > n)")
    ax2.legend()
    ax2.grid(alpha=0.3)

    ax3 = fig.add_subplot(gs[1, 0])
    Ls  = rho_range / (1 - rho_range)
    Lqs = (rho_range ** 2) / (1 - rho_range)
    ax3.plot(rho_range * 100, Ls,  color=cor_azul,    linewidth=2.5, label="L")
    ax3.plot(rho_range * 100, Lqs, color=cor_laranja, linewidth=2.5,
             linestyle="--", label="Lq")
    ax3.axvline(rho * 100, color=cor_vermelho, linestyle=":",
                linewidth=2, label=f"ρ atual = {rho:.0%}")
    ax3.set_title("Requisições no Sistema vs Utilização", fontweight="bold")
    ax3.set_xlabel("Utilização ρ (%)")
    ax3.set_ylabel("Número médio de requisições")
    ax3.set_ylim(0, min(30, Ls.max() * 1.1))
    ax3.legend()
    ax3.grid(alpha=0.3)

    ax4 = fig.add_subplot(gs[1, 1])
    Ws  = 1 / (mu - lamb_range)
    Wqs = lamb_range / (mu * (mu - lamb_range))
    ax4.plot(lamb_range, Ws,  color=cor_azul,    linewidth=2.5, label="W")
    ax4.plot(lamb_range, Wqs, color=cor_laranja, linewidth=2.5,
             linestyle="--", label="Wq")
    ax4.axvline(lamb, color=cor_vermelho, linestyle=":",
                linewidth=2, label=f"λ atual = {lamb}")
    ax4.set_title("Tempo Médio vs Taxa de Chegada", fontweight="bold")
    ax4.set_xlabel("Taxa de chegada λ")
    ax4.set_ylabel("Tempo médio")
    ax4.set_ylim(0, W * 10)
    ax4.legend()
    ax4.grid(alpha=0.3)

    plt.show()
  

Como tudo se encaixa na main

Antes de ver o código, é importante entender a ordem lógica das chamadas e por que cada uma depende das anteriores. Cada camada só faz sentido porque a anterior foi executada. Você não calcula percentis sem ter entendido W. Você não faz análise de sensibilidade sem ter visto o estado atual. E você não plota o dashboard sem ter todos os valores calculados.

Python
    #...Funções já implementadas


def main():
    LAMB = 80   
    MU   = 100  

    print("ANÁLISE COMPLETA DE FILA M/M/1 — SERVIDOR WEB")

    rho = calcular_utilizacao(LAMB, MU)

    L  = calcular_L(rho)
    Lq = calcular_Lq(rho)
    print(f"\nRequisições no sistema  (L) : {L:.4f}")
    print(f"Requisições na fila    (Lq): {Lq:.4f}")
    print(f"(diferença L - Lq = {L - Lq:.4f} ≈ ρ = {rho:.4f})")

    W  = calcular_W(LAMB, MU)
    Wq = calcular_Wq(LAMB, MU)
    print(f"\nTempo no sistema  (W) : {W:.4f}s  ({W*1000:.1f}ms)")
    print(f"Tempo na fila    (Wq): {Wq:.4f}s  ({Wq*1000:.1f}ms)")
    print(f"Verificação Lei de Little: λ×W = {LAMB*W:.4f} ≈ L = {L:.4f}")

    P0 = calcular_P0(rho)
    print(f"\nP(sistema vazio) (P0): {P0:.4f}  ({P0*100:.1f}% do tempo ocioso)")

    print(f"\nDistribuição de probabilidade — primeiros 8 estados:")
    print(f"   {'n':>3} | {'P(N=n)':>10} | {'P(N>n)':>10}")
    print(f"   {'-'*3}-+-{'-'*10}-+-{'-'*10}")
    for n in range(8):
        pn  = calcular_Pn(rho, n)
        pgt = calcular_prob_maior_que_n(rho, n)
        print(f"   {n:>3} | {pn:>10.6f} | {pgt:>10.6f}")

    n_alerta = 10
    risco = calcular_prob_maior_que_n(rho, n_alerta)
    print(f"\nRisco operacional:")
    print(f"   P(N > {n_alerta}) = {risco:.4f} ({risco*100:.2f}%)")
    print(f"   Ou seja: {risco*100:.1f}% do tempo há mais de {n_alerta} "f"requisições acumuladas.")
    print(f"\nPercentis de latência (SLA):")
    for p, nome in [(0.50, "P50"), (0.90, "P90"),
                    (0.95, "P95"), (0.99, "P99")]:
        t = calcular_percentil_tempo(LAMB, MU, p)
        print(f"   {nome}: {t*1000:.1f}ms")

    cap_80 = calcular_capacidade_maxima(MU, margem=0.80)
    cap_70 = calcular_capacidade_maxima(MU, margem=0.70)
    print(f"\nCapacidade recomendada:")
    print(f"Teto em 80% de utilização: {cap_80:.1f} req/s")
    print(f"Teto em 70% de utilização: {cap_70:.1f} req/s")
    print(f"λ atual: {LAMB} req/s → margem disponível (80%): "
          f"{cap_80 - LAMB:.1f} req/s")

    print(f"\nAnálise de sensibilidade — cenários de carga:")
    df = calcular_sensibilidade(LAMB, MU)
    print(df.to_string(index=False))
    print(f"\nObserve: +10% em λ dobra Lq (de 3.2 para 6.4).")
    print(f"Com +20%, Lq vai para 23. Com +30%, colapso")
    print(f"\n Gerando dashboard visual...")
    plotar_dashboard(LAMB, MU, rho, n_max=25)


if __name__ == "__main__":
    main()
  

Você pode pegar esse código completo no meu GitHub. Acessa lá!

Resultado da execução

Após executar todo o código, podemos notar várias informações acerca do servidor que possui os parâmetros λ e μ.

Por fim, abaixo é apresentado o dashboard visual que nos auxilia a ter uma visão mais ampla de todo os números apresentados até aqui.

Curtiu mas ainda não tem um servidor web ? Crie um servidor construindo seu próprio load balancer faça o teste !

Deixe um comentário