기존 아키텍처
동적 스케일 프로젝트를 적용하고 난 후의 아키텍처는 다음과 같습니다.
동적 스케일 프로젝트를 하면서 여러 문제를 해결했지만 여전히 킥보드는 고정된 Relay에 연결된다는 문제가 존재합니다. 이를 해결하려는 프로젝트가 바로 동적 할당 프로젝트입니다.
문제점
현재의 아키텍처는 다음과 같은 문제를 가집니다.
만약에 Relay C가 실행되고 있는 EC2에 문제가 생기게 된다면 11114 킥보드는 사용할 수 없게 됩니다. 11114 킥보드는 Relay C에만 연결될 수 있고 Relay C도 사용할 수 없기 때문에 11114 킥보드를 다른 Relay에 연결하는 것도 불가능합니다. 즉, Relay C가 복구되기 전까지 11114 킥보드는 사용할 수 없습니다.
Relay에 킥보드가 천 대 정도 연결되어 있다고 가정하면 Relay에 문제가 생겼을 때 천 대 정도의 킥보드를 사용할 수 없게 됩니다.
새로운 Relay 서버인 D를 생성한다면, 이 Relay에 킥보드를 연결할 수 있을까요? 새로운 Relay 서버를 만들더라도, 해당 서버의 IP를 가진 킥보드가 없기 때문에, 킥보드의 서버를 옮겨주지 않으면 Relay를 추가해도 킥보드와 연결될 수 없습니다.
즉, 위에서 다룬 문제점을 정리하면 아래와 같습니다.
•
Relay 서버에 문제가 생기면 그 Relay에 연결되어 있던 모든 킥보드를 이용할 수 없게 됩니다
•
Relay 서버를 동적으로 추가하더라도, 반드시 수동으로 킥보드의 서버를 이전해야 합니다
•
Relay 서버를 제거하기 위해서는 그 Relay 서버의 IP를 가지는 모든 킥보드의 서버를 이전해야만 합니다
이러한 문제를 해결하기 위해서 동적 할당 프로젝트를 시작하게 되었습니다.
동적 할당 프로젝트
위 문제점을 해결하는 방법은 굉장히 간단합니다. 사용자에게 웹 서버를 제공할 때 ALB와 ASG를 두는 것처럼 Relay 서버도 LB를 앞에 두어 킥보드가 LB를 바라보도록 하면 됩니다. 이런 TCP Load Balancing을 제공해주는 AWS 서비스가 바로 AWS NLB입니다.
NLB
NLB는 OIS 계층에서 4번째 계층에서 작동하는 LB로 ALB보다 높은 성능을 보장하며 TCP 트래픽에 대한 로드 밸런싱을 지원합니다. 또한 ALB와는 다르게 고정 IP를 지원합니다.
처음에는 NLB가 TCP 트래픽에 대한 로드 밸런싱을 지원하므로 아래와 같은 구조를 생각했습니다.
킥보드에게 NLB에 대한 도메인을 지정하자 Relay가 동적으로 생성되거나 제거되어도 잘 작동하는 것을 알 수 있었습니다. 그러나 테스트를 진행하다가 NLB의 Target Group에 문제가 있는 것을 발견하게 되었습니다. Relay 서버가 뜨고 Health Check도 정상적으로 되는데도 불구하고 Target Group에는 굉장히 느리게 등록되는 문제가 있었습니다. 공식 문서와도 다른 동작에 이상하다 생각해 다른 사례들을 검색해보았고 아래와 같은 이슈를 발견하게 되었습니다.
5년 전에 이미 제기된 이슈임에도 불구하고 아직까지 수정되지 않는 것을 보고 당분간 고쳐지지 않을 것으로 보였습니다. 동적 할당 프로젝트를 시작한 가장 큰 이유 중 하나는 안정성입니다. Relay 서버에 문제가 생겨도 개발자의 수동 대처 없이 자동으로 복구되도록 하는 것이 큰 목표 중 하나였으므로 NLB만을 사용하는 솔루션을 채택할 수는 없었습니다. 그래서 다른 솔루션을 찾아보기 시작했습니다.
HAProxy + NLB
HAProxy는 2001년에 나온 소프트웨어로 하드웨어 스위치를 대신하는 소프트웨어 로드 밸런서입니다. 네트워크 스위치에서 제공하는 L4, L7 기능과 로드 밸런싱 기능을 제공합니다.
HAProxy와 SRV 레코드를 연동하면 IP와 포트를 모두 지정할 수 있으므로, Relay를 ECS Task로 띄우고 AWS ECS에서 제공하는 기능을 사용하여 AWS Cloud Map에 ECS Task의 IP와 포트를 SRV 레코드로 등록하면 HAProxy와 ECS Task를 연결할 수 있을 것이라 생각했습니다.
ECS와 Cloud Map을 통합하면, ECS Task가 새로 실행되었을 때 Cloud Map을 통해 Route 53의 SRV 레코드에 ECS Task의 IP와 포트가 추가됩니다. HAProxy에서 SRV 레코드로부터 백엔드를 구성하면 HAProxy를 이용해 ECS Task를 로드밸런싱 할 수 있게 됩니다.
다만, HAProxy가 죽게 되면 여전히 킥보드를 이용할 수 없게 되는 것은 똑같으므로 HAProxy를 여러 대 구성하고 이를 NLB로 묶어서 가용성을 높였습니다. 이를 정리하면 아래와 같습니다.
킥보드는 NLB의 도메인으로 통신을 시도합니다. NLB는 킥보드의 요청을 받아 ECS로 구성된 HAProxy 중 하나에게 요청을 전달합니다. HAProxy는 미리 받아둔 SRV 레코드로 구성된 Relay 백엔드 서버 중 하나를 골라 요청을 전달하며, 결과적으로 킥보드와 Relay는 연결됩니다.
위에서 언급한 NLB의 Target Group 이슈는 여전히 존재하지만 동적으로 조절되는 Relay와는 달리 HAProxy는 Relay보다 성능에 있어서 더욱 여유롭기도 하고 Relay처럼 코드를 수정하고 배포하는 것이 아니기 때문에 HAProxy를 새로 추가하는 일이 매우 드뭅니다. 또한 직접 개발한 Relay와는 달리 HAProxy는 오랫동안 검증받아온 소프트웨어로 Relay보다 훨씬 안정적이라 할 수 있습니다. 따라서 해당 이슈가 HAProxy에게는 큰 문제가 되지 않는다고 생각했습니다.
NLB vs HAProxy + NLB
NLB만 사용하는 것이 아니라 HAProxy를 함께 사용할 때 발생하는 단점은 다음과 같습니다.
•
NLB는 AWS가 제공해주는 서비스로 HAProxy를 직접 구동하는 것보다 훨씬 높은 가용성을 보장합니다
•
NLB는 손쉽게 사용할 수 있지만 HAProxy는 복잡한 설정 파일을 구성해야 합니다
•
NLB를 사용하는 것과 달리, Haproxy를 사용하면 추가적인 관리 요소가 하나 늘어납니다
•
그림에서도 알 수 있듯이 HAProxy를 사용하는 구조가 훨씬 복잡합니다
HAProxy를 사용하는 구조를 고려했던 이유는 바로 안정성 떄문입니다. 마침 AWS DNA 세션에서 카오스 엔지니어링을 배우면서 AWS FIS에 대한 사용법을 익혔기 때문에 이를 팀원에게 공유하고 실제로 장애가 발생했을 때 복구 시간이 얼마나 소요되나 테스트해보기로 했습니다.
카오스 엔지니어링
카오스 엔지니어링은 아래와 같은 시나리오를 테스트했습니다. 이때 Relay는 모두 EC2가 아니라 ECS on EC2를 통해 구동한 후 카오스 엔지니어링을 진행했습니다.
•
NLB만 사용하는 경우
◦
ECS Task가 있는 EC2를 죽였을 때
◦
ECS Task를 죽였을 때
•
NLB와 HAProxy를 함께 사용하는 경우
◦
ECS Task가 있는 EC2를 죽였을 때
◦
ECS Task를 죽였을 때
◦
HAProxy Task가 죽은 경우
EC2 혹은 ECS에 장애를 발생시키기 위해서는 AWS FIS를 사용했으며 자세한 실험 결과는 아래와 같습니다.
위 실험 결과를 요약하면 다음과 같습니다.
•
NLB만 사용하는 경우
◦
ECS Task가 있는 EC2를 죽였을 때 : 킥보드가 다시 연결되는데 4분 57초
◦
ECS Task를 전부 죽였을 때
▪
킥보드가 다시 연결되는데 1분 50초 ~ 2분 10초
▪
Task 뜨고 Healthy 상태 되는데 3분 23초
◦
ECS Task를 반만 죽였을 때
▪
킥보드가 다시 연결되는데 6초
▪
Task 뜨고 Healthy 상태 되는데 2분 12초
•
NLB와 HAProxy를 함께 사용하는 경우 :
◦
ECS Task가 있는 EC2를 죽였을 때 : 킥보드가 다시 연결되는데 2분 40초
◦
ECS Task를 전부 죽였을 때
▪
킥보드가 다시 연결되는데 17초
▪
Task 뜨고 Healthy 상태 되는데 24초
◦
ECS Task를 반만 죽였을 때
▪
킥보드가 다시 연결되는데 2초(시뮬레이터 값으로 실제론 더 길 것으로 예상)
▪
Task 뜨고 Healthy 상태 되는데 1분(HAProxy SRV 레코드 갱신이 늦게된 것으로 보임)
◦
HAProxy Task를 전부 죽였을 때 : 킥보드가 다시 연결되는데 54초 ~ 59초
◦
HAProxy Task를 일부만 죽였을 때 : 킥보드가 다시 연결되는데 6초
위 실험 결과는 아래와 같이 요약할 수 있습니다.
•
Relay Task가 남아있는 경우, 어떤 방법을 선택하던 복구되는데 6초 정도의 시간이 걸린다
•
Relay Task에 문제가 생긴 경우, NLB 방식을 사용하여 Relay Task를 복구하면 Healthy 상태가 되는 데는 HAProxy 방식보다 몇 배 더 느리다
•
EC2에서 Relay Task에 문제가 발생한 경우, 해당 Task를 복구하고 Healthy 상태로 만드는 것은 HAProxy 방식보다 NLB 방식이 2배 더 느리다
즉, HAProxy 방법을 사용하면 우리의 예상대로 NLB만을 사용한 방식보다 훨씬 복구가 빨리 된다는 것을 알 수 있습니다. HAProxy는 복구 시간이 짧다는 것 외에도 아래와 같은 장점이 존재합니다.
•
NLB와 달리 HAProxy는 설정할 수 있는 값들이 많습니다. Health Check에 대한 설정이나 백엔드 구성에 대한 캐시 등 훨씬 자유롭게 서비스에 맞게 설정하는 것이 가능합니다
•
NLB의 경우 Task에 분배되는 방식을 자유롭게 조정할 수 없습니다. 하지만 HAProxy는 훨씬 자유롭게 Task에 커넥션을 분배할 수 있습니다.
•
NLB는 갱신이 느린 CloudWatch를 통해 지표를 확인할 수 있지만 HAProxy는 자체 Admin 사이트를 제공합니다. 또한 Prometheus Metric을 지원하므로 다른 방법으로 모니터링 대시보드를 구축하는 것도 가능합니다.
HAProxy의 커넥션 분배가 자유롭다는 특성을 이용하면 아래와 같이 테스트 풀을 만들 수도 있습니다.
HAProxy에서는 특정 백엔드의 커넥션 수를 제한할 수도 있습니다. 이 특성을 잘 활용하면 적은 수의 커넥션만 연결되는 Test Relay 서버를 만드는 것도 가능합니다.
이전 아키텍처에서 불편했던 점들 중 하나가 바로 테스트 풀이었습니다. Relay에 문제가 생기면 전체 서비스에 문제가 생기는 구조라 Relay를 잘 테스트하는 것이 중요했는데 이전 아키텍처에서는 수동으로 킥보드 풀을 관리해야만 이를 달성할 수 있었습니다.
그러나 HAProxy의 이 기능을 사용하면 HAProxy의 설정만 바꿔줘도 손쉽게 킥보드 테스트 풀을 생성하고 테스트할 수 있었습니다. 별도의 킥보드 테스트 풀을 따로 관리할 필요도 없었으며 테스트 풀 규모 또한 동적으로 쉽게 조정이 가능했습니다.
즉, 정리하면 아래와 같은 고려를 했고 결국 HAProxy와 NLB를 함께 사용하기로 했습니다.
•
NLB만 사용하는 것보다 HAProxy를 같이 사용하면 관리할 지점이 더 많아지지만, 이를 감수하고 HAProxy를 사용한다
◦
AWS 서비스를 사용하면 수동으로 관리해야만 하는 포인트가 많지 않다
◦
그대신 최대한의 관리 부담을 줄이기 위해서 HAProxy는 ECS Fargate로 구동한다
◦
HAProxy는 여유 있게 구성해 모든 HAProxy가 죽을 확률을 낮춘다. HAProxy의 성능덕분에 1개의 HAProxy만 살아있어도 우리 트래픽 정도에선 충분하다
•
NLB의 Target Group 문제는 여전히 존재하지만 HAProxy에게는 큰 문제가 되지 않는다
◦
HAProxy는 Relay와 달리 자주 배포하지 않아도 된다
◦
HAProxy는 오랫동안 관리된 소프트웨어로 Relay보다 더 안정적이다
•
HAProxy를 사용하는 이유는 빠른 복구 시간과 자유로운 설정이다
◦
Relay에 문제가 생기면 전사 서비스 장애로 이어진다. 따라서 조금이라도 빠른 복구 시간을 얻는 것이 최우선 목표다
◦
NLB의 경우 ALB와 달리 설정할 수 있는 것들이 거의 없다. 하지만 HAProxy를 사용하면 더욱 자유롭게 설정이 가능하고 테스트 풀도 쉽게 구성할 수 있다
◦
NLB와 달리 HAProxy를 사용하면 더욱 많은 지표를 실시간으로 얻을 수 있다.
여전히 존재하는 문제점
동적 스케일 프로젝트에서 언급한 ‘킥보드가 고정된 Relay에 연결된다’는 문제는 동적 할당 프로젝트를 통해서 해결했습니다. 그러나 동적 스케일 프로젝트 마지막에서 다룬 것처럼 다음과 같은 문제는 여전히 존재합니다.
•
동적 스케일 아키텍처를 채택하면서 성능 차이가 크지는 않지만 그래도 성능 차이가 존재합니다
•
이전에는 킥보드가 연결된 Relay가 이미 결정되어 있었기 때문에, 요청을 받은 Relay가 현재 킥보드와 통신을 끊었을 경우, 이를 즉시 Request 서버에 알릴 수 있었습니다. 그러나 모든 릴레이가 하나의 토픽을 구독하다 보니, Relay 스스로는 자신이 킥보드와 연결이 끊겨있다고 해서 실제로 킥보드가 모든 Relay와 연결이 끊긴 것인지 판단할 수 없게 되었습니다. 따라서, Request 서버는 통신이 끊긴 킥보드에 대한 요청을 보내면, 정해진 타임아웃 시간 동안 항상 기다려야 하는 문제가 생겼습니다
이는 나중에 Kafka 대신 Redis를 Biz와 Relay 사이의 요청 채널로 활용하여 해결할 수 있었습니다. 이에 대한 내용은 다음 문서에서 다룹니다.