[측정 보고서] 홈서버의 TCP 수신 윈도우·MSS·수신 처리량 — 값이 아니라 식을 남긴다

한 줄 요약: 홈서버의 TCP 수신 윈도우·MSS·수신 처리량을 도출식과 실측으로 양쪽 다 재고, 그 과정에서 관측 스택 전부를 짊어진 2 TB SSD 가 SATA II 포트에 물려 있다는 것까지 찾아냈습니다 — 남겨야 할 것은 “몇이었다” 는 값이 아니라, 그 값이 어떤 식에서 나왔고 그 식이 맞았다는 증거라는 이야기입니다.

측정 결과: 수신 윈도우는 rcvbuf/2 로 계산되고(기본 64 KiB, 상한 3 MiB), MSS 는 1448 B, 소켓→파일버퍼 복사는 858 MB/s, 실제 네트워크 수신은 11.0 MB/s 였습니다. 네트워크가 파일 버퍼 경로보다 78배 느립니다.

측정 환경: Intel i5-4690 4코어 · RAM 31 GiB · Debian 커널 6.1.0-50 · NIC 100 Mbps full duplex · 2026-08-07 측정. 모든 값은 계산식과 실측을 양쪽 다 제시합니다. 식만 있으면 검증이 안 되고, 값만 있으면 재현이 안 됩니다.


0. 측정 대상 요약

항목 결과 근거
수신 윈도우 (기본) 65,536 B tcp_rmem 기본 131072 ÷ 2
수신 윈도우 (상한) 3,145,728 B tcp_rmem 최대 6291456 ÷ 2
윈도우 스케일 (rcv) 7 실측 wscale:_,7 전 연결 일치
MSS 1,448 B MTU 1500 − 20 − 20 − 12
TCP → 파일버퍼 858 MB/s loopback 2 GiB 전송
TCP → 디스크 333 / 207 / 152 MB/s SSD(SATA III) / SSD(SATA II) / HDD
네트워크 수신 11.0 MB/s 인터넷 3회 반복

1. 수신 윈도우(Receive Window) — 수신측 기준

이 글에서 윈도우는 화면의 창이 아니라 TCP 헤더의 Window 필드 — 즉 수신측이 “지금 이만큼 더 받을 수 있다” 고 알리는 바이트 수를 가리킵니다.

1-1. 커널 파라미터 (실측)

net.ipv4.tcp_rmem            = 4096   131072   6291456     (min / default / max)
net.core.rmem_max            = 212992
net.ipv4.tcp_window_scaling  = 1
net.ipv4.tcp_adv_win_scale   = 1
net.ipv4.tcp_moderate_rcvbuf = 1      (자동 튜닝 켜짐)

1-2. 윈도우 크기 도출식

수신 버퍼 전부가 윈도우가 되지 않습니다. 커널이 스킵버퍼·구조체 오버헤드용으로 일부를 떼어두고, 그 비율을 정하는 것이 tcp_adv_win_scale 입니다.

adv_win_scale > 0 :  window = rcvbuf − (rcvbuf >> adv_win_scale)
adv_win_scale < 0 :  window = rcvbuf >> (−adv_win_scale)

이 서버는 adv_win_scale = 1 이므로:

window = rcvbuf − (rcvbuf >> 1) = rcvbuf ÷ 2

기본값 : 131,072  ÷ 2 =    65,536 B   (64 KiB)
상한값 : 6,291,456 ÷ 2 = 3,145,728 B   ( 3 MiB)

실측 검증 — loopback 소켓의 ss -ti 출력:

skmem:(...,rb6291456,...)   rcv_ssthresh:3145728
                └ 수신버퍼 6291456      └ 6291456 ÷ 2 = 3145728 ✓

식이 그대로 맞습니다. 인터넷 연결 쪽도 확인됩니다:

skmem:(...,rb131072,...)   rcv_ssthresh:64088   mss:1448
                              └ 65536 − 1448 = 64088 ✓

기본 버퍼(131072)의 절반인 65,536 에서 MSS 하나만큼(1448) 뺀 값이 광고됩니다. 커널이 마지막 세그먼트 자리를 남겨두기 때문입니다.

용어 — 여기서 “광고(advertise)” 란: 선전한다는 뜻이 아닙니다. TCP 헤더의 Window 필드나 SYN 의 MSS 옵션에 자기 값을 실어 상대에게 알리는 것을 RFC 9293·7323 이 advertise 라고 쓰고, 국내 네트워크 문헌과 번역서도 이를 그대로 광고로 옮깁니다. BGP 의 “경로를 광고한다” 와 같은 용법이라 업계에서 이미 굳은 표현이고, 그래서 이 글도 광고를 그대로 씁니다.

