Back to "Μαθαίνοντας LRUs - Όταν η υπέρβαση της ικανότητας κάνει το σύστημά σας καλύτερο"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

Architecture ASP.NET Bot Detection Caching CQRS Systems Design

Μαθαίνοντας LRUs - Όταν η υπέρβαση της ικανότητας κάνει το σύστημά σας καλύτερο

Tuesday, 09 December 2025

Τα περισσότερα συστήματα υποβαθμίζονται όταν υπερφορτώνονται. Μνήμη γεμίζει, ερωτήματα επιβραδύνουν, οι χρήστες παραπονιούνται, διακομιστές συντρίβονται.

Μερικά ασυνήθιστα παίρνουν Καλύτερα..

Αυτό το άρθρο δείχνει πώς ένα Ανάμνηση συμπεριφοράς βάσει LRU γίνεται αυτο-βελτιστοποίηση όταν χτυπά την ικανότητα - και πώς αυτό το μοτίβο τροφοδοτεί το σύστημα μάθησης στο δικό μου Κινητήρας ανίχνευσης bot. Αυτή είναι επίσης η μικρότερη δυνατή έκδοση του Αρχιτεκτονική DiSE - ελεγχόμενη εξέλιξη μέσω της πίεσης των πόρων.

Αν έχεις διαβάσει το άρθρο μου CQRS and Event SourcingΘα αναγνωρίσεις μερικά από τα μοτίβα εδώ, αλλά αυτό είναι το CQRS γυμνό στο κόκκαλο... όχι κατάστημα γεγονότων, όχι προβολές, όχι Μάρτεν... μόνο μια κρύπτη μνήμης, ένας εργάτης υποβάθρου, και το SQLite.

Η Βασική Ιδέα - Συμπεριφορική Μνήμη για τον προϋπολογισμό

Πριν βουτήξουμε, επιτρέψτε μου να ορίσω έναν όρο που θα χρησιμοποιήσω σε: Υπογραφή. Μια υπογραφή είναι οποιοδήποτε σταθερό κλειδί που αντιπροσωπεύει ένα μοτίβο συμπεριφοράς - ένα χασίς IP + User-Agent, ένα δακτυλικό αποτύπωμα των συνδυασμών κεφαλίδας, μια ταξινόμηση του ανιχνευτή "αυτό το αίτημα μοιάζει με X" . Η κρύπτη αποθηκεύει αυτές τις υπογραφές μαζί με μαθημένα βάρη που εξελίσσονται με την πάροδο του χρόνου.

Κι αν μπορούσες να φτιάξεις ένα σύστημα που:

  • Ανταποκρίσεις σε λιγότερο από 1 ms
  • Ποτέ δεν μπλοκάρει τη βάση δεδομένων γράφει
  • Αυτορυθμίσεις υπό πίεση
  • Ξεχνάει ό,τι δεν έχει σημασία.
  • Θυμάται τι κάνει.

Αυτό ακριβώς είναι. IMemoryCache με συρόμενη λήξη σας δίνει - αν καταλαβαίνετε τι χτίζετε.

Τι κάνει στην πραγματικότητα ένα LRU Cache

LRU (Least Recently Used) caches εισόδους έξωσης που δεν έχουν πρόσβαση πρόσφατα. Όταν η κρύπτη γεμίζει, τα πιο κρύα καταχωρήσεις πεταχτούν έξω για να κάνουν χώρο για ζεστές.

Οι περισσότεροι προγραμματιστές το βλέπουν ως περιορισμό. "Ω, όχι, η κρύπτη μου είναι γεμάτη, τα δεδομένα χάνονται!"

Αλλά για τα συστήματα συμπεριφοράς, αυτό είναι ένα χαρακτηριστικό.

// From WeightStore.cs - the bounded memory window
_cache = new MemoryCache(new MemoryCacheOptions
{
    SizeLimit = _cacheSize,           // e.g., 1000 entries
    CompactionPercentage = 0.25       // Remove 25% when limit reached
});

Αυτό SizeLimit Δεν είναι απλά ένας περιορισμός μνήμης. πίεση επιλογήςΟρίζει πόσο το σύστημα "θυμάται" και το αναγκάζει να επικεντρωθεί σε αυτό που έχει σημασία.

Συρόμενη Λήξη - Το Ρολόι που Ξεχνά

Συνδυάζεται με συρόμενη λήξη, μπορείτε να πάρετε αυτόματη ξεχνώντας:

// From WeightStore.cs:254-259
private MemoryCacheEntryOptions GetCacheEntryOptions()
{
    return new MemoryCacheEntryOptions()
        .SetSlidingExpiration(_slidingExpiration)  // 30 minutes
        .SetSize(1);  // Each entry counts as 1 toward size limit
}

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

