干货分享丨达观数据提升 Web服务端性能的技术经验

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

作者 / 张弸中,达观数据后端工程师

收录 / InfoQ

前言

作为直接面对来自客户请求的web服务端,无疑是要同时承受更多的请求,并为用户提供更好的体验。这个时候web端的性能常常会成为业务发展的瓶颈,提升性能刻不容缓。

达观数据在开发过程中总结了一些提升web服务端性能的经验,与大家分享。

问题分析

对于web服务端性能,首先我们分析一下相关指标。

从用户角度讲,用户调用web服务时,请求返回时间越短,用户体验越好。

从服务端角度讲,同一时间能承载用户请求量越大,服务端性能就越强。

综合两方面,我们总结性能优化的两个方向:

  • 增加服务端所能支撑并发请求的最大数量;
  • 提高每个请求处理速度。

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

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

如图1所示,为了说明方便,我们来举个实际的例子:

LVS(Keepalived)->Squid->Nginx->GO->MySQL

图1:服务端架构

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

(达观数据 http://www.datagrand.com/ 张弸中)

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

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

问题

大部分Web请求都是阻塞性质的,当一个请求被处理时,进程就会被挂起(占用cpu)直至请求完成。

在大多数情况下,Web请求完成的足够快,所以这个问题并不被关注。

然而,对于那些响应时间来完成的请求(像返回数据量大的请求或外部API),这意味着应用程序被锁定直至处理结束,这期间,其他的请求不会被处理,很明显,这些无效的等待时间浪费掉了,并且占用系统资源,严重的影响了我们可以负担的并发请求的数量。

解决办法

web服务端在等待上一个请求处理的过程中,我们可以让I/O循环打开以便处理其他应用请求,直到处理完成时启动一个请求并给予反馈,而不再是等待请求完成的过程中挂起进程。

这样,我们可以节省一些没有必要的等待时间,用这些时间去处理更多的请求,这样我们就可以大大增加请求的吞吐量,也就是在宏观上提高了我们可处理的并发请求数。

例子

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

场景:我们构建一个向远端(某个十分稳定的网站)发送HTTP请求的简单Web应用。

这期间,网络传输稳定,我们不考虑网络来带的影响。

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

图2:阻塞式响应

如图2,我们可以很容易看出,这里的问题是无论每个请求自身返回多么快,服务器对远端的访问请求往返都会产生足够大的滞后,因为进程直到请求完成并且数据被处理前都一直处于强制挂起状态。当一两个请求时这还不是一个问题,但达到100个(甚至10个)用户时,这意味着整体变慢。

如图,不到10秒时间10个相似用户的平均响应时间达到了1.99秒,共计29次。这个例子只展示了非常简单的逻辑。

如果你要添加其他业务逻辑或数据库的调用的话,结果会更糟糕。增加更多的用户请求时,同时可被处理的请求就会增长缓慢,甚至有些请求会发生超时或失败。

(达观数据 http://www.datagrand.com/ 张弸中)

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

图3:非阻塞式响应

正如你在图3中所看到的,我们从每秒3.20个事务提升到了12.59,在相同的时间内总共提供了118次请求。

这真是一个非常大的改善!正如你所想象的,随着用户请求增多和测试时间增长时,它将能够提供更多连接,并且不会遇到上面版本遭受的变慢的问题。从而稳定的提高了可负载的并发请求数。

先来介绍一下基础知识:

  • 一个应用程序是运行在机器上的一个进程
  • 进程是一个运行在自己内存地址空间里的独立执行体
  • 一个进程由一个或多个操作系统线程组成,这些线程其实是共享同一个内存地址空间的一起工作的执行体

问题

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

解决方法

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

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

其中最主要的问题是内存中的数据共享,它们会被多线程以无法预知的方式进行操作,导致一些无法重现或者随机的结果(称作 竞态)。

所以这个经典的方法明显不再适合现代多核/多处理器编程:thread-per-connection 模型不够有效。在诸多比较合适的范式中,有个被称作 CommunicatingSequential Processes(顺序通信处理)(CSP, C. Hoare 发明的)还有一个叫做 message passing-model(消息传递)(已经运用在了其他语言中,比如Erlang)。

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

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

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

例子

这里用GO语言中的goroutine来具体说明。

在go语言中,应用程序并发处理的部分被称作 goroutines(协程),它可以进行更有效的并发运算。在协程和操作系统线程之间并无一对一的关系:协程是根据一个或多个线程的可用性,映射(多路复用,执行于)在他们之上的;协程调度器在 Go 运行时很好的完成了这个工作。协程是轻量的,比线程更轻。

它们痕迹非常不明显(使用少量的内存和资源):使用 4K 的栈内存就可以在堆中创建它们。因为创建非常廉价,必要的时候可以轻松创建并运行大量的协程(在同一个地址空间中 100,000个连续的协程)。并且它们对栈进行了分割,从而动态的增加(或缩减)内存的使用;栈的管理是自动的,但不是由垃圾回收器管理的,而是在协程退出后自动释放。协程可以运行在多个操作系统线程之间,也可以运行在线程之内,让你可以很小的内存占用就可以处理大量的任务。

由于操作系统线程上的协程时间片,你可以使用少量的操作系统线程就能拥有任意多个提供服务的协程,而且 Go 运行时可以聪明的意识到哪些协程被阻塞了,暂时搁置它们并处理其他协程。甚至,程序可以在不同的处理器和计算机上同时执行不同的代码段。

我们通常想将一个长计算过程切分成几块,然后让每个goroutine各自负责一块工作,这样对于单一请求的响应时间有成倍的提升。

举个例子,有一个任务分3个阶段,a阶段去数据库a中取数据,b阶段去数据库b中取数据,c阶段合并数据返回。我们启动goroutine以后ab阶段可以一起进行,极大地缩短了响应时间。

说白了就是部分计算过程由串行转换为并行,一个任务不需要等待其他无关的任务执行完在执行,实际计算中程序的并行执行会更有用处。

关于这部分佐证的数据就不在这边过多叙述了,感兴趣的同学可以自己看一下这方面的资料。比如web服务端由ruby切换为go性能提升15倍的老故事(Ruby使用的是绿色线程,即只有一个CPU得到利用)。虽然这个故事可能有点夸大,但是并行带来的性能提升是毫无疑问的。

(Ruby切换为go:http://www.vaikan.com/how-we-went-from-30-servers-to-2-go/)。

问题

磁盘读取数据靠的是机械运动,每次读取数据花费的时间可以分为寻道时间旋转延迟传输时间三个部分:

  • 寻道时间指的是磁臂移动到指定磁道所需要的时间,主流磁盘一般在5ms以下;
  • 旋转延迟就是我们经常听说的磁盘转速,比如一个磁盘7200转,表示每分钟能转7200次,也就是说1秒钟能转120次,旋转延迟就是1/120/2 = 4.17ms;
  • 传输时间指的是从磁盘读出或将数据写入磁盘的时间,一般在零点几毫秒,相对于前两个时间可以忽略不计。

那么访问一次磁盘的时间,即一次磁盘IO的时间约等于5+4.17 = 9ms左右,听起来还挺不错的,但要知道一台500 -MIPS的机器每秒可以执行5亿条指令,因为指令依靠的是电的性质,换句话说执行一次IO的时间可以执行40万条指令,数据库动辄十万百万乃至千万级数据,每次9毫秒的时间,显然是个灾难。

解决办法

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

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

随着业务开发模式的变化,敏捷式开发被越来越多的团队采用,周期越来越短,很多数据库查询语句都是按照业务逻辑来写,时间久了常常就忽略了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’);
  5. 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如: 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请求,上面列举出了几个经常出现的问题,希望对阅读的各位有所帮助,当然如果有不同的想法欢迎一起来探讨。

达观数据专注于企业大数据技术服务,以独创的多层细智能挖掘算法,实现对海量用户行为和文本数据的深入分析和挖掘,为企业提供智能文本分析精准用户行为建模个性化推荐智能搜索等尖端数据挖掘功能。

作者介绍

张弸中,南京航空航天大学测控专业,现任达观数据后端工程师。拥有丰富的后端架构及相关技术应用经验,对物联网TCP通讯、电商平台(B2B、B2C、CRM、ERP)等相关领域有深刻的实践。

原文发布于微信公众号 - 达观数据(Datagrand_)

原文发表时间:2017-02-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏帘卷西风的专栏

关于cocos2dx 3.0升级崩溃报错(unable to load native library) 和(Fatal signal 11 (SIGSEGV) at 0x00000000)

         最近一直在Windows平台开发cocos-2dx游戏,期间做了一次引擎升级,升级到了3.0正式版本。Windows平台上表现很正常,没有出现...

862
来自专栏Java架构

Java程序员如何运用所掌握的技术构建一个完整的业务架构

创业之初,我们往往会为了快速迭代出产品,而选择最简单的技术架构,比如LAMP架构,SSH三层架构。这些架构可以适应初期业务的快速发展,但是,随着业务变得越来越复...

2053
来自专栏web前端教室

前几天有个同学问我,“什么是响应式编程”?另,它和函数式编程有啥区别?

前几天有个同学问我,啥叫响应式编程?当时我正在讲课没顾得上回他。今天晚上仔细写个文章回复他,顺便我自己也学习一下。 响应式编程的英文名,Reactive Pro...

2606
来自专栏北京马哥教育

用 Python 实现每秒处理 120 万次 HTTP 请求

学Python最简单的方法是什么?推荐阅读:30万年薪Python开发工程师成长魔法 用 Python 做到每秒处理上百万次 HTTP 请求,可能吗?也许不能,...

3766
来自专栏数据之美

CPU 100% 异常排查实践与总结

1、问题背景 昨天下午突然收到运维邮件报警,显示数据平台服务器cpu利用率达到了98.94%,而且最近一段时间一直持续在70%以上,看起来像是硬件资源到瓶颈需要...

4508
来自专栏贾老师の博客

雪崩与过载保护

2486
来自专栏Java架构沉思录

MongoDB的正确使用姿势

MongoDB是一个非常有前途的数据库,MongoDB官方对自己的定位是通用数据库,其实这个定位跟MySQL有些像。虽其流行度还远未达到MySQL的水平,但笔者...

1952
来自专栏java一日一条

如何构建一个较为通用的业务技术架构

创业之初,我们往往会为了快速迭代出产品,而选择最简单的技术架构,比如LAMP架构,SSH三层架构。这些架构可以适应初期业务的快速发展,但是,随着业务变得越来越复...

1161
来自专栏Bug生活2048

Ghost博客评论、统计、打赏接入

首先是选择问题,对于后端的我来说,在ghost基础赏进行二次开发显然有点难度,只能选择第三方插件来丰富自己的代码。

4413
来自专栏Java架构

Java程序员如何运用所掌握的技术构建一个完整的业务架构

1595

扫码关注云+社区

领取腾讯云代金券