Replicationκ³Ό Spring Boot

@루킀 Β· October 16, 2022 Β· 31 min read

μ‚¬μš©λ°°κ²½

ν˜„μž¬ μ§„ν–‰ν•˜κ³  μžˆλŠ” ν”„λ‘œμ νŠΈμ—μ„œ 쿠폰 μ‹œμŠ€ν…œμ„ λ§Œλ“€κ³  μžˆμŠ΅λ‹ˆλ‹€. ν˜„μž¬ μ‹œμŠ€ν…œμƒ λ‚΄λΆ€ μΈμ›λ“€λ§Œ μ‚¬μš©μ„ ν•˜κ³  있기 λ•Œλ¬Έμ— λΆ€ν•˜μ— λŒ€ν•œ 문제λ₯Ό κ±±μ •ν•˜μ§€ μ•Šμ•„λ„ λ˜μ§€λ§Œ, ν•΄λ‹Ή μ‹œμŠ€ν…œμ΄ μ‹€μ œ μƒμš©μ„œλΉ„μŠ€κ°€ λ˜μ–΄ λ§Žμ€ μ‚¬μš©μžλ“€μ΄ μ‚¬μš©ν•œλ‹€κ³  κ°€μ •ν•œλ‹€λ©΄ λΆ„λͺ… μ„œλ²„μ™€ DB에 λ§Žμ€ λΆ€ν•˜κ°€ 였게 될 κ²ƒμž…λ‹ˆλ‹€.

이번 κΈ€μ—μ„œλŠ” λΆ€ν•˜ λΆ„μ‚° 쀑 DBμ—μ„œμ˜ 처리 방법을 ν•œλ²ˆ κ³ λ―Όν•΄λ³΄κ³ μž ν•©λ‹ˆλ‹€. 그리고 고민의 결과와 μ μš©ν•œ 과정에 λŒ€ν•΄μ„œλ„ ν•¨κ»˜ μž‘μ„±ν•΄λ³΄λ €κ³  ν•©λ‹ˆλ‹€.

DB μ„±λŠ₯ ν™•μž₯ 방법

DB의 μ„±λŠ₯을 높이기 μœ„ν•΄μ„œλŠ” Scale up λ˜λŠ” Scale out ν•˜μ—¬ μ„±λŠ₯을 λ†’μ—¬μ•Ό ν•©λ‹ˆλ‹€. ν˜„μž¬ μ§„ν–‰ν•˜λŠ” ν”„λ‘œμ νŠΈμ—μ„œλŠ” Scale up을 ν•˜κΈ° μœ„ν•œ μžμ›μ„ μ œκ³΅λ°›μ„ 수 μ—†λŠ” 상황이기 λ•Œλ¬Έμ—, Scale up은 κ³ λ €ν•˜μ§€ μ•Šκ³  Scale out을 μ μš©ν•˜κΈ°λ‘œ κ²°μ •ν–ˆμŠ΅λ‹ˆλ‹€.

DBλ₯Ό Scale outν•˜λŠ” 방법은 Clusterningκ³Ό Replication으둜 ꡬ뢄할 수 μžˆμŠ΅λ‹ˆλ‹€. ν•œλ²ˆ 각각의 방법에 λŒ€ν•΄μ„œ μ•Œμ•„λ³΄λ„λ‘ ν•©μ‹œλ‹€.

Clustering

Clusterning은 DB μ„œλ²„λ₯Ό 2λŒ€ 이상, DB μŠ€ν† μ§€λ¦¬λ₯Ό 1λŒ€λ‘œ κ΅¬μ„±ν•˜λŠ” ν˜•νƒœμž…λ‹ˆλ‹€. DB μ„œλ²„ 2λŒ€λ₯Ό λͺ¨λ‘ Active μƒνƒœλ‘œ μš΄μ˜ν•œλ‹€λ©΄, DB μ„œλ²„ 1λŒ€κ°€ 죽더라도 λ‹€λ₯Έ DB μ„œλ²„ 1λŒ€λŠ” μ‚΄μ•„μžˆμ–΄ μ„œλΉ„μŠ€λ₯Ό μ •μƒμ μœΌλ‘œ ν•  수 μžˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ DB μ„œλ²„λ₯Ό μ—¬λŸ¬λŒ€λ‘œ λ‘κ²Œλ˜λ©΄, νŠΈλž˜ν”½μ„ λΆ„μ‚°ν•˜μ—¬ κ°λ‹Ήν•˜κ²Œ λ˜μ–΄ CPU와 Memory λΆ€ν•˜κ°€ μ μ–΄μ§€λŠ” μž₯점이 μ‘΄μž¬ν•©λ‹ˆλ‹€.

img

ν•˜μ§€λ§Œ, DB μ„œλ²„λ“€μ΄ ν•˜λ‚˜μ˜ DB μŠ€ν† λ¦¬μ§€λ₯Ό κ³΅μœ ν•˜κΈ° λ•Œλ¬Έμ— DB μŠ€ν† λ¦¬μ§€μ— 병λͺ©μ΄ μƒκΈ°λŠ” 단점이 μ‘΄μž¬ν•©λ‹ˆλ‹€.

이λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄μ„œ DB μ„œλŒ€ 2λŒ€ 쀑 1λŒ€λ₯Ό Stand-by 즉, μ‚¬μš©λŒ€κΈ° μƒνƒœλ‘œ 두어 단점을 λ³΄μ•ˆν•  수 μžˆμŠ΅λ‹ˆλ‹€. Stand-by μƒνƒœμ˜ μ„œλ²„λŠ” Active μƒνƒœμ˜ μ„œλ²„μ— λ¬Έμ œκ°€ 생겼을 λ•Œ, Fail Overλ₯Ό ν•˜μ—¬ μƒν˜Έ μ „ν™˜μ„ ν•˜μ—¬ μž₯애에 λŒ€μ‘μ„ ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό ν†΅ν•΄μ„œ, DB μŠ€ν† λ¦¬μ§€μ— λŒ€ν•œ 병λͺ© ν˜„μƒλ„ ν•΄κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

img 1

ν•˜μ§€λ§Œ, Fail Overκ°€ λ°œμƒν•˜λŠ” μ‹œκ°„ λ™μ•ˆμ€ 손싀이 μ‘΄μž¬ν•˜λ©°, DB μ„œλ²„ λΉ„μš©μ€ κ·ΈλŒ€λ‘œμΈλ° κ°€μš©λ₯ μ΄ 1/2κ°€ λœλ‹€λŠ” 단점이 μ‘΄μž¬ν•©λ‹ˆλ‹€.

λ˜ν•œ, Clustering이 κ°€μ§€λŠ” 본질적인 문제둜 DB μŠ€ν† λ¦¬μ§€μ— λ¬Έμ œκ°€ λ°œμƒν•˜λ©΄, 데이터λ₯Ό 볡ꡬ할 수 μ—†λ‹€λŠ” 치λͺ…적인 λ¬Έμ œκ°€ μ‘΄μž¬ν•©λ‹ˆλ‹€.

Replication

Replication은 각 DB μ„œλ²„κ°€ 각자 DB μŠ€ν† λ¦¬μ§€λ₯Ό 가지고 μžˆλŠ” ν˜•νƒœμž…λ‹ˆλ‹€. 그리고 이λ₯Ό μ‹€μ‹œκ°„μœΌλ‘œ λ™κΈ°ν™”ν•˜λ©΄μ„œ νŠΈλž˜ν”½μ„ μ—¬λŸ¬ MySQL μ„œλ²„λ‘œ λΆ„μ‚°μ‹œμΌœμ£ΌκΈ° λ•Œλ¬Έμ— λ°μ΄ν„°λ² μ΄μŠ€λ‘œ μΈν•œ 병λͺ© ν˜„μƒμ„ κ°œμ„ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

