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 Heatwave를 살펴보았습니다

Overview

안녕하세요. 너무 오랜만에 글을 올려봅니다. 올해도 벌써 반이 훌쩍지나버렸네요.

MySQL을 쓰시는 분들, 아니 RDBMS를 써오시던 분들의 가장 가려운 부분은 개인적으로 통계 쿼리 수행 속도라고 봅니다. 특히나 데이터 사이즈가 하루가 다르게 폭발적으로 증가해가는 상황에서 너무나도 반가운 소식이라고 봅니다. HTAP(Hybrid transactional/analytical processing) 구현이라 하는데..

오늘 이 포스팅에서는 서비스 활용 관점으로 Heatwave를 이야기해보도록 하겠습니다.

MySQL Heatwave는?

Oracle Cloud에서 제공하는 OLAP 분산 클러스터로, MySQL의 InnoDB데이터를 자동으로 Heatwave 클러스터로 동기화하여, 제공하는 스토리지 엔진 플러그인입니다.

Content is described in the surrounding text.

그림을 보면. 결국 HeatWave는 HeatWave Cluster와 HeatWave storage engine 두 가지로 분류해볼 수 있겠네요. 마치. MySQL ndb cluster와 같이. 🙂

  • HeatWave storage engine
    • 스토리지 엔진(SECONDARY 엔진)으로, MySQL서버와 HeatWave Cluster간 데이터의 통신 역할
    • 사용자의 쿼리가 실행되는 이것을 Heatwave쪽으로 Query pushdown 하여 쿼리를 실행
  • HeatWave Cluster
    • 데이터를 저장 및 프로세싱하는 분산 노드의 집합.
    • Oracle cloud 내부에 구성된 HeatWave Storage 레이어에 데이터에 저장.
    • InnoDB 데이터와 다른 저장소에 저장이 됨.

MySQL을 통해 들어온 쿼리를 HeatWave Storage 엔진을 통해 HeatWave Cluster에 Query pushdown하여 전달하여 분산 컴퓨팅을 한 후 쿼리 결과를 사용자에게 다시 제공을 해주는?? 기본적으로 모든 데이터 변경은 InnoDB Only라는 측면에서는 다른듯 하네요. 구조상으로는 ndb cluster와 유사한것 같은데.. 트랜잭션 상으로는 미묘하게 다른 느낌이 오네요.

MySQL InnoDB 스토리지 엔진과 직접적으로 연계하여 데이터를 처리해볼 수 있는 분석 용도의 분산 Column-Oriented Database 이라고 봐도 무관해보이네요.

Using HeatWave

거창하게 별도의 테이블을 만들어서 진행한다기 보다는.. 아래와 같이 ALTER 구문을 통해서 설정을 해볼 수 있다 하네요.

## 64KB 넘는 컬럼 제외
mysql> ALTER TABLE orders MODIFY description BLOB NOT SECONDARY;

## SECONDARY_ENGINE 엔진 지정
mysql> ALTER TABLE orders SECONDARY_ENGINE = RAPID;

## 데이터 복사(InnoDB->Heatwave)
mysql> ALTER TABLE orders SECONDARY_LOAD;

이 일련의 과정을 거치면, 이후부터는 서비스에서 사용하는 InnoDB변경 내역이 백그라운드에서 자연스럽게 Heatwave 데이터노드로 동기화 될 것이고, 이 내용을 기반으로 바로 OLAP쿼리를 수행해볼 수 있습니다. 만약 Heatwave에서 지원하는 쿼리만 사용한다고 했을 시에.. 사용자는 분석을 위해 별도의 데이터소스를 통하지 않고도, MySQL 데이터 소스 하나만으로도 원하는 결과를 깔끔(?)하게 도출해볼 수 있겠습니다.

## secondary 사용할래~ (Heatwave 사용할래)
SET SESSION use_secondary_engine=ON;

## secondary 사용할래~ (InnoDB에서 데이터 처리할래)
SET SESSION use_secondary_engine=OFF;

HeatWave에서 데이터를 제거하고 싶으면, 아래와 같이 SECONDARY_UNLOAD를 실행하면 됩니다.

깔끔하게 테이블도 그냥 InnoDB로만 변경하고자 한다면, 아래와 같이 SECONDARY_ENGINE을 NULL로 지정해야 합니다. 만약 Truncate 혹은 DDL작업이 필요하다면. 반드시 SECONDARY_ENGINE을 NULL로 변경해야 작업 수행할 수 있습니다.

## HeatWave Cluster에서 데이터 제거
mysql> ALTER TABLE orders SECONDARY_UNLOAD;

## SECONDARY_ENGINE 사용 비활성화
mysql> ALTER TABLE orders SECONDARY_ENGINE = NULL;

Workload Optimization

Heatwave에서는 데이터를 저장하는 방식(Encoding)과 인덱싱(Placement Key)을 하는 두가지 방법으로 워크로드에 맞는 설정을 해볼 수 있습니다. 기본적으로 모든 옵션은 각 칼럼들의 COMMENT내용을 활용하고 있으며. 대소문자를 구분하기에, 반드시 모두 대문자로 명시를 해야한다고 하네요. ex) COMMENT 'RAPID_COLUMN=ENCODING=VARLEN'

1. Encoding

Variable-length Encoding, Dictionary encoding 두가지 타입을 제공하며. 각각 다른 특성을 가집니다.

1.1. Variable-length Encoding

기본 인코딩 타입으로, NULL을 허용하며, 문자열 컬럼 저장에 효율적이라고 합니다.

MySQL에서 제공해주는 캐릭터셋을 지원하며, 무엇보다 데이터 처리 시 MySQL의 메모리가 크게 필요치 않기 때문에 자원 활용이 좋다고 하네요. Group by, Join, Limit, Order by 등을 지원합니다.

ALTER TABLE orders 
  MODIFY `O_COMMENT` VARCHAR(79) COLLATE utf8mb4_bin NOT NULL 
  COMMENT 'RAPID_COLUMN=ENCODING=VARLEN';

1.2. Dictionary encoding (SORTED)

데이터 가짓수(Distinct)가 적은 스트링 저장에 효율적이라 합니다.

예를 들면.. 코드와 같은? 메모리 사용을 MySQL 엔진 쪽을 활용하기 때문에. 관련 리소스 소모가 있을 수 있다고 하고.. 특성만 보면. B-map 인덱싱과 왠지 유사한 느낌적인 느낌도.. ^^

Join, Like 등을 비롯한 문자열 연관 오퍼레이션에 제약이 있다고 합니다. 마찬가지로, 컬럼 레벨로 아래와 같이 코멘트에 인코딩 옵션을 넣어서 설정해볼 수 있겠습니다.

