Cloud & hạ tầng

Tại sao mức sử dụng CPU cao thường có nghĩa là cơ sở dữ liệu của bạn đang chờ bộ nhớ

Tìm hiểu sâu về cách độ trễ bộ nhớ làm chậm CPU trong các phép tổng hợp cơ sở dữ liệu và cách phân vùng radix cải thiện hiệu quả sử dụng cache để tăng hiệu suất.

Minh họa dữ liệu di chuyển chậm từ bộ nhớ xa đến cache CPU nhanh
Minh họa được tạo riêng cho bài viết này

Được dịch tự động từ bản gốc tiếng Anh.

Khi một máy chủ cơ sở dữ liệu báo cáo mức sử dụng CPU 100%, điều đó không nhất thiết có nghĩa là nó đang xử lý số liệu với tốc độ tối đa. Thường thì bộ vi xử lý đang ở trạng thái nhàn rỗi, chờ dữ liệu được truyền từ bộ nhớ chính (main memory). Một phân tích kỹ thuật gần đây bởi đội ngũ Pivot tiết lộ rằng độ trễ bộ nhớ, chứ không phải độ phức tạp tính toán, mới là nút thắt cổ chai chính trong nhiều thao tác cơ sở dữ liệu phổ biến.

Chuyện gì đã xảy ra

Đội ngũ kỹ sư của Pivot đã điều tra lý do tại sao engine cơ sở dữ liệu của họ dường như hoạt động hết công suất nhưng lại chạy chậm hơn dự kiến trong các truy vấn tổng hợp (aggregation queries). Họ tập trung vào một thao tác GROUP BY tiêu chuẩn sử dụng bộ dữ liệu ClickBench, chứa 99 triệu dòng. Truy vấn cụ thể đếm số lần xuất hiện của các UserID duy nhất, tạo ra khoảng 17 triệu nhóm riêng biệt. Để cô lập các đặc điểm hiệu suất, họ đã chạy thử nghiệm trên một máy chủ bare-metal với bộ vi xử lý AMD EPYC 9124 Genoa, ghim quy trình vào một lõi đơn để tránh nhiễu từ các tác vụ song song.

Triển khai ban đầu sử dụng phương pháp ngây thơ (naive approach): một HashTable duy nhất phát triển theo thời gian, được đánh chỉ mục bằng UserID. Với mỗi dòng, engine kiểm tra xem khóa có tồn tại hay không và cập nhật số lượng. Phương pháp này mất 3,9 giây cho lần chạy đầu tiên và 3,6 giây cho các lần chạy tiếp theo khi dữ liệu đã được lưu trữ trong page cache của hệ điều hành. Mặc dù những thời gian này có vẻ chấp nhận được, các bộ đếm hiệu suất nội bộ lại kể một câu chuyện khác. CPU dành một nửa thời gian chỉ để chờ hoàn tất các truy cập bộ nhớ mà không làm gì cả.

Để giải quyết vấn đề này, các kỹ sư đã thay thế HashTable duy nhất bằng thuật toán phân vùng radix hai giai đoạn. Trong giai đoạn đầu tiên, họ băm (hash) từng khóa và phân phối các dòng vào 4.096 phân vùng riêng biệt dựa trên các bit hàng đầu của giá trị hash. Trong giai đoạn thứ hai, họ xử lý từng phân vùng riêng lẻ, tạo ra các HashTable tạm thời nhỏ cho mỗi phân vùng. Thay đổi này giảm thời gian truy vấn xuống còn 2,7 giây đối với dữ liệu đã được cache. Các tối ưu hóa thêm trong engine Pivot cuối cùng đã đưa thời gian trên một lõi xuống còn 1,8 giây, chứng minh rằng những thay đổi cấu trúc trong mẫu truy cập dữ liệu mang lại lợi ích đáng kể.

Cách thức hoạt động

Các CPU hiện đại thực thi lệnh nhanh hơn nhiều so với khả năng lấy dữ liệu từ RAM. Một bộ vi xử lý có thể hoàn thành 4 đến 8 lệnh mỗi nano giây, trong khi một lần truy cập DRAM đơn lẻ mất từ 50 đến 100 nano giây. Trong khoảng thời gian chờ đợi đó, CPU lẽ ra đã có thể thực hiện hàng trăm lệnh. Để giảm thiểu vấn đề này, CPU sử dụng các cache phân cấp (L1, L2, L3) nhanh hơn nhiều nhưng dung lượng nhỏ hơn bộ nhớ chính. Nếu dữ liệu cần thiết cho một thao tác không có trong cache, CPU sẽ bị đình trệ (stall).

Trong triển khai GROUP BY ngây thơ, HashTable phát triển để chứa hàng triệu khóa duy nhất. Khi bảng mở rộng, nó vượt quá dung lượng của các cache L1 và L2. Do đó, hầu như mọi lần tìm kiếm đều dẫn đến "cache miss" (lỗi cache), buộc CPU phải lấy dữ liệu từ DRAM chậm chạp. Các chỉ số hiệu suất đã xác nhận điều này: 50,1% chu kỳ CPU bị đình trệ do giới hạn bộ nhớ, và chỉ 16,1% chu kỳ được sử dụng cho công việc thực tế. Hệ thống đã kéo 4,5GB dữ liệu từ DRAM cho truy vấn này.

Phân vùng radix giải quyết vấn đề này bằng cách chia bài toán thành các khối nhỏ hơn vừa khít với cache. Bằng cách chia 17 triệu khóa thành 4.096 phân vùng, HashTable của từng phân vùng riêng lẻ vẫn đủ nhỏ để nằm trong cache L1 hoặc L2 nhanh chóng trong quá trình xử lý. Mặc dù cách tiếp cận này yêu cầu tổng số lệnh nhiều hơn—băm hai lần và quản lý nhiều vector—nó giảm đáng kể số lần truy cập DRAM chậm. Chạy tối ưu hóa cho thấy các lần đình trệ bộ nhớ giảm xuống còn 23,5% và công việc hữu ích tăng lên 32,2%. Lưu lượng DRAM giảm xuống còn 1,5GB, chứng minh rằng việc đánh đổi thêm lệnh CPU để lấy tính cục bộ cache tốt hơn là một chiến lược thắng lợi.

Chi tiết quan trọng

  • Truy cập bộ nhớ vào DRAM chậm hơn 50-100 lần so với truy cập cache L1, gây ra các lần CPU bị đình trệ đáng kể.
  • Phương pháp HashTable ngây thơ khiến 50,1% chu kỳ CPU bị lãng phí cho việc chờ bộ nhớ.
  • Phân vùng radix chia dữ liệu thành 4.096 khối, cho phép các tập dữ liệu làm việc (working sets) vừa khít với cache CPU.
  • Cách tiếp cận phân vùng được tối ưu hóa giảm chuyển dữ liệu DRAM từ 4,5GB xuống còn 1,5GB cho cùng một truy vấn.
  • Mặc dù thực hiện tổng số lệnh nhiều hơn, phương pháp thân thiện với cache đã giảm thời gian truy vấn hơn 25%.
  • Hiệu suất tối ưu hóa cuối cùng trên một lõi đạt 1,8 giây, trong khi thực thi song song 16 lõi đạt 171ms.

Tại sao điều này quan trọng

Đối với các kỹ sư phần mềm và nhà phát triển cơ sở dữ liệu, phân tích này nhấn mạnh rằng các chỉ số sử dụng CPU có thể gây hiểu lầm. Một máy chủ hiển thị mức sử dụng CPU 100% thực ra có thể đang thiếu hụt dữ liệu chứ không phải sức mạnh tính toán. Việc hiểu rõ sự khác biệt giữa khối lượng công việc bị giới hạn bởi tính toán (compute-bound) và khối lượng công việc bị giới hạn bởi bộ nhớ (memory-bound) là rất quan trọng để tinh chỉnh hiệu suất hiệu quả. Tối ưu hóa cho tính cục bộ cache thường mang lại cải thiện lớn hơn so với việc chỉ đơn thuần thêm nhiều lõi hoặc nâng cấp tốc độ xung nhịp.

Insight này áp dụng rộng rãi cho bất kỳ hệ thống nào xử lý các tập dữ liệu lớn với các thao tác tìm kiếm hoặc tổng hợp thường xuyên. Các nhà phát triển xây dựng backend hiệu suất cao nên cân nhắc bố cục dữ liệu và mẫu truy cập ngay từ giai đoạn đầu của quy trình thiết kế. Các thuật toán trông nặng nề hơn về mặt tính toán trên giấy có thể hoạt động tốt hơn trong thực tế nếu chúng tôn trọng các ràng buộc vật lý của phân cấp bộ nhớ. Bỏ qua những thực tế phần cứng này có thể dẫn đến các hệ thống mở rộng kém mặc dù có nguồn tài nguyên tính toán dồi dào.

Bạn có thể làm gì

  • Sử dụng các công cụ như perf stat để đo lường các chu kỳ CPU bị đình trệ do bộ nhớ và tỷ lệ lỗi cache, không chỉ thời gian CPU.
  • Phân tích các mẫu truy cập dữ liệu của bạn để xác định các thao tác chạm vào các vùng bộ nhớ lớn và ngẫu nhiên.
  • Cân nhắc phân vùng các tập dữ liệu lớn thành các khối nhỏ hơn vừa khít với kích thước cache L2 hoặc L3.
  • Đánh giá các thuật toán dựa trên tính cục bộ cache, không chỉ dựa trên độ phức tạp thời gian lý thuyết.
  • Ghim các quy trình quan trọng vào các lõi CPU cụ thể trong quá trình benchmarking để giảm nhiễu từ việc chuyển đổi ngữ cảnh (context switching).
  • Giám sát việc sử dụng băng thông DRAM như một chỉ số chính cho các nút thắt cổ chai bị giới hạn bởi bộ nhớ trong ứng dụng của bạn.
  • #database performance
  • #cpu architecture
  • #memory latency
  • #backend engineering
  • #optimization

Công cụ từ cửa hàng Bytechap

Đọc tiếp

Tất cả bài viết