반응형

제목: 컨커런트 자바 프로그램에서 에러 발견(Error Detection in Concurrent Java Programs)

저자: Graham Hughes 3, 미국

문서유형: 저널 페이퍼( 14페이지), 2006

 

기 존재하는 대규모 컨커런트(멀티쓰레드) 프로그램에서 컨커런시 에러(concurrency errors)를 발견하기 위해 모델 체커를 적용한 사례를 기술한 자료



모델 체킹

  • 애초에 하드웨어 검증(hardware verification)에 적용되다가 규모가 작고 독립적이며 의도적으로 단순화된 프로그램 모델이나 프로토콜 동작 모델에 적용되기 시작함
  • 모든 가능한 상태(feasible state)를 완전하게(exhaustively) 검증하며 모델의 동작에 대한 여러 속성(properties)도 검증할 수 있다. 다수 상태를 동시에 검토하는 것을 허용하는 수학적 형식(mathematical formalisms)의 도움을 받기도 함
  • 최근 들어 모델 체커를 일부 추출물(extracted artifact)이 아니라 프로그램 자체에 직접 런(run) 시키는 것이 타당해짐. 코드에 런 하도록 설계된 모델 체커의 예로 Verisoft, CMC, Java PathFinder 등이 있음


Java PathFinder(JPF)

  • Java로 쓰여진 타겟 소프트웨어의 컨커런시 에러를 발견하기 위해 본 논문에서 사용한 모델 체커인 JPF는 많은 수의 자바 머신 상태(Java machine states)를 탐색할 수 있다.
  • 파일 입/출력과 네트워크 트래픽을 제외한 거의 모든 퓨어 자바 코드(pure Java code)를 다룰 수 있지만 native methods는 다루지 못함
  • 컨커런트 코드의 모든 시리얼라이제이션(serialization)을 탐색할 수 있으므로 데드락 (deadlocks), 경쟁 상황(race conditions) 같은 에러를 발견할 가능성이 높음


타겟 코드베이스

  • 본 논문에서는 아래 그림과 같은 구조를 가진 대규모 클라이언트 서버 코드베이스를 모델 체커를 이용하여 분석함
  • 이 시스템은 다양한 사용자 인터페이스를 포함하며, 여러 외부 벤더 데이터베이스 제품(third party database products)과 통신해야만 함. 사용자 인터페이스는 자바 기반 thick client, Java applets, 또는 브라우저의 웹 페이지로 구현될 수 있고, 브라우저는 웹 서버상에서 돌아가는 JSPservlet 컴포넌트로 구성된 웹 티어(web tier)에 의해 서비스됨. 주요 프로세스 로직과 분석 엔진은 제 3 티어(third tier)에 상주하고, 4 티어(fourth tier)는 데이터베이스, 디렉토리, 문서 관리, 타 시스템으로의 커넥티비티(connectivity) 등이 포함된 기저 저장소(repositories) 역할을 함
  • 다양한 모듈이 Java RMI를 통해 통신하며, 천 개 이상의 클래스와 470,000 라인이 넘는 자바 코드로 구성됨
  • 이 코드베이스에서 사용된 Java Development Kit에 포함된 RMI 버전은 암묵적으로 컨커런트함(, 디폴트 RMI 서버가 각 클라이언트 마다 새로운 쓰레드를 생성함). 결과적으로 개발 및 디버깅에서 컨커런시 관련 문제가 발생할 수 있음

[코드베이스 구조]



데이터베이스 컨커런시(Database Concurrency)

