# springcloud **Repository Path**: javawcj/cloud ## Basic Information - **Project Name**: springcloud - **Description**: 包含 Spring Cloud 框架的基础知识,微服务架构、服务注册与发现、服务网关、配置中心等内容,并提供了大量的案例代码和实践练习,方便学习和掌握 Spring Cloud 的使用方法。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-05-26 - **Last Updated**: 2023-06-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SpringCloud # 1.认识微服务 随着互联网行业的发展,对服务的要求也越来越高,服务架构也从单体架构逐渐演变为现在流行的微服务架构。这些架构之间有怎样的差别呢? ## 1.1.单体架构 **单体架构**:将业务的所有功能集中在一个项目中开发,打成一个包部署。 ![image-20210713202807818](assets/image-20210713202807818.png) 单体架构的优缺点如下: **优点:** - 架构简单 - 部署成本低 **缺点:** - 耦合度高(维护困难、升级困难) ## 1.2.分布式架构 **分布式架构**:根据业务功能对系统做拆分,每个业务功能模块作为独立项目开发,称为一个服务。 ![image-20210713203124797](assets/image-20210713203124797.png) 分布式架构的优缺点: **优点:** - 降低服务耦合 - 有利于服务升级和拓展 **缺点:** - 服务调用关系错综复杂 分布式架构虽然降低了服务耦合,但是服务拆分时也有很多问题需要思考: - 服务拆分的粒度如何界定? - 服务之间如何调用? - 服务的调用关系如何管理? 人们需要制定一套行之有效的标准来约束分布式架构。 ## 1.3.微服务 微服务的架构特征: - 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责 - 自治:团队独立、技术独立、数据独立,独立部署和交付 - 面向服务:服务提供统一标准的接口,与语言和技术无关 - 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题 ![image-20210713203753373](assets/image-20210713203753373.png) 微服务的上述特性其实是在给分布式架构制定一个标准,进一步降低服务之间的耦合度,提供服务的独立性和灵活性。做到高内聚,低耦合。 因此,可以认为**微服务**是一种经过良好架构设计的**分布式架构方案** 。 但方案该怎么落地?选用什么样的技术栈?全球的互联网公司都在积极尝试自己的微服务落地方案。 其中在Java领域最引人注目的就是SpringCloud提供的方案了。 ## 1.4.SpringCloud SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud。 SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。 其中常见的组件包括: ![image-20210713204155887](assets/image-20210713204155887.png) 另外,SpringCloud底层是依赖于SpringBoot的,并且有版本的兼容关系,如下: ![image-20210713205003790](assets/image-20210713205003790.png) Hoxton.SR10,因此对应的SpringBoot版本是2.3.x版本。 ## 1.5.总结 - 单体架构:简单方便,高度耦合,扩展性差,适合小型项目。例如:学生管理系统 - 分布式架构:松耦合,扩展性好,但架构复杂,难度大。适合大型互联网项目,例如:京东、淘宝 - 微服务:一种良好的分布式架构方案 ①优点:拆分粒度更小、服务更独立、耦合度更低 ②缺点:架构非常复杂,运维、监控、部署难度提高 - SpringCloud是微服务架构的一站式解决方案,集成了各种优秀微服务功能组件 # 2.服务拆分和远程调用 任何分布式架构都离不开服务的拆分,微服务也是一样。 ## 2.1.服务拆分原则 这里我总结了微服务拆分时的几个原则: - 不同微服务,不要重复开发相同业务 - 微服务数据独立,不要访问其它微服务的数据库 - 微服务可以将自己的业务暴露为接口,供其它微服务调用 ![image-20210713210800950](assets/image-20210713210800950.png) ## 2.2.服务拆分示例 以课前资料中的微服务cloud-demo为例,其结构如下: ![image-20210713211009593](assets/image-20210713211009593.png) cloud-demo:父工程,管理依赖 - order-service:订单微服务,负责订单相关业务 - user-service:用户微服务,负责用户相关业务 要求: - 订单微服务和用户微服务都必须有各自的数据库,相互独立 - 订单服务和用户服务都对外暴露Restful的接口 - 订单服务如果需要查询用户信息,只能调用用户服务的Restful接口,不能查询用户数据库 ### 2.2.1.导入Sql语句 首先,将课前资料提供的`cloud-order.sql`和`cloud-user.sql`导入到mysql中: ![image-20210713211417049](assets/image-20210713211417049.png) cloud-user表中初始数据如下: ![image-20210713211550169](assets/image-20210713211550169.png) cloud-order表中初始数据如下: ![image-20210713211657319](assets/image-20210713211657319.png) cloud-order表中持有cloud-user表中的id字段。 ### 2.2.2.导入demo工程 用IDEA导入课前资料提供的Demo: ![image-20210713211814094](assets/image-20210713211814094.png) 项目结构如下: ![image-20210713212656887](assets/image-20210713212656887.png) 导入后,会在IDEA右下角出现弹窗: ![image-20210713212349272](assets/image-20210713212349272.png) 点击弹窗,然后按下图选择: ![image-20210713212336185](assets/image-20210713212336185.png) 会出现这样的菜单: ![image-20210713212513324](assets/image-20210713212513324.png) 配置下项目使用的JDK: ![image-20210713220736408](assets/image-20210713220736408.png) ## 2.3.实现远程调用案例 在order-service服务中,有一个根据id查询订单的接口: ![image-20210713212749575](assets/image-20210713212749575.png) 根据id查询订单,返回值是Order对象,如图: ![image-20210713212901725](assets/image-20210713212901725.png) 其中的user为null 在user-service中有一个根据id查询用户的接口: ![image-20210713213146089](assets/image-20210713213146089.png) 查询的结果如图: ![image-20210713213213075](assets/image-20210713213213075.png) ### 2.3.1.案例需求: 修改order-service中的根据id查询订单业务,要求在查询订单的同时,根据订单中包含的userId查询出用户信息,一起返回。 ![image-20210713213312278](assets/image-20210713213312278.png) 因此,我们需要在order-service中 向user-service发起一个http的请求,调用http://localhost:8081/user/{userId}这个接口。 大概的步骤是这样的: - 注册一个RestTemplate的实例到Spring容器 - 修改order-service服务中的OrderService类中的queryOrderById方法,根据Order对象中的userId查询User - 将查询的User填充到Order对象,一起返回 ### 2.3.2.注册RestTemplate 首先,我们在order-service服务中的OrderApplication启动类中,注册RestTemplate实例: ```java package cn.itcast.order; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @MapperScan("cn.itcast.order.mapper") @SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } @Bean public RestTemplate restTemplate() { return new RestTemplate(); } } ``` ### 2.3.3.实现远程调用 修改order-service服务中的cn.itcast.order.service包下的OrderService类中的queryOrderById方法: ![image-20210713213959569](assets/image-20210713213959569.png) ## 2.4.提供者与消费者 在服务调用关系中,会有两个不同的角色: **服务提供者**:一次业务中,被其它微服务调用的服务。(提供接口给其它微服务) **服务消费者**:一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口) ![image-20210713214404481](assets/image-20210713214404481.png) 但是,服务提供者与服务消费者的角色并不是绝对的,而是相对于业务而言。 如果服务A调用了服务B,而服务B又调用了服务C,服务B的角色是什么? - 对于A调用B的业务而言:A是服务消费者,B是服务提供者 - 对于B调用C的业务而言:B是服务消费者,C是服务提供者 因此,服务B既可以是服务提供者,也可以是服务消费者。 # 3.Eureka注册中心 假如我们的服务提供者user-service部署了多个实例,如图: ![image-20210713214925388](assets/image-20210713214925388.png) 大家思考几个问题: - order-service在发起远程调用的时候,该如何得知user-service实例的ip地址和端口? - 有多个user-service实例地址,order-service调用时该如何选择? - order-service如何得知某个user-service实例是否依然健康,是不是已经宕机? ## 3.1.Eureka的结构和作用 这些问题都需要利用SpringCloud中的注册中心来解决,其中最广为人知的注册中心就是Eureka,其结构如下: ![image-20210713220104956](assets/image-20210713220104956.png) 回答之前的各个问题。 问题1:order-service如何得知user-service实例地址? 获取地址信息的流程如下: - user-service服务实例启动后,将自己的信息注册到eureka-server(Eureka服务端)。这个叫服务注册 - eureka-server保存服务名称到服务实例地址列表的映射关系 - order-service根据服务名称,拉取实例地址列表。这个叫服务发现或服务拉取 问题2:order-service如何从多个user-service实例中选择具体的实例? - order-service从实例列表中利用负载均衡算法选中一个实例地址 - 向该实例地址发起远程调用 问题3:order-service如何得知某个user-service实例是否依然健康,是不是已经宕机? - user-service会每隔一段时间(默认30秒)向eureka-server发起请求,报告自己状态,称为心跳 - 当超过一定时间没有发送心跳时,eureka-server会认为微服务实例故障,将该实例从服务列表中剔除 - order-service拉取服务时,就能将故障实例排除了 > 注意:一个微服务,既可以是服务提供者,又可以是服务消费者,因此eureka将服务注册、服务发现等功能统一封装到了eureka-client端 因此,接下来我们动手实践的步骤包括: ![image-20210713220509769](assets/image-20210713220509769.png) ## 3.2.搭建eureka-server 首先大家注册中心服务端:eureka-server,这必须是一个独立的微服务 ### 3.2.1.创建eureka-server服务 在cloud-demo父工程下,创建一个子模块: ![image-20210713220605881](assets/image-20210713220605881.png) 填写模块信息: ![image-20210713220857396](assets/image-20210713220857396.png) 然后填写服务信息: ![image-20210713221339022](assets/image-20210713221339022.png) ### 3.2.2.引入eureka依赖 引入SpringCloud为eureka提供的starter依赖: ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-server ``` ### 3.2.3.编写启动类 给eureka-server服务编写一个启动类,一定要添加一个@EnableEurekaServer注解,开启eureka的注册中心功能: ```java package cn.itcast.eureka; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); } } ``` ### 3.2.4.编写配置文件 编写一个application.yml文件,内容如下: ```yaml server: port: 10086 spring: application: name: eureka-server eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka ``` ### 3.2.5.启动服务 启动微服务,然后在浏览器访问:http://127.0.0.1:10086 看到下面结果应该是成功了: ![image-20210713222157190](assets/image-20210713222157190.png) ## 3.3.服务注册 下面,我们将user-service注册到eureka-server中去。 ### 1)引入依赖 在user-service的pom文件中,引入下面的eureka-client依赖: ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` ### 2)配置文件 在user-service中,修改application.yml文件,添加服务名称、eureka地址: ```yaml spring: application: name: userservice eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka ``` ### 3)启动多个user-service实例 为了演示一个服务有多个实例的场景,我们添加一个SpringBoot的启动配置,再启动一个user-service。 首先,复制原来的user-service启动配置: ![image-20210713222656562](assets/image-20210713222656562.png) 然后,在弹出的窗口中,填写信息: ![image-20210713222757702](assets/image-20210713222757702.png) 现在,SpringBoot窗口会出现两个user-service启动配置: ![image-20210713222841951](assets/image-20210713222841951.png) 不过,第一个是8081端口,第二个是8082端口。 启动两个user-service实例: ![image-20210713223041491](assets/image-20210713223041491.png) 查看eureka-server管理页面: ![image-20210713223150650](assets/image-20210713223150650.png) ## 3.4.服务发现 下面,我们将order-service的逻辑修改:向eureka-server拉取user-service的信息,实现服务发现。 ### 1)引入依赖 之前说过,服务发现、服务注册统一都封装在eureka-client依赖,因此这一步与服务注册时一致。 在order-service的pom文件中,引入下面的eureka-client依赖: ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` ### 2)配置文件 服务发现也需要知道eureka地址,因此第二步与服务注册一致,都是配置eureka信息: 在order-service中,修改application.yml文件,添加服务名称、eureka地址: ```yaml spring: application: name: orderservice eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka ``` ### 3)服务拉取和负载均衡 最后,我们要去eureka-server中拉取user-service服务的实例列表,并且实现负载均衡。 不过这些动作不用我们去做,只需要添加一些注解即可。 在order-service的OrderApplication中,给RestTemplate这个Bean添加一个@LoadBalanced注解: ![image-20210713224049419](assets/image-20210713224049419.png) 修改order-service服务中的cn.itcast.order.service包下的OrderService类中的queryOrderById方法。修改访问的url路径,用服务名代替ip、端口: ![image-20210713224245731](assets/image-20210713224245731.png) spring会自动帮助我们从eureka-server端,根据userservice这个服务名称,获取实例列表,而后完成负载均衡。 # 4.Ribbon负载均衡 上一节中,我们添加了@LoadBalanced注解,即可实现负载均衡功能,这是什么原理呢? ## 4.1.负载均衡原理 SpringCloud底层其实是利用了一个名为Ribbon的组件,来实现负载均衡功能的。 ![image-20210713224517686](assets/image-20210713224517686.png) 那么我们发出的请求明明是http://userservice/user/1,怎么变成了http://localhost:8081的呢? ## 4.2.源码跟踪 为什么我们只输入了service名称就可以访问了呢?之前还要获取ip和端口。 显然有人帮我们根据service名称,获取到了服务实例的ip和端口。它就是`LoadBalancerInterceptor`,这个类会在对RestTemplate的请求进行拦截,然后从Eureka根据服务id获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,替换服务id。 我们进行源码跟踪: ### 1)LoadBalancerIntercepor ![1525620483637](assets/1525620483637.png) 可以看到这里的intercept方法,拦截了用户的HttpRequest请求,然后做了几件事: - `request.getURI()`:获取请求uri,本例中就是 http://user-service/user/8 - `originalUri.getHost()`:获取uri路径的主机名,其实就是服务id,`user-service` - `this.loadBalancer.execute()`:处理服务id,和用户请求。 这里的`this.loadBalancer`是`LoadBalancerClient`类型,我们继续跟入。 ### 2)LoadBalancerClient 继续跟入execute方法: ![1525620787090](assets/1525620787090.png) 代码是这样的: - getLoadBalancer(serviceId):根据服务id获取ILoadBalancer,而ILoadBalancer会拿着服务id去eureka中获取服务列表并保存起来。 - getServer(loadBalancer):利用内置的负载均衡算法,从服务列表中选择一个。本例中,可以看到获取了8082端口的服务 放行后,再次访问并跟踪,发现获取的是8081: ![1525620835911](assets/1525620835911.png) 果然实现了负载均衡。 ### 3)负载均衡策略IRule 在刚才的代码中,可以看到获取服务使通过一个`getServer`方法来做负载均衡: ![1525620835911](assets/1525620835911.png) 我们继续跟入: ![1544361421671](assets/1544361421671.png) 继续跟踪源码chooseServer方法,发现这么一段代码: ![1525622652849](assets/1525622652849.png) 我们看看这个rule是谁: ![1525622699666](assets/1525622699666.png) 这里的rule默认值是一个`RoundRobinRule`,看类的介绍: ![1525622754316](assets/1525622754316.png) 这不就是轮询的意思嘛。 到这里,整个负载均衡的流程我们就清楚了。 ### 4)总结 SpringCloudRibbon的底层采用了一个拦截器,拦截了RestTemplate发出的请求,对地址做了修改。用一幅图来总结一下: ![image-20210713224724673](assets/image-20210713224724673.png) 基本流程如下: - 拦截我们的RestTemplate请求http://userservice/user/1 - RibbonLoadBalancerClient会从请求url中获取服务名称,也就是user-service - DynamicServerListLoadBalancer根据user-service到eureka拉取服务列表 - eureka返回列表,localhost:8081、localhost:8082 - IRule利用内置负载均衡规则,从列表中选择一个,例如localhost:8081 - RibbonLoadBalancerClient修改请求地址,用localhost:8081替代userservice,得到http://localhost:8081/user/1,发起真实请求 ## 4.3.负载均衡策略 ### 4.3.1.负载均衡策略 负载均衡的规则都定义在IRule接口中,而IRule有很多不同的实现类: ![image-20210713225653000](assets/image-20210713225653000.png) 不同规则的含义如下: | **内置负载均衡规则类** | **规则描述** | | ------------------------- | ------------------------------------------------------------ | | RoundRobinRule | 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 | | AvailabilityFilteringRule | 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的..ActiveConnectionsLimit属性进行配置。 | | WeightedResponseTimeRule | 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 | | **ZoneAvoidanceRule** | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 | | BestAvailableRule | 忽略那些短路的服务器,并选择并发数较低的服务器。 | | RandomRule | 随机选择一个可用的服务器。 | | RetryRule | 重试机制的选择逻辑 | 默认的实现就是ZoneAvoidanceRule,是一种轮询方案 ### 4.3.2.自定义负载均衡策略 通过定义IRule实现可以修改负载均衡规则,有两种方式: 1. 代码方式:在order-service中的OrderApplication类中,定义一个新的IRule: ```java @Bean public IRule randomRule(){ return new RandomRule(); } ``` 2. 配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则: ```yaml userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务 ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则 ``` > **注意**,一般用默认的负载均衡规则,不做修改。 ## 4.4.饥饿加载 Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。 而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载: ```yaml ribbon: eager-load: enabled: true clients: userservice ``` # 5.Nacos注册中心 国内公司一般都推崇阿里巴巴的技术,比如注册中心,SpringCloudAlibaba也推出了一个名为Nacos的注册中心。 ## 5.1.认识和安装Nacos [Nacos](https://nacos.io/)是阿里巴巴的产品,现在是[SpringCloud](https://spring.io/projects/spring-cloud)中的一个组件。相比[Eureka](https://github.com/Netflix/eureka)功能更加丰富,在国内受欢迎程度较高。 ![image-20210713230444308](assets/image-20210713230444308.png) 安装方式: 下载 Nacos Server - 可以到 Nacos 的官网下载最新的 Nacos Server 版本。解压后可以看到 bin 目录下有启动脚本,在 MacOS 中可以选择 nacos/bin 目录下的 startup.sh 文件。 - 修改配置文件 在 nacos/conf 目录下找到 application.properties 文件,修改其中的数据库配置项和端口号等信息。 - 启动 Nacos 在终端中进入 Nacos 的 bin 目录,执行以下命令启动 Nacos: ``` sh startup.sh -m standalone -m 参数的值为 standalone,表示采用单机模式启动 Nacos。 ``` - 访问 Nacos 控制台 Nacos 启动完成后,在浏览器中输入以下 URL 可以访问 Nacos 的控制台: ``` http://localhost:8848/nacos/ ``` 其中端口号可以根据自己修改的配置文件中的配置进行调整 ## 5.2.服务注册到nacos Nacos是SpringCloudAlibaba的组件,而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos和使用Eureka对于微服务来说,并没有太大区别。 主要差异在于: - 依赖不同 - 服务地址不同 ### 1)引入依赖 在cloud-demo父工程的pom文件中的``中引入SpringCloudAlibaba的依赖: ```xml com.alibaba.cloud spring-cloud-alibaba-dependencies 2.2.6.RELEASE pom import ``` 然后在user-service和order-service中的pom文件中引入nacos-discovery依赖: ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` > **注意**:不要忘了注释掉eureka的依赖。 ### 2)配置nacos地址 在user-service和order-service的application.yml中添加nacos地址: ```yaml spring: cloud: nacos: server-addr: localhost:8848 ``` > **注意**:不要忘了注释掉eureka的地址 ### 3)重启 重启微服务后,登录nacos管理页面,可以看到微服务信息: ![image-20210713231439607](assets/image-20210713231439607.png) ## 5.3.服务分级存储模型 一个**服务**可以有多个**实例**,例如我们的user-service,可以有: - 127.0.0.1:8081 - 127.0.0.1:8082 - 127.0.0.1:8083 假如这些实例分布于全国各地的不同机房,例如: - 127.0.0.1:8081,在上海机房 - 127.0.0.1:8082,在上海机房 - 127.0.0.1:8083,在杭州机房 Nacos就将同一机房内的实例 划分为一个**集群**。 也就是说,user-service是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如图: ![image-20210713232522531](assets/image-20210713232522531.png) 微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。当本集群内不可用时,才访问其它集群。例如: ![image-20210713232658928](assets/image-20210713232658928.png) 杭州机房内的order-service应该优先访问同机房的user-service。 ### 5.3.1.给user-service配置集群 修改user-service的application.yml文件,添加集群配置: ```yaml spring: cloud: nacos: server-addr: localhost:8848 discovery: cluster-name: HZ # 集群名称 ``` 重启两个user-service实例后,我们可以在nacos控制台看到下面结果: ![image-20210713232916215](assets/image-20210713232916215.png) 我们再次复制一个user-service启动配置,添加属性: ```sh -Dserver.port=8083 -Dspring.cloud.nacos.discovery.cluster-name=SH ``` 配置如图所示: ![image-20210713233528982](assets/image-20210713233528982.png) 启动UserApplication3后再次查看nacos控制台: ![image-20210713233727923](assets/image-20210713233727923.png) ### 5.3.2.同集群优先的负载均衡 默认的`ZoneAvoidanceRule`并不能实现根据同集群优先来实现负载均衡。 因此Nacos中提供了一个`NacosRule`的实现,可以优先从同集群中挑选实例。 1)给order-service配置集群信息 修改order-service的application.yml文件,添加集群配置: ```sh spring: cloud: nacos: server-addr: localhost:8848 discovery: cluster-name: HZ # 集群名称 ``` 2)修改负载均衡规则 修改order-service的application.yml文件,修改负载均衡规则: ```yaml userservice: ribbon: NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则 ``` ## 5.4.权重配置 实际部署中会出现这样的场景: 服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。 但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。 因此,Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高。 在nacos控制台,找到user-service的实例列表,点击编辑,即可修改权重: ![image-20210713235133225](assets/image-20210713235133225.png) 在弹出的编辑窗口,修改权重: ![image-20210713235235219](assets/image-20210713235235219.png) > **注意**:如果权重修改为0,则该实例永远不会被访问 ## 5.5.环境隔离 Nacos提供了namespace来实现环境隔离功能。 - nacos中可以有多个namespace - namespace下可以有group、service等 - 不同namespace之间相互隔离,例如不同namespace的服务互相不可见 ![image-20210714000101516](assets/image-20210714000101516.png) ### 5.5.1.创建namespace 默认情况下,所有service、data、group都在同一个namespace,名为public: ![image-20210714000414781](assets/image-20210714000414781.png) 我们可以点击页面新增按钮,添加一个namespace: ![image-20210714000440143](assets/image-20210714000440143.png) 然后,填写表单: ![image-20210714000505928](assets/image-20210714000505928.png) 就能在页面看到一个新的namespace: ![image-20210714000522913](assets/image-20210714000522913.png) ### 5.5.2.给微服务配置namespace 给微服务配置namespace只能通过修改配置来实现。 例如,修改order-service的application.yml文件: ```yaml spring: cloud: nacos: server-addr: localhost:8848 discovery: cluster-name: HZ namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 命名空间,填ID ``` 重启order-service后,访问控制台,可以看到下面的结果: ![image-20210714000830703](assets/image-20210714000830703.png) ![image-20210714000837140](assets/image-20210714000837140.png) 此时访问order-service,因为namespace不同,会导致找不到userservice,控制台会报错: ![image-20210714000941256](assets/image-20210714000941256.png) ## 5.6.Nacos与Eureka的区别 Nacos的服务实例分为两种l类型: - 临时实例:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型。 - 非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例。 配置一个服务实例为永久实例: ```yaml spring: cloud: nacos: discovery: ephemeral: false # 设置为非临时实例 ``` Nacos和Eureka整体结构类似,服务注册、服务拉取、心跳等待,但是也存在一些差异: ![image-20210714001728017](assets/image-20210714001728017.png) - Nacos与eureka的共同点 - 都支持服务注册和服务拉取 - 都支持服务提供者心跳方式做健康检测 - Nacos与Eureka的区别 - Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式 - 临时实例心跳不正常会被剔除,非临时实例则不会被剔除 - Nacos支持服务列表变更的消息推送模式,服务列表更新更及时 - Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式 # 1.Nacos配置管理 Nacos除了可以做注册中心,同样可以做配置管理来使用。 ## 1.1.统一配置管理 当微服务部署的实例越来越多,达到数十、数百时,逐个修改微服务配置就会让人抓狂,而且很容易出错。我们需要一种统一配置管理方案,可以集中管理所有实例的配置。 ![image-20210714164426792](assets/image-20210714164426792.png) Nacos一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新。 ### 1.1.1.在nacos中添加配置文件 如何在nacos中管理配置呢? ![image-20210714164742924](assets/image-20210714164742924.png) 然后在弹出的表单中,填写配置信息: ![image-20210714164856664](assets/image-20210714164856664.png) > 注意:项目的核心配置,需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。 ### 1.1.2.从微服务拉取配置 微服务要拉取nacos中管理的配置,并且与本地的application.yml配置合并,才能完成项目启动。 但如果尚未读取application.yml,又如何得知nacos地址呢? 因此spring引入了一种新的配置文件:bootstrap.yaml文件,会在application.yml之前被读取,流程如下: ![img](assets/L0iFYNF.png) 1)引入nacos-config依赖 首先,在user-service服务中,引入nacos-config的客户端依赖: ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config ``` 2)添加bootstrap.yaml 然后,在user-service中添加一个bootstrap.yaml文件,内容如下: ```yaml spring: application: name: userservice # 服务名称 profiles: active: dev #开发环境,这里是dev cloud: nacos: server-addr: localhost:8848 # Nacos地址 config: file-extension: yaml # 文件后缀名 ``` 这里会根据spring.cloud.nacos.server-addr获取nacos地址,再根据 `${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}`作为文件id,来读取配置。 本例中,就是去读取`userservice-dev.yaml`: ![image-20210714170845901](assets/image-20210714170845901.png) 3)读取nacos配置 在user-service中的UserController中添加业务逻辑,读取pattern.dateformat配置: ![image-20210714170337448](assets/image-20210714170337448.png) 完整代码: ```java package cn.itcast.user.web; import cn.itcast.user.pojo.User; import cn.itcast.user.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @Slf4j @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @Value("${pattern.dateformat}") private String dateformat; @GetMapping("now") public String now(){ return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat)); } // ...略 } ``` 在页面访问,可以看到效果: ![image-20210714170449612](assets/image-20210714170449612.png) ## 1.2.配置热更新 我们最终的目的,是修改nacos中的配置后,微服务中无需重启即可让配置生效,也就是**配置热更新**。 要实现配置热更新,可以使用两种方式: ### 1.2.1.方式一 在@Value注入的变量所在类上添加注解@RefreshScope: ![image-20210714171036335](assets/image-20210714171036335.png) ### 1.2.2.方式二 使用@ConfigurationProperties注解代替@Value注解。 在user-service服务中,添加一个类,读取patterrn.dateformat属性: ```java package cn.itcast.user.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @Data @ConfigurationProperties(prefix = "pattern") public class PatternProperties { private String dateformat; } ``` 在UserController中使用这个类代替@Value: ![image-20210714171316124](assets/image-20210714171316124.png) 完整代码: ```java package cn.itcast.user.web; import cn.itcast.user.config.PatternProperties; import cn.itcast.user.pojo.User; import cn.itcast.user.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @Slf4j @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @Autowired private PatternProperties patternProperties; @GetMapping("now") public String now(){ return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat())); } // 略 } ``` ## 1.3.配置共享 其实微服务启动时,会去nacos读取多个配置文件,例如: - `[spring.application.name]-[spring.profiles.active].yaml`,例如:userservice-dev.yaml - `[spring.application.name].yaml`,例如:userservice.yaml 而`[spring.application.name].yaml`不包含环境,因此可以被多个环境共享。 下面我们通过案例来测试配置共享 ### 1)添加一个环境共享配置 我们在nacos中添加一个userservice.yaml文件: ![image-20210714173233650](assets/image-20210714173233650.png) ### 2)在user-service中读取共享配置 在user-service服务中,修改PatternProperties类,读取新添加的属性: ![image-20210714173324231](assets/image-20210714173324231.png) 在user-service服务中,修改UserController,添加一个方法: ![image-20210714173721309](assets/image-20210714173721309.png) ### 3)运行两个UserApplication,使用不同的profile 修改UserApplication2这个启动项,改变其profile值: ![image-20210714173538538](assets/image-20210714173538538.png) ![image-20210714173519963](assets/image-20210714173519963.png) 这样,UserApplication(8081)使用的profile是dev,UserApplication2(8082)使用的profile是test。 启动UserApplication和UserApplication2 访问http://localhost:8081/user/prop,结果: ![image-20210714174313344](assets/image-20210714174313344.png) 访问http://localhost:8082/user/prop,结果: ![image-20210714174424818](assets/image-20210714174424818.png) 可以看出来,不管是dev,还是test环境,都读取到了envSharedValue这个属性的值。 ### 4)配置共享的优先级 当nacos、服务本地同时出现相同属性时,优先级有高低之分: ![image-20210714174623557](assets/image-20210714174623557.png) # Nacos集群搭建 ## 1.集群结构图 官方给出的Nacos集群图: ![image-20210409210621117](assets/image-20210409210621117.png) 其中包含3个nacos节点,然后一个负载均衡器代理3个Nacos。这里负载均衡器可以使用nginx。 我们计划的集群结构: ![image-20210409211355037](assets/image-20210409211355037.png) 三个nacos节点的地址: | 节点 | ip | port | | ------ | ------------- | ---- | | nacos1 | 192.168.150.1 | 8845 | | nacos2 | 192.168.150.1 | 8846 | | nacos3 | 192.168.150.1 | 8847 | ## 2.搭建集群 搭建集群的基本步骤: - 搭建数据库,初始化数据库表结构 - 下载nacos安装包 - 配置nacos - 启动nacos集群 - nginx反向代理 ## 2.1.初始化数据库 Nacos默认数据存储在内嵌数据库Derby中,不属于生产可用的数据库。 官方推荐的最佳实践是使用带有主从的高可用数据库集群 这里我们以单点的数据库为例: 首先新建一个数据库,命名为nacos,而后导入下面的SQL: ```sql CREATE TABLE `config_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(255) DEFAULT NULL, `content` longtext NOT NULL COMMENT 'content', `md5` varchar(32) DEFAULT NULL COMMENT 'md5', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', `src_user` text COMMENT 'source user', `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', `app_name` varchar(128) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', `c_desc` varchar(256) DEFAULT NULL, `c_use` varchar(64) DEFAULT NULL, `effect` varchar(64) DEFAULT NULL, `type` varchar(64) DEFAULT NULL, `c_schema` text, PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info_aggr */ /******************************************/ CREATE TABLE `config_info_aggr` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(255) NOT NULL COMMENT 'group_id', `datum_id` varchar(255) NOT NULL COMMENT 'datum_id', `content` longtext NOT NULL COMMENT '内容', `gmt_modified` datetime NOT NULL COMMENT '修改时间', `app_name` varchar(128) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info_beta */ /******************************************/ CREATE TABLE `config_info_beta` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(128) NOT NULL COMMENT 'group_id', `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', `content` longtext NOT NULL COMMENT 'content', `beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps', `md5` varchar(32) DEFAULT NULL COMMENT 'md5', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', `src_user` text COMMENT 'source user', `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info_tag */ /******************************************/ CREATE TABLE `config_info_tag` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(128) NOT NULL COMMENT 'group_id', `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', `tag_id` varchar(128) NOT NULL COMMENT 'tag_id', `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', `content` longtext NOT NULL COMMENT 'content', `md5` varchar(32) DEFAULT NULL COMMENT 'md5', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', `src_user` text COMMENT 'source user', `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_tags_relation */ /******************************************/ CREATE TABLE `config_tags_relation` ( `id` bigint(20) NOT NULL COMMENT 'id', `tag_name` varchar(128) NOT NULL COMMENT 'tag_name', `tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(128) NOT NULL COMMENT 'group_id', `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', `nid` bigint(20) NOT NULL AUTO_INCREMENT, PRIMARY KEY (`nid`), UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`), KEY `idx_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = group_capacity */ /******************************************/ CREATE TABLE `group_capacity` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群', `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值', `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_group_id` (`group_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = his_config_info */ /******************************************/ CREATE TABLE `his_config_info` ( `id` bigint(64) unsigned NOT NULL, `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `data_id` varchar(255) NOT NULL, `group_id` varchar(128) NOT NULL, `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', `content` longtext NOT NULL, `md5` varchar(32) DEFAULT NULL, `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `src_user` text, `src_ip` varchar(50) DEFAULT NULL, `op_type` char(10) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', PRIMARY KEY (`nid`), KEY `idx_gmt_create` (`gmt_create`), KEY `idx_gmt_modified` (`gmt_modified`), KEY `idx_did` (`data_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = tenant_capacity */ /******************************************/ CREATE TABLE `tenant_capacity` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID', `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数', `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表'; CREATE TABLE `tenant_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `kp` varchar(128) NOT NULL COMMENT 'kp', `tenant_id` varchar(128) default '' COMMENT 'tenant_id', `tenant_name` varchar(128) default '' COMMENT 'tenant_name', `tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc', `create_source` varchar(32) DEFAULT NULL COMMENT 'create_source', `gmt_create` bigint(20) NOT NULL COMMENT '创建时间', `gmt_modified` bigint(20) NOT NULL COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`), KEY `idx_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info'; CREATE TABLE `users` ( `username` varchar(50) NOT NULL PRIMARY KEY, `password` varchar(500) NOT NULL, `enabled` boolean NOT NULL ); CREATE TABLE `roles` ( `username` varchar(50) NOT NULL, `role` varchar(50) NOT NULL, UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE ); CREATE TABLE `permissions` ( `role` varchar(50) NOT NULL, `resource` varchar(255) NOT NULL, `action` varchar(8) NOT NULL, UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE ); INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE); INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN'); ``` ## 2.2.下载nacos nacos在GitHub上有下载地址:https://github.com/alibaba/nacos/tags,可以选择任意版本下载。 本例中才用1.4.1版本: ![image-20210409212119411](assets/image-20210409212119411.png) ## 2.3.配置Nacos 将这个包解压到任意非中文目录下,如图: ![image-20210402161843337](assets/image-20210402161843337.png) 目录说明: - bin:启动脚本 - conf:配置文件 进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf: ![image-20210409212459292](assets/image-20210409212459292.png) 然后添加内容: ``` 127.0.0.1:8845 127.0.0.1.8846 127.0.0.1.8847 ``` 然后修改application.properties文件,添加数据库配置 ```properties spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC db.user.0=root db.password.0=123 ``` ## 2.4.启动 将nacos文件夹复制三份,分别命名为:nacos1、nacos2、nacos3 ![image-20210409213335538](assets/image-20210409213335538.png) 然后分别修改三个文件夹中的application.properties, nacos1: ```properties server.port=8845 ``` nacos2: ```properties server.port=8846 ``` nacos3: ```properties server.port=8847 ``` 然后分别启动三个nacos节点: ``` startup.cmd ``` ## 2.5.nginx反向代理 下载nginx安装包,解压到任意非中文目录下: ![image-20210410103322874](assets/image-20210410103322874.png) 修改conf/nginx.conf文件,配置如下: ```nginx upstream nacos-cluster { server 127.0.0.1:8845; server 127.0.0.1:8846; server 127.0.0.1:8847; } server { listen 80; server_name localhost; location /nacos { proxy_pass http://nacos-cluster; } } ``` 而后在浏览器访问:http://localhost/nacos即可。 代码中application.yml文件配置如下: ```yaml spring: cloud: nacos: server-addr: localhost:80 # Nacos地址 ``` ## 2.6.优化 - 实际部署时,需要给做反向代理的nginx服务器设置一个域名,这样后续如果有服务器迁移nacos的客户端也无需更改配置. - Nacos的各个节点应该部署到多个不同服务器,做好容灾和隔离 # 2.Feign远程调用 先来看我们以前利用RestTemplate发起远程调用的代码: ![image-20210714174814204](assets/image-20210714174814204.png) 存在下面的问题: •代码可读性差,编程体验不统一 •参数复杂URL难以维护 Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign 其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。 ![image-20210714174918088](assets/image-20210714174918088.png) ## 2.1.Feign替代RestTemplate Fegin的使用步骤如下: ### 1)引入依赖 我们在order-service服务的pom文件中引入feign的依赖: ```xml org.springframework.cloud spring-cloud-starter-openfeign ``` ### 2)添加注解 在order-service的启动类添加注解开启Feign的功能: ![image-20210714175102524](assets/image-20210714175102524.png) ### 3)编写Feign的客户端 在order-service中新建一个接口,内容如下: ```java package cn.itcast.order.client; import cn.itcast.order.pojo.User; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient("userservice") public interface UserClient { @GetMapping("/user/{id}") User findById(@PathVariable("id") Long id); } ``` 这个客户端主要是基于SpringMVC的注解来声明远程调用的信息,比如: - 服务名称:userservice - 请求方式:GET - 请求路径:/user/{id} - 请求参数:Long id - 返回值类型:User 这样,Feign就可以帮助我们发送http请求,无需自己使用RestTemplate来发送了。 ### 4)测试 修改order-service中的OrderService类中的queryOrderById方法,使用Feign客户端代替RestTemplate: ![image-20210714175415087](assets/image-20210714175415087.png) 是不是看起来优雅多了。 ### 5)总结 使用Feign的步骤: ① 引入依赖 ② 添加@EnableFeignClients注解 ③ 编写FeignClient接口 ④ 使用FeignClient中定义的方法代替RestTemplate ## 2.2.自定义配置 Feign可以支持很多的自定义配置,如下表所示: | 类型 | 作用 | 说明 | | ---------------------- | ---------------- | ------------------------------------------------------ | | **feign.Logger.Level** | 修改日志级别 | 包含四种不同的级别:NONE、BASIC、HEADERS、FULL | | feign.codec.Decoder | 响应结果的解析器 | http远程调用的结果做解析,例如解析json字符串为java对象 | | feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过http请求发送 | | feign. Contract | 支持的注解格式 | 默认是SpringMVC的注解 | | feign. Retryer | 失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 | 一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可。 下面以日志为例来演示如何自定义配置。 ### 2.2.1.配置文件方式 基于配置文件修改feign的日志级别可以针对单个服务: ```yaml feign: client: config: userservice: # 针对某个微服务的配置 loggerLevel: FULL # 日志级别 ``` 也可以针对所有服务: ```yaml feign: client: config: default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置 loggerLevel: FULL # 日志级别 ``` 而日志的级别分为四种: - NONE:不记录任何日志信息,这是默认值。 - BASIC:仅记录请求的方法,URL以及响应状态码和执行时间 - HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息 - FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。 ### 2.2.2.Java代码方式 也可以基于Java代码来修改日志级别,先声明一个类,然后声明一个Logger.Level的对象: ```java public class DefaultFeignConfiguration { @Bean public Logger.Level feignLogLevel(){ return Logger.Level.BASIC; // 日志级别为BASIC } } ``` 如果要**全局生效**,将其放到启动类的@EnableFeignClients这个注解中: ```java @EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class) ``` 如果是**局部生效**,则把它放到对应的@FeignClient这个注解中: ```java @FeignClient(value = "userservice", configuration = DefaultFeignConfiguration .class) ``` ## 2.3.Feign使用优化 Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括: •URLConnection:默认实现,不支持连接池 •Apache HttpClient :支持连接池 •OKHttp:支持连接池 因此提高Feign的性能主要手段就是使用**连接池**代替默认的URLConnection。 这里我们用Apache的HttpClient来演示。 1)引入依赖 在order-service的pom文件中引入Apache的HttpClient依赖: ```xml io.github.openfeign feign-httpclient ``` 2)配置连接池 在order-service的application.yml中添加配置: ```yaml feign: client: config: default: # default全局的配置 loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息 httpclient: enabled: true # 开启feign对HttpClient的支持 max-connections: 200 # 最大的连接数 max-connections-per-route: 50 # 每个路径的最大连接数 ``` 接下来,在FeignClientFactoryBean中的loadBalance方法中打断点: ![image-20210714185925910](assets/image-20210714185925910.png) Debug方式启动order-service服务,可以看到这里的client,底层就是Apache HttpClient: ![image-20210714190041542](assets/image-20210714190041542.png) 总结,Feign的优化: 1.日志级别尽量用basic 2.使用HttpClient或OKHttp代替URLConnection ① 引入feign-httpClient依赖 ② 配置文件开启httpClient功能,设置连接池参数 ## 2.4.最佳实践 所谓最近实践,就是使用过程中总结的经验,最好的一种使用方式。 自习观察可以发现,Feign的客户端与服务提供者的controller代码非常相似: feign客户端: ![image-20210714190542730](assets/image-20210714190542730.png) UserController: ![image-20210714190528450](assets/image-20210714190528450.png) 有没有一种办法简化这种重复的代码编写呢? ### 2.4.1.继承方式 一样的代码可以通过继承来共享: 1)定义一个API接口,利用定义方法,并基于SpringMVC注解做声明。 2)Feign客户端和Controller都集成改接口 ![image-20210714190640857](assets/image-20210714190640857.png) 优点: - 简单 - 实现了代码共享 缺点: - 服务提供方、服务消费方紧耦合 - 参数列表中的注解映射并不会继承,因此Controller中必须再次声明方法、参数列表、注解 ### 2.4.2.抽取方式 将Feign的Client抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用。 例如,将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用。 ![image-20210714214041796](assets/image-20210714214041796.png) ### 2.4.3.实现基于抽取的最佳实践 #### 1)抽取 首先创建一个module,命名为feign-api: ![image-20210714204557771](assets/image-20210714204557771.png) 项目结构: ![image-20210714204656214](assets/image-20210714204656214.png) 在feign-api中然后引入feign的starter依赖 ```xml org.springframework.cloud spring-cloud-starter-openfeign ``` 然后,order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中 ![image-20210714205221970](assets/image-20210714205221970.png) #### 2)在order-service中使用feign-api 首先,删除order-service中的UserClient、User、DefaultFeignConfiguration等类或接口。 在order-service的pom文件中中引入feign-api的依赖: ```xml cn.itcast.demo feign-api 1.0 ``` 修改order-service中的所有与上述三个组件有关的导包部分,改成导入feign-api中的包 #### 3)重启测试 重启后,发现服务报错了: ![image-20210714205623048](assets/image-20210714205623048.png) 这是因为UserClient现在在cn.itcast.feign.clients包下, 而order-service的@EnableFeignClients注解是在cn.itcast.order包下,不在同一个包,无法扫描到UserClient。 #### 4)解决扫描包问题 方式一: 指定Feign应该扫描的包: ```java @EnableFeignClients(basePackages = "cn.itcast.feign.clients") ``` 方式二: 指定需要加载的Client接口: ```java @EnableFeignClients(clients = {UserClient.class}) ``` # 3.Gateway服务网关 Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。 ## 3.1.为什么需要网关 Gateway网关是我们服务的守门神,所有微服务的统一入口。 网关的**核心功能特性**: - 请求路由 - 权限控制 - 限流 架构图: ![image-20210714210131152](assets/image-20210714210131152.png) **权限控制**:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。 **路由和负载均衡**:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。 **限流**:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。 在SpringCloud中网关的实现包括两种: - gateway - zuul Zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。 ## 3.2.gateway快速入门 下面,我们就演示下网关的基本路由功能。基本步骤如下: 1. 创建SpringBoot工程gateway,引入网关依赖 2. 编写启动类 3. 编写基础配置和路由规则 4. 启动网关服务进行测试 ### 1)创建gateway服务,引入依赖 创建服务: ![image-20210714210919458](assets/image-20210714210919458.png) 引入依赖: ```xml org.springframework.cloud spring-cloud-starter-gateway com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` ### 2)编写启动类 ```java package cn.itcast.gateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } } ``` ### 3)编写基础配置和路由规则 创建application.yml文件,内容如下: ```yaml server: port: 10010 # 网关端口 spring: application: name: gateway # 服务名称 cloud: nacos: server-addr: localhost:8848 # nacos地址 gateway: routes: # 网关路由配置 - id: user-service # 路由id,自定义,只要唯一即可 # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址 uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称 predicates: # 路由断言,也就是判断请求是否符合路由规则的条件 - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求 ``` 我们将符合`Path` 规则的一切请求,都代理到 `uri`参数指定的地址。 本例中,我们将 `/user/**`开头的请求,代理到`lb://userservice`,lb是负载均衡,根据服务名拉取服务列表,实现负载均衡。 ### 4)重启测试 重启网关,访问http://localhost:10010/user/1时,符合`/user/**`规则,请求转发到uri:http://userservice/user/1,得到了结果: ![image-20210714211908341](assets/image-20210714211908341.png) ### 5)网关路由的流程图 整个访问的流程如下: ![image-20210714211742956](assets/image-20210714211742956.png) 总结: 网关搭建步骤: 1. 创建项目,引入nacos服务发现和gateway依赖 2. 配置application.yml,包括服务基本信息、nacos地址、路由 路由配置包括: 1. 路由id:路由的唯一标示 2. 路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡 3. 路由断言(predicates):判断路由的规则, 4. 路由过滤器(filters):对请求或响应做处理 接下来,就重点来学习路由断言和路由过滤器的详细知识 ## 3.3.断言工厂 我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件 例如Path=/user/**是按照路径匹配,这个规则是由 `org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory`类来 处理的,像这样的断言工厂在SpringCloudGateway还有十几个: | **名称** | **说明** | **示例** | | ---------- | ------------------------------ | ------------------------------------------------------------ | | After | 是某个时间点后的请求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] | | Before | 是某个时间点之前的请求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] | | Between | 是某两个时间点之前的请求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] | | Cookie | 请求必须包含某些cookie | - Cookie=chocolate, ch.p | | Header | 请求必须包含某些header | - Header=X-Request-Id, \d+ | | Host | 请求必须是访问某个host(域名) | - Host=**.somehost.org,**.anotherhost.org | | Method | 请求方式必须是指定方式 | - Method=GET,POST | | Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/blue/** | | Query | 请求参数必须包含指定参数 | - Query=name, Jack或者- Query=name | | RemoteAddr | 请求者的ip必须是指定范围 | - RemoteAddr=192.168.1.1/24 | | Weight | 权重处理 | | 我们只需要掌握Path这种路由工程就可以了。 ## 3.4.过滤器工厂 GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理: ![image-20210714212312871](assets/image-20210714212312871.png) ### 3.4.1.路由过滤器的种类 Spring提供了31种不同的路由过滤器工厂。例如: | **名称** | **说明** | | -------------------- | ---------------------------- | | AddRequestHeader | 给当前请求添加一个请求头 | | RemoveRequestHeader | 移除请求中的一个请求头 | | AddResponseHeader | 给响应结果中添加一个响应头 | | RemoveResponseHeader | 从响应结果中移除有一个响应头 | | RequestRateLimiter | 限制请求的流量 | ### 3.4.2.请求头过滤器 下面我们以AddRequestHeader 为例来讲解。 > **需求**:给所有进入userservice的请求添加一个请求头:Truth=itcast is freaking awesome! 只需要修改gateway服务的application.yml文件,添加路由过滤即可: ```yaml spring: cloud: gateway: routes: - id: user-service uri: lb://userservice predicates: - Path=/user/** filters: # 过滤器 - AddRequestHeader=Truth, Itcast is freaking awesome! # 添加请求头 ``` 当前过滤器写在userservice路由下,因此仅仅对访问userservice的请求有效。 ### 3.4.3.默认过滤器 如果要对所有的路由都生效,则可以将过滤器工厂写到default下。格式如下: ```yaml spring: cloud: gateway: routes: - id: user-service uri: lb://userservice predicates: - Path=/user/** default-filters: # 默认过滤项 - AddRequestHeader=Truth, Itcast is freaking awesome! ``` ### 3.4.4.总结 过滤器的作用是什么? ① 对路由的请求或响应做加工处理,比如添加请求头 ② 配置在路由下的过滤器只对当前路由的请求生效 defaultFilters的作用是什么? ① 对所有路由都生效的过滤器 ## 3.5.全局过滤器 网关提供了31种,但每一种过滤器的作用都是固定的。如果我们希望拦截请求,做自己的业务逻辑则没办法实现。 ### 3.5.1.全局过滤器作用 全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。 定义方式是实现GlobalFilter接口。 ```java public interface GlobalFilter { /** * 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理 * * @param exchange 请求上下文,里面可以获取Request、Response等信息 * @param chain 用来把请求委托给下一个过滤器 * @return {@code Mono} 返回标示当前过滤器业务结束 */ Mono filter(ServerWebExchange exchange, GatewayFilterChain chain); } ``` 在filter中编写自定义逻辑,可以实现下列功能: - 登录状态判断 - 权限校验 - 请求限流等 ### 3.5.2.自定义全局过滤器 需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件: - 参数中是否有authorization, - authorization参数值是否为admin 如果同时满足则放行,否则拦截 实现: 在gateway中定义一个过滤器: ```java package cn.itcast.gateway.filters; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Order(-1) @Component public class AuthorizeFilter implements GlobalFilter { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 1.获取请求参数 MultiValueMap params = exchange.getRequest().getQueryParams(); // 2.获取authorization参数 String auth = params.getFirst("authorization"); // 3.校验 if ("admin".equals(auth)) { // 放行 return chain.filter(exchange); } // 4.拦截 // 4.1.禁止访问,设置状态码 exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); // 4.2.结束处理 return exchange.getResponse().setComplete(); } } ``` ### 3.5.3.过滤器执行顺序 请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter 请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器: ![image-20210714214228409](assets/image-20210714214228409.png) 排序的规则是什么呢? - 每一个过滤器都必须指定一个int类型的order值,**order值越小,优先级越高,执行顺序越靠前**。 - GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定 - 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。 - 当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。 详细内容,可以查看源码: `org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()`方法是先加载defaultFilters,然后再加载某个route的filters,然后合并。 `org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()`方法会加载全局过滤器,与前面的过滤器合并后根据order排序,组织过滤器链 ## 3.6.跨域问题 ### 3.6.1.什么是跨域问题 跨域:域名不一致就是跨域,主要包括: - 域名不同: www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com - 域名相同,端口不同:localhost:8080和localhost8081 跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题 解决方案:CORS,解释查看https://www.ruanyifeng.com/blog/2016/04/cors.html ### 3.6.2.模拟跨域问题 html代码: ``` Document
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 # 这次跨域检测的有效期
``` 放入tomcat或者nginx这样的web服务器中,启动并访问。 可以在浏览器控制台看到下面的错误: ![image-20210714215832675](assets/image-20210714215832675.png) 从localhost:8090访问localhost:10010,端口不同,显然是跨域的请求。 ### 3.6.3.解决跨域问题 在gateway服务的application.yml文件中,添加下面的配置: ```yaml spring: cloud: gateway: # 。。。 globalcors: # 全局的跨域处理 add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题 corsConfigurations: '[/**]': allowedOrigins: # 允许哪些网站的跨域请求 - "http://localhost:8090" allowedMethods: # 允许的跨域ajax的请求方式 - "GET" - "POST" - "DELETE" - "PUT" - "OPTIONS" allowedHeaders: "*" # 允许在请求中携带的头信息 allowCredentials: true # 是否允许携带cookie maxAge: 360000 # 这次跨域检测的有效期 ``` # 1.初识Docker ## 1.1.什么是Docker 微服务虽然具备各种各样的优势,但服务的拆分通用给部署带来了很大的麻烦。 - 分布式系统中,依赖的组件非常多,不同组件之间部署时往往会产生一些冲突。 - 在数百上千台服务中重复部署,环境不一定一致,会遇到各种问题 ### 1.1.1.应用部署的环境问题 大型项目组件较多,运行环境也较为复杂,部署时会碰到一些问题: - 依赖关系复杂,容易出现兼容性问题 - 开发、测试、生产环境有差异 ![image-20210731141907366](assets/image-20210731141907366.png) 例如一个项目中,部署时需要依赖于node.js、Redis、RabbitMQ、MySQL等,这些服务部署时所需要的函数库、依赖项各不相同,甚至会有冲突。给部署带来了极大的困难。 ### 1.1.2.Docker解决依赖兼容问题 而Docker确巧妙的解决了这些问题,Docker是如何实现的呢? Docker为了解决依赖的兼容问题的,采用了两个手段: - 将应用的Libs(函数库)、Deps(依赖)、配置与应用一起打包 - 将每个应用放到一个隔离**容器**去运行,避免互相干扰 ![image-20210731142219735](assets/image-20210731142219735.png) 这样打包好的应用包中,既包含应用本身,也保护应用所需要的Libs、Deps,无需再操作系统上安装这些,自然就不存在不同应用之间的兼容问题了。 虽然解决了不同应用的兼容问题,但是开发、测试等环境会存在差异,操作系统版本也会有差异,怎么解决这些问题呢? ### 1.1.3.Docker解决操作系统环境差异 要解决不同操作系统环境差异问题,必须先了解操作系统结构。以一个Ubuntu操作系统为例,结构如下: ![image-20210731143401460](assets/image-20210731143401460.png) 结构包括: - 计算机硬件:例如CPU、内存、磁盘等 - 系统内核:所有Linux发行版的内核都是Linux,例如CentOS、Ubuntu、Fedora等。内核可以与计算机硬件交互,对外提供**内核指令**,用于操作计算机硬件。 - 系统应用:操作系统本身提供的应用、函数库。这些函数库是对内核指令的封装,使用更加方便。 应用于计算机交互的流程如下: 1)应用调用操作系统应用(函数库),实现各种功能 2)系统函数库是对内核指令集的封装,会调用内核指令 3)内核指令操作计算机硬件 Ubuntu和CentOSpringBoot都是基于Linux内核,无非是系统应用不同,提供的函数库有差异: ![image-20210731144304990](assets/image-20210731144304990.png) 此时,如果将一个Ubuntu版本的MySQL应用安装到CentOS系统,MySQL在调用Ubuntu函数库时,会发现找不到或者不匹配,就会报错了: ![image-20210731144458680](assets/image-20210731144458680.png) Docker如何解决不同系统环境的问题? - Docker将用户程序与所需要调用的系统(比如Ubuntu)函数库一起打包 - Docker运行到不同操作系统时,直接基于打包的函数库,借助于操作系统的Linux内核来运行 如图: ![image-20210731144820638](assets/image-20210731144820638.png) ### 1.1.4.小结 Docker如何解决大型项目依赖关系复杂,不同组件依赖的兼容性问题? - Docker允许开发中将应用、依赖、函数库、配置一起**打包**,形成可移植镜像 - Docker应用运行在容器中,使用沙箱机制,相互**隔离** Docker如何解决开发、测试、生产环境有差异的问题? - Docker镜像中包含完整运行环境,包括系统函数库,仅依赖系统的Linux内核,因此可以在任意Linux操作系统上运行 Docker是一个快速交付应用、运行应用的技术,具备下列优势: - 可以将程序及其依赖、运行环境一起打包为一个镜像,可以迁移到任意Linux操作系统 - 运行时利用沙箱机制形成隔离容器,各个应用互不干扰 - 启动、移除都可以通过一行命令完成,方便快捷 ## 1.2.Docker和虚拟机的区别 Docker可以让一个应用在任何操作系统中非常方便的运行。而以前我们接触的虚拟机,也能在一个操作系统中,运行另外一个操作系统,保护系统中的任何应用。 两者有什么差异呢? **虚拟机**(virtual machine)是在操作系统中**模拟**硬件设备,然后运行另一个操作系统,比如在 Windows 系统里面运行 Ubuntu 系统,这样就可以运行任意的Ubuntu应用了。 **Docker**仅仅是封装函数库,并没有模拟完整的操作系统,如图: ![image-20210731145914960](assets/image-20210731145914960.png) 对比来看: ![image-20210731152243765](assets/image-20210731152243765.png) 小结: Docker和虚拟机的差异: - docker是一个系统进程;虚拟机是在操作系统中的操作系统 - docker体积小、启动速度快、性能好;虚拟机体积大、启动速度慢、性能一般 ## 1.3.Docker架构 ### 1.3.1.镜像和容器 Docker中有几个重要的概念: **镜像(Image)**:Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像。 **容器(Container)**:镜像中的应用程序运行后形成的进程就是**容器**,只是Docker会给容器进程做隔离,对外不可见。 一切应用最终都是代码组成,都是硬盘中的一个个的字节形成的**文件**。只有运行时,才会加载到内存,形成进程。 而**镜像**,就是把一个应用在硬盘上的文件、及其运行环境、部分系统函数库文件一起打包形成的文件包。这个文件包是只读的。 **容器**呢,就是将这些文件中编写的程序、函数加载到内存中允许,形成进程,只不过要隔离起来。因此一个镜像可以启动多次,形成多个容器进程。 ![image-20210731153059464](assets/image-20210731153059464.png) 例如你下载了一个QQ,如果我们将QQ在磁盘上的运行**文件**及其运行的操作系统依赖打包,形成QQ镜像。然后你可以启动多次,双开、甚至三开QQ,跟多个妹子聊天。 ### 1.3.2.DockerHub 开源应用程序非常多,打包这些应用往往是重复的劳动。为了避免这些重复劳动,人们就会将自己打包的应用镜像,例如Redis、MySQL镜像放到网络上,共享使用,就像GitHub的代码共享一样。 - DockerHub:DockerHub是一个官方的Docker镜像的托管平台。这样的平台称为Docker Registry。 - 国内也有类似于DockerHub 的公开服务,比如 [网易云镜像服务](https://c.163yun.com/hub)、[阿里云镜像库](https://cr.console.aliyun.com/)等。 我们一方面可以将自己的镜像共享到DockerHub,另一方面也可以从DockerHub拉取镜像: ![image-20210731153743354](assets/image-20210731153743354.png) ### 1.3.3.Docker架构 我们要使用Docker来操作镜像、容器,就必须要安装Docker。 Docker是一个CS架构的程序,由两部分组成: - 服务端(server):Docker守护进程,负责处理Docker指令,管理镜像、容器等 - 客户端(client):通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令。 如图: ![image-20210731154257653](assets/image-20210731154257653.png) ### 1.3.4.小结 镜像: - 将应用程序及其依赖、环境、配置打包在一起 容器: - 镜像运行起来就是容器,一个镜像可以运行多个容器 Docker结构: - 服务端:接收命令或远程请求,操作镜像或容器 - 客户端:发送命令或者请求到Docker服务端 DockerHub: - 一个镜像托管的服务器,类似的还有阿里云镜像服务,统称为DockerRegistry # 安装Docker Docker 分为 CE 和 EE 两大版本。CE 即社区版(免费,支持周期 7 个月),EE 即企业版,强调安全,付费使用,支持周期 24 个月。 Docker CE 分为 `stable` `test` 和 `nightly` 三个更新频道。 官方网站上有各种环境下的 [安装指南](https://docs.docker.com/install/),这里主要介绍 Docker CE 在 CentOS上的安装。 # 1.CentOS安装Docker Docker CE 支持 64 位版本 CentOS 7,并且要求内核版本不低于 3.10, CentOS 7 满足最低内核的要求,所以我们在CentOS 7安装Docker。 ## 1.1.卸载(可选) 如果之前安装过旧版本的Docker,可以使用下面命令卸载: ``` yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-selinux \ docker-engine-selinux \ docker-engine \ docker-ce ``` ## 1.2.安装docker 首先需要虚拟机联网,安装yum工具 ```sh yum install -y yum-utils \ device-mapper-persistent-data \ lvm2 --skip-broken ``` 然后更新本地镜像源: ```shell # 设置docker镜像源 yum-config-manager \ --add-repo \ https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo yum makecache fast ``` 然后输入命令: ```shell yum install -y docker-ce ``` docker-ce为社区免费版本。稍等片刻,docker即可安装成功。 ## 1.3.启动docker Docker应用需要用到各种端口,逐一去修改防火墙设置。非常麻烦,因此建议大家直接关闭防火墙! 启动docker前,一定要关闭防火墙后!! 启动docker前,一定要关闭防火墙后!! 启动docker前,一定要关闭防火墙后!! ```sh # 关闭 systemctl stop firewalld # 禁止开机启动防火墙 systemctl disable firewalld ``` 通过命令启动docker: ```sh systemctl start docker # 启动docker服务 systemctl stop docker # 停止docker服务 systemctl restart docker # 重启docker服务 ``` 然后输入命令,可以查看docker版本: ``` docker -v ``` 如图: ![image-20210418154704436](assets/image-20210418154704436.png) ## 1.4.配置镜像加速 docker官方镜像仓库网速较差,我们需要设置国内镜像服务: 参考阿里云的镜像加速文档:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors # 2.CentOS7安装DockerCompose ## 2.1.下载 Linux下需要通过命令下载: ```sh # 安装 curl -L https://github.com/docker/compose/releases/download/1.23.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose ``` ## 2.2.修改文件权限 修改文件权限: ```sh # 修改权限 chmod +x /usr/local/bin/docker-compose ``` ## 2.3.Base自动补全命令: ```sh # 补全命令 curl -L https://raw.githubusercontent.com/docker/compose/1.29.1/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose ``` 如果这里出现错误,需要修改自己的hosts文件: ```sh echo "199.232.68.133 raw.githubusercontent.com" >> /etc/hosts ``` # 3.Docker镜像仓库 搭建镜像仓库可以基于Docker官方提供的DockerRegistry来实现。 官网地址:https://hub.docker.com/_/registry ## 3.1.简化版镜像仓库 Docker官方的Docker Registry是一个基础版本的Docker镜像仓库,具备仓库管理的完整功能,但是没有图形化界面。 搭建方式比较简单,命令如下: ```sh docker run -d \ --restart=always \ --name registry \ -p 5000:5000 \ -v registry-data:/var/lib/registry \ registry ``` 命令中挂载了一个数据卷registry-data到容器内的/var/lib/registry 目录,这是私有镜像库存放数据的目录。 访问http://YourIp:5000/v2/_catalog 可以查看当前私有镜像服务中包含的镜像 ## 3.2.带有图形化界面版本 使用DockerCompose部署带有图象界面的DockerRegistry,命令如下: ```yaml version: '3.0' services: registry: image: registry volumes: - ./registry-data:/var/lib/registry ui: image: joxit/docker-registry-ui:static ports: - 8080:80 environment: - REGISTRY_TITLE=传智教育私有仓库 - REGISTRY_URL=http://registry:5000 depends_on: - registry ``` ## 3.3.配置Docker信任地址 我们的私服采用的是http协议,默认不被Docker信任,所以需要做一个配置: ```sh # 打开要修改的文件 vi /etc/docker/daemon.json # 添加内容: "insecure-registries":["http://192.168.150.101:8080"] # 重加载 systemctl daemon-reload # 重启docker systemctl restart docker ``` # 2.Docker的基本操作 ## 2.1.镜像操作 ### 2.1.1.镜像名称 首先来看下镜像的名称组成: - 镜名称一般分两部分组成:[repository]:[tag]。 - 在没有指定tag时,默认是latest,代表最新版本的镜像 如图: ![image-20210731155141362](assets/image-20210731155141362.png) 这里的mysql就是repository,5.7就是tag,合一起就是镜像名称,代表5.7版本的MySQL镜像。 ### 2.1.2.镜像命令 常见的镜像操作命令如图: ![image-20210731155649535](assets/image-20210731155649535.png) ### 2.1.3.案例1-拉取、查看镜像 需求:从DockerHub中拉取一个nginx镜像并查看 1)首先去镜像仓库搜索nginx镜像,比如[DockerHub](https://hub.docker.com/): ![image-20210731155844368](assets/image-20210731155844368.png) 2)根据查看到的镜像名称,拉取自己需要的镜像,通过命令:docker pull nginx ![image-20210731155856199](assets/image-20210731155856199.png) 3)通过命令:docker images 查看拉取到的镜像 ![image-20210731155903037](assets/image-20210731155903037.png) ### 2.1.4.案例2-保存、导入镜像 需求:利用docker save将nginx镜像导出磁盘,然后再通过load加载回来 1)利用docker xx --help命令查看docker save和docker load的语法 例如,查看save命令用法,可以输入命令: ```sh docker save --help ``` 结果: ![image-20210731161104732](assets/image-20210731161104732.png) 命令格式: ```shell docker save -o [保存的目标文件名称] [镜像名称] ``` 2)使用docker save导出镜像到磁盘 运行命令: ```sh docker save -o nginx.tar nginx:latest ``` 结果如图: ![image-20210731161354344](assets/image-20210731161354344.png) 3)使用docker load加载镜像 先删除本地的nginx镜像: ```sh docker rmi nginx:latest ``` 然后运行命令,加载本地文件: ```sh docker load -i nginx.tar ``` 结果: ![image-20210731161746245](assets/image-20210731161746245.png) ### 2.1.5.练习 需求:去DockerHub搜索并拉取一个Redis镜像 目标: 1)去DockerHub搜索Redis镜像 2)查看Redis镜像的名称和版本 3)利用docker pull命令拉取镜像 4)利用docker save命令将 redis:latest打包为一个redis.tar包 5)利用docker rmi 删除本地的redis:latest 6)利用docker load 重新加载 redis.tar文件 ## 2.2.容器操作 ### 2.2.1.容器相关命令 容器操作的命令如图: ![image-20210731161950495](assets/image-20210731161950495.png) 容器保护三个状态: - 运行:进程正常运行 - 暂停:进程暂停,CPU不再运行,并不释放内存 - 停止:进程终止,回收进程占用的内存、CPU等资源 其中: - docker run:创建并运行一个容器,处于运行状态 - docker pause:让一个运行的容器暂停 - docker unpause:让一个容器从暂停状态恢复运行 - docker stop:停止一个运行的容器 - docker start:让一个停止的容器再次运行 - docker rm:删除一个容器 ### 2.2.2.案例-创建并运行一个容器 创建并运行nginx容器的命令: ```sh docker run --name containerName -p 80:80 -d nginx ``` 命令解读: - docker run :创建并运行一个容器 - --name : 给容器起一个名字,比如叫做mn - -p :将宿主机端口与容器端口映射,冒号左侧是宿主机端口,右侧是容器端口 - -d:后台运行容器 - nginx:镜像名称,例如nginx 这里的`-p`参数,是将容器端口映射到宿主机端口。 默认情况下,容器是隔离环境,我们直接访问宿主机的80端口,肯定访问不到容器中的nginx。 现在,将容器的80与宿主机的80关联起来,当我们访问宿主机的80端口时,就会被映射到容器的80,这样就能访问到nginx了: ![image-20210731163255863](assets/image-20210731163255863.png) ### 2.2.3.案例-进入容器,修改文件 **需求**:进入Nginx容器,修改HTML文件内容,添加“传智教育欢迎您” **提示**:进入容器要用到docker exec命令。 **步骤**: 1)进入容器。进入我们刚刚创建的nginx容器的命令为: ```sh docker exec -it mn bash ``` 命令解读: - docker exec :进入容器内部,执行一个命令 - -it : 给当前进入的容器创建一个标准输入、输出终端,允许我们与容器交互 - mn :要进入的容器的名称 - bash:进入容器后执行的命令,bash是一个linux终端交互命令 2)进入nginx的HTML所在目录 /usr/share/nginx/html 容器内部会模拟一个独立的Linux文件系统,看起来如同一个linux服务器一样: ![image-20210731164159811](assets/image-20210731164159811.png) nginx的环境、配置、运行文件全部都在这个文件系统中,包括我们要修改的html文件。 查看DockerHub网站中的nginx页面,可以知道nginx的html目录位置在`/usr/share/nginx/html` 我们执行命令,进入该目录: ```sh cd /usr/share/nginx/html ``` 查看目录下文件: ![image-20210731164455818](assets/image-20210731164455818.png) 3)修改index.html的内容 容器内没有vi命令,无法直接修改,我们用下面的命令来修改: ```sh sed -i -e 's#Welcome to nginx#传智教育欢迎您#g' -e 's###g' index.html ``` 在浏览器访问自己的虚拟机地址,例如我的是:http://192.168.150.101,即可看到结果: ![image-20210731164717604](assets/image-20210731164717604.png) ### 2.2.4.小结 docker run命令的常见参数有哪些? - --name:指定容器名称 - -p:指定端口映射 - -d:让容器后台运行 查看容器日志的命令: - docker logs - 添加 -f 参数可以持续查看日志 查看容器状态: - docker ps - docker ps -a 查看所有容器,包括已经停止的 ## 2.3.数据卷(容器数据管理) 在之前的nginx案例中,修改nginx的html页面时,需要进入nginx内部。并且因为没有编辑器,修改文件也很麻烦。 这就是因为容器与数据(容器内文件)耦合带来的后果。 ![image-20210731172440275](assets/image-20210731172440275.png) 要解决这个问题,必须将数据与容器解耦,这就要用到数据卷了。 ### 2.3.1.什么是数据卷 **数据卷(volume)**是一个虚拟目录,指向宿主机文件系统中的某个目录。 ![image-20210731173541846](assets/image-20210731173541846.png) 一旦完成数据卷挂载,对容器的一切操作都会作用在数据卷对应的宿主机目录了。 这样,我们操作宿主机的/var/lib/docker/volumes/html目录,就等于操作容器内的/usr/share/nginx/html目录了 ### 2.3.2.数据集操作命令 数据卷操作的基本语法如下: ```sh docker volume [COMMAND] ``` docker volume命令是数据卷操作,根据命令后跟随的command来确定下一步的操作: - create 创建一个volume - inspect 显示一个或多个volume的信息 - ls 列出所有的volume - prune 删除未使用的volume - rm 删除一个或多个指定的volume ### 2.3.3.创建和查看数据卷 **需求**:创建一个数据卷,并查看数据卷在宿主机的目录位置 ① 创建数据卷 ```sh docker volume create html ``` ② 查看所有数据 ```sh docker volume ls ``` 结果: ![image-20210731173746910](assets/image-20210731173746910.png) ③ 查看数据卷详细信息卷 ```sh docker volume inspect html ``` 结果: ![image-20210731173809877](assets/image-20210731173809877.png) 可以看到,我们创建的html这个数据卷关联的宿主机目录为`/var/lib/docker/volumes/html/_data`目录。 **小结**: 数据卷的作用: - 将容器与数据分离,解耦合,方便操作容器内数据,保证数据安全 数据卷操作: - docker volume create:创建数据卷 - docker volume ls:查看所有数据卷 - docker volume inspect:查看数据卷详细信息,包括关联的宿主机目录位置 - docker volume rm:删除指定数据卷 - docker volume prune:删除所有未使用的数据卷 ### 2.3.4.挂载数据卷 我们在创建容器时,可以通过 -v 参数来挂载一个数据卷到某个容器内目录,命令格式如下: ```sh docker run \ --name mn \ -v html:/root/html \ -p 8080:80 nginx \ ``` 这里的-v就是挂载数据卷的命令: - `-v html:/root/htm` :把html数据卷挂载到容器内的/root/html这个目录中 ### 2.3.5.案例-给nginx挂载数据卷 **需求**:创建一个nginx容器,修改容器内的html目录内的index.html内容 **分析**:上个案例中,我们进入nginx容器内部,已经知道nginx的html目录所在位置/usr/share/nginx/html ,我们需要把这个目录挂载到html这个数据卷上,方便操作其中的内容。 **提示**:运行容器时使用 -v 参数挂载数据卷 步骤: ① 创建容器并挂载数据卷到容器内的HTML目录 ```sh docker run --name mn -v html:/usr/share/nginx/html -p 80:80 -d nginx ``` ② 进入html数据卷所在位置,并修改HTML内容 ```sh # 查看html数据卷的位置 docker volume inspect html # 进入该目录 cd /var/lib/docker/volumes/html/_data # 修改文件 vi index.html ``` ### 2.3.6.案例-给MySQL挂载本地目录 容器不仅仅可以挂载数据卷,也可以直接挂载到宿主机目录上。关联关系如下: - 带数据卷模式:宿主机目录 --> 数据卷 ---> 容器内目录 - 直接挂载模式:宿主机目录 ---> 容器内目录 如图: ![image-20210731175155453](assets/image-20210731175155453.png) **语法**: 目录挂载与数据卷挂载的语法是类似的: - -v [宿主机目录]:[容器内目录] - -v [宿主机文件]:[容器内文件] **需求**:创建并运行一个MySQL容器,将宿主机目录直接挂载到容器 实现思路如下: 1)在将课前资料中的mysql.tar文件上传到虚拟机,通过load命令加载为镜像 2)创建目录/tmp/mysql/data 3)创建目录/tmp/mysql/conf,将课前资料提供的hmy.cnf文件上传到/tmp/mysql/conf 4)去DockerHub查阅资料,创建并运行MySQL容器,要求: ① 挂载/tmp/mysql/data到mysql容器内数据存储目录 ② 挂载/tmp/mysql/conf/hmy.cnf到mysql容器的配置文件 ③ 设置MySQL密码 ### 2.3.7.小结 docker run的命令中通过 -v 参数挂载文件或目录到容器中: - -v volume名称:容器内目录 - -v 宿主机文件:容器内文 - -v 宿主机目录:容器内目录 数据卷挂载与目录直接挂载的 - 数据卷挂载耦合度低,由docker来管理目录,但是目录较深,不好找 - 目录挂载耦合度高,需要我们自己管理目录,不过目录容易寻找查看 # 3.Dockerfile自定义镜像 常见的镜像在DockerHub就能找到,但是我们自己写的项目就必须自己构建镜像了。 而要自定义镜像,就必须先了解镜像的结构才行。 ## 3.1.镜像结构 镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。 我们以MySQL为例,来看看镜像的组成结构: ![image-20210731175806273](assets/image-20210731175806273.png) 简单来说,镜像就是在系统函数库、运行环境基础上,添加应用程序文件、配置文件、依赖文件等组合,然后编写好启动脚本打包在一起形成的文件。 我们要构建镜像,其实就是实现上述打包的过程。 ## 3.2.Dockerfile语法 构建自定义的镜像时,并不需要一个个文件去拷贝,打包。 我们只需要告诉Docker,我们的镜像的组成,需要哪些BaseImage、需要拷贝什么文件、需要安装什么依赖、启动脚本是什么,将来Docker会帮助我们构建镜像。 而描述上述信息的文件就是Dockerfile文件。 **Dockerfile**就是一个文本文件,其中包含一个个的**指令(Instruction)**,用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层Layer。 ![image-20210731180321133](assets/image-20210731180321133.png) 更新详细语法说明,请参考官网文档: https://docs.docker.com/engine/reference/builder ## 3.3.构建Java项目 ### 3.3.1.基于Ubuntu构建Java项目 需求:基于Ubuntu镜像构建一个新镜像,运行一个java项目 - 步骤1:新建一个空文件夹docker-demo ![image-20210801101207444](assets/image-20210801101207444.png) - 步骤2:拷贝课前资料中的docker-demo.jar文件到docker-demo这个目录 ![image-20210801101314816](assets/image-20210801101314816.png) - 步骤3:拷贝课前资料中的jdk8.tar.gz文件到docker-demo这个目录 ![image-20210801101410200](assets/image-20210801101410200.png) - 步骤4:拷贝课前资料提供的Dockerfile到docker-demo这个目录 ![image-20210801101455590](assets/image-20210801101455590.png) 其中的内容如下: ```dockerfile # 指定基础镜像 FROM ubuntu:16.04 # 配置环境变量,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 ``` - 步骤5:进入docker-demo 将准备好的docker-demo上传到虚拟机任意目录,然后进入docker-demo目录下 - 步骤6:运行命令: ```sh docker build -t javaweb:1.0 . ``` 最后访问 http://192.168.150.101:8090/hello/count,其中的ip改成你的虚拟机ip ### 3.3.2.基于java8构建Java项目 虽然我们可以基于Ubuntu基础镜像,添加任意自己需要的安装包,构建镜像,但是却比较麻烦。所以大多数情况下,我们都可以在一些安装了部分软件的基础镜像上做改造。 例如,构建java项目的镜像,可以在已经准备了JDK的基础镜像基础上构建。 需求:基于java:8-alpine镜像,将一个Java项目构建为镜像 实现思路如下: - ① 新建一个空的目录,然后在目录中新建一个文件,命名为Dockerfile - ② 拷贝课前资料提供的docker-demo.jar到这个目录中 - ③ 编写Dockerfile文件: - a )基于java:8-alpine作为基础镜像 - b )将app.jar拷贝到镜像中 - c )暴露端口 - d )编写入口ENTRYPOINT 内容如下: ```dockerfile FROM java:8-alpine COPY ./app.jar /tmp/app.jar EXPOSE 8090 ENTRYPOINT java -jar /tmp/app.jar ``` - ④ 使用docker build命令构建镜像 - ⑤ 使用docker run创建容器并运行 ## 3.4.小结 小结: 1. Dockerfile的本质是一个文件,通过指令描述镜像的构建过程 2. Dockerfile的第一行必须是FROM,从一个基础镜像来构建 3. 基础镜像可以是基本操作系统,如Ubuntu。也可以是其他人制作好的镜像,例如:java:8-alpine # 4.Docker-Compose Docker Compose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器! ![image-20210731180921742](assets/image-20210731180921742.png) ## 4.1.初识DockerCompose Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。格式如下: ```json version: "3.8" services:   mysql:     image: mysql:5.7.25 environment: MYSQL_ROOT_PASSWORD: 123     volumes:      - "/tmp/mysql/data:/var/lib/mysql"      - "/tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf"   web:     build: .     ports:      - "8090:8090" ``` 上面的Compose文件就描述一个项目,其中包含两个容器: - mysql:一个基于`mysql:5.7.25`镜像构建的容器,并且挂载了两个目录 - web:一个基于`docker build`临时构建的镜像容器,映射端口时8090 DockerCompose的详细语法参考官网:https://docs.docker.com/compose/compose-file/ 其实DockerCompose文件可以看做是将多个docker run命令写到一个文件,只是语法稍有差异。 ## 4.2.安装DockerCompose 参考课前资料 ## 4.3.部署微服务集群 **需求**:将之前学习的cloud-demo微服务集群利用DockerCompose部署 **实现思路**: ① 查看课前资料提供的cloud-demo文件夹,里面已经编写好了docker-compose文件 ② 修改自己的cloud-demo项目,将数据库、nacos地址都命名为docker-compose中的服务名 ③ 使用maven打包工具,将项目中的每个微服务都打包为app.jar ④ 将打包好的app.jar拷贝到cloud-demo中的每一个对应的子目录中 ⑤ 将cloud-demo上传至虚拟机,利用 docker-compose up -d 来部署 ### 4.3.1.compose文件 查看课前资料提供的cloud-demo文件夹,里面已经编写好了docker-compose文件,而且每个微服务都准备了一个独立的目录: ![image-20210731181341330](assets/image-20210731181341330.png) 内容如下: ```yaml 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: 123 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: 123`:设置数据库root账户的密码为123 - `volumes`:数据卷挂载,这里挂载了mysql的data、conf目录,其中有我提前准备好的数据 - `userservice`、`orderservice`、`gateway`:都是基于Dockerfile临时构建的 查看mysql目录,可以看到其中已经准备好了cloud_order、cloud_user表: ![image-20210801095205034](assets/image-20210801095205034.png) 查看微服务目录,可以看到都包含Dockerfile文件: ![image-20210801095320586](assets/image-20210801095320586.png) 内容如下: ```dockerfile FROM java:8-alpine COPY ./app.jar /tmp/app.jar ENTRYPOINT java -jar /tmp/app.jar ``` ### 4.3.2.修改微服务配置 因为微服务将来要部署为docker容器,而容器之间互联不是通过IP地址,而是通过容器名。这里我们将order-service、user-service、gateway服务的mysql、nacos地址都修改为基于容器名的访问。 如下所示: ```yaml spring: datasource: url: jdbc:mysql://mysql:3306/cloud_order?useSSL=false username: root password: 123 driver-class-name: com.mysql.jdbc.Driver application: name: orderservice cloud: nacos: server-addr: nacos:8848 # nacos服务地址 ``` ### 4.3.3.打包 接下来需要将我们的每个微服务都打包。因为之前查看到Dockerfile中的jar包名称都是app.jar,因此我们的每个微服务都需要用这个名称。 可以通过修改pom.xml中的打包名称来实现,每个微服务都需要修改: ```xml app org.springframework.boot spring-boot-maven-plugin ``` 打包后: ![image-20210801095951030](assets/image-20210801095951030.png) ### 4.3.4.拷贝jar包到部署目录 编译打包好的app.jar文件,需要放到Dockerfile的同级目录中。注意:每个微服务的app.jar放到与服务名称对应的目录,别搞错了。 user-service: ![image-20210801100201253](assets/image-20210801100201253.png) order-service: ![image-20210801100231495](assets/image-20210801100231495.png) gateway: ![image-20210801100308102](assets/image-20210801100308102.png) ### 4.3.5.部署 最后,我们需要将文件整个cloud-demo文件夹上传到虚拟机中,理由DockerCompose部署。 上传到任意目录: ![image-20210801100955653](assets/image-20210801100955653.png) 部署: 进入cloud-demo目录,然后运行下面的命令: ```sh docker-compose up -d ``` # 5.Docker镜像仓库 ## 5.1.搭建私有镜像仓库 参考课前资料《CentOS7安装Docker.md》 ## 5.2.推送、拉取镜像 推送镜像到私有镜像服务必须先tag,步骤如下: ① 重新tag本地镜像,名称前缀为私有仓库的地址:192.168.150.101:8080/ ```sh docker tag nginx:latest 192.168.150.101:8080/nginx:1.0 ``` ② 推送镜像 ```sh docker push 192.168.150.101:8080/nginx:1.0 ``` ③ 拉取镜像 ```sh docker pull 192.168.150.101:8080/nginx:1.0 ```