简单项目一步一步进化到大量数据、大量并发架构优化方案

前言

  软件的架构都是从小到大的,除非是一开始就知道软件需要承受载的数据量和并发量,并且有成熟且有经验的团队以及资源来支撑,否则其他的项目都是先做基本架构,做出功能后期再根据实际情况做架构重构优化,相信这是大部分公司的处理办法。毕竟软件前期过度优化,不仅浪费大量时间和资源,还有可能适得其反,搞不好做了一个能承受亿级流量(现在大家都喜欢说亿级流量,好像搞得不管那个公司都能有亿级流量一样,但是我也要蹭蹭热闹)的程序,最后撑死也就服务个百万人的程序,但是仅仅是这套程序的基本运行都需要大量服务器来支撑,最后活活耗死公司,毕竟不是所有公司都有大量时间和资源来烧的。所以个人觉得架构不是越复杂越好,而是要符合产品定位,以及合理的后续优化计划。

当然合理的架构不代表就是简单的架构,而是要基于可以使用的时间,数据存储量,以及预估的并发量来设计架构,当然所有的架构设计也需要考虑后期的优化或者重构,毕竟如果完全没有考虑,导致后期根本无法扩展或者重构(无法重构是指重构难度已经太大,接近重做的成本或超过重做成本),到时候只能重做那么也不算是合理的架构

另外,本文只是表达我在工作总结和记录的一些解决方案的思路,没有实际处理方法或者对应的代码,同时后面的步骤可以根据实际情况来处理,不按照顺序或者跳过某些也是可以的,毕竟没有完美和通用的架构,只有更合适的架构

第一步:简单项目(单体项目+单机部署)

最简单也是开发最快捷的项目,所有东西都在同一个项目里面,一个基本的数据库,此时所有代码都在同一个项目里面(也有可能分了模块,但是最终打包成一个jar或者一个war包),部署也是单机部署,并发量不大,处理数据只要数据不算太多还是可以处理的,对于大部分公司初始项目,目前这种项目也是可以满足需求的

可能会产生的问题:

1. 首先单体项目常见的问题,随着代码越来越多,编译打包和启动会越来越慢,如果是单机部署,中途停机的时间也更长(当前可以在半夜没啥人用系统的时候更新,降低影响)

2. 故障会导致停机,因为是单机部署 ,一旦服务器出现什么故障或者项目本身出现灾难性错误导致服务停止,那么所有人都用不了了

3. 并发量上升会导致体验不好,正常来说,这种架构不会数据量过大的问题,因为先会被并发卡住,一个tomcat默认是150个线程,在传统servlet环境下一个请求会是一个线程,当然可以使用Web Flux来进行开发,这样虽然不能加快相应速度,但是会降低线程使用量,从一定程度降低线程数量

问题处理办法

针对问题一没有啥太好的办法,听说新版maven配置个什么东西会构建很快,但是没有试过

问题二:服务器安装第三方监控软件,可以监控http相应和进程状态,如果有问题可以执行重启服务。

问题三:可以调大线程(具体上限根据服务器系统和配置不同而不同),但是一旦线程数量超过CPU的承载数量,速度也是非常慢的,毕竟大家也清楚,线程不是一直运行的,在CPU里面也是切换运行的,线程数量太多,会导致CPU大量时间会浪费在线程上下文切换上面,所以线程也不是越多越好,当然如果你服务器有1000个CPU,那么运行1000个线程也是搓搓有余,所以具体线程数量还是要根据服务器实际配置来调。除了线程,还可以配置其他jvm参数,调整堆栈内存大小,通过降低GC频次增加并发数量,当然增加服务器配置肯定也是其中一个办法,还有优化Java代码加快运算速度,以及大部分程序慢的根本原因:慢sql,sql优化也不能专死角,比如一条sql语句,可能关联了一堆的表来查询,当你各种优化索引和语句,发现速度还是提升不上去的时候,也可以考虑通过多次执行来处理。

