我们有一个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);
    }
}

采样时间

笔记:
  • 这是针对空的数据库表-0行进行查询。
  • 计时只是故意在初次通话。后续调用的速度要快得多,但子AppDomain的速度要比父AppDomain慢3至5倍。

  • 运行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:

    c# - 为什么在不同的AppDomain中运行时,Entity Framework会明显变慢?-LMLPHP

    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:

    1. In Main before calling the plugin
    2. In Plugin before hitting the database
    3. In Plugin after hitting the database
    4. 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/

    10-17 02:38