人人都是架构师:分布式系统架构落地与瓶颈突破
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.2 系统服务化需求

既然服务化能够解决在拆分子系统时遇到的一些问题和瓶颈,那么接下来笔者就为大家详细讲解服务化的具体实施细节。笔者先为大家讲解服务化与RPC协议的具体关系,再实战演示目前在开源社区服务化领域最为开发人员所熟知的阿里分布式服务框架 Dubbo,以及企业在实施服务化改造后对现有系统产生的一些影响的解决方案。

1.2.1 服务化与RPC协议

在本章的前面几个小节中,笔者为大家详细介绍了究竟什么是服务化,以及为什么需要落地服务化架构。当然,在笔者正式开始为大家演示具体的实施细节之前,不得不提及的就是与服务化息息相关的RPC(Remote Procedure Call,远程过程调用)协议。从严格意义上来说,服务化其实只是一个抽象概念,而RPC协议才是用于实现服务调用的关键。RPC 由客户端(服务调用方)和服务端(服务提供方)两部分构成,和在同一个进程空间内执行本地方法调用相比,RPC 的实现细节会相对复杂不少。简单来说,服务提供方所提供的方法需要由服务调用方以网络的形式进行远程调用,因此这个过程也称为RPC请求,服务提供方根据服务调用方提供的参数执行指定的服务方法,执行完成后再将执行结果响应给服务调用方,这样一次RPC调用就完成了。

服务化框架的核心就是RPC,目前市面上成熟的RPC实现方案有很多,比如Java RMI、WebService、Hessian 及Finagle 等。在此需要注意,不同的RPC 实现对序列化和反序列化的处理也不尽相同,比如将对象序列化成XML/JSON等文本格式尽管具备良好的可读性、扩展性和通用性,但却过于笨重,不仅报文体积大,解析过程也异常缓慢,因此在一些特别注重性能的场景下,采用二进制协议更合适。看到这里或许有些人已经产生了疑问,实现服务调用无非就是跨进程通信而已,那是否可以使用Socket技术自行实现呢?带着疑问,笔者来为大家梳理一下完成一次RPC调用主要需要经历的三个步骤:

• 底层的网络通信协议处理;

• 解决寻址问题;

• 请求/响应过程中参数的序列化和反序列化工作。

其实,RPC 的本质就是屏蔽上述复杂的底层处理细节,让服务提供方和服务调用方都能够以一种极其简单的方式(甚至简单到就像是在实现一个本地方法和调用一个本地方法一样)来实现服务的发布和调用,使开发人员只需要关注自身的业务逻辑即可,如图1-8所示。

图1-8 RPC请求的调用过程

1.2.2 使用阿里分布式服务框架Dubbo实现服务化

由于RPC协议屏蔽了底层复杂的细节处理,并且随着后续服务规模的扩大要考虑服务治理等众多因素,因此笔者在生产环境中落地服务化架构时所使用的RPC框架就是阿里开源的分布式服务框架Dubbo。毫不客气地说,Dubbo真的是集万千宠爱于一身,目前大部分互联网企业都偏爱使用 Dubbo 来落地服务化架构,就是因为Dubbo的设计精良、使用简单、技术文档丰富。而且,Dubbo预留了足够多的接口使开发人员能够非常容易地对 Dubbo 进行二次开发,以便更好地满足业务需求,所以Dubbo 在开源社区拥有众多的技术拥护者和推进者。但是由于一些特殊原因,目前Dubbo的主干代码已经停止了更新(现在阿里内部主推HSF),当然这并不意味着我们在使用上得不到保障(目前 Dubbo 的版本已经相当稳定,而且由于很多坑别人都已经踩过,大家在使用过程中根本不需要去填坑,参考开源社区的解决方案即可),当当网基于Dubbo二次开发的Dubbox框架在开源社区同样广受好评。

如图 1-9 所示,Provider 作为服务提供方负责对外提供服务,当 JVM 启动时Provider会被自动加载和启动,由于Provider并不需要依赖任何的Web容器,因此它可以运行在任何一个普通的Java程序中。当Provider成功启动后,会向注册中心注册指定的服务,这样作为服务调用方的Consumer在启动后便可以向注册中心订阅目标服务(服务提供者的地址列表),然后在本地根据负载均衡算法从地址列表中选择其中的某一个服务节点进行RPC调用,如果调用失败则自动Failover到其他服务节点上。

图1-9 Dubbo服务调用

在此需要注意,在服务的调用过程中还存在一个不容忽视的问题,即监控系统。试想一下,如果在生产环境中业务系统和外围系统没有部署相对应的监控系统来帮助开发人员分析和定位问题,那么这与脱了缰的野马无异,当问题出现时必然会导致开发人员手足无措,相互之间推卸责任,因此监控系统的重要性不言而喻。值得庆幸的是,Dubbo 为开发人员提供了一套完善的监控中心,使我们能够非常清楚地知道指定服务的状态信息(如服务调用成功次数、服务调用失败次数、平均响应时间等),如图1-10所示。这些数据由Provider和Consumer负责统计,再实时提交到监控中心。