다만 뜻은 한 번 못 박아 둡니다 — 광고 = “내 수신 능력은 이만큼이다” 라고 상대에게 통보하는 것입니다. 광고된 값은 권유가 아니라 상대가 지켜야 하는 약속이라, 알린 것보다 더 보내면 규약 위반입니다. ss 출력의 advmss 에서 adv 가 바로 이 advertised 입니다.

1-3. 윈도우 스케일 인자 도출식

TCP 헤더의 윈도우 필드는 16비트라 최대 65,535 밖에 못 적습니다. 그보다 큰 윈도우를 알리려면 윈도우 스케일 옵션(RFC 7323)으로 “이 값을 2^n 배로 읽어라” 를 함께 보냅니다.

리눅스의 결정 로직:

space = tcp_rmem[2];              /* 6291456 */
while (space > 65535 && wscale < 14) {
    space >>= 1;
    wscale++;
}

전개하면:

6291456 → 3145728(1) → 1572864(2) → 786432(3) → 393216(4)
        → 196608(5)  →   98304(6)  →  49152(7) ← 65535 이하, 정지

rcv_wscale = 7

실측 검증 — 관측한 모든 연결이 wscale:<상대>,7 로 두 번째 값이 예외 없이 7 이었습니다. 첫 번째 값(13, 10, 8, 7)은 상대방이 광고한 값이라 제각각입니다.

따라서 광고 가능한 최대 윈도우는:

65,535 << 7 = 8,388,480 B (약 8 MiB)

버퍼 상한 3 MiB 보다 크므로, 이 서버에서 윈도우를 제한하는 것은 윈도우 필드가 아니라 버퍼 크기입니다.

1-4. 왜 이 값이 중요한가 — BDP

수신 윈도우는 “상대가 ACK 없이 보낼 수 있는 최대량” 이므로, 처리량 상한을 직접 결정합니다.

최대 처리량 = 수신 윈도우 ÷ RTT

이 서버에서 Cloudflare 까지 실측 RTT 는 minrtt:32.03 ~ rtt:36.9 ms 였습니다. 기본 윈도우 64 KiB 로 계산하면:

65,536 B ÷ 0.035 s = 1,872,457 B/s ≈ 1.87 MB/s ≈ 15 Mbps

100 Mbps 회선인데 15 Mbps 에서 막힙니다. 윈도우가 병목이 되는 전형적인 경우입니다.

실제로는 그렇게 되지 않았는데, tcp_moderate_rcvbuf = 1 로 자동 튜닝이 켜져 있어 버퍼가 커지기 때문입니다. 실측된 연결들의 버퍼는 처음 rb131072 에서 시작해 rb1223440, rb2572208 까지 자라 있었습니다. 2,572,208 ÷ 2 = 1,286,104 B 윈도우면:

1,286,104 B ÷ 0.035 s = 36.7 MB/s

회선 용량(11 MB/s)을 훌쩍 넘으므로 윈도우는 더 이상 제약이 아닙니다. 자동 튜닝이 BDP 를 따라잡아 준 것이고, 이게 꺼져 있었다면 회선의 1/6 만 쓰고 있었을 겁니다.


2. MSS (Maximum Segment Size)

2-1. 도출식

MSS 는 TCP 가 한 세그먼트에 실을 수 있는 페이로드 크기입니다.

MSS = MTU − IP 헤더 − TCP 헤더

이더넷 기준:

기본형   : 1500 − 20 − 20 = 1460 B
옵션 포함: 1460 − 12       = 1448 B

12 바이트는 타임스탬프 옵션(RFC 7323)입니다. net.ipv4.tcp_timestamps = 1 로 켜져 있고, 옵션은 매 세그먼트 TCP 헤더에 실리므로 그만큼 페이로드가 줄어듭니다. (타임스탬프 자체는 10 B 이고 정렬을 위한 NOP 2 B 가 붙어 12 B)

2-2. 실측 검증

pmtu:1500   advmss:1448   mss:1448

인터넷 연결 전부에서 일치했습니다. 리눅스는 advmss 를 광고할 때 자신이 쓸 옵션을 미리 빼고 알립니다.

루프백도 같은 식으로 설명됩니다:

pmtu:65535  advmss:65483
→ 65535 − 20(IP) − 32(TCP 20 + 옵션 12) = 65483 ✓

2-3. 예외 하나

