Category: Database

Database 글을 모아 놓은 카테고리

Oracle JDBC Memory Management

이 문서는 Oracle JDBC Memory Management 를 번역한 것 입니다. 일부 오역과 오타가 있을 수 있습니다.

Introduction

데이터베이스 애플리케이션들은 아주 많은 양의 메모리를 사용할 수 있다. 큰 스케일의 JDBC 애플리케이션들은 그들이 사용하는 아주 많은 메모리 때문에 성능상의 문제를 발생시킬 수 있다. 오라클 데이터베이스 10i, 11g JDBC 드라이버는 의도적으로 성능 향상을 위한 많은 메모리 사용과 트레이드 오프 관계다. Oracle Database 12c 드라이버는 메모리를 좀 더 절약하지만 여전히 아주 큰 애플리케이션은 메모리 문제를 일으킬 수 있다. 이 화이트 페이퍼는(White paper) 다양한 드라이버들이 메모리를 어떻게 사용하고 최고의 성능을 위해서 그들을 어떻게 튜닝해야 하는지에 대한 약간의 식견을 제공한다.

Oracle Database 12c 에서 드라이버들의 메모리 관리는 Oracle Database 12c 에서 소개된 VARCHAR 컬럼의 32K 제한을 지원하고, 메모리 사용을 최소화하면서 최고의 성능을 내도록 디자인 되었다. 12c 메모리 관리 체계는 약간의 오버헤드를 유발하지만, 감소된 메모리 공간은 일반적으로 10i 나 11g 보다 낫거나 동일한 성능을 제공한다. 대규모 데이터 애플리케이션도 12c 드라이버를 사용한다고 하더라도 많은 메모리를 사용할 수 있다. 만약 여러분의 애플리케이션이 수용할만한 성능을 낸다면 메모리 사용에 대해 고민할 이유가 없다. 만약 여러분의 애플리케이션이 바라던 성능이 나오지 않으면서 기대 이상의 많은 메모리를 사용한다면 이 문서를 읽어보길 바란다.

Where Does It All Go?

JDBC 드라이버는 많은 곳에 메모리를 사용하지만 가장 큰 문제는 쿼리 결과를 저장하기 위해 사용하는 버퍼(buffer) 다. 각 명령문은(Statement, PreparedStatement 와 CallableStatement 포함) 메모리에 쿼리 결과를 저장한다. 12c 에서 각 명령문은 쿼리 결과를 저장하기 위해 byte[] 버퍼세트을(buffer set) 가진다. 10i, 11g 에서 각 명령문은 두개의 버퍼를 가지는데 하나는 byte[] 이고 다른 하나는 char[] 이다. char[] 는 character 타입(CHAR, VARCHAR2, NCHAR, etc)의 모든 로우 데이터를(row data) 저장한다. 나머지 데이터 타입은 byte[] 에 저장된다. 

10i 와 11g 에서, 일반적으로 명령문이 실행될때에 SQL 스트링이 파싱되는데(구문 분석) 이때 버퍼가 할당 된다. 명령문은 (접속이) 닫힐때까지 이러한 두개의 버퍼를(char[], byte[]) 들고 있게 된다. SQL이 파싱될때 버퍼가 할당되면, 그 크기는(버퍼의 크기) 쿼리에 의해서 리턴되는 실제 로우 데이터(row data) 크기와 상관 없지만, 로우 데이터의 가능한 최대 크기와 관련이 있다. SQL 이 파싱된 후에, 모든 컬럼의(column) 타입을 알고 있고, 이 정보를 이용해 JDBC 드라이버는 각 컬럼을 저장하기 위해 필요한 최대 메모리 양을 계산할 수 있다. 드라이버는 각 페치(fetch) 할때 가지고 올 행수인(the number of rows) fetchSize 를 가진다. (역, 데이터를 가지고 오는 작업을 Fetch 라고 한다. Oracle JDBC 드라이버는 한번에 한 개의 로우를(레코드) 가지고 오는게 아니라 fetchSize 만큼 여러개의 로우를 한번에 가지고 올 수 있다.) 각 컬럼의 크기와 로우 개수와 함께, 드라이버는 단일 페치(single fetch)에서 리턴되어지는 데이터의 최대 크기를 정확하게 계산할 수 있게 되는데, 이것이 버퍼의 크기가 된다.

반면에, 12c 에서 버퍼는 드라이버가 서버로부터 결과 데이터를 읽을때 요청되고 할당 된다. 이것은 10i 나 11g 와 비교해 봤을때 약간의 추가적인 오버헤드 비용이 있지만 쿼리 결과를 저장하기 위해 필요로 하는 메모리 양이 최소화 된다. LONG, LONG RAW 와 같은 타입은 버퍼에 인라인(inline) 으로 저장하기에는 매우 클 수 있고 다루기 어렵다. 기본적으로, 만약 쿼리 결과가 LONG, LONG RAW 타입을 포함한다면 fetchSize는 1로 설정되고, 이것은 향후 명확해질 대부분의 메모리 문제를 해결한다. 

10i 와 11g 에서 Character 데이터는 char[] 버퍼에 저장된다. Java에서 chars 는 한 Character 당 2 byte 이다. (10i, 11g 에서) A VARCHAR2(10) 컬럼은 최대 10개의 Character 를 가질 것이며 Java 에서 chars 는 row 하나당 20byte 를 가질 것이다. A VARCHAR2(4000) 컬럼은 8k bytes/row 를 가질 것이다. 핵심은 실제 데이터 크기가 아니라 컬럼의 크기로 정해지고 있다는데 있다. A VARCHAR2(4000) 컬럼이 모두 NULL 을 가진다 하더라도(실제 데이터가 없다 하더라도) 여전히 8K bytes/row 의 버퍼 메모리가 할당 된다. 버퍼는 쿼리 결과를 보기전에 할당되기 때문에 드라이버는 반드시 아주 큰 쿼리 결과값을 예상해 충분히 큰 메모리를 할당해야 한다. VARCHAR2(4000) 으로 정의된 컬럼은 4000 Character 로 꽉꽉 채워질 수 있다. 버퍼는 반드시 4000 chars 를 가질수 있는, 실제 결과 데이터가 그렇게 크지 않다고 하더라도, 충분한 메모리를 할당해야 한다.

BFILE, BLOB 그리고 CLOB 값들은 로케이터(locator) 처럼 저장된다. 로케이터는 BFILE, BLOB 그리고 CLOB 컬럼에 각각 4K bytes 일 수 있는데, byte[] 는 적어도 4K bytes/row 를 가져야 한다. RAW 컬럼들은 4K Bytes 이상 가질 수 있다. 다른 타입들은 대체로 이 보다 적게 갖는다. 대충 다른 모든 타입은 22 bytes/colums/row 로 가정한다.

드라이버가 executeQuery 메소드를 실행하면, 데이터베이스는 SQL 을 구분 분석, 즉 파스(parse) 한다. 데이터베이스는 결과가 세 개의 컬럼을 가질거라고 보고할 것이다: a NUMBER(10), a VARCHAR2(40) 그리고 a DATE. 첫번째 컬럼은 대략 22 bytes/row 가 필요하다. 두 번째 컬럼은 40 chars/row 가 필요하다. 세번째 컬럼은 대략 22 bytes/row 가 필요하다. 따라서 하나의 로우(row) 는 22 + (40*2) + 22 = 124 bytes/row 를 필요로하게 된다. (각 Character 들은 2 bytes 라는 걸 기억해라) 기본 fetchSize 가 10 로우일 경우에 드라이버는 10 개의 char[], 10 * 40 = 400 chars (800 bytes) 그리고 10 개의 byte[], 10 * (22+22) = 440 bytes 그래서 총 1240 bytes 를 할당할 것이다. (역, 그냥 한 로우 124 bytes/row * 10 = 1240 bytes 다.) 1240 bytes 는 메모리 문제를 야기하지 않는다. 하지만 쿼리 결과가 아주 클 경우에는 문제가 될 수 있다.

최악의 경우로 쿼리 결과가 255 개의 VARCHAR2(4000) 컬럼 리턴한다고 가정해 보자. 각 컬럼은 8k bytes/row 를 갖는다. 225 개의 컬럼이니까 2040K bytes 혹은 2MB/row 다. 만약 fetchSize 가 1000 개의 로우(row) 라면 드라이버는 2GB char[] 메모리를 할당하려고 할 것이다. 이건 좋지 않다.

12c JDBC 드라이버들은 오직 쿼리 메타데이터(metadata) 와 실제 쿼리 데이터를 저장하기 위한 메모리를 할당 한다. (10i, 11g 에서는) 각 컬럼별로 최대 가능 값을 저장하기 위한 메모리를 미리 할당하는 대신에, 12c 드라이버들은 오직 실제 컬럼 값들을 저장하기에 필요한 것만큼만 메모리를 할당 한다. 이것은 추가적인 오버헤드를 요구받지만 거의 모든 상황에서 메모리 사용량이 크게 줄어든다. 

12c 드라이버는 결과 쿼리값에 15 bytes 를 할당하지만 (15 bytes/value), 실제 데이터 값을 저장하기 위해 훨씬 많은 메모리를 필요로 한다. 만약 값이 null 이면 오직 15 byte 만 할당 된다. 만약 컬럼이 VARCHAR2(32000) 이고 실제 데이터가 32,000 characters 면, 15 + 64,000 bytes (2bytes/char) 크기의 메모리를 할당 한다. 만약 컬럼에 다음 데이터가 1 character 이면 15 + 2 bytes 가 할당 된다. 

이것은 10i 와 11g 접근방법을 근본적으로 벗어난 것이다. 만약 모든 값들이 최대 크기라면 12c 는 10i 와 11g 보다 13 bytes/value 를 더 사용한다. 이것은 매우 드문 케이스다. 더 일반적으로 많은 NULL 과 크기가 가변적인 VARCHAR 값들이 있다. 아주 드문 케이스를 제외하고 12c 드라이버는 10i 이나 11g 보다 메모리를 적게 사용할 것이다. 

12c 드라이버는 오직 byte 버퍼만 사용한다. char 버퍼는 없다. 버퍼는 전부 같은 길이다.(The buffers are all the same length) 그들은 캐시되고 재사용되어진다. 버퍼 캐시는(buffer cache) Java Memory Management Team 과 협력해 메모리 소비를 최소화하면서 재사용성을 최적화하도록 디자인 되었다. 12c 버퍼 크기가 10i와 11g의 최소 버퍼 크기보다 크다는 점에 유의할 필요가 있다. 아주 작은 쿼리 결과에서는 더 많은 메모리를 사용하겠지만 12c 와 10i, 11g 비교했을때 몇 kilobytes 밖에 차이가 나지 않는다.

Managing the buffer sizes

버퍼에 의해서 사용되어지는 많은 양의 메모리를 관리하기 위해 사용자가 할 수 있는 것들이 몇가지 있다.

  • 신중하고 주의해서 테이블 정의하기
  • 신중하고 주의해서 쿼리 짜기
  • 신중하고 주의해서 fetchSize 를 지정

VARCHAR2(4000) 과 VARCHAR2(20) 와 같은 컬럼은 10i, 11g 와 아주 큰 차이를 만들어 낸다. VARCHAR2(4000) 컬럼은 8K bytes/row 를 필요로한다. 실제 컬럼이 20 character 보다 더 많이 값을 가지고 있지 않게되면 VARCHAR2(4000) 에 대한 10i, 11g 에 의해서 할당된 대부분의 버퍼는 낭비하게 된다. (역, VARCHAR2(4000) 컬럼에 실제 데이터는 20 char 가 들어가 있는데 10i 와 11g 는 컬럼의 크기를 기반으로 최대 값을 할당하기 때문에 VARCHAR2(4000) 컬럼을 위한 버퍼는 낭비하게 된다.) 12c 드라이버에서는 이런 일이 발생하지 않는다. 

오직 몇개의 컬럼만 필요한데도 SELECT * 을 실행하면 버퍼 크기뿐만 아니라 성능상에 큰 영향을 준다. 이것은 로우 컨텐츠를 가지고 오고, 바꾸고, 네트워크를 통해서 전송하고 자바 표현식으로 다시 바꾸는데 많은 시간이 든다. 오직 몇개의 컬럼만 필요한데도 많은 컬럼을 리턴하면 10i 와 11g 드라이버는 불필요한 결과를 저장하기 위해서 아주 큰 버퍼를 할당하게 된다. 12c 드라이버는 실제 컬럼값에 대해서만 메모리 할당을 강제한다. 이것은 10i 와 11g 보다 작을 수 있지만 12c 드라이버는 NULL 에 대해서도 15 bytes/value 를 할당하기 때문에 여전히 낭비다.

메모리 사용을 제어하는 기본적인 툴은 fetchSize 다. 비록 2MB 는 아주 크지만, 대부분의 자바 환경에서 그 크기를 버퍼에 할당하는 것은 전혀 문제를 발생시키지 않는다. Oracle database 11 의 최악의 케이스 결과로 255개의 VARCHAR2(4000) 컬럼 조차도 fetchSize 가 1 이라면 대부분의 애플리케이션에서 문제가 되지 않는다. Oracle database 12 의 최악의 케이스 1000 개에 VARCHAR2(32000) 은 10i와 11g 드라이버에서 64MB/row 를 필요로 한다.(3200 * 2 = 64000, 1000 개의 컬럼이 있으니까 64000 * 1000 = 64,000,000 이다.) 비록 fetchSize 가 1이라 할지라도 많은 자바 환경에서 문제를 발생시킬 수 있다. 12c 드라이버는 오직 실제 데이터를 저장하기 위해 필요로하는 메모리만 할당 한다. 

메모리 사용 이슈를 다루는 첫 단계는 SQL 을 검토하는 것이다. 각 쿼리에 대해 로우(row) 당 사용될 대략적인 크기를 계산하고 fetchSize 를 확인해라. 로우당 크기가 메우 크다면 좀 더 적은 컬럼을 페치(fetch) 하는게 가능한지(역, SELECT 에서 가지고 올 컬럼을 지정해야 한다. SELECT * 은 무조건 피해야 한다.) 혹은 데이터 크기를 좀 더 타이트하게 제약하기 위해 스크마 수정이 가능한지를 살펴보라. 마지막으로 적당한 크기에 버퍼를 유지하기 위한 fetchSize 를 지정한다. Oracle 은 비록 어떤 케이스에서 큰 크기가 알맞다라고 하더라도 fechSize 를 100 보다 크지 않기를 권한다. 아주 많은 로우를 리턴하더라도 일부 쿼리에 대해 fetchSize 100은 부적절하게 아주 클 수 있다. 

Note: 오라클의 특볗한 메소드 OracleStatement.defineColumnType 은 지나치게 큰 크기로 정의된 컬럼의 크기를 줄이기 위해 OCI 드라이버와 함께 사용되어 질 수 있다. 크기 인자와 함께 호출되면, 그 크기가 해당 컬럼에 대해 스키마에 정의된 크기보다 우선 한다. 이것은 문제를 해결하기 위해 스키마를 마음대로 수정할 수 없는 경우에 해결할 수 있게 해준다. 여러분은 주어진 명령문에서 defineColumnType 을 전혀 호출하지 않거나 그 명령문에서 모든 컬럼에 defineColumnType을 호출해야만 한다. 스키마를 고치는게 가능한 최고의 방법이다. defineColumnType 메소드는 BLOB 및 CLOB 컬럼의 LOB 프리 페치 크기를 지정하는 것을 제외하고 12C Thin 드라이버에서 지원되지 않는다.

One statement does not make a problem

VARCHAR2(32000) 타입의 1000 컬럼이나 setFetchSize(100000)과 같은 극단적인 케이스를 제외하고 단일 명령문은 메모리 사용 이슈를 발생시키지 않는다. 실제로, 문제는 수백 혹은 수천만 명령문 객체가 있는 시스템에서 나타난다. 대규모 시스템에는 동시에 수백 개의 연결이(connection) 열려 있을 수 있다. 각각의 연결은 한 두개의 명령문을 동시에 열 수 있다. 이렇게 큰 시스템은 아주 많은 물리적 메모리를 가진 머신에서 운영된다. 따라서 수백 개의 명령문을 오픈하는 대규모 시스템을 가정하면 심각한 메모리 문제는 발생하지 않을 것 같다. 물론, 드라이버도 많은 메모리를 사용할 수 있지만 메모리는 원래 그렇게 사용된다. 실제로 대규모 시스템도 메모리 문제를 피할 수 있다.

대규모 시스템은 같은 SQL 을 여러번 실행하는 경향이 있다. 성능상의 이유로 여러번 실행할때 마다 각각의 SQL에 대해서 처음부터 다시 만드는 것보다 PreparedStatement 를 재사용하는 것이 좋다. 그래서 대규모 시스템에는 각각 구별되는 SQL 에 대해서 하나 이상의 많은 PreparedStatement 를 가질 수 있다. 대부분의 대규모 시스템들은 Weblogic Server 같이 모듈러 프레임워크를 사용한다. 프레임워크안에 독립 컴포넌트들은 그들이 필요로 하는 PreparedStatement 를 생성한다. 이것은 PreparedStatement를 유지해야 할 필요성과 충돌한다. (역, 프레임워크 안에서 독립 컴포넌트들이 고유의 PreparedStatement 를 생성하기 때문에 재상용을 위해 준비된 Oracle JDBC 의 PreparedStatement 를 사용하지 않는다는 뜻이다.) 이 요구 사항을 해결하기 위해 프레임 워크는 명령문 캐싱을 제공한다. 명령문 캐싱을 가지고 있으면 각각의 연결이 메모리에 수백 혹은 그 이상의 PreparedStatement 를 가지는 것이 쉽다. 이것은 수백, 수천개의 커넥션이 생성되면 실제로 메모리 문제가 발생할 가능성이 있다. 

Oracle JDBC 드라이버는 드라이버에 내장된 명령문 캐시를, 묵시적 명령문 캐시(Implicit Statement Cache), 통해서 이 문제를 해결한다. (명시적 명령문 캐시도 있지만 여기서 다루지 않는다.) 묵시적 명령문 캐시는 투명하다. 사용자는 단지 새로운 객체를 생성하는 것처럼 PrepareStatement 를 호출한다. 만약 드라이버가 prepare 호출이 캐시에 있다면 새로운 객체를 캐시로부터 사용자에게 리턴한다. 하지만 없으면 새로운 객체를 생성한다. 문법적으로 사용자 코드는 새로운 객체와 재사용 객체를 구분할 수 없다. 성능적인 측면에서 새로운 객체를 생성하는 것보다 캐시에서 명령문을 리턴하는 것이 훨씬 빠르다. 캐시된 명령문의 첫 실행은 드라이버와 서버가 이전 실행으로부터 아주 많은 상태들을 재사용할 수 있음으로 해서 훨씬 빠르다. 

묵시적 명령문 캐시는 Oracle JDBC PreparedStatement 의 내부 구조를 알고 있다. 이것은 최적의 성능을 위한 구조로 관리를 할 수 있음을 뜻 한다. 특히, 이것은 char[] 와 byte[] 버퍼를 관리할 수 있다. Oracle 은 실제 애플리케이션에서 서로 다른 버퍼 관리 체계가 어떻게 동작해야 하는지 좀 더 잘 인식하도록 개발됨에 따라 드라이버 버전 마다 다른 방법으로 버퍼를 관리한다. 10i 와 11g JDBC 드라이버는 PreparedSatement 가 묵시적 명령문 캐시에서 리턴될때에만 char[] 와 byte[] 버퍼를 관리 할 수 있다. 만약 PreparedSatement 가 닫히지 않으면, Oracle JDBC 메모리 관리 드라어버는 명령문이 즉시 재사용되지 않을지를 알 방법이 없으며, 그래서 버퍼 사용 관리를 위해 아무것도 할 수 없다. 12c 드라이버는 좀 더 다이나믹한 버퍼 메커니즘을 가지고 있고 ResultSet 이 닫힐때 캐시에서 버퍼를 리턴한다. Oracle 은 여전히 묵시적 명령문 캐시 사용을 권장하지만, 12c 드라이버는 Weblogic Server 와 같은 다른 명령문 캐쉬를 사용할때에도 과도한 메모리 소비를 하지 않는다.

Statement Batching and Memory Use

row 데이터 버퍼는 Oracle JDBC 드라이버가 생성할 수 있는 유일하게 큰 버퍼가 아니다. Oracle JDBC 드라이버는 데이터베이스에 PreparedStatement 파라메터를 전달하기 위해 큰 버퍼를 생성할 수 있다. 애플리케이션은 일반적으로 한번에 적은 양의 데이터를 쓰고 그들의 쓰는것보다 더 많은 양의 데이터를 읽는다. 따라서 파라메터 데이터 버퍼는 row 데이터 버퍼보다 훨씬 작은 경향이 있다. 하지만 잘못된 명령문 배치를 사용하면 드라이버가 아주 큰 버퍼를 생성하게 된다.

애플리케이션이 파라메터 값을 지정하기 위해 PreparedStatement setXXX 를 호출할때, 드라이버는 값을 저장한다. 이것은 적은 메모리를 갖는다; 단지 String 과 같은 Objejct 타입, long 과 double 의 8byte, 다른 모든 것은 4 bytes 와 배열을 레퍼런스 한다. PreparedStatment 실행될때, 드라이버는 Java 타입이 아닌 SQL 타입으로 데이터베이스에 값들을 전달해야 한다. 10i, 11g 드라이버는 byte[] 와 char[] 버퍼를 생성한다. 파라메터 데이터는 버퍼에 저장할 SQL 표현으로 변환 된다. 그리고 나서 드라이버는 네트워크를 통해서 바이트를 전송한다. 드라이버는 버퍼를 할당하기 전에 실제 데이터 크기를 가지기 때문에, 최소 크기 버퍼를 생성할 수 있다. 만약 새로운 데이터 값들이 더 큰 byte[] 나 char[] 버퍼를 필요로한다면, 더 큰 버퍼를 할당 한다. 적당한 양의 메모리를 가지면, 단일 명령문 실행중에 Out of Memory 가 나지 않는다. 하지만 명령문 배칭은 다르다.

명령문 배칭은 하나의 연산으로(operation) 단일 DML 명령문을 여러번 실행한다. 이것을 실행하기 위해서 드라이버는 한번에 모든 DML 실행을 위해 모든 파라메터 값들을 전송해야만 한다. 이것은 드라이버가 모든 파라메터 데이터를 SQL 표현식으로 변환하고 버퍼에 저장해야만 한다. 배치에서 실행 수는, 배치 크기(batch size), 쿼리에 대한 fetch 크기와 유사하다. 단일 명령문 실행에 대한 매개 변수 변환이 메모리 문제를 일이키지는 않지만, 대규모 배치 크기라면 문제가 될 수 있다. 

실제로, 이것은 흔하지 않은 문제며 적합하지 않은 큰 배치 크기(수십만 개정도) 일 때만 나타난다. 매번 수백개의 정도 로우의 executeBatch 를 호출하는 것으로 문제를 해결해야 한다.

12c 드라이버는 아주 큰 배치 크기를 갖는 경우에 배치를 분리시킴으로서 이 문제를 해결한다. 이것은 배치에서 수백만개의 로우를 Out of Memory 없이 다룰 수 있게 해준다. 대신 한번에 모든 로우를 보내기 보다는 모든 로우를 보내기 위해서 여러번 왕복을 해야 할 것이다.(역, 배치를 나눴으니까..) Oracle 은 여전히 아주 큰 배치 크기일 경우에 좀 더 적은 수백개의 로우로 매번 executeBatch 를 호출하도록 권장한다.

Version Specific Memory Management

이번 섹션에서는 다양한 버전의 Oracle JDBC 드라이버들이 어떻게 버퍼 메모리를 다루며 최고의 성능을 내기위해서 사용자가 어떻게 드라이버를 튜닝할 수 있는지에 대해서 다룬다.

Note: 세부적인 메모리 관리를 논의할때에 10i와 11g 드라이버 버퍼가 구체적으로 char[] 와 byte[] 를 가진다는 것을 무시하는 것이 편리하다. 이번 섹션에서 기억해야할 것은 명령문에 대해서 char[] 와 byte[] 버퍼가 그냥 일반적으로 “버퍼” 라고 생각하는 것이다.

비록 Java 메모리 관리가 아주 훌륭하지만, 아주 큰 버퍼 할당은 비용이 많이 든다. 이것은 실제 메모리 할당 비용이 아니다. 그것은 매우 빠르다. 문제는 모든 버퍼를 Zero filled 를 요구하는 Java 언어다. 단순하게 큰 버퍼를 할당하는 것이 아닌 반드시 Zero filled 여야 한다. Zero filling 은 모든 할당된 버퍼의 바이트에 대해 터칭(touching)을 요구한다. 다중레벨 데이터 캐시를 가지는 현대의 프로세서들은 작은 버퍼를 가진다. 아주 큰 버퍼의 Zero filling 은 그 크기가 프로세서 데이터 캐시를 초과하고 Zero filling 작업은 메모리 스피드로 실행 되는데, 이는 대체로 프로세스 스피드보다 느리다. 이 문제에 대한 성능 테스트는 드라이버에서 버퍼할당이 아주 큰 성능상 하락을 반복적으로 보여줬다. 이로 인해 버퍼 할당 비용과 재사용을 위해 버퍼를 절약하는 데 필요한 메모리 공간 간의 균형을 맞추는 데 어려움을 겪는다.

Oracle Database Release 10g Oracle JDBC Drivers

초기 10g 드라이버는 메모리 관리에 대한 접근이 순진했다. 그들은 최고의 성능을 위해 메모리를 관리한다. PreparedStatement 가 처음 실행될때, 필요한 byte[] 와 char[] 버퍼가 할당 된다. 그것뿐이다. 버퍼는 PreparedStatement 가 해제 될때에만 해제 된다. 묵시적 명령문 캐시는 버퍼를 관리하기 위해 아무것도 하지 않는다. 묵시적 명령문 캐시에서 모든 PreparedStatement 는 할당된 byte[] 와 char[] 버퍼를 즉시 재사용되도록 유지한다. 따라서 이 드라이버 버전에서 메모리 관리는 setFetchSize 사용, 신중하고 주의깊은 스키마 설계, 신중하고 주의깊은 SQL 쿼리를 코딩하는 것 뿐이다. 초기 10g 드라이버는 충분히 빠르지만, OutOfMemoryExceptions 를 포함한 메모리 관리 이슈를 가질 수 있다.

Oracle Database Release 10.2.0.4 Oracle JDBC Drivers

10.2.0.4.0 드라이버는 초기 10g 드라이버에서 나타난 메모리 관리 이슈 문제를 해결하기 위해서 시작시에 커넥션 속성이 추가 됐다. 이 커넥션 속성은 모 아니면 도다. 만약 이 속성이 지정되면, PreparedStatement 를 묵시적 명령문 캐시로 반환하면 버퍼가 해제된다. 버퍼는 명령문이 캐시로부터 반환될때 재할당 된다. 이런 단순한 접근은 극적으로 메모리 사용을 줄여주지만 상당한 성능 비용을 필요로 한다. 앞에서 서술했듯이, 버퍼 할당은 비싸다.

connection 속성은 다음과 같다.

oracle.jdbc.freeMemoryOnEnterImplicitCache

이 값은 “true” 나 “false” 를 가지는 boolean 이다. 만약 “true” 라면 버퍼는 PreparedStatement 가 묵시적 명령문 캐시로 반환될때 해제 된다. 만약 “false” 라면, 기본 값이다, 버퍼는 초기 10g 드라이버처럼 유지 된다. 이 속성은 -D 를 통해 시스템 속성으로 지정하거나 getConnection 메소드에 커넥션 속성으로 지정할 수 있다. 주의할 것은 freeMemeoryOnEnterImplicitCache 세팅은 파라메터 값 버퍼 해제를 발생시키지 않으며 오직 로우 데이터(row data) 버퍼 해제만 발생시킨다.

Oracle Database Release 11.1.0.6.0 Oracle JDBC Drivers

JDBC 개발 팀은 10.2.0.4.0 에 모 아니면 도식의 접근법이 이상적이 않다는 것을 깨달았다. 11.1.0.6.0 드라이버는 메모리 관리에 좀 더 세련된 접근법을 가진다. 그건 사용하지 않는 메모리의 최소화, 버퍼 할당 비용의 최소화 라는 두가지 목표가 있다. 이 드라이버는 각 커넥션에 대해 내부 버퍼 캐시를 소개했다. PreparedStatement 가 묵시적 명령문 캐시로 반환되면 버퍼 캐시에 캐시된다. 

서론에 언급 된 바와 같이, 버퍼의 크기는 0에서 수십 또는 수백 메가 바이트까지 광범위하게 변할 수있다. 11.1.0.6.0 버퍼 캐시는 아주 단순하다. 모든 캐시된 버퍼들은 모두 같은 크기다. 버퍼는 묵시적 명령문 캐시에서 모든 PreparedStatement 에 대해서 사용되어질 수 있기 때문에 그 크기는 아주 큰 요청을 가지는 PreparedStatment 만큼의 충분히 큰 크기를 가진다. 만약 한번에 하나의 명령문만 사용중인 경우 하나의 버퍼만 있으며 해당 버퍼는 모든 PreparedStatement 에서 사용된다. 일부 또는 대부분의 명령문의 경우 버퍼가 아주 클 수 있지만, 캐시에 있는 적어도 하나의 명령문에 대해서는 적절한 크기일 수 있다. PreparedStatement 재사용 패턴이 너무 편중되지 않으면, 작은 버퍼를 유지하고 필요에 따라 큰 버퍼를 재 할당하는 것보다 큰 버퍼를 유지하고 지속적으로 재사용하는 것이 더 효과적이다. 여러 명령문이 한번에 열리거나 아주 큰 버퍼를 필요로하는 PreparedStatement 가 하나 있는 경우 잠재적으로 메모리 문제가 있다.

10MB 버퍼를 필요로하는 하나의 PreparedStatement 과 나머지는 아주 작은 버퍼를 사용하는 애플리케이션이 있다고 가정해 보자. 한번에 커넥션당 하나의 명령문만 사용하고 아주 큰 PreparedStatement 가 충분히 자주 사용되면 문제는 없다. 각 명령문이 사용할때마다 단일 10MB 버퍼를 할당하며 PreparedStatement 가 묵시적 명령문 캐시로 반환될때 버퍼 캐시로 캐시된다. 단일 10MB 버퍼는 한번 할당되며 다양한 PreparedStatement 에 의해서 지속적으로 재사용 된다. 이제 한번에 두개의 PreparedStatement 가 열리면 무슨 일이 생기는지 생각해보자. 둘다 버퍼를 가져야 한다. 모든 PreparedStatement 는 버퍼에 할당되어야 함으로 모든 버퍼는 반드시 같은 크기, 최대 크기를 똑같이 가져야만 한다. 두개의 PreparedStatement 가 한번에 열릴때 버퍼는 각각 10MB 여야 한다. 두번째 PreparedStatement 가 열리는 중에, 그것이 아주 작은 용량의 버퍼를 필요로한다고 하더라도 10MB 버퍼가 할당된다. 만약 세번째 명령문이 열리면, 세번째 10MB 버퍼가 할당된다. 수백개의 커넥션과 한번에 수백개의 PreparedStatement 가 열리는 대규모 시스템에서 모든 PreparedStatement 에 대한 아주 큰 버퍼 크기는 메모리를 넘치게할 가능성이 있다. 뒤돌아보니 (문제가) 명확하지만, 개발팀은 이것이 얼마나 큰 문제인지 판단하지 않았고 문제가 있음을 내부 테스트에서 인지하지 못했다. 이 이슈는 적절한 스키마 디자인, SQL 코딩 그리고 fetchSize 선택을 통해서 최소화 될 수 있다.

주의해야할 것은, 11.1.0.6.0 드라이버는 버퍼처럼 freeMemoryOnEnterImplicitCache 를 지원하지 않으며 그것을 캐시에 넣을때 PreparedStatement 에 의해서 항상 해제된다. 해제된 버퍼들은 내부 버퍼 캐시에 저장된다.

Oracle Database Release 11.1.0.7.0 Oracle JDBC Drivers

11.1.0.7.0 드라이버는 아주 큰 버퍼 문제를 해결하기 위해서 커넥션 속성을 소개 한다. 이 속성은 버퍼 캐시에 저장되어질 최대 버퍼 크기와 연관된다. 모든 큰 버퍼들은 PreparedStatement 가 묵시적 명령어 캐시로 집어넣을때 해제되고 PreparedStatement 가 캐시로부터 데이터를 받을때에 재할당 된다. 대부분의 PreparedStatement 가 예를들어 100KB 미만의 적당한 크기의 버퍼를 필요로하지만 일부는 훨씬 더 큰 버퍼를 필요로하는 경우, 커넥션 속성을 110KB 로 설정하면, 자주 최대 크기 버퍼 할당에 부담을주지 않고 자주 사용되는 작은 버퍼를 재사용 할 수 있다. 이 속성을 설정하면 성능 향상되고, OutOfMemoryException 을 방지할 수 있다.

이 커넥션 설정은

oracle.jdbc.maxCachedBufferSize

이 값은 “100000” 처럼 정수 문자열(int string) 이다. 기본값으로 Integer.MAX_VALUE 다. 이것은 내부 버퍼 캐시에 저장할 수 있는 버퍼에 대한 최대 크기다. 하나의 크기로 char[] 와 byte[] 버퍼를 모두 사용한다. 그 크기는 char[] 버퍼의 경우 문자로(chars) byte[] 버퍼의 경우 byte로 표시된다. 이것은 미리 정의된 크기가 아니라 최대 버퍼 크기다. 만약 maxCachedBufferSize 가 100KB 로 설정되었지만 최대 버퍼 크기가 100KB 보다 적은 50KB 라면, 버퍼 캐시의 버퍼들은 50KB 가 될 것이다. maxCachedBufferSize 값의 변경은 드라이버의 내부 버퍼 캐시에서 char[] 나 byte[] 버퍼를 포함하거나 제외할때만 성능이 달라진다. 아주 큰 변화, megabytes 조차, 차이가 없다. 마찬가지로 하나를 변경하면 PreparedStatement 의 버퍼를 포함하거나 제외하하는 일이 발생할때에 차이가 난다. 이 속성은 -D 를 통해 시스템 속성처럼 지정하거나 getConnection 을 통해서 커넥션 속성처럼 지정할 수 있다.

