微服务架构

微服务架构

注意:本文涉及面大而广,预计完整阅读时间在1个小时,可以挑章节选择性阅读。

简介

什么是微服务,以下定义来自维基百科

1
2
微服务 (Microservices) 是一种软件架构风格,它是以专注于单一责任与功能的小型功能区块 (Small Building Blocks) 为基础,
利用模块化的方式组合出复杂的大型应用程序,各功能区块使用与语言无关 (Language-Independent/Language agnostic) 的 API 集相互通信。

所以微服务是一种架构层面的概念,它是一种通过一套小型服务来开发单个应用的方法,每个服务运行在自己的进程中,并通过轻量级的机制进行通讯, 通常是通过HTTP。

微服务历史

微服务这个概念在2012年开始出现,作为加快应用程序开发进程的一种方法。

随后微服务架构实践开始在各大软件大会上被分享。2014年Martin Fowler发表文章《Microservices》,这是第一篇详细介绍微服务的文章。对微服务进行了定义,并与传统架构进行了对比,阐述了微服务的优势。

到了2015年,微服务已经很流行,凡是新建的项目,不论是初创公司还是老牌大厂,微服务都会是首选的架构。互联网企业是这样,就算是嵌入式环境下,微服务也被引入并且推广。

微服务与SOA

说到微服务,就不得不说SOA(Service-oriented architecture)架构。我们难免会有个疑问,微服务与SOA是什么关系,有的人说他们两个完全不同,有自己独特的应用范畴,有人认为他们两个类似,具有相同的工作原理,还有人说微服务是一种细粒度的SOA,或者微服务是SOA的一种应用。我们现在来找一下他们之间的关系。

应用范围

通常我们提到SOA的时候,似乎总是接受了SOA是拥有企业范围的,应用程序以服务的形式在该范围内彼此通信。SOA要求应用程序通过标准化接口来提供服务。而微服务架构似乎是拥有应用程序范围,仅关注一个应用程序内部的结构和组件。下图展示了这个差别。

fanwei

图片来自 IBM Developer

SOA架构侧重

当你分析SOA时,你会发现SOA包含两个侧重面。

一个是侧重集成。这个方面是指,SOA的目的之一就是适配现有系统中的的专用数据格式、协议、传输机制,并使用标准化的机制将这些适配公开为服务。这部分描述大概等于我们通常说的ESB(企业服务总线)。

适配和以标准化方式公开服务是SOA必不可少的,他侧重于系统集成和程序设计。见下图左。

另一个侧重面是功能重构,这个方面是指,如果当前系统提供了太细粒度的功能接口,导致公开了系统内部太多复杂的业务模型;或者所需的数据分散在多个数据系统;或者数据模型所使用的术语不同于业务部门,那么我们就需要创建新的程序,将多个现有的系统绑定在一起,这些程序在SOA的服务组件框架(Service Component Architecture,SCA)中被叫做服务组件,服务组件组成程序集,程序集构成应用(也叫业务组件)。

见下图右。

图片来自 IBM Developer

微服务架构侧重

从上面SOA的侧重中可以看到,从集成方面来讲,SOA跟微服务毫无关系,而从功能重构方面来讲,SOA和微服务是有类似的。

微服务主要强调在应用程序内部分解成服务组件,这点跟SOA的功能重构是相似的。实行微服务架构的应用,从外部来看,应用仍是相同的,是否使用微服务架构将不影响API的数量和API的粒度。微服务的微反映在内部组件的粒度。如下图,微服务应用程序在应用程序边界上公开与单体应用程序相同的接口。

sameapi

图片来自 IBM Developer

那么SOA和微服务在组件分离上有没有什么差别呢,其实一直以来,在应用内进行组件分离不是什么新鲜事,有很多实践被用来在应用内干净地分离逻辑。而微服务的特点是将分离更加彻底,把组件进行了绝对隔离,他们变成了网络上单独运行的进程。如下图,展示了从单个庞大的应用程序到微服务的变化过程。

compose

图片来自 IBM Developer

归纳差异

差异可以归纳为:

SOA面向企业范围,微服务面向应用范围。

SOA带有异构集成的语义,微服务没有这个语义。

SOA服务内部支持组件分离的架构,微服务则是更彻底的组件分离架构-组件在网络上隔离。

微服务的特征

下面的特征都是Microservices文章中提到的,虽然非常全面和系统,但是原文比较难懂,我只简单总结他。

服务组件化

在构建软件的时候,我们非常希望可以像现实世界拼装物件一样,通过拼接组件来创建一个应用。我们对组件的定义是:组件是一个可独立替换和独立升级的软件单元。

在过去,我们为大部分语言平台创建了大量的通用库。库作为组件的形式出现,库被连接到一个程序中,并通过函数调用的方式被使用。

微服务架构也会使用库,但是他们主要的组件化方式是把软件拆分成多个服务,这些服务运行在不同的进程中,通过web请求或者rpc进行交互。

微服务做这种拆分的好处是这些服务可以被独立的部署。如果你使用库来构建你的应用,当你修改其中一个库的时候,你就必须重新部署整个应用。但是如果应用被拆分成多个服务,你只需要部署你修改的那个服务就可以了。当然这并不是绝对的,如果你修改了接口,可能导致相关的服务得跟着一起重新部署。一个好的微服务架构需要最小化这种关联,通过内聚服务的边界,以及通过服务合约来进行服务的进化,减少这类改动。

服务组件相比库有个缺点,通过网络进行远程调用的成本更大,因此为了减少交互,服务接口通常是粗粒度的,这相对来说接口更复杂更难用。另外当你想要更改组件之间的职能分配时,跨进程会带来更大困难,因为不同服务持有的资源,数据是不同的,不一定能很轻松的实现其他服务能实现的功能。

另外,一个服务通常是只需要一个进程,但是也不一定,比如一个服务可能需要一个业务进程加上一个数据库进程。

组件化的服务在扩展时跟单体应用有很大的差别,主要表现在服务的按需扩展这一点,如下图:

servicescale

图片来自 微服务

单体应用不得不整体一起扩展,而组件化的服务则只在需要时才扩展。

业务全栈化

当我们想要拆分大型的应用时,通常的做法是按照职能分,比如UI,业务,DB,如下图左。这样的拆分有一个问题,就是当一个新功能被引入时,修改可能会涉及到UI+业务+DB,这样就会很容易带来跨部门的协作, 跨部门的协作往往是低效和高成本的。于是慢慢的,UI团队为了避免跨部门协作,把部分业务写进了UI层, 业务层为了跨部门协作,把DB和UI的业务写入了业务层。这将导致业务逻辑到处都是。

而微服务的拆分方式是不一样的,他会按照业务来拆分,这个服务将会包含广泛的技术栈,包括UI,包括DB,包括业务逻辑,最终这样一个微服务团队是包含全栈技术,甚至包括PM的, 如下图右。

这样看起来微服务架构中,一个微服务就得好多人。事实上不一定,有的团队中,一个服务一个人,有的团队里一个服务10个人,所以一个服务几个人还是不一定的。

seperatebybiz

图片来自microservices

项目产品化

有时候我们开发一个项目的时候,一旦项目交付,这个项目就不归这个开发团队负责了,而是交给专门的维护团队。微服务的实践者往往不是这样,开发团队会负责整个产品的生命周期,开发者会因此经常的接触他们的服务,因为他们必须为这个产品负一定的支持责任。

产品化的思维对单体应用也是用,但是微服务这样粒度的软件,更容易让开发者与这个服务以及服务的用户建立联系,因为微服务的功能都非常明确,不容易发生责任的交叉。

通信简单化

在不同进程之间通信时,通常会去加强通信机制本身,比如ESB就包含复杂的机制在消息编排,消息路由,消息转换上。

微服务却偏爱更轻量的方式,最常被使用的两种协议,一个是基于HTTP请求的API,一个是基于轻量消息传递通道。轻量消息传递,意味着消息总线只负责消息传递,不做别的额外的工作。

