背??景我们负责一个商业平台。有一次我们发现设置页面的加载速度极慢,简直离谱。用户等36s肯定是不可能的,所以我们就要开始优化之旅了。既然问路是一个网站响应的问题,Chrome这个强大的工具可以帮助我们快速找到优化的方向。通过Chrome的Network,除了可以看到接口请求的耗时,还可以看到一次时间的分布情况,选择一个没有那么多配置的项目,简单请??求一下就可以看到:虽然只是一个只有3条记录的项目,加载工程需要17s设置。通过Timing可以看到总的请求耗时17.67s,但是有17.57s处于Waiting(TTFB)状态。TTFB是TimetoFirstByte的缩写,指浏览器开始接收服务器响应数据的时间(后台处理时间+重定向时间),是反映服务器响应速度的重要指标。Profile火焰图+代码调优那么你大概可以知道,优化的大方向是在后端接口处理上。后端代码采用Python+Flask实现。不要盲目猜测,直接上Profile:第一波优化:功能交互重新设计说实话看到这段代码很绝望:什么都没有?只是因为协程或线程太多而看到很多gevent和线程?这时候就要结合代码来分析(为简洁起见,参数部分用“...”代替):?defget_max_cpus(project_code,gids):""""""...#定义另一个函数defget_max_cpu来获取cpu(project_setting,gid,token,headers):group_with_machines=utils.get_groups(...)hostnames=get_info_from_machines_info(...)res=fetchers.MonitorAPIFetcher.get(...)vals=[round(100-val,4)forts,valinres['series'][0]['data']ifnotutils.is_nan(val)]maxmax_val=max(vals)ifvalselsefloat('nan')max_cpus[gid]=max_val#启动线程批量请求forgidingids:t=Thread(target=get_max_cpu,args=(...))threads.append(t)t.start()#回收线程fortinthreads:t.join()returnmax_cpus可以通过代码中,为了更快的获取所有gid的cpu_max数据,为每个gid分配一个线程去请求,最后返回最大值。这里会出现两个问题:在webapi中创建和销毁线程的开销很大,因为接口会被频繁触发,线程操作也会频繁发生。尽量使用线程池之类的,降低系统开销。成本;请求是加载某个gid(组)下某台机器近7天的最大CPU值。你可以简单地想想。该值不是实时值或平均值,而是最大值。很多时候,可能没有想象中的那么值钱;既然知道了问题,就有了针对性的解决方案:调整功能设计,不再默认加载最大CPU值,取而代之的是用户点击加载(一是降低并发的可能性,二是不加载)影响整体);因为1的调整,去掉了多线程实现;看第一波优化后的火焰图:虽然这次看到的火焰图还有很大的优化空间,但至少看起来还算正常。第二波优化:Mysql操作优化处理我们从pagemarker(接口逻辑)放大火焰图观察:可以看到大量的操作是函数utils.py引起的数据库操作热点:get_group_profile_settings。同样需要进行代码分析:defget_group_profile_settings(project_code,gids):#获取MysqlORM操作对象ProfileSetting=unpurview(sandman.endpoint_class('profile_settings'))session=get_postman_session()profile_settings={}forgidingids:compound_name=project_code+':'+gidresult=session.query(ProfileSetting).filter(ProfileSetting.name==compound_name).first()ifresult:resultresult=result.as_dict()tag_indexes=result.get('tag_indexes')profile_settings[gid]={'tag_indexes':tag_indexes,'interval':result['interval'],'status':result['status'],'profile_machines':result['profile_machines'],'thread_settings':result['thread_settings']}。..(略)returnprofile_settings看到Mysql,第一反应是索引问题,所以先看数据库的索引,如果有索引应该不是瓶颈:奇怪这里已经有索引了,为什么这么快?看起来还是这个样子!就在我没有思路的时候,突然想起第一波优化的时候,发现gid(groups)越多影响越明显,然后回头看上面的代码,看到了一句:forgidingids:。..我好像明白了什么。这里每个gid查询一次数据库,项目往往有20到50+组,所以直接爆了。其实Mysql是支持单字段多值查询的,每条记录不会有太多的数据。我可以尝试使用Mysql的OR语法。除了避免多次网络请求外,还可以避免damnfor。正想着闲话少说开始的时候,眼角余光瞥到刚才的代码还有一个地方可以优化,那就是:看到这里,熟悉的小伙伴大概就明白了什么是继续。GetAttrPython获取对象的方法/属性时使用该方法。虽然不可或缺,但如果使用过于频繁,也会有一定的性能损耗。一起看看代码:defget_group_profile_settings(project_code,gids):#获取MysqlORM操作对象ProfileSetting=unpurview(sandman.endpoint_class('profile_settings'))session=get_postman_session()profile_settings={}forgidingids:compound_name=project_code+':'+gidresult=session.query(ProfileSetting).filter(ProfileSetting.name==compound_name).first()...在这个forthat已经遍历了很多次,session.query(ProfileSetting)重复执行无效,然后filter属性方法也经常被读取和执行,所以这里也可以优化一下。以下问题归纳如下:1、数据库查询没有批量查询;2.重复生成过多的ORM对象,造成性能损失;3.属性读取后没有复用,导致循环体内频繁遍历大量遍历getAttr,成本被放大;那么对症下药就是:defget_group_profile_settings(project_code,gids):#获取MysqlORM操作对象ProfileSetting=unpurview(sandman.endpoint_class('profile_settings'))session=get_postman_session()#循环中提到的批量查询和过滤outerquery_results=query_instance.filter(ProfileSetting.name.in_(project_code+':'+gidforgidingids)).all()#单独处理所有查询结果profile_settings={}forresultinquery_results:ifnotresult:continueresultresult=result.as_dict()gid=result['name'].split(':')[1]tag_indexes=result.get('tag_indexes')profile_settings[gid]={'tag_indexes':tag_indexes,'interval':result['interval'],'status':result['status'],'profile_machines':result['profile_machines'],'thread_settings':result['thread_settings']}...(略)返回profile_settings优化后的火焰图:优化前对比同一位置的火焰图:明显优化点:优化前,最底层的utils.py:get_group_profile_settings和database-相关热点大大减少。优化效果同项目界面响应时间从37.6s优化到1.47s。具体截图:优化总结就像一句名言:如果一个数据结构足够好,那么它不需要一个好的算法。在优化一个函数的时候,最快的优化是:去掉那个函数!第二快的事情是调整该功能触发器的频率或复杂性!从上到下,从用户的使用场景来考虑这种功能优化方式,往往会带来更简单高效的结果,呵呵!当然,很多时候我们并没有那么幸运。如果我们真的不能去除或调整,那么我们可以使用作为程序员的价值:Profile可以用于Python:cProfile+gprof2dot,Go:pprof+go-torch。很多时候看到的代码问题不一定是真正的性能瓶颈。需要结合工具客观分析,才能有效直击痛点!其实这个1.47s并不是最好的结果,还有更多优化的空间,比如:前端渲染展示的方式,因为整个表格是拼装了很多数据后展示的,单元格慢responsecan默认先展示菊花,返回数据再更新;火焰图看到还有很多细节可以优化,可以更换请求数据的外部接口,比如优化GetAttr相关的逻辑;更极端的是直接将Python转为GO;但是这些优化已经没有那么紧迫了,因为这个1.47s是一个比较大的项目的优化结果。事实上,大多数项目都可以在不到1秒的时间内返回。重新优化可能会花费更多,结果可能只有500ms到400ms。结果并没有那么划算。所以,我们一定要时刻明确自己的优化目标,时刻考虑投入产出比,在有限的时间内做出比较高的价值(如果有空,当然可以做到底)
