为啥实体框架在不同的 AppDomain 中运行时会明显变慢?

Posted

技术标签:

【中文标题】为啥实体框架在不同的 AppDomain 中运行时会明显变慢?【英文标题】:Why is Entity Framework significantly slower when running in a different AppDomain?为什么实体框架在不同的 AppDomain 中运行时会明显变慢? 【发布时间】:2013-08-27 03:37:24 【问题描述】:

我们有一个 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 行。 Timings 故意只关注第一次调用。后续调用要快得多,但子 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 的成本进行进一步研究后,似乎有人建议后续 AppDomain 必须重新 JIT 系统 DLL,因此创建 AppDomain 存在固有的启动成本。这就是这里发生的事情吗?我原以为 JIT-ing 会在 AppDomain 创建时进行,但调用它时可能是 EF JIT-ing?

re-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();
    

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

更新示例代码

    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 天。随意推荐一个更好的文件共享网站。

【问题讨论】:

您是否尝试过在同一个应用程序域中多次执行插件来查看?没有几个新的应用程序域。 您的意思是在 Program.cs 内部多次调用plugin.FirstPost() 以查看后续时间安排如何?如果是这样,是的,后续调用会快得多,但仍然相对提高。 我发现似乎有助于提高跨 AppDomain 调用性能的一件事是在 Main() 方法上添加:[LoaderOptimization(LoaderOptimization.MultiDomain)] @Iridium 感谢您的建议。我的理解是拥有LoaderOptimization 有利于跨AppDomains 序列化对象,这不是这里发生的事情。顺便说一句,我确实找到了一篇文章,建议 plugins have to be added to the GAC 使用对我们不利的属性。 FWIW,我已经定期在 AppDomains 内运行简单而粗糙的 EF 查询大约 6 个月,而生产代码没有任何放缓。 【参考方案1】:

这似乎只是子 AppDomains 的成本。 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|数据表:0 EF.vshost.exe|1|数据表:0 EF.vshost.exe|2|数据表:0 AppDomain0|0|数据表:12 AppDomain0|1|数据表:0 AppDomain0|2|数据表:0 AppDomain1|0|数据表:11 AppDomain1|1|数据表:0 AppDomain1|2|数据表:0 AppDomain2|0|数据表:10 AppDomain2|1|数据表:0 AppDomain2|2|数据表: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);
    

【讨论】:

【参考方案2】:

您应该在启动应用程序时多次运行该测试

第一次之后,性能差异完全在于您的主应用程序域和插件应用程序域之间的对象序列化。

请注意,应用程序域之间的每次通信都需要序列化和反序列化,这成本太高了。

您可以在 [SQL Server / .NET CLR] 存储过程上开发应用程序时看到这个问题,这些存储过程运行在单独的应用程序域而不是 sql server 引擎中。

【讨论】:

“测试”是指新建一个 DbContext 并调用 FirstOrDefault()?为什么你建议在启动应用程序时多次这样做?我故意只计时第一次通话,因为这是明显的区别。此外,如前所述,是的,后续调用要快得多,但仍然相对提高。 这里的序列化是什么?除了 Console.WriteLine() 之外,不应该有任何跨 AppDomain 通信? @Stajs 在工厂启用的 orm(s) 中,例如 NHibernate,您可以在新的 AppDomain 中完全加载 SessionFactory。但是在实体框架中,您只能拥有一个您无法访问的上下文提供者。实体框架在其上下文和加载到主应用程序域的其他对象之间有很多通信 @Stajs 我没有正确理解你的测试场景。由于分离应用程序域中的 DbContext 与主应用程序域中实体框架的静态对象的通信,您(总是)将有更长的时间导致分离的应用程序域。 感谢您的意见。我想我可能会通过尝试在主 AppDomain 中显示一些 EF 时间来混淆问题。我不相信 EF 在 AppDomains 之间进行任何通信。我已经用更多信息更新了这个问题,希望能更好地说明这一点。【参考方案3】:

也许我错了,但代码如下:

public class SamplePlugin : MarshalByRefObject, IPlugin

    public void Do()
    
        using (AppDb db = new AppDb())
        
            db.Posts.FirstOrDefault();
        
    

还有这些代码:

[LoaderOptimization(LoaderOptimization.MultiDomain)]
    static void Main(String[] args)
    
        AppDomain.CurrentDomain.AssemblyLoad += CurrentDomain_AssemblyLoad;

        var dir = Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory + @"..\..\..\EF\bin\Debug");

        var evidence = new Evidence();

        var setup = new AppDomainSetup  ApplicationBase = dir ;

        var domain = AppDomain.CreateDomain("Plugin", evidence, setup);

        domain.AssemblyLoad += domain_AssemblyLoad;

        var pluginDll = Path.Combine(dir, "EF.Plugin.dll");

        var anotherDomainPlugin = (IPlugin)domain.CreateInstanceFromAndUnwrap(pluginDll, "EF.Plugin.SamplePlugin");

        var mainDomainPlugin = new SamplePlugin();

        mainDomainPlugin.Do();    // To prevent side effects of entity framework startup from our test

        anotherDomainPlugin.Do(); // To prevent side effects of entity framework startup from our test

        Stopwatch watch = Stopwatch.StartNew();

        mainDomainPlugin.Do();

        watch.Stop();

        Console.WriteLine("Main Application Domain -------------------------- " + watch.ElapsedMilliseconds.ToString());

        watch.Restart();

        anotherDomainPlugin.Do();

        watch.Stop();

        Console.WriteLine("Another Application Domain -------------------------- " + watch.ElapsedMilliseconds.ToString());

        Console.ReadLine();
    

    static void CurrentDomain_AssemblyLoad(Object sender, AssemblyLoadEventArgs args)
    
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine("Main Domain : " + args.LoadedAssembly.FullName);
    

    static void domain_AssemblyLoad(Object sender, AssemblyLoadEventArgs args)
    
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine("Another Domain : " + args.LoadedAssembly.FullName);
    

在这种情况下,主应用程序域和另一个应用程序域之间没有真正的性能差异,您得到不同的结果是因为您的测试错误(-:(至少我认为它们是错误的),我也测试了主应用程序domain 通过直接调用 DbContext 和 first 或 default ,我的时间是相同的,差异在 1 - 2 毫秒之间,我不明白为什么我的结果与你的结果不同

【讨论】:

我很抱歉,如果我以一种看起来我不关心 EF 启动时间的方式提出了这个问题。这正是我试图用最初的问题来说明的:3x-5x 的性能损失必须包括 EF 启动。我已经更新了这个问题,希望能进一步澄清。感谢您在这方面的帮助。 [LoaderOptimization(LoaderOptimization.MultiDomain)] 不是必需的,在这种情况下没有任何区别。这只是一些调试/测试的剩余部分。 @Stajs 是的,您之前已经说过 [LoaderOptimization],总体而言,这是一个很好的属性,您可以在创建 appDomain 时将其传递给 appDomain 的构造函数,而不是使用它在某个地方的某个属性上。你的新更新太棒了,我要测试一下。

以上是关于为啥实体框架在不同的 AppDomain 中运行时会明显变慢?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的代码在线程 6:NSOperationQueue 中运行时会崩溃?

在 ASP.net 中运行时调用存储过程

当程序在 IntelliJ 中运行时,为啥我会收到 SSLHandshakeException 作为 JAR?

为啥在火花中运行时配置单元查询不起作用

当命令在 Ubuntu 终端中运行时,为啥 Dart 的“Process.start”不能执行 Ubuntu 命令?

为啥我的图像出现在 Android Studio 设计视图中,但在手机中运行时却没有?