Mudança de contexto: o custo oculto da multitarefa

📖 15min read

Se o servidor estiver lento, aumentar threads?

Quando entrei na empresa, o servidor Spring Boot API pelo qual eu era responsável ficava lento sempre que havia muito tráfego. Como não sabia a causa, comecei pesquisando no Google.

“Resposta lenta do servidor Spring Boot” “Ajuste de desempenho do Tomcat”

Como resultado da pesquisa, os conselhos mais comuns encontrados em blogs e comunidades foram simples. ‘Aumenta o tamanho do pool de threads do Tomcat. Sua solicitação está aguardando porque não há trabalhadores suficientes.’

Pensei: “Ah, estamos com falta de trabalhadores!” Eu pensei simplesmente. Abri imediatamente as configurações de application.yml e aumentei o número de threads do padrão de 200 para 2.000. Pelos meus cálculos, o número de trabalhadores aumentou 10 vezes, então a velocidade de processamento tinha que ser mais rápida.

Mas depois de ver a tela de monitoramento após a implantação, congelei. Na verdade, o servidor se moveu mais lentamente, o uso da CPU aumentou, mas o número de solicitações processadas diminuiu. Parecia que os trabalhadores estavam apenas trabalhando no ar sem fazer nenhum trabalho.

Por que diabos o número de trabalhadores aumentou, mas a fábrica ficou mais lenta? Ao investigar o motivo, me deparei com o custo mais caro do sistema operacional, a ‘troca de contexto’.

Ter muitos trabalhadores não é uma coisa boa. O custo de alterná-los pode ser maior.

Revisão: Threads, lembra?

Para quem está lendo este artigo pela primeira vez, ou para quem não está familiarizado com o conteúdo do artigo anterior (Parte 4: Processos e Threads), vamos retroceder um pouco.

Em nossa visão de mundo de “centro de logística digital”:

Spring Boot é basicamente multithreaded. Cada vez que chega uma solicitação, um trabalhador (thread) é designado para fazer o trabalho. Então pensei simplesmente: “Se houver mais trabalhadores, mais solicitações serão processadas simultaneamente, certo?”

Mas havia algo que esqueci. É o número de núcleos de CPU, os principais trabalhadores da nossa fábrica.

Trabalho em turnos em um centro de distribuição digital

Na verdade, o núcleo da CPU, o núcleo de trabalho de um computador, só pode realizar uma tarefa por vez. (Baseado em single core) No entanto, ouvimos músicas, codificamos e usamos o KakaoTalk ao mesmo tempo. Como isso é possível?

Isso ocorre porque o gerente da fábrica (OS) ordena que os trabalhadores (CPUs) “mudem de trabalho” em uma velocidade incrivelmente rápida. “Toque a música por 0,001 segundos e pare! Envie KakaoTalk pelos próximos 0,001 segundos e pare!”

Isso é ‘Compartilhamento de Tempo’, e o processo no qual o trabalhador larga a ferramenta e pega uma nova ferramenta é ‘Troca de Contexto’.

O preço da troca de tarefas: hora de trocar de roupa

Isso foi o que aconteceu quando aumentei o número de threads para 2.000.

A CPU é um corpo único, com 2.000 thread workers gritando: “Livre-se disso!” A CPU se reúne continuamente com 2.000 pessoas para processar o trabalho de maneira justa.

O problema é que é necessário ‘tempo de preparação’ ao passar do trabalho do trabalhador A para o trabalho do trabalhador B.

Esse “tempo para registrar, guardar e ler” é chamado de “custo de mudança de contexto (despesas gerais)”. Quando há trabalhadores adequados, este custo é insignificante. Mas e se houver muitos trabalhadores? A CPU entra em uma situação em que organiza os registros dos trabalhadores o dia todo, mas não consegue realizar nenhum ‘trabalho real (cálculo)’. Esse foi o verdadeiro motivo pelo qual meu servidor estava lento.

Durante o momento da mudança de tarefa, a fábrica fica parada.

[Verificação de código] É necessariamente mais rápido só porque há muitos threads?

Vale a pena ver, ouvir. Vamos provar isso com código. Vamos comparar a velocidade de execução da mesma quantidade de operações de adição usando um thread e dividindo-o em 1 milhão de threads. O bom senso sugere que 1 milhão de unidades deveria ser mais rápido, mas a realidade é diferente.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ContextSwitchingTest {
    private static final int TASK_COUNT = 1_000_000;

    public static void main(String[] args) throws InterruptedException {
        // 1. Processar com thread unica (sem revezamento)
        long start = System.currentTimeMillis();
        for (int i = 0; i < TASK_COUNT; i++) {
            simpleTask();
        }
        System.out.println("Tempo thread unica: " + (System.currentTimeMillis() - start) + "ms");

        // 2. Processar com muitissimas threads (causa context switching)
        // Criar um pool de threads ilimitado (aviso: pode travar o computador)
        ExecutorService executor = Executors.newCachedThreadPool();
        start = System.currentTimeMillis();
        for (int i = 0; i < TASK_COUNT; i++) {
            executor.submit(() -> simpleTask());
        }
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.HOURS);
        System.out.println("Tempo multi-thread: " + (System.currentTimeMillis() - start) + "ms");
    }

    private static void simpleTask() {
        int a = 1 + 1; // Tarefa muito leve
    }
}

Exemplos de resultados (variam de acordo com o ambiente):

Análise: A tarefa em si (1+1) é tão simples que pode ser concluída em um piscar de olhos. No entanto, no método multithread, o custo de criar um milhão de threads e fazer com que o sistema operacional alterne entre eles é milhares de vezes mais caro do que o tempo de operação. O umbigo é maior que a barriga

Lições da prática: Encontrando o ponto ideal

Então, quantos threads são apropriados para um servidor Spring Boot? A resposta depende “do que o servidor faz”.

No entanto, assim como na situação que vivi, aumentar cegamente o número para 2.000 é demais. Isso ocorre porque à medida que o número de threads aumenta, mais memória (pilha) é consumida e a CPU fica sobrecarregada devido aos custos de troca de contexto.

Recentemente, tecnologias sem bloqueio, como Node.js e WebFlux do Spring, estão atraindo a atenção para resolver esse problema. Eles usam a estratégia de “não aumentar o número de threads, deixe uma pessoa processar rapidamente sem parar”.

Não existe uma resposta incondicional. Você deve escolher dependendo se o seu serviço exige muitos ‘turnos’ ou ‘corrida’.

Encerramento: Não existe almoço grátis

Muitas vezes acreditamos erroneamente que “processar coisas ao mesmo tempo é mais rápido”. No entanto, no mundo dos computadores, ‘simultâneo’ é na verdade apenas trabalho em turnos de alta velocidade, o que é quase um truque.

Depois de compreender a alteração de contexto, você verá por que o ajuste do servidor não se trata apenas de “aumentar os números”. O aumento indiscriminado de threads pode se tornar um veneno que sufoca o servidor.

Agora, as fábricas internas do computador (CPU, RAM, Processo) parecem estar funcionando muito bem. Agora vamos abrir a porta da fábrica e sair. Como enviamos os dados criados em nossa fábrica para outra fábrica (cliente) distante?

Na próxima vez, falaremos sobre redes, HTTP e aquela rede rodoviária invisível.

Deixe um comentário