Αυτό δημιουργεί φυσική ξεχασμένη:

  • Οι IPs θα επανατοποθετηθούν
  • Υπογραφές περιστροφής μπότες
  • Μετατόπιση των προτύπων κυκλοφορίας
  • Οι χθεσινές επιθέσεις δεν είναι σήμερα.

Οι στατικές λίστες είναι μπαγιάτικες, η λήξη κρατάει τη μνήμη φρέσκα.

Το πρότυπο IMemoryCache - Μικροσκοπικό CQRS χωρίς να λέει CQRS

Εδώ είναι το μοτίβο που το κάνει να δουλεύει. SqliteWeightStore τάξη:

/// <summary>
///     SQLite implementation of the weight store with sliding expiration memory cache.
///     Uses a CQRS-style pattern with write-behind:
///     - Reads: Hit memory cache first (fast path), fall back to SQLite on miss
///     - Writes: Update cache immediately, queue SQLite writes for background flush
///     Sliding expiration provides automatic LRU-like eviction behavior.
/// </summary>

public class SqliteWeightStore : IWeightStore, IAsyncDisposable
{
    // Memory cache with sliding expiration - auto-evicts least recently used entries
    private readonly MemoryCache _cache;

    // Write-behind queue for batched SQLite persistence
    private readonly ConcurrentDictionary<string, PendingWrite> _pendingWrites = new();
    private readonly Timer _flushTimer;
    private readonly TimeSpan _flushInterval = TimeSpan.FromMilliseconds(500);

Αυτό είναι Ανεπίσημα CQRS Αντί να ακυρώσετε τις καταχωρήσεις κρυφής μνήμης μετά γράφει: Η κρύπτη ΕΙΝΑΙ το μοντέλο γραφήςSQLite είναι μόνο το ανθεκτικό βιβλίο.

flowchart LR
    subgraph Cache["In-Memory Behaviour Store"]
        A[Hot Signatures] --- B[Sliding Expiry]
    end
    subgraph DB["SQLite Ledger"]
        C[(Durable Write-Behind)]
    end

    A --Periodic Flush--> C
    B --Eviction--> D[Forgotten]

    style Cache fill:none,stroke:#10b981,stroke-width:2px
    style DB fill:none,stroke:#6366f1,stroke-width:2px

Βασική διορατικότητα: διαβάζει και γράφει πηγαίνει στη μνήμηΗ βάση δεδομένων είναι τελικά συνεπής - και αυτό είναι εντάξει.

Η Διαβάστε Μονοπάτι - Cache Πρώτα, Πάντα

Όταν ένας ανιχνευτής χρειάζεται ένα μαθημένο βάρος, χτυπάει την κρύπτη:

// From WeightStore.cs:421-472
public async Task<double> GetWeightAsync(
    string signatureType,
    string signature,
    CancellationToken ct = default)
{
    var key = CacheKey(signatureType, signature);

    // Check cache first (fast path - no DB access)
    if (_cache.TryGetValue(key, out LearnedWeight? cached) && cached != null)
    {
        _metrics?.RecordCacheHit(signatureType);
        return cached.Weight * cached.Confidence;
    }

    _metrics?.RecordCacheMiss(signatureType);

    // Cache miss - load from DB
    await EnsureInitializedAsync(ct);

    await using var conn = new SqliteConnection(_connectionString);
    await conn.OpenAsync(ct);

    var sql = $@"
        SELECT weight, confidence, observation_count, first_seen, last_seen
        FROM {TableName}
        WHERE signature_type = @type AND signature = @sig
    ";

    // ... execute query ...

    if (await reader.ReadAsync(ct))
    {
        // Cache the result for future reads
        var learnedWeight = new LearnedWeight { /* ... */ };
        _cache.Set(key, learnedWeight, GetCacheEntryOptions());

        return weight * confidence;
    }

    return 0.0;  // No learned weight exists
}

Καυτά μονοπάτια Ποτέ δεν χτύπησε τη βάση δεδομένωνΗ κρύπτη είναι η πηγή της αλήθειας για την ανάγνωση. SQLite είναι απλά εφεδρική αποθήκευση.

Η Γράψτε Μονοπάτι - Κρύπτη Αμέσως, Επιμονή Αργότερα

Όταν το σύστημα μαθαίνει κάτι νέο, ενημερώνει αμέσως την κρύπτη και ουρές η βάση δεδομένων γράφει:

// From WeightStore.cs:551-582
public Task UpdateWeightAsync(
    string signatureType,
    string signature,
    double weight,
    double confidence,
    int observationCount,
    CancellationToken ct = default)
{
    var key = CacheKey(signatureType, signature);

    // Update cache immediately (source of truth for reads)
    var learnedWeight = new LearnedWeight
    {
        SignatureType = signatureType,
        Signature = signature,
        Weight = weight,
        Confidence = confidence,
        ObservationCount = observationCount,
        FirstSeen = DateTimeOffset.UtcNow,
        LastSeen = DateTimeOffset.UtcNow
    };
    _cache.Set(key, learnedWeight, GetCacheEntryOptions());

    // Queue for async SQLite persistence (write-behind)
    QueueWrite(signatureType, signature, weight, confidence, observationCount);

    return Task.CompletedTask;
}

Ειδοποίηση: UpdateWeightAsync επιστροφές Task.CompletedTask Το γράψιμο είναι στην ουρά, δεν εκτελείται.

  • Υπο-εκατομμυριοστή καθυστέρηση γραφής
  • Δεν μπλοκάρεται ο I/O
  • Οι γραφές ενώνονται (η τελευταία νίκη γράφει)

Το Παρασκήνιο Φλάσερ - 500 μ. Βαρετής Μαγεία

Κάθε 500ms, εκκρεμούσες γραφές ξεπλένονται στο SQLite σε μία μόνο παρτίδα:

// From WeightStore.cs:274-357
public async Task FlushPendingWritesAsync(CancellationToken ct = default)
{
    if (_pendingWrites.IsEmpty) return;

    // Only one flush at a time
    if (!await _flushLock.WaitAsync(0, ct)) return;

    try
    {
        await EnsureInitializedAsync(ct);

        // Snapshot and clear pending writes atomically
        var writes = new List<PendingWrite>();
        foreach (var key in _pendingWrites.Keys.ToList())
        {
            if (_pendingWrites.TryRemove(key, out var write))
            {
                writes.Add(write);
            }
        }

        if (writes.Count == 0) return;

        await using var conn = new SqliteConnection(_connectionString);
        await conn.OpenAsync(ct);
        await using var transaction = await conn.BeginTransactionAsync(ct);

        try
        {
            var sql = $@"
                INSERT INTO {TableName}
                    (signature_type, signature, weight, confidence,
                     observation_count, first_seen, last_seen)
                VALUES (@type, @sig, @weight, @conf, @count, @now, @now)
                ON CONFLICT(signature_type, signature) DO UPDATE SET
                    weight = @weight,
                    confidence = @conf,
                    observation_count = @count,
                    last_seen = @now
            ";

            foreach (var write in writes)
            {
                await using var cmd = new SqliteCommand(sql, conn, transaction);
                // ... add parameters and execute ...
            }

            await transaction.CommitAsync(ct);
            _logger.LogDebug("Flushed {Count} pending writes in {Duration:F1}ms",
                writes.Count, sw.ElapsedMilliseconds);
        }
        catch
        {
            await transaction.RollbackAsync(ct);
            throw;
        }
    }
    finally
    {
        _flushLock.Release();
    }
}

Αυτό είναι event-sourcing-light- Παίρνεις...

  • Συγκεντρωτικά γράμματα (αποτελεσματική I/O)
  • Συνέπεια συναλλαγής
  • Ενισχυμένες ενημερώσεις (εάν η ίδια υπογραφή ενημερώνεται 10 φορές σε 500ms, μόνο η τελική τιμή είναι γραμμένη)
  • SQLite είναι απόλυτα ευχαριστημένος με αυτό το μοτίβο πρόσβασης

EMA Ενημερώσεις - Μαθαίνοντας με Exponential Moving Μέσους Όρους

Όταν μια νέα παρατήρηση φτάνει, το σύστημα χρησιμοποιεί εκθετική κίνηση μέσους όρους για να ενημερώσει τα βάρη:

// From WeightStore.cs:584-635
public Task RecordObservationAsync(
    string signatureType,
    string signature,
    bool wasBot,
    double detectionConfidence,
    CancellationToken ct = default)
{
    var key = CacheKey(signatureType, signature);

    // Calculate new weight using EMA in memory
    var alpha = 0.1;  // Learning rate
    var weightDelta = wasBot ? detectionConfidence : -detectionConfidence;

    double newWeight;
    double newConfidence;
    int newObservationCount;

    if (_cache.TryGetValue(key, out LearnedWeight? existing) && existing != null)
    {
        // Apply EMA: new_weight = old_weight * (1-α) + delta * α
        newWeight = existing.Weight * (1 - alpha) + weightDelta * alpha;
        newConfidence = Math.Min(1.0, existing.Confidence + detectionConfidence * 0.01);
        newObservationCount = existing.ObservationCount + 1;
    }
    else
    {
        // First observation
        newWeight = weightDelta;
        newConfidence = detectionConfidence;
        newObservationCount = 1;
    }

    // Update cache immediately
    var learnedWeight = new LearnedWeight
    {
        SignatureType = signatureType,
        Signature = signature,
        Weight = newWeight,
        Confidence = newConfidence,
        ObservationCount = newObservationCount,
        FirstSeen = existing?.FirstSeen ?? DateTimeOffset.UtcNow,
        LastSeen = DateTimeOffset.UtcNow
    };
    _cache.Set(key, learnedWeight, GetCacheEntryOptions());

    // Queue for persistence
    QueueWrite(signatureType, signature, newWeight, newConfidence, newObservationCount);

    return Task.CompletedTask;
}

Η φόρμουλα EMA εξομαλύνει τη μάθηση: new_weight = old_weight × (1 - α) + new_value × α

Με α = 0.1:

  • Νέα στοιχεία συμβάλλουν 10%
  • Τα ιστορικά στοιχεία συμβάλλουν κατά 90%
  • Αυτό εμποδίζει τις wild κούνιες από τις απλές παρατηρήσεις

Γιατί η Υπερχείλισις Κάνει το Σύστημα Καλύτερα.

Εδώ είναι η βασική αντίληψη που οι περισσότεροι άνθρωποι χάνουν.

Όταν γεμίσει η κρύπτη:

  • Οι υπογραφές χαμηλής συχνότητας πέφτουν
  • Μόνο ζεστές (συχνά προσβάσιμες) υπογραφές παραμένουν στη μνήμη
  • Η βάση δεδομένων υστερεί σε έναν κύκλο ξεφλούδισμα - και αυτό είναι εντάξει
  • Το σύστημα εστιάζει υπό πίεση

Σκεφτείτε το: αν 50.000 μοναδικές υπογραφές χτυπήσουν τον ανιχνευτή ρομπότ σας, αλλά έχετε μνήμη μόνο για 10.000, ποιες υπογραφές έχουν σημασία;

Οι πιο καυτές 10.000. - που συνήθως αντιπροσωπεύουν το 99% της πραγματικής κίνησης.

Στην παραγωγή, βλέπω περίπου 40.000 one-off υπογραφές ανά ημέρα (σκουπιστές που προσπαθούν μια φορά, τυχαίους ανιχνευτές, νόμιμους χρήστες που δεν επιστρέφουν ποτέ) και ίσως 5 ppl10.000 που επανεμφανίζονται συνεχώς. Αυτά τα 5 ppl10.000 είναι όπου το 99% του κινδύνου ζει. Οι υπογραφές μακράς ουράς; Θόρυβος. Η έξωσή τους δεν βλάπτει την ακρίβεια ανίχνευσης - μπορεί ακόμη και να το βελτιώσει μειώνοντας τα ψευδή θετικά από τα πρότυπα χαμηλής εμπιστοσύνης.

flowchart TB
    subgraph Input["50,000 Unique Signatures"]
        Hot[Hot Signatures\n~10,000]
        Cold[Cold Signatures\n~40,000]
    end

    subgraph Cache["Bounded Cache (10,000)"]
        Kept[Kept in Memory]
    end

    subgraph Evicted["Evicted"]
        Lost[Forgotten\nNoise Traffic]
    end

    Hot --> Kept
    Cold --> Lost

    style Hot fill:none,stroke:#10b981,stroke-width:2px
    style Cold fill:none,stroke:#94a3b8,stroke-width:2px
    style Kept fill:none,stroke:#10b981,stroke-width:2px
    style Lost fill:none,stroke:#ef4444,stroke-width:2px

Υπερχείλιση ακονίζει Το σύστημα αυτορυθμίζεται.

Όταν η υπερχείλιση δεν βοηθάει

Υπάρχουν ακραίες περιπτώσεις όπου η πίεση της LRU λειτουργεί εναντίον σου.

  • Η κάψα είναι πολύ μικρή.: Εάν η κρύπτη σας περιέχει μόνο 100 καταχωρήσεις, αλλά έχετε 1.000 πραγματικά σημαντικές υπογραφές, θα χάνετε συνεχώς και θα χάνετε αρκετά χρήσιμα μοτίβα πριν συσσωρεύσουν αρκετά στοιχεία.

  • Ενιαίες μεταφορές: Εάν τρέχετε σε ένα μικροσκοπικό εσωτερικό σύστημα όπου σχεδόν όλα είναι "καυτά" (μερικές μοναδικές υπογραφές, όλες επαναλαμβανόμενες), η υπερχείλιση σας δίνει λιγότερο όφελος.

  • Πρόβλημα ψυχρής εκκίνησηςΤα πάντα είναι εξίσου ψυχρά. Οι πρώτες λίγες ώρες θα έχουν υψηλότερα λανθασμένα θετικά ποσοστά μέχρι να εδραιωθούν οι θερμές υπογραφές.

Το μοτίβο λειτουργεί καλύτερα όταν έχετε υψηλή καρδινότητα με διανομή ισχύος-νόμου - Πολλές μοναδικές υπογραφές, αλλά ένα μικρό υποσύνολο που κυριαρχεί στην κίνηση.

Κρατώντας Cache και βάση δεδομένων σε συγχρονισμό - Tag-Based Invalidation

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

Συγχρονισμός Παρακμής

Όταν τα βάρη της βάσης δεδομένων αποσυντίθενται, η κρύπτη πρέπει να ακολουθήσει. DecayOldWeightsAsync Η μέθοδος χειρίζεται και τα δύο:

// From WeightStore.cs:725-766
public async Task DecayOldWeightsAsync(TimeSpan maxAge, double decayFactor, CancellationToken ct = default)
{
    await EnsureInitializedAsync(ct);

    await using var conn = new SqliteConnection(_connectionString);
    await conn.OpenAsync(ct);

    var cutoff = DateTimeOffset.UtcNow.Subtract(maxAge).ToString("O");

    // Decay old weights in the database
    var sql = $@"
        UPDATE {TableName}
        SET weight = weight * @decay,
            confidence = confidence * @decay
        WHERE last_seen < @cutoff
    ";

    await using var cmd = new SqliteCommand(sql, conn);
    cmd.Parameters.AddWithValue("@decay", decayFactor);
    cmd.Parameters.AddWithValue("@cutoff", cutoff);

    var updated = await cmd.ExecuteNonQueryAsync(ct);

    // Delete weights that have decayed below threshold
    var deleteSql = $@"
        DELETE FROM {TableName}
        WHERE confidence < 0.01 OR (ABS(weight) < 0.01 AND observation_count < 5)
    ";

    await using var deleteCmd = new SqliteCommand(deleteSql, conn);
    var deleted = await deleteCmd.ExecuteNonQueryAsync(ct);

    if (updated > 0 || deleted > 0)
    {
        _logger.LogInformation(
            "Weight decay: {Updated} decayed, {Deleted} deleted",
            updated, deleted);

        // Compact cache to remove stale entries
        _cache.Compact(0.25);
    }
}

Μετά την αποσύνθεση των αρχείων της βάσης δεδομένων, καλούμε _cache.Compact(0.25) - αυτό εξαναγκάζει την MemoryCache να κάνει έξωση στο 25% των καταχωρήσεων του, δίνοντας προτεραιότητα στην λιγότερο πρόσφατη χρήση. Η επόμενη ανάγνωση θα επαναφορτώσει τις νέες τιμές από τη βάση δεδομένων.

Ετικέτα-Βάση έξωσης

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

// From WeightStore.cs:768-777
/// <summary>
///     Evicts all cached entries for a specific signature type (tag-based eviction).
/// </summary>

public void EvictByTag(string signatureType)
{
    // MemoryCache doesn't natively support tag-based eviction, but we can compact
    // For now, just compact - sliding expiration will handle stale entries
    _cache.Compact(0.1);
    _logger.LogDebug("Compacted cache for signature type: {SignatureType}", signatureType);
}

NET's MemoryCache δεν έχει ιθαγενή tag-based έξωση όπως Redis, αλλά η συμπίεση επιτυγχάνει το ίδιο αποτέλεσμα: να εξαναγκάσει μπαγιάτικες καταχωρήσεις, ας διαβάσετε reputulate από τη βάση δεδομένων.

Η Στρατηγική Συγχρονισμού

Η βασική αντίληψη είναι ότι Ο τέλειος συγχρονισμός δεν είναι απαραίτητος.. Το σύστημα ανέχεται να παρασυρθεί επειδή:

  1. Η Cache χάνει το reload από την DB - σε περίπτωση έξωσης μιας καταχώρησης, η επόμενη διαβάζει φρέσκα δεδομένα
  2. Λαβές μπαγιάτικης λήξης - καταχωρήσεις που δεν έχουν πρόσβαση εντός 30 λεπτών auto-evict
  3. Ανανέωση δυνάμεων συμπίεσης - περιοδική συμπίεση σπρώχνει τις παλιές καταχωρήσεις
  4. Εγγραφή-πίσω από τις ενημερώσεις coalesces - πολλαπλές γρήγορες ενημερώσεις γίνονται ένα DB write

Αυτό είναι τελικά συνοχή γίνεται σωστά. Η κρύπτη παραμένει "κοντά" στη βάση δεδομένων χωρίς να απαιτεί περίπλοκη λογική ακύρωσης.

flowchart TB
    subgraph Sync["Cache-Database Synchronisation"]
        D[Database Decay] --> C[Cache Compact]
        E[Tag Eviction] --> C
        S[Sliding Expiration] --> M[Cache Miss]
        M --> R[Reload from DB]
    end

    style Sync fill:none,stroke:#6366f1,stroke-width:2px

Δεν υπάρχει κόλαση ακύρωσης κρυφής μνήμης, καμία πολύπλοκη παμπ/υποβρύχιο, μόνο συμπίεση και φυσική λήξη.

Πιο Βαθιά: Το Σύστημα Αφοπλίσεως

Σημείωση: Το υπόλοιπο άρθρο δείχνει πώς εφαρμόζω τις ίδιες ιδέες σε πλήρη φήμη μοτίβο - μηχανήματα κατάστασης, υστερία, και χρονική φθορά. Είναι το "έξτρα μίλι" για αυτά τα προσαρμοστικά συστήματα κατασκευής.

Υστέρηση και Δεκάδα

Για τη φήμη μοτίβο (παρακολούθηση αν μια υπογραφή είναι bot ή άνθρωπος με την πάροδο του χρόνου), ισχύουν οι ίδιες αρχές, αλλά με πρόσθετη επιδεξιότητα:

// From PatternReputation.cs:42-108
public record PatternReputation
{
    public required string PatternId { get; init; }
    public required string PatternType { get; init; }
    public required string Pattern { get; init; }

    /// <summary>Current bot probability [0,1]. 0 = human, 1 = bot, 0.5 = neutral</summary>

    public double BotScore { get; init; } = 0.5;

    /// <summary>Effective sample count - decays over time, increases with observations</summary>

    public double Support { get; init; } = 0;

    /// <summary>Current reputation state - determines fast-path behavior</summary>

    public ReputationState State { get; init; } = ReputationState.Neutral;

    // Computed properties
    public double Confidence => Math.Min(1.0, Support / 100.0);

    public bool CanTriggerFastAbort =>
        State is ReputationState.ConfirmedBad or ReputationState.ManuallyBlocked;

    public bool CanTriggerFastAllow =>
        State is ReputationState.ConfirmedGood or ReputationState.ManuallyAllowed;
}

Κρατικές Μεταβάσεις με Υστέρηση

Τα μοτίβα δεν γυρίζουν απευθείας από το Ουδέτερο προς το ΕπιβεβαιωμένοΜπάντ. Υπάρχει υστερία για την πρόληψη φτερουγίσματος:

// From PatternReputation.cs:367-421 - simplified
public PatternReputation EvaluateStateChange(PatternReputation reputation)
{
    if (reputation.IsManual)
        return reputation;

    var newState = reputation.State;
    var score = reputation.BotScore;
    var support = reputation.Support;

    switch (reputation.State)
    {
        case ReputationState.Neutral:
            // Can promote to Suspect or ConfirmedGood
            if (score >= 0.6 && support >= 10)
                newState = ReputationState.Suspect;
            else if (score <= 0.1 && support >= 100)
                newState = ReputationState.ConfirmedGood;
            break;

        case ReputationState.Suspect:
            // Can promote to ConfirmedBad or demote to Neutral
            if (score >= 0.9 && support >= 50)
                newState = ReputationState.ConfirmedBad;
            else if (score <= 0.4 || support < 10)
                newState = ReputationState.Neutral;
            break;

        case ReputationState.ConfirmedBad:
            // Can demote to Suspect (requires MORE evidence to forgive)
            if (score <= 0.7 && support >= 100)
                newState = ReputationState.Suspect;
            break;
    }

    // ... log state change and return ...
}

Σημειώστε την ασυμμετρία: είναι πιο εύκολο να μπλοκαριστείτε παρά να ξεμπλοκαριστείτε. ConfirmedBad → Suspect απαιτεί 100 υποστήριξη, ενώ Neutral → Suspect Χρειάζεται μόνο 10. Αυτό είναι σκόπιμο - είναι πιο δύσκολο να συγχωρήσεις παρά να υποψιαστείς.

Χρονική Παρακμή - Εκδηλωτική Ξεχνάς

Όταν τα μοτίβα πάνε ήσυχα, αποσυντίθενται προς το ουδέτερο:

// From PatternReputation.cs:334-361
public PatternReputation ApplyTimeDecay(PatternReputation reputation)
{
    if (reputation.IsManual)
        return reputation;

    var hoursSinceLastSeen = (DateTimeOffset.UtcNow - reputation.LastSeen).TotalHours;

    if (hoursSinceLastSeen < 1)
        return reputation;  // Too recent to decay

    // Score decay toward prior (0.5 = neutral)
    // new_score = old_score + (prior - old_score) × (1 - e^(-Δt/τ))
    var scoreDecayFactor = 1 - Math.Exp(-hoursSinceLastSeen / _options.ScoreDecayTauHours);
    var newScore = reputation.BotScore + (0.5 - reputation.BotScore) * scoreDecayFactor;

    // Support decay
    // new_support = old_support × e^(-Δt/τ)
    var supportDecayFactor = Math.Exp(-hoursSinceLastSeen / _options.SupportDecayTauHours);
    var newSupport = reputation.Support * supportDecayFactor;

    return reputation with
    {
        BotScore = Math.Clamp(newScore, 0, 1),
        Support = newSupport
    };
}

Οι προεπιλεγμένες σταθερές χρόνου:

