представлять
Сегодня, когда популярны распределенные системы и микросервисные архитектуры, сбои при вызове друг друга между сервисами стали нормой. Как обрабатывать исключения и как обеспечить согласованность данных, стало сложной проблемой в процессе проектирования микросервисов.
В разных бизнес-сценариях решения будут разными, общими методами являются:
-
блокировка повторной попытки;
-
2ПК, 3ПК традиционные дела;
-
Использование очередей, фоновая асинхронная обработка;
-
Компенсационные операции TCC;
-
локальная таблица сообщений (обеспечена асинхронность);
-
MQ-транзакции.
В этой статье речь идет о нескольких других моментах, а о традиционных делах 2PC и 3PC уже есть много онлайн-материалов, поэтому я не буду их здесь повторять.
блокировка повторной попытки
В микросервисной архитектуре блокировка повторных попыток является распространенным способом.
Пример псевдокода:
m := db.Insert(sql)
err := request(B-Service,m)
func request(url string,body interface{}){
for i:=0; i<3; i ++ {
result, err = request.POST(url,body)
if err == nil {
break
} else {
log.Print()
}
}
}
Как и выше, в случае сбоя API, запрашивающего службу B, инициируется не более трех повторных попыток. Если трижды не получается, распечатать лог, продолжить выполнение следующего или выдать ошибку на верхний уровень.
Такой подход приносит следующие проблемы:
-
Вызов службы B успешен, но из-за тайм-аута сети текущая служба считает, что произошел сбой, и продолжает повторять попытку, так что служба B сгенерирует 2 части одних и тех же данных.
-
Сбой при вызове службы B. Поскольку служба B недоступна, она все еще терпит неудачу после 3 повторных попыток.Запись, вставленная в БД текущей службой в предыдущем коде, становится грязными данными.
-
Повторная попытка увеличит задержку в восходящем направлении для этого вызова. Если нагрузка в нисходящем направлении велика, повторная попытка усилит нагрузку на нисходящие службы.
Первая проблема: решена за счет того, что API службы B поддерживает идемпотент.
Второй вопрос: Можно подправить данные через фоновые шаги синхронизации, но это не очень хороший способ.
Третья проблема: это существенная жертва для улучшения согласованности и доступности за счет блокировки повторных попыток.
Блокировка повторных попыток подходит для сценариев, в которых бизнес не чувствителен к требованиям согласованности. Если есть требование согласованности данных, для его решения должны быть введены дополнительные механизмы. Кроме того, обратите внимание на общедоступную колонку технологии числовых обезьян и ответьте на ключевое слово «9527», чтобы бесплатно получить последнюю видеоинструкцию по весеннему облаку Alibaba.
Асинхронная очередь
Внедрение очередей — распространенный и лучший способ развития решения. Следующий пример:
m := db.Insert(sql)
err := mq.Publish("B-Service-topic",m)
После того, как текущая служба записывает данные в БД, она отправляет сообщение в MQ, а независимая служба использует MQ для обработки бизнес-логики. По сравнению с блокировкой повторных попыток, хотя стабильность MQ намного выше, чем у обычных бизнес-сервисов, вызов push-сообщений в MQ по-прежнему имеет вероятность сбоя, например, проблемы с сетью, текущее время простоя службы и т. д. Это по-прежнему будет сталкиваться с той же проблемой блокировки повторной попытки, то есть запись БД будет успешной, но отправка не удастся.
Теоретически, в распределённой системе код с множественными обращениями к сервису имеет такую ситуацию, при длительной эксплуатации обязательно произойдёт сбой вызова. Это также одна из трудностей проектирования распределенных систем.
Компенсационная транзакция TCC
Когда есть требования к транзакциям и разделение неудобно, компенсационные транзакции TCC являются лучшим выбором.
TCC делит вызов каждой службы на 2 фазы и 3 операции:
-
Этап 1. Пробная операция: обнаружение бизнес-ресурсов и резервирование ресурсов, таких как инвентаризация и удержание.
-
Этап 2. Подтверждение операции. Отправьте резервирование ресурсов, чтобы подтвердить операцию «Попробовать». Например, обновить удержание инвентаря до вычета.
-
Этап 2. Отмена операции. После сбоя операции Try удерживаемые ресурсы освобождаются. Например, добавление удержания запасов.
TCC требует, чтобы каждая служба реализовывала API трех вышеуказанных операций.Операции, выполняемые за один вызов, прежде чем служба получит доступ к транзакции TCC, должны быть выполнены в два этапа и три операции.
Например, приложению торгового центра необходимо вызвать службу инвентаризации A, службу количества B и службу точек C следующим образом:
m := db.Insert(sql)
aResult, aErr := A.Try(m)
bResult, bErr := B.Try(m)
cResult, cErr := C.Try(m)
if cErr != nil {
A.Cancel()
B.Cancel()
C.Cancel()
} else {
A.Confirm()
B.Confirm()
C.Confirm()
}
В коде API-интерфейсы службы A, B и C вызываются соответственно для проверки и резервирования ресурсов, а операция подтверждения отправляется снова после успешного завершения операции; если операция Try службы C терпит неудачу, и C вызываются соответственно для освобождения резервного ресурса.
TCC решает проблему согласованности данных между несколькими службами и несколькими базами данных в рамках распределенной системы. Тем не менее, в методе TCC все еще есть некоторые проблемы, на которые необходимо обратить внимание при фактическом использовании, включая сбой вызова, упомянутый в предыдущем разделе.
пустой релиз
В приведенном выше коде, если C.Try() действительно терпит неудачу, избыточный вызов C.Cancel() ниже освобождает, а не блокирует ресурс. Это связано с тем, что текущая служба не может определить, действительно ли неудачный вызов блокирует ресурс C. Если он не вызывается, он на самом деле завершается успешно, но возврат невозможен из-за сетевых причин, из-за чего ресурсы C будут заблокированы и никогда не будут освобождены.
Пустой выпуск часто встречается в производственных средах, и службы должны поддерживать выполнение пустого выпуска при реализации API транзакций TCC.
выбор времени
В приведенном выше коде, если C.Try() завершается ошибкой, вызывается операция C.Cancel(). Из-за сетевых причин возможно, что запрос C.Cancel() сначала поступит на службу C, а запрос C.Try() придет позже, что вызовет проблему пустого выпуска и приведет к перерасходу ресурсов C. быть запертым и никогда не освобождаться.
Таким образом, служба C должна отклонить операцию Try() после освобождения ресурса. С точки зрения конкретной реализации, уникальный идентификатор транзакции может использоваться для различия между первой попыткой () и попыткой () после выпуска.
звонок не удался
Во время процесса вызова Cancel and Confirm по-прежнему будут возникать сбои, например, по общим сетевым причинам.
Сбой операции Cancel() или Confirm() приведет к тому, что ресурс будет заблокирован и никогда не будет освобожден. Общие решения для этой ситуации:
-
Блокировка повторной попытки. Но есть одни и те же проблемы, такие как простои, сбои все время.
-
Запись в журнал, постановка в очередь, а затем автоматическое или ручное вмешательство отдельной асинхронной службы. Но будут и проблемы.При записи логов или очередей будут сбои.
Теоретически неатомарный и транзакционный двухэтапный код будет иметь промежуточные состояния, а при наличии промежуточных состояний будет вероятность сбоя.
локальная таблица сообщений
Локальная таблица сообщений была первоначально предложена ebay.Это позволяет размещать локальную таблицу сообщений и таблицу бизнес-данных в одной базе данных, чтобы локальная транзакция могла использоваться для соответствия характеристикам транзакции.
Конкретный метод заключается в вставке данных сообщения при вставке бизнес-данных в локальную транзакцию. Затем выполните последующие операции: если другие операции выполнены успешно, удалите сообщение, если нет, не удаляйте его, прослушайте сообщение асинхронно и повторите попытку.
Локальная таблица сообщений — хорошая идея, и ее можно использовать по-разному:
Сотрудничайте с МК
Пример псевдокода:
messageTx := tc.NewTransaction("order")
messageTxSql := tx.TryPlan("content")
m,err := db.InsertTx(sql,messageTxSql)
if err!=nil {
return err
}
aErr := mq.Publish("B-Service-topic",m)
if aErr!=nil { // 推送到 MQ 失败
messageTx.Confirm() // 更新消息的状态为 confirm
} else {
messageTx.Cancel() // 删除消息
}
// 异步处理 confirm 的消息,继续推送
func OnMessage(task *Task){
err := mq.Publish("B-Service-topic", task.Value())
if err==nil {
messageTx.Cancel()
}
}
В приведенном выше коде messageTxSql — это фрагмент SQL, вставленный в локальную таблицу сообщений:
insert into `tcc_async_task` (`uid`,`name`,`value`,`status`)
values ('?','?','?','?')
Он выполняется в той же транзакции, что и бизнес-SQL, и либо завершается успешно, либо терпит неудачу.
Если отправка прошла успешно, она будет помещена в очередь.Если отправка прошла успешно, будет вызвана функция messageTx.Cancel() для удаления локального сообщения; если отправка не удалась, сообщение будет помечено как подтвержденное. Состояние в локальной таблице сообщений имеет два состояния: попытка и подтверждение, в зависимости от того, какое состояние можно отслеживать в OnMessage, чтобы инициировать повторную попытку.
Локальная транзакция гарантирует, что сообщения и службы будут записаны в базу данных.Независимо от того, прервется ли выполнение или произойдет сбой передачи по сети, можно отслеживать асинхронный мониторинг, что гарантирует, что сообщения будут отправлены в MQ.
С другой стороны, MQ гарантирует, что он обязательно достигнет службы-потребителя.Используя стратегию QOS MQ, служба-потребитель должна быть в состоянии обработать или продолжить доставку в следующую бизнес-очередь, тем самым гарантируя целостность транзакции.
Взаимодействие со службой поддержки
Пример псевдокода:
messageTx := tc.NewTransaction("order")
messageTxSql := tx.TryPlan("content")
body,err := db.InsertTx(sql,messageTxSql)
if err!=nil {
return err
}
aErr := request.POST("B-Service",body)
if aErr!=nil { // 调用 B-Service 失败
messageTx.Confirm() // 更新消息的状态为 confirm
} else {
messageTx.Cancel() // 删除消息
}
// 异步处理 confirm 或 try 的消息,继续调用 B-Service
func OnMessage(task *Task){
// request.POST("B-Service",body)
}
Это пример локальной таблицы сообщений + вызов других сервисов без внедрения MQ. Такое использование асинхронных повторных попыток и использование локальной таблицы сообщений для обеспечения надежности сообщений решает проблемы, вызванные блокировкой повторных попыток, что относительно часто встречается в повседневной разработке.
Если нет операции записи в БД локально, вы можете писать только в локальную таблицу сообщений, которая также обрабатывается в OnMessage:
messageTx := tc.NewTransaction("order")
messageTx := tx.Try("content")
aErr := request.POST("B-Service",body)
// ....
срок действия сообщения истек
Настройте обработчик для сообщений Try и Confirm локальной таблицы сообщений:
TCC.SetTryHandler(OnTryMessage())
TCC.SetConfirmHandler(OnConfirmMessage())
В функции обработки сообщений необходимо оценить, существует ли текущая задача сообщения слишком долго.Например, если она повторялась в течение часа или все еще терпит неудачу, рассмотрите возможность отправки электронных писем, текстовых сообщений, журналов тревог и т. д. ., и пусть ручное вмешательство.
func OnConfirmMessage(task *tcc.Task) {
if time.Now().Sub(task.CreatedAt) > time.Hour {
err := task.Cancel() // 删除该消息,停止重试。
// doSomeThing() 告警,人工介入
return
}
}
В функции «Попытка обработки» также необходимо отдельно оценить, не является ли текущая задача сообщения слишком короткой, поскольку сообщение в состоянии «Попытка» могло быть только что создано и не было подтверждено отправку или удаление. Это будет повторять выполнение обычной бизнес-логики, что означает, что успешные вызовы также будут повторяться; чтобы максимально избежать этой ситуации, вы можете определить, очень ли короткое время создания сообщения, и если оно короткое , вы можете пропустить это.
Механизм повторных попыток должен опираться на идемпотентность нижестоящего API в бизнес-логике.Хотя можно не обрабатывать его, проект должен стараться не мешать обычным запросам.
Независимая служба обмена сообщениями
Независимая служба сообщений — это обновленная версия локальной таблицы сообщений, которая выделяет локальную таблицу сообщений в независимую службу. Добавьте сообщение в службу сообщений перед всеми операциями, удалите сообщение, если последующие операции завершатся успешно, и отправьте подтверждающее сообщение в случае сбоя.
Затем используйте асинхронную логику для отслеживания сообщения и выполнения соответствующей обработки, которая в основном аналогична логике обработки локальной таблицы сообщений. Однако, поскольку добавление сообщения в службу сообщений не может быть помещено в транзакцию с локальной операцией, произойдет успешное добавление сообщения и последующая ошибка, и сообщение в это время является бесполезным сообщением.
Следующий пример сценария:
err := request.POST("Message-Service",body)
if err!=nil {
return err
}
aErr := request.POST("B-Service",body)
if aErr!=nil {
return aErr
}
MQ-транзакции
Некоторые реализации MQ поддерживают транзакции, например RocketMQ. Транзакцию MQ можно рассматривать как конкретную реализацию независимой службы сообщений, и логика полностью согласуется.
Отправьте сообщение в MQ перед всеми операциями. Если последующая операция будет успешной, Подтверждение подтвердит отправку сообщения. Если это не удастся, Отмена удалит сообщение. Транзакции MQ также имеют состояние подготовки, которое требует, чтобы логика обработки потребления MQ подтверждала успешность бизнеса.
Суммировать
С точки зрения практики распределенных систем, для обеспечения согласованности данных необходимо ввести дополнительные механизмы.
Преимущество TCC в том, что он действует на уровне бизнес-сервисов, не зависит от конкретной базы данных, не связан с конкретной инфраструктурой и имеет гибкую степень детализации блокировок ресурсов, что очень подходит для сценариев микросервисов. Недостатком является то, что для каждой службы необходимо реализовать 3 API, а для вторжений и изменений в бизнес необходимо обрабатывать различные исключения сбоев. Разработчикам сложно полностью справляться с различными ситуациями.Нахождение зрелого фреймворка может значительно снизить затраты, например Fescar Али.
Преимущества локальной таблицы сообщений заключаются в том, что она проста, не зависит от преобразования других служб и может использоваться в сочетании с вызовами служб и MQ, что более практично в большинстве бизнес-сценариев. Недостатком является то, что локальная база данных имеет больше таблиц сообщений, связанных с бизнес-таблицами.
Пример метода локальной таблицы сообщений в статье взят из библиотеки, написанной автором. Заинтересованные студенты должны обратить внимание на общедоступную учетную запись «Code Ape Technology Column» и ответить на ключевые слова «120"
Преимущество транзакции MQ и независимой службы сообщений заключается в извлечении общей службы для решения проблемы с транзакцией, избежании того, чтобы каждая служба имела таблицу сообщений и службу, связанную вместе, и увеличивает сложность обработки самой службы. Недостатком является то, что существует несколько MQ, поддерживающих транзакции, а вызов API для добавления сообщения перед каждой операцией увеличит задержку общего вызова, что является ненужным накладным расходом в большинстве бизнес-сценариев с нормальными ответами.