Abbiamo notato che alcune chiamate ai servizi Web molto piccole richiedevano molto più tempo del previsto. Abbiamo svolto alcune indagini e messo in atto alcuni timer e abbiamo ristretto il problema alla creazione di un'istanza del nostro DbContext Entity Framework 6. Non la query stessa, solo la creazione del contesto. Da allora ho messo un po 'di logging per vedere in media quanto tempo ci vuole per creare un'istanza del nostro DbContext e sembra che fosse di circa 50ms.
Dopo che l'applicazione è stata riscaldata, la creazione del contesto non è lenta. Dopo il riciclo di un'app inizia a 2-4 ms (che è ciò che vediamo nei nostri ambienti di sviluppo). La creazione del contesto sembra rallentare nel tempo. Nelle prossime due ore si insinuerà fino alla gamma di 50-80 ms e si spegnerà.
Il nostro contesto è un contesto code-first piuttosto ampio con circa 300 entità, tra cui alcune relazioni piuttosto complesse tra alcune entità. Stiamo eseguendo EF 6.1.3. Stiamo facendo un "un contesto per richiesta", ma per la maggior parte delle nostre chiamate all'API Web si tratta solo di una o due query. Creare un contesto che richiede 60 + ms e quindi eseguire una query da 1 ms è un po 'insoddisfacente. Abbiamo circa 10.000 richieste al minuto, quindi non siamo un sito poco utilizzato.
Ecco un'istantanea di ciò che vediamo. I tempi sono in MS, il grande tuffo è una distribuzione che ha riciclato il dominio dell'app. Ogni linea è uno dei 4 diversi server web. Si noti che non è sempre lo stesso server.
Ho fatto un dump della memoria per cercare di capire cosa sta succedendo e qui ci sono le statistiche dell'heap:
00007ffadddd1d60 70821 2266272 System.Reflection.Emit.GenericFieldInfo
00007ffae02e88a8 29885 2390800 System.Linq.Enumerable+WhereSelectListIterator`2[[NewRelic.Agent.Core.WireModels.MetricDataWireModel, NewRelic.Agent.Core],[System.Single, mscorlib]]
00007ffadda7c1a0 1462 2654992 System.Collections.Concurrent.ConcurrentDictionary`2+Node[[System.Object, mscorlib],[System.Object, mscorlib]][]
00007ffadd4eccf8 83298 2715168 System.RuntimeType[]
00007ffadd4e37c8 24667 2762704 System.Reflection.Emit.DynamicMethod
00007ffadd573180 30013 3121352 System.Web.Caching.CacheEntry
00007ffadd2dc5b8 35089 3348512 System.String[]
00007ffadd6734b8 35233 3382368 System.RuntimeMethodInfoStub
00007ffadddbf0a0 24667 3749384 System.Reflection.Emit.DynamicILGenerator
00007ffae04491d8 67611 4327104 System.Data.Entity.Core.Metadata.Edm.MetadataProperty
00007ffadd4edaf0 57264 4581120 System.Signature
00007ffadd4dfa18 204161 4899864 System.RuntimeMethodHandle
00007ffadd4ee2c0 41900 5028000 System.Reflection.RuntimeParameterInfo
00007ffae0c9e990 21560 5346880 System.Data.SqlClient._SqlMetaData
00007ffae0442398 79504 5724288 System.Data.Entity.Core.Metadata.Edm.TypeUsage
00007ffadd432898 88807 8685476 System.Int32[]
00007ffadd433868 9985 9560880 System.Collections.Hashtable+bucket[]
00007ffadd4e3160 92105 10315760 System.Reflection.RuntimeMethodInfo
00007ffadd266668 493622 11846928 System.Object
00007ffadd2dc770 33965 16336068 System.Char[]
00007ffadd26bff8 121618 17335832 System.Object[]
00007ffadd2df8c0 168529 68677312 System.Byte[]
00007ffadd2d4d08 581057 127721734 System.String
0000019cf59e37d0 166894 143731666 Free
Total 5529765 objects
Fragmented blocks larger than 0.5 MB:
Addr Size Followed by
0000019ef63f2140 2.9MB 0000019ef66cfb40 Free
0000019f36614dc8 2.8MB 0000019f368d6670 System.Data.Entity.Core.Query.InternalTrees.SimpleColumnMap[]
0000019f764817f8 0.8MB 0000019f76550768 Free
0000019fb63a9ca8 0.6MB 0000019fb644eb38 System.Data.Entity.Core.Common.Utils.Set`1[[System.Data.Entity.Core.Metadata.Edm.EntitySet, EntityFramework]]
000001a0f6449328 0.7MB 000001a0f64f9b48 System.String
000001a0f65e35e8 0.5MB 000001a0f666e2a0 System.Collections.Hashtable+bucket[]
000001a1764e8ae0 0.7MB 000001a17659d050 System.RuntimeMethodHandle
000001a3b6430fd8 0.8MB 000001a3b6501aa0 Free
000001a4f62c05c8 0.7MB 000001a4f636e8a8 Free
000001a6762e2300 0.6MB 000001a676372c38 System.String
000001a7761b5650 0.6MB 000001a776259598 System.String
000001a8763c4bc0 2.3MB 000001a8766083a8 System.String
000001a876686f48 1.4MB 000001a8767f9178 System.String
000001a9f62adc90 0.7MB 000001a9f63653c0 System.String
000001aa362b8220 0.6MB 000001aa36358798 Free
Mi sembra un bel po 'di metadata e tipo di errore.
Cose che abbiamo provato:
Cosa possiamo investigare ulteriormente? Capisco che la cache utilizzata da EF sia estesa per velocizzare le cose. Fa più cose nella cache, rallenta la creazione del contesto? C'è un modo per vedere esattamente cosa c'è in quella cache per rimpolpare le cose strane lì dentro? Qualcuno sa cosa possiamo fare specificamente per accelerare la creazione del contesto?
Aggiornamento - 30/05/17
Ho preso la fonte EF6 e ho compilato la nostra versione per rispettare alcuni tempi. Gestiamo un sito molto popolare e la raccolta di enormi quantità di informazioni sui tempi è complicata e non ho ottenuto il massimo che volevo, ma fondamentalmente abbiamo scoperto che tutto il rallentamento deriva da questo metodo
public void ForceOSpaceLoadingForKnownEntityTypes()
{
if (!_oSpaceLoadingForced)
{
// Attempting to get o-space data for types that are not mapped is expensive so
// only try to do it once.
_oSpaceLoadingForced = true;
Initialize();
foreach (var set in _genericSets.Values.Union(_nonGenericSets.Values))
{
set.InternalSet.TryInitialize();
}
}
}
Ogni iterazione di quel foreach colpisce per ciascuna delle entità definite da un DBSet nel nostro contesto. Ogni iterazione è relativamente breve 0,1 -3 ms, ma quando si aggiungono le 254 entità che abbiamo aggiunto. Non abbiamo ancora capito perché è veloce all'inizio e rallenta.
Qui è dove vorrei iniziare a risolvere il problema, senza passare a una soluzione più aziendale.
Our context is a fairly large code-first context with around 300 entities
Sebbene EF sia notevolmente migliorata nel tempo, inizierei seriamente a ritagliare le cose una volta arrivate a 100 entità (in realtà inizierei molto prima, ma questo sembra essere un numero magico che molte persone hanno affermato - il consenso?). Pensa a come progettare "contesti", ma usa invece la parola "dominio"? In questo modo puoi vendere i tuoi exec che stai applicando "domain driven design" per correggere l'applicazione? Forse stai progettando per i futuri "microservizi", quindi usi due parole d'ordine in un singolo paragrafo. ;-)
Non sono un grande fan di EF nello spazio Enterprise, quindi tendo ad evitarlo per applicazioni su larga scala o ad alte prestazioni. Il tuo chilometraggio può variare. Per SMB, probabilmente è perfettamente a posto. Mi imbatto in clienti che lo usano, comunque.
Non sono sicuro che le seguenti idee siano completamente aggiornate, ma sono altre cose che prenderei in considerazione, basate sull'esperienza.
Sembra che tu stia già eseguendo un qualche tipo di profiler sull'applicazione, quindi presumo che tu abbia anche esaminato le tue query SQL e i possibili guadagni in termini di prestazioni. Sì, so che non è il problema che stai cercando di risolvere, ma è qualcosa che può contribuire all'intero problema dal punto di vista dell'utente.
In risposta al commento di @ WiktorZichia sul non rispondere alla domanda sul problema delle prestazioni, il modo migliore per sbarazzarsi di questi tipi di problemi in un sistema aziendale è quello di sbarazzarsi di Entity Framework. Ci sono trade off in ogni decisione. EF è una grande astrazione e accelera lo sviluppo. Ma viene fornito con un sovraccarico non necessario che può danneggiare i sistemi su larga scala. Ora, tecnicamente, non ho ancora risposto alla domanda "come risolvo questo problema nel modo in cui sto cercando di risolverlo", quindi questo potrebbe ancora essere visto come un fallimento.