Category Archives: Microservices

自建一个简易的OpenAPI网关

网关(API Gateway)是请求流量的唯一入口,可以适配各类渠道和业务,处理各种协议接入、路由与报文转换、同步异步调用等,来管理 API 接口和进行请求流量控制,在微服务架构中,网关尤为重要。

预览图

背景

当然,现在已有很多开源软件,如 KongGraviteeZuul

这些开源网关固然功能齐全,但对于我们业务来说,有点太重了,我们有部分定制化需求,为此我们自建了一个轻量级的 OpenAPI 网关,主要供第三方渠道对接使用。

简介

功能特性

接口鉴权

  • 请求 5s 自动过期
  • 参数 md5 签名
  • 模块粒度的权限控制

接口版本控制

  • 支持转发到不同服务
  • 支持转发到同一个服务不同接口

事件回调

  • 事件订阅
  • 最大重试 3 次
  • 重试时间采用衰减策略(30s、60s、180s)

系统架构

从第三方请求 API 链路来说,第三方渠道通过 HTTP 协议请求 OpenAPI 网关,网关再将请求转发到对应的内部服务端口,这些端口层通过 gRPC 调用请求到服务层,处理完请求后依次返回。

从事件回调请求链路来说,服务层通过 HTTP 协议发起事件回调请求到 OpenAPI 网关,并立即返回成功。OpenAPI 网关异步完成第三方渠道事件回调请求。

系统架构

实现

网关配置

由于网关存在内部服务和第三方渠道配置,更为了实现配置的热更新,我们采用了 ETCD 存储配置,存储格式为 JSON。

配置分类

配置分为以下 3 类:

  • 第三方 AppId 配置
  • 内外 API 映射关系
  • 内部服务地址

配置结构

a、第三方 AppId 配置

AppId配置

b、内部服务地址

内部服务地址

c、内外 API 映射关系

API映射关系

配置更新

利用 ETCD 的 watch 监听,可以轻易实现配置的热更新。

配置热更新

当然也还是需要主动拉取配置的情况,如重启服务的时候。

拉取热更新

API 接口

第三方调用 API 接口的时序,大致如下:

API调用时序

参数格式

为了简化对接流程,我们统一了 API 接口的请求参数格式。请求方式支持 POST 或者 GET。

API调用时序

接口签名

签名采用 md5 加密方式,算法可描述为:

1、将参数 p、m、a、t、v、ak、secret 的值按顺序拼接,得到字符串;
2、md5 第 1 步的字符串并截取前 16 位, 得到新字符串;
3、将第 2 步的字符串转化为小写,即为签名;

PHP 版的请求,如下:

$appId = 'app id';
$appSecret = 'app secret';
$api = 'api method';

// 业务参数
$businessParams = [
  'orderId' => '123123132',
];

$time = time();
$params = [
  'p'  => json_encode($businessParams),
  'm'  => 'inquiry',
  'a'  => $api,
  't'  => $time,
  'v'  => 1,
  'ak' => $appId,
];

$signStr = implode('', array_values($params)) . $appSecret;
$sign = strtolower(substr(md5($signStr), 0, 16));

$params['s'] = $sign;

接口版本控制

不同的接口版本,可以转发请求到不同的服务,或同一个服务的不同接口。

接口版本控制

事件回调

通过事件回调机制,第三方可以订阅自己关注的事件。

接口版本控制

对接接入

渠道接入

只需要配置第三方 AppId 信息,包括 secret、回调地址、模块权限。

渠道AppId配置

即,需要在 ETCD 执行如下操作:

$ etcdctl set /openapi/app/baidu '{
    "Id": "baidu",
    "Secret": "00cf2dcbf8fb6e73bc8de50a8c64880f",
    "Modules": {
        "inquiry": {
            "module": "inquiry",
            "CallBack": "http://www.baidu.com"
        }
    }
}'

服务接入

a、配置内部服务地址

配置内部服务地址

即,需要在 ETCD 执行如下操作:

$ etcdctl set /openapi/backend/form_openapi '{
    "type": "form",
    "Url": "http://med-ih-openapi.app.svc.cluster.local"
}'

b、配置内外 API 映射关系

配置内部服务地址

同样,需要在 ETCD 执行如下操作:

$ etcdctl set /openapi/api/inquiry/createMedicine.v2 '{
    "Module": "inquiry",
    "Method": "createMedicine",
    "Backend": "form_openapi",
    "ApiParams": {
        "path": "inquiry/medicine-clinic/create"
    }
}'

c、接入事件回调

接入服务也需要按照第三方接入方式,并申请 AppId。回调业务参数约定为:

配置内部服务地址

Golang 版本的接入,如下:

const (
	AppId = "__inquiry"
	AppSecret = "xxxxxxxxxx"
	Version = "1"
)

type CallbackReq struct {
	TargetAppId string                 //目标APP Id
	Module      string                 //目标模块
	Event       string                 //事件
	Params      map[string]interface{} //参数
}

func generateData(req CallbackReq) map[string]string {
    params, _ := json.Marshal(req.Params)
	p := map[string]interface{}{
		"ak": req.TargetAppId,
		"m":  req.Module,
		"e":  req.Event,
		"p":  string(params),
	}

	pStr, _ := json.Marshal(p)
	postParams := map[string]string{
		"p":  string(pStr),
		"m":  "callback",
		"a":  "callback",
		"t":  fmt.Sprintf("%d", time.Now().Unix()),
		"v":  Version,
		"ak": AppId,
	}

	postParams["s"] = sign(getSignData(postParams) + AppSecret)
	
	return postParams
}

func getSignData(params map[string]string) string {
	return strings.Join([]string{params["p"], params["m"], params["a"], params["t"], params["v"], params["ak"]}, "")
}

func sign(str string) string {
	return strings.ToLower(utils.Md5(str)[0:16])
}

未来规划

  • 后台支持配置 AppId
  • 事件回调失败请求支持手动重试
  • 请求限流

怎么用API网关构建微服务|架构

当选择将应用程序构建为一组微服务时,需要确定应用程序客户端如何与微服务交互。在单体应用程序中,只有一组(通常是重复的、负载均衡的)端点。然而,在微服务架构中,每个微服务都会暴露一组通常是细粒度的端点。在本文中,我们将讨论一下这对客户端与应用程序之间的通信有什么影响,并提出一种使用API网关的方法。

