微服务开发及部署_《微服务:从设计到部署》总结笔记

英文原文出自:https://www.nginx.com/blog/introduction-to-microservices/

笔记首发于我的语雀,有空来帮我点个稻谷呀。

PS: 知乎对markdown支持真是各大平台里最蔡的orz

1 简介

monolithic app(都写在一起)的劣势

08a50e00ba6f4e08b6f9f99fbf373764.png
  • 代码依赖极其复杂
  • 难以理解
  • 启动时间变长
  • CPU密集型逻辑和内存消耗型逻辑无法拆开优化,浪费云计算资源
  • 可靠性变差:一处内存泄漏,全家升天
  • 难以做架构升级,例如更换语言或框架

微服务

ba36ed1338df5b5ae3b723d5fe5dfcf2.png
  • 把你的巨型应用拆解成小型的独立的互联的服务们
  • 服务之间互相之间提供服务和消费服务
  • 客户端通过API网关来调用服务们
  • 运行时的一组服务可以是多台物理机上的多个container
  • 服务拥有自己的独立的数据库存储,这意味着服务可以选择自己最适合的数据库类型。

微服务的优势

  • 拆解了复杂的应用,单个服务更好迭代和维护。
  • 每个服务可以自由地选择最合适的技术栈。
  • 服务可以分开部署,可以更加自由地进行持续集成。

微服务的劣势

  • 必须要开发服务间通信机制
  • 需要经常处理“部分失败”的情况,因为一个请求现在是是一组服务的调用。
  • 数据库结构设计和操作,例如跨表操作,数据一致性(事务)问题等。
  • 测试更加困难,因为要测试你的服务,你必须先启动其他服务。
  • 在维护或更新时,你通常需要更新多个服务。
  • 服务数量众多时,部署也是一个难题。

2 构建微服务:使用API网关

  • 【背景】例如在做一个电商宝贝详情时,你客户端需要调:购物车服务、订单服务、类目服务、评价服务、库存服务、发货服务、推荐服务....

不使用API网关,挨个调过去

  • 客户端通过移动网络发送N个网络请求,是非常不靠谱的
  • 服务可能用的并非是web友好的协议,例如thrift,无法提供服务。
  • 让重构微服务们变得困难,因为它们被客户端直接依赖了

使用API网关

  • API网关可以调用多个微服务, 然后提供一个“item/detail?id=xxxx”的统一接口
  • API可以顺便做负载均衡、监控、授权和认证等等多种统一功能
  • 【优】封装了应用的内部结构设计,客户端只需要和API网关通信
  • 【优】可以通过服务,自由组合出最适合客户端的API
  • 【劣】你需要单独开发、部署、管理API网关
  • 【劣】API网关可能成为开发的瓶颈,例如开发者暴露新的服务时要更新到API网关

实现API网关

  • 为了API网关的性能和可扩展性,应该支持异步NIO。
  • 在JVM上你可以使用Netty、Vertx、Spring Reactor等NIO框架
  • 在非JVM上你可以使用node.js来实现NIO
  • 你还可以使用Nginx Plus(要钱的)

使用Reactive Programming

  • 对于互不相关的服务,应该同时一起调用。有先后顺序的服务可能要定义好前置和后置。
  • 使用传统的异步+回调的方式来书写组合服务的代码会让你很快陷入回调地狱。
  • Java、Scala、JS中都有响应式的方案,你还可以使用ReactiveX来书写这类代码。

服务调用:

通常有两种调用方式

  • 异步——消息队列
  • 同步——HTTP或者Thrift等

服务发现

  • 过去你通常手动控制服务的ip地址,但是在微服务体系中,你无法做到。
  • 由于各个服务的随时扩容和升级,它们的地址都是在动态变化的。
  • 所以API网关需要有服务注册,服务发现的能力。不论是server-side发现,还是client-side发现。

处理部分失败

  • API网关不可以因为下游的失败而block住,它要根据场景来决定如何处理错误。
    • 例如只是商品推荐服务挂了,商品详情页接口应该仍然返回其他所有数据
    • 如果是商品基础信息挂了,商品详情页接口应该反馈错误给到客户端。
  • 可以返回硬编码打底数据,或者缓存数据。(API网关做, 服务无感知)
  • 安利了一下Netflix Hystrix, 但是似乎已经处于维护状态不再更新了。