  • Αποσύνθεση βαθμολογίας: 168 ώρες (7 ημέρες) - οι βαθμολογίες κινούνται 63% προς ουδέτερο μετά από μια εβδομάδα αδράνειας
  • Αποσύνθεση υποστήριξης336 ώρες (14 ημέρες) - Η εμπιστοσύνη μειώνεται 63% μετά από δύο εβδομάδες

Αυτό σημαίνει ότι μια επιβεβαιωμένη-κακή IP που πάει ήσυχα για ένα μήνα θα πέσει τελικά πίσω στο Neutral. Όχι επειδή αναμορφώθηκε - επειδή τα στοιχεία του έγιναν μπαγιάτικα.

Η Υπηρεσία Συντήρησης Υποβάθρου

Η ReputationMaintenanceService εκτελεί τρεις περιοδικές εργασίες:

// From ReputationMaintenanceService.cs:48-129
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    _logger.LogInformation("Reputation maintenance service starting");

    // Load persisted reputations on startup
    await _cache.LoadAsync(stoppingToken);

    var decayInterval = TimeSpan.FromMinutes(60);    // Hourly decay sweep
    var gcInterval = TimeSpan.FromHours(24);          // Daily garbage collection
    var persistInterval = TimeSpan.FromMinutes(5);   // Persist every 5 minutes

    var lastDecay = DateTimeOffset.UtcNow;
    var lastGc = DateTimeOffset.UtcNow;
    var lastPersist = DateTimeOffset.UtcNow;

    while (!stoppingToken.IsCancellationRequested)
    {
        await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        var now = DateTimeOffset.UtcNow;

        // Decay sweep: push stale scores toward neutral
        if (now - lastDecay >= decayInterval)
        {
            await _cache.DecaySweepAsync(stoppingToken);
            lastDecay = now;
        }

        // Garbage collection: remove old neutral patterns
        if (now - lastGc >= gcInterval)
        {
            await _cache.GarbageCollectAsync(stoppingToken);
            lastGc = now;

            var stats = _cache.GetStats();
            _logger.LogInformation(
                "Reputation stats: {Total} patterns, {Bad} bad, {Suspect} suspect",
                stats.TotalPatterns, stats.ConfirmedBadCount, stats.SuspectCount);
        }

        // Persistence: save to SQLite
        if (now - lastPersist >= persistInterval)
        {
            await _cache.PersistAsync(stoppingToken);
            lastPersist = now;
        }
    }

    // Final persist on shutdown
    await _cache.PersistAsync(CancellationToken.None);
}

Ο συλλέκτης σκουπιδιών αφαιρεί μοτίβα που είναι:

  • 90+ ημερών
  • Υποστήριξη ≤ 1,0
  • Σε Ουδέτερη κατάσταση

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

SQLite δεν είναι ένα αστείο - Είναι τέλειο εδώ

Πολλοί προγραμματιστές φθάνουν αντανακλαστικά για PostgreSQL ή Redis. Αλλά για αυτό το μοτίβο, SQLite είναι ιδανικό:

  1. Γράφει-πίσω εξαλείφει τα σημεία συμφόρησης - Ο περιορισμός single-writer του SQLite δεν έχει σημασία όταν οι γραφές είναι γεμάτες
  2. Τοπική αποθήκευση - χωρίς καθυστέρηση δικτύου, χωρίς πισίνες σύνδεσης
  3. Μηδέν ρύθμιση - απλά μια διαδρομή αρχείου
  4. Ιδανικό για την ανάπτυξη άκρων - τρέχει σε ένα Raspberry Pi
  5. Φορητό - η βάση δεδομένων είναι απλά ένα αρχείο που μπορείτε να αντιγράψετε γύρω