让我们想象一下,你要为一个购物应用程序开发一个原生移动客户端。你很可能需要实现一个产品详情页面,上面展示任何指定产品的信息。

例如,下图展示了在Amazon Android移动应用中滚动产品详情时看到的内容。

虽然这是个智能手机应用,产品详情页面也显示了大量的信息。例如,该页面不仅包含基本的产品信息(如名称、描述、价格),而且还显示了如下内容:

购物车中的件数;

订单历史;

客户评论;

低库存预警;

送货选项;

各种推荐,包括经常与该产品一起购买的其它产品,购买该产品的客户购买的其它产品,购买该产品的客户看过的其它产品;

可选的购买选项。

当使用单体应用程序架构时,移动客户端将通过向应用程序发起一次REST调用(GET api.company.com/productdetails/<productId>)来获取这些数据。负载均衡器将请求路由给N个相同的应用程序实例中的一个。然后,应用程序会查询各种数据库表,并将响应返回给客户端。

相比之下,当使用微服务架构时,产品详情页面显示的数据归多个微服务所有。下面是部分可能的微服务,它们拥有要显示在示例中产品详情页面上的数据:

购物车服务——购物车中的件数;

订单服务——订单历史;

目录服务——产品基本信息,如名称、图片和价格;

评论服务——客户的评论;

库存服务——低库存预警;

送货服务——送货选项、期限和费用,这些单独从送货方的API获取;

推荐服务——建议的产品。

我们需要决定移动客户端如何访问这些服务。让我们看看都有哪些选项。

客户端与微服务直接通信

