27

我们有一个 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 引用。我现在有足够的代表来发布图片,所以这是解决方案结构:

EF.sln

如您所见,只有插件引用了实体框架。我还验证了只有插件有一个带有 EntityFramework.dll 的 bin 文件夹。

我添加了一个帮助器来验证 EF 程序集是否已加载到 AppDomain 中。我还验证了(未显示)在调用数据库之后,还会加载其他 EF 程序集(例如动态代理)。

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

  1. 在调用插件之前在 Main
  2. 在点击数据库之前在插件中
  3. 在点击数据库后的插件中
  4. 调用插件后在 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 天。随意建议一个更好的文件共享网站。

4

3 回答 3

4

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

实体框架确实具有相对较高的启动成本,因此效果被放大了,但相比之下调用 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);
    }
}
于 2013-09-02T23:45:39.313 回答
3

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

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

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

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

于 2013-08-27T04:30:37.610 回答
0

也许我错了,但使用以下代码:

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);
    }

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

于 2013-08-31T10:15:31.343 回答