Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Redis

ความท้าทายของ Serverless:การเชื่อมต่อฐานข้อมูล

การออกแบบฐานข้อมูลแบบไร้เซิร์ฟเวอร์ ความท้าทายที่ยิ่งใหญ่ที่สุดในใจของเราคือการสร้างโครงสร้างพื้นฐานที่รองรับการกำหนดราคาต่อคำขอในลักษณะที่สร้างผลกำไร เราเชื่อว่า Upstash ทำสำเร็จแล้ว หลังจากที่เราเปิดตัวผลิตภัณฑ์ เราพบว่ามีความท้าทายที่สำคัญอีกอย่างหนึ่ง นั่นคือ การเชื่อมต่อฐานข้อมูล!

อย่างที่คุณทราบ Serverless Functions จะปรับขนาดจาก 0 เป็นอินฟินิตี้ ซึ่งหมายความว่าเมื่อฟังก์ชันของคุณมีการรับส่งข้อมูลจำนวนมาก ผู้ให้บริการระบบคลาวด์จะสร้างคอนเทนเนอร์ใหม่ (ฟังก์ชันแลมบ์ดา) แบบคู่ขนานและขยายแบ็กเอนด์ของคุณ หากคุณสร้างการเชื่อมต่อฐานข้อมูลใหม่ภายในฟังก์ชัน คุณจะสามารถเข้าถึงขีดจำกัดการเชื่อมต่อของฐานข้อมูลได้อย่างรวดเร็ว

หากคุณพยายามแคชการเชื่อมต่อภายนอกฟังก์ชันแลมบ์ดา ปัญหาอื่นก็จะเกิดขึ้น เมื่อ AWS หยุดการทำงานของ Lambda จะไม่ปิดการเชื่อมต่อ ดังนั้นคุณอาจจบลงด้วยการเชื่อมต่อที่ไม่ได้ใช้งาน/ซอมบี้ซึ่งยังคงคุกคามได้

ปัญหานี้ไม่ได้เกิดขึ้นเฉพาะกับ 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- การเชื่อมต่อชั่วคราว:เราไม่ใช้การเชื่อมต่อซ้ำ แต่เราสร้างการเชื่อมต่อใหม่สำหรับแต่ละคำสั่งและปิดการเชื่อมต่อทันที เราบันทึกเวลาแฝงของการสร้างไคลเอนต์ ping() และ client.quit() ไว้ด้วยกัน ดู benchEphemeral() ในส่วนของโค้ดด้านล่าง

2- ใช้การเชื่อมต่อซ้ำ:เราสร้างการเชื่อมต่อหนึ่งครั้งและนำการเชื่อมต่อเดิมมาใช้ซ้ำสำหรับคำสั่งทั้งหมด ที่นี่ เราบันทึกเวลาแฝงของ 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));
}

ดู repo หากคุณต้องการเรียกใช้การวัดประสิทธิภาพด้วยตัวคุณเอง

เรารันโค้ดเกณฑ์มาตรฐานนี้ในภูมิภาค AWS EU-WEST-1 ในการตั้งค่าสองแบบที่แตกต่างกัน การตั้งค่าแรกคือ SAME ZONE โดยที่ไคลเอ็นต์และฐานข้อมูลอยู่ในโซนความพร้อมใช้งานเดียวกัน การตั้งค่าที่สองคือ INTER ZONE ซึ่งไคลเอ็นต์รันในโซนความพร้อมใช้งานที่แตกต่างจากฐานข้อมูล เราใช้ประเภท Upstash Standard เป็นเซิร์ฟเวอร์ฐานข้อมูล

เราได้เห็นค่าใช้จ่ายในการสร้างและปิดการเชื่อมต่อใหม่ (วิธีชั่วคราว) อยู่ที่ 75 ไมโครวินาที (เปอร์เซ็นไทล์ที่ 99) ค่าโสหุ้ยคล้ายกันมากในการตั้งค่าอินเตอร์โซน (80 ไมโครวินาที)

จากนั้นเราตัดสินใจทำการทดสอบซ้ำในฟังก์ชัน AWS Lambda ผลลัพธ์ก็ต่างกัน โดยเฉพาะอย่างยิ่งเมื่อเราตั้งค่าหน่วยความจำของฟังก์ชันแลมบ์ดาให้ต่ำ (128 เมกะไบต์) เราได้เห็นโอเวอร์เฮดของการเชื่อมต่อ Redis ที่ใหญ่ขึ้น เราเห็นโอเวอร์เฮดเวลาแฝงสูงถึง 6-7 มิลลิวินาทีในฟังก์ชัน AWS Lambda

ข้อสรุปของเราเกี่ยวกับการเชื่อมต่อ Redis:

  • การเชื่อมต่อ Redis นั้นเบามากในระบบที่มีกำลัง CPU พอสมควร แม้ใน t2.micro
  • กำลังของ CPU ที่มีการกำหนดค่าเริ่มต้นของ AWS Lambda ต่ำมาก ซึ่งเพิ่มต้นทุนของการเชื่อมต่อ TCP อย่างมากเมื่อเทียบกับเวลาดำเนินการทั้งหมดของฟังก์ชัน Lambda
  • หากคุณใช้ฟังก์ชัน Lambda กับหน่วยความจำเริ่มต้น/ขั้นต่ำ คุณควรแคชการเชื่อมต่อ Redis ภายนอกฟังก์ชัน

คอนเทนเนอร์แช่แข็ง => การเชื่อมต่อซอมบี้

หลังจากที่ตระหนักว่าการเชื่อมต่ออาจมีค่าใช้จ่ายที่โดดเด่นในการตั้งค่า AWS Lambda บางอย่าง เราจึงตัดสินใจทำการทดสอบเพิ่มเติมเกี่ยวกับreusing connection ใน AWS แลมบ์ดา เราตรวจพบปัญหาอื่น นี่เป็นกรณีที่ยังไม่มีรายงาน

นี่คือไทม์ไลน์ที่เกิดขึ้น:

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 };
};

ขั้นตอนที่ 2 - ตัวจับเวลา-5 วินาที: AWS จะหยุดคอนเทนเนอร์หลังจากผ่านไปครู่หนึ่ง

ขั้นตอนที่ 3 - เวลา-60 วินาที: Upstash มีการหมดเวลา 60 วินาทีสำหรับการเชื่อมต่อที่ไม่ได้ใช้งาน ดังนั้นจึงฆ่าการเชื่อมต่อ แต่ไม่สามารถรับ ACK จากลูกค้าได้เนื่องจากถูกระงับ ดังนั้นการเชื่อมต่อเซิร์ฟเวอร์จะเข้าสู่สถานะ FIN_WAIT_2

STEP4 - เวลา-90 วินาที: เซิร์ฟเวอร์ Upstash ฆ่าการเชื่อมต่อโดยสมบูรณ์ โดยออกจากสถานะ FIN_WAIT_2

ขั้นตอนที่ 5 - เวลา 95 วินาที: ลูกค้าส่งคำขอเดียวกันและได้รับข้อยกเว้น ETIMEDOUT เนื่องจากไคลเอนต์ถือว่าการเชื่อมต่อเปิดอยู่ แต่ไม่ใช่ 🤦🏻 🤦🏻 🤦🏻

ขั้นตอนที่ 6 - เวลา-396 วินาที: 5 นาทีหลังจากส่งคำขอครั้งสุดท้าย AWS จะฆ่าคอนเทนเนอร์ทั้งหมด

ขั้นตอนที่7 - เวลา-400 วินาที: ลูกค้าส่งคำขอเดียวกัน ครั้งนี้ทำงานได้ดีเพราะคอนเทนเนอร์ถูกสร้างขึ้นตั้งแต่เริ่มต้น จึงไม่ข้ามขั้นตอนการเริ่มต้น มีการสร้างการเชื่อมต่อใหม่

ดังที่คุณเห็นด้านบน AWS จะละลายคอนเทนเนอร์และนำการเชื่อมต่อกลับมาใช้ใหม่ แต่การเชื่อมต่อถูกปิดจากฝั่งเซิร์ฟเวอร์และไม่สามารถสื่อสารได้เนื่องจากฟังก์ชันถูกระงับ ดังนั้นจึงมีปัญหาการซิงโครไนซ์ระหว่าง Upstash ที่ยกเลิกการเชื่อมต่อที่ไม่ได้ใช้งานและ AWS ที่จัดการฟังก์ชันที่ไม่ได้ใช้งาน ดังนั้น หากเราฆ่าการเชื่อมต่อที่ไม่ได้ใช้งานหลังจากที่ AWS ยุติฟังก์ชันแล้วเท่านั้น ก็จะไม่มีปัญหาใดๆ

