【问题标题】:Why does >16KB payload size result in huge decrease in throughput (TCP + TLS)?为什么 >16KB 的有效负载大小会导致吞吐量(TCP + TLS)大幅下降?
【发布时间】:2021-05-27 09:18:34
【问题描述】:

测试由服务器和客户端来回发送固定大小的消息,重复很长时间。它们都是单线程的,并且使用 Boost ASIO 库用 C++ 编写。通信是本地的,并且通过带有 TLS 的 TCP。我正在使用 bmon 监控环回接口,并使用 System Monitor 检查 CPU 使用情况。

当我使用 16384 的消息大小时,我看到 RX/TX 速度约为 200 MiB/s,CPU 使用率约为 60%。当我将消息大小更改为 16385 时,RX/TX 下降到大约 300 KiB/s,CPU 使用率下降到一个很小的百分比 (

我想知道吞吐量大幅下降的原因是什么?我怀疑它与 TLS 有关,因为使用纯 TCP 时没有下降。然而,2000:3 的下降似乎相当剧烈,尤其是考虑到该程序似乎也不受 CPU 限制。

我目前的猜测是 16384 是硬编码/配置的限制(可能是 TLS 最大记录大小?),超过这个数字需要额外的消息传递,但为什么吞吐量不会下降 ~2:1 而不是 2000:3 ?谁能帮忙解释一下?

这是完整的示例代码:

服务器.cpp

#include <functional>
#include <iostream>
#include <memory>
#include <system_error>
#include "asio.hpp"
#include "asio/ssl.hpp"

#define MESSAGE_SIZE 16385

class Server
{
public:
  struct Certs
  {
    std::string certificate_chain_file;
    std::string private_key_file;
    std::string verify_file;
  };

  using Stream = asio::ssl::stream<asio::ip::tcp::socket>;

  Server(Certs certs, unsigned short port)
    : acceptor(io_context, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port)),
      ssl_context(asio::ssl::context::tlsv12),
      rw_buf(new uint8_t[MESSAGE_SIZE])
  {
    ssl_context.set_options(
      asio::ssl::context::default_workarounds |
      asio::ssl::context::no_sslv2 |
      asio::ssl::context::no_sslv3 |
      asio::ssl::context::no_tlsv1 |
      asio::ssl::context::no_tlsv1_1 |
      asio::ssl::context::single_dh_use);

    ssl_context.use_certificate_chain_file(certs.certificate_chain_file);
    ssl_context.use_private_key_file(certs.private_key_file, asio::ssl::context::pem);

    ssl_context.set_verify_mode(
      asio::ssl::context::verify_peer |
      asio::ssl::context::verify_fail_if_no_peer_cert);
    ssl_context.load_verify_file(certs.verify_file);

    acceptor.async_accept(std::bind(&Server::onAccept, this, std::placeholders::_1, std::placeholders::_2));
  }

  void run()
  {
    io_context.run();
  }

  void onAccept(const std::error_code& error, asio::ip::tcp::socket socket)
  {
    if (error)
    {
      std::cerr << "Accept error=" << error.message() << std::endl;
      return;
    }

    stream.reset(new Stream(std::move(socket), ssl_context));

    asyncHandshake();
  }

  void asyncHandshake()
  {
    stream->async_handshake(
      asio::ssl::stream_base::server,
      std::bind(&Server::onHandshake, this, std::placeholders::_1));
  }

  void onHandshake(const std::error_code& error)
  {
    if (error)
    {
      std::cerr << "Handshake error=" << error.message() << std::endl;
      return;
    }

    asyncReadMessage();
  }

  void asyncReadMessage()
  {
    asio::async_read(
      *stream,
      asio::buffer(rw_buf.get(), MESSAGE_SIZE),
      std::bind(&Server::onRead, this, std::placeholders::_1, std::placeholders::_2));
  }

  void asyncWriteMessage()
  {
    asio::async_write(
      *stream,
      asio::buffer(rw_buf.get(), MESSAGE_SIZE),
      std::bind(&Server::onWrite, this, std::placeholders::_1, std::placeholders::_2));
  }

  void onRead(const std::error_code& error, size_t bytes_transferred)
  {
    if (error)
    {
      std::cerr << "Read error=" << error.message() << std::endl;
      return;
    }

    asyncWriteMessage();
  }

  void onWrite(const std::error_code& error, size_t bytes_transferred)
  {
    if (error)
    {
      std::cerr << "Write error=" << error.message() << std::endl;
      return;
    }

    asyncReadMessage();
  }

protected:
  asio::io_context io_context;
  asio::ip::tcp::acceptor acceptor;
  asio::ssl::context ssl_context;

  std::unique_ptr<Stream> stream;

  std::unique_ptr<uint8_t[]> rw_buf;
};

int main(int argc, char* argv[])
{
  Server::Certs certs
  {
    .certificate_chain_file = "server.crt",
    .private_key_file = "server.key",
    .verify_file = "ca.crt"
  };
  unsigned short port = 9090;

  Server server(certs, port);

  server.run();
}

客户端.cpp

#include <functional>
#include <iostream>
#include <memory>
#include <system_error>
#include "asio.hpp"
#include "asio/ssl.hpp"

#define MESSAGE_SIZE 16385
#define MESSAGE_BURST_SIZE 50000

class Client
{
public:
  struct Certs
  {
    std::string certificate_chain_file;
    std::string private_key_file;
    std::string verify_file;
  };

  using Stream = asio::ssl::stream<asio::ip::tcp::socket>;

  Client(Certs certs)
    : ssl_context(asio::ssl::context::tlsv12),
      rw_buf(new uint8_t[MESSAGE_SIZE])
  {
    ssl_context.set_options(
      asio::ssl::context::default_workarounds |
      asio::ssl::context::no_sslv2 |
      asio::ssl::context::no_sslv3 |
      asio::ssl::context::no_tlsv1 |
      asio::ssl::context::no_tlsv1_1 |
      asio::ssl::context::single_dh_use);

    ssl_context.use_certificate_chain_file(certs.certificate_chain_file);
    ssl_context.use_private_key_file(certs.private_key_file, asio::ssl::context::pem);

    ssl_context.set_verify_mode(
      asio::ssl::context::verify_peer |
      asio::ssl::context::verify_fail_if_no_peer_cert);
    ssl_context.load_verify_file(certs.verify_file);
  }

  void run(std::string host, unsigned short port)
  {
    stream.reset(new Stream(std::move(asio::ip::tcp::socket(io_context)), ssl_context));

    asio::ip::tcp::endpoint endpoint(asio::ip::address::from_string(host), port);

    stream->lowest_layer().async_connect(endpoint, std::bind(&Client::onConnect, this, std::placeholders::_1));

    io_context.run();
  }

  void onConnect(const std::error_code& error)
  {
    if (error)
    {
      std::cerr << "Connect error=" << error.message() << std::endl;
      return;
    }

    asyncHandshake();
  }

  void asyncHandshake()
  {
    stream->async_handshake(
      asio::ssl::stream_base::client,
      std::bind(&Client::onHandshake, this, std::placeholders::_1));
  }

  void onHandshake(const std::error_code& error)
  {
    if (error)
    {
      std::cerr << "Handshake error=" << error.message() << std::endl;
      return;
    }

    asyncWriteMessage();
  }

  void asyncReadMessage()
  {
    asio::async_read(
      *stream,
      asio::buffer(rw_buf.get(), MESSAGE_SIZE),
      std::bind(&Client::onRead, this, std::placeholders::_1, std::placeholders::_2));
  }

  void asyncWriteMessage()
  {
    asio::async_write(
      *stream,
      asio::buffer(rw_buf.get(), MESSAGE_SIZE),
      std::bind(&Client::onWrite, this, std::placeholders::_1, std::placeholders::_2));
  }

  void onRead(const std::error_code& error, size_t bytes_transferred)
  {
    if (error)
    {
      std::cerr << "Read error=" << error.message() << std::endl;
      return;
    }

    if (++message_count >= MESSAGE_BURST_SIZE)
    {
      return;
    }

    asyncWriteMessage();
  }

  void onWrite(const std::error_code& error, size_t bytes_transferred)
  {
    if (error)
    {
      std::cerr << "Write error=" << error.message() << std::endl;
      return;
    }

    asyncReadMessage();
  }

protected:
  asio::io_context io_context;
  asio::ssl::context ssl_context;

  std::unique_ptr<Stream> stream;

  std::unique_ptr<uint8_t[]> rw_buf;

  int message_count = 0;
};

int main(int argc, char* argv[])
{
  Client::Certs certs
  {
    .certificate_chain_file = "client.crt",
    .private_key_file = "client.key",
    .verify_file = "ca.crt"
  };

  std::string host = "127.0.0.1";
  unsigned short port = 9090;

  Client client(certs);

  client.run(host, port);
}

【问题讨论】:

  • "可能是 TLS 最大记录大小?"那是正确的。确切的值取决于密码套件,但它们都在 16K 附近。

标签: c++ sockets ssl boost-asio tls1.2


【解决方案1】:

我记得读过一篇关于在广泛的网络基础设施上进行调试的故事¹。

它的 TL;DR 是:巨型帧。

网络轨迹的所有部分都没有同等支持巨型帧,当它失败时,需要一些时间才能检测到错误,之后必须以更小的尺寸重试传输。

在某些情况下,重试/回退是最坏的情况,会导致吞吐量大大降低。

因此,将相关网络设备上的最大帧大小 (MTU) 调整为某些“安全公分母”可能会有所帮助。此外,尝试更好地控制线路上的数据包大小可能会有所帮助(我认为禁用 Nagle 的算法通常会出现在这里。当然,在这样做之前,请确保您绝对完全了解您的流量模式)。


¹不是我的故事,在适当的时候找到一个链接

【讨论】:

  • 这不是 this 的故事,但我想我开始记得它来自他们。
  • 谢谢@sehe!我认为 MTU 大小不是问题(在 MTU 为 65KB 的环回上执行此操作),但您从 cloudfare 链接的第二篇文章非常相关。我会发布一个答案来总结
【解决方案2】:

感谢@sehe 链接到this 文章,该文章描述了我认为相同或相似的问题。

引用文章:

为了解决这个问题,当套接字处于内存压力下时,会运行复杂的逻辑以释放一些空间。其中一项操作是 tcp_collapse,它将相邻的 TCP 数据包合并成一个更大的 sk_buff。这种行为几乎是垃圾收集 (GC) — 众所周知,当垃圾收集开始时,延迟必须达到峰值。

虽然我不确定这是我遇到的问题,但我尝试使用 SO_SENDBUF/SO_RCVBUF 套接字选项,看看吞吐量是否会提高。 RX/TX 速度 @ 16385B 消息大小升至 150 MiB/s。这低于 200 MiB/s @ 16384B 消息大小 - 这可能是由于 TLS 记录大小。

【讨论】:

    【解决方案3】:

    您是否尝试过扩大 ssl 流使用的发送缓冲区和接收缓冲区?

    boost::asio::socket_base::send_buffer_size sbs(8192000);
    boost::asio::socket_base::receive_buffer_size rbs(8192000);
    sslStream->lowest_layer().set_option(sbs);
    sslStream->lowest_layer().set_option(rbs);
    

    如果您的服务器没有很多连接,这可能会帮助您解决问题。

    【讨论】:

      猜你喜欢
      • 2017-02-16
      • 1970-01-01
      • 2015-02-13
      • 1970-01-01
      • 1970-01-01
      • 2012-12-07
      • 2016-08-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多