Разработка для обеспечения высокой доступности и кластеризации

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

JIRA предоставляет опцию развертывания с кластеризацией через JARA Data Center. Информация на этой странице поможет вам разработать свой плагин в условиях высокой доступности и кластеризации.

Топология кластера для нескольких активных серверов со следующими характеристиками:

  • В экземплярах JIRA используется один экземпляр родственной базы данных.
  • Индекс Lucene реплицируется почти в реальном времени, а копия хранится локально для каждого экземпляра.
  • Вложения хранятся в общем месте.
  • Экземпляры JIRA будут держать свои внутренние кэши данных согласованными.
  • Несколько экземпляров могут быть активны в любой момент времени.
  • Доступны широко-кластерные блокировки.
  • Балансировщик нагрузки должен быть настроен для обмена запросами(requests) между всеми активными узлами в кластере
    • Балансировщик нагрузки не будет направлять какие-либо запросы (requests ) на узел, который неактивен, и перенаправлять все запросы (requests ) для сеанса на один и тот же узел
  • Активные серверы будут:
    • Запрашивать веб-запросы (requests ).
    • Обрабатывать фоновые и повторяющиеся задачи.
    • Запланированные задачи могут быть настроены для запуска на одном или всех экземплярах в кластере.
    • Практически все ведут себя точно так же, как автономный сервер JIRA.
  • Пассивные серверы:
    • Не обрабатывают веб-запросы.
    • Не выполняют никаких фоновых или повторяющихся задач.
    • Находятся в состоянии, чтобы они могли взять на себя рабочую нагрузку в кратчайшие сроки, т. е. загружают все плагины, обновляют индекс Lucene и т. д.

РИСУНОК

Упрощенная архитектурная диаграмма.

Обязанности плагина

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

JIRA-дом

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

локальный дом

o   Журналы

o   Кэши, локальные кеши, которые в основном представляют собой индекс Lucene

o   tmp

o   Данные мониторинга

o   dbconfig.xm

o   lcluster.properties

общий дом

o   Данные, включая вложения и аватары

o   Кэши, общие

o   экспорт

o   Импорт

o   Плагины

 

Метод JiraHome.getHome () возвращает общий дом. Новый метод getLocalHome () возвращает локальный дом. Аналогично, JiraHome.getHomePath () возвращает путь к общему дому, а getLocalHomePath () возвращает путь к локальному дому.

 

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

Запланированные задачи и фоновые процессы

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

 

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

API-интерфейс планировщика Atlassian  доступен через Maven следующим образом:

Координаты Maven


<dependency>
   <groupId>com.atlassian.scheduler</groupId>
   <artifactId>atlassian-scheduler-api</artifactId>
   <version>1.0</version>
</dependency>

Примечание. API-интерфейс Atlassian Scheduler является транзитивной зависимостью jira-api, поэтому нет необходимости напрямую зависеть от нее.

 

Плагин должен сделать две вещи, чтобы запланировать работу:

  1. Зарегистровать JobRunner и
  2. Запланировать задание.

Движок работы (Job Runner)

Job Runner - это экземпляр класса, который реализует JobRunner и используется для выполнения необходимой работы. Планировщик вызовет метод runJob(final JobRunnerRequest jobRunnerRequest), передав объект JobRunnerRequest, который будет содержать любые параметры, заданные при планировании задания. Job Runner регистрируется с ключом, который должен быть уникальным для JobRunner, но любое количество заданий может быть запланировано для использования того же JobRunner. Рекомендуется использовать ключ модуля плагина для пространства имен вашего ключа JobRunner.

Пример


schedulerService.registerJobRunner(JobRunnerKey.of("com.example.schedulingplugin.JOBXX"), new SendFilterJob());

Регистрация плагина является переходной, и плагины должны регистрировать и отменить регистрацию своих JobRunners на событиях с «включенными плагинами» и «отключенными плагинами».

Планирование работы

Работы могут планироваться для запуска одним из двух способов:

  1. Однажды через весь кластер. Задания, запланированные для запуска по всему кластеру:
  • Являются постоянными(устойчивыми). Они хранятся в базе данных и сохраняются через перезагрузки JIRA и остаются, даже если плагин отключен или удален.(Сохраненные задания без JobRunner игнорируются до тех пор, пока JobRunner не будет зарегистрирован снова.)
  • Могут запускаться на любом узле кластера, а не только на узле, который первоначально планировал работу.
  • Будет выполняться только один раз для каждого запланированного времени выполнения
  • В некоторых случаях одновременно могут выполняться несколько экземпляров одного и того же задания. Это может произойти, если интервал работы меньше времени, необходимого для завершения задания. В таком случае узел может запустить новое задание, хотя другой экземпляр этого задания все еще выполняется на другом узле кластера. Чтобы этого избежать, ограничьте задания на определенные узлы, используя блокировки всего кластера.
  • Должен планироваться только один узел в кластере.

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

  1. Локально. Задания, которые планируется запустить локально:
  • Не постоянные. Они должны планироваться каждый раз, когда плагин включен и незапланирован, когда плагин отключен.
  • Должен планироваться на каждом узле кластера, если это подходит для требований вашего плагина.
  • Будет выполняться на узле, где они запланированы, а не на любом другом узле (кроме случаев, когда это запланировано на этом узле).

