首页 文章 精选 留言 我的

精选列表

搜索[多分片],共10000篇文章
优秀的个人博客,低调大师

“12306”的架构到底有多牛逼?

关注我们,设为星标,每天7:30不见不散,架构路上与您共享 回复"架构师"获取资源 作者丨绘你一世倾城 来源:https://juejin.im/post/5d84e21f6fb9a06ac8248149 “ 每到节假日期间,一二线城市返乡、外出游玩的人们几乎都面临着一个问题:抢火车票! 12306 抢票,极限并发带来的思考 虽然现在大多数情况下都能订到票,但是放票瞬间即无票的场景,相信大家都深有体会。 尤其是春节期间,大家不仅使用 12306,还会考虑“智行”和其他的抢票软件,全国上下几亿人在这段时间都在抢票。 “12306 服务”承受着这个世界上任何秒杀系统都无法超越的 QPS,上百万的并发再正常不过了! 笔者专门研究了一下“12306”的服务端架构,学习到了其系统设计上很多亮点,在这里和大家分享一下并模拟一个例子:如何在 100 万人同时抢 1 万张火车票时,系统提供正常、稳定的服务。 Github代码地址: https://github.com/GuoZhaoran/spikeSystem 大型高并发系统架构 高并发的系统架构都会采用分布式集群部署,服务上层有着层层负载均衡,并提供各种容灾手段(双火机房、节点容错、服务器灾备等)保证系统的高可用,流量也会根据不同的负载能力和配置策略均衡到不同的服务器上。 下边是一个简单的示意图: 负载均衡简介 上图中描述了用户请求到服务器经历了三层的负载均衡,下边分别简单介绍一下这三种负载均衡。 ①OSPF(开放式最短链路优先)是一个内部网关协议(Interior Gateway Protocol,简称IGP) OSPF 通过路由器之间通告网络接口的状态来建立链路状态数据库,生成最短路径树,OSPF 会自动计算路由接口上的 Cost 值,但也可以通过手工指定该接口的 Cost 值,手工指定的优先于自动计算的值。 OSPF 计算的 Cost,同样是和接口带宽成反比,带宽越高,Cost 值越小。到达目标相同 Cost 值的路径,可以执行负载均衡,最多 6 条链路同时执行负载均衡。 ②LVS (Linux Virtual Server) 它是一种集群(Cluster)技术,采用 IP 负载均衡技术和基于内容请求分发技术。 调度器具有很好的吞吐率,将请求均衡地转移到不同的服务器上执行,且调度器自动屏蔽掉服务器的故障,从而将一组服务器构成一个高性能的、高可用的虚拟服务器。 ③Nginx 想必大家都很熟悉了,是一款非常高性能的 HTTP 代理/反向代理服务器,服务开发中也经常使用它来做负载均衡。 Nginx 实现负载均衡的方式主要有三种: 轮询 加权轮询 IPHash轮询 下面我们就针对 Nginx 的加权轮询做专门的配置和测试。 Nginx 加权轮询的演示 Nginx 实现负载均衡通过 Upstream 模块实现,其中加权轮询的配置是可以给相关的服务加上一个权重值,配置的时候可能根据服务器的性能、负载能力设置相应的负载。 下面是一个加权轮询负载的配置,我将在本地的监听 3001-3004 端口,分别配置 1,2,3,4 的权重: #配置负载均衡upstreamload_rule{server127.0.0.1:3001weight=1;server127.0.0.1:3002weight=2;server127.0.0.1:3003weight=3;server127.0.0.1:3004weight=4;}...server{listen80;server_nameload_balance.comwww.load_balance.com;location/{proxy_passhttp://load_rule;}} 我在本地/etc/hosts 目录下配置了 www.load_balance.com 的虚拟域名地址。 接下来使用 Go 语言开启四个 HTTP 端口监听服务,下面是监听在 3001 端口的 Go 程序,其他几个只需要修改端口即可: packagemainimport("net/http""os""strings")funcmain(){http.HandleFunc("/buy/ticket",handleReq)http.ListenAndServe(":3001",nil)}//处理请求函数,根据请求将响应结果信息写入日志funchandleReq(whttp.ResponseWriter,r*http.Request){failedMsg:="handleinport:"writeLog(failedMsg,"./stat.log")}//写入日志funcwriteLog(msgstring,logPathstring){fd,_:=os.OpenFile(logPath,os.O_RDWR|os.O_CREATE|os.O_APPEND,0644)deferfd.Close()content:=strings.Join([]string{msg,"\r\n"},"3001")buf:=[]byte(content)fd.Write(buf)} 我将请求的端口日志信息写到了 ./stat.log 文件当中,然后使用 AB 压测工具做压测: ab-n1000-c100http://www.load_balance.com/buy/ticket 统计日志中的结果,3001-3004 端口分别得到了 100、200、300、400的请求量。 这和我在 Nginx 中配置的权重占比很好的吻合在了一起,并且负载后的流量非常的均匀、随机。 具体的实现大家可以参考 Nginx 的 Upsteam 模块实现源码,这里推荐一篇文章《Nginx 中 Upstream 机制的负载均衡》: https://www.kancloud.cn/digest/understandingnginx/202607 秒杀抢购系统选型 回到我们最初提到的问题中来:火车票秒杀系统如何在高并发情况下提供正常、稳定的服务呢? 从上面的介绍我们知道用户秒杀流量通过层层的负载均衡,均匀到了不同的服务器上,即使如此,集群中的单机所承受的 QPS 也是非常高的。如何将单机性能优化到极致呢? 要解决这个问题,我们就要想明白一件事: 通常订票系统要处理生成订单、减扣库存、用户支付这三个基本的阶段。 我们系统要做的事情是要保证火车票订单不超卖、不少卖,每张售卖的车票都必须支付才有效,还要保证系统承受极高的并发。 这三个阶段的先后顺序该怎么分配才更加合理呢?我们来分析一下: 下单减库存 当用户并发请求到达服务端时,首先创建订单,然后扣除库存,等待用户支付。 这种顺序是我们一般人首先会想到的解决方案,这种情况下也能保证订单不会超卖,因为创建订单之后就会减库存,这是一个原子操作。 但是这样也会产生一些问题: 在极限并发情况下,任何一个内存操作的细节都至关影响性能,尤其像创建订单这种逻辑,一般都需要存储到磁盘数据库的,对数据库的压力是可想而知的。 如果用户存在恶意下单的情况,只下单不支付这样库存就会变少,会少卖很多订单,虽然服务端可以限制 IP 和用户的购买订单数量,这也不算是一个好方法。 支付减库存 如果等待用户支付了订单在减库存,第一感觉就是不会少卖。但是这是并发架构的大忌,因为在极限并发情况下,用户可能会创建很多订单。 当库存减为零的时候很多用户发现抢到的订单支付不了了,这也就是所谓的“超卖”。也不能避免并发操作数据库磁盘 IO。 预扣库存 从上边两种方案的考虑,我们可以得出结论:只要创建订单,就要频繁操作数据库 IO。 那么有没有一种不需要直接操作数据库 IO 的方案呢,这就是预扣库存。先扣除了库存,保证不超卖,然后异步生成用户订单,这样响应给用户的速度就会快很多;那么怎么保证不少卖呢?用户拿到了订单,不支付怎么办? 我们都知道现在订单都有有效期,比如说用户五分钟内不支付,订单就失效了,订单一旦失效,就会加入新的库存,这也是现在很多网上零售企业保证商品不少卖采用的方案。 订单的生成是异步的,一般都会放到 MQ、Kafka 这样的即时消费队列中处理,订单量比较少的情况下,生成订单非常快,用户几乎不用排队。 扣库存的艺术 从上面的分析可知,显然预扣库存的方案最合理。我们进一步分析扣库存的细节,这里还有很大的优化空间,库存存在哪里?怎样保证高并发下,正确的扣库存,还能快速的响应用户请求? 在单机低并发情况下,我们实现扣库存通常是这样的: 为了保证扣库存和生成订单的原子性,需要采用事务处理,然后取库存判断、减库存,最后提交事务,整个流程有很多 IO,对数据库的操作又是阻塞的。 这种方式根本不适合高并发的秒杀系统。 接下来我们对单机扣库存的方案做优化:本地扣库存。 我们把一定的库存量分配到本地机器,直接在内存中减库存,然后按照之前的逻辑异步创建订单。 改进过之后的单机系统是这样的: 这样就避免了对数据库频繁的 IO 操作,只在内存中做运算,极大的提高了单机抗并发的能力。 但是百万的用户请求量单机是无论如何也抗不住的,虽然 Nginx 处理网络请求使用 Epoll 模型,c10k 的问题在业界早已得到了解决。 但是 Linux 系统下,一切资源皆文件,网络请求也是这样,大量的文件描述符会使操作系统瞬间失去响应。 上面我们提到了 Nginx 的加权均衡策略,我们不妨假设将 100W 的用户请求量平均均衡到 100 台服务器上,这样单机所承受的并发量就小了很多。 然后我们每台机器本地库存 100 张火车票,100台服务器上的总库存还是1 万,这样保证了库存订单不超卖,下面是我们描述的集群架构: 问题接踵而至,在高并发情况下,现在我们还无法保证系统的高可用,假如这 100台服务器上有两三台机器因为扛不住并发的流量或者其他的原因宕机了。 那么这些服务器上的订单就卖不出去了,这就造成了订单的少卖。 要解决这个问题,我们需要对总订单量做统一的管理,这就是接下来的容错方案。服务器不仅要在本地减库存,另外要远程统一减库存。 有了远程统一减库存的操作,我们就可以根据机器负载情况,为每台机器分配一些多余的“Buffer 库存”用来防止机器中有机器宕机的情况。 我们结合下面架构图具体分析一下: 我们采用 Redis 存储统一库存,因为 Redis 的性能非常高,号称单机 QPS 能抗 10W 的并发。 在本地减库存以后,如果本地有订单,我们再去请求 Redis 远程减库存,本地减库存和远程减库存都成功了,才返回给用户抢票成功的提示,这样也能有效的保证订单不会超卖。 当机器中有机器宕机时,因为每个机器上有预留的 Buffer 余票,所以宕机机器上的余票依然能够在其他机器上得到弥补,保证了不少卖。 Buffer 余票设置多少合适呢,理论上 Buffer 设置的越多,系统容忍宕机的机器数量就越多,但是 Buffer 设置的太大也会对 Redis 造成一定的影响。 虽然 Redis 内存数据库抗并发能力非常高,请求依然会走一次网络 IO,其实抢票过程中对 Redis 的请求次数是本地库存和 Buffer 库存的总量。 因为当本地库存不足时,系统直接返回用户“已售罄”的信息提示,就不会再走统一扣库存的逻辑。 这在一定程度上也避免了巨大的网络请求量把 Redis 压跨,所以 Buffer 值设置多少,需要架构师对系统的负载能力做认真的考量。 代码演示 Go 语言原生为并发设计,我采用 Go 语言给大家演示一下单机抢票的具体流程。 初始化工作 Go 包中的 Init 函数先于 Main 函数执行,在这个阶段主要做一些准备性工作。 我们系统需要做的准备工作有:初始化本地库存、初始化远程 Redis 存储统一库存的 Hash 键值、初始化 Redis 连接池。 另外还需要初始化一个大小为 1 的 Int 类型 Chan,目的是实现分布式锁的功能。 也可以直接使用读写锁或者使用 Redis 等其他的方式避免资源竞争,但使用 Channel 更加高效,这就是 Go 语言的哲学:不要通过共享内存来通信,而要通过通信来共享内存。 Redis 库使用的是 Redigo,下面是代码实现: ...//localSpike包结构体定义packagelocalSpiketypeLocalSpikestruct{LocalInStockint64LocalSalesVolumeint64}...//remoteSpike对hash结构的定义和redis连接池packageremoteSpike//远程订单存储健值typeRemoteSpikeKeysstruct{SpikeOrderHashKeystring//redis中秒杀订单hash结构keyTotalInventoryKeystring//hash结构中总订单库存keyQuantityOfOrderKeystring//hash结构中已有订单数量key}//初始化redis连接池funcNewPool()*redis.Pool{return&redis.Pool{MaxIdle:10000,MaxActive:12000,//maxnumberofconnectionsDial:func()(redis.Conn,error){c,err:=redis.Dial("tcp",":6379")iferr!=nil{panic(err.Error())}returnc,err},}}...funcinit(){localSpike=localSpike2.LocalSpike{LocalInStock:150,LocalSalesVolume:0,}remoteSpike=remoteSpike2.RemoteSpikeKeys{SpikeOrderHashKey:"ticket_hash_key",TotalInventoryKey:"ticket_total_nums",QuantityOfOrderKey:"ticket_sold_nums",}redisPool=remoteSpike2.NewPool()done=make(chanint,1)done<-1} 本地扣库存和统一扣库存 本地扣库存逻辑非常简单,用户请求过来,添加销量,然后对比销量是否大于本地库存,返回 Bool 值: packagelocalSpike//本地扣库存,返回bool值func(spike*LocalSpike)LocalDeductionStock()bool{spike.LocalSalesVolume=spike.LocalSalesVolume+1returnspike.LocalSalesVolume<spike.LocalInStock} 注意这里对共享数据 LocalSalesVolume 的操作是要使用锁来实现的,但是因为本地扣库存和统一扣库存是一个原子性操作,所以在最上层使用 Channel 来实现,这块后边会讲。 统一扣库存操作 Redis,因为 Redis 是单线程的,而我们要实现从中取数据,写数据并计算一些列步骤,我们要配合 Lua 脚本打包命令,保证操作的原子性: packageremoteSpike......constLuaScript=`localticket_key=KEYS[1]localticket_total_key=ARGV[1]localticket_sold_key=ARGV[2]localticket_total_nums=tonumber(redis.call('HGET',ticket_key,ticket_total_key))localticket_sold_nums=tonumber(redis.call('HGET',ticket_key,ticket_sold_key))--查看是否还有余票,增加订单数量,返回结果值if(ticket_total_nums>=ticket_sold_nums)thenreturnredis.call('HINCRBY',ticket_key,ticket_sold_key,1)endreturn0`//远端统一扣库存func(RemoteSpikeKeys*RemoteSpikeKeys)RemoteDeductionStock(connredis.Conn)bool{lua:=redis.NewScript(1,LuaScript)result,err:=redis.Int(lua.Do(conn,RemoteSpikeKeys.SpikeOrderHashKey,RemoteSpikeKeys.TotalInventoryKey,RemoteSpikeKeys.QuantityOfOrderKey))iferr!=nil{returnfalse}returnresult!=0} 我们使用 Hash 结构存储总库存和总销量的信息,用户请求过来时,判断总销量是否大于库存,然后返回相关的 Bool 值。 在启动服务之前,我们需要初始化 Redis 的初始库存信息: hmsetticket_hash_key"ticket_total_nums"10000"ticket_sold_nums"0 响应用户信息 我们开启一个 HTTP 服务,监听在一个端口上: packagemain...funcmain(){http.HandleFunc("/buy/ticket",handleReq)http.ListenAndServe(":3005",nil)} 上面我们做完了所有的初始化工作,接下来 handleReq 的逻辑非常清晰,判断是否抢票成功,返回给用户信息就可以了。 packagemain//处理请求函数,根据请求将响应结果信息写入日志funchandleReq(whttp.ResponseWriter,r*http.Request){redisConn:=redisPool.Get()LogMsg:=""<-done//全局读写锁iflocalSpike.LocalDeductionStock()&&remoteSpike.RemoteDeductionStock(redisConn){util.RespJson(w,1,"抢票成功",nil)LogMsg=LogMsg+"result:1,localSales:"+strconv.FormatInt(localSpike.LocalSalesVolume,10)}else{util.RespJson(w,-1,"已售罄",nil)LogMsg=LogMsg+"result:0,localSales:"+strconv.FormatInt(localSpike.LocalSalesVolume,10)}done<-1//将抢票状态写入到log中writeLog(LogMsg,"./stat.log")}funcwriteLog(msgstring,logPathstring){fd,_:=os.OpenFile(logPath,os.O_RDWR|os.O_CREATE|os.O_APPEND,0644)deferfd.Close()content:=strings.Join([]string{msg,"\r\n"},"")buf:=[]byte(content)fd.Write(buf)} 前边提到我们扣库存时要考虑竞态条件,我们这里是使用 Channel 避免并发的读写,保证了请求的高效顺序执行。 我们将接口的返回信息写入到了 ./stat.log 文件方便做压测统计。 单机服务压测 开启服务,我们使用 AB 压测工具进行测试: ab-n10000-c100http://127.0.0.1:3005/buy/ticket 下面是我本地低配 Mac 的压测信息: ThisisApacheBench,Version2.3<$revision: 1826891="">Copyright1996AdamTwiss,ZeusTechnologyLtd,http://www.zeustech.net/LicensedtoTheApacheSoftwareFoundation,http://www.apache.org/Benchmarking127.0.0.1(bepatient)Completed1000requestsCompleted2000requestsCompleted3000requestsCompleted4000requestsCompleted5000requestsCompleted6000requestsCompleted7000requestsCompleted8000requestsCompleted9000requestsCompleted10000requestsFinished10000requestsServerSoftware:ServerHostname:127.0.0.1ServerPort:3005DocumentPath:/buy/ticketDocumentLength:29bytesConcurrencyLevel:100Timetakenfortests:2.339secondsCompleterequests:10000Failedrequests:0Totaltransferred:1370000bytesHTMLtransferred:290000bytesRequestspersecond:4275.96[#/sec](mean)Timeperrequest:23.387[ms](mean)Timeperrequest:0.234[ms](mean,acrossallconcurrentrequests)Transferrate:572.08[Kbytes/sec]receivedConnectionTimes(ms)minmean[+/-sd]medianmaxConnect:0814.76223Processing:21517.611232Waiting:11113.58225Total:72322.818239Percentageoftherequestsservedwithinacertaintime(ms)50%1866%2475%2680%2890%3395%3998%4599%54100%239(longestrequest) 根据指标显示,我单机每秒就能处理 4000+ 的请求,正常服务器都是多核配置,处理 1W+ 的请求根本没有问题。 而且查看日志发现整个服务过程中,请求都很正常,流量均匀,Redis 也很正常: //stat.log...result:1,localSales:145result:1,localSales:146result:1,localSales:147result:1,localSales:148result:1,localSales:149result:1,localSales:150result:0,localSales:151result:0,localSales:152result:0,localSales:153result:0,localSales:154result:0,localSales:156... 总结回顾 总体来说,秒杀系统是非常复杂的。我们这里只是简单介绍模拟了一下单机如何优化到高性能,集群如何避免单点故障,保证订单不超卖、不少卖的一些策略 完整的订单系统还有订单进度的查看,每台服务器上都有一个任务,定时的从总库存同步余票和库存信息展示给用户,还有用户在订单有效期内不支付,释放订单,补充到库存等等。 我们实现了高并发抢票的核心逻辑,可以说系统设计的非常的巧妙,巧妙的避开了对 DB 数据库 IO 的操作。 对 Redis 网络 IO 的高并发请求,几乎所有的计算都是在内存中完成的,而且有效的保证了不超卖、不少卖,还能够容忍部分机器的宕机。 我觉得其中有两点特别值得学习总结: ①负载均衡,分而治之 通过负载均衡,将不同的流量划分到不同的机器上,每台机器处理好自己的请求,将自己的性能发挥到极致。 这样系统的整体也就能承受极高的并发了,就像工作的一个团队,每个人都将自己的价值发挥到了极致,团队成长自然是很大的。 ②合理的使用并发和异步 自 Epoll 网络架构模型解决了 c10k 问题以来,异步越来越被服务端开发人员所接受,能够用异步来做的工作,就用异步来做,在功能拆解上能达到意想不到的效果。 这点在 Nginx、Node.JS、Redis 上都能体现,他们处理网络请求使用的 Epoll 模型,用实践告诉了我们单线程依然可以发挥强大的威力。 服务器已经进入了多核时代,Go 语言这种天生为并发而生的语言,完美的发挥了服务器多核优势,很多可以并发处理的任务都可以使用并发来解决,比如 Go 处理 HTTP 请求时每个请求都会在一个 Goroutine 中执行。 总之,怎样合理的压榨 CPU,让其发挥出应有的价值,是我们一直需要探索学习的方向。 到此文章就结束了。如果今天的文章对你在进阶架构师的路上有新的启发和进步,欢迎转发给更多人。欢迎加入架构师社区技术交流群,众多大咖带你进阶架构师,在后台回复“加群”即可入群。 第一期打卡送书5本+1个腾讯视频VIP月卡(11月1日-12月1日) 这些年小编给你分享过的干货 《IDEA 2020.2最新破解教程,有效期到2089年》 《Kubernetes的前世今生》 《你们公司的架构师是什么样的?》 《Docker与CI持续集成/CD持续部署》 《还有40天,Java 11就要横空出世了》 《JDK 10 的 109 项新特性》 《学习微服务的十大理由》 《进大厂必须掌握的50个微服务面试问题》 转发在看就是最大的支持❤️ 本文分享自微信公众号 - Java架构师社区(mush_java_jg)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

60 亿次 for 循环,原来这么多东西

起因 有人在思否论坛上向我付费提问 当时觉得,这个人问的有问题吧。仔细一看,还是有点东西的 问题重现 编写一段 Node.js代码 varhttp=require('http');http.createServer(function(request,response){varnum=0for(vari=1;i<5900000000;i++){num+=i}response.end('Hello'+num);}).listen(8888); 使用 nodemon启动服务,用 time curl调用这个接口 首次需要7.xxs耗时 多次调用后,问题重现 为什么这个耗时突然变高,由于我是调用的是本机服务,我看 CPU使用当时很高,差不多打到 100%了.但是我后面发现不是这个问题. 问题排查 排除掉 CPU问题,看内存消耗占用。 varhttp=require('http');http.createServer(function(request,response){console.log(request.url,'url');letused=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'start',);console.time('测试');letnum=0;for(leti=1;i<5900000000;i++){num+=i;}console.timeEnd('测试');used=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'end',);response.end('Hello'+num);![](https://imgkr2.cn-bj.ufileos.com/13455121-9d87-42c3-a32e-ea999a2cd09b.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=E3cF2kymC92LifrIC5IOfIZQvnk%253D&Expires=1598883364)![](https://imgkr2.cn-bj.ufileos.com/1e7b95df-2a48-41c3-827c-3c24b39f4b5b.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=%252FANTTuhgbpIsXslXMc1qCkj2TMU%253D&Expires=1598883362)}).listen(8888); 测试结果: 内存占用和 CPU都正常 跟字符串拼接有关,此刻关闭字符串拼接(此时为了快速测试,我把循环次数降到 5.9亿次) 发现耗时稳定下来了 定位问题在字符串拼接,先看看字符串拼接的几种方式 一、使用连接符 “+” 把要连接的字符串连起来 vara='java'varb=a+'script' * 只连接100个以下的字符串建议用这种方法最方便 二、使用数组的 join 方法连接字符串 vararr=['hello','java','script']varstr=arr.join("") 比第一种消耗更少的资源,速度也更快 三、使用模板字符串,以反引号( ` )标识 vara='java'varb=`hello${a}script` 四、使用 JavaScript concat() 方法连接字符串 vara='java'varb='script'varstr=a.concat(b) 五、使用对象属性来连接字符串 functionStringConnect(){this.arr=newArray()}StringConnect.prototype.append=function(str){this.arr.push(str)}StringConnect.prototype.toString=function(){returnthis.arr.join("")}varmystr=newStringConnect()mystr.append("abc")mystr.append("def")mystr.append("g")varstr=mystr.toString() 更换字符串的拼接方式 我把字符串拼接换成了数组的 join方式(此时循环 5.9亿次) varhttp=require('http');http.createServer(function(request,response){console.log(request.url,'url');letused=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'start',);console.time('测试');letnum=0;for(leti=1;i<590000000;i++){num+=i;}constarr=['Hello'];arr.push(num);console.timeEnd('测试');used=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'end',);response.end(arr.join(''));}).listen(8888); 测试结果,发现接口调用的耗时稳定了( 注意此时是5.9亿次循环) 《javascript高级程序设计》中,有一段关于字符串特点的描述,原文大概如下: ECMAScript中的字符串是不可变的,也就是说,字符串一旦创建,他们的值就不能改变。要改变某个变量的保存的的字符串,首先要销毁原来的字符串,然后再用另外一个包含新值的字符串填充该变量 就完了? 用 +直接拼接字符串自然会对性能产生一些影响,因为字符串是不可变的,在操作的时候会产生临时字符串副本, +操作符需要消耗时间,重新赋值分配内存需要消耗时间。 但是,我更换了代码后,发现,即使没有字符串拼接,也会耗时不稳定 varhttp=require('http');http.createServer(function(request,response){console.log(request.url,'url');letused=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'start',);console.time('测试');letnum=0;for(leti=1;i<5900000000;i++){//num++;}constarr=['Hello'];//arr[1]=num;console.timeEnd('测试');used=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'end',);response.end('hello');}).listen(8888); 测试结果: 现在我怀疑,不仅仅是字符串拼接的效率问题,更重要的是 for循环的耗时不一致 varhttp=require('http');http.createServer(function(request,response){console.log(request.url,'url');letused=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'start',);letnum=0;console.time('测试');for(leti=1;i<5900000000;i++){//num++;}console.timeEnd('测试');constarr=['Hello'];//arr[1]=num;used=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'end',);response.end('hello');}).listen(8888); 测试运行结果: for循环内部的 i++其实就是变量不断的重新赋值覆盖 经过我的测试发现, 40亿次跟 50亿次的区别,差距很大, 40亿次的for循环,都是稳定的,但是 50亿次就不稳定了. Node.js的 EventLoop: 我们目前被阻塞的状态: 我电脑的CPU使用情况 优化方案 遇到了 60亿次的循环,像有使用多进程异步计算的,但是本质上没有解决这部分循环代码的调用耗时。 改变策略,拆解单次次数过大的 for循环: varhttp=require('http');http.createServer(function(request,response){console.log(request.url,'url');letused=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'start',);letnum=0;console.time('测试');for(leti=1;i<600000;i++){num++;for(letj=0;j<10000;j++){num++;}}console.timeEnd('测试');constarr=['Hello'];console.log(num,'num');arr[1]=num;used=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'end',);response.end(arr.join(''));}).listen(8888); 结果,耗时基本稳定, 60亿次循环总共: 推翻字符串的拼接耗时说法 修改代码回最原始的 +方式拼接字符串 varhttp=require('http');http.createServer(function(request,response){console.log(request.url,'url');letused=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'start',);letnum=0;console.time('测试');for(leti=1;i<600000;i++){num++;for(letj=0;j<10000;j++){num++;}}console.timeEnd('测试');//constarr=['Hello'];console.log(num,'num');//arr[1]=num;used=process.memoryUsage().heapUsed/1024/1024;console.log(`Thescriptusesapproximately${Math.round(used*100)/100}MB`,'end',);response.end(`Hello`+num);}).listen(8888); 测试结果稳定,符合预期: 总结: 对于单次循环超过一定阀值次数的,用拆解方式, Node.js的运行耗时是稳定,但是如果是循环次数过多,那么就会出现刚才那种情况,阻塞严重,耗时不一样。 为什么? 深度分析问题 遍历60亿次,这个数字是有一些大了,如果是40亿次,是稳定的 这里应该还是跟 CPU有一些 关系,因为 top 查看一直是在 升高 此处虽然不是真正意义上的内存泄漏,但是我们如果在一个循环中不仅要不断更新 i的值到 60亿,还要不断更新 num的值 60亿,内存使用会不断上升,最终出现两份 60亿的数据,然后再回收。( 因为GC自动垃圾回收,一样会阻塞主线程,多次接口调用后, CPU占用也会升高) 使用 for循环拆解后: for(leti=1;i<60000;i++){num++;for(letj=0;j<100000;j++){num++;}} 只要 num到 60亿即可,解决了这个问题。 哪些场景会遇到这个类似的超大计算量问题: 图片处理 加解密 ❝ 如果是异步的业务场景,也可以用多进程参与解决超大计算量问题,今天这里就不重复介绍了 ❞ 最后 如果感觉写得不错,可以点个 在看/ 赞,转发一下,让更多人看到 我是 Peter谭老师,欢迎你关注公众号: 前端巅峰,后台回复: 加群即可加入大前端交流群 本文分享自微信公众号 - 前端巅峰(Java-Script-)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

DataGrip 2020.2 发布,多引擎数据库平台

DataGrip 2020.2 发布了,主要更新内容包括: Data editor 单元格值的单独编辑器 数据编辑器中的 DML 预览 布尔值获得新 UI 新的裁剪数据界面 在数据编辑器的上下文菜单中导出到剪贴板 [MongoDB] 更好的过滤 SQL editor 用于检查的小部件 引入变量 对重命名的建议 更好的 JOIN 补全 刷新数据库快速修复(quick-fix) Google BigQuery 方言 TextMate 捆绑包 适用于通用方言的 SQL 2016 每行选择多个插入符 Database tree view 数据库树中的所有数据库和模式 用于创建视图的 UI [Oracle] 对数据库链接的基本支持 General 标签不再过长 驱动程序设置中的本机库 捆绑了 Git 和 Github 插件 [MongoDB] 为 *.js 文件运行配置 更新说明:https://blog.jetbrains.com/datagrip/2020/07/28/datagrip-2020-2/

优秀的个人博客,低调大师

要问技术多牛,请问IO模型知多少?

1. 引言 同步异步I/O,阻塞非阻塞I/O是程序员老生常谈的话题了,也是自己一直以来懵懵懂懂的一个话题。比如:何为同步异步?何为阻塞与非阻塞?二者的区别在哪里?阻塞在何处?为什么会有多种IO模型,分别用来解决问题?常用的框架采用的是何种I/O模型?各种IO模型的优劣势在哪里,适用于何种应用场景? 简而言之,对于I/O的认知,不能仅仅停留在字面上认识,了解内部玄机,才能深刻理解I/O,才能看清I/O相关问题的本质。 2. I/O 的定义 I/O 的全称是Input/Output。虽常谈及I/O,但想必你也一时不能给出一个完整的定义。搜索了谷歌,发现也尽是些冗长的论述。要想理清I/O这个概念,我们需要从不同的视角去理解它。 2.1. 计算机视角 冯•诺伊曼计算机的基本思想中有提到计算机硬件组成应为五大部分:控制器,运算器,存储器,输入和输出。其中输入是指将数据输入到计算机的设备,比如键盘鼠标;输出是指从计算机中获取数据的设备,比如显示器;以及既是输入又是输出设备,硬盘,网卡等。 用户通过操作系统才能完成对计算机的操作。计算机启动时,第一个启动的程序是操作系统的内核,它将负责计算机的资源管理和进程的调度。换句话说:操作系统负责从输入设备读取数据并将数据写入到输出设备。 所以I/O之于计算机,有两层意思: I/O设备 对I/O设备的数据读写 对于一次I/O操作,必然涉及2个参与方,一个输入端,一个输出端,而又根据参与双方的设备类型,我们又可以分为磁盘I/O,网络I/O(一次网络的请求响应,网卡)等。 2.2. 程序视角 应用程序作为一个文件保存在磁盘中,只有加载到内存到成为一个进程才能运行。应用程序运行在计算机内存中,必然会涉及到数据交换,比如读写磁盘文件,访问数据库,调用远程API等等。但我们编写的程序并不能像操作系统内核一样直接进行I/O操作。 因为为了确保操作系统的安全稳定运行,操作系统启动后,将会开启保护模式:将内存分为内核空间(内核对应进程所在内存空间)和用户空间,进行内存隔离。我们构建的程序将运行在用户空间,用户空间无法操作内核空间,也就意味着用户空间的程序不能直接访问由内核管理的I/O,比如:硬盘、网卡等。 但操作系统向外提供API,其由各种类型的系统调用(System Call)组成,以提供安全的访问控制。所以应用程序要想访问内核管理的I/O,必须通过调用内核提供的系统调用(system call)进行间接访问。 所以I/O之于应用程序来说,强调的通过向内核发起系统调用完成对I/O的间接访问。换句话说应用程序发起的一次IO操作实际包含两个阶段: IO调用阶段:应用程序进程向内核发起系统调用 IO执行阶段:内核执行IO操作并返回 2.1. 准备数据阶段:内核等待I/O设备准备好数据 2.2. 拷贝数据阶段:将数据从内核缓冲区拷贝到用户空间缓冲区 怎么理解准备数据阶段呢?对于写请求:等待系统调用的完整请求数据,并写入内核缓冲区;对于读请求:等待系统调用的完整请求数据;(若请求数据不存在于内核缓冲区)则将外围设备的数据读入到内核缓冲区。 而应用程序进程在发起IO调用至内核执行IO返回之前,应用程序进程/线程所处状态,就是我们下面要讨论的第二个话题阻塞IO与非阻塞IO。 3. IO 模型之阻塞I/O(BIO) 应用程序中进程在发起IO调用后至内核执行IO操作返回结果之前,若发起系统调用的线程一直处于等待状态,则此次IO操作为阻塞IO。阻塞IO简称BIO,Blocking IO。其处理流程如下图所示: 从上图可知当用户进程发起IO系统调用后,内核从准备数据到拷贝数据到用户空间的两个阶段期间用户调用线程选择阻塞等待数据返回。 因此BIO带来了一个问题:如果内核数据需要耗时很久才能准备好,那么用户进程将被阻塞,浪费性能。为了提升应用的性能,虽然可以通过多线程来提升性能,但线程的创建依然会借助系统调用,同时多线程会导致频繁的线程上下文的切换,同样会影响性能。所以要想解决BIO带来的问题,我们就得看到问题的本质,那就是阻塞二字。 4. IO 模型之非阻塞I/O(NIO) 那解决方案自然也容易想到,将阻塞变为非阻塞,那就是用户进程在发起系统调用时指定为非阻塞,内核接收到请求后,就会立即返回,然后用户进程通过轮询的方式来拉取处理结果。也就是如下图所示: 应用程序中进程在发起IO调用后至内核执行IO操作返回结果之前,若发起系统调用的线程不会等待而是立即返回,则此次IO操作为非阻塞IO模型。非阻塞IO简称NIO,Non-Blocking IO。 然而,非阻塞IO虽然相对于阻塞IO大幅提升了性能,但依旧不是完美的解决方案,其依然存在性能问题,也就是频繁的轮询导致频繁的系统调用,会耗费大量的CPU资源。比如当并发很高时,假设有1000个并发,那么单位时间循环内将会有1000次系统调用去轮询执行结果,而实际上可能只有2个请求结果执行完毕,这就会有998次无效的系统调用,造成严重的性能浪费。有问题就要解决,那NIO问题的本质就是频繁轮询导致的无效系统调用。 5. IO模型之IO多路复用 解决NIO的思路就是降解无效的系统调用,如何降解呢?我们一起来看看以下几种IO多路复用的解决思路。 5.1. IO多路复用之select/poll Select是内核提供的系统调用,它支持一次查询多个系统调用的可用状态,当任意一个结果状态可用时就会返回,用户进程再发起一次系统调用进行数据读取。换句话说,就是NIO中N次的系统调用,借助Select,只需要发起一次系统调用就够了。其IO流程如下所示: 但是,select有一个限制,就是存在连接数限制,针对于此,又提出了poll。其与select相比,主要是解决了连接限制。 select/epoll 虽然解决了NIO重复无效系统调用用的问题,但同时又引入了新的问题。问题是: 用户空间和内核空间之间,大量的数据拷贝 内核循环遍历IO状态,浪费CPU时间 换句话说,select/poll虽然减少了用户进程的发起的系统调用,但内核的工作量只增不减。在高并发的情况下,内核的性能问题依旧。所以select/poll的问题本质是:内核存在无效的循环遍历。 5.2. IO多路复用之epoll 针对select/pool引入的问题,我们把解决问题的思路转回到内核上,如何减少内核重复无效的循环遍历呢?变主动为被动,基于事件驱动来实现。其流程图如下所示: epoll相较于select/poll,多了两次系统调用,其中epoll_create建立与内核的连接,epoll_ctl注册事件,epoll_wait阻塞用户进程,等待IO事件。 epoll,已经大大优化了IO的执行效率,但在IO执行的第一阶段:数据准备阶段都还是被阻塞的。所以这是一个可以继续优化的点。 6. IO 模型之信号驱动IO(SIGIO) 信号驱动IO与BIO和NIO最大的区别就在于,在IO执行的数据准备阶段,不会阻塞用户进程。如下图所示:当用户进程需要等待数据的时候,会向内核发送一个信号,告诉内核我要什么数据,然后用户进程就继续做别的事情去了,而当内核中的数据准备好之后,内核立马发给用户进程一个信号,说”数据准备好了,快来查收“,用户进程收到信号之后,立马调用recvfrom,去查收数据。 乍一看,信号驱动式I/O模型有种异步操作的感觉,但是在IO执行的第二阶段,也就是将数据从内核空间复制到用户空间这个阶段,用户进程还是被阻塞的。 综上,你会发现,不管是BIO还是NIO还是SIGIO,它们最终都会被阻塞在IO执行的第二阶段。那如果能将IO执行的第二阶段变成非阻塞,那就完美了。 7. IO 模型之异步IO(AIO) 异步IO真正实现了IO全流程的非阻塞。用户进程发出系统调用后立即返回,内核等待数据准备完成,然后将数据拷贝到用户进程缓冲区,然后发送信号告诉用户进程IO操作执行完毕(与SIGIO相比,一个是发送信号告诉用户进程数据准备完毕,一个是IO执行完毕)。其流程如下: 所以,之所以称为异步IO,取决于IO执行的第二阶段是否阻塞。因此前面讲的BIO,NIO和SIGIO均为同步IO。 8. 总结 梳理完这些IO模型后,之前一直处于懵懂状态的阻塞,非阻塞,同步异步IO,终于算是有个概念了。同时也纠正了自己一直以来的误解,所以一路走来,愈发觉得返璞归真的重要性,只有如此,才能在快速更迭的技术演进中,以不变应万变。 本文综合多方资料写就,难免纰漏,但只有写下来,才能得以指正。所以,烦请各位看官不吝赐教。

优秀的个人博客,低调大师

DataGrip 2020.1.5 发布,多引擎数据库平台

DataGrip 2020.1.5 发布了,更新内容包括: 最重要的 Windows 上的 SQL Server 身份验证问题已修复 其他修复 DBE-9876:以只读模式强制执行将运行正确的查询 DBE-10727:模式比较器遵守列顺序 DBE-10715:[PostgreSQL:DataGrip 不再为 PostgreSQL 10 之前的版本引发时区错误 DBE-10724:[Snowflake]:“修改表”对话框会生成有效的代码 新特性 提供了一个新的快速修复程序:刷新架构(Refresh schema)。如果在刷新数据库后添加了新对象,则此修复程序将确保正确高亮查询。 此版本还重命名了模式比较器中的迁移按钮。 此外,创建 DDL 数据源时,带有源的文件夹将自动附加到项目。 如果尝试在数据导入过程中更改目标表的 DDL,DataGrip 会使用警告框再次确认要执行此操作。 还可以从 DataGrip UI 对 Oracle 数据库的密码进行更改。 更新说明:https://blog.jetbrains.com/datagrip/2020/06/25/datagrip-2020-1-5/

优秀的个人博客,低调大师

超多新功能!Apache APISIX 发布 1.2 版本

Apache APISIX 是云原生 API 网关,不仅可以帮你处理传统的南北向流量,也可以处理服务间的东西向流量。它是基于 Nginx 和 etcd 来实现,和传统 API 网关相比,Apache APISIX 具备动态路由、动态上游和插件热加载的特性,特别适合微服务体系下的 API 管理。 目前有众多国内外知名企业和科研机构正在使用 Apache APISIX,下图列出了主动登记的部分公司,未声明的公司数量众多,无法准确的统计。 本次发布的 Apache APISIX 1.2 版本,是新特性最多的一次发布,新增了 Kafka、CORS、TCP 和 UDP logger、代理缓存、代理镜像等多个插件,主要变更如下: 内核 🌅支持 etcd 集群. #1283 默认使用本地 DNS resolver. 支持在header_filter、body_filter和log阶段运行全局插件. 将目录lua/apisix修改为apisix(不向下兼容). 增加 dashboard 子模块. 允许自定义共享字典. 插件 🌅新增 Apache Kafka 插件. 🌅新增 CORS 插件. 🌅新增 TCP logger 插件. 🌅新增 UDP logger 插件. 🌅新增 proxy mirror 插件. 🌅新增 proxy cache 插件. 在 proxy-rewrite 插件中废弃 websocket 开关(不向下兼容). OAuth 插件中增加基于公钥的自省支持. response-rewrite 插件通过 base64 来支持传输二进制数据. gRPC 转码插件支持deadline. limit count 插件支持 redis 权限认证. Zipkin 插件支持名字和本地服务器 ip 的记录. Wolf-Rbac 插件增加change_pwd和user_info参数. Apache APISIX 的社区也在飞速发展,从加入 Apache 孵化器之初的 20 多位贡献者,增加到现在的 70 多位贡献者,其中包含 19 位 PPMC 和 22 位 committer。 加入 Apache APISIX 如果你希望使用修改任意配置都无需重启的 web 服务器,如果你希望使用代码简洁、云原生友好的微服务网关,那么 Apache APISIX 就是你不二的选择。 可以在 GitHub 上提交 issue 和 PR:https://github.com/apache/incubator-apisix。让我们一起努力,打造世界级的开源项目!

优秀的个人博客,低调大师

DataGrip 2019.3.4 发布,多引擎数据库环境

DataGrip 2019.3.4 发布了,这是 2019.3 系列的第 4 个 bug 修复更新,主要修复内容包括: 更好的子查询折叠 基于DBE-10131,为子查询折叠添加了一个新选项: 重要的错误修复 [Redshift] 同步可与最新的 JDBC 驱动程序一起使用。但是,这可能会导致严重的性能问题,因此,如果遇到任何问题,请报告。 [SQL Server] 导入数据可与 auto_increment 列一起正常使用 当表未解析时,通配符用于预览更新 修复了未使用表情检查中的误报 修复了冗余 COALESCE 检查中的误报 更新说明:https://blog.jetbrains.com/datagrip/2020/03/17/datagrip-2019-3-4/

优秀的个人博客,低调大师

DataGrip 2019.3.3 发布,多引擎数据库环境

DataGrip 2019.3.3 发布了,这是 2019.3 系列的第 3 个 bug 修复更新,主要修复内容包括: 将外键约束添加到表时,将使用正确的 schema。DBE-9743 执行 DROP 后再执行 CREATE 语句后,才会自动同步。DBE-9980 JOIN 补全不再冻结。DBE-9844 [Oracle] OracleSqlPlus 方言又回来了。DBE-10030 [ClickHouse]删除行按预期工作。 [BigQuery]标识符使用反引号引起来。 [BigQuery]打开表的数据时会生成LIMIT 和OFFSET。 更新说明: https://blog.jetbrains.com/datagrip/2020/02/13/datagrip-2019-3-3

优秀的个人博客,低调大师

DataGrip 2019.2.6 发布,多引擎数据库环境

DataGrip 2019.2.6 发布了,更新内容如下: 结果选项卡再次与控制台切换 导出数据时,MacOS Catalina 上不再冻结DBE-9334 修复了 Redshift 中外部表不显示的错误DBE-9181 修复了分页的各种错误DBE-9217 换位数据编辑器时,列名不再折叠DBE-8904 数据导入不再出现“多次指定列”错误DBE-9283 表和列的注释不再被截断DBE-8000 选择“标识”列时,不再有关于值数量的错误警告DBE-8924 发布公告:https://blog.jetbrains.com/datagrip/2019/10/31/datagrip-2019-2-6/

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册