3 构建微服务:微服务架构中的跨进程通信

  • 【背景】在monolithic应用中,组件之间互相通过语言级别的方法就可以进行调用,但是在微服务应用中,这些组件都被分布式地部署在不同的机器上的不同容器里。基本每个服务都是一个进程,所以服务们不得不使用跨进程通信(IPC)机制。

交互方式

  • 一对一通信 vs 一对多通信
  • 同步 vs 异步

65186e289254dbc967f43687ea2e888a.png
  • request/response: client发起请求然后同步等待响应结果。
  • notifications:client发出一个请求,但并不需要返回,也不等待。
  • request/async response: client发出一个请求,但响应是异步的,在等待响应的过程中,client并不阻塞。
  • publish/subscribe: client发出一条消息,被一个或多个感兴趣的服务消费。
  • public/async responses: client发出一个请求消息,然后等待一段时间来接收感兴趣的服务的返回。
  • 每个服务都可能用上述多种交互方式,例如下图

6556a46d7da7696f976d78721281fece.png

定义API

  • 服务的API是一种服务和它的客户端们之间的约定。
  • 使用某种接口定义语言(IDL)非常重要,例如Protobuf。
  • 你甚至可以使用API-first这种方式,也就是先定义API再实现它,来进行开发。
  • API定义依赖于你使用的IPC机制,例如使用消息那么API就需要消息通道和消息类型。

更新API

  • 在monolithic的app里,你一般改了API后,去代码里直接改所有的调用处.
  • 在微服务体系里, 更新API会困难得多,你没办法让你服务的消费者挨个更新。你可能要增量地添加新版本的服务,线上可能同时存在两个版本的API,这是一种非常重要的更新策略。
  • 为了实现上述能力,一种可行的方式是添加版本号,让多个版本的API同时存在。

处理部分失败

  • 在微服务体系中,部分失败是非常常见的,因为所有服务们都在不同的进程里。
  • 假设有一个场景,你的产品详情页里需要使用推荐服务,而此时推荐服务挂掉了:

cc20dae13b7956b44e66245dc3b3438a.png

如果你按照一直等待去设计,那么很有可能会消耗掉你所有的线程,导致产品详情服务彻底挂掉

  • 下面有4条由Netflix推荐的部分失败错误处理策略。
    • 网络超时:永远不要一直等待,一定要有超时重试/超时失败机制。
    • 限制等待中请求数量:等待中的请求应该有一个上限值,一旦上限达到了,就不应该再处理请求,这些超出限额的请求应该立即失败。
    • 短路模式:当失败率达到一定程度时,后续的请求应该立即失败,不再请求。短路后,应该每隔一段时间重试,如果重试成功,那么就不再短路。
    • 提供降级:提供失败时的降级逻辑,例如获得缓存数据。

IPC机制

  • 异步、基于消息的IPC机制
    • client向某服务发送一条消息,但并不等待它。如果这次调用需要返回,被调用服务会也异步通过消息返回给client。client完全基于异步,也就是不等待的方式来编写。
    • 消息一般由header和body构成,并且通过channel来交换。
    • 两种消息channel:1. 点对点 2. pub/sub
    • 案例:

f8870c82c384472428c105651ada1c58.png
    • 出行服务发一条消息“一个新出行创建了!”到名叫“新的出行”的pub/sub型channel中,通知所有感兴趣的服务(比如派单服务)。派单服务找到了合适的司机以后,发送一条消息“司机接单了!”到一个名叫“派单”的pub/sub型channel中,通知所有感兴趣的服务。
    • 消息队列实现有非常多种:RabbitMQ、Kafka、ActiveMQ.....
    • 用消息机制的优点:
      • 把服务的consumer和provider通过消息解耦了。
      • 消息可堆积,比起实时调用有更高的容错率。
      • 调用方式也解耦了,consumer和provider之间只需要遵守消息约定即可。
    • 用消息机制的缺点:
      • 更高的运维复杂性,消息系统也是一个要维护的系统啊!
      • 要实现请求-响应这种同步请求会更加麻烦
      • 【作者补充】消息队列的高可用、不被重复消费、可靠性、顺序性都是非常复杂的课题。
  • 同步、请求/响应型的IPC
    • 通常来说,一个线程会在等待请求时阻塞。有些可能已经通过Future或者Rx Observables这种Ractive Pattern的东西把它变成了异步的方式。
    • 但是在这种类型的IPC里,client通常希望请求能够尽快及时地返回。
    • 常见的主要由两种协议:Rest、Thrift
    • 基于HTTP方式的协议(Rest)的好处:
      • 简单熟悉
      • HTTPAPI可以直接在浏览器、Postman、curl等多种环境下测试。
      • 直接支持 请求/响应 模式
      • 没有任何中间代理,系统架构简单
    • 基于HTTP方式的协议(Rest)的坏处:
      • 只支持 请求/响应 模式
      • provider和consumer都必须活着不能挂
    • 基于HTTP方式的一些IDL或平台:RAML、Swagger
    • Thrift
      • 使用一个C风格的IDL来定义API,并且使用Thrift编译器来生成客户端和服务端代码模板。
      • 支持C++、Java、Pyhton、Ruby、Erlang、Node.js
      • Thrift方法可以返回空值,也就是可以实现单向的通知,并不一定要返回。
      • 支持JSON/二进制等格式
  • 消息格式
    • 不管用什么方式和语言,最好选择语言无关的消息格式,你无法保证将来你不换语言。
    • 在文本和二进制中,tradeoff大概就是包大小or人类可阅读性。

4 微服务架构中的服务发现

为什么使用服务发现?

  • 你以前,你可能把要调用的IP放在配置里,然后直接调用
  • 但是在云时代的微服务应用中,你应该不太能这么做,看图:

14ea206b54d931803d93a48f7ecc29f5.png
  • 你会发现,服务实例们都有着动态分配的网络位置,而且这些位置由于扩容、更新等等还在变化。
  • 目前主要由两种服务发现:client侧服务发现、server侧服务发现

client侧服务发现

93e16b7822adbc5f3436c09a0b122a16.png
  • 这种模式下,consumer决定了所调用服务的网络地址。所有的服务provider,都把自己的网络地址,注册到服务注册中心,consumer拿到了一坨ip地址之后,自己选择一个load-balancing策略,然后调用其中一个。
  • 服务Provider在启动时向注册中心注册服务,在结束进程时给注册中心注销服务。服务的健康通过心跳机制来进行保障。
  • Netflix OSS是一个client侧服务发现的好例子,Netflix Eureka是一个REST型服务注册中心。
  • Netflix Ribbon是一个IPC客户端,能够和Eureka合作,提供load-balancing。
  • 优点:1. 简单直接 2. 由于consumer决定了路由,可以做灵活的、应用特定的负载均衡策略
  • 缺点:耦合了consumer和服务注册中心,你必须给每个编程语言实现consumer侧的服务发现逻辑。

server侧服务发现

9a38cb9c9c06e1c9a908cb8d1dd87965.png
  • consumer通过一个load balancer来请求provider,load balancer要请求服务注册中心来获得所有可提供服务的实例以及它们的网络地址。也就是load-balancer决定了到底要请求谁。
  • 亚马逊的ELB就是一个server侧服务发现路由。ELB把服务注册直接做到了自己里面。
  • Nginx也可以充当server侧服务发现中的load balancer,比如这篇文章里,你可以使用Consul Template来动态更新nginx的反向代理。
  • 一些部署环境例如K8S或者Marathon,在集群里的每个宿主上都运行Proxy,这个Proxy就扮演了server侧服务发现中的load-balancer。如果你想给一个服务发请求,一个consumer必须通过proxy来调用。
  • 优点:
    • consumer和服务发现是完全解耦的,无脑请求load-balancer就好了。
    • 不用再给每个语言实现一套load-balancing机制了。
    • 有些部署环境甚至直接提供了这种load-balancing服务。
  • 缺点:如果没有提供好的load-balancing服务,你就要自己实现。

服务注册中心

  • 概念详解
    • 它就是一个服务实例们网络位置的数据库。
    • 它必须高可用,并实时更新。
    • 它其实是一坨实例们,这些实例们要通过某种副本协议来保持一致性
  • 比如Netflix Eureka,它是基于REST的。它通过POST方法来注册服务,每30秒通过PUT方法刷新一次,通过DELETE方法删除服务,通过GET方法来获得一个可用的服务实例。
  • Netflix为了实现服务注册的高可用性,通过运行N个Eureka实例,使用DNS的TEXT记录来存储Eureka集群的配置,这个配置也就是一个可用区->一组网络地址的map。当一个新的Eureka启动,它就请求DNS来获得Eureka集群配置,并且给它自己一个新的IP。Eureka的clients,services就请求DNS来发现Eureka实例们的地址,并且会尽可能选相同可用区的Eureka实例。
  • 其他的服务注册中心们还有
    • etcd:K8S和Cloud Foundry用的它
    • consul
    • Zookeeper:大家最熟悉了
  • 在K8S、Marathon、AWS中并没有显式的、单独的服务注册中心,因为它们是内置的基础设置。

服务注册的方式

  • 目前主要有两种方式来处理服务的注册和注销:自注册模式和三方注册模式。
  • 自注册模式

9500904a8a217b048cd0bb064d09b93b.png
    • 这种情况下,服务实例自己负责注册和注销它提供的服务,同时服务自己还需要不停地发送心跳包来阻止自己的注册过期。
    • Netflix OSS Eureka client是一个很好的自注册例子。Spring Cloud中也可以直接使用注解来实现注册方式。
    • 优点:简单直接、不需要其他系统组件。
    • 缺点:把服务实例和注册中心耦合起来了,你要实现各个编程语言的注册代码。
  • 三方注册模式

591902fab12d32c117772a1777582d02.png
    • 顾名思义,服务实例们要向一个注册管理者(registrar)来进行注册,而注册管理者也通过健康检查机制来跟踪服务实例们的情况,随时注销挂掉的服务实例。
    • 有一个开源的注册管理者叫做Registrator ,它能自动注册和注销以docker container方式部署的服务实例。Registrator支持etcd和Consul。
    • 还有一个有名的注册管理者是NetflixOSS Prana,它主要是为了非JVM语言设计的,它是一个sidecar应用,也就是说它跟着每一个服务实例运行。Prana和Eureka配合,向Eureka进行注册和注销服务。
    • 注册管理者,也是很多部署环境的内置基础设施,例如在AWS EC2和K8S中都是。
    • 优点:服务和服务注册中心解耦、你也不再需要去实现特定语言的注册逻辑。
    • 缺点:如果部署环境没提供注册管理者,那你就要自己实现。

5 微服务中事件驱动的数据管理

背景

  • 在一个monolithic的应用中,使用关系型数据库的一个好处是,你可以使用符合ACID原则的事务。
  • ACID原则
    • Atomicity 原子性: 变更要么都成功,要么都失败。
    • Consistency 一致性:数据库的状态永远是一致的。
    • Isolation 独立性:事务之间不会有交错执行的状态(因为可能会导致数据不一致)。
    • Durability 持久性:事务成功后,修改是持久的。
  • 另一个好处是你可以使用SQL,你可以轻松地进行多表操作,数据库帮你解决大部分性能问题。
  • 遗憾的是当切换到微服务架构以后,这些好处你就不再能够享受,因为所有的数据都被各自的微服务所拥有,也就是说他们拥有各自独立的数据库,数据的访问只能通过API层面来进行。
  • 来吧,更糟糕的是:不同的微服务可能还用了不同类型的数据库。例如NoSQL系列的,graph型的(例如Neo4j)。所以微服务架构中通常混用各种类型的数据库,我们称之为polyglot persistence 。

挑战1:如何在多个微服务中实现事务

  • 假设有一个在线的B2B商店,“客户服务”维护了客户信息,“订单服务”管理订单们,并且保障一个新订单不会用完客户的余额。
  • 在过去的monolithic版的应用中,我们用一个事务,就能搞定“检查余额够不够+创建订单”这件事。
  • 但是在微服务体系中,我们的ORDER表和CUSTOMER表现在是私有的:

7c1f249ae9bfb46d1a29920f74dc2fea.png
  • 订单服务并无法直接访问CUSTOMER表,它只能通过客户服务提供的API来间接修改CUSTOMER表。也许你可以使用分布式事务,也叫two-phase-commit(2PC)来解决。但是在现代化的应用中,你很难使用2PC。著名的CAP theorem告诉我们,在ACID中你要抉择要C还是A,而一般更好的选择都是A。而且,在很多NoSQL的数据库中根本不支持2PC。