만약 여러분이 maxCachedBufferSize 이 설정이 필요하다면, 큰 버퍼들이 필요로하는 SQL 쿼리에 대한 버퍼 크기를 측정하는 것으로부터 시작해야 한다. 이 과정에서 이러한 쿼리에 대한 fetch 크기를 튜닝하면 여러분이 바라는 성능을 얻을 수 있다. 실행 빈도와 버퍼 크기를 고려해 대부분의 명령문이 캐시된 버퍼를 사용할 수 있도록 크기를 정해야 하지만, Java 런타임이 새로운 버퍼를 할당해야하는 빈도를 최소화하기 위해 필요한 버퍼 수를 지원할 수 있을 정도의 충분히 작은 크기여야 한다.

어떤 애플리케이션들은 쓰레드 개수에 비해 많은 idle 커넥션을 가진다. 애플리케이션은 많은 데이터베이스 중에 하나에 접속을 필요로 하지만 한번에 하나만 접속한다. 만약 각 데이터베이스에 스레드당 대충 하나의 커넥션이 있고 스레드보다 더 많은 데이터베이스가 있는 경우에 스레드보다 더 많은 idle 커넥션을 가진다. 기본적으로 버퍼 캐시는 커넥션당 가지기 때문에 idle 커넥션으로 인해서 사용하지 않는 버퍼를 버퍼에 할당한다. 이것은 필요 이상으로 큰 메모리 공간이 필요함을 의미 한다. 비교적 극단적인 상황이지만, 발생할 수 있다.

이 경우에 커넥션 속성을 지정함으로써 해결할 수 있다.

oracle.jdbc.useThreadLocalBufferCache

이 속성의 값은 Boolean 문자열, “true” 나 “false” 다. 기본값은 “false” 다. 이 속성이 “true” 로 지정되면, 버퍼 캐시는 커넥션이 아닌 ThreadLoacl 에 직접 저장된다. 만일 커넥션보다 스레드 수가 적으면 사용되는 메모리 양이 줄어들 것이다. 이 속성은 -D 를 통해서 시스템 속성이나 getConnection 을 통해 커넥션 속성으로 지정할 수 있다. 

useThreadLocalBufferCache = “true” 를 사용하는 모든 커넥션은 같은 정적 ThreadLocal 필드를 공유하며 따라서 같은 버퍼 캐시 세트를 공유한다. 만얄 useThreadLocalBufferCache = “true” 설정되어 있다면, 사용하는 모든 커넥션은 같은 maxCacheBufferSize 를 가진다. 스레드가 처음 하나의 커넥션을 사용한 다음 다른 커넥션을 사용하는 경우 두 커넥션는 사용 된 버퍼의 크기와 수에 따라 서로의 성능에 간접적인 영향을 미친다. 보통 ThreadLocal 버퍼 캐시를 사용하는 모든 커넥션은 같은 애플리케이션의 일부일 것임으로 다른 커넥션을 사용하는 경우의 서로의 성능에 간접적인 영향을 미치는 일은 없다. 만약 한 쓰레드가 명령문을 생성하고 다른 쓰레드가 명령문을 닫는다면, 버퍼는 한개의 ThreadLocal 버퍼 캐시에서 다른 ThreadLocal 버퍼 캐시로 마이그레이션할 것이고 재사용되지 않을 것이다. 이것은 사례로 권장하지 않는다. 만약 모든 명령문이 하나의 ThreadLocal 에서 생성되었고 다른 쓰레드가 닫는다면 전혀 버퍼를 재사용하지 않게 된다. 다시말해 이것은 권장하지 않는다, 만일 여러분의 애플리케이션이 이러한 경우에 속한다면 절대로 useThreadLocalBufferCache 를 true 로 세팅하지 말라. 이것은 어떤 커넥션은 ThreadLocal 버퍼 캐시를 사용하고 어떤 것은 커넥션 버퍼 캐시당 기본값을 사용할 가능성이 있다.

Oracle Database Release 11.2 Oracle JDBC Drivers

11.2 드라이버는 11.1.0.7.0 보다 더 정교한 버퍼 캐시를 가진다. 이 버퍼 캐시는 다중버킷을(multiple bucket) 가진다. 버킷에 모든 버퍼들은 모두 같은 크기이며 이 크기는 미리 정해진다. 맨 처음 PreparedStatement 가 실행되면, 드라이버는 결과를 보관할 가장 작은 크기의 버퍼를(역, 최적크기의 버퍼) 갖는 버킷으로부터 버퍼를 얻는다. 만약 버킷에 버퍼가 없다면, 버킷에 미리 정해진 알맞은 크기의 새로운 버퍼를 할당한다. PreparedStatement 가 닫히면, 버퍼는 알맞은 버킷으로 반환된다. 버퍼들은 다양한 크기에 요구사항에 사용되므로 버퍼들은 보통 필요한 최소 크기보다 약간 크다. 불일치(역, 버퍼 크기의 불일치) 발생은 제한적이며 실제로 임펙트를 주지는 않는다.

11.2 드라이버는 항상 11.1 이나 10.2.0.4.0 드라이버보다 메모리를 같게 혹은 적게 사용한다. 어찌보면 11.2 드라이버가 충분한 성능을 내는데 비해 너무 적은 힙 크기로 실행되는 것이 말이 안되기도 한다. 더 적은 메모리로 실행된다는 이유만으로 그것이 효율적이라는 뜻은 아니다. 11.2 드라이버에서 대규모의 성능향상을 위해 -Xms 값을 크게 설정하는 것은 드문일이 아니다. 아래에 자바 힙 제어하기 섹션을 보라.

11.2 드라이버는 maxCachedBufferSize 를 지원하지만 별로 중요하지 않다. 11.1 에서 정확한 maxCachedBufferSize 설정은 탁월한 성능과 OutOfMemoryException 의 차이를 만들어 낸다. 11.2 드라이버에 maxCachedBufferSize 설정은 가끔 대규모 명령문 캐시와 광범위하게 나뉘는 버퍼 크기의 요구사항을 갖는 대규모 시스템에서 성능을 개선 시킬 수 있다. 11.2 에서 maxCachedBufferSize 는 최대 버퍼 크기의 log2 로 해석된다. 예를들어, maxCahcedBufferSize 가 20 으로 설정했다면 캐시 된 최대 크기 버퍼는 2^20 = 1048576 이 된다. 이전 버전과 호환성을 위해, 30 보다 큰 크기의 값은 log2 크기가 아닌 실제 크기처럼 해석되지만, 2의 거듭 제곱을 사용하는 것이 좋다.

maxCachedBufferSize 값의 합리적인 설정은 시스템에 임팩트를 주지 않는다. 만약 여러분이 maxCachedBufferSize 를 설정해야 한다면, 18부터 시작해라. 만약 16보다 적은 값을 설정 해야한다면 아마도 더 많은 메모리가 필요하다.

11.2 드라이버는 파라메터 데이터 버퍼를 위해서 같은 크기의 버퍼 캐시와 캐싱 스키마를 사용 한다. PreparedStatement 가 묵시적 명령문 캐시에 있으면 파라메터 데이터 버퍼들은 버퍼 캐시에 캐시 된다. PreparedStatement 가 맨 처음 실행되거나 묵시적 명령문 캐시에서 검색된 후 처음으로 실행될 때 버퍼 캐시로부터 파라메터 데이터 버퍼를 얻는다. 전형적으로 파라메터 데이터 버퍼들은 로우(row) 데이터 버퍼들보다 훨씬 작지만, 이런 버퍼들은 아주 큰 배치 크기를 가질 수 있을만큼 클 수 있다. 또, 11.2 드라이버는 Bfile, Blob, Clob 연산을 위한 char[] 버퍼와 대규모 byte[] 에 대해 다른 버퍼 캐시를 사용한다. 

11.2 드라이버도 useThreadLocalBufferCache 를 매우 잘 지원한다. 이것에 대한 기능과 이것을 언제/어떻게 사용할지 대한 권장사항은 11.1.0.7.0 과 동일하다.

또, 11.2 드라이버는 묵시적 명령문 캐시를 활성화 하기 위해서 새로운 속성을 추가한다.

oracle.jdbc.implicitStatementCacheSize

이 속성의 값은 “100” 과 같은 정수형 문자열이다. 이것은 명령문 캐시의 초기(initial) 크기다. 속성을 양수로 지정하면 묵시적 명령문 캐시가 활성화 된다. 기본값은 0 이다. 이 속성은 “-D” 를 통해서 시스템 속성이나 getConnection 을 통한 커넥션 속성으로 지정할 수 있다. OracleConnection.setStatementCacheSize 나 OracleConnection.setImplicitCachingEnabled 를 호출하면 implicitiStateCacheSize 값이 무시된다. 이 속성은 코드로 활성화하는 것이 비실용적인 경우 묵시적 명령문 캐시 활성화를 더 쉽게해 준다.

Oracle Database Release 12.1.0.1.0 Oracle JDBC Drivers

앞에서 설명했듯이, 12.1.0.1.0 드라이버는 다른 메모리 관리 체계를 사용한다. 12c 드라이버는 정의된 컬럼 크기에 민감하지 않다. VARCHAR2(32000) 컬럼은 같은 데이터에 대해 VARCHAR2(1) 보다 메모리를 더 사용하지 않는다. 10i 나 11g 드라이버 처럼, 12c 드라이버는 (쿼리 후 받은 데이터) 결과에서 fetchSize 와 실제 데이터 크기를 더한 컬럼의 수에 민감하다. Oracle 은 스키마 디자인이 다른 시스템 부분에 영향을 줄 수 있기 때문에 신중한 설계를 지속적으로 권장한다. 신중한 쿼리 디자인, 최소한의 컬럼과 로우(row) 세트 검색과 신중한 fetchSize 선택은 그 어떤것보다 중요하다.

12c 드라이버는 버퍼나 버퍼 캐시 크기를 제어하기 위한 옵션을 가지지 않는다. 

어떤 애플리케이션은 쓰레드 수에 비해 아주 많은 idle 커넥션을 가진다. 애플리케이션은 아주 많은 데이터베이스 중에 하나와 연결해야 하지만 오직 한번에 하나만 연결해야 한다. 만약 대략 각 데이터베이스를 위해 쓰레드당 하나의 연결이 있다고 한다면, 쓰레드보다 더 많은 idle 커넥션이 있게 된다. 기본적으로 버퍼 캐시는 커넥션당 이기 때문에 idle 커넥션은 버퍼 캐시에서 비사용 버퍼가 되는 결과가 된다. 이것은 필요 이상으로 많은 메모리가 필요함을 뜻한다. 이것은 비교적 드문 상황이지만, 알려지지 않은 것은 아니다. 

이 경우 커넥션 속성 설정을 통해 해결할 수 있다.

oracle.jdbc.useThreadLocalBufferCache

이 속성의 값은 Boolean 스트링, “true” 이거나 “false” 이다. 기본값은 false 다. 이 속성이 true 일때, 버퍼 캐시는 직접 커넥션을 저장하지 않고 ThreadLocal 에 저장 된다. 만약 커녁션보다 적은 쓰레드만 있다면, 이것은 많은 양의 사용할 메모리를 줄여준다. 이 속성은 -D 를 통한 시스템 속성이나 getConnection 을 통한 연결 속성으로 설정할 수 있다.

useThreadLocalBufferCache = “true” 를 사용하는 모든 커넥션은 같은 정적 ThreadLocal 필드를 공유하며 따라서 같은 버퍼 캐시 세트를 공유한다. 만얄 useThreadLocalBufferCache = “true” 설정되어 있다면, 사용하는 모든 커넥션은 같은 maxCacheBufferSize 를 가진다. 스레드가 처음 하나의 커넥션을 사용한 다음 다른 커넥션을 사용하는 경우 두 커넥션는 사용 된 버퍼의 크기와 수에 따라 서로의 성능에 간접적인 영향을 미친다. 보통 ThreadLocal 버퍼 캐시를 사용하는 모든 커넥션은 같은 애플리케이션의 일부일 것임으로 다른 커넥션을 사용하는 경우의 서로의 성능에 간접적인 영향을 미치는 일은 없다. 만약 한 쓰레드가 명령문을 생성하고 다른 쓰레드가 명령문을 닫는다면, 버퍼는 한개의 ThreadLocal 버퍼 캐시에서 다른 ThreadLocal 버퍼 캐시로 마이그레이션할 것이고 재사용되지 않을 것이다. 이것은 사례로 권장하지 않는다. 만약 모든 명령문이 하나의 ThreadLocal 에서 생성되었고 다른 쓰레드가 닫는다면 전혀 버퍼를 재사용하지 않게 된다. 다시말해 이것은 권장하지 않는다, 만일 여러분의 애플리케이션이 이러한 경우에 속한다면 절대로 useThreadLocalBufferCache 를 true 로 세팅하지 말라. 이것은 어떤 커넥션은 ThreadLocal 버퍼 캐시를 사용하고 어떤 것은 커넥션 버퍼 캐시당 기본값을 사용할 가능성이 있다. 

12c 드라이버는 좀 더 동적인 메모리 관리를 하기 때문에 LONG 및 LONG RAW 컬럼을 메모리로 직접 가져오는 것이 합리적일때가 있다. 만약 여러분의 애플리케이션이 LONG 이나 LONG RAW 에 대한 전체 값을 저장할 충분한 메모리를 가지고 있다면 oracle.jdbc.useFetchSizeWithLongColumn 속성을 설정할 수 있다. 기본적으로, 쿼리가 LONG이나 LONG RAW 컬럼으로 반환하는 경우 드라이버는 fetchSize 를 1로 설정하고 요청시에만 네트워크를 통해서 LONG 나 LONG RAW 값을 읽는다. 만약 이 설정을 설정하면 드라이버는 정의된 fetchSize 를 사용할 것이며 VARCHAR 나 RAW 처럼 전체 LONG 이나 LONG RAW 를 메모리에서 읽는다. LONG 과 LONG RAW 값이 아주 클 수 있기 때문에, 그 값의 크기를 정확하게 메모리에 맞는지를 알지 못한다면 이 설정을 지정해서는 안된다. 만약 이값이 적절하지 않다면 드라이버는 OutOfMemoryException 을 얻을 수 있다. 만일 그것을 저장할 힙(heap) 을 가지고 있다면 메모리에서 읽을 수 있는 가장 큰 단일 값은 2GB 이다.

연결 속성은 

oracle.jdbc.useFetchSizeWithLongColumns

이 값은 Boolean 스트링으로 “true”나 “false” 다. 만약 “true” 이면 LONG 과 LONG RAW 값들은 VARCHAR 과 RAW 값들처럼 메모리에서 읽는다. 기본값 “false” 면 쿼리에서 LONG 이나 LONG RAW 컬럼을 포함해 fetchSize 1 로 강제되고 요청시에만 네트워크를 통해서 LONG 과 LONG RAW 값들을 읽게 된다. 이 속성 역시 “-D” 를 통해서 시스템 속성으로 지정하거나 getConnection 메소드를 통한 연결 속성으로 지정이 가능하다. 주의할 것은 이 속성은 10i, 11g 드라이버에도 존재하지만 잘 사용되지 않는다.

Controlling the Java Heap

자바 런타임 메모리 튜닝은 블랙 아트 영역이다. 가장 중요한 두 가지 옵션은 -Xmx 와 -Xms 다. 자바 런타임 버전 및 OS 에 따라 다른 매개 변수가 있다. -Xmx 는 자바 런타임에서 사용할 최대 힙 크기를 설정한다. -Xms 는 초기 힙 크기를 설정한다. 디폴트 값은 OS 나 자바 런타임에 의존적이지만 대충 기본적으로 -Xmx 는 64MB, -Xms 는 1MB 다. 32 bit JVM 은 2GB 이상의 힙을 지원한다. 64bit JVM 은 아주 큰 힙을 지원할 것이다. 이 옵션들은 “k”, “m”, “g” 의 접미사를 가진 값을 받을 수 있는데, 이것은 kilo-, mega-, gigabytes 다. e.g -Xmx1G

Oracle JDBC 드라이버는 적절한 힙 크기로 실행될때 최고의 성능을 낼 수 있다. 대부분의 애플리케이션의 경우 애플리케이션 힙 크기를 일부 제한으로 늘리면 성능이 향상된다. 그 이후에 제한된 값 이상으로 애플리케이션 힙 크기를 늘려도 성능 차이는 없다. 만약 힙 크기가 머신이 가지고 있는 물리 메모리나 힙 메모리보다 훨씬 크기되면 세컨드 스토리지로 페이지되고 성능은 급격히 나빠질 것이다. 힙 크기를 설정할때, 최대 값 -Xmx 만으로는 출분하지 않다. 특히 11.2 드라이버는 메모리 할당에 그렇게 적극적이지 않다. 빈번하게 실행 가능한 최소값을 초과하여 힙 메모리를 늘리지 않는다. 만약 -Xmx 를 사용해 최대 힙 크기만 지정한다면, 11.2 드라이버는 설사 -Xmx 를 허용했다고 하더라도 최대 성능을 내기위한 충분한 메모리를 결코 사용하지 않는다. 만약 -Xms 를 사용해 최소 힙 크기를 함께 늘리면, 11.2 드라이버는 추가적인 메모리를 사용하게 만들 것이고 자주 최고의 성능을 제공할 것이다. 

애플리케이션 성능 튜닝시에 -Xmx 와 -Xms 모두 설정되고 모두 같은 값으로 설정된 고정된 힙 크기를 가지고 테스트하는 것은 매우 중요하다. JVM 이 힙 크기를 선택하도록 하면 고장된 힙 크기로 명확해진 성능이 모호하게 변화할 수 있다. 일반적으로 프로덕트 애플리케이션은 고정된 힙 크기에서도 잘 동작한다. -server 옵션 설정은 JVM 이 최소화 힙 크기에 대해서 좀 덜 적극적이게 될 것이고 이것은 약간의 성능 향상을 제공한다. -Xms 를 적절하게 설정하면 -server 설정만으로 제공되는 성능 이상으로 추가적인 성능을 제공하는 경우가 종종 있다. Oracle 은 서버 애플리케이션에 대해 -server, -Xms, -Xmx 설정을 권장한다. 보통 -Xms 와 -Xmx 는 같은 값으로 설정한다. 이것은 10i, 11g 그리고 12c 를 포함한 모든 Oracle JDBC 드라이버에 적용된다.

Conclusion

Oracle JDBC 드라이버는 최대 성능을 이룩하기 위해서 많은 양의 메모리를 사용할 수 있다. 만약 메모리가 여러분의 애플리케이션 성능을 제한한다면 최고 성능을 위해서 튜닝할 수 있다. 이상적으로 실제 애플리케이션 워크로드를 대표하는 재현 가능한 테스트 사례가 있어야 한다. 다양한 노브를 체계적으로 변경하고 테스트를 실행하면 최적의 설정을 식별할 수 있다. 모든 버전의 Oracle JDBC 드라이버에 대해, 묵시적 명령문 캐시 활성화, 적절한 데이터베이스 스키마 디자인, 신중한 SQL 코딩 그리고 적절한 fetch 크기 설정이 첫 단계다. 그 다음 Java 힙 크기를 대규모 실제 크기로 설정하기 위해 -Xmx 와 -Xms를 사용해 늘려라. 어떠한 값으로도 수용가능한 성능이 주어지면 -Xms 와 -Xmx 를 줄일 수 있다. 최대의 실제 힙 메모리를 가지고 성능이 원하는 것보다 낮고 10i 나 11g 드라이버를 사용 중이라면 필요에 따라 freeMemoryOnEnterImplictCache 이나 maxCachedBufferSize 를 사용해 실험해야 한다. 이것은 useThreadLocalBufferCache 를 설정해야할지 말지를 여러분의 애플리케이션 설계에서 명확하게 해준다. 만약 여러분이 모든 나머지 성능 영역을 찾고 있다면 자바 가비지 컬렉터 튜닝을 해봐야 겠지만 대부분의 경우 -Xmx 및 -Xms 설정이 필요로 하는 전부다.

MySQL 8 설치하기

MySQL 8 은 그동안의 버저닝을 버린 최초의 메이저 버전 업데이트라고 할 수 있다. 그만큼 기념할 만큼 큰 변화를 예고했던 버전이며 실제로 많은 변화가 있었다.

MySQL 8 의 설치는 5.7 과 크게 차이가 없다.

컴파일 옵션

컴파일러 옵션중에 스토리지 엔진관련해서 변경사항이 있었다. InnoDB, MyISAM, MERGE, MEMORY, CSV 엔진은 이제 기본이 됐다. 명시적으로 지정할 필요가 없다. 엔진 옵션으로 ARCHIVE, BLACKHOLE, EXAMPLE, FEDERATED 등을 선택할 수 있다.

-DMUTEX_TYPE 옵션으로 InnoDB 에 대한 뮤텍스 타입을 지정하는 건데, 기본값으로 event 다. 이를 위해서 libevent 가 필요하며 이를 지정하기 위해서 -WITH_LIBEVENT 를 해주면 된다.

MySQL 8 에서는 대부분의 라이브러리들이 bundle, system 으로 선택을 할 수있다. 안정성을 위해서 bundle 을 이용하는 걸 권장하는 듯 하지만 system 이나 별도 컴파일 한 라이브러리 사용을 위해서 지정해 줄수도 있다.

좀 더 많은 컴파일 옵션은 “2.9.4 MySQL Source-Configuration Options” 을 참조 하면 된다.

소스를 받아 컴파일할때 주의할 점은 바로 컴파일을 위한 하드디스크 공간이다. 이전 버전 보다 컴파일을 위한 하드디스크 공간을 많이 상용한다.

무려 6.6G 정도의 용량이 사용된다.

설치된 바이너리 용량 대략 1.3G 정도다.

 

설치 후 작업.

설치시에 SYSTEMD 옵션을 주었다면 이제 Systemd 를 사용할 수 있으며 MySQL 8 은 이를 위한 파일을 생성해 준다. 이 파일은 설치디렉토리에 lib/systemd/system 디렉토리에 파일이 존재한다.

심볼릭 링크를 생성해 준다.

 

계정 생성을 해줘야 하는데, 다음과 같이 해주면 된다.

 

필요한 디렉토리 생성해 준다. pid, sock 파일을 위한 run 디렉토리와 logs 디렉토리를 생성해준다. 사실 여기서 logs 디렉토리는 사용되지는 않는다.

 

소유권을 변경해준다. MySQL이 제대로 동작하기 위해서는 설치 디렉토리에 대한 소유권을 제대로 설정해 줘야 한다.

MySQL 라이브러리 시스템 인식.

데이터 디렉토리 초기화.

다음과 같이 데이터 디렉토리를 초기화 해준다.

MySQL 5.7 소스 설치

이 문서는 MySQL 5.7 소스 설치 문서 입니다.

Boost 라이브러리

MySQL 5.7 로 넘어오면서 GIS관련해 기능과 InnoDB Engine 에서 R-tree indexes 가 포함되었다. geometry compute 위한 많은 native code 들로 작성이 되었는데 이를 위해서 Boost.Geometry 를 이용했다. 따라서 소스 설치시에 이 라이브러리를 필요로한다.

Boost 라이브러리를 컴파일 단계에서 다음의 옵션으로 알려줄 수 있다.

-DWITH_BOOST: Cmake 컴파일러에게 Boost 지점를 알려준다. Boost 지점은 다음의 셋중에 하나여야 한다.

  • tarball/zip 파일
  • tarball/zip 파일을 포함하는 디렉토리
  • tarball/zip 파일을 압축해제한 디렉토리

-DDOWNLOAD_BOOST: boolean 값으로 Boost tarball/zip 파일을 자동으로 다운로드 받게 할지 말지를 결정한다.

두 옵션을 조합해서 사용하기도 한다. 우선순위는 DWITH_BOOST 이며 여기서 찾지 못할 경우에 DDOWNLOAD_BOOST 를 체크해 다운로드를 할지 말지를 결정한다.

하지만 최근의 MySQL 5.7 소스에는 boost 를 포함해서 배포한다. 따라서 DWITH_BOOST 옵션을 사용해서 포함된 boost 디렉토리를 지정해주면 된다.

준비

CentOS 가 최소설치되었다고 가정하고 시작했기 때문에 컴파일 환경을 구축해줘야 한다.

Ubuntu

Download and Unpack

Configure and Compile

cmake 를 사용하기 때문에 일반적인 configure 와는 다르다. cmake 대로 옵션을 제공한다. 다음과 같다. 보시면 무엇을 의미하는지 알수 있을 것이다.

  1. -CMAKE_INSTALL_PREFIX:PATH=/opt/mysql5.7.21
  2. DWITH_engine_STORAGE_ENGINE nor -WITHOUT_engine_STORAGE_ENGINE 사용하고자 하는 스토리지 엔진을 지정하는 옵션입니다. engine 에는 모두 대문자로 쓰며, MySQL에서 지원하는 엔진들을 적어주면 됩니다.  ex) -WITH_INNOBASE_STORAGE_ENGINE:BOOL=ON -WITHOUT_PARTITION_STORAGE_ENGINE:BOOL=ON
  3. -ENABLED_LOCAL_INFILE:BOOL=boolean SQL파일을 로드하게 해주는 기능을 켭니다.  -ENABLED_LOCAL_INFILE:BOOL=ON
  4. -WITH_EXTRA_CHARSETS:STRING=all 추가로 지원할 언어를 지정합니다. 기본값은 all 입니다. -WITH_EXTRA_CHARSETS:STRING=all
  5. -WITH_SSL:STRING=(no, yes, bundled, system) SSL 지원여부 입니다. system으로 할경우에 시스템에 설치된 SSL library를 이용하게 됩니다. SSL 관련 library가 설치되어 있어야 겠죠. -WITH_SSL:STRING=system
  6. -WITH_ZLIB:STRING=(bundled, system) system으로 할 경우에 시스템에 설치된 Library를 이용합니다. -WITH_ZLIB:STRING=system
  7. -WITH_READLINE:BOOL=boolean readline 지원여부입니다. -WITH_READLINE:BOOL=ON

이러한 옵션들은 다음과 같이 확인이 가능하다.

이제 필요한 옵션들을 다음과 같이 해준다. 그리고 컴파일하고 설치해준다.

설치후 작업

설치가 끝난 후에는 수동으로 옮겨주어야할 파일들이 존재 한다. 그래서 설치 후 작업을 다음과 같이 진행 하면 된다.

계정을 추가

심볼릭 링크를 다음과 같이 생성해 줍니다.

디렉토리 설정과 관련된 my.cnf 내용은 다음과 같다.

다음과 같이 필요한 디렉토리를 생성하고 소유권을 바꿔 준다.

MySQL 라이브러리 시스템 인식.

시스템 데이터베이스 생성.

정상적으로 끝났다면, /opt/dbstorage/mysql/logs/mysqld_error.log 파일에 임시 루트 패스워드가 기록된다. 이를 참조해서 초기 접속시에 활용해야 한다.

Systemd 스크립트 등록

Systemd 를 사용하기 위해서는 컴파일 옵션을 -DWITH_SYSTEMD=1 를 반드시 해줘야 한다. 그리고 다음과 같이 mysqld.service 파일을 작성한다.

이것을 /usr/lib/systemd/system/mysqld.service 로 저장하고 systemd 에 등록해주고 시작해 준다.

이렇게하고 제대로 구동되었는지를 살펴보면 된다.

패스워드 초기화.

MySQL 5.7 은 설치할때에 root 에 대한 임시패스워드를 발급한다. 하지만 이것은 오직 root 접속을 위한 것으로 임시패스워드 대신 다른 패스워드로 변경하기 전에는 다음과 같이 명령어가 듣질 않는다.

다음과 같이 패스워드를 변경해 준다.

 

MariaDB 10.2.13 소스 설치

MariadbMariaDB 는 MySQL 의 오픈소스 버전 입니다. MySQL 를 최초로 개발한 사람이 점점 폐쇄성이 짙어가는 MySQL 을 대체하기 위해 MySQL 을 복제하고 기능을 개선한 MySQL 의 또 다른 버전 입니다.

현재 MariaDB 는 10.2.13 버전 입니다. MariaDB 10 소스 설치를 해보겠습니다. 설치 환경은 다음과 같습니다.

  • CentOS 7
  • 64bit

준비

다음의 패키지가 설치되어 있어야 합니다.

 

다운로드 및 unpack

 

Configure and make and install

MariaDB 는 cmake 를 이용하기 때문에 일반 Configure 와는 다르게 이것을 이용 합니다.

 

설치후 작업

설치가 끝난 후에는 수동으로 옮겨주어야할 파일들이 존재 합니다. 그래서 설치 후 작업을 다음과 같이 진행 합니다.

계정을 추가 합니다.

디렉토리 설정과 관련된 my.cnf 내용은 다음과 같습니다.

다음과 같이 디렉토리를 생성하고 소유권을 바꿔 줍니다.

MariaDB 라이브러리 시스템 인식.

시스템 데이터베이스 생성.

Systemd 스크립트 등록.

user, group 을 mysql 에서 maria 로 바꿔 주고 등록해줍니다.

 

galera_recovery 파일에 mysql 를 maria 로 바꿔 줍니다.

 

다음과 같이 mariadb 를 시작해 줍니다.

 

MySQL 5.7 초기화 오류 메시지

MySQL 5.7 이 릴리즈 된지도 오래 지났다. 인기 있는 데이터베이스 시스템이라서 그런지 현장에서 많이 쓰이는 거 같다. 무엇보다도 Replication 기능의 향상이 많은 사용자를 끌어들이는 느낌이다.

이 글에서는 개인적으로 MySQL 5. 7 를 사용하면서 느낀 변화에 대해서 기술하고자 한다. 변화에 대한 기술은 MySQL 5.6 과 비교한 것이다.

데이터베이스 초기화

기존에는 mysql_install_db 를 사용했지만 이제는 mysqld 에 옵션으로 –initialize 를 주고 실행하면 시스템 데이터베이스와 테이블, innodb 저장소등을 만들어준다.

기존의 MySQL 5.6 에서 사용하는 my.cnf 파일을 가져다 초기화를 하면 위와 같이 warining 과 함께 ERROR 가 발생하면서 중단된다.

unkown variable ‘log_bin_basename’

분명히 메뉴얼에는 존재하는데도 인식이 안된다.  알수 없는 옵션이라고 하면서 데이터베이스 초기화가 안되는데 log_bin 옵션에다가 해주면 된다.

 

explicit_defaults_for_timestamp

TIMESTAMP 컬럼 데이터 타입에 대한 기본값에 대해 명시적으로 지정을 할지 말지를 결정하는 옵션. 기본은 OFF 이나 그럴 경우에 위와 같이 경고 메시지가 나온다.

TIMESTAMP 를 컬럼에서 사용할때 기본값을 명시하지 않으면 이전 버전에서는 “NOT NULL DEFAULT CURRENT_TIMSTAMP ON UPDATE CURRENT_TIMESTAMP” 가 된다. 하지만 5.7 에선 위 옵션을 이용해서 ON 으로 지정할 경우에 “NULL DEFAULT NULL” 값이 지정이 된다.

‘innodb-autoextend-increment’: unsigned value 10485760 adjusted to 1000

MySQL 5.7 에서 innodb-autoextend-increment 옵션 값은 1 ~ 1000 사이의 값이여야 한다. 단위는 MB 이여서 최고값 1000 은 1GB 가 된다. 이 경고 메시지는 10MB 라고 값을 입력함에 따라 값의 범위를 벗어나 생긴 것이다. 1 ~ 1000 사이의 값을 넣어주면 된다. 단, 단위는 생략.

이 옵션은 file-per-table 테이블스페이스 파일 이나 general 테이블 스페이스 파일에는 아무런 영향을 주지 않는다. 이러한 파일은 innodb-autoextend-increment 세팅과 상관없이 자동으로 확장된다. 초기의 확장은 소량으로 진행되지만 이후에는 4MB 씩 늘어난다.

Using innodb_support_xa is deprecated and the parameter may be removed in future releases. Only innodb_support_xa=ON is allowed

MySQL 5.7.10 버전 이후부터는 항상 활성화 된다. innodb_support_xa 를 비활성화 하면 replication 이 안전하지 않게 되고 바이너리 로그 그룹 커밋(Binary log group commit)과 관련된 성능 향상이 되지 않아 더이상 허용되지 않는다.

Using innodb_file_format is deprecated and the parameter may be removed in future releases.

5.7 이후에 기본값이 Barracuda 로 바뀌었으며 가까운 미래에 없어질 것이다. 원래 이 옵션은 5.1 버전으로 다운그레이드를 위해서 만들어졌으며 5.1 은 더 이상 지원을 하지 않게됨에 따라서 필요하지 않게 되었다.

옵션을 설정하지 않으면 된다.

MySQL USING VS ON 차이.

MySQL에서 JOIN 을 사용할때에 USING 이나 ON 을 사용한다. 결과적으로 뽑고자 하는 데이터는 모두 동일한데 과연 이둘의 차이는 무엇일까?

첫째로 사용법에서 차이가 있다. USING 은 두 테이블간 필드이름이 같은 경우에 사용한다.

employees 와 salaries 를 조인(JOIN)하는데 emp_no 를 키가 양쪽 테이블에 모두 있기 때문에 USING 을 사용할 수 있다.
하지만 만일 조인시에 컬럼 이름이 다를 경우에는 ON 을 사용한다. 물론, 컬럼 이름이 같은 것을 기반을 조인을 할때도 ON 을 상용해도 된다.

Xtrabackup 을 활용한 Replication 설정

Xtrabackup 을 이용한 Slave 추가 하기.

Master 에 Slave 를 추가해야 하는데, Online 상황에서 추가하기는 쉽지가 않다. 대부분 mysldump 를 이용해서 추가 하는 방법이나, mysql data 디렉토리를 압축해서 해제하는 방법을 쓰거나 둘중 하나이다.

하지만 이렇게하면 Online 상태를 유지할 수 없다. Online 상태를 유지면서 Master 의 데이터를 옮기는 방법으로는 Xtrabackup 을 이용하는게 제일 좋다.

먼저, Xtrabackup 툴을 Master 와 Slave 서버에 모두 설치해 준다. 그리고 Master 서버에 다음과 같이 백업을 위한 계정을 생성한다.

그리고 두가지를 생각해봐야 한다. 먼저 새롭게 준비된 Slave 에서 Master 에서 데이터를 땡겨올건지 아니면 Master 에서 Slave 로 쏴줄 것인지이다.

새로운 Slave -> Master 로 접속해서 데이터를 끌어오는 방법은 다음과 같다.

Master -> Slave 로 데이터를 쏴줄때는 다음과 같이 한다.

쏴줄때에 위와같이 하면 백업 진행중에 ssh 접속 패스워드 입력하라는 프롬프트가 나오고 지나가버립니다. 그래도 패스워드를 입력하면 됩니다.

백업 완료되면 디렉토리에 xtrabackup_binlog_info 파일 생성되고 다음과 같은 내용이 포함된다.

이 내용을 기반으로 Replication 을 할때에 활용한다.

데이터를 다 끌어오는 동안에 변경된 데이터를 적용해야하는데, 다음과 같이 한다.

그리고 다음과 같이 복원을 시행한다.

그리고 다음과 같이 리플리케이션을 걸어준다.

 

Oracle 12c 데이터베이스 삭제하기

Oracle 12c 데이터베이스 삭제하기 위해서는 다음과 같이 하면 된다.

/dev/shm 공유메모리 설정.

Oracle 12c 에서 메모리의 사용을 어떻게 할 것인가에 따라서 다음 두가지로 나뉜다.

  • AMM
  • ASMM

AMM을 사용할 경우에 init.ora 시작 파일에 MEMORY_TARGET, MEMORY_MAX_TARGET 의 값을 사용하며 ASMM 의 경우에는 SGA_TARGET, SGA_MAX_TARGET 값을 사용한다.

AMM은 SGA, PGA 내의 각종 메모리 구역을 사용하는 목적에 따라서 자동으로 조절해 항상 최대의 성능을 내도록 해준다. 이는 Oracle 11g 에서 소개된 것으로 Oracle 은 이를 적극 사용할 것을 권하고 있다. 하지만 한가지 문제가 있는데, Linux 시스템의 경우에 HugePageSize 를 사용할 수 없다.

ASMM은 SGA 만 자동으로 메모리를 관리해준다. PGA는 자동으로 해주지 않으며 이는 상황에 맞게 normal한 값을 주어야 하며 잘못 설정할 경우에 성능에 심각한 영향을 줄 수 있다. 하지만 Linux 시스템의 경우에 HugePageSize 를 사용할 수 있다.

AMM을 사용할 경우에 초점을 맞춰, 메모리 용량을 설정할 때에 Linux 시스템에 tmpfs 으로 마운트된 /dev/shm 에 Oracle 에서 사용할 메모리양 만큼 마운트를 해줘야 한다.

/dev/shm 는 다음과 같은 모습을 보인다.

/dev/shm 는 기본적으로 마운팅이 되는데, 이는 전체 물리메모리의 절반이 기본으로 된다.

왜 /dev/shm 의 크기가 중요한가?

이유는 간단하다. 이 크기만큼만 Oracle 이 사용할 수 있다. 정확하게는 AMM 기능을 사용할때에만 해당된다. 예를들어, init.ora 에 다음과 같이 /dev/shm 의 값보다 크게 설정했다고 치자.

/dev/shm 크기는 현재 4G 이다. 이 상태에서 Oracle 를 시작하면 다음과 같은 오류가 난다.

그렇다면 /dev/shm 값을 변경해줘야 한다. 어떻게 할까?

먼저, /etc/fstab 을 다음과 같이 변경한다.

그리고 /etc/fstab 를 기반으로 마운트를 전부 재마운트 하도록 한다.

위와같이 조정을 한 후에 init.ora 의 MEMORY_TARGET 를 4G 로 지정해도 정상적으로 동작하게된다.

ps, /etc/fstab 를 조작하지 않고 다음과 같이 수동으로 할 수 있다. 먼저 umount 를 다음과 같이 해준다.

그리고 다음과 같이 크기를 지정해 마운트 해주면 된다.

 

ps, 좀 더 정확하게 말하자면 /dev/shm 은 Virtual Shared Memory Filesystem 이다. Linux 는 공유 메모리를 위한 인프라가 다양한데, 그중에 하나가 파일시스템(Filesystem) 을 이용하는 것이다.

파일시스템에 데이터를 기록하면 여러 프로세스가 액세스를 할 수 있어 손쉽게 공유메모리 기능을 구현할 수 있다. 이것은 POSIX 에서 마련한 공유메모리 기법으로 보통 POSIX 공유메모리 인프라로 불리운다. 그것을 위해서 물리적 메모리의 일부를 Virtual Shared Memory Filesystem 으로 사용하는데, Linux 는 물리적 메모리의 50%를 /dev/shm 에 할당 한다.

Linux 에서 기본은 System V IPC 로 이는 커널파라메터 vm.shmmax, vm.shmall 의 값을 기준으로 공유메모리를 마련한다.

Oracle 11g 의 경우에는 POSIX 규격의 공유 메모리를 사용하기도 하고 System V IPC 규격의 공유 메모리를 설정에 따라서 사용하게 된다. AMM 기능은 POSIX 규격의 공유 메모리를, 그래서 /dev/shm 을 사용하며 이 크기에 따라서 Oracle 의 SGA+PGA 가 결정된다. 만일 AMM 기능을 사용하지 않을 경우에는 System V IPC 를 이용해야하기 때문에 커널 파라메터인 vm.shmmax, vm.shmall 값에 따라서 SGA 크기가 결정된다.

AMM 기능을 사용하면 HugePage 를 사용할 수 없다라고 하는데, 이는 당연한 것으로 HugePage 는 System V IPC 공유 메모리 인프라로 AMM 은 POSIX 규격의 공유 메모리 인프라를 사용하기 때문에 서로 호환이 될 수가 없는 것이다.

HugePage 는 System V IPC 의 공유 메모리를 구성하는 방법중에 하나로 기존의 4k 크기의 페이지를 2MB 크기의 페이지로 나뉘어진 공유 메모리를 사용하게 된다.

 

[번역] 5.6과 5.7 사이의 MySQL 기본 설정 변경.

이 글은 다음의 글을 번역한 것입니다. 전문 번역자가 아니기에 오류가 있음을 미리 밝힙니다.
https://www.percona.com/blog/2016/09/14/mysql-default-configuration-changes-between-5-6-and-5-7/

이 블로그에서, 우리는 MySQL 5.6 과 5.7 사이에 기본 설정값의 차이에 대해 논의할 것이다.

MySQL 5.7 은 여러분이 기대했던 다양한 새로운 기능이 추가되었다. 하지만, 현재 변수들 또한 알게모르게 변화가 있었다. MySQL 5.7은 5.6으로부터 거의 40개의 기본값이 변경되었다. 어떤 바뀐값들은 여러분의 서버 성능에 아주 크게 영향을 줄 것이고 어떤것은 알지도 모르체 넘어갈 거다. 나는 각각의 변화가 무엇이고 어떤 의미인지를 살펴볼 것이다.

sync_binlog 와 같은 값들은 여러분의 서버에 아주 큰 영향을 줄 수 있다. 내 동료는, Roel Van de Paar, 다른 블로그 글을 통해서 아주 상세하게 sync_binlog의 영향을 깊게 다뤘다. Sync_binlog 는 어떻게 MySQL이 binlog 를 디스크로 플러시(flush)하는지를 제어한다. 새로운 값 1은 MySQL에게 커밋(Commit) 하는 동안 모든 트랜잭션들을 디스크로 쓰도록 강제한다. 이전에는 binlog 를 플러싱하도록 강제하지 못했었고 OS가 binlog를 플러시하는 시점을 결정하는걸 그져 신뢰해야만 했다.(OS 가 binlog 를 플러시하도록 내버려뒀다.)
https://www.percona.com/blog/2016/06/03/binary-logs-make-mysql-5-7-slower-than-5-6/

Variables5.6.295.7.11
sync_binlog01

퍼포먼스 스키마 변수들은 유용하지 않은것처럼 보이지만, 많은 것들이 기본값이 -1 이다. MySQL은 자동적으로 조정되어지는 변수들을 호출하는데 이 표기법을 사용한다. 오직 performance_schema_max_file_classes 만이 (자동으로) 조정되지 않고 바뀐다. 이것은 퍼포먼스 스키마에서 사용되어지는 파일 명령어들의 숫자다. 이것은 당신이 바꿀 필요가 없는 값이다.

Variables5.6.295.7.11
performance_schema_accounts_size100-1
performance_schema_hosts_size100-1
performance_schema_max_cond_instances3504-1
performance_schema_max_file_classes5080
performance_schema_max_file_instances7693-1
performance_schema_max_mutex_instances15906-1
performance_schema_max_rwlock_instances9102-1
performance_schema_max_socket_instances322-1
performance_schema_max_statement_classes168-1
performance_schema_max_table_handles4000-1
performance_schema_max_table_instances12500-1
performance_schema_max_thread_instances402-1
performance_schema_setup_actors_size100-1
performance_schema_setup_objects_size100-1
performance_schema_users_size100-1

optimizer_switchsql_mode 변수들은 각각 활성화될수도 있고 약간 다른 행동을 유도하게할 수도 있다. MySQL 5.7 에서는 민감도와 보안이 향상시키는데 플래그를 두가지 변수값을 활용할 수 있다.(활성화하거나 다른행동을 하게하도록 값을 지정하는 행위) 이것은 추가적으로 옵티마이저에게(Optimizer) 여러분들의 쿼리가 정확하게 해석되도록 결정하는데 좀더 효율성을 제공한다.

optimzer_switch 에 추가된 세개의 플래그들, 모두 MySQL 5.6 에 존재하며, 옵티마이저의 효율성을 높이기위한 목적으로 MySQL 5.7의 기본값으로 설정되었다. duplicateweedout=on, condition_fanout_filter=on 그리고 derived_merge=on. duplicateweedout 은 옵티 마이저의 세미 조인 구체화 전략의 일부입니다. condition_fanout_filter 은 조건 필터링 사용을 제어하고 derived_merge은 derived 테이블의 머지를 제어하고 뷰를 외부쿼리 블럭으로 제어한다.

https://dev.mysql.com/doc/refman/5.7/en/switchable-optimizations.html

http://www.chriscalender.com/tag/condition_fanout_filter/

(위 설정들을) SQL mode 에 추가해도 직접적으로 성능에 영향을 주지 않지만 성능을 높일수 있는 쿼리 작성법을 개선해준다. 몇몇 주목할만한 변화로 select … group by 문에서 모든 필드를 요구하려면 SUM과 같은 함수를 사용하여 집계하거나 group by 절에 넣어야한다. MySQL은 그룹화해야 한다고 가정하지 않으며, 필드가 없는 경우 오류를 발생시킨다. Strict_trans_tables 는 트랜잭션 테이블과 함께 사용되는지에 따라 다른 효과를 발생시킵니다.

