Как стать автором
Обновить

Комментарии 23

А как обеспечивается согласованность между Реляционной БД и индексом? Допустим, если транзакция в БД закоммитилась, а Solr оказался недоступен по каким-то причинам?
Это обеспечивается доктриной, тем как вызываются EntityListener. Если взглянуть на \Doctrine\ORM\UnitOfWork::commit:
        ...
        try {
 	    ...
            $this->executeInserts($class);
	    ...

            $this->executeUpdates($class);
            ...

            $conn->commit();
        } catch (Exception $e) {
            $this->em->close();
            $conn->rollback();

            throw $e;
        }

в рамках вызова executeInserts и executeUpdates происходит вызов EntityListener который запускает индексацию. В случае если solr недоступен будет выброшен эксепшн "\Solarium\Exception\HttpException" и выполнен код в секции catch и изменения в базе откатятся через rollback
Хорошо, а если в Solr все запишется удачно, а транзакция откатится, например будет выброшено исключение в commit()?
Да это может случиться, предлагаю заглянуть в \Doctrine\DBAL\Connection::commit (у меня версия doctrine/dbal = v2.5.13):
     /*
     * @throws \Doctrine\DBAL\ConnectionException If the commit failed due to no active transaction or
     *                                            because the transaction was marked for rollback only.
     */
    public function commit()
    {
	...
	if ($this->_transactionNestingLevel == 0) {
	    throw ConnectionException::noActiveTransaction();
	}
	if ($this->_isRollbackOnly) {
	    throw ConnectionException::commitFailedRollbackOnly();
	}
	...
     }


Соотв. эксепшн может быть в ситуации когда «this->_transactionNestingLevel == 0» это возможно если не был вызван $connection->beginTransaction() перед $connection->commit(). Но т.к. индексация происходит в рамках $em->flush() то за вызов "$connection->beginTransaction()" отвечает UnitOfWork, соответственно тут мы в безопасности.

Второй случай когда может возникнуть эксепшн это "$this->_isRollbackOnly == true". Выполнение этого условия возможно в след. ситуациях (не тестировал, исхожу из чтения кода):
1. Когда был вызван явно \Doctrine\DBAL\Connection::setRollbackOnly для текущей транзакции до \Doctrine\DBAL\Connection::commit
2. Либо был вызван \Doctrine\DBAL\Connection::rollBack для вложенной транзакции до \Doctrine\DBAL\Connection::commit

В обоих случаях если работа идет стандартым способом через $em->flush() оба вышеописанных случая исключены. Имхо эти эксепшены могут возникнуть если разработчик явно использует beginTranscation и endTransaction и допускает ошибку например, забыв где то вызвать beginTranscation что в принципе отслеживается практически сразу либо использует вложенные транзакции, но тут уж он должен понимать тогда что он делает и к чему это приведет.

Более опасный источник десинхронизации на самом деле не в \Doctrine\DBAL\Connection::commit а в других EntityListener которые могут вызываться для индексируемой сущности. Если после успешной индексации в каком-либо другом EntityListener будет неотлавливаемый эксепшн то получим кейс что индексация в солр прошла но изменения в базу не записались. Тут могу только порекомендовать разработчику иметь это ввиду и не злоупотреблять EntityListener либо корректно обрабатывать ошибки в других EntityListener.

В итоге рекомендую на каждую измененную сущность делать свой $em->flush($entity) чтобы избежать десинхронизации и не злоупотреблять EntityListener. Как и любой фреймворк/библиотека DoctrineSolrBundle может использоваться неправильно или неоптимально что может привести к ошибке. Если у вас реализуется сложная логика в EntityListener или вы используется вложенные, «ручные» или еще как то усложненные транзакции то возможно DoctrineSolrBundle не будет вам удобен. Возможно в будущей версии я сделаю чтобы можно было отключить автоиндексацию при каждом flush и возможность вызвать индексацию вручную.

Еще рассинхронизация может произойти при останове/перезапуске веб-сервера, отключении питания, сбоях в работе сети (плдключения к БД, не удалось запрос COMMIT отравить). Во всеъ этих случаях подход, используемый в бандле может привести к рассинхронизации. И дело даже не в том, что что-то не так запрограммировано — дело в принципе, положенном в основу бандла. Для отказоустойчивой синхронизации между БД и индексом нужен двухфаный коммит.
Как вы себе представляете двухфазный коммит между postgresql и solr? Т.е. вы можете сделать например двухфазный коммит между двумя postgresql базами данных. Солр тут надо рассматривать как third-party api. Между такими разными продуктами это имхо надо городить такой свой огород что неизвестно что дешевле и лучше, пофиксить неконсистентность раз в полгода (что по сути просто пересохранить сущность) или поддерживать свой двухфазный велосипед для солр и рсубд. К тому же это разные продукты с разными целями и внутренним устройством.

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

Это можно сделать след образом (протестировано):
$entityManagerName = 'doctrine.orm.default_entity_manager';
$page = $this->get($entityManagerName)->getRepository('AppBundle\Entity\Page')
            ->find(3);

$page->setText($someText);

try {
    $this->get($entityManagerName)->flush($page);
} catch(\Exception $e) {
    if (!$em->isOpen()) {
        $this->container->get('doctrine')->resetManager($entityManagerName);
    }
    $page = $em->getRepository(get_class($page))->find($page->getId());
    $this->get('mdiyakov_doctrine_solr.manager.index_process_manager')->reindex($page);
}

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

И соотв. если сущность создается:
$entityManagerName = 'doctrine.orm.default_entity_manager';
$page = new AppBundle\Entity\Page();
...
$page->setText($someText);

try {
    $this->get($entityManagerName)->flush($page);
} catch(\Exception $e) {
    (как в примере выше)...
    $this->get('mdiyakov_doctrine_solr.manager.index_process_manager')->remove($page);
}


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

Как вариант вынести индексацию в очередь, но опять же это не гарант того что изменения будут приняты в солр например и не придется руками обрабатывать ошибку. В DoctrineSolrBundle предусмотрена валидация конфига индексируемых сущностей во время инициализации основных сервисов это поможет снизить возможные ошибки во время индексации.
Прекрасно представляю. Вместо того, чтобы сразу писать в Solr пишем необходимые для индексирования данные в отдельную таблицу, назовем ее log в той же РСУБД в той же транзакции. Получится что-то вроде журнала событий. Можно в json или xml положить, будет гибко. В другом процессе (cron, например) читаем необработанные события из log индексируем в solr. Если при обработке события произойдет сбой, событие не отметится как обработанное и процедура повторится. Изменения в индексах типа Solr как правило идемпотентны и могут быть повторены (если например фейл произошел при пометке события обработанным). Вот вам и 2PC.
А с подходом в бандле сколько не смотри на исключения, не читай код доктрины, не пиши тестов — все равно может произойти рассинхронизация. Его нельзя назвать отказоустойчивым. Аварийный останов веб-сервера тут никак не обработать, а его нужно учитывать.
То что вы с SO привели — я с этим не совсем согласен. Eventual Consistency можно сделать. В MySQL можно вообще бинлог читать и на его основе выполнять индексирование. В Postgres тоже вроде что-то добавили в последних версиях.
Вот вы уже начали изобретать «двухфазный» велосипед и это привело к переусложнению. По вашей логике отправка емейла по крону это двухфазный коммит? Во-первых то что вы описали это не двухфазный коммит, во-вторых даже несмотря на это ваше решение переусложнено и содержит лишнюю логику, в третьих покажу как можно эмулировать «двухфазный» коммит при индексации через cron и чем это грозит.