img 2

Master와 Slave둜 κ΅¬μ„±λœ κ΅¬μ‘°λŠ” Master μ„œλ²„μ—λŠ” INSERT, UPDATE, DELETE μž‘μ—…μ΄ μ „λ‹¬λ˜κ³  Slave μ„œλ²„μ—λŠ” SELECT μž‘μ—…μ„ μ „λ‹¬ν•©λ‹ˆλ‹€. SlaveλŠ” κ²°κ΅­ Master μ„œλ²„μ—μ„œ 볡제된 데이터이기 λ•Œλ¬Έμ— λ°μ΄ν„°μ˜ μ‘°μž‘μ΄ λ°œμƒν•  수 μžˆλŠ” INSERT, UPDATE, DELETE μž‘μ—…μ€ Master둜 전달이 되고 μ‘°νšŒλ§Œμ„ ν•˜λŠ” SELECT μž‘μ—…μ€ Slave μ„œλ²„λ₯Ό ν†΅ν•˜μ—¬ μ§„ν–‰ν•˜κ²Œ λ©λ‹ˆλ‹€.

μœ„μ˜ κ·Έλ¦Όμ—μ„œλŠ” Slave μ„œλ²„κ°€ ν•˜λ‚˜λΏμ΄μ§€λ§Œ μ„œλΉ„μŠ€μ— 맞게 Slave μ„œλ²„λ₯Ό μ—¬λŸ¬ 개둜 μ„€μ •ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ λ°œμƒν•˜λŠ” λŒ€λΆ€λΆ„μ˜ μΏΌλ¦¬λŠ” 쑰회인 SELECT인데 μ΄λŸ¬ν•œ 것을 Slave μ„œλ²„λ₯Ό 톡해 λΆ„μ‚°ν•˜μ—¬ μ²˜λ¦¬ν•  수 μžˆμœΌλ‹ˆ μ’€ 더 μ„±λŠ₯ ν–₯상을 κ°€μ Έκ°ˆ 수 μžˆμŠ΅λ‹ˆλ‹€.

MySQLμ—μ„œμ˜ Replication λ™μž‘κ³Όμ •μ„ 쑰금 더 μžμ„Ένžˆ μ‚΄νŽ΄λ³΄λ©΄ λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  1. μ‚¬μš©μžκ°€ INSERT, UPDATE, DELETE 쿼리λ₯Ό Master μ„œλ²„λ‘œ μš”μ²­ν•˜λ©΄, Master μ„œλ²„λŠ” 이λ₯Ό μ²˜λ¦¬ν•˜κ³  Master μ„œλ²„μ—μ„œ μΌμ–΄λ‚˜λŠ” λͺ¨λ“  λ³€κ²½ 이λ ₯듀을 λ°”μ΄λ„ˆλ¦¬ 둜그 μŠ€λ ˆλ“œλ₯Ό μ‚¬μš©ν•΄μ„œ λ°”μ΄λ„ˆλ¦¬ λ‘œκ·Έμ— μ €μž₯ν•œλ‹€.
  2. Slave μ„œλ²„μ—μ„œ λ³€κ²½ 내역을 μš”μ²­ν•˜λ©΄ Master μ„œλ²„μ˜ λ°”μ΄λ„ˆλ¦¬ 둜그 덀프 μŠ€λ ˆλ“œκ°€ λ°”μ΄λ„ˆλ¦¬ λ‘œκ·Έλ“€μ„ μ½μ–΄μ„œ Slave μ„œλ²„λ‘œ 데이터λ₯Ό μ „λ‹¬ν•œλ‹€.
  3. Slave μ„œλ²„μ˜ Slave I/O μŠ€λ ˆλ“œκ°€ μš”μ²­ν•œ λ³€κ²½ 내역듀을 Slave μ„œλ²„μ˜ 릴레이 λ‘œκ·Έμ— μ €μž₯ν•©λ‹ˆλ‹€.
  4. μ΅œμ’…μ μœΌλ‘œ Slave μ„œλ²„μ˜ Slave SQL μŠ€λ ˆλ“œκ°€ 릴레이 λ‘œκ·Έμ— 기둝된 데이터듀을 μ½μ–΄μ„œ Slave μ„œλ²„μ— μ μš©ν•©λ‹ˆλ‹€.

λ°”μ΄λ„ˆλ¦¬ λ‘œκ·Έλž€?

Master λ…Έλ“œμ—μ„œμ˜ DDL or DML κ°€μš΄λ° λ°μ΄ν„°μ˜ κ΅¬μ‘°λ‚˜ λ‚΄μš©μ„ λ³€κ²½ν•˜λŠ” λͺ¨λ“  쿼리 λ¬Έμž₯κ³Ό 이λ ₯을 κΈ°λ‘ν•˜λŠ” 논리적인 둜그λ₯Ό λ§ν•©λ‹ˆλ‹€.

전체적인 흐름은 λ‹€μŒ 도식과 κ°™μŠ΅λ‹ˆλ‹€.

img 3

κ·Έλž˜μ„œ.. μ–΄λ–€ 선택을?

결둠적으둜 DB Scale out을 ν•˜λŠ” λ°©λ²•μœΌλ‘œ Replication을 μ„ νƒν•˜κΈ°λ‘œ ν–ˆμŠ΅λ‹ˆλ‹€. μ΄λ ‡κ²Œ κ΅¬μ„±ν•˜κ²Œ 될 경우 DB μŠ€ν† λ¦¬μ§€λ₯Ό μ—¬λŸ¬κ°œλ‘œ 두기 λ•Œλ¬Έμ—, 데이터 볡ꡬλ₯Ό λͺ»ν•˜λŠ” 상황을 μ–΄λŠμ •λ„ 방지할 수 있으며, Clusteringκ³Ό λ‹€λ₯΄κ²Œ DB κ°€μš©λ₯ μ„ μ΅œλŒ€λ‘œ κ°€μ Έκ°ˆ 수 μžˆλ‹€λŠ” μž₯점을 κ°€μ Έκ°ˆ 수 μžˆλ‹€κ³  μƒκ°ν–ˆκΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.

μ—¬κΈ°μ„œ 고민인 점은 Slave DBλ₯Ό λͺ‡λŒ€λ‘œ μ„€μ •ν•˜λŠ”κ°€ μž…λ‹ˆλ‹€. ν˜„μž¬λ‘œμ¨λŠ” μ„œλΉ„μŠ€ μ‚¬μš©μžκ°€ λ§Žμ§€ μ•ŠκΈ° λ•Œλ¬Έμ— Master 1, Slave 1 ꡬ쑰둜 선택해도 λ¬Έμ œκ°€ μ—†μŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ ν˜„μž¬ μ§„ν–‰ν•˜λŠ” μ„€κ³„λŠ” λ‹€μˆ˜μ˜ μ‚¬μš©μžλ₯Ό κ³ λ €ν•˜μ—¬ μ§„ν–‰ν•˜κ³  있기 λ•Œλ¬Έμ— μΆ”ν›„ ν™•μž₯에 μš©μ΄ν•˜λ„λ‘ N개의 Slave DBλ₯Ό κΈ°μ€€μœΌλ‘œ 섀계λ₯Ό ν•˜κΈ°λ‘œ ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

img 4

μ‹€μ œ ν”„λ‘œμ νŠΈμ—λŠ” Master 1, Slave 2 둜 κ΅¬μ„±ν•˜μ—¬ μž‘μ—…μ„ μ§„ν–‰ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

MySQL μ„€μ •

Master, Slave DB Server 사전 μž‘μ—…

MySQL은 기본적으둜 127.0.0.1 즉, 둜컬 ν˜ΈμŠ€νŠΈμ—μ„œλ§Œ 접속할 수 μžˆλŠ”λ° μ•„λž˜μ™€ 같이 MySQL 섀정을 λ³€κ²½ν•˜μ—¬ μ™ΈλΆ€μ—μ„œ 접속이 κ°€λŠ₯ν•˜λ„λ‘ 변경을 ν•΄μ•Όν•©λ‹ˆλ‹€. λ‹€μŒκ³Ό 같이 변경을 ν•˜λ©΄ λ©λ‹ˆλ‹€.

vim /etc/mysql/mysql.conf.d/mysqld.cnf

bind-address           = 0.0.0.0
mysqlx-bind-address    = 0.0.0.0

Master DB Server μž‘μ—…

기본적인 λ°μ΄ν„°λ² μ΄μŠ€ λΆ€ν„° ν•œλ²ˆ 생성해보도둝 ν•˜κ² μŠ΅λ‹ˆλ‹€. λ¨Όμ € Master DB에 λŒ€ν•œ λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.

CREATE DATABASE test;

ν…ŒμŠ€νŠΈν•  ν…Œμ΄λΈ”μ΄ μžˆμ–΄μ•Ό ν•˜κΈ° λ•Œλ¬Έμ— ν…Œμ΄λΈ”λ„ 생성해 μ€λ‹ˆλ‹€.

CREATE TABLE user(
	id BIGINT NOT NULL AUTO_INCREMENT,
	name VARCHAR(255),
	PRIMARY KEY(id)
);

Replication 에 ν•„μš”ν•œ μ „μš© 계정을 생성해주고, Replication을 μœ„ν•œ κΆŒν•œμ„ λΆ€μ—¬ν•΄ μ€λ‹ˆλ‹€. rootλ₯Ό μ‚¬μš©ν•  경우 λ³΄μ•ˆμƒ λ¬Έμ œκ°€ 될 수 있기 λ•Œλ¬Έμ—, λ‹€μŒκ³Ό 같이 Slave κΆŒν•œμ„ λΆ€μ—¬ν•΄ μ€λ‹ˆλ‹€.

CREATE USER 'master'@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO 'master'@'%';

그리고 MySQL μ„€μ • 변경을 μœ„ν•΄ my.cnf 파일둜 μ΄λ™ν•˜μ—¬ 값을 μˆ˜μ •ν•΄μ€λ‹ˆλ‹€.

sudo vim /etc/mysql/my.cnf
[mysqld]
max_allowed_packet=1000M
server-id = 1
log-bin = mysql-bin
binlog_format = ROW
max_binlog_size = 500M
sync_binlog = 1
expire-logs-days = 7
binlog_do_db = test

μ„€μ •λ“€μ˜ μ„ΈλΆ€ 값듀은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  • maxallowedpacket : μ„œλ²„λ‘œ μ§ˆμ˜ν•˜κ±°λ‚˜ λ°›κ²Œλ˜λŠ” νŒ¨ν‚·μ˜ μ΅œλŒ€ 길이λ₯Ό μ„€μ •
  • server-id : μ„œλ²„μ˜ ID, λ ˆν”Œλ¦¬μΌ€μ΄μ…˜ ν† ν”Œλ¦¬μ§€(μ—°κ²°λœ 망)μ—μ„œλŠ” κ³ μœ ν•œ 각각의 μ„œλ²„ IDλ₯Ό κ°€μ Έμ•Ό 함
  • log-bin : λ°”μ΄λ„ˆλ¦¬ 둜그 파일 경둜 β†’ (/var/lib/mysql/mysql-bin.XXXXXX) ν˜•μ‹μœΌλ‘œ μ €μž₯
  • binlog_format : λ°”μ΄λ„ˆλ¦¬ 둜그의 μ €μž₯ ν˜•μ‹μ„ 지정. STATEMENT, ROW, MIXED 3가지 쀑 ν•˜λ‚˜λ₯Ό 선택이 κ°€λŠ₯

    • ν•΄λ‹Ή 섀정쀑 ROWλ₯Ό 선택. κ·Έ μ΄μœ λŠ” ν˜„μž¬ InnoDBλ₯Ό μ‚¬μš© 쀑이며, νŠΈλžœμž­μ…˜ 격리 μˆ˜μ€€μ΄ READ COMMITTED일 경우 ROW 기반 λ‘œκΉ…λ§Œ μ‚¬μš©μ΄ κ°€λŠ₯.
  • maxbinlogsize : λ°”μ΄λ„ˆλ¦¬ 둜그의 μ΅œλŒ€ 크기
  • sync_binlog : N개의 νŠΈλžœμž­μ…˜ λ§ˆλ‹€ λ°”μ΄λ„ˆλ¦¬ 둜그λ₯Ό λ””μŠ€ν¬μ— 동기화 μ‹œν‚¬μ§€ κ²°μ •. μ„€μ •ν•œ 1은 μ•ˆμ •μ μ΄μ§€λ§Œ, κ°€μž₯ 느린 μ„€μ •μž„
  • expire-logs-days: λ°”μ΄λ„ˆλ¦¬ λ‘œκ·Έκ°€ λ§Œλ£Œλ˜λŠ” κΈ°κ°„ μ„€μ •
  • binlogdodb : λ ˆν”Œλ¦¬μΌ€μ΄μ…˜μ„ μ μš©ν•  λ°μ΄ν„°λ² μ΄μŠ€ 이름 μ„€μ •

섀정이 λλ‚¬μœΌλ©΄ MySQL을 μž¬μ‹œμž‘ ν•΄μ€λ‹ˆλ‹€.

sudo systemctl restart mysql

그리고 Master의 μƒνƒœλ₯Ό 확인해 잘 λ°˜μ˜λ˜μ—ˆλŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€.

SHOW MASTER STATUS:

img 9

μ€‘μš”!

λ ˆν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œλŠ” 이 Fileκ³Ό Position κ°’μœΌλ‘œ Master - Slave μ„œλ²„ 동기화가 진행이 λ©λ‹ˆλ‹€. λ˜ν•œFileκ³Ό Position은 λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ μ–΄λ–€ μž‘μ—…μ„ ν•  λ•Œλ§ˆλ‹€ 변경이 λ©λ‹ˆλ‹€. Master DB의 ν•΄λ‹Ή 정보듀을 Slave DBμ—μ„œ κΈ°μž…ν•΄μ„œ μ‚¬μš©ν•˜κΈ° λ•Œλ¬Έμ—, 항상 확인 ν›„ Master - Slave 연동을 진행해야 ν•©λ‹ˆλ‹€.

Slave DB Server μž‘μ—…

μ΄μ–΄μ„œ μœ„μ—μ„œ μ„€λͺ…ν–ˆλ˜ λ‚΄μš©μ„ λ°”νƒ•μœΌλ‘œ Slave DBλ₯Ό μ„€μ •ν•΄ μ€λ‹ˆλ‹€.

CREATE DATABASE test;
CREATE USER 'slave'@'%' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON *.* TO 'slave'@'%';
$ vim /etc/mysql/my.cnf

