Як значно покращити продуктивність Java?


23

Команда в LMAX представила презентацію про те, як вони змогли зробити 100 к.с. / с при затримці менше 1 мс . Вони підкріпили цю презентацію блогом , технічним документом (PDF) та самим вихідним кодом .

Нещодавно Мартін Фаулер опублікував чудовий документ про архітектуру LMAX і згадує, що тепер вони здатні обробляти шість мільйонів замовлень в секунду, і виділяє кілька кроків, які команда зробила, щоб збільшити ще один порядок продуктивності.

Поки я пояснював, що ключ до швидкості процесора Business Logic - це робити все послідовно, в пам'яті. Просто це (і нічого насправді нерозумно) дозволяє розробникам писати код, який може обробляти 10K TPS.

Потім вони виявили, що концентрація на простих елементах хорошого коду може піднести це до діапазону 100K TPS. Це просто потребує добре розробленого коду та невеликих методів - по суті це дозволяє Hotspot зробити кращу роботу з оптимізації та для процесорів бути ефективнішими в кешуванні коду під час його роботи.

Щоб піднятися на інший порядок, знадобилося трохи більше кмітливості. Є кілька речей, які команди LMAX виявили корисними, щоб потрапити туди. Один з них полягав у написанні спеціальних реалізацій колекцій Java, які були розроблені таким чином, щоб вони не сприймали кеш-пам’яті та були обережними зі сміттям.

Ще одна методика досягнення цього найвищого рівня продуктивності - це приділення уваги тестуванню продуктивності. Я давно помітив, що люди багато говорять про методи підвищення ефективності, але одне, що насправді має значення, - це перевірити це

Фаулер зазначив, що знайдено кілька речей, але він згадав лише пару.

Чи є інші архітектури, бібліотеки, методи чи "речі", які корисні для досягнення таких рівнів продуктивності?


11
"Які інші архітектури, бібліотеки, методи чи" речі "корисні для досягнення таких рівнів продуктивності?" Навіщо питати? Це цитата остаточний список. Є багато-багато інших речей, жодна з яких не впливає на товари в цьому списку. Все, що хтось може назвати, не буде таким корисним, як цей список. Навіщо просити погані ідеї, коли ви цитуєте один з найкращих списків оптимізації, що коли-небудь створювався?
S.Lott

Було б непогано дізнатися, які інструменти вони використовували, щоб побачити, як генерований код працює в системі.

1
Я чув, як люди клянуться різними методами. Що я вважаю найефективнішим - це профілювання на рівні системи. Це може показати вам вузькі місця в тому, як ваша програма та навантаження здійснюють систему. Я б запропонував дотримуватися добре відомих вказівок щодо продуктивності та написання модульного коду, щоб потім було легко налаштувати його пізніше ... Я не думаю, що ви можете помилитися з систематизацією системи.
ritesh

Відповіді:


21

Існує всіляка техніка для високоефективної обробки транзакцій, і ця у статті Фаулера - лише одна з багатьох на межі кровотечі. Замість того, щоб перелічити купу методів, які можуть або не можуть бути застосовні до чиєїсь ситуації, я думаю, що краще обговорити основні принципи та те, як LMAX вирішує велику кількість їх.

Для широкомасштабної системи обробки транзакцій потрібно максимально виконати всі наступні дії:

  1. Мінімізуйте час, витрачений у найповільніші рівні зберігання. Від найшвидшого до найповільнішого на сучасному сервері у вас є: CPU / L1 -> L2 -> L3 -> RAM -> Disk / LAN -> WAN. Перехід від навіть найшвидшого сучасного магнітного диска до найповільнішої ОЗУ перевищує 1000 разів для послідовного доступу; випадковий доступ ще гірший.

  2. Мінімізуйте або усуньте час, витрачений на очікування . Це означає обмін якомога меншим станом, і, якщо стан потрібно поділити, уникаючи явних блокувань, коли це можливо.

  3. Розподіліть навантаження. Процесори не отримали набагато швидше , в останні кілька років, але вони вже отримали менше, і 8 ядер досить часто зустрічаються на сервері. Крім того, ви навіть можете поширити роботу на декілька машин, що є підходом Google; чудова річ у тому, що вона масштабує все, включаючи введення-виведення.

