第 7 天 SpringBoot 和 SpringCloud 微服务项目交付
Spring Cloud微服务项目交付
微服务扫盲篇
微服务并没有一个官方的定义,想要直接描述微服务比较困难,我们可以通过对比传统WEB应用,来理解什么是微服务。
单体应用架构
如下是传统打车软件架构图:
这种单体应用比较适合于小项目,优点是:
- 开发简单直接,集中式管理
- 基本不会重复开发
- 功能都在本地,没有分布式的管理开销和调用开销
当然它的缺点也十分明显,特别对于互联网公司来说:
- 开发效率低:所有的开发在一个项目改代码,递交代码相互等待,代码冲突不断
- 代码维护难:代码功能耦合在一起,新人不知道何从下手
- 部署不灵活:构建时间长,任何小修改必须重新构建整个项目,这个过程往往很长
- 稳定性不高:一个微不足道的小问题,可以导致整个应用挂掉
- 扩展性不够:无法满足高并发情况下的业务需求
微服务应用架构
微服务架构的设计思路不是开发一个巨大的单体式应用,而是将应用分解为小的、互相连接的微服务。一个微服务完成某个特定功能,比如乘客管理和下单管理等。每个微服务都有自己的业务逻辑和适配器。一些微服务还会提供API接口给其他微服务和应用客户端使用。
比如,前面描述的系统可被分解为:
每个业务逻辑都被分解为一个微服务,微服务之间通过REST API通信。一些微服务也会向终端用户或客户端开发API接口。但通常情况下,这些客户端并不能直接访问后台微服务,而是通过API Gateway来传递请求。API Gateway一般负责服务路由、负载均衡、缓存、访问控制和鉴权等任务。
微服务架构优点:
- 解决了复杂性问题。它将单体应用分解为一组服务。虽然功能总量不变,但应用程序已被分解为可管理的模块或服务
- 体系结构使得每个服务都可以由专注于此服务的团队独立开发。只要符合服务API契约,开发人员可以*选择开发技术。这就意味着开发人员可以采用新技术编写或重构服务,由于服务相对较小,所以这并不会对整体应用造成太大影响
- 微服务架构可以使每个微服务独立部署。这些更改可以在测试通过后立即部署。所以微服务架构也使得CI/CD成为可能
微服务架构问题及挑战
微服务的一个主要缺点是微服务的分布式特点带来的复杂性。开发人员需要基于RPC或者消息实现微服务之间的调用和通信,而这就使得服务之间的发现、服务调用链的跟踪和质量问题变得的相当棘手。
-
微服务的一大挑战是跨多个服务的更改
-
比如在传统单体应用中,若有A、B、C三个服务需要更改,A依赖B,B依赖C。我们只需更改相应的模块,然后一次性部署即可。
-
在微服务架构中,我们需要仔细规划和协调每个服务的变更部署。我们需要先更新C,然后更新B,最后更新A。
-
-
部署基于微服务的应用也要复杂得多
- 单体应用可以简单的部署在一组相同的服务器上,然后前端使用负载均衡即可。
- 微服务由不同的大量服务构成。每种服务可能拥有自己的配置、应用实例数量以及基础服务地址。这里就需要不同的配置、部署、扩展和监控组件。此外,我们还需要服务发现机制,以便服务可以发现与其通信的其他服务的地址
以上问题和挑战可大体概括为:
- API Gateway
- 服务间调用
- 服务发现
- 服务容错
- 服务部署
- 数据调用
https://www.kancloud.cn/owenwangwen/open-capacity-platform/1480155,自助餐吃吃喝喝,竟然秒懂微服务
微服务框架
如何应对上述挑战,出现了如下微服务领域的框架:
- Spring Cloud(各个微服务基于Spring Boot实现)
- Dubbo
- Service Mesh
- Linkerd
- Envoy
- Conduit
- Istio
了解Spring Cloud
https://spring.io
核心项目及组件
https://spring.io/projects
与Dubbo对比
做一个简单的功能对比:
核心要素 | Dubbo | Spring Cloud |
---|---|---|
服务注册中心 | Zookeeper | Spring Cloud Netflix Eureka |
服务调用方式 | RPC | REST API |
服务监控 | Dubbo-monitor | Spring Boot Admin |
断路器 | 不完善 | Spring Cloud Netflix Hystrix |
服务网关 | 无 | Spring Cloud Netflix Zuul |
分布式配置 | 无 | Spring Cloud Config |
服务跟踪 | 无 | Spring Cloud Sleuth |
消息总线 | 无 | Spring Cloud Bus |
数据流 | 无 | Spring Cloud Stream |
批量任务 | 无 | Spring Cloud Task |
…… | …… | …… |
从上图可以看出其实Dubbo的功能只是Spring Cloud体系的一部分。
这样对比是不够公平的,首先Dubbo
是SOA
时代的产物,它的关注点主要在于服务的调用,流量分发、流量监控和熔断。而Spring Cloud
诞生于微服务架构时代,考虑的是微服务治理的方方面面,另外由于依托了Spirng
、Spirng Boot
的优势之上,两个框架在开始目标就不一致,Dubbo
定位服务治理、Spirng Cloud
是一个生态。
Spring Boot交付实践
从零开始创建Spring Boot项目
通过File > New > Project,新建工程,选择Spring Initializr
配置Project Metadata:
配置Dependencies依赖包:
选择:Web分类中的Spring web和Template Engines中的Thymeleaf
配置maven settings.xml:
默认使用IDE自带的maven,换成自己下载的,下载地址:
链接: https://pan.baidu.com/s/1z9dRGv_4bS1uxBtk5jsZ2Q 提取码: 3gva
解压后放到D:\software\apache-maven-3.6.3
,修改D:\software\apache-maven-3.6.3\conf\settings.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<localRepository>D:\opt\maven-repo</localRepository>
<pluginGroups>
</pluginGroups>
<proxies>
</proxies>
<servers>
</servers>
<mirrors>
<mirror>
<id>alimaven</id>
<mirrorOf>central</mirrorOf>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
</mirror>
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>*</mirrorOf>
<name>Nexus aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</mirrors>
</settings>
替换springboot版本为2.3.5.RELEASE
直接启动项目并访问本地服务:localhost:8080
编写功能代码
创建controller包及HelloController.java
文件
package com.luffy.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String hello(String name) {
return "Hello, " + name;
}
保存并在浏览器中访问localhost:8080/hello?name=luffy
如果页面复杂,如何实现?
在resources/templates/
目录下新建index.html
<!DOCTYPE html>
<html>
<head>
<title>Devops</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div class="container">
<h3 th:text="${requestname}"></h3>
<a id="rightaway" href="#" th:href="@{/rightaway}" >立即返回</a>
<a id="sleep" href="#" th:href="@{/sleep}">延时返回</a>
</div>
</body>
</html>
完善HelloController.java
的内容:
package com.luffy.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
@RestController
public class HelloController {
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String hello(String name) {
return "Hello, " + name;
}
@RequestMapping("/")
public ModelAndView index(ModelAndView mv) {
mv.setViewName("index");
mv.addObject("requestname", "This is index");
return mv;
}
@RequestMapping("/rightaway")
public ModelAndView returnRightAway(ModelAndView mv) {
mv.setViewName("index");
mv.addObject("requestname","This request is RightawayApi");
return mv;
}
@RequestMapping("/sleep")
public ModelAndView returnSleep(ModelAndView mv) throws InterruptedException {
Thread.sleep(2*1000);
mv.setViewName("index");
mv.addObject("requestname","This request is SleepApi"+",it will sleep 2s !");
return mv;
}
}
如何在java项目中使用maven
为什么需要maven
考虑一个常见的场景:以项目A为例,开发过程中,需要依赖B-2.0.jar的包,如果没有maven,那么正常做法是把B-2.0.jar拷贝到项目A中,但是如果B-2.0.jar还依赖C.jar,我们还需要去找到C.jar的包,因此,在开发阶段需要花费在项目依赖方面的精力会很大。
因此,开发人员需要找到一种方式,可以管理java包的依赖关系,并可以方便的引入到项目中。
maven如何工作
查看pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
可以直接在项目中添加上dependency
,这样来指定项目的依赖包。
思考:如果spring-boot-starter-thymeleaf
包依赖别的包,怎么办?
spring-boot-starter-thymeleaf
同时也是一个maven项目,也有自己的pom.xml
查看一下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.3.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.11.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
<version>3.0.4.RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
这样的话,使用maven的项目,只需要在自己的pom.xml中把所需的最直接的依赖包定义上,而不用关心这些被依赖的jar包自身是否还有别的依赖。剩下的都交给maven去搞定。
如何搞定?maven可以根据pom.xml中定义的依赖实现包的查找
去哪查找?maven仓库,存储jar包的地方。
当我们执行 Maven 构建命令时,Maven 开始按照以下顺序查找依赖的库:
本地仓库:
-
Maven 的本地仓库,在安装 Maven 后并不会创建,它是在第一次执行 maven 命令的时候才被创建。
-
运行 Maven 的时候,Maven 所需要的任何包都是直接从本地仓库获取的。如果本地仓库没有,它会首先尝试从远程仓库下载构件至本地仓库,然后再使用本地仓库的包。
-
默认情况下,不管Linux还是 Windows,每个用户在自己的用户目录下都有一个路径名为 .m2/respository/ 的仓库目录。
-
Maven 本地仓库默认被创建在 %USER_HOME% 目录下。要修改默认位置,在 %M2_HOME%\conf 目录中的 Maven 的 settings.xml 文件中定义另一个路径。
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd"> <localRepository>D:\opt\maven-repo</localRepository> </settings>
*仓库:
Maven *仓库是由 Maven 社区提供的仓库,*仓库包含了绝大多数流行的开源Java构件,以及源码、作者信息、SCM、信息、许可证信息等。一般来说,简单的Java项目依赖的构件都可以在这里下载到。
*仓库的关键概念:
- 这个仓库由 Maven 社区管理。
- 不需要配置,maven中集成了地址 http://repo1.maven.org/maven2
- 需要通过网络才能访问。
私服仓库:
通常使用 sonatype Nexus来搭建私服仓库。搭建完成后,需要在 setting.xml中进行配置,比如:
<profile>
<id>localRepository</id>
<repositories>
<repository>
<id>myRepository</id>
<name>myRepository</name>
<url>http://127.0.0.1:8081/nexus/content/repositories/myRepository/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</profile>
方便起见,我们直接使用国内ali提供的仓库,修改 maven 根目录下的 conf 文件夹中的 setting.xml 文件,在 mirrors 节点上,添加内容如下:
<mirrors>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
在执行构建的时候,maven会自动将所需的包下载到本地仓库中,所以第一次构建速度通常会慢一些,后面速度则很快。
那么maven是如何找到对应的jar包的?
我们可以访问 https://mvnrepository.com/ 查看在仓库中的jar包的样子。
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
刚才看到spring-boot-starter-thymeleaf
的依赖同样有上述属性,因此maven就可以根据这三项属性,到对应的仓库中去查找到所需要的依赖包,并下载到本地。
其中groupId、artifactId、version
共同保证了包在仓库中的唯一性,这也就是为什么maven项目的pom.xml中都先配置这几项的原因,因为项目最终发布到远程仓库中,供别人调用。
思考:我们项目的dependency
中为什么没有写version
?
是因为sprintboot项目的上面有人
,来看一下项目parent
的写法:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
parent模块中定义过的dependencies
,在子项目中引用的话,不需要指定版本,这样可以保证所有的子项目都使用相同版本的依赖包。
生命周期及mvn命令实践
Maven有三套相互独立的生命周期,分别是clean、default和site。每个生命周期包含一些阶段(phase),阶段是有顺序的,后面的阶段依赖于前面的阶段。
- clean生命周期,清理项目
- 清理:mvn clean --删除target目录,也就是将class文件等删除
- default生命周期,项目的构建等核心阶段
- 编译:mvn compile--src/main/java目录java源码编译生成class (target目录下)
- 测试:mvn test--src/test/java 执行目录下的测试用例
- 打包:mvn package--生成压缩文件:java项目#jar包;web项目#war包,也是放在target目录下
- 安装:mvn install --将压缩文件(jar或者war)上传到本地仓库
- 部署|发布:mvn deploy--将压缩文件上传私服
- site生命周期,建立和发布项目站点
- 站点 : mvn site --生成项目站点文档
各个生命周期相互独立,一个生命周期的阶段前后依赖。 生命周期阶段需要绑定到某个插件的目标才能完成真正的工作,比如test阶段正是与maven-surefire-plugin的test目标相绑定了 。
举例如下:
-
mvn clean
调用clean生命周期的clean阶段
-
mvn test
调用default生命周期的test阶段,实际执行test以及之前所有阶段
-
mvn clean install
调用clean生命周期的clean阶段和default的install阶段,实际执行clean,install以及之前所有阶段
在linux环境中演示:
创建gitlab组,luffy-spring-cloud
,在该组下创建项目springboot-demo
-
提交代码到git仓库
$ git init $ git remote add origin http://gitlab.luffy.com/luffy-spring-cloud/springboot-demo.git $ git add . $ git commit -m "Initial commit" $ git push -u origin master
-
使用tools容器来运行
$ docker run --rm -ti 172.21.51.67:5000/devops/tools:v3 bash bash-5.0# mvn -v bash: mvn: command not found # 由于idea工具自带了maven,所以可以直接在ide中执行mvn命令。在tools容器中,需要安装mvn命令
为tools镜像集成mvn:
将本地的
apache-maven-3.6.3
放到tools
项目中,修改settings.xml
配置... <localRepository>/opt/maven-repo</localRepository> ...
然后修改Dockerfile,添加如下部分:
#-----------------安装 maven--------------------# COPY apache-maven-3.6.3 /usr/lib/apache-maven-3.6.3 RUN ln -s /usr/lib/apache-maven-3.6.3/bin/mvn /usr/local/bin/mvn && chmod +x /usr/local/bin/mvn ENV MAVEN_HOME=/usr/lib/apache-maven-3.6.3 #------------------------------------------------#
去master节点拉取最新代码,构建最新的tools镜像:
# k8s-master节点
$ git pull
$ docker build . -t 172.21.51.67:5000/devops/tools:v4 -f Dockerfile
$ docker push 172.21.51.67:5000/devops/tools:v4
再次尝试mvn命令:
$ docker run --rm -ti 172.21.51.67:5000/devops/tools:v4 bash
bash-5.0# mvn -v
bash-5.0# git clone http://gitlab.luffy.com/luffy-spring-cloud/springboot-demo.git
bash-5.0# cd springboot-demo
bash-5.0# mvn clean
# 观察/opt/maven目录
bash-5.0# mvn package
# 多阶段组合
bash-5.0# mvn clean package
想系统学习maven,可以参考: https://www.runoob.com/maven/maven-pom.html
Springboot服务镜像制作
通过mvn package
命令拿到服务的jar包后,我们可以使用如下命令启动服务:
$ java -jar demo-0.0.1-SNAPSHOT.jar
因此,需要准备Dockerfile来构建镜像:
FROM openjdk:8-jdk-alpine
COPY target/springboot-demo-0.0.1-SNAPSHOT.jar app.jar
CMD [ "sh", "-c", "java -jar /app.jar" ]
我们可以为构建出的镜像指定名称:
<build>
<finalName>${project.artifactId}</finalName><!--打jar包去掉版本号-->
...
Dockerfile
对应修改:
FROM openjdk:8-jdk-alpine
COPY target/springboot-demo.jar app.jar
CMD [ "sh", "-c", "java -jar /app.jar" ]
执行镜像构建,验证服务启动是否正常:
$ docker build . -t springboot-demo:v1 -f Dockerfile
$ docker run -d --name springboot-demo -p 8080:8080 springboot-demo:v1
$ curl localhost:8080
接入CICD流程
之前已经实现了shared-library,并且把python项目接入到了CICD 流程中。因此,可以直接使用已有的流程,把spring boot项目接入进去。
Jenkinsfile
sonar-project.properties
deploy/deployment.yaml
deploy/service.yaml
deploy/ingress.yaml
configmap/devops-config
Jenkinsfile
@Library('luffy-devops') _
pipeline {
agent { label 'jnlp-slave'}
options {
timeout(time: 20, unit: 'MINUTES')
gitLabConnection('gitlab')
}
environment {
IMAGE_REPO = "172.21.51.67:5000/demo/springboot-demo"
IMAGE_CREDENTIAL = "credential-registry"
DINGTALK_CREDS = credentials('dingTalk')
PROJECT = "springboot-demo"
}
stages {
stage('checkout') {
steps {
container('tools') {
checkout scm
}
}
}
stage('mvn-package') {
steps {
container('tools') {
script{
sh 'mvn clean package'
}
}
}
}
stage('CI'){
failFast true
parallel {
stage('Unit Test') {
steps {
echo "Unit Test Stage Skip..."
}
}
stage('Code Scan') {
steps {
container('tools') {
script {
devops.scan().start()
}
}
}
}
}
}
stage('docker-image') {
steps {
container('tools') {
script{
devops.docker(
"${IMAGE_REPO}",
"${GIT_COMMIT}",
IMAGE_CREDENTIAL
).build().push()
}
}
}