从理论上讲,客户端可以直接向每个微服务发送请求。每个微服务都有一个公开的端点(https ://<serviceName>.api.company.name)。该URL将映射到微服务的负载均衡器,由它负责在可用实例之间分发请求。为了获取产品详情,移动客户端将逐一向上面列出的N个服务发送请求。

遗憾的是,这种方法存在挑战和局限。一个问题是客户端需求和每个微服务暴露的细粒度API不匹配。在这个例子中,客户端需要发送7个独立请求。在更复杂的应用程序中,可能要发送更多的请求。例如,按照Amazon的说法,他们在显示他们的产品页面时就调用了数百个服务。然而,客户端通过LAN发送许多请求,这在公网上可能会很低效,而在移动网络上就根本不可行。这种方法还使得客户端代码非常复杂。

客户端直接调用微服务的另一个问题是,部分服务使用的协议不是Web友好协议。一个服务可能使用Thrift二进制RPC,而另一个服务可能使用AMQP消息传递协议。不管哪种协议都不是浏览器友好或防火墙友好的,最好是内部使用。在防火墙之外,应用程序应该使用诸如HTTP和WebSocket之类的协议。

这种方法的另一个缺点是,它会使得微服务难以重构。随着时间推移,我们可能想要更改系统划分成服务的方式。例如,我们可能合并两个服务,或者将一个服务拆分成两个或更多服务。然而,如果客户端与微服务直接通信,那么执行这类重构就非常困难了。

由于这些问题的存在,客户端与微服务直接通信很少是合理的。

使用API网关

通常,一个更好的方法是使用所谓的API网关。API网关是一个服务器,是系统的唯一入口。从面向对象设计的角度看,它与外观模式类似。API网关封装了系统内部架构,为每个客户端提供一个定制的API。它可能还具有其它职责,如身份验证、监控、负载均衡、缓存、“请求整形(request shaping)”与管理、静态响应处理。

下图展示了API网关通常如何融入架构:

API网关负责服务请求路由、组合及协议转换。客户端的所有请求都首先经过API网关,然后由它将请求路由到合适的微服务。API网管经常会通过调用多个微服务并合并结果来处理一个请求。它可以在Web协议(如HTTP与WebSocket)与内部使用的非Web友好协议之间转换。

API网关还能为每个客户端提供一个定制的API。通常,它会向移动客户端暴露一个粗粒度的API。例如,考虑下产品详情的场景。API网关可以提供一个端点(/productdetails?productid=xxx),使移动客户端可以通过一个请求获取所有的产品详情。API网关通过调用各个服务(产品信息、推荐、评论等等)并合并结果来处理请求。

Netflix API网关是一个很好的API网关实例。Netflix流服务提供给数以百计的不同类型的设备使用,包括电视、机顶盒、智能手机、游戏系统、平板电脑等等。最初,Netflix试图为他们的流服务提供一个通用的API。然而他们发现,由于各种各样的设备都有自己独特的需求,这种方式并不能很好地工作。如今,他们使用一个API网关,通过运行特定于设备的适配器代码来为每个设备提供一个定制的API。通常,一个适配器通过调用平均6到7个后端服务来处理每个请求。Netflix API网关每天处理数十亿请求。

API网关的优点和不足

如你所料,使用API网关有优点也有不足。使用API网关的最大优点是,它封装了应用程序的内部结构。客户端只需要同网关交互,而不必调用特定的服务。API网关为每一类客户端提供了特定的API。这减少了客户端与应用程序间的交互次数,还简化了客户端代码。

API网关也有一些不足。它增加了一个我们必须开发、部署和维护的高可用组件。还有一个风险是,API网关变成了开发瓶颈。为了暴露每个微服务的端点,开发人员必须更新API网关。API网关的更新过程要尽可能地简单,这很重要。否则,为了更新网关,开发人员将不得不排队等待。

不过,虽然有这些不足,但对于大多数现实世界的应用程序而言,使用API网关是合理的。

实现API网关

到目前为止,我们已经探讨了使用API网关的动机及其优缺点。下面让我们看一下需要考虑的各种设计问题。

性能和可扩展性

只有少数公司有Netflix的规模,每天需要处理数十亿请求。不管怎样,对于大多数应用程序而言,API网关的性能和可扩展性通常都非常重要。因此,将API网关构建在一个支持异步、I/O非阻塞的平台上是合理的。有多种不同的技术可以用于实现一个可扩展的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和JavaScript中的Promise,还有最初是微软为.NET平台开发的Reactive Extensions(RX)。Netflix创建了RxJava for JVM,专门用于他们的API网关。此外,还有RxJS for JavaScript,它既可以在浏览器中运行,也可以在Node.js中运行。使用响应式方法将使你可以编写简单但高效的API网关代码。

服务调用

基于微服务的应用程序是一个分布式系统,必须使用一种进程间通信机制。有两种类型的进程间通信机制可供选择。一种是使用异步的、基于消息传递的机制。有些实现使用诸如JMS或AMQP那样的消息代理,而其它的实现(如Zeromq)则没有代理,服务间直接通信。另一种进程间通信类型是诸如HTTP或Thrift那样的同步机制。通常,一个系统会同时使用异步和同步两种类型。它甚至还可能使用同一类型的多种实现。总之,API网关需要支持多种通信机制。

服务发现

API网关需要知道它与之通信的每个微服务的位置(IP地址和端口)。在传统的应用程序中,或许可以硬连线这个位置,但在现代的、基于云的微服务应用程序中,这并不是一个容易解决的问题。基础设施服务(如消息代理)通常会有一个静态位置,可以通过OS环境变量指定。但是,确定一个应用程序服务的位置没有这么简单。应用程序服务的位置是动态分配的。而且,单个服务的一组实例也会随着自动扩展或升级而动态变化。总之,像系统中的其它服务客户端一样,API网关需要使用系统的服务发现机制,可以是服务器端发现,也可以是客户端发现。下一篇文章将更详细地描述服务发现。现在,需要注意的是,如果系统使用客户端发现,那么API网关必须能够查询服务注册中心,这是一个包含所有微服务实例及其位置的数据库。

处理局部失败

在实现API网关时,还有一个问题需要处理,就是局部失败的问题。该问题在所有的分布式系统中都会出现,无论什么时候,当一个服务调用另一个响应慢或不可用的服务,就会出现这个问题。API网关永远不能因为无限期地等待下游服务而阻塞。不过,如何处理失败取决于特定的场景以及哪个服务失败。例如,在产品详情场景下,如果推荐服务无响应,那么API网关应该向客户端返回产品详情的其它内容,因为它们对用户依然有用。推荐内容可以为空,也可以,比如说,用一个固定的TOP 10列表取代。不过,如果产品信息服务无响应,那么API网关应该向客户端返回一个错误信息。

如果缓存数据可用,那么API网关还可以返回缓存数据。例如,由于产品价格不经常变化,所以如果价格服务不可用,API网关可以返回缓存的价格数据。数据可以由API网关自己缓存,也可以存储在像Redis或Memcached那样的外部缓存中。通过返回默认数据或者缓存数据,API网关可以确保系统故障不影响用户的体验。

在编写代码调用远程服务方面,Netflix Hystrix是一个异常有用的库。Hystrix会将超出设定阀值的调用超时。它实现了一个“断路器(circuit breaker)”模式,可以防止客户端对无响应的服务进行不必要的等待。如果服务的错误率超出了设定的阀值,那么Hystrix会切断断路器,在一个指定的时间范围内,所有请求都会立即失败。Hystrix允许用户定义一个请求失败后的后援操作,比如从缓存读取数据,或者返回一个默认值。如果你正在使用JVM,那么你绝对应该考虑使用Hystrix。而如果你正在使用一个非JVM环境,那么你应该使用一个等效的库。

小结

对于大多数基于微服务的应用程序而言,实现一个API网关是有意义的,它可以作为系统的唯一入口。API网关负责服务请求路由、组合及协议转换。它为每个应用程序客户端提供一个定制的API。API网关还可以通过返回缓存数据或默认数据屏蔽后端服务失败。在本系列的下一篇文章中,我们将探讨服务间通信。

from:http://www.tuicool.com/articles/bMnEbmv

微服务在微信的架构实践

微服务的理念与腾讯一直倡导的“大系统小做”有很多相通之处,本文将分享微信后台架构的服务发现、通信机制、集群管理等基础能力与其上层服务划分原则、代码管理规则等。
背景介绍

首先,我们需要敏捷开发。过去几年,微信都是很敏捷地在开发一些业务。所以我们的底层架构需要支撑业务的快速发展,会有一些特殊的需求。

另外,目前整个微信团队已经有一千多人了,开发人员也有好几百。整个微信底层框架是统一的,微信后台有千级模块的系统。比如说某某服务,有上千个微服务在跑,而集群机器数有几万台,那么在这样的规模下,我们会有怎么样的挑战呢?

我们一直在说“大系统小做”,联想一下,微服务与腾讯的理念有哪些相同与不同的地方呢?通过对比,最终发现还是有许多相通的地方。所以我挑出来讲讲我们的实践。

看过过去几个会议的内容,可能大家会偏向于讲整一个大的框架,比如整个云的架构。但是我这边主要讲的是几个特殊的点。

概览详情

开始看一下我们的结构。全球都有分布,主要有上海、深圳、香港、加拿大几个数据中心。

其中上海服务国内北方的用户,深圳负责南方用户,加拿大服务北美、南美和欧洲,香港服务东南亚、中东和非洲地区。

然后来看看我们的架构:

  • 最上边是我们的产品;
  • 然后有一个号称几亿在线的长连接和短连接的服务;
  • 中间有一个逻辑层,后台框架讲的主要是逻辑层往后这块,包括我们的 RPC、服务分组、过载保护与数据存储;
  • 底层有个 PaxosStore 存储平台。

整套就是这么个体系。微服务很容易去构建,但是规模变大后有哪些问题,需要哪些能力?这里挑出三个点来讲一下:

一、敏捷

希望你的服务很快实现,不太多去考虑。像我们早期互联网业务,甚至包括 QQ 等,我们很注重架构师的一个能力,他需要把握很多的东西。他设置每个服务的时候,要先算好很多资源,算好容灾怎么做。容灾这个问题直接影响业务怎么去实现的,所以有可能你要做一个具体逻辑的时候要考虑很多问题,比如接入服务、数据同步、容灾等等每个点都要考虑清楚,所以节奏会慢。

二、容错

当你的机器到了数万台,那每天都有大量机器会有故障。再细一点,可以说是每一个盘的故障更频繁一点。

三、高并发

基础架构

接下来看看我们的基础架构。

整个微服务的架构上,我们通常分成这些部分:

  • 服务布局
  • 服务之间怎么做一些远程调用
  • 容错(主要讲一下过载保护)
  • 部署管理
服务布局

分两层,一个是城市间。城市之间的数据是相对独立的,除了少数账号全球同步,大部分业务都希望做成电子邮件式的服务,各自有自身的环境在跑,之间使用类似于电子邮件的通信。所以我们选择让每个城市自治,它们之间有一个 200-400ms 的慢速网络,国内会快点,30ms。

而城市内部,就是每个园区是一套独立的系统,可以互相为对方提供备份。要求独立的电源与网络接入。

城市内部会有整套的划分,终端 –>接入层 –>逻辑层 –>存储层 都是完全独立的一套系统。

远程调用

看到很多框架,竟然是没有协程的,这很诧异。早年我们 QQ 邮箱、微信、图像压缩、反垃圾都是一个 web 服务,只有存储层会独立到后面去,甚至用 web 直连 MySQL。因为它早期比较小,后来变大之后就用微服务架构。

每个东西都变成一个小的服务,他们是跨机的。你可以想象一下,每天我们很多人买早餐的时候,掏出手机做一个微信支付,这一个动作在后台会引起上百次的调用。这有一个复杂的链路。在 2014 年之前,我们微信就是没有做异步的,都是同步的,在这么多调用里,A 服务调用 B,那要先等它返回,这样就占住了一条进程或者线程。所以其实 13 年的时候,我们发生了大大小小的故障,很大一部分原因就在这里。

然后 13 年底的时候,这个问题太严重了,严重到,比如发消息的时候,你去拿一个头像之类的,它只要抖动,就可能引发整一条调用链的问题,并且因为过程保护的不完善,它会把整个消息发送的曲线掉下去,这是我们很痛苦的时间。

然后当时我们就去考虑这些方案,13 年的时候抽出 3 个人重新做了一个完整的库 libco。(两千行),实现时间轮盘与事件处理链、常用网络编程模式、同步原语等。它分为三大块,事件驱动、网络 HOOK 和协程机制。

早期是多进程为主,当年切多线程的时候,也遇到一大波修改,后来线程里有了一个线程变量就好多了。如果没有这个东西,你可能要把许多变量改成参数再一层一层传递下去。有了线程变量就好多了。现在我们的协程变量也是这个意义,效果就像写一个宏一样。

另一个是,我们支持 CGI,早期库在 CGI 上遇到问题,所以没有推广。因为一个标准 CGI 服务是基于一些古老的接口的,像 getENV、setENV,就是说你的 coreString 是通过 ENV 来得到的,那么这个我们也把它给 HOOK 掉了,它会根据你的协程去分派。

最难的一个是 gethostbyname 方法,我发现很多人就连在异步编程里,处理 hostbyname 也可能是用了一套独立线程去做,或者你很辛苦地把整个代码抠出来重新写一遍,这个肯定是有很多问题的。所以我们 libco 就把这个 gethostbyname 给完整地支持了。

最后如果你还不爽,说一般业务逻辑可以这么干,那我还有很多后台代码怎么办呢?很多有经验的老的程序员可能要拿着他们那一堆很复杂的异步编程的代码来质疑我们,他们不认为他们的代码已经完全可以被协程所取代了。

他们有如下两个质疑:

  • 质疑性能:协程有很多切换,会不会带来更大开销?
  • 你可能处理几万并发就好,消耗个 1G 内存就行,但是我们这里是处理千万并发哦,这么大的规模,我不信任你这个东西。

这样我们其实是面临了一个问题,因为一些老代码,越是高级的人写的,它的技术栈越深,稍微改动一点代码,就出 BUG 了。

所以我们后来做了两个东西,一个是实际修改了相对简单的异步代码到 libco 里,然后性能更好了。因为在做异步编程的时候,你需要自己去维护很多的数据结构,做你的状态保存,它们的生存期有可能需要很久,你自然地会分配许多内存给它,当然你会用一些内存池去优化它,但是这些是有限的。

但是你用协程的话,很多变量就自然在一个连续的内存里了,相当于一个小的内存池,就比如 if……else……这个你没有必要去 new 一个东西保存状态的,直接放在栈里就行了,所以它的性能更好了。

第二个是,它要求很高的并发。由于协程要一个栈,我们一般开 128k,如果你对这个代码掌控得比较好,可能开 16k,就算是这样,你要开 1 万个协程,还是要 100 多 M 的内存。所以我们后来就在这基础上做了一个可以支持千万连接的协程模式。

Libco 是一个底层库,让你很方便开发,但是大部分开发人员不是直接面对 libco 的,我们花了一年时间把整个微信后台绝大部分逻辑服务、存储服务改成基于 libco,整个配置就直接通过配一台机器上的并发数配 10 倍甚至 20、30 倍,这样子就一下子把整个问题解决了。

过载保护

并发数上去后容易引发另一个问题,早期的时候,后端服务性能高,逻辑服务性能相对弱,很容易被 hold,不可能给后端发起很多连接,不具有“攻击性”,但修改完成后,整个前端变得很强,那可能对后端产生很大的影响。这个时候就要来考虑一下过载保护了。

一般会提到几个点。

1.轻重分离:

就是一个服务里边不要又有重的操作,又有轻的,这样过载的时候,大量的请求都被某些小请求拦截掉了,资源被占满了。

2.队列:

过载保护一般是说系统内部服务在做过去的事情,做无用功。它们可能待在某个队列里边,比如服务时间要求 100ms,但它们总是在做 1s 以前的任务,所以整个系统会崩溃。所以老的架构师会注重说配好每一个服务的队列长度,估算好。但是在繁忙的开发中,是很难去控制的。

3.组合命令式:

后端服务并不是只有一个,上边这个图中的例子,想要调用很多服务,然后 AB 都过载,它们每一个其实都只是过载一点,通过率可达到 80%,但是前端需要这两个服务的组合服务,那么这里就可能只能达到 60% 的通过率。然后后边如果是更多的服务,那么每个服务的一点点过载,到了前端就是很严重的问题。怎么解决呢?

这本书在 12、13 年的时候很火,里边提到了两个对我们有用的点。

  • 一个是“希望系统是分布式的,去中心化”,指系统过载保护依赖每一个节点自身的情况去做,而不是下达一个统一的中心指令。
  • 二是“希望整个控制是基于反馈的”,它举了一些例子,像抽水马桶,像过去炼钢铁的参数很难配,但是只要有一个反馈机制就好解决了。

于是我们构建了一套看起来有点复杂的过载保护系统。

整个系统基于反馈,然后它把整个拒绝的信息全程传递了。看到最右边,有几个典型的服务,从一个 CGI 调用一个后台服务,再调用另一个后台服务,它会在 CGI 层面就把它的重要程度往下传。回到刚才那个前端调用 A、B 服务的例子,使用这样的一种重要程度传递,就可以直接拒绝那些相同用户的 20% 的请求,这样就解决了这个问题。

 怎么配队列?

这个只是反映了生产者和服务者处理能力的差异,观察这个差异,就可以得到一个好的拒绝的数。你不需要去配它多长,只需要去看一个请求在队列里待的平均时间是否可以接受,是一个上涨趋势还是一个下降趋势。这样我们就可以决定要不要去拒绝。那这样几乎是全自动的。你只要配得相对大一点就行了,可以抗一些抖动。在接入之前就评估它,在过去一段时间内平均队列耗时多长,如果超过预支,我们就往下调。这样就把整个系统的过载能力提升了很多。

这是一个具体的做法,我们会考虑两个维度,一个是后台服务,可能服务很多不同的前端,它可能来源于一个支付的请求,经过层层调用,到达后台;或者是一个发消息的服务;它也可能是一个不重要的小服务,如果这个账户服务过载的时候,那么我们可以根据这个表来自动地优先去拒绝一些不那么重要的服务请求,使得我们核心服务能力可以更好地提供。这样整个系统就可以做到很好的过载保护。

数据存储

上边提到一个数据层,那我们是怎么去做数据的呢?

在过去很多年里,我们可能是尽可能去事务化、不追求强一致,一般是采用主备同步的方法。但我们的目标还是强一致的存储。

强一致是说,写一个数据之后,服务器的返回成功不会因为单机故障而丢失。早年我们用的是自己设计的协议,严格来证明的话,没有 Paxos 这么严谨,所以我们在过去一年多的时间内,重新做了一个 Paxos 存储。

它是一个同步复制的数据存储,支持各个园区之间的数据一致性,并且是可以多组多写的,就是说任何一个园区接入,它都可以进行数据的强制读写。另外它并不只是 key-value 模式,它支持 key-value、list、表。在微信这边很少会说完全依赖 key-value 的,因为很多业务都是有列表、表格等的请求,所以很多年前就开始用表格的存储。

Paxos 可用性很高,所以我们就敢做单表有亿行的设计,这样像公众号粉丝等需要很大的,几千万甚至几亿行的记录,就不用考虑自己去分表。并且这个存储可以使用类 SQL 的语句去做,它是完全保证事务的。

它还是插件化系统,不仅支持 LSM,还支持其它存储引擎。

然后它低成本,后台 CPU 有 E3-1230V3,也有 E5-2670 型号的,内存,CPU 与 ssd 之间有一些能力用不上,所以我们系统是可以灵活组合很多不同存储介质的。

这个系统是跑在同城的,也就是上海内部、深圳内部、加拿大内部和香港内部。它们之间的延迟相对较低,几毫秒的级别。这是一个非租约的,没有 leader,不存在切换的不可用期,随时都可以切换任何一个园区。负载均衡这一块我们沿用 kb64 架构,6 台机为一组。因为园区故障少,平时单机时,分摊 25% 的流量,整体比较稳定。6 台为一组时,整个作为一个 set,有很多 set 之间的适用一致性要去做,会有一个很细粒度的伸缩性,比如它可以 100 组扩展到 101 组。

 为什么用这么重的方式呢?

因为希望应用是 简单快速 的,不用假设一个数据写完之后还可能被回退掉,这样只会有很多额外的开销,会有很多问题。比如公众号,他们有很多素材库之类的很重要的存储,如果数据突然丢了,或者说回退了,没有了,那用户投诉是会很严重的。微信账号这边也是这样,如果一个账户注册了,但是这个数据回退了,那也是很严重的问题。

另一个原因是 可用性。在一个传统的主备系统里面,当主机挂掉,面临切不切备机的抉择,然后你会层层请示,说明目前的同步状况,甚至你不知道当前的同步状况,经过很多流程来请示是否切换备机。

而另外,它也不是一个高成本的方案。

 为什么不用 Raft 呢?

Raft 的开源很有价值,它把互联网后台的数据一致性能力提升了很多,就算是一个很小的团队,它也能直接用 Raft 获得一个强一致能力,而这可能就已经超过了许多互联网后台的强一致能力,因为很多后台都是用了很古老的架构,比如长期用到主机架构。

 Raft 与 Paxos 的区别是什么呢?

其实 Raft 和 Paxos 不是一个层面的概念,这个图就是典型的通过一个 log 变更 db 的架构,通过三条 log 一致性做到数据持久强一致性。那 Paxos 在哪里?在一个 log 的某一个 entry 那边,三个点构成一个常量。

那 Raft 是什么呢?它是整一个二维的东西,就是说,基于一个 Paxos 强一致协议做的一条 log,它整个就是一个 Raft。所以我们可以认为 Raft 其实是 Paxos(log)的一种选择。如果你允许绿色部分不存在,那它就不是 Raft。因为 Raft 的设计是你自己做的,它与 Paxos 没关系。

整个 PaxosStore 架构如图:

它包含了很多层,包括缓存和汇聚层、同步复制的组件等。

这一套方案是在线上用了好几千台的,是一个非租约的方案。存储引擎可以自由定制。如果想用大表,那可以用 leveldb。如果想用更强的 LSM,也可以选择。然后我们也有很多 Bitcask 的模型,更适合于内存的 key-value。

由于有几万台机,所以变很重要,我们也基于 BT 做了一套存储方案。它会以园区为根据地,通常一个变更,会以 BT 协议发送到每个园区里,然后园区内部把同机架机器分成一个分组,然后分组内再互传。就我了解,Facebook 和 Twitter、Ebay 都是这样做的。

作者介绍

许家滔,2005 年加入腾讯,见证 QQ 邮箱从百万到数亿用户的整个敏捷开发过程以及架构变迁。2011 年起负责微信后台基础架构,包括分布式存储平台和后台服务框架等,覆盖微信账号 / 消息 / 朋友圈核心存储等,并为公众号 / 微信支付 / 微信企业号等等业务提供组件支持,近两年专注于后台服务质量提升和高性能架构,在数千台机器上面构建了海量高并发 Paxos 存储系统,同时是开源软件 Tencent/libco 负责人。

Overload control for scaling WeChat microservices

Overload control for scaling WeChat microservices Zhou et al., SoCC’18

There are two reasons to love this paper. First off, we get some insights into the backend that powers WeChat; and secondly the authors share the design of the battle hardened overload control system DAGOR that has been in production at WeChat for five years. This system has been specifically designed to take into account the peculiarities of microservice architectures. If you’re looking to put a strategy in place for your own microservices, you could do a lot worse than start here.

WeChat

The WeChat backend at this point consists of over 3000 mobile services, including instant messaging, social networking, mobile payment, and third-party authorization. The platform sees between 10^{10} - 10^{11} external requests per day. Each such request can triggers many more internal microservice requests, such that the WeChat backend as a whole needs to handle hundreds of millions of requests per second.

WeChat’s microservice system accommodates more than 3000 services running on over 20,000 machines in the WeChat business system, and these numbers keep increasing as WeChat is becoming immensely popular… As WeChat is ever actively evolving, its microservice system has been undergoing fast iteration of service updates. For instance, from March to May in 2018, WeChat’s microservice system experienced almost a thousand changes per day on average.

WeChat classify their microservices as “Entry leap” services (front-end services receiving external requests), “Shared leap” services (middle-tier orchestration services), and “Basic services” (services that don’t fan out to any other services, and thus act as sinks for requests).

On a typical day, peak request rate is about 3x the daily average. At certain times of year (e.g. around the Chinese Lunar New Year) peak workload can rise up to 10x the daily average.

Challenges of overload control for large-scale microservice-based platforms

Overload control… is essential for large-scale online applications that need to enforce 24×7 service availability despite any unpredictable load surge.

Traditional overload control mechanisms were designed for a world with a small number of service components, a relatively narrow ‘front-door,’ and trivial dependencies.

… modern online services are becoming increasingly complex in their architecture and dependencies, far beyond what traditional overload control was designed for.

  • With no single entry point for service requests sent to the WeChat backend, the conventional approach of centralized load monitoring at a global entry point (gateway) is not applicable.
  • The service invocation graph for a particular request may depend on request-specific data and service parameters, even for requests of the same type. So when a particular service becomes overload it is very difficult to determine what types of requests should be dropped to mitigate the situation.
  • Excessive request aborts (especially when deeper in the call graph or later in the request processing) waste computational resources and affect user experience due to high latency.
  • Since the service DAG is extremely complex and continuously evolving, the maintenance cost and system overhead for effective cross-service coordination is too high.

Since one service may make multiple requests to a service it depends on, and may also make requests to multiple backend services, we have to take extra care with overload controls. The authors coin the term subsequent overload for the cases where more than one overloaded service is invoked, or a single overloaded service is invoked multiple times.

Subsequent overload raises challenges for effective overload control. Intuitively, performing load shedding at random when a service becomes overloaded can sustain the system with a saturated throughput. However, subsequent overload may greatly degrade system throughput beyond that intended…

Consider a simple scenario where service A invokes service B twice. If B starts rejecting half of all incoming requests, A’s probability of success drops to 0.25.

DAGOR overview

WeChat’s overload control system is called DAGOR. It aims to provide overload control to all services and thus is designed to be service agnostic. Overload control runs at the granularity of an individual machine, since centralised global coordination is too expensive. However, it does incorporate a lightweight collaborative inter-machine protocol which is needed to handle subsequent overload situations. Finally, DAGOR should sustain the best-effort success rate of a service when load shedding becomes inevitable due to overload. Computational resources (e.g. CPU, I/O) spent on failed service tasks should be minimised.

We have two basic tasks to address: detecting an overload situation, and deciding what to do about it once detected.

Overload detection

For overload detection, DAGOR uses the average waiting time of requests in the pending queue (i.e., queuing time). Queuing time has the advantage of negating the impact of delays lower down in the call-graph (compared to e.g. request processing time). Request processing time can increase even when the local server itself is not overloaded. DAGOR uses window-based monitoring, where a window is one second or 2000 requests, whichever comes first. WeChat clearly run a tight ship:

For overload detection, given the default timeout of each service task being 500ms in WeChat, the threshold of the average request queuing time to indicate server overload is set to 20ms. Such empirical configurations have been applied in the WeChat business system for more than five years with its effectiveness proven by the system robustness with respect to WeChat business activities.

Admission control

Once overload is detected, we have to decide what to do about it. Or to put things more bluntly, which requests we’re going to drop. The first observation is that not all requests are equal:

The operation log of WeChat shows that when WeChat Pay and Instant Messaging experience a similar period of service unavailability, user complaints against the WeChat Pay service are 100x those against the Instant Messaging service.

To deal with this in a service agnostic way, every request is assigned a business priority when it first enters the system. This priority flows with all downstream requests. Business priority for a user request is determined by the type of action requested. Although there are hundreds of entry points, only a few tens have explicit priority, all the others having a default (lower) priority. The priorities are maintained in a replicated hashtable.

When overload control is set to business priority level n, all requests from levels n+1 will be dropped. That’s great for mixed workloads, but suppose we have a flood of Payment requests, all at the same priority (e.g. p). The system will become overloaded, and hence move the overload threshold to p-1, when it will become lightly loaded again. Once light load is detected, the overload threshold is incremented to p again, and once more we are in overload. To stop this flip-flipping when overloaded with requests at the same priority level, we need a level of granularity finer than business priority.

WeChat has a neat solution to this. It adds a second layer of admission control based on user-id.

User priority is dynamically generated by the entry service through a hash function that takes the user ID as an argument. Each entry service changes its hash function every hour. As a consequence, requests from the same user are likely to be assigned to the same user priority within one hour, but different user priorities across hours.

This provides fairness while also giving an individual user a consistent experience across a relatively long period of time. It also helps with the subsequent overload problem since requests from a user assigned high priority are more likely to be honoured all the way through the call graph.

Combining business priority and user priority gives a compound admission level with 128 levels of user priority per business priority level.

With each admission level of business priority having 128 levels of user priority, the resulting number of compound admission levels is in the tens of thousands. Adjustment of the compound admission level is at the granule of user priority.

There’s a nice sidebar on why using session ID instead of user ID doesn’t work: you end up training users to log out and then log back in again when they’re experiencing poor service, and now you have a login storm on top of your original overload problem!

DAGOR maintains a histogram of requests at each server to track the approximate distribution of requests over admission priorities. When overload is detected in a window period, DAGOR moves to the first bucket that will decrease expected load by 5%. With no overload, it moves to the first bucket that will increase expected load by 1%.

A server piggy-backs its current admission level on each response message sent to upstream servers. In this way an upstream server learns the current admission control setting of a downstream service, and can perform local admission control on the request before even sending it.

End-to-end therefore, the DAGOR overload control system looks like this:

Experiments

The best testimony to the design of DAGOR is that it’s been working well in production at WeChat for five years. That doesn’t provide the requisite graphs for an academic paper though, so we also get a set of simulation experiments. The following chart highlights the benefits of overload control based on queuing time rather than response time. The benefits are most pronounced in situations of subsequent overload (chart (b)).

Compared to CoDel and SEDA, DAGOR exhibits a 50% higher request success rate with subsequent overloading when making one subsequent call. The benefits are greater the higher the number of subsequent requests:

Finally, in terms of fairness CoDel can be seen to favour services with smaller fan-out to overloaded services when under stress, whereas DAGOR manifests roughly the same success rate across a variety of requests.

Three lessons for your own systems

Even if you don’t use DAGOR exactly as described, the authors conclude with three valuable lessons to take into consideration:

  • Overload control in a large-scale microservice architecture must be decentralized and autonomous in each service
  • Overload control should take into account a variety of feedback mechanisms (e.g. DAGOR’s collaborative admission control) rather than relying solely on open-loop heuristics
  • Overload control design should be informed by profiling the processing behaviour of your actual workloads.

中文版链接: https://www.tuicool.com/articles/aYJjMvN

from:https://blog.acolyer.org/2018/11/16/overload-control-for-scaling-wechat-microservices/

消息系统在微服务间通讯的数据一致性

微服务是当下的热门话题,今天来聊下微服务中的一个敏感话题:如何保证微服务的数据一致性。谈到分布式事务,就避免不了CAP理论。

 

CAP理论是指对于一个分布式计算系统来说,不可能同时满足以下三点:

1. 一致性(Consistence) (等同于所有节点访问同一份最新的数据副本)

2. 可用性(Availability)(对数据更新具备高可用性)

3. 容忍网络分区(Partition tolerance)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。)

