随着互联网的不断发展,日常生活中越来越多的需求通过网络来实现,从衣食住行到金融教育,从口袋到身份,人们无时无刻不依赖着网络,而且越来越多的人通过网络来完成自己的需求。

作为直接面对来自客户请求的Web服务端,无疑要同时承受更多的请求,并为用户提供更好的体验。这个时候Web端的性能常常会成为业务发展的瓶颈,提升性能刻不容缓。本文作者在开发过程中总结了一些提升Web服务端性能的经验,与大家分享。

问题分析

对于Web服务端性能,首先我们分析一下相关指标。从用户角度讲,用户调用Web服务时,请求返回时间越短,用户体验越好。从服务端角度讲,同一时间能承载用户请求量越大,服务端性能就越强。综合两方面,我们总结性能优化的两个方向:

1. 增加服务端所能支撑并发请求的最大数量;

2. 提高每个请求处理速度。

明确了优化方向,首先介绍一种服务端通常的架构模式,即来自浏览器或者App的Web一个请求,在服务端经过哪几层结构被处理并返回的。

架构模式:IP负载均衡->缓存服务器->反向代理->应用服务器->数据库

如图1所示,为了说明方便,我们来举个实际的例子: LVS(Keepalived)->Squid->nginx->Go->MySQL

图1:服务端架构

我们对请求在每层做分发处理,这样可以使下一级结构有多个分支同时工作,来提高总体的最大并发数。

结合架构,我们来分析通常有哪些问题在拖了性能的后腿,以及找出对应的解决方法。

正常情况下,IP负载均衡,缓存服务器和nginx代理这几层主要是集群稳定性问题。容易出现性能瓶颈的地方往往是应用服务器层和数据库层,我们下面来列举几个例子:

1. 阻塞的影响

(1)问题:

大部分Web请求都是阻塞性质的,当一个请求被处理时,进程就会被挂起(占用CPU)直至请求完成。在大多数情况下,Web请求完成的足够快,所以这个问题并不被关注。然而,对于那些响应时间来完成的请求(像返回数据量大的请求或外部API),这意味着应用程序被锁定直至处理结束,这期间,其他的请求不会被处理,很明显,这些无效的等待时间浪费掉了,并且占用系统资源,严重的影响了我们可以负担的并发请求的数量。

(2)解决办法:

Web服务端在等待上一个请求处理的过程中,我们可以让I/O循环打开以便处理其他应用请求,直到处理完成时启动一个请求并给予反馈,而不再是等待请求完成的过程中挂起进程。这样,我们可以节省一些没有必要的等待时间,用这些时间去处理更多的请求,这样我们就可以大大增加请求的吞吐量,也就是在宏观上提高了我们可处理的并发请求数。

(3)例子

这里我们用Python的一款Web框架Tornado来具体说明改变阻塞方式提高并发性能。

场景:我们构建一个向远端(某个十分稳定的网站)发送HTTP请求的简单Web应用。这期间,网络传输稳定,我们不考虑网络带来的影响。

在这个例子中,我们使用Siege(一款压力测试软件)对服务端在10秒内执行大约10个并发请求。

如图2所示,我们可以很容易看出,这里的问题是无论每个请求自身返回多么快,服务器对远端的访问请求往返都会产生足够大的滞后,因为进程直到请求完成并且数据被处理前都一直处于强制挂起状态。当一两个请求时这还不是一个问题,但达到100个(甚至10个)用户时,这意味着整体变慢。如图,不到10秒时间10个相似用户的平均响应时间达到了1.99秒,共计29次。这个例子只展示了非常简单的逻辑。如果你要添加其他业务逻辑或数据库的调用的话,结果会更糟糕。增加更多的用户请求时,同时可被处理的请求就会增长缓慢,甚至有些请求会发生超时或失败。

图2:阻塞式响应

下面我们用Tornado执行非阻塞的HTTP请求。

如图3所示,我们从每秒3.20个事务提升到了12.59,在相同的时间内总共提供了118次请求。这真是一个非常大的改善!正如你所想象的,随着用户请求增多和测试时间增长时,它将能够提供更多连接,并且不会遇到上面版本遭受的变慢的问题。从而稳定的提高了可负载的并发请求数。

图3:非阻塞式响应

2. 计算效率对响应时间和并发数的影响

先来介绍一下基础知识:一个应用程序是运行在机器上的一个进程;进程是一个运行在自己内存地址空间里的独立执行体。一个进程由一个或多个操作系统线程组成,这些线程其实是共享同一个内存地址空间的一起工作的执行体。

(1)问题

传统计算方式单线程运行,效率低,计算能力弱。

(2)解决办法