第二步:单体负载均衡(单体项目+多机部署)

当一台服务器不管是增加配置还是优化设置,并发都已经会成为隐患的时候,或者投入的资源跟回报已经完全不成比例的时候,就需要进行第二步了,因为单台服务器已经被压榨到了经济极限的时候,就没有必须要考虑其他方案了。而第二步就是负载均衡,负载均衡能大幅度提升并发,同时也能提高可用性,并且开发效率也不错,所以如果是单体项目需要马上提升并发,可以先实现负载均衡,满足当下要求的同时,再继续改造

可能会产生的问题:

1. 定时任务会重复执行

2. 对于程序内部使用普通Java锁的情况可能会导致异常产生

3. 如果主键不是使用的数据库自增,且生成算法无法做到几乎不重复的话,会导致主键重复异常

问题处理办法

问题一:可以通过配置控制是否启用定时任务,保证只有其中一个服务启用定时任务,或者为了长期发展,定时任务单独出来,目前可以只是简单得提取定时任务

问题二:对于业务需要用锁的情况,可以采用数据库乐观锁的方式,当然也可以乘此机会引入redis,使用redis实现分布式锁或者引入zookeeper实现分布式锁(可以根据实际情况选择,但是redis到后期几乎所有项目都会用到,但是zookeeper就不一定会用上了)

问题三:使用数据库自增在目前是没有问题的,但是如果你的项目以后会有大量数据,可以考虑使用常见的算法来生成id,如雪花算法

第三步:增加缓存机制

做了负载均衡,但是并发量大的时候,会有大量查询请求到达数据库,也会引起相应慢导致并发下降以及服务卡顿,这时候需要先降低数据库访问次数,所以需要用到缓存机制。对于经常查询但是不会经常变化的数据存入缓存(不会经常变化不是指不会变化,而是变化的频次跟查询的次数明显不成正比的情况,比如查询10次更新1次这种,具体要根据实际情况处理)。虽然增加缓存能有效降低数据库查询频率,但是缓存也会容易踩坑,以下是使用缓存常见的一些问题

可能会产生的问题

1. 缓存一致性,缓存一致性包括缓存与数据库数据一致性以及缓存集群以及多级缓存中出现数据不一致的问题

2. 缓存雪崩,简单来说就是缓存未命中或者缓存失效瞬间,或者大量缓存同一时间过期,导致大量请求进入到数据库,导致数据库崩溃从而影响服务。当然如果真的已经出现缓存雪崩的时候,那么那时候架构也到达一定程度了,当然也是需要有对应的处理方案,防止后期出现这种问题

3. 缓存击穿,当被大量的请求缓存不能命中,且数据库也无此数据时,会导致所有请求到数据库中,导致数据库崩溃从而影响服务,跟缓存雪崩不一样的是,缓存击穿情况出现后,除非请求量降下来,否则一直无法改善,因为缓存一直不会生效

问题处理办法

缓存的三个问题造成的原因有很多,不是1,2句话能说清楚的,并且需要根据代码实际情况来处理以及避免,所以相信网上很多文章写的比我详细和清楚,这里我就不写了,毕竟不是本文讨论的内容

第四步:MySQL表分区

当并发到达一定程度时,数据库查询虽然通过缓存缓解了很多,但是写入可能会到达一个瓶颈,毕竟每个硬盘都是有一个写入速度的,加上数据越来越大,可能会导致硬盘不足的情况,这时候就需要使用MySQL表分区来缓解了,MySQL表分区之后可以把不同分区的数据放进不同的路径进行保存,不仅可以防止硬盘不够用,还可以将数据放到多个硬盘,增加写入速度。当然服务器不可能无限加硬盘,毕竟写入不仅仅是硬盘的事情 ,分区的规则需要根据实际情况来处理(因为目前个人主要是用MySQL,所以对其他数据库不是很了解,所以以MySQL为代表来写)

