OpenHarmony 统一互联开发指导——富对富文件互传
OpenHarmony 统一互联开发指导——富对富文件互传
适用版本:OpenHarmony 4.1 Release 及以上(API 11+),文中接口以 API 12 为准验证环境:两台 RK3568 开发板、OpenHarmony 5.0 Release、DevEco Studio 5.0 Release说明:本文基于应用沙箱的分布式文件目录实现「富设备对富设备」文件互传,属于统一互联底座能力的应用侧落地方式,不依赖厂商私有通道。
1 概述
1.1 场景
统一互联(All Devices, One Connect)面向跨设备、跨厂商的互联互通。富对富文件互传是其中最基础、使用频率最高的场景之一:
-
手机与平板之间互传照片、文档;
-
大屏与 PC 之间同步演示材料;
-
车机与手机之间交换行车记录、离线地图包;
-
办公场景下多台 OpenHarmony 设备共享工程文件。
典型诉求:
| 诉求 | 说明 |
|---|---|
| 免配网 | 设备已互信时,打开应用即可看到对方,不需要扫码或输密码 |
| 大文件 | 支持 GB 级文件,不能一次性读入内存 |
| 可恢复 | 断网、切后台、重启后能续传,不从头重来 |
| 可校验 | 传输完成后能验证内容一致性,而不是只看「进度 100%」 |
| 可清理 | 中间分片自动回收,不长期占用分布式目录 |
1.2 能力边界
本文方案的能力边界需要先说清楚,避免过度承诺:
-
传输通道:使用应用沙箱的分布式文件目录(
context.distributedFilesDir),由分布式文件系统在多设备间同步。它不是专门的「点对点 socket 协议」,因此没有连接句柄、没有服务端端口; -
访问前提:两台设备必须已完成互信(同账号或系统设置中确认),并且安装同一 bundleName、同一签名的应用;
-
一致性:分布式文件系统保证最终一致,不保证「写完立即可见」。应用层必须用「manifest 提交标记 + 分片大小/哈希校验」判断文件是否真正就绪;
-
性能:受 Wi-Fi 带宽与文件系统同步策略影响,单任务串行分片通常比多任务并发更稳定;大文件建议在前台或申请长时任务;
-
安全:分布式目录仅在可信设备间同步,不应把敏感明文长期留在该目录,任务完成后要及时清理。
2 技术架构
┌──────────────────────────────────────────────────────┐ │ 业务层:文件选择、进度展示、失败重试、历史记录 │ ├──────────────────────────────────────────────────────┤ │ 传输层:任务清单(manifest) + 定长分片 + 完成标记(ack) │ ├──────────────────────────────────────────────────────┤ │ 存储层:distributedFilesDir/transfer/<taskId>/ │ │ ├── chunk_000000.part ... │ │ ├── manifest.json (最后写入,提交标记) │ │ └── ack.json (接收端回执) │ ├──────────────────────────────────────────────────────┤ │ 互联底座:分布式文件系统 + 分布式设备管理 + 软总线 │ └──────────────────────────────────────────────────────┘
设计要点:
-
清单先行、提交后置:先写全部分片,最后写
manifest.json。接收端只在看到manifest.json后才开始处理,天然避免读到半包任务; -
分片定长:除最后一片外,每片大小固定,便于计算偏移、校验完整性和断点续传;
-
回执闭环:接收端校验通过后写
ack.json,发送端据此把任务标记为「已完成」,而不是写完就认为成功; -
幂等处理:同一
taskId重复扫描不重复合并,已存在的目标文件按策略覆盖或重命名。
3 前提条件与权限
3.1 权限与配置
module.json5:
{
"module": {
"name": "entry",
"requestPermissions": [
{
"name": "ohos.permission.DISTRIBUTED_DATASYNC",
"reason": "$string:reason_distributed_datasync",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
"reason": "$string:reason_background_transfer",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
-
DISTRIBUTED_DATASYNC:user_grant,必须在运行时动态申请; -
KEEP_BACKGROUND_RUNNING:需要配合backgroundTaskManager申请长时任务,仅在「传输中切后台」时使用,任务结束必须释放,否则会被系统回收甚至限制。
3.2 设备与账号
| 条件 | 检查方式 |
|---|---|
| 同一局域网 | 两台设备连接同一 SSID、同一网段 |
| 已互信 | 系统「设备互联」中双向可见,应用侧 getTrustedDeviceListSync() 能查到 |
| 同一应用 | 两端 bundleName、签名证书一致 |
| 系统版本 | 大版本尽量一致,避免能力位差异 |
设备发现与互信判断的完整实现见配套文章《OpenHarmony 统一互联设备发现不到对端设备问题分析报告》,本文直接复用其中的 UnifiedDeviceService。
4 端到端流程
发送端 接收端 │ │ ├─1. 发现设备 / 校验可信 ──────┤ │ │ ├─2. 生成 taskId、创建目录 ────┤ │ │ ├─3. 写入 chunk_*.part ────────┼─→ 分布式文件系统同步 │ │ ├─4. 写入 manifest.json ───────┼─→ 5. 轮询发现 manifest │ │ │ ├─6. 校验分片完整性/大小 │ │ │ ├─7. 合并为目标文件 + SHA-256 校验 │ │ │ ├─8. 写入 ack.json ──┐ │ │ │ ├─9. 轮询 ack.json ←───────────┼─────────────────────┘ │ │ ├─10. 清理 outbox 分片 ────────┼─→ 11. 清理已处理任务
时序上的两个「等待」必须用轮询实现,而不是回调:
-
发送端等
ack.json:分布式文件系统没有跨设备文件变更回调; -
接收端等分片齐全:manifest 可见时,部分分片可能尚未同步完成。
轮询间隔建议 2~3 秒,单任务超时 10 分钟,超时后进入可重试状态。
5 关键实现
5.1 任务模型与路径
import { common } from '@kit.AbilityKit';
export const CHUNK_SIZE: number = 4 * 1024 * 1024;
export class TransferManifest {
taskId: string = '';
fileName: string = '';
fileSize: number = 0;
chunkSize: number = CHUNK_SIZE;
chunkCount: number = 0;
sha256: string = '';
createdAt: number = 0;
senderName: string = '';
}
export class TransferAck {
taskId: string = '';
success: boolean = false;
message: string = '';
finishedAt: number = 0;
}
export class TransferPaths {
static root(context: common.UIAbilityContext): string {
return `${context.distributedFilesDir}/transfer`;
}
static taskDir(context: common.UIAbilityContext, taskId: string): string {
return `${TransferPaths.root(context)}/${taskId}`;
}
static chunkPath(taskDir: string, index: number): string {
return `${taskDir}/chunk_${index.toString().padStart(6, '0')}.part`;
}
static manifestPath(taskDir: string): string {
return `${taskDir}/manifest.json`;
}
static ackPath(taskDir: string): string {
return `${taskDir}/ack.json`;
}
}
说明:
-
context.distributedFilesDir是应用沙箱内的分布式文件目录,只有同一应用(同 bundleName + 同签名)在可信设备上可访问; -
任务目录按
taskId隔离,避免多任务互相干扰; -
分片文件名使用 6 位定长序号,保证字典序与数值序一致,便于排查。
5.2 发送端:分片写入与提交
import { fileIo as fs } from '@kit.CoreFileKit';
import { hash } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { util } from '@kit.ArkTS';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = 'UnifiedTransfer';
export class FileSender {
private context: common.UIAbilityContext;
constructor(context: common.UIAbilityContext) {
this.context = context;
}
async send(srcPath: string): Promise<TransferManifest> {
const stat = await fs.stat(srcPath);
if (stat.size <= 0) {
throw new Error('empty file');
}
const fileName = srcPath.substring(srcPath.lastIndexOf('/') + 1);
const taskId = `${Date.now()}_${Math.floor(Math.random() * 100000)}`;
const taskDir = TransferPaths.taskDir(this.context, taskId);
await fs.mkdir(taskDir, true);
const manifest = new TransferManifest();
manifest.taskId = taskId;
manifest.fileName = fileName;
manifest.fileSize = stat.size;
manifest.chunkSize = CHUNK_SIZE;
manifest.chunkCount = Math.ceil(stat.size / CHUNK_SIZE);
manifest.sha256 = (await hash.hash(srcPath, 'sha256')).toLowerCase();
manifest.createdAt = Date.now();
manifest.senderName = 'local';
const src = await fs.open(srcPath, fs.OpenMode.READ_ONLY);
try {
for (let i = 0; i < manifest.chunkCount; i++) {
const offset = i * manifest.chunkSize;
const length = Math.min(manifest.chunkSize, manifest.fileSize - offset);
const buffer = new ArrayBuffer(length);
await fs.read(src.fd, buffer, { offset: offset, length: length });
const dst = await fs.open(
TransferPaths.chunkPath(taskDir, i),
fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC
);
try {
await fs.write(dst.fd, buffer);
} finally {
await fs.close(dst);
}
}
} finally {
await fs.close(src);
}
// 最后写入 manifest,作为「任务提交」标记
await this.writeText(TransferPaths.manifestPath(taskDir), JSON.stringify(manifest));
hilog.info(0x0000, TAG, 'submit task %{public}s, chunks=%{public}d', taskId, manifest.chunkCount);
return manifest;
}
async waitAck(manifest: TransferManifest, timeoutMs: number = 10 * 60 * 1000): Promise<boolean> {
const taskDir = TransferPaths.taskDir(this.context, manifest.taskId);
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const text = await this.readText(TransferPaths.ackPath(taskDir));
const ack = JSON.parse(text) as TransferAck;
if (ack.taskId === manifest.taskId) {
return ack.success;
}
} catch (err) {
// ack 尚未同步过来,继续等待
}
await this.sleep(2000);
}
return false;
}
private async writeText(path: string, text: string): Promise<void> {
const file = await fs.open(path, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC);
try {
await fs.write(file.fd, text);
} finally {
await fs.close(file);
}
}
private async readText(path: string): Promise<string> {
const stat = await fs.stat(path);
const buffer = new ArrayBuffer(stat.size);
const file = await fs.open(path, fs.OpenMode.READ_ONLY);
try {
await fs.read(file.fd, buffer, { offset: 0, length: stat.size });
} finally {
await fs.close(file);
}
return util.TextDecoder.create('utf-8').decodeToString(new Uint8Array(buffer));
}
private sleep(ms: number): Promise<void> {
return new Promise<void>((resolve: () => void) => {
setTimeout(resolve, ms);
});
}
}
实现要点:
-
只读源文件一次:
fs.open后按偏移读取,避免反复打开; -
逐片写入,不并发:并发写多个分片会加剧分布式文件系统同步压力,实际测试中串行更稳定;
-
TRUNC不可省:重试写同一分片时,若不加TRUNC,旧内容尾部会残留,导致大小校验失败; -
错误要向上抛:
send失败时由业务层展示重试入口,不要静默吞掉BusinessError; -
waitAck用轮询:分布式目录没有文件变更监听,轮询是必要成本。
5.3 接收端:扫描、校验、合并
import { fileIo as fs } from '@kit.CoreFileKit';
import { hash } from '@kit.CoreFileKit';
import { util } from '@kit.ArkTS';
import { common } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
export class FileReceiver {
private context: common.UIAbilityContext;
private timer: number = -1;
private handling: Set<string> = new Set<string>();
constructor(context: common.UIAbilityContext) {
this.context = context;
}
start(onTaskDone: (manifest: TransferManifest, path: string) => void): void {
this.timer = setInterval(() => {
this.scan(onTaskDone);
}, 3000);
}
stop(): void {
if (this.timer >= 0) {
clearInterval(this.timer);
this.timer = -1;
}
}
private async scan(onTaskDone: (manifest: TransferManifest, path: string) => void): Promise<void> {
const root = TransferPaths.root(this.context);
let taskIds: string[] = [];
try {
taskIds = await fs.listFile(root);
} catch (err) {
// 首次运行目录尚未创建,属于正常情况
return;
}
for (const taskId of taskIds) {
if (this.handling.has(taskId)) {
continue;
}
const taskDir = `${root}/${taskId}`;
try {
const ackStat = await fs.stat(TransferPaths.ackPath(taskDir));
if (ackStat.size > 0) {
continue; // 已处理
}
} catch (err) {
// 无 ack,继续处理
}
try {
const manifestText = await this.readText(TransferPaths.manifestPath(taskDir));
const manifest = JSON.parse(manifestText) as TransferManifest;
this.handling.add(taskId);
const target = await this.merge(manifest, taskDir);
await this.writeAck(taskDir, manifest.taskId, true, 'ok');
onTaskDone(manifest, target);
hilog.info(0x0000, 'UnifiedTransfer', 'merged %{public}s -> %{public}s', taskId, target);
} catch (err) {
// 分片未同步完成或校验失败:本轮跳过,下一轮重试
hilog.warn(0x0000, 'UnifiedTransfer', 'task %{public}s not ready', taskId);
} finally {
this.handling.delete(taskId);
}
}
}
private async merge(manifest: TransferManifest, taskDir: string): Promise<string> {
const targetDir = `${this.context.filesDir}/recv`;
await fs.mkdir(targetDir, true);
const targetPath = `${targetDir}/${manifest.fileName}`;
const dst = await fs.open(targetPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC);
try {
for (let i = 0; i < manifest.chunkCount; i++) {
const chunkPath = TransferPaths.chunkPath(taskDir, i);
const stat = await fs.stat(chunkPath);
const expected = this.expectedSize(manifest, i);
if (stat.size !== expected) {
throw new Error(`chunk ${i} size mismatch: ${stat.size}/${expected}`);
}
const buffer = new ArrayBuffer(expected);
const src = await fs.open(chunkPath, fs.OpenMode.READ_ONLY);
try {
await fs.read(src.fd, buffer, { offset: 0, length: expected });
} finally {
await fs.close(src);
}
await fs.write(dst.fd, buffer);
}
} finally {
await fs.close(dst);
}
const digest = (await hash.hash(targetPath, 'sha256')).toLowerCase();
if (digest !== manifest.sha256) {
await fs.unlink(targetPath);
throw new Error('sha256 mismatch');
}
return targetPath;
}
private expectedSize(manifest: TransferManifest, index: number): number {
if (index < manifest.chunkCount - 1) {
return manifest.chunkSize;
}
return manifest.fileSize - manifest.chunkSize * (manifest.chunkCount - 1);
}
private async writeAck(taskDir: string, taskId: string, success: boolean, message: string): Promise<void> {
const ack = new TransferAck();
ack.taskId = taskId;
ack.success = success;
ack.message = message;
ack.finishedAt = Date.now();
const file = await fs.open(TransferPaths.ackPath(taskDir), fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC);
try {
await fs.write(file.fd, JSON.stringify(ack));
} finally {
await fs.close(file);
}
}
private async readText(path: string): Promise<string> {
const stat = await fs.stat(path);
const buffer = new ArrayBuffer(stat.size);
const file = await fs.open(path, fs.OpenMode.READ_ONLY);
try {
await fs.read(file.fd, buffer, { offset: 0, length: stat.size });
} finally {
await fs.close(file);
}
return util.TextDecoder.create('utf-8').decodeToString(new Uint8Array(buffer));
}
}
关键设计:
-
ack.json作为幂等标记:已存在则跳过,避免重复合并且产生同名覆盖; -
handling集合防重入:轮询间隔短于合并耗时时,同一任务不会被重复处理; -
大小校验代替「分片数量够」:分布式文件系统先创建元数据再写内容,只判断文件存在会读到空片;
-
哈希不匹配就删除目标文件:避免把损坏文件交付给业务层;
-
失败不写 ack:保留重试机会,直到超时由业务层提示用户。
5.4 断点续传
上面的实现已经具备「任务级重试」:分片未同步齐时整轮跳过,下一轮继续。若要支持「进程重启后续传」,需要额外落盘校验状态:
export class TransferState {
taskId: string = '';
verifiedChunkCount: number = 0;
updatedAt: number = 0;
}
实现思路:
-
接收端每校验通过一个分片,就把
verifiedChunkCount写入state.json; -
重启后从
verifiedChunkCount继续合并,已校验分片不重复读取; -
发送端如支持中断续传,可按分片索引跳过已成功写入且大小正确的分片;
-
state.json与分片在同一任务目录,随任务清理一起删除。
注意:断点续传的前提是源文件未变化。发送端应在 manifest 中记录文件大小、修改时间与哈希,续传前重新计算哈希,不一致则重新建任务。
5.5 进度上报
发送端可在每个分片写完后回调业务层:
export type ProgressCallback = (taskId: string, finished: number, total: number) => void;
界面侧按 1 秒节流刷新,避免每个分片都触发一次完整 UI 重绘:
private lastUpdate: number = 0;
private onProgress(taskId: string, finished: number, total: number): void {
const now = Date.now();
if (now - this.lastUpdate < 1000 && finished < total) {
return;
}
this.lastUpdate = now;
this.progress = Math.floor(finished * 100 / total);
}
进度只反映「已写入分片数 / 总分片数」,不代表对端已接收。界面应区分两个状态:
-
发送中:本端分片写入进度;
-
已完成:收到
ack.json且success === true。
5.6 后台传输与资源释放
大文件传输耗时较长,切后台时容易被冻结:
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { wantAgent, WantAgent } from '@kit.AbilityKit';
import { common } from '@kit.AbilityKit';
export class TransferKeepAlive {
private context: common.UIAbilityContext;
private running: boolean = false;
constructor(context: common.UIAbilityContext) {
this.context = context;
}
async start(): Promise<void> {
if (this.running) {
return;
}
const info: wantAgent.WantAgentInfo = {
wants: [
{
bundleName: this.context.abilityInfo.bundleName,
abilityName: this.context.abilityInfo.name
}
],
actionType: wantAgent.OperationType.START_ABILITY,
requestCode: 0,
actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
};
const agent: WantAgent = await wantAgent.getWantAgent(info);
await backgroundTaskManager.startBackgroundRunning(
this.context,
backgroundTaskManager.BackgroundMode.DATA_TRANSFER,
agent
);
this.running = true;
}
async stop(): Promise<void> {
if (!this.running) {
return;
}
await backgroundTaskManager.stopBackgroundRunning(this.context);
this.running = false;
}
}
注意事项:
-
长时任务必须在传输开始后申请、传输结束或失败后立即释放;
-
未释放会被系统判定为常驻后台,影响应用后续调度;
-
KEEP_BACKGROUND_RUNNING被拒时,降级为前台传输,并提示用户保持页面在前台。
5.7 清理策略
| 对象 | 时机 | 动作 |
|---|---|---|
| 发送端分片 | 收到 ack 且成功 | 删除整个任务目录 |
| 发送端分片 | 超时/失败 | 保留 24 小时,供重试后清理 |
| 接收端 ack | 发送端确认后 | 随任务目录一起删除 |
接收端 state.json | 任务完成 | 一并删除 |
| 目标文件 | 哈希校验失败 | 立即删除,避免污染业务目录 |
| 历史任务 | 应用启动时 | 扫描超过 7 天的任务目录,统一回收 |
清理必须在传输完成之后执行,不能在写 manifest 后立即删除分片,否则接收端读不到内容。
6 性能与可靠性
6.1 分片大小
| 分片大小 | 优点 | 缺点 | 建议场景 |
|---|---|---|---|
| 1 MB | 首次可见快、失败重传成本低 | 文件数量多,元数据开销大 | 小文件、弱网 |
| 4 MB | 平衡较好 | —— | 默认推荐 |
| 8 MB | 大文件吞吐高 | 单次内存占用大、重传成本高 | 局域网稳定、GB 级文件 |
单次读取会分配同等大小的 ArrayBuffer,因此分片不宜超过 8 MB,否则在 4GB 内存设备上容易出现内存压力。
6.2 可靠性设计清单
-
manifest.json最后写入,作为提交标记; -
每个分片校验大小,不只判断存在;
-
合并后校验整文件 SHA-256,不匹配立即删除;
-
ack.json幂等,重复扫描不重复合并; -
任务级超时(建议 10 分钟)+ 有限重试(建议 3 次);
-
长时任务及时释放;
-
任务目录定期清理,避免分布式目录膨胀;
-
传输前校验目标设备可信,避免写入了却读不到。
6.3 并发策略
-
同一时刻只跑 1~2 个传输任务,避免多个大文件争抢带宽与文件系统同步队列;
-
多文件场景建议排队:小文件优先,大文件串行;
-
需要并行时,按设备维度限流,不要按文件维度无限并发。
7 调试验证
7.1 常用命令
# 查看应用分布式目录(沙箱路径需替换为实际路径) hdc shell "ls -l /data/app/el2/100/base/com.example.unifiedlink/haps/entry/files" # 观察传输相关日志 hdc shell "hilog | grep -iE 'UnifiedTransfer|distributed_file|hmdfs'" # 确认设备互信状态(系统设置中查看,或应用内打印可信列表) hdc shell "ps -ef | grep -i distributed" # 抓取传输期间网络流量(确认是否走 Wi-Fi) hdc shell ifconfig wlan0
7.2 建议用例
| 用例 | 步骤 | 期望 |
|---|---|---|
| 单文件小文件 | 传 100 KB 文本 | 3 秒内完成,哈希一致 |
| 单文件大文件 | 传 1 GB 视频 | 进度单调递增,最终哈希一致 |
| 断网恢复 | 传输中关闭 Wi-Fi,10 秒后恢复 | 任务重试后完成,不产生损坏文件 |
| 切后台 | 传输中按 Home 键 | 有长时任务时继续,无长时任务时暂停不损坏 |
| 重复点击 | 连续点击发送同一文件 | 生成不同 taskId,互不覆盖 |
| 对端未安装 | 目标设备无同签名应用 | 发现阶段即不可见,不产生脏数据 |
| 磁盘不足 | 目标分区剩余空间小于文件大小 | 明确报错,清理半成品 |
| 进程重启 | 传输中杀进程后重启 | 已校验分片可续传,未完成分片重传 |
7.3 验收标准
-
100 次连续传输(混合大小文件)无损坏、无重复落盘;
-
断网/切后台/重启三类异常场景均可恢复或明确失败;
-
传输完成后 SHA-256 与源文件一致率 100%;
-
任务目录在完成后 1 分钟内清理干净;
-
传输期间 UI 帧率不低于 50 FPS,无明显卡顿。
8 常见问题
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 发送端写入成功,接收端一直无任务 | 两端非同一 bundleName/签名,或未互信 | 校验签名与可信列表 |
| manifest 可见但合并失败 | 分片尚未同步完成 | 保留重试,校验分片大小 |
| 合并后哈希不一致 | 分片被覆盖写入、未加 TRUNC、源文件变化 | 重建任务,写分片加 TRUNC |
| 同名文件被覆盖 | 目标目录直接使用原文件名 | 加时间戳或冲突重命名策略 |
| 传输中途卡死 | 分片过大导致内存压力,或后台被冻结 | 降分片大小,申请长时任务 |
| ack 一直等不到 | 接收端校验失败后未写 ack | 失败也写 ack(success=false) 便于定位 |
| 目录越来越大 | 任务完成后未清理 | 启动时扫描并回收历史任务 |
| 换设备后完全不可见 | 设备未互信或网络 AP 隔离 | 参考设备发现排查流程 |
9 最佳实践
-
先互通,再传文件:把设备发现、可信校验封装成独立服务,传输层只接收「可信 deviceId」,不要在传输代码里夹杂发现逻辑;
-
用清单协议而不是裸文件:
manifest + chunk + ack三件套是可恢复、可校验、可幂等的基础; -
所有判断都要可证伪:分片看大小、整文件看哈希、完成看 ack,不要用「进度 100%」代替成功;
-
异常路径优先设计:断网、切后台、进程被杀、磁盘满,这四类场景在富对富传输中出现频率远高于预期;
-
及时清理:分布式目录是共享资源,残留分片既占空间,也会干扰下一轮扫描;
-
统一日志规范:所有日志带
taskId,便于双端对齐时间线;建议同时打印分片索引与大小; -
版本兼容:
manifest增加version字段,后续协议升级时老版本能明确拒绝而不是解析出错。
更多推荐
所有评论(0)