ALTER TABLE orders 
  MODIFY `O_COMMENT` VARCHAR(79) COLLATE utf8mb4_bin NOT NULL 
  COMMENT 'RAPID_COLUMN=ENCODING=SORTED';

2. Data Placement Keys

기본적으로 PK 기반으로 데이터가 분산되어 관리되지만.. 성능 또는 기타 목적을 위해 별도로 생성하는 키라고 되어있고. 마치 여러개의 Clustering Key를 설정하는 듯한 개념으로 보이네요. Variable-length 인코딩만 지원합니다.테이블당 1~16까지 지정할 수 있으며. 하나의 숫자는 하나의 컬럼에만 할당 가능합니다. (복합 인덱싱 불가!)

ALTER TABLE orders 
  MODIFY date DATE COMMENT 'RAPID_COLUMN=DATA_PLACEMENT_KEY=1',
  MODIFY price FLOAT COMMENT 'RAPID_COLUMN=DATA_PLACEMENT_KEY=2';

지금까지 나름 메뉴얼 기반으로 HeatWave에 대해서 퀵하게 훑어보았습니다. 그러나, 역시 직접 해보는 것이 특성을 알아볼 수 있는 제대로된 접근이기에. 개인적으로 의문나는 부분 위주로 몇가지 테스트해보았습니다.

Setup HeatWave

앞에서 이야기를 한 것처럼, HeatWave는 Oracle Cloud에서만 사용할 수 있습니다. 테스트를 위해, 일단 가입을 하고 무료 테스트 코인을 받아서 아래와 같이 HeatWave 용도의 데이터베이스를 생성해봅니다.

생성을 완료했을지라도, 정작 해당 스토리지 엔진이 존재하지 않는데. 아직 HeatWave 클러스터를 생성하지 않았기 때문이죠.

 mysql> show engines;
+--------------------+---------+--------------+------+------------+
| Engine             | Support | Transactions | XA   | Savepoints |
+--------------------+---------+--------------+------+------------+
| FEDERATED          | NO      | NULL         | NULL | NULL       |
| MEMORY             | YES     | NO           | NO   | NO         |
| InnoDB             | DEFAULT | YES          | YES  | YES        |
| PERFORMANCE_SCHEMA | YES     | NO           | NO   | NO         |
| MyISAM             | YES     | NO           | NO   | NO         |
| MRG_MYISAM         | YES     | NO           | NO   | NO         |
| BLACKHOLE          | YES     | NO           | NO   | NO         |
| CSV                | YES     | NO           | NO   | NO         |
| ARCHIVE            | YES     | NO           | NO   | NO         |
+--------------------+---------+--------------+------+------------+

MySQL 인스턴스를 누르고, Heatwave 클러스터 구성을 하면. (위 위치에 메뉴가 있음) 클러스터 생성으로 변경이 되면서. 한참을 기다리다보면. HeatWave State: Active 상태로 변경됩니다.

MySQL에 접속을 해서 엔진을 확인해보면, 아래와 같이 RAPID 엔진이 정상적으로 올라와 있네요.

 mysql> show engines;
+--------------------+---------+--------------+------+------------+
| Engine             | Support | Transactions | XA   | Savepoints |
+--------------------+---------+--------------+------+------------+
| FEDERATED          | NO      | NULL         | NULL | NULL       |
| MEMORY             | YES     | NO           | NO   | NO         |
| InnoDB             | DEFAULT | YES          | YES  | YES        |
| PERFORMANCE_SCHEMA | YES     | NO           | NO   | NO         |
| RAPID              | YES     | NO           | NO   | NO         | <<==  올라옴
| MyISAM             | YES     | NO           | NO   | NO         |
| MRG_MYISAM         | YES     | NO           | NO   | NO         |
| BLACKHOLE          | YES     | NO           | NO   | NO         |
| CSV                | YES     | NO           | NO   | NO         |
| ARCHIVE            | YES     | NO           | NO   | NO         |
+--------------------+---------+--------------+------+------------+

Loading Test Data

Oracle 메뉴얼에 Airport 샘플 데이터를 공개해놓았기 때문에. 이것을 적극 활용해봐야겠죠? 큰 테이블 만들기도 참 귀찮았는대.. 아. 일단. 이 전에. MySQL에 붙기 위한.. Compution instance를 하나 생성부터 해야겠군요. 이 부분은 스킵!

MySQL클라이언트를 설치하고, 추가로 데이터 로딩에서 mysql shell을 쓰기에 같이 설치!

[opc@instance-20220516-1013 ~]$ sudo yum install mysql
[opc@instance-20220516-1013 ~]$ sudo yum install mysql-shell

정상적으로 MySQL에 쿼리를 실행할 수 있는 환경이 구성되었다면, 이제 테스트 데이터를 받아서, mysql shell로 데이터를 로딩해보도록 하겠습니다.

[opc@instance-20220516-1013 ~]$ wget https://downloads.mysql.com/docs/airport-db.tar.gz
[opc@instance-20220516-1013 ~]$ tar xzvf airport-db.tar.gz
[opc@instance-20220516-1013 ~]$ cd airport-db
[opc@instance-20220516-1013 airport-db]$ mysqlsh chan@10.0.0.143
Please provide the password for 'chan@10.0.0.143':   

MySQL 10.0.0.143:3306 ssl JS > util.loadDump("airport-db", {threads: 16, deferTableIndexes: "all", ignoreVersion: true})

데이터 로딩이 마무리되었고.. HeatWave를 테스트해볼 환경이 모두 준비 되었습니다.

mysql> show tables from airportdb;
+---------------------+
| Tables_in_airportdb |
+---------------------+
| airline             |
| airplane            |
| airplane_type       |
| airport             |
| airport_geo         |
| airport_reachable   |
| booking             |
| employee            |
| flight              |
| flight_log          |
| flightschedule      |
| passenger           |
| passengerdetails    |
| weatherdata         |
+---------------------+

Heatwave VS InnoDB

문서에 나온대로, 실제 성능을 체감해보도록 하겠습니다.

1. Initialize HeatWave

쿼리를 날리기에 앞서. airportdb에 있는 모든 테이블을 Heatwave로 변경을 시켜줘야겠죠? 이 과정이 생각보다 오래 걸리지 않더군요. (일부 테이블은 몇천만건 데이터를 가지고 있음에도..)

mysql> call sys.heatwave_load(json_array('airportdb'), null);
mysql> select name, load_status 
    -> from performance_schema.rpd_tables, 
    ->      performance_schema.rpd_table_id
    -> where rpd_tables.id = rpd_table_id.id;
