[toc]
认识微服务
01 服务框架演变
- 单体架构
- 概念:将业务的所有功能集中在一个项目中进行开发,打成一个包部署
- 优点:架构简单,部署成本低
- 缺点:耦合度高
- 分布式架构
- 概念:根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,成为一个服务
- 优点:降低服务耦合,有利于服务拓展升级
- 考虑问题:
- 服务拆分粒度如何?
- 服务集群地址如何维护?
- 服务之间如何实现远程调用?
- 服务健康状态如何感知?
- 微服务:是一种经过良好架构设计的分布式架构方案
- 微服务架构特征:
- 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
- 面向服务:微服务对外暴露业务接口
- 自治:团队独立,技术独立,数据独立,部署独立
- 隔离性强:服务调用做好隔离,容错,降级,避免出现级联问题
- 微服务架构特征:
02 微服务技术对比
- 微服务这种方案需要技术框架来落地,国内最知名为SpringCloud和阿里巴巴的Duddo
03 SpringCloud
- SpringCloud是目前全球使用最广泛的微服务框架
- SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验
- SpringCloud和SpringCloud搭配使用需要注意版本兼容
服务拆分与远程调用
01 服务拆分
- 服务拆分注意事项
-
- 不同微服务,不要重复开发相同业务
-
- 微服务数据独立,不要访问其它微服务的数据库
-
- 微服务可以将自己的业务暴露为接口,供其它微服务调用
-
02 服务间调用
-
微服务调用方式
- 基于RestTemplate发起的http请求实现远程调用
- http请求做远程调用是与语言无关的调用,只需直到对方的ip,端口,接口路径,请求参数即可
-
步骤:
-
- 注册RestTemplate
@Bean public RestTemplate restTemplate(){ return new RestTemplate(); }
-
- 服务远程调用RestTempla
@GetMapping("{orderId}") public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) { // 1. 查询订单 Order order=orderService.queryOrderById(orderId); // 2. 利用RestTemplate发起http请求,查询用户 //(1)url路径 String url="http://localhost:8081/user/"+order.getUserId(); //(2)发送http请求,实现远程调用 User user=restTemplate.getForObject(url,User.class); // 3. 封装user到Order order.setUser(user); // 4. 返回 return order; }
-
-
服务调用关系
- 服务提供者:暴露接口给其它微服务调用
- 服务消费者:调用其他微服务提供的接口
- 提供者和消费者角色是相对的
- 一个服务可以同时是服务提供者和消费者
Eureka 注册中心
01 远程调用的问题
- 服务消费者该如何获取服务提供者的地址信息?
- 如果有多个服务者,消费者如何选择?
- 消费者如何得知服务提供者的健康状态?
02 eureka原理
-
服务消费者该如何获取服务提供者的地址信息?
- 服务提供者启动时向eureka注册自己的信息
- eureka保存这些信息
- 消费者根据服务名称向eureka拉取提供者信息
-
如果有多个服务者,消费者如何选择?
- 服务消费者利用负载均衡算法选择一个执行
-
消费者如何得知服务提供者的健康状态?
- 服务提供者每30s向eurekaServer发送心跳请求,报告健康状态
- eureeure
-
在Eureka架构中,微服务角色有两类
- EurekaServer:服务端,注册中心
- 记录服务信息,心跳监控
- EureClient:客户端
- Provider:服务提供者
- 注册自己的信息到EurekaServer
- 每隔30s向EurekaServer发送心跳
- consumer:服务消费者
- 根据服务名称从EurekaServer拉取服务列表
- 基于服务列表做负载均衡,选中一个微服务后发起远程调用
- Provider:服务提供者
- EurekaServer:服务端,注册中心
03 搭建EurekaServer
- 搭建步骤
-
- 创建项目,引入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>
-
- 编写启动类,添加@EnableEurekaServer注解
@EnableEurekaServer @SpringBootApplication public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class,args); } }
-
- 添加application.yml文件,编写配置
server: port: 10086 spring: application: name: eurekaserver #eureka服务名称 eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka
-
04 服务注册
- 步骤如下:
-
- 在user-service项目引入
spring-cloud-starter-netflix-eureka-client
的依赖
- 在user-service项目引入
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
-
- 在
application.yml
文件,编写下面配置
- 在
server: port: 10086 spring: application: name: eurekaclient #eureka服务名称 eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka
-
05 服务发现
- 服务拉取是基于服务名称获取服务列表,然后在对服务列表做负载均衡
-
- 修改代码,修改url路径,用服务名代替ip和端口号
String url="http://userservice/user/"+order.getUserId();
- 修改代码,修改url路径,用服务名代替ip和端口号
-
- 在项目启动类中添加负载均衡注解
@LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); }
-
Ribbon 负载均衡原理
01 负载均衡原理
02 负载均衡策略
- 通过定义rule可以修改负载均衡规则
-
- 代码方式:在order-service中的OrderApplication类中,定义一个新的Rule,这种方法会对整个项目起作用,配置灵活,但修改时需要重新打包
@Bean public IRule randomRule(){ return new RandomRule(); }
-
- 配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则,直观方便,不需要重新打包,但无法做全局配置
userservice: ribbon: NFLoadBalancerRuleClassName:com.netflix.loadbalancer.RandomRule
-
03 懒加载
- Ribbon默认是采用懒加载,即第一访问时才回去创建LoadBalanceClient,请求时间较长。
- 而饥饿加载会在项目启动时创建,降低第一次访问的时间消耗,通过下面配置饥饿加载
ribbon:
eager-load:
enabled: true
clients: userservice # 指定对userservice这个服务饥饿加载
Nacos 注册中心
这里暂时不需要,以后再来填坑
http客户端Feign
01 Feign替代RestTemplate
-
RestTempla存在的问题
String url="http://userservice/user/"+order.getUserId(); User user=restTemplate.getForObject(url,User.class);
- 代码可读性差,编程体验不统一
- 参数复杂URL难以维护
-
Feign 是一个声明式htpp客户端,其作用可以让我们更优雅的实现http发送请求
-
定义和使用Feign客户端:
-
- 引入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
- 在order-service的启动类中添加注解开启Feign开关
@EnableFeignClients
- 在order-service的启动类中添加注解开启Feign开关
-
- 编写Feign客户端,主要基于SpringMVC的注解来声明说成调用信息
@FeignClient("userservice") public interface UserClient { @GetMapping("/user/{id}") User findById(@PathVariable("id") Long id); }
-
- 使用FeignClien中定义的方法代替RestTemplate
public Order queryOrderById(Long orderId) { // 1.查询订单 Order order = orderMapper.findById(orderId); // 2. 利用Feign远程调用 User user=userClient.findById(order.getUserId()); // 3。 封装user到Order order.setUser(user); // 4.返回 return order; }
-
02 自定义配置
- 配置feign的日志
-
- 配置文件方式
feign: client: config: default/userservice: #默认为全局配置,或使用服务名仅针对某个服务 loggerLevel: FULl # 日志级别
-
- 使用java代码
public class DefaultFeignConfiguration { @Bean public Logger.Level logLevel(){return Logger.Level.BASIC;} }
-
03 Feign使用优化
- 优化手段
-
- 使用连接池代替默认的URLConnection
- 引入HTTPClient依赖
<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>
feign: httpclient: enable: true # 支持HttpClient开关 max-connections: 200 #最大连接数 max-connections-per-route: 50 #单个路径最大连接数
-
- 日志级别,最好用basic或none
-
04 最佳实践
-
- 继承:给消费者的FeignClient和提供者的controller定义统一的父接口作为标准
- 服务紧耦合(API已经相同了)
- 父接口参数列表中的映射不会被继承
-
- 抽取:将FeignClient抽取为独立模块,并且把接口有关的POJO,默认的Feign配置都放到这个模块中,提供给所有消费者使用
-
抽取实践
- 步骤:
-
- 首先创建一个module,命名为feign-api,然后引入feign的starter依赖
-
- 将order-service中编写的UserClient,User,DefaultFeignConfiguration都复制到feign-api项目中
-
- 在order-service中引入feign-api依赖
-
- 修改order-service中的所有与上述三个组件相关部分的import部分,改成导入feign-api的包
-
- 重启测试
-
- 当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClien无法使用,有两种解决办法
-
- 指定FeignClient所在包
@EnableFeignClients(basePackages="cn.itcast.feign.clients")
- 指定FeignClient所在包
-
- 指定FeignCLient字节码
@EnableFeignClients(clients={UserClient})
- 指定FeignCLient字节码
-
- 步骤:
统一网关Gateway
01 为什么需要网关
-
网关功能
- 身份认证和权限校验
- 服务路由,负载均衡
- 请求限流
-
在springcloud中网关的实现有两种
- gateway和zuul
- zuul是基于Servlet的实现,属于阻塞式编程。
- 而SpringCloudGateway则是属于Spring5中提供的WebFlux,属于响应式编程的实现,具有更好的性能。
02 gateway快速入门
- 搭建网关服务的步骤
-
- 创建新的moudle,引入SpringCloudGateway的依赖和eureka的服务发现依赖
<!--gateway--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!--eureka--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
-
- 编写路由配置及eureka地址
- 路由配置包括:
-
- 路由id:路由的唯一标识
-
- 路由目标uri:路由的目标地址,http代表目标地址,lb代表根据服务名负载均衡
-
- 路由断言predicates:判断路由的规则
-
- 路由过滤器filters:对请求或响应做处理
-
server: port: 10010 spring: application: name: gateway cloud: gateway: routes: - id: user-service # 路由标识 uri: lb://userservice #路由的目标地址 predicates: #路由断言,判断请求是否符合规则 - Path=/user/** # 路径断言,判断路径是否以/user开头,如果是则符合 - id: order-service uri: lb://orderservice predicates: - Path=/order/**
-
03 断言工厂
- 在配置文件中写的断言规则只是字符串,这些字符串会被Pred Factory读取并处理,转变为路由判断的条件
具体使用可以参考官网
04 过滤器工厂
- 路由过滤器GatewayFilter
- GatewaFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理
- Spring提供了31种不同的路由过滤器工厂,同样详见官网
spring: application: name: gateway cloud: gateway: routes: - id: user-service # 路由标识 uri: lb://userservice #路由的目标地址 predicates: #路由断言,判断请求是否符合规则 - Path=/user/** # 路径断言,判断路径是否以/user开头,如果是则符合 - id: order-service uri: lb://orderservice predicates: - Path=/order/** filters: - AddRequestHeader = "" # 添加请求头 #default-filters: 这个是全路由过滤器,对所有的路由都生效,与routes平级 #- AddRequestHeader = "" # 添加请求头
05 全局过滤器
- 全局过滤器GlobalFilter的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter作用一样。
- 区别是GatewaFilter通过配置定义,处理逻辑是固定的。而GlobalFilter的逻辑需要自己写代码实现,定义方法是实现GlobalFilter接口
- 实现全局过滤器的步骤
-
- 实现GlobalFilter接口
-
- 添加@Order注解或实现Ordered接口
-
- 编写处理逻辑
-
@Order(-1)//设置优先级
@Component//bean
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取请求参数
MultiValueMap<String,String> params = exchange.getRequest().getQueryParams();;
// 2. 获取参数中的authorization参数
String auth = params.getFirst("authorization");
// 3. 判断参数值是否为admin
if("admin".equals(auth)){
// 4. 是,放行
return chain.filter(exchange);
}
// 5. 否,拦截
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
- 过滤器执行顺序
- 请求进入网关后会碰到三类过滤器:当前路由的过滤器,DefaultFilter,GlobalFilter。请求路由后,会将当前路由过滤器和DefaultFilter,GlobalFilter合并到一个过滤器链(集合)中,排序后依次执行每个过滤器
- 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。
- GlobalFilter通过实现Ordered接口或者添加@Order注解来指定order值,有编码者自己指定
- 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增
- 当过滤器的order值一样时,会按照 defaultFilter>路由过滤器>GlobalFilter的顺序执行
06 跨域问题处理
- 跨域:域名不一致就是跨域,主要包括
- 域名不同
- 域名相同,端口不同:
- 跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
- 解决方案:CORS
- gateway已经实现CORS,使用者需要配置
spring:
cloud:
gateway:
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
- "http://www.leyou.com"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
Docker
01 初始Docker
-
什么是Docker?
- 项目部署问题:大型项目组件较多,运行环境也较为复杂,部署时会碰到一些问题
- 依赖关系复杂,容易产生兼容性问题
- 开发,测试,生产环境有差异
- Docker如何解决依赖的兼容问题
- 将应用的Libs(函数库),Deps(依赖),配置和应用一起打包
- 将每个应用放到一个隔离容器去运行,避免相互干扰
- Docker如何解决不同系统环境的问题
- Docker将用户程序与所需要调用的系统(比如Ubuntu)函数库一起打包
- Docker运行到不同操作系统时,直接基于打包的库函数,借助于操作系统的Linux内核来运行
- 即Docker镜像中包含完整的运行环境,包含系统函数库,仅依赖系统的Linux内核,因此可以在任意Linux操作系统上运行
- 项目部署问题:大型项目组件较多,运行环境也较为复杂,部署时会碰到一些问题
-
Docker和虚拟机的差异
- docker是一个系统进程;虚拟机是在操作系统中的操作系统
- docker体积小,启动速度快,性能好;虚拟机体积大,启动速度慢,性能一般
-
Docker架构
- 镜像和容器
- 镜像(Image):Docker将应用程序及其所需依赖,函数库,环境,配置等文件打包在一起,称为镜像
- 容器(Container):镜像中的应用程序运行后形成的进程就是容器,只是docker会给容器做隔离,对外不可见
- Docker和DockerHub
- DockerHub:是一个Docker镜像的托管平台,这样的平台称为Docker Registry
- 国内有类似DockerHub的公开服务,如网易云镜像服务,阿里云镜像服务
- Docker是一个CS架构,由两部分组成
- 服务端(server):Docker守护进程,负责处理Docker指令,管理镜像,容器等
- 客户端(client):通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令
- 镜像和容器
-
安装Docker
跟着视频按即可
02 Docker基本操作
镜像操作
主要是查看文档
容器操作
-
创建一个Nginx容器
- 去docker hub查看Nginx的容器的命令
docker run --name containerName -p 80:80 -d nginx
- 命令解读:
- docker run:创建并运行一个容器
- –name:给容器起一个名字,比如叫做mn
- -p:将宿主机端口与容器端口映射,冒号左边是宿主机端口,右侧是容器端口
- -d:后台运行容器
- nginx:镜像名称
- 命令解读:
- 去docker hub查看Nginx的容器的命令
-
查看容器日志的命令:
docker logs
- 添加-f参数可以持续查看日志
-
查看容器状态:
docker ps
-
删除容器
docker rm
-
案例操作示例:
- 进入Nginx容器,修改HTML文件内容,添加“HELLO WORLD”:进入容器使用命令
docker exec -it containerName bash
- 命令解读
- docker exec:进入容器内部
- -it:给当前进入的容器创建一个标准输入,输出终端,允许我们与容器交互
- containerName:要进去的容器的名称
- bash:进入容器后执行的命令
- 命令解读
- 创建并运行一个redis容器,并且支持数据持久化
-
- 到DockerHub搜索redis镜像
-
- 查看Redis镜像文档中的帮助信息
-
- 利用docker run命令运行一个Redis容器
-
- 进入Nginx容器,修改HTML文件内容,添加“HELLO WORLD”:进入容器使用命令
数据卷(容器数据管理)
-
容器和数据耦合问题
-
- 不便于修改:当我们要修改Nginx的html内容时,需要进入容器内部修改,很不方便
-
- 数据不可复用:在容器内的修改对外是不可见的。所有修改对新创建的容器是不可复用的
-
- 升级维护困难,数据在容器内,如果要升级必然删除旧容器,所有数据会丢失
-
-
数据卷
- 感觉和C++的指针很像,Container调用指向宿主文件系统的某个目录的指针
- 感觉和C++的指针很像,Container调用指向宿主文件系统的某个目录的指针
-
操作数据卷
- 数据卷操作基本语法:
docker volume [COMMAND]
- create 创建一个volume
- inspect 显示一个或多个volume的信息
- ls 列出所有volume
- prune 删除未使用的volume
- rm 删除一个或多个指定的volume
- 操作案例:创建一个数据卷,并查看数据卷在宿主机的目录位置
- 数据卷操作基本语法:
-
挂载数据卷
- 创建容器时,可以通过-v参数来挂载一个数据卷到某个容器目录
- 举例
docker run --name mn -v html:/root/html -p 8080:80 nginx
- docker run 创建并运行容器
- –name mn:给容器起名叫mn
- -v html:/root/html: 将html数据卷挂载到容器内/root/html这个目录中
- -p 8080:80: 将宿主机的8080端口映射到容器内的80端口
- ngnix:镜像名称
- 案例:创建一个ngnix容器,修改html目录内index.html的内容(通过数据卷挂载)
-
如果容器运行时volume不存在,会自动被创建出来
案例:创建并运行一个MySQL容器,将宿主机目录直接挂载到容器中
见视频,这里使用 docker pull mysql
直接拉取最新docker镜像
总结
-
- docker run的命令通过-v参数挂载文件或目录到容器中
-
- -v volume名称:容器内目录
-
- -v 宿主机文件: 容器内文件
-
- -v 宿主机目录:容器内目录
-
- 数据卷挂载与目录直接挂载的
-
- 数据卷挂载耦合度低,由docker来管理目录,但是目录较深,不好寻找
-
- 目录挂载耦合度较高,需要自己记录管理目录,但便于寻找
03 Dockerfile自定义镜像
镜像结构
-
镜像是将应用程序及其所需的系统函数库,环境,配置,依赖打包而成
-
镜像是分层结构:
- BaseImage层:包含基本的系统函数库,环境变量,文件系统
- Entrypoint:入口,是镜像中应用启动的命令
- 其它:在BaseImage基础上添加依赖,安装程序,完成整个应用的安装和配置
Dockfile语法
Dockerfile就是一个文本文件,其中包含一个个指令(Instruction),用指令来说明要执行什么操作来构建镜像,每一个指令会形成一层Layer
指令 | 说明 | 示例 |
---|---|---|
FROM | 指定基础镜像 | FROM centos:7 |
ENV | 设置环境变量 | ENV key value |
COPY | 拷贝本地文件到镜像的指定目录 | COPY ./mysql-5.7.rpm /tmp |
RUN | 执行Linux的shell命令,一般是安装过程的命令 | RUN yum install gcc |
EXPOSE | 指定容器运行时监听的端口,是给镜像使用者看的 | EXPOSE 8080 |
ENTRYPOINT | 镜像中应用的启动命令,容器运行时调用 | ENTRY java -jar xx.jar |
构建Java项目
-
案例:基于centos创建一个新景象,运行一个java项目
-
- 新建一个空文件夹docker-demo
-
- 拷贝jar文件到该目录
-
- 拷贝jdk8.tar.gz到该目录
-
- 拷贝Dockerfile文件到该目录
-
- 进入docker-demo
-
- 运行命令
docker build -t javaweb:1.0
- 运行命令
-
-
基于java:8-alpine镜像,将一个Java项目构建为镜像
- 该镜像可以直接构建java镜像环境
-
- 新建空目录,其中新建文件Dockerfile
-
- 将jar包拷贝到这个目录
-
- 编写Dockerfile文件
-
- 基于java:8-alpine作为基础镜像
-
- 将app.jar拷贝到镜像
-
- 暴露端口
-
- 编写入口ENTRYPOINT
-
- 使用docker build命令构建镜像
-
- 使用docker run创建容器并运行
总结
-
- Dockerfile本质是一个文件,通过指令描述镜像的构建过程
-
- Dockerfile的第一行必须是FROM,从一个基础镜像来构建
-
- 基础镜像可以是基本操作系统,如centos,也可以是其他人制作好的镜像,如java:8-alpine
04 DockerCompose
认识DockerCompose
- 什么是DockerCompose
- DockerCompose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器
- Compose文件是一个文本文件,可以通过定义集群中的每个容器如何运行
- DockerCompose的安装
部署微服务集群
05 Docker镜像仓库
搭建私有镜像仓库
- 镜像仓库有公有的和私有的两种形式
- 公共仓库:例如Docker官方Docker Hub,网易云镜像服务,阿里云镜像服务等
- 除使用公开仓库外,用户可以在本地搭建Docker Registry。企业自己的镜像最好是采用私有Docker Registry来实现
- 创建过程,看视频
向镜像仓库推送镜像
推送镜像到私有镜像服务必须先tag,步骤如下:
- 重新tag本地镜像,名称前缀为私有仓库的地址:ip:8080/
docker tag ngnix:latest ip:8080/nginx:1.0
- 推送镜像
docker push ip:8080/nginx:1.0
- 拉取镜像
docker pull ip:8080/nginx:1.0
总结
- 推送本地镜像仓库前都必须重命名(docker tag)镜像,以镜像仓库地址为前缀
- 镜像仓库推送前需要把仓库地址配置到docker服务的daemon.json文件中,被docker信任
- 推送使用docker push命令
- 拉取使用docker pull命令