0%

什么是DockerCompose

  • DockerCompose可以基于Compose文件帮用户快速部署分布式应用,而无需手动一个个创建和运行容器
  • Compose文件是一个文本文件,通过指令定义集群中每个容器如何运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
version: "3.2"

services:
nacos:
image: nacos/nacos-server
environment:
MODE: standalone
ports:
- "8848:8848"
mysql:
image: mysql:5.7.25
environment:
MYSQL_ROOT_PASSWORD: 123456
volumes:
- "$PWD/mysql/data:/var/lib/mysql"
- "$PWD/mysql/conf:/etc/mysql/conf.d/"
userservice:
build: ./user-service
orderservice:
build: ./order-service
gateway:
build: ./gateway
ports:
- "10010:10010"

可以看到,其中包含5个service服务:

  • nacos:作为注册中心和配置中心
    • image: nacos/nacos-server: 基于nacos/nacos-server镜像构建
    • environment:环境变量
      • MODE: standalone:单点模式启动
    • ports:端口映射,这里暴露了8848端口
  • mysql:数据库
    • image: mysql:5.7.25:镜像版本是mysql:5.7.25
    • environment:环境变量
      • MYSQL_ROOT_PASSWORD: 123456:设置数据库root账户的密码
    • volumes:数据卷挂载,这里挂载了mysql的data、conf目录,其中有我提前准备好的数据
  • userserviceorderservicegateway:都是基于Dockerfile临时构建的
  • 修改cloud-demo项目,将数据库、nacos地址都命名为docker-compose中的服务名
  • 使用maven打包工具,将项目中的每个微服务都打包为app.jar
  • 将打包好的app.jar拷贝到cloud-demo中的每一个对应子目录中
  • 将cloud-demo上传至虚拟机,利用docker-compose up -d来部署

镜像结构

  • 镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成
  • 结构:
    • 入口:镜像运行入口,一般是程序启动的脚本和参数
    • 层:在BaseImage基础上添加安装包、依赖、配置等,每次操作都形成新的一层
    • 基础镜像:应用依赖的系统函数库、环境、配置、文件等

什么是Dockerfile

Dockerfile是一个文本文件,其中包含一个个的指令,用指令来说明要执行什么操作来构建镜像,每一个指令都会形成一层Layer

指令 说明 示例
FROM 指定基础镜像 FROM centos:6
ENV 设置环境变量,可在后面指令使用 ENV key value
COPY 拷贝本地文件到镜像的指定目录 COPY ./mysql-5.7.rpm /tmp
RUN 执行Linux的shell命令,一般是安装过程的命令 RUN yum install gcc
EXPOSE 指定容器运行时监听的端口,是给镜像使用者看的 EXPOSE 8080
ENTRYPOINT 镜像中应用的启动命令,容器运行时调用 ENTRYPOINT java -jar xx.jar

案例1:基于Ubuntu镜像构建一个新镜像,运行一个java项目

  • 新建空文件夹docker-demo
  • 拷贝项目jar包、jdk、Dockerfile到该目录
  • 进入decker-demo

  • 运行命令

1
docker build -t javaweb:1.0 .
  • 创建镜像
1
docker run --name web -p 8090:8090 -d javaweb:1.0

案例2:基于java:8-alpine镜像,将一个Java项目构建为镜像

思路如下:

  • 新建空目录,新建Dockerfile文件
  • 拷贝项目jar包到该目录
  • 编写Dockerfile文件
    • 基于java:8-alpine作为基础镜像
    • 将app.jar拷贝到镜像中
    • 暴露端口
    • 编写入口ENTRYPOINT
  • 使用build命令创建镜像,run命令创建容器并运行

镜像相关命令

  • 镜像名称一般分两部分组成:[repository]:[tag],例如:mysql:5.7。
  • 创建镜像
    • 从本地:从Dockerfile文件,利用docker build命令构建镜像
    • 从镜像服务器:docker pull命令
  • 查看镜像:docker images命令
  • 删除镜像:docker rmi命令
  • 推送镜像
    • docker push命令,推送到服务
    • docker save命令,保存为一个压缩包,可以用docker load命令加载

案例1

  • 前往doker hub
  • 查找Nginx镜像pull命令
  • 使用docker images查看镜像

