mdserver-web/plugins/webstats/tool_migrate.py

273 lines
9.2 KiB
Python
Raw Normal View History

2022-07-26 00:59:35 -04:00
# coding:utf-8
import sys
import io
import os
import time
import json
2026-07-02 00:27:43 -04:00
import fcntl
2026-07-06 08:27:17 -04:00
import shutil
2022-07-26 00:59:35 -04:00
2024-11-23 15:43:22 -05:00
web_dir = os.getcwd() + "/web"
if os.path.exists(web_dir):
sys.path.append(web_dir)
os.chdir(web_dir)
2022-07-26 00:59:35 -04:00
2024-11-23 15:43:22 -05:00
import core.mw as mw
from utils.crontab import crontab as MwCrontab
2022-07-26 00:59:35 -04:00
app_debug = False
if mw.isAppleSystem():
app_debug = True
def getPluginName():
return 'webstats'
def getPluginDir():
return mw.getPluginDir() + '/' + getPluginName()
def getServerDir():
return mw.getServerDir() + '/' + getPluginName()
2022-07-26 03:12:51 -04:00
def getConf():
conf = getServerDir() + "/lua/config.json"
return conf
def getGlobalConf():
conf = getConf()
content = mw.readFile(conf)
result = json.loads(content)
return result
def pSqliteDb(dbname='web_logs', site_name='unset', fn="logs"):
db_dir = getServerDir() + '/logs/' + site_name
if not os.path.exists(db_dir):
mw.execShell('mkdir -p ' + db_dir)
name = fn
file = db_dir + '/' + name + '.db'
if not os.path.exists(file):
conn = mw.M(dbname).dbPos(db_dir, name)
sql = mw.readFile(getPluginDir() + '/conf/init.sql')
sql_list = sql.split(';')
for index in range(len(sql_list)):
conn.execute(sql_list[index], ())
else:
conn = mw.M(dbname).dbPos(db_dir, name)
conn.execute("PRAGMA synchronous = 0", ())
conn.execute("PRAGMA page_size = 4096", ())
conn.execute("PRAGMA journal_mode = wal", ())
2022-07-28 12:52:58 -04:00
conn.autoTextFactory()
2022-07-28 12:50:36 -04:00
2022-07-26 03:12:51 -04:00
return conn
2026-07-06 08:01:23 -04:00
def batch_delete(conn, table, where_clause, batch_size=5000, pause_sec=0.05):
"""分批删除缩短单次持锁时间WAL 模式下可与 OpenResty 并发写入。"""
total = 0
while True:
sql = (
f"DELETE FROM {table} WHERE rowid IN "
f"(SELECT rowid FROM {table} WHERE {where_clause} LIMIT {batch_size})"
)
affected = conn.execute(sql)
if not isinstance(affected, int) or affected == 0:
break
total += affected
if pause_sec > 0:
time.sleep(pause_sec)
return total
2022-07-26 00:59:35 -04:00
def migrateSiteHotLogs(site_name, query_date):
2026-07-02 00:27:43 -04:00
start_time = time.time()
print(f"[{site_name}] 开始迁移日志...")
2022-07-26 03:12:51 -04:00
migrating_flag = getServerDir() + "/logs/%s/migrating" % site_name
hot_db = getServerDir() + "/logs/%s/logs.db" % site_name
hot_db_tmp = getServerDir() + "/logs/%s/logs_tmp.db" % site_name
history_logs_db = getServerDir() + "/logs/%s/history_logs.db" % site_name
2026-07-02 00:27:43 -04:00
if not os.path.exists(hot_db):
print(f"[{site_name}] 热日志数据库不存在,跳过")
return mw.returnMsg(True, f"{site_name} logs not exist, skip")
# 检查是否正在迁移
if os.path.exists(migrating_flag):
print(f"[{site_name}] 正在迁移中,跳过")
return mw.returnMsg(True, f"{site_name} is migrating, skip")
2026-07-06 07:24:36 -04:00
todayTime = time.strftime('%Y-%m-%d 00:00:00', time.localtime())
todayUt = int(time.mktime(time.strptime(
todayTime, "%Y-%m-%d %H:%M:%S")))
# 1. 备份到临时文件copy 期间短暂互斥,完成后立即解除)
2022-07-26 03:12:51 -04:00
try:
2026-07-02 00:27:43 -04:00
print(f"[{site_name}] 备份 {hot_db} -> {hot_db_tmp} ...")
2022-07-26 03:12:51 -04:00
mw.writeFile(migrating_flag, "yes")
2026-07-06 08:54:00 -04:00
time.sleep(0.5)
2026-07-06 08:05:10 -04:00
copy_start = time.time()
2026-07-06 08:49:20 -04:00
# import shutil
2026-07-06 08:27:17 -04:00
# shutil.copy(hot_db, hot_db_tmp)
2026-07-06 08:59:32 -04:00
mw.fastCopy(hot_db, hot_db_tmp, 256 * 1024)
2026-07-06 08:05:10 -04:00
print(f"[{site_name}] 备份完成,耗时 {time.time() - copy_start:.2f}s")
2022-07-26 03:12:51 -04:00
if not os.path.exists(hot_db_tmp):
2026-07-02 00:27:43 -04:00
return mw.returnMsg(False, f"{site_name} migrating fail, copy tmp file!")
except Exception as e:
2026-07-06 07:24:36 -04:00
return mw.returnMsg(False, f"{site_name} migrating fail: {e}")
finally:
2022-07-26 03:12:51 -04:00
if os.path.exists(migrating_flag):
os.remove(migrating_flag)
2026-07-06 07:24:36 -04:00
# 2. 从临时备份中迁移热日志数据到历史日志(读 logs_tmp不阻塞 live 写入)
2022-07-26 03:12:51 -04:00
try:
logs_conn = pSqliteDb('web_log', site_name, 'logs_tmp')
history_logs_conn = pSqliteDb('web_log', site_name, 'history_logs')
hot_db_columns = logs_conn.originExecute(
"PRAGMA table_info([web_logs])")
2026-07-02 00:27:43 -04:00
_columns = [c[1] for c in hot_db_columns if c[1] != "id"]
columns_str = ",".join(_columns)
placeholders = ",".join(["?"] * len(_columns))
logs_sql = f"select {columns_str} from web_logs where time<{todayUt}"
2022-07-26 03:12:51 -04:00
selector = logs_conn.originExecute(logs_sql)
2026-07-02 00:27:43 -04:00
# 批量插入配置
batch_size = 10000
batch_count = 0
total_count = 0
insert_sql = f"insert into web_logs({columns_str}) values({placeholders})"
while True:
logs = selector.fetchmany(batch_size)
if not logs:
break
batch_count += 1
total_count += len(logs)
# 使用原生 executemany 批量插入
history_logs_conn.executemany(insert_sql, logs)
history_logs_conn.commit()
if batch_count % 10 == 0:
elapsed = time.time() - start_time
print(f"[{site_name}] 已迁移 {total_count} 条记录, 耗时 {elapsed:.2f}s")
print(f"[{site_name}] 共迁移 {total_count} 条记录")
# 清理历史数据
2022-07-26 03:12:51 -04:00
gcfg = getGlobalConf()
save_day = gcfg['global']["save_day"]
2026-07-02 00:27:43 -04:00
print(f"[{site_name}] 删除 {save_day} 天前的历史数据...")
2022-07-26 03:12:51 -04:00
time_now = time.localtime()
2024-02-04 02:27:50 -05:00
save_timestamp = time.mktime((time_now.tm_year, time_now.tm_mon, time_now.tm_mday - save_day, 0, 0, 0, 0, 0, 0))
2026-07-02 00:27:43 -04:00
delete_sql = f"delete from web_logs where time <= {save_timestamp}"
2022-07-26 03:12:51 -04:00
history_logs_conn.execute(delete_sql)
history_logs_conn.commit()
2026-07-02 00:27:43 -04:00
history_logs_conn.execute("VACUUM;")
history_logs_conn.commit()
except Exception as e:
2026-07-06 07:24:36 -04:00
if os.path.exists(hot_db_tmp):
os.remove(hot_db_tmp)
print(f"[{site_name}] logs to history error: {e}")
2026-07-02 00:27:43 -04:00
return mw.returnMsg(False, f"{site_name} logs migrate error: {e}")
2026-07-06 08:01:23 -04:00
# 3. 分批删除已迁移的热日志并清理统计(不设 migrating 锁,不阻塞 OpenResty 写入)
2026-07-02 00:27:43 -04:00
try:
2022-07-26 03:12:51 -04:00
hot_db_conn = pSqliteDb('web_logs', site_name)
2026-07-06 08:01:23 -04:00
hot_db_conn.execute("PRAGMA busy_timeout = 30000")
2026-07-02 00:27:43 -04:00
2026-07-06 08:01:23 -04:00
print(f"[{site_name}] 分批删除已迁移的热日志...")
deleted = batch_delete(hot_db_conn, 'web_logs', f"time<{todayUt}")
print(f"[{site_name}] 已删除 {deleted} 条热日志")
2026-07-02 00:27:43 -04:00
2026-07-06 08:01:23 -04:00
print(f"[{site_name}] 分批删除180天前的统计数据...")
2022-07-26 03:12:51 -04:00
save_time_key = time.strftime(
2023-11-20 10:51:12 -05:00
'%Y%m%d00', time.localtime(time.time() - 180 * 86400))
2026-07-06 08:01:23 -04:00
stat_where = f"time<='{save_time_key}'"
for stat_table in ('request_stat', 'spider_stat', 'client_stat', 'referer_stat'):
stat_deleted = batch_delete(hot_db_conn, stat_table, stat_where)
print(f"[{site_name}] {stat_table} 已删除 {stat_deleted}")
2022-07-26 03:12:51 -04:00
2026-07-06 08:01:23 -04:00
print(f"[{site_name}] 执行 WAL checkpoint...")
hot_db_conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
2022-07-26 03:12:51 -04:00
except Exception as e:
2026-07-02 00:27:43 -04:00
print(f"[{site_name}] delete hot logs error: {e}")
2022-07-26 03:12:51 -04:00
finally:
if os.path.exists(hot_db_tmp):
os.remove(hot_db_tmp)
2026-07-06 09:02:13 -04:00
if os.path.exists(hot_db_tmp+"-shm"):
os.remove(hot_db_tmp+"-shm")
if os.path.exists(hot_db_tmp+"-wal"):
os.remove(hot_db_tmp+"-wal")
2022-07-26 03:12:51 -04:00
2026-07-02 00:27:43 -04:00
elapsed = time.time() - start_time
print(f"[{site_name}] 日志迁移完成,耗时 {elapsed:.2f}s")
2023-03-12 23:29:00 -04:00
if not mw.isAppleSystem():
mw.execShell("chown -R www:www " + getServerDir())
2023-03-15 06:28:31 -04:00
2026-07-02 00:27:43 -04:00
return mw.returnMsg(True, f"{site_name} logs migrate ok")
2022-07-26 00:59:35 -04:00
def migrateHotLogs(query_date="today"):
2026-07-02 00:27:43 -04:00
# 使用文件锁防止并发迁移
lock_file = getServerDir() + "/logs/migrate_hot_logs.lock"
lock_fd = None
try:
lock_fd = open(lock_file, 'w')
fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
print("开始迁移热日志...")
sites = mw.M('sites').field('name').order("add_time").select()
unset_site = {"name": "unset"}
sites.append(unset_site)
total_sites = len(sites)
success_count = 0
fail_count = 0
for i, site_info in enumerate(sites):
site_name = site_info["name"]
print(f"\n[{i+1}/{total_sites}] 处理站点: {site_name}")
migrate_res = migrateSiteHotLogs(site_name, query_date)
if migrate_res["status"]:
success_count += 1
else:
fail_count += 1
print(f"[{site_name}] 迁移失败: {migrate_res['msg']}")
print(f"\n迁移完成! 成功: {success_count}, 失败: {fail_count}")
return mw.returnMsg(True, f"logs migrate ok, success: {success_count}, fail: {fail_count}")
except BlockingIOError:
print("正在迁移中,跳过")
return mw.returnMsg(True, "正在迁移中,跳过")
except Exception as e:
print(f"migrateHotLogs error: {e}")
return mw.returnMsg(False, f"migrateHotLogs error: {e}")
finally:
if lock_fd:
try:
fcntl.flock(lock_fd.fileno(), fcntl.LOCK_UN)
lock_fd.close()
if os.path.exists(lock_file):
os.remove(lock_file)
except:
pass