Skip to content

Commit eca8065

Browse files
authored
tcp: setting nodelay on all connections (envoyproxy#14574)
This should have minimal effect, new server side connections had no-delay, codecs set no-delay, and upstream pools set no-delay. Traffic not using the tcp connection pool may be affected as well as raw use of the TCP client. Risk Level: Medium (data plane) Testing: new unit tests Docs Changes: n/a Release Notes: inline Runtime guard: envoy.reloadable_features.always_nodelay Signed-off-by: Alyssa Wilk <[email protected]>
1 parent a62c200 commit eca8065

File tree

17 files changed

+143
-22
lines changed

17 files changed

+143
-22
lines changed

docs/root/version_history/current.rst

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Minor Behavior Changes
99
----------------------
1010
*Changes that may cause incompatibilities for some users, but should not for most*
1111

12+
* tcp: setting NODELAY in the base connection class. This should have no effect for TCP or HTTP proxying, but may improve throughput in other areas. This behavior can be temporarily reverted by setting `envoy.reloadable_features.always_nodelay` to false.
1213
* upstream: host weight changes now cause a full load balancer rebuild as opposed to happening
1314
atomically inline. This change has been made to support load balancer pre-computation of data
1415
structures based on host weight, but may have performance implications if host weight changes

source/common/http/codec_client.cc

+3-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ CodecClient::CodecClient(Type type, Network::ClientConnectionPtr&& connection,
4949

5050
// We just universally set no delay on connections. Theoretically we might at some point want
5151
// to make this configurable.
52-
connection_->noDelay(true);
52+
if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.always_nodelay")) {
53+
connection_->noDelay(true);
54+
}
5355
}
5456

5557
CodecClient::~CodecClient() = default;

source/common/network/connection_impl.cc

+9-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "common/network/listen_socket_impl.h"
2121
#include "common/network/raw_buffer_socket.h"
2222
#include "common/network/utility.h"
23+
#include "common/runtime/runtime_features.h"
2324

2425
namespace Envoy {
2526
namespace Network {
@@ -775,7 +776,11 @@ ServerConnectionImpl::ServerConnectionImpl(Event::Dispatcher& dispatcher,
775776
TransportSocketPtr&& transport_socket,
776777
StreamInfo::StreamInfo& stream_info, bool connected)
777778
: ConnectionImpl(dispatcher, std::move(socket), std::move(transport_socket), stream_info,
778-
connected) {}
779+
connected) {
780+
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.always_nodelay")) {
781+
noDelay(true);
782+
}
783+
}
779784

780785
void ServerConnectionImpl::setTransportSocketConnectTimeout(std::chrono::milliseconds timeout) {
781786
if (!transport_connect_pending_) {
@@ -854,6 +859,9 @@ void ClientConnectionImpl::connect() {
854859
socket_->addressProvider().remoteAddress()->asString());
855860
const Api::SysCallIntResult result = socket_->connect(socket_->addressProvider().remoteAddress());
856861
if (result.rc_ == 0) {
862+
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.always_nodelay")) {
863+
noDelay(true);
864+
}
857865
// write will become ready.
858866
ASSERT(connecting_);
859867
} else {

source/common/runtime/runtime_features.cc

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ constexpr const char* runtime_features[] = {
6161
"envoy.reloadable_features.allow_500_after_100",
6262
"envoy.reloadable_features.allow_preconnect",
6363
"envoy.reloadable_features.allow_response_for_timeout",
64+
"envoy.reloadable_features.always_nodelay",
6465
"envoy.reloadable_features.consume_all_retry_headers",
6566
"envoy.reloadable_features.check_ocsp_policy",
6667
"envoy.reloadable_features.disable_tls_inspector_injection",

source/common/tcp/conn_pool.cc

+5-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "envoy/event/timer.h"
77
#include "envoy/upstream/upstream.h"
88

9+
#include "common/runtime/runtime_features.h"
910
#include "common/stats/timespan_impl.h"
1011
#include "common/upstream/upstream_impl.h"
1112

@@ -31,7 +32,10 @@ ActiveTcpClient::ActiveTcpClient(Envoy::ConnectionPool::ConnPoolImplBase& parent
3132
host->cluster().stats().upstream_cx_tx_bytes_total_,
3233
host->cluster().stats().upstream_cx_tx_bytes_buffered_,
3334
&host->cluster().stats().bind_errors_, nullptr});
34-
connection_->noDelay(true);
35+
36+
if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.always_nodelay")) {
37+
connection_->noDelay(true);
38+
}
3539
connection_->connect();
3640
}
3741

