epoll 로 올린 게임 서버가 몇 시간에 한 번씩 멎었다. 프로세스는 살아 있고 메모리도 정상인데 응답만 안 왔다.
재시작하면 괜찮아졌다가 또 그랬는데 규칙은 없었다. 두 시간 만에 그럴 때도 있고 여덟 시간 갈 때도 있었다.
Table of contents
Open Table of contents
죽은 게 아니라 멈춰 있었다
죽었으면 오히려 나았을 것이다. 로그에 뭐라도 남으니까.
이것은 아무것도 안 남았다. ps 에 보이고 top 의 CPU 도 0%에 가깝고 로그의 마지막 줄은 평범한 내용이었다.
CPU 가 0%라는 것이 단서였다. 무한 반복에 빠졌으면 이 값이 한 코어를 통째로 먹는 쪽으로 나온다. 뭔가를 하다가 죽은 것이 아니라 기다리고 있는 것이다.
멈춘 상태를 들여다봤다
멈춘 프로세스에 gdb 를 붙여서 각 스레드가 어디에 있는지 봤다.
Thread 3: __lll_lock_wait ... ← 잠금 대기
Thread 7: __lll_lock_wait ... ← 잠금 대기
둘 다 __lll_lock_wait 에서 잠금을 기다리고 있었다. 코드를 따라가 보니 이랬다.
| 스레드 3 | 스레드 7 | |
|---|---|---|
| 이미 잡은 것 | 방 | 사용자 |
| 기다리는 것 | 사용자 | 방 |
서로가 가진 것을 기다리고 있었고 둘 다 영원히 못 간다. 이름이 있는 상황이라는 것은 나중에 찾아봐서 알았고 그때는 그림을 그려 놓고 나서야 이래서 안 풀리는구나 했다.
재현을 먼저 하려고 하지 않고 붙잡은 상태에서 바로 본 것이 빨랐다. 멎어 있는 동안이 오히려 원인을 보기 가장 좋은 상태였다. 그래서 다음에 또 멎으면 재시작하기 전에 gcore 로 코어 덤프를 뜨는 절차를 남겼고 재시작하면 그 증거가 사라진다.
왜 가끔만 그랬나
이것이 궁금했다. 코드가 항상 그렇게 생겼는데 왜 매번 안 그러나.
두 스레드가 pthread_mutex_lock 을 정확히 겹치는 순간에 부를 때만 생기기 때문이었다. 하나가 조금만 빨리 끝나면 아무 일도 안 일어난다.
그래서 테스트에서 안 나온다. 사람이 적으면 겹칠 일이 거의 없고 사람이 많아질수록 자주 난다.
이것이 제일 무서웠다. 서비스가 잘될수록 자주 죽는 구조다.
해결 — 잠금 순서와 안전장치
찾아보니 방법이 몇 가지 있었다.
첫째는 순서를 정하는 것이다. 잠금을 잡는 순서를 전체가 똑같이 지키면 서로 엇갈릴 일이 없다.
규칙: 방 → 사용자 순서로만 잡는다. 반대로 잡지 않는다.
간단해 보이는데 지키는 것이 어려웠다. grep 으로 훑어도 코드가 여러 곳에 흩어져 있어서 어디서 어떤 순서로 잡는지 전부 찾아야 했다.
둘째는 한 번에 하나만 잡는 것이다. pthread_mutex_t 를 두 개 동시에 안 잡으면 애초에 생기지 않아서 가능한 곳은 이렇게 바꿨다.
셋째는 기다리는 시간에 한도를 두는 것이다. 일정 시간 안에 못 잡으면 포기하고 물러난다.
pthread_mutex_lock 은 주인이 놓을 때까지 무한정 기다리고 시간을 받는 인자가 없다. 한도를 주려면 pthread_mutex_timedlock 으로 바꾸거나 pthread_mutex_trylock 으로 안 잡히면 그 자리에서 돌아오게 해야 한다.
셋 다 했다. 첫째가 근본이고 둘째는 할 수 있는 곳만 했고 셋째는 안전장치다. 이것은 근본 해결이 아니지만 멎는 대신 실패하게 만들고 실패는 로그에 남으니 최소한 알 수는 있다.
남은 걱정
전부 찾았는지 확신이 없다. 코드를 뒤져서 두 개 이상 잡는 곳을 찾았는데 함수를 타고 들어가서 잡는 경우는 놓쳤을 수 있다. A 가 하나 잡고 B 를 부르는데 B 가 또 pthread_mutex_lock 을 부르면 겉으로는 하나씩 잡는 것처럼 보인다.
새로 짜는 코드는 어떻게 막는지도 모르겠다. 지금은 규칙을 문서에 적어 뒀는데 사람이 지키는 것에 기대는 방식이라 기계가 확인해 주면 좋겠는데 방법을 모르겠다.
순서 규칙이 늘어나면 어떻게 되는지도 걱정이다. 지금은 잠금이 몇 개 안 돼서 순서를 외울 수 있는데 스무 개가 되면 못 외운다.
이 셋은 아직 답이 없고 일단 지금 것만 막아 뒀다.
정리
- 응답이 없는데
CPU가 0%면 기다리는 중이고 죽은 것과 다르다 - 무한 반복이면 한 코어를 통째로 먹는 쪽으로 나온다
- 멈춘 프로세스에
gdb를 붙여 각 스레드의backtrace를 본다 - 두 스레드가 서로가 가진 것을 기다리면 영원히 안 풀린다
- 재현하려 하지 말고 멎어 있는 동안에 본다
- 재시작 전에
gcore로 덤프를 뜨는 절차를 남긴다 - 정확히 겹치는 순간에만 나므로 테스트에서 안 잡힌다
- 사람이 많을수록 자주 나서 서비스가 잘될수록 자주 죽는다
- 근본 해결은 잠금 잡는 순서를 하나로 정하는 것이다
pthread_mutex_lock에는 시간 한도를 못 주고timedlock이나trylock으로 간다- 시간 한도는 근본이 아니라 안전장치이고 멎는 대신 실패하게 만든다