系统稳定性之技术方案
📕我是廖志伟,一名Java开发工程师、《Java项目实战——深入理解大型互联网企业通用技术》(基础篇)、(进阶篇)、(架构篇)清华大学出版社签约作家、Java领域优质创作者、CSDN博客专家、阿里云专家博主、51CTO专家博主、产品软文专业写手、技术文章评审老师、技术类问卷调查设计师、幕后大佬社区创始人、开源项目贡献者。
📘拥有多年一线研发和团队管理经验,研究过主流框架的底层源码(Spring、SpringBoot、SpringMVC、SpringCloud、Mybatis、Dubbo、Zookeeper),消息中间件底层架构原理(RabbitMQ、RocketMQ、Kafka)、Redis缓存、MySQL关系型数据库、 ElasticSearch全文搜索、MongoDB非关系型数据库、Apache ShardingSphere分库分表读写分离、设计模式、领域驱动DDD、Kubernetes容器编排等。不定期分享高并发、高可用、高性能、微服务、分布式、海量数据、性能调优、云原生、项目管理、产品思维、技术选型、架构设计、求职面试、副业思维、个人成长等内容。
🌾阅读前,快速浏览目录和章节概览可帮助了解文章结构、内容和作者的重点。了解自己希望从中获得什么样的知识或经验是非常重要的。建议在阅读时做笔记、思考问题、自我提问,以加深理解和吸收知识。阅读结束后,反思和总结所学内容,并尝试应用到现实中,有助于深化理解和应用知识。与朋友或同事分享所读内容,讨论细节并获得反馈,也有助于加深对知识的理解和吸收。💡在这个美好的时刻,笔者不再啰嗦废话,现在毫不拖延地进入文章所要讨论的主题。接下来,我将为大家呈现正文内容。
文章目录
- 限流
- 熔断与降级
- 超时机制的设置与优化
- 重试机制在分布式系统中的应用与挑战
- 兼容性
- 系统层面隔离
- 环境的隔离
- 核心与非核心隔离
- 读写分离
- 线程池隔离
- 代码Review
限流
限流是服务提供者为了保护自身系统不被过量的请求打垮而采取的一种技术手段。当系统的流量负载超过处理能力时,限流策略可以有效地防止系统崩溃。京东内部无论是同步交互的JSF,还是异步交互的JMQ,都具备限流的能力,用户可以根据自身系统的实际情况进行设置。常见的限流算法包括计数器算法、滑动时间窗口算法、漏桶算法和令牌桶算法。以下是这些算法的优缺点对比:
流量计数器算法
优点:简单好理解。
缺点:单位时间很难把控,不平稳。
滑动时间窗口算法
优点:时间好把控。
缺点:
超过窗口时间的流量会被丢弃或降级。
没有削峰填谷的能力。
漏桶算法
优点:削峰填谷。
缺点:
漏桶大小的控制较难,太大容易给服务端造成压力,太小则会导致大量请求被丢弃。
漏桶给下游发送请求的速率固定,不够灵活。
令牌桶算法
优点:
削峰填谷。
动态控制令牌桶的大小,从而控制向下游发送请求的速率。
缺点:实现相对复杂,只能预先设计不适配突发请求。
技术方案建议
针对上述限流算法的特点,建议采用组合策略,以充分利用各种算法的优点,弥补其不足。具体方案如下:
混合使用漏桶算法和令牌桶算法
漏桶算法用于初步限流,通过固定速率的漏桶控制进入系统的流量,避免瞬间高流量冲击。
令牌桶算法用于精细控制,动态调节令牌桶大小,根据系统负载情况灵活调整对下游的请求速率。
滑动时间窗口算法辅助
使用滑动时间窗口算法辅助监控流量,通过时间窗口的设定,对超过窗口时间的请求进行合理处理,如降级或丢弃,保证系统近期高流量得到有效管理。
智能调节与自适应机制
结合AI智能调节机制,根据历史流量数据和实时负载情况,动态调整限流策略,使系统在各种流量场景下都能高效稳定运行。
通过上述技术方案,可以较为全面地应对不同类型的流量冲击,保证系统稳定,提升用户体验。
熔断与降级
熔断与降级:保障系统稳定性的双保险
在系统设计中,熔断和降级是两种重要的保护措施,用于防止系统因外部依赖问题而崩溃。虽然它们通常结合使用,但各有不同的应用场景和技术方案。
熔断(Circuit Breaking)
熔断的目的是防止系统被下游系统拖垮。例如,如果某个下游系统的接口性能严重下降甚至挂掉,会导致大量线程堆积,占用CPU和内存资源,进而影响整个系统的性能。严重情况下,甚至会导致系统崩溃,引发雪崩效应。通过熔断器,可以在检测到问题系统时,阻止流量继续请求该有问题的系统,从而保护我们的系统不被拖垮。
技术方案:
熔断器模式:常见的熔断器实现有Hystrix(已退役,推荐使用Resilience4j)和Sentinel。通过这些工具,可以配置熔断的条件,如失败次数、响应时间等。
配置中心:公司内部一般使用配置中心(如Ducc)来控制熔断开关。修改熔断状态需要在线操作,并且需要做好监控,以防止误操作。
降级(Degradation)
降级是一种有损操作,目的是在部分功能出现问题时,通过返回友好的提示或可接受的降级数据,保证系统整体的可用性。
技术方案:
人工降级:通过降级开关来控制,使用配置中心(如Ducc)进行开关管理。修改开关状态是线上操作,因此需要做好监控,确保操作的安全性。
自动降级:通过中间件实现自动降级,如Hystrix、Sentinel等。自动降级需要明确降级的条件,如失败的调用次数、响应时间等。
超时机制的设置与优化
在分布式系统中,由于网络不可靠,服务之间的交互常常面临通信失败的风险。为了应对这种情况,合理设置超时机制至关重要。超时设置不当可能会导致系统性能下降,甚至出现系统崩溃。
- 超时的作用
超时设置的主要目的是在系统无法及时得到响应时,快速失败并自我保护,避免无限期等待下游服务,从而耗尽系统资源。例如,当某个服务请求在指定时间内未得到响应,系统应能够及时识别该情况并采取相应的补救措施,如重试、降级或返回错误。
- 如何设置合理的超时时间
设置合理的超时时间需要逐步迭代,通常基于压力测试的响应时间来配置。新接口在上线时,会基于压力测试中的99百分位响应时间(TP99)来设定超时时间;而已稳定的接口,则会监控其线上表现的99百分位响应时间来进行配置。
- 漏斗原则
在设置超时时间时,需遵循漏斗原则,即从上游系统到下游系统的超时时间逐渐减少。这一原则的目的是确保各服务之间的调用能够有序进行,避免因为下游服务处理时间过长而影响上游服务的可用性和性能。
例如,如果服务A调用服务B的超时时间设置为500毫秒,而服务B调用服务C的超时时间设置为800毫秒,这种情况下,如果服务B调用服务C的处理时间超过500毫秒,服务A将因超时而失败,尽管服务B从自身角度看是“可用”的。因此,遵循漏斗原则,确保每个服务调用的超时时间设置合理,是保障系统整体稳定性和性能的关键。
通过合理设置和逐步优化超时机制,可以有效提升分布式系统的稳定性和用户体验。
重试机制在分布式系统中的应用与挑战
在分布式系统中,性能的主要影响因素是通信。无论是系统内部还是跨团队的沟通,信息交流往往占据研发过程的大部分时间。而在分布式系统中,查看调用链路会发现,系统本身的计算耗时其实很少,大部分时间是花在外部系统的网络交互上,比如与下游的业务系统、中间件(如MySQL、Redis、Elasticsearch等)的交互。
在与外部系统的一次请求交互中,系统希望能尽最大努力得到想要的结果。但由于网络不可靠,和下游系统交互时,通常会配置超时重试次数,希望在可接受的SLA范围内一次请求拿到结果。然而,重试并不是无限的,一般都有次数限制。偶尔的重试可以提高系统的可用性,但如果下游服务已经故障,重试反而会增加下游系统的负载,从而加剧故障。
在一次请求调用中,如果调用链路较长,服务之间的RPC交互都设置了重试次数,这时需要警惕重试风暴。例如,如果服务D出现问题,重试风暴会加重服务D的故障严重程度。
技术方案:
重试次数配置与限制:
为每个外部系统调用设置合理的重试次数和间隔。
对于关键服务,可以设置较高的重试次数,但要避免无限重试。
区分读写操作的重试策略:
读操作一般影响较小,可以简单重试。
写操作需要确保幂等性,即多次执行和一次执行的效果相同,避免数据不一致。
熔断与降级:
使用熔断器(如Hystrix)在下游服务故障时,快速切断请求,防止故障扩散。
降级策略确保在主服务不可用的情况下,提供有限但可用的替代方案。
监控与报警:
实时监控外部系统调用的状态,设置合理的报警阈值。
在重试次数激增时,及时报警并启动应急响应。
调用链追踪与优化:
使用Zipkin、Jaeger等分布式追踪系统,分析调用链,找出性能瓶颈。
定期优化网络交互和调用参数,减少不必要的交互和传输数据量。
兼容性
在对老系统或功能进行重构和迭代时,确保兼容性至关重要,否则上线后可能会引发重大线上问题,甚至导致公司内外损失。兼容性主要分为向前兼容性和向后兼容性,两者需要明确区分:
向前兼容性:指旧版本的软件或硬件能够兼容新版本的功能,即旧版本系统或软件能够处理新版本的数据和流量。
向后兼容性:指新版本的软件或硬件能够兼容旧版本的系统或组件,即新版本系统或软件能够处理旧版本的数据和流量。
根据系统的新老版本和数据变化,可以将系统兼容性划分为四个象限:
第一象限:新系统和新数据,这是系统改造后的最终状态。
第二象限:新系统但使用老数据,问题常出现在未做好向前兼容性,例如上线过程中产生的新数据,回滚后老系统无法处理。
第三象限:老系统和老数据,这是系统改造前的状态,一般问题在研发和测试阶段能解决,较少引发线上故障。
第四象限:老系统但使用新数据,问题常出现在未做好向后兼容性,上线后新系统影响老流程。
技术方案
针对第二象限问题(新系统老数据):
构造数据验证:通过构造新的数据来验证老系统,确保老系统能处理新数据,避免上线后因数据不兼容导致的问题。
针对第四象限问题(老系统新数据):
流量录制与回放:录制线上老流量,对新功能进行验证,确保老系统能兼容新数据,减少因数据变化导致的不兼容问题。
通过这些技术方案,可以有效减少因系统重构和迭代引发的线上故障,确保系统平稳过渡。
系统层面隔离
一个系统可以根据其响应时间和数据处理方式分为三种类型:在线系统、离线系统(批处理系统)和近实时系统(流处理系统)。每种系统有不同的需求和特点,需要不同的技术方案和隔离策略。
在线系统
在线系统是指那些实时响应客户端请求的系统。例如,手机应用的很多功能都属于在线系统。在线系统的主要衡量指标是响应时间。在线系统需要快速处理请求并返回结果,因此其性能优化通常侧重于减少延迟和提高并发处理能力。
离线系统
离线系统(批处理系统)处理大量的历史数据,通常用于生成报表或进行数据分析。例如,计算日订单量、月活跃用户数等。离线系统的主要衡量指标是吞吐量,即单位时间内处理的数据量。批处理作业可能需要定时运行,通常从几分钟到几天不等。因此,用户通常不会实时等待作业完成。
近实时系统
近实时系统(流处理系统)介于在线系统和离线系统之间,用于处理实时数据流。流处理系统通常会有触发源,如用户行为、数据库操作、传感器数据等。这些触发源作为消息通过消息代理中间件(如JMQ、Kafka)传递,消费者消费消息后再进行其他操作,如构建缓存、索引、通知用户等。
技术方案
为了确保这三种系统能够高效、稳定运行,我们需要进行隔离建设,具体措施如下:
服务单独部署:
在线系统:将在线系统作为一个独立的服务进行部署,如 jdl-uep-main。在线系统需要高性能和高并发能力,因此其部署环境应优化内存、CPU和响应时间。
离线系统和近实时系统:将离线系统和近实时系统作为另一个独立的服务进行部署,如 jdl-uep-worker。这些系统对实时性要求不高,但可能需要处理大量数据,因此其部署环境应优化存储和计算资源。
资源隔离:
使用容器化技术(如Docker)将不同系统隔离开来,确保在线系统不会因为离线系统或流处理系统的高负载而受到性能影响。
使用Kubernetes等容器编排工具进行资源管理和调度,确保各系统能够按需分配资源。
数据隔离:
确保在线系统、离线系统和流处理系统的数据存储分开,避免数据交叉影响。
使用不同的数据库实例或分区来存储不同类型的数据,确保数据访问性能和安全。
监控和报警:
为每个系统部署独立的监控和报警系统,实时监控其运行状态和性能指标。
设置合理的报警阈值,确保在系统出现异常时能够及时通知运维团队进行处理。
环境的隔离
在软件研发流程中,为确保各阶段的稳定性和安全性,环境隔离是必不可少的。通常我们会设置以下四种独立环境:
开发环境:供开发团队进行代码编写、功能实现和调试工作。
测试环境:用于测试团队开展功能验证、系统集成测试等质量保障工作。
预发布环境:由运营和产品团队进行用户验收测试(UAT),模拟真实使用场景,确保产品表现符合预期。
线上环境:作为最终部署环境,直接面向终端用户提供服务。
在部署过程中,必须遵循从应用层到中间件层再到存储层的分层部署原则,严禁跨环境调用,例如测试环境调用线上环境的数据或服务等。
数据的隔离
随着业务的发展,我们提供的服务往往需要支持多业务和多租户。为了确保各租户数据的安全性和隐私性,我们需要在存储层对数据进行隔离。数据隔离可以按照不同的粒度进行,主要有以下几种方式:
租户ID字段隔离:
在数据库的表中增加一个租户ID字段,通过该字段区分不同租户的数据。
示例表结构:
order_id:订单ID
sku_id:商品ID
tenant_id:租户ID
库级别隔离:
为每个租户分配独立的数据库,确保数据完全隔离。
示例:
京东零售用户数据存储在jdu库中。
ISV用户数据存储在自己的独立库中。
其他电商平台用户数据存储在各自的独立库中。
数据的隔离除了按照业务进行隔离外,还有按照环境进行隔离的,比如我们的数据库分为测试库,预发库,线上库,全链路压测时,我们为了模拟线上的环境,同时避免污染线上的数据,往往会创建影子库,影子表等。根据数据的访问频次进行隔离,我们将经常访问的数据称为热数据,不经常访问的数据称为冷数据;将经常访问的数据缓存到缓存,提高系统的性能。不经常访问的数据持久化到数据库或者将不使用的数据进行结转归档。
核心与非核心隔离
应用根据重要程度分为0、1、2、3级。业务流程也分为黄金和非黄金流程。例如,在交易过程中,订单系统和支付系统是核心,而通知系统相对不那么重要。为了确保核心系统的稳定性,我们会投入更多资源到订单和支付系统,通过异步方式将通知系统与其他两个系统隔离,避免互相影响。
读写分离
在领域驱动设计(DDD)中,CQRS(Command Query Responsibility Segregation)技术将写服务和读服务分开。写服务处理客户端的写命令,而读服务处理读请求。这种读写分离不仅能提高系统的可扩展性,还能提高可维护性。例如,应用层采用微服务架构,可以随意扩展机器来处理写操作,而存储层需要持久化,扩展较困难。除了应用层的CQRS,存储层面也会进行读写分离,例如采用一主多从的数据库架构,读请求可以路由到从库,从而分担主库压力,提高性能和吞吐量。
线程池隔离
线程是一种重要的系统资源,但创建和销毁线程会消耗大量系统资源。为了提高效率,我们采用线程池技术,通过复用线程来减少开销。然而,在共享线程池时,需要避免一个API接口的任务干扰其他接口的任务,因此我们需要实现线程池的隔离。
技术方案
为了实现线程池的隔离,我们可以为每个API接口分配独立的线程池。这样,即使某个API的任务出现异常,也不会影响到其他API的正常任务。
例如:
API1 使用线程池1
API2 使用线程池2
API3 使用线程池3
通过这种方式,确保了各个API的线程隔离,从而提高了系统的稳定性和可靠性。
代码Review
代码Review是软件开发过程中非常重要的一环,它有助于提高代码质量,减少错误,并保证代码风格的一致性。
技术方案
形成团队代码风格:团队内应统一代码风格,包括命名规范、缩进、注释等。新成员应遵守这些规范,以提高代码的可读性和维护性。
明确Review重点:在Review时,不要过于纠结细节,而应主要关注代码风格和结构。如果团队代码风格统一,通过Review代码风格就能发现大部分问题。在关注功能的同时,也要注意性能和安全性。
结对编程:鼓励结对编程,这样在有需求时,可以通过同事间的配合,提高代码质量。熟悉模块的同事可以负责细节把控,架构师则把控整体风格。
控制每次Review的代码量:不要一次性提交大量代码进行Review,应将代码按功能或模块细分,每次只Review一部分,这样更容易发现问题。
开放心态:Review不仅是找问题,更是学习和提升的过程。通过Review,虚心接受他人的建议,学习优秀的编码方式,提高自己的代码水平。
📥博主的人生感悟和目标
希望各位读者大大多多支持用心写文章的博主,现在时代变了,信息爆炸,酒香也怕巷子深,博主真的需要大家的帮助才能在这片海洋中继续发光发热,所以,赶紧动动你的小手,点波关注❤️,点波赞👍,点波收藏⭐,甚至点波评论✍️,都是对博主最好的支持和鼓励!
- 💂 博客主页: Java程序员廖志伟
- 👉 开源项目:Java程序员廖志伟
- 🌥 哔哩哔哩:Java程序员廖志伟
- 🎏 个人社区:Java程序员廖志伟
- 🔖 个人微信号:
SeniorRD
📙经过多年在CSDN创作上千篇文章的经验积累,我已经拥有了不错的写作技巧。同时,我还与清华大学出版社签下了四本书籍的合约,并将陆续出版。这些书籍包括了基础篇、进阶篇、架构篇的📌《Java项目实战—深入理解大型互联网企业通用技术》📌,以及📚《解密程序员的思维密码–沟通、演讲、思考的实践》📚。具体出版计划会根据实际情况进行调整,希望各位读者朋友能够多多支持!
🔔如果您需要转载或者搬运这篇文章的话,非常欢迎您私信我哦~