윈도우즈에서 사용할만한 EventLoop는 ProactorEventLoop 이 유일하다. (SelectorEventLoop은 1024개 이상의 소켓을 만들 수 없다.) ProactorEventLoop은 내부적으로 윈도우즈의 IOCP 를 사용한다. sock_accept 메소드가 IOCP의 AcceptEx를 호출할 때 미처 처리하지 못한 사례가 있다. sock_accept 구현상, 그 사례에서 발생하는 예외는 호출자가 처리하지 못한다. 결국 loop의 exception_handler가 불린다.
미처 처리하지 못한 사례란 다음과 같다.
요약하자면, 서버의 AcceptEx 가 완료되기 전에 backlog 에만 연결되어있던 클라이언트가 강제종료가 되면 ERROR_NETNAME_DELETED(OSError 64)가 발생한다는 것이다. 뭐 OSError Exception이 발생한다면 except 해주면 되잖아? 대략 아래와 같은 식으로 하면 잡히겠지 했다.
@asyncio.coroutine def accept(loop, s): while True: try: yield from loop.sock_accept(s) except asyncio.CancelledError: logging.info("on socket accept failed - cancelled error") except OSError: logging.info("on socket accept failed - os error ") else: logging.info("on socket accept")
해당 상황에서 on socket accept failed – cancelled error 메시지가 나오긴 한다. 그럼 잡힌것 아닌가? 그런데 아래와 같은 출력도 함께 발생한다.
ERROR:asyncio:Task exception was never retrieved future: exception=OSError(22, '지정된 네트워크 이름을 더 이상 사용할 수 없습니다', None, 64, None)> Traceback (most recent call last): File "d:\python342\Lib\asyncio\tasks.py", line 236, in _step result = next(coro) File "d:\python342\Lib\asyncio\windows_events.py", line 363, in accept_coro yield from future File "d:\python342\Lib\asyncio\futures.py", line 390, in __iter__ return self.result() # May raise too. File "d:\python342\Lib\asyncio\futures.py", line 277, in result raise self._exception File "d:\python342\Lib\asyncio\windows_events.py", line 560, in _poll value = callback(transferred, key, ov) File "d:\python342\Lib\asyncio\windows_events.py", line 351, in finish_accept ov.getresult() OSError: [WinError 64] 지정된 네트워크 이름을 더 이상 사용할 수 없습니다
그래서 ProactorEventLoop 소스를 까봤다.
# windows_events.py def accept(self, listener): self._register_with_iocp(listener) conn = self._get_accept_socket(listener.family) ov = _overlapped.Overlapped(NULL) ov.AcceptEx(listener.fileno(), conn.fileno()) def finish_accept(trans, key, ov): ov.getresult() # Use SO_UPDATE_ACCEPT_CONTEXT so getsockname() etc work. buf = struct.pack('@P', listener.fileno()) conn.setsockopt(socket.SOL_SOCKET, _overlapped.SO_UPDATE_ACCEPT_CONTEXT, buf) conn.settimeout(listener.gettimeout()) return conn, conn.getpeername() @coroutine def accept_coro(future, conn): # Coroutine closing the accept socket if the future is cancelled try: yield from future except futures.CancelledError: conn.close() raise future = self._register(ov, listener, finish_accept) coro = accept_coro(future, conn) tasks.async(coro, loop=self._loop) return future
1. 8번째 줄 finish_accept 함수에서 ERROR_NETNAME_DELETED(OSError 64) exception이 발생한다.
2. 이 exception은 이미 생성한 23번 째 줄 future 객체의 exception으로 등록된다.(windows_events.py의 560번째줄 참고)
3. accept_coro 라는 coroutine 에서도 그 future 객체를 yield from 하고 있으니 exception이 발생한다.
4. accept_coro 에서는 OSError 에 대한 except 처리를 안하고 있으니 25번째 줄 코드로 생성된 task(coro task라고 부르자)에 exception으로 등록된다.
5. coro task 가 삭제될 때 exception이 따로 처리 되지 않으면 eventloop은 exception_handler를 호출한다.
exception_handler가 호출되면 안되나? 나는 exception_handler 가 호출되면 서버가 종료되도록 설계했다.1 exception_handler를 최후의 수단이라고 생각하며, 이 한계 내에서 처리가 되어야 한다.
이 문제를 근본적으로 해결하려면 accept_co coroutine 을 수정해야 한다
#요렇게? @coroutine def accept_coro(future, conn): # Coroutine closing the accept socket if the future is cancelled try: yield from future except futures.CancelledError: conn.close() raise except OSError as exc: if exc.winerror == _overlapped.ERROR_NETNAME_DELETED: pass
이외에도 tasks.async 의 리턴 값을 future와 같이 호출자에게 리턴해서 coro task 의 add_done_callback 이라도 심을 수 있게 하던지…(아 이건 아니야…)
python asyncio가 수정되기 전까지는, 처음 링크건 IOCP 관련 포스팅에서 언급된 것처럼 AcceptEx 요청 개수를 늘려서 backlog 쪽에서 클라이언트가 대기하지 않는 방식을 사용하자. 난 이미 이 방식을 사용하고 있었는데 AcceptEx 요청 개수가 내가 원하는 순간 접속 시도 회수보다 작았기 때문에 문제가 발생했던 것으로 보인다. 제일 위에 언급했던 accept 함수를 아래와 같이 수정하면 될 것이다.
@asyncio.coroutine def accept(loop, s): for i in range(2000): loop.create_task(accept_impl(loop, s)) @asyncio.coroutine def accept_impl(loop, s): while True: try: yield from loop.sock_accept(s) except OSError: logging.error("oserror")
참고로 이 포스팅을 하기 위해 재확인차 작성했던 소스를 첨부한다.
https://gist.github.com/kernel0/400d040dcf2734457123
약간 설명하자면 listen 에 backlog 값을 크게 지정한다. (서버 소스의 accept방식이면 500정도만 되어도 잘 발생함)
더미 클라이언트로 여러 개의 소켓 접속 시도를 한다. 접속 시도 중에 더미 클라이언트 파이썬 프로세스를 강제 종료하면 문제가 재현된다.
- exception_handler가 호출될 때 서버를 다운 시키지 않으면 안되나?
나는 안된다고 생각한다. 명시적으로 처리하지 못하는 exception이 있으면 서버가 다운되는 게 옳다고 생각한다. 서버 개발자들마다 철학(?)이 다를텐데 내 생각에는 exception을 뭉개버리면 서버 상태는 개발자가 예측할 수 없는 값을 갖고 뒹구르다가 또 다른 exception을 만들 수도 있고 그렇다면 뭐가 문제인지 모를 수도 있다. 또한 잘못된 데이터라도 들고 있다가 엉뚱한 값을 DB에 저장이라도 하면 헬게이트가 열리겠지. ↩
“[python-asyncio] ProactorEventLoop의 sock_accept는 특정한 상황에서 처리할 수 없는 예외가 발생한다.”에 한개의 의견