+-----------------------------+-----------------------+
| NAME                        | LOAD_STATUS           |
+-----------------------------+-----------------------+
| airportdb.flight_log        | AVAIL_RPDGSTABSTATE   |
| airportdb.airport_geo       | AVAIL_RPDGSTABSTATE   |
| airportdb.flight            | AVAIL_RPDGSTABSTATE   |
| airportdb.passengerdetails  | AVAIL_RPDGSTABSTATE   |
| airportdb.passenger         | AVAIL_RPDGSTABSTATE   |
| airportdb.airplane          | AVAIL_RPDGSTABSTATE   |
| airportdb.weatherdata       | LOADING_RPDGSTABSTATE |
| airportdb.flightschedule    | AVAIL_RPDGSTABSTATE   |
| airportdb.booking           | AVAIL_RPDGSTABSTATE   |
| airportdb.employee          | AVAIL_RPDGSTABSTATE   |
| airportdb.airplane_type     | AVAIL_RPDGSTABSTATE   |
| airportdb.airport           | AVAIL_RPDGSTABSTATE   |
| airportdb.airline           | AVAIL_RPDGSTABSTATE   |
| airportdb.airport_reachable | AVAIL_RPDGSTABSTATE   |
+-----------------------------+-----------------------+

mysql> select count(*) from booking;
+----------+
| count(*) |
+----------+
| 54304619 |
+----------+

2. Query HeatWave

실행계획을 보면, 별다른 옵션없이 옵티마이저가 이 쿼리를 RAPID엔진을 사용하는 것으로 Extra부분에서 정보를 확인해볼 수 있겠습니다. 그런데 실제 가격별 카운팅 결과는 0.09초에 마무리가 되는.. 굉장히 좋은 성능을 보여주네요.

mysql> SET SESSION use_secondary_engine=ON;
mysql> explain
    -> SELECT booking.price, count(*)
    ->   FROM booking WHERE booking.price > 500
    ->  GROUP BY booking.price
    ->  ORDER BY booking.price LIMIT 10;
+----+----------+----------+--------------------------------------+
| id | rows     | filtered | Extra                                |
+----+----------+----------+--------------------------------------+
|  1 | 54202876 |    33.33 | Using ..Using secondary engine RAPID |
+----+----------+----------+--------------------------------------+

mysql> SELECT booking.price, count(*)
    ->   FROM booking WHERE booking.price > 500
    ->  GROUP BY booking.price
    ->  ORDER BY booking.price LIMIT 10;
+--------+----------+
| price  | count(*) |
+--------+----------+
| 500.01 |      860 |
| 500.02 |     1207 |
| 500.03 |     1135 |
| 500.04 |     1010 |
| 500.05 |     1016 |
| 500.06 |     1039 |
| 500.07 |     1002 |
| 500.08 |     1095 |
| 500.09 |     1117 |
| 500.10 |     1106 |
+--------+----------+
10 rows in set (0.09 sec)

3. Query InnoDB

InnoDB로만 데이터를 처리했을 시 결과입니다. 실행계획을 보면, 앞에서 명시되었던 RAPID 엔진 사용이 사라졌고. 실제 카운팅 쿼리를 수행해보면. 10초 이상.. 무려 1000배의 시간이 더 걸리는 성능을 보이죠. 일단, 이런 효율면에서는 확연하게 성능차가 나오네요.

mysql> SET SESSION use_secondary_engine=OFF;
mysql> explain
    -> SELECT booking.price, count(*)
    ->   FROM booking WHERE booking.price > 500
    ->  GROUP BY booking.price
    ->  ORDER BY booking.price LIMIT 10;
+----+----------+----------+--------------------------------------+
| id | rows     | filtered | Extra                                |
+----+----------+----------+--------------------------------------+
|  1 | 54202876 |    33.33 | Using where; Using..; Using filesort |
+----+----------+----------+--------------------------------------+
mysql> SELECT booking.price, count(*)
    ->   FROM booking WHERE booking.price > 500
    ->  GROUP BY booking.price
    ->  ORDER BY booking.price LIMIT 10;
+--------+----------+
| price  | count(*) |
+--------+----------+
| 500.01 |      860 |
| 500.02 |     1207 |
| 500.03 |     1135 |
| 500.04 |     1010 |
| 500.05 |     1016 |
| 500.06 |     1039 |
| 500.07 |     1002 |
| 500.08 |     1095 |
| 500.09 |     1117 |
| 500.10 |     1106 |
+--------+----------+
10 rows in set (10.66 sec)

이것 외에도 다른 다양한 쿼리가 몇몇 샘플로 더 공개되어 있지만, 이것은. 스킵하는 것으로..

Operational Test

아무리 엔진이 좋아도, 운영 측면에서 준비가 안되어있다면, 이것은 빛좋은 개살구일뿐입니다. 만약 서비스에서 사용을 한다면, 좋을만한 포인트가 무엇일지 기준으로 추가로 테스트해보았습니다.

1. Online DDL

아쉽게도… Heatwave로, 아니 SECONDARY_ENGINE=RAPID 으로 정의된 테이블에는 ALTER구문이 동작하지 않습니다. 만약, 테이블 구조 변경이 필요하다면. SECONDARY_ENGINE속성을 없앤 후에 ALTER를 수행해야합니다,

mysql> alter table heatwave_test add c10 varchar(10);
ERROR 3890 (HY000): DDLs on a table with a secondary engine defined are not allowed.

mysql> truncate table heatwave_test;
ERROR 3890 (HY000): DDLs on a table with a secondary engine defined are not allowed.

mysql> alter table partition_test secondary_engine = null;

mysql> truncate table heatwave_test;
Query OK, 0 rows affected (0.01 sec)

테이블 칼럼 추가 삭제 시에는 이런 부담을 안고 가야하기에. DDL이 절대 변하지 않을만한 상황에서 활용을 하는 것이 좋을 듯 하네요.

2. Partitioning

앞선 이야기에서처럼. SECONDARY_ENGINE=RAPID 으로 정의된 테이블에는 ALTER구문이 동작하지 않습니다. 이것은 파티셔닝 추가/삭제에 대해서도 마찬가지입니다.

사실 테이블에서는 디비에 부담없이 오래된 데이터를 정리하는 목적으로 파티셔닝을 많이 활용하기 때문에.. 아쉬운점이 많네요. 특히나. 이 테이블은 아무래도 분석 관련된 쿼리가 많이 활용될 것이라, 데이터도 많아지게 될 것이고. 아주 오래된 데이터를 효율적으로 제거하는 방안도 있어야할텐데..

무엇보다, 파티셔닝 테이블로 로딩을 했을지라도. RAPID 내부적으로는 의미가 없기도 하고요. 사실. Heatwave쪽에 별도의 데이터가 존재하기에. 당연한 결과이기는 합니다만..

mysql> SET SESSION use_secondary_engine=OFF;
+----+---------------------------------+---------+-------------+
| id | partitions                      | rows    | Extra       |
+----+---------------------------------+---------+-------------+
|  1 | p2017,p2018,p2019,p2020,p999999 | 9533500 | Using where |
+----+---------------------------------+---------+-------------+

