Load balancer amaliyoti: HAProxy bilan L4 va L7 misollar

Assalamu Alaykum bugun load balancerlar bilan real amaliyot qilib tekshirib ko'ramiz (buni iloji boricha real loyihalarga yaqinlashtirishga harakat qildim) .

Load balancer haqida to'liq tushunchaga ega bo'lmoqchi bo'lsangiz ushbu maqolani o'qib chiqing . Load balancer nima uchun kerak ?

Maqolani o'qib chiqqan bo'lsangiz misollarni boshlaymiz.

Bugun ushbu 4 xil load balancerni ishlatib ko'ramiz (2 ta layer uchun va 2 ta algoritmlar uchun) :

Layer 4 Pass-Through Load balancer misoli

Birinchi bo'lib oddiy server faylni yaratmiz :

const http = require('http');
const port = process.env.PORT || 3000;
const serverId = process.env.SERVER_ID || 'Server';
const server = http.createServer((req, res) => {
  const clientIP = req.socket.remoteAddress;
  console.log(`Request received on ${serverId}: client IP = ${clientIP}`);
  res.end(`Hello from ${serverId}!\n`);
});
server.listen(port, () => {
  console.log(`${serverId} listening on port ${port}`);
});

Endi server uchun Docker file yaratamiz :

FROM node:lts-slim
WORKDIR /app
COPY server.js ./
ENV PORT=3000
CMD ["node", "server.js"]

Endi HAProxy uchun config fayl yozamiz . Bu faylda HAProxy 4000 portni eshitib trafikni 3 ta serverga roundrobin dan foydalangan holda bo'lib beradi :

defaults
  mode tcp
  timeout connect 5s
  timeout client 50s
  timeout server 50s
frontend fe_tcp
  bind :4000 transparent
  default_backend be_tcp
backend be_tcp
  mode tcp
  balance roundrobin
  server s1 127.0.0.1:3001 source 0.0.0.0 usesrc clientip check
  server s2 127.0.0.1:3002 source 0.0.0.0 usesrc clientip check
  server s3 127.0.0.1:3003 source 0.0.0.0 usesrc clientip check

Endi bularni birgalikda ishga tushurish uchun docker-compose.yml faylni yozamiz:

version: "3.8"
services:
  haproxy:
    image: haproxy:latest
    network_mode: host
    cap_add:
      - NET_ADMIN
    sysctls:
      - net.ipv4.ip_nonlocal_bind=1
    volumes:
      - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
  backend1:
    build: .
    environment:
      - PORT=3001
      - SERVER_ID=Server1
    network_mode: host
  backend2:
    build: .
    environment:
      - PORT=3002
      - SERVER_ID=Server2
    network_mode: host
  backend3:
    build: .
    environment:
      - PORT=3003
      - SERVER_ID=Server3
    network_mode: host

bularni test qilish uchun esa so'rov yuboruvchi script yozamiz :

const http = require("http");
const TOTAL_REQUESTS = 1000;
const LB_URL = "http://localhost:4000";
let completedRequests = 0;
const responseCounts = {};
for (let i = 0; i < TOTAL_REQUESTS; i++) {
  http
    .get(LB_URL, (res) => {
      let body = "";
      res.on("data", (chunk) => {
        body += chunk;
      });
      res.on("end", () => {
        const match = body.match(/Server\d+/);
        if (match) {
          const serverName = match[0];
          responseCounts[serverName] = (responseCounts[serverName] || 0) + 1;
        } else {
          responseCounts["unknown"] = (responseCounts["unknown"] || 0) + 1;
        }
        completedRequests++;
        if (completedRequests === TOTAL_REQUESTS) {
          console.log("Responses received from servers:", responseCounts);
        }
      });
    })
    .on("error", (err) => {
      console.error("Request error:", err.message);
      completedRequests++;
      if (completedRequests === TOTAL_REQUESTS) {
        console.log("Responses received from servers:", responseCounts);
      }
    });
}

Note: Buni Linux yoki WSLda tekshirib ko'rish mumkin (chunki iptables o'zgaritishda Macda muammolar bo'ldi)

iptable ruxsatlarini berish uchun buyruq :

sudo modprobe xt_TPROXY
sudo modprobe xt_socket
sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -t mangle -N DIVERT
sudo iptables -t mangle -F DIVERT
sudo iptables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT
sudo iptables -t mangle -A DIVERT -j MARK --set-mark 1
sudo iptables -t mangle -A DIVERT -j ACCEPT
sudo ip rule add fwmark 1 lookup 100
sudo ip route add local 0.0.0.0/0 dev lo table 100

Unday holatda WSLga kirib unga nodeva haproxyo'rnatasiz , serverlani ishga tushurasiz va bu buyruq orqali haproxy ishga tushadi

