Διανεμημένες Αναπτύξεις
Εξυπηρέτηση ενός εγγράφου από πολλούς κόμβους
Όταν πολλοί κόμβοι εξυπηρετούν τα ίδια έγγραφα, ο κόμβος που αποδίδει ένα έγγραφο δεν είναι απαραίτητα ο κόμβος που απαντά στα αιτήματα σελίδων του προγράμματος περιήγησης. Αυτή η έκδοση χωρίζει αυτή τη δουλειά σε δύο σαφή μισά — μια πλευρά εγγραφής που δημοσιεύει τα τεχνητά ενός εγγράφου σε κοινόχρηστη αποθήκευση, και μια πλευρά ανάγνωσης που τα εξυπηρετεί από οποιονδήποτε κόμβο.
Αν η εφαρμογή σας αυτή τη στιγμή καλεί το UseDoconutWebFarm(...), αυτή η σελίδα είναι η αντικατάστασή του. Ούτε το UseDoconutWebFarm ούτε το WebFarmOptions δεν υπάρχουν σε αυτή την έκδοση.
Η τοπολογία σας δεν αλλάζει. Κατευθύνετε τη κοινόχρηστη ρίζα στην ίδια διαδρομή UNC που χρησιμοποιείτε σήμερα και κάθε κόμβος συνεχίζει να λειτουργεί όπως λειτουργεί τώρα. Η αλλαγή είναι ότι η δημοσίευση είναι μια ενέργεια που καλείτε, αντί για μια παρενέργεια της απόδοσης.
Τι αντικατέστησε το λογισμικό μεσαίας στρώσης web-farm
Προηγουμένως ένα λογισμικό μεσαίας στρώσης έκανε τα πάντα: εξυπηρετούσε προ‑αποδομένες εικόνες από έναν κοινόχρηστο φάκελο και έλεγχε περιοδικά ενώ ένας άλλος κόμβος συνέχιζε να τις γράφει. Η αναμονή ήταν ένας τυφλός προϋπολογισμός — «αποκλεισμός έως τρία λεπτά ελπίζοντας να εμφανιστεί η σελίδα».
Τώρα η ίδια εργασία είναι δύο κλήσεις που μπορείτε να δείτε και να κατανοήσετε:
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/τοπικό share |
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. Το token αποστέλλεται στον περιηγητή ακριβώς όπως κάνει ένα κανονικό token προβολέα.
Αυτή η μοναδική κλήση αντικαθιστά το προηγούμενο ζεύγος — το άνοιγμα του εγγράφου σε μια τοπική συνεδρία και στη συνέχεια η αποθήκευση των εικόνων στο share με το ExportToPng(sharedPath). Τα ExportToPng και ExportToCloud δεν έχουν ισοδύναμο εδώ.
Πλευρά ανάγνωσης — εξυπηρέτηση από το share
Τοποθετήστε τον χειριστή 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 μέχρι να γραφτούν, αντί να μπλοκάρουν το αίτημα.
Αυτό μεταφέρει την αναμονή στον πελάτη, όπου είναι περιορισμένη και ορατή. Ενεργοποιήστε την επανάληψη στο widget:
const objViewer = $('#div_ctlDoc').docViewer({
BasePath: '/',
ResPath: 'doconut-res/images',
retryOn409: true // retries 202 (and legacy 409) readiness responses
});Ένας πελάτης που δεν κάνει επανάληψη εμφανίζει σπασμένα πλακίδια για σελίδες που εξακολουθούν να αποδίδονται. Ο προϋπολογισμός επανάληψης και οι προεπιλογές του (retryInitialDelayMs, retryBackoffFactor, retryMaxDelayMs, retryMaxAttempts, retryMaxTotalMs) τεκμηριώνονται στο ViewerConfig.
Αυτό το ζεύγος — FirstPagePriority συν το ερώτημα της πλευράς ανάγνωσης — αντικαθιστά τις προηγούμενες ρυθμίσεις PageWaitTimeSeconds και StartWaitFromPage. Καμία από αυτές δεν υπάρχει πλέον.
Προαιρετικό: δημοσίευση στο παρασκήνιο
Από προεπιλογή, το PublishAsync αποδίδει πριν επιστρέψει. Για να επιστρέψετε αμέσως μόλις δημιουργηθεί το token και να επιτρέψετε σε έναν φιλοξενούμενο worker να αποδώσει στον ίδιο φάκελο, καταχωρίστε την επιλογή ουράς:
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.");
}Χωρίς αυτήν την καταχώριση, δεν καταχωρείται τίποτα και η δημοσίευση παραμένει συγχρονική — η προεπιλεγμένη διαδρομή είναι ακριβώς η ίδια byte‑for‑byte.
Όταν η δημοσίευση στο παρασκήνιο είναι ενεργή, δώστε στην πλευρά ανάγνωσης ένα μη μηδενικό ReadReadyPollMs ώστε να περιμένει σύντομα για μια σελίδα που ο worker δεν έχει ακόμη γράψει.
Ασφάλεια μεταξύ κόμβων
Τα διακριτικά δεσμευμένα σε συνεδρία υποθέτουν ότι ο κόμβος εξυπηρέτησης είδε το αρχικό αίτημα. Σε ένα σύνολο κόμβων (farm) δεν το έκανε, έτσι ο έλεγχος ανά συνεδρία θα απορρίψει νόμιμα αιτήματα. Γι' αυτό η διανεμημένη εφαρμογή αναφοράς ορίζει UnsafeMode = true.
UnsafeMode = true δεν είναι το μοντέλο ασφαλείας — είναι η αφαίρεση ενός που δεν ισχύει πια. Αντικαταστήστε το με υπογεγραμμένα εισιτήρια πρόσβασης, τα οποία κάθε κόμβος μπορεί να επαληθεύσει ανεξάρτητα:
builder.Services.AddDoconutDistributedDocumentSecurity(options => { /* … */ });Ποτέ μην μεταφέρετε το UnsafeMode = true σε μια εγκατάσταση με έναν μόνο κόμβο, και ποτέ μην το θεωρείτε συντόμευση για αποτυχία ελέγχου συνεδρίας σε έναν κόμβο — εκεί η σωστή λύση είναι AddSession() / UseSession(). Δείτε το Συνεδρίες & Ασφάλεια.
Κλείσιμο ενός δημοσιευμένου εγγράφου
CloseDocument(token) στο ενσωματωμένο Viewer απελευθερώνει μια τοπική συνεδρία. Για να αφαιρέσετε τα δημοσιευμένα τεχνητά του εγγράφου από το share, ο εκδότης παρέχει:
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στο share που ήδη χρησιμοποιείτε. - Δημοσιεύστε με
PublishAsync(...); διαγράψτε τυχόν κλήσειςExportToPng/ExportToCloud. - Εξυπηρετήστε με
UseDoconutCloud<FileShareCloudHandler>(...)στο κλάδο που καλεί ο πελάτης. - Ενεργοποιήστε το
FirstPagePriorityκαι τοretryOn409μαζί, ή αφήστε και τα δύο απενεργοποιημένα. - Αντικαταστήστε τον έλεγχο ανά συνεδρία με υπογεγραμμένα εισιτήρια πρόσβασης — μην στέλνετε μόνο
UnsafeMode = true. - Επαληθεύστε ανοίγοντας ένα έγγραφο σε έναν κόμβο και ζητώντας μια καθυστερημένη σελίδα από έναν άλλο.
Ήταν αυτή η σελίδα χρήσιμη;