Pull to refresh
64K+
9
Андрей Приемко@AndrewDeveloper

Программист-практик

13
Rating
14
Subscribers
Send message

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

Что касается интрузивных контейнеров, у них, безусловно, есть своя область применения. Однако это другой подход к организации данных: элементы сами содержат информацию, необходимую для включения в структуру данных. В результате контейнер может не владеть памятью элементов и вообще не заниматься её выделением. Поэтому вопрос «куда прикручивать аллокатор?» в таком случае просто не возникает: если контейнер не выделяет память, ему не нужен аллокатор.

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

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

Кстати, начиная с C++17 в стандартной библиотеке есть std::pmr::polymorphic_allocator, который позволяет использовать полиморфизм при выборе ресурса памяти, не превращая сам контейнер в класс с виртуальными функциями выделения и освобождения памяти.

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

Давно не работаю под Windows, поэтому не могу сказать, как обстоят дела в современной реализации.

Мне кажется, важно учитывать, что проектирование библиотек всегда связано с поиском компромиссов.

STL — это библиотека для обобщённого программирования. Её задача не просто предоставить набор готовых контейнеров и алгоритмов, а позволить применять алгоритмы к разным типам данных и структурам, повторно используя код.

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

2. Что касается аллокаторов, то их интерфейс довольно сложен, и я не считаю его идеальным. В частности, rebind используется потому, что возникает необходимость выделять память не только под пользовательские объекты, но и для внутренних структур данных контейнера.

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

Я не пытаюсь доказать, что дизайн STL идеален. Но для вывода о «фейл-дизайне» хотелось бы увидеть конкретный критерий: какое решение было бы лучше, какие реальные сценарии оно упростило бы и чем за это пришлось бы заплатить.

Исследовательский характер проекта не означает отрыва от практики.

Отвечу по пунктам. 1. Утверждение "Все что не вписывалось в красивую концепцию просто не реализовывалось. Например кольцевые списки." я считаю слишком категоричным. Сам факт того, что STL не содержит отдельного контейнера для какой-либо структуры данных, ещё не говорит о том, что её исключили ради сохранения красивой концепции. Нужно учитывать практическую востребованность и стоимость поддержки дополнительной абстракции. Кольцевой список можно использовать в некоторых задачах, но я не могу сходу назвать распространённый алгоритм, которому необходим именно кольцевой связный список.

Давайте продолжим обсуждение увлекательной темы. Отвечу по пунктам.

1.Тезис про «классический антипаттерн гвоздь-микроскоп» слишком категоричен, если не определены критерии, по которым StatefulSet може быть хуже. Само по себе использование StatefulSet для получения ID не является доказанным антипаттерном. Это одно из возможных решений, которое нужно сравнивать с альтернативами по требованиям и стоимости эксплуатации.

Возможные критерии сравнения: 1.Высокое потребление ресурсов StatefulSet по сравнению с ZK; 2.Время запуска; 3.Cущественное потребление ресурсов; 4.Отказоустойчивость; 5.Ограничения масштабирования;

ZooKeeper/etcd, на мой взгляд, не всегда является “решением по умолчанию”. Для эксплуатации ZooKeeper нужны определенные усилия для облеспечения кворума, выделение дисков, мониторинг кластера. Кроме того, для решения задачи получения nodeID нужно определить допустимый диапазон значений, получать номер не только при старте, но и при перезапуске пода. ZooKeeper/etcd сами по себе не знают, что такое workerId, сколько таких идентификаторов разрешено, какие значения свободны и как сопоставить выданный номер с конкретным генератором. Для этого нужно дополнительная логика.

2.Я не утверждал, что нужен распределённый счётчик Pod’ов. Мне нужен уникальный идентификатор генератора, который можно получить при помощи StatefulSet, не прибегая к развертыванию кластера ZK. Тезис про необходимость распределенного счетчика nodeId мне кажется не совсем точно отражающим суть проблемы. Нужно решать несколько проблем, в том числе: 1.Как определить когда номер освобождается и что делать с этим номером? 2.Как связать владение nodeId с Pod? 2.Как обнаружить исчезновение владельца? 3.Как исключить одновременное владение когда процесс завис и не отдает свой номер? Нужен координатор уникального владения worker ID,а не распределенный счетчик nodeId. В свете вопросов, связанных с развертыванием и эксплуатацией кластера ZK и создания механизма координации уникального владения worker ID, использование StatefulSet не кажется мне антипаттерном.

3 и 4.Я не говорил что проблема предлагаемого подхода в высокой нагрузке на Zookeeper, а обратил внимание на то, что эта система не умеет вести учет номеров workerId, так как ничего о них не знает. Согласен, что к ZooKeeper/etcd не нужно обращаться при каждой генерации ID. Координатор может использоваться только при получении nodeID, когда Pod стартует впервые или перезапускается. Мой аргумент был в другом: ZooKeeper/etcd сами по себе не знают о Snowflake workerId. Они предоставляют механизмы координации, но не готовый учёт номеров генераторов.

5.Утверждение о том, что “Генераторы должны быть в каждом микросервисе, которым нужны идентификаторы - в этом вся прелесть снежинки” слишком категорично. Да, генераторы могут быть частью микросервиса, в котором нужны идентификаторы. Тут всё зависит от того, насколько нужно масштабировать алгоритм “снежинки”. Об этом я писал в прошлом ответе. Сама по себе “снежинка” ничего не знает о микросервисах. Это алгоритм генерации распределённых идентификаторов, которому нужны уникальные идентификаторы одновременно работающих генераторов, а не микросервисная архитектура как таковая. Утверждение “весь профит съест сетевой трафик” без бенчмарков слишком сильное. Если микрорсервисы написаны на разных языках, то для каждого нужна реализация “снежинки”, получения worker ID, корректная обработка отвода часов назад и т.д. Это усложнит разработку.

Описанный Вами вариант является вполне разумной альтернативой. Я выбрал другой вариант, потому что в рамках серии статей хотел показать именно выделенный сервис генерации ID и Kubernetes-механику его масштабирования.

Согласен с тем, что в случае использования UUIDv7 не нужна координация. Но проблема в том, что получается слишком длинный ID. Вы пишете что UUIDv7 с кодированием в Base62 дает 22 URL-безопасных символа. В задаче по системному дизайну, которую я рассматривал в качестве отправной точки, говорится о том, что ссылка должна быть как можно короче (7 символов).

Спасибо за вопросы! К сожалению, я не имею опыта работы с Zookeeper\etcd, поэтому отвечу, основываясь исключительно на информации, которую только что прочитал об этих системах.

1.Да StatefulSet здесь используется исключительно ради получения гарантированно уникальной цифры из имени.

Мне кажется что внедрение ZooKeeper или etcd ради раздачи номеров — это избыточное усложнение. К тому же на Хабре уже была статья, в которой говорится, что настройка кластера ZooKeeper может быть непростой задачей, особенно для новичков.

Насколько я понял, и ZooKeeper, и etcd — это распределенные системы, работающие на алгоритмах консенсуса. Для кворума им нужно минимум 3 ноды. Ирония еще и в том, что для запуска надежного кластера ZooKeeper в K8s все равно придется использовать полноценный StatefulSet с дисками. То есть мы значительно усложняем систему.

К тому же ZooKeeper и etcd не умеют вести учет подов. Если мы выберем Deployment вместе с ZooKeeper/etcd, то придется писать сервис назначения свободного ID вручную. StatefulSet нативно берет логику отслеживания Id подов на себя.

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

У меня на хранение Id генератора отведено 3 бита, то есть может быть максимум 8 микросервисов-потребителей. Если их перестанет хватать для обработки запросов, то масштабировать эти сервисы дальше мы не сможем.

По поводу лишнего трафика трудно сказать, что создаст его больше - решение со StatefulSet или с использованием кластера Zookeeper. В моем решении количество запросов к генератору минимизируется за счет того, что в ответ на запрос потребитель получает не один Id, а сразу массив. При старте сервиса выполняется предварительная генерация Id, не дожидаясь запросов. Поэтому задержки здесь сводятся к минимуму. Конкуренция за генераторы минимизируется за счет gRPC балансировки нагрузки на клиенте.

Спасибо за вопросы! Постараюсь ответить подробно. Да, проблема с коллизиями nodeId решается с использованием statefulsets. В документации сказано что поды в StatefulSet имеют уникальный порядковый индекс, например, idgen-0, idgen-1, idgen-2. В коде я получаю этот индекс и преобразую в пару id-датацентра, id-машины. Поскольку индексы гарантированно уникальны в рамках одного SatefulSet, коллизии nodeId исключены. Это надежно, так как K8s гарантирует, что в один момент времени не будет двух подов с одинаковым индексом в одном SatefulSet. Даже если под упадет, K8s пересоздаст его с тем же самым номером. По поводу того, не слишком ли дорого выходит. Я думаю, что с точки зрения потребления ресурсов (CPU, RAM) StatefulSet поды вряд ли "весят" существенно больше чем поды Deployment. Поды StatefulSet развертываются последовательно, поэтому данный процесс происходит медленее, чем с подами Deployment. Но для сервиса генерации ID это не критично.

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

  1. Масштабирование экземпляров микросервиса в ответ на увеличение количества запросов генерации ID.

  2. Избавление от единой точки отказа в виде БД. Альтернативным вариантом вместо алгоритма Snowflake является сервер тикетов, основанный на использовании базы данных с автоинкрементом счетчика ID. При высокой нагрузке использование БД может стать узким местом и точкой отказа.

После распределения nodeId при помощи StatefulSet я не вижу причин по которым нельзя было бы сразу генерировать ID и использовать по назначению в любом сервисе, в том числе stateless.

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

Система не упадет. В описанном Вами случае она должна вернуть пользователю ошибку. Например, HTTP Статус 503. Service Unavailable (Сервис недоступен). Это временная проблема на стороне сервера. Она показывает, что сама ссылка, возможно, существует, но сервер сейчас не может её обработать. Ситуация недоступности URL Shortener является исключительной. Проблему поддержания нужного количества экземпляров сервисов можно решить при помощи механизма health check. Насколько я знаю, такие механизмы доступны при развертывании в облаке.

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

292G урлов стоит понимать как теоретический максимум, вытекающий из анализа требований. По данным из https://www.techcult.ru/internet/14774-kolichestvo-sajtov-prevysilo-milliard на начало 2025 года в инете насчитывается примерно 1,1 млрд сайтов. Но, как, говорится, есть нюансы. Во-первых, активно развиваемых владельцами сайтов будет намного меньше (15-20%), а остальные, насколько я понимаю, доступны для просмотра, но информацию на них уже давно не обновляют. При этом ссылки доступны и отвечают. Во-вторых, насколько я понимаю, Вы исходите из предположения, что на один доступный URL сайта можно сделать только одну ссылку. Но есть такие ресурсы как ютуб, на котором каждое видео имеет свой урл. По оценке, опубликованной здесь https://habr.com/ru/companies/ru_mts/articles/784806/ таких урлов уже больше 13 миллиардов.

Приведенная в комментарии ссылка у меня не открывается. Обращение к БД всегда происходит только при создании ссылок. Если нужно перейти по короткой ссылке, то система вначале ищет её в кэше Redis и только при отсутствии ссылки там происходит обращение к БД. Но и в этом случае можно оптимизировать нагрузку на БД путем создания реплик. Запись идет только на мастер ноду, а чтение со слейв.

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

Про похожие статьи по данной теме я написал во введении

На Хабре есть несколько статей (раз, два, три, четыре) по данной теме

из опыта - чем заморочененее собеседование тем меньше итоговая работа имет отношение к "пройденым конкурсам" )))

Согласен)))

Про алгоритм генерации uid и про балансировку нагрузки будут отдельные статьи. Материала довольно много, поэтому решил не делать эту статью ещё длиннее.

1

Information

Rating
488-th
Location
Таганрог, Ростовская обл., Россия
Registered
Activity

Specialization

Разработчик мобильных приложений, Разработчик игр
Git
PostgreSQL
Python
Linux
Docker
SQL
ООП
REST
Golang
C++