后台开发那些常用技术再次小结(二):Web服务层

  Web服务部分应当算是整个服务端开发中最重要、最复杂、同时也最具可玩性的部分了。业务逻辑的处理主要分布在Web服务层,这样一方面可以让其上层展示层不必关心业务逻辑,只专心负责展示和用户输入部分(也让其具有强大的伸缩性),而Web服务层本身也可以通过功能分割(增加副本、功能分割和数据分片是实现伸缩性的三大利器),以及单个服务自身的伸缩设计,最终构建一个功能强大的Web服务层。
  Web服务层采用一定的设计模式为上层提供调用接口,同时这些年来,跟随者移动互联网和与计算的大潮,各大厂商也喜欢将自己的服务通过接口的方式免费或者收费的形式提供给第三方使用,这也就是所谓的Web服务和前端应用平行部署的现象,在这种环境下的服务接口设计就显得会比以往更加的重要,当时那种裸socket之上自定义数据帧的时代显然是跟不上了。尤其这些年来,关于RESTful接口设计风格可谓是如日中天,无论是Google、Amazon这些互联网大佬,还是几个人组成的互联网创业公司,都号称自己开放的服务是符合RESTful风格的接口,但是正如我在V2ex论坛上看到大家所争论(《 RESTful 有用吗? HTTP 有 GET POST 就足够了?》)的一样:绝对纯正的RESTful风格接口的设计和实现在当下是不可能实现的,大家开放出来的接口要么就是根据实践情况有所妥协,要么就是封装了一个简易外壳的“伪RESTful接口”而已。
  这里看到一本《服务设计模式》,虽然后面的内容看的不是很明白,但是前面对于服务设计的总结还是不错的,在此进行归纳总结!归咎来说,Web服务主要是为集成不同的系统提供相应地方法,并通过HTTP来输出可重用业务功能,他们或者将HTTP作为一种简单的信息传输工具来承载数据传输(比如SOAP/WSDL服务),或者将HTTP作为一种完整的应用控制协议,将HTTP协议的内容定义服务的各种行为和语义(RESTful服务),前者被定义为以功能为中心的服务,后者被定义为以资源为中心的服务。
  以功能为中心的服务:该种风格的服务是指能够调用远程机器上的功能或者对象方法,而无需知道这些功能或者对象是如果实现的,因此这种框架技术需要处理跨编程语言、跨CPU构架、跨运行时环境的问题,同时对调用参数的约定、变量转换、错误处理还必须进行明确的规范。以功能为中心的服务现在基本是SOAP一枝独秀,其基本使用XML作为服务描述和消息编码方式,通过HTTP在客户端和服务端之间传递请求和响应。
  以功能为中心的服务基本不会用在动态语言中,因为这些动态语言和SOAP服务集成会比较困难,基本都是Java、C/C++这类静态语言。此外,由于SOAP请求是通过XML进行发送的,请求参数和方法名都通过XML封装了,URL里面没有远程调用需要的全部信息,所以对于HTTP层面基于URL的缓存就没法实现,使得伸缩性变差。
  以资源为中心的服务:以资源为中心的服务作为新一代的服务设计构架,已经成为Web应用集成事实上的标准,这种设计就是以资源为中心代表一类对象,在对象上只能执行有限的操作(比如创建、删除、更新、获取资源等),将和资源(对象)之间的交互标准化了。跟上面对应,利用HTTP缓存是可伸缩RESTful Web服务的一项重要技术,当设计的方法严格准守HTTP方法语义约束的生活,重要的GET方法的响应就应该是可缓存的,如果确保所有GET方法是只读且不会导致状态变化和数据更新,那么就可以安全的缓存该HTTP GET请求。当然,具体的业务类型可能也不允许这种形式的缓存。

一、Web服务API设计风格

  Web服务API设计风格包含:RPC API、消息API、资源API三种类型。