[mysqld]

max_allowed_packet=1000M
server-id = 2
log-bin  = mysql-bin
binlog_format   = ROW
max_binlog_size = 500M
sync_binlog     = 1
expire-logs-days= 7
slow_query_log = 1
read_only = 1

섀정을 λ³€κ²½ν–ˆκΈ° λ•Œλ¬Έμ— μ—­μ‹œ MySQL을 μž¬μ‹œμž‘ ν•΄μ€λ‹ˆλ‹€.

sudo systemctl restart mysql

그리고 μ΅œμ’…μ μœΌλ‘œ MySQLμ—μ„œ μ•„λž˜ 쿼리λ₯Ό μ‹€ν–‰ν•΄μ£Όλ©΄ λ©λ‹ˆλ‹€. μ—¬κΈ°μ„œ MASTER_LOG_FILEκ³Ό Β MASTER_LOG_POS 값은 μ•„κΉŒ Master DB STATUSμ—μ„œ λ‚˜μ˜¨ κ°’μœΌλ‘œ 섀정을 ν•˜λ©΄ λ©λ‹ˆλ‹€.

RESET SLAVE;

CHANGE MASTER TO MASTER_HOST='xxx.xxx.x.xxx', // private IP
        MASTER_USER='master',
        MASTER_PORT=8080,                     // 내뢀적인 이슈둜 8080 포트둜 μ—°κ²°
        MASTER_PASSWORD='password',
        MASTER_LOG_FILE='mysql-bin.00000x',
        MASTER_LOG_POS=157;

START REPLICA;

μ§€κΈˆκΉŒμ§€ 잘 섀정이 λ˜μ—ˆλ‹€λ©΄, λ‹€μŒ λͺ…λ Ήμ–΄λ₯Ό μž…λ ₯ν–ˆμ„ λ•Œ λ¬Έμ œκ°€ μ—†λ‹€λŠ” 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

show slave status\G;

img 12

μΆ”κ°€μ μœΌλ‘œ Master DB μ„œλ²„μ—μ„œ ν…Œμ΄λΈ” 생성과 데이터 생성을 해보면 λ°”λ‘œ Slave DB μ„œλ²„μ— 잘 λ°˜μ˜λ˜λŠ” 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

[Master DB]

img 10

[Slave DB]

img 11

SpringBoot μ„€μ •

DB μ„œλ²„λ₯Ό Master - Slave 둜 이쀑화 ν•˜μ˜€μœΌλ―€λ‘œ, SpringBootμ—μ„œ μ‚¬μš©ν•˜λŠ” DataSource도 Master - Slaveλ₯Ό 각각 μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€. 이λ₯Ό κ΅¬ν˜„ν•˜κΈ° μœ„ν•œ λ°©λ²•μœΌλ‘œ @Transactional μ• λ…Έν…Œμ΄μ…˜μ˜ 속성을 μ΄μš©ν•˜μ—¬ readOnly = false인 νŠΈλžœμž­μ…˜μ€ Master DataSourceλ₯Ό, readOnly = true인 νŠΈλžœμž­μ…˜μ€ Slave DataSourceλ₯Ό μ‚¬μš©ν•˜λ„λ‘ μ„€μ •ν•΄ μ£Όκ² μŠ΅λ‹ˆλ‹€.

application.yml μ„€μ •

μ €λŠ” Master 1, Slave 2 λ₯Ό λ°”νƒ•μœΌλ‘œ Replication을 κ΅¬μ„±ν–ˆκΈ° λ•Œλ¬Έμ— 3개의 DataSource에 λŒ€ν•œ 섀정을 μž‘μ„±ν•΄μ£Όμ–΄μ•Ό ν•©λ‹ˆλ‹€. μ—¬κΈ°μ„œ ν•˜λ‚˜ μœ μ˜ν•΄μ•Ό ν•  점은 일반적인 DataSource 등둝 λ°©λ²•κ³ΌλŠ” 과정이 쑰금 λ‹¬λΌμ§„λ‹€λŠ” μ μž…λ‹ˆλ‹€.

ν•˜λ‚˜μ˜ DataSource만 μ‚¬μš©ν•˜λŠ” κ²½μš°μ—λŠ” SpringBoot AutoConfiguration을 ν†΅ν•΄μ„œ μžλ™μœΌλ‘œ 빈으둜 λ§Œλ“€μ–΄μ Έ κ΄€λ¦¬λ˜μ—ˆμ§€λ§Œ, 2개 μ΄μƒμ˜ DataSourceλ₯Ό μ‚¬μš©ν•˜λŠ” κ²½μš°μ—λŠ” κ°œλ°œμžκ°€ 직접 λΉˆμ„ λ§Œλ“€μ–΄μ„œ μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.

spring:
  datasource:
    master:
      username: master
      password: password
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://XXX.XXX.XXX.XXX:3306/test
    slave1:
      username: slave1
      password: password
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://XXX.XXX.XXX.XXX:3306/test
	slave2:
      username: slave2
      password: password
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://XXX.XXX.XXX.XXX:3306/test

DataSource 빈 등둝

@Configuration
public class DataSourceConfig {

    private static final String MASTER_SERVER = "MASTER";
    private static final String SLAVE1_SERVER = "SLAVE1";
    private static final String SLAVE1_SERVER = "SLAVE2";

    @Bean
    @Qualifier(MASTER_SERVER)
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create()
                .build();
    }

    @Bean
    @Qualifier(SLAVE1_SERVER)
    @ConfigurationProperties(prefix = "spring.datasource.slave1")
    public DataSource slave1DataSource() {
        return DataSourceBuilder.create()
                .build();
    }

    @Bean
    @Qualifier(SLAVE2_SERVER)
    @ConfigurationProperties(prefix = "spring.datasource.slave2")
    public DataSource slave2DataSource() {
        return DataSourceBuilder.create()
                .build();
    }
}

λ‹€μŒκ³Ό 같이 각 DB μ„œλ²„μ— λŒ€μ‘λ˜λŠ” DataSource νƒ€μž…μ˜ λΉˆμ„ λ“±λ‘ν•˜λ©΄ λ©λ‹ˆλ‹€.

μœ„μ˜ μ½”λ“œλ₯Ό 보면, @Qualifier μ• λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•œ 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. 일반적으둜 같은 νƒ€μž… (μ—¬κΈ°μ„œλŠ” DataSource)의 빈이 2개 이상 λ“±λ‘λœ 경우 μŠ€ν”„λ§μ€ μ–΄λ–€ λΉˆμ„ μ£Όμž…ν•΄μ•Ό ν•˜λŠ”μ§€ λͺ¨λ¦…λ‹ˆλ‹€. 이λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄μ„œ @Qualifier μ• λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•˜μ—¬ 이름을 기반으둜 ν•œμ •μžλ₯Ό λͺ…μ‹œν•˜μ—¬μ„œ, μ£Όμž…λ°›μ„ λΉˆμ„ 지정할 수 μžˆλ„λ‘ λ§Œλ“€λ©΄ λ©λ‹ˆλ‹€.

μΆ”κ°€μ μœΌλ‘œ @ConfigurationProperties ****μ• λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•œ 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. ν•΄λ‹Ή μ• λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•˜λ©΄ application.yml에 λͺ…μ‹œν•œ μ—¬λŸ¬ μ„€μ • 쀑, νŠΉμ • prefix에 ν•΄λ‹Ήν•˜λŠ” μ„€μ • 값을 μžλ°” λΉˆμ— 맀핑할 μˆ˜κ°€ μžˆμŠ΅λ‹ˆλ‹€.

AbstractRoutingDataSource

μŠ€ν”„λ§μ€ Multi DataSource ν™˜κ²½μ—μ„œ μ—¬λŸ¬ DataSourceλ₯Ό λ¬Άκ³  λΆ„κΈ°ν•΄μ£ΌκΈ° μœ„ν•΄μ„œ AbstractRoutingDataSourceλΌλŠ” 좔상 클래슀λ₯Ό μ§€μ›ν•©λ‹ˆλ‹€.

AbstractRoutingDataSource λ₯Ό μ‚΄νŽ΄λ³΄λ©΄ λ‹€μŒκ³Ό 같이 κ΅¬μ„±λ˜μ–΄ μžˆλŠ” 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

	@Nullable
	private Map<Object, Object> targetDataSources;

	@Nullable
	private Object defaultTargetDataSource;

	private boolean lenientFallback = true;

	private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();

	@Nullable
	private Map<Object, DataSource> resolvedDataSources;

	@Nullable
	private DataSource resolvedDefaultDataSource;

	public void setTargetDataSources(Map<Object, Object> targetDataSources) {
		this.targetDataSources = targetDataSources;
	}
	...

	protected DataSource determineTargetDataSource() {
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
		Object lookupKey = determineCurrentLookupKey();
		DataSource dataSource = this.resolvedDataSources.get(lookupKey);
		if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
			dataSource = this.resolvedDefaultDataSource;
		}
		if (dataSource == null) {
			throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
		}
		return dataSource;
	}
	...
}

μœ„μ˜ μ½”λ“œλ₯Ό 보면 setTargetDataSources()λΌλŠ” λ©”μ„œλ“œλ₯Ό 톡해 Map을 μ „λ‹¬ν•©λ‹ˆλ‹€. μ΄λ•Œ Map의 Valueλ‘œλŠ” DataSourceλ₯Ό μ „λ‹¬ν•©λ‹ˆλ‹€. μ „λ‹¬λœ DataSourceλŠ” Map의 Keyλ₯Ό ν†΅ν•΄μ„œ 찾을 수 μžˆμŠ΅λ‹ˆλ‹€.

λ˜ν•œ determineTargetDataSource()λ©”μ„œλ“œλ₯Ό 보면 μ‹€μ œλ‘œ μ‚¬μš©λœ DataSourceλ₯Ό κ²°μ •ν•©λ‹ˆλ‹€. λ‚΄λΆ€ μ½”λ“œλ₯Ό 확인해보면 determineCurrentLookupKey() λΌλŠ” λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•΄μ„œ κ°€μ Έμ˜¬ DataSource의 Keyλ₯Ό κ²°μ •ν•©λ‹ˆλ‹€.

μ‹€μ œ 저희가 이λ₯Ό ν™œμš©ν•˜κΈ° μœ„ν•΄μ„œ AbstractRoutingDataSource λ₯Ό μƒμ†λ°›λŠ” ꡬ체 클래슀λ₯Ό λ§Œλ“€μ–΄μ„œ μœ„μ—μ„œ μ–ΈκΈ‰ν•œ determineCurrentLookupKey()λ©”μ„œλ“œλ₯Ό μ˜€λ²„λΌμ΄λ“œν•˜μ—¬ νŠΈλžœμž­μ…˜μ˜ readOnly 값에 따라 λ‹€λ₯Έ DataSource 의 Keyλ₯Ό λ°˜ν™˜ν•˜λ„λ‘ κ΅¬ν˜„ν•˜κ² μŠ΅λ‹ˆλ‹€.

public class RoutingDataSource extends AbstractRoutingDataSource {

    private RoutingCircular<String> routingCircular;

    @Override
    public void setTargetDataSources(final Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);

        routingCircular = new RoutingCircular<>(
                targetDataSources.keySet().stream()
                        .map(Object::toString)
                        .filter(key -> key.contains("slave"))
                        .collect(Collectors.toList())
        );
    }		

    @Override
    protected Object determineCurrentLookupKey() {
        boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();

        if (isReadOnly) {
            return routingCircular.getOne();
        }
        return "master";
    }
} 

ν•΄λ‹Ή μ½”λ“œλ₯Ό 보면, TransactionSynchronizationManager의 isCurrentTransactionReadOnly()λΌλŠ” λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•˜μ˜€λŠ”λ°, 이λ₯Ό ν†΅ν•΄μ„œ ν˜„μž¬ νŠΈλžœμž­μ…˜μ˜ read-only μ—¬λΆ€λ₯Ό 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

Slave DB의 닀쀑화 처리

μœ„μ˜ μ½”λ“œμ—μ„œλŠ” ν•œκ°€μ§€ μ„€λͺ…ν•˜μ§€ μ•Šμ€ 뢀뢄이 μžˆμŠ΅λ‹ˆλ‹€. λ°”λ‘œ setTargetDataSources()κ³Ό κ΄€λ ¨λœ μ„€λͺ…μΈλ°μš”. μ €λŠ” Slave DBλ₯Ό 2개 이상 μ‚¬μš©ν•˜λ €κ³  ν•˜κΈ° λ•Œλ¬Έμ— readOnly μ†μ„±λ§ŒμœΌλ‘œλŠ” 이λ₯Ό ν•΄κ²°ν•  수 μ—†μŠ΅λ‹ˆλ‹€. 이λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄μ„œ Slave DBλ₯Ό λ²ˆκ°ˆμ•„μ„œ μ‚¬μš©ν•˜λ„λ‘ κΈ°λŠ₯을 좔가해주도둝 ν•˜κ² μŠ΅λ‹ˆλ‹€.

public class RoutingCircular<T> {
    private List<T> dataSources;
    private Integer counter = 0;
        
    public RoutingCircular(final List<T> dataSources) {
        this.dataSources = dataSources;
    }
		
    public T getOne() {
        int circularSize = dataSources.size();
        if (counter + 1 > circularSize) {
            counter = 0;
        }
        return dataSources.get(counter++ % circularSize);
    }
}

λ‹€μŒκ³Ό 같이 RoutingCircular 클래슀λ₯Ό λ§Œλ“€μ–΄ ν†΅ν•΄μ„œ μ—¬λŸ¬κ°œμ˜ Slave DB의 DataSourceλ₯Ό μˆœμ„œλŒ€λ‘œ λ‘œλ“œλ°ΈλŸ°μ‹± ν•  수 있게 λ©λ‹ˆλ‹€.

λΌμš°νŒ…ν•  DataSource λ“±λ‘ν•˜κΈ°

μ§€κΈˆκΉŒμ§€ μœ„μ—μ„œ λ§Œλ“  RoutingDataSource에 μš°λ¦¬κ°€ μ‚¬μš©ν•  3κ°€μ§€μ˜ DataSource 정보λ₯Ό λ“±λ‘ν•˜κ³ , 이λ₯Ό 빈으둜 λ§Œλ“€μ–΄μ„œ μ΅œμ’…μ μœΌλ‘œ μŠ€ν”„λ§μ—μ„œ κ΄€λ¦¬λ˜λ„λ‘ μž‘μ—…μ„ μ²˜λ¦¬ν•΄μ£Όκ² μŠ΅λ‹ˆλ‹€. @Qualifierλ₯Ό μ΄μš©ν•΄μ„œ μ–΄λ–€ λΉˆμ„ μ£Όμž…λ°›μ„ 것인지 λͺ…ν™•νžˆ μ§€μ‹œν•΄μ„œ μž‘μ—…μ„ μ§„ν–‰ν•©λ‹ˆλ‹€.

@Bean
public DataSource routingDataSource(@Qualifier(MASTER_SERVER) DataSource masterDataSource,
																		@Qualifier(SLAVE1_SERVER) DataSource slave1DataSource,
																		@Qualifier(SLAVE2_SERVER) DataSource slave2DataSoucre) {
    RoutingDataSource routingDataSource = new RoutingDataSource();

    HashMap<Object, Object> dataSources = new HashMap<>();
    dataSources.put("master", masterDataSource);
    dataSources.put("slave1", slave1DataSource);
    dataSources.put("slave2", slave2DataSource);

    routingDataSource.setTargetDataSources(dataSources);
    routingDataSource.setDefaultTargetDataSource(masterDataSource);

    return routingDataSource;
}

ν•΄λ‹Ή 과정을 ν†΅ν•΄μ„œ μš°λ¦¬κ°€ κ΅¬ν˜„ν•œ RoutingDataSource의 μΈμŠ€ν„΄μŠ€λ₯Ό μƒμ„±ν•œ λ’€, 등둝해 쀄 각각의 DataSourceλ₯Ό String Type의 Key둜 λ§€ν•‘ν•˜κ³ , κΈ°λ³Έ DataSourceλ₯Ό Master둜 μ§€μ •ν•œ λ’€ Bean으둜 등둝을 해쀄 수 μžˆμŠ΅λ‹ˆλ‹€.

그리고 μ‹€μ œ Spring Bootμ—μ„œ μ‚¬μš©ν•˜κ²Œ 될 DataSource λ₯Ό λ§Œλ“œλŠ” μž‘μ—…μ„ μˆ˜ν–‰ν•˜λ©΄ λ©λ‹ˆλ‹€. μ—¬κΈ°μ„œ κ³ λ €ν•΄μ•Ό ν•  점은 μŠ€ν”„λ§μ΄ DataSourceλ₯Ό 톡해 Connection을 νšλ“ν•˜λŠ” 일련의 주기에 λŒ€ν•΄μ„œ 확인할 ν•„μš”κ°€ μžˆμŠ΅λ‹ˆλ‹€.

Spring의 Connection νšλ“ 방법과 LazyConnection 처리

μŠ€ν”„λ§μ€ νŠΈλžœμž­μ…˜μ— μ§„μž…ν•˜κΈ°λ§Œ ν•˜λ©΄ λ°”λ‘œ DataSourceλ₯Ό κ°€μ Έμ™€μ„œ Connection을 κ°€μ Έμ˜΅λ‹ˆλ‹€. 말둜만 ν•˜λ©΄ λ„ˆλ¬΄ μΆ”μƒμ μ΄λ‹ˆ Springμ—μ„œ Transactional을 μ²˜λ¦¬ν•˜λŠ” 과정을 μžμ„Έν•˜κ²Œ ν•œλ²ˆ μ•Œμ•„λ΄…μ‹œλ‹€. 밑에 μ •λ¦¬λœ μˆœμ„œλŒ€λ‘œ Spring Transactional이 λ™μž‘μ„ ν•˜κ²Œ λ©λ‹ˆλ‹€.

1. CglibAopProxy.DynamicAdvisedInterceptor.intercept( )

2. TransactionInterceptor.invoke( )

3. TransactionAspectSupport.invokeWithinTransaction( )

4. TransactionAspectSupport.createTransactionIfNecessary( )

5. AbstractPlatformTransactionManager.getTransaction( )

6. AbstractPlatformTransactionManager.startTransaction( )

7. AbstractPlatformTransactionManager.begin( )

8. AbstractPlatformTransactionManager.prepareSynchronization( )

9. TransactionAspectSupport.invokeWithinTransaction( )

10. InvocationCallback.proceedWithInvocation( )

11. @Transactional이 적용된 μ‹€μ œ 타깃이 μ‹€ν–‰

μš°λ¦¬κ°€ μ—¬κΈ°μ„œ κΌ­ 확인해봐야 ν•  뢀뢄은 7번과 8번 κ³Όμ •μž…λ‹ˆλ‹€. Spring AOPλŠ” 7번 κ³Όμ •μ—μ„œ DataSource둜 λΆ€ν„° Connection을 κ°€μ Έμ˜€κ³  8번 κ³Όμ •μ—μ„œ νŠΈλžœμž­μ…˜μ˜ ν˜„μž¬ μƒνƒœλ₯Ό ThreadLocal에 μ €μž₯을 ν•˜κ²Œ λ©λ‹ˆλ‹€. 즉, TransactionSynchronizationManager에 νŠΈλžœμž­μ…˜μ˜ 정보λ₯Ό 동기화 ν•˜λŠ” μž‘μ—…μ΄ DataSource둜 λΆ€ν„° Connection을 κ°€μ Έμ˜¨ 이후에 싀행이 λœλ‹€λŠ” κ²ƒμž…λ‹ˆλ‹€.

μ΄λ•ŒκΉŒμ§€μ˜ λ‚΄μš©μ„ 그림으둜 μ •λ¦¬ν•˜λ©΄ λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

img 5

ν•˜μ§€λ§Œ μ €ν¬λŠ” Replication을 μœ„ν•΄μ„œΒ AbstractRoutingDataSource을 κ΅¬ν˜„ν•˜μ—¬ νŠΈλžœμž­μ…˜ 속성에 따라 DataSourceλ₯Ό μ„ νƒν•΄μ„œ μ„ νƒν•œ DataSource의 Connection객체λ₯Ό 리턴할 수 μžˆλ„λ‘ κ΅¬ν˜„μ„ ν•΄μ•Ό ν–ˆμŠ΅λ‹ˆλ‹€.

μ΄λŸ¬ν•œ 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄μ„œ Springμ—μ„œλŠ” LazyConnectionDataSourceProxy 을 μ œκ³΅ν•©λ‹ˆλ‹€. Spring Docsλ₯Ό 확인해보면 λ‹€μŒκ³Ό 같은 λ‚΄μš©μ„ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

img 6

즉, μ‹€μ œ 쿼리λ₯Ό ν˜ΈμΆœν•˜κΈ° μ „κΉŒμ§€ JDBC Connection을 κ°€μ Έμ˜€μ§€ μ•Šκ³  Connection Proxyλ₯Ό μ£Όκ³ , λŒ€μƒ λ©”μ„œλ“œ μ•ˆμ—μ„œ 쿼리가 μ‹€μ œ λ°œμƒν•  λ•Œ μš°λ¦¬κ°€ μž‘μ„±ν•œ AbstractRoutingDataSource μ—μ„œ Connection을 μ–»μ–΄ 쿼리λ₯Ό μ‹€ν–‰ν•˜λ„λ‘ λ‘œμ§μ„ κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

μ„€λͺ…이 ꡉμž₯히 κΈΈμ—ˆλŠ”λ°μš”. 이제 λ°‘μ˜ μ½”λ“œμ™€ 같이 섀정을 ν•΄μ£Όλ©΄ 저희가 μ›ν•˜λŠ”λ°λ‘œ DataSourceλ₯Ό κ°€μ Έμ˜¬ 수 있게 λ©λ‹ˆλ‹€.

@Bean
@Primary
public DataSource dataSource() {
    DataSource determinedDataSource = routingDataSource(masterDataSource(), slave1DataSource(), slave2DataSource());
    return new LazyConnectionDataSourceProxy(determinedDataSource);
}

img 7

μ—¬κΈ°μ„œ @Primary μ• λ…Έν…Œμ΄μ…˜μ„ μž‘μ„±ν•΄μ€€ 것을 확인할 수 μžˆλŠ”λ°, 기본적으둜 Spring Boot의 AutoConfiguration은 ν•œ 개의 DataSourceλ₯Ό κ°€μ •ν•˜κ³  섀정이 λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. ν˜„μž¬λŠ” μ—¬λŸ¬ 개의 DataSourceλ₯Ό μ‚¬μš©ν•˜λŠ” 경우이기 λ•Œλ¬Έμ—, @Primary μ• λ…Έν…Œμ΄μ…˜μ„ λΆ™μ—¬μ£Όμ–΄μ„œ DataSource에 λŒ€ν•œ 지정을 ν•΄μ£Όμ–΄μ•Ό ν•©λ‹ˆλ‹€.

μ§€κΈˆκΉŒμ§€ 과정이 μ„±κ³΅μ μœΌλ‘œ μˆ˜ν–‰μ΄ λ˜μ—ˆλ‹€λ©΄ Replication을 μ •μƒμ μœΌλ‘œ λ™μž‘μ‹œν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€.

Replication 섀계λ₯Ό ν•˜λ©΄μ„œ κ³ λ €ν•œ λ¬Έμ œλ“€κ³Ό μ•žμœΌλ‘œμ˜ κ°œμ„ λ°©ν–₯

데이터 μ •ν•©μ„± 문제

이번 μž‘μ—…μ„ ν•˜λ©΄μ„œ κ°€μž₯ 고민을 ν–ˆλ˜ λΆ€λΆ„μž…λ‹ˆλ‹€. λ§Œμ•½ μ‹€μ‹œκ°„μœΌλ‘œ 계속 쿠폰이 λ°œν–‰λ˜κ³ , ν•œμͺ½μ—μ„œλŠ” 계속 μ‘°νšŒλ˜λŠ” ν™˜κ²½μ΄λΌλ©΄ 동기화 이전 μ‹œμ μ— 쑰회λ₯Ό ν•˜κ²Œ 될 경우 NPEκ°€ λ°œμƒν•˜κ²Œ 될 κ°€λŠ₯성은 λ‚¨μ•„μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ λ‹€μ–‘ν•œ 방법을 κ³ λ―Όν•΄λ³΄μ•˜λŠ”λ° 2가지 μ •λ„μ˜ 방법을 찾을 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

첫번째 방법은 λ°˜λ™κΈ°(Semi Async) λ°©μ‹μž…λ‹ˆλ‹€. MySQL Docsλ₯Ό μ‚΄νŽ΄λ³΄λ©΄ 비동기(Async) 방식이 μ•„λ‹Œ λ°˜λ™κΈ°(Semi Async) 방식을 μ‚¬μš©ν•  수 μžˆλ‹€κ³ λ„ λ‚˜μ™€μžˆμŠ΅λ‹ˆλ‹€. ν•΄λ‹Ή 방법을 ν†΅ν•΄μ„œ DB의 μ„±λŠ₯을 쑰금 ν¬κΈ°ν•˜κ³ , Master - Slave의 동기화λ₯Ό 쑰금 더 보μž₯ν•΄μ€λ‹ˆλ‹€. ν•˜μ§€λ§Œ 이 방식은 Masterκ°€ νŠΉμ • μ„Έμ…˜μ˜ νŠΈλžœμž­μ…˜μ„ μ²˜λ¦¬ν•˜κΈ° μœ„ν•΄μ„œ Slaveλ“€ 쀑 적어도 1κ°œκ°€ ACK ν•˜κ±°λ‚˜ timed out에 도달할 λ•ŒκΉŒμ§€ 기닀리기 λ•Œλ¬Έμ— μ„±λŠ₯에 μ•…μ˜ν–₯을 미쳐 Replication의 이점을 살리지 λͺ»ν•œλ‹€λŠ” 생각이 λ“€μ—ˆμŠ΅λ‹ˆλ‹€.

λ‘λ²ˆμ§Έ 방법은 νƒ€μž… μŠ€νƒ¬ν”„ λ°©μ‹μž…λ‹ˆλ‹€. 졜근 N초 ~ NλΆ„ κΉŒμ§€μ˜ κ°±μ‹  내역은 Masterμ—μ„œ 읽고, 이후 내역은 Slave μ—μ„œ μ½λŠ” 방법이라고 ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이 방법은 Master에 νŠΈλž˜ν”½μ΄ 많이 λͺ°λ € Master의 μž₯μ•  μœ„ν—˜μ΄ λ†’λ‹€λŠ” 생각이 λ“€μ—ˆμŠ΅λ‹ˆλ‹€.

결둠적으둜 μ €λŠ” μ§€κΈˆμ²˜λŸΌ MySQL Replicationμ—μ„œ 기본적으둜 μ œκ³΅ν•˜λŠ” 비동기 λ°©μ‹μœΌλ‘œ 처리λ₯Ό ν•˜κ³ , 데이터 뢈일치 ν˜„μƒμ„ λ‹€λ₯Έ λ°©μ‹μœΌλ‘œ ν•΄κ²°ν•˜κΈ°λ‘œ 결둠을 λ‚΄λ ΈμŠ΅λ‹ˆλ‹€.

ν˜„μž¬ μƒκ°ν•˜λŠ” 해결방법은 정합성이 μ€‘μš”ν•œ λ‘œμ§μ—μ„œλŠ” 쑰회 μš”μ²­ μ—­μ‹œ Master DBλ₯Ό νƒ€κ²Œλ” μ„€μ •ν•˜λŠ” 것 μž…λ‹ˆλ‹€. λ¬Όλ‘  Master DB의 λΆ€ν•˜κ°€ μƒκΈ°κ² μ§€λ§Œ, Replication을 μ§„ν–‰ν•˜μ§€ μ•Šμ•˜μ„ λ•Œλ³΄λ‹€λŠ” 훨씬 λΆ€ν•˜λ₯Ό λΆ„μ‚°μ‹œν‚¬ 수 있기 λ•Œλ¬Έμ— ν˜„μž¬ κ°€μš©ν•  수 μžˆλŠ” μ΅œλŒ€ν•œμ˜ Scale - up을 ν•œ ν›„, Master DBλ₯Ό 톡해 μ •ν•©μ„± 문제λ₯Ό μ²˜λ¦¬ν•˜κΈ°λ‘œ 결둠을 λ‚΄λ ΈμŠ΅λ‹ˆλ‹€.

Master μž₯μ• μ‹œ Slave의 승격 μ—¬λΆ€

λ§Œμ•½ Masterκ°€ μž₯μ• κ°€ λ‚œλ‹€λ©΄ Slave ν•˜λ‚˜λ₯Ό μŠΉκ²©μ‹œμΌœμ„œ 이λ₯Ό Master 처럼 μ‚¬μš©ν•΄μ•Ό ν•  κ²ƒμž…λ‹ˆλ‹€. μ§€κΈˆ κ΅¬μ‘°μ—μ„œ 문제라고 μƒκ°ν•˜λŠ” 점은 Master μž₯μ• μ‹œ κ΄€λ¦¬μžκ°€ μˆ˜λ™μœΌλ‘œ 이λ₯Ό μ²˜λ¦¬ν•΄μ•Ό ν•œλ‹€λŠ” μ μž…λ‹ˆλ‹€.

