데이터 베이스 샤딩 혹은 샤드라고도 하며 데이터 베이스를 구축함에 있어서 사용되는 기법을 알게 되어 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)
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)을 사용할 경우 분기가 가능하다. 추후 테스트.
- 결국 app, model 단에서 db가 분기되는 로직에서 활용이 가능할 것 같다.
- 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
'Develop' 카테고리의 다른 글
[Django] Admin에서 JS를 활용해 동적으로 연관 필드 업데이트하기(데이터 동적 변경) (2) | 2023.08.19 |
---|---|
[AWS Route53] 도메인 구매 후 route53에 연결하기 (https 연결, ACM, Routing Policy, 기존에 사용 중인 서브도메인을 활용) (0) | 2023.08.10 |
Pycurl Error : Celery와 SQS 구축 시 OpenSSL 이해하고 설정하기 [Celery, AWS SQS] (0) | 2023.04.23 |
Python 배포 : Python Image 경량화로 배포 속도 향상시키기(Docker Image) (0) | 2023.04.04 |
[AWS] Root Account - IAM Account | 계정과 보안 개념 (0) | 2023.03.12 |