可能会产生的问题

使用表分区本身不会影响代码或者其他(可能只是我目前没有发现),说是问题,其实也只能说是MySQL表分区的一些限制吧。

1. 分区数量限制(1024)

2. 分区规则相关对来较为固定

3. 服务CPU和总线压力不能分散

4. 分区规则会受表索引影响

第五步:分布式架构或者微服务架构

如果采用分布式架构,首先团队成员得跟上,因为以前一人天的工作量,在分布式或者微服务的架构上,开发可能最少都要1.5人天,其次分布式也会带来很多新的挑战比如分布式事物,服务治理等等。但是也有好处,比如功能可以单独更新,而不影响其他,甚至可以灰度发布以及可以根据服务实际情况来调整实例数量,比如我觉得前端接口访问量是后台的10倍,那么按照之前,我不管前端接口还是后台接口都要部署多个,因为他们是在一起的,这样不仅浪费资源,而且还会因为前端接口的请求量从而影响到后台接口。这种情况下,我们可以把前后端接口分离成为2个项目,部署的时候也可以后台部署1台服务器,前台的部署5台服务器,不仅不会相互影响,如果某台服务器配置更高一点,还可以提高此服务器的实例的权重,让其处理更多的请求,从而实现资源最大化利用

分布式还是微服务?

相信很多团队都会纠结这个问题,或者有些团队直接跟风,微服务火,那就上微服务就对了,为了方便大家做选择,我先说说我个人对于微服务和分布式的理解。首先分布式是为了分散压力,微服务是为了分散能力,微服务本身就是分布式的一种,不过微服务对于能力划分更为细致,常见的微服务正确来说是一个服务只提供一种能力,分布式对于异构支持相对来说较差(不同语言),而微服务对于异构支持能力较好。分布式主要是将服务分布部署,服务不能单独进行使用,而微服务则是将能力分布部署,可以独立使用。当然因为微服务的颗粒度更低,所以相对来说开发时长比分布式会更长一点,但是对于大型团队和大型项目来说,微服务后期的扩展可维护性会好很多

可能会产生的问题

1. 开发效率降低

2. 分布式事物问题

3. 分布式锁的问题

4. 唯一主键问题

5. 新的技术挑战和踩坑

6. 问题追踪更复杂,以前出现问题,直接查一个项目的日志就行了,并且日志有连续性,现在可能涉及到多个日志文件,且没有连续性

问题处理办法

问题一:规划设计,规范流程,快速跟进,当然如果你的团队实在不大,如果用了微服务或者分布式,可以按照实际情况来划分项目和服务,颗粒度粗一点,耦合度高一点,虽然都提倡低耦合,但是低耦合的代价是高工作量

问题二:尽可能的降低分布式生成的概率,比如同一个强事物流程尽可能写进一个服务中,当然如果无法避免,也有很多分布式事物处理方案,需要根据实际情况选择合适并且可以接受的方案,因为目前来说没有完美的分布式事务方案,常见的分布式服务框架有Seata

问题三和问题四之前已经说过

问题五可以加强团队内部培训和技术分享,以及采用成熟的技术方案来减轻,记得所有框架最好别用最新版的,毕竟人都是一样,容易写bug啊

问题六可以使用分布式日志系统以及搭配链路跟踪技术实现,方便排查问题

第六步:异步处理

截至目前,我们所有处理都是同步的,哪怕是分布式或者微服务中(当然里面也可以用异步调用),比如我们一个注册流程,除了需要在会员表里面写入一条数据之外,还需要赠送积分,然后赠送的积分又需要写入积分记录表,还需要统计当前时间片段注册人数以及查询配置的新用户送券,如果有配置还需要送券给用户,这就导致了我们的注册流程非常长,如果是微服务划分很细的话,可能涉及到一堆的服务,并且防止有问题,不同服务之间还需要使用分布式事物,并且服务之间相互调用还需要网络成本(内网也是需要的),所以会导致我们的注册时间很长。但是实际上我们注册本身只需要会员,其他都是附带的,那么我们就可以当写入会员成功之后,提交事物,返回成功,然后再异步调用各个服务。可以直接异步调用,也可以使用消息队列的方式实现,但是不论何种方式,一定需要容错机制,比如消息的持久化,异步的调用记录日志,防止其他服务出错时,导致数据最终有误,关键是当服务正常时,能自动恢复数据(因为你不可能回滚会员数据,只能按照规则送积分,送券等)。这样我们的注册流程本身会很快,尤其是当后期服务越来越复杂的时候,当然也肯定会产生一些新的问题

可能会产生的问题

1. 数据不一致,可能前端用户已经获取到了会员信息,但是积分信息还没有生成好

2. 数据丢失,如果异步调用或者消息队列没有做好持久化,并且服务出现某些不可控的异常的时候,可能会导致消息或事件丢失从而导致数据异常(丢失)

3. 增加了系统复杂性,从某种程度上来说,会增加系统的可用性,但是却会降低系统可靠性(或者叫准确性),因为注册会员主流程的动作变少了,所以就算积分或者优惠券服务不可用,但是不会影响数据,所以增加了系统可用性,但是一开始告诉别人注册送1000积分或者送100元优惠券,但是人家注册了却没有看到,所以又降低了可靠性,并且当系统的组件越来越多,复杂性也跟着长高了

4. 问题追踪更复杂

问题处理办法

问题一:加快处理速度,尽可能的再前端获取之前处理完成,同时前端也可以稍微延迟或者判断没有处理成功之前,做一些重新获取的操作,以及前期先隐藏部分数据或其他处理

问题二:加强持久化处理,比如异步调用则把持久化操作放进写入会员同一个事物中,消息队列则采用持久化队列且要确认送达,否则消息回退到队列中

问题三:尽量选择成熟稳定且大众化的处理方案,降低学习成本和踩坑

问题四参考上面

第七步:读写分离

虽然很多常用数据已经使用缓存来降低数据库的查询次数,但是不可避免还是会有很多查询进入数据库,以及很多查询可能只能进入数据库查询,长时间来说对于数据库压力也不小,所以采用读写分离,来将大部分查询转入从库,这样就算要做一些耗时的查询,也不用担心影响太大。读写分离可以采用中间件的方式或者代码式,中间件的方式可以在几乎不修改代码的方式下,实现读写分离,而代码式要么使用框架要么自己实现,某些框架也可以实现几乎不用改代码,但是不论如何,代码重新打包是肯定的。而中间件的方式相对来说性能不如代码式,且中间件的性能本身对系统影响颇大,所以具体采用哪种方案根据实际情况来处理

可能会产生的问题

1. 短时间数据不一致,当写入了之后马上查询,可能主库数据还未同步到从库上面,会导致查询到旧的数据或者根本没有查询到数据

问题处理办法

问题一:主从复制速度尽可能快,并且从库配置不要差主库太多,另外对于同一个请求中的写入后马上查询可以使用主库查询,当然对于某些数据,可以在写入后马上对缓存进行处理,但是需要注意缓存一致性,毕竟缓存和数据库操作本身不能依耐于事物处理

第八步:搜索引擎

当系统出现复杂搜索或者从大量数据中搜索以及全文检索的场景下,可以引入搜索引擎,当MySQL单表数量到千万级别时,就算有索引查询速度也会下降很多了,尤其是很多复杂场景,语句不一定都能命中索引(索引也不能太多,否则影响写入速度),而引入搜索引擎能近乎完美的处理大量数据查询的问题,同时很多分布式日志方案也是依耐于搜索引擎来实现的

可能会产生的问题

1. 链表查询支持较差,搜索引擎中没有表的概念,只有索引的概念,这里我们就把索引当成表,但是一般搜索引擎中都不支持2个索引关联查询

