# StyloBot Release Series: Ricercare e riparare la crescita senza limiti nel lungo periodo-Running M SK2NET Services

*Lungi-servitivi in funzione tendono a spostarsi lentamente verso la memoria senza confini . Qui' ecco come scopro quel spostamentoM SK3 come penso di ripararlo senza scriverci sopraMSC4 e un esempio funzionante da StyloBotMska5 della strata di similitude dei vettori che l'ha portata dal MSSK6 GB sul Large Object Heap al sotto 6 MBMSM8*

[<img src="/articleimages/stylobot-logo.svg" alt="StyloBot" width="120" />](https://www.stylobot.net)

> **StyloBot Release Series**
> 
> 1. [**Comportamento, Non identità**](/blog/stylobot-fingerprint): perché StyloBot modella i clienti comportamentalmente
> 2. [**Comportamento-Aware ASP.NET UI**](/blog/behaviour-aware-ux): il server- restituisce la superficie su quel risultato di rilevamento.
> 3. **Trovare e riparare la crescita senza limiti nei servizi NET Long-Running .**: la disciplina di affidabilità (esempio funzionanteM SK2 StyloBot)
> 4. [**Comportamento-Conoscere l'interfaccia grafica del tipoScript**](/blog/typescript-sdk): ExpressM SK1 Fastify, e componenti del browser
> 5. [**L'architettura della Sidecar**](/blog/sidecar-architecture): come il motore di rilevazione si collega a stack non -.NET
> 6. [**Imparare a diventare più veloce**](/blog/stylobot-release-learning): il sistema di apprendimento adattativo, quattroM SK2 memoria a livello di livello+, e la cassa del verdetto
> 7. [**Provare a fare ciò che non si ferma.**](/blog/stylobot-release-nondeterministic-testing): la disciplina di verificazioneM SK1 un file BDF guida la regressione , carico, e calibrazione
> 8. [**StyloExtract - un convertitore locale di HTML per Markdown**](/blog/stylobot-release-styloextract): l'HTML→Layere Markdown che si combina con il rilevatore , il walkeder bug lucidVIEW catturatoM SK3 e la catena di cibo per cani che lo rende onesto

<!--category-- ASP.NET, StyloBot, Bot Detection, Performance, Architecture -->
<datetime class="hidden">2026-06-01T10:30</datetime>

L'esempio lavorato è la strata di vectore del StyloBot, ma la disciplina non è't StylobotM SK2specifici . Qualunque lungoMSC4running MST5NET service che accumula lo stato dal traffico alla fine cresce una classe di errori che poteteMSSK6t catturare nei test o in breve attività di caricoMSR7 Si mostra solo dopo giorni di traffico realeMSL8 Questo post è il manuale che uso per scoprirloMsl9

Il modello comportamentale si trova in [Comportamento, Non identità](/blog/stylobot-fingerprint); la superficie ASP.NET in [Comportamento-Aware ASP.NET UI](/blog/behaviour-aware-ux); fonte a [github.comM SK1scottgal/stylobot](https://github.com/scottgal/stylobot).

[TOC]

---


## La classe del problema.

Longi-servitivi in funzione accumulano. Ogni cassaM SK2 ogni learned store , ogni MSC4 noiMST5 mantendremo solo le ultime richieste NMSL6 il buffere è un piccolo accumulatoreMSR7 Ognuna sembra bene da solaMsl8 InsiemeM SR9 su un processo che è stato in funzione per una settimanaMSTR11 formano una forma di memoria nessun test si riproduce maiMstr12

- Funziona bene in dev, CI, e il primo giorno di produzione.
- gradualmente si sposta verso OOM, poi cade o inizia a caricare.

Lo prendi da qui. *cercando deliberatamente.*, su un processo che ' è andato avanti abbastanza a lungo da far emergere la forma.

## passo 1: Rivisioni periodiche di affidabilità (l'atto di guardare)

Il primo pezzo della disciplina è: "' non è tecnico, ., ma ' è l'ingresso del calendario."

Ogni poche versioni, smetto di aggiungere funzionalità e guardo solo il sistema in funzione sotto un traffico realistico, chiedendo *Qualcuno di questi sembra sbagliato?* Non c'è un errore specifico, nessun obiettivo. Guardare soloM SK2 I sistemi che funzionano si muovono silenziosamente ; non falliscono a voce alta, finché non fallisce catastroficamenteMST5 Se guardate solo quando qualcosa si rompeMst6 voiMSt7 avete già persoM st8

La più recente revisione ha catturato il bug di questo post è su:. `Mostlylucid.BotDetection.Demo` Il processo si trova a un residente 20 GB che è stato messo sotto un traffico di test sintetico. QuelloM SK2 è il tipo di numero che non dovrebbe sopravvivere a trenta secondi di attenzioneMSC3

**Regola:** Mettete un slot ricurrente sul calendario per leggere le vostre stesse misurazioni. Non riparare niente. Solo guardareM SK2

## Step 2: Lasciate che i contatori vi sorprendano

L'outil giusto per "è qualcosa di sbagliato?" su un processo .NET è [`dotnet-counters`](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-counters). E'' gratis, è ' è già installatoM SK4 e vi dice la verità su quello che il vostro processo sta facendo proprio ora

Qualche comando dopo alla demo dello StyloBot:

```
dotnet.gc.last_collection.heap.size[loh]   13,393,217,096 bytes (13.4 GB)
dotnet.gc.heap.total_allocated              97 MB/sec
dotnet.exceptions[SqliteException]          4/sec
```

Quello' è abbastanza per sapere cosa c'è di sbagliato, senza leggere ancora nessun codice. [Grande massa di oggetti](https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/large-object-heap) Era di 13.4 GB e cresceva a quasi 100 MB/secM SK3 Qualcosa stava assegnando enormi oggetti in continuazioneMSL4 e il GenMSR5 GC non riusciva a riuscirci con loroMsl7

### Un rapido rifresco sul LOH (perche' ogni .NET dev lo colpisce alla fine.

Il Large Object Heap è una delle più comuni sorprese la prima volta che si presenta un lungo service -running .NET . Vale la pena ripensarlo, perché quasi tutti lo incontrano in modo difficile.

- Il GC ha tre generazioni (GenM SK1 Gen1, Gen+2) per brevi tipi normaliMSC4oggetti viventi . La maggior parte delle asignazioni vive e muore in Gen=0, che è economico di raccogliere
- **Qualunque cosa più grande di 85 KB non va in quelle generazioni.** Va dritto verso l'acqua. **Grande massa di oggetti**.
- Il LOH viene raccolto solo durante un **Gen2 GC**, che è la collezione più costosa .NET lo fa. GenM SK3 succede raramenteMSC4 e il tempo di lavoro cerca di evitarlo
- peggior, di solito il LOH è **non compacto** quando viene raccolto; rilascia solo gli orizzontiM SK1 Quindi anche dopo un Gen2, il LOH diventa frammentato : hai spazio liberoMST4 ma èMSSK5 nel sbagliato MST6 buchi di dimensione per la prossima asignazioneMSS7 nuovi grandi oggetti estendono l'erba piuttosto che la riutilizzareMSM8
- Effetto netto: continuare a assegnare grandi oggetti ad un ritmo stabileM SK1 e il tuo processo' la memoria sale su se gli oggetti sono ancora riferiti o menoMSC3 Sembra una perdita anche quando non è una cosa.

Le maggiori fonti di crescita casuale del LOH nei servizi reali .NETM SK1 nella mia esperienza:

| Source | Perché finisce sul LOH |
|---|---|
| `JsonSerializer.Serialize(obj)` che restituisce un `string` | Un oggetto di MB di 100 diventa un stringo di UTF di \200 MB di -16 di , tutti in una sola asignazione.
| `MemoryStream` Lasciate crescere senza limiti | Il buffer interno si raddoppia oltre 85 KB e rimane sulla LOH |
| `byte[] buf = new byte[n]` per il grande. `n` | Allocation diretta di LOH; molto comune nel fileM SK2network legge |
| `List<T>` che cresce dopo ~10 Referenza KM SK1elementi tipificati | Raccogliture dell'array di supporto 85 KB e attrae sulla LOH SSK4
| `string.Concat` / `StringBuilder` su grande testo | Finale `ToString()` E' una sola grande assegnazione |.
| `XmlSerializer` / `DataContractSerializer` di grandi grafici | La stessa forma del caso JSON |

Se vi ricordate solo una regola del pollice: **essere sospettosi di qualsiasi percorso di codice che produce un singolo grande oggetto contiguo su un timer o per richiesta.** Quella ' è la forma che rovina i lunghi servizi di gestione, e quella ' è esattamente la forma in cui stiamo per trovare nel StyloBot.

```mermaid
flowchart LR
    classDef input fill:none,stroke:#3b82f6,stroke-width:2px
    classDef store fill:none,stroke:#f59e0b,stroke-width:2px
    classDef async fill:none,stroke:#a855f7,stroke-width:2px
    classDef problem fill:none,stroke:#ef4444,stroke-width:2px

    A["Per-request handler<br/>Add to collection"]:::input --> B["Long-lived List / Dict / Buffer"]:::store
    C["Timer / autosave<br/>every N min"]:::async --> D["JsonSerializer / MemoryStream<br/>single &gt;85 KB allocation"]:::problem
    B --> D
    D --> E["Large Object Heap"]:::problem
    E --> F["Gen2 GC<br/>infrequent, expensive"]:::async
    F --> G["LOH not compacted by default<br/>fragments build up"]:::problem
    G --> H["Process RSS marches upward"]:::problem
```

strumenti che aiutano a diagnosticarlo:

- [`dotnet-counters`](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-counters) per numeri vivi (il tasso di LOH sopra ).

- [`dotnet-gcdump`](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-gcdump) e [`dotnet-dump`](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-dump) per immagini che si possono aprire in Visual Studio o PerfView.

- [PerfView](https://github.com/microsoft/perfview) stesso per tracciare le azioni fino ad un stack.

- **Profilisti JetBrains CLI** (quello che ho usato per questo lavoro di riprogettamento). Ora impacchettato come un vero .Tool globali NETM SK3 così senza testa \/ dal remoto |/ La profilazione del tempo di run-time CI non ha bisogno di un GUI da Rider.
  
  - [`JetBrains.dotMemory.GlobalTools`](https://www.jetbrains.com/help/dotmemory/Command-Line_Profiler.html): attaccare, scattare un fotogramma di memoria , aprire il `.dmw` lo spazio di lavoro più tardi per vedere quali radici conservate mantengono le azioni LOH.
  - [`JetBrains.dotTrace.GlobalTools`](https://www.jetbrains.com/help/profiler/Performance_Profiling__Profiling_Using_the_Command_Line.html): la stessa forma per campionare / il tracciamento ♫/ i dati della linea temporale ♫ . ♫ Usate questo quando avete bisogno di una pila piuttosto che di un contatore ♫
  
  ```bash
  dotnet tool install -g JetBrains.dotMemory.GlobalTools
  dotnet tool install -g JetBrains.dotTrace.GlobalTools
  
  dotMemory get-snapshot <pid> --save-to-dir=./snapshots
  dotTrace attach <pid> --profiling-type=Sampling --timeout=60s --save-to=./trace.dtp
  ```
  
  Divisione del lavoro per questo ritocco: `dotnet-counters` mi ha detto che il problema era la LOH; l'immagine della dotMemory mi ha raccontato che il timer autosave era la causa.

Ci sono scatole di fuga se ne avete davvero bisogno. `GCSettings.LargeObjectHeapCompactionMode = CompactOnce` forza una compactazione LOH di uno-off. [`RecyclableMemoryStream`](https://github.com/microsoft/Microsoft.IO.RecyclableMemoryStream) da Microsoft pools buffers per evitare l'allocation in primo luogo. Entrambi sono plastiM SK1 La vera soluzione è quasi sempre fermare la produzione dell'oggetto gigante in primo posto.

### Torniamo al diagnostico.

Quindi, ogni volta che si vede una crescita costante del LOH in un lungo servizio, la domanda è quasi sempre la stessa. *Che cosa'ha a disposizione oggetti molto grandiM SK1 su quale cadenza, e perché?* La forma della risposta (timerM SK1 handler di richiesta? serializzatoreMST3 pool di tamponiMst4 ti dice dove guardare nel codiceMSST5

Le eccezioni contro importavano anche. `4/sec` Di `SqliteException` è abbastanza bassa da non apparire nei log che qualcuno legge, ma abbastanza alta per dirti che un percorso di codice sta quietamente riprovando. Quello ' è il tipo di indizio che la revisione periodica rileva che l'incidente non reagisce maiM SK3

**Regola:** quando qualcosa sembra sbagliato, conferma con `dotnet-counters` prima di indovinare. I numeri ridurranno il raggio di ricerca in un ordine di grandezza.

## passo 3: sbagliatoM SK1odore di abstrazione (esempio funzionante: HNSW per il cachingMSC4

Questo è il punto in cui la disciplina diventa interessante, perché la tentazione è sempre di *Pieghe* il sintomo piuttosto che mettere in discussione l'astratto.

> ### Quick vocab (per le prossime sezioni)
> 
> - **Vector**: un'array di float di dimensione fixed- - eM SK3gMSC4 `float[64]`. Ogni slot è una proprietà misurata della richiesta ( conteggio delle sequenze di caratteri, timingM SK3 famiglia IPMSC4 ecceteraMNK5 Due richieste che " sembrano similiMRK7 producono vectori che sono vicini l'un all'altro in M64- spazio dimensionaleMMK9 Non ci si soprascende. `float[]`.
> - **La somiglianza dei vettori**: di solito [La somiglianza di cosine.](https://en.wikipedia.org/wiki/Cosine_similarity) - la cosine dell'angolo tra due vettori. più vicina a 1.0 = più simileM SK4 La matematica reale è un prodotto di punti diviso da due normeMSC5 una procedura chiamata
> - **Avvicinamento più vicino (ANNM SK1**: "given this vector, find the closest N out of millionsM SK3 fastMST4 BruteMSV5force comparison is OMSC6NM SV7 per queryMSP8 Gli algoritmi ANN ottengono risultati sub-MSV9millisecondi al costo occasionalmente di perdere il *Veramente.* Match più vicino.
> - **HNSW**: uno di quegli algoritmi ANN. Si costruisce un grafico stratificato dove la strata superiore ha alcuni pozziM SK2 nodi connessi e strati inferiori riempiono i dettagli . Si entra in alto e si zooma inMSC4 Eccellente per l'indexazione di una grande *stabile* set of vectors. Non progettato per " ogni richiesta aggiunge uno e quelli vecchi sparisconoM SK2 - il punto rilevante di tutto questo postMSC4
> - **Centroide**: la media di un gruppo di vettori - un vettore che riassume il cluster. Se avete 10,000 vettori che sembrano tutti molto similiM SK4 potete buttarli via e tenerne uno centroideMSC5 il centroide è un sostituto perdente ma compactoMNK6 Le strategie di compactazione usano questo per trasformare molta storia cruda in una piccola storia di riassuntoMMK7

Per StyloBot, la crescita del LOH ha portato a tre classi che erano identiche strutturalmente: nel processoM SK2. [HNSW](https://en.wikipedia.org/wiki/Hierarchical_navigable_small_world_graphs) (Hierarchical Navigable Small World) grafici ([originale Malkov & carta Yashunin](https://arxiv.org/abs/1603.09320)) usata per la ricerca della somiglianza sulla firmaM SK1 sessione, e vettori dell'intenzione . Ognuna aveva un campo comeMSC4

```csharp
private readonly List<float[]> _graphVectors = new();
```

Ognuno è stato alimentato da un handler di apprendimento che si è sottoscritto a `LearningEventType.FullDetection`, che spara su **ogni richiesta HTTP**. Ogni richiesta ha aggiunto un vettore . Ogni domanda. Non c'è stata evizioneM SK3

E ogni cinque minuti, un timer di autosave serializzava l'intero grafico a JSON e lo scriveva al disco:

```csharp
private readonly TimeSpan AutoSaveInterval = TimeSpan.FromMinutes(5);
```

A scala demo, `signatures.vectors.json` Era 104 MBM SK1 In produzione: `intent.meta.json` a 70 MBM SK1 `intent.vectors.json` La serializzazione JSON crea una sequenza contigua nella memoria a 100+ MB. Quella sequence va dritta al LOHM SK4 Tre indice , ogni cinque minutiMSC6 che cresce continuamenteMNK7

```mermaid
flowchart LR
    classDef input fill:none,stroke:#3b82f6,stroke-width:2px
    classDef async fill:none,stroke:#a855f7,stroke-width:2px
    classDef problem fill:none,stroke:#ef4444,stroke-width:2px

    R["HTTP request"]:::input --> H["LearningHandler<br/>FullDetection event"]:::async
    H --> L["List&lt;float[]&gt;<br/>graph vectors<br/>(no eviction)"]:::problem
    L -.5 min timer.-> J["JsonSerializer.Serialize<br/>~100 MB string"]:::problem
    J --> LOH["Large Object Heap"]:::problem
    L -->|grows every request| L
    LOH -.fragmentation.-> RSS["Process RSS climbs"]:::problem
```

Ora: la soluzione facile è aggiungere un cappo. `MaxVectors = 10_000`, Evacuazione dell'esercito dell'LRU, l'accompagnarlo . Avrebbe funzionatoM SK3 I numeri sarebbero scesiMSC4 il pannello sarebbe andato beneMST5 Avremmo anche sbagliato MST6

HNSW è una tecnologia eccezionale e la uso deliberatamente altrove ([Self-Database di Vector Hosted con Qdrant](/blog/self-hosted-vector-databases-qdrant), [La ricerca e l'indexazione ibride di RAG](/blog/rag-hybrid-search-and-indexing), [GraphRAG Minimum Viable](/blog/graphrag-minimum-viable-implementation)). [Pinecone](https://www.pinecone.io/learn/series/faiss/hnsw/), [Invecchiare](https://weaviate.io/developers/weaviate/concepts/vector-index#hnsw-index), [pgvector](https://github.com/pgvector/pgvector#hnsw), e [Qdrant](https://qdrant.tech/documentation/concepts/indexing/#vector-index) tutti lo usano internamente. Ma è un indice di corpus, non una cacheM SK3 e quello che il StyloBot aveva veramente bisogno era una cache *"per ogni impronta digitale attivaM SK1 tenere una piccola finestra di vettori comportamentali recenti così che la rilevazione possa confrontare la richiesta attuale con il comportamento passato da quelli simili."* Le impronte digitali del bot si ripetono (stay hot);le impronte digitales umane non lo fannoM SK2t (evict [Semplice quanto possibile](/blog/botdetection-part3-as-simple-as-possible) post ha classificato l'indice HNSW del processo in - come una caratteristica.

C'è' un rapido test per l'odore : se immaginate di schiacciare una copertina rigida sulla struttura, il risultato è semanticamente ancora la cosa che volevateM SK3 Una cassa HNSW con una costante agitazione non èMSC4un indiceMST5 maMST6un cattivo cassa indossando un indice MST7il vestitoMst8

**Regola:** Quando si vede una crescita senza limiti, la domanda non è: *"come capiamo questa struttura?"* Ma *" è la struttura giusta per quello che stiamo facendo.* La prima nasconde il sintomo.

## Passo 4: Prendete la forma giusta, non solo una cappotta

Una volta che'hai dato il nome a un'abstrazione sbagliata, la sostituzione di solito si scrive da sola . Per StyloBot erano due stratiM SK3

**Layere calda:** un confinato `BoundedVectorCache<TEntry>`, un rivestimento sottile intorno. [`ConcurrentDictionary`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2) con accesso-evizione prioritaria di frequenzaM SK1Il punteggio della retenzione dà ai bot-inträgi classificati un peso di sopravvivenza 2x, così che il cache stessoMSC5organizza intorno al modello di trafficoMNK6

```csharp
retentionScorer: (_, entry) => entry.WasBot ? 2.0 : 1.0
```

**Layere persistente (FOSS):** tre nuove table SQLite (`signature_centroids`, `session_centroids`, `intent_centroids`) immagazzinamento di centroidi compressi da un nightly `VectorCompactionService`. Vectori immagazzinati come float crudo32 gocce usate [`MemoryMarshal.AsBytes`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.memorymarshal.asbytes):

```csharp
internal static byte[] PackFloats(float[] v) =>
    MemoryMarshal.AsBytes(v.AsSpan()).ToArray();
```

Serializzazione binaria Compacta. Niente 100+ String JSON MBM SK2 Nient'altro LOH .

> **Nota del livello di memorizzazione.** SQLite è il FOSS / singolo- backend binario - la giusta chiamata per " lanciare l'exe su un Pi e andarseneM SK4 Il StyloBot commerciale usa PostgreSQL con [pgvector](https://github.com/pgvector/pgvector), che vi da una ricerca corretta di somiglianza indice (HNSW/IVFFlat *all'interno della base di dati.* - su un corpo stabile , dove appartiene l'HNSW, scala orizzontaleM SK3 e stato condiviso in tutta la flottaMSC4 Stessa regola architettonicaMNK5 backend diversoMMK6 [sqlite-vss](https://github.com/asg017/sqlite-vss) è una pietra miliare se si supera SQLite ma si vuole rimanere unito-binarioM SK1 l'aumento più pulito è il Postgres tier.

La ricerca di somiglianza sulla strata persistente FOSS è brutta-force cosine su tutte le righe, accelerata con SIMD M SK2[`System.Numerics.Tensors.TensorPrimitives`](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.tensors.tensorprimitives)). A scala compactaM SK1centroide ( centinaia a qualche migliaio di linee), una scansione completa richiede mSSK4 ms su un PiMSC5 QuelloMST6la fineMSL7 questo funziona solo in manipolatori di fondoMSR8 mai sulla strada veloce per la rilevazioneMsl9

Una nota su **L1/LM SK1 compaction**, dal momento che "compazione" sta facendo un lavoro reale in questa sezioneM SK3 Il servizio di compactazione funziona di notte e riduce la storia in due passaggiMSC4

- **L1**: prendiamo tutti i vectori grezzi scritti oggiM SK1 rimpiazzamo quelli che sono molto simili, e sostituiamo ogni gruppo con un centroide.
- **L2**: prendono LM SK1 centroidi che sono simili nei giorni, / settimane, e li uniscono di nuovo,. aggressivi,M SK4 si verifica meno spesso,; riduzioni di grande dimensione,MSC6altri ~10x). Ciò che sopravvive a L'2 è la forma a lungo termine del traffico, non il rumore transitorio.

Questo è lo stesso schema LSM-Motori di stoccaggio degli alberi (RocksDBM SK2 LevelDB , CassandraMSC4 uso per le SSTablesMSSK5 livello | 0 contiene dati recenti corretti |- dati ristretti |MSC8 livelli inferiori contieneno dati compressi | MSC9 | dati sommizzati | SMC10 | Borrowing it works because the access pattern is the same | : | la maggior parte dei lettori colpisce i dati più recenti | SSK12 | una piccola frazione dei lettori ha bisogno della coda lunga | МSK13 | non c'è niente di buono nel mantenere ogni fila nera per sempre |

```mermaid
flowchart LR
    classDef input fill:none,stroke:#3b82f6,stroke-width:2px
    classDef store fill:none,stroke:#f59e0b,stroke-width:2px
    classDef async fill:none,stroke:#a855f7,stroke-width:2px
    classDef good fill:none,stroke:#22c55e,stroke-width:2px

    R["HTTP request"]:::input --> FP["Fast path<br/>BoundedVectorCache.TryGet"]:::good
    FP -->|hit| S["Use similarity signal"]:::good
    FP -->|miss| N["null<br/>other detectors still run"]:::good
    R -.post-response.-> BG["Background learning handler"]:::async
    BG --> DB["SQLite (FOSS)<br/>or Postgres+pgvector (paid)"]:::store
    BG --> WARM["Warm cache for next time"]:::async
    WARM --> FP
    NC["Nightly VectorCompactionService<br/>L1 then L2"]:::async --> DB
    DB --> NC
```

La forma generale (**Cache caldo confinata per lo stato stabile + archivio persistente compacto per la storia + un compactore periodico tra loro**) si presenta più e più volte in sistemi di apprendimento a lungo -funzionano . vale la pena tenerlo nella vostra cassetta di attrezzi. Quando qualcuno si rivolge al HNSW o al pgvector per un carico di lavoro che \'\ è in realtà una cache \ ,\ questo è il più economico \ ,\ la cosa più semplice per cui avrebbero dovuto arrivare \.\

**Regola:** Preferisce la struttura di dati più semplice che corrisponda al *il modello del tempo di esecuzione*, non quello che corrisponde al set di dati ' la dimensione sulla carta.

## passo 5: Rendere il percorso veloce tollerare gli errori

Una cosa sottile, perché è una questione di contenzione piuttosto che di aggiunta.

Se il percorso di aggiustazione lento-include un'osservazione della base di dati, la tentazione è di fare un blocco del percorso veloce su di esso . DonM SK3t\. Il percorso veloce di rilevamento in StyloBot è sincronizzatoMSC5 e il controllo della somiglianza non è un lookup di dizionario che blocchi l'esempio.

```csharp
if (!_cache.TryGet(signatureId, out var entry))
    return null; // no signal this request - other 48 detectors still run
```

Una sbagliosa significa *Non c'è segnale di somiglianza in questa richiesta.*. Gli altri rilevatori continuano a funzionare. Un handler di fondo interroga SQLite dopo che la richiesta è completata e riscalda il cache per la prossima volta ; il percorso veloce non blocca mai una domanda di databaseM SK3

Questo è il punto in cui la maggior parte del caching va storto: le persone costruiscono un cache, fanno il fallback sincronizzato " per la correttezzaM SK3 e il percorso di errore diventa un MSC4ms pMST5 spikeMst6 Il cache era stato progettato per rendere le cose più velociMSt7 invece ha peggiorato il peggiore casoM st8

**Regola:** se puoi'non tollerare un errore, non lo haiM SK2non c'è una cassa . hai una fila fantasticaMSC4

## passo 6: Rivisione *ogni anno.* accumulatore, non solo quello forte

Una volta che avete trovato una struttura senza confini, assumete che ci siano altre.

L'audit è meccanico. Per ogni lungo dizionario-per un dizionaio viventeM SK2 lista , o settaMSC4 *Che cosa limita la sua dimensione, chi decide quando si lasciano le entrate, e qualeM SK2 è il caso peggiore sotto un traffico ostile ?* Se potete' non rispondere a tutti i tre in una sola frase, è un'infiltrazione . M SK4La stessa disciplina che la [Signali efemerali](/blog/ephemeral-signals) modello: ogni cosa che si accumula deve decadereM SK1 estrarre , o compacto.)

Per StyloBot, la maggior parte degli accumulatori erano già buoniM SK1distretti:

- `EphemeralPatternReputationCache`: cella rigida alle 10,000 entrate con decasione di fondo e estinzione delle LRU SSK2meccanica della decazione `Neutral` → `Suspect` → `ConfirmedBad` la macchina dello stato, e l'isteresia assimetrica vivono in [Imparare a diventare più veloce](/blog/stylobot-release-learning))
- `BehavioralPatternAnalyzer`: IMemoryCache con per-limiti di identità (50 percorsiM SK3 100 timing, \15-min TTL
- `DriftDetectionHandler`:  10,000 campioni
- `SessionEscalationService`: 35-minuta TTL con timer-evizione guidata

C'è bisogno di attenzione oltre le classi HNSW: `MarkovTracker._cohortBaselines`. Il [La catena di Markov](https://en.wikipedia.org/wiki/Markov_chain) Il tracciatore mantiene le matrice di transizione di base per la cohorta (separate dalle catene di firma per l'-, che avevano già avuto l'evizione delle LRU. `MaxTrackedSignatures`). Le linee di base della cohorta (una per una cohorte di traffico come "datacenterM SK3nove"o M"residenzialeMSC6intornamento~") non c'è stata alcuna evizione~. La regola R:eliminare le cohorti più fredde P(la minore transizione totale~M SK11 quando il vocabolario supera `SelfMaintenanceOptions.MarkovCohortSize`.

**Regola:** ogni singolo tono di raccolta risponde a tre domande o è un errore.

## passo 7: Rendere ogni confine configurabile

Il prossimo modo di fallimento dopo "no bound" è "bound cheM SK3 è sbagliato per questo hardware `MaxEntries = 10_000` va bene finché qualcuno non gestisce il vostro servizio su un Pi4, o in un contenitore con 256 MBM SK2

Per StyloBot, ogni limite è atterrato sotto un singolo. `SelfMaintenanceOptions` bloccare. `appsettings.json`. I predefinimenti funzionano per un server standard. Per l'hardware limitato ci sono `LowMemory` Presizione statica:

```csharp
public static SelfMaintenanceOptions LowMemory => new()
{
    SignatureCacheSize  = 1_000,
    SessionCacheSize    = 500,
    IntentCacheSize     = 300,
    MarkovCohortSize    = 2_000,
    CacheSlidingExpiration = TimeSpan.FromHours(1),
};
```

```csharp
builder.Services.AddBotDetection(opts =>
{
    opts.SelfMaintenance = SelfMaintenanceOptions.LowMemory;
});
```

O attraverso. `appsettings.json` per l'ambiente-Tuning specifico:

```json
{
  "BotDetection": {
    "SelfMaintenance": {
      "SignatureCacheSize": 1000,
      "SessionCacheSize": 500,
      "IntentCacheSize": 300,
      "CentroidRetentionDays": 14
    }
  }
}
```

Il punto più profondo, :, un confine che ' ha fissato in un posto è qualcosa di cui gli operatori possono ragionare. `const int` Le declarazioni sono qualcosa su cui nessuno può ragionare.

**Regola:** ogni confine che l'operatore potrebbe voler cambiare per la vita del loro hardware in un singolo blocco di configurazione, non sparso attraverso la base di codici.

## Come appare "fixed"

Per StyloBot in particolare (FOSS buildM SK1 LowMemory preset):

| Componente | Prima | Dopo S|
|---|---|---|
| Signature HNSW index | Unbounded LOH |
| Indice di sessione HNSW | Unbounded LOH | ~258 Cache calda KB SSK4
| Indice dell'intenzione HNSW | LOH senza confine SSK2 ~43 Cache calda KB S|
| I bufferi di auto-saldaggio JSON | 100-500 MB LOH ogni SSK3 min
| Linee di base della cohorta di Markov | Senza confine SSK2 ~1 MB
| Layere totale del vettore | **13+ GB LOH** | **<6 MB** |

Il modello di rilevamento non è cambiato. Che cosa è cambiata? *dove ci sono prove di somiglianza.* e *quando è stato permesso di influenzare il percorso veloce.*. Centroidi sopravviventi ricominciano a relazionare in SQLite (FOSS) o PostgresM SK3pgvector (paidMSC5 la compressione notturna produce ancora LMSSK6L\2; nessuno di questi richiede memoria senza confini

Su un Pi4 con il LowMemory preset, la costruzione FOSS si trova sotto 500 MB RSS dopo il riscaldamentoMSC3 indefinitamenteM SK4 Postgres pagatoMNK5 deploiementi supportati ereditano lo stesso caldoMRK6 disciplina di archiviamentoMMK7 la strata persistente scale semplicemente orizzontale invece che vivere vicino al processoMEK8 in ogni casoMZK9 stabile prevedibileMKK10 immagazzinatura di statoMGK11 raggiungibile dopo il caloreMKZ12 indipendentemente dal tempo di rialzo o dal trafficoMDK13

## La lezione generale.

L'aggiunta di una copertina limita la memoria senza cambiare l'architettura. La falsa astrazione rimane sbagliata; il sintomo diventa più silenziosoM SK2 La persona successiva che tocca il sistema eredita una struttura che quasi funziona , che è peggiore di quella che ovviamente non funziona

Il modello che si riproduce nei sistemi di apprendimento a lungo-running learning systems:

- **Cappare il percorso caldo.**: confinatoM SK1 in-memory , progettato per tollerare i mischiMSC4 politica di deportazione armonizzata con la carico di lavoro
- **Comprimere la storia.**: archivio binario persistente compacto , compresso periodicamente, accessibile fuori dal thread della richiesta
- **Controlliamo tutto il resto.**: ogni accumulatore risponde a cosa lo restringe , ciò che l'evicisce M SK2 cosa' è il caso peggiore
- **Centralizzare i bottoni.**: un blocco di configurazione, non 15 `const`Sì.
- **Programmare la ricerca**: il bug esiste solo nel sistema in corso

Fixare la forma, non il sintomo.

---


## Ancora una lettura.

**StyloBot**

- [Comportamento, Non identità](/blog/stylobot-fingerprint): il modello comportamentale e la classificazione Leiden di questa strata è la memoria per
- [Comportamento-Aware ASP.NET UI](/blog/behaviour-aware-ux): la superficie che consuma giudizi di rilevamento in Razor
- [Imparare a diventare più veloce](/blog/stylobot-release-learning): il sistema di apprendimento a quattro livelli e la cache dei vertici che questa disciplina della memoria mantiene limitata.
- [Bot Detection Part 2: Signature Pipeline and StyloBot Architecture](/blog/botdetection-part2-signature-pipeline-and-stylobot-architecture): la rilevazione originale-ingegneria architettonica post
- [Parte di Detezione del Bot 3: Semplicemente possibile](/blog/botdetection-part3-as-simple-as-possible): due - declino di lineaM SK2in ( dove è stato introdotto per la prima volta l'indice HNSW del processo in M-

**HNSW e ricerca dei vettori in questo blog**

- [Self-Database di Vector Hosted con Qdrant](/blog/self-hosted-vector-databases-qdrant): immersione profonda dell'HNSWM SK1 strati grafici, `M`, `ef_construct`, aggiustazione
- [La ricerca e l'indexazione ibride di RAG](/blog/rag-hybrid-search-and-indexing): HNSW come metà densa di un tubo di ricavamento ibrido
- [GraphRAG Minimum Viable](/blog/graphrag-minimum-viable-implementation): DuckDB VSS HNSW ti ha preso (`array_cosine_distance` contro `array_cosine_similarity`)
- [La ricerca semantica con ONNX e Qdrant](/blog/semantic-search-with-onnx-and-qdrant): HNSW configurazione tramite il cliente Qdrant .NET
- [GraphRAG: Perché la ricerca dei vettori si rompe al livello del corpus](/blog/graphrag-knowledge-graphs-for-rag): perché pure ANN non è'sufficiente su scala

**Riferizioni esterne**

- [HNSW (Wikipedia](https://en.wikipedia.org/wiki/Hierarchical_navigable_small_world_graphs)
- [Malkov & YashuninM SK1 *Efficiente e robusta ricerca approssimativa del vicino più vicino usando grafici gerarchici Navigabili Small World*](https://arxiv.org/abs/1603.09320): il foglio originale 2016
- [Pinecone: Faiss / HNSW spiegatore](https://www.pinecone.io/learn/series/faiss/hnsw/)
- [I concetti dell'indexazione Qdrant](https://qdrant.tech/documentation/concepts/indexing/#vector-index)
- [.NET Large Object Heap Docs](https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/large-object-heap)
- [`dotnet-counters` Riferire.](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-counters): l'attrezzatura che ha superato il LOH in primo luogo
- [JetBrains dotMemory command-line profiler](https://www.jetbrains.com/help/dotmemory/Command-Line_Profiler.html): il profilatore di memoria globale del CLI / globale- dell'outil usato per questa rielaborazione.
- [JetBrains dotTrace command-line profiler](https://www.jetbrains.com/help/profiler/Performance_Profiling__Profiling_Using_the_Command_Line.html): stessa forma per la performance / profilazione delle linee temporali
- [sqlite-vss](https://github.com/asg017/sqlite-vss): SQLite + Faiss, se e quando la stratagemma persistente supera la forza brutta

Source per la implementazione: [github.comM SK1scottgal/stylobot](https://github.com/scottgal/stylobot). Motore viventeM SK1 Armatura dashboard, e controlli commerciali a [stylobot.net](https://www.stylobot.net).