μ΄λŸ¬ν•œ 문제λ₯Ό Group Replicaiton으둜 해결해쀄 수 μžˆλ‹€κ³  ν•©λ‹ˆλ‹€.

img 8

Group Replication을 μ‚¬μš©ν•˜λ©΄ Master DB μž₯μ• μ‹œ μžλ™μœΌλ‘œ Slave DBκ°€ Master DB둜 μŠΉκ²©λ˜λŠ” μž₯점이 μžˆμŠ΅λ‹ˆλ‹€. μ΄λŸ¬ν•œ 이유 λ•Œλ¬Έμ— ν˜„μž¬ ꡬ쑰인 Master - Slave Replication κ΅¬μ‘°μ—μ„œ Group Replication으둜 λ³€κ²½ν•˜λŠ” 것이 쑰금 더 μž₯μ•  λŒ€μ²˜μ— 쒋을 것이라 생각이 λ“œλŠ”λ° μ •ν™•ν•œ λ‚΄μš©μ€ 쑰금 더 ν•™μŠ΅μ„ 해봐야 ν•  κ²ƒμœΌλ‘œ λ³΄μž…λ‹ˆλ‹€.

Replication μž‘μ—… κ³Όμ •μ—μ„œ κ²ͺ을 μˆ˜λ„ μžˆλŠ” λ¬Έμ œλ“€

Public key retrieval is not allowed

MySQL DB에 μ ‘μ†ν•˜λ €λ©΄ 기본적으둜 url, username, password 3가지 μ˜΅μ…˜μ΄ ν•„μš”ν•©λ‹ˆλ‹€. ν•˜μ§€λ§Œ MySQL 8.0 λ²„μ „λΆ€ν„°λŠ” λ³΄μ•ˆμ μΈ 이슈둜 useSSL μ˜΅μ…˜μ— λŒ€ν•œ 좔가적인 섀정이 ν•„μš”ν•©λ‹ˆλ‹€.

λ‹€μŒ 두 가지 μ˜΅μ…˜μ΄ ν•„μš”ν•œλ° μ΄λŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  • useSSL : DB에 SSL둜 μ—°κ²°
  • allowPublicKeyRetrieval: ν΄λΌμ΄μ–ΈνŠΈκ°€ μ„œλ²„μ—μ„œ κ³΅κ°œν‚€λ₯Ό μžλ™μœΌλ‘œ μš”μ²­ν•  수 μžˆλ„λ‘ μ„€μ •

μ΅œμ’…μ μœΌλ‘œ application.yml에 2가지 속성을 μΆ”κ°€ν•΄μ£Όλ©΄ μ •μƒμ μœΌλ‘œ 연결이 λ©λ‹ˆλ‹€.

jdbc:mysql://xxx.xxx.xx.x:3306/test_db?useSSL=false&allowPublicKeyRetrieval=true

cachingsha2password authentication plugin

MySQL 8.0은 SHA-246 hasing을 κ΅¬ν˜„ν•˜λŠ” 두 가지 인증 ν”ŒλŸ¬κ·ΈμΈμ„ μ§€μ›ν•©λ‹ˆλ‹€.

  • sha256_password : 기본적인 SHA-256 인증을 κ΅¬ν˜„ν•œ ν”ŒλŸ¬κ·ΈμΈ
  • cachingsha2password : sha256_password와 λ™μΌν•œλ° μ„±λŠ₯ ν–₯상을 μœ„ν•΄ μ„œλ²„ 캐싱을 이용

λ¬Έμ œλŠ” caching_sha2_passwordλ₯Ό μ‚¬μš©ν•˜λ €λ©΄ λ‹€μŒ 쑰건 쀑 ν•˜λ‚˜κ°€ λ§Œμ‘±ν•΄μ•Ό ν•œλ‹€λŠ” μ μž…λ‹ˆλ‹€.

  • SSL λ³΄μ•ˆμ—°κ²°μ„ μ‚¬μš©
  • RSA λ³΄μ•ˆμ„ μ μš©ν•œ λΉ„μ•”ν˜Έ 연결을 μ‚¬μš©

ν•˜μ§€λ§Œ μ €λŠ” ν…ŒμŠ€νŠΈ 용으둜 λ‘œμ»¬μ—μ„œ μž‘μ—…μ„ ν•˜κ³  μžˆμ—ˆκΈ° λ•Œλ¬Έμ— λ³΄μ•ˆ 연결을 μ‚¬μš©ν•˜μ§€ μ•Šμ•„ μ•„λž˜μ™€ 같은 λ¬Έμ œκ°€ λ°œμƒν–ˆμ—ˆμŠ΅λ‹ˆλ‹€.

Last_IO_Error: error connecting to master 'replication@test:3306' - retry-time: 60 retries: 1 message: Authentication plugin 'caching_sha2_password' reported error: Authentication requires secure connection.

이λ₯Ό ν•΄κ²°ν•˜λŠ” 방법은 μ΄μ „μ˜ mysql_native_passwordλ₯Ό μ‚¬μš©ν•˜λ©΄ λ©λ‹ˆλ‹€. μ‚¬μš©μžμ˜ μ•”ν˜Έλ₯Ό DDL을 ν†΅ν•΄μ„œ 이전 λ°©μ‹μœΌλ‘œ λ³€κ²½ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

mysql> ALTER USER 'replication'@'%' IDENTIFIED WITH mysql_native_password BY 'password';

그리고 MySQL을 μž¬μ‹œμž‘ν•˜λ©΄ μ •μƒμ μœΌλ‘œ MySQL에 접속할 수 μžˆλŠ” 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

단, ν•΄λ‹Ή 방식은 MySQL이 μ§€μ›ν•˜λŠ” μ˜ˆμ „ 방식이기 λ•Œλ¬Έμ— μ‹€μ œλ‘œ μš΄μ˜ν™˜κ²½μ—μ„œλŠ” λ³΄μ•ˆ μž‘μ—…μ„ μ²˜λ¦¬ν•œ ν›„, μ‚¬μš©ν•˜λŠ”κ²Œ 쒋을 κ²ƒμœΌλ‘œ λ³΄μž…λ‹ˆλ‹€.

마무리

μ§€κΈˆκΉŒμ§€ MySQL의 Replication κ³Ό Spring Bootμ—μ„œμ˜ ν™œμš©λ°©λ²•μ— λŒ€ν•΄μ„œ ν•™μŠ΅ν•œ λ‚΄μš©κ³Ό κ³ λ―Όν•œ 뢀뢄을 μž‘μ„±ν•΄λ³΄μ•˜μŠ΅λ‹ˆλ‹€. (μΆ”κ°€μ μœΌλ‘œ μ„±λŠ₯ ν…ŒμŠ€νŠΈ κ²°κ³Όλ₯Ό λ³΄μΆ©ν•΄μ„œ μž‘μ„±ν•  μ˜ˆμ •μž…λ‹ˆλ‹€) μœ„μ—μ„œ μ†Œκ°œλœ 방법듀이 정닡은 μ•„λ‹ˆκΈ° λ•Œλ¬Έμ—, μ•žμœΌλ‘œλ„ 쑰금 더 DB Scale-out에 λŒ€ν•΄μ„œ 고민을 ν•΄λ΄μ•Όκ² μŠ΅λ‹ˆλ‹€.

μ°Έκ³ ν•œ κ³³

@루킀
μ‹œκ°„μ΄ ν˜λŸ¬λ„ 변화에 λŒ€μ‘λ˜λŠ” μ½”λ“œλ₯Ό μΆ”κ΅¬ν•©λ‹ˆλ‹€.