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

Sta*_*ajs 27 .net c# entity-framework

我们有一个Windows服务,可以将一堆插件(程序集)加载到自己的AppDomain中.每个插件都与SOA意义上的"服务边界"对齐,因此负责访问自己的数据库.我们注意到,在单独的AppDomain中,EF的速度要慢3到5倍.

我知道EF第一次创建DbContext并点击数据库时,它必须做一些必须按AppDomain重复的设置工作(即不在AppDomains上缓存).考虑到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();
    }
}
Run Code Online (Sandbox Code Playgroud)

EF.Interfaces.csproj

IPlugin.cs

public interface IPlugin
{
    void FirstPost();
}
Run Code Online (Sandbox Code Playgroud)

EF.Plugin.csproj

MyContext.cs

public class MyContext : DbContext
{
    public IDbSet<Post> Posts { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

Post.cs

public class Post
{
    public int Id { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

样本时间

笔记:

  • 这是查询空数据库表 - 0行.
  • 蒂姆斯有意只看第一次电话.后续调用快得多,但子AppDomain与父AppDomain相比仍然相对慢3到5倍.

运行1


    outside plugin - new MyContext() : 55
    outside plugin - FirstOrDefault(): 783
     inside plugin - new MyContext() : 352
     inside plugin - FirstOrDefault(): 2675

跑2


    outside plugin - new MyContext() : 53
    outside plugin - FirstOrDefault(): 798
     inside plugin - new MyContext() : 355
     inside plugin - FirstOrDefault(): 2687

跑3


    outside plugin - new MyContext() : 45
    outside plugin - FirstOrDefault(): 778
     inside plugin - new MyContext() : 355
     inside plugin - FirstOrDefault(): 2683

AppDomain研究

在对AppDomains的成本进行进一步研究之后,似乎有人建议后续AppDomain必须重新JIT系统DLL,因此在创建AppDomain时存在固有的启动成本.那是怎么回事?我原本以为JIT-ing会在AppDomain上创建,但是在调用它时可能是EF JIT-ing?

重新JIT的参考:http: //msdn.microsoft.com/en-us/magazine/cc163655.aspx#S8

Timings听起来很相似,但不确定是否相关: 在新的AppDomain中建立的第一个WCF连接非常慢

更新1

基于@ Yasser建议在AppDomains上进行EF通信,我试图进一步隔离它.我不相信这是事实.

我已经完全删除了EF.csproj中的任何EF引用.我现在有足够的代表发布图像,所以这是解决方案结构:

EF.sln

如您所见,只有插件才能引用Entity Framework.我还验证了只有插件有一个带有EntityFramework.dll的bin文件夹.

我添加了一个帮助程序来验证是否已在AppDomain中加载EF程序集.我还验证了(未示出)在调用数据库之后,还加载了额外的EF程序集(例如动态代理).

因此,检查EF是否已在各个点加载:

  1. 在Main调用插件之前
  2. 在插入数据库之前的插件中
  3. 在点击数据库后的插件中
  4. 在调用插件后的Main中

...产生:

Main - IsEFLoaded: False
Plugin - IsEFLoaded: True
Plugin - new MyContext() : 367
Plugin - FirstOrDefault(): 2693
Plugin - IsEFLoaded: True
Main - 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

Helper.cs

(是的,我不会为此添加另一个项目...)

public static class Helper
{
    public static bool IsEFLoaded()
    {
        return AppDomain.CurrentDomain
            .GetAssemblies()
            .Any(a => a.FullName.StartsWith("EntityFramework"));
    }
}
Run Code Online (Sandbox Code Playgroud)

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());
    }
}
Run Code Online (Sandbox Code Playgroud)

更新2

@Yasser:只有命中数据库后才会将System.Data.Entity加载到插件中.最初只在插件中加载了EntityFramework.dll,但是也加载了后数据库的其他EF程序集:

加载的组件

拉链解决方案.该网站仅保留文件30天.随意建议一个更好的文件共享网站.

此外,我有兴趣知道您是否可以通过在主项目中引用EF来验证我的发现,并查看原始样本的时间模式是否可重现.

更新3

需要说明的是,这是我有兴趣分析的第一个呼叫时间,其中包括EF启动.在第一次调用时,从父AppDomain中的~800ms到子AppDomain中的~2700ms是非常明显的.在随后的呼叫中,从约1ms到约3ms几乎不可察觉.为什么第一次呼叫(包括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);
    }
}
Run Code Online (Sandbox Code Playgroud)

拉链解决方案.该网站仅保留文件30天.随意建议一个更好的文件共享网站.

Sta*_*ajs 4

这似乎只是子 AppDomain 的成本。一篇相当古老的帖子(可能不再相关)表明,除了必须 JIT 编译每个子 AppDomain 之外,还可能存在其他考虑因素,例如评估安全策略。

实体框架确实具有相对较高的启动成本,因此效果被放大,但相比之下,调用 System.Data 的其他部分(例如直接SqlDataReader)同样可怕:

EF.vshost.exe|0|SqlDataReader: 67
EF.vshost.exe|1|SqlDataReader: 0
EF.vshost.exe|2|SqlDataReader: 0

应用程序域0|0|SqlDataReader:313
应用程序域0|1|SqlDataReader:2
应用程序域0|2|SqlDataReader: 0

应用程序域1|0|SqlDataReader:290
应用程序域1|1|SqlDataReader:3
应用程序域1|2|SqlDataReader:0

应用程序域2|0|SqlDataReader:316
应用程序域2|1|SqlDataReader:2
应用程序域2|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);
    }
}
Run Code Online (Sandbox Code Playgroud)

即使是新晋的谦虚者也DataTable被夸大了:

EF.vshost.exe|0|数据表:0
EF.vshost.exe|1|数据表:0
EF.vshost.exe|2|数据表:0

应用程序域0|0|数据表:12
应用程序域0|1|数据表:0
应用程序域0|2|数据表:0

应用程序域1|0|数据表:11
应用程序域1|1|数据表:0
应用程序域1|2|数据表:0

应用程序域2|0|数据表:10
应用程序域2|1|数据表:0
应用程序域2|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);
    }
}
Run Code Online (Sandbox Code Playgroud)