सर्वरलेस के लिए डेटाबेस डिजाइन करते समय, हमारे दिमाग में सबसे बड़ी चुनौती एक ऐसा इंफ्रास्ट्रक्चर बनाना था जो लाभदायक तरीके से प्रति-अनुरोध मूल्य निर्धारण का समर्थन करे। हम मानते हैं कि Upstash ने इसे हासिल कर लिया है। उत्पाद लॉन्च करने के बाद, हमने देखा कि एक और बड़ी चुनौती थी: डेटाबेस कनेक्शन!
जैसा कि आप जानते हैं, सर्वरलेस फ़ंक्शंस 0 से अनंत तक स्केल करते हैं। इसका मतलब है कि जब आपके फ़ंक्शंस को बहुत अधिक ट्रैफिक मिलता है, तो क्लाउड प्रदाता नए कंटेनर (लैम्ब्डा फ़ंक्शंस) समानांतर में बनाता है और आपके बैकएंड को स्केल आउट करता है। यदि आप फ़ंक्शन के अंदर एक नया डेटाबेस कनेक्शन बनाते हैं तो आप तेजी से अपने डेटाबेस की कनेक्शन सीमा तक पहुंच सकते हैं।
यदि आप लैम्ब्डा फ़ंक्शंस के बाहर कनेक्शन को कैश करने की कोशिश करते हैं तो दूसरी समस्या होती है। जब AWS आपके लैम्ब्डा फ़ंक्शन को फ्रीज करता है, तो यह कनेक्शन को बंद नहीं करता। इसलिए आप कई आइडल/ज़ोंबी कनेक्शन के साथ समाप्त हो सकते हैं जो अभी भी खतरा पैदा कर सकते हैं।
यह समस्या Redis के लिए विशिष्ट नहीं है, यह उन सभी डेटाबेस पर लागू होती है जो TCP कनेक्शन पर भरोसा करते हैं (MySQL, Postgre, MongoDB आदि)। आप देख सकते हैं कि सर्वरलेस समुदाय serverless-mysql जैसे समाधान बना रहा है। ये क्लाइंट-साइड समाधान हैं। Upstash के रूप में, हमारे पास सर्वर-साइड को लागू करने और बनाए रखने का लाभ है। इसलिए हमने कनेक्शन की निगरानी करके और आइडल कनेक्शन को हटाकर समस्या को कम करने का फैसला किया। तो यहां एल्गोरिदम है: अधिकतम समवर्ती कनेक्शन के रूप में, हमारे पास एक डेटाबेस के लिए दो सीमाएं हैं, सॉफ्ट-लिमिट और हार्ड-लिमिट। जब एक डेटाबेस सॉफ्ट-लिमिट तक पहुंचता है तो हम आइडल कनेक्शन को समाप्त करना शुरू करते हैं। हम हार्ड-लिमिट तक पहुंचने तक नए कनेक्शन अनुरोधों को स्वीकार करना जारी रखते हैं। यदि डेटाबेस हार्ड लिमिट तक पहुंच जाता है तो हम नए कनेक्शन को अस्वीकार करना शुरू कर देते हैं।
कनेक्शन निष्कासन एल्गोरिदम
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 डॉक्स में सूचीबद्ध अधिकतम समवर्ती कनेक्शन सीमाएं सॉफ्ट लिमिट्स हैं।
एफेमेरल कनेक्शन
उपरोक्त एल्गोरिदम को तैनात करने के बाद, हमने सभी क्षेत्रों में अस्वीकृत कनेक्शन की संख्या में बड़ी कमी देखी। लेकिन फिर भी यदि आप सुरक्षित पक्ष पर रहना चाहते हैं, तो आप अपनी तरफ से भी समस्या हल कर सकते हैं। कनेक्शन का पुन: उपयोग करने के बजाय, आप फ़ंक्शन के अंदर Redis कनेक्शन खोल सकते हैं लेकिन जब भी आप 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 कनेक्शन कितने हल्के हैं। इस टेस्ट में हम दो दृष्टिकोणों की लेटेंसी नंबर की तुलना करते हैं:
1- EPHEMERAL CONNECTIONS: हम कनेक्शन का पुन: उपयोग नहीं करते। इसके बजाय हम प्रत्येक कमांड के लिए एक नया कनेक्शन बनाते हैं और तुरंत कनेक्शन बंद कर देते हैं। हम क्लाइंट निर्माण, ping() और client.quit() की लेटेंसी को एक साथ रिकॉर्ड करते हैं। कोड सेक्शन में benchEphemeral() मेथड देखें।
2- 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 है जहां क्लाइंट और डेटाबेस एक ही उपलब्धता क्षेत्र में हैं। दूसरा सेटअप INTER ZONE है जहां क्लाइंट डेटाबेस से अलग उपलब्धता क्षेत्र में चलता है। हमने डेटाबेस सर्वर के रूप में Upstash स्टैंडर्ड टाइप का उपयोग किया है।
हमने देखा कि एक नया कनेक्शन बनाने और बंद करने का ओवरहेड (एफेमेरल दृष्टिकोण) केवल 75 माइक्रोसेकंड (99वें पर्सेंटाइल) है। इंटरज़ोन सेटअप में ओवरहेड बहुत समान है (80 माइक्रोसेकंड)।
फिर हमने AWS Lambda फ़ंक्शंस के अंदर वही टेस्ट दोहराने का फैसला किया। परिणाम अलग थे। विशेष रूप से जब हम Lambda फ़ंक्शन की मेमोरी कम (128Mb) सेट करते हैं, तो हमने Redis कनेक्शन के बड़े ओवरहेड देखे। हमने AWS Lambda फ़ंक्शंस के अंदर 6-7 msec तक लेटेंसी ओवरहेड देखा।
Redis कनेक्शन के बारे में हमारे निष्कर्ष:
- उचित मात्रा में CPU पावर वाले सिस्टम पर Redis कनेक्शन वास्तव में हल्के होते हैं। t2.micro पर भी।
- डिफ़ॉल्ट AWS Lambda कॉन्फ़िगरेशन के साथ CPU पावर बहुत खराब है, जो TCP कनेक्शन की लागत को Lambda फ़ंक्शन के कुल निष्पादन समय के संबंध में काफी बढ़ा देता है।
- यदि आप डिफ़ॉल्ट/न्यूनतम मेमोरी के साथ Lambda फ़ंक्शंस का उपयोग करते हैं, तो आप फ़ंक्शन के बाहर Redis कनेक्शन को कैश करना बेहतर समझेंगे।
फ्रोजन कंटेनर => ज़ोंबी कनेक्शन
यह महसूस करने के बाद कि कुछ AWS Lambda सेटअप में कनेक्शन का उल्लेखनीय ओवरहेड हो सकता है, हमने AWS Lambda में कनेक्शन का पुन: उपयोग पर आगे टेस्ट करने का फैसला किया। हमने एक और समस्या का पता लगाया। यह एक एज केस था जिसकी किसी ने अभी तक रिपोर्ट नहीं की है।
यहां टाइमलाइन है कि यह कैसे होता है:
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 का आइडल कनेक्शन के लिए 60 सेकंड का टाइमआउट है। तो यह कनेक्शन को मारता है, लेकिन क्लाइंट से ACK नहीं मिल पाता क्योंकि यह फ्रोजन है। तो सर्वर कनेक्शन FIN_WAIT_2 स्टेट में चला जाता है।
STEP4 - time-90sec: Upstash सर्वर कनेक्शन को पूरी तरह से मारता है, FIN_WAIT_2 स्टेट से बाहर निकलता है।
STEP5 - time-95sec: क्लाइंट वही अनुरोध भेजता है और ETIMEDOUT अपवाद प्राप्त करता है। क्योंकि क्लाइंट मानता है कि कनेक्शन खुला है लेकिन यह नहीं है। 🤦🏻 🤦🏻 🤦🏻
STEP6 - time-396sec: अंतिम अनुरोध के 5 मिनट बाद, AWS कंटेनर को पूरी तरह से मारता है।
STEP7 - time-400sec: क्लाइंट वही अनुरोध भेजता है इस बार यह अच्छी तरह से काम करता है क्योंकि कंटेनर स्क्रैच से बनाया गया है इसलिए इनिशियलाइज़ेशन चरण नहीं छोड़ा जाता। एक नया कनेक्शन बनाया जाता है।
जैसा कि आप ऊपर देख सकते हैं, AWS एक कंटेनर को थॉ करता है और कनेक्शन का पुन: उपयोग करता है। लेकिन कनेक्शन को सर्वर साइड से बंद कर दिया गया है और इसे संप्रेषित नहीं किया जा सका क्योंकि फ़ंक्शन फ्रोजन था। तो Upstash द्वारा एक आइडल कनेक्शन को हटाने और AWS द्वारा एक आइडल फ़ंक्शन को संभालने के बीच एक सिंक्रनाइज़ेशन समस्या है। इसलिए यदि हम एक आइडल कनेक्शन को केवल AWS द्वारा एक फ़ंक्शन को समाप्त करने के बाद मारते हैं तो कोई समस्या नहीं होगी।
हमने Upstash कनेक्शन टाइमआउट को 310 सेकंड में बदल दिया, यह मानते हुए कि AWS 300 सेकंड में एक आइडल फ़ंक्शन को समाप्त करता है। इस परिवर्तन के बाद, समस्या गायब हो गई। यहां समस्या यह है कि AWS पारदर्शी नहीं है जब वे आइडल फ़ंक्शंस को समाप्त करते हैं। इसलिए हमें टेस्ट जारी रखने और यह पता लगाने की आवश्यकता है कि क्या समस्या फिर से होती है।
यह मुद्दा serverless-mysql लाइब्रेरी पर देखे गए मुद्दे के समान है। टिप्पणियों में, ETIMEDOUT अपवाद पर अनुरोध को रीट्राई करने का सुझाव दिया गया है। लेकिन रीट्राई करने के दो नुकसान हैं। पहला, आप एक लिखने के अनुरोध को रीट्राई कर सकते हैं जिसे प्रोसेस किया गया होगा और एक वास्तविक नेटवर्क समस्या के साथ टाइमआउट हो गया होगा। दूसरी समस्या विफल अनुरोध की अतिरिक्त लेटेंसी है।
GraphQL भी मदद करता है
कनेक्शन समस्याओं से छुटकारा पाने का एक तरीका कनेक्शनलेस API रखना है। Upstash Redis प्रोटोकॉल के अलावा GraphQL API का समर्थन करता है। GraphQL HTTP-आधारित है इसलिए इसमें कनेक्शन सीमा की समस्या नहीं है। समर्थित कमांड के लिए डॉक्स देखें। ध्यान रखें कि GraphQL API में Redis प्रोटोकॉल पर लेटेंसी ओवरहेड (लगभग 5msec) है।
निष्कर्ष
हम सर्वरलेस एप्लिकेशन के लिए एक सहज अनुभव के लिए Upstash डेटाबेस को कस्टमाइज़ करते हैं। हमारा नया सर्वर-साइड एल्गोरिदम उन निष्क्रिय कनेक्शन को हटा देता है जो AWS Lambda बड़ी मात्रा में बनाता है। आप Lambda फ़ंक्शन के अंदर अपने Redis क्लाइंट को खोल/बंद करके कनेक्शन की संख्या को कम कर सकते हैं लेकिन यदि आपकी फ़ंक्शन मेमोरी 1GB से कम है तो इसमें लेटेंसी ओवरहेड हो सकता है।
निष्कर्ष के रूप में, सर्वरलेस उपयोग के मामलों के लिए हमारी सिफारिश:
- यदि आपका उपयोग केस लेटेंसी-संवेदनशील है (उदाहरण के लिए 6msec आपके लिए बड़ा है) तो Redis क्लाइंट का पुन: उपयोग करें।
- यदि आप बहुत अधिक संख्या में समवर्ती क्लाइंट्स (1000 से अधिक) का अनुभव करते हैं तो Redis क्लाइंट का पुन: उपयोग करें।
- यदि आपका उपयोग केस लेटेंसी-संवेदनशील नहीं है तो फ़ंक्शन के अंदर Redis क्लाइंट को खोलें/बंद करें।
- यदि आपकी फ़ंक्शन में कम से कम 1GB मेमोरी है तो फ़ंक्शन के अंदर Redis क्लाइंट को खोलें/बंद करें।