DEV Community

Semaphore σε Middleware: Πλήρης Οδηγός και Συμπεριφορά σε Extreme Scale

Εισαγωγή

Σε πολλές εφαρμογές, ιδιαίτερα σε Cloud-based Azure Functions, συχνά χρειάζεται να ελέγχουμε πόσες concurrent εργασίες μπορούν να έχουν πρόσβαση σε ένα κρίσιμο resource ή διαδικασία. Στην περίπτωση μας, το resource είναι η** λειτουργία ανανέωσης (refresh) της Azure App Configuration** μέσω του middleware AppConfigurationRefreshMiddleware.

Για αυτό χρησιμοποιούμε ένα SemaphoreSlim, που λειτουργεί σαν "έξυπνη πόρτα" για τον περιορισμό των ταυτόχρονων προσβάσεων.


Τι είναι ένα Semaphore;

  • Semaphore: Ένα concurrency primitive που περιορίζει πόσα threads μπορούν να εισέλθουν σε μια κρίσιμη περιοχή ταυτόχρονα.
  • Στην .NET υπάρχει το SemaphoreSlim, μια lightweight υλοποίηση για async/await σενάρια.
  • Μπορεί να έχει:
  1. - InitialCount: Πόσα "slots" είναι διαθέσιμα για concurrent access.
  2. - MaxCount: Το ανώτατο όριο των ταυτόχρονων χρήσεων.

Στην περίπτωσή μας:

private static readonly SemaphoreSlim _refreshSemaphore = new(1, 1);
Enter fullscreen mode Exit fullscreen mode

Μόνο 1 concurrent refresh μπορεί να εκτελεστεί.

Τα υπόλοιπα invocations παρακάμπτουν το refresh αν το semaphore είναι κλειστό.


Πώς λειτουργεί το Middleware

Βήμα-βήμα

  1. Κάθε function invocation περνάει πρώτα από το middleware Invoke(FunctionContext context, FunctionExecutionDelegate next).

  2. Ελέγχει το τελευταίο refresh:

if (now - _lastRefreshUtc > _minRefreshInterval)
Enter fullscreen mode Exit fullscreen mode
  • Αν έχει περάσει το _minRefreshInterval (π.χ. 30 δευτερόλεπτα) από το τελευταίο refresh, προχωρά για ανανέωση.
  • Διαφορετικά, παρακάμπτει το refresh και καλεί αμέσως το next(context).
  1. Απόκτηση Semaphore
if (await _refreshSemaphore.WaitAsync(0))
Enter fullscreen mode Exit fullscreen mode
  • Αν το semaphore είναι ελεύθερο:
  1. Το invocation αποκτάει το "slot".
  2. Εκτελείται η μέθοδος refresher.TryRefreshAsync().
  3. Καταγράφεται η επιτυχία ή η αποτυχία στα logs.
  4. Τέλος, απελευθερώνεται το semaphore (Release()).
  • Αν το semaphore είναι κατειλημμένο:
  1. Το invocation παρακάμπτει το refresh.
  2. Δεν μπλοκάρει τη function.

  3. Συνέχεια pipeline

await next(context);
Enter fullscreen mode Exit fullscreen mode

Η actual function εκτελείται κανονικά.

Το αποτέλεσμα επιστρέφει στον Worker.


Τι συμβαίνει όταν έχουμε πολλούς χρήστες

Σενάριο: 10.000 concurrent invocations

  • Όλα τα invocations μοιράζονται το ίδιο static semaphore.
  • Μόνο ένα invocation θα εκτελέσει το refresh κάθε _minRefreshInterval.
  • Τα υπόλοιπα invocations συνεχίζουν απευθείας.
  • Δεν υπάρχει blocking bottleneck, όλα τα requests προχωρούν.

Σενάριο: 10.000.000 concurrent invocations σε cloud
Σε extreme scale:

  1. Scale-out του Functions Worker
    Το Azure Functions auto-scales instances ανάλογα με το load.
    Κάθε instance έχει το δικό του static semaphore.
    Συνεπώς, σε 10.000.000 users:
    Μπορεί να υπάρξουν πολλαπλά refreshes, αλλά 1 ανά instance ανά 30 δευτερόλεπτα.

  2. Resource impact
    Χρειάζονται περισσότερες CPU, RAM και threads.
    Το middleware δεν προκαλεί bottleneck, αλλά οι instances καταναλώνουν πόρους.

  3. Throttling & App Configuration limits
    Κάθε instance σέβεται _minRefreshInterval.
    Η Azure App Configuration έχει όρια για requests ανά subscription/region.
    Το throttling με semaphore και _minRefreshInterval μειώνει τον κίνδυνο rate limit errors.

  4. Cold starts
    Νέα instances που spin-up σε scale-out αρχίζουν με _lastRefreshUtc = DateTime.MinValue.
    Θα εκτελέσουν αμέσως refresh την πρώτη φορά, μετά throttling κάθε 30 δευτερόλεπτα.


Συμπέρασμα

  • Ο semaphore περιορίζει την concurrency του refresh.
  • Το _minRefreshInterval προστατεύει από υπερβολικές κλήσεις προς App Configuration.
  • Σε scale:
    Middleware είναι safe για εκατομμύρια users.
    Η μόνη προσοχή είναι resource allocation και scale-out behavior.

  • Logging & error handling είναι ενσωματωμένα, ώστε failures να μην επηρεάζουν τις actual functions.


Top comments (0)