ARTICLE DETAIL

资讯详情

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

用Shell脚本打造极简部署工具:零依赖实现秒级回滚与自动备份

用Shell脚本打造极简部署工具:零依赖实现秒级回滚与自动备份 刚维护一套老服务的时候我连续三次在部署上翻车第一次漏传了一个配置文件第二次重启顺序搞错导致服务起不来第三次更离谱回滚的时候找不到上一个版本的包。那天晚上我坐在电脑前想我需要一套部署工具但我不想为了一个小项目去装一整套配置中心、编排引擎和自动化平台那些东西的学习成本和维护成本比手工部署还高。于是我花了两个晚上写了几个shell脚本把它们放进一个目录起了个名字叫caveman——穴居人。意思是回到最原始的方式用最简单、笨拙但可靠的手段解决问题。这个决定后来帮我省了非常多的时间。这套工具陆续用了一年多中间也踩了不少坑这篇就把整个思路、代码和教训一起整理出来。caveman不是一个框架没有需要安装的依赖没有任何后台进程也没有图形界面。它就是一个目录下放着的几个bash脚本加上一份写着操作流程的备忘。它的全部思想概括成一句话能用一条命令解决的事绝不写一个类能用一个文件解决的事绝不做一个服务能在一台机器上完成的事绝不引入第三台。这套东西最初只是给我自己用的但后来团队里另一个同事接手这些服务时他花了十分钟读脚本第二十分钟就独立完成了一次发版。那一刻我觉得这套思路是对的。1. 从手工部署三次出错到动手写caveman1.1 场景还原那台不能随便装东西的服务器事情要从一台老服务器说起。跑着一个对外提供接口的服务代码量不大但那是生产环境不敢乱动。最初的操作流程是本地打包用scp传上去ssh上去解压手动改软链接再手动重启服务。听起来不复杂对吧但问题是人不是机器容易在重复劳动中出错。有一次我传上去之后忘了解压直接重启了服务结果服务起不来线上报错持续了整整十分钟。还有一次我在本地打包时用了旧代码部署上去之后回滚更是手忙脚乱。那台服务器还比较特殊操作系统版本偏老装新软件需要审批流程普通用户没有权限安装全局的软件包。这意味着像Ansible这种依赖Python和一堆库的工具压根过不了环境这一关。就算是能用我也不想引入一个几百MB的自动化工具来处理一个本来只需要几条命令的场景。当时我给自己列了一个需求清单要能一条命令完成从本地到服务器的发布要能在出错后一分钟内回滚到上一个版本要能自动保留历史版本方便随时回去看要能在服务器上做最基本的健康检查备份和日志清理要有但不能占用太多时间和空间。最重要的是这一切必须在尽量少的依赖下完成最好除了bash、rsync、tar、ssh这些操作系统自带的命令之外什么都不需要。1.2 三条铁律零依赖、单文件、可读性写这套脚本之前我先给自己定了几个约束这些约束后来被证明非常关键。第一零第三方依赖。所有脚本只依赖Linux系统自带的shell工具不依赖Python特定版本不依赖node环境不依赖任何需要额外安装的软件包。这样做最大的好处是换一台服务器马上就能跑不用先安装一堆依赖。第二单文件原则。一个脚本只干一件完整的事比如deploy.sh就负责发布rollback.sh只负责回滚backup.sh只做备份。脚本之间不互相调用不搞公共库需要重复的逻辑就拷贝一份宁可冗余也不做依赖。这样做的原因是当线上出问题时每一行代码都必须是能让人一眼看懂的而不是跳转到另一个文件的某个函数里。第三输出必须可读失败必须响亮。脚本里每个步骤都写清楚日志哪一步正在做什么必须打印出来。如果出错要么立即终止要么用非零退出码让调用方感知到。绝不允许看起来成功实际失败的静默错误。这个约束后来在排查问题的时候救了我不止一次。2. caveman里有什么五个脚本撑起一个应用的生命周期2.1 caveman-deploy用rsync解决发布的核心问题发布的核心问题只有两个把新的文件放到服务器上让服务跑起来。caveman-deploy把这两步合在了一起用rsync来传输文件。rsync -az --delete ./dist/ userserver:/opt/myapp/releases/20250120143000/ ssh userserver cd /opt/myapp ln -sfn releases/20250120143000 current systemctl restart myapp这里的核心逻辑是每次部署都新建一个带时间戳的目录把完整的构建产物同步进去然后把名为current的软链接切换到新目录最后重启服务。rsync的-a参数保留权限和时间戳-z压缩传输--delete保证远程目录和本地目录严格一致。有人会问为什么不直接覆盖/opt/myapp下的文件非要搞一个带时间戳的新目录原因是可回滚。每次发布都产生一个独立的历史版本目录回滚只需要切换软链接根本不需要重新传文件。这套思路其实非常古老但直到今天依然是最可靠的发布方式之一。2.2 caveman-rollback一分钟内回到上一个版本回滚脚本更简单做的事情就是列出已有的版本目录选一个指定的版本切软链接重启服务。ls -t /opt/myapp/releases | head -n 2 # 假设当前是release-20250120143000上一个版本是release-20250119120000 ssh userserver ln -sfn /opt/myapp/releases/release-20250119120000 /opt/myapp/current systemctl restart myapp用的时候先跑caveman-rollback --list查看可用的版本列表再跑caveman-rollback --to 20250119120000。整个回滚过程在正常情况下不超过一分钟。这个脚本最值钱的地方不是命令本身而是它逼迫我养成了每次发版都会产生新目录的习惯。只要目录存在回滚就永远有退路。2.3 caveman-backuptar和cron的古老组合数据备份我用的是tar加cron的组合没有任何花哨的东西。备份脚本做的事情是先打包数据目录再按日期命名归档最后清理七天前的旧备份。#!/usr/bin/env bash set -euo pipefail BACKUP_DIR/data/backups DATA_DIR/data/myapp-data TIMESTAMP$(date %Y%m%d%H%M%S) tar -czf $BACKUP_DIR/data-$TIMESTAMP.tar.gz -C /data myapp-data find $BACKUP_DIR -name data-*.tar.gz -mtime 7 -delete echo [OK] backup done: $BACKUP_DIR/data-$TIMESTAMP.tar.gz这个脚本挂在crontab里每天凌晨两点执行一次。我知道tar这种方式看起来非常原始但在不引入额外软件、不考虑跨平台、数据量适中的前提下tar就是最朴素的可靠方案。真正的安全问题不在备份技术而在于备份是否真的跑了、是否真的能恢复。为了让这个问题被及时发现caveman-backup在每次执行完后都向一个专门的日志文件追加一行我每周检查一次这个文件。2.4 caveman-check纯shell的系统体检脚本运维最怕的是服务器出问题但你没及时发现。caveman-check负责每日巡检只检查几个最核心的指标磁盘空间使用率、内存余量、服务进程存活状态、端口连通性。全部用纯shell命令完成没有任何额外的监控组件。#!/usr/bin/env bash set -euo pipefail threshold85 disk_usage$(df -h / | awk NR2 {print int($5)}) if [ $disk_usage -gt $threshold ]; then echo [WARN] disk usage is high: $disk_usage% fi if systemctl is-active --quiet myapp; then echo [OK] myapp is active else echo [CRIT] myapp is NOT running fi curl -sf http://127.0.0.1/healthz /dev/null echo [OK] healthz passed || echo [CRIT] healthz failed健康检查里那个/healthz接口非常关键。它能反映出服务本身是否健康而不仅仅是进程还存在。我经常打一个比方检查服务进程有没有在跑相当于看一个人有没有呼吸检查healthz接口才相当于量体温量血压。很多问题表现为进程活着但接口不可用所以这个检查我是每天必跑的。2.5 caveman-log日志切割与清理日志文件如果不处理增长速度会吓到人。caveman-log做的事情是每天对日志文件进行一次切割把前一天的日志归档成带日期的文件同时删除30天前的旧日志。mv /var/log/myapp/app.log /var/log/myapp/app-$(date -d yesterday %Y%m%d).log kill -USR1 $(cat /var/run/myapp.pid) find /var/log/myapp -name app-*.log -mtime 30 -delete第二行kill -USR1值得多说一句。很多服务会监听USR1信号收到信号后重新打开日志文件句柄。如果不做这一步即使你把app.log改名了服务还在向那个已经被改名的文件里写日志。这个问题有个专属名词叫日志句柄未释放是新手经常踩的坑而且症状很隐蔽——你会发现日志文件还是越来越大但你去目录里看却找不到那个文件。3. 完整实操一次带事故演练的发布与回滚3.1 部署前的准备工作部署前我会先做三件事第一在本地跑一遍完整测试确保这次要发布的代码没问题第二构建产物把前端静态文件或者后端二进制包准备好第三确认服务器上的磁盘空间够用因为每次发布都会新增一个完整版本目录如果版本目录保留了二十个每个500MB的话磁盘很快就满了。磁盘检查用一条命令搞定。df -h /opt/myapp | awk NR2 {print $5}我给自己定的规矩是磁盘使用率超过70%就不发版先清理旧版本再说。清理旧版本的操作很简单就是删掉/opt/myapp/releases下除了最近三个之外的所有目录。这里要特别提醒不要在部署脚本里自动删旧版本要留一个手动操作的机会否则你永远不知道哪次想回滚到某个更早的版本时目录已经在不知情的情况下被清掉了。3.2 执行ls启动部署准备好之后执行部署就一条命令。./caveman-deploy.sh userserver /opt/myapp ./dist脚本会依次完成生成本次发布目录名、rsync同步文件、切换软链接、重启服务。每个步骤的输出都是带时间戳的日志一眼就能看到当前进行到哪一步。整个过程通常在十秒内完成取决于代码量和网络速度。部署成功后我会手动验证一次healthz接口而不是完全信任脚本输出。3.3 出问题后的回滚动作模拟一次故障部署完成后healthz接口返回500。此时不需要慌张因为回滚的代价已经被caveman压得很低了。执行./caveman-rollback.sh --list查看可用版本再执行./caveman-rollback.sh --to 20250119120000切回上一个版本。[INFO] current - /opt/myapp/releases/20250120143000 [INFO] rolling back to /opt/myapp/releases/20250119120000 [INFO] switching symlink... [INFO] restarting myapp... [OK] rollback complete回滚后的验证和部署一样访问healthz确认接口恢复正常。从发现故障到回滚完成全程大约一分钟。这种秒级回滚能力是所有发布自动化方案的核心价值而它用最原始的软链接就能实现。4. 我踩过的坑通配符、断连和软链接的假把戏4.1 通配符展开一条rsync命令差点删光远程文件这是我在这套工具上踩过最痛的坑。最初我在部署脚本里写的是这样一条命令rsync -az --delete ./dist/* userserver:/opt/myapp/releases/xxx/注意./dist/*这个通配符。bash会在本地执行rsync之前先把通配符展开把dist目录下的所有文件名传给它。问题来了当dist目录为空时通配符展开的结果是什么都匹配不到bash默认会把./dist/*原样传给rsync。rsync发现本地没有这个文件配合--delete参数它会认为远程目录里的所有文件都应该被删除于是直接把远程目录清空了。后果非常严重发布目录里刚刚同步过去的文件全没了。虽然服务没有崩因为软链接还指在过去那个旧版本上但新版本已经废了。更隐蔽的是这个错误不是每次都会触发只有当dist目录恰好为空时才会发生这导致我第一次遇到时完全没有头绪。解决的办法很直接rsync带--delete参数时永远不要用通配符指定本地目录内容。正确写法是同步目录本身rsync -az --delete ./dist/ userserver:/opt/myapp/releases/xxx/这样rsync会用dist目录本身的内容和远程发布目录做对比dist为空时删除的就是远程发布目录里的所有文件这才是--delete的正常语义。写完脚本后我还加了个防御性判断dist目录不存在或为空时直接报错退出。4.2 ssh断连远程命令执行到一半就没了发布脚本里最危险的一步是在远程执行重启服务命令。原先我习惯把每条ssh命令分开写比如先ssh上去切软链接再ssh上去重启服务。这两条命令之间如果网络闪断就会出现软链接已经切了但服务还没重启的状态线上直接表现为服务版本错乱。后来我改成把远程操作合并成一条ssh命令用连接保证只有前一步成功才会执行后一步。ssh userserver cd /opt/myapp ln -sfn releases/xxx current systemctl restart myapp这样做的另一个好处是减少了ssh的握手次数传输效率更高。最重要的是这种写法提供了最基本的事务性保障任何一个环节失败后面的步骤都不会执行。虽然这不能和真正的分布式事务相比但在这个场景下已经足够用了。4.3 软链接切换的假把戏改了链接不代表服务切了新代码软链接切换成功服务也重启了但服务跑的还真是新代码吗不一定。这里有个很容易被忽略的细节systemctl restart myapp这个操作本身依赖systemd找到正确的可执行文件路径。如果你的systemd服务单元文件里写的是具体的二进制文件路径而不是通过软链接解析路径那么重启后跑的依然是旧代码。举例来说服务单元文件里如果写的是ExecStart/opt/myapp/current/bin/server那是正确的因为systemd会跟着软链接走到新版本目录。但如果写的是ExecStart/opt/myapp/releases/20250119120000/bin/server那不管你切没切软链接服务永远都在跑旧代码。这个坑我遇到过一次原因是复制别人的服务单元文件时把路径写死了。排查这个问题的方法是重启服务后执行ls -l /proc/$(pidof server)/cwd或者查看/proc/$(pidof server)/exe确认进程实际执行的文件路径。另外一个更简单的方式是readlink -f /opt/myapp/current看软链接最终指向哪个目录再和服务进程实际加载的可执行文件路径对比。4.4 备份脚本的静默失败cron环境下的隐形炸弹crontab里执行的脚本和交互式shell里执行脚本有很大区别。最大的坑是环境变量cron执行时PATH通常非常精简可能只有/usr/bin:/bin而你脚本里如果用了/usr/local/bin下的某个命令就会直接找不到。更烦人的是这种失败往往不会产生明显错误你只会发现备份文件没有按时生成。我的解决方式是在脚本开头显式声明完整的环境变量export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin另外一个建议是cron任务的输出一定要重定向到日志文件。比如在crontab里写成0 2 * * * /opt/caveman/backup.sh /var/log/caveman-backup.log 21。这样即使脚本出错你也能从日志里找到痕迹而不是完全不可见。我还养成了一个习惯每月随机挑一天假装数据丢了实际做一次恢复演练确保备份真的能用。演练的步骤很简单随便找一个tar.gz包解压到临时目录把里面的关键文件提取出来看看是否完整。5. 什么时候该用穴居人方案我的五问判断法5.1 五个问题帮你在引入任何工具前做决策写caveman这一年多我慢慢总结出一套判断标准。每当我需要引入一个新工具、新框架、新平台时都会先问自己五个问题第一这个方案依赖的东西五年后还有人维护吗这个问题用来过滤那些过度依赖某个小团队维护的开源项目。技术选型最怕的是选了一个看起来很先进但社区并不活跃的方案出了问题只能自己啃源码。第二如果请一周假回来同事看这套代码或配置能在十分钟内看懂吗这个问题用来过滤那些复杂度超标的方案。构建一套复杂的自动化系统本身需要成本交接给同事更需要成本。很多工程师容易犯的毛病是过度设计明明是三个节点的服务非要搞一个完整的编排平台。第三把服务器上的新软件全部卸载这个方案还成立吗这个问题用来验证方案是否真的依赖了某些加分项还是必需品。caveman的全部脚本在只有基础系统的服务器上都能跑这保证了它的可迁移性。第四出问题时能在十分钟内手工恢复吗这个问题衡量的是方案的可逆性和可诊断性。选择简单方案的最大好处是出问题时你能快速定位和修复因为每个环节你都懂复杂方案一旦出问题排查链路可能长到让人崩溃。第五引入这个工具节省的时间真的超过维护它花的时间吗这是最容易被忽略的问题。很多人为了省五分钟的发布操作花了两天时间去搭一个自动化平台之后每个月还要花几小时维护它。这种投资回报率其实是负数。5.2 哪些场景千万别用穴居人方案caveman这套思路不是万能的它有自己的适用边界。如果你负责的是一个几十台甚至上百台服务器的集群需要统一管理配置、滚动发布、灰度策略那caveman这种朴素的脚本方案就不够用了。它的定位是解决中小规模、单机或少量机器、单一应用场景下的部署运维问题而不是提供一个通用的集群管理方案。另外如果你的业务对安全合规有严格要求比如需要完整的操作审计、访问控制和权限隔离那么caveman这种把所有操作都收敛到root权限的做法显然不合适。这种场景下正规的运维平台和工具链仍然是必需品。任何方案都有它的适用场景关键在于识别场景。caveman给我的启示是工具的选择不应该取决于什么工具最流行而是什么工具在这个具体问题里的总成本最低。5.3 caveman和现代工具不是对立关系写了caveman之后我并没有变成彻底的复古主义者。实际上在一些场景下我依然会使用现代的托管服务和自动化平台。两者的关系不是对立的而是分层的核心简单的事情用最原始的手段保证确定性复杂且规模化的场景用专门的工具提升效率。一个比较形象的类比是做饭日常煮个面条不需要把全套专业厨具搬出来但如果是开餐厅那确实需要专业的厨房设备和管理流程。caveman教会我的不是拒绝工具而是建立一种工具必须服务于问题的意识。6. 给想尝试caveman思路的人一些建议如果你也想在自己的小项目上尝试这种极简运维方案我的建议是先从最痛的点开始而不是一次性搭建全部脚本。最简单的切入点就是先把发布流程脚本化哪怕脚本里只有一条rsync加一条ssh命令先跑起来再说。有了发布脚本再慢慢补回滚、备份和健康检查。一口气贪多反而容易把方案做复杂违背了穴居人原则本身。我还有一个很具体的建议给这套脚本建一个单独的目录比如就叫caveman同时写一份README。README里不要写太多只记录常用的几条命令和操作流程。这份文档的读者不是未来的你而是那个在深夜被线上故障叫醒、需要快速操作却脑子一片混乱的你。文档越短越容易被看完越清晰越容易在压力下用对。我自己的README只有一张表写着要发版跑哪条命令要回滚跑哪条命令要看日志跑哪条命令。另外一个使用心得是脚本里的每个输出都要用[INFO]、[WARN]、[CRIT]这样的前缀标记严重级别不要嫌它啰嗦。因为当故障发生时人是慌的面对一大段没有标记的输出很难快速定位问题出在哪一步。有了明确的前缀标记脑子不需要额外解析就可以知道该关注哪些行。这个细节在平时看起来无关紧要但真到紧急时刻会救命。做这一套东西最大的收获是它让我重新理解了简单这个词的含义。简单不是简陋不是功能缺失而是每一个存在的功能都有明确价值每一处设计都能被人轻松理解。用最原始的软链接实现秒级回滚拿tar做备份靠rsync同步文件——这些技术都不是什么新鲜玩意但它们组合起来解决了一个真实的问题而且非常可靠。如果你也在维护一个小项目并且被各种复杂工具弄得身心俱疲希望这套穴居人思路能给你一些参考。
返回列表