MoreRSS

site iconChen Junda修改

北京大学计算机应用技术硕士,上海微软工作。爱好包括计算机、游戏、羽毛球、纯音乐、电影和音乐剧。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Chen Junda的 RSS 预览

fork subgen实现纯本地AI视频字幕生成和翻译

2026-03-14 11:53:00

最近我想看一场时长 5 个多小时的日语演唱会录像,但这份录像没有可用字幕,我又不懂日语,没有字幕MC部分根本听不懂。

于是我想到可以用本地语音转录生成字幕。调研后我发现了 McCloudS/subgen 这个项目,发现它已经把“本地自动转录”这件事做得很完整,并且可以用jellyfin集成实现视频加入和播放时自动生成字幕。

在实际体验中,我进一步希望它能覆盖“转录后翻译”的需求,于是决定在原项目基础上做一层轻量扩展,把翻译功能补齐到同一条工作流中。

因此,我在原仓库上创建了一个fork https://github.com/ddadaal/subgen-translate ,实现了:

  • 在不破坏原有 webhook / Bazarr 使用习惯的前提下,增加“转录后翻译”能力
  • 添加使用CLI转录和翻译的功能,可以直接在命令处理文件,无需从媒体服务器走
    • 转录: uv run launcher.py -f "D:\Movies\movie.mp4" -t transcribe
    • 翻译: uv run launcher.py --srt "D:\Movies\movie.subgen.medium.jpn.srt" --srt-to zh
  • 添加一些工程管理的最佳实践,例如使用uv管理环境、subgen.env.local来编写本地配置等

个人使用场景

我的实际环境是一个典型的家庭局域网多机协作场景:

  • 一台较老的 Windows 笔记本作为 NAS 主机,部署了 Jellyfin,并通过外接硬盘盒存放媒体文件。
  • 局域网里其他机器没有可用 GPU,CPU 做转录与翻译(尤其翻译)速度过慢。
  • 因此需要另一台带 GPU 的机器专门承担转录/翻译计算。

在 Subgen 与 Jellyfin 集成时,有一个关键前提:Subgen 看到的媒体文件路径,必须与 Jellyfin 看到的路径完全一致。为了实现这一点,我

  1. 在 GPU 机器上把 NAS 外接硬盘映射成与 NAS 机器相同的盘符路径。
  2. 配置 Jellyfin 与 Subgen 的互通(网络可达、Webhook 与服务地址正确)。
  3. 让 Jellyfin 的“新增媒体/播放媒体”事件自动触发 GPU 机器上的 Subgen。

这样,Jellyfin 仍然负责媒体管理与播放触发,GPU 机器负责高耗时的转录与翻译,实现了“存储在 NAS、计算在 GPU 机器”的分工。

部署拓扑图

flowchart LR
	subgraph LAN[家庭局域网]
		subgraph NASHost[老 Windows 笔记本(NAS)]
			Jellyfin[Jellyfin 服务]
			Disk[外接硬盘盒 / 媒体库]
		end
 
		subgraph GPUHost[GPU 机器]
			Subgen[Subgen 服务]
			Model[Whisper + TranslateGemma]
		end
 
		Client[局域网播放器/客户端]
	end
 
	Disk -->|媒体文件路径| Jellyfin
	Disk -.同盘符映射.-> Subgen
	Client -->|播放/新增媒体| Jellyfin
	Jellyfin -->|Webhook 事件| Subgen
	Subgen -->|转录/翻译| Model
	Model -->|生成字幕(双语/纯译文)| Disk
	Jellyfin -->|读取字幕并展示| Client

使用体验

配置情况:

  • NAS机:i5 1135G7 15W + 16G
  • GPU机:R9 [email protected] + 64G DDR4 3200 + RTX 5070 Ti
  1. 转录和翻译过程都没有充分利用GPU,两个步骤的显卡利用率都只有 40%。转录过程需要 CPU 参与处理音频流,翻译过程应该是每一行都要重新推理影响速度

转录过程中CPU和GPU使用情况

  1. 视频时间越长,视频后半段就越容易出现错误、重复、未识别的情况,需要通过调整各种参数来缓解,至少需要打开vad功能,其他参数可让AI来调整。