한 상대(34.149.66.165, Google)만 mss:1400 이었습니다. 실제 사용 MSS 는 양쪽이 광고한 값 중 작은 쪽이므로, 이 연결만 1400 으로 내려갑니다. 터널링 헤더 여유를 두려는 쪽이 흔히 이렇게 합니다.

실사용 MSS = min(내 advmss, 상대 advmss) = min(1448, 1400) = 1400

3. TCP 버퍼 → 파일 I/O 버퍼 읽기 속도

3-1. 측정 방법

네트워크 회선의 영향을 배제하기 위해 루프백으로 측정했습니다. 목적이 “소켓 버퍼에서 파일 버퍼로 옮기는 비용” 이므로, 물리 NIC 를 태우면 회선 속도(11 MB/s)에 가려져 아무것도 안 보입니다.

# 송신: 무한 0 스트림
nc -l -p 9099 < /dev/zero

# 수신: 소켓에서 읽어 목적지로 쓰기 (2048 MiB)
nc 127.0.0.1 9099 | dd of=<목적지> bs=1M count=2048 iflag=fullblock

목적지를 셋으로 나눠 각 계층의 비용을 분리했습니다.

3-2. 결과

경로 소요 속도 무엇을 포함하나
① TCP → /dev/null 1.429 s 1.50 GB/s 소켓 수신 + 유저공간 복사만
② TCP → 파일 (페이지캐시) 2.503 s 858 MB/s ① + 페이지캐시 기록
③ TCP → 파일 + fdatasync 14.340 s 150 MB/s ② + 실제 디스크 flush

도출식은 단순합니다:

속도 = 전송량 ÷ 소요시간
① 2,147,483,648 B ÷ 1.42864 s = 1.50 GB/s
② 2,147,483,648 B ÷ 2.50308 s =  858 MB/s
③ 2,147,483,648 B ÷ 14.3397 s =  150 MB/s

계층별 한계비용(앞 단계와의 차이):

페이지캐시 기록 : 2.503 − 1.429 = 1.074 s → 2,147,483,648 ÷ 1.074 = 2.00 GB/s
디스크 flush    : 14.340 − 2.503 = 11.837 s → 2,147,483,648 ÷ 11.837 = 181 MB/s

3-3. 읽는 법

질문이 “TCP 버퍼에서 파일 I/O 버퍼로의 읽기 속도” 였으므로 답은 ②의 858 MB/s 입니다. 여기서 “파일 I/O 버퍼” 는 페이지 캐시를 뜻하고, ddwrite() 가 페이지 캐시에 들어간 시점에 반환하기 때문입니다.

③은 같은 작업에 “디스크에 실제로 앉을 때까지” 를 더한 값입니다. 이 값은 어느 디스크에 쓰느냐로 갈리므로 세 개를 모두 쟀습니다.

3-4. 디스크 3종 비교 — 그리고 예상 밖의 결과

이 서버에는 디스크가 셋 있습니다. 같은 조건(2 GiB, fdatasync)으로 순서를 바꿔가며 2회전 측정했습니다. 앞 테스트의 writeback 이 뒤 테스트를 오염시키지 않도록 매 측정 전 sync 후 3초 대기했습니다.

디스크 마운트 1회전 2회전 편차
Samsung 860 EVO 250 GB /srv/state 332 MB/s 333 MB/s 0.3 %
Samsung 870 EVO 2 TB /data 206 MB/s 207 MB/s 0.5 %
Seagate ST1000DX001 (HDD) /home 162 MB/s 152 MB/s 6.3 %

2 TB SSD 가 250 GB SSD 보다 38 % 느렸습니다. 재현성이 ±1 % 라 측정 오차가 아니었습니다.

원인은 디스크가 아니라 연결된 포트였습니다.

  sda  Samsung SSD 860   ata4   6.0 Gbps   /srv/state
  sdb  ST1000DX001 (HDD) ata1   6.0 Gbps   /home
  sdc  Samsung SSD 870   ata6   3.0 Gbps   /data     ← SATA II

/data 를 담은 2 TB SSD 만 SATA II(3 Gbps) 포트에 꽂혀 있습니다. 링크 상한을 계산하면:

SATA 3.0 Gbps : 8b/10b 인코딩이라 실효 대역 = 3.0 Gbps × 0.8 ÷ 8 = 286 MB/s
SATA 6.0 Gbps : 128b/130b 인코딩이라   = 6.0 Gbps × (128/130) ÷ 8 = 704 MB/s

측정된 207 MB/s 는 SATA II 실효 상한 286 MB/s 의 72 % 로, 링크가 병목입니다. 반면 860 EVO 의 333 MB/s 는 SATA III 상한 704 MB/s 의 47 % 밖에 안 되므로 링크가 아니라 드라이브 자신이 한계입니다.

이게 왜 중요하냐면, /data 에는 k3s 의 모든 PVC(Loki·Prometheus·Tempo·Vault)와 컨테이너 이미지가 들어 있기 때문입니다.

3-5. “6 Gbps 포트로 옮겨라” 는 조치가 성립하지 않았다

처음에는 조치를 “케이블을 6 Gbps 포트로 옮기면 끝” 이라고 적었습니다. 틀렸습니다. 옮길 포트가 없습니다.

컨트롤러가 부팅 때 보고하는 값부터 봅니다.

ahci 0000:00:1f.2: AHCI 0001.0300 32 slots 6 ports 6 Gbps 0x29 impl SATA mode
                                  └ 6포트          └ 컨트롤러는 6 Gbps 지원
                                                      └ 실장 마스크 0x29

0x29 = 0b101001 이므로 실제로 쓸 수 있는 포트는 0·3·5 세 개뿐입니다. 나머지 셋은 커널이 DUMMY 로 잡습니다.

ata1 = 포트 0   6.0 Gbps   sdb  ST1000DX001 (HDD)   /home
ata2         DUMMY
ata3         DUMMY
ata4 = 포트 3   6.0 Gbps   sda  Samsung 860 EVO     /srv/state
ata5         DUMMY
ata6 = 포트 5   3.0 Gbps   sdc  Samsung 870 EVO     /data

6 Gbps 포트 두 개는 이미 둘 다 차 있습니다. 빈 포트로 옮기는 선택지는 존재하지 않았습니다.

그렇다면 포트 5 가 3 Gbps 인 게 소프트웨어 제한은 아닌지 확인해야 합니다. 링크 레지스터를 봅니다.

ata1: SATA link up 6.0 Gbps (SStatus 133 SControl 300)
ata6: SATA link up 3.0 Gbps (SStatus 123 SControl 300)
                                      │        └ SControl[7:4] = 0 → 속도 제한 없음
                                      └ SStatus[11:8] = 2 → 현재 Gen2

SControl 의 속도 제한 필드가 양쪽 다 0 입니다. 커널이 ata6 만 깎은 게 아닙니다. 드라이브 쪽도 확인합니다.

sdc  Samsung SSD 870 EVO 2TB   SATA 3.3, 6.0 Gb/s (current: 3.0 Gb/s)
     UDMA_CRC_Error_Count = 0

드라이브는 6 Gb/s 를 지원하고, CRC 에러가 0 입니다. 케이블이 부실하면 Gen2 로 떨어지면서 CRC 에러가 쌓이는데 그런 흔적이 없습니다. 남는 설명은 하나뿐입니다 — 포트 5 자체가 Gen2 하드웨어입니다. 보드는 ASUS B85M-G 이고, B85 는 6 Gb/s 포트와 3 Gb/s 포트를 섞어 내보내는 칩셋입니다.

그래서 실제 조치는 이동이 아니라 맞교환입니다. 그리고 맞바꿀 상대는 HDD 가 아니라 250 GB SSD 입니다.

처음엔 HDD 와 바꾸는 안을 적었습니다. HDD 가 6 Gbps 포트를 쓰면서 152 MB/s 밖에 못 내니 낭비인 건 맞습니다. 그런데 어느 디스크가 실제로 일하고 있는지를 보면 답이 달라집니다.

포트 0 (6.0 Gbps)  HDD          /home        9.1 GB 사용   백업 보관용
포트 3 (6.0 Gbps)  860 EVO 250G /srv/state   532 MB 사용   ← 6 Gbps 를 놀리는 중
포트 5 (3.0 Gbps)  870 EVO 2TB  /data         64 GB 사용   ← 일은 여기서 다 한다

/data 에는 k3s 의 PVC 전부(Loki 34 GB · Prometheus 1.5 GB · Vault)와 컨테이너 이미지 28 GB 가 들어 있습니다. 반면 /srv/state532 MB 로 사실상 비어 있습니다. 120 배 더 많은 데이터와 관측 스택의 쓰기 부하 전부를 짊어진 디스크가 느린 포트에 물려 있고, 거의 놀고 있는 디스크가 빠른 포트를 쓰고 있습니다.

그러면 870 EVO 를 6 Gbps 로 올리는 방법은 둘입니다. 어느 쪽이든 누군가는 3 Gbps 포트로 내려가야 합니다.

