Apache Ignite로 분산 캐시 만들어보고 싶어요 (1탄)

Overview

안녕하세요. 벌써 일년이 넘는 시간동안 포스팅을 하지 않았네요.

그동안 업무적으로 많은 변화도 있던 격동의 시기였던지라, 제 삶의 가장 중요한 요소 중 하나라 생각하던 공유의 가치를 그만 간과하고 말았네요. 물론 포스팅을 멈춘 시간동안 놀지 않고 많은 경험을 쌓아보았습니다.

오늘은 그중 하나, 인메모리 데이터베이스인 Apache Ignite에 대해 이야기를 해보고자 합니다.

Apache Ignite?

이미 많은 분들이 Ignite 를 사용해보셨을 수도 있겠습니다만, 저는 DBA로 일을 하면서도 늘 제대로된 캐시 용도로 써보고 싶었던 분산 인메모리 데이터베이스가 Apache Ignite입니다.

Server 기반 클러스터링 구성도 가능하고, Application 기반 클러스터링도 가능합니다. 참고로, 이 포스팅에서 이야기할 내용은 Server 기반 클러스터링 구성이고, 인메모리가 아닌 파일 기반 분산 캐시 형태로 사용합니다. SSD로 인해 디스크 Random I/O가 과거 대비 압도적으로 향상된만큼, 캐시 데이터 또한 디스크에 저장하지 않을 이유는 없습니다.

저는 개인적으로 하단 세가지 특성을 Ignite의 가장 큰 강점이라 생각합니다.

  • Persistence Mode
  • SQL(JDBC) 지원
  • 분산 캐시
  • 스케일 인/아웃

Quick Start

도커 기반으로 로컬에서 기능적으로 간단하게 테스트해볼 수 있는 환경을 구성해봅니다.

## 도커에서 사용한 데이터 영역 마운트를 위해 디렉토리 생성
$ mkdir -p $HOME/ignite/storage

## persistenceEnabled만 들어간 설정을 생성하기
$ echo '<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="ignite.cfg" class="org.apache.ignite.configuration.IgniteConfiguration">
        <property name="dataStorageConfiguration">
            <bean class="org.apache.ignite.configuration.DataStorageConfiguration">
                <property name="defaultDataRegionConfiguration">
                    <bean class="org.apache.ignite.configuration.DataRegionConfiguration">
                        <property name="persistenceEnabled" value="true"/>
                    </bean>
                </property>
            </bean>
        </property>
    </bean>
</beans>' > $HOME/ignite/storage/config.xml

## 도커 시작
$ docker run -d                        \
  -p "10800:10800"                   \
  -v $HOME/ignite/storage:/storage   \
  -e IGNITE_WORK_DIR=/storage        \
  -e CONFIG_URI=/storage/config.xml  \
  --name ignite                      \
  apacheignite/ignite

