摘要:如果系统比较简单,未来也不会再变动,那么服务治理和配置管理的问题就不会太突出,但“不变”或多或少地意味着系统在开发完成的那一刻就已经停止发展了,这是一件可悲的事情。当我们的业务在不断发展时,客户和产品的需求越来越多,系统也会变得越来越复杂。如果没有对服务治理和
服务治理和配置管理
如果系统比较简单,未来也不会再变动,那么服务治理和配置管理的问题就不会太突出,但“不变”或多或少地意味着系统在开发完成的那一刻就已经停止发展了,这是一件可悲的事情。当我们的业务在不断发展时,客户和产品的需求越来越多,系统也会变得越来越复杂。如果没有对服务治理和配置管理提前做整体规划,那么当系统日趋复杂时,系统就会逐渐变得失控,甚至最后我们彻底失去对原有系统做出改动的能力,不得不重新设计系统架构。
所以,在设计系统和开发软件时,即便不能预见产品最终成熟时的模样,作为研发人员,我们还是要稍微有所远见并考虑得全面些。至少关于服务治理和配置管理的一些通用模式和准则,我们是可以掌握和遵守的。服务治理和配置管理并非实时流计算系统的核心问题,没有它们,我们也可以构建一个可用的系统。但作为辅助系统,如果将服务治理和配置管理的问题处理好了,它们有助于系统在随业务发展的过程中平稳演进,让我们少走很多弯路。本章就来讨论一下实时流计算系统中服务治理和配置管理的问题。
10.1 服务治理
在流计算系统中,流代表业务执行的过程。在流计算应用执行具体的计算逻辑时,可能会用到一些独立的服务。例如,使用IP分析服务将IP转化为省份和城市、使用地理位置解析服务将GPS坐标转化为省份和城市、使用第三方征信服务对个人信用风险做出评估等。这些独立的服务虽然与业务系统相关,但它们又明显不应该属于业务系统的主体执行流程。那我们应该如何在实时流计算应用中规划和组织业务的主体执行流程及它所依赖的其他独立服务呢?本节就来讨论这个问题。
10.1.1 流服务和微服务
当一个服务模块的输入和输出都是流的时候,我们称其为流服务。流服务的好处在于其可以直观地描述业务执行流程。流服务使用DAG来描述执行流程,DAG的每个节点代表一个业务单元,每个业务单元负责一定的业务逻辑。
在业务单元中,经常会用到一些具有特定功能的辅助性服务,如IP分析、GPS解析、第三方征信服务等。将实现这些辅助性功能的代码直接放入流计算应用的业务代码里,或许是一个好方法,特别是在我们非常在乎性能的时候。毕竟将这些辅助性功能的逻辑集成到流应用里,会减少相当多的I/O操作,确保了流应用的性能。但是这样做并不优雅。考虑下,如果流计算任务需要用到很多辅助性功能(这种情况其实相当常见),而且这些辅助性功能中的某些内部逻辑甚至相当复杂,那么将这些功能的实现代码全都放到业务流程的实现中,势必会造成业务逻辑和技术细节纵横交错、程序执行流程杂乱无章。
一种折中的方案是将辅助性功能抽取为独立的源码项目,将它们编译为库后再链接进流计算应用。这样一方面能够保证流计算应用的性能,另一方面避免了流计算应用的代码过于杂乱。这样做不失为一个比较好的办法,并且在性能优先的情况下可能是最优选择。但这样做也存在问题,即每次对辅助性功能服务做更改或升级时,流计算应用必须重新构建、测试和发布。
从服务治理的角度而言,我们还是应该将辅助性功能剥离出去,让它们成为单独的服务,对外提供REST或RPC的访问接口。图10-1描述了这种将辅助性功能剥离为单独微服务,由流服务调用接口访问的架构。这样,流计算应用负责整体的业务逻辑,而辅助性功能被封装在一个个独立的微服务内并对外提供友好的使用界面,整个流计算系统架构清晰,在将来需要调整时也更加灵活。
图10-1流服务和微服务关系
在流服务中调用外部的微服务也存在一个问题,即性能问题。在第5章讲解状态存储时,我们建议使用本地数据存储方案替代远程数据存储方案,原因在于远程数据存储方案可能会极大地降低流服务的性能。与此类似,在流服务中调用外部微服务时也涉及网络I/O,这同样会比较显著地降低流服务的性能。所以,我们要针对微服务的调用过程做优化。一方面,要小心谨慎地设计微服务,确保微服务能够快速地返回,不管是成功还是失败,都必须在给定的时间内快速返回。另一方面,流服务在调用微服务时,可以采取异步I/O的方式,这样能够保证流服务在处理事件时不会让CPU阻塞在等待微服务请求返回,从而提升流服务的吞吐能力。
另外,必须强调的是,在流计算中使用微服务最好采用只读方式,或者至少应该是幂等的。因为,如果流服务访问微服务时造成了外部状态的改变,就有可能破坏流计算应用整体的可靠性保证机制。关于出现这个问题的原因,我们在第6章讨论各种开源流计算框架的消息传达可靠性保证机制时已有所分析,这里不再赘述。
相比流服务,微服务是一种更加为大众所知的服务组织架构。从形式上,微服务和流服务最大的区别在于,微服务是请求并响应的模式,而流服务则是事件驱动的模式。微服务系统架构将复杂软件系统按业务功能划分为一个个独立的服务模块,每个服务模块独立开发、独立部署、独立提供服务,各独立服务模块之间天然是一种松耦合的状态。
微服务确实有助于我们分解复杂的系统,但与之而来的问题是,它会让业务系统变得复杂。相比流服务有一个提纲挈领的DAG代表了完整的业务流程,微服务系统如果没有额外的设计文档进行解释,那么我们是很难一下就弄清楚业务系统的完整执行流程的。
相比微服务而言,流服务的服务治理方案是“与生俱来”的,原因有以下几点。
第一,大部分流计算框架是构建在诸如YARN这样的分布式操作系统上的,所以它们所运行的环境已经云化。这意味着基于这些流计算框架构建的应用是可以自由横向伸缩的。
第二,大部分流计算框架或多或少地提供了管理界面,这让我们能够非常方便地监测和追踪运行在系统中的应用的状况。
第三,大部分流计算框架具备一定的容错机制,并且可在服务失败时自动完成服务恢复,不需要我们外部干预。
但是微服务就不一样了。微服务系统架构和服务治理还是有较大距离的,甚至可以说,服务治理的概念最初正是为了更好地管理微服务系统而提出的。针对微服务系统的服务治理方案多种多样,从Apache Dubbo 和Spring Cloud,到Docker Swarm 和Kubernetes,再到如今的Service Mesh等,各种微服务治理方案可谓方兴未艾,它们正在快速地发展和演进过程中。虽然像诸如Kubernetes和ServiceMesh等前沿、新颖的微服务治理方案确实非常有趣,但限于本书的主题,我们不展开讨论它们,强烈建议感兴趣的读者自行查阅相关资料。
笔者只在这里斗胆做一个预言,在诸如Kubernetes和ServiceMesh等基于资源云化技术和服务编排技术的服务治理平台更加成熟和普及时,未来微服务和流服务之间的边界将越来越模糊,直接基于这些服务治理平台开发流计算框架也未尝不是一件有趣的事。
接下来我们将通过Spring Cloud来讨论服务治理中的几个核心问题。
Spring Cloud是Java领域较著名、较流行的微服务开发框架。
Spring Cloud以Spring Boot为基础,围绕微服务提供了一系列服务治理功能,如服务注册及服务发现、负载均衡、容错保护、配置管理、链路追踪和服务网关等,如图10-2所示。Spring Cloud最大的作用不在于实现微服务,而在于更好地管理和监控整个微服务系统。系统提供了哪些微服务,哪个服务实例宕机了,服务是否中断,哪个服务性能不足,如何扩展或收缩服务的处理能力,系统整体的吞吐和时延如何,系统资源的使用效率怎样……方便快捷地给出所有这些问题的答案,让我们只需专注于业务逻辑的开发,这才是Spring Cloud(或者说服务治理)的价值所在。
1.服务注册及服务发现
当微服务系统因为业务功能的增加而逐渐变得复杂时,由于微服务架构松耦合的特点,微服务实例的组织会变得零散杂乱。这时我们就需要一个服务注册中心来统一管理这些微服务实例,否则我们将不得不手动管理大量的服务代理和服务实例IP,这一方面提高了服务和服务之间的耦合度,另一方面增加了运维的复杂性。服务注册用于服务提供者向服务注册中心注册自己所提供的服务,包括服务端点(endpoint)和服务内容等信息;而服务发现则是服务使用者从服务注册中心获取服务提供者信息的过程。当服务使用者从注册中心获取到提供服务的服务实例信息后,就可以向其发起服务请求了。
图10-2 Spring Cloud微服务系统
在Spring Cloud中,负责服务注册和服务发现的组件是SpringCloud Eureka,其中Eureka是“找到了,发现了”的意思。据说先贤阿基米德在一次洗澡时灵光乍现,发现了浮力的原理,当时他高兴坏了,手舞足蹈地叫喊着:“Eureka!”
2.负载均衡
负载均衡的作用有两个,一是为服务提供横向扩展或收缩服务实例数量的能力,二是为服务提供高可用的能力。负载均衡的实现方案有两种,一种是使用类似于Nginx这样的负载均衡器,另一种是直接在客户端实现多个服务实例之间的负载均衡。图10-3展示了这两种不同的负载均衡方案。Spring Cloud采用的是第二种方案,即直接在客户端实现负载均衡。Spring Cloud提供了一个名为Ribbon的HTTP负载均衡客户端工具。
Ribbon并非一个服务,而是一个工具类框架,因此只需集成在客户端使用即可,不需要另启额外的负载均衡器。
图10-3两种不同的负载均衡方案
3.容错保护
微服务架构中存在着非常多的服务单元。当某个单元出现故障时,就有可能因为服务间的依赖关系,故障发生蔓延,最终导致整个系统不可用。例如,在某个微服务体系中,服务A需要调用服务B,服务B又需要调用服务C。现在服务C中某个实例出现故障,响应非常缓慢,当服务B的请求被轮询分配到这个故障实例后,服务B的这个实例也受到影响,服务也变得缓慢。更有甚者,服务B的所有实例的请求都有一定概率被分配到这个有故障的C服务实例上,最终导致所有的服务B实例都出现处理缓慢的情况。依次类推,最终服务A的所有实例也会变得响应缓慢。
最终,整个系统因为服务C的一个实例故障,而变得不可用。如果一开始,当服务C的实例发生故障时,就将其剔除在服务提供者清单中,就可以避免这种故障蔓延的问题。等到故障实例被修复后,再重新将其添加到服务提供者清单中。针对这类问题,在微服务架构体系中产生了“断路器”的概念。所谓断路器,就是通过故障监控,当服务实例发生故障时,立刻向服务请求方返回一个错误响应,而不是让服务请求方长时间等待回应,卡死在调用的地方。
图10-4熔断器原理
Spring Cloud Hystrix组件除了实现“断路器”的功能外,还提供了更全面的服务降级、线程隔离、请求缓存、请求合并、服务监控等一系列服务保护功能。应该说,Hystrix提供的这些概念,对于我们构建高可用、高可靠系统是非常有启发性的,所以建议感兴趣的读者不妨自行研究下Hystrix的实现细节,分析它实现各种功能的思路和技巧。
4.配置管理
配置是微服务系统非常重要的组成部分。特别是当系统中存在大量的微服务实例时,配置会变得复杂。而不同模块、不同环境(开发、测试和生产)、不同版本等因素的存在,更是极大地增加了配置管理的复杂度。这个时候,一个统一的配置管理中心就变得十分重要。
使用Spring Cloud Config可以轻松地实现配置中心的功能。
Spring Cloud Config默认使用Git来存储配置信息,天然支持了配置信息的版本控制。不过目前Spring Cloud Config对动态配置更新的支持不是非常友好。一方面,Spring Cloud Config不支持配置变更后的自动通知配置刷新,必须手动刷新;另一方面,配置刷新时的粒度太粗,只有refresh命令可用于通知微服务刷新配置。这样,如果配置组织得不合理,如将系统配置和业务配置都放在动态配置的作用域内,系统配置和业务配置就容易存在全部更新的问题。或许这在最终服务功能上没什么不一样,但很明显这是比较危险的操作。考虑下,你本来只想修改业务的某个配置项,结果也刷新了另一个系统配置(如服务端口),无论服务端口是否真的修改了,这种做法都是风险比较高的事情,因为它产生了我们预期之外的副作用。所以,在使用Spring Cloud Config的动态刷新功能时,必须严格规划好动态配置的内容,以及它们的作用域,也就是用@RefreshScope注解严格限制动态配置刷新的内容和范围。当然话说回来,Spring框架总是给我们留下了自由发挥的空间,如果结合Spring Cloud Bus进行二次开发,对动态配置进行更加精细的控制也是能够实现的。
5.链路追踪
当业务变得复杂时,微服务间的调用关系势必会变得复杂。这时候,当一个客户请求过来时,如果一切正常,那么客户会即时得到响应。这种情况下一切都好。但是,如果系统某个环节发生故障,客户请求得不到正常响应,那我们该如何快速定位故障?这种情况下,对请求处理过程的完整链路追踪就变得非常重要。
Spring Cloud Sleuth提供了全链路调用追踪功能。图10-5展示了链路追踪的原理。Sleuth在进行链路追踪时使用了3个概念:trace、span和annotation。其中,trace代表一次完整的请求处理链路,链路中的每一次请求及响应被表示为一次span,而annotation用于标记每次span过程中具体的事件,如CS(Client Sent)表示客户端发起请求,SR(Server Received)表示服务端收到请求,SS(Server Sent)表示服务端返回请求的响应,CR(Client Received)表示客户端收到请求的响应。总体来说,使用链路追踪技术不仅有助于我们快速定位故障,而且有助于我们分析各环节处理时延,对系统进行性能优化。所以,链路追踪技术不愧是构建复杂业务系统必备之利器。另外,链路追踪技术除了可以于微服务外,在流服务领域如果系统不能保证消息可靠传输,那么也可以借鉴这种技术手段来追踪消息的处理过程。
图10-5链路追踪的原理
6.服务网关
大多数时候,当我们的业务系统最终提供给用户使用的时候,原本内网的一些服务端口需要暴露到互联网上,此时我们需要使用服务网关。服务网关最重要的功能是实现反向代理,以及对所有外网请求的认证和授权。
反向代理的作用在于将不同服务的请求路由到提供相应服务的服务器上去,这样可以将功能各不相同的独立服务汇聚为完整的业务界面,在相同的域名下对外提供服务。通过反向代理,即使在业务复杂、功能繁多、API丰富的情况下,我们也能轻松实现对外接口的统一。认证和授权是服务网关的另一个重要作用。如果暴露在公网上的服务没有认证和授权功能,你都不知道究竟是哪些人在访问你的服务,他们是好人还是坏人,是真人还是机器人,是善意的还是恶意的……当我们使用服务网关反向代理的功能将各个服务端口收纳到一起之后,就可以对所有外部请求进行统一的认证和授权了,从而不必让相同的代码和验证流程散布在系统各处。
当然,服务网关还有其他功能,如负载均衡、性能监控和流量控制等。可以说,我们希望通过AOP(Aspect Oriented Programming,面向切面编程)实现的那些功能,大多能够使用在服务网关上。
Spring Cloud提供了两个服务网关实现,即Zuul和Gateway。其中,Gateway采用了Netty框架实现,在高并发情况下比Zuul性能会好上不少,所以,对于新项目,不妨直接使用Gateway做服务网关。
来源:大数据架构师