谈谈编程 之 滥用内存的现象

好吧,今天又双叒叕要提小密圈,不过这次真的不是为了拉粉,而是因为恰好这个场景,在技术设计上很典型。


小密圈春节后爆发很快,那么技术上就遇到了一些瓶颈和问题,吴总就拉了几个好朋友做技术顾问,帮他们参谋一下,有幸我就成了其中之一。


先处理了两个数据库慢查询的问题,证明了我对数据库的索引理解依然还能用的上,不过这不是什么关键问题,因为系统负载和瓶颈不在这里。


那么,在对方的配合下,我们就对程序负载做了抽样的分析,并将一些执行开销略大的脚本负载,通过性能分析工具做了一下拆解,然后注意到,绝大部分开销较大的请求,是用户进入特定圈子首页的脚本,而这里开销最大的部分,是内存的频繁请求。限于信息安全的考虑,对过于细节的技术描述,很抱歉,我不会展开,我只基于这个案例,展开说一下关于比较常见的设计思路误区,和一些设计的思考方法。


通过这个案例我意识到一个现象,其实很多小创业公司都可能遇到的现象,一些技术人员,对系统开销的理解不够彻底,不够扎实,会有一些简单的标签化思维,比如说,数据库往往是性能负载的瓶颈,而使用内存数据存储,可以避开数据库的瓶颈,从而实现性能和响应能力的提升。


这个想法,不能说是错的,因为我们遇到数据库瓶颈的时候,也是通过内存数据存储来优化的,但不能简单化这个问题。并不是说,所有场合,内存存储都比数据库更优越。


以前我举过一个我们自己的案例,我们存储用户session的时候,用了mysql 的heap类型数据表,这是内存表,这个表的存取频率是极高的,但因为写入过于频繁,结果,锁表严重,经常卡死,导致系统因数据库等待被整个卡掉,当然,这可以认为是一个经验不足的菜鸟问题,结果换为innodb,行级锁,问题就解决了。 那么,从内存表换成物理表,看上去是性能下降的,但因为行级锁比表级锁更可靠,在频繁写入的情况下,如果i/o撑得住(请注意这个前提哦),那么实际上可靠性是更优的。


那么回来说,关系型数据库,我们常用的数据库结构,其索引类型,差不多是btree相关的,具体呢,我也不是很精通,在索引优化到位的情况下呢,其查询效率呢,趋近于log2(N)。而一些内存表结构,哈希索引,其查询效率,通常趋近于1,也就是寻址搜索,这个针对你明确的key- value查询,绝对是有优势的,(当然,根据一些信息安全测试的披露,在特殊情况下,可能查询效率会直线下降,但这个不是今天要谈论的重点,就列在这里,以免有人挑错。)


但这里有两个延伸问题


问题1:哈希索引只适用于key - value这样的简单查询,而关系型数据库可以适合较多的条件查询。 也就是哈希索引实际上适用空间是极为有限的。而且也不支持排序等操作。


问题2:并不是所有内存数据表结构都是哈希索引,比如前面提到的mysql 的heap表结构,其实也是关系型数据库,btree类型的索引。那么另一个特别要提到的,是redis有个常见结构,有序集合,因为这个结构支持排序,支持快速的排名统计,所以应用范畴也很广,但是要特别说明,这个表结构,存取的开销是远大于哈希结构的。


有序集合有一个极佳的替代关系型数据库的应用场景,就是积分排名的场景,以前技术大牛云风提到过一个经典案例,某友商的游戏,积分排名挂死数据库,这是很常见的一个问题。


一般使用数据库做积分排名,随便写一个,懂SQL的都能看懂。 select count(*) from gamepoints where points>$mypoints  。得到的统计数据+1,就是我的排名。但这个SQL的开销怎么计算,这是一个很经典的数据库负载测试题,在你使用points作为索引的前提下,其负载开销与你的排名线型相关,也就是,你要是排名靠前,负载几乎可以忽略,你要是排名靠后,负载直线上升。无论任何数据引擎,无论myisam,还是innodb,无论mysql还是sql server。如果不动技术架构,不改产品设计,纯SQL优化,两个字,没戏。  


