实现功能提要:我们在某服务器上搭建了测试环境进行项目功能开发,同时在其他多个集群上部署了多套相同的系统供公司内部人员使用,由于系统还在持续功能添加和完善中,每隔一段时间都需要对其他集群的系统进行手动更新,而更新的第一步就是数据库更新,为保证数据能完整同步到其他系统,我们测试过程中涉及到的DDL和DML操作都会记录在一个表更新sql文件中,便于其他系统数据库执行;但手动更新一段时间后发现每个集群系统更新进度不一样,导致每次数据库手动更新十分麻烦,为次,我准备写一个数据库自动更新脚本,在进行系统更新前,只需要一个启动脚本命令便可实现数据库更新。
脚本文件由三部分组成:start.sh,ansible.yaml,sql(sql目录下有new.sql,old.sql,diff.sql三个文件)
1、启动脚本start.sh
#!/bin/sh
#进入当前脚本目录
cd `dirname $0`;
# 旧文件清理
sql_path="./sql"
if [ -d "${sql_path}" ]; then
rm -rf ${sql_path}/*
else
mkdir ${sql_path}
fi
touch ${sql_path}/old.sql ${sql_path}/new.sql ${sql_path}/diff.sql
newsql="${sql_path}/new.sql"
oldsql="${sql_path}/old.sql"
diffsql="${sql_path}/diff.sql"
# 在bm-api-server pod中执行解压命令解压bm-server-biz.jar文件
bm_api_server=$(kubectl get pods -n bm-system | grep -e "^bm-api-server" | awk '{print $1}' | uniq)
kubectl -n bm-system exec -it $bm_api_server -- /bin/bash -c "jar -xvf bm-server-biz.jar BOOT-INF/classes/db/migration/V1.0.4__update_20221228.sql"
#复制old.sql到node01上
kubectl cp -n bm-system $bm_api_server:BOOT-INF/classes/db/migration/V1.0.4__update_20221228.sql ${oldsql}
#判断old.sql是否复制下来
if [ -s "${oldsql}" ]; then
echo "Then ols.sql is ok"
else
echo "Then old.sql is empty."
exit 1
fi
#从gitlab仓库获取最新版本的表更新sql文件
ansible-playbook ./ansible.yaml
#判断new.sql是否复制下来
if [ -s "$newsql" ]; then
echo "Then new.sql is ok."
kubectl -n bm-system exec -it $bm_api_server -- /bin/bash -c "rm -rf BOOT-INF/"
else
echo "Then new.sql is empty."
exit 1
fi
# 找出new.sql中不同的行
diff_lines=$(diff "$newsql" "$oldsql" | grep "^<" | sed -e 's/^< //')
# 输出不同行
if [[ -n "$diff_lines" ]]; then
echo "Different lines in $newsql:"
echo "$diff_lines" >> "${diffsql}"
else
echo "There is no difference between $newsql and $oldsql."
exit 1
fi
# 在数据库中执行需要更新的sql
database=$(kubectl get pods -n bm-system | grep -e "^metadata" | awk '{print $1}' | uniq)
kubectl -n dmcp-system exec -it ${database} -- /bin/bash -c "[ -f ./diff.sql ] && rm diff.sql && touch diff.sql;[ ! -f ./diff.sql ] && touch diff.sql"
#复制diff.sql复制到metadata pod中
kubectl cp -n bm-system ./sql/diff.sql $database:diff.sql
#执行sql
kubectl -n bm-system exec -it $database -- /bin/bash -c "cd /opt/dmdbms/bin ; ./disql BM/Baomi <<EOF
start /home/dmdba/diff.sql
EOF"
2、ansible.yaml
- name: Checkout new.sql file from the Git repository
hosts: localhost
become: true
vars:
git_repository: https://username:password@gitlab.com/group/repository.git
git_branch: master
tasks:
- name: Clone the Git repository
git:
repo: "{{ git_repository }}"
dest: /tmp/repo
update: yes
version: "{{ git_branch }}"
depth: 1
recursive: yes
- name: Copy file to destination
copy:
src: /tmp/repo/bm-server-biz/docs/update.sql
dest: /root/bm-sql-update/sql/new.sql
实现思路是,首先需要获取当前集群运行系统的表更新sql文件,其中一个办法就是到包含该文件的子系统中获取,由于该子系统的pod中是有maven构建的jar包的,只需要将jar包中表更新文件解压至指定目录,然后复制到集群本地,称该获取的文件为old.sql文件;然后通过ansible 的git获取到最新文件,称为new.sql;然后对比两个文件的差异(主要是要获取到从上次更新到目前新增的sql语句),将差异写进diff.sql中;最后将diff.sql复制到数据库pod中,并连接数据库实例,批量执行sql,于是就实现了该集群数据库的更新。
整个过程不难理解,只是在测试过程中出现了一些小的问题,下面将记录这些问题和解决方案。
1)如何实现在pod外面执行pod内部命令
使用kubectl exec命令进入到pod,再使用-c参数执行命令,-c 选项用于指定要执行命令的容器名称,指定该选项后,kubectl exec 命令将在指定的容器内执行命令。如果不指定该选项,则默认使用第一个容器。
如果要执行多条命令,可以使用分号 ; 或双竖线 || 连接多条命令。例如,要在指定的容器内执行两个命令,可以这样写:kubectl exec -it podname -c containername sh -c "command1; command2"
在上面的命令中,command1 和 command2 是要执行的两个命令。sh -c 表示要使用 Bash shell 来解释命令。
如果需要在多个容器中执行命令,可以使用 -c 选项指定多个容器名称,并使用 && 连接多个命令。例如:kubectl exec -it podname -c container1 sh -c "command1" && \ kubectl exec -it podname -c container2 sh -c "command2"
在上面的命令中,&& 表示只有前一个命令成功执行后,才会继续执行后续命令。这样可以确保命令执行的顺序和成功率。
2)实现k8s本地和pod间文件传输
"kubectl cp "命令用于将文件从pods复制到本地路径,反之亦然。
将文件从pod复制到本地
kubectl cp <pod_name>:<file_path> <destination_path>
将文件从pod的特定容器复制到本地
kubectl cp <pod_name>:<file_path> <destination_path> -c specific_container
将文件从本地复制到pod
kubectl cp <local_source_path> <pod_name>:<destination_path>
但是要注意的是,pod冒号后要直接加根目录,不能加“/”,否则报错。如以下命令会报错,需要将home前面的/去掉,另外本地路劲必须指向具体文件而不是一个目录,如将./test.cap改成./就会报错。
kubectl cp aks-ssh2-6cd4948f6f-fp9tl:/home/azureuser/test.cap ./test.cap
3)执行脚本的时候报错:syntax error: unexpected end of file!
用 Linux 系统编写的 shell 脚本,在 vim 的命令模式下,执行 :set ff 就能看到 fileformat 类型是 unix ,那么你的 shell 脚本里存在语法错误,可以使用二分法逐段检查错误,具体就是注释一部分,留一部分,然后执行 sh -n 脚本名字 来检查错误,知道排查处错误为止。
4)jar包解压某个文件到目录命令
jar -xvf bm-server-biz.jar BOOT-INF/classes/db/migration/update_20221228.sql
5)shell脚本细节
1、判断文件是否为空。使用文件路径作为参数,并使用 -s 测试操作符检查文件是否为空,r若为空则用exit 1 停止整个脚本,如:
file_path=$file if [ -s "$file_path" ]; then echo "file is not empty." else echo "file is empty." exit 1 fi
2、进入当前脚本目录
cd `dirname $0` 或者 cd $(dirname $0)
3、判断文件或者目录是否存在
# -f判断文件 FILE=/etc/resolv.conf if [ -f "$FILE" ]; then echo "$FILE exist" fi # -d判断目录 FILE=/etc/docker if [ -d "$FILE" ]; then echo "$FILE is a directory" fi # 或者 [ -d /etc/docker ] && echo "$FILE is a directory"
6)将拉到本地/tmp/repo/目录下的内容删除后,再执行ansible.yaml,出现报错:fatal: [localhost]: FAILED! => {"before": "19529cb0506ce62ea5609b3e97cc958618e5f226", "changed": false, "msg": "Local modifications exist in repository (force=no)."}
该错误提示说明Git仓库中已经存在本地未提交的修改,并且在执行git clone或git pull命令时指定了force=no参数,因此Git不允许覆盖本地的修改。解决方案是:强制覆盖本地修改;通过强制拉取Git仓库并覆盖本地修改。
cd /tmp/repo git reset --hard origin/master git clean -f -d git pull origin master
上述命令将会重置Git仓库的状态,并将本地工作目录中的所有文件还原为与远程仓库相同状态。
优化:
经测试,该数据库更新脚本能够实现数据库的更新,但是缺点也很明显:如脚本中涉及到ansible剧本,并不是所有的集群环境里都有ansible,所以需要想办法把这部分功能换成shell命令;另外就是实现过程还是有些复杂且不够自动化,经过导师的建议,我决定对其进行改进,利用Jenkins实现一个全自动化集群数据库更新方案,如下是实现的jenkinsfile脚本:
def checkoutRepo(repoUrl, path) {
checkout([
$class: 'GitSCM',
branches: [[name: 'origin/master']],
doGenerateSubmoduleConfigurations: false,
extensions: [[$class: 'SparseCheckoutPaths', sparseCheckoutPaths: [[path: path]]]],
submoduleCfg: [],
userRemoteConfigs: [[credentialsId: 'lsn', url: "git@git.damengbm.com:bm/${repoUrl}"]]
])
}
pipeline {
agent {
node {
label 'bm_115.160'
}
}
stages {
stage('change cluster') {
steps {
script {
def config_cluster = "${params.CONFIG_CLUSTER}"
withCredentials([file(credentialsId: config_cluster, variable: 'CONFIG_FILE')]) {
sh '''
cat $CONFIG_FILE > /root/.kube/config
export KUBECONFIG=/root/.kube/config
kubectl -n bm-system get pods
'''
}
}
}
}
stage('get old sql') {
steps {
sh '''
mkdir -p sql-update
touch sql-update/server_old.sql sql-update/ticket_old.sql
touch sql-update/server_new.sql sql-update/ticket_new.sql
cp /home/bm/workspace/bm-api-server/bm-server-biz/docs/表变更update.sql sql-update/server_old.sql
cp /home/bm/workspace/bm-api-ticket/bm-server-ticket/doc/表变更-update.sql sql-update/ticket_old.sql
'''
}
}
stage('git Checkout get new sql') {
steps {
script {
checkoutRepo("bm-api-server.git", "bm-server-biz/docs/表变更update.sql")
sh '''
cp bm-server-biz/docs/表变更update.sql sql-update/server_new.sql
rm -rf bm-server-biz/
'''
checkoutRepo("bm-api-ticket.git", "bm-server-ticket/doc/表变更-update.sql")
sh '''
cp bm-server-ticket/doc/表变更-update.sql sql-update/ticket_new.sql
rm -rf bm-server-ticket/
'''
}
}
}
stage('compare SQL Files') {
steps {
sh '''
diff --unchanged-line-format= --old-line-format= --new-line-format='%L' sql-update/server_old.sql sql-update/server_new.sql > sql-update/diff.sql || :
diff --unchanged-line-format= --old-line-format= --new-line-format='%L' sql-update/ticket_old.sql sql-update/ticket_new.sql >> sql-update/diff.sql || :
'''
}
}
stage('copy diff.sql File') {
steps {
sh '''
database=$(kubectl get pods -n bm-system | grep -e "^metadata" | awk '{print $1}' | uniq)
echo ${database}
kubectl cp -n bm-system sql-update/diff.sql ${database}:diff.sql
'''
}
}
stage('execute diff.sql File') {
steps {
sh '''
database=$(kubectl get pods -n bm-system | grep -e "^metadata" | awk '{print $1}' | uniq)
kubectl -n bm-system exec -i $database -- /bin/bash -c "cd /opt/dmdbms/bin ; ./disql DM_BM/Dbm8888 <<EOF
start /home/dmdba/diff.sql
EOF"
'''
}
}
}
}
最新版本:之前的版本获取old.sql是通过到api-server工程工作区下获取,也就意味着sql-update和api-server两个必须有一个先后执行顺序,即先执行sql-update再执行api-server,否则将不能获取到旧的sql文件,因为jenkins工程构建时会先检查源码管理部分是否开启,开启的话就根据url拉取代码,代码更新后再执行构建步骤中的shell脚本,我有想过改动api-server使其在拉取代码前先把旧的sql文件备份,查询到如下资料:
在Jenkins中,工程的执行顺序是可以根据你的配置进行调整的。
默认情况下,在构建过程中,Jenkins会先进行源代码拉取(从配置的Git仓库URL中拉取代码),然后执行构建步骤中定义的操作(如执行Shell脚本)。
这是因为源代码获取是构建过程中的一项关键步骤,通常需要先获取代码才能进行后续的构建和测试操作。
如果你的构建过程需要在代码拉取之前执行某些操作,你可以使用Jenkins的插件或命令来自定义构建流程。例如:1、Pre-SCM Build Step插件:该插件允许在源代码管理之前执行某些操作。你可以在构建步骤中添加一个"Pre-SCM Build Step"来定义在代码拉取之前需要执行的脚本或命令。
2、在构建步骤中添加额外的步骤:你可以在构建步骤中添加多个步骤,并根据需要进行排序。通过调整步骤的顺序,你可以控制在代码拉取之前或之后执行特定的操作。请注意,这些配置选项和插件的可用性可能取决于你所使用的Jenkins版本和安装的插件。建议参考Jenkins的文档和插件文档,以了解更多关于自定义构建流程的选项和最佳实践。
因为安装新的插件又要重启服务,而jenkins重启给我带来了很多麻烦,于是没有查到有这个插件后就放弃了这个办法;下面是尝试第二种办法:
#!/bin/bash
# 备份旧的sql文件
# 清理旧的代码目录(可选)
rm -rf api-server
# 克隆 Git 仓库到本地,只拉取api-server目录
git clone --depth 1 --branch <branch-name> <Git repository URL> api-server
# 切换到api-server目录
cd api-server
# 可以执行其他构建步骤,例如编译、测试等
# 在这之后需要执行的其他命令或脚本
第二个办法在测试过程中也发现了新的问题,也就是将旧的sql文件备份到old.sql后,如果先构建api-server,那么old.sql中是他上一次的旧文件,但到了构建sql-update时,他如何判断old.sql是新的还是旧的呢?要想在sql-update中顾全所有构建情况,这将是个很麻烦的事情,于是不得不想其他办法,比如将sql-update独立起来,不让他依赖api-server获取旧文件,这时我才意识到早该这样做了,下面是最新的jenkinsfile(也可以用shell脚本实现,因为在jenkinsfiile中执行shell命令太多要注意的了,之后会再尝试一下):
def checkoutRepo(repoUrl, path) {
checkout([
$class: 'GitSCM',
branches: [[name: 'origin/master']],
doGenerateSubmoduleConfigurations: false,
extensions: [[$class: 'SparseCheckoutPaths', sparseCheckoutPaths: [[path: path]]]],
submoduleCfg: [],
userRemoteConfigs: [[credentialsId: 'lsn', url: "git@git.dameng.com:mcp/${repoUrl}"]]
])
}
pipeline {
agent {
node {
label 'mcp_113.160'
}
}
stages {
stage('change cluster') {
steps {
script {
def config_cluster = "${params.CONFIG_CLUSTER}"
withCredentials([file(credentialsId: config_cluster, variable: 'CONFIG_FILE')]) {
sh '''
cat $CONFIG_FILE > /root/.kube/config
export KUBECONFIG=/root/.kube/config
kubectl -n dmcp-system get pods
'''
sh "echo '正在连接${params.CONFIG_CLUSTER}集群'"
}
}
}
}
stage('get diff sql') {
steps {
script {
//sh 'mkdir sql-update'
checkoutRepo("mcp-api-server.git", "mcp-server-biz/docs/表变更update.sql")
sh """
if [ ! -f 'sql-update/server${params.CONFIG_CLUSTER}' ]; then
touch sql-update/server${params.CONFIG_CLUSTER}
cp mcp-server-biz/docs/表变更update.sql sql-update/diff.sql
else
diff --unchanged-line-format= --old-line-format= --new-line-format='%L' sql-update/server${params.CONFIG_CLUSTER} mcp-server-biz/docs/表变更update.sql > sql-update/diff.sql || :
fi
rm -rf mcp-server-biz/
"""
/* checkoutRepo("mcp-api-ticket.git", "mcp-server-ticket/doc/表变更-update.sql")
sh """
if [ ! -f 'sql-update/ticket${params.CONFIG_CLUSTER}' ]; then
cp mcp-server-ticket/doc/表变更-update.sql sql-update/ticket${params.CONFIG_CLUSTER}
cat mcp-server-ticket/doc/表变更-update.sql >> sql-update/diff.sql
else
diff --unchanged-line-format= --old-line-format= --new-line-format='%L' sql-update/ticket${params.CONFIG_CLUSTER} mcp-server-ticket/doc/表变更-update.sql >> sql-update/diff.sql || :
fi
rm -rf mcp-server-ticket/
"""*/
}
}
}
stage('copy diff.sql File') {
steps {
sh '''
database=$(kubectl get pods -n dmcp-system | grep -e "^metadata" | awk '{print $1}' | uniq)
echo ${database}
kubectl -n dmcp-system exec -i $database -- /bin/bash -c \'if [ ! -f "./diff.sql" ]; then touch diff.sql; fi\'
kubectl cp -n dmcp-system sql-update/diff.sql ${database}:diff.sql
'''
}
}
stage('execute diff.sql File') {
steps {
sh '''
database=$(kubectl get pods -n dmcp-system | grep -e "^metadata" | awk '{print $1}' | uniq)
kubectl -n dmcp-system exec -i $database -- /bin/bash -c "cd /opt/dmdbms/bin ; ./disql DM_MCP/Dameng8888 <<EOF
start /home/dmdba/diff.sql
EOF"
'''
}
}
stage('update sql file') {
steps {
sh """
cat sql-update/diff.sql >> sql-update/server${params.CONFIG_CLUSTER}
"""
}
}
}
}