RESTful规范

1. 什么是REST

REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征)性状态转移。 它首次出现在2000年Roy Fielding的博士论文中,Roy Fielding是HTTP规范的主要编写者之一。 他在论文中提到:”我这篇文章的写作目的,就是想在符合架构原理的前提下,理解和评估以网络为基础的应用软件的架构设计,得到一个功能强、性能好、适宜通信的架构。REST指的是一组架构约束条件和原则。” 如果一个架构符合REST的约束条件和原则,我们就称它为RESTful架构。

REST本身并没有创造新的技术、组件或服务,而隐藏在RESTful背后的理念就是使用Web的现有特征和能力, 更好地使用现有Web标准中的一些准则和约束。虽然REST本身受Web技术的影响很深, 但是理论上REST架构风格并不是绑定在HTTP上,只不过目前HTTP是唯一与REST相关的实例。 所以我们这里描述的REST也是通过HTTP实现的REST。

2. 理解RESTful

要理解RESTful架构,需要理解Representational State Transfer这个词组到底是什么意思,它的每一个词都有些什么涵义。

下面我们结合REST原则,围绕资源展开讨论,从资源的定义、获取、表述、关联、状态变迁等角度,列举一些关键概念并加以解释。

  • 资源与URI
  • 统一资源接口
  • 资源的表述
  • 资源的链接
  • 状态的转移

2. 1 资源与URI

REST全称是表述性状态转移,那究竟指的是什么的表述? 其实指的就是资源。任何事物,只要有被引用到的必要,它就是一个资源。资源可以是实体(例如手机号码),也可以只是一个抽象概念(例如价值) 。下面是一些资源的例子:

  • 某用户的手机号码
  • 某用户的个人信息
  • 最多用户订购的GPRS套餐
  • 两个产品之间的依赖关系
  • 某用户可以办理的优惠套餐
  • 某手机号码的潜在价值

要让一个资源可以被识别,需要有个唯一标识,在Web中这个唯一标识就是URI(Uniform Resource Identifier)。

URI既可以看成是资源的地址,也可以看成是资源的名称。如果某些信息没有使用URI来表示,那它就不能算是一个资源, 只能算是资源的一些信息而已。URI的设计应该遵循可寻址性原则,具有自描述性,需要在形式上给人以直觉上的关联。这里以github网站为例,给出一些还算不错的URI:

  • https://github.com/git
  • https://github.com/git/git
  • https://github.com/git/git/blob/master/block-sha1/sha1.h
  • https://github.com/git/git/commit/e3af72cdafab5993d18fae056f87e1d675913d08
  • https://github.com/git/git/pulls
  • https://github.com/git/git/pulls?state=closed
  • https://github.com/git/git/compare/master…next

下面让我们来看看URI设计上的一些技巧:

  • 使用_或-来让URI可读性更好

曾经Web上的URI都是冰冷的数字或者无意义的字符串,但现在越来越多的网站使用_或-来分隔一些单词,让URI看上去更为人性化。 例如国内比较出名的开源中国社区,它上面的新闻地址就采用这种风格, 如http://www.oschina.net/news/38119/oschina-translate-reward-plan。

  • 使用/来表示资源的层级关系

例如上述/git/git/commit/e3af72cdafab5993d18fae056f87e1d675913d08就表示了一个多级的资源, 指的是git用户的git项目的某次提交记录,又例如/orders/2012/10可以用来表示2012年10月的订单记录。

  • 使用?用来过滤资源

很多人只是把?简单的当做是参数的传递,很容易造成URI过于复杂、难以理解。可以把?用于对资源的过滤, 例如/git/git/pulls用来表示git项目的所有推入请求,而/pulls?state=closed用来表示git项目中已经关闭的推入请求, 这种URL通常对应的是一些特定条件的查询结果或算法运算结果。

  • ,或;可以用来表示同级资源的关系

有时候我们需要表示同级资源的关系时,可以使用,或;来进行分割。例如哪天github可以比较某个文件在随意两次提交记录之间的差异,或许可以使用/git/git /block-sha1/sha1.h/compare/e3af72cdafab5993d18fae056f87e1d675913d08;bd63e61bdf38e872d5215c07b264dcc16e4febca作为URI。 不过,现在github是使用…来做这个事情的,例如/git/git/compare/master…next。

2. 2 统一资源接口

RESTful架构应该遵循统一接口原则,统一接口包含了一组受限的预定义的操作,不论什么样的资源,都是通过使用相同的接口进行资源的访问。接口应该使用标准的HTTP方法如GET,PUT和POST,并遵循这些方法的语义。

如果按照HTTP方法的语义来暴露资源,那么接口将会拥有安全性和幂等性的特性,例如GET和HEAD请求都是安全的, 无论请求多少次,都不会改变服务器状态。而GET、HEAD、PUT和DELETE请求都是幂等的,无论对资源操作多少次, 结果总是一样的,后面的请求并不会产生比第一次更多的影响。

下面列出了GET,DELETE,PUT和POST的典型用法:

GET

  • 安全且幂等
  • 获取表示
  • 变更时获取表示(缓存)
  • 200(OK) – 表示已在响应中发出
  • 204(无内容) – 资源有空表示
  • 301(Moved Permanently) – 资源的URI已被更新
  • 303(See Other) – 其他(如,负载均衡)
  • 304(not modified)- 资源未更改(缓存)
  • 400 (bad request)- 指代坏请求(如,参数错误)
  • 404 (not found)- 资源不存在
  • 406 (not acceptable)- 服务端不支持所需表示
  • 500 (internal server error)- 通用错误响应
  • 503 (Service Unavailable)- 服务端当前无法处理请求

