본문 바로가기

서버운영 (TA, ADMIN)/분산처리

[분산처리] 대용량 세션을 위한 로드밸런서

대용량 서비스를 운영하려면 부하 분산은 필수입니다. 대용량 트래픽을 장애 없이 처리하려면 여러 대의 서버에 적절히 트래픽을 분배해야 합니다. 기존에는 세션 서버를 위한 로드밸런서로 DNS와 L4를 이용하였으나 네이버에서는 최적화된 분산을 위하여 로드밸런서를 직접 개발했다고 합니다.

 

본 포스팅은 학습을 위해 http://d2.naver.com/helloworld/605418 문서를 참조하여 작성했습니다.

 

 

 

기존 로드 밸런서의 제약 사항

 

 

DNS(Domain Name System)

DNS는 도메인 이름을 IP 주소로 변환하는 기술입니다. 하나의 도메인 이름을 라운드로빈(Round Robin) 방식으로 여러 개의 IP 주소로 변환한다면 이것만으로도 쉽게 부하 분산이 가능합니다.

 

하지만 여기에는 두가지 단점이 있습니다.

 

첫째, 대부분의 클라이언트에서는 DNS 서버의 부하를 줄이고 성능을 향상하기 위해 일정 시간 동안 캐싱하기 때문에 부하 분산이 균등하게 되지 않습니다.

 

둘째, 특정 서버에 장애가 발생하더라도 장애 여부가 감지되지 않아 서비스에서 해당 서버를 제거할 수 없습니다. 이것을 보완하기 위해 health check로 장애를 감지하여 DNS 서버에서 제거할 수 있지만, 모든 DNS 서버에 적용되는 데에 상당히 시간이 소요될 뿐만 아니라 클라이언트의 캐싱 때문에 서비스에서 바로 제거되지는 않습니다.

 

 

L4

L4는 IP 주소와 포트를 기반으로 로드밸런싱하는 고가의 하드웨어로, 웬만한 서비스에서는 이것만으로도 부하 분산을 처리하기에 충분합니다.

 

여기에서 주의해야 할 점은 L4는 VIP(Virtual IP) 단위로만 로드밸런싱하기 때문에 반드시 하나의 VIP에서 연결된 서버의 수가 비슷해야 한다는 것입니다. 만일 서버 중 몇 대 문제가 생긴다면 동일한 VIP에 연결된 다른 서버 역시 연달아 부하가 발생하여 더욱 심각한 문제가 발생할 수 있습니다.

 

또한 L4의 스펙상 최대 세션 수는 존재하나 세션을 맺는 시나리오에 따라 최대 성능이 다르기 때문에 최대 세션 용량에 도달하지 않았음에도 추가로 세션을 연결할 수 없는 문제가 발생하기도 합니다.

 

마지막으로 통신사 장애 등으로 세션이 비정상적으로 종료된 경우 세션 서버에서는 클라이언트와 세션이 종료되고 정상적으로 다시 연결되었지만, L4에서는 세션 종료처리가 제대로 되지 않아 두 개의 세션을 동시에 유지하고 있어 다른 L4의 한계 용량을 초과할 수도 있게 됩니다.

 

 

 

제약사항이나 실제 장애 등을 통한 개선점

 

 

클라이언트 접속 제한

세션 서버에 장애가 발생하면, 클라이언트는 로드밸런서에 접속할 때 새로 정의된 프로토콜을 이용합니다. 새로 정의된 프로토콜은 세션 서버와 통신을 제한하여 장애 대응 시간을 줄일 수 있습니다.

 

세션 서버가 비정상적인 상황에서 클라이언트가 지속적으로 접속을 시도한다면 통신사의 네트워크 용량을 초과하여 심각한 문제를 일으킬 수 있습니다. 기존에는 응답 시간을 최소화하기 위해서 세션 서버와 세션을 항상 유지하려고 노력하는 와중에 망 부하가 발생할 수도 있습니다. 그래서 네이버는 로드밸런서와 통신이 되지 않으면 클라이언트 자체적을 재접속 타이머가 동작하도록 하여 망부화를 최소화하는 방안을 선택했습니다.

 

 

