05-系统架构设计

基本概念

系统可用性(Availability) :

高可用的系统,故障时间少,止损快,在任何给定的时刻都可以工作。

一般公司对系统可用性的要求在99.9%——99.99%之间,即:宕机时长在50分钟——500分钟之间。

系统可靠性(Reliability):

高可靠的系统,故障次数少,频率低,在较长的时间内无故障地持续运行。

系统稳定性(Stability):

在系统可靠性和可用性之上,即降低故障频次和提升止损速度的情况下,要求系统的性能稳定,不要时快时慢。

系统可用性和系统可靠性的区别:

如果系统在每小时崩溃一毫秒,它的可用性就超过99.9999%,但它还是高度不可靠的。

如果系统从来不崩溃,但每年的圣诞节前后停机两周,它是高度可靠的,但是系统的可用性只有96%。

故障时长:

总故障时长 = 故障发现时长 + 故障定位时长 + 故障解决时长

高并发解决方案

横向扩容(堆机器)

引入缓存

引入ES(高并发的查询场景中,包含了一些多维复杂场景查询)

分库分表

MQ消峰

单元化:

  • 将用户的请求流量,按照特定的规则,路由到不同的单元内。
  • 同时在单元内做到整个业务逻辑的闭环,起到分散系统高并发压力,快速支持扩容,快速切换容灾的作用。

如何搭建一个新系统

架构设计按照实施过程可分为工程架构,业务架构,部署架构等多个维度。

一个好的系统架构标准应该具备可扩展、可维护、可靠性、安全性和高性能等特点。

价值为先:

技术容易陷入的两个误区:

  • 来者不拒:产品经理提的需求,都是有道理的,全都负责完成。

  • 技术驱动:这种技术实现特别巧妙,让产品特性适配于技术实现。

这两类误区,很容易让研发对产品价值的理解形成偏差,容易对后续的技术迭代产生颠覆性的影响。

在方案出现歧义时,需要站在产品(商业)价值的视角审视方案并作出决策。

站在产品(商业)价值维度:

  • 能够让协作各方站在平等的视角看问题,不仅能够容易达成共识,也能更好地为业务演进和技术迭代做好规划。

架构设计:

架构模式描述了软件系统中各个组件之间的关系、职责和交互方式,从而为软件设计提供了一种规范和约束,进而提高软件生产效率。

主要体现在以下两个方面:

  • 帮助开发人员更好地组织和设计软件系统。
  • 促进团队之间的协作和沟通,使得团队成员更容易理解和分工。

工程框架:

新系统是从搭建项目的工程基础框架开始,包括目录结构、配置文件、代码模板等工程约束,主要用来规范项目结构、职责边界和代码风格,从而提高代码质量和可维护性。

具体包括以下几个方面:

  • 约定了各个模块的依赖关系和交互方式。
  • 规范接口交互协议。
  • 统一异常编码、捕获和处理。
  • 规范日志打印格式。
  • 其它公共规范约束。

技术选型:

工程架构的搭建除了基础框架外,还有就是各类基础中间件的选择,也就是常说的技术选型。

业务需求:

  • 了解业务需求,明确系统的功能、性能、安全以及未来的扩展需求。

技术特性:

  • 评估不同技术的特性,包括可用性、性能、安全性、可扩展性、可维护性等方面。

社区支持:

  • 考虑技术的社区支持程度,包括是否有活跃的社区、是否有大量的文档和教程、是否有成熟的第三方库等。

团队技能:

  • 根据团队的技能水平选择合适的技术,避免使用过于复杂或陌生的技术。
  • 否则后期的维护成本和迭代效率将成为一个大的难题。

成本效益:

  • 评估不同技术的成本效益,包括开发成本、运维成本、许可证费用等方面。
    • 如果有成熟的开源插件可用,应该尽量使用它们,而不是重新发明轮子。
    • 对于其他团队已经完成的任务,需要考虑是否可以复用。

风险评估:

  • 评估不同技术的风险,包括技术成熟度、安全漏洞、依赖关系等方面。

规范共识:

确保团队成员之间的沟通和理解达成一致。

通过制定规范和流程,可以减少重复工作和错误,避免冲突和误解,这有利于提高研发效率和质量。

  • 制定数据分层,异常管理,日志管理,监控管理的规范。

如何构建一个高可用的系统

减少故障次数:

限流,防刷,超时设置,熔断,降级

系统巡检:

系统巡检一般是应用在代码上线后,或是系统业务高峰期以前进行的,旨在提前发现并处理系统中的潜在问题。

业务高峰期以前进行,适合于业务波峰和波谷比较明显的情况。

巡检内容包括:

  • 应用、数据库、中间件服务器的硬件指标,比如:负载、CPU、磁盘、网络、内存、JVM等。

  • 系统QPS、TPS、接口响应时间、错误率等。

  • 是否有新增的慢查SQL,以及SQL执行时间和次数等,这点尤为关键。

故障复盘:

围绕故障本身去进行深挖,用追根究底的精神去发掘问题的本质,而不是仅仅停留在:

  • 开发的时候没有想到、测试的时候没有覆盖到、巡检的时候遗漏了等层面。

根据重要紧急、重要不紧急两个维度,制定短期和中期TODO。

务必明确执行人以及完成时间,并持续地监督跟进,直到所有的TODO全部完成。

另外,TODO必须是可落地的,而不是:

  • 下次开发的时候多思考、下次测试的时候多重视、下次巡检的时候多注意之类的口号流。

故障复盘不但是通过流程规范和技术策略,保证在以后的开发迭代中,系统不再引入增量的同类问题。

也是一种由点及面地去清理现有系统中的存量问题。

监控告警:

系统监控可以分为三层:基础监控、服务监控和业务监控。

上线规范:

80%的故障都是由于发布上线导致的,上线规范旨在可跳过 故障定位 环节,快速解决由本次上线而导致的系统故障。

无脑预案:

在整个过程中,只需要按照应急预案中的步骤执行,而不需要进行思考。

因为思考就会产生选择,而选择取舍是最耗费时间的事情。

故障演练:

旨在模拟生产环境中可能出现的故障,测试系统或应用在面对故障时的反应和响应能力。

系统架构的合理性

从研发角度思考:

系统的上下文清晰:

  • 明确知道和周围系统的调用关系,数据同步机制。

应用架构设计简单:

  • 架构分层合理,功能定位清晰,不会出现功能边界之外事情。

应用拆分合理:

  • 系统内的应用粒度在一个合理的范围内,应用间调用链路不应过长。

从业务角度来评估:

能解决当下业务需求和问题。

高效完成业务需求:

  • 能以优雅且可复用的方式解决当下所有业务问题。

前瞻性设计:

  • 能在未来一段时间都能以第2种方式满足业务,从而不会每次当业务进行演变时,导致架构翻天覆地的变化。

架构师的核心能力抽象能力

抽象能力就是一种化繁为简的能力。

  • 就是把一种复杂的事情变得简单的能力。

有三种方法:

  • 归纳法找共性,从多个问题中找到共同的问题提炼通用解决方案,去其糟粕取其精华。

  • 演绎法找关系,从多个问题中找关系,把多个问题串成一个问题,系统化解决问题。

  • 归纳法找特性:化繁为简需要不断的思考,不断的看清一件事的本质,这个事的解决方案越容易。

通过归纳法找共性:

通过归纳法找共性有两种方法,分别是找需求的共性和找信息的共性。

找需求的共性:

  • 从一类需求中找到共性问题,找到最大交集然后求解。
    • 收到一堆需求,能分析出共同的需求是什么。

找信息的共性:

  • 领域建模就是一种找信息共性的方法。
  • 领域建模首先就是要区分需求里哪些是变化的哪些是不变,把这个领域不变的信息沉淀成领域模型,基于领域模型做架构。

通过演绎法找关系:

找内部关系:

  • 内部关系就是找到业务的生命周期和系统内部的主链路。

找外部关系:

  • 梳理清楚架构的边界,什么做什么不做,什么是本领域的核心服务,这些服务提供给谁使用,需要依赖其他领域的核心服务有哪些。

通过归纳法找特性:

找特性是通过归纳法先找两个业务的共性。

  • 举一个例子,如果要精通JAVA要学习的内容会非常多,可能花很多时间学习也不一定能精通JAVA语言,投入产出比不高。

但是如果想化繁为简就必须先找到JAVA的特性,针对特性进行深入学习。

  • JAVA的两项特性技术是垃圾回收机制多线程框架,剩下的就和其他语言的特性差不多。

特性和找共性存在矛盾,所以在这个过程中需要做取舍:

  • 关键是面对当下的业务,判断什么当下或者未来最重要的事是什么。
  • 可能满足场景个性化需求虽然增加研发成本,但是能给业务带来技术壁垒。
  • 或者有没有一种方式能既满足共性需求又能满足部分个性化需求。

幂等问题

幂等指多次调用对系统的产生的影响是一样的。

  • 即对资源的作用是一样的,但是返回值允许不同。

Token机制:

![image-20241008160843294](https://jy-imgs.oss-cn-beijing.aliyuncs.com/img/20241008160844.png)

服务端提供了发送token的接口。

  • 在执行业务前,先去获取token,服务器会把token保存到redis中。

调用业务接口请求时,把token携带过去,一般放在请求头部。

服务器判断token是否存在redis中,存在表示第一次请求,可以继续执行业务。

  • 执行业务完成后,最后需要把redis中的token删除。

如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给client。

  • 这样就保证了业务代码,不被重复执行。

乐观锁机制:

update user set point = point + 20, version = version + 1 where userid=1 and version=1

唯一主键机制:

利用数据库的主键唯一约束的特性,解决了在insert场景时幂等问题。

但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键。

去重表机制:

![image-20241008160931090](https://jy-imgs.oss-cn-beijing.aliyuncs.com/img/20241008160932.png)

这个去重表中只要一个字段就行,设置唯一主键约束。

  • 当然根据业务自行添加其他字段。

把唯一主键插入去重表,再进行业务操作,且他们在同一个事务中。

这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。

去重表和业务表应该在同一库中,这样就保证了在同一个事务。

  • 即使业务操作失败了,也会把去重表的数据回滚。

  • 这个很好的保证了数据一致性。

这个方案比较常用去重表是跟业务无关的,很多业务可以共用同一个去重表

  • 只要规划好唯一主键就行了。

本地缓存一致性刷新

服务是多节点部署的,要保证是本地缓存一致性的,就要短时间内操作所有服务。

方案1-MQ广播消息

方案2-Zookeeper Watcher机制

方案3-RPC框架广播调用(如Dubbo广播调用方式)

方案4-分布式任务调度的广播执行任务

方案 特点
MQ广播消息 有消息积压、消息顺序的问题
Zookeeper Watcher机制 Zookeeper本身适合读多写少的场景
RPC框架广播调用(如Dubbo广播调用方式) 循环调用所有的实例,所有要考虑实例过多的情况
分布式任务调度的广播执行任务 便于定时发布

05-系统架构设计
https://janycode.github.io/2020/12/08/17_项目设计/01_业务设计/05-系统架构设计/
作者
Jerry(姜源)
发布于
2020年12月8日
许可协议