Вы можете передавать параметры планировщику при планировании задания. Эти параметры должны быть Serializable, и хотя вы можете включать в себя классы, определенные вашим плагином, это может усложнить для вашего плагина обновление ранее существовавшего расписания, если была изменена сериализованная форма объекта. Безопаснее хранить простые типы данных, такие как Списки, Карты, Строки и Длинные.


Schedule schedule = Schedule.forCronExpression(cronExpression);
JobConfig jobConfig = JobConfig.forJobRunnerKey(JobRunnerKey.of("com.example.schedulingplugin.JOBXX"))
                .withSchedule(schedule)
                .withParameters(ImmutableMap.<String, Serializable>of("SUBSCRIPTION_ID", subscriptionId));
JobId jobId = JobId.of("com.atlassian.example.schedulingplugin:" + subId);
schedulerService.scheduleJob(jobId, jobConfig);

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

Подробный пример использования SchedulerService доступен на BitBucket.

Обратная совместимость

Если вы хотите поддержать одну версию своего плагина, совместимую с предыдущими версиями JIRA, а также совместимую с кластером в JIRA 6.3 и выше, вам нужно будет использовать библиотеку atlassian-scheduler-compat.

Эта библиотека не предоставляет всех возможностей планировщика Atlassian, но обеспечивает функциональную эквивалентность средств обслуживания в JIRA до версии 6.3. Полностью обработанный пример доступен на Bitbucket.

Общие проблемы

  • В предыдущей реализации SAL PluginScheduler созданы службы для представления заданий плагина. Эти изменения нарушены этими изменениями, и ни API-интерфейс atlassian-scheduler, ни SAL PluginScheduler не будут продолжать это делать. Разработчики плагинов, возможно, ранее проинструктировали администраторов JIRA проконсультироваться на странице конфигурации служб, чтобы проверить установку плагина или изменить интервал выполнения заданий. Задания плагинов больше не перечислены там, поэтому эту документацию необходимо обновить, чтобы ссылаться на страницу сведений о планировщике.
  • Запланированные задания больше не настраиваются напрямую через администрирование пользовательского интерфейса . Разработчики плагинов, которые хотят, чтобы их задания настраивались, должны будут реализовать свои собственные страницы конфигурации для поддержки этой возможности и соответственно обновлять свою документацию.
  • Планирование заданий непосредственно с помощью библиотеки Quartz устарело, и возможность сделать это будет полностью удалена в JIRA 7.0.
  • Задания, которые были запланированы непосредственно с помощью Quartz, не переносятся задачами (tasks) обновления JIRA. Плагин должен выполнить свои собственные меры для переноса любых заданий и триггеров, которые он создал в API-интерфейс atlassian-scheduler.

Исполнители

Если плагин использует Executor (Исполнитель) для выполнения повторяющихся задач (tasks), он должен обязательно закрыть Executor на узле NodePassivatingEvent и перезапустить его в NodeActivatedEvent.

Кеши

Все кеши JIRA должны считаться недействительными на пассивном узле.

Когда узел активируется, все внутренние кеши JIRA будут очищены. JIRA также будет взаимодействовать с библиотекой AtlassianCache и сбрасывать все кэши, которыми она управляет.

Если плагин поддерживает какое-либо состояние в кэше, они должны использовать библиотеку AtlassianCache (v2.0 или выше) для управления своими кэшами. Плагины должны использовать кеши для самостоятельной загрузки, где это возможно. API Atlassian Cache доступен через Maven следующим образом:


<dependency>
    <groupId>com.atlassian.cache</groupId>
    <artifactId>atlassian-cache-api</artifactId>
    <version>2.0.2</version>
</dependency>

  • Примечание. API-интерфейс Atlassian Cache является транзитивной зависимостью jira-api, поэтому большинству плагинов не нужно явно указывать его.
  • Плагины должны использовать JiraPropertySetFactory.buildCachingPropertySet или JiraPropertySetFactory.buildCachingDefaultPropertySet, а не создавать их непосредственно с помощью PropertySetManager.getInstance ("cached", ...), чтобы получить набор свойств, который безопасен для использования в кластере.

 

Пример


// Self loading cache using Atlassian Cache
    private final Cache<String, String> myLowerCaseCache;

// To get or create the cache. This would normally be in your component's constructor
    cache = cacheManager.getCache(CachingAvatarStore.class.getName() + ".cache",
        new AvatarCacheLoader(),
        new CacheSettingsBuilder().expireAfterAccess(30, TimeUnit.MINUTES).build());    

// The loader class
    private class AvatarCacheLoader implements CacheLoader<Long, CacheObject<Avatar>>
    {
        @Override
        public CacheObject<Avatar> load(@NotNull final Long avatarId)
        {
            return new CacheObject<Avatar>(CachingAvatarStore.this.delegate.getById(avatarId));
        }
    }

// To retrieve an object from the cache
    cache.get(avatarId).getValue()

Atlassian Cache также обеспечивает безопасный механизм построения LazyReference, который фактически является кешем одного объекта. Это может быть очень полезно, если вам нужно поддерживать кеш всех экземпляров чего-либо, особенно если число известно относительно небольшим.


    CachedReference<AllProjectRoles> projectRoles;
   
// To get or create the reference. This would normally be in your components constructor
    projectRoles = cacheManager.getCachedReference(getClass(), "projectRoles",
                new AllProjectRolesLoader());

// The Supplier
    class AllProjectRolesLoader implements Supplier<AllProjectRoles>
    {
        public AllProjectRoles get()
        {
            return new AllProjectRoles(delegate.getAllProjectRoles());
        }
    }

// To retrieve the object
    return projectRoles.get();
// When you add/update/remove a project role reset the reference. This will be propagated across the cluster.
    projectRoles.reset();

Заметки:

  • В кластерной среде, когда вы получаете кеш, он уже может быть создан другим узлом в кластере. Когда плагин обновляется, объекты в кеше могут принадлежать другому загрузчику классов или другой версии класса. Чтобы защитить ваш плагин от проблем несовместимости классов, вы должны очистить кеш, когда ваш плагин включен (и отключен).
  • Кэши следует называть однозначно. В плагине вы, вероятно, должны использовать ключ модуля плагина в качестве пространства имен для ваших кешей.
  • Любой объект, который должен быть реплицирован в кластерной среде, должен быть сериализирован. Кроме того, классы для этих объектов должны быть доступны из класса пути приложения. Поэтому рекомендуется, чтобы при применении плагинов разработчики использовали классы из Java API (например, java.lang.String).
    • Все ключи должны быть сериализованы и на пути к классам.
    • Если вы используете кеш-память для самостоятельной загрузки и репликация записей является недействительной (по умолчанию для этого стиля кеша), тогда значения в кеше не обязательно должны быть сериализованы или в пути к классам.
  • Вы можете использовать CacheSettings и связанный с ним CacheSettingsBuilder, чтобы адаптировать поведение кэша к вашим потребностям.
  • Вы всегда должны пытаться установить политику истечения срока действия для создаваемых вами кешей, чтобы избежать чрезмерного потребления объема памяти. Для большинства приложений наиболее подходящим является истечение срока действия базы. Как правило, очень сложно определить подходящее количество записей в кеше, которое подходит для клиентов во всех масштабах.

Обратная совместимость

Если вы хотите сохранить одну версию своего плагина, совместимую с предыдущими версиями JIRA, а также совместимую с кластером в JIRA 6.3 и выше, вам нужно будет использовать библиотеку atlassian-cache-compat.

Эта библиотека предоставляет все возможности Atlassian Cache, но ее немного сложнее включить в ваш плагин. Полностью обработанный пример доступен на Bitbucket.

Кластерные замки

Иногда плагин хочет убедиться, что данная операция выполняется только на одном узле кластера за раз. Плагины могут сделать это, приобретя широко-кластерную блокировку, используя API Atlasian «Beehive». Например:


package com.example.locking;

import com.atlassian.beehive.ClusterLockService;
import java.util.concurrent.locks.Lock;
import javax.annotation.Resource;


public class MyService {
 
    // Your lock should have a globally unique name, e.g. using fully qualified class name, as here
    private static final String LOCK_NAME = MyService.class.getName() + ".myLockedTask";
    
    @Resource  // or inject via constructor, etc.
    private ClusterLockService clusterLockService;
    
    public void doSomethingThatRequiresAClusterLock() {
        final Lock lock = clusterLockService.getLockForName(LOCK_NAME);
        lock.lock();
        try {
            // Do the thing that needs a lock
        }
        finally {
            lock.unlock();
        }
    }
}      

