Русский
Русский
English
Статистика
Реклама

Транзакции

Паттерн сага как способ обеспечения консистентности данных

18.09.2020 14:13:41 | Автор: admin
Всем привет. Уже сейчас в OTUS открывает набор в новую группу курса Highload Architect. В связи с этим я продолжаю серию своих публикаций, написанных специально для этого курса, а также приглашаю вас на свой бесплатный демо урок по теме: Индексы в MySQL: best practices и подводные камни. Записаться на вебинар можно тут.



Введение


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

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

Паттерн Сага


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

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

Типов транзакций в саге несколько, целых четыре:

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

Организовывать сагу можно с помощью хореографии или оркестрации.

В случае с хореографической саги выделенный оркестратор отсутствует. На примере сервиса заказов и пользователей она может выглядеть так: сервис заказов получает запрос и создает заказ в состоянии PENDING, а затем публикует событие Заказ создан. Обработчик событий в сервисе пользователей обрабатывает данное событие, пытается зарезервировать товар и публикует результат в виде события. Сервис заказов обрабывает данное событие, подтверждая или отменяя заказ в зависимости от прочитанного результата.

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

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

Сага позволяет добиться ACD-модели (Atomicity + Consistency + Durability в терминах ACID), но одну букву мы потеряли. Недостаток буквы I приводит к известным проблемам недостатка изолированности. К ним относятся: потерянные обновления (lost updates) одна сага перезаписывает изменения, внесенные другой, не читая их при этом, грязное чтение (dirty reads) транзакция или сага читают незавершенные обновления другой саги, нечеткое/неповторяемое чтение (fuzzy/nonrepeatable reads) два разных этапа саги читают одни и те же данные, но получают разные результаты, потому что другая сага внесла изменения. Существует ряд паттернов, позволяющих пофиксить те или иные аномалии: семантическая блокировка, коммутативные обновления, пессимистическое представление, повторное чтение значения, файл изменений и по значению. Вопрос обеспечения изоляции остается открытым.

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

Заключение


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

Подробнее..

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

27.08.2020 10:09:54 | Автор: admin
Всем привет. Уже в сентябре OTUS открывает набор в новую группу курса Highload Architect. В связи с этим я продолжаю серию своих публикаций, написанных специально для этого курса, а также приглашаю вас на свой бесплатный вебинар, в рамках которого я подробно расскажу о программе курса и формате обучения в OTUS. Записаться на вебинар можно тут.



Введение


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

Согласованность


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

Причина проблемы


Почему обеспечение согласованности затруднено именно в микросервисной архитектуре? Дело в том, что данный архитектурный стиль зачастую предполагает применение паттерна database per service. Позволю себе напомнить, что этот паттерн заключается в том, что у каждого микросервиса своя независимая база или базы (базы, потому что помимо первичного источника данных может использоваться, например, кеш). Такой подход позволяет с одной стороны не добавлять неявные связи по формату данных между микросервисами (микросервисы взаимодействуют только явно через API), с другой стороны по максимуму использовать такое преимущество микросервисной архитектуры как technology agnostic (мы можем выбирать подходящую под особую нагрузку на микросервис технологию хранения данных). Но при всем при этом мы потеряли гарантию согласованности данных. Посудите сами, монолит общался с одной большой базой, которая предоставляла возможности по обеспечению ACID транзакций. Теперь баз данных стало много, а вместо одной большой ACID транзакции у нас много небольших ACID транзакций. Нашей задачей будет объединение всех этих транзакций в одну распределенную.

Оптимистичная согласованность


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

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

Варианты обеспечения консистентности


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

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

Двухфазный коммит


Механизм предельно прост: есть некоторый transaction manager, который собственно оркестрирует транзакцию. На первом этапе (prepare) transaction manager подает соответствующую команду для resource manager'ов, по которой они в свои журналы записывают данные, которые будут закоммичены. Получив подтверждение ото всех resource manager'ов об успешном завершении первого этапа, transaction manager начинает второй этап и подает следующую команду (commit), по которой resource manager'ы применяют принятые ранее изменения.

Несмотря на кажущуюся простоту, такой подход обладает рядом недостатков. Во-первых, если хотя бы один resource manager даст сбой на втором этапе, вся транзакция должна быть отменена. Таким образом, нарушается один из принципов микросервисной архитектуры устойчивость к отказам (когда мы приходили к распределенной системе, мы сразу закладывались на то, что отказ в ней является нормой а не исключительной ситуацией). Более того, если отказов будет много (а их будет много), то процесс отмены транзакций необходимо будет автоматизировать (в том числе и писать транзакции, откатывающие транзакции). Во-вторых, сам transaction manager является единой точкой отказа. Он должен уметь транзакционно выдавать id-шники транзакциям. В-третьих, поскольку хранилищу подаются специальные команды, логично предположить, что хранилище должно уметь это делать, то есть соответствовать стандарту XA, а не все современные технологии ему соответствуют (такие брокеры как Kafka, RabbitMQ и NoSQL решения как MongoDB и Cassandra не поддерживают двухфазные коммиты).

Вывод, напрашивающийся из всех этих факторов, был отлично сформулирован Крисом Ричардсоном: 2PC not an option (двухфазный коммит не вариант).

Вывод


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



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




Читать ещё:


Подробнее..

Транзакции. Часть 1. Конспект книги Designing Data-Intensive Applications

16.05.2021 10:11:39 | Автор: admin

Эта статья является конспектом книги Designing Data-Intensive Applications.

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

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

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

Скользкая концепция транзакции

