Как стать автором
Обновить

Комментарии 19

Почему было выбрано шардирование для распределения нагрузки ?

(Если не хранить в БД логи и файлы, то операций записи в базу будет относительно не много. А операции по чтению замечательно распределяются по readonly-репликам. )

Очень хорошо про логи подмечено, логи как правило это небольшие записи но они делаются очень часто, читаются редко. Логи к тому же редко хранятся в реляционных базках, о которых и шла речь в статье.
А вот когда у тебя много данных пишется и читается, причем с произвольным доступом, и ко всему к этому есть еще не функциональное требование "надо что бы отвечало менее 30мс". То шардинг тут не не сказать что избыточен, это классический путь размазывания нагрузки по железкам. Синхронные реплики помогут растащить читающую нагрузку, безусловно, но в записи нам это все еще ничего не прибавит.
И тут как всегда мы начинаем искать компромисс. И шардирование порой проще чем внедрение условного KV, особенно в больших компаниях.
С тезисом что в подавляющем большинстве случаев шардинг избыточен можно согласится, потому что наверное большая часть приложений в мире не имеет больших нагрузок и жестких требований к отзывчивости.

  1. Если коммит не синхронный, то реплики могут отставать. Если синхронный, то скорость записи сильно падает. С этим можно жить, но см. пункт 2.

  2. Если коммит несинхронный, то может возникнуть ситуация, когда WAL копится и не применяется на репликах из-за постоянных запросов. В итоге WAL забьется, и упадёт запись.

  3. Статистика хранится на мастере и реплицируется на реплики, то есть читать только с реплик нельзя, нужно чтение и на мастере

  4. Рано или поздно размер данных превысит какой-то порог, индекс перестанет попадать в кеш целиком, поедет статистика, pg начнет ошибаться в планах

Это из тех проблем, что я навскидку видел при отсутствии шардинга на условно больших Postgres базах

Масштабироваться по чтению через реплики это костыль

Я не понял - в начале пишется, что лучше шардировать средствами БД, а статья про приложения.

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

Если БД поддерживает нативное шардирование, то конечно лучше использовать его. В случае с postgresql к сожалению нативного шардирования нет.

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

Постгрес поддерживает шардирование при помощи комбинации партиционирования + FDW. Каждая секция таблицы смотрит на свой foreign server (который может быть HA кластером)

ulong/bigint - я хоть убей не понимаю, почему разработчики БД не сделают наконец беззнаковые типы? Это какое-то профессиональное упрямство продолжать выпускать версию за версией движков всех мастей с сотнями новых фич управления данными, но без совместимости с полнодиапазонными типами современных (уже лет 40 как) процедурных ЯП.

А почему просто не перейти на шардируемую БД?

Если есть возможность без боли перейти на шардируемую БД, то конечно лучше сделать так. Но расширение стека это довольно больная тема, и чем больше компания, тем больнее. Надо будет ответить на ряд вопросов: а какие гарантии у БД, а как долго будет происходить миграция, а кто будет это администрировать, а как быстро и за какую сумму можно найти экспертизу на рынке труда (разработчики, админы) ?
Если есть возможность в приемлемые сроки для бизнеса решить эти вопросы, то конечно стоит рассмотреть варианты с шардируемой БД.

Еще бы были нормальные.

Задачу получения консистентного бэкапа всех шардов как решили ?

Конкретно в сервисах нашей команды мы эту задачу не решали. Нам повезло. У нас достаточно держать данные консистентными в рамках одного шарда. Вопросом бекапов самих баз у нас занимается выделенная команда. 
Консистентный бекап это проблема которой стоит избегать, стараясь создавать такую схему данных, которая позволит определять шард как нечто автономное. Например нам надо шардировать БД с карточками товара, которая содержит инфу о товаре, цену, комментарии.
В случае если мы шардируем карточку по слоям - инфу о товаре на один шард, цену на второй а коменты на третий. То встает вопрос консистетного бекапа. А если мы шардируем по принципу id-товара%кол-во шардов = номер шарда на котором лежит и инфа о товаре и цена и коменты, то достаточно решить проблему надежного бекапирования каждого шарда отдельно.