案例2

  • 利用docker xx —help命令查看docker save和docker load语法
  • 使用docker tag 创建新镜像mynginx1.0
  • 使用docker save导出镜像到磁盘
  • 导出:docker save -o nginx.tar nginx:latest,使用ll命令查看
  • 删除当前镜像:docker rmi nginx:latest
  • 导入: docker load -i nginx.tar

容器相关命令

  • 查看所有运行的容器及状态:docker ps
  • 查看容器运行日志:docker logs
  • 进入容器执行命令:docker exec
  • 状态切换
    • 运行 → 暂停:docker pause
    • 暂停 → 运行: docker unpause
    • 运行 → 停止:docker stop
    • 停止 → 运行: docker start
  • 删除指定容器:docker rm

案例1:运行一个Nginx容器

  • 在docker hub查看Nginx的容器运行命令
1
docker run --name containerName -p 80:80 -d nginx
  • 命令解读:
    • docker run:创建并运行一个容器
    • —name:给容器起一个名字,必须唯一
    • -p:将宿主机端口与容器端口映射,左边是宿主机端口,右边是容器端口
    • -d:后台运行容器
    • nginx:镜像名称
  • 查看日志:docker logs mn

案例2:进入Nginx容器,修改HTML文件内容(不推荐使用)

  • 进入容器
1
docker exec -it mn /bin/bash
  • 命令解读
    • docker exec:进入容器内部,执行一个命令
    • -it:给当前进入的容器创建一个标准输入输出终端,允许交互
    • mn:容器名称
    • /bin/bash:进入后执行的命令,bash是一个linux终端交互命令
  • 进入指定路径:cd /usr/share/nginx/html
  • 替换内容
1
sed -i 's#Welcome to nginx#Hello world!#g' index.html

数据卷操作

容器与数据耦合的问题

  • 不便于修改
  • 数据不可复用

数据卷(volume)

是一个虚拟目录,指向宿主机文件系统中的某个目录。

操作数据卷

  • 数据卷操作的基本语法如下:
1
docker volume [COMMAND]
  • docker volume命令是数据卷操作,根据命令后跟随的command来确定下一步操作:
    • create:创建一个volume
    • inspect:显示一个或多个volume信息
    • ls:列出所有volume
    • prune:删除未使用的volume
    • rm:删除一个或多个volume

挂载数据卷

我们在创建容器时,可以通过-v参数来挂载一个数据卷到某个容器目录

1
2
3
4
5
docker run \
--name mn \
-v hrml:/root/html \ 把html数据卷挂载到容器内部的/root/html目录中
-p 8080:80
nginx \

案例1:创建一个nginx容器,修改容器内的html目录内index.html内容

  • 需求说明:把目录挂载到html数据卷上,方便操作其中内容

  • 步骤:

    • 创建容器并挂载数据卷到容器内的HTML目录
    1
    docker run --name mn -p 81:80 -v html:/usr/share/nginx/html -d nginx
    • 进入html数据卷所在位置,并修改HTML内容
  • 若数据卷不存在,docker会自动创建

案例2:创建并运行一个MySQL容器,将宿主机目录直接挂载到容器

  • 目录挂载语法
    • -v[宿主机目录]:[容器内目录]
    • -v[宿主机文件]:[容器内文件]
  • 上传压缩包,加载MySQL镜像
1
docker load -i /mysql.tar
  • 创建目录/tmp/myql/data
  • 创建目录/tmp/myql/conf,将配置文件上传
1
2
3
4
5
6
7
8
docker run \
--name mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-p 3306:3306 \
-v /tmp/mysql/conf/hmy.conf:/etc/mysql/conf.d/hmy.cnf \
-v /tmp/mysql/data:/var/lib/mysql \
-d \
mysql:5.7.25
  • 挂载/tmp/myql/data到mysql容器内数据存储目录

  • 挂载/tmp/myql/conf/hmy.cnf到mysql容器配置文件

  • 设置MySQL密码

镜像和容器

  • 镜像(Image):Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像。
  • 容器(Container):镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器做隔离,对外不可见。

Docker和DockerHub

  • DockerHub:DockerHub是一个Docker镜像的托管平台。这样的平台称为Docker Registry。

Docker架构

Docker是一个CS架构的程序,由两部分组成:

  • 服务端(server):Docker守护进程,负责处理Docker指令,管理镜像、容器等
  • 客户端(client):通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令。

