Масштабируемость
Термин масштабируемость часто используется неправильно. Для этого раздела предоставляется двойное определение:
- Масштабируемость — это возможность полностью использовать доступные вычислительные мощности в многопроцессорной системе (2, 4, 8, 32 или более процессоров).
- Масштабируемость — это возможность обслуживать большое количество клиентов.
Эти два связанных определения обычно называют увеличением масштаба. В конце этой статьи приведены советы по масштабированию.
В этом обсуждении основное внимание уделяется исключительно написанию масштабируемых серверов, а не масштабируемых клиентов, так как масштабируемые серверы являются более распространенными требованиями. В этом разделе также рассматривается масштабируемость только в контексте серверов RPC и RPC. Здесь не рассматриваются рекомендации по обеспечению масштабируемости, такие как сокращение конфликтов, предотвращение частых промахов кэша в расположениях глобальной памяти или ложный общий доступ.
Когда сервер получает вызов RPC, подпрограмма сервера (подпрограмма диспетчера) вызывается в потоке, предоставленном RPC. RPC использует пул адаптивных потоков, который увеличивается и уменьшается по мере изменения рабочей нагрузки. Начиная с Windows 2000 ядром пула потоков RPC является порт завершения. Порт завершения и его использование RPC настраиваются для процедур сервера с от нуля до низкого уровня состязания. Это означает, что пул потоков RPC агрессивно увеличивает количество обслуживаемых потоков, если некоторые из этих потоков блокируются. Он работает с предположением, что блокировка является редкой, и если поток блокируется, это временное условие, которое быстро устраняется. Такой подход обеспечивает эффективность серверов с низким уровнем состязания. Например, сервер RPC с пустым вызовом, работающий на сервере с частотой 550 МГц с восемью процессорами, который обращается через высокоскоростную сеть системной зоны (SAN), обслуживает более 30 000 пустых вызовов в секунду от более чем 200 удаленных клиентов. Это более 108 миллионов вызовов в час.
В результате агрессивный пул потоков фактически мешает, когда состязания на сервере высоки. Чтобы проиллюстрировать, представьте себе сервер с высокой нагрузкой, используемый для удаленного доступа к файлам. Предположим, что сервер использует самый простой подход: он просто синхронно считывает или записывает файл в потоке, в котором этот RPC вызывает подпрограмму сервера. Кроме того, предположим, что у нас есть сервер с четырьмя процессорами, обслуживающий множество клиентов.
Сервер будет начинаться с пяти потоков (на самом деле это зависит, но для простоты используется пять потоков). После того как RPC получает первый вызов RPC, он отправляет вызов в подпрограмму сервера, и подпрограмма сервера выдает операции ввода-вывода. Редко он пропускает кэш файлов, а затем блокирует ожидание результата. Как только он блокируется, пятый поток освобождается для получения запроса, а шестой поток создается в качестве горячего резерва. При условии, что каждая десятая операция ввода-вывода пропускает кэш и блокируется в течение 100 миллисекунд (произвольное значение времени), а сервер с четырьмя процессорами обслуживает около 20 000 вызовов в секунду (5000 вызовов на процессор), упрощенное моделирование прогнозирует, что каждый процессор будет порождать около 50 потоков. При этом предполагается, что вызов, который будет блокировать, происходит каждые 2 миллисекунда, а через 100 миллисекунд первый поток освобождается снова, поэтому пул стабилизируется на уровне около 200 потоков (50 на процессор).
Фактическое поведение сложнее, так как большое количество потоков приведет к дополнительным переключениям контекста, которые замедляют работу сервера, а также замедляют скорость создания новых потоков, но основная идея ясна. Количество потоков быстро повышается, когда потоки на сервере начинают блокировать и ожидать чего-то (будь то операции ввода-вывода или доступ к ресурсу).
RPC и порт завершения, который отправляет входящие запросы, будут пытаться сохранить количество используемых потоков RPC на сервере, равное количеству процессоров на компьютере. Это означает, что на сервере с четырьмя процессорами после возвращения потока в RPC, если имеется четыре или более пригодных для использования потоков RPC, пятый поток не может получить новый запрос и вместо этого будет находиться в состоянии горячего резервирования в случае, если один из используемых в данный момент потоков блокируется. Если пятый поток ожидает достаточно долго в качестве горячего резерва, а количество пригодных для использования потоков RPC будет меньше числа процессоров, он будет освобожден, то есть пул потоков уменьшится.
Представьте себе сервер с большим количеством потоков. Как упоминалось ранее, RPC-сервер заканчивается большим количеством потоков, но только в том случае, если потоки блокируются часто. На сервере, где часто блокируются потоки, поток, возвращающийся в RPC, вскоре удаляется из списка горячего резервирования, так как все используемые в настоящее время потоки блокируются и получает запрос на обработку. Когда поток блокируется, диспетчер потоков в ядре переключает контекст на другой поток. Этот переключатель контекста сам по себе использует циклы ЦП. Следующий поток будет выполнять другой код, получать доступ к разным структурам данных и будет иметь другой стек. Это означает, что частота попаданий в кэш памяти (кэши L1 и L2) будет значительно ниже, что приведет к замедлению выполнения. Одновременное выполнение многочисленных потоков увеличивает состязание за существующие ресурсы, такие как куча, критические разделы в коде сервера и т. д. Это еще больше увеличивает состязание по мере формирования конвоев на ресурсах. Если память низкая, нехватка памяти, оказываемая большим и растущим числом потоков, приведет к сбоям страницы, что еще больше увеличит скорость, с которой потоки блокируются, и приведет к созданию еще большего количества потоков. В зависимости от того, как часто он блокируется и сколько доступной физической памяти, сервер может либо стабилизироваться при некотором более низком уровне производительности с высокой скоростью переключения контекста, либо может ухудшиться до такой степени, что он только многократно обращается к жесткому диску и переключению контекста без выполнения каких-либо фактических работ. Эта ситуация, конечно, не будет отображаться при небольшой рабочей нагрузке, но тяжелая рабочая нагрузка быстро выводит проблему на поверхность.
Как это можно предотвратить? Если предполагается, что потоки блокируются, объявите вызовы как асинхронные, а после того как запрос попадает в подпрограмму сервера, поставить его в очередь в пул рабочих потоков, использующих асинхронные возможности системы ввода-вывода и (или) RPC. Если сервер, в свою очередь, выполняет вызовы RPC, сделайте их асинхронными и убедитесь, что очередь не становится слишком большой. Если подпрограмма сервера выполняет файловый ввод-вывод, используйте асинхронный файловый ввод-вывод, чтобы ставить в очередь несколько запросов к системе ввода-вывода и иметь только несколько потоков в очередь и получить результаты. Если серверная подпрограмма выполняет сетевые операции ввода-вывода, используйте асинхронные возможности системы для асинхронной выдачи запросов и получения ответов и использования как можно меньшего количества потоков. После завершения ввода-вывода или вызова RPC, выполненного сервером, завершите асинхронный вызов RPC, доставляющий запрос. Это позволит серверу работать с максимально возможным количеством потоков, что повышает производительность и количество клиентов, которые сервер может обслуживать.
RPC можно настроить для работы с балансировкой сетевой нагрузки ( NLB), если балансировка сетевой нагрузки настроена таким образом, чтобы все запросы с заданного адреса клиента отправлялись на один и тот же сервер. Так как каждый клиент RPC открывает пул подключений (дополнительные сведения см. в разделе RPC и сеть), важно, чтобы все подключения из пула данного клиента помещлись на одном серверном компьютере. При соблюдении этого условия кластер NLB можно настроить для работы в качестве одного большого RPC-сервера с потенциально отличной масштабируемостью.