5 Jenkins+Docker+SpringCloud微服务持续集成(下)
5.1 Jenkins+Docker+SpringCloud部署方案优化
上面部署方案存在的问题:
-
一次只能选择一个微服务部署
-
-
每个微服务只有一个实例,容错率低
优化方案:
-
在一个Jenkins工程中可以选择多个微服务同时发布
-
在一个Jenkins工程中可以选择多台生产服务器同时部署
-
每个微服务都是以集群高可用形式部署
5.2 Jenkins+Docker+SpringCloud集群部署流程说明
5.3 修改所有微服务配置
注册中心配置
# 集群版
spring
在启动微服务的时候,加入参数: spring.profiles.active 来读取对应的配置
修改其他微服务配置
除了Eureka注册中心以外,其他微服务配置都需要加入所有Eureka服务
# Eureka配置
eureka
提交代码
5.4 设计Jenkins集群项目的构建参数
1)安装Extended Choice Parameter插件
Extended Choice Parameter
2)创建流水线项目
3)添加参数 字符串参数:分支名称
多选框:项目名称
tensquare_eureka_server@10086,tensquare_zuul@10020,tensquare_admin_service@9001,tensquare_gathering@9002
最后效果:
5.5 完成微服务构建镜像,上传私服
//gitlab的凭证
def git_auth = "14ae86e8-c3b4-4d7d-afe1-8c23d9fed317"
//gitlab的地址
def git_url = "git@192.168.5.4:root/tensquare_bak.git"
// 构建版本的名称
def tag = "latest"
//Harbor私服地址
def harbor_url = "192.168.5.5:8080"
//Harbor的项目名称
def harbor_project_name = "tensquare"
//Harbor的凭证
def harbor_auth = "cd0b948d-e82b-4c0c-8a7c-8c6b8fb5454b"
node {
// 获取当前选择的项目名称
def selectedProjects = "${project_name}".split(',')
stage('拉取代码') {
checkout([$class: 'GitSCM', branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]]])
}
stage('代码审查') {
for(int i=0;i<selectedProjects.size();i++){
//取出每个项目的名称和端口
def currentProject = selectedProjects[i];
//项目名称
def currentProjectName = currentProject.split('@')[0]
//项目启动端口
def currentProjectPort = currentProject.split('@')[1]
//定义当前Jenkins的SonarQubeScanner工具
def scannerHome = tool 'sonarqube-scanner'
//引用当前Jenkins SonarQube环境
withSonarQubeEnv('sonarqube') {
sh """
cd ${currentProjectName}
${scannerHome}/bin/sonar-scanner
"""
}
}
}
stage('编译,安装公共工程') {
//编译,安装公共工程
sh "mvn -f tensquare_common clean install"
}
stage('编译,打包微服务工程,上传镜像') {
for(int i=0;i<selectedProjects.size();i++){
//取出每个项目的名称和端口
def currentProject = selectedProjects[i];
//项目名称
def currentProjectName = currentProject.split('@')[0]
//项目启动端口
def currentProjectPort = currentProject.split('@')[1]
//编译,构建本地镜像
sh "mvn -f ${currentProjectName} clean package dockerfile:build"
//定义镜像名称
def imageName = "${currentProjectName}:${tag}"
//给镜像打标签
sh "docker tag ${imageName} ${harbor_url}/${harbor_project_name}/${imageName}"
//登录Harbor,并上传镜像
withCredentials([usernamePassword(credentialsId: "${harbor_auth}",passwordVariable: 'password', usernameVariable: 'username')]) {
//登录
sh "docker login -u ${username} -p ${password} ${harbor_url}"
//上传镜像
sh "docker push ${harbor_url}/${harbor_project_name}/${imageName}"
}
//删除本地镜像
sh "docker rmi -f ${imageName}"
sh "docker rmi -f ${harbor_url}/${harbor_project_name}/${imageName}"
}
}
}
2)编译部署 for循环遍历分割详解
node {
// 获取当前选择的项目名称
def selectedProjects = "${project_name}".split(',')
stage('代码审查') {
for(int i=0;i<selectedProjects.size();i++){
//取出每个项目的名称和端口
def currentProject = selectedProjects[i];
//项目名称
def currentProjectName = currentProject.split('@')[0]
//项目启动端口
def currentProjectPort = currentProject.split('@')[1]
//定义当前Jenkins的SonarQubeScanner工具
def scannerHome = tool 'sonarqube-scanner'
//引用当前Jenkins SonarQube环境
withSonarQubeEnv('sonarqube') {
sh """
cd ${currentProjectName}
${scannerHome}/bin/sonar-scanner
"""
}
}
}
首先在node中定义获取当前项目的变量 def selectedProjects = "${project_name}".split(',')
-
selectedProjects
:这个是变量名称可以随意定义, -
${project_name}
:这里指的Extended Choice Parameter参数化构建中定义的项目名称 -
.split(',')
:这是一个分割语法,意思为以","来进行分割,因为之前在Extended Choice Parameter参数化变量中是这样定义的,同理,如果这里更换为其他的分割服务符号,那么在.split(',')
更换为对应的分割符号tensquare_eureka_server@10086,tensquare_zuul@10020,tensquare_admin_service@9001,tensquare_gathering@9002
-
这时就可以获取到对应的微服务,比如这个样子
selectedProjects=tensquare_eureka_server@10086
selectedProjects=tensquare_zuul@10020
selectedProjects=tensquare_admin_service@9001
selectedProjects=tensquare_gathering@9002目前已经成功获取到对应的微服务,但是还是无法直接使用,因为服务名称和端口号为同一个字符串并且使用"@"符号进行连接,那么这里可以使用之前的方法,对"@"符号进行分割再次分割,得到最后的服务名称和端口
for(int i=0;i<selectedProjects.size();i++){
基础的for循环取出语句
//取出每个项目的名称和端口
def currentProject = selectedProjects[i];
这里可以理解为,逐步取出selectedProjects变量中每个项目的名称及端口号
//项目名称
def currentProjectName = currentProject.split('@')[0]
对currentProject变量以@符号进行分割,并获取第0个字符,可以理解为获取@符号左边的字符,可以得出服务名称
//项目启动端口
def currentProjectPort = currentProject.split('@')[1]
对currentProject变量以@符号进行分割,并获取第1个字符,可以理解为获取@符号右边的字符,可以得出服务名称
经过两次分割(第一个以","分割从Extended Choice Parameter参数化构建中获取服务,第二次以"@"分割分割获取到的服务名称和端口)获得到
-
项目名称变量(currentProjectName)
-
项目端口号变量(currentProjectPort)
在进行代码质量扫描时更换相对于的变量,并且在编译,打包微服务工程,上传镜像时使用的for循环分割与此处逻辑一致
3)编译镜像测试
5.6 完成微服务多服务器远程发布
1)环境配置
-
安装docker
-
拷贝公钥到远程服务器
-
系统配置->添加远程服务器
2)添加参数
多选框:部署服务器
master_server,slave_server_01
最终效果:
3)修改Jenkinsfile构建脚本
//gitlab的凭证
def git_auth = "14ae86e8-c3b4-4d7d-afe1-8c23d9fed317"
//gitlab的地址
def git_url = "git@192.168.5.4:root/tensquare_bak.git"
// 构建版本的名称
def tag = "latest"
//Harbor私服地址
def harbor_url = "192.168.5.5:8080"
//Harbor的项目名称
def harbor_project_name = "tensquare"
//Harbor的凭证
def harbor_auth = "cd0b948d-e82b-4c0c-8a7c-8c6b8fb5454b"
node {
// 获取当前选择的项目名称
def selectedProjects = "${project_name}".split(',')
// 获取当前选择的服务器名称
def selectedServers = "${publish_server}".split(',')
stage('拉取代码') {
checkout([$class: 'GitSCM', branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]]])
}
stage('代码审查') {
for(int i=0;i<selectedProjects.size();i++){
//取出每个项目的名称和端口
def currentProject = selectedProjects[i];
//项目名称
def currentProjectName = currentProject.split('@')[0]
//项目启动端口
def currentProjectPort = currentProject.split('@')[1]
//定义当前Jenkins的SonarQubeScanner工具
def scannerHome = tool 'sonarqube-scanner'
//引用当前Jenkins SonarQube环境
withSonarQubeEnv('sonarqube') {
sh """
cd ${currentProjectName}
${scannerHome}/bin/sonar-scanner
"""
}
}
}
stage('编译,安装公共工程') {
//编译,安装公共工程
sh "mvn -f tensquare_common clean install"
}
stage('编译,打包微服务工程,上传镜像') {
for(int i=0;i<selectedProjects.size();i++){
//取出每个项目的名称和端口
def currentProject = selectedProjects[i];
//项目名称
def currentProjectName = currentProject.split('@')[0]
//项目启动端口
def currentProjectPort = currentProject.split('@')[1]
//编译,构建本地镜像
sh "mvn -f ${currentProjectName} clean package dockerfile:build"
//定义镜像名称
def imageName = "${currentProjectName}:${tag}"
//给镜像打标签
sh "docker tag ${imageName} ${harbor_url}/${harbor_project_name}/${imageName}"
//登录Harbor,并上传镜像
withCredentials([usernamePassword(credentialsId: "${harbor_auth}",passwordVariable: 'password', usernameVariable: 'username')]) {
//登录
sh "docker login -u ${username} -p ${password} ${harbor_url}"
//上传镜像
sh "docker push ${harbor_url}/${harbor_project_name}/${imageName}"
}
//删除本地镜像
sh "docker rmi -f ${imageName}"
sh "docker rmi -f ${harbor_url}/${harbor_project_name}/${imageName}"
//远程部署服务器应用
for(int j=0;j<selectedServers.size();j++){
//每个服务名称
def currentServer = selectedServers[j]
//添加微服务运行时的参数:spring.profiles.active
def activeProfile = "--spring.profiles.active="
if(currentServer=="master_server"){
activeProfile = activeProfile+"eureka-server1"
}else if(currentServer=="slave_server_01"){
activeProfile = activeProfile+"eureka-server2"
}
// 远程触发部署脚本
sshPublisher(publishers: [sshPublisherDesc(configName: "${currentServer}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "/opt/jenkins_shell/deployCluster.sh $harbor_url $harbor_project_name $currentProjectName $tag $currentProjectPort $activeProfile", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
}
echo "${currentProjectName}完成编译,构建镜像!"
}
}
}
4)远程部署 for循环遍历分割详解
for(int j=0;j<selectedServers.size();j++){
//每个服务名称
def currentServer = selectedServers[j]
//添加微服务运行时的参数:spring.profiles.active
def activeProfile = "--spring.profiles.active="
if(currentServer=="master_server"){
activeProfile = activeProfile+"eureka-server1"
}else if(currentServer=="slave_server_01"){
activeProfile = activeProfile+"eureka-server2"
}
}
这里远程部署到对应的服务器和之前编译,打包微服务工程,上传镜像时使用的for循环分割逻辑基本一致,
首先在node中定义获取当前项目的变量 def selectedServers = "${publish_server}".split(',')
-
selectedServers
:这个是变量名称可以随意定义, -
${publish_server}
:这里指的Extended Choice Parameter参数化构建中定义的服务器名称 -
.split(',')
:这是一个分割语法,意思为以","来进行分割,因为之前在Extended Choice Parameter参数化变量中是这样定义的,同理,如果这里更换为其他的分割服务符号,那么在.split(',')
更换为对应的分割符号master_server,slave_server_01
-
这时就可以获取到对应的微服务,比如这个样子
selectedServers = master_server
selectedServers = slave_server_01
目前已经成功获取到对应服务器名称,但是还是无法直接使用,因为要对应之前在eureka网关中定义的服务器地址来进行远程部署
for(int j=0;j<selectedServers.size();j++){
基础的for循环取出语句,这里不能再次使用i,因为在之前已经使用过
//每个服务名称
def currentServer = selectedServers[j]
这里可以理解为,逐步取出selectedServers变量中服务器名称
//添加微服务运行时的参数:spring.profiles.active
def activeProfile = "--spring.profiles.active="
因为要读取服务中的配置文件,所以这里使用变量(activeProfile)定义了参数(--spring.profiles.active)
//if 判断语句
if(currentServer == "master_server"){
activeProfile = activeProfile+"eureka-server1"
}else if(currentServer == "slave_server_01"){
activeProfile = activeProfile+"eureka-server2"
}
这里可以理解为
当currentServer等于master_server时,就读取配置文件中的eureka-server1
当currentServer等于slave_server_01时,就读取配置文件中的eureka-server2
5)编写deployCluster.sh部署脚本
#! /bin/sh
#接收外部参数
harbor_url=$1
harbor_project_name=$2
project_name=$3
tag=$4
port=$5
profile=$6
imageName=$harbor_url/$harbor_project_name/$project_name:$tag
echo "$imageName"
#查询容器是否存在,存在则删除
containerId=`docker ps -a | grep -w ${project_name}:${tag} | awk '{print $1}'`
if [ "$containerId" != "" ] ; then
#停掉容器
docker stop $containerId
#删除容器
docker rm $containerId
echo "成功删除容器"
fi
#查询镜像是否存在,存在则删除
imageId=`docker images | grep -w $project_name | awk '{print $3}'`
if [ "$imageId" != "" ] ; then
#删除镜像
docker rmi -f $imageId
echo "成功删除镜像"
fi
# 登录Harbor私服
docker login -u admin -p Harbor12345 $harbor_url
# 下载镜像
docker pull $imageName
# 启动容器
docker run -di -p $port:$port $imageName $profile
echo "容器启动成功"
6)项目测试
5.7 Nginx+Zuul集群实现高可用网关
1)添加nginx.conf文件
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
upstream zuulServer{
server 192.168.5.6:10020 weight=1;
server 192.168.5.7:10020 weight=1;
}
server {
listen 80;
listen [::]:80;
server_name localhost;
#access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
server {
listen 85;
listen [::]:85;
server_name _;
root /usr/share/nginx/html;
# Load configuration files for the default server block.
location / {
### 指定服务器负载均衡服务器
proxy_pass http://zuulServer/;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
2)修改Dockerfile
FROM nginx
COPY ./dist /usr/share/nginx/html
COPY ./nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
3)修改代码连接后端微服务服务
4)项目测试
由于是测试阶段,如果前端容器没有开通85端口,那么一定会造成Network Error
网关错误,正常环境中建议部署一台专门的转发服务器,来达成轮询访问的效果
造成这种原因是因为转发请求并没有到达容器内的Nginx 所有并不能完成转发,本次测试也是在启动容器前,修改了容器启动脚本,开通了85端口,所以可以正常访问
5.8 Jenkins的Master-Slave分布式构建
5.8.1 什么是Master-Slave分布式构建
Jenkins的Master-Slave分布式构建,就是通过将构建过程分配到从属Slave节点上,从而减轻Master节点的压力,而且可以同时构建多个,有点类似负载均衡的概念。
5.8.2 如何实现Master-Slave分布式构建
1)开启代理程序的TCP端口
Manage Jenkins -> Configure Global Security
2)新建节点
Manage Jenkins—Manage Nodes—新建节点
3)节点部署
下载jar包到slave节点服务器(slave节点服务器必须有java环境)
java -jar agent.jar -jnlpUrl http://192.168.5.3:808/computer/slave_01/jenkins-agent.jnlp -secret 2d2371ae0ed0e91ad74c67aabecb5af24d001ff7f5fb440cb5bc663b57a52898 -workDir "/root/jenkins"
4)完成部署
5)拉取测试