← Voltar para Projetos e Artigos
Caso real de produção · Diagnóstico e procedimento de contenção · Documento de referência
Uma tela de calendário parou de carregar para um único cliente de um sistema multi-tenant. Todos os outros tenants, rodando o mesmo código na mesma instância de SQL Server, seguiam normais.
| Fato observado | O que ele elimina |
|---|---|
| A query executada direto no SSMS respondeu em 9 ms (3 ms de CPU) | Elimina query lenta, índice faltando e estatística desatualizada |
| A base do cliente era menor que a base de teste (8 linhas na tabela principal) | Elimina volume de dados como causa |
| A lentidão era intermitente e terminava sempre em timeout de 30 s | Indica espera por recurso, não processamento lento |
| Todas as telas daquele cliente travavam, não apenas uma | Elimina um endpoint específico — aponta para recurso compartilhado |
A contagem de sessões abertas revelou o número que fechou o diagnóstico: 187 conexões ativas na base daquele cliente, todas ociosas (sleeping), para um sistema com 2 ou 3 usuários simultâneos.
Abrir uma conexão com um banco de dados é uma operação cara: envolve handshake de rede, autenticação e alocação de recursos no servidor. Para evitar pagar esse custo a cada requisição, o ADO.NET mantém um pool — um conjunto de conexões já abertas e prontas para reuso.
Quando a aplicação "abre" uma conexão, ela na verdade pega emprestada uma do pool. Quando "fecha", ela devolve a conexão ao pool, que a mantém viva para a próxima requisição.
Essa é exatamente a situação do caso: as salas estavam vazias (o banco respondia em 9 ms), mas não havia chave disponível (o pool estava esgotado por conexões que ninguém devolveu).
Este é o detalhe que torna o caso didático. O ADO.NET mantém um pool separado para cada connection string distinta. Em um sistema multi-tenant onde cada cliente tem seu próprio banco, cada cliente tem também o seu próprio pool, isolado dos demais.
IDataReader ou SqlConnection sem Close(), Dispose() ou using). Os três passos a seguir tratam o sintoma e devolvem o sistema ao ar. A correção definitiva é no código.Max Pool Size padrão do .NET é 100 por poolsleeping em número muito acima dos usuários reais é a assinatura do vazamentoListar exatamente quais sessões seriam encerradas, sem encerrar nenhuma. Este passo é apenas leitura: pode ser rodado em produção, a qualquer hora, sem efeito colateral.
-- Passo 1: apenas LISTA as sessões candidatas. Não altera nada.
SELECT
s.session_id,
s.status,
s.open_transaction_count,
DATEDIFF(MINUTE, s.last_request_end_time, GETDATE()) AS min_ocioso,
s.host_name,
s.program_name,
s.login_time,
'KILL ' + CAST(s.session_id AS VARCHAR) + ';' AS comando
FROM sys.dm_exec_sessions s
WHERE DB_NAME(s.database_id) = 'NOME_DA_BASE' -- 1. só a base afetada
AND s.is_user_process = 1 -- 2. ignora processos internos
AND s.session_id <> @@SPID -- 3. nunca a própria sessão
AND s.status = 'sleeping' -- 4. só as que não executam nada
AND s.open_transaction_count = 0 -- 5. só as sem transação aberta
AND s.program_name LIKE '%SqlClient%' -- 6. só a aplicação, não o SSMS
AND DATEDIFF(MINUTE, s.last_request_end_time, GETDATE()) >= 5 -- 7. ociosas há 5 min+
ORDER BY min_ocioso DESC;
Cada filtro do WHERE existe por um motivo. Entender o porquê de cada um é o que separa "rodar um script da internet" de "saber o que se está fazendo".
| # | Filtro | Por que existe |
|---|---|---|
| 1 | DB_NAME(...) = 'base' | Garante que nenhum outro tenant da instância seja tocado. É o filtro mais importante em ambiente compartilhado. |
| 2 | is_user_process = 1 | Exclui as sessões internas do próprio SQL Server, que nunca devem ser encerradas. |
| 3 | session_id <> @@SPID | Evita que o script mate a própria sessão que o está executando. |
| 4 | status = 'sleeping' | Sessão dormindo não está processando nada. Encerrar uma sessão em execução poderia interromper uma operação legítima em andamento. |
| 5 | open_transaction_count = 0 | A trava mais crítica. Matar sessão com transação aberta dispara rollback, que pode demorar e travar ainda mais o banco. |
| 6 | program_name LIKE '%SqlClient%' | Restringe às conexões vindas da aplicação .NET, preservando SSMS, jobs e ferramentas de terceiros. |
| 7 | min_ocioso >= 5 | Uma conexão ociosa há segundos pode estar entre duas operações de um usuário ativo. Cinco minutos indica abandono real. |
comando monta o KILL pronto de cada sessão. Isso permite copiar e executar manualmente uma ou outra, caso você prefira agir cirurgicamente em vez de rodar o loop do Passo 2.Executar KILL em cada sessão listada no Passo 1. O KILL encerra a sessão do lado do SQL Server, liberando o slot correspondente.
KILL do SQL Server não aceita subconsulta nem lista — ele exige um número literal de sessão por vez (KILL 53). Por isso é preciso iterar. O cursor é declarado como LOCAL FAST_FORWARD, que é a variação mais leve, somente leitura e de sentido único.-- Passo 2: encerra as sessões. Use os MESMOS filtros do Passo 1.
DECLARE @spid INT, @sql NVARCHAR(100), @mortas INT = 0;
DECLARE cur CURSOR LOCAL FAST_FORWARD FOR
SELECT s.session_id
FROM sys.dm_exec_sessions s
WHERE DB_NAME(s.database_id) = 'NOME_DA_BASE'
AND s.is_user_process = 1
AND s.session_id <> @@SPID
AND s.status = 'sleeping'
AND s.open_transaction_count = 0
AND s.program_name LIKE '%SqlClient%'
AND DATEDIFF(MINUTE, s.last_request_end_time, GETDATE()) >= 5;
OPEN cur;
FETCH NEXT FROM cur INTO @spid;
WHILE @@FETCH_STATUS = 0
BEGIN
BEGIN TRY
SET @sql = N'KILL ' + CAST(@spid AS NVARCHAR(10));
EXEC sp_executesql @sql;
SET @mortas = @mortas + 1;
END TRY
BEGIN CATCH
PRINT 'Falhou no SPID ' + CAST(@spid AS VARCHAR) + ': ' + ERROR_MESSAGE();
END CATCH
FETCH NEXT FROM cur INTO @spid;
END
CLOSE cur;
DEALLOCATE cur;
PRINT 'Sessoes encerradas: ' + CAST(@mortas AS VARCHAR);
TRY/CATCH dentro do loop: entre o momento em que o cursor leu a lista e o momento do KILL, uma sessão pode ter encerrado sozinha. Sem o tratamento, o erro abortaria o script no meio, deixando o restante das sessões vivas. Com ele, a falha é registrada e o loop continua.KILL. Pode haver alguns erros transitórios nesse intervalo.Confirmar numericamente que as conexões foram liberadas, e confirmar funcionalmente que a aplicação voltou ao normal.
-- Passo 3: confere quantas conexões restaram na base
SELECT COUNT(*) AS conexoes
FROM sys.dm_exec_sessions
WHERE DB_NAME(database_id) = 'NOME_DA_BASE'
AND program_name LIKE '%SqlClient%';
| Resultado | Leitura | Ação |
|---|---|---|
| Caiu para um punhado (menos de 10) | Conexões liberadas com sucesso. Diagnóstico confirmado. | Testar a aplicação e registrar os números |
| Caiu, mas a lentidão continua | O gargalo está no pool do lado da aplicação, não no servidor | Reciclar o application pool no IIS |
| Continua alto | Os filtros não casaram, ou as conexões são recriadas imediatamente | Revisar o nome da base e o program_name no Passo 1 |
PRINT)Esse registro serve a dois propósitos: comprovar o diagnóstico e medir o ritmo de acúmulo. Se o número voltar a 187 em três dias, você sabe que tem até três dias para corrigir o código.
NOME_DA_BASE pelo nome real do banco em todos os três passos. Execute sempre na ordem 1 → 2 → 3, e nunca pule o Passo 1.| Ordem | Passo | Risco | Tempo |
|---|---|---|---|
| 1 | Listar candidatas | Nenhum — somente leitura | < 1 s |
| 2 | Encerrar sessões | Baixo — filtros protegem sessões ativas | Segundos |
| 3 | Validar contagem | Nenhum — somente leitura | < 1 s |
| 4 | Testar a tela no app | Nenhum | 1 min |
Se após o Passo 2 a contagem caiu mas a lentidão persistiu, o pool preso está do lado do servidor de aplicação. O comando abaixo recicla o processo do IIS:
:: Executar no servidor de aplicação, com privilégio administrativo
%windir%\system32\inetsrv\appcmd recycle apppool /apppool.name:"NOME_DO_APP_POOL"
Uma operação que leva 9 ms e estoura em 30 s não está devagar — está parada, esperando algum recurso. Lentidão gradual e travamento por espera têm causas e soluções completamente diferentes. A primeira pergunta do diagnóstico deve ser sempre: o tempo está sendo gasto ou esperado?
A hipótese inicial natural era query lenta na base do cliente. Um SET STATISTICS IO, TIME ON derrubou essa hipótese em 30 segundos e evitou horas de trabalho em índices que não resolveriam nada.
| Padrão observado | O que ele sugere |
|---|---|
| Uma tela só, sempre lenta | Query, índice ou volume de dados |
| Todas as telas, intermitente, um tenant só | Recurso compartilhado por tenant — pool ou conexão |
| Todas as telas, todos os tenants | Infraestrutura, rede ou o servidor como um todo |
KILL compra tempo — a correção definitiva é garantir que todo IDataReader e toda SqlConnection sejam fechados em um bloco using ou finally. Enquanto isso não acontece, vale agendar um reciclo diário do application pool na madrugada para conter o acúmulo.KILL filtrado nas sessões ociosas daquela base — efeito imediato| Termo | Significado |
|---|---|
| Pool de conexões | Conjunto de conexões de banco já abertas e mantidas pelo ADO.NET para reuso entre requisições |
| Max Pool Size | Limite de conexões simultâneas de um pool. Padrão do .NET: 100 |
| Vazamento de conexão | Conexão emprestada do pool e nunca devolvida, por falta de Close(), Dispose() ou using |
Sessão sleeping | Conexão aberta no SQL Server que não está executando comando algum no momento |
| SPID | Server Process ID — identificador numérico de uma sessão no SQL Server |
@@SPID | Variável de sistema que retorna o SPID da sessão que está executando o comando |
| DMV | Dynamic Management View — visões de sistema que expõem o estado interno do SQL Server |
sys.dm_exec_sessions | DMV que lista todas as sessões conectadas, com status, banco, aplicação e transações abertas |
KILL | Comando que encerra uma sessão no SQL Server. Aceita apenas um SPID literal por vez |
Cursor FAST_FORWARD | Cursor somente leitura e de sentido único — a variação mais leve do SQL Server |
open_transaction_count | Número de transações abertas em uma sessão. Valor maior que zero exige cautela antes de qualquer KILL |
| Application pool (IIS) | Processo isolado do IIS que hospeda uma ou mais aplicações web |
| Rollback | Desfazimento de uma transação. Disparado automaticamente ao encerrar uma sessão com transação aberta |
| Multi-tenant | Arquitetura em que uma mesma aplicação atende vários clientes com isolamento de dados entre eles |
Caso de estudo — Pool de conexões esgotado em ambiente multi-tenant (SQL Server). Diagnóstico conduzido por eliminação de hipóteses, com procedimento de contenção validado em produção.
Fábio Medina Favarin · Documentação técnica · Caso real de produção.