Skip to content

Commit d2d6247

Browse files
excelle08meta-codesync[bot]
authored andcommitted
Three knobs to rebalance Compression/Encryption: partial mock-ZSTD, server-ZSTD env gate, driver↔server TLS (facebookresearch#746)
Summary: Pull Request resolved: facebookresearch#746 The t41 9-cell sweep (single×autoscale × rpc_fanout_scale on CPL/BGM/GRC, classified with the new v2 hot-func rules in D108210357) showed two persistent gaps vs prod multifeed_aggregator: - **Compression over-target**: bench 5.9-10.0% across cells vs prod 3.9-5.2%. Bench runs ZSTD on every outbound mock_services channel (`MOCK_COMPRESS_ZSTD=1`) AND on every server response (`compressThrift`), so the total compression CPU footprint is roughly 2× prod. - **Encryption under-target**: bench 0.9-1.8% on x86 vs prod 3.3-3.6%. The mock_services channel uses TLS (closes part of the gap) but the driver↔server channel runs plaintext over a raw `AsyncSocket`/libevent `bufferevent_socket`, leaving the bulk of the bench unencrypted. This diff adds three independent knobs that let us experiment with the rebalance without touching the production-aligned defaults. All knobs are env-var-driven (LeafNodeRank uses gengetopt and rejects unknown CLI flags); all default to current behavior so unset = no change. **Knob 1 — `MOCK_ZSTD_FRAC` (replaces all-or-nothing `MOCK_COMPRESS_ZSTD`):** Float in [0.0, 1.0] that controls the fraction of `MockServicesClient` instances that enable per-channel ZSTD `CompressionConfig`. Each leaf builds one client per `SREventBase` thread (~88-176 per process); a process-global atomic counter assigns each new client a monotonic index, and the decision `(idx % 100) < int(frac * 100)` gives deterministic per-channel selection. Matches prod's mix-of-compressed-and-plaintext downstream services. Backwards compat: when `MOCK_ZSTD_FRAC` is unset, falls back to legacy `MOCK_COMPRESS_ZSTD` (1→1.0, 0→0.0); default when both unset = 1.0. **Knob 2 — `FEEDSIM_SERVER_ZSTD` (gate for server-side response compression):** Boolean env var, default true. When "0", both `compressThrift()` and `compressPayload()` in `LeafNodeRank.cc` return the input unchanged (passthrough), bypassing the ZSTD codec entirely. Reads the env once via `static const bool` so request-time cost is a single branch. **Knob 3 — `FEEDSIM_DRIVER_TLS` (TLS on the driver↔server channel):** Boolean env var, default false. When "1": - `FeedSimServer` reads `FEEDSIM_TLS_CERT` / `FEEDSIM_TLS_KEY` env vars (wired by `run.sh` to the existing bench cert/key at `${FEEDSIM_ROOT}/certs/example.{crt,key}`), creates a `folly::SSLContext`, and wraps each accepted fd in `AsyncSSLSocket(ctx, evb, fd, true /*server*/)` via the `AcceptCallback` constructor. - `FeedSimDriver` lazy-inits a process-global `SSL_CTX` (TLS_client_method, `SSL_VERIFY_NONE`) on first connection, and wraps each `DriverConnection`'s libevent `bufferevent` with `bufferevent_openssl_socket_new(base, fd, SSL_new(ctx), BUFFEREVENT_SSL_CONNECTING, BEV_OPT_CLOSE_ON_FREE)`. Requires `libevent_openssl` (added to `CMakeLists.txt` via `find_library(LIBEVENT_OPENSSL_LIB NAMES event_openssl ...)` and appended to `ranking-cpp2`'s link line). Independent of `FEEDSIM_TLS`, which only covers the mock_services channel. No ALPN — the driver↔server protocol is custom binary, not Rocket. Files: - `MockServicesClient.cc`: add `envFloat()` helper + `resolveZstdFraction()` + atomic per-instance counter; replace `use_zstd = envBoolTrue(...)` with deterministic per-channel decision based on resolved fraction. `LOG_FIRST_N` logs the active fraction once per process. - `LeafNodeRank.cc`: gate `compressThrift()` and `compressPayload()` on `FEEDSIM_SERVER_ZSTD`. Each function reads the env once via `static const bool`. - `FeedSimServer.{cc,h}`: add `std::shared_ptr<folly::SSLContext>` member to `Impl`; in `run()` read `FEEDSIM_TLS_CERT` / `FEEDSIM_TLS_KEY` env vars and build the context if both set; pass the shared_ptr through `AcceptCallback`'s new ctor arg; in `connectionAccepted`, when ctx is set, wrap the fd in `AsyncSSLSocket` instead of plain `AsyncSocket`. Falls back to plain `AsyncSocket` when ctx is null. - `FeedSimDriver.cc`: include `<event2/bufferevent_ssl.h>` + `<openssl/ssl.h>`; add `getDriverSslCtxOrNull()` with lazy static SSL_CTX init from `FEEDSIM_DRIVER_TLS` env; in `DriverConnection` ctor, when ctx is non-null, wrap fd in `bufferevent_openssl_socket_new` instead of plain `bufferevent_socket_new`. - `CMakeLists.txt`: `find_library(LIBEVENT_OPENSSL_LIB NAMES event_openssl ...)` after the `find_program(GENGETOPT_EXECUTABLE)` block; append `${LIBEVENT_OPENSSL_LIB}` to `ranking-cpp2` next to `${LIBEVENT_LIB}`. - `run.sh`: pass through `MOCK_ZSTD_FRAC` and `FEEDSIM_SERVER_ZSTD` env vars; when `FEEDSIM_DRIVER_TLS=1`, also export `FEEDSIM_TLS_CERT` and `FEEDSIM_TLS_KEY` pointing at `${FEEDSIM_ROOT}/certs/example.{crt,key}` (with readability check that errors out if missing). Reviewed By: charles-typ Differential Revision: D109352301
1 parent f8ab6cd commit d2d6247

6 files changed

Lines changed: 262 additions & 10 deletions

File tree

packages/feedsim/run.sh

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,43 @@ main() {
780780
echo "MockServicesClient ZSTD: DISABLED (via MOCK_COMPRESS_ZSTD env)"
781781
fi
782782

783+
# t43 knobs (2026-06-10): three independent knobs for the bench-vs-prod
784+
# Compression / Encryption rebalance. See plan doc t41/t43 progress logs.
785+
#
786+
# MOCK_ZSTD_FRAC: float in [0.0, 1.0]. Fraction of MockServicesClient
787+
# channels that enable per-channel ZSTD. Replaces all-or-nothing
788+
# MOCK_COMPRESS_ZSTD with prod-realistic partial enablement (some
789+
# downstream services compress, others don't).
790+
if [ -n "${MOCK_ZSTD_FRAC:-}" ]; then
791+
export MOCK_ZSTD_FRAC
792+
echo "MockServicesClient ZSTD fraction: ${MOCK_ZSTD_FRAC} (overrides MOCK_COMPRESS_ZSTD)"
793+
fi
794+
# FEEDSIM_SERVER_ZSTD: 0 disables server-side response ZSTD
795+
# (compressThrift / compressPayload return passthrough). Default 1
796+
# preserves current behavior. Use to reduce the bench's Compression
797+
# CPU share when over-target.
798+
if [ "${FEEDSIM_SERVER_ZSTD:-1}" != "1" ]; then
799+
export FEEDSIM_SERVER_ZSTD=0
800+
echo "Server-side response ZSTD: DISABLED (FEEDSIM_SERVER_ZSTD=0)"
801+
fi
802+
# FEEDSIM_DRIVER_TLS: 1 enables TLS on the driver↔server channel
803+
# (DriverNodeRank ↔ LeafNodeRank). Server reads FEEDSIM_TLS_CERT /
804+
# FEEDSIM_TLS_KEY env vars (set here to the existing bench cert/key
805+
# under ${FEEDSIM_ROOT}/certs/); driver reads FEEDSIM_DRIVER_TLS
806+
# directly. Closes the bench's Encryption CPU undershoot (prod
807+
# 3.3-3.6% vs bench 0.9-1.5% in t41). Independent of FEEDSIM_TLS,
808+
# which only covers the mock_services channel.
809+
if [ "${FEEDSIM_DRIVER_TLS:-0}" = "1" ]; then
810+
export FEEDSIM_DRIVER_TLS=1
811+
export FEEDSIM_TLS_CERT="${FEEDSIM_ROOT}/certs/example.crt"
812+
export FEEDSIM_TLS_KEY="${FEEDSIM_ROOT}/certs/example.key"
813+
if [ ! -r "${FEEDSIM_TLS_CERT}" ] || [ ! -r "${FEEDSIM_TLS_KEY}" ]; then
814+
echo "ERROR: FEEDSIM_DRIVER_TLS=1 but ${FEEDSIM_TLS_CERT} or .key not readable" >&2
815+
exit 1
816+
fi
817+
echo "Driver↔Server TLS: ENABLED (cert=${FEEDSIM_TLS_CERT})"
818+
fi
819+
783820
# OMP_NUM_THREADS=1: cap PyTorch's OpenMP parallel backend pool to
784821
# 1 thread. at::set_num_threads(1) only affects libtorch's native
785822
# parallel backend; OpenMP-backed builds (which Meta's internal

packages/feedsim/third_party/src/workloads/ranking/CMakeLists.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ find_package(Snappy REQUIRED)
3838
find_package(LibLZMA REQUIRED)
3939
find_program(GENGETOPT_EXECUTABLE gengetopt REQUIRED)
4040

41+
# libevent_openssl: required by FeedSimDriver.cc's bufferevent_openssl_socket_new
42+
# (used when FEEDSIM_DRIVER_TLS=1). libevent ships this as a separate library
43+
# alongside the core libevent. The .pc is libevent_openssl.pc and the lib file
44+
# is libevent_openssl.{so,a}.
45+
find_library(LIBEVENT_OPENSSL_LIB
46+
NAMES event_openssl
47+
PATHS ${CMAKE_INSTALL_PREFIX}/lib /usr/local/lib /usr/lib /usr/lib64
48+
)
49+
if(NOT LIBEVENT_OPENSSL_LIB)
50+
message(WARNING "libevent_openssl not found; FEEDSIM_DRIVER_TLS will fail at runtime")
51+
endif()
52+
4153
include(if/CMakeLists.txt)
4254
add_dependencies(ranking-cpp2-target fbthrift)
4355
set_target_properties(
@@ -64,6 +76,7 @@ target_link_libraries(ranking-cpp2
6476
${SNAPPY_LIBRARY}
6577
${FOLLY_LIBRARIES}
6678
${LIBEVENT_LIB}
79+
${LIBEVENT_OPENSSL_LIB}
6780
${JEMALLOC_LIB}
6881
${LIBLZMA_LIBRARIES}
6982
${LIBSODIUM_LIBRARIES}

packages/feedsim/third_party/src/workloads/ranking/FeedSimDriver.cc

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
#include <event2/event.h>
1212
#include <event2/buffer.h>
1313
#include <event2/bufferevent.h>
14+
#include <event2/bufferevent_ssl.h>
1415
#include <event2/thread.h>
1516
#include <netdb.h>
1617
#include <netinet/tcp.h>
18+
#include <openssl/err.h>
19+
#include <openssl/ssl.h>
1720
#include <pthread.h>
1821
#include <signal.h>
1922
#include <string.h>
@@ -218,6 +221,39 @@ void DriverStats::printStats(uint32_t type, double elapsed_secs) const {
218221
}
219222
}
220223

224+
// FEEDSIM_DRIVER_TLS env gate: when "1", every DriverConnection wraps its
225+
// libevent bufferevent in a TLS bufferevent via OpenSSL. Bench-only — peer
226+
// cert verification is disabled (SSL_VERIFY_NONE). The SSL_CTX is process-
227+
// global (one per process), constructed lazily on first use to avoid paying
228+
// the OpenSSL init cost when TLS is off. Closes the bench's Encryption CPU
229+
// undershoot on the driver↔server channel (paired with FeedSimServer's
230+
// FEEDSIM_TLS_CERT / FEEDSIM_TLS_KEY env vars). See t41 progress log.
231+
namespace {
232+
SSL_CTX* getDriverSslCtxOrNull() {
233+
static SSL_CTX* s_ctx = []() -> SSL_CTX* {
234+
const char* env = std::getenv("FEEDSIM_DRIVER_TLS");
235+
if (env == nullptr || std::strcmp(env, "1") != 0) {
236+
return nullptr;
237+
}
238+
// OpenSSL >= 1.1 self-initializes; keep these calls as no-ops on
239+
// older libs for forward compat.
240+
SSL_library_init();
241+
SSL_load_error_strings();
242+
OpenSSL_add_all_algorithms();
243+
SSL_CTX* ctx = SSL_CTX_new(TLS_client_method());
244+
if (ctx == nullptr) {
245+
std::cerr << "FeedSimDriver: SSL_CTX_new failed" << std::endl;
246+
return nullptr;
247+
}
248+
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, nullptr);
249+
std::cout << "FeedSimDriver: TLS enabled via FEEDSIM_DRIVER_TLS=1"
250+
<< std::endl;
251+
return ctx;
252+
}();
253+
return s_ctx;
254+
}
255+
} // namespace
256+
221257
// ─── DriverConnection: one TCP connection to the server ─────────────────────
222258

223259
class DriverConnection {
@@ -238,7 +274,33 @@ class DriverConnection {
238274
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &optval, sizeof(optval));
239275
}
240276
evutil_make_socket_nonblocking(sockfd);
241-
bev_ = bufferevent_socket_new(base, sockfd, BEV_OPT_CLOSE_ON_FREE);
277+
SSL_CTX* ssl_ctx = getDriverSslCtxOrNull();
278+
if (ssl_ctx != nullptr) {
279+
// Per-connection SSL object owned by the bufferevent
280+
// (BEV_OPT_CLOSE_ON_FREE will SSL_free it). State is CONNECTING
281+
// because we already have an open TCP socket and need libevent to
282+
// drive the TLS handshake from the client side.
283+
SSL* ssl = SSL_new(ssl_ctx);
284+
if (ssl == nullptr) {
285+
std::cerr << "Error: SSL_new failed" << std::endl;
286+
abort();
287+
}
288+
bev_ = bufferevent_openssl_socket_new(
289+
base,
290+
sockfd,
291+
ssl,
292+
BUFFEREVENT_SSL_CONNECTING,
293+
BEV_OPT_CLOSE_ON_FREE);
294+
if (bev_ == nullptr) {
295+
std::cerr << "Error: bufferevent_openssl_socket_new failed"
296+
<< std::endl;
297+
SSL_free(ssl);
298+
::close(sockfd);
299+
abort();
300+
}
301+
} else {
302+
bev_ = bufferevent_socket_new(base, sockfd, BEV_OPT_CLOSE_ON_FREE);
303+
}
242304
}
243305

244306
~DriverConnection() {

packages/feedsim/third_party/src/workloads/ranking/FeedSimServer.cc

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
2727

2828
#include <folly/MPMCQueue.h>
2929
#include <folly/container/F14Map.h>
30+
#include <folly/io/async/AsyncSSLSocket.h>
3031
#include <folly/io/async/AsyncServerSocket.h>
3132
#include <folly/io/async/AsyncSocket.h>
3233
#include <folly/io/async/AsyncTransport.h>
3334
#include <folly/io/async/EventBase.h>
3435
#include <folly/io/async/EventBaseManager.h>
36+
#include <folly/io/async/SSLContext.h>
3537

3638
namespace feedsim {
3739

@@ -241,9 +243,11 @@ class AcceptCallback : public folly::AsyncServerSocket::AcceptCallback {
241243
public:
242244
AcceptCallback(
243245
std::vector<std::unique_ptr<WorkerThread>>& workers,
244-
const folly::F14FastMap<uint32_t, QueryCallback>& callbacks)
246+
const folly::F14FastMap<uint32_t, QueryCallback>& callbacks,
247+
std::shared_ptr<folly::SSLContext> ssl_ctx)
245248
: workers_(workers),
246249
callbacks_(callbacks),
250+
ssl_ctx_(std::move(ssl_ctx)),
247251
next_worker_(0) {}
248252

249253
void connectionAccepted(
@@ -260,12 +264,29 @@ class AcceptCallback : public folly::AsyncServerSocket::AcceptCallback {
260264
auto& worker = workers_[next_worker_];
261265
next_worker_ = (next_worker_ + 1) % workers_.size();
262266

263-
// Create connection on the worker's EventBase
267+
// Create connection on the worker's EventBase. When TLS is enabled
268+
// (FEEDSIM_TLS_CERT/FEEDSIM_TLS_KEY env vars set at server startup),
269+
// wrap the accepted fd in folly::AsyncSSLSocket so the TLS handshake
270+
// is performed on the worker's EventBase before any wire reads. This
271+
// closes the bench's Encryption CPU undershoot (prod ~3.3-3.6% vs
272+
// bench ~0.9-1.5% in t41). Plain AsyncSocket preserves the original
273+
// no-TLS behavior when the env vars are unset.
274+
auto ssl_ctx = ssl_ctx_;
264275
worker->evb->runInEventBaseThread(
265276
[fd, thread_id = worker->thread_id, &callbacks = callbacks_,
266-
evb = worker->evb.get()]() {
267-
auto socket = folly::AsyncSocket::newSocket(
268-
evb, folly::NetworkSocket::fromFd(fd));
277+
evb = worker->evb.get(), ssl_ctx]() {
278+
folly::AsyncSocket::UniquePtr socket;
279+
if (ssl_ctx) {
280+
// AsyncSSLSocket server-side: pass true for the server flag.
281+
// The handshake is initiated lazily on first read/write,
282+
// matching the existing client's connect-then-write pattern.
283+
folly::AsyncSSLSocket::UniquePtr ssl_sock(new folly::AsyncSSLSocket(
284+
ssl_ctx, evb, folly::NetworkSocket::fromFd(fd), true));
285+
socket.reset(ssl_sock.release());
286+
} else {
287+
socket = folly::AsyncSocket::newSocket(
288+
evb, folly::NetworkSocket::fromFd(fd));
289+
}
269290
// ServerConnection self-manages its lifetime
270291
new ServerConnection(std::move(socket), thread_id, callbacks);
271292
});
@@ -278,6 +299,7 @@ class AcceptCallback : public folly::AsyncServerSocket::AcceptCallback {
278299
private:
279300
std::vector<std::unique_ptr<WorkerThread>>& workers_;
280301
const folly::F14FastMap<uint32_t, QueryCallback>& callbacks_;
302+
std::shared_ptr<folly::SSLContext> ssl_ctx_;
281303
size_t next_worker_;
282304
};
283305

@@ -298,6 +320,11 @@ struct FeedSimServer::Impl {
298320
std::unique_ptr<AcceptCallback> accept_cb;
299321
std::vector<std::unique_ptr<WorkerThread>> workers;
300322

323+
// Optional SSL context — populated at startup from FEEDSIM_TLS_CERT /
324+
// FEEDSIM_TLS_KEY env vars. When set, accepted connections are wrapped
325+
// in AsyncSSLSocket. Driver-side env: FEEDSIM_DRIVER_TLS=1.
326+
std::shared_ptr<folly::SSLContext> ssl_ctx;
327+
301328
std::atomic<bool> running{false};
302329
};
303330

@@ -390,8 +417,38 @@ void FeedSimServer::run() {
390417
impl_->main_evb = std::make_unique<folly::EventBase>();
391418
impl_->server_socket = folly::AsyncServerSocket::newSocket(impl_->main_evb.get());
392419

420+
// Optional TLS: when FEEDSIM_TLS_CERT and FEEDSIM_TLS_KEY are both set,
421+
// create an SSL context and pass it through to AcceptCallback, which
422+
// wraps each accepted fd in AsyncSSLSocket. Reuses the same cert/key
423+
// pair as mock_services (FEEDSIM_TLS_CERT defaults to
424+
// ${FEEDSIM_ROOT}/certs/example.crt via run.sh wiring). Bench-only:
425+
// peer cert verification is not configured here; the client side
426+
// (FeedSimDriver) skips verify via SSL_VERIFY_NONE.
427+
{
428+
const char* cert_env = std::getenv("FEEDSIM_TLS_CERT");
429+
const char* key_env = std::getenv("FEEDSIM_TLS_KEY");
430+
if (cert_env != nullptr && key_env != nullptr && cert_env[0] != '\0' &&
431+
key_env[0] != '\0') {
432+
try {
433+
auto ctx = std::make_shared<folly::SSLContext>();
434+
ctx->loadCertificate(cert_env);
435+
ctx->loadPrivateKey(key_env);
436+
// No ALPN — FeedSim uses its own custom binary protocol over the
437+
// TLS-wrapped socket, not Rocket. The client similarly does not
438+
// advertise ALPN.
439+
impl_->ssl_ctx = std::move(ctx);
440+
std::cout << "FeedSimServer: TLS enabled (cert=" << cert_env
441+
<< " key=" << key_env << ")" << std::endl;
442+
} catch (const std::exception& e) {
443+
std::cerr << "FeedSimServer: failed to init SSL context: " << e.what()
444+
<< " — falling back to plaintext" << std::endl;
445+
impl_->ssl_ctx.reset();
446+
}
447+
}
448+
}
449+
393450
impl_->accept_cb = std::make_unique<AcceptCallback>(
394-
impl_->workers, impl_->query_callbacks);
451+
impl_->workers, impl_->query_callbacks, impl_->ssl_ctx);
395452

396453
impl_->server_socket->addAcceptCallback(
397454
impl_->accept_cb.get(), nullptr);

packages/feedsim/third_party/src/workloads/ranking/LeafNodeRank.cc

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,17 @@ std::string compressPayload(const std::string& data, int /*result*/) {
809809
folly::StringPiece output(
810810
data.data(),
811811
std::min(args.compression_data_size_arg, args.random_data_size_arg));
812+
// Honor FEEDSIM_SERVER_ZSTD passthrough (see comment above
813+
// compressThrift). For symmetry; this path is hit on the random-payload
814+
// workload variant. kServerZstdLocal mirrors the global once-per-process
815+
// read in compressThrift's kServerZstd.
816+
static const bool kServerZstdLocal = []() {
817+
const char* v = std::getenv("FEEDSIM_SERVER_ZSTD");
818+
return v == nullptr || std::strcmp(v, "0") != 0;
819+
}();
820+
if (!kServerZstdLocal) {
821+
return std::string(output.data(), output.size());
822+
}
812823
#ifdef BENCHPRESS_INTERNAL
813824
return getRandomStringCodec()->compress(output);
814825
#else
@@ -830,8 +841,27 @@ std::string decompressPayload(const std::string& data) {
830841
#endif
831842
}
832843

844+
// FEEDSIM_SERVER_ZSTD env gate: when "0", server-side response compression
845+
// is bypassed (passthrough). Default behavior (ZSTD on) preserved. Lets the
846+
// bench match prod's lighter Compression CPU share (prod 3.9-5.2% vs bench
847+
// 6-10% in t41) without ripping out the entire ZSTD path. Decision is read
848+
// once per process to avoid getenv() on every request — kServerZstd is
849+
// shared with compressPayload below.
850+
namespace {
851+
bool readServerZstdEnv() {
852+
const char* v = std::getenv("FEEDSIM_SERVER_ZSTD");
853+
return v == nullptr || std::strcmp(v, "0") != 0;
854+
}
855+
} // namespace
856+
833857
std::unique_ptr<folly::IOBuf> compressThrift(
834858
std::unique_ptr<folly::IOBuf> buf) {
859+
static const bool kServerZstd = readServerZstdEnv();
860+
LOG_FIRST_N(INFO, 1) << "Server-side response ZSTD: "
861+
<< (kServerZstd ? "ON" : "OFF (passthrough)");
862+
if (!kServerZstd) {
863+
return buf;
864+
}
835865
#ifdef BENCHPRESS_INTERNAL
836866
return getThriftPayloadCodec()->compress(buf.get());
837867
#else

0 commit comments

Comments
 (0)