L4 증설 시점 예측

최대 세션 수를 예측할 수 있으며, 앞으로 사용자가 늘어날 것에 대비하여 사전에 세션 서버를 증설할 수 있어야 합니다. L4는 일반적으로 세션 유지가 필요하지 않은 웹서버나 세션수가 적은 서버의 트래픽 분산에 주로 사용합니다. 앞에서 설명한 것처럼 L4의 스펙상 최대 세션수는 존재하지만, 세션을 맺는 시나리오에 따라 최대 성능이 다르기 때문에 정확한 성능을 알려면 직접 테스트해야 합니다. 그러나 수천만 세션을 직접 테스트하기란 현실적으로 불가능합니다. 그래서 실제로 문제가 발생하고 나서야 L4 최대 용량이 얼마인지 정확히 파악할 수 있습니다. 이런 이유로 서비스의 최대 세션 수를 예측할 수 있고 장애 발생 시 아는 범위 내에서 대처할 수 있는 로드밸런서를 개발하게 됩니다.

 

 

서버 단위 로드밸런스

특정 서버에 부하가 몰리는 것을 막기 위해서 VIP 단위가 아닌 세션 서버 단위의 로드밸런싱 기능이 필요합니다. 그러나 운영을 하다 보면 VIP 당 서버 수를 항상 동일하게 유지하는 것이 쉽지는 않습니다. 그래서 서버 단위로 로드밸런싱을 택합니다.

 

 

다양한 로드밸런싱 알고리즘

상황에 따라 적절한 로드밸런싱 알고리즘을 사용합니다. 가장 간단한 방법으로는 가용한 모든 서버에 라운드로빈으로 로드밸런싱하는 것입니다. 이런 경우 서버 재시작 등으로 인하여 세션 수의 불균형이 발생하면 다시 고르게 분배되는 데 상당한 시간이 소요됩니다. 그러나 Weight Least Connection 등의 알고리즘을 사용하여 세션 수가 적은 서버에 가중치를 두어 트래픽을 분산한다면 세션 수의 불균형이 발생하더라도 금방 고르게 세션이 분산되므로 세션 서버의 배포 등의 작업을 쉽게 할 수 잇습니다.

 

그렇다고 해서 무조건 그 서버에 트래픽을 몰아주면 해당 서버의 순간 TPS(transaction per second)가 높아 그대로 장애로 직결될 수 있음을 기억해야 합니다.

 

 

세션 서버 배포 시간 단축

불필요한 세션 서버를 서비스에서 빠르게 제거할 수 있어야 합니다. DNS에서 VIP를 삭제하여도 클라이언트에서 VIP 주소를 캐싱하고 있다면 몇 달이 지나도록 서버에 트래픽이 계속해서 유입될 수 있습니다. 따라서 서비스에서 제거하려고 DNS에서 세션 서버를 제거해도 세션 서버의 세션이 모두 끊어지기 전까지는 서비스에서 제거할 수 없습니다. 이런 DNS를 사용하지 않고 직접 개발한 로드밸런서를 통해 세션 서버의 주소를 요청한다면 이 문제는 해결할 수 있습니다.

 

 

전체적인 구조도

 

로드밸런서를 구성하는 모듈의 역할은 다음과 같습니다. 다양한 분산 알고리즘을 가진 조회 서버(lookup server)와 세션 서버를 관리하는 서버 매니저(server manager)로 나눌 수 있습니다. 그리고 세션 서버의 목록을 저장하기 위해 ZooKeeper를 사용하고, 세션 수 등을 비롯한 세션 서버 정보를 저장하기 위해 다양한 컬렉션을 제공하는 Redis를 사용합니다. 그리고 단말과 1:1 세션을 맺고 있는 세션 서버가 있습니다.

 

 

클라이언트

클라이언트는 세션 서버 주소를 얻기 위해 조회 서버에 접속하여, 접속 가능한 세션 서버의 주소를 가져옵니다. 세션 서버의 주소를 정상적으로 수신하면 해당 세션 서버로 연결을 요청합니다. 이 때 발생할 수 있는 상황은 크게 세 가지가 있습니다.

 

첫째, 모든 세션 서버의 장애로 정상적으로 동작하는 세션 서버가 없다면 조회 서버는 세션 서버가 없다는 메시지와 함께 일정 시간 후에 다시 접속하라고 응답합니다. 이런 경우 백오프 타임 후에 재접속을 시도합니다.

 

둘째, 단말의 네트워크가 비정상이거나 통신사의 장애로 네트워크가 정상적으로 동작하지 않아 조회 서버와 통신하는 것조차 불가능하다면 클라이언트는 자체적으로 타이머를 두어 네트워크 부하를 최소화합니다. 이런 경우 응답시간 최소화보다는 네트워크 망 부하 최소화를 위해서 모든 클라이언트들의 타이머 값이 서로 다르도록 백오프 알고리즘을 통해 타이머 값을 설정합니다.

 

백오프 알고리즘은 GCM(Google Cloud Messaging for Android)에서 사용되고 있는 알고리즘을 참고합니다. 기준이 되는 백오프 타임이 있고 이는 최대 백오프 타임(MAX_BaACKOFF_MS)을 초과하기 전까지는 두배씩 증가합니다. 실제 클라이언트에서 조회 서버에 접속하기 위해 재시도할 때마다 적용되는 백오프 타임은 기준이 되는 백오프 타임에 일정 수식을 사용하여 구합니다.

 

Random sRandom = new Random();

int backoffTimeMs = getBackoff();
int nextAttempt = backoffTimeMs / 2 + sRandom.nextInt(backoffTimeMs);

setAlarmManager(nextAttempt); // 실제 적용된 백오프 타임

if (backoffTimeMs < MAX_BACKOFF_MS) {
  setBackoff(backoffTimeMs * 2); // 다음 백오프 타임을 구하기 위한 currentBAckoffTime set
}

 

셋째, 정상적으로 세션 서버의 주소를 수신하여 세션 서버에 접속을 시도하더라도 접속이 원활하지 않을 수 있습니다. 이런 경우 일정 횟수 이상 재접속을 시도해도 세션 서버와 접속이 되지 않으면 해당 세션 서버에 장애가 발생했다고 판단합니다.

 

이런 비정상적인 경우 처음부터 로직을 다시 수행해야 하므로 비용이 많이 듭니다. 이런 비용을 줄이기 위해서 조회 서버로부터 두개의 세션 주소를 받고 첫번째 세션 서버에 접속되지 않는다면 두번째 세션서버 주소 (Alternative IP)에 접속을 시도하여 비용을 최소화합니다.

 

이런 비정상적인 경우 처음부터 로직을 다시 수행해야 하므로 비용이 많이 듭니다. 이런 비용을 줄이기 위해서 조회서버로부터 두 개의 세션 서버 주소를 받고 첫번째 세션 서버에 접속되지 않는다면 두번째 세션 서버 주소(Alternative IP)에 접속을 시도하여 비용을 최소화합니다.

 

 

세션 서버

세션 서버에 추가된 기능은 주기적으로 세션 수를 서버 매니저에게 알려주는 기능과 서버 매니저의 L7 health check 요청에 응답하는 기능입니다.

 

서버 매니저로부터 주기적으로 유입되는 health check 요청을 적절히 처리하여 세션 서버의 정상 동작 여부를 알려줍니다. 서버 매니저는 일정 시간 동안 health check에 실패하면 해당 세션 서버를 서버 목록에서 제거하기 때문에 자신의 상태를 정확히 전달해야 합니다. 응답할 때 자신의 현재 세션 수를 서버 매니저에게 알려줍니다.

 

 

조회 서버