要分享的议题
- 性能提高:在访问量逐渐增大的同时,如何增大单台服务器的 PV2 上限,增长 TPS3 ?
- RESTful:相较于传统的 SOAP1,RESTful 风格架构有哪些优势?作法有哪些区别?
- 微服务:随着企业愈来愈大,系统会愈来愈大,愈来愈难维护,如何在保证“稳”的同时,还保证有小企业的“灵活”?
简要的介绍
性能提高
最经常使用的性能提升方式能够经过使用服务器的集群来解决,简单粗暴的理解就是增长银行柜员的数量。可是,一味的只考虑从服务端提供性能,并非聪明的作法 —— 应该讲求性价比。固然,核心必须是提升服务器的 TPS,即在最短的时间内给最多的客户提供服务。服务器集群能够大幅提高总体的性能,可是咱们要讨论的是如何提高单台服务器的性能。javascript
- 服务器的压力主要来源于三个方面:CPU、网络和磁盘 IO。磁盘做为最容易达到瓶颈的一方,必须想办法减小 IO 操做。数据库做为数据持久性存储、磁盘开销的大户,这里主要就是要减小或合并数据库操做。
- 系统的流畅性取决于服务端和客户端的良好配合。网站类的项目,充分利用浏览器资源,不只能下降服务器压力,还能提供更好地客户体验。现代化的浏览器,通常都符合 RFC26164 规范。其中很重要的报头有:ETag 和 Last-Modified 报头 —— 浏览器的缓存设置开关,能够最大限度的利用客户端资源。
RESTful 架构风格
比如面向过程编程和面向对象编程,这二者并无明确的界限。在适当的地方用适当风格的架构,重点是物尽其用。但 RESTful 做为新兴的风格,必然有其优点:html
- 在 RESTful 架构中,关注点在于资源。每一个都有一个地址,资源自己就是方法调用的目标。方法列表对全部资源都是同样的。这些方法都是标准方法,包括 HTTP GET、POST、PUT、DELETE,还可能包括 PATCH、HEADER 和 OPTIONS。其指导思想是远端提供了一系列资源,客户端须要下载、展示、编辑和提交更改,重点放在本地。
- 在 RPC 架构中,关注点在于方法。在客户端看来,就是在客户端组合条件,而后在服务器中执行,最终再反馈给客户端。其指导思想是隐藏实现细节,或者关联其它 RPC 服务运算,重点放在了服务器。
微服务和单体式应用
现代化的单体式应用,一般采用模块化的方式,围绕核心模块并行开发。最终他们须要联合测试,部署成一个单体式的应用:C# 会部署成 IIS 的一个网站,Java 会打包成 War 格式部署 Tomcat 上。java
随着时间的推移,单体式在应对愈来愈多的新需求后,会变得愈来愈大。更不幸的是,由于公司资源和需求的不对等,许多仓促应对的代码会添加到应用中。这些代码在短时间内不会出现问题,可是修正 Bug 和正常的新功能添加会变得愈来愈困难,由于一般会涉及到多个模块,牵一发而动全身。此时就是单体式应用的瓶颈期,会考虑拆分红多个子系统。固然,这将会再维持一段时间,直到再出现类似的问题。ios
许多公司,好比 Amazon、eBay 和 NetFlix,经过采用微处理结构模式解决了上述问题。其思路不是开发一个巨大的单体式的应用,而是将应用分解为小的、互相链接的微服务。web
一个微服务通常完成某个特定的功能,好比下单管理、客户管理等等。每个微服务都有本身的业务逻辑和适配器。一些微服务还会发布 Api 给其它微服务和应用客户端使用。其它微服务完成一个 Web UI。正则表达式
性能提高
- 数据库静态化:数据库不包含运算逻辑,全部运算逻辑在程序内完成。
- 减小外部 IO:使用数据缓存、合并数据库操做、读写分离。
- 异步化:对于非必须的方法,异步执行使其不影响当前逻辑。
- 子系统拆分:拆分长时间运行的逻辑为 Windows 服务或 Job 。
一个栗子:电子商务系统,下单操做起始涉及到了对多个模块的调用。可是用户下单的时候,并不关心这些,只要获得一个下单成功的结果就能够了。咱们能够分析一下:系统首先要对用户提交的信息有效性校验,再就是业务数据准确性校验,最后提交到数据库。一个成功的电商系统,前二者必须能在很短的时间内完成,而且在秒杀特卖这种场景时不会形成数据库的崩溃。数据库
基于以上两点,咱们分析下如何优化秒杀特卖这种场景下的操做流程。编程
- 服务端对信息有效性的校验,操做频率最密集、速度要最快,因此不该该涉及除内存运算以外的操做,好比:Redis 和数据库读写、TCP/HTTP 远程调用等。
- 业务性数据校验,关联模块不少、速度要求较快,因此不该涉及慢速的 IO 操做,好比:数据库读写、HTTP 远程调用等。
- 写数据库频繁,在较短的时间内给数据库形成很大的持续压力、速度要求很快,因此这里能够采用当即反馈,稍后写入的方式执行。
数据库静态化
数据库的操做都是有锁的:Select 语句发布共享锁5,Insert、Update 和 Delete 发布排它锁6。因此说在操做同一张表的前提下,数据库操做都是串行7的。api
基于以上考虑,让数据库只作存储容器,不负责运算才是正途。正是由于数据库的操做是串行的,在大并发量写入时,任何一点的提高都是要争取的,因此这里要把运算的任务提到程序中执行。浏览器
- 存储过程由于把程序逻辑放在数据库,通常来讲确定包含运算任务,考虑通常开发的水平不能保证先用临时表存储预先计算好的数据(能作到也太繁琐了,很容易出现异常),最后再统一执行,因此首先要摒弃包含数据库写入类的存储过程。
- 程序内的运算,若是是在开启事务后仍然存在,也要算入数据库的运算任务。由于数据库事务开启后,独占的串行已经开始了,程序的运算时间不只占用了程序的运算时间,还占用了数据库事务的开启时长,在本质上并无减小数据库事务的开启时长。严格来算的话,这种作法甚至还不如上一条的作法优化。
因此,真正的数据库静态化是:首先在程序内运算,产生数据库要执行的 SQL 写入语句和参数,持续运算直到产生全部的数据库写入命令;再开启数据库事务,按照先进先出原则顺序连续执行数据库写入命令(此段时间内不能包含其它非数据库运算)。只有这样,才能保证命令的执行都是静态化的写入,而且锁定数据库的时间最短,保证最大化的下降数据库压力。
另一个,正是由于数据库的操做是串行的,因此在执行数据库写入的状况下,是不能读取的,要避免出现脏读,数据库的读写分离就颇有必要了。创建从库,由主库负责写入,从库负责读取,将数据库的压力均分到多台上。
数据库的读写分离要注意:刚写入数据库的数据,同步到从库须要 2 到 3 秒的时间,须要在业务上更改流程,以便于在用户检索时数据已同步。
减小外部 IO
因为磁盘的限制,其读写速度和内存不成比例,因此这里是第二个可能出现瓶颈的地方。能够考虑将配置信息预先读取到缓存的方式解决。
由于数据库是依托于硬盘而存在的,因此数据库的读写相对于有效性验证和业务验证来讲,是时间消耗大户。在单纯的考虑数据库写入的状况下,能够从系统内剥离订单的数据库写入业务。一来能够省掉无心义等待数据库写入的时间;二来能够减小 CPU 时间片的占用,将时间
另外一种是数据库的读写操做,在一个数据库事务内,是不能有第二个数据库执行相同的操做的。考虑到数据静态化中的介绍,数据库事务开启后,程序内的运算其实也是数据库的运算时间的。此时能够考虑推迟数据库事务的开启时间:首先在程序内运算产生要执行的数据库命令,再开启数据库事务,在连续的时间内执行批量执行数据库事务。即合并数据库操做到一次数据库执行中。
异步化
在开发的过程当中,不可避免的要和其关联的模块交互,而这些交互并不会对当前的业务逻辑产生影响。这种操做就应该改为异步的方式。在数据库交互的过程当中,若是用户不须要等待数据库的返回值,还能够将数据库执行异步化,在最短的时间内反馈执行结果。
一个栗子:用户下单的过程当中,提交订单的操做,其实并不关心提交成功失败,只是在后续跳转到订单详情的时候才会看到订单详情。这个流程就能够将数据库执行异步化,在服务器接收到用户的提交请求时,能够在校验数据后,直接反馈提交成功的响应给用户。接下来,经过异步队列的方式,保存到数据库。客户端在接收到提交成功的反馈后,提示用户提交成功,可是不给出订单的任何信息。用户只有主动点击了查看订单列表,才会执行数据库查询。这个时间差足够系统处理订单的真正提交操做了。
这样作最直接的好处是提升了网站的响应速度,优化了用户体验,在提高服务器 TPS 的同时,尚未提高数据库的压力。在秒杀特卖时,可以最大限度的避免超卖的状况。
子系统拆分
继续上面的例子,数据库的提交订单操做,和网站并无多少的关系。此时就能够考虑到将这一部分拆分出来,作成一个 Windows 服务,二者经过消息队列的方式通信。队列的串行读取正好符合了数据库的串行执行,在高峰时段也没有超过数据库的极限,形成宕机的状况。在超过服务极限的状况下,处理慢比不能处理老是要好的。
从操做系统上来讲,系统调度针对每一个进程都是平等的。此处将数据库执行操做从网站拆分出来,减小了数据库操做对 CPU 时间片的占用,侧面提高了网站的服务能力。
RESTful 架构风格与 SOAP 架构风格
- 属性路由:使用渐进式的 URI 替代传统平板式的方法名称 URI。
- 客户端缓存报头:使用 ETag 和 Last-Modified 报头减轻服务器压力。
- 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/products
和 api/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-Modified
和 ETag
如何帮助提升性能?
聪明的开发者会把 Last-Modified
和 ETag
跟请求的 HTTP 报头一块儿使用,这样可利用客户端(例如浏览器)的缓存。由于服务器首先产生 Last-Modified
/ETag
标记,服务器可在稍后使用它来判断页面是否已经被修改。本质上,客户端经过将该记号传回服务器要求服务器验证其(客户端)缓存。过程以下:
- 客户端请求一个页面(A)。
- 服务器返回页面A,并在给A加上一个
Last-Modified
/ETag
。 - 客户端展示该页面,并将页面连同
Last-Modified
/ETag
一块儿缓存。 - 客户再次请求页面A,并将上次请求时服务器返回的
Last-Modified
/ETag
一块儿传递给服务器。 - 服务器检查该
Last-Modified
或ETag
,并判断出该页面自上次客户端请求以后还未被修改,直接返回状态码 304 和一个空的响应体。
这里的客户端通常指浏览器,经过编程方式使用的客户端,通常不会处理这两个 HTTP 请求头。
微服务
目前不作深刻讨论。
-
SOAP(Simple Object Access Protocol)简单对象访问协议,是交换数据的一种协议规范,是一种轻量的、简单的、基于XML(标准通用标记语言下的一个子集)的协议,它被设计成在WEB上交换结构化的和固化的信息。 ↩
-
PV:(Page View)即页面浏览量,一般是衡量一个网络新闻频道或网站甚至一条网络新闻的主要指标。网页浏览数是评价网站流量最经常使用的指标之一,简称为 PV。监测网站 PV 的变化趋势和分析其变化缘由是不少站长按期要作的工做。Page Views 中的 Page 通常是指普通的 HTML 网页,也包含 PHP、JSP 等动态产生的 HTML 内容。来自浏览器的一次 HTML 内容请求会被看做一个 PV,逐渐累计成为 PV 总数。 ↩
-
TPS:(Transaction Per Second)每秒钟系统可以处理的交易或事务的数量。它是衡量系统处理能力的重要指标。TPS 是 LoadRunner 中重要的性能参数指标。 ↩
-
RFC2616:目前该规范已有部分更新。 ↩
-
共享锁:相似于读写锁中的读锁。能够多个一块儿读,可是排斥写锁。只有读锁释放后,才能进入写锁。 ↩
-
排它锁:相似于读写锁中的写锁。只能一个写,其他的操做都必须等待,直到当前写锁释放后。 ↩
-
串行:同一时间只容许一个线程操做,其他线程只能等待完成后,才能继续执行操做。 ↩
-
datetime 类型的约束,若是采用
/
作分隔符,必须放在最后一个,而且采用*
前导:{*x:datetime}
。目前,也只有这种写法能够跨多个 URI 段。 ↩ -
decimal
和double
两种数字类型,若是包含小数点将不能被正常解析,目前能够算 WebApi 框架的一个 Bug 。 ↩