一种解决办法就是完全避免使用线程。例如,可以使用多个进程将重担交给操作系统来处理。但是,有个劣势就是,我们必须处理所有进程间通信,通常这比共享内存的并发模型有更多的开销。

另一种办法是用多线程工作,不过,公认的,使用多线程的应用难以做到准确,同步不同的线程,对数据加锁,这样同时就只有一个线程可以变更数据。不过过去的软件开发经验告诉我们这会带来更高的复杂度,更容易使代码出错以及更低的性能。

其中最主要的问题是内存中的数据共享,它们会被多线程以无法预知的方式进行操作,导致一些无法重现或者随机的结果(称作“竞态”)。所以这个经典的方法明显不再适合现代多核/多处理器编程:thread-per-connection 模型不够有效。在诸多比较合适的范式中,有个被称作 Communicating Sequential Processes(顺序通信处理)(CSP, C. Hoare 发明的)还有一个叫做 message-passing-model(消息传递)(已经运用在了其他语言中,比如 Erlang)。

我们这里使用办法是利用并行的架构来处理任务,一个并发程序可以在一个处理器或者内核上使用多个线程来执行任务,但是只有同一个程序在某个时间点同时运行在多核或者多处理器上才是真正的并行。

并行是一种通过使用多处理器以提高速度的能力。所以并发程序可以是并行的,也可以不是。

并行模式可以同时使用多线程、多核、多处理器,甚至多计算机,这无疑可以调动更多资源,从而压缩响应时间,提升运算效率,极大地增强了服务端的性能。

例如java并行框架:Fork/Join框架

3. 磁盘I/O对性能的影响

(1) 问题

磁盘读取数据靠的是机械运动,每次读取数据花费的时间可以分为寻道时间、旋转延迟、传输时间三个部分,寻道时间指的是磁臂移动到指定磁道所需要的时间,主流磁盘一般在5ms以下;旋转延迟就是我们经常听说的磁盘转速,比如一个磁盘7200转,表示每分钟能转7200次,也就是说1秒钟能转120次,旋转延迟就是1/120/2 = 4.17ms;传输时间指的是从磁盘读出或将数据写入磁盘的时间,一般在零点几毫秒,相对于前两个时间可以忽略不计。那么访问一次磁盘的时间,即一次磁盘I/O的时间约等于9ms(5ms+4.17ms)左右,听起来还挺不错的,但要知道一台500 -MIPS的机器每秒可以执行5亿条指令,因为指令依靠的是电的性质,换句话说执行一次I/O的时间可以执行40万条指令,数据库动辄十万百万乃至千万级数据,每次9毫秒的时间,显然是个灾难。

(2) 解决办法

磁盘I/O对服务器性能的影响没有根本的解决办法,除非你把磁盘扔掉,换成别的东西。我们能在网上搜到各种存储介质的响应速度与价格,如果你有钱,你就可以任性的更换存储介质。

在不更换存储介质的条件下,我们可以减少应用程序对磁盘的访问次数,比如设置缓存,还可以把部分磁盘I/O放到请求周期外,比如用队列和栈来处理数据的I/O等。

4. 优化数据库查询

随着业务开发模式的变化,敏捷式开发被越来越多的团队采用,周期越来越短,很多数据库查询语句都是按照业务逻辑来写,时间久了常常就忽略了SQL查询的格式问题,造成数据库压力的增加,使数据库查询的响应变慢。这里简单介绍MySQL数据库中,几条被我们忽略的常见问题和优化方式:

    1. 最左前缀匹配原则,非常重要的原则,MySQL会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

    2. 尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录。

    3. 尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。

    4. 索引列不能参与计算,保持列“干净”,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’);应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用。

    5. 索引而进行全表扫描,如:

      select id from t where num is null

      可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:

      select id from t where num=0

    6. 应尽量避免在 where 子句中使用 or 来链接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:

      select id from t where num=10 or num=20
      可以这样查询:

      
select id from t where num=10
 union all
 select id from t where num=20 

    7. 下面的查询也将导致全表扫描(不能前置百分号):
      
 select id from t where name like ‘%abc%’
      
若要提高效率,可以考虑全文检索。

    8. in 和 not in 也要慎用,否则会导致全表扫描,如:
      
 select id from t where num in(1,2,3)
      
 对于连续的数值,能用 between 就不要用 in 了:

      select id from t where num between 1 and 3

