Файл: Инструменты повышения производительности программного обеспечения. Требования к производительности.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
Базы данных используют кеширование двояко:
-
Низкоуровневый кеш блоков данных -
Кеш запросов (где ключом является “SELECT FistName,LastName from Users Where ID=123“, а значением - прочитанный набор записей)
Кеш запросов хорошо помогает при READ-ONLY доступе к небольшому объему данных(чтобы полностью уместился в кеше). Следует понимать, это не замена полноценному кешированию на уровне приложения.
Дело в том, что СУБД “не знает” бизнес-логики приложения и оперирует только текстами SQL запросов. Это создает проблемы c дубликацией/запихиванием слишком большого объема данных в кеш/излишне агрессивном сбросе кеша.
Например, база данных получила SQL два запроса подряд:
-
SELECT * from user_profile where ID=123; -
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/инициализация нового потока на стороне СУБД). Пул заранее открытых соединений к серверу СУБД решает эту проблему. С другой стороны, больше открытых соединений - больший расход ресурсов.
-
Когда применять: Небольшие объемы данных, частые запросы к ним. -
Когда НЕ применять: Большие объемы данных(время на установление соединения пренебрежимо мало по сравнению с общей продолжительностью пересылки данных), редкие запросы (т.е., открытое соединение будет простаивать, разбазаривая ресурсы )
Потенциальные проблемы:
Намного увеличивается сложность. Без пула соединений, цикл прост:
-
Приложение получило задачу на выполнение -
Открыло соединение (новое, “чистое”) -
Послало запрос -
Получило ответ -
Закрыло соединение (если в приложении до этого произошел сбой, СУБД сама закроет, когда сработает 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.