根据定理,分布式系统只能满足三项中的两项而不可能满足全部三项。以上关于CAP的理论介绍来自维基百科。同理,如何保证微服务间的数据一致性也一直是一个持续的话题,其实就是 如何在这三者中做一个权衡

就之前此公众号已经有一系列的文章来讨论,关于微服务架构下的事务一致性的话题,包括 BASE理论,两阶段提交,三阶段提交,可靠事件模式,TCC模式,补偿模式等,想进一步了解的话可以参考这里:微服务架构下的数据一致性保证(一),微服务架构下的数据一致性保证(二),微服务架构下的数据一致性保证(三)。今天只是谈一谈其中的一种场景:使用消息系统进行微服务间通讯,如何来保证微服务间的数据一致性。

1. 问题的引出:

微服务架构之数据一致性问题

这里我们先以下面的一个例子来引出问题:以 公有云市场1中的一个部署产品来说,当用户想要部署一个公有云中已有的产品,比如Redis产品,用户会先去公有云市场中找到对应的Redis产品,当用户点击发布时,市场中会进行相应的记录,同时后台有真正负责部署的模块,此处我们叫部署模块。当产品部署成功后,部署模块和市场都会进行最终状态的同步。

1、公有云市场:此处指一个简单的模型,类似阿里云的云镜像市场或亚马逊aws中的镜像市场。在云镜像市场中,用户会选择其中感兴趣的产品比如mysql,然后进行付费,发布,这样省去了用户在自己的云平台环境中手动下载安装包,然后安装,配置,启动等一系列繁琐的过程。在云平台市场中,用户所需要做的只是一些必要的配置,然后点击启动就能完成一个产品的发布。一般都是这样一个先购买镜像后启动实例的过程。

