У меня есть приложение Blazor Interactive Server, работающее в службах приложений Azure. Он масштабируется/вводится как минимум с двумя экземплярами, а один раз - до 5.
В приложении у меня есть значок BackgroundService
, который я использую для отправки электронных писем, синхронизации с внешней службой, пометки событий как закрытых, когда они достигли даты окончания и т. д. Он запускается раз в 5 минут, и когда я сигнализирую об этом (меняю что-то, что хочу синхронизируйтесь как можно скорее).
Если был один случай, то это раз плюнуть. Я читаю все ожидающие элементы из БД и действую на них и/или обновляю запись базы данных.
Но при наличии более двух экземпляров возникает проблема с отправкой двух писем. Я могу настроить так, чтобы получать DbUpdateConcurrencyException
, когда отправлю письмо дважды, но к тому времени, когда я получу, что уже слишком поздно, я отправил 2 письма.
А когда Azure запускает новые экземпляры и закрывает старые для запуска обновления Windows и т. д., я не могу найти способ определить, что один экземпляр является единственным работающим. Плюс мне нужно в каждом случае сигнализировать работнику.
Итак, есть ли способ создать семафор или что-то в базе данных. Где тогда единственный фоновый поток, которому разрешено работать в течение 2 минут (ограничение по времени на случай, если Azure затем отключит этот сервер)? Или какой-то другой подход?
Поскольку вся работа, которую выполняет этот исполнитель, основана на базе данных, если какой-либо экземпляр обрабатывает фоновые элементы, он получит все элементы, необходимые всем экземплярам. Поэтому нет необходимости сигнализировать другим экземплярам, когда один экземпляр завершил обработку.
Обновление: Основываясь на комментариях ниже, позвольте мне немного добавить.
Вот почему это так сложно. Для некоторых ключевых элементов необходимо обрабатывать их по порядку: я хочу иметь несколько экземпляров, доступных для запуска, и мне нужна возможность для всех экземпляров сервера приложений начать обработку.
Хорошая новость: если один экземпляр получает сигнал, а другой работает, это работает. Второй экземпляр ничего не может сделать.
Или напишите свою очередь так, чтобы ее могли обрабатывать несколько процессоров очереди, а это означает, что процесс должен пометить элемент как выполняющийся так, чтобы это мог сделать только один процесс. А затем отметьте завершение, когда оно будет завершено.
Другой подход — просто не использовать какие-либо автоматические фоновые задания/планировщики, а вместо этого использовать внешнее cron
-задание (например, Azure Logic Apps) для выполнения запросов планировщика и позволить балансировщикам нагрузки Службы приложений Azure решать, какой экземпляр получает возможность запускать задание каждый раз.
Рабочим, обрабатывающим очередь, не нужно знать друг о друге или общаться между собой. Используют ли ваши сотрудники транзакции? uplock
подсказки? readpast
подсказки?
К сожалению, @AlwaysLearning Azure SQL не поддерживает распределенные транзакции. Я оплакиваю медленную смерть X/Open XA.
На самом деле самый простой способ сделать это — использовать существующие технологии. Например, NServiceBus... Он обрабатывает все, что вам нужно, «под капотом», без необходимости развертывания собственного. В противном случае, как вы говорите, любое из решений сложное с проблемами ремонтопригодности.
Возможно, вы можете использовать sp_getapplock, чтобы заблокировать функцию от других потоков.
Здесь у вас есть объекты базы данных SQL Server реализации распределенного мьютекса с ключом:
CREATE TABLE dbo.resource_locks
(
resource_id varchar(256) CONSTRAINT pk_resource_locks PRIMARY KEY,
expires_at datetime NOT NULL,
session_id uniqueidentifier NOT NULL
)
GO
CREATE PROCEDURE dbo.try_acquire_lock
@resource_id varchar(256),
@expires_at datetime,
@session_id uniqueidentifier OUT
AS
SET NOCOUNT ON;
SET @session_id = NEWID()
BEGIN TRY
INSERT INTO dbo.resource_locks(resource_id, expires_at, session_id)
VALUES (@resource_id, @expires_at, @session_id);
-- lock acquired
RETURN 0;
END TRY
BEGIN CATCH
/* 2627 is primary key violation error */
IF ERROR_NUMBER() != 2627 THROW
UPDATE dbo.resource_locks
SET
expires_at = @expires_at,
session_id = @session_id
WHERE
resource_id = @resource_id
/* It acquires expired locks only */
AND expires_at < GETUTCDATE()
IF @@ROWCOUNT = 0
BEGIN
-- failed to acquire lock. the lock is still active or the lock was just released (deleted)
SET @session_id = NULL
RETURN 1
END
-- expired lock acquired
RETURN 0;
END CATCH
GO
CREATE PROCEDURE dbo.try_renew_lock
@resource_id varchar(256),
@expires_at datetime,
@session_id uniqueidentifier
AS
SET NOCOUNT ON;
UPDATE dbo.resource_locks
SET expires_at = @expires_at
WHERE
resource_id = @resource_id
AND session_id = @session_id
IF @@ROWCOUNT = 0
BEGIN
-- failed to renew lock.
RETURN 1
END
-- lock renewed
RETURN 0
GO
CREATE PROCEDURE dbo.try_release_lock
@resource_id nvarchar(384),
@session_id uniqueidentifier
AS
SET NOCOUNT ON
DELETE FROM dbo.resource_locks
WHERE
resource_id = @resource_id
AND session_id = @session_id;
IF @@ROWCOUNT = 0
BEGIN
-- failed to release lock.
RETURN 1
END
-- lock released
RETURN 0
GO
Блокировки хранятся в таблице dbo.resource_locks
. Срок действия блокировок истекает в определенную дату и время. По истечении срока блокировки другие сеансы могут получить блокировку ресурса resource_id
. Таким образом, если процесс завершается, не сняв блокировку, другие процессы могут получить блокировку после истечения ее срока действия.
Когда нужно получить распределенный мьютекс для данного ключа, вызывается dbo.try_acquire_lock
, передавая ключ как resource_id
и соответствующее expires_at
datetime. Если хранимая процедура может получить блокировку, она вернет 0 и установит выходной параметр. Если ему не удастся получить блокировку, он вернет 1, а @session_id
будет @session_id
.
Если операция, которую необходимо выполнить под мьютексом, может завершиться после истечения срока действия мьютекса, вам необходимо продлить блокировку до истечения срока ее действия, передав новую дату истечения срока действия и NULL
, который вы получили при получении блокировки.
В конце операции вам следует снять блокировку, вызвав хранимую процедуру.
Пусть ваши инстансы проведут выборы руководства, и победителем станет тот, кто отвечает за управление
BackgroundService
. (Хотя экземпляры Служб приложений Azure не могут взаимодействовать друг с другом напрямую (т. е. через HTTP), они все равно могут использовать другие общие заявленные/общие постоянные носители, такие как ваша СУБД или экземпляр Redis (или даже общая файловая система), для обмена сообщениями и быть в курсе друг друга и таким образом координировать свои действия.