TGViewer
Channel Public Channel
dmedovich notes

dmedovich notes

@dmedovich_notes

бегущий за софт скиллами...

Links:
- https://dmedovich.com
- https://github.com/dmedovich

не кусаюсь! -> @dmedovich
Subscribers
156
Photos
9
Videos
0
Links
10
Recent Posts 19 shown
Post #30 137
В погоне за ядрами: история про микрооптимизацию хеш-функции

Иногда значительная часть CPU уходит на операцию, которая вообще не выглядит проблемой. Например, системе нужно решить, какой узел должен владеть конкретным ключом.
Это может быть:
- shard;
- cache node;
- worker;
- storage node;
- backend в динамическом пуле;
- узел, которому назначается конкретный объект.

Если узлов несколько — проблем нет. Но если их тысячи или десятки тысяч, выбор владельца сам становится горячей CPU-операцией.

Rendezvous Hashing, или Highest Random Weight (HRW), решает задачу довольно просто. Для каждого узла вычисляется score: score(key, node) а владельцем становится узел с максимальным score: owner(key) = argmax score(key, node). Если узлов N, то для одного ключа нужно вычислить score для всех N узлов. То есть сложность O(N), что на первый взгляд это не очень привлекательно. Но у HRW есть важное свойство: узлы могут иметь произвольные идентификаторы. Не обязательно иметь: 0, 1, 2, 3, ..., N-1.Идентификатором может быть uint64, hash, UUID, публичный ключ или любое другое значение, которое система использует для идентификации узла. Узел можно добавить или удалить, не перестраивая нумерацию остальных. Для многих распределённых систем это очень удобная модель.

А можно ли просто использовать другой алгоритм?
Существуют алгоритмы, которые смотрят на проблему с другой стороны.
- Jump Consistent Hash уменьшает стоимость поиска до O(log N) и использует очень мало памяти. Но его модель предполагает последовательную нумерацию buckets.
- Power Consistent Hash идёт ещё дальше и даёт O(1) ожидаемое время поиска, также работая с плотным диапазоном bucket ID. Это прекрасные решения, когда модель системы позволяет представить узлы как: 0 ... N-1

Но это уже немного другая задача. Если идентификаторы узлов произвольные и разреженные, возникает дополнительный слой: node ID -> dense bucketID. А при динамическом удалении узлов нужно ещё поддерживать соответствие между этими пространствами. В HRW такого слоя нет. Поэтому не пытаемся сделать сделать быстрее O(N), вместо этого пытаемся узнать насколько дешевым можно сделать сам O(N)-скан. Так появилась идея VRH.

В чём идея VRH
Пусть:
key — ключ;
node — идентификатор узла;
оба представлены как uint64.
VRH вычисляет score следующим образом:
k  = SplitMix64(key)

x = node XOR k
x += ROTL(k, 23)
x ^= x >> 29
x += ROTL(k, 41)
x ^= x << 17
x ^= x >> 32

score(key, node) = x

Здесь SplitMix64 используется для предварительного смешивания ключа, а дальше выполняется последовательность операций над 64-битным словом. У такой конструкции есть два свойства, которые для этой задачи особенно важны. Score не должен сталкиваться для разных узлов Для фиксированного ключа хочется, чтобы разные узлы давали разные scores. Иначе два разных узла могут получить одинаковый максимум, и результат начнёт зависеть от порядка обхода. У VRH это можно получить алгебраически. Для фиксированного key значение: k = SplitMix64(key) фиксировано. Дальше node проходит через композицию обратимых преобразований.

XOR
x = node XOR k
XOR с фиксированным значением обратим и является своей собственной противоположностью.

Сложение
x += c
Для uint64 это сложение по модулю 2^64. Оно также обратимо: обратная операция — вычитание той же константы по модулю 2^64

XOR со сдвигом
x ^= x >> r и x ^= x << r являются обратимыми xorshift-преобразованиями. Следовательно, вся последовательность является композицией биекций. То есть разные node дают разные score. Это полезное свойство, но биективность не доказывает статистическую равномерность распределения. Она доказывает отсутствие коллизий внутри отображения по node.

Эта формула хорошо ложится на SIMD. При обычном HRW для каждого узла мы делаем примерно одно и то же:
node 1 -> score
node 2 -> score
node 3 -> score
node 4 -> score

Но вычисления между узлами независимы. Нет зависимости вида: score(node[i+1]) зависит от score(node[i]). Значит, их можно выполнять параллельно. AVX2 предоставляет 256-битные регистры. В них помещается: 4 × uint64. Поэтому можно обработать четыре узла одновременно. После этого остаётся только найти максимум. Именно поэтому E19 задумывался как score-функция, удобная для дешёвого линейного скана.

В горячей части вычисления score VRH не использует умножение над node. Основные операции: XOR | ADD | SHIFT | ROTATE | XOR. Для этой конкретной задачи хотелось получить максимально простую последовательность операций, которую можно развернуть сразу на несколько независимых node ID.

Бенчмарки
Контур 1: выбор владельца
Основной эксперимент. Для каждого ключа необходимо вернуть один node: owner(key) Сравнение выполняется с go-rendezvous. Этот benchmark показывает эффект VRH как оптимизации ownership-операции.
Контур 2: полное ранжирование
Здесь измеряется уже другая операция: sort(nodes by score).
Для этого использовался nspcc-dev/hrw/v2.

Методология
Все основные результаты получены на одной машине:
CPU:    AMD Ryzen 5 2600
OS: Linux amd64
Go: 1.27.1

Все прогонялось минимум 10 раз, чтобы результат представлялся медианой

Результаты на картинке(в комментах) и выводы ниже:
Довольно интересно было конечно покопаться, иногда основная проблема находится не в сложности алгоритма, а в стоимости операции, которую мы выполняем N раз.
Сомневаюсь конечно, что когда нибудь довёдется что-то такое руками на больших проектах делать, но в копилочку положить вариант, куда еще можно посмотреть точно стоит. Ну и как коллега у себя это попробует спустя время сделаю ретроспективу результатов, все же это делалось чтобы перестать тратить CPU там, где его можно не тратить. (Ну еще и чтоб SIMD потрогать, а то постов о нем куча везде, а руками трогать негде)

код: https://github.com/dmedovich/VRH
GitHub GitHub - dmedovich/VRH Contribute to dmedovich/VRH development by creating an account on GitHub.
  • 🔥 2
  • 🏆 1
