介于传统巨石服务与微服务之间的一种选择

背景

公司有大部分开发人员缺乏去积极学习新技术新知识的行动, 甚至于就算给他们培训, 他们也懒得听或者觉得自己什么都懂. 所以考虑到当前团队和公司规模的一些因素,在系统架构升级过程中选择了一种折中的方案,兼顾上下,找到需求与技术的平衡点。

以我们的经验分析,tomcat服务下会部署一个或多个Web项目,具体看项目需求。一个项目中会混杂着各种功能模块的实现,每个功能模块的实现升级或bug修复都涉及到服务重启,虽然有nginx作为前置代理,但多少还是影响到客户的体验,而且一不小心重大bug全线崩盘...😂

Spring boot的出现给我们带来开发的便利点. 开始一个新的项目模块无需再去搞半天配置, 然后才开始写自己的业务逻辑. 只需打开Spring Initializr, 简单的点几下, 打开IDE就可以直接干活了. Springboot自带嵌入Tomcat的方式也是极其重要的一个功能点, 打包jar直接运行,** Make jar not war**. 直接上SpringCloud那套团队要掌握的知识点太多, 如果只是去掌握Springboot这套开发框架的话学习成本要低得多, 可以说是对传统开发思维模式基本没有什么影响. 大概就这么个背景促成了这个选择, 下面简单描述一下架构逻辑, 大概了解之后再决定要不要继续读下去, 避免浪费你的时间.

架构逻辑概览


仅看这图可能是没法看明白的, 相对简单了点. 下面以一个具体的请求例子来描述.

以请求获取用户信息为例, 如下:

https://api.demo.com/mars/user/get?id=user001

大家应该都很熟悉这里包含的信息类型, 我们大概拆分成以下几部分:

  • 请求服务域名: api.demo.com
  • 服务名称: mars
  • 请求路由映射: /user/get
  • 参数: id=user001

我们要获取的用户信息包括以下几块:

  • 用户基本信息(姓名, 电话, 地址.....)
  • 银行账户信息(信用卡信息, 储蓄卡信息, 借贷信息..)
  • 用户开通业务信息
  • 最近交易信息
  • 用户账户审计信息
  • .....还有其他信息

我们之前的做法是通过在同一个实例里面的一个入口方法请求不同的其他模块方法, 然后聚合返回前端所需要的数据. 不管怎么分包, 其实还是在同一个JVM实例里面运行.

现在做法是根据功能模块拆分成多个服务, 比如用户基本信息是一个单独的REST服务(单独可以运行的jar), 交易信息又是另外一个...

这里入口服务为mars, 那mars里面的一个入口方法会请求不同的服务REST接口(多次网络请求, 这里面又可以有多种玩法), 或许你会担心响应速度问题, 实际测试发现是有影响的, 但是这点影响对于人类感知来说基本可以忽略不计.

一个实例的服务以REST API的方式在多个服务之间共享, 但是实际并没有服务注册中心, 那么当部署多个服务实例的时候或者服务发布的时候怎么去保证服务的可用? 这个问题可以在Kong这个报网关里很好的解决. 关于Kong的介绍这里就不啰嗦, 官方文档很完美, 直接去官网看. 如果你使用或了解过Nginx, 看看这个图就明白了.

实际行动

说归说, 实际还得行动起来才知道行不行. 这套结构里面可灵活变通的东西很多, 不方便一一解释描述, 下面从0开始以我们实际情况来做个demo.

网络规划

既然云服务器是如此的流行, 那我们就以云服务器来实现, 实际自己去租IDC机柜部署也是大同小异. 所有服务器都在一个VPC下, 一共4台ECS, 网关服务器绑定弹性公网IP, 开放https服务.

一台网关服务器

Kong服务安装在这台上面, Kong的安装请参考官方文档, 入口防火墙开放443端口服务.
IP地址配置: 192.168.0.1 .

一台WEB服务器

用于部署前端服务, 没什么实际业务操作, 主要是为了方便扩展和业务数据安全隔离, WEB服务可能会请求到后端的多个Service服务, 聚合返回所需数据模型.
IP地址配置: 192.168.0.10

一台Service服务器

用于部署Service服务, 数据加解密, 业务逻辑处理集中在这里, 数据库基本上就只充当数据存储的功能, 没有Function, procedure这些.
IP地址配置: 192.168.0.20

一台数据库服务器

这个没什么特别的, 就是数据存储, 满足CRUD, 根据业务规模做好规划.
IP地址配置: 192.168.0.60

服务发布