这个场景,使用redis 的zrank来处理,爽爽的解决,毫无压力。这是这个结构最有价值的地方。但一切都是有条件的,其插入,更新,删改的系统开销,要远高于redis的其他数据结构。 而其价值,仅仅在这个场合,是具有绝对优势的。


说了这些铺垫,回到今天的场景,小密圈的圈子首页,究竟出了什么问题。


小密圈的研发,我估计是对数据库的使用不够自信,大量使用了内存来做数据存储和加载,而且,为了一些排序的需求,还大量使用的是有序集合类型,实话说,这样的内存处理,跟数据库处理,就同样的请求而言,性能上已经基本没有优势了。


但问题还不在这里,而是他们可能没有意识到,请求频次的问题。


打开一个小密圈的首页,先去内存读取这个圈子最新的帖子,这是一个常见的请求,可以理解;然后就可怕了,基于每个帖子循环,去查询该帖所有的评论,所有的赞,所有的赞赏,所有的文件,所有的图片;然后,基于每个评论,再去查询相关的图片,文件。。。


简单说就是,打开一个页面,要进行几十次乃至上百次的内存查询请求,而这些开销,用我的理解,是毫无意义的。


如果我写数据库查询,第一条SQL查询帖子,后面的用 where postid in (...),最多四条SQL (查询评论,查询附件,查询图片) 即可。 这种循环体内的重复查询,是非常要不得的,其系统负载开销是无谓的,毫无价值的。


那么,这里暴露的问题是什么呢?其实,系统设计的时候,应该有一个基本概念,就是如何尽可能减少不必要的请求,减少无谓的开销。用数据库也好,用内存也好,并无绝对对错之分,但应该基于一个原则,就是如何让系统更有效率的运行。


以前很多程序员面向对象编程,经常不注意这些开销和细节,用数据库查询,也经常有类似的问题,比如说,我读取一个图书列表,先通过某个搜索条件去搜索出所有图书id,按理说到这一步已经足够了,但因为面向对象啊,通过图书id去获得图书的名字,作者名字,页数,简介,是对象里的一个方法,然后就变成一个循环内不断去数据库请求,其实这些请求都是毫无意义的,第一个查询完全可以全部获取,逐一请求完全是多余的,但这样的案例数不胜数,几乎每次带技术新人,都会犯一遍这样的错误。


回到上面的问题,什么时候用内存,什么时候用数据库,我们对用内存的场合,是有一些自己的原则的。


第一,内存使用,是基于你的业务场景,而不是基于数据结构。

这是什么概念呢,内存是为了提速的对不对,是为了降低负载的对不对,所以内存的设计,是基于我业务场景的请求诉求设计,而不是把内存当数据库用。


以小密圈案例来说,如果我要用内存,我极端一点,这个圈子首页我要用内存处理,应该出现的东西,对不起,以圈子id为key,以其首页应该出现的内容为value。也就是一次请求,全部获取。(这不是最终方案,下面会继续说)


那么有数据变更怎么办,全部清空。


基本逻辑是这样(非最终逻辑),所有数据变更,都是变更数据库,并自动清掉相应的内存。

前端请求先看内存有没有这部分信息,有的话,直接展现,没有的话,从数据库生成这块内容并写入内存,展现。


第二,读写频率比非常关键


如上的设计逻辑,是基于读取频率远高于写入频率,但如果写入频率很高,甚至和读取频率接近,那么,这个逻辑,就无意义了。


所以使用内存的时候,对读写频率要有清醒的认识,否则很可能用了内存反而效率下降。


第三,基于写入频率做忙闲拆分


什么意思呢,比如上面,我说都堆一个value里,其实在这个场景,是不对的,只是说明,针对业务请求去设计的概念,为什么不对呢。


帖子的发布,一般而言,不会很多,很频繁对不对。

