go 언어에서 goroutine group을 같이 종료 시키기
By SeukWon Kang
update 15-03-29) http://blog.labix.org/2011/10/09/death-of-goroutines-under-control 를 같이 보시는 것도 재밌을것 같습니다. 비슷한 고민을 다르게 해결한 경우라고나 할까요.
update) 페북에 올라온 의견을 읽다보니 이글의 설명이 부족한듯 합니다. 아래 상황은 같은 context를 공유하는 goroutine들 간에 종료 신호를 주고 받기위한 방법중 (아마도 )가장 가볍고 빠른 방법을 만들어본것입니다. 이 제한이 없다면 다른 무수한 방법들이 가능할것입니다. 당장 map + mutex를 사용해서도 동일한 효과를 볼수 있을껍니다. ( 크고 무겁겠지요 ^^ ) 여기서 context를 공유한다는 것은 runstate를 struct의 field로 만들고 만들어진 object를 공유하는 여러 goroutine을 생각하시면 됩니다. 굳이 atomic을 사용하는 것은 당연히 multi-goroutine상에서 data coherency 문제를 해결하기 위한 것이구요. ( C 언어의 volatile 을 생각하시면 됩니다. ) 64개 제한은 같은 object를 공유하는 goroutine이 63를 넘어가는 것은 설계상의 고민이 부족한 경우일 것이라고 생각하기 때문에 큰 문제가 되지 않을것이라고 생각합니다. 정 필요하면 runstate를 2개를 쓰면 되겠지요. ^^
map + mutex를 사용하는 것은 전체 goroutine의 생사를 관리하는 용도로는 아주 좋은 것 같은 생각이 듭니다. 다만 제가 알기로는 이 map에 key로 딱 쓰기 좋은 goroutine고유의 id가 없는 것으로 알고 있어서 조금 고민이 필요할듯 합니다. 아마도 전에 소개한 idgen 같은 것을 사용해서 goroutine별 고유 ID를 만들어 쓰면 될지도 모르겠습니다. (그래도 나중에 block되어 있는 goroutine을 찾기위해 id - goroutine match를 할 방법이 없겠군요. )
// 이하 원문 입니다.
go 언어에서는 (외부에서) goroutine을 종료 시키는 방법은 없습니다. 외부에서 goroutine을 종료시키는 유일한 방법은 goroutine애 적절한 신호를 보내서 알아서 종료하도록 하는 방법 뿐입니다.
이 당연한 이야기를 왜 하냐 하면 장시간 실행되어야 하는 프로그램을 작성하면서 의외로 골치를 썩이게 되는 것이 더이상 필요 없어진 goroutine이 종료되지 않고 어딘가에서 멈춰 있는 상황이 종종 생기기 때문입니다.
단기간 실행되는 프로그램이라면 프로그램의 종료와 같이 goroutine도 끝나기 때문에 문제가 없는데 수십/수백시간 이상 실행되어야 하는 goguelike 서버나 , loadtester에서는 이런식의 goroutine leak가 꽤나 문제가 됩니다. 아무리 goroutine이 가볍다고 해도 최소 한개당 4kbyte의 스택을 소비하고 종료되지 않고 있는 goroutine이 reference하고 있을 object역시 GC 되지 않게 되지요.
참고로 goguelike/loadtester 공히 gorourine의 수는 수천정도 입니다.
덤으로 goroutine scheduler 에게도 (아마도) 부담이 가게 될것이구요.
이런 경우를 몇번 경험하고난 뒤로 항상 runtime.NumGoroutine() 을 사용해서 goroutine의 총수를 확인하는 습관이 들었습니다. 장기간 실행하면서 변화가 있는지를 계속 확인 하는 것이지요.
시간에 따라 늘어나는 것이 발견되면 명백한 버그니 그부분을 고쳐야 하는데 문제는 도데체 어느 부분에서 goroutine이 새고 있는지 파악하는 것이 꽤나 힘들다는 게 문제입니다.
경험에 의하면 논리적으로 한그룹을 이루는 여러개의 goroutine 에서 문제가 많이 발생하더군요. 여기서 논리적 그룹은 그중 하나의 goroutine이 종료되면 다른 goroutine역시 종료되어야 하는 집단을 이야기 합니다.
예를 들면 goguelike/loadtester 에서는 하나의 connection당 3개의 goroutine이 생성되는데 하나는 tcp recv를 담당하고 하나는 ai 를 담당하며 하나는 main logic을 담당하게 됩니다. 이중 하나가 종료해야 하는 경우 달랑 자신만 종료해 버리면 나머지 두개는 어디선가 block 된형태로 종료되지 않게 되는 것이지요. 더 큰 문제는 상황에 따라 셋중 종료상황이 최초 발생하는것도 일정하지 않다는 것입니다. 예를 들면 네트웍이 끊기면 recv goroutine이 , 예정된 시간이 지나면 main logic이 , ai가 사망하면 ai 가 스스로 종료를 해야 하는 것이지요. 그리고 그 상황을 같은 그룹내의 다른 go routine에게 전파 해야 합니다.
이것을 channel을 사용해서 전파하려고 하면 지나치게 많은 resource가 소비 되기에
sync/atomic을 사용한 조그마한 package를 만들었습니다.
이름이 runstate인 이유는 각 goroutine이 이 상태를 확인하다가 누군가가 종료하면서 종료신호를 켜면 각각이 자신에 해당하는 종료 flag를 켜고 종료를 하는 용도이기 때문입니다.
나중에 이 flag를 확인하면 어느 goroutine이 종료되었고 블럭된 상태로 종료 전인지를 알수있게 됩니다. ( 물론 이경우는 버그니 잡아야 겠지요. )
기본적으로는 uint64 를 플래그로 사용해서 0번 bit를 제외한 각 bit를 각 goroutine이 담당합니다. 0번 bit 는 아무 goroutine이 종료해야 하는 상황이 오면 켜는 tryStop flag 입니다. 각 goroutine은 자신이 종료해야 하는 상황이 오면 tryStop을 켜서 상황을 전파하고 종료 루틴에 들어갑니다. 종료 직전에 자신의 flag를 켬으로써 goroutine이 성공적으로 종료 됨을 기록합니다. 모든 goroutine은 runstate를 확인 하다가 0이 아닌 상황이 발견되면 종료 루틴을 시작하면 됩니다. ( 물론 종료직전 자신의 flag를 켜고 종료해야겠지요)
나중에 goroutine이 새는 상황이 발견되면 이 flag를 확인 함으로써 어느 goroutine이 종료되지 못했는지 찾아 버그를 수정할수 있겠지요.
flag용으로 uint64를 사용하고 있기때문에 최대 63개 (1개는 tryStop용 )까지의 goroutine group에 사용할수 있습니다.
어느 goroutine에 어느 bit를 할당할지는 각 프로그램에서 결정해 사용하면 되구요. ( 이걸 헛갈리면 대형 버그가 생길 겁니다. ;;; )
조그마한 ( 60여 라인짜리 짧은 패키집니다. ) 코드에 설명이 더 길군요.;;;
패키지 소스는 https://github.com/kasworld/runstate 에 있습니다.