Post #29 189
Прунинг сегментов в Amber: Ribbon, CQF и SuRF 😴

Хватит пока с нас индексов — поговорим немного о фильтрах и о том, где и как они позволяют отсекать максимум сегментов до полного скана.

Сегменты у нас append-only и запечатываются один раз. Запрос проходит по ним каскадом: на каждом шаге можно отсечь сегмент целиком.

Step 1: отсечение по времени

SparseIndex хранит для каждого сегмента [MinTS, MaxTS]. Time-bounded-запрос отсечёт всё, что не пересекается по времени, ещё до открытия индексного файла — достаточно простой проверки метаданных в памяти:
// Lookup returns the segments whose span overlaps [from, to].
func (s *SparseIndex) Lookup(from, to int64) []SegmentTimeRange {
result := make([]SegmentTimeRange, 0)
for _, r := range s.ranges {
if r.MaxTS < from || r.MinTS > to {
continue // сегмент вне диапазона — пропускаем
}
result = append(result, r)
}
return result
}


Step 2: membership-проверка по полю


Здесь уже подключаются sidecar-индексы, а то, какие именно индексы будут построены, зависит от кардинальности поля. level, service, host — поля низкой кардинальности, представленные инвертированными битмапами (MultiFieldIndex).

А вот trace_id из битмапа был исключён. Узнать о причинах можно тут, но если в двух словах: почти каждый trace_id имеет df = 1, а значит, хранить битмап для каждого trace_id попросту невыгодно. Вместо этого используются RibbonFilter и posting list для точных ID, если фильтр вернул ложноположительный результат.

В executor это выглядит как цепочка ранних выходов:
if !model.IsZeroTraceID(q.TraceID) {
if ribbon, ok := e.logRibbon(seg.FileName); ok {
if !ribbon.Contains(q.TraceID[:]) {
return 0, nil // сегмент целиком мимо, дальше не читаем
}
}
if pl, ok := e.logPosting(seg.FileName); ok {
ids := pl.Lookup(q.TraceID[:])
if len(ids) == 0 {
return 0, nil
}
// ...
}
}

Для токенов FTS работает тот же принцип.

Step 3: пропустить декомпрессию там, где сегмент всё равно придётся читать

Зачем читать то, что мы всё равно будем читать при совпадении? Давайте просто пропустим этот этап.

Для trace-summary-запросов (service / operation / duration) есть CoverIndex (.cidx) — колоночная, страйдовая проекция нужных полей в том же отсортированном порядке, что и posting list по service. Она отвечает на агрегирующий запрос без обращения к основному row store.

Все эти sidecar-индексы строятся за один проход при запечатывании сегмента. Изначально Bitmap, FTS, Ribbon и FTSRibbon строились каждый своим отдельным сканом. При пяти независимых декодированиях одних и тех же данных (и особенно при двойных токенизации и стемминге для FTS) это стало узким местом: seal не успевал за ingest.

Сейчас один проход кормит все индексы сразу, а Ribbon для FTS переиспользует токены непосредственно из состояния построения FTS-индекса, без повторной токенизации.

Ribbon Filter

Про Ribbon можно почитать подробнее тут и тут. Тем более что именно его, с небольшой модификацией, я и использую у себя в БД (подкрутил ширину окна — just for my case).

Counting Quotient Filter (CQF)

Ribbon и Bloom по сути умеют только одно: сказать, встречался ключ или нет. CQF идёт чуть дальше — он добавляет счётчик кратности ключа. А еще можно добавлять и удалять записи без полной пересборки.

В чём идея: ключ хешируется и режется на две части — quotient (q бит, индекс слота в массиве) и remainder (r бит, то, что реально хранится в этом слоте):
func (qf *CountingQF) split(key []byte) (q0, r0 uint64) {
h := hashKey(key) & maskBits(qf.q+qf.r)
return h >> qf.r, h & maskBits(qf.r)
}

Дальше — массив слотов размером 2^q, и на каждый слот три метабита вместо привычных Bloom-битов:
- occupied[i] — у quotient i где-то в массиве есть свой run;
- used[i] — слот i физически занят;
- runend[i] — слот i последний в своём run.

При коллизии по q (два разных ключа попали в один и тот же i) элементы физически сдвигаются вправо, формируя непрерывный run — отсортированный по r участок массива:
func (qf *CountingQF) locate(qIdx uint64) uint64 {
if !qf.isUsed(qIdx) {
return qIdx
}
start := qf.findClusterStart(qIdx)
runsBefore := 0
for k := start; k < qIdx; k++ {
if qf.isOccupied(k) {
runsBefore++
}
}
pos := start
for runsBefore > 0 {
for !qf.isRunEnd(pos) {
pos++
}
pos++
runsBefore--
}
return pos
}

locate считает, сколько занятых q находится перед искомым внутри кластера, и проходит мимо ровно такого количества run. Отсюда и требование: run физически никогда не может оказаться левее своего канонического индекса. Иначе locate его просто не найдёт — он ищет только вправо.

Реализовал CQF с двумя упрощениями относительно статьи три метабита на слот вместо двух битов, получаемых через rank/select в RSQF, unary-счётчик вместо escape-кодирования.
В обоих случаях гарантии сохраняютс, структура получается немного дороже по памяти, но проще в реализации и тестах. И если с основными операциями проблем не возникло, то вот с Delete пришлость повозиться, ибо руки из жопы

Succinct Range Filter (SuRF)

Второе ограничение Ribbon — точное совпадение, prefix-поиск отсутствует. Поэтому рассмотрим еще и структуру, которая обещает range-фильтрацию почти по цене обычного membership-фильтра.

SuRF — это сжатое префиксное дерево над отсортированным множеством ключей, усечённое до минимально необходимой глубины.

В чём идея:
- Строим обычный trie по байтам ключей.
- Урезаем каждую ветку в точке, где ключ уже однозначно отличим от соседних: дальше можно ничего не хранить, оставшийся хвост ключа для membership-проверки не нужен.
- Кодируем получившееся дерево в succinct-представлении. Для структуры дерева используем битовые последовательности LOUDS, а метки рёбер храним отдельно — по одному байту на символ. Дальше rank/select позволяют быстро перемещаться по этой компактной структуре.

По памяти получается порядка 10! бит на узел в зависимости от конфигурации, вместо десятков байт на узел в обычном trie с указателями. ! - размер сильно зависит от глубины усечения и того как именно у вас хранятся labels и доп суффикс/хэш биты

Дальше есть три варианта:
SuRF-Base — просто усечённое дерево, без дополнительных гарантий для точечных запросов. Дешёвый вариант, но false positive rate при membership-проверке зависит от того, насколько глубоко пришлось урезать дерево.

SuRF-Hash — добавляет к каждому листу несколько бит хеша полного ключа, чтобы довести false positive rate до нужного уровня для точечных запросов. По сути, это замена Bloom/Ribbon-фильтру.

SuRF-Real — вместо хеша хранит реальные оставшиеся байты ключа. Что делает возможными range-запросы типа: («все ключи между X и Y»), а не только membership-проверку.

У обычного Bloom/Ribbon-фильтра ключи представлены только хешами: порядок и структура исходного ключа полностью теряются. SuRF сохраняет порядок — а значит, в теории может быть одновременно и компактным (как Ribbon), и полезным для диапазонных и префиксных запросов, чего Ribbon не может в принципе, ни при каком бюджете памяти.

Итог:🛑

CQF добавляет возможность менять фильтр после построения.
SuRF добавляет поддержку prefix- и range-запросов. Оба закрывают ограничения Ribbon.

Но для нас это не то чтобы проблема. В Amber сегменты append-only и запечатываются один раз, поэтому после построения их уже не нужно менять. А большая часть полей — короткие структурированные ключи умеренной кардинальности, где точного совпадения вполне достаточно.

Оба фильтра — тяжёлые и сложные, и оба платят за возможности, которых у Ribbon нет, но которые нам, по-хорошему, не особо то и нужны. Возможно SuRF сможет подружиться с FTS, ибо последний работает с токенами, а не с лексикографическим порядком исходных ключей, но пока такого запроса нет, но карандашиком запишу😶‍🌫️😶‍🌫️
  • 🏆 5
  • 🔥 4
Post #27 301
Learned Index, или хоть кто-то умный...
Провёл небольшой эксперимент с индексом в Amber. Изначально поиск пересекающихся временных диапазонов был довольно простым: проходили по всем сегментам и проверяли, пересекается ли каждый из них с нужным диапазоном. Работало супер, но линейно. Первым решением было сделать бинарный интервальный индекс. Но в моменте ресерча я открыл для себя ещё и Learned Index, и понеслась...



В чём идея? А идея очень красивая.
Допустим, у нас есть массив с отсортированными ключами:
key:       10  20  30  40  50  60  70  80  90  100
position: 0 1 2 3 4 5 6 7 8
9
Обычный индекс хранит структуру, по которой мы ищем нужную нам позицию, а наш сегодняшний гость предлагает посмотреть чуть иначе на ситуацию: зачем искать позицию, если можно ее предсказывать?


Моделька учит приближенную зависимоть ключа к позиции, и для любых ключей которые мы ищем модель будет отвечать так: "он где-то вот тут - [x:y]", тобишь мы получаем диапазон где точно есть ответ.


А что есть моделька начнет врать? допустим предсказывает позицию 8, а реальная позиция окажется 10, тут повляется prediction err, который говорит нам следующее - prediction это не точный ответ, а лишь отправная точка для поиска его. Мы должны проверять диапазон вокруг prediction, а не слепо верить что модель его угадала.


По существу в этом и есть вся суть Learning index - сужать поиск как можно сильнее, он не обязан угадывать позицию идеально 10 раз из 10, да и врятли когда-то будет, вся его работа просто как можно сильнее сузить область поиска.


Подробнее про Learning Index
Подробнее про интеграцию в amber и результатов
  • 🔥 7
  • 🏆 1
Post #26 509
honey@lab:~$ cat amber/notes-2

😗😗😗
amber уже хранит логи и трейсы — почти бесплатно, потому что формат данных у них почти одинаковый. И вот сидишь, смотришь на это и думаешь: а не добавить ли ещё и метрики дабы гешталь закрыть. Тогда выходит одна база на все три сигнала. Никаких отдельных Loki, Tempo и Mimir под каждый — поднял один amber, и он хранит всё вместе. Нюанс один: метрики — совсем другой формат данных, а значит, и движок нужен свой.


Но об этом позже. А пока — небольшая рефлексия про mmap и почему понадобился новый индекс для amber.
https://dmedovich.com/posts/mmap-sigbus-amber.html

Ну а в следующих постах хочу сделать серию про метричный движок: как начинался, как проектировался и каких сил стоило засунуть его в amber.

P.s а еще кстати, клиент для IRC Libera.Chat сделал
Dmedovich One mmap for you, or two SIGBUSes for someone else? - Daniil Medovich Why amber rejects mmap in embedded mode: hidden I/O, SIGBUS, fail-stop semantics, and the AFT2 index format.
  • 🔥 5
  • 🏆 3
Post #24 575
honey@lab:~$ cat queen/dev-blog/01

☺️☺️☺️
Queen v0.7.0

Большая обновочка прикатила в мою queen.

Из новых фичей:
🐝tap/ — подсистема для наблюдения за миграциями: аналитика, фильтрация, объяснение и экспорт событий. Отличный локальный помощник для дебага и понимания ваших миграций.
🐝TUI — каюсь, подверг полному рефакторингу, разбил всё на читаемые модули и хендлеры, всё это поверх Bubble Tea.
🐝Также в TUI интегрирована tap-система — результат на видео ниже.
🐝Atomic-операции на уровне ядра королевы — для безопасных апдейтов и предсказуемых откатов.
🐝CLI тоже раздроблен — стал проще для чтения и поддержки.
🐝Доработан импорт goose-миграций в формат queen (пока только sql-like).
🐝Огромное количество интеграционных, unit и пара e2e-тестов — проверяем как можно больше инвариантов.
🐝Большой упор на Postgres — доводим его до prod-ready первым из драйверов.
🐝Появился сайтик с документацией, чтобы было легче стартовать и понимать queen.

Незначительное:
Фиксы и доработка документации по мелким багам в sqlite/clickhouse/cockroachdb.

🔗source:
Более подробный changelog
Документация
Код
  • 🔥 4
  • 🏆 3
Post #22 502
honey@lab:~$ cat amber/dev-blog/01


🏠Bitmap, ribbon, posting list — или как усидеть на трёх стульях и получить максимум от поиска и хранения
Когда я добавлял индексы в amber, trace_id попал в bitmap по умолчанию, я понимал что вешать битмапку на высококардинальныe поле в целом затея весьма плохая, но мне было инетерсно насколько(спойлер очень плохо).
Собственно попытался сделать "лучше", потом понял что "лучше" тоже неполное, и сделал правильно.

С чего все начиналось: bitmap на trace_id
Bitmap-индекс хорошо работает для полей с малым числом уникальных значений. Для level с пятью значениями (DEBUG, INFO, WARN, ERROR, FATAL):

INFO -> [1, 1, 0, 1, 0, ...]
ERROR -> [0, 0, 1, 0, 1, ...]

Пять компактных битмапов, хорошо сжимаются RLE, пересечение двух условий — AND двух битмапов. Дёшево.
Для trace_id ситуация другая. Trace_id — уникальное поле, почти одно значение на запись. Индекс:

a1b2c3d4... -> [1, 0, 0, 0, ...] // один бит на 100К
e5f6a7b8... -> [0, 1, 0, 0, ...]
f9c0d1e2... -> [0, 0, 1, 0, ...]
... (ещё 99 997 таких)

Вместо нескольких компактных битмапов — сотни тысяч почти пустых. Индекс раздувался, плохо сжимался, дорого строился при запечатывании сегмента.
В целом было очевидно, но хотелось на время поставить затычку пока искал более компактную реализацию.


✈️Собственно мы тут: ribbon filter
почитать про ribon filter: part1 part2 part3 part4
Ribbon filter — вероятностная структура (~2 бита на ключ). Отвечает на вопрос "есть ли X в этом сегменте?" без false negatives. Занимает несколько килобайт для сегмента в 100K записей.
Для trace_id запроса executor теперь делал так:

if ribbon, ok := e.logRibbon(seg.FileName); ok {
if !ribbon.Contains(q.TraceID[:]) {
return 0, nil // сегмент точно не содержит этот trace_id
}
}
// сканируем сегмент

Ribbon отвечает "нет" — пропускаем сегмент. Ribbon отвечает "возможно да" — сканируем.
Это лучше, чем bitmap. Индексы для 10M записей стали занимать 340 KiB вместо сотен MB. Запросы по trace_id ускорились — большинство сегментов пропускались.


🥴А что сломалось:
Запрос service=api AND trace_id=X до ribbon filter работал через bitmap intersection:

allowedIDs = bitmap(service=api) AND bitmap(trace_id=X) -> 1 запись

После ribbon filter: bitmap(trace_id=X) нет — intersection невозможен.
allowedIDs = только bitmap(service=api) = ~20K кандидатов из 100K.
Скан 20K записей вместо 1.
Для чистых trace_id=X запросов — ribbon + scan работает нормально. Для комбинированных запросов с trace_id — amber потерял пересечение.


💪И куда это нас привело: posting list
Posting list — inverted index для высококардинальных полей. Структура: trace_id --> []record_id.
Если в сегменте 2000 уникальных trace_id и у каждого в среднем 50 записей — это 2000 × 50 × 8 байт = 800 KB. Намного меньше раздутого bitmap.
Теперь lookup для trace_id:

ribbon filter -> пропускаем 99 из 101 сегментов

posting list -> получаем точный список record IDs для этого trace_id

roaring64.And(bitmap(service=api), posting_list(trace_id=X)) -> intersection

Комбинированные запросы снова работают правильно.Posting list хранится в .pidx sidecar рядом с сегментом, строится при запечатывании, загружается lazy через LRU при первом запросе к сегменту.


🚽Детали реализации:
Первый вариант builder использовал map[string][]uint64. Для 100K уникальных trace_id: map overhead (~12 bytes/entry) + string keys (~32 bytes) + slice headers (~32 bytes) = ~7.6 MB на сегмент во время build. Умножить на количество параллельных sealing — GC передаст тебе привет.
Поэтому перешел на flat sorted slice пар (key [16]byte, id uint64):

type rawPair struct {
key [16]byte
id uint64
}


🫰Итог:
Bitmap и posting list отвечают на один и тот же вопрос, но bitmap делает это через битовый вектор длиной N (дорого при высокой кардинальности), posting list — через явный список (дёшево при высокой кардинальности, дорого при низкой).
Ribbon filter отвечает на другой вопрос — и поэтому они не конкуренты, а дополняют друг друга.
  • 🏆 4
  • 🔥 2
Post #21 465
@lab:~$ cat post/honey-garden

🧑‍💻🧑‍💻🧑‍💻
2 недели, golang и react итоги:

Каждый раз, когда готовлюсь к секциям по сисдизу, иду к чат-боту и прошу сгенерировать мне ~20 вариантов продуктов, которые нужно задизайнить. Такой себе мок-собес: сидишь, строишь систему, записываешь себя и проговариваешь, что и почему хочешь добавить, и как будешь это делать.

Иногда он выдает очень интересные идеи, которые хочется вынести за рамки обычного вайтборда и попробовать реализовать. Одной из таких идей стал "внебрачный сын" GitHub и Reddit: взять лучшее от Reddit как социальной платформы и скрестить с техническим слоем GitHub (репозитории, actions и всё такое).

План, конечно, на уровне b2b ai slop pop saas. И если с форумом и социальной частью особо ничего придумывать не нужно, то вот как интегрировать все фичи GitHub — оказалось настоящим квестом (и да, спойлер: не получилось). При попытках разобраться, как работают Forgejo, Gitea и вообще сам Git, появилось два вопроса:

- как это вообще работает?
- почему это работает?

Весь флоу добавления такого функционала имел два пути:
либо делать прослойку, через которую дергать API Forgejo — что неполноценно, местами не работает, да и API не покрывает всего функционала GitHub;
либо форкать Forgejo на 400k строк и делать под себя… (зачем?).

Поэтому для первой итерации так называемого продукта было решено просто добавить интеграции существующих git-хостингов, чтобы можно было делиться репозиториями, PR-ами и т.д. внутри форума.

Теперь про сам форум и feed-ленту. Лента формируется по интересам (тегам) и состоит из RSS-постов и активностей самих пользователей (посты, обсуждения, статьи). Каждый объект в ленте можно обсуждать в Reddit-like комментариях, делиться ссылкой, тегать друзей или сохранять в стор, чтобы вернуться позже.

Сообщество: каждый пользователь может иметь только одно сообщество (чтобы не плодить спам-сетки). От лица сообщества и идет вся активность — посты, статьи и т.д.

Собственно, что тогда делают пользователи? Общаются в чатах (пока один общий на форум), комментируют посты. Позже появятся полноценные чат-комнаты (по аналогии с топиками в Telegram/Discord). Каждое сообщество сможет создавать такие топики и общаться там с фолловерами, создавать sub-топики по интересам и тд и тп.

Вряд ли это станет чем-то большим или хоть сколько-то популярным, но этот двухнедельный трип сильно открыл мне глаза на некоторые вещи в сисдизайне. И в целом идей много — можно добавить платный контент (он уже есть), графовую базу знаний в духе wiki-github для сообществ, хранение файлов и проектов внутри платформы

На самом деле многое уже реализовано, просто не отполировано. Но зацикливаться на доработках — себе дороже. Нужно поставить точку, зафиксировать результат и двигаться дальше.

Было ли полезно реализовать одну из идей сисдиза руками? Конечно. Особенно когда видишь заветные 1–2k RPS на нагрузочном тестировании.
Буду ли я еще делать что-то подобное? Скорее всего, нет — слишком много сил и времени уходит, которые можно вложить во что-то более полезное.

И скорее всего, чуть позде я сделаю его опенсорсным, что позволит поддерживать и развивать его.
  • 🔥 12
  • 🏆 1
Post #20 527
@lab:~$ cat anons/honey-garden

Что дальше?🙂

Итерации, когда ты с головой погружаешься в продукт, как это было с Queen и как это случилось с Amber, конечно, очень сильно выматывают. Ты сидишь днями и ночами, чтобы сделать действительно хороший продукт, которым будешь пользоваться на постоянке. У тебя есть идея, есть ресурсы, но когда ты сравниваешь свой продукт с тем, что другие команды делали годами, и тильтуешь с того, что где-то ты чуть медленнее или тяжелее, — это бьёт по морали очень сильно. Поэтому надо более умно менеджить свои силы и ресурсы, больше отдыхать и не распыляться на всё и сразу.


Поэтому мой текущий приоритет:

Это своя платформа, которую я пилю последние 2,5 месяца. Сейчас уже финальная стадия: сегодня связал фронт с бэком, раскатал на своём сервере и сейчас после небольшого отдыха пойду тестировать платформу. И дай Бог, на этой неделе уже открою альфа-доступы для первых юзеров.
  • 🔥 11
  • 🏆 4
Post #19 455
@lab:~$ cat amber/release


Amber: бенчмарки🧐🧐🧐

Что такое Amber?
Append-only хранилище для логов и трейсов. Один бинарник, HTTP/gRPC API, OTLP-совместимость, корреляция логов и трейсов из коробки.


Тестируемые инструменты:
- Amber (тестируемая)
- Loki 3.4.2
- ClickHouse 24.12
- OpenSearch 2.19.1


Методология
Стенд: AMD Ryzen 9 7950X (8 ядер, 5.5 ГГц), 16 GB RAM, NVMe 260 GB.
Датасет: 100 млн синтетических логов. 10 сервисов, 6 уровней, реальные body с UUID. Pre-generated NDJSON.


Нагрузка:
- Loadgen: 8 worker, batch 500 записей, без rate-limit
- Queries: 20 qps, 4 worker, 60 сек на сценарий


Группы бенчмарков:
R (read): R1-R5 latency p50, меряем скорость запросов разных типов
W (write): rec/s скорость ingest
M (memory/storage): M1 storage, M2 idle RSS Размер на диске, потребление RAM


Выводы:
1) Стабильная sub-100ms latency на всех типах запросов
2) 5x лучше storage efficiency
3) 100x меньше памяти на простое
4) Один бинарник вместо стека из 3-4 систем


Ingest - слабоват, но это трейдофф осознанный, приоритет отдавался размеру и скорости поиска, а не максимальной пропускной способности. Текущий throughput - rec/s = 10 млрд записей в сутки на одном узле чего вполне нам хватит за глаза.
  • 🔥 5
  • 💩 1
Post #18
Channel photo updated
Post #16 667
@lab:~$ cat news/posts

Итак, смешарики, случилось пару новостей.✈️✈️✈️

Переезд с GitHub на Codeberg — просто из-за политики микрослопа. Как-то не по себе, зная, что мой код используется для обучения копилота даже без согласия. К тому же Actions — суперрандомная вещь, которая ещё и платной стала (если выходишь за лимиты, а я, увы, вышел). Джобы могут просто сами по себе падать из-за желания серверов микрософта. Плюс очень много ботов, которые начинают засирать проекты ПРами с «апгрейдом документации за 20 баксов».

Так что все мажорные версии проектов будут на Codeberg, а бета-версии с экспериментальными фичами — на личном git, который я скоро подниму вместе с остальной инфрой. Об этом, конечно же, сделаю отдельный пост.

apiary 🤓
Мой новый малыш в медовом стеке. Если вкратце:
Меньше аннотаций, плюс куча фич, которых в swaggo нет — может, кому-то зайдёт.

🛍
amber:
Рисую UI и уже скоро дропну первую версию. Добавил также поддержку gRPC и OTLP-протокола. Осталось перевести всё на zerolog — и можно будет потыкать.

☺️
queen:
Тут пока тишина, так как ещё собираю фидбек + фокус на другие проекты.

⭐️
honeyhome:
Давно хотелось создать место, где будут все интересующие меня новости айтишечки. dev.to или Reddit, конечно, база контента, но и мусора там много. А мне хочется не пропускать золото, которое там иногда появляется.

В итоге решил сделать форум: завязать ленту на RSS-фидах, постах сообщества и пользователей. Сейчас уже закончил с авторизацией, регистрацией и профилями юзеров-сообществ. Осталось проработать флоу ленты и добавить пару киллер-фич — и на этой/следующей неделе дропну тестовый запуск. Надеюсь вы потрогаете и это будет интересно не только мне!

Раз уж много мест банят — будем создавать свои уголки.

Пишите, учитесь и любите друг друга и свое дело ребята, когда нибудь оно все кончится❤️🥰
  • 🔥 12
Post #15 715
honey@lab:~$ cat amber/news

😴😴😴
А вот и постик про amber!
Append-only база для хранения логов и трейсов. Код публиковать не буду, ибо пока что это все на уровне proof-of-concept, И показывать сырой код не хочется, в отличие от той же queen, которую я уже на всех своих проектах гоняю. Надеюсь что этот концептик вырастет во что-то хорошее. Впервые за последние 2 года идеи и наработки в продукты потихоньку начали переходить. (хостинг руки оторвал, переезжаю на новый😢)

Радостно👏

