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广播调用方式) | 循环调用所有的实例,所有要考虑实例过多的情况 |
分布式任务调度的广播执行任务 | 便于定时发布 |