把原来仅有的6个服务拆分成多个, 拆分后前后端服务加起来有30+, 这么多个服务还要多个实例部署, 手工操作肯定是不太现实的. 所以Jenkins持续构建就显得十分的重要.

Jenkins管道发布的创建

以前想在Jenkins实现自动发布一整套, 需要需要插件的配合, 先Jenkins有了pipeline的支持, 新建一个Job和Jenkins的交互配置减少了基本上一步完成, 剩余的都集中再Jenkinsfile的编写上. 关于Jenkinsfile脚本的编写, 请参考官方的Jenkinsfile文档

Jenkinsfile的里支持Groovy基本, 这很受程序猿的欢迎, 只要可以写代码, 有什么不可能的, O(∩_∩)O~. 这里定义不同的阶段去实现所需的操作, 通过Blue Ocean UI可以很直观的看到整个构建的过程. e.g.

Jenkinsfile关键步骤

在编写构建脚本文件时需要实现以下几个功能点:

  1. 服务构建之前需要先构建相应的组件(jar或者配置文件之类的), 组件直接优先关系也要定义清楚.
  2. 服务多实例部署在同一台机子上需要的端口定义.
  3. 服务发布模式: 滚动/金丝雀(先定义在基本里, 构建时根据用户选择执行)
  4. 服务发布前先将当前待替换的服务状态设置为服务不可用(http状态码503)
  5. 服务实例发布完成, 将该实例服务状态设置为可用(http状态码200)
  6. 当前实例成功发布, 继续下一个实例发布, 失败根据策略回滚, 者停止或恢复到指定版本.

Jenkinsfile 样例:

pipeline {
    agent any
    
    parameters {
     	string(name: 'LAST_SUCC_VERSION', defaultValue: 'X', description: '请输入上一个正确发布的版本(上一个build里的Revision: xxxxx),选择【滚动发布】此项无效')
        choice(name: 'REQUESTED_NODE_ACTION', choices: ['2','4','6','8','10'], description: '请选择应用部署节点个数')
        choice(name: 'DEPLOY_STRATEGY', choices: ['RollingUpdate','Canary'], description: '请选择应用部署策略:\n RollingUpdate: 滚动(🔁)发布, 中途不会暂停。\n Canary: 金丝雀(🐦)发布, 中途需要人工干预。')
    }
    
    environment { 
        def M2_HOME = '/opt/devops/apps/maven/bin'
        
        def R_SSH_PORT = '22'
        
        def DEPLOY_NAME = 'mars-1.0.0.jar'
        
        def WEB_NODE_01 = 'rock-gateway-node-01'
        
        def WEB_NODE_02 = 'rock-gateway-node-02'
        
        def BUNDLE_PATH = '/opt/boot-cls/bundle'
        
        def BIN_PATH = '/opt/boot-cls/bin'
    }
    
    stages {
    	stage('Strategy') {
            steps {
                echo "...Deploy ${params.REQUESTED_NODE_ACTION} Nodes Service With ${params.DEPLOY_STRATEGY} Strategy...";
                script{
                	if('Canary' == params.DEPLOY_STRATEGY){
                		echo "...Recovery standby version is ${params.LAST_SUCC_VERSION}...";
                	}
                }
        	}
        }
        stage('PreBuild') {
            steps {
                echo '...Building Env Prepare...'
                build job: 'config-files', propagate: true
            }
        }
        stage('rock-sextans') {
            steps {
                echo '...Building IPay-Sextans...'
                build job: 'rock-sextans', propagate: true
            }
        }
        stage('Build') {
            steps {
                echo '...Building...'
                sh '${M2_HOME}/mvn clean install -U -Ppro'
                archiveArtifacts artifacts: '**/target/*.jar', fingerprint: true
            }
        }
        stage('Unit Tests') {
            steps {
                echo '...Unit Testing..'
            }
        }
        stage ('Deployment') {
        	steps {
        		script{
    				def deployNodes = ['10010', '10011', '10012', '10013', '10014', '10015', '10016', '10017', '10018', '10019']
    				def serviceName = "mars"
    				def nodeNumbers = params.REQUESTED_NODE_ACTION as Integer;
    				
    				echo "...${nodeNumbers} Nodes to deploy"
    				
    				if(nodeNumbers>10){
    					error "...Can not perform deploy action, root cause: exceed max node support"
						currentBuild.result = 'FAILURE'
						sh "exit 1"
    				}
    				
                    for (int i = 0; i < nodeNumbers; ++i) {
                    	
                    	showStartBuildMsg("${deployNodes[i]}");
                    	
                    	echo "...Set ${serviceName}-${deployNodes[i]} service status to 503"
                    	setNodeStatus("${WEB_NODE_01}", "${deployNodes[i]}", "${serviceName}")
                    	
                        echo "...Deploying ${DEPLOY_NAME} on node ${WEB_NODE_01} use port ${deployNodes[i]}"
                        deploy2Node("${WEB_NODE_01}","${serviceName}-${deployNodes[i]}-deploy.sh")
                        
                        echo "...Check boot status"
	                	bootStatus("${WEB_NODE_01}", "${deployNodes[i]}", "${serviceName}")
	                	
	                	echo "...Check myStatus"
	                	e2eStatus("${WEB_NODE_01}", "${deployNodes[i]}", "${serviceName}")
	                	
	                	showDeployStatus("${deployNodes[i]}", "${serviceName}");
	                	
	                	echo "...Deploying ${DEPLOY_NAME} on standby node ${WEB_NODE_02} with port ${deployNodes[i]}"
                        deploy2StandByNode("${WEB_NODE_02}","${serviceName}-${deployNodes[i]}-standby.sh")
	                	
	                	showStandByNodeStatus("${WEB_NODE_02}", "${deployNodes[i]}", "${serviceName}");
	                	
	                	if('Canary' == params.DEPLOY_STRATEGY && i < nodeNumbers-1){
	                		
	                		def decisionDesc = '0- 继续部署下一个节点服务;  1-停止部署操作,并停止当前已部署节点服务;  终止(Abort)操作不会停止已经部署完成的节点服务';
	                		def optionsList = ['0','1'];
	                	
	                		if('X' != params.LAST_SUCC_VERSION){
	                			
	                			decisionDesc = '0- 继续部署下一个节点服务;  1-停止部署操作,并停止当前已部署节点服务; 2-恢复到正确版本: '+params.LAST_SUCC_VERSION+'; 终止(Abort)操作不会停止已经部署完成的节点服务';
	                			optionsList = ['0','1','2'];
	                		}
	                	
	                		def userChoice = input(ok:'确认', message: '请选择继续下一步操作?', parameters: [choice(name: 'MAKE_A_DECISION', choices: optionsList, description: decisionDesc)]);
			
				            if('1' == userChoice){
				            	
				            	stopMsg("${deployNodes[i]}")
				            	stopNodeService("${WEB_NODE_01}","${serviceName}-${deployNodes[i]}-ctr.sh")
				            	echo ".......Stop current deployed version completed........."
				            	
			            		echo ".......The build mission terminated🛑 by user........."
				            	currentBuild.result = 'FAILURE'
								sh "exit 1"
				            }
				            
				            if('2' == userChoice){
				            	
				            	stopMsg("${deployNodes[i]}")
				            	stopNodeService("${WEB_NODE_01}","${serviceName}-${deployNodes[i]}-ctr.sh")
			            		echo ".......Stop current deployed version completed........."
			            		
			            		recoverMsg(params.LAST_SUCC_VERSION)
			            		recoverNodeService("${WEB_NODE_01}","${serviceName}-${deployNodes[i]}-recover.sh", params.LAST_SUCC_VERSION)
			            		echo ".......Recover to version "+params.LAST_SUCC_VERSION+" mission accomplished........."
			            		
				            	currentBuild.result = 'FAILURE'
								sh "exit 1"
				            }
	                	}
                    }
                }
            }
        }
        stage("Completion"){
        	steps {
        		echo "⭐⭐⭐⭐⭐ Mission complete, everything goes smoothly⭐⭐⭐⭐⭐"
        	}
        }
    }
}

def showStartBuildMsg(nodePort){
	stage('Deploying '+ nodePort) {
		echo "...Deploying ${nodePort} Node Service"
	}
}

def stopMsg(nodePort){
	stage('Stop '+ nodePort) {
		echo "...Stop ${nodePort} Node Service"
	}
}

def recoverMsg(lastSuccssVersion){
	stage('Recover v'+ lastSuccssVersion) {
		echo "...Recover v${lastSuccssVersion} Node Service"
	}
}

def stopNodeService(nodeName, ctrScript){
	
	sh "ssh -p$R_SSH_PORT devops@${nodeName} sh ${BIN_PATH}/${ctrScript} stop"
	sleep time: 1, unit: 'SECONDS'
}

def showDeployStatus(nodePort, serviceName){

	stage(nodePort+' Deployed') {
		echo "...${serviceName}-${nodePort} Deployed successfully...🐸"
	}
}

def recoverNodeService(nodeName, recoveryScript, lastSuccVersion){
	
    sh "ssh -p$R_SSH_PORT devops@${nodeName} sh ${BUNDLE_PATH}/${recoveryScript} recovery ${lastSuccVersion}"
	sleep time: 35, unit: 'SECONDS'
}

