部署环境:阿里云ECS服务器
端口映射信息:
eureka1:8761 | eureka2:8762
config-server:8888
shopping-product:11100
shopping-order:11110
api-gateway:8080
open-api:8081
因为Module作为子项目,我们改写下对应的POM文件。
重新Build一下项目,能正常编译。但是此时Eureka Server是不能正常启动工作的,需要在application类增加
@EnableEurekaServer。
同样,我们修改POM文件,依赖于父项目,注意这里需要引入eureka-client和spring-boot-starter-web依赖。
需要在application类增加@EnableDiscoveryClient,同时修改配置文件。
重启Eureka Client,启动后再次访问Eureka Server管理界面,可以发现order-provider服务已注册。
之前我们的Eureka Server是单点服务,实际生产中,经常是多台注册中心,因此我们尝试下配置2台注册中心。
启动服务器实例1:
启动服务器实例2:
重启2台注册中心,启动后分别访问2台的管理界面,可以看到2台注册中心已经相互注册。
项目增加2个服务模块,并向Eureka Server注册:shopping-product(商品服务)、shopping-order(订单服务),实现相应业务逻辑,这部分详细实现不再阐述。
整体项目结构如下:
spring-cloud-app
--eureka-server(服务注册中心)
--shopping-common(购物公共模块)
--shopping-product(商品服务模块)
--shopping-order(订单服务模块)
系统架构如图,比较简单,一个集群服务中心,目前有2个服务提供并注册:
Spring Cloud Ribbon 是一个客户端的负载均衡器,它提供对大量的HTTP和TCP客户端的访问控制。
客户端负载均衡即是当浏览器向后台发出请求的时候,客户端会向 Eureka Server 读取注册到服务器的可用服务信息列表,然后根据设定的负载均衡策略(没有设置即用默认的),抉择出向哪台服务器发送请求。
假设有以下业务场景,shopping-order模块需要调用shopping-product提供的API接口。我们看如何实现。
第一种方法使用构造RestTemplate,调用远程API,这种方法url是写死,如果启动多台shopping-product服务的话,那又该如何?
第二种方法:我们启动2个shopping-product服务实例,分别是11100端口和9001端口,运行测试发现,会根据loadBalancerClient负载均衡机制帮我们选择一个服务地址,进行访问调用。
但这样依旧很是麻烦,接下来看第三种方法。第三种方法屏蔽了API的具体url信息,只用ServerId,并根据负载均衡规则,自动路由到对应的地址。
因为eureka包中已经添加了对Ribbon的依赖,我们可以增加断点,调试程序,发现进入RibbonLoadBalancerClient-->choose方法,返回负载均衡策略选择的ServiceInstance。
当然,我们也可以指定应用服务的负载均衡策略:
目前系统架构如图,实现shopping-product和shopping-order集群化部署,调用方式通过客户端负载均衡,来路由消费端的请求。
Feign是一个声明式的Web Service客户端,它的目的就是让Web Service调用更加简单。Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。
而Feign则会完全代理HTTP请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。Feign整合了Ribbon和Hystrix(关于Hystrix我们后面再讲),可以让我们不再需要显式地使用这两个组件。
总起来说,Feign具有如下特性:
shopping-product服务提供端暴露API。
shopping-order模块需要调用shopping-product接口,首先我们在服务调用端增加Maven依赖
启动类标注开启Feign服务
本地调用测试,可以正常返回接口数据。
上个环境中,我们有2个服务提供者,首先看下各自的配置,可以发现很大一部分都是重复的。
如果微服务架构中没有使用统一配置中心时,所存在的问题:
Spring Cloud Config 有它的一套访问规则,我们通过这套规则在浏览器上直接访问就可以。
{application} 就是应用名称,对应到配置文件上来,就是配置文件的名称部分,例如我上面创建的配置文件。
{label} 表示 git 分支,默认是 master 分支,如果项目是以分支做区分也是可以的,那就可以通过不同的 label 来控制访问不同的配置文件了。
config-server本身作为一个服务,也可以作为服务提供方,向服务中心注册,其他的服务想要获取配置文件,只需要通过服务名称就会访问。
Spring Cloud Config 在项目启动时加载配置内容这一机制,但是如果我们修改配置文件内容后,不会自动刷新。例如我们上面的项目,当服务已经启动的时候,去修改 github 上的配置文件内容,这时候,再次刷新页面,对不起,还是旧的配置内容,新内容不会主动刷新过来。那应该怎么去触发配置信息的动态刷新呢?
它提供了一个刷新机制,但是需要我们主动触发。那就是 @RefreshScope 注解并结合 actuator ,注意要引入 spring-boot-starter-actuator 包。
每次改了配置后,就用 postman 访问一下 refresh 接口,还是不够方便。 github 提供了一种 webhook 的方式,当有代码变更的时候,会调用我们设置的地址,来实现我们想达到的目的。
填上回调的地址
也就是上面提到的 actuator/refresh 这个地址,但是必须保证这个地址是可以被 github 访问到的。这样每当github上修改了配置文件,就自动通知对应的hook地址自动刷新。
整体项目结构如下:
spring-cloud-app
--config-server(统一配置中心)
--eureka-server(服务注册中心)
--shopping-common(购物公共模块)
--shopping-product(商品服务模块)
--shopping-order(订单服务模块)
1、异步处理
比如用户在电商网站下单,下单完成后会给用户推送短信或邮件,发短信和邮件的过程就可以异步完成。因为下单付款是核心业务,发邮件和短信并不属于核心功能,并且可能耗时较长,所以针对这种业务场景可以选择先放到消息队列中,有其他服务来异步处理。
2、应用解耦:
假设公司有几个不同的系统,各系统在某些业务有联动关系,比如 A 系统完成了某些操作,需要触发 B 系统及 C 系统。如果 A 系统完成操作,主动调用 B 系统的接口或 C 系统的接口,可以完成功能,但是各个系统之间就产生了耦合。用消息中间件就可以完成解耦,当 A 系统完成操作将数据放进消息队列,B 和 C 系统去订阅消息就可以了。这样各系统只要约定好消息的格式就好了。
3、流量削峰
比如秒杀活动,一下子进来好多请求,有的服务可能承受不住瞬时高并发而崩溃,所以针对这种瞬时高并发的场景,在中间加一层消息队列,把请求先入队列,然后再把队列中的请求平滑的推送给服务,或者让服务去队列拉取。
4、日志处理
kafka 最开始就是专门为了处理日志产生的。
当碰到上面的几种情况的时候,就要考虑用消息队列了。如果你碰巧使用的是 RabbitMQ 或者 kafka ,而且同样也是在使用 Spring Cloud ,那可以考虑下用 Spring Cloud Stream。Spring Cloud Stream 是消息中间件组件,它集成了 kafka 和 rabbitmq ,本文以rabbitmq 为例。
分析目前shopping-order项目中,创建订单的代码如下:
创建订单的同时,先调用商品接口扣减库存,如果占用库存成功,再生成订单。这样的话,生成订单的操作和占用商品库存的操作其实是耦合在一起的。在实际电商高并发、高流量的情况下,我们很少这么做。所以,我们要将业务解耦,实现订单和扣减库存的异步处理。
大体思路如下:生成订单==》通知商品调用库存==》商品占用库存==》通知订单占用成功==》更新订单占用库存状态
shopping-order、shopping-product项目中
启动应用程序,测试发送接口,发现spring-cloud-stream帮我们自动创建了一个队列,消息发送到这个队列,然后被接收端消费。
shopping-order作为库存占用命令的消息发送者,首先向shopping-product发送消息stock_apply(占用库存申请),shopping-product接收此消息进行库存处理,然后将库存占用处理的结果作为消息stock_result(占用库存结果)发送,shopping-order端再收到结果消息对订单状态进行更新。
执行调试结果,跟踪执行结果:生成订单同时发送库存申请命令,商品模块处理库存申请成功后,返回库存占用结果告知订单模块,从而实现订单生成和商品库存占用的逻辑的解耦。
在原有的架构基础上,我们对商品和订单服务进行了应用解耦,库存占用逻辑异步化,通过消息队列传递消息,并结合spring cloud stream对消息input和output绑定,使得在程序中很方便的进行消息发送和接收处理。
zuul的核心是一系列的filters, 其作用类比Servlet框架的Filter,或者AOP。zuul把请求路由到用户处理逻辑的过程中,这些filter参与一些过滤处理,比如Authentication,Load Shedding等
Zuul使用一系列不同类型的过滤器,使我们能够快速灵活地将功能应用于我们的边缘服务。这些过滤器可帮助我们执行以下功能:
默认的路由规则是按照服务的名称来路由服务,当然我们也可以自定义。在zuul中,路由匹配的路径表达式采用ant风格定义
这样我们再访问这个接口时,就提示 Not Found 错误了
之前路由的配置都是写在配置文件中,如果路由规则变化以后,需要重启网关服务。但是实际生产环境,一般都需要动态的加载路由的配置,不能轻易重启网关服务。
设想以下场景:我们需要判断用户请求的参数是否包含认证信息,如果包含token信息,则可以访问,否则禁止访问。可以用Zuul Filter很方便的实现在网关端,统一进行认证。
这里介绍一种限流的设计方案:
对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。如图所示,令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
Google公司已经实现了上述的令牌桶的算法,直接使用 RateLimiter 就可以通过Zuul实现限流的功能:
整体项目结构如下:
spring-cloud-app
--api-gateway(服务网关)
--config-server(统一配置中心)
--eureka-server(服务注册中心)
--shopping-common(购物公共模块)
--shopping-product(商品服务模块)
--shopping-order(订单服务模块)
目前所有的客户端请求,首先被发送到统一网关服务处理,然后由网关进行限流、熔断、权限验证、记录日志等等,然后根据自定义的路由规则,再分发到不同的应用服务中去,应用服务器返回处理结果后,由网关统一返回给客户端。
在分布式环境中,许多服务依赖项中的一些必然会失败。Hystrix是一个库,通过添加延迟容忍和容错逻辑,帮助你控制这些分布式服务之间的交互。Hystrix通过隔离服务之间的访问点、停止级联失败和提供回退选项来实现这一点,所有这些都可以提高系统的整体弹性。
更恐怖的是,如果这是一个多级调用,即此处的服务的调用结果还被其他服务调用了,这就形成了所谓的雪崩效应,后果将不堪设想。因此,需要某种机制,在一定的异常接口调用出现的时候,能够自动发现这种异常,并快速进行服务降级。
这样的话,shopping-order调用远程服务,超过3s之后,立刻返回错误处理,不会再阻塞。
如果某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。
Spring Coud 还给 Hytrix 提供了一个可视化的组件:
通过以上容错方法的实现,就可以构建更加稳定、可靠的分布式系统:
微服务架构是一个分布式架构,它按业务划分服务单元,一个分布式系统往往有很多个服务单元。由于服务单元数量众多,业务的复杂性,如果出现了错误和异常,很难去定位。主要体现在,一个请求可能需要调用很多个服务,而内部服务的调用复杂性,决定了问题难以定位。所以微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与,参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出了问题,很快定位。
OpenTracing 是一个轻量级的标准化层,它位于应用程序/类库和追踪或日志分析程序之间。
OpenTracing 的优势
OpenTracing 数据模型
OpenTracing 中的 Trace(调用链)通过归属于此调用链的 Span 来隐性的定义。特别说明,一条 Trace(调用链)可以被认为是一个由多个 Span 组成的有向无环图(DAG图),Span 与 Span 的关系被命名为 References。
例如:下面的示例 Trace 就是由8个 Span 组成:
每个 Span 包含以下的状态:(译者注:由于这些状态会反映在 OpenTracing API 中,所以会保留部分英文说明)
键值对中,键必须为 string,值可以是任意类型。但是需要注意,不是所有的支持 OpenTracing 的 Tracer,都需要支持所有的值类型。
每一个 SpanContext 包含以下状态:
OpenTracing 实现
事件类型
Zipkin Server主要包括四个模块:
Collector 接收或收集各应用传输的数据
Storage 存储接受或收集过来的数据,当前支持Memory,MySQL,Cassandra,ElasticSearch等,默认存储在内存中。
API(Query) 负责查询Storage中存储的数据,提供简单的JSON API获取数据,主要提供给web UI使用
Web 提供简单的web界面
首先,安装 zipkin,为了方便直接用 docker 进行安装,具体详见容器化部署章节,这里不再详述。
在服务调用的过程中,通过Sleuth将链路信息(经过抽样后的信息)统一上报给Zipkin,通过Zipkin就可以集中查看和管理微服务架构中的调用链路信息,便于开发人员与运维人员跟踪和调试问题。
-management 表示有管理界面的,可以浏览器访问。5672是访问端口,15672是管理端口。