微服务入门笔记
微服务简介
单片应用
应用程序的核心是业务逻辑,该业务逻辑由定义服务,域对象和事件的模块实现。围绕核心的是与外部世界接口的适配器。适配器的示例包括数据库访问组件,生成和使用消息的消息传递组件以及公开API或实现UI的Web组件。
尽管具有逻辑模块化的体系结构,但该应用程序却作为一个整体打包和部署。实际格式取决于应用程序的语言和框架。例如,许多Java应用程序打包为WAR文件,并部署在诸如Tomcat或Jetty之类的应用程序服务器上。其他Java应用程序打包为独立的可执行JAR。
成功的应用程序具有随着时间的流逝并最终变得庞大的习惯。在每次冲刺期间,开发团队都会实施更多Story,这当然意味着要添加许多行代码。几年后,小型,简单的应用程序将变得庞然大物。
- 应用程序的绝对大小也会减慢开发速度。应用程序越大,启动时间越长。
- 大型,复杂的整体应用程序的另一个问题是,这是持续部署的障碍。如今,SaaS应用程序的最新技术是每天将变更推送到生产中多次。
- 当不同的模块具有冲突的资源需求时,单片应用程序也可能难以扩展。例如,一个模块可能实现CPU密集型图像处理逻辑,并且理想情况下将部署在Amazon EC2 Compute Optimized实例中。另一个模块可能是内存数据库,最适合EC2内存优化的实例。但是,由于这些模块是一起部署的,因此您必须在硬件选择上进行折衷。
- 由于所有模块都在同一个进程中运行,因此任何模块中的错误(例如内存泄漏)都可能导致整个进程中断。
- 单片应用程序使采用新框架和语言变得极为困难。
微服务
服务通常实现一组不同的特征或功能,例如订单管理,客户管理等。每个微服务都是一个微型应用程序,具有自己的六边形体系结构,该体系结构由业务逻辑和各种适配器组成。某些微服务会公开其他微服务或应用程序客户端使用的API。其他微服务可能实现Web UI。在运行时,每个实例通常是云VM或Docker容器。
现在,应用程序的每个功能区域都由其自己的微服务实现。此外,该Web应用程序被分为一组简单的Web应用程序(例如,在我们的出租车叫车示例中,一个用于乘客,一个用于驾驶员)。这使得为特定用户,设备或特殊用例部署不同的体验变得更加容易。
每个后端服务公开一个REST API,大多数服务使用其他服务提供的API。例如,驾驶员管理使用通知服务器来通知可用的驾驶员潜在的行程。UI服务调用其他服务以呈现网页。服务还可以使用基于消息的异步通信。本系列后面的内容将更详细地介绍服务间通信。
优点
- 将原本是巨大的整体应用程序分解为一组服务。虽然功能总数不变,但该应用程序已分为可管理的块或服务。每个服务都有以RPC或消息驱动的API形式定义的边界。
- 使每个服务可以由专注于该服务的团队独立开发。开发人员可以自由选择任何有意义的技术,只要该服务遵守API合同即可。
- 微服务架构模式使每个微服务可以独立部署。开发人员无需协调其服务本地更改的部署。这些更改只要经过测试就可以部署。
- 微服务架构模式使每个服务都可以独立扩展。可以仅部署满足其容量和可用性约束的每个服务的实例数。
缺点
- 微服务应用程序是分布式系统这一事实而导致的复杂性。开发人员需要选择并实现基于消息传递或RPC的进程间通信机制。此外,由于请求的目的地可能很慢或不可用,它们还必须编写代码来处理部分失败。
- 另一个挑战是分区数据库体系结构。更新多个业务实体的业务交易相当普遍。由于只有一个数据库,因此在单一应用程序中实现这类事务很简单。但是,在基于微服务的应用程序中,您需要更新不同服务拥有的多个数据库。
- 测试微服务应用程序也要复杂得多。
- 实现跨多个服务的更改较难。
使用API网关构建微服务
客户端与微服务之间的直接通信
从理论上讲,客户端可以直接向每个微服务发出请求。每个微服务都有一个公共终结点(https:// *serviceName* .api.company.name)。该URL将映射到微服务的负载平衡器,该负载平衡器在可用实例之间分配请求。要检索产品详细信息,移动客户端将向上面列出的每个服务发出请求。
不幸的是,此选项存在挑战和局限性。一个问题是客户端需求与每个微服务公开的细粒度API之间的不匹配。
客户端直接调用微服务的另一个问题是,有些服务可能使用不支持Web的协议。一个服务可能使用Thrift二进制RPC,而另一服务可能使用AMQP消息传递协议。两种协议都不是特别适合浏览器或防火墙的协议,并且最好在内部使用。应用程序应在防火墙外部使用HTTP和WebSocket之类的协议。
这种方法的另一个缺点是很难重构微服务。随着时间的流逝,我们可能想更改将系统划分为服务的方式。例如,我们可以合并两个服务或将一个服务拆分为两个或多个服务。但是,如果客户直接与服务进行通信,那么执行这种重构可能会非常困难。
使用API网关
API网关是服务器,是系统的单个入口点。它与面向对象设计中的“ 外观”模式相似。API网关封装了内部系统架构,并提供了针对每个客户端量身定制的API。它可能还具有其他职责,例如身份验证,监视,负载平衡,缓存,请求整形和管理以及静态响应处理。
API网关负责请求路由,组合和协议转换。来自客户端的所有请求都首先通过API网关。然后,它将请求路由到适当的微服务。API网关通常会通过调用多个微服务并汇总结果来处理请求。它可以在内部使用的HTTP和WebSocket之类的Web协议与Web不友好的协议之间转换。
API网关还可以为每个客户端提供自定义API。它通常为移动客户端提供粗粒度的API。
优缺点
使用API网关的主要好处是它封装了应用程序的内部结构。客户端不必调用特定的服务,而只是与网关进行对话。API网关为每种客户端提供特定的API。这减少了客户端与应用程序之间的往返次数。它还简化了客户端代码。
API网关也有一些缺点。它是另一个必须开发,部署和管理的高可用性组件。API网关也有成为开发瓶颈的风险。开发人员必须更新API网关才能公开每个微服务的端点。重要的是,更新API网关的过程应尽可能轻巧。否则,开发人员将被迫排队等待更新网关。
要点
- 性能和可伸缩性。
- 使用反应式编程模型。API网关通过简单地将请求路由到适当的后端服务来处理一些请求。它通过调用多个后端服务并汇总结果来处理其他请求。对于某些请求(例如产品详细信息请求),对后端服务的请求彼此独立。为了最小化响应时间,API网关应同时执行独立的请求。但是,有时请求之间存在依赖关系。
- 服务调用。基于微服务的应用程序是一个分布式系统,必须使用进程间通信机制。进程间通信有两种样式。一种选择是使用基于消息的异步机制。一些实现使用消息代理,例如JMS或AMQP。诸如Zeromq之类的其他公司则没有经纪人,服务可以直接通信。进程间通信的另一种形式是同步机制,例如HTTP或Thrift。系统通常会同时使用异步和同步样式。
- 服务发现。API网关需要知道与之通信的每个微服务的位置(IP地址和端口)。在传统的应用程序中,您可能需要对位置进行硬连线,但是在现代的基于云的微服务应用程序中,这是一个不小的问题。基础结构服务(例如消息代理)通常将具有静态位置,可以通过OS环境变量指定该位置。但是,确定应用程序服务的位置并非易事。应用程序服务具有动态分配的位置。而且,服务的实例集会由于自动缩放和升级而动态更改。因此,与系统中的任何其他服务客户端一样,API网关也需要使用系统的服务发现机制。
- 故障处理。实现API网关时必须解决的另一个问题是部分失败的问题。每当一个服务调用另一个响应缓慢或不可用的服务时,在所有分布式系统中都会出现此问题。API网关绝不应无限期地阻塞等待下游服务。但是,它如何处理故障取决于特定的情况以及哪个服务出现故障。
微服务架构中的进程间通信
在整体应用程序中,组件通过语言级方法或函数调用相互调用。相反,基于微服务的应用程序是在多台计算机上运行的分布式系统。每个服务实例通常是一个进程。因此,如下图所示,服务必须使用进程间通信(IPC)机制进行交互。
互动方式
为服务选择IPC机制时,首先考虑服务如何交互是很有用的。客户端⇔服务交互方式多种多样。它们可以沿两个维度进行分类。第一个维度是互动是一对一还是一对多:
- 一对一–每个客户端请求仅由一个服务实例处理。
- 一对多–每个请求由多个服务实例处理。
第二个维度是交互是同步还是异步:
- 同步–客户端期望服务及时响应,甚至在等待时可能会阻塞。
- 异步–客户端在等待响应时不会阻塞,并且响应(如果有的话)不一定会立即发送。
一对一交互:
- 请求/响应–客户端向服务发出请求并等待响应。客户希望响应能够及时到达。在基于线程的应用程序中,发出请求的线程甚至可能在等待时阻塞。
- 通知(也称为单向请求)–客户端向服务发送请求,但不希望或未发送答复。
- 请求/异步响应–客户端将请求发送到服务,该服务以异步方式答复。客户端在等待时不会阻塞,并假设响应可能不会在一段时间内到达。
一对多互动有以下几种:
- 发布/订阅–客户端发布通知消息,该消息由零个或更多感兴趣的服务使用。
- 发布/异步响应–客户端发布请求消息,然后等待一定时间以等待感兴趣的服务的响应。
不断变化的API
服务的API始终会随着时间而变化。在整体应用程序中,更改API和更新所有调用程序通常很简单。在基于微服务的应用程序中,即使API的所有使用者都是同一应用程序中的其他服务,也要困难得多。通常,您无法强制所有客户端与服务同步升级。
处理API更改的方式取决于更改的大小。某些更改是次要的,并且与以前的版本向后兼容。
但是,有时您必须对API进行重大的,不兼容的更改。由于您不能强制客户端立即升级,因此服务必须在一段时间内支持较旧版本的API。
故障处理原则
- 网络超时–永远不会无限阻塞,并且在等待响应时始终使用超时。使用超时可确保资源不会无限期地被占用。
- 限制未完成请求的数量–限制客户端可以使用特定服务的未完成请求的数量。如果已达到限制,则发出其他请求可能毫无意义,并且这些尝试必须立即失败。
- 断路器模式 –跟踪成功和失败请求的数量。如果错误率超过配置的阈值,请使断路器跳闸,以便进一步尝试立即失败。如果大量请求失败,则表明该服务不可用,并且发送请求毫无意义。超时后,客户端应重试,如果成功,则合上断路器。
- 提供回退–当请求失败时执行回退逻辑。例如,返回缓存的数据或默认值,例如空的建议集。
IPC技术
基于消息的异步通信
使用消息传递时,进程通过异步交换消息进行通信。客户端通过向其发送消息来向服务发出请求。如果期望该服务进行答复,则通过将单独的消息发送回客户端来进行答复。由于通信是异步的,因此客户端不会阻止等待答复。而是编写客户端,假定不会立即收到答复。
一条消息由标头(例如发送方之类的元数据)和一条消息主体组成。消息通过通道交换。任何数量的生产者都可以将消息发送到一个频道。同样,任何数量的使用者都可以从频道接收消息。有两种渠道,点对点渠道和发布订阅渠道。点对点通道将消息传递给正从该通道读取的消费者中的一个。服务使用点对点渠道进行前面所述的一对一交互样式。发布订阅通道将每个消息传递给所有附加的使用者。服务将发布-订阅通道用于上述一对多交互样式。
使用消息传递有很多优点:
- 使客户端与服务脱钩–客户端仅通过向适当的通道发送消息即可发出请求。客户端完全不知道服务实例。它不需要使用发现机制来确定服务实例的位置。
- 消息缓冲–使用同步请求/响应协议(例如HTTP),客户端和服务在交换期间必须都可用。相反,消息代理将写入通道的消息排队,直到消费者可以处理它们为止。例如,这意味着即使订单履行系统很慢或不可用,在线商店也可以接受来自客户的订单。订单消息只是排队。
- 灵活的客户端-服务交互–消息支持前面描述的所有交互样式。
- 显式进程间通信–基于RPC的机制试图使调用远程服务看起来与调用本地服务相同。但是,由于物理定律和部分失效的可能性,它们实际上是完全不同的。消息传递使这些差异非常明显,因此开发人员不会陷入错误的安全感中。
但是,使用消息传递有一些缺点:
- 额外的操作复杂性–邮件系统是又一个必须安装,配置和操作的系统组件。消息代理必须高度可用,否则系统可靠性会受到影响。
- 实现基于请求/响应的交互的复杂性–请求/响应式的交互需要一些工作来实现。每个请求消息必须包含一个回复通道标识符和一个相关标识符。服务将包含相关ID的响应消息写入回复通道。客户端使用相关性ID将响应与请求进行匹配。使用直接支持请求/响应的IPC机制通常会更容易。
同步请求
当使用基于请求/响应的同步IPC机制时,客户端会将请求发送到服务。该服务处理请求并发送回响应。在许多客户端中,发出请求的线程在等待响应时会阻塞。
服务发现
服务实例具有动态分配的网络位置。而且,服务实例集会由于自动缩放,故障和升级而动态更改。因此,您的客户端代码需要使用更复杂的服务发现机制。
客户端发现
使用客户端发现时,客户端负责确定可用服务实例的网络位置,并在它们之间进行负载平衡请求。客户端查询服务注册表,该服务注册表是可用服务实例的数据库。然后,客户端使用负载平衡算法来选择可用的服务实例之一并发出请求。
服务实例的网络位置在启动时会在服务注册表中注册。实例终止时,将从服务注册表中将其删除。通常使用心跳机制定期刷新服务实例的注册。
客户端发现模式具有多种优点和缺点。这种模式相对简单,除了服务注册表之外,没有其他活动部分。此外,由于客户端知道可用的服务实例,因此它可以做出智能的,特定于应用程序的负载平衡决策,例如一致地使用哈希。这种模式的一个重大缺陷是它将客户端与服务注册表耦合在一起。您必须为服务客户端使用的每种编程语言和框架实现客户端服务发现逻辑。
服务端发现
客户端通过负载平衡器向服务发出请求。负载平衡器查询服务注册表,并将每个请求路由到可用的服务实例。与客户端发现一样,服务实例在服务注册表中注册和注销。
HTTP服务器和负载平衡器(例如NGINX Plus和NGINX)也可以用作服务器端发现负载平衡器。
服务器端发现模式具有多个优点和缺点。这种模式的一大好处是发现细节从客户端被抽象出来。客户只需向负载均衡器发出请求。这样就无需为服务客户端使用的每种编程语言和框架实现发现逻辑。
服务注册表
服务注册表是服务发现的一个关键部分。它是一个数据库,其中包含服务实例的网络位置。服务注册表需要高度可用且最新。客户端可以缓存从服务注册表获得的网络位置。但是,该信息最终将过时,并且客户端将无法发现服务实例。因此,服务注册表由使用复制协议维护一致性的服务器群集组成。
Netflix Eureka是服务注册表的一个很好的例子。它提供了一个REST API,用于注册和查询服务实例。服务实例使用POST
请求注册其网络位置。每隔30秒,它必须使用PUT
请求刷新其注册。通过使用HTTP DELETE
请求或实例注册超时来删除注册。如您所料,客户端可以使用HTTP GET
请求来检索注册的服务实例。
服务注册方式
自我注册
使用自我注册模式时,服务实例负责在服务注册表中进行自身注册和注销。同样,如果需要,服务实例会发送心跳请求以防止其注册过期。
自注册模式具有各种优点和缺点。好处之一是它相对简单,不需要任何其他系统组件。但是,主要缺点是它将服务实例耦合到服务注册表。您必须使用服务使用的每种编程语言和框架来实现注册码。
第三方注册
使用第三方注册模式时,服务实例不负责在服务注册表中自行注册。取而代之的是另一个称为服务注册器的系统组件来处理注册。服务注册商通过轮询部署环境或订阅事件来跟踪对正在运行的实例集的更改。当发现新的可用服务实例时,它将在服务注册表中注册该实例。服务注册商还注销终止的服务实例。下图显示了此模式的结构。
分布式数据管理问题
整体应用程序通常具有单个关系数据库。使用关系数据库的主要好处是您的应用程序可以使用ACID事务,这提供了一些重要的保证:
- 原子性–原子地进行更改
- 一致性–数据库状态始终是一致的
- 隔离–即使事务是同时执行的,看起来它们还是串行执行的
- 耐用性–交易一旦提交,便不会撤消
结果,您的应用程序可以简单地开始事务,更改(插入,更新和删除)多行并提交事务。
我们转向微服务架构时,数据访问变得更加复杂。这是因为每个微服务拥有的数据是该微服务专用的,并且只能通过其API访问。封装数据可确保微服务松散耦合,并且可以彼此独立发展。如果多个服务访问相同的数据,则模式更新需要对所有服务进行耗时且协调的更新。
更糟糕的是,不同的微服务通常使用不同种类的数据库。现代应用程序存储和处理各种数据,而关系数据库并不总是最佳选择。对于某些用例,特定的NoSQL数据库可能具有更方便的数据模型,并提供更好的性能和可伸缩性。
第一个挑战是如何实现在多个服务之间保持一致性的业务交易。
第二个挑战是如何实现从多个服务检索数据的查询。
事件驱动架构
对于许多应用程序,解决方案是使用事件驱动的体系结构。在这种体系结构中,微服务会在发生显着事件(例如更新业务实体)时发布事件。其他微服务订阅了这些事件。当微服务收到事件时,它可以更新自己的业务实体,这可能导致发布更多事件。
您可以使用事件来实现跨多个服务的业务交易。交易包括一系列步骤。每个步骤都包含一个微服务,该微服务更新业务实体并发布触发下一步的事件。
重要的是要注意,这些不是ACID交易。它们提供的保证要弱得多,例如最终的一致性。此事务处理模型已称为BASE模型。
原子性
在事件驱动的体系结构中,还存在原子更新数据库并发布事件的问题。例如,订购服务必须在ORDER表中插入一行并发布订购创建事件。这两个操作必须原子完成。如果服务在更新数据库之后但在发布事件之前崩溃,则系统会变得不一致。
使用本地事务发布事件
诀窍是在存储业务实体状态的数据库中具有一个EVENT表,该表充当消息队列。应用程序开始(本地)数据库事务,更新业务实体的状态,将事件插入EVENT表,然后提交事务。单独的应用程序线程或进程查询EVENT表,将事件发布到Message Broker,然后使用本地事务将事件标记为已发布。
挖掘数据库事务日志
在没有2PC的情况下实现原子性的另一种方法是,事件由挖掘数据库事务或提交日志的线程或进程发布。该应用程序更新数据库,这导致更改被记录在数据库的事务日志中。事务日志挖掘器线程或进程读取事务日志并将事件发布到Message Broker。
事务日志挖掘具有各种优点和缺点。一个好处是,它保证了每次更新都可以发布事件,而无需使用2PC。事务日志挖掘还可以通过将事件发布与应用程序的业务逻辑分开来简化应用程序。一个主要的缺点是事务日志的格式是每个数据库专有的,甚至可以在数据库版本之间进行更改。
使用事件源(类似区块链账本)
通过使用根本不同的,以事件为中心的方法来持久化业务实体,事件采购无需2PC就可以实现原子性。该应用程序不是存储实体的当前状态,而是存储一系列状态更改事件。该应用程序通过重播事件来重建实体的当前状态。只要业务实体的状态发生变化,就会在事件列表中附加一个新事件。由于保存事件是单个操作,因此它本质上是原子的。
事件源有几个好处。它解决了实现事件驱动的体系结构中的关键问题之一,并使得在状态改变时可靠地发布事件成为可能。结果,它解决了微服务体系结构中的数据一致性问题。另外,由于它保留事件而不是域对象,因此它可以避免对象关系阻抗不匹配的问题。事件源还提供了对业务实体所做的更改的100%可靠的审核日志,并使得可以实施临时查询来确定实体在任何时间点的状态。事件源的另一个主要优点是您的业务逻辑由交换事件的松散耦合的业务实体组成。这使得从单片应用程序迁移到微服务架构变得容易得多。
事件源也有一些缺点。这是一种不同且陌生的编程风格,因此存在学习曲线。事件存储区仅直接支持通过主键查找业务实体。您必须使用命令查询职责隔离(CQRS)来实现查询。结果,应用程序必须处理最终一致的数据。
部署策略
每个主机模式有多个服务实例
部署微服务的一种方法是使用“ 每个主机多个服务实例”模式。使用此模式时,您将配置一个或多个物理或虚拟主机,并在每个虚拟或虚拟主机上运行多个服务实例。在许多方面,这是应用程序部署的传统方法。每个服务实例在一个或多个主机上的一个知名端口上运行。
优点
- 资源使用相对高效。多个服务实例共享服务器及其操作系统。如果一个进程或进程组运行多个服务实例,例如共享同一个Apache Tomcat服务器和JVM的多个Web应用程序,则效率更高。
- 部署服务实例相对较快。您只需将服务复制到主机并启动即可。
缺点
- 除非每个服务实例是一个单独的进程,否则服务实例几乎没有隔离。尽管可以准确地监视每个服务实例的资源利用率,但是不能限制每个实例使用的资源。行为异常的服务实例可能会消耗主机的所有内存或CPU。
- 部署服务的运营团队必须知道如何执行服务的具体细节。服务可以用多种语言和框架编写,因此开发团队必须与操作共享许多细节。这种复杂性增加了部署期间出错的风险。
每个主机模式的服务实例
使用此模式时,每个服务实例都在其自己的主机上独立运行。此模式有两种不同的专业化:每个虚拟机的服务实例和每个容器的服务实例。
每个虚拟机模式的服务实例
优点
- 每个服务实例都可以完全隔离地运行。它具有固定数量的CPU和内存,无法从其他服务中窃取资源。
- 可以利用成熟的云基础架构。
- 封装了服务的实现技术。将服务打包为VM后,它将变成一个黑匣子。VM的管理API成为用于部署服务的API。部署变得更加简单和可靠。
缺点
- 资源利用效率较低。每个服务实例都有整个VM(包括操作系统)的开销。
每个容器模式的服务实例
当您使用“ 每个容器的服务实例”模式时,每个服务实例都在其自己的容器中运行。容器是操作系统级别的虚拟化机制。容器由在沙箱中运行的一个或多个进程组成。从进程的角度来看,它们具有自己的端口名称空间和根文件系统。您可以限制容器的内存和CPU资源。一些容器实现也具有I / O速率限制。容器技术的示例包括Docker和Solaris Zones。
无服务器部署
AWS Lambda是无服务器部署技术的示例。它支持Java,Node.js和Python服务。要部署微服务,请将其打包为ZIP文件,然后将其上传到AWS Lambda。您还提供元数据,元数据除其他事项外,还指定为处理请求(又称为事件)而调用的函数的名称。AWS Lambda自动运行您的微服务的足够实例来处理请求。您只需根据花费的时间和消耗的内存为每个请求付费。
将单片应用重构为微服务方法
不再增大整体项目
在实现新功能时,不应将更多代码添加到整体中。相反,此策略的主要思想是将新代码放入独立的微服务中。
前后端分离
缩小整体应用程序的一种策略是将表示层与业务逻辑和数据访问层分开。典型的企业应用程序至少包含三种不同类型的组件:
- 表示层–处理HTTP请求并实现(REST)API或基于HTML的Web UI的组件。在具有复杂用户界面的应用程序中,表示层通常是大量的代码。
- 业务逻辑层–作为应用程序核心并实现业务规则的组件。
- 数据访问层–访问基础结构组件的组件,例如数据库和消息代理。
提取服务
第三种重构策略是将整体中的现有模块转变为独立的微服务。每次提取模块并将其转换为服务时,整体都会收缩。一旦转换了足够多的模块,整体将不再是问题。它要么完全消失,要么变得足够小,以至于它只是另一种服务。
提取哪些模块
大型,复杂的整体应用程序由数十个或数百个模块组成,所有这些模块都是提取的候选对象。找出首先要转换的模块通常很困难。一个好的方法是从几个容易提取的模块开始。这将为您提供一般的微服务经验,尤其是提取过程的经验。之后,您应该提取那些将为您带来最大利益的模块。
如何提取
提取模块的第一步是定义模块和整体之间的粗粒度界面。它最有可能是双向API,因为整体将需要服务拥有的数据,反之亦然。由于模块和应用程序其余部分之间存在复杂的依赖关系和细粒度的交互模式,因此实现这样的API通常具有挑战性。由于域模型类之间存在大量关联,因此使用域模型模式实现的业务逻辑尤其难以重构。您通常需要进行重大的代码更改才能打破这些依赖性。下图显示了重构。
一旦实现了粗粒度接口,就可以将模块变成独立的服务。为此,您必须编写代码以使整体组件和服务能够通过使用进程间通信(IPC)机制的API进行通信。