Распределённые развертывания
Обслуживание одного документа с нескольких узлов
Когда несколько узлов обслуживают одни и те же документы, узел, который рендерит документ, не обязательно является тем узлом, который отвечает на запросы страниц браузера. Этот релиз делит эту задачу на две явные части — сторону записи, публикующую артефакты документа в общую память, и сторону чтения, обслуживающую их с любого узла.
Если ваше приложение сейчас вызывает UseDoconutWebFarm(...), эта страница является его заменой. Ни UseDoconutWebFarm, ни WebFarmOptions не существуют в этом релизе.
Ваша топология не меняется. Укажите общую корневую папку по тому же UNC‑пути, который вы используете сегодня, и каждый узел продолжит работать так же, как сейчас. Изменяется лишь то, что публикация становится операцией, которую вы вызываете, а не побочным эффектом рендеринга.
Что заменило middleware web‑farm
Раньше один middleware делал всё: он обслуживал предварительно отрендеренные изображения из общей папки и получал их, пока другой узел ещё их записывал. Ожидание было слепым бюджетом — «блокировать до трёх минут в надежде, что страница появится».
Сейчас эта же задача разбита на два вызова, которые вы можете видеть и контролировать:
node A OpenDocument request
-> PublishAsync(...) writes pages, document info,
search index and metadata
into RootPath/{token}/
-> returns the token
node B ?token=...&page=7
-> UseDoconutCloud<FileShareCloudHandler>
-> reads RootPath/{token}/ and serves the pageОбщая конфигурация, необходимая обеим частям
Публикующий пишет с помощью FileShareConfig, а обработчик чтения обслуживает с тем же. Если RootPath или UsePerTokenSubfolders различаются между ними, другой узел вернёт 404. Создайте конфигурацию в одном месте и используйте её с обеих сторон — в ссылочном приложении есть вспомогательный метод SharedStore.Config(...) именно для этого:
internal static class SharedStore
{
public static FileShareConfig Config(string rootPath, bool asyncPublish) => new()
{
Location = CloudLocation.FileShare,
RootPath = rootPath, // e.g. \\fileserver\doconut
UsePerTokenSubfolders = true,
ReadReadyPollMs = asyncPublish ? 250 : 0,
};
}FileShareConfig находится в Doconut.Clouds вместе с AzureConfig, AmazonConfig, GoogleCloudConfig, DropBoxConfig, RedisConfig, CDNConfig и FTPConfig. В предыдущей библиотеке они находились в Doconut.Configs.Cloud; имена классов не изменились.
| Свойство | Назначение |
|---|---|
Location | CloudLocation.FileShare для UNC/локального общего ресурса |
RootPath | Общая папка, доступная каждому узлу |
UsePerTokenSubfolders | Хранить артефакты каждого документа в отдельной папке {token} |
ReadReadyPollMs | Как долго сторона чтения ждёт страницу, которая ещё записывается; 0 отключает опрос |
Сторона записи — публикация документа
Внедрите DistributedDocumentPublisher и публикуйте вместо открытия локальной сессии. Один вызов рендерит и записывает полный набор артефактов — страницы, информацию о документе, поисковый индекс и метаданные:
var shareConfig = SharedStore.Config(rootPath, asyncPublish: false);
var documentOptions = new DocOptions { TimeOut = 60 };
var config = new PdfConfig { AllowSearch = true };
var publishOptions = new DistributedDocumentPublishOptions
{
CleanUpStagingDirectory = true,
FirstPagePriority = true,
};
DistributedDocumentPublishResult result = await _publisher.PublishAsync(
pathToFile, shareConfig, config, documentOptions, publishOptions, ct);
return Content(result.Token);DistributedDocumentPublishResult содержит Token, ArtifactPath, TotalPages и Format. Токен передаётся браузеру точно так же, как обычный токен просмотрщика.
Этот единый вызов заменяет прежнюю пару — открытие документа в локальной сессии и последующее выгрузку изображений на общий ресурс с помощью ExportToPng(sharedPath). ExportToPng и ExportToCloud здесь не имеют эквивалентов.
Сторона чтения — обслуживать из общего ресурса
Подмонтируйте обработчик FileShare на той же ветке, которую уже вызывает клиент:
app.UseDoconutResources();
app.MapWhen(
ctx => ctx.Request.Path.Value?.EndsWith("DocImage.axd", StringComparison.OrdinalIgnoreCase) == true,
branch => branch.UseDoconutCloud<FileShareCloudHandler>(options =>
{
options.Location = CloudLocation.FileShare;
options.CloudUploadConfig = SharedStore.Config(rootPath, asyncPublish: false);
}, "/"));UseDoconutCloud является обобщённым в этом выпуске: провайдер передаётся как типовой аргумент, а не как значение Location, переключаемое внутри middleware. Обработчики называются <Provider>CloudHandler — FileShareCloudHandler, AzureCloudHandler, AmazonS3CloudHandler, GoogleCloudHandler, DropBoxCloudHandler, RedisCloudHandler, FTPCloudHandler и CDNCloudHandler.
Обработчик CDN является только для чтения — он обслуживает то, что уже находится на CDN; загрузка не производится.
Приоритет первой страницы и HTTP 202
FirstPagePriority = true делает страницу 1 доступной сразу. Последующие страницы отвечают HTTP 202 Accepted, пока они не будут записаны, вместо блокировки запроса.
Это переносит ожидание на клиент, где оно ограничено и видно. Включите повторные попытки в виджете:
const objViewer = $('#div_ctlDoc').docViewer({
BasePath: '/',
ResPath: 'doconut-res/images',
retryOn409: true // retries 202 (and legacy 409) readiness responses
});Клиент, который не делает повторных попыток, показывает сломанные плитки для страниц, которые ещё рендерятся. Ограничения повторных попыток и их значения по умолчанию (retryInitialDelayMs, retryBackoffFactor, retryMaxDelayMs, retryMaxAttempts, retryMaxTotalMs) задокументированы в КонфигурацияПросмотрщика.
Эта пара — FirstPagePriority плюс опрос со стороны чтения — заменяет предыдущие настройки PageWaitTimeSeconds и StartWaitFromPage. Ни одна из них больше не существует.
Необязательно: публикация в фоновом режиме
По умолчанию PublishAsync рендерит до возврата. Чтобы вернуть ответ сразу после создания токена и позволить размещённому воркеру рендерить в ту же папку, зарегистрируйте очередь с согласия:
builder.Services.AddDoconut(options => { /* … */ });
builder.Services.AddDoconutDistributedAsyncPublish();Затем ставьте задачи в очередь вместо непосредственной публикации и задайте Async = true в параметрах публикации. Очередь ограничена: при её насыщении EnqueueAsync бросает DistributedPublishQueueFullException. Ответьте статусом 503 и заголовком Retry-After, чтобы клиенты делали паузу, а не зависали:
try
{
var token = await _publishQueue.EnqueueAsync(
pathToFile, shareConfig, config, documentOptions, publishOptions, ct);
return Content(token);
}
catch (DistributedPublishQueueFullException)
{
Response.Headers.RetryAfter = "5";
Response.StatusCode = 503;
return Content("The publish queue is full. Please retry.");
}Без этой регистрации ничего не регистрируется, и публикация остаётся синхронной — путь по умолчанию идентичен побайтно.
Когда включена фоновая публикация, задайте стороне чтения ненулевое значение ReadReadyPollMs, чтобы она коротко ожидала страницу, которую воркер ещё не записал.
Безопасность между узлами
Токены, привязанные к сессии, предполагают, что обслуживающий узел видел исходный запрос. В ферме это не так, поэтому проверка на уровне сессии отклоняла бы легитимные запросы. Поэтому распределённое демонстрационное приложение устанавливает UnsafeMode = true.
UnsafeMode = true не является моделью безопасности — это удаление модели, которая больше не применима. Замените её на подписанные билеты доступа, которые каждый узел может проверять независимо:
builder.Services.AddDoconutDistributedDocumentSecurity(options => { /* … */ });Никогда не используйте UnsafeMode = true в однопользовательском развертывании и никогда не рассматривайте его как обходную схему для неработающей проверки сессии на одном узле — в этом случае правильное решение — AddSession() / UseSession(). Смотрите Сессии и безопасность.
Закрытие опубликованного документа
CloseDocument(token) на внедрённом Viewer завершает локальную сессию. Чтобы удалить опубликованные артефакты документа из общего ресурса, издатель предоставляет:
bool CloseDistributedDocument(FileShareConfig fileShareConfig, string token)Передайте тот же FileShareConfig, с которым документ был опубликован.
Сопоставление параметр за параметром
| Ранее | Сейчас |
|---|---|
UseDoconutWebFarm(new WebFarmOptions { … }) | PublishAsync(...) на стороне записи плюс UseDoconutCloud<FileShareCloudHandler>(...) на стороне чтения |
WebFarmOptions.Path | FileShareConfig.RootPath |
PageWaitTimeSeconds / StartWaitFromPage | DistributedDocumentPublishOptions.FirstPagePriority плюс FileShareConfig.ReadReadyPollMs |
viewer.OpenDocument(...) then viewer.ExportToPng(share) | один вызов PublishAsync(...) |
viewer.ExportToCloud(cloudConfig) | PublishAsync(...) с конфигурацией провайдера |
UseDoconutCloud(new CloudOptions { Location = … }) | UseDoconutCloud<THandler>(options => …, pathPrefix) |
DocOptions.IsWebfarm / WebfarmPath | По‑прежнему присутствует, написано IsWebFarm / WebFarmPath — не требуется в пути публикации |
Doconut.Configs.Cloud | Doconut.Clouds |
Контрольный список
- Создайте
FileShareConfigв одном вспомогательном методе и вызывайте его из обеих частей. - Укажите
RootPathна общий ресурс, который вы уже используете. - Публикуйте с помощью
PublishAsync(...); удалите любые вызовыExportToPng/ExportToCloud. - Обслуживайте с помощью
UseDoconutCloud<FileShareCloudHandler>(...)на ветке, которую вызывает клиент. - Включите одновременно
FirstPagePriorityиretryOn409, либо отключите оба. - Замените проверку на сеанс подписанными билетами доступа — не поставляйте
UnsafeMode = trueотдельно. - Проверьте, открыв документ на одном узле и запросив позднюю страницу с другого.
Была ли эта страница полезной?