Oracle Statement Cache 설정 시 메모리 증가 이슈


작성자 : 김민석 ( lemonfish at g m a i l dot com )
JDBC를 이용해 데이터베이스를 다룰 때 일반적으로 사용하게 되는 클래스는 PreparedStatement다. 
응용에서의 주된 사용 패턴은 아래와 같다. 

  1. 바인딩 변수를 포함한 쿼리로 해당 클래스의 객체를 생성
  2. 바인딩 변수에 실제 값을 바인딩
  3. 쿼리 수행
  4. 2~3을 반복
  5. 종료

바인딩 변수를 사용해 쿼리의 형태는 유지한채로 조건만 달리하여 수행함으로서 아래와 같은 이점이 생긴다. 

  1. 데이터베이스 서버 입장에서는 바인딩 변수를 제외한 부분이 동일할 경우 실행계획의 재활용이 가능
  2. 응용 서버 입장에서는 Statement 클래스 대비 객체를 한번 생성하여 재활용할 수 있으니 객체 생성 비용면에서 절감

하지만 또 다른 트랜잭션이 발생할 경우 매번 1~5를 반복하기 때문에 이 또한 PreparedStatement의 생성과 초기화라는 비용이 발생하게 된다. (이는 일반적으로 Pooling이 이뤄지는 Connection 객체를 의미하지 않음) 

그래서 많은 Connection Pooling 라이브러리들 혹은 WAS들은 ( Weblogic, JBoss, WebSphere 등) Statement Cache 기능을 제공한다. 
이는 Pooling된 각각의 Connection 마다 지정된 갯수의 PreparedStatement를 보관할 수 있는 기능이다. 즉, 보관된 PreparedStatement는 각각의 트랜잭션이 발생할 때마다 생성과 초기화 없이 재활용이 가능하다. Connection도 재활용하는데 PreparedStatement라고 못할까.

위의 내용을 토대로 아래와 같은 간단한 공식이 나온다. 

어느 시점에 존재할 수 있는 캐싱된 PreparedStatement 의 최대 갯수 =
Connection Pool 크기 X Statement Cache 크기

예를 들어 Connection Pool Size가 20 이고, Statement Cache 크기를 50으로 잡았다면 최대 1000개의 PreparedStatement가 캐싱된 채로 메모리에 존재할 수 있다. 

지금까지의 내용 만으로는 메모리 이슈가 될만한 내용은 없다. 고작 PreparedStatement 객체 1000개가 머 별거라고... 이슈가 될 수가 없다. 

하지만,.... 여기서 Oracle의 구현방식으로 인한 문제가 발생한다. 

Oracle에서 제공하는 JDBC driver의 PreparedStatement 구현체는 T4CPreparedStatement인데 이 클래스는 PreparedStatement의 특성상 발생하는 반복 실행을 최적화하기위해 최초 실행 시 생성되는 ResultSet 정보를 토대로 버퍼를 생성한다. 이 때 버퍼의 크기를 산정하는 기본 공식은 아래와 같다.

버퍼 크기 = ResultSet에 포함된 컬럼의 사이즈 총합 * PreparedStatement에 지정된 기본 패치 사이즈

ResultSet에 포함된 컬럼의 사이즈 총합을 Oracle 샘플 스키마인 HR의 EMPLOYEES 테이블을 변형해서 예로 들어 보면

employee_id varchar2(10) -> 10byte 
first_name varchar2(100) -> 100byte
last_name varchar(100) -> 100byte
email varchar(200) -> 200byte
phone_number varchar(20) -> 20byte
hire_date date -> 7byte
job_id varchar2(10) -> 10byte
salary number(8,2) -> 21byte (number는 일단 최대치로 잡아본다)
commission_pct number(2,2) -> 21byte
manager_id varchar2(10) -> 10byte
department_id varchar2(10) -> 10byte 

509byte가 나온다. 여기에 기본 패치 사이즈 50으로 둘 경우를 적용해보면 버퍼의 크기는 509 * 50 = 25450byte 이다. 계산을 간편히 하기위해 25KiB 라고 하면 앞서 PreparedStatement 객체의 사이즈가 이 값이 된다. 

1000개의 PreparedStatement가 캐싱됐을 때 

1000 X 25KiB = 약 25MiB 

별 무리가 없어 보인다. 하지만 실무에서는 훨씬 큰 컬럼들이 존재하며 최대 컬럼사이즈의 합이 8KiB가 될 수 있음을 고려해야한다. 이를 토대로 실무의 사례를 들어본다.

어떤 WAS에 설정된 데이터소스 목록
A 데이터소스 : Pool Size 30, Statement Cache Size 100, 주요 테이블의 컬럼사이즈 합 평균 2KiB, 패치사이즈 50
B 데이터소스 : Pool Size 30, Statement Cache Size 100, 주요 테이블의 컬럼사이즈 합 평균 0.5KiB, 패치사이즈 50 
C 데이터소스 : Pool Size 30, Statement Cache Size 100, 주요 테이블의 컬럼사이즈 합 평균 4KiB, 패치사이즈 50

A에서 필요로 하는 메모리 약 300MiB ( 30 X 100 X ( 2KiB X 50 ) ) 
B에서 필요로 하는 메모리 약 75MiB ( 30 X 100 X ( 0.5KiB X 50 ) ) 
C에서 필요로 하는 메모리 약 600MiB ( 30 X 100 X ( 4KiB X 50 ) ) 
  
총합이 975MiB에 달한다. 응용에서 사용되는 메모리를 산정할 때 고려하지 않았던 큰 변수라고 볼 수 있다.
최초 응용의 메모리 산정 시 혹은 가용한 메모리 내에서 설정 변경 시 영향을 예상하기 위해서는
아래의 공식을 응용(혹은 WAS)에서 사용하는 모든 데이터소스에 대해 반복 적으로 적용한 후 총 합을 구해보면 된다.

데이터소스를 통해 접근 가능한 테이블 중 자주 참조되는 테이블들의 평균 행 크기 X PreparedStatement fetch Size X Connection Pool Size X Statement Cache Size

by killy | 2014/11/12 11:08 | 트랙백 | 덧글(0)

Spring Framework 프로시저 호출 시 성능저하 이슈 ( SimpleJdbcCall )


작성자 : 김민석 ( lemonfish at g m a i l dot com )
Spring Framework 2.5 ~ 4.1.2 ( 현재 최신 버전 ) 

현재 널리 쓰이는 모든 버전에서 발생 가능한 성능저하 이슈임. 

다만, 아래 클래스를 직간접적으로 이용하는 경우에 한함. 

org.springframework.jdbc.core.simple.SimpleJdbcCall 

SimpleJdbcCall 클래스는 프로시저 호출 코드 작성 시 자동 파라미터 매핑 기능을 제공하기 때문에 편의상 많이 이용하는데 그 편의 기능의 구현 방식 때문에 성능저하가 발생할 수 있음.

자동 파라미터 매핑 기능은 호출하고자 하는 프로시저의 파라미터 정보를 추출하기 위해 DB벤더 마다 제공하는 JDBC driver의 java.sql.DatabaseMetaData 구현체를 이용한다. 구현방식은 모든 벤더가 거의 유사한데 결론만 얘기하면 시스템 테이블에 대한 별도의 쿼리를 수행하도록 되어 있다. 

때문에 운영 시 집중적으로 많은 횟수가 호출되는 부분에 해당 기능을 활용할 경우 아래와 같은 이유로 성능저하가 발생한다.

  1. 의도한 쿼리 보다 더 많은 쿼리가 수행됨 ( N * 2 )
  2. 의도한 데이터 보다 더 많은 데이터를 가져옴 ( 원 데이터 + 파라미터 정보 )

Spring 공식 이슈 트래킹 시스템에 보고된 바 있는 성능저하 문제이나 편의성 차원에서 제공된 클래스이기 때문에 보완될 여지가 없음.

결과적으로 SimpleJdbcCall을 사용하지 않거나 사용해야 할 경우 별도 구현을 통해 DatabaseMetaData 클래스를 통해 최초 읽어온 파라미터 정보를 캐싱해야 한다. 

파라미터 정보를 캐싱하려는 경우에는 아래 사항을 참고 한다. 

  1. DatabaseMetaData 정보를 캐싱할 경우 동일 프로시저에 대해 최초 호출 시만 추가적인 쿼리 수행과 데이터 패치가 발생하며, 이후 부터는 캐싱된 정보를 활용하기 때문에 성능저하가 없다
  2. 하지만 캐싱 정보 저장을 위한 추가 메모리가 필요해 진다. ( 대부분 유의 할 필요 없음 )
  3. 운영 중 프로시저의 파라미터가 변경 될 경우 캐싱된 정보를 갱신 할 방법을 강구해야 한다. ( 해당 상황이 발생 할 일이 없는 경우 유의할 필요 없음 ) 


 
 

by killy | 2014/11/11 03:16 | Java | 트랙백 | 덧글(0)

Jackson 라이브러리 UTF-8 문자열 처리 시 버퍼 플러시 버그 !!중요!!


작성자 : 김민석 ( lemonfish at g m a i l dot com )
응용에서 POJO 모델 클래스에대한 JSON 직렬화와 역직렬화를 위해 주로 사용하는 Jackson 라이브러리에서 UTF-8 문자열 처리 시 인코딩을 위한 버퍼 플러시 루틴에 버그가 있다.

UTF-8 문자셋을 이용하는 경우에만 해당하며, 이외에는 발생한 경우가 없다. 

증상은 데이터의 직렬화 과정에서 한 글자가 누락되는 현상이며, 누락된 글자가 중요한 데이터인 경우 치명적인 문제가 될 수 있다.

1.6.2 버전에서 최초 보고 되었으며, 1.x 버전대의 최신 버전(1.9.13)이나 2.x 버전대로 업그레이드 할 경우 증상은 사라짐.

org.codehaus.jackson.impl.Utf8Generator 라는 클래스가 문제의 핵심 클래스다. 

해당 구현에서는 UTF-8 문자열의 인코딩을위해 바이트 버퍼를 할당하고 인덱스를 관리하는데 키, 속성, 값 단위로 버퍼에 텍스트를 한 글자씩 채워 넣다가 한번에 버퍼를 넘치는 텍스트가 입력되는 경우 버퍼를 플러시하는 동작을 수행한다. 버퍼 플러시는 현재의 오프셋 만큼의 데이터를 인코딩하여 출력하고 현재 버퍼의 오프셋을 0으로 초기화 시킨다.  
이때, 버퍼의 플러시를 야기한 한 글자를 플러시할 오프셋에 포함 시키지 않기 때문에  버퍼 플러시가 일어나는 경우 한 글자가 누락된다. 

상위 버전에서는 버퍼 플러시와 할당에 문제가 있음을 인지한 덕분인지 Utf8Generator의 구현이 변경되어 긴 문자열의 경우 기존과는 다른 처리를 수행한다. 

반드시 버전업 할 것을 권장한다.  


by killy | 2014/11/11 02:16 | Java | 트랙백 | 덧글(0)

◀ 이전 페이지          다음 페이지 ▶


rss

skin by FreeCssTemplates