Tag Archives: 微服务

自建一个简易的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

微服务化之缓存的设计

本文章为《互联网高并发微服务化架构实践》系列课程的第五篇

前四篇为:

微服务化的基石——持续集成

微服务的接入层设计与动静资源隔离

微服务化的数据库设计与读写分离

微服务化之无状态化与容器化

在高并发场景下,需要通过缓存来减少数据库的压力,使得大量的访问进来能够命中缓存,只有少量的需要到数据库层。由于缓存基于内存,可支持的并发量远远大于基于硬盘的数据库。所以对于高并发设计,缓存的设计时必不可少的一环。

一、为什么要使用缓存

为什么要使用缓存呢?源于人类的一个梦想,就是多快好省的建设社会主义。

多快好省?很多客户都这么要求,但是作为具体做技术的你,当然知道,好就不能快,多就没法省。

可是没办法,客户都这样要求:

这个能不能便宜一点,你咋这么贵呀,你看人家都很便宜的。(您好,这种打折的房间比较靠里,是不能面向大海的)

你们的性能怎么这么差啊,用你这个系统跑的这么慢,你看人家广告中说速度能达到多少多少。(您好,你如果买一个顶配的,我们也是有这种性能的)

你们服务不行啊,你就不能彬彬有礼,穿着整齐,送点水果瓜子啥的?(您好,我们兰州拉面馆没有这项服务,可以去对面的俏江南看一下)

这么贵的菜,一盘就这么一点点,都吃不饱,就不能上一大盘么。(您好,对面的兰州拉面10块钱一大碗)

怎么办呢?劳动人民还是很有智慧的,就是聚焦核心需求,让最最核心的部分享用好和快,而非核心的部门就多和省就可以了。

你可以大部分时间住在公司旁边的出租屋里面,但是出去度假的一个星期,选一个面朝大海,春暖花开的五星级酒店。

你可以大部分时间都挤地铁,挤公交,跋涉2个小时从北五环到南五环,但是有急事的时候,你可以打车,想旅游的时候,可以租车。

你可以大部分时间都吃普通的餐馆,而朋友来了,就去高级饭店里面搓一顿。

在计算机世界也是这样样子的,如图所示。

越是快的设备,存储量越小,越贵,而越是慢的设备,存储量越大,越便宜。

对于一家电商来讲,我们既希望存储越来越多的数据,因为数据将来就是资产,就是财富,只有有了数据,我们才知道用户需要什么,同时又希望当我想访问这些数据的时候,能够快速的得到,双十一拼的就是速度和用户体验,要让用户有流畅的感觉。

所以我们要讲大量的数据都保存下来,放在便宜的存储里面,同时将经常访问的,放在贵的,小的存储里面,当然贵的快的往往比较资源有限,因而不能长时间被某些数据长期霸占,所以要大家轮着用,所以叫缓存,也就是暂时存着。

二、都有哪些类型的缓存

当一个应用刚开始的时候,架构比较简单,往往就是一个Tomcat,后面跟着一个数据库。

简单的应用,并发量不大的时候,当然没有问题。

然而数据库相当于我们应用的中军大帐,是我们整个架构中最最关键的一部分,也是最不能挂,也最不能会被攻破的一部分,因而所有对数据库的访问都需要一道屏障来进行保护,常用的就是缓存。

我们以Tomcat为分界线,之外我们称为接入层,接入层当然应该有缓存,还有CDN,这个在这篇文章中有详细的描述,微服务的接入层设计与动静资源隔离

Tomcat之后,我们称为应用层,应用层也应该有缓存,这是我们这一节讨论的重点。

最简单的方式就是Tomcat里面有一层缓存,常称为本地缓存LocalCache。

这类的缓存常见的有Ehcache和Guava Cache,由于这类缓存在Tomcat本地,因而访问速度是非常快的。

但是本地缓存有个比较大的缺点,就是缓存是放在JVM里面的,会面临Full GC的问题,一旦出现了FullGC,就会对应用的性能和相应时间产生影响,当然也可以尝试jemalloc的分配方式。

还有一种方式,就是在Tomcat和Mysql中间加了一层Cache,我们常称为分布式缓存。

分布式缓存常见的有Memcached和Redis,两者各有优缺点。

Memcached适合做简单的key-value存储,内存使用率比较高,而且由于是多核处理,对于比较大的数据,性能较好。

