큐빅(CUBIC) 버그로 인한 큐익(QUIC) 성능 저하, 한 줄 코드 수정으로 해결!

by DD
3주 전
조회수 14

리눅스 큐빅(CUBIC) 혼잡 제어 알고리즘의 최적화 과정에서 발생한 버그가 퀴치(quiche)의 큐익(QUIC) 구현에 영향을 미쳐 성능 저하(Performance Degradation)를 유발함

큐빅(CUBIC)의 혼잡 윈도우(Congestion Window)가 최소값에 고정되어 데이터 전송이 불가능해지는 치명적인 버그(Critical Bug) 발생

문제 해결을 위해 유휴 시간(Idle Time) 측정 방식을 개선하여 큐빅(CUBIC)의 성능 회복(Performance Recovery)을 달성함

10ms RTT 환경에서 30% 패킷 손실 발생 시, 60% 테스트 실패율을 보였으나, 수정 후 100% 테스트 통과(Test Pass)를 기록

큐빅(CUBIC) 알고리즘의 동작 원리

큐빅(CUBIC)은 손실 기반 혼잡 제어 알고리즘으로, 네트워크의 가용 대역폭을 최대화하는 것을 목표로 한다. 혼잡 윈도우(Congestion Window, cwnd)를 조절하여 데이터 전송 속도를 제어하며, 손실이 없으면 cwnd를 증가시키고, 손실이 발생하면 cwnd를 감소시킨다.

cwnd 증가: 손실이 없는 경우, 큐빅(CUBIC)은 cwnd를 증가시켜 전송률을 높인다.

cwnd 감소: 손실이 감지되면, 네트워크 혼잡을 의미하므로 cwnd를 줄여 전송률을 낮춘다.

epoch_start: 큐빅(CUBIC)은 epoch_start를 기준으로 cwnd의 성장 곡선을 계산하며, 유휴 상태 이후 cwnd가 과도하게 증가하는 문제를 해결하기 위해 epoch_start를 조정한다. 이러한 메커니즘을 통해 네트워크의 가용 대역폭(Available Bandwidth)을 효율적으로 활용한다.

버그 발생 원인: 유휴 시간 측정의 문제

본문에서 설명하는 버그는 큐빅(CUBIC) 알고리즘의 유휴 시간 측정 방식에서 비롯되었다. 큐익(QUIC) 구현에서 on_packet_sent() 함수 내에서 bytes_in_flight가 0인 경우, 연결이 유휴 상태로 간주하고, congestion_recovery_start_time을 조정한다.

잘못된 유휴 시간 계산: cwnd가 최소값일 때, last_sent_time을 기준으로 유휴 시간을 계산하여 실제보다 과도하게 긴 유휴 시간을 측정한다.

RTO(Retransmission Timeout) 문제: RTT(Round Trip Time)와 유사한 14ms의 델타(delta)가 유휴 시간으로 계산되어, congestion_recovery_start_time을 미래로 이동시킨다.

무한 루프(Infinite Loop): 이로 인해 큐빅(CUBIC)은 회복 상태와 혼잡 회피 상태를 반복하며, cwnd가 최소값에 고정되는 데스 스파이럴(Death Spiral)에 빠진다. 이러한 문제는 큐익(QUIC)의 성능 저하를 야기한다.

문제 해결: last_ack_time 활용

버그를 해결하기 위해, 유휴 시간 측정 방식을 개선하여 last_ack_time을 활용하도록 수정했다. bytes_in_flight가 0이 되는 시점을 last_sent_time이 아닌 last_ack_time으로 변경하여, 실제 유휴 시간을 정확하게 측정하도록 했다.

last_ack_time 추가: 큐빅(CUBIC) 상태에 last_ack_time을 추가하여, ACK(Acknowledgement)가 도착한 시간을 기록한다.

유휴 시간 계산 변경: on_packet_sent() 함수에서 유휴 시간을 계산할 때, last_sent_time 대신 last_ack_time을 사용하여, 실제 유휴 시간을 정확하게 측정한다.

성능 회복: 이 수정으로 인해, 큐빅(CUBIC)은 최소 cwnd 상태에서도 정상적으로 동작하며, 100% 테스트 통과율을 달성했다. 이로써 큐익(QUIC)의 성능 안정성(Performance Stability)을 확보했다.

Reno와의 비교: 큐빅(CUBIC)의 특이점

본문에서는 큐빅(CUBIC)과 또 다른 손실 기반 혼잡 제어 알고리즘인 리노(Reno)를 비교하여, 큐빅(CUBIC)의 버그가 특정 상황에서만 발생하는 이유를 설명한다. 리노(Reno)는 큐빅(CUBIC)과 달리, 유휴 시간 조정과 관련된 문제가 발생하지 않았다.

성장률 차이: 큐빅(CUBIC)은 cwnd의 성장률이 더 공격적이며, 유휴 시간 이후 cwnd를 빠르게 증가시키려는 경향이 있다.

Reno의 안정성: 리노(Reno)는 큐빅(CUBIC)보다 완만한 성장 곡선을 가지므로, 유휴 시간 조정에 따른 문제의 영향을 덜 받는다.

특정 조건: 큐빅(CUBIC)의 버그는 cwnd가 최소값에 고정되고, 모든 ACK가 bytes_in_flight를 0으로 만드는 특정 조건에서만 발생한다. 이러한 조건은 큐빅(CUBIC)의 성능 저하(Performance Degradation)를 유발한다.

실제 프로덕션 환경에서의 시사점

본 사례는 혼잡 제어 알고리즘의 미세한 조정이 시스템 전체의 성능에 큰 영향을 미칠 수 있음을 보여준다. 특히, 큐익(QUIC)과 같이 복잡한 프로토콜을 구현할 때는, 엣지 케이스(Edge Case)에 대한 철저한 테스트와 검증이 필요하다는 점을 강조한다.

테스트 중요성: 최소 cwnd와 같은 특수한 상황에서의 동작을 테스트하는 것이 중요하며, 이를 통해 예상치 못한 버그를 사전에 발견할 수 있다.

모니터링 강화: 분산 시스템 환경에서는 관측 가능성(Observability)을 확보하여, 문제 발생 시 신속하게 원인을 파악하고 대응할 수 있도록 해야 한다.

지속적인 개선: 오픈소스 프로젝트에 기여하고, 성능 개선을 위한 노력을 지속하는 것이 중요하다. 큐익(QUIC)의 성능 최적화(Performance Optimization)를 위해서는 지속적인 연구와 개선이 필요하다.

When "idle" isn't idle: how a Linux kernel optimization became a QUIC bug