朴素系统优化思维的实践

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了朴素系统优化思维的实践相关的知识,希望对你有一定的参考价值。

作者:京东物流 严孝男

一、问题

去年年中时候,我有个好朋友(可以叫他华哥)顶着当时还很严重的疫情形式激情创业,斥巨资承包了他原公司食堂的几个摊位,摇身一变成了老板。当了老板的华哥没有丝毫懈怠,不但做了充足的市场调研,还结合他自己以前就餐时的痛点做了创新,比如以前食堂除了最常规的面,饺子,米线一类的之外就是一份份的卖炒菜,差不多一份荤菜十几块,一份素菜近十块的样子,这就导致一个问题,一般男生花了几十块钱也就只能吃到2-3个菜,不但营养不够丰富,万一踩坑遇到了原本抱有很高期待但发现实际菜并不好吃的情况,体验就更差了。

所以华哥借鉴了市面上麻辣烫自选称重模式的特点推出了自助选菜称重的模式,餐台上会摆放很多种做好的菜(荤素凉都有),大家根据自己的喜好自己打菜,主食的米饭和馒头免费,粥和汤也免费,然后还提供一些收费的主食比如红薯,玉米一类的,打菜的流程就是大家从台子两边按顺序开始自选打菜,然后选择主食,然后选择汤粥,然后结账刷卡,如下图所示:

朴素系统优化思维的实践_工程技术

华哥不愧是前互联网大厂的金牌产品经理,其敏锐的抓住了用户的痛点,并很好的给出了相应的解决方案,自助称重模式自从推出后就受到了同事们的热烈欢迎,每次都排了长长的队伍,甚至中午11点半开餐,不到11点20就有很多同事在排队等着,写到这里我想举个排长队的例子给大家一个直观的印象,我最开始想到的例子是五道口那个枣糕店门口排的长队,后来一想现在京东2号楼B座4楼餐厅里排瓦罐的队伍好像更贴切。

华哥开始的时候非常开心,但一个月后做了营收盘点发现有点不对,虽然看上去队伍排得很长很火爆的样子,但实际上营收并不如预期。华哥分析了一下排除了客单价低的因素,自选模式下好多菜大家看到后都想来一点,一来二去就会打好大一盘,基本都是20元起步的客单价;然后就剩下单量低这个可能性了,实际分析一下就可以发现,因为菜的可选品种很多,所以选菜环节每个人需要花很长的时间选菜,再加上需要打汤和打饭,一个人实际完成整个取餐的过程耗时是很长的,虽然后面的同事可以跟在前面同事后面串行打菜,但因为每个人的喜好不一样,所以每个人在不同菜前的停留时间不一样,这就导致当前面的同事在某个菜盘前耗时稍长的时候,后面的同事是处于等待状态的。

而且有的时候还会遇到一些极端的情况,比如有些同事会在某些他爱吃的菜前停留很久挑挑拣拣,还有些同事会在打免费汤时拿着大勺顺逆时针交替着疯狂搅动,以此企图捞起汤里那些零散的沉底的菜叶和鸡蛋白,华哥就亲眼目睹了他以前汇报的经理在辣子鸡丁菜盆里翻来覆去的寻找隐匿在辣椒深处的那一点点鸡肉,每发现一块鸡肉时经理的脸上还会露出那种心满意足充满成就感的笑容,话说回来其实在经理挑鸡肉时整个队伍实际是处于完全停滞状态的,所以综合来看整个队伍的执行就餐过程是非常缓慢的,也就导致实际打完餐付费的人数并不如想象中多。

二、方案

后来在一次好友聚会时,华哥和我聊起了这个事情,他问我:你们搞技术的不都各种吹嘘什么系统优化,降本增效一类的吗,你帮我想想办法。听完华哥这略带挑衅意味的要求,我突然觉得自己身上有了很重的责任感,觉得自己要守住技术人的尊严。于是自己好好想了想,然后觉得这个商业问题实际上也可以看成一个技术问题,这个餐台可以看成一个系统,打餐的流程可以认为是系统的一次交互流程,每个打菜的同事可以看成是一次调用,因为每次调用执行起来的性能太差,导致系统整体的吞吐量太低,影响了整体系统的效能,因此整个系统的效能很低,虽然当时已经是酒过三巡,脑子不太清醒了,但是自己还是尽力给华哥想了好几个办法。

2.1 系统扩容

