2013년 6월 3일 월요일

교착 상태(Deadlock)

 대규모의 동시 처리 시스템을 개발하는 경우, 전통적인 프로그램 모델은 멀티 프로세스나 스레드를 사용하는 것이다. 멀티 프로세스나 스레드는 공유 자원(메모리, 파일, 디바이스 등)을 사용하여 상태나 정보를 공유한다. 즉 정보를 읽거나 쓴다. 멀티 프로세스나 스레드들은 경우에 따라 공유 자원을 동시에 사용하므로 동시성 문제가 발생한다. 이를 방지하기 위해 프로세스나 스레드들은 상호 배타적인 동기 메커니즘을 사용한다. 즉 프로그램에 잠금(lock)을 사용하여 멀티 프로세스나 스레드들이 공유 자원을 동시적에 사용하는 것을 배타적으로 관리한다. 크리티컬 섹션(critical section), 뮤텍스(mutex), 세마포(semaphore) 같은 것들이 이런 잠금이다. 그런데 멀티 프로세스나 스레드들이 공유 자원을 배타적으로 이용하다 보면 문제가 발생한다. 일반적으로 잠금을 사용하면 공유 자원을 사용하는 데 있어서 올바른 행동을 보장받을 수 있지만, 그럼에도 불구하고 공유 자원을 사용하는 프로세스나 스레드들은 어쩌다가 교착 상태(deadlock)에 빠지게 된다. 다음 그림은 이런 교착 상태의 예이다.




 위 그림처럼 두 스레드가 두 자원을 잠금을 사용하면서 동시에 배타적으로 참조하는 시스템인 경우, 두 스레드는 이상 없이 잘 동작하다가 어느 순간에 아무 것도 실행하지 않고 공유 자원의 잠금이 풀리기만을 기다리는 교착 상태에 빠지게 된다. 그런데 이런 교착 상태가 실행 중 발생하는 것을 예상하는 것은 상당히 어렵다. 왜냐면 일반적인 교착은 위 그림처럼 단순한 것이 아니라, N개의 자원과 스레드들이 참여하여 발생하기 때문이다. N의 자원과 스레드들이 포함된 순환 교착을 예측하거나 해결하는 것은 고도의 프로그램 기술을 가진 개발자나 아키텍트에게도 결코 쉬운 일이 아니다. 일단 이런 문제는 디버깅하는 것도 상당히 어렵다. 일정 규모의 시스템을 개발하거나 운영해본 전문가들은 이런 문제들이 실제로 가끔 발생하며, 원인도 알 수 없고, 유일한 해결책(?)은 시스템의 재기동이란 것을 경험해 보았을 것이다. 이런 불가사의한 문제의 원인 중 하나가 바로 일정 개수 이상의 공유 자원과 스레드들이 포함된 순환 교착(circular deadlock)이다.

 좀더 구체적인 예를 들어보자. 프로그램에서 싱글톤 패턴(Singleton pattern)을 사용하는 경우 교착 상태에 빠질 수 있다. 싱글톤이 바로 공유 자원이며 이 싱글톤에 값을 쓰거나 읽기 위해 잠금을 사용하는 스레드들이 경쟁하기 때문이다. 싱글톤은 상당히 유용한 패턴이고 많은 곳에서 활용된다. 그러므로 개발자가 프로그램에서 사용하지 않더라도 개발자가 쓰는 프레임워크나 라이브러리 내부에서도 사용될 수 있다. 물론 프레임워크나 라이브러리는 정교하게 동기화되기 때문에 거의 교착에 빠지지 않을 것이지만, 그럼에도 불구하고 싱글톤이 개발자도 모르게 사용될 수 있고, 우연치 않게 여러 라이브러리들이 조합되면서 여러 싱글톤이 여러 스레드들과 경쟁하는 상황이 만들어지면서 교착은 발생할 수 있다.

 이번엔 좀더 현실적인 예를 보자. 일반적으로 웹에서 서블릿 프로그램을 개발할 때, 서블릿 클래스에 멤버 변수를 정의하지 말라는 말을 한다. 왜냐면 서블릿 객체는 톰캣이나 웹로직 서버 등과 같은 서블릿 컨테이너에서 싱글톤처럼 사용되기 때문이다. 다시 말해 서블릿 컨테이너는 한 번 생성한 서블릿 객체를 재활용 한다. 그런데 이런 사실은 모르는 개발자들이 의외로 많다. 그 결과 일반 클래스처럼 멤버 변수를 선언하고 웹 요청을 처리하는 과정에서 해당 멤버 변수에 읽고 쓰기를 반복하는 경우, 불규칙적으로 멤버 변수의 자료 손상을 경험하게 된다. 그리고 이 현상은 단위 테스트를 진행할 때에는 전혀 발견되지 않고 있다가, 실제 운영 환경에서 실행될 때 발견되는 경우가 많다. (실제로 모 금융 기관의 시스템에서도 이런 현상이 운영 중에 가끔 발생하여, 결국 운영 중에 해결되었다.) 그 이유는 단위 테스트 과정은 스레드들이 경쟁하지 않는 환경으로 멤버 변수를 훼손하는 원인이 제거되어 있고, 통합 테스트나 부하 테스트, 인수 테스트 과정도 멤버 변수를 훼손할 정도의 스레드 경쟁 상황이 쉽게 발생하지 않다가, 운영 환경에 와서야 비로소 여러 스레드들이 제대로 경쟁함으로 서블릿 클래스의 멤버 변수를 훼손하는 동시 사용의 순간이 등장하기 때문이다.

 또 다른 예로, 데이터베이스에서도 교착은 상당히 빈번히 발생한다. 데이터베이스는 여러 세션들이 공유 자원인 테이블을 공유하면서 빈번히 테이블에 레코드를 쓰거나, 읽거나, 갱신하거나, 삭제한다. 이 과정에서 데이터베이스는 자원의 무결성을 보장하기 위해 동기 메커니즘을 사용한다. 데이터베이스는 묵시적이든 명시적이든 트랜잭션을 사용하는데, 바로 이 트랜잭션이 바로 동기 메커니즘 즉 잠금이다. 그러므로 트랜잭션에 여러 테이블들이 포함되고 여러 세션들이 이 트랜잭션을 동시에 실행하는 경우, 프로그램들은 교착에 빠질 수 있다. 그러므로 대부분의 데이터베이스는 이런 교착 상태를 해결하는 메커니즘을 별도로 내장하는데, 오라클의 경우 교착 상태를 판단하는 데 기본적으로 60초를 기준 시간으로 한다. 오라클 데이터베이스 엔진은 60초 동안 응답이 없으면 교착 상태로 판단하고 관련 세션들 중 일부를 끊어 문제를 해결한다. 그러나 실시간 비즈니스에서 데이터베이스의 교착은 60초 동안 거래가 중단됨을 의미한다. 극단적인 예로 미사일 방어 시스템이 적의 미사일을 60초 동안 추적하지 못했을 경우를 생각해 보라! 또는 금융 거래 시스템에서 이체 마감 시간에 이체가 60초 동안 중단된 경우를 생각해 보라!

 일반적으로 대규모 동시 처리 시스템에서 모든 플랫폼들이 정상적으로 운영 중이고, 모든 미들웨어와 프로그램들이 정상적으로 운영 중임에도 불구하고 교착은 발생할 수 있다. 그리고 이 교착은 시스템 다운은 아니지만 보이지 않는 거래 중지를 만들어 마치 해당 시스템이 다운된 것과 같은 효과를 발휘한다. 일반적으로 기업은 시스템의 가용성 문제를 해결하기 위해 이중화나 HA(High Availability), 클러스터 등 고비용을 투자한다. 그에 비해 애플리케이션 소프트웨어의 성능이나 품질에 대해서는 제대로 대비하거나 관리하지 않고 있는 것이 현실이다. 대비라고 한다면 또다시 고가의 상용 플랫폼이나 미들웨어를 도입하여 문제를 해결하려고 한다!!! 그러나 애플리케이션이 발생시키는 교착의 문제는 애플리케이션이 동작하는 플랫폼이나 미들웨어가 해결해 주는 것이 아니다. 단지 그들은 보조적인 모니터링이나 기술지원을 해줄 수 있을 뿐이다. 즉 애플리케이션에서 해결해야 하는 문제이다. 그러므로 만약 이런 문제를 사전에 대비하는 경우, 교착으로 인한 서비스의 중단을 해결하는 것뿐만 아니라, 경우에 따라서는 최적화된 잠금 관리로 마법 같은 안정성과 성능 향상을 이룰 수 있게 된다.

 기업 입장에서 볼 때, 대규모 동시 처리 시스템 개발에 애플리케이션의 교착 방지 등 가용성을 위한 전문 인력을 추가적으로 활용하는 것이 추가적인 비용처럼 보일지 모르지만, 이들은 소프트웨어의 가용성과 그에 따른 성능을 상대적으로 높여줌으로,  가용성을 위해 필요한 하드웨어나 플랫폼, 미들웨어의 비용을  절감시켜 준다. 즉 개발 및 유지보수에 따른 전체 비용은 줄어 들 수 있다. 그러므로 대규모 동시 처리 시스템을 개발에는 애플리케이션들의 교착을 방지하고 소프트웨어의 가용성을 높이기 위해 멀티 프로세스나 멀티 스레드 애플리케이션 개발 경험 많은 개발자나 아키텍트가 설계와 개발에 참여도록 해야야 한다.



댓글 2개: