构建产物打好了,要往测试机上放一份。你敲下 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
HostName 192.168.1.201
User user_00
Port 36000
IdentityFile ~/.ssh/id_ed25519
之后所有命令都可以只写 prev 这个别名。
scp -r dist prev:/home/data/services/goods-prev.yesdat.com/
ssh prev
rsync -avz 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 的增量判断默认看文件大小和修改时间,不一致才传。真正厉害的是跨机器传输时它会用滚动校验算法,只把文件里变化的那些块发过去,大文件小改动的场景能省掉绝大部分流量。