我们有一个 Windows 服务,可以将一堆插件(程序集)加载到它们自己的 AppDomain 中。每个插件都与 SOA 意义上的“服务边界”对齐,因此负责访问自己的数据库。我们注意到,在单独的 AppDomain 中,EF 的速度要慢 3 到 5 倍。
我知道 EF 第一次创建 DbContext 并访问数据库时,它必须做一些设置工作,这些工作必须在每个 AppDomain 中重复(即不跨 AppDomain 缓存)。考虑到 EF 代码完全独立于插件(因此独立于 AppDomain),我预计时间与父 AppDomain 的时间相当。为什么它们不同?
已尝试同时针对 .NET 4/EF 4.4 和 .NET 4.5/EF 5。
示例代码
EF.csproj
程序.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 的成本进行了进一步研究后,似乎有人建议后续 AppDomain 必须重新 JIT 系统 DLL,因此创建 AppDomain 存在固有的启动成本。这就是这里发生的事情吗?我原以为 JIT-ing 会在 AppDomain 创建时进行,但也许它在被调用时是 EF JIT-ing?
重新 JIT 的参考:http: //msdn.microsoft.com/en-us/magazine/cc163655.aspx#S8
Timings 听起来很相似,但不确定是否相关: First WCF connection made in new AppDomain is very slow
更新 1
根据@Yasser 的建议,即跨 AppDomain 进行 EF 通信,我试图进一步隔离这一点。我不相信会是这样。
我已经从 EF.csproj 中完全删除了任何 EF 引用。我现在有足够的代表来发布图片,所以这是解决方案结构:
如您所见,只有插件引用了实体框架。我还验证了只有插件有一个带有 EntityFramework.dll 的 bin 文件夹。
我添加了一个帮助器来验证 EF 程序集是否已加载到 AppDomain 中。我还验证了(未显示)在调用数据库之后,还会加载其他 EF 程序集(例如动态代理)。
因此,检查 EF 是否已在各个点加载:
- 在调用插件之前在 Main
- 在点击数据库之前在插件中
- 在点击数据库后的插件中
- 调用插件后在 Main
...产生:
主要 - IsEFLoaded:假 插件 - IsEFLoaded:真 插件 - 新的 MyContext():367 插件 - FirstOrDefault(): 2693 插件 - IsEFLoaded:真 主要 - IsEFLoaded:假
所以看起来 AppDomains 是完全隔离的(如预期的那样),并且插件内部的时间是相同的。
更新的示例代码
程序.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();
}
}
助手.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 中的 ~800ms 到子 AppDomain 中的 ~2700ms 非常明显。在随后的调用中,从 ~1ms 到 ~3ms 几乎不明显。为什么子 AppDomains 内的第一次调用(包括 EF 启动)要贵得多?
我已经更新了示例,只关注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 天。随意建议一个更好的文件共享网站。