caoz的心得与分享,只此一家,别无分号.

如何应对并发(2) - 请求合并及异步处理

发布日期:2015-11-18 12:27:58 +0000

先说昨天有人反应的问题


有网友提醒,说数据查询只能用到一个索引,这个表达不精确,只限于单表的查询,而联表查询实际上每个表都可以有其独立的索引被用到。


非常感谢这个提醒,其实呢,这里必须额外解释一下。

我刚工作的时候呢,特别喜欢写复杂的SQL,觉得自己特酷,写出一个复杂的连表查询逻辑感觉智商优越感爆棚,然后还十分得瑟的给人看这SQL写的思路多牛逼。但是工作十多年后呢,慢慢意识到这样其实不对,特别是面对高并发,高处理请求的时候,联表查询所带来的问题不仅仅是效率的问题,更包括分布式,扩展性的问题,后来我们就制定了一个原则,禁止使用联表查询。所以我系列文章里不会提及任何涉及联表查询的优化问题。可能有的朋友会觉得这样是不是有些极端,是的。但是对于应对高并发的业务场景,这一条其实并不是我个人的规定,很多公司和架构师也都有这样的规定。


那么禁止联表查询会带来一系列无法满足查询需求的问题,这个在后面的文章我会提到,在计划中这是第四篇的内容。


另外,我可能是跟草根打交道比较多,加上我自己学数据库和编程基本都是野路子,所以特别理解草根创业者,以及野路子程序员面对技术问题的困境和纠结,实话说,因为一直以来跟草根创业者沟通比较多,在实战中遇到的各种优化问题和处理场景,我敢说要比很多大公司的架构师还要多,但也实话说可能并不是那些大公司所遇到的问题那么深。所以我的风格一直是,让菜鸟能够更容易理解和领悟,达到处理较高量级的性能优化水平。但是从几年前架构师大会上,就总有一些逼格很高的技术人员瞧不起我的路数,觉得太low太没技术含量,这个,我也是承认的,不过,敬请自便吧。


我所提到的几个案例大家可以看到,其实都是非常典型的,使用场景广泛的,一般小公司很可能撞死在里面的案例,昨天还有人留言以前一直被蜘蛛拖死论坛,看了我的文章后才恍然大悟。但是昨天没有完全解读解决方案,请允许我挖个坑,因为饭要一口口吃,我这个系列会逐渐把处理思路一步步贯通。


很多人都知道说,如果查询请求过多,其实可以用内存来做缓存,比如memcache,比如redis,很多缓存方案,但是如果更新请求过多,那么缓存就没法用了。而更新请求往往比查询请求更消耗资源,这样系统i/o压力就非常大。


当然,这里我要额外说一句,并不是查询请求缓存化了系统效率就会提升,和缓存设计有关,同时缓存的使用也会带来新的风险。


1、如果缓存命中率不高,可能反而是负担

很多人觉得加了缓存就能提高效率,未必。如果缓存命中率不高,查询完缓存没有再去查询数据库,那么实际上是额外开销,只有命中率超过50%,才是有价值的缓存。


2、如果缓存设计不合理,系统开销只会更高

这个我们自己也遇到过,比如有些人喜欢把大量记录扔在缓存里,一条记录可能内容几百k甚至几兆,但是在我们用的时候可能只用到这个记录的某几个字段,这时候通过缓存去读取记录然后再从记录中拆解到这个字段的开销,比直接从数据库中读取这个记录的指定字段,系统开销要大不少。导致你需要更多的缓存服务器,当然,好处是数据库毕竟压力降了(数据库分布式比缓存服务器分布式设计上要复杂一些),但是对于我这样的抠门屌丝程序员,这种开销我是不能忍受的,有钱任性的人可以无视。


3、雪崩效应的风险,

缓存使用分两种,一种是只通过缓存调用,系统后台定时更新的,如果数据丢失或损坏无需从数据库读取;另一种是先从缓存查询,如果没有符合的记录再找数据库,那么就存在这样的风险,一旦缓存内容被重置或缓存服务器出现问题导致大量内容丢失,那么所有请求全部回源,数据库瞬间过载崩溃,导致系统架构响应崩溃。


所以,缓存设计也是一门重要的学问,然而,这部分,很抱歉,不展开。


部分内容明天可能会说一下。


今天说的重点是,关于更新请求,是不是真的不能缓存呢?其实不是。


这就是今天要明确的,请求合并,异步处理。

第一,请求合并。

先说个极端案例,以前有个挺不错的技术,但是早期接触数据库不多,刚开始做服务端的时候,设计了一套框架,然后用框架来实现业务逻辑,但是后来做性能压测就不行,我当时帮他分析,一下子就发现问题了。


一个游戏角色,设置了一个操作对象,那么比如说角色生命的增加或减少,是一个方法,经验的增加或减少,是一个方法,金钱的增加和减少,是一个方法,以此类推, 那么游戏角色pk后,很多数值发生了改变,就依次执行这些方法,这不挺正常的逻辑么?但是我们看到的是,对同一个数据表同一条记录的不同字段,执行了多次更新操作,这些请求就是没必要的,完全可以合并成一条update语句。


再说个常见初级程序员易犯错误,比如要列一个列表,显示符合条件的图书目录和作者信息,那么这人设置了如下方法,book.search(条件), book.read(id),先执行book.search,返回所有符合条件的图书id列表,然后循环执行book.read,读取所需要的作者信息,问题就来了,先执行了一个查询,然后在循环中不断执行查询操作。而实际上我们知道,其实一条SQL就解决了。


以上两个案例都来源于一种思考习惯,就是我们常见的使用框架,使用面向对象的开发方式,这种方式当然优点多多,但在涉及性能优化的场合,往往其中存在大量的重复逻辑和冗余请求,往往很多可以合并的操作没有合并,很多程序员习惯用这样的方式思考,当然你说协同方便,开发效率高(其实我觉得也未必),但是调优成本就高很多了。 我可能属于上古时期学编程的,面向对象的思路一直不太灵光,所以我写程序的时候偏重于面向过程,缺点就是写出来的东西很low很让某些人不齿,优点就是调优的时候往往看的更清楚。


以上这两个案例都是指在同一个用户操作行为中程序员编码不注意出现的重复请求操作,都是面向对象的编程中容易犯的错误,但是稍微有一些经验的程序员应该都能避免。


下面说另一类常见问题,就是不同用户操作行为中出现的类重复请求操作,是否可以合并呢?答案是,其实也可以,这就是今天说的第二点,异步处理。


常见案例,一个论坛,帖子页,用户每访问一次,就要 update post set views=views+1 where postid = $postid;一个热门论坛一天访问几百万次,上千万次,这个update操作就会执行几百万次,上千万次,别忘了这个post表又是访问请求最高的,会不会锁死?会不会响应不过来?


第二个常见案例,还是一个社区,用户每次刷新页面,每次访问,都要记录 update users set lastact =$now where userid=$uid; 为了记录这个用户是否一致活跃及最后活跃的时间,(展示在社区中可以提高社区的活跃度,提高用户间交流的成功率),那么这个网站登录用户每天访问了多少pv,这个更新就执行了多少次。而users表显然也是一个高频率的查询需求的表。


那么这两个案例,有优化空间么?


其实有,而且很简单,这两个数据,其实你说实时性需要是不是那么高,是不是每个请求都必须立即处理,实际上并不一定,但是我还是希望处理更快一些,因为毕竟希望别人看到这个帖子的访问数,以及别人看到这个用户的最后时间,是非常接近的,而不是很久之后才处理的。那么怎么处理呢?就是当发生这样的行为的时候,把这个行为写到缓存里,在缓存里维持一个队列,最好用队列方式,(如果用memcache,数组的下标用increment方法,否则高访问量可能会导致数据覆盖,不展开解释了),然后后台启用一个cron任务,每分钟执行,把队列里的数据拿出来,


案例1,对同一个帖子的views做汇总。(热门帖子往往点击特别频繁)

案例2,对同一个用户最后活跃时间的更新请求,只保留最后一条。

实测数据,越是火爆的社区,合并率越高,更新请求可以合并掉70%左右。异步更新的延迟时间不超过1分钟。如果延时加长,比如2分钟一执行,或5分钟一执行,合并率效率会更高,但是可能导致用户体验下降。


以上就是今天要说的,请求合并和异步更新,这里注意的是,异步更新的内容,属于“丢了其实关系也不大”的数据,如果是非常核心的数据,异步更新要注意数据丢失的危险。


那么肯定有人会问,我用了一个开源系统,我怎么知道哪些可以合并,哪些不能?


下面继续讲方法论,就是你对一个毫不熟悉的系统,如何快速分析其冗余请求的构成和合并的可能性,以及合并可能带来的开销降低呢?


之前有个朋友的公司,几年前做社交游戏的时候,腾讯合作,腾讯一推用户数咔嚓就上去了,然后后台就有点撑不住,请我过去看看,那么,对他们的游戏的产品,怎么开发的,代码怎么写的,我肯定是毫不知情,就是突然叫过去来分析,这怎么分析呢?

慢查询日志肯定是要看的。

昨天讲的 去数据库里,先show processlist;看到有疑问的SQL,去explain,然后set profiling=1;大家回忆一下,看看索引是不是对的,看看哪些SQL本身是有问题的。这些不赘述了。


下面,重点是,一般大家都会把数据查询封装成一个类对吧,让他们从这个类里加一段代码,干嘛呢,输出都执行了哪些SQL。(每秒请求非常高,所以增加日志的i/o压力也很大,为了避免线上业务受影响,采用抽样输出,比如先算个随机数,符合什么数的才输出,然后根据抽样比例反推请求规模,输出结果存到 /dev/shm 目录下,为什么是这个目录,自己想一下。)


打开日志我看什么呢?

第一,看查询和更新的比例。

第二,看最多查询的数据表有哪些,最多更新的数据表有哪些。

第三,看最多查询的数据表最多查询的SQL是什么样子的,最多更新的数据表最多执行更新的SQL是怎样的,算出各自每秒的请求频率。

第四,关键分析,最多查询的SQL,基于同一主键查询的比例多不多(潜台词,可以缓存化)。最多更新的SQL,基于同一主键的更新的比例高不高(潜台词,可以合并请求,异步处理,当然必须根据具体业务诉求再核对一遍)


以上的操作并不需要额外编程或复杂的处理,首先用眼睛看日志找规律,其次基于规律用grep 来统计。 然后把内容整理后,询问相关的程序员,每条问题SQL的业务逻辑是什么,然后毕竟还是要让他们一线的程序员来评估业务逻辑上这些操作是否可以合并,缓存,或者异步处理。 但我想说的是,通过这种分析方式,很多非常重复的查询,非常重复的更新请求可以快速定位,即便是一个陌生系统,也可以快速找到症结所在,掌握这一种分析方法,你对系统性能优化的理解和处理能力,就会上升一个台阶。




其实,其实根据两个多月运营公众号的观察和分析,我发现,写技术文章挺受累不讨好的,转发也不多,赞赏也不多,也不太容易拉粉; 公众号运营有一个规律,和知乎上皮去获取赞的规律一样,写实不如写虚,写内容不如写立场。


真的,但是我还是想把这个系列写下去。


这么多文章,我总结两条分享给大家,一是思考方式,二是分析方法,不论是做产品,做运营,创业,做投资,还是做技术。正确的思考方式,正确的分析方法,是最重要的。