본문 바로가기
FrameWork/Spring

[Java] Id 생성전략(UUID 선택 및 테스트) & spring JPA Bulk insert

by 계범 2023. 5. 5.

 

 

대량의 데이터를 복사해줘야하는 일이 생겼는데,

현재 상황으로는 성능이 좋지 않아 bulk insert를 통해 성능 개선하고자 한다.

 

현재 서비스의 구조 및 현황

더보기
  • Spring Boot 2.6.6
  • hikari CP를 통해 db 연결 관리 (파라미터 스토어를 통해 db 정보 주입)
  • MariaDB 10.6.8
    • ID 채번 전략 : identity
  • 멀티 모듈
    • Entity 모듈을 nexus에 올리고, api 모듈에서 gradle로 라이브러리 추가하는 형태로 사용
  • 멀티 테넌시 구조
    • 공용 스키마와 각 회사들의 스키마로 구분되어 사용

 

bulk Insert ID 전략 선택

더보기

Id 생성 전략

  1. IDENTITY(Auto increment): 데이터베이스에서 자동으로 ID 값을 생성하는 전략
  2. SEQUENCE: 데이터베이스에서 시퀀스 값을 생성하여 ID 값으로 사용하는 전략.
  3. TABLE: 별도의 ID 값을 관리하는 테이블을 만들어서 그 테이블에서 ID 값을 생성하는 전략
  4. UUID: 무작위로 생성된 UUID 값을 ID 값으로 사용하는 전략

identity는 bulk insert 시 1개씩 id 생성을 db에 맡겨야하기에 성능 제약이 있고
sequence는 다량의 시퀀스 값을 받아와서 처리가 가능하나, 지정된 시퀀스 중 사용 개수만큼 사용되고 나머진 버려지게 된다.(지정된 개수만큼 처리되는 방식)

table은 데이터 삽입 마다 해당 테이블에 접근하여 값을 갱신 해야하기 때문에, 대량 데이터 삽입 시 문제된다.

그래서 최종 선택은 UUID

  • UUID는 32글자의 고유한 식별자를 생성하는 방식이다.
  • 모든 테이블, 모든 서버 등에서 고유하기에 scale out 시 좋다.
  • identity는 순서대로 생성되기에 id를 통해 유추 및 악의적인 사용이 가능하지만, uuid는 불가능하다.(보안 강점)
  • 32글자이기에, 데이터 크기가 크고 인덱스도 커지게 된다. ( 용량 이슈 )
  • 글자 형태로 저장되어있기에, maria db의 인덱스 스캔 능력이 떨어지게 된다. ( 성능 이슈 )

 

UUID 단점 해결 방법

더보기

UUID 단점을 해결 하기 위해 두가지를 실행하려고 한다.

 

1 ) Char 기반 → binary 기반으로 변경

UUID는 16진수의 숫자로 16개 총 32글자의 문자열을 가진다.

(maria db에서 char는 1글자 당 1byte 총 32byte)

 

char 기반으로 저장하게 되면 2가지의 단점이 존재하는데,

일단 용량을 32byte 차지하게 되므로 long(bigInt 8byte)기반 auto_increment에 비해 4배 가까이 차지하게 된다.

또한 maria db 같은 rdbms는 대체적으로 정수타입의 id에 최적화 되어있기때문에 속도도 떨어지게 된다.

 

해당 char → binary로 저장하게되면서 2가지 다 챙길 수 있게 된다!

16진수의 숫자들을 binary로 변환하여 db에 저장하면 된다.

 

char -> binary 변환 과정

UUID : 8d9ddca4-df64-11ed-a6fe-e3fbd4698a8f

하이픈 제거 후 16진수로 2글자씩

8d 9d dc a4 df 64 11 ed a6 fe e3 fb d4 69 8a 8f

해당 16진수 binary 변환

10001101 10011101 11011100 10100100 11011111 01100100 00010001 11101101 10100110 11111110 11100011 11111011 11010100 01101001 10001010 10001111

1개당 1bit 8개면 1byte 총 16byte로 변환

 

2) random UUID → time based UUID 로 변경

maria DB의 기본 Engine인 InnoDB에선 pk에 클러스터링 인덱스가 자동 생성되고 pk 값에 의한 데이터 레코드가 물리적으로 정렬된다.

 

