把一台旧手机变成家庭服务器,以及它教我的事


起因

我有一台闲置的旧安卓手机,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 版本,现场编译又会把手机拖挂。

一句话:这台手机是个好的“采集 + 存储 + 触发”设备,不是“算力”设备。 模型该放在有算力的地方,弱设备只负责它擅长的事。

这个结论,其实正是我最近在做的方向的核心 —— 知道什么该放在哪,比什么都想往边缘塞更重要。


这台手机现在还在家里跑着。这篇博客,某种程度上也是它的产物之一。