2014년 6월 16일 월요일

Memcached 복제 (3)

KMC 설치


KMC (Kakao Mem Cached)를 빌드하기 위해서는 MySQL 5.6 빌드시에 필요한 기본 라이브러리는 모두 설치가 되어 있어야 한다. 대표적으로 libaio-devel과 ncurses-devel 그리고 cmake 등은 설치되어 있지 않은 경우가 많으니 꼭 확인해보도록 하자. (물론 빌드하면서 에러가 출력되면 그때 그때 하나씩 설치해도 무방하다.)

그리고 KMC에서 꼭 필요한 아래 Memcached client 관련 라이브러리도 다운로드해서 설치하도록 하자.

  • libmemcached-1.0.4-1.el5.remi.x86_64.rpm
  • libmemcached-devel-1.0.4-1.el5.remi.x86_64.rpm


패키지의 이름은 무관하나 버전은 가능하면 크게 벗어나지 않도록 하자.

이제 KMC를 빌드하면 되는데, cmake를 실행할때에는 아래와 같이 몇가지 옵션들이 추가로 필요하다.

# cd mysql-5.6.14_kmc
# mkdir Release
# cd Release
# cmake .. \
'-DBUILD_CONFIG=mysql_release' \
'-DCMAKE_INSTALL_PREFIX=/usr/local/mysql' \
'-DWITH_INNODB_MEMCACHED=ON' \
'-DENABLED_LOCAL_INFILE=OFF' \
'-DHAVE_QUERY_CACHE=OFF' \
'-DOPTIMIZER_TRACE=OFF' \
'-DENABLE_DEBUG_SYNC=OFF' \
'-DENABLED_PROFILING=OFF' \
'-DWITH_ARCHIVE_STORAGE_ENGINE=OFF' \
'-DWITH_EMBEDDED_SERVER=OFF' \
'-DCOMPILATION_COMMENT=kmc' \
'-DENABLE_DTRACE=OFF'

물론 위의 모든 옵션이 꼭 필요한 것은 아니지만, 성능이나 Memcached plugin의 바이너리 로깅 기능 등을 위해서 필요하므로 혹시 변경이 필요한 경우에는 신중히 고려해서 적용하도록 하자.

cmake가 끝나면, make && make install 명령으로 KMC를 설치하면 된다. MySQL 프로그램이 설치되는 디렉토리는 INSTALL_PREFIX로 명시했던 /usr/local/mysql로 설치될 것이다.

프로그램의 설치가 완료되면, 일반적인 MySQL 서버 설치와 동일하게 $MYSQL_HOME/scripts/mysql_install_db 스크립트를 이용해서 MySQL 기본 딕셔너리 테이블들을 생성후, MySQL 서버를 시작하면 된다.

Memcached 관련 스키마 생성

MySQL 서버가 정상적으로 시작되었다면,
우선 Memcached 플러그인을 작동시키기 위해서 필요한 기본 스키마들을 생성하도록 하자. MySQL Memcached 플러그인이 작동하기 위해서 필요한 초기 스키마들은 아래 파일이 미리 준비되어 있으므로, source 명령으로 실행해주기만 하면 된다.

mysql> source $MYSQL_HOME/share/innodb_memcached_config.sql
mysql> use innodb_memcache
mysql> show tables

아래와 같은 InnoDB 테이블들이 정상적으로 준비되었는지 확인해보자.


  • cache_policies
  • config_options
  • containers

테이블이 준비되었다면, 이제 KMC를 위한 스키마들을 생성하도록 하자.

mysql> USE innodb_memcache;
mysql> INSERT INTO `containers` VALUES ('default','kmc','kmc_template','k','v','f','c','e','PRIMARY');

mysql> CREATE DATABASE kmc;
mysql> USE kmc;

