Envoy 내부 구조 · 누구를 고를 것인가 · 이론
세션 고정이 가끔 풀리는 이유
한 줄 요약
클러스터가 "보낼 만한 곳" 을 추려 놓으면, 그중 하나를 고르는 일이 부하 분산이다. 고르는 방식은 상태를 얼마나 기억하는가로 갈린다 — 아무것도 안 기억하면 무작위, 차례를 기억하면 라운드 로빈, 요청의 열쇠를 기억하면 해시 링이다. 무엇을 고르든 대가가 따라온다.
왜 이게 필요했나
서버가 세 대면 요청을 3분의 1씩 나누면 될 것 같다. 실제로 대부분은 그렇다. 문제가 되는 것은 두 가지 요구가 들어올 때다.
하나는 "이 사용자는 늘 같은 서버로 보내 주세요" 다. 서버가 로컬 캐시를 들고 있거나 세션을 메모리에 두고 있으면 이 요구가 성능과 정확성을 모두 좌우한다. 다른 하나는 "서버 한 대를 빼는데 전체가 다시 섞이면 곤란합니다" 다. 캐시를 들고 있는 서버들이 한꺼번에 자리를 바꾸면, 빼는 순간 전체 캐시 적중률이 바닥으로 떨어지고 백엔드가 그 부하를 받는다.
이 두 요구를 함께 푸는 것이 일관 해싱이고, Envoy 에서는 RING_HASH 와 MAGLEV 가 그것이다.
어떻게 동작하나
| lb_policy | 어떻게 고르나 | 대가 |
| --- | --- | --- |
| ROUND_ROBIN | 목록을 돌아가며 하나씩 | 워커마다 차례를 따로 기억한다 |
| LEAST_REQUEST | 무작위로 둘을 뽑아 진행 중인 요청이 적은 쪽 | 요청 비용이 들쭉날쭉할 때 유리 |
| RANDOM | 매번 무작위 | 기억할 것이 없어 싸다. 표본이 작으면 치우친다 |
| RING_HASH | 열쇠를 해시해 링에서 가장 가까운 엔드포인트 | 링을 만드는 비용. 링이 성기면 분포가 치우친다 |
| MAGLEV | 고정 크기 조회표로 같은 성질을 더 싸게 | 표 크기가 고정이라 엔드포인트가 아주 많으면 제약 |
엔드포인트마다 load_balancing_weight 를 줄 수 있다. 가중치 2 는 "두 번에 한 번씩 더" 가 아니라 전체 몫에서 두 배를 차지한다는 뜻이다. 셋의 가중치가 2·1·1 이면 합이 4 이므로 12번에 6 대 3 대 3 이 된다.
해시 기반 방식에는 열쇠가 필요하다. 그것을 고르는 것이 라우트의 hash_policy 이고, 헤더 · 쿠키 · 질의 파라미터 · 출발지 IP 중에서 뽑을 수 있다. 여기서 자주 놓치는 것 하나 — 열쇠를 뽑지 못한 요청은 해시가 없으므로 그냥 무작위로 간다. 쿠키가 없는 첫 요청, 헤더를 빠뜨린 내부 호출이 그렇다. "대부분은 고정되는데 가끔 다른 데로 갑니다" 의 정체가 대개 이것이다.
링의 성질도 알아 둘 값어치가 있다. 단순히 '해시 값을 서버 수로 나눈 나머지' 를 쓰면 서버 수가 바뀌는 순간 거의 모든 열쇠가 자리를 옮긴다. 링 방식은 사라진 서버에 걸려 있던 열쇠만 옮겨 가고 나머지는 그대로다. 대신 링이 성기면(minimum_ring_size 가 작으면) 엔드포인트가 링 위에 고르게 퍼지지 않아 분포 자체가 치우친다.
현장에서 만나는 모습
"9번 보냈는데 3 대 3 대 3 이 안 나옵니다." Envoy 의 워커 스레드는 각자 라운드 로빈 차례를 기억한다. 기본 concurrency 는 코어 수이므로, 적은 요청으로 세면 워커마다 흩어져 숫자가 매번 다르다. 분배를 세는 실험은 --concurrency 1 로 한다. 운영에서는 세지 말고 통계로 본다.
"세션 고정을 켰는데 가끔 풀립니다." 열쇠가 없는 요청이 섞여 있는지부터 본다. 그리고 엔드포인트가 들고 나는 순간에는 일부가 옮겨 가는 것이 정상이다 — 일관 해싱은 '아무도 안 옮긴다' 가 아니라 '옮기는 수를 최소로' 다.
특정 서버만 뜨거워지는 경우. 해시 열쇠의 분포가 치우쳤을 때 생긴다. 테넌트 아이디를 열쇠로 썼는데 테넌트 하나가 트래픽의 절반이면, 그 테넌트가 걸린 서버가 절반을 받는다. 이럴 때는 열쇠를 더 잘게 쪼개거나(사용자 단위로) 그 테넌트만 따로 뺀다.
방식을 고르는 기준. 정리하면 질문 두 개로 줄어든다. 첫째, 같은 요청이 같은 곳으로 가야 하는가. 로컬 캐시나 세션이 걸려 있으면 그렇고, 그러면 해시 기반으로 간다. 둘째, 요청 비용이 들쭉날쭉한가. 어떤 요청은 5밀리초이고 어떤 요청은 2초라면 차례대로 나누는 방식은 느린 요청을 받은 서버에 계속 일을 더 얹는다. 그때는 진행 중인 요청 수를 보는 LEAST_REQUEST 가 낫다. 둘 다 아니면 라운드 로빈으로 두는 것이 가장 예측하기 쉽고, 규모가 아주 커지면 상태를 기억하지 않는 무작위가 오히려 싸진다.
공식 문서: [Load balancing overview](https://www.envoyproxy.io/docs/envoy/v1.38.3/intro/arch_overview/upstream/load_balancing/overview) · [Cluster configuration](https://www.envoyproxy.io/docs/envoy/v1.38.3/api-v3/config/cluster/v3/cluster.proto) · [HTTP route components](https://www.envoyproxy.io/docs/envoy/v1.38.3/api-v3/config/route/v3/route_components.proto)
다음 실습에서 할 것
같은 업스트림 셋을 두고 방식만 바꿔 가며 숫자를 센다. 라운드 로빈이 정확히 나누는 것, 가중치가 몫을 바꾸는 것, 무작위가 작은 표본에서 치우치는 것을 차례로 확인하고, 해시 링에서는 같은 사용자가 한 곳에 붙는 것과 서버 한 대를 뺐을 때 몇 명이 자리를 옮기는지를 직접 센다. 마지막으로 열쇠를 헤더에서 질의 파라미터로 바꿔, 열쇠가 없는 요청이 고정되지 않는 것까지 본다.