Запуск по крону выполнения операций записи в солр это не двухфазный коммит, вы не получаете от солр никакого потдверждения о том возможна транзакция или нет во время записи в базу. А соответственно и вся суть двухфазного коммита теряется. При двухфазном коммите либо все участники выполняют транзакцию либо нет(делается rollback). У вас же база выполняет транзакцию а солр нет (читаем теорию en.wikipedia.org/wiki/Two-phase_commit_protocol). Solr не реализует ACID (https://ru.wikipedia.org/wiki/ACID) да ему это и не надо соотв. какой вообще может быть разговор о двухфазном коммите скажите мне?

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

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

Теперь как можно эмулировать «двухфазный» коммит при индексации через крон. Для каждой индексируемой сущности заводите поле updated_at с DateTime когда был послдений апдейт и реализуете версионность, можно хранить всего две версии — актуальную и предыдущую (причем это касается только индексируемых в солр полей сущности). Скрипт синхронизации с солр по крону достает из базы все сущности у которых updated_at больше временной метки последней синхронизации. И для актуальной версии каждой сущности делает запрос в солр на обновление данных, если солр выкидывает эксепшн и говорит что, например, данные каким то образом не валидны то вы пишите в актуальную версию данные из предыдущей версии для сущности и удаляете предыдущую. Т.о. мы реализуем механизм роллбэка в базе в случае фейла при записи в солр.

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

Да мое решение не отказустойчиво в случае внезапного отказа оборудования. Но этого и не требуется. В случае отказа оборудования вы можете просто запустить переиндексацию и все синхронизируется. В 99% случаев синхронной синхронизации реализованной в бандле достаточно.
Чем это грозит? Представьте что менеджер через какую-нибудь cms тулзовину отредактировала страницу и сохранила. Она видит что сохранение успешно прошло и забывает про эту страницу. Спустя секунду запускается скрипт синхронизации с солр. Он пытается индексировать актуальную версию страницы в солр но солр говорит например что данные не валидны, превышено кол-во символов или еще что нибудь. Тогда происходит откат в базе на пред. версию. И по итогу все изменения теряются, менеджер не в курсе что это произошло и данные не обновлены. Соотв. надо реализовать уведомление о такой ситуации и тд… По итогу это все перерастает в снежний ком всякого говна. Занавес.


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

Так вы можете все солровские правила учесть при записи в таблицу log, чтобы не писалось в log и транзакция откатилась.
Solr не реализует ACID (https://ru.wikipedia.org/wiki/ACID) да ему это и не надо соотв. какой вообще может быть разговор о двухфазном коммите скажите мне?

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

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


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

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

В 99% случаев синхронной синхронизации реализованной в бандле достаточно.

Как процент посчитали?

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


Вы серьезно ?) Как насчет hot reload для перезапуска веб-сервера

Вы хотите сказать что это нормально просто взять и остановить сервер бд, без того чтобы вывести maintanence page предварительно? Если вы сервер бд стопанете у вас элементарно сайт работать не будет.

В ваших ответах столько эмоций, зачем? Я вас пытаюсь показать лучшее решение


У меня из эмоций только искреннее удивление вашим трактовкам двухфазного коммита) Вы не лучшее решение пытаетесь показать, вы меня убеждаете в том что запуск скрипта по крону это двухфазный коммит. Я не спорю с фактом что возможна десинхронизация только если свет вырубиться в помещении. Но тк. Solr это вторичное хранилище, вспомогательное а никак не основное то это приемлимо и решается переиндексацией.
Hot reload'ом во всех случаях не отделаешься. Он не всесилен и не вся конфигурация через него меняется. Это отговорки. Админ, который ребутнет сервер, может и не знать, что что-то потом надо переиндексировать.

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


Тут дело в моменте остановки — именно в этот момент может много чего в базу и в индекс писаться, что приведет в вашем случае к неконсистентности. Или так по-вашему тоже не бывает?

Но тк. Solr это вторичное хранилище, вспомогательное а никак не основное то это приемлимо и решается переиндексацией.


Тгда уж лучше по крону с дельтами все индексировать и следить «не рассинхронизировался ли индекс» не надо. Кто за этим будет следить?
Зачем писать индексируемые данные в отдельную таблицу log.


Это эмуляция журнала на уровне приложения. Все транзакционные БД имеют внизу такой-же журнал, но нескоолько в другом виде. Такие журналы — основа обеспечения консистентности и воссстановления после сбоев. Вот полезная книга об этом
то что бд имеют журнал это понятно и для этого есть вполне определенные причины. Зачем это дублировать на уровне приложения вот это непонятно, думаю по ссылке в книге там не учат эмулировать этот журнал на уровне приложения)
Ну назвали бы эти «причины». Основная причина наличия журнала — возможность восттановления после сбоя до определенной точки — обеспечение согласованности. Зачем дублировать? Затем чтобы обеспечить согласованность в конечном счете между солром и данными в БД. И вы сами признали то, что решение с доп. таблицей обепечивает надежность даже в случае аварийных остановов. Понятно зачем дублирование?
Теперь как можно эмулировать «двухфазный» коммит при индексации через крон. Для каждой индексируемой сущности заводите поле updated_at с DateTime когда был послдений апдейт и реализуете версионность, можно хранить всего две версии — актуальную и предыдущую (причем это касается только индексируемых в солр полей сущности). Скрипт синхронизации с солр по крону достает из базы все сущности у которых updated_at больше временной метки последней синхронизации. И для актуальной версии каждой сущности делает запрос в солр на обновление данных, если солр выкидывает эксепшн и говорит что, например, данные каким то образом не валидны то вы пишите в актуальную версию данные из предыдущей версии для сущности и удаляете предыдущую. Т.о. мы реализуем механизм роллбэка в базе в случае фейла при записи в солр.


И теряете при этом данные.
так да, в этом и суть моего примера) Это пример как можно эмулировать двухфазный коммит с возможностью отката в базе и что из этого может получиться. Это пример как НЕ надо делать
А я это и так знаю, зачем написали?
Ваша интерпретация и реализация 2PC никуда не годится. Вы неправильно его понимаете.
Еще раз, я единственное что пытаюсь донести что двухфазный коммит состоит из двух фаз.

При двухфазном коммите либо все участники выполняют транзакцию либо нет(делается rollback). У вас же база выполняет транзакцию а солр нет (читаем теорию en.wikipedia.org/wiki/Two-phase_commit_protocol). Solr не реализует ACID (https://ru.wikipedia.org/wiki/ACID) да ему это и не надо соотв. какой вообще может быть разговор о двухфазном коммите скажите мне?


Реализация ACID подразумевает изоляцию и возможно отката транзакции, т.к. Solr это не реализует то и соотв. у него нет соотв. механизмов роллбэка.

А у вас «крутая библиотека», которая не является велосипедом, и которая при этом не обеспечивает отказоустойчивости?


У меня библиотека которая предлагает свой способ реализации инедкса в солр и поиска по нему. Крутая она или нет, я не знаю, время покажет. И я не изобрел ничего нового тут, принцип синхронизации через EntityListener используется и вдругих решениях, это не я придумал)

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

И это не двухфазный коммит, тут нет никаких фаз, нет возможности откатить транзакцию всем участникам процесса.
У нас 2PC между таблицей log в РСУБД и индексом solr. Фаза подготовки — вытаскиваем событие из таблицы log, индексируем в solr. Фаза фиксации — записываем в РСУБД, что событие обработано. Если фейл произойдет на фазе подготовки по причине отказа недоступности solr — операция повторится. Если на фазе фиксации — тоже повторится. Консистентность гарантирована. Да роллбека в solr нет, но он не обязателен для 2PC — там главное обеспечить согласованность и мы ее сохраняем при таком подходе. Индекс Solr идемпотентен — можно записать второй раз, ничего плохого не случится.
Таким образом, я вижу два отказоустойчивых решения. Первое — полностью индексировать все данные по крону, можно с дельтами. Второе решение — с доп. таблицей с событиями и другим процессом. Второе можно не обязательно по крону — можно и демона написать, который будет поллить таблицу, будет near real time. Ну или cron раз в минуту). И первое и второе реализуется легко. Попробуйте.

Но как часто это случается и даже если это случилось идете и запускаете переиндексацию и всё.

Вот он, авось). Это можно если вы на одном проекте сидите. А если не вы будете поддерживать? За каждым следить? Нет уж, пусть лучше само восстанавливается. А в чем конкретно сложность приведенного мной решения?
Ну вот вы уже и демона писать начали) Демон на пыхе не самое тривиальное решение. В общем я еще раз объясняю при вашем подходе может возникнуть ошибка записи в солр, ну например из-за невалидных данных в солр (при записи date например кривой формат даты или пустое значение в required поле ), и повторно пытаться запускать запись смысла нет. При вашем подходе ваш «двухфазный коммит» будет ждать когда руками отредактируют данные, либо схему solr. И по итогу какое то время опять будет неконсистентность. А если еще логирование плохо настроено то вообще неизвестно когда вы узнаете что у вас там что то неиндексируется. Опять же если индексация реализована так что при таком эксепшне стопать выполняющийся скрипт, то у вас и след изменения зависнут, пока вы не почините битый документ.

Мой подход предпологает синхронную запись в солр которая запускается как правило в потоке пользователя. Открыл страницу, отредактировал, сохранил, на фоне происходит инексация в солр. Если что то не сохранилось/грохнулось это видно сразу, как правило в таких случаях идешь обратно на страницу и пересохраняешь. Это уже дело разработчика обработать эксепшн и указать пользователю что делать. Это не вопрос моего бандла. Я привел выше как можно откатить индексацию в солр если flush вызвал exception.

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

Я указал в статье что индексация реализована через EntityListener. Вы как разработчик вольны выбрать устраивает вас такой подход или нет. Если вас лично не устраиват или вы собираетесь проводить платежные транзакции через солр — ради бога не пользуйтесь моим бандлом, пишите поллинги/демоны тем более для вас это похоже плёвое дело. Но я не собираюсь городить демонов и сприты поллинга в моейм случае. Спасибо за дискуссию
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации