Довгий-сервіси, які працюють, повільно рухаються у напрямку необмеженої пам 'яті . ось як я знаходжу цей рух МSK3 як я думаю про те, щоб його виправити без паперування над ним M SK4 і працюючий приклад з StyloBot ' векторного шару схожості, який переніс його від MSC6 GB на Великому Об' єктному Пагорбі до під МSK7 MB.
Серія випуску StyloBot
- Подібність, Не ідентичність: чому StyloBot моделює клієнтів поведінково
- Подібність-Усвідомлення ASP.NET UI: сервер - віддав поверхню над результатом виявлення
- Винайти та виправити безмежний ріст у довгостроковій-Running M SK1NET Services: дисципліна надійності
- Подібність-Усвідомлення інтерфейсу TypScript: Express , Fastify, і компоненти браузера
- Архітектура Sidecar: як реагуючий двигун підключається до не МSK1NET стеків
- Навчитися швидше: адаптивна система навчання , чотири МSK2 пам 'ять на рівні M SK3 і кэш вердикта
- Випробовуємо те, що не зупиняє нас: контрольна дисципліна : один БДФ файл керує регресією МSK2 завантаженням МСК3 та калібрацією
- StyloExtract - локальний інтерфейс навчання HTML для конвертера Markdown: HTML → Марка-даун, який поєднується з детектором МSK2 walker bug lucidVIEW caught МSK3 and the dogfood loop that made it honest
Вироблений приклад - векторний шар StyloBot,, але дисципліна - це 't Stylo Bot-спеціальнийM SK3 Будь-яка довга МSK4гортання МSK5NET-сервіс, який накопичує стан з потоку, врешті-решт, зростає до класу błędów, які ви можете зафіксувати в тестах чи в коротких завантаженняхMSC7 Він з 'являється лише після днів реального потокуMスク8 Цей пост є підручником, який я використовую для його віднаймки
Модель поведінки Подібність, Не ідентичність; поверхня ASPM SK1NET в Подібність-Усвідомлення ASP.NET UI; джерело на github.comM SK1scottgal/stylobot.
Довгий-сервіси, що працюють, накопичуютьсяM SK1 Кожна кэша , кожний магазин навчання МSK3 кожне " ми M SK5 ми просто будемо зберігати останні N-запити М SK6 буфер - маленький накопичувач . кожен з них виглядає добре сам по собі MSC8 Разом МSK9 в процесі, який МSK10 працював тиждень-на-всьогоден МСК11 вони формують форму пам 'яті, яку жодний тест ніколи не повторює М СК12
Ви спіймаєте це навмисно шукаючи його, про процес, який МSK1 тривав досить довго, щоб форма з 'явилась
Перша частина цієї дисципліни - це: МСК0 , Не технічна . ".". Це МСК2 , календарний запис.
Кожні кілька випусків, Я припиняю додавати додатки і просто дивлюся на працюючу систему під реальним рухомM SK1 запитуючи чи щось із цього виглядає неправильно? Ніяких конкретних помилок, немає метиM SK1 Просто дивлячись. Работі системи мовчазно лежать ; вони не втрачають звуку МSK4 не втручаються аж до того, як вони катастрофічно втрачатимуть звуку
Найновіша перевірка виявила błąd, що ця стаття про: Mostlylucid.BotDetection.Demo процес, який сидить на 20 жителі Великобританії, встановленному під синтетичною тестовою траєкторією . Таке число МSK2 не повинно пережити тридцять секунд уваги
Правило: ставить повторюваний відлік на календарі, щоб читати власні метрики. Нічого не виправитиM SK1 Просто подивитися
Правильний інструмент для " є дещо неправильним ?" на МSK2NET-процесі dotnet-counters. Це МSK1 це безкоштовне МSK2 це ' це вже вбудовано СМСК4 і це говорить вам правду про те, що ваш процес робить зараз
Кілька команд пізніше на demo 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
Це достатньо, щоб знати, що не так, не читаючи коду. Величезний об 'єкт був на 13.4 ГБ, і зростав майже МSK1 МБ МSK2 секунда . Щось постійно розподіляло величезні об 'єкти мSK4, а ген M2 GC МСК6 не міг піднятися з ними ммSK7
Великий кусок об 'єктів - це одна з найпоширеніших несподіванок, коли ви вперше застосовуєте довгу-програму M SK1NET-сервіс. ЦеMSC3 варте переробки, бо майже кожен зустрічається з ним по-важкому :
Найбільші причини випадкового росту LOH у реальних послугах .NETM SK1 з мого досвіду:
| Źródło | Чому він опинився на ЛOH МSK2 |
|---|---|
JsonSerializer.Serialize(obj) повертаючи string |
Об 'єкт МSK1 МБ стає МSK2 Мб UTF string -16 string , все в одній долиці |
MemoryStream ви дозволяєте рости безмежно |
Внутрішній буфер подвоюється за МSK1 КБ і залишається на LOH |
byte[] buf = new byte[n] для великих n |
пряме виділення LOH ; дуже поширено в файлі |
List<T> що росте за минулим ~10 K відмітка МSK1 надруковані речі МSK2 Крести опорної массиви 85 КБ і приземлиться на ЛОХ |
|
string.Concat / StringBuilder над великим текстом |
Финал ToString() є одним величезним розподілом |
XmlSerializer / DataContractSerializer великих графів |
Те ж саме, що і в випадку з JSON |
Якщо ви пам 'ятаєте лише одне правило: бути підозрюваними до будь-якого кодового шляху, який виробляє єдиний супутний великий об 'єкт на таймері або за запит. Те, що "'" - це форма, яка руйнує довгі services , а МSK2 і MSК3 є точною формою, яку ми МСК4 збираємося знайти в 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
інструменти, які допомагають його діагностувати:
dotnet-counters для живих чисел (показчик LOH вище МSK1
dotnet-gcdump і dotnet-dump для зйомок, які можна відкрити в Visual Studio або PerfView.
PerfView для відстеження розподілу до стопки.
Профильери JetBrains CLI ( те, що я насправді використовував для цієї переробки ). Зараз упакований як правильний МSK2 NET глобальні інструменти МSK3 так безголовний МСК4 віддалений MСК5 CI розробка пробігу часу не потребує вказівного інтерфейсу gui
JetBrains.dotMemory.GlobalTools: під 'єднати , зробити знімок з пам' яті МSK2 відкрити .dmw робочий простір пізніше, щоб побачити, які залишені коріння тримають LOH розподіли.JetBrains.dotTrace.GlobalTools: одна і та сама форма для зразку МSK1 відображання МSK2 дані про часову лінію . Використовуйте це, коли вам потрібна стопка, а не шкала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
Розділ праці на цю переробку: dotnet-counters сказав мені, що проблема в тому, що LOH
Є шлюпки для порятунку, якщо вам їх дійсно потрібно. GCSettings.LargeObjectHeapCompactionMode = CompactOnce змушує потиснути LOH на один - RecyclableMemoryStream від буферів Microsoft для того, щоб уникнути розподілу в першу чергу. Обидві штуковиниM SK1 Справжнє рішення - майже завжди припинити виробляти гігантський об 'єкт в першу очередь.
Отже, коли ви бачите постійний ріст LOH у довгостроковій-працевлаштуванніM SK1, питання майже завжди таке ж що' це розподіляє дуже великі об 'єкти , на яку каденцію Форма відповіді (timerM SK1 обладнатель запитів? сеrializerMSC3 буферний фондМSK4 вказує вам, куди подивитися в коді
Винятки також мали значення. 4/sec від SqliteException low enough to not show in logs anyone reads, but high enough to tell you a code path is silently retrying
Правило: коли щось виглядає не так, підтвердити з dotnet-counters перед припущенням. Числа зменшать радіус пошуку за порядок величиниM SK1
Ось де стає цікавою дисципліну: МСК0 , тому що спокуса ",", always , зламка симптом, а не питання абстракції.
Гнучкий vocab ( для наступних секційM SK1
- Вектор: фіксованийM SK1масштабний ряд плавників - eMSC3g.
float[64]each slot is a measured property of the request ( header count , timing МSK3 IP familyfloat[].- Схожість вектора: зазвичай Схожість косину - косину куту між двома векторами . Близьше МSK2 МSK3 більш схожеM SK4 Фактична математика - це дотиковий продукт, поділений двома нормами; один метод під назвоюMSC6
- Приблизний найближчий сусід (ANNM SK1МSK0 " беручи до уваги цей вектор МSK2 знаходити найближче N з мільйонів , швидкіше M SK4 Brute - порівнює силу OMSC6NM SK7 за запитом MSC8 алгоритми АNN отримують результат на менше ніж МСК9 мілісекунд за рахунок того, що іноді не вистачає правда найближчий матч.
- HNSW: один із тих алгоритмів ANN . Він створює шаровий графік, де верхній шар має декілька зразків МSK2 з 'єднані вузли та нижні шари заповнюють деталями M SK3 Ви вводите вгору і збільшуєте зображення MSC4 Чудово для індексування великого , стабільний набір векторів. Не розроблений для M SK1 кожна просьба додає один, а старі зникають " МSK3 відповідний момент цілого поста
- Центроїди: середній показник групи векторів - один вектор, який підсумовує кластер МSK2 Якщо у вас є МSK3 вектори, які всі виглядають майже однаково \ , ви можете відкинути їх і залишити один центроід \ МSK5 центроид є поганим, але компактним замінником . \ Сокремлення використовують це, щоб перетворити велику кількість сирої історії на маленьку історію підсумку \
Для StyloBot, ріст LOH спричинив три класи, які були структурно ідентичними : в процесіM SK2 HNSW (Hierarchічний навігаційний маленький світM SK1 графи МSK2оригінальний Мальков & Яшунин), що використовується для пошуку схожості над підписом
private readonly List<float[]> _graphVectors = new();
Кожна з них була нагоджена вчителем, який підписувався LearningEventType.FullDetection,, що реагує на кожен HTTP запит. Кожна просьба додала вектору . Кожня просьма МSK2 Ніхто не виїхав
І кожні п 'ять хвилин - МСК0 - автоматичний таймер відтворює весь графік на JSON і записує його на диск - МSK1
private readonly TimeSpan AutoSaveInterval = TimeSpan.FromMinutes(5);
На рівні демонстрації, signatures.vectors.json був 104 MBM SK1 В produkcji: intent.meta.json на 70 MBM SK1 intent.vectors.json на 51 MBM SK1 Серіалізація JSON створює континуальну струну в пам 'яті на ♫ 100+ ♫ MB ♫ МSK3 ♫ Ця струна йде прямо до LOH ♫. ♫ Три індекси ♫МSK5 ♫ кожні п' ять хвилин ♫
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
Тепер : легким рішенням є додавати капулу . MaxVectors = 10_000, Виселення з ЛРУ МSK1 корабель це . Це було б спрацювало МСК3 Номери були б нижче MСК4 панель обкладинки виглядала б непогано М СК5 Це також було б неправильним M СК6
HNSW - це чудова технологія, і я використовую її навмисно в інших місцях.Self-Схований векторні бази з Qdrant, Гібридні пошуки та індексування RAG, Найменше життєздатний граф РАГ). Pinecone, Вов 'язати, pgvector, і Кдрант всі користуються нею всередині. МСК0, але це - индекс корпусу. МSK2, а не кэш. ,, і те, чого насправді потребував StyloBot - кэш " для кожного активного відбитка пальців , тримайте маленьке вікно недавніх векторів поведінки, щоб детекція могла порівняти теперішній запит з минулим наслідком схожих Ті ж самі відбитки пальців Bot повторюються. (тримати гаряче ); людські відбити пальця не мають Так просто, як це можливо пост, який вміщував в ” - ” процес Індекс HNSW як функцію “ ; ” цей пост є наступним ” МSK2 ”, визнаючи, де це неправильне ”
МSK0 - це швидкий тест на запах. МSK1 - якщо уявити, що ви кладете тверду кришку на конструкцію. , - це результат, який, семантично, є тим, що вам хотілося.
Правило: коли ви бачите безмежний ріст, " Яким чином ми покриємо цю структуру ?" але "" МСК0 - це правильна структура для того, що ми робимо "". Перший приховує симптоми. Другий слухає, що він вам говорить.
Як тільки ви написали неправильну абстракцію, ,, замінник зазвичай пише сам себе. Для StyloBot це були два шари.
Горячий шар: а обмежена BoundedVectorCache<TEntry>, тонкий рюкзак навколо ConcurrentDictionary з доступом- виведення частоти priorytetуM SK1 Шордер затримки дає Botу - класифікованим записам MSC3x вагу виживання , так що self-キャッシュ МSK5 організовується навколо шаблону руху M SK6
retentionScorer: (_, entry) => entry.WasBot ? 2.0 : 1.0
Утримований шар (FOSS): три нові таблиці SQLite (signature_centroids, session_centroids, intent_centroids) зберігання стислених центродів з нічного VectorCompactionService. Вектори зберігаються як сирові плавники МSK1 бульбашки, використовуючи MemoryMarshal.AsBytes:
internal static byte[] PackFloats(float[] v) =>
MemoryMarshal.AsBytes(v.AsSpan()).ToArray();
Компактна бінарна сеrialізація. Ні 100+ МБ струни JSONM SK2 Ні LOH.
Запис на рівні зберігання. SQLite - це FOSS / один - бінарний заголовок МSK2 правильний дзвінок для МSK3 кидати exe на Pi і йти геть \ ." Коммерційний StyloBot використовує PostgreSQL з pgvector, дасть вам правильний індексований пошук схожості всередині бази даних - на стабільному корпусі , там, де належить HNSW МSK2 горизонтальна скала M SK3 і спільний стан по всьому флоту sqlite-vss є кроком вперед, якщо ви переростаєте SQLite, але хочете залишатися одним.
Поиск схожості на первісному шарі FOSS - brute-force cosine over all rows, accelerated with SIMD M SK2System.Numerics.Tensors.TensorPrimitives). На стисненому МSK1центрoidному масштабі МSK2 з сотнями до декількох тисяч рядків ), повний скан займає \ ~1-2 \ ms на Пі \ МSK5 , що ' \ є хорошим \ : \ , це працює лише в фонових обладначів
Запис на L1/LM SK1 компактація, з того часу, як МSK1комплікація МSK2 робить реальну роботу в цій секції . Комплікаційна служба працює щоночі і зменшує історію в два проходи
Це одна і та сама модель LSM-Tree Storage Engines M SK1RocksDB, LevelDB , CassandraMSC4 Use for SSTablesMska5 level Мska6 holds recent fineMSKA7grained dataM Ska8 lower levels hold compactedMска9 summarised data Mska10 borrowing it works because the access pattern is the sameMске11 most reads hit recent dataМSK12 a small fraction of readings need the long tailMske13 nothing benefits from keeping every raw row foreverMsek14
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
Ogólnа форма (закритий гарячий кэш для стабільного стану + компактний постійний склад для історії + періодичний компактор між ними) з 'являється знову і знову в довгих - функціональних системах навчання МSK2 Це МSK3 варте того, щоб ви зберігали його у вашому наборі інструментів M SK4 Коли хтось звертається до HNSW або pgvector для навантаження на роботу, що – МSK5 – це насправді кэш , це дешевша MSC7 простіша річ, яку він мав би досягнути замість цього MST8
Правило: віддають перевагу найпростішій структурі даних, яка підходить модель часу бігу, не той, що підходить до набору даних ' розмір на папері
Чудовий: МСК0 , тому що "'" має на меті стриманість, а не додавання .
Якщо повільний-фіксний шлях включає в себе пошук бази даних ,, то є спокуса зробити його блоком швидкого шляхуMSC2 DonM SK3t. Швидкий шлях виявлення у StyloBot синхроннийМSK5 і перевірка подібності не є пошуком словника, що блокує МSK6
if (!_cache.TryGet(signatureId, out var entry))
return null; // no signal this request - other 48 detectors still run
Помилка означає не схожість сигнал цього запиту. інші детектори все ще запускаються . Працівник, що керує базами даних, запитує SQLite після закінчення запиту і нагріває кэш на наступний раз
Це те місце, де більшість Caching іде не так. : люди будують Cache , роблять зворотній спір синхронний МSK2 для правильності M SK3 і шлях пропущення стає МSK4 ms p мSK5 піком \ . Cache мав на меті зробити речі швидшими \ МSK7 замість цього він робив найгірший випадок гіршим .
Правило: якщо ви можете' не терпіти пропущення , ви не маєте
Як тільки ви знаходите одну безмежну структуру, уявіть, що є інші.
Аудит є механічним. що визначає його розмір - МСК0, хто вирішує, колиဝင်ки залишаться - МSK1, а що - ' - найгірший випадок під ворожим рухом? Якщо ви не можете відповісти на всі три запитання в одному реченні, Ефемеральні сигнали модель: будь-що, що накопичується, має зруйнуватися
Для StyloBot, більшість акумуляторів вже були добрими
EphemeralPatternReputationCache: жорстка капелька на МSK1 записах з розпадом фону і виведенням LRU МSK2механіка розпаду , Neutral → Suspect → ConfirmedBad станова машина, і асимметрична істеріза живуть в Навчитися швидше)BehavioralPatternAnalyzer: IMemoryCache з перM SK1limiдами ідентичностіDriftDetectionHandlerМSK0 10,000 зразкиSessionEscalationServiceМСК0 35- Minuтний ТТЛ з таймеромМSK2виведення за кермомПотрібно було звернути увагу на інші класи HNSW: MarkovTracker._cohortBaselines. Марковський ланцюг відслідковувач зберігає базові матриці переходу в коhorті per-, separate from per -signature chainsM SK3 which already had LRU eviction at MaxTrackedSignatures). Початкові показники коhorти МSK1 один на коhorту руху, як МSK2 дата-центр - новий МиSK4 або миSK5 житловий МіSK6 повернення МСК7 взагалі не було виселення СМСК8 фиксація ЯМСК9 виселити найхолодніший коhorт ІМСК10 найменший об 'єм переходів УМСК11 коли словник перевершує SelfMaintenanceOptions.MarkovCohortSize.
Правило: Кожна колекція на один тон відповідає на три запитання, або: МСК0 - це баг , : - це те, що його об 'єднує ,
Наступний режим непрацездатності після "без прив 'язкуM SK1 - "зав' язаний, що МSK3 неправильний для цієї аппаратурії MaxEntries = 10_000 все гаразд, доки хтось не запустить вашу службу на Пі4, або в контейнері з M SK1 MB.
Для StyloBot, кожне обмеження приземлилося під одним SelfMaintenanceOptions заблокувати appsettings.json. Погані варіанти працюють для стандартного сервера . Для обмеженого обладнання там LowMemory Статичний пресет:
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;
});
Або через appsettings.json для середовища-спеціальне налаштуванняM SK1
{
"BotDetection": {
"SelfMaintenance": {
"SignatureCacheSize": 1000,
"SessionCacheSize": 500,
"IntentCacheSize": 300,
"CentroidRetentionDays": 14
}
}
}
Глубоша точка – МСК0 – це межа, яка встановлена в одному місці, і її оператори можуть пояснити. const int заяви - це те, про що ніхто не може подумати.
Правило: кожен кордон оператора міг би хотіти змінити для свого обладнання життя в одному блокі конфігурації, не розсіяний через кодову базуM SK1
Для StyloBot особливо (FOSS build, LowMemory presetM SK2
| Komponenт МSK1 Перед МSK2 Після | |
|---|---|
| Індекс HNSW для підпису МSK1 Необмежений LOH МSK2 ~256 КБ гарячий кэш мSK4 | |
| Індекс сеансу HNSW МSK1 Необмежений LOH МSK2 ~258 КБ гарячий кэш M | |
| Індекс інтенції HNSW | |
| Бюфери автозаощадження JSON | |
| Марковські базові лінії Коhorти МSK1 Необмежений МSK2 ~1 MB | |
| Вкупний векторний шар МSK1 13+ ГБЛО | <6 MB |
Модель виявлення незмінна. Що змінилося? де існують докази схожості і коли ' дозволяє впливати на швидкий шлях. Centroids survive restarts in SQLite (FOSS) or PostgresM SK3pgvector
На Пі4 з пресетом LowMemoryM SK1 будування FOSS сидить під 500 МБ RSS після нагрівання , неодмінно M SK4 платна постгресса МSK5 закріплені депозиції успадковуються так само гаряче MSC6 касетна дисципліна ; постійний шар просто масштабується горизонтально, замість того, щоб жити поруч з процесом
Доdanie капсуни обмежує пам 'ять, не змінюючи архітектуру. МСК0. Неправильна абстракція залишається неправильним. МSK1. Симптом просто затихає. .. Наступна людина, яка торкається системи, успадкує структуру, яка майже працює. ММК3. Вона гірша за ту, що очевидно не працює.
Модель, яка повторюється у довготривалих системах навчання
constsУправляйте формою, не симптомомM SK1
Серія випуску StyloBot
HNSW і векторний пошук у цьому блогі
M, ef_construct, налаштуванняarray_cosine_distance проти array_cosine_similarity)Внешні показники
dotnet-counters довідка: знаряддя, що покрило поверхню LOH в першу чергуИсточник implementу: github.comM SK1scottgal/stylobot. Живий двигун , панель управления МSK2 та коммерціальні прилади stylobot.net.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.