В конце 2000-х годов приобрели популярность нереляционные (NoSQL) базыданных. Их целью было улучшить существующее положение дел с реляционными БД с помощью новых моделей данных, репликациии секционирования. Транзакции оказались главной жертвой этогоновшества: многие базы нового поколения полностью от них отказались или поменяли значение термина: теперь он стал у них означать намного более слабый наборфункциональных гарантий, чем ранее.

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

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

Обеспечиваемые транзакциями гарантии функциональной безопасности частоописываются известной аббревиатурой ACID (atomicity, consistency, isolation,durability атомарность, согласованность, изоляция и сохраняемость).

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

Системы, не соответствующие критериям ACID, иногда называются BASE: как правило, доступна (BasicallyAvailable), гибкое состояние (Soft state) и конечная согласованность (Eventualconsistency). Это понятие еще более расплывчатое, чем ACID.

Давайте посмотрим на определения атомарности, согласованности, изоляции и сохраняемости. Это позволит уточнить наши представления о транзакции.

Атомарность

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

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

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

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

Согласованность

Слово согласованность ужасно перегружено.

  • Согласованность реплик и вопрос конечной согласованности, возникающий в асинхронно реплицируемых системах.

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

  • В теореме CAP слово согласованность используется для обозначения линеаризуемости (linearizability).

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

О первых трех пунктах можно подробнее прочесть в книге. Для репликации и секционирования автор книги выделил по целой главе.

К сожалению, одно и то же слово применяется как минимум в четырех различныхсмыслах.

Идея согласованности в смысле ACID состоит в том, что определенные утверждения относительно данных (инварианты) должны всегда оставаться справедливыми например, в системе бухгалтерского учета кредит всегда должен сходитьсяс дебетом по всем счетам. Если транзакция начинается при допустимом (в соответствии с этими инвариантами) состоянии базы данных и любые производимыево время транзакции операции записи сохраняют это свойство, то можно бытьуверенными, что система всегда будет удовлетворять инвариантам.

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

Изоляция

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

Рис. 1 - Состояние гонки между двумя клиентами, конкурентно увеличивающимизначение счетчикаРис. 1 - Состояние гонки между двумя клиентами, конкурентно увеличивающимизначение счетчика

Изоляция в смысле ACID означает, что конкурентно выполняемые транзакции изолированы друг от друга они не могут помешать друг другу. Классические учебники по базам данных понимают под изоляцией сериализуемость (serializability).То есть каждая транзакция выполняется так, будто она единственная во всей базе.БД гарантирует, что результат фиксации транзакций такой же, как если бы они выполнялись последовательно, хотя в реальности они могутвыполняться конкурентно.

Сохраняемость

Сохраняемость (durability) обязательство базы не терять записанных (успешно зафиксированных) транзакций данных, даже в случае сбоя аппаратного обеспечения илифатального сбоя самой БД.

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

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

Выводы по ACID

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

  • Атомарность. Если посередине последовательности операций записи происходит ошибка, то транзакцию необходимо прервать, а выполненные до тогомомента операции аннулировать.

  • Изоляция. Конкурентно выполняемые транзакции не должны мешать друг другу. Например, если одна транзакция выполняет несколько операций записи, тодругая должна видеть или все их результаты, или никакие, но не какое-то подмножество.

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

Обработка ошибок и прерывание транзакций

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

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

Хотя повторение прерванных транзакций простой и эффективный механизмобработки ошибок, он имеет недостатки:

  • Если причина ошибки в перегруженности, то повтор транзакции толькоусугубит проблему.

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

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

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

Слабые уровни изоляции

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

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

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

Автор книги не стал отдельно рассматривать уровень изоляции чтение незафиксированных данных (read uncommitted). Он предотвращает грязные операции записи,но не грязные операции чтения.

Чтение зафиксированных данных

Этот уровень изоляции обеспечивает две гарантии:

  • При чтении из БД клиент видит только зафиксированные данные (никакихгрязных операций чтения).

  • При записи в БД можно перезаписывать только зафиксированные данные (никаких грязных операций записи).

Если транзакция записала данные в базу, но еще не была зафиксирована или была прервана и другая транзакция увидела эти незафиксированные данные, то такая операция чтения называется грязной (dirty read).

Выполняемые при уровне изоляции транзакций read committed (чтение зафиксированных данных) транзакции должны предотвращать грязные операции чтения.Это значит, что любые операции записи, выполняемые транзакцией, становятсявидны другим транзакциям только после фиксации данной.

Рис. 2 - Никаких грязных операций чтения: пользователь 2 видит новое значение x только после фиксации результатов транзакции, выполняемой пользователем 1Рис. 2 - Никаких грязных операций чтения: пользователь 2 видит новое значение x только после фиксации результатов транзакции, выполняемой пользователем 1

Если более ранняя операция записи представляет собой часть еще не зафиксированной транзакции и более поздняя транзакция перезаписывает незафиксированное значение, то такая операция называется грязной операцией записи.

Чтение зафиксированных данных предотвращает казусы, например, как на рис. 3, связанные с грязной операцией записи.

Рис. 3 - В случае грязных операций записи конфликтующие операции записи различных транзакций могут оказаться перепутаныРис. 3 - В случае грязных операций записи конфликтующие операции записи различных транзакций могут оказаться перепутаны

Чтение зафиксированных данных очень популярный уровень изоляции. Он используется по умолчанию в Oracle 11g, PostgreSQL, SQL Server 2012, MemSQLи многих других базах данных.

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

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

Изоляция снимков состояния и воспроизводимое чтение

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

Однако на этом уровне изоляции все еще существует множество возможных ошибок конкурентного доступа. Например, на рис. 4 показана одна из вероятныхпроблем при чтении зафиксированных данных.