sudo haproxy -f haproxy.cfg //haproxyni ishga tushurasiz
Load balancer ishga tushdi
Load balancer ishga tushdi
test script
test script

Natijaga qarasak client ip adress bir xil ammo bu localhost bo'lganligi uchun real holatda har xil ip addresslar bo'ladi.

Layer 4 proxy mode misoli

Bu yerda server va docker fayllar bir xil bo'ladi . Faqat HAProxy konfiguratsiyasi biroz o'zgaradi.

defaults
  mode tcp
  timeout connect 5s
  timeout client 30s
  timeout server 30s
frontend fe_tcp
  bind *:4000
  default_backend be_tcp
backend be_tcp
  balance roundrobin
  server s1 backend1:3000 check
  server s2 backend2:3000 check
  server s3 backend3:3000 check

va docker-compose.yml fayl minimal o'zgarish bilan :

version: '3.8'
services:
  haproxy:
    image: haproxy:latest
    container_name: haproxy-l4-proxy
    ports:
      - "4000:4000"
    volumes:
      - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
    depends_on:
      - backend1
      - backend2
      - backend3
  backend1:
    build: .
    container_name: backend1
    environment:
      - PORT=3000
      - SERVER_ID=Server1
  backend2:
    build: .
    container_name: backend2
    environment:
      - PORT=3000
      - SERVER_ID=Server2
  backend3:
    build: .
    container_name: backend3
    environment:
      - PORT=3000
      - SERVER_ID=Server3

Va endi terminalni ochib shu papkada bu buyruqni ishga tushuramiz :

docker-compose up --build
layer 4 proxy mode
layer 4 proxy mode

Ko'rib turganingizdek ip address uchun load balancer ip address kelmoqda va bu proxy uslubida ishlamoqda va har bir server teng miqdorda so'rov bilan ishlamoqda.

Layer 7 Least connection algoritmi bilan misol

Birinchi bo'lib server fayllarga qo'shimcha sekinlashish uchun setTimeout qo'yamiz (so'rovlar har xil vaqtda javob berilib aloqa kanallari soni ko'payishi uchun) . Vaqt taxminiy tanlanadi doim bir xil emas . Server fayl :

const http = require("http");
const port = process.env.PORT || 3000;
const serverId = process.env.SERVER_ID || "Server";
const MAX_DELAY = process.env.MAX_DELAY
  ? parseInt(process.env.MAX_DELAY, 10)
  : 1000;
const server = http.createServer((req, res) => {
  const delay = MAX_DELAY ? Math.floor(Math.random() * MAX_DELAY) : 0;
  setTimeout(() => {
    res.end(`Hello from ${serverId}! (response delayed ${delay}ms)\n`);
  }, delay);
});
server.listen(port, () => {
  console.log(`${serverId} listening on port ${port}`);
});

Docker fayl o'zgarmaydi, HAProxy konfiguratsiyasi o'zgaradi chunki algoritm va layer o'zgarmoqda hamda , qaysi server tanlangani va o'sha paytda nechta connection borligini loglarga ham chiqaramiz.

haproxy config fayl( haproxy.cfg ):

global
  log stdout format raw local0
defaults
  mode http
  log global
  option httplog
  timeout connect 5s
  timeout client 30s
  timeout server 30s
  log-format "%ci:%cp -> %b/%s | srv_conn=%sc backend_conn=%bc"
frontend fe_http
  bind *:4000
  default_backend be_http
backend be_http
  balance leastconn
  option http-server-close
  server s1 backend1:3000 check
  server s2 backend2:3000 check
  server s3 backend3:3000 check

docker-compose.yml fayl :

version: "3.8"
services:
  haproxy:
    image: haproxy:latest
    container_name: haproxy-l7-leastconn-log
    ports:
      - "4000:4000"
    volumes:
      - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
    depends_on:
      - backend1
      - backend2
      - backend3
  backend1:
    build: .
    container_name: backend1
    environment:
      - PORT=3000
      - SERVER_ID=Server1
      - MAX_DELAY=1000
  backend2:
    build: .
    container_name: backend2
    environment:
      - PORT=3000
      - SERVER_ID=Server2
      - MAX_DELAY=1000
  backend3:
    build: .
    container_name: backend3
    environment:
      - PORT=3000
      - SERVER_ID=Server3
      - MAX_DELAY=1000

endi docker-compose up -build buyrug'ini beramiz va testni ishga tushuramiz.

layer 7 least connection
layer 7 least connection

Ko'rib turganingizdek so'rovlar bir biriga yaqin ammo har xil aloqa kanali bo'lganiligi uchun sal farq qilmoqda

haproxy-l7-leastconn-log | 192.168.65.1:33616 -> be_http/s1 | srv_conn=2 backend_conn=13

HAProxy loglari esa so'rov qaysi serverga yuborilganini be_http/s1 bu server 1 va unda 2 ta aloqa kanali umumiy backend bilan esa 13 ta aloqa kanali mavjud.

Layer 7 Least response time algoritmi bilan misol

Bu uslubda endi Load balancerni ham nodejsda yozamiz murakkabligi va bashqarish oson bo'lishi hamda API gatewaysifatida ko'rishimiz mumkin.

load-balancer.js

const http = require("http");
const backends = [
  { name: "Server1", url: "http://backend1:3000" },
  { name: "Server2", url: "http://backend2:3000" },
  { name: "Server3", url: "http://backend3:3000" },
];
const totalTime = new Array(backends.length).fill(0);
const reqCount = new Array(backends.length).fill(0);
function getAvg(i) {
  if (reqCount[i] === 0) return null;
  return totalTime[i] / reqCount[i];
}
function pickServer() {
  const untried = backends.findIndex((_, i) => reqCount[i] === 0);
  if (untried !== -1) return untried;
  let best = 0;
  let bestAvg = getAvg(0);
  for (let i = 1; i < backends.length; i++) {
    const avg = getAvg(i);
    if (avg < bestAvg) {
      bestAvg = avg;
      best = i;
    }
  }
  return best;
}
function statsLine() {
  return backends
    .map((b, i) => {
      const avg = getAvg(i);
      return (
        `${b.name}: count=${reqCount[i]}, ` +
        `totalTime=${totalTime[i]}ms, ` +
        `avg=${avg !== null ? avg.toFixed(2) + "ms" : "N/A"}`
      );
    })
    .join(" | ");
}
const lb = http.createServer((clientReq, clientRes) => {
  const idx = pickServer();
  const target = backends[idx];
  console.log(
    `\n[REQUEST] Choosing ${target.name} ` +
    `(lowest avg so far)\n Stats BEFORE: ${statsLine()}`,
  );
  const start = Date.now();
  const options = {
    hostname: new URL(target.url).hostname,
    port: new URL(target.url).port || 80,
    path: clientReq.url,
    method: clientReq.method,
    headers: clientReq.headers,
  };
  const proxyReq = http.request(options, (proxyRes) => {
    let body = "";
    proxyRes.on("data", (chunk) => (body += chunk));
    proxyRes.on("end", () => {
      const duration = Date.now() - start;
      totalTime[idx] += duration;
      reqCount[idx] += 1;
      console.log(
        `[RESPONSE] ${target.name} replied in ${duration}ms\n` +
        ` Stats AFTER: ${statsLine()}`,
      );
      clientRes.writeHead(proxyRes.statusCode, proxyRes.headers);
      clientRes.end(body);
    });
  });
  proxyReq.on("error", (err) => {
    console.error(`[ERROR] ${target.name}: ${err.message}`);
    clientRes.writeHead(502);
    clientRes.end("Bad Gateway");
  });
  clientReq.pipe(proxyReq);
});
lb.listen(4000, () => console.log("Fastest-response LB listening on :4000\n"));

docker-compose.yml fayl ham node uchun o'zgaradi

docker-compose.yml :

version: "3.8"
services:
  lb_fastest:
    build:
      context: ./load-balancer
      dockerfile: Dockerfile
    container_name: l7_nodejs_lb_least_response_time
    environment:
      - LB_PORT=4000
    ports:
      - "4000:4000"
    depends_on:
      - backend1
      - backend2
      - backend3
  backend1:
    build: .
    container_name: backend1
    environment:
      - PORT=3000
      - SERVER_ID=Server1
      - MAX_DELAY=1000
  backend2:
    build: .
    container_name: backend2
    environment:
      - PORT=3000
      - SERVER_ID=Server2
      - MAX_DELAY=1000
  backend3:
    build: .
    container_name: backend3
    environment:
      - PORT=3000
      - SERVER_ID=Server3
      - MAX_DELAY=1000
layer 7 least response time natjisa
layer 7 least response time natjisa

Ko'rib turganingizdek_server3 tez javob berganligi uchun ko'p so'rovlar_shu serverga yo'naltirilgan .Loglarda esa qaysi server nechta so'rov olgani va o'rtacha ketgan vaqti ko'rsatilgan ( bu to'liqroq tushunish uchun yordam beradi) .

Barcha kodlarni bu yerdan topishingiz mumkin. Repo link

Xulosa

Xulosa qilib aytadigan bo'lsak qanchalik aqlli uslubdan foydalanmoqchi va boshqaruv bizda bo'lishini xohlasak load balancerni o'zimiz yozishimizga to'g'ri keladi (yoki qimmatlarini olib ishlatishga).