1.1 了解微服务架构
提到微服务(Microservice)架构,相信很多人都不陌生,它已经是当前开发社区中的热门词,与其相关的图书、技术文章、教程和资料也非常多。本小节会对微服务架构进行简要介绍。如果读者希望了解更多的内容,可以参考其他资料。
首先需要明确的是,微服务架构并不是解决全部问题的万能钥匙,它只适合于解决特定的问题。一般来说,微服务架构适合用于在线服务的后端实现中。
在线服务一般采用客户端-服务器的架构。服务器端以开放API的方式提供数据。网页客户端、移动客户端和第三方应用都是开放API的消费者。随着相关业务不断发展,服务器端的开发面临诸多挑战。
· 需要开发新功能来满足用户不断增长的需求,提升产品的竞争力。
· 扩展应用来满足不断增长的用户所带来的访问请求,提高用户的访问速度。
· 需要快速并且持续地更新应用来添加新功能和修复错误。
对于以上这些挑战,单体应用的架构不能有效地解决,或多或少都存在一些问题。
1.1.1 了解单体应用存在的问题
在传统的服务器端架构中,整个应用被打包成单一的部署单元,也就是通常所说的单体应用(Monolith)。单体应用在应对上述挑战时存在着局限性,不太能满足业务的需求,其局限性体现在如下几个方面。
随着应用复杂度不断提高,单体应用的代码逻辑超出了普通开发人员的理解能力。单体应用的全部代码逻辑都整合在一起。在开始的时候,由于应用的功能较少,代码量较少,因此容易理解。随着应用持续更新,越来越多的新功能被加入进来,导致代码量不断膨胀,以至于越发难以理解。尤其对团队的新成员来说,理解现有的代码是一个巨大的挑战。
虽然可以通过组件化技术把单体应用划分成多个单元,从而降低每个单元的复杂度。但是在实际的开发中,组件化的效果并不是很理想。这一方面是因为公共代码的存在。这些共享的代码被多个组件使用,使得这些组件之间产生了紧密的耦合。另外一方面是由于软件开发过程中的一些不规范行为造成组件之间的接口划分不够清晰。开发人员经常会在一些组件之间引入不必要的依赖,所产生的结果是造成了组件之间错综复杂的依赖关系。比如,接口在组件化中起着非常重要的作用。在引用其他组件的代码时,应该引用的是接口,而不是具体的实现。然而在实现中,很可能不小心引用了具体的实现。组件化的这些问题都可以通过良好的软件工程实践来解决,但也对开发团队的管理和技术水平提出了更高的要求。
代码复杂度的提高带来的问题是开发速度变慢。开发人员需要更多的时间来理解和评估所做的改动会对已有代码造成的影响,尤其是公共代码的修改会对很多组件造成影响。一个看似很小的改动,可能会造成灾难性的后果。如果没有足够的自动化测试的保证,对代码进行重构也会变得更加困难,进而降低开发人员持续改进代码的积极性。新功能的添加也变得更困难。随着代码量的增加,本地环境上的开发和调试变得更加耗时。在代码修改之后,编译和重启应用的时间过长,使得开发人员的宝贵时间被浪费在无意义的等待上,降低了开发效率。
由于开发速度变慢,应用的新功能和错误修复的上线速度也会被拖慢。在每次代码提交之后,持续集成服务需要构建整个应用,并运行全部的测试用例。应用的代码越复杂,所需要的构建时间就越长。在线服务都要求能够及时响应用户的需求,以最快的速度添加新功能和修复问题。这就意味着每次部署应用的时间间隔要尽可能地短。单体应用要满足这个需求的难度很大。
单体应用的可靠性也相对比较差。整个应用在运行时只有一个进程。不同的组件在运行时没有必要的隔离。任何一个组件中出现的问题,都可能导致整个应用的崩溃。单个组件在运行时可能占用大量的CPU和内存资源,导致其他组件由于资源不足而无法正常工作。一旦出现问题之后,只能重启整个应用。由于单体应用的启动时间一般比较长,会导致无法及时恢复服务。
单体应用无法高效扩展。当生产环境上部署的应用的处理能力不能满足业务的需求时,需要进行扩展。扩展分成垂直扩展和水平扩展。垂直扩展指的是增加单个应用实例所能使用的资源,包括CPU、内存、磁盘和网络带宽等;水平扩展指的是增加应用实例的数量。由于垂直扩展存在资源上限,仅使用垂直扩展无法满足在线服务的需求,需要与水平扩展相结合。单体应用在进行水平扩展时,只能以应用为单位来进行,无法有效地分配资源。
单体应用通常只使用单一的技术栈,包括编程语言、开发框架、第三方库、数据库和消息中间件等。这就要求所有的开发人员都掌握相同的技术栈的相关知识。在实际的开发中,由于需要解决问题的类型不同,不同的业务功能有其最适合的技术栈。强制使用单一的技术栈的做法会影响开发的效率。对于一个单体应用来说,一旦选定了技术栈,要对它进行更新是一件非常困难的事情。在进行更新时,开发团队可能需要暂停新功能的开发和错误修复,以方便进行代码迁移。在实际的开发中,受限于业务上时间进度的压力,开发团队不太可能有这样的机会,而只能渐进式地进行修改。所产生的效果是应用的技术栈不断地老化,积压了大量的技术负债,带来了更多的问题。
1.1.2 微服务架构概述
为了解决单体应用的这些问题,应用的架构需要进行调整。单体应用的这些问题的根源在于过高的复杂度。降低复杂度最基本的做法是使用分治法(Divide and Conquer),也就是把应用划分成若干个独立的单元,再把这些单元组合起来。这种分治法的思想就是微服务架构的理论基础。
在介绍微服务架构之前,首先要介绍的是更早之前出现并流行的面向服务的架构(Serv-ice-Oriented Architecture,SOA)。SOA的特点是把软件系统划分成多个功能单元。不同的单元之间使用通信协议在网络上进行交互。每个单元被称为服务。SOA中的服务通常代表一个业务活动。服务的实现本身是自包含的,并且可以独立部署和更新。软件系统通过服务之间的交互来实现不同的业务逻辑。由于架构上的优越性,SOA在出现之后得到了广泛的流行。遗憾的是,早期的SOA实现包含了太多基于XML的规范,使得SOA的实现过于复杂和低效。SOA从2009年之后就不再流行了。
微服务架构被认为是SOA架构风格的一种变体。微服务架构中的服务与SOA中的服务有着相同的含义,不同之处在于“微”。微服务的“微”体现在两个方面:第一个方面是微服务中服务的粒度相对较小,通常只针对特定的应用场景,实现复杂度和相关的代码量都较小;第二个方面是微服务中的服务之间使用轻量级的通信协议,抛弃了基于XML的复杂规范,使得服务之间的交互既简洁又高效。
对于微服务架构,目前并没有一个统一的定义。不同的人对它有不同的理解,也产生了不同的描述方式。一般来说,微服务架构风格把应用划分成若干个服务。每个服务有自己独立的进程。服务之间通过轻量级传输机制进行交互。这个表述中涵盖了微服务架构的一些重要特征。事实上,我们关注更多的并不是微服务架构的定义,而是它所具备的特征。这些特征可以帮助我们确定是否应该选用微服务架构。下面是微服务架构的一些重要特征。
微服务架构使用服务作为基本的单元。每个服务运行在独立的进程中,只能通过服务开放的API来进行访问。服务使用API规范来描述其对外的公开接口。服务的内部实现对外并不可见。服务交互时使用的是类似REST或gRPC这样的轻量级通信方式。每个服务可以独立部署和更新。在图1-1中,外部访问者通过API网关来访问不同微服务提供的API。微服务之间以不同的通信协议来交互,包括REST和gRPC。
从项目管理的方面来说,微服务架构的开发团队围绕业务场景来组织。每个服务与特定的业务功能相对应。单个服务的开发团队的规模较小,包含了开发、测试和运维相关的全部人员。每个服务的团队可以自主选择最适合的技术栈。开发团队不但负责开发和测试,还需要对服务进行维护。较小的开发团队意味着更少的沟通成本和更高的开发效率。
·图1-1 微服务架构中的服务调用
在数据存储方面,微服务架构中的服务一般有自己专用的数据存储。开发团队可以根据服务的需求来选择最适合的存储方式。关系型数据库和NoSQL数据库都可以使用。相对于传统的关系型数据库,NoSQL数据库可以极大地降低特定类型服务的存储实现的难度。比如,与社交网络相关的数据,使用Neo4J这样的图形数据库,可以在对象模型和存储模型之间进行直接的映射,不仅实现简单,而且性能更好。
使用了微服务架构之后,应用以服务为单元进行扩展。更小的扩展单元意味着更强的灵活性,以及对资源更合理的分配。对于一个应用来说,不同的业务功能所要处理的负载存在很大的差异。每个微服务在运行时的实例数量也各不相同,并且可以随着负载的变化而动态调整。
在图1-2中,外部的边框是进程的边界,不同的形状表示不同的单元。图1-2上方表示的是单体应用,所有单元在同一个进程的边界内。在进行扩展时,单体应用只能整体扩展。右侧是扩展之后的状态。每一个立方体表示一台主机。单体应用的扩展方式是整体的复制。一般的做法是每台主机上部署一个独立的应用。
图1-2下方表示的是微服务架构的应用。不同的形状表示不同的微服务,这些微服务都在各自的进程边界内。在进行扩展时,每个微服务的实例数量不尽相同,根据负载来确定。同一主机上可以运行不同微服务的进程。
·图1-2 微服务架构的扩展方式
微服务架构的目标是解决在线服务开发中所面临的挑战。微服务架构虽然解决了单体应用的扩展性问题,但是也带来了新的问题。应用自身的复杂度并没有消失,而是以新的形式出现。把应用划分成多个服务之后,单个服务的开发复杂度降低了,而复杂度则被转移到了服务之间的交互上。使用微服务架构之后,应用变成了一个分布式系统。服务之间通过进程间的通信方式来交互。当希望调用一个微服务的API时,首先需要通过服务发现机制找到该API的实际访问地址,再根据通信协议的要求来发送请求给目标服务的某个实例。被调用的微服务有可能处于离线状态,或者因为负载过大而延迟过高。这就要求调用方能够处理可能出现的不同错误情况,确保系统的健壮性。
服务之间交互的另外一种形式体现在数据上。每个服务可能使用不同的数据存储方式,并且只保存当前服务相关的数据。服务之间的数据需要保证一致性。在单体应用中,由于通常使用的是单一的关系型数据存储,可以使用数据库事务的ACID特性来保证数据的一致性。在微服务架构中,由于可能涉及多个类型不同的数据库,一般的选择是保证数据的最终一致性。最终一致性允许在某个时间点上出现数据不一致的情况。保证数据的一致性是微服务设计和实现中的重要一环。
微服务架构的另外一个复杂性来自于系统运维,包括服务的构建、部署和监控。每个微服务需要被独立进行部署,包括微服务自身和所依赖的支撑服务。由于微服务团队可以自主选择技术栈,所使用的支撑服务的类型也各不相同。所有这些服务和第三方应用都需要进行相应的安装、配置和更新。对于大量的安装配置工作,自动化是必然的选择。持续集成和持续部署是实现微服务架构时不可或缺的一部分。
1.1.3 云原生与微服务架构
在介绍了微服务架构的基本概念之后,下一步需要考虑的是如何实现微服务架构。微服务架构与生俱来的复杂度是开发每个微服务架构的应用时绕不开的难题。在实现微服务架构时,当然希望把全部的精力集中在实现有价值的业务逻辑上,而不是处理微服务架构自身的问题。这就意味着需要选择能够帮助应对这些复杂性挑战的平台和工具。对这些复杂性应对能力的高低,也成为挑选平台和工具的衡量标准。
在Java平台上,目前最流行的选择是Spring Cloud。Spring Cloud是多个开源项目组成的开发套件,用来实现分布式系统中的常见模式,如配置管理、服务发现和熔断器等。Spring Cloud可以用来实现微服务架构的应用。Spring Cloud的优势在于提供了一个抽象框架,可以避免供应商锁定的问题。对于同一个模式,可以自由地切换底层的实现方式,比如Netflix或阿里巴巴的实现。Spring Cloud使用Spring框架开发。对于一直工作在Spring框架上的团队来说,Spring Cloud是一个不错的选择。
相对于Spring Cloud来说,基于Kubernetes的云原生(Cloud Native)技术是一个更好的选择。顾名思义,云原生技术的概念由云和原生两个部分组成。“云”指的是云平台,负责管理计算资源;“原生”指的是应用专门面向云平台进行设计与实现,与云平台进行深度绑定,从而充分利用云平台提供的功能。
在软件开发中,原生通常代表着高效和难以迁移。原生应用针对特定平台而设计,可以充分利用底层平台的特性,因此运行起来非常高效。也因为这个原因,原生应用与特定平台存在紧密的耦合,很难迁移到其他平台。云原生应用同样具有这两个特征。
云原生微服务架构是云原生技术和微服务架构的结合。微服务作为一种架构风格,所解决的问题是复杂软件系统的架构与设计。云原生技术作为一种实现方式,所解决的问题是复杂软件系统的运行和维护。这两者所要解决的问题相辅相成。微服务架构可以选择不同的实现方式,如Spring Cloud或私有实现,并不一定非要使用云原生技术。同样的,云原生技术可以用来实现不同架构的应用,包括微服务架构的应用或是单体应用。
云原生技术和微服务架构是非常适合的组合。这其中的原因在于,云原生技术可以有效地解决微服务架构所带来的实现上的复杂度。微服务架构难以落地的一个突出原因是它过于复杂,对开发团队的组织管理方式、技术水平和运维能力都提出了极高的要求。一直以来只有少数技术实力雄厚的大企业会采用微服务架构。云原生技术可以弥补微服务架构的这一个短板,极大地降低微服务架构实现的复杂度,使得广大的中小企业也可以在实践中应用微服务架构。
1.1.4 云原生的发展趋势
云原生技术从出现以来,一直在快速地发展。总体来说,云原生技术的发展有如下几个方面的趋势。
云原生技术的第一个发展趋势是标准化和规范化。云原生技术的基础是容器化和容器编排技术,最常用的技术是Docker和Kubernetes。随着云原生技术的发展,在CNCF[1]和Linux基金会等组织的促进下,云原生技术的标准化和规范化工作正在不断地推进,其目的是促进技术的发展和避免供应商锁定的问题。这对于整个云原生技术生态系统的健康发展至关重要。目前已有的标准和规范包括开放容器倡议(Open Container Initiative)提出的容器镜像规范和运行时规范,以及CNCF中的一些项目。
云原生技术的第二个发展趋势是平台化,以服务网格(Service Mesh)技术为代表。这一趋势的出发点是增强云平台的能力,从而降低开发和运维的复杂度。流量控制、身份认证和访问控制、性能指标数据收集、分布式服务追踪和集中式日志管理等功能都可以由底层平台来提供,这极大地降低了中小企业在运行和维护云原生应用时的复杂度。从另外一个方面来说,这也促进了相关的开源软件和商业解决方案的发展。不仅有Istio和Linkerd这样流行的开源服务网格实现,也有越来越多的公司提供商用的支持。这为不同技术水平的企业提供了多样的选择。
云原生技术的第三个发展趋势是应用管理技术的进步,以操作员(Operator)模式[2]为代表。在Kubernetes平台上部署和更新应用时,传统的基于资源声明YAML文件的做法,已经逐步被Helm所替代。操作员模式在Helm的基础上更进一步,以更高效、自动化和可扩展的方式对应用部署进行管理。CNCF中的孵化项目Operator Framework是创建Operator的框架。Opera-torHub则是社区共享Operator实现的平台。
云原生技术的第四个发展趋势是开源实现的流行,以CNCF的开源项目为代表。云原生技术所涉及的技术点众多。每一个技术点都需要相应的实现。CNCF组织了很多开源项目,覆盖众多的技术点。CNCF的项目通常作为相关技术点的推荐实现。CNCF中的流行项目包括Helm、Jaeger、Envoy、etcd、Kubernetes、Prometheus、gRPC、Fluentd和Harbor等。
最后一个发展趋势与云原生应用的开发相关。社区中出现了越来越多的微服务开发框架,不同的编程语言都有相应的开源实现。以Java平台为例,Eclipse基金会的MicroProfile提出了微服务开发的规范,而Quarkus、Micronaut和Helidon都是新兴的微服务开发框架。这些框架对微服务开发进行了针对性的优化,尽可能地降低应用的资源消耗,并提升启动速度。通过GraalVM的支持,可以创建出启动速度快、耗费资源更少、体积小的Java微服务的原生可执行文件。可以预期的是,所有的Java微服务框架都会提供GraalVM的支持。