在今年的CNUTCon全球容器技术大会2016上,Netflix资深架构师介绍了支撑其庞大业务的微服务生态系统,本文从总结了现场的演讲,试图为各位还原Netflix微服务的面目:
Netflix是一家在全球范围内提供流视频服务的公司,截止到2016年已经拥有8300+万订阅用户,每天播放时间达到了1亿2千万小时,是北美互联网峰值下载量的1/3。
为了支撑庞大的用户访问,Netflix从2009便下决心向云原生的微服务生态系统演进,在2016年完成了整体应用迁徙到云端,目前拥有500+的微服务,在系统演进的7年内,Netflix的流量增长了1000多倍,可谓是开着火箭换发动机。坚决的转向微服务+云原生让Netflix演进为一家成功实践微服务架构的互联网公司,更重要的是Netflix几乎开源了公司内部全部的微服务架构技术栈,这套架构在Netflix公司大规模分布式微服务环境中经过数年的生产环境检验被证明是可靠的。对于打算采用微服务架构的公司来说,可以在这套事实标准架构的基础上进行符合自身业务需求的定制化。所以看看Netflix给出的菜:
Archaius:服务配置管理组件 https://github.com/Netflix/archaius
对于传统的单体应用,配置文件可以解决配置问题,但试想一下在一个500+的微服务生态中,微服务采用多种语言开发,配置文件格式五花八门,如果把每个微服务的配置文件都改一遍,假设修改完配置文件后还要重启服务,那是怎么样的噩梦,简直酸爽。所以,对于微服务架构而言,一个通用的配置中心是必不可少的。
Archaius提供的DynamicIntProperty类可以在配置发生变化时动态地获取配置,并且不需要重启应用,一个简单的例子如下:
DynamicIntProperty prop=DynamicPropertyFactory.getInstance().getIntProperty("myProperty", DEFAULT_VALUE);
// prop.get() may change value at runtime
myMethod(prop.get());
这样的特性还不够方便,所以Archaius还提供了一个回调方式让微服务来响应配置变化
prop.addCallback(newRunnable() {
public void run() {
// ...
}
});
Archaius作为配置中心是如何获取到配置项的呢?Archaius采用了pulling架构从配置源获取配置,并支持多种方式包括JDBC、REST、配置文件等等,换句话说Archaius可以从数据库(比如Amazon DynamoDB)、配置文件等源获取配置。并且Archaius可以启动一个定时器,定时从配置源同步配置项,这个定时器则可以由用户配置。
另外我们在实践中知道对于一个微服务而言,在sit、uat、prod等环境中配置项一般是不一样的,所以Archaius可以保存同一个配置项的不同版本,这些版本可以来自于不同的配置源,用户可以为同一个配置项的不同版本定义等级(hierarchy),这样不同环境就可以获取不同hierarchy中的配置了。
在一个微服务生态中,成千上百的微服务都从配置中心动态的读取配置信息,而配置中心又在从配置源同步配置,所以这里就很自然的出现了一个读写安全的问题,好消息是Archaius已经解决了这个问题,Archaius是线程安全的,读写可以并发进行。
Eureka:服务注册发现框架 https://github.com/Netflix/eureka
Eureka在希腊语中用以表达发现某件事物、真相时的感叹词。
传说阿基米德在洗澡的时候发现,当他坐进浴盆里时有许多水溢出来,溢出来的水的体积正好应该等于他身体的体积,发现了不规则物体体积精确计算的方法,不禁高兴的从浴盆跳了出来,边跑边喊叫着“Eureka!”(引用自:维基百科)
言归正传,Eureka是一个基于RESTful,用来发现运行在AWS域(Region)中的中间层服务,同时具备负载均衡、故障恢复的服务注册与发现框架。
RESTful API或者更广泛的来说HTTP API,以及Thrift API的调用client至少需要知晓服务端的IP地址以及端口,对于传统的应用而言,这些信息基本是固定的,即便有更新也可以通过修改client的配置文件让client知晓更新后的信息。但是对于基于云端的微服务生态来说,这样做并不容易,下图简单的描述了这个问题,总的来说在微服务生态中:
服务实例的网络位置都是动态分配的。由于扩展、失败和升级,服务实例会经常动态改变,因此,客户端代码需要使用更加复杂的服务发现机制。
(引用自《Service Discovery in a Microservices Architecture》,作者:Chris Richardson,译者:DaoCloud Lychee Li)
Eureka1.x已经被Eureka2.x替代,因为1.x到2.x发生了重大改变,笔者就只介绍下Eureka2.x版本。最新的Eureka2.x版本采用fine grain subscription模式替代了pull based模式,可以提高更新速度,减少延迟。
Eureka分为服务端与客户端,客户端通过自注册模式(self-registration pattern)被服务端发现,客户端将自身的识别码与服务状态发送服务端,由服务端处理后完成服务注册操作。客户端除了发送注册信息外,还负责发送心跳、更新、销毁信号到服务端。服务端则通过心跳信号判断客户端服务的存活。
Eureka的服务端包含write server与read server集群,write server集群用于处理上面提到的客户端注册与注册信息维护,write server采用最终一致算法保证注册信息在各个节点中的一致性。read server读取write server中的注册信息,并将注册信息推送给Eureka客户端。read server如何知道将哪些注册信息推送给哪些客户端呢?就是通过最开始提到的fine grain subscription模式,客户端可以向read server发起订阅请求,订阅请求声明了客户端所需依赖的服务,当这些依赖服务有更新时,read server就负责将更新信息推送给订阅了的客户端。总结一下,微服务可以通过Eureka客户端向Eureka服务端的write server发送注册信号让自身可以被发现,微服务同时也可以通过Eureka客户端向Eureka服务端的read server发送订阅信息来获取依赖服务的更新信息。当然Eureka的服务端是一个集群,Eureka客户端还需要一个发现机制来发现集群中的服务端位置,服务端也需要一种机制来发现同伴位置,这里不做介绍,感兴趣的同学可查阅https://github.com/Netflix/eureka
Ribbon: IPC客户端框架 https://github.com/Netflix/ribbon
在单体应用中获取一种服务或者能力,只需要通过调用一个函数来实现,一般函数对象,调用者对象,数据都在同一个进程中,所以靠编译器或者解释器就可以实现调用。但是在微服务中,各个服务在不同的进程中,所以微服务间的交互需要通过进程间通讯(IPC)的方式解决。Netflix给出的方案就是Ribbon。Ribbon的基础功力总结来说是三多: 多协议(HTTP、TCP、 UDP);多格式(Avro、XML、JSON、Thrift、Google Protocol Buffers);多模式(同步模式、异步模式)。这些IPC客户端的基本的功能就不详细介绍了。当然Ribbon不止是一个IPC客户端框架,更重要的是Ribbon提供了客户端负载均衡,服务端的负载均衡非常常见,Ribbon在客户端实现了一个软件级别的负载均衡,用于从一个集群中选出服务端地址。Ribbon的客户端负载均衡包含三个组件:Rule、Ping、ServerList,其中Rule是负载均衡的逻辑组件,Ping是服务端存活情况的探测组件,在后台运行,ServerList用于维护服务端集群列表,这个列表可以是静态的也可以是动态的。Ribbon自带了多个负载均衡规则(Rule)可以通过配置进行选择,同时结合上面提到的Archaius配置管理中心可以实现配置的动态更新,不但可以更新负载均衡策略还可以更新ServerList,而且更新配置动态生效不需要重启客户端,这样在客户端就可以实现灵活的流量调整,所以Netflix开源的不单单是几个框架而是一套解决方案,对计算、网络以及存储的抽象化是这些特性的基础。
Karyon:服务端框架 https://github.com/Netflix/karyon
上面介绍了客户端框架Ribbon,再介绍了下Netflix给出的服务端框架Karyon。我们知道对于一个服务端框架而言,其重要的设计哲学就是"don't call me, I will call you",这样的设计将开发人员从大量有共性的逻辑中解救出来,可以专注于业务逻辑的实现。Karyon也践行了这一设计哲学,按照Netflix的说法Karyon是一个用于开发云端web service所需的最小系统(原文: blueprint),Karyon提供的特性包括:应用初始化、运行时诊断、配置管理、服务发现等等。对于一个web service而言需要在启动时获取并加载配置、初始化依赖等,Karyon通过应用初始化组件使得完成了这些初始化操作变得非常简单,比如Karyon可以在启动时自动从Archaius获取对应的配置并初始化。对于一个微服务生态中的web service而言,在自身启动后,需要可以被发现采用对外提供服务,为此Karyon集成了服务发现组件,开发人员则只需要提供一个名为eureka-client.properties的配置文件,包含服务名称、服务IP地址/端口、URL等配置,Karyon负责自动向Eureka发起服务注册。除此之外Karyon还有许多特性,比如服务健康检查、管理后台等,在这些特性的帮助下开发人员在服务端开始时所需做的就只剩下业务逻辑实现了。
Hystrix:断路器组件 https://github.com/Netflix/Hystrix
断路器是一种能在远程服务不可用时自动断开,并在远程服务恢复时自动闭合的开关。断路器对于微服务系统非常重要,因为微服务系统中存在着复杂的依赖关系,一旦某个服务发生延迟甚至出错如果不能对其进行隔离,应用本身也会处于被拖垮的危险,在高并发的环境下,由于雪崩效应的作用,应用资源将被耗尽造成应用瘫痪。
对于基于云端的微服务来说,可靠性并不是有保证的,事实上即使每个服务的可靠度达到99.999%在一个包含100+微服务的系统中也不能保证整个系统可靠度达到99.99%。所以断路器或者服务容错是非常必要的。Hystrix拥有多种容错策略,包括:阻止并发超出服务的最高限制,超过后拒绝请求并回退;当限流或者断路发生时提供多种回退方式进行保护;提供多种隔离策略比如bulkhead、swimlane、circuit breaker来保护应用不受某一依赖故障的影响。有了Hystrix,开发人员可以在需要容错的环节使用HystrixCommand或者HystrixObservableCommand对象来封装自己的调用就可以自动的获得容错能力,同时Hystrix还会为每个依赖维护一个线程池并做限流与回退,这样做使得应用的每个依赖互相隔离,当故障发生时限制了应用可以使用的资源数量(比如线程、队列等等),并通过回退策略来决定故障发生时返回何种response。
微服务架构还需要解决两个重要的挑战:
第一个挑战就是如何实现业务事务,保持多个服务的一致性。第二个挑战就是如何从多个服务中检索数据,实现查询。
(引用自《Service Discovery in a Microservices Architecture》,作者:Chris Richardson,译者:DaoCloud Lychee Li)
第一个挑战说明微服务应用在保持事务原子性上不再像单体应用那样简单,因为微服务之间是松散的。举个简单的例子,两个微服务都要需要一项数据(比如客户ID),但是这两个微服务由两个团队独立开发,使用的开发语言不一样,连使用的数据库类型都不一样,有些微服务使用了NoSQL的数据库。那么如何保持数据的一致性就是我们面临的第一个挑战。
第二个挑战引申自第一个挑战,我们如何从多个服务中查询数据?对于单体应用而言所有的数据都在一个数据库中,并且数据是结构化的(表就是数据的结构化表示),我们很容易从多个表中查询数据,但是在整个微服务系统中数据变得没有结构了,数据是分散的,当然我们可以通过API的方式从各个服务中获取数据并拼装成结构化数据,但这样做效率太低了,微服务将变成慢服务,有没有更好的方式呢?事实上我们可以参照单体应用的思路,单体应用将数据结构化,在微服务系统中我们可以将多个微服务结构化,而事件就是这个结构的“主键”。对于这两个挑战更详细的解决方式可以参考:Event-Driven Data Management for Microservices,作者Chris Richardson在文章中介绍了如何采用事件驱动的架构来应对挑战。
后话:
上面介绍的开源框架已经可以覆盖微服务实施过程中需要的大部分基础框架,当然还有一些框架也是不可或缺的,感兴趣的同学可以自己查阅,比如:
Zuul: 网关服务https://github.com/Netflix/zuul
Blitz4j:日志组件 https://github.com/Netflix/blitz4j
SimianArmy:猴子军团 https://github.com/Netflix/SimianArmy
这不是一个必须的组件但是猴子军团改变了我对于应用可靠性的认识,通常我们认为应用要可靠运行需要为应用创造一个稳定的运行环境,但事实上这样做并不能保证可靠性,故障或者灾难一般发生在我们难以预料的情况下,做好万全的准备事实上被证明是不可行的,所以对抗风险的办法是反过来欢迎风险,猴子军团可以在生产环境的制造不同种类的故障,评估在不正常状态下系统的应对能力发现弱点,为持续改进我们的应用提供数据支撑。打不死你的只会让你更强!
Servo: Java的Metrics组件 https://github.com/Netflix/servo