@worldbugинтересный опыт, спасибо, что поделились.

Шардирование на приложении так же помогает строго соблюдать границы применимости хранилища и реализовывать бизнес-логику явно понимая, что у вас шарды, т.е. даже если у вас шардирование средствами БД - она не будет себя вести как одна очень большая база, например, точно не получится делать JOIN-ы.

Вы, как овнеры сервисы понятно, что адаптировались к новой реальности. А что произошло с вашими клиентами:

  1. Подразумевается, что клиент, который вызывает getItemsByIDдолжен сам ретраить, если запрос до базы не прошел? При этом какая обычно логика ретраев у ваших клиентов - запросить все заново или только то, что не пришло, или он заранее знает, что там CA система (по CAP теореме)? Вижу, что через ошибку протекает абстракция шардов до клиентов.

  2. Вызывающий AddItems,если не удалось, записать тоже ретраит? С дедлайном ли это делает? Допустим он читает с Kafka и пишет в базу, часть данных записалось, часть нет - что при этом он делает с сообщением - commit или dead letter queue?

  3. Есть ли какие-либо ограничения на вызовы со стороны клиента, типа "максимальное количество затрагиваемых шардов". Например, клиентский запрос может содержать ID со всех шардов, таким образом он может формировать request-ы так, что capacity всей шардированной базы будет равно capacity одного шарда. Т.е. допустим 1 шард держит нагрузку 10K RPS, и шардов 32 штуки, при этом клиент в request пихает 32 ID с каждого шардого по 1 ID, и делает 10K RPS => он положил все ваши 32 шарда. Требования к устойчивости к нагрузке не выполнено.

Еще парадокс, лучше не показывайте это сообщение менеджерам =)

Допустим железка из под шарда стоит 10k$, и максимум может обслуживать 10k RPS.

Таким образом цена обслуживания 1 RPS == 1$, однако если клиент формирует запрос, который касается нескольких шардов, то стоимость обслуживания будет 1 RPS == N$, где N, число затрагиваемых шардов.

В итоге получается платим больше, но держим RPS столько же ;)


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

За железку вы же платите один раз , а запросы обрабатываете постоянно. Единственное это стоимость обслуживания - диски+память(которые выходят из строя)+канал интернета.

Мне кажется цена rpc должна быть меньше

Спасибо за оценку.
Код в статье это просто демо, ни в коем случае не продакшен. Протекающие абстракции в демо-коде допустимы, дабы не размывать суть примера чистым кодом.
Понятное дело клиент который дергает GRPC метод не получает в случае ошибки sql`ный error. В проде если мы не получили данные с шарда на пакетный запрос, в зависимости от бизнес требований мы можем вернуть ответом

  1. Обозначить что не смогли получить данные по ID

  2. Вообще не обозначать никак ошибку при получении данных, прикинутся что такого ID не существует и вернуть пустое поле

  3. Обозначить что при попытке получения данных по ID мы получили ошибку

По поводу ретраев и ограничений, у нас собственный фреймворк scratch (можете найти доклады с конференций о нем, даже когда то Авито вдохновились и сделали свой scratch). Там встроенная поддержка ретраев, рейтлимитов и CircuitBreaker`ов. На стороне приложения надо просто настроить, далее интерфейсный слой сделает все за вас.
То же самое с точки зрения Storage, в нашей платформенной библиотеке для работы с sql базами есть встроенный рейтлимимтер, который на уровне sql дравйвера не дает приложению по ошибке заDDoSить базу.
Тут с точки зрения архитектуры я бы придерживался позиции что сервисный слой не должен думать о том что он может положить базу, пусть об этом забоится sql драйвер, в крайнем случае верхнеуровневая имплементация интерфейса storage / repository.

Есть где почитать об этих наработках?

Что делать, если надо сделать JOIN данных средствами БД, но они на разных шардах?

Например, клиент попал по id на один шард, его заказ - на другой.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий