18 декабря 2016 г.

Распараллеливание синхронных вызовов в Oracle SOA Suite 11g

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

Если рассматривать ситуацию требующую выполнение большого числа последовательных синхронных вызовов, необходимо добиться возможности распараллеливания этих вызовов, что к сожалению не всегда возможно, например из-за ограничений платформы.
Рассмотрим случай, когда на платформе Oracle SOA Suite 11g (OSS) веб-сервис принимает какой-либо бизнес-объект, содержащий большое количество записей, которые требуется переслать дальше в подсистему.  Такой сценарий обычно реализуется в одном потоке BPEL-процесса, с циклом содержащим вызовы сохранения полученных записей.

При асинхронной реализации веб-сервиса сохранения данных в подсистеме, мы можем использовать стандартные механизмы распараллеливания вызовов OOS. В BPEL 2.0 это компонент forEach, а для BPEL 1.1 связка компонентов FlowN + While, в которые оборачиваются асинхронные вызовы. Использование этих компонентов OSS позволяет без труда распараллелить множественные асинхронные вызовы.

Вышеописанный подход предоставляет псевдо-параллелизм, в реальности, потоки FlowN выполняются в одном потоке Java-машины, а ускорение обеспечивается, за счет наделения асинхронных вызовов неблокирующим поведением. Такое поведение подразумевает, что после выполнения запроса, не происходит блокирование потока до получения ответа, а продолжаются выполнения вызовов и прием ответов для ранее сделанных вызовов.

К сожалению, такой подход не работает при работе с синхронными вызовами, FlowN не позволяет наделить неблокирующим поведением синхронные вызовы. Независимо от потока FlowN, каждый последующий вызов не начнется, пока предыдущий синхронный вызов не получит ответ. (п.10.1.1 Developer's Guide)

Далее будет предложен простой подход, позволяющий распараллелить синхронные вызовы стандартными компонентами OSS.

Подходы распараллеливания удобно рассматривать в сравнении с последовательной версией процесса, поэтому сперва реализуем последовательную версию веб-сервиса, кратко опишем данный процесс в п.1-2.

Содержание:

1. Создание целевого синхронного веб-сервиса.

Стандартными средствами OSS, создадим композит с BPEL-процессом, который реализует простой синхронный SOAP веб-сервис. Единственной задачей веб-сервиса будет имитация обработки, для чего установим задержку в 3 секунды* с помощью стандартного компонента Wait, как это показано на рисунке 1.
Рисунок 1 - Синхронный веб-сервис, имитирующий задержку

*при стандартных настройках, OOS игнорирует задержки <=2 сек. Для изменения данного поведения требуется изменить параметр MinBPELWait в System MBean Browser, через Enterprise Manager.

2. Последовательная версия процесса.

Последовательную версию процесса реализуем в отдельном композите. В BPEL-процессе, создаем цикл, в котором происходит обращение к веб-сервису, реализованному на предыдущем шаге.
Время работы последовательного процесса будет пропорционально расти количеству итераций цикла, так как вызовы происходят последовательно.
Рисунок 2 - Последовательная версия процесса

3. Параллельная версия процесса.

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

Для использования при  распараллеливании стандартных компонентов платформы (такие как FlowN), нам необходимо работать с целевым синхронным веб-сервисом как с асинхронным. Для этого можно применить структурный шаблон проектирования - Адаптер (Adapter/Wrapper), который часто используется в императивном программировании для приведения различных интерфейсов. 

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

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

Рисунок 3 - Обращение к BPEL-процессу как к внешнему веб-сервису

На рисунке 4 изображен прототип, где мы видим проброс входящего сообщения для выполнения дальнейшего вызова, и возврат результата синхронного вызова через ответное сообщение асинхронной обертки.
Рисунок 4 - Асинхронная обертка

Одной из особенностей данного подхода, является необходимость реализации механизма обработки SOAP Faults, так как асинхронный веб-сервис по своей природе не подразумевает такую возможность.
В данной статье будет рассмотрен, простой подход асинхронного возврата ошибок, в ответное сообщение асинхронной обертки добавим поле, для записи текстового представления ошибки, получаемого функцией ora:getFaultAsString(). 
…
<assign name="AssignFault" xmlns="http://schemas.xmlsoap.org/ws/2003/03/business-process/">
    <copy>
 <from expression="ora:getFaultAsString()"/>
     <to variable="outputVariable" part="payload"
                         query="/client:processResponse/client:error"/>
    </copy>
</assign>
…
О другом подходе обработки ошибок, при асинхронных вызовах, можно почитать здесь.

Логику обработки ошибок и возврат сообщения необходимо реализовать в блоке CatchAll, тогда инстанс процесса будет завершаться корректно в статусе terminated, а не зависать в статусе running.

Перейдем к распараллеливанию цикла в котором происходят вызовы.

Теперь обращение к целевому синхронному веб-сервису буде происходить через асинхронную обертку. 

Поместим цикл в компонент FlowN и Scope, теперь цикл будет выполнятся в нескольких потоков в отдельных скопах. Для работы с различными данными в потоках, условием продолжения $index <= $count, где:
index - счетчик (инкрементируемая переменная) цикла, начальным значением которого присваиваем номер потока;
count - общее количество записей, т.е. необходимое количество синхронных вызовов.

Счетчик внутри потока инкрементируем путем прибавления к нему общего количества потоков.

Внутри цикла, заменяем синхронный вызов на асинхронные операции invoke и receive.

Очень важно, для BPEL-процесса адаптера, добавить параметр nonBlockingInvoke = true. Что собственно и добавляет неблокирующее поведение для асинхронных вызовов. (FMW Performance and Tuning Guide)
Рисунок 4 - Добавление параметра

Обратите внимание, переменные счетчика и асинхронных вызовов должны быть определенны внутри скопа FlowN, иначе все потоки будут модифицировать одни и те же переменные.

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

Для предотвращения конфликта приема сообщений в параллельных потоках, необходимо для операций вызова и приема определить converstaion id, иначе возникнет исключение:
<bpelFault>
    <faultType>0</faultType>
    <conflictingReceive xmlns="http://schemas.xmlsoap.org/ws/2003/03/business-process/">
     <part name="summary">
  <summary>Conflicting receive. A similar receive activity is being declared in the same process. Another receive activity or equivalent (currently, onMessage branch in a pick activity) has already been enabled with the partnerLink "AsyncService", operation name "processResponse" and correlation set "" (or conversation ID). Appendix A - Standard Faults in the BPEL 1.1 specification specifies a fault should be thrown under these conditions. Redeploy the process after removing the conflicting receive activities. </summary>
 </part>
    </conflictingReceive>
</bpelFault>3
Conversation id определяется как атрибут к операциям асинхронных вызова и приема, и должен быть одинаковым в пределах одного потока для обеих операций, но обязательно отличным от других потоков, чтобы платформа могла определить соответствие асинхронных операций вызова и приема.
<invoke name="AsyncInvoke" partnerLink="AsyncService"
                        portType="ns1:AsyncWrapper" operation="process"
                        bpelx:invokeAsDetail="no"
                        inputVariable="AsyncInvoke_InputVariable"
                        bpelx:conversationId="$convId"/>
<receive name="AsyncReceive" createInstance="no"
                         variable="AsyncReceive_InputVariable"
                         partnerLink="AsyncService"
                         portType="ns1:AsyncWrapperCallback"
                         operation="processResponse"
                         bpelx:conversationId="$convId"/>
В значение conversation id, часто бывает полезно вывести значимую информацию, которая будет отображена в Enterprise Manager и может помочь при отладке, например "thread 1/10; entry 75/500". 

Описанных в данной секции действий достаточно для реализации параллельной версии процесса.

4. Оценка ускорения от распараллеливания.

При оценке ускорения от распараллеливания, стоит учитывать закон Амдала:
В случае, когда задача разделяется на несколько частей, суммарное время её выполнения на параллельной системе не может быть меньше времени выполнения самого длинного фрагмента.
Если рассматривать идеальный случай, когда 100% задачи можно распараллелить, то ускорение должно быть близко к значению количества работающих параллельно потоков.
К сожалению, на практике так не бывает, основные ситуации которые стоит учитывать:
- в OSS предусмотрены параметры определяющие число возможных параллельных неблокирующих вызовов и количества параллельных потоков обрабатываемых платформой;
- при большом количестве параллельных вызовов, целевая система может не справляться с их обработкой;
- при высокой загрузке собственных серверов, OOS будет не способна поддерживать большое число параллельных потоков;
Описанные выше ситуации лишь базовые вещи на которые стоит обратить внимание, каждый случай требует индивидуального подхода и настройки платформы в промышленной среде исходя из типа и конфигурации вычислительных мощностей. 

Время выполнения, рассмотренного в статье примера**:
- последовательной версии: 48 сек.
- параллельной версии, 2 потока: 27 сек.
- параллельной версии, 5 потоков: 15 сек.
- параллельной версии, 10 потоков: 8 сек.
- параллельной версии, 20 потоков: 12 сек.***
** рассматривайте приведенное время как условные единицы, так как все замеры сделаны в равных условиях, но при проведении тестов на других серверах время может кардинально отличаться.

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

5. Возможные проблемы.

На этапе отладки системы встретилась проблема. При использовании OOS в кластерной конфигурации с использованием больше трех параллельных потоков в BPEL-процессе,  в моменты пиковой загрузки, в Enterprise Manager периодически возникали ошибки:
<bpelFault>
 <faultType>
   <message>0</message>
 </faultType>
 <remoteFault xmlns="http://schemas.oracle.com/bpel/extension">
  <part name="summary">
   <summary>oracle.fabric.common.FabricInvocationException</summary>
  </part>
  <part name="detail">
   <detail>null</detail>
  </part>
  <part name="code">
   <code>null</code>
  </part>
 </remoteFault>
</bpelFault>
К сожалению, заведение Oracle Support Request не дало решение данной проблемы, из-за отсутствия данной ошибки в логах платформы и серверов.

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

Так же отмечу, что в MBeans OOS есть недокументированный пул определяющий количество возможных одновременных неблокирующих вызовов. Данный параметр напрямую влияет на ускорение достижимое при описанном виде распараллеливания.
Его значение можно изменить в Enterprise Manager: "SOA-Administration" -> "BPEL Service Engine Properties" -> "More BPEL Configuration Properties…" -> DispatcherNonBlockInvokeThreads

В целом, применение описанного подхода распараллеливания показало стабильную работу в промышленной среде, помогло ускорить процесс в ~2.5 раза и избежать JTA таймаута.

Все примеры, используемые в статье, доступны на github.

Комментариев нет:

Отправить комментарий