P.S Думал еще рассказать как впн сервис собирали за месяц, от проектирования до деплоя и первых кастомеров, ибо очень много забавных историй и интересных решений было по ходу работы. Но покумекав понял что придется много чего фильтровать, чтобы чувствительные темы обходить, так что в не в этот раз.

Зато когда я приду в следующий раз, сможете уже поиграться с amber + покажу ui к ней, постораюсь сделать красиво💅🏻
  • 🔥 11
  • 🏆 2
Post #14 700
@lab:~$ cat queen/news

😴😴😴
Оп, подведу немного итоги фидбека по queen, а после расскажу че будет дальше, погнали:

Queen☺️:

1. ManualChecksum апгрейднится до автоматической генерации через AST.
Сейчас ManualChecksum — полностью ручная работа. Если меняем логику функции и забыли обновить версию ("v1" → "v2"), checksum изменение не поймает. Буду делать генерацию checksum из нормализованного AST через go/parser. Сложно, но критично важно для пчелки.


2. UpTo(version) / DownTo(version)
UpSteps(n) и Down(n) удобны, но есть запрос на возможность применить или откатить миграции сразу до конкретной версии N. Добавлю UpTo(version) / DownTo(version) в следующем релизе.


3. Semver автогенерация
Сейчас semver не поддерживает автогенерацию, что в целом логично — но меня попросили добавить хотя бы минорную автоматику. Грех спорить, ресёрч будет проведён и реализован.(по возможности, конечно ничего не обещаю)


4. DryRun / Explain для Go-функций
DryRun и Explain для Go-функций показывают только метаинформацию — показать что именно будет выполнено невозможно по природе вещей в целом. Но то что это слепое пятно при ревью плана заставляет меня грустить. Добавлю опциональное поле Description в Migration struct — тогда queen plan и queen explain будут его показывать если заполнено.(надеюсь это хоть как то сгладит углы)


5. Хуки OnBefore / OnAfter
На этапе планирования от хуков отказался — у нас таких практик почти не встречал. Но дружочек-пирожочек из-за бугра активно показал как работают событийные хуки для уведомлений в slack, инвалидации кэша и т.д. Почитаю, посмотрю как это устроено у других. Ничего не обещаю, но всё может быть.


6. Расширяемые метаданные
Текущие метаданные (AppliedBy, Hostname, Environment и т.д.) — это хорошо, но есть запрос на кастомные поля: версии сервисов, деплов и прочее. Звучит логично, появится в ближайших версиях.


7. NoTransaction флаг
Сейчас все миграции выполняются в транзакции, из-за чего CREATE INDEX CONCURRENTLY в PostgreSQL не выполнится. Но проблем оказлось чуть больше — у каждого из 6 поддерживаемых драйверов своя специфика работы с DDL в транзакциях. Надо изучить вопросик по всем драйверам отдельно, скорее всего добавлю флаг NoTransaction: true на уровне миграции.(но опять же надо изучить специфику для каждой базы)


8. CI/CD и rebuild
Ребилды — это главная боль и один из базовых трейдоффов миграций-в-коде. Любой фикс в миграциях может выкатиться в минуты или десятки минут ожидания пересборки. На текущий момент не знаю как улучшить опыт здесь, но буду экспериментировать с разными вариантами.


9. Нативный --dry-run для CI
Есть запрос на нативный --dry-run флаг на уровне CI — как у goose. Солидарен, будет сделано в ближайших релизах.


10. rollback-on-failure
Текущий откат в CI/CD выглядит как откат на уровне инфраструктуры — kubectl rollout undo откатывает деплой, но не базу. Кейс: миграция применилась успешно, потом упало приложение — БД при этом не откатится. Буду добавлять rollback-on-failure режим. Важный граничный кейс в дизайне: если миграция уже завершилась успешно, а приложение упало после — Queen без внешнего сигнала об этом не узнает. Над этим уже работаю.


11. queen import — полноценная миграция с goose(и не только)
Сейчас queen import --from goose просто конвертирует SQL-файлы в Go-код — одноразовая операция. Хочется иметь полноценную миграцию: Queen будет уметь читать базу с уже применёнными goose-миграциями и переносить их историю в Queen как уже применённые записи — без повторного выполнения SQL. Я еще на старте хотел сделать, но слишком сложно оказалось, отдал приоритет другим фичам, а щас самое время вернуться и довести до ума!


12. Health endpoint / экспорт метрик
Добавить возможность экспортировать состояние миграций как метрику или health endpoint. Куда ж без этого.


13. Нативная интеграция с секретами
Скорее всего будет переезд на чистую ENV-only конфигурации без DSN в .queen.yaml ибо и vault и awsmm работают с обоими нативно


(тут должна была быть инфа про Amber, но тг не дал по лимиту ее вставить👎👎)
Многа жирных схем и бенчмарков ожидается в посте😋
  • 🔥 5
Post #13 665
@lab:~$ cat smth/news

Итак, малютки, надо бы тишину разбавить. По новостям у нас следующее.

Queen(update!)
- Библиотека для миграций претерпела сильные изменения: развернулся на 180 градусов и полностью отказался от поддержки NoSQL-баз данных, ибо слишком много усложнений, которые могут привести к ухудшению DX как для самих NoSQL-баз, так и для SQL.

- Возникли небольшие проблемы с YDB, но думаю, в скором времени добавлю и её поддержку, ибо продукт, конечно, супер мощный и удобный.

- Добавил википедию для библиотеки:

Чтобы понять общее направление, в котором в будущем пойдёт разработка и на какой стул сядет королева, я отправил её на ревью нескольким командам разработки из пары бигтехов. Надеюсь получить хороший (по объёму) фидбек, чтобы сделать выводы и выбрать направление развития моей красавицы.
Фидбек от нескольких команд уже есть, и он достаточно противоречивый: одним нравится отказ от .sql, другим — не очень. Но буду ждать остальных, чтобы собрать полную картину.

Amber (new!)
Ой, а что это?

Append-only log/trace база данных. Когда наши продукты вырастают до 10 сервисов и больше, становится немного трудновато собирать со всех трейсы и логи. Мы, конечно, можем начать разворачивать наш дефолтный стек, а именно Elasticsearch / ClickHouse + Loki + Logstash — и он будет потрясно работать. Но для небольших и средних продуктов есть цена такого стека, а именно:

- Elasticsearch — Java heap, JVM tuning, mapping explosion, счёт за память и ещё куча проблем (ну и шизокачели с лицензиями тоже в копилку).

