왜 하필 혼잡제어를 건드리는 거지?
TCP 혼잡제어는 아무래도 굉장히 기본적인 부분이고 건드려선 안 될 것같은 느낌이 든다. 하지만 적절한 튜닝이 동반된다면 내가 원하는 방향의 동작을 이끌어낼 수 있는 가성비가 우월한 부분이다. 최근에는 공격적인 처리량(Throughput)를 지양하고 주변와 손실을 고려하는 BBRv3가 등장했지만 현재 기기의 최고 성능보다는 전반적인 조화를 추구하는 경향이 있다. 게이머들은 조화보단 좋은 처리량을 원할 것이지만, 그러면서도 예측 가능한 동작이 필요하다. 나는 이것을 위한 혼잡 제어 패치를 소개하고자 한다.
기존의 BBRv1: 뭐가 문제인가?
기존의 BBR은 대역폭의 하락에 대해 델타를 계산해서 반영하는 등의 안전 장치가 빠져 있고, 심한 경우 관성적으로 패킷을 보낼 수 있다. 이러한 문제점을 해결하려면 델타, 즉 하락의 변화량을 계산해서 그에 비례해서 패킷 송출 간격을 늘이면 패킷이 균일하게 줄줄이 소시지처럼 착착 전송될 수 있도록 해야 한다.
설계
- 대역폭 하락 시 Delta 계산
- 대역폭의 하락에 비례해서 송출 간격을 늘린다
- 델타로 보아 지연이 심화되면 혼잡 윈도우를 강제로 깎음
- 네트워크에 머무는 패킷도 강제적으로 줄어든다 아주 간단하지만 알기 쉬운 알고리즘이다. BBRv3만큼의 정교한 조화는 아니지만 낮은 Throughput을 유지하면서도 적당한 버퍼 점유(Bufferbloat) 제어를 할 수 있다. 이것은 결과적으로 LAN 지연 스파이크를 줄여서 밀리세컨드 단위조차 중요한 온라인 게이밍 시 긍정적인 영향을 줄 수 있다.
- 델타로 보아 지연이 심화되면 혼잡 윈도우를 강제로 깎음
기존 BBR이 브레이크가 부족한 F1 레이싱카, 내가 개조한 것이 성능 좋은 브레이크가 달린 시판 스포츠카라면, BBRv3는 주변 환경에 기민하게 반응하고 제어되는 고급 오프 로드 자동차라고 생각할 수 있다. 일반적인 게이머 입장에서는 적당히 안 엉키면서도 빠른 네트워크 환경을 필요로 하니 산업적인 사용이 아닌 이러한 상황에선 유효한 패치라고 볼 수 있다.
벤치마크 결과
튜토리얼에 비전공자도 쉽게 따라 하는 리눅스 커널 해킹-지터를 잡아보자로 되어 있는 강의에 포함된 패치를 적용한 Linux 6.18, Debian 13에 탑재된 제네릭 6.12, 그리고 이것까지 적용된 커널을 비교하겠다. 가능하면 6.18까지 맞추는 것이 좋으나 네트워크 스택의 변화는 적으니 편의 상 기본 커널을 사용했다. 제네릭은 Vanilla, 지난 패치는 ECMP Patched, 이번 패치는 mountain-v0.1로 표기한다.
Ping comparison mountain-v0.1
Positive values in “Δ vs …” mean lower-is-better improvement (latency / jitter-proxy reduced). “Jitter” row is shown for reference only: the post reports stdev, while ping prints mdev.
Local ping: 127.0.0.1 (ms)
| Metric | Vanilla | ECMP Patched | mountain-v0.1 (6.18.0-mountain+) | Δ vs Vanilla | Δ vs ECMP Patched |
|---|---|---|---|---|---|
| Min | 0.011000 | 0.010000 | 0.010000 | +9.09% | +0.00% |
| Avg | 0.029343 | 0.017279 | 0.014000 | +52.29% | +18.98% |
| Max | 4.650 | 0.059000 | 0.067000 | +98.56% | -13.56% |
| Max-Min (spread) | 4.639 | 0.049000 | 0.057000 | +98.77% | -16.33% |
| Jitter (stdev in post / mdev in ping) | 0.146496 | 0.006409 | 0.006000 | — | — |
Note: mountain-v0.1 values are from ping summary (min/avg/max/mdev). Vanilla/ECMP Patched values are from the post’s computed table. WAN test is excluded because I must regenerate whole test files with other kernels.
Diff 첨부
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -7682,18 +7682,8 @@ static int __napi_poll(struct napi_struct *n, bool *repoll)
netdev_err_once(n->dev, "NAPI poll function %pS returned %d, exceeding its budget of %d.\n",
n->poll, work, weight);
+ if (likely(work < weight)) {
+ /* traffic is low, decrease weight for next time */
+ if (work < (weight / 2) && weight > 16)
+ n->weight = max(weight / 2, 16);
+ return work;
+ }
+
+ /* If we got here, work == weight, which means high traffic.
+ * Increase weight for next time.
+ */
+ if (n->weight < (NAPI_POLL_WEIGHT * 4))
+ n->weight = min(n->weight * 2, NAPI_POLL_WEIGHT * 4);
- if (likely(work < weight)) return work;
/* Drivers must not modify the NAPI state if they
* consume the entire weight. In such cases this code@@ -11432,7 +11422,6 @@ int register_netdevice(struct net_device *dev)
dev_init_scheduler(dev);.io/
netdev_hold(dev, &dev->dev_registered_tracker, GFP_KERNEL);+ list_netdevice(dev);
add_device_randomness(dev->dev_addr, dev->addr_len);diff --git a/net/ipv4/tcp_bbr.c b/net/ipv4/tcp_bbr.c
index cecfe6b81..760941e55 100644--- a/net/ipv4/tcp_bbr.c+++ b/net/ipv4/tcp_bbr.c@@ -77,11 +77,6 @@
#define BBR_SCALE 8 /* scaling factor for fractions in BBR (e.g. gains) */
#define BBR_UNIT (1 << BBR_SCALE)
+/* TWEAK: Bandwidth delta parameters for pacing reduction */
+#define BW_DELTA_ALPHA (BBR_UNIT / 2) /* Rate of change for pacing */
+#define BW_DELTA_CEILING (BBR_UNIT / 4) /* Max reduction factor */
+#define BW_DELTA_FLOOR (BBR_UNIT * 3 / 4) /* Min pacing gain */
- /* BBR has the following modes for deciding how fast to send: */
enum bbr_mode {
BBR_STARTUP, /* ramp up sending rate rapidly to fill pipe */@@ -129,8 +124,7 @@ struct bbr {
u32 ack_epoch_acked:20, /* packets (S)ACKed in sampling epoch */
extra_acked_win_rtts:5, /* age of extra_acked, in round trips */
extra_acked_win_idx:1, /* current index in extra_acked array */+ reduce_cwnd:1, /* TWEAK: reduce cwnd after pacing drop */
+ unused_c:5;- unused_c:6; };
#define CYCLE_LEN 8 /* number of phases in a pacing gain cycle */@@ -532,12 +526,6 @@ static void bbr_set_cwnd(struct sock *sk, const struct rate_sample *rs,
if (!acked)
goto done; /* no packet fully ACKed; just apply caps */
+ /* TWEAK: Reduce cwnd if pacing gain dropped significantly */
+ if (bbr->reduce_cwnd) {
+ cwnd = max_t(s32, cwnd - acked, 1);
+ bbr->reduce_cwnd = 0; /* Consume flag */
+ }
- if (bbr_set_cwnd_to_recover_or_restore(sk, rs, acked, &cwnd))
goto done;
@@ -775,7 +763,6 @@ static void bbr_update_bw(struct sock *sk, const struct rate_sample *rs)
struct tcp_sock *tp = tcp_sk(sk);
struct bbr *bbr = inet_csk_ca(sk);
u64 bw;+ u64 old_bw = bbr_bw(sk); /* TWEAK: Scrape old BW */
bbr->round_start = 0;
if (rs->delivered < 0 || rs->interval_us <= 0)@@ -791,6 +778,10 @@ static void bbr_update_bw(struct sock *sk, const struct rate_sample *rs)
bbr_lt_bw_sampling(sk, rs);
- /* Divide delivered by the interval to find a (lower bound) bottleneck
- * bandwidth sample. Delivered is in packets and interval_us in uS and
- * ratio will be <<1 for most connections. So delivered is first scaled.
- */ bw = div64_long((u64)rs->delivered * BW_UNIT, rs->interval_us);
/* If this sample is application-limited, it is likely to have a very@@ -799,40 +790,15 @@ static void bbr_update_bw(struct sock *sk, const struct rate_sample *rs)
* bw, causing needless slow-down. Thus, to continue to send at the
* last measured network rate, we filter out app-limited samples unless
* they describe the path bw at least as well as our bw model.- *
- * So the goal during app-limited phase is to proceed with the best
- * network rate no matter how long. We automatically leave this
- * phase when app writes faster than the network can deliver :) */
if (!rs->is_app_limited || bw >= bbr_max_bw(sk)) {
/* Incorporate new sample into our max bw filter. */
minmax_running_max(&bbr->bw, bbr_bw_rtts, bbr->rtt_cnt, bw);
}+
+ /* START TWEAK: Pacing reduction on bandwidth drop */
+ do {
+ u64 max_bw = bbr_max_bw(sk);
+ u64 delta, pacing_factor;
+
+ if (!max_bw || bw >= old_bw)
+ break;
+
+ /* Calculate reduction factor based on bandwidth drop */
+ delta = ((old_bw - bw) * BBR_UNIT) / max_bw;
+ pacing_factor = (delta * BW_DELTA_ALPHA) >> BBR_SCALE;
+
+ if (pacing_factor > BW_DELTA_CEILING)
+ pacing_factor = BW_DELTA_CEILING;
+
+ /* Apply the factor to the current pacing_gain */
+ if (pacing_factor < BBR_UNIT)
+ bbr->pacing_gain = (bbr->pacing_gain * (BBR_UNIT - pacing_factor)) >> BBR_SCALE;
+
+ bbr->reduce_cwnd = 0;
+ if (bbr->pacing_gain < BW_DELTA_FLOOR) {
+ bbr->reduce_cwnd = 1;
+ /* Don't let pacing gain get too low */
+ if (bbr->pacing_gain < BBR_UNIT / 8)
+ bbr->pacing_gain = BBR_UNIT / 8;
+ }
+ } while (0);
+ /* END TWEAK */ }
소스 코드
이 내용은 마운틴 커널 0.1 버전으로 릴리즈되었다. 관심이 있다면 패치된 NVIDIA 오픈 드라이버와 함께 다운로드해서 설치해 보도록 하자.
마치며
지금은 12월 23일이고 이틀 뒤면 12월 25일이다. 메리 크리스마스…라지만 솔직히 냄새나는 오타쿠라서 난 별로 즐길게 없다. 코딩하는 크리스마스로 뿌듯하게 보내보자.