За словами Фоулера, до кожного з них LMAX застосовує такий підхід:

  1. Залишайте весь стан у пам’яті постійно . Більшість двигунів баз даних дійсно роблять це все одно, якщо вся база даних може вміститись у пам'яті, але вони не хочуть залишати нічого випадковому, що зрозуміло на торговій платформі в режимі реального часу. Щоб зняти це, не додаючи тонни ризику, їм довелося побудувати купу легкої інфраструктури резервного копіювання та відмовлення.

  2. Для потоку вхідних подій використовуйте безблокову чергу ("деструктор"). На відміну від традиційних тривалих черг повідомлень, які остаточно не блокуються, і насправді зазвичай передбачають болісно повільні розподілені транзакції .

  3. Не багато. LMAX кидає цю під шину, виходячи з того, що навантаження взаємозалежні; результат одного змінює параметри для інших. Це критичний застереження, яке Фаулер прямо відкликає. Вони роблять деякі використовують паралелізм для того , щоб забезпечити відмовостійкості, але все бізнес - логіки обробляється на одному потоці .

LMAX - не єдиний підхід до широкомасштабного OLTP. І хоча вона сама по собі є досить блискучою, вам не потрібно використовувати техніку крайнюго кровотоку для того, щоб досягти такого рівня продуктивності.

З усіх вищезазначених принципів, №3, мабуть, є найважливішим та найефективнішим, адже, чесно кажучи, апаратне забезпечення є дешевим. Якщо ви можете правильно розподілити навантаження на півдесятка ядер і кілька десятків машин, то небо є межею для звичайних методів паралельних обчислень . Ви здивуєтеся, скільки пропускної здатності ви можете зняти, не маючи нічого, окрім купи черг на повідомлення та дистрибутора круглої робіни. Це, очевидно, не настільки ефективно, як LMAX - насправді навіть не близько - але пропускна здатність, затримка та економічність є окремими проблемами, і тут ми говоримо конкретно про пропускну здатність.

Якщо у вас є такі ж особливі потреби, що і LMAX - зокрема, спільний стан, який відповідає діловій реальності на відміну від поспішного вибору дизайну - тоді я б запропонував спробувати їх компонент, тому що я не бачив багато інше, що відповідає цим вимогам. Але якщо ми просто говоримо про високу масштабованість, то я б закликав вас зробити більше досліджень розподілених систем, оскільки вони є канонічним підходом, який сьогодні застосовує більшість організацій (Hadoop та пов'язані з ними проекти, ESB та суміжні архітектури, CQRS, які також Фоулер згадки тощо).

SSD також перейдуть на зміну ігор; Можливо, вони вже є. Тепер ви можете мати постійне сховище з аналогічним часом доступу до оперативної пам’яті, і хоча серверні диски з рівнем сервера все ще жахливо дорогі, вони з часом зменшаться в ціні, коли рівень прийняття зросте. Це було широко досліджено, і результати були досить приголомшливими і з часом будуть покращуватися, тому ціла концепція "зберігати все в пам'яті" набагато менш важлива, ніж раніше. Тому я ще раз намагаюся зосередитись на конкурентності, коли це можливо.


Обговорення принципів, що лежать в основі принципів, є чудовим, а ваш коментар - чудовим і ... якщо тільки у папері Фоулера не було посилання на ногу, щоб кешувати забуті алгоритми en.wikipedia.org/wiki/Cache-oblivious_algorithm (що добре вписується в категорія номер 1 у вас є вище) Я б ніколи не натрапляв на них. Отже ... що стосується кожної вищенаведеної вами категорії, чи знаєте ви про топ-3 речі, які людина повинна знати?
Дакота Північ

@Dakotah: Я б навіть не почав турбуватися про місце кешу, якщо і поки я повністю не усунув дисковий ввід / вивід, саме там переважна більшість часу витрачається на очікування переважної більшості програм. Крім цього, що ви маєте на увазі під "топ-3 речами, які людина повинна знати"? Топ 3 що, щоб знати про що?
Aaronaught

Перехід від затримки доступу до оперативної пам’яті (~ 10 ^ -9s) до затримки магнітного диска (~ 10 ^ -3s середній випадок) - це ще кілька порядків більше 1000x. Навіть SSD все ще мають час доступу, виміряні сотнями мікросекунд.
Sedate Alien

@Sedate: Затримка так, але це швидше питання пропускної здатності, ніж сирість затримки, і коли ви отримаєте минулий час доступу та загальну швидкість передачі, диски не такі вже й погані. Тому я зробив різницю між випадковим та послідовним доступом; для випадкових сценаріїв доступу , які він робить в першу чергу стає латентної проблемою.
Aaronaught

@Aaronaught: Після повторного читання я вважаю, що ви прав. Можливо, слід зазначити, що весь доступ до даних повинен бути максимально послідовним; Значні переваги можуть мати і при доступі до даних в порядку з оперативної пам'яті.
Присілок Чужий

10

Я думаю, що найбільший урок, який потрібно навчитися з цього, - це те, що потрібно почати з основ:

  • Хороші алгоритми, відповідні структури даних і нічого не роблять "по-справжньому нерозумно"
  • Добре врахований код
  • Тестування продуктивності

Під час тестування продуктивності ви профілюєте свій код, знаходите вузькі місця та виправляєте їх по черзі.

Занадто багато людей стрибають прямо на частину "виправити їх по черзі". Вони витрачають купу часу на написання "користувацьких реалізацій колекцій java", тому що вони просто знають, що вся причина у їхній системі повільна - через пропуски кешу. Це може бути фактором, що сприяє, але якщо ви переходите до налаштування такого коду низького рівня, ви, швидше за все, пропустите більшу проблему використання ArrayList, коли вам слід використовувати LinkedList, або що справжня причина вашої системи повільно, тому що ваша ОРМ ледаче завантажує дітей суб'єкта господарювання і, таким чином, робить 400 окремих поїздок до бази даних для кожного запиту.


7

Не буду особливо коментувати код LMAX, тому що я думаю, що це досить чітко визначено, але ось кілька прикладів того, що я зробив, що призвело до значних покращуваних показників.

Як завжди, це методи, які слід застосовувати, як тільки ви дізнаєтесь, що у вас є проблеми та потрібно підвищити продуктивність - інакше ви, ймовірно, просто будете робити передчасну оптимізацію.

  • Скористайтеся правильною структурою даних та створіть користувальницьку, якщо потрібно - виправіть конструкцію структури даних, що сприятиме покращенню, яке ви коли-небудь отримаєте завдяки мікрооптимізаціям, тому зробіть це спочатку. Якщо ваш алгоритм залежить від ефективності на багатьох швидких O (1) зчитуваннях випадкового доступу, переконайтеся, що у вас є структура даних, яка це підтримує! Щоб скористатися цим правилом, варто перестрибнути кілька обручів, наприклад, знайти спосіб, який ви можете представляти свої дані в масиві, щоб дуже швидко використовувати зчитувані з індексом О (1) показники.
  • Процесор швидше, ніж доступ до пам'яті - ви можете зробити досить багато розрахунків за час, необхідний для зчитування однієї випадкової пам'яті, якщо пам'ять не знаходиться в кеш-пам'яті L1 / L2. Зазвичай варто зробити розрахунок, якщо це заощадить вам прочитане пам'ять.
  • Допомога компілятору JIT з остаточним прийняттям фінальних полів, методів та класів дає змогу отримати конкретні оптимізації, які справді допомагають компілятору JIT. Конкретні приклади:

    • Компілятор може припустити, що в кінцевому класі немає підкласів, тому може перетворити виклики віртуальних методів у статичні виклики методів
    • Компілятор може розглядати статичні кінцеві поля як константу для приємного поліпшення продуктивності, особливо якщо константа потім використовується у розрахунках, які можна обчислити під час компіляції.
    • Якщо поле, що містить об’єкт Java, ініціалізується як остаточне, то оптимізатор може усунути як перевірку нуля, так і віртуальний метод відправки. Приємно.
  • Замініть класи колекцій на масиви - це призводить до менш читаного коду і складніше у підтримці, але майже завжди швидше, оскільки видаляє шар непрямості та отримує переваги від безлічі приємних оптимізацій доступу до масиву. Зазвичай хороша ідея у внутрішньому циклі / чутливому коді після того, як ви визначили його як вузьке місце, але уникайте іншого заради читабельності!

  • Використовуйте примітиви, де це можливо - примітиви в принципі швидші, ніж їх об'єктні еквіваленти. Зокрема, бокс додає величезну кількість накладних витрат і може викликати неприємні паузи GC. Не дозволяйте забороняти будь-які примітиви, якщо ви дбаєте про продуктивність / затримку.

  • Мінімізуйте низькорівневе блокування - замки дуже низькі на низькому рівні. Знайдіть способи або повністю уникнути блокування, або заблокувати грубозернистий рівень, так що вам потрібно нечасто фіксувати великі блоки даних, і код низького рівня можна продовжувати, не турбуючись про проблеми з блокуванням або паралельністю.

  • Уникайте виділення пам’яті - це може насправді загальмувати вас загалом, оскільки збирання сміття JVM неймовірно ефективне, але дуже корисно, якщо ви намагаєтеся потрапити на надзвичайно низьку затримку та вам потрібно мінімізувати паузи GC. Існують спеціальні структури даних, які можна використовувати, щоб уникнути розподілу, зокрема бібліотека http://javolution.org/ є чудовою та помітною для них.

Я не згоден з тим, щоб зробити методи остаточними . JIT здатний зрозуміти, що метод ніколи не перекривається. Крім того, якщо підклас завантажується пізніше, він може скасувати оптимізацію. Також зауважте, що "уникати розподілу пам'яті" також може ускладнити роботу GC і, таким чином, уповільнити роботу, тому використовуйте обережно.
maaartinus

@maaartinus: що стосується finalдеяких JIT, це може з'ясувати, а інших - ні. Це залежить від реалізації (як і багато порад щодо налаштування продуктивності). Погодьтеся щодо розподілів - вам потрібно це порівняти. Зазвичай я виявив, що краще усунути виділення, але YMMV.
mikera

4

Крім того, що вже було сказано у чудовій відповіді компанії Aaronaught, я хотів би зазначити, що подібний код може бути досить важким для розробки, розуміння та налагодження. "Хоча це дуже ефективно ... викрутити дуже просто ...", як згадував один із їхніх хлопців у блозі LMAX .

  • Для розробника, який звик до традиційних запитів і блокувань , кодування нового підходу може відчувати себе як їзда на дикій коні. Принаймні, це був мій власний досвід, коли експериментував з Phaser, концепція якого згадується в технічному документі LMAX. У цьому сенсі я б сказав, що цей підхід торгує спорами блокування для мозку розробника .

З огляду на вище, я вважаю, що ті, хто обирає Disruptor та подібні підходи, краще переконуються у наявності ресурсів розвитку, достатніх для підтримки їх рішення.

Загалом, підхід, що порушує ситуацію, виглядає досить перспективно. Навіть якщо ваша компанія не може дозволити собі це використати, наприклад, з вищезгаданих причин, подумайте переконати керівництво "вкласти" певні зусилля для його вивчення (і SEDA взагалі) - тому що якщо вони цього не зробити, то є шанс, що одного дня їх клієнти залишать їх за користь більш конкурентоспроможного рішення, що вимагає 4x, 8x тощо менше серверів.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.