基于互联网架构演进,构建秒杀系统

网站建设4年前发布
101 0 0

以用户为中心,提供快速的网页访问体验。主要参数有较短的响应时间、较大的并发处理能力、较高的吞吐量与稳定的性能参数。,可分为前端优化、应用层优化、代码层优化与存储层优化。,①服务尽量进行拆分(微服务)---- 提高项目吞吐能力。,②尽量将请求拦截在上游服务(多级缓存)--- 90% ----> 数据库压力非常小,闲庭信步,数据库架构(主从架构)。,③代理层:做限速,限流。,④服务层:按照业务请求做队列的流量控制(流量削峰)。,伸缩性是指在不改变原有架构设计的基础上,通过添加/减少硬件(服务器)的方式,提高/降低系统的处理能力。,云原生:项目运行云端,可以随时动态扩容—K8S。,8C+16G : 2000QPS +-。, (此数字是估算结果,真实结果受到代码编写数据结构,业务逻辑,架构、rt,以现实测试结果)。,SOA --- 微服务 --- 业务拆分模块 --- 新业务需求 --- 根据新业务需求创建新模块服务。,可以方便地进行功能模块的新增/移除,提供代码/模块级别良好的可扩展性。,对已知问题有有效的解决方案,对未知/潜在问题建立发现和防御机制。对于安全问题,首先要提高安全意识,建立一个安全的有效机制,从政策层面,组织层面进行保障,比如服务器密码不能泄露,密码每月更新,每周安全扫描等。,以制度化的方式,加强安全体系的建设。同时,需要注意与安全有关的各个环节。安全问题不容忽视,包括基础设施安全,应用系统安全,数据保密安全等。,常用的加解密算法(单项散列加密[MD5、SHA],对称加密[DES、3DES、RC]),非对称加密[RSA]等。,单体架构(all in one) à水平拆分/SOA架构à微服务架构 àkubernetes云原生架构(微服务迁移到云原生)à ServiceMesh (服务网格架构,下一代微服务架构,云原生架构:istio) à serverless 架构 (无服务架构)。,企业架构转型:数字化转型。,传统架构过渡到云原生架构(容器云)。,(1)单体架构——所有业务都在同一个应用中,没有进行任何拆分。,注意:集中式架构模式,所有的请求都集中在同一个服务上面,对服务压力较大;因此这样的架构适合并发较小的架构;同时 同一个服务器中,数据库,项目都会抢占服务内存,cpu资源,造成服务性能问题。,(2)单体架构优化。,(3)单体架构流量预估(单体架构真的不能承受亿级流量??)单体架构:中小型企业,创业公司。,某网站平均一天下单量100w单,根据100w 评估一下系统的流量!,①产生的时间段:11:00 – 2:00  5:00 – 12:00 ,订单产生时间段:12h。,②每下一单会发生多少个请求:50QPS x 3 = 150 QPS。,100w / day * 150 QPS = 1.5 亿 ----- 亿级流量。,1.5亿/12 h = 1250 QPS / 60min = 20W / 60s = 3400 QPS。,(4)单点架构优缺点。,单体架构优点:,随着业务流量增大,需求的增多,必须对架构进行改进,就需要对项目进行业务拆分;(水平拆分,垂直拆分)。,数据库水平拆分,垂直拆分模式:,(1)水平拆分模式。,(2)垂直拆分:SOA架构。,注意:微服务架构就是水平拆分和垂直拆分的架构结合,就是微服务架构。,ServiceMesh服务网格架构,CNCF把ServiceMesh定义为云原生架构,ServiceMesh落地级实现的成熟框架:Istio框架。,问题:为什么要是有ServiceMesh架构?,Spring Cloud alibaba微服务架构存在问题?,--ServiceMesh出现就是为了解决微服务架构中存在一些问题?,以上一系列的问题,作为架构师,开发人员都需要全盘的考虑;开发微服务架构在服务治理,服务监控非常困难。,以上的工作和业务没有太多的关系,但是架构人员必须考虑,架构,设计,因此这些配套工作都会大大降低我们的开发效率,提升开发难度,增加开发成本。,Serverless架构体系:无服务架构,面向未来的架构体系,从开发人员来说,不需要关心底层哪些和业务没有关系的代码,只需要开发业务即可。,例如:向CDN上传图片,视频文件。,这样的概念,思想就叫做Serverless。,总结:架构选型的时候,必须选择企业合适的架构,而不是采用最新架构。,(1)jvm 堆内存空间对象太多(Java线程,垃圾对象),导致内存被占满,程序跑不动—性能严重下降。,调优:及时释放内存,(2)垃圾回收线程太多,频繁回收垃圾(垃圾回收线程也会占用内存资源,抢占cpu资源),必然会导致程序性能下降。,调优:防止频繁gc。,(3)垃圾回收导致stw(stop the world)。,调优:尽可能的减少gc次数。,(是否有这么大的空间)。,Jvm堆内存空间大小的设置:必须设置一个合适的内存空间,不能太大,也不能太小。,(1) gc的时间足够小(堆内存设置足够小)。,垃圾回收时间足够小,以为着jvm堆内存空间设置小一些,这样的话 垃圾对象寻址的时候消耗的时间就非常短,然后整个垃圾回收非常快速。,(2) gc的次数足够少 (jvm堆内存设置的足够大)。,Gc次数足够少,jvm堆内存空间必须设置的足够大;这样垃圾回收触发次数就会相应减少。,注意:原子1 ,原则2 相互冲突的,原则1&&原则2 。需要进行balance,内存空间既不能设置太大,也不能设置太小。,(3) 发生fullgc 周期足够长 (最好不发生full gc)。,JVM调优的本质:回收垃圾,及时释放内存空间。,但是什么是垃圾?,在内存中间中,哪些没有被引用的对象就是垃圾(高并发模式下,大量的请求在内存空间中创建了大量的对象,这些对象并不会主动消失,因此必须进行垃圾回收,当然Java垃圾回收必须我们自己编写垃圾回收代码,Java提供各种垃圾回收器帮助回收垃圾,JVM垃圾回收是自动进行的)。,一个对象的引用消失了,这个对象就是垃圾,因此此对象就必须被垃圾回收器进行回收,及时释放内存空间。,Jvm提供了2种方式找到这个垃圾对象:,引用计数算法:对每一个对象的引用数量进行一个计数,当引用数为0时,那么此对象就变成了一个垃圾对象。,存在问题:不能解决循环引用的问题,如果存在循环引用的话,无法发现垃圾。,这三个对象处于循环引用的状态,引用计数都不为0,因此无法判断这个3个对象是垃圾。,根据根对象向下进行遍历,如果遍历不到的对象就是垃圾。,JVM提供了3种方式清除垃圾,分别是:,①第一种算法:mark-sweep 标记清楚算法。,优点:简单,高效。,缺点:清除的对象都不是一个连续的空间,清除垃圾后,产生很多内存碎片;不利于后期对象内存分配,及寻址。,②第二种算法:copying拷贝算法。,一开始就把内存控制一份为2,分为2个大小相同的的内存空间,另一半空间展示空闲:,优点:简单,内存空间是连续的,不存在内存空间碎片。,缺点:内存空间浪费。,③第三种算法:mark-compact标记整理(压缩)算法。,Java提供很多的垃圾回收器:10种垃圾回收器。,特点:,常用的垃圾回收器组合:,Serial : 年轻代的垃圾回收器,单线程的垃圾回收器;Serial Old是老年代的垃圾回收器,也是一个单线程的垃圾回收器,合适单核心cpu。,Parallel Scavenge + Parallel Old,Parallel Scavenge + Parallel Old : ,并行的垃圾回收器;吞吐量优先的垃圾回收器组合,是JDK8默认的垃圾回收器;问题 : 什么是并发,并行?,PS + PO 回收垃圾的时候,采用的多线程模式回收垃圾。,parNew : 并行垃圾回收器,年轻代的垃圾回收器。,CMS : 并发垃圾回收器,回收老年代的垃圾。,年轻代垃圾回收器:parNew。,老年代垃圾回收器:CMS。,注意:任何的垃圾回收器都无法避免 STW ,因此jvm调优实际上就是调整stw的时间。,使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小。,而定,整体被控制 在1MB到32MB之间,且为2的N次幂,即1MB,2MB,4MB,8MB,16MB,32MB。可以通过-XX:G1HeapRegionsize设定。,所有的Region大小相同,且在JVM生命周期内不会被改变。,通过内存分代模型结构:大多数对象都会在年轻代被回收掉(90%+),很多对象都在15次的垃圾回收中被回收掉了,只有超过15次还没被回收掉的才会进入到老年代区域。,1、ps+po : 当堆内存被装满了,才会触发垃圾回收(eden区域满了,触发了垃圾回收,old区域满了,触发垃圾回收)。,2、cms 垃圾回收器。,一个新对象被创建了,但是这个对象是一个大对象(查询全表),eden区域已经放不下了,此时会发生什么?,明确:jvm调优本质,服务器硬件配置:4cpu,8GB内存 --- jvm调优内存,考虑内存。,压力测试:查看在此内存设置模式下性能情况:,根据压力测试结果,发现JVM参数设置,和之前没有设置吞吐能力没有太大的变化,因为测试样本不足以造成 gc,fullgc时间上差异。,问题:根据什么标准判断参数设置是否合理呢??根据什么指标进行调优呢?1、发生几次gc, 是否频繁的发送gc?2、是否发生fullgc ,full gc发生是否合理3、gc的时间是否合理4、oom。,输出日志启动指令如下所示:,输出日志指令:,执行启动指令后,在本地产生gc.log文件:,GC日志分析: 使用https://gceasy.io/导入gc.log 进行在线分析即可。,Gc日志分析报告:,总结:可以发现 业务线程执行时间占比达到99%+,说明gc时间在整个业务执行期间所占用的时间非常少,几乎不会影响程序性能;导致业务线程执行时间占比高的原因是:,存在问题:发生full gc。,GC详细数据分析:,查询gc内存模型:jstat -gcutil PID  查询此进程的内存模型。,Metaspace永久代空间:默认为20m(初始化大小);当metaspace被占满后,就会发生扩容,一旦metaspace发生一次扩容,就会同时发送一次fullgc 。,Sun公司推荐设置:年轻代占整个堆内存 3/8。,发现full gc 已经没有发生了。,Sun公司推荐设置:整个堆的大小=年轻代 + 老年代 + 永久代(256m)年轻代占整个堆内存3/8 , -Xmx4000m , 因此整个堆内存设置大小为4000m,也就是说年轻代大小应该设置为1.5G:,年轻代大小,老年代大小比值根据业务实际情况设置比例,(通过设置相应的比例:减少相应yonggc ,fullgc)。,JVM调优的原则中:要求尽量防止fullgc的发生;因此可以把fullgc设置的稍微大一些;以为old区域装载对象很长时间才能装满(或者永远都装不满),发生fullgc概率就非常小。,官方给定设置:可以设置eden,s区域大小:8:1:1 à  -XX:SurvivorRatio = 8。,此调优的原理:尽量让对象在年轻代被回收;调大了eden区域的空间,让更多对象进入到eden区域,触发gc时候,更多的对象被回收。,可以发现业务占比时间发送提升,说明gc时间更少了。,总结:JVM调优(调整内存大小、比例) 降低 gc次数,减少gc时间,从而提升服务性能。,调优标准:项目上线后,遇到问题,调优。,并行的垃圾回收器:parallel scavenge(年轻代) + parallel old(老年代) ---- 是JDK默认的垃圾回收器。,显式的配置PS+PO垃圾回收器:-XX:+UseParallelGC -XX:+UseParallelOldGC。,并行垃圾回收器(年轻代),并发垃圾回收器(老年代) :ParNew + CMS (响应时间优先垃圾回收器)。,显式配置:parNew+CMS垃圾回收器组合:-XX:+UseParNewGC。,-XX:+UseConcMarkSweepGC。,说明:CMS只有再发生fullgc的时候才起到作用,CMS一般情况下不会发生;因此在jvm调优原则中表示尽量防止发生fullgc; 因此CMS在JDK14被已经被废弃。,G1垃圾回收器是逻辑上分代模型,使用配置简单。,经过测试,发现g1 gc次数减少,由原来的28次减少为21次,但是gc总时长增加很多;时间增加,以为着服务性能就没有提升上去。,(1)避免网页出现错误。,(2)增加数据库稳定性。,(3)优化用户体验。,数据库数据处理(困难):数据库扩容非常困难—想要通过扩容提升数据库性能Web服务器扩容是非常简单的。,web服务器是无状态服务,可以随时进行扩容;但是数据库不能随意进行扩容,一旦扩容就会影响数据完整性,数据一致性;项目架构中提升性能:,大多数企业:数据库采用主从架构解决问题;数据分表,分库,数据归档数据,能热分离。,试验:10 connections , 40w个样本进行测 ,TPS = 22000 TPS。,经过测试:connection=20, connection=50都进行了测试,发现当connection=20的时候,性能已经下降了,此时TPS=18000 TPS, 当connection=50的时候,TPS = 12000 TPS。,经过测试:连接池最合理的连接数量设置:[10-15]。,connectionTimeout : 配置建立TCP连接的超时时间 ,客户端和mysql建立连接超时,断开连接(释放连接)。,sockettimeout: 配置发送请求后等待响应的超时时间;(客户端和mysql建立连接是socket连接, 一旦发送网络异常,客户端无法感知,一直阻塞状态,一直等待服务端给相应结果,其实由于网络异常,这个链接变成死链接)。,单体架构:,秒杀系统,mysql都会抢占同一个服务器cpu资源,内存资源;一旦cpu资源,内存资源出现满负荷状态,就会影响服务性能。,分离部署:,通过分离部署后,发现性能提升非常不明显,因为无论是在单机,还是在分布式情况下。机器性能都不是满负荷运作的情况。,从上往下看:openresty是否会存在性能瓶颈,目前来看性能瓶颈不在openresty, 因为openresty(nginx) 底层使用c语言开发的,吞吐能力5w TPS。,性能瓶颈一定出现在项目,数据库这个位置。,项目优化:扩容,缓存。,数据库优化:扩容,数据库其他优化。,此时此刻对这个架构进行TPS 预测:TPS = 1600。,主要内容:多级缓存(堆内缓存,分布式缓存,接入层缓存,lua+redis缓存)。,在系统架构设计中,多级缓存非常重要,尤其是构建亿级流量的系统,缓存是必不可少优化选项;因此缓存可以成倍的提升系统性能(吞吐能力),使用了缓存后,尽可能把请求拦截在上游服务器(缓存中:缓存数据命中,直接返回,不在访问后端服务器),因此下游服务器来说,压力就会变小。,在系统架构中应该使用那些缓存:,在本系统中实现缓存是:,思考:JVM进程级别的缓存(缓存数据放入jvm堆内存中),存在以下问题?,答案:,分布式缓存:Redis --- AP模型,在海量的缓存数据中,存储一定概率的数据丢失。,接入层缓存:openresty+lua。,创建一个guavaCache对象:把对象交给spring管理。,缓存业务实现:,
,二级缓存(堆内存缓存,redis缓存):对于系统来说性能提升情况如何?,根据压力测试结果显示:TPS吞吐能力提升效果相当显著。,没有缓存:TPS = 800 , 加缓存:TPS = 26000RT响应时间:100ms左右,基本上满足接口性能需求。,本小节中,探索openresty接入层缓存,使用openresty内存字典来实现接入层缓存;如果缓存数据在接入层命中,后端服务器就不会再收到请求了。,lua接入指令:https://www.nginx.com/resources/wiki/modules/lua/#directives。,(1)开启openresty内存字典。,lua_shared_dict ngx_cache 128m; # 在openresty服务器开辟一块128m空间存储缓存数据。,(2)lua脚本方式,实现缓存接入。,经过内存字典缓存的部署后,发现TPS = 55000。,RT响应时间也是非常之快速。,Openresty内部集成的lua(lua是一个脚本语言,lua+openresty(集成luaJIT)实现lua脚本解释执行),缓存架构如下所示:,说明:使用lua+redis缓存结构,尽可能把请求拦截在上游服务器,减轻后端服务器压力,提升项目吞吐能力。,Lua脚本:mysql,redis, mongdb, es ……. , 说明可以直接使用lua+openresty构建高并发性能的网站。,OpenResty集成Redis库:使用lua脚本操作Redis,只需要引入Redis库即可实现:,如何使用redis库文件:lua脚本中引入lua库,即可使用lua库中函数方法。,开发:lua+redis缓存实现。,总结:之前经过服务器优化实现,jvm优化实现,数据库连接池优化实现,多级缓存优化,部署拓扑结构变化对性能影响—压力测试验证优化结果;--- 这些优化操作都是对读操作进行的优化。,系统中:写操作进行优化 --- 具体涉及到的业务:下单实现。,秒杀下单业务分析(业务问题,性能,一致性问题)解决下单操作业务问题(锁—锁优化)。,前提:一系列的验证(身份信息,token,手机号,商品信息是否上架,是否是秒杀商品,商品状态,库存是否ok,活动是否开始…..)。,秒杀实现,业务上是非常之简单的,但是在高并发压力下,也面临一系列的挑战。,思考题:超卖产生的原因是什么?,请提出解决方案,如何避免超卖现象的发生呢?答案:,加锁目的:防止多个线程对共享资源的并发修改;一旦加锁,多个线程就进行排队执行,因此在高并发模式,这样的操作是一个灾难;明确:任何的加锁动作,都会导致性能急剧下降。,队列的特点:,对普通下单:没有上锁的操作,验证库存超卖的现象;超卖现象非常严重,1000个库存,超卖了843个库存。,因此现在加锁:控制共享资源库存防止并发修改。,以上加锁操作无法控制库存, 原因是什么?,经过验证,发现加lock锁,没有控制住库存。,出现以上的原因:锁和事务冲突;导致此时这个锁根本不起作用。,问题:事务何时提交的?,问题:针对于以上问题(锁事务冲突的问题),你的解决方案是什么?,解决方案:锁上移 (表现层,AOP锁(√))。,AOP锁实现:,使用1000个样本经过多次测试,发现库存都可以进行完美的控制,因此aop锁可以实现库存控制的,不会出现超卖的问题。,原则上,构建完整一个系统,整体思路上还需考虑压力测试、分布式环境下数据一致性、接口幂等性问题,在此就不赘述。,实现压力测试(及时发现系统的问题,发现系统性能瓶颈),根据压力测试结果对系统进行优化,问题修复,压力测试验证性能是否有提升;服务端优化(tomcat服务器优化,undertow服务器优化),压力测试验证性能提升结果。,另外涉及Kubernetes原生迁移也是一项架构师领域考虑的问题。甚至全局规划人力成本这一计算很复杂的课题,会涉及时间成本、代码量成本、需求管理成本等各类成本。

© 版权声明

相关文章