Статьи

Балансировка нагрузки и отказоустойчивость Apache CXF

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

В итоге мы получили микробиблиотеку с балансировкой нагрузки (ESB / UDDI / WS-Addressing казались интересными альтернативами, но в нашей ситуации они были избыточным). Если бы мы только знали, что Apache CXF уже поддерживает все эти функции (почти) из коробки?

Не вините нас, хотя, только ссылка на эту функцию указывает на очень плохую страницу документации (если вы называете 404 «плохой»). Если его нет в официальной документации, я бы ожидал найти его в книге по разработке веб-сервисов Apache CXF — к сожалению, там тоже не повезло. Но разве вы сами не исследуете такие возможности? Это конфигурация клиента, с которой мы начинаем:

01
02
03
04
05
06
07
08
09
10
11
       xmlns:jaxws="http://cxf.apache.org/jaxws"
       xmlns:clustering="http://cxf.apache.org/clustering"
       xmlns:util="http://www.springframework.org/schema/util">
 
    <jaxws:client id="testServiceClient"
                  serviceClass="com.blogspot.nurkiewicz.cxfcluster.SimpleService"
                  address="http://serverA/simple">
    </jaxws:client>
 
</beans>
Интерфейс конечной точки здесь не важен, достаточно знать, что testServiceClient внедряется в некоторые другие сервисы, и функции балансировки нагрузки и переключения при сбое не должны влиять на существующий код. Обратите внимание, что служебный адрес является фиксированным и жестко запрограммированным (разумеется, его можно вывести и прочитать при запуске).

Удивительно, что включение аварийного переключения само по себе довольно просто, понятно и самоочевидно (несмотря на то, что это XML):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
<jaxws:client id="testServiceClient"
              serviceClass="com.blogspot.nurkiewicz.cxfcluster.SimpleService"
              address="http://serverA/simple">
 
    <jaxws:features>
        <clustering:failover>
            <clustering:strategy>
                <bean class="org.apache.cxf.clustering.RandomStrategy">
                    <property name="alternateAddresses">
                        <util:list>
                            <value>http://serverB/simple</value>
                            <value>http://serverC/simple</value>
                            <value>http://serverD/simple</value>
                        </util:list>
                    </property>
                </bean>
            </clustering:strategy>
        </clustering:failover>
    </jaxws:features>
 
</jaxws:client>
Адрес serverA используется в качестве основной конечной точки, но при сбое все конечные точки аварийного переключения ( serverB , serverC и serverD ) проверяются в случайном порядке. Чтобы немного поиграть с этой конфигурацией, я советую вам включить ведение журнала запросов и ответов Apache CXF:
1
2
3
4
5
<cxf:bus>
    <cxf:features>
        <cxf:logging/>
    </cxf:features>
</cxf:bus>
Еще раз (!) В официальной документации не упоминается об очень удобном параметре конфигурации prettyLogging, который можно применять к функции ведения журнала, чтобы сделать правильное форматирование запросов и ответов XML (новые строки и отступы) перед регистрацией. Я не рекомендовал бы это для настройки производства, но во время разработки и тестирования форматирование сообщений SOAP неоценимо:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
<bean id="abstractLoggingInterceptor" abstract="true">
    <property name="prettyLogging" value="true"/>
</bean>
<bean id="loggingInInterceptor" class="org.apache.cxf.interceptor.LoggingInInterceptor" parent="abstractLoggingInterceptor"/>
<bean id="loggingOutInterceptor" class="org.apache.cxf.interceptor.LoggingOutInterceptor" parent="abstractLoggingInterceptor"/>
 
<cxf:bus>
    <cxf:inInterceptors>
        <ref bean="loggingInInterceptor"/>
    </cxf:inInterceptors>
    <cxf:outInterceptors>
        <ref bean="loggingOutInterceptor"/>
    </cxf:outInterceptors>
    <cxf:outFaultInterceptors>
        <ref bean="loggingOutInterceptor"/>
    </cxf:outFaultInterceptors>
    <cxf:inFaultInterceptors>
        <ref bean="loggingInInterceptor"/>
    </cxf:inFaultInterceptors>
</cxf:bus>
Таким образом, наш сервис прекрасно переключается на резервные конечные точки, если основная не доступна. Но у нас есть четыре эквивалентных сервера, и мы хотим, чтобы наш клиент относился к ним одинаково, поражая каждый с одинаковой вероятностью (round robin? Random?). Вот когда балансировка нагрузки выходит на сцену:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
<jaxws:client id="testServiceClient" serviceClass="com.blogspot.nurkiewicz.cxfcluster.SimpleService">
 
    <jaxws:features>
        <clustering:loadDistributor>
            <clustering:strategy>
                <bean class="org.apache.cxf.clustering.SequentialStrategy">
                    <property name="alternateAddresses">
                        <util:list>
                            <value>http://serverA/simple</value>
                            <value>http://serverB/simple</value>
                            <value>http://serverC/simple</value>
                            <value>http://serverD/simple</value>
                        </util:list>
                    </property>
                </bean>
            </clustering:strategy>
        </clustering:loadDistributor>
    </jaxws:features>
 
