微服务设计
1. API Gateway
以毛剑在实际架构中的案例作为分析的材料
毛剑在架构初期其实是采用直连的方式,连接的客户端可能是浏览器也有可能是手机,直接裸着连服务,然后就遇到了一系列的问题。
我们进行了 SOA 服务化的架构演进,按照垂直功能进行了拆分,对外暴露了一批微服务,但是因为缺乏统一的出口面临了不少困难:
·客户端到微服务直接通信,强耦合。
·需要多次请求,客户端聚合数据,工作量巨大,延迟高。
·协议不利于统一,各个部门间有差异,需要端来兼容。
·面向“端”的API适配,耦合到了内部服务。
·多终端兼容逻辑复杂,每个服务都需要处理。
·统一逻辑无法收敛,比如安全认证、限流。
下图就是最初的V1.0的架构
我们之前提到了我们工作模型,要内聚模式配合。继这上面的v1.0版本之后,我们自然的而然的就想到在客户端与后端服务之间加一个app-interface的组件。
我们新增了一个 app-interface 用于统一的协议出口,这就是下面的v2.0版本。在服务内进行大量的 dataset join,按照业务场景来设计粗粒度的 API,给后续服务的演进带来的很多优势:
·轻量交互:协议精简、聚合。
·差异服务:数据裁剪以及聚合、针对终端定制化API。
·动态升级:原有系统兼容升级,更新服务而非协议。
·沟通效率提升,协作模式演进为移动业务 网关小组。
BFF 可以认为是一种适配服务,将后端的微服务进行适配(主要包括聚合裁剪和格式适配等逻辑),向无线端设备暴露友好和统一的 API,方便无线设备接入访问后端服务。
v2.0的BFF架构的版本最致命的一个问题是整个 app-interface 属于 single point of failure,严重代码缺陷或者流量洪峰可能引发集群宕机。
于是继续改进,在V2.0的版本上,将BFF根据业务模块拆成多个,每个BFF网关都有自己的任务,于是v3.0的架构就横空出世,就是下图。
v 3.0的版本在后续使用中比较突出的问题也是逐渐浮现出来:
·单个模块也会导致后续业务集成复杂度高,根据康威法则,单块的无线BFF和多团队之间就出现不匹配问题,团队之间沟通协调成本高,交付效率低下。
·很多跨横切面逻辑,比如安全认证,日志监控,限流熔断等。随着时间的推移,代码变得越来越复杂,技术债越堆越多。
跨横切面(Cross-Cutting Concerns)的功能,需要协调更新框架升级发版(路由、认证、限流、安全),因此全部上沉,引入了 API Gateway,把业务集成度高的 BFF 层和通用功能服务层 API Gateway 进行了分层处理。
在新的架构中,网关承担了重要的角色,它是解耦拆分和后续升级迁移的利器。在网关的配合下,单块 BFF 实现了解耦拆分,各业务线团队可以独立开发和交付各自的微服务,研发效率大大提升。另外,把跨横切面逻辑从 BFF 剥离到网关上去以后,BFF 的开发人员可以更加专注业务逻辑交付,实现了架构上的关注分离(Separation of Concerns)。
我们业务流量实际为:
移动端 -> API Gateway -> BFF -> Mircoservice,**在 FE Web业务中,BFF 可以是 nodejs 来做服务端渲染(SSR,Server-Side Rendering),注意这里忽略了上游的 CDN、4/7层负载均衡(ELB**)。
API 网关的产品有很多,比如Java的zuul或者是大名鼎鼎的Kong又或者是Api six,OR.....
2. Mircoservice 划分
微服务架构时遇到的第一个问题就是如何划分服务的边界。在实际项目中通常会采用两种不同的方式划分服务边界,即通过业务职能(Business Capability)或是 DDD (领域驱动设计)的限界上下文(Bounded Context)。
·Business Capability
由公司内部不同部门提供的职能。例如客户服务部门提供客户服务的职能,财务部门提供财务相关的职能。
·Bounded Context
限界上下文是 DDD 中用来划分不同业务边界的元素,这里业务边界的含义是“解决不同业务问题”的问题域和对应的解决方案域,为了解决某种类型的业务问题,贴近领域知识,也就是业务。
这本质上也促进了组织结构的演进:Service per team
举个例子,按照毛剑的说法就是B站做微服务架构演进的初期,按照业务拆解服务,比如说现在就有两个业务,一个叫稿件服务,一个叫视频服务,这是初期的一个架构设计,之后在架构调整中,觉得稿件和视频都是和创作的业务相关的,所以将原来的稿件和视频都划分在创作的业务域下。
CQRS
微服务的划分还有一个非常重要的模式就是CQRS(Command Query Responsibility Segregation)故名思义是将 command 与 query 分离的一种模式。query 很好理解,就是我们之前提到的「查询」,那么 command 即命令又是什么呢?
CQRS 将系统中的操作分为两类,即「命令」(Command) 与「查询」(Query)。命令则是对会引起数据发生变化操作的总称,即我们常说的新增,更新,删除这些操作,都是命令。而查询则和字面意思一样,即不会对数据产生变化的操作,只是按照某些条件查找数据。
CQRS 的核心思想是将这两类不同的操作进行分离,然后在两个独立的「服务」中实现。这里的「服务」一般是指两个独立部署的应用。在某些特殊情况下,也可以部署在同一个应用内的不同接口上。
Command 与 Query 对应的数据源也应该是互相独立的,即更新操作在一个数据源,而查询操作在另一个数据源上。
看到这里,你可能想到一个问题,既然数据源进行了分离,如何做到数据之间的同步呢?
来看下面的例子
基于上面说的创作服务,由于稿件的服务需要做审核,稿件库有很多的业务域,上流的服务查询稿件服务查询状态,很不方便,稿件服务有更改稿件最后审核状态的接口,这些接口是不叫敏感的业务api接口,不敢给别人轻易的调用。于是毛剑就想着就做读写分析,需要另外一个DB去流式的更新稿件状态,然后将查询稿件结果的变成一个服务,以rpc的方式暴露出去。这就和CQRS的核心思想不谋而合。如图所示的架构:
稿件服务可以直接修改db中的结果,然后通过canal消息中间件订阅mysql的binlog,将订阅的binlog交给Kafka,有一个稿件服务的job订阅这些binlog然后将另一个存放稿件结果表的db进行update.来保证最后两个db的一致性。
在稿件服务演进过程中,我们发现围绕着创作稿件、审核稿件、最终发布稿件有大量的逻辑揉在一块,其中稿件本身的状态也有非常多种,但是最终前台用户只关注稿件能否查看,我们依赖稿件数据库 binlog 以及订阅 binlog 的中间件 canal,将我们的稿件结果发布到消息队列 kafka 中,最终消费数据独立组建一个稿件查阅结果数据库,并对外提供一个独立查询服务,来拆分复杂架构和业务。我们架构也从 Polling publisher -> Transaction log tailing 进行了演进(Pull vs Push)。--毛剑
3. Mircoservice 安全
对于外网的请求来说,我们通常在 API Gateway 进行统一的认证拦截,一旦认证成功,我们会使用 JWT 方式通过 RPC 元数据传递的方式带到 BFF 层,BFF 校验 Token 完整性后把身份信息注入到应用的 Context 中,BFF 到其他下层的微服务,建议是直接在 RPC Request 中带入用户身份信息(UserID)请求服务。
·API Gateway -> BFF -> Service
Biz Auth -> JWT -> Request Args
对于服务内部,一般要区分身份认证和授权。
比如说内部的grpc服务,依赖grpc证书可以知道调用者是谁,知道了调用者之后再去控制中心去查询权限,也就是根据RBAC去查询权限做认证。这样内部服务的身份认证和授权就完成了。授权根据信任度分成三个等级,全信任,半信任以及0信任,也就是所谓的裸奔。
·Full Trust
安全等级最高,服务于服务之间的通讯走tls,交互密钥,加密通讯。防止抓包嗅探和监听。
·Half Trust
服务于服务之间都知道对方是谁,但是通讯不加密。
·Zero Trust
这个就是裸奔了,谁都可以来向我发起请求。
要选择哪一种level完全取决于架构师自己咯,根据实际场景按需选择。