@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를 복제해서 여러대의 DB 서버를 사용하는 것이다. Transaction을 읽기, 쓰기 전용으로 나누는 것에 추가적으로 READ 작업을 담당하는 DB와 쓰기 작업을 담당하는 DB로 이중화하면 DB의 부하를 분한시킬 수 있고 확장성 측면에서도 이점을 가져갈 수 있다.
여기서 기준이 되는 DB 서버를 Master DB라고 지칭하고 하위에 사용하는 레플리카 DB를 Slave DB라 지칭한다. Master DB는 데이터 삽입/변경/삭제(CUD) 즉 쓰기 작업을 담당하고, Master DB에서 해당 작업을 수행할 경우 그 명령을 Slave DB에도 똑같이 보낸다.
replication은 다음과 같은 순서로 진행된다.
MySQL에서 데이터 & 스키마의 변경 사항을 기록하는 로그이다. MySQL 서버에서 실행된 모든 데이터 변경 작업(INSERT, UPDATE, DELETE)을 기록하여 데이터베이스의 변경 이력을 추적한다.
데이터를 아카이브한 후 해당 이벤트를 기록한 binary log가 있으면 원하는 시점으로 데이터를 복구할 수 있고, DB를 변경하는 모든 이벤트가 저장되어 있으므로 이를 Slave DB에서 다시 실행하는 것 만으로도 데이터베이스 복사가 가능해진다.
binlog dump thread는 MySQL에서 복제 작업(replication)을 수행할 때 Master DB에서 사용되는 스레드이다. 이 스레드가 하는 일은 단순히 Slave DB가 이벤트 요청 시 binary log에 락을 걸고, 이벤트를 읽어 Slave DB로 이벤트를 전송한다. binary log에 걸린 락은 thread가 log를 읽은 시점에 바로 해제된다.
Slave DB에 있는 2개의 thread 중 하나로 Master DB로부터의 binary log 요청 및 저장(relay log로 복사)을 담당한다. Slave DB가 마지막까지 읽었던 이벤트를 기억해 다음 이벤트를 요청하는 방식을 사용한다.
Relay log는 Slave DB에서 사용되며 Master DB로 부터 binary log를 받아와 저장해 Slave DB에 Master DB의 변경사항을 반영할 수 있다. 보통 SQL thread가 읽은 이벤트는 relay log 파일에서 지워지지만, SQL thread가 멈춘 상태에서 relay log 파일의 크기가 설정된 최대 크기에 도달하거나 설정된 보관 기간을 초과할 경우, Relay log 파일은 자동으로 회전(rotated)되어 새로운 파일로 교체된다. 이 과정에서 이전에 처리된 이벤트들은 제거되고, 새로운 이벤트들이 새로운 파일에 계속해서 기록된다.
SQL thread는 I/O thread로부터 만들어진 relay log를 SQL 명령문으로 변환하고 Slave DB에 적용해 Master DB와의 동기화를 담당하는 스레드이다. 트랜잭션 단위로 변경 사항을 읽어들이게 되고, 스레드가 작업을 마치게 되면 relay log를 통해 수행한 이벤트들은 소비되어 relay log 파일에서 제거된다.
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를 연결했다.
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를 연결하는 과정에서 해당 오류가 발생한다.
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_Running와 Slave_SQL_Running이 Yes라면 올바르게 연결된 것이다. 만약 Slave_IO_Running가 No 혹은 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_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_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를 사용하려면
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_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_Error: Replica failed to initialize applier metadata structure from the repository
slave DB가 master DB로부터 받은 이벤트를 처리하기 위한 메타데이터를 초기화하는 데 실패했다는 것을 의미한다. 아래의 사유로 인해 발생할 수 있다.
내가 겪은 케이스는 다른 에러로 인해 replication 설정을 다시 걸었는데, 이후 start slave; 명령어를 사용하니 해당 에러가 발생했다. 이에 대한 해결 방안은
stop slave; # slave서버의 복제 중지 (I/O스레드 중지, 마스터로부터 이벤트 수신 X)
reset slave; # slave의 복제 설정 초기화(default - clear X) -> 이전에 발생한 에러 관련 설정 제거됨
start slave;
위 명령어를 순차적으로 사용해 에러 관련 설정 정보를 제거 후 다시 slave를 실행시키는 것이다.
대부분 이슈의 경우 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