相对于基于内存的函数调用,进程间的调用有所不同,如果还是像函数调用那样,会导致跨进程调用过多,性能受到影响,所以服务间的调用尽量是粗粒度的,而不像函数调用那样细粒度。

管理去中心化

集中式的管理的结果是在单一平台上实行标准化,但是这种方式的好处是有限的,因为这种解决方式并不是万能的,我们更加倾向于采用适当的工具解决适当的问题,整体式的应用在一定程度上比多语言环境更有优势,但通常不是的。

把应用拆分后,你可以在开发上有更多选择,可以用Node.js来开发页面,用C++来开发高效的组件,用NoSql替换Mysql来提高读写性能,这都可以。分成不同服务后意味着你有了更多的选择。

应用分拆后,你还可以分别去管理这些服务,服务团队将为他们开发的服务负责,包含7*24小时的运行。每天凌晨3点被报警吵醒会让你更加关注代码的质量。

应用分拆后,数据也被分拆,这对数据更新带来挑战。单体应用是使用事务来保证一致性,而分布式事务是出了名的难以实现,所以微服务架构强调服务之间的无事务协调,并且明确认识到一致性可能只是最终一致性,而问题也将通过补偿操作来处理。处理一致性问题是一个新的挑战,但是通常业务上允许存在一定程度的不一致来快速响应需求,同时使用某种恢复过程来处理错误。只要处理错误的代价低于强一致性带来的业务损失(主要是性能)那么这就值得。

交付自动化

基础设施自动化技术在过去几年中得到了长足的发展:云计算,特别是AWS的发展,减少了构建、发布、运维微服务的复杂性。许多使用微服务架构的产品或者系统,它们的团队拥有丰富的持续部署和持续集成的经验。团队使用这种方式构建软件将更广泛的依赖基础设施自动化技术。这是这种自动化技术的流程:

pipeline

一个单体应用可以被构建被测试然后被推送到线上环境中,而一旦你投资了一整个推送到线上环境的自动化路线,那么你会发现部署更多的应用并没那么可怕。毕竟部署一个应用的过程是枯燥的,而部署三个应用的过程还是枯燥的,没有差别。

我们看到团队使用广泛的基础设施自动化的另一个领域是管理生产环境中的微服务。但是与我们上面的断言”只要部署很无聊,单体和微服务之间没有太大的区别”相反,每个部署的运维环境可能会截然不同。

失效常态化

使用服务作为组件的一个结果在于应用需要有能容忍服务的故障的设计,任何服务调用都可能因为服务的不可用而失败,调用者就需要尽可能优雅的来处理这种结果。这对比单体应用是一个劣势,因为这引入了额外的复杂性。这也导致微服务团队时刻反思服务失败如何影响用户体验。

因为服务随时可能失败,所以能够自动检测失败并且自动恢复服务就非常重要。微服务应用程序投入大量比重来进行应用程序的实时监测,比如检查架构方面的指标(每秒多少次数据请求),比如检查业务相关指标(例如每分钟收到多少订单)。语义监测(定期运行一系列自动化测试用例,来测试线上环境是否有问题)可以提供一套早期预警系统,触发开发团队跟进和调查。

设计发展化

微服务从业者,通常有持续改进设计的背景,并且把服务组件化看做是一个更进一步的工具来控制应用的变化,同时不减缓应用的变化。控制变化不意味着减缓变化, 用正确的方法和工具,你可以频繁、快速且控制良好的更改你的应用。

当你决定将应用拆分成微服务时,你将面临如何设计你的服务的问题,你拆分服务的原则是什么。回头看组件的定义是一个可独立替换和独立升级的软件单元,因此我们可以想象我们需要寻找的拆分点是当我们重写了这个组件后,不会影响这个组件的相关协作者。

可替换性其实是一个更一般原则的特例,这个原则是通过变化来驱动模块化。你应该保持同一时间的变化只位于同一个模块中。很少变化的部分应该与大量变化的部分处于不同的模块中,而如果两个服务总是一起变化,那么这两个模块应该被合并。而可替换性其实就是一种可以独立变更的特性。

应用拆分成微服务后,给我们提供了更细粒度发布应用的机会。单体应用的任何修改都需要发布整个应用,而微服务,你只需要重新部署修改的服务。这个过程可以简化和加速整个流程。但坏处是,你必须担心一个服务的变化会阻断其消费者。传统的集成方法试图使用版本管理解决兼容问题,但是微服务世界的偏好是只把版本管理作为最后的手段,多版本会带来复杂的测试和维护成本。我们可以避免大量的版本管理,通过把服务设计成对其他服务的变化尽可能的宽容。

微服务周边

前面对微服务的特性做了很多的总结,那假如我们要在实践中使用微服务架构,我们需要做哪些准备呢,或者说我必须具备了什么条件,我才能引入微服务架构?

下图是与微服务相关的拓扑图,我们来详细展开讲解一下。

spiderdraw

图中包含3种颜色,绿色部分属于微服务运行时的4大基础设施,红色部分属于微服务架构的辅助支撑,蓝色部分属于微服务的优缺点。

首先我们来看绿色的4个分支,分别是服务发现,服务配置,服务监控,请求追踪。

服务发现

微服务架构中,再常见不过的莫过于扩容缩容了,那某个服务的上游如何应对下游的扩容缩容的。一种糟糕的方式是, 上游配置下游的ip列表,下游服务扩容缩容后通知所有的上游,让他们更新ip列表,这种方式有明显的缺陷,比如扩容还好一点只是上游不能很快发现新机器,缩容的话就麻烦了,上游会因为请求不到部分机器而报错;同时这种方式把服务严重耦合在一起,牵一发而动全身。

所以我们常常使用服务发现的机制来应对服务的扩缩容,在服务启动成功后,把自己的服务地址(ip,端口等信息)注册到注册中心,上游服务则通过订阅或者轮询的方式从注册中心获取下游服务的地址,动态更新下游的ip列表。

图片来自infoQ

注册中心的原理当然非常简单,但是还是有一些注意点要提一下,

获取本地地址

获取本机地址的方式通常有两种,一种是获取指定网卡的ip地址或者直接获取非回环地址的ip地址。

一种是与注册中心建立socket连接,然后获取该socket的本地地址来作为注册的IP。

优雅下线

服务下线一般是在应用退出之前调用注册中心来下线服务,但是这里有两个隐患,一个是下线接口可能因为网络原因耗费很长的时间,导致服务长时间无法下线成功,上游服务还是会继续请求过来;一个是调用下线接口后,要记得做业务清理,也就是要等待当前正在处理的业务完成,否则可能出现数据丢失,文件损坏,响应丢失,交易中断等问题。

通常的解决方案是,添加进程退出的钩子,下线服务时,脚本发送kill给进程,进程调用下线接口,同时给下线接口添加超时时间,比如30s,如果超时了这个请求还没有返回,就调用kill -9来杀死该进程。 而在下线服务时,进程可以异步去做业务清理。下线流程如下:

graceoffline

图片来自 优雅停机方案

扩展元数据

简单地将 IP 和 port 信息注册上去,可以满足基本的服务调用的需求,但是在业务发展到一定程度的时候,我们还会有这些需求:

想知道某个 HTTP 服务是否开启了 TLS.

对相同服务下的不同节点设置不同的权重,进行流量调度。

将服务分成预发环境和生产环境,方便进行切流。

不同机房的服务注册时加上机房的标签,以实现同机房优先的路由规则。

一个良好的服务注册中心在设计最初就应该支持这些扩展字段。

探活机制

由于服务自身的注册混合下线并不总是成功,所以注册中心和服务之间还需要有额外的探活机制来检测服务是否在线。通常有两种模式,客户端模式和服务端模式。

