Computer >> Máy Tính >  >> Lập trình >> Redis

Thách thức của Serverless:Kết nối cơ sở dữ liệu

Thiết kế cơ sở dữ liệu cho serverless, thách thức lớn nhất trong đầu chúng tôi là xây dựng cơ sở hạ tầng hỗ trợ định giá theo yêu cầu một cách có lợi. Chúng tôi tin rằng Upstash đã đạt được điều này. Sau khi ra mắt sản phẩm, chúng tôi thấy rằng có một thách thức lớn khác:Kết nối cơ sở dữ liệu!

Như bạn đã biết, Serverless Functions có quy mô từ 0 đến vô cùng. Điều này có nghĩa là khi các hàm của bạn nhận được nhiều lưu lượng truy cập, nhà cung cấp dịch vụ đám mây sẽ tạo song song các vùng chứa mới (hàm lambda) và mở rộng quy mô phụ trợ của bạn. Nếu bạn tạo một kết nối cơ sở dữ liệu mới trong hàm thì bạn có thể nhanh chóng đạt đến giới hạn kết nối của cơ sở dữ liệu của mình.

Nếu bạn cố gắng lưu vào bộ đệm ẩn kết nối bên ngoài các hàm lambda thì một vấn đề khác sẽ xảy ra. Khi AWS đóng băng chức năng Lambda của bạn, nó không đóng kết nối. Vì vậy, bạn có thể kết thúc với nhiều kết nối không hoạt động / zombie mà vẫn có thể đe dọa.

Vấn đề này không dành riêng cho Redis, nó áp dụng cho tất cả các cơ sở dữ liệu dựa trên kết nối TCP (Mysql, Postgre, MongoDB, v.v.). Bạn có thể thấy cộng đồng serverless đang tạo ra các giải pháp như serverless-mysql. Đây là các giải pháp phía khách hàng. Với tư cách là Upstash, chúng tôi có lợi thế trong việc triển khai và duy trì phía máy chủ. Vì vậy, chúng tôi quyết định giảm thiểu vấn đề bằng cách theo dõi các kết nối và loại bỏ các kết nối không hoạt động. Vì vậy, ở đây thuật toán:Là kết nối tối đa đồng thời, chúng ta có hai giới hạn cho cơ sở dữ liệu, giới hạn mềm và giới hạn cứng. Khi cơ sở dữ liệu đạt đến giới hạn mềm, chúng tôi bắt đầu chấm dứt các kết nối không hoạt động. Chúng tôi tiếp tục chấp nhận các yêu cầu kết nối mới cho đến khi đạt đến giới hạn cứng. Nếu cơ sở dữ liệu đạt đến giới hạn cứng thì chúng tôi bắt đầu từ chối các kết nối mới.

Thuật toán loại bỏ kết nối

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
}

Lưu ý rằng giới hạn kết nối đồng thời tối đa được liệt kê trong tài liệu Upstash là giới hạn mềm.

Kết nối phù du

Sau khi triển khai thuật toán trên, chúng tôi đã thấy số lượng kết nối bị từ chối ở tất cả các khu vực đã giảm đáng kể. Nhưng nếu bạn muốn được an toàn, bạn cũng có thể giải quyết vấn đề từ phía mình. Thay vì sử dụng lại kết nối, bạn có thể mở kết nối Redis bên trong hàm nhưng cũng có thể đóng chúng bất cứ khi nào bạn hoàn tất với Redis như bên dưới:

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

Đoạn mã trên giúp bạn giảm thiểu số lượng kết nối đồng thời. Mọi người hỏi về chi phí độ trễ của các kết nối mới. Kết nối Redis được biết là rất nhẹ.

Các kết nối của Redis có thực sự nhẹ không?

Chúng tôi đã chạy thử nghiệm điểm chuẩn để xem mức độ nhẹ của các kết nối Redis. Trong thử nghiệm này, chúng tôi so sánh số lượng độ trễ của hai cách tiếp cận:

1- KẾT NỐI TỔNG THỂ:Chúng tôi không sử dụng lại kết nối. Thay vào đó, chúng tôi tạo một kết nối mới cho mỗi lệnh và đóng kết nối ngay lập tức. Chúng tôi ghi lại độ trễ của quá trình tạo máy khách, ping () và client.quit () cùng nhau. Xem benchEphemeral() trong phần mã bên dưới.

2- SỬ DỤNG KẾT NỐI:Chúng tôi tạo một kết nối một lần và sử dụng lại cùng một kết nối cho tất cả các lệnh. Ở đây, chúng tôi ghi lại độ trễ của ping() hoạt động. Xem benchReuse() phương pháp bên dưới.

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

Xem repo, nếu bạn muốn tự chạy điểm chuẩn.

Chúng tôi đã chạy mã điểm chuẩn này trong khu vực AWS EU-WEST-1 trong hai thiết lập khác nhau. Thiết lập đầu tiên là CÙNG VÙNG nơi máy khách và cơ sở dữ liệu nằm trong cùng một vùng khả dụng. Thiết lập thứ hai là INTER ZONE nơi máy khách chạy trong vùng khả dụng khác với cơ sở dữ liệu. Chúng tôi đã sử dụng loại Upstash Standard làm máy chủ cơ sở dữ liệu.

Chúng tôi đã thấy chi phí tạo và đóng một kết nối mới (phương pháp tiếp cận tạm thời) chỉ là 75 micro giây (phân vị thứ 99). Chi phí rất tương tự trong thiết lập giữa các khu vực (80 micro giây).

Sau đó, chúng tôi quyết định lặp lại thử nghiệm tương tự bên trong các hàm AWS Lambda. Các kết quả đã khác nhau. Đặc biệt là khi chúng tôi đặt bộ nhớ của hàm Lambda ở mức thấp (128Mb), chúng tôi đã thấy chi phí lớn hơn của các kết nối Redis. Chúng tôi đã thấy chi phí độ trễ lên đến 6-7 msec bên trong các hàm AWS Lambda.

Kết luận của chúng tôi về các kết nối Redis:

  • Các kết nối Redis thực sự nhẹ trên hệ thống có lượng CPU hợp lý. Ngay cả trên t2.micro.
  • Sức mạnh của CPU với cấu hình AWS Lambda mặc định rất kém, điều này làm tăng đáng kể chi phí kết nối TCP so với tổng thời gian thực thi của hàm Lambda.
  • Nếu bạn sử dụng các hàm Lambda với bộ nhớ mặc định / tối thiểu, thì tốt hơn bạn nên lưu vào bộ nhớ cache kết nối Redis bên ngoài hàm.

Frozen Container => Kết nối Zombie

Sau khi nhận ra rằng kết nối có thể có chi phí đáng chú ý trong một số thiết lập AWS Lambda, chúng tôi đã quyết định thực hiện thêm các thử nghiệm về reusing connection trong AWS Lambda. Chúng tôi đã phát hiện một vấn đề khác. Đây là một trường hợp nguy hiểm mà chưa ai báo cáo.

Đây là dòng thời gian diễn ra như thế nào:

BƯỚC1 - hẹn giờ-0 giây: Chúng tôi gửi một yêu cầu, lưu vào bộ nhớ đệm kết nối bên ngoài hàm lambda.

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

BƯỚC 2 - hẹn giờ-5 giây: AWS đóng băng vùng chứa sau một thời gian ngắn.

BƯỚC 3 - thời gian-60 giây: Upstash có thời gian chờ là 60 giây cho các kết nối không hoạt động. Vì vậy, nó giết kết nối, nhưng không thể lấy ACK từ máy khách vì nó bị đóng băng. Vì vậy, kết nối máy chủ chuyển sang trạng thái FIN_WAIT_2.

BƯỚC 4 - thời gian-90 giây: Máy chủ upstash ngắt hoàn toàn kết nối, thoát khỏi trạng thái FIN_WAIT_2.

BƯỚC 5 - thời gian-95 giây: Khách hàng gửi cùng một yêu cầu và nhận ngoại lệ ETIMEDOUT. Vì máy khách giả định kết nối được mở nhưng thực tế không phải vậy. 🤦🏻 🤦🏻 🤦🏻

BƯỚC 6 - thời gian-396 giây: 5 phút sau yêu cầu cuối cùng, AWS sẽ hủy hoàn toàn vùng chứa.

BƯỚC7 - thời gian 400 giây: Khách hàng gửi cùng một yêu cầu lần này nó hoạt động tốt vì vùng chứa được tạo từ đầu nên bước khởi tạo không bị bỏ qua. Một kết nối mới được tạo.

Như bạn có thể thấy ở trên, AWS làm tan một vùng chứa và sử dụng lại kết nối. Nhưng kết nối đã bị đóng từ phía máy chủ và nó không thể được giao tiếp vì chức năng đã bị đóng băng. Vì vậy, có một vấn đề đồng bộ hóa giữa Upstash loại bỏ kết nối không hoạt động và AWS xử lý một chức năng không hoạt động. Vì vậy, nếu chúng tôi hủy kết nối không hoạt động chỉ sau khi AWS chấm dứt một chức năng thì sẽ không có bất kỳ vấn đề nào.

Chúng tôi đã thay đổi thời gian chờ kết nối Upstash thành 310 giây với giả định rằng AWS kết thúc một chức năng không hoạt động sau 300 giây. Sau thay đổi này, vấn đề đã biến mất. Vấn đề ở đây là AWS không minh bạch khi họ chấm dứt các chức năng nhàn rỗi. Vì vậy, chúng tôi cần tiếp tục kiểm tra và cố gắng phát hiện xem sự cố có xảy ra lần nữa hay không.

Sự cố này khá giống với sự cố được thấy trên thư viện serverless-mysql. Trong các nhận xét, người ta đã đề xuất thử lại yêu cầu theo ngoại lệ ETIMEDOUT. Nhưng thử lại có hai nhược điểm. Trước tiên, bạn có thể thử lại một yêu cầu ghi có thể đã được xử lý và hết thời gian chờ do sự cố mạng thực sự. Vấn đề thứ hai là độ trễ tăng thêm của yêu cầu không thành công.

GraphQL cũng hỗ trợ

Một cách để loại bỏ các vấn đề kết nối để có API không kết nối. Upstash hỗ trợ API GraphQL ngoài giao thức Redis. GraphQL dựa trên HTTP nên nó không có vấn đề về giới hạn kết nối. Kiểm tra tài liệu để biết các lệnh được hỗ trợ. Hãy lưu ý rằng API GraphQL có chi phí độ trễ (khoảng 5msec) so với giao thức Redis.

Kết luận

Chúng tôi tùy chỉnh cơ sở dữ liệu Upstash để có trải nghiệm mượt mà cho các ứng dụng không máy chủ. Thuật toán phía máy chủ mới của chúng tôi loại bỏ các kết nối không hoạt động mà AWS Lambda tạo ra nhiều. Bạn có thể giảm thiểu số lượng kết nối bằng cách mở / đóng ứng dụng Redis bên trong hàm Lambda nhưng điều này có thể có chi phí độ trễ nếu bộ nhớ hàm của bạn dưới 1GB.

Như một kết luận, khuyến nghị của chúng tôi cho các trường hợp sử dụng không có máy chủ:

  • Nếu trường hợp sử dụng của bạn nhạy cảm với độ trễ (ví dụ:6msec là quá lớn đối với bạn) thì hãy sử dụng lại ứng dụng Redis.
  • Nếu bạn gặp phải số lượng khách hàng đồng thời rất cao (hơn 1000) thì hãy sử dụng lại ứng dụng Redis.
  • Nếu trường hợp sử dụng của bạn không nhạy cảm với độ trễ thì hãy mở / đóng ứng dụng Redis bên trong hàm.
  • Nếu hàm của bạn có bộ nhớ ít nhất 1GB thì hãy mở / đóng ứng dụng Redis bên trong hàm.

Hãy cho chúng tôi biết phản hồi của bạn trên Twitter hoặc Discord.