Interview/Network

Load balancing 로드 밸런싱

김 정출 2024. 10. 19. 13:39

Load balancing 로드 밸런싱

  • 로드밸런싱은 여러 서버 또는 네트워크 장비에 걸쳐서 네트워크 트래픽이나 컴퓨팅 부하를 분산시키는 기술을 의미합니다.
  • 주요 목표는 하나의 서버에 트래픽이나 부하가 집중되는 것을 방지하고, 전체 시스템의 성능, 가용성, 신뢰성을 향상시키는 것입니다. 로드밸런서는 이 역할을 수행하는 장비나 소프트웨어를 의미합니다.

1. 로드밸런싱

로드밸런싱의 필요성

  1. 성능 향상: 여러 서버로 트래픽을 분산시킴으로써 각 서버의 처리 성능을 최적화할 수 있습니다.
  2. 가용성 증가: 하나의 서버가 장애를 일으키더라도 다른 서버가 대신 처리할 수 있게 하여 서비스의 가용성을 보장합니다.
  3. 확장성 향상: 서버 수를 추가하여 손쉽게 시스템을 확장할 수 있으며, 확장성 있는 시스템 구조를 유지할 수 있습니다.

로드밸런싱의 기본 개념

로드밸런서는 클라이언트의 요청을 받고, 적절한 서버로 요청을 전달합니다. 클라이언트는 특정 서버의 존재를 인식하지 못하며, 로드밸런서가 모든 서버를 대신해서 관리하게 됩니다.

로드밸런싱의 주요 기능

  1. 트래픽 분산: 여러 서버로 네트워크 트래픽을 나누어 처리합니다.
  2. 장애 조치 (Failover): 특정 서버에 장애가 발생하면 다른 서버로 트래픽을 전환합니다.
  3. 상태 모니터링: 서버의 상태를 지속적으로 감시하여 정상 상태의 서버만 선택하도록 합니다.
  4. SSL 오프로딩: 로드밸런서가 SSL 인증을 대신 처리하여 서버의 부하를 줄입니다.

로드밸런싱의 유형

  1. 레이어 4 (L4) 로드밸런싱:
    • 설명: 전송 계층에서 IP 주소와 포트 번호를 기준으로 트래픽을 분배합니다.
    • 장점: 빠르고 효율적이며, 패킷 수준에서의 로드밸런싱이 가능.
    • 단점: 애플리케이션 레벨의 데이터에 대한 세세한 처리가 불가능.
  2. 레이어 7 (L7) 로드밸런싱:
    • 설명: 애플리케이션 계층에서 URL, HTTP 헤더, 쿠키 등의 정보를 바탕으로 트래픽을 분배합니다.
    • 장점: 특정 애플리케이션 로직에 따른 트래픽 분배가 가능하며, 더 세밀한 제어가 가능.
    • 단점: 처리에 더 많은 리소스가 필요하고, 설정이 복잡.

로드밸런서의 종류

  1. 하드웨어 로드밸런서: 전용 하드웨어 장비를 사용하여 로드밸런싱을 수행합니다. 높은 성능과 안정성을 제공하지만 비용이 높을 수 있습니다.
  2. 소프트웨어 로드밸런서: 소프트웨어로 동작하는 로드밸런서로, 오픈 소스 제품 (예: HAProxy, Nginx)이나 클라우드 기반 로드밸런서 (AWS ELB, Azure Load Balancer 등)가 있습니다.
  3. DNS 기반 로드밸런싱: DNS를 사용하여 서버 IP 주소를 반환하는 방식으로 트래픽을 분산시킵니다. 전 세계적으로 분산된 데이터 센터를 사용할 때 유용합니다.

로드밸런싱의 대표적인 사용 예

  • 웹 서비스: 다수의 웹 서버를 운영하여 대규모 트래픽을 처리할 때 로드밸런서를 통해 트래픽을 분산.
  • 데이터베이스: 데이터베이스의 읽기 부하를 여러 읽기 전용 서버로 분산하여 처리 성능 향상.
  • 마이크로서비스 아키텍처: 다양한 마이크로서비스 간 트래픽을 균등하게 분배하여 서비스를 안정적으로 운영.

2. 로드밸런싱 알고리즘

로드밸런싱 알고리즘은 여러 서버에 트래픽을 분산시켜 서버의 부하를 줄이고 서비스의 가용성과 성능을 최적화하는 기술입니다. 이를 위해 여러 가지 알고리즘이 사용되며, 각각의 알고리즘은 특정 상황에 적합한 방식으로 동작합니다. 주요 로드밸런싱 알고리즘을 몇 가지 설명하겠습니다:

1. 라운드 로빈 (Round Robin)

  • 설명: 요청을 순서대로 각 서버에 분배하는 방식입니다. 첫 번째 요청은 첫 번째 서버로, 두 번째 요청은 두 번째 서버로 보내는 식으로, 마지막 서버까지 분배가 되면 다시 첫 번째 서버로 돌아가 반복됩니다.
  • 장점: 구현이 간단하고 서버 간에 균등한 분배가 가능.
  • 단점: 서버의 상태나 부하를 고려하지 않기 때문에 비효율적일 수 있음.
class RoundRobinLoadBalancer:
    def __init__(self, servers):
        """
        서버 리스트를 초기화하고 인덱스를 0으로 설정합니다.
        
        :param servers: 트래픽을 분산할 서버들의 리스트
        """
        self.servers = servers
        self.index = 0

    def get_next_server(self):
        """
        순차적으로 다음 서버를 선택합니다.
        
        :return: 선택된 서버
        """
        server = self.servers[self.index]
        self.index = (self.index + 1) % len(self.servers)  # 인덱스 순환
        return server

# 서버 리스트를 정의
servers = ["Server-1", "Server-2", "Server-3", "Server-4"]

# 로드밸런서 인스턴스 생성
load_balancer = RoundRobinLoadBalancer(servers)

# 10개의 요청을 순차적으로 분배
for i in range(10):
    server = load_balancer.get_next_server()
    print(f"Request {i + 1} is handled by {server}")

2. 가중 라운드 로빈 (Weighted Round Robin)

  • 설명: 각 서버에 가중치를 부여하고, 가중치에 따라 더 많은 요청을 특정 서버로 분배하는 방식입니다. 예를 들어, 서버 A의 가중치가 2, 서버 B의 가중치가 1이면, A는 두 번, B는 한 번씩 요청을 받습니다.
  • 장점: 서버의 처리 능력에 따라 분배 가능.
  • 단점: 서버 상태의 실시간 변화에는 대응하지 못함.
class WeightedRoundRobinLoadBalancer:
    def __init__(self, servers_with_weights):
        """
        서버와 가중치 정보를 초기화합니다.
        
        :param servers_with_weights: (서버, 가중치) 형태의 리스트
        """
        self.servers = []
        self.weights = []
        self.current_weight = 0
        self.current_index = -1
        
        # 서버와 가중치를 분리하여 리스트로 저장
        for server, weight in servers_with_weights:
            self.servers.append(server)
            self.weights.append(weight)
        self.max_weight = max(self.weights)
        self.gcd_weight = self._gcd_of_weights(self.weights)

    def _gcd_of_weights(self, weights):
        """
        가중치들의 최대 공약수를 계산합니다.
        """
        from math import gcd
        gcd_weight = weights[0]
        for weight in weights[1:]:
            gcd_weight = gcd(gcd_weight, weight)
        return gcd_weight

    def get_next_server(self):
        """
        가중치 기반 라운드 로빈 알고리즘을 통해 다음 서버를 선택합니다.
        
        :return: 선택된 서버
        """
        while True:
            self.current_index = (self.current_index + 1) % len(self.servers)
            if self.current_index == 0:
                self.current_weight = self.current_weight - self.gcd_weight
                if self.current_weight <= 0:
                    self.current_weight = self.max_weight
                    if self.current_weight == 0:
                        return None
            
            if self.weights[self.current_index] >= self.current_weight:
                return self.servers[self.current_index]

# 서버와 가중치 정보 (서버, 가중치) 형태의 리스트
servers_with_weights = [("Server-1", 5), ("Server-2", 1), ("Server-3", 1)]

# 로드밸런서 인스턴스 생성
load_balancer = WeightedRoundRobinLoadBalancer(servers_with_weights)

# 10개의 요청을 순차적으로 분배
for i in range(10):
    server = load_balancer.get_next_server()
    print(f"Request {i + 1} is handled by {server}")
  • 가중치 리스트와 서버 리스트를 분리하여 초기화합니다.
  • *최대 가중치와 가중치들의 최대 공약수(GCD)**를 계산합니다. 이는 알고리즘이 각 서버에 부하를 공평하게 분배하는 데 중요합니다.
  • get_next_server 메소드에서는 가중치가 높은 서버가 더 많은 요청을 받도록 로직을 구성합니다.
  • 매 요청마다 현재 인덱스를 증가시키고, 필요 시 현재 가중치를 감소시킵니다. 가중치가 0이 되면 다시 최대 가중치로 설정합니다.
  • 가중치 조건에 맞는 서버를 선택합니다.

3. 최소 연결 (Least Connections)

  • 설명: 현재 연결된 클라이언트 수가 가장 적은 서버에 새로운 요청을 분배하는 방식입니다.
  • 장점: 트래픽이 고르게 분배되고, 서버의 부하에 따라 유동적으로 요청을 분배.
  • 단점: 서버 간의 부하 상태를 실시간으로 모니터링해야 하며, 설정이 복잡할 수 있음.’
class LeastConnectionsLoadBalancer:
    def __init__(self, servers):
        """
        서버 리스트를 초기화하고, 각 서버의 연결 수를 0으로 초기화합니다.
        
        :param servers: 트래픽을 분산할 서버들의 리스트
        """
        self.servers = {server: 0 for server in servers}

    def get_next_server(self):
        """
        최소 연결 수를 가진 서버를 선택합니다.
        
        :return: 선택된 서버
        """
        # 연결 수가 가장 적은 서버를 선택
        server = min(self.servers, key=self.servers.get)
        # 선택된 서버의 연결 수를 증가
        self.servers[server] += 1
        return server

    def release_connection(self, server):
        """
        특정 서버의 연결 수를 감소시킵니다.
        
        :param server: 연결을 해제할 서버
        """
        if self.servers[server] > 0:
            self.servers[server] -= 1

    def display_connections(self):
        """
        현재 각 서버의 연결 수를 출력합니다.
        """
        for server, connections in self.servers.items():
            print(f"{server}: {connections} connections")

# 서버 리스트 정의
servers = ["Server-1", "Server-2", "Server-3"]

# 로드밸런서 인스턴스 생성
load_balancer = LeastConnectionsLoadBalancer(servers)

# 10개의 요청을 순차적으로 분배
for i in range(10):
    server = load_balancer.get_next_server()
    print(f"Request {i + 1} is handled by {server}")

# 현재 각 서버의 연결 수를 표시
load_balancer.display_connections()

# 일부 서버에서 연결을 해제
load_balancer.release_connection("Server-1")
load_balancer.release_connection("Server-2")

# 연결 해제 후의 상태를 표시
print("\\nAfter releasing some connections:")
load_balancer.display_connections()

4. IP 해시 (IP Hash)

  • 설명: 클라이언트의 IP 주소를 해싱하여 특정 서버에 매핑하는 방식입니다. 같은 클라이언트가 항상 동일한 서버로 연결되도록 보장할 수 있습니다.
  • 장점: 세션 지속성(Sticky Session)을 유지할 수 있음.
  • 단점: 해시 충돌 시 특정 서버에 트래픽이 집중될 수 있음.
import hashlib

class IPHashLoadBalancer:
    def __init__(self, servers):
        """
        서버 리스트를 초기화합니다.
        
        :param servers: 트래픽을 분산할 서버들의 리스트
        """
        self.servers = servers

    def get_server(self, client_ip):
        """
        클라이언트의 IP 주소를 해싱하여 특정 서버를 선택합니다.
        
        :param client_ip: 클라이언트의 IP 주소
        :return: 선택된 서버
        """
        # IP 주소를 해싱하여 해시 값을 구함
        hash_value = int(hashlib.md5(client_ip.encode()).hexdigest(), 16)
        # 서버 리스트의 길이로 해시 값을 나눈 나머지를 인덱스로 사용
        server_index = hash_value % len(self.servers)
        return self.servers[server_index]

# 서버 리스트 정의
servers = ["Server-1", "Server-2", "Server-3"]

# 로드밸런서 인스턴스 생성
load_balancer = IPHashLoadBalancer(servers)

# 클라이언트 IP 주소 리스트
client_ips = ["192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4", "10.0.0.1"]

# 각 클라이언트 IP에 대한 서버 매핑 결과 출력
for ip in client_ips:
    server = load_balancer.get_server(ip)
    print(f"Client IP {ip} is handled by {server}")
  • "192.168.1.1"이라는 IP 주소를 MD5로 해싱하면 "c0a80101b45f35e097a0c0f7a6d4b5c1"와 같은 해시 문자열이 생성됩니다.
  • 이 문자열을 16진수로 취급하여 정수로 변환하면 16진수의 각 자릿값을 조합하여 큰 정수가 됩니다.

5. 가중 최소 연결 (Weighted Least Connections)

  • 설명: 최소 연결 알고리즘과 유사하지만, 서버의 가중치를 고려하여 연결 수를 조정하는 방식입니다.
  • 장점: 서버의 처리 능력과 현재 부하를 고려한 분배가 가능.
  • 단점: 설정 및 모니터링이 복잡할 수 있음.
class WeightedLeastConnectionsLoadBalancer:
    def __init__(self, servers_with_weights):
        """
        서버와 가중치 정보를 초기화하고, 각 서버의 연결 수를 0으로 초기화합니다.
        
        :param servers_with_weights: (서버, 가중치) 형태의 리스트
        """
        self.servers = {server: {"weight": weight, "connections": 0} for server, weight in servers_with_weights}

    def get_next_server(self):
        """
        가중치 기반 최소 연결 알고리즘을 통해 다음 서버를 선택합니다.
        
        :return: 선택된 서버
        """
        # 가중치를 고려한 연결 수를 계산하고, 최소 값의 서버를 찾음
        def weighted_connections(server):
            weight = self.servers[server]["weight"]
            connections = self.servers[server]["connections"]
            return connections / weight

        # 가중치를 고려한 연결 수가 가장 적은 서버 선택
        selected_server = min(self.servers, key=weighted_connections)
        # 선택된 서버의 연결 수 증가
        self.servers[selected_server]["connections"] += 1
        return selected_server

    def release_connection(self, server):
        """
        특정 서버의 연결 수를 감소시킵니다.
        
        :param server: 연결을 해제할 서버
        """
        if self.servers[server]["connections"] > 0:
            self.servers[server]["connections"] -= 1

    def display_connections(self):
        """
        현재 각 서버의 연결 수와 가중치를 출력합니다.
        """
        for server, info in self.servers.items():
            print(f"{server}: {info['connections']} connections (Weight: {info['weight']})")

# 서버와 가중치 정보 (서버, 가중치) 형태의 리스트
servers_with_weights = [("Server-1", 5), ("Server-2", 3), ("Server-3", 1)]

# 로드밸런서 인스턴스 생성
load_balancer = WeightedLeastConnectionsLoadBalancer(servers_with_weights)

# 10개의 요청을 순차적으로 분배
for i in range(10):
    server = load_balancer.get_next_server()
    print(f"Request {i + 1} is handled by {server}")

# 현재 각 서버의 연결 수를 표시
load_balancer.display_connections()

# 일부 서버에서 연결을 해제
load_balancer.release_connection("Server-1")
load_balancer.release_connection("Server-2")

# 연결 해제 후의 상태를 표시
print("\\nAfter releasing some connections:")
load_balancer.display_connections()

6. 랜덤 선택 (Random Selection)

  • 설명: 서버를 무작위로 선택하여 요청을 분배하는 방식입니다.
  • 장점: 설정이 간단함.
  • 단점: 부하 분배의 효율성이 떨어질 수 있음.
import random

class RandomLoadBalancer:
    def __init__(self, servers):
        """
        서버 리스트를 초기화합니다.
        
        :param servers: 트래픽을 분산할 서버들의 리스트
        """
        self.servers = servers

    def get_next_server(self):
        """
        랜덤하게 다음 서버를 선택합니다.
        
        :return: 선택된 서버
        """
        return random.choice(self.servers)

# 서버 리스트 정의
servers = ["Server-1", "Server-2", "Server-3", "Server-4"]

# 로드밸런서 인스턴스 생성
load_balancer = RandomLoadBalancer(servers)

# 10개의 요청을 랜덤하게 분배
for i in range(10):
    server = load_balancer.get_next_server()
    print(f"Request {i + 1} is handled by {server}")

7. URL 해시 (URL Hash)

  • 설명: 요청 URL을 해싱하여 서버에 매핑하는 방식으로, 특정 URL이 항상 동일한 서버에 연결되도록 합니다.
  • 장점: 특정 서비스나 자원을 특정 서버로 고정할 수 있음.
  • 단점: 특정 서버에 트래픽이 집중될 가능성이 있음.
import hashlib

class URLHashLoadBalancer:
    def __init__(self, servers):
        """
        서버 리스트를 초기화합니다.
        
        :param servers: 트래픽을 분산할 서버들의 리스트
        """
        self.servers = servers

    def get_server(self, url):
        """
        URL을 해싱하여 특정 서버를 선택합니다.
        
        :param url: 요청된 URL 경로
        :return: 선택된 서버
        """
        # URL을 해싱하여 해시 값을 구함
        hash_value = int(hashlib.md5(url.encode()).hexdigest(), 16)
        # 서버 리스트의 길이로 해시 값을 나눈 나머지를 인덱스로 사용
        server_index = hash_value % len(self.servers)
        return self.servers[server_index]

# 서버 리스트 정의
servers = ["Server-1", "Server-2", "Server-3", "Server-4"]

# 로드밸런서 인스턴스 생성
load_balancer = URLHashLoadBalancer(servers)

# 요청된 URL 리스트
urls = ["/home", "/about", "/contact", "/products", "/services", "/home", "/contact"]

# 각 URL에 대한 서버 매핑 결과 출력
for i, url in enumerate(urls, 1):
    server = load_balancer.get_server(url)
    print(f"Request {i} for URL '{url}' is handled by {server}")

로드밸런싱 알고리즘의 선택은 시스템 구조, 서버의 처리 능력, 트래픽의 특성에 따라 달라질 수 있습니다. 예를 들어, 사용자가 많은 웹 애플리케이션에서는 세션 지속성이 중요할 수 있으며, 데이터베이스 서버 간의 부하 분배에서는 최소 연결 기반의 알고리즘이 더 효과적일 수 있습니다.