当前位置: 首页 > 科技观察

微服务必须具备的3个基本功能!

时间:2023-03-15 14:07:11 科技观察

我们在对微服务架构有了一个整体的了解,有了服务的前提之后,一个完整的微服务请求需要涉及到什么呢?这包括微服务框架的三个基本功能:发布和引用服务服务的注册和发现发布和引用远程通信服务首先,我们面临的第一个问题就是如何发布服务和引用服务。具体来说,这个服务的接口名称是什么,有什么参数,返回值是什么类型等等,通常是接口描述信息。常见的发布和引用方式包括:RESTfulAPI/DeclarativeRestfulAPIXMLIDL一般来说,无论使用哪种方式,都需要服务端定义并实现接口,例如:@exa(id="xxx")publicinterfacetestApi{@PostMapping(value="/soatest/{id}")StringgetResponse(@PathVariable(value="id")finalIntegerindex,@RequestParam(value="str")finalStringData);}}具体实现如下:publicclasstestApiImplimplementstestApi{@OverrideStringgetResponse(finalIntegerindex,finalStringData){return"ok";}}DeclarativeRestfulAPI,经常使用HTTP或HTTPS协议调用服务,性能相对较差。首先服务端定义并实现如上接口,然后服务提供者可以使用restEasy这样的框架通过servlet发布服务,服务消费者直接引用定义好的接口调用。另外还有一个类似feign的方法,就是服务器的发布依赖springmvc控制器,框架只根据客户端模板化的http请求调用。在这种情况下,需要与服务器控制器协商接口定义,以便客户端直接引用该接口发起调用。使用XML私有rpc协议的会选择xml配置方式描述接口,效率更高,如dubbo、motan等。同理,服务端如上定义并实现接口,服务端通过server.xml暴露文件接口。服务消费者是指通过client.xml调用的接口。但是这种方式对业务代码的侵入程度较高。当xml配置发生变化时,服务消费者和服务提供者都需要更新。IDLIDL是一种接口描述语言,常用于跨语言调用。最常用的IDL包括Thrift协议和gRpc协议。例如,gRpc协议使用Protobuf来定义接口。写好proto文件后,使用语言对应的protoc插件生成服务端和客户端对应的代码,然后直接使用即可。但是如果参数字段太多,proto文件就会显得很大,难以维护。而如果经常需要更改字段,比如删除字段,PB就无法做到向前兼容。一些提示无论哪种方式,都需要在接口发生变化时通知服务消费者。消费者对API的强依赖是无法避免的,接口变化导致的各种调用失败也很常见。所以如果有变化,尽量使用增加新接口的方法,或者为每个接口定义一个版本号。在使用上,大部分人对外选择Restful,对内选择Xml,跨语言IDL。一些问题服务在实际发布和引用中还存在很多问题,其中大部分问题都与配置信息有关。比如一个简单的接口调用超时配置,这个配置应该配置在服务层面还是接口层面?是放在服务提供者这边还是服务消费者那一边?在实践中,大多数服务消费者会忽略这些配置,所以服务提供者有必要提供一个默认的配置模板,相当于一个预定义的流程。每个服务消费者从服务提供者那里继承了预定义的配置后,还需要能够执行自定义配置覆盖。但是,比如一个服务有100个接口,每个接口都有自己的超时配置,而这个服务有100个消费者,当服务节点发生变化时,会发生100*100条注册中心消息Notification,这个比较可怕,可能引发网络风暴。服务注册与发现假设你发布了一个服务并部署在一台机器上,消费者如何才能找到你服务的地址呢?可能有人会说是DNS,但是DNS有很多缺陷:维护麻烦,更新延迟不能在客户端做负载均衡不能实现端口级的服务发现其实在分布式系统中,有一个很重要的角色叫做注册中心,就是用来解决这个问题的。使用注册中心寻址和调用的过程如下:服务启动时,向注册中心注册自己,并周期性发送心跳报告生存状态。客户端调用服务时订阅注册中心,将节点列表缓存到本地,然后与服务端建立连接(当然这里可以使用懒加载)。发起调用时,在本地缓存节点列表中,根据负载均衡算法选择服务器发起调用。当服务器节点发生变化时,注册中心可以感知并通知客户端。注册中心的实现主要需要考虑以下几个问题:自一致性和可用性注册方式存储结构服务健康监控状态变化通知一致性和可用性是一个老命题,即CAP(consistency,availability,partitionfaulttolerance).我们知道不可能同时满足CAP,所以有一个trade-off。常见的注册中心大致分为CP注册中心和AP注册中心。典型的CP注册中心有zookeeper、etcd、consul,它们牺牲可用性来保证一致性,使用zab协议或者raft协议来保证一致性。AP注册中心牺牲一致性来保证可用性。感觉只能列出eureka了。每个eurekaserver单独保存节点列表,可能存在不一致的情况。关注公众号Java技术栈,后台回复:cloud,可以获取我整理的SpringCloud系列教程,很全。从理论上讲,AP类型远比仅针对注册中心的CP类型更合适。可用性的要求远高于一致性。一致性只需要保证最终的一致性即可,可以通过各种容错策略来弥补不一致性。保证高可用的方式其实有很多,比如集群部署或者多IDC部署。Consul是多IDC部署保证可用性的典型例子。它使用wangossip来保持跨机房的状态同步。注册方式与注册中心交互的方式有两种,一种是在应用中集成SDK,另一种是通过其他方式在应用外间接与注册中心交互。SpringBoot系列教程已经整理出来了:https://github.com/javastacks/spring-boot-best-practice这应该是应用中最常见的方式了。客户端和服务端都集成了相关的SDK与注册中心进行交互。比如选择zookeeper作为注册中心,那么可以使用curatorsdk进行服务注册和发现。应用外consul提供了应用外注册的解决方案。consul代理或第三方Registrator可以监控服务状态,并负责服务提供者的注册或销毁。而ConsulTemplate则可以定时从注册中心拉取节点列表,刷新LB配置(比如通过Nginx的upstream),相当于完成了服务消费者的负载均衡。存储结构注册中心一般采用目录化的层次结构存储相关信息,一般分为服务-接口-节点信息。同时,登记中心一般会进行分组。分组的概念很广,可以按机房划分,也可以按环境划分。节点信息主要包括节点的地址(ip和端口号),以及一些节点的其他信息,如请求失败的重试次数、超时设置等。当然在很多情况下,接口层实际上可能会被去掉,因为考虑到接口数量多,节点过多会带来很多问题,比如前面提到的网络风暴。服务健康监测服务生命状态监测也是注册中心必备的功能。在zookeeper中,每个client都会和server保持一个长连接,并产生一个session。在session过期期间,client会定时向server发送心跳包检测链路是否正常,server会重新设置下一次session的过期时间,如果在session过期时间内没有检测到client的心跳包期间,它将被视为不可用并从节点列表中删除。状态变化通知注册中心具备服务健康检测能力后,还需要将状态变化通知客户端。在zookeeper中,可以通过listener-watcher的process方法获取服务变化。服务的远程通信如上,服务消费者已经正确引用了服务,发现了服务的地址,那么如何向这个地址发起请求呢?要解决服务之间的远程通信问题,我们需要考虑一些问题:网络I/O处理传输协议序列化方式网络I/O处理简单来说就是客户端如何处理请求?服务器如何处理请求?关注公众号Java技术栈,后台回复:面试,可以拿到我整理的Java网络编程系列的面试题和答案,很全。首先,从客户端来说,我们可以在从注册中心获取节点信息的时候创建连接,但是更多的时候,我们会选择在第一次发起请求的时候创建连接。另外,我们经常会为这个节点维护一个连接池,用于连接重用。如果是异步的,我们还需要对每个请求进行编号,维护一个请求池,以便在响应返回时找到对应的请求。当然,这不是必须的。很多框架都会帮我们做这些事情,比如rxNetty。从服务器端来说,处理请求的方式可以追溯到Unix的五种IO模型。我们可以直接使用Netty、MINA等网络框架来处理服务器请求,或者如果你很感兴趣,也可以自己实现一个通信框架。最常见的传输协议当然是直接使用Http协议。双方无需关注和理解协议内容,方便直接,但自然性能会有所打折扣。还有就是目前比较火的http2协议,它有很多优秀的特性,比如二进制数据,头部压缩,多路复用等。但从自身的实践来看,http2距离生产还有很长的路要走。最简单的例子就是升级到http2后,所有的header名称都会变成小写,并且不区分大小写。这时候,就会出现相容性问题。当然,如果追求更高效可控的传输,可以自定义一个私有协议,基于tcp传输。私有协议的定制需要通信双方了解其特性,设计时还需要注意预留扩展字段和处理粘包、分包等问题。序列化方式往往需要在发送端进行编码,在网络传输前后在服务端进行解码,主要是为了减少网络传输时的数据传输量。常用的序列化方式有文本类型,如XML/JSON,二进制类型,如Protobuf/Thrift等。在序列化方面的考虑,一个是性能。Protobuf的压缩大小和压缩速度都比JSON快很多,性能也更好。二是兼容性。相对而言,JSON具有更强的前向和后向兼容性,可以用于界面变化频繁的场景。这里还是需要强调一下,使用每一个序列化,都需要了解它的特点,把握界面变化时的边界。比如jackson的FAIL_ON_UNKNOW_PROPERTIES属性,kryo的CompatibleFieldSerializer,jdk序列化会严格比较serialVersionUID等等。