当前位置: 首页 > 后端技术 > Python

Python:记录一个网页界面优化

时间:2023-03-25 23:40:21 Python

背景我们负责一个业务平台。有一次我们发现设置页面的加载非常慢。开启优化之旅。既然问路是一个网站响应的问题,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):""""""...#定义另一个获取cpu的函数defget_max_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)]max_val=max(vals)ifvalselsefloat('nan')max_cpus[gid]=max_val#启动线程批量请求gidingids:t=Thread(target=get_max_cpu,args=(...))threads.append(t)t.start()#回收线程fortinthreads:t.join()returnmax_cpus通过代码可以看出,为了更快的获取到gids的所有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()如果结果:result=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':结果['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)被反复执行,invalidly是的,然后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()#批量查询提过滤器外循环query_results=query_instance.filter(ProfileSetting.name.in_(project_code+':'+gidforgidingids)).all()#单独处理所有查询结果profile_settings={}forresultinquery_results:ifnotresult:continueresult=result.as_dict()gid=result['name'].split(':')[1]tag_indexes=result.get('tag_indexes')profile_settings[gid]={'tag_indexes':tag_indexes,'interval':结果['间隔'],'状态':结果['状态'],'profile_machines':结果['profile_machines'],'thread_settings':结果['thread_settings']}...(略)returnprofile_settings优化后的火焰图:对比优化前同一位置的火焰图:明显的优化点:优化前,最底层的utils.py:get_group_profile_settings和数据库相关的热点大大减少优化效果响应同项目界面时间从37.6秒优化到1.47秒。具体截图:优化总结就像一句名言:如果一个数据结构足够好,那么它不需要一个好的算法。在优化一个函数的时候,最快的优化是:去掉那个函数!第二快的事情是调整该功能触发器的频率或复杂性!从上到下,从用户的使用场景来考虑这种功能优化方式,往往会带来更简单高效的结果,呵呵!当然,很多时候我们并没有那么幸运。如果我们真的不能去除或调整,那么我们可以使用作为程序员的价值:Profile可以用于Python:cProfile+gprof2dot,Go:pprof+go-torch。很多时候看到的代码问题不一定是真正的性能瓶颈。需要结合工具客观分析,才能有效直击痛点!其实这个1.47s并不是最好的结果,还有更多优化的空间,比如:前端渲染展示的方式,因为整个表格是拼装了很多数据后展示的,单元格慢responsecan默认先展示菊花,返回数据再更新;火焰图显示还有很多细节可以优化,可以更换请求数据的外部接口,比如优化GetAttr相关的逻辑;更极端的是直接将Python转为GO;但是这些优化已经没有那么紧迫了,因为这个1.47s是一个比较大的项目的优化结果。事实上,大多数项目都可以在不到1秒的时间内返回。重新优化可能会花费更多,结果可能只有500ms到400ms。结果并没有那么划算。所以,我们要时刻明确自己的优化目标,时刻考虑投入产出比,在有限的时间内做出比较高的价值(如果有空,当然可以做到最后)欢迎大家给予咨询交流,QQ讨论群:258498217转载请注明出处:https://segmentfault.com/a/1190000020956724