본문 바로가기
Develop

데이터베이스 샤딩 : Django[DRF]로 Shard_DB에 create, read 구현하기

by hongreat 2023. 5. 14.

 

 

데이터 베이스 샤딩 혹은 샤드라고도 하며 데이터 베이스를 구축함에 있어서 사용되는 기법을 알게 되어 django 로 가볍게 구현해보며 이해를 하고자했다.

 

 

 

 

1. 샤딩에 대해서

  • 쉽게말하면 수평적으로 데이터베이스 구조를 확장하는 것이다. 파티션과 동일한 개념이라고 생각된다.
  • 데이터베이스 샤딩은 대규모 데이터베이스를 여러 대의 머신에 저장하는 프로세스 이다.
    • 단일 머신 또는 데이터베이스 서버는 제한된 양의 데이터만 처리.
    • 데이터베이스 샤딩은 데이터를 샤드라는 작은 청크로 분할하고 여러 데이터베이스 서버에 저장한다. (나누는 프로세스의 결과물)

애플리케이션이 커짐에 따라 데이터 양이 증가할 때 고민해 볼 수 있을 것 같다.

데이터 양이 많아지면 데이터베이스에서 병목 현상이 발생할 수 있고, 이는 애플리케이션 성능에 영향을다 준다. 즉, 여러 샤드에서 더 작은 데이터 세트를 병렬로 처리하는 방식으로 데이터 처리를 분산시키는 것.

 

2. docker로 (샤드)데이터베이스 구축 및 세팅

도커 네트워크 생성

도커 네트워크를 생성해준다.

docker network create shard-network

Docker는 컨테이너 간 통신이나 컨테이너와 호스트 간의 통신 등 다양한 네트워크를 구성할 수 있다. (각 컨테이너는 고유한 IP 주소를 할당받게 되고, 다른 컨테이너와 통신가능)

Docker에서 컨테이너를 네트워크로 연결할 때, 기본적으로 **bridge**라는 이름의 가상 네트워크를 사용한다. 하지만 이 외에도 사용자가 직접 네트워크를 생성하여 컨테이너를 연결할 수 있는데, 이때 생성한 네트워크는 Docker 호스트와 분리된 가상 네트워크로 컨테이너 간에 안전하게 통신할 수 있다.

따라서, 샤딩을 구축하기 전에 Docker 네트워크를 생성하는 것은 샤드 노드 간의 안전한 통신을 위함이다. 이를 통해 샤드 노드 간에 안전하게 데이터를 전송하고, 샤딩된 데이터베이스를 구성할 수 있다.

 

 

 

docker DB(postgresql) 생성 및 shart-newwork 연결

도커 호스트에서 생성한 네트워크에 샤드 노드 컨테이너들을 연결하는 방법 이 경우, 컨테이너를 생성할 때 --network 옵션을 이용하여 네트워크를 지정할 수 있다. (docker network create 명령어를 이용하여 생성한 shard-network 네트워크에 컨테이너를 연결하고자 할 때)

docker run -d \\
--name postgres-container-shard1 -p 5432:5432 --network=shard-network -e POSTGRES_USER=postgres -e POSTGRES_DB=shard_db_1 \\
-e POSTGRES_PASSWORD=password \\
-e PGDATA=/var/lib/postgresql/data/pgdata \\
postgres

docker run -d \\
--name postgres-container-shard2 -p 5433:5432 --network=shard-network -e POSTGRES_USER=postgres -e POSTGRES_DB=shard_db_2 \\
-e POSTGRES_PASSWORD=password \\
-e PGDATA=/var/lib/postgresql/data/pgdata \\
postgres
# 샤드 1번 DB
docker run -d --name=postgres-shard1 -p 5432:5432 --network=shard-network -e POSTGRES_PASSWORD=password postgres

# 샤드 2번 DB
docker run -d --name=postgres-shard2 -p 5433:5432 --network=shard-network -e POSTGRES_PASSWORD=password postgres

 

 

컨테이너 확인

 

 

 

django에서 샤딩된 데이터 베이스에 연결

  • django setting 에서 shard db 사용
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'shard_db_1',
        'USER': '',
        'PASSWORD': '',
        'HOST': 'localhost',
        'PORT': '5432',
    },
    'shard_1': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'shard_db_1',
        'USER': '',
        'PASSWORD': '',
        'HOST': 'localhost',
        'PORT': '5432',
    },
    'shard_2': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'shard_db_2',
        'USER': '',
        'PASSWORD': '',
        'HOST': 'localhost',
        'PORT': '5433',
    },
}

 

 

 

 

3. 구현

 

 

 

Router설정

class ShardDBRouter:
    databases = settings.DATABASES
    route_app_labels = {"user"}

    def db_for_read(self, model, **hints):
        """
        모델 인스턴스의 읽기 작업에 대해 사용할 데이터베이스를 반환한다.
        model은 쿼리가 수행되는 모델 클래스이며, hints는 선택적이다.
        읽기 작업에 대해 각 모델 클래스가 사용할 데이터베이스를 반환할 수 있다.
        => app 단(or model) 에서 샤딩이 필요 할 경우 분기 처리 할 수 있을 듯!
        """
        db_name = "default"
        return db_name

    def db_for_write(self, model, **hints):
        """
        모델 인스턴스의 쓰기 작업에 대해 사용할 데이터베이스를 반환한다.
        모델 클래스가 쓰기 작업에 대해 사용할 데이터베이스를 반환할 수 있습니다.
        => 이 역시도 app 단(or model) 에서 샤딩이 필요 할 경우 분기 처리에 사용
        """

        db_name = "default"
        return db_name

    @staticmethod
    def allow_relation(obj1, obj2, **hints):
        """
        두 개의 모델 인스턴스 obj1과 obj2가 상호 작용할 수 있는지 여부를 판단하고
        모델 클래스 간에 관계를 허용할 수 있는지 여부를 결정한다.
        """
        return True

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        """
        지정된 데이터베이스 db에서 app_label 애플리케이션의 model_name 모델의 마이그레이션을 수행할 수 있는지 여부를 결정한다.
        => 이 과정을 잘 사용하면 DB 내용변경에 있어서 검증에 대한 로직도 구현할 수 있을듯?!
        model_name은 선택적으로 지정가능.
        """
        migrate = False
        if app_label in self.route_app_labels:
            # shard_1, shard_2에서 모두 migration을 허용.
            if db in ["shard_1", "shard_2"]:
                migrate = True
        return migrate

 

 

Create

 

 

  • 유저를 생성하는 상황( 회원가입 시 / POST )
  • location(지역) 필드 값을 입력받는다.
    • 3가지 Case : KO , US , ETC(기타)
  • TO-BE : KO 유저(or ETC) 는 shard_1 DB에, US 유저는 shard_2 DB에 생성 되어야 한다.
  • solution
    • db_for_write 함수에서 별짓을 다해봤지만, model.location 이 추적되지 않았다. instance로 지정이 안된것인가 싶어 지정하고 해봐도.. 안됨.
    • 결국 model 의 save 함수에서 location 의 값에 따라 using db를 다르게 변경하도록 mapping 해줌으로 해결했다.
    class User(BaseModelMixin, AbstractUser):
        first_name = None
        last_name = None
        username = None
        email = models.EmailField(verbose_name="이메일", unique=True)
        location = models.CharField(verbose_name="지역", max_length=32)  # KO,US
    
        USERNAME_FIELD = "email"
        REQUIRED_FIELDS = []
    
        objects = UserManager()
    
        class Meta:
            db_table = "user"
            verbose_name = "유저"
            verbose_name_plural = verbose_name
    
        def __str__(self):
            return self.email
    
        def save(self, *args, **kwargs):
            database_mapper = {
                "KO": "shard_1",
                "US": "shard_2",
            }
            obj_location = self.location
            using_db = database_mapper.get(str(obj_location), "default")
            kwargs["using"] = using_db
            super().save(*args, **kwargs)
    

KO user => shard_1
US user => shard_2

 

Read

 

  • 유저 목록을 불러오는 상황( 유저 리스트 / GET )
  • queryparam 로 location(”이용지역”) 필드 값을 입력받는다.
    • TO-BE : KO 유저 는 shard_1 DB에, US 유저는 shard_2 DB를 이용하며 데이터를 가져와야한다.
  • solution
    • db_for_read 함수에서 데이터 접근시 (ex: location이 US인 유저 조회) 시 US 의 유저가 담긴 shard_2 의 DB 연결을 시도.
    • Router 클래스의 함수에서 무슨 짓을 해도 request(query문)에 접근이 안됨.( 내가 못한 걸수도..)
    • 어떤 app과 model 로 요청을 보냈는지는 tracking이 가능했다.
      • 결국 app, model 단에서 db가 분기되는 로직에서 활용이 가능할 것 같다.
        • 예를 들어서, 한국서비스에서는 A기능( A app or A model)을 사용하고 미국 서비스에서는 B기능( B app or B model)을 사용할 경우 분기가 가능하다. 추후 테스트.
    • view에서 queryparam에 따라 using db를 다르게 변경하도록 mapping 함으로 해결되었다.
class UserViewSet(
    mixins.ListModelMixin,
    GenericViewSet,
):

    queryset = User.objects.all()
    filter_class = UserFilter

    def get_queryset(self):
        queryset = super().get_queryset()
        location_params = self.request.GET.get("location")
        database_mapper = {
            "KO": "shard_1",
            "US": "shard_2",
        }
        using_db = database_mapper.get(location_params, "default")
        return queryset.using(using_db)

 

 

reference