普通视图

发现新文章,点击刷新页面。
昨天以前ddadaal.me

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

2026年3月14日 11:53

最近我想看一场时长 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 5900X@4.2Ghz + 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年3月6日 18:50

实在不太放心把 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年2月2日 20:34

前言

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

硬件配置回顾

配置详情
CPUAMD 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上下文下:

专用显存512M32G
显存占用20.8G21.3G
Vulkan速度、总数(token/s)17.26 (6970)42.89 (6107)
Vulkan 首token (s)0.80.8
ROCm速度、总数(token/s)15.28 (5262)14.42 (6351)
ROCm 首token (s)0.040.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 16K512M 24K32G 16K
显存占用25.8G28.3G27.5G
Vulkan速度、总数(token/s)3.74 (4059)3.51 (3676)9.41 (6801)
Vulkan 首token (s)36.6739.2918.32
ROCm速度、总数(token/s)4.15 (3723)3.10 (3713)9.42 (4198)
ROCm 首token (s)24.5926.569.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

主动选择改变

对我来说,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个小时中无比后悔又浪费了一天。

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

混乱的一年

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

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

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

长春冰雪大世界

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

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

2025年3月8日 08:47

问题发现

最近在一个项目中,我遇到了一个使用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是代理服务。

ServerConnectionsRequests/secAvg LatencyMax LatencyTotal RequestsTimeoutsTimeout %Total ErrorsError %
5001509776.196.03ms291.00ms9783900.00%00.00%
50011009294.1212.84ms537.33ms9300600.00%00.00%
50011509322.4531.57ms1.31s9327600.00%00.00%
50012008688.8963.29ms2.14s8695100.00%00.00%
50015008769.33163.54ms4.99s877411210.14%1210.14%
500110008200.5898.60ms4.96s82284670.08%670.08%
500120008808.86102.43ms4.95s88480540.06%540.06%
500150007769.21248.05ms314.09ms78333250.03%11341.45%
5001100007531.77453.61ms592.39ms76076130.02%61768.12%
5001200006601.98414.19ms582.31ms66423150.02%1591723.96%
5000502673.4974.76ms2.34s2676300.00%00.00%
50001002884.0840.05ms909.96ms2886000.00%00.00%
50001502729.8494.20ms2.16s2732200.00%00.00%
50002002586.36176.31ms3.61s2588700.00%00.00%
50005002375.25192.97ms4.96s23767700.29%700.29%
500010002454.10139.51ms5.00s24629900.37%900.37%
500020002449.30326.00ms4.85s24597330.13%330.13%
500050001786.15326.11ms5.00s180384762.64%17689.80%
5000100002016.6478.55ms3.78s20313100.05%630631.04%
5000200001398.343.40ms639.14ms1407200.00%16275115.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.03ms503.92ms9.17ms0.00%0.00%0.00%0.00%0.00%0.00%
500后端163.54ms506.11ms132.43ms0.14%0.00%0.13%0.14%0.00%0.13%
2000后端102.43ms533.43ms105.53ms0.06%0.00%0.08%0.06%0.00%0.15%
5000后端248.05ms529.92ms358.23ms0.03%0.00%0.05%1.45%7.73%2.23%
10000后端453.61ms607.15ms641.06ms0.02%0.00%0.00%8.12%9.89%12.64%
20000后端414.19ms610.10ms375.80ms0.02%0.00%0.00%23.96%30.43%35.20%
50代理74.76ms519.05ms54.27ms0.00%0.00%0.00%0.00%0.00%0.00%
500代理192.97ms717.28ms208.93ms0.29%0.00%0.29%0.29%0.00%0.29%
2000代理326.00ms726.51ms417.52ms0.13%2.03%0.21%0.13%2.03%0.21%
5000代理326.11ms892.77ms434.87ms2.64%1.35%2.87%9.80%13.92%10.33%
10000代理78.55ms788.52ms89.68ms0.05%2.90%0.01%31.04%36.76%36.39%
20000代理3.40ms777.70ms94.90ms0.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的成绩如下(未列出的连接数并未发生错误):

ServerConnectionsRequests/secAvg LatencyMax LatencyTotal RequestsTimeoutsTimeout %Total ErrorsError %
500150007310.92501.39ms516.91ms7372800.00%9021.22%
5001100007060.75501.72ms517.20ms7115400.00%59008.29%
5001200006493.01501.33ms512.36ms6553600.00%1590224.26%
500050005910.59636.54ms3.46s5961100.00%9031.51%
5000100005441.54657.59ms1.25s5495300.00%590010.74%
5000200004857.47663.12ms2.11s4891200.00%1590232.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写的😀

用大模型总结文章:效果很好,但是玄学

2025年2月14日 23:54

Azure AI Language Service的结果太差了

去年我给我的文章增加了AI文章总结功能。在介绍此功能的文章,我提到当时这个功能是通过Azure AI Language Service Text Summarization的功能实现的。

当时我已经发现,这个功能在英文文章上效果还行,但是在中文文章上就基本不可用。

比如,上一篇文章2024年总结的总结结果是:

本文讲述了作者在毕业后的第一年,通过深入体验现有生活,旅游,搬家,工作和生活。

这写的是啥?

这是人话吗?且不说用的英文逗号,前两个分句看着还行,后面就变成关键词的叠加,完全没有概括意思。

一年过去了,DeepSeek全球爆火,而我又想起了这个问题,又重新尝试了用原有的方案生成概括,结果差不多,仍然不可用。

拜托,随便一个大模型都应该比这个好吧!

去研究了一下此功能的文档,发现文档上完全没有这个功能的任何细节信息,没说用的什么模型、什么Prompt,也不让用户自己定义模型。一年过去了,功能效果丝毫没有改进,似乎像是被放弃了。说得通,毕竟是微软。

用Azure AI部署的DeepSeek R1总结文章

实话说,我很少直接和大模型聊天。我使用AI基本只有让Copilot回答编程问题以及生成代码,在编程场景之外我基本完全不用AI,所以也一直不知道怎么把AI应用到我自己的工作和生活流中。我的工作也和AI毫无关系,即使公司策略是All in AI,但是我组仍然和AI似乎扯不上边。

而此时,需求终于来了,为何不让大模型帮我总结文章?

Azure AI是一个微软做的Model as a service平台,可以直接在上面部署、使用、微调模型,不需要自己管理基础设施。DeepSeek R1模型发布后没几天,Azure AI可以直接就支持了部署(公告),甚至没有价格表,意思是:它是免费的?

我立刻去Azure AI上注册了Project,部署了DeepSeek R1。

用Azure AI部署模型非常方便:

  1. 注册好Project
  2. 进入模型市场,选择DeepSeek R1,填一个Deployment Name,部署
  3. 然后拿着Azure AI Endpoint, API Key以及这个Deployment Name,根据文档安装调用TS SDK
  4. 设计一个Prompt
  5. 调用SDK

点点就能部署模型

Prompt我随便想了一个:

Summarize the article in the next message in language ${languageCode} in 100 words. Return the result in plain text format, without any other information.

其中包含三个消息:

  1. 语言是${languageCode},这是文章的语言参数(cn, en),这样文章的总结和文章本身的语言一致
  2. 要求返回纯文本信息,不包含markdown
  3. 100字左右

另外,R1模型总是会返回思考过程,思考过程用<think>标签包裹,所以还需要在结果中把<think>中去除。

然后我立刻把2024年总结扔给AI,很快就拿到了结果:

作者回顾毕业一年后生活,探讨如何在最佳年龄充分体验当下。通过密集旅行与亲友刷新地图边界,感受陪伴的意义;开启独居生活后在郊区保租房打造个人空间,享受烹饪与自由。工作方面身处技术运维"小而美"团队,虽遇职业发展隐忧但珍惜当前性价比高的稳定状态。面对年龄增长带来的体能下降与生活抉择困惑,既珍视现有平衡又焦虑是否该打破舒适区。最终秉持认同感驱动原则,既保持现状又在游戏启发中思考:是继续深耕现有生活,还是用黄金年龄冒险转型,答案留待明年揭晓。

哇!这效果真不错啊,句子比我自己写得工整多了!

于是,我将脚本应用到博客中的所有文章,基本上所有文章都获得了极好的效果。这里给出几个例子:

文章原总结现总结
在西雅图,给生活换个环境本文描述了作者在参加微软的Aspire Start Strong+活动时,感受到了全新的生活环境和社交元素。作者参与微软组织的Aspire Start Strong+活动,赴美体验全新生活节奏以缓解职场单调感。西雅图市区高楼林立却人流稀少,郊区以分散式城市结构为主,无车出行困难。活动中高强度社交贯穿全程,跨文化交流时语言障碍明显,英语听说能力面临实战考验。与全球同龄校招生互动后,作者反思环境对人际关系的影响:校园阶段共同目标易建立联结,职场后各自生活轨迹差异导致社交需求转变。短暂六天打破常规,接触多元文化,更深刻思考个人追求与环境的关系。
从调库到翻源代码:给wakapi增加SQL Server支持本文作者在上一篇文章中提到了如何为博客增加点击量监测,并部署到Azure。 后来,同学推荐了wakapi项目,重新实现了wakatime的后端API。 作者发现wakapi并没有原生支持SQL Server,但使用了gorm作为数据库访问框架。 作者通过重用Dialector的逻辑,为关键词加上引号,把同一个go类型在不同的数据库中映射为不同的列类型,以及使用merge into语句来模拟upsert的行为。 他认为go非常explicit,虽然他不喜欢它,但它非常简单。 解决这些问题后,PR顺利合并进了主分支。作者在尝试将Wakapi项目迁移至SQL Server时,遇到一系列数据库适配问题。包括SQL语法差异、ORM框架配置、外键约束冲突及GORM库的Upsert功能缺陷。通过修改原生SQL语句、动态调整时间字段类型映射、重构外键关系、手动处理唯一索引冲突,最终解决兼容性问题并成功合并代码。此次实践深入了解了SQL Server特性与GORM内部机制,验证了通过实际项目攻坚学习技术的有效性。
A Kotlin DI Framework in 50 Lines and Thoughts on KotlinThe document discusses the challenges and benefits of using a Dependency Injection (DI) framework in a Java project. It highlights the two main options for dependency management: introducing a full-blown DI framework or using traditional object instantiation or simple factory pattern, which can be time-consuming and cumbersome. The author uses the example of a simple Java project where the interface and implementation class pattern was used to decouple the interface and implementation, but it also introduced complexity. The document suggests using delegation and classpath scan capability to achieve minimal dependencies and extra code, and provides a code example to help understand the process.The article discusses choosing dependency injection (DI) for small projects, comparing full DI frameworks (verbose) versus factory patterns (clumsy). The author developed a lightweight DI solution using Kotlin’s delegation and classgraph for scanning. Annotations (@Service, @ServiceImpl) mark interfaces and implementations, while a di() function delegates dependency resolution, enabling singleton injection with minimal code. Benefits include simplicity, circular dependency support, and dynamic resolution, though limitations include no init block usage and lack of advanced features. The author praises Kotlin’s modern features (null checks, lambdas) and how diverse programming paradigms expand problem-solving approaches, emphasizing tools’ influence on design thinking.

本地模型的效果比较玄学

本来这篇文章到这里就该结束了,可是写到第一段的时候,突然想到:

随便一个大模型都应该比这个好吧!

那……我本地跑一个呢?

我的台式机是3070,一搜得知这个显卡最大可以装DeepSeek R1 8B版本。于是赶紧打开许久没有打开的Ollama,拉下deepseek-r1:8b

ollama pull deepseek-r1:8b

Windows版本的Ollama会在本地11434端口启动Ollama的API(定义),而由于我们是用的JS,Ollama单独出了ollama-js SDK,用起来更简单。

我直接pnpm i -D ollama,学习了ollama的SDK的使用,直接把给完整版R1用的Prompt发给R1 8B,拿到以下结果:

这篇文章通过作者的个人经历和思考,探讨了在“最好的年龄”该如何度过这一主题。作者从20多岁进入巅峰年龄开始,经历了身体素质逐渐下降、工作环境变化以及面对各种选择的内心挣扎。\n\n### 主要内容总结:\n1. 身体与年龄的关系:作者承认随着年龄增长,身体素质逐渐不如从前,尤其是到了“下半场”,这可能对日常生活和工作产生影响。\n \n2. 工作环境与不确定性:虽然选择了一家外企,因为其稳定性和良好的发展机会,但半年后发现公司结构频繁调整,高层变动较大,这让他对未来充满担忧。\n\n3. 面对选择的内心挣扎:\n - 职业与生活的平衡:作者更倾向于保持现状,尤其是对稳定工作的认可和对外资企业组织结构和晋升路线的信任。\n - 物质与生活的影响:虽然对大城市有向往,但也清楚地认识到当前平淡的生活状态的珍贵,不愿为了追求新潮而放弃现有的优秀工作。\n\n4. 游戏中的价值观:通过《沙石镇时光》这款游戏,作者找到了认同感和热爱一件有意义的事的精神状态,这成为他生活中重要的支撑力量。\n\n### 总结:\n文章表达了作者在成熟与不稳定之间的平衡问题上缺乏明确答案,内心对现状的满意与对变化的恐惧之间的矛盾。尽管没有给出明确的选择建议,但通过个人经历和情感描写,揭示了在最佳年龄如何度过这一哲学性问题,鼓励读者反思自己的生活态度和价值观选择。\n\n文章语言流畅,情感真挚,通过对工作、生活和游戏的多角度描述,展现了作者内心的复杂性和对未来的不确定性。

嗯?这个模型怎么不听话?这个Prompt提到的三点(语言、字数、格式),字数和格式的要求根本没有满足!

Summarize the article in the next message in language ${languageCode} in 100 words. Return the result in plain text format, without any other information.

我试了很多次prompt,仍然没找到什么方法能够让它同时满足这三个需求。同一个提示词,有时候能生成不含markdown的文本,有时候生成又包含;文字字数的限制也是不一定生效。试了多次也没获得好的结果。

除了DeepSeek R1 8B,同时还试了llamafamily/llama3-chinese-8b-instruct,而这个模型的效果就好一些,但是生成多了也会出现不听话等问题。

本文讲述了一名25岁男青年在他的第二十年代度过的时间,他在这段时期里,选择了深入体验现有的生活,在旅游和工作方面都有所变化。他认为这个阶段是他的成长期,是他开始独立生活、选择自己喜欢的事物,并且接受不确定性的阶段。在文章中,他对未来充满了想法和担忧,但最终还是无法预测。

小模型的效果确实和大模型没法比,而不同小模型的精调不一致,效果也差别很大。

推理过程中GPU计算量不大,主要是占了很多的显存。看来接下来换个16G显存的显卡,应该就可以跑更高级的模型了。

运行推理过程的GPU占用

代码不是业务逻辑,而是大模型调用脚本

这次体验让我认识到一点,用LLM写功能的流程和传统的软件工程完全不同:

在传统软件中,业务逻辑总是精确地通过代码表示。不管需求多么复杂,这些需求总会在代码中出现。

而用LLM做的功能,不管写什么需求、是什么领域,写出来的代码是基本上都是一样的,代码本身只是个大模型API Caller,实际上的业务逻辑包含在大模型里,二者的接口是提示词。

如何编写提示词完全就是一个玄学,完全和精确、科学完全不沾边。不同的提示词就得出完全不一样的结果,甚至同一个Prompt得到的结果都不一定相同。

这种不确定性让我感觉有点不安。传统的软件即使再复杂,如果模块划分合理、测试充分,起码行为是可预测的,也总做或多或少的维护。而用大模型实现的功能,世界上没有人能直到它是怎么运行的,下次能不能用、有没有可能出什么问题,完全靠天决定。

不过AI确实解决了很多之前想都不敢想让机器解决的问题,很多问题也不需要那么精确。希望以后能找到更多大模型适用的使用场景。

2024年总结

2024年12月31日 19:45

最好的年龄该如何度过?通过深入体验现有的生活

毕业后的第一年,没有物质压力,有大把时间可以自由支配。这最好的年龄该怎样度过? 我今年的答案:深入体验现有生活

旅游,和朋友和家人刷新地图的边界

随着之前的朋友毕业、上班,有了更多时间,以及个人工作生活的稳定,今年的旅行数量可能大于之前26年来的总和,三个国家,国内十余个城市和景点。根据航旅纵横的数据,只算飞机,今年的飞行里程甚至超越了99.13%的用户。要是有规划地选择航司,现在应该能飞个银卡吧。

航旅纵横数据

旅游照片

我不是一个非常喜欢旅游的人,如果只有一个人,外出旅游不会成为我的首要选择。正因为有了朋友和家人一起,我才能有机会和动力去打卡这些可能之前都没有听说过的地方。比旅游本身更重要的,是和朋友和家人在一起的这段经历。很感谢你们愿意花时间陪我去做我一个人不会去做的事情。

和朋友们欣赏演出的票根

搬家,开启独居生活

和大多数人一样,上大学前和家人合住,上大学后和舍友合住,而在今年8月,随着合住的大学同学搬走,我终于租下了一个一室一厅的公寓,第一次开始独居生活。

我的“第一套房子”是一个公司附近的保租房公寓。好处很明显:离公司比较近(电动车10分钟),楼体和装修新,空间勉强够用,各个功能区分割比较现代和合理(甚至是三分离的卫生间),且也没有遇到太多质量问题。

但更重要的是,没有其他选择了!公司地处偏远,附近几乎全是老破小楼梯房,电梯房本来就寥寥无几,而离地铁和公司近的房子只有几栋10年前的动迁房,而实地看房的第一印象就是廉价的装修:本来就只是为了出租而进行的装修,在10年后更加显现出岁月的痕迹,租金却也没比保租房公寓便宜多少。

于是也不用犹豫了,很快定下合同,还没入住的时候我就迫不及待地购入了升降桌,在家人的帮助下,将这个可能会是待的时间最多的地方好好打造了一番,还正好蹭上手机号所在的运营商的活动,拉上了18块一个月500M的宽带,开始美美地独享这个小窝。

客厅(工作室)以及升降桌

由于公寓是住宅标准,所以有正常的厨房和煤气。一个人住,也不用拘泥于吃饭的形式:想吃火锅却懒得买电火锅,也可以直接凑在灶台前吃。

涮肉

为了解锁更多菜谱,我还买入了一个电压力锅,平时可以当快速的电饭锅,当想做需要长时间炖煮的菜品,如烧牛腩、焗鸡、梅菜烧肉时,因为锅可以产生压力,所以这些菜品都可以用一小时左右的时间完成,操作也非常方便。

焗全鸡

这次搬家,算是达成了独立生活的第一个小目标。接下来又追求什么呢?

公司有非常宽松的在家工作的政策,越来越老油条的我去公司的次数越来越少。而只要进城,不管具体去哪儿,甚至都得先花40分钟才能到城市边缘。即使不进城,当我在外完成每天8000步,差不多6-7km的散步任务时,路线上只有老破小、厂房和大货车。

阳台外景色,楼下是菜地,对岸是厂房

突然有一天想通了:既然不用每天通勤,为何不进城呢?公寓的单位租金已经100块/平米,接近中外环的楼梯房,为什么不再加点钱,去体验真正的大城市生活?

下列打油诗来自这个知乎回答,我觉得不仅是设备,任何选择都适用此原理。

“顶配论”

明年的7、8月又将迎来搬家,我已经开始期待下一个房子,以及真正的住在大城市的体验了。

工作,难得的平衡,却隐含风险

今年是第一个完整工作的年份,也第一次享受了公司组织的福利和活动:

  • 6月份的西雅图之行是个意外之喜,花了公司3万块免费旅游,在西雅图给生活换了个环境
  • 每年例行的迪士尼票还正好在国庆大假前,人流量反常地少,以至于没有买速通还能一天玩完所有项目
  • 年底第一次接触滑雪,直接选择找教练练单板,2小时好歹学会了落叶飘,能正常滑下来了

工作中的小小福利

我对公司和小组的工作也有更深入的理解。我发现,我很幸运在一个“小而美”的组:

  • 产出为运维领域的技术产品,并不是在做黄赌毒、投机倒把、坑蒙拐骗的帮凶,符合我自己的价值观
  • 所用技术本身和市场并不过于脱节,且以功能而非技术分工,鼓励大家了解项目的各个方面,而非仅仅是个螺丝钉
  • 主要为企业用户,以稳定为重,节奏较慢
  • 同事友好,人员组成也比较稳定

再加上宽松和假期和在家工作政策,听起来是一个性价比高、适合享受生活的工作。

当然,有时候也会有一些担忧:

  • 无法和当前大热的AI扯上关系,发展空间受限
  • 客户本身的需求也不复杂,造成产品深度和难度不够
  • 想卷都卷不起来,很多时候有一拳打在棉花上的感觉,再加上经典的大公司病,很多时间和精力都无谓浪费掉了
  • 实习两年后我甚至仍然是全组最年轻的成员,没有新人加入

所谓相对稳定也是当年选择公司的一个重要原因。

  • 作为J人,有计划、有秩序的生活让我觉得安心
  • 大公司确实有比较成熟的组织结构、产品规划、工作流程以及晋升路线,作为小兵需要考虑和有能力左右的事情不多
  • 公司大多数同事都是要么毕业就来,要么在外面没干多长时间就过来并在此长时间工作

可是,

  • 仅今年上半年期间,认识的朋友所在的组有的被多次调整所在组织和业务,有的直接离开中国,像我组这样组织结构和业务没有变化的反而是少数
  • 也就在一年以前,公司因为在AI领域的大手笔动作,股价接连高升,彷佛50年的企业即将迎来第二春,结果今年上半年多次组织结构调整,几次财报均不达预期,明年初裁员的传言连父母都有所耳闻
  • 认识了Intel的被裁员的老哥,想起五年前第一次来到闵大荒这边看到Intel的大楼,还觉得Intel的统治地位牢不可破,谁能想到也就不到5年,Intel的市值已经不足当年濒临倒闭的AMD一半

外企总是被公认为稳定。可是,仅仅半年就能发生这么多变动,谁知道明天又会有什么惊喜?

该有所改变吗?

回答文章开头,最好的年龄该如何度过?,为什么会有这么一个问题?

今年有一次羽毛球局,和一个偶尔组的同事大姐姐混双打男双,打完后她对我说的第一句话是:“感觉你的身体素质不如之前”

混双打男双局里,混双方男队员的能力能很大程度影响表现,而能力主要又分技术和身体素质。我没有做技术和身体素质的专门训练,但我自认为和之前也没有什么区别。排除了这些因素,那为什么身体素质下降了?只能归结于年龄大了一岁。20多岁是人类的巅峰年龄,而我已经处于20多岁的下半场,身体素质已经开始走下坡路,时间已经不站在我的这边。

工作之后,生活逐渐稳定,可内心却充满了各种患得患失:

  • 想留在大城市,但又觉得付出了金钱和住房质量,却并没有充分利用到大城市独有的东西
  • 清晰地明白当前平淡的生活状态的珍贵,但有时候又想追求更年轻化、更有活力的生活
  • 舍不得这在各个方面都十分优秀的工作,但又认识到稳定的预期已不存在,变化不可避免

该维持现有状态,还是应该有所改变?

今年我也面临过一些选择,进行了或者仍然在进行很多心理斗争。在这些选择和心理斗争的过程中,心里没有决定身体没有行动二者互为因果,最终保持不变成为了今年对这个问题的答案。

今年我游戏时间最长的游戏是《沙石镇时光》。在这款游戏里,玩家扮演一个新的沙石镇工坊主,去到一个没落的城镇开始职业生涯,认识了大量的镇民、经历了各种各样的事,最终让这个没落的城镇重现辉煌。

steam资料

这游戏剧情平平无奇甚至有点幼稚,好在内容丰富,设定也比较接地气。玩完后,给我印象最深刻的反而是主线的精神状态:认同、热爱一件有意义的事,和一个团队一起去追求它

回想自本科以来,所有我真心投入了大量时间精力的事,都是因为我认可它。如果我不认可一件事,那么无论这件事能给我提供多大的物质回报,我都没有办法说服自己去做它。即使不得不去做,最终都放弃了。

我总是尝试做一个理性的人,可是这一点就是最大的不理性。很多选择,即使理性告诉我它就是最优解,即使我知道我的想法是有局限性的,可是我还是无法接受。

明年还会出现选择,且会更加急迫。最好的年龄正在慢慢消失,明年我会做出什么选择呢?是充分珍惜当前难得的生活工作状态,在已经拥有的基础上尝试改良?还是打破平衡,用最好的年龄去冒险?那就只能明年才知道了。

博客的发展2:重写,重生

2023年6月17日 22:10

鸽了4年的更新

