본문 바로가기
컴퓨터/Server

멀티 쓰레드 서버 버그잡기 2

by 김짱쌤 2015. 5. 27.

멀티 쓰레드 서버 버그잡기 2

전체 쓰레드 상황 빠른 스캔

  • DB 쓰레드들은 GQCS에서 건전하게 대기 중

    enter image description here

  • IO 쓰레드들은 FlushSend에서 방황 중

    enter image description here

FlushSend 빠른 스캔

  • I/O 쓰레드 대부분이 FastSpinlock에 걸려있다. 데...데드락?
  • 하지만 해당 임계영역에 블로킹 걸릴 소지가 없다. 단일 Lock만 사용하기에 Lock자원의 교착상태가 발생할 여지가 없다.

    enter image description here

  • 실제로 덤프 시점에 하나의 쓰레드가 락을 빠져나가는 것이 포착되었다. Lock은 문제가 아니야. FlushSend를 호출하는 DoSendJob으로 화살을 돌려본다.

DoSendJob 빠른 스캔

  • DoSendJob은 Blocking이 걸리기 쉬운 구조로 생겼다. 의심이 증폭된다.

    enter image description here

  • 세션의 FlushSend가 실패하게되면 LSendRequestSessionList가 pop되지 못하고 다시 루프를 반복한다. 만약 FlushSend가 계속 실패한다면 무한 루프에 빠지게 될 것!

FlushSend 가 실패하는 경우

  • 세션의 SendPendingCount가 0이 아닌 경우 SendFlush가 실패한다.

    enter image description here

  • SendPendingCount는 세션의 SendFlush가 성공하면, 즉 유효한 Send작업을 요청한 시점에서 값을 증가시킨다.

    enter image description here

  • SendPendingCount는 SendCompletion에서 값을 감소시킨다.

    enter image description here

  • 즉 SendPendingCount는 현재 연기된 Send작업의 개수를 축적시키는 변수이다.

  • FlushSend에서 SendPendingCount가 0이 아니면 실패하게하는 것은 SendPendingCount를 2이상이 되지 않게 만드려는 의도로 받아들일 수 있다.

  • 왜 SendPendingCount가 2이상이면 안될까? FlushSend의 의미부터 다시 생각해봐야한다.

FlushSend의 의미

  • 그 기능과 이름에서 부터 풍겨오는 강력한 느낌은 축적한 sendbuffer를 한꺼번에 보내버리는 애라는 거다.

  • 세션은 PostSend 라는 메소드를 통해 SendBuffer에 보낼 데이터를 축적한다. 그리고 쓰레드별로 Send가 예약된 세션을 담는 리스트 LSendRequestSessionList에 현재 세션을 push한다.

    enter image description here

  • 그리고 I/O 쓰레드는 GQCS와 들어온 I/O를 처리하는 DoIocpJob 과 타이머 작업을 처리하는 DoTimerJob이 끝나면 아까 본 DoSendJob을 통해서 자신의 LSendRequestSessionList에 담긴 세션들을 FlushSend한다.

    enter image description here

  • 따라서 FlushSend란 여기저기서 요청하는 Send작업이 필요할때마다 WSASend를 처리하는 것이 아니라 한 프레임에 보내야하는 데이터를 모아서 한꺼번에 처리하는 구조를 위해 만들어진 메소드라고 볼 수 있다. 축적하여 나중에 한꺼번에 보내는 구조는 Nagle알고리즘과 비슷한 역할을 하지 않을까 생각해 본다.

SendPendingCount가 2이상이 되면 안되는 이유

  • 정확하게 이야기하면 SendPendingCount가 1인 상황에서 FlushSend를 처리하면 안되는 이유이다.

  • SendPendingCount가 1인 상황, 즉 이미 SendBuffer 전체에 대해서 Send요청을 보냈지만, 완료는 되지 않은 상황에서 다시 FlushSend를 한다고 생각해보자.

  • FlushSend작업이 성공했다고 해서 아직 Send가 완벽하게 끝난것은 아니다. 확실히 전체 버퍼가 다 전송될 것이라고 보장할 수 없기 때문이다. 그래서 버퍼가 Remove되는 , 즉 버퍼에 저장된 데이터가 필요없어지는 순간은 sendCompletion이 호출되는 시점이다.

    enter image description here

  • 그래서 FlushSend가 성공해도 SendBuffer는 여전히 보낸 데이터를 가지고 있다. 그 상황에서 다시 FlushSend를 호출하면 이전에 Send요청을 한 데이터가 아직 버퍼에 남은 상황에서 다시 재전송을 하게된다. 이러면 중복전송을 할 뿐만아니라 받는 입장에서는 well-serialized data를 받을 수 없다.

  • 그래서 우리의 훌륭한 교본 EduServer_IOCP에서는 FlushSend한 데이터가 완벽하게 SendComplete 될 때까지 SendFlush를 블로킹한다.

문제는?

  • 문제는 이 Blocking 작업이 해당 세션만 Blocking할 뿐아니라 작업하는 I/O 쓰레드를 Blocking한다는 것에 있다. (Blocking의 의미를 생각하면 당연하지만...)

  • 모든 I/O 쓰레드가 이 Blocking 상태에 놓이게 된다고 가정해보자. Blocking이 해제되기 위해서는 FlushSend한 세션이 SendCompletion이 되어서 PendingCount를 줄여줘야한다. SendCompletion되기 위해서는 GQCS를 호출하여 완료된 I/O작업을 받아서 처리해줄 쓰레드가 필요하다. 그런데 그 작업을 해줄 쓰레드는 다 SendCompletion만을 기다리고 있다. 그야말로 교착상태의 표본이다.

  • 실제로 현재 발생한 덤프시점에서 모든 I/O쓰레드는 FlushSend에 있고, 모든 SendPendingCount가 1이다.

    enter image description here

  • DoSendJob의 블로킹 구조때문에 I/O쓰레드들이 교착상태에 빠지는 것이 이 버그를 만든 주된 원인이다

언제 이런 경우가 발생하는가?

  • 이 상황이 만들어지는 주된 조건은 모든 I/O쓰레드가 처리하는 FlushSend작업에서 Blocking이 걸리는 것이다. 따라서 쓰레드 개수보다 많은 복수의 세션이 SendPendingCount가 1인 상황에서 다시 FlushSend를 호출하는 상황이 필요하다.

  • 세션이 FlushSend를 중복 처리하게 되려면, 짧은 시간동안 여러번 해당 세션이 Send작업을 요청해야한다. PostSend할 때마다 LSendRequestSessionList에 push되므로 DoSendJob에 들어가기전에 리스트에 같은 세션이 두개 있기만 해도 FlushSend블로킹이 발생한다.

  • 일반적으로 서버가 한 세션에 요청하는 Send는 한번이다. 왜냐하면 대부분 REQUEST & RESPONSE 형태로 서버와 핑퐁하기 때문이다. 하지만 Broadcast같이 서버가 일방적으로 여러개의 세션에 전송하는 경우, 다른 요청과 곂쳐 중복 Send하는 경우가 발생할 수 있다.

  • 현재 EduServer_IOCP는 다수의 유저간 채팅을 구현하기위해 Broadcast를 지원하고 있는데, 아마도 이 Broadcast가 자주 요청되어 중첩된 Send요청이 다발적으로 발생한 것으로 보인다.

해결 방안

  • 교착상태를 해결하기 위한 간단한 아이디어는 GQCS를 하는 별도의 쓰레드를 항상 확보해두는 것이다. 그러면 위 문제가 발생하지는 않을 것이다. 하지만 그만큼 SendJob를 처리하는 쓰레드가 줄어들어서 Send양이 많아지면 더 지연이 발생할 것이라는 단점이 있다.

  • 두번째 아이디어는 SendFlush작업을 GCE처럼 처음 걸린 하나의 쓰레드가 도맡아서 처리하는 것이다. GCE의 장점들을 많이 가져올 수 있다는 점은 좋지만, Lock-Free한 전역 큐가 필요하고, 그 큐에 push/pop 작업이 매우 빈번하기에 부하가 많많치 않을 것같아서 포기했다.

  • 마지막 아이디어는 쓰레드에 블로킹을 거는 대신 세션에 블로킹을 거는 방법이다. 세션에 블로킹을 건다는 것은 사실 불가능하기에, FlushSend를 실패한 세션에 대해서는 다음번 SendJob으로 지연시킨다는 개념이다. 실제 구현에 적용한 방법이기도 하다. 위 문제를 해결한 코드를 제시하면서 이 글을 마치려고한다.

    enter image description here


'컴퓨터 > Server' 카테고리의 다른 글

Logging  (4) 2015.05.16
Exception Filter  (1) 2015.05.15
Stored Procedures  (0) 2015.05.09
DB 연동하기  (0) 2015.05.09
멀티쓰레드 서버 버그잡기  (0) 2015.05.04