## 클러스터를 ACTIVE로 전환
$ docker exec ignite bash -c 'yes | /opt/ignite/apache-ignite/bin/control.sh --set-state ACTIVE'
Warning: the command will change state of cluster with name "fc4636efa81-50c9c9bd-a798-46a0-992b-c821b80f623f" to ACTIVE.
Press 'y' to continue . . . Control utility [ver. 2.15.0#20230425-sha1:f98f7f35]
2023 Copyright(C) Apache Software Foundation
User: root
Time: 2023-10-05T05:50:44.866
Command [SET-STATE] started
Arguments: --set-state ACTIVE
--------------------------------------------------------------------------------
Cluster state changed to ACTIVE.
Command [SET-STATE] finished with code: 0
Control utility has completed execution at: 2023-10-05T05:50:45.445
Execution time: 579 ms

## 클러스터 상태 확인
$ docker exec ignite bash -c 'yes | /opt/ignite/apache-ignite/bin/control.sh --state'
Control utility [ver. 2.15.0#20230425-sha1:f98f7f35]
2023 Copyright(C) Apache Software Foundation
User: root
Time: 2023-10-05T05:51:54.598
Command [STATE] started
Arguments: --state
--------------------------------------------------------------------------------
Cluster  ID: e6f53c11-62a4-4fec-8259-ae2a43ce29d5
Cluster tag: busy_burnell
--------------------------------------------------------------------------------
Cluster is active
Command [STATE] finished with code: 0
Control utility has completed execution at: 2023-10-05T05:51:54.902
Execution time: 304 ms

이제, 간단한 기능테스트를 해볼 수 있는 환경을 구성하였습니다.

How to use?

JAVA thin client로 캐시로 사용하는 방법과, Ignite JDBC를 통해 SQL데이터를 처리할 수 있는 방안 두 가지를 간단하게 살펴보겠습니다. maven 기준 하단 의존성이 필요합니다.

<dependencies>
    <dependency>
        <groupId>org.apache.ignite</groupId>
        <artifactId>ignite-core</artifactId>
        <version>2.15.0</version>
    </dependency>
</dependencies>

1. Using with java-thin-client

단순하게 캐시를 하나 만들고, put/get/count 정도를 해주는 로직입니다. 하단은 단순한 데이터 처리처럼 보이지만, 실제 운영 환경에서 초당 수만건 처리도 별다른 CPU 리소스 사용 없이도 너끈히 가능합니다. 개인적인 선호도에 따라, 오퍼레이션 실패 시 Retry 기능도 적절하게 구현해서 첨가한다면 더욱 강력한 프로그램이 되겠죠? ^^

import org.apache.ignite.Ignition;
import org.apache.ignite.cache.CachePeekMode;
import org.apache.ignite.client.ClientCache;
import org.apache.ignite.client.ClientCacheConfiguration;
import org.apache.ignite.client.IgniteClient;
import org.apache.ignite.configuration.ClientConfiguration;

import java.util.HashMap;
import java.util.HashSet;

import static org.apache.ignite.cache.CacheWriteSynchronizationMode.FULL_SYNC;

public class IgniteThinClientTest {

  public static void main(String[] args) {
    final ClientConfiguration igniteConfiguration = new ClientConfiguration()
        .setAddresses("127.0.0.1:10800")
        .setPartitionAwarenessEnabled(true)
        .setTimeout(5000);
    final IgniteClient igniteClient = Ignition.startClient(igniteConfiguration);

    final String cacheName = "ignite:cache";
    ClientCacheConfiguration igniteCacheConfiguration = new ClientCacheConfiguration();
    igniteCacheConfiguration.setWriteSynchronizationMode(FULL_SYNC);
    igniteCacheConfiguration.setBackups(1);
    igniteCacheConfiguration.setName(cacheName);
    igniteCacheConfiguration.setStatisticsEnabled(true);
    igniteClient.getOrCreateCache(igniteCacheConfiguration);

    ClientCache<String, String> cache = igniteClient.cache(cacheName);
    cache.put("key1", "abcde1");
    System.out.println("key1=>"+cache.get("key1"));
    System.out.println("Size=>"+cache.size(CachePeekMode.ALL));

    cache.putAll(new HashMap<String, String>() {{
      put("key2", "abcde2");
      put("key3", "abcde3");
      put("key4", "abcde4");
    }});
    System.out.println("GetAll=>"+
        cache.getAll(new HashSet<String>() {{
          add("key2");
          add("key3");
          add("key4");
        }})
    );
    System.out.println("Size=>"+cache.size(CachePeekMode.ALL));

    cache.remove("key1");
    System.out.println("Size=>"+cache.size(CachePeekMode.ALL));

    cache.clear();
    System.out.println("Size=>"+cache.size(CachePeekMode.ALL));

    igniteClient.close();
  }
}

<결과>

key1=>abcde1
Size=>1
GetAll=>{key2=abcde2, key3=abcde3, key4=abcde4}
Size=>4
Size=>3
Size=>0

추가로 하단을 읽어보시면, 나름 트랜잭션과 같은 고급 기능도 있으니, 참고하시면 좋겠습니다. (빙산의 일각만 소개함)
참고) https://ignite.apache.org/docs/latest/thin-clients/java-thin-client

2. Using with JDBC

SQL기반으로 데이터 처리하는 영역은 갈수록 광범위해지고 있습니다. 특히나, JAVA에서는 JDBC라는 강력한 인터페이스를 제공하며, 이를 잘 구현해놓으면 다양한 데이터처리를 SQL로 쉽게 접근하여 처리 가능합니다. Ignite에서도 JDBC 드라이버를 제공하고, 이를 기반으로 SQL로 데이터 질의가 가능합니다.

구현 자체는 기존 SQL 서버들과 붙는 것과 크게 다르지 않습니다. 단지, UPSERT 구문 및 각 데이터베이스별로 제공하는 일부 특이한 쿼리 패턴을 제외하고는.. ^^

import java.sql.*;

public class IgniteJDBCTest {

  public static void main(String[] args) throws ClassNotFoundException, SQLException {

    Class.forName("org.apache.ignite.IgniteJdbcThinDriver");
    Connection conn = DriverManager.getConnection("jdbc:ignite:thin://127.0.0.1:10800");
    Statement stmt = conn.createStatement();
    stmt.execute("create table if not exists person (\n"
                     + "  name varchar, \n"
                     + "  details varchar, \n"
                     + "  primary key(name)\n"
                     + ") with \"backups=1\"");
    stmt.execute("MERGE INTO person (name, details) VALUES ('key1', 'abcde1')");
    stmt.execute("MERGE INTO person (name, details) VALUES ('key2', 'abcde2')");
    stmt.execute("MERGE INTO person (name, details) VALUES ('key3', 'abcde3')");
    stmt.execute("MERGE INTO person (name, details) VALUES ('key4', 'abcde4')");
    {
      ResultSet rs = stmt.executeQuery("select * from person");
      while (rs.next()) {
        System.out.println(rs.getString(1) + "=>" + rs.getString(2));
      }
      rs.close();
    }
    {
      ResultSet rs = stmt.executeQuery("select count(*) from person");
      while (rs.next()) {
        System.out.println("Total=>" + rs.getString(1));
      }
      rs.close();
    }
    stmt.close();
    conn.close();
  }
}

<결과>

key1=>abcde1
key2=>abcde2
key3=>abcde3
key4=>abcde4
Total=>4

참고) https://ignite.apache.org/docs/latest/SQL/JDBC/jdbc-driver

Conclusion

오늘 이 포스팅에서는 Apache Ignite에 대한 간단한 기술 맛보기 정도만 소개를 했습니다. 물론, Ignite 혹은 Gridgain(상용버전)을 서비스에서 잘 사용하고 있는 분들도 분명 많을 것입니다. 일부는 Ignite에 대해 효율성을 공감하실 분도 계실 것이고, 일부는 아직은 아리송한 생각을 가지고 계실 수도 있습니다.

앞서 이야기를 했었지만, 저는 개인적으로 Apache Ignite에 대해서 하단 정도를 가장 큰 강점으로 뽑고 싶습니다. 물론 Ignite가 인메모리데이터이고, 다양한 기능을 제공해주기는 하지만, (개인적인 생각으로는) 결과적으로는 캐시서버일 분, 그 이상의 의미를 부여할 필요는 없다고 봅니다.

  • Persistence Mode
  • SQL(JDBC) 지원
  • 분산 캐시
  • 스케일 인/아웃

다음 포스팅에서는 위에서 소개를 했던 두가지 내용에 이어서, 분산 캐시 구성 방법에 추가로 모니터링 구성 방안을 이야기해보도록 하겠습니다. 아, 추가로, Thin Client 와 JDBC의 두 가지 퍼포먼스를 간단하게 로컬에서 테스트를 해서 공유를 하는 것도 좋겠네요.

감사합니다.

MySQL binlog파서와 memcached plugin의 콜라보레이션!

Overview

6개월도 훌쩍 넘은 시간에. 간만에 포스팅합니다. 그동안 OGG javaue든, MySQL Binlog파서든.. 흐르는 데이터를 핸들링하는 고민으로 하루하루를 지내왔던 것 같아요. 그러던 중 이전 포스팅에서 주제로 삼았던, InnoDB memcached plugin을 Binlog parsing을 통해 데이터를 맞추면 좋을 것 같다는 생각이 들었습니다.
오늘 이 자리에서는 이런 답답함을 극복하고자, Binlog 이벤트를 활용하여, 최신 데이터를 유지시키는 방안에 대해서 이야기를 해보도록 하겠습니다.

MySQL Binary log?

MySQL에서 데이터복제를 위해서는 Binnary Log(binlog)를 쓰게 되는데, 이중 ROW 포멧으로 만들어지는 이벤트를 활용하여 다양한 데이터 핸들링이 가능합니다.
file

