降低线上轮询压力:后台标签页暂停轮询、关闭 access log
Browse files线上容器(cpu-basic,2 共享 vCPU)实测 1841 req/s,超出承载数倍,
导致连返回静态 JS 都要 55 秒。日志拆解后,热点全是自家前端的全局轮询接口:
/auth/session 375/s
/api/chat-room/unread 359/s
/api/chat-room/direct-conversations 350/s
/api/presence/ping 334/s
/api/announcements 106/s
四项改动(均不改变任何功能,用户感知不到差别):
1) Dockerfile 去掉 --access-log
实测 access log 占日志体积一半(约 285 KB/s、1800 行/秒),容器只有
2 核,每条请求都多一次格式化 + write 系统调用。注意 main.py 末尾的
uvicorn.run(access_log=False) 在线上不会执行(CLI 启动),所以必须改这里。
2) use-chat-room-unread:只在页面可见时轮询
这个 hook 挂在全局导航,每个标签页都在跑,含切到后台/被遗忘的标签页。
后台没人看角标,请求却照打。切回页面时立刻补拉一次。
3) announcements-popover:同样只在可见时轮询
该组件在顶栏挂了 2 份(移动端/桌面端各一),每个标签页跑两份。
4) use-auth-guard:权限兜底同步 5s → 30s
原本是 force 校验(绕过本地缓存直打服务端),是全站最重接口、占请求 20%。
权限和登录态不会 5 秒一变;focus / visibilitychange 仍会立刻强制校验一次,
真实感知比 30 秒更灵敏。
presence 心跳保持后台上报(约 25s)不变 —— 那是「在线人数」要统计后台标签页的
有意设计,属功能行为,未改动。
真机验证(artifacts/_visibility_gate_test.py)10/10 通过:
可见时 unread / direct-conversations 正常轮询
切后台后四项 gated 接口 45s 内全部 0 次,而 presence 心跳仍在
切回前台 5s 内立刻补拉 unread / direct-conversations / announcements
- Dockerfile +5 -1
- web/src/components/announcements-popover.tsx +27 -2
- web/src/lib/use-auth-guard.ts +10 -1
- web/src/lib/use-chat-room-unread.ts +28 -2
|
@@ -69,4 +69,8 @@ PY
|
|
| 69 |
|
| 70 |
EXPOSE 7860
|
| 71 |
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
EXPOSE 7860
|
| 71 |
|
| 72 |
+
# ⚠️ 不要加 --access-log:线上实测它占日志体积一半(约 285 KB/s,1800 条/秒),
|
| 73 |
+
# 而容器只有 2 个共享 vCPU,每条请求都多一次格式化 + write 系统调用。
|
| 74 |
+
# 注意 main.py 末尾的 uvicorn.run(access_log=False) 在线上**不会执行**(这里是 CLI 启动),
|
| 75 |
+
# 所以只改 main.py 无效,必须在这里保持一致。
|
| 76 |
+
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
@@ -166,10 +166,35 @@ export function AnnouncementsPopover({ session }: { session: StoredAuthSession }
|
|
| 166 |
}
|
| 167 |
};
|
| 168 |
void load();
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
return () => {
|
| 171 |
active = false;
|
| 172 |
-
|
|
|
|
| 173 |
};
|
| 174 |
}, [itemsStorageKey]);
|
| 175 |
|
|
|
|
| 166 |
}
|
| 167 |
};
|
| 168 |
void load();
|
| 169 |
+
|
| 170 |
+
// ⚠️ 只在页面可见时轮询:这个组件在顶栏挂了 2 份(移动端 / 桌面端各一),
|
| 171 |
+
// 每个标签页都在跑。后台标签页没人看公告,却在持续打服务端。
|
| 172 |
+
// 切回页面时立刻补拉一次,感知不到差别。
|
| 173 |
+
let timer: number | undefined;
|
| 174 |
+
const stopPolling = () => {
|
| 175 |
+
if (timer === undefined) return;
|
| 176 |
+
window.clearInterval(timer);
|
| 177 |
+
timer = undefined;
|
| 178 |
+
};
|
| 179 |
+
const startPolling = () => {
|
| 180 |
+
if (timer !== undefined) return;
|
| 181 |
+
timer = window.setInterval(() => void load(), 5 * 60_000);
|
| 182 |
+
};
|
| 183 |
+
const onVisibilityChange = () => {
|
| 184 |
+
if (document.visibilityState === "visible") {
|
| 185 |
+
void load();
|
| 186 |
+
startPolling();
|
| 187 |
+
} else {
|
| 188 |
+
stopPolling();
|
| 189 |
+
}
|
| 190 |
+
};
|
| 191 |
+
|
| 192 |
+
startPolling();
|
| 193 |
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
| 194 |
return () => {
|
| 195 |
active = false;
|
| 196 |
+
stopPolling();
|
| 197 |
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
| 198 |
};
|
| 199 |
}, [itemsStorageKey]);
|
| 200 |
|
|
@@ -15,7 +15,16 @@ type UseAuthGuardResult = {
|
|
| 15 |
session: StoredAuthSession | null;
|
| 16 |
};
|
| 17 |
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
function sameSession(left: StoredAuthSession | null, right: StoredAuthSession) {
|
| 21 |
return Boolean(
|
|
|
|
| 15 |
session: StoredAuthSession | null;
|
| 16 |
};
|
| 17 |
|
| 18 |
+
/**
|
| 19 |
+
* 权限 / 登录态的兜底同步间隔。
|
| 20 |
+
*
|
| 21 |
+
* ⚠️ 原来是 5 秒一次 **force** 校验(绕过 10 秒本地缓存,每次都实打服务端),
|
| 22 |
+
* 这是全站最重的轮询接口 —— 线上实测 /auth/session 约 375 req/s,占全站请求 20%。
|
| 23 |
+
* 而权限和登录态本来就不会 5 秒一变,30 秒完全够用;
|
| 24 |
+
* 另外下面还监听 focus / visibilitychange,用户切回标签页会**立刻**强制校验一次,
|
| 25 |
+
* 所以真实感知比 30 秒更灵敏。
|
| 26 |
+
*/
|
| 27 |
+
const AUTH_PERMISSION_SYNC_INTERVAL_MS = 30_000;
|
| 28 |
|
| 29 |
function sameSession(left: StoredAuthSession | null, right: StoredAuthSession) {
|
| 30 |
return Boolean(
|
|
@@ -141,11 +141,37 @@ export function useChatRoomUnread(enabled: boolean, active: boolean) {
|
|
| 141 |
window.addEventListener("chatgpt2api:chat-room-read", onRead);
|
| 142 |
|
| 143 |
// 停在聊天室页面时无需轮询(页面内部自己会更新已读)。
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
return () => {
|
| 147 |
disposed = true;
|
| 148 |
-
|
|
|
|
| 149 |
window.removeEventListener("chatgpt2api:chat-room-read", onRead);
|
| 150 |
};
|
| 151 |
}, [enabled, active]);
|
|
|
|
| 141 |
window.addEventListener("chatgpt2api:chat-room-read", onRead);
|
| 142 |
|
| 143 |
// 停在聊天室页面时无需轮询(页面内部自己会更新已读)。
|
| 144 |
+
//
|
| 145 |
+
// ⚠️ 还要**只在页面可见时轮询**:这个 hook 挂在全局导航上,每个打开页面的标签页
|
| 146 |
+
// 都会跑(含切到后台/被遗忘的标签页)。后台标签页的未读角标没人看,
|
| 147 |
+
// 但请求照样打满服务端 —— 线上实测这几个轮询接口合计上千 req/s,把 2 核容器压垮。
|
| 148 |
+
// 切回页面时下面会立刻补拉一次,所以用户感知不到任何差别。
|
| 149 |
+
let timer: number | undefined;
|
| 150 |
+
const stopPolling = () => {
|
| 151 |
+
if (timer === undefined) return;
|
| 152 |
+
window.clearInterval(timer);
|
| 153 |
+
timer = undefined;
|
| 154 |
+
};
|
| 155 |
+
const startPolling = () => {
|
| 156 |
+
if (active || timer !== undefined) return;
|
| 157 |
+
timer = window.setInterval(load, UNREAD_POLL_INTERVAL_MS);
|
| 158 |
+
};
|
| 159 |
+
const onVisibilityChange = () => {
|
| 160 |
+
if (document.visibilityState === "visible") {
|
| 161 |
+
void load();
|
| 162 |
+
startPolling();
|
| 163 |
+
} else {
|
| 164 |
+
stopPolling();
|
| 165 |
+
}
|
| 166 |
+
};
|
| 167 |
+
|
| 168 |
+
startPolling();
|
| 169 |
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
| 170 |
|
| 171 |
return () => {
|
| 172 |
disposed = true;
|
| 173 |
+
stopPolling();
|
| 174 |
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
| 175 |
window.removeEventListener("chatgpt2api:chat-room-read", onRead);
|
| 176 |
};
|
| 177 |
}, [enabled, active]);
|