别的用户读不了索引:Permission denied
主线位置:磁盘主线第一步,目录里的
meta.json、.managed.json和两个锁文件。每个文件是什么见一个段,atomic_write和ManagedDirectory见 Directory。
flowchart TB cm["commit"] --> aw["atomic_write"] aw --> tf["tempfile 0600"] tf --> mv["改名为 meta.json"] mv --> other["别的用户 open"] other --> e1["读 meta.json 拒绝"] other --> e2["写 meta.lock 拒绝"]
实验 crate:exp-tantivy/pita-lab,src/bin/p14_perm.rs:create 建索引并提交一次,commit 再提交一次,open 做 Index::open_in_dir 加 reader()。本机只有一个普通用户,「别的用户读不到」用 chmod 拿掉属主自己的权限来模拟,得到的是同一个 EACCES。M2 Max、macOS 27、rustc 1.99.0,release,umask 022。
现象
建索引后的权限,以及手动放开、再提交一次:
umask 022
created:
600 .managed.json, 600 meta.json, 644 fd83143ea39847f182f5b1a7a1b4ad09.store
$ chmod 644 meta.json .managed.json
one more commit:
600 .managed.json, 600 meta.json, 644 2c76ed9ba407477e821967009d0f4938.store, 644 fd83143ea39847f182f5b1a7a1b4ad09.store
- 段文件按 umask 是 0644,
meta.json和.managed.json却是 0600。 - 手动
chmod 644只管到下一次提交:提交后两个文件又是 0600。
读不了这两个文件时打开索引的报错:
$ chmod 000 meta.json
open_in_dir + reader: Err(OpenReadError(IoError { io_error: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }, filepath: "meta.json" }))
$ chmod 600 meta.json; chmod 000 .managed.json
open_in_dir + reader: Err(OpenReadError(IoError { io_error: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }, filepath: ".managed.json" }))
典型场景:索引由一个服务账号写,另一个账号下的运维脚本、备份校验程序、或者以不同用户跑的查询服务去读。
原因
0600 来自 tempfile。 meta.json 和 .managed.json 都走 atomic_write:在同一目录建临时文件、写完、sync_data、改名:
let mut tempfile = tempfile::Builder::new().tempfile_in(parent_path)?;
tempfile.write_all(content)?;
tempfile.flush()?;
tempfile.as_file_mut().sync_data()?;
tempfile.into_temp_path().persist(path)?;
tempfile 3.27 建临时文件的默认权限是 0o600(它的 Builder::permissions 文档:「Permissions default to 0o600 for tempfiles」),tantivy 没有改它。改名保留 inode 和权限,所以每次提交都是一个新的 0600 文件,之前的 chmod 随旧文件一起被替换掉。段文件走 open_write,是普通的创建,所以按 umask 是 0644。
reader 还要写一个锁文件。 即使 meta.json 能读,reader 打开段时要拿 META_LOCK(open_segment_readers,防止 GC 在打开过程中删文件),而 MmapDirectory::acquire_lock 是以写方式打开 .tantivy-meta.lock、不存在就创建。这个文件按 umask 是 0644,别的用户没有写权限:
-rw-r--r-- 1 user staff 0 Oct 29 18:04 .tantivy-meta.lock
$ chmod 444 .tantivy-meta.lock
open_in_dir + reader: Err(LockFailure(IoError(Os { code: 13, kind: PermissionDenied, message: "Permission denied" }), None))
所以另一个用户要读这个索引,三样都得有:meta.json、.managed.json 可读,.tantivy-meta.lock 可写(不存在时还要能在目录里创建它)。tantivy 的设计假设是读写索引的都是同一个用户。0.27.0 之后 main 上没有改 src/directory/。
atomic_write 从 0.14 起用 tempfile0.14 之前,MmapDirectory::atomic_write 用的是 atomicwrites crate 的 AtomicFile。PR #866(31d18dca1,「Removing dependency to atomicwrites」)把它换成 tempfile::NamedTempFile::new() 加 persist,CHANGELOG 0.14.0 记作「Remove dependency to atomicwrites #833」。NamedTempFile::new() 建在系统临时目录里,而改名不能跨文件系统;两周后 PR #878(151498cbe,「Creating the tempfile for atomicwrites in the same directory as the MmapDirectory.」)改成 tempfile::Builder::new().tempfile_in(...),也就是今天这段代码的写法。两个提交都首次发布在 0.14。从那以后 meta.json 就是 tempfile 建的文件,权限跟着 tempfile 走。
修法
最简单:读写索引的进程用同一个用户。 查询服务、运维脚本都切到写索引的那个账号。
确实要给另一个用户读: 包一层 Directory,只改 atomic_write,让临时文件以 0644 创建,其余方法全部转发给 MmapDirectory:
#[derive(Clone, Debug)]
struct ShareableDir(MmapDirectory, std::path::PathBuf);
impl Directory for ShareableDir {
fn atomic_write(&self, path: &Path, data: &[u8]) -> std::io::Result<()> {
let mut tmp = tempfile::Builder::new()
.permissions(std::fs::Permissions::from_mode(0o644))
.tempfile_in(&self.1)?;
tmp.write_all(data)?;
tmp.as_file_mut().sync_data()?;
tmp.into_temp_path().persist(self.1.join(path))?;
Ok(())
}
// get_file_handle, delete, exists, open_write, atomic_read, sync_directory,
// acquire_lock, watch: forward to self.0
}
用 Index::create(ShareableDir(..), schema, settings) 建、Index::open(ShareableDir(..)) 打开,写两次:
created (ShareableDir):
644 .managed.json, 644 8a4588a9ef2343a688d4aec2f3b94010.store, 644 meta.json
one more commit (ShareableDir):
644 .managed.json, 644 8a4588a9ef2343a688d4aec2f3b94010.store, 644 8f1b92fd98f94392a03d0fb727475049.store, 644 meta.json
open_in_dir + reader: Ok, num_docs 2
acquire_lock 一定要转发给 MmapDirectory:trait 的默认实现是「文件存在即上锁」,而 MmapDirectory 用过的锁文件会一直留在目录里,用默认实现就会一直以为锁被占着(两种实现见 IndexWriter)。另外把 .tantivy-meta.lock 的权限放开一次(比如 chmod 666,或者归到一个共同的组再 g+w):这个文件建好以后不会被删除重建,改一次就一直有效。读者进程用普通的 Index::open_in_dir 就行。
本机只有一个用户,「另一个用户真的能读」没有直接验证;上面验证的是三个文件的权限位,以及缺了任何一个时的报错。
怎么检查
ls -la 索引目录:meta.json、.managed.json是-rw-------,.tantivy-meta.lock是-rw-r--r--,属主不是读者进程的用户,就会中招。- 用读者的账号试一次:
sudo -u <读者> <程序> open <目录>,或者直接sudo -u <读者> cat meta.json。 - 报错里
filepath是meta.json或.managed.json,或者是LockFailure(IoError(... PermissionDenied ...)),都是这个问题。
小结
meta.json、.managed.json每次提交都由 tempfile 重建,权限 0600,chmod只管到下一次提交;段文件是 0644。- reader 还要以写方式打开
.tantivy-meta.lock;它是 0644,别的用户写不了。 - 读写用同一个用户最省事;要共享,就包一层
Directory让atomic_write用 0644,并放开.tantivy-meta.lock的写权限。