- ClickHouse хоть быстрее и компактнее для нас, но всё ещё SQL-СУБД с таблицами и схемами. Это круто работало бы, если бы логи были структурированными, но у нас они — полуструктурированные события с произвольными атрибутами, так что и тут мимо.

- Loki — наверное, самое близкое к тому, что мне было нужно, но индексирование, построенное на label, накладывает очень неприятные ограничения.
а еще FTS платный… кринж

Все это крутые и сильные инструменты, но для маленьких и средних продуктов они накладывают слишком большую операционную сложность, которая просто непропорциональна задаче.

Поэтому в нашем кейсе, когда у нас ~100 млн событий в день, я просто не вижу смысла в поднятии такого зоопарка. Хочется видеть сервис, который запускается одной командой, пишет на диск и отвечает на запросы быстрее, чем мы их пишем.

Про это и есть Amber. Через денёк-другой напишу большой пост про Amber с бенчмарками — может, кому-то будет интересно посмотреть, что там под капотом.

Что-то про пчёл?

Недавно я наблюдал за пчёлами на собственной пасеке и понял, что их поведение куда сложнее, чем кажется на первый взгляд. Колония медоносных пчёл — это не просто набор отдельных групп насекомых, а суперорганизм с жёстким разделением труда, развитой системой коммуникации и тонкой оптимизацией использования ресурсов.

Меня прям заинтересовало их социальное устройство: одни пчёлы занимаются внутренними работами в улье, другие — разведкой и сбором нектара, третьи — строительством и вентиляцией гнезда. Все эти роли координируются так, что семья функционирует как единое целое и в одиночку ни одна рабочая пчела выжить не может.

После этих наблюдений захотелось глубже погрузиться в биологию поведения пчёл, разобраться в ключевых поведенческих циклах — от фуражировки и танца разведчиц до строительства сотов и защиты гнезда — а затем перенести эти механизмы в наш с вами компьютерный мир , реализовав агентно-ориентированную симуляцию на гошке. Может даже подчерпну чего-нибудь у природы для реализации других своих проектов, кто знает...

Надеюсь пчёлы в симуляции не построят коммунизм xd
  • 🔥 9
  • 🏆 4
Post #12 1.13K
@lab:~$ cat smth/sheets

🤬🤬🤬
Именно так выглядит моя Queen после того, как я понял, что всего одна строчка в интерфейсе драйвера повлечёт за собой каскад изменений, которые по размеру будут сопоставимы с половиной всей библиотеки...

Вот кстати та самая строчка:
type Driver interface {
Exec(ctx context.Context, fn func(*sql.Tx) error) error
}

Казалось бы, что не так? А то, что в мире есть не только SQL-базы данных, но ещё и NoSQL, а значит наш sql.Tx специфичен только для database/sql. И в целом бо́льшая часть кода привязана к SQL.

Причём я знал, что в тулзе будут не только SQL-миграции, но в начале разработки сделал упор именно на них, думая, что NoSQL по аналогии с другими библиотеками добавлю без лишних проблем. А потом случился неприятный инцидент при попытке добавить поддержку MongoDB, что вставило палки в колёса — ведь в драйвере есть строчка выше, и в целом логика работы с NoSQL отличается намного сильнее, чем я ожидал.

Что ж, с велосипеда мы упали. Как чинить этот дёготь? Если честно, сижу ночью и пытаюсь найти тот самый оптимальный вариант, как сделать по уму...


Были идеи, конечно — и какой-нибудь Session-интерфейс намутить, но тут теряется как лёгкость понимания API, так и типобезопасность. Как и в следующем варианте, где я думал исхитриться через type switch, что меня в целом не устраивает, поскольку пропадал бы автокомплит и есть шансы поймать приколюху в проде. Был также вариант сделать просто отдельный драйвер для NoSQL, но это настолько отвратительный костыль, что мне аж плохо стало.

И тут как итог всего брейншторма выбор падает на два варианта:

• базовый интерфейс + расширения под каждую БД
• дженерики


Конечно, есть вариант переписать частично лишь Driver.Exec, но мне не нравятся эти правки, которые ни вам, ни нам. Либо делать хорошо, либо не делать вовсе, так что поутру придётся садиться за рефакторинг королевы. Да, потрачу чуть больше времени и чуть больше попаболи, но зато будущий я скажет себе спасибо (по крайней мере на это надеюсь).

Ну и статейка с более подробным разбором проблем и самим переездом в будущем не заставит себя ждать. В целом не думаю, что планы по релизу Queen в первую неделю марта 2026 сдвинутся из-за этого — наоборот, даже надеюсь, что все эти новшества ускорят внедрение поддержки новых баз данных.


У медведя совсем опилки в голове мёдом заплыли, видите до чего доводит одно маленькое упущение несколько недель назад🥴🥴🥴

тильт конечно имеется ⚠️
  • 🔥 5
Post #11 1.11K
@lab:~$ cat post/news

👌👌👌

Такс смешарики, чего у нас случилось нового и что по новостям.
• Большой апдейт по queen это пет-проект, над идеей и минимальной реализацией которого я работал последние 1,5 месяца. Невероятную мотивацию даёт простая уверенность в том, что ты пишешь продукт, который действительно тебе нужен, который решает твою проблему, а не очередную todo-аппу. Сложно — да, интересно — конечно.

• https://dmedovich.com/ немного обновился, и теперь там есть блог. Все будущие статьи и заметки по разработке будут выходить именно там. А здесь я буду транслировать повседневные заметки и микроновости.

Тааакс.. а теперь по планам на ближайшее время🧑‍💻🧑‍💻🧑‍💻
• В Тбанк щас идёт контекст на стажку, говорят много алгосиков. Залечу в ближайшее время на борьбу с ними, если в штанишки подливу на пущу, то дропну тут решения, может пригодятся тем кто пойдёт.

• queen так как это первый мой настолько любимый пет-проект которого я как сына хочу вырасти во что-то серьёзное, то в ближайшее время я сосредоточусь на том чтобы оформить библиотеку по всей красоте opensource проектов, чтобы issue с багами и предложениями не терялись в общей корзине, и простому обывателю был понятен примерный roadmap проекта.

Кстати о нём, на данный момент у нас есть драйвера для:
• PostgreSQL
• SQLite
• MySQL
• MariaDB
• ClickHouse (спасибо хигу)
• CockroachDB (спасибо хигу)

Так же идет работа над:
• YandexDB
• Cassandra
• MongoDB
• Oracle
• Redshift
• MS SQL Server
• ScyllaDB

