Situation
이전 에피소드에서 언급하였듯 이 마이그레이션의 성능 향상은 어느 정도 담보가 되었기에 최대한 에러 없이 API를 옮기는 것에 집중하였습니다. 에러 없이 옮기기 위해서는 API Spec이 있는 것이 가장 좋았지만, 그것이 일단 없었고요. 프런트에서 사용하는 타입에 대한 정의가 있으면 좋았겠지만 JS와 TS가 산재되어있고 관리가 잘 되어있지 않은지 오래인지라, 네트워크 탭을 눌러서 하나씩 까보면서 하거나, Django 코드를 보고 추리하면서 했어야했습니다. Django 코드에서도 pydantic 과 같은 Typing이 전혀 없어서 꼼꼼히 보고 옮기더라도 잠재적인 에러의 위험성이 있었습니다. 이에 실제 production 환경에서의 트래픽으로 직접 테스트를 해서 에러율을 보고 배포를 하고 싶었습니다.
Task
아래 테크 블로그 글을 참고하였고 같은 방식으로 API 응답을 비교하고자 하였습니다.
이를 구현하기 위해선 다음과 같은 Task를 수행했어야했습니다.
•
클라이언트 트래픽 복제
•
API Request, Response 로깅
•
로그 모니터링 시스템 구축
Action
API Request, Response 로깅
우선 NestJS와 Django에 API Request, Response 로그 모듈을 다음과 같이 구현하였습니다.
NestJS의 경우 Winston으로 구현하였습니다.
NestJS의 경우에도 로깅 미들웨어와 winston으로 구현하였습니다.
Django에는 로깅 미들웨어를 사용하여 구현하였습니다.
코드
Nginx를 이용한 트래픽 복제
이후 저희 OTL의 전반적인 Architecture는 다음과 같았습니다.
이 아키텍처에서 저는 EC2 내부의 nginx에서 트래픽을 복제하고자 했습니다.
찾아보니 nginx mirror directive가 있어서, 이를 활용하여 개발 서버에 떠있는 새로 만들어진 NestJS 서버로 요청을 보내고자 하였습니다. (
Module ngx_http_mirror_module)
이 과정에서 사소한 문제가 있었는데요. 바로 NestJS의 인증방식은 토큰인 반면 Django의 인증 방식은 session 기반 방식이었던 것입니다. 로그인 로직은 그래서 테스트를 못하더라도, 인증이 필요한 다른 정보들의 경우 Django에서 일단 인증을 거치면 NestJS에서도 인증을 거칠 수 있도록 수정해야했습니다.
Django는 session_id 쿠키에 session_key를 담아서 보내주고 있었는데요. 이것은 DB에 저장하고 있었습니다. 따라서 NestJS에서도 쿠키에서 session_key를 꺼낸 다음 base64decode를 진행한 후, user를 식별하는 id를 찾아서 꺼내주는 식으로 진행하였습니다.
저희 NestJS 서버는 당시에 AuthChain 형식으로 쉽게 인증 로직을 추가할 수 있었기 때문에, 이러한 구현은 손쉽게 추가할 수 있었습니다. (참고: 🫢OTL 인증 로직 개선기)
현재는 위 아키텍처와 많이 변경되었으며, 리소스들은 전부 스팍스 내부의 물리서버로 이전되었습니다.
하지만 테스트를 진행하던 와중 치명적인 문제가 하나 있었는데요. nginx mirror directive를 이용해 테스트 서버를 배포하여 요청/응답 로깅하고 있었는데 실제 프러덕션 서버의 응답속도가 평소보다 더 느려지는 경우가 종종 있었습니다. 이유를 확인하기 위해 nginx 로그를 확인한 결과 타임아웃 문제였습니다.
타임아웃 문제가 평소에 생기지 않는 엔드포인트였기에, 우선적으로 NestJS 테스트 서버를 곧바로 중단시켰습니다. 그러자 타임아웃 문제가 해결되었습니다. 로컬에서 테스트를 해본 결과, mirror directive는 mirror 모듈에서 응답한 response를 drop하는 것은 맞지만 보낸 요청을 안 기다리지는 않았습니다. 따라서 production 보다 사양이 좋지 않던 dev환경의 컴퓨팅 파워로는 응답속도가 길어지면서 타임아웃이 났던 것입니다.
이에 이 방법은 실패로 돌아갔습니다.
두 번째로 시도한 것은 아래와 같은 teeproxy를 이용한 방법입니다.
teeproxy는 Go로 만들어진 트래픽 복제를 위한 라이브러리입니다.
이를 테스트해본 결과 nginx mirror와 같은 timeout 문제는 없었습니다. 이에 production 환경에 실행해봤으나… Django 웹앱에서 같이 돌리기에는 문제가 있었습니다. 트래픽을 복제하는데 EC2의 CPU사용량을 많이 써버려서 부하가 많이 늘어버렸기 때문입니다. 이렇게 두 번째 방법도 실패로 돌아갔습니다.
Result
저희는 9월 추석 시즌에 배포를 계획했었지만, 저 2번의 시도가 모두 실패로 돌아가고 난 이후는 이미 8월 말이었습니다. 계산 상 2주간의 빡센 QA를 한 번 더 하는 것이 맞을지 아니면 이 작업에 시간을 더 쓰는 것이 맞을지 고민한 결과, 이 작업을 하느라 QA 기회를 잃어버리는 것이 더 큰 기회비용이라 생각하였고 결국 구현하지 못했습니다.
이 아키텍처는 성공한다면 같은 방법으로 안정적인 테스트/배포/AB-test를 있었기에 더욱 아쉬움이 남습니다. 이번 실패를 계기로 하나의 어플리케이션 레벨이 아니라 시스템 아키텍처링/인프라에도 확장된 지식과 경험을 얻을 수 있었던 점이 배움으로써 남습니다.
Reflection
해당 구현의 실패 이후, 실패한 이유를 구체적으로 나누어서 살펴보았습니다. 물론 표면적인 이유는 ‘배포까지의 시간이 촉박함’이겠지만, 이는 현상일 뿐 본질적인 문제는 아니라고 생각하였습니다.
•
실력 부족
◦
만약 트래픽 복제가 성공했다고 하더라도, 모니터링 시스템을 구축하는데에 상당한 시간이 들었을 것입니다. 이에 모니터링 시스템 구축까지 성공적으로 마친 상태에서 배포까지 충분한 검증 데이터를 모으지 못했을 것입니다. 백엔드 개발자로서 인프라 영역에 숙련되있는 것이 얼마나 중요한지, 그리고 그것이 실제 개발에 어떻게 응용될 수 있는지를 알 수 있는 시간이었습니다.
•
문제 해결력
◦
일단은 nginx의 mirror directive가 안 된다는 점을 깨닫고 나서는 바로 다른 방법을 서치하기 바빴던 것 같습니다. 지나고 보면 nginx를 하나 더 놓음으로써 가능한 부분이 아니었나 생각이 됩니다. 예를 들어 기존에 있는 것을 제외하고 추가적으로 배치할 nginx를 secondary nginx라고 했을 때, 이 nginx는 어떤 응답이든 static한 200응답을 반환하도록 하고, prouction에서 사용되고 있는 nginx에서는 mirror directive로 secondary nginx로 트래픽을 보낸다면 그 의존성을 완전히 끊을 수 있지 않았나 싶습니다.
◦
좀 더 다양한 소스를 찾아보면 좋았을 것 같습니다. teeproxy 외에도 다음과 같이 훨씬 유명한 오픈 소스 라이브러리가 있었습니다.
goreplay

◦
뿐만 아니라 AWS 단에서도 이미 트래픽 미러링에 대한 기술 블로그가 존재했습니다. 저희와 같은 온프레미스가 아닌 클라우드 환경이었다면 사용하기 좋은 방법인 것 같습니다.