Tối ưu hóa băng thông tổng hợp TB với JuiceFS
Giới thiệu
Với sự phát triển bùng nổ của khối lượng dữ liệu và quy mô mô hình, các kịch bản mà nhiều khách hàng thường xuyên truy cập cùng một dữ liệu ngày càng trở nên phổ biến. Việc sử dụng bộ nhớ đệm phân tán giúp tổng hợp bộ nhớ đệm cục bộ của nhiều nút để hình thành một bể bộ nhớ đệm lớn, từ đó cải thiện tỷ lệ trúng bộ nhớ đệm, tăng cường băng thông đọc và IOPS, giảm độ trễ đọc và đáp ứng nhu cầu hiệu suất cao.
Tuy nhiên, việc trao đổi dữ liệu giữa các nút phụ thuộc nhiều vào hiệu suất mạng. Băng thông không đủ có thể hạn chế tốc độ truyền dữ liệu và làm tăng độ trễ; độ trễ mạng cao có thể ảnh hưởng đến khả năng phản hồi của bộ nhớ đệm và giảm hiệu quả của hệ thống. Ngoài ra, tài nguyên CPU tiêu tốn trong quá trình xử lý dữ liệu mạng cũng có thể trở thành một nút thắt cổ chai, kìm hãm hiệu suất tổng thể.
Để giải quyết những vấn đề này, phiên bản JuiceFS Enterprise Edition 5.2 vừa được phát hành đã giới thiệu nhiều tối ưu hóa cho việc truyền tải mạng giữa các nút bộ nhớ đệm. Điều này đã giải quyết hiệu quả một số khía cạnh hiệu suất và cải thiện khả năng sử dụng băng thông của thẻ mạng (NIC). Sau khi tối ưu hóa, tải CPU của khách hàng đã giảm hơn 50%, và tải CPU của nút bộ nhớ đệm giảm xuống còn một phần ba so với mức trước tối ưu hóa. Băng thông đọc tổng hợp đạt 1.2 TB/s, gần như bão hòa băng thông mạng TCP/IP (dựa trên cụm bộ nhớ đệm phân tán với 100 nút GCP 100 Gbps).
Tối ưu hóa hiệu suất mạng Golang
Trong các môi trường quy mô lớn, chẳng hạn như khi hàng ngàn khách hàng truy cập dữ liệu từ hơn 100 nút bộ nhớ đệm phân tán, số lượng kết nối mạng đến các nút bộ nhớ đệm tăng lên đáng kể, dẫn đến tải trọng lập lịch Golang tăng và giảm hiệu suất sử dụng băng thông mạng.
Chúng tôi đã giải quyết hai vấn đề chính là tái sử dụng kết nối và kích hoạt sự kiện epoll bằng cách giới thiệu các cơ chế đa dạng hóa và tối ưu hóa ngưỡng kích hoạt sự kiện. Điều này đã giảm áp lực hệ thống và cải thiện hiệu suất xử lý dữ liệu.
Đa dạng hóa
Nếu một kết nối chỉ có thể xử lý một yêu cầu tại một thời điểm, khả năng đồng thời tổng thể của hệ thống sẽ bị giới hạn bởi số lượng kết nối. Để cải thiện khả năng đồng thời, một số lượng lớn kết nối phải được thiết lập. Do đó, chúng tôi đã giới thiệu việc đa dạng hóa kết nối. Nó cho phép nhiều gói yêu cầu được gửi đồng thời qua cùng một kết nối đến phía đối tác. Thông qua việc đa dạng hóa, khả năng thông lượng của một kết nối có thể được cải thiện hiệu quả.
Vì các kết nối TCP đơn có nút thắt về hiệu suất, chúng tôi đã giới thiệu khả năng điều chỉnh động số lượng kết nối dựa trên lưu lượng thời gian thực trong kiến trúc đa dạng hóa để đạt được sự cân bằng tốt nhất giữa hiệu suất và tài nguyên. Hệ thống tự động tăng hoặc giảm số lượng kết nối dựa trên lưu lượng mạng hiện tại:
- Khi khối lượng yêu cầu của người dùng cao, lưu lượng tổng tiếp tục tăng và các kết nối hiện có không còn đáp ứng được yêu cầu băng thông, hệ thống tự động tăng số lượng kết nối để cải thiện hiệu quả sử dụng băng thông.
- Khi các yêu cầu trở nên không hoạt động và tổng băng thông giảm, hệ thống tự động giảm số lượng kết nối để tránh lãng phí tài nguyên và vấn đề phân mảnh gói.
Một lợi thế khác của việc đa dạng hóa là hỗ trợ gộp gói nhỏ. Thông qua việc quản lý kết nối hiệu quả, nhiều luồng dữ liệu gói nhỏ có thể được truyền cùng nhau qua cùng một kết nối vật lý. Điều này giảm bớt các truyền tải mạng cũng như tải trọng từ các cuộc gọi hệ thống và chuyển đổi không gian giữa nhân và không gian người dùng.
Cụ thể, ở phía người gửi, luồng gửi liên tục lấy nhiều yêu cầu từ kênh gửi cho đến khi kênh trống hoặc dữ liệu tổng cộng cần gửi đạt 4 KiB trước khi gửi chúng cùng nhau. Điều này gộp nhiều gói nhỏ thành một khối dữ liệu lớn hơn, giảm cuộc gọi hệ thống và truyền tải mạng. Ở phía người nhận, dữ liệu từ NIC được đọc theo từng khối vào một bộ đệm vòng, từ đó các dịch vụ cấp trên lấy các đoạn một cách tuần tự và phân tích ra các gói dữ liệu ứng dụng hoàn chỉnh.
Cài đặt ngưỡng nhận (SO_RCVLOWAT)
Trong khung mạng của Golang, chế độ kích hoạt bờ của epoll được sử dụng mặc định để xử lý các socket. Một sự kiện epoll được tạo ra khi trạng thái của socket thay đổi (ví dụ, từ không có dữ liệu đến có dữ liệu).
Để giảm tải trọng bổ sung do các sự kiện thường xuyên được kích hoạt bởi một lượng nhỏ dữ liệu, chúng tôi đã thiết lập SO_RCVLOWAT (ngưỡng thấp cho việc nhận socket) để kiểm soát kernel kích hoạt các sự kiện epoll chỉ khi khối lượng dữ liệu trong bộ đệm nhận socket đạt một số byte nhất định. Điều này giảm số lượng cuộc gọi hệ thống được kích hoạt bởi các sự kiện thường xuyên và làm giảm tải trọng I/O mạng.
Ví dụ:
go
conn, _ := net.Dial("tcp", "example.com:80")
syscall.SetsockoptInt(conn.(net.TCPConn).File().Fd(), syscall.SOL_SOCKET, syscall.SO_RCVLOWAT, 512*1024)
Sau khi thử nghiệm, trong các kịch bản kết nối đồng thời cao, số lượng sự kiện epoll đã giảm 90% xuống khoảng 40,000 mỗi giây. Hiệu suất bộ nhớ đệm trở nên ổn định hơn, với tải CPU duy trì khoảng 1 core cho mỗi GB băng thông.
Tối ưu hóa zero-copy: giảm tiêu thụ CPU và bộ nhớ
Trong giao tiếp mạng Linux, zero-copy là một kỹ thuật giúp giảm hoặc loại bỏ việc sao chép dữ liệu không cần thiết giữa không gian nhân và không gian người dùng để giảm tiêu thụ CPU và bộ nhớ, từ đó cải thiện hiệu quả truyền tải dữ liệu. Nó đặc biệt thích hợp cho các kịch bản chuyển dữ liệu quy mô lớn. Các sản phẩm cổ điển như nginx, Kafka, và haproxy sử dụng công nghệ zero-copy để tối ưu hóa hiệu suất.
Một số công nghệ zero-copy phổ biến bao gồm cuộc gọi hệ thống mmap, cũng như các cơ chế như sendfile, splice, tee, vmsplice, và MSG_ZEROCOPY.
sendfilelà một cuộc gọi hệ thống do Linux cung cấp để đọc dữ liệu tệp trực tiếp từ đĩa vào bộ đệm kernel và truyền nó đến bộ đệm socket thông qua DMA mà không đi qua không gian người dùng.splicecho phép di chuyển dữ liệu trực tiếp trong không gian kernel, hỗ trợ truyền dữ liệu giữa bất kỳ hai mô tả tệp nào (với ít nhất một đầu là một pipe). Sử dụng một pipe làm trung gian, dữ liệu được chuyển từ mô tả đầu vào (ví dụ, một tệp) đến mô tả đầu ra (ví dụ, một socket), trong khi kernel chỉ thao tác các con trỏ trang, tránh việc sao chép dữ liệu thực tế.
So với sendfile, splice linh hoạt hơn, hỗ trợ các kịch bản truyền không phải tệp (chẳng hạn như chuyển tiếp giữa các socket), và thích hợp hơn cho các môi trường mạng đồng thời cao, chẳng hạn như máy chủ proxy. sendfile có thể giảm số lượng cuộc gọi hệ thống nhưng chặn luồng hiện tại, ảnh hưởng đến hiệu suất đồng thời. Chúng tôi đã sử dụng splice và sendfile trong các kịch bản khác nhau để tối ưu hóa các luồng truyền dữ liệu nhằm đạt được kết quả tốt nhất.
Lấy splice làm ví dụ, khi một khách hàng yêu cầu dữ liệu tệp, họ gửi yêu cầu đến nút bộ nhớ đệm thông qua bộ nhớ đệm phân tán. Chúng tôi đã sử dụng công nghệ zero-copy splice để tối ưu hóa luồng dữ liệu ở phía người gửi. Đường truyền dữ liệu cụ thể như sau:
Khi dữ liệu bộ nhớ đệm cần được gửi đến khách hàng, quy trình sử dụng giao diện splice để đọc dữ liệu trực tiếp từ tệp vào bộ đệm kernel (bộ đệm trang). Dữ liệu sau đó được truyền qua splice đến một pipe đã được tạo trước đó, và dữ liệu trong pipe tiếp tục được truyền qua splice đến bộ đệm socket. Vì dữ liệu vẫn nằm trong không gian kernel trong suốt quá trình này, chỉ có hai lần sao chép DMA xảy ra mà không có bất kỳ sao chép CPU nào, đạt được dịch vụ bộ nhớ đệm zero-copy.
Mặc dù vẫn còn tiềm năng tối ưu hóa ở phía người nhận, nhưng do yêu cầu cao về phần cứng và môi trường runtime cho việc triển khai zero-copy và tính không phổ quát của nó, chúng tôi chưa giới thiệu các giải pháp liên quan trong kiến trúc hiện tại.
Tối ưu hóa quy trình kiểm tra CRC
Trong các phiên bản trước, việc đọc một khối dữ liệu từ bộ nhớ đệm phân tán yêu cầu hai lần kiểm tra CRC (chu kỳ dư):
- Giai đoạn tải đĩa: Khi tải dữ liệu từ đĩa vào bộ nhớ, để kiểm tra xem dữ liệu trên đĩa có bị hỏng bit hay không.
- Giai đoạn truyền mạng: Người gửi tính toán lại CRC và ghi vào tiêu đề gói. Người nhận tính toán lại CRC sau khi nhận dữ liệu và so sánh với giá trị trong tiêu đề gói để đảm bảo dữ liệu không bị thay đổi hoặc hỏng trong quá trình truyền tải.
Để giảm tải trọng CPU không cần thiết, phiên bản mới đã tối ưu hóa quy trình kiểm tra CRC trong quá trình truyền mạng. Người gửi không còn tính toán lại CRC mà thay vào đó sử dụng giá trị CRC đã lưu trên đĩa, gộp nó vào một tổng CRC viết vào tiêu đề gói. Người nhận chỉ thực hiện một lần CRC sau khi nhận dữ liệu.
Để hỗ trợ đọc ngẫu nhiên, các khối dữ liệu được lưu trữ cần có giá trị CRC được tính toán theo từng đoạn—mỗi 32 KB. Tuy nhiên, quá trình truyền mạng yêu cầu giá trị CRC cho tất cả dữ liệu được truyền. Do đó, chúng tôi đã gộp hiệu quả các giá trị CRC phân đoạn thành một giá trị CRC tổng thể bằng phương pháp bảng tra cứu, với tải trọng gần như không đáng kể. Phương pháp này giảm một lần tính toán CRC trong khi đảm bảo tính nhất quán và toàn vẹn dữ liệu, giảm tiêu thụ CPU của người gửi và cải thiện hiệu suất tổng thể.
Kết luận
Trong các kịch bản như huấn luyện và suy diễn mô hình quy mô lớn, quy mô dữ liệu đang tăng lên với tốc độ chưa từng thấy. Là một thành phần quan trọng kết nối tính toán và lưu trữ, bộ nhớ đệm phân tán phân phối dữ liệu nóng trên nhiều nút, cải thiện đáng kể hiệu suất truy cập hệ thống và khả năng mở rộng. Điều này giúp giảm áp lực cho lưu trữ backend, tăng cường sự ổn định và hiệu quả của toàn bộ hệ thống.
Tuy nhiên, khi phải đối mặt với áp lực dữ liệu và truy cập khách hàng ngày càng tăng, việc xây dựng bộ nhớ đệm phân tán quy mô lớn hiệu suất cao vẫn gặp nhiều thách thức. Do đó, hệ thống bộ nhớ đệm phân tán của JuiceFS, được triển khai dựa trên Golang, đã trải qua các tối ưu hóa sâu sắc xung quanh cơ chế I/O của Golang trong thực tế và giới thiệu các công nghệ then chốt như zero-copy. Điều này giúp giảm tải CPU, cải thiện hiệu quả sử dụng băng thông mạng, và làm cho hệ thống trở nên ổn định và hiệu quả hơn trong các kịch bản tải cao.
Chúng tôi hy vọng rằng một số kinh nghiệm thực tiễn trong bài viết này có thể cung cấp tham khảo cho các nhà phát triển đang đối mặt với các vấn đề tương tự. Nếu bạn có bất kỳ câu hỏi hoặc phản hồi nào cho bài viết này, hãy tham gia thảo luận về JuiceFS trên GitHub và cộng đồng trên Slack.