4年前的博客的发展1中,我提到了当时博客的几个问题。后来,我通过一个非常hack的方式解决了中文字数统计的问题(修复gatsby-transformer-remark插件中文词数统计错误问题,但是最重要的重构样式和完善UI设计的问题一直搁置,并且随着时间和技术的发展,项目也遇到了不少的问题,例如

  1. 样式的混乱使得我一直使用老的bootstrap v4版本,无法升级到最新的bootstrap版本
  2. gatsby生态更新太快,很多组件我无法理解它们具体做了什么工作

5月底研究生答辩完后,本来计划好的旅行,在我出发的前一天被我二阳直接推迟了。阳了后基本上也就只能在宿舍呆着。呆着也是呆着,我想起来博客这个”我的门面“。这三年博客本身基本没有任何更新,基本属于年久失修的状态。于是我决定给我的博客来个大手术。

新博客的亮点

  • 完全使用Next.js编写

由于研究生期间的项目(PKUHPC/SCOW)是完全使用Next.js编写的,我对Next.js非常熟悉。而Next.js本身也是一个非常成熟的React框架,并且也支持导出为静态网站的功能,并且有很多网站均使用了Next.js来作为它们的主页、博客等信息发布平台,所以我在想是否能重用之前的经验,用Next.js来搭建新的网站。一顿操作下来,除了遇到了一些和Gatsby的思路不太一样的地方,整个体验还是挺不错的。

  • 完全兼容原有博客

新的网站和原有博客在功能性、整体布局以及各个页面的URL方面都完全一致,原有的所有使用习惯和URL都可以直接使用,原来的所有功能现在都仍然支持,包括但不限于多语言页面、多语言文章、RSS等。这才是重写的真正含义吧:所有代码都完全重写了,但是不会影响任何已有的使用体验。

  • 使用Tailwind编写样式,去除CSS in JS方案

这是本次重写最重要的地方。我原来是CSS in JS的狂热爱好者,认为使用JS编写网站的所有方便是网页开发的最终目标。现在,我虽然仍然认为CSS in JS方案带来的灵活性是所有其他方案都不可比拟的,但是我也认识到很多情况下样式并不需要那么高的灵活性。另外,由于样式最终还是要到达CSS的层次,在把CSS in JS和其他第三方样式解决方案(例如之前用的bootstrap)集成的过程中,需要大量代码来将两套完全独立的样式系统整合起来。这也是之前样式代码极度混乱的根本原因。

例如,原来代码中的导航栏组件同时使用了bootstrap的Navbar组件,并通过styled-components在这个组件的基础上自定义了样式。在自定义样式时,还引用了TS中定义的样式变量。有的组件甚至为了使用bootstrap的定义在SCSS中的变量,故还引用了自定义的SCSS文件。而由于有的变量是定义在SCSS中,有的是在TS代码中的,所有很多变量(例如颜色)都需要定义两次。

import { Navbar } from "reactstrap";
 
const StyledNavbar = styled(Navbar)`
  && {
    max-width: ${widths.mainContent}px;
    margin-left: auto;
    margin-right: auto;
    padding: 4px 8px;
 
    transition: width 0.2s ease-in-out;
  }
`;
原来的标题栏实现部分代码

另外,我也认识到以传统HTML/CSS来布局和样式的一些优势,例如将UI与具体的开发框架解耦、更好的性能、以及甚至能在不启用JS的环境下展示页面等。当前,以tailwind为主的以传统的HTML/CSS为基础的样式方案非常火,这次我也直接采用tailwind以及基于tailwind的纯HTML/CSS组件库daisyui来编写新的博客,并体验到了前所未有的开发效率和开发体验。直接写语义化的类型名,确实比写JS代码要方便太多了。

tailwind自动完成体验

实现

完全采用Next.js App Router

Next.js的App Router功能可谓是万众期待,虽说有评论说这个功能(以及后续的Server Actions)把Next.js变成了PHP,但是不可否认的是,App Router极大地提高了开发体验和灵活度。

在新的博客中,App Router带来的各种优势里,让我最受用的是以下两点:

  • React Server Component (RSC,服务器端组件)

React Server Component (RSC)实际上是React的概念,在2020年就提出来了(Introducing Zero-Bundle-Size React Server Components - React Blog)。简单来说,原来的React的组件都是运行在客户端的。浏览器首先把项目代码下载下来,然后再浏览器中运行代码,这些代码将会通过浏览器端DOM API在浏览器上画出UI,并处理用户的交互。而React Server Component允许用户编写运行在服务器端的React组件。而Next.js 13第一次实现了这一概念。

这颠覆了传统的前端开发模式。代码在服务器端运行的,这就意味着组件可以直接执行在服务器端才能执行的代码,例如访问数据库等,而不再需要单独的一套API来实现客户端和服务器端之间的交互。

在新的博客中,所有博客的内容都是以本地文件的方式存放在contents目录下。所有的页面会去读取自己所需要的数据,之后将这些数据渲染出来。

假设我们的网页不是一个静态网站,而是一个传统的React+后端的模式,那要实现这个功能,我们首先需要设计一个API来获取后端的数据,在后端,我们编写一个服务器实现这个API,然后在前端,我们通过fetch调用这个API,拿到数据后在UI上渲染出来。

// 后端,编写API
const app = express();
 
app.get("/articles/:id", async (req, res) => {
  const content = await readContent(req.params.id);
 
  res.send(content);
});
 
app.listen(5000, () => {});
后端
// 前端,通过fetch API获取数据
export const Page = ({ id }) => {
  const [data, setData] = useState();
 
  useEffect(() => {
    fetch("http://localhost:5000/articles/" + id)
      .then((x) => x.json())
      .then((x) => setData(x));
  }, []);
 
  return data ? (
    <ArticleContent data={data}/>
  ) : <Loading />;
前端

然后通过RSC,我们可以直接使用React来实现这个需求:

export default async ({ params }: Props) => {
 
  const data = await readContent(params.id);
 
  return (
    <ArticleContent data={data} />
  );
};

这区别实在太大了。不再需要一个单独的后端项目,不再需要复杂的API设计、管理、调用、维护,从获取数据到渲染UI的过程非常直观。甚至说如果这个ArticleContent组件不需要用户交互的话,用户甚至不需要下载这个组件的代码,浏览器不启用JS就能访问网页。

从某种角度来说,App Router确实是把React变成了PHP这类传统的服务器端渲染的方案。但是,毕竟Web前端是JS的世界,PHP等不能直接使用后端语言编写前端的交互逻辑,只能做一些简单的模板替换的功能,一旦涉及一些复杂的逻辑和交互,就不能不重新使用JS,而这就要求两套不同的语言,两套不同的工具链以及两套不同的生态,以及前后端之间的交互。而Next.js是以前端为基础,用一种非常自然的方式将前后端融合在一起,用同一套生态编写从前端交互到后端逻辑整个链条,实际上是一套和传统完全不一样的方案。

  • Colocation,即把相似用处的文件放在相近的位置

在原来的pages目录下,每个文件定义了一个页面。例如/pages/test.tsx/pages/test/test2.tsx就分别对应/test/test/test2两个路径。但是,在绝大多数情况下,一个页面中的代码都不能在一个代码中完全写完。对于一些公用的组件,例如布局的header, footer等,我们可以把这些代码放在类似layoutscomponents的目录下,这些组件不涉及任何业务逻辑,可由具体的业务页面引用并组装。

现在博客下,会由多个页面使用的组件

但是还有一些组件,它只在某个特定的页面下有用,例如为了完成某个特定的业务逻辑的组件。这种组件一般来说又是过于复杂,不能把它直接写在页面文件中,但是如果把这些组件直接放在页面组件文件的旁边,那么它们会被当成一个新的页面。

由于在原来的事件中,我会创建一个pageComponents目录用于存放这种位于真正的基础组件(components)和页面(pages)之间的组件。例如在下图中,pageComponents/admin/AllUsersTable.tsx就是一个比较复杂的、涉及到业务的组件,它只会在pages/admin/users.tsx中被使用。

pageComponents

除了这一方案,我也看到了一些项目采用的是和Angular类似的Module概念,把某个功能相关的代码都放在一个modules/模块名目录下,然后在pages目录下引用模块下的页面组件。

但是不管是什么方案,实际上都是在为一个文件=一个路径这一概念打补丁。这一概念看着很美好,但是只要项目复杂度稍微高一点就会遇到上述的问题。同一个功能,有的代码在pages下,有的在pageComponents下,这会使得文件非常混乱。

App Router解决了这一个问题。在App Router下,路径由目录(而文件)定义。每个目录下,只有一些特殊的文件会被Next.js处理(例如page.tsx为这个页面的组件,layout.tsx为这个路径下的公共布局,其他文件Next.js直接忽略,都由自己组织。这就使得我们可以把一个页面所需要的组件拆分出来,放在和页面相同的目录下。

例如在现在的项目中,app/articles/[[...params]]包括了文章列表页面的定义,其中就需要一个文件列表页面的布局的组件ArticleList。这个组件很明显需要被拆分出来。在原来的实践中,这种组件就应该被放在pageComponents或者components里。但是这个组件实际上只会在这个路径下被使用,所以使用App Router后,我们就可以把这个组件放在这个页面的文件(page.tsx)的旁边。这样,我们保证了所有这个页面相关的业务逻辑(公共组件不包含业务逻辑)都存放在这个路径下,这对代码后续维护、多人合作开发等方面都有非常多的好处。

现在博客下,文章列表页面布局

App Router所带来的优点远不止这两点。由于本博客是个静态的博客,且整体布局较为简单,所以并没有用上使用Next.js的动态功能,但是在我其他的项目中,App Router的嵌套布局Nested Layout)、Server Actions带来的直接在前端代码中调用后端逻辑的能力都极大地提高了网站开发的效率。

Next.js静态生成

静态网站

我之前使用Next.js的项目都是传统的前端应用,也就是编译为前端+一个提供服务器端渲染(SSR)能力的Express后端的传统的Next.js项目。但是Next.js一直还支持直接生成只包括HTML/CSS/JS的静态网站的能力。

传统的单页应用(SPA)会把整个应用编译为一个(或者多个)JS bundle以及一个实际上并不包含真正UI的模板HTML。用户访问任何路径时,都会下载这个HTML。这个HTML的唯一作用就是提供一个根DOM组件以及引用编译好的JS Bundle。JS Bundle将会被自动下载,通过浏览器的History API在浏览器端实现路由功能,并负责通过DOM API渲染用户的UI。

vite build构建出的项目

而Next.js生成的静态网站和Gatsby, Hugo等静态网站生成器相同,会在编译时对每个路径获取这个路径所需要的数据,并将这些数据渲染成HTML。渲染出的结果中,每个路径都有对应的HTML。比如在下图中,about/me.html就对应了/about/me路径,并且其中包含了在服务器端渲染后的UI。用户访问路径时,会直接获取这个HTML,并直接就能渲染出已经渲染好的内容,无需等待下载和执行JS Bundle的过程。

next build构建出的项目

和Gatsby的区别

在我上次重写ddadaal.me时,我选用了Gatsby,因为当时Gatsby的生态更加的成熟,有大量现成的模板、插件和教程可供使用和参考。几年后的现在,Next.js的静态生成功能也是非常成熟了,并也提供了很多的API来实现静态网站渲染的功能。但是和Gatsby项目,Next.js提供静态网站渲染的API的思路有所不同。

Gatsby主要通过GraphQL让开发者访问数据(Gatsby and GraphQL)。开发者在页面中可以声明这个页面所需要的数据的GraphQL查询,并在页面中通过props访问读取到的数据以及渲染UI。在编译时,gatsby将会负责运行这些查询,并将数据传递给需要数据的组件。而可以访问到的数据,则可以通过插件或者自定义gatsby-node.ts脚本来向后端的GraphQL服务器中增加数据节点。

// https://github.com/ddadaal/ddadaal.me/blob/57fe926eb0/src/pages/slides.tsx
// 声明需要的数据
export const query = graphql`
  query Slides {
    allSlide(filter: {type: { eq: "dir" }}) {
      nodes {
        name
        html_url
        type
      }
    }
  }
`;
 
interface Props {
  data: {
    allSlide: {
      nodes: { name: string; html_url: string }[];
    };
  };
}
 
const Slides: React.FC<Props> = (props) => {
  // 通过Props读取获取到的数据
  const { data: { allSlide: { nodes } } } = props;
 
  // 使用这些数据渲染UI
};

当然,要想更灵活地访问数据和创建页面,开发者还可以通过gatsby-node.ts编写编译时在node端执行的脚本。这个脚本是在编译器在Node.js中运行的,故可以访问任何本地数据。Gatsby还提供了大量Gatsby Node API来帮助用户创建页面、GraphQL数据等。

createPage({
  // 生成页面的路径
  path: "/articles/" + pageIndex,
  // 页面所对应的React组件
  component: indexTemplate,
  // 组件所需要的数据
  context: {
    limit: pageSize,
    skip: pageIndex * pageSize,
    pageCount,
    pageIndex: pageIndex,
    ids: notIgnoredGroups
      .slice(pageIndex * pageSize, pageIndex * pageSize + pageSize)
      .map((x) => x.frontmatter.id),
  },
});

总的来说,Gatsby通过GraphQL和Gatsby Node API将UI数据完全隔离开来。用户定义各个页面所需要的数据类型,一方面编写脚本或者插件将各类数据源转换为GraphQL等页面需要的数据,另一方面编写React代码将这些数据渲染成UI。

而在当前使用App Router的Next.js项目中,获取数据以及渲染页面的方法有所不同。Next.js的路由一直是基于文件路径的,没有类似Gatsby Node API的API以及gatsby-node.ts的脚本可以用来手动创建各个页面。取而代之的,是

  • 通过文件路径定义路径
  • 通过generateStaticParams函数获取所有可能的路径参数
  • 通过RSC同时实现数据获取和渲染

例如说,我的博客中/about路径下包含了/about/me/about/odyssey/about/project三个路径,分别对应3篇文章。要实现/about路径,我需要

  • 定义app/about/[id]目录
  • generateStaticParams中,返回id参数的所有可能值["me", "odyssey", "project"]
  • app/about/[id]/page.tsx(实际上不是这个路径)中,定义一个RSC,获取到路径的ID的参数,之后获取到对应的文章内容,并同时根据文件内容渲染出UI

可以看出,通过Next.js,我们不再需要GraphQL将数据和页面分割开来,而可以直接使用RSC同时完成读取数据和渲染UI的功能。通过generateStaticParams列举了所有可能的路径,然后对每个路径渲染它对应的RSC,生成了每个路径的页面,从而编译出了整个网页。

自定义的markdown渲染流程

在之前使用Gatsby时,我直接使用了一些现成的gatsby插件(如gatsby-plugin-remark)来帮助我完成把markdown渲染成HTML的过程,故我对markdown渲染的流程几乎没有了解。但是在Next.js中没有这些插件了,所以我就需要自己去学习markdown渲染的知识,并自己完成markdown渲染的工作。

当前,项目中是使用remarkrehype生态实现markdown的渲染的。remark是一套把分析并转换markdown的生态,包含由大量插件。它可以分析markdown文件并将其转换为AST,并支持通过各类插件对这个AST进行分析和转换。而rehype类似remark,只不过rehype是针对HTML的。整个渲染流程可以通过unifiedjs连接起来。

现在,博客在渲染markdown时,经历了以下的步骤:

了解了markdown的渲染过程给我带来了几个好处:

第一,我可以自己自定义渲染的流程了

之前,对于一些已有插件不支持的功能,我是通过一些比较hack的方式完成的。例如文章的Table of Contents,我是通过在渲染后通过DOM API分析页面中各个h1/h2/h3等元素来动态生成的。而现在,我可以自己去寻找可以解析TOC的插件(@stefanprobst/rehype-extract-toc),并将它插入到渲染的流程中,并在最后得到结果并自己完成渲染的过程。又比如,我想给渲染出的标题的前面增加一个图标,点击这个图标就获取到跳转到这个标题的URL。通过rehype-react,我可以很简单的实现这一点。

src/components/article/ArticleContent.tsx
.use(rehypeReact, {
  // ...
  components: {
    // ...
    // 使用自定义的React组件渲染h1/h2/h3组件
    h1: ((props) => <HeadingWithLink element="h1" props={props} />) satisfies
      ComponentType<JSX.IntrinsicElements["h1"]>,
    h2: ((props) => <HeadingWithLink element="h2" props={props} />) satisfies
      ComponentType<JSX.IntrinsicElements["h2"]>,
    h3: ((props) => <HeadingWithLink element="h3" props={props} />) satisfies
      ComponentType<JSX.IntrinsicElements["h3"]>,
  },
})

标题链接

第二,我可以完全自己控制RSS的渲染过程了

之前我采用的是gatsby-plugin-feed插件,通过定义GraphQL以及一些自定义的参数来生成RSS流。由于当时无法控制markdown的渲染结果,所以在生成RSS的时候感觉非常不自然。另外这个插件还不支持在开发时运行,所以我在开发时不能测试RSS的编译结果。现在,我可以自己创建一个app/rss.xml/route.ts的Route Handler,并和渲染文章页面一样,手动创建RSS的信息,以及各篇文章在RSS中的渲染结果。

静态生成图片

在编写整个网站的过程中,最大的挑战是如何生成博客内容所需要的图片

在所有能查找到的使用Next.js编写博客网站的文章和项目中(如Next.js官方blog-starter模板),都是通过public目录来引用图片等静态文件的。在编译时,public目录下的文件将会直接复制到构建目录下,在部署后,这些文件将可以直接通过/访问。

但是这并不能满足我的需求。因为我的博客中,博客文章和图片都是放在contents下的同一个目录下的。而contents目录不能被公开访问。

博客文章和图片存放在同一个目录下

<!-- 在Markdown中通过和md文件的相对路径访问 -->
![图片注释](./decompile.png)

一个简单粗暴的解决方案是编写一个脚本,在编译后把所有静态文件从contents复制到public下,并在编译markdown时,修改所有的图片路径到编译后的路径中。但是这个做法也太不优雅了,有没有什么更好的、不需要自定义编译流程的方案呢?

答案是Route Handler

Route Handler可以使开发者对某个路径编写自定义的处理逻辑。通过Route Handler,我可以定义一个专门用于获取静态文件的路径。我定义了一个/articles/asset/[...path]的route handler,当使用GET方法访问这个路径的时,handler将会去读取这个路径的对应的文件的内容,并以流的形式返回。Route handler同样支持generateStaticParams。通过这个方法,我遍历contents下的所有静态文件。这样,在编译时,Next.js将会把contents下所有文件的路径都传入这个Route Handler并运行,并将handler的结果(也就是文件内容)存放在/articles/asset/contents/{文件相对于contents的路径}下,发布后可以通过/articles/asset/contents/{相对路径}这个URL访问到图片。

src/asset/[...path]/route.ts
export async function GET(request: NextRequest, { params }: { params: { path: string[] }}) {
  const fullPath = params.path.join("/");
 
  const fileStat = await stat(fullPath);
 
  // 读取文件流并返回
  const stream = createReadStream(fullPath);
 
  // @ts-ignore
  return new NextResponse(Readable.toWeb(stream), {
    headers: {
      "Content-Type": lookup(fullPath) ?? "application/octet-stream",
      "Content-Length": fileStat.size,
    },
  });
}
 
export async function generateStaticParams() {
 
  // 遍历所有路径
  const paths: { path: string[] }[] = [];
 
  async function rec(dir: string[]) {
    const dirents = await readdir(dir.join("/"), { withFileTypes: true });
 
    for (const dirent of dirents) {
      if (dirent.isDirectory()) {
        await rec(dir.concat(dirent.name));
        continue;
      }
 
      paths.push({ path: dir.concat(dirent.name) });
    }
  }
 
  await rec(["contents"]);
  return paths;
}

编译后的articles/asset路径,包含有所有的静态文件

现在图片有了,下一步是将markdown中对图片的引用地址修改到真实的编译后的地址。这实际上很简单,通过rehype-react将HTML的<img>使用自己的组件渲染,然后在自己的组件中把src属性修改为真正的图片路径即可。

src/components/article/ArticleContent.tsx
.use(rehypeReact, {
  // ..
  components: {
    // 使用自己的Image组件渲染HTML中的<img>
    img: ((props) => <ArticleImageServer article={article} props={props} />) satisfies
      ComponentType<JSX.IntrinsicElements["img"]>,

我们使用了Next.js自带的<Image>组件来显示图片(在ArticleImage中)。这个组件有很多友好的功能,例如通过指定图片大小防止Layout Shift,在支持的环境下支持先加载更小的图片以更快的显示界面等。通过把<Image>和自定义的markdown流程相结合,我们就实现了显示图片的需求。

多主题

本次更新在样式方面最大的可见的更新就是支持了多主题。目前网站已经开放了12个主题可供选择,当然,我的美学能力远远不够自己设计这么多的主题,所有主题都是由daisyui提供的

daisyui首先定义了一些固定的颜色变量,这样在编码时,所有元素的颜色都可以通过颜色变量,而不是写死的颜色值,来指定。例如下面的代码就指定了一个背景颜色为base-200、文本颜色为text-contentul组件。

<ul className="bg-base-200 text-base-content">
</ul>

之后,daisyui通过识别<html>组件的data-theme数据属性来获取到当前用户所选择的主题,并通过CSS选择器将对应的颜色的CSS变量修改为对应的主题的对应颜色变量的值。要想切换主题,只需要修改<html>组件的data-theme数据属性即可。

data-theme

由于各个主题的颜色风格大相径庭,我原来博客的代码背景就不可以使用了。新的主页背景必须得能够自适应完全不同的颜色风格。要想实现这一点,新的背景必须是使用CSS动态生成的。

于是我在网上找到了一个绝妙的网站,这个页面中提供了数十种纯用CSS实现的背景动画。我选中了其中的第三个Floating Squares。这个动画的背景色、以及各个方块的颜色都是由CSS的定义的,我需要做的,就是把其中的基础颜色替换为当前所使用的主题的CSS变量,这样,新的背景就能自动和当前所使用的主题相匹配了。

.area {
  /* 使用daisyui的颜色变量 */
  background: linear-gradient(to bottom, hsl(var(--p)), hsl(var(--pf)));
  width: 100%;
  height: 100vh;
}
 
.circles li {
  position: absolute;
  display: block;
  list-style: none;
  width: 20px;
  height: 20px;
  /* 使用daisyui的颜色变量 */
  background: hsl(var(--a));
  animation: animate 25s linear infinite;
  bottom: -150px;
}

总结

本来我一开始并没有打算完全重写整个项目,而是打算在已有的代码上做一些小修小补。但是当我真的打开了代码开始准备修改时,就发现了原来的代码中各个组件之间的依赖像是形成了蜘蛛网一样,完全无法下手。随便改任何一处已有的代码,都会涉及到巨量的其他代码,可谓牵一发而动全身。想起来2018年开始写Gatsby的ddadaal.me的时候,一大驱动力就是原有的屎山实在无法维护了。而5年后,当年的新代码变成了新的屎山,而被更新的代码取代之。果然,和世间其他万物一样,技术在发展,代码也是常用常新,需要不停地跟上时代的步伐。重写后的博客功能和以往一致,但是更轻、更快、更易维护,也算是一次成功的重构实践。

2022年总结

2022年12月31日 23:59

转折点

又是一个转折点,是2021年总结的最后一节小标题。不出意外的,2022年确实是一个转折点

玩具项目开始“玩真的”

上半年,继续维持2021年下半年的状态,像是做个玩具一样,在一个人的宿舍没有压力地写着实验室的项目。

可是,像是被赶鸭子上架一样,五月份项目放到GitHub开源(仓库地址),在一个小会上做报告,六月份写了一篇论文投到HPC China会议上然后拿了满分评价和优秀论文提名,之后项目组多了好多学弟学妹、老师和同事。公开的demo集群(点击访问,用户名密码参考仓库README)上线了,潜在用户来找我们了:项目突然变得认真了。

GitHub动态

会议论文评分

飞书组织

从一个随便写着玩的小程序员到啥都管的“小老板”,带人给了我更大的压力。不同的人有不同的背景、不同的能力、不同的期望。应该把什么样的工作给谁做?我自己应该把事情做到什么程度?我应该提供什么样的支持?什么事情应该他自己思考?怎样高效地沟通?如何我的想法更高效、更准确地告诉其他人,如何让同事更好的告诉我他所想的?如何和一个有数年、甚至数十年工作经验的人合作?这些问题对不同的人都有不同的、且随时都在变化的答案。我的一切行为可能都会影响同事接下来的一段时间如何开展工作,一丁点没有考虑到的细节,一点工作需要的、但是没有写进文档的、而且我也没亲口告诉的知识点都可能让别人浪费数倍的时间。甚至,我连我需要什么样的人这样基础的问题,我都没法给出一个自信的回答。如何作为一个并没有实际工业界经验的小小研究生来说,我只能根据在课堂和网络上看到的以及短时间的实习中体验到的经验依葫芦画瓢,尝试建立一个尽可能高效的团队。目前看来还有很长的路要走。

meme

项目本身也有很长的路要走。项目复杂起来之后,CI/CD流水线、自动化测试、文档、协作工具、仓库管理、版本发布,甚至如何保证代码风格的一致性,这些繁琐的事情的重要性越来越高了。代码之外,用户究竟需要什么样的功能,如何让用户部署和使用系统更方便,什么样的需求该做和不该做,项目应该朝什么样的方向发展,虽然最终拍板的不全是我,但是这些问题都需要我的参与和讨论。这些问题都不是确定的问题,也没有一个固定的量化指标,但是对于项目来说,可能比写100个功能点更重要。

我现在才算摸到的真正的工程的冰山一角。我自认为我能够勇敢面对困难,但是这半年来,我经常想逃避,issue打开了就放在那里,随便挑点简单的功能点写写代码,而不去想真正紧急的问题,审PR有时候也粗心大意,出现过好几次有重大问题的bug的PR进了主分支的情况。工作后挑战就更大了,如何平衡工作、生活和项目变得格外重要。什么工作都有难点,我不想坐科研的“冷板凳”,但是也逃不过工程的不确定性和复杂度啊。

新的娱乐方式

5月份,北京疫情严重了,进入了准封城状态,而高校当然是封控急先锋。5月份封校了,早期我们住校外宿舍区的连宿舍区都出不去,被迫在万柳这个弹丸之地开发娱乐活动。扔沙包、跳广场舞、甚至放风筝,世界仿佛回到了我甚至还没有出生的80、90年代。

扔沙包

放风筝

这些室外活动随着学校开通燕园班车以及后续学校解封就消失了,但是室内活动却保留了下来。在工作之余,和朋友们一起吃烧烤、打麻将,还是非常惬意的。

打麻将

吃烧烤

下半年,认识了更多的羽毛球球友,高峰期打球频率甚至提高到了一天2小时。甚至第一次加入了院队,第一次获得了一件有自己名字的队服,第一次和队友参加了几次团体和单项的小比赛。虽然水平很一般,几场比赛都没出线,但是这确实是第一次的体验。我也希望能够在最后一个学期里和同学们多多练球涨球,起码比赛不一轮游一次吧。

队服

最后的比赛和实习

看我的简历就可以知道,我的本科和研究生生活就是用各类项目比赛撑起来的。2021年和本科老搭档参与了一个AI创新应用大赛,本以为已经烂尾,却突然复活并获奖。虽说只是个优胜奖,但是也足够为我这6年的小比赛生涯画上句号了。

获奖名单

去年末,面试并加入了之前完全没有听说过的alluxio,并在里面第一次体验到了一个外企背景的小技术公司的工作生活状态,并第一次给一个还算出名的开源项目做出了一点不是很简单的贡献(PR)。

PR

作为一个长期把微软作为第一择业目标的纯粹的微软粉,暑期实习选择了上海的C+AI大组,并在上海数十天的40度的天气、以及此起彼伏的疫情下onsite了整个暑假。这个暑假也是第一次和同学租房(在苏州时租的单人公寓),体验了一把老破小,过了两个月自己买菜做饭的居家生活。体验很不错,但是决定之后再也不租老破小了,高端小区买不起,多花几百块钱租一套体验一下也是很不错的。

做饭

房子外景

躺平生活

回到了原点的职业选择

已接受MS Offer

兜兜转转又回到了原点呀。

本科前两年半时,我拿定主意直接工作,却在最后时刻,抱着想看看有没有新的机会的想法,极限转弯踏入了研究生的大门。而研究生的前两年的体验让我认识到高校做工程不靠谱,回去看业界的机会时,发现实验室做的工作不能让我踏入一个所谓更“高阶”的工作,我能投向的业界工作和读本科时基本没有区别。后来,当我意识到,当前国内大环境下体制内的技术工作也并非一无是处,想把握北大应届毕业生这个机会了解一下体制内的机会,但却错过了这个时间窗口,最后仍然投向了微软,和三年前的唯一的区别也就是大组(C+AI vs STCA)和工作地点(苏州 vs 上海)的区别了。

说遗憾肯定是有点遗憾的,毕业时选择工作可能是最重要的人生选择。有些地方毕业时不进,之后就再也不进去了。而很多人都向往的北大学历最后在找工作方面并没有发挥什么用处。但是这几年让我认识到,人是会变的,现在的想法只能代表现在,不能代表未来。甚至有几个被项目的事情困扰的晚上,我还在认真考虑现在去科研是不是还来得及。考虑了这么多,最后只会发现,一切都是有好有坏。在微软,起码短期内实现Work Life Balance、收入还可以的“小目标”实现起来还是比较简单的,而且还能接触到先进的软件产品和软件项目管理方式。虽然世界大环境对外企不利,但是世界变换得太快了,几年后的日子谁又能预测得到呢?

虽然终点一样,但是心态不一样了。在体制外,没有各种各样的限制,没人管的另一面就是自己为自己负责。这三年大环境的变化让我意识到,和体制内不同,公司是要赚钱的,我们和公司只是合作关系。发展得好,一起吃肉;发展得不好,分道扬镳。换公司(无论是自愿的还是非自愿的)对任何人来说几乎是肯定的事情。一定会发生的事情也没什么需要担忧的,需要做的只是为它做准备。公司的目标是为客户提供服务赚钱,那我们也得想办法增加自己作为个人(而不是作为公司的一员)对他人、对社会的价值。目前来看,实验室的项目似乎迈入了一个正确的轨道,采用的客户越多,越能体现我的价值。如果它真能发展起来,那对我个人的发展来说是一个大大的加分项。

迎接未知的新生活

这一年像是2019年的重演。升到毕业年级;暑假去外地微软实习;纠结了几个月后强势转向,2019年从就业选择了保研,2022年,从偏向体制内选择了上海微软。甚至直到疫情防控放开前,我还在担心明年会不会又像2020年一样,无法返校过最后一个学期。

但是有一点不同:现在,无论愿不愿意,都得迎接新生活了

高中时,所有精力都放在高考,“一分一千人”,似乎一切事情高考完就结束了;大学时,毕业就是一个看得见的里程碑,想工作的同学去实习,想科研的同学去科研,大家都为即将到来的新的生活而努力。而现在毕业了,工作了,没有看得到的终点了,反倒之前以学业为理由不想考虑的事情一下子都变得重要了起来,之前遇到了可以找家长、找老师、找学校帮忙的事情现在必须得自己花精力处理。大到找对象、结婚、买房、下一代、升职加薪、照顾父母,小到租房、做饭、做家务等日常琐事,都变成了一个一个无法逃避的事情。同时,现在朝夕相处的同学们也会有自己的生活,绝大多数时间都需要自己想办法度过。

2月和目前在国外的同学一起逛重庆,在南山上拍的照

站在校园生活的末尾才发现,即将挥手告别的生活可能是最简单、最单纯的生活,即将离开的地方将会是之后永远的回忆。

转折点之后,我们将迎来未知的新生活。新生活下,每个人都是自己生活的第一责任人。

2021年总结

2021年12月31日 23:59

暴风雨之前的宁静

站在2021的年尾回看这一年,发现这一年着实有点波澜不惊。

这一年中没什么非常关键的里程碑事件。没有2018年的各种比赛和活动充实我的简历(以至于现在还在吃当年的老本);没有2019年决定保研,确定了未来三年的生活状态,以及实习,第一次进入梦想中的公司去工作;没有2020年的本科结束,告别至今对我最具意义的四年,以及研究生开学,迎接一段外表看上去光鲜、但是自己却不那么肯定的研究生生活。这一年发生的各种事件看上去都那么的日常,有时候日常地甚至连朋友圈都懒得发。

但是,可以遇见到,2022年将会是一个“腥风血雨”的一年。再次准备实习面试,再次暑期实习,参加秋招,确定出路,每个步骤都是一场不可避免的硬仗。用暴风雨之前的宁静来总结2021年实在是再合适不过了。

本年度的事件

项目和比赛

我一直认为,不被人使用的项目没有意义。所以,即使项目是大家都觉得没啥技术含量的CRUD,只要真的有人用,我仍然会接受并且愿意花心思和时间把它做好。所以上半年除了上课,很大一部分时间投入到了data-competition.pku.edu.cn的开发上了。这个项目之前写过文章详细介绍,我这里就不讲了。

5月份的时候参加中心主办的“新手友好”的CTF比赛,结果就会做娱乐题和web题,其他题一点思路都没有,说到底还是我太垃圾了。最后趁着大佬还在睡午觉没有发力、以及相关技术(JWT)之前开发用过偷了个web题的一血奖,本以为还能蹭个30名万岁,结果比赛DDL当天早上一觉起来被卷王卷下去了,行吧。

CTF

这学期年中的时候参加了学院组织的持续48小时的hackathon比赛,由于是线上,所以差点现场一起卷的感觉。但是瑕不掩瑜,4个人在48小时从0开始卷了个类似大众点评的小程序出来,也算是学了个新技术(虽然小程序其实可以说就是个阉割版和私有版的web)。

hackathon奖状

年末的时候还参加了个比赛,说好10月中旬结束的,结果到现在还没结束,日程表一改又改,也是无语了。

城市旅游/参观

和长距离的旅游相比,我更喜欢城市/城郊的旅游和参观,原因很简单:规划更简单、当日去当日回。加上疫情期间学校对进出京比较严格,以及北京作为首都,还是存在不少值得一逛的地方,所以这一年通过和同学和社区的方式,一起参观了北京城里和城郊的多处公园、博物馆等景点。

  • 公园:圆明园、颐和园、玉渊谭公园、北京动物园、奥林匹克森林公园、798艺术区(可能不算公园……)
  • 博物馆:农业展览馆、天文馆、古动物馆、园林博物馆、电影博物馆、民航博物馆、铁道博物馆、首都博物馆
  • 雁栖湖
  • 阳台山
  • 古北水镇

城里各个点的地图,不包含郊区的景点

可以发现大多数景点都较为冷门。有的景点的门票实在是约不上有空的时间,如故宫等;但是更重要的是我不太喜欢人太多的景点。人太多的景点,看到的人比看到的景还多,那还有什么意义?所以这些景点大多数我都尽量选择工作日去参观的,避开北京汹涌的人流。这也是作为学生的福利的了吧,以后工作了之后,想避开高峰都做不到了。

这些景点总结一下,

  • 各个博物馆虽然离城里近的人多、人少的太远,但是质量普遍较高,例如电影博物馆的20多个展厅个个内容充实,强烈推荐;
  • 雁栖湖太远而且风景不如南京玄武湖(玄武湖yyds),票价较为,而且周边除了国科大啥都没有,吃饭的选择都比较少,劝退;
  • 公园较为普通,适合散步聊天,动物园假期人流量太大(我是五一去的),建议避开高峰;
  • 古北水镇很偏(都快到河北了),但是开发得还可以,而且人比较少,参观体验良好并且市郊铁路12块钱直达,附近酒店、民宿也比较便宜,附近还有长城景点,适合周末放松;
  • 阳台山是跟随社团去的,总体体验参考下图:

活动结束发的说说

运动和减肥

这一年可能唯一能称为标志性的事件就是我的体重人生中第一次降低到了标准值(BMI<24)。当前最低下过150斤(149.5,下图,是前一天晚上运动后第二天早上吃饭之前的结果,不具普遍意义),平时一般稳定在155左右。

当前最低体重

自从我记事开始我都是在超重或者肥胖的状态,且深受大体重的困扰,比如体力不行,运动成绩特别差,中考体育几乎全员满分的情况下只得了41/50分等等问题,没有得心理问题已经是万幸了。大学第一学期在一学期没有出过校、天天吃食堂的情况下降了30斤之后,之后的情况虽然比高中要好多了,但是仍然是在超重状态,且在正负20斤的范围内徘徊。直到研究生第一学年结束后,体重仍然离标准体重差了十几斤。终于有一天,我的体重终于降下来了!

其实根据这5年的经验,我觉得减重最重要的还是饮食,运动用处不大。

拿大一上学期和大三下学期举例:这两个学期,我的运动量基本都是一周2-4小时羽毛球的水平,但是大一上学期我每天吃20块的三餐,午饭晚饭基本就是一份鸡肉(因为鸡肉便宜,而且除了肉没很多骨头之类的其他配料)、一份青菜和2两米饭,完全没有出去吃过(那一学期就没有出过学校),除了有时候吃点水果,完全没有零食和奶茶等三餐之外的东西。而大三大四时对吃基本什么限制了,有时候晚上2两饺子+鸭削粉丝汤,中午30块的香锅,每周还出去吃2、3顿。这样的最终结果,就是大一上学期220斤起点少了30斤、大三180斤左右的起点最后甚至好像还重了5、6斤的样子。

研究生期间的情况其实也差不多。到目前这三个学期,第一学期2-4个小时羽毛球,第二学期没有羽毛球,平时基本没有其他运动,但是5、6月保持两三天一次<6配速的5km,第三学期4-6小时羽毛球,吃的情况差不太多,但是体重变化方面,前两学期每学期-10斤左右,第三个学期下降得比较明显,快20斤了,但是我觉得还是因为吃得少了:这学期因为懒得去学校,所以早上基本每天一碗纯麦片,中午吃万柳的比校内食堂种类和量少太多、但是价格贵不少的食,这样摄入得少,体重也就下来了。

跑步

现在既然体重已经回归正常体重了,所以我现在也没有继续控制体重了,想吃啥吃啥,有时候甚至还吃点零食,所以最近体重没有继续往下走了。啊,不胖的感觉真好。

技术、工作、职业发展

浪费掉的暑假

7月,因为自己懒,在实习和选择了回家过最后一个暑假这两个选择中,选择了后者,并给自己找了个借口:继续写yaarxiv([GitHub](https://github.com/ddadaal/yaarxiv)这个项目。

commit记录

这是我今年最后悔的一个选择。

这个项目的主要功能2020年9月就已经完工,可是这一年多时间了,一点推进的迹象都没有。上个学期整整三个月,这个项目就发生了这三个变化:加了个logo,注册了个域名,上了个HTTPS,没了。

我本应该从上个学年这个项目的情况推测出这个项目完全没有前途,结果仍然把宝贵的暑假时间投入到这个巨坑中,结果在开学之后发现认识的同学个个要么实习、要么学生工作、要么论文在投,各种方向做得有声有色(至少能往简历上写),感到十分后悔。后悔归后悔,想着赶紧推动另一个实验室项目,结果在整整一个月的时间内项目(又)一点的动静都没有,连个会都拉不起来,连个需求都理不出来,焦虑感直接拉满。那段时间找各种事情来做,做leetcode、做tidb的实验、各种看新技术,但是由于是焦虑驱动的,根本无法坚持下来(leetcode刷了几天没刷了),而且碰到难题无法静下来debug(比如tidb实验的一个bug到现在都没de出来),所以那段时间也可以说是一无所获。

为了缓解焦虑而做leetcode,无法坚持下来

直到项目终于开始之后,我像是抓到了一个救命稻草一样,把我研究生能否有任何值得一提的成果全部压到那个项目上去了,所以接下来几个月基本全身心投入到了那个项目的开发上去。做了2个月之后,项目终于初步可用并内部上线试用了。但是现在想起来,那个项目只是个业务逻辑稍微复杂、涉及外部系统有点多的CRUD,仍然只是个玩具,仍然不具有什么含金量,并不能拯救我的简历,焦虑如闪电般归来。

工作和职业发展

明年就要找工作了,上半年暑期实习,下半年秋招。最近刷校内的BBS,总能看到很多同学的求帮忙比较offer的帖子,现在大家一般都不会只看薪资,普遍更重视职业发展、稳定性等因素,这也造成选择一个比一个难。而我因为不愿意做行政工作,也不愿意加班,完全没有做过科研,选择面瞬间少了很多(比如本校同学热衷的选调对我来说就完全不是一个选项)而且我很讨厌(怕)面试,理想情况是面个1、2次、拿到不那么差的offer就停止了,所以从某种角度来说可能明年也不会那么纠结(当然这种拿着多个offer的纠结和拿着1、2较差的offer是两种纠结)。

在职业发展上,纯业务的发展路径是不可持续的:业务是可以复制的,而且目前互联网的发展明显出现了瓶颈,各种“优化”层出不穷就是一个证据。另外一条路是走纯技术路线,掌握并精通业界的实用技术,如分布式、数据库、UI等,虽然这才是做“技术人”的正途,但是其难度较大,门槛较高(比如应用规模必须大到一定程度才会遇到分布式的需求和对应的问题,但是做实验室的玩具项目基本就完全用不到这些问题;比如只有面向关注UI的用户才会对UI提出要求,才能倒逼去学习和实践CRUD之外的UI技术,但是一般内部的业务都不重视UI,能用就行了)。这种需求的培养的脱节已经被受到很多批评了,但是现实就是这样:工程确实不是学校的重点。

我之前的技术栈都在web上,但是受限于实验项目的需求和人力(毕竟只有一个人),而且我有种“用不上的东西就学不进去”的坏习惯,所以无法通过项目学习公司想要的高的技术难度上,而需要写没什么简单、但是量大的业务代码(写这些成果在找工作的时候基本没有用,面试官只会觉得没什么难度,大家都能做。其实到目前为止我手里的项目、比赛在找工作的时候基本没什么含金量)而如果换方向,因为我已经在这个方向上投入了这么多,现在距离找工作也只有几个月的时间了,贸然换方向存在极大的风险:很容易最后变成两边都是半吊子,两边的工作都找不到的问题。其实,我现在掌握的技能已经太过分散,什么都会一点=什么都不会,如果不背面经,连正经大厂实习都不好找。

除了开发,我仍然参与搭建和维护实验室的一些基础设施。我也认同管理、基础设施等看似和技术无关的东西对于软件项目同等重要。但是同样是因为当前手里的项目难度和规模上不去,这些管理和运维的经验在业界看来就是个笑话,还不如不写,省得到时候面试官想,就这?

说到底,这几年所做的工作基本对找工作没什么帮助。所以最后找工作,还是只能像本科生一样,靠算法题和八股文。算法题方面,由于我算法水平很差,所以在11月开始坚持每天一道LeetCode的每日一题,到现在能不能做出来另说,起码看到题不会慌了,也是一个好的进步吧。而八股文方面,还是在这个假期好好静下心来背,就当是一个更大的、更重要的期末考试了。

LeetCode

日常学习生活状态

一个正常的学习路径是入门的时候先学习掌握基础、个人技能,之后通过项目学习多人合作以处理更复杂的问题,可是我到现在的路径是反过来的:本科特别推崇多人合作,基本所有项目都是多人合作完成的;结果上了研究生之后项目却都变成个人做了,甚至实验室都变成了甲乙方关系,实验室只管提需求,我把需求分析、设计、实现、测试、部署、运维等事情全部搞定,甚至还得去push甲方,否则东西做完了,又没人用,重蹈覆辙。

在这个学期之前,我其实一直在找能够一起做项目的同伴,最好还有大佬可以抱,还不用自己动脑想做什么了。但是现在已经完全放弃了,或许一个人做也挺好的,毕竟每个人都有自己的想法,不能强求,能依赖的都只有自己。换个角度看,读研和工作也没什么区别,研究生三年可真就完全变成了用三年经验和工资的差价换一个研究生学历了。

由于就是一个人做项目,加上课在研一都上完了,所以我的日常生活也基本变成了“一个人的狂欢”。白天在宿舍/实验室一个人写项目,有问题去微信上问问甲方,饭点到了吃饭,中午到了午睡,正常情况下白天一句话都不用说。实验室也能不去尽量不去:宿舍又方便又暖和(或者凉快,取决于拒绝),平时又没有人非常自由,不去实验室还可以避开早晚通勤高峰,宿舍还有配置比实验室高的电脑,因此除了老师要我去实验室,以及宿舍区的食堂实在太难吃了,有时候还得去学校改善下伙食之外,实验室基本都看不到我。每天最开心的时候还是晚上有时和新认识的其他同学打打球,晚上11点后舍友回来了和室友聊天的时候。

虽然最近几个月的生活似乎就是copy-paste,几乎没有什么变化,但是这种确定性还是挺让人心安的。要是以后我的生活也能够像这样,每天稳定地做着自己喜欢的事情,我也已经很满足了。

又是一个转折点

有时候我在想,要是两年前我选择了工作,现在是怎样的呢?现在是股票翻倍,升职加薪,还是被优化了,现在正忙着各种找下家呢?有没有后悔当时没有保研呢?要是当时加入了AI的浪潮,现在是在读博做学术,还是也是在考虑找工业界的工作呢?现在是在拿着几篇论文意气风发呢,还是idea想不出来、实验卡住了,和现在一样、甚至比现在更焦虑呢?在做出某个选择的时候没感觉那么重要,可只有回头看的时候才知道人生的轨迹被改变了。这也说明,没有最好的出路,只有适合自己的才是好的。希望明年结束的时候,能够找到一个满意的出路吧。

借助Docker,把VPN当作HTTP代理来用

2021年2月8日 10:13

需求

虽然放假了,但是学校还有活要做,而且学校的活的代码仓库在学校内建的内网GitLab上,在外网需要VPN才能访问。但是,大家都知道连接VPN会让整个系统里的所有流量都走VPN,但是很多流量其实是不需要走VPN的,比如聊天、看视频网站等,VPN是有带宽限制的,把所有流量都走VPN会使得不需要走VPN的网络请求变慢。

VPN方案下网络流量走向

那么,有没有办法可以让需要访问内网资源的程序走VPN,不需要访问的程序不走VPN呢?

代理

代理服务器的基本行为就是接收客户端发送的请求后转发给其他服务器。代理不改变请求URI,並不会直接发送给前方持有资源的目标服务器。(维基百科

简单的理解,代理服务器(proxy server)就是一个程序,它会将它收到的流量转发到其他服务器上。这个服务器有可能就是真正的目标服务器,也有可能是其他代理服务器。

当前大部分程序都支持设置代理服务器。如果一个程序设置了代理服务器,那么它发送的流量将不会直接发送到真正的目标服务器上的,而是发送到代理服务器,由代理服务器进行处理后发送到目标服务器上。

代理服务器下网络流量走向

如果有在内网的代理服务器……

如果我们有一台在内网的服务器,并且这个服务器可以从公网被访问,那么事情就很简单了:

我们将在这台服务器上开一个代理程序,这个代理程序就是简单地把收到的请求再转发到真正的目标服务器上。 然后在我们本机上,把需要内网资源的程序的代理服务器设置为这个服务器的地址。

这样设置之后,我们本地的需要内网资源的程序的流量将会被发送到这个内网的服务器上,内网服务器收到了,将会把请求转发到内网的服务器上。这样,我们的程序就可以访问内网的资源了。

内网代理服务器方案下的网络流量走向

最终方案:把连接到VPN的服务器作为代理

那我们没有一台在内网的、可以被公网所访问的服务器怎么办呢?

这时,VPN的作用就出来了:连接了VPN的电脑的所有流量都会转发到内网去,那么这台电脑就可以被看作一个在内网的服务器。

我们可以在本机上起一个虚拟机或者docker容器,使这台虚拟机或者容器连接到VPN。同时,在这个虚拟机或者容器上起一个代理程序,工作就是简单地把收到的请求转发到真正的目标服务器上。

我们将需要走内网的程序的代理设置为这个虚拟机或容器,那么需要走内网的程序的流量将会首先发送到虚拟机或者容器。由于这个虚拟机或容器连接了VPN,那么由这个服务器发送的请求就会走隧道到内网,能够访问到我们需要的内网的服务。而没有设置这个虚拟机或容器为代理服务器的程序的流量将会不经过内网,直接连接到互联网服务。这样,我们的问题就解决了!

VPN作为HTTP代理服务器的最终方案

设置代理

最后一步,如何给一个程序设置代理呢?

这个需要根据程序而定,一般是如下的策略:

  • 有的程序自己有设置代理的设置,需要看程序的帮助界面
    • gitnpm
  • GUI程序一般会跟随系统代理设置
    • 去系统的设置菜单里寻找
  • 一般命令行程序会使用HTTP_PROXYHTTPS_PROXYhttp_proxyhttps_proxy环境变量的值作为代理服务器

在我们的原始需求中,我们使用的git。所以,我们可以在仓库中使用git config http.proxy 代理地址git config https.proxy 代理地址来设置,使得这个仓库的pull和push操作都使用代理地址对应的代理服务器。我们只在仓库层面进行设置,只有这个仓库的操作会走这个代理服务器,其他仓库不会使用这个代理服务器。

git使用代理访问内网仓库

借助Docker的实现

根据这个原理,我编写了一套使用Docker容器来实现这个解决方案的脚本,可以在经过简单的设置之后,使用一条docker-compose up命令就可以简单地启动这样一个代理服务器,方便使用。由于使用了docker,所以Windows/*nix等支持docker的操作系统全部支持。

仓库地址为:https://github.com/ddadaal/vpn-as-http-proxy

感兴趣的同学可以进入仓库参考一下使用方法和实现原理。使用方法写在README文件中了。由于每个学校和组织的VPN地址、参数等都不相同,我也鼓励大家尝试找到连接自己学校的VPN的命令,并将命令模板通过PR方式贡献给本仓库,这样使得更多的同学能够更简单地使用这个解决方案。

2020年总结

2020年12月31日 15:10

2020年的四个阶段

对我来说,2020年可以分为四个阶段:

  • 与毕设一起在家度过的上半年
  • 在南大的最后半个月
  • 最后的无忧无虑的暑假
  • 研究生的第一个学期

其中前三个阶段都在我的上一篇文章:美好的回忆和未知的未来:写在研究生开学前中这篇文章中做了总结,所以这里就#include一下,前三个部分的内容可以参考上一篇文章。

本篇总结一下这三个月研究生生活,记录一下这一年里一些值得纪念的生活小事,并用一些对本年的经历的一些思考来结尾,以此来告别这个魔幻的2020年。

三个月研究生生活总结

课程

一般提到研究生课程这东西,大家都会习惯性忽略,说:研一主要就上课就行了。但是呢,研一这研究生课程这东西嘛,它虽然不重要,但是也挺闹心的。

研究生课程确实不重要,体现在成绩的高低上:

毕业只要及格,连评奖评优都只需要及格,只要及格之后,成绩项的得分就是75分的定值。而且老师也一般都不会太严格,一般的课一年挂的人也是1个或者2个。一句名言:

研究生课程最好的GPA是及格GPA。

但是它又很重要,这体现在能否及格上:

没有补考,只有重修,而且挂两三门直接退学?另外我导师说,他的第一个研究生就是因为课程考不及格而退学的。

退学?

更麻烦的是,这里的课程和本科的课完全不一样:之前总结了一下,本科的课一般只需要做两件事:背书做大作业。背书这个就不说了,大家都背,考试的题目和背的题目基本也大差不差;而本科的大作业基本都是我所喜欢和擅长的,因此我本科的成绩才能比较高。

而到这里就不一样了:信科是重理论重科研的,所以它的课也是重理论重科研的。

所以软件测试课上完基础知识,接下来是现场想论文框架?30分钟完成思考问题,提出解决方案,用实例验证,提出解决方案的不足之处的全过程,还要做PPT?上课这一点点时间开脑洞出来的方法我还真不信能用……我还以为是真的拿几个软件制品来真正测试一下呢。

所以一门叫做“海量图数据的管理和挖掘"的课程,一次三个小时的课讲20多篇论文的算法/方法,也不管大家能不能听懂?下图是某一次课上课涉及的内容的来源论文,前面一页PPT还有10个。

某一次课内容的来源

还有一门课,数据库原理与技术,说实话上课质量确实比较高,内容很充实,讲的很清晰,但是无平时作业,考核是通过读论文和最后的纸面考试实现的,而且考试的难度有点过分,是不可能背出来的……其实相比起来问题也不大,但是我觉得这种课至少应该写点代码吧……

我选择的最后一门课高级编译技术,是在UT Austin的CS 380C上进行的简化,去掉了不少内容(比如寄存器分配等)。虽然这课难度比较高,而且又有平时作业、又有大作业、又有考试,但是由于它的内容是经典的、成体系的,上课的节奏比较可以接受,可以说是我这学期最喜欢的课程之一了。但是一开学就布置的三个大作业我一直在上周(12月中)才知道,难度也是相当大(123),时间也很紧张,还是相当难顶的。截至发稿,三个作业也就做了1.05个(按得分记),而且已经遇到了很困难的、还没想到方法解决的问题……

这些课上课的内容让我觉得我本科根本不是学计算机的(好吧,软工的确不是计算机)。整个研一期间,除了上课和写作业,基本没做成什么其他事情,最近期末这段时间疯狂赶DDL,啥都不想干了。说好的研究生期间是给老师打工呢?现在看来,我倒是希望给老师打工了,至少打工的时候的工作能给我带来一些成就感和满足感。

DDL

学工

在上一篇文章中我提到研究生期间的人际关系将会变窄,所以在开学的时候我去申请了信科研会的职位。当时我已经意识到研一可能会有很多工作,所以在投志愿的时候选择了看起来较为轻松的部门,职位由于只能选择部长和副部长,所以我选择了副部长,准备尝试去划水和了解一学年。

其实这时候我已经觉得有点奇怪了:为什么招新会直接招部长级别呢?新生什么都不懂,直接负责一个部门的事务是不是风险有点大?而且更奇怪的是,面试的时候我当面强调不要部长,后面却直接给了我部长职位;更主要的是之后发现部门的工作确实不是我喜欢的(而且甚至有点不太乐意干的),而且感觉课程压力变大了,所以没过一个月,就找了接盘侠,把研会退了……

说到底,其实还是自己的原因,一开始没有仔细了解部门的工作,过于冲动了,所以造成后面的一些不方便的事情,现在见到之后的研会的同学还是有点小尴尬。

但是研会的机会让我认识了一位上一届的研会的部长,他现在在负责北大的微软学生俱乐部。南大微俱的三年让我印象深刻(2016至2019,和南京大学微软学生俱乐部一起成长)。在开学、申研会之前,我就想继续在北大微俱继续做事,但是在百团大战的时候我并没有发现北大微俱,以为北大微俱已经凉了,本来还有一点小遗憾。所以当时他问我要不要加入微俱的时候,我想都没想直接同意了。

之后发现,北大微俱去年断档了,甚至已经被取消注册了,今年的北大俱乐部基本可以说是“从头再来”。企业俱乐部本来就很难办,面向研究生的更难办,所以我本科时的“面向新生”的策略基本不能应用在年级更高的同学身上。所以其实在之后,北大微俱也就组织了一次去MSRA的参观活动,其他活动似乎很难办。

北大微俱组织的MSRA参访活动

所以其实说到底这一学期我根本没有任何学工活动。虽然没有学生工作让我平时少了很多事情,轻松了很多(可以把更多时间放在肝课程作业上……),但是这使得我失去了最好的认识其他朋友的机会,所以我现在人际关系其实是比较成问题的。

人际关系

人际关系方面,基本和我在上一篇文章中说的一样:

认识的人:当然,研究生和工作期间也会认识新同学新朋友,但是由于参加的活动将会减少,社交面会急剧变窄;而且,研究生期间的同学和以后的同事都是成年人了,都懂了自己想要什么,都有了自己的安排,都有了自己的人脉圈甚至家庭,不能像本科时的兄弟们一样时时刻刻泡在一起。

唯一值得庆幸的是,我和室友以及实验室同年级同学的关系还可以,和清华的本科同学关系更好了,还加了一个打羽毛球的小圈子,但是也就仅此而已了:这些就是我当前认识的所有圈子。而且,大家都知道自己想要什么,都在朝着自己的方向去做。所以除了约饭、上课以及偶尔出去看个电影之外,也找不到还能一起做什么,大家都很忙。

唉,每次想找人却找不到的人时候,就想念本科的兄弟萌,十一的时候和兄弟萌短暂的几天相聚,也当是重温我的逝去的美好青春了。

十一回南大玩

大环境是个更大的问题,简单来说,和其他同学像是两个世界的人

就像之前说的,信科主要注重科研,所以周围的同学也基本都是科研方向(或者选调方向),而我仍然想做工程。我的能力、兴趣、方向和他们不说是冲突的,至少是正交的。所以不像本科时大家方向都大致相同或者互补,现在我完全找不到能一起做事的人。这几个月实验室有两个工程项目,都只能我一个人做;想参加一些项目相关的比赛,找不到愿意参加的同学;好不容易有个课程有个组队的工程项目,一个简单的社交网站平台,结果身边的同学都完全没做过稍微正经一点的工程开发工作(比如项目或者工程任务的实习)……

核心问题并不是技能不匹配,有兴趣的话,工程相关的知识其实比科研所需要和使用的知识简单多了,但是问题是我和其他人互相对对方的工作没有兴趣:我没兴趣去想idea做实验,其他人没有兴趣做工程。我觉得从工作中认识同伴是最好的、最自然的认识的人的方式,而兴趣的不匹配加上上面说的学工的情况,从工作中认识同伴这条路基本已经堵死了。

其他生活中的小事

入音乐剧坑

这半年去现场看了一次《第一次约会》中文版(豆瓣),在去看之前专门先看了一下百老汇原版,总的评价是原版质量中规中矩,是一个很典型的爱情轻喜剧。中文版和原版相比,除了把台词和歌曲翻译成汉语、把一些梗改成了中国人更能理解的梗(比如男女主犹太教vs基督教改成了本地人和外地人),其他的(剧情、舞台设计、舞蹈等)基本没有变化,个人觉得这种翻译还是有点生硬的感觉,可能是因为先看了原版先入为主的缘故吧。

但是看剧体验并不佳,主要原因是世纪剧场的音效太垃圾了,二楼靠前的座位只能勉强听清台词,歌词就完全无法听清了,而且由于是中文剧所以没有字幕,所以这很严重地影响了对剧情的理解。比如犹太教和基督教的“冲突”点的歌《The Girl for You》在原剧里相当搞笑,但是在这次表演中由于音效的原因,除了知道剧情改成了外地人以外,改的词完全没听清楚在讲什么,只能看到舞台上热火朝天的舞蹈,感觉怪尴尬的。其他歌也基本是同样的问题,我看过原剧所以理解每首歌主要讲的内容,但是没看过的观众就比较尴尬了。我的票也是中档价格的票了,但是体验仍然这么差,只能给剧院差评了。希望明年去的剧院能好好考虑一下音效效果,连歌都听不清,音乐剧的效果也就大打折扣了。

剧票

本年度还看了不少电影和音乐剧,列表如下图。

本年度的电影和音乐剧列表

今年开始可以说正式踏入了音乐剧大坑,通过万能的B站看了20多部音乐剧录像。在《剧院魅影》、《猫》等经典之外,还有很多音乐剧通过歌声和台词精彩地讲授了一个或温暖、或悲伤、或欢乐、或引人思考的故事:

  • 虽然是BE(Bad Ending)、但是感觉很温暖、帮助找回童心的《寻找梦幻岛》(Finding Neverland)
  • 标题特别欢乐,但是实际上是个悲剧,而且还是倒叙使得悲情更加严重的《Merrily We Roll Along》
  • 即使是个相当通俗的商业喜剧,但是加入了劝学和励志元素、价值观还挺正的《Legally Blonde》
  • 聚焦社交恐惧症人群、但是实际上可能戳中了每个人都可能遇到的困境:孤独的《致埃文·汉森》(Dear Evan Hansen)

当然,这其中也有一些虽然称作经典,但是剧情和价值观都值得吐槽的剧(如《西贡小姐》(Miss Saigon)),很多剧也只是讲解一个简单的故事,也没什么更深入的含义。但是总体来说,音乐剧确实带来了电影不同的全新的体验,帮我解锁了一个电影以外的更大的世界,当然也解锁了更多的歌曲专辑。

由于疫情的原因,这几年可能都很难能够在国内看到原版的国外音乐剧(希望明年的《剧院魅影》能正常上演),还是稍显遗憾的,但是一些中文版的剧评价也相当不错,也值得专程去剧院欣赏。希望疫情能够早日结束,也希望国产剧能够越来越好。

希望能正常上演

成都之行

10月底和一名同学参加了对方一个看起来并不太想参加的比赛,所以有机会从学校生活中喘喘气,去成都玩了5天。最大的感受是和北京比,成都的物价是真的便宜,每顿都在外面吃,最后人均总共才200多一点:在北京在外面吃我就没吃过一顿人均低于100的。

成都之行

装机

从双十一开始买电脑配件,12月12日终于抢到了电脑的最后一个组件5900X,历时一个多月,终于装好了我的第一台自己装的电脑。配置如下:

组件型号价格渠道
CPUAMD Ryzen R9 5900X4099JD
CPU散热器雅浚ProArtist Gratify5 G5249JD
主板华硕 TUF GAMING B550M-PLUS WIFI749JD
内存G.SKILL Trident Z 3200 16G *2999JD
主SSD三星 PM9A1 512G PCIe 4 NVMe866淘宝
副SSD海康威视 C2000PRO 1T PCIe 3.0 NVMe749天猫
显卡映众 GeForce RTX 3070 冰龙版4199JD
机箱迎广 301 黑色459JD
电源长城 G7 750W 金牌全模组569JD
机箱风扇酷冷至尊 漩涡120 ARGB *5 + RGB和风扇集线器435JD
总价13373

从价格来说,其实这套配置的价格已经算比较高的了,如果要讲究性价比的话,有很多地方可以缩配,缩到12000以下应该是比较简单的,比如:

  • 买丐版3070,-300
  • 缩SSD,-400
  • 去掉RGB,-500
  • 缩点电源,650W应该也够用,-100
  • 机箱也可以缩缩,-200

而且如果只是臭打游戏的话,5900X也很overkill,5800X应该能够非常好的满足需求,不需要加这上千块强上5900X(更别提5900X现在还在耍猴了……),这样甚至可以压缩到10000以内。再减1000换个什么3060Ti,噫,性价比爆表。

另外,本套是个mATX配置,机箱空间较小,操作并不如标准ATX方便,对我这种第一次独自装机的不太友好,所以花了整整两天才完成,还拆掉了一些不该拆掉的螺丝,造成现在侧面盖板无法完全合上,而且完全没有理线,所以现在看上去很乱……另外,其实宿舍空间相当充足,mATX既没有ATX的扩展性,也没有ITX的便携性,有点小亏,下次装机还是要么ATX要么ITX了,mATX在中间不伦不类的,两头不占好。

PC

不管怎么说,这台电脑最难得的是在这个耍猴的年代,CPU和显卡都是从京东原价抢到的,其他配件也是近期最低价,配置也是相当满意,5900X太强了,用了几天后用笔记本感到明显的反应迟钝,而且,24个框框看上去太爽了!

原价买到的5900X和3070

24个框框!!!!

总结

总的来说,研究生生活方面和我之前考虑的差别不大,除了课程的难度让我有点措手不及以外,其他方面基本大差不差。而信科从任何角度来说(包括但不限于课程设计、院系发展的方向、同学的基本情况),确实不适合我。可能这也是当时做选择的时候考虑不周到吧:只考虑了学校,没有考虑专业的匹配性。本院的学长学姐对清华和上交的偏爱确实也有他们的理由。人际关系方面比较受限,更别提其他方面了。而日常生活方面也是比较平淡,但是一些日常的简单的活动(如打球、看电影等)也可以暂时从现实生活中稍微找到一点乐趣。

对于选校,现在说后悔也没用了,幸运的是导师和实验室的氛围非常好,可能会涉及的工作可能也是北大所有实验室里我最适合的了。至于让人头秃的上课,顺利的话,只有研一会上课,之后都是以实验室为主。希望以后能够不这么挣扎,能够找回自己当初选这里的初心,真正写点实用的、能为他人带来帮助的代码。

另外,我发现,不管是日常生活中还是写代码的过程中,我似乎很久没有再找到大一大二时的激情了,那种可以为了一个问题可以花几天时间啥都不想就想深追到底的激情,那种电影《心灵奇旅》中的忘我的感受。我仍然记得软工2大作业中途有3天我满脑子都只有一个CI的问题,除了吃饭上课就是在电脑前尝试解决这个问题,其他什么问题都不担心。而现在不管做什么事,总是会担心各种各样的事情,担心DDL,担心考试,担心以后的工作,担心35岁被辞退,担心会一个人去面对未知的生活,一个人去感受生活中的乐趣而无人一起分享这份快乐,一个人去接受来自生活的挑战,而在遇到挫折时最多只能自己在床上大哭一场然后下床继续装作一个坚强的人。这是成为社会人的必修课吗?

使用X11 Forwarding在WSL 2中运行GUI程序

2020年6月4日 16:15

WSL 2使用GUI

众所周知,WSL 2开始使用真正的Linux内核,所以理论上来说,我们已经可以在WSL 2运行几乎所有的Linux程序,包括带有GUI的Linux程序。但是目前来说,由于第一方X11 Server的缺失,WSL 2主要还是在命令行中使用。

Build 2020上,微软已经宣称开始做WSL 2的GUI支持,并已经给出了宣传图。

Build 2020上WSL 2的GUI支持图片(来源:https://devblogs.microsoft.com/commandline/the-windows-subsystem-for-linux-build-2020-summary/#wsl-gui)

但是,事实上,借助X11 Forwarding,我们现在已经可以做到类似的效果了。

如果你之前折腾过在WSL上跑GNOME等各种桌面环境,则这篇文章的原理和那些文章是一致的,但是由于不需要启动桌面环境,其使用更加方便,各种Linux程序窗口和Windows的集成更加无缝,个人认为实用性比启动一个Linux桌面环境再在里面启动程序更强。

下图为跑在WSL 2上的IDEA、通过IDEA启动的JavaFX程序与Windows计算器共存的截图,注意看到状态栏上的窗口图标的IDEA图标,完美与Windows进行兼容。

效果图

X11 Forwarding

X Window是目前Linux上使用得最为广泛的窗口系统。虽然它有各种各样的问题(包括不支持多个显示器不同DPI的硬伤),我在使用Linux桌面的时候也经常推荐Wayland作为代替,但是其X11 Forwarding的特性是相当的有用。

下图是X11的架构。

X11架构(来源:https://en.wikipedia.org/wiki/X_Window_System_protocols_and_architecture)

在X11系统中,显示GUI的程序(Client,包括各种浏览器、IDE等的程序)和用于真正负责显示、以及捕捉键盘、鼠标等输入的服务器(Server)是分离的,且二者之间通过网络通信的。

对于普通的Linux系统,其Server和Client是跑在一台机器上的。但是,由于二者之间是通过网络进行通信的,所以X11窗口系统不需要Server和Client运行在同一台机器上。

所以,我们可以在其他电脑(甚至不需要是Linux)上跑一个X Server,然后通过配置DISPLAY环境变量,让X Client和位于网络上的X Server相连,就能让在一台电脑上运行的程序的GUI显示在另一台电脑上。

这就是X11 Forwarding,将一台电脑的程序的GUI Forward到另一台电脑上去显示。

配置

X11 Forwarding的原理很简单,配置其实也很简单。其分为两个大部分:Windows端(Server端)和WSL端(Client端)。

Windows端

在Windows端需要安装一个X Server。

X Server有多个,我比较建议vcxsrv(名字记忆:vc x server),因为其功能和操作比较简单。

可以从上面给的github链接进行安装,也可以通过scoop进行安装,scoop安装命令如下:

scoop install vcxsrv

安装成功后,在开始菜单或者其他启动器启动新安装的XLaunch,会弹出一个向导以配置X Server的属性。在一步选择Multiple windows,第三步选择Disable access control,其他不变。

第一步:选择Multiple windows

第二步:什么都不选

第三步:勾选Disable access control

第四步:确定以启动

最后在状态栏可以看到一个新的图标,鼠标悬浮上去可以看到其地址,其格式是{电脑的hostname}:{地址},记住后面这个地址。例如说,下图中,其地址为0.0

状态栏图标和地址

WSL端

WSL端需要做两个工作:

  • 设置DISPLAY变量
  • 配置字体等GUI相关的设置

设置DISPLAY

DISPLAY环境变量用来指定X Server的地址。由于WSL 2使用虚拟机,其IP和Windows不同,所以需要进行一些特殊方法来访问Windows上的X Server。

在.bashrc或者.zshrc里加入以下代码,然后重新进入WSL。

# 若安装了Docker for Windows,且启动了WSL 2后端
export WINDOWS_HOST="host.docker.internal"
 
# 若没有安装Docker for Windows,则可以从/etc/resolv.conf中读取Windows的IP
# 这个IP有可能会变,所以不能直接一劳永逸。
# export WINDOWS_HOST=$(grep nameserver /etc/resolv.conf | awk '{print $2}')
 
# 可以尝试使用Windows的hostname,未尝试过
# 但是我在Hyper-V虚拟机中使用hostname访问Windows有时候会遇到奇怪的卡住的问题,不推荐
# export WINDOWS_HOST={你的Windows的hostname}
 
# 下面的"地址"替换为之前记住的Windows上的X Server的地址,一般(以及上面的例子)是0:0
export DISPLAY="$WINDOWS_HOSRT:地址"

配置字体

由于WSL的distro一般都比较简单,可能没有安装字体以及对应的配置,所以需要自己手动安装字体。

这里以noto-sans-cjk举例,你可以自己安装自己想要的字体。

字体安装后不能直接启动GUI程序,现在直接启动会遇到奇怪的报错问题(忘记截图了,可以自己试试)。

可以安装一个简单的X程序来初始化,并验证X11环境是否已经配置成功:如xclock:

sudo pacman -S xorg-xclock

使用xclock测试是否能够正常使用

安装之后,确认DISPLAY变量已经设置后,输入

xclock

若弹出一个时钟窗口,则配置成功。

xclock

效果

现在你可以开始在命令行里启动各种程序了:

Intellij IDEA在WSL 2上启动IDEA比Windows上快多了,我正在考虑将整个Java环境迁移到WSL 2中。

Intellij IDEA

VSCode也可以以Linux窗口来启动,其启动速度也比Windows快多了:

VSCode

在设置中设置以下后VSCode可以隐藏标题栏,但是这样之后无法拖动窗口,最大化窗口后也无法还原,所以不建议。

{
  "window.titleBarStyle": "custom"
}

VSCode隐藏标题栏

一些其他细节:

  • 这些X Window的剪贴板和Windows剪贴板也是同步的,可以自由地双向复制粘贴
  • vcxsrv适配了高DPI,在高分屏上使用完全没有问题

限制

这样的解决方案虽然看上去很完美,但是有一些限制,但是可用性也非常高了:

  • 无法隐藏丑陋的标题栏
  • 无法直接调用Windows的输入法,需要在Linux下重新配置输入法
  • 有可能仍然遇到字体问题(如在现在的情况下编译JavaFX程序),安装完一整套字体、正确设置/etc/locale.gen之后即可修复。

后续启动

以后重启电脑后,只需要重新启动XLauncher,然后在WSL中启动GUI程序即可使用。

延伸和总结

这套配置也不仅限WSL,任何虚拟机和远端电脑上的Linux应用程序均可以使用SSH和X11 Forwarding进行连接。

使用SSH和X11 Forwarding可以解决很多远程桌面相关的问题。

我们很多时候使用虚拟机安装Linux系统,其实并不是要使用Linux的桌面环境,而是使用Linux的命令行和一些GUI程序。

对于命令行程序,使用SSH完全可以解决;

而对于GUI程序,我们也可以通过这篇文章介绍的X11 Forwarding来解决。

和正常的连接到Linux再进行操作相比,SSH和X11 Forwarding的优势其实很明显:

  • 避免进入Linux的GUI环境,而可以在我们熟悉的Windows/macOS环境下使用Linux的各种功能
  • 避免配置Virtualbox Guest软件、Hyper-V的Enhanced Session等虚拟机的增强软件
  • 由于避开了图形界面的模拟,SSH和X11 Forwarding也通常有更好的性能

对于WSL来说,目前的解决方案仍然有一些限制,但是我们可以发现在Build 2020上的效果图已经没有的标题栏,说明微软也正在解决一些X11和Windows集成的问题。相信当微软的X11集成正式发布时,我们在Windows上使用Linux程序的体验,可能比在Linux上使用Linux程序更好了。

2019年春微软实习面试经验

2019年4月21日 10:30

介绍

经过几个月的准备、投简历、面试和(最让人难受的)等待,终于在4月16日拿到了微软苏州STCA SE的实习Offer。所以在这里记录一下这几个月来所经历过的实习相关的事情,并想发表一下关于实习、关于未来计划的一些思考。

面试前

面试前最重要的当然是刷题选择职位投递简历。在这次夏季实习之前的2018年底和2019年初,我还经历了一些和微软相关的面试经历。而且,这次夏季实习生的投递简历的过程也是一波三折,在上海和苏州的职位中纠结,最终还是选择了苏州。下图为投递过但最后撤回的职位。

申请列表

2018年底:微软冬季实习

2018年10月份的时候看到了冬季实习的消息(当时的微信推送),感觉非常激动,于是很快就把简历投了上去,选择的职位也是微软苏州STCA的SE Intern。

等了一个月,终于在刚下课的时候等到了来自苏州的一个电话,一接果然是HR,心里非常激动,以为和SAP一样要电面了,可没想到就问了一个可以去的时间,我报了1个月(寒假嘛)后,HR就随便说了说如果有的话过几天会有HR来通知的就挂了。然后从同学那里听说这种实习一般都得需要3个月😔,所以就放弃了,最后果然也没有下一步。

2019年1月:MSRA和上海Blockchain

之后,在俱乐部邀请两位去过MSRA的学长学姐来介绍实习经历(俱乐部微信推送)后,学长表示可以内推。心里没忍住,就让学长帮忙内推了MSRA,接下来的事在这篇面经中讲的比较清楚,这里就不再说了。这次是我第一次接受比较正式的面试,成功体验到从自信到自闭的全过程,最终还收获了宝贵的我拒了MSRA的经历。

其实,在这段时间里,我除了投了MSRA,还投过了上海某个区块链相关的职位(上图中最后一个)。在拒了MSRA之后我心灰意冷,向和继续在你院浪费半年时间的残酷命运低头,所以过了几天区块链这个职位的HR打来电话问面试时间,我就直接拒绝了面试。

之后迎来了愉快的、本来准备刷题但是最后在家连LeetCode都没打开的寒假。

2019年2月:夏季实习投递简历

在寒假后第一个工作日就看到了夏季实习生开始投简历的消息(当时的微信推送)。因为刚开学,所以时间比较充足,所以在这次网申开始后,我花了一段时间更新简历,之后立刻开始投简历的。投简历经过了以下阶段。

  • 苏州STCA上海C+AI上海C+AI Open Source

一开始以为可以投多个职位,所以浏览了一下实习列表,除了一定会投的苏州STCA,还发现了上海的两个职位。本来以为上海微软就只有支持,结果发现其实也是有开发岗的。其中普通的C+AI应该和STCA比较相似,但是另一个C+AI Open Source的职位吸引住了我。仔细了解后发现这个工作主要是给VSCode写Java扩展……我是VSCode(当然还有其他微软产品,除了Surface)的忠实用户,对这个开源和开发扩展的工作非常感兴趣,于是也把它选上了。

  • 苏州STCA上海C+AI上海C+AI Open Source

后来听说只能投一个职位。我纠结再三,最后还是感觉在在MS写开源在MS写Java听上去非常酷,而且这样也能提高自己混开源社区的能力,也可以避免把自己螺丝钉化,而且还可以去体验上海的生活,所以最后留下了上海Open Source的职位。

  • 苏州STCA上海C+AI上海C+AI Open Source

结果又没过几天就听说有学长可以帮忙内推。这可难办了:内推可以直接免掉笔试,而我之前对MS的笔试(好吧那是2015年……)的印象是:不会做。而且不仅当时的我不会做,现在的我也不会做啊……虽然刷了一段时间的题但是还是深知自己算法有多么的弱鸡(不管是从自己做题的要死要活的感受以及和同学比较),所以免笔试对我来说是个巨大的诱惑;但是学长只能内推STCA,而上海的是C+AI,这是两个大组,不能互推。又纠结再三,追求稳妥的我最后还是选择了先上车再说,留下了苏州STCA的申请。有关苏州还是上海这一点,文章最后还会继续讲。

刷题

当然了,投简历不重要,在面试前的这段时间里最重要的是刷题准备面试。而MS的面试=做题(这一点请看下面面试部分),所以面试前的准备就是两个字:

刷题

刷题的感觉每个人都不一样,对我来说刷题就是两个字痛苦。几乎每遇到一个题我大脑都是空白的状态,即使刷过50、100、150题以后也是如此;给自己安排的配额是每天5道,确实发生过很快就能解决5道的情况,但是那是少数,大多数时候都是2小时2道medium,每道题一段时间后想不出来就忍不住看Solution,之后心态爆炸准备找3道easy水过,结果被easy难住……;其他同学刷个几十道甚至几道就能在面试场上游刃有余,拿到新题至少有个思路不会惊慌失措,而我甚至在刷了下图这么多题后看到新题仍然是一脸懵逼,心里发慌;更别提其他人刷题就是放松、“找到真正的快乐”,而对我就是折磨……

我的Leetcode情况

总之,这段刷题经历让我终身难忘,也是我最后选择先上车再说的原因:我确实是不想再经历刷题的痛苦了……

PS: 在4月的时候看了下微软笔试题,发现当时选择内推是正确的,也证明了我对自己的认知没有错。

面试

3月1日结束了投简历的环节,然后就边刷题边焦虑地等面试。终于在3月12日收到了一封现场面试邀请,确认自己有空的时间后在3月18日收到了3月20日面试的通知。生日面试可真刺激。第一次面试包含两轮面试,两轮都基本就是在白板上做算法题,每轮1个小时,听学长说,只要过一轮就会又三面。

第一轮

第一轮的面试官把我从楼下带到面试房间后,随便寒暄了几句就开始做题。第一轮的题目是这样的:

n个人,以i,j元组代表i关注j. 输入关注关系的数组(例如[(1,2), (2,3)]表示用户1关注用户2,用户2关注用户3),且认为每个人自己关注自己。计算所有人的被直接关注和间接关注的人数(比如i关注j,j关注k,那么i间接关注k,k的被关注量为3)

这个题目的做法是DFS,即对每个用户(比如说i),寻找(k, i)出现的个数,然后对每个(k, i),寻找k的关注量。用一个Boolean数组表示一个用户是否被访问,从而解决环的问题。最终写出来的算法和图的DFS遍历比较相似。

看起来很简单,都知道是DFS,但是当时不知道脑子怎么抽了想的是带回溯的DFS……然后卡了40分钟没想出来怎么处理环,最后面试官(应该是看不下去了才)来提醒了一下不带回溯,并指出来哪些行要改,但是我还是直到最后一分钟才做出来……由于是在MS办公楼一个小房间里里做的,当时卡住的时候紧张地满身大汗,话都不想说了……这种情绪还是得控制一下才好。这轮根据面试官的反应和个人感觉应该是挂了的。

第二轮

面完第一轮已经一个小时了,于是休息了几分钟就迎来了第二位面试官。和第一位不太一样的是,第二位面试官一开始还问了20分钟左右的项目经历。我趁势推广了我的博客,面试官在我博客上看到了An Infinite Loop Caused by Updating a Map during Iteration这篇文章,就饶有兴趣地让我讲讲这个bug以及怎么发现它的。这个部分我个人感觉讲的不错(毕竟算法题这么屎的我还敢面试MS的原因是我项目经验应该还不错吧……),面试官的反应也不错,这让我稍微放松了一些。

之后,还是得做题。这轮面试第一题是这样:

在二叉搜索树中,按照节点值的大小顺序,找一个节点的上一个节点。

这个题比较简单,因为对二叉搜索数进行中序遍历,其结果的顺序就是从小到大的。所以这个题最简单的做法就是先做一个中序遍历,将结果存储到数组中,然后在数组中查找要查找的节点。

很明显这不是最优的,果不其然面试官要求进行优化。一个简单的优化是只记录前一项(而不是保存之前所有的节点),在中序遍历中,首先检查当前节点是不是要查找的节点。如果是,则返回前一项;否则,将当前节点记录下来,然后继续遍历

以上两个思路可以参考LeetCode 173。之前的两个写法都是用递归的,所以面试官到这里又继续问,能否将递归改成循环。这个也是个比较套路化的做法,背下来就可以了,也可以自己写几个递归算法然后尝试用栈把递归改成循环,可以参考LeetCode 94的Solution。值得一提的是我不知道在这里又出什么问题了,又以为碰上了第一轮一样的错误(环),然后又开始纠结,紧张地满身大汗,话都不想说了。当时心里全想的是完了,然后又立刻安慰自己没过还有其他出路……不过还好,稍微冷静下来后仔细看了下代码发现自己纯粹是没事找事,所以就直接给面试官看了。

这个题到这里就结束了。之后面试官又出了一道题:

二叉树中,输入两个节点,检验两个节点是不是同深度但异父母。

这个题最简单的思路就是寻找从根节点到两个节点的路径,然后比较路径的长度和路径的倒数第二项(即节点的父节点),也可以采用BFS的思想。

这个题做了之后就让询问面试官一些问题后就结束了。

结束一二面后

这一二面还是比较刺激的,随便两道题就将我算法弱鸡的本质毫无保留地暴露出来。这时的心情还是比较复杂的:一方面第一面表现实在是太差了,但另一方面个人感觉第二面地表现应该还是可以接受地,面试官的反应应该也比较正面,再加上两面过一面就可以三面,所以还是有希望的。

令人喜悦的是3月21日就收到了三面的安排邮件,确认时间后在3月29日收到了4月4日三面的邮件。还是从学长那里了解到三面的形式不一,可能又会做题,但也可能只聊天。但是我当时已经松懈了,再加上考试的压力,我就没有继续准备了,准备自由发挥,认命了。

三面

4月4日前去三面,是和另外一批内推的同学一起面试。他们一天面完三轮,而我们先面了两轮的,这次去只用面试最后一轮就可以了。

见到面试官后,面试就直接开始了。一开始它看我简历,就直接问我React的一些问题,例如

  • 三个框架的区别
  • 为什么用React(API简单,概念简洁)
  • React和Vue检测数据变化的方式更喜欢哪一个(当然是有利有弊了)

后面还看我简历上提了ASP.NET Core,又问了ASP.NET Core中Request进来到Controller总共经历了哪些步骤。这谁顶得住啊.jpg……只好在脑子里扒两年前用ASP.NET Core时的一些技术细节,然后支支吾吾说了个Filter……还好面试官接受了我ASP.NET Core就xjb用用的说辞,就没有继续往下问了。

然后又进入了喜闻乐见的刷题环节。这次两道题都是LeetCode原题。第一题LeetCode 26,题目是:

删除排序数组中重复元素

这个的思路就是双指针,如果两个指针的值相等,就把第二个指针后面的所有数据往前移;如果不相等,两个指针都+1

写出来之后,还要写几个测试用例。由于当时还没经过czy老师的教导,就只能凭感觉瞎写了几个。面试官看来也不准备继续在测试用例耗时间,就出了第二题:LeetCode 10

带. *,不带括号的正则表达式匹配。

看到这个我又惊又喜:惊的是这个题在LeetCode上可是Hard题啊;喜的是这个题我还是认真做过,现在还记得思路。根据心里还记得一些思路,用最简单的递归(没有DP)将代码写了出来。面试官似乎有点不太熟悉这个做法,他说他本来想让我写状态机。他一提到状态机,我就想到了编译原理的词法分析的过程,想到了正则表达式中序转后序->后序表达式转NFA->NFA转DFA->使用DFA分析字符串的过程,心里一阵发毛:谁tm能现场写这么多,这谁顶得住啊.jpg * 2。之后就和面试官说,这个用状态机比较复杂,然后就DFA、NFA、闭包啥的吹了一通,面试官可能看时间有点来不及了就没让写了)。

最后还是问了问问题,了解到这个面试官是在MS做Microsoft 365企业应用的,前端正好就用的React + TypeScript(可能这是为什么他来面试我的原因把),后端使用的是ASP.NET Core,他们还在用Microsoft Bot Framework做一些聊天的应用。正好我都有点熟悉,于是和面试官聊了聊这几个技术,现场气氛十分愉悦。

三面后

三面结束后感觉还是比较放松的,毕竟题目都做出来了,聊天也没啥问题,感觉没啥理由挂我)。中午微软包了饭(果然还是微软特色:盒饭),没想到居然遇到了二面面试官……闲聊了几句,比如什么面试情况,微软的工作时间,听说微软的工作时间一般945或者1055的时候感觉这也太棒了吧.jpg,尤其是在现在到处996的情况下,真的是一股清流。

吃完饭等了一会,下午1点多HR找到我说面试官的反馈还是比较positive的,虽然不能肯定但是offer应该是稳了。非常膨胀,以至于忘了第二天清明节还不提前买票,然后(再次)体验了一把4个半小时的汽车。

等车时的抱怨说说

Offer

清明节后第一天收到了面试通过邮件,又过了一周正式收到了Offer,又过了几天收到了分组名单,我分在了Office 365的部门。

感想

到这里我的春招应该就结束了。接下来要做的事情比较简单:

  • 和SAP提出离职

讲道理,SAP真的是良心企业,不仅是955,各种岗前培训也表明了SAP是真心实意把候选人当作人才来培养的,就这样走了还真的感觉有点对不起SAP)。希望还有学弟学妹们把握住机会。

  • 在第三学期各个课程的轰炸中存活

czy,zh,4门课3个组队4个大作业,zh四个pre五个报告,测试上三节课就机考,微笑。

但是每个阶段都有不一样的问题:面试的问题解决了,一些之前搁置的问题又重新变得突出了:

  • 工作还是保研

这个问题纠结了我很久。读个研,用两年时间换个学历,不知道到底有多么重要。学历对我来说倒不是为了什么得到更好的工作(读了研最后不还是这些工作?),也不是薪资的问题(有的公司研究生起薪比本科生高,但是微软不是),而是退路问题。

一直说计算机行业不怎么看学历,但是个人感觉这只不过是计算机行业发展前期供小于求的劳动力供应情况造成的。这几年计算机行业人才井喷,劳动力供应飞速增长,可是随着行业结束野蛮发展的状态,劳动力需求增长减缓,甚至不增反降。作为一个硬指标和门槛,学历的重要性肯定会提高,甚至不排除发展成金融行业那样的情况。如果以后微软凉了(外企护城河再深,效率再高,国内企业在各种政策、情怀和奋斗比加持下,并不是不可能动摇外企目前看似坚固的业务的根基;再加上国内越来越加深的“国产情怀”,以及国内各种保护政策,微软等外企在国内业务凉掉的可能性并不是不存在),得重新出去找工作的时候,有可能在学历关就被刷掉。这就非常难办了。

但是从个人角度来说,由于各种原因(特别在你院的三年),我确实不想再呆在学校,更加期望能够投入工作,做一些真正有用的东西。就像在我的关于里说的那样,我还是希望我的工作能够有利于对其他人有用(这也是我喜欢微软的一大原因,Make Other People Cool),能够帮助他人提高工作效率,并且做一些自己真的想做的事情,而不是在学校里(这里省略一些字)。

感觉目前来说这个问题还是只能先去实习,亲身感受下工作的环境后再定。

  • 上海还是苏州

之前更加喜欢苏州,并不是因为苏州的自然人文环境等(个人确实不太care这些),而是因为苏州的生活成本和最重要的房价。除了这个方面,在其他例如发展前景(不太看好苏州发展前景,毕竟有个上海,纯个人想法请不要因为这个点撕我)、新事物的应用机遇资源(比如说对后代)等其他方便,上海不仅现在更好,以后也只会更好不会更差。

但是对于生活成本这一点,最近查了查上海和苏州房价的对比,发现苏州并没有想象中的那么便宜。当然上海整体会更高,但是上海稍偏远一点的地方的房价和苏州贵一点的地方的相比也并没有离谱到哪儿去。再加上上海的薪资会比苏州高,所以其实这个选择还不是那么容易做的。

目前的打算还是先在苏州实习,在实习过程中更多地了解一下两个城市地情况和转正上海的事情。

总结

不管怎样,我的微软实习申请部分到现在就结束了,现在最重要的工作是在这学期的课程轰炸下存活,同时也要在以后找个时候重新开始算法题,毕竟转正面试也是要考算法题的嘛。

2017年总结

2017年12月31日 23:45

是时候来简单梳理下我的2017年了。

寒假:

  1. 去年今天的这个时候(2016年12月31日),我都已经到家了。而今年却要一直考到考试周倒数第二天。真是风水轮流转啊。同时,今年也是第一次在外面过元旦。
  2. 南星计划。第一次参加这种社会实践活动,和20多个学校的小伙伴一起进行了第一次联合宣讲,建立的志愿咨询群也一直持续着它的价值。
  3. 又双叒叕折腾出一套博客,又双叒叕以为能一直维护和使用下去,结果还是几乎弃坑了:新功能没加,依赖更新后代码编译不通过不管,前端资源都5M了也没想着优化,文章也没写几篇,最重要的是上了软工二课后看原来的代码就是一坨那啥。又不知道啥时候又能把它捡回来,或者说,我真的要继续去做它吗?其他大佬要么学得更基础(算法等),要么学业内目前的高端的知识(机器学习什么的),做这种项目对以后找工作来说真的有意义吗?做这种项目真的能提高我的“技术”吗?

大一第二学期:

  1. 我到现在都想知道为什么当时的我要花一个Surface Book 2的钱去报托福课。上了一半,另一半拖到现在,不知道什么时候才想继续。而且这也给了我一个教训:做什么事都要三思。
  2. EL比赛,第一次拿到奖金(500块)。
  3. 去北京玩了几天,高中同学招待得非常到位,真的非常感谢他们!
  4. 当时学长说大一下是大学最轻松的一个学期,当时还不信,到了大二才知道确实是这样。
  5. 建议列表里大一的学弟学妹,大一下确实是最轻松的,尤其是还少了一门计组课,有安排请尽快付诸行动,到了大二,一切都晚了。

暑假:

  1. 无聊的小学期。十几年不变的课程,所有人都对它反感,不知道为什么还在继续开,白白占了10天的宝贵的假期时间。
  2. 社会实践。当时的豪情壮志并没有完全做到,虎头蛇尾是最准确的形容词,有点对不起组员。
  3. 微软学生夏令营。有幸来自全国的巨佬在一起,认识了许多人,更加强了信仰。
  4. 比寒假短的暑假。

大二第一学期:

  1. 和大一完全不同,高考结束后经历的第二次大改变。
  2. 第一次在社团有个一官半职,有点想法却很多都因为自己经验不够显得都太naïve,想做出一点改变却总是做不到。
  3. 软工二是大二上的万恶之源。从开始到现在,每天心里都是软工二,每天都想着大作业,除了软工二啥都不想做,一些时间上任何课都是在做软工二。所以整个大二上几乎没有碰个人项目,也没有接触其他的领域(比如创新项目)。感觉有点过了,主次不分?
  4. 上一条成立也幸亏数据结构水的不成样子。一门非常重要的计算机基础课程上成PPT Reading以及离散数学代码版,这个专业是怎么评上A的?以及学成这个样子,真的就够了吗?
  5. 奖学金到手瞬间透支。
  6. 想通过换输入法和键盘布局加快打字速度。dvorak键位学了2个月,打字练习正确率能到90%,然后放弃了,12月转而学双拼来提高中文速度,到现在能日常使用了,但是速度还是不如全拼。我也花了非常多的时间在这个无意义的转换上。
  7. 朋辈导师一直到11月才正式开始,感觉太晚了,从我自己的经验,11月已经过了最困难的适应期,如果能在刚入学的时候甚至暑假就开始,可能能发挥更大的作用。但即使如此,它也不是这两个月没干什么事的借口。新的一年应该多给点学弟学妹们做实事,不能让他们失望。
  8. 要说这学期忙,可是,有的同学同时在多个组织任职并把事情做得很好,有的同学还在学习其他前沿的知识,有的同学在周末安排了各种课外的活动和学习,我和他们上的同样的课,为什么他们就能安排这么多的事情?为什么我就啥都没做,却感觉忙得不可开交?
  9. 脱单。从来没有这学期一样这么想脱单,可是自己太怂,感觉自己很菜等一些原因,也只能在脑子里想想了。

写到这里,时间显示着23点16分。2017年虽然不像2016年一样,是人生的新阶段的起点,可是在这一年里,我经历了许多人生的第一次,尝试做一个不一样的自己。有开心,也有难过;有收获,也有遗憾。

数十公里外的市中心的热闹的人群准备着新年的倒计时,而我也安静地等着电脑右下角的2017/12/31跳到2018/1/1,和大家一起说出那一句

2018年新年快乐

</2017><2018>

Python语言实现的符合本福特定律的十进制固定长度随机数发生器

2017年12月10日 00:04

前言

本福特定律改变了人们对随机的认识。之前人们认为,在一组自然的随机数中,以每一个数字打头的数占总频数的频率是一样的。但是本福特定律却用严谨的数学语言证明了不同数字打头的数并不是一样的。本福特定律说明,在b进位制中,以数n起头的数出现的概率为log_b(1+1/n)。这篇文章中提出了一个在现有的平均概率随机数生成器的基础上实现一个十进制下符合本福特定律的、固定位数的随机数生成器。这篇文章展示它的效果,介绍它的算法以及它的代码实现。

代码

运行需要Python 3,不需要其他库依赖。

import math, random
from functools import reduce
 
def possibility_for_n(n):
    return 0 if n==0 else math.log(1+1/n,10)
 
def distribution_list(n):
    result = []
    for i in range(0,10):
        r = 0
        for j in range(int(math.pow(10,n-2)),int(math.pow(10,n-1))):
            r = r + possibility_for_n(j*10+i)
        result.append(r)
return result
 
def search_list(distlist):
    return list(map(lambda i: sum(distlist[:i+1]), range(0,10)))
 
def generate_digit(n):
    rand = random.random()
    searchlist = search_list(distribution_list(n))
    if rand <= searchlist[0]:
        return 0
    for index in range(0,10):
        if rand > searchlist[index] and rand <= searchlist[index+1]:
            return index+1
 
def generate_one(length):
    return reduce(lambda x,y: x*10+y, map(lambda i: generate_digit(i), range(1,length+1)))
 
def generate_multiple(length, num):
    return [generate_one(length) for i in range(0,num)]

代码使用

调用generate_one函数来生成一个随机数,参数为数字位数。 调用generate_multiple函数生成指定个数个随机数,第一个参数为数位数,第二个参数为生成个数。

示例:

> generate_one(4)

> 4937

> generate_multiple(4,10)

> [6179, 4971, 7735, 1392, 5046, 4750, 4412, 2249, 1530, 8443]

代码效果

此代码可以生成指定位数的、指定个数的符合本福特定律的随机数集。

对于代码生成的如下50个4位随机数,

[2152, 6766, 2117, 4239, 5047, 7623, 2497, 1382, 8081, 4431, 2983, 8968, 1669, 6670, 7242, 3819, 1565, 1399, 2102, 1706, 3257, 8281, 8735, 9197, 5254, 5872, 6805, 2526, 3951, 1271, 4271, 1540, 3713, 1124, 1452, 6037, 3279, 6424, 1550, 2596, 9411, 1272, 1996, 3735, 2403, 3007, 4303, 1146, 1513, 1098]

统计其首位各个数字出现频率,与理论计算值对照,得到如下表格:

开头数字n实际频率p_a (n)理论频率p_e (n)
1(n_start)0.30.3010
20.160.1760
30.140.1249
40.080.0969
50.060.0792
60.10.0669
70.040.0580
80.080.0512
9(n_end)0.040.0458

使用公式

α=(sum(k=n_start to n_end)((p_a (k)-p_e (k))^2))/(n_start-n_end+1)

度量实际频率与理论频率的偏离程度,数字越小越好。

上例的结果为

α_1=0.00038025004826301343。

以同样方法计算前两位与理论值的误差,结果为

α_2 = 0.00013919331883366668

作为对比,平均概率的随机数生成器生成的4位50个数字

[2665, 2249, 4658, 3974, 2232, 1486, 1616, 7235, 4730, 5351, 1973, 4399, 7146, 2650, 7414, 9585, 2561, 1599, 1839, 6233, 1449, 8225, 7896, 2726, 4011, 7821, 8365, 2231, 4490, 2247, 9524, 7578,4203, 1741, 5035, 9362, 9340, 1798, 5398, 1676, 9247, 8809, 2948, 4933, 1040, 9487, 6529, 5773, 8622, 6047]

α_1 = 0.0037032530835963864

数字越多,其结果越贴近理论值,以下为10000个4位随机数首位各个数字的频率与理论对照值的表格(为了节省篇幅,不附上具体数字):

开头数字n实际频率p_a (n)理论频率p_e (n)
10.29910.3010
20.17830.1760
30.12270.1249
40.09860.0969
50.07800.0792
60.07010.0669
70.05600.0580
80.05000.0512
90.04720.0458

相关指标为:

α_1 = 0.000003909609950132077

α_2 = 0.000002385500926719904

算法分析

若设p(n)为本福特定律中指出的以n开头的数字占所有数字的出现的频率,那么对于从高位起第n位(最高位为第1位),数字i出现的频率为

sum(k=10^(n-2) to 10^(n-1)-1)(p(10k+i))

对于n=1,它的起始为0。对于最高位0,频率为0,对于其他位置出现的0,可以不特殊处理。

例如,对于从高位第2位,它出现2的概率为p(12)+p(22)+p(32)+⋯+p(92),而对于高位第4位,它出现5的概率为p(10005)+p(10015)+⋯+p(99995)

可以看到,

每一位的每一个数字的概率都可以通过这个公式计算出来,所以每一位上每一个数字出现的概率都是一定的,并且是可以计算出来的。

所以针对每一位数,根据它对应的频率生成一次随机数,可以保证最后的结果能够满足本福特定律。

称每一位每一个数字出现的概率按对应数据大小排序的结果为分布表(distribution list,d)。最高位的分布表为

d_0 = [0,0.3010,0.1760,0.1249,0.0969,0.0792,0.0669,0.0580,0.0512,0.0458]

,分别对应0,1,2,3,4,5,6,7,8,9为起始的数字的出现概率。

接下来问题转到了如何根据确定的概率分布生成一位数字。

定义概念搜索表(search list, s)。搜索表定义如下:

s[i]= sum(k=0 to i)(d[k]) ,∀i∈[0,n_end-n_start]

用人话说,搜索表的第n项等于其对应分布表首项到第n项的概率之和。

那么上文d_0对应的搜索表为

s_0=[0,0.3010,0.4771,0.6021,0.6990,0.7782,0.8451,0.9031,0.9542,1.000]

根据定义,搜索表最后一项一定为1。 下面需要假设现有随机数的发生器可以在[0,1)区间按平均的概率生成随机数。幸运的是,几乎所有语言都提供了生成这种随机数的机制。

现在假设生成为随机数为r∈[0,1),那么在搜索表中寻找i∈[0,n_end-n_start],使得r>s[i]且r≤s[i+1],取i+1作为本位随机数结果。如果r≤s[0],取0。因为搜索表的每一项是单增的,可以保证i唯一;由于搜索表最后一项一定为1,可以保证i一定存在。

根据这个算法得出的数字能够保证了每一位数字出现的概率和搜索表符合。

对每一位运用此算法,计算其对应的分布表和搜索表,就可以生成指定位数的随机数。

代码分析

代码分为6个部分,分别为以n开头的数字的频率(possibility_for_n(n)函数)本位的分布表(distribution_list(n)函数)本位的搜索表(search_list(distlist))生成第n位(generate_digit(n)函数)生成一个数(generate_one(length))以及生成多个数(generate_multiple(length, num))

以n开头的数字的频率

def possibility_for_n(n):
    return 0 if n==0 else math.log(1+1/n,10)

这段代码很好理解:计算以n开头的数字的频率。由于数字不能以0开头,所以以0开头的数字的频率为0。

本位的分布表

def distribution_list(n):
    result = []
    for i in range(0,10):
        r = 0
        for j in range(int(math.pow(10,n-2)),int(math.pow(10,n-1))):
            r = r + possibility_for_n(j*10+i)
        result.append(r)
    return result

根据定义生成第n位的分布表。

本位的搜索表

def search_list(distlist):
    return list(map(lambda i: sum(distlist[:i+1]), range(0,10)))

参数为搜索表对应的分布表。为了简洁,这里采用了map高阶函数。它的作用是把第二个参数的每一项作为参数执行第一项的函数,函数的返回值组成新列表作为表达式结果。distlist[:i+1]表示取distlist的从0项到第i项的子表。

生成第n位

def generate_digit(n):
    rand = random.random()
    searchlist = search_list(distribution_list(n))
    if rand <= searchlist[0]:
        return 0
    for index in range(0,10):
        if rand > searchlist[index] and rand <= searchlist[index+1]:
            return index+1

参数n的含义为第n位。这段函数中,第一句代码生成随机数rand ∈[0,1)(random.random()正好如此),之后生成第n位的搜索表,然后根据上文所阐述的搜索index。这里为了简洁,使用了顺序搜索,虽然它时间复杂度为O(n),弱于二分法,但是由于每个表的规模恒定为常数9,这个复杂度可以接受。

生成一个数

def generate_one(length):
    return reduce(lambda x,y: x*10+y, map(lambda i: generate_digit(i), range(1,length+1)))

参数为数的位数(长度)。这里为了简洁,采用了map和reduce两个高阶函数。Map不再赘述。reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,把结果继续和序列的下一个元素做累积计算。

例如,reduce(lambda x,y: x+y, [1,2,3])的结果是6,计算过程为:

[1,2,3]->[1+2,3]->[3,3]->[3+3]->[6]->6。

这里首先采用map生成第1位到第length位的数字,然后采用reduce方法将这些数字合并成一个数字。

生成多个数

def generate_multiple(length, num):
  return [generate_one(length) for i in range(0,num)]

length为数的位数,num为生成数的个数。函数调用num次generate_one(length)方法,返回结果。

总结

这篇文章展示了一个实用的符合本福特定律的十进制固定长度随机数生成器,并对它的原理和代码实现进行了简要的介绍。在此基础上,可以很容易地扩展到任意进制的随机数长度生成器。但是,这个算法仍然有改进空间,例如可以根据相同的原理实现任意长度的生成器。读者如果有兴趣,可以对此进行更深一步的研究。

参考

[1] https://en.wikipedia.org/wiki/Benford%27s_law

[2] http://blog.iharder.net/2010/11/10/benford-how-to-generate-your-own-benfords-law-numbers/

[3] https://softwareengineering.stackexchange.com/questions/255892/unevenly-distributed-random-number-generation

[4] https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014317852443934a86aa5bb5ea47fbbd5f35282b331335000

❌
❌