Διανεμημένες Αναπτύξεις

Εξυπηρέτηση ενός εγγράφου από πολλούς κόμβους

Όταν πολλοί κόμβοι εξυπηρετούν τα ίδια έγγραφα, ο κόμβος που αποδίδει ένα έγγραφο δεν είναι απαραίτητα ο κόμβος που απαντά στα αιτήματα σελίδων του προγράμματος περιήγησης. Αυτή η έκδοση χωρίζει αυτή τη δουλειά σε δύο σαφή μισά — μια πλευρά εγγραφής που δημοσιεύει τα τεχνητά ενός εγγράφου σε κοινόχρηστη αποθήκευση, και μια πλευρά ανάγνωσης που τα εξυπηρετεί από οποιονδήποτε κόμβο.

Αν η εφαρμογή σας αυτή τη στιγμή καλεί το UseDoconutWebFarm(...), αυτή η σελίδα είναι η αντικατάστασή του. Ούτε το UseDoconutWebFarm ούτε το WebFarmOptions δεν υπάρχουν σε αυτή την έκδοση.

Η τοπολογία σας δεν αλλάζει. Κατευθύνετε τη κοινόχρηστη ρίζα στην ίδια διαδρομή UNC που χρησιμοποιείτε σήμερα και κάθε κόμβος συνεχίζει να λειτουργεί όπως λειτουργεί τώρα. Η αλλαγή είναι ότι η δημοσίευση είναι μια ενέργεια που καλείτε, αντί για μια παρενέργεια της απόδοσης.

Τι αντικατέστησε το λογισμικό μεσαίας στρώσης web-farm

Προηγουμένως ένα λογισμικό μεσαίας στρώσης έκανε τα πάντα: εξυπηρετούσε προ‑αποδομένες εικόνες από έναν κοινόχρηστο φάκελο και έλεγχε περιοδικά ενώ ένας άλλος κόμβος συνέχιζε να τις γράφει. Η αναμονή ήταν ένας τυφλός προϋπολογισμός — «αποκλεισμός έως τρία λεπτά ελπίζοντας να εμφανιστεί η σελίδα».

Τώρα η ίδια εργασία είναι δύο κλήσεις που μπορείτε να δείτε και να κατανοήσετε:

text
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(...) για αυτόν τον λόγο:

csharp
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; τα ονόματα των κλάσεων δεν άλλαξαν.

ΙδιότηταΣκοπός
LocationCloudLocation.FileShare για ένα UNC/τοπικό share
RootPathΟ κοινόχρηστος φάκελος που μπορεί να προσεγγίσει κάθε κόμβος
UsePerTokenSubfoldersΔιατηρεί τα τεχνητά κάθε εγγράφου στον δικό του φάκελο {token}
ReadReadyPollMsΠόσο χρόνο η πλευρά ανάγνωσης περιμένει για μια σελίδα που εξακολουθεί να γράφεται· 0 απενεργοποιεί τον περιοδικό έλεγχο

Πλευρά εγγραφής — δημοσίευση του εγγράφου

Ενσωματώστε το DistributedDocumentPublisher και δημοσιεύστε αντί να ανοίξετε μια τοπική συνεδρία. Μία κλήση αποδίδει και γράφει το πλήρες σύνολο τεχνητών — σελίδες, πληροφορίες εγγράφου, ευρετήριο αναζήτησης και μεταδεδομένα:

csharp
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 στο ίδιο κλάδο που καλεί ήδη ο πελάτης:

csharp
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:

javascript
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 να αποδώσει στον ίδιο φάκελο, καταχωρίστε την επιλογή ουράς:

csharp
builder.Services.AddDoconut(options => { /* … */ });
builder.Services.AddDoconutDistributedAsyncPublish();

Στη συνέχεια, προσθέστε στην ουρά αντί να δημοσιεύετε ενσωματωμένα, και ορίστε Async = true στις επιλογές δημοσίευσης. Η ουρά είναι περιορισμένη: όταν γεμίσει, το EnqueueAsync ρίχνει DistributedPublishQueueFullException. Απαντήστε με κωδικό 503 και κεφαλίδα Retry-After ώστε οι πελάτες να μειώσουν την προσπάθεια αντί να κολλήσουν:

csharp
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 δεν είναι το μοντέλο ασφαλείας — είναι η αφαίρεση ενός που δεν ισχύει πια. Αντικαταστήστε το με υπογεγραμμένα εισιτήρια πρόσβασης, τα οποία κάθε κόμβος μπορεί να επαληθεύσει ανεξάρτητα:

csharp
builder.Services.AddDoconutDistributedDocumentSecurity(options => { /* … */ });

Ποτέ μην μεταφέρετε το UnsafeMode = true σε μια εγκατάσταση με έναν μόνο κόμβο, και ποτέ μην το θεωρείτε συντόμευση για αποτυχία ελέγχου συνεδρίας σε έναν κόμβο — εκεί η σωστή λύση είναι AddSession() / UseSession(). Δείτε το Συνεδρίες & Ασφάλεια.

Κλείσιμο ενός δημοσιευμένου εγγράφου

CloseDocument(token) στο ενσωματωμένο Viewer απελευθερώνει μια τοπική συνεδρία. Για να αφαιρέσετε τα δημοσιευμένα τεχνητά του εγγράφου από το share, ο εκδότης παρέχει:

text
bool CloseDistributedDocument(FileShareConfig fileShareConfig, string token)

Περάστε το ίδιο FileShareConfig με το οποίο δημοσιεύθηκε το έγγραφο.

Χαρτογράφηση ρύθμισης-προς-ρύθμιση

ΠροηγούμενοΤώρα
UseDoconutWebFarm(new WebFarmOptions { … })PublishAsync(...) στο τμήμα εγγραφής συν UseDoconutCloud<FileShareCloudHandler>(...) στο τμήμα ανάγνωσης
WebFarmOptions.PathFileShareConfig.RootPath
PageWaitTimeSeconds / StartWaitFromPageDistributedDocumentPublishOptions.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.CloudDoconut.Clouds

Λίστα ελέγχου

  1. Δημιουργήστε το FileShareConfig σε ένα βοηθητικό και καλέστε το και από τις δύο πλευρές.
  2. Ορίστε το RootPath στο share που ήδη χρησιμοποιείτε.
  3. Δημοσιεύστε με PublishAsync(...); διαγράψτε τυχόν κλήσεις ExportToPng / ExportToCloud.
  4. Εξυπηρετήστε με UseDoconutCloud<FileShareCloudHandler>(...) στο κλάδο που καλεί ο πελάτης.
  5. Ενεργοποιήστε το FirstPagePriority και το retryOn409 μαζί, ή αφήστε και τα δύο απενεργοποιημένα.
  6. Αντικαταστήστε τον έλεγχο ανά συνεδρία με υπογεγραμμένα εισιτήρια πρόσβασης — μην στέλνετε μόνο UnsafeMode = true.
  7. Επαληθεύστε ανοίγοντας ένα έγγραφο σε έναν κόμβο και ζητώντας μια καθυστερημένη σελίδα από έναν άλλο.

Ήταν αυτή η σελίδα χρήσιμη;