লোড ব্যালেন্সিং — অ্যালগরিদম ও কৌশল
এই পাঠে যা শিখবেন
- লোড ব্যালেন্সার কী এবং কেন হরাইজন্টাল স্কেলিং-এর জন্য অপরিহার্য
- Round Robin, Weighted Round Robin, Least Connections, IP Hash অ্যালগরিদমের কাজ ও তুলনা
- L4 বনাম L7 লোড ব্যালেন্সারের পার্থক্য
- হেলথ চেক কীভাবে অসুস্থ সার্ভার রোটেশন থেকে বাদ দেয়
- Python দিয়ে Round Robin ও Least Connections বাস্তবায়ন করে ফলাফল তুলনা করা
১ · লোড ব্যালেন্সারের কাজ
লোড ব্যালেন্সারLoad Balancerক্লায়েন্ট ও একাধিক ব্যাকএন্ড সার্ভারের মাঝে বসা একটি কম্পোনেন্ট, যা ইনকামিং রিকোয়েস্ট একটি নির্দিষ্ট অ্যালগরিদম অনুযায়ী সার্ভারগুলোর মধ্যে বিতরণ করে। L10-এ আমরা দেখেছি হরাইজন্টাল স্কেলিং মানে একাধিক মেশিন যোগ করা — কিন্তু শুধু মেশিন যোগ করলেই কাজ হয় না, কেউ একজনকে ঠিক করতে হয় কোন রিকোয়েস্ট কোন মেশিনে যাবে। এই কাজটাই করে লোড ব্যালেন্সার — L01-এর ফ্লো-ডায়াগ্রামে ক্লায়েন্ট ও অ্যাপ সার্ভারের মাঝে যে বক্সটি ছিল, সেটিই এই লোড ব্যালেন্সার।
২ · মূল লোড ব্যালেন্সিং অ্যালগরিদম
প্রতিটি রিকোয়েস্ট পালাক্রমে পরের সার্ভারে যায় — সহজ, সব সার্ভার সমান ক্ষমতাসম্পন্ন হলে ভালো।
শক্তিশালী সার্ভার বেশি রিকোয়েস্ট পায় (একটি ওজন/weight অনুপাতে) — অসম ক্ষমতার সার্ভারের জন্য।
যে সার্ভারে এই মুহূর্তে সবচেয়ে কম active connection আছে, তাকে পরের রিকোয়েস্ট দেওয়া হয় — অসম রিকোয়েস্ট-সময়ের ক্ষেত্রে ভালো।
ক্লায়েন্টের IP হ্যাশ করে সবসময় একই সার্ভারে পাঠানো হয় — session stickiness দরকার হলে ব্যবহৃত হয়।
Round Robin সবচেয়ে সরল — প্রতিটি নতুন রিকোয়েস্ট তালিকার পরের সার্ভারে যায়, শেষে পৌঁছালে আবার শুরু থেকে (L05-এর round-robin DNS-এর মতোই ধারণা, কিন্তু এখানে প্রতিটি HTTP রিকোয়েস্টের স্তরে)। কিন্তু এটি ধরে নেয় প্রতিটি রিকোয়েস্ট প্রায় সমান খরচের — বাস্তবে কিছু রিকোয়েস্ট (যেমন একটি ভারী রিপোর্ট জেনারেশন) অন্যদের চেয়ে অনেক বেশি সময় নিতে পারে। এই ক্ষেত্রে Least Connections ভালো কাজ করে — এটি প্রতিটি সার্ভারের বর্তমান সক্রিয় কানেকশন সংখ্যা ট্র্যাক করে এবং সবচেয়ে কম-লোডেড সার্ভারকে পরের রিকোয়েস্ট দেয়, যার ফলে Round Robin-এর চেয়ে প্রকৃত লোড আরও সমানভাবে ভাগ হতে পারে।
৩ · L4 বনাম L7 লোড ব্যালেন্সার
L4 (ট্রান্সপোর্ট লেয়ার) লোড ব্যালেন্সার শুধু IP অ্যাড্রেস ও পোর্ট নম্বর দেখে রাউটিং সিদ্ধান্ত নেয় —
এটি HTTP-এর ভেতরের কনটেন্ট বোঝে না, শুধু নেটওয়ার্ক প্যাকেট ফরোয়ার্ড করে। এটি খুবই দ্রুত কিন্তু অ-নমনীয়।
L7 (অ্যাপ্লিকেশন লেয়ার) লোড ব্যালেন্সার পুরো HTTP রিকোয়েস্ট পড়ে — হেডার, URL পাথ, কুকি — এবং সেই
অনুযায়ী রাউট করতে পারে (যেমন /api/* এক সেট সার্ভারে, /images/* অন্য সেটে পাঠানো)। এই
নমনীয়তার মূল্য হলো সামান্য বাড়তি প্রসেসিং ওভারহেড, কারণ প্রতিটি রিকোয়েস্টের কনটেন্ট পার্স করতে হয়।
৪ · হেলথ চেক — অসুস্থ সার্ভার বাদ দেওয়া
লোড ব্যালেন্সার নিয়মিত বিরতিতে প্রতিটি ব্যাকএন্ড সার্ভারে একটি ছোট "পিং" (হেলথ চেক রিকোয়েস্ট) পাঠায়। কোনো সার্ভার সাড়া না দিলে বা এরর ফেরত দিলে, লোড ব্যালেন্সার সেটিকে সাময়িকভাবে রোটেশন থেকে বাদ দেয় — নতুন রিকোয়েস্ট আর সেই সার্ভারে পাঠানো হয় না, যতক্ষণ না পরবর্তী হেলথ চেকে সে আবার সুস্থ প্রমাণিত হয়। এভাবে একটি ক্র্যাশড বা ওভারলোডেড সার্ভার পুরো সিস্টেমকে প্রভাবিত না করে স্বয়ংক্রিয়ভাবে সরে যায় — L37-এ ফেইলওভার প্রসঙ্গে আমরা এই ধারণায় আবার ফিরে আসব।
Round Robin জিজ্ঞাসা করে "কার পালা?" — Least Connections জিজ্ঞাসা করে "কে সবচেয়ে ফাঁকা?" — IP Hash জিজ্ঞাসা করে "এই ক্লায়েন্ট আগে কোথায় গিয়েছিল?" প্রতিটি প্রশ্নের উত্তর ভিন্ন পরিস্থিতিতে ভিন্ন সুবিধা দেয়।
৫ · কোড দিয়ে দেখা — Round Robin বনাম Least Connections
নিচে একটি ৩-সার্ভার সেটআপে দুটি অ্যালগরিদম বাস্তবায়ন করা হয়েছে। লক্ষ্য করুন — সার্ভারগুলোর শুরুর কানেকশন সংখ্যা অসম হওয়ায় (কিছু সার্ভার আগে থেকেই বেশি ব্যস্ত), Least Connections প্রথম কয়েকটি রিকোয়েস্ট Round Robin থেকে ভিন্নভাবে রাউট করে।
servers = ["S1", "S2", "S3"]
def round_robin(servers, num_requests):
assignments = []
for i in range(num_requests):
assignments.append(servers[i % len(servers)])
return assignments
def least_connections(servers, initial_connections, num_requests):
connections = dict(initial_connections)
assignments = []
for _ in range(num_requests):
# সবচেয়ে কম কানেকশনের সার্ভার বাছাই, টাই হলে নাম অনুযায়ী নির্ণায়ক ক্রম
chosen = min(servers, key=lambda s: (connections[s], s))
assignments.append(chosen)
connections[chosen] += 1
return assignments, connections
print("=== Round Robin (৬টি রিকোয়েস্ট) ===")
rr = round_robin(servers, 6)
for i, s in enumerate(rr, 1):
print(f" রিকোয়েস্ট {i} -> {s}")
initial_connections = {"S1": 3, "S2": 1, "S3": 1} # S1 আগে থেকেই বেশি ব্যস্ত
print(f"\n=== Least Connections (শুরুর কানেকশন: {initial_connections}) ===")
lc, final_connections = least_connections(servers, initial_connections, 6)
for i, s in enumerate(lc, 1):
print(f" রিকোয়েস্ট {i} -> {s}")
print(f"\nRound Robin ফলাফল: {rr}")
print(f"Least Connections ফলাফল: {lc}")
print(f"শেষে প্রতিটি সার্ভারের কানেকশন সংখ্যা: {final_connections}")
print(f"\nদুই অ্যালগরিদম কি একই ক্রমে রাউট করল? {rr == lc}")
লোড ব্যালেন্সিং অ্যালগরিদম বেছে নেওয়া নির্ভর করে ট্র্যাফিকের প্রকৃতির উপর — সব রিকোয়েস্ট প্রায় সমান হলে Round Robin যথেষ্ট; রিকোয়েস্টের সময়কাল অসম হলে Least Connections ভালো; নির্দিষ্ট ক্লায়েন্টকে নির্দিষ্ট সার্ভারে রাখা দরকার হলে (যেমন WebSocket-এর sticky session, L08) IP Hash দরকার হতে পারে। কোনো একটি অ্যালগরিদম সব পরিস্থিতিতে সেরা নয়।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ একটি WebSocket-ভিত্তিক চ্যাট সার্ভিসে (L08) IP Hash কেন Round Robin-এর চেয়ে বেশি প্রাসঙ্গিক হতে পারে?
L08-এ আমরা দেখেছি WebSocket কানেকশন দীর্ঘস্থায়ী ও স্টেটফুল — একবার একটি নির্দিষ্ট সার্ভারে কানেকশন খুললে, সেই কানেকশনের ডেটা সেই সার্ভারের মেমরিতেই থাকে। যদি লোড ব্যালেন্সার Round Robin ব্যবহার করে প্রতিটি নতুন রিকোয়েস্ট ভিন্ন সার্ভারে পাঠায়, একই ক্লায়েন্টের পুনঃসংযোগ (reconnect) বা পরবর্তী HTTP কল ভিন্ন সার্ভারে চলে যেতে পারে যেটি তার আসল কানেকশনের কথা জানেই না। IP Hash (বা কুকি-ভিত্তিক stickiness) নিশ্চিত করে একই ক্লায়েন্ট বারবার একই সার্ভারে যাচ্ছে, যা স্টেটফুল কানেকশনের সাথে সামঞ্জস্যপূর্ণ। তবে L08-এই আমরা এটাও দেখেছি pub/sub একটি আরও নমনীয় বিকল্প যা এই নির্ভরতা এড়ায়।
প্র ০২ লোড ব্যালেন্সার নিজেই কি একটি single point of failure (L10) হয়ে উঠতে পারে?
হ্যাঁ, এবং এটি একটি গুরুত্বপূর্ণ পর্যবেক্ষণ — L10-এ আমরা যে সমস্যা এড়াতে হরাইজন্টাল স্কেলিং করেছি (একটি সার্ভার ডাউন হলে পুরো সিস্টেম বন্ধ), সেই একই সমস্যা যদি লোড ব্যালেন্সার নিজেই একক মেশিন হয় তাহলে ফিরে আসে — লোড ব্যালেন্সার ডাউন হলে কোনো রিকোয়েস্টই কোনো ব্যাকএন্ড সার্ভারে পৌঁছাতে পারবে না। বাস্তব সিস্টেমে তাই প্রায়ই একাধিক রিডানডেন্ট লোড ব্যালেন্সার রাখা হয় (সক্রিয়-প্যাসিভ বা সক্রিয়-সক্রিয়, L37-এ বিস্তারিত), অথবা DNS-স্তরে একাধিক এন্ট্রি পয়েন্ট রাখা হয়, যাতে লোড ব্যালেন্সার নিজেও একটি একক ব্যর্থতার বিন্দু না হয়।
প্র ০৩ L7 লোড ব্যালেন্সার L4-এর চেয়ে বেশি "স্মার্ট" হলে, সব সিস্টেমে সবসময় L7 ব্যবহার না করার কারণ কী?
L7 লোড ব্যালেন্সারকে প্রতিটি রিকোয়েস্টের পুরো HTTP কনটেন্ট (হেডার, বডি, পাথ) পার্স করতে হয় রাউটিং সিদ্ধান্ত নেওয়ার আগে — এটি L4-এর তুলনায় বেশি CPU ও সময় লাগায়, কারণ L4 শুধু নেটওয়ার্ক প্যাকেটের IP/পোর্ট দেখেই দ্রুত সিদ্ধান্ত নিতে পারে, বিষয়বস্তু না বুঝেই। যেসব ক্ষেত্রে অতি উচ্চ থ্রুপুট ও ন্যূনতম লেটেন্সি সবচেয়ে গুরুত্বপূর্ণ এবং কনটেন্ট-ভিত্তিক রাউটিং দরকার নেই (যেমন একটি সাধারণ TCP-লেভেল লোড স্প্রেড), সেখানে L4-এর সরলতা ও গতি বেশি মূল্যবান। L7-এর নমনীয়তা তখনই দরকার যখন URL পাথ, হেডার বা কুকির উপর ভিত্তি করে রাউটিং সিদ্ধান্ত নিতে হয়।
অনুশীলন
-
চিন্তা করুন: একটি সিস্টেমে কিছু রিকোয়েস্ট (ছবি আপলোড) অন্যদের (লাইক বাটন) চেয়ে অনেক বেশি সময় নেয়। এই পরিস্থিতিতে Round Robin ব্যবহার করলে কী সমস্যা হতে পারে?
Round Robin অন্ধভাবে পালাক্রমে রাউট করে, তাই একটি সার্ভার একাধিক ভারী "ছবি আপলোড" রিকোয়েস্ট পরপর পেয়ে যেতে পারে যখন অন্য সার্ভার শুধু হালকা "লাইক" রিকোয়েস্ট পাচ্ছে — এতে একটি সার্ভার ওভারলোডেড হয়ে যেতে পারে অন্যগুলো তুলনামূলক ফাঁকা থাকা সত্ত্বেও। Least Connections এই সমস্যা এড়ায় কারণ এটি প্রকৃত সক্রিয় কানেকশন সংখ্যা দেখে সিদ্ধান্ত নেয় — একটি সার্ভার ভারী রিকোয়েস্টে ব্যস্ত থাকলে তার কানেকশন-সংখ্যা বেশি দেখাবে, তাই নতুন রিকোয়েস্ট স্বাভাবিকভাবেই কম-ব্যস্ত সার্ভারে যাবে।
-
কোড এক্সটেন্ড করুন: উপরের কোড সেলে একটি
weighted_round_robin(servers, weights, num_requests)ফাংশন লিখুন যেখানেweights = {"S1": 3, "S2": 1, "S3": 1}— অর্থাৎ S1 প্রতি ৫টির মধ্যে ৩টি রিকোয়েস্ট পাবে।একটি সহজ সমাধান — প্রতিটি সার্ভারকে তার ওজন অনুযায়ী একটি "expanded" তালিকায় repeat করে সেই তালিকার উপর সাধারণ round robin চালানো:
def weighted_round_robin(servers, weights, num_requests): expanded = [] for s in servers: expanded.extend([s] * weights[s]) # S1 তিনবার, S2 একবার, S3 একবার -> মোট ৫ স্লট assignments = [] for i in range(num_requests): assignments.append(expanded[i % len(expanded)]) return assignments weights = {"S1": 3, "S2": 1, "S3": 1} wrr = weighted_round_robin(servers, weights, 10) print(wrr) # expanded = [S1, S1, S1, S2, S3] (৫ স্লট) -> ১০টি রিকোয়েস্ট এই প্যাটার্ন দুইবার পুনরাবৃত্তি করবেএই পদ্ধতিতে প্রতি ৫টি রিকোয়েস্টের মধ্যে S1 ৩ বার, S2 ও S3 প্রতিটি ১ বার করে পাবে — ঠিক ওজন অনুপাত অনুযায়ী।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫১টি পাঠ পরবর্তী পাঠ — Stateless বনাম Stateful সার্ভিস ডিজাইন — এখনই পড়ুন।
- ভার্টিক্যাল বনাম হরাইজন্টাল স্কেলিং পূর্ববর্তী পাঠ হরাইজন্টাল স্কেলিং কেন লোড ব্যালেন্সিং ছাড়া কাজ করে না তা এই পাঠে দেখা হয়েছে।
- Stateless বনাম Stateful সার্ভিস ডিজাইন পরবর্তী পাঠ লোড ব্যালেন্সিং কার্যকর হওয়ার জন্য সার্ভার কেন স্টেটলেস হওয়া দরকার তা বিস্তারিত জানুন।
- সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics ও System Design — সব এক জায়গায়।