Розподілені розгортання
Обслуговування одного документа з кількох вузлів
Коли кілька вузлів обслуговують одні й ті ж документи, вузол, який рендерить документ, не обов'язково є тим вузлом, який відповідає на запити браузера щодо сторінки. Це випуск розділяє цю задачу на дві чіткі частини — write side, який публікує артефакти документа у спільне сховище, та read side, який обслуговує їх з будь-якого вузла.
Якщо ваш застосунок зараз викликає 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, // наприклад \\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 | Як довго read side чекає на сторінку, яка ще записується; 0 вимикає polling |
Write side — публікація документа
Впровадьте 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(
path, 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 // повторні спроби 202 (і застарілий 409) відповіді готовності
});Клієнт, який не повторює запити, показує пошкоджені плитки для сторінок, які ще рендеряться. Бюджет повторень та його типові значення (retryInitialDelayMs, retryBackoffFactor, retryMaxDelayMs, retryMaxAttempts, retryMaxTotalMs) задокументовані у Конфігурація переглядача.
Ця пара — FirstPagePriority плюс опитування сторони читання — замінює попередні налаштування PageWaitTimeSeconds та StartWaitFromPage. Жодне з них більше не існує.
Необов’язково: публікація у фоні
За замовчуванням PublishAsync рендериться перед поверненням. Щоб повернутися одразу після створення токену і дозволити розміщеному воркеру рендерити у ту ж папку, зареєструйте чергу opt-in:
builder.Services.AddDoconut(options => { /* … */ });
builder.Services.AddDoconutDistributedAsyncPublish();Потім ставте в чергу замість публікації в рядку та встановіть Async = true у параметрах публікації. Черга обмежена: коли вона переповнюється, EnqueueAsync генерує DistributedPublishQueueFullException. Відповідайте на це за допомогою 503 та заголовка Retry-After, щоб клієнти відступали, а не зависали:
try
{
var token = await _publishQueue.EnqueueAsync(
path, 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, щоб вона коротко чекала на сторінку, яку воркер ще не записав.
Безпека між вузлами
Токени, прив’язані до сесії, передбачають, що обслуговуючий вузол бачив початковий запит. У фермі це не так, тому перевірка per-session відхиляла б легітимні запити. Ось чому розподілене довідкове застосування встановлює 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(...) потім 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окремо. - Перевірте, відкривши документ на одному вузлі та запросивши пізню сторінку з іншого.
Чи була ця сторінка корисною?