0

我目前正在开发一个需要高吞吐量的 Service Fabric 微服务。

我想知道为什么我无法在我的工作站上使用环回实现每秒超过 500 条 1KB 消息。

我删除了所有业务逻辑并附加了一个性能分析器,只是为了测量端到端的性能。

似乎大约 96% 的时间用于解决客户端问题,而只有大约 2% 的时间用于处理实际的 Http 请求。

我在一个紧密的循环中调用“发送”进行测试:

private HttpCommunicationClientFactory factory = new HttpCommunicationClientFactory();

public async Task Send()
{
    var client = new ServicePartitionClient<HttpCommunicationClient>(
         factory,
         new Uri("fabric:/MyApp/MyService"));

    await client.InvokeWithRetryAsync(c => c.HttpClient.GetAsync(c.Url + "/test"));
}

探查器输出

对此有什么想法吗?根据文档,我调用服务的方式似乎是 Service Fabric 最佳实践。

更新:缓存 ServicePartioningClient 确实提高了性能,但是使用分区服务,我无法缓存客户端,因为我不知道给定 PartitionKey 的分区。

更新 2:很抱歉,我在最初的问题中没有包含完整的详细信息。在最初实现基于套接字的通信时,我们注意到 InvokeWithRetry 的巨大开销。

如果您使用 http 请求,您不会注意到太多。一个 http 请求已经花费了大约 1 毫秒,因此为 InvokeWithRetry 添加 0.5 毫秒并不是那么明显。

但是如果你使用原始套接字,在我们的例子中需要 0.005 毫秒,为 InvokeWithRetry 增加 0.5 毫秒的开销是巨大的!

这是一个 http 示例,使用 InvokeAndRetry 需要 3 倍的时间:

public async Task RunTest()
{
    var factory = new HttpCommunicationClientFactory();
    var uri = new Uri("fabric:/MyApp/MyService");
    var count = 10000;

    // Example 1: ~6000ms
    for (var i = 0; i < count; i++)
    {
        var pClient1 = new ServicePartitionClient<HttpCommunicationClient>(factory, uri, new ServicePartitionKey(1));
        await pClient1.InvokeWithRetryAsync(c => c.HttpClient.GetAsync(c.Url));
    }

    // Example 2: ~1800ms
    var pClient2 = new ServicePartitionClient<HttpCommunicationClient>(factory, uri, new ServicePartitionKey(1));
    HttpCommunicationClient resolvedClient = null;
    await pClient2.InvokeWithRetryAsync(
        c =>
        {
            resolvedClient = c;
            return Task.FromResult(true);
        });

    for (var i = 0; i < count; i++)
    {
        await resolvedClient.HttpClient.GetAsync(resolvedClient.Url);
    }
}

我知道 InvokeWithRetry 添加了一些我不想从客户那里错过的好东西。但它是否需要在每次调用时解析分区?

4

1 回答 1

2

我认为实际上对此进行基准测试并看看实际有什么不同会很好。我使用一个有状态的服务创建了一个基本设置,该服务打开一个 HttpListener 和一个以三种不同方式调用该服务的客户端:

  • 为每个呼叫创建一个新客户端并按顺序执行所有呼叫

    for (var i = 0; i < count; i++)
    {
        var client = new ServicePartitionClient<HttpCommunicationClient>(_factory, _httpServiceUri, new ServicePartitionKey(1));
        var httpResponseMessage = await client.InvokeWithRetryAsync(c => c.HttpClient.GetAsync(c.Url + $"?index={id}"));
    }
    
  • 仅创建一次客户端,并按顺序在每次调用中重复使用它

    var client = new ServicePartitionClient<HttpCommunicationClient>(_factory, _httpServiceUri, new ServicePartitionKey(1));
    for (var i = 0; i < count; i++)
    {
        var httpResponseMessage = await client.InvokeWithRetryAsync(c => c.HttpClient.GetAsync(c.Url + $"?index={id}"));
    }
    
  • 为每个调用创建一个新客户端并并行运行所有调用

    var tasks = new List<Task>();
    for (var i = 0; i < count; i++)
    {
        tasks.Add(Task.Run(async () =>
        {
            var client = new ServicePartitionClient<HttpCommunicationClient>(_factory, _httpServiceUri, new ServicePartitionKey(1));
            var httpResponseMessage = await client.InvokeWithRetryAsync(c => c.HttpClient.GetAsync(c.Url + $"?index={id}"));
        }));
    }
    Task.WaitAll(tasks.ToArray());
    

然后,我对一些计数进行了测试,以获得一种平均值:

不同客户端和呼叫模型的每个呼叫持续时间

现在,这应该被视为它是什么,而不是在受控环境中进行完整和全面的测试,有许多因素会影响这种性能,例如集群大小,被调用的服务实际上做了什么(在这种情况下什么都没有)以及有效负载的大小和复杂性(在这种情况下是一个非常短的字符串)。

在这个测试中,我还想看看 Fabric Transport 的表现如何,并且性能类似于 HTTP 传输(老实说,我预计会稍微好一点,但在这种琐碎的场景中可能看不到)。

值得注意的是,对于 10,000 个调用的并行执行,性能显着下降。这可能是由于服务耗尽了工作内存。这样做的影响可能是某些客户端调用出现故障并在延迟后重试(待验证)。我测量持续时间的方式是所有呼叫完成之前的总时间。同时应该注意的是,测试并没有真正允许服务使用多个节点,因为所有调用都路由到同一个分区。

总而言之,重用客户端的性能影响是名义上的,对于琐碎的调用,HTTP 执行类似于 Fabric 传输。

于 2017-01-22T17:14:58.183 回答