Tengo una API de .net core usando Entity Framework Core. El contexto DB está registrado en startup.cs así:
services.AddDbContext<AppDBContext>(options => options.UseSqlServer(connectionString, providerOptions => providerOptions.CommandTimeout(60)));En la cadena de conexión configuré
Pooling=true;Max Pool Size=100;Connection Timeout=300El controlador llama a los métodos en un servicio que, a su vez, hace llamadas a los métodos aysnc en un repositorio para la recuperación y el procesamiento de datos.
Todo funcionó bien si el usuario simultáneo tiene menos de 500 durante la prueba de carga. Sin embargo, más allá de ese número, empiezo a ver muchos errores de tiempo de espera caducado. Cuando revisé la base de datos, no hay punto muerto, pero pude ver más de 100 conexiones en modo de suspensión (la API está alojada en dos pods de kubernetes). Supervisé estas conexiones durante la prueba y parecía que en lugar de reutilizar las conexiones inactivas actuales, se agregaron nuevas al grupo. Según tengo entendido, el núcleo del marco de la entidad administra las conexiones de apertura y cierre, pero este no parecía ser el caso. ¿O me estoy perdiendo algo?
El error se ve así:
StatusCode":500,"Message":"Error:Timeout expired. El período de tiempo de espera transcurrió antes de obtener una conexión del grupo. Esto puede haber ocurrido porque todas las conexiones agrupadas estaban en uso y se alcanzó el tamaño máximo del grupo. Rastreo de pila:
en
Microsoft.Data.ProviderBase.DbConnectionFactory.TryGetConnection(DbConnection owningConnection, TaskCompletionSource`1 reintento, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionInternal& connection)\n
en
Microsoft.Data.ProviderBase.DbConnectionInternal.TryOpenConnectionInternal(DbConnection outsideConnection, DbConnectionFactory connectionFactory, TaskCompletionSource
1 retry, DbConnectionOptions userOptions)\n at Microsoft.Data.SqlClient.SqlConnection.TryOpen(TaskCompletionSource1 reintento, SqlConnectionOverrides overrides)\n en Microsoft.Data. SqlClient.SqlConnection.Open(SqlConnectionOverrides overrides)\n
en Microsoft.Data.SqlClient.SqlConnection.Open()\n en
Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenInternal(Errores booleanos esperados)\n
en
Microsoft.EntityFrameworkCore.Storage.RelationalConnection.Open(Errores booleanos esperados)\n en Microsoft.EntityFrameworkCore.Storage.RelationalConnection.BeginTransaction(IsolationLevel aislamientoNivel)\n................... ..
Un ejemplo de cómo se utilizó el dbcontext :
el controlador llama a un método en una clase de servicio:
var result = await _myservice.SaveUserStatusAsync(userId, status); luego en 'myservice' :
var user = await _userRepo.GetUserAsync(userId); ....set user status to new value and then return await _userRepo.UpdateUserAsync(user); luego en 'userrepo' :
_context.user.Update(user); var updated = await _context.SaveChangesAsync(); return updated > 0;Actualizar:
Muchas gracias a Ivan Yang, quien generosamente ofreció la recompensa. Aunque todavía estoy investigando, he aprendido mucho al leer todos los comentarios y respuestas a continuación. Esto es lo que he intentado hasta ahora: aumenté el tamaño del grupo a 200 (sé que no es la forma correcta de solucionar el problema), aumenté la cantidad de pods para que la API ahora se ejecute en 4 pods y asigné más memoria a cada vaina. El resultado final hasta ahora ha sido bueno: 500 errores desaparecen por completo con hasta 2000 usuarios concurrentes. Actualizaré esta pregunta con mis hallazgos después de probar otras opciones.
Intente desactivar la agrupación. La agrupación intenta mantener abiertas las conexiones. Si lo apaga, usa el tiempo de espera del proveedor de 60 segundos que configuró en el DI
las fugas de conexión causan tales problemas, las conexiones probablemente no se cierren correctamente a menos que se use el recolector de basura para desechar todas esas conexiones colgantes usando IDisposable , finalmente se puede agregar una cláusula para garantizar que las conexiones se cierren después de su uso.
El enlace es útil para entender este problema.
En lo que respecta al marco de la entidad, el tamaño máximo del grupo se puede lograr manteniendo muchos objetos en el contexto de la base de datos, mientras que puede materializarlos utilizando las funciones FirstOrDefault o ToList , ya que las consultas pueden mantener conexiones con el servidor de la base de datos.
Tenga en cuenta que DbContext implementa IDisposable .
La mejor práctica (por muchas razones, no solo por la administración de conexiones) es actualizar su DbContext en una declaración de uso:
using(MyContext context = new MyContext()) { // do your work }Escribí una pequeña biblioteca que lo ayuda a implementar y hacer cumplir un patrón como este.
Error: el tiempo de espera expiró. El período de tiempo de espera transcurrió antes de obtener una conexión del grupo. Esto puede haber ocurrido porque todas las conexiones agrupadas estaban en uso y se alcanzó el tamaño máximo del grupo.
Esto es casi siempre una fuga de conexión. Y aquí el hecho de que sus consultas son de corta duración y ve conexiones inactivas en el servidor lo confirma. En algún lugar estás dejando una conexión abierta.
Un DbContext abrirá/cerrará la conexión subyacente y la devolverá al grupo en Dispose. Pero si inicia una transacción en una conexión y no confirma ni revierte, la conexión se segregará en el grupo y no se reutilizará. O si devuelve un IEnumerable o un DataReader que nunca se itera ni se desecha, la conexión no se puede reutilizar.
Mire las sesiones "dormidas" para ver cuál fue su última consulta y haga una referencia cruzada con su código para rastrear el sitio de la llamada que filtró la conexión. Primero intente con los DMV, p.
select s.session_id, s.open_transaction_count, ib.event_info from sys.dm_exec_sessions s cross apply sys.dm_exec_input_buffer(s.session_id,null) ibO inicie un seguimiento de eventos extendidos si es necesario.