以上都是在理想的情况下进行的,大致流程如下图:

此时,市场和部署模块都是是独立的微服务,当平台用户申请开通产品后,公有云市场会先进行一系列的初始化工作,并向部署模块中发送部署请求,当部署模块部署成功或者失败后,会进行相应的记录,市场也会将状态记录到本地数据库。由于市场和部署都是以微服务形式存在,都有自己的本地事务,此时,我们已经无法通过本地事务的控制来保证操作的原子性了。那么问题就随之而来:

假如市场模块在向部署模块发送完请求之后,市场微服务出现了数据库的连接异常(比如连接数据库的网络异常,数据库漂移等),此时市场会向前端报错,提示部署过程中出错,导致部署失败,但实际上部署模块已经在后台默默的为用户开通了实例。
同样的问题也会出现在,当向部署模块发送完请求后市场微服务出现了宕机等意外情况,市场微服务的数据库中干脆直接没有保存用户的此次开通的请求,但实际上部署模块却已经在这个过程中开通过了产品实例。

如果公有云平台对用户资源的实例限制是5个,即一个用户(比如试用版的用户)最多只能开通5个产品实例,则用户此时在市场中最多只能开4个,因为有一个实例在部署模块成功部署,但是市场模块却并不清楚,此时就出现了数据不一致的严重问题。那么该如何解决此类问题呢?如何解决这类业务前后不一致的问题呢?