def deploy2StandByNode(nodeName, standbyScript){
	
    sh "ssh -p$R_SSH_PORT devops@${nodeName} sh ${BUNDLE_PATH}/${standbyScript} standby"
	sleep time: 15, unit: 'SECONDS'
}

def showStandByNodeStatus(nodeName, nodePort, serviceName){

	stage(nodeName+' StandBy') {
		echo "...${serviceName}-${nodePort} StandBy Deployed successfully...🐸"
	}
}

def setNodeStatus(nodeName, nodePort, serviceName){
	
	try{
	    HEALTH_STATUS = sh(returnStdout: true, script: "curl -s \"http://${nodeName}:${nodePort}/${serviceName}/actuator/health\" | jq -r \".status\"").trim()
		if (HEALTH_STATUS != 'DOWN') {
			sh "curl -s \"http://${nodeName}:${nodePort}/${serviceName}/outOfService?key=IAMSURE\""
			sleep time: 35, unit: 'SECONDS'
		}
		
		HEALTH_STATUS = sh(returnStdout: true, script: "curl -s \"http://${nodeName}:${nodePort}/${serviceName}/actuator/health\" | jq -r \".status\"").trim()
	    if (HEALTH_STATUS != 'DOWN') {
			error "Set ${serviceName}-${nodePort} status failed: ${HEALTH_STATUS}"
			currentBuild.result = 'FAILURE'
			sh "exit 1"
		}else{
			echo "...Set ${serviceName}-${nodePort} status to 503 successfully"
		}
		sleep time: 35, unit: 'SECONDS'
	}catch(exc){
		echo "...${serviceName}-${nodePort} not start yet: " + exc.toString()
	}
}

def deploy2Node(nodeName, deployScript){
	
    sh "ssh -p$R_SSH_PORT devops@${nodeName} sh ${BUNDLE_PATH}/${deployScript} deploy"
	sleep time: 35, unit: 'SECONDS'
}

def bootStatus(nodeName, nodePort, serviceName){

	stage('Boot '+nodePort) {
		HEALTH_STATUS = sh(returnStdout: true, script: "curl -s \"http://${nodeName}:${nodePort}/${serviceName}/actuator/health\" | jq -r \".status\"").trim()
	    if (HEALTH_STATUS != 'UP') {
			error "🐞 ${serviceName}-${nodePort} boot status is ${HEALTH_STATUS}"
			currentBuild.result = 'FAILURE'
			sh "exit 1"
		}else{
			echo "...${serviceName}-${nodePort} boot status UP🐸"
		}
	}
}

def e2eStatus(nodeName, nodePort, serviceName){

	stage('E2E '+nodePort) {
		HEALTH_STATUS = sh(returnStdout: true, script: "curl -s http://${nodeName}:${nodePort}/${serviceName}/myStatus").trim()
	    if (HEALTH_STATUS != 'UP') {
			error "🐞 ${serviceName}-${nodePort} E2E status is ${HEALTH_STATUS}"
			currentBuild.result = 'FAILURE'
			sh "exit 1"
		}else{
			echo "...${serviceName}-${nodePort} E2E status UP🐸"
		}
	}
}

具体情况根据实际情况来.

运维

由于拆分的服务比较多, 线上bug定位复杂问题随之而来, 那就需要收集log, 而且多个服务之间的log要能连起来, 可追踪, 最简单的方法就是ELK三件套. 每个服务log格式加上追踪的ID. e.g:

log4j.logging.format=%d{yyyy-MM-dd HH:mm:ss.SSS} - [%X{X-B3-TraceId:-}, %X{X-B3-SpanId:-}, %X{X-B3-ParentSpanId:-}, %X{X-Span-Export:-}, ${PID:-}] [%thread] %ex %-5level %logger [line: %line] -- %msg%n

这样查bug的时候, 只要服务写了log, 那在kibana里面查询跟踪起来还是很快的.

总结关键点

  • 使用Springboot给开发人员带来的便利性, just need make a simple jar, that`s it, no more complexity.

  • 充分利用Kong网关提供的功能, 丰富的插件, 服务健康检查, 断路器.

  • 尽量编写自动化的构建和测试脚本, Jenkinsfile

关于Kong如何创建一个可用服务, 可参考使用Kong创建服务4步曲

这里也只是抛砖引玉简单描述了一下目前使用到的一些框架模式, 具体有什么不明白的都可以问我, 微信: rssbar, 用于杂事多, 不一定能马上回复.

各种架构服务流行的今天, 不管是框架也好架构也罢, 选择适合自己的才是最好的.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wenkaishun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值