B/S 类项目改善的一些建议

要分享的议题

  1. 性能提高:在访问量逐渐增大的同时,如何增大单台服务器的 PV2 上限,增长 TPS3
  2. RESTful:相较于传统的 SOAP1,RESTful 风格架构有哪些优势?作法有哪些区别?
  3. 微服务:随着企业愈来愈大,系统会愈来愈大,愈来愈难维护,如何在保证“稳”的同时,还保证有小企业的“灵活”?

简要的介绍

性能提高

最经常使用的性能提升方式能够经过使用服务器的集群来解决,简单粗暴的理解就是增长银行柜员的数量。可是,一味的只考虑从服务端提供性能,并非聪明的作法 —— 应该讲求性价比。固然,核心必须是提升服务器的 TPS,即在最短的时间内给最多的客户提供服务。服务器集群能够大幅提高总体的性能,可是咱们要讨论的是如何提高单台服务器的性能。javascript

  1. 服务器的压力主要来源于三个方面:CPU、网络和磁盘 IO。磁盘做为最容易达到瓶颈的一方,必须想办法减小 IO 操做。数据库做为数据持久性存储、磁盘开销的大户,这里主要就是要减小或合并数据库操做。
  2. 系统的流畅性取决于服务端和客户端的良好配合。网站类的项目,充分利用浏览器资源,不只能下降服务器压力,还能提供更好地客户体验。现代化的浏览器,通常都符合 RFC26164 规范。其中很重要的报头有:ETagLast-Modified 报头 —— 浏览器的缓存设置开关,能够最大限度的利用客户端资源。

RESTful 架构风格

比如面向过程编程和面向对象编程,这二者并无明确的界限。在适当的地方用适当风格的架构,重点是物尽其用。但 RESTful 做为新兴的风格,必然有其优点:html

  1. 在 RESTful 架构中,关注点在于资源。每一个都有一个地址,资源自己就是方法调用的目标。方法列表对全部资源都是同样的。这些方法都是标准方法,包括 HTTP GET、POST、PUT、DELETE,还可能包括 PATCH、HEADER 和 OPTIONS。其指导思想是远端提供了一系列资源,客户端须要下载、展示、编辑和提交更改,重点放在本地。
  2. 在 RPC 架构中,关注点在于方法。在客户端看来,就是在客户端组合条件,而后在服务器中执行,最终再反馈给客户端。其指导思想是隐藏实现细节,或者关联其它 RPC 服务运算,重点放在了服务器。

微服务和单体式应用

现代化的单体式应用,一般采用模块化的方式,围绕核心模块并行开发。最终他们须要联合测试,部署成一个单体式的应用:C# 会部署成 IIS 的一个网站,Java 会打包成 War 格式部署 Tomcat 上。java

随着时间的推移,单体式在应对愈来愈多的新需求后,会变得愈来愈大。更不幸的是,由于公司资源和需求的不对等,许多仓促应对的代码会添加到应用中。这些代码在短时间内不会出现问题,可是修正 Bug 和正常的新功能添加会变得愈来愈困难,由于一般会涉及到多个模块,牵一发而动全身。此时就是单体式应用的瓶颈期,会考虑拆分红多个子系统。固然,这将会再维持一段时间,直到再出现类似的问题。ios

许多公司,好比 Amazon、eBay 和 NetFlix,经过采用微处理结构模式解决了上述问题。其思路不是开发一个巨大的单体式的应用,而是将应用分解为小的、互相链接的微服务。web

一个微服务通常完成某个特定的功能,好比下单管理、客户管理等等。每个微服务都有本身的业务逻辑和适配器。一些微服务还会发布 Api 给其它微服务和应用客户端使用。其它微服务完成一个 Web UI。正则表达式

性能提高

  1. 数据库静态化:数据库不包含运算逻辑,全部运算逻辑在程序内完成。
  2. 减小外部 IO:使用数据缓存、合并数据库操做、读写分离。
  3. 异步化:对于非必须的方法,异步执行使其不影响当前逻辑。
  4. 子系统拆分:拆分长时间运行的逻辑为 Windows 服务或 Job 。

一个栗子:电子商务系统,下单操做起始涉及到了对多个模块的调用。可是用户下单的时候,并不关心这些,只要获得一个下单成功的结果就能够了。咱们能够分析一下:系统首先要对用户提交的信息有效性校验,再就是业务数据准确性校验,最后提交到数据库。一个成功的电商系统,前二者必须能在很短的时间内完成,而且在秒杀特卖这种场景时不会形成数据库的崩溃。数据库

基于以上两点,咱们分析下如何优化秒杀特卖这种场景下的操做流程。编程

  1. 服务端对信息有效性的校验,操做频率最密集、速度要最快,因此不该该涉及除内存运算以外的操做,好比:Redis 和数据库读写、TCP/HTTP 远程调用等。
  2. 业务性数据校验,关联模块不少、速度要求较快,因此不该涉及慢速的 IO 操做,好比:数据库读写、HTTP 远程调用等。
  3. 写数据库频繁,在较短的时间内给数据库形成很大的持续压力、速度要求很快,因此这里能够采用当即反馈,稍后写入的方式执行。

数据库静态化

数据库的操做都是有锁的:Select 语句发布共享锁5,Insert、Update 和 Delete 发布排它锁6。因此说在操做同一张表的前提下,数据库操做都是串行7的。api

基于以上考虑,让数据库只作存储容器,不负责运算才是正途。正是由于数据库的操做是串行的,在大并发量写入时,任何一点的提高都是要争取的,因此这里要把运算的任务提到程序中执行浏览器

  1. 存储过程由于把程序逻辑放在数据库,通常来讲确定包含运算任务,考虑通常开发的水平不能保证先用临时表存储预先计算好的数据(能作到也太繁琐了,很容易出现异常),最后再统一执行,因此首先要摒弃包含数据库写入类的存储过程
  2. 程序内的运算,若是是在开启事务后仍然存在,也要算入数据库的运算任务。由于数据库事务开启后,独占的串行已经开始了,程序的运算时间不只占用了程序的运算时间,还占用了数据库事务的开启时长,在本质上并无减小数据库事务的开启时长。严格来算的话,这种作法甚至还不如上一条的作法优化。

因此,真正的数据库静态化是:首先在程序内运算,产生数据库要执行的 SQL 写入语句和参数,持续运算直到产生全部的数据库写入命令;再开启数据库事务,按照先进先出原则顺序连续执行数据库写入命令(此段时间内不能包含其它非数据库运算)。只有这样,才能保证命令的执行都是静态化的写入,而且锁定数据库的时间最短,保证最大化的下降数据库压力。

另一个,正是由于数据库的操做是串行的,因此在执行数据库写入的状况下,是不能读取的,要避免出现脏读,数据库的读写分离就颇有必要了。创建从库,由主库负责写入,从库负责读取,将数据库的压力均分到多台上。

数据库的读写分离要注意:刚写入数据库的数据,同步到从库须要 2 到 3 秒的时间,须要在业务上更改流程,以便于在用户检索时数据已同步。

减小外部 IO

因为磁盘的限制,其读写速度和内存不成比例,因此这里是第二个可能出现瓶颈的地方。能够考虑将配置信息预先读取到缓存的方式解决。

由于数据库是依托于硬盘而存在的,因此数据库的读写相对于有效性验证和业务验证来讲,是时间消耗大户。在单纯的考虑数据库写入的状况下,能够从系统内剥离订单的数据库写入业务。一来能够省掉无心义等待数据库写入的时间;二来能够减小 CPU 时间片的占用,将时间

另外一种是数据库的读写操做,在一个数据库事务内,是不能有第二个数据库执行相同的操做的。考虑到数据静态化中的介绍,数据库事务开启后,程序内的运算其实也是数据库的运算时间的。此时能够考虑推迟数据库事务的开启时间:首先在程序内运算产生要执行的数据库命令,再开启数据库事务,在连续的时间内执行批量执行数据库事务。即合并数据库操做到一次数据库执行中。

异步化

在开发的过程当中,不可避免的要和其关联的模块交互,而这些交互并不会对当前的业务逻辑产生影响。这种操做就应该改为异步的方式。在数据库交互的过程当中,若是用户不须要等待数据库的返回值,还能够将数据库执行异步化,在最短的时间内反馈执行结果。

一个栗子:用户下单的过程当中,提交订单的操做,其实并不关心提交成功失败,只是在后续跳转到订单详情的时候才会看到订单详情。这个流程就能够将数据库执行异步化,在服务器接收到用户的提交请求时,能够在校验数据后,直接反馈提交成功的响应给用户。接下来,经过异步队列的方式,保存到数据库。客户端在接收到提交成功的反馈后,提示用户提交成功,可是不给出订单的任何信息。用户只有主动点击了查看订单列表,才会执行数据库查询。这个时间差足够系统处理订单的真正提交操做了。

这样作最直接的好处是提升了网站的响应速度,优化了用户体验,在提高服务器 TPS 的同时,尚未提高数据库的压力。在秒杀特卖时,可以最大限度的避免超卖的状况。

子系统拆分

继续上面的例子,数据库的提交订单操做,和网站并无多少的关系。此时就能够考虑到将这一部分拆分出来,作成一个 Windows 服务,二者经过消息队列的方式通信。队列的串行读取正好符合了数据库的串行执行,在高峰时段也没有超过数据库的极限,形成宕机的状况。在超过服务极限的状况下,处理慢比不能处理老是要好的。

从操做系统上来讲,系统调度针对每一个进程都是平等的。此处将数据库执行操做从网站拆分出来,减小了数据库操做对 CPU 时间片的占用,侧面提高了网站的服务能力。

RESTful 架构风格与 SOAP 架构风格

  1. 属性路由:使用渐进式的 URI 替代传统平板式的方法名称 URI。
  2. 客户端缓存报头:使用 ETagLast-Modified 报头减轻服务器压力。
  3. CRUD:使用 GET、POST、PUT 和 DELETE 方法区分数据库 CRUD 操做。

属性路由

第一个 WebApi 版本使用的是基于公约的路由。在该类型的路由中, 你能够定义一个或多个被参数化字符串的模版。当这个框架接收到一个请求时,它匹配一个 URI 到路由模版。

基于公约的路由的一个优点就是:这个模版被定义在一个单独的地方,路由规则一致的被应用于全部的控制器。不幸的是,基于公约的路由是很难支持确切的URI模式,而这个确切的 URI 模式在 RESTful Api 中是很广泛的。好比,资源常常包含子资源:客户下了订单,电影有演员,书有做者等等,它是很天然的建立这些 URI 来反应这些关系:

/customers/3/orders

这种类型的 URI 在基于公约的路由下是比较难实现的。尽管它能作到,可是若是你有许多控制器或者不少资源类型时,不能很好的被扩展。但对于属性路由,它是很容易的为这个 URI 定义一个路由,你能够简单的添加一个属性到控制器的动做上:

[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomerId(int customerId) { ... }

方便的 Api 版本控制

有时候咱们须要开发一个功能的新版本,可是并不想对现有的功能产生影响,好比:api/v1/productsapi/v2/products 能够被路由到不一样的控制器。在开发阶段就作出较好了区分,而且当新的版本正式商用后,也能够方便的对 V1 版本的控制器过时或停用。

重载 URI 片断

在下面的例子中,12306 表示一个特定的车票,而 notravelled 表示未出行的车票集合。

/tickets/12306
/tickets/notravelled

经过天然语义,人们能够很容易的理解这些 URI 的含义,可是基于公约的方式并不能很方便的解决这个问题。

路由约束

属性路由添加了公约路由时代所没有的约束特性,可让你在路由模版中限制参数被匹配。常规的语法是 {parameter:constraint},例如:

[Route("users/{id:int}"]
public User GetUserById(int id) { ... }

[Route("users/{name}"]
public User GetUserByName(string name) { ... }

若是 URI 的 id 片断是一个 int 类型的,那么第一个路由将会被选择,不然第二个路由将会被选择。属性路由约定特殊规则的路由优先匹配,最后才匹配没有任何约束的路由。注意不要出现两种可能的匹配,不然会出现多匹配的问题,好比:

[Route("{id:int}")]
public string Get(int id)

[Route("{id:decimal}")]
public string Get(decimal id)

这里须要注意的是,WebApi 框架有一个 Bug,不支持小数点,好比:/values/v1/8.3 将不会被解析成 decimal 类型。

下面是被支持的约束列表:

约束 描述 用法演示
bool 类型匹配(Boolean 类型)
datetime 类型匹配(DateTime 类型) {x:datetime8}
decimal 类型匹配(Decimal 类型) {x:decimal9}
double 类型匹配(64 位浮点数) {x:double9}
float 类型匹配(32 位浮点数)
guid 类型匹配(Guid)
int 类型匹配(32 位整数)
long 类型匹配(64 位整数)
alpha 字符组成(必须由拉丁字母组成)
regex 字符组成(必须与指定的正则表达式匹配)
max 值范围(小于或等于指定的最大值)
min 值范围(大于或等于指定的最小值)
range 值范围(在指定的最小值和最大值之间)
maxlength 字符串最大长度(小于或等于指定的长度)
minlength 字符串最小长度(大于或等于指定的长度)
length 字符串长度(等于指定的长度或者长度在指定的范围内)

客户端缓存报头

基础知识

什么是 Last-Modified

在浏览器第一次请求某一个 URL 时,服务器端的返回状态会是 200,内容是你请求的资源,同时有一个 Last-Modified 的属性标记此文件在服务期端最后被修改的时间,格式相似这样:

Last-Modified: Fri, 12 May 2006 18:53:33 GMT

客户端第二次请求此 URL 时,根据 HTTP 协议的规定,浏览器会向服务器传送 If-Modified-Since 报头,询问该时间以后文件是否有被修改过:

If-Modified-Since: Fri, 12 May 2006 18:53:33 GMT

若是服务器端的资源没有变化,则自动返回 HTTP 304(Not Modified)状态码,内容为空,这样就节省了传输数据量。当服务器端代码发生改变或者重启服务器时,则从新发出资源,返回和第一次请求时相似。从而保证不向客户端重复发出资源,也保证当服务器有变化时,客户端可以获得最新的资源。

什么是 ETag

HTTP 协议规格说明定义 ETag被请求变量的实体值;另外一种说法是,ETag 是一个能够与 Web 资源关联的记号(Token):典型的 Web 资源能够一个 HTML 页,但也多是 JSON 或 XML 文档。服务器单独负责判断记号是什么及其含义,并在 HTTP 响应头中将其传送到客户端,如下是服务器端返回的格式:

ETag: W/"9e10cdada3f741f6b0802ee31179837d"

客户端的查询更新格式是这样的:

If-None-Match: W/"9e10cdada3f741f6b0802ee31179837d"

若是 ETag 没改变,则返回状态码 304 内容不返回,这也和 Last-Modified 同样。本人测试 ETag 主要在断点下载时比较有用。

Last-ModifiedETag 如何帮助提升性能?

聪明的开发者会把 Last-ModifiedETag 跟请求的 HTTP 报头一块儿使用,这样可利用客户端(例如浏览器)的缓存。由于服务器首先产生 Last-Modified/ETag 标记,服务器可在稍后使用它来判断页面是否已经被修改。本质上,客户端经过将该记号传回服务器要求服务器验证其(客户端)缓存。过程以下:

  1. 客户端请求一个页面(A)。
  2. 服务器返回页面A,并在给A加上一个 Last-Modified/ETag
  3. 客户端展示该页面,并将页面连同 Last-Modified/ETag 一块儿缓存。
  4. 客户再次请求页面A,并将上次请求时服务器返回的 Last-Modified/ETag 一块儿传递给服务器。
  5. 服务器检查该 Last-ModifiedETag,并判断出该页面自上次客户端请求以后还未被修改,直接返回状态码 304 和一个空的响应体。

这里的客户端通常指浏览器,经过编程方式使用的客户端,通常不会处理这两个 HTTP 请求头。

微服务

目前不作深刻讨论。


  1. SOAP(Simple Object Access Protocol)简单对象访问协议,是交换数据的一种协议规范,是一种轻量的、简单的、基于XML(标准通用标记语言下的一个子集)的协议,它被设计成在WEB上交换结构化的和固化的信息。 

  2. PV:(Page View)即页面浏览量,一般是衡量一个网络新闻频道或网站甚至一条网络新闻的主要指标。网页浏览数是评价网站流量最经常使用的指标之一,简称为 PV。监测网站 PV 的变化趋势和分析其变化缘由是不少站长按期要作的工做。Page Views 中的 Page 通常是指普通的 HTML 网页,也包含 PHP、JSP 等动态产生的 HTML 内容。来自浏览器的一次 HTML 内容请求会被看做一个 PV,逐渐累计成为 PV 总数。 

  3. TPS:(Transaction Per Second)每秒钟系统可以处理的交易或事务的数量。它是衡量系统处理能力的重要指标。TPS 是 LoadRunner 中重要的性能参数指标。 

  4. RFC2616:目前该规范已有部分更新。 

  5. 共享锁:相似于读写锁中的读锁。能够多个一块儿读,可是排斥写锁。只有读锁释放后,才能进入写锁。 

  6. 排它锁:相似于读写锁中的写锁。只能一个写,其他的操做都必须等待,直到当前写锁释放后。 

  7. 串行:同一时间只容许一个线程操做,其他线程只能等待完成后,才能继续执行操做。 

  8. datetime 类型的约束,若是采用 / 作分隔符,必须放在最后一个,而且采用 * 前导:{*x:datetime}。目前,也只有这种写法能够跨多个 URI 段。 

  9. decimaldouble 两种数字类型,若是包含小数点将不能被正常解析,目前能够算 WebApi 框架的一个 Bug 。 

相关文章
相关标签/搜索