原文链接:https://www.nginx.com/blog/introduction-to-microservices/ 现在,微服务在社交媒体(文章、博客、论坛)和会议报告中得到了广泛的关注。它们正在快速增长到Gartner Hype Cycle中膨胀期望的高峰(Peak of Inflated Expectations)。与此同时,软件界的怀疑论者认为微服务并不是什么新鲜事。反对者声称这个想法只是对SOA的重塑。然而,尽管大肆宣传和怀疑,微服务架构模式具有显着的优势,特别是在: 敏捷开发 企业复杂应用程序交付 这篇博客文章是关于设计,构建和部署微服务的七部分系列文章中的第一篇。 将了解微服务架构模式以及它与更传统的单体架构模式的比较。本系列将介绍微服务架构的各种元素和优缺点,它是否对项目有意义,以及如何应用它。 我们先来看看为什么要考虑使用微服务。 构建单体应用 让我们假设要开始建立一个全新的出租车应用程序,旨在与Uber和Hailo竞争。 在一些初步会议和需求收集之后,可以手动创建一个新项目,也可以使用Rails,Spring Boot,Play或Maven附带的生成器创建一个新项目。 这个新的应用程序将具有模块化的六边形体系结构,如下图所示: 应用程序的中心是业务逻辑,它由定义服务、域对象和事件的模块共同组成,周围是外部接口的适配器,包括: 数据库访问组件 生成和使用消息的消息传递组件 暴露API或实现UI的Web组件 尽管逻辑上具有模块化体系结构,但整个应用程序仍然作为一个整体进行打包和部署,打包后最终的格式取决于应用程序的开发语言和框架。例如: 许多Java应用程序打包为WAR文件,并部署在应用程序服务器(如Tomcat或Jetty)上 其他Java应用程序打包为自包含的可执行JAR文件 Rails和Node.js应用程序打包为目录层次结构 以这种方式编写的应用程序非常常见。 它们很容易开发,因为我们的IDE和其他工具专注于构建单体应用。 这些类型的应用程序也很容易测试。只需启动应用程序并使用Selenium测试UI即可实现端到端测试。 单体应用也易于部署。只需将打包的应用程序复制到服务器即可。还可以通过在负载均衡器后面运行多个副本来扩展应用程序。 在项目的早期阶段,它运作良好。 走向单体应用的地狱 开发难题 不幸的是,这种简单的方法有很大的局限性。成功的应用程序都会随着时间的推移而增长并最终变得非常巨大。在每个开发周期内,开发团队会实现很多功能,这意味着添加许多代码行。几年之后,小型简单的单体应用将成长为一个巨大的臃肿程序。 举一个极端的例子,我最近采访了一位开发人员,他正在编写一个工具来分析其数百万行代码(line of code, LOC)的应用程序中数千个JAR之间的依赖关系。我相信多年来大量开发人员的共同努力才能创造出这样的野兽。 一旦应用程序成为一个庞大而复杂的巨型组件,开发团队很可能处于一个痛苦的世界,敏捷开发和交付的任何尝试都将陷入困境。一个主要问题是应用程序非常复杂。它对于任何一个开发人员来说都太大了,无法完全理解。因此,正确修复错误和实现新功能变得困难且耗时。更重要的是,这往往是一个向下螺旋,如果代码库难以理解,则无法正确进行更改,最终会得到一个巨大的,难以理解的大泥球。应用程序的庞大规模也将减缓开发速度。应用程序越大,启动时间越长。 在最近的一项调查中,一些开发人员报告启动时间长达12分钟,也听过应用程序长达40分钟的启动时间。如果开发人员经常不得不重新启动应用程序服务器,那么他们当天的大部分时间都将花在等待上,他们的工作效率会受到影响。 持续集成难题 大型,复杂的单体应用的另一个问题是它成为了持续部署的障碍。如今,SaaS应用程序的最新技术是每天多次将更改推向生产。由于必须重新部署整个应用程序以更新它的任何一部分,因此在单体应用上使用持续部署非常困难。还有上面提到的超长的启动时间也是问题。此外,通常我们都不太了解变更将会产生的影响,因此可能需要进行大量的手动测试。所以,几乎不可能进行持续部署。 当不同模块具有相互冲突的资源需求时,单体应用也难以扩展。 例如: 一个模块可能实现CPU密集型图像处理逻辑,理想情况下将部署在Amazon EC2计算优化实例中。 另一个模块可能是内存数据库,最适合Amazon EC2内存优化实例。 但是,由于这些模块一起部署,所以必须在硬件选择上妥协。 可靠性难题 单体应用的另一个问题是可靠性。由于所有模块都在同一进程中运行,因此任何模块中的错误(例如内存泄漏)都可能会导致整个进程失败。此外,由于应用程序的所有实例都相同,因此该错误将影响整个应用程序的可用性。 最后,单体应用使得采用新框架和语言变得极其困难。因此,采用新技术存在巨大障碍。所以,在项目开始时所做的任何技术选择都很重要。 例如,假设使用XYZ框架编写了200万行代码。重写整个应用程序以使用更新的ABC框架是非常昂贵的(在时间和成本上),即使该框架相当好。 总而言之:拥有一个成功的业务关键型应用程序,该应用程序已经发展成为一个巨大的单体应用,开发人员很少了解它。它是使用过时的,非生产性的技术编写的,这使得招聘有才能的开发人员变得困难。该应用程序难以扩展且不可靠。因此,敏捷开发和应用程序的交付是不可能的。 所以你对此能做些什么? 微服务-解决复杂性 许多组织,如亚马逊,eBay和Netflix,通过采用现在称为微服务架构模式解决了这个问题。而不是构建一个可怕的单体应用,我们的想法是将的应用程序拆分为一组较小的,互连的服务。 服务通常实现一组不同的特性或功能,例如订单管理,客户管理等。每个微服务都是一个迷你应用程序,它有自己的六边形体系结构,包括业务逻辑和各种适配器。一些微服务会暴露其他微服务或应用程序客户端使用的API,其他微服务可能会实现Web UI。在运行时,每个实例通常是云VM或Docker容器。 例如,前面描述的系统的可能分解如下图所示: 现在,应用程序的每个功能模块都由自己的微服务实现。此外,Web应用程序被分成一组更简单的Web应用程序(例如一个用于乘客,一个出租车驾驶员)。这样可以更轻松地为特定用户,设备或专用用例部署不同的体验。 每个后端服务都公开一个REST API,大多数服务都使用其他服务提供的API(例如,驾驶员管理模块使用通知服务器告知空闲的司机有一个潜在的行程订单)。UI服务调用其他服务以呈现网页。服务还可以使用基于消息的异步通信。本系列后面将详细介绍服务间通信。 一些REST API也暴露给司机和乘客使用的移动应用程序。但是,应用程序不能直接访问后端服务,而是由称为API网关的中介调解来进行通信。API网关负责负载均衡、缓存、访问控制、API计量和监控等任务,可以使用NGINX来部署API网关。本系列的后续文章将介绍API网关。 微服务架构模式对应于Scale Cube的Y轴,Scale Cube是一个可扩展性的3D模型,来自《可扩展性艺术》这本书。另外两个坐标轴是: X轴:在负载均衡器后面运行多个相同的应用程序副本 Z轴(或称为数据分区):请求的属性(例如,一行的主键或一个客户的身份)用于将请求路由到特定服务 应用程序通常一起使用这三种类型的坐标轴: Y轴将应用程序分解为微服务,如本节第一张图所示 在运行时,X轴在负载均衡器后面运行每个服务的多个实例,以实现吞吐量和可用性 某些应用程序也可能使用Z轴来对服务进行分区 下图显示了如何在Amazon EC2上通过Docker部署行程管理(Trip Management)服务。 在运行时,行程管理服务由多个服务实例组成,每个服务实例都是Docker容器。容器在云上的多个VM中运行,以提高可用性。在服务实例的前面是一个负载均衡器,例如NGINX,它跨服务实例分配请求。负载均衡器还可能处理其他问题,例如缓存,访问控制,API计量和监控。 微服务架构模式显着的影响应用和数据库之间的关系。每个服务都有自己的数据库模式(database schema),而不是与其他服务共享单个数据库模式(database schema)。这种方法与企业范围数据模型的想法不一致,并且,这样经常导致一些数据的重复。但是,如果希望从微服务中受益,那么每个服务拥有一个数据库模式(database schema)是必不可少的,因为它可以确保松散耦合。下图显示了示例应用程序的数据库体系结构。 每个服务都有自己的数据库,服务可以使用最适合其需求的数据库类型,即所谓的多语言持久性体系结构。例如,驾驶员管理系统(用于发现靠近潜在乘客的驾驶员)必须使用支持高效地理查询的数据库。 从表面上看,微服务架构模式与SOA类似。这两种方法中体系结构都由一组服务组成。但是,微服务架构模式相对于面向服务架构(Service-Oriented Architecture,SOA),它没有Web服务规范(web service specifications,WS-*)的商业化和感知包,也没有企业服务总线(Enterprise Service Bus,ESB)。基于微服务的应用程序支持更简单,轻量级的协议,如REST,而不是WS-*。他们也极力避免使用ESB,而是在微服务本身中实现类似ESB的功能。微服务架构模式也拒绝SOA的其他部分(例如,canonical schema)。 微服务的优点 微服务架构模式有许多重要的好处。 解决复杂性问题。它将原本可能是一个巨大的单体应用分解为一组服务。 虽然功能总量不变,但应用程序已分解为可管理的模块或服务。每个服务都以RPC或消息驱动的API的形式明确定义边界。微服务架构模式强制实现一定程度的模块化,实际上使用单一代码库非常难以符合要求。而且,独立的服务能够更速度的开发,而且更容易理解和维护。 每个服务都能够独立开发。只要服务符合API规范,开发人员可以自由选择任何有意义的技术。 当然,大多数组织都希望避免完全无政府状态并限制技术选择。这种自由意味着开发人员在开始新项目可以避免使用可能过时的技术。在编写新服务时,他们可以选择使用当前流行的技术。此外,由于服务相对较小,因此使用当前新技术重写旧服务变得可行。 每个服务能够独立部署。开发人员永远不需要因为环境的变化而协调服务的部署。 一旦测试完成,就可以部署这些类型的更改。例如,UI团队可以执行A/B测试并快速迭代UI变更。微服务架构模式使得持续部署成为可能。 每个服务能够独立扩展。可以仅部署满足其容量和可用性约束的每个服务的实例数。可以使用最符合服务资源要求的硬件。 例如,可以在EC2计算优化实例上部署CPU密集型图像处理服务,在EC2内存优化实例上部署内存数据库服务。 微服务的缺点 正如弗雷德布鲁克斯(Fred Brooks )30年前说的那样,没有技术银弹,与其他所有技术一样,微服务架构也存在缺陷。 微服务架构模式名称本身就是一个缺点。微服务一词过分强调服务规模。 事实上,有些开发人员主张构建极其细粒度的10-100 LOC服务。虽然小型服务更可取,但重要的是要记住它们是达到目的的手段,而不是目标本身。 微服务的目标是充分分解应用程序,以促进敏捷应用程序的开发和部署。 由于微服务应用本身是分布式系统所带来的复杂性。 开发人员需要选择并实现基于RPC或消息驱动的进程间通信机制。 还必须编写代码来处理部分失败,因为请求的目标可能很慢或不可用。虽然这不是造火箭,但它比单体应用复杂得多,在单体应用中,模块通过语言级方法/过程调用相互通信。 微服务架构模式的另一个挑战是分区数据库架构。 更新多个业务实体的业务事务相当常见。在单个数据库的单体应用中实现这些类型的事务是微不足道的。但是,在基于微服务的应用程序中,需要更新由不同服务所拥有的多个数据库。 通常不会选择使用分布式事务,不仅仅是因为CAP定理,更因为它们根本不受当前许多高度可扩展的NoSQL数据库和消息传递代理组件的支持。最终必须使用基于最终一致性的方法,这对开发人员来说更具挑战性。 测试微服务更复杂。 例如,使用诸如Spring Boot之类的现代框架,编写一个启动单体Web应用程序并测试其REST API的测试类是很简单的。相反,微服务的类似测试类需要启动该服务及其依赖的任何服务(或至少为这些服务配置存根)。虽然这不是造火箭,但不要低估这样做的复杂性。 微服务架构模式的另一个挑战是实现跨多个服务的升级回滚。例如,假设正在实现一个需要同时变更服务A,服务B和服务C的功能,其中A依赖于B而B依赖于C: 在单体应用中,只需更改相应的模块,集成更改,并一次性部署它们。 在微服务架构模式中,需要仔细规划和协调每个服务的变更顺序。例如,需要按照C->B->A的顺序依次更新服务。幸运的是,大多数更改通常只影响一个服务,而需要协调的多服务更改相对较少。 部署微服务更复杂。 单体应用只是部署在传统负载均衡器后面的一组相同服务器上。每个应用程序实例都配置有基础结构服务的位置(主机和端口),例如,数据库和消息代理组件。 微服务通常由大量服务组成,例如,根据Adrian Cockcroft的说法,Hailo有160种不同的服务,Netflix有超过600种服务。 每个服务都有多个运行时实例,这些都可迁移变动的部分,需要被配置,部署,扩展和监控。 还需要实现一种服务发现机制,该机制使服务能够发现它需要通信的任何其他服务的位置(主机和端口)。传统的故障工单和手动的操作方法无法适用这样的复杂程度。因此,成功部署微服务需要开发人员更好地控制部署方法,并且实现高度自动化。 一种自动化方法是使用现成的PaaS(例如,Cloud Foundry)。PaaS为开发人员提供了一种部署和管理微服务的简便方法,这使他们免受诸如采购和配置IT资源等问题的困扰。同时,配置PaaS系统和网络的专业人员可以确保遵守最佳实践和公司策略。 自动部署微服务的另一种方法是开发私有PaaS。一个典型的方法是使用集群解决方案,如Kubernetes,以及Docker等技术。在本系列的后面部分,我们将介绍基于软件的应用程序交付方法(如,NGINX Plus,它可以轻松处理缓存,访问控制,API计量和微服务级别的监控),这可以帮助解决这个问题。 总结 构建复杂的应用程序本身就很困难,单体应用仅适用于简单轻量级的应用程序。如果将它用于复杂的应用程序,将陷入无尽的痛苦。 尽管存在缺点和实施挑战,但微服务架构模式是复杂的不断变化发展的应用程序的更好选择。在后面的博客文章中,我将深入探讨微服务架构模式的各个方面的细节,并讨论诸如服务发现,服务部署选项以及将单体应用重构为服务的策略等主题。
原文链接 这是本系列的第二篇文章,将讨论使用API网关构建微服务。当选择将应用程序构建为一组微服务时,需要确定应用程序的客户端如何与微服务进行交互。 使用单体应用,只有一组(通常是复制的,负载均衡的)端点。 在微服务架构中,每个微服务都暴露出一组更细粒度的端点。 在本文中,我们将研究它如何影响客户端到应用程序的通信,并提出一种使用API网关的方法。 简介 让我们假设正在为购物应用程序开发原生移动客户端,需要实现商品详细信息页面,该页面显示有关给定产品的所有信息。 例如,下图显示了在亚马逊Android移动应用程序中滚动浏览商品详细信息时看到的内容。 即使这是智能手机应用程序,商品详细信息页面也会显示很多信息。例如,不仅有基本的产品信息(如名称,描述和价格),而且此页面还显示: 购物车中的商品数量 订单历史记录 客户评论 低库存警告 运输选项 各种建议,包括在购买此商品是经常一起购买的商品,以及购买此产品的客户查看的其他产品 替代购买选项 使用单体应用架构时,移动客户端将通过单个REST调用来检索此数据(GET api.company.com/productdetails/productId)到应用程序。负载均衡器将请求路由到N个相同的应用程序实例之一。然后,应用程序将查询各种数据库表并将响应返回给客户端。 当使用微服务架构时,商品详细信息页面上显示的数据由多个微服务拥有。以下是一些潜在的微服务,它们拥有示例商品详细信息页面上显示的数据: 购物车服务:购物车中的商品数量 订购服务:订单历史 目录服务:产品基本信息,例如名称,图片和价格 审核服务:客户评论 库存服务:低库存警告 送货服务:运输选项,截止日期、成本、运输提供商的API 推荐服务:推荐项目 我们需要决定移动客户端如何访问这些服务。 客户端与微服务直接通信 理论上,客户端可以直接向每个微服务发出请求。每个微服务都有一个公共端点(https://serviceName.api.company.name)。此URL将映射到微服务的负载均衡器,该负载均衡器跨可用实例分发请求。要检索商品详细信息,移动客户端将向上述每个微服务发出请求。 不幸的是,这种方式存在挑战和局限: 客户端的需求与每个微服务公开的细粒度API之间的不匹配。 此示例中的客户端必须发出七个单独的请求,在更复杂的应用程序中,它可能会发出更多的请求(例如,亚马逊描述了在渲染产品页面时如何涉及数百种服务)。虽然客户端可以通过局域网发出这么多请求,但是这在公共互联网上效率太低,而且在移动网络上肯定是行不通的。这种方法也使客户端代码更加复杂。 可能会使用非Web友好的协议: 一个服务可能使用Thrift二进制RPC 而另一个服务可能使用AMQP消息传递协议 这两种协议都不适合浏览器或防火墙,最好在内部网络中使用,应用程序应在防火墙之外使用HTTP和WebSocket等协议。 重构微服务变得困难。随着时间的推移,我们可能想要修改系统拆分为微服务的划分方式,重新定义微服务模块的边界。 例如,我们可能合并两个服务或将服务拆分为两个或更多服务。但是,如果客户端直接与服务通信,那么执行这种重构可能非常困难。 由于这些问题,客户直接与微服务通信变的没有意义。 使用API网关 通常,更好的方法是使用API网关。API网关是进入系统的单一入口点服务。它类似于面向对象设计的外观模式(Facade Pattern)。API网关封装了内部系统体系结构,并提供了为每个客户端量身定制的API。它可能还有其他职责,例如: 身份验证 监视 负载均衡 缓存 请求转换和管理 静态响应处理 下图显示了API网关通常如何适合该体系结构: API网关负责请求路由,组合和协议转换。 客户的所有请求首先通过API网关。然后它将请求路由到适当的微服务。API网关通常通过调用多个微服务并聚合结果来处理请求。它可以在Web协议(如HTTP和WebSocket)和内部使用的Web友好协议之间进行转换。 API网关为每个客户端提供自定义API,通常为移动客户端公开粗粒度API。 例如,展示商品详细信息的方案。API网关可以提供端点(/productdetails?productid=xxx),使端点能够通过单个请求检索所有商品详细信息。API网关通过调用各种服务(目录服务,推荐服务,审核服务等)来处理请求,并将结果组合在一起。 API网关示例Netflix API网关。 Netflix流媒体服务可用于数百种不同类型的设备,包括电视,机顶盒,智能手机,游戏系统,平板电脑等。最初,Netflix试图为其流媒体服务提供一种通用的API。然而,他们发现由于各种设备和它们的独特需求,通用API不能很好地工作。现在,他们使用API网关,通过运行特定设备的适配器代码为每个设备提供量身定制的API。通常,适配器平均调用六到七个后端服务来处理每一个请求。Netflix API Gateway每天处理数十亿个请求。 API网关的优势和缺点 优势:使用API网关封装了应用程序的内部结构。 客户端只需与网关通信,而不必调用特定服务。API网关为每种客户端提供特定的API。这减少了客户端和应用程序之间的往返次数,还简化了客户端代码。 缺点:API网关是另一个必须开发,部署和管理的高可用组件。 API网关也存在开发瓶颈的风险。开发人员必须更新API网关才能公开每个微服务的端点。重要的是,更新API网关的过程尽可能轻量级。否则,开发人员将被迫排队等待更新网关。尽管存在这些缺点,但对于大多数应用来说,使用API网关是很有意义的。 实现API网关 现在我们已经了解了使用API网关的动机和权衡,让我们来看看需要考虑的各种设计问题。 性能和可扩展性 虽然只有少数公司能够得到Netflix的运营规模,每天需要处理数十亿的请求。但是,对于大多数应用程序,API网关的性能和可伸缩性是非常重要。因此,需要在支持异步,非阻塞I/O的平台上构建API网关。 有多种不同的技术可用于实现可扩展的API网关: 在JVM上,可以使用基于NIO的框架(如Netty、Vertx、Spring Reactor或JBoss Undertow) 非JVM的主流选择是Node.js,基于Chrome的JavaScript引擎构建的平台 另一种选择是使用NGINX Plus NGINX Plus提供了一个成熟,可扩展,高性能的Web服务器和反向代理,可轻松部署,配置和编程。NGINX Plus可以管理身份验证,访问控制,负载平衡请求,缓存响应,并提供应用程序感知的运行状况检查和监视。 使用反应式编程模型 API网关通过简单地将它们路由到适当的后端服务来处理某些请求。它通过调用多个后端服务并聚合结果来处理其他请求。 对于某些请求(例如,显示商品详细信息的请求),对后端服务的请求彼此独立。为了最大限度地缩短响应时间,API网关应同时执行独立请求 有时请求之间存在依赖关系。在将请求路由到后端服务之前,API网关可能首先需要通过调用身份验证服务来验证请求,例如: 要获取有关客户愿望清单中商品的详细信息 API网关必须首先检索包含该商品信息的客户信息文件 然后检索每个商品的信息 另一个有趣的API组合示例是Netflix Video Grid。 使用传统的异步回调方法编写API组合代码很快就会导致回调地狱。代码将纠结、难以理解,并且容易出错。更好的方法是以声明式风格使用反应式编程模型编写API网关代码。反应式编程的抽象概念示例,包括: Scala中的Future Java 8中的CompletableFuture:Netflix为JVM创建了RxJava,专门用于其API网关 JavaScript中的Promise:用于JavaScript的RxJS,它可以在浏览器和Node.js中运行 Reactive Extensions(也称为Rx或ReactiveX),最初由Microsoft为.NET平台开发 使用反应式编程模型将能够编写简单而有效的API网关代码。 服务调用 基于微服务的应用程序是一个分布式系统,必须使用进程间通信机制。进程间通信有两种类型: 基于消息传递的异步机制。某些实现使用消息代理(例如JMS或AMQP)。如Zeromq,是无代理的,服务直接沟通。 HTTP或Thrift等同步机制。系统通常同时使用异步和同步两种风格,甚至可能使用每种风格的多个实现。因此,API网关将需要支持各种通信机制。 服务发现 API网关需要知道与之通信的每个微服务的位置(IP地址和端口): 在传统的应用程序中,可能需要硬连接来定位服务位置 在现代的基于云的微服务应用程序中,这也是一个非常重要的问题 基础结构服务(例如,消息代理)通常具有静态位置,可以通过OS环境变量指定。但是,确定应用程序服务的位置并不容易,应用服务具有动态分配的位置。由于自动扩展和升级,服务实例集会动态更改。因此,与网络中的任何其他服务客户端一样,API网关需要使用系统的服务发现机制:服务器端发现或客户端发现。 稍后的文章将更详细地描述服务发现。现在,值得注意的是,如果系统使用客户端发现,那么API网关必须能够查询服务注册表(Service Registry),它是所有微服务实例及其位置的数据库。 处理部分失败 实现API网关时必须解决的另一个问题是部分失败的问题。一个服务调用另一个响应缓慢或不可用的服务,所有分布式系统都会出现此问题。API网关永远不应无限期地阻塞等待下游服务,如何处理故障取决于具体方案和哪个服务失败。 如果推荐服务在商品详细信息方案中没有响应,则API网关应将剩余的商品详细信息返回给客户端,因为它们对用户仍然有用。推荐项目可以是空的,也可以替换为Top10的商品名单。 如果目录服务没有响应,那么API网关应该向客户端返回错误,或者,API网关也可以返回缓存的数据(例如,由于商品价格不会经常变化,API网关可以返回缓存的定价数据)。 数据可以由API网关本身缓存,也可以存储在外部缓存中,例如Redis或Memcached。通过返回默认数据或缓存数据,API网关可确保系统故障不会影响用户体验。 Netflix Hystrix是一个非常有用的库,用于编写调用远程服务的代码。 Hystrix的超时调用次数可以设定阈值,它实现了断路器模式,阻止客户端不必要地等待无响应的服务。 如果服务的错误率超过指定的阈值,Hystrix将使断路器跳闸,并且所有请求将在指定的时间段内立即失败 Hystrix允许在请求失败时定义回退操作(例如,从缓存读取或返回默认值) 如果使用的是JVM,那么一定要考虑使用Hystrix 如果在非JVM环境中运行,则应使用其他等效的库 总结 对于大多数基于微服务的应用程序,实现API网关是有意义的,它充当系统的单一入口点。 API网关负责请求路由,组合和协议转换,它为每个应用程序的客户端提供自定义API。API网关还可以通过返回缓存或默认数据来屏蔽后端服务中的故障。在本系列的下一篇文章中,我们将介绍服务之间的通信。
原文链接:https://www.nginx.com/blog/building-microservices-inter-process-communication/ 这是我们系列中关于使用微服务架构构建应用程序的第三篇文章。 第一篇文章介绍了微服务架构模式,将其与单体应用架构模式进行了比较,并讨论了使用微服务的优缺点。 第二篇文章描述了应用程序的客户端如何通过称为API网关的中介与微服务进行通信。 在本文中,我们将了解系统中的服务如何相互通信。 第四篇文章将探讨服务发现密切相关问题。 介绍 在单体应用中,组件通过语言级方法或函数调用相互通信。相比之下,基于微服务的应用程序是在多台机器上运行的分布式系统,每个服务实例通常都是一个进程。因此,如下图所示,服务必须使用进程间通信(IPC)机制进行交互。 稍后我们将介绍具体的IPC技术,但首先让我们探讨各种设计问题。 交互模式 在为服务选择IPC机制时,首先应考虑服务如何交互。有各种client⇔service交互模式。它们可以按两个维度进行分类: 第一个维度:交互是一对一 or 一对多: 一对一:每个客户端请求只由一个服务实例处理 一对多:每个客户端请求都由多个服务实例处理 第二个维度:交互是同步 or 异步: 同步:客户端期望服务端及时响应,甚至可能在等待时阻塞 异步:客户端在等待响应时不会阻塞,并且响应(如果有)不一定立即发送 下表显示了各种交互方式: 一对一 一对多 同步 请求/响应 - 异步 通知 发布/订阅 异步 请求/异步响应 发布/异步响应 存在以下类型的一对一交互: (同步)请求/响应:客户端向服务发出请求并等待响应,客户端希望响应及时到达。在基于线程的应用程序中,发出请求的线程甚至可能在等待时阻塞 (异步)通知(例如,单向请求):客户端向服务发送请求但不等待回复 (异步)请求/异步响应:客户端向服务发送请求,该服务以异步方式进行回复,客户端在等待期间不会阻塞,并且假设响应可能不会在一段时间内到达 存在以下类型的一对多交互: (异步)发布/订阅:客户端发布通知消息,该消息由零个或更多感兴趣的服务使用 (异步)发布/异步响应:客户端发布请求消息,然后等待一定时间以获取感兴趣的服务的响应 每项服务通常使用这些交互方式的组合。对于某些服务,单个IPC机制就足够了。其他服务可能需要使用IPC机制的组合。下图显示了当乘客请求行程时,出租车应用程序中的服务可能如何交互。 这些服务使用: 通知:乘客的智能手机向行程管理服务发送通知以请求找到可乘坐的出租车 请求/响应:行程管理服务通过使用请求/响应来调用乘客管理服务来验证乘客的帐户是否有效 发布/订阅:行程管理服务创建行程并使用发布/订阅来通知其他服务,包括分派系统(Dispatcher,用于定位可用的驱动程序)。 现在我们已经了解了交互风格,让我们来看看如何定义API。 定义API 服务的API是服务与其客户端之间的契约。无论选择哪种IPC机制,使用某种接口定义语言(IDL)精确定义服务的API都很重要。使用API优先方法定义服务有很好的理论依据。可以通过编写接口定义并与客户端开发人员一起查看来开始开发服务。只有在对API定义进行迭代后才能开始实现该服务。预先进行此设计可增加构建满足客户端需求的服务的机会。 正如将在本文后面看到的,API定义的性质取决于使用的IPC机制: 如果使用消息传递,则API由消息通道和消息类型组成 如果使用HTTP,则API由URL以及请求和响应格式组成 稍后我们将更详细地描述一些IDL。 升级API 服务的API总是随着时间的推移而变化。 在单体应用中,通常可以直接更改API并更新所有调用者。 在微服务中,即使API的所有使用者都是同一应用程序中的其他服务,也要困难得多。 通常无法强制所有客户端与服务保持同步升级。此外,可能会逐步部署新版本的服务,以便同时运行旧版本和新版本的服务。制定处理这些问题的战略非常重要。 如何处理API的更改取决于这次API变更的大小。 某些更改是次要的,并且这些变更能够向后兼容。 例如,向请求或响应添加属性。此时,应该遵守稳健性原则来设计客户端和服务的API。保证使用旧版API的客户端能够继续访问新版本的服务。该服务为缺少的请求属性提供默认值,客户端忽略任何额外的响应属性。 使用IPC机制和消息传递格式,使得我们可以轻松的升级API。 有时必须对API进行主要的,不兼容的更改。 由于无法强制客户端立即升级,因此服务必须在一段时间内支持旧版本的API。如果使用的是基于HTTP的机制(如REST),则一种方法是将版本号嵌入URL中。每个服务实例可能同时处理多个版本。或者,可以部署各自处理特定版本的不同实例。 处理部分失败 正如前一篇关于API网关的文章所述,在分布式系统中,存在部分失败的风险。由于客户端和服务是单独的进程,因此服务可能无法及时响应客户端的请求: 由于故障或维护,服务可能会关闭 服务可能过载并且对请求的响应非常缓慢 例如,前一篇文章中的商品详细信息方案。假设推荐服务没有响应,客户端的简单实现可能会无限期地阻止响应。这不仅会导致糟糕的用户体验,而且在许多应用程序中它会消耗诸如线程之类的宝贵资源。最终运行时将耗尽线程并变得无响应,如下图所示。 要防止出现此问题,必须设计服务以处理部分故障。 Netflix描述了一个很好的方法。处理部分失败的策略包括: 网络超时:永远不会无限期阻塞,并在等待响应时始终使用超时。使用超时可确保资源永远无限期地捆绑在一起。 限制未完成请求的数量:对客户端可以对特定服务进行的未完成请求数施加上限。如果已达到限制,则发出其他请求可能毫无意义,并且这些尝试需要立即失败。 断路器模式:跟踪成功和失败请求的数量。如果错误率超过配置的阈值,使断路器跳闸,以便进一步的尝试立即失败。如果大量请求失败,则表明该服务不可用,并且发送请求毫无意义。超时后,客户端应再次尝试,如果成功,请关闭断路器。 提供回退:在请求失败时执行回退逻辑。例如,返回缓存数据或默认值,例如空集推荐。 Netflix Hystrix是一个开源库,可以实现上述和其他模式。如果使用的是JVM,那么一定要考虑使用Hystrix。而且,如果在非JVM环境中运行,则应使用等效库。 IPC技术 有许多不同的IPC技术可供选择。 服务可以使用基于HTTP的REST或Thrift等基于请求/响应的同步通信机制 可以使用异步的,基于消息的通信机制,例如AMQP或STOMP,还有各种不同的消息格式 服务可以使用人类可读的基于文本的格式,例如JSON或XML 可以使用二进制格式(更有效),例如Avro或Protocol Buffers 稍后我们将研究同步IPC机制,但首先让我们讨论异步IPC机制。 异步,基于消息的通信使用消息传递时 进程通过异步交换消息进行通信。客户端通过向服务发送消息来向服务发出请求。 如果希望服务回复,则通过向客户端发送单独的消息来实现。由于通信是异步的,因此客户端不会阻塞等待回复 否则,客户端会认为回复不会立即收到。 消息由消息头(元数据,如发件人)和消息体组成,并且,消息是通过通道交换。任何数量的生产者都可以向通道发送消息,同样,任何数量的消费者都可以从通道接收消息。有两种渠道: 点对点:点对点通道向正在从通道读取的一个消费者传递消息。服务使用点对点通道来实现前面描述的一对一交互样式 发布/订阅:通道将每条消息传递给所有订阅的消费者。服务使用发布/订阅通道来实现上述的一对多交互样式 下图显示出租车应用程序如何使用发布/订阅通道。 行程管理服务通过将生产的消息写入发布/订阅通道来向感兴趣的服务(如分派系统Dispatcher)通知新的行程。分派系统通过向发布/订阅通道写入驱动响应消息来查找可用的驱动程序并通知其他服务。 有许多消息系统可供选择,应该选择一个支持各种编程语言的: 某些消息传递系统支持标准协议,如AMQP和STOMP 其他消息系统具有专用(有参考文档)的协议。有大量的开源消息系统可供选择,包括: RabbitMQ Apache Kafka Apache ActiveMQ NSQ 在较高的层面上,它们都支持某种形式的消息和通道,它们都力求稳定,高性能和可扩展。但是,每个消息代理的消息传递模型的细节存在显着差异。 使用消息传递有许多优点 将客户端与服务分离:客户端只需向适当的通道发送消息即可发出请求。客户端完全不知道服务实例。它不需要使用发现机制来确定服务实例的位置。 消息缓冲: 使用同步请求/响应协议(如HTTP),客户端和服务都必须在交换期间可用。 消息代理将写入通道的消息排队,直到消费者可以处理它们。例如,即使订单处理系统缓慢或不可用,在线商店也可以接受来自客户的订单,订单消息只是在排队。 灵活的客户端-服务交互:消息通知支持前面描述的所有交互方式。 显式的进程间通信:基于RPC的机制尝试使调用远程服务看起来与调用本地服务相同。然而,由于物理定律和部分失效的可能性,它们实际上是完全不同的。消息传递使得这些差异非常明确,因此开发人员不会陷入虚假的安全感。 使用消息传递还有一些缺点 额外的操作复杂性:消息传递系统是另一个必须安装,配置和操作的系统组件。消息代理必须具有高可用性,否则会影响系统可靠性。 实现基于请求/响应的交互的复杂性:请求/响应式交互需要一些工作来实现。每个请求消息必须包含回复信道标识符和相关标识符。该服务将包含相关ID的响应消息写入回复通道。客户端使用相关ID将响应与请求进行匹配。使用直接支持请求/响应的IPC机制通常更容易。 现在我们已经考虑使用基于消息传递的IPC,让我们查看基于请求/响应的IPC。 同步,请求/响应IPC 当使用基于请求/响应的同步IPC机制时,客户端向服务发送请求。该服务处理请求并发回响应。在许多客户端中,使请求在等待响应时阻塞的线程。其他客户端可能使用由Futures或Rx Observables封装的异步,事件驱动的客户端代码。但是,与使用消息传递时不同,客户端假定响应将及时到达。有许多协议可供选择。两种流行的协议是REST和Thrift。我们先来看看REST。 REST 今天,以RESTful风格开发API是时髦的。REST是一种(几乎总是)使用HTTP的IPC机制。 REST中的一个关键概念是资源,它通常表示业务对象,如客户或商品,或业务对象的集合。 REST使用HTTP谓词来操作资源,这些资源使用URL引用。例如: GET请求返回资源的表示形式,该形式可能是XML文档或JSON对象的形式。 POST请求创建新资源,PUT请求更新资源。 引用REST的创建者Roy Fielding:"REST provides a set of architectural constraints that, when applied as a whole, emphasizes scalability of component interactions, generality of interfaces, independent deployment of components, and intermediary components to reduce interaction latency, enforce security, and encapsulate legacy systems.”---《Architectural Styles and the Design of Network-based Software Architectures》 REST提供了一组架构约束,当作为一个整体应用时,强调组件交互的可扩展性,接口的通用性,组件的独立部署以及中间组件,以减少交互延迟,实施安全性并封装遗留系统。 下图显示了出租车应用程序可能使用REST的方法之一。 乘客的智能手机通过向行程管理服务的/trip资源发出POST请求来发起行程。该服务通过向乘客管理服务发送有关乘客信息的GET请求来处理请求。在验证乘客被授权创建行程后,行程管理服务创建行程并向智能手机返回201响应。 许多开发人员声称他们的基于HTTP的API是RESTful的。然而,正如菲尔丁在这篇博文中所描述的那样,并非所有这些都是。 Leonard Richardson(无关系)定义了一个非常有用的REST成熟度模型,它包含以下级别。 级别0:API的客户端通过向其唯一的URL端点发出HTTP POST请求来调用该服务。每个请求指定要执行的操作,操作的目标(例如业务对象)和任何参数 级别1:API支持资源的概念。要对资源执行操作时客户端会发出POST请求并指定要执行的操作和参数 级别2:API使用HTTP谓词执行操作: 检索GET 创建POST 更新PUT 请求查询参数和请求体(如果有)来指定操作的参数。这使服务能够利用Web基础结构,例如缓存GET请求。 级别3:API的设计基于HATEOAS(Hypertext As The Engine Of Application State,超文本作为应用程序状态引擎)原则。基本思想是GET请求返回的资源表示包含用于对该资源执行允许操作的链接。例如,客户端可以使用为响应发送的GET请求而返回的Order结果中的链接来取消订单在一个检索订单的请求中。 HATEOAS的好处包括: 不再需要将URL硬连接到客户端代码中 由于资源的表示包含允许操作的链接,因此客户端不必猜测可以对当前状态的资源执行哪些操作 使用基于HTTP的协议有很多好处: HTTP简单而且熟悉。 可以使用扩展(如Postman)在浏览器中测试HTTP API或使用curl从命令行(假设使用JSON或其他一些文本格式)测试。 它直接支持请求/响应式通信。 HTTP对防火墙友好。 它不需要中间代理,这简化了系统的体系结构。 使用HTTP有一些缺点: 它只直接支持请求/响应交互风格。可以使用HTTP进行通知,但服务器必须始终发送HTTP响应。 因为客户端和服务直接通信(没有中间缓冲消息),所以它们必须在交换期间运行。 客户端必须知道每个服务实例的位置(即URL)。如前一篇关于API网关的文章所述,这是现代应用程序中的一个重要问题。客户端必须使用服务发现机制来定位服务实例。 开发人员社区最近重新发现了RESTful API的接口定义语言的价值。有几个选项,包括RAML和Swagger。某些IDL(如Swagger)允许定义请求和响应消息的格式。其他如RAML要求使用单独的规范,如JSON Schema。除了描述API之外,IDL通常还具有从接口定义生成客户端存根和服务器骨架的工具。 Thrift Apache Thrift是REST的有趣替代品。它是用于编写跨语言RPC客户端和服务器的框架。Thrift提供了一个C风格的IDL来定义的API。可以使用Thrift编译器生成客户端存根和服务器端骨架。编译器为各种语言生成代码,包括C ++,Java,Python,PHP,Ruby,Erlang和Node.js。 Thrift接口由一个或多个服务组成。服务定义类似于Java接口。它是强类型方法的集合。Thrift方法可以返回(可能是void)值,也可以定义为单向请求。 返回值的方法实现了交互的请求/响应样式。客户端等待响应并可能抛出异常。 单向方法对应于交互的通知方式。服务器不发送响应。 Thrift支持各种消息格式: JSON:JSON对人类和浏览器友好 二进制:二进制比JSON更有效,因为它解码速度更快 紧凑二进制:紧凑二进制是一种节省空间的格式 Thrift还为提供了包括原始TCP和HTTP在内的传输协议选择。原始TCP可能比HTTP更有效。但是,HTTP是对防火墙,浏览器和人类友好的。 消息格式 现在我们已经查看了HTTP和Thrift,让我们来看看消息格式的问题。如果使用的是消息系统或REST,则可以选择消息格式。其他IPC机制(如Thrift)可能只支持少量消息格式,可能只支持一种。在任何一种情况下,使用跨语言消息格式都很重要。即使今天用单一语言编写微服务,未来很可能会使用其他语言。 消息格式主要有两种: 文本:基于文本的格式示例包括JSON和XML。这些格式的一个优点是它们不仅具有人类可读性,而且具有自我描述性。 在JSON中,对象的属性由名称-值对的集合表示。 在XML中,属性由命名元素和值表示。这使消息的使用者能够选择它感兴趣的值并忽略其余的值。因此,可以轻松地向后兼容对消息格式的微小更改 XML文档的结构由XML模式指定。随着时间的推移,开发人员社区已经意识到JSON也需要类似的机制。一种选择是使用JSON Schema,可以是独立的,也可以作为IDL的一部分,如Swagger。 使用基于文本的消息格式的缺点是消息往往是冗长的,尤其是XML。由于消息是自描述的,因此除了值之外,每条消息都包含属性的名称。另一个缺点是解析文本的开销。因此,可能需要考虑使用二进制格式。 二进制:有几种二进制格式可供选择。 如果使用的是Thrift RPC,则可以使用二进制Thrift。 如果选择消息格式,常用选项包括Protocol Buffers和Apache Avro。这两种格式都提供了一个用于定义消息结构的类型IDL。区别是: Protocol Buffers使用标记字段 Avro消费者需要知道模式才能解释消息。 因此,试用Protocol Buffers的API演变比使用Avro更容易 这篇博文是Thrift,Protocol Buffers和Avro的绝佳比较。 总结 微服务必须使用进程间通信机制进行通信。在设计服务的通信方式时,需要考虑各种问题:服务如何交互,如何为每个服务指定API,如何更新API以及如何处理部分失败故障。微服务可以使用两种IPC机制,异步消息传递和同步请求/响应。在本系列的下一篇文章中,我们将研究微服务架构中的服务发现问题。
原文链接 这是我们系列中关于使用微服务构建应用程序的第四篇文章。在本文中,我们将探讨与服务发现密切相关的问题。 为何需要服务发现 让我们假设正在编写一些代码来调用具有REST API或Thrift API的服务。为了发出请求,代码需要知道服务实例的网络位置(IP地址和端口)。 在物理硬件上运行的传统应用程序中,服务实例的网络位置是相对静态的。例如,代码可以从偶尔更新的配置文件中读取网络位置。 在现代的基于云的微服务应用程序中,这是一个难以解决的问题,如下图所示。 服务实例具有动态分配的网络位置。此外,由于自动扩展,故障和升级,服务实例集会动态更改。因此,客户端代码需要使用更复杂的服务发现机制。有两种主要的服务发现模式: 客户端发现 服务器端发现 我们先来看看客户端发现。 客户端发现模式 使用客户端发现时,客户端负责确定可用服务实例的网络位置以及跨这些实例的负载均衡请求。客户端查询服务注册表,该服务注册表是可用服务实例的数据库。然后,客户端使用负均衡算法选择一个可用的服务实例并发出请求。下图显示了此模式的结构。 服务实例的网络位置在启动时向服务注册表注册。实例终止时,它将从服务注册表中删除。通常使用心跳机制定期刷新服务实例的注册。 Netflix OSS提供了客户端发现模式的一个很好的例子。Netflix Eureka是一个服务注册表。它提供了一个REST API,用于管理服务实例注册和查询可用实例。Netflix Ribbon是一个IPC客户端,与Eureka一起在可用服务实例之间加载均衡请求。我们将在本文后面更深入地讨论Eureka。 客户端发现模式具有各种优点:这种模式相对简单,除了服务注册表之外,没有其他移动部件。所以,当客户端知道了可用的服务实例后,就可以进行智能的、基于特定应用程序的负载均衡决策算法(例如,使用一致性哈希算法)来访问服务。 这种模式的一个重要缺点是:它将客户端与服务注册表耦合在一起,必须为服务使用的每种编程语言和框架实现客户端服务发现逻辑。 现在我们已经了解了客户端发现,让我们来看看服务器端发现。 服务器端发现模式 服务发现的另一种方法是服务器端发现模式。下图显示了此模式的结构。 客户端通过负载均衡器向服务发出请求。负载均衡器查询服务注册表并将每个请求路由到可用的服务实例。与客户端发现一样,服务实例在服务注册表中注册或注销。 AWS Elastic Load Balancer(ELB)是服务器端发现路由器的示例。ELB通常用于对来自Internet的外部流量进行负载均衡。但是,也可以使用ELB来均衡虚拟私有云(VPC)内部的流量。客户端使用其DNS名称通过ELB发出请求(HTTP或TCP)。 ELB负载均衡一组已注册的弹性计算云(EC2)实例或EC2容器服务(ECS)容器之间的流量。不需要单独的服务注册表。相反,EC2实例和ECS容器已在ELB中注册完成。 HTTP服务器和负载均衡器(如NGINX Plus和NGINX)也可用作服务器端发现负载均衡器。例如,这篇博文描述了使用Consul Template动态重新配置NGINX反向代理。 Consul Template是一种工具,可定期从存储在Consul服务注册表中的配置数据中重新生成配置文件。每当文件更改时,它都会运行shell命令。 在博客文章描述的示例中,Consul Template生成一个nginx.conf文件,该文件用于配置反向代理,然后运行一个命令使NGINX重新加载配置。更复杂的实现可以使用HTTP API或DNS动态重新配置NGINX Plus。 某些部署环境(如Kubernetes和Marathon)在群集中的每个主机上运行一个代理服务。代理服务作为服务器端发现模式负载均衡器使用。为了向服务发出请求,客户端使用主机的IP地址和端口通过代理服务路由请求。然后,代理将请求透明地转发到在群集中某处运行的可用服务实例。 服务器端发现模式的优势和不足: 这种模式的一个很大好处是服务发现的细节被抽象出客户端。客户端只是向负载均衡器发出请求。这消除了为服务端使用的每种编程语言和框架实现发现逻辑的需要,并且在某些部署环境(如,kubernetes和Marathon)免费提供此功能。 这种模式也有一些缺点。除非部署环境提供负载均衡器,否则它是需要设置和管理的另一个高可用性系统组件。 服务注册表 服务注册表是服务发现的关键部分,它是一个包含服务实例网络位置的数据库。服务注册表需要具有高可用性和数据最新性。 客户端可以缓存从服务注册表获取的网络位置,但是,该信息最终会过时而导致客户端无法发现服务实例。服务注册表由一组服务组成,这些服务器通过复制协议来保持数据的一致性。 如前所述,Netflix Eureka是服务注册表的一个很好的例子。它提供了一个REST API,用于注册和查询服务实例。 服务实例使用HTTP POST请求注册其网络位置。每隔30秒,它必须使用HTTP PUT请求刷新注册信息 通过使用HTTP DELETE请求来删除注册信息,当实例注册超时,也会自动删除注册信息 客户端可以使用HTTP GET请求检索已注册的服务实例 Netflix通过在每个Amazon EC2可用区域中运行一个或多个Eureka服务器来实现高可用性。每个Eureka服务器都在具有弹性IP地址的EC2实例上运行。DNS TEXT记录用于存储Eureka群集配置,该配置是从可用区域到Eureka服务器的网络位置列表的映射。当Eureka服务器启动时,它会查询DNS以检索Eureka群集配置,找到其对等端,并为自己分配未使用的弹性IP地址。 Eureka客户端通过查询DNS来发现Eureka服务器的网络位置。客户端更希望在同一可用区中使用Eureka服务器,但是,如果在同一个可用区内没有可访问的Eureka服务器,那么客户端会在另一个可用区中访问Eureka服务器。 服务注册表的其他示例包括: etcd:用于共享配置和服务发现的高可用的、分布式的、一致性的键值对存储。使用etcd的两个值得注意的项目是Kubernetes和Cloud Foundry。 consul:用于发现和配置服务的工具。它提供了一个API,允许客户注册和发现服务。Consul可以执行运行状况检查以确定服务可用性。 Apache Zookeeper:一种广泛使用的高性能协调服务,适用于分布式应用程序。 此外,如前所述,某些系统(如Kubernetes,Marathon和AWS)没有明确的服务注册表。服务注册表只是基础结构的内置部分。现在我们已经了解了服务注册表的概念,让我们看看如何在服务注册表中注册服务实例。 服务注册选项 如前所述,服务实例必须在服务注册表中注册和注销。有几种不同的方式来处理注册和注销。 服务实例自我注册,即自注册模式 用于某些其他系统组件来管理服务实例的注册,即第三方注册模式 自注册模式 使用自注册模式时,服务实例负责向服务注册表注册和注销自身,如果需要,服务实例会发送心跳请求以防止其注册过期。下图显示自注册模式的结构。 这种方法的一个很好的例子是Netflix OSS Eureka客户端,它会处理服务实例注册和注销的所有方面。Spring Cloud项目实现了各种模式,包括服务发现,可以轻松地使用Eureka自动注册服务实例。只需使用@EnableEurekaClient批注对Java Configuration类进行批注。 自注册模式的优缺点: 好处:它相对简单并且不需要任何其他系统组件。 缺点:它将服务实例耦合到服务注册表,必须在服务使用的每种编程语言和框架中实现注册代码。 将服务与服务注册表分离的替代方法是第三方注册模式。 第三方注册模式 使用第三方注册模式时,服务实例不负责向服务注册表注册自己。相反,另一个称为服务注册器的系统组件处理注册。服务注册器通过轮询部署环境或订阅事件来跟踪运行实例集的更改。当它发现有新的服务实例运行时,会将新的服务实例注册到服务注册表中,同时,服务注册器还负责注销已终止运行的服务实例。下图显示了此模式的结构。 服务注册器的例子: Registrator:它会自动注册和注销部署为Docker容器的服务实例。Registrator支持多个服务注册表,包括etcd和Consul。 NetflixOSS Prana:它主要用于以非JVM语言编写的服务,它是一个与服务实例并行运行的sidecar应用程序。Prana使用Netflix Eureka注册和注销服务实例。 服务注册器是部署环境的内置组件。由Autoscaling Group创建的EC2实例可以自动注册到ELB。Kubernetes的services会自动注册并被发现。 第三方注册模式的优缺点: 好处:服务与服务注册表分离。不需要为开发人员使用的每种编程语言和框架实现服务注册逻辑。相反,服务实例注册在专用服务中以集中方式处理。 缺点:除非它内置于部署环境中,否则它是需要设置和管理的另一个高可用性系统组件。 总结 在微服务应用程序中,正在运行的服务实例集合会动态更改,每个服务实例都会动态分配网络位置。因此,客户端要向服务实例发出请求,就必须使用服务发现机制。 服务发现的关键部分是服务注册表,它是可用服务实例的数据库,提供管理API和查询API。 使用管理API在服务注册表中注册和注销服务实例 使用查询API来发现可用的服务实例 有两种主要的服务发现模式: 在使用客户端发现的系统中,客户端查询服务注册表,选择可用实例并发出请求。 在使用服务端发现的系统中,客户端通过负载均衡器发出请求,负载均衡器查询服务注册表并将请求转发给可用实例。 服务实例在服务注册表中注册和注销有两种主要方式: 自注册模式:服务实例向服务注册表进行注册和注销 第三方注册模式:某些其他系统组件代表服务处理注册和注销 在某些部署环境中,需要使用服务注册表(如Netflix Eureka,etcd或Apache Zookeeper)作为服务发现基础组件。 在其他部署环境中,内置了服务发现。例如,Kubernetes和Marathon自带处理服务实例注册和注销的功能,它们在每台主机上运行一个代理服务器作为服务器端发现模式中的负载均衡器。 HTTP反向代理和负载均衡器(如NGINX)也可用作服务器端发现模式中的负载均衡器。服务注册表可以将路由信息推送到NGINX并调用正常的配置更新;例如,可以使用Consul Template。NGINX Plus支持其他动态重新配置机制,它可以使用DNS从注册表中提取有关服务实例的信息,并提供用于远程重新配置的API。在未来的博客文章中,我们将继续深入研究微服务的其他方面。
原文链接 这是关于使用微服务构建应用程序的系列文章的第五篇。第一篇文章介绍了微服务架构模式,并讨论了使用微服务的优缺点。第二篇和第三篇文章描述了微服务架构中通信的不同方面。第四篇文章探讨了服务发现的密切相关问题。在本文中,研究微服务架构中出现的分布式数据管理问题。 微服务与分布式数据管理问题 单体应用程序通常具有单个关系数据库。使用关系数据库的一个主要好处是:应用程序可以使用ACID事务,它提供了一些重要保证: 原子性:自动进行更改 一致性:数据库的状态始终保持一致 隔离性:即使事务同时执行也像串行执行 持久性:一旦事务提交,它就不会被撤消 因此,应用程序可以简单地开始事务,更改(插入、更新和删除)多行,并提交事务。 使用关系数据库的另一个好处是:它提供SQL(一种丰富的、声明性的、标准化的查询语言)。可以轻松编写来自多个表的数据的组合查询,然后,RDBMS(query planner)确定执行查询的最佳方式,不必担心诸如如何访问数据库之类的低级细节。因为所有应用程序的数据都在一个数据库中,所以很容易查询。 不幸的是,当我们转向微服务架构时,数据访问变得更加复杂。这是因为每个微服务拥有的数据对于该微服务是私有的,并且只能通过其API访问,封装数据可确保微服务松散耦合,并且可以彼此独立地迭代更新。如果多个服务访问相同的数据,则架构更新需要对所有服务进行耗时的协同更新。 更糟糕的是,不同的微服务通常使用不同类型的数据库。现代应用程序存储和处理各种数据,关系数据库并不总是最佳选择。对于某些用例,特定的NoSQL数据库可能具有更方便的数据模型,并提供更好的性能和可伸缩性。例如: 存储和查询文本的服务使用文本搜索引擎(如Elasticsearch)是有意义的。 存储社交图数据的服务应该使用图形数据库,例如Neo4j。 因此,基于微服务的应用程序通常使用SQL和NoSQL数据库的混合,即所谓的多语言持久性方法。用于数据存储的分区多字节持久架构具有许多优点,包括: 松散耦合的服务 更好的性能和可扩展性 但是,它确实引入了一些分布式数据管理挑战。 第一个挑战是如何实现维护多个服务之间一致性的业务事务。要了解这个问题,让我们来看一个在线B2B商店的例子。 客户服务部门维护有关的客户信息表,包括其信用额度。订单服务管理订单相关信息,并且必须验证新订单是否超过客户的信用额度。 在单体应用中,订单服务可以简单地使用ACID事务来检查可用信用额度并创建订单。 在微服务中,ORDER和CUSTOMER表对其各自的服务是私有的,如下图所示。 订单服务无法直接访问CUSTOMER表。它只能使用客户服务提供的API。订单服务可能使用分布式事务,也称为两阶段提交(2PC),但是,2PC在现代应用中通常不是一个可行的选择。 CAP定理要求在可用性和ACID样式的一致性之间进行选择,并且可用性通常是更好的选择。 此外,许多现代技术,如大多数NoSQL数据库,都不支持分布式事物。维护服务和数据库之间的数据一致性至关重要,因此我们需要另一种解决方案。 第二个挑战是如何实现从多个服务检索查询数据。例如: 假设应用程序需要显示客户及其最近的订单。 如果订单服务提供用于检索客户订单的API,那么可以使用应用程序端联接来检索此数据。 应用程序从客户服务中检索客户,并从订单服务中检索客户的订单。 但是,假设订单服务仅支持按主键查找订单(可能它使用的NoSQL数据库仅支持基于主键的检索)。在这种情况下,没有明显的方法来检索所需的数据。 事件驱动体系结构 对于许多应用程序,解决方案是使用事件驱动的体系结构。在这种体系结构中,微服务在发生重要事件时发布事件(例如,在更新业务实体时)。其他微服务订阅这些事件,当微服务接收事件时,它可以更新自己的业务实体,这样的更新操作可能触发发布更多事件。 可以使用事件来实现跨多个服务的业务事务。交易包含一系列步骤,每个步骤都包括一个微服务更新业务实体并发布触发下一步的事件。 以下序列图显示了在创建订单时如何使用事件驱动方法检查可用额度。微服务通过消息代理服务(Message Broker)交换事件。 订单服务创建状态为NEW的订单并发布订单创建事件。 客户服务使用订单创建事件,保留订单信用额度,并发布信用额度保留事件。 订单服务使用信用额度保留事件,并将订单状态更改为OPEN。 更复杂的情况可能涉及其他步骤,例如,在检查客户信用额度的同时预订库存。 如果: (a)每个服务原子地更新数据库并发布事件(稍后会更多) (b)消息代理服务保证事件至少被传递一次 那么可以实现跨多个服务的业务事务。 值得注意的是,这不是ACID事务。它们提供了许多较弱的保证,例如最终的一致性。此事务模型称为BASE模型。 还可以使用事件来维护预加入多个微服务所拥有的数据的物化视图,维护视图的服务订阅相关事件并更新视图。例如,维护客户订单视图的客户订单视图更新程序订阅并处理客户服务和订单服务发布的事件。 当客户订单视图更新程序服务收到客户或订单事件时,它会更新客户订单视图数据。可以使用文档数据库(如MongoDB)实现客户订单视图,并为每个客户存储一个文档。客户订单视图查询服务通过查询客户订单视图数据存储来处理客户和最近订单的请求。 事件驱动的体系结构优点: 它支持跨多个服务的事务的实现,并提供最终的一致性 它还使应用程序能够维护物化视图 事件驱动的体系结构缺点: 编程模型比使用ACID事务时更复杂: 必须实现事务补偿机制以从应用程序级的故障中恢复;例如,如果信用额度检查失败,则必须取消客户订单。 必须处理不一致的数据;因为事务所做的更改是可见的,如果应用程序从尚未更新的物化视图中读取,则应用程序也会看到不一致。 订阅事件的服务必须检测并忽略重复的事件。 实现原子性 在事件驱动的体系结构中,还存在数据库的原子更新操作和发布事件的问题。例如: 订单服务必须在ORDER表中插入一行数据 并发布Order Created事件 这两个操作必须以原子方式完成。 如果服务在更新数据库之后但在发布事件之前崩溃,则系统会变得不一致。确保原子性的标准方法是使用涉及数据库和消息代理服务的分布式事务。但是,由于上述原因,例如CAP定理,这正是我们不想做的。 使用本地事务发布事件 实现原子性一种方法是:应用程序使用仅涉及本地事务的多步骤过程来发布事件。诀窍是在存储业务实体状态的数据库中有一个EVENT表,它充当消息队列。 应用程序开始(本地)数据库事务,更新业务实体的状态,将事件插入EVENT表,并提交事务。单独的应用程序线程或进程查询EVENT表,将事件发布到消息代理服务,然后使用本地事务将事件标记为已发布。下图显示了该设计。 订单服务将一行插入ORDER表,并将Order Created事件插入EVENT表。事件推送线程或进程在EVENT表中查询到未发布的事件,并将事件发布出去,然后更新EVENT表以将事件标记为已发布。 这种方法的优缺点: 好处:它保证在不依赖于分布式事务的情况下为每次更新发布事件。并且,应用程序发布的是事务级别的事件。 缺点:它可能容易出错,因为开发人员必须记住发布事件。这种方法的局限性在于,由于其有限的事务和查询功能,在使用某些NoSQL数据库时实施起来很困难。 通过让应用程序使用本地事务来更新状态和发布事件,此方法消除了对分布式事务的需求。现在让我们看一下通过让应用程序简单地更新状态来实现原子性的方法。 挖掘数据库事务日志 在没有分布式事务的情况下实现原子性的另一种方法是由挖掘数据库事务或提交日志的线程或进程发布事件。 应用程序更新数据库,这会导致更改操作记录在数据库的事务日志中。Transaction Log Miner线程或进程读取事务日志并将事件发布到消息消息代理服务。下图显示了该设计。 LinkedIn Databus项目。Databus挖掘Oracle事务日志并发布与更改相对应的事件。LinkedIn使用Databus来保持各种派生数据存储与记录系统一致。 AWS DynamoDB中的流机制,它是一个托管的NoSQL数据库。DynamoDB流包含在过去24小时内对DynamoDB表中的项目进行的按时间排序的更改(创建,更新和删除操作)序列。应用程序可以从流中读取这些更改,例如,将它们作为事件发布。 事务日志挖掘的优缺点: 好处:它保证在不使用分布式事务的情况下为每次更新发布事件。事务日志挖掘还可以通过将事件发布与应用程序的业务逻辑分离来简化应用程序。 缺点:事务日志的格式是每个数据库专有的,甚至可以在数据库版本之间进行更改。此外,从事务日志中记录的低级更新中反向设计高级业务事件可能很困难。 事务日志挖掘通过让应用程序做一件事来消除对分布式事务的需求:更新数据库。现在让我们看一个消除更新并仅依赖于事件的不同方法。 使用事件溯源 事件溯源通过使用完全不同的,以事件为中心的方法来保持业务实体,从而在没有分布式事务的情况下实现原子性。应用程序存储一系列状态改变事件,而不是存储实体的当前状态。应用程序通过重放事件来重建实体的当前状态,每当业务实体的状态发生变化时,都会在事件列表中附加一个新事件。由于保存事件是单个操作,因此它本质上是原子的。 要了解事件溯源的工作原理,请将Order实体视为示例: 在传统方法中,每个订单映射到ORDER表中的行(例如,ORDER_LINE_ITEM表中的行)。 在使用事件溯源时,订单服务会以状态更改事件的形式存储订单:已创建,已批准,已发货,已取消。每个事件都包含足够的数据来重建订单的状态。 事件持续存储在事件存储器中,事件存储器是保存事件的数据库。 事件存储器有一个用于添加和检索实体事件的API。事件存储器的行为与我们之前描述的体系结构中的消息代理服务相似,它提供了一个API,使服务可以订阅事件。事件存储器向所有感兴趣的订阅者提供所有事件状态变更数据。事件存储器是事件驱动的微服务架构的支柱。 事件溯源的好处: 它解决了实现事件驱动架构的关键问题,可以在状态变更时可靠地发布事件 它解决了微服务架构中的数据一致性问题 它持久存储事件而不是域对象,所以它避免了对象-关系不匹配问题 它还提供对业务实体更改时100%可靠的审计日志,并且可以实现在任何时间点确定实体状态的时态查询功能 业务逻辑由交换事件的松散耦合的业务实体组成,这使得从单体应用迁移到微服务架构变得更加容易 事件溯源的缺点:这是一种不同的,不熟悉的编程风格,因此有一定学习成本。事件存储仅直接支持按主键查找业务实体,必须使用命令查询责任隔离(CQRS)来实现查询。因此,应用程序必须处理最终一致的数据。 总结 在微服务架构中,每个微服务都有自己的私有数据存储。不同的微服务可能使用不同的SQL和NoSQL数据库。虽然此数据库体系结构具有显着优势,但它会产生一些分布式数据管问题: 第一个挑战是:如何实现维护多个服务之间一致性的业务事务。 第二个挑战是:如何实现从多个服务检索数据的查询。 对于许多应用程序,解决方案是使用事件驱动的体系结构。 实现事件驱动架构的一个挑战是:如何以原子方式更新状态以及如何发布事件。有几种方法可以实现此目的,包括: 将数据库用作消息队列 数据库事务日志挖掘 事件溯源 在未来的博客文章中,我们将继续深入研究微服务的其他方面。
原文链接 这是关于使用微服务构建应用程序的系列文章的第六篇。第一篇文章介绍了微服务架构模式,并讨论了使用微服务的优缺点。以下文章讨论了微服务架构的不同方面:使用API网关,进程间通信,服务发现和事件驱动的数据管理。在本文中,我们将介绍部署微服务的策略。 动机 部署单体应用意味着运行单个(通常是大型)应用程序的多个相同副本。通常会配置N个服务器(物理或虚拟),并在每个服务器上运行M个实例。单体应用的部署并不总是完全简单,但它比部署微服务应用程序简单得多。 微服务应用程序包含数十甚至数百个服务。服务以各种语言和框架编写,每个都是一个迷你应用程序,具有自己的特定部署,资源,扩展和监控要求。 例如,需要根据根据服务的需求运行每个服务特定数量的实例。同时,必须为每个服务实例提供适当的CPU,内存和I/O资源。 更具挑战性的是:尽管存在这种复杂性,但部署服务必须快速,可靠且具有成本效益。 有一些不同的微服务部署模式。让我们首先看一下每个主机多服务实例模式。 单主机多服务实例模式 部署微服务的一种方法是使用单主机多服务实例模式。使用此模式时,可以配置一个或多个物理或虚拟主机,并在每个主机上运行多个服务实例。这是应用程序部署的传统方法,每个服务实例都在一个或多个主机上的一个已知的端口上运行。下图显示了此模式的结构。 这种模式有几种变体。 一种变体是:每个服务实例是进程或进程组。例如: Java服务实例部署为Apache Tomcat服务器上的Web应用程序 Node.js服务实例可能包含父进程和一个或多个子进程 另一个变体是:在同一进程或进程组中运行多个服务实例。例如: 可以在同一个Apache Tomcat服务器上部署多个Java Web应用程序 或者在同一个OSGI容器中运行多个OSGI包 单主机多服务实例模式的优点: 资源使用相对有效。多个服务实例共享服务器及其操作系统。如果进程或进程组运行多个服务实例,例如,多个Web应用程序共享同一个Apache Tomcat服务器和JVM,则效率更高。 部署服务实例的速度相对较快。只需将服务复制到主机并启动它。 如果服务是用Java编写的,则复制JAR或WAR文件。 对于其他语言,例如Node.js或Ruby,可以复制源代码。 在任何一种情况下,通过网络复制的字节数相对较小。 此外,由于开销很小,所以启动服务通常非常快。如果服务是它自己的进程,你只需启动它。否则,如果服务是在同一容器进程或进程组中运行的多个实例之一,则可以将其动态部署到容器中,也可以重新启动容器。 每个主机多服务实例模式的缺点: 几乎没有服务实例的隔离,除非每个服务实例是一个单独的进程。虽然可以准确地监视每个服务实例的资源利用率,但无法限制每个实例使用的资源。 行为不当的服务实例可能会占用主机的所有内存或CPU。如果多个服务实例在同一进程中运行,则根本没有隔离。例如,所有实例可能共享相同的JVM堆。行为不端的服务实例可能很容易破坏在同一进程中运行的其他服务。并且无法监视每个服务实例使用的资源。 部署服务的运营团队必须知道如何执行该操作的具体细节。服务可以用各种语言和框架编写,因此开发团队必须与操作共享许多细节。这种复杂性增加了部署期间出错的风险。 单主机多服务实例模式具有一些明显的缺点,现在让我们看看部署避免这些问题的微服务的其他方法。 单主机单服务实例模式 单主机单服务实例模式。使用此模式时,可以在其自己的主机上独立运行每个服务实例。此模式有两种不同的特殊化: 单虚拟机的服务实例 单容器的服务实例 单虚拟机单服务实例模式 当使用单虚拟机单服务实例模式,将每个服务打包为虚拟机(VM)镜像,例如Amazon EC2 AMI。每个服务实例都是使用该VM镜像启动的VM(例如,EC2实例)。下图显示了此模式的结构: 这是Netflix用于部署其视频流服务的主要方法。 Netflix使用Aminator将其每项服务打包为EC2 AMI。每个正在运行的服务实例都是EC2实例。可以使用各种工具来构建自己的VM。可以配置持续集成(CI)服务器(例如,Jenkins)以调用Aminator将的服务打包为EC2 AMI。 Packer.io是自动创建VM镜像的另一种选择。与Aminator不同,它支持各种虚拟化技术,包括EC2,DigitalOcean,VirtualBox和VMware。 Boxfuse公司有一种令人信服的方法来构建VM镜像,它克服了我在下面描述的虚拟机的缺点。Boxfuse将Java应用程序打包为最小的VM镜像。这些镜像构建速度快,启动快,并且因为它们暴露有限的攻击面而更安全。 CloudNative公司拥有Bakery,这是一个用于创建EC2 AMI的SaaS产品。可以将CI服务器配置为在微服务通过测试后调用Bakery。然后,Bakery将的服务打包为AMI。使用Bakery等SaaS产品意味着不必浪费宝贵的时间来设置AMI创建基础架构。 单虚拟机单服务实例模式的优点: 每个服务实例都完全隔离运行:它具有固定数量的CPU和内存,不能从其他服务中窃取资源。 利用成熟的云基础架构:诸如AWS之类的云提供了有用的功能,例如负载均衡和自动扩展。 封装了用于实现服务的技术:一旦服务被打包为VM,它就变成了黑盒子,VM的管理API成为部署服务的API。部署变得更加简单和可靠。 单虚拟机单服务实例模式的缺点: 资源利用效率较低:每个服务实例都有整个VM的开销,包括操作系统。 在典型的公共IaaS中,VM具有固定的大小,并且VM可能未充分利用。 迁移,公共IaaS通常会为VM收费,无论他们是忙还是闲。AWS等IaaS提供自动扩展功能,但很难对需求变化做出快速反应。因此,经常需要过度配置VM,这会增加部署成本。 部署新版本的服务通常很慢: VM镜像由于其大小而构建缓慢 VM的实例由于其大小运行很慢 操作系统通常需要一些时间来启动(但请注意,这并非普遍适用,因为存在Boxfuse构建的轻量级VM) 通常组织中的其他人负责大量无差别的繁重工作。除非使用像Boxfuse这样的工具来处理构建和管理VM的开销。这项必要但耗时的活动会分散的核心业务。 现在让我们看一下部署微服务的替代方法,这些微服务更轻量级但仍具有VM的许多优点。 单容器单服务实例模式 当使用单容器单服务实例模式时,每个服务实例都在其自己的容器中运行。容器是操作系统级别的虚拟化机制,在沙箱中运行的一个或多个进程,它们有自己的端口、命名空间、根文件系统,可以限制容器的内存和CPU资源,某些容器实现也具有I/O速率限制。容器技术的示例包括Docker和Solaris Zones。下图显示了此模式的结构: 要使用此模式,请将服务打包为容器镜像(是由运行服务所需的应用程序和库组成的文件系统镜像)。一些容器镜像由完整的Linux根文件系统组成,其他更轻巧。 例如,要部署Java服务,需要构建一个包含Java运行时(可能是Apache Tomcat服务器)和已编译的Java应用程序的容器镜像。将服务打包为容器镜像后,即可启动一个或多个容器。 通常在每个物理或虚拟主机上运行多个容器,可以使用集群管理器(如Kubernetes或Marathon)来管理容器。集群管理器将主机视为资源池,根据容器所需的资源和每个主机上可用的资源决定放置每个容器的位置。 单容器单服务实例模式的好处类似于VM的好处: 将的服务实例彼此隔离,可以轻松监视每个容器所消耗的资源。 与VM类似,容器封装了用于实现服务的技术。容器管理API还可用作管理服务的API。 > 与VM不同,容器是一种轻量级技术。容器镜像通常非常快速构建(例如,在我的笔记本电脑上,将Spring Boot应用程序打包为Docker容器只需5秒钟)。 容器也很快启动,因为没有冗长的操作系统启动机制,当容器启动时,运行的就是服务。 单容器单服务实例模式的缺点: 虽然容器基础设施正在快速成熟,但它并不像VM的基础设施那样成熟。 容器不如VM安全,因为容器彼此共享主机OS的内核。 负责管理容器镜像是大量无差别的繁重工作。 除非使用托管容器解决方案(如GKE或ECS),否则,必须管理容器基础结构以及可能运行的VM基础结构。 容器通常部署在收费的虚拟机基础架构上,因此,为了处理负载峰值,可能会产生过度配置VM的额外成本。有趣的是,容器和虚拟机之间的区别可能会变得模糊。如前所述,Boxfuse VM的构建和启动速度很快。 Clear Containers项目旨在创建轻量级VM。正如2017年12月宣布的那样,Clear Containers的开发现在继续在开源Kata Containers项目中进行。对unikernel的关注越来越多, Docker Inc. 最近收购了Unikernel Systems。 还有一种更新且越来越流行的无服务器部署概念,这种方法可以避免在容器或虚拟机中部署服务之间做出选择的问题。让我们看看下一步。 无服务模式部署 AWS Lambda是无服务部署技术的一个示例。它支持Java,Node.js和Python服务。要部署微服务: 请将其打包为ZIP文件并将其上载到AWS Lambda 提供元数据,其中包括为处理请求而调用的函数的名称(例如,事件) AWS Lambda自动运行足够的微服务实例来处理请求。只需根据所花费的时间和消耗的内存为每个请求付费。当然,魔鬼在细节中,很快就会看到AWS Lambda有局限性。但是,作为开发人员或组织中的任何人都不需要担心服务器,虚拟机或容器的任何方面,这一概念令人难以置信。 Lambda函数是无状态服务。它通常通过调用AWS服务来处理请求。例如,将镜像上载到S3存储桶时调用的Lambda函数可以将数据项插入DynamoDB镜像表,并将消息发布到Kinesis流以触发镜像处理。Lambda函数还可以调用第三方Web服务。 有四种方法可以调用Lambda函数: 直接使用Web服务请求 自动响应AWS服务(如S3,DynamoDB,Kinesis或简单电子邮件服务) 自动生成的事件,通过AWS API网关处理来自应用程序客户端的HTTP请求 定期任务,根据类似cron的执行计划 AWS Lambda是部署微服务的便捷方式。基于请求的定价意味着只需为服务实际执行的工作付费。此外,由于不负责IT基础架构,因此可以专注于开发应用程序。 但是,有一些重大的局限性,它不能用于部署长时间运行的服务(例如,使用来自第三方消息代理的消息服务): 请求必须在300秒内完成 服务必须是无状态的,因为理论上AWS Lambda可能为每个请求运行单独的实例。它们必须使用其中一种受支持的语言编写。服务也必须迅速开始;否则,他们可能会因为超时而被终止。 总结 部署微服务应用程序具有挑战性。有数十甚至数百种服务以各种语言和框架编写,每个都是一个迷你应用程序,具有自己的特定部署模式,资源,扩展和监视要求。 有几种微服务部署模式,包括: 单虚拟机单服务实例 单容器单服务实例 无服务模式(AWS Lambda) 在本系列的下一部分和最后一部分中,我们将介绍如何将单体应用迁移到微服务架构。
原文链接 这是我的系列文章中关于使用微服务构建应用程序的第七篇也是最后一篇文章。第一篇文章介绍了微服务架构模式,并讨论了使用微服务的优缺点。以下文章讨论了微服务架构的不同方面:使用API网关,进程间通信,服务发现,事件驱动的数据管理和部署微服务。 在本文中,我们将介绍将单体应用迁移到微服务的策略。我希望这一系列文章能让单主机对微服务架构,它的优点和缺点以及何时使用它有一个很好的理解。也许微服务架构非常适合单主机的组织。也许单主机正在处理大型复杂的单体应用,单主机每天开发和部署应用程序都很缓慢而且很痛苦。微服务似乎是一个遥远的必杀技,有一些策略可以用来摆脱单体应用的地狱。在本文中,我将介绍如何将单体应用逐步重构为一组微服务。 重构到微服务概述 将单体应用转换为微服务的过程是应用程序现代化的一种形式。这是开发人员几十年来一直在做的事情。因此,在将应用程序重构为微服务时,我们可以重用一些想法。 一种不推荐的的策略是推翻重写。这就是单主机将所有开发工作集中在从头开始构建基于微服务的新应用程序的时候。虽然听起来很吸引人,但它风险极大,很可能以失败告终。正如Martin Fowler所说的那样,“Big Bang重写的唯一保证是Big Bang!” 更合理的方式是,你应该逐步重构你的单体应用。单主机逐步构建一个由微服务组成的新应用程序,并将其与单主机的单体应用一起运行。随着时间的推移,单体应用程序实现的功能量会缩小,直到它完全消失或者变成另一个微服务。这种策略类似于以70英里/小时的速度在高速公路上行驶时为单主机的汽车提供服务,具有挑战性,但风险远低于尝试推翻重写。 Martin Fowler将此应用程序现代化策略称为Strangler应用程序。这个名字来自在热带雨林中发现的扼杀藤蔓(a.k.a. strangler fig)。为了到达森林树冠上方的阳光,荆棘藤生长在树周围。有时,树死了,留下了一棵树状的藤蔓。应用程序现代化遵循相同的模式。我们将构建一个新的应用程序,包括遗留应用程序周围的微服务,最终将会死亡。 让我们来看看这样做的不同策略。 策略1-停止挖掘 Holes法则认为每当你在一个洞里,你应该停止挖掘。当单主机的整体应用程序变得无法管理时,这是一个很好的建议。换句话说,你应该停止使整体更大。这意味着在实现新功能时,不应向单体应用添加更多代码。相反,这个策略的主要思想是将新代码放在一个独立的微服务中。下图显示了应用此方法后的系统架构。 除了新服务和传统单体应用之外,还有另外两个组件。 请求路由器,它处理传入(HTTP)请求。它类似于前面文章中描述的API网关。路由器将对应于新功能的请求发送到新服务。它将遗留请求路由到单体应用。 胶水代码,它将新服务与单体应用结合在一起。服务很少孤立存在,并且通常需要访问单体应用所拥有的数据。粘合代码驻留在单体应用程序,新服务或两者中,负责数据集成。该服务使用粘合代码来读取和写入单体应用所拥有的数据。 服务可以使用三种策略来访问单体应用的数据: 调用单体应用程序提供的远程API 直接访问单体应用的数据库 维护自己的数据副本,该数据与单体应用的数据库同步 胶水代码(也称为反腐败层),这是因为粘合代码阻止了具有其自己的原始域模型的服务被传统单体应用的域模型中的概念污染。胶水代码在两种不同的模型之间进行转换。 反腐败一词首先出现在Eric Evans的必读书《领域驱动设计》中,然后在白皮书中进行了细化。制定反腐败层可能是一件非常重要的事情。但是如果你想要从单体应用程序的地狱中走出来,那么创造一个是至关重要的。 将新功能实现为轻量级服务的好处: 可以防止整体变得更难以管理 该服务可以独立于整体进行开发,部署和扩展 可以为创建的每项新服务体验微服务架构的优势 但是,这种方法无法解决整体问题,要解决这些问题,需要打破单体应用。让我们来看看这样做的策略。 策略2-拆分前端和后端 缩小整体应用程序的策略是将表示层与业务逻辑层和数据访问层分开。典型的企业应用程序至少包含三种不同类型的组件: 表示层:处理HTTP请求并实现(REST)API或基于HTML的Web UI的组件。在具有复杂用户界面的应用程序中,表示层通常是大量代码。 业务逻辑层:作为应用程序核心并实现业务规则的组件。 数据访问层:访问基础架构组件(如数据库和消息代理)的组件。 表示层与业务逻辑层和数据访问层之间通常存在清晰的分离。业务层具有粗粒度API,它由一个或多个部分组成,其封装业务逻辑组件。此API是一个天然接缝,单主机可以沿着该接缝将整体分割为两个较小的应用程序: 一个应用程序包含表示层。 另一个应用程序包含业务逻辑层和数据访问层。 分割后,表示层应用对业务逻辑层应用进行远程调用。下图显示了重构之前和之后的体系结构。 以这种方式拆分整体结构有两个主要好处: 它使单主机能够彼此独立地开发,部署和扩展这两个应用程序。特别是,它允许表示层开发人员在用户界面上快速迭代并轻松执行A/B测试。 它暴露了一个可以由单主机开发的微服务调用的远程API。 然而,这种策略只是部分解决方案,一个或两个应用程序很可能是一个难以管理的巨型应用。单主机需要使用第三种策略来消除剩余的整体块或整体块。 策略3-提取服务 将单体应用中现有模块转换为独立的微服务。每次提取模块并将其转换为服务时,单体应用都会缩小。一旦你转换了足够的模块,单体应用程序将不再是一个问题。它要么完全消失,要么变得足够小以至于它只是另一个微服务。 确定要转换为微服务的模块的优先级 大型复杂的单体应用程序由数十个或数百个模块组成,所有模块都是待转换的候选模块。首先确定转换哪些模块通常具有挑战性,一个好的方法是从一些易于提取的模块开始。这将为单主机提供通用的的微服务转换过程的经验,为之后单主机确定转换哪些带来最大好处的模块提供帮助。 将模块转换为微服务通常很耗时,按照获得的利益最大化对模块进行排名: 转换经常变化的模块通常是有益的。将模块转换为微服务后,可以独立于整体开发和部署它,这将加速开发。 转换具有与其他整体结构明显不同的资源要求的模块也是有益的。例如,将具有内存数据库的模块转换为微服务是有用的,然后可以将该服务部署在具有大量内存的主机上。 转换实现计算成本昂贵的算法的模块是值得的,因为该服务可以部署在具有大量CPU的主机上。通过将具有特定资源需求的模块转换为微服务,可以使应用程序更容易扩展。 在确定要转换哪些模块时,查找现有的粗粒度边界(a.k.a接缝)很有用。它们使模块变成微服务更容易,迁移成本更低。这种边界的一个例子是仅通过异步消息与应用程序的其余部分通信的模块,它可以相对便宜并且易于将该模块转换为微服务。 如何转换模块 转换模块的第一步是在模块和整体结构之间定义粗粒度接口。它很可能是双向API,因为整体将需要服务拥有的数据,反之亦然。由于模块与应用程序其余部分之间存在纠缠的依赖关系和细粒度的交互模式,因此实现此类API通常具有挑战性。 由于域模型类之间存在大量关联,因此使用域模型模式实现的业务逻辑对于重构尤其具有挑战性。经常需要进行重要的代码变更以打破这些依赖关系。实现粗粒度界面后,即可将模块转换为独立服务。为此,必须编写代码以使单体应用和微服务能够通过使用进程间通信(IPC)机制的API进行通信。下图显示了重构之前,期间和之后的体系结构。 在此示例中,模块Z是要转换的候选模块。它的组件由模块X使用,它使用模块Y。 第一个重构步骤是定义一对粗粒度的API。 第一个接口是模块X用于调用模块Z的入站接口。 第二个接口是模块Z用于调用模块Y的出站接口。 第二个重构步骤将模块转换为独立服务。 入站和出站接口由使用IPC机制的代码实现。很可能需要通过将Module Z与微服务框架相结合来构建服务,该框架处理诸如服务发现之类的跨领域问题。一旦转换了一个模块,就可以开发,部署和扩展另一个独立于整体和任何其他服务的模块。甚至可以从头开始重写服务;在这种情况下,将服务与整体结构集成的API代码成为一个反腐败层,可在两个域模型之间进行转换。每次提取服务时,都会朝着微服务的方向迈出新的一步。随着时间的推移,整体结构将缩小,将拥有越来越多的微服务。 总结 将现有应用程序迁移到微服务的过程是应用程序现代化的一种形式。不应该通过从头开始重写单体应用来转向微服务。相反,单体应用应该逐步将应用程序重构为一组微服务。 单体应用转换为微服务可以使用三种策略: 将新功能实现为微服务 从业务逻辑层和数据访问层中拆分表示层 将整体中的现有模块转换为服务 随着时间的推移,微服务的数量将会增长,开发团队的敏捷性和速度将会提高。