한 줄 요약: 홈서버의 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 버퍼” 는 페이지 캐시를 뜻하고, dd 는 write() 가 페이지 캐시에 들어간 시점에 반환하기 때문입니다.
③은 같은 작업에 “디스크에 실제로 앉을 때까지” 를 더한 값입니다. 이 값은 어느 디스크에 쓰느냐로 갈리므로 세 개를 모두 쟀습니다.
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/state 는 532 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 는 무관)
하드코딩된 장치명이 한 군데도 없습니다. 교체 후 sda 와 sdc 가 뒤바뀌어도 마운트는 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 | 1× |
소켓에서 파일 버퍼로 옮기는 경로가 네트워크보다 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
참고
- RFC 9293 — Transmission Control Protocol (TCP) · 현행 TCP 정본. 헤더의 Window 필드와 SYN 의 MSS 옵션, 그리고 이 글이 ‘광고’로 옮긴 advertise 용법이 여기 있다.
- RFC 7323 — TCP Extensions for High Performance · 1-3 의 윈도우 스케일 옵션과, 2-1 에서 MSS 를 12 바이트 깎은 타임스탬프 옵션의 근거.
- Linux Kernel — IP Sysctl ·
tcp_rmem·tcp_window_scaling·tcp_adv_win_scale·tcp_moderate_rcvbuf의 정의. 1-1 에서 읽은 값이 각각 무엇을 정하는지. - Linux Kernel — Documentation for /proc/sys/net/ ·
net.core.rmem_max는 위 IP Sysctl 문서가 아니라 이쪽에 있다.
이어서 읽기
이 측정은 같은 홈서버 위에서 했다. 그 서버를 어떻게 세웠고 무엇을 보고 있는지는 아래 두 글에 있다.
답글 남기기