MySQL은 복제 기능을 사용해 서비스의 고가용성을 실현할 수 있지만, 단순히 소스-레플리카 구조로 구성한다고 해서 고가용성이 실현되는 것은 아니다. 소스 서버에서 장애가 발생한다면, 사용자가 여러 작업을 통해서 복구 또는 Failover 등을 별도로 실행해야 했기 때문이다.
InnoDB Cluster 는 MySQL 서버의 빌트인 형태의 HA 솔루션으로 도입되었으며, 조금 더 쉽고 편리하게 구성 및 안정적인 고가용성 구성을 할 수 있게 되었다. InnoDB Cluster 는 그 자체적으로 어떤 기능이라고 하기 보다는 고가용성 구성을 하기 위한 구성요소의 집합된 개념으로 바라볼 수 있다.
그렇다면 InnoDB Cluster는 어떻게 구성되어 있고 무엇을 통해 고가용성을 보장하는지 알아보도록 하자.
MySQL InnoDB Cluster는 MySQL Group Replication, MySQL Shell, MySQL Router 이 세 가지 구성 요소를 통해 자동 복제, 장애 감지, 자동 복구, 트래픽 라우팅등의 기능을 지원한다.

소스 서버의 데이터를 레플리카 서버로 동기화하는 기본적인 복제 역할 뿐아니라, 복제에 참여하는 서버들에 대한 자동화된 멤버 관리를 담당한다.
어플리케이션과 MySQL 서버 사이에서 동작하는 미들웨어로, 어플리케이션에서 수행한 쿼리를 적절한 MySQL로 전달하는 프록시 역할을 담당한다.
MySQL Shell은 MySQL 용 고급 클라이언트 및 코드 편집기이다. JS, Python 기반의 스크립트 작성 기능과 클러스터 구성 등의 어드민 작업을 할 수 있게 하는 API를 제공한다.
InnoDB 클러스터에서 MySQL 서버들은 그룹 복제 형태로 복제가 구성 되며, 각 노드는 Primary 혹은 Secondary 중 하나로 동작하게 된다. 여기서 읽기/쓰기가 모두 가능한 서버가 Primary이다.
그룹 복제 설정에 따라 Primary는 하나만 존재할 수도 있고 여러 대가 존재할 수도 있다. 또한, 그룹 복제를 구성할 때 고가용성을 위해 최소 3대의 서버 이상으로 구성해야하는데, 이는 3대 이상 부터 서버 한 대에 대한 장애 발생에 적절히 대응할 수 있기 때문이다.
이렇게 InnoDB 클러스터를 구성하면 클라이언트는 MySQL 서버로 직접 접근하는 것이 아니라 MySQL 라우터에 연결해서 쿼리를 실행한다. MySQL 라우터는 InnoDB 클러스터에 속한 서버들에 대한 메타데이터 정보를 지니며, 이를 통해 쿼리를 적절한 서버로 전달한다. 그렇기에 클러스터는 클러스터 내부의 노드까지는 알 필요가 없고, 단순히 MySQL 라우터 서버만 커넥션으로 설정해두면 된다.
결국 클러스터 내의 서버에 장애가 발생하면, 그룹 복제가 먼저 감지해서 복제 그룹에서 제외시키며, MySQL 라우터는 이러한 변경을 인지하고 연결된 클라이언트가 정상적인 서버로만 전달될 수 있도록 한다. 클라이언트나 어플리케이션 입장에서 보았을 때는, 특정 서버의 장애가 발생해도 별도의 조치 없이 그대로 쿼리를 실행하면 되는 것이다.
그룹 복제는 기존 MySQL 복제를 기반으로 구현되어 내부적으로 바이너리 로그, 릴레이 로그, GTID를 사용한다. 하지만, 복제 구성 형태와 트랜잭션 처리 방식 측면에서는 완전히 다른 방식이다.
기존 복제의 경우에는 일반적으로 소스-레플리카 구조로 단방향 복제가 이루어지지만, 그룹 복제에서는 복제에 참여하는 서버들이 하나의 복제 그룹으로 묶인 클러스터 형태 및 서로 통신하며 양방향으로 복제를 처리할 수도 있다. 즉, 하나의 복제 그룹에서 쓰기 처리 서버가 여러 대 존재할 수 있는 것이다.
또한, 트랜잭션이 커밋되려면 정족수 이상의 노드가 트랜잭션 순서에 동의해야 한다. (이는 뒤에 더 자세하게 살펴보겠다.)