第一个想到的办法就是扩容,在工程技术领域当遇到系统性能不达标时,第一个想到的解决方案也一般都是扩容,工程领域里的扩容一般可以分垂直扩容和水平扩容两种方式:垂直扩容是通过提升单体实例的硬件能力来提升单体处理能力,水平扩容则是通过增加实例节点的方式来增加整个系统的处理能力。

套用这两个理论,看看怎么提升餐台的吞吐,好像垂直扩容这块能做的不多,总不能把打饭的勺子升级一下变成德国原装进口高温武火蹴练镀金勺吧;不过虽然垂直扩容没什么好办法, 但是水平扩容好像能做的事情很多了,只要多增加几套打菜餐台,这样并行执行的2条打饭队伍就可以变成4条,甚至8条,直接实现了多线程并发,这样系统整体的吞吐能力可以立马获得翻倍式提升,效果不但见效块,效果也可谓是立竿见影,于是我给画了一个水平扩容示意图,如下图所示:

朴素系统优化思维的实践_分布式缓存_02

不过水平扩容的方案很快就被华哥否了,虽然在工程技术领域,随着云原生技术的成熟,应用级别的扩容缩容都是很成熟的提升系统处理能力的解决方案了,但是在华哥这里,想再搭一个餐台是不可能的,且不说华哥承包的摊位没有这么大的地方去搞第二个餐台,就算有,从新施工装修,水电改造一系列的成本也几乎是不可能实现的。

虽然这个世界上能用钱解决的问题都不叫问题,但现在的问题是华哥没钱了。

2.2 单次执行优化

提升系统并发能力的路走不通后,那么提升系统的吞吐量的办法就是缩短单条请求的处理执行时间,这样单位时间内系统处理的请求条数就会有提升,从而提升系统吞吐量,那回到餐台这里,就变成了需要缩短单人打餐的时间,尤其是遇到华哥前经理那种在单个菜盘前会耗费大量时间的情况该如何优化呢?

我们拆分一下每次调用,把在每个菜盘前打菜的过程可以模拟理解为执行一段逻辑,这样全部的打菜过程可以被拆解成一个个小的代码块,总的调用时间是由这些代码块的执行时间之和决定的,从工程技术视角的话就是保证每段逻辑都在一个可预期的时间内完成,所以每段逻辑都可以通过一个超时判断逻辑来控制每段代码的执行时间,这里举一个百度搜索的例子,百度为了增强返回结果的多样性,推出了阿拉丁架构,每个query经过星图模型解析后会分发给不同的垂类,每个垂类会加工生产属于自己业务领域的卡片,然后阿拉丁的root应用聚合垂类返回的各个结果并返回给用户,那某些垂类场景执行会比较慢,比如当遇到用户搜一款药的场景时,健康垂类的应用会根据搜索人的经纬度筛选附近的o2o的药店,并计算该药品在该门店的促销折扣价,这种计算往往会耗时很久,所以root应用会增加一个380ms的超时判断,对所有的垂类应用都是一样,当你返回的内容超过这个时间后结果会被丢弃,举这个例子让大家可以明白通过增加对每个环节的超时设置,这样可以保证整体的流程在一个可控的时间范围内得到执行,从而保证用户体验的一致性。

程序里的超时好加,因为程序没有喜怒哀乐,但打餐的场景不一样,总不能在每个菜后面安排一个服务员在背后数123计时,超过5s往前推他一把,总不能这样吧,究其原因就是打菜是主观能动的,他想在一个菜前停多久就停多久,想到这个问题后,我有了主意,把用户自主停留的权利给剥夺,创造统一的停留时间,所以我给华哥设计了一套超时装置,那就是在餐台的两边各增加一套自动传送装置,类似于飞机场里安检后赶去航站楼的传送带一样,这样人们在两边打菜时不需要自己走动了,而且每个人在每个菜盘前停留时间是一样的,就不会出现一个人在某个菜前停留时间过久的问题,也避免了餐台因前面某个人的长时间停留而出现整体停滞的问题,提升了餐台的吞吐量,而且传送带的增加还有个好处就是人不多时可以开得很慢甚至停掉,在高峰期时可以适当增加传送带的速度,从而控制每一个人打菜的时间,保障整个餐台的吞吐率。

朴素系统优化思维的实践_工程技术_03