POST

  • 不安全且不幂等
  • 使用服务端管理的(自动产生)的实例号创建资源
  • 创建子资源
  • 部分更新资源
  • 如果没有被修改,则不过更新资源(乐观锁)
  • 200(OK)- 如果现有资源已被更改
  • 201(created)- 如果新资源被创建
  • 202(accepted)- 已接受处理请求但尚未完成(异步处理)
  • 301(Moved Permanently)- 资源的URI被更新
  • 303(See Other)- 其他(如,负载均衡)
  • 400(bad request)- 指代坏请求
  • 404 (not found)- 资源不存在
  • 406 (not acceptable)- 服务端不支持所需表示
  • 409 (conflict)- 通用冲突
  • 412 (Precondition Failed)- 前置条件失败(如执行条件更新时的冲突)
  • 415 (unsupported media type)- 接受到的表示不受支持
  • 500 (internal server error)- 通用错误响应
  • 503 (Service Unavailable)- 服务当前无法处理请求

PUT

  • 不安全但幂等
  • 用客户端管理的实例号创建一个资源
  • 通过替换的方式更新资源
  • 如果未被修改,则更新资源(乐观锁)
  • 200 (OK)- 如果已存在资源被更改
  • 201 (created)- 如果新资源被创建
  • 301(Moved Permanently)- 资源的URI已更改
  • 303 (See Other)- 其他(如,负载均衡)
  • 400 (bad request)- 指代坏请求
  • 404 (not found)- 资源不存在
  • 406 (not acceptable)- 服务端不支持所需表示
  • 409 (conflict)- 通用冲突
  • 412 (Precondition Failed)- 前置条件失败(如执行条件更新时的冲突)
  • 415 (unsupported media type)- 接受到的表示不受支持
  • 500 (internal server error)- 通用错误响应
  • 503 (Service Unavailable)- 服务当前无法处理请求

DELETE

  • 不安全但幂等
  • 删除资源
  • 200 (OK)- 资源已被删除
  • 301 (Moved Permanently)- 资源的URI已更改
  • 303 (See Other)- 其他,如负载均衡
  • 400 (bad request)- 指代坏请求
  • 404 (not found)- 资源不存在
  • 409 (conflict)- 通用冲突
  • 500 (internal server error)- 通用错误响应
  • 503 (Service Unavailable)- 服务端当前无法处理请求

下面我们来看一些实践中常见的问题:

  • POST和PUT用于创建资源时有什么区别?

POST和PUT在创建资源的区别在于,所创建的资源的名称(URI)是否由客户端决定。 例如为我的博文增加一个java的分类,生成的路径就是分类名/categories/java,那么就可以采用PUT方法。不过很多人直接把POST、GET、PUT、DELETE直接对应上CRUD,例如在一个典型的rails实现的RESTful应用中就是这么做的。

我认为,这是因为rails默认使用服务端生成的ID作为URI的缘故,而不少人就是通过rails实践REST的,所以很容易造成这种误解。

  • 客户端不一定都支持这些HTTP方法吧?

的确有这种情况,特别是一些比较古老的基于浏览器的客户端,只能支持GET和POST两种方法。

在实践上,客户端和服务端都可能需要做一些妥协。例如rails框架就支持通过隐藏参数_method=DELETE来传递真实的请求方法, 而像Backbone这样的客户端MVC框架则允许传递_method传输和设置X-HTTP-Method-Override头来规避这个问题。

  • 统一接口是否意味着不能扩展带特殊语义的方法?

统一接口并不阻止你扩展方法,只要方法对资源的操作有着具体的、可识别的语义即可,并能够保持整个接口的统一性。

像WebDAV就对HTTP方法进行了扩展,增加了LOCK、UPLOCK等方法。而github的API则支持使用PATCH方法来进行issue的更新,例如:

PATCH /repos/:owner/:repo/issues/:number

不过,需要注意的是,像PATCH这种不是HTTP标准方法的,服务端需要考虑客户端是否能够支持的问题。

  • 统一资源接口对URI有什么指导意义?

统一资源接口要求使用标准的HTTP方法对资源进行操作,所以URI只应该来表示资源的名称,而不应该包括资源的操作。

通俗来说,URI不应该使用动作来描述。例如,下面是一些不符合统一接口要求的URI:

  • GET /getUser/1
  • POST /createUser
  • PUT /updateUser/1
  • DELETE /deleteUser/1

如果GET请求增加计数器,这是否违反安全性?

安全性不代表请求不产生副作用,例如像很多API开发平台,都对请求流量做限制。像github,就会限制没有认证的请求每小时只能请求60次。

但客户端不是为了追求副作用而发出这些GET或HEAD请求的,产生副作用是服务端”自作主张”的。

另外,服务端在设计时,也不应该让副作用太大,因为客户端认为这些请求是不会产生副作用的。

  • 直接忽视缓存可取吗?

即使你按各个动词的原本意图来使用它们,你仍可以轻易禁止缓存机制。 最简单的做法就是在你的HTTP响应里增加这样一个报头: Cache-control: no-cache。 但是,同时你也对失去了高效的缓存与再验证的支持(使用Etag等机制)。

对于客户端来说,在为一个REST式服务实现程序客户端时,也应该充分利用现有的缓存机制,以免每次都重新获取表示。

  • 响应代码的处理有必要吗?

HTTP的响应代码可用于应付不同场合,正确使用这些状态代码意味着客户端与服务器可以在一个具备较丰富语义的层次上进行沟通。

例如,201(”Created”)响应代码表明已经创建了一个新的资源,其URI在Location响应报头里。

假如你不利用HTTP状态代码丰富的应用语义,那么你将错失提高重用性、增强互操作性和提升松耦合性的机会。

如果这些所谓的RESTful应用必须通过响应实体才能给出错误信息,那么SOAP就是这样的了,它就能够满足了。

2. 3 资源的表述

上面提到,客户端通过HTTP方法可以获取资源,是吧? 不,确切的说,客户端获取的只是资源的表述而已。 资源在外界的具体呈现,可以有多种表述(或成为表现、表示)形式,在客户端和服务端之间传送的也是资源的表述,而不是资源本身。 例如文本资源可以采用html、xml、json等格式,图片可以使用PNG或JPG展现出来。