그렇기 때문에 새로운 데이터가 들어오게 되면, 재정렬 과정이 일어나게 되는데 해당 과정을 줄이기 위해 시간 기반의 uuid를 사용하고자 한다.

ns 기반의 시간과 random한 난수를 합쳐서 uuid를 뽑을 예정이기에, 중복 가능성은 현저히 낫다고 판단된다.

 

UUID 적용

더보기

build.gradle 추가

// UUID
implementation 'com.github.f4b6a3:uuid-creator:5.3.2'

entity

public class TestTimeBasedUuid {

    @Id
    @Column(length = 16)
    private UUID sn;

    @Column(nullable = false)
    private String name;

    @PrePersist
    public void createSn() {
        this.sn = UuidCreator.getTimeOrdered();
    }

}
  • @PrePersist를 통해 데이터베이스 저장 전에 sn을 자동으로 삽입될 수 있게 처리

https://github.com/f4b6a3/uuid-creator

 

사용할 UUID 자세하게 뜯어보기

더보기

Time Based UUID (v1)

시간 기반의 UUID (Universally Unique Identifier)는 일반적으로 UUID의 버전 1을 의미합니다. 시간 기반의 UUID는 여러 구성 요소로 이루어져 있으며, 각각 다음과 같은 의미를 가지고 있습니다.

  1. Timestamp (60 bits): UUID 생성 시점의 타임스탬프입니다. 1582년 10월 15일부터 100나노초 단위로 표현되며, 총 60비트로 구성되어 있습니다. 이 값은 3400년까지 유니크한 값을 보장할 수 있습니다.
  2. Clock Sequence (14 bits): 시스템 클럭이 변경될 때마다 증가하는 값입니다. 이 값은 14비트로 구성되어 있으며, 임의로 설정할 수도 있고 현재 시간을 기반으로 설정할 수도 있습니다. 이 값의 목적은 동일한 시간에 여러 UUID가 생성되는 것을 방지하는 것입니다.
  3. Node (48 bits): 일반적으로 MAC 주소를 사용하여 시스템의 물리적 주소를 나타냅니다. 이 값은 48비트로 구성되어 있으며, 물리적 주소가 없는 경우에는 랜덤한 값으로 설정할 수 있습니다. 이 값은 UUID 생성에 참여하는 모든 시스템에서 유니크한 값을 보장하는데 사용됩니다.

시간 기반 UUID의 형식은 다음과 같습니다:

time_low - time_mid - time_high_and_version - clock_seq_and_reserved - node

이 구조에서 time_low, time_mid, time_high_and_version은 Timestamp를 나타내며, clock_seq_and_reserved는 Clock Sequence를 나타냅니다. 마지막으로 node는 노드 값을 나타냅니다.

node(mac 주소) 값을 넣어주지 않으면, random 난수로 지정된다.

 

Ordered UUID(v6)

Time Based UUID를 재정렬하여 최적화한 것.

time_high|time_mid|time_low_and_version|clk_seq_hi_res|clk_seq_low|node

 

이외에도 다양한 uuid 버전이 있지만, 공식문서 바탕인 v1을 선택해서 사용한다.

https://datatracker.ietf.org/doc/html/rfc4122

 

bulk insert를 위한 라이브러리

더보기

id 전략을 변경하더라도 1개씩 쿼리문을 날린다면 네트워크 트래픽과 Disk I/O 늘리게 되므로,

bulk insert를 지원해주는 라이브러리를 사용해야한다.

 

대표적으로 spring Batch, spring JDBC, JPA Batch Insert, Jooq 등등이 있지만,

가장 간단하게 적용할 수 있는 Hibernate에서 지원하는 JDBC Batch insert를 사용하려고 한다.

(saveAll로 bulk insert가 된다! 별도의 코드 작성 X)

아래와 같이 정의만 하면 된다.

 

application.yml

spring:
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 50
        order_inserts: true
        order_updates: true


코드 상 적용은 entityFactory 내 setProperties로 적용하면 된다.

properties.put(AvailableSettings.STATEMENT_BATCH_SIZE, 500);
properties.put(AvailableSettings.ORDER_INSERTS, true);
properties.put(AvailableSettings.ORDER_UPDATES, true);