Το σχήμα είναι ελάχιστο:

CREATE TABLE IF NOT EXISTS learned_weights (
    signature_type TEXT NOT NULL,
    signature TEXT NOT NULL,
    weight REAL NOT NULL,
    confidence REAL NOT NULL,
    observation_count INTEGER NOT NULL DEFAULT 1,
    first_seen TEXT NOT NULL,
    last_seen TEXT NOT NULL,
    PRIMARY KEY (signature_type, signature)
);

CREATE INDEX IF NOT EXISTS idx_signature_type ON learned_weights(signature_type);
CREATE INDEX IF NOT EXISTS idx_confidence ON learned_weights(confidence);
CREATE INDEX IF NOT EXISTS idx_last_seen ON learned_weights(last_seen);

Αν χρειάζεστε μεγαλύτερη κλίμακα, αλλάξτε σε PostgreSQL. Αν χρειάζεστε HA ή αντιγραφή, αλλάξτε σε Redis ή μια κατανεμημένη κρύπτη. Η αρχιτεκτονική δεν αλλάζει - μόνο η συμβολοσειρά σύνδεσης. SQLite είναι η προεπιλογή για ανάπτυξη άκρη, όχι μια θρησκεία.

Το DiSE Tie-In - Αποτυχία ως Εξέλιξη

Εάν DiSE είναι η πλήρης εξελικτική μηχανή, αυτό το μοτίβο κρύπτη είναι τα μιτοχόνδρια - το μικρότερο κομμάτι που εξακολουθεί να συμπεριφέρεται σαν εξέλιξη υπό περιορισμό.

Αυτό το μοτίβο εφαρμόζει τις αρχές DiSE στο ελάχιστο επίπεδο:

  • Περιορισό των piόρων → Πίεση επιλογής
  • LRU έξωση → Φυσική επιλογή (επιβάτες είναι η πιο κατάλληλη)
  • Χρονική φθορά → Ξεχνώντας επιτρέπει την προσαρμογή
  • Ενημερώσεις EMA → Μετατροπή μέσω παρατήρησης
  • Υστέρηση → Σταθερότητα μέσω της αντίστασης στην αλλαγή

Η πλήρης κρύπτη δεν είναι αποτυχία. εξελικτική πίεση. Το σύστημα αυτο-βελτιώνει: ζεστές υπογραφές παραμένουν, ψυχρές υπογραφές έξωση, και η μνήμη συμπεριφοράς συγκλίνει προς αυτό που πραγματικά έχει σημασία.

Χωρίς εκπαίδευση ML, χωρίς εξωτερικά μοντέλα, μόνο αρχιτεκτονική που συμπεριφέρεται σαν ζωντανό σύστημα.

Συμπέρασμα - Απλή Δομή, Επείγουσα Συμπεριφορά

Ολόκληρο το μοτίβο καταλήγει στο:

  1. Η Κέιτς είναι η πηγή της αλήθειας για το διάβασμα. - sub-millisecond access
  2. Γράφει ενημέρωση μνήμης αμέσως, επιμένει αργότερα - χωρίς μπλοκάρισμα I/O
  3. Η λήξη της σύρραξης παρέχει αυτόματη LRU - ενσωματωμένο σε .NET
  4. Τραυματισμένο μέγεθος δημιουργεί πίεση επιλογής - υπερχείλιση ακονίζει την εστίαση
  5. Φλος φόντου διατηρεί SQLite σε συγχρονισμό - η τελική συνέπεια είναι μια χαρά.
  6. Η χρονική αποσύνθεση μπορεί να ξεχάσει - μπαγιάτικα στοιχεία εξαφανίζονται

Το μικρό κατάστημα συμπεριφοράς σας ενεργεί περισσότερο σαν ένα ζωντανό σύστημα από CRUD. Θυμάται τι έχει σημασία, ξεχνάει τι δεν κάνει, και παίρνει καλύτερα κάτω από πίεση.

Ελάχιστη αρχιτεκτονική → αναδυόμενη ορθότητα. Υπερχείλιση → καλύτερη εστίαση. Πίεση → σταθερότητα.

Αν θέλετε να το δείτε αυτό σε δράση, ελέγξτε έξω ως επί το πλείστον διαυγής. botdetection - και το Σειρά αρχιτεκτονικής DiSE για την βαθύτερη φιλοσοφία.

Σύνδεσμοι

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.