我们有一个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
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; }
}
邮政
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);
}
}
采样时间
笔记:
运行1
outside plugin - new MyContext() : 55 outside plugin - FirstOrDefault(): 783 inside plugin - new MyContext() : 352 inside plugin - FirstOrDefault(): 2675
Run 2
outside plugin - new MyContext() : 53 outside plugin - FirstOrDefault(): 798 inside plugin - new MyContext() : 355 inside plugin - FirstOrDefault(): 2687
Run 3
outside plugin - new MyContext() : 45 outside plugin - FirstOrDefault(): 778 inside plugin - new MyContext() : 355 inside plugin - FirstOrDefault(): 2683
AppDomain research
After some further research in to the cost of AppDomains, there seems to be a suggestion that subsequent AppDomains have to re-JIT system DLLs and so there is an inherent start-up cost in creating an AppDomain. Is that what is happening here? I would have expected that the JIT-ing would have been on AppDomain creation, but perhaps it is EF JIT-ing when it is called?
Reference for re-JIT:http://msdn.microsoft.com/en-us/magazine/cc163655.aspx#S8
Timings sounds similar, but not sure if related:First WCF connection made in new AppDomain is very slow
Update 1
Based on @Yasser's suggestion that there is EF communication across the AppDomains, I tried to isolate this further. I don't believe this to be the case.
I have completely removed any EF reference from EF.csproj. I now have enough rep to post images, so this is the solution structure:
As you can see, only the plugin has a reference to Entity Framework. I have also verified that only the plugin has a bin folder with an EntityFramework.dll.
I have added a helper to verify if the EF assembly has been loaded in the AppDomain. I have also verified (not shown) that after the call to the database, additional EF assemblies (e.g. dynamic proxy) are also loaded.
So, checking if EF has loaded at various points:
- In Main before calling the plugin
- In Plugin before hitting the database
- In Plugin after hitting the database
- In Main after calling the plugin
... produces:
Main - IsEFLoaded: False Plugin - IsEFLoaded: True Plugin - new MyContext() : 367 Plugin - FirstOrDefault(): 2693 Plugin - IsEFLoaded: True Main - IsEFLoaded: False
So it seems that the AppDomains are fully isolated (as expected) and the timings are the same inside the plugin.
Updated Sample code
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程序集:
Zipped solution。该网站仅保留文件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
Updated Sample Code
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);
}
}
Zipped solution。该网站仅保留文件30天。随时建议一个更好的文件共享站点。
最佳答案
这似乎只是子AppDomain的成本。 rather ancient post(可能不再相关)暗示,除了必须JIT编译每个子AppDomain之外,可能还有其他考虑因素,例如评估安全策略。
Entity Framework确实具有相对较高的启动成本,因此会放大效果,但是对于比较调用System.Data的其他部分(例如,纯正的SqlDataReader
)来说同样可怕:
EF.vshost.exe|0|SqlDataReader: 67 EF.vshost.exe|1|SqlDataReader: 0 EF.vshost.exe|2|SqlDataReader: 0 AppDomain0|0|SqlDataReader: 313 AppDomain0|1|SqlDataReader: 2 AppDomain0|2|SqlDataReader: 0 AppDomain1|0|SqlDataReader: 290 AppDomain1|1|SqlDataReader: 3 AppDomain1|2|SqlDataReader: 0 AppDomain2|0|SqlDataReader: 316 AppDomain2|1|SqlDataReader: 2 AppDomain2|2|SqlDataReader: 0
public class SamplePlugin : MarshalByRefObject, IPlugin
{
public void Do(int i)
{
var watch = Stopwatch.StartNew();
using (var connection = new SqlConnection("Data Source=.\\sqlexpress;Initial Catalog=EF.Plugin.MyContext;Integrated Security=true"))
{
var command = new SqlCommand("SELECT * from Posts;", connection);
connection.Open();
var reader = command.ExecuteReader();
reader.Close();
}
watch.Stop();
Console.WriteLine(AppDomain.CurrentDomain.FriendlyName + "|" + i + "|SqlDataReader: " + watch.ElapsedMilliseconds);
}
}
甚至更新不起眼的
DataTable
也会膨胀:EF.vshost.exe|0|DataTable: 0 EF.vshost.exe|1|DataTable: 0 EF.vshost.exe|2|DataTable: 0 AppDomain0|0|DataTable: 12 AppDomain0|1|DataTable: 0 AppDomain0|2|DataTable: 0 AppDomain1|0|DataTable: 11 AppDomain1|1|DataTable: 0 AppDomain1|2|DataTable: 0 AppDomain2|0|DataTable: 10 AppDomain2|1|DataTable: 0 AppDomain2|2|DataTable: 0
public class SamplePlugin : MarshalByRefObject, IPlugin
{
public void Do(int i)
{
var watch = Stopwatch.StartNew();
var table = new DataTable("");
watch.Stop();
Console.WriteLine(AppDomain.CurrentDomain.FriendlyName + "|" + i + "|DataTable: " + watch.ElapsedMilliseconds);
}
}
关于c# - 为什么在不同的AppDomain中运行时,Entity Framework会明显变慢?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/18456491/