生产事故经验分享-高并发相关技术

演讲大纲:

  1. 讲述当前案例

  2. 总述需要做哪些工作

  3. 介绍高性能

  4. 介绍高可靠性

1、讲述当前案例

各位老师下午好:

我是来自产品研发处的王海波,今天我要分享的生产事故经验是有关高并发的。结合前两个季度我们所复盘过的几十个生产事故案例来看,尽管引起线上事故的原因各不相同,有数据库慢查询、缓存热点数据、消息队列、日志等等,但却具备一个共性,即发生的事故的前提或者说是导火索都是系统的并发流量很大。如果我们开发的系统访问量很低,QPS是两位数的时候,就算我写了一个慢查询,就算我不用缓存,系统也不会有什么问题;可是当你接口或者系统流量较大时,也许就是一个小小的疏忽,也极有可能引起系统崩掉。就像火箭上的任何一个零件有问题,也会引起发射失败。正如墨菲定律说的那样,任何一件事情都没有表面看起来那么简单。

就拿案例1来说,A系统在上线前未对流量做好充分的预估,他只是按照每日的访问量去做设计,没有预料到可能出现的瞬时流量过高的问题。在发券过程中,涉及到多次的数据库的读写,且是以事务形式进行的,当并发量较大时,导致数据库和系统性能下降。总结其几个问题:

1) 直接操作数据库库存字段。 瞬时高并发流量直接落到数据库上,去查询数据库中的活动库存字段,且每次都需要互斥锁避免超卖。导致数据库以及业务接口性能严重下降。

2) 未做任何限流措施。 每次只发2000张券,而在活动开始的时候,请求数是远远大于这个数字的,可以高出几个数量级。每个请求都需要去查数据库库存判断是否已经消耗完。

3) 未采取降级等手段。 在活动期间,一些非必要的查询接口没有进行降级,导致活动期间的一些操作直接影响到了主活动。

4) 硬件配置较差。 营销系统涉及到的链路比较多,有很多的上下游系统,其中一个系统在活动开始前横向或者纵向扩展,服务器配置较低,只有2c4G,且未做机器扩容。

2、总述需要做哪些工作

总的来讲,就幻灯片案例1来说,它就没有针对性去考虑高并发的场景。如何通过缓存减少数据库的访问,如何去做限流,降级,如何使用锁,如何去防止黄牛,去做反作弊等等。如果我们营销优惠力度比较大,就一定会引起黄牛的注意,比如各大电商的抢茅台活动,每次都会有很多黄牛来抢,他们都很专业,所以,基本上凡是涉及到秒杀的系统或者平台一定都会有一个专门的风控团队来去防止作弊。总的来说,想设计出一个完善的秒杀这种高并发系统并不是一件容易的事情,需要考虑的因素很多。

对于高并发系统,它的特点就是 在短时间内同时有大量用户请求访问,我们需要系统能够快速、稳定地响应这些请求 。那设计 高并发系统的核心是实现对资源的有效压榨,可以使用有限的资源去应对大量的请求。

从高并发的指标来看,我们必须要做到系统的高性能以及高可用性。所以在做系统架构设计和开发时,也是要以这两个结果为导向,从不同层面去优化系统。

3、介绍高性能

1、数据库

数据库是系统最重要的组成部分,很多情况下,我们的瓶颈也都处在数据库层面。

读写分离:一主多从。读请求路由到从库,写请求路由到主库。通过读写分离能够分摊单库的压力,但要注意主从延迟的问题,特别是高并发时的读写。

分库分表:当简单的读写分离依然成为性能瓶颈的时候,就需要考虑对数据库进行分库或者分表。通过分库分表解决了单表单库的压力,真正做到了分而治之。

分布式数据库:分库分表是目前用于提升DB性能的比较通用的解决方案,但分库分表后,随着数据量不断增大,仍然有可能遇到瓶颈。如果在继续扩大分库分表的数量,会比较麻烦,如果之前是按照hash取模进行分片的,还要重新对数据进行分片。所以,分库分表是较好的提升性能的方案,但却不是最终的解决方案。下一代解决方案应该是原生分布式数据库,开发者和DBA都不需要自己分库分表,所有的工作都是原生数据库来完成,使用者仍然可以像单库单表一样使用。

目前国内也有很多比较好的分布式数据库产品,如TiDB,还有俄罗斯的ClickHouse,阿里的云分布式数据库。其中TiDB目前也是在很多银行得到了应用,像中国银行,北京银行,光大银行。杭州银行直接就将其应用到了核心系统中了。

2、缓存

在计算机领域,缓存无处不在,比如CPU缓存,磁盘缓存,操作系统的缓存(TLB,PageCache以及应用层面的缓存,比如客户端缓存、CDN缓存、Nginx缓存、DB缓存,DNS缓存等等。通过使用缓存,可以在一定程度上协调不同硬件(网络)之间的处理速度差异的问题,增大数据获取的速度,提升系统的响应性能。尤其是数据库缓存,这个应该是我们开发系统时必不可少的。在DB层上方增加一层缓存层,一是可以提升数据读取的速度,二是可以避免大量数据请求落到DB上,减轻数据库的压力。

案例中其实完全可以把活动库存放到Redis中,像优惠券,满减,满赠,限时购等这些实际上都是营销的一种手段。对于一个平台来讲,营销的最重要且唯一的目的就是引流,造势。因此,对于活动库存,可以少卖,但不能超卖,这是最基本的原则。随后通过Redis+lua脚本就可以防止超卖,并且有较高的性能。我们知道Redis本身的性能瓶颈在于IO,所以在Redis4引入了后台线程,做一些类似lazy free等异步操作,Redis6引入了IO多线程,只是命令执行一直是单线程执行,结合lua脚本,实现命令的原子性,这样就很完美的实现了库存的扣减,也可防止超卖,这也是很多平台在使用的主要方案。

当然,使用缓存也不是万事大吉了,还要注意一些细节问题:

热点数据、Redis bigkey,这些都是可能会影响性能的地方。

一致性:怎么保证缓存数据和原始数据的一致性?是强一致性还是弱一致性?比如CPU缓存,其是通过MESI以及内存屏障来保证一致性的,这个不需要开发者关注,但应用级别的缓存一致性需要我们自己实现。

空间消耗:如何能够利用更小的空间存储更多的数据,因为缓存空间毕竟是有限的,比如可以考虑位图来存储一些统计类的数据。用户是否在线,用户每天的签到情况,每日的用户数,活跃数等等

淘汰机制:需要把一些不经常访问的数据淘汰掉,LRU,LFU,FIFO这些都是我们常见的算法。

雪崩、穿透、击穿:另外还有缓存雪崩,穿透,击穿等等。这些也都是我们平时在使用缓存时需要注意的地方。

3、并发编程

并发编程,主要目的是能够充分利用系统资源,能够以一个最大的资源利用率去运行程序。

通常我们提并发编程一般都包括并发和并行。并发是指在一个CPU内,多个程序可以看似并发地执行(实际上并不是);并行就是可以充分使用多核CPU的优势,同一时间可以保证有多个CPU在执行任务。

常见的并发编程方式包括:IO多路复用,多进程,多线程,协程,异步IO等。

IO多路复用:Netty,Redis,nginx都可以看到IO多路复用的影子,通过某种机制能可以一旦指定的IO文件描述符准备就绪了,内核会通知进程处理,从而提升并发处理能力。机制包括select,poll,epoll,目前实际使用的都是epoll机制,主要得益于其采用事件驱动的方式来进行处理,并且引入红黑树存储就绪事件,从而提高查询性能。

多进程: php典型的多进程,我们用的数据库pg也是多进程。python Web服务器gunicorn,pre-fork。通过一个master管理不同的子进程 worker。但我们都知道多进程的上下文切换是比较大的,所以相比于其他的并发编程来讲,其性能并不是最优的。因此经常情况下,多进程会和其他的并发编程方式结合使用。比如Nginx,实际上是多进程+IO多路复用的方式来实现的。

多线程:多线程就比较常见了,写JAVA的都是在玩多线程,Mysql,Memcached等。相比于多进程来讲,可能会比多进程的上下文切换付出的代价更小。Netty,是一个典型的主从Reactor多线程模型,boss group负责接收请求,封装channel,并转发给work group,work group中的selector会进行IO处理,这里实际上就是IO多路复用的机制,随后会将就绪的时间丢给worker线程池去做处理。netty是一个非常优秀的通信框架,几乎很多有名的框架都采用netty,Dubbo,Lettuce,RocketMQ等等。当然netty的高性能还有其他的技术结合,如零拷贝,对象池,CAS,无锁队列等。

协程:说完多线程,再看一下另外一个并发方式,协程。写Java的应该接触不到,因为已经冻结的JDK版本里还不支持,写过python或者golang的应该都接触过,尤其是golang,在golang中,goroutine go func()就可以开启了。相比于多线程,其切换的成本更低,所以你会看到很多高并发的场景都使用golang开发,小米的大秒系统,字节也是几乎都是用golang,腾讯等等。

异步AIO:其实还有异步编程,但目前基本上都是框架自己在应用层面实现的,

并发编程在提高效率的同时,也会引入其他的问题,其中最重要的就是数据的安全性。当多个任务同时处理一个共享变量时,由于是并发处理,所以会出现不可预知的结果。比如Nginx里,一个master,多个worker,当有多个请求过来时,如果没有锁的机制,很容易出现惊群效应。

为了解决上述的问题,一个最常见的解决方案就是加锁。比如Linux中有信号量、互斥锁、自旋锁、RCU,JAVA各种内置锁;数据库中的各种锁,表锁,行锁有记录锁、间隙锁,临建锁、插入意向锁等等。

引入了锁,那可能还会引入另外的问题,比如死锁和活锁。死锁比较常见了,解决死锁就是破坏死锁成立的四个条件;活锁也比较有意思,我比较喜欢的一个关于活锁的解释:虽然你看起来很努力,但结果却没有因为你的努力而发生任何改变。

另外一种是在集群或者分布式场景下的资源同步,分布式锁。常见的有基于ZK,基于数据库,基于Redis的。新核心那边是自研了一种基于分库分表场景下的分布式锁,还有一种是基于ETCD的,我们产品研发处今年就提出了一种基于ETCD实现的分布式锁的方法,感兴趣的老师可以私下联系我进行交流。

还有就是关于并发编程的,我们产品研发处也正在编写一个相关书籍,书名暂定是《多任务编程思想》,预计会在今年年底或者明年年初完成。

锁介绍

4、 零拷贝

I/O是成为系统瓶颈的主要因素,零拷贝可以减少CPU拷贝次数,减少用户态和内核态的切换,从而提升传输效率。像Nginx就有用到,如果配置过nginx的配置文件的,会经常看到有 sendfile on,sendfile主要用于文件传输的场景,可以避免数据经过用户缓冲区的拷贝。其他还有mmap,splice,绕过缓冲区的直接IO等零拷贝技术;还有Netty, RocketMQ也都使用了零拷贝技术。

池的使用,线程池,近程池等等;

5、异步

通过将请求处理异步化,提高接口响应,注意线程池的配置和隔离;

消峰,解耦,比如RocketMQ,Kafka都是比较优秀的消息队列中间件;

6、硬件

1)纵向扩展

单机配置:CPU,磁盘,内存、网卡、网络带宽

2)横向扩展

集群:单服务多实例,以集群的方式部署,通过负载均衡实现请求分摊。选择合适的负载均衡技术也是关键,硬件解决方案有F5,软件的有Nginx,HAProxy,LVS等等;

分布式:通过服务拆分,划分微服务,分而治之,服务边界清晰,相互独立,通过微服务治理以及API网关,为前端输出提供一套完整的分布式系统;当然要考虑分布式一致性,容错性等方面。ZK,Nacos,ES,RedisCluster。

分布式比较复杂,要考虑很多,常见的CAP,BASE,共识算法有Paxos,Raft,ZAB等。区块链里有工作量证明。

7、预处理

我们知道系统在启动时会通过懒汉加载的方式来延迟对象的创建,从而提高系统启动时间,然而这可能导致在运行期间因为对象的创建、数据的加载等问题导致性能下降。因此可以做一些预处理。系统的预热一般有JVM预热、缓存预热、文件预热、DB预热等,通过预热的方式为高并发流量的到来做好准备。

预热实际应用的场景有很多,比如营销活动就可以提前将活动数据加载到缓存中。常用的消息队列RocketMQ,它就实现了文件预热,在进行内存映射后,会预先写入数据到文件中,并且将文件内容加载到 page cache,当消息写入或者读取的时候,可以直接命中 page cache。

刚才在零拷贝中已经讲到了mmap,RocketMQ使用的是mmap函数将磁盘文件化作4kb的内存页映射到内存区域中,从而使得用户态程序和操作系统共同映射同一块虚拟内存地址。程序可以对内存页进行读写,背后由操作系统负责将缺失的内存页从磁盘读到内存。

所以一开始的mmap函数映射的磁盘文件在内存中是没有数据页的,如果突发的对它高频随机读取数据页,就会发生很多缺页中断很多用户态->内核态转换,以及磁盘IO。这个场景在MQ中很容易出现,当一个CommitLog写满之后,创建一个新的,然后大量的msg还等着写入到这个新commitLog文件中,必然发生大量缺页中断读取磁盘。会造成MQ的写入性能抖动,在客户端这里也会报大量的写入异常。

4、介绍高可用性

高可用即整个系统在高并发的情况下,系统仍然是在大部分时间都是可用的,无论系统内部发生任何故障,系统都尽量不崩掉。

降级,熔断,异步,消息队列,集群、分布式、数据冗余。

想要做到可用性,就有着多种做法:

1、集群和分布式,

集群的作用除了分摊承载压力,同样是一种冗余备份,当集群中一部分机器挂掉,会自动将其摘除,转到其他服务商。从而保证整个服务仍然可用。有些框架会自己实现高可用,有些框架会借助一些成熟的工具来实现,比如KeepAlived,ZK等等。

数据库主从模式下,当master节点挂掉,可以实现自动切换。另外分布式系统都具备高可用的特点,RedisCluster中某个master节点出现问题,会根据Raft协议选举出新的master节点。

2、消息队列,

异步,这两个应该是一起的,通过消息队列,将一些任务异步执行,可以削弱流量高峰,比如秒杀活动中,可以将下单请求放在消息队列中,削减整个系统的瞬时处理请求,保证系统的可用性;

3、降级,

大促期间通常会将系统中不重要的服务停掉,避免占用系统资源。如电商系统基本上在大促之前,将很服务降级,如评论、我的收藏,历史订单查询等。就比如幻灯片上的这个案例,它本应该在活动开始前,将卡券中心整合促销查询功能进行降级的,这些查询在活动开始中已经影响了整个系统的性能。

4、限流 ,

这在秒杀等高并发场景中是必不可少的。限流有两个目的。1、防止瞬时高并发流量同时打到我们系统上,超过了服务的负载能力;或者就算服务器本身可承受,但由于大量请求过来,可能导致服务器所在的网络运营商发现了流量异常超标,已经影响了整个出口带宽,运营商都会直接把你ip封掉。不知道大家听没听说过2009年的暴风影音事件,因为两家游戏公司恶意竞争,一个公司恶意攻击另外一家公司网站,导致DnsPod服务器受到Ddos攻击,再加上当时著名的暴风影音使用了免费的DnsPod服务器,也受到了影响,那时的暴风今非昔比,用户量巨大,导致大量域名解析请求达到了电信的DNS服务器,占用了典型机房的1/3的带宽,于是乎,被电信封掉了ip。这一封不要紧,直接导致使用DnsPod解析的网站全部无法访问,数量达到10万+,当时影响还挺大的。有感兴趣的可以看看历史回顾,很精彩。

2、撇去多余的无效流量。比如案例中实际上每期只发放2000张券来抢,但活动开始时瞬时流量可能10万以上,或者几十万以上的流量,大多数很多都是无用的,这些流量完全没有必要再去走我们系统的业务逻辑。

限流算法有计数器法,令牌桶,漏斗等等。

5、熔断

熔断是保证当后端业务有异常时,可以及时摘除,避免影响整个链路。比如RTT较长,就可摘除上游服务。sentinel是一个不错的选择,或者Hytrix框架等。

6、机房异地多活

这个是很重要的,我曾经在联通当过一段的网络工程师,遇到过很多奇葩事件导致机房内设备出现问题。比如光纤断了,设备端口down了,光模块坏了,机房断电等等,如果服务器都在同一个机房还是有风险的,最好是不同的机房或者异地。像小米就会有c3,c4集群,两个集群会对接不同的运营商的不同的机房。

对于任何一次秒杀活动或者大促之前,开始前都要进行 流量预估、全链路压测,灾备演练, 服务降级等等准备工作。

7、云原生

云原生主要是包括容器化,微服务架构和容器的编排。这里只说容器的编排,当前实现方式就是k8s,使用 k8s 进行容器编排、服务发现和负载均衡 。此外,可实现弹性的扩缩容,系统的自动恢复和拉起。

这里还是建议大家要尽快使用我们的Devops平台,.git代码提交后,自动扫描,编译,打包镜像,上传,并完成自动部署。

通过使用容器化技术和 k8s 弹性伸缩技术,该电商平台可以提高系统的稳定性、性能和可用性,从而提高性能和实现更好的用户体验。

5、结束语

高并发看着是一个高大上的东西,但是不能因为是高大上就动不动为高并发做各种设计。系统的设计最忌讳的是过度设计,如果最高并发量也只有几十,那就没有各种分布式,各种微服务,各种负载均衡了。淘宝的系统也是逐步的演进的,不是一蹴而就的。小米有品最开始的后端服务只有两个,数据库只有读写分离,后来因为业务不断发展,才开始做微服务,分库分表等系统升级。

好,那我今天的分享就到此结束了,谢谢。

高并发

--------EOF---------
微信分享/微信扫码阅读