1 概述
1.1 背景
为什么需要架构设计?随着软件规模的增加,当系统由许多部分组成时,算法和数据结构不在构成主要的设计问题,整个系统的软件架构就会产生一些列新的设计问题。
- 系统规模庞大,内部耦合严重,开发效率低
- 系统耦合严重,牵一发动全身,维护困难,修改和扩展困难;
- 系统逻辑复杂,容易出问题,稳定性差,出问题后很难排查和修复。
无论是结构化编程,面向对象编程还是软件架构,都是对达到一定规模的软件进行拆分,随着软件复杂度不断增加,拆分力度越来越粗,拆分的层次越来越高。
1.2 目的
架构为了应对软件系统复杂度而提出的一个解决方案,架构设计的目的时为了解决软件系统复杂度带来的问题。
- 这么多需求,从哪里开始下手进行架构设计呢?
- 架构设计要考虑高性能、高可用、可扩展,全部设计完估计要1个月,但老大只给了1周时间。
- 业界A公司用的X架构,B公司用到的Y方案,两个差别比较大,该参考哪一个呢?
如果明确了“架构设计时为了解决软件复杂度”目的后,就很好回答:
- 这么多需求,从哪里开始下手呢?—— 熟悉和理解需求,识别系统复杂度所在的地方,然后针对复杂点进行架构设计。
- 架构需要考虑高性能,高可用,可扩展,设计时间不够 —— 架构设计并不是面面俱到, 根据业务识别出自己业务的复杂点,有针对性的解决问题
- 参考A公司还是B公司的架构? —— 理解不同架构解决的复杂点,然后和自己的业务复杂点对比,参考复杂点相似的方案,或者没有办法参考,自己设计。好的设计绝对不是模仿,不是生搬硬套某个模式,而是对问题深刻理解之上的创造与创新。
比如我们做的工业互联网平台,根据系统的这些特点,我们可以识别到如下复杂度:
- 可用性:软件出问题会影响生产,所以需要进行高可用设计
- 展性性:对接很多千差万别的协议,每个工厂的需求会变化,所以需要系统有很强的扩展性。
- 高性能:高并发,大流量在工厂环境不存在。工厂的生产非常的稳定,不会有突发的流量。
- 成本:主要是采集的网关和服务器,服务器数量少对成本影响小,网关数量多对成本影响大。
2 复杂度的来源
2.1 高性能
- 技术发展带来的性能提升不一定会带来复杂度的提升,如内存变得更大,cpu运算更快,机械硬盘变为SSD。
- 新开辟的技术领域才会带来复杂度的提升,单体计算机内部为了高性能带来的复杂度,如多进程和多线程。以及多台计算机集群带来的复杂度。
不是最新的技术就是最好的 需要结合业务进行分析,判断,选择和组合。Redis采用的单线程,Memcache采用的是多线程,这些系统都实现了高性能,但是内部实现差异很多大。
2.1.1 集群的复杂度
虽然计算机的性能快速发展,但和业务的发展速度相比,还是小巫见大巫了,尤其时进入互联网时代后,业务发展远远超过了业务的发展速度。通过大量机器提升性能,并不仅仅是增加机器这么简单,让多台机器配合起来达到高性能的目的,是一个复杂的任务。
2.1.1.1 任务分配
随着服务器增加,负载均衡也会成为瓶颈,负载均衡也需要跟着上集群。
2.1.1.2 任务分解
如果业务越来越复杂,1台机器扩展到10台,性能只提升了5倍。为了继续提升性能,就需要进行任务分解,将业务拆分为更多的组成部分。
- 简单的系统更容易做到高性能,功能简单,更容易进行针对性的优化。
- 可以针对单个任务进行扩展,容易发现出现性能瓶颈的子任务,只需要针对瓶颈子系统进行性能以优化和提升。
子系统不是越多越好,系统拆分的太细,完成某个任务,系统间调用的次数会呈指数级上升,系统间调用通过网络传输,性能比系统内调用低得多。如何把握这个粒度变得的很关键。
2.2 高可用
系统的高可用方案五花八门,本质上都是通过冗余来实现高可用。一台机器不够两台,一个机房可能断电就2个机房,一个城市可能出现天灾,那就2个城市,一条网络可能故障,那就2条或者3条(移动,联通电信一起来)。 高性能和高可用都是通过增加基础实施(服务器和网络带宽等)来达到目的,但区别是:高性能是为了“扩展”处理性能,高可用是为了“冗余”处理单元
2.2.1 计算高可用
涉及到1主多备(keepalived的高可用方案就是只有1个主提供服务,所以是1主多备),还是n主m备(如果采用eureka进行高可用,那就是n主0备,所有的服务都提供服务)
2.2.2 存储高可用
将数据从一台机器搬到另一台机器,需要经过线路进行传输。线路传输的速度是毫秒级别,分布在不同地方的计方,传输耗时需要几十甚至上百毫秒。这意味着数据不一致,“业务 = 数据 + 逻辑”。
存储高可用的难点不在于如何备份数据,而在于如何减少或者规避数据不一致对业务造成的影响。
2.2.3 状态决策
通过冗余来实现的高可用系统,状态决策本质上就不可能做到完全正确。
- 独裁式:只有一个决策者,决策者出现问题整个系统就无法进行决策了。
- 协商式:系统之间通过信息交换进行决策。难点在于如果信息交换出现问题,就会导致决策出现问题,出现多个主,或者没有主的情况。
- 民主式:如zookeeper, 采用paxos算法,按照多数取胜的原则。
无论采取什么样的方案,都无法做到任何场景都没有问题
2.3 可扩展性
软件系统与硬件或者建筑相比,有一个很大的差异:软件系统在发布后可以不断地修改和演进,这就意味着不断有新需求需要实现。
2.3.1 预测变化
- 不能每个点都考虑可扩展性
- 不能完全不考虑可扩展性
- 所有的预测都存在出错的可能性
2.3.2 应对变化
- 系统需要拆分出变化层和稳定层
- 设计变化层和稳定层之间的接口
- 设计抽象层和实现层。
2.4 低成本
对于工业软件,服务器的数量只有几台,但是车间里面操作的终端和网关数量会比较多,如果有100台设备,平均5台设备一个终端和网关,那就需要20个终端和20个网关,如果一个终端节省500块钱,那么整个方案就可以节省2W块钱。如果一个工厂有几百台设备,那么就可以为工厂节省十几万的成本,那么产品的竞争力会有很大的提升。
只有创新才能解决低成本,如网关侧为了快速上线使用java开发,后续使用改为go开发,会大大降低系统资源,节省硬件成本,如果再进一步,将网关进一步简化,更多的功能拿到服务器侧,稳定性会提升,硬件成本会进一步降低。 这里也是判断和取舍,选择了快速出产品,然后再不断对产品进行演进。
无论是引入新技术还是创造新技术,都是一件复杂的事情,引入新技术需要熟悉新技术,并且和现有技术结合起来。创造新技术就更难了,需要有全新的理念和技术,并且比旧技术有质的飞跃。
2.5 安全
- 功能安全: XSS攻击,SQL注入,CSRF攻击等,现在的框架都内嵌了常见的安全功能,大大减少了安全相关功能的重复开发,但是框架只能预防常见的安全漏洞和风险(XSS攻击,SQL注入,CSRF攻击等),无法预知新的安全问题。所以只能在攻防大战中逐步完善,不可能一劳永逸的解决。
- 架构安全:如防火墙设置不同的访问控制策略。但是如果采用DDoS攻击大量消耗带宽,出口带宽耗尽时,整个业务对用户来说也是不可用的。所以互联网架构没有太好的设计手段,更多的依靠运营商或者云服务商强大的带宽和流量清洗能力,较少自己设计和实现。
2.6 规模
很多企业级的系统,既没有高性能的要求,也没有双中心高可用的要求,也不需要很高的扩展性,但是经常会被说这个系统好复杂。因为发展历史长,不断增加新功能——量变引起质变,数量超过一定阈值后,复杂度就发生质的变化。
- 功能越来越多:系统间都是有联系的,功能越多复杂度越高。
- 数据越来越多:系统数据越来越多时,也会由量变带来质变。如Mysql单表的数据因不同的业务和场景会有不同的最优值,但不管怎样都肯定有一定的限度的,一般推荐5000万行左右。如果单表达到了10亿行,就会产生很多问题。
3 设计的原则
3.1 合适原则
没有最好的架构只有适合业务的架构,“合适优于业界领先”,避免过度设计。
- 团队的规模决定了架构,没那么多人干不了那么多活,否则会失败。
- 没有积累想一步登天,业界的方案都不是一对天才灵机一动,加班做出来的,都是逐步发展和完善的。
- 业务成就技术,没有业务场景却要实现一个空想的系统,注定会失败。
优秀的架构是在企业当前的人力、条件、业务等各种约束下设计出来的。最好的架构一旦脱离了它所使用的场景,一切都将是空谈。
3.2 简单原则
逻辑清晰,结构简单的架构,需要进行合理的分层,对服务进行合理拆分,对模块进行合理划分,让整个系统做到高内聚,低耦合。
- 结构的复杂性:组件越多,组件间关系越复杂,系统的复杂度越高。容易出现故障,容易相互影响,定位问题困难。
- 逻辑的复杂性:如果所有功能和逻辑都在一个组件中,会导致系统庞大,版本多,发布和维护困难。合理的模块划分,合理的分层。
KISS原则(Keep It Simple, Stupid!)
3.3 演化原则
演化优于一步到位。
对于建筑来说,永恒是主题;对于软件来说,变化是主题, 软件架构需要根据业务发展不断变化,不要试图设计一步到位的软件架构,期望不管业务如何变化,架构都稳如磐石。
软件设计其实更加类似于大自然“设计”一个生物,通过演化让生物适应环境,逐步变得更强大。大型网站都是从小型网站发展而来。网站的价值在于网站能做什么,而不在于它是怎么做的。大型网站架构技术的核心不是从无到有搭建一个大型网站,而是能够从一个小型网站伴随业务的发展,慢慢的演化成一个大型网站。创新的业务发展模式对网站架构逐步提出更高要求,才使得创新的网站架构得以发展成熟。是业务成就了技术,事业成就了人,而不是相反。 大公司的经验和成功模式固然重要,值得学习借鉴,但如果因此而变得盲从,就失去了坚持自我的勇气,在架构演化的道路上迟早会迷路。脱离网站业务发展的实际,一味的只求时髦的技术,可能会将网站技术发展引入崎岖的小道,架构之路越走越难。
架构师要时刻提醒自己,不要贪大求全,或者盲目照搬大公司的做法。应该认真分析当前业务特点,明确业务面临的主要问题,设计合理的架构,快速落地满足业务的需要(机会不等人,时间就是金钱),然后在运行中不断完善架构,不断随着业务演化架构。
技术是用来解决业务问题的,而业务问题可以通过业务的手段解决。
首先采用简单的方式(简单原则),满足当前业务(合适原则),随着业务的发展逐步演化(演化原则)。
4 架构设计流程
4.1 识别复杂度
如果运气不好,接手了一个多个复杂度都存在问题的系统,那该怎么办? 答案是一个一个解决问题,先解决主要矛盾,重要的事情只有一件。不要幻想一次架构重构解决所有问题。 如果一个系统存在如下问题:
- 系统不稳定
- 子系统数量太多,系统关联复杂,开发效率低
- 不支持异地多活。
同时解决这么多问题肯定会面临很多困难,要做的事情太多,会无从下手。正确的做法,将复杂度问题列出来,根据业务,技术,团队等情况综合后排序,优先解决最主要的复杂度问题。 如上面的系统,可以选择优先将子系统数量降下来,降下来后,可以提升开发效率,易于开发和调试是最重要的事情,可能一些小问题也基本消失了。然后再把系统做稳定。最后做异地多活。
如以工业网关为例, 复杂度主要是:
- 设备协议千差万别
- 需要有很好的可扩展性,随时增加协议
- OT/IT网络大多数情况下不是互通的
- 网络会出现问题,需要有采集数据缓存和补报的功能
4.2 设计备选方案
软件技术层出不穷,但是经过时间考验,已经被各种场景验证过的成熟技术其实更多。如高可用的主备方案,集群方案,高性能的负载均衡,多路复用,可扩展的分层,插件化等技术。绝大部分时候,我们明确了目标后,按图索骥就能找到可选的解决方案。只有这种方式不能满足需求的时候,才会考虑进行方案的创新,事实上创新的绝大部分情况也是基于现有的成熟技术。
架构师最好做多个备选方案,写出来可以整理思路进行更好的评估,利于评审,避免自己经验的局限。设计备选方案的合理方法:
- 数量以3 ~ 5个为佳。少于3个可能考虑不周全;多余5个则耗费了太多的经历和时间,方案之间可能差异不大。
- 备选方案差异要比较明显。例如主备方案和集群方案差异明显。主备方案用zookeeper还是keepalived的差异也很明显。
- 备选方案不要局限于已经成熟的技术。架构师可能自觉或不自觉的使用熟悉的技术,对新技术有一种不放心的感觉
- 备选方案不是最终方案,不用非常详细,既浪费时间也会进入细节,架构关注的是全部而不是细节。
备选方案关注的是技术选型,而不是技术细节。
还是以工业网关为例:
- 方案一:基于开源的edgex,需要增加对接平台层,增加数据补报功能
- 方案二:基于开源方案Node-Red,需要增加对接平台层,增加数据补报功能
- 方案三:自研方案,每个协议一个服务,服务启动时向网关管理服务进行注册。启动时从平台获取配置,实时配置通过平台实时下发。数据上报失败存储数据,然后感知网络变化进行补报
4.3 评估和选择备选方案
列出关注的属性,然后从这些属性的维度去评估每个方案,再挑选适当的情况的最优方案。 质量属性主要包括:性能,可用性,扩展性,硬件成本,项目投入,复杂度,安全性等。
如果一个购物网站现在的TPS是1000, 预期一年内发展到2000 TPS(翻一番已经是很好的情况了),性能方案只要能超过2000 TPS就是合适的方案,不要有这样的担心万一运气好业务翻了10倍呢?不要因为小概率事件而过度设计,基于合理的概率进行设计就可以了。 根据演化原则,出现这种问题是好事啊,业务发展迅猛,钱和人都不是问题,重新做的代价也可以接受。利用“合适”和“简单”原则,快速上线,尽快让业务有翻10倍的机会才是最重要的。
完成方案的360环评后,我们要基于评估结果整理出360环评表。一目了然的看到各个方案大的优劣点。360环评表只能帮助我们分析各个备选方案,不能帮我们选择具体方案,因为没有完美的方案。如开源组件工作量小,但是扩展性差。自研工作量大,扩展性好等。
怎么选择呢?
优先级法,给各个指标排优先级,那个满足最高优先级,选哪个。架构师综合当前业务发展情况,团队人员规模和技能,业务发展预测等因素,将质量数形排优先级,优先选优先级最高的。
属性 | 方案1 | 方案2 | 方案3 |
---|---|---|---|
性能 | 高 | 中 | 高 |
复杂度 | 中 | 高 | 高 |
硬件成本 | 中 | 中 | 低 |
运维性 | 高 | 高 | 高 |
可靠性 | 高 | 高 | 高 |
人力投入 | 低 | 低 | 高 |
开发周期 | 短 | 短 | 长 |
开发定制 | 快 | 中 | 快 |
技术栈匹配 | 高 | 低 | 高 |
根据项目的特点灵活增减。
优先级:项目可控性,开发周期,定制开发的优先级比较高。所以很自然先排除方案2。 由于使用开源的方案可以直接使用,虽然没有发布正式的生产版本,但是也可以运行,我们的技术栈也是java,完全可以hold住。缺点是java占用的资源多一些,但是是微服务的架构,后续可以慢慢切换到后续的go版本呢。 所以根据合理,简单和演化的原则,选择了基于edgex方案进行开发。
4.4 详细方案设计
完成备选方案的设计和选择后,接下来就是对所选的备选方案进行细化,形成一个可以落地的设计方案。
详细方案设计就是将方案设计的关键技术细节给确定下来。
- 如确定使用kafka做消息队列,需要确定副本数,集群节点数。如何保证数据不丢,去重和保序等
- 如果确定使用Mysql分库分表,就需要确定哪些表需要拆分,按照什么维度拆分,使用类似sharding-jdbc的中间件,还是自己实现等。
- 如果确定使用Nginx做负载均衡,那nginx的主备怎么做,负载均衡的策略用哪个(轮询,权重分配还是ip hash?)等。
其实,详细设计方案里面也有一些属性点和备选方案类似,如也有很多备选方案,同样面临选择,但是这里的技术方案选择是相对轻量级的,只需要简单的根据这些技术的使用场景选择就可以了。
详细设计阶段可能遇到极端的情况发现备选方案不可行,一般情况下是设计备选方案时遗漏了某个关键技术点或者关键质量属性。如方案设计忽略开发周期的项目,项目需要半年内交付,开发需要1年,这种情况就需要进行权衡了,是加人能解决的吗?还是风险很大需要重新设计架构。
如何避免推翻备选方案呢?
- 架构师不仅要进行备选方案设计和选型,还要对备选方案的关键细节有较深入的理解。如果选择kafka,需要对kafka的设计原理有深入的了解,如分区,副本,集群,主题等技术点。不能倒听图说它很牛就选它。架构师没时间对所有的技术深入理解的时候,可以让开发团队成员承担,架构师来评估。架构师对架构负责,但是不是说整个架构全部由架构师设计。
- 通过设计团队进行设计,博采众长,汇集大家的智慧。当时我们设计一个公司下一代产品架构的时候就是从5个组各抽取1个架构师组成架构小组进行设计的。
- 分步骤,分阶段,分系统的方式进行设计,尽量降低复杂度。如果某个细节复杂度过高,适当降低复杂度,可以减少风险。
- 架构设计也是一个迭代的过程,提前讨论,提前评审,可以降低返工的风险。
5 架构师的职责
架构师除了架构设计,软件开发等技术类工作,通常还需要承担一些管理职能,规划产品路线,估算人力资源和时间资源,安排人员职责分工,确定里程碑节点,指导工程师工作。这些管理事务需要对产品技术架构,功能模块划分,技术风险熟悉的工程师参与或直接负责。
作为架构师,应该时刻提醒自己主线是什么,而不是实现的细节。
5.1 共享美好的蓝图
- 清楚的描述蓝图:产品做什么,不做什么,要达到什么目标。
- 蓝图的价值:能为用户创造什么价值,实现什么的样的市场目标,产品最终的样子
- 蓝图足够简单:一句话能够描述蓝图。
架构师要保持对目标和蓝图的关注
5.2 共同参与架构
架构师对架构负责,但不是说一定要架构师自己完成架构。
- 架构一定要让项目参与者充分评审和讨论
- 一些子系统的架构同样也可以分给项目成员设计
5.3 学会妥协
不需要在项目中证明自己是正确的,我们是来做软件的,实现客户需求的,不是来当老大的。技术方案和架构的反对意见,意味着方案和架构被关注,被试图理解和接受。不用太敏感,架构师应该坦率的分享自己的设计思路,让别人理解自己的想法,并且努力理解别人的想法,求同存异。
技术细节的争论应该立即验证而不是继续讨论,当深入到技术细节的时候,意味着架构设计和各方意见已经趋于一致。
5.4 团队成长
做项目不但要给客户创造价值,为公司盈利,还要让项目成员成长。要让项目成员在知识技能和业务水平都得到提高。
6 架构设计和程序设计
- 架构设计的关键思维是判断和取舍, 是一种平衡的艺术
- 程序设计的关键思维是逻辑和实现。
选型是判断和取舍,是否引入新技术是判断和取舍,是选择业界先进的技术还是团队熟悉的技术也是取舍。要取得极致的性能,就要在其他地方(比如通用性,易用性和成本等方面)有所牺牲。
- 设计一个wms系统时,是采用简单的keepalived方法做高可用,还是采用负载均衡做高可靠。
- wms和wcs两个系统是使用rest通信,wcs存数据到数据库,还是直接写公共表,然后wcs轮询数据库;还是通过消息队列进行发布和消息持久化。
基于我们的wms业务出发,系统只需要满足高可用,并发量不大,所以采用简单的keepalived,开发和维护更简单。了解到wms还是需要通知其他外部系统,是一个很典型的生产者消费者模型,还可以利用消息队列的持久化,还可以不用轮询数据库,还可以发布到外部系统,所以选择通过消息队列通信。
采用时间换空间,空间换时间还是通过复杂的设计提高系统性能,这也是判断和取舍。
- 空间换时间: 设计安全域的时候,因为在转发路径上,同时满足不同维度快速查询和遍历的需求。所以在不同的维度设计hash和链表,还是用了写时复制的RCU作为锁,保证高性能,但是浪费了更多的内存。
- 时间换空间:网络传输时进行压缩。
- 通过复杂的设计提高性能:在工业互联网平台的数据汇聚服务里面,采集数据上报很快,解析上报的tag需要读取采集管理的配置项,采用了本地缓存,获取配置项时直接从进程内获取,但是需要处理缓存更新增加了复杂度,但是提高了系统性能。
7 一个的架构案例
7.1 工业互联网平台
7.1.1 网络架构
工业互联网平台和工业网关共同作为整个工业信息化服务的平台,为工厂赋能。
7.1.2 系统架构
工业互联网平台需要:
- 通过技术中台,提供基础设施,让服务的快速接入
- 基础平台层:所有服务都要使用基础服务
- 打造业务中台,不断积累沉淀基础业务功能
- 通过App层快速适应变化,应对工厂千遍万化的需求
7.1.3 控制流
- 用户配置通过从浏览器发出请求
- 请求到达API网关,API网关进行认证和操作记录。将请求代理转发给业务模块。后续安全相关的操作都会放在网关一起做点,如xss的<>等的转义等。
- 业务服务写入数据库(如果是读取操作,先从缓存读取,如果不存在读取数据库,并更新缓存)
- 业务模块通过AMQP通知其他服务
- 其他服务订阅AMQP并响应通知
7.1.4 数据流
- 采集网关完成数据采集
- 通过MQTT将采集的数据送往数据汇聚
- 数据汇聚获取tag转义信息,首先查看本地缓存,如果不存在去采集管理获取
- 将采集的数据进行适当清洗和转换
- 数据汇聚通过AMQP通知其他服务
- 其他服务订阅AMQP并响应通知
- 其他服务计算并将结果写入数据库(也可能是时序数据库)和缓存
7.2 工业互联网网关
工业互联网网关为了解决工业协议碎片化的问题。所以通过分层的结构解决采集和上报的问题。
- 上报层提供标准的互联网接口如MQTT,Kafka,Restful对外发布数据。
- 支撑层和核心层提供核心的功能,如数据持久化,服务注册发现等功能。
- 设备服务层,每个协议一个设备服务,保证了协议的快速添加,修改。
这个项目可以基于开源的Edgex项目进行修改实现。
- 为了和工业互联网平台对接,所以新增了Adapter适配层进行适配。
- edgex的client和distro通过数据库通信,直接修改为记录再内存中。
- edgex的metadata和distro是设计给单机使用的,为了避免数据的不一致性,采用网关启动从工业互联网平台拉数据,新增配置实时配置,网关侧的配置的数据都存放在内存中。
- edgex的采集和上报性能的优化方式,采集和上报都采用连接池 + 线程池的方式让性能大幅提高。
- 对于实时性要求不高的场景,可以将数据缓存一段时间,批量上报。
- edgex对于保存的数据没有补报的机制,需要增加补报的机制。
- edgex原来是通过模板代码的方式,会导致很多的冗余代码,需要将公共代码抽象到公共jar中,减少不必要的代码同步。
- 我们要提供方便测试,查询和统计的接口,不仅方便发现问题,也问题定位。对于数据上报流程,从设备服务-> core-data -> export-distro -> mqtt -> 数据汇聚 -> influxdb涉及多个环节。 我们需要在每个环节对数据进行统计,证名自己模块的正确与否,数据计数建议采用原子变量。
- 阻塞队列的使用:可以削峰填谷;还可以限制流量,避免系统崩溃;还可以让各个模块间解耦。MQTT是第三方模块,我们不方便在模块内部增加统计计数,不能直接通过MQTT统计。所以接收到消息后,直接塞到阻塞队, 区分MQTT发送丢失还是influxdb写入慢。