但是缺点也比较明显,Memcached严格来讲没有集群机制,横向扩展完全靠客户端来实现。另外Memcached无法持久化,一旦挂了数据就都丢失了,如果想实现高可用,也是需要客户端进行双写才可以。

所以可以看出Memcached真的是设计出来,简简单单为了做一个缓存的。

Redis的数据结构就丰富的多了,单线程的处理所有的请求,对于比较大的数据,性能稍微差一点。

Redis提供持久化的功能,包括RDB的全量持久化,或者AOF的增量持久化,从而使得Redis挂了,数据是有机会恢复的。

Redis提供成熟的主备同步,故障切换的功能,从而保证了高可用性。

所以很多地方管Redis称为内存数据库,因为他的一些特性已经有了数据库的影子。

这也是很多人愿意用Redis的原因,集合了缓存和数据库的优势,但是往往会滥用这些优势,从而忽略了架构层面的设计,使得Redis集群有很大的风险。

很多情况下,会将Redis当做数据库使用,开启持久化和主备同步机制,以为就可以高枕无忧了。

然而Redis的持久化机制,全量持久化则往往需要额外较大的内存,而在高并发场景下,内存本来就很紧张,如果造成swap,就会影响性能。增量持久化也涉及到写磁盘和fsync,也是会拖慢处理的速度,在平时还好,如果高并发场景下,仍然会影响吞吐量。

所以在架构设计角度,缓存就是缓存,要意识到数据会随时丢失的,要意识到缓存的存着的目的是拦截到数据库的请求。如果为了保证缓存的数据不丢失,从而影响了缓存的吞吐量,甚至稳定性,让缓存响应不过来,甚至挂掉,所有的请求击穿到数据库,就是更加严重的事情了。

如果非常需要进行持久化,可以考虑使用levelDB此类的,对于随机写入性能较好的key-value持久化存储,这样只有部分的确需要持久化的数据,才进行持久化,而非无论什么数据,通通往Redis里面扔,同时统一开启了持久化。

三、基于缓存的架构设计要点

所以基于缓存的设计:

1、多层次

这样某一层的缓存挂了,还有另一层可以撑着,等待缓存的修复,例如分布式缓存因为某种原因挂了,因为持久化的原因,同步机制的原因,内存过大的原因等,修复需要一段时间,在这段时间内,至少本地缓存可以抗一阵,不至于一下子就击穿数据库。而且对于特别特别热的数据,热到导致集中式的缓存处理不过来,网卡也被打满的情况,由于本地缓存不需要远程调用,也是分布在应用层的,可以缓解这种问题。

2、分场景

到底要解决什么问题,可以选择不同的缓存。是要存储大的无格式的数据,还是要存储小的有格式的数据,还是要存储一定需要持久化的数据。具体的场景下一节详细谈。

3、要分片

使得每一个缓存实例都不大,但是实例数目比较多,这样一方面可以实现负载均衡,防止单个实例称为瓶颈或者热点,另一方面如果一个实例挂了,影响面会小很多,高可用性大大增强。分片的机制可以在客户端实现,可以使用中间件实现,也可以使用Redis的Cluster的方式,分片的算法往往都是哈希取模,或者一致性哈希。

四、缓存的使用场景

当你的应用扛不住,知道要使用缓存了,应该怎么做呢?

场景1:和数据库中的数据结构保持一致,原样缓存

这种场景是最常见的场景,也是很多架构使用缓存的适合,最先涉及到的场景。

基本就是数据库里面啥样,我缓存也啥样,数据库里面有商品信息,缓存里面也放商品信息,唯一不同的是,数据库里面是全量的商品信息,缓存里面是最热的商品信息。

每当应用要查询商品信息的时候,先查缓存,缓存没有就查数据库,查出来的结果放入缓存,从而下次就查到了。

这个是缓存最最经典的更新流程。这种方式简单,直观,很多缓存的库都默认支持这种方式。

场景2:列表排序分页场景的缓存

有时候我们需要获得一些列表数据,并对这些数据进行排序和分页。

例如我们想获取点赞最多的评论,或者最新的评论,然后列出来,一页一页的翻下去。

在这种情况下,缓存里面的数据结构和数据库里面完全不一样。

如果完全使用数据库进行实现,则按照某种条件将所有的行查询出来,然后按照某个字段进行排序,然后进行分页,一页一页的展示。

但是当数据量比较大的时候,这种方式往往成为瓶颈,首先涉及的数据库行数比较多,而且排序也是个很慢的活,尽管可能有索引,分页也是翻页到最后,越是慢。

在缓存里面,就没必要每行一个key了,而是可以使用Redis的列表方式进行存储,当然列表的长短是有限制的,肯定放不下数据库里面这么多,但是大家会发现其实对于所有的列表,用户往往没有耐心看个十页八页的,例如百度上搜个东西,也是有排序和分页的,但是你每次都往后翻了吗,每页就十条,就算是十页,或者一百页,也就一千条数据,如果保持ID的话,完全放的下。

如果已经排好序,放在Redis里面,那取出列表,翻页就非常快了。

可以后台有一个线程,异步的初始化和刷新缓存,在缓存里面保存一个时间戳,当有更新的时候,刷新时间戳,异步任务发现时间戳改变了,就刷新缓存。

场景3:计数缓存

计数对于数据库来讲,是一个非常繁重的工作,需要查询大量的行,最后得出计数的结论,当数据改变的时候,需要重新刷一遍,非常影响性能。

因此可以有一个计数服务,后端是一个缓存,将计数作为结果放在缓存里面,当数据有改变的时候,调用计数服务增加或者减少计数,而非通过异步数据库count来更新缓存。

计数服务可以使用Redis进行单个计数,或者hash表进行批量计数

场景4:重构维度缓存

有时候数据库里面保持的数据的维度是为了写入方便,而非为了查询方便的,然而同时查询过程,也需要处理高并发,因而需要为了查询方便,将数据重新以另一个维度存储一遍,或者说将多给数据库的内容聚合一下,再存储一遍,从而不用每次查询的时候都重新聚合,如果还是放在数据库,比较难维护,放在缓存就好一些。

例如一个商品的所有的帖子和帖子的用户,以及一个用户发表过的所有的帖子就是属于两个维度。

这需要写入一个维度的时候,同时异步通知,更新缓存中的另一个维度。

在这种场景下,数据量相对比较大,因而单纯用内存缓存memcached或者redis难以支撑,往往会选择使用levelDB进行存储,如果levelDB的性能跟不上,可以考虑在levelDB之前,再来一层memcached。

场景5:较大的详情内容数据缓存

对于评论的详情,或者帖子的详细内容,属于非结构化的,而且内容比较大,因而使用memcached比较好。

五、缓存三大矛盾问题

1、缓存实时性和一致性问题:当有了写入后咋办?

虽然使用了缓存,大家心里都有一个预期,就是实时性和一致性得不到完全的保证,毕竟数据保存了多份,数据库一份,缓存中一份,当数据库中因写入而产生了新的数据,往往缓存是不会和数据库操作放在一个事务里面的,如何将新的数据更新到缓存里面,什么时候更新到缓存里面,不同的策略不一样。

从用户体验角度,当然是越实时越好,用户体验越流畅,完全从这个角度出发,就应该有了写入,马上废弃缓存,触发一次数据库的读取,从而更新缓存。但是这和第三个问题,高并发就矛盾了,如果所有的都实时从数据库里面读取,高并发场景下,数据库往往受不了。

2、缓存的穿透问题:当没有读到咋办?

为什么会出现缓存读取不到的情况呢?

第一:可能读取的是冷数据,原来从来没有访问过,所以需要到数据库里面查询一下,然后放入缓存,再返回给客户。

第二:可能数据因为有了写入,被实时的从缓存中删除了,就如第一个问题中描述的那样,为了保证实时性,当数据库中的数据更新了之后,马上删除缓存中的数据,导致这个时候的读取读不到,需要到数据库里面查询后,放入缓存,再返回给客户。

第三:可能是缓存实效了,每个缓存数据都会有实效时间,过了一段时间没有被访问,就会失效,这个时候数据就访问不到了,需要访问数据库后,再放入缓存。

第四:数据被换出,由于缓存内存是有限的,当使用快满了的时候,就会使用类似LRU策略,将不经常使用的数据换出,所以也要访问数据库。

第五:后端确实也没有,应用访问缓存没有,于是查询数据库,结果数据库里面也没有,只好返回客户为空,但是尴尬的是,每次出现这种情况的时候,都会面临着一次数据库的访问,纯属浪费资源,常用的方法是,讲这个key对应的结果为空的事实也进行缓存,这样缓存可以命中,但是命中后告诉客户端没有,减少了数据库的压力。

无论哪种原因导致的读取缓存读不到的情况,该怎么办?是个策略问题。

一种是同步访问数据库后,放入缓存,再返回给客户,这样实时性最好,但是给数据库的压力也最大。

另一种方式就是异步的访问数据库,暂且返回客户一个fallback值,然后同时触发一个异步更新,这样下次就有了,这样数据库压力小很多,但是用户就访问不到实时的数据了。

3、缓存对数据库高并发访问:都来访问数据库咋办?

我们本来使用缓存,是来拦截直接访问数据库请求的,从而保证数据库大本营永远处于健康的状态。但是如果一遇到不命中,就访问数据库的话,平时没有什么问题,但是大促情况下,数据库是受不了的。

一种情况是多个客户端,并发状态下,都不命中了,于是并发的都来访问数据库,其实只需要访问一次就好,这种情况可以通过加锁,只有一个到后端来实现。

另外就是即便采取了上述的策略,依然并发量非常大,后端的数据库依然受不了,则需要通过降低实时性,将缓存拦在数据库前面,暂且撑住,来解决。

六、解决缓存三大矛盾的刷新策略

1、实时策略

所谓的实时策略,是平时缓存使用的最常用的策略,也是保持实时性最好的策略。

读取的过程,应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。如果命中,应用程序从cache中取数据,取到后返回。

写入的过程,把数据存到数据库中,成功后,再让缓存失效,失效后下次读取的时候,会被写入缓存。那为什么不直接写缓存呢?因为如果两个线程同时更新数据库,一个将数据库改为10,一个将数据库改为20,数据库有自己的事务机制,可以保证如果20是后提交的,数据库里面改为20,但是回过头来写入缓存的时候就没有事务了,如果改为20的线程先更新缓存,改为10的线程后更新缓存,于是就会长时间出现缓存中是10,但是数据库中是20的现象。

这种方式实时性好,用户体验好,是默认应该使用的策略。

2、异步策略

所谓异步策略,就是当读取的时候读不到的时候,不直接访问数据库,而是返回一个fallback数据,然后往消息队列里面放入一个数据加载的事件,在背后有一个任务,收到事件后,会异步的读取数据库,由于有队列的作用,可以实现消峰,缓冲对数据库的访问,甚至可以将多个队列中的任务合并请求,合并更新缓存,提高了效率。

当更新的时候,异步策略总是先更新数据库和缓存中的一个,然后异步的更新另一个。

一是先更新数据库,然后异步更新缓存。当数据库更新后,同样生成一个异步消息,放入消息队列中,等待背后的任务通过消息进行缓存更新,同样可以实现消峰和任务合并。缺点就是实时性比较差,估计要过一段时间才能看到更新,好处是数据持久性可以得到保证。

一是先更新缓存,然后异步更新数据库。这种方式读取和写入都用缓存,将缓存完全挡在了数据库的前面,把缓存当成了数据库在用。所以一般会使用有持久化机制和主备的redis,但是仍然不能保证缓存不丢数据,所以这种情况适用于并发量大,但是数据没有那么关键的情况,好处是实时性好。

在实时策略扛不住大促的时候,可以根据场景,切换到上面的两种模式的一个,算是降级策略。

3、定时策略

如果并发量实在太大,数据量也大的情况,异步都难以满足,可以降级为定时刷新的策略,这种情况下,应用只访问缓存,不访问数据库,更新频率也不高,而且用户要求也不高,例如详情,评论等。

这种情况下,由于数据量比较大,建议将一整块数据拆分成几部分进行缓存,而且区分更新频繁的和不频繁的,这样不用每次更新的时候,所有的都更新,只更新一部分。并且缓存的时候,可以进行数据的预整合,因为实时性不高,读取预整合的数据更快。

有关缓存就说到这里,下一节讲分布式事务。

from:https://mp.weixin.qq.com/s/-9wHpKGf7aJSbtShpCcoVg

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

微服务是当下的热门话题,今天来聊下微服务中的一个敏感话题:如何保证微服务的数据一致性。谈到分布式事务,就避免不了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