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’.

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.

[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”.

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.