我们使用我前面搭建的微服务架构进行演示
教程路径:https://blog.csdn.net/weixin_56320090/article/details/117323546
关于搭建基础的微服务架构大家可以参考我的这个项目。
Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速 实现动态服务发现、服务配置、服务元数据及流量管理。 从上面的介绍就可以看出,nacos的作用就是一个注册中心,用来管理注册上来的各个微服务。
接下来,我们就在现有的环境中加入nacos,并将我们的两个微服务注册上去.
1.安装nacos
下载地址: https://github.com/alibaba/nacos/releases
下载zip格式的安装包,然后进行解压缩操作
注意:nacos1.3.2版本后默认以集群模式启动,如果想要单机启动需要使用一下命令启动(任何版本都支持命令启动)
#命令启动
startup.cmd -m standalone
3.访问客户端的nacos页面
成功访问代表启动成功,账号和密码都是“nacos”
可以看到现在我们的服务是没有的,接下来我们把我们的微服务注册到nacos上
在需要注册的微服务工程的pom.xml中添加依赖
<!-- 添加nacos的发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-nacos-discovery</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
# 注册微服务的名称
spring.application.name=shop-product
#要注册到的nacos的ip
spring.cloud.nacos.discovery.server-addr=192.168.31.89:8899
Feign是Spring Cloud提供的一个声明式的伪Http客户端, 它使得调用远程服务就像调用本地服务一样简单, 只需要创建一个接口并添加一个注解即可。
Nacos很好的兼容了Feign, Feign默认集成了 Ribbon, 所以在Nacos下使用Fegin默认就实现了负载均衡的效果。
在需要调用其他微服务的工程中加入此依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
@SpringBootApplication
@EnableDiscoveryClient //开启nacos的注解
@EnableFeignClients //开启feign的注解
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
}
//声明此接口连接的微服务
@FeignClient(value = "shop-product")
public interface ProductFeign {
public Product findByIdProduct(@PathVariable("pid") Integer pid );
}
使用openfeign的注意事项
//@GetMapping("/product/findByIdProduct/{pid}")需要访问的其他微服务的请求路径
@GetMapping("/product/findByIdProduct/{pid}")
// public Product findByIdProduct(@PathVariable(“pid”) Integer pid );
//方法名,返回类型,权限修饰符,参数都要与要访问的方法一致,强烈建议直接把方法复制过来去掉方法体。
比如:我们这边要访问的是 名为“shop-product”的微服务,访问路径全名为“/product//findByIdProduct/{pid}”,方法为“ public Product findByIdProduct(@PathVariable(“pid”) Integer pid )”
一定要保证路径正确,方法一致,微服务名正确
在我们注入 private ProductFeign productFeign;这个接口后,我们调用此接口的方法时,就会调用对应微服务的方法来获取数据。
@RestController
@CrossOrigin
@RequestMapping("/order")
public class OrderController_openfeign {
@Resource
private OrderDao dao;
@Autowired
private ProductFeign productFeign;//修改处
@GetMapping("payOrder")
public String payOrder(Integer pid , Integer num){
Order order = new Order();
order.setUid(1);
order.setNumber(num);
order.setUsername("沈金坡");
order.setPid(pid);
Product product = productFeign.findByIdProduct(pid); //修改处
if (product!=null){
order.setPname(product.getPname());
order.setPprice(product.getPprice());
}else {
throw new RuntimeException("您购买的商品已下架");
}
int i = dao.insert(order);
if(i>0){
return "下单成功";
}
return "失败";
}
}
启动两个微服务。调用order的方法,此时order的方法会调用product微服务内的方法
大家都都知道在微服务架构中,**一个系统会被拆分为很多个微服务。**那么作为客户端(pc androud ios 平板)要如何去调用这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调用。
上面的这些问题可以借助API网关来解决。
所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服 务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控(黑白名单)、路由转发等等。 添加上API网关之后,系统的架构图变成了如下所示:
我们使用Spring Cloud Gateway来做网关。
Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor 等术开发的网关,**它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。**它的目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控和限流。
优点:
缺点:
要求: 通过浏览器访问api网关,然后通过网关将请求转发到商品微服务
<dependencies>
<!--gateway依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--fastjson依赖 在全局过滤器内用到-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
</dependencies>
@SpringBootApplication
public class MyGatway {
public static void main(String[] args) {
SpringApplication.run(MyGatway.class,args);
}
}
yml书写格式在这里比较方便
server:
port: 9000
spring:
application:
name: shop-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.31.89:8899
gateway:
routes:
- id: shop-product # 路由的唯一标识,只要不重复都可以,如果不写默认会通过UUID产生,一般写成被路由的服务名称
uri: lb://shop-product # 被路由的地址
order: 0 #表示优先级 数字越小优先级越高
predicates: #断言: 执行路由的判断条件
- Path=/product/** #请求同中是以/product开头的请求会被路由到 shop-product 微服务中
- id: shop-order
uri: lb://shop-order
order: 0
predicates:
- Path=/order/**
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1.0
Predicate(断言, 谓词) 用于进行条件判断,只有断言都返回真,才会真正的执行路由。
断言就是说: 在 什么条件下 才能进行路由转发
SpringCloud Gateway包括许多内置的断言工厂,所有这些断言都与HTTP请求的不同属性匹配体如下:
AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期
BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期
BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内
实例:
-After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]
接收一个IP地址段,判断请求主机地址是否在地址段中
-RemoteAddr=192.168.1.1/24
CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。 判断请求
cookie是否具有给定名称且值与正则表达式匹配。
-Cookie=chocolate, ch.
HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否
具有给定名称且值与正则表达式匹配。
-Header=X-Request-Id, \d+
HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。
-Host=**.testhost.org
MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。
-Method=GET
PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。
-Path=/foo/{segment}基于Query请求参数的断言工厂
QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具
有给定名称且值与正则表达式匹配。
-Query=baz, ba.
WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
routes:
-id: weight_route1 uri: host1 predicates:
-Path=/product/**
-Weight=group3, 1
-id: weight_route2 uri: host2 predicates:
-Path=/product/**
-Weight= group3, 9
1 作用: 过滤器就是在请求的传递过程中,对请求和响应做一些手脚
2 生命周期: Pre Post
3 分类: 局部过滤器(作用在某一个路由上) 全局过滤器(作用全部路由上)
在Gateway中, Filter的生命周期只有两个:“pre” 和 “post”。
Gateway 的Filter从作用范围可分为两种: GatewayFilter与GlobalFilter。
局部过滤器是针对单个路由的过滤器。
内置过滤器:
在SpringCloud Gateway中内置了很多不同类型的网关路由过滤器。
全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。
SpringCloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:
—————内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验。
开发中的鉴权逻辑:
自定义全局过滤器 要求:必须实现GlobalFilter,Order接口
@Component
public class LoginFilter implements Ordered, GlobalFilter {
}
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取请求头中携带的token数据
String token = exchange.getRequest().getQueryParams().getFirst("token");
//获取请求对象
ServerHttpRequest request = exchange.getRequest();
//获取响应对象
ServerHttpResponse response = exchange.getResponse();
//判断token中是否有数据
if(StringUtils.isNotEmpty(token)){
//不为空就放行
return chain.filter(exchange);
}
//判断是否为登录请求
if(exchange.getRequest().getPath().toString().contains("/login")){
//是请求路径就放行
return chain.filter(exchange);
}
//如果未登录,自定义响应内容
DataBuffer buffer=null;
try{
HashMap<String, Object> commonResult = new HashMap<>();
commonResult.put("code",401);
commonResult.put("msg","登录失效");
byte[] bytes = JSON.toJSONString(commonResult).getBytes("utf-8");
buffer = response.bufferFactory().wrap(bytes);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-type","application/json;charset=UTF-8");
}catch (UnsupportedEncodingException e){
e.printStackTrace();
}
//返回指定的数据
return response.writeWith(Mono.just(buffer));
}
//此方法就是声明拦截器的优先级 数越小,优先级越大
@Override
public int getOrder() {
return 0;
}
自此,拦截器配置成功。
**在大型系统的微服务化构建中,一个系统被拆分成了许多模块。**这些模块负责不同的功能,组合成系统,最终可以提供丰富的功能。在这种架构中,一次请求往往需要涉及到多个服务。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心,也就意味着这种架构形式也会存在一些问题:
分布式链路追踪(Distributed Tracing),就是将一次分布式请求还原成调用链路,进行日志记录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。
在这边我们使用“Sleuth”+“Zipkin”来记录
SpringCloud Sleuth主要功能就是在分布式系统中提供追踪解决方案。它大量借用了Google Dapper的设计, 先来了解一下Sleuth中的术语和相关概念。
Trace (一条完整链路–包含很多span(微服务接口))
由一组Trace Id(贯穿整个链路)相同的Span串联形成一个树状结构。为了实现请求跟踪,当请求到达分布式系统的入口端点时,只需要服务跟踪框架为该请求创建一个唯一的标识(即TraceId),同时在分布式系统内部流转的时候,框架始终保持传递该唯一值,直到整个请求的返回。那么我们就可以使用该唯一标识将所有的请求串联起来,形成一条完整的请求链路。
Span
代表了一组基本的工作单元。为了统计各处理单元的延迟,当请求到达各个服务组件的时候,也通过一个唯一标识(SpanId)来标记它的开始、具体过程和结束。通过SpanId的开始和结束时间戳,就能统计该span的调用时间,除此之外,我们还可以获取如事件的名称。请求信息等元数据。
Annotation
用它记录一段时间内的事件,内部使用的重要注释:
我这里在最大的父工程内添加,就等于在所有的子工程下都加入了依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-sleuthartifactId>
dependency>
功能演示:
后台展示:
可以看到,再加入Sleuth后,在调用此微服务时,就会生成日志文件
Zipkin 是 Twitter 的一个开源项目,它基于Google Dapper实现,它致力于收集服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储展现、查找和我们可以使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的REST API接口来辅助我们查询跟踪数据以实现对分布式系统的监控程序,从而及时地发现系统中出现的延迟升高问题并找出系统性能瓶颈的根源
除了面向开发的 API 接口之外,它也提供了方便的UI组件来帮助我们直观的搜索跟踪信息和分析请求链路明细,比如:可以查询某段时间内各用户请求的处理时间等。
Zipkin 提供了可插拔数据存储方式:In-Memory、MySql、Cassandra 以及 Elasticsearch。
Zipkin 分为两端,一个是 Zipkin 服务端,一个是 Zipkin 客户端,客户端也就是微服务的应用。客户端会配置服务端的 URL 地址,一旦发生服务间的调用的时候,会被配置在微服务里面的 Sleuth 的监听器监听,并生成相应的 Trace 和 Span 信息发送给服务端。
Zipkin的 github 地址:https://github.com/apache/incubator-zipkin
在放有此jar包的路径下打开cmd执行此命令
java -jar zipkin-server-2.12.9-exec.jar
http://localhost:9411(路径)
ZipKin客户端和Sleuth的集成非常简单,只需要在微服务中添加其依赖和配置即可。
我这里在最大的父工程内添加,就等于在所有的子工程下都加入了依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
在每个需要链路追踪的微服务中加入配置
yml文件下配置:注意缩进
spring:
application:
nacos:
discovery:
server-addr: 192.168.31.89:8899
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1.0
properties文件配置:
spring.zipkin.base-url=http://localhost:9411
spring.sleuth.sampler.probability=1.0
连接ZipKin的路径
spring.zipkin.base-url=http://localhost:9411
样本抽取比例1.0就是100%
spring.sleuth.sampler.probability=1.0
http://localhost:7000/order-serv/order/prod/1
Zipkin Server默认会将追踪数据信息保存到内存,但这种方式不适合生产环境。Zipkin支持将追踪数据持久化到mysql数据库或elasticsearch中。
REATE TABLE IF NOT EXISTS zipkin_spans (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL,
`id` BIGINT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`parent_id` BIGINT,
`debug` BIT(1),
`start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL',
`duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query' )
ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_spans ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `id`) COMMENT 'ignore insert on duplicate';
ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`, `id`) COMMENT 'for joining with zipkin_annotations';
ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds';
ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames';
ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range';
CREATE TABLE IF NOT EXISTS zipkin_annotations (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id',
`span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id',
`a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1',
`a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB',
`a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation',
`a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp',
`endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address',
`endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null' )
ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds';
ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames';
ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces';
ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`) COMMENT 'for dependencies job';
CREATE TABLE IF NOT EXISTS zipkin_dependencies (
`day` DATE NOT NULL,
`parent` VARCHAR(255) NOT NULL,
`child` VARCHAR(255) NOT NULL,
`call_count` BIGINT )
ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_dependencies ADD UNIQUE KEY(`day`, `parent`, `child`);
java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=mysql --
MYSQL_HOST=127.0.0.1 --MYSQL_TCP_PORT=3306 --MYSQL_DB=zipkin --MYSQL_USER=root -
-MYSQL_PASS=root
当ZipKin 服务关闭后,数据都被持久化到mysql中,下次启动还会有记录