1.1 RPC API

  RPC API的核心概念是远程过程调用的思想,通过RPC客户端首先向远程服务器发进程发送消息,再阻塞自己,等待接收响应,请求的消息可以标识要执行的过程,同时也包括一组固定的参数映射作为远程过程的参数;远程服务收到消息时候会进行检查,并根据消息中的过程名称及其参数调用相关过程(服务);如果需要,服务器将对应的响应结果返回给客户端,客户端提取结果并继续执行。
  这是我们所最熟悉的服务设计风格,因为其历史悠久并被广泛使用,相对容易理解而且使用简单(有大量现成的服务框架可用)。这种风格API通常使用XML定义数据类型和消息,使用WSDL(Web Service Description Language)进行服务描述,用WS-Policy/WS-Security等规范来定义对客户端的授权、加密等规则;当然也可以使用非XML的方法,比如JSON-RPC这类的规范来实现。
  考虑因素
  (1) 因为RPC和本地方法很相像,所以通常会像声明本地方法签名的方式来创建服务签名,这样的话客户端必须严格按照参数的顺序、类型来发送参数,导致使用不灵活。所以通常建议将服务签名设计为单个参数类型,或者多个可选参数类型的服务。
  (2) RPC API的特点是位置透明性,服务的真实位置应该是完全隐藏的,即客户端开发者调用本地过程和调用远程过程的代码都应该大致相同,这样服务就可以按照需要进行自由移动,然后通过其他方法通知客户端服务的具体位置,比如存储在配置文件或者数据库中供客户端查询。
  (3) 除了默认的请求-响应模式,RPC API还可以使用请求-确认的交互模式。这种情况下服务端收到请求后会将消息转发到一个异步后台进程,并向客户端返回一个简单确认。通常消息首先存储在消息队列或者固化到数据库中,通过这种消息接收和消息处理时间分离,系统就可以更好的处理负荷峰值,控制消息处理速率。
  (4) 使用RPC API的客户端在发送消息后,也可以不需要阻塞,而采用异步响应处理器的方式提高效率。
  (5) RPC API通常使用XML或者JSON进行编码,也可以采用二进制数据编码,从而压缩网络传输、节省带宽、减少延迟,不过副作用就是二进制编码会增加客户端、服务器端的计算量。

1.2 消息API

  RPC API的服务端和客户端是强耦合的,无论调用名、调用签名、数据结构发生变化,都需要两方面的协调完成。
  基于消息API(文档API)的服务,会在一个给定的URI上收到一个或者多个自描述的消息,消息体包含主要的数据,同时还可以附加一些标头用于身份验证、请求有效截止日期等信息。消息API通常手法表转化的消息格式(比如SOAP)。当客户端向一个指定URI发送消息后,客户端可以选择阻塞来等待响应,当消息到达服务器时候Web服务执行反序列化处理,检查消息并选择适当的处理器来处理请求,通过将客户端和真正的处理器(远程过程)进行隔离,Web服务提供了一个间接层的作用。
  这种模式意在情调是基于消息设计的,API提供一个接收断点,而Web服务则扮演者分发器的角色,客户端的消息通常包括命令消息、事件消息、文档消息类型。
  考虑因素
  (1) 消息API通常使用WSDL描述符,依赖框架辅助生成客户端服务连接器代码。具体的消息再通过某种特定的传输协议(比如HTTP)进行绑定,并在指定URI上输出其内容。
  (2) 下次API的服务通常采用请求-确认模式,而不是请求-响应模式。

1.3 资源API

  这种方式是对所有资源(比如文本文件、媒体文件、数据库表中的数据、业务过程等)分配一个URI,使用HTTP作为一种完整的应用协议以定义标准的服务行为,采用标准化的媒体类型和可能获得的状态码来交换信息。这样资源API的服务通过使用请求的URI、客户端发起的HTTP方法、一同提交来的请求信息及媒体类型等既可以判断客户端的意图,可以将这个规则表达为表述性状态转移(Representational State Transfer, REST)原则,因为客户端和服务端之间交换消息的生活,资源的状态发生了转变。
  资源API中的URI是一个逻辑意义上的地址,通过向这个URI发送请求就可以调用对应服务或获得相应地资源,而实际的资源和URI可能存在一对多的关系,但是一个URI应该只能用于引用一个逻辑资源。资源API通过如下HTTP标准方法来访问资源:

  • PUT:用于创建或者更新资源;
  • GET:用于检索资源的表述;
  • DELETEL:用于删除一个资源;
  • POST:该行为多样性,作为以上相对而言非标准行为,同时当服务器禁用了PUT、DELETE或者防火墙阻塞了这两种方法时候,可以用POST作为一种替代方法(隧道化执行)。很多REST的倡导者不支持使用POST隧道化的行为,因为这模糊了请求的目的,而且实现上也不利于对结果进行缓存;
  • OPTIONS:用于发现目标URI所支持的HTTP方法;
  • HEAD:用于获取URI上交换的与媒体类型有关的元数据,类似于GET只是不返回资源的表述。

  上面最常用的PUT、GET、DELETE基本可以映射为CURD(Create、Update、Retrieve、Delete)操作,而POST可以用于其他无法映射的行为。由于HTTP协议将这些行为预先定义好了,所以客户端不必学习特殊的API,而只需要知道每个URI上面可以使用的方法,以及每种方法使用时机就可以了,服务开发人员会按照标准来实现对应的功能。
  HTTP规范定义了那些方法是安全的(Safe)和幂等(Idempotent)的,如果操作不会产生副作用就被认为是安全的,其不会触发创建、更新、删除操作,GET、HEAD、OPTIONS方法可以实现为安全的操作;幂等性指的无论调用同一个过程多少次,只要参数相同,那么就应该返回相同的结果(出错、超时等情况不考虑),GET、HEAD、PUT、DELETE、OPTIONS都是幂等的,但是POST不是幂等的。POST方法也可以实现为幂等的,比如在客户端请求中插入一个唯一的标识符,服务在执行它的时候进行逻辑检查,如果已经被处理则拒绝这个请求。
  资源URI会利用标准HTTP状态码,向客户端提供处理结果,通常可以返回最小量的数据表示结果因而优化了网络利用率。不过有时候响应码也会有含糊不清的意义,这也是RESTful在实现中缺陷之所在,否则那些号称符合RESTful风格的开放接口都不会另外列出一大串HTTP错误码对应的实际错误描述了。
  考虑因素
  (1) 使用不同类型的客户端,这时候资源API是最好的选择,因为HTTP协议的广泛流行使得资源API可以得到最广泛的支持。
  (2) 可寻址能力,因为资源API的设计上,URI通常是有意义的信息(比如账号ID),那么这些信息很可能被恶意挖掘和使用。可以使用UUID映射将这些信息进行隐藏,或者采用身份验证和逻辑授权机制,确认调用者身份、限制每个调用者可执行的操作,以起到对应保护的作用。
  (3) 客户端一般没有代码自动生成能力,即无法自动产生服务的客户端连接器,客户端必须采用标准的HTTP进行请求。
  (4) 面向资源的服务通常使用请求-响应模式,但也可以使用请求-确认交互模式。服务可以将请求转发到异步后台进程,而向客户端立即返回一个确认消息(HTTP状态202)。
  (5) 资源API通常会为同一个逻辑资源提供多种表现形式,不必为每种表现形式使用不同的URI,这通常使用媒体类型协商来满足客户端的偏好的。
  (6) 资源API风格可以天生有效利用针对HTTP专门设计的缓存技术(比如反向代理缓存),以减小队原始服务器的压力,资源API尤其适合读操作较多的场景。

二、客户端和服务端之间的交互风格

  客户端和Web服务之间基本通信模式是点对点的链接,在连接建立后通常在多次交换数据中采用同一个连接,这样有助于最小化建立和释放连接所带来的延迟。但是在实际上,网络流量在传输的过程中总要通过各种中介,比如防火墙、反向代理等,这些中介一方面可以实现安全功能,过滤或者阻止某些请求,实现负载均衡及周边缓存的功能。

2.1 请求-响应模式

  该模式下客户端发给服务端的请求,会被立即执行处理,并通过同一个客户端链接返回处理结果,是客户端和服务端最常见、最易于理解的交互模式。这种客户端和服务端之间的交互是同步的,会按照严格的顺序发生,通常客户端提交请求后不能继续做其他的操作,直到请求的服务提供了响应,所以要求这种服务没有或者尽小的延迟。
  考虑因素
  (1) 请求-响应模式是高时间耦合度的交互,因为客户端的请求都需要立即得到处理,所以大量并发的请求可能会耗尽系统的处理能力。服务架构师必须理解服务的典型工作负载,并依据期望的负载来调整服务器、数据库、网络资源的容量,包括“垂直扩展”和“水平扩展”提高性能。通过将服务有请求-响应模式修改为请求-确认-轮训或者请求-确认-回调的模式,在时间上将接收请求、处理请求、发送响应进行分离,前者让客户端空闲时候轮训响应,后者要求客户端提供自己的服务以便接收回调消息,可以缓解服务器峰值压力的风险,但是实现和调试的复杂度较高。
  (2) 默认情况下,请求-响应服务的客户端会阻塞以等待响应,通过异步响应处理器可以改善客户端的性能。
  (3) 请求-响应在客户端和服务端之间可以插入中介者,比如代理服务器可以缓存查询结果,防火墙可以阻止或者过滤网络流量,检查证书等功能。

2.2 请求-确认模式

  请求-确认模式是可以使Web服务免受负载峰值影响,虽然通过异步响应处理器器可以缓解这个问题,但是在总体处理能力有限的情况下,如果请求的处理时间过长,那么客户端连接就会超时,这种情况通常会导致丢失响应。
  传统上可以采用网络可寻址的消息队列技术(面向消息的中间件),这样无论目标系统的运行状况如何,客户端都可以随时将消息发送给远程系统,即使无法连接远程队列基础构架也可以将消息先保存在本地,然后不断尝试发送直至成功。消息到达远程队列后会慢慢发送到目标系统,中间可以通过节流或控制处理请求速率保护远程系统。
  简单的队列具有发射后不管的特点,服务会接收请求、处理请求,但是不会发送响应,客户端无法感知服务是否被接收、是否被处理、处理的结果如何。解决的方法是对请求生成特定的标识符或者URI以作为唯一性的键值,将来所有参与会话的各方都可以引用这个键值,以形成一个特定请求的上下文处理逻辑。
  考虑因素
  请求-确认模式的难点是提供更新或者最终处理结果,通常实现的方法有:
  (1) 轮训:该模式实现简单,客户端定期轮训另外一个Web服务以获取更新信息或者最终处理结果。要实现这个功能客户端必须先得到一些轮训的先决条件,资源API通常返回客户端轮序的URI地址,而RPC API和消息API通常提供查询的标识符。
  请求-确认-轮序的缺点是客户端效率不高,如果轮训频率不高那么得到更新、处理结果就会有明显的延时滞后,如果轮序太过频繁也会对Web服务器带来负载,浪费网络流量。
  (2) 回调和转发:这种模式下客户端不会轮训另外一个服务获取结果,而由请求处理器主动将信息推送回客户端或者其他参与者(请求-确认-转发模式),此时客户端和服务器就互相交换角色了。这种模式下Web服务端必须获得一个回调服务列表,这可以由资源API客户端在请求中包含回调URI,或者RPC API和消息API通过WS-Addressing标识头提供,也可以是通过某种标识在数据库中查询回调信息。
  这种模式实现起来比轮训更具挑战性,但是如果原始客户端不能或者不愿意提供一个可以公开寻址的回调服务,那么该种方式就不能使用。

2.3 媒体类型协商模式

  比如在使用资源API服务的不同客户端通常可以有不同的媒体偏好,比如有的喜欢XML而有的客户端好JSON。指明客户端偏好的方法很多,比如可以将媒体类型作为最终URI的一部分或者文件资源的扩展名,这种方式简单直观而且可以被浏览器等客户端支持,但是:客户端必须知道如何对每个资源构造正确的URI,显然增加了客户端和服务端的耦合和复杂度。
  解决的最好方法是使用标准HTTP协议的进行内容协商,客户端可以在HTTP请求头中指定一种或者多种媒体类型首选项,那么服务端可以参考按照预设的格式生成响应。实现的方法有:
  (1) 服务器驱动的协商是最常用的一种协商方式,客户端通过Accept Request标头来提供自己的媒体偏好,可以一次提供多个偏好,也可以指明媒体类型的优先级,没有提供的话服务端可以默认选择一个,常见的媒体类型比如:application/xml、application/json、text/plain、text/html等,这些流行的类型会被自动序列化反序列化,而自定义类型可以需要自己编写对应的序列化器了。
  (2) 客户端驱动的协商方式中,在客户端在发送请求的时候还是通过HTTP的Accept Request标头来携带它的媒体偏好,Web服务端收到请求后在响应中提供一组客户端可以考虑使用的URI,然后客户端根据这个响应来选择特定的URI发起真正的资源请求。
  考虑因素
  (1) 服务器与客户端驱动的协商中,当服务拥有者期望为客户端提供选择,同时又需要维持对类型选择过程的控制时候,采用服务器驱动的协商比较合适,这有助于简化客户端逻辑,减少往返协商的流量,只是此时如果客户端没有提供明确偏好时,服务器的响应就可能不合适客户端;当期望客户端可以更多控制接收到的类型可以选择客户端驱动的协商,缺点是客户端必须发起两次请求-响应的数据交换,依次用于查询服务地址列表,另一次用于获得最终响应。
  两种方法比较而言,服务端驱动协商因为是同一个URI返回多种类型的资源,不利于中介缓存,而客户端驱动的协商可以最好的利用中介缓存,因为URI和特定的媒体资源存在一对一的关系。
  (2) 请求处理器中的代码应该可能会被复制,这时候应该考虑使用设计模式中的命令模式,可以降低复制代码维护的痛点。

2.4 链接服务

  通过Web服务可以将其他的服务信息输出给客户端,通常只需要发布几个根Web服务的地址作为最初的访问点,此后在每个响应中包含相关服务的地址信息,客户端解析响应之后就可以发现后续服务URI,那么客户端根据服务列表有选择的使用其他服务了。该方式的优点有:
  (1) 响应可以只是提供一组对最近请求有上下文意义的服务地址,对客户端完成正确的工作流转换具有指导意义;
  (2) 由于服务的地址是服务端返回的,可以确保这些地址是有效的,如果依赖客户端构造服务地址通常会出现各种各样的问题;
  (3) 增加、减少、修改服务变得更加容易,只需要在响应中做出相应修改就可以了,客户端需要修改识别新的服务,对于旧的不可用服务服务端返回404就可以了;
  考虑因素
  (1) 这种模式主要同上述的资源API一起使用;
  (2) 这种服务容易遭受中间人攻击,因为中间人可以截获响应或者构造响应指向一个恶意服务。通常需要采用TLS安全传输,也可以采用数字签名保护消息不被篡改。

本文完!

参考