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
Tuesday, 09 December 2025
Τα περισσότερα συστήματα υποβαθμίζονται όταν υπερφορτώνονται. Μνήμη γεμίζει, ερωτήματα επιβραδύνουν, οι χρήστες παραπονιούνται, διακομιστές συντρίβονται.
Μερικά ασυνήθιστα παίρνουν Καλύτερα..
Αυτό το άρθρο δείχνει πώς ένα Ανάμνηση συμπεριφοράς βάσει LRU γίνεται αυτο-βελτιστοποίηση όταν χτυπά την ικανότητα - και πώς αυτό το μοτίβο τροφοδοτεί το σύστημα μάθησης στο δικό μου Κινητήρας ανίχνευσης bot. Αυτή είναι επίσης η μικρότερη δυνατή έκδοση του Αρχιτεκτονική DiSE - ελεγχόμενη εξέλιξη μέσω της πίεσης των πόρων.
Αν έχεις διαβάσει το άρθρο μου CQRS and Event SourcingΘα αναγνωρίσεις μερικά από τα μοτίβα εδώ, αλλά αυτό είναι το CQRS γυμνό στο κόκκαλο... όχι κατάστημα γεγονότων, όχι προβολές, όχι Μάρτεν... μόνο μια κρύπτη μνήμης, ένας εργάτης υποβάθρου, και το SQLite.
Πριν βουτήξουμε, επιτρέψτε μου να ορίσω έναν όρο που θα χρησιμοποιήσω σε: Υπογραφή. Μια υπογραφή είναι οποιοδήποτε σταθερό κλειδί που αντιπροσωπεύει ένα μοτίβο συμπεριφοράς - ένα χασίς IP + User-Agent, ένα δακτυλικό αποτύπωμα των συνδυασμών κεφαλίδας, μια ταξινόμηση του ανιχνευτή "αυτό το αίτημα μοιάζει με X" . Η κρύπτη αποθηκεύει αυτές τις υπογραφές μαζί με μαθημένα βάρη που εξελίσσονται με την πάροδο του χρόνου.
Κι αν μπορούσες να φτιάξεις ένα σύστημα που:
Αυτό ακριβώς είναι. IMemoryCache με συρόμενη λήξη σας δίνει - αν καταλαβαίνετε τι χτίζετε.
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 λεπτά, είναι έξωση, όχι επειδή είναι λάθος, γιατί δεν είναι πλέον σχετικό.
Αυτό δημιουργεί φυσική ξεχασμένη:
Οι στατικές λίστες είναι μπαγιάτικες, η λήξη κρατάει τη μνήμη φρέσκα.
Εδώ είναι το μοτίβο που το κάνει να δουλεύει. 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
Βασική διορατικότητα: διαβάζει και γράφει πηγαίνει στη μνήμηΗ βάση δεδομένων είναι τελικά συνεπής - και αυτό είναι εντάξει.
Όταν ένας ανιχνευτής χρειάζεται ένα μαθημένο βάρος, χτυπάει την κρύπτη:
// 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 Το γράψιμο είναι στην ουρά, δεν εκτελείται.
Κάθε 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- Παίρνεις...
Όταν μια νέα παρατήρηση φτάνει, το σύστημα χρησιμοποιεί εκθετική κίνηση μέσους όρους για να ενημερώσει τα βάρη:
// 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:
Εδώ είναι η βασική αντίληψη που οι περισσότεροι άνθρωποι χάνουν.
Όταν γεμίσει η κρύπτη:
Σκεφτείτε το: αν 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 πραγματικά σημαντικές υπογραφές, θα χάνετε συνεχώς και θα χάνετε αρκετά χρήσιμα μοτίβα πριν συσσωρεύσουν αρκετά στοιχεία.
Ενιαίες μεταφορές: Εάν τρέχετε σε ένα μικροσκοπικό εσωτερικό σύστημα όπου σχεδόν όλα είναι "καυτά" (μερικές μοναδικές υπογραφές, όλες επαναλαμβανόμενες), η υπερχείλιση σας δίνει λιγότερο όφελος.
Πρόβλημα ψυχρής εκκίνησηςΤα πάντα είναι εξίσου ψυχρά. Οι πρώτες λίγες ώρες θα έχουν υψηλότερα λανθασμένα θετικά ποσοστά μέχρι να εδραιωθούν οι θερμές υπογραφές.
Το μοτίβο λειτουργεί καλύτερα όταν έχετε υψηλή καρδινότητα με διανομή ισχύος-νόμου - Πολλές μοναδικές υπογραφές, αλλά ένα μικρό υποσύνολο που κυριαρχεί στην κίνηση.
Η κρύπτη είναι η πηγή της αλήθειας για την ανάγνωση, αλλά η βάση δεδομένων είναι το ανθεκτικό βιβλίο.
Όταν τα βάρη της βάσης δεδομένων αποσυντίθενται, η κρύπτη πρέπει να ακολουθήσει. 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 από τη βάση δεδομένων.
Η βασική αντίληψη είναι ότι Ο τέλειος συγχρονισμός δεν είναι απαραίτητος.. Το σύστημα ανέχεται να παρασυρθεί επειδή:
Αυτό είναι τελικά συνοχή γίνεται σωστά. Η κρύπτη παραμένει "κοντά" στη βάση δεδομένων χωρίς να απαιτεί περίπλοκη λογική ακύρωσης.
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
};
}
Οι προεπιλεγμένες σταθερές χρόνου:
Αυτό σημαίνει ότι μια επιβεβαιωμένη-κακή 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);
}
Ο συλλέκτης σκουπιδιών αφαιρεί μοτίβα που είναι:
Αυτό εμποδίζει το λεξικό της μνήμης από το να μεγαλώνει αδέσμευτα, διατηρώντας παράλληλα πολύτιμα μαθημένα μοτίβα.
Πολλοί προγραμματιστές φθάνουν αντανακλαστικά για PostgreSQL ή Redis. Αλλά για αυτό το μοτίβο, SQLite είναι ιδανικό:
Το σχήμα είναι ελάχιστο:
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 είναι η πλήρης εξελικτική μηχανή, αυτό το μοτίβο κρύπτη είναι τα μιτοχόνδρια - το μικρότερο κομμάτι που εξακολουθεί να συμπεριφέρεται σαν εξέλιξη υπό περιορισμό.
Αυτό το μοτίβο εφαρμόζει τις αρχές DiSE στο ελάχιστο επίπεδο:
Η πλήρης κρύπτη δεν είναι αποτυχία. εξελικτική πίεση. Το σύστημα αυτο-βελτιώνει: ζεστές υπογραφές παραμένουν, ψυχρές υπογραφές έξωση, και η μνήμη συμπεριφοράς συγκλίνει προς αυτό που πραγματικά έχει σημασία.
Χωρίς εκπαίδευση ML, χωρίς εξωτερικά μοντέλα, μόνο αρχιτεκτονική που συμπεριφέρεται σαν ζωντανό σύστημα.
Ολόκληρο το μοτίβο καταλήγει στο:
Το μικρό κατάστημα συμπεριφοράς σας ενεργεί περισσότερο σαν ένα ζωντανό σύστημα από CRUD. Θυμάται τι έχει σημασία, ξεχνάει τι δεν κάνει, και παίρνει καλύτερα κάτω από πίεση.
Ελάχιστη αρχιτεκτονική → αναδυόμενη ορθότητα. Υπερχείλιση → καλύτερη εστίαση. Πίεση → σταθερότητα.
Αν θέλετε να το δείτε αυτό σε δράση, ελέγξτε έξω ως επί το πλείστον διαυγής. botdetection - και το Σειρά αρχιτεκτονικής DiSE για την βαθύτερη φιλοσοφία.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.