微服务
微服务是一种经过良好架构设计的分布式架构方案,微服务架构特征:
- 单一职责:微服务拆分粒度小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
- 面向服务:微服务对外暴露业务接口
- 自治:团队独立、技术独立、数据独立、部署独立
微服务技术对比
微服务这种方案需要技术框架来落地,全球的互联网公司都在积极尝试自己的微服务落地技术。在国内最知名的就是SpringCloud和阿里巴巴的Dubbo。
Dubbo | SpringCloud | SpringCloudAlibaba | |
---|---|---|---|
注册中心 | zookeeper、Redis | Eureka、Consul | Nacos、Eureka |
服务远程调佣 | Dubbo协议 | Feign(HTTP协议) | Dubbo、Feign |
配置中心 | 无 | SpringCloudConfig | SpringCloudConfig、Nacos |
服务网关 | 无 | SpringCloudGateway、Zuul | SpringCloudGateway、Zuul |
服务监控和保护 | dubbo-admin,功能弱 | Hystix | Sentinel |
SpringCloud
- SpringCloud是目前国内使用最广泛的微服务框架。
- 官网
- SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱既用体验
- SpringCloud与SpringBoot的版本兼容关系如下:
服务拆分及远程调用
服务拆分
服务拆分注意事项:
- 不同微服务,不要重复开发相同业务
- 微服务数据独立,不要访问其他微服务的数据库
- 微服务可以将自己的业务暴露为接口,供其他微服务调用
创建工程
-
创建数据库:
CREATE DATABASE cloud-order; USE `cloud-order`; DROP TABLE IF EXISTS `tb_order`; CREATE TABLE `tb_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单id', `user_id` bigint(20) NOT NULL COMMENT '用户id', `name` varchar(100) DEFAULT NULL COMMENT '商品名称', `price` bigint(20) NOT NULL COMMENT '商品价格', `num` int(10) DEFAULT '0' COMMENT '商品数量', PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `username` (`name`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=109 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; insert into `tb_order`(`id`,`user_id`,`name`,`price`,`num`) values (101,1,'Apple 苹果 iPhone 12 ',699900,1),(102,2,'雅迪 yadea 新国标电动车',209900,1),(103,3,'骆驼(CAMEL)休闲运动鞋女',43900,1),(104,4,'小米10 双模5G 骁龙865',359900,1),(105,5,'OPPO Reno3 Pro 双模5G 视频双防抖',299900,1),(106,6,'美的(Midea) 新能效 冷静星II ',544900,1),(107,2,'西昊/SIHOO 人体工学电脑椅子',79900,1),(108,3,'梵班(FAMDBANN)休闲男鞋',31900,1); CREATE DATABASE `cloud-user` ; USE `cloud-user`; DROP TABLE IF EXISTS `tb_user`; CREATE TABLE `tb_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(100) DEFAULT NULL COMMENT '收件人', `address` varchar(255) DEFAULT NULL COMMENT '地址', PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `username` (`username`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=109 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; insert into `tb_user`(`id`,`username`,`address`) values (1,'柳岩','湖南省衡阳市'),(2,'文二狗','陕西省西安市'),(3,'华沉鱼','湖北省十堰市'),(4,'张必沉','天津市'),(5,'郑爽爽','辽宁省沈阳市大东区'),(6,'范兵兵','山东省青岛市');
-
导入项目
远程调用
-
注册RestTemplate:在order-service的OrderApplication中注册RestTemplate (
配置类
)/** * 创建RestTemplate并注入Spring容器 * @return */ @Bean public RestTemplate restTemplate(){ return new RestTemplate(); }
-
注入RestTemplate
@Autowired private RestTemplate restTemplate;
-
发送请求
public Order queryOrderById(Long orderId) { // 1.查询订单 Order order = orderMapper.findById(orderId); // 2.利用RestTemplate发起HTTP请求,查询用户 // 2.1.url路径 String url = "http://localhost:8081/user/"+order.getUserId(); // 2.2.发送HTTP请求,实现远程调用 User user = restTemplate.getForObject(url, User.class); // 3. 封装user到Order order.setUser(user); // 4.返回 return order; }
提供者与消费者
- 服务提供者:一次业务中,被其它微服务调用的服务。(提供接口给其它微服务)
- 服务消费者:一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口)
Eureka注册中心
服务调用出现的问题
- 服务消费者该如何获取提供者的地址信息?
- 服务提供者启动时向Eureka注册自己的信息
- Eureka保持这些信息
- 消费者根据服务名称向Eureka拉取提供者信息
- 如果有多个服务提供者,消费者该如何选择?
- 服务消费者利用负载均衡算法,从服务列表中挑选一个
- 消费者如何得知服务提供者的健康状态?
- 服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态
- Eureka会更新记录服务列表信息,心跳不正常会被剔除
- 消费者就可以拉取到最新的信息
Eureka的作用
在Eureka架构中,微服务角色有两类:
- EurekaServer:服务端,注册中心
- 记录服务信息
- 心跳监控
- EurekaClient:客户端
- Provider:服务提供者
- 注册自己的信息到EurekaServer
- 每隔30秒向EurekaServer发送心跳
- consumer:服务消费者
- 根据服务名称从EurekaServer拉取服务列表
- 基于服务列表做负载均衡,选中一个微服务后发起远程调用
- Provider:服务提供者
搭建EurekaServer
搭建EurekaServer服务步骤如下:
-
创建项目,引入spring-cloud-starter-netflix-eureka-server的依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>
-
编写启动类,添加
@EnableEurekaServer
注解 -
编辑
application.yml
文件,添加下面配置server: port: 10086 #服务端口 spring: application: name: eurekaserver #服务名称 eureka: client: service-url: #Eureka的地址信息 defaultZone: http://127.0.0.1:10086/eureka
服务注册
将user-service服务注册到EurekaServer
-
在user-server项目引入
spring-cloud-starter-netflix-eureka-client
的依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
-
在application.yml文件,编写下面配置
server: port: 10086 #服务端口 spring: application: name: userservice #服务名称 eureka: client: service-url: #Eureka的地址信息 defaultZone: http://127.0.0.1:10086/eureka
服务发现
服务拉取是基于服务名称获取服务列表,然后在对服务列表做负载均衡
-
修改OrderService的代码,修改访问的路径,用服务名代替ip、端口:
-
在order-service项目的启动类OrderApplication中的RestTemplate添加 负载均衡 注解:
@LoadBalanced
Ribbon负载均衡
负载均衡策略
Ribbon的负载均衡规则是一个叫做IRule的接口来定义的,每一个子接口都是一种规则:
负载均衡策略:
内置负载均衡规则类 | 规则描述 |
---|---|
RoundRobinRule | 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则 |
AvailabilityFilteringRule | 对以下两种服务器进行忽略: 1、在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加 2、并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AVailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户的<clientName>,<clientConfigNameSpace>,ActiveConnectionsLimit属性进行配置 |
WeightedResponseTimeRule | 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择一个服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。然后再对Zone内的多个服务做轮询 |
BestAvailableRule | 忽略哪些短路的服务器,并选择并发数较低的服务器 |
RandomRule | 随机选择一个可用的服务器 |
RetryRule | 重试机制的选择逻辑 |
修改负载均衡策略
通过定义IRule实现可以修改负载均衡规则,有两种方式:
-
代码方式(全局):在order-service中的OrderApplication类(
配置类
)中,定义一个新的IRule:@Bean public IRule randomRule(){ return new RandomRule(); }
-
配置文件方式:在order-service的application.yml
userservice: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #负载均衡规则
饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
ribbon:
eager-load:
enabled: true #开启饥饿加载
clients: userservice #指定对userservice这个服务饥饿加载,这里可以添加多个服务(采用数组的方式)
Nacos注册中心
下载安装
Nacos是阿里巴巴产品,现在是SpringCloud中的一个组件。相比Eureka功能更加丰富,在国内受欢迎程度较高
-
官网下载地址
-
解压:解压路径不能包含中文路径
-
Windows启动命令:
startup.cmd -m standalone
-
访问:http://127.0.0.1:8848/
-
登录:用户名和密码默认都是
nacos
Nacos入门
-
在cloud-demo父工程中添加
spring-cloud-alibaba
的管理依赖<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.5.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency>
-
注释掉order-service和user-service中原有的Eureka依赖
-
添加nacos的客户端依赖
<!--nacos客户端--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
-
修改user-service&order-service中的application.yml,注释Eureka地址,添加nacos地址
spring: cloud: nacos: discovery: server-addr: localhost:8848 #nacos 服务端地址
-
启动测试
Nacos服务分级存储模型
- 服务调用尽可能选择本地集群的服务,跨集群调用延迟较高
- 本地集群不可访问时,再去访问其它集群
服务集群属性
-
修改application.yml,添加如下内容
spring: cloud: nacos: discovery: cluster-name: GZ #配置集群名称,也就是机房位置,例如:GZ,贵州
-
重启服务查看Nacos
NacosRule负载均衡
-
在order-service中设置负载均衡的IRule为NacosRule,这个规则优先会寻找与自己同集群的服务
userservice: ribbon: NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
根据权重负载均衡
- 在Nacos设置实例的权重值,首先选中实例后面的编辑按钮
- 将群众设置为0.1(0-1之间),测试可以发现8081被访问到的频率大大降低
环境隔离(namespace)
Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离
-
在Nacos控制台可以创建namespace,用来隔离不同环境
-
然后填写一个新的命令空间信息
-
保存后会在控制台看到这个命令空间的id:
-
修改order-service的application.yml,添加namespace
spring: cloud: nacos: discovery: namespace: ae6c2eb8-c143-4854-ad08-7e2c2c744290 #命名空间,填Id
-
不同namespace下的服务不可见
-
配置非临时实例
spring: cloud: nacos: discovery: ephemeral: false #是否是临时实例
HTTP客户端Feign
Feign代替RestTemplate
Feign是一个声明式的HTTP客户端,官网地址
其作用就是帮助我们优雅的实现HTTP请求的发送,解决上面提到的问题
RestTemplate方式调用存在的问题
String url = "http://userservice/user/" + order.getUserId():
User user = restTemplate.getForObject(url,User.class);
- 代码可读性差,编程体验不统一
- 参数复杂URL难以维护
定义和使用Feign客户端
-
引入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
在order-service的启动类加
@EnableFeignClients
注解开启Feign的功能: -
编写Feign客户端:
@FeignClient("userservice") //指定服务名称 public interface UserClient { @GetMapping("/user/{id}") User findByID( @PathVariable("id") Long id ); }
主要是基于SpringMVC的注解来声明远程调用的信息,比如:
- 服务名称:userservice
- 请求方式:GET
- 请求路径:/user/{id}
- 请求参数:Long id
- 返回值类型:User
-
使用Feign客户端
@Autowired private UserClient userClient; //注入userClient 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; }
自定义Feign的配置
Feign运行自定义配置来覆盖默认配置,可以修改的配置如下:
类型 | 作用 | 说明 |
---|---|---|
feign.Logger.Level | 修改日志级别 | 包含四种不同的级别:NONE、Basic、HEADERS、FULL |
feign.codec.Decoder | 响应结果的解析器 | HTTP远程调用的结果做解析,例如解析json字符串为java对象 |
feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过HTTP请求发送 |
feign.Contract | 支持的注解格式 | 默认是SpringMVC的注解 |
feign.Retryer | 失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 |
配置Feign日志有两种方式:
方式一:配置文件方式
-
全局生效
feign: client: config: default: loggerLevel: FULL
-
局部生效
feign: client: config: userservice: #服务名称 loggerLevel: FULL
方式二:java代码方式,需要先声明一个Bean:
public class DefaultFeignConfiguration {
@Bean
public Logger.Level logLevel(){
return Logger.Level.BASIC;
}
}
-
全局配置,则把他放到@EnableFeignClients这个注解中:
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
-
局部配置,则把它放到@FeignClient这个注解中:
@FeignClient(value = "userservice",configuration = DefaultFeignConfiguration.class)
Feign的性能优化
Feign底层的客户端实现:
- UTLConnection:默认实现,不支持连接池
- Apache HTTPClient:支持连接池
- OKHTTP:支持连接池
因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection
Feign添加HTTPClient的支持:
-
引入依赖
<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>
-
配置连接池
feign: httpclient: enabled: true # 开启Feign对HttpClient的支持 max-connections: 200 # 最大连接数 max-connections-per-route: 50 # 每个路径的最大连接数
Feign的最佳实践
方式一(继承):给消费者的FeignClient和提供者的Controller定义统一的父接口作为标准
- 服务紧耦合
- 父接口参数列表映射不会被继承
方式二(抽取):将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
抽取FeignClient
-
首先创建一个module,命名为feign-api,然后引入feign的starter依赖
-
将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
-
在order-service中引入feign-api的依赖
<!--引入feign的统一api--> <dependency> <groupId>com.xiaowu.demo</groupId> <artifactId>feign-api</artifactId> <version>1.0</version> </dependency>
-
修改order-service中所有上述三个组件有关的import部分,改成导入feign-api中的包
-
重启测试
当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。用两种方式解决:
-
方式一:指定FeignClient所在包
@EnableFeignClients(basePackages = "com.xiaowu.feign.clients")
-
方式二:指定FeignClient字节码
@EnableFeignClients(clients = {UserClient.class})
统一网关Gateway
网关作用介绍
网关的功能:
- 身份认证和权限校验
- 服务路由、负载均衡
- 请求限流
网关的技术实现
在SpringCloud中网关的实现包括两种:
- gateway
- zuul
Zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能
搭建网关服务
-
创建一个新的module,引入SpringCloudGateway的依赖和nacos的服务发现依赖
<!--nacos服务注册发现依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--网关gateway依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
-
编写主方法
@SpringBootApplication public class GatewayApplication { public static void main( String[] args ) { SpringApplication.run(GatewayApplication.class,args); } }
-
编写路由配置及nacos地址
server: port: 10010 spring: application: name: gateway cloud: nacos: discovery: server-addr: localhost:8848 #nacos地址 gateway: routes: - id: user-service # 路由标识,必须唯一 uri: lb://userservice #路由的目标地址 predicates: # 路由断言,判断请求是否符合规则 - Path=/user/** # 路径断言,判断路径是否以/user开头,如果是则符合 - id: order-service uri: lb://orderservice predicates: - Path=/order/**
-
启动测试
网关路由可以配置的内容包括:
- 路由id:路由唯一标识
- uri:路由目的地,支持lb和http两种
predicates:路由断言,判断请求是否符合要求,符合则转发到路由目的地
- filters:路由过滤器,处理请求或响应
路由断言工厂(Route Predicate Factory)
- 我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件
- 例如Path=/user/**是按照路径匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的
- 像这样的断言工厂在SpringCloudGateway还有十几个
Spring提供了11种基本的Predicate工厂:
名称 | 说明 | 示例 |
---|---|---|
After | 在某个时间点后的请求 | - After=2023-01-20T17:12:47.789-07:00[America/Denver] |
Before | 是某个时间点之前的请求 | - Before=2023-01-20T17:12:47.789-07:00[Asia/Shanghai] |
Between | 是某两个时间点之前的请求 | - Between=2023-01-20T17:12:47.789-07:00[Asia/Shanghai],2025-01-20T17:12:47.789-07:00[Asia/Shanghai] |
Cookie | 请求必须包含某些Cookie | - Cooke=chocolate,ch.p |
Header | 请求必须包含某些Header | - Header=X-Request,\d+ |
Host | 请求必须是访问某个host(域名) | - Host = **.somehost,**.anotherhost.ort |
Method | 请求方式必须是指定方式 | - Method=GET,POST |
Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/bule/** |
Query | 请求参数必须包含指定参数 | - Query=name,jack或者Query=name |
RemoteAddr | 请求者的ip必须是指定范围 | - RemoteAdd=192.168.1.1/24 |
Weight | 权重处理 |
路由过滤器 GatewayFilter
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理
Spring提供了31种不同的路由过滤器工厂。例如:
名称 | 说明 |
---|---|
AddRequestHeader | 给当前请求添加一个请求头 |
RemoveRequestHeader | 移除请求中的一个请求头 |
AddResponseHeader | 给响应结果中添加一个响应头 |
RemoveResponseHeader | 从响应结果中移除一个响应头 |
RequestRateLimiter | 限制请求流量 |
…… |
给所有进入userservice的请求添加一个请求头
给所有进入userservice的请求添加一个请求头:Truth=xiaowu
实现方式:在gateway中修改application.yml文件,给userservice的路由添加过滤器:
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos地址
gateway:
routes:
- id: user-service # 路由标识,必须唯一
uri: lb://userservice #路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否以/user开头,如果是则符合
filters: #过滤器
- AddRequestHeader= Truth, xiaowu
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
默认过滤器: 如果要对所有的路由都生效,则可以将过期工厂写到default下,如
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos地址
gateway:
routes:
- id: user-service # 路由标识,必须唯一
uri: lb://userservice #路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否以/user开头,如果是则符合
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
default-filters: # 默认过滤器,会对所有的路由请求都生效
- AddRequestHeader= Truth, xiaowu
全局过滤器 GlobalFiltering
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFiltering的作用一样。
区别在于GatewayFiltering通过配置定义,处理逻辑是固定的。而GlobalFiltering的逻辑需要自己写代码实现
定义方式是实现GlobalFiltering接口
public interface GlobalFilter{
/**
* 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
* @param exchange 请求上文,里面可以获取Request、Response等信息
* @param chain 用来把请求委托给下一个过滤器
* @return {@code Mana<Void>} 返回表示当前过滤器业务结束
* /
Mono<Void> filter(ServerWebExchange exchange,GatewayFilterChain chain);
}
定义全局过滤器,拦截并判断用户身份
需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:
-
参数中是否有authorization
-
authorization参数值是否为admin
-
如果同时满足则放行,否则拦截
@Order(-1) //优先级 @Component public class AutherizeFilter implements GlobalFilter { @Override public Mono<Void> filter( ServerWebExchange exchange, GatewayFilterChain chain ) { // 1.获取请求参数 ServerHttpRequest request = exchange.getRequest(); MultiValueMap<String, String> queryParams = request.getQueryParams(); // 2.获取参数中的authorization参数 String auth = queryParams.getFirst("authorization"); // 3.判断参数是否等于 admin if("admin".equals(auth)){ // 4.是,放行 return chain.filter(exchange); } // 5.否,拦截 // 5.1.设置状态码 exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); // 5.2.拦截请求 return exchange.getResponse().setComplete(); } }
过滤器执行顺序
请求进入网关会喷到三类过滤器:当前路由的过滤器、DefaultFiltering、GlobalFiltering
请求路由后,会将当前路由过滤器和DefaultFiltering、GlobalFiltering,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器
- 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。
- GlobalFiltering通过实现Ordered接口,或者添加@Order注解来指定Order值,由我们自己指定
- 路由过滤器和DefaultFiltering的Order由Spring指定,默认是按照声明顺序从1递增
- 当过滤器的Order值一样时,会按照DefaultFilter>路由过滤器>GlobalFilter的顺序执行
跨域问题处理
跨域:域名不一致就是跨域,主要包括:
- 域名不同:www.taobao.com和www.taobao.org和www.jd.com和miaosha.jd.com
- 域名相同:端口不同,localhost:8080和localhost8081
跨域问题:浏览器禁止请求的发起者与服务端发生跨域Ajax请求,请求被浏览器拦截的问题
解决方案:CORS
网关处理跨域采用的同样是CORS方案,并且只需要简单配置即可实现:
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos地址
gateway:
routes:
- id: user-service # 路由标识,必须唯一
uri: lb://userservice #路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否以/user开头,如果是则符合
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
default-filters: # 默认过滤器,会对所有的路由请求都生效
- AddRequestHeader= Truth, xiaowu
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站跨域请求
- "http://localhost:8090"
- "http:127.0.0.1"
allowedMethods: # 允许跨域的Ajax请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许请求中携带头信息
allowCredentials: true # 是否允许携带Cookie
maxAge: 360000 # 这次跨域检查的有效期
MQ
初始MQ
同步通讯和异步通讯
同步调用的问题
微服务间基于Feign的调用就属于同步方式,存在一些问题。
- 耦合度高: 每次加入新的需求,都要修改原来的代码
- 性能下降: 调用者需要等待服务提供者响应,如果调用链过长则响应时间等于每次调用的时间之和
- 浪费资源: 调用链中的每个服务在等待响应过程中,不能释放请求占用资源,高并发场景下会极度浪费系统资源
- 级联失败: 如果服务提供者出现问题,所有调用方都会跟着出问题,如同多米诺骨牌一样,迅速导致整个微服务群故障
异步调用方案
异步调用常见实现就是事件驱动模式
事件驱动优势
- 优势一:服务解耦
- 优势二:性能提升,吞吐量提高
- 优势三:服务没有强依赖,不担心级联失败问题
- 优势四:流量削峰
异步通信的缺点
- 依赖于Broker的可靠性、安全性、吞吐能力
- 架构复杂了,业务没有明显的流程线,不好追踪管理
什么是MQ
MQ(MessageQueue):消息队列,字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。
RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
---|---|---|---|---|
公司/社区 | Rabbit | Apache | 阿里 | Apache |
开发语言 | Erlang | Java | Java | Scala&Java |
协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,STOMP,REST,XMPP,AMQP | 自定义协议 | 自定义协议 |
单机吞吐量 | 一般 | 差 | 高 | 非常高 |
消息延迟 | 微妙级 | 毫秒级 | 毫秒级 | 毫秒以内 |
消息可靠性 | 高 | 一般 | 高 | 一般 |
RabbitMQ快速入门
RabbitMQ概述和安装
RabbitMQ是基于Erlang语言开发的开源消息通信中间件,官网
安装
-
将下载的安装包上传到Linux
-
安装docker命令:
yum -y install docker
-
启动到dockker:
systemctl start docker
-
加载镜像RabbitMQ:
docker load -i mq.tar
-
执行下面命令来运行MQ容器:
- RABBITMQ_DEFAULT_USE:用户名
- RABBITMQ_DEFAULT_PASS:密码
- name:名字
- hostname:主机名
- p:端口映射
- 1672:管理端口
- 5672:消息通信端口
- rabbitmq:镜像名称
docker run -e RABBITMQ_DEFAULT_USER=xiaowu -e RABBITMQ_DEFAULT_PASS=123456 --name mq --hostname mq1 -p 15672:15672 -p 5672:5672 -d rabbitmq:3-management
-
查看时候运行成功:
docker ps
-
浏览器访问:http://主机地址:15672/
-
登录
RabbitMQ中的几个概念:
- channel:操作MQ的工具
- exchange:路由消息队列中
- queue:缓存消息
- virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组
消息模型介绍
MQ的官方文档中给出了5个MQ的Demo示例,对应了几种不同的用法:
- 基本消息队列(BasicQueue)
- 工作消息队列(WorkQueue)
- 发布订阅(Publish、Subscribe),又根据交换机类型不同分为三种:
- Fanout Exchange:广播
- Direct Exchange:路由
- Topic Exchange:主题
HelloWorld案例
官方的HelloWorld是基于最基础的消息队列模型来实现的,只包括三个角色:
- publisher:消息发布者,将消息发送到队列queue
- queue:消息队列,负责接受并缓存消息
- consumer:订阅队列,处理队列中的消息
实现步骤:
- 导入工程
- 运行publisher服务中的测试类PublisherTest中的测试方法testSendMessage()
- 查看RabbitMQ控制台消息
- 启动consumer服务,查看是否能接收消息
基本消息队列的消息发送流程:
- 建立connection
- 创建channel
- 利用channel声明队列
- 利用channel向队列发送消息
基本消息队列的消息接收流程:
- 建立connection
- 创建channel
- 利用channel声明队列
- 定义consumer的消费行为handleDelivery()
- 利用channel将消费者与队列绑定
SpringAMQP
什么是SpringAMQP
Basic Queue简单队列模型
- 在父工程中引入spring-amqp的依赖
- 因为publisher和consumer服务都需要amqp依赖,因此这里把依赖直接放到父工程mq-demo中
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> <version>3.1.1</version> </dependency>
- 在publisher服务中利用RabbitTemplate发送消息到simple.queue这个队列
-
在publisher服务中编写application.yml,添加mq连接信息
spring: rabbitmq: host: 192.168.2.102 #主机地址 port: 5672 #端口 virtual-host: / #虚拟主机 username: xiaowu #用户名 password: 123456 #密码
-
在publisher服务中新建一个测试类,编写测试方法
@RunWith(SpringRunner.class) //声明测试类需要注入容器 @SpringBootTest //单元测试 public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendMessageSimpleQueue( ) { String queueName = "simple.queue"; String message = "Hello,spring amqp"; rabbitTemplate.convertAndSend(queueName,message); } }
-
- 在consumer服务中编写消费逻辑,绑定simple.queue这个队列
- 在consumer服务中编写application.yml,添加mq连接信息:
spring: rabbitmq: host: 192.168.2.102 #主机地址 port: 5672 #端口 virtual-host: / #虚拟主机 username: xiaowu #用户名 password: 123456 #密码
- 在consumer服务中新建一个类,编写消费逻辑
@Component //实现Bean注入 public class SpringRabbitListener { @RabbitListener(queues = "simple.queue") //监听消息,queues:消息的名称,可以是多个 public void listenSimpleQueue(String msg){ System.out.println("消费者接收到simple.queue消息:"+msg); } }
- 在consumer服务中编写application.yml,添加mq连接信息:
Work Queue工作队列模型
模拟WorkQueue,实现一个队列绑定多个消费者
基本思路如下:
- 在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue
- 在consumer服务中定义两个消息监听者,都监听simple.queue队列
- 消费者1秒处理50条消息,消费者2每秒处理10条消息
通过测试发现,消费者1处理完后需要等待消费者2处理
消费者预取限制
修改application.yml文件,设置preFetch这个值,可以控制预取消息
spring:
rabbitmq:
host: 192.168.2.102 #主机地址
port: 5672 #端口
virtual-host: / #虚拟主机
username: xiaowu #用户名
password: 123456 #密码
listener:
simple:
prefetch: 1 #每次只能取得到一条消息,处理完成ACR之后才能取到下一个消息
发布、订阅模型-Fanout
发布订阅模式与之前案例的区别就是允许将同一消息发送给多个消费者。实现方式是加入了Exchange(交换机)
Fanout Exchange会将接收到的消息路由到每一个跟其绑定的queue
实现思路如下:
-
在consumer服务中,利用代码声明队列,交换机,并将两则绑定
@Configuration public class FanoutConfig { // 声明xiaowu.fanout 交换机 @Bean public FanoutExchange fanoutExchange(){ return new FanoutExchange("xiaowu.fanout"); } // 声明队列fanout.queue1 @Bean public Queue fanoutQueue1(){ return new Queue("fanout.queue1"); } // 绑定队列1到交换机 @Bean public Binding fanoutBinding1(Queue fanoutQueue1,FanoutExchange fanoutExchange){ return BindingBuilder .bind(fanoutQueue1) .to(fanoutExchange); } // 声明队列fanout.queue2 @Bean public Queue fanoutQueue2(){ return new Queue("fanout.queue2"); } // 绑定队列2到交换机 @Bean public Binding fanoutBinding2(Queue fanoutQueue2,FanoutExchange fanoutExchange){ return BindingBuilder .bind(fanoutQueue2) .to(fanoutExchange); } }
-
在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
@RabbitListener(queues = "fanout.queue1") //监听消息,queues:消息的名称,可以是多个 public void listenFanoutQueue1(String msg){ System.out.println("消费者接收到fanout.queue1消息:"+msg); } @RabbitListener(queues = "fanout.queue2") //监听消息,queues:消息的名称,可以是多个 public void listenFanoutQueue2(String msg){ System.out.println("消费者接收到fanout.queue2消息:"+msg); }
-
在publisher中编写测试方法,向xiaowu.fanout发送消息
@Test public void testSendFanoutExchange(){ // 交换机名称 String exchangeName = "xiaowu.fanout"; //消息 String message = "hello,every one!"; rabbitTemplate.convertAndSend(exchangeName,"",message); }
发布、订阅模型-Direct
Direct Exchange会将接收到的消息根据规则路由到指定的Queue,因此成为路由模式(routes)
- 每一个Queue都与Exchange设置一个BindingKey
- 发布者发送消息时,指定消息的RoutingKey
- Exchange将消息路由到BindingKey与消息RoutingKey一致的队列
实现思路如下:
-
利用@RabbitListener声明Exchange、Queue、RoutingKey
@RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue1"), //队列名称 exchange = @Exchange(name = "xiaowu.direct",type = ExchangeTypes.DIRECT), //name:交换机名称 type:交换机类型 key = {"red","blue"} //BindingKey ))
-
在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
@RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue1"), //队列名称 exchange = @Exchange(name = "xiaowu.direct",type = ExchangeTypes.DIRECT), //name:交换机名称 type:交换机类型 key = {"red","blue"} //BindingKey )) public void listentDirectQueue1(String msg){ System.out.println("消费者接收到direct.queue1消息:"+msg); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue2"), //队列名称 exchange = @Exchange(name = "xiaowu.direct",type = ExchangeTypes.DIRECT), //name:交换机名称 type:交换机类型 key = {"red","yellow"} //BindingKey )) public void listentDirectQueue2(String msg){ System.out.println("消费者接收到direct.queue2消息:"+msg);
-
在publisher中编写测试方法,向xiaowu.direct发送消息
@Test public void testSendDirectExchange(){ // 交换机名称 String exchangeName = "xiaowu.direct"; //消息 String message = "hello,blue"; rabbitTemplate.convertAndSend(exchangeName,"blue",message); }
发布、订阅模型-Topic
TopicExchange与DirectExchange类似,区别在于RoutingKey可以是多个单词的列表,并且以
.
分割
Queue与Exchange指定BindingKey时可以使用通配符:
- #:代表0个或多个单词
- *:代表一个单词
实现思路如下:
-
利用@RabbitListener声明Exchange、Queue、RoutingKey
@RabbitListener(bindings = @QueueBinding( value = @Queue(name = "Topic.queue1"), //队列名称 exchange = @Exchange(name = "xiaowu.Topic",type = ExchangeTypes.TOPIC), //name:交换机名称 type:交换机类型 key = "china.#" //BindingKey ))
-
在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue
@RabbitListener(bindings = @QueueBinding( value = @Queue(name = "Topic.queue1"), //队列名称 exchange = @Exchange(name = "xiaowu.Topic",type = ExchangeTypes.TOPIC), //name:交换机名称 type:交换机类型 key = "china.#" //BindingKey )) public void listentTopicQueue1(String msg){ System.out.println("消费者接收到Topic.queue1消息:"+msg); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "Topic.queue2"), //队列名称 exchange = @Exchange(name = "xiaowu.Topic",type = ExchangeTypes.TOPIC), //name:交换机名称 type:交换机类型 key = "#.news" //BindingKey )) public void listentTopicQueue2(String msg){ System.out.println("消费者接收到Topic.queue2消息:"+msg); }
-
在publisher中编写测试方法,向xiaowu.topic发送消息
@Test public void testSendTopicExchange(){ // 交换机名称 String exchangeName = "xiaowu.Topic"; //消息 String message = "小吴在学Java,敲得都是Bug"; rabbitTemplate.convertAndSend(exchangeName,"china.news",message); }
消息转换器
在SpringAMQP的发送方法中,接收消息的类型是Object,也就是说我们可以发送任意对象类型的消息,SpringAMQP会帮我们序列化为字节后发送
-
在consumer中利用@Bean声明一个队列:
@Bean public Queue objectQueue(){ return new Queue("object.queue"); }
-
在publisher中发送消息测试
@Test public void testSendObjectQueue(){ Map<String,Object> msg = new HashMap<>(); msg.put("name","小吴"); msg.put("age",21); rabbitTemplate.convertAndSend("object.queue",msg); }
Spring对消息对象的处理事由org.springframework.amqp.support.converter.MessageConverter来处理的。而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化
如果要修改只需要定义一个Mess阿哥Convert二类型的Bean即可。推荐使用JSON方式序列化,步骤如下:
-
在publish服务引入依赖
<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> <version>2.15.2</version> </dependency>
-
在publisher服务(配置类)声明MessageConverter
@Bean public MessageConverter messageConverter(){ return new Jackson2JsonMessageConverter(); }
接收JSON消息
-
在consumer服务引入Jackson依赖
<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> <version>2.15.2</version> </dependency>
-
在consumer服务(配置类)定义MessageConverter
@Bean public MessageConverter messageConverter(){ return new Jackson2JsonMessageConverter(); }
-
然后定义一个消费者,监听object.queue队列并消费消息
@RabbitListener(queues = "object.queue") public void listenObjectQueue( Map<String,Object> msg ){ System.out.println("接收到的object.queue:"+msg); }
Docker
初识Docker
什么是Docker
Docker是一个快速交付应用,运行应用的技术:
- 可以将程序及其依赖、运行环境一起打包为一个镜像,可以迁移到任意Linux操作系统
- 运行时利用沙箱机制形成隔离容器,各个应用互不干扰
- 启动、移除都可以通过一行命令完成,方便快捷
项目部署问题
大型项目组件较多,运行环境也较为复杂,部署时会碰到一些问题:
- 依赖关系复杂,容易出现兼容性问题
- 开发、测试、生产环境差异
Docker如何解决依赖的兼容问题
- 将应用的Libs(函数库)、Deps(依赖)
- 配置与应用一起打包
- 将每个应用发到一个隔离容器去运行,避免互相干扰
Docker如何解决不同系统环境的问题
- Docker将用户程序与所需要调用的系统(比如CentOS)函数库一起打包
- Docker运行到不同操作系统时,直接基于打包的库函数,借助于操作系统的Linux内核来运行
镜像和容器
- 镜像(Image): Docker将应用程序及其所需要的依赖、函数库、环境、配置等文件打包在一起,称为镜像。
- 容器(Container): 镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器做隔离,对外不可见。
Docker和DockerHub
- DockerHub:DockerHub是一个Docker镜像托管平台。这样的平台称为Docker Registry。
- 国内也有类似于DockerHub的公开服务,比如网易云镜像服务、阿里云镜像库等。
Docker架构
Docker是一个CS架构的程序,由两部分组成:
- 服务端(Server):Docker守护进程,负责处理Docker指令,管理镜像、容器等
- 客户端(Client):通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令。
安装Docker
企业部署一般都是采用Linux操作系统,而其中CentOS发行版占比最多,因此我们在CentOS下安装Docker
-
卸载:如果之前安装过旧版本的Docker,可以使用下面命令卸载:
yum remove -y docker \ docker-client \ Docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-selinux \ docker-engine-selinux \ docker-engine \ docker-ce
-
安装yum工具
yum install -y yum-utils \ device-mapper-persistent-data \ lvm2 --skip-broken
-
更新本地镜像源:
# 设置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
-
安装Docker:
yum install -y docker-ce
-
启动Docker:启动Docker前要关闭防火墙或者放行端口
- 关闭防火墙
systemctl stop firewalld #关闭防火墙 systemctl disable firewalld #禁止关机重启
- 启动Docker:
systemctl start docker #启动 systemctl stop docker #停止 systemctl restart docker #重启
- 查看版本:
docker -v
- 关闭防火墙
配置镜像:阿里云文档
Docker基本操作
镜像操作
镜像名称一般分两部分组成:[repository]:[tag]
在没有指定tag时,默认是latest,代表最新版本的镜像
从DockerHub中拉取一个Nginx镜像并查看
- 首先去镜像仓库搜索Nginx镜像
- 查看镜像:
docker images
利用docker save将Nginx镜像导出磁盘,然后再通过Load加载回来
- 利用
docker xx --help
命令查看docker shave
和docker load
语法
容器操作
创建运行一个Nginx容器
-
去docker hub查看Nginx的容器运行命令
docker run --name mn -p 80:80 -d nginx
- doucker run:创建并运行一个容器
- - -name:给容器起一个名字,比如mn
- -p:将宿主机端口与容器端口映射,冒号左侧是宿主机端口,右侧是容器端口
- -d:后台运行容器
- Nginx:镜像名称
进入Nginx容器,修改HTML文件内容,添加“小吴在敲Bug”
-
进入容器
docker exec -it mn bash
- docker exec:进入容器内部,执行一个命令
- -it:给当前进入的容器创建一个标准输入、输出终端,允许我们与容器交互
- bash:进入容器后执行的命令,八十是一个Linux终端交换命令
-
进入Nginx的HTML所在目录
/usr/shar/nginx/html
cd /usr/shar/nginx/html
-
修改index.html的内容
sed -i 's#Welcome to nginx#小吴在敲Bug#g' index.html sed -i 's#<head>#<head><meta charset="utf-8">#g' index.html
数据卷
容器与数据耦合的问题
数据卷(volume): 是一个虚拟目录,指向宿主机文件系统的某个目录
数据卷操作的基本语法如下:
docker volume [COMMAND]
docker volume命令是数据操作,根据命令跟随的command来确定下一步操作:
- create —— 创建一个volume
- inspect —— 显示一个或多个volume的信息
- ls —— 列出所有的volume
- prune —— 删除未使用的volume
- rm —— 删除一个或多个指定的volume
创建一个数据卷,并查看数据卷在宿主机的目录位置
-
创建数据卷
docker volume create html
-
查看所有数据
docker volume ls
-
查看数据卷详细信息卷
docker volume inspect html
挂载数据卷
我们在创建容器时,可以通过 -v
参数来挂载一个数据到某个容器目录
docker run \ #创建并运行容器
--name mn \ # 给容器起名字
-v html:/root/html \ #把html数据卷挂载到容器内的/root/html/这个目录中
-p 80:80 \ #把宿主机的80端口映射到容器内的80端口
nginx #镜像名称
创建一个Nginx容器,修改容器内的html目录内的index.html内容
-
创建容器并挂载数据卷到容器内的HTML目录
# 如果容器运行时volume不存在,会自动创建出来 docker run --name mn -p 80:80 -v html:/usr/share/nginx/html -d nginx
-
进入HTML数据卷所在位置,并修改HTML内容
# 查看HTML数据卷的位置 docker volume inspect html # 进入该目录 cd /var/lib/docker/volumes/html/_data # 修改文件 vi index.html
创建并运行一个MySQL容器,将宿主机目录直接挂载到容器
提示:目录挂载与数据卷挂载的语法是类似的:
- -v [宿主机目录]:[容器内目录]
- -v[宿主机文件]:[容器内文件]
实现思路如下:
-
将下载好的mysql.tar文件上传到虚拟机,通过load命令加载为镜像
-
创建目录/tmp/mysql/data
-
创建目录/tmp/mysql/conf,将hmy.cnf文件上传到/tmp/mysql/conf
-
去DockerHub查阅资料,创建运行MySQL容器,要求
- 挂载/tmp/mysql/data到容器内数据存储目录
- 挂载/tmp/mysql/conf/hmy.cnf到MySQL容器的配置文件
- 设置MySQL密码
docker run \ --name mysql \ -e MYSQL_ROOT_PASSWORD=root \ -p 3306:3306 \ -v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \ -v /tmp/mysql/data:/var/lib/mysql \ -d \ mysql:5.7.25
Dockerfile自定义镜像
镜像结构
- 镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。
- 基础镜像(BaseImage):应用依赖的系统函数库、环境、配置、文件等
- 层(Layer):在BaseImage基础上添加安装包、依赖、配置等,每次操作都形成新的一层
- 入口(Entrypoint):镜像运行入口,一般是程序启动的脚步和参数
- 其它:在BaseImage基础上添加依赖,安装程序,完成整个应用的安装和配置
Dockerfile语法
Dockerfile就是一个文本文件,其中包含一个个的指令(Instruction),用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层(Layer)
官方文档
指令 | 说明 | 实例 |
---|---|---|
FORM | 指定基础镜像 | 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 |
基于Ubuntu镜像构建一个新镜像,运行一个Java项目
- 创建一个空文件夹docker-demo
- 将docker-demo.jar文件上传到docker-demo这个目录
- 将jdk8.tar.gz文件上传到docker-demo这个目录
- 将Dockerfile上传到docker-demo这个目录
- 进入docker-demo
- 运行命令:
docker build -t javaweb:1.0 .
基于Java:8-alpine镜像,将一个Java项目构建为镜像
-
新建一个空的目录,然后再目录中新建一个文件,命名为Dockerfile
-
将docker-demo.jar文件上传到这个目录
-
编写Dockerfile文件:
- 基于java:8-alpine作为基础镜像
- 将app.jar拷贝到镜像中
- 暴露端口
- 编写入口ENTRYPOINT
# 指定基础镜像 FROM java:8-alpine COPY ./docker-demo.jar /tmp/app.jar # 暴露端口 EXPOSE 8090 # 入口,java项目的启动命令 ENTRYPOINT java -jar /tmp/app.jar
-
使用docker build命令构建镜像
-
使用docker run创建容器并运行
DockerCompose
初始DockerCompose
- Docker Compose可以基于Compose文件帮我们快速的部署分布式应用,而无需收到一个个创建和运行容器
- Compose文件是一个文本文件,通过指令定义集群汇总的每个容器如何运行
- DockerCompose官网
安装DockerCompose
-
下载
# 安装 curl -L https://get.daocloud.io/docker/compose/releases/download/1.29.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
-
修改文件权限:
chmod +x /usr/local/bin/docker-compose
-
命令补全设置
echo "199.232.68.133 raw.githubusercontent.com" >> /etc/hosts curl -L https://raw.githubusercontent.com/docker/compose/1.29.1/contrib/completion/bash/docker-compose -o /etc/bash_completion.d/docker-compose
项目部署
将之前学习的cloud-demo微服务集群利用DockerCompose部署
-
修改自己的cloud-demo项目,将数据库、nacos地址都命名为docker-compose中的服务名
-
使用maven打包工具,将项目中的每个微服务都打包为app.jar
-
将打包好的app.jar拷贝到cloud-demo中的每一个对应的子目录
-
在每个对应的子目录创建
Dockerfile
文件并写入FROM java:8-alpine COPY ./app.jar /tmp/app.jar ENTRYPOINT java -jar /tmp/app.jar
-
数据库文件
-
在项目根目录创建
docker-compose.yml
文件并编写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"
-
将cloud-demo上传到虚拟机
-
进入cloud-demo目录,利用
docker-compose up -d
来部署
Docker镜像厂库
镜像仓库(Docker Registry)有公共和私有的两种形式:
- 公共仓库:列入Docker官方的Docker Hub,国内也有一些云服务商提供类似于Docker Hub的公开服务,比如网易云镜像服务、DaoCloud镜像服务、阿里云镜像服务等
- 除了使用公开仓库外,用户还可以再本地搭建私有Docker Registry。企业自己的镜像最好是采用私有Docker Registry来实现
搭建私有厂库
简化版镜像仓库
Docker官方的Docker Registry是一个基础版本的Docker镜像仓库,具备仓库管理的完整功能,但是没有图形化界面。命令如下:
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 可以查看当前私有镜像服务中包含的镜像
图形化界面版本
-
配置docker信任地址,因为私服采用的是http协议,默认不被Docker信任,所以需要做一个配置:
# 打开要修改的文件 vi /etc/docker/daemon.json # 添加内容:配置之间要用逗号分隔 "insecure-registries":["http://192.168.2.102:8090"] # 重加载 systemctl daemon-reload # 重启docker systemctl restart docker
-
使用DockerCompose部署带有图象界面的DockerRegistry
-
新建空文件夹registry-ui:
mkdir registr-ui
-
创建docker-compose.yml文件:
vi registr-ui/docker-compose.yml
,内容如下version: '3.0' services: #官方Docker Registr registry: image: registry volumes: - ./registry-data:/var/lib/registry ui: #图形化界面 image: joxit/docker-registry-ui:static ports: #暴露端口8090 - 8090:80 environment: #服务部署标题和registry内部访问地址 - REGISTRY_TITLE=小吴私有仓库 - REGISTRY_URL=http://registry:5000 depends_on: - registry
-
进入registry-ui运行docker-compose,日志查看
docker-compose up -d docker-compose logs -f
-
-
访问8090端口查看下
-
推送镜像到私有镜像服务必须先tag,步骤如下
-
重新tag本地镜像,名称前缀为私有仓库的地址:192.168.2.102:8090/
docker tag nginx:latest 192.168.2.102:8090/nginx:1.0
-
推送镜像
docker push 192.168.2.102:8090/nginx:1.0
-
拉取镜像
docker pull 192.168.2.102:8090/nginx:1.0
-