Рис. 4 - Асимметрия чтения: Алиса видит базу данных в несогласованном состоянииРис. 4 - Асимметрия чтения: Алиса видит базу данных в несогласованном состоянии

Подобная аномалия носит название невоспроизводимого чтения (nonrepeatable read)или асимметрии чтения (read skew): если Алиса прочитала бы баланс счета 1 опятьв конце транзакции, то увидела бы значение ($600), отличное от прочитанного предыдущим запросом. Асимметрия считается допустимой при изоляции уровнячтения зафиксированных данных: видимые Алисе балансы счетов были, безусловно, зафиксированы на момент их чтения.

В случае Алисы это лишь временная проблема. Однако в некоторых ситуациях подобные временные несоответствия недопустимы.

  • Резервное копирование. Резервная копия представляет собой копию всей базыданных, и ее создание на большой БД может занять несколько часов. Операциизаписи в базу продолжают выполняться во время создания резервной копии.Следовательно, может оказаться, что одни части копии содержат старые версииданных, а другие новые. В случае восстановления БД из подобной резервной копии упомянутые расхождения (например, пропавшие деньги) станут извременных постоянными.

  • Аналитические запросы и проверки целостности. Иногда приходится выполнятьзапросы, просматривающие значительные части базы данных. Они также могут быть частьюпериодической проверки целостности (мониторинга на предмет порчи данных).Если подобные запросы будут видеть разные части БД по состоянию на различные моменты времени, то их результаты будут совершенно бессмысленными.

Изоляция снимков состояния чаще всего используемое решение этой проблемы. Ее идея состоит в том, что каждая из транзакций читает данные из согласованного снимка состояния БД, то есть видит данные, которые были зафиксированыв базе на момент ее (транзакции) начала.

В других источниках данный уровень изоляции может называться повторяющимся чтением (repeatable read)

Операциичтения не требуют никаких блокировок. С точки зрения производительности основной принцип изоляции снимков состояния звучит как чтение никогда не блокирует запись, а запись чтение.

Базы данных применяют для реализации изоляции снимков состояния похожий механизм, действие которого по части предотвращения грязных операций чтения мы наблюдали на рис. 2. БД должна хранить для этого несколько различныхзафиксированных версий объекта, поскольку разным выполняемым транзакциямможет понадобиться состояние базы на различные моменты времени. Вследствиехранения одновременно нескольких версий объектов этот метод получил названиемноговерсионного управления конкурентным доступом (multiversion concurrencycontrol, MVCC).

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

Рис. 5 - Реализация изоляции снимков состоянияс помощью многоверсионных объектовРис. 5 - Реализация изоляции снимков состоянияс помощью многоверсионных объектов

В каждой строке таблицы есть поле created_by, содержащее идентификатор транзакции, вставившей эту строку в таблицу. Более того, в каждой строке таблицы естьполе deleted_by, изначально пустое. Если транзакция удаляет строку, то строка на самом деле не удаляется из базы данных, а помечается для удаления путемустановки значения этого поля в соответствии с идентификатором запросившейудаление транзакции. В дальнейшем, когда уже никакая транзакция точно не обратится к удаленным данным, процесс сборки мусора БД удалит все помеченныедля удаления строки и освободит занимаемое ими место.

Более подробно о правилах видимости для согласованных снимков состояния можно прочесть в книге.

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

Ссылки на все части

Подробнее..

Транзакции. Часть 2. Конспект книги Designing Data-Intensive Applications

23.05.2021 14:09:40 | Автор: admin

Эта статья является конспектом книги Designing Data-Intensive Applications.

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

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

Асимметрия записи и фантомы

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

Теперь представим, что Алиса и Боб два дежурных доктора на конкретной смене.Оба плохо себя чувствуют, и оба решили попросить отгул. К сожалению, они нажали на кнопку запроса отгула примерно в одно время. Происходящее после этогопоказано на рис. 1.

Рис. 1 - Пример асимметрии записи, вызванной ошибкой в коде приложенияРис. 1 - Пример асимметрии записи, вызванной ошибкой в коде приложения

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

Такая аномалия носит название асимметрии записи (write skew). Это не грязная операция записи и не потеря обновления, поскольку две наши транзакции обновляют два различных объекта. Наличие конфликта тут менее заметно, но это, безусловно, состояние гонки:если две транзакции выполнялись бы одна за другой, то второй врач не получил быотгула.

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

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

BEGIN TRANSACTION;SELECT * FROM doctorsWHERE on_call = trueAND shift_id = 1234 FOR UPDATE;UPDATE doctorsSET on_call = falseWHERE name = 'Alice'AND shift_id = 1234;COMMIT;

Предложение FOR UPDATE указывает базе установить блокировкуна все возвращенные данным запросом строки.

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

Такой эффект, при котором операция записи в одной транзакции меняет результатзапроса на поиск в другой, называется фантомом (phantom).

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

Сериализуемость

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

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

  • действительно последовательное выполнение транзакций;

  • двухфазную блокировку;

  • методы оптимистического управления конкурентным доступом, например,сериализуемую изоляцию снимков состояния (SSI).

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

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

Хотя эта идея довольно очевидна, создатели баз данных только относительнонедавно решили, что однопоточный цикл выполнения транзакцийоправдывает себя. Как случилось, что однопоточное выполнение стало считаться допустимым?

Этот пересмотр концепции был вызван двумя факторами.

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

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

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

  • Все транзакции должны быть маленькими и быстрыми, поскольку одна-единственная медленная транзакция может застопорить всю работу.

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

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

Двухфазная блокировка (2PL). В течение долгого времени в базах данных широко использовался только один алгоритм сериализуемости: двухфазная блокировка (two-phase locking, 2PL).

