头像链接落盘 + 并发获取 + 单块上限按命令行实测值收紧
一、头像链接的反查结果落盘(avatar_cache/uid_urls.jsonl,7 天有效期) 老录播的 jsonl 里没有头像链接,合并时要按 uid 向 AcFun 反查;但结果原来只存在内存里, 进程一重启就得把整场 uid 重新查一遍。实测一个 uid 约 1 秒,几百人的场次要几分钟, 日志还跟着打「正在获取头像:已处理 x/y 个用户」,看起来就像在重新下载头像 (实测图片缓存本身是好的:同进程第二轮、重启进程后第三轮都是「新增 0、重写 0」)。 现在查到就落盘(追加写,加载时顺手压掉重复/过期行),重启后直接读记录不再查。 有效期 7 天是为了主播换头像后能自己更新过来。 二、头像获取改成 6 路并发 查链接和下图都要走网络,串行做几百人要好几分钟。每个任务开头仍过闸门, 暂停/停止的响应不受影响(任务走带缓冲的 channel,worker 提前退出也不会卡住投递端)。 三、日志分清楚「复用」和「真下载」 原来的 looked++ 只看「这一条没有链接」,内存记忆命中时也会打印「已按 uid 向 AcFun 反查」, 误导成在重下。现在汇总成一句:链接复用 X / 向 AcFun 查 Y,图片复用缓存 Z / 新下载 W。 四、单块条数上限从 200 收紧到 120(线上事故) 线上一次 4 万条弹幕的合并在第 73 块失败: "fork/exec ...: The filename or extension is too long"(就是 200 条上限那一版)。 真机实测:一块里每条弹幕来自不同用户时,每条占「-i 头像路径 + scale 行 + overlay 行」 约 241 字符 —— 120 条 28864 字符、200 条 48288 字符,而 Windows 的 CreateProcess 限 32767。 现在上限 120(给中文路径和其它参数留 3KB 余量),并加了一道自检:真超了就报 「ffmpeg 命令过长(N 字符):这一块有 X 条弹幕、Y 个头像」,而不是烂在 exec 里。 顺带试过 -filter_complex_script(滤镜写文件、命令行只留文件名), 实测这个 ffmpeg 构建没有该选项(Unrecognized option),所以走收紧条数这条路。 验证: - gofmt / go vet / go build / GOOS=linux go build 通过;go test 全绿(11.0s); go test -race 全绿(15.7s);staticcheck -checks=all 零告警 - 新增 avatar_cache_test.go(11 个)与 command_len_test.go(3 个)用例;反向验证扩到 17 项全过 - 隔离实例:e2e.py 36 项、smoke_extra.py 33 项全过 - 新增 smoke_manyuid.py(300 条密集弹幕 / 300 个不同用户 / 全带头像,就是线上事故的复现场景): 合并成功、分 3 块(120/120/60 条)、日志无「命令过长」; 第二次合并同一场「图片复用缓存 300 / 新下载 0」
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 按最坏情况(每条弹幕一个不同用户、每个都带头像)渲染一块,返回这一块的命令行长度。
|
||||
// 长度从 renderChunk 自己的自检错误里读出来;没触发自检就返回 (0, false)。
|
||||
//
|
||||
// 线上的那次事故就是这里没守住:单块 200 条时实测 48288 字符,远超 Windows 的 32767,
|
||||
// 而 exec 只回一句 "The filename or extension is too long",看不出跟弹幕数量有关。
|
||||
func chunkCommandLenProbe(t *testing.T, n int) (length int, over bool) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
avatars := make(map[int64]string, n)
|
||||
records := make([]danmuRecord, 0, n)
|
||||
for i := 0; i < n; i++ {
|
||||
uid := int64(1000000 + i)
|
||||
// 头像路径按真实缓存目录的形态造(uid_12位摘要.png),故意做得和线上一样长
|
||||
avatars[uid] = filepath.Join(dir, avatarCacheDir,
|
||||
fmt.Sprintf("%d_%012x.png", uid, i*0x9e3779b1))
|
||||
records = append(records, danmuRecord{
|
||||
Time: float64(i) * 0.05, UID: uid, Name: "观众甲",
|
||||
Content: "这条弹幕稍微长一点测试宽度", Lane: i % 20, Width: 480,
|
||||
})
|
||||
}
|
||||
ctx := &burnContext{
|
||||
ffmpegFile: filepath.Join(dir, "没有这个-ffmpeg.exe"), // 自检在 exec 之前,跑不到它
|
||||
outFile: filepath.Join(dir, "out.mp4"),
|
||||
meta: danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 42},
|
||||
avatars: avatars,
|
||||
dir: dir,
|
||||
}
|
||||
err := ctx.renderChunk(danmuChunk{start: 0, end: 60, records: records}, "chunk.ass", ctx.outFile, false, nil)
|
||||
if err == nil {
|
||||
t.Fatal("用不存在的 ffmpeg 路径不该成功")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "命令过长") {
|
||||
return 0, false
|
||||
}
|
||||
if _, scanErr := fmt.Sscanf(err.Error(), "ffmpeg 命令过长(%d 字符", &length); scanErr != nil {
|
||||
t.Fatalf("没能从错误里读出长度:%v / %s", scanErr, err)
|
||||
}
|
||||
return length, true
|
||||
}
|
||||
|
||||
// 按硬上限切出来的块,在最坏情况下也不能顶破命令行上限
|
||||
func TestChunkCommandLengthUnderLimit(t *testing.T) {
|
||||
if length, over := chunkCommandLenProbe(t, maxDanmuPerChunkHard); over {
|
||||
t.Fatalf("按硬上限 %d 条渲染,命令长达 %d 字符,已经超过安全值 %d",
|
||||
maxDanmuPerChunkHard, length, maxCommandLineLen)
|
||||
}
|
||||
}
|
||||
|
||||
// 块大到一定程度必须被自检拦住,并报出「多少条弹幕、多少个头像」这种能定位的信息
|
||||
func TestRenderChunkReportsTooLongCommand(t *testing.T) {
|
||||
length, over := chunkCommandLenProbe(t, 300)
|
||||
if !over {
|
||||
t.Fatal("300 条的最坏情况应当被自检拦住,否则交给 exec 只会报一句看不出原因的错")
|
||||
}
|
||||
if length <= maxCommandLineLen {
|
||||
t.Fatalf("自检报出的长度 %d 应当大于阈值 %d", length, maxCommandLineLen)
|
||||
}
|
||||
}
|
||||
|
||||
// 硬上限本身要留出足够的余量:按实测每条约 241 字符,上限条数不能逼近 32767
|
||||
func TestChunkHardLimitLeavesHeadroom(t *testing.T) {
|
||||
const perRecordWorstCase = 241 // 2026-09-26 真机实测(每条一个不同用户)
|
||||
if est := maxDanmuPerChunkHard*perRecordWorstCase + 200; est > maxCommandLineLen {
|
||||
t.Fatalf("硬上限 %d 条按最坏情况估算要 %d 字符,超过了安全值 %d",
|
||||
maxDanmuPerChunkHard, est, maxCommandLineLen)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user