xiaolongmr commited on
Commit
d188720
·
1 Parent(s): d24ebec

降低线上轮询压力:后台标签页暂停轮询、关闭 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 CHANGED
@@ -69,4 +69,8 @@ PY
69
 
70
  EXPOSE 7860
71
 
72
- CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860", "--access-log"]
 
 
 
 
 
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"]
web/src/components/announcements-popover.tsx CHANGED
@@ -166,10 +166,35 @@ export function AnnouncementsPopover({ session }: { session: StoredAuthSession }
166
  }
167
  };
168
  void load();
169
- const timer = window.setInterval(() => void load(), 5 * 60_000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  return () => {
171
  active = false;
172
- window.clearInterval(timer);
 
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
 
web/src/lib/use-auth-guard.ts CHANGED
@@ -15,7 +15,16 @@ type UseAuthGuardResult = {
15
  session: StoredAuthSession | null;
16
  };
17
 
18
- const AUTH_PERMISSION_SYNC_INTERVAL_MS = 5_000;
 
 
 
 
 
 
 
 
 
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(
web/src/lib/use-chat-room-unread.ts CHANGED
@@ -141,11 +141,37 @@ export function useChatRoomUnread(enabled: boolean, active: boolean) {
141
  window.addEventListener("chatgpt2api:chat-room-read", onRead);
142
 
143
  // 停在聊天室页面时无需轮询(页面内部自己会更新已读)。
144
- const timer = active ? undefined : window.setInterval(load, UNREAD_POLL_INTERVAL_MS);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
 
146
  return () => {
147
  disposed = true;
148
- if (timer !== undefined) window.clearInterval(timer);
 
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]);