Очевидно что сразу не охватить весь тот список баз которые поддерживают другие инструменты миграций, но такой список максимально приблизит нашу королеву к проектам разработчикам. А сейчас королева уйдет на большой рефакторинг, дабы повысить качество кода и облегчить как вам так и мне в будущем работу с ней.


А пока вы пойдете читать, про то как медведь королеву пчел приручал, сам медведь пойдет готовить википедию для queen и оформлять для неё проектик.

🐝🐝🐝
  • 🔥 7
Post #10 1.15K
honey@lab:~$ cat post/news

👋👋👋
Немного выпал из мира на период праздников — жаль, что пришлось сдвинуть многие планы. Но потихоньку возвращаюсь в течение😴

Что случилось и что у нас нового:

• honey-task обновился и получил 10 новых алго‑задач. Там уже собрано многое из того, что попадается на собеседованиях, так что глобальных апдейтов в ближайшее время не будет. Пальчики можно и на этом на тренировать.

• Работаю над своей библиотекой для миграций — скоро выйдет отдельный пост с подробностями.

• Теперь есть диджитал пасека, в обозримом будущем там появится блок со статьями, а пока все новости дублирую в тг. Милости прошу: https://dmedovich.com/ 🧑‍💻

• Ну и поделюсь небольшими идеями по следующим возможным проектам.👀

Возвращаюсь в строй — дальше больше✈️
  • 🔥 16
  • 🏆 2
Post #9 1.38K
honey@lab:~$ cat post/practice

Как я пытался подружиться с AI-агентами... и что из этого вышло 🧑‍💻

Пришел ответ на вакансию Go-разработчика. Его суть — реализовать AI-агента для автоматизации браузера. Мой финальный босс получается.

Решил попробовать — посмотреть, кто эти ваши агенты.🧃

Что от него хотели:
• Управлять браузером программно (не headless, чтобы всё было видно).
• Поддерживать сессии (человек залогинился — агент продолжил).
• Использовать Claude/OpenAI для принятия решений.
• Обрабатывать сложные, многошаговые задачи.
• Самое сложное: уметь работать с ограничениями по токенам, не загружая в AI всю страницу целиком.
• Никаких заготовленных селекторов или шаблонов действий. Агент должен сам до всего додуматься.

Взял питон + ключик от Anthropic API, я пошел создавать этого агента, спусят полтора часа я уже сидел в браузере и смотрел как он рыскает по яндекс лавке, в поиске бургеров. Но к сожалению было очень много попаболи...

😡 Первая попаболь: Жадный до токенов DOM
Одним из инструментов агента был get_page_content(), который слал AI всю структуру страницы.
Каждый запрос пожирал по ~10к токенов. Через час мои тестовые лимиты испарились в никуда.
Skill issue? Безусловно.

Собственно в ожидание пока мне выдадут новый ключ, я пошёл искать, а как правильно организовывать работу агентов? Ответом стала — Sub-agent architecture. Это как микросервисы, но для AI. Главный агент-координатор и куча мелких специалистов:

• DOMSubAgent — аналитик, отвечает на вопросы о странице.
• TaskPlannerSubAgent — разбивает «купить бургер» на шаги: зайти → найти → добавить.

Такая схема сэкономила токены в 2-3 раза. Каждый саб-агент знает своё дело и не грузит контекст лишним.

😡 Вторая попаболь: Слишком много инструментов
Я создал 19 инструментов для клика: по element_id, по тексту, по CSS, по координатам... Агент в них просто потерялся. Он не знал, каким инструментов когда пользоваться. Пришлось резать, оставив только самое необходимое.

😡 Третья попаболь: Лишние абстракции и AI-рефлексия
Сначала я накрутил абстракций над CSS-селекторами, хотя нужно было передавать их напрямую.

Потом создал Vision Sub-agent, который по скриншоту искал элементы. Он отвечал: «Кнопка справа на карточке товара!». Главный агент, не получив чёткий селектор, снова и снова вызывал его. Началась бесконечная AI-рефлексия... И это не говоря про дебаг промтов для каждого агента — отдельная удручающая история.😴

Ну и под конец уже, у меня случилась трагедия с морсом и глинтвейном🧃
Дал агенту задание: «Закажи морс». Всё шло хорошо, пока он не зашёл в раздел «Соки и морсы».
На первой позиции Яндекс лавки любезно разместил глинтвейн (типа «попробуй, вдруг понравится»).

Это свело агента с ума.
Он 10 минут пытался понять, почему «глинтвейн» — это не «морс».
• Один саб-агент клал глинтвейн в корзину.
• Другой — проверял корзину, видел ошибку и удалял его.
• Главный агент, в попытке разрешить конфликт, открыл два окна и начал сравнивать морс и глинтвейн, задавая сам себе философские вопросы.

Он сравнивал все: температуру напитка, ингредиенты, условия в которых их принимают, культурный вектор, после чего дал такой ответ:
> Semantic similarity < 15%. Classification failed.

Через 15 минут борьбы он таки осознал разницу и добавил нужный товар. А у меня — кончились токены. Идти просить ещё один ключ я не стал, отправил работу как есть.

У этой компании очень много вакансий лежит на hh, если кто-то еще будет делать тестовое, то надеюсь мои ошибки и/или наработки помогут вам выполнить его намного лучше.

Как сказал бы агент: skill issue🗿
  • 🔥 15
  • 🏆 3
  • 🤔 1
Post #8 1.16K
@lab:~$ cat post/study

Вспомнил, что задачки решаю не только я, и некоторые ждали обновлений🕺

Добавил 30 задач по интерфейсам в Go:
• 1-10: базовые интерфейсы и паттерны
• 11-20: системные компоненты (middleware, schedulers, pools)
• 21-30: распределенные системы (Raft, SAGA, Map-Reduce, кэши)

Удачи и терпения тем, кто будет решать 🫡

🧃honey-task — задачи по Go(deleted)
  • 🔥 17
  • ✍ 4
Older posts →

About this channel

How can I read @dmedovich_notes without a Telegram account?
TGViewer shows the public web preview Telegram publishes for dmedovich notes: recent posts, photos, videos and the subscriber count, with no app, login or account.
How many subscribers does dmedovich notes have?
dmedovich notes (@dmedovich_notes) has 156 subscribers on Telegram, refreshed roughly every 30 minutes.
Does dmedovich notes know I viewed it here?
No. Public channel previews carry no viewer identity, and TGViewer has no accounts or tracking of what you look up.
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →