La prima connessione WCF fatta nel nuovo AppDomain è molto lenta

Ho una libreria che uso che usa WCF per chiamare un servizio http per ottenere le impostazioni. Normalmente la prima chiamata impiega circa 100 millisecondi e le chiamate successive richiedono solo pochi millisecondi. Ma ho scoperto che quando creo un nuovo AppDomain, la prima chiamata WCF da quell’AppDomain richiede più di 2,5 secondi.

Qualcuno ha una spiegazione o una correzione sul perché la prima creazione di un canale WCF in un nuovo AppDomain richiederebbe così tanto tempo?

Questi sono i risultati del benchmark (quando si esegue senza debugger allegato in release a 64 bit), si noti come nella seconda serie di numeri le prime connessioni richiedono più di 25 volte

Running in initial AppDomain First Connection: 92.5018 ms Second Connection: 2.6393 ms Running in new AppDomain First Connection: 2457.8653 ms Second Connection: 4.2627 ms 

Questo non è un esempio completo ma mostra la maggior parte di come ho prodotto questi numeri:

 class Program { static void Main(string[] args) { Console.WriteLine("Running in initial AppDomain"); new DomainRunner().Run(); Console.WriteLine(); Console.WriteLine("Running in new thread and AppDomain"); DomainRunner.RunInNewAppDomain("test"); Console.ReadLine(); } } class DomainRunner : MarshalByRefObject { public static void RunInNewAppDomain(string runnerName) { var newAppDomain = AppDomain.CreateDomain(runnerName); var runnerProxy = (DomainRunner)newAppDomain.CreateInstanceAndUnwrap(typeof(DomainRunner).Assembly.FullName, typeof(DomainRunner).FullName); runnerProxy.Run(); } public void Run() { AppServSettings.InitSettingLevel(SettingLevel.Production); var test = string.Empty; var sw = Stopwatch.StartNew(); test += AppServSettings.ServiceBaseUrlBatch; Console.WriteLine("First Connection: {0}", sw.Elapsed.TotalMilliseconds); sw = Stopwatch.StartNew(); test += AppServSettings.ServiceBaseUrlBatch; Console.WriteLine("Second Connection: {0}", sw.Elapsed.TotalMilliseconds); } } 

La chiamata ad AppServSettings.ServiceBaseUrlBatch sta creando un canale per un servizio e chiamando un singolo metodo. Ho usato wireshark per guardare la chiamata e ci vogliono solo pochi millisecondi per ottenere una risposta dal servizio. Crea il canale con il seguente codice:

 public static ISettingsChannel GetClient() { EndpointAddress address = new EndpointAddress(SETTINGS_SERVICE_URL); BasicHttpBinding binding = new BasicHttpBinding { MaxReceivedMessageSize = 1024, OpenTimeout = TimeSpan.FromSeconds(2), SendTimeout = TimeSpan.FromSeconds(5), ReceiveTimeout = TimeSpan.FromSeconds(5), ReaderQuotas = { MaxStringContentLength = 1024}, UseDefaultWebProxy = false, }; cf = new ChannelFactory(binding, address); return cf.CreateChannel(); } 

Dalla profilatura dell’app emerge che nel primo caso la costruzione del canale e la creazione del canale e la chiamata del metodo richiedono meno di 100 millisecondi

Nel nuovo AppDomain, la costruzione del canale ha richiesto 763 millisecondi, 521 millisecondi per creare il canale, 1,098 millisecondi per chiamare il metodo sull’interfaccia.

TestSettingsRepoInAppDomain.DomainRunner.Run () 2.660,00 TestSettingsRepoInAppDomain.AppServSettings.get_ServiceBaseUrlBatch () 2.543.47 Tps.Core.Settings.Retriever.GetSetting (stringa, !! 0, !! 0, !! 0) 2.542.66 Tps.Core.Settings.Retriever.TryGetSetting (stringa, !! 0 e) 2,522.03 Tps.Core.Settings.ServiceModel.WcfHelper.GetClient () 1,371.21 Tps.Core.Settings.ServiceModel.IClientChannelExtensions.CallWithRetry (class System.ServiceModel.IClientChannel) 1,098,83

MODIFICARE

Dopo aver usato perfmon con l’object di caricamento CLR .NET, posso vedere che quando carica il secondo AppDomain sta caricando più classi in memoria di quanto non faccia inizialmente. La prima linea piatta è una pausa che inserisco dopo il primo appdomain, lì ci sono 218 classi caricate. Il secondo AppDomain causa il caricamento di 1.944 classi totali.

Presumo che sia il caricamento di tutte queste classi che occupano tutto il tempo, quindi ora la domanda è, che classi sta caricando e perché?

inserisci la descrizione dell'immagine qui

AGGIORNARE

La risposta risulta essere dovuta al fatto che solo un AppDomain è in grado di avvantaggiarsi delle DLL native del sistema di immagini. Quindi la lentezza nel secondo appdomain era il fatto di dover riutilizzare tutto il System. * Dll usato da wcf. Il primo appdomain poteva usare le versioni native pre-esistenti di quelle DLL, quindi non aveva lo stesso costo di avvio.

Dopo aver esaminato il LoaderOptimizationAttribute suggerito da Petar, che sembrava risolvere il problema, utilizzando MultiDomain o MultiDomainHost il secondo AppDomain impiega la stessa quantità di tempo della prima volta per accedere a roba su wcf

Qui puoi vedere l’opzione di default, nota come nel secondo AppDomain nessuno degli assembly dice Native, nel senso che devono essere tutti riabilitati, che è quello che prendeva tutto il tempo

inserisci la descrizione dell'immagine qui

Ecco dopo aver aggiunto LoaderOptimization (LoaderOptimization.MultiDomain) a Main. Puoi vedere che tutto è caricato nell’AppDomain condiviso

inserisci la descrizione dell'immagine qui

Ecco dopo l’utente LoaderOptimization (LoaderOptimization.MultiDomainHost) al main. Puoi vedere che tutte le DLL di sistema sono condivise, ma le mie dll e le eventuali non presenti nel GAC vengono caricate separatamente in ciascun AppDomain

inserisci la descrizione dell'immagine qui

Quindi, per il servizio che ha generato questa domanda utilizzando MultiDomainHost è la risposta, perché ha un tempo di avvio rapido e posso scaricare AppDomains per rimuovere gli assembly creati dynamicmente che il servizio utilizza

Puoi decorare l’attributo Main con LoaderOptimization per dire al loader CLR come caricare le classi.

 [LoaderOptimization(LoaderOptimization.MultiDomain)] MultiDomain - Indicates that the application will probably have many domains that use the same code, and the loader must share maximal internal resources across application domains. 

Avete un proxy HTTP definito in IE? (forse uno script di configurazione automatica). Questo può essere una causa.

Altrimenti direi che è il tempo necessario per caricare tutte le DLL. Prova a deparare la creazione del proxy dalla chiamata actull al servizio, per vedere cosa ci vuole del tempo.

Ho trovato il seguente articolo che parla di come solo il primo AppDomain può utilizzare le dll di immagini native, quindi un appdomain figlio sarà sempre costretto a JIT molte cose che l’AppDomain iniziale non è necessario. Questo potrebbe portare all’impatto delle prestazioni che sto vedendo, ma sarebbe ansible in qualche modo non ottenere questa penalità sulle prestazioni?

Se è presente un’immagine nativa per l’assembly, solo il primo AppDomain può utilizzare l’immagine nativa. Tutti gli altri AppDomain dovranno compilare il codice JIT, il che può comportare un costo significativo della CPU.