客户端模式是指客户端定时向注册中心发送心跳来表明服务状态正常,心跳可以是TCP形式,也可以是http形式,可以通过短连接形式,也可以通过长连接形式。ZooKeeper维持的session本质上是一种长连接的客户端心跳机制。

服务端模式是指注册中心主动调用服务发布的某个接口,返回结果成功表示服务状态正常。这个接口可以是HTTP接口,也可以是RPC接口。服务端模式相较客户端模式的好处是除了判断出服务活着之外,还能判断出服务工作正常。但是缺点是每个服务发布的接口不同,无法做到通用。

推拉问题

服务的扩缩容通知机制通常都是基于观察者模式,一个服务的节点更新后,会推给订阅该服务的所有服务。是一种推的模式。

但是推的模式会面临丢失的问题,比如你下线的时间中他更新了,或者网络问题持续推送失败,导致丢失更新。所以注册中心支持拉的机制是很必要的, 服务可以在刚上线时拉取所有关注的服务,并定时向注册中心拉取服务信息。

本地容灾

首先,下游服务的信息需要在内存中缓存,防止注册中心发生不可用。

接着,本地缓存文件也需要缓存下游服务信息,防止注册中心不可用时,服务自己发生了重启,这时内存中的信息就丢失了,本地缓存文件可以提供下游服务信息。

还有,你需要提供本地容灾文件夹,文件夹中正常时候没有文件,当注册中心长时间不可用,而下游服务又在期间发生了变更时,我们就可以通过在容灾文件夹中添加容灾文件来启动本地容灾,这时服务会忽略本地缓存文件,而从容灾文件中读取配置。此时效果等价于直接修改掉本地缓存文件。

LVS+服务树

另一种自动感知服务扩缩容的方式是借助LVS。LVS是一种负载均衡方案,即服务对外提供一个VIP,上游访问VIP就可以自动均匀的访问服务的多台机器,而无需知道每一台服务器的ip。

微服务架构的公司一般都有服务树,当服务进行扩缩容时,服务树相应的就会发生变更,这时服务树系统可以更新VIP下面所挂的机器列表,而上游服务可以继续无感知的调用VIP。这就相当于服务注册的工作交给服务树系统来完成了,服务获取的工作则交给LVS来完成了。VIP机制的唯一问题是有的时候服务树上的节点迁移或者拆分,相应的VIP就会发生更改,这种情况上游就得手动切换VIP了。

不知道你有没有考虑过一个问题,就是注册中心本身的扩缩容是如何被感知的。

通常的做法是注册中心的VIP通过公司全局的一个URL暴露出来,注册中心本身的扩缩容通过VIP屏蔽。

关于LVS的工作原理可以参考LVS的原理-工作模式

架构举例

我们已经可以完全依赖开源组件来搭建服务发现功能了,比如etcd+registrator+confd,这分别是三个独立的组件,其中etcd负责数据存储,registrator负责服务注册,confd负责数据dump。
etcd是一个强一致性的存储服务,在CAP机制中追求CP。
registrator通过监听docker的unix套接字来获知容器的启动和停止事件,从而获取该变更容器的信息,把其中ip和port等信息注册到etcd中去,一台物理机只需要部署一个registrator。
confd会监听etcd中配置的变化,然后根据配置的模板生成配置文件,接着执行指定的命令。

etcdbasedregistry

图片来自 Service Discovery

注意etcd追求CP,因此etcd本身会存在不可用的短暂时期,这样存在丢失更新的可能,比如registrator检测到修改并注册给etcd,但是etcd不可用,这时候更新就丢失了。registrator应对这种情况的方式是watch+轮询,除了监听容器的启动和停止事件外,还会定时轮询全量的容器,依然可以在轮询中检测到容器变更。

可以看到服务发现在架构上需要具备3大组件,一个负责存储,一个负责注册,一个负责dump。在各个公司自研的服务发现框架中,经常可见把registrator和confd功能合并到一个组件中,同时可能会在每个container中部署一个sdk或者agent,来支持更加灵活的服务注册和dump加载机制。

服务配置

微服务节点数量非常多,通过人工登录每台机器手工修改,效率低,容易出错,特别是在部署和排除故障时,需要快速增删改配置,人工操作显然是不行的。除此之外,有的配置需要运行期动态修改和调整,人工操作是无法做到的。所以微服务需要一个统一的配置中心来管理所有微服务节点的配置。配置中心包括版本管理,节点管理,配置同步等功能。

注意,我们通常使用的灰度放量,AB实验等流量控制方法,其本质是把流量分组规则下发到某个节点上,这个节点可能是一个router,可能是某个服务,然后在这个节点上的程序读取配置,根据分组规则得出流量属于哪个组,由此来决定后续的流程或者后续的下游。所以我把各种实验的机制也归类到服务配置当中。

微服务配置中心往往支持下面这些特性中的大部分,但是不一定是全部。

配置实时推送

当配置发生变更的时候,配置中心需要将配置实时推送到应用客户端。配置变更有的是通过hook Git Repo的更新,有的是通过在控制台修改配置,或者可能还是别的触发途经。而配置除了有更新时实时推送之外,通常也会支持定时拉取来作为补充。

配置版本管理

当配置变更不符合预期的时候,需要根据配置的发布版本进行回滚。配置中心需要具备配置的版本管理和回滚能力,可以在控制台上查看配置的变更情况或进行回滚操作。

配置灰度发布

配置的灰度发布是配置中心比较重要的功能,当配置的变更影响比较大的时候,需要先在部分应用实例中验证配置的变更是否符合预期,然后再推送到所有应用实例。

权限管理

配置的变更和代码变更都是对应用运行逻辑的改变,重要的配置变更常常会带来很大的影响,对于配置变更的权限管控和审计能力同样是配置中心重要的功能。

配置中心往往以项目纬度或者配置纬度进行权限管理,配置的owner可以授权其他用户进行配置的修改和发布。

多集群支持

当对稳定性要求比较高,不允许各个环境相互影响的时候,需要将多个环境的配置进行物理隔离。也就是说需要支持每个环境有单独的配置中心。这个往往通过部署多套系统来支持。

多环境支持

当多个环境不需要严格的物理隔离的时候,我们可以通过配置中心的逻辑隔离方式来做到多环境支持。

比如可以通过在创建配置的时候就要求指定好配置所在的环境,这样配置只会下发到指定的环境。

比如通过支持配置的命名空间,不同环境的客户端访问不同命名空间的配置即可。

多语言支持

由于微服务可能由不同的语言组成,因此支持多语言也是一个重要的特性。

一个可行的方案是配置中心提供通用的HTTP API,然后官方支持的语言通过提供的SDK接入,暂时不支持的语言也可以通过低成本进行接入。

服务监控

微服务在落地的过程中,监控是其中需要关注的重点之一,微服务的监控面临着众多的难点:

监控对象动态可变,无法预先知晓。

监控范围和种类繁杂,各类监控难以互相融合

软件系统通常会被拆分为数十甚至数百个微服务,这种拆分会使得监控数据爆炸增长

监控系统本身必须保证可靠,必须支持云上部署,以及快速水平扩容

这其中,对象动态可变我们容易理解,数据量暴增我们容易理解,而系统高可用和可扩展可以参见一般的高可用方案,剩下来,我只重点列举一下监控的种类,包含了机器监控,进程监控,metrics监控,中间件监控,和日志监控。

机器监控

机器监控主要对微服务实例所运行的基础设施进行监控,包括设施的运行状态,资源使用情况。一般微服务会运行在容器中,因此这个监控通常即包含物理机器又包含了容器的运行状态和资源(CPU,内存,磁盘,网络)使用情况。

进程监控

进程监控主要对微服务实例进行监控,包括进程的端口使用状况,以及进程的资源(CPU,内存,磁盘,网络)使用情况。

metrics监控

metrics主要对微服务调用指标进行监控,包括接口的请求总数(qps),请求时延(latency),成功率(success ratio)。

