Backend Portal | Программирование
16.8K subscribers
1.56K photos
147 videos
42 files
1.35K links
Присоединяйтесь к нашему каналу и погрузитесь в мир Backend-разработки

Связь: @devmangx

РКН: https://clck.ru/3FobxK
Download Telegram
Большинство думают, что каждый .sort() работает одинаково во всех языках.

Это не так.

Разные языки, разные алгоритмы:

> C++ → Introsort
> std::sort() = Quicksort + Heapsort + Insertion

> Python → Timsort
> list.sort() = Stable (стабильная сортировка)

> Java → Зависит от типа
> Arrays.sort(Object[]) = Timsort (стабильная)
> Arrays.sort(int[]) = Dual-Pivot Quicksort

> JavaScript → Зависит от движка
> Chrome V8 = гибрид в стиле Timsort
> Firefox = гибрид Merge Sort

> C → Нет фиксированной гарантии
> qsort() обычно Quicksort

> Go → PDQSort + Insertion

> Rust → Выбираешь сам
> .sort() = стабильная Merge Sort
> .sort_unstable() = PDQSort

> Swift → Интроспективный гибрид
> Ближе к PDQSort + Insertion

Один и тот же .sort(), но в каждом языке за ним могут стоять разные алгоритмы.

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
7👍3
Если ты учишь backend в 2026, прокачай вот эти 8 тем:

1️⃣Дизайн REST API

2️⃣Аутентификация (JWT / OAuth)

3️⃣Индексы в базе данных

4️⃣Кэширование (Redis)

5️⃣Rate limiting

6️⃣Логирование и мониторинг

7️⃣База по Docker

8️⃣Основы system design

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
9👍3
Что такое LGTM?

L – Loki
структурированные, поисковые, коррелируемые логи.

G – Grafana
визуализация всего, что происходит в системе.

T – Tempo
распределенный трейсинг между сервисами.

M – Mimir
истина про поведение системы в виде таймсерий.

Вместе они отвечают на три ключевых вопроса:

что произошло? → логи
насколько все плохо? → метрики
где конкретно тормозит? → трейсы

Без LGTM дебаг распределенных систем это гадание на кофейной гуще. ❤️

Плейлист-курс по LGTM (Loki + Grafana + Tempo + Mimir)

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥102💊1
Ваша база PostgreSQL уперлась в потолок.

Текущие метрики:

- База 2 ТБ на db.r5.4xlarge
- 85% read, 15% write
- Лаг read-replica: в среднем 200 мс, пики до 2 секунд
- Количество подключений в пик упирается в максимум (500)
- Самые медленные запросы: сложные JOIN’ы по 4 таблицам с 100M+ строк
- VACUUM не успевает, растет число мертвых кортежей (dead tuples)
- Хранилище растет на 50 ГБ/месяц

Команда спорит, что делать:

- Добавить PgBouncer для пуллинга соединений
- Внедрить read-replica + роутинг запросов
- Шардировать базу
- Вынести горячие таблицы в DynamoDB
- Залить деньгами (апгрейд до db.r5.12xlarge)

В этом квартале можно выбрать только 2.

Какие 2 и в каком порядке?

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
5
Нашел эту статью о транзакциях в базах данных и реализации ACID в реляционных базах данных действительно интересной, рекомендую прочитать.

https://www.datacamp.com/blog/acid-transactions

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
5🔥2
Паттерны сложности по времени, которые надо знать, чтобы не тупить на собесах

1️⃣for (i = 0; i < n; i++)O(n)

2️⃣Два вложенных цикла (n × n)O(n²)

3️⃣for (j = 0; j < i; j++) внутри внешнего цикла → O(n²)
(потому что суммарно 1 + 2 + ... + (n-1))

4️⃣i = i * 2 пока i < nO(log n)

5️⃣i = i / 2 пока i > 1O(log n)

6️⃣Два раздельных цикла по nO(n)
(в Big-O константы отбрасываем: O(n) + O(n) = O(n))

7️⃣while (n > 0) n /= 2O(log n)

8️⃣T(n) = T(n-1) + O(1)O(n)

9️⃣T(n) = 2T(n/2) + O(n)O(n log n)
(типичный merge sort)

1️⃣0️⃣T(n) = T(n-1) + T(n-2)O(2ⁿ)
(наивная рекурсия Фибоначчи, без мемоизации)

Если умеешь сходу маппить “паттерн → сложность”, вопросы про time complexity на интервью перестают быть проблемой.

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
7
p50, p95 и p99.

Сколько тут этих p и что они значат?

Коротко: p = percentile (процентиль).
Он показывает распределение производительности, а не среднее значение.