</jaxws:client>
Обратите внимание, что сам клиент больше не определяет атрибут адреса. Это говорит о том, что список alternateAddresses используется исключительно во всех вызовах, а первичный адрес не существует, что на самом деле так. SequentialStrategy будет использовать одну конечную точку за другой, обеспечивая хорошую циклическую реализацию ( также доступна RandomStrategy ). Также в этой конфигурации вы получите аварийное переключение бесплатно — в случае сбоя какой-либо конечной точки будут проверены все конечные точки, начиная с первой (очевидно, кроме той, которая только что вышла из строя).
Большой! Теперь клиенты CXF стали гораздо более жесткими и отказоустойчивыми. Но в нашем путешествии для повышения доступности и минимизации простоев, когда альтернативные узлы загружаются только при запуске приложения (другими словами — добавление нового сервера требует перезапуска всех клиентов) слишком ограничено. К счастью, мы можем сделать нашу балансировку нагрузки более динамичной за два простых шага.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
<jaxws:client id="testServiceClient" serviceClass="com.blogspot.nurkiewicz.cxfcluster.SimpleService">
 
    <jaxws:features>
        <clustering:loadDistributor>
            <clustering:strategy>
                <bean class="org.apache.cxf.clustering.SequentialStrategy">
                    <property name="alternateAddresses" ref="alternateAddresses"/>
                </bean>
            </clustering:strategy>
        </clustering:loadDistributor>
    </jaxws:features>
 
</jaxws:client>
 
<util:list id="alternateAddresses" list-class="java.util.concurrent.CopyOnWriteArrayList">
    <value>http://serverA/simple</value>
    <value>http://serverB/simple</value>
    <value>http://serverC/simple</value>
    <value>http://serverD/simple</value>
</util:list>
Ничего особенного, извлекающего вложенный анонимный боб. Но имея доступ к этому списку (обратите внимание, что я использовал java.util.concurrent.CopyOnWriteArrayList ), мы можем внедрить его в любой другой сервис, возможно, изменив его состояние. Как я знаю, что это будет работать? Что ж, я потратил несколько дней на отладку Apache CXF, чтобы, наконец, обнаружить алгоритм балансировки нагрузки: при первом вызове CXF запрашивает у стратегии список возможных узлов. Затем он передает этот список обратно в стратегию с просьбой выбрать один (маленький wtf здесь…). Стратегия решает, какой адрес использовать, и удаляет выбранный адрес из списка (еще один маленький здесь…) Когда CXF обнаруживает, что список пуст, история повторяется сам. Таким образом, если мы заменим список альтернативных адресов во время выполнения, после одного раунда новый список будет возвращен в базовую инфраструктуру CXF.
Поскольку я большой сторонник JMX, вот как мы собираемся изменить список адресов (вы можете использовать любой механизм, который вам нравится):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
@ManagedResource
public class AlternateAddressesManager {
 
    @Resource
    private List alternateAddresses;
 
    @ManagedOperation
    public void addAlternateAddress(String address) {
        alternateAddresses.add(address);
    }
 
    @ManagedOperation
    public boolean removeAlternateAddress(String address) {
        return alternateAddresses.remove(address);
    }
 
    @ManagedAttribute
    public List getAlternateAddresses() {
        return Collections.unmodifiableList(alternateAddresses);
    }
 
}
Да, это тот же список alternateAddresses, который используется SequentialStrategy , поэтому, просто изменив его, мы изменим поведение адресации CXF. Возможно, мы могли бы расширить CopyOnWriteArrayList, добавив несколько дополнительных методов с поддержкой JMX (или, используя гибкость Springs, выставить методы List напрямую через JMX!), Но это снизило бы удобство обслуживания, и я бы посчитал это плохим дизайном.
Наконец, мы можем запустить jconsole или JVisualVM, как показано на скриншотах ниже, и пользоваться нашей инфраструктурой балансировки нагрузки:

