Google Analytics

среда, 20 июля 2011 г.

Особенности паттернов параллельного программирования для бизнес приложений [PoEAA]

Параллельное программирование давно уже перестало быть уделом матёрых гуру. Трудно представить современное приложение, реализация которого игнорировала бы этот вопрос. Аналогично трудно представить прикладного программиста, который бы не держал на вооружении паттернов корпоративных приложений от Фаулера с компанией. Собственно об особенностях присущих этим паттернам и пойдёт речь в этом посте. Материал представленный далее почти не коррелирует с технологиями реализации, поэтому может быть интересен любым прикладным программистам не смотря на то, что примеры базируются на Java и PostgreSQL. И ещё одно замечание, чтобы не вносить путаницу с названиями паттернов, я буду использовать их оригинальные названия на английском языке.
Поведение реализаций Offline Lock паттернов при различных уровнях одновременного доступа [Optimistic Offline Lock, Pessimistic Offline Lock]

В этом пункте обсуждается поведение при различных уровнях одновременного доступа для двух видов контроля оптимистического и пессимистического на базе реализаций паттернов Offline Lock. Поведение будет рассматриваться по результатам синтетического теста производительности. Собственно производительность отнюдь не всегда является проблемой для программного обеспечения. Всё зависит от конкретной ситуации. Например в системе, где скорость проведения операций ограничена скоростью пользовательского ввода, маловероятно, что возникнут проблемы с недостаточной производительностью этих операций. И наоборот, в системе, где скорость выполнения операций ограничена только возможностями аппаратного обеспечения и реализацией, недостаточная производительность может являться большой проблемой. 

Приступим к измерению производительности оптимистического и пессимистического видов контроля одновременного доступа для реализаций паттернов Optimistic Offline Lock и Pessimistic Offline Lock при различных уровнях параллельного доступа. Для этого измерения понадобится таблица с данными на которых мы будем получать тестовые результаты. Схема таблицы и тестовые данные представлены ниже:
CREATE TABLE values
(
    "id" integer PRIMARY KEY,
    "field1" varchar(4096),
    "field2" decimal(20, 2),
    "locked" boolean,
    "version" integer DEFAULT 0
);

INSERT INTO values 
(
    "id", "field1", "field2", "locked", "version"
)
VALUES 
(
    1,'record 1', 0.00, false, 0
);

Рассмотрим реализацию тестового приложения на базе которого будут проводиться измерения. В реализации используются Spring Framework и Hibernate технологии являющиеся одними из самых распространенных в мире Java. Использование данных фреймворком позволило сделать код теста максимально лаконичным и понятным. Первым делом рассмотрим класс сущности (отражает строку таблицы values):
@Entity
@Table(name="values", schema="public")
public class Value {

    @Id
    private Integer id;
    private String field1;
    private BigDecimal field2;
    private Boolean locked;
    private Integer version;

    //геттеры и сеттеры
}
Реализация каждого из паттернов Offline Lock состоит из двух классов DAO и Task. В DAO реализована вся логика доступа к БД и проверки блокировок. В Task код для изменения данных с учётом типа контроля управления одновременного доступа и специфичный код для обеспечения тестирования. Замечание 1: Pessimistic Offline Lock реализован без использования паттерна Lock Manager с целью обеспечения максимального сходства реализаций двух паттернов (Pessimsitic и Optimistic). Замечание 2: при реализации паттерна Optimistic Offline Lock использовалось ручное управление версиями опять же по причинам сходства, в реальном приложении предпочтительней использовать встроенный механизм Hibernate.

Код реализации Optimistic Offline Lock
@Repository("optimisticDao")
@Scope("prototype")
public class ValueDaoWithOptimisticControl implements ValueDao {

    //прочие атрибуты и методы

    @Transactional
    @Override
    public Value loadValue(Integer id) {
        //Загрузка сущности из БД 
        Value value =
            (Value) sessionFactory.getCurrentSession().get(
                Value.class, 
                id
                );

        return value;
    }

    @Transactional
    @Override.html
    public void storeValue(Value value) {
        //Загрузка сущности из БД с захватом блокировки на текущую транзакцию
        Value oldValue =
            (Value) sessionFactory.getCurrentSession().get(
                Value.class, 
                value.getId(), 
                new LockOptions(LockMode.PESSIMISTIC_WRITE)
                );
        
        if (!value.getVersion().equals(oldValue.getVersion()))  
            throw new OptimisticLockingFailureException("Value was modified");
                
        oldValue.setField1(value.getField1());
        oldValue.setField2(value.getField2());
        oldValue.setVersion(value.getVersion() + 1);        
    }

}
@Component("optimisticTest1")
@Scope("prototype")
public class ValueTaskWithOptimisticControl implements ValueTask {
    
    //прочие атрибуты и методы

    @Override
    public void run() {
        try {
            startLatch.await();
        } catch (InterruptedException e2) {
            Thread.currentThread().interrupt();
        }
        
        while (!Thread.currentThread().isInterrupted()) {
            Value value = null;
            //Загрузка значения
            try {
                value = valueDao.loadValue(id);
            }
            catch (NestedRuntimeException e) {
                continue;
            }
            
            value.setField2(value.getField2().add(BigDecimal.ONE));
            
            //Сохранение значения и проверка блокировки
            try {
                valueDao.storeValue(value);
                countOfChanges++;
            }
            catch (NestedRuntimeException e) {
                //Версия изменилась
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e1) {
                    Thread.currentThread().interrupt();
                    break;
                }
                continue;
            }            
        }
        
        finishLatch.countDown();
    }

}

Код реализации Pessimsitic Offline Lock
@Repository("pessimisticDao")
@Scope("prototype")
public class ValueDaoWithPessimisticControl implements ValueDao {

    //прочие атрибуты и методы

    @Transactional
    @Override
    public Value loadValue(Integer id) {
        //Загрузка сущности из БД с захватом блокировки на текущую транзакцию
        Value value =
            (Value) sessionFactory.getCurrentSession().get(
                Value.class, 
                id, 
                new LockOptions(LockMode.PESSIMISTIC_WRITE)
                );
        
        if (value.isLocked()) 
            throw new PessimisticLockingFailureException("Value have already locked");
            
        value.setLocked(true);
        
        return value;
    }

    @Transactional
    @Override
    public void storeValue(Value value) {
        //Загрузка сущности из БД с захватом блокировки на текущую транзакцию
        Value oldValue =
            (Value) sessionFactory.getCurrentSession().get(
                Value.class, 
                value.getId(), 
                new LockOptions(LockMode.PESSIMISTIC_WRITE)
                );
        
        if (!oldValue.isLocked()) 
            throw new PessimisticLockingFailureException("Value have not locked");
        
        oldValue.setField1(value.getField1());
        oldValue.setField2(value.getField2());
        oldValue.setLocked(false);
        
    }
    
}

@Component("pessimisticTest1")
@Scope("prototype")
public class ValueTaskWithPessimisticControl implements ValueTask {

    //прочие атрибуты и методы

    @Override
    public void run() {
        try {
            startLatch.await();
        } catch (InterruptedException e2) {
            Thread.currentThread().interrupt();
        }
        
        while (!Thread.currentThread().isInterrupted()) {
            Value value = null;
            //Загрузка значения с захватом блокировки.
            //В случае конфликта попытка повторяется через промежуток времени.
            while (value == null) {
                try {
                    value = valueDao.loadValue(id);
                }
                catch (NestedRuntimeException e) {
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e1) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            }
            
            if (value == null) continue;
            
            value.setField2(value.getField2().add(BigDecimal.ONE));
            
            try {
                valueDao.storeValue(value);
                countOfChanges++;
            }
            catch (NestedRuntimeException e) {                
                continue;
            }            
        }
        
        finishLatch.countDown();
    }

}
Все кто интересуется полным кодом теста могут скачать архив и поэкспериментировать самостоятельно. Для сборки проекта используется maven 3. Запустить тест можно следующими командами из директории с проектом:
export MAVEN_OPTS=-server
mvn exec:java -Dexec.mainClass="concurrency.patterns.Application" \
  -Dexec.args="-time ВРЕМЯ_РАБОТЫ_В_МИЛИСЕКУНДАХ \
  -concurrency_rate КОЛИЧЕСТВО_ПОТОКОВ -id ИДЕНТИФИКАТОР_СТРОКИ -action ТИП_ТЕСТА"
Результаты выполнения тестов с разными уровнями одновременного доступа (даны для интервала выполнения 10 секунд) представлены на графике ниже. Измерения производились на 4-х ядерном cpu.

Признаться честно, для меня данный результат стал немного неожиданным. Я рассчитывал на гораздо меньшую степень деградации Pessimistic Offline Lock. По данным результатам видно, что оба подхода имеют сравнимый уровень деградации. При первой реализации теста, по недосмотру, я загружал данные в классе ValueDaoWithOptimisticControl с захватом блокировки, что приводило к гораздо большей деградации реализации Optimistic Offline Lock. В итоговой реализации же Pessimistic Offline Lock имеет более крутой спад за счёт большего количества запросов к БД на выполнение действия. При пессимистичном виде контроля одновременного доступа выполняются 4 запроса к БД (2 select for update и 2 update установка и снятие блокировки), при оптимистическом 3 запроса (1 select, 2 select for update и update). Собственно вся разница в производительности обуславливается отличием в запросах, потому как именно на них уходит основное время выполнения. По сути оба подхода обеспечивают сериализацию выполнения действия над строкой в БД и поэтому итоговый результат приблизительно равен времени выполнения теста делённого на время выполнения одного действия. Главное отличие в поведении заключается в выполнение действия без гарантии того, что результат будет сохранен. Другими словами необязательное потребление ресурсов. Такое поведение свойственно оптимистическому виду контроля. Это плата за отсутствие нужды в освобождении занятых ресурсов при аварийном завершении.

Представленная выше реализация паттернов не является самой производительной, выжать из неё больше можно сведя загрузку и сохранения к двум update-запросам для пессимистического подхода и к select-запросу c update-запросом для оптимистического. Данная оптимизация не будет иметь принципиального значения в нашем контексте.

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

Захваченные ресурсы при аварийном завершении [Pessimistic Offline Lock]

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

Сброс блокировок по событи

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

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

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

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

Не использовать пессимистичный подход

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

Применение паттернов параллельного программирования PoEAA

Паттерны блокировок являются средством контроля одновременного доступа на протяжении бизнес-транзакции. Бизнес-транзакция может распространяться на несколько системных транзакций, что не позволяет использовать механизмы синхронизации СУБД с областью действия ограниченной системной транзакцией. К тому же длинные системные транзакции и долгие захваты блокировок способствуют появлению проблем с доступностью БД для обслуживания. Вопросы производительности в данном случае не являются проблемой, потому как они в большей степени зависят от политики использования блокировок, а не от конкретных механизмов реализации.

Общее предназначение паттернов параллельного программирования PoEAA - это решения проблем одновременного доступа на уровне бизнес слоя. То есть построение отдельного механизма синхронизации. Не смотря на это для их корректной реализации необходимо понимать низкоуровневые механизмы которые предоставляют СУБД: блокировки, изоляция транзакций.

Дополнительные источники

Архив с кодом тестового приложения
Pessimistic Offline Lock
Optimistic Offline Lock


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

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