Galera Cluster MySQL 고가용성, NGINX Plus로 달성
이 포스트에서는 MySQL, Galera Cluster 및 NGINX Plus R5에서 도입된 TCP 로드 밸런싱 기능을 사용하여 고가용성 데이터베이스 클러스터를 생성하고 테스트합니다. NGINX Plus로 적절한 MySQL 로드 밸런싱 구성과 데이터베이스 클러스터에서 쓰기 충돌 및 장애를 처리하는 기술을 시연합니다.

목차
1. Galera Cluster 소개
2. MySQL 로드 밸런싱을 위한 NGINX Plus 구성
3. Galera Cluster 테스트
4. 병렬 업데이트 문제 처리
5. MySQL 고가용성을 위한 더 나은 솔루션
5-1. 데이터베이스 장애에 대한 복원력
1. Galera Cluster 소개
Galera Cluster 는 MySQL 데이터베이스 서버 클러스터를 위한 동기식 복제 솔루션입니다. Galera Cluster 의 모든 노드에 데이터베이스 쓰기가 즉시 복제되며, 모든 데이터베이스 서버는 주(Primary) 노드로 작동합니다. 일부 주의 사항과 신중한 성능 테스트를 통해, 로드 밸런싱된 Galera Cluster 는 매우 높은 가용성이 필요한 중요한 비즈니스 데이터의 단일 MySQL 데이터베이스 대신 사용될 수 있습니다.
이 포스트의 예시에서는 stock MySQL을 사용하며, DigitalOcean에서 실행 중인 Ubuntu 이미지에 세 개의 데이터베이스 서버 (db1, db2, db3)를 배포하기 위해 이 설치 지침을 따릅니다. 설치 및 클러스터 부트스트래핑은 신중하게 수행되어야 하며, 각각의 데이터베이스 서버가 두 개의 피어로부터 복제되는 안정적인 클러스터를 생성하는 것이 최종 목표입니다. Galera Cluster 는 MariaDB와 Percona XtrDB Cluster도 지원합니다.
autoindex id, 값(문자열), 동일한 값이 테이블에 삽입될 때마다 증가하는 개수(정수)를 포함하는 간단한 데이터베이스 테이블(test.data)로 시작합니다.
CREATE TABLE data (
id INTEGER NOT NULL AUTO_INCREMENT,
value CHAR(30),
count INTEGER,
PRIMARY KEY (value),
KEY (id)
);
하나의 데이터베이스 인스턴스에서 이 CREATE를 실행하고 테이블이 피어 인스턴스에 복제되는지 확인합니다.
2. MySQL 로드 밸런싱을 위한 NGINX Plus 구성
NGINX Plus를 구성하여 라운드 로빈 방식(기본값)으로 세 서버 간에 데이터베이스 연결의 부하를 분산합니다.
stream {
upstream db {
server db1:3306;
server db2:3306;
server db3:3306;
}
server {
listen 3306;
proxy_pass db;
proxy_connect_timeout 1s; # detect failure quickly
}
}
그런 다음 NGINX Plus를 통해 데이터베이스에 연결하고 SQL을 사용하여 연결할 인스턴스를 결정합니다.
# mysql -u galera -p --protocol=tcp
Enter password: ********
mysql> SHOW VARIABLES WHERE Variable_name = 'hostname';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| hostname | db2 |
+---------------+-------+
NGINX Plus가 라운드 로빈 방식으로 3개의 서버에 로드 밸런싱을 하고 있는지 확인하기 위해 반복적으로 재연결할 수 있습니다.
3. Galera Cluster 테스트
데이터베이스 클러스터를 테스트하기 위해 테이블에 항목을 삽입하고 일부로 충돌을 유발합니다. 새 항목은 1부터 시작하며, 중복된 항목(충돌)은 카운트를 증가시킵니다.
mysql> INSERT INTO data (value, count) VALUES ( '$value', 1 )
ON DUPLICATE KEY UPDATE count=count+1;
첨부된 간단한 Perl 스크립트(query1.pl)는 100개의 항목을 삽입하거나 증가시키고, 각 트랜잭션이 전송된 인스턴스의 이름을 출력합니다.
$ ./query1.pl
db3
db1
db2
db3
...
mysql> SELECT * FROM data;
+-----+-----------+-------+
| id | value | count |
+-----+-----------+-------+
| 3 | value-000 | 1 |
| 4 | value-001 | 1 |
...
| 101 | value-098 | 1 |
| 102 | value-099 | 1 |
+-----+-----------+-------+
100 rows in set (0.04 sec)
UPDATE가 데이터베이스 간에 라운드 로빈으로 로드 밸런싱되고, 연속적으로 더 많은 UPDATE를 실행할 때 카운트가 올바르게 업데이트되는 것을 관찰합니다.
$ ./query1.pl ; ./query1.pl ; ./query1.pl
...
mysql> SELECT * FROM data;
+-----+-----------+-------+
| id | value | count |
+-----+-----------+-------+
| 3 | value-000 | 4 |
| 4 | value-001 | 4 |
...
| 101 | value-098 | 4 |
| 102 | value-099 | 4 |
+-----+-----------+-------+
100 rows in set (0.04 sec)
4. 병렬 업데이트 문제 처리
새로운 데이터베이스 테이블로 다시 시작합니다.
mysql> DROP TABLE data;
mysql> CREATE TABLE data ( id INTEGER NOT NULL AUTO_INCREMENT, value CHAR(30),
count INTEGER, PRIMARY KEY (value), KEY (id) );
업데이트를 병렬로 수행합니다(인스턴스 20개).
$ for i in {1..20} ; do ( ./query1.pl& ) ; done
데이터베이스에서 가끔 오류가 발생합니다.
DBD::mysql::st execute failed: Deadlock found when trying to get lock; try
restarting transaction at ./query1.pl line 42.
표를 살펴보면 20개까지 증가한 항목이 거의 없음을 알 수 있습니다.
mysql> SELECT * FROM data;
+------+-----------+-------+
| id | value | count |
+------+-----------+-------+
| 1 | value-000 | 14 |
| 31 | value-001 | 15 |
...
| 2566 | value-098 | 18 |
| 2601 | value-099 | 20 |
+------+-----------+-------+
100 rows in set (0.03 sec)
이는 Galera Cluster 복제 프로세스의 결과입니다. 동일한 레코드에 대한 UPDATE가 데이터베이스 간에 병렬로 실행될 때 데드락이 발생할 수 있고, 데이터베이스는 해당 트랜잭션을 거부합니다.
일부 상황에서는 이러한 동작이 허용될 수 있습니다. 만약 애플리케이션이 충돌하는 업데이트를 병렬로 제출하는 가능성이 없고, 애플리케이션 코드가 매우 드문 거부 트랜잭션을 정상적으로 처리할 경우(예: 사용자에게 오류 반환) 심각한 문제가 아닐 수 있습니다.
이 동작이 허용되지 않은 경우 가장 간단한 솔루션은 업스트림 서버 그룹에서 하나의 주 데이터베이스 인스턴스를 지정하는 것입니다. 다른 인스턴스를 백업으로 표시하고 비활성화하면 됩니다.
upstream db {
server db1:3306;
server db2:3306 backup;
server db3:3306 down;
}
이 구성으로는 모든 트랜잭션이 db1로 라우팅 됩니다. db1이 실패하면 현재 연결이 끊기고 NGINX Plus 는 새로운 연결을 위해 db2로 장애 조치(failover)합니다. db3는 클러스터에서 조용한 파트너로 작동하며, db1, db2로부터만 업데이트를 받습니다.
이 구성을 테스트하면 각 데이터베이스 인스턴스에서 모든 항목이 존재하고 올바른 count 값인 20을 갖는 것을 확인할 수 있습니다. 그러나 테스트 중에 db1이 실패하면 일부 트랜잭션이 손실될 수 있습니다.
5. MySQL 고가용성을 위한 더 나은 솔루션
트랜잭션이 실패하는 경우는 다양합니다. 매우 높은 수준의 보호가 필요한 경우 애플리케이션이 실패한 트랜잭션을 적절하게 감지하고 재시도할 수 있도록 해야 합니다. NGINX Plus 와 Galera Cluster 는 나머지 처리를 담당할 수 있습니다.
테스트 클라이언트에서는 query2.pl에 표시된 것처럼 짧은 일시 중지 후 트랜잭션을 다시 시작하는 예외 처리기에서 오류를 포착하고 데이터베이스 트랜잭션을 래핑하는 것으로 충분합니다.
my $backoff = 0.1; # exponential backoff, in seconds
TRY:
eval {
... YOUR CODE HERE ...
... perform DB operations, and call die() on failure
} or do {
print "Failed: $@";
select( undef, undef, undef, $backoff );
$backoff *= 1.5;
goto TRY;
}
이 수정된 접근 방식으로 시스템은 병렬로 업데이트를 처리하고 데드락 오류가 발생하는 경우에는 트랜잭션을 다시 시도합니다. 이를 통해 카운트가 올바르게 증가되고 시스템이 여러 개의 활성 기본 노드로 신뢰성을 유지하는 것이 보장됩니다.
mysql> SELECT * FROM data;
+------+-----------+-------+
| id | value | count |
+------+-----------+-------+
| 1 | value-000 | 20 |
| 33 | value-001 | 20 |
...
| 2964 | value-098 | 20 |
| 2993 | value-099 | 20 |
+------+-----------+-------+
100 rows in set (0.04 sec)
5-1. 데이터베이스 장애에 대한 복원력
이 시스템은 데이터베이스 장애에도 견고합니다. 데이터베이스 서버를 종료한 다음 클러스터에 병렬 업데이트를 제출하여 장애를 시뮬레이션할 수 있습니다. NGINX 서버 구성에서 proxy_connect_timeout 값을 1초(1s)로 줄여서 NGINX가 연결 실패를 신속히 감지할 수 있도록 합니다.
Failed: Can't connect to DBI:mysql:test:dev: Lost connection to MySQL server at
'reading initial communication packet', system error: 0 at ./query2.pl line 28.
Failed: Can't execute 'SHOW VARIABLES WHERE Variable_name = 'hostname';': Lost
connection to MySQL server during query at ./query2.pl line 33.
Failed: Can't execute 'INSERT INTO data (value, count) VALUES ( 'value-020', 1 )
ON DUPLICATE KEY UPDATE count=count+1': Unknown command at ./query2.pl line 45.
테스트 중에 이러한 오류에도 불구하고, 어떤 트랜잭션도 삭제되거나 두 번 실행되지 않았으며, 세 개의 데이터베이스는 일관된 상태를 유지했습니다. 적절한 애플리케이션 로직, NGINX Plus 로드 밸런싱 및 Galera Cluster 의 조합은 견고하고 고성능이며 무엇보다 100% 신뢰할 수 있는 MySQL 데이터베이스 클러스터를 제공합니다.
자체 환경에서 NGINX Plus를 사용하여 Galera Cluster MySQL 로드 밸런싱을 시도해보려면 30일 무료 평가판을 시작하거나 NGINX STORE에 문의하십시오.
사용 사례에 대해 최신 소식을 더 빠르게 전달받고 싶으시면 아래 뉴스레터를 구독하세요.
댓글을 달려면 로그인해야 합니다.