public class AmazonRdsReadReplicaAwareDataSourceFactoryBean extends AmazonRdsDataSourceFactoryBean {
public AmazonRdsReadReplicaAwareDataSourceFactoryBean(AmazonRDS amazonRDS, String dbInstanceIdentifier,
String password) {
super(amazonRDS, dbInstanceIdentifier, password);
}
@Override
protected DataSource createInstance() throws Exception {
DBInstance dbInstance = getDbInstance(getDbInstanceIdentifier());
// If there is no read replica available, delegate to super class
if (dbInstance.getReadReplicaDBInstanceIdentifiers().isEmpty()) {
return super.createInstance();
}
HashMap<Object, Object> replicaMap = new HashMap<>(dbInstance.getReadReplicaDBInstanceIdentifiers().size());
for (String replicaName : dbInstance.getReadReplicaDBInstanceIdentifiers()) {
replicaMap.put(replicaName, createDataSourceInstance(replicaName));
}
// Create the data source
ReadOnlyRoutingDataSource dataSource = new ReadOnlyRoutingDataSource();
dataSource.setTargetDataSources(replicaMap);
dataSource.setDefaultTargetDataSource(createDataSourceInstance(getDbInstanceIdentifier()));
// Initialize the class
dataSource.afterPropertiesSet();
return new LazyConnectionDataSourceProxy(dataSource);
}
...
3. Read Only Transaction 여부
→ TransactionSynchronizationManager 사용
모듈은 읽기 부하 분산을 Connection을 수준에서 처리합니다.
ReadOnlyRoutingDataSource가 Connection을 열 때는 현재 트랜잭션의 readOnly 여부를 살펴봅니다.
readOnly가 맞다면 Read Replica로 향하는 DataSource 중 하나를 랜덤 선택하고 Connection을 만듭니다.
현재 트랜잭션의 readOnly 여부를 판단할 수 있다.
단, 이미 수립된 트랜잭션에 대한 정보를 요청하는 것이라 트랜잭션 생성중에는 이용할 수 없다.
구현 애로사항
→ LazyConnectionDataSource 사용
사실 위 시나리오에서 트랜잭션의 readOnly를 얻는 데에 애로사항이 있습니다. Connection이 만들어지는 기본 시점 때문입니다.
JpaTransactionManager.doBegin()
JpaTransactionManager에서 Connection 생성 시점
트랜잭션 매니저는 트랜잭션을 생성하는 중에 DataSource에게 Connection을 요청한다.
이 시점에 DataSource는 TransactionSynchronizationManager.isCurrentTransactionReadOnly()를 사용할 수 없다.
따라서 Connection 생성을 트랜잭션 진입 이후로 늦춰주는 장치가 필요합니다. 여기서 **LazyConnectionDataSource***를 이용할 수 있습니다.
LazyConnectionDataSource로 ReadOnlyRoutingDataSource를 감싸는 이유
DataSource를 감싸 프록시 커넥션을 반환시키기 위해서.
실제 Connection이 필요하게 될 경우에서야 감싼 ReadOnlyRoutingDataSource의 .getConnection() 을 호출하게 된다.
해당 시점엔 트랜잭션의 readOnly를 판단 가능하다. 따라서 Read Replica로 향하는 DataSource 중 하나에게 Connection을 열어달라고 부탁할 수 있다.
LazyConnectionDataSourceProxy.getConnection() 에서 프록시를 리턴하는 모습
의존성 방향
JpaTransactionManager → LazyConnectionDataSource→ ReadOnlyRoutingDataSource → one of Read Replica DataSource
연관 챕터
1
개요
부하 분산 아키텍처는 단일 엔드포인트를 사용자에게 노출하고, 뒷 단의 트래픽 라우팅은 투명하게 감추는 게 이상적입니다.
아마존 관계형 DB 서비스 RDS는, 비용을 20% 가량 더 지불해 Aurora라는 래퍼를 사용하여 해당 부문을 향유할 수 있습니다.
하지만 일반적인 RDS(MySQL, PostgreSQL ...)를 사용할 경우 부하 분산 책임은 AWS가 아닌 응용 계층에 지워집니다.
이 글은 단일 엔드포인트를 사용할 수 없어 응용에서 직접 엔드포인트를 분기 선택할 경우 어떤 구현이 가능한지를 다룹니다.
Spring Cloud AWS JDBC 모듈을 사례로 들어 RDS의 Read Replica와 연동하는 부분을 살펴보았습니다.
RDS
AWS의 관계형 DB 솔루션: Relational Database Service
RDS Read Replica
Master 인스턴스와 비동기 복제 메커니즘이 수립된 인스턴스
응용의 책임
좀 더 자세하게 응용 계층에게 필요한 역할을 묘사하면 이렇습니다.
① Read Replica들의 엔드포인트를 알아야 합니다.
② 현재 DB로 수행하는 작업이 읽기인지 쓰기인지 판단해야 합니다.
③ 읽기 작업시 Read Replica를 적절하게 순환 선택하며 Connection을 수립해야 합니다.
Spring Cloud AWS JDBC
기본 셋업 개발자가 하는 세팅은 이게 다입니다.
[spring-cloud-aws-jdbc] 모듈이 Read Replica를 분기 선택하는 과정을 투명하게 수행해 줍니다.
readReplicaSupport
옵션을 줄 경우 ReadOnlyRoutingDataSource를 얻습니다.How to resolve
읽기 부하를 분산하기 위해 필요한 요소들을 모듈이 어떻게 획득하는지 살펴봅니다.
1. RDS & Read Replica의 엔드포인트
→ AWS SDK, AWS 유저 크레덴셜, AWS Region, DB 인스턴스 식별자
부하 분산 시스템은 가용한 자원을 훤히 들여다 볼 수 있어야 합니다. RDS는 AWS 서비스이며, AWS 사용자에게 할당된 자원임에 착안하면, AWS SDK를 쓰지 않을 이유가 없습니다.
모듈은 AWS SDK + 유저 크레덴셜 + DB 식별자로 연결하려는 Master와 Read Replica의
DBInstance
객체를 얻습니다. 참고로 AWS 서비스는 특정 Region에 국한되어 제공되므로 Region 정보도 필요합니다.2. Data Sources
이제
DBInstance
를 다 알기 때문에 DB의 메타정보를 획득할 수 있습니다. 여기에 DB 접근용 username/password만 있으면 DataSource를 생성 가능합니다.모듈은 읽기 부하를 밸런싱해주는 ReadOnlyRoutingDataSource를 만듭니다. 다시 이 빈의 내부에는 Master 및 Read Replica에 대한 DataSource들이 저장됩니다.
AmazonRdsReadReplicaAwareDataSourceFactoryBean
.createInstance()
3. Read Only Transaction 여부
→ TransactionSynchronizationManager 사용
모듈은 읽기 부하 분산을 Connection을 수준에서 처리합니다.
readOnly
여부를 살펴봅니다.readOnly
가 맞다면 Read Replica로 향하는 DataSource 중 하나를 랜덤 선택하고 Connection을 만듭니다.ReadOnlyRoutingDataSource
.determineCurrentLookupKey()
구현 애로사항
→ LazyConnectionDataSource 사용
사실 위 시나리오에서 트랜잭션의
readOnly
를 얻는 데에 애로사항이 있습니다. Connection이 만들어지는 기본 시점 때문입니다.JpaTransactionManager.doBegin()
JpaTransactionManager에서 Connection 생성 시점
트랜잭션 매니저는 트랜잭션을 생성하는 중에 DataSource에게 Connection을 요청한다. 이 시점에 DataSource는
TransactionSynchronizationManager.isCurrentTransactionReadOnly()
를 사용할 수 없다.따라서 Connection 생성을 트랜잭션 진입 이후로 늦춰주는 장치가 필요합니다. 여기서 **LazyConnectionDataSource***를 이용할 수 있습니다.
LazyConnectionDataSource로 ReadOnlyRoutingDataSource를 감싸는 이유
DataSource를 감싸 프록시 커넥션을 반환시키기 위해서. 실제 Connection이 필요하게 될 경우에서야 감싼 ReadOnlyRoutingDataSource의
.getConnection()
을 호출하게 된다. 해당 시점엔 트랜잭션의readOnly
를 판단 가능하다. 따라서 Read Replica로 향하는 DataSource 중 하나에게 Connection을 열어달라고 부탁할 수 있다.LazyConnectionDataSourceProxy.getConnection()
에서 프록시를 리턴하는 모습의존성 방향 JpaTransactionManager → LazyConnectionDataSource→ ReadOnlyRoutingDataSource → one of Read Replica DataSource
참고자료 spring-cloud-aws-jdbc (스프링독) spring-cloud-aws (깃허브) LazyConnectionDataSourceProxy란? (블로그)
@caffeine-library/readers-system-design-interview