49

重新理解微服务

 6 years ago
source link: http://www.10tiao.com/html/773/201806/2247487946/1.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

微服务是个说的挺长时间的概念,也是比较成熟的技术体系。像 Spring Cloud,甚至提供了微服务所需要的全套框架,包括注册中心 (Eureka)、配置中心 (Config)、断路器 (Hytrix)、API 网关 (Zuul) 等组件。微服务体系庞杂,每个组件都能独自成章。本文作者仅从个人的经验和实践出发,谈了谈自己对微服务及其部分内涵的思考和理解。

作者张珂目前任职于杭州大树网络技术有限公司,担任首席架构师,负责系统整体业务架构以及基础架构,熟悉微服务、分布式设计、中间件领域,对运维、测试、敏捷开发等相关领域也有所涉猎。(同时也欢迎关注我的微信,ID:gh_bd9717312199)。

微服务与 SOA/ESB 的异同

微服务与更早就起来的 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) 到基础设施层,给微服务的治理带来了新的方向。

微服务的关键特性 服务粒度

服务的粒度,切分到多大算合适? 太粗的话,这服务就涵盖过多的业务逻辑,从而难维护、易出错;太细了,就会搞出很多的工程,造成很大的工程维护和通信成本。

主流说法是依据康威定律——团队的交流机制应该和组织机构相匹配。应用到软件领域来看,如果某个应用,需要多个组织之间一起交流和修改,那么它的交流机制就大于组织机构了,出现了不匹配的情况,那么这个应用很可能就太粗而需要拆分。

这里有个不太好懂的地方——既然系统架构和团队组织机构想匹配,那我们是先定系统架构呢,还是先定团队组织机构呢? 这有点类似先有鸡还是先有蛋。我觉得可以这么来理解:无论是团队怎么定、还是架构怎么定,这都是跟着业务的发展而发展的,可以说都是业务的衍生发展而来。所以系统架构设计,首要做的还是业务理解和切分——业务切分决定了服务切分、业务切分也决定了团队组织。

业务切分有两种简单办法:

  1. 参照业内同类公司的划分:比如电商,业内比较成熟的:支付、库存、订单、搜索、用户等;

  2. 将自身业务的主要信息流画出来,先找出其中的名词或动名词,它就可能是个服务

eg:在我们的线上贷款业务中,典型的 user case 是这样:

  1. 用户导入几项金融资料数据

  2. 系统根据信息清洗出部分衍生变量

  3. 系统跑欺诈规则

  4. 系统计算授信,给出额度

  5. 用户试算得到月费率和利息

  6. 系统人工信审

  7. 系统放款

  8. 到还款日时用户还款或者我们系统主动扣款

将其中的名词整理出来,整理流程大概就是如下图:

这些都是候选服务。根据其复杂度和相关性,做适当的拆解和合并,形成了如下几个子系统及服务。

治理范围

从服务的角度来看,对外公开的是契约——即我们系统提供哪些特性,而内部算法 / 数据都应该隐藏起来,而在不同服务间“是共享数据库还是独享数据库”上,实践中的冲突和困惑,体现地比较明显。

我们假想个流程,ServiceA 的李雷需要更新 User 表的某个字段,如果大家数据库表都共享的,李雷只要写个 SQL 就解决了。但一旦把 User 表服务化后,归到 UserCenter 这个服务自治之后,问题就麻烦了:

  1. 李雷要去找 UserCenter 团队——假设是韩梅梅接了这个需求,好在是个女生,男女搭配干活不累——讲清楚他的需求或提供需求文档;

  2. 韩梅梅理解了需求,设计接口、提供文档、评审并准备开发;

  3. 韩梅梅可能手里有其他事,所以这个需求大概要等几天才能开发;

  4. 终于韩梅梅开发完了,她要自测、部署;上游李雷开始联调,如果有问题,需要双方再沟通解决;

  5. 联调完毕上线,韩梅梅的 UserCenter 先上,李雷的业务系统再接着上;

从这可以看到,一旦一个人、一个系统做的事,变成了 2 个人、两个系统来做,那要多出多少麻烦了。所以我完全理解,在公司早期,所以业务系统共享一套数据库表,是多么地务实。我们功夫贷在创业之初也是这么做的,在创业 2 年后,它的弊端开始密集体现,而服务化改造过程中,我们也是付出了相当大的代价。