挑战2:如何进行多表查询

  • 假设你的客户端需要展示客户的信息以及他所有的订单。
  • 如果订单服务提供了根据客户id查它订单的服务,那么你可以直接调这个服务。
  • 但是如果订单服务,没有提供这个服务,比如只支持按照订单id查询时,你该怎么办呢?

事件驱动架构

  • 在大部分应用中,解决方案就是使用数据驱动架构。当一个值得注意的事情发生了以后,这个微服务发出一个事件,比如“它更新了某个entity”这件事。其他对此感兴趣的微服务订阅这些事件,当它们收到这件事情时,就做它们自己的业务逻辑,比如把自己对于这个entity的冗余字段也更新一下。
  • 你可以使用事件来实现跨越多个服务的事务,这样的一个事务,包括的许多步骤,每个步骤包括一个微服务更新自己的业务逻辑entity,并且发出一个事件通知下一些微服务。来看下面这个流程:
    • 图1:订单服务创建了一个新订单,发出一个“一个订单创建辣!!”的消息

b88021cd15475692009398317b35a12e.png
  • 图2:客户服务消费这个“一个订单创建辣!!”消息,并且给这个订单预留了余额,然后发送一个“已预留余额”事件

667cf9c7d87104dbf4ab84577df5ee06.png
  • 图3:订单服务消费“余额已预留”事件,并且把订单状态设为“创建成功”

6a33196fa7c1044862e21da871ab1f88.png
- 这里面需要假设
- 每个服务自动地更新数据库,并且发布事件
- 消息代理必须保证事件至少都被交付了一次
  • 尽管如此你也只是实现了事务,没有实现ACID的事务,这仅仅是提供了弱保障,例如eventual consistency. 这样的事务模型被称为BASE model.
  • 【解决查询问题】你还可以使用事件来维护一个额外"物料化视图",这个视图包含的预先join好的,来自多个微服务的数据。例如可以专门有一个“客户订单视图”服务,来专门订阅相关事件(客户服务产生的事件、订单服务产生的事件等),然后更新这个视图。

7e1579d9768b025d7f4023beaedb131b.png
    • 你可以使用文档化数据库例如MongoDB来实现这个额外视图,并且给每一个客户都存一个document。这样当有一个客户来的时候,你可以光速地反馈这个客户的相关订单,因为你已经事先存好了。
  • 事件驱动架构的优点
    • 它使得跨多个服务的普通事务得以实现。
    • 它让应用可以通过“物料化视图”实现快速查询。
  • 事件驱动架构的缺点
    • 编程将更加更加复杂
    • 你必须得实现补偿事务(回滚等)来从应用级错误中恢复。例如:当你检查余额失败时,你应该取消订单。
    • 应用将要面对数据不一致的情况,例如“物料化视图”中的数据并非最新的。而且还要处理事件相关的各种问题,比如重复消费(幂等性),漏消费等问题(其实也就是消息队列常见问题系列)。

实现原子性

  • 【背景】在事件驱动架构里,你还会遇到一个原子性问题
    • 假设订单服务要插入订单表,同时发出一个“订单创建辣!”事件,这两步操作必须要原子地完成。
    • 假设服务在插入了订单表,发出事件之前挂掉了,那么系统就会出现一致性问题。
    • 通常标准的做法是使用分布式事务,但是如上面所说(比如CAP theorem),这并不是我们想做的。
  • 使用本地事务完成发送事件
    • 这个操作有个术语叫做multi‑step process involving only local transactions
    • 借助本地事务,其实就是你要一个本服务的事件表,它的作用类似消息队列:

77804304667845fb12687674dd81c269.png
      • 事务1:订单服务插入一个新订单,并且在事件表里插入一个新的订单创建事件。
      • 事务2:事件发送线程读取事件表找到未发送的事件,更新时间并标记事件已发送。
      • 【笔记】这样就保障了“插表+创未发事件”和“事件发送并更新事件状态”的原子性,首先这两个因为是事务所以一定全成功或者全失败,其次是万一在两者之间的时候挂掉了,重启以后事件发送线程还会继续事务性地读取未发事件并重新发送。
    • 优势:不依赖2PC(分布式事务)也能保障每次更新的原子性,发送的也是仍然是业务逻辑层面的事件。
    • 劣势:写代码容易漏写(??我反正没看懂原文啥意思,因为复杂就特么能漏写??)。当使用NoSQL型数据库时会很困难,因为它们的查询能力和事务性都比较差。
  • 挖掘数据库事务日志
    • 另一种不用2PC实现原子性的方法就是,有一个单独的线程去挖掘数据库事务或者commit日志。也就是数据库被更新了以后,就有日志,而这个挖日志线程就读日志,然后发消息给消息代理。

