Комментарии 48
У нас в двигле за счетчики ответственен один конфигурируемый класс (в конфигурации задаются имена и пределы счетчиков) ничего заумного и хитрых формул. БД — PostgreSQL.
Например счетчик постов устроен так — при опубликовании поста идет вызов из этого класса фии increment( counterName, +1), при скрытии increment( counterName, -1 ). В случае попытки декриментировать/инкрементировать меньше/больше предела возвращает false и не трогает счетчик. Счетчик просто хранится в ячейке таблицы БД счетчиков. Это просто ячейка — ничего более. Консистентность не проверяется.
Обычно работа со счетчиками идет в коде. Однако есть БД-шная часть с точно таким-же функционалом — можно на триггеры навешивать.
В том-же классе есть спец. функция indexing — она пересчитывает все известные и зарегистрированные в классе счетчики по заложенным в конфигурацию алгоритмам. На момент работы блочит таблицу счетчиков на запись.
P.S. Есть также возможность ленивых счетчиков (используется редко) — это когда просто отдаются данные и раз в какое-то время вызывается функция конкретно их перерасчета. Тоже работает норм.
P.P.S. indexing умеет перерассчитывать только один или несколько конкретных счетчиков, а не только все.
Хм… Не до конца понял. В одном из проектов я тоже делал возможность пересчёта одного или нескольких счётчиков. Счётчики задаются декларативными правилами типа:
class Experience(models.Model):
review_count = models.PositiveSmallIntegerField(u"Число отзывов", default=0, db_index=True)
review_rated_count = models.PositiveSmallIntegerField(u"Число отзывов с оценками", default=0)
review_rating_sum = models.FloatField(u"Сумма оценок", default=0)
class Counters(Counters):
review_count = Counter('Review.experience', lambda review: review.published)
review_rated_count = Counter(
'Review.experience',
lambda review: review.published and bool(review.rate)
)
review_rating_sum = Counter(
'Review.experience',
lambda review: review.rate if review.published and review.rate is not None else 0
)
class Review(models.Model):
experience = models.ForeignKey(Experience, related_name='reviews')
Т.е. тут тоже обычные счётчики даже не в отдельной таблице, а прямо в основных моделях.
В вашем случае increment( counterName, ±1) вызывается вручную в методах публикации/распубликации или это происходит автоматически на основе конфигурации? Как примерно устроена конфигурация? Как работает пересчёт?
Каждый счетчик в конфигурации это: имя, начальное значение и ф-я перерасчета.
Например перерасчет счетчика постов будет чем-то аля:
counter.value = $DB->selectCell( «select count(*) from `posts` where `show`='1'» ) в MySQL нотации.
Если счетчик сложный, но при этом укладывается в язык DB (обычно так и есть) — то обычно пишется хранимка, которая потом вызывается. Ну и, если индексация делается на уровне БД — то это всегда хранимка.
У нас тоже финансовые операции присутствуют, и, некоторые напрямую зависят от счётчиков. По этому когда я пришёл на проект и обнаружил MyISAM, первым делом бросился переводить на InnoDB и заворачивать в транзакции. Но к тому моменту накопилось столько мусорного кода, что банально не найдены и необеззаражены все места где что-то типа
order = Order.objects.get(pk=order_id)
order.status = Order.STATUS_PENDING_PAYMENT
order.save()
Т.е. без транзакций и SELECT FOR UPDATE, без, хотя бы save(update_fields=['status']) и прочее. И, конечно, это ломает счётчики.
А кто у вас добавляет в очередь пометку о необходимости обновить счётчик?
Например, в южных сранах в электросчетчике делается маленькое отверстие, куда капается сироп. Сквозь дырочку в счетчик попадают фараоновы муравьи и забивают весь механизм
В результтате, ничего не работает, несмотря на хитрую механику
У меня на Хабре постоянно отображается рейтинг -0.8, который упал до этой отметки однажды с чего-то в районе 60.
С тех пор этот рейтинг никуда не двигается, независимо от полученных плюсов и минусов за комментарии, хотя раньше эта цифра плясала постоянно.
Я писал в поддержку, но ответа не получил.
Есть ли какой-нибудь хак, как на Хабре можно пересчитать счетчкики?
Спасибо.
Иногда такой подход оправдан. Но для некоторых вещей просто неприемлем. Например, из Трипстера: запись на экскурсию с ограниченным числом участников. После того, как максимальное число людей оплатило, регистрация на событие закрывается, неоплаченные заказы отменяются, всем участникам и гиду приходят уведомления.
Второй вариант — значения счётчиков вам постоянно нужны для выборок.
Если дело в нагрузке — ну, вы же используете какие-нибудь виды кэша? memcache, например. Фактически, задача сводится к задаче инвалидации кэша.
Да, я имел в виду именно это. И да, дело, по большей части, в дороговизне полного пересчёта на каждое изменение. Никакое кеширование, тут помочь не может — что именно вы будете кешировать? :)
0. При первом запросе страницы посчитали (SELECT COUNT...), положили в кэш.
1. При последующих запросах значение счетчика берется их кэша.
2. Что-то изменилось — удалили из кэша сохраненное значение.
3. GOTO 0.
Прогнозирую, что при слишком частых изменениях данные в кэше надолго не будут задерживаться, но тут зависит от частного случая. Если речь о такой нагрузке и счетчике с шестью нулями, то можно не удалять кэш после каждого изменения, а просто устанавливать время его «жизни», скажем, на 5-10 минут. Не суть важно, что написано на странице: 352874 комментария или 352875 комментариев.
Да, только ели счётчиков много полностью на триггерах их довольно тяжело поддерживать. Добавление новых, переименование или изменение условий на существующих счётчиках, миграция структуры бд и т.п. будет каждый раз головной болью.
Идея ловить тригерами изменения и отправлять на обработку в очередь на той же БД (PGQ) мне, в принципе понравилась. Этот подход позволял отлавливать только изменения, при этом писать логику их обработки на нормальном питоне. Но сама PGQ по сравнению с Celery отвратительна — это раз. Триггеры отлавливающие изменения приходилось обновлять вместе с миграцией БД, короче не слишком приятно выходило. Но потенциал в этом подходе есть.
А чем очередь помогает? Считать в очереди или не нет это вопрос нужен ли вам синхронный счётчик или можно иметь значение с задержкой. В некоторых случаях при отказе в обновлении счётчика нужно откатить транзакцию оменить всё действие. Иногда это вопрос скорости отклика, если на какое-то действие (лайк, к примеру) обновляется слишком много счётчиков и это приведёт к ощутимой для пользователя задержке при синхронном обновлении, то нужно перенести часть рассчётов в очередь.
Допустим у вас в профиле список хабов, в которые вы внесли максимальный вклад. Это запрос по таблице счётчиков [hub_id, user_id] --> rating с сортировкой по убыванию рейтинга. Рейтинг на хабе это, к примеру, сумма рейтингов опубликованных и неудалённых постов пользователя на хабе. Что вы будете класть в очередь? Как на основе этого будете обновлять счётчик? Чем поможет в этом случае кеш?
Увеличивает доступность сервиса, так как ресурсоемкие вычисления производятся последовательно, а не параллельно.
Увеличивается отзывчивость интерфейса, так как пользователь не ждет обновления счетчика.
Ну тут мы кажется сошлись во мнениях, я в комментарии написал то же самое.
В случае изменения рейтинга/удаления/отправки в черновик в очередь отправится id поста.
Ну допустим пользователь отредактировал пост сменив хаб и скрыв в черновики. Как мы узнаем что хаб был сменён (а значит нужно вычесть рейтинг поста из рейтинга в старом хабе) и что пост не был в черновиках до этого?
кешем будет таблице счётчиков [hub_id, user_id] --> rating с сортировкой по убыванию рейтинга.
Ну если я потом смогу по этому кешу сделать запрос
SELECT hub_id, rating
FROM user_hub_rating
ORDER BY rating DESC
LIMIT 10
То у нас просто разное понимание терминологии что считать кешом.
Ок. Нам в очередь пришёл id поста и мы посмотрели по changelog-у, или получили сразу в виде параметров помимо post_id следующие поля: old_hub_id, old_is_published, old_is_deleted, old_user_id, и из базы (которая к этому моменту кстати могла опять измениться) или как-то ещё вычислили актуальные на момент срабатывания счётчика hub_id, is_published, is_deleted, user_id. Да, может это звучит необычно, но на dirty реализована передача черновика другому пользователю, по этому предположим что автор тоже может измениться. Какой вы напишите обработчик для обновления вашего «кеша» рейтинга пользователя на хабе?
Если схлопывать несколько обновлений в один, то готов с натяжкой согласиться. Тем не менее, два SELECT SUM() имеет сложность O(N1) + O(N2), где N1 и N2 число постов в старом и новом хабе (это при наличии индексов, при отсутствии это число всех постов на Хабре). Т.е. SQL, каким бы волшебным он не казался, честно пробежится по всем постам хаба и просуммирует рейтинг. Если схлопывания, про который вы писали, нет (а это иногда ограничение бизнес-требований), при большой соц. сети с кучей показателей, то полный пересчёт на производительности скажется драматически.
Рейтинг пользователя на хабе скорее всего да, не вызовет большого перебора, немного ступил. Но одновременно с ним нужно пересчитывать ещё рейтинг хаба. Там будет перебор всех постов хаба. Но мне всё же странно, почему вы наставиваете на полном пересчёте вместо инкрементного обновления, когда оно намного производительнее. Вам не верится что инкрементное обновление может не сбиться? :) Думаете что 1+1+1 в какой-то момент может стать 2 или 4, если повторять эту операцию много раз?
Логично что отправка письма в фоне с повтором при неудаче, т.к. уведомление тут вторично. Во всех проектах отправка email/sms/push и прочих уведомлений делается так. С утвердительным ответом от платёжной системы наоборот, т.к. оплата важнее. А к чему это вы?
Так как оно отправляется синхронно
А почему синхронно?
Ведь для создания тикета отправка письма это side effect. А как вообще вы ошибки хендлите? Как сделать еще N попыток отправки. У вас же в любой момент времени может отвалиться любая часть приложения/инфраструктуры. Как вы вообще достигаете HA?
Откатывать транзакцию или нет — зависит от бизнес-требований. Можем ли мы совершить действие если обновлние сётчика не гарантировано? Можем ли мы позволить себе иметь задержку при рассчёте счётчика? Можем ли мы не менять значение счётчика, если было подряд +1 и -1, или, например нам важно получить рейтинг 100, а потом обратно 99, т.к. при достижении 100 срабатывает триггер и пост становится золотым. Очереди — это замечательно, очень удобно и, во многих случаях незаменимо. Они просто не имеют, ИМХО, прямого отношения к теме топика.
Ну хорошо :) Вы используете очередь. В очереди пересчитываете счётчик полностью, т.к. инкрементное обновление считаете ненадёжным. Благодаря «схлопыванию» удаётся снизить накладные расходы, т.к. «при реактивном изменении счетчика (100500 хомячков в секунду) расчет производится только 1 раз на over 9000 фактических изменений». Я правильно вас понял?
Ну допустим пользователь отредактировал пост сменив хаб и скрыв в черновики. Как мы узнаем что хаб был сменён (а значит нужно вычесть рейтинг поста из рейтинга в старом хабе) и что пост не был в черновиках до этого?
Вообще для таких случаев используется CRDT. Вам просто в очередь надо положить unpublish событие. Ну и в очередь обычо складываются не только post_id, а и пачка мета информации.
Как считать счётчики и не сбиться со счёта