七牛云首席布道师
《Go语言程序设计》译者,Go语言/容器虚拟化技术布道师、实践者。
5年以上互联网创业经验和企业级产品研发、运营经验,同时也是互联网产品基础架构解决方案专家。
随着互联网网民数的爆发式增加以及人们对随时随地接入互联网诉求的加强,互联网产品需要面对的并发请求量越来越大,云计算的诞生和普及为海量并发请求的应用提供了弹性的硬件支撑。
本案例分享基于微服务的应用架构设计,内容涉及如何构建一个微服务应用,服务注册与发现,微服务测试和典型的微服务架构设计模式,以及微服务架构在七牛的实践案例。
构建一个微服务应用
服务注册与发现
微服务测试
典型微服务架构设计模式
七牛微服务架构实践
首先我们通过一个最简单的例子来看下如何构建一个微服务应用。
图 1 是一个完整服务的代码,它和普通的应用程序没什么区别,只是功能非常少,业务非常简单。把它编译之后部署在服务端就能跑起来,我们从上往下解释一下这段代码干了什么事情:
在第 6 行我们引入一个包 “github.com/koding/kite”,这是一个开源的微服务框架包,使用它可以快速方便的构建一个微服务应用;
第 11 和 12 行我们定义一个微服务,并将其取名为 first;
第 15 行开始我们为这个微服务添加了一个 square 操作指令供外部调用,这是负责完成具体功能的业务单元,在这里它负责将外部传入过来的值进行求平方计算后返回给调用方;
最后在第 21 行我们把它注册到框架提供的某个地方,并声明了它的访问地址和协议。
图 1. 服务端代码
这个服务实现了一个供外部调用的功能,我们把它称之为 server 端。接下来我们再实现一个调用这个它的客户端,我们称之为 client 端。在真实的微服务环境中,并没有严格区分 server 端和 client 端,只要有需要,它们都可能会相互调用。
同样的,我们从上往下看看图 2 这段代码干了什么事情:
除了第 6 行引入的 “github.com/koding/kite” 包之外,我们还引入了另外一个 “github.com/koding/kite/protocol” 包,它用于做环境中的微服务发现;
第 15 行开始我们通过配置环境中的信息在环境中查找所有名字为 “first” 的微服务。之所以说「查找所有」,是因为在更复杂的生产环境中,一个微服务往往可能包含多个副本,并且这个副本数是动态伸缩的,因此需要专门的服务注册和发现过程来动态注册和动态查找,我们后面会讲到更具体的模式;
在第 22 行中我们选择第 1 个结果,也就是第一个微服务进行访问;
在第 26 行中传入参数 4,调用指令 “square” 求平方,得到结果;
这是一个服务调用另一个服务的演示过程,从中可以看出它涉及到服务的构建、注册和发现。后面这个 client 端例子在演示中虽然没有监听端口,只是访问别的微服务,但是它也能监听端口对外提供服务,本质上 server 和 client 两端从微服务架构角度说并没有严格区分,只是在不同时候扮演不同的角色履行不同的职责。
上面两个示例中我们提到服务注册和发现。实际生产环境中,微服务的种类和每个微服务副本数可能都非常多,特别是在相互调用之后,它们杂糅在一起的业务可能非常复杂。同时,复杂多变的环境决定了系统中运行的微服务随时可能被启停,这也是拥抱微服务的价值点之一:主动拥抱系统的不稳定性,通过消除这种不稳定性带来的影响来达到高可用的目的。为此,需要引入服务注册和发现机制来帮助系统治理这些服务。
我们再来看一下,为什么这么简单的代码中还需要多一个服务注册的步骤?
一个微服务起来之后,要对外提供服务,必须让别人知道它的存在,因此需要有一个地方让它「注册」,我们把它称为「注册中心」。由于微服务的启停是随时都可能发生的,和启停对应的是服务的注册和解除注册,因此在这里我们说到注册的时候默认也隐含解除注册,后续不再赘述。解除注册是为了保证下线的服务不会被调用方访问到。
假如这两个服务都有多个实例副本,它部署起来如图 3 所示。在实际生产环境中,每个服务实例 A、B 和 C 都是动态变化的,整个服务的过程中可能一直有多个副本在那里跑着,但不一定都是相同的实例,它们的 IP 在服务动态伸缩过程中会发生变化。那么如何让客户端及时的知道这种变化呢?我们知道,微服务提倡多个服务解耦比较干净,因此让一个服务主动通知另外一个服务它的变化是不太现实的,这样侵入对方业务太深了,耦合太紧。同时,在后端有多个服务实例的情况下,如何将客户端的请求负载均衡的分发到各个实例中呢?
图 3. 没有服务发现的问题
在这些问题面前,和传统的单体架构相比,微服务中服务的注册和发现能力非常重要,甚至有些微服务框架自带了服务的注册和发现功能(比如我们给的例子中用到的 kite 这个框架就自带了这样的功能)。
一个服务起来之后,它的注册和解除注册过程可以由自己去完成,也可以由第三方工具去完成。比如在客户端和微服务端之间的负载均衡器可能会帮助微服务完成服务的注册和解除注册等操作,客户端和微服务自身都不需要关心,也即不需要他们各自的业务逻辑里面实现这些操作。还有一些情况是在客户端自带服务查询模块,它先从服务注册中心查询可用的服务,然后再按照这个查询结果去请求后端服务实例。下面我们分别解释这两种模式。
图 4. 客户端服务发现
图 4 中给出的是以客户端查询为主的服务注册和发现机制,后端服务实例起来之后,会以主动或者被动的形式注册到「注册中心」。客户端自带的「查询模块」会从「注册中心」查询可用的服务,然后按照查询结果去请求后端服务实例。
这样做的好处是:
「注册中心」在服务之外维护,使用简单,对已有的微服务架构侵入小;
客户端直接请求后端实例,查询完成后请求链路不需要经过其它中间环节。
其缺点在于:
客户端和「注册中心」绑定;
客户端的实现取决于具体语言或者框架,每个客户端都得自己去实现。
图 5. 服务端服务发现
我们再来看下服务端实现的服务注册和发现机制,如图 5 所示。同样的,后端服务实例起来之后,会通过主动或者被动的方式注册到「注册中心」,但是客户端不需要实现查询模块去查询服务了,取而代之的是我们在服务端添加一个「路由」环节,它负责代理客户端的所有请求,从后端实例获取结果之后返回给客户端。与客户端服务发现机制相比,它具有以下优点:
客户端不需要做额外的变更;
有些云服务公司已经提供类似产品可以满足需求了,可以直接接入。
但它也有它的缺点:
除非是托管在云服务提供商那里,否则还需要额外的服务端来部署「路由」或者「负载均衡器」部分,这也就意味着需要保证这个部分的可靠性和可用性,需要额外的系统设计和运维工作;
请求多了一个路由代理的步骤,增加系统总体耗时。
图 6. 测试金字塔
我们今天介绍微服务,首先假设系统到了一定的复杂度,使用传统的单体架构已经不能满足需求了,必须将单体架构上的功能拆分成职责和功能单一的微服务才能更好的跟上业务的发展。因此,由微服务构建的系统往往是一个复杂的分布式系统,它的测试涉及到从里到外的各个环节。
接下来我们分三部分来介绍分布式系统中涉及到的测试:常规测试、混沌测试和流量重现。
我们先来看一个测试金字塔,如图 6 所示,它包含:单元测试、集成测试、组件测试、端到端测试和探索性测试。接下来我们要介绍的「常规测试」包含金字塔地下的四部分,它还包含另外一种不太常见类型的测试,也即契约测试,下面会介绍到。而混沌测试或者猴子测试则属于探索性测试。
1. 单元测试
单元测试是几乎所有系统开发都会涉及到的环节,它覆盖最细粒度的测试范围,一般基于类甚至是方法来进行自动化测试。它测试的范畴一般不涉及跨网络的调用,因此相对简单。在传统的 Web 开发中,非常流行的测试驱动开发 TDD 里面讲的测试一般基于单元测试,单元测试用例不仅定义了业务边界,还将业务模块进行了细分。
2. 集成测试
集成测试将相关模块组合在一起,在业务上构成一个子系统进行测试,它的主要目的在于验证构成一个子系统的所有路劲上的调用是否正确,模块组合在一起后调用产生的结果是否符合预期。比如两个服务之间的调用,或者服务和数据库之间的通信,一般都属于集成测试的范畴。
3. 组件测试
与集成测试相反,组件测试关注的是组件内部功能的完备性。对于微服务应用来说,一个微服务即一个组件,为了测试这个组件内部功能的完备性,需要尽量减少外部环境对其造成的影响。当该微服务依赖于一些外部服务或者数据库调用的时候,我们可以 mock 一些外部服务,同时使用内存数据来代替数据库数据,这样可以尽量减少外部服务调用对它产生的影响,只关注单个微服务自身的测试。
4. 契约测试
契约测试和组件测试的所需的边界非常像,对于微服务来说,它们的所涉及的边界都是微服务本身。但和组件测试不同的是,契约测试更关注输入参数和输出结果的合法性,一定的合法输入必须得到一定的合法输出,也即测试一个服务是否满足一定的契约;另外,契约测试还关注相应的结果是在多长时间内得到的,也即服务响应的性能。这时候对一个服务的测试,其所依赖的服务不能通过 mock 或者内存数据来代替。
5. 端到端测试
为了验证一个系统是否符合客户需求和商业目标,需要一个覆盖产品完整链路的测试。对于有用户 UI 界面的产品,端到端的测试意味着包括 UI 界面的测试和后端服务的测试。由于涉及到的环节最多,因此这样的测试运行起来也比较慢,出现故障的时候排查问题也比较麻烦。为了加快这个测试环节,通常建议:
将尽可能多的测试需求自动化。
不必每个组件的修改都触发端到端测试,可以在所有更小粒度的测试完成之后再做一遍端到端测试。
之所以把上面所有的测试称为「常规测试」,是因为这些测试基本上在所有类型的现代化软件开发中都可能涉及到,它其实不是微服务架构都有的,微服务架构下的这些测试只是边界不太一样。统计对比表明,上线之前完整的测试可以避免 90% 以上的错误导致的故障。
关于常规的测试,我们只在这里做简单的介绍,想详细了解每种测试覆盖的范畴或者最佳实践的同学可以参考这个 Martin Fowler 网站上 Toby Clemson 关于微服务测试的分享:http://martinfowler.com/articles/microservice-testing/
在分布式系统领域,Netflix 发明了一种更为古怪的测试,叫混沌测试,Netflix 称之为猴子测试。什么意思呢?它假设,如果你的系统足够健壮,那么随便启停某些服务并不会影响系统的整体运行,用户并不会感知到服务的故障。我们经常讲要构建容忍故障的高可用服务,但是如果故障没有来,就没法验证这样的服务是否可以容忍某些极端情况的故障。为此,Netflix 在系统中引入了一系列搞破坏的「猴子」,它会主动给系统的各个部分制造麻烦,比如随时不小心关闭一台机器,但是你的服务还得继续运行,所有故障必须自动恢复,并且不能被用户感知到。
图 7. Netflix猴子军团
Netflix 的猴子军团:
Chaos Monkey: 随机杀死实例
Latency Monkey: 人为引入延迟
Chaos Gorilla: 模拟整个可用区突然断电
猴子军团中的几种不同类型的机器人分别代表了不同的破坏性,它在稳定的线上环境中随机选择破坏。这样的测试模拟了自然灾难,对线上环境进行了全黑盒式的演练,让线上系统产生「免疫」。遗憾的是,即便所幸躲过所有这些「自然灾难」,也无法说明系统是没问题的。
更多关于猴子军团的信息可以参考以下链接:
Chaos Monkey Released Into The Wild: http://techblog.netflix.com/2012/07/chaos-monkey-released-into-wild.html
What is Simian Army? https://github.com/Netflix/SimianArmy/wiki
Automated Failure Testing: http://techblog.netflix.com/2016/01/automated-failure-testing.html
我们前面提到端到端的测试很难做,主要是难在它整个链路太长,耗时太长,环节也难以控制。另外一个难点在于,我们通常很难获得和线上一样真实的流量去进行端到端的测试,即便某个环节能够成功模拟相应的请求,但也不是所有请求的比例和线上都是一致的。
为了模拟线上请求,我们可以讲线上流量截获后导入到测试环境中通过「流量重现」的方式,在一个更加真实的模拟环境中观察和调整。在这里介绍一个用 Go 写的开源工具:Gor https://github.com/buger/gor
图 8. 流量重现工具 Gor
通过监听线上服务的请求,它能够截获线上环境的流量,并将其在测试和开发环境中重现,如上图所示。
图 9. 典型微服务设计模式
图 9 是一个典型的微服务架构图。服务注册和发现的架构图前面讲过,为了简化图的结构,此图不再提及。客户端的请求首先会到达一个负责对外沟通的 API Gateway,它负责识别客户端类型,解析和理解客户端的请求,并将请求分发到后端对应的微服务中完成。
一般来讲,基于 HTTP REST API 的请求都是同步执行的,但是有些场景使用异步的方式更为合理,比如大视频转码服务,它很难在很短时间内完成。同时,为了最大化微服务之间的解耦,应尽量减少和简化微服务之间的通信。为此,我们引入消息队列作为异步通信的通道。
在微服务的后端,如果每个微服务都按照三层结构来部署,除了服务本身之外,还包括缓存层和持久化的数据库层。
这样就构成了一个典型的微服务应用架构,各项服务可以自由伸缩,相互之间依赖最小化,通过共享队列的方式来进行数据共享和信息同步。当然,这样一个架构要通过猴子军团的测试,首先得保证每个组件都是高可用甚至是跨机房部署的,比如 API Gateway、消息队列以及每个服务依赖的 Cache 和 DB 都随时可能挂掉。
图 10. 七牛微服务设计实践
图 10 展示的是七牛数据处理平台的微服务架构示意图。七牛的数据处理平台每天处理接近百亿的数据处理请求,这些请求包括图片缩放裁剪等可以实时完成的同步请求,也包括音视频转码等无法实时完成的异步请求。所有这些请求通过统一的网关进入负载均衡器,再由负载均衡器分发给后端的处理实例。后端的处理实例中,每个实例只部署一种类型的服务,同时维持一个 Agent 可以从存储中读取数据,以及一个 Cache 用于维持状态。由于业务相对简单,Cache 可以直接基于内存,并且没有持久化的要求。所有持久化的工作都由客户端主动发起,由后端 Agent 完成后对存储进行读写。
Q1:关于微服务的注册和发现,目前有哪些成熟的产品?
A1:比较成熟的用于协同的是 Zookeeper,在开源容器产品里面用的比较多的是 etcd。
Q2:微服务健康状况,运行情况监控是如何做的?
A2:如果是使用容器化的部署方式,监控方面目前很多都是基于 Google 的 cAdvisor 来做的。
Q3:微服务测试,打桩是怎么做的?
A3:打桩测试不太了解,有朋友介绍使用这个工具来做http://www.mbtest.org/
Q4:请问微服务是否部署了多个实例,同服务的不同实例间是否有分布式锁来保护?
A4:如果需要保证服务之间的原子性,可以使用分布式锁,但应该和微服务的初衷有点冲突。建议尽量改成异步通信的方式,借助 message queue 等来通信。
Q5:微服务的部署,老师比较推荐哪种方式?蓝绿、金丝雀,或者是其他?
A5:我们的业务是灰度。
Q6:针对服务总线、API GateWay、OpenAPI之间的区别是什么呢?您刚才所讲的服务发现属于这里面谁的范畴?
A6:服务总线是针对数据传输讲的。API Gateway 的针对请求处理和分发的。OpenAPI 功能上应该差不多,主要是面向第三方平台的开发者。API Gateway 除了终端(Web/iOS/Android)提供 API 功能之外,还提供了统一的入口、微服务接口聚合以及授权认证、后端服务负载均衡等功能。
Q7:怎么解决动态拓展Docker,网络问题?
A7:要看具体是啥问题…… 网络方面 Docker 官方有提供方案,k8s 也有,一般私有部署可以直接使用开源的版本。
Q8:如果应用已经基于Spark这样的分布式框架构建了,再想做微服务拆分有什么比较好的方案吗?
A8:我理解你跑在 Spark 上面的计算任务更依赖的是 Spark,而不是微服务的优势。因此如果你要考虑微服务话或者为了充分利用容器云平台的能力,可以考虑先把你依赖的 Spark 容器化,后面再考虑计算任务容器化的事情。
Q9:老师,一个客户端应用调用多个微风服务,数据一致性怎么解决?
A9:我理解多个微服务之间的数据一致性是指他们共用了相同的数据或者数据库,有多个微服务对他们进行读写。这个问题可以使用事件驱动的编程模型来解决,简单来讲就是维护一个事件服务端,让各个服务对数据的修改都以事件的形式通知它,再由它去通知其它所有相关的服务。参考 Event Sourcinghttp://martinfowler.com/eaaDev/EventSourcing.html
Q10:以什么标准来进行微服务颗粒度划分?
A10:大家都说以业务的最小单元来拆分服务,但对业务的最小单元却没有统一的标准,也不可能有统一的标准,比如我们做存储业务的和你电商业务的标准就没法统一起来。我个人的看法是,就像康威定律里面说的,服务的复杂性决定了服务的大小和拆分的合理性,除了「以业务的最小单元来拆分」之外,维护某个微服务的团队不应该太大。还有,拆分之后本来应该是尽量解耦的,所以可以看拆分之后是否带来更复杂的异步通信。
本文来自云栖社区合作伙伴"DBAplus",原文发布时间:2016-08-05