ROW Event는 특정 테이블에 대한 정보를 알려주는 Table map event가 우선 선행하고, 해당 테이블과 연계된 데이터가 뒤따르게 됩니다. 참고로, (InnoDB에서) 트랜잭션 처리가 이루어지면 아래와 같은 이벤트로 바이너리 로그에 기록이 됩니다.

[SQL Event] begin;
[TABLE MAP Event] tableA
  [WRITE ROW Event] data[]
  [WRITE ROW Event] data[]
  [WRITE ROW Event] data[]
[TABLE MAP Event] tableB
  [UPDATE ROW Event]  <data[], data[]>
  [UPDATE ROW Event] < data[], data[]>
[XID Event]

그리고, MySQL에서는 binlog_row_image 에 따라 전체데이터(FULL)로 기록할 것인지, 최소한의 정보(MINIMAL)만 기록할 것인지 설정할 수 있습니다.

"FULL" binlog_row_image

변경 이전/이후 모든 데이터를 기록하게 되는데, 많은 정보가 포함되어 있는만큼 다양한 핸들링을 해볼수 있습니다만.. 바이너리 로그 사이즈가 지나치게 커질 수 있다는 리스크가 있습니다.
게시판 조회수를 예로 들자면.. 게시물내용과, 게시물조회수가 동일한 테이블에 있는 상황이라면, 단순히 조회수 1이 증가할 뿐인데.. 이에 대한 바이너리 로그에는 게시물내용까지 포함되어 기록되는 결과로 이어집니다. (잘나가는 사이트에서는 환장할지도 몰라용.)

"MINIMAL" binlog_row_image

변경된 데이터와 PK 데이터만 포함하는.. 정말 최소한의 데이터만 바이너리로그에 기록합니다. 이렇게되면, 로그 사이즈 리스크에서는 자유로워졌지만.. 자유로운 데이터 핸들링에서는 아무래도 제약이 있을 수밖에 없습니다.

참고로, FULL이든 MINIMAL이든, 어떤 칼럼들이 변경되었는지에 대한 정보를 BIT SET로 묶어서 TABLE MAP Event에 포함시켜 전달을 하죠. MINIMAL 이미지 에서도 제대로 처리하기 위해서는 이녀석을 잘 보고, 정말로 변경된 칼럼을 잘 선별해야 합니다. 🙂

Binlog parser & Memcached plugin

자! Binlog 포멧에 대한 구구절절한 이야기는 이정도로 하고, MySQL Row format의 이런 특성을 통해서 무엇을 상상해볼 수있을 지 이야기를 해보도록 하겠습니다. (바로 아래와 같은 상상?)
file

이런 구조에서 고민을 해야할 것은 두가지 정도라고 생각하는데요.

1. DDL 작업
기존 서비스DB에, Memcached plugin 설정 시 칼럼 맵핑 작업을 통해, 여러 칼럼들을 묶어서 memcache protocal에 실어나를 수 있습니다만.. InnoDB plugin으로 인한 대상 테이블에 대한 DDL 제약이 생각보다 많이 생기게 됩니다.

2. 최신 캐시 데이터
캐시서비스를 운영한다면, 가장 최신의 데이터를 캐시에 유지하는 것이라고 포인트라고 개인적으로 생각합니다.

그런데, ROW format에는 어떤 경우는 PK에 대한 정보는 반드시 포함이 되어 있다고 했죠! 그렇다면, 위 그림과 같이, Binlog 이벤트를 받으면, 전달받은 PK를 기반으로 소스DB에서 데이터를 SELECT하고, 결과를 JSON으로 변환해서 타겟DB에 덮어쓰는 것은 어떨까요?
스키마로 묶여있던 칼럼들을 JSON으로 변환(스키마리스)해서 넣기 때문에, DDL 작업으로부터 자유롭습니다. 그리고, 데이터 변경 시 소스에서 가장 최신의 데이터로 적용하기 때문에, 데이터 일관성 측면에서도 유리합니다. (Binlog에 저장된 순으로 데이터를 기록하게 되면, 결과적으로 변경되는 과정이 사용자에게 노출될 수도 있겠죠?)

Binlog이벤트를 받아서, 최신 데이터를 JSON형태로 유지를 시키면, 어플리케이션에서는 그냥 memcache protocal을 통해 데이터를 GET만 하면, 굉장히 빠른 속도로 데이터 처리가 가능해지는 것이죠.