Счастливый? На самом деле, нет. При изучении исходного кода CXF я натолкнулся на этот ужасный комментарий JavaDoc о классах LoadDistributorFeature и FailoverTargetSelector, которые играют значительную роль в процессе балансировки нагрузки:
/ **
* […]
* Обратите внимание, что эта функция изменяет канал на лету и таким образом делает
* Клиент не безопасен.
* /
Сфокусируйтесь на тексте, выделенном жирным шрифтом (хорошо, честно, я не понимаю остальных) Если вы некоторое время работали с Spring, вы знаете, что bean-компонент testServiceClient является общим синглтоном, который используется несколькими потоками одновременно (нет, создание области прототипа не поможет; почему?), В отличие от стандартных сессионных EJB-компонентов без состояния, которые объединены К счастью, у Spring есть встроенное решение для этого. Но прежде чем я наконец нашел правильное решение, возникло несколько препятствий.
Во-первых, тег jaxws: client из пространства имен CXF не позволяет определять область действия bean-компонента, по умолчанию — singleton, в то время как мы хотим объединить наших клиентов. Поэтому мне пришлось вернуться к старому доброму определению bean-компонента с помощью org.apache.cxf.jaxws.JaxWsProxyFactoryBean . Нет проблем, немного более многословно, хотя, если вы не можете / не хотите использовать пользовательские пространства имен Spring, вы могли бы использовать его с самого начала. Теперь лучшая часть: я могу просто обернуть любой bean-компонент с областью действия прототипа в специальный прокси-сервер, и Spring автоматически создаст пул объектов (на основе библиотеки commons-pool ) и создаст столько экземпляров bean, сколько необходимо, чтобы каждый компонент использовался только одним нить. Вот конфигурация:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<bean id="testServiceClientFactoryBean" class="org.apache.cxf.jaxws.JaxWsProxyFactoryBean">
    <property name="serviceClass" value="com.blogspot.nurkiewicz.cxfcluster.SimpleService"/>
    <property name="features">
        <util:list>
            <bean class="org.apache.cxf.clustering.LoadDistributorFeature">
                <property name="strategy">
                    <bean class="org.apache.cxf.clustering.SequentialStrategy">
                        <property name="alternateAddresses" ref="alternateAddresses"/>
                    </bean>
                </property>
            </bean>
        </util:list>
    </property>
</bean>
 
<bean id="testServiceClientTarget" factory-bean="testServiceClientFactoryBean" factory-method="create" scope="prototype" lazy-init="true"/>
 
<bean id="testServiceClient" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="targetSource">
        <bean class="org.springframework.aop.target.CommonsPoolTargetSource">
            <property name="targetClass" value="com.blogspot.nurkiewicz.cxfcluster.SimpleService"/>
            <property name="targetBeanName" value="testServiceClientTarget"/>
            <property name="maxSize" value="10"/>
            <property name="maxWait" value="5000"/>
        </bean>
    </property>
</bean>
Вы заметили атрибуты пула maxSize и maxWait ? Они безумно классные ! Вы можете запретить Spring создавать более 10 клиентов в пуле, и если пул пуст (все бины в настоящее время используются), нам следует подождать не более 5000 мс (и то, что происходит потом, настраивается!) Это на самом деле очень простой, но мощный механизм регулирования, намного более простой, чем JMS или пулы явных потоков, мы получаем абсолютно бесплатно! Например, не хотите обслуживать более 20 одновременно работающих клиентов веб-служб? Сделайте так, чтобы ваш компонент службы доступа к конечной точке сервера был объединен с размером, ограниченным 20. Клиент, превышающий это ограничение, будет отклонен, так как компонент службы недоступен.
Конечно, в мире взрослых ничего не работает, как ожидалось. Я быстро обнаружил, что JaxWsProxyFactoryBean.create не является потокобезопасным, и сообщил о CXF-3558 . В качестве обходного пути мне пришлось синхронизировать фабрику клиентов, используемую CommonsPoolTargetSource, просто подклассифицировав ее:
01
02
03
04
05
06
07
08
09
10
11
12
import org.apache.commons.pool.ObjectPool;
import org.apache.commons.pool.PoolUtils;
import org.springframework.aop.target.CommonsPoolTargetSource;
 
public class SynchCommonsPoolTargetSource extends CommonsPoolTargetSource {
 
    @Override
    protected ObjectPool createObjectPool() {
        return PoolUtils.synchronizedPool(super.createObjectPool());
    }
 
}
Синхронизация фабрики кажется общей потребностью, поэтому я создал SPR-8382 — возможно, он найдет свой путь к официальному выпуску. Кстати, работая над этой статьей, я также сообщил об ошибке IDEA-70365 ложная ошибка «Не удалось автоматически подключить» для bean-компонентов типа List .
В заключение! Наша балансировка нагрузки и аварийное переключение работают как шарм. Следующим шагом будет временное удаление узлов, которые не работают в течение пары секунд, и увеличение этого времени, если после этого конечная точка все еще не работает. Но Apache CXF имеет настолько ужасный API в этой области, что мне пришлось на некоторое время покинуть эту тему. Может быть, вы можете помочь?

Ссылка: Включение балансировки нагрузки и отработки отказа в Apache CXF от нашего партнера JCG Томаша из блога NoBlogDefFound .

Статьи по Теме :