华哥听到我这个有点天才的想法后愣了很久,盘算了一下可能性后他觉得这个办法还真的可行,只是需要等到十一或者五一长假期间动工在两边增加传送带,终于听到一个可行方案的华哥有点兴奋,两腮也泛出了点点的红晕。

2.3 非核心流程剔除

看到华哥接纳了我的这个方案,我顿时感受到了很大的鼓励,于是又继续思考这块流程还能怎么优化,在工程技术领域,一个流程在承受很大流量时还可以做的一个事情就是流程简化,只保留核心的流程环节,也就是大家常说的黄金流程,而将非核心的业务节点从主流程中剔除,这样精简后的主流程可以一定程度上缩短执行时间,而且主流程执行的逻辑少了,出错的概率也同时就降低了,举一个京东零售的下单计算流程为例,零售侧结算时需要做以下的事情:

朴素系统优化思维的实践_执行时间_04

实际上结算这块还有很多的非主要节点要处理,比如删除购物车中相关已结算商品,预占自提柜等等,但是这些属于非核心的流程,可以从主流程中剔掉。

回到餐台这里,什么流程是黄金流程,没错,就是那些和营收直接相关的流程,而那些不产生收益的项目,比如米饭馒头,汤粥的环节就可以认为是非主要流程,可以从主流程中剔掉,这样不断简化了大家取餐的主流程,而且还节省了餐厅的空间,剩余的空间可以用来做几件事,一个是可以多放一些收费的主食或者增加一些菜品,以此可以增加收入,第二个可以增加一些自助收银设备,之前2个收银台在之前打餐比较慢时可以满足需求,但现在整个流程简化了,整体每个人的打餐速度提升了,这样2个收银台就会变成新的瓶颈,尤其是遇到有扫码直付的同学就会瓶颈的更明显,这样通过增加收银台的数量,从而提升了收银环节的并发处理能力,保证了整个取餐流程的流畅,避免新的性能瓶颈的出现,完美!

朴素系统优化思维的实践_执行时间_05

华哥听完这个建议很满意,他正嫌餐台太小摆放的菜系不够多呢,这样空间被更合理的利用到能带来收益的食品上了,正合华哥之意,华哥很开心的敬了我一个。

2.4 分布式缓存

除此之外,互联网增加系统吞吐能力,缩短单次执行时间的一个很主要的法宝利器就是利用分布式缓存技术,分布式缓存技术可以让很多存在系统瓶颈的调用通过缩短数据获取时间从而极大缩短处理时间,在这里分布式缓存技术是不是也可以利用到餐台这里呢。

我想了一下,前面从主流程中拿掉的免费主食部分和汤粥部分可以利用缓存的原理,尤其是CDN缓存的原理,把主食和汤粥分布式的放在离同事们就餐的餐桌附近,这样可以让就餐的同事们最近范围就可以盛到主食和汤粥,表面上看对营收没提升,但实际上一是大家打饭近了,就餐体验好了,二是大家打饭加饭方便了,就餐的时间就会降低,从而提升餐桌的利用率。保证下一个打到饭的同事能快速找到座位,体验同样也会提升。这里我就不画图了,相信大家都能明白。

三、后记

华哥整体听完我的优化方案后低头陷入了沉思,许久之后他抬起头看着我,眼神有些许迷离,我顿时有点紧张,以为他要系统点评一下我的方案,没想到华哥开口问我的是,现在几点了,我才意识到华哥刚才是喝多了低头睡着了。

性能优化:关于缓存的一些思考

作者 | 烛衡


利用缓存做性能优化的案例非常多,从基础的操作系统到数据库、分布式缓存、本地缓存等。它们表现形式各异,却有着共同的朴素的本质:弥补CPU的高算力和IO的慢读写之间巨大的鸿沟。


和架构选型类似,每引入一个组件,都会导致复杂度的上升。以缓存为例,它带来性能提升的同时,也带来一些问题,需要开发者设计和权衡。


本文的思维脉络如下:


性能优化:关于缓存的一些思考


一 缓存和多级缓存


1 缓存的引入


在初期业务量小的时候,数据库能承担读写压力,应用可以直接和DB交互,架构简单且强壮。


经过一段时间发展后,业务量迎来了大规模增长,此时DB查询压力和耗时都在增长。此时引入分布式缓存,在减少DB压力的同时,还提供了更高的QPS。