그룹 복제는 별도 플러그인으로 구현되어 있으며, 이를 사용하기 위해서는 각 MySQL 서버에 그룹 복제 플러그인이 설치되어 있어야 한다. 이 플러그인은 내부적으로는 여러 계층의 아키텍처로 구성되어 있다. 이 아키텍처는 트랜잭션의 실행부터 복제, 충돌 감지, 합의까지 다양한 컴포넌트가 유기적으로 동작하는 구조로 되어 있다.

Group Replication 플러그인은 MySQL 서버와의 통신을 위해 다양한 API 인터페이스를 제공한다. 이 API는 Capture, Apply, Lifecycle 세 가지 주요 카테고리로 나뉜다.
이 API는 복제 플러그인이 MySQL 서버의 코어와 느슨하게 결합되도록 하며, 대부분 트랜잭션 실행 파이프라인의 특정 지점에 Hook 형태로 삽입된다. 이를 통해 양방향 통신이 가능해진다.
이 계층은 실제로 Group Replication이 동작하는 핵심 로직이 구현된 부분이다. 서버로부터 전달된 요청은 이 계층의 각 기능별 컴포넌트로 분기된다.
복제 프로토콜 계층은 트랜잭션 충돌 감지 및 순서 보장을 담당한다. 트랜잭션을 수신하면 글로벌 순서를 맞춰 브로드캐스트하며, 이를 통해 모든 노드가 동일한 순서로 트랜잭션을 수신하게 된다.
여기에서 Certifier가 동작하면서 트랜잭션 충돌을 감지하고, Group Commit의 일관성을 보장한다.
Group Replication의 핵심이자 가장 하단에 위치한 두 계층은 다음과 같다.
GCS (Group Communication System API)
XCom (eXtended COMmunications, Paxos 기반 엔진)
MySQL Group Replication은 복제 그룹 내에서 쓰기 권한을 가진 인스턴스를 하나로 제한할지, 여러 개로 허용할지에 따라 크게 두 가지 모드로 나뉜다.
싱글 프라이머리 모드는 기본 설정이며, 하나의 노드만 쓰기를 허용하고 나머지 노드는 읽기 전용으로 동작한다.

쓰기 충돌이 발생하지 않아서 복잡도가 낮으며, 장애 발생 시 자동 선출을 통해 새로운 프라이머리 노드가 설정된다. 이때 프라이머리가 선출되는 기준은 시스템 변수인 group_replication_member_weight 값을 비교하여 가장 높은 노드를 선택하며, 만약 동일한 가중치를 가진다면 UUID가 사전순으로 가장 빠른 노드를 선택한다.
멀티 프라이머리 모드는 모든 노드에서 쓰기 작업이 가능한 모드로, 분산 환경 또는 쓰기 분산이 필요한 경우 유용하다.

이렇게 여러 노드에 발생한 쓰기는 그룹의 다른 멤버들로 전파되어 각 멤버에서 다시 처리되므로 모든 노드들이 같은 서버 스펙을 가지도록 MySQL 버전을 동일하게 유지하는 것이 좋다. 이 모드에서는 모든 노드가 쓰기를 처리할 수 있으므로, 각 트랜잭션은 Conflict Detection & Certifier 단계를 거쳐 충돌 여부를 판단하고, 커밋 전에 다수 노드의 동의을 얻어야하는 등 복잡성이 증가한다.
MySQL Group Replication은 단순히 데이터를 복제하는 것을 넘어서, 모든 트랜잭션이 그룹 전체에서 일관되게 처리되도록 강력한 메커니즘을 갖추고 있다. 이를 위해 합의와 인증단계를 거친 후 최종적으로 그룹의 각 서버들에 적용된다.
합의(Consensus)는 트랜잭션을 모든 노드가 동일한 순서로 수신하고, 일관된 결과를 내도록 보장하기 위한 절차다. 클라이언트가 한 그룹 멤버에서 트랜잭션을 실행하고 커밋 요청을 보내면 XCom 엔진을 통해 트랜잭션에서 변경한 데이터에 대한 WriteSet과 커밋될 당시의 gtid_executed 스냅숏 정보, 이벤트 로그 데이터 등이 포함된 트랜잭션 데이터를 다른 그룹 멤버들로 전파한다. 이렇게 트랜잭션 데이터를 전파하며 Paxos 기반의 프로토콜을 바탕으로 멤버들 간의 합의를 수행되며, 최종적으로 합의가 완료되어 과반수 이상에 해당하는 ACK를 전달받으면 해당 멤버는 그 다음 프로세스를 진행한다. 만약, 과반수 이상의 멤버로부터 응답을 받지 못하면 트랜잭션은 적용되지 않는다.
이렇게, 다수의 그룹 멤버들에서 실행된 트랜잭션들은 합의 단계를 거친 후 글로벌하게 정렬 되어, 동일한 순서로 인증 단계(Certification)를 거치게 된다. 이 단계는 받은 트랜잭션이 기존에 커밋된 트랜잭션들과 충돌하지 않는지 확인하는 절차이다. 트랜잭션의 WriteSet을 기반으로 이미 커밋된 트랜잭션들과 충돌 여부를 비교하고, 충돌이 없으면 커밋, 있다면 Abort한다. 이러한 충돌은 사실 그룹 멤버 전체가 쓰기를 처리할 수 있는 멀티 프라이머리 모드에서만 발생 가능하며, 단일 서버에서 쓰기가 수행되는 싱글 프라이머리 모드에서는 발생하지 않는다.
이후, 전달 받은 트랜잭션 로그 데이터를 바탕으로 릴레이 로그를 작성하고, Binary log에도 기록해서 최종적으로 트랜잭션을 반영하게 된다.

