18

我有一个使用 WCF 调用 http 服务来获取设置的库。通常第一次调用大约需要 100 毫秒,随后的调用只需要几毫秒。但是我发现当我创建一个新的 AppDomain 时,来自该 AppDomain 的第一个 WCF 调用需要 2.5 秒以上。

有没有人解释或修复为什么在新的 AppDomain 中首次创建 WCF 通道需要这么长时间?

这些是基准测试结果(当在 64 位版本中未附加调试器运行时),请注意在第二组数字中,第一个连接如何花费 25 倍的时间

Running in initial AppDomain
First Connection: 92.5018 ms
Second Connection: 2.6393 ms

Running in new AppDomain
First Connection: 2457.8653 ms
Second Connection: 4.2627 ms

这不是一个完整的示例,但显示了我如何生成这些数字的大部分内容:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Running in initial AppDomain");
        new DomainRunner().Run();

        Console.WriteLine();
        Console.WriteLine("Running in new thread and AppDomain");
        DomainRunner.RunInNewAppDomain("test");

        Console.ReadLine();
    }
}

class DomainRunner : MarshalByRefObject
{
    public static void RunInNewAppDomain(string runnerName)
    {
        var newAppDomain = AppDomain.CreateDomain(runnerName);
        var runnerProxy = (DomainRunner)newAppDomain.CreateInstanceAndUnwrap(typeof(DomainRunner).Assembly.FullName, typeof(DomainRunner).FullName);

        runnerProxy.Run();
    }

    public void Run()
    {
        AppServSettings.InitSettingLevel(SettingLevel.Production);
        var test = string.Empty;

        var sw = Stopwatch.StartNew();
        test += AppServSettings.ServiceBaseUrlBatch;
        Console.WriteLine("First Connection: {0}", sw.Elapsed.TotalMilliseconds);

        sw = Stopwatch.StartNew();
        test += AppServSettings.ServiceBaseUrlBatch;
        Console.WriteLine("Second Connection: {0}", sw.Elapsed.TotalMilliseconds);
    }
}

对 AppServSettings.ServiceBaseUrlBatch 的调用正在创建一个服务通道并调用一个方法。我使用了wireshark 来观看通话,只需要几毫秒就可以得到服务的响应。它使用以下代码创建通道:

public static ISettingsChannel GetClient()
{
    EndpointAddress address = new EndpointAddress(SETTINGS_SERVICE_URL);

    BasicHttpBinding binding = new BasicHttpBinding
    {
        MaxReceivedMessageSize = 1024,
        OpenTimeout = TimeSpan.FromSeconds(2),
        SendTimeout = TimeSpan.FromSeconds(5),
        ReceiveTimeout = TimeSpan.FromSeconds(5),
        ReaderQuotas = { MaxStringContentLength = 1024},
        UseDefaultWebProxy = false,
    };

    cf = new ChannelFactory<ISettingsChannel>(binding, address);

    return cf.CreateChannel();
}

从分析应用程序可以看出,在第一种情况下,构建通道工厂、创建通道和调用方法所需的时间不到 100 毫秒

在新建的 AppDomain 中构建通道工厂耗时 763 毫秒,创建通道耗时 521 毫秒,调用接口上的方法耗时 1098 毫秒。

TestSettingsRepoInAppDomain.DomainRunner.Run() 2,660.00 TestSettingsRepoInAppDomain.AppServSettings.get_ServiceBaseUrlBatch() 2,543.47 Tps.Core.Settings.Retriever.GetSetting(string,!!0,!!0,!!0) 2,542.66 Tps.Core.Settings.Retriever.TryGetSetting (string,!!0&) 2,522.03 Tps.Core.Settings.ServiceModel.WcfHelper.GetClient() 1,371.21 Tps.Core.Settings.ServiceModel.IClientChannelExtensions.CallWithRetry(class System.ServiceModel.IClientChannel) 1,098.83

编辑

在将 perfmon 与 .NET CLR Loading 对象一起使用后,我可以看到,当它加载第二个 AppDomain 时,它会将比最初更多的类加载到内存中。第一条平线是我在第一个 appdomain 之后的暂停,它加载了 218 个类。第二个 AppDomain 总共加载了 1,944 个类。

我假设所有这些类的加载都占用了所有时间,所以现在的问题是,它加载了哪些类,为什么?

在此处输入图像描述

更新

答案原来是因为只有一个 AppDomain 能够利用本机图像系统 dll。因此,第二个 appdomain 的缓慢之处在于它必须重新启动 wcf 使用的所有 System.* dll。第一个 appdomain 可以使用这些 dll 的预先生成的本机版本,因此它没有相同的启动成本。

在调查 Petar 建议的LoaderOptimizationAttribute之后,这似乎确实解决了问题,使用MultiDomain 或 MultiDomainHost会导致第二个 AppDomain 花费与第一次通过 wcf 访问内容相同的时间

在这里你可以看到默认选项,注意在第二个 AppDomain 中没有一个程序集说 Native,这意味着它们都必须重新设置,这是一直以来所花费的

在此处输入图像描述

这是在将 LoaderOptimization(LoaderOptimization.MultiDomain) 添加到 Main 之后。您可以看到所有内容都加载到共享的 AppDomain 中

在此处输入图像描述

这是在用户 LoaderOptimization(LoaderOptimization.MultiDomainHost) 到 main 之后。您可以看到所有系统 dll 都是共享的,但是我自己的 dll 以及任何不在 GAC 中的 dll 都被单独加载到每个 AppDomain 中

在此处输入图像描述

因此,对于使用 MultiDomainHost 提示此问题的服务是答案,因为它具有快速启动时间,并且我可以卸载 AppDomains 以删除该服务使用的动态构建的程序集

4

3 回答 3

9

您可以使用LoaderOptimization属性装饰您的 Main以告诉 CLR 加载器如何加载类。

[LoaderOptimization(LoaderOptimization.MultiDomain)]
MultiDomain - Indicates that the application will probably have many domains that use the same code, and the loader must share maximal internal resources across application domains.
于 2012-04-19T07:18:16.980 回答
1

你有在 IE 中定义的 HTTP 代理吗?(也许是一个自动配置脚本)。这可能是一个原因。

否则我猜这是加载所有 dll 所需的时间。尝试将代理创建从对服务的实际调用中分离出来,看看是什么花费了时间。

于 2012-04-14T00:08:14.967 回答
1

我发现以下文章讨论了如何只有第一个 AppDomain 可以使用本机图像 dll,因此子 appdomain 将始终被迫 JIT 初始 AppDomain 不需要的许多东西。这可能会导致我看到的性能影响,但是有可能以某种方式不得到这种性能损失吗?

如果程序集有原生镜像,则只有第一个 AppDomain 可以使用原生镜像。所有其他 AppDomain 都必须对代码进行 JIT 编译,这可能会导致大量 CPU 成本。

于 2012-04-18T21:11:51.270 回答