최근 회사에서 하고 있는 작은 프로젝트에 시도해본 실험의 경험이 유의미한 것 같아서 한줄 적어보려고 한다.
작은 개발 회사에서는 보통 서버 운영비를 낮추면서도 일정 수준 이상의 퍼포먼스를 내야한다는 요구를 하는 경우가 많다.
하지만 대부분 그런 지식은 단기간에 습득이 되지 않고, 일정 수준 이상의 트래픽을 경험해야 깊은 고민을 하면서 성장통을 겪으며 성장을 하게 되는 것 같다.
실제 배포 환경과의 유사한 상황을 연출하기 위해서 docker 컨테이너에 리소스 사용 제한을 걸어서 CPU 속도를 매우 낮게 사용되도록 제한 두고 테스트를 진행
사용자가 수천개의 Row를 가진 엑셀 파일을 업로드 하게 되면, 기본기 조합을 가지는 두개의 필드 값을 기준으로 DB에 중복이 있는 것은 UPDATE 쿼리를 중복이 없는 새로운 데이터라면 INSERT 쿼리를 하는 서비스 로직
이러한 하나의 API Call이 있다.
문제 발생 지점은 LOCK 된 테이블 상관 없이 모든 데이터 변조 쿼리(INSERT, UPDATE, DELETE)가 있는 서비스 로직에 API Call을 날리게 되면 앞에 있던 서비스 동작이 완료 되기 전까지 다음 API Call이 동작하지 않고 있다.
2번 문제를 생각하게 된 이유를 설명해보자면, 평소에 전혀 사용하지 않았던 Go로 백엔드 프로젝트를 해보자고 생각이 들어서 진행을 하다가 다른 언어 단어에서 지원하던 멀티 프로세싱을 지원하는 프레임워크를 쓰다가 단일 프로세스만 지원하는 프레임워크로 옮기고 코드를 작성한지 1일차가 되던 시절이었기 때문에 프로세스 갯수에 대한 문제를 처음 떠올리게 되었다.
그래서 찾아보다가 Gin(Go의 Web FrameWork)이 싱글 프로세스만 동작을 지원한다는걸 알게 되어서 Load Balancer를 설정해서 여러 프로세스를 라운드 로빈 방식으로 묶어서 서빙하면 단일 엔드포인트로 묶어서 할 수 있으니 큰 문제가 없을거라 생각하고 NGINX 설정을 간단히 교체해서 배포
upstream app {
server api-1:8080;
server api-2:8080;
server api-3:8080;
}
server {
listen 8080;
server_name localhost;
location / {
proxy_pass http://app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
}
파일 업로드 후 INSERT 쿼리가 많은 서비스 로직 Call 하고 postman runner로 다른 엔드포인트 부하를 주니 처음엔 잘 되는 것 처럼 보였으나, 결국 실제 INSERT 쿼리가 왕창 들어가는 시점부터 같은 문제 현상이 발견 되어서 이 방법 역시 아니었다는 것을 깨우치게 되었다.
아 이게 특정 업스트림에 Health Check 하는 API 엔드포인트를 만들어서 거기에 timeout이 뜨면 다음 서버로 Call 요청을 넘기게 하면 되지 않을까? 하는 생각에 NGINX PLUS에서만 지원하는 health check 기능을 별도의 모듈을 다시 넣어서 NGINX를 재빌드하는 과정을 거쳐보았다.
그닥 어렵지가 않아서 모듈 설치한 도커 NGINX 이미지를 가지고 배포를 다시 했더니 동작이 되나 싶었더니 동일한 문제가 다시 발생
점점 판이 커지는 기분인데 예전에 혼자서 실험했던 MySQL 가지고 Master & Slave(예전 방식 이름)을 Docker-compose로 관리하는 것을 만들었는데 Slave 구성을 자동으로 하는 것을 실패했고, 이건 여전히 자동으로 구성 되게 하는 방법은 없는 것으로 알고 있음.
복제본을 만들어서 구성을 하면 해결 되는 거지 않을까? 하는 찰나의 짧디 짧은 생각을 하고 빠르게 시스템 구축을 하고 난 뒤 Master에 들어온 데이터가 바로 Slave에 복제 되어서 가는 것을 보고 한번에 잘못 되었음을 감지했다. '지금 내가 조회가 느려서 이 짓을 한게 아닌데...'
데이터베이스 스케일 아웃을 하더라도 결국 Writer는 하나인 구조이기 때문에 백날 천날 DB 수를 10만대를 늘린다고 해서 지금 겪고 있는 이 문제를 해결하는데에는 아무런 도움이 되지 않는 것을 깨달았다.
4번의 큰 변화를 줬음에도 불구하고 연속 된 실패를 맛을 보면서도 남은 방안들을 계속 생각하던 와중에 SQL Browser로 다른 INSERT 쿼리를 날려보는데 데기큐 같은 곳에 쌓인걸 보고 순간적으로 아!
라는 단어가 떠오름과 동시에 지금까지 했던 모든 행동들이 머리 속을 지나가면서 한대씩 치고 갔다.
SQL 쿼리 문들을 자세히 보니 서로 접점이 전혀 없는 테이블들이 있어서 이들을 아예 분리를 해서 서로 다른 DBMS에 넣어서 하면 정상 작동이 될거다 라는 확신을 가지고 곧바로 진행을 했다.
드디어 성공!
어차피 성공한 김에 긴가 민가 하는 생각을 가지고 마지막 실험을 시도해봤다.
DBMS를 두개를 만들지 않고 한 DBMS에서 그냥 CREATE DATABASE
를 두개를 해서 Meta DB와 Data DB를 구성해도 SQL 대기큐에는 전혀 영향을 주지 않을거란 생각에 시도했더니
이것도 성공!
사용할 수 있는 디비 자원이 열악하고 DML(Data Manipulation Language) 쿼리가 동시에 엄청 많이 발생할 수 있는 구조라면 DATABASE를 하나 더 만들어서 사용하면 문제가 해결 될 것이다.
여기서 올 수 있는 사이드 이펙트로는 ORM에서 여러 데이터베이스에 대한 처리가 되어야 한다는 전재 조건이다. 물론 이 또한 해결할 수 있는 방법을 API 서버 패키지를 두개 만들어서 분리해서 작업하는 수 밖에 없다.