把一台旧手机变成家庭服务器,以及它教我的事
起因
我有一台闲置的旧安卓手机,640GB 存储,插着电扔在家里。与其吃灰,不如把它变成一台家庭服务器 —— 跑监控、当 NAS、做远程下载。
约束很硬:不 root。所以一切都在 Termux(安卓上的终端环境)里,以一个普通 App 的身份运行。这个约束后来定义了整件事的成败。
一夜下来,这台手机上跑起了:Prometheus + Grafana(监控)、File Browser + rclone(存储)、aria2(下载)、还有一条把另一台手机的照片自动按日期归档的流水线。全部通过 Tailscale 组网,人在外面也能访问。
但真正值钱的不是“装了什么”,是搞清楚了它到底怎么才会挂。
第一课:自愈系统的核心假设是错的
我给所有服务写了 watchdog:每 15 分钟检查一次,挂了就拉起来。用安卓的 JobScheduler 定时触发。听起来很稳。
直到某次整个 Termux 被系统杀掉 —— 等了 30 分钟,什么都没恢复。
查下来才明白:安卓强杀一个应用时,会连带取消它注册的所有 JobScheduler 任务。 也就是说,当最需要自愈的时候(应用被杀),自愈机制本身也没了。「15 分钟自愈」只覆盖“应用还活着、单个服务死了”这一种情况 —— 而那恰恰是最不严重的情况。
更打脸的是,我一度以为把电池优化设成「不受限制」能防止被杀。错了 —— 那管的是省电策略(Doze / App Standby),而杀我的是低内存杀手(LMK),两套完全独立的机制。
教训:自愈系统要先问“我这套机制在什么情况下会和被监控对象一起失效”。
第二课:不要在自己画的线前停下,然后当成结论
后来我想系统地测:这台手机到底怎样才会被杀?于是写了一堆压力测试。
第一轮结果让我很意外 —— 全都杀不掉:
- 分配 14GB 内存:活着
- 8 线程烧满 CPU 十分钟:活着(系统靠降频扛住,温度锁死在 80°C)
- 开 60 个进程:活着
我差点得出“这手机很稳”的结论。直到我意识到:这些上限全是我自己拍的,不是设备的极限。 这台手机有 15GB 内存 + 7GB swap,可寻址是 22GB,而我只测到 14GB —— 根本没撞墙。
把内存推到 30GB、不给内核喘息之后,它在约 17GB 时被杀。而且判据不是“内存不够”,是“两级回收同时见底”:swap 剩 105MB、页缓存剩 258MB,内核实在无处可回收了才动手。
进程数也一样 —— 推到约 400 个才触发(ulimit 报的是 58911,但安卓在系统层卡死得早得多)。
教训:“测了没事”如果用的是自己设的安全参数,那是假阴性。边界应该由设备定义(内存总量、文档上的硬限制),不是由你为了安全选的数字。
第三课:计数器对,不代表事情做对了
这一夜抓了二十多个 bug,绝大多数的共同点是 不报错:
- CPU 频率监控:面板上一条正常的曲线。实际上 8 个核的标签解析错了,全塌缩成一条空标签序列 —— Prometheus 不报错、
up恒为 1、日志干净。发现它的唯一线索是“序列数该是 8 却只有 1”这个对不上的数字。 - 照片归档:统计说生成了 5 张缩略图。实际写进了错误的目录 —— 计数完全正确,只是写错了地方。
- 传输脚本:日志明明白白写着“已暂停下载”,而下载还在全速跑 —— 因为脚本里一处
set -u下的未定义变量让它中途夭折,状态却先写了。
这些都不是靠看日志发现的,是靠去数目标端的实际文件数、实际序列数、实际能否登录才暴露的。
教训:验证要看“事情有没有真做成”,不是看“程序有没有报成功”。这两者的差距,是我这一夜最大的收获。
尾声:边缘设备跑 AI 的真实边界
最后我想试试在手机上跑 AI。结论很干脆:
- 纯文本 4B 模型:能跑,几十秒出结果 —— 但必须手动限制上下文长度,否则 KV cache 会吃掉 10GB(模型文件才 2.5GB),把整个环境撑爆。
- 视觉模型(能看图对话的那种):能加载,但一张图要算 3-5 分钟,而持续几分钟的高占用会触发系统清理 —— 这是和“瞬时内存尖峰”不同的另一种杀法。
- 想跑真正快的检测模型(几十毫秒那种):框架在这个环境里根本装不上 —— 没有预编译的 ARM 版本,现场编译又会把手机拖挂。
一句话:这台手机是个好的“采集 + 存储 + 触发”设备,不是“算力”设备。 模型该放在有算力的地方,弱设备只负责它擅长的事。
这个结论,其实正是我最近在做的方向的核心 —— 知道什么该放在哪,比什么都想往边缘塞更重要。
这台手机现在还在家里跑着。这篇博客,某种程度上也是它的产物之一。