资源的表述包括数据和描述数据的元数据,例如,HTTP头”Content-Type” 就是这样一个元数据属性。

那么客户端如何知道服务端提供哪种表述形式呢?

答案是可以通过HTTP内容协商,客户端可以通过Accept头请求一种特定格式的表述,服务端则通过Content-Type告诉客户端资源的表述形式。

在URI里边带上版本号

有些API在URI里边带上版本号,例如:

  • http://api.example.com/1.0/foo
  • http://api.example.com/1.2/foo
  • http://api.example.com/2.0/foo

如果我们把版本号理解成资源的不同表述形式的话,就应该只是用一个URL,并通过Accept头部来区分,还是以github为例,它的Accept的完整格式是:application/vnd.github[.version].param[+json]

对于v3版本的话,就是Accept: application/vnd.github.v3。对于上面的例子,同理可以使用使用下面的头部:

  • Accept: vnd.example-com.foo+json; version=1.0
  • Accept: vnd.example-com.foo+json; version=1.2
  • Accept: vnd.example-com.foo+json; version=2.0

使用URI后缀来区分表述格式

像rails框架,就支持使用/users.xml或/users.json来区分不同的格式。 这样的方式对于客户端来说,无疑是更为直观,但混淆了资源的名称和资源的表述形式。 我个人认为,还是应该优先使用内容协商来区分表述格式。

如何处理不支持的表述格式

当服务器不支持所请求的表述格式,那么应该怎么办?若服务器不支持,它应该返回一个HTTP 406响应,表示拒绝处理该请求。

2. 4 资源的链接

我们知道REST是使用标准的HTTP方法来操作资源的,但仅仅因此就理解成带CURD的Web数据库架构就太过于简单了。

这种反模式忽略了一个核心概念:”超媒体即应用状态引擎(hypermedia as the engine of application state)”。 超媒体是什么?

当你浏览Web网页时,从一个连接跳到一个页面,再从另一个连接跳到另外一个页面,就是利用了超媒体的概念:把一个个把资源链接起来.

要达到这个目的,就要求在表述格式里边加入链接来引导客户端。在《RESTful Web Services》一书中,作者把这种具有链接的特性成为连通性。

2. 5 状态的转移

有了上面的铺垫,再讨论REST里边的状态转移就会很容易理解了。

不过,我们先来讨论一下REST原则中的无状态通信原则。初看一下,好像自相矛盾了,既然无状态,何来状态转移一说?

其实,这里说的无状态通信原则,并不是说客户端应用不能有状态,而是指服务端不应该保存客户端状态。

2. 5.1 应用状态与资源状态

实际上,状态应该区分应用状态和资源状态,客户端负责维护应用状态,而服务端维护资源状态。

客户端与服务端的交互必须是无状态的,并在每一次请求中包含处理该请求所需的一切信息。

服务端不需要在请求间保留应用状态,只有在接受到实际请求的时候,服务端才会关注应用状态。

这种无状态通信原则,使得服务端和中介能够理解独立的请求和响应。

在多次请求中,同一客户端也不再需要依赖于同一服务器,方便实现高可扩展和高可用性的服务端。

但有时候我们会做出违反无状态通信原则的设计,例如利用Cookie跟踪某个服务端会话状态,常见的像J2EE里边的JSESSIONID。

这意味着,浏览器随各次请求发出去的Cookie是被用于构建会话状态的。

当然,如果Cookie保存的是一些服务器不依赖于会话状态即可验证的信息(比如认证令牌),这样的Cookie也是符合REST原则的。

2. 5.2 应用状态的转移

状态转移到这里已经很好理解了, “会话”状态不是作为资源状态保存在服务端的,而是被客户端作为应用状态进行跟踪的。客户端应用状态在服务端提供的超媒体的指引下发生变迁。服务端通过超媒体告诉客户端当前状态有哪些后续状态可以进入。

这些类似”下一页”之类的链接起的就是这种推进状态的作用——指引你如何从当前状态进入下一个可能的状态。

RPC服务和HTTP服务对比

很长时间以来都没有怎么好好搞清楚RPC(即Remote Procedure Call,远程过程调用)和HTTP调用的区别,不都是写一个服务然后在客户端调用么?这里请允许我迷之一笑~Naive!本文简单地介绍一下两种形式的C/S架构,先说一下他们最本质的区别,就是RPC主要是基于TCP/IP协议的,而HTTP服务主要是基于HTTP协议的,我们都知道HTTP协议是在传输层协议TCP之上的,所以效率来看的话,RPC当然是要更胜一筹啦!下面来具体说一说RPC服务和HTTP服务。

OSI网络七层模型

在说RPC和HTTP的区别之前,我觉的有必要了解一下OSI的七层网络结构模型(虽然实际应用中基本上都是五层),它可以分为以下几层: (从上到下)

  • 第一层:应用层。定义了用于在网络中进行通信和传输数据的接口;
  • 第二层:表示层。定义不同的系统中数据的传输格式,编码和解码规范等;
  • 第三层:会话层。管理用户的会话,控制用户间逻辑连接的建立和中断;
  • 第四层:传输层。管理着网络中的端到端的数据传输;
  • 第五层:网络层。定义网络设备间如何传输数据;
  • 第六层:链路层。将上面的网络层的数据包封装成数据帧,便于物理层传输;
  • 第七层:物理层。这一层主要就是传输这些二进制数据。

实际应用过程中,五层协议结构里面是没有表示层和会话层的。应该说它们和应用层合并了。我们应该将重点放在应用层和传输层这两个层面。因为HTTP是应用层协议,而TCP是传输层协议。好,知道了网络的分层模型以后我们可以更好地理解为什么RPC服务相比HTTP服务要Nice一些!

RPC服务

从三个角度来介绍RPC服务:分别是RPC架构,同步异步调用以及流行的RPC框架。

RPC架构

先说说RPC服务的基本架构吧。允许我可耻地盗一幅图哈~我们可以很清楚地看到,一个完整的RPC架构里面包含了四个核心的组件,分别是Client ,Server,Client Stub以及Server Stub,这个Stub大家可以理解为存根。分别说说这几个组件:

  • 客户端(Client),服务的调用方。
  • 服务端(Server),真正的服务提供者。
  • 客户端存根,存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。
  • 服务端存根,接收客户端发送过来的消息,将消息解包,并调用本地的方法。

RPC主要是用在大型企业里面,因为大型企业里面系统繁多,业务线复杂,而且效率优势非常重要的一块,这个时候RPC的优势就比较明显了。实际的开发当中是这么做的,项目一般使用maven来管理。比如我们有一个处理订单的系统服务,先声明它的所有的接口(这里就是具体指Java中的interface),然后将整个项目打包为一个jar包,服务端这边引入这个二方库,然后实现相应的功能,客户端这边也只需要引入这个二方库即可调用了。为什么这么做?主要是为了减少客户端这边的jar包大小,因为每一次打包发布的时候,jar包太多总是会影响效率。另外也是将客户端和服务端解耦,提高代码的可移植性。

同步调用与异步调用

什么是同步调用?什么是异步调用?同步调用就是客户端等待调用执行完成并返回结果。异步调用就是客户端不等待调用执行完成返回结果,不过依然可以通过回调函数等接收到返回结果的通知。如果客户端并不关心结果,则可以变成一个单向的调用。这个过程有点类似于Java中的callablerunnable接口,我们进行异步执行的时候,如果需要知道执行的结果,就可以使用callable接口,并且可以通过Future类获取到异步执行的结果信息。如果不关心执行的结果,直接使用runnable接口就可以了,因为它不返回结果,当然啦,callable也是可以的,我们不去获取Future就可以了。

流行的RPC框架

目前流行的开源RPC框架还是比较多的。下面重点介绍三种:

  1. gRPC是Google最近公布的开源软件,基于最新的HTTP2.0协议,并支持常见的众多编程语言。 我们知道HTTP2.0是基于二进制的HTTP协议升级版本,目前各大浏览器都在快马加鞭的加以支持。 这个RPC框架是基于HTTP协议实现的,底层使用到了Netty框架的支持。
  2. Thrift是Facebook的一个开源项目,主要是一个跨语言的服务开发框架。它有一个代码生成器来对它所定义的IDL定义文件自动生成服务代码框架。用户只要在其之前进行二次开发就行,对于底层的RPC通讯等都是透明的。不过这个对于用户来说的话需要学习特定领域语言这个特性,还是有一定成本的。
  3. Dubbo是阿里集团开源的一个极为出名的RPC框架,在很多互联网公司和企业应用中广泛使用。协议和序列化框架都可以插拔是及其鲜明的特色。同样 的远程接口是基于Java Interface,并且依托于spring框架方便开发。可以方便的打包成单一文件,独立进程运行,和现在的微服务概念一致。

偷偷告诉你集团内部已经不怎么使用dubbo啦,现在用的比较多的叫HSF,又名“好舒服”。后面有可能会开源,大家拭目以待。

HTTP服务

其实在很久以前,我对于企业开发的模式一直定性为HTTP接口开发,也就是我们常说的RESTful风格的服务接口。的确,对于在接口不多、系统与系统交互较少的情况下,解决信息孤岛初期常使用的一种通信手段;优点就是简单、直接、开发方便。利用现成的http协议进行传输。我们记得之前本科实习在公司做后台开发的时候,主要就是进行接口的开发,还要写一大份接口文档,严格地标明输入输出是什么?说清楚每一个接口的请求方法,以及请求参数需要注意的事项等。比如下面这个例子:
POST http://www.httpexample.com/restful/buyer/info/share
接口可能返回一个JSON字符串或者是XML文档。然后客户端再去处理这个返回的信息,从而可以比较快速地进行开发。但是对于大型企业来说,内部子系统较多、接口非常多的情况下,RPC框架的好处就显示出来了,首先就是长链接,不必每次通信都要像http一样去3次握手什么的,减少了网络开销;其次就是RPC框架一般都有注册中心,有丰富的监控管理;发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。

总结

RPC服务和HTTP服务还是存在很多的不同点的,一般来说,RPC服务主要是针对大型企业的,而HTTP服务主要是针对小企业的,因为RPC效率更高,而HTTP服务开发迭代会更快。总之,选用什么样的框架不是按照市场上流行什么而决定的,而是要对整个项目进行完整地评估,从而在仔细比较两种开发框架对于整个项目的影响,最后再决定什么才是最适合这个项目的。一定不要为了使用RPC而每个项目都用RPC,而是要因地制宜,具体情况具体分析。

分布式架构的演进

分布式架构的演进

系统架构演化历程-初始阶段架构

初始阶段 的小型系统 应用程序、数据库、文件等所有的资源都在一台服务器上通俗称为LAMP

特征:
应用程序、数据库、文件等所有的资源都在一台服务器上。

描述:
通常服务器操作系统使用linux,应用程序使用PHP开发,然后部署在Apache上,数据库使用Mysql,汇集各种免费开源软件以及一台廉价服务器就可以开始系统的发展之路了。

系统架构演化历程-应用服务和数据服务分离

好景不长,发现随着系统访问量的再度增加,webserver机器的压力在高峰期会上升到比较高,这个时候开始考虑增加一台webserver

特征:
应用程序、数据库、文件分别部署在独立的资源上。

描述:
数据量增加,单台服务器性能及存储空间不足,需要将应用和数据分离,并发处理能力和数据存储空间得到了很大改善。

