터널 위의 대화형 셸이 파일 복사가 도는 동안 몇 초간 얼어붙었다가 아무 일도 없었다는 듯이 이어진다. 문제는 다운로드에만 있는데도 셸도 함께 기다리고, 둘은 같은 순간에 회복한다. 그 패턴은 많은 대화를 실어 나르는 하나의 신뢰성 있는 스트림의 서명이며, 사람들이 먼저 시도하는 거의 모든 튜닝 변경을 견딘다.

증상은 설정이 아니라 메커니즘을 가리키므로 주의 깊게 읽을 가치가 있다: 멈춤이 공유되니 원인도 공유되고, 공유되는 대상은 바깥 연결이다. 그동안 로그의 나머지는 정상으로 보인다.

증상: 모든 것이 한꺼번에 멈춘다

안쪽 연결 둘이 함께 막히는 것이 단서다. 느린 다운로드와 느린 셸은 서로 다른 순서 번호와 서로 다른 혼잡 상태를 가진 별개의 TCP 대화이므로, 한쪽의 결함이 다른 쪽을 건드려서는 안 된다. 둘이 같은 박자로 멈춘다면 그 위의 무언가가 그들의 바이트를 정렬하고 있다.

그 무언가는 터널의 전송 계층이다. TCP 모드에서 OpenVPN은 캡슐화된 패킷을 피어로 가는 하나의 TCP 연결에 쓰고, TCP의 계약은 단일 바이트 스트림의 순서 보장 전달이다. 뒤따르는 바이트는 어느 안쪽 대화에 속하든 빠진 바이트보다 먼저 애플리케이션에 전달되지 않는다.

증상이 무엇을 말하지 않는지 보라. 경로가 고장 났다거나, 서버가 과부하 상태라거나, 암호화가 느리다고 말하지 않는다. 그것들은 다른 형태를 만든다: 고르게 낮은 처리량, 시간 초과, 또는 한 방향에만 영향을 주는 실패다. 잃어버린 세그먼트가 재전송되면서 끝나는 공유 멈춤은 분명히 순서 효과다.

하나의 바깥 바이트 스트림, 많은 안쪽 대화

터널이 다운로드와 셸을 동시에 실어 나른다고 상상해 보라. 바깥 연결의 세그먼트 하나가 사라진다. 다음 세그먼트들은 수신 호스트에 도착할 수 있고 수신 측 TCP 스택은 이를 확인 응답하지만, 빈틈이 메워질 때까지 위로 전달할 수는 없다. 따라서 OpenVPN은 셸을 위해 읽을 것이 없으며, 셸 자체의 바이트는 오래전에 도착했는데도 그렇다.

이것은 하나의 연결 안에서 일어나는 큐 선두 차단이며, 두 애플리케이션이 안쪽 연결을 공유할 것을 요구하지 않는다. 그들은 바깥 연결을 공유하며, 그것으로 충분하다.

이차 효과가 상황을 더 나쁘게 만든다. 확인 응답을 보지 못하는 안쪽 송신자는 바깥 계층이 이미 전달하려는 데이터를 다시 보낼 수 있다. 그 재전송은 세그먼트를 잃을 수 있음을 방금 증명한 경로 위에서 추가 터널 트래픽이 되므로, 복구 작업이 정작 중요한 복구와 경쟁한다.

이 중 어느 것도 붕괴를 보장하지 않는다. 왕복 시간이 짧고 큐가 넉넉한 깨끗한 경로에서는 이런 배치가 수년간 충분히 쓸 만할 수 있다. 손실, 지연, 큐 깊이가 동시에 겹칠 때 고장이 나타나며, 그래서 보고가 잘 갖춰진 사무실이 아니라 혼잡한 업링크와 모바일 링크 주변에 몰린다.

더 긴 내부 타이머가 도울 수 없는 이유

경로에서 rto_min으로 최소 재전송 타임아웃을 올리면 그 경로를 쓰는 로컬 생성 TCP 트래픽의 타이머 기반 재전송이 미뤄진다. 그것은 실제 효과가 있는 실제 손잡이이며, 첫 번째 문제 대신 이차 문제를 다룬다.

ip route show
ip route show cache 2>/dev/null | head
ss -tin dst 10.70.0.1

할 수 없는 것은 바깥 바이트를 순서 밖으로 풀어 주거나, 빠진 세그먼트를 더 빨리 복구하거나, 원격 애플리케이션 서버가 같은 타이머를 쓰게 만드는 일이다. 수신 스택은 여전히 순서대로 전달하고, 먼 쪽 송신자는 여전히 자기 왕복 시간 측정을 쓴다.

또한 다른 모든 손실 복구 메커니즘을 끄지도 않는다. 실제 손실에 타이머 기반 복구가 필요할 때 더 큰 최소값은 애플리케이션이 행동하기 전에 더 오래 기다리게 하며, 이는 지연에 민감한 세션이 필요로 하는 것과 정반대다.

고정된 열두 초 하한이 TCP 터널이 존재한다는 것만으로 당연한 듯이 이 주제의 조언에 등장한다. RFC 6298에 설명된 재전송 타이머는 측정된 왕복 시간과 지수적 백오프에서 유도되므로, 고정 하한은 특정 경로를 위한 정책 선택이지 프로토콜 요구 사항이 아니다. 넓게 적용하면 증상을 감추는 동시에 그 경로가 포함하는 모든 흐름의 복구를 바꾼다.

tcp-nodelay가 하는 일과 하지 않는 일

전송 소켓에서 Nagle 방식 버퍼링을 끄는 터널 옵션은 목적이 좁다: 작은 쓰기가 전송 전에 더 많은 데이터를 기다리지 않게 막는다. 이는 상호작용 트래픽이 그 밖에는 건강한 경로에서 추가 지연을 피하도록 돕는다.

전송을 무순서로 만들지 않으며, 세그먼트를 잃은 뒤의 복구를 줄이지도 않는다. 재전송 타임아웃으로 멈추는 셸은 옵션이 순서 규칙을 건드린 적이 없으므로 그 옵션이 켜져 있어도 계속 멈춘다.

둘 다 지연 해결책으로 설명되기 때문에 혼동하기 쉽다. 하나는 버퍼링이 만든 인위적 대기를 없애고, 다른 하나는 전송이 보장하는 바를 바꿔야 하며 어떤 소켓 옵션도 그렇게 하지 않는다.

전송 계층을 공정하게 비교하는 방법

운영자가 허용하고 두 서비스가 모두 있다면, 같은 작업 부하를 실어 나르는 UDP 서비스와 TCP 서비스를 같은 서버로 가는 같은 접속 경로에서 비교하라. 두 테스트 사이에 국가, 제공자 또는 서버 세대를 바꾸면 결과를 지배할 변수를 끌어들인다.

처리량 숫자가 아니라 공유 멈춤을 보라. 바깥 손실 동안 여러 안쪽 애플리케이션이 함께 막히는 것이 큐 선두 차단을 식별하는 관찰이며, 속도 테스트 숫자 하나가 낮은 것은 그렇지 않다.

터널 인터페이스와 물리 인터페이스에서의 승인된 캡처는 안쪽과 바깥 재전송이 서로 다른 순서 공간을 가진 서로 다른 연결에 속하므로 두 계층을 깔끔하게 분리한다. 바깥 스트림이 빈틈을 보이는 동안 안쪽 재전송이 보이는 것이 직접 증거다.

UDP로 바꾸면 추가 신뢰성 스트림이 사라지고 안쪽 TCP가 스스로 복구한다. 무손실이나 낮은 지연을 약속하지 않으며, 포화된 업링크, 지나치게 큰 큐, MTU 문제는 여전히 고쳐야 한다. 터널 밖 트래픽은 터널의 순서 빈틈 뒤에 결코 붙잡히지 않지만, 두 경로가 하나의 혼잡한 링크를 공유할 때는 여전히 영향을 받을 수 있고, 그 공유 병목은 여기에 접어 넣는 대신 따로 조사할 가치가 있다.

관련 글