前言
在过去的两年里,我所在的公司积累了丰富的微服务经验,尤其是在高并发场景下。身边有不少朋友也从事微服务开发,但在高并发读写方面的经验相对较少,毕竟这样的经验并不容易获得。在我们所在行业,微服务的应用也相当普遍。我记得刚入职的时候,我们公司和旁边一家使用Java开发微服务的公司进行了比较。我们整个园区业务基本一致的。我们经常和他们公司的技术人员交流问题和解决方案。有趣的是,我们得知在相同的服务器配置下,我们的系统可以处理多一倍的客户和流量。当然,他们的代码和内存中是否还有提升的空间,我们无法得知。
一年后,我们公司收购了隔壁公司,他们的客户都成功接入到我们的系统中。令人欣慰的是,我们并没有增加太多的配置就能够顺利地接入他们的客户。至今,我们的门店客户数量已经达到了5000+,会员数超过了3000万。由于我们所处的行业需要与机器和人打交道,因此读写并发都非常高,高峰期的QPS接近10000,TPS达到5000。
在高并发的挑战中,我们曾经遇到过各种困难,现在愿意分享我的经验给大家。
千锤百炼:
以上是该产品的技术栈,在产品的技术栈方面,许多卓越项目都是通过渐进的方式发展起来的。通常情况下,人们难以确定产品是否适合市场,因此大多数公司都采用快速迭代的方法来推进产品。一旦产品在市场上取得一定份额,就可以逐步进行性能优化。初始阶段,并没有引入太多的技术栈,而是在每个阶段逐渐叠加起来。许多人可能会希望初始设计的架构就是完美的,但却缺乏对项目生命周期的理解。这导致了项目开发时间的延迟,有时甚至在项目完全开发完成或刚刚上线时,由于各种原因而推翻或放弃整个项目。
在项目初期,我们可能会因为技术选型错误而导致某些业务或性能受到影响。这时,我们需要自己整合组件,分析资源,并在选择技术栈时考虑到代码结构,以确保留有可维护性的空间。要善于在错误发生时学习和调整,同时在项目演进的过程中持续优化。
技术选型原则
1.项目需求优先: 技术选型应该始终以项目的具体需求为基础。了解项目的功能、性能、安全性、可扩展性和其他关键要求是决策的首要步骤。
2.团队熟悉度: 选择团队熟悉的技术栈,可以减少学习曲线和开发时间。熟悉的技术栈也有助于降低项目风险,因为团队更容易解决可能出现的问题。
3.社区支持和生态系统: 选择得到广泛社区支持、有活跃的开发社区并且有健全的生态系统的技术。这有助于在遇到问题时获得支持,同时提供了更多的工具和库,有助于加速开发过程。
3.开源和许可证: 考虑选择开源技术,因为它们通常具有更大的社区支持和更好的可扩展性。同时,仔细了解技术的许可证,确保它符合项目的法律和商业需求。
4.性能和扩展性: 根据项目的性能需求选择合适的技术。同时,考虑技术栈的扩展性,以确保在未来项目的发展中能够满足需求。
5.成本和资源: 考虑技术的成本,包括开发、维护和培训成本。同时,确保有足够的人力和硬件资源支持所选技术栈。
6.可维护性和易用性: 选择易于维护和使用的技术,以降低代码维护的难度,提高开发效率。
微服务
微服务是一种软件架构模式,其核心理念是将大型应用程序拆分为小而独立的服务。这些小服务可以独立开发、部署和维护,从而提高灵活性和可维护性。微服务的主要作用在于分解大型项目,分散压力,同时实现解耦,使得系统更容易扩展和维护。
简而言之,微服务是将复杂的系统拆解成小而自治的服务单元,各自独立运行。这样的架构有助于提高系统的弹性和可伸缩性。虽然微服务的成本相对较高,但其带来的灵活性和高可用性往往是值得考虑的。
在我身边的朋友的公司,尽管他们可能没有大规模的流量,但采用微服务仍然有助于解耦大项目,提高代码的可维护性。对于是否使用微服务,确实需要综合考虑成本、团队技术水平以及项目规模等因素,以确保最终选择符合实际需求和资源情况。
如何微
在业务领域中,拆分业务通常有两种主要方法。一种是传统的按功能划分,采用 MVC 架构;另一种是基于领域驱动设计(DDD)思想的拆分。无论选择哪种方式,最终目标都是解决业务上的问题。因此,我们不应该过于纠结于采用哪种方式来拆分,而是应该将精力放在如何拆分以满足业务需求,如高可用性、可扩展性和低成本等方面。
无论采用哪种拆分方式,都需要考虑以下因素:成本、团队技术水平、业务情况和运维等。以电商平台为例,涉及到商品、会员、订单和支付等功能。我们可以首先将业务拆分为商品服务、会员服务、订单服务和支付服务。在初期,可以使用单实例数据库。如果需要新增秒杀功能,我们可以新建一个秒杀功能的服务,并将商品信息冗余到秒杀服务中。甚至可以将秒杀订单与普通订单分开管理。这样做的好处在于隔离,即使秒杀服务的流量再大,也不会影响主流程业务,从而避免整个系统崩溃。
而DDD则更加要求关注业务,在拆分上则要更加对业务的通透及领域相关的知识面,所以团队在拆分上会更容易遇到困难,需要综合考虑团队情况。这篇文章也不会过多的介绍DDD,如需了解DDD则可以去购买相关书籍。
因此,拆分业务应该更多地围绕业务需求展开,而不是仅考虑技术框架。业务是一个变化多端的东西,业务前期需要快速迭代。因此,我们的技术选择应该根据业务每个阶段的需求来调整。没有任何项目可以一蹴而就,要在团队中脱颖而出,需要对业务有深刻的理解,并具备解决业务问题的思路。下面是我们的架构图
遇到的问题
1.数据库
其实在大部分系统中,数据库是最大的瓶颈,毕竟在我们所有操作都是围绕着数据库,在数据库达到一定的数据量后,只要还在运转业务都会有读或写出现卡顿或锁情况,在读写并发高的情况下更为明显。
数据库索引与实物问题
对于数据库的监控,我们需要关注慢查询和锁情况。使用阿里云数据库产品会自带监控工具,如DAS控制台。对于自建的 MySQL 数据库,我们可以使用自带的慢查询日志或第三方工具如 MySQL Performance Schema 来监控数据库语句情况。我们整个团队都是5年经验以上的工程师,但由于项目赶上线,也会出现全表查询或使用or,<>等语句导致不命中索引和大事务的问题,那这时候监控工具作用就来了,可以快速的帮我们定位问题语句,我们只需要优化语句,尽量缩小事务,加索引即可。由于索引是基础知识此处就不展开讲解了。在大事务中我们还引入了分布式锁来减少读锁的事务,分布式锁需要注意锁的超时机制,锁的释放,锁的粒度等,当然我们也可以使用乐观锁来解决事务锁问题。
读多写少的场景问题
在读多写少的场景下,当项目的数据量和并发量达到一定水平时,我们需要考虑如何减轻主库的压力,通常可以采取以下措施:读写分离操作:通过读写分离可以将读请求和写请求分流到不同的数据库实例上。在阿里云云数据库中,可以在集群地址的编辑对话框中设置读写模式为“可读可写”。如果是自建的 MySQL 数据库,则需要进行相应的配置和集群搭建。通过读写分离,可以降低主库的读压力,提升系统的读取性能。使用缓存:可以利用缓存技术如 Redis 或 Elasticsearch 来缓解各种业务场景的压力。缓存技术可以有效降低数据库的读取压力,提高系统的响应速度。需要定制好缓存的维护方案,以确保缓存的有效性和一致性。需要注意读取延迟问题:由于从库存在读取延迟的情况,业务上需要进行相应的调整。可以根据业务需求,设置延迟多少时间来读取数据,或者在一些业务场景下强制主库读写。总之,在读多写少的场景下,通过读写分离、缓存、解决读取延迟以及主从复制等措施,可以有效地减轻主库的压力,提升系统的性能和可用性。当然主从方案还可以用来解决高可用的问题,例如双主多从,主备方案等。
缓解读写压力问题
在读写场景多的情况下,我们还可以使用表分区来缓解读写压力,在polardb mysql开启partition_level_mdl_enabled后,即可使用命令创建表分区,我们目前是按照门店来创建的;但值得注意的是表分区需要选择合适的分区键,避免过多的分区数量,避免频繁的分区操作,定期维护分区,利用分区查询优化性能等问题。
数据归档问题
针对某些业务场景使用数据归档来解决数据库瓶颈问题的优化方案,可以考虑以下几点。业务场景的选择: 确定适合数据归档的业务场景,如订单数据、仓库操作数据、会员操作记录等。对于不适合归档的业务,需要根据具体情况进行评估,并考虑其他性能优化手段。数据归档策略: 制定合适的数据归档策略,包括归档周期、归档条件等。根据业务需求和数据增长情况,确定归档的时间范围和频率,以确保数据库性能的稳定和可靠。查询与导出操作的调整: 针对数据归档后的查询和导出操作,需要对相应的业务逻辑进行调整和优化。可以考虑引入新的查询接口或导出方式,以支持跨归档数据的查询和导出需求。归档数据的管理和存储: 确保归档数据的有效管理和存储,包括数据备份、恢复、压缩等操作。同时,需要考虑归档数据的访问权限和保密性,以确保数据安全性。综上所述,通过合理选择业务场景、制定有效的归档策略、调整查询与导出操作、管理归档数据和持续改进优化,可以很好地利用数据归档来解决数据库瓶颈问题,并提高系统性能和稳定性。
分库分表问题
我们的业务初期都是一个实例下包含了订单,商品等库,随着业务发展,则拆分成独立实例下的数据库,但在业务高速增长的情况下还不能好好解决数据量的问题。那这时候我们则需要分库分表了,架构设计初期我们为了减少联表查询很多都数据都是冗余的,所以我们在拆分时并不考虑垂直拆分,我们需要的是水平拆分,那我们如何水平拆分把风险降到最低呢?很简单,用我们客户表角度来思考,因为客户表不会很大,并且可以给客户划分很多属性,例如vip类,普通客户类等来分库分表。例如:在会员服务中,由于表结构都是通过客户id来区分数据的,那么我们这时候就可以根据客户id的属性来水平分库分表,这样在请求头传入客户id,就可以根据客户id来找到对应的库来进行数据的读写。同时没有破坏表结构和增加风险,解决问题的方案有千万种我们可以结合自己的业务多多思考。
2.grpc问题
当业务达到一定的量级,grpc会出现所有请求都会打到一个节点上,那我们则需要做grpc的负载均衡,做grpc的关注点则是dns://解析,该解析起到负载均衡的关键,因为gRPC 客户端通过这个主机名向 DNS 服务器发送请求,DNS 服务器会返回一个或多个服务实例的 IP 地址列表。客户端随后可以从返回的 IP 地址列表中选择一个地址来进行连接,通常使用轮询或随机选择等方式来进行负载均衡。详细可以了解官网https://learn.microsoft.com/zh-hk/aspnet/core/grpc/loadbalancing?view=aspnetcore-6.0
3.消息队列问题
消费问题
在高并发环境中,除了常见的消费积压问题外,我们还经常面临数据一致性问题。针对这些挑战,我们通常采取以下策略来解决。增加消费者Pod数量: 通过增加消费者Pod的数量,可以提高消息处理的并发能力,从而减少消息积压。优化消费者逻辑: 对消费者的逻辑进行优化,包括减少不必要的处理步骤、优化算法等,以提高消息处理的效率。增加Kafka分区或RabbitMQ分片: 通过增加消息队列的分区或分片数量,可以增加消息的并行处理能力,从而减少消息积压。
除了针对消息积压问题的解决方案,还需要解决数据一致性问题。对于使用旧版的DotNetCore.CAP插件的情况,虽然该插件本身可以依赖本地表来处理数据一致性问题,但在多个消费者Pod同时处理同一条消息的情况下,仍然可能出现数据一致性问题。针对这种情况,我们可以引入分布式锁机制。通过在消息处理逻辑中引入分布式锁,可以保证同一条消息只会被一个消费者Pod处理,从而确保数据一致性。这样即使有多个消费者Pod同时处理同一条消息,也能够通过分布式锁来保证数据一致性,避免出现问题。
引入消息队列多实例
提升消费能力最好的方式是多实例mq,目前DotNetCore.CAP还不支持多实例发送,那么我们是如何实现多实例的呢;首先我们需要每个服务绑定其中一个mq实例,接着写一个工厂模式的生产者,生产者需要读取每个topic绑定了多少个实例,如果绑定了3个实例则循环3个代理(每个代理对应一个mq实例)发送,如果该topic只绑定1个则就发送到他对应的代理上,我们需要对代理服务进行一个grpc调用,通过每个代理服务来发送cap任务出去。而我们的每个服务只能绑定一个mq实例,所以我们需要在配置中心先配置好哪个服务用哪个实例。这样消费者就可以消费到对应topic,这样就可以做到代理服务A用rabbitmq实例,代理服务B用kafaka实例,而且还能减少连接数等好处。
总结
今天的分享到此为止。有人可能会想,为什么整篇文章都没有提到高可用性呢?其实,我们在设计方案时也考虑了很多高可用性的策略,从网关的限流熔断到业务层面的降级服务,再到数据层面的双主集群等等。虽然这些方案都很重要,但由于篇幅有限,这次就不一一介绍了。
在接触微服务之前,我也研究了很多相关资料。但直到真正开始使用微服务,才意识到其中的困难重重。每当业务增加或变更时,就会面临新的问题。只有不断学习、思考,才能应对种种挑战。我还记得《铁齿铜牙纪晓岚》中的一段话,和珅说过:“易子而食,你当然听说过,那是史书上的四个字而已。我是亲眼见过的呀!这换孩子吃啊,就是锅里的一堆肉啊!”这句话让我深有感触。这一段话前后的场景是讲和珅如何使用有限的救济款来救济灾民的对话。和珅在救灾时灵活运用自己的经验所得及思考来处理问题,这正是我们在实际工作中需要做的。
很多时候,我们只是死记硬背书本知识,而忽略了实际场景的情况,导致问题无法解决或变得更加严重。这并不是说读书无用,而是要懂得如何读书,读完后如何实践,如何思考问题,如何解决问题。
回到企业用工的问题上,很多企业喜欢招聘年轻的程序员。虽然年轻程序员也有优秀的个体,但总体来说,大部分年轻程序员他们是一步步成长起来的。在解决问题时,经验丰富的程序员更具优势,因为经验丰富的程序员更懂得把控风险,思维成熟等。很多企业过于注重成本,却忽略了创造价值和降低风险,这是值得深思的。
最后,我想告诉大家,我身边有很多优秀的程序员,他们每天都在学习。他们不仅学习技术书籍,还涉及到项目管理、产品设计、心理学等各个领域的知识。活到老,学到老,认清自己的方向,不断努力前行吧!
标签:服务,架构,数据库,业务,并发,实例,归档,net6,我们 From: https://www.cnblogs.com/weirun/p/17957546