что такое пул в программировании
Как это работает в мире java. Пул потоков
Основной принцип программирования гласит: не изобретать велосипед. Но иногда, чтобы понять, что происходит и как использовать инструмент неправильно, нам нужно это сделать. Сегодня изобретаем паттерн многопоточного выполнения задач.
Представим, что у вас которая вызывает большую загрузку процессора:
Мы хотим как можно быстрее обработать ряд таких задач, попробуем*:
Время выполнения 104 сек.
Как вы заметили, загрузка одного процессора на один java-процесс с одним выполняемым потоком составляет 100%, но общая загрузка процессора в пользовательском пространстве составляет всего 2,5%, и у нас есть много неиспользуемых системных ресурсов.
Давайте попробуем использовать больше, добавив больше рабочих потоков:
ThreadPoolExecutor
Для ускорения мы использовали ThreadPool — в java его роль играет ThreadPoolExecutor, который может быть реализован непосредственно или из одного из методов в классе Utilities. Если мы заглянем внутрь ThreadPoolExecutor, мы можем найти очередь:
в которой задачи собираются, если запущено больше потоков чем размер начального пула. Если запущено меньше потоков начального размера пула, пул попробует стартовать новый поток:
Каждый addWorker запускает новый поток с задачей Runnable, которая опрашивает workQueue на наличие новых задач и выполняет их.
ThreadPoolExecutor имеет очень понятный javadoc, поэтому нет смысла его перефразировать. Вместо этого, давайте попробуем сделать наш собственный:
Теперь давайте выполним ту же задачу, что и выше, с нашим пулом.
Меняем строку в MultithreadClient:
Время выполнения практически одинаковое — 15 секунд.
Размер пула потоков
Попробуем еще больше увеличить количество запущенных потоков в пуле — до 100.
Мы можем видеть, что время выполнения увеличилось до 28 секунд — почему это произошло?
Существует несколько независимых причин, по которым производительность могла упасть, например, из-за постоянных переключений контекста процессора, когда он приостанавливает работу над одной задачей и должен переключаться на другую, переключение включает сохранение состояния и восстановление состояния. Пока процессор занято переключением состояний, оно не делает никакой полезной работы над какой-либо задачей.
Количество переключений контекста процесса можно увидеть, посмотрев на csw параметр при выводе команды top.
На 8 потоках:
На 100 потоках:
Как выбрать размер пула?
Размер зависит от типа выполняемых задач. Разумеется, размер пула потоков редко должен быть захардокожен, скорее он должен быть настраиваемый а оптимальный размер выводится из мониторинга пропускной способности исполняемых задач.
Предполагая, что потоки не блокируют друг друга, нет циклов ожидания I/O, и время обработки задач одинаково, оптимальный пул потоков = Runtime.getRuntime().availableProcessors() + 1.
Если потоки в основном ожидают I/O, то оптимальный размер пула должен быть увеличен на отношение между временем ожидания процесса и временем вычисления. Например. У нас есть процесс, который тратит 50% времени в iowait, тогда размер пула может быть 2 * Runtime.getRuntime().availableProcessors() + 1.
Другие виды пулов
Пул потоков с ограничением по памяти, который блокирует отправку задачи, когда в очереди слишком много задач MemoryAwareThreadPoolExecutor
Пулы потоков
Потоки (thread) в приложении можно разделить на три категории:
Нагружающие процессор (CPU bound).
Блокирующие ввод-вывод (Blocking IO).
Неблокирующие ввод-вывод (Non-blocking IO).
У каждой из этих категорий своя оптимальная конфигурация и применение.
Для задач, требующих процессорного времени, нужен пул с заранее созданными потоками с количеством потоков равным числу процессоров. Единственная работа, которая будет выполняться в этом пуле, — вычисления на процессоре, и поэтому нет смысла превышать их количество, если только у вас не какая-то специфическая задача, способная использовать Hyper-threading (в таком случае вы можете использовать удвоенное количество процессоров). Обратите внимание, что в старом подходе «количество процессоров + 1» речь шла о смешанной нагрузке, когда объединялись CPU-bound и IO-bound задачи. Мы не будем такого делать.
Проблема с фиксированным пулом потоков заключается в том, что любая блокирующая операция ввода-вывода (да и вообще любая блокирующая операция) «съест» поток, а поток — очень ценный ресурс. Получается, что нам нужно любой ценой избегать блокировки CPU-bound пула. Но к сожалению, это не всегда возможно (например, при использовании библиотек с блокирующим вводом-выводом). В этом случае всегда следует переносить блокирующие операции (ввод-вывод и другие) в отдельный пул. Этот отдельный пул должен быть кэшируемым и неограниченным, без предварительно созданных потоков. Честно говоря, такой пул очень опасен. Он не ограничивает вас и позволяет создавать все больше и больше потоков при блокировке других, что очень опасно. Обязательно стоит убедиться, что есть внешние ограничения, то есть существуют высокоуровневые проверки, гарантирующие выполнение в каждый момент времени только фиксированного количества блокирующих операций (это часто делается с помощью неблокирующей ограниченной очереди).
Последняя категория потоков (если у вас не Swing / SWT) — это асинхронный ввод-вывод. Эти потоки в основном просто ожидают и опрашивают ядро на предмет уведомлений асинхронного ввода-вывода, и пересылают эти уведомления в приложение. Для этой задачи лучше использовать небольшое число фиксированных, заранее выделенных потоков. Многие приложения для этого используют всего один поток! У таких потоков должен быть максимальный приоритет, поскольку производительность приложения будет ограничена ими. Однако вы должны быть осторожны и никогда не выполнять какую-либо работу в этом пуле! Никогда, никогда, никогда. При получении уведомления вы должны немедленно переключиться обратно на CPU-пул. Каждая наносекунда, потраченная на поток (потоки) асинхронного ввода-вывода, добавляет задержки в ваше приложение. Поэтому производительность некоторых приложений можно немного улучшить, сделав пул асинхронного ввода-вывода в 2 или 4 потока, а не стандартно 1.
Глобальные пулы потоков
Относитесь осторожно к любому фреймворку или библиотеке, затрудняющему настройку пула потоков или устанавливающему по умолчанию пул, которым вы не можете управлять.
Пул потоков
Дата изменения: 09.10.2017
В сложных многоуровневых приложениях часто заложена реализация сотен потоков. Параллельные вычисления, обработка асинхронного ввода-вывода, мощные графические интерфейсы – эти задачи никогда не реализуются в однопоточных приложениях.
Каждый поток требует времени на выполнение и ресурсов вычислительной машины. Притом, что большую часть своего виртуального жизненного цикла потоки ожидают, когда для них освободится ресурс или – бездействуют, иногда опрашивая элементы управления об их состоянии. Для того, чтобы свести время простоя к минимуму – сделать многопоточные приложения более эффективными в С# существует пул потоков.
Что такое пул потоков
Чтобы поставить метод для выполнения в потоке в очередь потока пула нужно использовать метод QueueUserWorkItem, делегат или метод, указав его в качестве параметра. Делегат имеет сигнатуру void WaitCallback(Object obj), где obj – объект, который содержит данные для использования делегатом. Эта функция добавлена для организации пользовательских callback-функций (обработчиков событий).
Использование пула потоков
Пример использования методов класса ThreadPool:
Результаты компиляции листинга выше зависят от вычислительной мощности машины, на которой будет запущена программа. Так как максимальное количество потоков и количество потоков ввода вывода компилятор задаст сам исходя ресурсов компьютера.
Рекомендации по использованию
Пул потоков никогда не бывает приоритетным, поэтому все находящиеся в нем потоки – фоновые и будут завершены, как только завершатся главные потоки (с высоким приоритетом). Программисту необходимо за этим следить. Как следствие потоки в пуле не имеют приоритета и идентифицирующего имени, они не именные, а носят роль обслуживающих.
Прервать работу потока, который запускается из пула нельзя, как нельзя его идентифицировать.
Пул потоков – это автоматическое средство для задач, которые требуют временных запусков потоков. Если потоки должны быть активны все время существования приложения, лучше воспользоваться функциональностью класса Thread и реализовать взаимодействие между потоками другими средствами.
Общие сведения о пуле данных в Кластеры больших данных SQL Server
В этой статье описывается назначение пулов данных SQL Server в кластере больших данных SQL Server. В следующих разделах содержатся сведения об архитектуре, функциональных возможностях и сценариях использования пула данных.
В этом пятиминутном видео содержатся общие сведения о пулах данных, а также показано, как запрашивать данные из пулов данных.
Архитектура пула данных
Пул данных состоит из одного или нескольких экземпляров пула данных SQL Server, которые предоставляют постоянное хранилище SQL Server для кластера. Это позволяет обрабатывать запросы производительности кэшированных данных к внешним источникам данных и разгружать работу. Данные принимаются в пул данных посредством запросов T-SQL или из заданий Spark. Для повышения производительности в больших наборах данных принимаемые данные распределяются по сегментам базы данных и хранятся во всех экземплярах SQL Server в пуле. Поддерживаются следующие методы распределения: циклический перебор и репликация. Чтобы оптимизировать доступ для чтения, для каждой таблицы в каждом экземпляре пула данных создается кластеризованный индекс columnstore. Пул данных выступает в качестве киоска данных горизонтального масштабирования для кластеров больших данных Кластеры больших данных SQL Server.
Доступом к экземплярам SQL Server в пуле данных управляет главный экземпляр SQL Server. Наряду с внешними таблицами PolyBase для хранения кэша данных создается также внешний источник данных для пула данных. Контроллер в фоновом режиме создает базу данных в пуле данных с таблицами, которые соответствуют внешним таблицам. В главном экземпляре SQL Server реализован прозрачный рабочий процесс: контроллер перенаправляет определенные запросы внешней таблицы к экземпляру SQL Server в пуле данных (для этого может использоваться вычислительный пул), выполняет запросы и возвращает результирующий набор. Данные в пуле данных можно только получать или запрашивать, но их изменение не поддерживается. Таким образом, для любого обновления данных необходимо удалить таблицу, а затем воссоздать ее и повторно внести в нее данные.
Сценарии пула данных
Обычно пулы данных используются для создания отчетов. Например, сложный запрос, в котором объединено несколько источников данных PolyBase, используемый для создания еженедельного отчета, можно разгрузить в пул данных. Кэшированные данные обеспечивают быстрое локальное вычисление и позволяют не возвращаться к исходным наборам данных. Аналогичным образом данные панели мониторинга, требующие периодического обновления, можно кэшировать в пуле данных для оптимизированного создания отчетов. Кэширование наборов данных в пуле данных также полезно для повторного изучения Машинного обучения Azure.
Дальнейшие действия
Дополнительные сведения о Кластеры больших данных SQL Server см. в следующих ресурсах.
Способы взаимодействия сервисов друг с другом. Пулинг/пуш. Достоинства/недостатки. Выбор
Курьер доставил заказ. По смене статуса заказа надо уведомить заинтересованные стороны об этих событиях.
Клиент отправляет сообщение в чат поддержки. Нужно уведомить сервисы поддержки о поступивших данных от клиента.
Построение отчёта завершено. Ожидающий отчёт пользователь может его загрузить. Надо его уведомить об этом.
Знакомые/типовые ситуации. Одному сервису надо уведомить другой (другие) о происшедших событиях.
Давайте немного усложним:
Какие способы уведомления есть?
Активность со стороны сервера
Это, в общем-то, типовое решение. Сервер держит список заинтересованных сторон. По мере появления событий выполняет HTTP-запросы к клиентам.
Подвариант этого решения: Websocket. Сервер отправляет события в сокеты всем подписанным сторонам.
Повторы, обработка ошибок
Рано или поздно любой TCP/HTTP-канал сталкивается с недоступностью другой стороны. Что делать после возникновения ошибки? Повторять запросы? Что делать с вновь поступающими запросами? Ждать, пока успешно выполнятся предыдущие?
Рассмотрим виды ошибок:
Получив неустранимую ошибку, клиент может только записать её в лог. То есть, если полная остановка доставки сообщений не приемлема, то, получив неустранимую ошибку, типовым решением будет считать, что «уведомление доставлено», и переходить к доставке следующих уведомлений. Вероятно, это единственный нормальный путь.
Идя по этому пути, надо постоянно и внимательно следить за мониторами таких ошибок. Анализировать трафик на тему «почему возникла неустранимая ошибка?» и «можно ли жить дальше с этой ошибкой».
Но это не самая большая проблема.
Более интересными являются проблемы:
500-е ошибки
Мы выполняем запрос-передачу данных для сервера X. Происходит 500-я ошибка. Что это?
Возможны два варианта:
Сервис-приёмник данных по какой-то причине именно сейчас не работает (перегружается, переключается БД итп). В этом случае повтор запроса в дальнейшем приведёт нас к успеху.
В сервисе допущена ошибка, приводящая к 500. В этом случае, сколько бы повторов мы ни сделали, до исправления кода в приёмнике ситуация не изменится.
То есть, по повторяемости запросов ошибки у нас делятся на три вида:
Те, которым повтор поможет (сетевые, устранимые 500-ки).
Те, которым повтор не поможет, но выглядят как те, которым поможет (неустранимые 500-ки).
Те, которым повтор не поможет (например 40x-ки).
Разрабатывая политику повторов, помимо указанной проблемы, имеем ещё множество других проблем:
Как часто повторять запросы?
Не будем ли мы «укладывать» внешний сервис, повторяя запросы?
Не будем ли сами «укладываться», если одна из внешних систем по какой-то причине имеет некорректный TCP-стек ( iptables DROP )?
Если посмотреть на систему повторов запросов, то обнаружится, что практически в каждом случае она выбирается индивидуально.
Если сервис, генерирующий событие, и занимается доставкой его до заинтересованных сторон, то имеем
минимальный лаг доставки
минимальная нагрузка на хранилище сообщений;
необходимость повторов в случае неуспеха доставок
необходимость ведения реестра, кому что доставлено и кому что нужно доставить
двусмысленность некоторых ошибок: непонятно, можно (нужно) ли повторять, или нет
система повторов может быть причиной DDoS для клиентских сервисов.
Также есть некоторое количество организационных минусов:
После того, как клиент прекратил де-факто работу (тут два варианта: сервера выключены, сервера не выключены), система продолжает доставлять ему уведомления.
Вебсокет в режиме клиент-сервер
Часть описанных проблем решает постоянное соединение, инициируемое клиентом. Однако именно часть.
Пулинг
Достоинства пулинга
Максимально быстрое восстановление работоспособности после факапов.
Недостатки пулинга
минимальный лаг доставки сообщений равен интервалу пулинга, который обычно выбирается ненулевым
множество сервисов пулящих один создают существенно бОльшую нагрузку, нежели случай с активным сервисом. Сервисы, для которых нет сейчас никаких сообщений, всё равно создают нагрузку на подсистему доставки сообщений.
Ещё один неочевидный, организационный недостаток пулинга: часто способ получения новой порции данных связан со структурой хранения данных.
отсутствие двусмысленности, описанной выше
наиболее быстрое восстановление работоспособности после сбоя
максимальная независимость от сетевого стека TCP на клиенте
нет необходимости хранить/майнтенить список клиентов.
Лаг доставки
Как правило, запросы для пулинга формулируются как «есть ли данные для меня; если есть, то какие?». Такие запросы (в случае, если они некорректно спроектированы) зачастую имеют следующие проблемы:
при перегрузках количество данных в ответе может расти, или время выполнения запроса ухудшаться.
В случае, если получение очередной порции данных сопровождается простой выборкой из BTREE индекса, то и ответ на вопрос «есть ли данные?», как правило, сравнительно бесплатен. Об индексах поговорим ниже.
А сейчас давайте рассмотрим алгоритм работы традиционного пулера.
Обрабатываем полученные данные
Пауза соответствующая интервалу
Если рассматривать этот алгоритм, то видим, что переменная index и есть то, что связывает нас со структурой хранения данных.
Такой алгоритм, как правило, используют новички и. приводят себя к трудноустранимой проблеме: запросы с большими значениями index сделать индексируемыми крайне сложно. Почти невозможно.
Почему разработчик попадает в такую ситуацию? Потому что проектирует БД и API отдельно друг от друга. А нужно посмотреть на все компоненты в целом и на влияние их друг на друга.
Проблема состоит в том, что в БД, как правило, данные хранятся в виде плоских таблиц. Когда мы получаем очередную порцию данных с одними и теми же условиями фильтрации, то приходится делать что-то вроде следующего:
Как сделать план независящим от положения смещения? Использовать вместо смещения выборку из индекса:
Первичная инициализация. index := 0
Выполняем запрос limit данных, передавая в запрос index
Пауза, соответствующая интервалу
В системе с такой архитектурой, как правило, уже нет существенных препятствий к снижению интервала до минимальных значений (вплоть до нуля).
Но давайте ещё порефлексируем над архитектурой. Что плохого в ней?
Алгоритм привязан к структуре данных
Выполняется практически полностью на стороне клиента
Вследствие предыдущей проблемы сложно, например, централизованно модифицировать его на иную работу после факапов/проблем.
Пользователь может сам подставлять в index произвольные значения. Иногда это может быть нежелательно или приводить к багам, которые разработчику сервера сложно понять.
Давайте ещё раз модифицируем алгоритм. Заменим index на state и управлять им будем с сервера:
Выполняем запрос limit данных, передавая в запрос значение state
Что мы получили? Гибкость.
Переменная state определяется только сервером и не обязана быть привязанной к числу смещения. При желании в этой переменной можем хранить JSON со многими полями.
Если в переменной state хранится не только позиция окна, а, например, и значения фильтров и криптоподпись, то эту переменную имеет смысл называть курсором. Переименуем переменную ещё раз и избавимся от постоянных задержек:
Если данные были, перейти к шагу 2
Таким образом, получаем алгоритм, минимизирующий число запросов, если данных для клиента нет, и запрашивающий данные с максимальной производительностью, если таковые имеются.
Рекомендации по работе с курсорами:
Поскольку хранением курсора между запросами озадачен клиент, то имеет смысл хранить в курсоре и версию ПО сервера. В этом случае можно написать дополнительный код, обеспечивающий обратную совместимость (конвертацию форматов курсоров).
Во избежание трудных багов весь набор фильтров, полученных в первом запросе, хорошо хранить в курсоре и в последующих запросах игнорировать параметры фильтрации не из курсора. Перфекционисты могут даже выделить инициализацию курсора в отдельный запрос.
Во избежание введения в соблазн пользователей использовать в своём коде какие-то данные из курсора, лучше не использовать человекочитаемую строку в значении курсора. JSON, пропущенный через base64-кодирование (и криптоподписанный) подходит идеально.
Пример. Изменение алгоритма после сбоя.
Любая система гарантированной доставки сообщений из точки А в точку B в случае факапов будет накапливать пул недоставленных сообщений. После восстановления работоспособности будет период времени, когда приёмник данных сильно отстаёт от источника.
В случае, если порядок доставки сообщений возможно нарушать, то обработчик запроса с курсором может (продетектировав значительное отставание) начать возвращать два потока данных: тот, на который подписан клиент, и тот же, но с более актуальными данными.
Таким образом, пользователи, запросившие отчёт прямо во время факапа, продолжат его ждать (и дождутся). А пользователи, запросившие отчёт после факапа, получат его с небольшой задержкой.
Пример алгоритма серверной стороны, включающего второй поток в случае сильного отставания, приведён на рисунке.
Пофантазировав, схему можно дополнить не одним, а несколькими фолбеками.
Курсорная репликация
Описанные курсоры можно использовать для репликации данных с сервиса на сервис.
Часто один сервис должен иметь у себя кеш/реплику части данных другого сервиса. При этом требований синхронности к этой реплике нет. Поменялись данные в сервисе A. Они должны максимально быстро поменяться и в сервисе B.
Например, мы хотим реплицировать табицу пользователей с сервиса на сервис.
Для такой репликации можно использовать что-то готовое из инструментария баз данных, а можно сделать небольшой «велосипед». Предположим, что пользователи хранятся в БД PostgreSQL. Тогда делаем следующее:
модифицируем изменяющие пользователей запросы, чтобы на каждое изменение записи пользователя значение lsn устанавливалось бы из растущей последовательности
строим по полю lsn (уникальный) BTREE индекс.
В этом случае обновление записи пользователя будет выглядеть примерно так:
А запрос для работы курсора будет выглядеть как-то так:
Итоги
Почти во всех случаях, когда применяется активная система уведомлений зависимых сервисов, её можно заменить описанной курсорной подпиской.
При этом проблемы доступности клиентов, настроек, работоспособности TCP-стека останутся у клиентов
Максимально быстрое и простое восстановление после простоя/сбоев. Отсутствие двусмысленностей в кодах ошибок.