1) 모델 체킹을 위한 환경 셋업

  • 개발팀이 스트레스 테스트 중에 드물게 나타나는 데드락이 있음을 모델체킹팀에게 알려오고 이 문제를 검토해 줄 것을 요청함
  • 모델체킹팀은 타겟 코드베이스의 구조상 데이터베이스 인터페이스가 메인 코드와는 완전히 분리되어 RMI 서버 뒤에서 런 되므로 이 데드락이 데이터베이스 인터페이스 코드와 관련 있을 것으로 추정하고 RMI 인터페이스에서 분석을 시작하기로 결정함
  • 모델체킹팀은 이 분석을 위해서 데이터베이스 서버를 올리고 request에 대응하는데 필요한 최소한의 환경을 수작업으로 구축함
  • 데이터베이스 서버가 JPF 하에서 돌아가도록 하기 위해 아래와 같이 일부 Java system 클래스를 변경할 필요가 생김
    - RMI
    패키지가 네트워크에 연결을 시도하지 않는 버전으로 대체됨
    -
    시스템의 로깅 메커니즘에 의해 사용되는 Date 클래스가 항상 동일한 시간을 주도록 변경됨(타임스탬프를 제외하고 나머지가 동일한 프로그램 상태의 증식을 막기 위한 목적)
    -
    로칼라이제이션 클래스는 필수적이지 않은 것으로 간주되어 빼냄
    - JDBC
    및 관련된 데이터베이스 액세스 클래스를 원래 코드와 동작은 동일하지만 불필요한 데이터를 저장하지 않는 버전으로 대체함


2) 데이터베이스 어뎁터 통신 프로토콜에 존재하는 버그

  • 셋업된 환경에서 RMI 인터페이스를 사용하여 데이터베이스 서버를 드라이빙하기 시작함. 이와 관련된 프로토콜 부분은 아래와 같다(아래의 상태 머신 그림 참조).
    - 단계1: 클라이언트 코드는 RMI를 이용하여 서브시스템의 공표된(published) 인터페이스인 DbAdapter를 획득한다.
    - 단계2: 클라이언트 코드가 DbAdapter 상의 getConnection()을 호출한다(DbConnection을 얻게 됨)
    - 단계3: 클라이언트 코드가 DbConnection을 통해 데이터베이스를 사용한다
    - 단계4: 클라이언트 코드가 커넥션을 릴리즈 한다. 단계2로 돌아감

[RMI 프로토콜]


  • 위 프로토콜은 실제 사용하기에는 너무 단순하며 DbConnection 오브젝트 생성이 매우 느리고 데이터베이스 서버는 할당된(하지만 미사용인) 커넥션들의 풀(pool)을 유지해야만 함. 이때 클라이언트가 어떤 이유로든 단계4를 수행하지 않으면(, 클라이언트 크래시, 네트워크 에러, hung 클라이언트 등) 해당 클라이언트가 사용중인 DbConnection 오브젝트가 절대 풀로 반환되지 않아 리소스 누출(a resource leak)이 야기될 수 있음
  • 이를 막기 위해 코드베이스는 아래 그림과 같은 수정된 프로토콜을 구현하고자 함. , 데이터베이스 서버가 단계2에서 커넥션 각각을 체크해 오리지널 클라이언트가 아직 살아있는지 판단함. 서버는 클라이언트가 죽었다고 판단된 경우 기 할당된 커넥션을 새로운 클라이언트에게 준다.

[구현을 의도한 RMI 프로토콜]


  • 타겟 코드베이스에 구현된 프로토콜에서 클라이언트가 죽었는지 살았는지 결정하는데 사용된 방법이 커넥션이 5분간 지속되는지를 보는 것임. 네트워크 커넥션이 실제로 끊어진 경우라면 이 프로토콜의 동작이 그런대로 정확함. 반면 어떤 이유에서든 클라이언트가 트랜잭션을 처리하는데 5분 이상이 걸리면(, 데이터베이스 row locks, 데이터베이스 백업, 네트워크 지연, 과부하, 클라이언트에서의 장시간 컴퓨테이션 등) 해당 클라이언트의 DbConnection이 다른 클라이언트에게 주어질 위험이 있다. 실제 구현된 프로토콜은 아래 그림과 같다.