명령문은 그것이 유효하지 않거나 데이터 변경을 위한 명령문에서 값이 누락된다면 트랜잭션 테이블은 롤백되었었다. 트랜잭션 엔진을 사용하지 않는 테이블에서 MySQL은 유효하지 않은 데이터가 발생한 레코드에 의존해 행동한다. 첫번째 row 라면 트랜잭션 엔진의 동작과 똑같이 동작하진다. 만일 그렇지 않다면 유효하지 않은 값은 가장 근사한 유효한 값 혹은 컬럼의 기본값으로 변경되어진다. 경고는 발생되지만 데이터는 insert 된다.

Variables5.6.295.7.11
optimizer_switchindex_merge=on
index_merge_union=on
index_merge_sort_union=on
index_merge_intersection=on
engine_condition_pushdown=on
index_condition_pushdown=on
mrr=on,mrr_cost_based=on
block_nested_loop=on
batched_key_access=off
materialization=on, semijoin=on
loosescan=on, firstmatch=on
subquery_materialization_cost_based=on
use_index_extensions=on
index_merge=on
index_merge_union=on
index_merge_sort_union=on
index_merge_intersection=on
engine_condition_pushdown=on
index_condition_pushdown=on
mrr=on
mrr_cost_based=on
block_nested_loop=on
batched_key_access=off
materialization=on
semijoin=on
loosescan=on
firstmatch=on
duplicateweedout=on
subquery_materialization_cost_based=on
use_index_extensions=on
condition_fanout_filter=on
derived_merge=on
sql_modeNO_ENGINE_SUBSTITUTIONONLY_FULL_GROUP_BY
STRICT_TRANS_TABLES
NO_ZERO_IN_DATE
NO_ZERO_DATE
ERROR_FOR_DIVISION_BY_ZERO
NO_AUTO_CREATE_USER
NO_ENGINE_SUBSTITUTION

binlog 와 관련해 몇가지 변수가 변경되었다. MySQL 5.7 에 binlog_error_action가 업데이트 됐는데, binlog 를 쓰는중에 오류가 있다면 서버는 중단된다. 이런 일은 흔하지 않지만, 이런일이 발생하면 여러분의 애플리케이션과 리플리케이션에 큰 피해를 발생시키고 그것이 고쳐질때까지 서버는 그 어떤 추가적인 트랜잭션도 실행하지 않는다.

binlog 의 기본 포맷도 이전의 statement 대신에 ROW 로 변경되었다. Statement 는 로그에 적은 데이터를 쓴다. 하지만 많은 명령문들이, update … order by rand()을 포함해, 정확하게 복제되어지지 않았었다. 이러한 비결정적인 명령문은(non-deterministic statements) 마스터와 슬레이브에서 서로 다른 결과셋을 가진다. ROW 포맷으로 변화는 좀 더 많은 데이터를 binlog 에 쓰지만, 정보가 정확하고 올바른 복제를 보장한다.

MySQL은 전통적인 binlog 포지션 방식대신에 GTID 를 사용하는 리플리케이션에 초점을 맞추기 시작했다. MySQL이 시작하거나 재시작할때, 이전에 사용된 GTID 의 목록을 생성해야 한다. 만일 binlog_gtid_simple_recovery가 OFF거나 FALSE 라면 서버는 새로운 binlog 로 시작하고 previous_gtids_log_event 대해서 binlog 파일 검색을 거꾸로 반복한다. 만일 이것이 ON, True 라면 서버는 최신과 가장 오래된 binlog 파일을 검토하고 사용된 gtid 를 계산한다. Binlog_gtid_simple_recovery는 binlogs 를 좀 더 빠르게 파악하게 해주며, 특히 GTID 이벤트가 없는 아주 많은 수의 바이너리 로그를 빠르게 파악해준다. 하지만, 특별한 경우에 이것은 gtid_executedgtid_purged 가 잘못 채워질 수도 있다. 이것은 오직 MySQL 5.7.5 나 그 이후버전에 의해서 새로운 binlog 가 생성되어질때 발생하거나 SET GTID_PURGED 명령문이 5.7.7 보다 이전버전에서 실행될때 발생된다.

5.7에서 업데이트된 또다른 리플리케이션 기반 변수는 slave_net_timeout 이다. 이것은 60s 미만이여야 한다. 이전 버전의 리플리케이션 쓰레드는 적어도 한 시간에 문제가 발생될때까지 마스터로 연결이 깨졌다고 판단하지 않았다. 이 변화는 연결에 문제가 있다는 것을 좀더 빠르게 알려주고 여러분이 이슈를 알기전에 복제가 크게 뒤쳐지지 않도록 해준다.

Variables5.6.295.7.11
binlog_error_actionIGNORE_ERRORABORT_SERVER
binlog_formatSTATEMENTROW
binlog_gtid_simple_recoveryOFFON
slave_net_timeout360060

InnoDB 버퍼 풀 변경은 서버 시작 및 중지 시간에 영향을 준다.

innodb_buffer_pool_dump_at_shutdowninnodb_buffer_pool_load_at_startup 는 서버를 ‘warm up’ 하지 못하도록 함께 쓰인다. 이름처럼, 이것은 셧다운시에 버퍼풀 덤프를 유발하고 시작시에는 로드 된다. 비록 여러분이 100Gb 용량의 버퍼풀을 가지고 있어도, 쓰여진 데이터는 훨씬 작기 때문에 버퍼풀 용량과 똑같은 디스크 용량을 확보할 필요는 없다. 디스크에 쓰여지는 것은 실제 데이터, 테이블공간 과 페이지ID 를 찾는데 필요한 정보만이다.

Variables5.6.295.7.11
innodb_buffer_pool_dump_at_shutdownOFFON
innodb_buffer_pool_load_at_startupOFFON

이제 MySQL 은 5.6과 그 이전 버전에 InnoDB 에 구현된 몇가지 옵션들을 기본값으로 만들었다. InnoDB 의 체크섬 알고리즘을 innodb 에서 crc32로 개선했는데, 이는 최근 Intel CPU 의 하드웨어 가속 기능을 활용할 수 있는 장점이 있다.

Barracuda 파일 포맷은 5.5 이후부터 활용되기 시작해 5.6 에서 많은 개선이 이루어졌으며 이제 5.7 에서는 기본값이 되었다. Barracuda 포맷은 압축과 동적 로우 포맷을 사용할 수 있다. 내 동료 Alexey 는 압축된 포맷의 사용과 최적화 때 그가 본 결과에 대해서 글을 썼다. https://www.percona.com/blog/2008/04/23/real-life-use-case-for-barracuda-innodb-file-format/

innodb_large_prefix 는 기본값이 ‘on’ 이며 Barracuda 파일 포맷과 조합해서 사용할 경우에 좀 더 큰 index key prefix 를(3072 bytes보다 큰) 생성할 수 있다. 이것은 아주 큰 텍스트 필드의 인덱스에서 장점이 된다. 만약 이것이 ‘off’ 라면, 로우 포맷은 동적이거나 압축이 되지 않으며, 767 bytes 보다 큰 index prefix는 자동으로 잘린다. MySQL은 5.7.6에서 더 큰 InnoDB 페이지 크기 (32k 및 64k)를 도입했다.

MySQL 5.7 은 innodb_log_buffer_size 값이 충분히 커졌다. InnoDB 는 바이네리 로그에 디스크로 쓰는 동안에 로그 트랜잭션를 위한 로그 버퍼를 사용한다. 증가된 크기는 로그를 디스크로 플러쉬하는 빈도를 줄이고, I/O를 줄이며, 커밋하기 전에 디스크에 기록 할 필요없이 로그에보다 큰 트랜잭션을 저장할 수 있다.

MySQL 5.7 은 MySQL 5.5 에서 thread contention 을 줄이기 위해서 InnoDB의 퍼지 연산들을 백그라운드 쓰레드로 옮겼다. 최신 버전에서 퍼지 쓰레드는 4개로 기본값이 늘었지만 1에서 32쓰레드까지 언제든지 변경할 수 있다.

MySQL 5.7 은 이제 innodb_strict_mode 가 기본값이 됐고 몇몇 경고들은 오류로 바뀌었다. create table, alter table, create index, optimize table 명령문에서 문법 오류는 에러를 발생시키고 실행전에 사용자가 수정해야만 한다. 또 insert 나 update 구문이 선택한 페이지 크기에 비해 아주 큰 레코드 때문에 실패하지 않도록 레코드 크기를 체크할 수 있다.

Variables5.6.295.7.11
innodb_checksum_algorithminnodbcrc32
innodb_file_formatAntelopeBarracuda
innodb_file_format_maxAntelopeBarracuda
innodb_large_prefixOFFON
innodb_log_buffer_size838860816777216
innodb_purge_threads14
innodb_strict_modeOFFON

MySQL 은 equality 범위를 평가할때 옵티마이저가 index로 넘어가는 횟수를 증가시켰다. 만약 옵티마이져가 eq_range_index_dive_limit 보다 더 많이 index로 넘어가길 원한다면, MySQL 5.7 에서 기본값 200, index 통계를 사용한다. 여러분은 이것을 0부터, 인엑스 다이빙 제한, 4294967295 까지 수정할 수 있다. 이것은 테이블 통계가 무작위 샘플의 카디널리티를 기반으로 하기 때문에 쿼리 성능에 큰 영향을 줄 수 있다. 이로 인해 최적화 프로그램은 인덱스 다이빙보다 검토 할 훨씬 많은 행 집합을 평가하고, 옵티마이 저가 쿼리를 실행하기 위해 선택한 방법을 변경하게 할 수 있다.

MySQL 5.7 에서 log_warnings 은 사라졌다. 대신 log_error_verbosity 를 사용된다. 기본값은 3이고 에러로그에 errors, warnings, notes 가 기록된다. 여러분은 이것을 1(errors 만) 이나 2(errors 와 warnings) 로 변경할 수 있다. 에러 로그를 참조할때, verbosity 는 좋은 것이다. 하지만 이것은 error 로깅을 위해서 디스크 공간과 I/O를 증가 시킨다.

table_open_cache_instances 는 MySQL 5.7.8 부터 변경 되었다. instance 의 숫자는 1에서 16까지 증가된다. 이것은 두가지 장점이 있는데, DML 구문의 contention 을 줄여주고, 캐쉬 접근의 세분화 된다. instance 수를 증가시킴으로써, DML 구문은 전체 캐쉬가 아닌 하나의 instance 만 잠글수 있다. 한가지 중요한 사실은 DDL 구문은 여전히 전체 캐쉬에 잠금(lock)를 필요로 한다. 시스템에서 많은 수의 세션이 캐쉬에 접근하면 성능은 증가 된다.

Variables5.6.295.7.11
eq_range_index_dive_limit10200
log_warnings12
table_open_cache_instances116

5.7 에서 기본값에 많은 변경이 있다. 하지만 이렇게 많은 옵션들은 오랫동안 존재해왔고 사용자에게 친숙해야 한다. 많은 사람들이 이 변수들을 사용했고 그들은 MySQL을 앞으로 나가게 하는 가장 좋은 방법이다. 하지만 기억해야 할 것은, 여러분은 여전히 이러한 변수들을 편집할 수 있고 서버가 여러분의 데이터를 위해 최고 동작하도록 그들을 설정할 수 있다.