title: springCloud从0到丧心病狂
copyright: true
toc: true
author: tyrantqiao
categories: []
date: 2021-07-19 07:07:29
password:
tags:

  • springCloud
  • Java

Spring Cloud

微服务架构领域核心

组件配置

Eureka、Ribbon、Feign、Hystrix、Zuul 等组件

业务场景设计

比如说这样子的业务场景,外卖 APP,用户下单喜欢的菜,支付订单,扣减菜的库存,通知外卖小哥以及商家出货。

像以上的业务场景,按微服务切分就可以有订单服务模块、库存模块、商家模块、快送模块

其中由订单模块统筹大局进行管理

  • 操作订单的支付
  • 管控库存的扣减
  • 通知商家出货
  • 通知快送模块进行配送

组件间的协作

微服务间怎么用这些组件进行串联

Erureka

首先要调用微服务,就需要知道对应的地址,服务信息等,这个就是由 Eureka 服务注册中心管理

Eureka service-registration-and-discovery

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-exUae45F-1628430573938)(https://raw.githubusercontent.com/tyrantqiao/picgo/master/img/Eureka%E6%B5%81%E7%A8%8B%E5%9B%BE.drawio.png)]

Eureka 连环炮

  • EurekaServer 要部署多少台机器
  • 系统服务对 EurekaServer 实际访问流量以及 TPS
  • Eureka 怎么存储服务的地址信息,数据结构是怎么样的
  • 服务拉取注册表的方式以及频率
  • Eureka 从技术层面上如何抗住日千万级别的访问流量
Eureka 拉取服务以及心跳机制

关于拉取频率和心跳机制可以参考官方文档

Being an instance also involves a periodic heartbeat to the registry (through the client’s serviceUrl) with a default duration of 30 seconds. A service is not available for discovery by clients until the instance, the server, and the client all have the same metadata in their local cache (so it could take 3 heartbeats)

翻译过来,就是 Eureka client 心跳机制为 30 秒一次,通知 Eureka Server 正常。对于每一个 Eureka Client 而言,去 Eureka Server 拉取最新的服务也是 30 秒一次,看看注册表有没有更新的内容。
PS: 服务只要在实例、server、client 的本地缓存都有同样数据时,才能对外发布使用,最长的话可能需要花费三次心跳时间。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CqVabVBb-1628430573940)(https://raw.githubusercontent.com/tyrantqiao/picgo/master/img/Eureka%E6%B5%81%E7%A8%8B%E5%9B%BE%E5%BF%83%E8%B7%B3.drawio.png)]

Eureka 的访问量计算

比如说有 50 个服务、每个服务有 3 个机器,那么就是有 503=150 个服务实例。每个服务实例正常是每分钟发送两次心跳信号,拉取两次注册表,当它请求 4 次,4150=600 次每分钟。

换算成秒也才 600/60s=10TPS,这方面的压力不大,假如部署的服务更多,那大致在 TPS1、200 左右,那么 Eureka 是怎么保证高性能的呢?

Eureka 的数据结构
public abstract class AbstractInstanceRegistry implements InstanceRegistry {// 内存使用ConcurrentHashMap进行存储相关服务注册信息private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap();
}
  • ConcurrentHashMap

    • key 服务名字比如说配送服务
    • Map<String,Lease<InstanceInfo>> 服务对应的服务实例,比如说配送服务 1,配送服务 2
      • String 对应着服务实例的 id
      • Lease<InstanceInfo> Lease 记录了服务上一次心跳,Purpose is to avoid accumulation of instances in AbstractInstanceRegistry as result of ungraceful shutdowns that is not uncommon in AWS environments.
        • InstanceInfo 也就是每个实例的具体信息,比如说 ip 地址等信息

讲完了数据结构,那么技术上如何保证这份内存实例可以高并发读写呢?答案是多级缓存的机制

Eureka 多级缓存
public class ResponseCacheImpl implements ResponseCache {private final ConcurrentMap<Key, Value> readOnlyCacheMap = new ConcurrentHashMap<Key, Value>();private final LoadingCache<Key, Value> readWriteCacheMap;private final AbstractInstanceRegistry registry;
  • 缓存机制

    • 先从 ReadOnlyCacheMap 读取配置信息
    • 如果没有再从 readWriteCacheMap 里面读取注册表
    • 最后读取实际的注册表即 registry 中的内容
  • 如果有注册等行为发生时

    • 先登记到 registry 中,然后调用invalidateCache过期掉readWriteCacheMap
    • 此时 client 还可以读取 readOnlyCacheMap 中的内容
    • 后台也有起TimeTask任务定期将ReadWriteCacheMap中内容同步到ReadOnlyCacheMap,即此时 ReadOnlyCacheMap 也会清空
    • 如果有 client 拉取服务,从 registry 获取最新数据,再填充缓存

通过这个机制,可以有效分担读写流量,保证性能以及高吞吐量

总流程图如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JY9i5qEu-1628430573941)(https://raw.githubusercontent.com/tyrantqiao/picgo/master/img/Eureka%E5%A4%9A%E7%BA%A7%E7%BC%93%E5%AD%98.drawio.png)]

读写锁优化机制

此处很多官方文档都写错了的囧 o(╯□╰)o,AbstractInstanceRegistry在代码实现时是读的操作上写锁,写的操作上读锁,与网上一堆文章是相反的,下面记录下 Eureka 为什么要怎么做。

快速简要:读写锁将读、写锁分开,实现读读不互斥,读写互斥,写写互斥,这样子可以保证并发读的场景,适用于读多写少的场景,比如说像 Eureka 这种读明显大于写的场景。

方法名 是读逻辑还是写逻辑 读锁 写锁 不适用锁
register
internalCancel
statusUpdate
deleteStatusOverride
getApplicationDeltas
getApplicationDeltasFromMultipleRegions
renew
evict
getApplicationsFromMultipleRegions

为什么写操作上读锁与读操作上写锁相反呢?这是由于 Eureka 应用集合一致性哈希码导致的,我们先看到写操作上读锁的两个方法getApplicationDeltas以及getApplicationDeltasFromMultipleRegions


// 上锁主要是为了recentlyChangedQueue 以及registry的共享变量的应用实例状态一致,保证返回的赠礼应用实例集合的状态是准确的try {// !!!!! 写锁上锁 !!!!!!!!!!write.lock();Iterator<RecentlyChangedItem> iter = this.recentlyChangedQueue.iterator();logger.debug("The number of elements in the delta queue is :{}", this.recentlyChangedQueue.size());// 取recentlyChangedQueue放到apps,最后会做一个hashcode的计算while (iter.hasNext()) {Lease<InstanceInfo> lease = iter.next().getLeaseInfo();InstanceInfo instanceInfo = lease.getHolder();logger.debug("The instance id {} is found with status {} and actiontype {}",instanceInfo.getId(), instanceInfo.getStatus().name(), instanceInfo.getActionType().name());Application app = applicationInstancesMap.get(instanceInfo.getAppName());if (app == null) {app = new Application(instanceInfo.getAppName());applicationInstancesMap.put(instanceInfo.getAppName(), app);apps.addApplication(app);}app.addInstance(decorateInstanceInfo(lease));}if (includeRemoteRegion) {for (String remoteRegion : remoteRegions) {RemoteRegionRegistry remoteRegistry = regionNameVSRemoteRegistry.get(remoteRegion);if (null != remoteRegistry) {Applications remoteAppsDelta = remoteRegistry.getApplicationDeltas();if (null != remoteAppsDelta) {for (Application application : remoteAppsDelta.getRegisteredApplications()) {if (shouldFetchFromRemoteRegistry(application.getName(), remoteRegion)) {Application appInstanceTillNow =apps.getRegisteredApplications(application.getName());if (appInstanceTillNow == null) {appInstanceTillNow = new Application(application.getName());apps.addApplication(appInstanceTillNow);}for (InstanceInfo instanceInfo : application.getInstances()) {appInstanceTillNow.addInstance(instanceInfo);}}}}}}}// !!! 获取所有的实例,然后再计算应用hashCode,在下面这个方法中我们可以看到对registry的调用,写锁也就是主要为了锁最近改变列表以及这个registry  !!!!Applications allApps = getApplicationsFromMultipleRegions(remoteRegions);apps.setAppsHashCode(allApps.getReconcileHashCode());return apps;} finally {write.unlock();}public Applications getApplicationsFromMultipleRegions(String[] remoteRegions) {boolean includeRemoteRegion = null != remoteRegions && remoteRegions.length != 0;logger.debug("Fetching applications registry with remote regions: {}, Regions argument {}",includeRemoteRegion, remoteRegions);if (includeRemoteRegion) {GET_ALL_WITH_REMOTE_REGIONS_CACHE_MISS.increment();} else {GET_ALL_CACHE_MISS.increment();}Applications apps = new Applications();apps.setVersion(1L);// !!!!!!!对registry的调用查询 !!!!!!for (Entry<String, Map<String, Lease<InstanceInfo>>> entry : registry.entrySet()) {Application app = null;if (entry.getValue() != null) {for (Entry<String, Lease<InstanceInfo>> stringLeaseEntry : entry.getValue().entrySet()) {Lease<InstanceInfo> lease = stringLeaseEntry.getValue();if (app == null) {app = new Application(lease.getHolder().getAppName());}app.addInstance(decorateInstanceInfo(lease));}}if (app != null) {apps.addApplication(app);}}if (includeRemoteRegion) {for (String remoteRegion : remoteRegions) {RemoteRegionRegistry remoteRegistry = regionNameVSRemoteRegistry.get(remoteRegion);if (null != remoteRegistry) {Applications remoteApps = remoteRegistry.getApplications();for (Application application : remoteApps.getRegisteredApplications()) {if (shouldFetchFromRemoteRegistry(application.getName(), remoteRegion)) {logger.info("Application {}  fetched from the remote region {}",application.getName(), remoteRegion);Application appInstanceTillNow = apps.getRegisteredApplications(application.getName());if (appInstanceTillNow == null) {appInstanceTillNow = new Application(application.getName());apps.addApplication(appInstanceTillNow);}for (InstanceInfo instanceInfo : application.getInstances()) {appInstanceTillNow.addInstance(instanceInfo);}} else {logger.debug("Application {} not fetched from the remote region {} as there exists a "+ "whitelist and this app is not in the whitelist.",application.getName(), remoteRegion);}}} else {logger.warn("No remote registry available for the remote region {}", remoteRegion);}}}// 计算哈希codeapps.setAppsHashCode(apps.getReconcileHashCode());return apps;}

以及 hashCode 计算代码以及方程式

appsHashCode=eachInstance(status+count)appsHashCode =eachInstance( status+count) appsHashCode=eachInstance(status+count)

比如说 4 个 UP、0 个 DOWN 就类似
appsHashCode=UP∗4+DOWN∗0appsHashCode= UP*4 + DOWN*0 appsHashCode=UP∗4+DOWN∗0

    /*** Gets the hash code for this <em>applications</em> instance. Used for* comparison of instances between eureka server and eureka client.** @return the internal hash code representation indicating the information*         about the instances.*/@JsonIgnorepublic String getReconcileHashCode() {TreeMap<String, AtomicInteger> instanceCountMap = new TreeMap<String, AtomicInteger>();populateInstanceCountMap(instanceCountMap);return getReconcileHashCode(instanceCountMap);}

可能有点疑惑,读写锁与这个一致性哈希计算到底有什么关系。

1️⃣ 要保证这个哈希 code 是一致性的,也就是获取时不允许有写操作的发生。
2️⃣ 不允许有写操作,那我们上读锁也可以,这段时间不会有写操作,同时也可以让其他人读取。
3️⃣ 但问题来了,在本例中,registry 以及 recentlyChangedQueue 的写方法更多,读取相对更少,可以参考下面两张截图
4️⃣ 对于 Eureka 这种读多的场景,肯定是要优先保证读的性能,那么我们就肯定是得让读操作不受影响。
5️⃣ 写操作多,假如我们给写操作上写锁,那么写期间,就没办法做读了,阻塞住
6️⃣ 但如果我们给写操作上的是读锁,随便它怎么改 registry、recentlyChangedQueue,也不会影响到别人。
此外,假如我们真的需要获取应用增量实例信息,我们这时是尝试上写锁,会与读锁冲突,如果拿不到,那我们就等待读锁释放。等拿到写锁后,我这个获取实例的动作是互斥的,能保证数据一致性的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MFkBXtUx-1628430573942)(https://raw.githubusercontent.com/tyrantqiao/picgo/master/img/AbstractRegistry%E5%86%99%E6%93%8D%E4%BD%9C1.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Em4QV0He-1628430573943)(https://raw.githubusercontent.com/tyrantqiao/picgo/master/img/AbstractInstance%E5%86%99%E6%93%8D%E4%BD%9C2.png)]

Hystrix

微服务系统中经常会面临一种问题,当某某服务宕机时,怎么保证整个系统的稳定性。比如说配送服务宕机了,大量的请求涌入了进来,由于配送服务不响应,导致订单全部 hang 住,继而拖垮整个应用。

这种问题又称为服务雪崩问题

为了避免这种问题,引入了 Hystrix 的组件进行服务熔断、隔离、降级的情况。HyStrix 的核心原理在于设置很多的小线程池,通过对小线程池的管控实现熔断、降级等

举个例子:比如说外卖配送的逆风服务宕机了,但其他的外卖小哥还可以正常服务啊,完全可以把逆风服务这块的功能熔断,优先尝试其他服务,这种也可以成为降级。如果我们彻底不要配送服务,就称为熔断。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Alovlrhc-1628430573944)(https://raw.githubusercontent.com/tyrantqiao/picgo/master/img/Hystrix%E7%86%94%E6%96%AD.drawio.png)]

Zuul

API 服务网关,提供服务给前端、移动端等系统,便于做统一的认证、安全、限流等情况,同时前端也不需要了解微服务的部署情况以及相应的信息

Feign

在 Eureka 中我们知道了服务对应的地址信息,我们还需要 Feign 来帮我们实现轻松的服务调用,建立服务连接、发起请求、获取响应、解析响应等等。

Feign 的核心原理在于用动态代理进行调用

  • 对使用了@FeignClient注解的接口创建动态代理
  • 调用接口的动态代理
  • 根据接口上的@RequestMapping@FeignClient@pathVariable等注解构造注解地址
  • 根据产生的地址进行远程服务调用

Ribbon

上面我们知道了 Feign 要组装远程服务地址,但如果服务有多个,那要怎么取舍,怎么做负载均衡呢,这时就需要 Ribbon 来做服务的负载均衡,由它来选择最终要调用的服务

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ud3aWFd1-1628430573945)(https://raw.githubusercontent.com/tyrantqiao/picgo/master/img/Feign%252BRibbon.drawio.png)]

SpringCloud 组件架构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2uhk4xFF-1628430573945)(https://raw.githubusercontent.com/tyrantqiao/picgo/master/img/springCloud%E7%BB%84%E4%BB%B6%E6%9E%B6%E6%9E%84.drawio.png)]

SpringCloud从0到丧心病狂相关推荐

  1. SpringCloud 2021.0.1 SpringCloudGateway 3.1.1新版中GlobalFilter使用OpenFeign失败(503)的问题

    SpringCloud 2021.0.1 SpringCloudGateway 3.1.1新版中GlobalFilter使用OpenFeign失败的问题 简单说下好了,这个问题其实在springClo ...

  2. SpringCloud DataFlow — 0. 本地部署

    SpringCloud DataFlow - 0. 本地部署部署 SpringCloud DataFlow - 1. 自定义Processor SpringCloud DataFlow - 2. 自定 ...

  3. Java开源项目:saas小程序商城(SpringCloud + Oauth2.0 + ShiroRedis + JWT + Gateway + Nacos + Nginx+Vue+Mysql)

    项目采用**SpringCloud **主流开源框架,,设计轻巧,使用简单,开发人员接手与二次开发简单易懂: 项目完成了对阿里云.腾讯云.微信生态的快速接入与代码示例,并成功运用到了商业中,方便大家学 ...

  4. springcloud 2.0 服务链路追踪踩坑以及一些小小的理解

    在微服务系统中,随着业务的发展,系统会变得越来越大,这样一来各个服务之间的调用关系也就变得越来越复杂.一个 HTTP 请求会调用多个不同的服务接口来处理返回最后的结果,在这个调用过程中,可能会因为某个 ...

  5. 【SpringCloud从0到6】 第二节:注册中心 - eureka

    从本节开始 , 我们就正式开始编写一个cloud基础微服务了 , 为了代码更简洁及更方便入门 , 在此采用了maven子模块的结构 . 1 . 创建maven主模块 , 配置cloud相关参数 pom ...

  6. 微服务实践之通信(OpenFeign)详解-SpringCloud(2021.0.x)-6

    [版权申明] 非商业目的注明出处可自由转载 出自:shusheng007 首发于:shusheng007.top 文章目录 概述 OpenFeign简介 基本使用 新建provider与consume ...

  7. Spring-Cloud 从0开始(二) Eureka-Client

    前奏 上一篇中我们理解了微服务的基本概念 && Eureka-Server集群的搭建. 当然,有了服务注册中心还远远不够,我们还得有服务消费者,这样服务注册才有存在的价值,在Sprin ...

  8. 微服务实践之量度监控(micrometer,prometheus,grafana)-SpringCloud(2021.0.x)-5

    [版权申明] 非商业目的注明出处可自由转载 出自:shusheng007 文章目录 前言 概念 Prometheus(普罗米修斯) Grafana Micrometer 关键概念 Meter Mete ...

  9. 【SpringCloud从0到6】 第二节:Spring Cloud 和 Dubbo

    1 . 框架背景 当前主流上较常用的java微服务框架 , 就是cloud和dubbo .  dubbo是阿里公司开源的RPC框架 , 背景很优秀 , 技术也是一等一的强 , 在spring clou ...

  10. 【SpringCloud从0到6】 第一节:初识微服务微服务的雪崩效应

    1 . 传统单机服务 概念 : 所有的功能模块全部写在一起 , 打到一个war包里进行发布 , 除了容器(Tomcat等)以外基本没有其他依赖 . 它的结构图如下 : 单机服务中 , 一个模块包含了U ...

最新文章

  1. HTML标签strong和em
  2. MySQL中的行级锁,表级锁,页级锁
  3. Window 7 下的某些服务不能随便禁用! 无法立即删除.exe文件,因为禁用了Application Experience服务。...
  4. Linux中查看负载
  5. Java DataInputStream readBoolean()方法(带示例)
  6. Android(Lollipop/5.0) Material Design(六) 使用图像
  7. 所谓中央空调VRV指的是什么
  8. Nginx日志配置及分割
  9. koa2与php比较,模仿KOA,用php来写一个极简的开发框架
  10. 自动开票失败可能出现的错误信息
  11. 【大数据面试宝典】 第一篇 Hadoop 面试题
  12. JSP简介和会话技术
  13. CSP—— 登机牌条码(多项式的求解以及多项式的除法)
  14. c++运行时报Floating point exception错误
  15. 通用技术金工工艺高考必考必看的知识点含攻丝与套丝讲解
  16. Java Web学习day25------Vue和综合案例
  17. 编写一个函数,判断某个数是否为水仙花数。
  18. python pptx文本提取
  19. 解锁“不可能”:苏门答腊犀牛让我开始重新思考AI
  20. [体检]悲从中来,伤不起

热门文章

  1. 【AJAX 教程】Jquery 实现 AJAX 请求的三种方式
  2. 微积分“小糊涂”,难担大任
  3. ToDesk版本更新,引入RTC传输技术,是否早以替代向日葵远程控制?
  4. 推荐.Net、C# 逆向反编译四大工具利器(请勿用来非法行为)
  5. 银河帝国----迈向基地
  6. LSD源代码编译运行
  7. 8255并行接口与交通灯控制
  8. zookeeper报错Have smaller server identifier, so dropping the connection
  9. VScode插件管理(C/C++)
  10. Stata连享会:分享一大堆资料