caffeine-library / system-design-interview

🌱 가상 면접 사례로 배우는 대규모 시스템 설계 기초를 읽는 스터디
4 stars 0 forks source link

[additional] DB 읽기 분산 사례 : RDS Read Replica + Spring Cloud AWS #18

Closed binchoo closed 2 years ago

binchoo commented 2 years ago

연관 챕터

1

개요

부하 분산 아키텍처는 단일 엔드포인트를 사용자에게 노출하고, 뒷 단의 트래픽 라우팅은 투명하게 감추는 게 이상적입니다.

아마존 관계형 DB 서비스 RDS는, 비용을 20% 가량 더 지불해 Aurora라는 래퍼를 사용하여 해당 부문을 향유할 수 있습니다.

하지만 일반적인 RDS(MySQL, PostgreSQL ...)를 사용할 경우 부하 분산 책임은 AWS가 아닌 응용 계층에 지워집니다.

이 글은 단일 엔드포인트를 사용할 수 없어 응용에서 직접 엔드포인트를 분기 선택할 경우 어떤 구현이 가능한지를 다룹니다.

Spring Cloud AWS JDBC 모듈을 사례로 들어 RDS의 Read Replica와 연동하는 부분을 살펴보았습니다.

RDS

image

AWS의 관계형 DB 솔루션: Relational Database Service

RDS Read Replica

Master 인스턴스와 비동기 복제 메커니즘이 수립된 인스턴스

image

응용의 책임

좀 더 자세하게 응용 계층에게 필요한 역할을 묘사하면 이렇습니다.

① Read Replica들의 엔드포인트를 알아야 합니다.

② 현재 DB로 수행하는 작업이 읽기인지 쓰기인지 판단해야 합니다.

③ 읽기 작업시 Read Replica를 적절하게 순환 선택하며 Connection을 수립해야 합니다.

Spring Cloud AWS JDBC

기본 셋업 개발자가 하는 세팅은 이게 다입니다.

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:jdbc="http://www.springframework.org/schema/cloud/aws/jdbc"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aws-context="http://www.springframework.org/schema/cloud/aws/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/cloud/aws/context
        http://www.springframework.org/schema/cloud/aws/context/spring-cloud-aws-context.xsd
        http://www.springframework.org/schema/cloud/aws/jdbc
        http://www.springframework.org/schema/cloud/aws/jdbc/spring-cloud-aws-jdbc.xsd">

    <aws-context:context-credentials>
        <aws-context:simple-credentials
                access-key="${aws.user.access-key}" secret-key="${aws.user.secret-key}" />
    </aws-context:context-credentials>

    <aws-context:context-region region="${aws.region}"/>

    <jdbc:data-source
            db-instance-identifier="${rds.instance-id}" username = "${rds.username}" password="${rds.password}"
            read-replica-support="true"/>
</beans>

[spring-cloud-aws-jdbc] 모듈이 Read Replica를 분기 선택하는 과정을 투명하게 수행해 줍니다.

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()

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.determineCurrentLookupKey()

public class ReadOnlyRoutingDataSource extends AbstractRoutingDataSource {
  ...
  @Override
  protected Object determineCurrentLookupKey() {
  if (TransactionSynchronizationManager.isCurrentTransactionReadOnly() && !this.dataSourceKeys.isEmpty()) {
      return this.dataSourceKeys.get(getRandom(this.dataSourceKeys.size()));
  }

  return null;
  }
  ...
}

TransactionSynchronizationManager.isCurrentTransactionReadOnly()

현재 트랜잭션의 readOnly 여부를 판단할 수 있다. 단, 이미 수립된 트랜잭션에 대한 정보를 요청하는 것이라 트랜잭션 생성중에는 이용할 수 없다.

구현 애로사항

LazyConnectionDataSource 사용

사실 위 시나리오에서 트랜잭션의 readOnly를 얻는 데에 애로사항이 있습니다. Connection이 만들어지는 기본 시점 때문입니다.

JpaTransactionManager.doBegin()

image

따라서 Connection 생성을 트랜잭션 진입 이후로 늦춰주는 장치가 필요합니다. 여기서 **LazyConnectionDataSource***를 이용할 수 있습니다.

LazyConnectionDataSourceProxy.getConnection() 에서 프록시를 리턴하는 모습

image

의존성 방향 JpaTransactionManager → LazyConnectionDataSource→ ReadOnlyRoutingDataSource → one of Read Replica DataSource

참고자료 spring-cloud-aws-jdbc (스프링독) spring-cloud-aws (깃허브) LazyConnectionDataSourceProxy란? (블로그)

@caffeine-library/readers-system-design-interview