# dht-search `dht-search` 是集 DHT Metadata 采集 RocksDB 持久化 Tantivy 搜索索引和 HTTP API 于一体的应用 ## 构建准备 RocksDB 包含 C++ 代码并在构建时使用 bindgen 因此需要 libclang Windows 可以把 libclang 安装到工作区本地目录 ```powershell python -m pip install --target .tools\libclang libclang $env:LIBCLANG_PATH = "$PWD\.tools\libclang\clang\native" cargo build -p dht-search --release ``` `.tools` 只用于本地构建不会部署到运行设备 ## 配置 根目录 `config.toml` 包含全部运行配置和内容过滤规则 ```powershell cargo run -p dht-search -- --config config.toml ``` 相对目录以 `config.toml` 所在目录为基准解析 也可以通过命令行覆盖数据目录 ```powershell cargo run -p dht-search -- --data-dir D:\data\dht-search ``` 服务持续运行直到收到 Ctrl+C SIGINT 或 SIGTERM 生产 Web 页面需要先在 `src/web` 目录执行 `bun run build` Axum 会从 `http.web_dir` 提供构建结果 ### 当前运行模板网络配置 `config.toml` 默认使用经过本机一分钟资源测试的 Bitmagnet 等效激进配置 主动 DHT 查询仍共享 `max_outbound_queries_per_second` 总预算 所有队列保持有界 可以按设备和网络条件主动下调 | 配置项 | 运行模板值 | 作用 | |---|---:|---| | `max_outbound_queries_per_second` | `1000` | 三类主动 DHT UDP 查询的合计每秒速率硬上限 | | `outbound_query_burst` | `200` | 空闲后允许立即消费的 UDP 查询数 | | `find_node_queries_per_second` | `10` | `find_node` 自身速率上限 | | `find_node_max_in_flight` | `100` | 同时等待响应的 `find_node` 数量 | | `new_destinations_per_minute` | `12000` | 每分钟首次探测的新 UDP 目标数量 | | `peer_lookups_per_second` | `200` | 每秒启动的 infohash Peer 查找数量 | | `peer_lookup_max_active` | `200` | 同时运行的 Peer 查找数量 | | `sample_queries_per_second` | `60` | BEP-51 采样查询速率 | | `sample_max_in_flight` | `100` | 同时等待响应的 BEP-51 采样请求数量 | | `sample_new_node_percent` | `50` | 按节点地址稳定分流到直接采样通道的比例 | | `sample_candidate_queue_capacity` | `8192` | 新发现节点直接采样通道的有界容量 | | `sample_fallback_to_iterative` | `false` | 采样节点没有返回 Peer 时快速结束以扩大覆盖面 | | `metadata_workers` | `400` | 同时处理的 Metadata 任务数量 | | `metadata_connects_per_second` | `400` | 每秒真正开始的 Peer TCP 连接硬上限 | Metadata 下载和可用性握手共用 `metadata_connects_per_second` 预算不会各自叠加 ### 按需可用性验证 搜索结果和详情访问只会把已过冷却期的种子异步加入持久化验证队列 HTTP 响应不会等待 DHT 或 Peer 网络 | 配置项 | 默认值 | 作用 | |---|---:|---| | `verification.enabled` | `true` | 是否启用按需可用性验证 | | `verification.queue_capacity` | `10000` | 持久化验证队列容量 | | `verification.max_active` | `8` | 同时验证的种子数量 | | `verification.max_peer_attempts` | `3` | 每个种子最多握手的 Peer 数量 | | `verification.lease_secs` | `60` | 异常退出后验证任务重新可领取的租约时间 | | `verification.poll_interval_millis` | `250` | 持久化队列轮询间隔 | 详情访问使用高优先级 搜索结果使用普通优先级 队列满时高优先级可以替换最旧普通任务 可用性分为 `unknown` `active` 和 `possibly_stale` 一次或多次验证失败只表示当前可能没有可连接 Peer 不会删除种子 新抓取记录会把成功下载 Metadata 的来源 Peer 视为一次有效验证 旧记录按需复查时会同时使用新 DHT 结果和已保存的成功来源 Peer 热度是近期 DHT 发现强度最近出现时间和可连接 Peer 数的综合活跃度分数 不代表全球下载量 这组参数仍保留全局速率限制 如果同机代理或路由器再次出现不稳定应优先降低总 UDP 预算和 Metadata 建连速率 Windows 下索引每五秒批量提交 临时文件占用会自动指数退避重试且不会停止采集 HTTP 服务或丢失 RocksDB 待索引状态 ### 磁盘空间保护 应用固定每分钟检查 `data_dir` 所在文件系统的总容量和剩余空间 低于保护阈值时先停止接收新 Metadata DHT 状态更新索引任务和可用性验证任务 已经进入有界持久化队列的记录会继续排空 随后进入只读保护 只读保护始终启用且不需要用户配置 保护阈值取文件系统总容量的 5% 并限制在 512 MiB 到 20 GiB 之间 恢复缓冲取总容量的 1% 并限制在 256 MiB 到 5 GiB 之间 文件系统扩容后会自动采用新阈值 只读保护期间 RocksDB 和 Tantivy 不再产生业务写入 现有搜索详情健康检查和 Web 页面仍然可用 剩余空间达到自动恢复阈值后重新接受写入 使用两个阈值可以避免临界空间附近反复暂停和恢复 磁盘空间探测失败时采用保守策略进入保护状态 `/stats` 返回 `disk_state` `disk_available_bytes` 阈值 活跃写入数 探测失败数 状态转换数和拒绝任务数 Web 运行状态使用绿色或黄色状态点展示正常与保护状态 ### 日志轮转和保留 应用会在读取配置后初始化日志并始终输出到终端 因此 Docker 可以直接通过 `docker logs` 读取运行日志 文件日志默认同时写入 `data/logs` 并允许从 Web 高级设置关闭 | 配置项 | 默认值 | 作用 | |---|---:|---| | `logging.directory` | `data/logs` | 日志文件目录 相对主配置文件解析 | | `logging.file_enabled` | `true` | 启用滚动文件日志 | | `logging.rotation` | `daily` | 轮转周期 支持 `minutely` `hourly` `daily` 和 `never` | | `logging.retain_files` | `7` | 最多保留的匹配日志文件数量 | | `logging.file_prefix` | `dht-search` | 日志文件名前缀 | 默认按天轮转时保留 7 个文件约等于保留最近 7 天 日志组件只清理同目录中同时匹配前缀和 `.log` 后缀的普通文件 不删除目录和符号链接 清理失败会输出错误但不会让服务退出 关闭文件日志不会影响终端输出 ### RocksDB 检查点备份和恢复 RocksDB 是唯一权威数据源 应用使用 RocksDB 原生 Checkpoint API 在线生成一致快照 备份期间采集可以继续运行 Tantivy 不进入备份因为它可以从 RocksDB 完整重建 | 配置项 | 默认值 | 作用 | |---|---:|---| | `backup.enabled` | `false` | 是否启用自动检查点 默认关闭且不在 Web 展示 | | `backup.directory` | `data/backups` | 检查点目录 相对主配置文件解析 | | `backup.interval_secs` | `21600` | 每 6 小时创建一次检查点 | | `backup.retain_checkpoints` | `3` | 保留最近 3 个自动检查点 | | `backup.create_on_start` | `true` | 每次启动后立即创建一次检查点 | 需要自动检查点时可以直接修改 `config.toml` 备份目录与数据目录位于同一磁盘时 RocksDB 会尽量通过硬链接减少复制开销 放到其他磁盘时可能复制全部数据库文件 创建前会检查备份磁盘剩余空间 磁盘保护期间自动跳过而不会阻塞服务 `/stats` 返回检查点成功失败跳过清理数量 最近成功时间耗时和记录数量 自动清理只处理名称严格匹配 `checkpoint-` 加二十位时间戳的直接子目录 不会删除手工目录文件或符号链接 恢复必须在服务停止后执行 数据目录独占锁会阻止运行中的服务和恢复命令同时操作 ```powershell target\release\dht-search.exe ` --config config.toml ` --restore-checkpoint data\backups\checkpoint-00000001775400000000 ``` 恢复命令会先以只读方式校验源检查点 再复制到数据目录内的暂存目录并二次校验 然后切换 `rocksdb` 目录 原数据库不会删除而是保留为 `rocksdb.pre-restore-*` 方便人工回滚 Tantivy 作为派生数据会被删除 下次正常启动自动从恢复后的 RocksDB 重建 确认恢复数据无误后可以人工删除 `rocksdb.pre-restore-*` 释放空间 不要在服务运行时移动或删除这些目录 ### 运行诊断历史 应用把可删除的运行资源采样写入独立的 `data/diagnostics.sqlite3` RocksDB 仍然是唯一业务权威数据源 删除诊断数据库不会影响种子数据搜索索引或恢复 | 配置项 | 默认值 | 作用 | |---|---:|---| | `diagnostics.enabled` | `true` | 是否采集并保存运行诊断历史 | | `diagnostics.database` | `data/diagnostics.sqlite3` | 独立 SQLite 数据库路径 | | `diagnostics.sample_interval_secs` | `10` | 实时资源采样间隔 | | `diagnostics.raw_retention_hours` | `24` | 原始采样保留时间 | | `diagnostics.minute_retention_days` | `30` | 每分钟快照保留时间 | | `diagnostics.queue_capacity` | `128` | SQLite writer 有界队列容量 | SQLite 使用 WAL 和单独 writer 线程 原始采样超过 24 小时自动删除 同一分钟只保留最新快照且超过 30 天自动删除 写入失败不会停止采集和搜索核心服务 诊断快照包含进程内存 CPU 时间线程句柄 DHT 流量队列深度 RocksDB Block Cache MemTable Compaction 和 SST 状态 Tantivy Writer 预算和提交耗时以及 HTTP 并发错误分类平均 P95 和最大延迟 ### 配置管理 配置文件只是强类型配置 DTO 的 TOML 持久化形式 服务通过 `GET /config` 返回当前文件配置和稳定修订号 通过 `PUT /config` 接受完整 DTO 更新前会执行与启动时相同的完整校验 保存时先在同目录写入并同步临时文件再原子替换目标文件 旧修订号返回 `409 Conflict` 防止多个页面互相覆盖 除 `dht.enabled` 外当前版本不在线修改正在运行的 DHT 存储索引和监听器 保存其他配置后返回 `restart_required = true` 并在重启服务后统一生效 命令行覆盖字段也会单独返回并继续优先于文件配置 诊断页通过 `/collectors` 选择集群总览或单个采集器并展示对应 DHT Peer 和进程指标 独立采集器页面负责按节点启停与修改配置 每个节点拥有独立修订号并由协调器集中持久化 通过 Web 保存会按 DTO 重新生成 TOML 原有手写注释不会保留 管理接口默认随 HTTP 服务提供 因此生产部署不应把 `/config` 暴露到不受信任的公网入口 ### Metadata 安全限制 应用会在 Metadata 下载和进入 RocksDB 前执行两层资源与结构校验 | 配置项 | 默认值 | 作用 | |---|---:|---| | `metadata_limits.max_metadata_bytes` | `10485760` | 下载阶段允许的最大 info 字典字节数 | | `metadata_limits.max_files` | `20000` | 单个种子允许的最大文件数量 | | `metadata_limits.max_name_bytes` | `1024` | 种子名称最大 UTF-8 字节数 | | `metadata_limits.max_path_bytes` | `4096` | 单个文件路径最大 UTF-8 字节数 | | `metadata_limits.max_path_depth` | `64` | 单个文件路径最大目录层级 | 空名称 空文件列表 控制字符 空路径段 `.` `..` 大小溢出和声明总大小不一致会被分类拒绝 通过完整 Metadata 校验后被拒绝的 infohash 只在 RocksDB 保存原因规则指纹时间和次数 不保存名称或文件列表 相同规则下再次发现时不会重复下载 修改限制后规则指纹变化并允许重新判断 `/stats` 返回 `metadata_filtered` 总数以及 `metadata_filtered_*` 分类计数 Web 运行状态展示本次运行的过滤总数 可以通过命令行覆盖独立数据目录进行测试 不会污染正式数据目录 ```powershell cargo run -p dht-search -- --config config.toml --data-dir data-filter-test Invoke-RestMethod http://127.0.0.1:8080/stats | ConvertTo-Json -Depth 5 ``` ### 无效文件隐藏规则 `config.toml` 的 `content_filter` 区域控制哪些种子标题和内部文件不参与详情与搜索 默认内部规则会隐藏 BitComet `_____padding_file_` 文件以及 `.pad` 和 `.____padding_file` 填充目录 RocksDB 始终保存完整原始 Metadata 用户隐藏规则不会删除文件或种子也不会改变 `content_key` 和内容组成员关系 标题命中或全部内部文件被隐藏时只隐藏对应 infohash 版本 同组其他版本仍可展示 配置只包含 `torrent_name_patterns` 和 `file_patterns` 两组规则 内部文件规则同时检查 basename 和规范化完整路径 Web 使用两个多行文本框编辑并按行切分 空行和重复规则自动忽略 每行默认是不区分大小写的通配符 `*` 表示任意长度字符 `?` 表示一个字符并匹配完整字段 使用 `regex:` 前缀可以编写 Rust `regex` 语法的正则表达式 规则保存后详情立即使用最新投影 后台扫描内容组并以投影哈希只提交真正变化的 Tantivy 文档 扫描游标和待删除任务持久化且允许连续保存时自动收敛到最新规则 `/stats.filter` 与 Web 系统页展示进度 内部文件隐藏后列表只返回可见文件但总大小和原始文件数量保持 Metadata 原值 `visible_file_count` 单独用于详情分页 ### 采样去重和 Peer 查找 BEP-51 返回的 infohash 会先进入有界批量准入队列并由 RocksDB 精确判断 新发现节点按地址稳定分流 同一地址只进入直接采样或 `find_node` 通道 直接采样队列满时会安全回退到抓取池 `/stats` 会分别报告候选队列直接采样请求响应 Hash 去重丢弃以及成功 Metadata 的网络来源 已有 infohash 只更新最后发现时间和发现次数不会再次执行 Peer Lookup 未知 infohash 首先只向返回样本的 DHT 节点查询一次 `get_peers` 只有单点查询没有返回 Peer 时才降级为有限迭代查找 `/stats` 中的 `sampled_hashes_filtered` `peer_lookup_preferred_succeeded` 和 `peer_lookup_fallbacks` 用于观察提前去重和单点优先效果 ## Linux 资源限制 生产环境仍可能同时使用较多 TCP socket Linux 生产运行必须把文件描述符上限提高到至少 65536 ```bash prlimit --nofile=65536:65536 -- \ /opt/dht-search/dht-search --config /opt/dht-search/config.toml ``` systemd 服务需要设置 ```ini [Service] LimitNOFILE=65536 ``` 文件描述符上限过低时 DHT 连接会挤占 Tantivy 和 RocksDB 打开文件所需的描述符并导致服务安全停止 ## API 默认只监听 `127.0.0.1:8080` ```text GET /health GET /ready GET /stats GET /diagnostics/current GET /diagnostics/history?range_secs=3600&resolution=raw GET /diagnostics/history?range_secs=86400&resolution=minute GET /config PUT /config GET /search?q=ubuntu&offset=0&limit=20 GET /search?q=%2A.iso GET /search?q=&min_size=1048576&max_size=10737418240&extension=mkv GET /search?q=流浪地球&min_files=1&availability=active&heat=hot&sort=heat GET /contents/{content_key}?offset=0&limit=20 GET /torrents/{infohash}?file_offset=0&file_limit=100 ``` `limit` 被限制在 1 到 100 之间且 `offset` 最大为 10000 搜索支持中文英文数字和文件名片段匹配 搜索支持不区分大小写的单扩展名快捷语法 例如 `*.iso` 直接匹配包含 ISO 文件的已索引内容 `Ubuntu *.iso` 表示普通关键词和扩展名条件同时满足 普通关键词继续匹配标题别名文件名和路径片段 搜索接口不接受正则模式 `mode` 参数 也不接受 `*iso` `ubuntu*` `?` 或多段扩展名等任意通配符表达式 过滤参数还包括 `min_files` `max_files` `first_seen_after` `first_seen_before` `last_seen_after` `last_seen_before` `availability` 和 `heat` 排序支持 `relevance` `latest` `oldest` `heat` `size_desc` `size_asc` 和 `discoveries` 有关键词时默认按相关性排序 空查询默认按最近发现排序 搜索结果按 `content_key` 精确折叠并通过 `variant_count` 返回变体数量 `/contents/{content_key}` 用于分页查看全部 infohash 和磁力链接 搜索响应包含 `heat` 和 `availability` 摘要 详情响应包含完整验证时间 Peer 数和连续失败次数 详情文件列表默认返回 100 条且单次最多 200 条 使用 `file_offset` 翻页避免超大种子一次向浏览器返回全部文件 生产 Web 页面使用 `/` `/system` `/workers` 和 `/settings` 四个路由 分别提供搜索运行诊断采集器管理和服务配置 API 路径继续保持独立避免单页回退冲突 ## 数据恢复 RocksDB 是权威数据源而 Tantivy 是可重建索引 当 Tantivy Schema 或索引文档格式变化以及索引缺失损坏时 应用在独立代际目录构建影子索引 旧索引继续提供搜索且新收录内容在切换后统一可见 影子索引清空 RocksDB 待索引状态并通过文档数校验后原子更新活动指针 影子索引通过内部构建清单跨重启恢复 构建失败磁盘保护或进程退出不会删除活动索引 没有旧索引时会提供正在初始化的部分结果并明确标记结果尚不完整 切换成功后立即清理旧索引 Windows 文件占用造成的清理失败只记录警告而不影响服务 `/stats` 的 `index` 字段区分已保存 infohash 可搜索内容组活动索引文档影子索引文档和待索引数量 并返回重建原因状态进度开始完成时间和错误 每个内容组按文件大小降序和规范化路径升序选择最多 2048 个文件 完整路径文本总预算为 256 KiB 被选择文件的名称路径和扩展名参与搜索 文件总数大小和详情仍使用全部可见文件 默认相关性优先完整种子名称 名称片段 别名 文件名和完整路径 热度与最后发现时间只用于文本同分结果 用户显式选择的时间热度大小和发现次数排序不变 项目当前处于开发阶段 持久化结构变化时直接清理测试数据重新采集 不维护旧测试数据库兼容层 正常退出会先停止 DHT 和诊断采样 再排空持久化队列提交剩余索引最后关闭 HTTP 服务 ## 规模基准 `dht-benchmark` 使用确定性数据调用真实 RocksDB 写入内容聚合待索引状态 Tantivy 索引和搜索接口 默认每十条记录生成一个相同内容的不同 infohash 用于同时覆盖精确去重和内容折叠场景 性能测量必须使用 release 构建并从小规模逐步增加 ```powershell $env:LIBCLANG_PATH = "$PWD\.tools\libclang\clang\native" cargo run --release -p dht-search --bin dht-benchmark -- ` --records 10000 ` --cleanup cargo run --release -p dht-search --bin dht-benchmark -- ` --records 100000 ` --query-iterations 100 ` --cleanup cargo run --release -p dht-search --bin dht-benchmark -- ` --records 1000000 ` --query-iterations 100 ``` | 参数 | 默认值 | 作用 | |---|---:|---| | `--records` | `10000` | 生成记录数量 上限一千万 | | `--generation-batch-size` | `1000` | 单批生成并暂存在内存的记录数量 | | `--index-batch-size` | `1000` | 每次 Tantivy 提交的内容文档数量 | | `--files-per-record` | 不指定 | 为大型文件集合基准固定每条记录的文件数量 范围 1 到 20000 | | `--index-max-retries` | `20` | Windows 临时 IO 错误的最大连续重试次数 | | `--query-iterations` | `50` | 每类查询正式采样次数 | | `--query-warmup` | `5` | 每类查询预热次数 | | `--duplicate-every` | `10` | 每多少条创建一个相同内容的不同 infohash 零表示禁用 | | `--file-name-ngram-max` | `8` | 实验文件名 N-Gram 最大长度 只支持 4 或 8 | | `--file-name-ngram-positions` | 不启用 | 为 `2..4` 实验记录位置并使用长关键词短语查询 | | `--file-name-ngram-split` | 不启用 | 将 2 至 3 字符 Basic 倒排与四字符位置字段拆分且必须同时启用位置索引 | | `--output-dir` | `benchmark-data` | 独立运行数据和 JSON 报告根目录 | | `--cleanup` | 不启用 | 报告写入后删除本次 RocksDB 和 Tantivy 数据 | 终端和 JSON 报告包含写入吞吐索引吞吐查询平均值与 P50/P95/P99 RocksDB 与 Tantivy 字节占用每条平均占用和进程峰值内存 `benchmark-data/runs` 保存每次未清理的数据库和索引 `benchmark-data/reports` 始终保留 JSON 报告 两者均不进入 Git