十、Docker
10.1 Docker工作原理
大型项目组件较多,运行环境也较为复杂,部署时会碰到一些问题:
- 依赖关系复杂,容易出现兼容性问题
- 开发、测试、生产环境有差异
会出现一堆的问题,版本之间的不兼容呀,各个Linux服务之间的不兼容
10.1.1 Docker解决版本依赖之间的兼容问题
- 将应用的Libs(函数库)、Deps(依赖)、配置与应用一起打包
- 将每个应用放到一个隔离容器去运行,避免互相干扰(沙箱容器)
10.1.2 Docker解决不同系统环境的问题
- 内核与硬件交互,提供操作硬件的指令
- 系统应用封装内核指令为函数,便于程序员调用
- 用户程序基于系统函数库实现功能
- Docker将用户程序与所需要调用的系统(比如Ubuntu)函数库一起打包
- Docker运行到不同操作系统时,直接基于打包的库函数,借助于操作系统的Linux内核来运行
Docker如何解决大型项目依赖关系复杂,不同组件依赖的兼容性问题?
Docker允许开发中将应用、依赖、函数库、配置一起打包,形成可移植镜像
Docker应用运行在容器中,使用沙箱机制,相互隔离
Docker如何解决开发、测试、生产环境有差异的问题
Docker镜像中包含完整运行环境,包括系统函数库,仅依赖系统的Linux内核,因此可以在任意Linux操作系统上运行
兼容问题
将应用Libs(函数库)与应用、配置、依赖一起打包,形成镜像,可以迁移到任意Linux操作系统
Docker运行在容器中,相互隔离
10.1.3 Docker介绍
Docker是一个开放平台,用于开发、运输和运行应用程序。 Docker使您能够将应用程序与基础架构分离,以便您可以快速交付软件。通过利用Docker的方法来运输,测试和部署代码,您可以将代码更快地推向生产环境,并使您更轻松地管理它们。
Docker是一个快速交付应用、运行应用的技术:
可以将程序及其依赖、运行环境一起打包为一个镜像,可以迁移到任意Linux操作系统
运行时利用沙箱机制形成隔离容器,各个应用互不干扰
启动、移除都可以通过一行命令完成,方便快捷
10.2 Docker与虚拟机的区别
虚拟机(virtual machine)是在操作系统中模拟硬件设备,然后运行另一个操作系统,比如在 Windows 系统里面运行 Ubuntu 系统,这样就可以运行任意的Ubuntu应用了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A0N7YgNG-1682515523415)(null)]
Docker直接调用操作系统
Docker需要借助Hypervisor,才能调用操作系统
特性 | Docker | 虚拟机 |
性能 | 接近原生 | 性能较差 |
硬盘占用 | 一般为 MB | 一般为GB |
启动 | 秒级 | 分钟级 |
- docker是一个系统进程;虚拟机是在操作系统中的操作系统
- docker体积小、启动速度快、性能好;虚拟机体积大、启动速度慢、性能一般
更多的是使用Docker进行部署项目
10.3 Docker架构
10.3.1 镜像与容器
镜像(Image):Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像。
将应用程序及其依赖、环境、配置打包在一起
容器(Container):镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器做隔离,对外不可见。
镜像运行起来就是容器,一个镜像可以运行多个容器
10.3.2 镜像共享
DockerHub:DockerHub是一个Docker镜像的托管平台。这样的平台称为Docker Registry
10.3.3 Docker架构
Docker是一个CS架构的程序,由两部分组成:
- 服务端:接收命令或远程请求,操作镜像或容器
- 客户端:发送命令或者请求到Docker服务端
10.4 Docker基本操作
镜像名称一般分两部分组成:[repository]:[tag]。
在没有指定tag时,默认是latest,代表最新版本的镜像
10.4.1 镜像操作过程
10.4.2 拉取Ngxin镜像案例
官网
docker pull nginx
1
步骤一:利用docker xx --help命令查看docker save和docker load的语法
步骤二:使用docker save导出镜像到磁盘
步骤三:使用docker load加载镜像
- 构建镜像:docker build
- 拉取镜像:docker pull
- 查看镜像:docker image
- 删除镜像:docker rmi(remove image)
- 推送镜像到服务:docker push
- 保存镜像为压缩包:docker save
- 加载压缩包为镜像:docker load
10.5 容器
容器之间的运行、暂停、停止之间的转换,不能从暂停到停止、停止到暂停
- 暂停:将容器内的进程挂起,内存暂存,可以恢复
- 停止:杀死进程,操作系统将容器占用的内存回收
- 运行:docker run
暂停:docker pause
暂停到运行:docker unpause
运行到停止:docker stop 容器ID
停止到运行:docker start
查看容器运行日志:docker log
查看所有容器运行状态:docker ps
进入容器执行命令:docker exec
删除指定的容器:docker rm 容器ID
10.5.1 案例:运行nginx容器
docker run --name containerName -d -p 8080:80 nginx
docker run :创建并运行一个容器
–name : 给容器起一个名字,比如叫做mn
-p :将宿主机端口与容器端口映射,冒号左侧是宿主机端口(相当于你正在使用Windows,端口可变的),右侧是容器端口(相当于虚拟机上应用容器的端口)
将端口暴露,让服务器进行管理,当访问服务器到指定的ip和端口时,就能访问到容器
-d:后台运行容器
nginx:镜像名称,例如nginx
docker run命令的常见参数:
- –name:指定容器名称
- -p:指定端口映射
- -d:让容器后台运行
查看容器日志的命令:
- docker logs
- 添加-f 参数可以持续查看日志
查看容器状态:
- docker ps
10.5.2 进入Nginx容器,修改HTML文件内容
- 进入容器。进入我们刚刚创建的nginx容器的命令为
docker exec -it mynginx bash
命令解读:
udocker exec :进入容器内部,执行一个命令
u-it : 给当前进入的容器创建一个标准输入、输出终端,允许我们与容器交互
umn :要进入的容器的名称
ubash:进入容器后执行的命令,bash是一个linux终端交互命令
- 进入nginx的HTML所在目录 /usr/share/nginx/html
cs /usr/share/nginx/html • 1
- 修改index.html的内容
sed -i 's#Welcome to nginx#Shier#g' index.html sed -i 's#<head>#<head><meta charset="utf-8">#g' index.html
查看容器状态:
docker ps
添加-a参数查看所有状态的容器
删除容器:
docker rm
不能删除运行中的容器,除非添加 -f 参数
进入容器:
命令是docker exec -it [容器名] [要执行的命令]
exec命令可以进入容器修改文件,但是在容器内修改文件是不推荐的
10.6 数据卷(volume)
10.6.1 容器与数据耦合的问题
10.6.2 数据卷介绍
- 虚拟目录,指向宿主机文件系统中的某个目录
- 将容器与数据分离,解耦合,方便操作容器内数据,保证数据安全
docker volume create html # 创建html数据卷 docker volume ls # 查看数据卷 docker volume inspect docker volume prune
10.6.3 挂载数据卷
- 创建容器并挂载数据卷到容器内的HTML目录
docker run --name mn -v html:/usr/share/nginx/html -p 80:80 -d nginx
- 进入html数据卷所在位置,并修改HTML内容
# 查看html数据卷的位置 docker volume inspect html # 进入该目录 cd /var/lib/docker/volumes/html/_data # 修改文件 vi index.html
如果容器运行时volume不存在,会自动被创建出来
10.6.4宿主机目录直接挂载到容器
提示:目录挂载与数据卷挂载的语法是类似的:
-v [宿主机目录]:[容器内目录]
-v [宿主机文件]:[容器内文件]
实现思路如下:
在将课前资料中的mysql.tar文件上传到虚拟机,通过load命令加载为镜像
创建目录/tmp/mysql/data
创建目录/tmp/mysql/conf,将课前资料提供的hmy.cnf文件上传到/tmp/mysql/conf
去DockerHub查阅资料,创建并运行MySQL容器,要求:
挂载/tmp/mysql/data到mysql容器内数据存储目录
挂载/tmp/mysql/conf/hmy.cnf到mysql容器的配置文件
设置MySQL密
docker run的命令中通过 -v 参数挂载文件或目录到容器中:
-v volume名称:容器内目录
-v 宿主机文件:容器内文件
-v 宿主机目录:容器内目录
数据卷挂载与目录直接挂载的
数据卷挂载耦合度低,由docker来管理目录,但是目录较深,不好找
目录挂载耦合度高,需要我们自己管理目录,不过目录容易寻找查看
0.7 DockerFile自定义镜像
镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。
10.7.1 镜像结构
镜像是分层结构,每一层称为一个Layer
- BaseImage层:包含基本的系统函数库、环境变量、文件系统
- Entrypoint:入口,是镜像中应用启动的命令
- 其它:在BaseImage基础上添加依赖、安装程序、完成整个应用的安装和配置
10.7.2 DockerFile
Dockerfile就是一个文本文件,其中包含一个个的指令****(Instruction),用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层Layer。
通过指令来构建镜像,每一层就会形成一个Layer:
DockerFile
# 指定基础镜像 FROM ubuntu:16.04 #FROM java:8-alpine # 配置环境变量,JDK的安装目录 ENV JAVA_DIR=/usr/local # 拷贝jdk和java项目的包 COPY ./jdk8.tar.gz $JAVA_DIR/ COPY ./docker-demo.jar /tmp/app.jar # 安装JDK RUN cd $JAVA_DIR \ && tar -xf ./jdk8.tar.gz \ && mv ./jdk1.8.0_144 ./java8 # 配置环境变量 ENV JAVA_HOME=$JAVA_DIR/java8 ENV PATH=$PATH:$JAVA_HOME/bin # 暴露端口 EXPOSE 8090 # 入口,java项目的启动命令 ENTRYPOINT java -jar /tmp/app.jar
基于java:8-alpine作为基础镜像(更加简单快速实现)
# 指定基础镜像 FROM java:8-alpine COPY ./docker-demo.jar /tmp/app.jar # 暴露端口 EXPOSE 8090 # 入口,java项目的启动命令 ENTRYPOINT java -jar /tmp/app.jar
10.8 Docker-Compose
- Docker Compose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器
- Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。
10.9 Docker镜像仓库
- 推送本地镜像到仓库前都必须重命名(docker tag)镜像,以镜像仓库地址为前缀
- 镜像仓库推送前需要把仓库地址配置到docker
- 服务的 daemon.json 文件中,被docker信任
- 推送使用 docker push 命令
- 拉取使用docker pull命令
十一、服务异步通讯
11.1 MQ(消息队列)
11.1.1 同步通讯和异步通讯
学习MQ之前,先学习同步通讯和异步通讯
同步通讯
同步通讯指的是通讯双方需要在时间上保持一致,也就是一个操作必须等待另一个操作完成后才能执行下一个操作。
比如一个请求需要等待服务器返回结果之后才能继续处理下一个请求。
同步通讯方式的特点:
简单明了
适用于处理量不大、并发量不高的场景
缺点:
阻塞等待:在同步通讯过程中,请求方需要等待响应方返回数据,这个等待过程可能会阻塞请求方的线程,从而导致程序无法继续执行其他任务。
慢速处理:由于同步通讯需要等待响应方返回数据后再进行下一步处理,所以它的处理速度相对慢,特别是在高并发量和大数据量的情况下。
处理逻辑复杂:同步通讯需要明确的时间规划和处理顺序,这往往需要编写更复杂的代码逻辑,增加了程序的开发难度和维护成本。
可靠性低:由于同步通讯需要双方在时间上保持一致,所以如果其中一个方出现了问题,可能会导致整个通讯过程失败,进而影响到整个系统的正常运行。
同步调用存在问题:
耦合度高:每次加入新的需求,都要修改原来的代码
性能下降:调用者需要等待服务提供者响应,如果调用链过长则响应时间等于每次调用的时间之和。
资源浪费:调用链中的每个服务在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源
级联失败:如果服务提供者出现问题,所有调用方都会跟着出问题,如同多米诺骨牌一样,迅速导致整个微服务群故障
异步通讯
异步通讯则是指通讯双方不需要时间上保持一致,也就是一个操作可以在另一个操作执行的过程中继续执行。
异步通讯方式特点:
高效性
灵活性
适用于处理大数据量、高并发量的场景
缺点:
多线程开销:由于异步通讯需要使用多线程来处理并发请求,所以会占用更多的系统资源和内存空间,进而增加系统开销和负担。
处理逻辑复杂:由于异步通讯需要使用回调函数等机制来处理回应消息,所以编写程序时需要设计更复杂的逻辑和代码结构。
可读性低:异步通讯中的回调函数容易形成层层嵌套的调用关系,这会影响程序的可读性和可维护性。
调试难度大:由于异步通讯中的执行顺序不是固定的,同时又涉及到多线程、事件驱动等复杂的技术,所以对程序进行调试比同步通讯更为困难。
异步通讯的典型例子包括消息队列、事件驱动等。
在团队合作中的异步通信发生在没有实时对话或交互的情况下,比如通过邮件、留言等方式进行沟通;而同步通信则是在实时对话或交互的情况下进行,比如在线会议、视频通话等。需要根据具体的情况选择使用异步或同步通讯方式,以达到最佳的工作效果。
异步调用
异步调用常见实现就是事件驱动模式
统一发送,但是服务执行的时间并不要同时完成
异步调用优势
- 服务解耦
- 性能提升,吞吐量提高
- 服务没有强依赖,不担心级联失败问题
- 流量削峰
四种MQ:主要是使用RabbiMQ 、RocketMQ、Kafka
11.2 RabbitMQ入门
11.2.1 RabbitMQ介绍
RabbitMQ是基于Erlang语言开发的开源消息通信中间件,官网地址:https://www.rabbitmq.com/
11.2.2 RabbitMQ结构
- channel:操作MQ的工具
- exchange:路由消息到队列中
- queue:缓存消息
- virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组
11.2.3 常见的消息模型
基本消息队列
基本消息队列的消息发送流程:
建立connection
创建channel
利用channel声明队列
利用channel向队列发送消息
基本消息队列的消息接收流程:
建立connection
创建channel
利用channel声明队列
定义consumer的消费行为handleDelivery()
利用channel将消费者与队列绑定
11.3 SpringAMQP
SpringAmqp的官方地址:https://spring.io/projects/spring-amqp
AMQP:用于应用程序或之间传递业务消息的开放标准。协议与语言和平台无关,符合微服务独立性的要求
11.3.1利用SpringAMQP实现HelloWorld中的基础消息队列功能-Basic Queue 简单队列模型
引入AMQP依赖
因为publisher和consumer服务都需要amqp依赖,因此这里把依赖直接放到父工程mq-demo:
<!--AMQP依赖,包含RabbitMQ--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
在publisher中编写测试方法,向Test.queue发送消息
- 在publisher服务中编写application.yml,添加mq连接信息
spring: rabbitmq: addresses: 8.134.37.7 port: 5672 username: shier password: 123456 virtual-host: /
在publisher服务中新建一个测试类,编写测试方法
@RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSimpleQueue() { String queueName = "Test2.queue"; String message = "hello, spring amqp!"; rabbitTemplate.convertAndSend(queueName, message); } }
SpringAMQP接收消息:
- 引入amqp的starter依赖
- 配置RabbitMQ地址
- 定义类,添加@Component注解
- 类中声明方法,添加@RabbitListener注解,方法参数就时消息
注意:消息一旦消费就会从队列删除,RabbitMQ没有消息回溯功能
11.3.2 Work Queue 工作队列模型
Work queue,工作队列,可以提高消息处理速度,避免队列消息堆积
一个发布者,多个消费者
模拟WorkQueue,实现一个队列绑定多个消费者
生产者循环发送消息到simple.queue
在publisher服务中添加一个测试方法,循环发送50条消息到simple.queue队列
/** * 发送多个消息 * * @throws InterruptedException */ @Test public void testWorkQueue() throws InterruptedException { String queueName = "Test.queue"; String message = "你好啊WorkQueue"; for (int i = 0; i < 50; i++) { rabbitTemplate.convertAndSend(queueName, message + i); Thread.sleep(30); } }
在consumer服务中添加一个消费者,也监听simple.queue
/** * 消费者1 * * @param msg * @throws InterruptedException */ @RabbitListener(queues = "Test.queue") public void listenWorkQueueMessage1(String msg) throws InterruptedException { System.out.println("消费者1接收到消息 :【" + msg + "】" + LocalTime.now()); Thread.sleep(20); } /** * 消费者2 * * @param msg * @throws InterruptedException */ @RabbitListener(queues = "Test.queue") public void listenWorkQueueMessage2(String msg) throws InterruptedException { System.err.println("消费者2接收到消息 :【" + msg + "】" + LocalTime.now()); Thread.sleep(200); }
消息预取是消息中间件中的一种重要机制,用于在高并发情况下减轻消息消费者的压力,提高消息传递的效率和稳定性。
消息预取的工作原理如下:
消息消费者向消息队列发送拉取请求。
消息队列返回一批消息给消费者。
消费者在本地缓存这些消息,并逐一处理。
当本地缓存中的消息处理完毕后,再向消息队列发送拉取请求,继续获取更多的消息。
通过消息预取机制,可以避免消息消费者频繁向消息队列发送拉取消息的请求,减少网络传输开销和系统资源占用,提高消息消费的效率和可靠性。
消费预取限制:修改application.yml文件,设置preFetch这个值,可以控制预取消息的上限
spring: rabbitmq: addresses: 8.134.37.7 virtual-host: / port: 5672 username: shier password: 123456 listener: simple: prefetch: 1 # 每次读取一条信息
多个消费者绑定到一个队列,同一条消息只会被一个消费者处理
通过设置prefetch来控制消费者预取的消息数量
11.3.3 发布、订阅模型-Fanout Exchange
以下程序的代码 在语雀查看
发布订阅模式与之前案例的区别就是允许将同一消息发送给多个消费者。实现方式是加入了exchange(交换机)。
常见exchange类型包括:
- Fanout:广播
- Direct:路由
- Topic:话题
注意:exchange负责消息路由,而不是存储,路由失败则消息丢失
Fanout Exchange 会将接收到的消息广播到每一个跟其绑定的queue
交换机的作用:
- 接收publisher发送的消息
- 将消息按照规则路由到与之绑定的队列
- 不能缓存消息,路由失败,消息丢失
- FanoutExchange的会将消息路由到每个绑定的队列
声明队列、交换机、绑定关系的Bean:
- Queue
- FanoutExchange
- Binding
11.3.4 发布、订阅模型-Direct Exchange
Direct Exchange 会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式(routes)。
每一个Queue都与Exchange设置一个BindingKey(类似于暗号,暗号对上,则进行输出数据)
发布者发送消息时,指定消息的RoutingKey
Exchange将消息路由到BindingKey与消息RoutingKey一致的队列
可以指定多个key,最后就是都能接受到
描述下Direct交换机与Fanout交换机的差异?
Fanout交换机将消息路由给每一个与之绑定的队列
Direct交换机根据RoutingKey判断路由给哪个队列
如果多个队列具有相同的RoutingKey,则与Fanout功能类似
基于@RabbitListener注解声明队列和交换机有哪些常见注解?
@Queue
@Exchange
11.3.5 发布、订阅模型-Topic Exchange
TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并且以 .
分割。
Queue与Exchange指定BindingKey时可以使用通配符:
#
:代指0个或多个单词
*
:代指一个单词
11.3.6 消息转换器
说明:在SpringAMQP的发送方法中,接收消息的类型是Object,也就是说我们可以发送任意对象类型的消息,SpringAMQP会帮我们序列化为字节后发送。
Spring的对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的。而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化。
如果要修改只需要定义一个MessageConverter 类型的Bean即可。推荐用JSON方式序列化,步骤如下:在 publisher服务引入依赖
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>
在publisher服务声明MessageConverter
@Bean public MessageConverter jsonMessageConverter(){ return new Jackson2JsonMessageConverter(); }
SpringAMQP中消息的序列化和反序列化是怎么实现的?
- 利用MessageConverter实现的,默认是JDK的序列化
- 注意发送方与接收方必须使用相同的MessageConverter