中间件监控

通常一些中间件,比如Mysql,Redis,MemCached,Kafka等不会算作自己的服务,只是算作一个依赖项,但是他们的状况却对服务本身可用性有重要指向。

这些中间件的监控项除了qps和latency之外,还包括缓存命中率,消息阻塞个数,消费延迟时间等。

日志监控

日志监控通常是监控自定义的业务数据和指标,程序把信息记录到日志中,监控系统通过实时匹配日志来得到监控指标。

这些指标包括进程core,进程panic,业务降级率,warning数,客户端版本统计等。

与上面监控配套的是服务报警功能,监控系统要做的是能够配置好报警阈值,报警接收人,报警分级,报警说明等信息。

请求追踪

微服务的特点决定了服务的部署是分布式的,大部分功能模块都是单独部署运行的,彼此之间通过复杂交错的通信网络交互,这种架构下,请求会经过很多个微服务的处理和传递,我们难免会遇到这样的问题:

分散在各个服务器上的日志怎么处理?

如果业务流出现了错误和异常,如何定位是哪个点出的问题?

如何跟踪业务流程的处理顺序和结果?

Dapper

基于这些问题,Google公司研发了Dapper分布式跟踪系统,发布了相关论文,以此提供了分布式跟踪的实现思路。在这套跟踪系统理论中,有3个核心概念:

traceID: 用来标识每一条业务请求链的唯一ID,需要全局唯一,TraceID需要在整个调用链路上传递。

Annotation:业务自定义的埋点信息,可以是手机号、用户ID等关键信息。

span:请求链中的每一个环节为一个Span。

每一个span有一个spanID来标识自身这个环节,parentID表示前一个span的ID(你可以认为span信息是一个结构体,包含多个属性),然后依次形成请求链,所以spanID也需要向下游传递。除此之外span中的信息还可以包括请求开始时间,结束时间,RPC调用的调用方信息和被被调用方信息。当我们希望一个spanid可以对应一个RPC请求时,我们需要做到spanid全局唯一,如果没有这个需要,spanid不必全局唯一。下图是串接span的示意图。

spanids

图片来自Dapper论文

基于这3个概念,跟踪系统的实现可以分成4个步骤。

日志记录

每一个服务在日志中记录下traceid,span信息,和各自需要的annotation信息。

假如公司层面有一些公共的网络库或者日志库,可以把记录traceid和spanid的工作隐藏起来,做到无侵入,服务方只需要记录自己关注的annotation信息即可。

1
2
3
4
5
6
来自http://jm.taobao.org/2014/03/04/3465/
淘宝鹰眼的实现是:

在前端请求到达服务器时,应用容器在执行实际业务处理之前,会先执行EagleEye的埋点逻辑,分配全局唯一traceID,并存储在ThreadLocal,同时存储到ThreadLocal还有一个RpcID(等同于spanID),此时RpcID=0.随后RPC框架每次发起RPC调用都会从ThreadLocal取出RpcId,traceID,并把RpcID+1,与traceID一起通过网络传递到RPC的对端。
对端服务收到RPC请求时,从请求附件取出traeID和RpcID,并存入ThreadLocal,如果还要调用下游则重复前面的步骤。
这里可以看到服务各方完全不需要关注埋点的细节。

日志收集

通过安插在每台服务机器上的agent进行日志收集,比如使用logstash。

收集来的海量数据需要存储到专门的存储集群中去,比如使用HDFS,HBASE。

eagleeyestore

图片来自 鹰眼下的淘宝

日志分析

分析收集来的日志数据,分离线和实时。

离线分析:

根据traceID汇总调用日志;

根据spanID还原调用关系;

分析链路状态,比如耗时等;

实时分析:

对单条日志直接分析,不需要汇总,比如建立索引;

得到链路上的调用情况,比如QPS,latency,error ratio;

日志展示

通过分布式跟踪系统的可视化监控页面,展示调用链和各项统计信息,避免去服务器上查看日志的烦恼。

接下来我们来看图中红色部分,服务部署和服务通信,这两块内容在别的架构中也有存在,只是微服务有他自己的特点。服务部署和服务通信,属于微服务的辅助支撑。

服务通信

我们在前面列举微服务特征时就提过,微服务的通信是简单化的,它强调使用轻量的通信机制,包括轻量的消息通道,轻量的消息协议。

消息通道

轻量消息通道意味着消息总线只负责消息路由,不处理消息编排(组合排序等),消息转换等额外的工作。有的同学说我们的微服务不需要消息总线,我们都是服务之间直连的,这样是不是更轻量呢?我们要说明,作为消息通道的消息总线并不总是需要的,这很大程度上取决于业务模型,消息总线适合生产者-消费者模型,生产者发布消息,消费者订阅消息并消费消息。

我们以RabbitMQ为例,RabbitMQ作为一个消息代理来实现分布式系统之间的通信,从而促进微服务的松耦合。

rabbitmq

图片来自RabbitMQ下的生产消费者模式与订阅发布模式

生产者和消费者都是通过TCP连接到RabbitMQ的BrokerServer,生产者把消息发布到Exchange,并指明RoutingKey,生产者不关心有哪些Queue和放到哪些Queue,由Broker负责根据RoutingKey把消息路由到相关的Queue,根据规则(完全匹配或者通配符匹配或者广播),一个消息是可以路由到多个Queue的。同一个Queue也可以被多个消费者消费,来分担某一类消息的流量。Queue是事先创建好的,并指定了RoutingKey的,这个RoutingKey跟生产者指明的RoutingKey的意义一样,只是Queue的RoutingKey可以包含通配符用来匹配一类消息。RabbitMQ还为生产者端提供了Confirm机制,为消费者端提供了ACK机制来确保消息成功进入Queue和成功被消费。

可以看到,RabbitMQ所做的工作就是可靠地把生产者产生的消息路由到消费者手中,没有对消息本身做任何的操作。这是符合微服务架构简单化通信的特征的。

通信协议

从不同公司的实践来看,微服务使用的协议多种多样,有基于HTTP的,有直接基于TCP的;有使用thrift的,也有protobuf或者JSON格式的。尽管在《microservices》一文中提到:

1
The two protocols used most commonly are HTTP request-response with resource API's and lightweight messaging.

”最常用的两种协议是基于资源API的HTTP请求-响应,和轻量的通信。“

资源API我们可以理解为面向资源的架构,是符合REST风格的一种架构。

1
2
3
4
5
6
来自《RESTFUL WEB SERVICES中文版》:http://www.sendsms.cn/box/dl/_25B1_25E0_25B3_25CC_25BC_25BC_25CA_25F5/_25D4_25AD_25C0_25ED_25CB_25BC_25CF_25EB/_25C9_25E8_25BC_25C6_25BC_25DC_25B9_25B9/RESTful_Web_Service.pdf
第80页

REST并不是一种架构,而是一组设计原则,你可以讲“在遵守这些原则方面,一个架构做得比另一个架构好”,但是你不能讲"REST架构",因为不存在一个叫"REST架构"的东西。
...
作为一组设计原则,REST是非常通用的。具体地说,它并不限定于WEB,REST不依赖于HTTP机制或者URI结构。但因为我讨论的是WEB服务,所以特地用WEB相关技术来讲解面向资源的架构(ROA)。我想在特定的编程语言中探讨如何用HTTP和URI来实现REST。假如将来出现非基于WEB的REST式架构,它的最佳实践将根ROA的差不多,只是具体细节会有点差别。

但同时在文章的注释处也说明了,在极端规模下,有些组织会转而使用二进制的协议,但这并不会影响微服务通信简单化的特性。
既然协议没有严格限定,那我们来讨论下不同通信协议的选择问题。

调用方式

调用方式主要有REST和RPC,我们来对比看看。正如上面来自《RESTFUL WEB SERVICES中文版》的内容所说,REST原则并不限定于WEB,也不依赖于HTTP。但是为了讨论方便,我们以最常见的基于HTTP的WEB服务来作为使用REST设计原则的代表。

