При использовании транзакций в «традиционных» n-уровневых системах жизнь относительно проста. Например, когда вы запускаете транзакцию и возникает ошибка или сбой, вы прерываете транзакцию и легко откатываете любые изменения, возвращая согласованность и спокойствие всей системы. Это возможно по той причине, что транзакция изолирует сделанные в ней изменения от остального мира. Одно из базовых предположений, лежащих в основе транзакций, состоит в том, что время, прошедшее с начала транзакции до ее окончания, короткое. При таком предположении мы можем позволить себе роскошь позволить транзакции блокировать наши ресурсы (например, базы данных) и маскировать изменения от других, пока транзакция выполняется. Транзакции предоставляют четыре основных гарантии — атомарность, согласованность, изоляция и долговечность, которые обычно помнят по их аббревиатуре — ACID.
К сожалению, в распределенном мире, SOA или иным образом, редко рекомендуется использовать атомарные краткосрочные транзакции (более подробно см. Антишаблон перекрестных транзакций в главе 10). Действительно, тот факт, что кросс-сервисные транзакции являются препятствием, является одной из основных причин, по которой мы должны рассмотреть использование шаблона Saga.
Одним из очевидных недостатков Sagas является то, что вы не можете выполнять откат. Два условия, упомянутые выше, блокировка и изоляция больше не выполняются, поэтому вы не можете предоставить необходимую гарантию. Тем не менее, поскольку взаимодействия, особенно длительные, могут прерваться или быть отменены, саги предлагают понятие компенсаций. Компенсации это круто; у нас не может быть отката, поэтому вместо этого мы обратим операцию взаимодействия и сделаем псевдооткат. Если мы добавили сто (долларов / штук / еще много чего) во время первоначального действия, мы просто вычтем те же 100 в компенсации. Легко, правда?
Неправильно — как вы, наверное, знаете, это не легко. К сожалению, существует ряд проблем с компенсациями. Эти проблемы проистекают из того факта, что, в отличие от транзакций ACID, изменения, сделанные действиями Saga, не являются изолированными. Отсутствие изоляции означает, что другие взаимодействия со службой могут работать с данными, которые были изменены действием других саг, и сделать невозможной компенсацию. Например, если запрос к одной службе изменяет статус готовности космического челнока на «все готово», а другая служба вызывает запуск челнока в зависимости от этого состояния, было бы слишком поздно для первой службы попытаться изменить статус «все готово» теперь, когда «птица покинула курятник». Более простой (простите за каламбур) бизнес-сценарий — это любое взаимодействие, в котором вы работаете с ограниченными ресурсами, напримерзаказ из обычно ограниченного запаса.
Рассмотрим, например, сценарий на рисунке 6.1 ниже. Клиент заказывает товар. Служба заказа запрашивает товар со склада, так как хочет отправить товар покупателю (возможно, путем уведомления другой службы). В то же время в сервисе склада заказанный товар вызывает порог пополнения запаса, который инициирует заказ на пополнение запаса от поставщика. Тогда клиент решает отменить заказ — что теперь?
Рисунок 6.1. Глава 6 посвящена соединению Сервисов с потребителями Сервисов на уровнях и уровнях, выходящих за рамки базовых шаблонов обмена сообщениями.
Следует ли отменить заказ на пополнение запасов? Может ли он быть отменен в соответствии с условиями заказа поставщика? Кроме того, клиент, запрашивающий товар между заказом и отменой, может получить уведомление об отсутствии на складе, что заставит его обратиться к нашим конкурентам. Это может быть особенно проблематичным для заказов, которые могут быть отменены, например, при бронировании отелей, отпусков и т. Д.
Другое ограничение компенсаций и сам образец саги, в этом отношении, — то, что это требует координатора. Координатор означает доверие к внешнему субъекту, то есть к внешним (большинству) службам, участвующим в саге, чтобы все было понятно. Это является проблемой для некоторых целей SOA, поскольку она ставит под угрозу автономию и вводит нежелательную связь с внешним координатором.
Вопрос тогда
Как мы можем эффективно обеспечить уровень гарантии в слабосвязанной манере при сохранении автономности и согласованности услуг?
Мы уже обсуждали ограничения компенсаций, что, конечно, является одним из вариантов решения этой проблемы. Опять же, одна проблема заключается в том, что мы не можем позволить себе вносить мини-изменения, так как тогда мы будем зависеть от внешней стороны, чтобы установить рекорд. Другая проблема с компенсациями состоит в том, что мы выставляем эти «полугосударства», которые, по сути, являются внутренними деталями услуг, внешнему миру. Увеличение площади контракта на услуги, особенно благодаря внутренним деталям делает службы менее гибкими и более связанными с их средой (см. также шаблон защиты «белых ящиков» в главе 10)
Мы также упоминали, что распределенные транзакции не являются ответом, поскольку они оба слишком долго блокируют внутренние ресурсы (Saga может продолжаться несколько дней …?), А также оказывают чрезмерное доверие внешним службам, которые могут быть внешними по отношению к организации.
Похоже, что это своего рода болото, к счастью, в реальной жизни уже найден способ справиться с аналогичной потребностью в нечетких, половинных гарантиях — оговорках!
Внедрите шаблон бронирования и предоставьте сервисам уровень гарантии на внутренние ресурсы в течение ограниченного времени.
Рисунок 6.2 Схема бронирования. Служба, которая реализует резервирование, рассматривает некоторые сообщения как «резервирующие», в которых она пытается защитить внутренний ресурс и отправляет подтверждение в случае успеха. Когда сообщение считается «подтверждающим», служба подтверждает правильность бронирования. В промежутке между сервисами можно выбрать истечение срока бронирования на основании внутренних критериев.
Шаблон Reservation означает, что в службе будет внутренний компонент, который будет обрабатывать резервирования. В его обязанности входит
§ Резервирование — резервирование, когда приходит сообщение, которое считается «резервирующим». Например, когда поступает заказ, в дополнение к обновлению некоторого долговременного хранилища (например, базы данных) для заказа необходимо установить таймер или время истечения для подтверждения заказа, в качестве альтернативы он может установить некоторый маркер того, что заказ не является окончательным.
§ Валидация — проверка того, что резервирование остается в силе, до завершения процесса. В вышеупомянутом сценарии заказа необходимо убедиться, что предметы, предназначенные для заказа, не были переданы кому-либо еще.
§ Срок действия — отметка недействительного бронирования при изменении условий. Например, если VIP-клиент хочет зарезервированный товар, система может предоставить его для нее. Это также должно сделать недействительным мое бронирование, поэтому, когда я, наконец, попытаюсь заявить об этом, система узнает, что оно пропало. Истечение срока действия также может быть рассчитано, как, например, | мы храним книгу для вас до полудня завтра »
Резервирование может быть явным, т. Е. Контракт будет иметь действие ReserveBook или неявным. В случае неявного заказа служба самостоятельно решает, что будет считаться резервирующим сообщением, а что будет считаться подтверждающим сообщением, например, действие, такое как «Заказ», вызовет внутреннее резервирование, а действие, подобное закрытию саги, будет служить подтверждающим сообщением. Когда резервирование является неявным, реализация потребителя сервиса, вероятно, будет проще, так как дизайнеры-потребители, вероятно, будут рассматривать истечение срока резервирования как «простые» сбои, тогда как, когда оно явное, они, вероятно, будут обрабатывать состояние резервирования.
Бронирование происходит в деловых операциях по всему миру каждый день. Самый очевидный пример — оформление заказа на рейс. Вы отправляете запрос на комнату (инициируете сагу), в котором говорите, что приедете в определенную дату, скажем, на конференцию, и выезжаете на другую (завершите сагу). Отель говорит, хорошо, у нас есть номер для вас (бронирование) — при условии, что вы подтвердите свой приезд в установленный срок (ограниченное время). Даже если все прошло хорошо, вы все равно можете приехать в отель, только чтобы узнать, что ваш номер был передан другому лицу (ограниченная гарантия). Идея шаблона резервирования состоит в том, чтобы скопировать это поведение во взаимодействие сервисов, чтобы сервисы, поддерживающие резервирование, предлагали своего рода «ограниченную блокировку» в течение ограниченного времени и с ограниченным уровнем гарантии. Ограниченный уровень гарантии означает, что, как в реальной жизни,службы могут перебронировать, а затем решить, что перебронирование осуществляется с помощью различных стратегий, таких как «первая очередь»; VIP первым обслужен и т. Д.
Легко видеть, что Бронирование применяется к сервисам, которые обрабатывают «реальные» бронирования как часть их бизнес-логики, например, сервис заказа для отелей (используется в приведенном выше примере) или авиакомпании и т. Д. Однако бронирование подходит для множество других сценариев, где сервисы вызываются для предоставления гарантий на внутренние ресурсы. Например, в одной системе, которую я построил, мы использовали резервирование как часть процесса инициации саги. Система использует шаблон экземпляра службы (см. Главу 3), где некоторые службы имеют состояние (причины выходят за рамки этого обсуждения). Естественно, что сервисы имеют ограниченную способность обрабатывать потребителей (то есть экземпляр может обрабатывать n-количество одновременных саг / событий).
Это означает, что когда инициализируется сага, всем участникам саги необходимо знать случаи, которые являются частью саги. Пока один сервисный экземпляр инициирует саги, все в порядке. Однако, как показано на рисунке 6.3 ниже, когда две или более служб (или экземпляров) инициируют саги одновременно, они могут (и при условии достаточной нагрузки / времени, которые они будут) обе пытаются выделить один и тот же экземпляр службы своим относительным сагам. На иллюстрации мы видим, что как Инициатор A, так и Инициатор B хотят использовать Участника A и Участника B. Участник A имеет емкость 2, поэтому для обоих Инициаторов все в порядке. Служба B, однако, имеет ограниченную пропускную способность, поэтому, по крайней мере, одна из Sagas должна будет потерпеть неудачу при выделении, то есть не запускаться.
Рисунок 6.3. Пример ситуации, в которой может быть полезен шаблон резервирования
Шаблон резервирования позволил нам упорядоченно управлять этим процессом распределения ресурсов путем реализации двухпроходного протокола (чем-то похожего на двухфазную фиксацию). Инициатор просит каждого потенциального участника зарезервировать себя для саги. Каждый участник пытается зарезервировать себя и уведомить в случае успеха, поэтому в приведенном выше сценарии A скажет «да» обоим, а B скажет «да» одному из них. Если инициатор получает ОК от всех задействованных служб (в течение тайм-аута), он сообщает всем участникам конкретные случаи в саге (то есть инициирует его).
Участники резервируют себя только на короткий период времени. По истечении установленного времени ожидания участники самостоятельно удаляют обязательство. В качестве примечания, я просто скажу, что инициатор и другие участники саги не могут предположить, что участник будет там только потому, что они «официально» являются частью саги, и система все еще должна обрабатывать различные сценарии сбоев. Шаблон резервирования используется здесь только для предотвращения перерасхода и не предоставляет никаких транзакционных гарантий.
Резервирование в некоторой степени похоже на блокировку и, таким образом, «в некоторой степени» представляет некоторые риски, связанные с распределением блокировок. Эти риски не присущи шаблону, но могут легко проявиться, если вы не будете обращать внимания во время реализации (например, используя блокировки базы данных для реализации).
Первый риск, который стоит обсудить, это тупик. Всякий раз, когда вы начинаете резервировать что-нибудь, особенно. в распределенной среде вы вводите потенциал для тупиков. Например, если бы у обоих участников была возможность для одной саги, инициатор A сначала связывается с участником A, а затем с участником B и инициатором B в обратном порядке — у нас был бы потенциал тупика. В этом случае есть несколько механизмов, которые предотвращают этот тупик. Первый характерен для шаблона Reservation, когда участники сами снимают «замок». Однако, например, если существует механизм повторных попыток инициировать саги (поскольку оба будут терпеть неудачу после тайм-аута), и одни и те же ресурсы будут распределяться снова и снова, может возникнуть тупик после всех
Еще один риск, на который следует обращать внимание при реализации бронирования, — отказ в обслуживании (как злонамеренный, так и побочный продукт неправильного использования). DoS может происходить по аналогичным причинам, обсуждаемым в тупике (т. Е. Если вы вступаете в тупик, у вас также есть DoS). Другой способ заключается в использовании резервирования путем постоянного повторного бронирования. В зависимости от времени ожидания резервирования обычные брандмауэры могут не обнаруживать DoS, поэтому вы можете рассмотреть возможность использования Service Firewall (глава 4), чтобы помочь смягчить этот поток.
Помимо обсуждаемых выше рисков, еще одна вещь, на которую следует обратить внимание, это то, что, когда вы вводите Reservation, вы, вероятно, добавите дополнительные сетевые вызовы. Система, рассмотренная выше, упоминает, что, когда она вводит другой вызов, сообщите членам Саги, какие экземпляры участвуют в саге.
В дополнение к шаблону Service Firewall, упомянутому выше, другим шаблоном, связанным с резервированием, может быть шаблон Active Service (см. Главу 2). Шаблон Active Service может использоваться для обработки истечения срока резервирования при реализации по таймеру. Однако обратите внимание, что иногда лучше с точки зрения ресурсов обрабатывать истечение пассивно, а не активно, как мы увидим, рассматривая варианты реализации в следующем разделе.
1.1.3Технологическое картирование
В отличие от многих шаблонов, описанных в этой книге, шаблон Reservation является скорее бизнес-шаблоном, чем технологическим. Это означает, что нет прямого однозначного технологического сопоставления, чтобы это произошло. С другой стороны, с точки зрения кода шаблон относительно легко реализовать.
Одна вещь, которую вы должны сделать, это поддерживать постоянную ветку в сервисе, чтобы быть уверенным, что по истечении срока аренды или резервирования кто-то будет там для очистки. Одним из вариантов является шаблон Active Service, упомянутый выше. Вы можете использовать технологии, которые поддерживают синхронизированные события, которые предоставляют «услугу пробуждения» для вас. Например, если вы работаете на сервере EJB 3.0, вы можете использовать таймеры одиночного действия, то есть таймеры, которые вызывают событие только один раз, чтобы выполнить это. В приведенном ниже листинге 6.1 приведен простой фрагмент кода для установки таймера на отключение в зависимости от времени, полученного в сообщении. Другие технологии обеспечивают аналогичный механизм для достижения того же эффекта.
Листинг 6.1: установка события таймера для таймера на основе сообщения для установки таймера (с использованием JBOSS)
public class TimerMessage implements MessageListener {
@Resource
private MessageDrivenContext mdc;
.
.
.
public void onMessage(Message message) {
ObjectMessage msg = null;
try { #1
if (message instanceof ObjectMessage) {
msg = (ObjectMessage) message;
TimerDetailsEntity e = (TimerDetailsEntity) msg.getObject();
TimerService timerService = messageDrivenCtx.getTimerService();
// Timer createTimer(Date expiration, Serializable info) #2
Timer timer = timerService.createTimer(e.Date, e);
}
} catch (JMSException e) {
e.printStackTrace();
mdc.setRollbackOnly();
} catch (Throwable te) {
te.printStackTrace();
}
}
.
.
.
(Annotation) <#1 some vanilla code to process a message and get the interesting entity out of it >
(Annotation) <#2 Here is where we set the single action timer based on the info in the message we’ve just got>
Timer based cancellation, as described above, might be an overkill if the reservation implementation is simple. For instance the Reservation in listing 6.2 below (implemented in C#) is used by the participants discussed in the Saga and reservation sample discussed in the previous section.
Code Listing 6.2 Simple in-memory, non-persistent reservation
public Guid Reserve(Guid sagaId)
{
try
{
Rwl.TryWLock();
var isReserverd = Allocator.TryPinResource(localUri, sagaId);
if (!isReserverd) #1
return Guid.Empty;
//Some code to set the expiration #2
return sagaId; #3
}
finally
{
Rwl.ExitWLock();
}
}
(Annotation) <#1 The allocator is a resource allocation control, which manages, among other things, the capacity of the service. If we didn’t succeed in marking the service as belonging to the Saga, we can’t allocate the service to the specific Saga>
(Annotation) <#2 Here is where we need to add code to mark when the reservation expired, the previous example (6.1) used timers , we’ll try to do something different here>
(Annotation) <#3 successful reservation returns the SagaId this assures the caller that the reply it got is related to the request it sent – a simple Boolean might be confusing >
Since the Reservation in listing 6.2 does not involve heavy service resources (like, say, a database etc.), we can implement a passive handling of reservation expiration, which will be more efficient than a timer based one. Listing 6.3 below shows both a revised reservation implementation which removes timeout reservation before it commits. Note that an expired reservation can still be committed if no other reservation occurred in between or the capacity of the service is not exceeded.
Code Listing 6.3 passive reservation expiration handling (added on top of the code from listing 6.2)
public Guid Reserve(Guid sagaId)
{
try
{
Rwl.TryWLock();
RemoveExpiredReservations(); #1
var isReserverd = Allocator.TryPinResource(localUri, sagaId);
if (!isReserverd)
return Guid.Empty;
OpenReservations[sagaId] = DateTimeOffset.Now + MAX_RESERVERVATION; #2
return sagaId;
}
finally
{
Rwl.ExitWLock();
}
}
private void RemoveExpiredReservations()
{
var reftime = DateTimeOffset.Now;
var ids = from item in OpenReservations where item.Value < reftime select item.Key;
if (ids.Count() == 0) return;
var keys=ids.ToArray();
foreach (var id in keys)
{
OpenReservations.Remove(id);
Allocator.FreePinnedResources(id);
}
}
(Annotation) <#1 Added a small method (RemoveExpiredReservations which also appears in the listing) to clean expired reservations. This method is ran everytime the service needs to handle a new reservation request and it cleans up expired reservations. Note that there is no timer involved, reservation are only cleaned if there is a new reservation to process>
(Annotation) <#2 Instead of a timer the reservation is done by marking down when the reservation will expire>
The code samples above show that implementing Reservation can be simple. This doesn’t mean that other implementations can’t be more complex. For example if you want/need to persist the reservation or distribute a reservation between multiple service instances etc., but at its core it shouldn’t be a heavy or complex process.
Another implementation aspect is whether reservations are explicit or implicit. Explicit reservation means there will be a distinct “Reserve” message. This usually means there will also be a “Commit” type message and that the service or workflow engine that request the Reservation might find itself implementing a 2-phase commit type protocol, which isn’t very pleasant, to say the least.
The other alternative is implicit where the service decides internally when to reserve and what conditions to commit the reservation and when to reject it. As usual the tradeoff is between simple implementation to the service and simple implementation for the service consumer
As usual, we wrap up pattern by taking a brief look at some business drives (or scenarios) that can drive us to use the reservation pattern.
In essence, the main drive to reservation is the need for commitment from resources and since it is a complementary pattern to Sagas it also has similar quality attributes. As mentioned above Reservation helps provide partial guarantees in long running interactions thus the quality attribute that point us toward it is Integrity.
Quality Attribute (level1) |
Quality Attribute (level2) |
Sample Scenario |
Integrity |
Correctness |
Under all conditions, failure receive payment within 5 business days will cancel the order and shipping |
Integrity |
Predictability |
Under normal conditions, the chances of a customer getting billed for a cancelled order shall be less than 5% |
Table 6.2 Reservation pattern quality attributes scenarios. These are the architectural scenarios that can make us think about using the Decoupled Invocation pattern.
Reservations is a protocol level pattern which that involves Reservation involves exchange of messages between service consumers and services. The next pattern is one of the enablers of such message exchange , it is also a one of the confusing pattern since a lot of commercial offerings which include it include gazillion other capabilities — yes I am talking about the ServiceBus