构建产物打好了,要往测试机上放一份。你敲下 scp -p 36000 -r dist user@1.2.3.4:/home/data/,回车之后终端不动了,几十秒后报一个 ssh: connect to host 1.2.3.4 port 22: Connection refused。端口明明写了啊。
这个错我踩过不止一次,问题出在 p 的大小写上。scp 指定端口用的是大写 -P,小写 -p 是保留文件的修改时间和权限位,跟端口一点关系没有。而 ssh 命令恰恰相反,它用的是小写 -p。两个天天一起用的命令,在同一个参数上约定相反。
这篇把 scp 的用法从头理一遍,顺带把免密配置、常见报错的排查路径,以及什么时候该换成 rsync 说清楚。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
scp底层到底走的什么通道,为什么它天然是加密的- 上传和下载两个方向的写法,路径顺序怎么记
-P和-p的大小写坑,以及-r递归的边界- 通配符在
scp里由谁展开,为什么.next这类隐藏目录传不上去 - 免密配置怎么做,
~/.ssh/config能帮你省掉多少参数 - 五类常见报错的排查顺序
- 传大目录、断点续传、增量同步这些场景为什么该换
rsync sftp的取舍,以及新版 OpenSSH 对scp的态度变化
一、scp 其实就是跑在 ssh 上的 cp
scp 的全称是 secure copy,名字里的 secure 不是它自己实现的。它做的事情很简单,建立一条 ssh 连接,然后在这条连接上把文件内容送过去。
所以你能 ssh 上去的机器,基本就能 scp;ssh 连不上的机器,scp 也一定连不上。这个关系很重要,排查问题的时候先用 ssh 试一次,能把一半的可能性直接排除掉。
同样的道理,ssh 那套认证机制 scp 全部复用。你配了密钥登录,scp 就不用输密码;服务器改了 SSH 端口,scp 也得跟着改。传输过程是加密的,这一点跟 FTP 那种明文协议完全不同,往公网传东西的时候这是个硬性优势。
理解了这层关系,scp 的参数就不用死记了,它无非是「ssh 的连接参数」加上「cp 的复制参数」拼起来的。
二、两个方向的基本写法
scp 的参数顺序和 cp 一样,永远是「源在前,目标在后」。区别只是源和目标里可以有一个带上 用户@主机: 前缀。
2.1 从服务器下载文件
在本地电脑上执行,注意最后要写清楚本地的落地路径。
scp username@servername:/path/filename /tmp/local_destination |
比如把 192.168.0.101 上的 /home/poetry/test.txt 拉到本地的 ~/Downloads/。
scp root@192.168.0.101:/home/poetry/test.txt ~/Downloads/ |
原文这条命令只写了远端路径,把本地目标路径漏了。漏掉的话 scp 会报 usage 错误,不会默认下到当前目录,这点和 wget 不一样。想下到当前目录得显式写个 .。
2.2 递归拷贝目录
传目录必须加 -r,不加的话 scp 会告诉你 not a regular file 然后什么都不做。
scp -P 36000 -r user_00@192.168.1.201:/usr/local/services/nginx/conf ~/Downloads/ |
这里的 -P 36000 是远端 SSH 服务监听的端口,不是 22 的时候必须显式指定。
2.3 传目录到服务器
方向反过来,把带 用户@主机: 的那一侧挪到后面就行。
scp -r /tmp/local_dir username@servername:remote_dir |
远端路径不以 / 开头的话,是相对于该用户家目录的相对路径。生产脚本里我一律写绝对路径,相对路径在换了个部署账号之后会悄悄传到别的地方去,这种错很难发现。
三、大小写和参数顺序上的几个坑
3.1 -P 和 -p
前面提过一次,这里单独拎出来说,因为它是最高频的翻车点。
scp 的 -P 大写才是端口,小写 -p 是 preserve,保留文件的 mtime、atime 和权限位。ssh 的端口参数则是小写 -p。真要说原因,scp 的小写 -p 是从 cp -p 继承过来的,端口只好让给大写。
记不住的话有个笨办法,把端口写进 ~/.ssh/config 里,之后两个命令都不用带端口参数了。第五节会讲怎么配。
3.2 通配符是本地 shell 展开的
原文里有这么一条命令,注释说 deployBuildFiles 下的 .next 默认不上传到服务器。
scp -P36000 -r deployBuildFiles/** user_00@192.168.1.201:/home/data/services/goods-prev.yesdat.com/ |
这个现象值得展开讲讲,因为它不是 scp 的行为,是 shell 的行为。
deployBuildFiles/** 这串东西在 scp 拿到之前,就已经被本地 shell 替换成一堆具体的路径了。而 bash 默认不启用 globstar,** 和 * 的效果完全一样,只匹配一层;更关键的是,glob 默认不匹配以 . 开头的文件名。所以 .next、.env 这类隐藏文件根本没进入参数列表,scp 压根不知道它们的存在。
所以「.next 默认不上传」这句话,准确的说法是「.next 没被通配符匹配到」。
想连隐藏文件一起传,最省事的做法是直接传目录本身而不是它下面的内容。
scp -P 36000 -r deployBuildFiles user_00@192.168.1.201:/home/data/services/goods-prev.yesdat.com/ |
这样远端会多出一层 deployBuildFiles 目录,可以接受的话这是最不容易出错的写法。要是必须打平,bash 里可以 shopt -s dotglob 打开隐藏文件匹配,但这个开关会影响整个脚本的后续行为,我一般不推荐在部署脚本里随手开。
顺着这块聊,通配符如果写在远端那一侧,展开的就不是本地 shell 而是远端 shell 了,路径里带空格和特殊字符的时候还得考虑两层引号转义。这也是 scp 在复杂场景下容易失控的地方之一。
3.3 -C 压缩和 -l 限速
传文本类的文件(日志、SQL 备份、前端产物里的 JS)加个 -C 开压缩,跨机房传的时候能省不少时间。传已经压缩过的东西(图片、视频、tar.gz)加 -C 是白费 CPU,甚至更慢。
-l 可以限带宽,单位是 Kbit/s。白天在办公网传几个 G 的备份,不限速的话整个办公室的网都会被你吃掉。
scp -C -l 8000 -P 36000 backup.sql user_00@192.168.1.201:/data/backup/ |
四、五类常见报错怎么排
scp 的报错信息普遍比较简短,但对应的原因其实很好区分。
Permission denied (publickey,password),这是认证没过。先单独 ssh 一次确认账号密码或者密钥对不对,如果 ssh 能上去 scp 却不行,看看是不是密钥有 passphrase 而脚本环境里没有 ssh-agent。
Permission denied 但发生在传输开始之后,那是远端目录的写权限问题,跟登录无关。ssh 上去 ls -ld 看一眼目标目录属主。
No such file or directory,scp 不会帮你创建中间目录,目标路径的父目录必须已经存在。这跟 mkdir -p 的习惯不一样,很多人在这卡住。
Host key verification failed,服务器重装或者 IP 复用之后,本机 ~/.ssh/known_hosts 里存的指纹对不上了。正确做法是确认这台机器确实是你自己的,然后 ssh-keygen -R 主机名 把旧记录删掉,不要图省事全局关掉 host key 检查。
卡住不动没有任何输出,多半是网络层被防火墙 DROP 了(不是 REJECT,所以不返回 Connection refused,只能等超时)。加 -v 能看到卡在哪一步,是 TCP 建连还是 SSH 握手,一眼就分得清。
排查顺序我一般是这样:先 ssh 一次看连通性和认证,再 ls -ld 看目标目录权限,最后才去看 scp 的参数写没写对。
五、免密配置,以及 ssh config 能省掉多少事
每次传文件都输一遍密码,写进脚本更是没法自动化。密钥登录配一次就一劳永逸。
本地生成密钥对,已经有 ~/.ssh/id_rsa.pub 的话跳过这步。
ssh-keygen -t ed25519 -C "deploy@laptop" |
然后把公钥推到服务器上。
ssh-copy-id -p 36000 user_00@192.168.1.201 |
ssh-copy-id 做的事情就是把你的公钥追加到远端的 ~/.ssh/authorized_keys 里,顺便把权限位设对。手工做也行,但权限位这块很容易错,~/.ssh 必须是 700,authorized_keys 必须是 600,权限太宽 sshd 会直接拒绝这个公钥,而且日志里不一定给你写清楚原因。这个我排查过一次,最后是看 /var/log/secure 才发现的。
配好之后再进一步,把连接参数写进 ~/.ssh/config。
Host prev |
之后所有命令都可以只写 prev 这个别名。
scp -r dist prev:/home/data/services/goods-prev.yesdat.com/ |
端口、用户名、密钥路径全都不用带了,前面说的 -P 大小写问题也就自然消失了。我自己的感受是,这个配置文件是提升 SSH 相关操作体验最划算的一件事,五分钟配好,后面几年都受益。
六、什么时候该换成 rsync
scp 的模型是「把源文件从头到尾读一遍,全量写到目标」。这个模型简单可靠,但有两个硬伤。
一是没有增量。前端项目 dist 目录几百个文件,你只改了一个 JS,scp 还是把几百个文件重传一遍。
二是没有断点续传。传到 90% 网断了,重来一次还是从 0 开始。传几百兆的时候这很折磨。
rsync 就是来解决这两件事的。
rsync -avz --partial --progress dist/ prev:/home/data/services/goods-prev.yesdat.com/ |
拆开看这几个参数。-a 归档模式,等价于保留权限、时间戳、软链接、递归这一整套;-v 打印传了哪些文件;-z 传输时压缩;--partial 保留传了一半的临时文件,下次接着传;--progress 显示进度。
rsync 的增量判断默认看文件大小和修改时间,不一致才传。真正厉害的是跨机器传输时它会用滚动校验算法,只把文件里变化的那些块发过去,大文件小改动的场景能省掉绝大部分流量。
这里有个坑要注意,rsync 源路径末尾的斜杠是有语义的。dist/ 表示「把 dist 里的内容传过去」,dist 表示「把 dist 这个目录本身传过去」。少一个斜杠就会多套一层目录,这个我踩过,部署完页面 404,找了半天才发现产物躺在 .../dist/dist/ 里。
另外两个常用的参数。--delete 让目标端删掉源端已经不存在的文件,做「完全镜像」的时候需要,但它是把双刃剑,路径写错就是删库级别的事故,加之前务必先用 -n(dry-run)跑一遍看输出。--exclude 排除不想传的东西,比如 --exclude 'node_modules' --exclude '.git'。
我的经验是这样分工:一次性传单个文件,或者临时看一眼某个配置,用 scp 手快;只要是重复执行的部署流程、目录同步、大文件传输,一律上 rsync。前提是两端都装了 rsync,这是它唯一的门槛,容器镜像里经常没有。
七、sftp 的位置,以及 scp 这些年的变化
sftp 是第三个选项,它跑在同一条 SSH 通道上,但提供的是一个交互式会话,可以 ls、cd、get、put,像个命令行版的 FTP 客户端。
什么时候它比 scp 顺手?你不确定远端目录结构,需要边看边传的时候。scp 得先 ssh 上去看一眼路径,退出来再拼命令,sftp 一个会话里就搞定了。它也支持非交互式的批处理模式,可以把一串命令写进文件里用 -b 执行。
还有一点值得提。这些年 OpenSSH 对 scp 的态度有变化,scp 用的那个古老协议存在一些设计上的问题(比较有名的是远端可以影响本地写入哪些文件),较新的 OpenSSH 版本里 scp 底层已经改成走 SFTP 协议,官方也建议优先使用 sftp 或 rsync。
这个变化带来的实际影响是,某些依赖旧协议行为的写法(比如远端通配符展开的一些细节)在新老版本上表现可能不一致。具体你手上这台机器是什么行为,以 man scp 和 OpenSSH 官方公告为准,我不建议按某个记忆里的版本号去推断。
说实话我自己日常还是在敲 scp,肌肉记忆改不过来。但新写的部署脚本我已经全部换成 rsync 了,一方面是增量和断点续传确实需要,另一方面也是省得去纠结不同机器上 scp 的行为差异。
服务器上的其他日常操作,我另外整理过一篇 日常频繁使用的Linux命令,传完文件之后的验证和排查可以接着那篇看。想把这些传输动作包成可复用的部署脚本,shell入门 那篇里的参数处理和错误处理部分正好配套。
总结
scp 值得记的东西不多,核心就三件。
它跑在 ssh 上,所以 ssh 连不上的机器 scp 一定也连不上,排查从 ssh 开始最快。参数顺序永远是源在前目标在后,传目录必须 -r,指定端口是大写 -P(小写 -p 是保留时间戳权限,那是 cp 的遗产)。目标路径的父目录必须已经存在,scp 不会帮你 mkdir -p。
通配符是本地 shell 展开的,隐藏文件默认匹配不到,所以传整个目录比传 目录/** 更不容易出错。
真正拉开效率差距的是后面两步。把连接参数写进 ~/.ssh/config,端口用户密钥全都不用再敲;重复执行的同步换成 rsync -avz --partial,增量加断点续传,改一个文件不用重传整个目录,但记住源路径末尾那个斜杠的语义,还有 --delete 前先 dry-run。