참고로 MySQL InnoDB memcached 플러그인에 대한 내용은 예전에 시리즈로 포스팅을 한 적이 있습니다. (그냥 참고삼아.ㅎ)

uldra-binlog-json-transfer

최근 밤에 잠이 잘 안와서, 간단하게 구현해보았습니다. 개인적으로 CDC관련된 자체 아류작이 워낙 많다보니.. 이제는 하루이틀 집중하면, 간단한 처리 구현은 아예 찍어내고 마네요. -_-;;
https://github.com/gywndi/uldra-binlog-json-transfer
file

Conclusion

MySQL Binlog 이벤트 처리를 활용하여 Memcached 데이터를 잘 응용해볼 수 있는 방안에 대해서 정리해봤습니다. 그리고, 고민의 결과는 이미 위에서 깃헙에 공유를 해놓았고요. ^^

uldra-binlog-json-transfer로 만든 내용은 아래와 같습니다.
1. MySQL Binlog parser
2. PK로 소스 테이블에서 데이터 SELECT
3. 결과를 JSON으로 변환하여 타겟DB에 저장

프로토타입 수준으로 만들어놓은 프로젝트이기에.. 지적질 환영이고, 의견 교류 너무 좋아하고 환장합니다. 🙂

다들 코로나 멋지게 극복하세요.

트위터의 새로운 분산 관리 라이브러리 Gizzard를 소개합니다.

Overview

바로 이전 “하루 2.5억 트윗을 저장하는 트위터의 새로운 저장 스토어” 포스팅에서 트위터의 새로운 저장 스토어에 관해서 전반적으로 설명 드렸는데요, 이번에는 그 중 Gizzard에 관해서 심층 분석(?)을 해볼까합니다.

Gizzard는 트위터에서 데이터를 분산 및 복제 관리하기 위한 자체 개발 프레임워크입니다. 클라이언트와 서버 사이에 위치하며 모든 데이터 요청을 처리하는 구조입니다. Gizzard 관련 몇 가지 키워드는 아래와 같습니다.

  1. 분산 관리(Sharding)
    – 분할(Partitioning)
    – 복제(Replication)
  2. 부하분산(Load-Balancing)
  3. 장애복구(Fail-Over)
  4. 멱등성(idempotent) / 가환성(commutative)
    – 멱등성 : 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질
    – 가환성 : 연산의 순서를 바꾸어도 그 결과가 변하지 않는 일

분산 관리(Sharding)이란?

과거에는 서비스 성능 저하가 발생하면 곧바로 해당 서버에 CPU또는 Memory 사이즈를 증설하여 성능 이슈를 해결하였습니다. 하지만, 최근 Web 서비스에서 데이터 사이즈가 급증하여, 더 이상은 서버 성능 고도화만으로는 한계가 있기 때문에, 다수 장비에 데이터를 분산 위치(Data Sharding)하여 데이터를 처리하는 움직임이 일반화되고 있습니다.

Scale up VS Scale out
Scale up VS Scale out

분산 처리는 Hadoop, Cassandra, MongoDB 등 NOSQL 분야에서 유행처럼 번져나가고 있으며, 최근에는 기존 RDBMS 를 활용한 분산 관리 기법도 방안이 모색되고 있습니다. 사실 RDBMS의 대표적인 선두 주자인 Oracle도 예전부터 Grid Control 원칙 하에 데이터 분산 관리에 초점을 맞추고 있습니다.

데이터 분산 즉 Data Sharding이란 데이터를 조각으로 관리하자는 것인데,  Sharding을 크게 Partitioning(분할)Replication(복제) 두 가지로 나눕니다.

“Partitioning”은 데이터를 조각화하여 다수 서버에 위치하는 것입니다. 물론 각각의 데이터 조각들은 서버에 저장할 수 있도록 충분히 작아야 하고, 데이터를 효과적으로 관리하고 질의가 가능해야 하겠죠.

“Replication”은 여러 서버에 데이터 복사본을 위치하는 것입니다. 이 기술은 각각의 장비에서 동작하고 질의 처리를 할 수 있으며, 데이터 복사본 추가만으로 “READ” 관련 트래픽을 효과적으로 처리할 수 있습니다. 또한 다른 Node 어딘가에는 복사본 데이터가 존재하기 때문에, 특정 데이터에 장애가 발생하거나 유실이 되어도 쉽게 데이터 복구를 할 수 있다는 이점이 있습니다.

