Файл: Инструменты повышения производительности программного обеспечения. Требования к производительности.docx

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 03.02.2024

Просмотров: 34

Скачиваний: 1

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.


Решение принимаем по каждому виду данных/бизнес сущности отдельно. Ищем баланс с учетом размера данных, стоимости их вычисления/чтения с HDD, вероятности их повторной востребованности, размера, который они занимают в кеше/частоты изменения данных/того, насколько “болезненно” будет для пользователя получение “устаревших” данных из кеша.

Как выбираем размер кеша

Отмерим для кеша слишком мало памяти - нам не хватит места для данных. Отмерим слишком много - будем тратить больше времени на поиск в нем. Ищем баланс, используем статистику использования кеша.

Как очищаем кеш

Предположим, прочитали мы из медленного HDD/СУБД, что Иванов - начальник транспортного цеха. И закешировали в RAM на 15 минут. А через минуту пришел сотрудник HR и перевел Иванова на должность начальника цеха готовой продукции.

Пока все тривиально, наше приложение должно:

  • Обновить данные об Иванове в СУБД

  • Удалить устаревшие данные  об Иванове из кеша

Что если у вас есть внешняя система, которая тоже может менять данные?

Например, данные департамента HR импортируются из другого филиала?

Кеш должен позволять очистить данные о любой выбранной бизнес-сущности/все данные:

  • Посредством API (которое “будет вызвано” процессом импорта данных)

  • Посредством commandline tool или пользовательского интерфейса, доступного только администратору - для упрощения сопровождения системы

Распределенные системы с Мастер-Мастер архитектурой добавляют новую сложность. Представьте, что у вас два серверных центра, в Москве и в Питере, и оба одновременно читают/изменяют одни и те же данные(master-master database replication). Необходимо реплицировать между центрами не только измененные данные, но и извещения наподобие “уважаемый Другой Дата Центр, сбросьте Ваш кеш о пользователе 123, его данные поменялись”. В качестве транспорта для “сбрось кеш” извещений, вероятно, следует использовать тот же канал, что и для репликации самих данных.

Как определяем права доступа на кешированные данные

Предположим, наши клиенты расположены далеко от дата центра. Network latency варьирует от 0.1 ms (миллисекунды) в LAN до 100 ms в интернет. Есть смысл установить кеширующий сервер, поближе к ним, и кешировать данные в нем. Что если данные, к которым есть доступ у клиента X, не должны быть доступны клиенту Y? Типичные решения:


  • Кеширующий сервер делает запрос “разрешено ли Y смотреть данные с ID 17?” в центр данных (что, к сожалению, добавляет Network latency)

  • Клиент Y предварительно получает в центре данных тикет, в котором, в зашифрованном виде, содержится описание его прав доступа(и у кеширующего сервера есть ключ дешифрования)

Как проверяем “свежесть” данных

Продолжая пример с network latency, как кеширующий сервер проверит, доступны  ли,обновились ли данные в дата центре?

Прямолинейный способ - загрузить их заново, более эффективный - загрузить их только если контрольная сумма данных изменилась. Пример из спецификации HTTP - http://en.wikipedia.org/wiki/HTTP_ETag.

Целостность данных в кеше

Дедупликация - мы хотим, чтобы разные модули системы совместно использовали кешированные общие данные, вместо того чтобы дублировать их.

Зависимости между кешированным данными - когда мы удаляем из кеша устаревшие данные, нужно удалить и те, которые от них зависят.

Говоря об имплементации, потребуется

  • спецификация для имен ключей в кеше

  • спецификация формата данных в кеше

  • спецификация зависимостей (“когда чистите в кеше Пользователя, вычистите и его Профиль”)

Желательно иметь библиотеку, которая одновременно и  документирует спецификации в коде и вынуждает, в хорошем смысле, программистов выполнять их.

Статистика использования кеша

Количество попаданий/промахов, для каждого класса данных

Предварительный прогрев кеша

Может потребоваться при большой зависимости производительности приложения от кеширования/большом объеме данных. Чтобы избежать резкого “проседания” производительности после полного сброса кеша/перезапуска приложения.  

Persistent cache(сохраняет данные даже после рестарта)

Может потребоваться, если стоимость вычисления данных/извлечения из HDD/СУБД высока

Специфика кеширования запросов в СУБД/ORM

Базы данных используют кеширование двояко:

  1. Низкоуровневый кеш блоков данных

  2. Кеш запросов (где ключом является “SELECT FistName,LastName from Users Where ID=123“, а значением - прочитанный набор записей)


Кеш запросов хорошо помогает при READ-ONLY доступе к небольшому объему данных(чтобы полностью уместился в кеше). Следует понимать, это не замена полноценному кешированию на уровне приложения.

Дело в том, что СУБД “не знает” бизнес-логики приложения и оперирует только текстами SQL запросов. Это создает проблемы c дубликацией/запихиванием слишком большого объема данных в кеш/излишне агрессивном сбросе кеша.

Например, база данных получила SQL два запроса подряд:

  1. SELECT * from user_profile where ID=123;

  2. UPDATE user_profile SET compensation_coeffcient=1.5 WHERE employee_grade=4;


Устарели ли результаты “SELECT …”? С точки зрения приложения - нет, потому что поле compensation_coeffcient им не используется, либо пользователь с ID==123 не имеет “employee_grade==4”. Но СУБД либо не может этого знать либо не может эффективно отследить.

Поэтому явный контроль над кешированием на уровне приложения дает наибольший выигрыш в производительности.

Предварительные вычисления (Precompute)

Когда стоимость вычисления данных велика, вероятность повторного обращения к тем же данным значительна и частота изменений данных мала - имеет смысл предварительно вычислить их и сохранить в persistent storage.

Пример: 

Проблема - Чтение из СУБД неких редко изменяемых,но часто требуемых данных выполняется медленно из-за сложных SQL запросов со множеством JOIN’ов. Изменить структуру СУБД мы не хотим(другие бизнес-процессы требуют именно такой структуры).

Решение - Выполнять чтение и преобразование данных фоновым процессом, который сохраняет уже обработанные данные в “кеширующей таблице” в СУБД.

Пакетные операции

Объединение задач в “пакет” (batch) позволяет экономить на накладных расходах.

Типичная ошибка - не использовать SQL Batch при вставке большого количества записей в СУБД. Вставка данных требует от СУБД обработать входные данные/проанализировать SQL запрос/записать транзакцию в WAL(http://en.wikipedia.org/wiki/Write-ahead_logging)/перестроить индекс таблицы.

Вставка десяти записей означает - вы платите десять раз. При использовании Batch insert (пример для MySQL http://dev.mysql.com/doc/refman/5.0/en/insert.html) - платите накладные расходы один раз.

Предварительная инициализация

Пример: Пул соединений

Типичное серверное приложение извлекает, по запросу пользователя, данные из СУБД(Сервера поиска/внешней системы/и т.д.)

Установка нового соединения съедает время(network latency/инициализация нового потока на стороне СУБД). Пул заранее открытых соединений к серверу СУБД решает эту проблему. С другой стороны, больше открытых соединений - больший расход ресурсов.


  • Когда применять: Небольшие объемы данных, частые запросы к ним.

  • Когда НЕ применять: Большие объемы данных(время на установление соединения пренебрежимо мало по сравнению с общей продолжительностью пересылки данных),  редкие запросы (т.е., открытое соединение будет простаивать, разбазаривая ресурсы )

Потенциальные проблемы:

Намного увеличивается сложность. Без пула соединений, цикл прост:

  1. Приложение получило задачу на выполнение

  2. Открыло соединение (новое, “чистое”)

  3. Послало запрос

  4. Получило ответ

  5. Закрыло соединение (если в приложении до этого произошел сбой, СУБД сама закроет, когда сработает timeout )

Типовые проблемы при  использовании пула соединений:

  • Шаг №2 может надолго заблокировать поток. Например, приложение содержит ошибку и  не всегда возвращает соединение в пул. В результате все незанятые соединения в пуле оказались исчерпаны. И  реализация метода “openConnection” в пуле ждет, когда освободится одно из занятых сейчас соединений, пока не сработает таймаут(или вечно...)

  • Полученное на шаге №2 соединение уже использовалось ранее и могло “унаследовать” от предыдущего потока целую россыпь проблем (НЕзакрытая транзакция/НЕснятые блокировки/мусорные настройки Transaction Isoloation Level|Charset conversion/мусор в server side SQL variables).

  • Соединение может быть уже закрыто сервером СУБД из-за ошибок, либо длительной неактивности.

  • Полученный на шаге №4 ответ может быть вовсе не ответом на посланный в №3 запрос. Это может быть ответ на запрос, посланный предыдущим “пользователем” соединения.

Поэтому использование persistent соединений требуют поддержки на уровне реализации пула/протокола/драйвера БД/СУБД.

  • Сброс состояния соединение при возвращении в пул - СУБД должно предоставлять такую возможность и драйвер в приложении должен ею пользоваться.

  • Идентификация на уровне драйвера/протокола - к какому посланному ранее запросу относится полученный драйвером от СУБД ответ.

  • Отслеживание и четкое разграничение драйвером ошибок на сетевом уровне(timeout/connection closed) от ошибок уровня приложения (SQL query has error). Первые делают невозможным использование соединения/требуют выбросить его из пула. Вторые позволяют продолжить его использование.

  • Необходим лимит на количество в пуле (не больше, чем сервер приложений может обработать параллельно).

  • Лимит на начальное количество соединений в пуле.

  • Настройка - Сколько добавлять в пул за раз, когда не хватает.

  • Timeout на извлечение из пула. Добавление нового соединения может занять от доли секунды до часов(перегруженный сервер СУБД/сбой или неправильная настройка в файерволе или load balancer’е). Последнее, чего мы хотим - толпа потоков в сервере приложений, заблокированных навечно, потому что в пуле закончились соединения и он заблокировался при добавлении нового.

  • Timeout на соединение с СУБД  - см выше.

  • Timeout на получение данных из СУБД - см выше.

  • Heartbeat  - пул должен регулярно делать “ping” на каждом соединении, чтобы СУБД не закрыло его за неактивность .

  • Перезапуск пула без перезапуска приложения(см выше список проблем из-за которых перезапуск может понадобится). Перезапуск крупного server side приложения может потребовать несколько минут прежде чем работоспособность восстановиться полностью ( “прогрев” кешей/инициализация/установка network соединений).

  • Реконфигурация пула без перезапуска приложения(см выше).


Параллелизм

Когда выгодно (НЕ)использовать параллелизм

Параллельное выполнение задач ускоряет процесс - если мы не пытаемся выполнить параллельно больше работы, чем можем.

Например, у нас есть сервер - калькулятор. Пользователи посылают ему арифметические выражения, например “2+2” и сервер вычисляет результат - в нашем примере “4”. Аппаратное обеспечение - 1 (один) CPU, с одним ядром. Сколько пользователей сервер может обслужить параллельно?Ответ - только одного! Потому что используемый ресурс - только CPU и RAM. При этом единственное ядро CPU является “бутылочным горлышком”.  

Представьте, что для выполнения одного запроса серверу требуется одна секунда времени CPU. (Это конечно невероятно много для того, чтобы сложить “2 и 2”, но  в нашем учебном примере так нагляднее).

Представьте, что к серверу обратились 60 пользователей одновременно.

Сколько времени нужно, чтобы их обслужить используя один поток? Одна секунда * 60 пользователей = одна минута.  Одного пользователя сервер обслужит за 1 секунду, второму придется подождать 2, самому невезучему - 60. Хотя бы 30 пользователей будут обслужены быстрее чем за 30 секунд.  

Если мы распараллелим  работу внутри сервера между 60 потоками, каждый обслуживает один запрос, все запроcы обрабатываются строго параллельно - сколько времени пользователи будут ждать ответа? 60 секунд. Причем, ВСЕ ПОЛЬЗОВАТЕЛИ будут ждать по 60 секунд. Многопоточность/параллелизм только ухудшают производительность, когда мы пытаемся запихнуть в систему больше работы, чем она способна выполнить.

 

Другой пример - WEB сервер, раздающий HTML/JS/CSS файлы. Аппаратное обеспечение - 4 ядра CPU.  Сколько пользователей сервер может обслужить параллельно? Намного больше 4-х, потому используемые ресурсы - не столько CPU/RAM, сколько Disk I/O. Дисковой ввод-вывод намного медленнее CPU и поэтому является “бутылочным горлышком”. Серверу следует создать намного больше 4-x потоков - они все равно будут ждать, когда завершится чтение с диска. Если потоков будет слишком мало -тогда уже ИХ КОЛИЧЕСТВО станет “бутылочным горлышком”.

Вывод

Ключевая метафора для понимания производительности - это “конвейер задач”. Которые прокачиваются через “трубу”. Количество и последовательность задач в конвейере определяются требованиями заказчика и техническими решениями программиста.

Пропускная способность трубы определяется ее шириной (Bandwitch) и длиной(Latency). Которые в свою очередь определены лежащим в фундаменте системы Hardware/Software.