Làm thế nào để cải thiện đáng kể hiệu năng Java?


23

Nhóm nghiên cứu tại LMAX có bài thuyết trình về cách họ có thể thực hiện 100 nghìn TPS với độ trễ dưới 1 ms . Họ đã sao lưu bài thuyết trình đó bằng một blog , tài liệu kỹ thuật (PDF)chính mã nguồn .

Gần đây, Martin Fowler đã xuất bản một bài báo xuất sắc về kiến ​​trúc LMAX và đề cập rằng họ hiện có thể xử lý sáu triệu đơn hàng mỗi giây và nêu bật một vài bước mà nhóm thực hiện để đạt được thứ tự hiệu suất khác.

Cho đến nay tôi đã giải thích rằng chìa khóa cho tốc độ của Bộ xử lý logic nghiệp vụ là thực hiện mọi thứ một cách tuần tự, trong bộ nhớ. Chỉ cần làm điều này (và không có gì thực sự ngu ngốc) cho phép các nhà phát triển viết mã có thể xử lý 10K TPS.

Sau đó, họ phát hiện ra rằng việc tập trung vào các yếu tố đơn giản của mã tốt có thể đưa điều này vào phạm vi 100K TPS. Điều này chỉ cần mã được bao bọc kỹ lưỡng và các phương thức nhỏ - về cơ bản, điều này cho phép Hotspot thực hiện công việc tối ưu hóa tốt hơn và để CPU hoạt động hiệu quả hơn trong việc lưu trữ mã khi chạy.

Phải mất một chút thông minh hơn để đi lên một thứ tự cường độ khác. Có một số điều mà nhóm LMAX thấy hữu ích để đạt được điều đó. Một là viết các triển khai tùy chỉnh của các bộ sưu tập Java được thiết kế thân thiện với bộ đệm và cẩn thận với rác.

Một kỹ thuật khác để đạt được mức hiệu suất cao nhất đó là tập trung vào kiểm tra hiệu suất. Từ lâu tôi đã nhận thấy rằng mọi người nói rất nhiều về các kỹ thuật để cải thiện hiệu suất, nhưng một điều thực sự tạo ra sự khác biệt là kiểm tra nó

Fowler đã đề cập rằng có một số điều đã được tìm thấy, nhưng anh ta chỉ đề cập đến một cặp vợ chồng.

Có kiến ​​trúc, thư viện, kỹ thuật hoặc "những thứ" khác hữu ích để đạt được mức hiệu suất như vậy không?


11
"Những kiến ​​trúc, thư viện, kỹ thuật hoặc" những thứ "nào khác hữu ích để đạt được mức hiệu suất như vậy?" Hỏi làm gì? Quote đó là các danh sách cuối cùng. Có rất nhiều thứ khác, không thứ nào có tác động tốt của các mặt hàng trong danh sách đó. Bất cứ điều gì khác mà bất cứ ai có thể đặt tên sẽ không hữu ích như danh sách đó. Tại sao yêu cầu những ý tưởng tồi khi bạn trích dẫn một trong những danh sách tối ưu hóa tốt nhất từng được tạo ra?
S.Lott

Sẽ thật tuyệt khi tìm hiểu những công cụ mà họ đã sử dụng để xem cách mã được tạo chạy trên hệ thống.

1
Tôi đã nghe nói về những người thề bằng tất cả các loại kỹ thuật. Những gì tôi đã tìm thấy hiệu quả nhất là hồ sơ cấp hệ thống. Nó có thể cho bạn thấy những vướng mắc trong cách chương trình và khối lượng công việc của bạn đang thực hiện hệ thống. Tôi sẽ đề nghị tuân thủ các hướng dẫn nổi tiếng về hiệu suất và viết mã mô-đun để bạn có thể dễ dàng điều chỉnh nó sau này ... Tôi không nghĩ bạn có thể sai với cấu hình hệ thống.
ritesh

Câu trả lời:


21

Có tất cả các loại kỹ thuật để xử lý giao dịch hiệu suất cao và một trong những bài viết của Fowler chỉ là một trong số nhiều kỹ thuật mới. Thay vì liệt kê một loạt các kỹ thuật có thể hoặc không thể áp dụng cho tình huống của bất kỳ ai, tôi nghĩ tốt hơn là nên thảo luận về các nguyên tắc cơ bản và cách LMAX giải quyết một số lượng lớn chúng.

Đối với một hệ thống xử lý giao dịch quy mô cao, bạn muốn thực hiện tất cả những điều sau đây càng nhiều càng tốt:

  1. Giảm thiểu thời gian dành cho các tầng lưu trữ chậm nhất. Từ nhanh nhất đến chậm nhất trên một máy chủ hiện đại bạn có: CPU / L1 -> L2 -> L3 -> RAM -> Đĩa / LAN -> WAN. Bước nhảy từ ngay cả đĩa từ hiện đại nhanh nhất sang RAM chậm nhất là hơn 1000 lần để truy cập tuần tự ; truy cập ngẫu nhiên thậm chí còn tồi tệ hơn.

  2. Giảm thiểu hoặc loại bỏ thời gian chờ đợi . Điều này có nghĩa là chia sẻ càng ít trạng thái càng tốt, và, nếu trạng thái phải được chia sẻ, tránh các khóa rõ ràng bất cứ khi nào có thể.

  3. Truyền bá khối lượng công việc. CPU đã không nhanh hơn nhiều trong vài năm qua, nhưng chúng đã nhỏ hơn và 8 lõi là khá phổ biến trên máy chủ. Ngoài ra, bạn thậm chí có thể truyền bá công việc trên nhiều máy, đó là cách tiếp cận của Google; điều tuyệt vời ở đây là nó thu nhỏ mọi thứ kể cả I / O.

Theo Fowler, LMAX sử dụng cách tiếp cận sau đây cho từng điều sau:

  1. Giữ tất cả trạng thái trong bộ nhớ mọi lúc. Hầu hết các công cụ cơ sở dữ liệu thực sự sẽ làm điều này bằng mọi cách, nếu toàn bộ cơ sở dữ liệu có thể nằm gọn trong bộ nhớ, nhưng chúng không muốn để lại bất cứ điều gì có thể xảy ra, điều này có thể hiểu được trên nền tảng giao dịch thời gian thực. Để giải quyết vấn đề này mà không gây thêm rủi ro, họ đã phải xây dựng một loạt các cơ sở hạ tầng dự phòng và dự phòng nhẹ.

  2. Sử dụng hàng đợi không khóa ("người gây rối") cho luồng sự kiện đầu vào. Ngược lại với độ bền cao truyền thống hàng đợi tin nhắn mà là dứt khoát không khóa miễn phí, và trong thực tế thường liên quan đến đau đớn chậm giao dịch phân tán .

  3. Không nhiều. LMAX ném cái này dưới xe buýt trên cơ sở khối lượng công việc phụ thuộc lẫn nhau; kết quả của một thay đổi các tham số cho những người khác. Đây là một cảnh báo quan trọng , và một điều mà Fowler gọi rõ ràng. Họ làm làm cho một số sử dụng đồng thời để cung cấp khả năng chuyển đổi dự phòng, nhưng tất cả các logic kinh doanh được xử lý trên một chủ đề duy nhất .

LMAX không phải là cách tiếp cận duy nhất đối với OLTP quy mô cao. Và mặc dù nó khá tuyệt vời theo đúng nghĩa của nó, bạn không cần phải sử dụng các kỹ thuật vượt trội để có thể đạt được mức hiệu suất đó.

Trong tất cả các nguyên tắc trên, # 3 có lẽ là quan trọng nhất và hiệu quả nhất, bởi vì, thẳng thắn, phần cứng là rẻ. Nếu bạn có thể phân vùng chính xác khối lượng công việc trên nửa tá lõi và vài chục máy, thì giới hạn của bầu trời đối với các kỹ thuật tính toán song song thông thường . Bạn sẽ ngạc nhiên về số lượng thông lượng bạn có thể rút ra mà không có gì ngoài một loạt các hàng đợi tin nhắn và một nhà phân phối vòng tròn. Rõ ràng là nó không hiệu quả như LMAX - thực sự thậm chí không gần - nhưng thông lượng, độ trễ và hiệu quả chi phí là những mối quan tâm riêng biệt, và ở đây chúng tôi đang nói cụ thể về thông lượng.

