MySQL 활용해 DB Replication 적용해보기 (Master - Slave 구조)

송어·2024년 4월 29일
0
@Transactional(readOnly = true)
@Service
@RequiredArgsConstructor
public class ProductService {


	private final ProductRepository productRepository;
    private final ProductNumberFactory productNumberFactory;

    public List<ProductResponse> getSellingProducts() {

        List<Product> products = productRepository.findAllBySellingTypeIn(forDisplay());

        return products.stream()
                .map(ProductResponse::of)
                .collect(Collectors.toList());
    }

	@Transactional
    public ProductResponse createProduct(ProductCreateServiceRequest request) {

        String latestProductNumber = productNumberFactory.createNextProductNumber();
        Product product = request.toEntity(latestProductNumber);

        Product savedProduct = productRepository.save(product);

        return ProductResponse.of(savedProduct);
    }

}

최근 테스트 코드 공부를 하면서 @Transactional의 readOnly = true 키워드에 대해 간결하게 학습하는 시간을 가졌었다. readOnly = true를 설정하게 되면 JPA의 세션 플러시 모드가 MANUAL로 설정되어 강제로 flush를 호출하지 않는 한 CUD작업이 동작하지 않는다.
또한 조회되는 Entity가 조회용임을 인식하고 변경 감지를 위한 Snapshot를 따로 보관하지 않아 메모리 절약의 이점도 챙길 수 있다.

이 외에도 불필요한 트랜잭션 ID를 제거해 트랜잭션 ID 설정과 관련된 오버헤드를 최소화할 수 있다.(트랜잭션 ID 유일성 보장, 트랜잭션 간 충돌방지 등의 추가 작업 최소화)

관련 공부를 하면서 Transaction 세팅에 따라 DB도 READ 작업과 CUD 작업을 나누어서 할 수 있도록 이중화해보는 방법에 대한 키워드를 전달받았다.

DB Replication

DB Replication 이란 DB를 복제해서 여러대의 DB 서버를 사용하는 것이다. Transaction을 읽기, 쓰기 전용으로 나누는 것에 추가적으로 READ 작업을 담당하는 DB와 쓰기 작업을 담당하는 DB로 이중화하면 DB의 부하를 분한시킬 수 있고 확장성 측면에서도 이점을 가져갈 수 있다.

여기서 기준이 되는 DB 서버를 Master DB라고 지칭하고 하위에 사용하는 레플리카 DB를 Slave DB라 지칭한다. Master DB는 데이터 삽입/변경/삭제(CUD) 즉 쓰기 작업을 담당하고, Master DB에서 해당 작업을 수행할 경우 그 명령을 Slave DB에도 똑같이 보낸다.

동작 방식 상세 설명

replication은 다음과 같은 순서로 진행된다.

  • Master DB가 binary log를 만들어 이벤트(데이터 변경 사항)를 기록
  • 각 Slave DB는 어떤 이벤트까지 저장되어 있는지를 기억하는 상태
  • lave DB의 IO thread를 통해 Master DB에 이벤트를 요청하고 전달 받음
  • Master DB는 이벤트 요청을 받으면 binlog dump thread를 통해 클라이언트에게 이벤트를 전송함
  • Slave DB의 IO thread는 전송받은 binlog dump thread를 통해 전달받은 binary log를 이용해 relay log를 만들어 이벤트를 저장한다.
  • Slave DB의 SQL thread에서 relay log를 읽어 이벤트를 다시 실행해 Slave DB에 데이터를 복사

용어 설명

1. binary log

MySQL에서 데이터 & 스키마의 변경 사항을 기록하는 로그이다. MySQL 서버에서 실행된 모든 데이터 변경 작업(INSERT, UPDATE, DELETE)을 기록하여 데이터베이스의 변경 이력을 추적한다.
데이터를 아카이브한 후 해당 이벤트를 기록한 binary log가 있으면 원하는 시점으로 데이터를 복구할 수 있고, DB를 변경하는 모든 이벤트가 저장되어 있으므로 이를 Slave DB에서 다시 실행하는 것 만으로도 데이터베이스 복사가 가능해진다.

2. binlog dump thread

binlog dump thread는 MySQL에서 복제 작업(replication)을 수행할 때 Master DB에서 사용되는 스레드이다. 이 스레드가 하는 일은 단순히 Slave DB가 이벤트 요청 시 binary log에 락을 걸고, 이벤트를 읽어 Slave DB로 이벤트를 전송한다. binary log에 걸린 락은 thread가 log를 읽은 시점에 바로 해제된다.

3. I/O thread

Slave DB에 있는 2개의 thread 중 하나로 Master DB로부터의 binary log 요청 및 저장(relay log로 복사)을 담당한다. Slave DB가 마지막까지 읽었던 이벤트를 기억해 다음 이벤트를 요청하는 방식을 사용한다.

4. Relay log

Relay log는 Slave DB에서 사용되며 Master DB로 부터 binary log를 받아와 저장해 Slave DB에 Master DB의 변경사항을 반영할 수 있다. 보통 SQL thread가 읽은 이벤트는 relay log 파일에서 지워지지만, SQL thread가 멈춘 상태에서 relay log 파일의 크기가 설정된 최대 크기에 도달하거나 설정된 보관 기간을 초과할 경우, Relay log 파일은 자동으로 회전(rotated)되어 새로운 파일로 교체된다. 이 과정에서 이전에 처리된 이벤트들은 제거되고, 새로운 이벤트들이 새로운 파일에 계속해서 기록된다.

5. SQL thread

SQL thread는 I/O thread로부터 만들어진 relay log를 SQL 명령문으로 변환하고 Slave DB에 적용해 Master DB와의 동기화를 담당하는 스레드이다. 트랜잭션 단위로 변경 사항을 읽어들이게 되고, 스레드가 작업을 마치게 되면 relay log를 통해 수행한 이벤트들은 소비되어 relay log 파일에서 제거된다.

container 세팅

version: '3'
services:
  db-master:
    build:
      context: ./master
      dockerfile: Dockerfile
    restart: always
    environment:
      MYSQL_DATABASE: 'testdb'
      MYSQL_USER: 'user'
      MYSQL_PASSWORD: '1234'
      MYSQL_ROOT_PASSWORD: '{password}'

    ports:
      - '4000:3306'
    container_name: master-db
    volumes:
      - my-db-master:/var/lib/mysql
      - my-db-master:/var/lib/mysql-files
    networks:
      - net-mysql

  db-slave:
    build:
      context: ./slave
      dockerfile: Dockerfile
    restart: always
    environment:
      MYSQL_DATABASE: 'testdb'
      MYSQL_USER: 'user'
      MYSQL_PASSWORD: '1234'
      MYSQL_ROOT_PASSWORD: '{password}'
    ports:
      - '4001:3306'
    container_name: slave-db
    volumes:
      - my-db-slave:/var/lib/mysql
      - my-db-slave:/var/lib/mysql-files
    networks:
      - net-mysql

volumes:
  my-db-master:
  my-db-slave:

networks:
  net-mysql:
    driver: bridge

docker-compose 명세이다. context는 master & slave 별 경로를 따로 지정해주었다.

failed to solve: failed to compute cache key: failed to calculate checksum of ref a6b2d3be-7d2a-498f-ac0b-7354e5573073::642bzq6zogx9slk3n5okrnat0: failed to walk /var/lib/docker/tmp/buildkit-mount4230017971/master: lstat /var/lib/docker/tmp/buildkit-mount4230017971/master: no such file or directory

지정을 따로 안해주니 경로를 찾을 수 없다고 나왔다.

그리고 master와 slave의 volume 설정 후 network를 연결했다.

master & slave

FROM mysql:8.0

COPY my.cnf /etc/mysql/my.cnf
CMD ["--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"]

master & slave의 Dockerfile 명세는 동일하다. my.cnf를 커스텀해 경로에 넣어주어야 하므로 맞춰서 작성해주었다.

[mysqld]
character-set-client-handshake = FALSE
character-set-server           = utf8mb4
collation-server               = utf8mb4_unicode_ci
default-time-zone='+9:00'

log-bin = mysql-bin
server-id=1

default_authentication_plugin=mysql_native_password

master의 cnf 명세이다. log-bin = mysql-bin으로 설정해 binlog 파일의 이름을 따로 설정해주었다.

[mysqld]
character-set-client-handshake = FALSE
character-set-server           = utf8mb4
collation-server               = utf8mb4_unicode_ci
default-time-zone='+9:00'

log_bin = mysql-bin
server-id=2 # master와 server-id는 반드시 다르게 설정해주어야 함

relay_log = /var/lib/mysql/mysql-relay-bin

read_only = 1

default_authentication_plugin=mysql_native_password

slave의 cnf파일 명세이다. master의 cnf파일과 거의 동일하지만 server-id=2로 masterDB와 다르게 설정해주어야 한다.

 Last_IO_Errno: 13117
                Last_IO_Error: Fatal error: The replica I/O thread stops because source and replica have equal MySQL server ids; these ids must
be different for replication to work (or the --replicate-same-server-id option must be used on replica but this does not always make sense; plea
se check the manual before using it).

server-id를 동일하게 설정할 시 master-slave를 연결하는 과정에서 해당 오류가 발생한다.

상세 setting 시나리오

docker-compose up -d

docker-compose.yml파일이 있는 경로에서 해당 명령어를 실행하면, compose파일에 작성한 명세대로 master & slave DB가 띄워질 것이다.

docker network ls
NETWORK ID     NAME               DRIVER    SCOPE
2a89a0b961d0   bridge             bridge    local
546af0ad21a4   docker_net-mysql   bridge    local # DB와 연결된 network 확인
859df5991029   host               host      local
fcf08119a4f3   none               null      local
6976fb7d24e2   practice           bridge    local
docker inspect docker_net-mysql # 네트워크 세부 정보 체크
 "Containers": {
            "8226dcecd26a7b6df2cd1c917526f16387e52402425ab66b108ff4f2f4f7cb8e": {
                "Name": "master-db",
                "EndpointID": "875bd9a190bb87b6a48881197146b01c25f9310323c157f39bfff36c59cf0f3e",
                "MacAddress": "02:42:ac:1b:00:02",
                "IPv4Address": "172.xx.0.2/16", # slave 연결에 사용
                "IPv6Address": ""
            },
            "f82287e600867565dbe4c941490988feddc443729f5fda2fc0bfdcb08dc57646": {
                "Name": "slave-db",
                "EndpointID": "9fb1a6e523d8f297042d38a9cb5c96246150727110a99d9e3963cac9c63650ba",
                "MacAddress": "02:42:ac:1b:00:03",
                "IPv4Address": "172.xx.0.3/16",
                "IPv6Address": ""
            }
        },

DB에 연결된 network 세부 정보를 조회해 master-db의 IPv4Address를 따야한다. slave 연결에 해당 IP가 사용되므로 반드시 기억해두자.

docker exec -it # master-db mysql -u root -p # master db 접속

show master status; # master db의 binlog 및 positionNumber 확인

SHOW VARIABLES LIKE 'server-id'; # server-id 체크 -> 1이어야함 !!

master db로 접속해 db의 binlog 및 positionNumber를 확인한다.

+---------------+----------+--------------+------------------+-------------------+
| File          | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+---------------+----------+--------------+------------------+-------------------+
| mysql-bin.000003 |   157 |              |                  |                   |
+---------------+----------+--------------+------------------+-------------------+

file이름과 postion 번호를 기억 & 기록해두어 slave와의 연결에 사용할 것이다.

docker exec -it # slave-db mysql -u root -p # slave db 접속

SHOW VARIABLES LIKE 'server-id'; # server-id 체크 -> 반드시 2이어야함 !!

이제 slave DB로 접속 후 server-id를 먼저 확인해 cnf파일에 설정한 내용과 일치하는지 확인한다. 만약 server-id가 1인 경우 작성한 cnf파일이 반영되지 않은 것이다.

CHANGE MASTER TO MASTER_HOST='{masterDB의 IP}',
    MASTER_USER='root',
    MASTER_PASSWORD='{masterDB의 password}',
    MASTER_LOG_FILE='{masterDB의 binlog}',
    MASTER_LOG_POS={masterDB의 positioin번호};

해당 명령어를 사용해 master DB 정보를 slave DB에 설정해 master로부터 데이터 복제를 위한 설정을 지정한다.

start slave;

이후 위 명령어를 사용해 slave를 실행시킨다.

show slave status\G;

해당 명령어로 replication 설정이 잘 되어있는지 확인한다.

*************************** 1. row ***************************
               Slave_IO_State: Waiting for source to send event
                  Master_Host: 172.27.0.2
                  Master_User: root
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000003
          Read_Master_Log_Pos: 157
               Relay_Log_File: mysql-relay-bin.000002
                Relay_Log_Pos: 326
        Relay_Master_Log_File: mysql-bin.000003
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB:
          Replicate_Ignore_DB:
           Replicate_Do_Table:
       Replicate_Ignore_Table:
      Replicate_Wild_Do_Table:
  Replicate_Wild_Ignore_Table:

// 이하 생략

위같은 정보들이 출력된다. Slave_IO_RunningSlave_SQL_RunningYes라면 올바르게 연결된 것이다. 만약 Slave_IO_RunningNo 혹은 Connecting이라고 나온다면 문제가 생긴 것이므로 확인해 보아야 한다.

에러 정보의 경우 Last_IO_Errno, Last_IO_Error, Last_SQL_Errno, Last_SQL_Error에 출력될 것이다.

show global variables like '%read_only%';

추가적으로 위 명령어를 사용해 이전에 slave DB의 cnf파일에 적용한 read_only 옵션이 적용되었는지 체크해보자.

+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_read_only      | OFF   |
| read_only             | ON    |
| super_read_only       | OFF   |
| transaction_read_only | OFF   |
+-----------------------+-------+

이렇게 나온다면 성공적으로 적용된 것이다.

발생한 이슈

Last_IO_Errno: 2003

 Last_IO_Error: Error connecting to source 'root@172.21.0.2:xxxx'. This was attempt 1/86400, with a delay of 60 seconds between a
ttempts. Message: Can't connect to MySQL server on '172.21.0.2:xxxx' (111)

slave DB가 master DB에 연결할 수 없을 때 발생하는 문제이다. 연결 시 입력한 masterDB의 IP주소나 port번호가 일치하지 않는 경우 발생한다. 내가 겪은 케이스는 포워딩한 port번호 4000번을 slaveDB 연결시 등록해 본래 masterDB가 가진 port번호인 3306과 일치하지 않아 해당 에러가 발생했다.

Last_IO_Errno: 2061

 Last_IO_Error: Error connecting to source 'root@172.21.0.2:3306'. This was attempt 1/86400, with a delay of 60 seconds between a
ttempts. Message: Authentication plugin 'caching_sha2_password' reported error: Authentication requires secure connection.

caching_sha2_password는 기본적인 SHA-256 인증을 구현한 플러그인인 sha256_password와 동일하지만 성능 향상을 위해 서버 캐싱을 이용하는 인증 플러그인이다.

caching_sha2_password를 사용하려면

  • SSL 보안연결 사용
  • RSA 보안을 적용한 비암호 연결 사용
  • 위 두가지의 경우 caching_sha2_password는 mysql의 보안 모듈을 이용
    보안 연결이나 RSA 보안을 사용하지 않은 경우에 위의 에러가 발생하는데, 이에 대한 해결 방법은 사용자의 인증 플러그인을 MySQL8.0이전 인증 방식인mysql_native_password로 지정하는 것이다.

MySQL 8.0부터는 default_authentication_plugin 이 mysql_native_password 에서 caching_sha2_password 로 변경되었기 때문에 해당 에러가 발생한다.

ALTER USER 'yourusername'@'localhost' IDENTIFIED WITH mysql_native_password BY 'youpassword';

DDL을 통해 변경하거나,

[mysqld]

default_authentication_plugin=mysql_native_password # 추가

cnf파일에 해당 설정을 추가하면 된다.

위 설정으로도 에러가 발생하는 경우 slave DB에 replication설정 시 GET_SOURCE_PUBLIC_KEY=1 옵션을 추가하자

Last_IO_Errno: 13117

Last_IO_Error: Fatal error: The replica I/O thread stops because source and replica have equal MySQL server ids; these ids must
be different for replication to work (or the --replicate-same-server-id option must be used on replica but this does not always make sense; plea
se check the manual before using it).

위에서 설명했던 masterDB의 server-id와 slaveDB의 server-id가 같은 경우 발생한다. 사전에 master & slave DB의 server-id를 다르게 설정하지 않은 경우 발생하거나 작성한 cnf가 제대로 반영되지 않는 경우 발생하므로 반드시 체크하자.

Last_SQL_Errno: 13124

Last_SQL_Error: Replica failed to initialize applier metadata structure from the repository

slave DB가 master DB로부터 받은 이벤트를 처리하기 위한 메타데이터를 초기화하는 데 실패했다는 것을 의미한다. 아래의 사유로 인해 발생할 수 있다.

  • slave DB가 master DB로부터 이벤트를 제대로 받지 못한 경우
  • 복제 설정이 잘못 구성되어 있는 경우

내가 겪은 케이스는 다른 에러로 인해 replication 설정을 다시 걸었는데, 이후 start slave; 명령어를 사용하니 해당 에러가 발생했다. 이에 대한 해결 방안은

stop slave; # slave서버의 복제 중지 (I/O스레드 중지, 마스터로부터 이벤트 수신 X)

reset slave; # slave의 복제 설정 초기화(default - clear X) -> 이전에 발생한 에러 관련 설정 제거됨

start slave;

위 명령어를 순차적으로 사용해 에러 관련 설정 정보를 제거 후 다시 slave를 실행시키는 것이다.

replication 재설정

대부분 이슈의 경우 slave DB에 replication을 재설정하는 방식으로 해결했다.
replication 재설정 작업을 하기 전 stop slave 명령어로 I/O 스레드를 중지해야 한다.

ERROR 3021 (HY000): This operation cannot be performed with a running replica io thread; run STOP REPLICA IO_THREAD FOR CHANNEL '' first.

I/O스레드를 중지하지 않은 상태에서 replication 재설정 시 해당 에러가 발생한다.

참고

https://blog.seulgi.kim/2015/05/how-mysql-replication.html
https://www.coovil.net/db-replication/
https://velog.io/@kyeun95/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EB%A6%AC%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-Replication%EC%9D%B4%EB%9E%80

0개의 댓글

관련 채용 정보