一連のプラグイン (アセンブリ) を独自の AppDomain にロードする Windows サービスがあります。各プラグインは、SOA の意味での「サービス境界」に合わせて調整されているため、独自のデータベースへのアクセスを担当します。別の AppDomain では、EF が 3 倍から 5 倍遅くなることがわかりました。
EF が初めて DbContext を作成してデータベースにアクセスするとき、AppDomain ごとに繰り返す必要がある (つまり、AppDomain 間でキャッシュされない) いくつかのセットアップ作業を行う必要があることを私は知っています。EF コードがプラグインに対して完全に自己完結型である (したがって、AppDomain に自己完結型である) ことを考慮すると、タイミングが親 AppDomain からのタイミングに匹敵すると予想していました。なぜ違うのですか?
.NET 4/EF 4.4 と .NET 4.5/EF 5 の両方をターゲットにしてみました。
サンプルコード
EF.csproj
Program.cs
class Program
{
static void Main(string[] args)
{
var watch = Stopwatch.StartNew();
var context = new Plugin.MyContext();
watch.Stop();
Console.WriteLine("outside plugin - new MyContext() : " + watch.ElapsedMilliseconds);
watch = Stopwatch.StartNew();
var posts = context.Posts.FirstOrDefault();
watch.Stop();
Console.WriteLine("outside plugin - FirstOrDefault(): " + watch.ElapsedMilliseconds);
var pluginDll = Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory + @"..\..\..\EF.Plugin\bin\Debug\EF.Plugin.dll");
var domain = AppDomain.CreateDomain("other");
var plugin = (IPlugin) domain.CreateInstanceFromAndUnwrap(pluginDll, "EF.Plugin.SamplePlugin");
plugin.FirstPost();
Console.ReadLine();
}
}
EF.Interfaces.csproj
IPlugin.cs
public interface IPlugin
{
void FirstPost();
}
EF.Plugin.csproj
MyContext.cs
public class MyContext : DbContext
{
public IDbSet<Post> Posts { get; set; }
}
Post.cs
public class Post
{
public int Id { get; set; }
}
SamplePlugin.cs
public class SamplePlugin : MarshalByRefObject, IPlugin
{
public void FirstPost()
{
var watch = Stopwatch.StartNew();
var context = new MyContext();
watch.Stop();
Console.WriteLine(" inside plugin - new MyContext() : " + watch.ElapsedMilliseconds);
watch = Stopwatch.StartNew();
var posts = context.Posts.FirstOrDefault();
watch.Stop();
Console.WriteLine(" inside plugin - FirstOrDefault(): " + watch.ElapsedMilliseconds);
}
}
サンプルタイミング
ノート:
- これは、空のデータベース テーブル (0 行) に対してクエリを実行しています。
- タイミングは意図的に最初の呼び出しだけを見ています。後続の呼び出しははるかに高速ですが、親 AppDomain と比較して、子 AppDomain では相対的に 3 倍から 5 倍遅くなります。
実行 1
外部プラグイン - new MyContext() : 55 外部プラグイン - FirstOrDefault(): 783 プラグイン内 - new MyContext() : 352 プラグイン内 - FirstOrDefault(): 2675
実行 2
外部プラグイン - new MyContext() : 53 外部プラグイン - FirstOrDefault(): 798 プラグイン内 - new MyContext() : 355 プラグイン内 - FirstOrDefault(): 2687
実行 3
外部プラグイン - new MyContext() : 45 外部プラグイン - FirstOrDefault(): 778 プラグイン内 - new MyContext() : 355 プラグイン内 - FirstOrDefault(): 2683
AppDomain 調査
AppDomains のコストをさらに調査した結果、後続の AppDomains はシステム DLL を再 JIT する必要があるため、AppDomain の作成には固有の起動コストがかかるという提案があるようです。それがここで起こっていることですか?JIT 処理は AppDomain の作成時に行われると予想していましたが、呼び出されたときはおそらく EF JIT 処理でしょうか?
再 JIT のリファレンス: http://msdn.microsoft.com/en-us/magazine/cc163655.aspx#S8
タイミングは似ていますが、関連があるかどうかはわかりません: 新しい AppDomain で作成された最初の WCF 接続が非常に遅い
更新 1
AppDomains全体でEF通信があるという@Yasserの提案に基づいて、これをさらに分離しようとしました。私はこれが事実だとは思わない。
EF.csproj から EF 参照を完全に削除しました。画像を投稿するのに十分な担当者ができたので、これがソリューションの構造です。
ご覧のとおり、プラグインのみが Entity Framework への参照を持っています。また、プラグインのみが EntityFramework.dll を含む bin フォルダーを持っていることも確認しました。
EF アセンブリが AppDomain に読み込まれているかどうかを確認するヘルパーを追加しました。また、データベースへの呼び出し後、追加の EF アセンブリ (動的プロキシなど) も読み込まれることを確認しました (表示されていません)。
したがって、EF がさまざまな時点で読み込まれたかどうかを確認します。
- プラグインを呼び出す前にメインで
- データベースにアクセスする前のプラグイン
- データベースにアクセスした後のプラグイン
- プラグインを呼び出した後のメイン
... を生成します:
メイン - IsEFLoaded: False プラグイン - IsEFLoaded: True プラグイン - new MyContext() : 367 プラグイン - FirstOrDefault(): 2693 プラグイン - IsEFLoaded: True メイン - IsEFLoaded: False
そのため、AppDomains は (予想どおり) 完全に分離されており、タイミングはプラグイン内で同じです。
更新されたサンプル コード
Program.cs
class Program
{
static void Main(string[] args)
{
var dir = Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory + @"..\..\..\EF.Plugin\bin\Debug");
var evidence = new Evidence();
var setup = new AppDomainSetup { ApplicationBase = dir };
var domain = AppDomain.CreateDomain("other", evidence, setup);
var pluginDll = Path.Combine(dir, "EF.Plugin.dll");
var plugin = (IPlugin) domain.CreateInstanceFromAndUnwrap(pluginDll, "EF.Plugin.SamplePlugin");
Console.WriteLine("Main - IsEFLoaded: " + Helper.IsEFLoaded());
plugin.FirstPost();
Console.WriteLine("Main - IsEFLoaded: " + Helper.IsEFLoaded());
Console.ReadLine();
}
}
Helper.cs
(ええ、私はこのために別のプロジェクトを追加するつもりはありませんでした…)
public static class Helper
{
public static bool IsEFLoaded()
{
return AppDomain.CurrentDomain
.GetAssemblies()
.Any(a => a.FullName.StartsWith("EntityFramework"));
}
}
SamplePlugin.cs
public class SamplePlugin : MarshalByRefObject, IPlugin
{
public void FirstPost()
{
Console.WriteLine("Plugin - IsEFLoaded: " + Helper.IsEFLoaded());
var watch = Stopwatch.StartNew();
var context = new MyContext();
watch.Stop();
Console.WriteLine("Plugin - new MyContext() : " + watch.ElapsedMilliseconds);
watch = Stopwatch.StartNew();
var posts = context.Posts.FirstOrDefault();
watch.Stop();
Console.WriteLine("Plugin - FirstOrDefault(): " + watch.ElapsedMilliseconds);
Console.WriteLine("Plugin - IsEFLoaded: " + Helper.IsEFLoaded());
}
}
更新 2
@Yasser: System.Data.Entity は、データベースにヒットした後にのみプラグインにロードされます。最初は EntityFramework.dll のみがプラグインに読み込まれますが、データベース後の他の EF アセンブリも読み込まれます。
圧縮されたソリューション。このサイトはファイルを 30 日間しか保持しません。より良いファイル共有サイトを提案してください。
また、メイン プロジェクトで EF を参照し、元のサンプルのタイミング パターンが再現可能かどうかを確認することで、私の調査結果を検証できるかどうかを知りたいです。
アップデート 3
明確にするために、私が分析に興味を持っているのは、EF の起動を含む最初の呼び出しのタイミングです。最初の呼び出しで、親 AppDomain の ~800 ミリ秒から子 AppDomain の ~2700 ミリ秒になることは非常に顕著です。後続の呼び出しでは、1 ミリ秒から 3 ミリ秒になることはほとんど目立ちません。最初の呼び出し (EF スタートアップを含む) が子 AppDomains 内で非常に高価なのはなぜですか?
FirstOrDefault()
ノイズを減らすための呼び出しだけに焦点を当てるようにサンプルを更新しました。親 AppDomain で実行し、3 つの子 AppDomain で実行するタイミング:
EF.vshost.exe|0|FirstOrDefault(): 768 EF.vshost.exe|1|FirstOrDefault(): 1 EF.vshost.exe|2|FirstOrDefault(): 1 AppDomain0|0|FirstOrDefault(): 2623 AppDomain0|1|FirstOrDefault(): 2 AppDomain0|2|FirstOrDefault(): 1 AppDomain1|0|FirstOrDefault(): 2669 AppDomain1|1|FirstOrDefault(): 2 AppDomain1|2|FirstOrDefault(): 1 AppDomain2|0|FirstOrDefault(): 2760 AppDomain2|1|FirstOrDefault(): 3 AppDomain2|2|FirstOrDefault(): 1
更新されたサンプル コード
static void Main(string[] args)
{
var mainPlugin = new SamplePlugin();
for (var i = 0; i < 3; i++)
mainPlugin.Do(i);
Console.WriteLine();
for (var i = 0; i < 3; i++)
{
var plugin = CreatePluginForAppDomain("AppDomain" + i);
for (var j = 0; j < 3; j++)
plugin.Do(j);
Console.WriteLine();
}
Console.ReadLine();
}
private static IPlugin CreatePluginForAppDomain(string appDomainName)
{
var dir = Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory + @"..\..\..\EF.Plugin\bin\Debug");
var evidence = new Evidence();
var setup = new AppDomainSetup { ApplicationBase = dir };
var domain = AppDomain.CreateDomain(appDomainName, evidence, setup);
var pluginDll = Path.Combine(dir, "EF.Plugin.dll");
return (IPlugin) domain.CreateInstanceFromAndUnwrap(pluginDll, "EF.Plugin.SamplePlugin");
}
public class SamplePlugin : MarshalByRefObject, IPlugin
{
public void Do(int i)
{
var context = new MyContext();
var watch = Stopwatch.StartNew();
var posts = context.Posts.FirstOrDefault();
watch.Stop();
Console.WriteLine(AppDomain.CurrentDomain.FriendlyName + "|" + i + "|FirstOrDefault(): " + watch.ElapsedMilliseconds);
}
}
圧縮されたソリューション。このサイトはファイルを 30 日間しか保持しません。より良いファイル共有サイトを提案してください。