mysql> SET SESSION use_secondary_engine=ON;
+----+------------+---------+---------------------------------------+
| id | partitions | rows    | Extra                                 |
+----+------------+---------+---------------------------------------+
|  1 | NULL       | 9533500 | Using .. Using secondary engine RAPID |
+----+------------+---------+---------------------------------------+

파티셔닝이 안되는 것은 아니지만, 우선 파티셔닝을 했을지라도 파티셔닝 구조변경을 위해서는 HeatWave재구성을 해야한다는 측면에서는 매리트가 없습니다. 이럴꺼면 그냥 테이블 단위 파티셔닝을 개발 레벨에서 구현을 해서 사용하는 것이 훨씬 유리해보이네요.

그냥 파티셔닝은 비효율적이다라는 정도로 정리!

Performance TEST

사실 Cloud에서 동작을 하는 것이라. 성능적으로 기하 급수적으로 변화시킬만한 파라메터가 없는 것은 사실입니다. 그러나, 적어도 Heatwave 테이블과 InnoDB로만 구성된 테이블간의 성능차이는 확인해봐야겠죠?

mysql> create database bmt;
mysql> use bmt;

mysql> create table tb_innodb_only(
    ->  i int not null primary key auto_increment,
    ->  c1 varchar(100) not null,
    ->  c2 varchar(100) not null,
    ->  c3 varchar(100) not null,
    ->  c4 varchar(100) not null,
    ->  c5 varchar(100) not null,
    ->  c6 varchar(100) not null,
    ->  c7 varchar(100) not null,
    ->  ts timestamp
    -> );

mysql> create table tb_innodb_rapid(
    ->  i int not null primary key auto_increment,
    ->  c1 varchar(100) not null,
    ->  c2 varchar(100) not null,
    ->  c3 varchar(100) not null,
    ->  c4 varchar(100) not null,
    ->  c5 varchar(100) not null,
    ->  c6 varchar(100) not null,
    ->  c7 varchar(100) not null,
    ->  ts timestamp
    -> );
mysql> alter table tb_innodb_rapid secondary_engine=rapid;
mysql> alter table tb_innodb_rapid secondary_load;

1. InnoDB VS HeatWave

InnoDB와 HeatWave 양쪽으로 두 벌의 데이터가 존재하기 때문에. SELECT와 같이 둘중 하나 담당하는 쿼리 테스트는 의미없을 듯 하고. 양쪽 모두 영향을 미치는 데이터 변경으로 테스트를 해보겠습니다. 테스트 트래픽은 쉽게쉽게 mysqlslap 유틸을 활용하여 생성해보도록 하겠습니다.

1.1 InnoDB performance

10건 데이터를 100개 프로세스가 나눠서 넣는 것으로. 총 10번 수행합니다.

mysqlslap                     \
  -u chan -p -h 10.0.0.143    \
  --concurrency=100           \
  --iterations=10             \
  --number-of-queries=100000  \
  --create-schema=bmt         \
  --no-drop                   \
  --query="insert into tb_innodb_only values (null, uuid(), uuid(), uuid(), uuid(), uuid(), uuid(), uuid(),now());"

Benchmark
  Average number of seconds to run all queries: 20.136 seconds
  Minimum number of seconds to run all queries: 18.896 seconds
  Maximum number of seconds to run all queries: 20.851 seconds
  Number of clients running queries: 100
  Average number of queries per client: 1000

유입 쿼리량을 보니.. 대략 초당 6,000 정도 쿼리를 처리하네요.

|Com_insert|5804|
|Com_insert|5430|
|Com_insert|6218|
|Com_insert|5759|
|Com_insert|6173|
|Com_insert|5823|
|Com_insert|5586|
|Com_insert|5460|
|Com_insert|6085|
|Com_insert|5842|
|Com_insert|6312|
|Com_insert|5807|
|Com_insert|6210|
|Com_insert|6036|

1.2 HeatWave Performance

이번에는 동일한 트래픽을 Heatwave로 구성된 테이블에 줘보도록 하겠습니다. 마찬가지로, 10건 데이터를 100개 프로세스가 나눠서 넣습니다.

mysqlslap                     \
  -u chan -p -h 10.0.0.143    \
  --concurrency=100           \
  --iterations=10             \
  --number-of-queries=100000  \
  --create-schema=bmt         \
  --no-drop                   \
  --query="insert into tb_innodb_rapid values (null, uuid(), uuid(), uuid(), uuid(), uuid(), uuid(), uuid(),now());"

Benchmark
  Average number of seconds to run all queries: 20.271 seconds
  Minimum number of seconds to run all queries: 19.184 seconds
  Maximum number of seconds to run all queries: 21.355 seconds
  Number of clients running queries: 100
  Average number of queries per client: 1000

유입 쿼리량을 보니.. 대략 초당 6,000 정도 쿼리를 처리하네요. 차이가 거의 없다고 해야하나.. -_-;

|Com_insert|5925|
|Com_insert|6201|
|Com_insert|5621|
|Com_insert|5923|
|Com_insert|5837|
|Com_insert|5609|
|Com_insert|5926|
|Com_insert|5268|
|Com_insert|5977|
|Com_insert|5630|
|Com_insert|6185|
|Com_insert|5046|
|Com_insert|6335|
|Com_insert|5761|
|Com_insert|6222|
|Com_insert|5926|

2. Count query for paging

생각없이 검색 쿼리를 날려보았는데. 의외의 결과가 나와서 추가로 적어봅니다.

서비스에서 많이 사용하는 쿼리 중 제일 컨트롤이 어려운 부분은 사실 페이징입니다. 데이터가 폭발적으로 증가할수록 언제나 늘 고민을 해야하는 것도. 바로 게시판 타입의 서비스이기도 하죠. 물론 오프셋 기반의 페이징(특정 ID값 이전의 10건씩 가져오는 방식)은 소셜 서비스의 방대한 데이터를 처리하기 위해 많이들 사용하는 추세이기는 합니다만.. 모든 요구사항을 이런식으로 구현하기에도 명백히 한계가 있습니다.

그런데. Heatwave를 태우게 되면. 단순 카운팅 뿐만 아니라. 아래와 같이 별도의 검색조건을 추가로 준다고 할지라도. 큰 무리없이 데이터 카운팅 결과를 가져옵니다. (0.05초 VS 10.84초)

#####################################
# Heatwave
#####################################
mysql> SET SESSION use_secondary_engine=ON;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT count(*)
    ->   FROM booking
    ->  WHERE booking.price > 500
    ->    AND seat like '24%';
+----------+
| count(*) |
+----------+
|     1407 |
+----------+
1 row in set (0.05 sec)

#####################################
# Without heatwave
#####################################
mysql> SET SESSION use_secondary_engine=OFF;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT count(*)
    ->   FROM booking
    ->  WHERE booking.price > 500
    ->    AND seat like '24%';
+----------+
| count(*) |
+----------+
|     1407 |
+----------+
1 row in set (10.84 sec)

의외로. 제 개인적으로는 워드프레스와 상당히 궁합이 맞을 수 있겠다는 생각이…?? 새로운 발견!!

Conclusion

지금까지 몇가지를 추가로 살펴본 HeatWave는.. 이렇게 정리를 해보고싶네요.

  1. Oracle Cloud에서만 사용 가능
  2. InnoDB 데이터와 별개의 분산 스토리지에 저장된다. (InnoDB first)
  3. Online DDL은 불가하고. DDL시에도 secondary_engine을 비활성 필요
  4. InnoDB는 파티셔닝 구성해도, HeatWave는 내부적으로 파티셔닝 구성 안되어 있음.
    파티셔닝의 관리 역시 순탄치 않음
  5. InnoDB와 성능 차이(INSERT)가 없음.
  6. 게시판 카운팅 쿼리에 나름 적합(?)

무엇보다 Oracle cloud에서만 사용할 수 있다는 점이 아쉽기는 하지만.

OnPremise로 가능할지라도. Heatwave 클러스터 구축 및 관리가 필요할 것이니. 이것이 꼭 단점으로만 부각할만한 것은 아니라고 봅니다. 그리고. Oracle cloud에서는 의외로.. MySQL binlog가 활성화되어있고, Replication 계정을 추가할 수 있기에.. 데이터 핸들링 측면에서는 꽤나 좋은 환경이라는 생각도 문득 드네요. ㅎㅎ (DBaaS인데 이 점은 정말 매력적인 포인트입니다. 데이터의 흐름을 가두지 않는 것. 아 근데 나중에 막힐라나?? -_-;;)

이런 측면을 놓고보면, 안정성/성능을 좀더 파악해봐하겠지만, 개인적으로는 서

긴 글 읽으시느라 고생많았습니다. 오랜만에 포스팅을 마치겠습니다.

Fluentd? 나만의 에이전트 패키징!

Overview

세상에는 수많은 모니터링 도구들이 있습니다. 최근 많이 사용하고 있는 시계열 데이터베이스인 Prometheus와 수많은 exporter가 그중 하나입죠. 매트릭 수집에 최적화된 이런 구성은 시스템의 상태 값을 수집하기에는 더없이 좋은 시스템이기는 합니다만, 로그성 데이터 수집(에러로그 혹은 syslog)에는 아무래도 한계를 가집니다.

이 경우, td-agent와 같은 범용적인 로그 수집 에이전트를 활용하게 되는데요. (혹은 자체적으로 구현을 하거나) 타팀과 혼재해서 사용하는 경우 문제 발생소지가 있긴합니다. 참고로, td-agent는 ruby 뿐만 아니라, 필요한 라이브러리들을 패키지 내부에 포함시켜서, OS 의존성을 최소화합니다.

오늘 포스팅에서는 td-agent와 같이 fluentd를 패키징하는 방법에 대해서 이야기를 해보도록 하겠습니다.

Packaging environment

이제부터 이야기할 내용은 기본적으로 CentOS7 기반을 전제로 합니다. 참고로, CentOS6 경우에는 EOL 여파인지 모르지만.. YUM 레파지토리 관리 뿐만 아니라.. 소소한 몇몇 문제가 있어서. 조금 귀찮아지더라고요. 🙂

패키징 환경을 구성하기 위한 방안은 Docker를 이용해보는 방법과 Vagrant를 활용하여 빠르게 OS이미지를 받아오는 방법이죠.

1. Docker

Docker가 정상적으로 구성이 되어 있는 환경에서, 아래와 같이 간단하게 CentOS7 환경을 만들어보겠습니다. 만약 centos:7 이미지가 없으면, 자동으로 이미지를 받아와서 컨테이너를 잘 만들어줍니다.

$ docker run -d -it --name=pkg-dev centos:7 bash
$ docker exec -it pkg-dev bash
[root@f02a337793f9 /]# yum -y install net-tools sysstat telnet bash wget openssl md5 tar bzip2 patch gcc git autoconf openssl-devel

2. Vagrant

VirtualBox를 쓸 수 있는 환경이라면, Vargrant도 좋은 대안이기도 합니다. 여기서는 맥북 환경 기준으로 vagrant 구성해보도록 하겠습니다.

## Install vagrant with brew
$ brew install cask
$ brew install --cask virtualbox
$ brew install --cask vagrant
$ brew install --cask vagrant-manager
 
$ echo 'export PATH=$PATH:/opt/vagrant/bin:.' >> ~/.zshrc
$ . ~/.zshrc

$ mkdir -p ~/Document/vagrant/centos7-node01
$ cd ~/Document/vagrant/centos7-node01

## Vagrant file 
$ echo "
Vagrant.configure('2') do |config|
  config.vm.box = 'centos/7'
  # config.vm.network "private_network", ip: "192.168.56.60"
  config.vm.provider 'virtualbox' do |vb|
    vb.gui = true
    vb.memory = '2048'
    vb.customize ['modifyvm', :id, '--audio', 'none']
  end
  config.vm.provision 'shell', inline: <<-SHELL
    yum update -y
    yum -y install net-tools sysstat telnet bash wget openssl md5 tar bzip2 patch gcc git autoconf openssl-devel
    echo 'test12' | passwd --stdin root
    sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/g' /etc/ssh/sshd_config   
    /sbin/service sshd restart
  SHELL
end" > Vagrantfile

vagrant up

참고로, 위 Vagrant 파일은 2G 메모리(vb.memory = ‘2048’)와 사용전 필요할만한 것들은 이것저것(?) 설치를 하고, root 패스워드를 'test12'로 구성하는 설정입니다. 만약 내부네트워크를 구성하고 싶다면, 위 설정에서 주석을 풀고 아이피를 적당하게 변경하시고 vagrant up 을 수행하면 됩니다.

이것 외에도 사실.. KVM을 활용한 리눅스 자체에서 Virtual machine을 생성하는 방법도 있지만, 이건 스킵! 🙂

이제 나만의 패키징을 위한 OS 세팅은 완료되었으니, 이제부터 제대로 시작을 해보도록 하겠습니다.

Ruby packaging

Fluentd는 기본적으로 ruby 위에서 구동되는 프로그램입니다. 그리고 최종적인 목표는 아래 그림과 같이, 패키징한 루비 위에, fluentd를 구성하고, 기타 필요한 fluentd 전용 플러그인을 설치하는 것입니다.

첫번째 단계로 우선 Ruby를 특정 디렉토리 안에 패키징을 해보죠. 이것을 위해 ruby-install 이라는 유틸리티를 사용토록 하겠습니다.
https://github.com/postmodern/ruby-install

########################
## Disable ssl 
########################
$ echo "sslverify=false" >> /etc/yum.conf"
$ yum -y install make

########################
# ruby-install
########################
$ export V_RUBY_INSTALL="0.8.2"
$ wget -O ruby-install-${V_RUBY_INSTALL}.tar.gz https://github.com/postmodern/ruby-install/archive/v${V_RUBY_INSTALL}.tar.gz
$ tar -xzvf ruby-install-${V_RUBY_INSTALL}.tar.gz
$ cd ruby-install-${V_RUBY_INSTALL}/
$ make install

########################
# embeded-ruby
########################
$ export V_RUBY="2.7.4"
$ export LD_LIBRARY_PATH=/opt/my-agent/lib
$ export PATH=$PATH:/opt/my-agent/bin
$ ruby-install --install-dir /opt/my-agent ruby ${V_RUBY} -- --enable-shared CPPFLAGS=-I/opt/my-agent/include LDFLAGS=-L/opt/my-agent/lib

이 과정을 거치고나면, /opt/my-agent 하단에 하단 같은 모습으로 구성되어 있는 것을 확인해볼 수 있습니다.

$ ls -al /opt/my-agent
total 24
drwxr-xr-x 6 root root 4096 Sep  2 08:06 .
drwxr-xr-x 1 root root 4096 Sep  2 08:06 ..
drwxr-xr-x 2 root root 4096 Sep  2 08:06 bin
drwxr-xr-x 3 root root 4096 Sep  2 08:06 include
drwxr-xr-x 3 root root 4096 Sep  2 08:06 lib
drwxr-xr-x 5 root root 4096 Sep  2 08:06 share

이제 fluentd 패키징을 위한 Ruby 환경 구성은 마무리되었습니다.

Install fluentd

Ruby가 패키징이 잘 이루어졌으니. 이제 fluentd를 루비 환경에 잘 넣어보도록 하겠습니다.

## fluentd
$ cd /opt/my-agent
$ ./bin/gem install fluentd
$ ./bin/fluentd -s conf
Installed conf/fluent.conf.

$ ls -al
total 28
drwxr-xr-x 7 root root 4096 Sep  2 09:04 .
drwxr-xr-x 1 root root 4096 Sep  2 08:06 ..
drwxr-xr-x 2 root root 4096 Sep  2 09:03 bin
drwxr-xr-x 3 root root 4096 Sep  2 09:04 conf
drwxr-xr-x 3 root root 4096 Sep  2 08:06 include
drwxr-xr-x 3 root root 4096 Sep  2 08:06 lib
drwxr-xr-x 5 root root 4096 Sep  2 08:06 share

이제 각각 필요할만한 라이브러리들을 컴파일하면서 /opt/my-agent/lib에 넣어보도록 해보죠. jemalloc, libyaml, openssl이 없을수도 있을만한 환경을 위해서, 아래와 같이 각각 컴파일을 해서 진행합니다. 그리고, mysql을 직접적으로 접근할 수도 있기에, mysql client 관련 라이브러리도 하단과 같이 잘 포함시켜 줍니다.

## jemalloc
$ cd ${HOME}
$ git clone https://github.com/jemalloc/jemalloc.git
$ cd jemalloc
$ ./autogen.sh --prefix=/opt/my-agent
$ make && make install
 
## libyaml
$ cd ${HOME}
$ export V_LIB_YAML="0.2.5"
$ wget --no-check-certificate http://pyyaml.org/download/libyaml/yaml-${V_LIB_YAML}.tar.gz
$ tar xzvf yaml-${V_LIB_YAML}.tar.gz
$ cd yaml-${V_LIB_YAML}
$ ./configure --prefix /opt/my-agent
$ make && make install
 
## openssl
$ cd ${HOME}
$ export V_OPENSSL="1.1.1"
$ wget https://www.openssl.org/source/old/${V_OPENSSL}/openssl-${V_OPENSSL}.tar.gz
$ tar xzvf openssl-${V_OPENSSL}.tar.gz
$ cd openssl-${V_OPENSSL}
$ ./config --prefix=/opt/my-agent
$ make && make install
 
## mysql client
$ cd ${HOME}
$ yum -y install mysql-devel mysql-libs
$ cp -R /usr/lib64/mysql/libmysqlclient* /opt/my-agent/lib/
$ mkdir -p /opt/my-agent/include
$ cp -R /usr/include/mysql /opt/my-agent/include/

자! 이제 기본적인 fluentd 패키징은 완료하였습니다. 이제부터는 필요한 플러그인들을 설치할 단계입니다.

Install fluentd plugins

제 입장에서는 사용해볼만한 플로그인은 크게 아래 세가지로 꼽아볼 수 있을 듯 하네요.

$ cd /opt/my-agent/bin
$ ./gem install fluent-plugin-out-http 
$ ./gem install fluent-plugin-mysqlslowquery
$ ./gem install fluent-plugin-mysql-query

각 플러그인에 간단하게 설명을 해보죠.

3.1. fluent-plugin-out-http

source로부터 전달받은 내용을 특정 HTTP API로 전달하는 플러그인으로, 저는 개인적으로 에러로그 수집에 활용을 해보고 있습니다. 에러로그 수집을 위한 샘플입니다. 참고로, http://192.168.56.101:5000/errorlog 수집 API 는 별도로 구현을 해야하는 것은 아시죠? (기회가 된다면, 이것도 한번. 코드로 공유를. ㅎㅎ)