再往后发展,分布式缓存也成为了瓶颈,高频的QPS是一笔负担;另外缓存驱逐以及网络抖动会影响系统的稳定性,此时引入本地缓存,可以减轻分布式缓存的压力,并减少网络以及序列化开销。


性能优化:关于缓存的一些思考


2 读写的性能提升


缓存通过减少IO操作来获得读写的性能提升。有一个表格,可以看见磁盘、网络的IO操作耗时,远高于内存存取。


  • 读优化:当请求命中缓存后,可直接返回,从而略过IO读取,减小读的成本。

  • 写优化:将写操作在缓冲中合并,让IO设备可以批量处理,减小写的成本。

性能优化:关于缓存的一些思考

缓存带来的QPS、RT提升比较直观,不补充介绍。

3 缓存 Miss


缓存Miss是必然会面对的问题,缓存需保证在有限的容量下,将热点的数据维护在缓存中,从而达到性能、成本的平衡。

缓存通常使用LRU算法淘汰近期不常用的Key。

近似LRU

可以先试想严格LRU的实现。假设Redis当前有50W规模的key,先通过Keys 遍历获得所有Key,然后比对出空闲时间最长的某个key,最后执行淘汰。这样的流程下来,是非常昂贵的,Keys命令是一笔不小的开销,其次大规模执行比对也很昂贵。

当然严格LRU实现的优化空间还是有的,YY一下,可以通过活跃度分离出活跃Key和待回收Key, 淘汰时只关注待回收key即可;回收算法引入链表或者树的结构,使Key按空闲时间有序,淘汰时直接获取。然而这些优化不可避免的是,在缓存读写时,这些辅助的数据结构需要同步更新,带来的存储以及计算的成本很高。

在Redis中它采用了近似LRU的实现,它随机采样5个Key,淘汰掉其中空闲时间最长的那个。近似LRU实现起来更简单、成本更低,在效果上接近严格LRU。它的缺点是存在一定的几率淘汰掉最近被访问的Key,即在TTL到期前也可能被淘汰。

性能优化:关于缓存的一些思考

避免短期大量失效

在一些场景中,程序是批量加载数据到缓存的, 比如通过Excel上传数据,系统解析后,批量写入DB和缓存。此时若不经设计,这批数据的超时时间往往是一致的。缓存到期后,本该缓存承担的流量将打到DB上,从而降低接口甚至系统的性能和稳定性。
可以利用随机数打散缓存失效时间,例如设置TTL=8hr+random(8000)ms。

4 缓存一致性


系统应尽量保证DB、缓存的数据一致性,较常使用的是cache aside设计模式。

避免使用非常规的缓存设计模式: 先更新缓存、后更新DB; 先更新DB、后更新缓存(cache aside是直接失效缓存)。这些模式的不一致风险较高。

缓存设计模式

业务系统通常使用cache aside 模式,操作系统、数据库、分布式缓存等会使用write throgh、write back。

性能优化:关于缓存的一些思考

cache aside的缓存不一致

Cache aside模式大部分时间运行良好,在一些极端场景下,仍可能出现不一致风险。主要来自两方面:

1.由于中间件或者网络等问题,缓存失效失败。

2.出现意外的缓存失效、读取的时序。

缓存失效失败很容易理解,不做补充。主要介绍时序引起的不一致问题。

考虑这样的时间轴,A线程发现cache miss后重新加载缓存,此时读的数据还是老的, 另一个线程B更新数据并失效缓存。若B线程失效缓存的操作完成时间早于A线程,A线程会写入老的数据。

性能优化:关于缓存的一些思考

缓存不一致有一些缓解方法,例如延迟双删、CDC同步。这些方案都提升了系统复杂度,需综合考虑业务的容忍度,方案的复杂度等。

  • 延迟双删:主线程失效缓存后,将失效指令放入延时队列,另一个线程轮询队列获取指令并执行。

  • CDC同步:通过canal订阅MySQL binlog的变更,上报给Kafka,系统监听Kafka消息触发缓存失效。


二 从堆内存到直接内存


1 直接内存的引入


Java本地缓存分两类,基于堆内存的、基于直接内存的。

采用堆内存做缓存的主要问题是GC,由于缓存对象的生命周期往往较长,需要通过Major GC进行回收。若缓存的规模很大,那么GC会非常耗时。

采用直接内存做缓存的主要问题是内存管理。程序需自主控制内存的分配和回收,存在OOM或者Memory Leak的风险。另外直接内存不能存取对象,在操作时需进行序列化。

