架构那些事儿 - 工作指南
架构师,架构设计,如何做架构
2021-05-18
35 次浏览
面对一个系统来了,该如何设计?下面就是我的一些个人经验总结。
1. 了解需求
对一个陌生的事情来说,了解需求总是第一步的,这里特别要忌讳的事情就是把自己的经验强加给需求提出者甚至是批评需求提出者的设计,正确的做法是多问问题。这一步的关键就是沟通,一定要多沟通,多听需求提出者,甚至是产品使用者说什么,不要主观臆测,不要自以为是。如果需求提出者也不确定,那么你就要把问题变成选择题,帮他缩小问题范围,甚至帮助其理清真正的需求。
a. 了解功能需求
功能需求就是产品有哪些功能,最好能处理成用户故事,例如:用户可以发表博文,这个故事,我们要了解前置条件是什么?(用户是不是要登录?)限制条件是什么?(要不要做字数限制?是不是要做脏字处理?要不要做代码过滤?等等),后置条件是什么?(发表之后去哪里,系统干了什么事情?)
b. 了解技术需求
技术需求包括了很多功能上看不到,但是技术上又必须去考虑的事情。
例如:预期多少人使用?同时在线多少?并发量是多少?是用客户端/Web/App/iOS/Android?有没有国际化?编码语言用什么?数据库有没有要求?预算有没有要求(这决定了是不是有硬件限制)?等等
在这个环节,不仅仅是架构师,同时也要让整个开发团队都明白将要面对的系统是什么样的。
2. 架构和模式确定(全局设计或者顶层设计)
需求理清之后,就要开始考虑架构和模式的事情了。先确定是单体还是分布式,这决定了你的后续设计。
-
a. 单体的优势就是开发速度快,系统复杂度低,考虑的东西少,比较容易拿捏,团队人员要求少;缺点就是后续扩展会比较困难(当然如果分布式设计的不好还不如从单体开始)。如果需求里面scale要求不高或者没有,面向的用户数量不多(通常是B类用户),并发低这一类的场景,用这个是最合适。
-
b. 分布式的优势就是扩展性好,健壮性强。缺点就是系统复杂度高,非功能性需求会很多(例如需要考虑到内部系统之间是如何协作的),团队成员要求多。如果需求里面scale有一定要求及以上,面向的用户数量比较多(通常是C类用户),并发高这一类的场景,用这个就很合适。
然后再确定架构,有几个曾经或者现在还比较著名和流行的架构:
a. BS和CS架构
这种架构比较落后了,除了一些小软件,或者工具软件,这里基本不会再这么考虑了。
b. SOA
面向服务的架构,它将系统中的不同功能单元进行划分,通过中立的接口定义来连接各个服务,它和微服务很像,但是它不关注粒度的问题,一个子系统可能包含好几个大能力,而另外一个子系统可能只有一两个小能力,这些子系统在全局视图中是等级别的。目前很多团队有意识或者无意识的都在采用这种模式。
c. 消息总线
这个架构的特点就是通过一个消息总线(例如一个消息队列)来使各个子系统协作。SOA模式通常与这个模式合并使用,SOA通过消息总线串起所有的服务。这个模式最关键的就是消息总线,所以消息总线有可能会有单体的问题,它的进阶方向是一是采用分布式的消息总线,二是将消息总线变成去中心化的。
d. 微服务
这是现在比较流行的模式了,优点有很多,通常靠RPC和Restful来使不同的微服务之间进行协作。定义好了协作接口,就可以使得开发人员关注自己的微服务开发,甚至不用关系其实现的具体编程语言。缺点也很明显,微服务到底多“微”,百家人有百家言,粒度越细,那么系统复杂度也越厉害,而且是呈指数级别的上市,同时还要关注APIGateway,熔断,服务治理等等。当然,你也不必在一开始就考虑到这么多,粒度也不用这么细,循序渐进是正确的做法。
e. 响应式
响应式架构包含了一系列新的概念,例如Actor模型,异步消息通信等等。这个架构的优异性能能够解决几乎任何高频率使用场景,例如:高频交易系统、移动短信等等。但是其复杂的编程模式会让3-5年甚至5-8年经验的开发者难以适应。采用这个架构,基本上会采用两个著名的框架之一,Actor和Vert.x。
f. CQRS
命令与查询分离,和响应式模式类似,使用这个模式对当前大多数开发者来说,需要适应,特别是读写分离加上事件溯源(Event Sourcing)。当然,读写分离模式也会让系统设计变得稍微复杂一些,要完全把同一个数据读写分开,需要考虑得更加细节。这个模式也可以解决很多高频并发的场景。
g. 中台
当然,中台并非是一个单纯的将系统如何有效组织在一起的架构,它更加是一种领域划分的概念,阿里是这个模式的提出者,通常有业务中台和数据中台,俗称双引擎,抛开概念来看,它更多的像是一种子系统的逻辑领域划分。中台模式是一个演变的过程,是从细节中总结归纳出来的。要实践这个模式,需要长期对系统进行架构改进。
总之,这里一定要关注的一点就是,不要过度设计,盲目趋势,一定要结合自己面对的需求来设计。言必微服务,动则分布式,都不一定是最佳选择,同时还给老板增加了不必要的成本。
3. 技术选型
确定了整体架构和模式,接下来就要做技术选型了。这里主要就是对一些常见流行的技术产品做一个核心概念了解即可,不一定非要精确到每一个技术的细节。
a. 编程语言
一般来说后端用Java/Go/NodeJS等等都可以,前端Vue/React/Angular甚至JQuery等都行,但是这里有一个原则,就是别用小众的,例如前端用ember,这会让你的团队技术成本上升,因为越大众的东西,用的人越多,网上的资料也越多,BUG也越少。但是,还要说一点,就是也不要拒绝新的好的东西,时刻关注趋势,可以在一些小的系统中进行试点,技术不能丢失了好奇心,这是原动力。
b. 技术框架
编程语言选定之后,技术框架基本也就定了,Web技术发展了这么多年,很多框架都已经很稳定了,例如Java的Spring,NodeJS的Express,这些框架都是不用多说,直接选择即可。技术框架帮我们解决了单个系统的分层问题,现在几乎所有的框架都有最佳实践,而这里面就包括了技术分层。前端框架同样如此。
c. 数据库SQL或者NoSQL
首先要明确是不管是SQL也好,NoSQL也罢,都需要找到正确的场景,而不是凭借自己的爱好。
i. SQL
SQL数据库也叫关系型数据库,将数据存储在行和列中。支持事务。在CAP理论中,实现的是数据一致性(因为分区容错总是成立),而可用性较低。所以适合需要数据严谨的地方,例如金融相关的业务。
著名的SQL数据库有Oracle,MySQL,Maria DB,PostgreSQL等。
选择SQL的明显指征就是数据必须是可靠的或者十分关注原子性、一致性、隔离性、耐久性(ACID),以及数据结构变动不频繁。
ii. NoSQL
NoSQL数据库通常我们用非关系型数据库来表示。在CAP理论中,实现的是可用性(因为分区容错总是成立,所以与数据一致性对立),而一致性通常采用最终一致性。所以适合需要快速响应,而数据不需要很严谨的地方,例如twitter的推文,博客的文章,一些新闻内容等等。那么NoSQL家族大致分为这几种:
-
- KV数据库:数据以KV的形式存储,就是哈希表的那个KV,这里最著名的就是Redis了,也是很多人深恶痛绝的面试八股之一。其常常作为缓存中间件和简单的消息中间件。
-
- 文档数据库:数据以文档的形式存储,每个文档都是一些格式的集合(例如json格式),著名的就是mongodb了,其常常与NodeJS结伴出现。
-
- 宽列数据库:数据存储在不同的列中,而每一行不必有相同的列,列是行的容器。这类数据库受到了谷歌的Bigtable论文的影响,设计的目的就是处理大数据集。所以有大数据处理的场合,就会采用。最出名的两个就是hbase和Cassandra了。
-
- 图数据库:数据存储在图结构中。图我们知道是有顶点和边,所以图数据库也会记录这些,同时还会记录顶点的属性。这类数据库就是用来解决图关系的,例如:金融行业的反洗钱模型,社交网络的社交图谱等等。
选择NoSQL的指征就是数据最终一致性甚至小误差是允许的,但是需要快速响应,方便扩展;以及数据结构变动频繁,或者暂时不确定数据应该如何组织。
iii. 查询
SQL数据通常使用标准SQL来查询,这个查询是很强大的,而NoSQL有不同的查询方式。而对于全文索引和大数据量的查询支持,NoSQL通常有更好的效率,例如MongoDB就支持MapReduce的方式。当然现在的SQL数据库也只是相对较弱,如果项目本身不涉及大数据,或者海量数据,那么SQL数据库也完全足以。
iv. 扩展
扩展性来说SQL数据库通常是垂直方向的,这意味着最有效的方式是增加CPU、内存、硬盘的性能,这通常比较贵。当然也可以采用增加服务器的方式,但是这可能涉及到需要分库分表,或者需要解决信息同步等等问题,例如MySQL采用binlog同步的方式。这些花费通常比NoSQL是要昂贵得多的。
NoSQL是水平方向的,这意味着最有效的方式是不停的增加服务器即可。NoSQL通常都自带了容错策略,数据分发、同步、备份策略。扩展的配置也十分简单。
系统需求指征不是特别明显的情况下,通常以MySQL作为默认选择是一个不错的开始,当然,当前的趋势是PostgreSQL,但是团队的学习成本肯定会增加,因为相对于MySQL,国内还不常见。这里要补充一下,特别注意一点,尽量少用甚至别用数据库的触发器和存储过程实现业务,这是一个非常差的实践,常常会令你的系统陷入万劫不复。
d. 消息队列
消息队列在很多场景都能够用到,例如:发送通知邮件,处理业务(系统)事件,甚至处理高频交易等地方都有使用。著名的消息队列有RabbitMQ,kafka和Rocketmq等等。当然,上述几个都是独立的中间件,使用起来也不复杂,能够很好的完成pub/sub,容错容灾都很好。即便是业务本身很简单,不需要这么好的容灾也可以使用他们,因为他们真的方便快捷,容易上手。当然,也不是没有其他的选择,例如Java中有ArrayBlockingQueue,也可以作为消息队列,还有Google Guava中的EventBus也可以作为消息队列,当然还有大名鼎鼎的Disruptor。
e. 缓存
缓存其实不应该是系统设计中前期就考虑的事情,在使用缓存之前,可以首先考虑使用数据库的索引技术。如果需求中有明确的指征是要用到缓存的,那么也必须提前考虑。
i. 应用程序缓存
本地缓存技术的话通常应用框架都会集成,例如Spring就默认选择了caffeine,缓存可以设计在数据访问层之上,也可以在应用层上。如果是分布式的环境下。我们还可以考虑用redis或者memcache等中间件来作为分布式缓存容器。
ii. CDN
CDN可以对静态资源进行缓存,其将内容分发到世界各地,用户获取的网站资源可以就近获得,大大提高了效率。是否使用CDN在于成本考虑,如果没有成本忧虑,能上则上。CDN的供应商有很多,可以择优选择一家。
f. 搜索引擎
如果系统本身的规模不大,或者不是内容服务型的系统,那么数据库的全文索引足够处理搜索。否则,搜索引擎是在设计初期不得不考虑的一件事情,幸运的是这个年代有非常多的强大的搜索引擎可供选择。
i. Lucene
Lucene其实只是实现了搜索引擎中最核心的部分,所以通常有不少的开源搜索引擎基于lucence做封装。采用这个产品的初衷应该是想要完全自己掌控搜索的方方面面。所以,这通常不是第一考虑的产品。
ii. Solr
Solr就是包装了lucene的搜索引擎产品,其专注于全文检索,简单来说就是一个纯粹的搜索产品。不少互联网巨头都有用到Solr,例如:亚马逊,Netffix,eBay。它突出的特点就是实时索引,还能处理文档(如Word和PDF)。
iii. ElasticSearch
同样是包装了lucene,不过ES通常和它的兄弟logstash,kibana一起出现,当然单独使用也是可以的。ES会比Solr更重一些,它需要的服务器性能也要求更好。除了全文检索与以外,它还支持分析统计。同时,ES更好扩展,社区也更活跃。也非常与时俱进,很适合云部署。
总之,很可能这个阶段并不会发生,因为团队或者公司已经有成熟的技术选型,而且技术选型是并非完全是在这个阶段,通常这个阶段确定大部分的选型就可以了,还有很多不能确定的选型,可以在未来的设计中去完成。时刻保持新鲜感的架构师们在这个地方不要那么激进,把一些未成熟的技术放入到备选方案中。
4. 模型设计
一旦开始建模,就意味着需要将需求翻译成开发人员能够看懂且能够执行的语言。
领域驱动这个词在最近几年是非常流行的,完整的DDD包含了非常多的概念和内容,但是大多数的系统都只是形式上采用了领域设计,领域对象还是数据对象,并没有区分聚合根和值对象。不管如何,对业务的领域划分和边界确定是首先且必要的。
a. 领域模型
这里一定要区别开数据模型,领域模型不是数据模型,领域模型也不等于领域对象,领域模型是对事物的高度抽象,例如电商中的Product,Order等等,它确实很像数据模型,但是它并不是。
领域中包含了一系列的同类或者需要完成这个领域业务的对象。例如电商中的订单,它除了订单本身外,还有订单的状态,以及完成这一系列业务对应的数据对象。
领域与领域之间是黑盒的,电商中的商品领域和订单领域有各自的数据模型和业务流程,它们对外是不可分割的,如果可以分割,那么可能需要优化领域模型。
没有放之四海的标准,也不要盲目模仿行业翘楚的模型,就算同一个领域在相同的行业中内里可能也不尽相同,所以对应需求做出最适合的领域建模是第一考虑的。
b. 场景模型
当领域建模完成后,就可以开始场景建模,场景建模更加贴合需求的业务流程,但是,这里要将领域与领域之间串联起来,同时也是应证领域建模是否合理,建模粒度是否合适。如果有不能满足,或者将事情变得繁琐,那么必然是需要重构领域建模的。
c. 业务和数据模型
当完成上述两项之后,可以回到领域中去完成业务和数据建模,这里的模型更加贴合代码,核心细节将被一一找出,当业务和数据模型确定的时候,基本上一个1-3年的编码人员都能依此模型快速编码。在这个阶段,一定要注意业务与数据的关系,数据与数据的关系。不管模型是与数据库表一一对应,还是通过聚合根加值对象的方式来处理,亦或者是存在事件对象等等,都需详细阐明。也就是说业务通过系统怎么做,有哪些对象来参与,哪些对象需要存储,存储策略是什么,查询策略是什么,有没有状态,状态如何转换都需要在这里逐一理清。
d. 前端模型
前端模型主要是与客户端交互的模型,不管是API还是模板技术,都存在与客户端交互的情况。
i. API
API主要是接口对象,这里的建议是不要将系统的数据对象直接作为接口对象,接口对象应该是协助前端完成交互的对象。比较朴素的做法是顶层的Request和Response接口,然后每个接口对应实现其各自的Request和Response对象。比较优雅一点的做法是抽象成为接口对象,例如User,Order等对象(区别一下系统的数据对象,这里可能会有系统没有的对象,有些又可能是几个对象的聚合,还有一些隐藏了属性,等等)。可以参考类似于Github这样的API进行设计。
ii. 模板技术
模板技术即便是在前端框架满地走的时代也并不过时,模板对象的前端模型通常就是form表单和界面数据的集合(通常可以直接用一个Map作为当前页面的数据集)。和API略微不同,由于模板的处理是在服务端,所以不会暴露系统数据对象在外,所以这里可以直接将数据对象存入页面的数据集中,然后在模板中进行显示。所以,这里需要建模的仅仅就只有form表单了。
建模完成时,必然会产出的是模型的全景视图,顶层视图是各个业务领域以及它们之间的关系,各个业务领域内部又有动态视图和静态视图,也就是场景和数据模型。特别注意的是,建模过程中,大量的细节将被挖掘出来,这个时候一定不要替需求人员去做决定,例如用户名字段的限制如果没有明确,那么就需要与需求人员沟通去设置。
5. 测试设计
当模型设计完成之后,就开始进行测试的设计,这里的测试不同于测试部门,只专注于验证系统是否能够正确运行和满足性能的需求,同时也是在代码交付给测试团队之前需要完成的自省。
a. 单元测试
TDD也即是测试驱动开发实际上是非常好的实践,除了正常的业务流程测试,有效的边界处理,异常流处理都应该在单元测试中体现。好的单元测试应该覆盖所有的业务代码。如何写好单元测试,也是架构师需要带给开发人员的理念。
b. 性能测试
在有明确性能要求的情况下,一些关键代码也需要做性能测试,特别是并发情况下的健壮性和容错处理。如果没有明确性能,那么这需要架构师评估系统相关数据决定是否进行多大程度的性能测试。如果是金融相关的系统,在钱的处理上一定要做到并发测试。
c. 自动化测试
不管团队是否包含测试团队,自动化测试环境的搭建都至关重要,同时拒绝低代码覆盖率的单元测试,每次提交都必须通过自动化的测试,以使得团队中各个成员之间的协作是高效的。如果没有通过自动化测试就进行打包的系统只是一堆不能正确运行的垃圾。
完成模型和测试设计后,编码其实已经不是一件很困难的事情了。
6. 部署设计
接下来关注的是部署,这里要关心的问题也有很多,包括需要多少子系统或者微服务,网络结构是怎么样的,是否需要容器技术,是否存在自动化编译和部署(CI/CD)等等。非常好的是近期开始流行的DevOps思想完全可以作为部署设计的指导原则。所以我们要做的事情:
a. 部署架构
如果采用的是领域设计和SOA,微服务等,部署架构就比较清晰,每个微服务或者子系统都是独立部署的,而业务领域可以内聚在一起。数据库和三方中间件也是独立部署的。这里还要考虑的地方就是要区分网络环境,开发、测试和生产环境要区别开来,系统在环境之间的转换流程要设计清晰。
b. 容器化
容器化是十分必要的,它可以非常好的避免系统打包后在不同环境下的运行差异(例如出现开发环境下能处理,但是上线就不对了的情况)。同时容器化也使得运维团队的工作更加的高效,运维的全景视图更加的清晰,监控更加的便捷。
c. 自动化
自动化可以有效降低部署过程中出现的错误,有效的支撑持续的集成和部署,同时针对不同环境的自动化集成和部署,还需要将不同环境的配置和可执行文件打包,这样可以在不同的环境下重复安装,极大的提升了开发和运维的效率。
d. 灰度发布
灰度发布正逐渐被各个行业和团队所接受和采用,其能够很好的支持产品和营销的新想法,帮助其更好的改进产品。在采用了灰度发布的情况下,系统部署与前端的导向都是比较容易思考到的,这里的难点在于数据库的设计,一种是两个数据库,灰度结果变更后做两个数据合并,第二种是增量扩展数据结构,灰度结果变更后做数据的保留或删减。
e. 容错灾备
容错和灾备在部署设计的初期是需要考虑的,当系统宕机的时候,能够快速的恢复服务,特别是一些比较重要的系统,例如金融系统。因为一些监管认证的要求,两地三中心逐渐成了标配。由于这个考虑必然是一个非常花成本的考虑,所以这里要因地制宜,结合成本来处理。但是也不要完全不考虑。
部署要考虑的方面其实还真不少,上面都是一些高层视图,如果具体到单个系统中,还要考虑内存、CPU、硬盘等等。这里一定不要怀疑为什么架构师在初期就要考虑部署,因为只有将部署的思想融入到了开发环节,才能最大的提高部署效率和正确性。
在以前,通过利用敏捷思想将一个团队从2-3个月部署一次缩短到2-4周部署一次已经非常的厉害,但是DevOps却能将部署缩短到每天一次到几次。这就是思想的进步,这种进步是每个架构师都必须去学习和追求的。
7. 技术细节
这个地方与产品层面完全不同,它更加关注技术细节的方面,例如:
- a. 时间是用LocalTime还是Date
- b. ID是自增的,还是采用生成策略
- c. 配置是采用文件还是放在数据库中
- d. 性别是枚举还是数字
- e. …
有些技术细节可能不是在一开始就能够被发现,这需要架构师不停的跟踪自己的系统进展,这里就引出了另外一个很重要的话题,就是架构师是否要参与编码,这一点我想是必然的,架构师是一定要编码的,但是不是参与到具体的某个业务流程的编写,而是要参与到重点和难点部分,同时要时常与工程师结对编程,传播正确的编程理念和最新的架构思想。
8. 设计误区
很多架构师在设计的时候都不自觉的进入一些误区:
a. 迷信大公司的解决方案
这个现象十分常见,因为阿里是这个架构,或者因为行业老大这么讲过,所以我们要采用这个设计;有的甚至是在讨论会上,某位或者某几位从阿里挖过来的工程师说“阿里就是这么搞的”,或者“Facebook就是这么做的”,瞬间就会令场面失控。架构师一定要坚持适合自己公司土壤的才是最好的。如果一味的迷信别人的解决方案,拿来主义,迟早架构要迷失。
b. 过度自信
这个就是拿着锤子看什么都像钉子,我用XXX方案解决过类似的问题,完全没问题,殊不知同样的问题在不同的公司,面对不同的客户都可能有不同的做法,过度自信只会让系统走向失败的深渊。
c. 为技术而技术
这个也是很多架构师容易犯的错误,追求时髦的技术,很可能完全不适用,或者成本很高,我们设计系统的目的是为业务落地服务的,不是为了如何让我们自己爽而存在的。
d. 企图用技术解决所有问题
这种情况最明显的就是为了迎合上级而盲目拍胸脯,没问题,能办到,能解决,很可能有些问题并不需要技术的手段来解决。
9. 总结
这个指南只是我在做架构的工作中如此去做,写下来作为自己的工作总结,以及后续或者看到这篇文章的人做架构的时候的一份参考手册,当然它不一定是准确的,但至少在我过去组织的系统中是适用的。而且,世界上唯一不变的事情就是变化,所以技术也好,架构也好,都是在发展的,工作的标准也是向着更高效,更透明,更易管理的方向发展,所以作为架构师也好,技术主管,技术总监也好,要不停的学习和提高自己。总之,不管哪一行,学无止境。