微服务是个说的挺长时间的概念,也是比较成熟的技术体系。像 Spring Cloud,甚至提供了微服务所需要的全套框架,包括注册中心 (Eureka)、配置中心 (Config)、断路器 (Hytrix)、API 网关 (Zuul) 等组件。微服务体系庞杂,每个组件都能独自成章。本文作者仅从个人的经验和实践出发,谈了谈自己对微服务及其部分内涵的思考和理解。 作者张轲目前任职于杭州大树网络技术有限公司,担任首席架构师,负责系统整体业务架构以及基础架构,熟悉微服务、分布式设计、中间件领域,对运维、测试、敏捷开发等相关领域也有所涉猎。(同时也欢迎关注我的微信,ID:gh_bd9717312199)。 微服务与更早就起来的 SOA 是什么关系? 个人觉得如果从概念上来说,微服务和 SOA 都是一回事,强调把整个系统,按照多个服务的方式去组合及通信,而不是揉合在一起,但它们的内涵有很大的区别。 SOA 诞生在早期企业级的应用,其业务复杂、技术体系多样,SOA 强调的是各个服务之间,尤其是异构系统、遗留系统之间,建立起一套统一的协议和通信 (SOAP),以及寻址服务 (UDDI),它的侧重点在集成和兼容;与 SOA 同期的另一种概念 ESB(企业总线),强调通过一根总线服务,把所有服务串联起来,由 ESB 总线来屏蔽各种不同业务系统自身业务 / 语言 / 协议的特殊性,各服务以一种统一的方式,与总线相连,从而降低接入成本。 这两种概念,我感觉在国内没有太发展起来。一是国内的软件起步相对较晚,系统的整体复杂度——多厂商、多语言 / 技术栈、历史遗留系统的问题,还不算突出。而对于公司内部的产品系,又没有必要使用 SOA、UDDI 来做复杂的集成。随着互联网的兴起和用户量的迅速爆发,企业自身的产品的微服务化的需求,快速发展起来,而与此同时 SOA 这种以 XML 为基础的 SOAP 协议、以寻址为主要作用的 UDDI,不能使用互联网产品的发展——SOAP 的 XML 协议内容太多,造成性能明显下降;HTTP 协议的效率不如 RPC;UDDI 只有寻址,缺少服务治理等功能。 在此种大背景下,以服务切分 + 服务注册 + 服务治理 + 限流降级 +RPC+ 监控等为主要内涵的微服务,就快速发展起来的。国内的阿里巴巴走在前列,以 Dubbo 为代表在国内互联网企业中得到广泛应用;后来 Spring 官方发布 Spring Cloud,揉合了一系列自研或其他企业捐赠的开源项目,发布微服务领域的 Spring Cloud 产品。各自都有各自的优势和劣势,而随着这些年来,微服务的继续下沉 (sidecar 和 service mesh) 到基础设施层,给微服务的治理带来了新的方向。 服务的粒度,切分到多大算合适? 太粗的话,这服务就涵盖过多的业务逻辑,从而难维护、易出错;太细了,就会搞出很多的工程,造成很大的工程维护和通信成本。 主流说法是依据康威定律——团队的交流机制应该和组织机构相匹配。应用到软件领域来看,如果某个应用,需要多个组织之间一起交流和修改,那么它的交流机制就大于组织机构了,出现了不匹配的情况,那么这个应用很可能就太粗而需要拆分。 这里有个不太好懂的地方——既然系统架构和团队组织机构想匹配,那我们是先定系统架构呢,还是先定团队组织机构呢? 这有点类似先有鸡还是先有蛋。我觉得可以这么来理解:无论是团队怎么定、还是架构怎么定,这都是跟着业务的发展而发展的,可以说都是业务的衍生发展而来。所以系统架构设计,首要做的还是业务理解和切分——业务切分决定了服务切分、业务切分也决定了团队组织。 业务切分有两种简单办法:
eg:在我们的线上贷款业务中,典型的 user case 是这样:
将其中的名词整理出来,整理流程大概就是如下图: 这些都是候选服务。根据其复杂度和相关性,做适当的拆解和合并,形成了如下几个子系统及服务。 从服务的角度来看,对外公开的是契约——即我们系统提供哪些特性,而内部算法 / 数据都应该隐藏起来,而在不同服务间“是共享数据库还是独享数据库”上,实践中的冲突和困惑,体现地比较明显。 我们假想个流程,ServiceA 的李雷需要更新 User 表的某个字段,如果大家数据库表都共享的,李雷只要写个 SQL 就解决了。但一旦把 User 表服务化后,归到 UserCenter 这个服务自治之后,问题就麻烦了:
从这可以看到,一旦一个人、一个系统做的事,变成了 2 个人、两个系统来做,那要多出多少麻烦了。所以我完全理解,在公司早期,所以业务系统共享一套数据库表,是多么地务实。我们功夫贷在创业之初也是这么做的,在创业 2 年后,它的弊端开始密集体现,而服务化改造过程中,我们也是付出了相当大的代价。 随着用户量和数据量的上升,这种共享数据库表的最明显的弊端就是慢查询越来越多——因为谁都可以操作任何一张表、而开发过程中或者是对业务理解不够、或者是 SQL 能力不足,很容易写出慢 SQL 来,其结果就是导致 DB 的 CPU 飙升到 100%、或者是 IOPS 被打满,从而全 APP 被拖慢甚至无法提供服务。这种危害是相当巨大的。 所以,从运行时的慢 SQL 带来的巨大杀伤力来说,数据库应该是隐藏在服务内部,该服务由熟悉该业务的固定团队维护、也会做很多优化。虽然开发阶段慢了,但是运行时稳定了、系统的可用性得到了保障。只是这件事,不应该在创业初期就做,那样会比较严重地放缓系统迭代速度、更应该在系统规模相对较大的时候来改造。 当然,我们说改造是要付出代价的。不仅之前的一个库中的表,要分成不同的库,各服务的程序要做不小的改造,其中最困难的是,同一张表的字段,可能会属于各个不同的应用。看下面这个 User 表。 开始的时候,User 表只包含了完全业务无关的属性,但随着系统的发展,一些和业务相关的字段 (上图红色部分) 逐渐地被加进来——这也不完全是决策时犯的错误,而是本身这属性是否和业务有关,也不是很容易界定。所以逐渐会发现,很多系统都会依赖这张表,从而交织难以拆分。各个服务可能都需要有这张表,而各自维护自己所关心的那部分字段及功能。 在我们的实践中,服务化的过程以及数据迁移,大约是这样的步骤 (以“用户中心”应用为例):
在微服务之后,各个系统只对某一块业务负责,那么就有可能需要对服务做一些聚合。下面是常见的两种模式: 这是聚合服务的模式,由 web 应用去负责聚合后端服务或做个性化处理,这是它的好处——可以根据自身的业务做任何组合和处理,而它的坏处也很明显——对于不需要特殊处理的,也得过它一道。 这是后台服务自包含的模式。某个后台应用,依赖于其他服务,于是就将其他服务的相关调用都处理完了,或者这么理解——后台服务也有多个层次:库存服务、支付服务、发票服务是最底层的,交易服务是更上层一些的共享服务,从而达到封装细粒度服务的目的,与此同时,它的个性化也就丧失了。假如有个交易,是不需要发票服务的,那么这种模式就不是太灵活。 从我个人的经验来看,我是倾向于聚合服务这种模式。每个前端应用,还都是应该有个自己的后台服务,去完成很多小的功能 (比如更新 APP 版本、展示首页广告、记录埋点等 APP 特有 feature)、以及聚合。而对于不需要 App-Server 处理、直接使用后台服务的,应该能够通过 gateway 直接调用,而不需要 App-Server 来做代理转发。 容错的目的就是在出现问题的时候,仍然能够正常提供服务,其具体表现形式有这么几种:
上面这些特性,有些是通过 RPC 框架来实现 (重试)、有些是应用控制 (调用替代服务、异步 + 定时补偿)、有些可以通过 Hytrix 这样的断路保护框架来实现。容错也比较简单,但为了容错确实也需要增加不少开发工作量,它就像买保险,有的人看重风险、愿意付出一些代价来买一份适合的保险;有的人比较乐观,不相信灾难会降临到自己身上,所以这就看一个公司对自己的要求了。从我个人的观点来看,公司到达千万用户级以上,就需要比较严肃地考虑这个了,因为一次全局事故,带来的损失就会是不小。 限流主要有两种算法:令牌桶算法和漏桶算法。 对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。如下图所示,令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。 从互联网实践的角度看,我觉得这两种方法都不是很理想。主要原因是看我们怎么理解流量控制这个事情。在互联网领域,系统的最大处理能力,是一个比较核心的指标。假设系统 (或某接口服务) 只能同时支撑 10000 个请求同时处理 (这也是非常容易模拟测试的),那么它所关心的,就是在任何一个时间点的执行中任务,是否超过了 10000,而这是令牌算法和漏桶算法都提供不了的。
所以这两种算法,我认为它都不是从精确的系统承载量角度出发,更像是一些预估或外界因素所引发的流控——比如该系统 1 分钟只允许处理 100 个请求,以一种比较粗略的方式、来保护系统不过载;该系统依赖的第三方不能超过每天 XX 次的请求量; 从请求出发的角度,还有令牌算法的优化版——滑窗模式。以 X 个滑窗作为一个周期,比如 1 秒作为 1 个窗口,3 个窗口作为一个周期,在这个周期内令牌蓄水池。3 秒到了则在排队等待令牌的请求都置拒绝。这样防止在流量阻塞的时候,随着时间推移,很多用户已经等不及离开了,而他们的请求还在这里排队,导致最新用户的请求无法获得令牌。 而如果从系统承载力的角度,既能最大发挥系统能力,又不会过载,个人认为最好的方法“响应模式——在访问开始前,计数器 +1;访问结束,计数器 -1;保证计数器不超过阀值,也就是当前系统正在处理的任务,不超过阀值”。 从另一个角度看,令牌算法和漏桶算法被很多框架完好支持,比如 Nginx,这样对于大部分接口,尤其是处于安全角度考虑的限流,就是个很好的策略。在 Nginx 中配下就可以了,不需要业务去衡量自身负载、再开发相应代码。所以这也是种取舍——如果要快速覆盖、尤其在产品初期,尽量地保护自己的系统,尤其是安全原因,那么令牌或漏桶算法是很好的选择;如果针对一些核心接口,希望能在保护自己系统的同时、尽量多地发挥系统潜力,那么就开发“响应模式”是更好的选择。 CAP(Consistence、Available、Partition) 理论是很熟悉的分布式理论——在同一时间 CAP 不能全部满足、只能满足其中两个。而由于分布式系统的特点,Partition 是必须要满足的,所以只能要么 CP、要么 AP,即要么系统可用,但数据可能不一致;要么数据一致,但系统不可用。 这里对“为什么 Partition 必须要满足”解释一下。CAP 主要是针对有状态、即有数据的,典型形态就是存储类产品。因为数据才有一致性、分区同步这样的场景。在存储类产品中,为了避免单点故障,都是需要主从结构、或者集群结构,这就势必有相互之间的数据复制——主从的话,是主向从复制;集群的话,是多副本复制。这就必然涉及到网络通信,而网络我们说不是非常稳定的,“满足 Partition”就是在网络不稳定的时候,比如主和从网络短时不通了,这时候产品还能够正常提供服务。这就是“Partition 必须要满足”的原因,否则就有较大的单点风险。 既然 P 必须要满足,则只能选 AP 或者 CP 了。就互联网企业来说,保证服务可用性更为重要,所以 AP 往往是主流选择。在我的经验中,金融财务相关领域可能会用到 AP 这样的强一致,这往往是通过有 ACID 特性的 RDMS(Oracle、MySQL) 来实现的。 前面我们提到,分布式的服务治理,数据被隐藏到服务内部了,那么对数据的修改就由原先的直接操作变成了接口调用,原先可能可以通过 Transaction 来实现多表更新的 ACID,现在实现不了了,那在保证 AP 的同时,Consistence 怎么办呢? 此时 BASE 理论也就应运而生。 BASE(Base Available、Soft State、Eventually Consistence)——基本可用、暂时不一致、最终一致。短时间内数据不一致,可能会造成一定的脏读,但最终会达成一致,而达成一致的速度窗口,也就是个比较重要的指标。Paxos 和 Raft 算法是两个主流的最终一致性的算法。从 BASE 的定义来看,对于准确性高度敏感的金融财务领域,可能就不合适。 在存储类产品中,使用 Paxos 或 Gossip 算法,主要是用于协调各个节点的状态和版本,以完成同步。而在微服务领域中,我们面对的是各个 RPC 或 http 通信的不同类的应用服务 (可能使用这些算法也可以,复杂度应该是比较高,反正我是没试过),那么又怎么做到最终一致? 主要策略有两个:撤销、补偿。前者是努力恢复到操作前的一致状态,后者是努力保证成功、达到操作后的一致状态。看下图,Server 的某个业务操作中,要分别调用 Service-X、Service-Y、Service-Z 三个服务,才能完成。此时如果调用 Service-Z 的过程中出现错误了,怎么保证最终一致性? 按照上面的撤销或者补偿,就有两个策略:
在实际的实践中,除了类似 Service-Z 的环节失败,还有入库失败、网络通信失败、发送 MQ 失败等各种可能失败的环节,我在后面一篇《功夫贷的支付服务,是怎么实现最终一致性的》这篇文章里,拿一个具体的 case 详细地介绍了它的实现,供参考。要实现一个严谨的最终一致性,还是比较复杂的,所幸在全系统中,真正要保证绝对最终一致性的功能点,还是比较少的。 容量评估后续会专门拿个 case 来介绍实践,我们主要是强调拿线上环节通过路由、监控等策略,在线测试评估容量。搭建性能测试环境,在现在已经是相对落后的手段。 对于如今的互联网系统来说,越来越复杂,支撑系统必不可少,每一章都可以单独列文章来分析其原理,这里只列出些目录。
如果,Google 早已解决不了你的问题。 如果,你还想知道 Apple、Facebook、IBM、阿里等国内外名企的核心架构设计。 来,我们在深圳准备了 ArchSummit 全球架构师峰会,想和你分享:
|
|