선요약 : 서비스의 가용성을 늘리고 TPS를 향상시키기 위해 다중 분산 서버를 도입했습니다. 현재 세션 기반으로 인증 처리를 하고 있는데, 서버간 데이터 불일치로 인한 문제를 해결하기 위해 레디스를 세션 서버로 사용했습니다.
# 분산 서버 개념
단일 웹 어플리케이션 서버로 구성된 프로젝트는 하나의 서버가 모든 사용자 요청을 받는다. 이 경우 여러가지 문제가 생길 수 있다.
- 하나의 서버에 모든 트래픽이 몰리기 때문에 서버 부담이 커진다.
- 하나의 서버에 예상치 못한 오류가 발생할 경우 서비스 전체가 죽는다.
- 글로벌 서비스의 경우, 물리적으로 거리가 먼 한 대의 서버만 사용했을 때 네트워크 자체로 병목이 될 여지가 있다.
요컨데 성능과 확장성, 그리고 가용성 측면에서 좋지 않다. 서버를 여러개 두면 전술한 문제가 어느정도 해소된다. 서버가 여러개이기 때문에 각 서버에 요청을 분산시키면 더 많은 요청 트래픽을 처리할 수 있고, 여러 개의 서버 중 하나에 문제가 생겨도 다른 서버들이 멀쩡하므로 서비스를 계속 이어나갈 수 있다.
서버는 하나의 IP주소를 갖는데 요청을 어떻게 분산시키지? 라고 묻는다면, 여러개의 서버 앞에 또다른 서버를 두어, 해당 서버는 요청을 분산시키는 서버로 동작하는 방법을 사용할 수 있다. 내장 서버로 톰캣을 사용하는 스프링 웹 어플리케이션 서버와 구분되는 개념으로, 어플리케이션 서버 앞단에서 요청을 분산하는 로드밸런서, 그리고 정적 컨텐츠의 제공자로서 동작하는 서버를 웹서버라고 한다.(웹 어플리케이션 서버와는 다르다! 웹 어플리케이션 서버와는...!)
웹 서버 솔루션으로는 엔진엑스를 사용할 것이다. 현재 다중서버를 사용하면서 달성하고자 하는 목적은 다음과 같다.
- 많은 동시 요청을 처리하기
- 간단한 설정으로 로드밸런싱 달성하기
- 리버스 프록시로써 동작하게 하기
아파치의 강점은 다양한 모듈을 붙일 수 있다는 점인데, 현재 웹서버는 좋은 성능의 간단한 웹 서버 자체로 동작하는 것이 더 중요해 설정 파일 작성이 상대적으로 간단하고, 동시 요청 처리에 더 강력한 엔진엑스를 사용하기로 했다.
기존에 1개였던 웹 어플리케이션 서버(이하 WAS)를 2개로 늘리고, WAS 앞단에 웹 서버(이하 WS)를 둔 프로젝트 아키텍처 구조는 아래와 같다.
각각의 사용자 요청 HTTP 메세지를 엔진엑스가 1번서버, 2번서버에 나눠 요청을 분산시킨다. 200개의 요청이 들어오는 상황에서, 기존에 1대의 서버만 운용했다면 1개의 WAS가 200개를 전부 처리해야 했겠지만 이렇게 서버를 나누면 1개의 WAS는 100개만 처리해도 된다. DB 서버가 따로 병목으로 동작하지 않는 한, 더 높은 성능을 낼 수 있도록 서버가 진화한 것이다.
이와같이 서버 개수를 늘려 서비스의 성능을 향상시키는 것을 Scale-out 방식이라고 한다. (Cf. CPU 개수나 RAM 용량을 늘리는 방식으로 서비스의 성능을 향상시키는 것은 Scale-up이라고 부른다)
# 세션 불일치로 인한 인증정보 유실
scale out 방식을 통해 향상된 성능을 테스트하려 POST 요청을 날리는데, 테스트 중간중간에 403 Forbidden 에러가 떴다. 로그를 보니 post 요청에 필요한 auth 정보가 없다.. 즉 인증(로그인) 처리가 되어있지 않다는 뜻이었다. 단일 서버로 테스트했을 때는 발행사지 않았던 오류가 약 50%의 비율로 일어나 문제의 이유를 생각해보니, 현재 인증 처리를 세션 기반으로 하고 있던 것이 문제였다.
세션은 서버에 저장되는 정보이다. "세션을 통한 사용자 인증"은 사용자 요청 헤더에 존재하는 Cookie값을 기반으로 사용자 specific한 인증 정보를 서버에 보관해놨다가, 사용자가 Cookie 헤더와 함께 요청을 보내면 해당하는 Cookie값에 맞는 세션 데이터를 통해 인증 처리를 하는 방식이다. 문제는 각 서버가 각자의 세션 저장소를 갖고 있기 때문에 발생한다.
예시와 그림을 통해 설명해보겠다. 사용자 A가 아이디와 비밀번호를 입력하여 로그인 요청을 날리는 상황을 가정해보자.
사용자 A의 로그인 요청은 로드밸런서에 의해 1번 서버로 보내진다. 사용자의 cookie에 맞게 1번 세션에 A의 인증 정보가 들어있다. 다음 요청에서 같은 cookie 값을 가진 사용자(A)가 1번 서버로 요청을 보내면, 서버는 A를 로그인된 유저로 인식할 것이다.
그러나 엔진엑스의 로드밸런싱은 다른 설정을 하지 않았을 때 Round Robin으로 이뤄진다. 사용자는 같은 엔진엑스 서버로 요청을 보냈지만, 다음과 같이 이번엔 2번 서버로 요청이 전달되었다고 가정하자.
2번 서버의 세션엔 A의 인증 정보가 없다. 그렇기 때문에 최초로 로그인한 서버가 아닌 다른 서버로 요청이 전달되었을 경우 인증 정보가 사라지고, 로그인 처리 역시 풀리게 된다. 현재 이 문제에 봉착해있던 것이다.
이를 어떻게 해결하면 좋을까? 해결방법을 생각하기 위해 다중 서버에서 로드밸런싱과 세션 관리가 어떻게 이뤄지는지 살펴보자.
# 세션 저장 방식
1. Sticky Session
처음 요청을 전달한 서버로만 요청을 전달하는 방식이다. 앞선 예시에서 사용자 A의 요청을 1번 서버로 전달했으므로, A의 다음, 다다음, 그리고 그 다음 요청도 계속 1번 서버로 전달하는 것이다.
이 방식을 채택하면 앞선 세션 불일치 문제를 신경쓰지 않아도 된다. 특정 사용자의 요청은 한 서버로만 전달될 것이므로, 사용자의 세션 정보 역시 한 서버에서만 관리하면 되기 때문이다. 그러나 sticky session엔 문제점이 있다.
- 하나의 서버에 너무 많은 요청이 몰릴 위험이 있다.
- 그로 인해 서버에 문제가 생기면, 해당 서버의 세션 데이터가 모두 유실될 위험이 있다.
기껏 가용성을 늘리기 위해 다중서버를 사용했지만, 이 이점을 충분히 활용하지 못하는 것이다. 그리고 같은 public IP를 공유하는 여러 요청이 있을 경우 하나의 서버에 요청이 몰릴 확률은 더욱 증가한다.
2. Session Clustering
여러 대의 WAS의 공유 세션을 클러스터링 하는 방식이다. 클러스터링이란, DB 분산 방법중 하나로 여러 대의 서버를 두어 한 대의 서버에 문제가 생겼을 때도 서비스를 계속하기 위해 분산 서버간 데이터를 공유하는 방식이다.
여러 대의 WAS끼리 세션 데이터를 공유하면서 마치 하나의 저장소처럼 작동할 수 있도록 하는 방식이다.
이 방식은 꽤나 유효하지만, 각 WAS에 모든 WAS의 세션이 공유되므로 세션 메모리 관리가 힘들어질 가능성이 있다. 세션 메모리 용량 자체도 클테고, 한 서버에서 세션 업데이트가 일어났을 때 모든 서버의 세션 저장소가 업데이트되므로 성능 측면에서 좋지 못할 것이다. 또한 서버를 추가하거나 삭제할 때 세션 저장소 클러스터링 설정을 수정해줘야 하므로 서버를 유연하게 만들기 힘들어진다.
3. Global Session
세션 저장소 역할을 하는 제3의 서버를 두는 방식이다. 아키텍처 구조는 아래와 같이 바뀐다.
이 방식을 사용하면 앞서 언급한 세션 관리 방식의 단점을 극복할 수 있다. 사용자의 요청이 어떤 WAS로 향하든 같은 세션 저장소가 사용되므로 인증 정보가 유실될 일이 없다. 또한 WAS가 얼마든지 추가/삭제되도 같은 세션 서버를 바라보게 하면 되므로 설정이 간단해진다.
세션 서버로는 캐시 서버로 사용되었던 Redis 서버를 사용하겠다. 프로젝트 yml파일에 아래 설정을 추가해주는 것으로 간단히 gloabal 세션을 구성할 수 있다.
spring:
session:
store-type: redis
# Append : JWT와의 차이점
JWT는 Json Web Token의 약자로, 사용자 인증 정보를 세션에 담아두는 대신 사용자 요청에 포함된 토큰으로 인증 정보를 확인하는 방법이다.
기존의 세션 기반 인증은 사용자 요청 cookie값에 저장된 sessionID에 맞는 세션 정보를 가지고 인증을 처리한다. 이 경우, 한 서버에 사용자가 몰리면 그만큼 많은 사용자들의 세션 정보를 서버에 저장해야하므로 세션 관리가 힘들어질 수 있다. 대신 일련의 인증 정보가 모두 포함된 토큰을 사용자에게 발행해서, 사용자의 요청에 해당 토큰을 붙여서 보내도록 구성할 수 있다. 이 때 사용되는 "토큰"을 JWT라고 한다.
JWT의 특징은 다음과 같다.
- 자체 포함 : 사용자 정보와 credential등 인증 전반에 필요한 정보를 토큰이 모두 가지고 있다. 따라서 서버가 인증을 위한 정보를 추가 관리할 필요가 없다.
- JSON 형식 : JSON Web Token의 약자답게, JSON 형식으로 토큰 정보를 저장한다.
- Signiture : 해시 기반의 사인을 통해 토큰의 위/변조를 방지한다.
JWT는 같은 인증 확인을 위해 매 요청마다 세션 데이터를 확인할 필요가 없기 때문에, 인증 정보를 공유하는 모든 서비스에서 사용이 가능하다. 클라이언트가 인증에 필요한 모든 정보를 가지고 있다는 특징이 보안 측면에선 다소 미스더라도, 세션 관리가 다소 힘든 MSA 환경에서 확장성 높게 인증 정보를 관리할 수 있다.
# 성능 개선 사항
결국 다중 서버로 변경 뒤 얼마나 더 많은 트래픽을 빠른 시간에 받아낼 수 있었는지가 관건이겠지?
결과는 다음과 같다. 평균 TPS는 5분간 같은 부하를 줬을 때 TIMEOUT 없이 응답을 내려줄 수 있는 기준으로 측정했다.
POST의 경우 3배를 넘는 향상이 일어났는데, POST의 경우 단일 서버로 처리하기 힘든 메세징 과정때문에 서버를 추가했을 경우 그 성능 향상이 더 도드라진 것 같다.
캐시 기능을 사용하지 않은 단순 GET 요청의 경우 약 2.4배의 TPS 개선을 이뤄냈다. DB의 경우 읽기 전용으로 엘라스틱 서치를 쓰고 있었는데, ES 인스턴스 2개를 샤딩하고 있어서 원래 검색속도가 빠른 ES는 병목으로 작용하기 힘들 것이다(아마). 서버 개수와 비례해서 TPS가 더 늘어나지 않을까 예상할 수 있겠다.
timeout이 날 경우 병목으로 작용하는 부분을 어떻게 골라낼까요? 물어봤더니 결국은 "가정"을 하는 것이 필요하다고 하셨다. 아키텍처의 어떤 기능의 성능이 전반적으로 어느 정도일지, 병목을 일으킬 가능성은 없을지 판단하고 해당 부분에 단위적으로 부하를 걸어 단위적으로 개선을 하는 과정이 필요한 모양이다.