提问者:小点点

为什么当通过TCP发送消息的速率增加时,请求-响应消息对的延迟会减少?


我设置了一个客户端和一个服务器通过TCP连接进行通信,我经历了奇怪的延迟行为,我无法理解。

客户端向服务器发送请求消息,服务器向客户端发送响应消息。 我将延迟定义为从发送请求消息到接收响应消息的时间。 我可以以不同的速率发送请求消息(限制请求的频率),但是我总是在任何时候最多有一个未完成的请求消息。 即没有并发/重叠的请求-响应消息对。

我用三种方法实现了请求和响应消息的发送:第一种是直接在TCP套接字上使用自己的序列化方法等,第二种是使用HTTP2使用gRPC在RPC上进行通信,第三种是使用Apache Thrift(一个类似于gRPC的RPC框架)。 gRPC依次以4种不同的客户机/服务器类型实现,对于Thrift,我有3种不同的客户机/服务器类型。

在所有解决方案中,当增加请求消息的发送速率时,我都体验到延迟的减少(在gRPC和Thrift中,请求-响应对通过RPC方法进行通信)。 当完全不限制请求速率,而是在收到响应后立即发送新请求时,可以观察到最佳延迟。 延迟是使用std::chrono::steady_clock原语测量的。 我不知道是什么引起的。 我确保在开始真正的测试之前通过发送10K请求消息来预热TCP连接(通过TCP慢启动阶段)。

如何实现节流和测量延迟(在客户端ofc上):

double rate;
std::cout << "Enter rate (requests/second):" << std::endl;
std::cin >> rate;
auto interval = std::chrono::microseconds(1000000)/rate;

//warmup-phase is here, but not included in this code.

auto total_lat = std::chrono::microseconds(0);
auto iter_time = start_time;
int i = 0;
for(i = 0; i < 10000; i++){ // send 10k requests.
  iter_time = std::chrono::steady_clock::now();
  RequestType request("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
  ResponseType response;
  auto start = std::chrono::steady_clock::now();
  sendRequest(request); //these looks different depending on gRPC/Thrift/"TCP"
  receiveResponse(&response);
  auto end = std::chrono::steady_clock::now();
  auto dur = std::chrono::duration_cast<std::chrono::microseconds>(end-start);
  total_lat+=dur;
  std::this_thread::sleep_until(iter_time+interval); //throttle the sending..
}
// mean latency: total_lat / i

我使用docker-compose在单独的docker容器中运行客户机/服务器,我还在kubernetes集群中运行它们。 在这两种情况下,我经历了相同的行为。 我在想,也许我的节流/时间测量代码正在做一些我不知道/不理解的事情。

在所有情况下,TCP套接字都设置为TCP_NODELAY。 服务器是单线程的,多线程的,非阻塞的,阻塞的,各种不同的变化,客户机是同步的,异步的等等,所以很多变化,但在它们之间的行为都是一样的。

有什么能引起这种行为的想法吗?


共1个答案

匿名用户

现在,我认为延迟问题不在于网络堆栈,而在于生成和接收消息的速率。

您的测试代码似乎没有任何实时保证,这也需要在容器中设置。 这意味着您的“for循环”并不是每次都以相同的速度运行。 OS调度程序可以停止它以运行其他进程(这就是进程共享CPU的方式)。 这种行为在容器化机制中可能变得更加复杂。

虽然TCP中存在可能导致延迟变化的机制(正如@DNT所提到的),但我认为您不会看到它们。 尤其是如果服务器和客户端是本地的。 这就是为什么在查看TCP堆栈之前,我会先排除消息生成和接收的速率。