fix: 诊断脚本改用 venv/manage.py 并修复 grep 二进制日志
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,18 +1,16 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# 服务器卡顿定位 — 一次跑完,输出「卡在哪一层」结论
|
# 服务器卡顿定位 — 一次跑完,输出「卡在哪一层」结论
|
||||||
# 用法: bash deploy/profile-server-lag.sh
|
# 用法: bash deploy/profile-server-lag.sh
|
||||||
# 只读诊断,不改任何配置
|
|
||||||
|
|
||||||
set -e
|
|
||||||
DJANGO_DIR="/opt/1panel/apps/openresty/nginx/www/sites/43.142.166.152.443/django"
|
DJANGO_DIR="/opt/1panel/apps/openresty/nginx/www/sites/43.142.166.152.443/django"
|
||||||
NGINX_LOG="/opt/1panel/apps/openresty/nginx/log/access.log"
|
NGINX_LOG="/opt/1panel/apps/openresty/nginx/log/access.log"
|
||||||
|
PYTHON="${DJANGO_DIR}/venv/bin/python3"
|
||||||
cd "$DJANGO_DIR"
|
cd "$DJANGO_DIR"
|
||||||
|
|
||||||
echo "=============================================="
|
echo "=============================================="
|
||||||
echo " 星阙服务器卡顿定位 — $(date '+%Y-%m-%d %H:%M:%S')"
|
echo " 星阙服务器卡顿定位 — $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
echo "=============================================="
|
echo "=============================================="
|
||||||
|
|
||||||
# ---------- A. 进程层:Gunicorn 是否在排队 ----------
|
|
||||||
echo ""
|
echo ""
|
||||||
echo ">>> [A] Gunicorn 进程 / 端口"
|
echo ">>> [A] Gunicorn 进程 / 端口"
|
||||||
systemctl is-active gunicorn-dianjing.service 2>/dev/null || echo "systemd: inactive"
|
systemctl is-active gunicorn-dianjing.service 2>/dev/null || echo "systemd: inactive"
|
||||||
@@ -27,125 +25,96 @@ for path in "/" "/hqhd/"; do
|
|||||||
echo " 127.0.0.1:8001${path} -> ${T}s"
|
echo " 127.0.0.1:8001${path} -> ${T}s"
|
||||||
done
|
done
|
||||||
|
|
||||||
# ---------- B. 数据库层:握手 + 单条 SQL ----------
|
|
||||||
echo ""
|
echo ""
|
||||||
echo ">>> [B] MySQL 延迟(从本机 Django 目录测)"
|
echo ">>> [B] MySQL 延迟(Django 连接池)"
|
||||||
python3 - <<'PY'
|
"$PYTHON" manage.py shell -c "
|
||||||
import time, sys
|
import time
|
||||||
try:
|
from django.db import connection
|
||||||
import app_secrets
|
import app_secrets
|
||||||
import pymysql
|
print(f' 当前配置: {app_secrets.DATABASE_HOST}:{app_secrets.DATABASE_PORT} / {app_secrets.DATABASE_NAME}')
|
||||||
except Exception as e:
|
for label, sql in [('连接+查询', 'SELECT 1'), ('订单count', 'SELECT COUNT(*) FROM orders_order WHERE Status IN (1,7)')]:
|
||||||
print(f" 无法导入: {e}")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
host, port = app_secrets.DATABASE_HOST, int(app_secrets.DATABASE_PORT)
|
|
||||||
user, pwd, db = app_secrets.DATABASE_USER, app_secrets.DATABASE_PASSWORD, app_secrets.DATABASE_NAME
|
|
||||||
print(f" 当前配置: {host}:{port} / {db}")
|
|
||||||
|
|
||||||
def bench(label, h, p):
|
|
||||||
try:
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
conn = pymysql.connect(host=h, port=p, user=user, password=pwd, database=db, connect_timeout=5)
|
|
||||||
t_conn = (time.perf_counter() - t0) * 1000
|
|
||||||
t1 = time.perf_counter()
|
|
||||||
with conn.cursor() as c:
|
|
||||||
c.execute("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=%s", (db,))
|
|
||||||
c.fetchone()
|
|
||||||
t_q = (time.perf_counter() - t1) * 1000
|
|
||||||
conn.close()
|
|
||||||
print(f" [{label}] {h}:{p} 新建连接={t_conn:.0f}ms 单条SQL={t_q:.0f}ms")
|
|
||||||
return t_conn, t_q
|
|
||||||
except Exception as e:
|
|
||||||
print(f" [{label}] {h}:{p} 失败: {e}")
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
cur_c, cur_q = bench("生产库", host, port)
|
|
||||||
loc_c, loc_q = bench("本机Docker", "172.17.0.15", 3306)
|
|
||||||
|
|
||||||
print("")
|
|
||||||
if cur_q and cur_q > 30:
|
|
||||||
print(" >>> 结论B: 数据库在远程,每条SQL ~50ms+,接口跑10+条SQL就会慢200ms+")
|
|
||||||
if loc_q and loc_q < 10 and cur_q and cur_q > 30:
|
|
||||||
print(" >>> 修复: app_secrets.py 改 DATABASE_HOST=172.17.0.15 PORT=3306 后 restart gunicorn")
|
|
||||||
PY
|
|
||||||
|
|
||||||
# ---------- C. Django 层:模拟 dddhq 打了多少条 SQL ----------
|
|
||||||
echo ""
|
|
||||||
echo ">>> [C] /dingdan/ddhq 一次请求打几条 SQL、耗时多少"
|
|
||||||
python3 - <<'PY'
|
|
||||||
import os, sys, time
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'a_long_dianjing.settings')
|
|
||||||
sys.path.insert(0, '.')
|
|
||||||
|
|
||||||
try:
|
|
||||||
import django
|
|
||||||
django.setup()
|
|
||||||
from django.db import connection, reset_queries
|
|
||||||
from django.conf import settings
|
|
||||||
settings.DEBUG = True # 仅本脚本进程内开启 query log
|
|
||||||
|
|
||||||
reset_queries()
|
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
|
with connection.cursor() as c:
|
||||||
|
c.execute(sql)
|
||||||
|
c.fetchone()
|
||||||
|
ms = (time.perf_counter() - t0) * 1000
|
||||||
|
print(f' [{label}] {ms:.0f}ms')
|
||||||
|
" 2>/dev/null || echo " B段失败,请确认 venv 可用"
|
||||||
|
|
||||||
# 不跑完整 view(要 token),只测典型 ORM 链:订单列表 count + page
|
|
||||||
from orders.models import Order
|
|
||||||
from django.db.models import Q
|
|
||||||
qs = Order.query.filter(Q(Status__in=[1, 7])).order_by('-CreateTime')
|
|
||||||
total = qs.count()
|
|
||||||
page = list(qs[:5].values('OrderID', 'Status', 'CreateTime'))
|
|
||||||
|
|
||||||
elapsed = (time.perf_counter() - t0) * 1000
|
|
||||||
n = len(connection.queries)
|
|
||||||
sql_ms = sum(float(q.get('time', 0)) for q in connection.queries) * 1000
|
|
||||||
|
|
||||||
print(f" 仅订单 count+取5条: SQL条数={n} Django记录SQL耗时={sql_ms:.0f}ms 总耗时={elapsed:.0f}ms 订单总数={total}")
|
|
||||||
print(f" (完整 /ddhq 还有会员/标签/商家等查询,实际 SQL 通常 15~30 条)")
|
|
||||||
if n > 0:
|
|
||||||
print(" 最慢的3条SQL:")
|
|
||||||
for q in sorted(connection.queries, key=lambda x: float(x.get('time', 0)), reverse=True)[:3]:
|
|
||||||
sql = q['sql'][:120].replace('\n', ' ')
|
|
||||||
print(f" {float(q['time'])*1000:.0f}ms {sql}...")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" Django 测速失败: {e}")
|
|
||||||
import traceback; traceback.print_exc()
|
|
||||||
PY
|
|
||||||
|
|
||||||
# ---------- D. Nginx 层:最近真实接口 rt= 分布 ----------
|
|
||||||
echo ""
|
echo ""
|
||||||
echo ">>> [D] Nginx 最近 hqhd 接口 rt=(真实用户体感)"
|
echo ">>> [C] 订单列表 ORM(近似 dddhq 核心查询)"
|
||||||
|
"$PYTHON" manage.py shell -c "
|
||||||
|
import time
|
||||||
|
from django.db import connection, reset_queries
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db.models import Q
|
||||||
|
from orders.models import Order
|
||||||
|
settings.DEBUG = True
|
||||||
|
reset_queries()
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
qs = Order.query.filter(Q(Status__in=[1, 7])).order_by('-CreateTime')
|
||||||
|
total = qs.count()
|
||||||
|
page = list(qs[:5].values('OrderID', 'Status', 'CreateTime'))
|
||||||
|
elapsed = (time.perf_counter() - t0) * 1000
|
||||||
|
n = len(connection.queries)
|
||||||
|
sql_ms = sum(float(q.get('time', 0)) for q in connection.queries) * 1000
|
||||||
|
print(f' SQL条数={n} SQL耗时={sql_ms:.0f}ms 总耗时={elapsed:.0f}ms 订单总数={total}')
|
||||||
|
" 2>/dev/null || echo " C段失败"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ">>> [D] Nginx 真实接口 rt=(这是最终体感,最重要)"
|
||||||
if [ -f "$NGINX_LOG" ]; then
|
if [ -f "$NGINX_LOG" ]; then
|
||||||
echo " 最近20条 hqhd:"
|
echo " 最近20条 hqhd:"
|
||||||
grep 'hqhd' "$NGINX_LOG" | tail -20 | while read -r line; do
|
grep -a 'hqhd' "$NGINX_LOG" | tail -20 | while read -r line; do
|
||||||
RT=$(echo "$line" | grep -oP 'rt=\K[0-9.]+' || echo "?")
|
RT=$(echo "$line" | grep -oP 'rt=\K[0-9.]+' || echo "?")
|
||||||
URI=$(echo "$line" | grep -oP '"(GET|POST) \K[^ ]+' || echo "?")
|
URI=$(echo "$line" | grep -oP '"(GET|POST) \K[^ ]+' || echo "?")
|
||||||
echo " rt=${RT}s $URI"
|
echo " rt=${RT}s $URI"
|
||||||
done
|
done
|
||||||
echo ""
|
echo ""
|
||||||
echo " 各接口 rt 中位数(最近500条 hqhd):"
|
STATS=$(grep -a 'hqhd' "$NGINX_LOG" | tail -500 | grep -oP 'rt=\K[0-9.]+' | sort -n | awk '
|
||||||
grep 'hqhd' "$NGINX_LOG" | tail -500 | grep -oP 'rt=\K[0-9.]+' | sort -n | awk '
|
|
||||||
{ a[NR]=$1; sum+=$1 }
|
{ a[NR]=$1; sum+=$1 }
|
||||||
END {
|
END {
|
||||||
if(NR==0){ print " 无数据"; exit }
|
if(NR==0){ print "NONE"; exit }
|
||||||
mid=(NR%2)?a[(NR+1)/2]:(a[NR/2]+a[NR/2+1])/2
|
mid=(NR%2)?a[(NR+1)/2]:(a[NR/2]+a[NR/2+1])/2
|
||||||
printf " 样本=%d 中位rt=%.3fs 平均rt=%.3fs 最大rt=%.3fs\n", NR, mid, sum/NR, a[NR]
|
printf "%d %.3f %.3f %.3f", NR, mid, sum/NR, a[NR]
|
||||||
}'
|
}')
|
||||||
|
if [ "$STATS" != "NONE" ]; then
|
||||||
|
N=$(echo "$STATS" | awk '{print $1}')
|
||||||
|
MID=$(echo "$STATS" | awk '{print $2}')
|
||||||
|
AVG=$(echo "$STATS" | awk '{print $3}')
|
||||||
|
MAX=$(echo "$STATS" | awk '{print $4}')
|
||||||
|
echo " 最近500条: 样本=$N 中位rt=${MID}s 平均rt=${AVG}s 最大rt=${MAX}s"
|
||||||
|
echo ""
|
||||||
|
if awk -v m="$MID" 'BEGIN{exit(m>0.15)}'; then
|
||||||
|
echo " >>> 结论D: API 偏慢(中位>150ms),查数据库/B/C段"
|
||||||
|
elif awk -v m="$MID" 'BEGIN{exit(m>0.08)}'; then
|
||||||
|
echo " >>> 结论D: API 略慢但可接受(80~150ms)"
|
||||||
|
else
|
||||||
|
echo " >>> 结论D: 服务器 API 正常(中位<80ms),和2月一样快"
|
||||||
|
echo " 若小程序还卡,不是后端接口慢,查:手机网络/图片加载/页面连发多个接口"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo " 最近慢请求 rt>500ms:"
|
||||||
|
grep -a 'hqhd' "$NGINX_LOG" | grep -E 'rt=0\.[5-9]|rt=[1-9]' | tail -10 | while read -r line; do
|
||||||
|
RT=$(echo "$line" | grep -oP 'rt=\K[0-9.]+' || echo "?")
|
||||||
|
URI=$(echo "$line" | grep -oP '"(GET|POST) \K[^ ]+' || echo "?")
|
||||||
|
echo " rt=${RT}s $URI"
|
||||||
|
done
|
||||||
else
|
else
|
||||||
echo " 找不到 $NGINX_LOG"
|
echo " 找不到 $NGINX_LOG"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------- E. 系统层:CPU/内存/磁盘/journal ----------
|
|
||||||
echo ""
|
echo ""
|
||||||
echo ">>> [E] 系统资源"
|
echo ">>> [E] 系统资源"
|
||||||
echo " load: $(cat /proc/loadavg 2>/dev/null || uptime)"
|
echo " load: $(cat /proc/loadavg 2>/dev/null || uptime)"
|
||||||
free -h 2>/dev/null | head -2 || true
|
free -h 2>/dev/null | head -2 || true
|
||||||
journalctl --disk-usage 2>/dev/null | head -1 || true
|
journalctl --disk-usage 2>/dev/null | head -1 || true
|
||||||
|
|
||||||
# ---------- F. 并发层:谁在狂刷 ----------
|
|
||||||
echo ""
|
echo ""
|
||||||
echo ">>> [F] 最近1分钟请求最多的 IP+接口(是否有人刷接口拖慢)"
|
echo ">>> [F] 请求最多的 IP+接口"
|
||||||
if [ -f "$NGINX_LOG" ]; then
|
if [ -f "$NGINX_LOG" ]; then
|
||||||
tail -3000 "$NGINX_LOG" | grep 'hqhd' | awk '{
|
tail -3000 "$NGINX_LOG" | grep -a 'hqhd' | awk '{
|
||||||
for(i=1;i<=NF;i++) if($i ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) ip=$i
|
for(i=1;i<=NF;i++) if($i ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) ip=$i
|
||||||
if(match($0, /"(GET|POST) ([^ ]+)/, m)) uri=m[2]
|
if(match($0, /"(GET|POST) ([^ ]+)/, m)) uri=m[2]
|
||||||
if(ip && uri) c[ip" "uri]++
|
if(ip && uri) c[ip" "uri]++
|
||||||
@@ -154,15 +123,8 @@ if [ -f "$NGINX_LOG" ]; then
|
|||||||
for(k in c) arr[n++]=c[k]" "k
|
for(k in c) arr[n++]=c[k]" "k
|
||||||
asort(arr)
|
asort(arr)
|
||||||
for(i=n;i>0 && i>n-10;i--) print " "arr[i]
|
for(i=n;i>0 && i>n-10;i--) print " "arr[i]
|
||||||
}' 2>/dev/null || echo " (需 gawk 支持 match 第三参数,可忽略)"
|
}' 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=============================================="
|
echo "=============================================="
|
||||||
echo " 怎么看结果:"
|
|
||||||
echo " - A 里 curl >1s 且 B 里 SQL>30ms → 卡在数据库远程连接"
|
|
||||||
echo " - A 里 curl 快但 D 里 rt 慢 → 卡在 Nginx/上游配置或并发"
|
|
||||||
echo " - C 里 SQL条数>20 → 卡在接口 SQL 太多(后端代码)"
|
|
||||||
echo " - E 里 journal 很大/load 很高 → 卡在系统资源"
|
|
||||||
echo " - F 里同一 IP 每秒同一接口 → 卡在被人刷接口"
|
|
||||||
echo "=============================================="
|
|
||||||
|
|||||||
Reference in New Issue
Block a user