Nếu bạn có cùng loại nhu cầu đặc biệt mà LMAX thực hiện - cụ thể là trạng thái chia sẻ tương ứng với thực tế kinh doanh trái ngược với lựa chọn thiết kế vội vàng - thì tôi khuyên bạn nên thử thành phần của chúng, vì tôi chưa thấy nhiều khác phù hợp với những yêu cầu đó. Nhưng nếu chúng ta chỉ đơn giản nói về khả năng mở rộng cao thì tôi mong bạn nên nghiên cứu thêm về các hệ thống phân tán, bởi vì chúng là cách tiếp cận kinh điển được sử dụng bởi hầu hết các tổ chức hiện nay (Hadoop và các dự án liên quan, ESB và các kiến ​​trúc liên quan, CQRS mà Fowler cũng đề cập, và như vậy).

SSD cũng sẽ trở thành một công cụ thay đổi cuộc chơi; có thể tranh luận, họ đã được. Giờ đây, bạn có thể có bộ nhớ vĩnh viễn với thời gian truy cập tương tự RAM, và mặc dù SSD cấp máy chủ vẫn còn đắt khủng khiếp, cuối cùng chúng sẽ giảm giá khi tỷ lệ chấp nhận tăng. Nó đã được nghiên cứu rộng rãi và kết quả khá khó hiểu và sẽ chỉ trở nên tốt hơn theo thời gian, vì vậy toàn bộ khái niệm "giữ mọi thứ trong bộ nhớ" ít quan trọng hơn trước đây. Vì vậy, một lần nữa, tôi sẽ cố gắng tập trung vào đồng thời bất cứ khi nào có thể.


Thảo luận về các nguyên tắc là các nguyên tắc cơ bản là tuyệt vời và nhận xét của bạn là tuyệt vời và ... trừ khi bài báo của fowler không có tài liệu tham khảo trong các ghi chú để lưu trữ các thuật toán lãng quên en.wikipedia.org/wiki/Cache-oblivious_alacticm (phù hợp với loại số 1 bạn có ở trên) Tôi sẽ không bao giờ vấp phải chúng. Vậy ... đối với từng danh mục bạn có ở trên, bạn có biết 3 điều hàng đầu mà một người nên biết không?
Bắc Dakotah

@Dakotah: Tôi thậm chí sẽ không bắt đầu lo lắng về địa phương bộ đệm trừ khi và cho đến khi tôi đã loại bỏ hoàn toàn I / O đĩa, đó là nơi mà phần lớn thời gian được chờ đợi trong phần lớn các ứng dụng. Bên cạnh đó, bạn có ý nghĩa gì với "3 điều hàng đầu mà một người nên biết"? Top 3 cái gì, để biết về cái gì?
Aaronaught

Bước nhảy từ độ trễ truy cập RAM (~ 10 ^ -9 giây) sang độ trễ của đĩa từ (trường hợp trung bình ~ 10 ^ -3s) là một vài đơn đặt hàng có cường độ lớn hơn 1000 lần. Ngay cả SSD vẫn có thời gian truy cập được đo bằng hàng trăm micro giây.
An thần người ngoài hành tinh

@Sedate: Độ trễ có, nhưng đây là câu hỏi về thông lượng hơn là độ trễ thô và một khi bạn có được thời gian truy cập trong quá khứ và vào tổng tốc độ truyền, các đĩa không quá tệ. Đó là lý do tại sao tôi phân biệt giữa truy cập ngẫu nhiên và truy cập liên tiếp; cho các kịch bản truy cập ngẫu nhiên nó không chủ yếu trở thành một vấn đề độ trễ.
Aaronaught

@Aaronaught: Khi đọc lại, tôi cho rằng bạn đúng. Có lẽ một điểm nên được thực hiện là tất cả các truy cập dữ liệu phải tuần tự nhất có thể; lợi ích đáng kể cũng có thể có khi truy cập dữ liệu theo thứ tự từ RAM.
An thần người nước ngoài

10

Tôi nghĩ rằng bài học lớn nhất để học hỏi từ điều này là bạn cần bắt đầu với những điều cơ bản:

  • Các thuật toán tốt, cấu trúc dữ liệu phù hợp và không làm bất cứ điều gì "thực sự ngu ngốc"
  • Mã tốt
  • Kiểm tra năng suất

Trong quá trình kiểm tra hiệu năng, bạn lập hồ sơ mã của mình, tìm các nút thắt cổ chai và sửa từng lỗi một.

Quá nhiều người nhảy ngay vào phần "sửa từng cái một". Họ dành rất nhiều thời gian để viết "triển khai tùy chỉnh các bộ sưu tập java", vì họ chỉ biết rằng toàn bộ lý do hệ thống của họ chậm là do lỗi bộ nhớ cache. Đó có thể là một yếu tố góp phần, nhưng nếu bạn chuyển sang điều chỉnh mã cấp thấp như vậy, bạn có thể bỏ lỡ vấn đề lớn hơn khi sử dụng ArrayList khi bạn nên sử dụng LinkedList hoặc lý do thực sự là hệ thống của bạn chậm là vì ORM của bạn là những đứa trẻ lười biếng tải một thực thể và do đó thực hiện 400 chuyến đi riêng biệt đến cơ sở dữ liệu cho mỗi yêu cầu.


7

Không đặc biệt nhận xét về mã LMAX bởi vì tôi nghĩ rằng đó là amply bị loại bỏ, nhưng đây là một số ví dụ về những điều tôi đã làm đã dẫn đến cải thiện hiệu suất đáng kể.

Như mọi khi, đây là những kỹ thuật nên được áp dụng một khi bạn biết rằng bạn có vấn đề và cần cải thiện hiệu suất - nếu không bạn có thể chỉ đang thực hiện tối ưu hóa sớm.

  • Sử dụng cấu trúc dữ liệu phù hợp và tạo một cấu trúc tùy chỉnh nếu cần - thiết kế cấu trúc dữ liệu chính xác làm giảm bớt sự cải tiến mà bạn sẽ nhận được từ tối ưu hóa vi mô, vì vậy hãy làm điều này trước tiên. Nếu thuật toán của bạn phụ thuộc vào hiệu suất của nhiều lần đọc truy cập ngẫu nhiên O (1) nhanh, hãy đảm bảo bạn có cấu trúc dữ liệu hỗ trợ việc này! Thật đáng để nhảy qua một số vòng để có được điều này, ví dụ như tìm cách mà bạn có thể biểu diễn dữ liệu của mình trong một mảng để khai thác các lần đọc được lập chỉ mục O (1) rất nhanh.
  • CPU nhanh hơn truy cập bộ nhớ - bạn có thể thực hiện khá nhiều phép tính trong thời gian cần thiết để đọc một bộ nhớ ngẫu nhiên nếu bộ nhớ không nằm trong bộ đệm L1 / L2. Nó thường đáng để thực hiện một phép tính nếu nó giúp bạn đọc bộ nhớ.
  • Giúp trình biên dịch JIT với các trường, phương thức và lớp cuối cùng tạo ra các tối ưu hóa cụ thể thực sự giúp trình biên dịch JIT. Ví dụ cụ thể:

    • Trình biên dịch có thể giả định rằng một lớp cuối cùng không có lớp con, vì vậy có thể biến các cuộc gọi phương thức ảo thành các cuộc gọi phương thức tĩnh
    • Trình biên dịch có thể coi một trường cuối cùng tĩnh là một hằng số để cải thiện hiệu suất tốt, đặc biệt nếu sau đó hằng số được sử dụng trong các phép tính có thể được tính toán tại thời điểm biên dịch.
    • Nếu một trường có chứa một đối tượng Java được khởi tạo là cuối cùng, thì trình tối ưu hóa có thể loại bỏ cả kiểm tra null và gửi phương thức ảo. Tốt đẹp.
  • Thay thế các lớp bộ sưu tập bằng các mảng - điều này dẫn đến mã ít đọc hơn và khó bảo trì hơn nhưng hầu như luôn nhanh hơn vì nó loại bỏ một lớp gián tiếp và lợi ích từ nhiều tối ưu hóa truy cập mảng tốt. Thông thường một ý tưởng tốt trong các vòng lặp bên trong / mã nhạy cảm hiệu suất sau khi bạn đã xác định nó là một nút cổ chai, nhưng tránh để lại cho mục đích dễ đọc!

  • Sử dụng nguyên thủy bất cứ nơi nào có thể - nguyên thủy nhanh hơn về cơ bản so với tương đương dựa trên đối tượng của chúng. Đặc biệt, quyền anh bổ sung một lượng chi phí khổng lồ và có thể gây ra tạm dừng GC khó chịu. Đừng cho phép bất kỳ nguyên thủy nào được đóng hộp nếu bạn quan tâm đến hiệu suất / độ trễ.

  • Giảm thiểu khóa cấp thấp - khóa rất đắt ở mức thấp. Tìm cách để tránh khóa hoàn toàn hoặc khóa ở mức độ chi tiết thô để bạn chỉ cần khóa không thường xuyên trên các khối dữ liệu lớn và mã cấp thấp có thể tiến hành mà không phải lo lắng về vấn đề khóa hoặc đồng thời.

  • Tránh phân bổ bộ nhớ - điều này thực sự có thể làm bạn chậm lại vì bộ sưu tập rác JVM rất hiệu quả, nhưng rất hữu ích nếu bạn đang cố gắng đạt độ trễ cực thấp và cần giảm thiểu tạm dừng GC. Có các cấu trúc dữ liệu đặc biệt mà bạn có thể sử dụng để tránh phân bổ - đặc biệt là thư viện http://javolution.org/ là tuyệt vời và đáng chú ý cho những thứ này.

