在上一篇文章中,我们聊了团队在实施微服务时会遇到的一些新的挑战以及应对它们的基本方法,本章我们将继续深入我们的讨论,去漫游更多微服务带来的drawback,以及它们的解决方案。为什么我总是把重心放到微服务的drawback上?因为显而易见的,微服务架构就是现在和未来(至少很长一段时间)的架构,我们都会去实施它,因此它衍生的drawback将会困扰我们所有人!认识到这些drawback以及知道怎么去应对,这对整个团队都至关重要。
API Gateway
把巨大的单体应用通过不同的视角,拆分成了功能“相对”单一的微服务,不同的团队负责各自不同的服务,这是软件开发中separate concerns
理念的直接体现。但是具体怎么拆分微服务,怎么去separate
concerns,这更多是一门经验积累出的艺术而不是教条化的技术,我们也不会在本文中更深入地探讨这个话题(不过如果你感兴趣,强烈建议你读一读DDD这本书)。
不过假如当我们把业务拆分成了微服务,更进一步地,当我们很好的隔离了关注点
,每个服务都做到了较单一的职责,这时你会不可避免地面临一个新问题,就是如何聚合这些服务。举个例子,比如我们在构建一个在线电子书的电商网站,当用户搜索某本书之后,会点击查看该书的详情页面。一般来说,一个网站的商品详情页面会包含:
- 商品的简介
- 商品的价格
- 折扣等活动信息
- 用户评价
- 猜你喜欢
很显然,以上详情页需要展示的5个部分都是功能相对独立的部分,大概率我们会以5个不同的微服务来应对,比如商品系统、定价系统、活动系统、评价系统、推荐系统。当详情页面需要这些信息的时候,客户端如何获取这些数据呢?如果由客户端直接去挨个调用不同的微服务提供的接口然后再进行数据拼接,显然,这对所有人来说都是灾难!
就好比当我们要写一个程序完成特定任务时,我们可以把大任务分解,划分成一个一个的小函数,但是单独的函数并不能完成任何工作,我们还需要在main函数里调用、组合这些小函数,最终才能达成我们的目的。想一想,如果linux只提供了regxp的头文件,不提供grep命令;如果只提供dirent.h头文件而不提供ls命令;如果只提供stdio.h头文件而不提供cat命令,我们平时工作将多么困难!
微服务就是这些小函数!我们还需要一个单独的服务来组合这些微服务,提供业务级
的具体服务——我们称之为API Gateway。这很像设计模式中的Facade(门面)模式,它屏蔽了内部的细节,提供了统一的视图。API Gateway把客户端和后端分散的微服务进行隔离,客户端告诉API Gateway它需要哪些数据,然后API Gateway再去不同的微服务中加载对应的数据返回给客户端。
通过在客户端和众多微服务之间引入API Gateway,其直接带来了至少以下几个好处:
- 客户端不用关心后端领域模型(也就是微服务是怎么划分的,哪些数据放在哪个服务)
- 不用去处理上一章中讲的微服务中的很多新问题,服务发现啊熔断啊之类的…
- 显著减少了网络请求次数,一次性就取回了所有需要的数据
- 大大降低了客户端程序的复杂性
不过与此同时,引入API Gateway也引入了更多的复杂性,因为它本身也是一个需要部署的服务,而且是直接和客户端打交道的服务,它对稳定性和可用性的要求是非常高的。同时还有另一个不可忽视的问题,实施微服务是把问题大化小的的过程,而API Gateway大多数时候是聚合各种微服务的功能,也就是小化大的过程。这也意味着它的功能可能会很复杂,它的定位也表明它注定和separate concerns无缘,相反,它是aggregate concerns。
举例来说,由于不同设备尺寸大小和网络环境不同,商品详情页可能需要根据实际情况进行不同的呈现。比如Apple Watch,由于屏幕小且网络状况不稳定,如果要在这之上展示商品详情,可能只需要商品的简介和价格;而桌面浏览器由于屏幕大且通常直接插网线网速快,所以需要展示尽可能更多的内容;对于手机APP,也会有不同的展示策略。你可以看到,同样是商品详情,它也会有很多不同的业务需求。
随着时间的推移,由于不同端有不同的运营思路,API Gateway中商品详情的API根据不同种类的客户端逐渐分成了不同的API,甚至可能由不同的团队去负责迭代。不过这种演化也是正常的,因为“商品详情”这个需求本质上就是一个宽泛的需求,只要你给“商品详情”之前加上不同的定语(极速版、完整版…),它就会成为不一样的需求。
针对不同客户端而定制化API,我们有一个专有名词,叫做BFF(Back-end For Front-end)。这就是说,此时的这个后端服务是为前端(客户端)而生的,它的存在是为了满足前端的渲染需求。BFF是API Gateway的一种比较常用的形式。
上面说的还是看起来同一个API会逐渐分裂成多个的case,实际上更多的是下面的这种情况。同样是电商网站,商品详情
和用户修改密码
是完全不想关的业务需求,也就是完全不相关的API。因此很大可能,它们会由不同的团队去负责开发。不同业务的团队,负责开发自己的API,但是由于它们都是直接和客户端打交道,于是都在API Gateway之上进行开发,这会有问题吗?
当然会有问题!如果所有功能都放进同一个应用的代码仓库,API Gateway又变成了巨无霸级的单体应用,逐渐抵消掉我们微服务架构带来的优势。因此,更科学的方式是,我们把API Gateway分成 商品相关API Gateway,账户相关API Gateway,物流相关API Gateway…
这种有着明确业务特征的API Gateway,实际上是对微服务的进一步封装,我们也称之为BFF,或者业务代理。不过由于BFF也是一个微服务,那么问题似乎又回到原点了——客户端还是直接和多个微服务打交道?
其实不然。首先来说,BFF这类微服务已经是一种业务的代理了,它虽然是服务,但已经不“微”了。它和客户端直接去调用最原始的微服务有本质区别。没有BFF以前展示商品详情,客户端需要去调各种微服务获取数据并聚合,而有了BFF之后只需要调用商品详情相关的BFF提供的API即可,客户端本身只负责渲染。它们的差别就像使用grep命令和import regexp包一样。
其次,API Gateway不单单就是BFF的集合,它作为直接和客户端打交道的Gateway,也天然适合做很多额外的工作,比如鉴权、限流、Tracing、统计……这些功能可以在统一的入口来完成,以减轻后置服务的负担。比如说,如果API Gateway不做鉴权,后续的微服务基于防御性编程的理念都需要自己鉴权。而如果这个工作由API Gateway统一做了,其它服务就不需要做了,这样节省了很多不必要的请求和资源。再比如说Tracing,我们前面聊Tracing的时候也说过,把和用户请求相关的所有RPC串联起来,靠的是TraceID。但是TraceID怎么产生呢?由API Gateway在请求被转给各微服务之前生成就是一种很好的方式。
由于API Gateway更多是逻辑上的,很多时候Nginx(1.11+)转发流量时也可以设置给请求附带一个16位随机数$request_id
作为header头,这样来看Nginx也可以说是API Gateway的一种表现形式。
所以,真正的API Gateway应该由两部分组成,一部分是负责流量接入以及提供统一的服务。流量接入对应的英文单词是Routing
,主要是指根据URI把请求转发到对应的BFF。配合服务端服务发现,客户端只需要使用一个IP地址即可接入系统。而统一的服务上面也提到了,大致包括:
- 鉴权
- 限流
- 缓存(optional)
- Tracing
- 统计
这是API Gateway的第一个部分,而第二个部分则是BFF组成的集合。因此,API Gateway大致看起来应该是这样的:
apigateway.jpg
API Gateway是一个对微服务架构来说至关重要的组件,但是由于BFF是业务相关的,现在市面上开源的API Gateway几乎都是上述第一部分,也就是不包含BFF的部分,比如java生态中spring cloud的zuul。Nginx由于天然就适合来做这部分工作(流量接入层),因此Nginx官方也出了Nginx Plus来实现API Gateway。还有很多基于Nginx+Lua的方案,比如大名鼎鼎的kong。tyk是用Go实现的轻量级API Gateway,也是一个不错的选择。不过最终选择用哪个方案需要根据你的团队实际情况,在使用体验、性能、二次开发便捷性、可扩展性和软件生态等多方面做权衡。
但是由于API Gateway实在是太重要,如果在系统中使用了API Gateway,那么一旦它出问题将导致整个系统的崩溃。另一方面,围绕它还有非常多的工作可以做,比如说API管理,以及很多服务大盘的数据收集,比如API的请求数、失败率等等。它很快就会变得非常复杂,因此大多数API Gateway开源软件同时也提供了收费版,使得你可以通过花钱解决问题。不过按照常理,一旦某个开源软件提供enterprise版本,那么就意味着这个领域是一个非常有挑战性的领域——不然它们怎么赚钱呢?
以上便是API Gateway从大体上的一个介绍,不过这里要多说两句。你可能会问:微服务是让我们进行拆分,而API Gateway是让我们聚合,这算是个反范式(bad practice)吗?
首先,我认为这并不是反范式。其次要说,任何范式都是建立在明确的上下文之上的,没有什么范式能够处理所有问题的,不要因为极端地追随范式而不明所以地落入了陷阱。如果真的按照微服务的原教旨主义,那么最佳实践就是Serverless,现在市面上炒得很火的AWS Lambda就是Serverless服务。也就是说,微服务的原教旨认为,最理想的情况就是所有的函数都独立部署!当然它有它的好处,但是我认为也需要区分应用场景。前面你也看到了,即使仅仅是把单体应用的模块单独部署成服务,都引入了那么多问题,更别说把所有的函数都单独部署了。对于绝大多数公司来说,这绝对是灾难!
微服务说到底其实就是计算机领域或者说是自然界中一直就存在的一种思想的实践,这种思想就是:大化小。大化小,然后分而治之,所谓的divide and conquer。我们把一个非常复杂的系统进行拆分,把那些不相关或者可以自闭环的逻辑抽离出来,这样我们更方便去修改和增强,而不至于影响到别的部分。这个其实就是职责划分。
但是你始终应该要明白,我们对系统进行这样的划分的本质目的,是为了更好地实现我们系统对外输出的能力。系统对外提供的能力本身就是一个复杂的业务,就是一个聚合形态,DDD中所谓的聚合根。拆分服务只是我们应对复杂性的一种方法,但是业务本身就是复杂的,聚合的。因此,不论你系统怎么微服务甚至是Serverless,你对用户永远不可能直接提供服务,始终有个地方需要进行聚合。而这个聚合,可能是BFF,但最终一定是API Gateway,不论你在设计系统过程中有没有明确这个术语,但是你一定有一个地方做了API Gateway一部分的工作!
当然,这里并不是我在强调API Gateway是多么重要还是什么,也不是在说微服务原教旨主义有什么问题。这里只是想说明:
客观的业务逻辑是不灭的,if else的数量不会因为架构而减少,只是从一个地方转移到了另一个地方
而我们能做的,就是让if else的职责更明确,方便我们之后好修改…建议大家有空也可以看看领域驱动设计相关的书籍,虽然我司大佬说这是架构骗术的升级
(我也有点赞同),但是里面的一些方法确实有助于你更合理地去拆分领域模型,使得你微服务的职责更合理。
接下来的文章,我们将开始我们的Docker之旅。我们将一起回忆在没有Docker的日子里,进行微服务实践是多么的痛苦,然后看看Docker是怎么样革命性地解救我们。我们将详细地聊聊Docker的一些原理,以及为什么很多做公有云的小伙伴对Docker都不屑一顾。总之,微服务的优雅实践离不开Docker(除非贵司有钱,OP多)
网友评论