2. 数据延迟,常见的做法是在写入数据库之后,异步写入(异步线程或者消息队列)搜索引擎,或者通过读取数据库日志(主从复制的做法),将数据库最新的数据写入搜索引擎中,这样就必然导致会产生一定的延迟

3. 学习成本

问题处理方法

问题一:常见的一些处理方法有客户端链接、提前聚合写入到搜索引擎等,具体可以查询网上的方法,选择合适自己的方法实现

问题二:通常做法是,强逻辑业务还是查询数据库,非强逻辑业务查搜索引擎,比如我如果要根据查询出来的数据生成或者执行某些操作,那么对于数据准确性要求非常高的,就是强逻辑业务,那么就从数据库查询。而对于一些数据准确性要求不算太高的,就算数据暂时不准确也不会影响后续操作或者造成不良影响的,属于非强逻辑业务。比如我只是单纯查询当前系统的销售额,虽然数据库里面的更准确,但是只是简单得查看而已,那么从搜索引擎中统计出来就没有关系,因为就算现在不准确,等下也会更新成为正确的,但是如果是双11的活动,我们在直播,要给大家看我们会在什么时候到达上亿营业额,那么就只能查询数据库,因为就算搜索引擎里面延迟1秒,也会造成一些影响(当然不可能真正的查询数据库,除非是不想让别人下单了,这里只是打个比方)。

问题三跟其他一样,加强内部培训和分享或者聘请专业人士和外援

第九步:分库分表

其实大部分公司的架构师一开始设计的时候就做好了分库分表了,之所以放到这里说实话是因为我本身还是非常抵触分库分表的,如果其他技术能满足要求我绝对是不会优先考虑分库分表的(毕竟不是每个软件定位就是服务于千万用户),之所以分库分表我是不到非得不已不考虑的原因就是:分库分表水太深,我怕把控不住,分库分表带来的附带影响实在太多。当然如果你的数据量以及写入量很大,那么分库分表又是必须要做的一步,毕竟一台数据库写入数据终究是有限的。当然不管怎么说,就算一开始不做分库分表,但是正常来说,只要后期有做分库分表的可能性,一般都会在前期表结构设计上,预留用于分库分表的字段,或者结构中某些字段能满足后期分库分表需求。而分库分表的好处也是明显可见的,不管是写入还是查询压力,都能合理的分散到不同服务器中去,当然前提是你的设计是合理的。分库分表跟读写分离一样,常见的方案有中间件式以及框架式,比如MyCat和sharding-JDBC。而因为分库分表会产生太多问题,以下只是简单罗列一下常见的问题

可能会产生的问题

1. 事物一致性

2. 唯一主键重复

3. 跨节点关联查询

4. 跨节点分页,排序,函数等问题

5. 迁移扩容

6. 框架或者技术问题,就我目前采用过mycat和sharding-JDBC都或多或有些问题或者说局限性

问题处理方法

以上大部分问题都不是一两句话能讲清楚的,建议在做分库分表之前,一定要提前了解能力和局限,以及常见问题的应对方案,另外就算暂时不做分库分表,但是后期可能要处理的,也最好从一开始就预留好相关字段,方便后期处理

第十步:集群

集群不是指服务集群,而是整个系统的集群方案,其中包括:服务集群、中间件集群、数据库集群、文件服务集群等。如常见的:zookeeper集群、nacos集群、redis集群、MySQL集群等。

集群可以能增强系统的处理能力、分散压力、提高可用性,当然集群也会带来部署成本、运维成本、系统复杂性的增加,而选择合适的集群模式也有助于降低成本

其他细节

面对大量并发的时候,除了查询数据要快(多级缓存+引擎+数据库分库分表以及优化),也要防止极端情况,比如大量并发的时候,就算你处理的再快,服务器也不能处理所有请求的时候,就必须要防止服务雪崩了,因为一旦其中一个环节或者服务被拖慢,可能导致整个系统被拖慢,但此时又有大量请求在涌入,从而导致所有人的请求都不能被处理,这时候就也就必须要做对应的处理来防止这种情况。常见的一些处理如下:

