ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

VS Code本地代码自动同步远程机器:SFTP、rsync与排错

VS Code本地代码自动同步远程机器:SFTP、rsync与排错 本地写代码、远端跑程序这个组合几乎每个做后端服务、嵌入式、算法训练、自动化运维的人都绕不开。我最早在实验室带项目时就吃过亏笔记本上改完一个 Python 脚本用 scp 传上去跑出来的结果和本地完全不一样排查了两个小时才发现是远端还留着三天前的老版本文件。后来陆续试过远程开发、SFTP 同步插件、自己写 rsync 守护脚本三种路子各有各的适用边界也各有各的坑。这篇文章就把vscode 实现本地代码自动同步到远程机器这件事彻底拆开讲从为什么要单独做同步这件事到三种主流路线的能力对比再到可以直接抄走的配置文件、rsync 参数选择依据、开机自启的服务写法以及日志显示同步成功但远端文件没变这种经典故障的完整排查链路。不管你是刚接触远程开发的新手还是已经用过一段时间但同步行为总有点飘的老手应该都能在里面找到能直接落地的部分。1. 先算清账本地写码、远端执行为什么手动传文件迟早会翻车很多人一开始都觉得同步是小事改完文件拖一下、传一下不就完了。这个判断在前两周是成立的到第三周就会崩。不是因为操作有多难而是因为人肉同步这件事没有任何可靠性保证它依赖的是你的记忆力而记忆力在赶进度的时候是最先被牺牲的东西。1.1 三种典型场景里同步到底在同步什么先区分清楚场景不然后面选方案会选错。第一种是远端算力型本地机器性能一般训练、编译、压测都在一台配置更好的服务器上跑。这种情况下远端是执行环境本地是编辑环境代码的实际运行依赖、数据、GPU 驱动全在远端。第二种是环境一致性型项目需要特定内核版本、特定 CUDA、特定系统库本机装不上或者装上了会和别的项目打架只能放在一台固定的测试机上。第三种是设备联调型目标是一块开发板、一台工控机、一个边缘盒子本地只能写代码跑必须在那台设备上。这三种场景对同步的要求其实不一样。第一种通常只关心源码目录数据集不动第二种往往连配置文件、脚本、模型权重一起动第三种最麻烦因为设备上的目录结构可能是只读的还可能涉及交叉编译产物。判断自己的场景属于哪一类直接决定了后面是上传整个工作区还是只上传几个子目录是一保存就传还是打个标记再批量传。还有一个容易被忽略的点同步的方向。绝大多数人是本地改、远端跑单向就够但如果你在远端调试时顺手改了线上脚本第二天本地一同步这个改动就被覆盖了。所以从第一天起就要想清楚这条链路是单向覆盖还是双向合并。单向的配置复杂度低一个数量级能单向就绝对不要做双向。1.2 手动 scp、拖拽上传与最后一次忘了传的代价我统计过自己带过的几个项目手动传输导致的返工大致分三类。第一类是漏传改了三四个文件只传了记得住的那两个跑出来的行为和预期不符然后花时间怀疑逻辑、怀疑环境最后发现是文件版本不一致。这类问题的排查成本极高因为它伪装成了业务 Bug。第二类是多传把__pycache__、.venv、node_modules一起怼上去几万个小文件在 SFTP 上一个个建连接慢到怀疑人生还会把远端磁盘塞满。第三类是删不掉本地删掉的旧脚本远端还留着某些框架会自动扫描目录并加载于是幽灵代码继续生效。这种最难查因为它们根本不报错只是悄悄地跑着你以为已经删掉的逻辑。算下来每次手动传输平均 20 秒一天二十次就是六七分钟一年下来是几十个小时而其中任何一次出错带来的排查时间都比省下的操作时间多得多。更重要的是手动同步会让你在心理上不愿意改代码——你会不自觉地攒一批改动再传这就彻底破坏了小步验证的节奏。所以自动同步的核心价值不是省时间而是恢复小步迭代的节奏。2. 路线选型Remote-SSH、SFTP 类插件、rsync 守护各自的能力边界圈子里最常见的三种做法我按代码实际存放在哪、谁提供语言服务、断网了会怎样这三个维度做了区分。选之前一定要想明白一件事你是想要本地有一份、远端也有一份还是只保留远端那一份。这个决定会直接决定后面所有配置。2.1 Remote-SSH把整个工作区搬到远端VS Code 自带的远程开发能力本质上是在远端跑一个轻量的服务端进程编辑器只是前端界面。你在侧边栏里看到的文件树其实是远端磁盘上的文件代码补全、跳转定义、格式化全部由远端的语言服务提供。这类方案最舒服的地方是零同步——因为压根没有两份代码。配置上主要靠~/.ssh/config。把连接信息写成一个 Host 别名比如Host devbox HostName 192.168.1.50 User devuser Port 22 IdentityFile ~/.ssh/id_ed25519 ServerAliveInterval 30 ServerAliveCountMax 6然后在 VS Code 的设置里把remote.SSH.configFile指向这个文件连接时直接选devbox。ServerAliveInterval这两行是我强烈建议加的长连接在办公网里很容易因为空闲被中间设备掐断加了心跳之后基本不会再出现打着字突然掉线。但 Remote-SSH 有几个明确的边界。第一远端必须有足够的内存跑语言服务如果是一块只有 512MB 内存的嵌入式板子索引一启动就被 OOM 杀掉。第二远端需要能装扩展某些环境下扩展市场是访问不了的。第三断网即停工因为文件在远端网络一断你连看一眼代码都不行。所以它适合远端是一台正经服务器、本机主要负责显示的团队场景不适合本地要能离线写、远端只是偶尔跑一下的个人场景。2.2 SFTP 类插件本地为主远端当镜像这是绝大多数人最早接触的方案。工作区还是在本机插件监听文件变化把改动通过 SFTP 协议推上去。本地语言服务完整可用断网了照样能写代码恢复连接后再同步过去。这个方案的优势是心智负担低你的文件树、Git 操作、终端全在本机和平时写代码没有任何区别。配置文件通常放在工作区的.vscode/sftp.json里改几个字段就能跑。它的边界也很清楚。第一同步粒度粗多数插件只能做到文件级的增删改做不到 rsync 那种块级增量大文件每次都是整传。第二删除和重命名的处理不一定可靠需要显式打开对应开关。第三符号链接、文件权限、属主这些元信息经常丢。第四也是最要命的一点useTempFile这类机制会让某些监听文件变化的远端程序看到半截文件或者临时文件名导致远端重启或者读到空内容。所以我的判断是单个小项目、文件数在几千以内、没有复杂的元信息需求插件方案完全够用而且上手最快。一旦文件数上万或者需要保留执行权限就该往第三种方案走。2.3 rsync 文件监听最接近透明的一层这套做法是自己搭一个文件系统监听器负责感知本地变化一个 rsync 命令负责把变化推过去。听起来比插件麻烦但它有三个插件给不了的能力。第一是增量算法。rsync 默认会在传输前对文件做滚动校验只传差异块改了一个大文件里的几行实际传输量可能只有几 KB。第二是元信息可控权限、属主、时间戳、软链接都能通过参数精确指定。第三是完全可脚本化你可以加日志、加重试、加过滤、加通知把它做成一个真正意义上的后台服务。代价是要自己处理事件风暴、非交互式 SSH 环境、开机自启这些工程细节。后面第 4 章会把完整链路写出来。2.4 三条路线放在一张表里对比维度Remote-SSHSFTP 类插件rsync 文件监听代码实际存储位置远端唯一一份本地为主远端副本本地为主远端副本语言服务运行在哪远端本地本地断网时能否继续写码不能能能首次全量传输耗时无需传输中等中等后续增量极快删除与重命名跟踪天然一致需显式开启--delete天然处理权限与属主保留天然一致常丢失参数可控配置复杂度低低中高适合场景远端算力足、网络稳定单机小项目脚本化、需长期无人值守提示不要指望一种方案覆盖所有情况。我的实际做法常常是混合的——主力开发用 SFTP 类插件保证写码体验跑批任务时用 rsync 脚本做一次强一致的全量对齐两者并不冲突。3. SFTP 插件的自动同步sftp.json 里每个字段到底在管什么选定插件路线之后真正决定成败的不是插件本身而是配置文件里那几个字段。我见过太多人复制了一份网上的配置跑起来发现保存了但远端没变然后开始换插件、重装、重启其实问题就出在某一个布尔值上。3.1 连接与认证先从免密登录打通配置文件再漂亮SSH 不通都是白搭。所以第一步永远是在普通终端里手动验证一次免密登录ssh -i ~/.ssh/id_ed25519 -p 22 devuser192.168.1.50 echo ok pwd这条命令能干净地打印出ok和远端家目录才说明密钥、权限、known_hosts 都没问题。常见卡点有三个私钥文件权限太开放chmod 600解决、远端~/.ssh目录权限不对chmod 700、远端authorized_keys权限不对chmod 600。这三个权限问题在图形界面的编辑器里是看不到任何提示的只会表现为连接超时。另外要注意编辑器的扩展进程和你的终端可能不是同一个用户环境。如果你用的是 keychain 或者 ssh-agent编辑器启动时可能拿不到 agent 的 socket表现出来就是终端能免密、编辑器却反复要密码。这种情况下最省事的做法是在配置里直接写privateKeyPath指向具体的密钥文件绕开 agent。3.2 触发时机uploadOnSave 与 watcher 的区别这两个机制经常被混为一谈实际上它们走的是两条完全不同的路径。uploadOnSave顾名思义是保存动作触发的。你在 VS Code 里按了保存插件才上传。它的好处是精准、不浪费坏处是它只认编辑器内的保存——如果你在终端里用命令行改了一个文件或者在 Git 里切了分支它不会上传。watcher是基于文件系统监听的配置项通常是watcher.files指定监听范围watcher.autoUpload决定是否自动上传。它的好处是覆盖范围广外部工具产生的改动也能捕获坏处是容易触发事件风暴尤其是编译产物目录被纳入监听范围时一次编译可能产生上千个事件同步队列直接堵死。我的建议是日常开发只开uploadOnSave需要批量对齐时手动执行一次全量同步命令。如果确实需要 watcher比如项目里有代码生成器那就一定要把生成产物目录写进 ignore。autoUpload的值除了布尔值之外很多插件还支持传一个毫秒数作为轮询间隔这种模式在文件系统事件不可靠的环境下更稳但延迟会明显上升。3.3 ignore 与 watcher.files 的写法陷阱ignore数组的匹配规则是很多人的第一道坎。多数插件的实现是前缀匹配为主、辅以简单的通配符而不是完整的 glob。这意味着写node_modules能生效写**/node_modules/**反而可能不生效——因为它不支持双层星号。我的做法是只用最朴素的形式一个目录名或一个文件名一行不要加任何路径分隔符ignore: [ .vscode, .git, .DS_Store, .venv, __pycache__, node_modules, dist, build, *.pyc, *.log, *.swp ]watcher.files则相反它需要的是包含规则。这里最常见的错误是写成files: *.py结果子目录里的 Python 文件全部不被监听因为它们不匹配根目录下的单层通配。正确写法是递归通配让它覆盖所有层级。还有一个隐蔽的坑如果你把.git排除掉了但是用 Git 切换分支工作区文件会大量变化而uploadOnSave不会感知到。切完分支后记得手动做一次全量同步否则远端代码和本地分支看到的完全是两回事。3.4 一份可以直接抄的完整配置下面这份配置我在多个项目上用过覆盖了认证、触发、忽略、删除同步几个关键点。放在工作区根目录的.vscode/sftp.json{ name: devbox-project, host: 192.168.1.50, protocol: sftp, port: 22, username: devuser, remotePath: /home/devuser/project, privateKeyPath: /home/localuser/.ssh/id_ed25519, uploadOnSave: true, useTempFile: false, downloadOnOpen: false, openSsh: false, connectTimeout: 10000, ignore: [ .vscode, .git, .DS_Store, .venv, __pycache__, node_modules, dist, build, *.pyc, *.log ], watcher: { files: **/*, autoUpload: false, autoDelete: false }, syncOption: { delete: true, update: false } }几个字段的选择理由值得说明一下。useTempFile我关掉了因为很多远端程序会监听目录临时文件的存在会让它们误触发重启或者读到不完整的内容。downloadOnOpen也关掉了打开一个远端存在的同名文件时插件默认会去远端拉一份覆盖本地这在本地已经改过的情况下是灾难性的。syncOption.delete打开是为了让本地删除能传导到远端避免幽灵文件但这也意味着任何误删都会被同步过去所以这个开关要在你确认理解之后才打开。connectTimeout显式设成 10 秒是因为默认值在一些跨网段场景下偏短网络稍有抖动就报错。注意.vscode/sftp.json里含有主机地址和用户名如果项目仓库是多人共享的不要提交这个文件。推荐做法是维护一份.vscode/sftp.json.example提交上去真实配置写进.gitignore每个人本地自己填。4. 自建 rsync inotify 链路从一条命令到开机自启当文件数量上去、或者需要保留执行权限、或者需要在 CI 机器上无人值守地跑同步时插件就不够用了。这时候自己搭一条 rsync 链路是值得的。整个链路其实就是四步一条能干活的 rsync 命令、一只能感知变化的监听器、一层能抗住事件风暴的合并逻辑、一个能开机自动拉起的服务。4.1 rsync 参数逐个说清哪些必须有哪些是累赘先看一条我常用的命令rsync -rlptD --delete --partial --exclude-from.rsyncignore \ -e ssh -i $HOME/.ssh/id_ed25519 -o StrictHostKeyCheckingaccept-new \ $LOCAL_DIR/ devuser192.168.1.50:/home/devuser/project/这里有几个反复推敲过的选择。用-rlptD而不是大家更熟悉的-a。-a等价于-rlptgoD多出来的g和o是保留属主和属组而这只有在远端有 root 权限时才有效普通用户执行会直接报错退出。所以在非 root 场景下-a反而不如显式写-rlptD干净。-l保留软链接、-p保留权限、-t保留时间戳、-D保留设备文件和特殊文件这几个是必需的。没有加-z。压缩在跨公网的链路上确实能省带宽但在局域网里压缩和解压的 CPU 开销常常超过它省下的传输时间尤其是代码文件本来就小。这一点和很多教程的说法不一样但你可以拿--stats实测一下自己的网络环境再决定。--partial让中断的传输可以续传网络不稳定时非常有用。--delete让远端严格镜像本地配合.rsyncignore使用。另外如果你的项目里有大文件模型权重、数据集可以加-W--whole-file跳过 rsync 的滚动校验在局域网里整传反而更快——因为校验本身要读取两端的完整文件内容。.rsyncignore的内容和.gitignore语法基本一致但注意 rsync 的排除是先匹配先生效顺序会影响结果复杂规则建议查手册而不是凭感觉写。4.2 用 inotifywait 驱动增量触发顺便解决事件风暴监听器用inotifywait。这里有一个非常关键的写法差异需要说清楚。很多教程会让你用inotifywait -m进入持续监听模式然后在管道后面接一个 while 循环处理每一个事件。这个写法在文件不多的时候没问题但一旦遇到批量操作切分支、批量格式化、代码生成事件会在极短时间内涌进来几百上千个每一个都触发一次 rsync进程数爆炸机器直接卡死。我的做法是不用-m而是用循环里的阻塞等待加一个固定的合并窗口#!/usr/bin/env bash set -euo pipefail LOCAL_DIR${LOCAL_DIR:-$HOME/project} REMOTE_HOST${REMOTE_HOST:-devuser192.168.1.50} REMOTE_DIR${REMOTE_DIR:-/home/devuser/project} SSH_KEY${SSH_KEY:-$HOME/.ssh/id_ed25519} SETTLE_SECONDS${SETTLE_SECONDS:-1} sync_once() { rsync -rlptD --delete --partial \ --exclude-from$LOCAL_DIR/.rsyncignore \ -e ssh -i $SSH_KEY -o StrictHostKeyCheckingaccept-new -o BatchModeyes \ $LOCAL_DIR/ $REMOTE_HOST:$REMOTE_DIR/ echo [$(date %F %T)] synced } sync_once while true; do inotifywait -r -q \ -e modify,create,delete,move,attrib \ --exclude (\.git/|node_modules/|__pycache__/|\.venv/|dist/|build/) \ $LOCAL_DIR /dev/null sleep $SETTLE_SECONDS sync_once || echo [$(date %F %T)] sync failed, will retry on next event done逻辑很简单inotifywait不带-m时会阻塞到第一个事件发生然后退出脚本拿到控制权之后先sleep一小段时间把这一波事件里的后续都吸收掉因为它们发生在 sleep 期间不产生额外触发然后再执行一次同步。窗口设成 1 秒是我实测下来体验最好的值——足够吸收常见的事件风暴又不会让你觉得改完等半天才同步。如果同步失败了脚本不退出等下一个事件再试。这个设计的理由是网络抖动导致的失败是暂时的为了一次失败就退出服务反而需要人工干预不如让它自愈。4.3 交给 systemd非交互式环境下的 SSH 坑脚本在终端里跑得好好的做成服务就失败这是最经典的坑原因几乎都出在环境变量上。systemd 启动服务时用的是极简环境HOME可能没设置SSH_AUTH_SOCK一定没有PATH也短得可怜。而 rsync 调用 ssh 时需要在$HOME/.ssh下找 known_hosts、需要在$PATH里找到 ssh 二进制。所以服务单元里必须把这些补上[Unit] DescriptionAuto sync local project to remote host Afternetwork-online.target Wantsnetwork-online.target [Service] Typesimple Userdevuser EnvironmentHOME/home/localuser EnvironmentPATH/usr/local/bin:/usr/bin:/bin EnvironmentLOCAL_DIR/home/localuser/project EnvironmentREMOTE_HOSTdevuser192.168.1.50 EnvironmentREMOTE_DIR/home/devuser/project EnvironmentSSH_KEY/home/localuser/.ssh/id_ed25519 ExecStart/home/localuser/bin/autosync.sh Restarton-failure RestartSec5 StandardOutputjournal StandardErrorjournal [Install] WantedBydefault.target几个要点值得展开。Afternetwork-online.target保证服务在网络真正可用之后才启动不加的话开机阶段 rsync 会因为解析不到主机名直接失败。BatchModeyes这个 ssh 参数我在脚本里加了作用是禁止任何交互式提问——包括密码输入和 host key 确认提示。如果不加一个第一次连接的主机在服务环境下会卡在是否继续连接的提示上而这个提示根本没人能看到表现为服务永远挂在启动阶段。相对的StrictHostKeyCheckingaccept-new让它自动接受新主机的指纹同时保留对已知主机指纹变化的告警。如果你的机器支持用户级服务用systemctl --user会更干净因为它天然带着用户的环境。启用步骤是三步mkdir -p ~/.config/systemd/user cp autosync.service ~/.config/systemd/user/ systemctl --user daemon-reload systemctl --user enable --now autosync.service loginctl enable-linger $USER最后那行enable-linger很关键不加的话用户注销之后所有用户级服务都会被停掉。加上之后服务才能在没有登录会话的情况下继续运行。验证状态用systemctl --user status autosync.service看日志用journalctl --user -u autosync.service -f。4.4 更贴近 VS Code 的两种拉起方式如果你不想动系统服务还有两种更轻的挂载方式。第一种是用工作区的tasks.json把同步命令注册成一个任务并且配置成文件夹打开时自动运行{ version: 2.0.0, tasks: [ { label: start-autosync, type: shell, command: ${workspaceFolder}/scripts/autosync.sh, isBackground: true, problemMatcher: [], runOptions: { runOn: folderOpen }, presentation: { reveal: silent, panel: dedicated } } ] }配好之后第一次打开工作区时会弹出一次信任提示确认之后每次打开文件夹都会自动在后台拉起同步。runOptions.runOn设成folderOpen是这个方案的核心没有它就必须手动按快捷键启动。isBackground: true让 VS Code 知道这个任务不会自己结束不会一直转圈等你。第二种是把同步逻辑收进保存动作靠一个保存钩子插件来做。好处是和编辑行为绑定得最紧坏处是它同样只认编辑器内的保存终端里的改动不受影响。两种方式我通常二选一不会同时开着因为两条链路同时跑容易产生竞争。提示如果你把同步脚本挂进了tasks.json记得脚本本身要有幂等性——被重复拉起时不能把远端搞乱。前面那个sync_once加阻塞等待的结构天然满足这个要求。5. 排错链路同步显示成功但远端代码没变该怎么一步步定位这类问题的麻烦之处在于它没有报错。日志里写着同步完成本地文件确实改了远端就是老样子。这时候最忌讳的是从结果往回猜正确的做法是把链路切成段从前往后逐段验证找到第一个不成立的那一段。5.1 把链路切成五段逐段验证整条链路大致是五段文件系统事件产生、本地监听器捕获、本地脚本执行、SSH 通道建立、远端写入。我在排查时按这个顺序走通常三分钟内能定位。第一段事件是否产生。开一个终端跑inotifywait -m -r ./ 项目目录然后在编辑器里改一个文件保存看终端里有没有输出。没有输出说明文件系统事件这一层就断了——直接跳到 5.4 节这个问题几乎一定和挂载类型有关。第二段监听器是否捕获。检查监听命令里的--exclude正则。正则写错的时候不会报错只会静默地把所有路径都排除掉。验证方法是把 exclude 临时删掉再试一次。第三段本地脚本是否执行。在脚本的sync_once函数第一行加一句写日志比如echo [$(date)] triggered /tmp/autosync.log。日志没动说明脚本压根没跑起来问题在服务配置或者任务配置不在同步逻辑。第四段SSH 是否真的连通。这一步很多人会跳过因为在自己的终端里ssh是好用的。但服务环境不一样——试着手动模拟一遍env -i HOME/home/localuser PATH/usr/bin:/bin ssh -i 密钥 -o BatchModeyes 用户主机 echo ok。这条命令如果卡住或者报错问题就找到了。env -i清空所有环境变量效果和服务环境一致。第五段远端路径是否正确。前面四段都通了但远端还是没变那多半是路径拼接出了问题。rsync 的源路径带不带结尾斜杠语义完全不同./project/表示把 project 目录里的内容同步过去./project表示把 project 这个目录本身同步过去。如果你配的是remotePath: /home/devuser/project而源写成了不带斜杠的形式结果就是远端变成了/home/devuser/project/project你去看老路径当然没变化。5.2 高频故障对照表下面这张表是我自己踩过或者帮别人排过的故障里出现频率最高的一批按现象整理可以直接当速查表用。现象可能原因验证方法终端里手动跑脚本成功开机后不生效服务环境缺HOME、PATH或缺network-online.target用env -i模拟服务环境跑一遍改了 Python 文件不同步监听包含规则只覆盖了根目录单层换成递归通配或用inotifywait手动验证远端出现大量旧文件未开启删除同步插件看syncOption.deletersync 看是否有--delete远端文件权限变成 644脚本无法执行未保留权限位rsync 加-p或显式指定--chmodF755,D755远端文件大小是 0 或内容截断编辑器临时文件机制被远端程序读到关闭useTempFile一直提示连接超时端口不对、防火墙拦截、主机指纹变更ssh -vvv观察握手过程停在哪一步中文文件名远端乱码两端 locale 不一致统一设置 UTF-8 编码编译一次就卡死编译产物目录被纳入监听把build、dist、node_modules加进排除定时同步但远端版本总是慢一拍监听窗口过长或轮询间隔过大缩短SETTLE_SECONDS或降低轮询间隔同步后远端服务没重启只同步了文件没有触发远端重载在同步脚本末尾加一条远端重启命令最后一条值得单独说一句。很多人把同步和生效当成一回事其实对于需要重启才能加载新代码的服务来说同步完只是文件到位了进程还在用旧的。解决的思路是在sync_once成功之后追加一条远端命令比如通过ssh执行一个重载脚本。但要注意节流不能每次文件变动都重启一次进程通常的做法是让远端脚本自己做判断或者给重启加一个最小间隔限制。5.3 权限、属主与换行符这类看不见的问题有三个问题不会报错但会在运行阶段给你制造莫名其妙的故障。第一个是权限位丢失。SFTP 类插件在很多实现下会把上传的文件统一设成 644一个原本可执行的 shell 脚本传过去就不能跑了报权限不足。解决办法是在粘贴配置时就想好rsync 走-p保留权限插件方案则在同步完成后用一条远端chmod补一刀。第二个是属主不对。如果远端程序以特定用户运行而你的同步账号是另一个用户文件属主就会是同步账号导致远端进程读不到或者写不了。这个只能通过让同步账号和运行账号一致、或者让两者同组并设置合适的组权限来解决。用--owner --group强改属主需要 root普通用户下会直接失败所以不要盲目抄这个参数。第三个是换行符。本地是 Windows、远端是 Linux 的组合特别容易出这个问题。一个 shell 脚本传过去跑起来报bad interpreter: /bin/bash^M就是换行符惹的祸。解决办法分两层编辑器里给这类文本文件设成 LF同时在仓库里放一个.gitattributes声明*.sh text eollf。同步工具本身不负责转换换行符它只负责把字节搬过去所以这个问题必须在上游解决。5.4 WSL 与挂载盘下的 inotify 失效这个坑我觉得值得单独拎出来讲因为它的表现非常反直觉所有配置都没问题rsync 命令手动跑也能成功就是自动触发怎么都不生效。原因在于 Linux 的 inotify 机制依赖于文件系统的实现而某些类型的挂载点压根不支持事件通知。最典型的就是 WSL 环境下去访问 Windows 侧磁盘的挂载路径比如/mnt/c/...以及某些网络文件系统挂载。文件能读写但内核层面不会产生事件监听器自然什么都收不到。表现是inotifywait -m命令跑起来了没有任何报错你去改文件终端里一片安静。解决办法有三个方向。第一把项目放在支持事件通知的文件系统上在 WSL 场景下就是把代码仓库挪到 WSL 自己的文件系统里而不是放在 Windows 侧的盘符下。这个改动收益最大不只是同步编译和文件扫描的性能也会显著提升。第二换成轮询模式用带间隔的循环去比对文件时间戳牺牲实时性和一些性能换取可用性。第三换成插件方案因为插件内部的保存触发不依赖文件系统事件只依赖编辑器的保存动作天然绕开了这个问题——这也解释了为什么很多人在 WSL 里插件能用、自己的脚本不能用。6. 让它长期跑下去冲突、忽略规则与 Git 的边界能在一次会话里跑通和能连续跑三个月不出事完全是两件事。最后这一部分聊的都是长期运行才会暴露出来的问题。6.1 双向改动与回滚别让 --delete 变成删库删除同步是一把双刃剑。它能让远端严格镜像本地避免幽灵文件但一旦你把某个目录在本地移走、或者误删了一整个文件夹下一次同步就会原样传导到远端。如果远端那份是唯一副本就彻底没了。我给自己定的三条规则是这样的。第一远端永远要有一份 Git 之外的冷备份可以是另一个目录的定期 tar 快照也可以是另一台机器上的副本。第二开启删除同步的同时开启保护开关rsync 有--max-deleteN参数可以限制单次同步最多删多少个文件超过阈值就中止并报错。这个参数在误删整个目录时能救你一命rsync -rlptD --delete --max-delete50 ...第三绝不在同步目标上做人工修改。一旦你承认远端可以被手工改就必须处理双向冲突而双向合并的复杂度会立刻上一个台阶。我的做法是从流程上禁止这件事远端目录设成只读更安全真要调试就在本地改完再同步过去。万不得已需要双向的时候可以给 rsync 加--update让时间戳更新的一方胜出。这个策略很粗糙在两台机器都改过同一个文件时结果不确定但至少不会用旧覆盖新。真正的双向合并还是交给 Git 更靠谱。6.2 忽略规则的维护成本.gitignore和同步工具的忽略规则是两套东西但它们的目标高度重合都只想跟踪源码不想跟踪产物。既然如此为什么不合并可以合并但有风险。.gitignore的语法比大多数同步工具支持的子集要大得多把一份复杂的.gitignore直接喂给插件很可能遇到某些规则不被支持的问题而不被支持的规则会让本该排除的目录被同步过去。所以我的做法是分开维护但在项目 README 里注明两份规则必须同步更新并且在.rsyncignore顶部加一行注释提醒。常见的必排除项有一个稳定的清单版本控制目录、依赖目录node_modules、.venv、vendor、构建产物dist、build、target、out、语言缓存__pycache__、.mypy_cache、.pytest_cache、编辑器配置.vscode、.idea、本地环境文件.env.local、大文件与数据集。清单不长但漏掉任何一项都可能让同步从秒级变成分钟级。6.3 同步与 Git 各管一段一个很容易混淆的点是既然有 Git 了为什么还要同步答案在于两者解决的问题完全不同。Git 管的是版本历史它需要 commit、需要 push、需要在远端有一个仓库同步管的是工作区状态它要的是我这边文件现在长什么样那边立刻也是什么样中间不需要任何提交动作。这个区别决定了它们的使用时机。写代码、验证、调试阶段用同步快速、无侵入、不留历史阶段性完成后用 Git 提交留档、协作、可回溯。如果每次调试都要先 commit 再 pull节奏会被彻底打乱而且会制造大量无意义的提交记录。但两者需要明确边界。不要把同步工具指向 Git 仓库的元数据目录也就是.git因为并发访问可能导致索引损坏。切分支之后手动全量同步一次不要指望增量同步能覆盖分支切换带来的大面积变化。以及如果远端也有一份 Git 仓库注意同步不要破坏它的工作区状态——最稳妥的做法是让远端的那份只作为运行目录Git 操作都在本地做。6.4 日志、告警与它到底还活着吗后台服务的最大问题是它失败的时候不出声。rsync 在遇到权限错误、磁盘满、远端目录被删这些问题时会返回非零退出码但如果你没有把它记录下来就永远不会知道。我的做法是在同步脚本里区分处理正常同步只写一行带时间戳的简短记录失败时写完整的 stderr并且在连续失败达到一定次数后通过一个轻量的通知渠道比如写入一个特定的文件、或者调用一个 HTTP 接口发出信号。日志量也要控制不要开-v因为每同步一次就刷屏几百行真出问题的时候你根本找不到有用信息。还有一个很实用的自检手段周期性做一致性校验。用rsync -n -c -i做一次 dry-run 加校验和比对如果不输出任何差异行说明两端完全一致如果输出了差异说明有文件没同步成功。这个检查可以每天跑一次比盯着日志可靠得多。代价是它需要读取两端所有文件内容大仓库会比较慢所以放在低峰期执行。最后分享一个我用了很久的小技巧在同步目标的根目录放一个.last_sync文件每次成功同步后写入当前时间戳。这样任何人登录到远端cat .last_sync就知道这条链路是不是还活着不用登到本地去看服务状态也不需要额外的监控系统。简单但极其好用。
返回列表