Следует отметить:

  • Замки подходят для таких вещей, как:
    • Обеспечение того, чтобы данная операция выполнялась только один раз в кластере за раз
    • Выполнение работы атомарно, например, чтение из базы данных, а затем возврат некоторых обновленных значений
  • Чтобы поддерживать хорошую производительность, блокировки не должны использоваться для высококонкурентных операций.
  • Замки, предоставленные Beehive, теперь являются реентерабельными(повторно используемыми); код, который требует блокировки реентера, должен, вероятно, быть реорганизован для использования не-реентерабельных замков, но там, где это невозможно, оно должно вести себя корректно в любом случае.
  • Если ваш плагин должен работать с продуктом / версией Atlassian, который еще не поддерживает кластеризацию, ему необходимо объявить зависимость от библиотеки com.atlassian.beehive: beehive-compat, которая предоставляет ClusterLockServiceFactory, из которой вы можете получить ClusterLockService , Эта служба обеспечит блокировку JVM в некластерной среде и кластерных блокировках в кластерной среде. Если ваш плагин будет использоваться когда-либо только с продуктом, который предоставляет ClusterLockService, вы можете напрямую вставлять эту услугу, не используя фабрику(заводскую установку).

Обратная совместимость

Если вы хотите пддержать одну версию своего плагина, совместимую с предыдущими версиями JIRA, а также совместимую с кластером в JIRA 6.3 и выше, вам нужно будет использовать библиотеку beehive-compat.

Эта библиотека предоставляет все возможности Atlassian Beehive, но ее немного сложнее включить в ваш плагин. Полностью обработанный пример доступен на Bitbucket.

Кластерные сообщения

Плагин может отправлять и слушать короткие сообщения, которые отправляются между узлами в кластере. Указав «канал», плагин может зарегистрировать ClusterMessageConsumer с помощью ClusterMessagingService. Когда сообщение отправляется на этот канал с другого узла, будут вызываться все зарегистрированные ClusterMessageConsumer. Ниже приведен базовый пример:


package com.example.messaging;

import com.atlassian.jira.cluster.ClusterMessageConsumer;
import com.atlassian.jira.cluster.ClusterMessagingService;

public class MyService {

    private static final String EXAMPLE_CHANNEL = "EXAMPLE";
    private final ClusterMessagingService clusterMessagingService;
    private final MessageConsumer messageConsumer;

    public MyService(final ClusterMessagingService clusterMessagingService) {
        this.clusterMessagingService = clusterMessagingService;
        messageConsumer = new MessageConsumer();
        clusterMessagingService.registerListener(EXAMPLE_CHANNEL, messageConsumer);
    }

    public void actionRequiringOtherNodesToBeNotified() {
        // Perform action
        clusterMessagingService.sendRemote(EXAMPLE_CHANNEL, "Action completed");
    }

    private static class MessageConsumer implements ClusterMessageConsumer {

        @Override
        public void receive(final String channel, final String message, final String senderId) {
            // Handle message
        }
    }
}

Следует отметить:

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

События

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

Существует четыре события, которые будут выпущены в следующих сценариях:

  • Активация, то есть плагин работает на пассивном узле, который продвигается как активный узел:
    • NodeActivatingEvent - выдается в начале процесса активации
    • NodeActivatedEvent - выдается в конце процесса активации. Плагины могут слушать это, если у них есть специальная обработка для выполнения, когда узел становится активным.
  • Пассивирование, т. е. плагин работает на активном узле, и этот узел переводится в режим ожидания:
    • NodePassivatingEvent - выдается в начале процесса пассивации. Плагины должны останавливать любые фоновые задачи, которые они выполняют, когда получают это событие.
    • NodePassivatedEvent - выдается в конце процесса пассивации.

 

Дополнительные сведения о прослушивании событий см. В разделе «Написание прослушивателей событий JIRA с помощью библиотеки atlassian-event»

Веб-запросы

Никакие веб-запросы не должны достигать плагина, когда узел находится в пассивном состоянии. Ответственность за конфигурацию балансировки нагрузки и JIRA лежит на их блокировке.

Общее руководство

Не поддерживаете состояние! Чем меньше состояний поддерживает плагин, тем более масштабируемым он будет и тем легче адаптируется к кластерной среде.

Тестирование вашего плагина

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

Маркировка вашего плагина как совместимого с кластером для Marketplace

Когда вы указываете свою первую версию плагина, совместимую с кластером, в Marketplace, измените файл дескриптора atlassian-plugin.xml. Это сообщает Marketplace и UPM, что ваш плагин совместим с Центром обработки данных (или кластером). Добавьте в раздел  plugin-info плагина-информации следующий параметр:


<param name="atlassian-data-center-compatible">true</param>

Вот пример общего  блока plugin-info с этим параметром:


<plugin-info>
        <description>${project.description}</description>
        <version>${project.version}</version>
        <vendor name="${project.organization.name}" url="${project.organization.url}" />
        <param name="atlassian-data-center-compatible">true</param>
    </plugin-info>    

По материалам Atlassian JIRA  Server Developer Developing for high availability and clustering