Gizzard란 무엇일까요?

Gizzard는 데이터를 분할하여 관리하는 프레임워크입니다. 트위터에서 자체 개발한 또 다른 분산 데이터 저장소이고, 데이터를 쉽게 구성하고 관리하기 위한 미들웨어입니다. 장애 발생에 유연하게 대처할 수 있고, 분산 데이터를 쉽게 관리할 수 있으며, Scala기반으로 만들어져있기 때문에 Config가 쉽다고 합니다.(저는 아직 Scala를 써보지는 않아서.. 써보고 말씀드릴께요.^^)

사실 많은 오픈 소스 분산 데이터 관리 시스템이 많이 나오고는 있지만, 효과적으로 데이터 구조를 정의하고 시스템 장애에 유연하게 대처하거나 데이터 일관성 유지 문제를 해결하는데에는 많은 어려움이 있는 것은 사실입니다.

Gizzard Architect
Gizzard Architect

Gizzard는 트위터에 최적화된 오픈 소스 분산 관리 프레임워크입니다. 물론 앞선 문제를 모든 시스템 상황과 사용자 요구사항을 충족하지는 못하지만, 대부분 데이터 저장소에 존재하는 문제점을 “어느정도” 유연하게 대처합니다.

Gizzard는 데이터를 분할/복제 관리하고, 네트워크 상에서 어플리케이션(PHP/JAVA/Ruby)와 저장소(MySQL/ Lucene/Redis) 사이에 위치합니다.

Partitioning 구성 정보는 데이터 저장 위치를 정의하는 선행 맵핑 테이블에 저장이 됩니다. 그리고 Partition 내부적으로 데이터 조각에 관한 Replication(복제)를 유지하는데, 이것은 “Replicate Tree”로 제어됩니다. 또한 쉬운 Migration과 Load-balancing이 가능하고, 장애 시 유연하게 대처합니다. 멱등성과 가환성 원칙에 의해서 복제 데이터 간 일관성을 유지한다는 특징도 있습니다.

