首页 > 解决方案 > 为什么 >16KB 的有效负载大小会导致吞吐量(TCP + TLS)大幅下降?

问题描述

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

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

我想知道吞吐量大幅下降的原因是什么?我怀疑它与 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);
}

标签: c++socketssslboost-asiotls1.2

解决方案


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

它的 TL;DR 是:巨型帧。

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

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

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


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


推荐阅读