সার্ভারলেসের জন্য ডেটাবেস ডিজাইন করার সময় আমাদের মনে সবচেয়ে বড় চ্যালেঞ্জ ছিল পার-রিকোয়েস্ট প্রাইসিং প্রফিটেবলভাবে সাপোর্ট করে এমন ইনফ্রাস্ট্রাকচার বানানো। আমরা মনে করি Upstash এটি অর্জন করেছে। প্রোডাক্ট লঞ্চ করার পর আমরা দেখলাম আরেকটি বড় চ্যালেঞ্জ: ডেটাবেস কানেকশন!
আপনি জানেন, Serverless Functions ০ থেকে অনন্ত স্কেল করে। অর্থাৎ যতক্ষণ আপনার ফাংশনগুলোর ট্রাফিক বেশি হয়, ক্লাউড প্রোভাইডার নতুন কন্টেইনার (ল্যাম্বডা ফাংশন) প্যারালেলে তৈরি করে এবং আপনার ব্যাকএন্ড স্কেল আউট করে। আপনি যদি ফাংশনের ভিতরে নতুন ডেটাবেস কানেকশন তৈরি করেন তবে আপনি দ্রুত আপনার ডেটাবেসের কানেকশন লিমিটে পৌঁছাতে পারেন।
আপনি যদি ল্যাম্বডা ফাংশনের বাইরে কানেকশন ক্যাশ করার চেষ্টা করেন তবে আরেকটি সমস্যা হয়। AWS যখন আপনার Lambda ফাংশন ফ্রিজ করে, এটি কানেকশন ক্লোজ করে না। তাই আপনি অনেক আইডল/জোম্বি কানেকশনসহ শেষ হতে পারেন যা এখনও হুমকি।
এই সমস্যা শুধু Redis-এ সীমাবদ্ধ নয়, এটি TCP কানেকশন Deutschlands নির্ভর করে এমন সব ডেটাবেসে প্রযোজ্য (MySQL, PostgreSQL, MongoDB ইত্যাদি)। সার্ভারলেস কমিউনিটি serverless-mysql এর মতো সমাধান বানাচ্ছে। ये ক্লায়েন্ট-সাইড সমাধান। Upstash হিসেবে আমাদের সার্ভার-সাইড ইমপ্লিমেন্ট এবং মেইনটেইন করার সুবিধা আছে। তাই আমরা কানেকশন মনিটর করি এবং আইডল কানেকশন ইভিক্ট করি সমস্যা মিটিগেট করতে। এখানে অ্যালগরিদম: max-concurrent-connection হিসেবে ডেটাবেসের জন্য দুটি লিমিট রেখেছি, soft-limit এবং hard-limit। ডেটাবেস soft-limit-এ পৌঁছালে আমরা আইডল কানেকশন টারমিনেট করা শুরু করি। hard-limit পৌঁছা পর্যন্ত নতুন কানেকশন রিকোয়েস্ট এক্সেপ্ট করি। hard limit পৌঁছালে নতুন কানেকশন রিজেক্ট করা শুরু হয়।
কানেকশন ইভিকশন অ্যালগরিদম
if( current_connection_count < SOFT_LIMIT ) { ACCEPT_NEW_CONNECTIONS }
if( current_connection_count > SOFT_LIMIT && current_connection_count < HARD_LIMIT ) {
ACCEPT_NEW_CONNECTIONS
START_EVICTING_IDLE_CONNECTIONS
}
if( current_connection_count > HARD_LIMIT ) { REJECT_NEW_CONNECTIONS }
নোট: Upstash ডক্সে লিস্ট করা ম্যাক্স কনকারেন্ট কানেকশন লিমিটগুলো হলো soft limits।
Ephemeral Connections
উপরের অ্যালগরিদম ডিপ্লয় করার পর সব রিজনে রিজেক্টেড কানেকশনের সংখ্যা ভেরিয়ালি কমেছে। কিন্তু সেফ সাইডে থাকতে চাইলে আপনার সাইডেও সমস্যা সমাধান করতে পারেন। কানেকশন রিয়ুজ করার বদলে, ফাংশনের ভিতরে Redis কানেকশন খুলুন এবং কাজ শেষ হলে ক্লোজ করুন নিচের মতো:
exports.handler = async (event) => {
const client = new Redis(process.env.REDIS_URL);
/* do stuff with redis */
await client.quit();
/* do other stuff */
return { response: "response" };
};
উপরের কোড আপনাকে কনকারেন্ট কানেকশন কাউন্ট মিনিমাইজ করতে সাহায্য করে। লোকজন নতুন কানেকশনের লেটেন্সি ওভারহেড নিয়ে জিজ্ঞাসা করেন। Redis কানেকশন খুব লাইটওয়েট হিসেবে পরিচিত।
Redis কানেকশন সত্যিই লাইটওয়েট কি?
আমরা একটি বেঞ্চмар্ক টেস্ট চালিয়েছি দেখতে Redis কানেকশন কত লাইটওয়েট। এই টেস্টে দুটি পদ্ধতির লেটেন্সি তুলনা করেছি:
- EPHEMERAL CONNECTIONS: কানেকশন রিয়ুজ করি না। প্রতিটি কমান্ডের জন্য নতুন কানেকশন তৈরি করি এবং অবিলম্বে ক্লোজ করি। ক্লায়েন্ট ক্রিয়েশন, ping() এবং client.quit() একসাথে লেটেন্সি রেকর্ড করেছি। কোডের
benchEphemeral()মেথড দেখুন। - REUSE CONNECTIONS: একবার কানেকশন তৈরি করি এবং সকল কমান্ডের জন্য সেম কানেকশন রিয়ুজ করি। এখানে শুধু
ping()অপারেশনের লেটেন্সি রেকর্ড করেছি।benchReuse()মেথড দেখুন।
async function benchReuse() {
const client = new Redis(options);
const hist = hdr.build();
for (let index = 0; index < total; index++) {
let start = performance.now() * 1000; // to μs
client.ping();
let end = performance.now() * 1000; // to μs
hist.recordValue(end - start);
await delay(10);
}
client.quit();
console.log(hist.outputPercentileDistribution(1, 1));
}
async function benchEphemeral() {
const hist = hdr.build();
for (let index = 0; index < total; index++) {
let start = performance.now() * 1000; // to μs
const client = new Redis(options);
client.ping();
client.quit();
let end = performance.now() * 1000; // to μs
hist.recordValue(end - start);
await delay(10);
}
console.log(hist.outputPercentileDistribution(1, 1));
}
রিপো দেখুন যদি নিজে বেঞ্চмар্ক চালাতে চান।
আমরা এই বেঞ্চमार্ক কোড AWS EU-WEST-1 রিজনে দুটি ভিন্ন সেটআপে চালিয়েছি। প্রথম সেটআপ SAME ZONE যেখানে ক্লায়েন্ট এবং ডেটাবেস একই availability zone-এ। দ্বিতীয় সেটআপ INTER ZONE যেখানে ক্লায়েন্ট ডেটাবেস থেকে ভিন্ন availability zone-এ রান করে। আমরা Upstash Standard টাইপ ডেটাবেস সার্ভার হিসেবে ব্যবহার করেছি।
আমরা দেখেছি নতুন কানেকশন তৈরি ও ক্লোজ করার ওভারহেড (ephemeral approach) শুধু ৭৫ মাইক্রোসেকেন্ড (99th percentile)। Interzone সেটআপেও ওভারহেড সিমিলার (৮০ মাইক্রোসেকেন্ড)।
তারপর আমরা একই টেস্ট AWS Lambda ফাংশনের ভিতরে রিপিট করলাম। ফলাফল ভিন্ন ছিল। বিশেষ করে যখন Lambda ফাংশনের মেমরি কম (128Mb) সেট করলাম, Redis কানেকশনের বড় ওভারহেড দেখলাম। AWS Lambda ফাংশনের ভিতরে ৬-৭ মিলিসেকেন্ড পর্যন্ত লেটেন্সি ওভারহেড দেখলাম।
Redis কানেকশন সম্পর্কে আমাদের উপসংহার:
- যথেষ্ট CPU পাওয়ার সম্পন্ন সিস্টেমে Redis কানেকশন সত্যিই লাইটওয়েট। t2.micro-তেও।
- ডিফল্ট AWS Lambda কনফিগারেশনে CPU পাওয়ার খুব কম, যা TCP কানেকশন কস্টকে Lambda ফাংশনের টোটাল এক্সিকিউশন টাইমের সাপেক্ষে উল্লেখযোগ্যভাবে বাড়ায়।
- আপনি ডিফল্ট/মিনিমাম মেমরির Lambda ফাংশন ব্যবহার করলে Redis ক্লায়েন্ট ফাংশনের বাইরে ক্যাশ করাই ভালো।
Frozen Container => Zombie Connection
কিছু AWS Lambda সেটআপে কানেকশনের নোটেবল ওভারহেড বুঝতে পারার পর, আমরা AWS Lambda-এ reusing connection-এ আরও টেস্ট করলাম। আমরা আরেকটি সমস্যা ডিটেক্ট করলাম। এটি একটি এজ কেস যা কেউ রিপোর্ট করেনি।
কীভাবে ঘটে তার টাইমলাইন:
STEP1 - timer-0sec: ল্যাম্বডা ফাংশনের বাইরে কানেকশন ক্যাশ করে একটি রিকোয়েস্ট পাঠান।
if (typeof client === "undefined") {
var client = new Redis("REDIS_URL");
}
module.exports.hello = async (event) => {
let response = await client.get("foo");
return { response: response + "-" + time };
};
STEP2 - timer-5sec: AWS ছোট সময় পর কন্টেইনার ফ্রিজ করে।
STEP3 - time-60sec: Upstash-এ আইডল কানেকশনের জন্য ৬০ সেকেন্ড টাইমআউট। তাই এটি কানেকশন কিল করে, কিন্তু ক্লায়েন্ট থেকে ACK পায় না কারণ ফাংশন ফ্রোজেন। তাই সার্ভার কানেকশন FIN_WAIT_2 স্টেটে চলে যায়।
STEP4 - time-90sec: Upstash সার্ভার কানেকশন সম্পূর্ণরূপে কিল করে, FIN_WAIT_2 স্টেট থেকে বের হয়।
STEP5 - time-95sec: ক্লায়েন্ট একই রিকোয়েস্ট পাঠায় এবং ETIMEDOUT এক্সেপশন পায়। কারণ ক্লায়েন্ট অনুমান করে কানেকশন ওপেন আছে কিন্তু এটা নেই। 🤦🏻 🤦🏻 🤦🏻
STEP6 - time-396sec: লাস্ট রিকোয়েস্টের ৫ মিনিট পর AWS কন্টেইনার সম্পূর্ণরূপে কিল করে।
STEP7 - time-400sec: ক্লায়েন্ট আবার একই রিকোয়েস্ট পাঠায়, এইবার ভালো কাজ করে কারণ কন্টেইনার স্ক্র্যাচ থেকে তৈরি হয়েছে তাই ইনিশিয়ালাইজেশন স্কিপ হয় নাই। নতুন কানেকশন তৈরি হয়।
উপরের মতো, AWS একটি কন্টেইনার থ করায় এবং কানেকশন রিয়ুজ করে। কিন্তু কানেকশন সার্ভার সাইড থেকে ক্লোজ হয়ে গেছে এবং কমিউনিকেট করা যেনি কারণ ফাংশন ফ্রোজেন ছিল। তাই Upstash আইডল কানেকশন ইভিক্ট করার এবং AWS আইডল ফাংশন হ্যান্ডল করার মাঝে একটি সিঙ্ক্রোনাইজেশন সমস্যা। যদি আমরা আইডল কানেকশন কিল করি AWS ফাংশন টার্মিনেট করার পর, তবে কোনো ইস্যু হবে না।
আমরা Upstash কানেকশন টাইমআউট ৩১০ সেকেন্ড করলাম ধরে নিচ্ছি AWS ৩০০ সেকেন্ডে আইডল ফাংশন টার্মিনেট করে। এই চেঞ্জের পর সমস্যা गायেব হয়ে গেল। সমস্যা হলো AWS আইডল ফাংশন কখন টার্মিনেট করে সেটা ট্রান্সপারেন্ট নয়। তাই আমাদের টেস্ট চালিয়ে থাকতে হবে এবং ইস্যু আবার হয় কিনা ডিটেক্ট করতে হবে।
এই ইস্যু serverless-mysql লাইব্রেরির ইস্যু-এর সাথে খুব সিমিলার। কমেন্টসে ETIMEDOUT এক্সেপশনে রিকোয়েস্ট রিট্রাই করার সাজেশন দেওয়া হয়েছে। কিন্তু রিট্রাইয়ের দুটি ড্রব্যাক। প্রথম, আপনি একটি রাইট রিকোয়েস্ট রিট্রাই করতে পারেন যা প্রসেস হয়ে গিয়েছে কিন্তু রিয়েল নেটওয়ার্ক সমস্যায় টাইমআউট হয়েছে। দ্বিতীয় সমস্যা হলো ফেইলড রিকোয়েস্টের এক্সট্রা লেটেন্সি।
GraphQL-ও সাহায্য করে
কানেকশন ইস্যু দূর করার একটি উপায় হলো কানেকশন-লেস API। Upstash Redis প্রোটোকলের পাশাপাশি GraphQL API সাপোর্ট করে। GraphQL HTTP-ভিত্তিক তাই কানেকশন লিমিট ইস্যু নেই। সাপোর্টেড কমান্ডের জন্য ডক্স চেক করুন। সতর্ক থাকুন GraphQL API-তে Redis প্রোটোকলের ওপর ~৫ms লেটেন্সি ওভারহেড আছে।
উপসংহার
আমরা Upstash ডেটাবেস কাস্টমাইজ করি সার্ভারলেস অ্যাপ্লিকেশনের জন্য স্মুথ এক্সপেরিয়েন্স দিতে। আমাদের নতুন সার্ভার-সাইড অ্যালগরিদম AWS Lambda- তৈরি করা ইনएक্টিভ কানেকশন রিমুভ করে। আপনি Lambda ফাংশনের ভিতরে Redis ক্লায়েন্ট খুলে/বন্ধ করে কানেকশন সংখ্যা মিনিমাইজ করতে পারেন কিন্তু ফাংশন মেমরি ১GB কম হলে লেটেন্সি ওভারহেড হতে পারে।
উপসংহার হিসেবে, সার্ভারলেস ইউজ কেসের জন্য আমাদের রিকমেনডেশন:
- আপনার ইউজ কেস লেটেন্সি-সেনসিটিভ হলে (যেমন ৬ms বড় লাগে) Redis ক্লায়েন্ট রিয়ুজ করুন।
- খুব বেশি কনকারেন্ট ক্লায়েন্ট অনুভব করলে (১০০০ এর ওপর) Redis ক্লায়েন্ট রিয়ুজ করুন।
- আপনার ইউজ কেস লেটেন্সি-সেনসিটিভ না হলে ফাংশনের ভিতরে Redis ক্লায়েন্ট খুলে/বন্ধ করুন।
- আপনার ফাংশনে কমপক্ষে ১GB মেমরি থাকলে ফাংশনের ভিতরে Redis ক্লায়েন্ট খুলে/বন্ধ করুন।