ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • ClickHouse MergeTree 알아보기
    Database/Clickhouse 2023. 11. 7. 08:10
    728x90
    반응형

    - 목차

     

    소개.

    ClickHouse 는 러시아의 검색 사이트인 Yandex 에서 개발한 Column 기반의 데이터베이스입니다.
    분석용 데이터베이스로 사용되구요. Row 기반이 아닌 Column 기반으로 데이터를 취급하는 것이 큰 특징입니다.
    흔히 로그성 데이터들이 ClickHouse 가 취급하는 데이터들입니다.
     
    로그성 데이터의 예를 들면,
    - 시스템 또는 서버의 로그 데이터
    - 서버의 상태 정보들 (CPU, Memory 사용량, Network IO 등)
    - 웹/앱 내부에서 사용자들의 행동데이터
    - IoT 센서 데이터
    등이 존재합니다.
     
    이러한 로그성 데이터들의 큰 특징은 High Volume Write 가 빈번합니다.
    즉, 데이터의 생성의 양이 매우 많습니다.
    일반적인 경우의 데이터 생성 시나리오와는 크게 다르죠.
    회원가입을 한다던지, 온라인 거래와 같은 트랜잭션 느낌의 시나리오와는 크게 다릅니다.
    이러한 데이터 처리 형식을 OLTP (Online Transaction Processing) 이라고 합니다.
     
    회원가입과 관련된 예를 하나 들어보겠습니다.
    MySQL 로 OLTP 를 구현하는 경우에 여러 데이터들이 transactional 하게 생성되어야합니다.
    사용자 정보와 사용자와 연관된 여러 데이터들이 초기화되어야하며
    (예를 들어, 사용자의 장바구니 데이터 생성이나 개인 정보를 저장할 암호화 정보들 등)
    이 과정에서 하나라도 처리가 실패되면 Rollback 되어야하죠.
    MySQL 의 Index 관점에서도 Overhead 가 존재합니다.
    Primary Key 를 다루는 B-tree Index 는 모든 데이터 하나하나의 생성에 관여하게 되죠.
    이러한 의미에서 기존의 데이터베이스와 OLTP 는 큰 용량의 데이터 생성에 적합하지 않습니다.
     
    반면 ClickHouse 의 MergeTree 는 OLAP (Online Analytical Processing) 형식의 데이터 처리에 특화되어 있습니다.
    데이터의 하나 하나를 처리하는데에 큰 비중을 들이지 않습니다.
    LSM Tree 와 같이 선 Data-Write 후 Merge or Filter 전략을 따릅니다.
     
    이어지는 글에서 ClickHouse 의 MergeTree 가 어떻게 데이터를 관리하는지 알아보도록 하겠습니다.

    ClickHouse Container 실행.

    먼저 Docker 로 ClickHouse 컨테이너를 실행하는 방법을 설명하겠습니다.
    MergeTree 에 대한 설명과 더불어 실습을 같이 진행해보려고 합니다.
    "bitnami/clickhouse" 인 오피셜 이미지를 사용할 예정입니다.
     
    < ClickHouse Docker Container 실행 >

    docker run -d \
    --name clickhouse \
    --platform linux/amd64 \
    --env CLICKHOUSE_ADMIN_PASSWORD=1234 \
    --env ALLOM_EMPTY_PASSWORD=yes \
    -p 8123:8123 \
    -p 9000:9000 \
    bitnami/clickhouse:22.3.20

     
    < ClickHouse Client 실행 >
    ClickHouse 내부에 clickhouse-client 실행 파일이 존재합니다.
    컨테이너 내부에서 해당 clickhouse-client 을 실행하여 클라이언트 인터페이스를 활성화합니다.

    docker exec -it clickhouse clickhouse-client --user default --password 1234

     
     

    MergeTree ?

    MergeTree 는 MySQL InnoDB 와 같이 데이터베이스의 스토리지 엔진입니다.
    "소개" 영역에서 설명했듯이 MergeTree 는 트랜잭션 데이터를 처리하는 것과 다릅니다.
    Transaction 처리를 하지 않구요.
    Row 들을 일일이 관리하는 Index 체계가 아닙니다.
    대신 많은 양의 데이터 생성을 수용하기 위해서 LSM Tree 와 같이 선 Data-Write 후 Merge or Filter 전략 을 따릅니다.
    이에 대해서 설명해보겠습니다.
     

    데이터 생성과 system.parts.

    먼저 테이블을 하나 생성해보겠습니다.
     
    < user_behaviors Table DDL >

    CREATE TABLE user_behaviors
    (
        `user_id` String,
        `page_location` String,
        `event_time` DateTime
    )
    ENGINE = MergeTree
    ORDER BY event_time
    SETTINGS index_granularity=32;

     
    user_behaviors 테이블은 온라인 사용자의 행동데이터를 보관하기 위한 테이블입니다.
    - user_id : 사용자 식별 정보
    - page_location : 온라인 상의 위치
    - event_time : 이벤트 발생 시각 정보
     
    그리고 사용자 행동 데이터를 하나 생성합니다.
     
    < Alice 의 행동 데이터 >

    insert into user_behaviors(user_id, page_location, event_time)
    values ('Alice', 'https://naver.com', now());

     
    그리고 system.parts 라는 시스템 테이블을 조회합니다.
    all_1_1_0 이라는 system.parts 테이블의 Row 가 하나 생성되어 있습니다.
    참고로 system.parts 이러지는 내용에서 설명하겠지만,
    Table 의 Row 들이 저장되는 File 을 의미합니다. (실제 파일입니다.)
     
    < system.parts 조회 >
     
    all_1_1_0 이라는 이름의 system.part 가 생성되었구요.
    Alice 의 행동 데이터 1개를 저장하기 때문에 rows 는 1 입니다.

    select name, rows, min_block_number, max_block_number, level, data_version
    from system.parts
    where active = 1 and table = 'user_behaviors';
    
    +---------+----+----------------+----------------+-----+------------+
    |name     |rows|min_block_number|max_block_number|level|data_version|
    +---------+----+----------------+----------------+-----+------------+
    |all_1_1_0|1   |1               |1               |0    |1           |
    +---------+----+----------------+----------------+-----+------------+

     

    추가적으로 데이터를 생성해보겠습니다.

     
    < Sequential Data Write >
     
    아래의 패턴을 보면, 데이터의 생성과 함께 system.parts 의 Row 가 계속 추가됨을 확인할 수 있습니다.
    Bob, Chris, Daniel 3명의 행동데이터를 추가합니다.
    각 사용자에 해당하는 system.parts 가 1개씩 생성됩니다.
    그리고 rows 의 값 또한 1이죠.
    즉, 데이터가 생성될 때마다 데이터를 저장하는 물리적인 파일이 1개씩 생성됩니다.
     

    insert into user_behaviors(user_id, page_location, event_time)
    values ('Bob', 'https://naver.com', now());
    
    select name, rows, min_block_number, max_block_number, level, data_version
    from system.parts
    +---------+----+----------------+----------------+-----+------------+
    |name     |rows|min_block_number|max_block_number|level|data_version|
    +---------+----+----------------+----------------+-----+------------+
    |all_1_1_0|1   |1               |1               |0    |1           |
    |all_2_2_0|1   |2               |2               |0    |2           |
    +---------+----+----------------+----------------+-----+------------+
    
    
    insert into user_behaviors(user_id, page_location, event_time)
    values ('Chris', 'https://naver.com', now());
    
    select name, rows, min_block_number, max_block_number, level, data_version
    from system.parts
    where active = 1 and table = 'user_behaviors';
    
    +---------+----+----------------+----------------+-----+------------+
    |name     |rows|min_block_number|max_block_number|level|data_version|
    +---------+----+----------------+----------------+-----+------------+
    |all_1_1_0|1   |1               |1               |0    |1           |
    |all_2_2_0|1   |2               |2               |0    |2           |
    |all_3_3_0|1   |3               |3               |0    |3           |
    +---------+----+----------------+----------------+-----+------------+
    
    
    insert into user_behaviors(user_id, page_location, event_time)
    values ('Daniel', 'https://naver.com', now());
    
    select name, rows, min_block_number, max_block_number, level, data_version
    from system.parts
    where active = 1 and table = 'user_behaviors';
    
    +---------+----+----------------+----------------+-----+------------+
    |name     |rows|min_block_number|max_block_number|level|data_version|
    +---------+----+----------------+----------------+-----+------------+
    |all_1_1_0|1   |1               |1               |0    |1           |
    |all_2_2_0|1   |2               |2               |0    |2           |
    |all_3_3_0|1   |3               |3               |0    |3           |
    |all_4_4_0|1   |4               |4               |0    |4           |
    +---------+----+----------------+----------------+-----+------------+

     
     
    < Bulk Insert >
     
    아래는 3개의 데이터를 추가하는 Bulk Insert 입니다.
    Bulk Insert 쿼리 실행 이후에 또 1개의 system.parts 테이블의 Row 가 생성됩니다.
    Emily, Fabian, Gail 의 행동 데이터를 한번에 생성합니다.
    그럼 위의 Sequential Insert 와 달리 3개의 데이터에 매칭되는 Row 하나가 생성됩니다.
    이러한 Insert 되는 데이터의 묶음 또는 단위를 Block 이라고 합니다.
    "all_5_5_0" 이라는 system.parts 테이블의 Row 가 Bulk Insert 에 매칭되는 Row 입니다.
    rows 의 값이 3개 인 것이 보이시죠 ?
    3개의 데이터가 block_number 가 5번인 block 으로 관리됩니다.

    insert into user_behaviors(user_id, page_location, event_time)
    values ('Emily', 'https://naver.com', now())
         , ('Fabian', 'https://naver.com', now())
         , ('Gail', 'https://naver.com', now());
    
    select name, rows, min_block_number, max_block_number, level, data_version
    from system.parts
    where active = 1
      and table = 'user_behaviors';
      
    +---------+----+----------------+----------------+-----+------------+
    |name     |rows|min_block_number|max_block_number|level|data_version|
    +---------+----+----------------+----------------+-----+------------+
    |all_1_1_0|1   |1               |1               |0    |1           |
    |all_2_2_0|1   |2               |2               |0    |2           |
    |all_3_3_0|1   |3               |3               |0    |3           |
    |all_4_4_0|1   |4               |4               |0    |4           |
    |all_5_5_0|3   |5               |5               |0    |5           |
    +---------+----+----------------+----------------+-----+------------+

     
     
    < optimize table >
     
    그리고 optimize table 쿼리를 실행하거나 또는 시간이 약간 흐르게 되면,
    아래처럼 system.parts 의 Row 들이 하나로 합쳐집니다.
     

    optimize table user_behaviors;
    
    select name, rows, min_block_number, max_block_number, level, data_version
    from system.parts
    where active = 1
      and table = 'user_behaviors';
      
    +---------+----+----------------+----------------+-----+------------+
    |name     |rows|min_block_number|max_block_number|level|data_version|
    +---------+----+----------------+----------------+-----+------------+
    |all_1_5_1|7   |1               |5               |1    |1           |
    +---------+----+----------------+----------------+-----+------------+

     
     

    Merge Operation.

    위 내용에서 데이터의 생성과 system.parts 테이블이 어떤 관련이 있는지 살펴보았습니다.
    그리고 system.parts 테이블을 실제 File 을 의미한다고 말씀드렸는데요.
    Table 의 Row 가 추가되면, 실제 File 이 생성됩니다.

    아래 예시가 지금까지 생성했던 파일의 정보입니다.
    "/var/lib/clickhouse/data" 디렉토리 하위에 파일들이 생성됩니다.
    "database 이름 / table 이름" 으로 이어지는 하위 경로로 내려가면,
    system.parts 테이블에서 살펴보았던 "all_x_x_x" 파일들을 확인할 수 있습니다.

    이러한 파일들을 Data Part 라고 부릅니다.
     
    < Data Part 리스트 >

    ls -al /var/lib/clickhouse/data/default/user_behaviors/
    total 40
    drwxr-x--- 9 1001 root 4096 Nov  6 22:36 .
    drwxr-x--- 3 1001 root 4096 Nov  6 22:31 ..
    drwxr-x--- 2 1001 root 4096 Nov  6 22:31 all_1_1_0
    drwxr-x--- 2 1001 root 4096 Nov  6 22:36 all_1_5_1
    drwxr-x--- 2 1001 root 4096 Nov  6 22:33 all_2_2_0
    drwxr-x--- 2 1001 root 4096 Nov  6 22:34 all_3_3_0
    drwxr-x--- 2 1001 root 4096 Nov  6 22:34 all_4_4_0
    drwxr-x--- 2 1001 root 4096 Nov  6 22:36 all_5_5_0
    drwxr-x--- 2 1001 root 4096 Nov  6 22:31 detached
    -rw-r----- 1 1001 root    1 Nov  6 22:31 format_version.txt

     
     
    그리고 시간이 흐르면 아래와 같이 1개의 파일만이 존재하게 됩니다.
     
    < 머지된 Data Part 리스트 >
    "all_1_5_1" 이라는 파일 하나만 남게되는데요.
    이것은 all_1_1_0, all_2_2_0, all_3_3_0, all_4_4_0, all_5_5_0 들이 Merge 된 결과입니다.

    ls -al /var/lib/clickhouse/data/default/user_behaviors/
    total 20
    drwxr-x--- 4 1001 root 4096 Nov  6 22:45 .
    drwxr-x--- 3 1001 root 4096 Nov  6 22:31 ..
    drwxr-x--- 2 1001 root 4096 Nov  6 22:36 all_1_5_1
    drwxr-x--- 2 1001 root 4096 Nov  6 22:31 detached
    -rw-r----- 1 1001 root    1 Nov  6 22:31 format_version.txt

     
    ClickHouse 의 MergeTree 는 많은 양의 데이터의 생성 쿼리를 받아들이고,
    Data Part 를 계속 생성합니다.
    그리고 Background 에서 Merge Operation 이 진행되죠.

     
     

    level.

    Merge Operation 은 무한정 시도되지 않습니다.
    Merge Operation 이 시도된 횟수를 level 이라고 하는데요.
     
    아래의 예시처럼 system.parts 는 level 이라는 칼럼을 가집니다.

    select name, rows, min_block_number, max_block_number, level, data_version
    from system.parts
    where active = 1
      and table = 'user_behaviors';
    +---------+----+----------------+----------------+-----+------------+
    |name     |rows|min_block_number|max_block_number|level|data_version|
    +---------+----+----------------+----------------+-----+------------+
    |all_1_5_1|7   |1               |5               |1    |1           |
    +---------+----+----------------+----------------+-----+------------+

     

    Data Part 의 Merge Level 은 1부터 15까지의 값을 가집니다.
    level 15 가 Final Level 입니다.
    Final Level 에 도달할 때까지 Merge Operation 이 계속 진행되게 됩니다.

    반응형
Designed by Tistory.