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

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

По картинке думал будет инструкция по настоящим счётчикам как на картинке, а тут очередная фигня про веб:(
НЛО прилетело и опубликовало эту надпись здесь

На картинке счётчик постов, если приглядеться :)

Когда то делал подобное под Telligent Community. Было проще и без заумных рассуждений.

А расскажете? Как было, что было проще? И да, какие из моих рассуждений показались вам заумными, может я переформулирую? :)

Да рассказывать особо нечего. Заумные — в смысле сложные.

Ну так я же старался проще :) Подскажите что кажется сложным. Я надеялся, что самым понятным будет код, кроме которого можно особо ничего и не читать

Хм… Вроде как никогда не считал это проблемой. В большинстве случаев точность этих данных не сильно важна.

У нас в двигле за счетчики ответственен один конфигурируемый класс (в конфигурации задаются имена и пределы счетчиков) ничего заумного и хитрых формул. БД — 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 (обычно так и есть) — то обычно пишется хранимка, которая потом вызывается. Ну и, если индексация делается на уровне БД — то это всегда хранимка.

А понял, спасибо

Торопился :) Поправляюсь:

Каждый счетчик в конфигурации это: имя, начальное значение, мин (или отсутствие его), макс (или отсутствие его) и ф-я перерасчета.
у нас вся работа построена через очереди, соответственно в каждый месседж в очереди просто добавлено какой счетчик оно меняет и как, например {«counter»:«payments_mts_123», «value»:-1} соответственно что бы ни происходило, это отражается на счетчиках. ну и у нас финансовые транзакции и показатели, соответственно там и проще и сложнее

У нас тоже финансовые операции присутствуют, и, некоторые напрямую зависят от счётчиков. По этому когда я пришёл на проект и обнаружил 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.
С тех пор этот рейтинг никуда не двигается, независимо от полученных плюсов и минусов за комментарии, хотя раньше эта цифра плясала постоянно.
Я писал в поддержку, но ответа не получил.
Есть ли какой-нибудь хак, как на Хабре можно пересчитать счетчкики?
Спасибо.

Не, не могу помочь. На Хабре я давно не работаю

не хранить значения счетчиков, а вычислять, результаты вычисления кэшировать — чем такой вариант не подходит?

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


Второй вариант — значения счётчиков вам постоянно нужны для выборок.

Можно поподробнее, почему неправильно «Пересчитывать счётчик полностью при каждом изменении связанных с ним объектов»? Может, я неправильно понял, что имелось в виду. Зачем вообще инкрементировать счетчик в коде? Обычно это что-то вроде «SELECT COUNT(*) WHERE isDeleted == false AND isDraft == false». Добавьте своих условий, оберните в любимую ORM и готово.

Если дело в нагрузке — ну, вы же используете какие-нибудь виды кэша? memcache, например. Фактически, задача сводится к задаче инвалидации кэша.

Да, я имел в виду именно это. И да, дело, по большей части, в дороговизне полного пересчёта на каждое изменение. Никакое кеширование, тут помочь не может — что именно вы будете кешировать? :)

Кешировать именно значение счетчика.
0. При первом запросе страницы посчитали (SELECT COUNT...), положили в кэш.
1. При последующих запросах значение счетчика берется их кэша.
2. Что-то изменилось — удалили из кэша сохраненное значение.
3. GOTO 0.

Прогнозирую, что при слишком частых изменениях данные в кэше надолго не будут задерживаться, но тут зависит от частного случая. Если речь о такой нагрузке и счетчике с шестью нулями, то можно не удалять кэш после каждого изменения, а просто устанавливать время его «жизни», скажем, на 5-10 минут. Не суть важно, что написано на странице: 352874 комментария или 352875 комментариев.

А, понятно. Я сначала не понял, что вы про хранение счётчиков только в кеше. Я тут ответил.

Можно добавить триггеры в БД на события insert и delete, с увеличением и уменьшением значения поля счетчика. Возможно, ещё и на update, если того требует логика (как пример с постами-черновиками). Так счетчик всегда будет актуальным.

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


Идея ловить тригерами изменения и отправлять на обработку в очередь на той же БД (PGQ) мне, в принципе понравилась. Этот подход позволял отлавливать только изменения, при этом писать логику их обработки на нормальном питоне. Но сама PGQ по сравнению с Celery отвратительна — это раз. Триггеры отлавливающие изменения приходилось обновлять вместе с миграцией БД, короче не слишком приятно выходило. Но потенциал в этом подходе есть.

Вообще не так все. Для правильного подсчета счетчиков надо использовать очередь событий (для этого kafka можно взять) в которую писать лайк или анлайк. На очереди висит consumer(s) который пересчитывает в кеш количество лайков.

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


Допустим у вас в профиле список хабов, в которые вы внесли максимальный вклад. Это запрос по таблице счётчиков [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, а и пачка мета информации.
НЛО прилетело и опубликовало эту надпись здесь
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации