봉황대 in CS

How to Debug in a Multi-threaded Parallel Program 본문

Data Centric Computing

How to Debug in a Multi-threaded Parallel Program

등 긁는 봉황대 2024. 5. 13. 18:28

현재 진행하고 있는 연구는 특정 workload에서, thread의 개수가 증가하면 performance(이 연구에서는 throughput)도 같이 좋아지는 scalable 한, 그리고 lock-free로 동작하는 스토리지 시스템을 설계하는 것이다. 따라서 multi-threaded 환경에서 발생하는 concurrency issue들을 '매우 잘' 해결해야 한다.

 

이 '매우 잘'에는 수많은 내용이 함축되어 있는데.. n달 동안 내가 설계한 것을 구현하고, 디버깅하고, 디버깅 완료하면 또 다른 문제를 마주하고, 디자인 변경하고, 더 큰 문제를 마주해서 다시 이전 버전으로 되돌아가고, 다시 디버깅하고, ... 이러한 작업을 계속 진행하고 있다.

 

 

문제는, 어떠한 이슈를 마주하고 디버깅을 진행할 때마다 매번 뇌지컬로 thread들과 싸우는 느낌이 드는 것이었다. 디펜스 게임을 하는 것 마냥 로직에 빵꾸가 있지는 않은지, 다른 thread가 의도치 않게 접근하는 부분은 없는지를 머릿속으로 계속 생각해야 했다.

 

왜 그러했는가를 정리해보면,

 

1. printf, std::sout 등 직접 상태를 출력해 보는 것에는 한계가 있다.

특히 이번에 해결해야 했던 문제는 thread의 개수가 32개 이상으로 많을 때 또는 workload가 클 때(insert 데이터가 10^8개 이상) 일 때에만 나타나는 문제였다.

 

IDE는 CLion을 쓰고 있는데, 출력문이 와다다다다 나오다가 'Low Memory'가 뜨면서 IDE가 죽어버리는 일이 잦았고, 수많은 출력문에서 문제가 되는 지점을 찾아내기란 쉽지 않다.

 

결국에는 test 코드를 통해, workload 실행 이후 의도와는 다른 값들이 저장되어 있는 부분이 존재한다면 그 부분에 대한 컴포넌트들 만을 출력하도록 했다. 하지만 이건 설계의 전체적인 부분을 보는 것이 아니며, 의도치 않은 행위가 발생했던 딱 그 시점의 상태를 출력하는 것이 아닌 그 행위의 결과를 출력하는 것이기 때문에, 어느 상황에 이런 결과가 발생하는지는 직접 생각해내야 했다.

 

2. debugger 사용에도 한계가 있다.

GDB 또는 CLion 내장 debugger를 사용하면 되는 것이 아닌가?라고 의문이 들었을 것이다.

 

여러 thread가 실행되고 있는 상황이거나 workload가 너무 크다면 debugger를 통해 문제가 되는 지점까지 가는 것도 어렵고, 그 문제가 발생하는 것을 관찰하기도 어렵다.

 

그러나 진짜 문제는 따로 있는데, multi-threaded 환경에서의 문제는 대다수 instruction reordering 때문에 발생한다. 즉, 컴파일러에서 최적화 때문에 instruction들을 재배치하기 때문에 프로그래머가 의도하지 않은 동작이 발생하게 되는 것이다. 따라서 debugger를 통해서 관찰하고 있는 순간에는 이러한 재배치가 발생하지 않을 가능성이 있고, 컴파일러가 instruction들을 추가하는 경우도 있어 문제를 아예 재현하지 못하는 경우가 비일비재하게 발생한다.

(이는 std::sout을 통해서 중간중간 출력을할 때에도 가끔 발생하는 문제이다.)

 

 

위와 같은 이유로, debugger는 segmentation fault이 발생했을 때 그 위치를 찾는 용도로만 사용하고 있고

multi-threading issue들은 거의 뇌지컬로 승부를 보고 있었다.

 


 

이런 삶을 계속 살고 있다 보니, multi-threaded 환경에서의 디버깅은 이렇게 고달픈 게 맞나..? 하는 생각이 들었다.

그리고 지도 교수님을 포함한 거장(king)들은 어떻게 디버깅을 하는지 궁금해져서 이번 1:1 미팅 때 여쭤보게 되었다.

 

말씀해 주신 방법은 총 3가지였다.

 

1. 컴포넌트들의 byte 수를 줄이고, workload의 size를 줄여서, 출력문들을 통해 확인한다.

debugger를 사용할 수 없으니, printf, std::sout 같은 출력문에 의지할 수밖에 없다. 그리고 컴포넌트의 크기가 크다면 클수록, 그리고 workload가 크면 클수록 출력문으로 확인하는 것은 불가능하므로 각각의 size를 줄여서 디버깅을 진행한다.

 

e.g., 문제가 발생하는 컴포넌트가 1K짜리였다면 512 bytes, 256 bytes, ... 등으로 크기를 줄여서 디버깅을 진행한다.

단, 내가 이번에 마주한 문제처럼 workload size가 커졌을 때만 발생하는 문제라면 시도가 아예 불가능한 방법이다.

 

2. 코드를 하나씩 주석 처리하면서 확인한다.

물론 코드들을 주석 처리 한다면 프로그램이 정상적으로 실행되지는 않을 것이다. 하지만 출력문들이 어디까지 출력되었는지는 확인할 수 있기 때문에 문제를 발생하는 것으로 예상되는 지점을 점점 좁힐 수 있게 되고, 결국에는 문제가 발생하는 원인을 찾아낼 수 있게 된다.

 

3. 뇌지컬.

어떤 것이든 명확한 해결책이 될 수는 없고, 결국에는 1. 뇌박죽 / 2. 그동안 쌓은 경험들을 통해, 즉 짬에서 나오는 바이브를 통해 승부를 봐야 한다.

 


 

이번에 해결한 문제가 어쩌면 이 연구에서 가장 큰 이슈였는데(이제 최적화할 일만 남은 듯..? 사망 플래그 세운 건가? 아무튼..), 궁극의 방법인 뇌지컬로 어찌저찌 잘 해결해서 다행이다. 그리고 1, 2번 방법은 생각치도 못했는데, 교수님께 여쭤보길 정말 잘했다고 생각이 들었다.

 

다음에도 multi-threading 관련해서 해결해야 하는 문제가 있다면 위의 전수받은 방법을 사용해 보겠노라 결심하며, 글을 마친다.

 

 

반응형
Comments