mysql> DROP TABLE IF EXISTS `kmc_template`;
mysql> CREATE TABLE `kmc_template` (
  `k` varchar(255) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL DEFAULT '',
  `v` mediumblob,
  `f` int(11) NOT NULL DEFAULT '0',
  `c` bigint(20) unsigned NOT NULL DEFAULT '0',
  `e` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`k`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 STATS_PERSISTENT=0;

mysql> INSERT INTO `kmc_template` VALUES ('1','DO-NOT-REMOVE',0,0,0);

KMC를 위한 my.cnf 설정

이제 마지막으로 Memcached를 위한 설정들을 my.cnf 파일에 적용하고 Memcached 서비스가 가능토록 MySQL 서버를 기동해보도록 하자. (MySQL 서버가 기동중이라면 셧다운하도록 하자.)

...
innodb_data_file_path = ibdata1:64M:autoextend
...
innodb_log_file_size = 32M
innodb_log_files_in_group = 2
innodb_log_buffer_size = 16M

## Memcached -----------------------------------------------------------------------------
daemon_memcached_option = '-m 20480 -p 11211 -c 80000 -t 8 -s /tmp/memcached.sock'
innodb_api_enable_binlog = 1
innodb_api_trx_level=0 ## READ-UNCOMMITTED
innodb_api_bk_commit_interval=1
daemon_memcached_r_batch_size=1
daemon_memcached_w_batch_size=1
innodb_api_enable_mdl=OFF

## Replication:Binary log
server-id  = 1
## Replication query is always idempotent
slave_exec_mode = IDEMPOTENT
## Memcached connection string :: with socket file
kmc_connect_string = '--SOCKET="/tmp/memcached.sock" --BINARY-PROTOCOL --NOREPLY --TCP-NODELAY --TCP-KEEPALIVE'
binlog-checksum = NONE
sync_binlog = 0
master_info_repository = FILE
relay_log_info_repository = FILE
sync_master_info    = 0
sync_relay_log      = 0
sync_relay_log_info = 0
slave_checkpoint_group = 100000
slave_checkpoint_period = 1000
max_binlog_size = 100M
expire_logs_days=1
expire_max_log_files = 15
binlog-format = ROW

대 부분 기본적으로 설정되어 있는 옵션들도 있지만, 안정성과 성능을 위해서 설정되어 있는 것이므로, 큰 문제가 없다면 최소한 이 정도의 옵션은 my.cnf에 포함시키도록 하자.

daemon_memcached_option 옵션은 Memcached 쓰레드가 기동되면서 사용하는 옵션이므로 신중히 설정하도록 하자. 그리고 kmc_connect_string는 MySQL Slave의 SQL Thread가 바이너리 로그를 읽어서 Replay시에 접속하는 Memcached 서버의 정보이므로, 로컬 Memcached 서버의 접속 정보를 설정하면 된다. 또한 InnoDB는 거의 데이터 저장소로 사용되지 않으므로, innodb의 데이터 파일과 리두 로그의 크기는 최소화해서 설정해주는 것이 좋다.

이제 MySQL 서버를 기동하고, Memcached의 기능을 테스트해보자.
물론, KMC의 복제를 연결하는 방법과 복제 상태를 확인하는 방법은 기존 MySQL 서버와 동일하다.

KMC 테스트

MySQL 서버가 기동되면, 아래와 같이 간단히 Memcached의 기능을 테스트를 수행해볼 수 있다.

[root@matt001 ~]# telnet localhost 11211
Trying 127.0.0.1...
Connected to localhost.localdomain (127.0.0.1).
Escape character is '^]'.

set matt 0 0 9
Seong Uck
STORED

get matt
VALUE matt 0 9
Seong Uck
END

delete matt
DELETED

get matt
END

quit
Connection closed by foreign host.

만약 Memcached로 접속하는 socket file (/tmp/memcached.sock)이 정상적으로 작동하는지 확인하고자 한다면 아래와 같이 netcat 유틸리티를 이용하면 된다.

[root@matt001 ~]# nc -U /tmp/memcached.sock
set matt 0 0 9
SEONG UCK
STORED

get matt
VALUE matt 0 9
SEONG UCK
END

quit
[root@matt001 ~]#

물론 복제된 데이터를 확인하고자 한다면, 마스터에서 SET을 실행하고 슬레이브 Memcached로 접속해서 GET으로 데이터를 확인해보면 된다. 또한 바이너리 로그를 mysqlbinlog 유틸리티로 열어서 내용을 확인해보는 것이 좋은 방법일 것이다.



2014년 6월 15일 일요일

Memcached 복제 (2)

Memcached(Cache-only 모드) 복제 아키텍쳐

1편에서 언급했던 것처럼, MySQL Memcached 플러그인이 Caching과 InnoDB-only 모드로 작동하는 경우에는 Memcached를 통한 변경 내용이 InnoDB 스토리지 엔진에 적용되기 때문에 바이너리 로그에도 그 변경 내용이 기록된다. 즉 Caching과 InnoDB-only 모드에서는 자동적으로 복제 기능이 적용되는 것이다.

하지만 이 프로젝트의 목적은 InnoDB를 완전히 사용하지 않고 MySQL 서버를 Memcached 전용 서버로 사용하는 것이기 때문에, Cache-only 모드만 고려 대상인 것이다. 그런데 1편에서도 언급했듯이, 문제는 Cache-only 모드에서는 Memcached의 변경 내용이 MySQL 서버의 바이너리 로그에 기록되지 않는다는 것이다.

Cache-only 모드에서 바이너리 로그를 작동하도록 하는 것이 이 프로젝트의 목표인데, 이를 위해서는 아래 그림과 같이 마스터 MySQL의 바이너리 로그 기록과 슬레이브 MySQL의 릴레이 로그 재생(Replay) 기능을 수정했다.
(그림에서 파란색 선은 원래 MySQL 5.6이 가지고 있던 데이터의 흐름이며, 갈색 두꺼운 선이 수정된 기능을 의미한다.)



위의 그림에서 파란색 점선 화살표는, 이해를 돕기 위해서 MySQL Memcached의 Caching과 InnoDB-only 모드 작동 방식을 표시해둔 것이다.

그림에 표시된 번호별로 조금 더 상세히 살펴보도록 하자.
  1. InnoDB API에서는 Memcached를 통해서 유입된 변경 내용을 정해진 규칙(innodb_api_bk_commit_interval, daemon_memcached_r_batch_size, daemon_memcached_w_batch_size 시스템 변수)에 따라서 InnoDB 스토리지 엔진으로 전달하고, 바이너리 로그에 기록하도록 구현되어 있었다. 하지만 이 로직은 Cache-only 모드에는 해당되지 않는데, 이를 Cache-only 모드에서도 작동하도록 수정했다. Original MySQL에서는 Cache-only 모드에서는 InnoDB 테이블이 필요치 않지만, 이 기능을 위해서 Caching이나 InnoDB-only 모드와 같이 Memcached 플러그인을 위한 설정 테이블(innodb_memcache DB)과 컨테이너 테이블이 필요하다. 물론 데이터가 저장되지는 않지만, 껍데기 구조가 필요한 것이다.
  2. 슬레이브에서는 IO Thread에 의해서 만들어진 Relay log를 가져와서 다시 Memcached에 적용(SET/ADD/DELETE)해야 한다. 하지만 Original MySQL 서버에서는 Replication SQL Thread가 Memcached를 통하지 않고 즉시 InnoDB Storage Engine으로 데이터를 반영해버린다. 그래서 Replication SQL Thread가 Memcached Plugin으로 접근할 수 있는 방법이 필요한데, 이를 위해서 MySQL 서버에 libmemcached.so를 같이 빌드해서 Replication SQL Thread가 Memcached client로 작동할 수 있도록 했다.
  3. Memcached Client로 작동하는 Replication SQL Thread에서는 최대한 빠르게 Relay 로그의 내용을 Replay해야 하므로, TCP Socket 포트(11211)보다는 Unix socket을 사용하도록 했으며, Async 모드로 Memcached Plugin에 변경 내용을 적용하도록 수정했다.


그 이외의 개선 사항


  • MySQL Memcached Plugin이 TCP socket과 Unix domain socket을 동시에 지원하도록 개선
  • Memcached Plugin의 상태를 MySQL 서버의 상태 값(Global status variables)으로 모니터링할 수 있도록, 매 초단위로 Memcached의 상태 값을 MySQL 서버 상태 값으로 복사
    mysql> show global status like '%kmc%';
    +----------------------------------+--------------+
    | Variable_name                    | Value        |
    +----------------------------------+--------------+
    | Innodb_kmc_connection_structures | 2004         |
    | Innodb_kmc_curr_connections      | 2002         |
    | Innodb_kmc_curr_items            | 70638969     |
    | Innodb_kmc_pointer_size          | 64           |
    | Innodb_kmc_threads               | 8            |
    | Innodb_kmc_total_connections     | 5090         |
    | Innodb_kmc_total_items           | 778120599    |
    | Innodb_kmc_bytes                 | 18648687816  |
    | Innodb_kmc_bytes_read            | 460650586044 |
    | Innodb_kmc_bytes_written         | 497924594841 |
    | Innodb_kmc_cmd_get               | 7781206118   |
    | Innodb_kmc_cmd_set               | 778120599    |
    | Innodb_kmc_evictions             | 498675673    |
    | Innodb_kmc_get_hits              | 1043938514   |
    | Innodb_kmc_get_misses            | 6737267604   |
    | Innodb_kmc_limit_maxbytes        | 21474836480  |
    +----------------------------------+--------------+
  • Caching이나 Cache-only 모드와는 달리 InnoDB 잠금과 무관하게 작동함
    (하지만 여전히 InnoDB의 트랜잭션이나 일부 자원은 할당해서 사용함 --> 개선 예정)
  • MySQL 5.6.14의 메모리 릭과 Memcached plugin의 expiration time 설정 버그 보완
  • MySQL 서버의 각종 Disk sync 로직을 Async로 전환
  • expire_max_log_files 시스템 변수 추가
    바이너리 로그 파일의 개수를 이용한 자동 삭제 기능
  • 바이너리 로그 자동 삭제 기능의 이상 현상 모니터링용 상태 변수 Binlog_purge_failed 추가
    • 0 : 정상
    • 1 : ACTIVE 상태의 바이너리 로그를 삭제하려고 시도한 경우 설정
    • 2 : USE 상태의 바이너리 로그를 삭제하려고 시도한 경우 설정
  • kmc_connect_string 시스템 변수 추가
    Replication SQL Thread가 릴레이 로그 Replay시 접속할 Memcached 접속 정보 설정


사용 가능한 Memcached Operation

안타깝게도 KMC(Kakao Mem Cached, 복제 가능한 Memcached 서버)의 특성이나 구현 제약으로 인해서, 현재 버전에서는 일부 Memcached operation을 지원하지 않는다.

사용 가능
  • GET
  • SET
  • ADD
  • DELETE
  • REPLACE
사용 불가능
  • INCREMENT
  • DECREMENT
  • CAS

성능 고려 사항

일반적인 Original Memcached와 비교해서 GET 성능은 차이가 없다. 하지만 SET이나 ADD 그리고 DELETE와 같은 Operation은 Original Memcached와는 달리 KMC는 Memcached의 캐시에 기록후 바이너리 로그에 기록해야 한다. 물론 바이너리 로그 기록은 Sync mode로 작동하는 것은 아니지만 여전히 Original Memcached에 비해서 상당히 많은 자원을 사용하게 된다. 그래서 SET/ADD/DELETE Operation의 성능은 Original Memcached에 비해서 상당히 낮은 결과를 보일 것이다.

하지만, 일반적으로 Memcached의 특성상 SET/ADD/DELETE Operation의 수치는 그렇게 높지 않은 것이 일반적이다. 한번이라도 SET되면 그 이후부터는 SET operation이 줄어들고 GET operation이 늘어나기 때문이다. 만약 SET/ADD가  초당 10K 이상씩 실행되는 Memcached가 있다면, 이는 캐시 효율이 상당히 낮은 Memcached라고 볼 수 있다. 또한 이런 Memcached는 오히려 서비스의 처리 성능에 오버헤드만 유발하고 있다고 볼 수도 있다.

일반적인 Intel x86 장비에서 대략 초당 10K 정도 SET/ADD/DELETE operation을 한계로 고려하고 있는데, 이는 단일 서버의 한계가 아니라 복제 지연으로 인한 한계이다.

KMC 기타 제약 사항

  • Memcached 컨테이너 테이블의 이름은 "kmc_" 키워드로 시작해야 한다.
  • CAS와 INCR 그리고 DECR 명령 사용 불가
  • KMC 내부적으로 Memcached 프로토콜에서 flags 필드의 상위 2바이트를 사용함
    • Memcached의 flags중에서 하위 2바이트만 사용한다면 문제 없음

KMC 다운로드




** 마지막 편에서는 MySQL Memcached를 설치하고, 사용하는 방법 그리고 추천하는 설정에 대해서 간단히 소개하도록 하겠다.

2014년 6월 10일 화요일

Memcached 복제 (1)

개요

모든 응용 프로그램은 영구적으로 저장되어야 하는 데이터를 가지게 마련이며,
이런 데이터들은 MySQL 서버와 같은 RDBMS나 NoSQL류의 DBMS에 저장되고 있다.
하지만 웹 기반의 응용 프로그램에서는 매우 빈번하게 데이터를 DBMS로부터 가져와서 가공하고 그 결과를 클라이언트로 전송해야 하는 요건을 가지고 있는데,
이렇게 많은 요청들을 매번 DBMS에서 가져간다는 것은 상당히 부담스러운 작업중 하나이다.
MySQL과 같은 RDBMS에서는 레코드 하나를 가져가기 위해서도 내부적으로 많은 복잡한 과정들을 거쳐야 하며,
때로는 메모리에 상주되지 않은 데이터를 위해서 고 비용의 디스크 읽기를 거쳐야 할 수도 있다. 
이는 HBase나 Cassandra와 같은 NoSQL류의 데이터베이스에서도 마찬가지이다. 때로는 NoSQL류의 데이터베이스는 데이터 읽기 작업이 RDBMS보다 훨씬 고비용인 경우가 많다.

그래서 이렇게 자주 사용되는 데이터는 빠르게 접근할 수 있도록 Memcached나 Redis와 같은 메모리 기반의 캐시 솔루션을 도입해서 구현하는 것이 매우 일반적인 형태이다.
Memcached와 Redis 모두 많은 개발자들로부터 관심을 받고 있는 캐시 솔루션이지만, 여전히 이 솔루션들은 각자의 단점들을 가지고 있다.

(이 글은 Memcached와 Redis중 어떤 솔루션이 더 나은지를 비교하는 것이 목적이 아니므로, 관심 대상이 아닌 Redis에 대한 언급은 생략하도록 하겠다.)
각 단점중에서 Memcached가 가진 단점 중 대표적인 것은 Memcached의 데이터가 복제되지 않는다는 것이다. (물론 그 이외에도 많은 단점들이 있을 것이다.)
최근에는 많은 회사들이 Memcached 서버의 데이터 복제나 여러 Memcached 서버간의 데이터 동기화에 대한 요건을 가지고 있으며, 페이스북의 경우에는 MySQL 서버의 바이너리 로그를 분석해서 Memcached로 변경 내용을 Relay해주는 기능들을 구현해서 사용중이기도 하다.
카카오에서도 Memcached의 이런 단점을 보완하기 위한 방법을 고려하던중, Memcached 플러그인이 내장된 MySQL 5.6 버전이 릴리즈되었다.

MySQL 5.6  Memcached 플러그인

우선 본격적인 이야기에 앞서서, MySQL 5.6의 Memcached 플러그인에 대한 기본적인 내용을 조금 살펴보도록 하자.
MySQL 5.6에 내장된 Memcached 플러그인은 아래와 같이 3가지 캐시 모드로 작동할 수 있다.

  1. Innodb-only
  2. Caching
  3. Cache-only


애초에 MySQL Memcached 플러그인은 InnoDB의 NoSQL 인터페이스로 도입된 솔루션인 관계로, InnoDB 스토리지 엔진과 매우 밀접하게 연결되어 있다.

  • 첫 번째 Innodb-only 모드에서는 Memcached 플러그인이 자체적인 캐시 메모리 공간을 가지지 않고, InnoDB 스토리지 엔진에 Memcached 프로토콜을 이용해서 데이터를 저장하고 읽어가기 위한 방법을 제공하는 것이다.즉 Memcached 프로토콜을 이용해서 SET/DELETE 그리고 GET 오퍼레이션이 실행되면, MySQL Memcached 플러그인은 그 명령을 InnoDB 스토리지 엔진으로 그대로 전달해서 실제 내부적으로는 InnoDB 테이블의 데이터를 변경하거나 조회해서 클라이언트로 반환하게 되는 것이다.



  • 두 번째 Caching 모드에서는 InnoDB 버퍼 풀과는 별도로 MySQL Memcached 플러그인이 자체적인 캐시 메모리 공간을 가지는 방식이다. 이 모드에서는 데이터를 ADD/SET/DELETE 오퍼레이션이 수행되면, MySQL Memcached 플러그인의 캐시 메모리 공간에 데이터를 변경함과 동시에 변경 내용을 InnoDB 테이블에도 같이 적용하게 된다.물론 Memcached 플러그인에 적용된 변경 사항이 InnoDB 테이블에 즉시 적용될지 또는 모아서 배치 형태로 처리될지는 MySQL 서버의 옵션을 통해서 조절할 수 있다.그리고 GET 오퍼레이션이 수행되면, Memcached 플러그인의 캐시 메모리에 존재하는 데이터라면 Memcached 플러그인 레벨에서 처리가 완료되고 그렇지 않다면 Memcached 플러그인이 InnoDB 테이블을 조회해서 클라이언트로 반환하게 된다.이 캐시 모드는 마치 MySQL 서버와 Memcached 서버를 하나로 묶어둔 것처럼 작동하는 방식이다.



  • 세 번째 Cache-only 모드는 MySQL Memcached 플러그인이 주 역할을 수행하며 InnoDB 스토리지 엔진은 전혀 처리에 관여하지 않게 된다.물론 Memcached의 캐시 메모리 공간에만 데이터가 기록되기 때문에 MySQL 서버가 재시작되면 캐시 메모리의 데이터는 모두 사라지게 될 것이다.


첫 번째와 두 번째 캐시 모드에서는 모두 Memcached 플러그인을 통해서 최종적으로는 InnoDB 스토리지 엔진으로 데이터가 전달되기 때문에 Memcached 플러그인을 통해서 유입된 데이터라 하더라도 바이너리 로그에 기록되어서 슬레이브 MySQL 서버로 전달된다.
하지만 순수한 Memcached 서버와 같은 방식으로 작동하는 세 번째 모드는 Memcached를 통한 데이터 유입이 InnoDB 스토리지 엔진으로 전달되지 않기 때문에 바이너리 로그에 기록되지 않는다.
MySQL Memcached 플러그인이 제공하는 3가지 캐시 모드중에서 첫번째와 두번째는가 각각의 목적과 용도에 맞게 훌륭하게 사용될 수 있을 것으로 보지만, 사실 3번째 Cache-only 모드는 왠지 MySQL 서버에서 필요로 할 기능처럼은 보이지 않는다. 
그런데 여기에서 제일 쓸모가 없을 것으로 보이는 세 번째 캐시 모드에서 Memcached 서버의 데이터 복제에 대한 가능성을 찾을 수 있었다.

만약 MySQL Memcached 플러그인의 Cache-only 모드에서, Memcached 플러그인을 통해서 유입된 데이터가 InnoDB 스토리지 엔진으로는 전달되지 않지만 바이너리 로그에는 기록된다면 어떻게 될까 ?

이 기능이 보완된다면, MySQL 서버를 이용해서 Memcached 서버의 데이터 복제나 동기화가 구현되는 것이다.
결국 이는 MySQL 서버의 복제 기능을 Memcached 서버에 결합하는 효과를 얻을 수 있게 되는 것이다.

물론, 바이너리 로그 기록 하나만 수정해서 모든 것이 해결되는 것은 아닐 것이다. 언급하지 않은 작은 문제들과 제약 사항들이 도사리고 있을 것이다.
하지만 MySQL 서버의 안정적인 복제 방식을 Memcached 서버에 도입할 수 있게 된다면 더 없이 좋은 솔루션이 될 것이다. 그것도 많은 시간을 투자하지 않고서 ...

(2편에서 계속 ...)