当时高并发项目差点让我丢了工作!看我如何配置优化
bigegpt 2024-10-20 04:27 3 浏览
本文主要介绍的微服务是spring cloud,它一个服务治理框架和一系列框架的由序集合,其利用springboot的开发便利性巧妙的简化了分布式系统基础设施的开发,如服务发现注册、负载均衡、断路器、数据监控等,都可以用springboot的开发风格做到一键启动和部署。学习和了解微服务对快速熟悉和掌握不同项目的服务架构大有益处
分为以下几个部分进行介绍:
一、微服务应用的简单业务场景
二、Spring Cloud核心组件:Eureka
三、Spring Cloud核心组件:Feign
四、Spring Cloud核心组件:Ribbon
五、Spring Cloud核心组件:Hystrix
六、Spring Cloud核心组件:Zuul
七、高并发场景下配置优化
八、总结
一、业务场景介绍
先来给大家说一个都熟悉的支付订单的功能,大体的流程如下:
- 用户创建一个订单并支付,需要将订单状态更新为“已支付”
- 更新商品库存
- 通知物流发货
- 增加用户购物积分
上述业务场景的流程涉及到订单服务、库存服务、物流服务、用户积分服务。其调用关系如下图:
二、Spring Cloud核心组件:Eureka
考虑一个问题:订单服务想要调用库存服务、物流服务,或者是积分服务,怎么调用?
- 传统方式:通过传统的ip:port的方式调用各种服务,如果订单服务依赖的其他服务地址更改了,就需要修改订单服务配置,然后重新部署。阿西吧!
- eureka出场,eureka是微服务架构的注册中心,专门负责服务的注册和发现。Eureka有两个组件组成:Eureka server和Eureka client,一张图认识一下:
- 上面的图描述了Eureka的基本架构,有3个角色组成
- Eureka Server:提供服务注册和发现
- Service Provider:服务提供方,将自身服务注册到Eureka Server,从而使消费方能够找到
- Service Consumer:服务消费方,定时从Eureka获取注册服务列表,消费服务
- 再来看一下业务场景中,这些服务上都有Eureka Client组件,Service Provider启动时将自己注册到Eureka Server上,注册的意思就是告诉Eureka Server 自己在哪台服务器,端口号等,Eureka Server 的注册表会保存这些服务的元数据信息。这个时候如果订单服务调用库存服务,Service Consumer通过Eureka Client组件 向Eureka Server 问一下库存服务的ip是什么,端口是什么,然后调用库存服务。订单服务调用其他服务亦是如此。
- 千万级访问量:eureka各个服务默认每隔30秒拉取注册表,每隔30s发起一次心跳,看看Eureka是如何实现千万级别访问量
- 假设现在有一个大型的分布式系统,一共100个服务,每个服务部署在20台机器上,机器是4核8G的标准配置。也就是说,相当于你一共部署了100 * 20 = 2000个服务实例,有2000台机器。每台机器上的服务实例内部都有一个Eureka Client组件,它会每隔30秒请求一次Eureka Server,拉取变化的注册表。此外,每个服务实例上的Eureka Client都会每隔30秒发送一次心跳请求给Eureka Server。
那么大家算算,Eureka Server作为一个微服务注册中心,每秒钟要被请求多少次?一天要被请求多少次?
按标准的算法,每个服务实例每分钟请求2次拉取注册表,每分钟请求2次发送心跳
- 这样一个服务实例每分钟会请求4次,2000个服务实例每分钟请求8000次
- 换算到每秒,则是8000 / 60 = 133次左右,我们就大概估算为Eureka Server每秒会被请求150次
- 那一天的话,就是8000 * 60 * 24 = 1152万,也就是每天千万级访问量
深入原理:
- Eureka server注册数据结构:
- 如上图所示,图中的这个名字叫做registry的CocurrentHashMap,就是注册表的核心结构。各个服务的注册、服务下线、服务故障,全部会在内存里维护和更新这个注册表。
- 各个服务每隔30秒拉取注册表,每隔30s发起一次心跳,都在这Map数据结构中操作
一句话概括:维护注册表、拉取注册表、更新心跳时间,全部发生在内存里!
- 这个ConcurrentHashMap的key就是服务名称,比如“订单服务的名字是:order-service,这里就是order-service”, value则代表了一个服务的多个服务实例。Lease的类,它的泛型是一个叫做InstanceInfo,代表服务实例的具体信息,比如机器的ip地址、hostname以及端口号。而这个Lease,里面则会维护每个服务最近一次发送心跳的时间
- Eureka Server为了避免同时读写内存数据结构造成的并发冲突问题,采用了多级缓存机制(ReadOnlyCacheMap、ReadWriteCacheMap)提升服务请求的响应速度。
在拉取注册表的时候:
- 首先从ReadOnlyCacheMap里查缓存的注册表。
- 若没有,就找ReadWriteCacheMap里缓存的注册表。
- 如果还没有,就从内存中获取实际的注册表数据。
在注册表发生变更的时候:
- 会在内存中更新变更的注册表数据,同时过期掉ReadWriteCacheMap。
- 此过程不会影响ReadOnlyCacheMap提供人家查询注册表。
- 一段时间内(默认30秒),各服务拉取注册表会直接读ReadOnlyCacheMap
- 30秒过后,Eureka Server的后台线程发现ReadWriteCacheMap已经清空了,也会清空ReadOnlyCacheMap中的缓存
- 下次有服务拉取注册表,又会从内存中获取最新的数据了,同时填充各个缓存。
总结一下:
- Eureka Client:负责将这个服务的信息注册到Eureka Server中
- Eureka Server:注册中心,里面有一个注册表,保存了各个服务所在的机器和端口号
三、Spring Cloud核心组件:Feign
实现优雅的rpc调用:解决完服务在哪里之后,该如何去调用服务呢?难道订单服务要自己写一大堆代码,跟其他服务建立网络连接,然后构造一个复杂的请求,接着发送请求过去,最后对返回的响应结果再写一大堆代码来处理吗?想到这里是不是直想挠头,为了保住自己本来就不多的头发!来看看Feign是如何处理的吧!
看完上面的代码是不是感觉干净清爽!没有底层的建立连接、构造请求、解析响应的代码,直接就是用注解定义一个 FeignClient接口,然后调用那个接口就可以了,Feign都帮你做好了。
Feign实现这些的原理:一个关键机制就是使用了动态代理。
- 首先,如果你对某个接口定义了@FeignClient注解,Feign就会针对这个接口创建一个动态代理
- 接着你要是调用那个接口,本质就是会调用 Feign创建的动态代理,这是核心中的核心
- Feign的动态代理会根据你在接口上的@RequestMapping等注解,来动态构造出你要请求的服务的地址
- 最后针对这个地址,发起请求、解析响应
四、Spring Cloud核心组件:Ribbon
服务集群部署:如果库存服务部署了3台怎么办,如下所示:
- 192.168.10.10:7000
- 192.168.10.11:7000
- 192.168.10.13:7000
问题来了,Feign怎么知道该请求哪台机器呢?这时就轮到Ribbon出马了,其提供了解决微服务负载均衡的问题。Ribbon 是一个基于Http和TCP的客服端负载均衡工具,它是基于Netflix Ribbon实现的。它不像spring cloud服务注册中心、配置中心、API网关那样独立部署,但是它几乎存在于每个spring cloud 微服务中。包括feign提供的声明式服务调用也是基于该Ribbon实现的。
- Ribbon的负载均衡默认使用的最经典的Round Robin轮询算法。如果订单服务对库存服务发起10次请求,那就先让你请求第1台机器、然后是第2台机器、第3台机器,接着再来—个循环,第1台机器、第2台机器。。。以此类推。其他常见的负载均衡策略还包括权重轮询WeightedResponseTimeRule,随机策略RandomRule,最少并发数策略BestAvailableRule等等
此外,Ribbon是和Feign以及Eureka紧密协作,完成工作的,具体如下:
- 首先Ribbon会从 Eureka Client里获取到对应的服务注册表,也就知道了所有的服务都部署在了哪些机器上,在监听哪些端口号。
- 然后Ribbon就可以使用默认的Round Robin算法,从中选择一台机器
- Feign就会针对这台机器,构造并发起请求
对上述整个过程,再来一张图:
五、Spring Cloud核心组件:Hystrix
断路器,旨在通过熔断机制控制服务和其依赖服务之间的延迟和故障。比如:在一个大型的微服务架构里,一个服务可能要依赖很多服务,像本文的业务场景,订单服务依赖库存服务、物流服务、积分服务三个服务。假设订单服务最多只有100个线程可以处理请求,如果库存服务挂了,一般会抛出一个异常。如果系统处于高并发的场景下,大量请求涌过来的时候,订单服务的100个线程都会卡在请求库存服务这块,这导致订单服务没有一个线程可以处理请求,服务器完全不响应任何请求。
如上图,这么多服务互相调用,要是不做任何保护的话,某一个服务挂了,就会引起连锁反应,导致别的服务也挂。比如服务B挂了,会导致服务A的线程全部卡在请求服务B这里,没有一个线程可以工作。上面这个,就是微服务架构中恐怖的服务雪崩问题。
- 解决方式:
使用Hystrix。Hystrix是隔离、熔断以及降级的一个框架。Hystrix处理方式是提供很多个小小的线程池(其中一种解决方式),比如订单服务请求库存服务是一个线程池,请求物流服务是一个线程池,请求积分服务是一个线程池。每个线程池里的线程就仅仅用于请求那个服务。
- 打个比方:现在很不幸,服务B挂了,会咋样?
当然会导致服务A的那个用来调用服务B的线程都卡死不能工作了啊!但是由于服务A调用服务C、服务D的这两个线程池都是正常工作的,所以这两个服务不会受到任何影响。但是如果服务B都挂了,服务A每次调用都要去卡住几秒钟干啥呢?有意义吗?当然没有!所以我们直接对服务B熔断不就得了,比如在5分钟内请求积分服务直接就返回了,不要去走网络请求卡住几秒钟,这个过程,就是所谓的熔断!如果服务A知道服务B已经熔断了,可以做一些特殊的处理,比如返回一些兜底的数据或者友好的提示,这个过程,就是所谓的降级。
为帮助大家更直观的理解,接下来用一张图,梳理一下Hystrix隔离、熔断和降级的全流程:
六、Spring Cloud核心组件:Zuul
前面的文章我们介绍了,Eureka用于服务的注册于发现,Feign和Ribbon支持服务的调用以及均衡负载,Hystrix处理服务的熔断防止故障扩散。我们还是少考虑了一个问题,外部的应用如何来访问内部各种各样的微服务呢?在微服务架构中,后端服务往往不直接开放给调用端,而是通过一个API网关根据请求的url,路由到相应的服务。当添加API网关后,在第三方调用端和服务提供方之间就创建了一面墙,这面墙直接与调用方通信进行权限控制,后将请求均衡分发给后台服务
为什么需要微服务网关?
在微服务架构模式下后端服务的实例数一般是动态的,动态改变的服务实例的访问地址信息很难被客户端发现。因此在基于微服务的项目中为了简化前端的调用逻辑,引入API Gateway作为轻量级网关,而且有一个网关之后,还有很多好处,比如可以做统一的降级、限流、认证授权、安全,等等。
七、高并发下配置优化
- 合理的hystrix线程池大小:
假设你的服务A,每秒钟会接收30个请求,同时会向服务B发起30个请求,然后每个请求的响应时长经验值大概在200ms,那么你的hystrix线程池需要多少个线程呢?计算公式是:30(每秒请求数量) * 0.2(每个请求的处理秒数) + 4(给点缓冲buffer) = 10(线程数量)。
为什么10个线程可以轻松抗住每秒30个请求?
一个线程200毫秒可以执行完一个请求,那么一个线程1秒可以执行5个请求,理论上,只要6个线程,每秒就可以执行30个请求。也就是说,线程里的10个线程中,就6个线程足以抗住每秒30个请求了。剩下4个线程都在玩儿,空闲着。那为啥要多搞4个线程呢?很简单,因为你要留一点buffer空间。万一在系统高峰期,系统性能略有下降,此时不少请求都耗费了300多毫秒才执行完,那么一个线程每秒只能处理3个请求了,10个线程刚刚好勉强可以hold住每秒30个请求。所以你必须多考虑留几个线程。
- 合理的超时时间设置
一个接口,理论的最佳响应速度应该在200ms以内,或者慢点的接口就几百毫秒。如果一个接口响应时间达到1秒+,建议考虑用缓存、索引、NoSQL等各种你能想到的技术手段,优化一下性能。否则你要是胡乱设置超时时间是几秒,甚至几十秒,万一下游服务偶然出了点问题响应时间长了点呢?那你这个线程池里的线程立马全部卡死!合理的超时时间设置为多少?答案应该不超过300毫秒(这个需要根据实际压测数据做些优化)。为啥呢?如果你的超时时间设置成了500毫秒,想想可能会有什么后果?考虑极端情况,如果服务B响应变慢,要500毫秒才响应,你一个线程每秒最多只能处理2个请求了,10个线程只能处理20个请求。大量的线程会全部卡死,来不及处理那么多请求,最后用户会刷不出来页面。如果你的线程池大小和超时时间没有配合着设置好,很可能会导致服务B短暂的性能波动,瞬间导致服务A的线程池卡死,里面的线程要卡顿一段时间才能继续执行下一个请求。哪怕一段时间后,服务B的接口性能恢复到200毫秒以内了,服务A的线程池里卡死的状况也要好一会儿才能恢复过来。你的超时时间设置的越不合理,比如设置的越长,设置到了1秒、2秒,那么这种卡死的情况就需要越长的时间来恢复。所以说,此时你的超时时间得设置成300毫秒,保证一个请求300毫秒内执行不完,立马超时返回。这样线程池里的线程不会长时间卡死,可以有条不紊的处理多出来的请求,大不了就是300毫秒内处理不完立即超时返回,但是线程始终保持可以运行的状态。这样当服务B的接口性能恢复到200毫秒以内后,服务A的线程池里的线程很快就可以恢复。
八、总结:
总结一下,上述几个Spring Cloud核心组件,在微服务架构中,分别扮演的角色:
- Eureka:各个服务在启动时,Eureka Client组件会将本服务注册到Eureka Server,集群以应用名做区分,并且Eureka Client还可以反过来从Eureka Server拉取注册表,从而知道其他服务在哪里
- Ribbon:微服务之间发起请求的时候,基于Ribbon做负载均衡,从服务集群中选择一台
- Feign:基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求
- Hystrix:发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题
- Zuul:微服务路由
当然spring cloud家族还有Spring Cloud Config、Spring Cloud Bus、Spring Cloud for Cloud Foundry、Spring Cloud Cluster、Spring Cloud Consul、Spring Cloud Security、Spring Cloud Sleuth、Spring Cloud Data Flow、Spring Cloud Stream、Spring Cloud Task、Spring Cloud Zookeeper、Spring Cloud Connectors、Spring Cloud Starters、Spring Cloud CLI等等,具体信息请查看:
中文文档:https://springcloud.cc/
英文文档:http://spring.io/projects/spring-cloud
来源:网易工程师-周延旭
有任何问题欢迎留言交流~
整理总结不易,如果觉得这篇文章有意思的话,欢迎转发、收藏,给我一些鼓励~
有想看的内容或者建议,敬请留言!
最近利用空余时间整理了一些精选Java架构学习视频和大厂项目底层知识点,需要的同学欢迎私信我发给你~一起学习进步!有任何问题也欢迎交流~
Java日记本,每日存档超实用的技术干货学习笔记,每天陪你前进一点点~
相关推荐
- Java 泛型大揭秘:类型参数、通配符与最佳实践
-
引言在编程世界中,代码的可重用性和可维护性是至关重要的。为了实现这些目标,Java5引入了一种名为泛型(Generics)的强大功能。本文将详细介绍Java泛型的概念、优势和局限性,以及如何在...
- K8s 的标签与选择器:流畅运维的秘诀
-
在Kubernetes的世界里,**标签(Label)和选择器(Selector)**并不是最炫酷的技术,但却是贯穿整个集群管理与运维流程的核心机制。正是它们让复杂的资源调度、查询、自动化运维变得...
- 哈希Hash算法:原理、应用(哈希算法 知乎)
-
原作者:Linux教程,原文地址:「链接」什么是哈希算法?哈希算法(HashAlgorithm),又称为散列算法或杂凑算法,是一种将任意长度的数据输入转换为固定长度输出值的数学函数。其输出结果通常被...
- C#学习:基于LLM的简历评估程序(c# 简历)
-
前言在pocketflow的例子中看到了一个基于LLM的简历评估程序的例子,感觉还挺好玩的,为了练习一下C#,我最近使用C#重写了一个。准备不同的简历:image-20250528183949844查...
- 55顺位,砍41+14+3!季后赛也成得分王,难道他也是一名球星?
-
雷霆队最不可思议的新星:一个55号秀的疯狂逆袭!你是不是也觉得NBA最底层的55号秀,就只能当饮水机管理员?今年的55号秀阿龙·威金斯恐怕要打破你的认知了!常规赛阶段,这位二轮秀就像开了窍的天才,直接...
- 5分钟读懂C#字典对象(c# 字典获取值)
-
什么是字典对象在C#中,使用Dictionary类来管理由键值对组成的集合,这类集合被称为字典。字典最大的特点就是能够根据键来快速查找集合中的值,其键的定义不能重复,具有唯一性,相当于数组索引值,字典...
- c#窗体传值(c# 跨窗体传递数据)
-
在WinForm编程中我们经常需要进行俩个窗体间的传值。下面我给出了两种方法,来实现传值一、在输入数据的界面中定义一个属性,供接受数据的窗体使用1、子窗体usingSystem;usingSyst...
- C#入门篇章—委托(c#委托的理解)
-
C#委托1.委托的定义和使用委托的作用:如果要把方法作为函数来进行传递的话,就要用到委托。委托是一个类型,这个类型可以赋值一个方法的引用。C#的委托通过delegate关键字来声明。声明委托的...
- C#.NET in、out、ref详解(c#.net framework)
-
简介在C#中,in、ref和out是用于修改方法参数传递方式的关键字,它们决定了参数是按值传递还是按引用传递,以及参数是否必须在传递前初始化。基本语义对比修饰符传递方式可读写性必须初始化调用...
- C#广义表(广义表headtail)
-
在C#中,广义表(GeneralizedList)是一种特殊的数据结构,它是线性表的推广。广义表可以包含单个元素(称为原子),也可以包含另一个广义表(称为子表)。以下是一个简单的C#广义表示例代...
- 「C#.NET 拾遗补漏」04:你必须知道的反射
-
阅读本文大概需要3分钟。通常,反射用于动态获取对象的类型、属性和方法等信息。今天带你玩转反射,来汇总一下反射的各种常见操作,捡漏看看有没有你不知道的。获取类型的成员Type类的GetMembe...
- C#启动外部程序的问题(c#怎么启动)
-
IT&OT的深度融合是智能制造的基石。本公众号将聚焦于PLC编程与上位机开发。除理论知识外,也会结合我们团队在开发过程中遇到的具体问题介绍一些项目经验。在使用C#开发上位机时,有时会需要启动外部的一些...
- 全网最狠C#面试拷问:这20道题没答出来,别说你懂.NET!
-
在竞争激烈的C#开发岗位求职过程中,面试是必经的一道关卡。而一场高质量的面试,不仅能筛选出真正掌握C#和.NET技术精髓的人才,也能让求职者对自身技术水平有更清晰的认知。今天,就为大家精心准备了20道...
- C#匿名方法(c#匿名方法与匿名类)
-
C#中的匿名方法是一种没有名称只有主体的方法,它提供了一种传递代码块作为委托参数的技术。以下是关于C#匿名方法的一些重要特点和用法:特点省略参数列表:使用匿名方法可省略参数列表,这意味着匿名方法...
- C# Windows窗体(.Net Framework)知识总结
-
Windows窗体可大致分为Form窗体和MDI窗体,Form窗体没什么好细说的,知识点总结都在思维导图里面了,下文将围绕MDI窗体来讲述。MDI(MultipleDocumentInterfac...
- 一周热门
- 最近发表
- 标签列表
-
- mybatiscollection (79)
- mqtt服务器 (88)
- keyerror (78)
- c#map (65)
- resize函数 (64)
- xftp6 (83)
- bt搜索 (75)
- c#var (76)
- mybatis大于等于 (64)
- xcode-select (66)
- mysql授权 (74)
- 下载测试 (70)
- linuxlink (65)
- pythonwget (67)
- androidinclude (65)
- logstashinput (65)
- hadoop端口 (65)
- vue阻止冒泡 (67)
- oracle时间戳转换日期 (64)
- jquery跨域 (68)
- php写入文件 (73)
- kafkatools (66)
- mysql导出数据库 (66)
- jquery鼠标移入移出 (71)
- 取小数点后两位的函数 (73)