LabHub
배우기 러닝패스 코스

연결 하나가 느려지자 나머지가 전부 멈췄다 · 여럿을 한 자리에서 지켜본다 · 이론

여럿을 한 자리에서 지켜본다

LabHub 에서 이어서 보기

한 줄 요약

다중화 호출은 여러 소켓을 한 번에 지켜봐 주지만, 준비됐다는 답은 지금 읽어도 안 막힌다는 뜻일 뿐입니다.

왜 이게 필요했나

연결 하나가 나머지를 막지 않게 하려면, 기다리는 자리를 연결마다 두지 말고 한 곳으로 모아야 합니다. 연결마다 스레드를 주는 방법도 있지만 스레드는 각각 스택을 가지고 문맥 교환 비용이 있어서, 연결 수천 개를 목표로 하면 다른 종류의 한계에 먼저 닿습니다. 다중화는 반대 방향입니다. 실행 흐름은 하나로 두고, 기다리는 일만 커널에 맡깁니다.

어떻게 동작하나

프로그램은 커널에게 소켓 목록과 각 소켓에 대해 무엇이 궁금한지를 알려 줍니다. 읽을 것이 생기는지, 쓸 자리가 생기는지입니다. 커널은 그중 하나라도 조건이 맞을 때까지 프로그램을 재우고, 깨울 때 조건이 맞은 것만 돌려줍니다. 그래서 연결이 몇 개든 기다리는 자리는 하나입니다.

리눅스에는 이 일을 하는 호출이 여럿 있고, 오래된 것부터 한계가 다릅니다. [select(2)](https://man7.org/linux/man-pages/man2/select.2.html) 는 문서 첫 줄에 경고를 달아 두었습니다. glibc 구현에서 fd_set 이 고정 크기라 FD_SETSIZE 인 1024 보다 작은 번호의 서술자만 지켜볼 수 있고, 이 제한은 바뀌지 않을 것이니 최신 애플리케이션은 poll 이나 epoll 을 쓰라는 내용입니다. [epoll(7)](https://man7.org/linux/man-pages/man7/epoll.7.html) 은 관심 목록과 준비 목록을 커널 안에 따로 두어, 지켜보는 서술자가 많아져도 잘 늘어나도록 만들어졌습니다.

파이썬에서는 이 차이를 직접 고르지 않아도 됩니다. [selectors](https://docs.python.org/3/library/selectors.html) 모듈의 DefaultSelector 가 그 플랫폼에서 가장 효율적인 구현을 골라 줍니다. 프로그램이 다루는 개념은 세 가지로 줄어듭니다.

selector.register(sock, selectors.EVENT_READ, data)for key, events in selector.select(timeout=1.0):    if events & selectors.EVENT_READ:        ...selector.modify(sock, selectors.EVENT_READ | selectors.EVENT_WRITE, data)

여기서 가장 자주 오해하는 것이 준비됐다는 말의 뜻입니다. 읽기 준비는 데이터가 있다는 뜻이 아니라 지금 읽기를 시도해도 멈추지 않는다는 뜻입니다. 상대가 연결을 닫았을 때도 읽기 준비로 깨어나고, 그때 recv 는 0바이트를 돌려줍니다. 0바이트는 빈 메시지가 아니라 더 이상 올 것이 없다는 신호입니다. 이 둘을 섞으면 끊긴 연결을 계속 지켜보며 루프가 쉬지 않고 도는 프로그램이 됩니다.

더 나아가 select(2) 의 BUGS 절은 읽기 준비로 보고된 서술자에 대해 이어지는 읽기가 그래도 막힐 수 있다고 적고 있습니다. 예로 든 상황은 데이터가 도착했지만 검사 결과 체크섬이 맞지 않아 버려지는 경우이며, 문서는 그래서 막히면 안 되는 소켓에는 O_NONBLOCK 을 쓰는 편이 안전하다고 권합니다. 정리하면, 다중화는 논블로킹을 대신하는 것이 아니라 함께 쓰는 것입니다. 준비 통지는 언제 시도할지를 정해 주고, 논블로킹은 시도가 어긋났을 때 프로그램을 세우지 않게 해 줍니다.

깨어난 뒤 할 일에도 규칙이 있습니다. 듣는 소켓이 읽기 준비로 깨어났다면 기다리는 연결이 하나라는 보장이 없습니다. 한 번의 깨어남에 여러 연결이 큐에 들어 있을 수 있으므로, accept 가 [EAGAIN 또는 EWOULDBLOCK](https://man7.org/linux/man-pages/man2/accept.2.html) 을 낼 때까지 반복해서 비워야 합니다. 같은 문서는 이미 끊어진 연결에 대해 ECONNABORTED 가 날 수 있다고도 적습니다. 그 하나 때문에 나머지 대기열을 포기하면 안 됩니다.

현장에서 만나는 모습

사고는 보통 CPU 사용률 100퍼센트로 나타납니다. 연결은 늘지 않았는데 이벤트 루프가 쉬지 않고 도는 것입니다. 원인을 따라가 보면 대개 둘 중 하나입니다. 끊긴 연결을 관심 목록에서 빼지 않았거나, 보낼 것이 없는데도 쓰기 준비를 계속 지켜보고 있는 것입니다. 쓸 자리는 거의 항상 비어 있으므로 쓰기 준비는 거의 항상 참이고, 그러면 select 가 즉시 돌아옵니다. 다음 모듈에서 이 문제를 정면으로 다룹니다.

반대 방향의 사고도 있습니다. 이벤트 하나를 처리하면서 다른 블로킹 호출을 해 버리는 것입니다. 파일을 동기로 읽거나, 다른 서비스에 블로킹으로 요청을 보내거나, 무거운 계산을 그 자리에서 하는 경우입니다. 다중화 서버는 실행 흐름이 하나뿐이라, 이 한 번의 멈춤이 지켜보던 연결 전부의 멈춤이 됩니다. 구조를 바꿨다고 해서 블로킹이 사라지는 것은 아니고, 블로킹이 훨씬 비싸지는 것입니다.

다음 퀴즈에서 할 것

준비됐다는 통지가 보장하는 것과 보장하지 않는 것을 구분해 보세요. 읽기 준비로 깨어난 소켓에서 recv 가 0을 돌려준 상황, 듣는 소켓이 한 번 깨어났을 때 기다리던 연결이 셋인 상황, 그리고 쓰기 준비가 계속 참인 상황을 각각 어떻게 다뤄야 하는지가 퀴즈의 내용입니다.