# subgen.env.local
SUBGEN_KWARGS={'vad': True}
  1. faster-whisper支持多种模型( https://deepwiki.com/SYSTRAN/faster-whisper#supported-model-variants ),但是不同模型的使用体验有较大区别:
    • medium:在 i5-1135G7 上用CPU大约可以做到 1s/s(每 1 秒处理 1 秒原视频),台式机RTX 5070 Ti 6s/s (每秒处理约6秒原视频),速度可以接受
    • large-v3:模型大小 3G,在台式机 RTX 5070 Ti 上最快可达 9s/s,和medium差不多,因为瓶颈在CPU上
    • large-v3-turbo:模型体积和 medium 差不多,都是约 1.5G;但在我的环境里只能正常处理视频开头,后面基本识别不出文字,估计也和调参有关,而且既然large-v3也这么快了,直接用large-v3就好
    • distil-large-v3:只支持识别英文
  2. faster-whisper只支持CUDA 12,不支持最新的CUDA 13
  3. 翻译过程按最简单的每行推理一次的写法,使用原版 translategemma-4b-it和默认参数在RTX 5070 Ti上推理一次2.3s,速度勉强可以接受。但是考虑到字幕的每一行一般较短,将多个字幕合并后同时推理效率更高,所以提供了批量翻译的功能,每一次推理翻译多行,需要根据README中的描述以及本地硬件的情况调整相关参数
  4. 如果只是偶尔用一次、而且每次翻译的数量不多的话,字幕翻译和合并功能其实有很多在线的免费服务可以用,且速度和质量都非常好(甚至比本地模型效果更好)
    1. 翻译: https://translatesubtitles.co/
    2. 合并: https://subtitletools.com/

把nanobot关进Docker后,如何同时保留浏览器可视化与自动化

2026-03-06 18:50:00

实在不太放心把 nanobot 这类可以直接操作本地电脑的程序直接装在操作系统上,所以我选择把 nanobot 放在容器里运行。但是nanobot很多有意义的工作又需要和宿主机上的环境(例如浏览器)交互,而浏览器上很多网站需要我们先去登录才可以正常使用,这就需要一个既可以由 nanobot操作、也可以由我们自己的操作的浏览器

经过一番查找,终于找一个不影响 nanobot 本身的方法,操作是在部署 nanobot的 docker-compose.yaml 目录下再创建一个 docker-compose.override.yaml,内容如下:

services:
  chromium-vnc-cdp:
    image: linuxserver/chromium:latest
    container_name: chromium-vnc-cdp
    ports:
      - "3000:3000" # Web 界面
    shm_size: "2gb"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Shanghai
      - CHROME_CLI=--remote-debugging-address=127.0.0.1 --remote-debugging-port=9222
 
  chromium-cdp-proxy:
    image: alpine/socat
    container_name: chromium-cdp-proxy
    restart: unless-stopped
    network_mode: "service:chromium-vnc-cdp"
    command: "TCP-LISTEN:19222,fork,bind=0.0.0.0,reuseaddr TCP:localhost:9222"

启动后,给 nanobot 一条明确指令:

之后都使用 chromium-vnc-cdp:19222 这个 CDP 端口操作浏览器。

为什么是两个容器

chromium-vnc-cdp 的职责是提供浏览器本体和 Web 访问界面(3000 端口),这样我们可以直接使用localhost:3000访问这个浏览器。

chromium-cdp-proxy 的职责是把 Chromium 容器里只监听 127.0.0.1:9222 的 CDP 端口,转发成同网络命名空间下可访问的入口。实际上这两个容器在同一个网络中,所以需要换个端口监听,这里选择了19222,其他任何端口都可以。

这里有一个关键限制:根据 pyppeteer 相关讨论中的实践结论,--remote-debugging-address=0.0.0.0 往往需要和 --remote-debugging-port--headless 一起使用;但一旦使用 --headless,就无法达到“实时查看浏览器界面”的目标。

来源:https://github.com/pyppeteer/pyppeteer/pull/379#issuecomment-2072215518

因此我不走“浏览器直接对外暴露 CDP”的路线,而是保留有界面的 Chromium,再通过独立的 socat proxy 容器做端口转发。

这样拆分有三个好处:

  1. 浏览器容器保持默认安全策略,CDP 仍然只在本地监听。
  2. 通过 socat 单独做代理,不需要改 Chromium 镜像或启动脚本。
  3. nanobot 只需要记住一个固定地址(chromium-vnc-cdp:19222),配置简单且稳定。

实际效果

这套配置完成后:

  1. 你可以在 3000 端口看到浏览器 Web 界面。
  2. nanobot 可以通过约定好的 CDP 地址持续复用同一个浏览器环境。
  3. 浏览器自动化和人工观察(VNC/Web)可以并行进行,排障体验更好。

可划分显存 != 统一内存:AI Max+ 395 64G AI推理性能

2026-02-02 20:34:00

前言

之前写过一篇关于HP战99 Ultra(搭载AMD AI Max+ 395)的使用体验,今天聊聊这台笔记本在AI推理场景下的表现。作为这台机器宣传的主要场景,AI推理的实际使用情况却优点一言难尽。

硬件配置回顾

配置 详情
CPU AMD Ryzen AI Max+ 395 16C32T Zen5
内存 64G LPDDR5 8000MT 4通道可划分显存
显卡 Radeon 8060S 40CU RDNA3.5
显存 可在BIOS里将几个固定挡位的内存分配给显存

关键概念:可划分显存 vs 统一内存

在深入分析数据之前,需要理解几个重要概念:

  • 传统显存:传统独立显卡的固定显存,容量固定,如RTX 4090的24G
  • 可划分显存:静态分配机制,将内存的一部分固定划给GPU作为显存使用,如AI Max+ 395
  • 统一内存:内存和显存统一寻址,整个内存空间CPU和GPU都可以访问,无需显式分配(主要见于Apple M系列芯片)

重要区别:AI Max+ 395使用可划分显存架构,需要静态分配部分内存给GPU使用;而统一内存无需显式分配,灵活性更高。

AI推理测试数据

为了方便,以及因为我至今没能在WSL下成功运行rocminfo也就没办法跑vllm等主流推理引擎方案(就离谱),本次测试均在Windows下使用LM Studio运行。

GLM 4.7 Flash

这是个MoE模型,总参数量30B激活3B的规模,主要测试Q4_K_M量化的情况。在这个量化等级下,在模型大小为18.13GB

以下的给出Prompt为:

编写一个科技公司的官网的HTML

另外值得一提的是,LM Studio中对AMD显卡有两种Runtime:Vulkan和ROCm。我本以为两种Runtime不会有什么很大的区别,但是实际测试下来却axm并非如此。

在16K上下文下:

专用显存 512M 32G
显存占用 20.8G 21.3G
Vulkan速度、总数(token/s) 17.26 (6970) 42.89 (6107)
Vulkan 首token (s) 0.8 0.8
ROCm速度、总数(token/s) 15.28 (5262) 14.42 (6351)
ROCm 首token (s) 0.04 0.34

Vulkan在32G专用显存下的速度实在是过于逆天,于是我重新跑了数次,结果均非常接近。后面我们还能拿到如此让人匪夷所思的成绩。

16K的上下文只能说勉强够用。既然还有这么多显存可用,不妨试试更多的长度上下文。根据LM Studio估计,不同长度上下文的显存使用估计值:

  • 16K上下文:18.59G
  • 64K上下文:19.57G
  • 最大支持(198K):22.3G

看起来MoE模型的一大好处就是可以把上下文拉大!于是我选择GLM 4.7 Flash最大支持的长上下文 198K下,虽然LM Studio的估计显存占用也仅有22.3G,但是512M专用显存的无法正常加载:

无法正常加载模型

只有在32G下可以正常使用,在Vulkan下获得了**37.28 token/s(7857 token,首token 0.15s)**的成绩。

同时我还测试了Q6_K的量化模型,在16K上下文、32G专用显存下:

  • 模型大小:24.61GB
  • 预计显存占用:25.12GB
  • 推理性能
    • ROCm:13.79 token/s(6419 token,首token 0.33s)
    • Vulkan:25.51 token/s(5717 token,首token 0.20s)

再次看到了不知道该说是Vulkan逆天还是ROCm的成绩!ROCm作为AMD官方的方案,居然被Vulkan拉开了如此大的差距。

在不开启思考的情况下,10 token/s的速度还是可以应付日常使用的。

Qwen3 VL 32B

稠密模型的情况就不一样了。这一部分我选择了Qwen 3 VL 32B来测试。

这是个支持图像输入的模型,于是我去stackoverflow上截了如下这一张图,

Stack Overflow截图

并给出prompt:

使用html和css重现这个HTML页面

以下为结果:

专用显存 512M 16K 512M 24K 32G 16K
显存占用 25.8G 28.3G 27.5G
Vulkan速度、总数(token/s) 3.74 (4059) 3.51 (3676) 9.41 (6801)
Vulkan 首token (s) 36.67 39.29 18.32
ROCm速度、总数(token/s) 4.15 (3723) 3.10 (3713) 9.42 (4198)
ROCm 首token (s) 24.59 26.56 9.46s

可以看到,在24K上下文已经到32G显存的极限了(28G)。但不管有没有独立显存,这个推理速度用起来已经是比较难受的级别了。

分配48G给显存?

395的另一个宣传点是可以将75%的内存划给显存,在64G的型号上,BIOS中最高可以将48G的内存划给显存。

听起来很美?48G显存甚至可以高量化跑32B模型了!

Qwen 3 VL 32B的Q6_K量化模型大小为28.08G,在32G显存下可以加载,但是推理的时候因为显存不够了,速度比可以完全在显存中的Q4版本慢很多。经过测试,Q5_K_M是最大的32G显存可以充分的量化规格。

而这时候你想到,48G显存岂不是就可以接近这个问题了?

可是事实却是:16G的系统内存不仅使得正常的系统操作会开始缓慢甚至卡顿,甚至模型都无法正常加载!而我已经LM Studio中有三个选项和显存和内存全部调整为不给内存太多压力了:

  • KV缓存卸载到GPU内存中:打开,显存够大!
  • 保持模型在内存中:关闭
  • 尝试mmap():将磁盘上映射到内存中空间中,关闭

Qwen 3 VL 32B的Q6_K模型无法加载

内存可划分为显存 != 共享内存

395的主要的宣传口号,就是内存可以当作显存用。这话当然不假,BIOS里确实可以将内存划分给显存,但是,它和我们预期的共享内存完全是两码事:

  • 被划分给显存部分不可以再作为内存使用
  • 每次切换显存需要重启,不可无缝切换

那,如果我们不划分显存,直接把内存当显存用呢?其实现在的推理框架都支持把内存当显存用,但是以下两个问题让用内存当显存的方案下的推理速度惨不忍睹:

  1. 内存与显存的速度之间有巨大差距
  2. 内存中的数据仅能由CPU计算,而CPU在AI计算场景下速度非常缓慢,且CPU和显卡的计算数据需频繁相互拷贝

理论上来说,395的内存和显存均为同一款芯片,问题1不存在,但实际上问题2的问题仍然无法避免:即使是在同一块芯片上,显存仍然不能直接用内存部分的部分,内存和显存之间拷贝仍然非常频繁。

以下为使用512M专用显存(上)和32G专用显存(下)使用Vulkan运行GLM 4.7 Flash Q4_K_M时的任务管理器的图片,可以看出,512M的专用显存下GPU利用率只有70%左右,而32G下可以到达90%以上。而右上角的Copy也可以看出512M专用显存下显存一直在进行复制的操作。

512M显存跑GLM 4.7 Flash Q4 16K

32G显存跑GLM 4.7 Flash Q4 16K

同一现象也出现在512M专用显存下运行Qwen 3 VL 32B Q4_K_M的情况,GPU利用率更是只有50%,而Copy图中也能一直看到复制的过程,而整个过程中CPU也在(艰难地)参与运算。而CPU参与计算在笔记本场景下有抢功耗的问题,更影响了GPU的性能发挥。

512M显存跑Qwen 3 32B Q4KM,16K

更进一步地,如果把上下文拉到24K,进一步加大显存的需求量,在512M专用显存下情况更加恶化了:GPU有接近一半的时间都闲着。要知道,这个时候显存需求甚至才26G!

512M显存跑Qwen 3 32B Q4KM,24K

总结

我用两个字总结395的优点:能用

  • 大显存确实可以跑一些正常显卡无法跑的模型,虽然慢,但是能跑比不能跑好!
  • 成本相对较低(相比高端显卡)(也只是相对了)
  • 4060移动端的绝对算力,不算高,但是愿意等等的话,它能跑的模型还是能给出结果的

可是这台笔记本形态、64G的总内存的设备却有点尴尬:

  • 为了兼顾日常使用,实际上最多只能32G给显存
  • 80W的最高功耗,无法充分发挥CPU的性能
  • 手动划分显存操作失去了灵活性

所以395确实非常适合小主机场景:

  • 这类主机在分配96G显存的情况下还有32G可以用于日常场景,比64G=48G+16G实用太多
  • 这类主机的性能释放普遍超过100W,也有更完善的散热方案,可以更完美地发挥CPU和GPU的性能
  • 和395刚出来的时候AMD更羸弱的AI生态相比,至少现在主流的推理场景(LM Studio,Ollama、ComfyUI)都已经可以用了(至少我在搜索了包括AMD官网的无数地方后,终于还是找到了AMD官方支持的pytorch

甚至小主机的价格也比笔记本形态的设备(64G 19999)便宜太多(128G普遍15000,希望还没开始涨)。在这个内存价格疯涨的年代,能以这个价格有一台可以跑大模型的机器已经很不容易了。

2025年总结

2025-12-31 15:30:00

主动选择改变

对我来说,2025年的前半年和后半年是完全不同的。

主动告别了一个熟悉的工作,做出一个必定会做出的选择,期望能回到一个熟悉的工作状态,在一个全新的起点重新开始,却开始不停接受充满未知、充满了混乱的挑战。

离职,一个一定会做出的选择

不知道什么时候,我开始认为离开微软是一个艰难、遗憾、但是又一定会发生的事情。

一方面,在公司两年来,虽然绩效都是拉满,但是所做的、所参与的所有项目都胎死腹中,而新的被分配的AI有关的项目怎么看都很不靠谱,很难推动;年初,校招进微软,待了十余年的直接manager从公司离职;身边同事的升职空间和奖金肉眼可见的越来越小;而每个季度都能传出裁员的消息,和身边同事讨论的都是裁员、relocate、大礼包。

另一方面,可能是所有初进社会人的共同点,总是对现状不满,总是还有着自己的想法,想着换一个环境可能会更能实现自己的理想。

想着早晚会做,不如现在就做。于是我在我第一份工作的第23个月,在2025年正好过了一半的日子,我终于决定主动踏出这一步。

离职

可能最黑色幽默的是,我那在之前从不停歇的裁员潮中稳如磐石的组,在我离开三个月后,被全部裁员。也就是说,这一次,我是否主动选择,对结果并没有什么变化,反而主动让我与20多W的赔偿金失之交臂。

“回到”“原”工作

新的工作,其实也没有那么新:“不新”体现在我回到了研究生期间的、由我从第一行开始从零开始的项目,并且一直以兼职的身份在参与。而“新”则是工作内容的新。从兼职到全职,从一个“局外人”到一个“局内人”,看似相同的工作,看似可以回到研究生期间以及兼职期间的更积极主动的工作状态,但其实是进入一个全新的、未知的、不停地尝试和否定的循环。

解决技术问题?

俗话说,所有软件项目到后面都会变成屎山。更别提一个一开始就是没有好好设计,作为一个玩物开始的项目了。

第一个commit

四年可以发生很多事情:项目从开源到闭源,连带着很多设计都需要跟着改变;所使用的框架和技术从无到有,群雄争霸到逐渐稳定;项目功能逐渐增多,需求越来越复杂,发展目标越来越不清晰……在这么大的变化下,事情总是会朝着阻力最小的方向发展。而ToG项目的本质,就决定了大部分工作都是纯业务的,甚至于还会专门花精力做一些不可复制的、临时性的工作。看起来这些工作很没有价值?可是以业务的眼光看,这些工作才是有价值的。

  • 项目中大量使用全局变量维持全局状态?无所谓,你的客户不会部署多个实例。
  • 项目中存在大量重复代码,风格样式不统一?无所谓,不影响功能,重构反而影响交付节奏
  • 明知项目中隐藏着大量的暗雷,但没有精力、时间和能力建立完善的测试方案,甚至都不知道哪里有问题?似乎也无所谓,反正目前客户没有遇到,遇到了

尝试使用效率更高的风格检查工具,却完全不敢动

说到底,只有需求才能定义什么是该做的,什么是不该做的。客户关心的才值得投入精力和人力,而客户不关心的,投入一分一毫的资源都有可能是对时间、精力的挥霍。

解决流程问题?

既然项目的技术本身没什么可做的,于是我将目光投入了一些其他让我不舒服的点。从流程完善到繁琐的大公司到小公司,当然有极大的不适应。其中,信息分散是让我最头疼的:

  • 30个人分了四个飞书组织、三个钉钉组织
  • 文档信息分散到腾讯文档、金山文档、飞书文档中
  • 工作信息和私人信息混杂,工作信息又在飞书又在微信,群聊也有飞书和微信群,想找信息,根本不知道在哪个地方能找
    • 我平时有两台电脑混合着用,没有一台电脑有完整的微信信息(这里再次亲切问候张小龙)
  • 会议有时候在飞书,有时候在腾讯文档
  • 在实际上的多地base的情况下,没有统一的日历管理,甚至不知道你的同事是否已经请假

断开的微信消息记录

于是在来了公司之后的两个月中,我尝试整理流程和推动文档化办公,例如

  • 设立需求管理和评审流程、开发和评审流程、测试流程、发布流程、部署流程
  • 要求所有可能会重用的信息都必须落实到一个文档中

这些措施有的顺利落地,有的难产;有的受到欢迎,但大部分推动起来困难重重:

  • 你的同事认为飞书响应速度很慢,功能不好用,不愿意使用
  • 你的同事并不认为一个问题的解决方案值得被写入文档
  • 你的同事每天都有无数的事情,什么样的流程和方式才是真正可以被广泛接受并正常使用的?

这些问题说到底,和上一段一样,哪些真的是问题?

如果所有成员都已经习惯了工作生活都用微信,微信里聊工作是最方便的方案,即使微信每发一封文件都要复制一份,发到最后自己都不知道哪份是最后的方案;所有人本来就坐在一起,已经习惯了有问题就现场聊天,让留文档反而是负担,即使第二天就忘了第一天聊了什么。

如果想解决的问题本来就不被认为是问题,那解决方案自然也毫无意义。

应该解决什么问题?

如果让你指出你所在公司存在的不合理的地方,你能提出多少条?我相信大多数人都能提出很多,并会对公司对这些问题熟视无睹充满了不满以及无奈。

但是当我屁股反转,真正做到“老板”的位置上后才发现,不是所有问题都可以被解决的。很多你认为存在的问题,实际上并不是问题;很多你根本没有意识到的问题,反而已经在暗处默默地影响工作效率、氛围和情绪;很多你认为你解决了问题,实际上反而让情况更糟。比解决问题更重要的是发现问题,评估问题,以及在采取措施后观察效果。而这些工作将会没有标准流程,没有标准方案,甚至于没有反馈,只能通过从各个渠道收集大量信息和反馈,分析信息,小步快跑地去做出对应的调整。

这半年来,我自认为发现了无数的问题,也尝试了一些措施去“解决”无数的问题。但是回头看来,有什么问题是被解决了,我到底提供了什么价值,又给各个同事添加了多少麻烦?我没有答案,也不知道如何回答。

双城生活

这半年不得不体验了北京和长沙的双城生活,每两周在北京和长沙之间切换一次base地。

航旅纵横

这种生活的前期是新鲜的。北京虽然租房贵,但是多亏有朋友的帮衬,能够免费在西城住上租金过万的、房龄10年左右的电梯房,体验一下全国最核心的城区的生活体验(事实证明北京市中心真不适合年轻人);长沙租房便宜,即使是工作地旁边的公寓也只有1000多,可以体验到通勤走路10分钟的生活。而两地之间的飞机通勤还让我第一次获得了航空公司的常旅客卡,加上信用卡的福利,基本可以实现休息室自由。

挑战赛获取金卡

但是这种新鲜感只是短期的,短暂的体验之后,迎来的是缺乏归属感。

大家都说租房是一种临时生活,我个人持部分肯定的态度。我目前没有对大件的需求较少,即使有,租房并不会阻止我采购升降桌、人体工学椅这类的大件,大不了,叫个搬家公司就搬走了。可是两地通勤让我彻彻底底体验到了这个感受。

由于有两个居住地,两地的生活设施都不完整。台式机在北京,于是在长沙时只能用工作笔记本应付平时的休闲,没有显示器,小小的笔记本屏幕也完全无法获得一个较好的娱乐休闲的体验;衣服也分布在两地,在11月初两地各自入冬后,由于厚被子还在北京,在长沙10多度的时候仅有薄被子,凌晨3点被冻醒不得不开启空调才能继续入睡;两周时间说长不长说短不短,每次切换工作地的时候都要考虑各类要带走的衣物和生活,然后将宝贵的周末的至少6个小时浪费在路途中。什么都是临时的,什么都是够用就好,不常用的东西就不买。在长沙公寓的桌子和椅子都是海鲜市场的二手货;之前每周做2-3顿饭,现在甚至连厨具都没有采购;甚至于当被要求提供常住地时,都要考虑下写哪个位置。

这种临时的感觉也让我没有任何爱好和社交的欲望。这几年几部乐队的动画让我想重拾小时候的电子琴爱好,但是由于居住环境的不稳定性,不敢买任何大件,而开放琴行一般都是钢琴,和电子琴在对手的能力的要求、可以演奏的音乐的类型有比较大的区别(换句话说就是我菜,弹不了钢琴),并且无论是在北京还是长沙,琴行离居住地都有很远的距离。社交层面,在北京的时候,现在还可以找到之前的朋友;而在长沙的休息日,每天睡眠最多7小时的我,可以在装有万恶之源平板架的床上躺14个小时,在不躺的那10个小时中无比后悔又浪费了一天。

世界上有不少人过着或者已经习惯了这种不定的生活,但是经过半年的尝试,我还是不能说我已经习惯这样的生活方式。

混乱的一年

这是一个混乱的一年。公司变了,工作内容变了,生活地点变了,生活状态变了。

是变好还是变坏了?在这一次的变化是主动选择的,是我一定会做出的选择,之后呢?了解了现状,做出这么尝试,而之后应该获取什么样的信息,做出什么样的改变?

这一年我还没休过一天假期。很幸运能在这个一年的最后一周,稍微告别一下这充满挑战的工作,和老朋友去之前从未去过的东北体验寒风和冰雪,时隔一年再次体验滑雪,然后被滑雪劝退。

长春冰雪大世界

在旅途中和朋友聊到五年后的职业发展情况,然而这毕业两年半的经历,让我不敢再奢谈这么久远的未来。

14寸16核32线程:搭载AMD AI Max+ 395的HP战99 Ultra使用感受

2025-07-26 09:14:00

为什么选这一台?

最近有一次换笔记本的机会,因为之后需要频繁旅行,于是选择了14寸的笔记本作为主力机;考虑到平时开发经常需要占用大量内存,32G的内存不够用,需要64G。而现在轻薄本里提供64G选项的,主流的选择只有可以自己加内存的ThinkPad T14p,但是T14p的CPU和GPU性能都在轻薄本中中规中矩,且被11代酷睿折磨了5年的我,现在已经变成了十足的I黑😅。而AMD只有Max+系列的设备有搭载64G及以上内存的产品,且当前只有华硕幻X和战99Ultra(在国外称ZBook Ultra G1a)这两款。前者平板形态虽然非常帅,但是在非桌面场景下使用体验较差,所以最终选择了后者。主要配置如下:

配置 配置
CPU AMD Ryzen AI Max+ 395 16C32T Zen5
内存 64G LPDDR5 8000MT 4通道
硬盘 SK Hynix PC801 2TB PCIe 4
显卡 Radeon 8060S 40CU RDNA3.5
显示器 2880x1800 144Hz OLED带触屏
重量 ~1.6kg
电池 74Wh

16核32线程的Zen5加40CU的RDNA 3.5居然能装在14寸的笔记本上,在这颗处理器真正面世之前简直是不可想象的。虽然用脚趾头想都知道这么小尺寸的笔记本不可能可以发挥这颗处理器的全部性能,但是,规模大了,提高多少功耗就能提高多少性能,而实测下来这台笔记本的功耗在14寸的笔记本中也不低,所以在所有同样的尺寸和功耗等级的笔记本中,这颗CPU的性能应该也是最强的。而四通道内存甚至在MSDT平台上花多少钱都体验不到。

由于价格原因,大多数人应该没有机会能用到这样的笔记本,网上也少有这款设备真实的使用体验的评测。到现在也算用了这个处理器的笔记本3周了,可以说说体验。

设备

性能和功耗

买这台笔记本就是为了性能。这台笔记本的性能确实很强。

插电的状态下CPU-Z可以跑760/12300左右。这段时间内CPU功耗最高80W,虽然不能一直持续,但是长时间跑仍然可以稳定70W。14寸笔记本能稳定70W的据我所知还是比较少的,再加上这个CPU恐怖的规模,基本可以确定这颗CPU的性能应该就是这个尺寸笔记本的顶尖了。

插电性能

离电状态下CPU-Z的结果是326/6892,正常功耗在35W,长时间功耗在25W左右,这个功耗在离电状态下也比较正常,并且可以看出,CPU在低功耗下倾向于重点使用一个CCD,这也是控制功耗的一个做法。

离电性能

两个CCD的使用率分布

打游戏的话,最近只玩了三角洲,大战场下2880*1800分辨率超高画质有100帧左右。

内存性能测试结果这里也放一下。考虑到是LPDDR5内存,延迟较高,但是读写性能确实体现出了4通道内存的水平。

内存性能

续航

影响笔记本续航的因素有很多,并且随着驱动的更新,结果也会变化,这里只说一下我这段时间的使用情况。在正常的主要使用微信和浏览器的办公场景下,CPU基本上在6-15W范围波动,整机功耗12-20W。由于电池是74Wh,所以整机续航在5小时左右。

电池供电下的典型功耗

这个续航表现只能说中规中矩。主要原因我认为还是空载功耗仍然太高。上面这个图里,虽然核心只占了1.41W,但是整个CPU居然消耗了7.75W,和Lunar Lake以及苹果M系列处理器的1W以下的外围功耗比起来,这个功耗实在是过于离谱了。

充电

万万没想到,这台电脑的充电情况比我遇到过的所有设备都要复杂。

PD 100W以下不可用

首先,这台电脑的充电器是C口的,没有问题。但是,这台电脑并不支持最常见的PD 65W充电! 插入PD 65W充电器后直接不充电,而不是像一些联想笔记本一样慢速充电。

插入65W充电器后系统提示

研究后发现,这台设备的包装盒上明确写着仅支持100W以上的PD充电器。

包装盒上充电功率的标识

但是具体测试下来,设备支持联想T34WD-40(京东)上的96W Type-C供电,但是系统会提示低功耗充电器,且CPU功耗被限制在了45W。在这个功耗限制下,CPU-Z全核最高频率2.8Ghz,跑分9500左右,比未限制状态下的12300低了22%,还是下降得比较严重的。并且,现在市面上,除了DELL有两款支持PD 140W给笔记本供电的显示器外,其他显示器都最高只支持90W。所以一线通,这个我本来已经习以为常的办公解决方案,现在也不得不放弃了,在工作时必须再插一个充电器,不过也还好,只是多一根充电器线的事。

96W供电的提示

高功率PD充电器兼容性问题

设备自带的充电器是支持PD 140W的大板砖,除了重,没什么问题。

自带充电器

从上可以看到,其支持28V 5A也就是PD标准的140W的充电器。了解了一下,目前市面上真正在使用PD标准进行高功率充电器笔记本,似乎也就只有MBP一家了。没想到苹果的充电在手机上扭扭捏捏,好不容易搞个C口还和很多iPhone 15出现之前的老C口设备存在兼容问题,反而在笔记本上如何激进地拥抱标准。

可能是覆盖的产品太少,支持这个标准的充电器并不多,并且同样是声称支持MBP和这个标准的充电器,在这台电脑上仍然存在兼容性问题。

酷态科的15号充电器是140W充电器里价格最便宜的(179不带线),支持小米快充,也声称支持MBP,但是实际使用中并不兼容这台电脑,插入电脑后,系统充电图标显示一下,HP的软件弹出如下的提示并且系统稍微卡一下,然后马上断开,以此循环。为了验证是不是线的问题,我还快速购入了酷态科的支持240W的6A线,但是问题依旧。

酷态科

插入充电器后的提示

没有办法,只能换了Anker的充电器。虽然Anker的产品相比起来都更贵,但是考虑到Anker牌子和销量更大,兼容性应该更好,于是花更高的价格(279)购入了Anker的支持140W的充电器。而这个充电器配上自带的电源线一切正常,没有任何问题,但是要是配上之前提到的酷态科的充电线也会有时候会弹出上面的提示并且系统会卡一下(因为电池供电会影响处理器和其他硬件的设置)。

Anker

这个充电器带一个屏幕,可以实时显示系统使用的功耗。在CPU-Z跑分、CPU功耗在70W左右时候,整机功耗为120W+,100W的PD确实不能发挥整台机器的所有能力。

Anker

内存和显存共享

搭载这块CPU的设备的宣传材料都是本地跑AI大模型。内存和显存共享芯片确实是一个大卖点。

宣传语

具体来说,这个电脑支持在BIOS里将固定量的内存分配给显存。这样分配给显存的内存就不可以再作为系统的内存使用了。这个功能似乎很多AMD核显本都有。

在BIOS里分配内存给显存

如上图所示,我的64G版本,可以手动选择将512MB、4GB、8GB、16GB、32GB或48GB的内存分配给显存。如果分配8G,于是日常就只有55G的内存可用了。

分配显存后的任务管理器

但是,这个分配量只是GPU独享的,剩下的内存是真正的共享内存,GPU同样可以支配。根据之前看到的一份材料,GPU可以动态支配剩下的其中一半的容量。也就是说,如果给GPU独享分配8G,那么GPU实际能用的是8 + (64-8)/2 = 35.5 GB。其实对于大多数使用情况来说也完全够用了。

考虑到AMD羸弱的AI生态,我买这台电脑的唯一考虑其实是变态的CPU性能和大内存,所以我没有必要给GPU分配独享的存储,于是最后就选择了512MB给独显独享。这样配置下来,GPU实际上最大也可以用32GB左右的显存,打打游戏也够用了。

模具、外围配置等

这台设备的模具及外围配置同样一般。一句话总结:CPU以外就是个6000块左右设备的水平。

厚度和重量:最厚处18.5mm,重量1.6kg左右,和MBP一个水平,和轻薄不沾边(考虑到我之前的笔记本是980g的Yoga Carbon,这落差就更大了)

屏幕:一块2880x1800 144Hz的OLED屏幕,但是最高亮度只有400nit,且是镜面屏、触控层存在网格纹,甚至似乎还是PWM调光。

触控板:大小、滑动和按键手感还可以,但是并非压感触控

键盘:中规中矩的笔记本键盘,和ThinkPad等不可比。

另外还有一些小问题,例如

  • 有的时候突然无法调整屏幕亮度,只能重启才能解决 亮度
  • 无法连接到华为路由X1 Pro的Wi-Fi 7热点,只能连接到Wi-Fi 5的兼容模式

总结

综上,虽然这台笔记本性能很强,但是我还是不推荐这台笔记本给绝大多数人。

  • 对于需要64G内存的,ThinkPad T14p除了CPU之外,完全满足需求
  • 对于需要打游戏的,20CU的Radeon 8060S在2K级别分辨率下可用,但是A卡仍然存在一定兼容性问题,且没有DLSS等老黄黑科技加持,游戏体验仍然不如NVIDIA独显的。同样的价格已经可以买幻14这类搭载NVIDIA独显的游戏本了
  • 相当于笔记本,AMD或者OEM似乎更愿意把395这块CPU放在一体机上,我非常同意。395非常适合作为一个AI开发者或者小型团队的移动AI推理工作站(考虑到AMD的软件生态,可能也就只有推理容器做了),而不是放在一个笔记本上作为一个正常的CPU。目前市面上搭载395+128G内存的一体机价格在13000-15000,且性能释放在100W以上,完全没有必要花更高的价格买一台笔记本(战99Ultra 395+128G的配置国补后仍然需要24000)

这种小众机器看着非常诱人,但是价格昂贵,和外围设备兼容性存疑,并且遇到问题(例如充电器)都无法在网上找到解决方案,厂商的后续支持(例如驱动更新等)也一定会比较有限。只有像我这样什么都要的且愿意折腾的,才可以建议试试这台机器😊。

这台机器在史无前例地在14寸的机器下提供了16C32T CPU+64G/128G四通道内存的配置。虽然表现很亮眼,但是最终市场上设备种类少,价格贵,销量也低,后续AMD还会不会出下一代呢?

Node是并发性能的绊脚石吗?测试Express服务器的基准并发能力

2025-03-08 08:47:00

问题发现

最近在一个项目中,我遇到了一个使用Node.js编写的请求转发服务的性能瓶颈问题。这个服务的主要工作看似非常简单:获取用户的请求,将请求体(body)转发到后端的服务器,然后将服务器的响应原样返回给客户端。

然而,在进行压力测试时,我们发现当并发请求达到约2000时,系统表现出了明显的性能问题:

  1. 大约6%的请求出现错误
  2. 服务器资源利用率极不平衡 - 只有一个CPU核心达到了100%利用率,而其他核心几乎处于闲置状态

大家都说Node.js的IO性能并不算差。这个现象引发了我的思考: 是什么限制了Node.js在这种场景下的性能表现? 一个看似简单的请求转发工作,为何无法充分利用多核资源?

带着这些疑问,我决定对Node.js代理服务的基准性能进行一次深入研究。我想了解在没有任何特殊优化的情况下,一个标准的Express服务究竟能够处理多少并发请求。这将帮助我确定问题是否出在Node.js本身的并发处理能力上,并在之后遇到类似性能问题或技术选项的时候,对Node本身所能达到的极限能力有个心理预期。

实验设计

为了进行这项研究,我采取了以下步骤:

  1. 编写两个简单的项目:

    • 一个模拟AI后端服务的Express服务器
    • 一个标准的Express代理服务,负责转发请求到模拟的AI后端
  2. 使用wrk作为性能测试工具,这是一个常用的HTTP基准测试工具,能够产生大量并发连接来测试服务器性能

  3. 在相同硬件条件下,测试不同并发级别下的性能表现,包括:

    • 请求成功率、超时率
    • 响应时间
    • 各个核心的CPU利用率

实验实现

模拟后端服务

这个项目是一个简单的Express服务器,它接收POST请求,并返回一个模拟的AI响应。

为了模拟真实响应,这个服务器返回结果前可能会延迟一段时间。我同样会测试延迟不同的时间会对代理服务的性能表现的影响。

代理服务

代理服务同样使用Express实现,它的核心功能是:

  1. 接收来自客户端的请求
  2. 提取请求中的payload
  3. 将payload转发到后端AI服务
  4. 等待后端响应
  5. 将后端响应传回给客户端

这个服务保持了最小化的实现,没有添加额外的错误处理、负载均衡或缓存等优化措施,以便我能够测试Node.js的基准性能。

测试结果与分析

测试运行于WSL2,CPU为5900X 12C24T @ 4.5 Ghz,Node版本22.14.0。

后端服务直接返回

使用6个线程和不同的连接数,超时时间设置为5s,使用wrk对两个服务分别进行压力测试。其中,运行在5001端口的是模拟后端服务,5000是代理服务。

Server Connections Requests/sec Avg Latency Max Latency Total Requests Timeouts Timeout % Total Errors Error %
5001 50 9776.19 6.03ms 291.00ms 97839 0 0.00% 0 0.00%
5001 100 9294.12 12.84ms 537.33ms 93006 0 0.00% 0 0.00%
5001 150 9322.45 31.57ms 1.31s 93276 0 0.00% 0 0.00%
5001 200 8688.89 63.29ms 2.14s 86951 0 0.00% 0 0.00%
5001 500 8769.33 163.54ms 4.99s 87741 121 0.14% 121 0.14%
5001 1000 8200.58 98.60ms 4.96s 82284 67 0.08% 67 0.08%
5001 2000 8808.86 102.43ms 4.95s 88480 54 0.06% 54 0.06%
5001 5000 7769.21 248.05ms 314.09ms 78333 25 0.03% 1134 1.45%
5001 10000 7531.77 453.61ms 592.39ms 76076 13 0.02% 6176 8.12%
5001 20000 6601.98 414.19ms 582.31ms 66423 15 0.02% 15917 23.96%
5000 50 2673.49 74.76ms 2.34s 26763 0 0.00% 0 0.00%
5000 100 2884.08 40.05ms 909.96ms 28860 0 0.00% 0 0.00%
5000 150 2729.84 94.20ms 2.16s 27322 0 0.00% 0 0.00%
5000 200 2586.36 176.31ms 3.61s 25887 0 0.00% 0 0.00%
5000 500 2375.25 192.97ms 4.96s 23767 70 0.29% 70 0.29%
5000 1000 2454.10 139.51ms 5.00s 24629 90 0.37% 90 0.37%
5000 2000 2449.30 326.00ms 4.85s 24597 33 0.13% 33 0.13%
5000 5000 1786.15 326.11ms 5.00s 18038 476 2.64% 1768 9.80%
5000 10000 2016.64 78.55ms 3.78s 20313 10 0.05% 6306 31.04%
5000 20000 1398.34 3.40ms 639.14ms 14072 0 0.00% 16275 115.66%

数据比较多,值得关注的结论如下:

对于后端服务:

  • 200-500连接数开始已经出现了5s内无法完成的超时请求
  • 2000-5000连接数开始出现并非超时的错误,说明此时node本身已经无法接受更多请求
  • 连接数打到5000后,错误率开始指数上升

对于代理服务:

  • 几乎所有指标都大幅差于后端服务,在50连接时请求数就已经只有后端服务的1/3
  • 2000连接数开始错误率即开始指数上升
  • 把平均延迟和后台服务的平均延迟作差,可以发现代理本身逻辑执行在1.5-2s附近

CPU使用

在所有实验中,我还记录了CPU各个核心的使用率,下面是测试后端、2000个连接数时其中一秒的CPU使用率,可以看到,只有一个核心(2)很忙,其他核心没有被充分利用。其他所有数据都具有类似的情况。

10:51:38 AM  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
10:51:39 AM  all    5.93    0.00    1.52    0.00    0.00    4.97    0.00    0.00    0.00   87.58
10:51:39 AM    0    1.75    0.00    0.88    0.00    0.00   11.40    0.00    0.00    0.00   85.96
10:51:39 AM    1    0.00    0.00    1.00    0.00    0.00    1.00    0.00    0.00    0.00   98.00
10:51:39 AM    2   84.00    0.00    6.00    0.00    0.00    0.00    0.00    0.00    0.00   10.00
10:51:39 AM    3    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00
10:51:39 AM    4   11.00    0.00    3.00    0.00    0.00    0.00    0.00    0.00    0.00   86.00
10:51:39 AM    5   18.18    0.00    3.03    0.00    0.00    0.00    0.00    0.00    0.00   78.79
10:51:39 AM    6    1.00    0.00    1.00    0.00    0.00    0.00    0.00    0.00    0.00   98.00
10:51:39 AM    7    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00
10:51:39 AM    8    4.00    0.00    1.00    0.00    0.00    0.00    0.00    0.00    0.00   95.00
10:51:39 AM    9    2.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00   98.00
10:51:39 AM   10    2.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00   98.00
10:51:39 AM   11    0.00    0.00    1.98    0.00    0.00    0.00    0.00    0.00    0.00   98.02
10:51:39 AM   12    3.96    0.00    4.95    0.00    0.00    0.00    0.00    0.00    0.00   91.09
10:51:39 AM   13    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00
10:51:39 AM   14    4.50    0.00    3.60    0.00    0.00   10.81    0.00    0.00    0.00   81.08
10:51:39 AM   15    0.00    0.00    0.00    0.00    0.00    2.04    0.00    0.00    0.00   97.96
10:51:39 AM   16    2.65    0.00    0.88    0.00    0.00   15.93    0.00    0.00    0.00   80.53
10:51:39 AM   17    0.92    0.00    1.83    0.00    0.00   12.84    0.00    0.00    0.00   84.40
10:51:39 AM   18    2.44    0.00    4.07    0.00    0.00   19.51    0.00    0.00    0.00   73.98
10:51:39 AM   19    0.00    0.00    2.75    0.00    0.00   13.76    0.00    0.00    0.00   83.49
10:51:39 AM   20    4.63    0.00    0.00    0.00    0.00   11.11    0.00    0.00    0.00   84.26
10:51:39 AM   21    2.73    0.00    0.00    0.00    0.00   11.82    0.00    0.00    0.00   85.45
10:51:39 AM   22    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00
10:51:39 AM   23    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00

较慢的后端服务、打日志的差别

在上述实验中,后端服务收到请求就直接返回。但是实际上后端服务可能需要一定时间处理。打印日志也是Node服务常见的实践。这两个场景到底对资源消耗有多大呢?

为了更直观地对比不同场景下的性能表现,多设计两个场景

  1. 后端等到500ms后才返回结果
  2. 后端、代理端每收到请求和响应就将请求和响应的URL打印出来(console.log

我整理了以下对比表格,重点关注平均延迟(Avg Latency)、超时率(Timeout %)和错误率(Error %)这三个关键指标:

连接数 服务器 直接返回-延迟 延迟-延迟 输出日志-延迟 直接返回-超时率 延迟-超时率 输出日志-超时率 直接返回-错误率 延迟-错误率 输出日志-错误率
50 后端 6.03ms 503.92ms 9.17ms 0.00% 0.00% 0.00% 0.00% 0.00% 0.00%
500 后端 163.54ms 506.11ms 132.43ms 0.14% 0.00% 0.13% 0.14% 0.00% 0.13%
2000 后端 102.43ms 533.43ms 105.53ms 0.06% 0.00% 0.08% 0.06% 0.00% 0.15%
5000 后端 248.05ms 529.92ms 358.23ms 0.03% 0.00% 0.05% 1.45% 7.73% 2.23%
10000 后端 453.61ms 607.15ms 641.06ms 0.02% 0.00% 0.00% 8.12% 9.89% 12.64%
20000 后端 414.19ms 610.10ms 375.80ms 0.02% 0.00% 0.00% 23.96% 30.43% 35.20%
50 代理 74.76ms 519.05ms 54.27ms 0.00% 0.00% 0.00% 0.00% 0.00% 0.00%
500 代理 192.97ms 717.28ms 208.93ms 0.29% 0.00% 0.29% 0.29% 0.00% 0.29%
2000 代理 326.00ms 726.51ms 417.52ms 0.13% 2.03% 0.21% 0.13% 2.03% 0.21%
5000 代理 326.11ms 892.77ms 434.87ms 2.64% 1.35% 2.87% 9.80% 13.92% 10.33%
10000 代理 78.55ms 788.52ms 89.68ms 0.05% 2.90% 0.01% 31.04% 36.76% 36.39%
20000 代理 3.40ms 777.70ms 94.90ms 0.00% 0.00% 0.00% 115.66% 232.21% 107.78%

从这个对比表格中,我们可以得出几个重要观察:

  1. 延迟500ms的影响:当后端服务增加500ms延迟后,整体延迟有增加,连接数越多,平均延迟增加越少,但是增加的延迟仍然会使得错误率增加

  2. 超时情况分析:在大多数连接数下,超时率都相对较低;但在代理服务的中等连接数(2000-5000)场景下,超时率明显上升,特别是在延迟返回的测试中,显示出代理服务在处理较长延迟请求时的瓶颈

  3. 日志输出的影响:与直接返回相比,增加日志输出确实会增加延迟,但影响不是特别显著。在低并发情况下(50-500连接),日志对后端服务的影响较小;但在高并发时(10000+连接),日志输出会明显增加系统负担

  4. 错误率增长点:无论哪种场景,代理服务的错误率普遍高于后端服务,且在5000连接数左右开始出现明显的错误率上升

  5. 极端高并发下的异常:在20000连接的极端情况下,所有配置都表现出较高的错误率,但代理服务的延迟反而下降,这可能是因为大量请求被直接拒绝,导致成功请求的平均延迟降低

Go

为了对比,我又准备了用Go使用标准库net/http编写的相同功能的两个程序,并在后端500ms延迟、不打log的情况下,测出go的成绩如下(未列出的连接数并未发生错误):

Server Connections Requests/sec Avg Latency Max Latency Total Requests Timeouts Timeout % Total Errors Error %
5001 5000 7310.92 501.39ms 516.91ms 73728 0 0.00% 902 1.22%
5001 10000 7060.75 501.72ms 517.20ms 71154 0 0.00% 5900 8.29%
5001 20000 6493.01 501.33ms 512.36ms 65536 0 0.00% 15902 24.26%
5000 5000 5910.59 636.54ms 3.46s 59611 0 0.00% 903 1.51%
5000 10000 5441.54 657.59ms 1.25s 54953 0 0.00% 5900 10.74%
5000 20000 4857.47 663.12ms 2.11s 48912 0 0.00% 15902 32.51%

二者的差距还是在预期的,go从来没有发生过超时,整体延迟、错误率数据也比node好很多。如在5000个连接下,代理程序出现1.51%的错误,node版本出现9.08%的错误,差距在6倍左右。但是需要注意的是,go可以利用全部CPU核心,而node的js线程只能利用一个核心,如果启动多个node并进行负载均衡,最终结果不一定有很大的差别。

结论

通过这些简单数据,我们发现

  • Node本身的单线程模型无法利用所有CPU的能力,即使在纯网络IO任务中也可能成为性能瓶颈
  • 在这次实验中,express在2000连接数是个门槛,在此之上,超时率、错误率会迅速增加
    • 并且,我的测试机器是消费级CPU,而一般服务器CPU并不能达到如此的单核性能,所以在生产环境中的性能表现会更差

要想解决这个问题,在代码中做出一定的优化,例如减少日志打印、简化Node中的逻辑等,也会有一定的效果。如果优化代码的效果不佳,唯一的办法是运行多个node进程,可以考虑的方法主要是启动多个node服务并增加负载均衡,或者使用node cluster让node可以启动多个worker process。

本实验中的代码和结果均在 https://github.com/ddadaal/node-express-concurrency-baseline-test 中可用。

Acknowledgements

整个测试项目、测试脚本甚至本篇文章基本都是直接使用Copilot + Claude 3.7 Sonnet模型生成的,不得不说,让AI生成大框架、自己再来完善细节的做法确实能提高不少效率。这种实验的大多数工作实际上是框架代码,代码本身逻辑简单,但是需要使用大量API、编写繁琐的测试逻辑、数据分析以及做表,手写非常耗费精力,让AI来做这些繁琐的工作实在是再合适不过了。写文章也可以让AI帮忙做,我之前写篇文章至少需要花一整天,这一次居然一个上午就搞定了。

哦,上面这一句话不是AI写的😀