Featured image of post  Tại sao cần sử dụng ETCD? - đỏ 99 vip

Tại sao cần sử dụng ETCD? - đỏ 99 vip

Trải nghiệm đẳng cấp với đỏ 99 vip - Đỏ 99 vip mang lại những trải nghiệm đặc biệt nhất.

Ngày 05 tháng 05 năm 2022 - Trần Hạo - Bình luận - 39 bình luận - 53.036 lượt xem

Trước tiên, hãy nói về lý do tại sao chúng tôi chọn sử dụng etcd. Chúng ta sẽ bắt đầu từ một API Gateway mà đội nhóm chúng tôi đã phát triển và mã nguồn mở – Easegress (source code).

Easegress là một sản phẩm API Gateway mà chúng tôi tự phát triển và cung cấp dưới dạng mã nguồn mở. Không giống như Nginx chỉ đơn thuần làm nhiệm vụ proxy ngược, Easegress có khả năng thực hiện nhiều chức năng cao cấp hơn như: dàn dựng API, khám phá dịch vụ, thiết kế đàn hồi (chẳng hạn như cơ chế circuit breaker, giới hạn lưu lượng, retry), xác thực và ủy quyền (JWT, OAuth2, HMAC), hỗ trợ các kiến trúc Cloud Native như microservices, Service Mesh, Serverless/FaaS, cũng như có thể ứng dụng trong việc xử lý tải cao, phát hành phiến bản nhẹ nhàng, kiểm tra hiệu suất toàn bộ chuỗi và cả lĩnh vực Internet of Things (IoT)…

Chính vì vậy, vào năm 2017, chúng tôi nhận thấy rằng không thể xây dựng phần mềm đáp ứng được những yêu cầu này dựa trên nền tảng của các gateway hiện tại như Nginx. Vì vậy, chúng tôi quyết định viết lại hoàn toàn từ đầu. Một ví dụ khác cũng tương tự là Envoy của Lyft, nhưng họ dùng ngôn ngữ C++, còn chúng tôi lựa chọn Go vì nó dễ tiếp cận hơn.

Easegress có ba yếu tố thiết kế cốt lõi:

  • Thứ nhất, khả năng tự động chọn leader cho cụm máy mà không phụ thuộc vào bất kỳ bên thứ ba nào.
  • Thứ hai, cấu trúc pipeline xử lý plugin theo kiểu dòng lệnh Linux (hỗ trợ Go/WebAssembly).
  • Thứ ba, tích hợp một Data Store nội bộ để quản lý và chia sẻ dữ liệu giữa các nút trong cụm.

Một hệ thống phân tán cần phải có cơ chế đồng thuận mạnh mẽ dựa trên thuật toán Paxos hoặc Raft để đảm bảo rằng tất cả các nút trong cụm đều có cùng trạng thái và dữ liệu cấu hình đồng bộ. Điều này rất quan trọng để duy trì tính nhất quán của cụm. Nếu thiếu điều này, thì việc vận hành một hệ thống phân tán sẽ trở nên khó khăn. Đây chính là lý do tại sao các công cụ như Zookeeper hay etcd xuất hiện và trở nên phổ biến.

Zookeeper là một phần mềm mã nguồn mở rất nổi tiếng, được sử dụng rộng rãi trong các môi trường sản xuất của nhiều công ty lớn cũng như các phần mềm khác như Kafka. Tuy nhiên, sự phụ thuộc vào Zookeeper khiến cho hệ thống trở nên phức tạp hơn khi vận hành. Do đó, Kafka trong phiên bản mới nhất đã chuyển sang sử dụng thuật toán chọn leader nội bộ thay vì dựa vào Zookeeper. Về phía cộng đồng Go, etcd đóng vai trò chủ lực và là thành phần quan trọng trong việc tạo ra cụm Kubernetes. Ban đầu, Easegress sử dụng giao thức gossip để đồng bộ trạng thái (với mục tiêu áp dụng trên mạng rộng), nhưng sau đó nhận thấy giao thức này quá phức tạp và khó debug, đặc biệt là khi chưa gặp trường hợp thực tế cần đến API Gateway trên mạng rộng. Vì vậy, cách đây khoảng 3 năm, chúng tôi đã chuyển sang sử dụng phiên bản etcd embed để tăng độ ổn định, và thiết kế này vẫn được giữ nguyên đến tận ngày nay.