<source>
  @type tail
  path /data/mysql/*.err
  pos_file /opt/my-agent/log/mysql.error.pos
  tag mysql.error
  format none
</source>
 
<filter mysql.error>
  @type record_transformer
  enable_ruby
  <record>
    hostname ${hostname}
    timestamp ${time.to_i}
  </record>
</filter>
 
<match mysql.error>
  @type http
  buffer_type file
  buffer_path /opt/my-agent/log/mysql.error.*.buffer
  flush_interval 1s
  endpoint_url   http://192.168.56.101:5000/errorlog
  http_method    post
  #serializer     json
  #bulk_request   true
</match>

v1.2.2에서는 라인단위로 명시된 endpoint_url을 매번 호출하는 형태로 구현이 되어 있었습니다.

# bulk_request: false
error line1 ========> API call
error line2 ========> API call
error line3 ========> API call
error line4 ========> API call
error line5 ========> API call

# bulk_request: true
error line1 
error line2 
error line3 
error line4 
error line5 ========> API call (line1~line5)

일반적인 상황에서는 큰 문제가 되지 않지만, 몇천 라인의 에러로그가 순식간에 생성이 되었을 때.. 수집 효율이 굉장히 떨어질 수 밖에 없겠죠. 현 버전에서는 bulk_request옵션이 추가되면서, 대략 1MB 미만(600K~800K)으로 데이터를 끊어서 application/x-ndjson로 묶어서 API를 호출합니다. (한번에 전달받은 메시지를 묶어서 디비 insert 처리를 하니, 만건 로그도 큰 무리없이 한번에 잘 넣긴 하네요. ㅎ)

옵션에 대한 추가 내용은 하단 깃을 읽어보시고, 필요한 것들을 잘 사용해보면 좋겠네요. ^^
https://github.com/fluent-plugins-nursery/fluent-plugin-out-http

3.2. fluent-plugin-mysqlslowquery

MySQL 슬로우 쿼리 수집을 위한 플러그인으로.. 슬로우 로그 위치를 지정해놓으면, 잘 파싱해서 전달해줍니다. 아래는 샘플 설정입니다.

<source>
  @type mysql_slow_query
  path /var/lib/mysql/mysql-slow.log
  tag mysql.slow
  <parse>
    @type none
  </parse>
</source>
 
<match mysql.slow>
  @type http
  buffer_type file
  buffer_path /opt/uldra-agent/log/mysql.slow.*.buffer
  flush_interval 1s
  endpoint_url    http://192.168.56.101:5000/slowlog
  serializer      json
  bulk_request    true
  http_method     post
</match>

사실 이 플러그인이 편리하기는 하지만.. 문제는 한번에 다량의 슬로우 쿼리가 발생했을 시.. 이 플러그인에서 무한정 리소스를 먹기 때문에.. 개인적으로는 활용하고 있지는 않습니다. 이렇게 활용할 바에는 차라리, 앞선 에러로그 수집과 같은 방식으로 tail로 소스를 받아서 병합을 API 서버 레벨에서 해주는 것이 훨씬 안정적이고 유리할 듯 하네요. ^^

그리고, 아쉽게도. 이 프로젝트는 더이상 개발이 되지 않는듯한? (혹은 다른 어디선가 새롭게? ㅎㅎ)
https://github.com/yuku/fluent-plugin-mysqlslowquery

3.3. fluent-plugin-mysql-query

MySQL에 쿼리를 날려서 데이터를 추출하는 플러그인 입니다. fluentd로 주기적으로 데이터베이스로부터 데이터를 추출해서 결과를 타겟으로 던질 때 좋을만한 플러그인입니다.

<source>
  @type           mysql_query
  host            127.0.0.1
  port            3306
  username        fluentd
  password        fluentd123
  interval        30s
  tag             mysql-query-01
  query           select 'chan' j, now() t;
  record_hostname yes
  nest_result     yes                 # Optional (default: no)
  nest_key        data                # Optional (default: result)
  row_count       yes                 # Optional (default: no)
  row_count_key   row_count           # Optional (default: row_count)
</source>
 
<match mysql-query-01>
  @type http
  buffer_type file
  buffer_path /opt/db-agent/log/mysql.query01.*.buffer
  flush_interval 1s
  endpoint_url    http://192.168.56.101:5000/query
  http_method     post
  serializer      json
  bulk_request    true
  http_method     post
</match>

개인적으로는 사용하고 있지는 않지만.. 향후 필요한 경우 요긴하게 활용하기 위해 저는 기본적으로 넣어놓습니다.
https://github.com/y-ken/fluent-plugin-mysql-query

Startup script

td-agent의 init.d 스크립트를 약간의 변경(?)을 주어서 구성해보았습니다. 중간 부분을 보게되면, jemalloc으로 프로그램을 구동하는 것을 확인할 수 있습니다.

#!/bin/sh
### BEGIN INIT INFO
# Provides:          my-agent
# Required-Start:    $network $local_fs
# Required-Stop:     $network $local_fs
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: data collector for Treasure Data
# Description:       my-agent is a data collector
### END INIT INFO
# pidfile:           /opt/my-agent/my-agent.pid
 
export PATH=/sbin:/opt/my-agent/sbin:/bin:/usr/bin
 
MY_AGENT_NAME=my-agent
MY_AGENT_HOME=/opt/my-agent
MY_AGENT_DEFAULT=/etc/sysconfig/my-agent
MY_AGENT_USER=root
MY_AGENT_GROUP=root
MY_AGENT_RUBY=${MY_AGENT_HOME}/bin/ruby
MY_AGENT_BIN_FILE=${MY_AGENT_HOME}/sbin/my-agent
MY_AGENT_LOG_FILE=${MY_AGENT_HOME}/log/my-agent.log
MY_AGENT_PID_FILE=${MY_AGENT_HOME}/my-agent.pid
MY_AGENT_LOCK_FILE=/var/lock/subsys/my-agent
MY_AGENT_OPTIONS="--use-v1-config"
 
# timeout can be overridden from /etc/sysconfig/my-agent
STOPTIMEOUT=120
 
# Read configuration variable file if it is present
if [ -f "${MY_AGENT_DEFAULT}" ]; then
  . "${MY_AGENT_DEFAULT}"
fi
 
# Arguments to run the daemon with
MY_AGENT_ARGS="${MY_AGENT_ARGS:-${MY_AGENT_BIN_FILE} --log ${MY_AGENT_LOG_FILE} ${MY_AGENT_OPTIONS}}"
START_STOP_DAEMON_ARGS="${START_STOP_DAEMON_ARGS}"
 
# Exit if the package is not installed
[ -x "${MY_AGENT_RUBY}" ] || exit 0
 
# Source function library.
. /etc/init.d/functions
 
# Define LSB log_* functions.
lsb_functions="/lib/lsb/init-functions"
if test -f $lsb_functions ; then
  . $lsb_functions
else
  log_success_msg()
  {
    echo " SUCCESS! $@"
  }
  log_failure_msg()
  {
    echo " ERROR! $@"
  }
  log_warning_msg()
  {
    echo " WARNING! $@"
  }
fi
 
# Check the user
if [ -n "${MY_AGENT_USER}" ]; then
  if ! getent passwd | grep -q "^${MY_AGENT_USER}:"; then
    echo "$0: user for running ${MY_AGENT_NAME} doesn't exist: ${MY_AGENT_USER}" >&2
    exit 1
  fi
  mkdir -p "$(dirname "${MY_AGENT_PID_FILE}")"
  chown -R "${MY_AGENT_USER}" "$(dirname "${MY_AGENT_PID_FILE}")"
  START_STOP_DAEMON_ARGS="${START_STOP_DAEMON_ARGS} --user ${MY_AGENT_USER}"
fi
 
if [ -n "${MY_AGENT_GROUP}" ]; then
  if ! getent group -s files | grep -q "^${MY_AGENT_GROUP}:"; then
    echo "$0: group for running ${MY_AGENT_NAME} doesn't exist: ${MY_AGENT_GROUP}" >&2
    exit 1
  fi
  MY_AGENT_ARGS="${MY_AGENT_ARGS} --group ${MY_AGENT_GROUP}"
fi
 
if [ -n "${MY_AGENT_PID_FILE}" ]; then
  mkdir -p "$(dirname "${MY_AGENT_PID_FILE}")"
  chown -R "${MY_AGENT_USER}" "$(dirname "${MY_AGENT_PID_FILE}")"
  MY_AGENT_ARGS="${MY_AGENT_ARGS} --daemon ${MY_AGENT_PID_FILE}"
fi
 
# 2012/04/17 Kazuki Ohta <k@treasure-data.com>
# Use jemalloc to avoid memory fragmentation
if [ -f "${MY_AGENT_HOME}/lib/libjemalloc.so" ]; then
  export LD_PRELOAD="${MY_AGENT_HOME}/lib/libjemalloc.so"
fi
 
kill_by_file() {
  local sig="$1"
  shift 1
  local pid="$(cat "$@" 2>/dev/null || true)"
  if [ -n "${pid}" ]; then
    if /bin/kill "${sig}" "${pid}" 1>/dev/null 2>&1; then
      return 0
    else
      return 2
    fi
  else
    return 1
  fi
}
 
#
# Function that starts the daemon/service
#
do_start() {
  # Set Max number of file descriptors for the safety sake
  # see http://docs.fluentd.org/en/articles/before-install
  ulimit -n 65536 1>/dev/null 2>&1 || true
  local RETVAL=0
  daemon --pidfile="${MY_AGENT_PID_FILE}" ${START_STOP_DAEMON_ARGS} "${MY_AGENT_RUBY}" ${MY_AGENT_ARGS} || RETVAL="$?"
  [ $RETVAL -eq 0 ] && touch "${MY_AGENT_LOCK_FILE}"
  return $RETVAL
}
 
#
# Function that stops the daemon/service
#
do_stop() {
  # Return
  #   0 if daemon has been stopped
  #   1 if daemon was already stopped
  #   2 if daemon could not be stopped
  #   other if a failure occurred
  if [ -e "${MY_AGENT_PID_FILE}" ]; then
    # Use own process termination instead of killproc because killproc can't wait SIGTERM
    if kill_by_file -TERM "${MY_AGENT_PID_FILE}"; then
      local i
      for i in $(seq "${STOPTIMEOUT}"); do
        if kill_by_file -0 "${MY_AGENT_PID_FILE}"; then
          sleep 1
        else
          break
        fi
      done
      if kill_by_file -0 "${MY_AGENT_PID_FILE}"; then
        echo -n "Timeout error occurred trying to stop ${MY_AGENT_NAME}..."
        return 2
      else
        rm -f "${MY_AGENT_PID_FILE}"
        rm -f "${MY_AGENT_LOCK_FILE}"
      fi
    else
      return 1
    fi
  else
    if killproc "${MY_AGENT_PROG_NAME:-${MY_AGENT_NAME}}"; then
      rm -f "${MY_AGENT_PID_FILE}"
      rm -f "${MY_AGENT_LOCK_FILE}"
    else
      return 2
    fi
  fi
}
 
#
# Function that sends a SIGHUP to the daemon/service
#
do_reload() {
  kill_by_file -HUP "${MY_AGENT_PID_FILE}"
}
 
do_restart() {
  if ! do_configtest; then
    return 1
  fi
  local val=0
  do_stop || val="$?"
  case "${val}" in
  0 | 1 )
    if ! do_start; then
      return 1
    fi
    ;;
  * ) # Failed to stop
    return 1
    ;;
  esac
}
 
do_configtest() {
  eval "${MY_AGENT_ARGS} ${START_STOP_DAEMON_ARGS} --dry-run -q"
}
 
RETVAL=0
case "$1" in
"start" )
  echo -n "Starting ${MY_AGENT_NAME}: "
  do_start || RETVAL="$?"
  case "$RETVAL" in
  0 )
    log_success_msg "${MY_AGENT_NAME}"
    ;;
  * )
    log_failure_msg "${MY_AGENT_NAME}"
    exit 1
    ;;
  esac
  ;;
"stop" )
  echo -n "Stopping ${MY_AGENT_NAME}: "
  do_stop || RETVAL="$?"
  case "$RETVAL" in
  0 )
    log_success_msg "${MY_AGENT_NAME}"
    ;;
  * )
    log_failure_msg "${MY_AGENT_NAME}"
    exit 1
    ;;
  esac
  ;;
"reload" )
  echo -n "Reloading ${MY_AGENT_NAME}: "
  if ! do_configtest; then
    log_failure_msg "${MY_AGENT_NAME}"
    exit 1
  fi
  if do_reload; then
    log_success_msg "${MY_AGENT_NAME}"
  else
    log_failure_msg "${MY_AGENT_NAME}"
    exit 1
  fi
  ;;
"restart" )
  echo -n "Restarting ${MY_AGENT_NAME}: "
  if do_restart; then
    log_success_msg "${MY_AGENT_NAME}"
  else
    log_failure_msg "${MY_AGENT_NAME}"
    exit 1
  fi
  ;;
"status" )
  if kill_by_file -0 "${MY_AGENT_PID_FILE}"; then
    log_success_msg "${MY_AGENT_NAME} is running"
  else
    log_failure_msg "${MY_AGENT_NAME} is not running"
    exit 1
  fi
  ;;
"condrestart" )
  if [ -f "${MY_AGENT_LOCK_FILE}" ]; then
    echo -n "Restarting ${MY_AGENT_NAME}: "
    if do_restart; then
      log_success_msg "${MY_AGENT_NAME}"
    else
      log_failure_msg "${MY_AGENT_NAME}"
      exit 1
    fi
  fi
  ;;
"configtest" )
  if do_configtest; then
    log_success_msg "${MY_AGENT_NAME}"
  else
    log_failure_msg "${MY_AGENT_NAME}"
    exit 1
  fi
  ;;
* )
  echo "Usage: $0 {start|stop|reload|restart|condrestart|status|configtest}" >&2
  exit 1
  ;;
esac

Conclusion

나만의 에이전트를 만들어보았습니다. (별것아니쥬?)

지금까지 td-agent과 유사하게, ruby와 기타 등등의 라이브러리/플러그인들을 패키징하는 나만의 fluentd 에이전트를 만들어보는 과정에 대해서 이야기를 해보았습니다. 물론, 범용적인 td-agent를 잘 활용해보는 방법도 있겠지만, 타 부서와의 혼용된 환경을 방지하고자, 나만의 에이전트로 패키징을 해보았습니다. 남은 것은 패키징한 나만의 에이전트에 나만의 요구사항을 잘 얹어서, 서비스 모니터링을 최적으로 유지시키는 것이겠죠.

각자의 서비스에 맞는 좋은 사례가 공유되었으면 하는 마음으로.. 오늘 포스팅을 마칩니다. 🙂