방안 3 Gbps 로 내려가는 쪽 그쪽이 잃는 것 케이스에서 만지는 것
A. HDD ↔ 870 EVO HDD (152 MB/s) 없음 — 152 ≪ SATA II 상한 286 부팅 디스크
B. 860 EVO ↔ 870 EVO 860 EVO (333 MB/s) 333 → 286 MB/s 약 14 % 손실 SSD 둘만

정직하게 적자면 A 가 성능상으로는 무손실입니다. HDD 는 SATA II 상한에 닿지도 않으니까요. 반면 B 는 860 EVO 가 333 MB/s 에서 SATA II 실효 상한 286 MB/s 에 걸립니다.

그럼에도 B 를 택했습니다. 이유는 둘입니다.

첫째, 손실이 걸리는 곳이 거의 안 쓰이는 디스크입니다. 860 EVO 가 지고 있는 /srv/state 는 532 MB 이고, 그중 실제 쓰기가 일어나는 건 PostgreSQL 데이터 디렉터리(466 MB)뿐입니다. 이 DB 는 앞 절에서 보듯 캐시 적중률이 99.97 % 라 디스크를 거의 때리지 않습니다. 상한이 14 % 깎여도 닿을 일이 없는 상한입니다.

둘째, A 는 부팅 디스크의 케이블을 뽑아야 합니다. HDD 에 /, /var, /home, /boot/efi 가 전부 올라가 있습니다. 얻는 게 “이미 안 쓰는 상한 14 %” 인데 걸어야 하는 건 부팅 가능성이라면, 교환 비율이 맞지 않습니다. B 는 부팅에 관여하지 않는 디스크 둘만 건드립니다.

교환 후  포트 0 (6.0 Gbps) ── HDD          그대로. 손 안 댐 (부팅 디스크)
         포트 3 (6.0 Gbps) ── 870 EVO 2TB  링크 상한 286 → 704 MB/s
         포트 5 (3.0 Gbps) ── 860 EVO 250G 상한 286 MB/s — 닿지 않는 상한

“빈 포트로 옮기면 되지 않나” 는 왜 안 되는가. ata2·3·5 가 비어 보이지만 커널은 이걸 DUMMY 로 잡습니다. 실장 마스크 0x29 에 없다는 뜻이고, 비어 있는 포트가 아니라 애초에 배선되지 않은 포트입니다. 커넥터 자체가 없으니 속도를 따질 일도 없습니다.

이 교체에 데이터 이동이 필요한가 — 아니었습니다

“큰 걸 메인으로” 라고 하면 데이터를 옮겨야 할 것 같지만, 확인해 보니 옮길 게 없습니다. 이미 큰 쪽이 메인 역할을 하고 있고, 잘못된 건 포트뿐입니다.

케이블만 바꿔도 되는지는 장치명에 의존하는 설정이 있는지로 갈립니다. 셋 다 확인했습니다.

/etc/fstab        전부 UUID=...          (/dev/sd* 는 설치 당시 주석에만 있음)
k8s PersistentVolume  전부 경로 기반      (/srv/state/postgres, /data/k3s/storage/...)
EFI 부트 엔트리    GPT 파티션 UUID 참조    (부팅은 HDD, SSD 는 무관)

하드코딩된 장치명이 한 군데도 없습니다. 교체 후 sdasdc 가 뒤바뀌어도 마운트는 UUID 를 따라가고, PV 는 마운트된 경로를 보므로 아무것도 모릅니다. 즉 데이터 이동도 설정 변경도 필요 없는 순수 물리 작업입니다.

그래도 백업은 뜬다 — 다만 64 GB 를 뜨지는 않는다

물리 작업이라 케이블·정전기·오연결 사고가 남습니다. 그래서 백업은 뜨되, 무엇이 되살릴 수 없는지부터 갈랐습니다.

되살릴 수 없음 크기 되살릴 수 있음 크기
/data/k3s/server (클러스터 자체) 79 MB Loki PVC 34 GB
Vault raft + 감사 로그 58 MB 컨테이너 이미지 28 GB
/srv/state (Postgres) 532 MB Prometheus PVC 1.5 GB
합계 669 MB 합계 63.5 GB

Loki 와 Prometheus 는 보존이 72 시간이라 어차피 사흘이면 사라질 데이터고, 이미지는 다시 받으면 됩니다. 64 GB 중 실제로 지켜야 할 것은 669 MB, 1 % 입니다.

백업할 때 k3s 를 먼저 멈추는 이유가 하나 있습니다. k3s 의 데이터스토어가 WAL 모드 SQLite 입니다.

state.db  51 MB    state.db-wal  30 MB    state.db-shm  64 KB

돌아가는 중에 state.db 만 복사하면 WAL 에 있는 30 MB 를 통째로 놓칩니다. 세 파일을 다 복사해도 그 사이 쓰기가 들어가면 어긋납니다. Vault 의 raft(BoltDB)도 같은 문제고, raft 스냅샷을 뜨려면 토큰이 필요합니다. 어차피 전원을 내려야 하는 작업이라 멈추는 비용은 이미 치르고 있습니다.

그런데 “멈췄다” 를 확인하지 않아서 한 번 틀렸습니다

처음 백업 스크립트에는 이렇게 적었습니다.

systemctl stop k3s 를 하면 Vault 파드도 같이 멈추니, 토큰 없이 일관된 사본을 얻는다.

그대로 돌렸고, 스크립트는 성공했다고 보고했습니다. 틀렸습니다.

$ systemctl cat k3s | grep -E 'KillMode|Delegate'
KillMode=process
Delegate=yes

k3s 유닛은 설계상 서비스 프로세스만 죽이고 containerd 와 워크로드 컨테이너는 계속 돌립니다. 백업이 끝난 뒤 확인해 보니 이랬습니다.

vault-0   restartCount: 0   startedAt: 2026-08-03T15:51:05Z   ← 사흘 전. 안 멈췄다
          Sealed: false                                        ← 봉인도 안 됐다

살아서 쓰고 있는 raft.db 를 복사하고 있었습니다.

원인이 하나 더 있었습니다. 정지를 기다리는 루프를 이렇게 썼는데,

for i in $(seq 1 30); do
  pgrep -x containerd-shim || pgrep -f 'containerd-shim-runc' || break
  sleep 2
done

shim 은 언제나 떠 있으므로 이 조건은 break 에 도달할 수가 없습니다. 30번을 다 돌고 60초 뒤에 그냥 다음 줄로 넘어갔습니다. 확인이 아니라 타임아웃이었고, 로그에는 정지됨 이라고 찍혔습니다. 조용히 실패하는 검증은 검증이 아닙니다.

고친 방식은 둘입니다. 순서를 바꿔 Vault 를 replicas=0 으로 먼저 내리고(API 가 살아 있어야 하므로 k3s 보다 먼저입니다) 그 다음 k3s 를 멈춥니다. 그리고 두 정지 모두 프로세스 이름을 넘겨짚는 대신 직접 확인합니다 — 파드가 사라졌는지, state.db 를 연 파일 디스크립터가 0 인지. 확인에 실패하면 복사하지 않고 죽습니다.

고친 뒤 다시 뜨고 두 사본을 비교했습니다.

1차 (핫 카피)  raft.db sha256 = 3be34ef4788c8115...
2차 (콜드 카피) raft.db sha256 = 2dc688281d7221a4...   ← 다르다
vault-0  startedAt: 14:31:54Z (백업 종료 14:31:52 직후)  Sealed: true

해시가 다릅니다. 1차가 살아 있는 파일을 떴다는 증거이고, 2차는 파드가 실제로 재기동됐다는(=멈췄다는) 증거입니다.

이 글 전체가 “식과 실측을 양쪽 다” 라고 말해놓고, 정작 백업 스크립트에서는 멈췄을 것이라는 가정을 확인하지 않았습니다. 3-5 절에서 "3 Gbps 포트가 원인""6 Gbps 포트로 옮기면 된다" 를 구분해야 했던 것과 정확히 같은 실수를, 같은 문서 안에서 한 번 더 했습니다.

### ⚠️ 이 작업의 진짜 위험은 백업이 아니다 Vault 가 shamir 봉인(5 분할 / 임계 3)이고 auto-unseal 이 없습니다. 전원을 내렸다 올리면 Vault 는 봉인된 채로 뜹니다. 데이터가 아무리 멀쩡해도 unseal 키 3 개가 없으면 열 수 없습니다. 백업을 아무리 잘 떠도 이건 해결되지 않습니다. 케이블을 뽑기 전에 키부터 확인해야 합니다. 재현 스크립트가 이걸 맨 처음 물어보게 해둔 이유입니다.

BIOS 에서 꺼둔 포트가 있다면 실장 마스크가 0x29 보다 커질 수 있습니다. 그 경우 맞교환 없이 870 EVO 만 빈 6 Gbps 포트로 옮기면 되므로, BIOS 의 SATA 포트 설정을 먼저 보는 편이 낫습니다.

3-6. 재는 김에 나온 것 — 870 EVO 의 재할당 섹터

링크 속도를 확인하려고 SMART 를 열었다가 같이 보였습니다.

Samsung SSD 870 EVO 2TB
  Reallocated_Sector_Ct    393    (정규값 83 / 임계 10)
  Runtime_Bad_Block        393
  Wear_Leveling_Count      519    (정규값 78)
  Power_On_Hours        31,628    (약 3.6 년)
  Uncorrectable_Error_Cnt    0
  overall-health          PASSED

임계값(10)에는 한참 여유가 있고 정정 불가 에러도 0 이라 당장 조치할 상태는 아닙니다. 다만 이 디스크에 k3s 의 PVC 가 전부 올라가 있으므로, 재할당 섹터 수를 주기적으로 보는 편이 낫습니다. 관측 스택을 굴리면서 정작 그 스택이 앉은 디스크는 안 보고 있었다는 뜻이기도 합니다.

측정하며 배운 것: 처음 한 번만 재고 끝냈다면 “2 TB SSD 가 느리다” 는 결론으로 갔을 겁니다. 순서를 뒤집어 재현성을 확인하고, 그래도 차이가 남자 디스크가 아니라 경로를 의심한 것이 답으로 이어졌습니다. 그리고 원인을 찾은 뒤에도 한 번 더 확인해야 했습니다 — “3 Gbps 포트가 원인” 과 “6 Gbps 포트로 옮기면 된다” 는 다른 문장입니다. 앞은 측정이고 뒤는 가정이었는데, 실장 포트를 세어보기 전까지 그 차이를 몰랐습니다.

② 값에 대한 주의: 858 MB/s 는 페이지 캐시가 깨끗한 상태에서 잰 값입니다. 2 GiB 쓰기를 연달아 반복하면 더티 페이지가 dirty_ratio 에 걸려 커널이 쓰기를 조절하므로 200 MB/s 대까지 떨어집니다. 이 항목을 잴 때는 직전 테스트의 writeback 이 끝났는지 반드시 확인해야 합니다.


4. 네트워크 수신 속도

4-1. 이론 최대치 도출

회선                = 100 Mbps (실측 /sys/class/net/enp3s0/speed, full duplex)
전선 위 프레임 총길이 = MTU 1500 + 이더넷 헤더·FCS 18 + 프리앰블·IFG 20 = 1538 B
TCP 페이로드         = 1500 − IP 20 − TCP 20 − 옵션 12 = 1448 B

회선 효율 = 1448 ÷ 1538 = 94.15 %
이론 최대 = 100 Mbps × 0.9415 = 94.15 Mbps = 11.22 MB/s

18 B 는 목적지·출발지 MAC(12) + 타입(2) + FCS(4), 20 B 는 프리앰블/SFD(8) + 프레임 간격(12)입니다. 전선 위에서 실제로 소비되지만 페이로드가 아닌 부분이라 효율 계산에 들어갑니다.

4-2. 실측

Cloudflare 속도 측정 엔드포인트에서 50 MB 를 3회 받았습니다.

1회차: 10.98 MB/s (4.3 s)
2회차: 10.98 MB/s (4.3 s)
3회차: 11.00 MB/s (4.3 s)
달성률 = 11.00 ÷ 11.22 = 98.0 %

회선이 포화됐습니다. 남은 2 %는 TLS 레코드 오버헤드와 HTTP 프레이밍입니다.

4-3. 측정이 알려주는 것 — 병목은 어디에나 있을 수 있다

교차검증으로 다른 소스(OVH)에서도 받아봤는데 1.0 Mbps 가 나왔습니다. 회선이 느려진 게 아니라 상대 서버가 느렸던 것입니다.

이게 처리량 측정의 함정입니다. 처리량 측정은 언제나 경로 전체에서 가장 좁은 곳을 재고, 그 좁은 곳이 내 회선이라는 보장이 없습니다. Cloudflare 측정이 유효했던 이유는 결과가 이론 최대치의 98 %라서 “내 회선이 병목” 임이 숫자로 증명됐기 때문입니다. 1.0 Mbps 결과는 내 서버에 대해 아무것도 알려주지 않습니다.

측정값 하나만 놓고 “우리 회선이 느리다” 고 결론 내리면 안 되는 이유입니다.


5. 종합 — 병목은 네트워크다

계층 속도 상대 배수
TCP → /dev/null 1,500 MB/s 136×
TCP → 파일 버퍼 858 MB/s 78×
TCP → 디스크 (SSD, SATA III) 333 MB/s 30×
TCP → 디스크 (SSD, SATA II) 207 MB/s 19×
TCP → 디스크 (HDD) 152 MB/s 14×
네트워크 수신 11.0 MB/s

소켓에서 파일 버퍼로 옮기는 경로가 네트워크보다 78배 빠릅니다. 네트워크로 들어온 데이터를 파일에 쓰는 작업에서 파일 버퍼 경로는 병목이 될 수 없습니다. 가장 느린 HDD 에 fsync 를 걸어도 여전히 14배 빠릅니다.

바꿔 말하면, 이 서버에서 네트워크 수신 성능을 개선하려면 100 Mbps NIC 부터 손봐야 하고, 버퍼 튜닝이나 디스크 교체로는 얻을 게 없습니다. 수신 윈도우도 자동 튜닝이 이미 BDP 를 따라잡아 놓았으니 손댈 이유가 없습니다.


부록 — 재현 명령

# 커널 파라미터
cat /proc/sys/net/ipv4/tcp_rmem /proc/sys/net/ipv4/tcp_adv_win_scale \
    /proc/sys/net/ipv4/tcp_window_scaling /proc/sys/net/ipv4/tcp_moderate_rcvbuf

# 실제 소켓의 윈도우·MSS
ss -tim state established '( dport = :443 )'
#   확인할 필드: mss, advmss, pmtu, wscale, rcv_ssthresh, rb(=skmem 두 번째)

# MTU / NIC 속도
cat /sys/class/net/enp3s0/mtu /sys/class/net/enp3s0/speed

# TCP → 파일버퍼 (터미널 창 두 개 필요)
nc -l -p 9099 < /dev/zero
nc 127.0.0.1 9099 | dd of=/path/file bs=1M count=2048 iflag=fullblock
nc 127.0.0.1 9099 | dd of=/path/file bs=1M count=2048 iflag=fullblock conv=fdatasync

# 네트워크 수신 (같은 소스로 3회 이상, 이론치와 비교할 것)
curl -o /dev/null -w '%{speed_download}\n' 'https://speed.cloudflare.com/__down?bytes=50000000'

# SATA 링크 속도 — "느린 디스크" 를 의심하기 전에 먼저 볼 것
cat /sys/class/ata_link/link*/sata_spd            # 협상된 링크 속도
journalctl -k -b | grep -E 'ahci .* impl|SATA link up'
#   AHCI 줄의 '0x.. impl' 이 실장 포트 마스크. 빈 포트가 있는지 여기서 판단한다.
#   'SATA link up' 의 SControl[7:4] 가 0 이면 소프트웨어 속도 제한은 없다는 뜻.

smartctl -i /dev/sdX | grep 'SATA Version'        # 드라이브가 지원/현재 쓰는 속도
smartctl -A /dev/sdX | grep -E 'CRC_Error|Reallocated'
#   CRC 에러가 0 인데 Gen2 로 붙어 있으면 케이블이 아니라 포트가 원인이다.

# 케이블을 뽑기 전에: 장치명에 의존하는 설정이 있는지
grep -v '^#' /etc/fstab                          # UUID= 인가 /dev/sdX 인가
kubectl get pv -o custom-columns=NAME:.metadata.name,PATH:.spec.local.path
efibootmgr -v                                    # 부팅이 어느 디스크에 걸려 있나
#   셋 다 장치명을 안 쓰면 포트 교체에 설정 변경이 필요 없다.

포트 교체 절차

# 1) 백업 (k3s 를 멈춰 SQLite WAL·Vault raft 를 일관되게 뜬다)
sudo ./scripts/ssd-swap-backup.sh          # k3s-migration 저장소

# 2) 전원 차단 후 860 EVO ↔ 870 EVO 케이블 교환. HDD 는 건드리지 않는다.

# 3) 부팅 후 확인
cat /sys/class/ata_link/link*/sata_spd      # 870 이 6.0 Gbps 인가
lsblk -o NAME,SIZE,MODEL,MOUNTPOINT         # 마운트가 제자리인가 (UUID 라 따라온다)

# 4) Vault 봉인 해제 — 키 3/5 가 필요하다
kubectl -n vault exec -it vault-0 -- vault operator unseal   # 3회

# 5) 재측정 — 207 MB/s 에서 400 MB/s 대로 올라가야 성공
nc -l -p 9099 < /dev/zero &
nc 127.0.0.1 9099 | dd of=/data/bench/f bs=1M count=2048 iflag=fullblock conv=fdatasync

참고


이어서 읽기

이 측정은 같은 홈서버 위에서 했다. 그 서버를 어떻게 세웠고 무엇을 보고 있는지는 아래 두 글에 있다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다