source/common/tcp/original_conn_pool.cc

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "envoy/event/timer.h"
77
#include "envoy/upstream/upstream.h"
88

9+
#include "common/runtime/runtime_features.h"
910
#include "common/stats/timespan_impl.h"
1011
#include "common/upstream/upstream_impl.h"
1112

@@ -400,7 +401,9 @@ OriginalConnPoolImpl::ActiveConn::ActiveConn(OriginalConnPoolImpl& parent)
400401

401402
// We just universally set no delay on connections. Theoretically we might at some point want
402403
// to make this configurable.
403-
conn_->noDelay(true);
404+
if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.always_nodelay")) {
405+
conn_->noDelay(true);
406+
}
404407
}
405408

406409
OriginalConnPoolImpl::ActiveConn::~ActiveConn() {

source/common/upstream/health_checker_impl.cc

+3-1
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,9 @@ void TcpHealthCheckerImpl::TcpActiveHealthCheckSession::onInterval() {
568568

569569
expect_close_ = false;
570570
client_->connect();
571-
client_->noDelay(true);
571+
if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.always_nodelay")) {
572+
client_->noDelay(true);
573+
}
572574
}
573575

574576
if (!parent_.send_bytes_.empty()) {

source/extensions/filters/network/common/redis/client_impl.cc

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
#include "envoy/extensions/filters/network/redis_proxy/v3/redis_proxy.pb.h"
44

5+
#include "common/runtime/runtime_features.h"
6+
57
namespace Envoy {
68
namespace Extensions {
79
namespace NetworkFilters {
@@ -64,7 +66,9 @@ ClientPtr ClientImpl::create(Upstream::HostConstSharedPtr host, Event::Dispatche
6466
client->connection_->addConnectionCallbacks(*client);
6567
client->connection_->addReadFilter(Network::ReadFilterSharedPtr{new UpstreamReadFilter(*client)});
6668
client->connection_->connect();
67-
client->connection_->noDelay(true);
69+
if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.always_nodelay")) {
70+
client->connection_->noDelay(true);
71+
}
6872
return client;
6973
}
7074

source/server/connection_handler_impl.cc

+4-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "common/event/deferred_task.h"
1313
#include "common/network/connection_impl.h"
1414
#include "common/network/utility.h"
15+
#include "common/runtime/runtime_features.h"
1516
#include "common/stats/timespan_impl.h"
1617

1718
#include "extensions/transport_sockets/well_known_names.h"
@@ -589,7 +590,9 @@ ConnectionHandlerImpl::ActiveTcpConnection::ActiveTcpConnection(
589590
active_connections_.listener_.stats_.downstream_cx_length_ms_, time_source)) {
590591
// We just universally set no delay on connections. Theoretically we might at some point want
591592
// to make this configurable.
592-
connection_->noDelay(true);
593+
if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.always_nodelay")) {
594+
connection_->noDelay(true);
595+
}
593596
auto& listener = active_connections_.listener_;
594597
listener.stats_.downstream_cx_total_.inc();
595598
listener.stats_.downstream_cx_active_.inc();

test/common/network/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ envoy_cc_test(
8686
"//test/mocks/api:api_mocks",
8787
"//test/mocks/buffer:buffer_mocks",
8888
"//test/mocks/event:event_mocks",
89+
"//test/mocks/network:io_handle_mocks",
8990
"//test/mocks/network:network_mocks",
9091
"//test/mocks/stats:stats_mocks",
9192
"//test/test_common:environment_lib",

test/common/network/connection_impl_test.cc

+32-12
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ class TestClientConnectionImpl : public Network::ClientConnectionImpl {
122122
public:
123123
using ClientConnectionImpl::ClientConnectionImpl;
124124
Buffer::Instance& readBuffer() { return *read_buffer_; }
125+
ConnectionSocketPtr& socket() { return socket_; }
125126
};
126127

127128
class ConnectionImplTest : public testing::TestWithParam<Address::IpVersion> {
@@ -399,11 +400,13 @@ TEST_P(ConnectionImplTest, SetServerTransportSocketTimeout) {
399400
ConnectionMocks mocks = createConnectionMocks(false);
400401
MockTransportSocket* transport_socket = mocks.transport_socket_.get();
401402
IoHandlePtr io_handle = std::make_unique<IoSocketHandleImpl>(0);
403+
// Avoid setting noDelay on the fake fd of 0.
404+
auto local_addr = std::make_shared<Network::Address::PipeInstance>("/pipe/path");
402405

403406
auto* mock_timer = new NiceMock<Event::MockTimer>(mocks.dispatcher_.get());
404407
auto server_connection = std::make_unique<Network::ServerConnectionImpl>(
405408
*mocks.dispatcher_,
406-
std::make_unique<ConnectionSocketImpl>(std::move(io_handle), nullptr, nullptr),
409+
std::make_unique<ConnectionSocketImpl>(std::move(io_handle), local_addr, nullptr),
407410
std::move(mocks.transport_socket_), stream_info_, true);
408411

409412
EXPECT_CALL(*mock_timer, enableTimer(std::chrono::milliseconds(3 * 1000), _));
@@ -418,10 +421,11 @@ TEST_P(ConnectionImplTest, SetServerTransportSocketTimeoutAfterConnect) {
418421
ConnectionMocks mocks = createConnectionMocks(false);
419422
MockTransportSocket* transport_socket = mocks.transport_socket_.get();
420423
IoHandlePtr io_handle = std::make_unique<IoSocketHandleImpl>(0);
424+
auto local_addr = std::make_shared<Network::Address::PipeInstance>("/pipe/path");
421425

422426
auto server_connection = std::make_unique<Network::ServerConnectionImpl>(
423427
*mocks.dispatcher_,
424-
std::make_unique<ConnectionSocketImpl>(std::move(io_handle), nullptr, nullptr),
428+
std::make_unique<ConnectionSocketImpl>(std::move(io_handle), local_addr, nullptr),
425429
std::move(mocks.transport_socket_), stream_info_, true);
426430

427431
transport_socket->callbacks_->raiseEvent(ConnectionEvent::Connected);
@@ -436,11 +440,12 @@ TEST_P(ConnectionImplTest, ServerTransportSocketTimeoutDisabledOnConnect) {
436440
ConnectionMocks mocks = createConnectionMocks(false);
437441
MockTransportSocket* transport_socket = mocks.transport_socket_.get();
438442
IoHandlePtr io_handle = std::make_unique<IoSocketHandleImpl>(0);
443+
auto local_addr = std::make_shared<Network::Address::PipeInstance>("/pipe/path");
439444

440445
auto* mock_timer = new NiceMock<Event::MockTimer>(mocks.dispatcher_.get());
441446
auto server_connection = std::make_unique<Network::ServerConnectionImpl>(
442447
*mocks.dispatcher_,
443-
std::make_unique<ConnectionSocketImpl>(std::move(io_handle), nullptr, nullptr),
448+
std::make_unique<ConnectionSocketImpl>(std::move(io_handle), local_addr, nullptr),
444449
std::move(mocks.transport_socket_), stream_info_, true);
445450

446451
bool timer_destroyed = false;
@@ -598,7 +603,24 @@ TEST_P(ConnectionImplTest, ConnectionStats) {
598603
MockConnectionStats client_connection_stats;
599604
client_connection_->setConnectionStats(client_connection_stats.toBufferStats());
600605
EXPECT_TRUE(client_connection_->connecting());
606+
607+
// Make sure that NO_DELAY starts out false, so that the check below verifies that it transitions
608+
// to true actually tests something.
609+
int initial_value = 0;
610+
socklen_t size = sizeof(int);
611+
Api::SysCallIntResult result = testClientConnection()->socket()->getSocketOption(
612+
IPPROTO_TCP, TCP_NODELAY, &initial_value, &size);
613+
ASSERT_EQ(0, result.rc_);
614+
ASSERT_EQ(0, initial_value);
615+
601616
client_connection_->connect();
617+
618+
int new_value = 0;
619+
result = testClientConnection()->socket()->getSocketOption(IPPROTO_TCP, TCP_NODELAY,
620+
&initial_value, &size);
621+
ASSERT_EQ(0, result.rc_);
622+
ASSERT_EQ(0, new_value);
623+
602624
// The Network::Connection class oddly uses onWrite as its indicator of if
603625
// it's done connection, rather than the Connected event.
604626
EXPECT_TRUE(client_connection_->connecting());
@@ -1877,22 +1899,19 @@ TEST_P(ConnectionImplTest, DelayedCloseTimeoutNullStats) {
18771899
}
18781900

18791901
// Test DumpState methods.
1880-
TEST_P(ConnectionImplTest, NetworkSocketDumpsWithoutAllocatingMemory) {
1902+
TEST_P(ConnectionImplTest, NetworkAndPipeSocketDumpsWithoutAllocatingMemory) {
18811903
std::array<char, 1024> buffer;
18821904
OutputBufferStream ostream{buffer.data(), buffer.size()};
18831905
IoHandlePtr io_handle = std::make_unique<IoSocketHandleImpl>(0);
1906+
// Avoid setting noDelay on the fake fd of 0.
1907+
auto local_addr = std::make_shared<Network::Address::PipeInstance>("/pipe/path");
18841908
Address::InstanceConstSharedPtr server_addr;
1885-
Address::InstanceConstSharedPtr local_addr;
18861909
if (GetParam() == Network::Address::IpVersion::v4) {
18871910
server_addr = Network::Address::InstanceConstSharedPtr{
18881911
new Network::Address::Ipv4Instance("1.1.1.1", 80, nullptr)};
1889-
local_addr = Network::Address::InstanceConstSharedPtr{
1890-
new Network::Address::Ipv4Instance("1.2.3.4", 56789, nullptr)};
18911912
} else {
18921913
server_addr = Network::Address::InstanceConstSharedPtr{
18931914
new Network::Address::Ipv6Instance("::1", 80, nullptr)};
1894-
local_addr = Network::Address::InstanceConstSharedPtr{
1895-
new Network::Address::Ipv6Instance("::1:2:3:4", 56789, nullptr)};
18961915
}
18971916

18981917
auto connection_socket =
@@ -1914,12 +1933,12 @@ TEST_P(ConnectionImplTest, NetworkSocketDumpsWithoutAllocatingMemory) {
19141933
contents,
19151934
HasSubstr(
19161935
"remote_address_: 1.1.1.1:80, direct_remote_address_: 1.1.1.1:80, local_address_: "
1917-
"1.2.3.4:56789"));
1936+
"/pipe/path"));
19181937
} else {
19191938
EXPECT_THAT(
19201939
contents,
19211940
HasSubstr("remote_address_: [::1]:80, direct_remote_address_: [::1]:80, local_address_: "
1922-
"[::1:2:3:4]:56789"));
1941+
"/pipe/path"));
19231942
}
19241943
}
19251944

@@ -1928,10 +1947,11 @@ TEST_P(ConnectionImplTest, NetworkConnectionDumpsWithoutAllocatingMemory) {
19281947
OutputBufferStream ostream{buffer.data(), buffer.size()};
19291948
ConnectionMocks mocks = createConnectionMocks(false);
19301949
IoHandlePtr io_handle = std::make_unique<IoSocketHandleImpl>(0);
1950+
auto local_addr = std::make_shared<Network::Address::PipeInstance>("/pipe/path");
19311951

19321952
auto server_connection = std::make_unique<Network::ServerConnectionImpl>(
19331953
*mocks.dispatcher_,
1934-
std::make_unique<ConnectionSocketImpl>(std::move(io_handle), nullptr, nullptr),
1954+
std::make_unique<ConnectionSocketImpl>(std::move(io_handle), local_addr, nullptr),
19351955
std::move(mocks.transport_socket_), stream_info_, true);
19361956

19371957
// Start measuring memory and dump state.

test/common/tcp/conn_pool_test.cc

-1
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,6 @@ class TcpConnPoolImplDestructorTest : public Event::TestUsingSimulatedTime,
319319
EXPECT_CALL(*connection_, addReadFilter(_));
320320
EXPECT_CALL(*connection_, connect());
321321
EXPECT_CALL(*connection_, setConnectionStats(_));
322-
EXPECT_CALL(*connection_, noDelay(true));
323322
EXPECT_CALL(*connection_, streamInfo()).Times(2);
324323
EXPECT_CALL(*connection_, id()).Times(AnyNumber());
325324

test/extensions/filters/network/common/redis/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ envoy_cc_test(
5858
"//test/mocks/thread_local:thread_local_mocks",
5959
"//test/mocks/upstream:host_mocks",
6060
"//test/test_common:simulated_time_system_lib",
61+
"//test/test_common:test_runtime_lib",
6162
"@envoy_api//envoy/extensions/filters/network/redis_proxy/v3:pkg_cc_proto",
6263
],
6364
)

test/extensions/filters/network/common/redis/client_impl_test.cc

+61-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "test/mocks/network/mocks.h"
1515
#include "test/mocks/upstream/host.h"
1616
#include "test/test_common/simulated_time_system.h"
17+
#include "test/test_common/test_runtime.h"
1718

1819
#include "gmock/gmock.h"
1920
#include "gtest/gtest.h"
@@ -75,8 +76,9 @@ class RedisClientImplTest : public testing::Test,
7576
EXPECT_CALL(*upstream_connection_, addReadFilter(_))
7677
.WillOnce(SaveArg<0>(&upstream_read_filter_));
7778
EXPECT_CALL(*upstream_connection_, connect());
78-
EXPECT_CALL(*upstream_connection_, noDelay(true));
79-
79+
if (legacy_nodelay_) {
80+
EXPECT_CALL(*upstream_connection_, noDelay(true));
81+
}
8082
redis_command_stats_ =
8183
Common::Redis::RedisCommandStats::createRedisCommandStats(stats_.symbolTable());
8284

@@ -145,6 +147,7 @@ class RedisClientImplTest : public testing::Test,
145147
Common::Redis::RedisCommandStatsSharedPtr redis_command_stats_;
146148
std::string auth_username_;
147149
std::string auth_password_;
150+
bool legacy_nodelay_{};
148151
};
149152

150153
TEST_F(RedisClientImplTest, BatchWithZeroBufferAndTimeout) {
@@ -338,6 +341,62 @@ TEST_F(RedisClientImplTest, Basic) {
338341
client_->close();
339342
}
340343

344+
TEST_F(RedisClientImplTest, BasicLegacyNodelay) {
345+
legacy_nodelay_ = true;
346+
TestScopedRuntime scoped_runtime;
347+
Runtime::LoaderSingleton::getExisting()->mergeValues(
348+
{{"envoy.reloadable_features.always_nodelay", "false"}});
349+
InSequence s;
350+
351+
setup();
352+
353+
client_->initialize(auth_username_, auth_password_);
354+
355+
Common::Redis::RespValue request1;
356+
MockClientCallbacks callbacks1;
357+
EXPECT_CALL(*encoder_, encode(Ref(request1), _));
358+
EXPECT_CALL(*flush_timer_, enabled()).WillOnce(Return(false));
359+
PoolRequest* handle1 = client_->makeRequest(request1, callbacks1);
360+
EXPECT_NE(nullptr, handle1);
361+
362+
onConnected();
363+
364+
Common::Redis::RespValue request2;
365+
MockClientCallbacks callbacks2;
366+
EXPECT_CALL(*encoder_, encode(Ref(request2), _));
367+
EXPECT_CALL(*flush_timer_, enabled()).WillOnce(Return(false));
368+
PoolRequest* handle2 = client_->makeRequest(request2, callbacks2);
369+
EXPECT_NE(nullptr, handle2);
370+
371+
EXPECT_EQ(2UL, host_->cluster_.stats_.upstream_rq_total_.value());
372+
EXPECT_EQ(2UL, host_->cluster_.stats_.upstream_rq_active_.value());
373+
EXPECT_EQ(2UL, host_->stats_.rq_total_.value());
374+
EXPECT_EQ(2UL, host_->stats_.rq_active_.value());
375+
376+
Buffer::OwnedImpl fake_data;
377+
EXPECT_CALL(*decoder_, decode(Ref(fake_data))).WillOnce(Invoke([&](Buffer::Instance&) -> void {
378+
InSequence s;
379+
Common::Redis::RespValuePtr response1(new Common::Redis::RespValue());
380+
EXPECT_CALL(callbacks1, onResponse_(Ref(response1)));
381+
EXPECT_CALL(*connect_or_op_timer_, enableTimer(_, _));
382+
EXPECT_CALL(host_->outlier_detector_,
383+
putResult(Upstream::Outlier::Result::ExtOriginRequestSuccess, _));
384+
callbacks_->onRespValue(std::move(response1));
385+
386+
Common::Redis::RespValuePtr response2(new Common::Redis::RespValue());
387+
EXPECT_CALL(callbacks2, onResponse_(Ref(response2)));
388+
EXPECT_CALL(*connect_or_op_timer_, disableTimer());
389+
EXPECT_CALL(host_->outlier_detector_,
390+
putResult(Upstream::Outlier::Result::ExtOriginRequestSuccess, _));
391+
callbacks_->onRespValue(std::move(response2));
392+
}));
393+
upstream_read_filter_->onData(fake_data, false);
394+
395+
EXPECT_CALL(*upstream_connection_, close(Network::ConnectionCloseType::NoFlush));
396+
EXPECT_CALL(*connect_or_op_timer_, disableTimer());
397+
client_->close();
398+
}
399+
341400
class ConfigEnableCommandStats : public Config {
342401
bool disableOutlierEvents() const override { return false; }
343402
std::chrono::milliseconds opTimeout() const override { return std::chrono::milliseconds(25); }

0 commit comments

Comments
 (0)