API 서버 성능 수치화를 위해 K6 테스트를 진행하였다. 수치화하기 전까지는 몰랐던 문제가 로드 테스트를 위해 수면 위로 무수히 올라오기 시작했다. 올라오기 보다는, 로드 테스트가 배드 스멜 센서가 된 느낌이 강하지만.
그냥 좋다니까 쓰는 프레임워크, 라이브러리, 서비스들이 실제로 성능이 어떠한 지에 대해서는 신경을 크게 안 썼던 것 같다. 그냥, 돌아만 가면 되니까.
서버 성능 수치화를 통해 어떤 부분들이 성능을 저해하는가? 어떻게 해야 성능을 개선할 수 있는가? 에 대해서 고민하게 만들었고 실제로 이를 통해 그 동안 잠복하고 있었던 문제를 다수 찾아낼 수 있었다. Node.js 특성을 살린 서버에 대해서도 생각해보게 되었고. (이벤트 루프 측면에서)
로드 테스트가 서버의 성능 자체를 객관적으로, 그리고 실제 유저들의 예상 수용량을 정확하게 가늠하기 위해 쓴다- 라는 생각은 여전히 안 든다. 실제 유저 플로우와 유사한 로드 테스트는 작성하기 어렵고, 가벼운 DB 쿼리 API와 엄청나게 무거운, CPU 비중이 높은 API 는 현재 측정했을 때는 거의 50배 가까이 차이나기 때문에, ‘일반적으로 1vCPU 서버 한대로 500 명 정도는 버틸 수 있어요~’ 라고 말해도 악당 10명이 서버를 죽게 만들 수 있어서 섣불리 테스트만으로 단언하기는 어렵다.
그럼에도 불구하고 로드 테스트를 반드시 작성해야 되는 이유는 에버리지 케이스에 대한 대략적인 가늠은 할 수 있게 해주고, 현재 서버가 비교적 높은 부하에서 효율적으로 동작하는지, 아니면 1초에 일반적인 API 몇 개도 제대로 못 다루는 비효율 덩어리인지 판단할 수 있는 지표가 된다.
테스트를 처음 하고 서버 퍼포먼스가 나쁘다는걸 알았을 때는 정말 죽고 싶은 심정이었지만 그만큼 그걸 개선했을 때의 기쁨도 컸다. 단순히 말해 내 퍼포먼스가 숫자로, 눈에 보이니까 재밌긴 하다.
Worst Case 강박증; 매사에 Worst Case만 생각함
대학 생활 동안 알고리즘만 주구장창 하면서 생긴 문제점인데, 항상 Worst Case를 생각하려고 한다. 물론 Worst Case를 생각하는 건 좋은 점도 있다고 생각하는데, 이번주 프로젝트를 진행하면서 장점보다는 단점이 좀 많이 드러난 것 같다.
위에서 말했듯 API 서버에서도 고밀도로 와도 괜찮은 API와 저밀도로 와도 위험한 API가 있는데, 현 프로젝트의 경우 대부분의 DB 쿼리는 인덱스를 이용하고 explain으로 인덱스 스캔을 직접 확인하면서 튜닝을 해서 의외로 가벼운 편이다.
무거운 API의 대표 주자가 인증 관련인데, DB에 비밀번호를 평문 그대로 저장할 수는 없기 때문에, DB 가 탈취당하더라도 비밀번호 평문이 노출되지 않도록 하기 위해 단방향 암호화를 하게 된다. 그렇다고 해서 이 해시 연산이 지나치게 효율적이면 딕셔너리 어택 등 컴퓨팅 파워를 앞세운 기법들에 의해서도 비밀번호 평문이 역추론될 수 있기 때문에 어느 정도로 무거워야 한다. 찾아보니 보통 연산 한번에 250ms 정도를 요구한다.
보안을 위해 250ms를 쓴다, 그것까지는 이해하고 있었는데 로드 테스트를 하면서 이 로그인 한번이 테스트 결과를 아예 박살을 내놓으면서 좀 생각이 바뀌기 시작했다. 분명 평균적인 케이스에선 300명까지도 수용되던 서버가 악당 단 4명의 로그인 신공에 그대로 독점당하는 것이다.
뇌에서 Worst Case 경보가 울리기 시작했다. 삐용삐용, ‘너희 프로젝트는 별 다른 공격 수단도 안 갖춘 4명한테 죽을 수 있는 서버입니다.’
단순히 생각해서 프로덕션 데모 발표 때 캠퍼 100명만 들어와도 이들이 순차 로그인을 하는데 25초가 걸린다. 동시에 우다다 한다면 가장 오래 기다리는 사람은 로그인 클릭하고 25초 동안 멍때리게 된다. 기다리면 다행이지만 왜 안 되지 하고 로그인 버튼을 연타한다면? 정신이 아득해진다.
그렇다고 패스워드 보안을 희생하고 해시 라운드를 낮출 수도 없는 노릇이라 가불기에 걸려서 바로 뇌가 정지됐다. 쥐 같은 동물이 상위 포식자를 만나면 그대로 얼어 붙고, 사람은 정말 어찌할 수 없는 재난에 맞닥뜨리면 사고가 정지되거나 미쳐버리지 않나? 딱 그런 케이스였다. Worst Case가 계속 뇌를 떠나질 않아서 개발 퍼포먼스가 내내 저하됐다.
그리고 마스터 클래스에서 그대로 질문을 했었는데 의외로 답은 심플했다. ‘서버를 더 많이 사세요’. 그렇다, 돈으로 해결이 안 되는 문제란 없다.
이런 Worst Case 에 대한 걱정이 아니더라도 그냥 이번 주는 개발을 하면서 있을 수 있는 최악의 시나리오를 겪은 것 같다. NCP에서 오토 스케일링을 지원하는 것을 보고 가볍게 테스트해보고, 되겠다 싶어서 뛰어들은 컨테이너화. K8s 보다는 러닝 커브가 낮다는 도커 스웜. 실제로 도입을 해보니 정말 별의 별 문제가 연달아서 계속 터졌다. 문제를 치워도 새로운 파생 문제가 2개씩 생기고, 그걸 또 치웠더니 4개씩 생기고. 아.. NCP는 모니터링을 켰는데도 누락되는 서버가 있고, 오토 스케일링 반응도 이랬다가 저랬다가 하고, 도커 스웜도 쉽다고 해서 들어갔더니 마냥 쉽다기 보다는 ‘애초에 이걸 가지고 할 수 있는게 많지가 않아서 할게 없다’ 에 가까웠다. 로드 밸런싱 설정도 못 다뤄, 서비스로 만들고 업데이트할 때는 환경 변수 세팅도 못해, 업데이트 한정 헬스 체크도 못 하고, YAML 스펙은 도커 컴포즈보다 옛날인지 제약도 많았고, 사람들이 왜 K8s 를 쓰는지 단번에 이해가 됐다.
일을 하면서 파생되는 문제도 많은데 동시에 처리해야 되는 일의 폭도 넓었다. NCP 오토 스케일링 - 개별 서비스의 도커 컨테이너화 - 도커 컴포즈를 통한 로컬 테스트 - 도커 스웜을 통한 - K6 테스트 - 임시 테스트로 드러난 비효율 코드 개선 - NGINX - … 하필이면 이 기능들이 다 서로 연결되어 있어서 어느 하나를 먼저 다 끝내고 진입을 할 수도 없어서 이 많은 주제들을 계속 번갈아가면서 다루느라 컨텍스트 스위치 비용도 엄청났고, 그러면서 각 주제별로 파생된 문제들은 또 많고(처음 써봐서 그런 것도 있지만), 파생된 문제들 중은 시간을 엄청나게 잡아먹으면서 결국 결론은 ‘지금은 해결 못함’인 케이스도 있었고(대표적으로 NGINX의 프록시 캐시, 시간 들여 적용했더니 캐시 PURGE가 유료 기능인걸 마지막에서야 알았고, 이걸 대체하기 위한 다른 방법들-예를 들어 NGINX 소스 코드를 이용한 커뮤니티 캐시 퍼지 모듈 빌드하여 탑재-을 시간 더 들여 시도해봤지만 2022년 말에 유효한 선택지는 아무것도 없었다, 그리고 윈도우가 도커 스웜 기능을 멀쩡히 쓸 수 있는게 없는데 경고도 안 띄워줘서 여기에도 시간 무진장 날려먹었고), 개발 효율은 바닥을 찍고, Worst Case가 뇌를 점거하고, 일정이 지연되어서 https와 리팩토링, 테스트 코드도 다음 주로 밀리고, clinic은 손도 못 대고. 정말 최악의 한주였다.