Обратите внимание, что, хотя название двухфазной блокировки (2PL) оченьсхоже с названием двухфазной фиксации транзакций (2PC), это две совершенно разные вещи.

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

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

Из-за столь большого количества блокировок часто встречается ситуация, когдатранзакция A ждет снятия блокировки транзакции B и наоборот. Такая ситуацияназывается взаимной блокировкой (deadlock). База данных автоматически обнаруживает взаимные блокировки между транзакциями и прерывает одну из них, чтобыостальные могли продолжить работу.

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

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

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

Сериализуемая изоляция снимков состояния (SSI). В этом конспекте мы увидели довольно мрачную картину управления конкурентнымдоступом в БД. С одной стороны, имеющиеся реализации сериализуемости или демонстрируют низкую производительность (двухфазная блокировка), илиплохо масштабируются (последовательное выполнение). С другой слабые уровниизоляции показывают высокую производительность, но подвержены различнымсостояниям гонки (потеря обновлений, асимметрия записи, фантомы и т. д.). Такчто же, сериализуемая изоляция и хорошая производительность вещи принципиально взаимоисключающие?

Очень многообещающим представляется алгоритм под названиемсериализуемая изоляция снимков состояния (serializable snapshot isolation, SSI).Он обеспечивает полную сериализуемость за счет лишь небольшого сниженияпроизводительности по сравнению с обычной изоляцией снимков состояния. SSI относительно новый метод: он был впервые описан в 2008 году.

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

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

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

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

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

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

  • выявление операций чтения устаревших версий MVCC-объектов (перед чтением произошла незафиксированная операция записи);

  • выявление операций записи, влияющих на предшествующие операции чтения(операция записи произошла после чтения).

Выявление операций чтения устаревших версий MVCC объектов. Изоляция снимков состояния обычно реализуется с помощьюMVCC.Транзакция, читающая из согласованного снимка состояния в базе данных MVCC,игнорирует все операции записи, которые были выполнены транзакциями, ещене зафиксированными на момент получения снимка состояния. На рис. 2 транзакция 43 видит, что Алиса находится на дежурстве (on_call = true), посколькутранзакция 42 не зафиксирована. Однако на моментфиксации транзакции 43 транзакция 42 уже зафиксирована. Это значит, что операция записи, проигнорированная при чтении из согласованного снимка состояния,теперь уже вступила в силу и исходные условия транзакции 43 более не соответствуют действительности.

Рис. 2 - Выявление чтения транзакцией устаревших значений из снимка состояния MVCCРис. 2 - Выявление чтения транзакцией устаревших значений из снимка состояния MVCC

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

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

Рис. 3 - Выявление при сериализуемой изоляции снимков состояния случаев модификации транзакцией данных, прочитанных другой транзакциейРис. 3 - Выявление при сериализуемой изоляции снимков состояния случаев модификации транзакцией данных, прочитанных другой транзакцией

На рис. 3 обе транзакции, 42 и 43, выполняют поиск дежурящих в смену 1234 врачей. При наличии индекса по shift_id база может воспользоваться его записью 1234, чтобы отметить факт чтения транзакциями 42 и 43 этих данных (еслииндекса нет, информацию можно отслеживать на уровне таблицы). Сама информация требуется только временно: после завершения выполнения транзакции(ее фиксации или прерывания) и завершения всех конкурентных транзакций БДможет спокойно забыть о том, какие данные читались этой транзакцией.

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

На рис. 3 транзакция 43 уведомляет транзакцию 42, что прочитанные ею в предыдущей операции чтения данные устарели и наоборот. Первой выполняется фиксация транзакции 42, и она проходит успешно: хотя операция записи транзакции 43влияет на 42, транзакция 43 пока еще не зафиксирована, поэтому данная операциязаписи еще не вступила в силу. Однако на момент фиксации транзакции 43 конфликтующая операция записи транзакции 42 уже была зафиксирована, так чтотранзакцию 43 придется прервать.

Вывод

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

Без транзакций различные сценарии ошибок (сбои процессов, разрывы сети, отключения электропитания, переполнение диска, неожиданная конкурентность и т. д.)способны привести к несогласованности данных.

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

  • Грязные операции чтения. Клиент читает записанные другим клиентомданные до их фиксации. Уровень изоляции чтения зафиксированных данныхи более сильные предотвращают грязные операции чтения.

  • Грязные операции записи. Клиент перезаписывает данные, которые другойклиент записал, но еще не зафиксировал. Практически все реализации транзакций предотвращают грязные операции записи.

  • Асимметрия чтения (невоспроизводимое чтение). Клиент видит различные части базы данных по состоянию на разные моменты времени. Чаще всего такуюпроблему предотвращают с помощью изоляции снимков состояния, при которойтранзакция читает данные из согласованного снимка состояния, соответствующего определенному моменту времени. Обычно это реализуется благодарямноговерсионному управлению конкурентным доступом (MVCC).

  • Потерянные обновления. Два клиента выполняют в конкурентном режиме циклчтения изменения записи. Один переписывает записанные другим данныебез учета внесенных им изменений, так что данные оказываются потеряны.Некоторые реализации изоляции снимков состояния предотвращают эту аномалию автоматически, а в других требуется установка блокировки вручную(SELECT FOR UPDATE).

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

  • Фантомные чтения. Транзакция читает объекты, соответствующие определенному условию поиска. Другой клиент выполняет операцию записи, котораякаким-то образом влияет на результаты этого поиска. Изоляция снимков состояния предотвращает непосредственно фантомные чтения, но фантомы в контексте асимметрии записи требуют отдельной обработки, например, блокировокпо диапазону значений индекса.

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

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

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

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

Ссылки на все части

Подробнее..

Как синхронизировать сценарий без транзакций? Штатными средствами Java

15.06.2021 02:14:19 | Автор: admin

Давайте представим, что вы параноик, и параноик вдвойне, когда дело касается многопоточности. Предположим, что вы делаете backend некого функционала приложения, а приложение переодически дергает на вашем серверы какие-то методы. Все вроде хорошо, но есть одно но. Что если ваш функционал напрямую зависит от каких-либо других данных, того же банального профиля например? Встает вопрос, как гарантировать то, что сценарий отработает именно так, как вы планировали и не будет каких-либо сюрпризов? Транзакции? Да это можно использовать, но что если Вы фантастический параноик и уже представляете как к вам на сервер летит 10 запросов к одному методу от разных клиентов и все строго в одно время. А в этот момент бизнес-логика данного метода завязана на 100500 разных данных. Как всем этим управлять? Можно просто синхронизировать метод и все. Но что если летят еще и те запросы, держать которые нет смысла? Тут уже начинаются костыли. Я пару раз уже задавался подобным вопросом, и были интересно, ведь задача до абсурда простая и повседневная (если вы заботитесь о том, чтобы не было логических багов конечно же :). Сегодня решил подумать, как это можно очень просто и без костылей реализовать. И решение вышло буквально на 100 строк кода.

Немного наглядного примера

Давайте предположим, что есть водитель и есть пассажир. Водитель не может менять машину до тех пор, пока клиент, например подтверждает поездку. Это что получается, клиент соглашался на поездку с одними характеристиками машины, а по факту у водителя другая машина? Не дела! Можно организовать что-то подобное:

String result = l.lock(new ArrayList<Locker.Item>() {{    add(new Locker.Item(SimpleType.TRIP, 1));    add(new Locker.Item(SimpleType.USER, 2));}}, () -> {    // Тут выполняем отмену поездки и держим водителя на привязи    // Кстати если кто-то где-то вызовет USER=2 (водитель), то он также будет ждать    // ну или кто-то обратится к поездке TRIP=1    // А если обратится к USER=3, то уже все будет нормально :)    // так как никто не блокировал третьего пользователя :)    return "Тут любой результат :)";    });

Элегантно и просто! :)

Исходники тут - https://github.com/GRIDMI/GRIDMI.Sync

Камнями не бросаться! :)

Подробнее..

PostgreSQL Antipatterns накручиваем себе проблемы

28.06.2020 20:20:26 | Автор: admin
Некоторые ситуации в работе PostgreSQL кажутся неочевидными, пока не попытаешься детально понять, почему это работает так. Из-за незнания таких особенностей иногда разработчик сам провоцирует проблемы для нормальной работы своего приложения в будущем.

Сегодня разберем пару примеров, как неудачная организация БД и кода могут превратить наше приложение в клубок проблем:

  • накрутка serial при ON CONFLICT
  • накрутка счетчика транзакций


Накрутка serial при ON CONFLICT


Давайте представим, что нам понадобилась небольшая таблица-словарь на пару десятков тысяч записей что-то вроде списка форматов ошибок PostgreSQL.

Наше приложение (или все-таки разработчики?) любит суррогатные ключи, поэтому сразу добавим в таблицу в качестве PRIMARY KEY автоинкремент-поле с типом serial. Точнее, smallserial ведь мы точно знаем, что строк будет не больше 215:

CREATE TABLE tbl(  pk    smallserial      PRIMARY KEY, val    integer      UNIQUE);

Пытаться вставлять данные в него мы иногда будем, но новых среди них будет немного. Поэтому для удобства вставки, чтобы не заниматься обработкой исключений уникальности в своем коде, воспользуемся появившимся с версии 9.5 функционалом INSERT ... ON CONFLICT ...:

INSERT INTO tbl(val) VALUES(1) ON CONFLICT DO NOTHING;-- 1 строкаINSERT INTO tbl(val) VALUES(1) ON CONFLICT DO NOTHING;-- 0 строк, и никаких ошибок!

Теперь оставим наше приложение спокойно работать, и Вот ровно с такой ошибкой к нам и прибегут через несколько дней или недель:

ERROR:  nextval: reached maximum value of sequence "tbl_pk_seq" (32767)

И потом начнут приходить все чаще и чаще. Но как появилось столько записей? Почему не сработал ON CONFLICT?

Дело в том, что они и не появились. Давайте еще раз с нуля посмотрим на происходящее в нашей таблице:

TRUNCATE TABLE tbl RESTART IDENTITY;INSERT INTO tbl(val) VALUES(1) ON CONFLICT DO NOTHING RETURNING *;-- 1 строка: pk = 1, val = 1INSERT INTO tbl(val) VALUES(1) ON CONFLICT DO NOTHING RETURNING *;-- 0 строкINSERT INTO tbl(val) VALUES(2) ON CONFLICT DO NOTHING RETURNING *;-- 1 строка: pk = 3, val = 2

Как так pk = 3, ведь вставилось всего 2 строки? Мы запутались


На самом-то деле, все объясняется просто. Посмотрим внимательно, что из себя представляет наша таблица:

_tmp=# \d tbl                             Table "public.tbl" Column |   Type   | Collation | Nullable |             Default--------+----------+-----------+----------+--------------------------------- pk     | smallint |           | not null | nextval('tbl_pk_seq'::regclass) val    | integer  |           |          |Indexes:    "tbl_pkey" PRIMARY KEY, btree (pk)    "tbl_val_key" UNIQUE CONSTRAINT, btree (val)

Типы данных smallserial, serial и bigserial не являются настоящими типами, а представляют собой просто удобное средство для создания столбцов с уникальными идентификаторами (подобное свойству AUTO_INCREMENT в некоторых СУБД). В текущей реализации запись:

CREATE TABLE имя_таблицы (    имя_столбца SERIAL);

равнозначна следующим командам:

CREATE SEQUENCE имя_таблицы_имя_столбца_seq AS integer;CREATE TABLE имя_таблицы (    имя_столбца integer NOT NULL DEFAULT nextval('имя_таблицы_имя_столбца_seq'));ALTER SEQUENCE имя_таблицы_имя_столбца_seq OWNED BY имя_таблицы.имя_столбца;

То есть при определении такого типа создаётся целочисленный столбец со значением по умолчанию, извлекаемым из генератора последовательности.
То есть наш smallserial превратился в тыкву в поле smallint с DEFAULT-значением из последовательности tbl_pk_seq.

А последовательность штука нетранзакционная:
Значение, выделенное из последовательности, считается задействованным, даже если строку с этим значением не удалось вставить в таблицу. Это может произойти, например, при откате транзакции, добавляющей данные.
То есть мы сначала сгенерировали DEFAULT-значение, использовали значение pk = 2, а потом его не вставили в таблицу из-за конфликта уникальности val, скрыв проблему с помощью ON CONFLICT DO NOTHING. И после очередной такой попытки у нас просто кончилась последовательность.

Что делать?


  • хорошо
    Стараться не использовать лишние суррогатные ключи в таблицах, где уникальный ключ и так уже есть.
  • просто
    Сконвертировать поле и вместо smallserial использовать serial или bigserial это позволит продлить агонию приложения на месяцы или даже годы.
  • разумно
    Не использовать serial и ON CONFLICT на таблицах с ожидаемо существенным количеством конфликтующих вставок.
  • странно
    Написать триггер INSTEAD OF для аналогичного по структуре VIEW (или можно хранимую процедуру, но мы ведь не ищем легких путей).

Давайте в научно-познавательных целях попробуем собрать последний вариант:

CREATE TABLE tbl(  pk    smallserial      PRIMARY KEY, val    integer      UNIQUE);-- отвязываем DEFAULTALTER TABLE tbl ALTER COLUMN pk DROP DEFAULT;-- создаем "промежуточное" VIEWCREATE VIEW _tbl AS TABLE tbl;CREATE OR REPLACE FUNCTION tbl_serial() RETURNS trigger AS $$BEGIN  IF NEW.pk IS NULL THEN    LOOP -- эмуляция UPSERT через цикл      PERFORM 1 FROM tbl WHERE val = NEW.val;      EXIT WHEN FOUND; -- выходим при наличии такого значения в словаре      BEGIN        NEW.pk = nextval(pg_get_serial_sequence('tbl', 'pk'));        INSERT INTO tbl VALUES(NEW.*);        RETURN NEW;      EXCEPTION        WHEN unique_violation THEN -- защита от конкурентной вставки      END;    END LOOP;  END IF;  RETURN NULL;END;$$ LANGUAGE plpgsql;-- триггер INSTEAD OF выполняется "вместо" заказанной операции над VIEWCREATE TRIGGER serial INSTEAD OF INSERT ON _tbl  FOR EACH ROW    EXECUTE PROCEDURE tbl_serial();

Обратите внимание, что дальнейшие вставки мы производим как бы во VIEW:

INSERT INTO _tbl(val) VALUES(1) ON CONFLICT DO NOTHING RETURNING *;-- 1 строка: pk = 1, val = 1INSERT INTO _tbl(val) VALUES(1) ON CONFLICT DO NOTHING RETURNING *;-- 0 строкINSERT INTO _tbl(val) VALUES(2) ON CONFLICT DO NOTHING RETURNING *;-- 1 строка: pk = 2, val = 2

Ура! Получили ровно то, что хотели, хоть и весьма нетривиально. Поэтому все получилось аккуратно, но котик несколько насторожен.



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

Накрутка счетчика транзакций


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

Давайте установим некоторое предельное значение времени ожидания, на которое мы готовы пойти:

SET statement_timeout = '1s';

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

BEGIN TRANSACTION;  INSERT INTO _tbl(val) SELECT 1 FROM pg_sleep(0.1); -- эмулируем задержку  SAVEPOINT sp1;  INSERT INTO _tbl(val) SELECT 2 FROM pg_sleep(0.6);  SAVEPOINT sp2;  INSERT INTO _tbl(val) SELECT 3 FROM pg_sleep(1.1);  -- ERROR:  canceling statement due to statement timeout  ROLLBACK TO SAVEPOINT sp2;COMMIT TRANSACTION;

Давайте проверим, что первые две записи успешно сохранились в нашей таблице, несмотря на возникновение ошибки в ходе выполнения транзакции:

_tmp=# SELECT xmin, * FROM tbl;   xmin    | pk | val-----------+----+----- 926944639 |  1 |   1 926944641 |  2 |   2(2 rows)

Вот только у наших записей оказался разный идентификатор создавшей транзакции он увеличивается с каждым вызовом SAVEPOINT.
Для детального понимания внутренней механики работы транзакций, субтранзакций и 2PC в PostgreSQL рекомендую ознакомиться со статьей Transactions in PostgreSQL and their mechanism от Movead Li.
На практике такая ситуация приводит к тому, что autovacuum: VACUUM ... (to prevent wraparound) мы будем видеть очень и очень часто, а если ресурсы сервера не резиновые это может стать проблемой.

Что делать?


  • не можем позволить себе ждать завершения операции бесконечно
  • не хотим терять весь прогресс транзакции
  • не должны неоправданно накручивать счетчик транзакций

Единственный приемлемый вариант лавировать между Сциллой и Харибдой.

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

Ну как, стало немного полегче?..

Подробнее..

Recovery mode Мои пожелания к СУБД будущего, а также к Росреестру в части транзакционности

07.08.2020 08:23:27 | Автор: admin

Клиент взаимодействует с базой данных.
С сайта http://corchaosis.ru, автор картины Jonathan Tiong.

Помимо того, что я являюсь программистом (преимущественно, это Delphi + всякие разные СУБД, в последнее время ОРАКЛ, + немного PHP), у меня есть хобби это купля и продажа квартир. Я покупаю квартиру на этапе строительства от более менее надёжного застройщика по вкусной цене (например, сейчас таким застройщиком является Самолёт, квартиры возле м. Некрасовка продаются), дожидаюсь сдачи дома (часто на два года позже, с недорогими предложениями такое случается), делаю в ней ремонт и затем продаю за 95-100% её рыночной цены.

Так вот, я (как и все) столкнулся с проблемой отсутствия у РосРеестра транзакционности.

Проблема отсутствия у Росреестра транзакционности сделок

В программировании Транзакция, а в недвижимости это Сделка с альтернативой (а также, как её часть, Договор о банковской ячейке), и там всё немного более сложно. Рассказываю.

Вася пришёл на просмотр квартиры, которую продаёт Петя. И Васе всё очень понравилось, в том числе и цена, но у Васи денег нет. Так начинается наша история.

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

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

Два эльфа Средиземья, Маглор и Маэдрос, обладают подходящей (критериям Пети) недвижимостью в городе Валинор, которую срочно распродают, так как отправляются служить Мелькору. На языке риэлторов это называется Свободная продажа.

Итак, Вася находит клиента Серёжу. Теперь, Петя находит два подходящих ему варианта в городе Валинор. Выходим на оформление сделки. Допустим для простоты, что никто из участников сделки не использует ипотеку и не имеет долевым собственником несовершеннолетних. Таким образом, теперь должны совершиться следующие действия:
1. Серёжа передаёт деньги Пете.
2. Вася передаёт свою квартиру Серёже.
3. Петя передаёт свою квартиру Васе.
4. Или Маглор, или Маэдрос, передают свою квартиру в Валиноре Пете и получают деньги Серёжи.
5. Малкор и Маэдрос идут в Мордор служить Мелькору.

Идеально было бы передать в Росреестр на выполнение следующий скрипт:
START TRANSACTION
Квартиру Васи отдать Серёже.
Квартиру Пети отдать Васе.
begin
Квартиру Малкора отдать Пете
Деньги Серёжи отдать Малкору
ЕСЛИ_ОШИБКА:
Квартиру Маэдроса отдать Пете
Деньги Серёжи отдать Маэдросу
end
COMMIT TRANSACTION

Это упрощённый скрипт сделки с альтернативой, предполагающий, что у всех квартир один взрослый (и дееспособный) собственник, что их стоимости равны, и что оплата риэлторов (если они есть) оплачивается вне привязки к этапам сделки.

Однако, Росреестр не поддерживает транзакционность. Все действия будут выполняться последовательно и независимо, друг за другом, без отката транзакции в целом если не выполнилось одно из них. Максимум, что можно достичь учитывая, что Росреестр и МФЦ не работают с передачей наличных средств это заложить деньги в банковскую ячейку, с условиями доступа к ним Васи, Пети, Серёжи (если вообще никакая сделка не зарегистрирована), и иных действующих лиц, по факту предъявления ими зарегистрированных Росреестром договоров. (И кстати, банки самостоятельно проверку подлинности договоров не осуществляют, то есть доверяют подлинности бумаг участников сделки).

Кроме рисков неполного выполнения транзакции, другая проблема в том, что если другие участники могут въехать в своё новое жильё не дожидаясь полного оформления (привет, вопрос недоплаты коммунальных платежей!), то Маглор и Маэдрос нескоро отправятся служить Мелькору, и возможно, Маглор не сможет подержать в своих руках сильмариллы, он просто не успеет. Сделки с недвижимостью выполняются последовательно, и оформление каждой сделки будет длиться не менее чем 9 рабочих дней.

Кроме этого, Росреестр не поддерживает обременение строящегося по ДДУ жилья, а мог бы, это элементарное действие в отношении простого фьючеса.

Теперь перейдём к недостаткам и моим хотелкам про СУБД

1) Первое это отсутствие системы контроля версий. Если со стороны Delphi я веду разработку в своей песочнице, и сделанные мной изменения не появятся у других программистов до момента их коммита, то с СУБД не так. И даже если мне доверяют полный (по крайней мере, в рамках нужного для поставленной передо мной задачи) доступ к боевой БД, а такое случается, я не могу на ней разрабатывать. Пока я буду отлаживаться, всё рухнет. Это что за каменный век??? Сделайте песочницу разработчикам.

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

3) Третье и тут я воспользуюсь терминологией Оракла отсутствует возможность вызвать простой скрипт Insert или Update, использующий Returning, так, как мы вызываем Select. Возможно, это не проблемы Оракла, а проблемы стыка Delphi + Oracle.

4) Четвёртое необходимость назначения создаваемым мной процедурам и функциям полномочий там, где я делать этого не хочу. Я не хочу задавать, а потом менять, полномочия пользователей процедуре и функции. Почему, если я явно не написал Grant-ы, система не могла бы сама посмотреть на задействованные объекты, и в соответствии с правами на действия с ними наделять или нет тех или иных пользователей правом на вызов функции? Я готов написать для этого при написании функций и процедур одно ключевое слово. Или, ещё лучше, пусть пользователь начнёт выполнение, а если ветка алгоритма приведёт его к запросу на который у пользователя нет прав, то выкинет с ошибкой.
Подробнее..

Скрытые особенности Биткоина

05.04.2021 14:09:20 | Автор: admin

Обзор неочевидных для многих отличий Биткоина от существующих платежных систем

Ранее в статье Биткоин принципиально новая монетарная система речь шла о том, что любая денежная (монетарная) система базируется на трех основных составляющих денежной массе, денежных транзакциях и владении деньгами. В отличие от управляемой и монополизированной государством традиционной монетарной системы, основанной на фиатных (фидуциарных) деньгах, Биткоин является децентрализованной денежной системой и управляется правилами (протоколом), зашитыми в его программный код. Это в корне меняет три основных составляющих, лежащих в основе этой системы деньги эмитирует программный код, а не государство; транзакции между контрагентами осуществляются напрямую, без посредников; распоряжаться деньгами на биткоин-адресах может только владелец их приватных ключей, никто другой не может получить к ним доступ.

Но помимо этого Биткоин, как платежная система, имеет ряд других уникальных особенностей о которых либо не знают, либо не придают им значения.

1. В Биткоин-сети нет аккаунтов.

В отличие от других платежных систем, у пользователей Биткоина нет своего аккаунта, т.е. счета, который был бы каким-то образом привязан к пользователю. Клиенты банков имеют аккаунты в виде банковских счетов, идентифицируемых по паспорту и/или налоговому номеру клиента. Банковские карты платежных сетей Visa и MasterCard также связаны с аккаунтами их пользователей. Клиенты электронной платежной сети PayPal имеют аккаунты, связанные с адресом их электронной почты. Во всех других платежных системах существуют какие-то способы идентификации личности и её счетов. В Биткоине этого нет!

С одной стороны это делает пользование Биткоином псевдонимным. С другой в случае утраты приватных ключей восстановить доступ к биткоин-адресам невозможно. В Биткоине только приватный ключ дает право пользования деньгами, хранящимися на соответствующем ему биткоин-адресе. Нет ключа нет биткоинов! Или, как говорится в популярной среди биткоинеров поговорке Not your keys, not your coins (Не твои ключи, не твои биткоины).

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

Отчасти эта проблема решается сторонними сервисами верхнего уровня, которые предоставляют доступ к аккаунту клиента и через него к приватным ключам, которые хранятся на серверах этих сервисов. Это делается за счет утраты анонимности пользователь должен указать свои реквизиты для восстановления доступа к аккаунту, как правило, e-mail и/или номер телефона. А по требованиям KYC/AML и свои личные данные имя и фамилию, адрес проживания и т.п.

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

2. В биткоин-кошельках не хранятся биткоины.

Эта парадоксальная фраза сбивает с толку многих начинающих (и не только) пользователей Биткоина. Но в действительности это так! В Биткоин кошельках хранятся приватные ключи от биткоин-адресов, а не сами монеты. Более того, см. п.3

3. Биткоины не существуют как сущности.

Звучит, как каламбур, но это так! Ранее, в п.1, для простоты понимания я написал, что биткоины хранятся на биткоин-адресе. В действительности это не совсем так. Биткоин (монета) не является объектом, даже цифровым. Его невозможно представить в виде набора данных. И, поскольку это не объект, его невозможно хранить в том понимании, которое мы обычно вкладываем в понятие хранение. Биткоин (монета) существует только в контексте транзакции. Да и сами транзакции в Биткоин-сети отличаются от банковских записей в бухгалтерских книгах. И это следующая особенность Биткоин-сети , которая может многих окончательно запутать.

4. Биткоины не отправляются ни на адреса, ни с адресов.

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

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

Ниже на картинке приведен пример простой транзакции. У отправителя (его принято называть Элис/Alice) на неизрасходованном выходе (UTXO) ранней транзакции есть 10 BTC. Элис переводит 8 BTC получателю (принято называть Бобом/Bob), а остаток (сдача) в размере 2 BTC возвращается на биткоин-адрес отправителя (Элис). При этом выход с 10 BTC становится израсходованным и создаются два новых неизрасходованных выхода (UTXO) c 8 BTC и 2 BTC. Но первый может теперь разблокировать только Боб, а второй только Элис.

Пример транзакции в Биткоине.

Из этого следует, что баланс любого биткоин-адреса это сумма всех неизрасходованных выходов транзакций (UTXO), которые заблокированы публичным ключом владельца этого адреса.

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

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

5. Биткоины никогда не бывают в пути.

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

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

А как же двойная трата, спросите вы? Да, действительно, если я отправляю средства на другой адрес, а они сразу с моего адреса не списываются, значит я могу снова отправить их на другой адрес. Одними и теми же монетами купить пиццу и пиво к ней!

На самом деле, большинство программных кошельков это не позволяют делать. Но никто не мешает это сделать напрямую, послав подписанное распоряжение о переводе средств в Биткоин-сеть. Что же произойдет в этом случае? Да всё просто! Одна из двух транзакций не пройдет. И не обязательно это будет вторая. Тут возможны два случая:

  1. Обе транзакции попали в один мемпул. Майнер пропустит одну и забракует по нехватке средств вторую.

  2. Транзакции попали в разные мемпулы (к разным майнерам). В блокчейн будет записан блок только с одной транзакцией. После этого вторая транзакция станет невалидной.

Так что, или пицца, или пиво! :)

P.S. Есть еще и другие особенности Биткоина, но об этом в следующей статье.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru