Phát triển với Java thời gian thực - Phần 2: Cải thiện chất lượng dịch vụ
Tóm tắt: Một số ứng dụng Java™ không cung cấp được chất lượng hợp lý của
dịch vụ mặc dù đạt được các mục tiêu hiệu năng khác, chẳng hạn như thời gian trễ
trung bình hoặc thông lượng tổng thể. Bằng cách đưa ra các đoạn dừng hoặc ngắt
không chịu kiểm soát của ứng dụng, ngôn ngữ Java và hệ thống thời gian chạy đôi
khi có thể chịu trách nhiệm về không đáp ứng các độ đo hiệu năng của ứng dụng.
Bài viết này là bài thứ hai trong loạt bài ba phần, giải thích nguồn gốc căn nguyên
của trễ và ngắt trong một JVM và mô tả các kỹ thuật cho phép bạn có thể dùng để
giảm thiểu các căn nguyên, nhằm ứng dụng của bạn cung cấp chất lượng dịch vụ
ổn định hơn.
Tính đa dạngtrong một ứng dụng Java —thường gây ra do các đoạn dừng, hoặc
trễ, xảy ra vào nhữnglúc không thể đoán trước được —có thể xảy ra qua ngăn
xếp phần mềm. Các trễ có thể xuất hiện do:
Phần cứng (trong các quá trình xử lý chẳng hạn như nhớ nhanh).
Phần đệm (xử lý của các ngắt quản lý hệ thống chẳng hạn như dữ liệu về
nhiệt độ bộ xử lý trungtâm).
Hệ điều hành (trả lời một ngắt hoặc khai thác một hoạt động thông minh đã
lên lịch thường kì).
Các chương trình khác chạy trên cùng hệ thống.
JVM (gom rác, biên dịch Đúng lúc, và tải lớp).
Chính ứng dụng Java.
is 603 operations / second Histogram of operation times: 9ms - 10ms 9942 99 % 10ms - 11ms 2 0 % 11ms - 12ms 32 0 % 30ms - 40ms 4 0 % 70ms - 80ms 1 0 % 200ms - 300ms 6 0 % 400ms - 500ms 6 0 % 500ms - 542ms 6 0 % Bạn có thể thấy rằng hầu như tất cả các phép toán hoàn tất trong 10 mili-giây, nhưng một số phép toán mất hơn một nửa giây (chậm hơn 50 lần.) Đó đúng là một sự khác nhau! Chúng ta hãy xem cách chúng ta có thể loại bỏ một số thay đổi này bằng cách loại bỏ các trễ xuất hiện do việc nạp lớp Java, Biên dịch mã riêng JIT, GC, và xử lí. Đầu tiên chúng ta đã thu thập danh sách các lớp được nạp bởi ứng dụng qua việc chạy đầy đủ với -verbose:class. Chúng ta đã lưu lại kết quả vào một tệp và sau đó sửa đổi nó để có một tên được định dạng thích hợp trên mỗi dòng của tệp đó. Chúng ta đã gộp một phương thức preload() vào lớp Server để nạp các lớp, JIT biên dịch tất cả các phương thức của các lớp đó, và sau đó vô hiệu hóa bộ biên dịch JIT, như trình bày trong Liệt kê 9: Liệt kê 9. Nạp trước các lớp và các phương thức cho máy chủ private void preload(String classesFileName) { try { FileReader fReader = new FileReader(classesFileName); BufferedReader reader = new BufferedReader(fReader); String className = reader.readLine(); while (className != null) { try { Class clazz = Class.forName(className); String n = clazz.getName(); Compiler.compileClass(clazz); } catch (Exception e) { } className = reader.readLine(); } } catch (Exception e) { } Compiler.disable(); } Việc nạp lớp không phải là một vấn đề quan trọng trong máy chủ đơn giản của chúng ta vì phương thức TaskHandler.run() của chúng ta rất đơn giản: một khi lớp nào đó được nạp, việc nạp lớp xảy ra không nhiều sau đó trong khai thác của Server, nó có thể được xác thực bằng cách chạy với -verbose:class. Lợi ích chính thu được từ việc biên dịch các phương thức trước khi chạy bất kỳ phép toán TaskHandler được đo. Mặc dù chúng ta đã có thể sử dụng được một vòng lặp khởi động (warm-up loop), cách tiếp cận này có xu hướng riêng cho JVM vì sự suy nghiệm mà bộ biên dịch JIT sử dụng để chọn ra các phương thức để biên dịch khác với các cài đặt JVM. Việc sử dụng dịch vụ Compiler.compile() đưa vào hoạt động biên dịch có thể điều khiển được nhiều hơn, nhưng như chúng ta đã đề cập trước đó trong bài viết, chúng ta sẽ chờ đợi sự giảm thông lượng khi dùng tiếp cận này. Các kết quả từ việc chạy ứng dụng với các tuỳ chọn này là: $ java -Xms700m -Xmx700m -Xgcpolicy:optthruput Server 6 10000 operations in 20936 ms Throughput is 477 operations / second Histogram of operation times: 11ms - 12ms 9509 95 % 12ms - 13ms 478 4 % 13ms - 14ms 1 0 % 400ms - 500ms 6 0 % 500ms - 527ms 6 0 % Chú ý rằng mặc dù các trễ dài nhất không thay đổi nhiều, biểu đồ này ngắn hơn nhiều so với ban đầu. Nhiều trễ ngắn hơn thấy được ngay từ bộ biên dịch JIT nên việc thực hiện các biên dịch trước đó và sau đó vô hiệu hoá bộ biên dịch JIT rõ ràng là một bước tiến. Một sự nhận xét thú vị khác là số thời gian hoạt động chung đã chiếm thời gian lâu hơn một chút (từ khoảng 9 đến 10 mili-giây, đến 11-12 mili-giây). Các phép toán đã bị chậm lại do chất lượng của mã được tạo ra bởi một biên dịch JIT bị áp đặt trước khi gọi các phương thức đó thường thấp hơn các phương thức của bộ mã được áp dụng đầy đủ. Đó không phải là một kết quả bất ngờ, vì một trong những lợi thế to lớn của bộ biên dịch JIT là khai thác các đặc tính động của ứng dụng đang chạy để làm cho nó chạy hiệu quả hơn. Chúng ta sẽ tiếp tục sử dụng bộ mã nạp trước lớp này và biên dịch trước phương thức này trong phần còn lại của bài viết. Vì GCStressThread của chúng ta gây nên thay đổi đều đặn tập hợp dữ liệu sống, việc sử dụng chính sách GC sản sinh là không mong muốn, để đảm bảo lợi ích nhiều thời gian đoạn dừng hơn. Thay vào đó, chúng ta đã thử bộ gom rác thời gian thực trong sản phẩm Thời gian Thực WebSphere của IBM dùng cho Linux Thời gian Thực V2.0 SR1. Các kết quả ban đầu thật thất vọng, thậm chí sau khi chúng ta đã bổ sung tuỳ chọn -Xgcthreads8, cho phép bộ gom sử dụng 8 xử lí GC chứ không phải xử lí đơn lẻ mặc định. (Bộ gom không thể theo kịp một cách tin cậy tốc độ phân bổ của ứng dụng này với chỉ một xử lí GC đơn lẻ.) $ java -Xms700m -Xmx700m -Xgcpolicy:metronome -Xgcthreads8 Server 6 10000 operations in 72024 ms Throughput is 138 operations / second Histogram of operation times: 11ms - 12ms 82 0 % 12ms - 13ms 250 2 % 13ms - 14ms 19 0 % 14ms - 15ms 50 0 % 15ms - 16ms 339 3 % 16ms - 17ms 889 8 % 17ms - 18ms 730 7 % 18ms - 19ms 411 4 % 19ms - 20ms 287 2 % 20ms - 30ms 1051 10 % 30ms - 40ms 504 5 % 40ms - 50ms 846 8 % 50ms - 60ms 1168 11 % 60ms - 70ms 1434 14 % 70ms - 80ms 980 9 % 80ms - 90ms 349 3 % 90ms - 100ms 28 0 % 100ms - 112ms 7 0 % Việc sử dụng thời gian thực bộ gom đã giảm bớt thời gian phép toán tối đa một cách đáng kể, nhưng nó cũng tăng thêm thời gian phép toán kéo dài. Tồi hơn nữa, tốc độ truyền thông giảm đáng kể. Bước cuối cùng là sử dụng các RealtimeThread — chứ không phải là các xử lí Java thông thường — đối với các xử lí làm việc. Chúng ta đã tạo ra một lớp RealtimeThreadFactory cho phép chúng ta có thể cung cấp cho dịch vụ Executors, như trong Liệt kê 10: Liệt kê 10. Lớp RealtimeThreadFactory import java.util.concurrent.ThreadFactory; import javax.realtime.PriorityScheduler; import javax.realtime.RealtimeThread; import javax.realtime.Scheduler; import javax.realtime.PriorityParameters; class RealtimeThreadFactory implements ThreadFactory { public Thread newThread(Runnable r) { RealtimeThread rtThread = new RealtimeThread(null, null, null, null, null, r); // adjust parameters as needed PriorityParameters pp = (PriorityParameters) rtThread.getSchedulingParameters(); PriorityScheduler scheduler = PriorityScheduler.instance(); pp.setPriority(scheduler.getMaxPriority()); return rtThread; } } Việc vượt qua một cá thể của lớp RealtimeThreadFactory đến dịch vụ Executors.newFixedThreadPool() làm cho các xử lí làm việc là các RealtimeThread bằng cách sử dụng việc lập lịch biểu FIFO với quyền ưu tiên cao nhất sẵn có. Bộ gom rác sẽ vẫn còn ngắt các xử lí này khi nó cần thực hiện công việc, nhưng không có tác vụ ưu-tiên-thấp-hơn nào khác sẽ can thiệp với các xử lí làm việc: $ java -Xms700m -Xmx700m -Xgcpolicy:metronome -Xgcthreads8 Server 6 Handled 10000 operations in 27975 ms Throughput is 357 operations / second Histogram of operation times: 11ms - 12ms 159 1 % 12ms - 13ms 61 0 % 13ms - 14ms 17 0 % 14ms - 15ms 63 0 % 15ms - 16ms 1613 16 % 16ms - 17ms 4249 42 % 17ms - 18ms 2862 28 % 18ms - 19ms 975 9 % 19ms - 20ms 1 0 % Với thay đổi cuối cùng này, chúng ta cải thiện đáng kể cả thời gian phép toán xấu nhất (xuống chỉ còn 19 mili-giây) cũng như là thông lượng tổng thể (lên đến 357 phép toán mỗi giây). Như vậy chúng ta đã cải thiện đáng kể theo độ đa dạng của các thời gian phép toán, nhưng chúng ta đã trả một giá khá đắt về hiệu năng thông lượng. Phép toán của bộ gom rác, sử dụng đến 3 mili-giây cứ mỗi 10 mili-giây, giải thích tại sao một phép toán mà thường mất khoảng 12 mili-giây có thể được kéo dài đến 4 - 5 mili-giây, đó là lý do số lượng lớn các phép toán bây giờ lại mất khoảng 16 - 17 mili-giây. Sự cắt giảm thông lượng gần như chắc chắn nhiều hơn bạn mong đợi vì JVM thời gian thực, ngoài việc sử dụng bộ gom rác thời gian thực Metronome, cũng đã sửa đổi việc khoá các sơ khởi (primitives) cho phép chống được việc đảo ngược quyền ưu tiên, một vấn đề quan trọng khi việc lập lịch biểu FIFO được dùng (xem "Java thời gian thực, Phần 1: Sử dụng mã Java để lập trình các hệ thống thời gian thực"). Đáng tiếc là, việc đồng bộ hoá giữa xử lí chủ (master thread) và các xử lí làm việc tạo nên nhiều gia tăng ảnh hưởng về thông lượng, mặc dù nó không được đo đạc như là một bộ phận của bất kỳ thời gian phép toán (nên nó không thể hiện trong biểu đồ). Như vậy khi máy chủ của chúng ta được lợi từ các sửa đổi được thực hiện để cải thiện khả năng dự đoán, chắc chắn nó trải qua một sự giảm thông lượng khá lớn. Dù sao, nếu vài phép toán chạy lâu khó tin nổi, ứng với mức độ chất lượng không thể chấp nhận được, thì sử dụng các RealtimeThread với một JVM thời gian thực có thể chính là giải pháp đúng. Tóm lược Trong thế giới của các ứng dụng Java, thông lượng và thời gian chờ theo truyền thống đã trở thành các độ đo được ứng dụng chọn và các nhà thiết kế băng chuẩn để báo cáo và tối ưu hoá. Lựa chọn này đã có một tác động rộng khắp lên sự tiến hoá của các thời gian chạy Java được xây dựng nên để cải thiện hiệu năng. Mặc dù các thời gian chạy Java được khởi động như là các phiên dịch viên với thời gian chờ chạy và thông lượng vô cùng chậm, các JVM hiện đại có thể cạnh tranh tốt với các ngôn ngữ khác về các thước đo này đối với nhiều ứng dụng. Mặc dù cho đến thời kỳ tương đối gần đây cũng không thể được nói giống như vậy về một số thước đo khác mà có thể có một ảnh hưởng lớn về một hiệu năng nhận biết của ứng dụng — nhất là độ đa dạng, mà ảnh hưởng đến chất lượng dịch vụ. Việc đưa ra Java thời gian thực đã cho các nhà thiết kế ứng dụng các công cụ mà họ cần phải nhắm đến nguồn thay đổi trong một JVM và trong các ứng dụng của họ để cung cấp chất lượng dịch vụ mà các khách hàng và tác nhân tiêu thụ của họ chờ đợi. Bài viết này đã giới thiệu một số kỹ thuật mà bạn có thể sử dụng để sửa đổi một ứng dụng Java để giảm bớt các đoạn dừng và sự thay đổi mà bắt nguồn từ JVM và từ việc lập lịch biểu xử lí. Việc giảm bớt sự thay đổi thường gây ra việc cắt bớt về thời gian chờ và hiệu năng thông lượng. Mức độ theo đó việc cắt bỏ có thể chấp nhận được xác định các công cụ nào là thích hợp cho một ứng dụng đặc thù. Mục lục Nhằm vào các nguồn biến đổi Một thí dụ về máy chủ Java Tóm lược
File đính kèm:
- Phát triển với Java thời gian thực, Phần 2 Cải thiện chất lượng dịch vụ.pdf