直接内存能减少GC压力,因为它只需要保存直接内存的引用,而对象本身是存储在直接内存中。引用晋升到老年代后占用的空间很小,对GC的负担可忽略。

直接内存的回收依赖System。gc的调用,但这个调用JVM不保证执行、也不保证何时执行,它的行为是不可控的。程序一般需要自行管理,成对去调用malloc、free,依托于这种“手工、类C”的内存管理,可以增加内存回收的可控性和灵活性。

2 直接内存管理


由于直接内存的分配和回收比较昂贵,需要通过内核操作物理内存。申请的时候一般是申请大的内存快,然后再根据需求分配小块给线程。回收的时候不直接释放,而是放入内存池来重用。

如何快速找到一个空闲块、如何减少内存碎片、如何快速回收等等,它是一个系统性的问题,也有很多专门的算法。

Jemalloc是综合能力较好的算法,free BSD、Redis默认采用了该算法,OHC缓存也建议服务器配置该算法。Netty的作者实现了Java版本,感兴趣的可以阅读。

性能优化:关于缓存的一些思考

三 CPU 缓存


利用上分布式缓存、本地缓存之后,还可以继续提升的就是CPU缓存了。它虽不易察觉,但在高并发下对性能存在一定的影响。

CPU缓存分为L1、L2、L3 三级,越靠近CPU的,容量越小,命中率越高。当L3等级的缓存都取不到数据的时候,需从主存中获取。

性能优化:关于缓存的一些思考

性能优化:关于缓存的一些思考

1 CPU cache line


CPU缓存由cache line组成,每一个cache line为64字节,能容纳8个long值。在CPU从主存获取数据时,以cache line为单位加载,于是相邻的数据会一并加载到缓存中。很容易想到,数组的顺序遍历、相邻数据的计算是非常高效的。

性能优化:关于缓存的一些思考

2 伪共享 false sharing


CPU缓存也存在一致性问题,它通过MESI协议、MESIF协议来保证。

伪共享来源于高并发时cache line出现了缓存不一致。同一个cache line中的数据会被不同线程修改,它们相互影响,导致处理性能降低。

性能优化:关于缓存的一些思考

上图模拟一个伪共享场景,NoPadding是线程共享对象,thread0 会修改no0、thread1 会修改no1。当thread0修改时,除了修改自身的cache line,依据CPU缓存协议还会导致thread1对应的cache line失效,这时thread1 发现cache miss后从主存加载,修改后又导致thread0 的cache line 失效。

 
   
   
 
   
     
     
   
NoPadding {
long no0;
long no1;
}


3 伪共享解决方案


padding

通过填充,让no0、no1落在不同的cache line中:

 
   
   
 
   
     
     
   
Padding {
long p1, p2, p3, p4, p5, p6, p7;
volatile long no0 = 0L;
long p9, p10, p11, p12, p13, p14;
volatile long no1 = 0L;
}

案例:jctools

Contended 注解

委托JVM填充cache line:

 
   
   
 
   
     
     
   
@sun.misc.Contended static final class CounterCell {
        volatile long value;
        CounterCell(long x) { value = x; }
    }

案例:JDK源码中LongAdder中的Cell、ConcurrentHashMap的CounterCell。

无锁并发

无锁并发可以从本质上解决伪共享问题,它无需填充cache line,并且执行效率是最高的。

案例:disruptor


四 总结


近来由于业务对接口RT提出了更高的要求,在性能优化的过程中,缓存的使用是非常多的。借此机会记录下在这段时间的思考。私以为,在引入某一项技术的时候,需整体的去看,了解其概念、原理、适用场景、注意事项,这样可以在设计之初就规避掉一些风险。

分布式缓存、本地缓存、CPU缓存涵盖的内容非常多,本文做了一些归纳。对细节感兴趣的同学可以阅读《Redis 设计与实现》、disruptor 设计文档及代码。

更多精彩

性能优化:关于缓存的一些思考






以上是关于朴素系统优化思维的实践的主要内容,如果未能解决你的问题,请参考以下文章

朴素贝叶斯算法优化与 sklearn 实现

性能优化:关于缓存的一些思考

三年经验月薪50k我是怎么做到的?Java岗

CF792E Colored Balls思维

算法朴素贝叶斯分类算法原理与实践

[Mdp] lc1937. 扣分后的最大得分(dp优化+前后缀优化+周赛250_3)