- Service Mesh实战:用Istio软负载实现服务网格
- 周遥
- 538字
- 2020-08-27 21:03:20
1.3 服务化时期
1.3.1 应用到服务
重复建设在软件工程思想中向来就是不可取的,工程师们设想将共同的功能组件抽象出来,形成独立的“应用”,不过这里的关键问题就是这些“应用”没有直观上的入口,因此用传统的思想来定义并不妥。为此工程师们想出了“服务(Service)”一词来描述这种功能性的组件,它们为应用提供通用功能性的支撑,“服务”于具体应用。
如今“服务”这个词在软件工程中已经泛化,可以说,凡是向其他组件提供“支撑”功能的系统都可以称为服务,甚至还出现“软件即服务”(SaaS)这样的概念。在这种概念下,软件不再是整体打包一次性卖给消费者,而是消费者在使用时按需获取,而且服务还会不定期升级,消费者无须感知,并可以按使用时长或者资源的消耗率来计算费用。
“服务”思想其实就是把其他应用当成了消费者,为其提供特定功能。上述应用架构中的“登录、采集用户基本信息”等功能可以抽象成一个名为“用户中心”的服务组件。以此类推,还可以继续抽象出“消息中心”“索引搜索”“文件存储”“缓存系统”等通用服务组件,总的原则就是尽量提取公共的部分以避免重复的开发工作。
之后,功能的开发也即服务的开发了。
1.3.2 远程调用
服务拆分后,就像天上的繁星一样分散到不同网络的各角落了,那么现在的问题是如何互相访问呢?前面说过了,服务与应用不同,没有可视化的页面可以访问,而且调用者是计算机程序而不是人类用户,工程师们需要一个程序之间的通信协议,就好像下面的对话一样明了方便。
“我需要用户A的记录,请给我它的结构及数据”
“好,这是A的记录的结果及数据,请拿好”
“远程过程调用(Remote Procedure Call,简称RPC)”协议应运而生,其作用就是让应用(服务)之间的程序调用变得像本地一样简单,这种技术完全屏蔽了各种网络拓扑的复杂性,只要知道对方的“地址”就可以发起调用。
RPC在Java生态中,最早出现在1.1版本中的RMI(Remote Method Invocation)中。Oracle后来又提出的EJB那一套,基于JAX-RPC接口——JAX-RPC基于SOAP(简单对象访问协议),调用地址都是由“类目服务器(Name Server)”来注册管理的。
在这种方案下,访问某个远程服务需通过以下类似代码:
1: public class HelloClient { 2: public static void main(String args[]){ 3: try { 4: //在RMI服务注册表中查找名称为RHello的对象,并调用其上的方法 5: IHello rhello =(IHello)Naming.lookup(〝rmi://localhost:8888/RHello〝); 6: System.out.println(rhello.helloWorld()); 7: System.out.println(rhello.sayHelloToSomeBody(〝熔岩〝)); 8: } catch(NotBoundException e){ 9: e.printStackTrace(); 10: } catch(MalformedURLException e){ 11: e.printStackTrace(); 12: } catch(RemoteException e){ 13: e.printStackTrace(); 14: } 15: } 16: }
可以看到上述代码中有一个URI,这就是该服务的地址,消费端通过它可以发起访问以获得数据。
1.3.3 虚拟IP地址
可不要认为RPC就只是调用。这里有个前提,就是服务端得把自己的地址注册上去,但是在集群环境下有多台机器,如何处理呢?这些地址URI又怎么告诉消费方呢?另外,当服务不可用时,谁又去把这些注册信息拿掉呢?
这里工程师们想到的解决方案就是前面提到的负载均衡设备概念中的“虚拟IP地址(简称VIP)”,只需要在注册的时候填写VIP,那么在调用的时候,请求经过负载均衡设备,其就会自动地将流量均分到下属已经注册的机器中,并且在应用上下线的时候,会自动对下发的机器列表进行调整。
在增加了RPC功能后,工程师们终于可以将公共的服务单独部署,现在整个架构如图1.5所示。
图1.5 “服务化后的部署架构”
1.3.4 复杂的调用关系
历史的车轮继续向前,业务量再一次发生了巨大的增长,请求不断涌进来,新的应用不断增加,这使得公共服务也变得越来越多,整个调用网由最开始的十几条线变成成百上千条甚至上万条,甚至到了后期,已经没人能弄清楚到底有多少条调用关系了。
图1.6就是某企业的全局调用关系,可以看到,这里面的调用链错综复杂。
图1.6 “某公司的服务调用关系”
1.3.5 服务治理
复杂的服务订阅关系必须要很好地维护起来,否则在出现问题的时候可能完全不知从何入手,所以这个时候,一种叫“服务治理(Service Governance)”的软负载产品出现了。它的基础工作跟注册中心一样是维护一个订阅关系,不过在订阅关系的数据之上,还提供了更多高级功能,比如:根据负载情况选择最优服务节点、多版本链路支持、订阅关系鉴权限流降级以及灰度发布等。
经典的RPC框架Dubbo就有一个专门的服务治理子系统,其底层采用ZooKeeper作为数据存储,如今经常提到的Spring Cloud则是采用名为Eureka的自研产品。
Dubbo的“服务治理”采用的是ZooKeeper,由于其自身数据结构的设计,其服务关系存储采用的是树状结构,如图1.7所示:
图1.7 “Dubbo在ZooKeeper中的数据存储结构”
这种存储结构的优点在于可以很容易地监听数据变化,例如需要监听“服务A”对应的“提供者”变化情况时,只需要将监听点挂载到服务A的“提供者”父节点即可。同理,如果需要同时监听“提供者”与“消费者”,只需要将监听挂载到“服务”节点即可。然而不足也是很明显的,例如需要进行关联查询时,树状的层次结构就不那么方便了。比如需要查询机器M1订阅的所有服务,这种情况下只能逐一遍历,其时间复杂度为O(n),其中n为服务数量,并不高效,再加上ZooKeeper强一致的设计,从性能与功能上来讲,我个人认为都不适合作为“服务发现”来使用。
现今,工程师们普遍倾向使用etcd来实现“注册中心”,其特点是采用KV存储,对于结构性查询,更加轻量、易运维,优势更佳。
订阅关系可不仅在RPC中,任何链路,只要存在服务一说,都会涉及这个概念,比如著名的开源消息框架RocketMQ,也会使用一个目录服务器(官方名称是nameserver)来维护Topic与Broker之间的映射关系。
服务治理维护的数据,从根本上说就是“提供者”与“消费者”之前的映射关系,例如有3台机器在提供服务A,则标记为Aprovider= {M1, M2, M3},同时有2个消费者在调用A的服务,则记为Asubscriber= {N1, N2},可以看见这两条数据均为矢量数据,因此如何较好地结构化存储,是链路关系维护的关键。
当然可以尝试将其存储在数据库中,但这里的问题是,订阅关系通常是实时变化的,而且需要通知功能。比如提供A服务的M1这台机器宕机,消费者Asubscriber需要监听到这种变化,否则会将请求路由到一个已经宕机或者下线的服务上,导致错误的请求。更复杂的情况是,可能一个关系变化会连锁式地影响到其他链路。
例如B服务其实就是A服务的一个消费者N3,而B服务本身又有消费者,那么当M1出现故障时,Aprovider变成{M2, M3},这时B的流量入口从之前的三台下降到了两台,B服务的链路能力是有下降的,即整条链路的吞吐量都受到了影响。如何及时、直观地反映问题,到如今也是个棘手的问题,这点更是弹性调度不可或缺的基础。
可以说软负载中的“服务治理”发展到今天已经远不只“服务注册”那么简单了。
1.3.6 旁路负载
由于“服务治理”系统的存在,工程师们再也不用为应用之间复杂的调用关系而担惊受怕了,分布式得以空前发展。
因为“服务治理”通常都提供健康检测功能,已经能够很好地维护服务对应的机器列表,所以之前讲到的VIP(虚拟IP地址)便变得不那么必需了。这个时候工程师们想出了一种“旁路负载”(亦称透明负载)的软负载架构,它的特点是并不直接通过代理流量来分发负载流量,而是在RPC客户端中直接埋入负载及其他链路逻辑,这样就省下了网关代理这层,但是由于对外需要统一暴露接口,因此对外的网关仍然需要保留。
当然,对外的网关已经不是简单的流量负载均衡,像“接口版本管理、攻击防御、请求重定向”等功能也被加入了,这个时候它有了一个更贴切的名字,叫“API网关”。像读者熟知的Spring Cloud框架,其Zuul组件充当的便是“API网关”的角色。
最终系统架构便变成了图1.8的样子:
图1.8 “采取旁路负载后的架构”
如此一来,系统内部的访问阻碍已经减少到很少了,同时随着Docker容器化及DevOps思想的迅速崛起,服务的粒度可以拆分得比以前更细,让需求开发更加独立、互不影响,这就是之后出现的微服务一说。