לדלג לתוכן

4.2 איזון עומס הרצאה

איזון עומס - Load Balancing

כשיש לנו כמה שרתים, צריך מישהו שיחליט לאיזה שרת לשלוח כל בקשה. זה ה-Load Balancer.


מה זה Load Balancer?

Load Balancer הוא רכיב שיושב בין הלקוחות לשרתים ומפזר בקשות.

לקוחות → [Load Balancer] → שרת 1
                         → שרת 2
                         → שרת 3

מטרות:
1. פיזור עומס שווה בין שרתים
2. ביצוע health checks - הסרת שרתים שנפלו
3. SSL termination - פענוח HTTPS פעם אחת


אלגוריתמי פיזור

Round Robin

שולחים בקשות לשרתים לפי תור - 1, 2, 3, 1, 2, 3...

class RoundRobinLoadBalancer:
    def __init__(self, servers: list[str]):
        self.servers = servers
        self.current = 0

    def get_server(self) -> str:
        server = self.servers[self.current]
        self.current = (self.current + 1) % len(self.servers)
        return server


lb = RoundRobinLoadBalancer(["server1:8000", "server2:8000", "server3:8000"])
print(lb.get_server())  # server1
print(lb.get_server())  # server2
print(lb.get_server())  # server3
print(lb.get_server())  # server1 - מתחיל מחדש

מתי: כשכל הבקשות דומות בכבדותן.

חסרון: לא לוקח בחשבון שבקשה אחת יכולה לקחת 0.1ms ואחרת 10 שניות.

Weighted Round Robin

כמו Round Robin, אבל שרתים חזקים יותר מקבלים יותר בקשות.

class WeightedRoundRobinLoadBalancer:
    def __init__(self, servers: list[tuple[str, int]]):
        # servers: [(address, weight), ...]
        self.pool = []
        for address, weight in servers:
            self.pool.extend([address] * weight)
        self.current = 0

    def get_server(self) -> str:
        server = self.pool[self.current]
        self.current = (self.current + 1) % len(self.pool)
        return server


lb = WeightedRoundRobinLoadBalancer([
    ("server1:8000", 3),  # שרת חזק - 3x בקשות
    ("server2:8000", 1),  # שרת חלש - 1x בקשות
])
# pool: [server1, server1, server1, server2, server1, server1, server1, server2, ...]

Least Connections

שולחים לשרת עם הכי פחות חיבורים פעילים.

class LeastConnectionsLoadBalancer:
    def __init__(self, servers: list[str]):
        self.connections = {server: 0 for server in servers}

    def get_server(self) -> str:
        return min(self.connections, key=self.connections.get)

    def add_connection(self, server: str):
        self.connections[server] += 1

    def remove_connection(self, server: str):
        self.connections[server] -= 1

מתי: כשבקשות שונות בזמן הטיפול. שרת עסוק לא מקבל בקשות חדשות.

IP Hash

שולחים תמיד לאותו שרת לפי כתובת ה-IP של הלקוח.

class IPHashLoadBalancer:
    def __init__(self, servers: list[str]):
        self.servers = servers

    def get_server(self, client_ip: str) -> str:
        index = hash(client_ip) % len(self.servers)
        return self.servers[index]

מתי: כשצריך session affinity - אותו לקוח צריך תמיד להגיע לאותו שרת (למשל session בזיכרון השרת).


Layer 4 לעומת Layer 7

Layer 4 - Transport Layer

Load Balancer שרואה רק IP ו-port. לא מבין את תוכן הבקשה.

TCP: 192.168.1.100:443 → server1:8000
                       → server2:8000

מהיר מאוד, אבל לא יכול לנתב לפי URL, headers, cookies.

Layer 7 - Application Layer

Load Balancer שמבין HTTP. יכול לנתב לפי כל פרמטר של הבקשה.

GET /api/tasks    → task-service-cluster
GET /api/users    → user-service-cluster
GET /api/reports  → report-service-cluster (שרת חזק יותר)

Nginx ו-HAProxy הם load balancers פופולריים. AWS Application Load Balancer הוא Layer 7.


Health Checks

Load Balancer צריך לדעת אילו שרתים פעילים.

import requests
import threading
import time


class HealthAwareLoadBalancer:
    def __init__(self, servers: list[str]):
        self.all_servers = servers
        self.healthy_servers = list(servers)
        self.current = 0
        self._start_health_checks()

    def _check_health(self, server: str) -> bool:
        try:
            response = requests.get(f"http://{server}/health", timeout=2)
            return response.status_code == 200
        except Exception:
            return False

    def _health_check_loop(self):
        while True:
            for server in self.all_servers:
                is_healthy = self._check_health(server)
                if is_healthy and server not in self.healthy_servers:
                    print(f"[LB] שרת {server} חזר לפעולה")
                    self.healthy_servers.append(server)
                elif not is_healthy and server in self.healthy_servers:
                    print(f"[LB] שרת {server} הוסר (לא בריא)")
                    self.healthy_servers.remove(server)
            time.sleep(10)

    def _start_health_checks(self):
        thread = threading.Thread(target=self._health_check_loop, daemon=True)
        thread.start()

    def get_server(self) -> str:
        if not self.healthy_servers:
            raise Exception("אין שרתים זמינים!")
        server = self.healthy_servers[self.current % len(self.healthy_servers)]
        self.current += 1
        return server

Consistent Hashing - Hash עקבי

הבעיה: כשמוסיפים או מסירים שרת cache, hash רגיל יגרום ל-invalidation של רוב ה-cache.

3 שרתים: key % 3 → 0, 1, 2
4 שרתים: key % 4 → 0, 1, 2, 3

מעבר מ-3 ל-4 שרתים: 75% מהkeys ישנו את השרת שלהם.

הפתרון: Consistent Hashing - מניח את השרתים ו-keys על "טבעת":

       server1
      /        \
  key3          key1
 /                  \
server3            server2
 \                  /
  key4          key2
      \        /
       (server1)

כל key הולך לשרת הראשון שנמצא "ימינה" ממנו בטבעת.

תוצאה: הוספה/הסרה של שרת אחד מחייבת re-distribution של ~1/N מה-keys בלבד.

Consistent Hashing משמש ב-AWS DynamoDB, Apache Cassandra, ו-Akamai CDN.


סיכום

אלגוריתם מתאים ל
Round Robin בקשות דומות בכובד
Weighted Round Robin שרתים בעלי כוח שונה
Least Connections בקשות שונות מאוד בזמן
IP Hash session affinity
Consistent Hashing cache distribution