2010년 2월 21일 일요일

개발자의 악몽 : "간헐 죽는 문제

소프트웨어를 개발해서 '출시'를 해본 사람에게, 품질부서에서 알려주는 버그들 중 어떤 종류의 버그가 가장 괴로운가 라고 물어본다면 누구나 이구동성으로 "간헐 죽는 문제" 라고 답할 거다. 그 괴로움은 경험해본 사람만이 안다.

"XX 기능을 8시간 동안 테스트 하던 중 1회 S/W 죽는 문제 발생하였음. 재현 불가" 

도대체 어쩌란 말인가. 개발자도 난감하지만 사실 품질 검증하는 사람도 난감하기는 마찬가지다. 그들이라고 왜 어떻게 하면 재현된다고 친절하게 가르쳐 주고 싶지 않겠는가. 정말 아무리 다시 테스트를 해봐도 구체적인 재현 루트가 없어 보이니 저런 식으로 말할 수밖에. 

나는 수년간 가전제품에 들어가는 임베디드 소프트웨어 개발에 종사했었고 저런 문제들을 숱하게 보아왔다. 대체로 출시 일정이 거의 닥쳐서야 저런 문제들이 하나 둘 중요한 이슈가 된다. 개발자들은 쉽게 수정이 가능한 재현 가능한 문제들을 먼저 해결하고, 저런 문제는 뒤로 미루게 마련이다. 결국 그렇게 미루고 미루다 출시 시점이 가까워지면 남는 문제는 저런 문제밖에 없다. 일반 PC프로그램과 달리 전자제품에 탑재되는 임베디드 소프트웨어는 제품을 출시하고 나면 수정하는 것이 쉽지 않기 때문에 그 스트레스는 이루 말할 수 없다.

많은 경우 저런 문제들은 깔끔하게 원인 분석이 되지 않는다. 출시는 해야겠기에 해결책이라고 어떻게든 뭔가 고치긴 하지만 그걸 만든 개발자도, 그걸 검증하는 사람도 누구도 "확신"은 없다. 그쯤되면 거의 하루하루가 기도하는 심정이다. "부디 문제가 생기지 않게 해주세요" 라고. 왜 이런 문제들이 발생하는가?. 컴퓨터란 0/1의 지극히 논리적인 기계가 아니었던가? 항상 같은 결과를 출력해야 할 컴퓨터가 왜 저렇게 애매모호한 문제를 보일 수 있는 걸까?

컴퓨터 프로그램은 아주 단순하게 보면 입력을 받아 결과를 출력하는 일종의 기계이다. 그리고 입력이 완벽하게 같다면 항상 같은 결과를 출력한다. 하지만 프로그램에 주어지는 입력은 기본적으로 완벽하게 같을 수 없다. 왜냐하면 여기서의 입력이란 프로그램 실행할 때 입력하는 입력 파라미터 뿐 아니라, 임의의 시간에 들어오는 외부의 인터럽트 예를 들면 타이머 인터럽트나, 키 입력 인터럽트, 심지어 시간에 OS의 IO buffer 상태등 수많은 요소들을 포함하기 때문이다. 이러한 입력은 비 결정적 (non-deterministic) 이다. 이러한 종류의 비결정적 입력으로 인한 가장 대표적인 문제는 multi thread 프로그램에서 잘 나타난다.

Thread 1

S1: if ( index > 0 )

S2:      memcpy(buf+ index, data, len)        

Thread 2

S3: index += len;

(*) [2] 의 논문에서 수정 인용.

위의 예에서 Thread 1이 S1을 실행한 후 S2를 수행하기 직전에 OS가 설정한 timer interrupt가 발생하고 그로 인해 context switch가 발생해 Thread 2가 수행되는 경우를 생각해 보자. 본래 프로그램의 의도는 S1 -> S2 -> S3 의 순서로 수행되는 것이었으나 중간에 발생한 context switch에 의해 S1 -> S3 -> S2 가 수행이 되고 결과적으로 S2는 엉뚱한 곳에 데이터를 복사해 overflow 등의 오류가 발생하게 된다. 요즘의 Multi core 환경에서는 Thread1과 Thread2 가 context switch 없이 동시에 각기 다른 코어에서 수행되는게 가능하기 때문에 이러한 문제는 더더욱 빈번하게 발생할 수 있다.

이와 같이 보기에는 멀쩡해 보이는 코드지만 thread 간의 수행 순서에 따라 결과가 달라지는 종류의 문제가 대표적인 재현 안됨 현상의 원인이다. 이런 종류의 버그를 heisenbugs라고 한다.  프로그래머는 이런 문제가 발생하지 않게 하기 위해 언제든 다른 thread가 수행될 수 있다는 걸 항상 염두에 두고, lock 등으로 적절한 보호를 해 줘야 한다. 하지만 사람의 머리 구조상 이게 쉽지 않다. 게다가 위와 같이 하나의 lock 만으로 간단히 해결되는 게 아닌 수많은 다양한 종류의 문제들이 존재한다 [3].

이런 괴로움을 잘 알고 있는 나로서는, 공부를 다시 시작하면서 알게된 최근의 연구 결과들을 보고 감동하지 않을 수 없었다. 그중 대표적인 두가지를 들자면 Microsoft의 CHESS [1] 와, UIUC의 CTrigger [2] 가 있다. 근본적으로 thread를 사용하는 한 모든 가능한 수행 조합은 거의 무한대이기 때문에 (exponential) 완벽하게 모든 버그를 잡아내는 것은 불가능하지만, 이들 논문의 결과는 많은 실제적인 문제를 해결하는데 큰 도움을 줄 수 있는 것으로 보인다. 물론 기존에도 valgrind 와 같은 유용한 툴이 있었고 실제 현장에서는 이조차도 널리 쓰이고 있지 않는 현실에서 이러한 최근의 연구 결과가 현장에 반영되기까지는 꽤나 긴 시간이 필요하리라 생각되지만, 개발자의 고충을 아는 한 사람으로서 그 시간이 단축되기를 간절히 희망한다.

내가 만약 이전에 몸담았던 기업의 CTO라면 이러한 연구 결과를 재빨리 수용해서 사내의 개발 환경에 맞도록 툴을 커스터마이즈 해서, 개발자들에게 교육을 시킬 것이다. S/W의 품질이 높아지는 것은 물론이거니와, 검증 기간 단축, 그리고 개발자들의 정신 건강에 엄청난 도움이 될 테니까 말이다.

Reference

[1] M. Musuvathi, S. Qadeer, T. Ball, G. Basler, P. A. Nainar, I. Neamtiu Finding and Reproducing Heisenbugs in Concurrent Programs.In Operating System Design and Implementation (OSDI), 2008.

[2] Soyeon Park, Shan Lu, and Yuanyuan Zhou. CTrigger: Exposing Atomicity Violation Bugs from Their Hiding Places. In the proceedings of the 14th International Conference on Architecture Support for Programming Languages and Operating Systems (ASPLOS'09), March 2009. [PDF]

[3] Shan Lu, Soyeon Park, Eunsoo Seo, Yuanyuan Zhou. Learning from Mistakes --- A Comprehensive Study on Real World Concurrency Bug Characteristics. In the proceedings of the 13th International Conference on Architecture Support for Programming Languages and Operating Systems (ASPLOS'08), March 2008.[PDF]

[출처] 개발자의 악몽 : "간헐 죽는 문제" |작성자 덤덤2

댓글 없음:

댓글 쓰기