随着用户量和数据量的上升,这种共享数据库表的最明显的弊端就是慢查询越来越多——因为谁都可以操作任何一张表、而开发过程中或者是对业务理解不够、或者是 SQL 能力不足,很容易写出慢 SQL 来,其结果就是导致 DB 的 CPU 飙升到 100%、或者是 IOPS 被打满,从而全 APP 被拖慢甚至无法提供服务。这种危害是相当巨大的。

所以,从运行时的慢 SQL 带来的巨大杀伤力来说,数据库应该是隐藏在服务内部,该服务由熟悉该业务的固定团队维护、也会做很多优化。虽然开发阶段慢了,但是运行时稳定了、系统的可用性得到了保障。只是这件事,不应该在创业初期就做,那样会比较严重地放缓系统迭代速度、更应该在系统规模相对较大的时候来改造。

当然,我们说改造是要付出代价的。不仅之前的一个库中的表,要分成不同的库,各服务的程序要做不小的改造,其中最困难的是,同一张表的字段,可能会属于各个不同的应用。看下面这个 User 表。

开始的时候,User 表只包含了完全业务无关的属性,但随着系统的发展,一些和业务相关的字段 (上图红色部分) 逐渐地被加进来——这也不完全是决策时犯的错误,而是本身这属性是否和业务有关,也不是很容易界定。所以逐渐会发现,很多系统都会依赖这张表,从而交织难以拆分。各个服务可能都需要有这张表,而各自维护自己所关心的那部分字段及功能。

在我们的实践中,服务化的过程以及数据迁移,大约是这样的步骤 (以“用户中心”应用为例):

  1. 创建新应用 UserCenter,梳理清楚其的业务边界和所涉及的数据表;

  2. 收集和分析其他系统对这些数据表的需求,并在 UserCenter 中开发接口,以备上游系统调用;

  3. 逐渐改造上游系统,使其由原先的读取数据库,修改为调用 UserCenter 接口。由于有多个上游系统和功能需要改在,因此这个阶段会比较长,上游系统在这个时间周期内,也会“访问接口服务”和“直接访问数据库”这两种形态并存;

  4. 检查并确认上游系统都改造完毕上线,此时理论上应该没有上游系统直接读取 UserCenter 的表了,都通过接口了,此时准备迁移 UserCenter 的表数据;

  5. 建立 New UserCenter DB,并通过 DB 同步机制,实时地将 UserCenter 的表数据由老库同步到新库。在新库同步完成之前,UserCenter 的应用仍然使用的是老库里的表;

  6. 新库同步完毕,UserCenter 应用切换到新库,此时所有的新数据都会进新库,而老库理论上是不用了;

  7. 断开新老库的同步链,同时 rename 老库的表 (先不删,同时在 rename 前一定要断开同步链,否则新库也会被同步 rename 掉了)。如果此时万一有某个系统的功能,在之前的系统改造 / 测试中遗漏了没被发现,仍然是直接读取的数据库表,那么这时候就会报错 (因为表名被 rename 掉了,找不到了)。此时就是个恢复窗口,赶紧把表 rename 回来,减少损失,然后再继续处理。这也是前面千万不能直接把老表删除的原因;

  8. 运行几天没问题之后,再把老库的表删除,整个服务化过程结束;

服务组合

在微服务之后,各个系统只对某一块业务负责,那么就有可能需要对服务做一些聚合。下面是常见的两种模式:

这是聚合服务的模式,由 web 应用去负责聚合后端服务或做个性化处理,这是它的好处——可以根据自身的业务做任何组合和处理,而它的坏处也很明显——对于不需要特殊处理的,也得过它一道。

这是后台服务自包含的模式。某个后台应用,依赖于其他服务,于是就将其他服务的相关调用都处理完了,或者这么理解——后台服务也有多个层次:库存服务、支付服务、发票服务是最底层的,交易服务是更上层一些的共享服务,从而达到封装细粒度服务的目的,与此同时,它的个性化也就丧失了。假如有个交易,是不需要发票服务的,那么这种模式就不是太灵活。

从我个人的经验来看,我是倾向于聚合服务这种模式。每个前端应用,还都是应该有个自己的后台服务,去完成很多小的功能 (比如更新 APP 版本、展示首页广告、记录埋点等 APP 特有 feature)、以及聚合。而对于不需要 App-Server 处理、直接使用后台服务的,应该能够通过 gateway 直接调用,而不需要 App-Server 来做代理转发。

容错

容错的目的就是在出现问题的时候,仍然能够正常提供服务,其具体表现形式有这么几种:

  • 当调用下游服务 B 出错的时候,可以在安全的情况下考虑重试;

  • 当调用下游服务 B 出错的时候,可以调用替代服务 B';

  • 当调用下游服务 B 出错的时候,是否可以返回某个默认值、或者返回最近一次的值?

  • 当调用下游服务 B 超时的时候,如果超时请求达到一定数量,则需要熔断,以保证自身其他服务能正常提供服务,而不会被拖垮;

  • 当调用下游服务 B 出错的时候,能否以异步 + 定时任务补偿的方式代替?

上面这些特性,有些是通过 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 的过程中出现错误了,怎么保证最终一致性?

按照上面的撤销或者补偿,就有两个策略:

  1. 撤销:Service-X 和 Service-Y 提供反向的撤销接口。如果调用 Service-Z 失败,则调用 Service-X 和 Service-Y 的反向撤销接口,以恢复到操作前的状态。如果撤销的过程中失败? 呵呵,那又要补偿了。

  2. 补偿:Service-X 和 Service-Y 都执行成功了,那么 Service-Z 调用失败,在“确保 Service-Z 只要恢复正常、必然能执行成功的前提下 (无论是系统自动还是人工)”,通过定时任务重试或者 MQ 的机制,补偿重试,再不行人工处理,直到 Service-Z 成功,以达到都成功的状态。这里“确保 Service-Z 必然能执行成功”非常重要。以账户转账举例,如 Service-Z 的操作是从账户上扣 100 元,但他的余额只有 10 元,那无论怎么重试、人工,都是不可能成功的,也就不可能达到最终都操作成功的一致性状态,此时要么提前校验、锁定,要么就采用前面的撤销的思路。

在实际的实践中,除了类似 Service-Z 的环节失败,还有入库失败、网络通信失败、发送 MQ 失败等各种可能失败的环节,我在后面一篇《功夫贷的支付服务,是怎么实现最终一致性的》这篇文章里,拿一个具体的 case 详细地介绍了它的实现,供参考。要实现一个严谨的最终一致性,还是比较复杂的,所幸在全系统中,真正要保证绝对最终一致性的功能点,还是比较少的。

容量评估 / 测试

容量评估后续会专门拿个 case 来介绍实践,我们主要是强调拿线上环节通过路由、监控等策略,在线测试评估容量。搭建性能测试环境,在现在已经是相对落后的手段。

支撑系统

对于如今的互联网系统来说,越来越复杂,支撑系统必不可少,每一章都可以单独列文章来分析其原理,这里只列出些目录。

  • 业务监控系统:如订单量、转化率、各类报表等,以及相应的预警子模块。这主要是从业务视角来看的监控,很直接有效;

  • 日志系统:开源的选择之一就是 ELK 套装,在数据量大的情况下,如果要把这三个搞稳定运行,工作量也是不小的;

  • 分布式调用链系统:跟踪请求在全系统中的去向、以及快速定位出问题的地方;

  • 服务设施监控系统:传统的 CPU、Memory、Disk 这个很多产品都提供,但还不够,应用内的情况,我们也要知道,主要包括:线程池使用情况、GC 的频率和时间、线程栈

  • 技术指标监控系统:Error 率、Latency、Exception 统计等

  • 运维支撑系统:资源管理、容器化、部署、灰度、可用性指标等

  • 测试平台:接口测试、MQ 测试、集成测试、性能测试、测试环境构建、持续集成


如果,Google 早已解决不了你的问题。

如果,你还想知道 Apple、Facebook、IBM、阿里等国内外名企的核心架构设计。

来,我们在深圳准备了 ArchSummit 全球架构师峰会,想和你分享:

  • 微信百亿消息背后的万级机器是怎么做 AI 调度的

  • 滴滴三核心引擎之一的地图,如何计算路径规划和道路匹配

  • 微博如何做万亿级关系的实时协同推荐

  • 微众区块链首席架构师的两个具体案例实操

  • 阿里菜鸟全球跨域 RPC 架构设计

  • 前特斯拉视觉深度学习负责人带来的核心技术解析

  • 微服务楷模 Netflix 在 FaaS 上的最新实践


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK