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.

pdf29 trang | Chuyên mục: Java | Chia sẻ: dkS00TYs | Lượt xem: 1904 | Lượt tải: 0download
Tóm tắt nội dung 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ụ, để xem tài liệu hoàn chỉnh bạn click vào nút "TẢI VỀ" ở trên
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:

  • pdfPhá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
Tài liệu liên quan