评论,图片这些还好,都不多。

点赞是个很频繁的操作对不对。


刚才提到,写入频率过高,那么就会带来这个内存生成的额外开销很大,又是不必要的,这时候怎么处理呢。


点赞单独拎出来。每个帖子的点赞,可以单独做个key ,value的哈希内存表。


那么这时候,首页是这样的,先通过内存获取一个大数组,包括所有首页的帖子,评论,图片链接,作者信息,附件等等。


然后,循环获取每个帖子的点赞信息。


等等,刚才不是说不要循环获取么?

那,这就是你看两害取其轻的问题了,而且这里的点赞数据,是哈希索引,查询开销远低于有序集合类型。


所以,点赞这个频繁操作,就不涉及首页其他数据的更新,忙闲拆分,把负载高的请求尽可能降下去。


第四,请求合并,异步处理


上面提到的


基本逻辑是这样(非最终逻辑),所有数据变更,都是变更数据库,并自动清掉相应的内存。


这里还有一个坑,如果这个数据变更的写入规模特别频繁,数据库依然撑不住怎么办。


请求合并,异步处理,比如说,点赞操作特别频繁,那么,这个请求,可以扔到一个内存队列里,同时变更对应的key-value 缓存,呐,队列结构,这又是一个特定的数据结构。


然后,定时在后台cron,读取内存数据队列,这时候,很多请求是可以合并的,本来很多条SQL的可能可以并为一条SQL。


我以前有个习惯,在cron会计算一下写入的合并率,这个指标也很重要。


比较典型的是那种统计性的东西,比如帖子访问了多少次,如果不做合并的话每次执行一个 帖子阅读数+1操作,但放到队列里,定时后台合并,效果是非常显著的,合并率往往可以超过70%。(热门贴占据大部分访问量)


以上一些思考方式和原则,是我们之前使用内存经常用到的,那么核心还是那句话,要对负载,对系统开销有清醒的认识,对无谓的请求,不必要的请求要敏感,以上没有什么特别难懂或高深的技巧,都是基于一些朴素的系统理解,做出的判断和选择。我常说我是经济适用架构师,其实我也不是科班出身,也没什么算法基础,和angela zhu这样的算法大牛比,真的没有一点可比性,但面对很多很基础的技术场景,其实就是用最朴素的思想去思考,先搞明白,系统的开销集中在哪里,都用到哪里去了,这些开销都是必须的么,然后思考一下,如何减少请求,减少不必要的开销,实在无法减少的,如何实现分布,实现扩展。


养成这样的思考习惯和分析习惯,很多常见的性能问题,其实都是可以解决的。




你看,我没打什么广告吧。


我发现我也是有毛病,有人送钱都不要,又双叒叕推掉了一个广告,价格也公道,产品也说的过去,纠结了半天还是没接,真不是清高,真没救了。


再说个没救的,小密圈今天做第八期推荐,昨天他们编辑问我怎么介绍自己的圈子合适,我的回复是,你们不要推荐我。


今天下午咖啡厅跟人聊天,我总恍惚觉得,如果我是旁边桌的人,对我一无所知,听我那么喷,一定觉得我是吹牛逼,各种不要脸,幸亏今天是跟男人聊天,否则一定会觉得我是吹牛逼骗妹纸。 


我说的都是


说句不夸张的,我要是玩命薅搞付费社群,一年薅个一两百万很容易啊,有啥意思;

投的那点钱赔了就陪了,脸我可丢不起;

我身边那帮老朋友好多个身价几十亿,几个亿的;

我要融资的话,找几个老朋友说一声就行了。

好多朋友劝我做基金啊,lp多的是啊,我自己不愿意做,怕搞砸了对不起人家。

我真想回去找份工作的话,年薪200万打底的机会肯定有的,但伺候不了人了啊。


听上去总觉得自己就是特么的一骗子呢,最关键的是我还真没啥钱。


就酱吧。

你可能感兴趣的:(谈谈编程 之 滥用内存的现象)