Сколько вообще бывает этих p?
Процентили есть от p0 до p100, но на практике обычно смотрят на:

• p50 -> медиана
• p90 -> первые “подтупливания”
• p95 -> базовая линия для SLA
• p99 -> tail latency (хвостовая задержка)
• p99.9 -> системы с ультра-надежностью (Google и Amazon следят за p99.9)

Представь 100 пользователей, которые бьют в твой API.

p50 = 120ms -> 50 пользователей уложились в 120ms или быстрее, 50 были медленнее.
p95 = 800ms -> 95 пользователей уложились в 800ms или быстрее, 5 были медленнее.
p99 = 2.5s -> 99 пользователей уложились в 2.5s или быстрее, 1 был медленнее (вот тут и живет фрустрация).

Почему трекать процентили, а не среднее?
Потому что средняя задержка умеет прятать боль.

Пример:

100ms
110ms
120ms
130ms
5000ms

Среднее ≈ 1,092ms
p50 = 120ms
p95 = 5000ms

Зная p50, p95 и p99, ты можешь:

• заметить всплески задержек до того, как все упадет
• понять реальный UX пользователей
• найти проблемы со скейлингом и контеншеном
• безопаснее настроить ретраи, таймауты и backpressure
• задать адекватные SLA
• не допустить системы “быстро, но ненадежно”

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
5🔥2
Как работают куки и сессии на примере аутентификации

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
3
Есть очень интересный подход к хешированию, называется Robin Hood Hashing. Это как раз из тех идей, которые одновременно простые и элегантные. Смотри.

В обычных хеш-таблицах с open addressing, когда случается коллизия, ты просто идёшь дальше по слотам, пока не найдёшь пустой. Проблема в том, что одни ключи оказываются почти рядом со своей “идеальной” позицией, а другим не везёт и их уносит далеко.

Из-за этого у неудачливых ключей получаются длинные probe-sequence, и поиск начинает заметно проседать.

Robin Hood Hashing чинит это минимальным изменением стратегии вставки. Когда вставляешь новый ключ, ты сравниваешь, насколько он далеко от своей “домашней” позиции, с тем ключом, который сейчас занимает слот. Если новый ключ “беднее” (то есть он уже дальше от своего идеала), он забирает место, а вытесненный ключ продолжает пробинг дальше.

По сути, ты “крадёшь у богатых” (у ключей, которые уютно сидят рядом с домом) и “отдаёшь бедным” (тем, кого утащило далеко). Отсюда и название Robin Hood.

В итоге дисперсия длины пробинга по всем ключам остаётся очень маленькой. Никого не оставляют сильно “позади”.

Этот алгоритм можно встретить в стандартной реализации HashMap в Rust, в нескольких высокопроизводительных индексах баз данных и в системах управления памятью, где важна cache efficiency. Он ещё популярен в хеш-таблицах, где нужна предсказуемая скорость поиска, а не просто хороший средний случай.

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

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

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
5👍4🔥2🤔1
Похоже, компании реально переосмысляют весь SDLC вокруг агентов и пытаются прийти к миру, где инженеры, возможно, вообще не будут писать ни одной строчки кода руками.

Я тут общался с одним инженерным лидом, и по ощущениям многие уже готовятся к полностью agentic SDLC. В репозиториях начинают появляться файлы под Claude и прочие agent-штуки, под внутренние инструменты пилят MCP, и в целом многое меняется. Код-ревью тоже автоматизируют, и это только один из пунктов.

Вот такие времена :)

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
💊10🔥6🤔1
This media is not supported in your browser
VIEW IN TELEGRAM
Ограничение скорости за 40 секунд

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥13
Любая база данных делает компромиссы по производительности.

Это не случайность. Это осознанные решения в дизайне, чтобы закрывать нужды конкретных приложений.

Самые частые:

Time / Space
Делаем базу быстрее, расплачиваясь памятью или диском. Храним одни и те же данные в нескольких форматах, заранее считаем агрегаты, используем сжатие и т.д. Классический CS tradeoff.

Durability
Улучшаем latency, ослабляя долговечность. Вы делаете fsync сразу или позже? Что для вас durability: 1, 2 или N копий на диске? Нужны копии между несколькими DC?

Read / Write
Типичный пример: B-tree обычно лучше для чтения, LSM лучше для записи. Можно ухудшить чтения ради более высокой пропускной способности на запись.

Startup time
Меняем время ответа на запросы на время старта. Например, делаем индексы непостоянными (non-persistent): при каждом рестарте их нужно пересобирать, зато записи не обязаны явно обновлять индексы на диске.

Consistency
Если eventual consistency ок, это упрощает и ускоряет выполнение запросов. Strong consistency требует больше локов и блокировок.

Нужно заранее понимать свои требования по всем пунктам и выбирать DBMS под них.

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
4
Видел интересное видео от CTO Zerodha про то, как они масштабировали Postgres с 7+ млн таблиц.
Технология просто выносит мозг, а если копнуть глубже, становится еще веселее:

- синхронщина? не масштабируется, значит выкидываем.
- все в async. тяжелую генерацию отчетов ставим в очередь.
- независимый middleware, которому все равно на базу и на приложение.
- собрали “DungBeetle” на Go: обобщенные, независимые от СУБД HTTP API, чтобы тянуть отчеты из любой базы.
- результаты сливаем в отдельную Results DB, а приложение читает уже оттуда.

Вот так выглядит настоящий масштаб.

Смотреть видео

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
5
Хочешь посмотреть, как Java эволюционировала, фича за фичей?

brunoborges сделал офигенный сайт, где изменения языка Java показаны рядом, в формате side-by-side. Идеально для студентов и для тех, кто модернизирует легаси-приложения.

Зацени:
https://javaevolved.github.io

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
4🤔4
Повышения это про проактивность, а не реакцию постфактум.

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

Не жди, пока менеджер сам поднимет тему. Начни разговор первым. Просить повышение нормально. Многие стесняются, но тебе не обязательно. Сформулируй свой кейс на повышение и посмотри, как менеджер отреагирует.

Дальше два варианта: либо менеджер скажет, что ты готов, либо скажет, что пока нет. Если готов, отлично. Работайте вместе с менеджером, чтобы оформить пакет на повышение и убедиться, что все сходится под ближайший цикл.

Если пока не готов, вместе с менеджером составьте понятный, выполнимый план. Он может занять от пары кварталов до нескольких лет, и это ок. Относись к нему как к чеклисту. Закроешь пункты, заходи с этим кейсом снова.

И если ты сделал все, что просили, но тебе все равно говорят "пока нет", то возможные пути такие:

1. подождать: может реально нет открытых позиций
2. жестче, но корректно обсудить выше по цепочке и снова презентовать кейс
3. поискать возможности роста в соседних командах
4. начать искать новую роль в другом месте и, когда придет время, аккуратно и ответственно перейти

Тут нет универсально правильного или неправильного выбора. Единственная реально плохая реакция это сорваться на резкие или эмоциональные слова из разочарования. Всегда держи профессиональный тон.

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

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
7
Продвинутые паттерны конкурентности в Go:
Bridge Channel

bridge разворачивает канал каналов в один выходной канал. Он читает каждый внутренний канал по порядку и форвардит значения в выходной.

Если у тебя есть последовательность каналов (например, из постраничных вызовов API или продьюсеров, которые отдают данные чанками), Bridge превращает это в один цельный поток.

В основном это отлично подходит для пагинированных или батчевых продьюсеров.

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
4👍2
Один умный человек, написал эссе про алгоритм BM25, который лежит в основе Elasticsearch, Apache Solr и Apache Lucene.

Зачем? Просто потому что ему было нечем заняться, он нырнул в кроличью нору информационного поиска: пытался понять, почему и где TF-IDF в итоге упирается в свои пределы, как на самом деле устроен BM25 под капотом, и почему системы вроде Elasticsearch, Solr и Lucene сделали его своей дефолтной функцией ранжирования.

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

Этот текст должен дать тебе нормальную ментальную модель, чтобы понимать BM25 и также IDF из популярного TF-IDF. Зацени.

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
16🔥2
This media is not supported in your browser
VIEW IN TELEGRAM
Лучшая тулза для создания софтверных диаграмм.
И вдобавок бесплатная и с поддержкой GitHub

Идеально подходит для UML, флоу и процессов.
Экспортирует в картинку, PDF, HTML и другое.

http://app.diagrams.net

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
9👍3
Вот одна из самых базовых вещей про то, как работает память в операционных системах, но со временем многие это просто забывают. Небольшое напоминание: когда вы вызываете malloc(), вы на самом деле не получаете память, вы получаете обещание.

Когда вы делаете malloc на 1 ГБ, ОС говорит: «Окей, вот тебе 1 ГБ», но реально пока не отдает ни одного байта. На этом этапе выделяется виртуальная память, а фактическое потребление RAM, то есть RSS (Resident Set Size), остается прежним.

Это и есть lazy allocation. ОС назначает реальные страницы только тогда, когда вы начинаете в них писать. Вы можете вызвать malloc() хоть на терабайты на ноутбуке с 16 ГБ RAM, и вызов пройдет успешно. Падение, тот самый segmentation fault, случится позже, когда вы попытаетесь эту память использовать, а система уже не сможет ее реально предоставить.