系统架构演化历程-使用缓存改善性能

特征:
数据库中访问较集中的一小部分数据存储在缓存服务器中,减少数据库的访问次数,降低数据库的访问压力。

描述:
系统访问特点遵循二八定律,即80%的业务访问集中在20%的数据上。
缓存分为本地缓存和远程分布式缓存,本地缓存访问速度更快但缓存数据量有限,同时存在与应用程序争用内存的情况。

系统架构演化历程-使用应用服务器集群

在做完分库分表这些工作后,数据库上的压力已经降到比较低了,又开始过着每天看着访问量暴增的幸福生活了,突然有一天,发现系统的访问又开始有变慢的趋势了,这个时候首先查看数据库,压力一切正常,之后查看webserver,发现apache阻塞了很多的请求,而应用服务器对每个请求也是比较快的,看来 是请求数太高导致需要排队等待,响应速度变慢

特征:
多台服务器通过负载均衡同时向外部提供服务,解决单台服务器处理能力和存储空间上限的问题。

描述:
使用集群是系统解决高并发、海量数据问题的常用手段。通过向集群中追加资源,提升系统的并发处理能力,使得服务器的负载压力不再成为整个系统的瓶颈。

系统架构演化历程-数据库读写分离

享受了一段时间的系统访问量高速增长的幸福后,发现系统又开始变慢了,这次又是什么状况呢,经过查找,发现数据库写入、更新的这些操作的部分数据库连接的资源竞争非常激烈,导致了系统变慢

特征:
多台服务器通过负载均衡同时向外部提供服务,解决单台服务器处理能力和存储空间上限的问题。

描述:
使用集群是系统解决高并发、海量数据问题的常用手段。通过向集群中追加资源,使得服务器的负载压力不在成为整个系统的瓶颈。

系统架构演化历程-反向代理和CDN加速

特征:
采用CDN和反向代理加快系统的 访问速度。

描述:
为了应付复杂的网络环境和不同地区用户的访问,通过CDN和反向代理加快用户访问的速度,同时减轻后端服务器的负载压力。CDN与反向代理的基本原理都是缓存。

系统架构演化历程-分布式文件系统和分布式数据库

随着系统的不断运行,数据量开始大幅度增长,这个时候发现分库后查询仍然会有些慢,于是按照分库的思想开始做分表的工作

特征:
数据库采用分布式数据库,文件系统采用分布式文件系统。

描述:
任何强大的单一服务器都满足不了大型系统持续增长的业务需求,数据库读写分离随着业务的发展最终也将无法满足需求,需要使用分布式数据库及分布式文件系统来支撑。
分布式数据库是系统数据库拆分的最后方法,只有在单表数据规模非常庞大的时候才使用,更常用的数据库拆分手段是业务分库,将不同的业务数据库部署在不同的物理服务器上。

系统架构演化历程-使用NoSQL和搜索引擎

特征:
系统引入NoSQL数据库及搜索引擎。

描述:
随着业务越来越复杂,对数据存储和检索的需求也越来越复杂,系统需要采用一些非关系型数据库如NoSQL和分数据库查询技术如搜索引擎。应用服务器通过统一数据访问模块访问各种数据,减轻应用程序管理诸多数据源的麻烦。

系统架构演化历程-业务拆分

特征:
系统上按照业务进行拆分改造,应用服务器按照业务区分进行分别部署。

描述:
为了应对日益复杂的业务场景,通常使用分而治之的手段将整个系统业务分成不同的产品线,应用之间通过超链接建立关系,也可以通过消息队列进行数据分发,当然更多的还是通过访问同一个数据存储系统来构成一个关联的完整系统。

纵向拆分:
将一个大应用拆分为多个小应用,如果新业务较为独立,那么就直接将其设计部署为一个独立的Web应用系统

纵向拆分相对较为简单,通过梳理业务,将较少相关的业务剥离即可。

横向拆分:将复用的业务拆分出来,独立部署为分布式服务,新增业务只需要调用这些分布式服务

横向拆分需要识别可复用的业务,设计服务接口,规范服务依赖关系。
系统架构演化历程-分布式服务

特征:
公共的应用模块被提取出来,部署在分布式服务器上供应用服务器调用。

描述:
随着业务越拆越小,应用系统整体复杂程度呈指数级上升,由于所有应用要和所有数据库系统连接,最终导致数据库连接资源不足,拒绝服务。

转载:https://www.zhihu.com/question/22764869/answer/31277656

子系统拆分的一点总结

公司系统做了一年多,慢慢也有点规模了。从最初只有一个APP + 一个server的模式,到现在有多个子系统,多个客户端。这个过程中,积累了一些想法,本文简单总结一下

系统拆分的好处

基本上,比较小的系统,单进程集中部署就可以了。集中部署不代表一定不好,在系统规模很小的时候,或许是最适合的,因为调用关系简单,开发也比较容易。但是系统慢慢变大了以后,我认为拆分系统,分布式部署就变得更为合理了。

拆分系统至少有这些我体会到的好处:

1、停掉系统的一个部分,只会影响相关业务,不会造成整体业务中断。特别是一个新的模块上线,尚未稳定的时候,可能会有错误挂掉,或者主动重启维护等,如果是集中部署,就会造成整个系统都不可用。但是分布式部署的情况下,只会中断小范围的业务。当然,就算是集中部署,利用集群,分批重启,也可以实现同样的目的

2、对压力大的节点,可以单独部署集群。比如我们的系统,数据同步模块的负载是最高的,那么就可以针对这个子系统单独部署集群,其他负载低的模块,可以部署在一起,或者单独部署,都比较灵活。当然,要实现水平伸缩,对系统设计本身也有要求,比如至少要实现无状态服务等

3、代码分离,便于权限控制。一般来说,集中部署的代码也是在一起的,如果希望负责子系统A的小组,不需要接触到子系统B的代码,那么分成2个代码库就非常容易实现。相反,如果代码都是在一起的,控制就比较困难。因为不能只开放一部分代码给开发人员,这样不利于在本地搭建开发环境

4、按责任田制度,小团队维护特定模块。跟上面一点比较类似,每个小团队的责任边界比较清晰

按业务垂直拆分系统

拆分系统也要根据实际情况,有不同的选择。我们早期的时候是根据业务,垂直拆分子系统,比如划分成微站,数据同步,连锁等。这样做的好处是,每个子系统都是可以独立跑起来的,比如说把微站子系统运行起来,微站的页面就都能访问了,数据也是该系统自己负责读写的。但是缺点也很明显,就是冗余的代码比较多。比如连锁和微站,2个子系统都需要查询企业信息,那么就各自都写了这部分代码,其实接口几乎是一模一样的,存在很大的复用空间。重复行为基本上都是不好的,这个应该说是开发人员的共识

网状结构

后来做了一点调整,基本上子系统还是按照业务拆分的。但是每个子系统都对外提供服务,比如基础数据查询模块,提供了查询企业信息的接口。连锁和微站子系统,自己就不重复查了,而是以HTTP方式,调用基础数据查询模块的这个接口。这种方式的优缺点和上一种方案大致相反。消除了重复代码,但是模块之间存在依赖关系。如果基础数据查询模块不跑起来,那微站模块虽然能跑起来,但是相关的数据就没有了

而且这样调用关系会比较复杂一点,因为本地调用都变成了HTTP接口调用,意味着业务模块,需要知道去哪里调用所需的服务,可能需要配置很多IP地址(如果依赖很多外部服务的话)。并且这个IP地址是经常需要变化的,不同的开发人员,本地的开发环境地址都不一样;开发环境和生产环境的地址也不一样;生产环境的集群配置变化了,也可能造成地址的变化。系统的复杂性变得比较高

星型结构

再后来为了解决这个调用的问题,TOPO演进为星型结构,有一个中心节点。业务模块把所有的内部请求都发到这个中心节点上,由中心节点负责转发到服务提供者上。这样对于业务模块来说,就不需要知道服务提供方的实际地址,只要把所有请求都发到中心节点上就可以了。映射的工作由中心节点来完成,需要类似这样的映射:

service1       192.168.1.110:8080/svc1

service2       192.168.1.110:8080/svc2

service3       192.168.1.111:8080/svc3

……

这个工作,在服务的数量和复杂度不是太高的时候,只需要一个简单的路由就可以了,不需要专门的服务治理方案。比如我们早期采用的就是nginx,把nginx当做内部的服务中心来使用,借助server_name,proxy_pass,up_stream等特性,已经足以满足需求。但是当服务的数量和复杂度达到一个量级,就需要有专门的方案,来处理服务的注册、发布、寻址、负载均衡、队列、失败重试等需求了

转载:https://blog.csdn.net/kyfxbl/article/details/43506343

互联网项目架构经验分享

罗马不是一天建成的,架构也不是一蹴而就的,需求-重构-上线不断的循环才有造就了架构之美或者架构之殇。

从事it开发工作已经8个年头了,参与10多个项目的开发,主导数个互联网项目的架构设计,主要是电商或者电商相关的项目,从开始的无从下手到现在的轻车熟路,过程磕磕绊绊,所幸都没有夭折,基本上顺利上线。读过一些架构相关的书籍,书中的架构的思路,实现过程和方式方法和自己架构设计的过程差别很大(我都在怀疑是否接触的层次太低了,哈哈),所以就想把自己的一些经验整理一下,整理总结思路,做为一种沉淀,另外一方面也可以和大家互相交流学习,知道自己的不足才能有更大的进步。

 

1、什么是架构设计

“架构设计是人们对一个结构内的元素及元素间关系的一种主观映射的产物。架构设计是一系列相关的抽象模式,用于指导大型软件系统各个方面的设计。” 来自百度百科。资料中的定义是准确、完备和书面化的,仍然很难理解架构设计的本质。通俗的描述,架构设计就像是小学考试中解答应用题的过程,但是解决的问题更复杂,构思设计的过程更庞大,解题的工作量更大。

2、项目的质量指标: 功能,性能和扩展性

软件开发的最终目标是使用代码去实现抽象的业务逻辑,可以从3个方面衡量:功能,性能和扩展性。

功能:功能目标是应用的基本要求,如果不能实现既定的功能逻辑,应用就失去了存在的意义,因此实现产品需求是应用的基本的目标。

性能:在基本的功能之上,会有一些性能的要求,但是很少有产品经理或者用户能提前提出这样的要求,因此架构师要有丰富的经验去发现和解决(或者为未来提升性能做准备)性能问题。性能的主要衡量有:单次请求的相应时间,单实例请求并发数,服务最大并发量等。

扩展性:目前互联网应用的开发模式:快速响应,迭代开发;提出需求,快速相应,尽快上线,是骡子是马拉出来溜溜。所以这就要求系统的架构设计要更好的响应新的需求和需求变更。

3、架构设计的主要过程

3年多来,数个项目的架构经验,我自己的架构设计过程是 : 确定问题域,数据建模,模块划分,关键流程描述,技术选型,代码实现,验收测试

3.1、确定问题域

记得小学考试后拿到老师改过的卷子,对着一个个的大红叉都会懊恼:”哎,又看错题目了“。错误的方向危害大于错误的方法,没有找对方向,项目就会南辕北辙,远远偏离目标。来自产品经理或者用户的需求描述就是我们的问题域,但是来自于产品经理的需求描述会比较全面,内容也很多,来自用户比较简单,相对比较模糊。比如电商项目的需求文档会非常大,拿到一个几十上百页的需求文档(有程序员拍砖说,我家的产品只一段话”像XXX网站的功能copy一下吧”,哈哈)时,往往不知道从何处下手,所以我们要从繁杂的问题域中找到关键问题。

a、用户可以在我们的网站上购买XXX商品。

从a条出发,又延伸出来几个问题:

b、用户访问:用户登录,注册

c、商品来源:商品的管理,增删改查等

d、交易的过程:订单的管理等

从d交易出发,又能延伸出来

e、用户付钱:支付

f、商家配送:收货地址,配送流程

不断的展开问题域,就可以把整个流程转起来。当然实际应用的时候我们不会把所有的问题域都总结出来,确定了关键问题就可以开始数据建模了。

上面我们分析的是功能问题域,这会也需要确定一下性能和扩展性的问题域。性能的问题域应该是针对关键路径确定的,比如商品浏览,订单创建,订单支付等。针对于这些关键路径问题,可以定义一些问题域,比如单实例支持1000pv/秒商品浏览,100单/秒的订单提交等。

扩展性是最难把握的,因为每个人经历不同,针对同样的项目会对未来需求有不同的预期,因此怎么把握当前的功能和未来的变化,如何平衡性能和扩展的关系,是架构师设计的关键。以我的的经验来看,扩展把我关键问题,优先满足关键问题的性能,确定最小功能集。确定最小功能集的优势可以快速实现,快速验证需求的准确性,每次需求开发都完成最小和最关键的需求。设计的时候要满足一些思想和原则,OOP(面向对象设计)原则:1、单一职责原则;2、开放闭合原则;3、里氏替换原则;4、依赖倒置原则;5、接口隔离原则;数据库设计三范式等等。扩展的问题域也可以参考友商或者与有经验的产品运营沟通,大致了解存在的扩展性。电商项目可能会有:抢购,预定,团购等业务都是电商的一些扩展需求。

3.2、数据建模

确定了问题域就可以开始答题了,确定数据模型。大部分的应用基本使用的仍然是关系型数据库,所以我们针对问题域先创建数据表,当然也存在一些项目使用NoSQL存储或者不持久化数据,这里确定的就是问题域的实体类。3.1中描述的问题域每个名称都是一个数据表(或者实体对象),用户,商品,订单,支付流水,收货地址,配送单。

我比较喜欢使用powerdesigner做数据库模型,可以直观的看到表结构,方便修改,可以生成大部分DB的DDL SQL。为上面找出的名词(实体结构)创建表结构,然后根据产品需求文档一条一条的阅读判断,当前表结构是否可以满足需求,如果不能满足,在表中添加列或者添加新的表来满足此需求,不断的去丰富表结构直到完全满足需求。当然在建模的过程中也会调整原来的表结构,毕竟不断的增加需求,会引起数据模型的变化,所以最初建立的肯定不完整,不断调整直到满足所有需求。

数据建模时的几个心得:

a、数据表包含自增id,创建时间createTime,更新时间updateTime和版本号version

自增的id:主键,根据id查询或者更新时,速度毕竟快。

createTime和updateTime记录创建时间和最后的更新时间,排查问题的关键点

version:编辑时version++,是一个很方便的乐观锁,能比较大的提升数据库的性能

b、不使用外键,这点有一些和数据库设计的规范相悖,但是这是来自真实经验总结,外键约束带来的数据完整性的优势远远小于更新逻辑实现的难度。从性能和扩展性来看不使用外键也是利大于弊,大数据高并发大流量的互联网应用提供性能的常用方法是:提高数据库的访问速度,缓存数据,数据库分库分表支撑高并发等,外键是对这些方法的一个制约。

c、不使用id作为表关联,虽然我们不创建外键约束,但是不代表表之间没有关联关系。所以表之间仍然会有外键,但是没有外键约束,设计这个外键的时候要考虑数据的增长型,数据没有确定的规模,那么参考增长的速度,我们可以设定一个未来3-5年的数据规模,如果单表不能满足,则数据存在分表的可能性,那么表间的关联使用全局的唯一id的方式一个更好的选择。全局唯一id的方式有很多算法,使用数据库(oracle的sequence和mysql的自增id,这里是为生成id的特殊表的自增)生成是一个比较好的方式,当然也可以使用组合方式添加数据类型,时间,地域等方式,也有使用uuid算法计算的方式,只要可以满足不重复的特点,选取那种方式可以参考一下产品的意见,因为这个字段用户可能感知。

d、表有没有多少列的标准?记得刚开始做设计的时候,经常怀疑自己是不是分的表太多或者太少,太大了是不是会影响性能,太少了是不是有点画蛇添足。应用最初的设计最符合设计原则和设计思想的,没有收到工期,团队分割,实现难度等非设计因素的影响,所以我们应该尽量的坚持最初的设计,克服其他因素的影响。

e、冗余字段是否有必要?我的做法是不使用,保持原有的设计,如果系统真的流量比较大,查询性能太低,可以通过把读服务从业务系统中分离(这里要注意不是数据库的读写分离)。从业务上把读写分开,做一些便于查询和提高性能的设计,通过一些数据抽取方式同步数据。这里会有人提出异议,这样做会导致用户的读延迟,其实展示性数据对数据的延迟是有很大的容忍度的,只有业务系统需要做到数据的一致,那么业务系统对数据的读取是针对性的,很少会出现需要很多关联数据的情况,所以最初设计系统时尽量少的使用冗余字段去提供查询的方便性和性能。

以下是电商应用部分表(商品和订单业务相关)设计,仅供参考:

3.3、模块划分

如果数据模型主要为满足功能目标的话,模块划分会比较多的兼顾性能和扩展性,常用的互联网应用的模块划分和部署结构有如下几种(这里讨论的划分和部署是互联网应用的服务器端),不考虑浏览器和APP等客户端,当然介绍的几种也是常用的结构。

单实例结构:

优势: 结构简单,便于开发部署

劣势:可能存在性能瓶颈,扩展性差,系统耦合性高

集群结构:

 

备注:db层作为整体描述,可能存在单DB,读写分离,分库分表或者数据cluster等技术

优势:性能大大提供(理论上可以无上限)

劣势:负载存在平衡的可能,仍然会存在性能瓶颈,扩展性差

分布式结构:

分布式系统是把不同的业务切分到不同的实例中,功能相关的聚合到同一个实例中,系统间使用网络协议通信的一种结构。

优势:扩展性强,高内聚,低耦合

劣势:结构复杂, 事务控制难度大,开发工作量大

混合结构:

混合结构是分布结构基础之上每个模块又实现集群结构,所以这个模式放大了分布式结构的优势和劣势

优势: 扩展性强,高内聚,低耦合,健壮性高

劣势: 结构复杂, 事务控制难度大,开发工作量大

大部分的互联网应用的结构都可以用上面的4个结构描述,当然这里只是简单的描述,一些应用为了提高数据库访问会加上DB缓存,为了提高页面的访问速度做页面静态化和CDN,为了应对大数据的存储和检索使用NoSQL数据库等,但是我们设计的结构是不受影响的。如何选择系统结构,可以从如下几方面考虑:

1、系统是否能满足未来2-3年的增长,如果采用混合结构的系统工作量要远远大于单例结构的系统,对初创企业来说,上线才是最大的需求,所以拼速度的时候就要放弃优雅。如果公司有一定的规模,开发的应用是核心业务或者未来的核心业务,采用扩展性强的混合结构应对未来的快速发展的业务需求是一个更好的选择。

2、人力因素,混合结构要比单实例的结构工作量增加很多,并且对团队整体的技术水平有较高的要求,所以要”量力而行”。

3、时间因素,很多互联网公司的工期不是技术评估的,是由”市场”确定的,所以火烧眉毛的时候就别讲究性能和扩展了,上线再说。

4、架构不是一成不变的,不断增加的访问和不断变化的需求改变着系统的架构。快速响应,不断迭代才是互联网应用的方式。所以最初的架构,尽量的做到高内聚低耦合,这样不断的提高系统的短板,逐渐完善系统结构。

分布式结构电商模块描述:

显示层:

前台:用户端界面显示层,依赖用户服务,商品服务,交易服务和支付服务

后台:运营端界面显示层,运营人员管理各种数据的界面。 依赖用户服务,商品服务,交易服务和支付服务;

服务层:为界面提供RPC服务

用户服务:注册,登录,用户管理等

商品服务:商品浏览,库存展示,商品管理,库存管理等

交易服务:购物车服务,订单计算, 订单提交,订单列表等

支付服务:生成支付链接,支付成功跳转,支付成功逻辑处理等

基础组件:

DB:数据存储

Redis:使用redis实现用户状态session机制,便于将来集群部署;实现购物车功能,用户购物车服务端持久化,便于用户跨浏览器购物车管理。

第三方支付组件:用于与第三方支付服务交互

3.4、关键流程描述

关键流程描述是检查系统架构是否满足需求和指导开发的必要条件。关键流程描述是使用流程图解决关键问题的过程,它的使用者是团队其他成员和自己,所以格式不重要,其他人能明白就好。

一些书写的经验如下:

1、有始有终,流程应该是从用户进入应用开始到离开应用的完整过程,比如交易的过程,应该从用户开始浏览商品到用户支付成功这一个过程。

2、流程图突出重点,比如上面举例的交易过程,应该突出交易相关的流程判断,不必描述用户注册,找回密码等过程。

3、简要说明,避免过于详细,比如交易的过程中需要更新库存,但是不需要描述更新库存前的库存校验这些是提交订单的内部实现。

示例如下:

3.5、技术选型

1、如果非必要请使用常用的技术,框架等,常用技术和框架使用者多,所以会比较少的遇到非业务问题。曾经参与的一个项目,其中一个模块由一个比较熟悉python的同学负责,系统刚刚上线,因为一些原因要离职,没有人可以接下来,只好找其他的语言重新开发了一遍。

2、熟悉的优于强大的,尽量采取团队比较熟悉的技术或者使用团队中有人可以指导的技术。记得5年前为甲方公司做一个需求和Bug跟踪的工作流的系统,轻率的决定使用JBPM,因为团队中没有人研究过,所以花了大量的时间使用这个框架,最后也没有很好的使用,导致项目步履蹒跚。

3.6、代码实现

代码首先是给人读的,其次才是给机器读,所以良好的代码结构是项目存活更长时间的良药。

1、代码分层:功能单一原则,mvc是互联网应用的一种基础模式,从功能层次上划分为,v显示层,c控制层,m业务层。以Java实现业务分层如下:

自上而下的层级,

controller:页面控制层,用于页面出参入参转换和页面跳转

vo:贫血实体对象,用于页面和业务层的数据传输

bo:业务实现层

dao:数据访问层,用于处理与数据库的交互

po:贫血实体对象,用于dao与数据库传输,和数据库表列意义对应

2、命名规范

代码中使用的类,方法,变量,参数等,采用统一风格命名,英文或者中文拼音,驼峰或者下划线分隔,尽量采用业界常用风格。类,变量和参数采用使用名词,方法使用动词等。

3、注释风格

采用统一的注释风格,方法内一般采用行注释,其他地方采用段注释。

3.7、验收测试

积极配合测试团队对项目的测试,他们是为项目健康上线保驾护航的人,不是挑刺的人。

4、总结

1、同样的题目有多种解法,我们做的只是其中的一种,所以要接受别人的质疑和建议,这样才能使系统完善。

2、没有银弹,没有解决一切问题的方法,那么也不可能使用一种方法解决所有问题, 所以要根据需求,团队,时间等选择合适的方式方法。

来自:http://mp.weixin.qq.com/s?__biz=MzA4NDc2MDQ1Nw==&mid=2650237940&idx=1&sn=2cb33010b12db3a7bde7a89878c80337&scene=0