c++ - 为什么 >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);
}
解决方案
我记得读过一篇关于在广泛的网络基础设施上进行调试的故事¹。
它的 TL;DR 是:巨型帧。
网络轨迹的所有部分都没有同等支持巨型帧,当它失败时,需要一些时间才能检测到错误,之后必须以较小的大小重试传输。
在某些情况下,重试/回退是最坏的情况,导致吞吐量大大降低。
因此,将相关网络设备上的最大帧大小 (MTU) 调整为某些“安全公分母”可能会有所帮助。此外,尝试对线路上的数据包大小进行更精细的控制可能会有所帮助(我认为禁用 Nagle 算法通常会出现在这里。当然,在这样做之前,请确保您绝对完全了解您的流量模式)。
¹不是我的故事,在适当的时候找到一个链接
推荐阅读
- postgresql - postgresql 中的 TIMESTAMPDIFF 别名
- c# - 使用 Jsonsoft 反序列化期间单行字符串失败
- r - 管道 df 到 geom_hline 参数
- android - 使用 google oauth 登录返回“代码:1”
- azure-devops - Azure DevOps 本地缓存容器作业
- javascript - youtube v3 api 搜索在 Chrome 中失败,但在 Firefox 中有效
- css - CSS - 打印 html 文档
- mysql - 内部连接和 AND 值 IN(查询)上的 MySQL 语法错误
- html - 在 Boostrap 4 中将多个导航选项卡与多个导航栏同步
- excel - SumIF 值 <> 下一列中的“#NV”