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) :
L4 Pass-Through: Bu uchun HAProxy ning transparent modeni yoqqan holatda ishlatamiz (WSLda).
L4 Proxy:Bu uchun HAProxy ning TCP modeni yoqqan holatda docker compose bilan ishlatamiz.
L7 Least Connections: HAProxydan foydalangan holda so'rov kelgan payt qaysi server tanlashini loglarda ko'ramiz.
L7 Least Response Time_:_ NodeJsda proxy load balancer quramiz va undan foydalangan holda serverlarga so'rovlar jo'natamiz.
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 checkEndi 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: hostbularni 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 100Unday 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

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 checkva 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=Server3Va endi terminalni ochib shu papkada bu buyruqni ishga tushuramiz :
docker-compose up --build
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 checkdocker-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=1000endi docker-compose up -build buyrug'ini beramiz va testni ishga tushuramiz.

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=13HAProxy 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
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).