Easegress lưu trữ mọi thông tin cấu hình, dữ liệu giám sát và cả dữ liệu tùy chỉnh của người dùng vào etcd. Điều này giúp cho các plugin của người dùng không chỉ hoạt động trong một pipeline duy nhất mà còn có thể trao đổi dữ thiên hạ bet j77 liệu giữa các nút trong cụm. Mục tiêu hàng đầu của chúng tôi luôn là đảm bảo tính linh hoạt trong việc mở rộng mã nguồn, đặc biệt đối với phần mềm mã nguồn mở. Đó cũng là lý do tại sao Google thường chọn Go làm ngôn ngữ cho các phần mềm mã nguồn mở của mình, cũng như lý do tại sao Go đang dần thay thế C/C++ trong việc phát triển các thành phần PaaS.

Vấn đề thực tế

Sau khi đã trình bày lý do tại sao chúng tôi sử dụng etcd, bây giờ tôi muốn chia sẻ một vấn đề thực tế mà chúng tôi gặp phải. Một người dùng của Easegress đã báo cáo rằng khi cấu hình hơn một nghìn pipeline, dung lượng bộ nhớ của Easegress tăng lên đáng kể – vượt qua 10GB và không giảm xuống.

Người dùng mô tả tình huống như sau:

Khi khởi tạo một HTTP object với 1000 pipelines trên Easegress phiên bản 1.4.1, bộ nhớ chiếm khoảng 400MB ngay sau khi khởi động xong. Sau 80 phút, con số này tăng lên 2GB và sau 200 phút thì đạt mức 4GB, mặc dù không có bất kỳ yêu cầu nào gửi tới Easegress.

Thông thường, số lượng pipeline lớn như vậy là không cần thiết. Thay vì cấu hình từng API riêng lẻ, chúng ta thường nhóm các API có liên quan vào một pipeline duy nhất, tương tự đỏ 99 vip như cách cấu hình location trong Nginx. Tuy nhiên, trong trường hợp này, người dùng rõ ràng muốn kiểm soát chi tiết hơn ở mức độ thấp.

Qua điều tra, chúng tôi phát hiện ra rằng hầu hết bộ nhớ bị tiêu thụ bởi etcd. Mặc dù lượng dữ liệu chúng tôi lưu vào etcd chỉ có vài key và kích thước không vượt quá 10MB, nhưng tổng dung lượng bộ nhớ chiếm dụng lại lên tới 10GB. Chúng tôi ban đầu nghi ngờ rằng etcd có vấn đề rò rỉ bộ nhớ. Kiểm tra trên GitHub của etcd, chúng tôi thấy rằng các phiên bản 3.2 và 3.3 trước đây có lỗi rò rỉ bộ nhớ, nhưng vấn đề này đã được sửa chữa. Hơn nữa, Easegress sử dụng phiên bản 3.5 mới nhất của etcd, và mức độ rò rỉ bộ nhớ không bao giờ lớn đến như vậy. Do đó, chúng tôi bắt đầu nghi ngờ rằng có thể chúng tôi đã sử dụng etcd sai cách. Để xác minh điều này, chúng tôi đã dành khoảng hai ngày để nghiên cứu kỹ lưỡng thiết kế của etcd.

Kết quả cho thấy rằng etcd có một số thiết kế tiêu tốn rất nhiều bộ nhớ, đặc biệt là:

Raft Log: Etcd sử dụng Raft Log để giúp các follower đồng bộ dữ liệu. Dữ liệu log này được lưu trữ trong bộ nhớ chứ không phải trên đĩa cứng, và tối thiểu phải giữ lại 5000 request gần nhất. Nếu kích thước của một key lớn, chẳng hạn như 1MB, thì 5000 logs sẽ tiêu tốn 5000MB = 5GB bộ nhớ. Vấn đề này đã được thảo luận trong danh sách issue của etcd (#12548), nhưng cuối cùng không có giải pháp rõ ràng. Giá trị 5000 này là cố định và không thể thay đổi (tham khảo mã nguồn liên quan đến DefaultSnapshotCatchUpEntries).

1
2
3
4
5
6
// DefaultSnapshotCatchUpEntries là số lượng entries cho follower chậm 
// để catch-up sau khi compacting các entries của raft storage.
// Chúng tôi mong đợi follower có độ trễ ở mức millisecond với leader.
// Thông lượng tối đa là khoảng 10K. Giữ lại 5K entries là đủ để giúp
// follower catch up.
DefaultSnapshotCatchUpEntries uint64 = 5000

Lịch sử cho thấy rằng đội ngũ phát triển etcd đã giảm giá trị này từ 10000 xuống còn 5000 vì nhận thấy rằng giá trị cũ quá tốn kém về mặt bộ nhớ, nhưng họ vẫn giữ lại 5000 để đảm bảo rằng các follower có thể kịp thời đồng bộ.

Ngoài ra, còn có một số yếu tố khác dẫn đến việc tăng tiêu thụ bộ nhớ của etcd:

  1. Index: Mỗi cặp key-value trong etcd đều có một B-tree index trong bộ nhớ. Chi phí của index này phụ thuộc vào độ dài của key và số lượng phiên bản được lưu trữ. Do đó, kích thước của B-tree cũng chịu ảnh hưởng bởi độ dài của key và số lượng phiên bản lịch sử.

  2. mmap: Etcd sử dụng mmap, một kỹ thuật cổ xưa của Unix, để ánh xạ tệp tin vào bộ nhớ ảo. Điều này khiến kích thước db càng lớn thì nhu cầu bộ nhớ càng cao.

  3. Watcher: Các kết nối Watcher cũng tiêu tốn rất nhiều bộ nhớ, đặc biệt khi số lượng kết nối và số lượng Watcher lớn.

Rõ ràng, etcd được thiết kế để đạt hiệu suất cao, nhưng điều này đôi khi đi kèm với chi phí bộ nhớ đáng kể.

Trong trường hợp của Easegress, vấn đề chính có lẽ nằm ở Raft Log. Ba vấn đề còn lại ít có khả năng là nguyên nhân gây ra tình trạng này. Đối với index và mmap, chúng tôi nghĩ rằng việc sử dụng compact và defreg (nén và dọn dẹp mảnh vỡ) sẽ giúp giảm bớt vấn đề bộ nhớ, nhưng điều này không phải là nguyên nhân cốt lõi trong trường hợp của người dùng.

Nguyên nhân chính của vấn đề là do Easegress thực hiện thống kê dữ liệu cho mỗi pipeline (ví dụ: M1, M5, M15, P99, P90, P50…). Mỗi pipeline có thể tạo ra khoảng 1KB-2KB dữ liệu thống kê, và Easegress gom tất cả dữ liệu thống kê của 1000 pipeline vào một key duy nhất. Kết quả là một key có kích thước trung bình khoảng 2MB, và với 5000 logs trong bộ nhớ, etcd tiêu tốn tới 10GB bộ nhớ.

Cuối cùng, giải pháp của chúng tôi rất đơn giản: thay vì ghi tất cả dữ liệu thống kê vào một key duy nhất, chúng tôi chia nhỏ chúng thành nhiều key nhỏ hơn. Như vậy, tổng lượng dữ liệu được lưu trữ không thay đổi, nhưng kích thước của mỗi entry trong Raft Log giảm đáng kể. Trước đây là 5000 logs với kích thước 2MB (tổng cộng 10GB), giờ chỉ còn 5000 logs với kích thước 1KB (tổng cộng 500MB). Giải pháp này được thực hiện qua PR #542.

Tổng kết

Để sử dụng etcd hiệu quả, bạn cần lưu ý một số thực hành tốt sau đây:

  • Tránh sử dụng các key và value có kích thước lớn vì chúng sẽ tiêu tốn rất nhiều bộ nhớ thông qua Raft Log và B-tree multi-version indexing.
  • Hạn chế kích thước DB quá lớn và sử dụng compact/defreg để giảm thiểu bộ nhớ.
  • Tránh sử dụng quá nhiều Watch Client và Watchers vì chúng tiêu tốn rất nhiều tài nguyên.
  • Luôn cập nhật lên phiên bản mới nhất của Go và etcd để tránh các vấn đề bộ nhớ tiềm ẩn. Ví dụ: trong Go, cơ chế回收 bộ nhớ đã được cải tiến từ MADV_FREE trong phiên bản 1.12 sang MADV_DONTNEED trong phiên bản 1.16. Sự khác biệt giữa hai cơ chế này là FREE giữ lại vùng bộ nhớ mà hệ điều hành có thể tái sử dụng khi cần thiết, trong khi DONTNEED giải phóng ngay lập tức.

Hy vọng bài viết này sẽ giúp ích cho những ai đang làm việc với etcd. Xin mời mọi người theo dõi phần mềm mã nguồn mở của chúng tôi!

Built with Hugo
Theme Stack thiết kế bởi Jimmy