Поиск на сайте: Расширенный поиск


Новые программы oszone.net Читать ленту новостей RSS
CheckBootSpeed - это диагностический пакет на основе скриптов PowerShell, создающий отчет о скорости загрузки Windows 7 ...
Вы когда-нибудь хотели создать установочный диск Windows, который бы автоматически установил систему, не задавая вопросо...
Если после установки Windows XP у вас перестала загружаться Windows Vista или Windows 7, вам необходимо восстановить заг...
Программа подготовки документов и ведения учетных и отчетных данных по командировкам. Используются формы, утвержденные п...
Red Button – это мощная утилита для оптимизации и очистки всех актуальных клиентских версий операционной системы Windows...
OSzone.net Microsoft Разработка приложений Облако/Azure Балансировка нагрузки на закрытые конечные точки в рабочих ролях RSS

Балансировка нагрузки на закрытые конечные точки в рабочих ролях

Текущий рейтинг: 0 (проголосовало 0)
 Посетителей: 815 | Просмотров: 1133 (сегодня 0)  Шрифт: - +

В начале января Дэвид Браун (David Browne) и я работали над решением для балансировки нагрузки на внутренние точки сервиса в рабочих ролях Windows Azure. Как правило, конечные точки сервиса в рабочих ролях публикуются, чтобы средство балансировки нагрузки мог распределять вызовы между экземплярами. Однако заказчику, с которым мы работали, требовалось, чтобы конечные точки не были общедоступными. Кроме того, заказчик не хотел мириться с задержками при выполнении некоторых типов операций в очереди. Как удовлетворить эти требования?

Рассматривая различные варианты, мы с Дэвидом в конечном счете выработали два подхода к решению этой трудной задачи. Сегодня я изложу свои соображения и покажу несколько фрагментов кода, которые использовались для создания прототипа решения в соответствии с одним из двух подходов.

Не желая случайно ограничить производительность конечного решения, мы исключили вариант на основе программного прокси. Вместо этого я выбрал следующее. Программный механизм будет предоставлять допустимый IP-адрес для вызовов сервиса, а вызывающий узел будет кешировать конечную точку в течение заданного времени для уменьшения издержек, связанных с разрешением конечной точки. При этом я рассмотрел три основные стратегии:

  • статическое назначение — присваивание конечной точки сервиса каждому вызывающему узлу;
  • централизованное управление — один узел отслеживает и управляет всеми назначениями для каждого вызывающего узла;
  • кооперативное управление — любой узел может сообщать, доступен ли он для вызовов сервиса.

Каждая из этих стратегий имеет свои плюсы и минусы.

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

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

Централизованное управление

Типичное средство балансировки нагрузки, которое принимает информацию о работоспособности узлов и распределяет запросы к сервису на основе этой информации, использует централизованное управление. Оно собирает информацию об узлах, сведения о назначениях и любые данные о текущей работе, и направляет запросы, отправленные на Virtual IP (VIP), определенному узлу.

В этом сценарии центральная точка делала бы в основном то же самое с тем исключением, что не выступала бы в роли прокси для запроса. Вместо этого вызывающий узел должен обращаться к центральному контроллеру, чтобы получить подходящий адрес для вызова, а контроллер назначает адрес исходя из того, что ему известно (рис. 1). Вызывающий узел кеширует конечную точку и использует ее в течение предопределенного времени, по истечении которого процесс разрешения повторяется заново.

*
Увеличить

Рис. 1. Централизованное управление

Основная логика сосредоточена в центральном контроллере; он должен отслеживать все информацию, необходимую для определения того, какому узлу назначать вызовы. Логика может быть простой, на основе алгоритма карусели, а может быть и сложной, требующей сбора всех данных и анализа информации о работоспособности. Дополнительное усложнение может быть связано с тем, что у разных конечных точек сервиса существуют разные критерии определения доступности, что потребует от центрального контроллера детального знания реализаций сервиса в пуле рабочих узлов.

Что больше всего отпугивает в такой реализации, так это тот факт, что выход из строя центрального контроллера повлечет за собой остановку всей системы. Это означает, что для обеспечения высокой доступности центрального контроллера потребуется реализовать отдельное решение.

В некоторых роботизированных и матричных системах (robotic and matrix systems) рабочие узлы сами выбирают основной контроллер и, если он перестает отвечать, они просто выбирают новый основной контроллер. Хотя это хорошая архитектура, поскольку она сочетает в себе централизованное и кооперативной управление, она также сильно усложняет реализацию механизма распределения нагрузки.

Кооперативное управление