เราเปลี่ยนการหมดเวลาการเชื่อมต่อ Upstash เป็น 310 วินาทีโดยสมมติว่า AWS ยุติฟังก์ชันที่ไม่ได้ใช้งานใน 300 วินาที หลังจากการเปลี่ยนแปลงนี้ ปัญหาก็หายไป ปัญหาที่นี่คือ AWS ไม่โปร่งใสเมื่อยุติฟังก์ชันที่ไม่ได้ใช้งาน เราจึงต้องทดสอบต่อไปและพยายามตรวจหาว่าปัญหาเกิดขึ้นอีกหรือไม่

ปัญหานี้ค่อนข้างคล้ายกับปัญหาที่พบในไลบรารี serverless-mysql ในความคิดเห็น ขอแนะนำให้ลองส่งคำขออีกครั้งตามข้อยกเว้น ETIMEDOUT แต่การลองใหม่มีข้อเสียสองประการ ขั้นแรก คุณอาจลองส่งคำขอเขียนใหม่ซึ่งอาจได้รับการประมวลผลและหมดเวลาเนื่องจากปัญหาเครือข่ายจริง ปัญหาที่สองคือเวลาแฝงเพิ่มเติมของคำขอที่ล้มเหลว

GraphQL ช่วยด้วย

วิธีหนึ่งในการกำจัดปัญหาการเชื่อมต่อเพื่อให้มี API ที่ไม่มีการเชื่อมต่อ Upstash รองรับ GraphQL API นอกเหนือจากโปรโตคอล Redis GraphQL ใช้ HTTP ดังนั้นจึงไม่มีปัญหาขีดจำกัดการเชื่อมต่อ ตรวจสอบเอกสารสำหรับคำสั่งที่รองรับ ระวัง GraphQL API มีค่าใช้จ่ายแฝง (ประมาณ 5 มิลลิวินาที) บนโปรโตคอล Redis

บทสรุป

เราปรับแต่งฐานข้อมูล Upstash เพื่อประสบการณ์ที่ราบรื่นสำหรับแอปพลิเคชันแบบไร้เซิร์ฟเวอร์ อัลกอริธึมฝั่งเซิร์ฟเวอร์ใหม่ของเราจะลบการเชื่อมต่อที่ไม่ได้ใช้งานซึ่ง AWS Lambda สร้างขึ้นจำนวนมาก คุณลดจำนวนการเชื่อมต่อได้โดยเปิด/ปิดไคลเอ็นต์ Redis ภายในฟังก์ชัน Lambda แต่อาจมีค่าโสหุ้ยในการตอบสนองหากหน่วยความจำฟังก์ชันของคุณน้อยกว่า 1GB

โดยสรุป คำแนะนำของเราสำหรับกรณีการใช้งานแบบไร้เซิร์ฟเวอร์:

  • หากกรณีการใช้งานของคุณมีความละเอียดอ่อนในการตอบสนอง (เช่น 6msec นั้นสำคัญสำหรับคุณ) ให้นำไคลเอนต์ Redis มาใช้ใหม่
  • หากคุณพบลูกค้าพร้อมกันจำนวนมาก (มากกว่า 1,000 ราย) ให้ใช้ไคลเอ็นต์ Redis ซ้ำ
  • หากกรณีการใช้งานของคุณไม่ไวต่อเวลาในการตอบสนอง ให้เปิด/ปิดไคลเอ็นต์ Redis ภายในฟังก์ชัน
  • หากฟังก์ชันของคุณมีหน่วยความจำอย่างน้อย 1GB ให้เปิด/ปิดไคลเอ็นต์ Redis ภายในฟังก์ชัน

แจ้งให้เราทราบความคิดเห็นของคุณเกี่ยวกับ Twitter หรือ Discord