7b35496c26ad5eb2db22a0668fc05127.png
    • 一种实现方式就是使用开源的LinkedIn Databus ,它可以挖掘Oracle事务日志并发送消息。LinkdeIn用它来保障多个数据存储的一致性。
    • 另一个例子是streams mechanism in AWS DynamoDB,是一种托管的NoSQL数据库,它有一种DynamoDB流,按照时间顺序记录了24小时内的增删改查。应用可以读这些变化来发送事件。
    • 优势:保障了每次更新都可以发送事件。事务日志也可以简化应用逻辑,因为把事件发送和应用业务逻辑拆分开来了。
    • 劣势:每个数据库的事务日志都不太一样,并且随着数据库版本变化,而且你还需要做高层业务逻辑到底层数据库日志的转换。
  • 使用数据溯源
    • 数据溯源是:不直接存储实体现在的状态,它则是存储数据变化的事件(是不是想到了Redux的Action和时间旅行?)。应用会回放所有的数据变化事件来更新实体的状态。相当于先事件再业务了,所以保障了原子性。
    • 举个 ,按照传统的做法,一个ORDER实体对应数据库里ORDER表,但是在数据溯源方式里,表里存的都是状态变化:订单创建、确认、发货、取消等等。

0e19a11506d9975510807a29162dbf0e.png
    • 事件都存储在了Event Store,这个Event Store其实就像架构里的事件代理。它提供了其他服务订阅这些事件的API。
    • 优势:
      • 顺便就实现了事件代理,一举两得。
      • 它用一种微服务的方式解决了数据一致性问题。
      • 由于是持久化事件而非实体,他基本避免了 object‑relational impedance mismatch problem。
      • 业务逻辑和业务实体耦合程度低,这让它具备更好的迁移性。
    • 劣势:它是一种完全不一样的编程范式,有陡峭的学习曲线。Event Store只直接支持按主键查询业务实体。你必须使用Command Query Responsibility Segregation来实现查询。

6 选择一种微服务部署策略

动机

  • 部署monolithic应用时,你通常是在N台物理机(或虚拟机)上部署M个相同的服务实例。这种部署要比微服务要更直接、简单。
  • 微服务通常包括上百的服务,并且它们用了不用的语言和框架。为了让某个功能运行,你可能要起一坨服务才能work。每个服务都有自己单独的部署、资源消耗、扩容、监控的方式。所以微服务的部署虽然很困难,但是也必须要保障快速、可靠、低耗。

每个宿主机多个服务实例模式

  • 这种模式就是,你在多台机器上部署多个不同的服务实例,不同的服务实例在不同的端口上。

b6bfc094e0131f2c0a259aa64a2d236f.png
  • 这种模式还有很多变种,例如
    • 每一个服务实例是一个或一组进程
      • 部署一个Java服务实例作为一个web应用放在Apache Tomcat 上面。
      • 一个Node.js服务实例可能包含一个父进程和多个子进程。
    • 在同一个进程或进程组运行多个服务实例
      • 你可以在同一个Tomcat上运行多个java web应用
      • 在OSGI容器上运行多个OSGI的bundle
  • 优势
    • 资源利用相对合理有效,它们共享操作系统和服务器。例如:多web应用共享tomcat和JVM。
    • 部署非常快速,你只要把代码或者构建产物copy到机器上然后启动就行了。
    • 启动服务非常迅速,因为服务进程就是它自己,它就启动自己就好了。
  • 劣势
    • 服务实例之间没有太多隔离,就算你可以监控每个服务用了多少资源,你没办法限制每个服务实例能用多少。有一些服务也许可以消耗掉所有的内存和CPU。如果是同一个进程下的多个服务实例,就更加没有隔离了,他们可能共享比如JVM堆。
    • 运维团队必须知道部署你这个服务的细节,因为不同的应用实现方式都不一样,语言、框架、依赖、环境都可能不太一样。这会增加运维的风险。

每个宿主机一个服务实例模式

  • 另一种方式就是,每个宿主机只有一个服务实例。这种模式下主要由两种子模式:每个虚拟机一个服务实例和每个容器一个服务实例
  • 每个虚拟机一个服务实例
    • 你把每一个服务都打成一个虚拟机镜像,例如 Amazon EC2 AMI。每个服务实例都是一个运行这个镜像的虚拟机

6e5e09c66ca947e98b75b25f9a715be7.png
    • Netflix就用这种方式来部署它的视频串流服务,它使用 Aminator来把每个服务打成AWS EC2 AMI,每个服务实例运行在一个EC2身上。
    • 还有很多其他工具你可以用来构建你自己的VM,你可以用一些持续集成工具(比如Jenkins)来调用Animator来打包成虚拟机镜像。Packer.io 也是一个不错的选择,它支持各种虚拟化技术,不仅只有EC2。
    • CloudNative有一个叫Bakery的服务,它是一个用来创建EC2 AMI的SaaS。你可以配置你的CI服务器来调用Bakery(当然前提是你过了你的测试),Bakery会帮你把你的服务打包成成一个AMI。
    • 优势
      • VM之间相互独立,拥有固定的CPU和内存,不会互相攫取资源。
      • 可以借助成熟的云计算基础设施,例如AWS,来快速扩张和部署。
      • 把你的服务封装成了VM以后,就是黑盒了,部署不需要关心细节。
    • 劣势
      • 资源利用更加低效,因为有时候服务并不能榨干虚拟机的性能,而虚拟机就会有性能剩余。
      • 基于上一点,很多云服务按照虚拟机数量收费,并不管你的虚拟机忙还是闲。
      • 当部署新版本的虚拟机时通常很慢,因为虚拟机大小通常都是比较大的,并且初始化,启动操作系统等等也需要时间。
      • 维护虚拟机本身也是一个很重的担子。
  • 每个容器一个服务实例模式
    • 每个服务实例运行在它自己的容器里。容器是一种 virtualization mechanism at the operating system level,你可以简单理解为更轻量级更厉害的虚拟机。一个容器包含了一个或多个运行在沙箱里的进程。你可以限制每个容器的CPU和内存等资源。常见的容器技术有Docker和Solaris Zones.

55679b6f4317f1e9e6a398ed50bd0ba4.png
    • 为了使用这种模式,你要把你的服务打包成一个容器镜像,容器镜像包含了一个完整的linux文件系统,服务本身的代码,相关的依赖等,一切的目的都是为了让这单个服务跑起来。比如你要打包一个java应用的容器镜像,你可能就需要一个java运行时,一个Tomcat,以及你编译后的jar包。
    • 当你需要在一个机器上运行多个容器的时候,你可能就需要服务编排工具了,例如Kubernetes 或者 Marathon。它基于容器对资源的需求以及现在剩下的资源,来决定容器到底放哪里。
    • 优势
      • 和VM一样,隔离了你的每个服务实例。
      • 和VM一样,封装了你使用的技术,容器可以不关心细节就部署。
      • 不同于VM,容器更轻,构建也更快,启动也更快.
    • 劣势
      • 容器经常部署在按VM数量收费的IaaS上,也就是说,你可能要花额外的钱。
  • 容器和VM的边界正在慢慢消失,两者正在互相靠拢。

Serveless部署

  • AWS Lambda就是一个非常典型serveless部署,他支持你用各种语言写代码,这些代码作为一个函数,可以直接响应请求或事件。AWS会帮你解决下面的机器、内存等物理需求,你只需要关心业务逻辑就好了。
  • 一个Lambda函数是一个无状态服务,它可以直接和AWS其他的服务交互,比如当S3插进来一个新东西的时候,可以让一个函数响应并做后处理。函数还可以调用其他三方服务
  • 你有这么些方法可以调用一个函数
    • 直接通过你的服务来请求。
    • 通过AWS其他服务产生的事件触发。
    • 提供给AWS的Api gateway来提供HTTP服务。
    • 周期性的运行,基于一种cron的时间表。
  • 你能看到,AWS Lambda是一种非常方便的部署微服务的方式,并且它还基于请求收费,你只需要为你的使用量付费。你也不用关心底层的IT基础设施.
  • 劣势
    • 它不适合用来做长时间运行的服务,比如要从某个消息代理持续消费消息.
    • 请求必须在300秒以内完成.(AWS的限制吧)
    • 服务必须无状态,而且上一次函数调用和下一次函数调用很可能不在一台机器上。

7 从一个单应用重构为微服务

  • 你最好不要使用“Big Bang”策略,也就是完全重写你的服务。既危险又耗时。
  • 你可以增量性地重构你的应用,你可以逐渐地构建一个基于微服务的新应用,和你的原来应用同时运行。随着时间的推移,所有的功能都被慢慢的从原应用迁移到微服务。

策略1 停止挖坑

  • 不要继续把这个monolithic的天坑继续挖了,如果要加新功能,不要加到这个大应用里。你把新需求用微服务的模式来做。

823e56be34dffa2542f5c9157dcc64a0.png
  • 如图,你新加了一个request router,把新功能的请求路由到你的新微服务里,老功能路由到老monolithic应用里。有点像API网关。
  • 你还加了一坨胶水代码,其实就是为了新老服务之间互相调用,因为他们之间也可能有交互。你可以用RPC、直接访问数据库、访问老数据库同步过来的数据等方式来实现这种调用。
  • 策略优势:它阻止了原应用变得更加难以维护。新开发的服务可以独立地开发和部署。你可以立即开始享受微服务带给你的好处。
  • 但这个策略没有对原应用做任何优化,你需要看策略2来如何改造原应用。

策略2 分离前后端

  • 这个策略主要思路是,帮你分离展示层和业务逻辑层以及数据访问(DAO)层,从而做到让原来的monolithic应用缩小一些。通常一个典型的企业级应用有这些组件层:
    • 展示层:处理HTTP请求和展示web界面的组件们。
    • 业务逻辑层:应用实现业务逻辑的核心组件们。
    • 数据访问层:应用访问数据和消息代理的基础组件们。
  • 有一种常见的做法,你可以把你的应用按照下图,拆分成两个子应用:

139b9bf06fb9264e2cf458fc5da8183c.png
    • 一层子应用包括了展示层。
    • 另一层子应用包含了业务逻辑和数据访问。
  • 策略优势
    • 它让你能够单独地部署和扩容两个应用,它们各自都可以快速迭代
    • 由于业务逻辑层和数据访问层单独抽离了一个应用,你的新微服务现在可以调用这坨UI无关的服务了。
  • 然而这个策略仍然只是一个部分解决方案,有可能拆分后两个应用还是会变成难以维护的monolithic应用,所以你还需要看策略3。

策略3 抽取服务

  • 这个策略就是要把现有的在你原应用里的模块转换成单独的微服务。每次你抽出来一个新的微服务,原应用就变小了。只要你抽得足够多,原来的这个大应用就会消失或者干脆也变成一个微服务。
  • 转换成微服务的优先级
    • 首先你最好先抽象容易抽象的模块,这样你就能先积累抽象微服务的经验
    • 然后你应该优先抽取能给你带来最大收益的模块,所以你需要给你的模块排一个优先级。
    • 你还可以先抽象要特别的物理资源的模块,比如某个模块特别需要内存数据库,你可以先抽这个模块,然后把它放到内存比较大的环境里。
    • 抽象模块的粒度,可以按照这样一个简单原则:比如某个模块和其他的模块的交流都可以通过异步消息完成,那么这个模块就可以抽出来。
  • 如何抽取一个模块
    • 首先要定义抽出来的模块如何和系统进行交互,通常是一组双向的API。但通常都比较难,因为这种API会和系统耦合得比较多。用了Domain Model pattern 的就更难重构了。
    • 一旦你实现了粗粒度的接口,你就要把模块抽出来做一个单独的服务了。这时候为了同心,你还需要使用IPC机制。

b16aa4eb6bd8dfed491b7a35870ba721.png
    • 在上面的例子中,模块Z是要被抽象出来的模块。它的组件被模块X和模块Y使用了,所以
      • 第一步就是要定义出一组粗粒的API来让X和Y通过API和Z交互。
      • 第二步就是把模块弄成单独的服务。这时候你需要IPC通信来完成他们之间的跨服务调用。
    • 你甚至可以按照新的API来重新写这个抽象的服务。你每抽出来一个服务,你就朝着微服务方向前进了一步,随着时间推移,你的原应用终将消散,而你就把它演进成了一套微服务。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值