방언 설정을 하라는 글도 있는데, 우린 자동으로 잘 먹는다.

Using dialect: org.hibernate.dialect.MariaDB106Dialect

GPT 설명 글

  1. spring.jpa.properties.hibernate.jdbc.batch_size: 이 속성은 한 번의 JDBC batch insert 작업에 삽입될 레코드 수를 설정합니다. 이 경우, batch_size가 50으로 설정되어 있으므로 한 번의 batch 작업에서 최대 50개의 레코드가 삽입됩니다. 이 값을 적절히 조절하면 성능과 리소스 사용량을 최적화할 수 있습니다.
  2. spring.jpa.properties.hibernate.order_inserts: 이 속성은 Hibernate가 같은 타입의 엔티티를 batch insert 작업에 순서대로 그룹화하도록 설정합니다. 이렇게 하면 같은 타입의 엔티티가 연속적으로 처리되어 batch insert 작업의 성능이 향상됩니다. 이 속성을 true로 설정하면 삽입 작업이 순서대로 정렬되어 성능이 향상됩니다.
  3. spring.jpa.properties.hibernate.order_updates: 이 속성은 Hibernate가 같은 타입의 엔티티를 batch update 작업에 순서대로 그룹화하도록 설정합니다. 이렇게 하면 같은 타입의 엔티티가 연속적으로 처리되어 batch update 작업의 성능이 향상됩니다. 이 속성을 true로 설정하면 업데이트 작업이 순서대로 정렬되어 성능이 향상됩니다.
 

 

각 종 테스트 진행

더보기

 

test_identity 테이블

identity
 

uuid 기반 테이블

uuid
 
time_based_uuid
time 기반 생성 후 정렬 uuid
 
 

테스트 조건 및 확인사항

  • api 테스트
  • batchsize = 500
  • order_inserts=true
  • local환경 maria db 테스트
  • 10번 실행 평균 값으로 표기

 

테스트 확인사항

  • api 실행속도
  • query 실행속도
  • 데이터의 메모리 사용량
  • api 실행에 따른 메모리,cpu 사용량은 이번엔 체크하지 않음

 

batch size 적용. 100,000개 데이터 삽입.

목록
identity
random uuid
time based uuid(v1)
sequential uuid(v6)
api 응답 시간 평균(s)
91.50
5.45
5.27
4.92
query 대기 시간(s)
33.27
3.85
3.68
3.53
data size(kb)
40508.50
78465.0
47725.27
49,785.25

 

batch size 적용. 100만개 데이터 풀스캔.

목록
identity
random uuid
time based uuid(v1)
sequential uuid(v6)
api 응답 시간 평균(s)
3.82
4.08
3.74
3.79
query 대기 시간(s)
0.22
0.27
0.27
0.23

https://developer111.tistory.com/83

https://www.percona.com/blog/store-uuid-optimized-way/#crayon-60fa2fbab27f7557869434

블로그들에선 정렬된 uuid와 random uuid의 select 상황에서 큰 차이가 벌어졌는데,
직접 테스트 진행해본 결과는 큰 차이는 없었다..

(테이블의 컬럼을 너무 적게 설정해서 그런가..?!)

time-based-uuid와 sequential-uuid도 차이가 적은데,
sequential-uuid의 경우 바뀌는 데이터가 더 뒤로 가기때문에 차이가 좀 있을거라고 생각했지만 큰 차이가 없었다.

최종적으로 선택은 RFC-4122 공식 문서에 정의된 time based uuid(v1) 을 사용하기로 했다.


추가적으로 해당 라이브러리에서 테스트했던 내용들 공유.

https://github.com/f4b6a3/uuid-creator/wiki/5.0.-Benchmark

https://datatracker.ietf.org/doc/html/rfc4122 RFC-4122 문서

 

성능 확인 명령어

더보기

performance_schema 키는 법

SET GLOBAL performance_schema = 'ON';

이렇게 하면 켜져 있는 상태에서 킬 수 있지만, 재시작 시 꺼지므로 서버 구성 파일 내에서도 키는 코드를 넣어야함.

dv 환경에 한하여 데브옵스팀에 요청하여 켰다. ㅎㅎ

