近几年来随着互联网的飞速发展,新的架构实践方式不断涌现,但是有一件事情是永恒不变的,那就是-“架构之道”;关于如何设计出灵活、高可用性以及能够快速适应变化的系统架构,我们依旧还有很大的发挥空间。本文会介绍关于如何构建前沿的、易维护的、安全的架构的几个要点,同时你也可以把它当作系统设计的准则或者用它来验证现有的架构是否合理。 就像我们经常所说的:没有最好的架构,只有最合适的架构。一个好的架构师,可以根据具体的需求、所拥有的资源等因素综合考虑而设计出最优的架构方案。特别是现在,业务的飞速变化、数据无处不在等这些因素的影响下,技术和框架也需要在变化的过程中不断地打磨和提升以适应新的业务需要。可能当时是最好的架构,但是后来我们还是要跟着业务的变化去做改进。这并不是一件坏事情,我们只要做好应对变化的准备即可。
与代码无关 架构师这个词的意义非常广泛,有些架构师是指在公司负责编写软件的某些模块的人。当然多数公司并没有这样的职位,他们会有一些技术leader来负责具体的功能。我们这里所要讲的架构师不会太过于关注代码的细节,而是更关注系统各个模块之间如何协同、交互等一些更全局的一些事情。他们主要关注一些可能经常会被人遗忘但是又会为系统带来恶劣影响的部分,职责是确保所有的功能都能够以较好的质量及时被交付。这种人对于软件产品的成功有着举足轻重的地位,当然他们往往在一个公司里面可能同时负责好几个项目。
想象一下,两个不同的架构师来建造一艘太空飞船。第一个选择用纸来糊一个样子比较好看的,然后把这艘飞船放到一个漂亮、大小合适的玻璃橱窗里面保护起来。飞般看起来可能像下面的这个样子:
第二个架构师决定用乐高模型来拼一个太空飞船,它们可以随意组合并且比较坚固,所以并不需要额外的特殊保护。
两艘飞船看起来都是挺不错的,但是第一个用了较长的时间来完成并且后来当他们需要对这艘飞船做改进的时候,问题就暴露出来了。
第一位架构师几乎炸了,因为每一次的改动时候,他们必须要移除那个保护罩,并且需要重新再造一艘完整的飞船。虽然他们已经有了所有的模型,再加上造飞船对他来说已经很熟悉,但是他们还是花了很长的时间去完成每一次改造,另外还需要再做一个新的保护罩才能装的下那艘新的飞船。
但是对于第二位架构师来说,这一切都是不需要的。他只需要对产生影响的一些组件进行改造,制作新的组件,当一切准备就绪再添加到原来的飞船即可。
再后来,第二位架构师希望能更进一步的优化他们的制作过程,因为他们现在投入了很多的时间在上面。在经过一段时间的研究之后,他们决定尝试用一种新的材料和方法来制作这艘飞船。也就是3D打印,他们申请了一台3D打印机,制作了所有的模型,这样他们就可将一些常规的任何通过3D打印机自动完成了。
当然,这只是一个很简单的例子。但是我们能从中学到什么呢?虽然两位架构师在最开始的时候都能成功的完成最初始的功能,但是他们都面临着变化所带来的系统的调整。在集成阶段,复杂性开始显现出来,和最开始的目标无关,最终整个设计是否足够灵活、可调整、以及模块化起着至关重要的作用。
软件的架构至关重要,仅仅有较好的代码来完成功能不足以成为一个优秀的解决方案。因为它不仅仅涉及到代码,还有我们所写的各个模块之间如果交互和集成、数据如何存储、我们怎么样来进行开发和测试、以及在引进变动的时候的难易程度等等。
这些事情都是和编写代码无关,但是需要我们来花时间考虑, 并且是整个系统最后是否成功的决定性因素。
需要考虑的细节 还有一些原则比如:模块化、轻耦合、无共享架构;减少各个组件之前的依懒、注意服务之间依懒所有造成的链式失败及影响等。
DDD给我们提供了在不同的特定领域上下文以及业务功能的基础上拆分组件的指导方法。
把服务独立出来提供特定的功能,同时方便在应对变化的时候能够不影响其它的服务。
在大多数情况下,如果需要同步更新很多个服务则说明系统的耦合还不够低。当然,再完美的原则也会有例外的时候。比如当你想把系统部署在一些IoT设备上的时候,你可能要一次性部署所有的组件。这是允许的,但是,请尽量考虑服务之间的耦合及灵活性以应对将系统部署在不同平台上的需求。
即便如此,我们也不可能完全避免耦合,它总是会出现在某些场景下。这就需要我们提取一些抽象层将服务之间的交互定在契约上来避免复杂,提升灵活性。这就需要我们有一种辨别能力,能够找到那些必须放在一起来做处理而不能拆解的功能。如果这些功能是值得放在一起的,那我们就可以将它独立成一个微服务,遵循高聚合的设计原因。
我们要记住的是,系统的设计要做到比较容易地增加或者修改原来的组件。无状态的架构是系统高扩展性的基石。
特别需要注意服务和组件之间如何交互,了解不同的协议的优缺点,包括速度以及可用性,来帮助我们来决定哪一种是最适合我们的。
基础设施、配置、测试、开发、运维 为配置管理定义策略,因为同时发生的配置变化对系统所有造成的影响也是很重要的,所以需要由全局层面的的自动化更新方案来完成。
在如今,对于一个数据敏感的大型解决方案来说如果没有自动化的一些基础设施和稳固的开发、测试和部署流程,那就和自杀无异。我们需要花费一定时间来计划和准备开发、测试、生产环境,可能还需要一些额外的环境以备不时之需。
测试流程和策略也是非常重要的。一些最佳实践使用Blue-Green开发、金丝雀部署、A/B测试等。尽量保持测试环境与生产环境是一致的,至少硬件结构上来说应该是一样的。一定要做压力和负载测试,并且尽可能快的在生产上进行这样的测试,这样能够更快速、精确的帮我们找到线上的问题。
可调整的架构同时也意味着服务要可以被灵活独立的部署以及简单的基础运维操作。
利用不可变基础设施的优势 不可变基础架构,就是说系统一旦部署,就不再更变升级。当服务/应用需要升级时,只要部署一个新版系统,摧毁旧版就好了。在这个过程中,系统对外服务是几乎是持续的。(译者注)
保证打包/持续集成进程是基于统一的途径,并且不会对正在运行的服务进行任何更改(比如 禁用ssh),所有的更新都应该通过定义好的自动化配置和打包操作将它们应用到所有的对应的系统上,来避免配置遗漏。比如手工某个环境上修改配置,可能会漏掉其它环境的配置。
开发团队不应该过度关注基础设施,因为有一天可能基础设施也会发生改变,所以与业务相关的开发不要和基础设施有过重的绑定。
代码之间的东西(in-between the code) "in-between the code" 可以统一的概括为一些基础设施所提供的功能,比如:服务发现、请求路由、网络通迅层、代理、负载均衡等等。很多生产上的错误并不是因为代码的业务逻辑错误或者每个独立组件本身的问题,而是由于这些把各个组件协调起来的一些通用基础设施。
随着系统的变化越来越快,更要注意我们所更改的一些组件,考虑可用性和扩展性。制定最小风险的计划来应对出现的问题。
无处不在的坑
做一个偏执狂。为失败而设计架构 - 列举所有可能失败的地方。和团队头脑风暴对所有可能失败的地方进行质疑然后提出保护方案。
如果连接建立失败怎么办? 如果花费的时间超出预计怎么办? 如果请求返回不清楚的数据或者不正确的答案怎么办? 如果请求返回的数据不是约定好的怎么办? 如果出现很高的并发能应对吗? 如果服务挂、机组、整个数据中心挂掉了怎么办? 如果数据库损坏了怎么办? 如果部署的时候失败了怎么办? 如果在部署成功之后生产环境上某些功能失败了怎么办? 集成性这方面的错误可以有千万种可能,那么我们应该如何来避免? 使用一些技术比如:熔断(circuit breaker)、超时设定(timeouts)、握手信号(handshaking)、舱壁(bulkheads)来帮助我们保护这些系统集成之前的问题。
熔断模式(circuit breaker)可以参考电路熔断,如果一条线路电压过高,保险丝会熔断,防止火灾。放到我们的系统中,如果某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。
舱壁模式(bulkheads)该模式像舱壁一样对资源或失败单元进行隔离,如果一个船舱破了进水,只损失一个船舱。比如采用微服务是首选,比如Docker。Docker是进程隔离的,单个 Docker失效不会影响其他Docker容器。或者把大的并行处理工作,由多个线程池来负荷分担。
当然,如果当它开始工作的时候,说明我们的系统出现了比较大的问题,需要我们去调查分析。
注意那些不能看到代码的组件、依懒项以及共享的资源。除了有正确的开发和测试流程以外,还应该尽量使用和真实生产环境一样的数据、以及硬件网络配置来进行测试。
跟踪系统的响应来防止一些比较常见的问题比如服务不可用的情况,留意系统的平均响应时间,当它有异常的时候需要寻找原因以及采取相应的行动。
搭建日志、监控、以及系统操作的自动化操作平台。由于微服务相对来说较独立,可以更方便检测失败 所以监控起来会更容易一些。一些比较不错的方式是在收集和分析日志的时候使用关联ID、通用日志数据格式等。注意日志数据可能会非常庞大,所以要考虑日志的时间周期,定义对日志进行归档。另外还有一些不错的工具可以将数据可视化在页面中,可以更直观的看到一些重要的进程。
为了保证服务的更新不会影响客户端的使用,对于服务的版本控制也是非常重要的。有些情况下同时运行不同版本的服务也是很常见的情形,我们要做好长期向后兼容的准备。
务必要记住的事情 大多数情况下我们并不是从零开始去构建,而是在现有的系统上继续添砖加瓦,而原有的系统在开发、运维、以及架构灵活性上都存在一些问题。想必很多优秀的开发人员在经历这样的情况的时候,都会想去拆解、重构整个系统,但是我们需要谨慎地来完成这个事情。当系统以错误的方式成拆分成组件或者服务单元之后也是一件很危险的事情 。
大多数系统在一开始的时候都是一个单体应用,后来不断地被拆解成为微服务。下面有一些基本的理念可以在我们做拆解地时候当作参考:
在开始拆分前了解具体的业务需求和业务领域 注意一些和其它业务共享的功能和数据,它们需要被正确地模块化 这种迁移和升级适合一步一步、一点一点地来完成,仅仅做当前最合适的事情 在开始之前很好地理解业务领域的范围及边界,因为对边界的调整将是非常昂贵的 对于改造有清晰的结构此次会涉及到哪些团队的调整 人、团队、和组织的影响 这个话题太大,大到我们需要专门写一篇文章来细述。在这里简单地概括一下,在本文中我们所提到的架构的灵活性以及稳健的开发、测试、运维等流程都会影响企业的组织结构。合适的组织结构能够给团队更大的灵活性并且更有机会持续地创新,在这种组织结构下,团队可以根据自己的节奏来工作。
组织不应该按技术来拆分团队,比如前端团队、移动端团队、后端团队或者根据不同的技术语言拆分团队等,而是应该按照微服务来拆分团队(也可以理解为按独立的业务单元来拆分)。这样在一个团队里面就会包含各种不同的技术,可以用不同的语言来实现服务,这也给团队更多的自由和自主权。
如何实践? 容器化和集群工具
Docker Docker Swarm Kubernetes Mesos Serf Nomad 基础设施自动化/部署
Jenkins Terraform Vagrant Packer Otto Chef、Puppet、 Ansible 配置
Edda Archaius Decider ZooKeeper 服务发现
Eureka Prana Finagle ZooKeeper Consul 路由和负载均衡
Denominator Zuul Netty Ribbon HAProxy NGINX 监控、跟踪、日志
Hystrix Consul health checks Zipkin Pytheus SALP Elasticsearch logstash 数据协议
Protocol Buffers Thrift JSON/XML/Other text
一些关于以上工具的介绍
由于本文涉及到大量的开源组件,下面简单列举一些以供参考(部分内容来自于互联网)。
Docker Swarm
Swarm发布于2014年12月的DockerCon,用以管理Docker集群,并将其抽象为一个虚拟整体暴露给用户。其架构以及命令比较简单,同时也为希望管理Docker集群的Docker爱好者降低了学习和使用门槛。
Kubernetes
Kubernetes是Google开源的容器集群管理系统,其提供应用部署、维护、 扩展机制等功能,利用Kubernetes能方便地管理跨机器运行容器化的应用。
Apache Mesos
Apache Mesos是由加州大学伯克利分校的AMPLab首先开发的一款开源群集管理软件,支持Hadoop、ElasticSearch、Spark、Storm 和Kafka等应用架构。
Mesos Aurora
Aurora 也是Apache的开源项目之一,是长期运行服务和计划作业的 Mesos 框架。Aurora 通过一个共享机器池运行应用和服务,并且负责保持它们的运行,知直到永远。当机器失效的时候,Aurora 会智能的重新规划这些作业到健康的机器上。
Vagrant
Vagrant是一个基于Ruby的工具,用于创建和部署虚拟化开发环境。它 使用Oracle的开源VirtualBox虚拟化系统,使用 Chef创建自动化虚拟环境。
Packer
Packer是一个开源工具,用于从单一配置来源为多平台创建相同的机器映像。目前支持的平台包括Amazon EC2、DigitalOcean、OpenStack、VirtualBox和VMware。
Terraform
Terraform 是一个安全和高效的用来构建、更改和合并基础架构的工具。采用 Go 语言开发。Terraform 可管理已有的流行的服务,并提供自定义解决方案。
Consul
Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置。与其他分布式服务注册与发现的方案,Consul的方案更"一站式",内置了服务注册与发现框 架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心方案,不再需要依赖其他工具(比如ZooKeeper等)。使用起来也较为简单。Consul用Golang实现,因此具有天然可移植性(支持Linux、windows和Mac OS X);安装包仅包含一个可执行文件,方便部署,与Docker等轻量级容器可无缝配合。
Eureka
Eureka 是一个基于 REST 的服务,它主要是用于定位服务,以实现 AWS 云端的负载均衡和中间层服务器的故障转移。
Ribbon
Ribbon 是 Netflix 发布的云中间层服务开源项目,其主要功能是提供客户侧软件负载均衡算法。
Zuul
Zuul 是提供动态路由,监控,弹性,安全等的边缘服务。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门。Zuul 可以适当的对多个 Amazon Auto Scaling Groups 进行路由请求。
Finagle
Finagle是Twitter基于Netty开发的支持容错的、协议无关的RPC框架,该框架支撑了Twitter的核心服务。
Zipkin
Zipkin 是 Twitter 的一个开源项目,允许开发者收集 Twitter 各个服务上的监控数据,并提供查询接口。该系统让开发者可通过一个 Web 前端轻松的收集和分析数据,例如用户每次请求服务的处理时间等,可方便的监测系统中存在的瓶颈。
Hystrix
Hystrix旨在通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix具备拥有回退机制和断路器功能的线程和信号隔离,请求缓存和请求打包(request collapsing,即自动批处理,译者注),以及监控和配置等功能。Hystrix源于Netflix API团队在2011年启动的弹性工程工作,而目前它在Netflix每天处理着数百亿的隔离线程以及数千亿的隔离信号调用。Hystrix是基于Apache License 2.0协议的开源的程序库,目前托管在GitHub上。
ZooKeeper
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
etcd
etcd是一个高可用的键值存储系统,主要用于共享配置和服务发现。etcd是由CoreOS开发并维护的,灵感来自于 ZooKeeper 和 Doozer,它使用Go语言编写,并通过Raft一致性算法处理日志复制以保证强一致性。Raft是一个来自Stanford的新的一致性算法,适用于分布式系统的日志复制,Raft通过选举的方式来实现一致性,在Raft中,任何一个节点都可能成为Leader。Google的容器集群管理系统Kubernetes、开源PaaS平台Cloud Foundry和CoreOS的Fleet都广泛使用了etcd。
Protocol Buffers
Protocol Buffers是Google公司开发的一种数据描述语言,类似于XML能够将结构化数据序列化,可用于数据存储、通信协议等方面。它不依赖于语言和平台并且可扩展性极强。现阶段官方支持C 、JAVA、Python等三种编程语言,但可以找到大量的几乎涵盖所有语言的第三方拓展包。
通过它,你可以定义你的数据的结构,并生成基于各种语言的代码。这些你定义的数据流可以轻松地在传递并不破坏你已有的程序。并且你也可以更新这些数据而现有的程序也不会受到任何的影响。
Thrift
Thrift是一种可伸缩的跨语言服务的发展软件框架。它结合了功能强大的软件堆栈的代码生成引擎,以建设服务,工作效率和无缝地与C ,C#,Java,Python和PHP和Ruby结合。thrift是facebook开发的,我们现在把它作为开源软件使用。thrift允许你定义一个简单的定义文件中的数据类型和服务接口。以作为输入文件,编译器生成代码用来方便地生成RPC客户端和服务器通信的无缝跨编程语言。
原文地址 http://lenadroid.github.io/posts/adjustable-flexible-architecture.html