Enhancing Performance in Microservice Architecture with Kitex

Bài tóm tắt ByteDance 2024 3. Quy mô lớn Độ khó: Khó

ByteDance · 2024 · 3. Quy mô lớn · Độ khó: Khó — bài gốc

Bài của team CloudWeGo (ByteDance) tổng kết các tối ưu hiệu năng framework RPC Kitex từ 2019 đến 2023. Bài trước về lịch sử framework: ByteDance 2022. Nhiều biểu đồ so sánh trong bài gốc không có số trong văn bản.

Bối cảnh

  • Thời monolith, gọi hàm tốn nano giây; chuyển sang microservice, mỗi lời gọi thành RPC tốn mili giây, chênh khoảng 10^6. Theo bài, lý do tách là trần tính toán của một máy và hiệu suất phát triển không tăng tuyến tính theo kích thước repo và team; cái giá là chi phí RPC trên mọi đường gọi.
  • Kitex bắt đầu năm 2019, mở mã nguồn 2021; số microservice nội bộ tăng mạnh từ 2019 đến 2023.

Tối ưu theo từng chặng của một RPC

  • Codec: reflection lúc chạy, nhiều lời gọi hàm và object nhỏ, khó tái dùng bộ nhớ (tải GC). Sinh code FastThrift/FastPB: tính Size() trước để cấp một khối nhớ cố định, ghi tuần tự, hàm con được inline. Phần CPU dành cho codec giảm từ 3,58% xuống 0,98%.
  • JIT (Frugal, cho Thrift): code sinh ra phình theo số trường và phụ thuộc phiên bản công cụ của từng người. JIT sinh mã lúc chạy, dùng thanh ghi và inline sâu hơn: còn 0,78%.
  • Mạng (Netpoll): net chuẩn của Go một goroutine mỗi kết nối (nặng khi nhiều instance hai phía), không tự biết kết nối đã đóng, Write([]byte) không nhận vùng nhớ rời nên phải chép. Netpoll tái dùng goroutine, đọc ghi zero-copy, tuning cho gói nhỏ đồng thời cao, có cả sửa Go runtime (ưu tiên lập lịch) và syscall theo lô trong kernel nội bộ.
  • Trong cùng máy với service mesh: sidecar thêm một chặng; hijack bằng iptables tốn kém. UDS thay TCP nhanh hơn chút ít (benchmark: avg 127 → 118 µs, p99 232 → 205 µs phía client). ShmIPC: bộ nhớ chia sẻ làm kênh dữ liệu, UDS làm kênh báo sự kiện.
  • Biến gọi khác máy thành cùng máy (pod affinity): scheduler container đặt instance của upstream và downstream lên cùng máy vật lý khi có thể; control plane tính trọng số động để đẩy thêm traffic vào đường nội máy (vẫn giữ cân bằng tải toàn cục); framework chọn gửi thẳng tới instance cùng máy hay qua mesh proxy.

Tuning khi chạy online

  • GC: GOGC=100 mặc định chạy GC mỗi khi heap gấp đôi; service dùng 100 MB trên máy 4 GB vẫn GC dày, phá tái dùng object. gctuner (mã nguồn mở): đặt ngưỡng (ví dụ 70% giới hạn bộ nhớ), dưới ngưỡng thì nâng GOGC, vượt thì hạ, kẹp trong 50–500. Lưu ý khi nhiều tiến trình chung bộ nhớ hoặc bộ nhớ có đỉnh đột ngột.
  • "Ảo giác" của container: cgroup giới hạn thời gian CPU chứ không phải số CPU; container 4 core thấy 4 CPU nhưng thread chạy trên nhiều core hơn, dùng hết quota trong chu kỳ 100 ms thì cả container bị throttle tới hết chu kỳ.
  • Song song và timeout: gửi 4 request song song, mỗi cái cần 50 ms, timeout 100 ms; nếu bên kia lúc đó chỉ có 2 CPU (còn làm việc khác, GC) thì request thứ ba timeout. Không phải lúc nào giảm song song cũng đúng; chỉnh GOMAXPROCS, độ song song trong code, và dùng interface theo lô khi được. Chừa CPU cho sidecar trong cùng container.

Bài học cho SWE

  • Tách microservice biến lời gọi hàm thành RPC đắt hơn sáu bậc; ở quy mô lớn, framework RPC và đường tới sidecar thành chi phí hạ tầng đáng kể (Monolith và microservices — đi và quay lại, Chi phí hạ tầng và build vs buy).
  • GC mặc định không được thiết kế cho service nhạy p99; tune theo giới hạn bộ nhớ thật thay vì theo kích thước heap (Đổi ngôn ngữ và runtime).
  • Đọc số CPU trong container là đọc sai; giới hạn là quota thời gian, vượt thì bị throttle cả container.
  • Tối ưu từ codec, mạng, IPC đến lập lịch là cùng một chuỗi; vài phần trăm CPU ở framework nhân với hàng chục nghìn service là đáng.

Liên quan