Web服务端性能提升实践的更多相关文章

  1. IOS开发系列之阿堂教程:玩转IPhone客户端和Web服务端交互(客户端)实践

    说到ios的应用开发,我们不能不提到web server服务端,如果没有服务端的支持,ios应用开发就没有多大意义了,因为从事过手机开发的朋友都知道(Android也一样),大量复杂业务的处理和数据库 ...

  2. 关于如何提高Web服务端并发效率的异步编程技术

    最近我研究技术的一个重点是java的多线程开发,在我早期学习java的时候,很多书上把java的多线程开发标榜为简单易用,这个简单易用是以C语言作为参照的,不过我也没有使用过C语言开发过多线程,我只知 ...

  3. 如何提高Web服务端并发效率的异步编程技术

    作为一名web工程师都希望自己做的web应用能被越来越多的人使用,如果我们所做的web应用随着用户的增多而宕机了,那么越来越多的人就会变得越来越少了,为了让我们的web应用能有更多人使用,我们就得提升 ...

  4. web服务端的架构演变

    此文已由作者肖凡授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 最近Lofter项目碰到很多性能上的问题,特别是数据库相关的,每次推送后,告警就会第一时间到来.这些问题随着产 ...

  5. 提高服务端性能的几个socket选项

    提高服务端性能的几个socket选项 在之前的一篇文章中,作者在配置了SO_REUSEPORT选项之后,使得应用的性能提高了数十倍.现在介绍socket选项中如下几个可以提升服务端性能的选项: SO_ ...

  6. Java服务端性能优化

    <Java程序性能优化>说性能优化包含五个层次:设计调优.代码调优.JVM调优.数据库调优.操作系统调优. 常用的几个代码优化方案: 使用单例 对于IO处理.数据库连接.配置文件解析加载等 ...

  7. 我是如何将一个老系统的kafka消费者服务的性能提升近百倍的

    ☞☞☞ 我是如何将一个老系统的kafka消费者服务的性能提升近百倍的 ☜☜☜ ○○○○○○○○○○○○○○○ 大家好,又见面了~ kafka作为一种高吞吐量的分布式发布订阅消息系统,在业务系统中被广泛 ...

  8. winform客户端利用webClient实现与Web服务端的数据传输

    由于项目需要,最近研究了下WebClient的数据传输.关于WebClient介绍网上有很多详细介绍,大概就是利用WebClient可以实现对Internet资源的访问.无外乎客户端发送请求,服务端处 ...

  9. chrome debug 服务端性能

    设置 http header 在 chrome 查看服务端性能 \Yii::$app->getResponse()->headers->set('Server-Timing', 'c ...

随机推荐

  1. python实现base64算法加密

    python本身有base64加密的模块,不过是用C写的,封装成了.so文件,无法查看源码,本着学习的心态,自己实现了一遍,算法 原理参考 浅谈Base64编码算法. 代码如下: # coding:u ...

  2. ZOJ2482 IP Address 2017-04-18 23:11 44人阅读 评论(0) 收藏

    IP Address Time Limit: 2 Seconds      Memory Limit: 65536 KB Suppose you are reading byte streams fr ...

  3. AtCoder - 4351 Median of Medians(二分+线段树求顺序对)

    D - Median of Medians Time limit : 2sec / Memory limit : 1024MB Score : 700 pointsProblem Statement ...

  4. matlab pca基础知识

    PCA的一些基本资料 最近因为最人脸表情识别,提取的gabor特征太多了,所以需要用PCA进行对提取的特征进行降维. 本来最早的时候我没有打算对提取的gabor特征进行降维,但是如果一个图像时64*6 ...

  5. spring的事务传播属性

    一.Propagation (事务的传播属性) Propagation : key属性确定代理应该给哪个方法增加事务行为.这样的属性最重要的部份是传播行为.有以下选项可供使用:PROPAGATION_ ...

  6. 项目笔记---事半功倍之GhostDoc(二)

    前言 前一篇文章<项目笔记---事半功倍之StyleCop(一)>提到如何约束代码,规范代码风格,这一节,我们将了解如何快速生成符合规则的代码注释---GhostDoc 一.安装Ghost ...

  7. linux系统编程之信号(五):信号集操作函数,信号阻塞与未决

    一,信号集及相关操作函数 信号集被定义为一种数据类型: typedef struct { unsigned long sig[_NSIG_WORDS]: } sigset_t 信号集用来描述信号的集合 ...

  8. JPA注解指南

    PA注解持久化类很方便,需要jar包:ejb3-persistence.jar.我用以下三个类来说明用法. @SuppressWarnings("serial") @Entity ...

  9. Asp.net MVC4 记录在线用户数及登录时长

    Global.asax.cs文件 public class MvcApplication : System.Web.HttpApplication { /// <summary> /// ...

  10. django系列5.4--ORM中执行原生SQL语句, Python脚本中调用django环境

    ORM执行原生sql语句 在模型查询API不够用的情况下,我们还可以使用原始的SQL语句进行查询. Django 提供两种方法使用原始SQL进行查询:一种是使用raw()方法,进行原始SQL查询并返回 ...