从架构演变谈微服务框架在分布式中的作用
1、架构演变过程
单体架构
我们最先接触的单体架构,整个系统就只有一个工程,打包往往是打成了 war 包,然后部署
到单一 tomcat 上面,这种就是单体架构,如图:
假如系统按照功能划分了,商品模块,购物车模块,订单模块,物流模块等等模块。那么所
有模块都会在一个工程里面,这就是单体架构。
单体架构优点
- 结构简单,部署简单
- 所需的硬件资源少
- 节省成本
单体架构缺点
- 版本迭代慢,往往改动一个代码会影响全局
- 不能满足一定并发的访问
- 代码维护困难,所有代码在一个工程里面,存在被其他人修改的风险
单体架构横向扩展
随着业务的拓展,公司的发展,单体架构慢慢的不能满足我们的需求,我们需要对架构进行
变动,我们能够想到的最简单的办法就是加机器,对应用横向扩展。
如图:
这种架构貌似暂时解决了我们的问题,但是用户量慢慢增加后,我们只能通过横向加机器来
解决,还是会存在版本迭代慢,代码维护困难的问题。而且用户请求往往是读多写少的情况,
所以可能真正需要扩容的只是商品模块而已,而现在是整个工程都扩容了,这无形中是一种
资源的浪费,因为其他模块可能根本不需要扩容就可以满足需求。所以我们有必要对整个工
程按照模块进行拆分,拆分后的架构图如下:
模块拆分后,模块和模块之间是需要通过接口调用的方式进行通信,模块和模块之间通过分
流软件进行负载均衡。这个架构解决前面的资源浪费问题和代码管理问题,因为我们是对系
统拆分了,各个模块都有单独的工程,比如我修改商品模块,就不需要担心会不会影响购物
车模块。但是这种架构扩展非常麻烦,一旦需要横向加机器,或者减机器都需要修改 nginx
配置,一旦机器变多了以后,nginx 的配置量就是一个不能完成的工作。OK,这时候 SOA 服
务治理框架就应运而生,架构图如下:
基于注册中心的 SOA 框架,扩展是非常方便的,因为不需要维护分流工具,但我们启动应
用的时候就会把服务通过 http 的方式注册到注册中心。
在 SOA 框架中一般会有三种角色:1、注册中心 2、服务提供方 3、服务消费方
- 注册中心
在注册中心维护了服务列表 - 服务提供方
服务提供方启动的时候会把自己注册到注册中心 - 服务消费方
服务消费方启动的时候,把获取注册中心的服务列表,然后调用的时候从这个服务列表中选
择某一个去调用。
微服务工程的特点:
- 扩展灵活
- 每个应用都规模不大
- 服务边界清晰,各司其职
- 打包应用变多,往往需要借助 CI 持续集成工具
2、搭建简单的微服务工程
2.1、注册中心搭建
Springcloud 中,我们选择 eureka 作为注册中心,springcloud 工程是基于 springboot 工程的。
pom.xml 中 jar 包依赖:
Springcloud 的版本
Eureka 服务端启动器导入
Springcloud 的依赖仓库导入
Application.properties 配置文件
启动类
2.2、服务提供方
Pom 的 jar 包依赖
其他都跟 eureka 服务端是一样的,只是服务提供方要把服务注册到 eureka
服务端,所以服务提供方就是 eureka 的客户端,所以需要导入 eureka 客户端的启动器。
bootstrap.properties
启动类
服务消费方
pom 和属性配置文件基本上差不多,消费要负责调用服务提供方,所以需要调用客户端
服务调用的时候就根据服务提供方的服务名称来调用的
服务提供方的名称,再加上服务提供方的接口名就可以完成调用了。
服务提供方和服务消费启动的时候都会往服务注册中心注册服务,eureka 服务端也可以通过
界面查看到服务注册情况
3、Eureka 用户认证
连接到 eureka 的时候需要带上连接的用户名和密码
Eureka 服务端改造
添加启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
代码配置,关闭 csrf 验证
@EnableWebSecurity
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//关闭csrf
http.csrf().disable();
//开启认证:URL格式登录必须是httpBasic
http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
}
}
Application.yml 配置
spring:
security:
user:
name: admin
password: admin
basic:
enabled: true
Eureka 客户端改造
跟 eureka 连接的时候要带上用户名密码
eureka:
client:
serviceUrl:
defaultZone: http://admin:admin@localhost:8763/eureka/
4、服务续约保活
当客户端启动想 eureka 注册了本身服务列表后,需要隔段时间发送一次心跳给 eureka 服务
端来证明自己还活着,当 eureka 收到这个心跳请求后才会知道客户端还活着,才会维护该
客户端的服务列表信息。一旦因为某些原因导致客户端没有按时发送心跳给 eureka 服务端,
这时候 eureka 可能会认为你这个客户端已经挂了,它就有可能把该服务从服务列表中删除
掉。
有关续约保活的配置客户端配置
#服务续约,心跳的时间间隔
eureka.instance.lease-renewal-interval-in-seconds=30
#如果从前一次发送心跳时间起,90 秒没接受到新的心跳,讲剔除服务
eureka.instance.lease-expiration-duration-in-seconds=90
#表示 eureka client 间隔多久去拉取服务注册信息,默认为 30 秒
eureka.client.registry-fetch-interval-seconds=30
服务端配置
#自我保护模式,当出现出现网络分区、eureka 在短时间内丢失过多客户端时,
会进入自我保护模式,即一个服务长时间没有发送心跳,eureka 也不会将其删
除,默认为 true
eureka.server.enable-self-preservation=true
#Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低
于 85%,如果低于 85%,Eureka Server 会将这些实例保护起来
eureka.server.renewal-percent-threshold=0.85
#eureka server 清理无效节点的时间间隔,默认 60000 毫秒,即 60 秒
eureka.server.eviction-interval-timer-in-ms=60000
5、Eureka 健康检测
Eureka 默认的健康检测只是你校验服务连接是否是 UP 还是 DOWN 的,然后客户端只会调用
状态为 UP 状态的服务,但是有的情况下,虽然服务连接是好的,但是有可能这个服务的某
些接口不是正常的,可能由于需要连接 Redis,mongodb 或者 DB 有问题导致接口调用失败,
所以理论上服务虽然能够正常调用,但是它不是一个健康的服务。所以我们就有必要对这种
情况做自定义健康检测。
Application.properties 配置
#开启健康检测
#健康检测
eureka.client.healthcheck.enabled=true
自定义健康检测代码
public static boolean canVisitDb = true;
/*
* 这个接口只为了检测db连接是否ok
* */
@RequestMapping("/db/{can}")
public void setDb(@PathVariable boolean can) {
canVisitDb = can;
}
@Configuration
public class MyHealthIndicator implements HealthIndicator {
@Override
public Health health() {
if(ProviderController.isCanLinkDb){
return new Health.Builder(Status.UP).build();
}else{
return new Health.Builder(Status.DOWN).build();
}
}
}
@Configuration
public class MyHealthCheckHandler implements HealthCheckHandler {
@Autowired
private MyHealthIndicator myHealthIndicator;
@Override
public InstanceStatus getStatus(InstanceStatus instanceStatus) {
Status status = myHealthIndicator.health().getStatus();
if(status.equals(Status.UP)){
return InstanceStatus.UP;
}else{
return InstanceStatus.DOWN;
}
}
}
我们可以在 health 方法里面去连接数据库,如果连接异常了则返回 DOWN,如果没异常则
方法 UP,这个 health 方法是线程去掉的,隔一段时间掉一次
检测检测依赖的 jar 包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
6、服务下线
比如有些情况是服务主机意外宕机了,也就意味着服务没办法给 eureka 心跳信息了,但是
eureka 在没有接受到心跳的情况下依赖维护该服务 90s,在这 90s 之内可能会有客户端调用
到该服务,这就可能会导致调用失败。所以我们必须要有一个机制能手动的立马把宕机的服
务从 eureka 服务列表中清除掉,避免被服务调用方调用到。
调用服务下线的接口:
这个接口是调用 eureka 服务端的接口
http://localhost:8763/eureka/apps/MICRO-ORDER/localhost:micro-order:8084
7、Eureka 高可用
Eureka 热备份的架构图如下
整个微服务中存在多个 eureka 服务,每个 eureka 服务都是相互复制的,会把客户端注册进
来的服务复制到 eureka 集群中的其他节点里面来。其实简单来说就是 eureka 每个节点相互
复制。
具体配置如下:
端口为 8761 的 eureka 服务端把自己注册到 8762 的 eureka 服务端
同样的道理,8762 注册到 8761
配置存在在两个配置文件中
启动的时候按照指定配置文件启动
java -jar springcloud-eureka.jar --spring.profiles.active=8761
8、Ribbon API
Ribbon 是一个独立的组件,是用来进行远程接口调用的,代码如下
通过 getForObject 方法可以掉到用 micro-order 服务的,queryUser 接口。然后在调用期间会
存在负载均衡,micro-order 服务对应有几个服务实例就会根据负载均衡算法选择某一个去
调用。
Get 请求
getForEntity:此方法有三种重载形式,分别为:
getForEntity(String url, Class<T> responseType)
getForEntity(String url, Class<T> responseType, Object... uriVariables)
getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables)
getForEntity(URI url, Class<T> responseType)
注意:此方法返回的是一个包装对象 ResponseEntity其中 T 为 responseType 传入类型,
想拿到返回类型需要使用这个包装类对象的 getBody()方法
getForObject:此方法也有三种重载形式,这点与 getForEntity 方法相同:
getForObject(String url, Class<T> responseType)
getForObject(String url, Class<T> responseType, Object... uriVariables)
getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables)
getForObject(URI url, Class<T> responseType)
注意:此方法返回的对象类型为 responseType 传入类型
Post 请求
post 请求和 get 请求都有ForEntity 和ForObject 方法,其中参数列表有些不同,除了这两个
方法外,还有一个 postForLocation 方法,其中 postForLocation 以 post 请求提交资源,并返
回新资源的 URI
postForEntity:此方法有三种重载形式,分别为:
postForEntity(String url, Object request, Class<T> responseType, Object... uriVariables)
postForEntity(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables)
postForEntity(URI url, Object request, Class<T> responseType)
注意:此方法返回的是一个包装对象 ResponseEntity其中 T 为 responseType 传入类型,
想拿到返回类型需要使用这个包装类对象的 getBody()方法
postForObject:此方法也有三种重载形式,这点与 postForEntity 方法相同:
postForObject(String url, Object request, Class<T> responseType, Object... uriVariables)
postForObject(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables)
postForObject(URI url, Object request, Class<T> responseType)
注意:此方法返回的对象类型为 responseType 传入类型
postForLocation:此方法中同样有三种重载形式,分别为:
postForLocation(String url, Object request, Object... uriVariables)
postForLocation(String url, Object request, Map<String, ?> uriVariables)
postForLocation(URI url, Object request)
注意:此方法返回的是新资源的 URI,相比 getForEntity、getForObject、postForEntity、
postForObject 方法不同的是这个方法中无需指定返回类型,因为返回类型就是 URI,通过
Object… uriVariables、Map<String, ?> uriVariables 进行传参依旧需要占位符,参看 postForEntity
部分代码
9、负载均衡算法
//线性轮训
new RoundRobinRule();
//可以重试的轮训
new RetryRule();
//根据运行情况来计算权重
new WeightedResponseTimeRule();
//过滤掉故障实例,选择请求数最小的实例
new BestAvailableRule();
//随机
new RandomRule();
10、Ribbon 配置
Application.properties 配置
#点对点直连测试配置
# 关闭 ribbon 访问注册中心 Eureka Server 发现服务,但是服务依旧会注
册。
#true 使用 eureka false 不使用
ribbon.eureka.enabled=true
spring.cloud.loadbalancer.retry.enabled=true
#指定调用的节点
micro-order.ribbon.listOfServers=localhost:8001
#单位 ms ,请求连接超时时间
micro-order.ribbon.ConnectTimeout=1000
#单位 ms ,请求处理的超时时间
micro-order.ribbon.ReadTimeout=2000
micro-order.ribbon.OkToRetryOnAllOperations=true
#切换实例的重试次数
micro-order.ribbon.MaxAutoRetriesNextServer=2
#对当前实例的重试次数 当 Eureka 中可以找到服务,但是服务连不上时将会
重试
micro-order.ribbon.MaxAutoRetries=2
micro-order.ribbon.NFLoadBalancerRuleClassName=com.netfli
x.loadbalancer.RandomRule
micro-order.ribbon.NFLoadBalancerPingClassName=com.netfli
x.loadbalancer.PingUrl
代码配置
使用@RibbonClients 加载配置
这个配置类只针对 micro-order 服务,微服务系统里面有很多服务,这就可以区别化配置。
配置 configuration 配置类的时候,一定要注意,配置类不能陪@ComponentScan 注解扫描到,
如果被扫描到了则该配置类就是所有服务共用的配置了。
11、Ribbon 单独使用
Ribbon 是一个独立组件,可以脱离 springcloud 使用的
需要依赖两个 jar