추가로 사용할 옵션 같은 건 따로 넣어줘야한다.

로컬에선 C:\Program Files\MariaDB 10.6\data 해당 경로의 my.ini 파일을 수정한 후 재부팅으로 켜줬다.

[mysqld]
datadir=C:/Program Files/MariaDB 10.6/data
port=3306
innodb_buffer_pool_size=4055M
character-set-server=utf8
// 퍼포먼스 스키마 사용 설정
performance_schema=ON
// history 사용을 위한 설정
performance_schema_events_statements_history_size=300000
[client]
port=3306
plugin-dir=C:\Program Files\MariaDB 10.6/lib/plugin

DB 설정 확인

SELECT * FROM performance_schema.setup_consumers; # 실제 해당 설정들 켜져있는 지 확인
SHOW VARIABLES LIKE 'performance_schema_%'; # 사이즈 확인
# 관련 명령어 켜주기(키고 싶은거만 키고 싶으면 like 문 대신 넣어줄 것)
UPDATE performance_schema.setup_consumers SET ENABLED = 'YES' WHERE name like 'events_statements_%';

쿼리 실행 속도

SELECT
    THREAD_ID,
    LEFT(DIGEST_TEXT, 100) AS QUERY,
    MIN(TIMER_START),
    MAX(TIMER_END),
    (MAX(TIMER_END) -  MIN(TIMER_START)) / 1000000000000 AS EXECUTION_TIME_S
FROM performance_schema.events_statements_history_long
WHERE EVENT_NAME = 'statement/sql/insert'
GROUP BY THREAD_ID
ORDER BY EXECUTION_TIME_S DESC;

쿼리 대기 시간

SELECT
    schema_name,
    DIGEST_TEXT AS query,
    COUNT_STAR AS exec_count,
    SUM_TIMER_WAIT / 1000000000000 AS total_latency_s,
    AVG_TIMER_WAIT / 1000000000000 AS avg_latency_s,
    MAX_TIMER_WAIT / 1000000000000 AS max_latency_s,
    MIN_TIMER_WAIT / 1000000000000 AS min_latency_s
FROM performance_schema.events_statements_summary_by_digest
WHERE schema_name = 'tenant-khb1122'
ORDER BY total_latency_s DESC
LIMIT 100;
 
  • exec_count: 쿼리 실행 횟수
  • total_latency_s: 쿼리의 총 대기 시간(초)
  • avg_latency_s: 쿼리의 평균 대기 시간(초)
  • max_latency_s: 쿼리의 최대 대기 시간(초)
  • min_latency_s: 쿼리의 최소 대기 시간(초)

테이블 데이터 크기

SHOW TABLE STATUS FROM `tenant-khb1122` WHERE name = 'test_uuid';
 
  • Name: 테이블 이름
  • Engine: 테이블에 사용된 스토리지 엔진(예: InnoDB, MyISAM)
  • Version: 테이블 정의 파일의 버전
  • Row_format: 행 데이터 저장 형식 (예: Compact, Dynamic, Fixed, Compressed)
  • Rows: 테이블의 행 수 추정치. 이 값은 스토리지 엔진에 따라 정확하지 않을 수 있음
  • Avg_row_length: 평균 행 길이. (Data_length / Rows)
  • Data_length: 테이블의 데이터 크기 (바이트 단위)
  • Max_data_length: 테이블의 최대 데이터 크기 (바이트 단위)
  • Index_length: 인덱스 크기 (바이트 단위)
  • Data_free: 테이블의 미사용 공간 (바이트 단위)
  • Auto_increment: 다음 AUTO_INCREMENT 값
  • Create_time: 테이블이 생성된 날짜 및 시간

참조 사이트

https://dev.mysql.com/doc/refman/8.0/en/performance-schema-events-statements-current-table.html

https://chat.openai.com/

https://midasitweb-jira.atlassian.net/wiki/spaces/tech/pages/3610476558
https://developer111.tistory.com/83

https://www.percona.com/blog/store-uuid-optimized-way/#crayon-60fa2fbab27f7557869434

https://github.com/f4b6a3/uuid-creator

https://datatracker.ietf.org/doc/html/rfc4122

댓글