[Test] 데이터베이스 PK 전략 - TSID vs UUID 벤치마크 테스트

neo-the-cow·2024년 4월 18일
0

이것저것 테스트

목록 보기
2/2
post-thumbnail

0. 들어가기 전에

  • 스프링 프로젝트를 진행하며 에포크 기반 랜덤 값을 리턴하는 Tsid 라이브러리를 사용하다가 궁금한 점을 테스트를 통해 확인했습니다.
  • UUID와 비교했을때 어떤 장점이 있을까?라는 질문을 가지고 테스트 했습니다.
  • 모든 테스트 코드는 깃허브에서 확인하실 수 있습니다.

1. 테스트

  • UUIDTSID의 생성, Set 삽입 연산의 처리 속도를 비교합니다.
  • 동시 요청 수는 각각 256, 512, 1,024, 4,096, 8,192, 100,000, 300,000, 500,000회로 고정합니다.
  • f4b6a3/tsid-creator 라이브러리를 사용했습니다.

build.gradle 의존성 추가

implementation 'com.github.f4b6a3:tsid-creator:5.2.6'

1.1 테스트 코드

  • TSID 생성 테스트
package org.example.benchmark;

import com.github.f4b6a3.tsid.Tsid;
import com.github.f4b6a3.tsid.TsidFactory;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TsidGenerateTest {

    @ParameterizedTest
    @ValueSource(ints = {256, 512, 1_024, 4_096, 8_192, 100_000, 300_000, 500_000})
    void test(int numbersOfRequests) throws InterruptedException {
        ExecutorService service = Executors.newCachedThreadPool();
        List<Callable<Tsid>> tasks = getTsidCallables(numbersOfRequests);

        long start = System.currentTimeMillis();
        service.invokeAll(tasks);

        ResultWriter.write("generate", System.currentTimeMillis() - start);
    }

    private List<Callable<Tsid>> getTsidCallables(int numbersOfRequests) {
        List<Callable<Tsid>> callables = new ArrayList<>();
        TsidFactory factory = TsidFactory.newInstance256();
        while (callables.size() < numbersOfRequests) {
            callables.add(factory::create);
        }
        return callables;
    }

}
  • UUID 생성 테스트
package org.example.benchmark;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class UuidGenerateTest {

    @ParameterizedTest
    @ValueSource(ints = {256, 512, 1_024, 4_096, 8_192, 100_000, 300_000, 500_000})
    void test(int numbersOfRequests) throws InterruptedException {
        ExecutorService service = Executors.newCachedThreadPool();
        List<Callable<UUID>> tasks = getUuidCallables(numbersOfRequests);

        long start = System.currentTimeMillis();
        service.invokeAll(tasks);

        ResultWriter.write("generate", System.currentTimeMillis() - start);
    }

    private List<Callable<UUID>> getUuidCallables(int numbersOfRequests) {
        List<Callable<UUID>> callables = new ArrayList<>();
        while (callables.size() < numbersOfRequests) {
            callables.add(UUID::randomUUID);
        }
        return callables;
    }

}
  • TSID Set 삽입 테스트
package org.example.benchmark;

import com.github.f4b6a3.tsid.TsidFactory;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TsidSetInsertTest {

    @ParameterizedTest
    @ValueSource(ints = {256, 512, 1_024, 4_096, 8_192, 100_000, 300_000, 500_000})
    void test(int numbersOfRequests) throws InterruptedException {
        ExecutorService service = Executors.newCachedThreadPool();
        List<Long> tsids = getIds(numbersOfRequests);
        Set<Long> idSet = new TreeSet<>();
        List<Callable<Boolean>> tasks = getTasks(tsids, idSet);

        long start = System.currentTimeMillis();
        service.invokeAll(tasks);

        ResultWriter.write("set-insert", System.currentTimeMillis() - start);
    }

    private List<Long> getIds(int numbersOfRequests) {
        List<Long> tsids = new ArrayList<>();
        TsidFactory factory = TsidFactory.newInstance256();
        while (tsids.size() < numbersOfRequests) {
            tsids.add(factory.create().toLong());
        }
        return tsids;
    }

    private List<Callable<Boolean>> getTasks(List<Long> ids, Set<Long> idSet) {
        List<Callable<Boolean>> tasks = new ArrayList<>();
        ids.forEach(id -> tasks.add(() -> {
            synchronized (idSet) {
                return idSet.add(id);
            }
        }));
        return tasks;
    }

}
  • UUID Set 삽입 테스트
package org.example.benchmark;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class UuidSetInsertTest {

    @ParameterizedTest
    @ValueSource(ints = {256, 512, 1_024, 4_096, 8_192, 100_000, 300_000, 500_000})
    void test(int numbersOfRequests) throws InterruptedException {
        ExecutorService service = Executors.newCachedThreadPool();
        List<String> uuids = getIds(numbersOfRequests);
        Set<String> idSet = new TreeSet<>();
        List<Callable<Boolean>> tasks = getTasks(uuids, idSet);

        long start = System.currentTimeMillis();
        service.invokeAll(tasks);

        ResultWriter.write("set-insert", System.currentTimeMillis() - start);
    }

    private List<String> getIds(int numbersOfRequests) {
        List<String> uuids = new ArrayList<>();
        while (uuids.size() < numbersOfRequests) {
            uuids.add(UUID.randomUUID().toString());
        }
        return uuids;
    }

    private List<Callable<Boolean>> getTasks(List<String> ids, Set<String> idSet) {
        List<Callable<Boolean>> tasks = new ArrayList<>();
        ids.forEach(id -> tasks.add(() -> {
            synchronized (idSet) {
                return idSet.add(id);
            }
        }));
        return tasks;
    }

}
  • .csv 파일 생성
package org.example.benchmark;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class ResultWriter {

    public static void write(String fileName, long duration) {
        String path = String.format("src/test/resources/results/%s.csv", fileName);
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(path, true))) {
            writer.append(String.format(",%s", duration));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

}
  • 테스트 자동 반복, .csv 파일 포매팅 스크립트
# 생성 테스트
#!/bin/bash

for i in {1..10}; do
  echo "$i 회차 실행..."
  echo "test$i,256,512,1_024,4_096,8_192,100_000,300_000,500_000" >> src/test/resources/results/generate.csv
  echo -n "tsid" >> src/test/resources/results/generate.csv
  ./gradlew test --tests "org.example.benchmark.TsidGenerateTest"
  echo -e -n "\nuuid" >> src/test/resources/results/generate.csv
  ./gradlew test --tests "org.example.benchmark.UuidGenerateTest"
  echo -e "\n" >> src/test/resources/results/generate.csv
done

-----------------------------------------------------------------------
# Set 삽입 테스트
#!/bin/bash

for i in {1..10}; do
  echo "$i 회차 실행..."
  echo "test$i,256,512,1_024,4_096,8_192,100_000,300_000,500_000" >> src/test/resources/results/set-insert.csv
  echo -n "tsid" >> src/test/resources/results/set-insert.csv
  ./gradlew test --tests "org.example.benchmark.TsidSetInsertTest"
  echo -e -n "\nuuid" >> src/test/resources/results/set-insert.csv
  ./gradlew test --tests "org.example.benchmark.UuidSetInsertTest"
  echo -e "\n" >> src/test/resources/results/set-insert.csv
done
  • 테스트 결과를 .csv 파일로 출력하고 엑셀 차트에 불러와 보기 편하게 만들어 봅니다.

2. 결과

2.1 테스트 결과 지표

공유 스프레드시트 링크 - 생성 테스트
공유 스프레드시트 링크 - Set 삽입 테스트
결과 차트를 만들어 표와 그래프로 나타낸 문서입니다.

  • 생성 테스트

  • Set 삽입 테스트

  • 생성 테스트Set 삽입 테스트에서 모두 평균 소요시간이 UUID 대비 50% 이하로 유의미한 차이를 확인 할 수 있습니다.

2.2 그밖에 또 다른 차이가 있을까?

  • TSIDUUID 모두 유니크한 식별자 값을 할당할 수 있지만 다음과 같은 차이점이 있습니다.

저장공간의 효율성

  • 관계형 데이터베이스 MySQL을 사용할 때, UUID 타입은 BINARY(16) 자료형을 사용하고 TSID 타입은 BIGINT 자료형을 사용합니다.
  • 이 때, BINARY(16)128bit, BIGINT64bit를 차지해 I/O작업과 저장공간에서 효율성을 기대할 수 있습니다.

비교, 범위 연산 처리

  • 바이너리 데이터는 일반적으로 정수형 데이터에 비해 복잡한 비교, 범위 연산 처리를 수행하기 때문에 BIGINT 자료형을 사용하는 TSID가 조회 쿼리 수행시에도 더 나은 성능을 기대할 수 있습니다.

4. 맺음말

TSIDUUID의 생성, Set 삽입 연산 중 한꺼번에 많은 작업이 집중될 때 소요시간을 비교해봤습니다.
테스트코드, 과정에서 이상한점, 문제점 등 이슈가 발견되면 댓글이나 이메일로 알려주세요. :)

profile
Hi. I'm Neo

0개의 댓글