2. 引入消息框架,解决数据不一致问题

这里我们采用了 消息通信框架Kafka,通过事件机制来完成相应的需求。

在采用Kafka来完成消息的投递的同时,不可避免地也会面对消息的丢失的意外情况。这里我们先来看一下我们实现的主场景,然后在后面我们会接着探讨,如何在业务层面保证消息的绝对投递和消费。

消息发送方的处理

流程处理如下:

我们来分析一下此种设计如何能够满足我们的需求:

市场模块操作Product和Event是在本地事务进行,保证了本地操作的一致性。

如果开通产品时,市场领域在事件发布之前就发生了异常,宕机或者数据库无法连接,根据设计,事件发布定时器和开通产品的Service是分离操作,此时发生宕机等意外事件,并不会影响数据库中的数据,并 在下次服务器正常后事件发布定时器会去Event表中查找尚未发布的数据进行发布并更新消息状态为PUBLISHED.

如果是在更新库中的状态时发生了意外呢?此时消息已经发出到Kafka broker,则下次服务正常时,会将这些消息重新发送,但是因为有了Key的唯一性,部署模块判断这些是重复数据,直接忽略即可。

当产品部署成功后,Market事件监听器收到通知,准备更新数据库时发生了意外宕机等,下次服务正常启动后事件监听器会从上次的消息偏移量处进行监听并更新Event表。

消息接收方的处理

下面我们来看一下消息的接收方部署模块如何处理从Kafka Broker接收到的消息呢?

以下是部署模块对消息处理的流程图,此处部署模块的部署过程使用了简略的示意图。实际的场景中,部署动作以及更新状态是一个复杂的过程,同时可能依赖轮询来完成操作。

部署模块的事件监听器,在收到通知后,直接调用部署的Service,更新Deploy_table表中的业务逻辑,同时更新Event_Table中的消息状态。另一方面, 部署模块的 Event定时器,也会定时从Event_Table中读取信息并将结果发布到Kafka Broker, 市场模块收到通知后进行自己的业务操作。

至于采用这种模式的原理以及原因和市场领域类似,这里不再赘述。

3.引入补偿+幂等机制,

保证消息投递的可靠性

刚才也谈到,Kafka等市面上的大多数消息系统本身是无法保证消息投递的可靠性的。所以,我们也必须要从业务上对消息的意外情况进行保证。下面,我们探讨一下如何从业务上来保证消息投递的绝对可靠呢?

这里,我们就要引入 补偿机制+幂等操作,我们在前面的步骤中已经将Event进行了数据库持久化,我们还需要以下几个步骤来从业务上对消息的绝对可靠进行保证:

一、完善事件表字段

我们在Event表中增加两个新的字段count和updateTime,用来标识此消息发送或者重试的次数。正常情况下,count为1,表示只发送一次。

二、定时补偿加错误重试

同时 使用异常事件发布定时器,每隔2分钟(此时间只是一个示例,实际应用中应大于业务中正常业务处理逻辑的时间)去Event表中查询状态为PUBLISHED的消息,如果对应的消息记录更新时间为两分钟之前的时间,我们就悲观的认为此消息丢失,进行消息的重发,同时更新字段updateTime并将count计数加1。

三、最后一道防线:对账记录,人工干预

如果发现重发次数已经大于5,则认为此时已经无法依靠消息系统来完成此消息的投递,需要最后的一道保障,就是记录下来并在日常进行的人工对账中人工审核。

 

四、幂等去重

何为幂等呢?因为存在重试和错误补偿机制,不可避免的在系统中存在重复收到消息的场景,接口的幂等性能提高数据的一致性.在编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。

由于我们的定时补偿机制,消息的消费端也应该保证部署服务的操作是幂等的,即针对同一条消息多次发送的情况,我们应该保证这个消息实际上只会执行一次。这里如果发现消息是重复发送的,则直接将数据库中的执行结果读出并将结果推送到broker中,从而保证了消息的幂等性。

现在我们来分析一下此种策略如何保证的消息的绝对投递:

每条消息的产生都会在数据库中进行记录,保证消息的不丢失。

异常消息发布定时器会定时去Event表中查看异常消息,发现没有回应的数据则认为消息丢失,进行消息补偿,重新发送,如果连续5次依然失败则认为发生了异常,进行记录并人工干预对账。

对于部署模块(消息的消费端),如果消息丢失,则市场模块就无法收到回应(对应的Event表记录中的状态也不会修改),最终效果也会同#2情况,市场模块进行消息重发,如果重发次数超出了限制则会触发对账记录的业务逻辑。

4. 总结

本文通过采用消息系统进行微服务间的通信,加上一些设计上的变更,既保证了正常情况下(99.9%以上的情况)的逻辑的正确执行,也保证了极端情况下的数据一致性,满足了我们的业务需求,同时依赖市面上消息中间件强大的功能,极大的提高了系统的吞吐量。

针对Kafka等本身不可靠的问题,我们又通过修改业务场景的设计来保证了在极端情况下消息丢失时消息的可靠性,对应的也保证了业务的可靠性。此处只是以Kafka举例,如果是顾虑Kafka的本身消息不可靠的限制,可以考虑使用RabbitMQ或RocketMQ等市面上流行的消息通信框架。

概括来说,此方案主要保证了以下4个维度的一致性:

本地事务保证了业务持久化与消息持久化的一致性。

定时器保证了消息持久与消息投递的一致性。

消息中间件保证了消息的投递和消费的一致性。

业务补偿+幂等保证了消息失败下的一致性。

使用此种方案的弊端就是编码会大幅增加,为不同的微服务间增加不少额外的工作量,同时会产生较多的中间状态。对于业务中时间要求苛刻的场景,此方案不合适。(此处却符合本文中举例的场景,因为产品的开通,需要对容器进行操作,本身就是一个耗时的过程。)

数据一致性是微服务架构设计中唯恐避之不及却又不得不考虑的话题。通过保证最终数据的一致性,也是对CAP理论的一个折衷妥协方案,关于此方案的优劣,也不能简单的一言而概之,而是应该根据场景定夺,适合的才是最好的。

所以,我们在对微服务进行业务划分的时候就尽可能的避免“可能会产生一致性问题”的设计。如果这种设计过多,也许是时候考虑改改设计了。

from:http://windpoplar.iteye.com/blog/2353205