Любой человек с опытом управления, которому приходилось заставлять кого-либо что-то сделать, знает, насколько это непросто — добиться того, чтобы этот некто действительно выполнил свою работу. Гораздо рациональнее прямо спросить человека, есть ли у него время сделать то-то и то-то (конечно, при условии, что он может оценить необходимые для этого усилия). Такой модели я и решил последовать.

Идея в том, что каждый из вызывающих узлов будет начинать с текущей назначенной конечной точкой сервиса и запрашивать, по-прежнему ли она доступна (рис. 2). Если нет, узел продолжит проход по доступному пулу по принципу карусели до тех пор, пока один из узлов не ответит на его запрос положительно (рис. 3). После этого для уменьшения издержек, связанных с процессом разрешения конечной точки, используется тот же механизм устаревания кеша, о котором я упоминал ранее.

*
Увеличить

Рис. 2. Кооперативное управление

*
Увеличить

Рис. 3. Перераспределение нагрузки на другой узел

Положительная сторона такой архитектуры в том, что поддержка высокой доступности (HA) заложена в саму архитектуру. В реализации каждого узла сервиса должна быть логика, определяющая его полное состояние; от этой информации зависит, будет ли он считаться доступным. Таким образом, если узел возвращает отрицательный ответ или ошибку либо истекает время ожидания ответа, вызывающий узел запрашивает следующий доступный узел сервиса и, если он доступен, направляет свои вызовы сервиса этой конечной точке.

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

Прототип

Прототип делает следующее:

  • настраивает стандартный механизм для определения доступности;
  • вызывающий кеширует информацию о доступном узле в течение короткого периода;
  • можно отключать один из узлов на заданное время, чтобы наглядно увидеть, как все вызовы перераспределяются на второй узел;
  • как только узел становится вновь доступным, у вызывающего должна быть возможность возврата к предыдущему узлу.

Некоторые подводные камни. Во-первых, я не делаю ничего для интеллектуального определения доступности, а просто настраиваю механизм балансировки, не беспокоясь о том, насколько «разумным» является принимаемое прототипом решение. Кроме того, я не обрабатываю ошибки и тайм-ауты, но их следует обрабатывать точно так же, как и негативные ответы на запрос о доступности. Наконец, я просто забираю все рабочие роли в развернутой системе, тогда как в настоящей реализации нужно действовать более интеллектуально, чтобы определять все доступные конечные точки сервиса. Для этого можно использовать, например, механизм реестра или просто пытаться обращаться к каждой конечной точке сервиса и в случае успешного вызова помечать данную конечную точку как доступную. Мой код, тем не менее, запрашивает специфическую закрытую конечную точку, и, если она отличается для каждого сервиса, это можно было бы использовать как дифференцирующее звено.

Первым делом нужно получить список IP-адресов от рабочих ролей в развернутой системе. Для этого я должен сконфигурировать роли. В случае рабочих ролей я открываю окно конфигурирования и добавляю внутренние конечные точки сервиса, как показано на рис. 4.

*

Рис. 4. Добавление внутренней конечной точки сервиса к рабочей роли

Я также пометил рабочие роли в развернутой системе как PrivateServices. Используя API объекта RoleEnvironment и эту метку, можно легко получать узлы:

if (_CurrentUriString == null) {
  System.Collections.ObjectModel.ReadOnlyCollection<RoleInstance>
    ServiceInstances = null;
  System.Collections.ObjectModel.ReadOnlyCollection<RoleInstance>
    WebInstances = null;

  ServiceInstances =
    RoleEnvironment.Roles["PrivateServices"].Instances;
  WebInstances =
    RoleEnvironment.Roles["ServiceBalancingWeb"].Instances;

Чтобы найти начальный узел для проверки доступности, я использую порядковый номер узла. Если веб-ролей больше, чем рабочих, то вместо порядкового номера применяется функция вычисления по модулю. После этого я могу выполнять перебор и проверку конечных точек (рис. 5).

Рис. 5. Проверка конечных точек

while (!found && !Abort) {
  string testuri =
    ServiceInstances[idxSvcInstance].InstanceEndpoints[
    "EndPointServices"].IPEndpoint.ToString();
  found = CheckAvailability(testuri);
  if (found) {
    ServiceUriString = testuri;
  }
  else {
    idxSvcInstance++;
    if (idxSvcInstance >= ServiceInstances.Count) {
      idxSvcInstance = 0;
    }
    loopCounter++;
    if (loopCounter == ServiceInstances.Count) {
      Abort = true;
    }
  }
}

Обратите внимание на вызов функции CheckAvailability (рис. 6). В этой функции я создаю привязку, используя None в качестве режима защиты, так как конечная точка является исключительно внутренней. Я создаю экземпляр клиента сервиса, задаю разумный интервал ожидания и возвращаю это значение.

Рис. 6. Функция CheckAvailability

static public bool CheckAvailability(string uri) {
  bool retval = true;
  Binding binding = new NetTcpBinding(SecurityMode.None);
  EndPointServicesRef.EndPointServicesClient endpointsvc =
    new EndPointServicesRef.EndPointServicesClient(binding,
    new EndpointAddress(@"net.tcp://" + uri));
  endpointsvc.InnerChannel.OperationTimeout =
    new System.TimeSpan(0,0,0,0, 5000);

  try {
    retval = endpointsvc.IsAvailable();
  }
  catch (Exception ex) {
    // Todo: handle exception
    retval = false;
  }
  return retval;
}

Если при вызове происходит ошибка, я просто возвращаю false, и цикл переходит к следующему узлу для проверки его доступности. Но заметьте, что для определения номера экземпляра веб-роли, в которой в данный момент выполняется код, я разбираю идентификатор экземпляра. Чтобы это вообще работало, мне пришлось открыть произвольную внутреннюю конечную точку (которая могла бы быть внешней). Если бы я этого не сделал, идентификатор не увеличивался бы и его разбор был бы бесполезным, так как каждый узел казался бы единственным.

Другой способ создания списка узлов — перебор всех узлов с идентификацией порядковой позиции текущего узла, выполняющего код, в списке или простое упорядочение их по последнему октету IP-адресов. Любой из этих двух способов был бы чуть более «защищенным от дурака», но в данном конкретном примере я просто использовал идентификатор экземпляра.

Еще одна закавыка связана с тем, что структура идентификатора в реальной развернутой системе отличается от таковой в инфраструктуре разработки, что вынуждает учитывать это в коде разбора идентификаторов:

string[] IdArray =
  RoleEnvironment.CurrentRoleInstance.Id.Split('.');
int idxWebInstance = 0;
if (!int.TryParse((IdArray[IdArray.Length - 1]),
  out idxWebInstance)) {
  IdArray = RoleEnvironment.CurrentRoleInstance.Id.Split('_');
  idxWebInstance = int.Parse((IdArray[IdArray.Length - 1]));
}

Этот код должен вернуть IP-адрес доступной конечной точки, который можно кешировать в статической переменной. Затем я устанавливаю таймер. Когда срабатывает событие таймера, я обнуляю адрес конечной точки, заставляя код снова искать допустимую конечную точку для использования сервисами:

System.Timers.Timer invalidateTimer =
  new System.Timers.Timer(5000);
invalidateTimer.Elapsed += (sender, e) =>
  _CurrentUriString = null;
invalidateTimer.Start();

Этот код должен вернуть IP-адрес доступной конечной точки, который можно кешировать в статической переменной. Затем я устанавливаю таймер. Когда срабатывает событие таймера, я обнуляю адрес конечной точки, заставляя код снова искать допустимую конечную точку для использования сервисами:

Запуск демонстрации

Теперь я модифицирую исходную страницу и ее отделенный код просто для того, чтобы она показывала узел, с которым у нее установлена связь. Я также добавлю кнопку для отключения узла. Обе части кода весьма тривиальны. Кроме того, я добавлю в UI метку и командную кнопку. В метке я буду выводить идентификатор назначенной конечной точки, а кнопка позволит мне отключать узел и наблюдать, как все веб-роли остаются сопоставленными с единственной конечной точкой до тех пор, пока отключенный узел вновь не будет включен. В обработчик события загрузки страницы (в отделенном коде) я вставлю несколько строк для получения конечной точки (рис. 7).

Рис. 7. Код демонстрационной страницы

protected void Page_Load(object sender, EventArgs e) {
  string UriString = EndpointManager.GetEndPoint();
  LastUri=UriString;

  Binding binding = new NetTcpBinding(SecurityMode.None);

  EndPointServicesRef.EndPointServicesClient endpointsvc =
    new EndPointServicesRef.EndPointServicesClient(binding,
    new EndpointAddress(@"net.tcp://" + UriString));
  lblMessage.Text = "WebInstacne ID: " +
    RoleEnvironment.CurrentRoleInstance.Id.ToString() +
    " is Calling Service @ " + UriString + " & IsAvailable = " +
    endpointsvc.IsAvailable().ToString();
  cmdDisable.Enabled=true;
}

Поскольку я лишь хочу продемонстрировать кооперативную балансировку, я не стал реализовать другой метод или интерфейс сервиса, а просто повторно использовал метод IsAvailable.

Прототип приложения в действии показан на рис. 8. Сначала вы видите идентификатор (в данном случае в инфраструктуре разработки), IP-адрес и доступность узла. Обновление страницы приводит к выдаче запроса на балансировку. При щелчке кнопки отключения запускается небольшой кусок кода, настраивающий вызов DisableNode для текущей конечной точки:

protected void cmdDisable_Click(object sender, EventArgs e) {
  Binding binding = new NetTcpBinding(SecurityMode.None);
  EndPointServicesRef.EndPointServicesClient endpointsvc =
    new EndPointServicesRef.EndPointServicesClient(binding,
    new EndpointAddress(@"net.tcp://" + LastUri));
  endpointsvc.DisableNode();
}

*

Рис. 8. Демонстрационное приложение в действии

Метод DisableNode просто присваивает булевы значения паре переменных, а затем настраивает таймер так, чтобы при его срабатывании вызывался метод EnableNode. Значение таймера немного превышает срок устаревания кешированной конечной точки, чтобы это было проще продемонстрировать в тестовом прогоне:

public void DisableNode() {
  AvailabilityState.Enabled = false;
  AvailabilityState.Available = false;

  System.Timers.Timer invalidateTimer =
    new System.Timers.Timer(20000);
  invalidateTimer.Elapsed += (sender, e) => EnableNode();
  invalidateTimer.Start();
}

После отключения узла последующие запросы, исходящие от разных веб-серверов, должны направляться конечной точке одного и того же рабочего узла.

За рамками примера

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

В этом примере я намеренно сделал так, чтобы вызывающий узел выполнял код разрешения конечной точки в процессе запуска роли. Он кеширует конечную точку в статическом члене или в реальном кеше, который обновляется на основе интервала устаревания. Однако это можно было бы включить в реализацию сервиса, обеспечив более тонкий контроль, но лишившись манипуляций на уровне комбинации IP-адреса и порта. В зависимости от конкретной задачи и проекта инфраструктуры сервиса я мог бы предпочесть один стиль другому.

Чтобы все это работало в производственной среде, нужно учитывать некоторые вещи, перечисленные ниже.

  • Интеллектуальная логика определения доступности. Вы должны не только анализировать такие элементы, как процессор, диск, состояние серверных соединений и др., но и использовать пороговые значения, по достижении которых переключается битовый флаг «доступен/недоступен».
  • Логика обработки ситуации, когда все узлы сообщают о своей недоступности.
  • Принятие решений о длительности кеширования конечной точки.
  • Дополнительные методы в EndpointManager для изменения настроек, удаления узлов из пула и общего обслуживания в процессе работе.
  • Вся обработка типичных исключений и диагностика обычно включается в реализацию сервиса.

Я понимаю, что вы, возможно, считаете эти вещи сами собой разумеющимися, но предпочитаю придерживаться правила «никаких догадок».

А теперь пара слов о подходе Дэвида. Он формирует матрицу между доменами Fault и Upgrade в попытке обеспечить соответствие доступности вызывающего и конечной точки, отдавая предпочтение конечным точкам в тех же доменах. Я считаю это отличной идеей. Комбинация наших реализаций могла бы гарантировать, что ваша веб-роль по возможности обрабатывается рабочей ролью в соответствии с тем же соглашением об уровне обслуживания (SLA), что и сервис, но в случае недоступности выделенных рабочих ролей можно было бы перераспределять нагрузку на любой другой узел.

Заключение

Надеюсь, что со временем платформа Windows Azure будет сама поддерживать балансировку нагрузки для закрытых конечных точек. Ну а пока, если у вас есть необходимость в такой балансировке (низкоуровневые сокеты почти всегда требуют определенного уровня защиты, так как являются внутренними), программное решение, вероятно, будет самым простым способом решения этой задачи. Отделяя вызовы разрешения конечной точки от вызовов собственно сервиса и делая их частью процесса запуска, вы сможете сохранить ясность производственного кода и изолировать его от инфраструктурного кода. Благодаря этому, как только появится аппаратная функция балансировки нагрузки для закрытых конечных точек, ваши сервисы продолжат свою работу, а вы сможете просто отключить заложенный в них код балансировки.

Автор: Джозеф Фулц  •  Иcточник: MSDN Magazine  •  Опубликована: 02.09.2011
Нашли ошибку в тексте? Сообщите о ней автору: выделите мышкой и нажмите CTRL + ENTER


Оценить статью:
Вверх
Комментарии посетителей
Комментарии отключены. С вопросами по статьям обращайтесь в форум.