서버 최적화 & 데이터 마이그레이션 작업 - 2 : API 서버 재구축

최민길(Gale)·2022년 12월 30일
1

안녕하세요ㅎㅎ 이전 게시글에 이어서 API 서버를 재구축 작업을 진행해보려고 합니다. 기존 API 서버의 경우 DB 서버와 합쳐져 있어 서버 스펙을 변경하는데 있어서 DB 서버에 종속적이었습니다. 하지만 이번에 DB 서버를 분리하게 되면서 서버 스펙을 독립적으로 변경할 수 있어 더 효율적인 서버 운영이 가능해질 것이라고 기대하고 있습니다ㅎㅎ

우선 EC2에서 새로운 서버를 구축했습니다. 기존 서버의 경우 CPU 8코어에 RAM 16G 모델을 사용했는데 비용이 상당히 많이 발생하다보니(한 달에 약 60만원..) NGINX의 특성 상 메모리를 많이 잡아먹지 않으며 CPU 코어 수에 따라 Worker Process를 할당받아 요청을 처리하는 것이 효율적이다 보니 CPU 2코어에 1G RAM의 스펙으로 서버를 구축했습니다.

앞서 구축한 DB 서버와의 연동을 위해 RDS 보안 그룹에 구축한 EC2의 IP 주소를 추가했습니다. 이 때 퍼블릭 IP 대신 같은 VPC 내부에서 통신하는데 사용되는 프라이빗 IP 주소를 추가해야 정상적으로 작동합니다.

이미지 참고 : https://server-talk.tistory.com/308

저는 PHP로 API 서버를 구축했습니다. PHP로 API 서버를 구축하기 위해선 NGINX와 함께 FastCGI를 이용한 통신을 진행해야 합니다. 기존 웹 서버의 경우 정적 데이터를 처리합니다. 하지만 요청된 데이터가 정적 데이터가 아니라 PHP와 같은 동적 데이터의 경우 요청에 들어있는 주소를 확인하여 주소와 매칭되는 프로그램이 있을 경우 해당 프로그램으로 데이터를 넘겨 처리하고 그 결과를 웹 서버를 통해 반환하는 프로세스를 거칩니다. 이 때 웹 서버와 애플리케이션 간의 통신을 진행하는 인터페이스를 CGI(Common Gateway Interface)라고 합니다.

하지만 CGI는 매우 큰 단점이 있는데, 바로 요청이 들어올 때마다 프로세스를 생성한다는 작동 방식입니다. 초기 아파치에서도 위와 같은 문제가 발생했는데, 동시에 많은 요청이 발생할 경우 생성된 프로세스 간 컨텍스트 스위칭이 많이 발생하게 되어 이로 인한 오버헤드로 인해 성능 저하 문제가 발생합니다.

따라서 이런 단점을 개선하기 위해 FastCGI가 도입되었습니다. FastCGI는 요청이 들어올 때마다 프로세스를 생성하는 방식 대신, NGINX처럼 하나의 프로세스로 여러 요청을 처리하여 동시 요청이 많아져도 프로세스가 많아짐에 따라 발생하는 컨텍스트 스위칭이 적게 발생하여 성능 저하 문제를 해결한 방식입니다.

FastCGI 설정은 NGINX에서 진행합니다. NGINX를 설치하면 Ubuntu 20.04 버전 기준 /etc/nginx/nginx.conf 파일에서 관련 설정을 추가할 수 있으며 디폴트 설정값은 /etc/nginx/sites-avilable/default 파일에 존재합니다. 위의 코드는 default 파일에 존재하는 코드이며 다음과 같이 FastCGI 설정 코드가 주석 처리가 되어있습니다.

PHP에서 FastCGI를 통해 데이터를 넘겨줄 수 있는 방식은 크게 unix 소켓을 이용한 통신 방식과 tcp 소켓을 이용한 통신 방식이 있습니다. 우선 소켓 통신 방식의 경우 HTTP 통신과 달리 커넥션을 계속 유지하고 있어 잦은 데이터 통신 시 더 효율적인 방식입니다. (HTTP 통신 방식의 경우 Keep Alive 설정을 통해 커넥션을 더 길게 유지할 순 있습니다.) tcp 소켓 방식은 TCP 프로토콜을 사용하여 네트워크 상에서 처리되는 반면, unix 소켓 방식은 하나의 물리적 서버 내부의 프로세스들끼리 통신하며 네트워킹을 위한 라우팅 작업이 필요 없어 더 빠르게 데이터를 처리할 수 있습니다. 현재 투다 서비스의 경우 웹 서버와 PHP 서버의 분리가 필요할 정도로 리퀘스트가 많지 않아서 unix 소켓을 이용한 통신 방식을 체택했습니다.

이미지 참고 : https://pronist.dev/72

투다의 성능 향상을 위해 JIT 컴파일러를 도입한 PHP 8.0 버전을 설치했습니다. (2022.12.30 기준 8.2 버전까지 출시되었으나 기존 코드 중 deprecated된 부분이 많아 기존 코드 버전인 8.0 버전으로 설치했습니다.) PHP의 경우 인터프리터 언어로서 일반적인 인터프리터 언어의 동작 방식과 유사합니다. 우선 코드를 가져와 토큰화를 진행한 후 이를 추상 구문 트리로 변환합니다. 변환된 추상 구문 트리를 Zend VM에서 리프 노드에서 루트 노드까지 이동하면서 해석하는 방식으로 로직을 처리합니다. PHP 5.5 버전 이후로는 추상 구문 트리를 바이트 코드로 변환한 후 이를 Opcache라는 캐시에 저장하여 같은 코드를 재실행 시 추상 구문 트리 생성 프로세스를 반복하지 않아 더 빠르게 실행이 가능했습니다.

하지만 여전히 Zend VM을 거쳐서 실행되어 성능의 손해가 발생하는데, 이를 JIT 컴파일러를 통해 해결할 수 있습니다. JIT 컴파일러는 런타임 중 바이트 코드를 기계어로 바꾼 다음 Opcache에 추가합니다. 이를 통해 바이트 코드를 VM에서 별도로 실행하지 않고 바로 명령을 처리할 수 있기 때문에 기존 방식에 비해 2배 이상 더 빠른 처리 속도를 올릴 수 있습니다.

JIT 컴파일러 설정은 php.ini 파일에서 진행합니다. Ubuntu 20.04 버전 기준 /etc/php/8.0/fpm/php.ini 경로에 위치해있으며 이 파일에서 JIT 컴파일러 뿐만 아니라 PHP의 여러 설정값들을 변경할 수 있습니다. 저는 date.timezone 값을 Asia/Seoul로 변경하여 시간값을 동기화시켰고 memory_limit 값을 512M로 설정하여 PHP의 메모리 사용 제한을 높혔습니다. 또한 Opcache와 JIT 컴파일러 설정 및 메모리 할당값도 조정하여 설정을 완료했습니다.

데이터 통신 보안을 위한 HTTPS 설정 이후 PHP 코드의 업데이트가 발생할 경우 배포 과정에서 발생하는 딜레이를 해결함과 동시에 많은 요청을 효과적으로 처리하기 위해 2대의 EC2로 나누어 NGINX 리버스 프록시를 이용하여 로드밸런싱을 진행하였습니다. 위에서 구축한 서버 스펙으로 하나의 EC2를 추가 구축한 후 각각의 IP 주소를 Upstream을 통해 설정한 후 이 값을 proxy pass하여 서버로 오는 요청을 2개의 EC2로 나누어 처리합니다. 배포 시에는 Upstream 안의 배포할 서버의 IP를 주석 처리한 후 배포를 진행하며 배포 완료 시 주석을 해제하는 방식으로 사용자들이 서비스를 연속적으로 이용할 수 있는 시스템을 구축하였습니다.

이상으로 데이터 마이그레이션 작업을 진행하면서 같이 병행한 API 서버 재구축 작업에 대한 포스팅이었습니다. 현재는 PHP로 서버를 운영하고 있지만 추후 Spring boot를 이용한 API 서버로 마이그레이션하는 것도 계획 중에 있어서 이 부분도 추후 포스팅 진행하도록 할게요. 긴 글 읽어주셔서 감사합니다!

profile
저는 상황에 맞는 최적의 솔루션을 깊고 정확한 개념의 이해를 통한 다양한 방식으로 해결해오면서 지난 3년 동안 신규 서비스를 20만 회원 서비스로 성장시킨 Software Developer 최민길입니다.

0개의 댓글