本文方案仅供技术参考与娱乐!
数据很重要,所以我们要经常备份。
那么怎么备份呢?假如我们使用云厂商的数据库,里面已经自带了快照和备份功能了,只要你愿意花钱,就能帮你解决大部分技术问题。它们这些数据库往往是部署在单独一 / 多台主机实例上,不会放在容器里去跑。为什么?俺也不是专业的运维,俺也不知道。这里给个知乎链接作为参考
对于我们个人项目而言,一般不会去购买昂贵的数据库实例,往往 云主机容器部署
+ serverless
+ oss/cdn
就能满足绝大部分开发的需求了。很多时候简单的 docker compose up -d
就够用了,然后再加个开源的BAAS
平台:supabase
一起组网,开发爽的不要不要的。
说远了,接下来进入本篇的正题:如何设计一个方案,把 Github Repo
转化为我们数据库备份的对象存储,并利用CI
持续化集成呢?
显然,思考如何把大象装进冰箱,我们要把整个过程拆解成以下几个步骤:
这样我们只需要依次实现对应的功能,再把功能串联起来就达到我们的目标了。
本文运行环境:云主机为
华为云
,数据库为postgres
备份数据库通常非常简单,主要分为 2
步:
Amazon S3
/Aliyun OSS
/Tencent Cloud COS
/...这个很容易理解,写个 shell
脚本,导出数据库,上传到 OSS
。然后把它设置成定时任务就行。
然而本文的邪道方案中,我们需要使用 github action
来远程连上云主机,然后执行脚本获取数据库备份,再同步到 git
仓库。
这显然要复杂许多,于是我就学习了一会shell
编程,写了一段脚本,具体思考调试过程可以见注释:
#!/bin/sh
FILENAME=$(date +"%Y%m%d-%H%M%S") # 时间戳文件名
BASENAME="${FILENAME}.dump" # +后缀
KEY_PATH=./xxx.pem # ssh私钥路径
DESTINATION=root@xxx.xxx.xxx.xxx # 云主机登录用户以及ip地址
DUMP_FILE_PATH=/path/to/${BASENAME} # 云主机 dump 文件路径
CONTAINER_NAME=container-name # 云主机数据库容器名称
PG_USER=postgres # 云主机数据库容器登录用户
# Permissions 0400 for './*.pem' are too open
# 修改私钥权限,避免 Permissions too open 问题
chmod 400 $KEY_PATH
echo " -> Connecting $DESTINATION and Dumping"
# dump datebase from docker container
# option StrictHostKeyChecking=accept-new for ssh key prompt
# 这里设置 StrictHostKeyChecking=accept-new 来避免初次由于 .ssh/known_hosts 不存在,导致的 prompt 问题
# 相当于执行了3个命令,ssh <command> / docker exec <command> / sh -c "db_dump"
# 把 dump 出来的数据文件,放到docker的挂载卷中
ssh -o StrictHostKeyChecking=accept-new -i $KEY_PATH $DESTINATION "docker exec -u $PG_USER $CONTAINER_NAME sh -c \"pg_dump -Fc postgres > /var/lib/postgresql/data/${BASENAME}\""
echo " -> Downloading $DUMP_FILE_PATH"
# download dump file to git repo
# 把dump文件下载到本地
scp -i $KEY_PATH $DESTINATION:$DUMP_FILE_PATH ./${BASENAME}
echo " -> Deleting $DUMP_FILE_PATH"
# delete dump file
# 下载完成后,删除服务器上的 dump 文件
ssh -i $KEY_PATH $DESTINATION rm $DUMP_FILE_PATH
echo ""
echo "...done!"
echo ""
其中,使用秘钥和 StrictHostKeyChecking=accept-new
都是为了免 prompt
登录。在执行备份数据库命令时要注意字符串的转义。
另外在调试时,还遇到了一个问题,我们 docker exec -it <c_name>
,进入容器中执行 su <user>
是可行的,但是直接 docker exec <c_name> <command>
里面 su
,生成出来的 dump
文件所属却是 root
! 必须要使用 -u
参数,指定 <user>
才行。这里我并不理解,希望懂的人可以告诉我这个问题的原因。
通过这些步骤,就顺利的把数据库备份文件,给下载到了 git
仓库里了。
既然我们已经下载到了数据库文件了,我们就要对这些文件进行管理。
比如我们目标是,保存最近 7
次备份的文件,那么显然我们要把比较旧的数据库文件给删除掉,那么怎么做呢?
这里我也做了一个简单设计:
DUMPS=$(ls | grep ".dump$") # 获取当前目录所有的 .dump
COUNT=$(echo "$DUMPS" | wc -l) # .dump文件个数
SORTED_LIST=$(echo "$DUMPS" | sort -k1.1n) # 按照时间排个序
KEEP_BLOB_COUNT=7 # 保留数据文件的个数
if [ "$COUNT" -gt "${KEEP_BLOB_COUNT}" ]; then
DEL_COUNT=$(expr $COUNT - ${KEEP_BLOB_COUNT}) # 删除个数
echo "DELETE COUNT:${DEL_COUNT}"
DEL_LIST=$(echo "$SORTED_LIST" | head -n $DEL_COUNT) # 删除文件名列表
for i in $DEL_LIST; do
echo "DELETEING ${i}..."
rm $i # 删除过时的数据
echo "DELETE ${i} SUCCESSFUL!"
done
fi
我们知道 git
仓库一直是在增大的,当我们删除一个文件的时候,看似这个文件从我们的工作目录中消失了,实际上这个文件并没有被删除,而是跑到了 git history
里面去,久而久之这个项目就越来越大了,因为之前所以被删除的文件,还是被保存在 .git
文件夹里。
那么怎么避免这个问题呢?
Github
给了一个解决方案见 removing-sensitive-data-from-a-repository。文章里,给我们介绍了 2
种工具,分别是 java
写的 BFG Repo-Cleaner
和 python
写的 git-filter-repo
。
然而我都不想用...
我回想起了很多年前,看到的一则谣言:程序员枪杀四名同事的新闻。
霎时,想到了一个天怒人怨的命令:git push -f
!
我们可以使用这个方式,强制更新我们的 git
仓库,把它当成一个 OSS
来用啊!
那么脚本就很容易设计出来了:
git config --global user.email "your_name@gmail.com"
git config --global user.name "icebreaker-bot"
git checkout --orphan latest_branch # 创建个纯洁的孤儿分支
git add -A
git commit -am "project recreate"
git branch -D main
git branch -m main
git push -fu origin main # 嘿嘿
当然,这段脚本只适合在自己把控范围内去使用。切勿在工作中使用,不然就会出现几把机械键盘直接砸到脸上的暴力场景。
最后,我们接下来把上述这三段脚本,串联起来。然后写一个 yml
文件交给 action
定时执行就大功告成啦!
name: Sync_Datebase
on:
schedule:
# UTC时间触发
- cron: "0 0 * * *"
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run sync script
run: |
chmod 755 ./bak.sh
chmod 755 ./del.sh
chmod 755 ./git-clear.sh
./bak.sh
这样每次执行完这个脚本,整个仓库焕然一新,历史永远就只有一条了。(笑~)
这种方式去备份数据库,显然是一种邪魔歪道,有点钻牛角尖,不过思考实现的过程却比较有趣,有兴趣的同学可以参照本文实现一下。
还有 Github
ssh
下载实际上速度是很快的,但是由于某些 zg 特色因素,大概率网速会变成小水管。这种情况可以转而使用某些国内代码托管商,来尝试这个方案。
最后方案并不完美,欢迎建议和意见。