- malloc(1GB) -> виртуальная память +1 ГБ, RSS +0 МБ
- memset(ptr, 0, 1GB) -> RSS +1 ГБ

Почему это важно? Потому что именно RSS реально конкурирует за ресурсы. Именно на него смотрит OOM killer, и именно он в итоге тормозит систему.

У вас может быть процесс, который в профайлере показывает 10 ГБ выделенной памяти, а реально потребляет только 2 ГБ RAM. Разница возникает потому, что большая часть этих областей, выделенных через malloc, была зарезервирована, но к ней так и не было доступа.

Кстати, именно поэтому утечки памяти могут долго оставаться незаметными. Если вы сделали malloc(), потеряли указатель и ни разу в эту память не записали, то вы утекли по виртуальному адресному пространству, но не по физической памяти.

Самый простой способ посмотреть RSS процесса, это запустить ps aux. В выводе вы увидите VSZ (virtual) и RSS (resident) в КБ. Это можно получить и программно :)

Так что всегда следите именно за RSS, а не только за объемом памяти, выделенной через malloc, если хотите понимать, сколько памяти процесс реально потребляет.

👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
8👍5
Паттерн, который я использую в Go для запуска долгоживущих сервисов или фоновых процессов.

Идея в том, что сервис реализует всего один блокирующий метод Run(ctx), который делает всю основную работу: обслуживает запросы, следит за изменениями и так далее.
Чтобы его остановить, достаточно отменить контекст.

За счет этого интерфейс получается очень простым. Если нужен неблокирующий старт/стоп, их можно собрать прямо в том месте, где они нужны:

stop() -> просто используйте context.WithCancel. Возвращаемый cancel() и будет вашей функцией остановки.

start() -> оберните Run(ctx) в замыкание и запустите как goroutine. Владельцем goroutine остается вызывающая сторона, а не сам сервис. Это позволяет держать сервис простым и предсказуемым.

Также можно добавить канал done, чтобы понимать, когда Run полностью завершился, для graceful shutdown.

Этот подход особенно хорош, когда нужно управлять сразу многими такими сервисами. Если запускать их через errgroup, вы получаете:

- автоматическую отмену всех сервисов, если один из них завершился с ошибкой
- один общий Wait() для ожидания завершения всех сервисов

// Service only implements a blocking Run(ctx). Cancel the context to stop it.

type Service struct{}

func (s *Service) Run(ctx context.Context) error {
fmt.Println("running")

// Do busy work: serve requests, watch for changes, etc.
// Return when ctx is canceled
<-ctx.Done()

fmt.Println("stopped")
return nil
}

func main() {
ctx := context.Background()
s := &Service{}

// 1. blocking: just call Run with a context
ctx1, _ := context.WithTimeout(ctx, 2*time.Second) // canceled after 2 seconds for demo
err := s.Run(ctx1)

// 2. non-blocking: implement ad-hoc start/stop and wait for completion if needed
ctx2, stop := context.WithCancel(ctx)
done := make(chan error, 1)
start := func() { // start() wrapper to send the result of Run to done channel
done <- s.Run(ctx2)
}

go start() // non-blocking start in a goroutine
time.Sleep(2 * time.Second)
stop() // cancel ctx to signal shutdown
err = <-done // wait for Run to return if needed
}


👉 @BackendPortal
Please open Telegram to view this post
VIEW IN TELEGRAM
1👍1
Избегайте использования CASE в WHERE

Хотя SQL допускает использование выражений CASE внутри WHERE, они часто делают запросы менее читаемыми и усложняют понимание их логики. Логика фильтрации оказывается спрятана внутри условного выражения вместо того, чтобы быть выраженной напрямую через булевы предикаты. Кроме того, оборачивание условий в CASE может затруднить для оптимизатора запросов анализ предиката и эффективное использование индексов. Во многих случаях это делает запрос non-SARGable (Search ARGument Able), то есть база данных не может эффективно использовать индексы для поиска нужных строк.

Вот наивный вариант:

SELECT *
FROM Products
WHERE CASE
WHEN Category = 'Electronics' THEN Price < 500
ELSE Price < 100
END;


В этом запросе логика фильтрации встроена внутрь выражения CASE, из-за чего условие становится менее прозрачным и потенциально более сложным для эффективной оценки оптимизатором.

Лучший способ выразить такую логику, это использовать операторы AND и OR напрямую в условии фильтрации. Явно записанные булевы предикаты делают логику проще для чтения и понимания, потому что условия хорошо видны, а не спрятаны внутри CASE.

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

SELECT *
FROM Products
WHERE Price < 100
OR (Category = 'Electronics' AND Price < 500);


👉 @BackendPortal
👍74💊1