限流熔断降级

简单来说首先预估服务的最大处理能力,可以通过压测来测试服务器的极限,然后根据极限设置合理的流量限制,但是注意实际情况可能比压测时更复杂,所以最好比测试的结果更低。限制流量后可以选择直接失败或者排队等处理,防止服务器承受超过服务本身能处理的请求量。同时也对服务器产生的异常进行监控,如果发现请求中出现的异常超过一定比例,或者相应速度明显变慢或慢响应达到一定比例,那么防止更多异常和卡顿,对服务进行熔断降级处理,可以直接一段时间对此服务器停止访问或者降低权重,使进入的流量降低,等恢复正常之后再恢复,防止服务雪崩。而sentinel可以实现上面的这些需求,但是真心吐槽一下,感觉阿里虽然开源了这个框架,并且这个框架虽然的确能力不错,但是怀疑它主要的目的是推广阿里的xxx服务,因为不管是官方文档还是demo还是其他版本兼容性,只能说要多简陋有多简陋。虽然限流熔断之后会导致部分用户无法正常使用,但是好过所有人都不能正常使用

请求排队处理

对于大量请求,如果处理不过来,但是又不想告诉客户失败,也可以使用消息队列来排队处理,比如之前常见的抢购解决方案,抢购者抢购信息先存入消息队列,就直接返回,然后等消息队列处理之后来查询结果,这样至少给客户的感觉是服务没有奔溃,而且服务本身的确没有奔溃

加强版负载均衡

之前虽然讲过部署多台服务器来实现负载均衡,但是在请求量不大的情况下,大部分功能的负载均衡可能都是nignx,或者好一点的是F5,但是即使如此,入口本身也有可能超过承载量,所以对于入口本身(nginx,F5)也需要使用到负载均衡,可以使用DNS负载均衡以及阿里的负载均衡服务来实现自建负载均衡的入口,避免入口服务器崩溃导致所有服务不能使用

异地多活

异地多活可以提高访问速度(客户从最近节点访问),也可以提高服务可用性,就算某地机房出现故障或者某机房内服务挂掉,其他机房也不受影响,异地多活中,每个区域的服务都是独立的系统,不依赖于任意其他区域的东西,但是数据又要相互同步以及防止相互冲突

缓存预热

当使用了缓存,且缓存较大时,需要有对应的缓存预热方案,毕竟大部分缓存是基于内存的,一旦缓存重启或者发生其他问题,会导致大量缓存失效,从而大量请求落入数据库中,导致数据库奔溃。所以当缓存数据量较大时,一定先预热缓存,也就是当缓存中有一定数据量的时候再提供服务,并且缓存无法使用的时候,需要对数据库请求限流,防止太多请求落入数据库中,这里的数据库限流不是指控制数据库连接数量等,而是超过限制的流量应该直接失败,防止大量请求堆积造成服务雪崩事件。当然如果本身使用了多级缓存,并且缓存也做好了集群,一般来说没有这种极端情况产生,但是最好也要有对应的方案处理

Authorship: 作者
Article Link: https://raye.wang/2022/01/11/%E7%AE%80%E5%8D%95%E9%A1%B9%E7%9B%AE%E4%B8%80%E6%AD%A5%E4%B8%80%E6%AD%A5%E8%BF%9B%E5%8C%96%E5%88%B0%E5%A4%A7%E9%87%8F%E6%95%B0%E6%8D%AE%E3%80%81%E5%A4%A7%E9%87%8F%E5%B9%B6%E5%8F%91%E6%9E%B6%E6%9E%84%E4%BC%98%E5%8C%96%E6%96%B9%E6%A1%88/
Copyright: All posts on this blog are licensed under the CC BY-NC-SA 4.0 license unless otherwise stated. Please cite Raye Blog !