[실제 구현된 RMI 프로토콜]


  • 특정 클라이언트가 타임아웃이 되어 그 커넥션이 다른 클라이언트에게 주어진다면 두 클라이언트의 트랜잰션이 교차될(interleaved) 가능성이 있음. 더불어 JDBC 표준은 java.sql.Connection 오브젝트가 쓰레드 안전(thread safe) 해야 함을 강요하지 않으므로 결과적으로 쓰레드 안정성(thread safety)과 데이터베이스 정확성(database correctness)의 두 가지 측면 모두에서 심각한 결과를 낳게 될 수 있음
  • 이 결함(두 개의 다른 클라이언트가 동일한 DbConnection을 가지게 됨)은 컨커런트 동작이 있는 경우에만 모습을 나타낸다. 아주 부하가 높은 상황을 제외하고 클라이언트의 정상적인 트랜잭션이 5분 이상 걸릴 가능성이 별로 없으므로 해당 결함을 테스팅에서 찾아내기가 쉽지 않다.
  • 이 결함을 해결하기 위해 클라이언트가 죽었는지 확인하는데 더 구체적인 방법을 사용하거나 또는 이런 종류의 동작을 피하도록 프로토콜을 재구성 하도록 데이터베이스팀에게 권고함


캐시 검증(Cache Verification)

  • 개발팀의 요청에 따라 데이터베이스 액세스를 가능한 적게 발생하도록 하는 것이 목적인 오브젝트 캐시를 분석함
  • 이 코드는 FindBugs 같은 단순한 정적 체커로도 발견할 수 있는 다수의 단순 컨커런시 에러와 부적절한 설계 의사결정을 포함하고 있었음
  • 여러 다른 오브젝트에 동작하는 다수의 쓰레드를 사용하여 대상 시스템을 JPF로 런 시켰을 때 모든 쓰레드가 동일한 데이터베이스 오브젝트(object #0)에서 동작하려 하는 이상 현상을 발견함(, 완전히 별개의 프로세스 정의를 가지고 시작한 여러 쓰레드가 모두 동일한 프로세스 정의를 가지고 종료가 됨)
  • 이 에러를 추적하면서 오브젝트 캐시가 데이터베이스로부터 프로세스 정의(a process definition)를 읽어 들일 때 해당 프로세스 정의의 고유한 오브젝트 식별자(object identifier)를 로컬 필드에 저장하는 것을 알게 됨. 새로운 프로세스 정의가 생성될 때는 그 식별자가 아직 결정되지 않은 상태이고 이 프로세스 정의가 데이터베이스에 저장된 후에 고유한 오브젝트 식별자가 할당됨. 오브젝트 캐시는 자신이 가지고 있는 오브젝트 식별자 카피를 업데이트 해 주어야 하는데 그냥 copy set 전체를 디폴트 값인 0으로 놔두어서 문제가 생김


맺음말

  • 본문의 모델 체킹 관련 나올 수 있는 질문으로 어디를 조사해야 할지 어떻게 알게 되었나?” “어떤 종류의 문제들이 모델 체킹을 사용하기에 적절한가등이 있음
  • 본 논문의 경우 개발팀에 의해 조사해야 할 문제가 정해짐. 첫 번째 경우는 개발팀이 원인을 찾는데 곤란을 겪고 있던 문제였고, 두 번째는 특정 모듈이 견고하지 검증해달라는 개발팀의 요청에 의해 이루어짐
  • 양쪽 경우 모두 분석할 대상 모듈의 입력을 기술하는 상태 머신(a state machine)을 추출하였고, “두 클라이언트가 하나의 DbConnection를 공유하는 경우가 있는가” “캐시가 데이터베이스와 동기화가 되지 않을 수도 있는가같은 단순 속성(simple properties)의 위반 사례가 있는지를 찾으면서 해당 상태 머신을 탐색함
  • 타겟 시스템이 컨커런시를 폭 넓게 사용했기 때문에(RMI 클라이언트가 하는 것처럼 암묵적으로 사용하거나 또는 캐시의 경우처럼 분명하게 사용) 이런 속성들을 테스팅 하는 것이 쉽지 않음. 테스팅은 또한 클라이언트가 DbConnection을 가지면 결국에는 이것을 릴리즈 할 것인가같은 liveness 속성에 답하는 데도 곤란을 겪음. 하지만 모델 체커의 적절한 사용을 통해 이런 속성들을 체계적으로 조사하는 것이 가능함


반응형

+ Recent posts