跨域问题

跨域:域名不一致就是跨域,主要包括:

  • 域名不同:www.taobao.com和www.taobao.org等等
  • 域名相同,端口不同:localhost:8080和localhost:8081

跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题

解决方案:CORS

跨域问题处理

网关处理跨域采用的同样是CORS方案,并且只需要简单配置即可实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 # 这次跨域检测的有效期

GatewayFilter

  • GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理
  • Spring提供了31种不同的路由过滤器工厂,例如
名称 说明
AddRequestHeader 给当前请求添加一个请求头
RemoveRequestHeader 移除请求中的一个请求头
AddResponseHeader 给响应结果中添加一个请求头
RemoveResponseHeader 从响应结果中移除一个请求头
RequestRateLimiter 限制请求流量
  • yml配置文件中
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
filters: # 过滤器,对指定路由有效
- AddRequestHeader=Truth,Itcast is freaking aowsome!
default-filters: # 默认过滤器,对所有路由请求有效
- AddRequestHeader=Truth,Itcast is freaking aowsome!

全局过滤器GlobalFilter

  • 全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样
  • 区别在于GatewayFilter通过配置定义,处理逻辑固定。而GlobalFilter的逻辑需要自己代码实现
  • 定义方式是实现GlobalFilter接口
1
2
3
4
5
6
7
8
public interface GlobalFilter {
/*
处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
@param exchange 请求上下文,里面可以获取Request,Response等信息
@param chain 用来把请求委托给下一个过滤器
@return {@code Mono<Void>} 返回标示当前过滤器业务结束
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//设置过滤器优先级,值越小优先级越高,也可以实现Ordered接口
@Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

//1.获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> params = request.getQueryParams();

//2.获取参数中的authorization参数
String auth = params.getFirst("authorization");

//3.判断参数是否等于admin,是,放行
if("admin".equals(auth)) {
return chain.filter(exchange);
}

//4.否,拦截
//4.1 设置状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); //设置为未登录
//4.2 拦截请求
return exchange.getResponse().setComplete();

}
}

过滤器执行

  • 请求进入网关后会碰到三类过滤器:当前路由的过滤器,DefaultFilter,GlobalFilter
  • 请求路由后,会将当前路由过滤器和DefaultFilter,GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器
  • 类型不同的问题:GlobalFilter会传入过滤器适配器,而适配器实现了GatewayFilter接口

过滤器执行顺序

  • 每一个过滤器都必须指定一个int类型的order值,order越小优先级越高
  • GlobalFilter通过实现Ordered接口或@Order注解指定Order值
  • 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增
  • 当过滤器order一样时,会按照defaultFilter > 路由过滤器 > GlobalFilter的顺序执行

  • 在配置文件中写的断言规则只是字符串,会被Predicate Factory读取并处理,转变为路由判断条件
  • Spring提供了11种基本的Predicate工厂
名称 说明 实例
After 某个时间点后的请求 - After=2022-06-05T17:42:244-07:00[America/Denver]
Before 某个时间点之前的请求 - Before=2023-06-05T17:42:244+08:00[Asia/Shanghai]
Between 某两个时间点之间的请求 - Between=2022-06-05T17:42:244-07:00[America/Denver],2023-06-05T17:42:244+08:00[Asia/Shanghai]
Cookie 请求必须包含某些Cookie - Cookie=chocolate, ch.p
Header 请求必须包含某些Header - Header=X-Request-Id, \d+
Method 请求必须是指定方式 - Method=GET, POST
Path 请求必须是指定路径规则 - Path=/red/{segment},/blue/**
Query 请求必须包含指定参数 - Query=name,jack或者- Query=name
RemoteAddr 请求的ip必须是指定范围 -RemoteAddr=192.168.1.1/24
Weight 权重处理

为什么需要网关

  • 身份认证和权限校验
  • 服务路由,负载均衡
  • 请求限流

网关技术的实现

  • gateway:基于Spring5中提供的WebFlux,属于响应式编程的实现,具有更好的性能
    • zuul:基于Servlet的实现,属于阻塞式编程

搭建网关服务

  • 创建新的module,引入SpringCloudGateway的依赖和naocs的服务发现依赖
  • 创建启动类
  • 编写路由配置及nacos地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:80 # nacos地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id 自定义 只要唯一即可
# uri: http://localhost:8081 固定目标地址
uri: lb://userservice #路由的目标地址 lb就是负载均衡 后面是服务名称
predicates: # 路由断言 也就是判断请求是否符合路由规则条件
- Path=/user/** # 按照路径匹配 只要以/user/开头就符合要求

Feign底层客户端实现

  • URLConnection:默认实现,不支持连接池
  • Apache HttpClient:支持连接池
  • OKHttp:支持连接池

优化Feign的性能主要包括

  • 使用连接池代替默认的URLConnection
  • 日志级别,最好用basic或者none

Feign添加HttpClient支持

  • 引入依赖
1
2
3
4
5
<!--引入HttpClient依赖-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
  • 配置连接池
1
2
3
4
5
feign:
httpclient:
enabled: true # 支持httpClient开关
max-connections: 200 # 最大连接数
max-connections-per-route: 50 # 单个请求路径最大连接

Feign的最佳实践

  • 方式一(继承):给消费者的FeignClient和提供者的controller定义统一的父接口作为标准
    • 客户端和服务端共享接口,紧耦合
    • 对Spring MVC 无效
  • 方式二(抽取):将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到该模块中,提供给消费者调用
    • 消费者需要方法不同,单独对于一个消费者来说,有的方法较为多余

抽取FeignClient

  • 首先创建一个module,命名为feign-api,然后引入feign的starter依赖
  • 将order-service中编写的UserClient,User,DefaultFeignConfiguration都复制到项目中
  • 在order-service中引入feign-api的依赖
  • 修改order-service中与上述三个组件有关的import部分,改成导入feign-api中的包
  • 重启测试
  • 当定义的Client不在Spring扫描范围时,Client无法使用。两种方案:

    • 指定FeignClient所在包
    1
    @EnableFeignClients(basePackages = "cn.itcast.feign.clients")
    • 指定FeignClient字节码
    1
    @EnableFeignClients(clients = {UserClient.class})

RestTemplate方式调用存在的问题

1
2
3
4
//2.1 url路径
String url = "http://userservice/user/" + order.getUserId();
//2.2 发送http请求,实现远程调用
User user = restTemplate.getForObject(url, User.class);
  • 代码可读性差,编程体验不统一
  • 参数复杂URL难以维护

Feign是什么?

Feign是一个声明式的http客户端,其作用就是帮助我们优雅的实现http请求的发送

https://github.com/OpenFeign/feign

定义和使用Feign客户端

  • 引入依赖
1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • 启动类添加@EnableFeignClients注解
  • 实现client接口
1
2
3
4
5
6
@FeignClient("userservice")
public interface UserClient {

@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
  • 调用接口发送http请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class OrderService {

@Autowired
private OrderMapper orderMapper;

@Autowired
private UserClient userClient;

public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
//2.利用Feign发起远程调用
User user = userClient.findById(order.getUserId());
//2.1 封装User对象到Order
order.setUser(user);
// 4.返回
return order;
}
}

自定义Feign配置

Feign运行自定义配置来覆盖默认配置,可以修改的配置如下

类型 作用 说明
feign.Logger.Level 修改日志级别 四种级别:NONE,BASIC,HEADERS,FULL
feign.codec.Decoder 响应结果的解析器 http远程调用的结果做解析
feign.codec.Encoder 请求参数编码 将请求参数编码,便于通过http请求发送
feign.Contract 支持的注解格式 默认是SpringMVC的注解
feign.Retryer 失败重试机制 请求失败的重试机制,默认没有,会使用Ribbon的

一般需要配置的是日志级别

Feign日志配置
  • 配置文件方式

    • 全局生效
    1
    2
    3
    4
    feign:
    client:
    default: #全局配置
    loggerLevel: FULL #日志级别
    • 局部生效
    1
    2
    3
    4
    feign:
    client:
    userservice: 服务名称
    loggerLevel: FULL #日志级别
  • Java代码方式,需要声明Bean

1
2
3
4
5
6
public class FeignClientConfiguration {
@Bean
public Logger.Level.feignLogLevel() {
return Logger.Level.BASIC;
}
}
  • 全局配置,放在@EnableFeignClients注解中
1
@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)
  • 局部配置,放到@FeignClent注解中
1
@FeignClient(value = "userservice", configuration = FeignClientConfiguration.class)