LabHub
배우기 러닝패스 코스

연결 하나가 느려지자 나머지가 전부 멈췄다 · 말 없는 연결을 언제 끊나 · 이론

말 없는 연결을 언제 끊나

LabHub 에서 이어서 보기

한 줄 요약

조용한 연결과 죽은 연결은 소켓만 봐서는 구분되지 않으므로, 마지막 활동 시각을 프로그램이 직접 적어 두어야 합니다.

왜 이게 필요했나

다중화 서버는 연결을 오래 들고 있도록 만들어져 있습니다. 그것이 장점이자 문제입니다. 손님이 요청을 보내고 답을 받은 뒤 아무 말도 하지 않으면, 서버는 그 연결을 계속 지켜봅니다. 파일 서술자 하나, 수신·송신 버퍼, 그리고 연결마다 프로그램이 만든 상태가 그대로 남습니다. 이런 연결이 쌓이면 어느 순간 새 연결을 받지 못하게 되는데, 그때 보이는 오류는 대개 자원 부족이라 원인이 한참 떨어진 곳을 가리킵니다.

더 나쁜 경우는 상대가 이미 사라진 경우입니다. 노트북 뚜껑을 닫거나 중간의 장비가 상태를 잊으면, 상대는 FIN 도 RST 도 보내지 못합니다. 서버 쪽 소켓은 그대로 열려 있고, 읽기 준비는 영원히 오지 않습니다. 이 상태를 half-open 이라 부릅니다. 소켓의 상태만 조회해서는 조용한 것인지 죽은 것인지 알 수 없습니다.

어떻게 동작하나

해법은 소켓에 묻지 말고 시간을 기록하는 것입니다. 연결마다 마지막으로 의미 있는 일이 일어난 시각을 적어 두고, 주기적으로 지금 시각과 비교해 오래된 것을 끊습니다.

conn.last_active = clock()                 # 읽거나 보낸 뒤에 갱신stale = [key for key, conn in conns.items()         if now - conn.last_active >= idle_timeout]

세 가지를 정해야 합니다. 무엇을 활동으로 셀 것인가, 얼마나 자주 검사할 것인가, 그리고 시계를 어느 것으로 쓸 것인가입니다.

활동의 정의는 서비스마다 다릅니다. 바이트를 받은 것만 활동으로 볼 수도 있고, 응답을 보낸 것까지 포함할 수도 있습니다. 중요한 것은 정의를 한 곳에 모아 두는 것입니다. 여러 곳에서 제각각 시각을 갱신하면, 왜 이 연결이 안 끊겼는지 나중에 설명할 수 없게 됩니다.

검사 주기는 다중화 호출의 대기 시간과 묶입니다. 이벤트가 하나도 없으면 프로그램은 select 안에서 자고 있으므로, 아무리 정확한 만료 목록을 만들어도 깨어나지 않으면 아무도 끊기지 않습니다. 그래서 대기 시간에 상한을 둡니다. 이벤트가 없어도 주기적으로 깨어나 한 바퀴 훑도록 하는 것입니다.

시계는 경과 시간을 재는 용도이므로 벽시계가 아니라 단조 시계를 씁니다. 시각 동기화로 벽시계가 뒤로 조정되면 아직 만료되지 않은 연결이 만료된 것으로 보이거나, 반대로 영원히 만료되지 않을 수 있습니다. 검사를 위해 시계를 주입할 수 있게 만들어 두면, 몇 초를 실제로 기다리지 않고도 만료 판단을 시험할 수 있습니다.

커널에도 비슷한 장치가 있습니다. [tcp(7)](https://man7.org/linux/man-pages/man7/tcp.7.html) 과 [socket(7)](https://man7.org/linux/man-pages/man7/socket.7.html) 이 설명하는 TCP keepalive 는 오래 조용한 연결에 빈 조사를 보내 상대가 살아 있는지 확인합니다. 다만 이것은 전송 계층의 생존 확인이지 애플리케이션의 유휴 정책이 아닙니다. 상대 프로세스가 멈춰 있어도 커널은 응답할 수 있으므로 keepalive 는 통과하고, 그래도 업무는 진행되지 않습니다. 두 장치는 서로를 대신하지 않습니다.

끊을 때는 순서가 있습니다. 관심 목록에서 빼고, 프로그램이 들고 있던 연결별 상태를 지우고, 소켓을 닫습니다. 소켓만 닫고 목록에 남겨 두면 다음 깨어남에서 없는 연결을 찾게 되고, 반대로 목록에서만 빼고 닫지 않으면 서술자가 새어 나갑니다. 실습에서는 이 정리를 한 함수에 모읍니다.

만료 대상을 고르는 일과 실제로 끊는 일을 나누는 것도 같은 이유입니다. 고르면서 동시에 지우면, 순회하는 중에 자료구조가 바뀌어 어떤 연결은 건너뛰게 됩니다. 더 실질적인 이유는 시험입니다. 고르는 함수가 순수하면 시계를 넣어 주는 것만으로 경계 조건을 확인할 수 있고, 실제로 소켓을 열거나 몇 초를 기다릴 필요가 없습니다. 경과 시간이 제한과 정확히 같을 때 끊을 것인지 같은 질문은 이렇게 해야 답을 고정할 수 있습니다.

정리한 뒤 로그를 남길 때는 이유를 함께 적습니다. 상대가 닫아서 끊은 것과 유휴 제한으로 우리가 끊은 것은 원인이 전혀 다른데, 로그에 연결 종료라고만 적혀 있으면 나중에 둘을 구분할 방법이 없습니다. 운영에서 유휴 제한을 조정할지 판단하는 근거가 바로 이 구분입니다.

현장에서 만나는 모습

운영에서 자주 보는 조합은 이렇습니다. 연결 수 그래프는 톱니 없이 단조롭게 올라가고, 초당 요청 수는 평소와 같습니다. 즉 새 손님이 오는데 옛 손님이 나가지 않는 것입니다. 이런 그래프는 유휴 정리가 없거나, 있어도 이벤트가 없을 때 깨어나지 않는 구조일 때 나타납니다.

유휴 제한을 너무 짧게 잡아 생기는 반대 방향의 사고도 흔합니다. 연결을 재사용하도록 만들어 둔 클라이언트가 매번 새 연결을 열게 되어, 지연이 늘고 포트가 빠르게 소모됩니다. 그래서 숫자는 클라이언트의 재사용 주기와 함께 정해야 합니다. 서버만 보고 고른 유휴 제한은 대체로 한쪽에서 틀립니다.

다음 퀴즈에서 할 것

연결이 조용한 것과 끊긴 것을 무엇으로 구분할지, 이벤트가 하나도 없을 때 만료 검사가 어떻게 돌아가는지, 그리고 keepalive 가 무엇을 보장하고 무엇을 보장하지 않는지를 정리해 보세요. 실습에서 만들 idle_keys 는 고르기만 하고 지우지 않는데, 그 이유도 함께 생각해 보면 좋습니다.