Tôi không đồng ý với việc đưa ra phương pháp cuối cùng . JIT có thể tìm ra rằng một phương thức không bao giờ bị ghi đè. Hơn nữa, trong trường hợp một lớp con được tải sau đó, nó có thể hoàn tác tối ưu hóa. Cũng lưu ý rằng "tránh phân bổ bộ nhớ" cũng có thể làm cho công việc của GC trở nên khó khăn hơn và do đó làm bạn chậm lại - vì vậy hãy thận trọng khi sử dụng.
maaartinus

@maaartinus: liên quan đến finalmột số JIT có thể tìm ra nó, những người khác thì không. Đó là phụ thuộc thực hiện (như nhiều mẹo điều chỉnh hiệu suất). Đồng ý về việc phân bổ - bạn phải điểm chuẩn này. Thông thường tôi đã thấy tốt hơn là loại bỏ phân bổ, nhưng YMMV.
mikera

4

Khác với những gì đã được nêu trong một câu trả lời xuất sắc từ Aaronaught, tôi muốn lưu ý rằng mã như thế có thể khá khó để phát triển, hiểu và gỡ lỗi. "Mặc dù rất hiệu quả ... nhưng nó rất dễ bị hỏng ..." như một trong những người của họ được đề cập trong blog LMAX .

  • Đối với một nhà phát triển đã từng sử dụng các truy vấn và khóa truyền thống , mã hóa cho cách tiếp cận mới có thể giống như cưỡi con ngựa hoang. Ít nhất đó là kinh nghiệm của riêng tôi khi thử nghiệm với Phaser , khái niệm được đề cập trong bài viết kỹ thuật LMAX. Theo nghĩa đó, tôi sẽ nói rằng phương pháp này giao dịch khóa tranh chấp cho sự tranh chấp não của nhà phát triển .

Đưa ra ở trên, tôi nghĩ rằng những người chọn Disruptor và các cách tiếp cận tương tự tốt hơn đảm bảo rằng họ có đủ nguồn lực phát triển để duy trì giải pháp của họ.

Nhìn chung, phương pháp Disruptor có vẻ khá hứa hẹn với tôi. Ngay cả khi công ty của bạn không đủ khả năng sử dụng nó, ví dụ như vì những lý do nêu trên, hãy cân nhắc việc thuyết phục ban quản lý của bạn "đầu tư" một số nỗ lực vào việc nghiên cứu nó (và nói chung là SEDA ) - bởi vì nếu họ không có cơ hội thì một ngày nào đó khách hàng của họ sẽ để họ ủng hộ một số giải pháp cạnh tranh hơn đòi hỏi máy chủ ít hơn 4x, 8x, v.v.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.