解决 Frigate 周期性硬盘无用写入问题
图都是从聊天记录里扒的, 面向群友 debug
之前需要个门铃摄像头
但现有方案要么需要上云, 要么就得装全家桶
都不干净, 还不如自己做
于是就搓出来了个:
TL-IPC55AE 加 3D 打印外壳, 顺便解决了原装摄像头底座过于方便拆的问题
录像和监控报警用的是 Frigate
它的物体识别准确率还可以, 并且支持的硬件多, 开启硬件加速后总体功耗非常低, 省电
最关键是可以仅在检测到运动时录像, 不像 scrypted 必须 7x24 小时的
对硬盘十分友好, 1T 可以存三年
问题
然后就遇到了标题说的问题:
硬盘它每隔几秒, 就响一下
周期非常稳定, 声音一听就是在写入
一看果然有个 700k 的写入
把 Frigate 停掉就没有了
但 Frigate 说自己啥都没检测到, 并没有在录像
然后 iotop 能看见 Total DISK WRITE
一直为 0 但是有 Actual DISK WRITE
这个行为就比较怪了, 可能是文件系统在写 metadata
可我开着 relatime 能有什么更新这么频繁?
还是上 strace
怎么有个 chown? ctime 变了那肯定有写入了
bcachefs 的 lazytime
看起来还不工作, 找源头修吧
简单 rg 了一下 frigate 里没有, 那再去 sqlite 里找找
就找到了这段:
/* The owner of the rollback journal or WAL file should always be the
** same as the owner of the database file. Try to ensure that this is
** the case. The chown() system call will be a no-op if the current
** process lacks root privileges, be we should at least try. Without
** this step, if a root process opens a database file, it can leave
** behinds a journal/WAL that is owned by root and hence make the
** database inaccessible to unprivileged processes.
**
** If openMode==0, then that means uid and gid are not set correctly
** (probably because SQLite is configured to use 8+3 filename mode) and
** in that case we do not want to attempt the chown().
*/
if( openMode && (flags & (SQLITE_OPEN_WAL|SQLITE_OPEN_MAIN_JOURNAL))!=0 ){
robustFchown(fd, uid, gid);
}
sqlite 在以 root 身份打开 wal 的时候会同步 wal 与 db 的权限/所有者
这本应只在创建文件的时候进行, 但它设计上又没有区分打开还是创建, 只设置了个 O_CREAT
chown 就只能每次都运行了
修起来很容易, 但 sqlite 不接受直接的代码贡献, 那就有时间再说吧
Frigate
更大的问题在 frigate 这边, 它竟然拿 root 跑服务!
frigate 官方提供了好几种部署方式, 但里面其实都是 docker, 并且不是常规的一个服务一个容器
而是把所有服务全塞到一起, 然后用 s6 来管理
这就让权限管理非常头大, 再加上调用 GPU 所需的权限, 事情就更复杂了
一个晚上解决不了, 还是先用个简单方案, 让它先跑着再说
SQLite
众所周知
SQLite 的跨平台做得非常好, 你甚至可以给它写一个 pastbin 后端
原因是它有一个 VFS 层, 对所有底层接口做了抽象
然后这个 VFS 呢,还支持 override 它内部使用的 system call, 方便 debug
我们就可以拿它暴力删除 chown (或者不那么暴力, 只过滤掉 wal 的 chown)
首先找到库的位置:
docker exec frigate bash -c 'lsof -p $(pgrep python) | grep sqlite'
python3 107 root mem REG 0,66 1318776 119654 /usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6
python3 107 root mem REG 0,66 99840 118646 /usr/lib/python3.9/lib-dynload/_sqlite3.cpython-39-x86_64-linux-gnu.so
python3 107 root mem REG 0,66 9347208 143171 /usr/local/lib/python3.9/dist-packages/pysqlite3/_sqlite3.cpython-39-x86_64-linux-gnu.so
从 frigate 的 dockerfile 里看它是自己打包的 sqilte, 所以不是第一个
然后 lib-dynload 里是 python ffi 的东西, 没有我们需要的符号, 也排除
再从 sqlite 复制一下 vfs object 的定义, 塞给 cffi
from cffi import FFI
ffi = FFI()
lib = ffi.dlopen("/usr/local/lib/python3.9/dist-packages/pysqlite3/_sqlite3.cpython-39-x86_64-linux-gnu.so")
ffi.cdef("""
typedef const char *sqlite3_filename;
typedef struct sqlite3_file sqlite3_file;
typedef struct sqlite3_int64 sqlite3_int64;
typedef struct sqlite3_vfs sqlite3_vfs;
typedef void (*sqlite3_syscall_ptr)(void);
typedef int (*fchown_ptr)(int, int, int);
typedef struct sqlite3_vfs {
int iVersion; /* Structure version number (currently 3) */
int szOsFile; /* Size of subclassed sqlite3_file */
int mxPathname; /* Maximum file pathname length */
sqlite3_vfs *pNext; /* Next registered VFS */
const char *zName; /* Name of this virtual file system */
void *pAppData; /* Pointer to application-specific data */
int (*xOpen)(sqlite3_vfs*, sqlite3_filename zName, sqlite3_file*,
int flags, int *pOutFlags);
int (*xDelete)(sqlite3_vfs*, const char *zName, int syncDir);
int (*xAccess)(sqlite3_vfs*, const char *zName, int flags, int *pResOut);
int (*xFullPathname)(sqlite3_vfs*, const char *zName, int nOut, char *zOut);
void *(*xDlOpen)(sqlite3_vfs*, const char *zFilename);
void (*xDlError)(sqlite3_vfs*, int nByte, char *zErrMsg);
void (*(*xDlSym)(sqlite3_vfs*,void*, const char *zSymbol))(void);
void (*xDlClose)(sqlite3_vfs*, void*);
int (*xRandomness)(sqlite3_vfs*, int nByte, char *zOut);
int (*xSleep)(sqlite3_vfs*, int microseconds);
int (*xCurrentTime)(sqlite3_vfs*, double*);
int (*xGetLastError)(sqlite3_vfs*, int, char *);
/*
** The methods above are in version 1 of the sqlite_vfs object
** definition. Those that follow are added in version 2 or later
*/
int (*xCurrentTimeInt64)(sqlite3_vfs*, sqlite3_int64*);
/*
** The methods above are in versions 1 and 2 of the sqlite_vfs object.
** Those below are for version 3 and greater.
*/
int (*xSetSystemCall)(sqlite3_vfs*, const char *zName, fchown_ptr);
sqlite3_syscall_ptr (*xGetSystemCall)(sqlite3_vfs*, const char *zName);
const char *(*xNextSystemCall)(sqlite3_vfs*, const char *zName);
/*
** The methods above are in versions 1 through 3 of the sqlite_vfs object.
** New fields may be appended in future versions. The iVersion
** value will increment whenever this happens.
*/
} sqlite3_vfs;
sqlite3_vfs* sqlite3_vfs_find( const char* name );
""")
@ffi.callback("int(int, int, int)")
def mychown(fd, pid, gid):
return 0
vfs = lib.sqlite3_vfs_find(b"unix")
vfs.xSetSystemCall(vfs, b"fchown", mychown)
最后要让它在 import sqlite3
之前被执行, 所以写个 volumes 映射让 docker 把它放到随便一个 __init__.py
里
volumes:
- ./block_fchown.py:/opt/frigate/frigate/__init__.py
就搞定啦
BTW:
xSetSystemCall
不能简单传个 null 过去, 是重置
其他
等有时间还是看看怎么精细控制权限, 不然所有人就都知道我拿 root 跑着个 frigate 了, 而 docker 又那么容易逃逸
危
顺便还发现 TP-LINK 没有使用标准的 ONVIF Profile T 做双向语音, 而是自己做了个 RTSP MULTITRANS 扩展, 里面跑的东西其实和 tapo 的私有协议差不多, 只是把外层从 HTTP 换成了 RTSP, 基于 go2rtc 现有的 tapo 实现改一个出来应该不难, 也放进 TODO 里好了