티스토리 뷰
1. 2PC의 개념
2PC는 분산환경에서 트랜잭션의 원자성을 지키기 위해 사용하는 프로토콜이다.
무엇이 문제인가
- 흔히 말하는 MSA 환경: 서비스 단위로 DB와 서버 어플리케이션이 분리되어 있는 환경에서
- 여러 인스턴스 & 각 DB를 거치는 트랜잭션(이를 분산트랜잭션이라 한다)을 어떻게 all or nothing으로 처리할 수 있는가?
- 이런 환경에서는 db transaction (begin, end) 또는 서버 어플리케이션 코드 단위의 트랜잭션 기능(ex. @Transactional)은 원자성을 보장해주지 못한다.
이를 해결하기 위해 등장한 것이 2PC이다.
우선 알아야 할 단어가 있는데 바로 노드다.
노드란: 분산 시스템 내에서 트랜잭션에 참여하는 개별 인스턴스 or 서버 (주로 DB)를 지칭한다.
노드는 두 가지 종류가 있는데,
- Coordinator (조정자): 분산 트랜잭션이 제대로 진행되는지 관리하는 노드
- Participator (참여자): 조정자 외 분산 트랜잭션에 참여하는 다른 모든 노드
2PC는 그 이름처럼 2 단계(phase)로 나뉘어 진행된다.
1) 준비단계 Prepare Phase
- Coordinator가 트랜잭션 Participator들에게 트랜잭션에 참여할 수 있는지 확인한다.
- 각 Participator는 트랜잭션을 로컬에서 준비하고, 성공할 수 있는지 여부를 응답한다. (Y/N)
- 모든 Participator들이 Y를 보내야 다음 단계로 넘어간다.


2) 커밋 단계 Commit Phase
- 모든 Participator가 YES를 보냈다면 조정다는 Commit 명령을 내린다.
- Participator가 commit이 제대로 되지 않았다면 전체 Rollback 한다.

과정을 하나의 다이어그램으로 표현하면 다음과 같다.

이론적 설명은 직관적이고 쉽다.
하지만 실제 구현은 어떤 방식으로 이뤄질까?
2. 2PC의 구현
구현 레벨에서의 조정자는
- 트랜잭션을 관리하는 주체로, 어플리케이션 서버 내에서 작동할 수도 있고, 별도의 트랜잭션 관리 시스템일 수도 있다.
- 2가지의 방법이 있는데, 분산된 각 노드별로 있거나 중앙화 시켜서 한 곳에서 있게 할 수 있다.
Spring, MySQL 기술스택을 사용하는 MSA환경에서
서버 A, B, C가 각각 DB 1,2,3 을 사용하고 있다고 가정해보자.
이때 이 2PC를 구현하기 위해 필요한 기술은 3가지가 있다.
1) Spring Transaction Manager
- JTA (Java Transaction API)
- JtaTransactionManager
2) JTA Coordinator
- JTA 트랜잭션 관리자는 각 DB와의 트랜잭션을 조정한다.
- 트랜잭션이 시작되면 MySQL DB에 Prepare, Commit, Rollback을 조정한다.
3) MySQL의 XA Transaction
- MySQL에서 지원하는 분산 트랜잭션의 표준이다.
여기서 별개의 이슈가 발생하는데 바로 각 노드들이 참여자들에 대한 정보를 모두 알아야한다는 것이다.
1) 트랜잭션 조정자가 모든 노드를 알아야하는 이유
- 조정자는 트랜잭션을 시작하고, 그 트랜잭션에 어떤 데이터베이스가 참여할지를 알고 있어야 함
- 각 참여노드는 조정자와 협력하여 트랜잭션 상태(준비 완료 또는 실패)를 공유
- 조정자는 모든 참여 노드에서 Prepare 메시지에 대한 응답을 받은 후 최종적으로 커밋 또는 롤백을 결정
이 과정에서, 조정자는 트랜잭션에 참여하는 모든 데이터베이스(노드)를 알아야 올바른 트랜잭션 상태를 유지할 수 있다.
2) 어떻게 애플리케이션이 모든 노드를 인식하게 될까?
- 사전 구성:
- 보통 분산 트랜잭션에 참여하는 데이터베이스 정보는 사전에 구성
- Spring 같은 프레임워크에서는 각 데이터베이스에 대한 트랜잭션 설정을 명시적으로 구성
- JTA와 XA 트랜잭션:
- JTA 트랜잭션 관리자는 분산 트랜잭션의 참여 데이터베이스를 인식. 각 데이터베이스는 XA 트랜잭션을 통해 트랜잭션에 참여
- 애플리케이션 코드에서는 주로 트랜잭션 관리자가 해당 트랜잭션에 포함된 데이터베이스들과 상호작용하도록 설정
- 트랜잭션 범위에서 자동으로 참여:
- Spring에서는 여러 데이터베이스에 접근하는 로직이 있으면, 트랜잭션 관리자(JtaTransactionManager)가 그 트랜잭션에 참여하는 각 데이터베이스를 자동으로 인식하고 관리
- 즉, 비즈니스 로직에서 여러 데이터베이스를 접근하면 트랜잭션 관리자는 각 데이터베이스와 트랜잭션을 조정할 수 있도록 준비
3) 문제점 및 해결책
이 방식에서는 조정자가 모든 데이터베이스에 대한 정보를 사전에 알아야 하고, 그로 인해 확장성 문제가 발생한다.
새로운 데이터베이스가 추가되면 해당 정보를 조정자가 알아야 하고, 이때마다 트랜잭션 참여 구성이 업데이트가 필요하게 된다.
이 확장성 문제를 동적 서비스 디스커버리로 해결한다.
이는 각 서버나 데이터베이스 노드가 자동으로 트랜잭션에 참여할 수 있도록 하고, 중앙화 된 조정자가 동적으로 참여 노드를 관리할 수 있도록 지원한다. 그 중 하나인 Eureka를 예시로 들겠다.
4) Eureka를 사용한 2PC 구현 예시
Eureka 서버 설정
// EurekaServerApplication.java
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
서버 A,B,C의 Spring Boot 설정 (Eureka Client)
application.yml (서버 A의 경우, B와 C도 유사하게 설정)
server:
port: 8081 # 서버 A의 포트
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/ # Eureka 서버 URL
instance:
hostname: server-a # 서버 A의 호스트 이름
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_a # 서버 A의 데이터베이스
username: root
password: password
jta:
enabled: true
logging:
level:
org.springframework: DEBUG
서버 B와 C는 각각 포트, 호스트명, 데이터베이스 URL을 다르게 설정하고 Eureka 서버에 연결되도록 설정
MySQL 데이터베이스에서 XA 트랜잭션 활성화
SET GLOBAL innodb_support_xa=1;
Spring에서 JTA 트랜잭션 설정
각 어플리케이션서버는 JTA 트랜잭션 사용해서 2PC 트랜잭션 관리
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>javax.transaction-api</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
</dependency>
JTA 트랜잭션 관리 설정
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(UserTransaction userTransaction, TransactionManager transactionManager) throws Throwable {
return new JtaTransactionManager(userTransaction, transactionManager);
}
@Bean
public UserTransaction userTransaction() throws Throwable {
com.arjuna.ats.jta.UserTransactionImp userTransactionImp = new com.arjuna.ats.jta.UserTransactionImp();
return userTransactionImp;
}
@Bean
public TransactionManager transactionManager() throws Throwable {
com.arjuna.ats.jta.TransactionManagerImple transactionManagerImple = new com.arjuna.ats.jta.TransactionManagerImple();
return transactionManagerImple;
}
}
분산 트랜잭션 코드
서버 A에서 분산 트랜잭션을 관리하고, 서버 B와 C에 대한 트랜잭션을 실행한다고 가정
@RestController
public class DistributedTransactionController {
@Autowired
private DataSource dataSourceA;
@Autowired
private RestTemplate restTemplate;
@Transactional
@PostMapping("/distributed-transaction")
public String distributedTransaction() throws Exception {
// 서버 A의 데이터베이스에 트랜잭션 수행
try (Connection connectionA = dataSourceA.getConnection()) {
connectionA.setAutoCommit(false);
PreparedStatement stmt = connectionA.prepareStatement("INSERT INTO table_a (data) VALUES (?)");
stmt.setString(1, "Data for Server A");
stmt.executeUpdate();
// 서버 B와 C로 REST 요청을 보내 트랜잭션을 처리함
String serverBResponse = restTemplate.postForObject("http://server-b:8082/transaction", null, String.class);
String serverCResponse = restTemplate.postForObject("http://server-c:8083/transaction", null, String.class);
if ("SUCCESS".equals(serverBResponse) && "SUCCESS".equals(serverCResponse)) {
connectionA.commit();
return "Distributed transaction committed successfully";
} else {
connectionA.rollback();
throw new Exception("Transaction failed on server B or C");
}
} catch (Exception e) {
throw new Exception("Distributed transaction failed", e);
}
}
}
3. 한계점
- 분산 트랜잭션을 사용할 수 있는 application이면 어떤 DB든 가능
- NoSQL X
- 이종간 DB 구현 어려움
- DB 이중화 구조와 비슷하기 때문에 하나의 API 서버에 요청이 들어오고, 내부적으로 DB가 분산되어 있을 때 사용하는 형태
- 성능 오버헤드
- 2단계로 이뤄지고 트랜잭션 참여 노드 <-> 코디네이터 간의 네트워크 통신 발생
- 레이턴시 발생 -> 트랜잭션 시간 증가 -> bottle neck 유발 (처리량 & 타임아웃)
- 리소스 락
- 2PC에서는 트랜잭션이 완료될 때까지 모든 참여 노드가 데이터를 락(Lock) 상태 유지
- Prepare 단계에서 모든 리소스가 OK를 반환하면 트랜잭션이 끝날 때까지 해당 리소스는 다른 트랜잭션이 접근하지 못함
- 데드락 발생 할 수 있음
'실무 개발' 카테고리의 다른 글
분산 트랜잭션 처리하기 (0) | 2025.02.22 |
---|---|
커스텀 어노테이션으로 중복 코드 제거하기 (0) | 2023.10.31 |
NGINX는 왜 Apache보다 좋은가? (0) | 2022.06.17 |
- Total
- Today
- Yesterday
- nginx 내부
- decorator
- 카카오
- Spring
- IOC
- PatternSyntaxException
- 디자인패턴
- 프로그래밍 모델
- 2020 KAKAO
- Kakao Blind
- 신규 아이디 추천
- spring cloud sleuth
- Java #GC #가비지콜렉터 #Garbage Collector
- 모던 자바 인 액션
- 2019 Kakao Blind
- behavior parameterization
- Java #JIT #JVM
- WORE
- 코테
- 카카오 인턴
- zipkin
- WORA
- 스프링
- KAKAO 2021
- Java
- 2021
- 카카오코테
- jvm
- okhttp3
- 카카오 코테
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |