后端架构
架构设计和优化
秒杀系统为什么复杂?
1)im系统,例如qq或者微博,每个人都读自己的数据(好友列表、群列表、个人信息);
2)微博系统,每个人读你关注的人的数据,一个人读多个人的数据;
3)秒杀系统,库存只有一份,所有人会在集中的时间读和写这些数据,多个人读一个数据。
例如:小米手机每周二的秒杀,可能手机只有1万部,但瞬时进入的流量可能是几百几千万。 又例如:12306抢票,票是有限的,库存一份,瞬时流量非常多,都读相同的库存。读写冲突,锁非常严重,这是秒杀业务难的地方。
参考:
如何优化秒杀架构?
首先了解秒杀系统为什么复杂?
优化的方式有以下几个点。
第一层,客户端节流。
(a)产品层面,用户点击“查询”或者“购票”后,按钮置灰,禁止用户重复提交请求;
(b)JS层面,限制用户在x秒之内只能提交一次请求;
这样就能挡住80%+的请求,这种办法只能拦住普通用户(但99%的用户是普通用户)对于群内的高端程序员是拦不住的。firebug一抓包,http长啥样都知道,js是万万拦不住程序员写for循环,调用http接口的,这部分请求怎么处理?
第二层,后端站点层面限流。
在站点层面,对uid进行请求计数和去重,甚至不需要统一存储计数,直接站点层内存存储(这样计数会不准,但最简单)。一个uid,5秒只准透过1个请求,这样又能拦住99%的for循环请求。
5s只透过一个请求,其余的请求怎么办?缓存,页面缓存,同一个uid,限制访问频度,做页面缓存,x秒内到达站点层的请求,均返回同一页面。同一个item的查询,例如车次,做页面缓存,x秒内到达站点层的请求,均返回同一页面。如此限流,既能保证用户有良好的用户体验(没有返回404)又能保证系统的健壮性(利用页面缓存,把请求拦截在站点层了)。
页面缓存不一定要保证所有站点返回一致的页面,直接放在每个站点的内存也是可以的。优点是简单,坏处是http请求落到不同的站点,返回的车票数据可能不一样,这是站点层的请求拦截与缓存优化。
好,这个方式拦住了写for循环发http请求的程序员,有些高端程序员(黑客)控制了10w个肉鸡,手里有10w个uid,同时发请求(先不考虑实名制的问题,小米抢手机不需要实名制),这下怎么办,站点层按照uid限流拦不住了。
第三层,服务层拦截。
对于写请求,做请求队列,每次只透有限的写请求去数据层(下订单,支付这样的写业务)
1w部手机,只透1w个下单请求去db
3k张火车票,只透3k个下单请求去db
如果均成功再放下一批,如果库存不够则队列里的写请求全部返回“已售完”。
对于读请求,怎么优化?cache抗,不管是memcached还是redis,单机抗个每秒10w应该都是没什么问题的。如此限流,只有非常少的写请求,和非常少的读缓存mis的请求会透到数据层去,又有99.9%的请求被拦住了。
当然,还有业务规则上的一些优化。回想12306所做的,分时分段售票,原来统一10点卖票,现在8点,8点半,9点,...每隔半个小时放出一批:将流量摊匀。
其次,数据粒度的优化:你去购票,对于余票查询这个业务,票剩了58张,还是26张,你真的关注么,其实我们只关心有票和无票?流量大的时候,做一个粗粒度的“有票”“无票”缓存即可。
第三,一些业务逻辑的异步:例如下单业务与 支付业务的分离。这些优化都是结合 业务 来的,我之前分享过一个观点“一切脱离业务的架构设计都是耍流氓”架构的优化也要针对业务。
参考:
如何进行容量设计?
容量设计的场景如下:
场景一:pm要做一个很大的运营活动,技术老大杀过来,问了两个问题:
(1)机器能抗住么?
(2)如果扛不住,需要加多少台机器?
场景二:系统设计阶段,技术老大杀过来,又问了两个问题:
(1)数据库需要分库么?
(2)如果需要分库,需要分几个库?
常见的容量评估包括数据量、并发量、带宽、CPU/MEM/DISK等。
容量设计的步骤如下:
【步骤一:评估总访问量】
如何知道总访问量?对于一个运营活动的访问量评估,或者一个系统上线后PV的评估,有什么好的方法?
答案是:询问业务方,询问运营同学,询问产品同学,看对运营活动或者产品上线后的预期是什么。
举例:58要做一个APP-push的运营活动,计划在30分钟内完成5000w用户的push推送,预计push消息点击率10%,求push落地页系统的总访问量?
回答:5000w*10% = 500w
【步骤二:评估平均访问量QPS】
如何知道平均访问量QPS?
答案是:有了总量,除以总时间即可,如果按照天评估,一天按照4w秒计算。
举例1:push落地页系统30分钟的总访问量是500w,求平均访问量QPS
回答:500w/(30*60) = 2778,大概3000QPS
举例2:主站首页估计日均pv 8000w,求平均访问QPS
回答:一天按照4w秒算,8000w/4w=2000,大概2000QPS
提问:为什么一天按照4w秒计算?
回答:一天共24小时60分钟60秒=8w秒,一般假设所有请求都发生在白天,所以一般来说一天只按照4w秒评估
【步骤三:评估高峰QPS】
系统容量规划时,不能只考虑平均QPS,而是要抗住高峰的QPS,如何知道高峰QPS呢?
答案是:根据业务特性,通过业务访问曲线评估
举例:日均QPS为2000,业务访问趋势图如下图,求峰值QPS预估?
回答:从图中可以看出,峰值QPS大概是均值QPS的2.5倍,日均QPS为2000,于是评估出峰值QPS为5000。
说明:有一些业务例如“秒杀业务”比较难画出业务访问趋势图,这类业务的容量评估不在此列。
【步骤四:评估系统、单机极限QPS】
如何评估一个业务,一个服务单机能的极限QPS呢?
答案是:压力测试
在一个服务上线前,一般来说是需要进行压力测试的(很多创业型公司,业务迭代很快的系统可能没有这一步,那就悲剧了),以APP-push运营活动落地页为例(日均QPS2000,峰值QPS5000),这个系统的架构可能是这样的:
1)访问端是APP
2)运营活动H5落地页是一个web站点
3)H5落地页由缓存cache、数据库db中的数据拼装而成
通过压力测试发现,web层是瓶颈,tomcat压测单机只能抗住1200的QPS(一般来说,1%的流量到数据库,数据库500QPS还是能轻松抗住的,cache的话QPS能抗住,需要评估cache的带宽,假设不是瓶颈),我们就得到了web单机极限的QPS是1200。一般来说,线上系统是不会跑满到极限的,打个8折,单机。
线上允许跑到QPS1000。
【步骤五:根据线上冗余度回答两个问题】
好了,上述步骤1-4已经得到了峰值QPS是5000,单机QPS是1000,假设线上部署了2台服务,就能自信自如的回答技术老大提出的问题了:
(1)机器能抗住么? -> 峰值5000,单机1000,线上2台,扛不住
(2)如果扛不住,需要加多少台机器? ->需要额外3台,提前预留1台更好,给4台更稳
除了并发量的容量预估,数据量、带宽、CPU/MEM/DISK等评估亦可遵循类似的步骤。
参考:
服务器线程数究竟设置多少比较合理?
首先了解服务线程模型,这里描述的是针对IO线程与工作线程通过队列解耦类模型这个模型而言。
Web-Server通常有个配置,最大工作线程数,后端服务一般也有个配置,工作线程池的线程数量,首先了解以node为例的libev底层的线程池原理。这个线程数的配置不同的业务架构师有不同的经验值,有些业务设置为CPU核数的2倍,有些业务设置为CPU核数的8倍,有些业务设置为CPU核数的32倍。
首先明白几个子问题:
提问:工作线程数是不是设置的越大越好?
回答:肯定不是的
1)一来服务器CPU核数有限,同时并发的线程数是有限的,1核CPU设置10000个工作线程没有意义
2)线程切换是有开销的,如果线程切换过于频繁,反而会使性能降低
提问:调用sleep()函数的时候,线程是否一直占用CPU?
回答:不占用,等待时会把CPU让出来,给其他需要CPU资源的线程使用
不止调用sleep()函数,在进行一些阻塞调用,例如网络编程中的阻塞accept()【等待客户端连接】和阻塞recv()【等待下游回包】也不占用CPU资源
提问:如果CPU是单核,设置多线程有意义么,能提高并发性能么?
回答:即使是单核,使用多线程也是有意义的
1)多线程编码可以让我们的服务/代码更加清晰,有些IO线程收发包,有些Worker线程进行任务处理,有些Timeout线程进行超时检测
2)如果有一个任务一直占用CPU资源在进行计算,那么此时增加线程并不能增加并发,例如这样的一个代码:while(1){ i++; }
该代码一直不停的占用CPU资源进行计算,会使CPU占用率达到100%。
3)通常来说,Worker线程一般不会一直占用CPU进行计算,此时即使CPU是单核,增加Worker线程也能够提高并发,因为这个线程在休息的时候,其他的线程可以继续工作。
Worker线程在执行的过程中,有一部计算时间需要占用CPU,另一部分等待时间不需要占用CPU,通过量化分析,例如打日志进行统计,可以统计出整个Worker线程执行过程中这两部分时间的比例,例如:
1)时间轴1,3,5,7【上图中粉色时间轴】的计算执行时间是100ms
2)时间轴2,4,6【上图中橙色时间轴】的等待时间也是100ms
得到的结果是,这个线程计算和等待的时间是1:1,即有50%的时间在计算(占用CPU),50%的时间在等待(不占用CPU):
1)假设此时是单核,则设置为2个工作线程就可以把CPU充分利用起来,让CPU跑到100%
2)假设此时是N核,则设置为2N个工作现场就可以把CPU充分利用起来,让CPU跑到N*100%
结论:
N核服务器,通过执行业务的单线程分析出本地计算时间为x,等待时间为y,则工作线程数(线程池线程数)设置为 N*(x+y)/x,能让CPU的利用率最大化。
经验:
一般来说,非CPU密集型的业务(加解密、压缩解压缩、搜索排序等业务是CPU密集型的业务),瓶颈都在后端数据库,本地CPU计算的时间很少,所以设置几十或者几百个工作线程也都是可能的。
参考:
服务线程模型
了解常见的服务线程模型,有助于理解服务并发的原理,一般来说互联网常见的服务线程模型有如下两种:
IO线程与工作线程通过队列解耦类模型
如上图,大部分Web-Server与服务框架都是使用这样的一种“IO线程与Worker线程通过队列解耦”类线程模型:
1)有少数几个IO线程监听上游发过来的请求,并进行收发包(生产者)
2)有一个或者多个任务队列,作为IO线程与Worker线程异步解耦的数据传输通道(临界资源)
3)有多个工作线程执行正真的任务(消费者)
这个线程模型应用很广,符合大部分场景,这个线程模型的特点是,工作线程内部是同步阻塞执行任务的(回想一下tomcat线程中是怎么执行Java程序的,dubbo工作线程中是怎么执行任务的),因此可以通过增加Worker线程数来增加并发能力。
纯异步线程模型
任何地方都没有阻塞,这种线程模型只需要设置很少的线程数就能够做到很高的吞吐量,Lighttpd有一种单进程单线程模式,并发处理能力很强,就是使用的的这种模型。该模型的缺点是:
1)如果使用单线程模式,难以利用多CPU多核的优势
2)程序员更习惯写同步代码,callback的方式对代码的可读性有冲击,对程序员的要求也更高
3)框架更复杂,往往需要server端收发组件,server端队列,client端收发组件,client端队列,上下文管理组件,有限状态机组件,超时管理组件的支持。
一个典型的工作线程的处理过程,从开始处理start到结束处理end,该任务的处理共有7个步骤:
1)从工作队列里拿出任务,进行一些本地初始化计算,例如http协议分析、参数解析、参数校验等
2)访问cache拿一些数据
3)拿到cache里的数据后,再进行一些本地计算,这些计算和业务逻辑相关
4)通过RPC调用下游service再拿一些数据,或者让下游service去处理一些相关的任务
5)RPC调用结束后,再进行一些本地计算,怎么计算和业务逻辑相关
6)访问DB进行一些数据操作
7)操作完数据库之后做一些收尾工作,同样这些收尾工作也是本地计算,和业务逻辑相关
分析整个处理的时间轴,会发现:
1)其中1,3,5,7步骤中【上图中粉色时间轴】,线程进行本地业务逻辑计算时需要占用CPU
2)而2,4,6步骤中【上图中橙色时间轴】,访问cache、service、DB过程中线程处于一个等待结果的状态,不需要占用CPU,进一步的分解,这个“等待结果”的时间共分为三部分:
2.1)请求在网络上传输到下游的cache、service、DB
2.2)下游cache、service、DB进行任务处理
2.3)cache、service、DB将报文在网络上传回工作线程
如何优化单点系统的可用性和性能?
首先明白,哪些情况下会存在单点系统?
一个典型的互联网高可用性架构:
(1)客户端层,这一层是浏览器或者APP,第一步先访问DNS-server,由域名拿到nginx的外网IP
(2)负载均衡层,nginx是整个服务端的入口,负责反向代理与负载均衡工作
(3)站点层,web-server层,典型的是tomcat或者apache
(4)服务层,service层,典型的是dubbo或者thrift等提供RPC调用的后端服务
(5)数据层,包含cache和db,典型的是主从复制读写分离的db架构
在这个互联网架构中,站点层、服务层、数据库的从库都可以通过冗余的方式来保证高可用,但至少
(1)nginx层是一个潜在的单点
(2)数据库写库master也是一个潜在的单点
再来看看单点系统存在什么问题?
单点系统一般来说存在两个很大的问题:
(1)非高可用:既然是单点,master一旦发生故障,服务就会受到影响
(2)性能瓶颈:既然是单点,不具备良好的扩展性,服务性能总有一个上限,这个单点的性能上限往往就是整个系统的性能上限。
最后才是单点系统应该如何优化?
1、使用shadow-master解决单点系统的高可用问题。
shadow-master是一种很常见的解决单点高可用问题的技术方案。
“影子master”,顾名思义,服务正常时,它只是单点master的一个影子,在master出现故障时,shadow-master会自动变成master,继续提供服务。
shadow-master它能够解决高可用的问题,并且故障的转移是自动的,不需要人工介入,但不足是它使服务资源的利用率降为了50%,业内经常使用keepalived+vip的方式实现这类单点的高可用。
master正常时:
(1)client会连接正常的master,shadow-master不对外提供服务
(2)master与shadow-master之间有一种存活探测机制
(3)master与shadow-master有相同的虚IP(virtual-IP)
当发现master异常时:
shadow-master会自动顶上成为master,虚IP机制可以保证这个过程对调用方是透明的
nginx亦可用类似的方式保证高可用,数据库的主库master(主库)亦可用类似的方式来保证高可用,只是细节上有些地方要注意:
传统的一主多从,读写分离的db架构,只能保证读库的高可用,是无法保证写库的高可用的,要想保证写库的高可用,也可以使用上述的shadow-master机制:
(1)两个主库设置相互同步的双主模式
(2)平时只有一个主库提供服务,言下之意,shadow-master不会往master同步数据
(3)异常时,虚IP漂移到另一个主库,shadow-master变成主库继续提供服务
需要说明的是,由于数据库的特殊性,数据同步需要时延,如果数据还没有同步完成,流量就切到了shadow-master,可能引起小部分数据的不一致。
2、减少与单点的交互
怎么来减少与单点的交互,这里提两种常见的方法。
批量写
批量写是一种常见的提升单点性能的方式。
例如一个利用数据库写单点生成做“ID生成器”的例子:
ID生成器
(1)业务方需要ID
(2)利用数据库写单点的auto increament id来生成和返回ID
这是一个很常见的例子,很多公司也就是这么生成ID的,它利用了数据库写单点的特性,方便快捷,无额外开发成本,是一个非常帅气的方案。
潜在的问题是:生成ID的并发上限,取决于单点数据库的写性能上限。
如何提升性能呢?批量写
ID生成器2
(1)中间加一个服务,每次从数据库拿出100个id
(2)业务方需要ID
(3)服务直接返回100个id中的1个,100个分配完,再访问数据库
这样一来,每分配100个才会写数据库一次,分配id的性能可以认为提升了100倍。
客户端缓存
客户端缓存也是一种降低与单点交互次数,提升系统整体性能的方法。
还是以GFS文件系统为例:
GFS文件系统
(1)GFS的调用客户端client要访问shenjian.txt,先查询本地缓存,miss了
(2)client访问master问说文件在哪里,master告诉client在chunk3上
(3)client把shenjian.txt存放在chunk3上记录到本地的缓存,然后进行文件的读写操作
(4)未来client要访问文件,从本地缓存中查找到对应的记录,就不用再请求master了,可以直接访问chunk-server。如果文件发生了转移,chunk3返回client说“文件不在我这儿了”,client再访问master,询问文件所在的服务器。
根据经验,这类缓存的命中非常非常高,可能在99.9%以上(因为文件的自动迁移是小概率事件),这样与master的交互次数就降低了1000倍。
3、水平扩展
想方设法水平扩展,消除系统单点,理论上才能够无限的提升系统系统。
以nginx为例,如何来进行水平扩展呢?
第一步的DNS解析,只能返回一个nginx外网IP么?答案显然是否定的,“DNS轮询”技术支持DNS-server返回不同的nginx外网IP,这样就能实现nginx负载均衡层的水平扩展。
DNS-server部分,一个域名可以配置多个IP,每次DNS解析请求,轮询返回不同的IP,就能实现nginx的水平扩展,扩充负载均衡层的整体性能。
数据库单点写库也是同样的道理,在数据量很大的情况下,可以通过水平拆分,来提升写入性能。
遗憾的是,并不是所有的业务场景都可以水平拆分,例如秒杀业务,商品的条数可能不多,数据库的数据量不大,就不能通过水平拆分来提升秒杀系统的整体写性能(总不能一个库100条记录吧?)
参考:
什么是负载均衡?如何实现?
负载均衡(Load Balance)是分布式系统架构设计中必须考虑的因素之一,它通常是指,将请求/数据【均匀】分摊到多个操作单元上执行,负载均衡的关键在于【均匀】。
常见的负载均衡方案:
常见互联网分布式架构如上,分为客户端层、反向代理nginx层、站点层、服务层、数据层。可以看到,每一个下游都有多个上游调用,只需要做到,每一个上游都均匀访问每一个下游,就能实现“将请求/数据【均匀】分摊到多个操作单元上执行”。
下面对每一层的实现分开来单独介绍。
1、【客户端层】到【反向代理层】的负载均衡,是通过“DNS轮询”实现的:
DNS-server对于一个域名配置了多个解析ip,每次DNS解析请求来访问DNS-server,会轮询返回这些ip,保证每个ip的解析概率是相同的。这些ip就是nginx的外网ip,以做到每台nginx的请求分配也是均衡的。
2、【反向代理层】到【站点层】的负载均衡,是通过“nginx”实现的。通过修改nginx.conf,可以实现多种负载均衡策略:
1)请求轮询:和DNS轮询类似,请求依次路由到各个web-server
2)最少连接路由:哪个web-server的连接少,路由到哪个web-server
3)ip哈希:按照访问用户的ip哈希值来路由web-server,只要用户的ip分布是均匀的,请求理论上也是均匀的,ip哈希均衡方法可以做到,同一个用户的请求固定落到同一台web-server上,此策略适合有状态服务,例如session(可以这么做,但强烈不建议这么做,站点层无状态是分布式架构设计的基本原则之一,session最好放到数据层存储)。
3、【站点层】到【服务层】的负载均衡,是通过“服务连接池”实现的。
上游连接池会建立与下游服务多个连接,每次请求会“随机”选取连接来访问下游服务。
4、【数据层】的负载均衡,要考虑“数据的均衡”与“请求的均衡”两个点,常见的方式有“按照范围水平切分”与“hash水平切分”。
数据的均衡是指:水平切分后的每个服务(db,cache),数据量是差不多的。
请求的均衡是指:水平切分后的每个服务(db,cache),请求量是差不多的。
按照范围水平切分方案为:
每一个数据服务,存储一定范围的数据
user0服务,存储uid范围1-1kw user1服务,存储uid范围1kw-2kw
这个方案的好处是:
(1)规则简单,service只需判断一下uid范围就能路由到对应的存储服务
(2)数据均衡性较好
(3)比较容易扩展,可以随时加一个uid[2kw,3kw]的数据服务
不足是:
(1)请求的负载不一定均衡,一般来说,新注册的用户会比老用户更活跃,大range的服务请求压力会更大
按照hash水平切分方案为:
每一个数据服务,存储某个key值hash后的部分数据,上图为例:
user0服务,存储偶数uid数据
user1服务,存储奇数uid数据
这个方案的好处是:
(1)规则简单,service只需对uid进行hash能路由到对应的存储服务
(2)数据均衡性较好
(3)请求均匀性较好
不足是:
(1)不容易扩展,扩展一个数据服务,hash方法改变时候,可能需要进行数据迁移
参考:
如何针对接入层制定最佳解决方案?
首先了解什么是负载均衡?如何实现?,接入层指的就是客户端层到站点层之间。
nginx、lvs、keepalived、f5、DNS轮询,每每提到这些技术,往往讨论的是接入层的这样几个问题:
1)可用性:任何一台机器挂了,服务受不受影响
2)扩展性:能否通过增加机器,扩充系统的性能
3)反向代理+负载均衡:请求是否均匀分摊到后端的操作单元执行
首先需要对上面的名词进行了解:
1)nginx:一个高性能的web-server和实施反向代理的软件
2)lvs:Linux Virtual Server,使用集群技术,实现在linux操作系统层面的一个高性能、高可用、负载均衡服务器
3)keepalived:一款用来检测服务状态存活性的软件,常用来做高可用
4)f5:一个高性能、高可用、负载均衡的硬件设备(听上去和lvs功能差不多?)
5)DNS轮询:通过在DNS-server上对一个域名设置多个ip解析,来扩充web-server性能及实施负载均衡的技术
加下来看看接入层的技术演进之路:
1、【裸奔时代(0)单机架构】
裸奔时代的架构图如上:
1)浏览器通过DNS-server,域名解析到ip
2)浏览器通过ip访问web-server
缺点:
1)非高可用,web-server挂了整个系统就挂了
2)扩展性差,当吞吐量达到web-server上限时,无法扩容
注:单机不涉及负载均衡的问题
2、【简易扩容方案(1)DNS轮询】
假设tomcat的吞吐量是1000次每秒,当系统总吞吐量达到3000时,如何扩容是首先要解决的问题,DNS轮询是一个很容易想到的方案:
此时的架构图如上:
1)多部署几份web-server,1个tomcat抗1000,部署3个tomcat就能抗3000
2)在DNS-server层面,域名每次解析到不同的ip
优点:
1)零成本:在DNS-server上多配几个ip即可,功能也不收费
2)部署简单:多部署几个web-server即可,原系统架构不需要做任何改造
3)负载均衡:变成了多机,但负载基本是均衡的
缺点:
1)非高可用:DNS-server只负责域名解析ip,这个ip对应的服务是否可用,DNS-server是不保证的,假设有一个web-server挂了,部分服务会受到影响
2)扩容非实时:DNS解析有一个生效周期
3)暴露了太多的外网ip
3、【简易扩容方案(2)nginx】
tomcat的性能较差,但nginx作为反向代理的性能就强多了,假设线上跑到1w,就比tomcat高了10倍,可以利用这个特性来做扩容:
此时的架构图如上:
1)站点层与浏览器层之间加入了一个反向代理层,利用高性能的nginx来做反向代理
2)nginx将http请求分发给后端多个web-server
优点:
1)DNS-server不需要动
2)负载均衡:通过nginx来保证
3)只暴露一个外网ip,nginx->tomcat之间使用内网访问
4)扩容实时:nginx内部可控,随时增加web-server随时实时扩容
5)能够保证站点层的可用性:任何一台tomcat挂了,nginx可以将流量迁移到其他tomcat
缺点:
1)时延增加+架构更复杂了:中间多加了一个反向代理层
2)反向代理层成了单点,非高可用:tomcat挂了不影响服务,nginx挂了怎么办?
4、【高可用方案(3)keepalived】
为了解决高可用的问题,keepalived出场了。可以参考如何优化单点系统的可用性和性能?
此时:
1)做两台nginx组成一个集群,分别部署上keepalived,设置成相同的虚IP,保证nginx的高可用
2)当一台nginx挂了,keepalived能够探测到,并将流量自动迁移到另一台nginx上,整个过程对调用方透明
优点:
1)解决了高可用的问题
缺点:
1)资源利用率只有50%
2)nginx仍然是接入单点,如果接入吞吐量超过的nginx的性能上限怎么办,例如qps达到了50000咧?
5、【scale up扩容方案(4)lvs/f5】
nginx毕竟是软件,性能比tomcat好,但总有个上限,超出了上限,还是扛不住。
lvs就不一样了,它实施在操作系统层面;f5的性能又更好了,它实施在硬件层面;它们性能比nginx好很多,例如每秒可以抗10w,这样可以利用他们来扩容,常见的架构图如下:
此时:
1)如果通过nginx可以扩展多个tomcat一样,可以通过lvs来扩展多个nginx
2)通过keepalived+VIP的方案可以保证可用性
99.9999%的公司到这一步基本就能解决接入层高可用、扩展性、负载均衡的问题。
不管是使用lvs还是f5,这些都是scale up的方案,根本上,lvs/f5还是会有性能上限,假设每秒能处理10w的请求,一天也只能处理80亿的请求(10w秒吞吐量*8w秒),那万一系统的日PV超过80亿怎么办呢?(好吧,没几个公司要考虑这个问题)
6、【scale out扩容方案(5)DNS轮询】
水平扩展,才是解决性能问题的根本方案,能够通过加机器扩充性能的方案才具备最好的扩展性。
facebook,google,baidu的PV是不是超过80亿呢,它们的域名只对应一个ip么,终点又是起点,还是得通过DNS轮询来进行扩容:
此时:
1)通过DNS轮询来线性扩展入口lvs层的性能
2)通过keepalived来保证高可用
3)通过lvs来扩展多个nginx
4)通过nginx来做负载均衡,业务七层路由
参考:
如何针对服务层制定最佳解决方案?
在什么是负载均衡?如何实现?中提到过,上游连接池会建立与下游服务多个连接,每次请求会“随机”选取连接来访问下游服务。
那么这个随机,是如何计算的呢?我们需要考虑一个情况:
后端的service有可能部署在硬件条件不同的服务器上:
1)如果对标最低配的服务器“均匀”分摊负载,高配的服务器的利用率不足;
2)如果对标最高配的服务器“均匀”分摊负载,低配的服务器可能会扛不住;
连接池应该根据service的处理能力,动态+自适应的进行负载调度。
只要为每个下游service设置一个“权重”,代表service的处理能力,来调整访问到每个service的概率,例如:
假设service-ip1,service-ip2,service-ip3的处理能力相同,可以设置weight1=1,weight2=1,weight3=1,这样三个service连接被获取到的概率分别就是1/3,1/3,1/3,能够保证均衡访问。
假设service-ip1的处理能力是service-ip2,service-ip3的处理能力的2倍,可以设置weight1=2,weight2=1,weight3=1,这样三个service连接被获取到的概率分别就是2/4,1/4,1/4,能够保证处理能力强的service分别到等比的流量,不至于资源浪费。
至于权重的计算规则可以自定义动态算法,比如每当service成功处理一个请求,认为service处理能力足够,权重动态+1;
每当service超时处理一个请求,认为service处理能力可能要跟不上了,权重动态-10(权重下降会更快);
为了方便权重的处理,可以把权重的范围限定为[0, 100],把权重的初始值设为60分。
针对疑似不可用的服务,需要进行过载保护,最简易的方式,服务端设定一个负载阈值,超过这个阈值的请求压过来,全部抛弃,或给这个service喘一大口气,于是设定策略:接下来的若干时间内,例如1分钟(为什么是1分钟,根据经验,此时service一般在发生fullGC,差不多1分钟能回过神来),请求不再分配给这个service。
不但能借助“动态权重”来实施动态自适应的异构服务器负载均衡,还能在客户端层面更优雅的实施过载保护,在某个下游service快要响应不过来的时候,给其喘息的机会。
参考:
如何实施异构服务器的负载均衡及过载保护?_w3cschool
高并发最佳方案
高并发的概念是指通过设计保证系统能够同时并行处理很多请求。
常见的指标为:
响应时间:系统对请求做出响应的时间。例如系统处理一个HTTP请求需要200ms,这个200ms就是系统的响应时间。
吞吐量:单位时间内处理的请求数量。
QPS:每秒响应请求数。在互联网领域,这个指标和吞吐量区分的没有这么明显。
并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。
提高系统并发能力的方式,方法论上主要有两种:垂直扩展(Scale Up)与水平扩展(Scale Out)。
垂直扩展:提升单机处理能力,水平扩展:只要增加服务器数量,就能线性扩充系统性能。
首先需要明白自己项目的并发能力到底是多少容量,可以参考如何进行容量设计?
在架构上,可以参考什么是负载均衡?如何实现?
在业务代码层面,可以参考如何优化秒杀架构?
真正实现了水平扩展架构后,就可以通过无限扩容来完成无限性能架构。
参考:
高可用最佳方案
高可用是指通过设计减少系统不能提供服务的时间。
假设系统一直能够提供服务,我们说系统的可用性是100%。
如果系统每运行100个时间单位,会有1个时间单位无法提供服务,我们说系统的可用性是99%。
很多公司的高可用目标是4个9,也就是99.99%,这就意味着,系统的年停机时间为8.76个小时。
单点是系统高可用的大敌,单点往往是系统高可用最大的风险和敌人,应该尽量在系统设计的过程中避免单点。方法论上,高可用保证的原则是“集群化”,或者叫“冗余”:只有一个单点,挂了服务会受影响;如果有冗余备份,挂了还有其他backup能够顶上。
保证系统高可用,架构设计的核心准则是:冗余。
有了冗余之后,还不够,每次出现故障需要人工介入恢复势必会增加系统的不可服务实践。所以,又往往是通过“自动故障转移”来实现系统的高可用。
接下来我们看下典型互联网架构中,如何通过冗余+自动故障转移来保证系统的高可用特性。
首先回忆什么是负载均衡?如何实现?中提到的常见互联网架构。
1、【客户端层->反向代理层】的高可用
【客户端层】到【反向代理层】的高可用,是通过反向代理层的冗余来实现的。以nginx为例:有两台nginx,一台对线上提供服务,另一台冗余以保证高可用,常见的实践是keepalived存活探测,相同virtual IP提供服务。
自动故障转移:当nginx挂了的时候,keepalived能够探测到,会自动的进行故障转移,将流量自动迁移到shadow-nginx,由于使用的是相同的virtual IP,这个切换过程对调用方是透明的。
2、【反向代理层->站点层】的高可用
【反向代理层】到【站点层】的高可用,是通过站点层的冗余来实现的。假设反向代理层是nginx,nginx.conf里能够配置多个web后端,并且nginx能够探测到多个后端的存活性。
自动故障转移:当web-server挂了的时候,nginx能够探测到,会自动的进行故障转移,将流量自动迁移到其他的web-server,整个过程由nginx自动完成,对调用方是透明的。
3、【站点层->服务层】的高可用
【站点层】到【服务层】的高可用,是通过服务层的冗余来实现的。“服务连接池”会建立与下游服务多个连接,每次请求会“随机”选取连接来访问下游服务。
自动故障转移:当service挂了的时候,service-connection-pool能够探测到,会自动的进行故障转移,将流量自动迁移到其他的service,整个过程由连接池自动完成,对调用方是透明的(所以说RPC-client中的服务连接池是很重要的基础组件)。
4、【服务层>缓存层】的高可用
【服务层】到【缓存层】的高可用,是通过缓存数据的冗余来实现的。
缓存层的数据冗余又有几种方式:第一种是利用客户端的封装,service对cache进行双读或者双写。
缓存层也可以通过支持主从同步的缓存集群来解决缓存层的高可用问题。
以redis为例,redis天然支持主从同步,redis官方也有sentinel哨兵机制,来做redis的存活性检测。
自动故障转移:当redis主挂了的时候,sentinel能够探测到,会通知调用方访问新的redis,整个过程由sentinel和redis集群配合完成,对调用方是透明的。
说完缓存的高可用,这里要多说一句,业务对缓存并不一定有“高可用”要求,更多的对缓存的使用场景,是用来“加速数据访问”:把一部分数据放到缓存里,如果缓存挂了或者缓存没有命中,是可以去后端的数据库中再取数据的。
这类允许“cache miss”的业务场景,缓存架构的建议是:
将kv缓存封装成服务集群,上游设置一个代理(代理可以用集群冗余的方式保证高可用),代理的后端根据缓存访问的key水平切分成若干个实例,每个实例的访问并不做高可用。
缓存实例挂了屏蔽:当有水平切分的实例挂掉时,代理层直接返回cache miss,此时缓存挂掉对调用方也是透明的。key水平切分实例减少,不建议做re-hash,这样容易引发缓存数据的不一致。
5、【服务层>数据库层】的高可用
大部分互联网技术,数据库层都用了“主从同步,读写分离”架构,所以数据库层的高可用,又分为“读库高可用”与“写库高可用”两类。
5.1、【服务层】到【数据库读】的高可用,是通过读库的冗余来实现的。
既然冗余了读库,一般来说就至少有2个从库,“数据库连接池”会建立与读库多个连接,每次请求会路由到这些读库。
自动故障转移:当读库挂了的时候,db-connection-pool能够探测到,会自动的进行故障转移,将流量自动迁移到其他的读库,整个过程由连接池自动完成,对调用方是透明的(所以说DAO中的数据库连接池是很重要的基础组件)。
5.2、【服务层】到【数据库写】的高可用,是通过写库的冗余来实现的。
以mysql为例,可以设置两个mysql双主同步,一台对线上提供服务,另一台冗余以保证高可用,常见的实践是keepalived存活探测,相同virtual IP提供服务。
自动故障转移:当写库挂了的时候,keepalived能够探测到,会自动的进行故障转移,将流量自动迁移到shadow-db-master,由于使用的是相同的virtual IP,这个切换过程对调用方是透明的。
参考:
数据量很大的架构如何设计?比如100亿数据量,1万列属性,10万吞吐量的业务需求
如何解决100亿数据量,1万属性,多属性组合查询,10万并发查询的技术难题。
每个公司的发展都是一个从小到大的过程,撇开并发量和数据量不谈,先看看
(1)如何实现属性扩展性需求
(2)多属性组合查询需求
最开始,可能只有一个招聘品类,那帖子表可能是这么设计的:
tiezi(tid,uid, c1, c2, c3)
那如何满足各属性之间的组合查询需求呢?
最容易想到的是通过组合索引:
index_1(c1,c2) index_2(c2, c3) index_3(c1, c3)
随着业务的发展,又新增了一个房产类别,新增了若干属性,新增了若干组合查询,于是帖子表变成了:
tiezi(tid,uid, c1, c2, c3, c10, c11, c12, c13)
其中c1,c2,c3是招聘类别属性,c10,c11,c12,c13是房产类别属性,这两块属性一般没有组合查询需求
但为了满足房产类别的查询需求,又要建立了若干组合索引(不敢想有多少个索引能覆盖所有两属性查询,三属性查询)
是不是发现玩不下去了?
传统解决方案:
新增属性是一种扩展方式,新增表也是一种方式,有友商是这么玩的,按照业务进行垂直拆分:
tiezi_zhaopin(tid,uid, c1, c2, c3)
tiezi_fangchan(tid,uid, c10, c11, c12, c13)
这些表,这些服务维护在不同的部门,不同的研发同学手里,看上去各业务线灵活性强,这恰恰是悲剧的开始:
(1)tid如何规范?
(2)属性如何规范?
(3)按照uid来查询怎么办(查询自己发布的所有帖子)?
(4)按照时间来查询怎么办(最新发布的帖子)?
(5)跨品类查询怎么办(例如首页搜索框)?
(6)技术范围的扩散,有的用mongo存储,有的用mysql存储,有的自研存储
(7)重复开发了不少组件
(8)维护成本过高
(9)…
想想看,电商的商品表,不可能一个类目一个表的。
再来看看58同城的方案:
将不同品类,异构的数据统一存储起来,采用的就是类似version+ext的方式:
tiezi(tid,uid, time, title, cate, subcate, xxid, ext)
(1)一些通用的字段抽取出来单独存储
(2)通过cate, subcate, xxid等来定义ext是何种含义
(3)通过ext来存储不同业务线的个性化需求
例如招聘的帖子:
ext : {“job”:”driver”,”salary”:8000,”location”:”bj”}
而二手的帖子:
ext : {”type”:”iphone”,”money”:3500}
58同城最核心的帖子数据,100亿的数据量,分256库,异构数据mysql存储,上层架了一个服务,使用memcache做缓存,就是这样一个简单的架构,一直坚持这这么多年。上层的这个服务,就是58同城最核心的统一服务IMC(Imformation Management Center),注意这个最核心,是没有之一。
解决了海量异构数据的存储问题,遇到的新问题是:
(1)每条记录ext内key都需要重复存储,占据了大量的空间,能否压缩存储
(2)cateid已经不足以描述ext内的内容,品类有层级,深度不确定,ext能否具备自描述性
(3)随时可以增加属性,保证扩展性
解决方案未为缩短ext的大小,通过key转为数字。json里的key不再是”salary” ”location” ”money” 这样的长字符串了,取而代之的是数字1,2,3,4,这些数字是什么含义,属于哪个子分类,值的校验约束,统一都存储在类目、属性服务里。
这样就对原来帖子表ext里的
ext : {“1”:”driver”,”2”:8000,”3”:”bj”}
ext : {”4”:”iphone”,”5”:3500}
解决了key压缩,key描述,key扩展,value校验,品类层级的问题,还有这样的一个问题没有解决:每个品类下帖子的属性各不相同,查询需求各不相同,如何解决100亿数据量,1万属性的查询需求,是58同城面临的新问题。
搜索可以走单独的搜索引擎服务。
总结:
合理的kv表来实现可扩展性,配合强大的搜索引擎服务来进行查询。
参考:
如何解决架构中的依赖问题?
首先明白什么是架构中的依赖问题,比如说:
数据库换了一个ip,此时往往连接此数据库的上游需要修改配置重启,如果数据库有很多上游调用方,改配置重启的调用方会很多,每次换ip的成本往往很高,成为大家共性的痛点。
根本上,这是一个“架构耦合”的问题,是一个架构设计上“反向依赖”的问题。
通用性的说,就是变动方是A,配合方却是BCDE(或者说需求方是A,改动方确是BCDE)。
明白了什么是架构依赖,再来看看怎么解决架构依赖,中心思想为:解耦都是抽取共性成为新的一层。
【case1:公共库导致耦合】
三个服务s1/s2/s3,通过一个公共的库biz.jar来实现一段业务逻辑,s1/s2/s3其实间接通过biz.jar耦合在了一起,一个业务s1修改一块公共的代码,导致影响其他业务s2/s3,架构上是不合理的。
优化方案1:业务垂直拆分
业务垂直拆分
如果biz.jar中实现的逻辑“业务特性”很强,可以拆分为biz1.jar/biz2.jar/biz3.jar,来对s1/s2/s3进行解耦。这样的话,任何业务的改动,影响范围只是自己,不会影响其他人。
优化方案2:服务化
服务化
如果biz.jar中实现的逻辑“业务共性”很强,可以将biz.jar优化为biz.service服务,来对s1/s2/s3进行解耦。服务化之后,兼容性能更好的通过接口自动化回归测试来保证。
基础服务的抽象,本身是一种共性聚焦,是系统解耦常见的方案。
【case2:服务化不彻底导致耦合】
服务化是解决“业务共性”组件库导致系统耦合的常见方案之一,但如果服务化不彻底,service本身也容易成为业务耦合点。
典型的服务化不彻底导致的业务耦合的特征是,共性服务中,包含大量“根据不同业务,执行不同个性分支”的代码。
switch (biz-type)
case biz-1 : exec1
case biz-2 : exec2
case biz-3 : exec3
在这种架构下,biz-1/biz-2/biz-3有个性的业务需求,可能导致修改代码的是共性的biz-service,使其成为研发瓶颈,架构上也是不合理的。
8优化方案:业务特性代码上浮,业务共性代码下沉,彻底解耦*
把swithc case中业务特性代码放到业务层实现,这样biz-1/biz-2/biz-3有个性的业务需求,升级的是自己的业务系统。
【case3:notify的不合理实现导致的耦合】
消息发送方不关注消息接收方的执行结果,如果采用调用的方式来实现通知,会导消息发送方和消息接收方耦合。
优化方案:通过MQ实现解耦
消息发送方upper将消息发布给MQ,消息接收方从MQ去订阅,任何新增对消息的消费,upper都不需要修改代码。
【case4:配置中的ip导致上下游耦合】
下游服务换ip,可能导致多个服务调用方修改配置重启。上下游间接的通过ip这个配置耦合在了一起,架构不合理。
优化方案:通过内网域名而不是ip来进行下游连接
如果在配置中使用内网域名来进行下游连接,当下游服务或者数据库更换ip时,只需要运维层面将内网域名指向新的ip,然后统一切断原有旧的连接,连接就能够自动切换到新的ip上来。这个过程不需要所有上游配合,非常帅气,强烈推荐!
【case5:下游扩容导致上下游耦合】
这次不是换换ip这么简单了,下游服务提供方原来是集群(ip1/ip2/ip3,当然,上游配置的是内网域名),现在集群要扩容为(ip1/ip2/ip3/ip4/ip5),如果没有特殊的架构设计,上游往往需要修改配置,新增扩容后的节点,再重启,导致上下游耦合。
这类方案,需要用到配置中心。
配置中心是一个典型的 逻辑上解耦、物理上不解耦 的一个架构优化工具,
配置中心的一些 要点 :
所有 通用配置,基础配置 将 由配置中心统一维护 ,数据只存储一份,不再有“配置私藏”。
所有上游通过配置中心来 订阅下游配置
所有下游的 配置变更 ,例如扩容时, 通过配置中心统一修改
配置中心将变更后的配置 通知 所有上游订阅方
订阅方得知下游服务扩容或者缩容后,通过 动态连接池 ,自动新增或者销毁连接,实现自动扩容与缩容,大部分 服务发现 都是这么做的。
参考:
如何设计配置模块?
随着互联网业务的越来越复杂,用户量与流量越来越大,“服务化分层”是架构演进的必由之路。
站点应用会调用服务,上游服务调用底层服务,依赖关系会变得非常复杂。为了保证高可用,一个底层服务往往是若干个节点形成一个集群提供服务。
针对配置数据的处理,有以下几个阶段:
配置私藏
“配置私藏”是配置文件架构的最初级阶段,每个上游都有一个专属的私有配置文件,记录被调用下游的每个节点配置信息。
如上图:
1)用户中心user-service有ip1/ip2/ip3三个节点
2)service1调用了用户中心,它有一个专属配置文件s1.conf,里面配置了us的集群是ip1/ip2/ip3
3)service2也调用了用户中心,同理有个配置文件s2.conf,记录了us集群是ip1/ip2/ip3
4)web2也调用了用户中心,同理w2.conf,配置了us集群是ip1/ip2/ip3
这种方案的问题就是下游的任何改动,都需要上游跟随去改动。这种架构设计是明显耦合的,参考如何解决架构中的依赖问题?
全局配置
对于通用的服务,建立全局配置文件,消除配置私藏:
1)运维层面制定规范,新建全局配置文件,例如/opt/globalconf/global.conf,如果配置较多,注意做好配置的垂直拆分
2)对于服务方,如果是通用的服务,集群信息配置在global.conf里
3)对于调用方,调用方禁止配置私藏,必须从global.conf里读取通用下游配置
这么做的好处:
1)如果下游容量变化,只需要修改一处配置global.conf,而不需要各个上游修改
2)调用方下一次重启的时候,自动迁移到扩容后的集群上来了
3)修改成本非常小,读取配置文件目录变了
这种方案的不足之处就是:如果调用方一直不重启,就没有办法将流量迁移到新集群上去了。
但是解决方案也是存在的:
1)文件监控组件FileMonitor
作用是监控文件的变化,起一个timer,定期监控文件的ModifyTime或者md5就能轻松实现,当文件变化后,实施回调。
2)动态连接池组件DynamicConnectionPool
“连接池组件”是RPC-client中的一个子组件,用来维护与多个RPC-server节点之间的连接。所谓“动态连接池”,是指连接池中的连接可以动态增加和减少(用锁来互斥或者线程安全的数据结构很容易实现)。
配置中心
全局配置文件是一个能够快速落地的,解决“修改配置重启”问题的方案,但它仍然解决不了,服务提供方“不知道有多少个上游调用了自己”这个问题。
如果不知道多少上游调用了自己, “按照调用方限流” “绘制全局架构依赖图” 等需求便难以实现,怎么办,可以采用“配置中心”来解决。
是把项目中各种配置、各种参数、各种开关,全部都放到一个集中的地方进行统一管理,并提供一套标准的接口。当各个服务需要获取配置的时候,就来「配置中心」的接口拉取。当「配置中心」中的各种参数有更新的时候,也能通知到各个服务实时的过来同步最新的信息,使之动态更新。
几个主流配置中心方案:
Apollo:携程开源的分布式配置中心;
Spring Cloud Config;
Disconf:百度开源的分布式配置中心;
走配置中心的缺点就是:系统复杂度相对较高,对配置中心的可靠性要求较高,一处挂全局挂。
参考:
如何提高跨公网调用服务的稳定性
很多时候,业务需要跨公网调用一个第三方服务提供的接口,为了避免每个调用方都依赖于第三方服务,往往会抽象一个服务:
•解除调用方与第三方接口的耦合
•当第三方的接口变动时,只有服务需要修改,而不是所有调用方均修改
调用流程如下:
(1)业务调用方调用内部service
(2)内部service跨公网调用第三方接口
(3)第三方接口返回结果给内部service
(4)内部service返回结果给业务调用方
这种设计有一个问题,就是:
当有一个接口跨公网第三方调用超时时,可能导致所有接口都不可用,即使大部分接口不依赖于跨公网第三方调用。
主要原因是发生了网络抖动,或者接口超时时,工作线程被占满的情况。
普通的优化方案为:
•增大工作线程数(不根本解决问题)
•降低超时时间(不根本解决问题)
•垂直拆分,N个接口拆分成若干个服务,使得在出问题时,被牵连的接口尽可能少(依旧不根本解决问题,难道一个服务只提供一个接口吗?)
比较优雅的解决方案为:
•业务能接受旧数据:读取本地数据,异步代理定期更新数据
•有多个第三方服务提供商:多个第三方互备
•向第三方同步数据:本地写成功就算成功,异步向第三方同步数据
参考:
如何确保session的一致性?
什么是session? 服务器为每个用户创建一个会话,存储用户的相关信息,以便多次请求能够定位到同一个上下文。
Web开发中,web-server可以自动为同一个浏览器的访问用户自动创建session,提供数据存储功能。最常见的,会把用户的登录信息、用户信息存储在session中,以保持登录状态。
如何确保session的一致性呢?
1、session同步法
多个web-server之间相互同步session,这样每个web-server之间都包含全部的session
缺点是:所有web-server都包含所有session数据,数据量受内存限制,无法水平扩展
2、客户端存储法
服务端存储所有用户的session,内存占用较大,可以将session存储到浏览器cookie中,每个端只要存储一个用户的数据了
缺点是:不安全、浪费http流量、大小受cookie限制
3、反向代理hash一致性
让同一个用户的请求保证落在一台web-server上。
方案一:四层代理hash
反向代理层使用用户ip来做hash,以保证同一个ip的请求落在同一个web-server上
方案二:七层代理hash
反向代理使用http协议中的某些业务属性来做hash,例如sid,city_id,user_id等,能够更加灵活的实施hash策略,以保证同一个浏览器用户的请求落在同一个web-server上。
优点:
•只需要改nginx配置,不需要修改应用代码
•负载均衡,只要hash属性是均匀的,多台web-server的负载是均衡的
•可以支持web-server水平扩展(session同步法是不行的,受内存限制)
不足:
•如果web-server重启,一部分session会丢失,产生业务影响,例如部分用户重新登录
•如果web-server水平扩展,rehash后session重新分布,也会有一部分用户路由不到正确的session
session一般是有有效期的,所有不足中的两点,可以认为等同于部分session失效,一般问题不大。
对于四层hash还是七层hash,个人推荐前者:让专业的软件做专业的事情,反向代理就负责转发,尽量不要引入应用层业务属性,除非不得不这么做(例如,有时候多机房多活需要按照业务属性路由到不同机房的web-server)。
4、后端存储
将session存储到redis或db里。
对于db存储还是cache,个人推荐后者:session读取的频率会很高,数据库压力会比较大。如果有session高可用需求,cache可以做高可用,但大部分情况下session可以丢失,一般也不需要考虑高可用。
参考:
通信
即时通讯系统的实现,短轮询、长轮询、长连接(SSE) 和 WebSocket 间的区别?
短轮询和长轮询的目的都是用于实现客户端和服务器端的一个即时通讯。
短轮询的基本思路就是浏览器每隔一段时间向浏览器发送 http 请求,服务器端在收到请求后, 不论是否有数据更新,都直接进行 响应。这种方式实现的即时通信,本质上还是浏览器发送请求,服务器接受请求的一个过程,通过让客户端不断的进行请求,使得客 户端能够模拟实时地收到服务器端的数据的变化。这种方式的优点是比较简单,易于理解。缺点 是这种方式由于需要不断的建立 ht tp 连接,严重浪费了服务器端和客户端的资源。当用户增加时,服务器端的压力就会变大,这 是很不合理的。
长轮询的基本思路是,首先由客户端向服务器发起请求,当服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将 这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则到达一定的时间限制才返回。 客户端 JavaScript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立 连接。长轮询和短轮询比起来,它的 优点是明显减少了很多不必要的 http 请求次数,相比之下节约了资源。长轮询的缺点在于, 连接挂起也会导致资源的浪费。
SSE是HTML5新增的功能,全称为Server-Sent Events,服务器发送事件,也叫长连接。它可以允许服务推送数据到客户端。SSE 的基本思想是,服务器使用流信息向服务器推送信息。严格地说,http 协议无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息。也就是说,发送的不是一次性的数 据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就 是这样的例子。SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 http 协议,目前除了 IE/Edge,其他浏览器都支 持。它相对于前面两种方式来说,不需要建立过多的 http 请求,相比之下节约了资源。前端用EventSource 对象来接收该事件
上面三种方式本质上都是基于 http 协议的,我们还可以使用 WebSocket 协议来实现。 WebSocket 是 Html5 定义的一个新协 议,与传统的 http 协议不同,该协议允许由服务器主动的向客户端推送信息。使用 WebSocket 协议的缺点是在服务器端的配置 比较复杂。WebSocket 是一个全双工的协议,也就是通信双方是平等的,可以相互发送消息, 而 SSE 的方式是单向通信的,只能 由服务器端向客户端推送信息,如果客户端需要发送信息就是属于下一个 http 请求了。
参考:
轮询、长轮询、长连接、websocket - 云+社区 - 腾讯云
打日志
登录相关
怎么实现多个网站之间共享登录状态
在多个网站之间共享登录状态指的就是单点登录。多个应用系统中,用户只需要登录一次就可以 访问所有相互信任的应用系统。
单点登录的本质就是在多个应用系统中共享登录状态。如果用户的登录状态是记录在 Session 中的,要实现共享登录状态,就要先共享 Session,比如可以将 Session 序列化到 Redis 中,让多个应用系统共享同一个 Redis,直接读取 Redis 来获取 Session。
因为不同的应用系统有着不同的域名,尽管 Session 共享了,但是一个企业不同应用的域名不同,依然可能出现跨站or跨域。
前端方面的实现方式:
1、父域 Cookie
首先掌握简单聊一下cookie。
2、认证中心
我认为单点登录可以这样来实现,首先将用户信息的验证中心独立出来,作为一个单独的认证中 心,该认证中心的作用是判断客户端发送的账号密码的正确性,然后向客户端返回对应的用户信息,并且返回一个由服务器端秘钥加密 的登录信息的 token 给客户端,该token 具有一定的有效时限。当一个应用系统跳转到另一个应用系统时,通过 url 参数的方式 来传递 token,然后转移到的应用站点发送给认证中心,认证中心对 token 进行解密后验证,如果用户信息没有失效,则向客户端 返回对应的用户信息,如果失效了则将页面重定向会单点登录页面。
3、LocalStorage 跨域
数据库设计
常见的水平切分算法有哪些?
常见的水平切分算法有“范围法”和“哈希法”:
范围法:以业务主键uid为划分依据,将数据水平切分到两个数据库实例上去:
•user-db1:存储0到1千万的uid数据
•user-db2:存储1千万到2千万的uid数据
哈希法:也是以业务主键uid为划分依据,将数据水平切分到两个数据库实例上去:
•user-db1:存储uid取模得1的uid数据
•user-db2:存储uid取模得0的uid数据
这两种方法在互联网都有使用,其中哈希法使用较为广泛。
如何设计一套靠谱的数据库架构?
以用户中心这个模块为例来说明。
用户中心是一个常见业务,主要提供用户注册、登录、信息查询与修改的服务,其核心元数据为: User(uid, uname, passwd, sex, age,nickname, …) 其中:
•uid为用户ID,主键
•uname, passwd, sex, age, nickname, …等为用户的属性
数据库设计上,一般来说在业务初期,单库单表就能够搞定这个需求。
单库架构
最常见的架构设计如上:
•user-service:用户中心服务,对调用者提供友好的RPC接口
•user-db:一个库进行数据存储
分组架构
分组解决的是“数据库读写高并发量高”问题,所实施的架构设计。
分组架构是最常见的一主多从,主从同步,读写分离数据库架构:
•user-service:依旧是用户中心服务
•user-db-M(master):主库,提供数据库写服务
•user-db-S(slave):从库,提供数据库读服务
主和从构成的数据库集群称为“组”。
分组的特点:
同一个组里的数据库集群:
•主从之间通过binlog进行数据同步
•多个实例数据库结构完全相同
•多个实例存储的数据也完全相同,本质上是将数据进行复制
分组解决的问题:
大部分互联网业务读多写少,数据库的读往往最先成为性能瓶颈,如果希望:
•线性提升数据库读性能
•通过消除读写锁冲突提升数据库写性能
•通过冗余从库实现数据的“读高可用”
此时可以使用分组架构,需要注意的是,分组架构中,数据库的主库依然是写单点。
分片架构
分片架构是大伙常说的**水平切分(sharding)**数据库架构:
•user-service:依旧是用户中心服务
•user-db1:水平切分成2份中的第一份
•user-db2:水平切分成2份中的第二份
分片后,多个数据库实例也会构成一个数据库集群。
对于分库和分表的选择:
强烈建议分库,而不是分表,因为:
•分表依然公用一个数据库文件,仍然有磁盘IO的竞争
•分库能够很容易的将数据迁移到不同数据库实例,甚至数据库机器上,扩展性更好
具体的操作参考常见的水平切分算法有哪些?
分片架构解决的问题:
大部分互联网业务数据量很大,单库容量容易成为瓶颈,此时通过分片可以:
•线性提升数据库写性能,需要注意的是,分组架构是不能线性提升数据库写性能的
•降低单库数据容量
分组+分片架构
如果业务读写并发量很高,数据量也很大,通常需要实施分组+分片的数据库架构:
•通过分片来降低单库的数据量,线性提升数据库的写性能
•通过分组来线性提升数据库的读性能,保证读库的高可用
垂直切分
除了水平切分,垂直切分也是一类常见的数据库架构设计,垂直切分一般和业务结合比较紧密。
还是以用户中心为例,可以这么进行垂直切分:
User(uid, uname, passwd, sex, age, …)
User_EX(uid, intro, sign, …)
•垂直切分开的表,主键都是uid
•登录名,密码,性别,年龄等属性放在一个垂直表(库)里
•自我介绍,个人签名等属性放在另一个垂直表(库)里
进行垂直切分的细节:
根据业务对数据进行垂直切分时,一般要考虑属性的“长度”和“访问频度”两个因素:
•长度较短,访问频率较高的放在一起
•长度较长,访问频度较低的放在一起
这是因为,数据库会以行(row)为单位,将数load到内存(buffer)里,在内存容量有限的情况下,长度短且访问频度高的属性,内存能够load更多的数据,命中率会更高,磁盘IO会减少,数据库的性能会提升。
垂直切分解决的问题:
垂直切分即可以降低单库的数据量,还可以降低磁盘IO从而提升吞吐量,但它与业务结合比较紧密,并不是所有业务都能够进行垂直切分的。
参考:
为什么会有冗余表?
互联网很多业务场景的数据量很大,此时数据库架构要进行水平切分,水平切分会有一个patition key,通过patition key的查询能够直接定位到库,但是非patition key上的查询可能就需要扫描多个库了。
例如订单表,业务上对用户和商家都有订单查询需求:
Order(oid, info_detail)
T(buyer_id, seller_id, oid)
如果用buyer_id来分库,seller_id的查询就需要扫描多库。
如果用seller_id来分库,buyer_id的查询就需要扫描多库。
这类需求,为了做到高吞吐量低延时的查询,往往使用“数据冗余”的方式来实现,就是文章标题里说的“冗余表”:
T1(buyer_id, seller_id, oid)
T2(seller_id, buyer_id, oid)
同一个数据,冗余两份,一份以buyer_id来分库,满足买家的查询需求;
一份以seller_id来分库,满足卖家的查询需求。
冗余表的实现方式?
【方法一:服务同步写】
(1)业务方调用服务,新增数据
(2)服务先插入T1数据
(3)服务再插入T2数据
(4)服务返回业务方新增数据成功
优点:
(1)不复杂,服务层由单次写,变两次写
(2)数据一致性相对较高(因为双写成功才返回)
缺点:
(1)请求的处理时间增加(要插入次,时间加倍)
(2)数据仍可能不一致,例如第二步写入T1完成后服务重启,则数据不会写入T2
【方法二:服务异步写】
(1)业务方调用服务,新增数据
(2)服务先插入T1数据
(3)服务向消息总线发送一个异步消息(发出即可,不用等返回,通常很快就能完成)
(4)服务返回业务方新增数据成功
(5)消息总线将消息投递给数据同步中心
(6)数据同步中心插入T2数据
优点:
(1)请求处理时间短(只插入1次)
缺点:
(1)系统的复杂性增加了,多引入了一个组件(消息总线)和一个服务(专用的数据复制服务)
(2)因为返回业务线数据插入成功时,数据还不一定插入到T2中,因此数据有一个不一致时间窗口(这个窗口很短,最终是一致的)
(3)在消息总线丢失消息时,冗余表数据会不一致
【方法三:线下异步写】
(1)业务方调用服务,新增数据
(2)服务先插入T1数据
(3)服务返回业务方新增数据成功
(4)数据会被写入到数据库的log中
(5)线下服务或者任务读取数据库的log
(6)线下服务或者任务插入T2数据
优点:
(1)数据双写与业务完全解耦
(2)请求处理时间短(只插入1次)
缺点:
(1)返回业务线数据插入成功时,数据还不一定插入到T2中,因此数据有一个不一致时间窗口(这个窗口很短,最终是一致的)
(2)数据的一致性依赖于线下服务或者任务的可靠性
冗余表应该先写正表还是先写反表?
【如果出现不一致】,谁先做对业务的影响较小,就谁先执行。
以上文的订单生成业务为例,buyer和seller冗余表都需要插入数据:
T1(buyer_id, seller_id, oid)
T2(seller_id, buyer_id, oid)
用户下单时,如果“先插入buyer表T1,再插入seller冗余表T2”,当第一步成功、第二步失败时,出现的业务影响是“买家能看到自己的订单,卖家看不到推送的订单”
相反,如果“先插入seller表T2,再插入buyer冗余表T1”,当第一步成功、第二步失败时,出现的业务影响是“卖家能看到推送的订单,卖家看不到自己的订单”
由于这个生成订单的动作是买家发起的,买家如果看不到订单,会觉得非常奇怪,并且无法支付以推动订单状态的流转,此时即使卖家看到有人下单也是没有意义的。
因此,在此例中,应该先插入buyer表T1,再插入seller表T2。
如何保证冗余表的数据一致性?
从冗余表的实现方式?的几种实现中可以看出,无论哪一种都不能保证数据的原子性。所以应该如何保证数据的一致性呢?
【方法一:线下扫面正反冗余表全部数据】
线下启动一个离线的扫描工具,不停的比对正表T1和反表T2,如果发现数据不一致,就进行补偿修复。
优点:
(1)比较简单,开发代价小
(2)线上服务无需修改,修复工具与线上服务解耦
缺点:
(1)扫描效率低,会扫描大量的“已经能够保证一致”的数据
(2)由于扫描的数据量大,扫描一轮的时间比较长,即数据如果不一致,不一致的时间窗口比较长
【方法二:线下扫描增量数据】
每次只扫描增量的日志数据,就能够极大提高效率,缩短数据不一致的时间窗口
(1)写入正表T1
(2)第一步成功后,写入日志log1
(3)写入反表T2
(4)第二步成功后,写入日志log2
当然,我们还是需要一个离线的扫描工具,不停的比对日志log1和日志log2,如果发现数据不一致,就进行补偿修复
优点:
(1)虽比方法一复杂,但仍然是比较简单的
(2)数据扫描效率高,只扫描增量数据
缺点:
(1)线上服务略有修改(代价不高,多写了2条日志)
(2)虽然比方法一更实时,但时效性还是不高,不一致窗口取决于扫描的周期
【方法三:实时线上“消息对”检测】
这次不是写日志了,而是向消息总线发送消息
(1)写入正表T1
(2)第一步成功后,发送消息msg1
(3)写入反表T2
(4)第二步成功后,发送消息msg2
这次不是需要一个周期扫描的离线工具了,而是一个实时订阅消息的服务不停的收消息。
假设正常情况下,msg1和msg2的接收时间应该在3s以内,如果检测服务在收到msg1后没有收到msg2,就尝试检测数据的一致性,不一致时进行补偿修复
优点:
(1)效率高
(2)实时性高
缺点:
(1)方案比较复杂,上线引入了消息总线这个组件
(2)线下多了一个订阅总线的检测服务
可以根据业务对一致性的需求程度决定使用哪一种方法。
参考:
多库多事务(分布式事务)如何保证数据一致性?
首先说明,这个是无法完美解决的,只能降低不一致的概率。
务来保证数据库层面数据的ACID特性,操作全部成功,则全部提交,如果任何一个出现问题,则全部回滚,以保证数据的一致性。互联网的业务特点,数据量较大,并发量较大,经常使用拆库的方式提升系统的性能。如果进行了拆库,余额、订单、流水可能分布在不同的数据库上,甚至不同的数据库实例上,此时就不能用事务来保证数据的一致性了。
有以下方案
补偿事务
补偿事务是一种在业务端实施业务逆向操作事务,来保证业务数据一致性的方式。
事务补偿即在事务链中的任何一个正向事务操作,都必须存在一个完全符合回滚规则的可逆事务。如果是一个完整的事务链,则必须事务链中的每一个业务服务或操作都有对应的可逆服务。对于Service服务本身无状态,也不容易实现前面讨论过的通过DTC或XA机制实现的跨应用和资源的事务管理,建立跨资源的事务上下文。因此也较难以实现真正的预提交和正式提交的分离。
该方案的不足是:
(1)不同的业务要写不同的补偿事务,不具备通用性
(2)没有考虑补偿事务的失败
(3)如果业务流程很复杂,if/else会嵌套非常多层
事务拆分与后置提交优化
将长事务拆分为多个本地短事务,由事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
相当于
trx1.exec();
trx1.commit();
trx2.exec();
trx2.commit();
trx3.exec();
trx3.commit();
优化为:
trx1.exec();
trx2.exec();
trx3.exec();
trx1.commit();
trx2.commit();
trx3.commit();
参考:
缓存设计
什么情况下需要用到缓存?
缓存是一种提高系统读性能的常见技术,对于读多写少的应用场景,我们经常使用缓存来进行优化
例如对于用户的余额信息表account(uid, money),业务上的需求是:
(1)查询用户的余额,SELECT money FROM account WHERE uid=XXX,占99%的请求
(2)更改用户余额,UPDATE account SET money=XXX WHERE uid=XXX,占1%的请求
由于大部分的请求是查询,我们在缓存中建立uid到money的键值对,能够极大降低数据库的压力。
有了数据库和缓存两个地方存放数据之后(uid->money),每当需要读取相关数据时(money),操作流程一般是这样的:
(1)读取缓存中是否有相关数据,uid->money
(2)如果缓存中有相关数据money,则返回【这就是所谓的数据命中“hit”】
(3)如果缓存中没有相关数据money,则从数据库读取相关数据money【这就是所谓的数据未命中“miss”】,放入缓存中uid->money,再返回
缓存的命中率 = 命中缓存请求个数/总缓存访问请求个数 = hit/(hit+miss)
上面举例的余额场景,99%的读,1%的写,这个缓存的命中率是非常高的,会在95%以上。
当发生写操作时,是更新缓存还是淘汰缓存?
更新缓存:数据不但写入数据库,还会写入缓存
淘汰缓存:数据只会写入数据库,不会写入缓存,只会把数据淘汰掉
更新缓存的优点:缓存不会增加一次miss,命中率高
淘汰缓存的优点:简单
那到底是选择更新缓存还是淘汰缓存呢,主要取决于“更新缓存的复杂度”。
淘汰缓存操作简单,并且带来的副作用只是增加了一次cache miss,建议作为通用的处理方式。
当发生写操作时,是先更新数据库,还是先更新缓存?
对于一个不能保证事务性的操作,一定涉及“哪个任务先做,哪个任务后做”的问题,解决这个问题的方向是:
如果出现不一致,谁先做对业务的影响较小,就谁先执行。
假设先写数据库,再淘汰缓存(这种是常见的方案,简单,通过数据库事务保证):
第一步写数据库操作成功,第二步淘汰缓存失败,则会出现DB中是新数据,Cache中是旧数据,数据不一致。
假设先淘汰缓存,再写数据库(需要考虑数据库读写时序带来的不一致性,参考什么情况下缓存和数据库会出现数据不一致性?):
第一步淘汰缓存成功,第二步写数据库失败,则只会引发一次Cache miss。
参考:
什么情况下缓存和数据库会出现数据不一致性?
在当发生写操作时,是先更新数据库,还是先更新缓存?提到过,如果先更新数据库,再淘汰缓存失败(或者时间过长),会出现不一致性。
就算是先淘汰缓存,再写数据库,也有可能出现不一致性,数据库层面的读写并发,引发的数据库与缓存数据不一致。
在分布式环境下,数据的读写都是并发的,上游有多个应用,通过一个服务的多个部署(为了保证可用性,一定是部署多份的),对同一个数据进行读写,在数据库层面并发的读写并不能保证完成顺序,也就是说后发出的读请求很可能先完成(读出脏数据),脏数据又入了缓存,缓存与数据库中的数据不一致出现了。
(a)发生了写请求A,A的第一步淘汰了cache(如上图中的1)
(b)A的第二步写数据库,发出修改请求(如上图中的2)
(c)发生了读请求B,B的第一步读取cache,发现cache中是空的(如上图中的步骤3)
(d)B的第二步读取数据库,发出读取请求,此时A的第二步写数据还没完成,读出了一个脏数据放入cache(如上图中的步骤4)
参考:
Serverless
什么是Serverless
Serverless 是 FaaS 与 BaaS 的组合。所谓的 FaaS 就是函数即服务,而 BaaS 则指的是后端即服务,两者搭配,共同成为 Serverless 架构不可获取的部分,为开发者提供降本提效的技术红利。
从特性角度指出,对于被认为是 Serverless 架构的产品或者服务,还需要具备按量付费和弹性伸缩等特点。
参考:
Serverless解决的核心问题是什么
让开发者专注于业务代码,不再考虑资源的事情。
代码的执行不再需要手动分配资源。不需要为服务的运行指定需要的资源(比如使用几台机器、多大的带宽、多大的磁盘等),只需要提供一份代码,剩下的交由 Serverless 平台去处理就行了。当前阶段的实现平台分配资源时还需要用户方提供一些策略,例如单个实例的规格和最大并发数,单实例的最大 CPU 使用率。理想的情况是通过某些学习算法来进行完全自动的自适应分配。
参考:
Serverless的技术使用场景是?
剥离 Task/Job 类业务处理逻辑:在一些微服务架构的业务系统中,利用 Serverless 异步/异步任务的能力实现这些系统任务处理需求,函数计算提供了 HTTP、SDK、定时、事件触发等多种便于用户提交请求,执行相关任务的集成方式。
剥离 MQ 业务消息处理逻辑:企业的业务系统中通常都会存在很多由消息中间件链接的多个业务子系统,通过函数计算提供的消息类云产品事件触发能力,将原来通过监听消息队列,主动拉取消息进行消费的逻辑利用 Serverless 触发器代替,消息处理的逻辑由函数计算承担,利用事件驱动实现消息消费和消息处理的解耦,统一依靠事件驱动提供的可靠消费能力,实现消息处理逻辑的 Serverless 化。
剥离文件类处理业务逻辑:关于一些文件、视频处理类的业务,数据在文件系统和 DB 之间流转,利用函数计算提供的 OSS 触发器、DB 类触发器(OTS),通过事件驱动的方式快速完成相关的数据处理逻辑。
剥离数据处理加工类的业务逻辑:希望能够把一些数据处理类的业务逻辑,利用消息产品提供的 Serverless ETL 能力进行处理,并可以根据业务需要,利用函数计算快速实现源端和目标端的扩展。
参考:
Serverless的业务使用场景是?
针对一些算法任务、高性能计算以及 AI 相关的一些推理任务、广告图片的识别、智能的运维的一些能力。 通常这些部分属于业务系统相对独立的部分,利用函数计算,快速部署了一个用于推理的算法模型, 通过请求的方式将推理需要的图片,或者一组参数提到给函数。如果中间想引入一层解耦逻辑的话,可以使用 MQ, 再利用 MQ 的事件触发任务执行。最后的结果会提交到 OSS,可以利用事件驱动进一步对产生的文件进行处理。
消费电子领域客户 Serverless 解决方案,IOT 相关的视频传输,从 IOT 上采集数据进一步进行分析,最后支持客户端消费这些视频数据。
微博针对图片访问和处理加工的 Serverless 场景案例,利用函数计算,实现冷数据的访问和图片的个性化处理。
教育行业有一个很明显的现象,存在普遍的编码转播、直播录制、直播内容审核的需求,需要在直播的过程中,通过截帧的方式去审核直播内容的合法性。
文娱行业,针对影片内容动态截帧审核,以及切片转码的 Serverless 解决方案。下图展示的是南瓜电影的一个技术方案,使用函数计算去实现这样一个能力。
游戏行业数据加工、战斗结算、游戏发包等场景。游戏业界几家头部客户目前都已经在使用这样一些 Serverless 方案组合,实现自己的业务系统。这些场景都是很特点,以战斗结算为例,并不是在你副本执行任务的过程中需要不断实时的进行计算,通常只有在一个副本可能快要结束的时候,或者在副本执行任务过程中需要进行战斗的时候才需要计算,是典型的 Serverless 应用场景。
参考: