1、什么是高并发html
高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它一般是指,经过设计保证系统可以同时并行处理不少请求。mysql
高并发相关经常使用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。nginx
响应时间:系统对请求作出响应的时间。例如系统处理一个HTTP请求须要200ms,这个200ms就是系统的响应时间。程序员
吞吐量:单位时间内处理的请求数量。web
QPS:每秒响应请求数。在互联网领域,这个指标和吞吐量区分的没有这么明显。redis
并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通信系统,同时在线量必定程度上表明了系统的并发用户数。sql
2、如何提高系统的并发能力数据库
互联网分布式架构设计,提升系统并发能力的方式,方法论上主要有两种:垂直扩展(Scale Up)与水平扩展(Scale Out)。编程
垂直扩展:提高单机处理能力。垂直扩展的方式又有两种:json
(1)加强单机硬件性能,例如:增长CPU核数如32核,升级更好的网卡如万兆,升级更好的硬盘如SSD,扩充硬盘容量如2T,扩充系统内存如128G;
(2)提高单机架构性能,例如:使用Cache来减小IO次数,使用异步来增长单服务吞吐量,使用无锁数据结构来减小响应时间;
在互联网业务发展很是迅猛的早期,若是预算不是问题,强烈建议使用“加强单机硬件性能”的方式提高系统并发能力,由于这个阶段,公司的战略每每是发展业务抢时间,而“加强单机硬件性能”每每是最快的方法。
无论是提高单机硬件性能,仍是提高单机架构性能,都有一个致命的不足:单机性能老是有极限的。因此互联网分布式架构设计高并发终极解决方案仍是水平扩展。
水平扩展:只要增长服务器数量,就能线性扩充系统性能。水平扩展对系统架构设计是有要求的,如何在架构各层进行可水平扩展的设计,以及互联网公司架构各层常见的水平扩展实践,是本文重点讨论的内容。
3、常见的互联网分层架构
常见互联网分布式架构如上,分为:
(1)客户端层:典型调用方是浏览器browser或者手机应用APP
(2)反向代理层:系统入口,反向代理
(3)站点应用层:实现核心应用逻辑,返回html或者json
(4)服务层:若是实现了服务化,就有这一层
(5)数据-缓存层:缓存加速访问存储
(6)数据-数据库层:数据库固化数据存储
整个系统各层次的水平扩展,又分别是如何实施的呢?
4、分层水平扩展架构实践
反向代理层的水平扩展
反向代理层的水平扩展,是经过“DNS轮询”实现的:dns-server对于一个域名配置了多个解析ip,每次DNS解析请求来访问dns-server,会轮询返回这些ip。
当nginx成为瓶颈的时候,只要增长服务器数量,新增nginx服务的部署,增长一个外网ip,就能扩展反向代理层的性能,作到理论上的无限高并发。
站点层的水平扩展
站点层的水平扩展,是经过“nginx”实现的。经过修改nginx.conf,能够设置多个web后端。
当web后端成为瓶颈的时候,只要增长服务器数量,新增web服务的部署,在nginx配置中配置上新的web后端,就能扩展站点层的性能,作到理论上的无限高并发。
服务层的水平扩展
服务层的水平扩展,是经过“服务链接池”实现的。
站点层经过RPC-client调用下游的服务层RPC-server时,RPC-client中的链接池会创建与下游服务多个链接,当服务成为瓶颈的时候,只要增长服务器数量,新增服务部署,在RPC-client处创建新的下游服务链接,就能扩展服务层性能,作到理论上的无限高并发。若是须要优雅的进行服务层自动扩容,这里可能须要配置中内心服务自动发现功能的支持。
数据层的水平扩展
在数据量很大的状况下,数据层(缓存,数据库)涉及数据的水平扩展,将本来存储在一台服务器上的数据(缓存,数据库)水平拆分到不一样服务器上去,以达到扩充系统性能的目的。
互联网数据层常见的水平拆分方式有这么几种,以数据库为例:
按照范围水平拆分
每个数据服务,存储必定范围的数据,上图为例:
user0库,存储uid范围1-1kw
user1库,存储uid范围1kw-2kw
这个方案的好处是:
(1)规则简单,service只需判断一下uid范围就能路由到对应的存储服务;
(2)数据均衡性较好;
(3)比较容易扩展,能够随时加一个uid[2kw,3kw]的数据服务;
不足是:
(1) 请求的负载不必定均衡,通常来讲,新注册的用户会比老用户更活跃,大range的服务请求压力会更大;
按照哈希水平拆分
每个数据库,存储某个key值hash后的部分数据,上图为例:
user0库,存储偶数uid数据
user1库,存储奇数uid数据
这个方案的好处是:
(1)规则简单,service只需对uid进行hash能路由到对应的存储服务;
(2)数据均衡性较好;
(3)请求均匀性较好;
不足是:
(1)不容易扩展,扩展一个数据服务,hash方法改变时候,可能须要进行数据迁移;
这里须要注意的是,经过水平拆分来扩充系统性能,与主从同步读写分离来扩充数据库性能的方式有本质的不一样。
经过水平拆分扩展数据库性能:
(1)每一个服务器上存储的数据量是总量的1/n,因此单机的性能也会有提高;
(2)n个服务器上的数据没有交集,那个服务器上数据的并集是数据的全集;
(3)数据水平拆分到了n个服务器上,理论上读性能扩充了n倍,写性能也扩充了n倍(其实远不止n倍,由于单机的数据量变为了原来的1/n);
经过主从同步读写分离扩展数据库性能:
(1)每一个服务器上存储的数据量是和总量相同;
(2)n个服务器上的数据都同样,都是全集;
(3)理论上读性能扩充了n倍,写仍然是单点,写性能不变;
缓存层的水平拆分和数据库层的水平拆分相似,也是以范围拆分和哈希拆分的方式居多,就再也不展开。
5、总结
高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它一般是指,经过设计保证系统可以同时并行处理不少请求。
提升系统并发能力的方式,方法论上主要有两种:垂直扩展(Scale Up)与水平扩展(Scale Out)。前者垂直扩展能够经过提高单机硬件性能,或者提高单机架构性能,来提升并发性,但单机性能老是有极限的,互联网分布式架构设计高并发终极解决方案仍是后者:水平扩展。
互联网分层架构中,各层次水平扩展的实践又有所不一样:
(1)反向代理层能够经过“DNS轮询”的方式来进行水平扩展;
(2)站点层能够经过nginx来进行水平扩展;
(3)服务层能够经过服务链接池来进行水平扩展;
(4)数据库能够按照数据范围,或者数据哈希的方式来进行水平扩展;
各层实施水平扩展后,可以经过增长服务器数量的方式来提高系统的性能,作到理论上的性能无限。
1、关于并发咱们说的高并发是什么?
在互联网时代,高并发,一般是指,在某个时间点,有不少个访问同时到来。
高并发,一般关心的系统指标与业务指标?
QPS:每秒钟查询量,广义的,一般指指每秒请求数
响应时间:从请求发出到收到响应花费的时间,例如:系统处理一个HTTP请求须要100ms,这个100ms就是系统的响应时间
带宽:计算带宽大小需关注两个指标,峰值流量和页面的平均大小
PV:综合浏览量(Page View),即页面浏览量或者点击量,一般关注在24小时内访问的页面数量,即“日PV”
UV:独立访问(UniQue Visitor),即去重后的访问用户数,一般关注在24小时内访问的用户,即“日UV”
2、关于三种应对大并发的常见优化方案
【数据库缓存】
为何是要使用缓存?
缓存数据是为了让客户端不多甚至不访问数据库,减小磁盘IO,提升并发量,提升应用数据的响应速度。
【CDN加速】
什么是CDN?
CDN的全称是Content Delivery Network,CDN系统可以实时地根据网络流量和各节点的链接、负载情况以及到用户的距离等综合信息将用户的请求从新导向离用户最近的服务节点上。
使用CDN的优点?
CDN的本质是内存缓存,就近访问,它提升了企业站点(尤为含有大量图片和静态页面站点)的访问速度,跨运营商的网络加速,保证不一样网络的用户都获得良好的访问质量。
同时,减小远程访问的带宽,分担网络流量,减轻原站点WEB服务器负载。
【服务器的集群化,以及负载均衡】
什么是七层负载均衡?
七层负载均衡,是基于http协议等应用信息的负载均衡,最经常使用的就是Nginx,它可以自动剔除工做不正常的后端服务器,上传文件使用异步模式,支持多种分配策略,能够分配权重,分配方式灵活。
内置策略:IP Hash、加权轮询
扩展策略:fair策略、通用hash、一致性hash
什么是加权轮询策略?
首先将请求都分给高权重的机器,直到该机器的权值降到了比其余机器低,才开始将请求分给下一个高权重的机器,即体现了加权权重,又体现了轮询。
1、什么是高可用
高可用HA(High Availability)是分布式系统架构设计中必须考虑的因素之一,它一般是指,经过设计减小系统不能提供服务的时间。
假设系统一直可以提供服务,咱们说系统的可用性是100%。
若是系统每运行100个时间单位,会有1个时间单位没法提供服务,咱们说系统的可用性是99%。
不少公司的高可用目标是4个9,也就是99.99%,这就意味着,系统的年停机时间为8.76个小时。
百度的搜索首页,是业内公认高可用保障很是出色的系统,甚至人们会经过www.baidu.com 能不能访问来判断“网络的连通性”,百度高可用的服务让人留下啦“网络通畅,百度就能访问”,“百度打不开,应该是网络连不上”的印象,这实际上是对百度HA最高的褒奖。
2、如何保障系统的高可用
咱们都知道,单点是系统高可用的大敌,单点每每是系统高可用最大的风险和敌人,应该尽可能在系统设计的过程当中避免单点。方法论上,高可用保证的原则是“集群化”,或者叫“冗余”:只有一个单点,挂了服务会受影响;若是有冗余备份,挂了还有其余backup可以顶上。
保证系统高可用,架构设计的核心准则是:冗余。
有了冗余以后,还不够,每次出现故障须要人工介入恢复势必会增长系统的不可服务实践。因此,又每每是经过“自动故障转移”来实现系统的高可用。
接下来咱们看下典型互联网架构中,如何经过冗余+自动故障转移来保证系统的高可用特性。
3、常见的互联网分层架构
常见互联网分布式架构如上,分为:
(1)客户端层:典型调用方是浏览器browser或者手机应用APP
(2)反向代理层:系统入口,反向代理
(3)站点应用层:实现核心应用逻辑,返回html或者json
(4)服务层:若是实现了服务化,就有这一层
(5)数据-缓存层:缓存加速访问存储
(6)数据-数据库层:数据库固化数据存储
整个系统的高可用,又是经过每一层的冗余+自动故障转移来综合实现的。
4、分层高可用架构实践
【客户端层->反向代理层】的高可用
【客户端层】到【反向代理层】的高可用,是经过反向代理层的冗余来实现的。以nginx为例:有两台nginx,一台对线上提供服务,另外一台冗余以保证高可用,常见的实践是keepalived存活探测,相同virtual IP提供服务。
自动故障转移:当nginx挂了的时候,keepalived可以探测到,会自动的进行故障转移,将流量自动迁移到shadow-nginx,因为使用的是相同的virtual IP,这个切换过程对调用方是透明的。
【反向代理层->站点层】的高可用
【反向代理层】到【站点层】的高可用,是经过站点层的冗余来实现的。假设反向代理层是nginx,nginx.conf里可以配置多个web后端,而且nginx可以探测到多个后端的存活性。
自动故障转移:当web-server挂了的时候,nginx可以探测到,会自动的进行故障转移,将流量自动迁移到其余的web-server,整个过程由nginx自动完成,对调用方是透明的。
【站点层->服务层】的高可用
【站点层】到【服务层】的高可用,是经过服务层的冗余来实现的。“服务链接池”会创建与下游服务多个链接,每次请求会“随机”选取链接来访问下游服务。
自动故障转移:当service挂了的时候,service-connection-pool可以探测到,会自动的进行故障转移,将流量自动迁移到其余的service,整个过程由链接池自动完成,对调用方是透明的(因此说RPC-client中的服务链接池是很重要的基础组件)。
【服务层>缓存层】的高可用
【服务层】到【缓存层】的高可用,是经过缓存数据的冗余来实现的。
缓存层的数据冗余又有几种方式:第一种是利用客户端的封装,service对cache进行双读或者双写。
缓存层也能够经过支持主从同步的缓存集群来解决缓存层的高可用问题。
以redis为例,redis自然支持主从同步,redis官方也有sentinel哨兵机制,来作redis的存活性检测。
自动故障转移:当redis主挂了的时候,sentinel可以探测到,会通知调用方访问新的redis,整个过程由sentinel和redis集群配合完成,对调用方是透明的。
说完缓存的高可用,这里要多说一句,业务对缓存并不必定有“高可用”要求,更多的对缓存的使用场景,是用来“加速数据访问”:把一部分数据放到缓存里,若是缓存挂了或者缓存没有命中,是能够去后端的数据库中再取数据的。
这类容许“cache miss”的业务场景,缓存架构的建议是:
将kv缓存封装成服务集群,上游设置一个代理(代理能够用集群冗余的方式保证高可用),代理的后端根据缓存访问的key水平切分红若干个实例,每一个实例的访问并不作高可用。
缓存实例挂了屏蔽:当有水平切分的实例挂掉时,代理层直接返回cache miss,此时缓存挂掉对调用方也是透明的。key水平切分实例减小,不建议作re-hash,这样容易引起缓存数据的不一致。
【服务层>数据库层】的高可用
大部分互联网技术,数据库层都用了“主从同步,读写分离”架构,因此数据库层的高可用,又分为“读库高可用”与“写库高可用”两类。
【服务层>数据库层“读”】的高可用
【服务层】到【数据库读】的高可用,是经过读库的冗余来实现的。
既然冗余了读库,通常来讲就至少有2个从库,“数据库链接池”会创建与读库多个链接,每次请求会路由到这些读库。
自动故障转移:当读库挂了的时候,db-connection-pool可以探测到,会自动的进行故障转移,将流量自动迁移到其余的读库,整个过程由链接池自动完成,对调用方是透明的(因此说DAO中的数据库链接池是很重要的基础组件)。
【服务层>数据库层“写”】的高可用
【服务层】到【数据库写】的高可用,是经过写库的冗余来实现的。
以mysql为例,能够设置两个mysql双主同步,一台对线上提供服务,另外一台冗余以保证高可用,常见的实践是keepalived存活探测,相同virtual IP提供服务。
自动故障转移:当写库挂了的时候,keepalived可以探测到,会自动的进行故障转移,将流量自动迁移到shadow-db-master,因为使用的是相同的virtual IP,这个切换过程对调用方是透明的。
5、总结
高可用HA(High Availability)是分布式系统架构设计中必须考虑的因素之一,它一般是指,经过设计减小系统不能提供服务的时间。
方法论上,高可用是经过冗余+自动故障转移来实现的。
整个互联网分层系统架构的高可用,又是经过每一层的冗余+自动故障转移来综合实现的,具体的:
(1)【客户端层】到【反向代理层】的高可用,是经过反向代理层的冗余实现的,常见实践是keepalived + virtual IP自动故障转移
(2)【反向代理层】到【站点层】的高可用,是经过站点层的冗余实现的,常见实践是nginx与web-server之间的存活性探测与自动故障转移
(3)【站点层】到【服务层】的高可用,是经过服务层的冗余实现的,常见实践是经过service-connection-pool来保证自动故障转移
(4)【服务层】到【缓存层】的高可用,是经过缓存数据的冗余实现的,常见实践是缓存客户端双读双写,或者利用缓存集群的主从数据同步与sentinel保活与自动故障转移;更多的业务场景,对缓存没有高可用要求,可使用缓存服务化来对调用方屏蔽底层复杂性
(5)【服务层】到【数据库“读”】的高可用,是经过读库的冗余实现的,常见实践是经过db-connection-pool来保证自动故障转移
(6)【服务层】到【数据库“写”】的高可用,是经过写库的冗余实现的,常见实践是keepalived + virtual IP自动故障转移
互联网公司,这样的场景是否似曾相识:
场景一: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等评估亦可遵循相似的步骤。
互联网架构设计如何进行容量评估:
【步骤一:评估总访问量】 -> 询问业务、产品、运营
【步骤二:评估平均访问量QPS】-> 除以时间,一天算4w秒
【步骤三:评估高峰QPS】 -> 根据业务曲线图来
【步骤四:评估系统、单机极限QPS】 -> 压测很重要
【步骤五:根据线上冗余度回答两个问题】 -> 估计冗余度与线上冗余度差值
【业务场景】
有一类写多读少的业务场景:大部分请求是对数据进行修改,少部分请求对数据进行读取。
例子1:滴滴打车,某个司机地理位置信息的变化(可能每几秒钟有一个修改),以及司机地理位置的读取(用户打车的时候查看某个司机的地理位置)。
void SetDriverInfo(long driver_id, DriverInfoi); // 大量请求调用修改司机信息,可能主要是GPS位置的修改
DriverInfo GetDriverInfo(long driver_id); // 少许请求查询司机信息
例子2:统计计数的变化,某个url的访问次数,用户某个行为的反做弊计数(计数值在不停的变)以及读取(只有少数时刻会读取这类数据)。
void AddCountByType(long type); // 大量增长某个类型的计数,修改比较频繁
long GetCountByType(long type); // 少许返回某个类型的计数
【底层实现】
具体到底层的实现,每每是一个Map(本质是一个定长key,定长value的缓存结构)来存储司机的信息,或者某个类型的计数。
Map<driver_id, DriverInfo>
Map<type, count>
【临界资源】
这个Map存储了全部信息,当并发读写访问时,它做为临界资源,在读写以前,通常要进行加锁操做,以司机信息存储为例:
void SetDriverInfo(long driver_id, DriverInfoinfo){
WriteLock (m_lock);
Map<driver_id>= info;
UnWriteLock(m_lock);
}
DriverInfo GetDriverInfo(long driver_id){
DriverInfo t;
ReadLock(m_lock);
t= Map<driver_id>;
UnReadLock(m_lock);
return t;
}
【并发锁瓶颈】
假设滴滴有100w司机同时在线,每一个司机没5秒更新一次经纬度状态,那么每秒就有20w次写并发操做。假设滴滴日订单1000w个,平均每秒大概也有300个下单,对应到查询并发量,多是1000级别的并发读操做。
上述实现方案没有任何问题,但在并发量很大的时候(每秒20w写,1k读),锁m_lock会成为潜在瓶颈,在这类高并发环境下写多读少的业务仓井,如何来进行优化,是本文将要讨论的问题。
上文中之因此锁冲突严重,是由于全部司机都公用一把锁,锁的粒度太粗(能够认为是一个数据库的“库级别锁”),是否可能进行水平拆分(相似于数据库里的分库),把一个库锁变成多个库锁,来提升并发,下降锁冲突呢?显然是能够的,把1个Map水平切分红多个Map便可:
void SetDriverInfo(long driver_id, DriverInfoinfo){
i= driver_id % N; // 水平拆分红N份,N个Map,N个锁
WriteLock (m_lock [i]); //锁第i把锁
Map[i]<driver_id>= info; // 操做第i个Map
UnWriteLock (m_lock[i]); // 解锁第i把锁
}
每一个Map的并发量(变成了1/N)和数据量都下降(变成了1/N)了,因此理论上,锁冲突会成平方指数下降。
分库以后,仍然是库锁,有没有办法变成数据库层面所谓的“行级锁”呢,难道要把x条记录变成x个Map吗,这显然是不现实的。
假设driver_id是递增生成的,而且缓存的内存比较大,是能够把Map优化成Array,而不是拆分红N个Map,是有可能把锁的粒度细化到最细的(每一个记录一个锁)。
void SetDriverInfo(long driver_id, DriverInfoinfo){
index= driver_id;
WriteLock (m_lock [index]); //超级大内存,一条记录一个锁,锁行锁
Array[index]= info; //driver_id就是Array下标
UnWriteLock (m_lock[index]); // 解锁行锁
}
和上一个方案相比,这个方案使得锁冲突降到了最低,但锁资源大增,在数据量很是大的状况下,通常不这么搞。数据量比较小的时候,能够一个元素一个锁的(典型的是链接池,每一个链接有一个锁表示链接是否可用)。
上文中提到的另外一个例子,用户操做类型计数,操做类型是有限的,即便一个type一个锁,锁的冲突也多是很高的,尚未方法进一步提升并发呢?
【无锁的结果】
void AddCountByType(long type /*, int count*/){
//不加锁
Array[type]++; // 计数++
//Array[type] += count; // 计数增长count
}
若是这个缓存不加锁,固然能够达到最高的并发,可是多线程对缓存中同一块定长数据进行操做时,有可能出现不一致的数据块,这个方案为了提升性能,牺牲了一致性。在读取计数时,获取到了错误的数据,是不能接受的(做为缓存,容许cache miss,却不容许读脏数据)。
【脏数据是如何产生的】
这个并发写的脏数据是如何产生的呢,详见下图:
1)线程1对缓存进行操做,对key想要写入value1
2)线程2对缓存进行操做,对key想要写入value2
3)若是不加锁,线程1和线程2对同一个定长区域进行一个并发的写操做,可能每一个线程写成功一半,致使出现脏数据产生,最终的结果即不是value1也不是value2,而是一个乱七八糟的不符合预期的值value-unexpected。
【数据完整性问题】
并发写入的数据分别是value1和value2,读出的数据是value-unexpected,数据的篡改,这本质上是一个数据完整性的问题。一般如何保证数据的完整性呢?
例子1:运维如何保证,从中控机分发到上线机上的二进制没有被篡改?
回答:md5
例子2:即时通信系统中,如何保证接受方收到的消息,就是发送方发送的消息?
回答:发送方除了发送消息自己,还要发送消息的签名,接收方收到消息后要校验签名,以确保消息是完整的,未被篡改。
当当当当 => “签名”是一种常见的保证数据完整性的常见方案。
【加上签名以后的流程】
加上签名以后,不但缓存要写入定长value自己,还要写入定长签名(例如16bitCRC校验):
1)线程1对缓存进行操做,对key想要写入value1,写入签名v1-sign
2)线程2对缓存进行操做,对key想要写入value2,写入签名v2-sign
3)若是不加锁,线程1和线程2对同一个定长区域进行一个并发的写操做,可能每一个线程写成功一半,致使出现脏数据产生,最终的结果即不是value1也不是value2,而是一个乱七八糟的不符合预期的值value-unexpected,但签名,必定是v1-sign或者v2-sign中的任意一个
4)数据读取的时候,不但要取出value,还要像消息接收方收到消息同样,校验一下签名,若是发现签名不一致,缓存则返回NULL,即cache miss。
固然,对应到司机地理位置,与URL访问计数的case,除了内存缓存以前,确定须要timer对缓存中的数据按期落盘,写入数据库,若是cache miss,能够从数据库中读取数据。
在【超高并发】,【写多读少】,【定长value】的【业务缓存】场景下:
1)能够经过水平拆分来下降锁冲突
2)能够经过Map转Array的方式来最小化锁冲突,一条记录一个锁
3)能够把锁去掉,最大化并发,但带来的数据完整性的破坏
4)能够经过签名的方式保证数据的完整性,实现无锁缓存
咱们常用事务来保证数据库层面数据的ACID特性。
举个栗子,用户下了一个订单,须要修改余额表,订单表,流水表,因而会有相似的伪代码:
start transaction;
CURDtable t_account; any Exception rollback;
CURDtable t_order; any Exceptionrollback;
CURDtable t_flow; any Exceptionrollback;
commit;
若是对余额表,订单表,流水表的SQL操做所有成功,则所有提交,若是任何一个出现问题,则所有回滚,以保证数据的一致性。
互联网的业务特色,数据量较大,并发量较大,常用拆库的方式提高系统的性能。若是进行了拆库,余额、订单、流水可能分布在不一样的数据库上,甚至不一样的数据库实例上,此时就不能用事务来保证数据的一致性了。这种状况下如何保证数据的一致性,是今天要讨论的话题。
补偿事务是一种在业务端实施业务逆向操做事务,来保证业务数据一致性的方式。
举个栗子,修改余额表事务为
int Do_AccountT(uid, money){
start transaction;
//余额改变money这么多
CURDtable t_account with money; anyException rollback return NO;
commit;
return YES;
}
那么补偿事务能够是:
int Compensate_AccountT(uid, money){
//作一个money的反向操做
returnDo_AccountT(uid, -1*money){
}
同理,订单表操做为
Do_OrderT,新增一个订单
Compensate_OrderT,删除一个订单
要保重余额与订单的一致性,可能要写这样的代码:
// 执行第一个事务
int flag = Do_AccountT();
if(flag=YES){
//第一个事务成功,则执行第二个事务
flag= Do_OrderT();
if(flag=YES){
// 第二个事务成功,则成功
returnYES;
}
else{
// 第二个事务失败,执行第一个事务的补偿事务
Compensate_AccountT();
}
}
该方案的不足是:
(1)不一样的业务要写不一样的补偿事务,不具有通用性
(2)没有考虑补偿事务的失败
(3)若是业务流程很复杂,if/else会嵌套很是多层
例如,若是上面的例子加上流水表的修改,加上Do_FlowT和Compensate_FlowT,可能会变成一个这样的if/else:
// 执行第一个事务
int flag = Do_AccountT();
if(flag=YES){
//第一个事务成功,则执行第二个事务
flag= Do_OrderT();
if(flag=YES){
// 第二个事务成功,则执行第三个事务
flag= Do_FlowT();
if(flag=YES){
//第三个事务成功,则成功
returnYES;
}
else{
// 第三个事务失败,则执行第2、第一个事务的补偿事务
flag =Compensate_OrderT();
if … else … // 补偿事务执行失败?
flag= Compensate_AccountT();
if … else … // 补偿事务执行失败?
}
}
else{
// 第二个事务失败,执行第一个事务的补偿事务
Compensate_AccountT();
if … else … // 补偿事务执行失败?
}
}
单库是用这样一个大事务保证一致性:
start transaction;
CURDtable t_account; any Exception rollback;
CURDtable t_order; any Exceptionrollback;
CURDtable t_flow; any Exceptionrollback;
commit;
拆分红了多个库,大事务会变成三个小事务:
start transaction1;
//第一个库事务执行
CURDtable t_account; any Exception rollback;
…
// 第一个库事务提交
commit1;
start transaction2;
//第二个库事务执行
CURDtable t_order; any Exceptionrollback;
…
// 第二个库事务提交
commit2;
start transaction3;
//第三个库事务执行
CURDtable t_flow; any Exceptionrollback;
…
// 第三个库事务提交
commit3;
一个事务,分红执行与提交两个阶段,执行的时间实际上是很长的,而commit的执行实际上是很快的,因而整个执行过程的时间轴以下:
第一个事务执行200ms,提交1ms;
第二个事务执行120ms,提交1ms;
第三个事务执行80ms,提交1ms;
那在何时系统出现问题,会出现不一致呢?
回答:第一个事务成功提交以后,最后一个事务成功提交以前,若是出现问题(例如服务器重启,数据库异常等),均可能致使数据不一致。
若是改变事务执行与提交的时序,变成事务先执行,最后一块儿提交,状况会变成什么样呢:
第一个事务执行200ms;
第二个事务执行120ms;
第三个事务执行80ms;
第一个事务执行1ms;
第二个事务执行1ms;
第三个事务执行1ms;
那在何时系统出现问题,会出现不一致呢?
问题的答案与以前相同:第一个事务成功提交以后,最后一个事务成功提交以前,若是出现问题(例如服务器重启,数据库异常等),均可能致使数据不一致。
这个变化的意义是什么呢?
方案一总执行时间是303ms,最后202ms内出现异常均可能致使不一致;
方案二总执行时间也是303ms,但最后2ms内出现异常才会致使不一致;
虽然没有完全解决数据的一致性问题,但不一致出现的几率大大下降了!
事务提交后置下降了数据不一致的出现几率,会带来什么反作用呢?
回答:事务提交时会释放数据库的链接,第一种方案,第一个库事务提交,数据库链接就释放了,后置事务提交的方案,全部库的链接,要等到全部事务执行完才释放。这就意味着,数据库链接占用的时间增加了,系统总体的吞吐量下降了。
trx1.exec();
trx1.commit();
trx2.exec();
trx2.commit();
trx3.exec();
trx3.commit();
优化为:
trx1.exec();
trx2.exec();
trx3.exec();
trx1.commit();
trx2.commit();
trx3.commit();
这个小小的改动(改动成本极低),不能完全解决多库分布式事务数据一致性问题,但能大大下降数据不一致的几率,带来的反作用是数据库链接占用时间会增加,吞吐量会下降。对于一致性与吞吐量的折衷,还须要业务架构师谨慎权衡折衷。
1、需求缘起
Web-Server一般有个配置,最大工做线程数,后端服务通常也有个配置,工做线程池的线程数量,这个线程数的配置不一样的业务架构师有不一样的经验值,有些业务设置为CPU核数的2倍,有些业务设置为CPU核数的8倍,有些业务设置为CPU核数的32倍。
“工做线程数”的设置依据是什么,到底设置为多少可以最大化CPU性能,是本文要讨论的问题。
在进行进一步深刻讨论以前,先以提问的方式就一些共性认知达成一致。
提问:工做线程数是否是设置的越大越好?
回答:确定不是的
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线程也可以提升并发,由于这个线程在休息的时候,其余的线程能够继续工做
了解常见的服务线程模型,有助于理解服务并发的原理,通常来讲互联网常见的服务线程模型有以下两种
IO线程与工做线程经过队列解耦类模型
如上图,大部分Web-Server与服务框架都是使用这样的一种“IO线程与Worker线程经过队列解耦”类线程模型:
1)有少数几个IO线程监听上游发过来的请求,并进行收发包(生产者)
2)有一个或者多个任务队列,做为IO线程与Worker线程异步解耦的数据传输通道(临界资源)
3)有多个工做线程执行正真的任务(消费者)
这个线程模型应用很广,符合大部分场景,这个线程模型的特色是,工做线程内部是同步阻塞执行任务的(回想一下tomcat线程中是怎么执行Java程序的,dubbo工做线程中是怎么执行任务的),所以能够经过增长Worker线程数来增长并发能力,今天要讨论的重点是“该模型Worker线程数设置为多少能达到最大的并发”。
纯异步线程模型
任何地方都没有阻塞,这种线程模型只须要设置不多的线程数就可以作到很高的吞吐量,Lighttpd有一种单进程单线程模式,并发处理能力很强,就是使用的的这种模型。该模型的缺点是:
1)若是使用单线程模式,难以利用多CPU多核的优点
2)程序员更习惯写同步代码,callback的方式对代码的可读性有冲击,对程序员的要求也更高
3)框架更复杂,每每须要server端收发组件,server端队列,client端收发组件,client端队列,上下文管理组件,有限状态机组件,超时管理组件的支持
however,这个模型不是今天讨论的重点。
了解工做线程的工做模式,对量化分析线程数的设置很是有帮助:
上图是一个典型的工做线程的处理过程,从开始处理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将报文在网络上传回工做线程
最后一块儿来回答工做线程数设置为多少合理的问题。
经过上面的分析,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计算的时间不多,因此设置几十或者几百个工做线程也都是可能的。
N核服务器,经过执行业务的单线程分析出本地计算时间为x,等待时间为y,则工做线程数(线程池线程数)设置为 N*(x+y)/x,能让CPU的利用率最大化