결국 Group Replication에서 각 멤버들은 동일한 트랜잭션을 적용하지만, 실제 적용 시점까지 완전히 일치하는 것은 아니다. 한 멤버에서 쓰기를 수행한 후 바로 다른 멤버에서 데이터를 읽으면 다른 데이터를 볼 수 있고, 기존 Primary에 장애가 발생하여 새로운 Primary가 선출되는 경우 두 노드가 일치하지 않으면 데이터의 불일치를 마주할 수도 있다.
그래서 MySQL 8.0.14 버전 이후로부터 group_replication_consistency시스템 변수를 통해 트랜잭션 일관성 수준을 설정하여, 필요에 따라 원하는 수준의 일관성을 선택할 수 있게 되었다.
시스템 변수의 기본 값이며, 해당 시스템 변수가 도입되기 전에 사용하였던 기본적인 일관성 수준과 동일하다. 이름 그대로 최종적 일관성만 보장하는 수준이며, 읽기 전용 및 읽기-쓰기 트랜잭션이 별도 제약없이 바로 수행가능하다.
결국 다른 그룹 멤버들에서 일시적으로 이전 데이터가 읽혀질 수 있다. 또한, Primary Failover 상황이 발생하였을 때, 새로운 Primary가 이전 트랜잭션이 모두 적용되기 전이라면 트랜잭션 충돌도 가능하다.
아래 그림과 같은 상황에서 T2 트랜잭션이 T1이 완전히 Member 3에 적용되기 전에 실행 되었으므로, 최신 데이터가 아닐 수도 있고 T1과 충돌할 수도 있다.

BEFORE_ON_PRIMARY_FAILOVER 일관성 수준은 새로운 Primary가 선출되는 상황에서만 영향을 미치는 일관성 수준이다. 만약, Failover가 발생했는데 아직 이전 Primary의 트랜잭션을 적용하고 있는 경우 새로 선출된 Primary로 요청되는 트랜잭션은 모두 적용될 때까지 보류 된다.
이렇게 한다면, Primary Failover가 발생할 때 클라이언트는 항상 최신값을 볼 수 있다. 하지만, 트랜잭션이 모두 반영되기 전까지 요청이 지연되기에 트랜잭션 간의 갭이 큰 경우 대기 시간이 길어질 수 있다.
BEFORE 일관성 수준은 트랜잭션 시작 전에 그룹의 선행 트랜잭션이 모두 완료되었는지 확인하는 일관성 수준이다. 아래 그림 처럼 Member 3에서 T1 트랜잭션이 적용되고 나서 T2 트랜잭션이 수행된다.

이 일관성 수준으로 설정된 읽기 전용 및 읽기-쓰기 트랜잭션은 항상 최신 데이터를 읽으며, 처리 시간은 선행 트랜잭션의 처리 시간에 영향을 받는다. 그래서 앞선 트랜잭션의 처리 시간이 길다면 처리가 지연된다고 볼 수 있다.
AFTER 일관성 수준은 트랜잭션 커밋 후, 자신의 변경사항이 그룹 내 다른 멤버에 적용되었는지 확인하는 일관성 수준이다. 읽기-쓰기 트랜잭션은 다른 모든 멤버들에서도 커밋될 준비가 됐을 때까지 기다린 후 최종 처리 되며, 읽기 전용 트랜잭션은 변경사항이 없으므로 제약 없이 처리된다. 아래 그림처럼, 그룹의 다른 멤버로 부터 Prepare응답을 받으면 Member 1에서 최종적으로 커밋이 수행된다.

이렇게 모든 멤버들에 대한 커밋을 된 것을 확인하는 일관성 수준이기 때문에 후에 그룹의 어떤 멤버에서든 일관된 최신 데이터를 얻을 수 있다.
BEFORE 수준과 AFTER 수준이 결합된 형태로, 읽기-쓰기 트랜잭션은 모든 선행 트랜잭션이 적용될 때까지 기다린 후 실행되며, 트랜잭션이 다른 멤버들에서도 커밋이 준비되어 응답을 보내면 그때 최종적으로 커밋된다. 읽기 전용 트랜잭션은 모든 선행 트랜잭션이 적용될 때까지 대기한 후 실행된다.
즉, 읽기-쓰기 트랜잭션은 AFTER, 읽기 트랜잭션은 BEFORE 일관성 수준으로 동작하는 것이다.

Group Replication은 모든 멤버가 동기화 상태를 유지하며 고가용성을 제공해야 하므로, 일부 멤버가 느려서 복제 지연이 발생할 경우 전체 시스템의 안정성에 영향을 줄 수 있다. 앞서 소개한 트랜잭션 일관성 수준을 조정해서 이 같은 문제를 해결할 수 있지만 근본적인 원인 해결법은 아니고 지연이 장시간 지속된다면 해결책이 될 수 없다.
그렇기에 흐름 제어 메커니즘이 사용되며, 이를 통해 멤버 간 트랜잭션 갭을 적게 유지해서 최대한 동기화된 상태로 유지될 수 있게 한다.
🚦 동작 방식
- 보조 멤버가 받은 트랜잭션을 적용하지 못하고 큐에 쌓기 시작함
- 이 큐의 크기가 설정된 시스템 변수 이상으로 커지면, 해당 멤버는 흐름 제어 요청을 브로드캐스트함
- 흐름 제어가 활성화되면
- 프라이머리 서버가 새로운 트랜잭션 수락 속도를 줄이거나 일시 정지
- 다른 멤버들도 트랜잭션 처리량을 조절
- 느린 멤버가 트랜잭션을 처리해 큐가 줄어들면 흐름 제어 해제
즉, 느린 멤버가 과도하게 뒤처지면 복제 지연이 심각해져, 최종 일관성이 깨지거나 장애 시 복구가 어려워 지기에 전체적인 트랜잭션 처리 속도를 조절하는 것이다.
InnoDB 클러스터는 고가용성을 핵심 목표로 하기 때문에, 장애 발생 시 자동으로 감지하고 대응하는 메커니즘이 내장되어 있다. 이 메커니즘에서는 문제 상태에 있는 멤버를 식별하고 해당 멤버를 Group Replication에서 제외시킴으로써 그룹이 정상적으로 동작할 수 있도록 한다.
이를 위해 Group Replication에서는 각 멤버 간에 상태 정보를 지속적으로 교환하여 정상 여부를 판단한다. 이때, 멤버 간 네트워크 통신이 끊기거나 응답이 일정 시간 이상 지연되면, 해당 멤버는 그룹에서 추방된다.
멤버가 추방되고 나서 다른 그룹 멤버들과 통신을 다시 재개할 수 있는 경우에 해당 멤버는 자신이 추방되었음을 알게 된다. 멤버가 추방되면 그룹 뷰가 변경되며 그룹 멤버들은 다른 그룹 뷰 ID를 가지게 되는데, 이때 추방된 멤버가 가진 그룹 뷰 ID와 다르므로 이때 알아차리는 것이다. MySQL 8.0.16 버전 이후로부터 멤버는 그룹에서 추방되면 group_replication_autorejoin_tries 시스템 변수 값에 따라 재가입을 시도하게 된다.
추방된 멤버가 다시 다른 멤버들과 통신이 되지 않는 경우에는 자신이 추방되었음을 알지 못한다. 기본적으로 이와 같은 네트워크 분할로 인해 분리되는 경우 group_replication_unreachable_majority_timeout 변수를 통해 일정 시간 동안 대기한 후 스스로 탈퇴하도록 설정할 수 있다. 기본값은 0이라 기본적으로는 탈퇴하지 않고 계속 남아있다. 이때, 소수에 속한 멤버들에서도 트랜잭션이 실행될 수는 있지만 정족수를 맞추지 못했기에 보류된 상태로 남아있다가 그룹에서 탈퇴할 때, 트랜잭션을 모두 롤백한다.
멤버가 그룹에 새로 가입하거나 혹은 탈퇴 후 재합류할 때, 기존 멤버들과 데이터를 동기화하여 일관성을 유지해야한다. 그래서 누락된 트랜잭션들을 다른 그룹 멤버에서 가져와 적용하는 복구 프로세스를 수행하는데 이를 “분산 복구”라고 한다. 이때, 복구 작업을 위해 랜덤으로 선택하는 멤버가 존재하는데 이를 기증자 멤버라고 하며, 온라인 상태인 모든 멤버들을 선택할 수 있다.
복구 작업 시 먼저 가입하는 멤버에서 group_replication_applier복제 채널의 릴레이 로그를 확인하는데, 만약 이전에 가입한 적이 있다면 탈퇴하는 시점에 릴레이 로그에는 있으나 아직 반영되지 않은 트랜잭션이 존재할 수 있기 때문이다. 그래서 먼저 이러한 트랜잭션을 적용하는 것으로 복구 작업을 시작한다.
이후, 다른 그룹 멤버에 연결해서 분산 복구 작업을 진행하는데 이는 두 가지 방식을 사용해서 작업을 진행한다.
기존 멤버가 가진 Binary Log를 기반으로 복제되지 않은 트랜잭션만 전송하여 동기화 하는 방식이다. 그렇기 때문에 매우 빠르고 리소스 소모가 적다.
다른 멤버로 부터 전체 스냅샷을 전달받아 데이터를 동기화 하는 방식이다. 만약 새로운 노드나 트랜잭션 갭이 크다면 이 방식으로 복구를 시작하고, 그렇지 않다면 Binary Log 복제 방식을 사용한다.
그래서 분산 복구 작업은 다음과 같은 세 단계로 이루어진다.
가입 멤버가 이전에 그룹에 가입한 적이 있는 경우 릴레이 로그에 있는 아직 적용안된 트랜잭션을 적용한다.

가입 멤버는 그룹의 기존 멤버들에서 기증자 멤버를 선택해서 데이터 또는 누락된 트랜잭션을 가져온다. 이 작업을 진행하는 동안 현재 그룹에서 처리되는 트랜잭션들은 내부적으로 캐싱해둔다.

글로벌 복구 단계에서 캐싱해둔 트랜잭션을 적용하여 최종적으로 그룹에 참여한다.

MySQL Shell은 MySQL을 위한 고급 클라이언트 툴로, 단순히 CLI 수준을 넘어 스크립트 기반 자동화, 클러스터 관리 기능, JS 및 Python 인터페이스를 제공한다.
# 쉘 실행 및 JS 모드 진입
$ mysqlsh --uri root@localhost:3306 --js
# 클러스터 생성
\> var cluster = dba.createCluster("myCluster");
# 클러스터에 노드 추가
\> cluster.addInstance("root@node2:3306");
# 클러스터 상태 확인
\> cluster.status();
# 클러스터에서 노드 제거
\> cluster.removeInstance("root@node3:3306");
# SQL 모드 전환
\> \sql
MySQL Router는 클라이언트와 InnoDB 클러스터 사이에 위치해 트래픽을 적절한 서버로 라우팅해주는 프록시 서버이다. 클라이언트는 클러스터의 노드를 직접 알 필요 없이 Router만 알면 되는 구조가 된다.

위 그림처럼 어플리케이션에서는 MySQL 라우터를 커넥션 설정에 사용하며, 라우터 내부에서는 클러스터 내 MySQL 서버들에 대한 정보를 메모리에 캐시하며 이를 주기적으로 갱신한다. 만약, 클러스터의 서버 구성이 변경되면 MySQL 라우터는 갱신된 정보를 자동으로 감지하므로 따로 어플리케이션 단에서 정보를 변경해줄 필요가 없다.
또한, MySQL 라우터는 어플리케이션 서버에서 요청한 쿼리들을 여러 MySQL 노드에 나눠서 처리하도록 로드 밸런싱을 수행할 수도 있다. 그 뿐 아니라 MySQL 서버에서 장애가 발생한 경우 자동으로 감지하고 다른 MySQL 서버로 쿼리 실행을 재시도한다. 어플리케이션 서버단에서는 별도의 장애 조치 없이 정상적인 쿼리 결과 값을 받아올 수 있게 되는 것이다.
$ mysqlrouter --bootstrap root@localhost:3306 --user=mysqlrouter
위의 명령어를 실행하면 클러스터 메타데이터를 기반으로 라우터 설정이 자동으로 생성되고, 포트 번호도 자동으로 할당된다. ex) Read-Write: 6446, Read: 6447
https://dev.mysql.com/doc/refman/8.0/en/mysql-innodb-cluster-introduction.html
https://dev.mysql.com/blog-archive/group-replication-consistent-reads-deep-dive/
https://docs.oracle.com/cd/E17952_01/mysql-5.7-en/group-replication-view-changes.html