图1-10 Dubbo服务状态监控

接下来笔者就为大家演示Dubbo的基本使用,本书使用的Dubbo版本为2.5.3,开发人员可以通过Maven依赖的方式下载Dubbo构件,如下所示:

Dubbo 项目的 pom.xml 文件中依赖有一些其他的第三方构件,如果版本过低或者与当前项目中依赖的构件产生冲突时,那么可以在项目的 pom.xml 文件中使用Maven提供的<exclusions/>标签来排除依赖,自行下载指定版本的相关构件即可。除此之外,大家还需要注意一点,Dubbo的源码是在Java 5版本上进行编译的,因此在项目中使用Dubbo时,JDK版本应该高于或等于Java 5。

成功下载好运行 Dubbo 所需的相关构件后,首先要做的事情就是定义服务接口和服务实现,如下所示:

在上述程序示例中,服务接口 UserService 中提供了一个用于模拟用户登录的login()方法,它的服务实现为 UserServiceImpl 类。如果用户正确输入账号和密码,那么结果将返回“true”,否则返回“false”。服务接口需要同时包含在服务提供方和服务调用方两端,而服务实现对于服务调用方来说是隐藏的,因此它仅仅需要包含在服务提供方即可。

从理论上来说,尽管Dubbo可以不依赖任何的第三方构件,只需有JDK的支撑就可以运行,但是在实际的开发过程中,为了避免 Dubbo 对业务代码造成侵入,笔者推荐大家将Dubbo集成到Spring中来实现远程服务的发布和调用。在Provider的Spring配置信息中发布服务,如下所示:

成功启动 Provider 后,便可以在注册中心看见由 Provider 发布的远程服务。在Consumer的Spring配置信息中引用远程服务,如下所示:

成功启动Consumer后,便可以对目标远程服务进行RPC调用(使用Dubbo进行服务的发布和调用,就像实现一个本地方法和调用一个本地方法一样简单,因此笔者省略了服务调用的相关代码)。关于Dubbo的更多使用方式,本书不再一一进行讲解,大家可以参考Dubbo的用户指南:http://dubbo.io/User+Guide-zh.htm。

1.2.3 警惕Dubbo因超时和重试引起的系统雪崩

在上一个小节中,笔者为大家演示了Dubbo 框架的一些基本使用方法,尽管使用Dubbo来实现RPC调用非常简单,但是刚接触Dubbo框架的开发人员极有可能因为对其不熟悉而跌进一些“陷阱”中。因此,开发人员需要重视看似微不足道的细节,这往往可以非常有效地避免系统在大流量场景下产生雪崩现象。在对数据库、分布式缓存进行读/写访问操作时,我们往往都会在程序中设置超时时间,一般笔者不建议在生产环境中将超时时间设置得太长,否则如果应用获取不到会话,又长时间不肯返回,那么一定会对业务产生较大的影响。但将超时时间设置得太短又可能适得其反,因此超时时间究竟应该如何设置需要根据实际的业务场景而定,大家可以在日常的压测过程中仔细评估。

为 Dubbo 设置超时时间应该是有针对性的,比较简单的业务执行时间较短,可以将超时时间设置得短一点,但对于复杂业务而言,则需要将超时时间适当地设置得长一点。笔者之前曾经提及过,Dubbo的Consumer会在本地根据负载均衡算法从地址列表中选择某一个服务节点进行RPC调用,如果调用失败则自动Failover到其他服务节点上,默认重试次数为两次,服务调用超时就意味着调用失败,需要进行重试,如图1-11所示。在此需要注意,如果一些复杂业务本身就需要耗费较长的时间来执行,但超时时间却被不合理地设置为小于服务执行的实际时间,那么在大流量场景下,系统的负载压力将被逐步放大,最终产生蝴蝶效应。假设有1000个并发请求同时对服务A进行RPC调用,但都因超时导致服务调用失败,由于Dubbo默认的Failover机制,共将产生3000次并发请求对服务A进行调用,这是系统正常压力的3倍,若处于峰值流量时情况可能还会更糟糕,大量的并发重试请求很可能直接将Dubbo的容量撑爆,甚至影响到后端存储系统,导致资源连接被耗尽,从而引发系统出现雪崩。

图1-11 Dubbo的Failover机制

还有一点很重要,并不是任何类型的服务都适合Failover的,比如写服务,由于需要考虑幂等性,因此笔者建议调用失败后不应该进行重试,否则将导致数据被重复写入。只有读服务开启Failover才会显得有意义,既然不需要考虑幂等性,就可以通过Failover来提升服务质量。

除了Failover,Dubbo也提供了其他容错方案供开发人员参考,如下所示:

上述相关参数除了可以在服务调用方配置,也适用于服务提供方,如果服务调用方和服务提供方配置有相同的参数,默认以服务调用方的配置信息为主。关于更多配置信息,本书就不再一一进行讲解,大家可以参考Dubbo的配置参考手册。

1.2.4 服务治理方案

当企业的系统架构逐渐演变到服务化阶段时,架构师重点需要考虑的问题是服务如何拆分、粒度如何把控,以及服务或服务之间的RPC调用应该如何实现。当这些问题迎刃而解后,并不意味着我们就能够一劳永逸,随着服务规模逐渐扩大,一些棘手的问题都会暴露出来,因此架构师必须具备一定的前瞻性,提前规划和准备充足的预案去应对将来可能发生的种种变故,否则这就等于给自己挖坑,与其花大把时间去填坑,不如用更多的精力去思考企业在大规模服务化前应该如何实施服务治理。

从严格意义上来说,Dubbo不仅是一个RPC框架,更是一个服务治理框架,因为Dubbo几乎提供了一套完整的服务治理方案,所以它从诞生起就备受瞩目和爱戴。由于服务治理涉及的范围非常广泛,可能会导致一些人对服务治理的概念比较模糊,或者根本不理解服务治理的重要性和必要性,笔者归纳的关于服务治理的三个基础要素如下所示:

• 服务的动态注册与发现;

• 服务的扩容评估;

• 服务的升/降级处理。

在1.2.2节中,笔者曾为大家介绍过Dubbo的注册中心,那么大家思考一下,为什么需要引入注册中心呢?当服务变得越来越多时,如果把服务的调用地址(URL)配置在服务调用方,那么URL的配置管理将变得非常麻烦,因此引入注册中心的目的就是实现服务的动态注册和发现,让服务的位置更加透明,这样服务调用方将得到解脱,并且在客户端实现负载均衡和Failover将会大大降低对硬件负载均衡器的依赖,从而减少企业的支出成本。部署监控中心是为了更好地掌握服务的状态信息,因为有了这些统计数据后我们才能够准确地知道指定服务的热度,毕竟单台服务器的处理能力有限。为了应对大促活动,扩容机器是提升服务器并行处理能力一个非常重要的常规手段,为了避免盲目扩容造成资源浪费,运维人员可以将统计数据作为参考指标,并通过调整服务器的权重比例来综合评估究竟需要扩容多少节点才能够合理且有效地支撑用户流量。服务的升/降级处理也非常重要,在一些特殊场景下,如果系统容量在支撑核心业务时都捉襟见肘,那么完全可以对一些次要服务进行降级处理,牺牲部分功能来保证系统的核心服务不受影响。当某些新上线的服务可能在实现上存在缺陷时,也可以采用服务降级的手段来确保系统的整体稳定性,以后再将这些降级服务进行升级处理即可。

关于服务黑白名单、服务权限控制、服务负责人,以及服务资源调度等其他的服务治理问题,大家可以参考 Dubbo 的用户指南。而关于服务调用跟踪的问题,大家可以直接阅读1.3节。

Dubbo 为开发人员提供的监控中心和管理控制台都需要单独安装和部署,大家可以参考Dubbo的管理员指南:http://dubbo.io/Administrator+Guide-zh.htm。如图1-12所示,在管理控制台中服务治理包含的功能有:路由规则、动态配置、服务降级、访问控制、权重调节及负载均衡等,成功安装好dubbo-admin后,便可以对其进行访问和实施服务治理。

图1-12 Dubbo管理控制台界面

1.2.5 关于服务化后的分布式事务问题

从单机系统演变到分布式系统并不像书本中描述得那样简单,不同的业务之间必然会存在较大的差异,因此企业在实施服务化改造时肯定会困难重重,即使最终服务成功被拆分出来,架构师还需要提前思考和规划后续的服务治理等问题。当然这一切都还不是终点,就像本书封面插图所示的那座冰山,我们能够看见的问题其实只是冰山一角,大型网站架构演变过程中等待我们解决的技术难题还有很多。大家思考一下,实施服务化改造后事务的问题应该如何解决?或许很多同学都会毫不犹豫地指出,分布式事务简直让人感到“痛心疾首”。的确,就算是银行业务系统也并不一定都采用强一致性,那么我们是否还有必要去追求强一致性呢?

其实分布式事务一直就是业界没有彻底解决的一个技术难题,没有通用的解决方案,没有高效的实现手段,但是这并不能成为我们不去解决的借口。网络上有一句非常著名的段子,“此处不留爷,自有留爷处;处处不留爷,爷走出国路”,既然分布式事务实施起来非常困难,那么我们为什么不换个思路,使用其他更优秀的替代方案呢?只要能够保证最终一致性,哪怕数据会出现不一致的短暂窗口期又有什么关系?在架构的演变过程中,哪个是主要矛盾就优先解决哪一个,就像我们对JVM进行性能调优一样,吞吐量和低延迟这两个目标本身就是相互矛盾的,如果吞吐量优先,那么GC就必然需要花费更长的暂停时间来执行内存回收;反之,频繁地执行内存回收,又会导致程序吞吐量的下降,因此大家要学会权衡和折中。关于最终一致性的实现方案,大家可以直接阅读5.2.8节和5.4.2节。