Подсчёт уникальных посетителей при 100 000 страниц
Предпосылки: Клиенты и соединения, HyperLogLog.
Аналитическая система считает уникальных посетителей по каждой странице сайта за каждый день. Сайт имеет 100 000 страниц и миллионы пользователей. Нужны дневные, недельные и месячные агрегаты.
SET хранит каждый элемент как строку. Миллион user_id в SET — порядка 50 МБ. При 100 000 страниц × 30 дней = 3 миллиона SET’ов — это сотни гигабайт RAM. Bitmap требует числовые ID и отвечает на вопрос «был ли конкретный пользователь?», но не позволяет объединять дневные счётчики в недельные без дублирования. HyperLogLog расходует фиксированные 12 КБ на счётчик независимо от кардинальности, и PFMERGE объединяет дневные HLL в недельные с сохранением уникальности:
class UniqueVisitorTracker
def initialize(redis_pool)
@redis = redis_pool
end
def track(page_slug, visitor_id)
key = "uv:#{page_slug}:#{Date.today.iso8601}"
@redis.with { |r| r.pfadd(key, visitor_id) }
end
def daily_count(page_slug, date)
key = "uv:#{page_slug}:#{date.iso8601}"
@redis.with { |r| r.pfcount(key) }
end
def count_for_period(page_slug, dates)
keys = dates.map { |d| "uv:#{page_slug}:#{d.iso8601}" }
@redis.with do |r|
# PFCOUNT на нескольких ключах выполняет внутренний merge
r.pfcount(*keys)
end
end
def site_wide_uniques(date)
# Объединить HLL по всем страницам за день
# SCAN вместо KEYS — не блокирует event loop (см. [блокирующие команды](../blocking-pitfalls.md))
page_keys = @redis.with { |r| r.scan_each(match: "uv:*:#{date.iso8601}").to_a }
return 0 if page_keys.empty?
@redis.with { |r| r.pfcount(*page_keys) }
end
end3 миллиона HLL-счётчиков × 12 КБ = 36 ГБ. Те же данные в SET — сотни ГБ. Стандартная ошибка HyperLogLog — 0.81%. Для аналитических дашбордов, где «≈1 024 000» и «1 024 000» неразличимы, это приемлемо. Для точного ответа «был ли конкретный пользователь на странице?» HLL не подходит — для этого нужен SET или Bitmap.