Gizzard를 자세히 살펴봅시다.

  • Gizzard는 네트워크 서비스 미들웨어입니다.
    클라이언트(PHP/Ruby/JAVA)와 데이터 저장소(Partitioning과 Replication으로 구성) 사이에 위치하고 모든 데이터 질의는 오직 Gizzard를 통해서 이루어집니다. 특별한 인스턴스는 형태가 없는 구조이고,  JVM위에서 실행되며, 상당히 효율적입니다. 트위터에서 사용하고 있는 Gizzard는 머신 당 초당 10,000 쿼리를 수용할 수 있다고 합니다.
  • Gizzard는 어떤 종류의 데이터 저장소에도 적용할 수 있습니다.
    Gizzard는 애초에 네트워크를 통해서 데이터 저장소에 복제하도록 디자인되었습니다. 그렇기 때문에 데이터 저장소를 SQL Server, Lucene, Redis 등 어떠한 솔루션을 선택해도 큰 문제가 없습니다. 그리고 Gizzard는 모든 쓰기 작업을 “멱등성” 및 “가환성”의 원칙에 의거하여 동작하기 때문에, 저장소 자체에 관한 제약을 걸지는 않습니다. 즉, 저장 시스템 자체에 존재하는 제약 사항에는 여전히 의존한다고 볼 수 있습니다. 예를 들어 RDBMS 에서 Unique 조건이 있는 경우 Gizzard에서는 정상적으로 Writing을 시도하나, 데이터베이스 내부에서는  제약사항에 걸려 데이터 입력이 막히게 됩니다.
    Gizzard에 관한 중요한 사실은 순차적으로 데이터가 적용된다는 것을 보장하지 않는다는 것입니다. 그렇기 때문에 쓰기 연산 적용 순서와 관계없이 데이터 일관성을 유지할 수 있도록  설계하는 것이 가장 중요합니다.
  • Gizzard는 데이터 맵핑 선행 테이블에서 데이터 조각을 관리합니다.
    Gizzard는 특정 Shard를 관리를 데이터 범위 관리를 통해서 수행합니다. 선행 테이블의 맵핑 정보 안에는 특정 데이터가 어떤 범위의 구역에 저장이 되어 있는 것에 대한 정보가 숫자 범위로 저장이 되어 있습니다.

    Gizzard Partitioning
    Gizzard Partitioning

    조금 더 자세하게 풀자면, 특정 데이터에 관한 키 값을 Hash 함수로 돌리고 결과 값을 Gizzard에 전달하면, Gizzard는 해당 데이터가 어떤 구역에 속할 지 숫자 정보를 생성합니다. 이러한 함수는 프로그래밍 가능하기 때문에 사용자 구미에 따라서 지역성 혹은 균형성 면에서 최적화할 수 있습니다.

  • Gizzard는 “Replication Tree”로 복제 데이터를 관리합니다.
    선행 테이블에서 지칭하고 있는 각 데이터 조각들은 물리 혹인 논리적인 형태로 구현될 수 있습니다. 물리적인 형태는 특정 데이터 저장소를 의미하고, 논리적인 형태는 데이터 조각에 관한 Tree를 의미합니다. 트리 안의 각 Branch들은 데이터의 논리적인 변형을 나타내고, 각 Node들은 데이터 저장소를 의미합니다. 예를 들어서 아래와 같이 두 단계의 “Replicating Tree”가 있습니다. 하지만 이것들은 선행 테이블에 지칭을 해서 관리되는 오직 하나의 파티션에 저장될 뿐입니다.

    Gizzard Replication
    Gizzard Replication

    위 그림에서 Replicate라는 Branch 역할은 간단합니다. 단지 자신의 모든 자식 Node에 쓰기만 반복하고, 읽기 균형을 유지한다는 단순한 전략 하에 동작합니다. 데이터 저장소 요구사항에 맞게 추가적인 트랜잭션 혹은 Node 개수 전략을 추가하여 데이터 조각을 재구성할 수 있습니다. 그러나 Gizzard는 기본적으로 Replicating, Write-Only, Read-Only, Blocked 등 몇 가지 전략을 제공합니다.
    복제 구성에 관한 세부 형상은 Partition에 따라 다양합니다. 자주 사용되는 데이터에는 더욱 많은 복제를 추가하면 될 것이고, 간헐적으로 사용되는 데이터에는 적은 데이터 복제를 가질 수 있도록 유도하면 됩니다. 한 데이터에 관해서 Primary/Secondary/Tertiary 유지할 수도 있고, 복제를 유지하지 않아도 되는 데이터인 경우에는 Striping Partitioning 구성하여 복제를 하나도 유지하지 않을 수도 있습니다.

  • Gizzard는 장애 상황에 강한 시스템입니다.
    분산 시스템에서 장애 대처는 가장 큰 이슈입니다. 다수 컴퓨터가 동시에 동작하는 구조이기 때문에, 특정 장비에서 예기치 않게 언제든지 장애가 발생할 수 있습니다. Gizzard는 어떠한 장애 상황에서도 유연하게 대처할 수 있도록 디자인되었습니다. 만약 특정 파티션 내에 있는 복제 데이터가 유실되었을 지라도, Gizzard는 읽기/쓰기 요청을 남은 다른 정상적인 복제 데이터로 전환합니다.

    Gizzard Failover
    Gizzard Failover

    위와 같이, 데이터 조각 안의 “특정 복제 데이터 불능”이 발생하면  Gizzard는 최대한 빠르게 다른 정상적인 복제 데이터에 읽기/쓰기 작업을 시도할 것입니다. 그리고 문제가 발생했던 복제 데이터가 정상으로 돌아오면, 변경 이력을 다시 적용을 합니다. 여기서 가장 기본적인 원칙은 트랜잭션 단위 기록을 견고하게 구현한다는 것입니다. 모든 데이터 복제는 비 동기로 이루어지겠지만, 최대한 지연이 없이 진행됩니다.

    Gizzard Failover
    Gizzard Failover

    만약 “모든 복제 데이터 불능 상태”이 빠지면 해당 데이터에만 읽기 요청이 불가할 뿐 다른 정상적인 데이터에는 영향을 미치지 않습니다. 데이터 조각 안의 모든 복제 데이터가 불능에 빠지면, 일단은 “Error Queue”라는 곳에 상태를 기록하고 버퍼에 변경 내용을 쌓습니다. 그리고 파티션 정상화가 되면, 변경된 데이터를 다시 적용합니다. 최종 데이터 일관성은 “멱등성”과 “가환성” 원칙 하에 유지되기 때문에 이전 실패가 적용되기 전에 최근 변경 사항이 먼저 적용된다고 하더라도 결과적으로는 아무런 문제가 없습니다.

  • Gizzard는 데이터 이관 방안을 제시합니다.
    경우에 따라서 데이터를 다른 장비로 데이터 이관 작업이 필요합니다. 로드발란싱을 위한 경우도 있고, 데이터가 적은 장비로 옮기는 경우도 있으며, 하드웨어 장애로 인한 경우도 있습니다. Gizzard에서 논리적인 Shard들을 다른 장비로 이관하는 방식을 설명하겠습니다.

    Gizzard Data Migration
    Gizzard Data Migration

    Original에서 Target로 이관한다고 가정했을 때, “Replicate Shard”가 Original 와 Target 사이에, “WriteOnly Shard”는 Target 앞에 위치할 것입니다. 그리고 Original 에서 Target으로 데이터가 복사됩니다. WriteOnly Shard는 Target이 사용 가능한 시점까지 유지하며, 전체 데이터가 복사되기 전까지 Target에서는 어떠한 데이터 조회도 불가합니다. 쓰기 작업이 순서에 관계없이 중복으로 발생할 수 있기 때문에 모든 쓰기 작업은 반드시 멱등성 및 가환성 원칙 하에 동작해야 합니다.

  • 데이터 일관성은 멱등성 및 가환성 원칙 하에 유지합니다.
    동일 데이터를 동시에 변경하려고 하면 데이터 충돌(Confliction) 이 발생합니다. Gizzard는 데이터는 적용 순서를 보장하지 않기 때문에 모델링 시 반드시 이러한 점을 염두 해야 합니다.

    Gizzard Concurrent
    Gizzard Concurrency

    높은 가용성과 데이터가 저장되는 순서를 보장하는 것보다 훨씬 간단한 방식입니다. 예를 들어, Rowz에서는 Timestamp를 기준으로 새로운 데이터 변경 요청 여부를 구분합니다.

타 시스템과 비교

  • GIZZARD Replicate VS MySQL Replication
    MySQL Replication과 가장 큰 차이는 Gizzard는 데이터가 저장되는 순서를 보장하지 않는다는 점입니다. 모든 데이터 일관성은 멱등성 및 가환성에 의거하에 보장되며, 일시적인 데이터 불일치는 허용합니다. 물론 일시적인 데이터 불일치를 허용한다는 점은 MySQL Replication도 마찬가지지만, 데이터를 순차적으로 적용한다는 점은 확연하게 다릅니다. 만약 데이터가 순차적으로 수행되다가 실패를 한다면, 그 이후 적용되어야하는 데이터는 실패했던 연산 처리 후 처리될 수 있습니다.
    Gizzard VS MySQL
  • GIZZARD VS Cassandra
    트위터에서 사용하고 있는 두 데이터 저장 스토어를 간단하게 비교해보았습니다. Gizzard는 데이터 일관성을 멱등성/가환성에 의거하여 유지합니다. 하지만 Cassandra는 데이터 일관성을 Gossip 통신과 Timestamp로 최신 데이터 결정한다는 가장 큰 차이가 있습니다. Cassandra에서 CorrencyLevel 설정 값에 따라서 동시성 레벨을 조정할 수 있는데, 서비스 특성 및 사용자 요구 사항에 따라서 다양하게 설정할 수 있습니다.
    Gizzard VS Cassandra

마치며..

최근 이슈가 되는 대용량 데이터 시스템을 보면 공통적인 부분이 데이터 분산 처리입니다. 이제는 과거 기가바이트 단위가 아닌 테라바이트 단위로 데이터가 증가하기 때문에, 분산 처리에 관한 기술은 지속적인 습득이 필요하겠네요. “하루 2.5억 트윗을 저장하는 트위터의 새로운 저장 스토어” 포스팅을 작성을 하면서 많은 생각을 하게 만든 새로운 경험이었습니다.^^

읽어주셔서 감사합니다.

참고자료)
How Twitter Stores 250 Million Tweets A Day Using MySQL
Gizzard: a library for creating distributed datastores