REST是一种架构设计原则,主要有这些特征:

可寻址性,它使用URI定位资源,一个URI只能指示一个资源;

无状态性,请求里包含服务器所需的全部信息,服务器不依赖于任何之前请求提供的信息;

连通性,客户端应用状态自己保存,并在服务器提供的链接的指引下发生变迁;

接口一致性,使用统一的方法来操作资源,对于HTTP而言,统一的操作方法是GET/POST/PUT/DELETE/OPTIONS/HEAD

可以看到REST使用基于文本的应用层协议,效率低;REST利用通用的协议HTTP来发布服务,因此各种语言都能轻松接入;REST采用统一接口 ,使得每个服务都以同样的方式使用HTTP接口。

RPC即远程方法调用,应用可以像调用本地方法一样调用远程方法。RPC调用往往跨越传输层和应用层,RPC框架的客户端会基于TCP或者UDP来传递带请求参数的数据到服务端,服务端经过同样的流程返回数据到客户端,使用方可以不关注网络细节。

使用RPC的服务更在意性能,他们通常选择二进制协议,效率高;同时RPC需要自己制定报文结构,这导致他的通用性受到影响。比如最简单的结构定义如下,试想,每种语言为了接入它,都得实现一遍Parser,不像HTTP已经是一种通用的协议,他的Parser已经被广泛实现。

1
2
3
4
5
6
# 一种最简单的私有报文结构
# 前8个字节为magic number,表示协议的开端,接着8个字节为payload的长度,随后跟着长度为length的payload
┌────────────────────────────────────┐
│ magic │ length │ payload │
└────────────────────────────────────┘

相应的,http的报文结构:

httpstruct

图片来自 网络协议之Tcp、Http

所以总结如下,应用可以根据需要选择一个合适的服务调用方式:

比较项 REST RPC
传输协议 HTTP TCP/UDP
性能
通用性
序列化协议

序列化协议用来做什么呢,我们对照前面的私有报文结构来看,序列化协议是关于你如何把你的数据序列化到私有协议的payload(对应http中的body),以及如何把payload反序列化成你要的数据的过程。不管是REST还是RPC,序列化协议都是没有严格定义的,你可以自己选择。关于选择序列化协议的依据,我建议参考下面几个维度。

流行度

流行度高不光意味着他在跨语言跨平台方面更突出,还意味着更低的学习成本,更稳定的功能库,和更小的风险。

性能

性能包括空间开销和时间开销。

空间开销:序列化需要在原有的数据上加上描述字段,以为反序列化解析之用,过多的描述信息会对网络和磁盘带来巨大压力,特别在海量数据下。

时间开销:复杂的序列化协议导致过长的编解码时间,过长的时间会导致系统响应缓慢。

可调试性

序列化后的数据正确性在调试中比较困难,难以确定是序列化方还是反序列化方导致的问题。

所以具有较好可调试性的序列化协议需具备一些特点,

一种是序列化库需要提供序列化成可读数据的接口,便于查看数据可定位问题(protobuf为例)。

一种是序列化后的数据本身具有可读性,这方面xml和json就具备人眼可读的特点。

可扩展性

如果能够新增业务字段,而不影响老的服务,这将大大提高系统的灵活性和扩展性。

安全性

这一点主要是指当需要满足加密要求的时候,你如何支持。

选项一是你的序列化库能集成到https,这样一来加密的要求可以满足。

选项二是你有自身的ssl方案。比如thrift自身实现ssl传输层方案

我们可以想到并没有一个协议是样样都占优的,比如二进制协议的性能往往好于字符形式的,但是二进制协议的可调式性却往往差于字符形式的。

我们只需要根据自己的业务场景和特点,选择最适合你的序列化协议。

服务部署

服务发布和治理

在前面讲微服务特征的时候讲到了微服务的交付自动化:

pipeline

我们可以知道自动化交付是微服务架构必不可少的条件。那除了自动化交付外,还有哪些部署相关的措施是微服务必须的呢?

容器已经被社区接受为交付微服务的一种理想手段,一个轻量级的基于容器的服务部署平台主要包括容器调度系统,发布系统,镜像治理中心,资源治理平台等模块。

容器调度

屏蔽容器细节,将整个集群抽象成容器资源池,支持按需申请和释放容器资源,物理机发生故障时能够实现自动故障迁移 (fail over)。比如kubernetes, mesos, swarm. 这三个项目都能够实现对容器的编排调度,历史上也存在过这三个项目的竞争:

1
2
3
4
5
来自《阿里巴巴云原生实践15讲》https://developer.aliyun.com/special/mvp/cloudnative
容器编排之争:
相比于 Docker 体系以“单一容器”为核心的应用定义方式,Kubernetes 项目则提出了一整套容器化设计模式和对应的控制模型, 从而明确了如何真正以容器为核心构建能够真正跟开发者对接起来的应用交付和开发范式.
而 Docker公司(的swarm)、 Mesosphere公司(的mesos)以及 Google公司(的Kubernetes)在“应用”这一层上的不同理解和顶层设计,其实就是所谓“编排之争”的核心所在。
2017 年末,Google在过去十年编织全世界最先进的容器化基础设施的经验,最终帮助Kubernetes项目取得到了关键的领导地位,并将CNCF(Cloud Native Computing Foundation)这个以“云原生” 为关键词的组织和生态推向了巅峰。

说到容器调度说到kubernetes,就必须讲到云原生应用。

我们知道kubernetes调度的是容器,容器包含的是应用,这里面的应用就是云原生应用。

云原生的意思,一句话解释是应用生而为云。是指应用专门就是为了在云平台部署运行而设计开发的。但是其实大多数传统的应用,不做任何改动,都是可以在云平台运行起来的,只要云平台支持这个传统应用运行所需的计算机架构和操作系统。只不过这种运行模式,仅仅是把虚拟机当物理机一样使用,不能够真正利用起来云平台的能力,所以传统应用不算是云原生应用。云计算平台的核心能力就是提供按需分配资源的能力,而云原生应用的设计理念就是让部署到云平台的应用能够利用云平台的能力实现按需使用资源实现弹性伸缩。

微服务架构是实现云原生应用的一种架构模式,只要微服务按照一定的设计理念(比如云原生的12要素)去设计,就能实验微服务架构下的系统具有按需使用资源的能力。

镜像治理

基于 Docker Registry,封装一些轻量级的治理功能。VMware 开源的 harbor是目前社区比较成熟的企业级产品,在 Docker Registry 基础上扩展了权限控制,审计,镜像同步,管理界面等治理能力。

资源治理

在容器云环境中,企业需要对应用,人员 ,容器配额和数量等相关信息进行治理。治理的核心是分配好应用、人员和资源之间的权限关系。对于容器管理平台来说,主要关注的资源有计算资源、存储资源和网络资源.

发布平台

面向用户的发布管理控制台,支持发布流程编排。它和其它子系统对接交互,实现基本的应用发布能力。发布平台要对接自动化交付系统和容器调度系统。

这几个模块之间的关系如下图所示

delpoyflow

用户的操作流程如下:

应用通过自动化交付系统集成后生成镜像,开发人员将镜像推到镜像治理中心;

用户在资源治理平台申请发布应用,填写发布配额发布策略等相关信息,然后等待审批通过;

发布申请审批通过,开发人员通过发布平台发布应用;

发布平台通过查询资源治理平台获取发布规格信息;

发布平台向容器云发出启动容器实例指令;

容器云从镜像治理中心拉取镜像并启动容器;

负载均衡和高可用

微服务架构中的服务数量众多,每个服务都希望自己的服务节点可以动态扩展,流量可以负载均衡,服务自身可以高可用。因此我们希望用一种通用的方案来实现,并把它放到部署系统中。

负载均衡

负载均衡通常有两种方式,一种服务端方式,一种客户端方式。

服务端

服务端方式是指服务提供方自己提供一个负载均衡器,客户端统一访问这个负载均衡器,由负载均衡器自己来进行负载分配。

能提供服务端方式的负载均衡的产品有两大类,一类是基于NAT原理,通过篡改数据包来进行请求的路由。相关的产品硬件上有F5,软件上有LVS,这F5和LVS的的实现原理是类似的,通过修改底层数据的源IP和目标IP来实现数据路由,区别是F5的核心是用硬件芯片来完成的,在性能上要强于LVS。对LVS工作原理感兴趣的可以参考LVS的原理-工作模式

另一类是基于反向代理的原理,通过转发数据包的方式来进行请求的路由。比如Nginx和HAProxy,相比LVS工作在内核空间,这两个软件工作在用户空间,因此无权修改底层数据包,只能依靠上层数据的转发来实现,从原理上就可以推测其性能是不及LVS的(这里主要指相同CPU消耗上的处理能力)。Nginx和HAProxy做为反向代理,工作原理即接收请求数据->转发给后面的某一台主机->接收主机的响应->转发给客户端。但是相比LVS,他们支持更多的负载均衡特性,最典型的是支持基于HTTP的URL进行分流,这一点对于很多服务来说是很友好的。

服务端方式的负载均衡与部署系统的集成一个是在服务的上线过程中,部署系统首先把部分容器从LVS或者nginx的下游摘除,避免流量接入,随后新容器启动成功,再把容器添加到下游机器列表中。一个是在扩缩容的操作中,一旦扩缩容发生就更新LVS或者nginx的下游机器列表。

客户端

客户端方式是指服务调用方持有服务的所有机器列表,并根据一定的负载均衡策略选取其中的一台进行访问。

这种方式的实现离不开服务发现机制,服务调用方从服务注册中心获取服务最新的机器列表,然后按一定的策略进行访问,因此这种方式的关键是客户端要知道最新的全量的机器列表。

客户端方式的负载均衡与部署系统的集成一个是在服务的上线过程中,部署系统下线一台容器的时候,服务进程会向注册中心请求下线机器节点,启动一台容器的时候,服务进程又会向注册中心请求上线机器节点。一个是在扩缩容的操作中,一旦扩缩容发生服务进程就会向注册中心发出上线或者下线的请求。这样服务的调用方就可以获取到最新的机器列表了。

高可用

高可用是一个包含多种场景的课题,在这里我首先讲负载均衡下的高可用,然后再讲更广泛的高可用。

负载均衡下的高可用

在这种场景下,我们刚才已经做到了在上线和扩缩容场景下动态更新服务节点列表,因此增添删除节点已经不会影响服务的可用性。我们唯一还要考虑的是服务中途故障的话我们怎么动态移除故障节点。我们的方案是进行探活,一旦检测到故障就把节点移除,一旦节点恢复就把节点添加回来。关于探活可以参见前面服务发现探活机制一节的内容。无论是F5还是LVS还是Nginx和HAProxy,都是需要跟探活机制协作来完成服务的高可用的。只不过有的是自带探活机制,有的需要利用插件,有的得结合第三方的程序或者脚本。

更广泛的高可用

放眼的其他场景下,高可用在形式上主要表现为主备高可用,双主高可用和集群高可用。

主备高可用

主备高可用是指使用冗余的方式提供机器的备份,备机正常不提供服务,只有主机提供服务,当主机故障时,服务自动切换到备机上。当主机恢复时,根据需要,可以让恢复的主机继续提供服务,备机不工作;或者可以让恢复的主机成为原备机的备机,原备机继续提供服务。比如下图中LVS在较小规模下的主备高可用方案:

lvssmallha

LVS主和LVS备通过VRRP协议来实现主备高可用。

1
2
3
来自百度百科:https://baike.baidu.com/item/%E8%99%9A%E6%8B%9F%E8%B7%AF%E7%94%B1%E5%99%A8%E5%86%97%E4%BD%99%E5%8D%8F%E8%AE%AE/2991482

一组VRRP路由器协同工作,共同构成一台虚拟路由器。该虚拟路由器对外表现为一个具有唯一固定的IP地址和MAC地址的逻辑路由器。处于同一个VRRP组中的路由器具有两种互斥的角色:主控路由器和备份路由器,一个VRRP组中有且只有一台处于主控角色的路由器,可以有一个或者多个处于备份角色的路由器VRRP协议从路由器组中选出一台作为主控路由器,负责ARP解析和转发IP数据包,组中的其他路由器作为备份的角色并处于待命状态,当由于某种原因主控路由器发生故障时,其中的一台备份路由器能在瞬间的时延后升级为主控路由器,由于此切换非常迅速而且不用改变IP地址和MAC地址,故对终端使用者系统是透明的。

双主高可用

双主高可用的目的是为了消除主备模式浪费备机的缺点,同一时间总是只能有一台机器处于工作状态。

假设我们能让主备机器同时工作,并且互为主备,任何一台出问题,另一台还是可以工作。

解决方法还是通过VRRP,我们知道VRRP的原理是两台(或者更多)机器组成一个虚拟路由器,争相宣称自己是某个IP的拥有者,成为主控路由器,同一时间只能有一个机器成为主控路由器,成为有效的IP拥有者。那么我们只要让这两个机器维护两个虚拟路由器,让机器1成为路由器1的的主控路由器,机器2成为路由器1的备份路由器;同时让机器2成为路由器2的主控路由器,机器1成为路由器2的备份路由器:

shuangzhu

这样服务的上游无论使用何种方式,只要同时使用ip1和ip2就可以充分的利用两台机器来同时提供服务,任何一台故障后另一台都能够继续承担两个ip的工作。

但是这里可能有个事情会让你不浪费机器的梦想落空,因为你需要让你的机器能够抗下两倍的访问量,不然一台故障后,另一台是扛不住的,这样就相当于你的机器需要有一半的性能是用来容灾的,本质上还是浪费了一半的性能。

集群高可用

集群高可用是指多台机器组成集群共同提供服务,只要在集群容忍的限度内,同时故障若干台机器都不影响集群正常提供服务。

一种集群组成方式是基于我们前面讲到的负载均衡+探活机制,这种方式下通过及时的摘除故障机器来保证可用性。

一种集群组成方式是若干个主备机器的集合,也就是集群内的机器是主1,备1,主2,备2,主3,备3,等等。那是不是配置多个VRRP协议呢,不是的,因为有时候我们希望主备进程能够知道自己当前处于主还是备,并相应的做一些操作,这时候需要通过引入一个程序,检测主机状态,一旦主机出故障,就把备机升级成主机,如果备机有多个备1备2备3,那么还要让其余的备机挂到新的主机下去。比如redis的哨兵程序就是做这个工作的,下图展示了哨兵监视其中一组主备的工作场景(哨兵是支持监视多组主备机器的):

shaobinwatch

shaobinchange

图片来自 redis-Sentinel哨兵原理与实战

不过呢,并不是所有的主备集合组成的集群都需要哨兵程序,服务进程本身也可以完成哨兵的工作。比如redis3开始支持的redis cluster,它组成的redis集群可以不依赖哨兵程序实现高可用。redis的服务节点之间会不停的通过gossip协议互相通信,用来检测其可用性和传递节点信息(节点名字,节点状态,节点角色等)。

1
2
3
来自: https://www.jianshu.com/p/8279d6fd65bb
Gossip 过程是由种子节点发起,当一个种子节点有状态需要更新到网络中的其他节点时,它会随机的选择周围几个节点散播消息,收到消息的节点也会重复该过程,直至最终网络中所有的节点都收到了消息。这个过程可能需要一定的时间,由于不能保证某个时刻所有节点都收到消息,但是理论上最终所有节点都会收到消息,因此它是一个最终一致性协议。

当A节点检测不到B的时候,A会把B的不可用状态通过gossip协议进行传播,当半数以上节点认为B不可用时就认为B真的不可用了,通过消息传播,所有节点都会认为B已经不可用。如果B是备节点那么不会发生什么,如果B是主节点,那么B的备节点(可能有多个)就会参与到选举的过程中,经过半数同意后选举成功,并广播告知别的节点,之后新的主节点接替原来B的工作。下图为redis cluster示意图:

rediscluster

图片来自新浪博客

通过本质分类

上面总结了高可用的多种形式的分类,但是却并不是基于本质的分类。最本质的其实应该在于是否转嫁了高可用的特性。

我们设想假如运行VRRP协议的机器所在网段的路由器故障了,那么依赖于消息组播的VRRP协议也就无法正常工作了,他的高可用还能有效吗? 假如redis集群的哨兵故障了,那么依赖于哨兵的redis集群还能实现高可用吗?假如LVS架构下DirectServer故障了,那么还能正常摘除故障机器实现高可用吗?当然是不行了。

所以这类高可用本质上是转嫁了高可用,或者说是依赖于一个外部的高可用的程序来实现我这个服务的高可用,VRRP依赖高可用的路由器,基于哨兵的redis集群依赖高可用的哨兵,LVS依赖高可用的DirectServer。

那么另一类就是自身实现了高可用机制,不需要外部程序的依赖。这类程序往往在集群内部实现了选举机制,能够有效应对主机故障的场景。比如前面讲到的基于gossip的redis cluster就是一个自身高可用的例子;比如redis的哨兵本身也是一个自身高可用的集群,而不是单一的进程;另一个鼎鼎大名的自身高可用的例子是ZooKeeper,很多其他的系统通过ZooKeeper其实了他们的高可用。

接下来我们看微服务拓扑图中的蓝色部分,这部分属于微服务的优缺点,包括微服务架构的优势和微服务架构的缺点。由于优缺点也起到了对微服务的总结作用,因此我把优缺点放到跟微服务周边相同的目录层级上来讲。

微服务的优点

当回头看微服务的优缺点时,我们发现大部分的优缺点在微服务的特征那一节已经提到了。比如微服务的优点总结来说有3个:扩展性,可靠性,独立性。我们尝试着直接摘微服务特征中的语句来看看。

扩展性:

1
2
来自服务组件化小节:
组件化的服务在扩展时跟单体应用有很大的差别,主要表现在服务的按需扩展这一点。

这一点是在资源分配上的扩展性。由于一个微服务是在网络上独立的进程,因此当某个服务的性能不足或过量的时候,我们完全可以单独地进行扩缩容。

1
2
来自设计发展化小节:
可替换性其实是一个更一般原则的特例,这个原则是通过变化来驱动模块化。你应该保持同一时间的变化只位于同一个模块中。很少变化的部分应该与大量变化的部分处于不同的模块中,而如果两个服务总是一起变化,那么这两个模块应该被合并。而可替换性其实就是一种可以独立变更的特性。

这一点是服务增减上的扩展性。我们可以在业务发展的过程中根据变化来驱动服务的拆分和合并,也就是在服务的增减上是有很大的扩展性的。

可靠性:

1
2
来自失效常态化小节:
使用服务作为组件的一个结果在于应用需要有能容忍服务的故障的设计,任何服务调用都可能因为服务的不可用而失败,调用者就需要尽可能优雅的来处理这种结果。这对比单体应用是一个劣势,因为这引入了额外的复杂性。

失效常态化本身是微服务的一个缺点,但是当我们做到了”尽可能优雅的来处理这种结果“,我们就将劣势转变成了优势,我们的解决手段就是异常隔离。我们客观的认为服务随时可能出问题,我们要做的就是做好容错处理,具体措施在缺点那一节中展开。有了异常隔离,整个系统的可靠性就得到了提升,假如某个服务故障,整个系统因为有了异常隔离,依然可以工作,即便服务能力有所下降。

独立性:

1
2
来自服务组件化小节:
微服务做这种拆分的好处是这些服务可以被独立的部署。如果你使用库来构建你的应用,当你修改其中一个库的时候,你就必须重新部署整个应用。但是如果应用被拆分成多个服务,你只需要部署你修改的那个服务就可以了。当然这并不是绝对的,如果你修改了接口,可能导致相关的服务得跟着一起重新部署。一个好的微服务架构需要最小化这种关联,通过内聚服务的边界,以及通过服务合约来进行服务的进化,减少这类改动。

大意就是指微服务可以独立部署,独立开发,独立测试。

微服务的缺点

调用成本大

1
2
来自服务组件化:
服务组件相比库有个缺点,通过网络进行远程调用的成本更大,因此为了减少交互,服务接口通常是粗粒度的,这相对来说接口更复杂更难用。

由于微服务的独立性,服务之间的调用需要通过网络来完成,这相比单体应用在本地内存的调用要低效的太多。所以我们或者通过改造接口为粗粒度的来减少调用次数,或者把微服务应用于业务对网络耗时不敏感的系统,也就是网络耗时相比业务耗时占比很小的系统。

分布式事务弱

1
2
来自管理去中心化:
应用分拆后,数据也被分拆,这对数据更新带来挑战。单体应用是使用事务来保证一致性,而分布式事务是出了名的难以实现,所以微服务架构强调服务之间的无事务协调,并且明确认识到一致性可能只是最终一致性,而问题也将通过补偿操作来处理。处理一致性问题是一个新的挑战,但是通常业务上允许存在一定程度的不一致来快速响应需求,同时使用某种恢复过程来处理错误。只要处理错误的代价低于强一致性带来的业务损失(主要是性能)那么这就值得。

由于数据拆分带来数据一致性无法保证,除非实现一个严谨的分布式事务的方案。但是分布式事务的实现方案(常见的有二阶段提交、三阶段提交)往往不够轻量,流程过于复杂,因此微服务强调无事务协调,或者说只追求最终一致性。

出错频率高

1
2
来自失效常态化小节:
使用服务作为组件的一个结果在于应用需要有能容忍服务的故障的设计,任何服务调用都可能因为服务的不可用而失败,调用者就需要尽可能优雅的来处理这种结果。这对比单体应用是一个劣势,因为这引入了额外的复杂性。

在讲可靠性这个优点的时候我们就看过这段话,正如话中所说,失效常态化是相较单体应用的一个劣势。那么我们要怎么来应对这个问题呢。我们的解决手段就是异常隔离,这包括了多种手段:超时机制,重试机制,熔断机制,隔离机制,限流机制,降级机制。

超时机制

当某个下游服务过于繁忙的时候,他可能会来不及处理上游发起的某个请求,导致上游迟迟等不到他的返回,这会影响上游服务的吞吐量,这会导致整个请求的链路耗时很大,但是上游或者请求的源头是不可能无限期的等待的,他会有个他能忍受的最长时间,所以我们调用下游服务时候需要设定一个超时时间,一旦等待超过了超时时间,请求就当做下游错误进行返回。

重试机制

在微服务的调用中,一旦涉及到网络传输,网络通道很多是不稳定的,会有偶尔的抖动,抖动时的调用可能就会超时,很多场景就需要重试机制来保证。

重试是分场景的,有的错误就算重试在多次也无效,比如权限问题,密码错误等,不断重试反而造成资源浪费,总耗时更大。重试的前提故障只是暂时的,而不是永久的,我们重试下次可能会成功。

熔断机制

服务熔断则对于目标服务的请求和调用大量超时或失败,这时应该熔断该服务的所有调用,并且对于后续调用应直接返回,从而快速释放资源,确保在目标服务不可用的这段时间内,所有对它的调用都是立即返回,不会阻塞的。随后每隔一段时间释放少量请求到熔断的服务中,如果服务恢复正常则恢复对服务的请求。如果下游服务包含多个IP,那么熔断的对象可以是服务的某个IP。

熔断机制的设计需要包含三个方面,一个是熔断判断机制,比如50%失败率熔断,比如连续失败10次熔断等;一个是熔断恢复机制,比如间隔10s释放一个请求到熔断的服务中,如果还是失败继续保持熔断,如果成功就释放更多的请求进来;一个是熔断报警,触发熔断要及时报警,及时发现和修复故障服务。

隔离机制

隔离机制的目的是让局部的问题不要影响全部,比如接口的隔离,A接口出问题后,不要影响B接口继续提供服务;比如对上游的隔离,如果某个上游疯狂请求该服务,该服务应该做好隔离机制,避免影响其他上游正常请求该服务。可以通过以下方式来实现:

  1. 为每个接口设立单独的线程池,不同线程池之间互不影响。
  2. 为每个接口设立单独的限流值,不同接口独立限流,一个接口流量过大不会导致别的接口资源不足。
  3. 为每个上游设立单独的限流值,每个上游独立限流,一个上游流量过大不会导致处理别的上游的资源不足。

限流机制

服务限流是指当系统资源不够,不足以应对大量请求,即系统资源与访问量出现矛盾的时候,我们为了保证有限的资源能够正常服务,因此对系统按照预设的规则进行流量限制或功能限制的一种方法。

限流的实现方法有计数器,队列,漏桶,令牌桶。

计数器

最简单的实现方式,来一个请求计数器加一,处理完减一,当计数器大于某个阈值,则拒绝处理新的请求。

队列

基于FIFO队列,所有请求都进入队列,后端程序从队列中取出待处理的请求依次处理。队列可以设置最大长度,超过的就拒绝服务。队列还有个特性是可以支持优先级,重要的请求可以优先被处理。

漏桶

把请求比作是水,水来了都先放进桶里,并以限定的速度出水,当水来得过猛而出水不够快时就会导致水直接溢出,即拒绝服务。漏斗算法跟FIFO队列的机制基本是一样的,都是以一定的速度消费积压的请求。

loutong

图片来自接口限流算法:漏桶算法&令牌桶算法

令牌桶

令牌桶算法的原理是系统以恒定的速率产生令牌,然后把令牌放到令牌桶中,令牌桶有一个容量,当令牌桶满了的时候,再向其中放令牌,那么多余的令牌会被丢弃;当想要处理一个请求的时候,需要从令牌桶中取出一个令牌,如果此时令牌桶中没有令牌,那么则拒绝该请求。

lingpaitong

图片来自接口限流算法:漏桶算法&令牌桶算法

令牌桶相比漏斗和队列有个好处是在空闲的时候令牌桶可以积压充足的令牌,这些令牌可以在一定程度上满足短时间的一个流量高峰,不会让这波请求发生排队等候的情况。

滑动窗口
滑动窗口确保任何时间点到他之前一秒的时间段内,都满足限流值。

slidewindowlimit

图片来自三种常见的限流算法(漏桶、令牌桶、滑动窗口)

这种限流方法的好处是任何最新的一秒时间都是满足限流要求的,相比一秒一秒的切分统计周期要好很多。另外相比漏桶和令牌桶他缺了一个缓冲器。

降级机制

降级设计,本质上为了解决资源不足和访问量过大的问题。在有限的资源内对部分系统进行降级,使能够抗住大量的请求。暂时牺牲部分功能,系统能够平稳运行。

降级需要牺牲的一些方面:

一致性: 从强一致性到最终一致性。

停掉次要功能: 停止访问不重要的功能,释放更多的资源。

简化功能: 一些功能简化掉,不返回全量数据,或者返回不那么准确的数据。

如果此时你感受到了异常隔离的复杂性,那么你是时候了解一下ServiceMesh了.

ServiceMesh

ServiceMesh中文名一般叫服务网格,服务网格是一个基础设施,功能在于处理服务间通信,职责是负责实现请求的可靠传递,专门用于解决服务间通信中错误处理的复杂性。通常实现为轻量级的网络代理,与应用程序部署在一起,对应用程序透明。如下示意图:

servivecmeshsidecar

图片来自 Pattern: Service Mesh

在Service Mesh架构中,服务框架的功能都集中实现在SideCar里,并在每一个服务消费者和服务提供者的本地都部署一个SideCar,服务消费者和服务提供者只管自己的业务实现,服务消费者向本地的SideCar发起请求,本地的SideCar根据请求的路径向注册中心查询,得到服务提供者的可用节点列表后,再根据负载均衡策略选择一个服务提供者节点,并向这个节点上的SideCar转发请求,服务提供者节点上的SideCar完成流量统计、限流等功能后,再把请求转发给本地部署的服务提供者进程,从而完成一次服务请求。整个流程你可以参考下面这张图:

servicemeshcallflow

图片来自 微服务架构ServiceMesh

既然SideCar能实现服务之间的调用拦截功能,那么服务之间的所有流量都可以通过SideCar来转发,这样的话所有的SideCar就组成了一个服务网格:

servicemeshnet

图片来自 Pattern: Service Mesh

这时候通过一个统一的地方与各个SideCar交互,就能控制网格中流量的运转了,这个统一的地方就在Sevice Mesh中就被称为Control Plane。

servicemeshcontroldo

图片来自 微服务架构ServiceMesh

Control plane的作用如上图所示,分别是:

服务发现

服务消费者把请求发送给SideCar后,SideCar会查询Control Plane的注册中心来获取服务提供者节点列表。

关于服务的注册,一种形式是服务自己注册到注册中心,前提是这个注册中心是Control plane能够交互的,比如常用的Zookeeper,Eurake。一种形式是SideCar主动把服务注册到注册中心,服务提供者无需关心服务注册的事情,比如微博的Weibo Mesh。这时注册中心就可以是Control plane私有的了,反正注册和发现都是Control plane自己来交互。

负载均衡

通过Control Plane动态修改SideCar中的负载均衡配置。然后SideCar从Control Plane获取到服务提供者节点列表信息后,就按照配置用一定的负载均衡算法从可用的节点列表中选取一个节点发起调用。

请求路由

Control Plane动态改变服务提供者节点列表,然后SideCar从Control Plane中获取服务提供者信息。比如需要进行A/B测试、灰度发布或者流量切换时,就可以动态地改变请求路由。

故障处理

Control Plane动态配置故障处理的方式和参数,然后当服务之间的调用出现故障,SideCar就根据配置加以控制,通常的手段有超时重试、熔断等。

安全认证

在Control Plane配置一个服务可以被谁访问。然后在SideCar中审计调用者和调用方。

监控上报

经过SideCar的调用信息会发给Control Plane,再转发给监控系统。

日志记录

经过SideCar的日志信息会发给Control Plane,再转发给日志系统。

配额控制

在Control Plane配置每个服务的每个调用方的最大调用次数。然后在SideCar中审计调用次数。

参考

架构师

微服务

Microservices

鹰眼下的淘宝

序列化和反序列化

微服务之负载均衡

重试 熔断 限流 降级

虚拟路由器冗余协议

Pattern: Service Mesh

Redis Cluster 原理分析

微服务架构ServiceMesh

主流微服务配置中心对比

微服务架构技术栈选型手册

微服务架构—优雅停机方案

哨兵Redis Sentinel基本原理

RESTful_Web_Service中文版

华为内部资料-VRRP原理讲解

服务降级,服务熔断,服务限流

一分钟了解微服务的好处和陷阱

微服务、SOA 和 API:是敌是友?

微服务架构下,如何实现分布式跟踪?

接口限流算法:漏桶算法&令牌桶算法

Nginx健康检查(health_check)实践

构建双主高可用HAProxy负载均衡系统

RabbitMQ下的生产消费者模式与订阅发布模式

三种常见的限流算法(漏桶、令牌桶、滑动窗口)

面向服务的体系架构(SOA)和业务组件(BC)的思考

Dapper, a Large-Scale Distributed Systems Tracing Infrastructure