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
Monday, 01 June 2026
långvarig service tenderar att sakta drifta mot ett ogränsat minne. Här är det. Detta är hur jag ser den driften. Det här är vad jag tänker på att fixa utan att skriva på den. Det är ett fungerande exempel från StyloBot. Masket av vektors likhet som tog den från GB på Big Object Heap till under MB.
StyloBot-utsläppsserien
- Behavior, Inte Identitet: varför StyloBot modellerar klienter beteendemässigt
- Behavior-Är medveten ASP.NET UI: servern - skickade ut ytan över det upptäcktsresultatet
- Att hitta och lösa gränslös tillväxt i långtids-, -- och / eller körande- -.-nätverk: tillförlitlighetsdisciplinen ( fungerande exempel
- Behavior-Akvis typscript-UI: Express , Fastify, och webbladerkomponenter
- Sidecar-arkitekturen: hur detektoren kopplas till icke--.NET-skivor
- Att lära sig bli snabbare: adaptiv inlärningssystem, ,, fyra, M SK2, övningsminnet, ,, och bedömningsキャッシュen
- Att testa det som inte stannar kvar: kontrolldisciplinen : en BDF-fielddriver regression M SK2 belastning , och kalibrering
- StyloExtract - en lokal lärande HTML omvandlare till Markdown: HTML, →, Markdown-lagret som paras med detektorn, ,, walkersbug lucidVIEW som fångade, M SK3, och den hundföda-loopan som gjorde det ärligt
Det fungerade exemplet är StyloBots vektorlayer, , men disciplinen är: ' t Stylobot, -, specifikt, M SK3, Allt långt fungerande -, NET-tjänster som samlar upp tillstånd från trafiken växer till en klass av bugs du kan ta tag i.
Behavioralmodellen finns i Behavior, Inte Identitet; ASP.NET ytan i Behavior-Är medveten ASP.NET UI; källa på github.com/scottgalM SK2stylobot.
lång tid.-. körde tjänster samlas in. .. varje cache, ,. varje lärandebutik. ,. varje . M SK4. vi, '. vi håller bara de sista N-ansökningarna. Mska6. buffern är en liten samlare. Mske7. Var och en ser bra ut på egen hand. M Ska8. Tillsammans med Mska9. i en process som varit igång i en vecka. M ska11. de skapar ett minne som inget test någonsin återskapar.
Du fångar det här med medvetet att leta efter den, på en process som
Det första stycket i disciplinen är: "'", inte tekniskt, . , det är:
Varje få release , Jag slutar att lägga till egenskaper och tittar bara på det som körs under realistisk trafik, frågar ser något av det här ut fel? Inga specifika bugs, inga mål.
Den senaste recensionen fångade bugs den här postan är om: Mostlylucid.BotDetection.Demo process som sitter vid en GB-resident som ställs under syntetisk testtrafik.
Reglern: sätter en återhämtande lucka på kalendern för att läsa dina egna mätvärden. Att inte fixa något
Det rätta verktyget för " är något fel på ?" dotnet-counters. Det, ', är gratis, ,, det, ', är redan installerat, ,, och den berättar sanningen om vad din process gör just nu.
Några kommandor senare på StyloBot demo:
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
Det är tillräckligt för att veta vad som är fel utan att läsa någon kod än. Den stora objekthaufen var på 13.4 GB och växte på nästan 100 MB/secM SK3 Något allocaterade enorma objekt kontinuerligt
Den stora objektmasken är en av de vanligaste överraskningar när du först profilerar en lång service.
De största orsakerna till oavsiktlig LOH-ökning i verkliga .NET-tjänster
| källa ♫ | ♫ Varför hamnar det på LOH ♫ | ||||||
|---|---|---|---|---|---|---|---|
JsonSerializer.Serialize(obj) återvänder en string |
Ett ♫ 100 ♫ objekt blir ett ♫200 ♫ MB UTF ♫ | ||||||
MemoryStream du låter växa obegränsad |
Internal buffer dubbleras förbi 85 KB och stannar på LOH | ||||||
byte[] buf = new byte[n] för stora n |
Direkt LOH-alokation | ||||||
List<T> som växer förbi ~10 Kreferens |
- | skriftippade föremål | ryggradsskivan korsar ♫ 85 | KB och landar på LOH | МSK4 | ||
string.Concat / StringBuilder över större text |
slutligen ToString() är en enda enorm fördelning |
||||||
XmlSerializer / DataContractSerializer av stora diagram |
Samma form som JSON fallet |
Om du bara minns ett tumregler: vara misstänksam mot vilken kodsträng som producerar ett enskilt, sammanhängande stort objekt på en timer eller per begäran. Att "'" är den form som förstör långa tjänster, och det är precis den form vi kommer att hitta i StyloBot.
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 >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
verktyg som hjälper att diagnostisera det:
dotnet-counters för riktiga siffror (LOH-mätaren ovanför ).
dotnet-gcdump och dotnet-dump för kortbilder som du kan öppna i Visual Studio eller PerfView.
PerfView själv för att spåra fördelningarna ner till en stapel.
JetBrains CLI profiler ( vad jag faktiskt använde för den här omställningen
JetBrains.dotMemory.GlobalTools: ansluta , ta ett minnessnapshot , öppna .dmw arbetsyta senare för att se vilka bevarade rötter som håller LOH-aloktionerna.JetBrains.dotTrace.GlobalTools: samma form för att ta prover | / | spårning ♫ / | data från tidsramen ♫ МSK3 ♫ Använd detta när du behöver en stapel istället för en mätare ♫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
Tilldelning av arbetskraft för denna omställning dotnet-counters berättade för mig att LOH var problemet.
Det finns flykthäckar om du verkligen behöver dem. GCSettings.LargeObjectHeapCompactionMode = CompactOnce tvingar en one-off LOH kompaktion RecyclableMemoryStream från Microsoft pools buffer för att undvika fördelningen på första plats. Båda är plaster. Det verkliga lösningen är nästan alltid att sluta producera det gigantiska objektet på första Stelle
Så när du ser en stabil LOH-ökning i en lång service, så är frågan nästan alltid densamma. vad "'" innebär att man ska fördela väldigt stora objekt på vilken tidpunkt, , och varför Svarets form (timer? begäranbehandlare ? serialisator M SK3 bufferpool ?) talar om var man ska titta i kod
De undantagliga motsvarigheterna spelade också någon roll 4/sec av SqliteException är tillräckligt låg för att inte dyka upp i journaler någon läser, men tillräckligt hög för att säga att en kodpfad tyst testar om igen.
Reglern: när något ser fel ut, bekräfta med dotnet-counters före att gissa. siffrorna kommer att förväxla sökradiet med en storleksordning
Det är här som disciplinen blir intressant, eftersom frestelsen fläcka symptomet snarare än att ifrågasätta abstraktionen
Quick vocab ( för de kommande sektionernaM SK1
- Vektor: ett fixerat array av float
float[64]. Varje lucka är en mätt egenskap av begäran. ( siffransräkning, , timing, M SK3 IP-familjen, ,, etc., .).. Två begäran som МSK6, som känns lika, MSC7, producerar vektorer som är nära varandra i den dimensionella utrymmet.float[].- Vektors likhet: vanligtvis kosine likhet - kosinusen av vinkeln mellan två vektorer . närmare 1.0 = mer liknär . Den faktiska matematiken är en punktprodukt som är delat av två normer
- Ungefär närmaste granne (ANNM SK1: "given den här vektorn, hittar den närmaste N av miljonerM SK3 snabbtMSC4 Brutto-- krafts jämförelse är O~(NM SK7~ per query~ .~ ANN-algoritmer får sub--~millisekunderresultat till kostnaden att ibland missa sant närmaste match.
- HNSW: en av dessa ANN-algoritmer . Den bygger en lagerad graf där den övre skivan har ett fåtal brunn M SK2 sammankopplade noder och lägre skivor fyller i detaljer . Du går in på toppen och zoomar in stabila rader vektorer. inte designad för " varenda begäran lägger till en och gamla försvinner " | | - den relevanta punkten i hela det här meddelandet
- Centroider: genomsnittet i en grupp av vektorer ♫ - ♫ en vektor som summerar klustern ♫. ♫ Om du har ♫ 10,000 ♫ vektorr som alla ser ungefär likadana ut ♫ МSK4 ♫ så kan du kasta bort dem och behålla ett centroid ♫
För StyloBot ledde LOH-ökningen till tre klassier som var strukturellt identiska HNSW (Hierarchisk navigerad liten världoriginal Malkov & Yashunins pappers) som används för likhetssökningar över undertecknande , sessie M SK2 och intentionsvectorn . Var och en hade ett fält som
private readonly List<float[]> _graphVectors = new();
Var och en fick hjälp av en lärande handledare som LearningEventType.FullDetection, som aktiveras varje HTTP- begäran. Varje begäran satte till sig en vektor . varje begäran . Det fanns ingen utfånganing
Och var femte minut, ,, serialiserade en själv sparande timer hela grafen till JSON och skrev den på disket.
private readonly TimeSpan AutoSaveInterval = TimeSpan.FromMinutes(5);
På demoskala, signatures.vectors.json var 104 MB intent.meta.json på 70 MBM SK1 intent.vectors.json vid 51 MBM SK1 JSON serialisering skapar en sammanhängande raden i minnet vid ♫ 100+ ♫ MB ♫. ♫ Den raden går direkt till LOH ♫
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<float[]><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
Nu är det enkelt att lägga till ett kapsyl MaxVectors = 10_000, LRU utfånganing , skicka iväg det . Det skulle ha fungerat, . siffrorna skulle ha kommit ner, , brädan skulle se bra ut, M SK5 Det hade också varit fel
HNSW är en utmärkt teknik och jag använder den medvetet på andra ställen.Self-Hosted Vector Databases with Qdrant, RAG Hybridsökning och indexering, Minimalt tänkbara grafRAG). Pinecone, Veva, pgvector, och Qdrant alla använder den internt, men det är en indeks av ett corpus, inte ett kass, och vad StyloBot faktiskt behövde var en kass. " för varje aktiv fingeravtryck , spara ett litet fönster av nyliga beteendevektorer så att spårning kan jämföra den nuvarande begäran mot det förflutna beteendet från liknande Bot fingerprints repeti (stay hot); human fingerprint donM SK2t (evict Så enkel som möjligt post listade inprocessen HNSW-indexet som en egenskap
Det finns ett snabbt prov på lukten, : om du föreställer dig att sätta en hård kapsyl på konstruktionen. , är resultatet fortfarande semantiskt det du ville ha.
Reglern: när man ser en gränslös tillväxt, så är frågan inte " hur kan vi täcka den här strukturen men " är den rätta strukturen för vad vi gör Den första gömmer symptomet. Den andra hör vad det säger till dig.
När du fått den felaktiga abstraktionen, ' och , så skriver ert substituerande vanligtvis upp sig själv.
Heiß lager: en begränsad BoundedVectorCache<TEntry>, en tunn förpackning runt ConcurrentDictionary med tillgång.
retentionScorer: (_, entry) => entry.WasBot ? 2.0 : 1.0
Hållbar lager (FOSS): tre nya SQLitetablar (signature_centroids, session_centroids, intent_centroids) lagring av komprimerade centroider från en nattlig VectorCompactionService. Vector som lagras som råfloat MemoryMarshal.AsBytes:
internal static byte[] PackFloats(float[] v) =>
MemoryMarshal.AsBytes(v.AsSpan()).ToArray();
Kompakt binär serialisering. Nej 100+ MB JSON-strängarM SK2 Nej LOHMSC3
Storage tier note. SQLite är den FOSS-versionen / eninlägsen - det binära bakvändet | | - | det rätta samtalet för | " | släppa exet på Pi och gå iväg | МSK4 | Commercial StyloBot använder PostgreSQL med pgvector, ger dig rätt indexerade likheterssökningar inuti databasen - på ett stabilt korpus , där HNSW tillhör M SK2 horisontell skala, , och gemensamt tillstånd över en flod, sqlite-vss är en startplötslig punkt om du vill bli bättre på SQLite men vill vara enskild.
Sammalikhetssökningen på den FOSS-härdliga lagern är brute-force cosine över alla raderSystem.Numerics.Tensors.TensorPrimitives). På kompakta, -, centraliserade skalor, (, hundratusen till ett par tusen rader och ),, tar en full skanning ~1-2, ms på en Pi,
En anteckning om Compaction:, eftersom "kompakationen ♫ ♫ " ♫ gör verkligt arbete i den här sektionen ♫ . ♫ Kompaktionsservicen kör nattligt och reducerar historien på två gång ♫
Det här är samma mönster LSM-trädslagsmotorer | ( | RocksDB |, | LevelDB ♫ , | Cassandra | ) | användar för SSTables | МSK5 | nivå | 0 | håller nygranna data |- | strukturerade data ♫ ; | lägre nivåer håller kompakta data ♪ , | sammanfattade data \ . | att låna det fungerar eftersom tillgångsmönstret är samma | : | de flesta läsarna träffar nydata ♪ ; | en liten bråkdel av läsarna behöver den långa stapeln | SMk13 | inget gynnar från att hålla varje rå rad för alltid |
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
Den generella formen (bounded hot cache for the steady state + compact persistent store for history + a periodic compactor between them) dyker upp om och om igen i långa, fungerande lärande system. . Det är värt att hålla det i sin verktygslåda.. När någon tar sig fram till HNSW eller pgvector för en arbetslast som ' i själva verket är ett kass.
Reglern: föredrar den enklaste datastrukturen som passar starttidsmönster, inte den som passar in i datauppsättningen
Ett subtilt: ,, eftersom det handlar om tystnad snarare än addition.
Om den långsamma-fix-stigen inbegriper en databassuppsökning, är frestelsen att blockera den snabba vägen på den . DonMSC3 tM SK4 Detektorns snabba bana i StyloBot är synkroniseradMska5 och likhetskontrollen är en icke-blockerande ordbokuppsökning
if (!_cache.TryGet(signatureId, out var entry))
return null; // no signal this request - other 48 detectors still run
En miss innebär ingen likhetsignal för den här begäran. De andra detektörerna fortsätter att fungera . En bakgrundsbehandlare frågar SQLite efter att beställningen tagit slut och värmer upp cachet till nästa gång
Det är här som de flesta kackersystem går fel på.: människor bygger upp en kacker, ,, gör fallbacken synkroniserad, ", för korrekthet, ",, och missstigen blir en "50", mps p, msp5, spike, M SK6. Cachet var tänkt att göra saker snabbare, ;, men istället gjorde det värsta fallet värre.
Reglern: om du kan' inte tolerera en miss
När du har funnit en ogränsad struktur så antar du att det finns andra. Den högljudda är den som bröt först.
Den är mekanisk. vad gränsar dess storlek, ,, vem bestämmer när inskrywingerna lämnar, M SK1, och vad som är det värsta fallet under fiendtrafiken? Om du kan svara på alla tre i en mening Äphemerala signaler modell: allt som samlas måste förrutsna , utfånga, , eller kompakta
För StyloBot, var de flesta akkumulatorer redan bra-begränsadeM SK2
EphemeralPatternReputationCache: hårda kapsyln vid 10,000 siffran med bakgrundsdeka och LRU-utskrävning Neutral → Suspect → ConfirmedBad statmaskin, och asymmetrisk hysteresi lever i Att lära sig bli snabbare)BehavioralPatternAnalyzer: IMemoryCache med per-identitetsgränser (50 vägarDriftDetectionHandler: 10,000 mönsterSessionEscalationService: 35-minuter TTL med timer-driven bortföringEn sak som behövdes uppmärksamhet bortom HNSW-klassen MarkovTracker._cohortBaselines. Den Markovkedjan spåraren håller per-kohorts baseline-transition matriser M SK1separade från per -signaturkedjorn, som redan hade LRU utfångas vid MaxTrackedSignatures). Kohortens baseliner | ( | en per trafikkohort som |" | datacenter | - | ny | МSK4 | eller |" | beboestad ♫ - | återvänd ♫ SelfMaintenanceOptions.MarkovCohortSize.
Reglern: varenda enton-samtal besvarar tre frågor eller så: ' är en bug, what bounds it, what evicts it, ,, what,', what is its worst?
Nästa avbrottsmodi efter "no bound" är "bound som ' inte stämmer för den här hårdvaran MaxEntries = 10_000 är okej tills någon driver din tjänst på en Pi4, eller i en behållare med M SK1 MB.
För StyloBot, landade varje gräns under en enda SelfMaintenanceOptions blockera in appsettings.json. Standardstandarden fungerar för en standardserver LowMemory statisk preset:
public static SelfMaintenanceOptions LowMemory => new()
{
SignatureCacheSize = 1_000,
SessionCacheSize = 500,
IntentCacheSize = 300,
MarkovCohortSize = 2_000,
CacheSlidingExpiration = TimeSpan.FromHours(1),
};
builder.Services.AddBotDetection(opts =>
{
opts.SelfMaintenance = SelfMaintenanceOptions.LowMemory;
});
Eller via appsettings.json för miljön-specifik tunning:
{
"BotDetection": {
"SelfMaintenance": {
"SignatureCacheSize": 1000,
"SessionCacheSize": 500,
"IntentCacheSize": 300,
"CentroidRetentionDays": 14
}
}
}
Den djupare punkten, :, en gräns som sets på ett ställe är något som operatörer kan resonera med. const int deklarationer är något som ingen kan resonera med.
Reglern: varje gräns som operatören kanske vill ändra för sina hårdvara-liv i en konfigurationsblock
Speciellt för StyloBot (FOSS build, LowMemory preset
| Komponent | Innan | Efter | |
|---|---|---|---|
| Handskrift HNSW-index ♫ | ♫ Ogränsad LOH ♫ МSK2 ♫ ♫ | ||
| Sessions HNSW-index | Ogränsad LOH ♫ ♫ | ♫ | |
| Intens HNSW-index | Ogränsad LOH | ||
| JSON-autosave buffer | 100-500 MB LOH varje 5 min | ||
| Markov-kohorts baseliner | Ogränsad | ||
| Hela vektorskivan | 13+ GB LOH | <6 MB |
Detektormodellen är oföränderlig. Vad som ändrats är där likheterna finns och när det tillåts att påverka den snabba vägen. Centroider överlever återuppstartar i SQLite (FOSS) eller PostgresM SK3pgvector | (paid |); | nattlig kompaktion producerar fortfarande L | L
På en Pi4 med LowMemory preset , placerar den FOSS-byggnaden under 500 MB RSS efter uppvärmning M SK3 oändligt . Belönad Postgres MSC5 stödda utpostningar ärvt samma varmhet Mska6 kassdiskriminering Mske7 den bestående lagern skalas horisontellt istället för att bo bredvid processen M Ska8 på vilket sätt som helst Msaka9 förutsägbar stabiltMska10 tillståndets omlopp M ska11 nått efter upplärmningen Mcka12 oberoende av upptime eller trafiken Mikka13
Att lägga till en kapsyl begränsar minnet utan att ändra arkitekturen.
Pattern som återhämtar sig i långa system
constsFixera formen, inte symptomet.
StyloBot-utsläpp serie
HNSW och vektorsökning i den här bloggen
M, ef_construct, tunningarray_cosine_distance mot array_cosine_similarity)Externa referenser
dotnet-counters referensram: redskapet som först nådde upp LOH: nkällan till implementeringen: github.com/scottgalM SK2stylobot. Livmotorn , brädan, , och kommersiella kontrollen stylobot.net.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.