2 điểm bởi GN⁺ 2025-03-01 | 1 bình luận | Chia sẻ qua WhatsApp
  • Trước đây, mức sử dụng CPU trên hệ thống của tôi đã lên tới 3.200%, tức là cả 32 lõi đều bị lấp đầy
  • Tôi đang dùng runtime Java 17, và khi kiểm tra thời gian CPU trong thread dump rồi sắp xếp theo thời gian CPU, tôi phát hiện nhiều thread tương tự nhau
  • Phân tích đoạn mã gây ra vấn đề
    • Thông qua stack trace, tôi xác định được dòng 29 trong lớp BusinessLogic
    • Đoạn mã đó có dạng lặp qua danh sách unrelatedObjects rồi chèn giá trị của relatedObject vào treeMap
    • Đây là đoạn mã không hiệu quả vì không sử dụng unrelatedObject bên trong vòng lặp

Sửa mã và kiểm thử

  • Loại bỏ vòng lặp không cần thiết và sửa thành một dòng treeMap.put(relatedObject.a(), relatedObject.b());
  • Tôi đã chạy unit test trước và sau khi sửa, nhưng không thể tái hiện vấn đề
  • Ngay cả khi kích thước của treeMapunrelatedObjects đều vượt quá 1.000.000 phần tử, vấn đề vẫn không xảy ra

Phát hiện nguyên nhân của vấn đề

  • treeMap đang bị nhiều thread truy cập đồng thời và không hề được đồng bộ hóa
  • Đây là vấn đề phát sinh khi nhiều thread cùng lúc sửa đổi TreeMap

Tái hiện vấn đề bằng thực nghiệm

  • Tôi tiến hành một thử nghiệm trong đó nhiều thread cập nhật ngẫu nhiên vào TreeMap dùng chung
  • Thiết lập try-catch để bỏ qua NullPointerException
  • Kết quả thử nghiệm cho thấy mức sử dụng CPU tăng vọt lên tới 500%

Kết luận

  • Việc sửa đổi đồng thời một TreeMap không được đồng bộ hóa có thể gây ra vấn đề hiệu năng nghiêm trọng
  • Để tránh vấn đề này, nên đồng bộ hóa TreeMap hoặc sử dụng collection an toàn luồng như ConcurrentMap

1 bình luận

 
GN⁺ 2025-03-01
Ý kiến Hacker News
  • Tôi từng nghĩ race condition sẽ gây hỏng dữ liệu hoặc deadlock, nhưng chưa nghĩ rằng nó cũng có thể gây ra vấn đề hiệu năng. Dữ liệu có thể bị hỏng theo cách tạo ra vòng lặp vô hạn

    • Tôi cho rằng về nguyên tắc, lỗi, hành vi bất thường hoặc cảnh báo trong dự án đều phải được sửa. Vì chúng có thể gây ra những vấn đề tưởng như không liên quan
    • Việc các collection cốt lõi của Java vốn không thread-safe theo thiết kế là điều đã được biết rõ. OP nên kiểm tra xem ở những phần khác của code có nhiều thread cùng thao tác trên collection hay không
    • Cách khắc phục dễ nhất là bọc TreeMap bằng Collections.synchronizedMap hoặc chuyển sang ConcurrentHashMap rồi sắp xếp khi cần
    • Có thể khiến từng thao tác riêng lẻ trên map trở nên thread-safe, nhưng không thể chắc các thao tác liên tiếp có thread-safe hay không. Cũng không thể chắc đối tượng sở hữu TreeMap là thread-safe hay không
    • Một cách giải quyết còn gây tranh cãi là theo dõi các node đã thăm, nhưng đây không phải cách hay. Collection vẫn không thread-safe và vẫn có thể thất bại theo những cách tinh vi khác
    • Một lập trình viên chú ý đến chi tiết có thể nhận ra tổ hợp thread và TreeMap, hoặc đề xuất không dùng TreeMap nếu không cần các phần tử đã sắp xếp. Nhưng trong trường hợp này đã không như vậy
    • Vấn đề là đã vi phạm hợp đồng của collection, nên dù đổi TreeMap sang HashMap thì vẫn là sai
  • Trong code có nhiều thread hoạt động, chiến lược chắc chắn duy nhất là làm cho mọi đối tượng trở nên bất biến, còn những đối tượng không thể bất biến thì phải được giới hạn trong các phần nhỏ, tự khép kín và được kiểm soát nghiêm ngặt

    • Chúng tôi đã viết lại module cốt lõi theo các nguyên tắc này, và nó đã chuyển từ một nguồn gây sự cố dai dẳng thành một trong những phần codebase bền vững nhất
    • Nhờ có các hướng dẫn này mà việc review code trở nên dễ hơn rất nhiều
  • Câu "gần như không thể ssh vào" khiến tôi nhớ đến thời học cao học dùng Sun UltraSparc 170

    • Một người dùng mới hoặc sinh viên đã cố chạy công việc song song, chia một tệp văn bản lớn thành nhiều phần theo số dòng rồi xử lý các phần đó song song
    • Rất nhiều RAM bị dùng hết, và các nỗ lực swap đã phải seek điên cuồng để đọc những phần khác nhau của cùng một tệp
    • Không thể có được dấu nhắc đăng nhập từ console, nhưng đã có một phiên đăng nhập sẵn và có thể lấy root session để xử lý vấn đề
    • Vấn đề là không hiểu được giới hạn của hệ thống
  • Có thể đơn giản rút gọn code thành như sau

    • Code gốc chỉ thực hiện <i>treeMap.put</i> khi <i>unrelatedObjects</i> không rỗng. Đây có thể là một bug
    • Cần kiểm tra xem <i>a</i> và <i>b</i> có luôn trả về cùng một giá trị hay không, và xem <i>treeMap</i> có thực sự hoạt động như một map hay không
  • Một cách khác để tạo ra vòng lặp vô hạn là dùng triển khai <i>Comparator</i> hoặc <i>Comparable</i> không thực hiện một thứ tự toàn phần nhất quán

    • Điều này không liên quan đến concurrency, và có thể xảy ra tùy theo dữ liệu cụ thể và thứ tự xử lý
  • Có thể cân nhắc dùng bộ đếm tăng dần để phát hiện chu kỳ, và ném ngoại lệ nếu vượt quá độ sâu của cây hoặc kích thước collection

    • Cách này gần như không tốn thêm bộ nhớ hay CPU, và có khả năng được chấp nhận hơn
  • Trong Java, thực hiện thao tác đồng thời trên các đối tượng không thread-safe tạo ra những bug thú vị nhất

  • Có câu hỏi liệu một TreeMap không được bảo vệ có thể gây ra mức sử dụng 3,200% hay không

    • Tôi đã từng thấy vấn đề tương tự vào khoảng năm 2009, và nó vẫn có thể xảy ra
    • Thật thất vọng đối với những ai nghĩ data race chỉ là một vấn đề hơi tệ một chút
  • Tác giả đã phát hiện ra một dạng Poison Pill. Điều này phổ biến hơn trong các hệ thống event sourcing, nơi một thông điệp giết chết mọi thứ mà nó chạm vào

    • Khi cấu trúc dữ liệu rơi vào trạng thái bất hợp pháp, mọi thread về sau đều sẽ mắc kẹt trong cùng một quả bom logic
  • Ngoại lệ trong thread là một vấn đề cực lớn

    • Có câu chuyện săn bug kinh hoàng với C++, select(), và các thread ném ngoại lệ lung tung