#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ РОТАЦИЯ СТАКАНА — держать 7 суток вместо бесконечного накопления. Зачем: arena_l2_binance + arena_l2_bybit = 3.4 ГБ из 3.78 ГБ всей базы, растут ~300 МБ в сутки. Читает их только raas_api.py, и по статистике Postgres — 11 обращений за всю историю. Данные копятся и не используются. Осторожность: * удаляем ПАКЕТАМИ, чтобы не держать долгую блокировку на живой таблице; * перед удалением показываем, сколько останется — если после ротации в таблице окажется меньше MIN_KEEP строк, НЕ трогаем (значит сбор сломан и старое — всё что есть); * VACUUM после удаления не запускаем автоматически: место вернётся постепенно, а полный VACUUM FULL блокирует таблицу. python3 l2_rotate.py сухой прогон python3 l2_rotate.py --apply выполнить python3 l2_rotate.py --apply --days 14 """ import argparse, os, sys, time TABLES = ["arena_l2_binance", "arena_l2_bybit"] BATCH = 50_000 MIN_KEEP = 10_000 # если после ротации останется меньше — не трогаем def dsn(): d = os.environ.get("PGDSN") if d: return d # как подключаются боты: локальный Postgres, база sovereign for cand in ("/opt/arena/.pgdsn", "/opt/arena/grid_lab/.pgdsn"): if os.path.exists(cand): return open(cand).read().strip() return "host=127.0.0.1 port=5432 dbname=sovereign user=postgres" def main(): ap = argparse.ArgumentParser() ap.add_argument("--apply", action="store_true") ap.add_argument("--days", type=int, default=7) a = ap.parse_args() import psycopg2 conn = psycopg2.connect(dsn()) conn.autocommit = True cur = conn.cursor() print(f"хранить: {a.days} суток\n") plan = [] for t in TABLES: cur.execute(f"SELECT count(*) FROM {t}") total = cur.fetchone()[0] cur.execute(f"SELECT count(*) FROM {t} WHERE ts < now() - interval '{a.days} days'") old = cur.fetchone()[0] cur.execute(f"SELECT pg_size_pretty(pg_total_relation_size('{t}'))") size = cur.fetchone()[0] keep = total - old ok = keep >= MIN_KEEP print(f" {t:20} {size:>10} всего {total:>9,} старше {a.days}д {old:>9,} " f"останется {keep:>8,} {'ок' if ok else 'ПРОПУСК: останется мало'}") if ok and old: plan.append((t, old)) if not a.apply: print("\nсухой прогон. Для выполнения добавьте --apply") return for t, old in plan: print(f"\n{t}: удаляю {old:,} строк пакетами по {BATCH:,}") done, t0 = 0, time.time() while True: cur.execute( f"DELETE FROM {t} WHERE ctid IN (" f" SELECT ctid FROM {t} WHERE ts < now() - interval '{a.days} days' " f" LIMIT {BATCH})") n = cur.rowcount done += n if n: print(f" удалено {done:,} из {old:,} ({time.time()-t0:.0f}с)", flush=True) if n < BATCH: break cur.execute(f"SELECT count(*), pg_size_pretty(pg_total_relation_size('{t}')) FROM {t}") c, s = cur.fetchone() print(f" итог: {c:,} строк, {s}") print("\nМесто вернётся не сразу: страницы освобождаются автовакуумом.") print("Полный VACUUM FULL блокирует таблицу — вручную и в тихое время.") conn.close() if __name__ == "__main__": main()