使用split_size优化的ODPS SQL的场景

使用split_size优化的ODPS SQL的场景首先有两个大背景需要说明如下:说明1:split_size,设定一个map的最大数据输入量,单位M,默认256M。用户可以通过控制这个变量,从而达到对map端输入的控制。设置语句:set odps.sql.mapper.split.size=256。一般在调整这个设置时,往往是发现一个map instance处理的数据行数太多。说明2:小文件越多,需要instance资源也越多,MaxCompute对单个Instance可以处理的小文件数限制为120个,如此造成浪费资源,影响整体的执行性能(文件的大小小于块Block 64M的文件)。场景一:单记录数据存储太少原始Logview Detail:可以发现Job只调起一个Map Instance,供处理了156M的数据,但这些数据共有5千多万的记录(单记录平均3个byte),花费了25分钟。此外,从TimeLine看可以发现,整个Job耗费43分钟,map占用了超过60%的时间。故可对map进行优化。优化手段:调小split_size为16M优化之后的logview:优化后,可以发现,Job调起了7个Map Instance,耗时4分钟;某一个Map处理了27M的数据,6百万记录。(这里可以看出set split_size只是向Job提出申请,单不会严格生效,Job还是会根据现有的资源情况等来调度Instance)因为Map的变多,Join和Reduce的instance也有增加。整个Job的执行时间也下降到7分钟。场景二:用MapJoin实现笛卡尔积原始logview:可以发现,Job调起了4个Map,花费了3个小时没有跑完;查看详细Log,某一个Map因为笛卡尔的缘故,生成的数据量暴涨。综合考虑,因为该语句使用Mapjoin生成笛卡尔积,再筛选符合条件的记录,两件事情都由map一次性完成,故对map进行优化。策略调低split_size优化后的logview:优化后,可以看到,Job调度了38个map,单一map的生成数据量下降了,整体map阶段耗时也下降到37分钟。回头追朔这个问题的根源,主要是因为使用mapjoin笛卡尔积的方式来实现udf条件关联的join,导致数据量暴涨。故使用这种方式来优化,看起来并不能从根本解决问题,故我们需要考虑更好的方式来实现类似逻辑。本文作者:祎休阅读原文本文为云栖社区原创内容,未经允许不得转载。

March 20, 2019 · 1 min · jiezi

一次网站的性能优化之路 -- 天下武功,唯快不破

首屏作为直面用户的第一屏,其重要性不言而喻,如何加快加载的速度是非常重要的一课。本文讲解的是:笔者对自己搭建的个人博客网站的速度优化的经历。效果体验地址: http://biaochenxuying.cn1. 用户期待的速度体验2018 年 8 月,百度搜索资源平台发布的《百度移动搜索落地页体验白皮书 4.0 》中提到:页面的首屏内容应在 1.5 秒内加载完成。也许有人有疑惑:为什么是 1.5 秒内?哪些方式可加快加载速度?以下将为您解答这些疑问!移动互联网时代,用户对于网页的打开速度要求越来越高。百度用户体验部研究表明,页面放弃率和页面的打开时间关系如下图所示:根据百度用户体验部的研究结果来看,普通用户期望且能够接受的页面加载时间在 3 秒以内。若页面的加载时间过慢,用户就会失去耐心而选择离开,这对用户和站长来说都是一大损失。百度搜索资源平台有 “闪电算法” 的支持,为了能够保障用户体验,给予优秀站点更多面向用户的机会,“闪电算法”在 2017 年 10 月初上线。闪电算法 的具体内容如下:移动网页首屏在 2 秒之内完成打开的,在移动搜索下将获得提升页面评价优待,获得流量倾斜;同时,在移动搜索页面首屏加载非常慢(3 秒及以上)的网页将会被打压。2. 分析问题未优化之前,首屏时间居然大概要 7 - 10 秒,简直不要太闹心。开始分析问题,先来看下 network :主要问题:第一个文章列表接口用了 4.42 秒其他的后端接口速度也不快另外 js css 等静态的文件也很大,请求的时间也很长我还用了 Lighthouse 来测试和分析我的网站。Lighthouse 是一个开源的自动化工具,用于改进网络应用的质量。 你可以将其作为一个 Chrome 扩展程序运行,或从命令行运行。 为 Lighthouse 提供一个需要审查的网址,它将针对此页面运行一连串的测试,然后生成一个有关页面性能的报告。未优化之前:上栏内容分别是页面性能、PWA(渐进式 Web 应用)、可访问性(无障碍)、最佳实践、SEO 五项指标的跑分。下栏是每一个指标的细化性能评估。再看下 Lighthouse 对性能问题给出了可行的建议、以及每一项优化操作预期会帮我们节省的时间:从上面可以看出,主要问题:图片太大一开始图片就加载了太多知道问题所在就已经成功了一半了,接下来便开始优化之路。2. 优化之路网页速度优化的方法实在太多,本文只说本次优化用到的方法。2.1 前端优化本项目前端部分是用了 react 和 antd,但是 webpack 用的还是 3.8.X 。2.1.1 webpack 打包优化因为 webpack4 对打包做了很多优化,比如 Tree-Shaking ,所以我用最新的 react-create-app 重构了一次项目,把项目升级了一遍,所有的依赖包都是目前最新的稳定版了,webpack 也升级到了 4.28.3 。用最新 react-create-app 创建的项目,很多配置已经是很好了的,笔者只修改了两处地方。打包配置修改了 webpack.config.js 的这一行代码:// Source maps are resource heavy and can cause out of memory issue for large source files.const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== ‘false’;// 把上面的代码修改为: const shouldUseSourceMap = process.env.NODE_ENV === ‘production’ ? false : true;生产环境下,打包去掉 SourceMap,静态文件就很小了,从 13M 变成了 3M 。还修改了图片打包大小的限制,这样子小于 40K 的图片都会变成 base64 的图片格式。{ test: [/.bmp$/, /.gif$/, /.jpe?g$/, /.png$/,/.jpg$/,/.svg$/], loader: require.resolve(‘url-loader’), options: { limit: 40000, // 把默认的 10000 修改为 40000 name: ‘static/media/[name].[hash:8].[ext]’, }, }2.1.2 去掉没用的文件比如之前可能觉得会有用的文件,后面发现用不到了,注释或者删除,比如 reducers 里面的 home 模块。import { combineReducers } from ‘redux’import { connectRouter } from ‘connected-react-router’// import { home } from ‘./module/home’import { user } from ‘./module/user’import { articles } from ‘./module/articles’const rootReducer = (history) => combineReducers({ // home, user, articles, router: connectRouter(history)})2.1.3 图片处理把一些静态文件再用 photoshop 换一种格式或者压缩了一下, 比如 logo 图片,原本 111k,压缩后是 23K。首页的文章列表图片,修改为懒加载的方式加载。之前因为不想为了个懒加载功能而引用一个插件,所以想自己实现,看了网上关于图片懒加载的一些代码,再结合本项目,实现了一个图片懒加载功能,加入了 事件的节流(throttle)与防抖(debounce)。代码如下:// fn 是事件回调, delay 是时间间隔的阈值function throttle(fn, delay) { // last 为上一次触发回调的时间, timer 是定时器 let last = 0, timer = null; // 将throttle处理结果当作函数返回 return function() { // 保留调用时的 this 上下文 let context = this; // 保留调用时传入的参数 let args = arguments; // 记录本次触发回调的时间 let now = +new Date(); // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值 if (now - last < delay) { // 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器 clearTimeout(timer); timer = setTimeout(function() { last = now; fn.apply(context, args); }, delay); } else { // 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应 last = now; fn.apply(context, args); } };}// 获取可视区域的高度const viewHeight = window.innerHeight || document.documentElement.clientHeight;// 用新的 throttle 包装 scroll 的回调const lazyload = throttle(() => { // 获取所有的图片标签 const imgs = document.querySelectorAll(’#list .wrap-img img’); // num 用于统计当前显示到了哪一张图片,避免每次都从第一张图片开始检查是否露出 let num = 0; for (let i = num; i < imgs.length; i++) { // 用可视区域高度减去元素顶部距离可视区域顶部的高度 let distance = viewHeight - imgs[i].getBoundingClientRect().top; // 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明元素露出 if (distance >= 100) { // 给元素写入真实的 src,展示图片 let hasLaySrc = imgs[i].getAttribute(‘data-has-lazy-src’); if (hasLaySrc === ‘false’) { imgs[i].src = imgs[i].getAttribute(‘data-src’); imgs[i].setAttribute(‘data-has-lazy-src’, true); // } // 前 i 张图片已经加载完毕,下次从第 i+1 张开始检查是否露出 num = i + 1; } }}, 1000);注意:给元素写入真实的 src 了之后,把 data-has-lazy-src 设置为 true ,是为了避免回滚的时候再设置真实的 src 时,浏览器会再请求这个图片一次,白白浪费服务器带宽。具体细节请看文件 文章列表2.2 后端优化后端用到的技术是 node、express 和 mongodb。后端主要问题是接口速度很慢,特别是文章列表的接口,已经是分页请求数据了,为什么还那么慢呢 ?所以查看了接口返回内容之后,发现返回了很多列表不展示的字段内容,特别是文章内容都返回了,而文章内容是很大的,占用了很多资源与带宽,从而使接口消耗的时间加长。从上图可以看出文章列表接口只要返回文章的 标题、描述、封面、查看数,评论数、点赞数和时间即可。所以把不需要给前端展示的字段注释掉或者删除。// 待返回的字段 let fields = { title: 1, // author: 1, // keyword: 1, // content: 1, desc: 1, img_url: 1, tags: 1, category: 1, // state: 1, // type: 1, // origin: 1, // comments: 1, // like_User_id: 1, meta: 1, create_time: 1, // update_time: 1, };同样对其他的接口都做了这个处理。后端做了处理之后,所有的接口速度都加快了,特别是文章列表接口,只用了 0.04 - 0.05 秒左右,相比之前的 4.3 秒,速度提高了 100 倍,简直不要太爽, 效果如下:此刻心情如下:2.3 服务器优化你以为前后端都优化一下,本文就完了 ?小兄弟,你太天真了,重头戏在后头 !笔者服务器用了 nginx 代理。做的优化如下:隐藏 nginx 版本号一般来说,软件的漏洞都和版本相关,所以我们要隐藏或消除 web 服务对访问用户显示的各种敏感信息。如何查看 nginx 版本号? 直接看 network 的接口或者静态文件请求的 Response Headers 即可。没有设置之前,可以看到版本号,比如我网站的版本号如下:Server: nginx/1.6.2设置之后,直接显示 nginx 了,没有了版本号,如下:Server: nginx开启 gzip 压缩nginx 对于处理静态文件的效率要远高于 Web 框架,因为可以使用 gzip 压缩协议,减小静态文件的体积加快静态文件的加载速度、开启缓存和超时时间减少请求静态文件次数。笔者开启 gzip 压缩之后,请求的静态文件大小大约减少了 2 / 3 呢。gzip on;#该指令用于开启或关闭gzip模块(on/off)gzip_buffers 16 8k;#设置系统获取几个单位的缓存用于存储gzip的压缩结果数据流。16 8k代表以8k为单位,安装原始数据大小以8k为单位的16倍申请内存gzip_comp_level 6;#gzip压缩比,数值范围是1-9,1压缩比最小但处理速度最快,9压缩比最大但处理速度最慢gzip_http_version 1.1;#识别http的协议版本gzip_min_length 256;#设置允许压缩的页面最小字节数,页面字节数从header头得content-length中进行获取。默认值是0,不管页面多大都压缩。这里我设置了为256gzip_proxied any;#这里设置无论header头是怎么样,都是无条件启用压缩gzip_vary on;#在http header中添加Vary: Accept-Encoding ,给代理服务器用的gzip_types text/xml application/xml application/atom+xml application/rss+xml application/xhtml+xml image/svg+xml text/javascript application/javascript application/x-javascript text/x-json application/json application/x-web-app-manifest+json text/css text/plain text/x-component font/opentype font/ttf application/x-font-ttf application/vnd.ms-fontobject image/x-icon;#进行压缩的文件类型,这里特别添加了对字体的文件类型gzip_disable “MSIE [1-6].(?!.*SV1)”;#禁用IE 6 gzip把上面的内容加在 nginx 的配置文件 ngixn.conf 里面的 http 模块里面即可。是否设置成功,看文件请求的 Content-Encoding 是不是 gzip 即可。设置 expires,设置缓存 server { listen 80; server_name localhost; location / { root /home/blog/blog-react/build/; index index.html; try_files $uri $uri/ @router; autoindex on; expires 7d; # 缓存 7 天 } }我重新刷新请求的时候是 2019 年 3 月 16 号,是否设置成功看如下几个字段就知道了:Staus Code 里面的 form memory cache 看出,文件是直接从本地浏览器本地请求到的,没有请求服务器。Cache-Control 的 max-age= 604800 看出,过期时间为 7 天。Express 是 2019 年 3 月 23 号过期,也是 7 天过期。注意:上面最上面的用红色圈中的 Disable cache 是否是打上了勾,打了勾表示:浏览器每次的请求都是请求服务器,无论本地的文件是否过期。所以要把这个勾去掉才能看到缓存的效果。终极大招:服务端渲染 SSR,也是笔者接下来的方向。3.1 测试场景一切优化测试的结果脱离了实际的场景都是在耍流氓,而且不同时间的网速对测试结果的影响也是很大的。所以笔者的测试场景如下:a. 笔者的服务器是阿里的,配置是入门级的学生套餐配置,如下:b. 测试网络为 10 M 光纤宽带。3.2 优化结果优化之后的首屏速度是 2.07 秒。最后加了缓存的结果为 0.388 秒。再来看下 Lighthouse 的测试结果:比起优化之前,各项指标都提升了很大的空间。4. 最后优化之路漫漫,永无止境,天下武功,唯快不破。本次优化的前端与后端项目,都已经开源在 github 上了,欢迎围观。前端:https://github.com/biaochenxuying/blog-react后端:https://github.com/biaochenxuying/blog-nodegithub 博客地址:https://github.com/biaochenxuying/blog如果您觉得这篇文章不错或者对你有所帮助,请给个赞或者星呗,你的点赞就是我继续创作的最大动力。关注公众号并回复 福利 可领取免费学习资料,福利详情请猛戳: 免费资源获取–Python、Java、Linux、Go、node、vue、react、javaScript ...

March 17, 2019 · 3 min · jiezi

如何测试小程序? 腾讯智慧零售保障优衣库小程序体验优化

作者:WeTest小编商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处。原文链接:https://wetest.qq.com/lab/view/445.html如何测试小程序? 腾讯智慧零售保障优衣库小程序体验优化又是一年315,传统零售行业引起的消费者投诉不容小视。那在传统零售转型智慧零售的消费变革下,身为鹅厂的质量部门,我们又能为他们提供些什么帮助呢?_持续创新,优衣库尝试小程序作为全球知名服装品牌之一,优衣库通过与腾讯智慧零售团队合作,整合腾讯生态与自有流量,融合了线上线下的多个场景,消费者可以第一时间了解新品资讯,优惠信息,库存信息并通过多种形式快速下单,为消费者提供便捷、可信赖的消费服务。无论是在线下还是线上,优衣库都非常重视用户体验,因此优衣库对自身小程序的质量要求极高,确保每位顾客都能有流畅无碍的购物体验。智慧零售团队提供“个性化、全面、详细、专业”的功能测试服务为了验证优衣库小程序的功能问题,选择了智慧零售团队功能用例测试服务,测试团队由测试经验丰富的专家团队带领,根据行业特性,在用例设计上覆盖产品的特定业务流程,为优衣库的产品定制专属功能测试服务用例。1)结合客户需求,设计针对性用例根据需求,智慧零售团队设计用例覆盖了200多个测试项。涵盖了优衣库官方商城、购买商品流程等最为核心的场景功能。2)基于小程序特性,调整测试重点在测试过程中,针对小程序的测试特点,智慧零售团队调整了测试重点,增加小程序首屏加载,小程序登录等测试内容;同时针对微信授权,如读取个人信息,读取位置等测试项也逐一进行验证。3)高效执行,快速定位问题智慧零售团队在2个工作日内即输出一份详细的测试报告,帮助产品了解测试的整体情况,包括执行情况、测试设备列表、问题分布,问题列表。直观了解本次测试检测出的产品缺陷,并划分重要等级。测试用例里会列举每个测试步骤及测试结果,帮助开发商更精准定位问题。此次测试帮助优衣库小程序发现5个功能问题,保证优衣库小程序在“商品搜索”“下单流程”等核心场景在双十一期间稳定运行。优衣库的相关负责人表示此次合作保障了优衣库在 “双十一狂欢购” 等大型活动中用户的良好购物体验,特别是在搜索商品、购买商品流程上的体验畅通无阻,维护优衣库的品牌影响力。测试场景截图精细测试 铸就品质智慧零售团队联合WeTest团队,在测试领域提供为用户提供更高效、可靠的专业测试服务。腾讯WeTest (wetest.qq.com)是由腾讯官方推出的一站式品质开放平台。十余年品质管理经验,致力于质量标准建设、产品质量提升。腾讯WeTest为移动开发者提供兼容性测试、云真机、性能测试、安全防护、企鹅风讯(舆情分析)等优秀研发测试工具,为百余行业提供解决方案,覆盖产品在研发、运营各阶段的测试需求,历经千款产品磨砺。金牌专家团队,通过5大维度,41项指标,360度保障您的产品质量。

March 16, 2019 · 1 min · jiezi

前端开发者必备的Nginx知识

nginx在应用程序中的作用解决跨域请求过滤配置gzip负载均衡静态资源服务器nginx是一个高性能的HTTP和反向代理服务器,也是一个通用的TCP/UDP代理服务器,最初由俄罗斯人Igor Sysoev编写。nginx现在几乎是众多大型网站的必用技术,大多数情况下,我们不需要亲自去配置它,但是了解它在应用程序中所担任的角色,以及如何解决这些问题是非常必要的。下面我将从nginx在企业中的真实应用来解释nginx在应用程序中起到的作用。为了便于理解,首先先来了解一下一些基础知识,nginx是一个高性能的反向代理服务器那么什么是反向代理呢?正向代理与反向代理代理是在服务器和客户端之间假设的一层服务器,代理将接收客户端的请求并将它转发给服务器,然后将服务端的响应转发给客户端。不管是正向代理还是反向代理,实现的都是上面的功能。正向代理正向代理,意思是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。正向代理是为我们服务的,即为客户端服务的,客户端可以根据正向代理访问到它本身无法访问到的服务器资源。正向代理对我们是透明的,对服务端是非透明的,即服务端并不知道自己收到的是来自代理的访问还是来自真实客户端的访问。反向代理 反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。反向代理是为服务端服务的,反向代理可以帮助服务器接收来自客户端的请求,帮助服务器做请求转发,负载均衡等。反向代理对服务端是透明的,对我们是非透明的,即我们并不知道自己访问的是代理服务器,而服务器知道反向代理在为他服务。基本配置配置结构下面是一个nginx配置文件的基本结构:events { }http { server { location path { … } location path { … } } server { … }}main:nginx的全局配置,对全局生效。events:配置影响nginx服务器或与用户的网络连接。http:可以嵌套多个server,配置代理,缓存,日志定义等绝大多数功能和第三方模块的配置。server:配置虚拟主机的相关参数,一个http中可以有多个server。location:配置请求的路由,以及各种页面的处理情况。upstream:配置后端服务器具体地址,负载均衡配置不可或缺的部分。内置变量下面是nginx一些配置中常用的内置全局变量,你可以在配置的任何位置使用它们。| 变量名 | 功能 | | —— | —— | | $host| 请求信息中的Host,如果请求中没有Host行,则等于设置的服务器名 || $request_method | 客户端请求类型,如GET、POST| $remote_addr | 客户端的IP地址 ||$args | 请求中的参数 ||$content_length| 请求头中的Content-length字段 ||$http_user_agent | 客户端agent信息 ||$http_cookie | 客户端cookie信息 ||$remote_addr | 客户端的IP地址 ||$remote_port | 客户端的端口 ||$server_protocol | 请求使用的协议,如HTTP/1.0、·HTTP/1.1` ||$server_addr | 服务器地址 ||$server_name| 服务器名称||$server_port|服务器的端口号|解决跨域先追本溯源以下,跨域究竟是怎么回事。跨域的定义同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。通常不允许不同源间的读操作。同源的定义如果两个页面的协议,端口(如果有指定)和域名都相同,则两个页面具有相同的源。nginx解决跨域的原理例如:前端server的域名为:fe.server.com后端服务的域名为:dev.server.com现在我在fe.server.com对dev.server.com发起请求一定会出现跨域。现在我们只需要启动一个nginx服务器,将server_name设置为fe.server.com,然后设置相应的location以拦截前端需要跨域的请求,最后将请求代理回dev.server.com。如下面的配置:server { listen 80; server_name fe.server.com; location / { proxy_pass dev.server.com; }}这样可以完美绕过浏览器的同源策略:fe.server.com访问nginx的fe.server.com属于同源访问,而nginx对服务端转发的请求不会触发浏览器的同源策略。请求过滤根据状态码过滤error_page 500 501 502 503 504 506 /50x.html; location = /50x.html { #将跟路径改编为存放html的路径。 root /root/static/html; }根据URL名称过滤,精准匹配URL,不匹配的URL全部重定向到主页。location / { rewrite ^.$ /index.html redirect;}根据请求类型过滤。if ( $request_method !~ ^(GET|POST|HEAD)$ ) { return 403; }配置gzipGZIP是规定的三种标准HTTP压缩格式之一。目前绝大多数的网站都在使用 GZIP 传输 HTML、CSS、JavaScript 等资源文件。对于文本文件,GZip 的效果非常明显,开启后传输所需流量大约会降至 1/4 ~ 1/3。并不是每个浏览器都支持gzip的,如何知道客户端是否支持gzip呢,请求头中的Accept-Encoding来标识对压缩的支持。启用gzip同时需要客户端和服务端的支持,如果客户端支持gzip的解析,那么只要服务端能够返回gzip的文件就可以启用gzip了,我们可以通过nginx的配置来让服务端支持gzip。下面的respone中content-encoding:gzip,指服务端开启了gzip的压缩方式。 gzip on; gzip_http_version 1.1; gzip_comp_level 5; gzip_min_length 1000; gzip_types text/csv text/xml text/css text/plain text/javascript application/javascript application/x-javascript application/json application/xml;gzip开启或者关闭gzip模块默认值为 off可配置为 on / offgzip_http_version启用 GZip 所需的 HTTP 最低版本默认值为 HTTP/1.1这里为什么默认版本不是1.0呢?HTTP 运行在 TCP 连接之上,自然也有着跟 TCP 一样的三次握手、慢启动等特性。启用持久连接情况下,服务器发出响应后让TCP连接继续打开着。同一对客户/服务器之间的后续请求和响应可以通过这个连接发送。为了尽可能的提高 HTTP 性能,使用持久连接就显得尤为重要了。HTTP/1.1 默认支持 TCP 持久连接,HTTP/1.0 也可以通过显式指定 Connection: keep-alive 来启用持久连接。对于 TCP 持久连接上的 HTTP 报文,客户端需要一种机制来准确判断结束位置,而在 HTTP/1.0 中,这种机制只有 Content-Length。而在HTTP/1.1 中新增的 Transfer-Encoding: chunked 所对应的分块传输机制可以完美解决这类问题。nginx同样有着配置chunked的属性chunked_transfer_encoding,这个属性是默认开启的。Nginx 在启用了GZip的情况下,不会等文件 GZip 完成再返回响应,而是边压缩边响应,这样可以显著提高 TTFB(Time To First Byte,首字节时间,WEB 性能优化重要指标)。这样唯一的问题是,Nginx 开始返回响应时,它无法知道将要传输的文件最终有多大,也就是无法给出 Content-Length 这个响应头部。所以,在HTTP1.0中如果利用Nginx 启用了GZip,是无法获得 Content-Length 的,这导致HTTP1.0中开启持久链接和使用GZip只能二选一,所以在这里gzip_http_version默认设置为1.1。gzip_comp_level压缩级别,级别越高压缩率越大,当然压缩时间也就越长(传输快但比较消耗cpu)。默认值为 1压缩级别取值为1-9gzip_min_length设置允许压缩的页面最小字节数,Content-Length小于该值的请求将不会被压缩默认值:0当设置的值较小时,压缩后的长度可能比原文件大,建议设置1000以上gzip_types要采用gzip压缩的文件类型(MIME类型)默认值:text/html(默认不压缩js/css)负载均衡什么是负载均衡如上面的图,前面是众多的服务窗口,下面有很多用户需要服务,我们需要一个工具或策略来帮助我们将如此多的用户分配到每个窗口,来达到资源的充分利用以及更少的排队时间。把前面的服务窗口想像成我们的后端服务器,而后面终端的人则是无数个客户端正在发起请求。负载均衡就是用来帮助我们将众多的客户端请求合理的分配到各个服务器,以达到服务端资源的充分利用和更少的请求时间。nginx如何实现负载均衡Upstream指定后端服务器地址列表upstream balanceServer { server 10.1.22.33:12345; server 10.1.22.34:12345; server 10.1.22.35:12345;}在server中拦截响应请求,并将请求转发到Upstream中配置的服务器列表。 server { server_name fe.server.com; listen 80; location /api { proxy_pass http://balanceServer; } }上面的配置只是指定了nginx需要转发的服务端列表,并没有指定分配策略。nginx实现负载均衡的策略轮询策略默认情况下采用的策略,将所有客户端请求轮询分配给服务端。这种策略是可以正常工作的,但是如果其中某一台服务器压力太大,出现延迟,会影响所有分配在这台服务器下的用户。upstream balanceServer { server 10.1.22.33:12345; server 10.1.22.34:12345; server 10.1.22.35:12345;}最小连接数策略将请求优先分配给压力较小的服务器,它可以平衡每个队列的长度,并避免向压力大的服务器添加更多的请求。upstream balanceServer { least_conn; server 10.1.22.33:12345; server 10.1.22.34:12345; server 10.1.22.35:12345;}最快响应时间策略依赖于NGINX Plus,优先分配给响应时间最短的服务器。upstream balanceServer { fair; server 10.1.22.33:12345; server 10.1.22.34:12345; server 10.1.22.35:12345;}客户端ip绑定来自同一个ip的请求永远只分配一台服务器,有效解决了动态网页存在的session共享问题。upstream balanceServer { ip_hash; server 10.1.22.33:12345; server 10.1.22.34:12345; server 10.1.22.35:12345;}静态资源服务器location ~ .(png|gif|jpg|jpeg)$ { root /root/static/; autoindex on; access_log off; expires 10h;# 设置过期时间为10小时 }匹配以png|gif|jpg|jpeg为结尾的请求,并将请求转发到本地路径,root中指定的路径即nginx本地路径。同时也可以进行一些缓存的设置。小结nginx的功能非常强大,还有很多需要探索,上面的一些配置都是公司配置的真实应用(精简过了),如果您有什么意见或者建议,欢迎在下方留言… ...

March 11, 2019 · 2 min · jiezi

彻底弄懂函数防抖和函数节流

原博客地址,欢迎star函数防抖和节流函数防抖和函数节流:优化高频率执行js代码的一种手段,js中的一些事件如浏览器的resize、scroll,鼠标的mousemove、mouseover,input输入框的keypress等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能。为了优化体验,需要对这类事件进行调用次数的限制。函数防抖在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。根据函数防抖思路设计出第一版的最简单的防抖代码:var timer; // 维护同一个timerfunction debounce(fn, delay) { clearTimeout(timer); timer = setTimeout(function(){ fn(); }, delay);}用onmousemove测试一下防抖函数:// testfunction testDebounce() { console.log(’test’);}document.onmousemove = () => { debounce(testDebounce, 1000);}上面例子中的debounce就是防抖函数,在document中鼠标移动的时候,会在onmousemove最后触发的1s后执行回调函数testDebounce;如果我们一直在浏览器中移动鼠标(比如10s),会发现会在10 + 1s后才会执行testDebounce函数(因为clearTimeout(timer)),这个就是函数防抖。在上面的代码中,会出现一个问题,var timer只能在setTimeout的父级作用域中,这样才是同一个timer,并且为了方便防抖函数的调用和回调函数fn的传参问题,我们应该用闭包来解决这些问题。优化后的代码:function debounce(fn, delay) { var timer; // 维护一个 timer return function () { var _this = this; // 取debounce执行作用域的this var args = arguments; if (timer) { clearTimeout(timer); } timer = setTimeout(function () { fn.apply(_this, args); // 用apply指向调用debounce的对象,相当于_this.fn(args); }, delay); };}测试用例:// testfunction testDebounce(e, content) { console.log(e, content);}var testDebounceFn = debounce(testDebounce, 1000); // 防抖函数document.onmousemove = function (e) { testDebounceFn(e, ‘debounce’); // 给防抖函数传参}使用闭包后,解决传参和封装防抖函数的问题,这样就可以在其他地方随便将需要防抖的函数传入debounce了。函数节流每隔一段时间,只执行一次函数。定时器实现节流函数:请仔细看清和防抖函数的代码差异function throttle(fn, delay) { var timer; return function () { var _this = this; var args = arguments; if (timer) { return; } timer = setTimeout(function () { fn.apply(_this, args); timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器 }, delay) }}测试用例:function testThrottle(e, content) { console.log(e, content);}var testThrottleFn = throttle(testThrottle, 1000); // 节流函数document.onmousemove = function (e) { testThrottleFn(e, ’throttle’); // 给节流函数传参}上面例子中,如果我们一直在浏览器中移动鼠标(比如10s),则在这10s内会每隔1s执行一次testThrottle,这就是函数节流。函数节流的目的,是为了限制函数一段时间内只能执行一次。因此,定时器实现节流函数通过使用定时任务,延时方法执行。在延时的时间内,方法若被触发,则直接退出方法。从而,实现函数一段时间内只执行一次。根据函数节流的原理,我们也可以不依赖 setTimeout实现函数节流。时间戳实现节流函数:function throttle(fn, delay) { var previous = 0; // 使用闭包返回一个函数并且用到闭包函数外面的变量previous return function() { var _this = this; var args = arguments; var now = new Date(); if(now - previous > delay) { fn.apply(_this, args); previous = now; } }}// testfunction testThrottle(e, content) { console.log(e, content);}var testThrottleFn = throttle(testThrottle, 1000); // 节流函数document.onmousemove = function (e) { testThrottleFn(e, ’throttle’); // 给节流函数传参}其实现原理,通过比对上一次执行时间与本次执行时间的时间差与间隔时间的大小关系,来判断是否执行函数。若时间差大于间隔时间,则立刻执行一次函数。并更新上一次执行时间。异同比较相同点:都可以通过使用 setTimeout 实现。目的都是,降低回调执行频率。节省计算资源。不同点:函数防抖,在一段连续操作结束后,处理回调,利用clearTimeout 和 setTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能。函数防抖关注一定时间连续触发的事件只在最后执行一次,而函数节流侧重于一段时间内只执行一次。常见应用场景函数防抖的应用场景连续的事件,只需触发一次回调的场景有:搜索框搜索输入。只需用户最后一次输入完,再发送请求手机号、邮箱验证输入检测窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。函数节流的应用场景间隔一段时间执行一次回调的场景有:滚动加载,加载更多或滚到底部监听谷歌搜索框,搜索联想功能高频点击提交,表单重复提交文档中出现的源代码都在这里: 防抖、节流参考资料:浅析函数防抖与函数节流JavaScript专题系列-防抖和节流7分钟理解JS的节流、防抖及使用场景防抖、节流可能这些参考资料中有某些错误,但是表示感谢,博客中有些内容用了里面的资料。 ...

March 9, 2019 · 1 min · jiezi

利用有序高效实施交并差集合运算

【摘要】 看起来很简单的集合运算放在大数据的场景下,如果还想获得高性能就需要充分了解数据特征和计算特征才能设计出高效算法。充分利用序运算就是一种好办法!不妨去乾学院看看:利用有序高效实施交并差集合运算 交并差是常见的集合运算,SQL 中对应的 intersect/union/minus 计算也很简单。不过当数据量较大时,这类集合运算性能往往偏低,尤其当参与计算的数据量超过内存容量时,性能表现会十分糟糕。 本文专门针对这种情况下的高性能计算(HPC)需求,讨论如何使用集算器 SPL 语言通过有序计算思路显著提高大数据量下交并差三类集合运算的性能。下面讨论中使用了一个实际用户在数据库选型时的评测用例:数据基于数据库的 2 个表,共计 105 亿行数据,执行相关运算后,以输出第一批 500 条记录所用时间来衡量哪个数据库性能更优。数据描述索引样例数据a1-a52 列值:2018-01-07 00:00:00,8888888888,MQJqxnXMLM,ccTTCC7755,aaa8,ppppaaaavv,gggggttttt,MQJqxnXMLM,ccTTCC7755,aaa8,ppppaaaavv,gggggttttt,MQJqxnXMLM,ccTTCC7755,aaa8,ppppaaaavv,gggggttttt,MQJqxnXMLM,ccTTCC7755,aaa8,ppppaaaavv,gggggttttt,MQJqxnXMLM,ccTTCC7755,aaa8,ppppaaaavv,gggggttttt,MQJqxnXMLM,ccTTCC7755,aaa8,ppppaaaavv,gggggttttt,MQJqxnXMLM,ccTTCC7755,aaa8,ppppaaaavv,gggggttttt,MQJqxnXMLM,ccTTCC7755,aaa8,ppppaaaavv,gggggttttt,MQJqxnXMLM,ccTTCC7755,aaa8,ppppaaaavv,gggggttttt,MQJqxnXMLM,ccTTCC7755,aaa8,ppppaaaavv,gggggttttt2018-01-07 00:00:00,4444444444,dv@bi-lyMF,qqoovv22ww,)))777FFF4,jjjjIIIIVV,aaaaaRRRRR,dv@bi-lyMF,qqoovv22ww,)))777FFF4,jjjjIIIIVV,aaaaaRRRRR,dv@bi-lyMF,qqoovv22ww,)))777FFF4,jjjjIIIIVV,aaaaaRRRRR,dv@bi-lyMF,qqoovv22ww,)))777FFF4,jjjjIIIIVV,aaaaaRRRRR,dv@bi-lyMF,qqoovv22ww,)))777FFF4,jjjjIIIIVV,aaaaaRRRRR,dv@bi-lyMF,qqoovv22ww,)))777FFF4,jjjjIIIIVV,aaaaaRRRRR,dv@bi-lyMF,qqoovv22ww,)))777FFF4,jjjjIIIIVV,aaaaaRRRRR,dv@bi-lyMF,qqoovv22ww,)))777FFF4,jjjjIIIIVV,aaaaaRRRRR,dv@bi-lyMF,qqoovv22ww,)))777FFF4,jjjjIIIIVV,aaaaaRRRRR,dv@bi-lyMF,qqoovv22ww,)))777FFF4,jjjjIIIIVV,aaaaaRRRRR2018-01-07 00:00:00,9999999999,bxk3J/2YDd,ppvv–88,uuuNNNBBBA,BBBBhhhhjj,_____PPPPP,bxk3J/2YDd,ppvv–88,uuuNNNBBBA,BBBBhhhhjj,_____PPPPP,bxk3J/2YDd,ppvv–88,uuuNNNBBBA,BBBBhhhhjj,_____PPPPP,bxk3J/2YDd,ppvv–88,uuuNNNBBBA,BBBBhhhhjj,_____PPPPP,bxk3J/2YDd,ppvv–88,uuuNNNBBBA,BBBBhhhhjj,_____PPPPP,bxk3J/2YDd,ppvv–88,uuuNNNBBBA,BBBBhhhhjj,_____PPPPP,bxk3J/2YDd,ppvv–88,uuuNNNBBBA,BBBBhhhhjj,_____PPPPP,bxk3J/2YDd,ppvv–88,uuuNNNBBBA,BBBBhhhhjj,_____PPPPP,bxk3J/2YDd,ppvv–88,uuuNNNBBBA,BBBBhhhhjj,_____PPPPP,bxk3J/2YDd,ppvv–88,uuuNNNBBBA,BBBBhhhhjj,_____PPPPP测试用例l 交集(intersect)select * from A, B where a1>‘2018-01-07 02:14:43’ and a1 < ‘2018-01-07 04:14:43’ and a3=b1 or a7 = b2intersectselect * from A, B where a1>‘2018-01-07 12:14:43’ and a1 < ‘2018-01-07 14:14:43’ and a3=b1 or a7=b2l 并集(union)select * from A, B where a1>‘2018-01-07 02:14:43’ and a1 < ‘2018-01-07 04:14:43’ and a3=b1 or a7 = b2unionselect * from A, B where a1>‘2018-01-07 12:14:43’ and a1 < ‘2018-01-07 14:14:43’ and a3=b1 or a7=b2l 差集(minus)select * from A, B where a1>‘2018-01-07 02:14:43’ and a1 < ‘2018-01-07 04:14:43’ and a3=b1 or a7 = b2minusselect * from A, B where a1>‘2018-01-07 12:14:43’ and a1 < ‘2018-01-07 14:14:43’ and a3=b1 or a7=b2用例分析 分析上述 SQL 可以发现,此计算场景为大数据量的多对多集合运算。查询条件的前半段(a1>‘2018-01-07 02:14:43’ and a1 < ‘2018-01-07 04:14:43’ and a3=b1)是 A 表 2 个小时内的数据与 B 表进行多对多关联;而后半段(or a7 = b2)则是 A 表全量数据和 B 表进行多对多关联。因此,这个用例主要考察的是大表 A 和小表 B 多对多关联后的集合运算性能。 实测时,该 SQL 使用 MPP 数据库得不到查询结果(运行时间超过 1 小时),因为数据量很大,内存无法容纳全部数据,从而造成数据库在运算时频繁进行磁盘交互,导致整体性能极低。 按照我们一贯的思路,要实施高性能计算必须设计符合数据特征和计算特征的算法,而不是简单地使用通用的算法。这里,为了避免过多的磁盘交互(这也是大数据规模计算的首要考虑目标),最好只遍历一次 A 表就能完成计算。观察数据可以发现,A 表包含时间字段(a1),而且在时间字段(a1)和关联字段(a3、a7)上均建有索引,同样 B 表的两个字段(b1、b2)也建有索引,这样,我们就可以设计出这样的算法:1) 根据 A 表数据生成的特点,逐秒读取 A 表数据(每秒 24000 条);2) 针对每秒的数据循环处理,根据过滤条件逐条与 B 表关联,返回关联后结果;3) 对两部分数据,即用于交并差的两个集合进行集合运算。通过以上三步就可以完成全部计算,而整个过程中对 A 表只遍历了 2 次(分别得到用于交并差的两个集合)。当然,整个过程中由于数据量太大,集算器将通过延迟游标的方式进行归并,游标归并时数据需要事先排序,所以在 1)和 2) 步之间还需要对每秒的 24000 条数据按照关联字段和其他字段排序,会产生一些额外的开销。下面是具体的集算器 SPL 脚本。SPL 实现 这里分主子两个程序,主程序调用子程序分别获得交 / 并 / 差运算的两个集合并进行集合运算,子程序则根据参数计算集合,也就是说用例中的交并差三类计算可以使用同一个子程序脚本。子程序脚本(case1_cursor.dfx)A1:在 otherCols 中记录 A 表 52 个字段中除参与运算的 a1,a3,a7 外其他所有字段名称,用于生成 SQL 查询A2:连接数据库A3:SQL 语句串,用于根据条件查询 A 表所有列数据A4:查询 B 表数据,针对 b1,b2 进行分组计数(以便在后续计算中减少比较次数),并按 b1,b2 排序(用于后续有序归并)A5:按照 5 天时间内的秒数进行循环B5:每次循环中在起始时间(2018-01-07 00:00:00)上加相应的秒数,查询那一秒产生的数据(24000 条)B6:按照关联字段以及其他字段排序B7:循环处理一秒内的每条 A 表数据C7:根据单条 A 表数据,在 B 表中查找符合条件的记录C8:返回计算后包含 A 表和 B 表所有字段值的结果集,这里使用了 A.news() 函数,用来计算得到序表 / 排列的字段值合并生成的新序表 / 排列,具体用法请参考http://doc.raqsoft.com.cn/esproc/func/news.html主程序脚本交集(intersect)A1,A2:通过 cursor()函数调用子程序脚本,并传入 2 个时间段参数;cursor() 函数原理请参考:《百万级分组大报表开发与呈现》A3:根据子程序返回的游标序列进行归并,使用 @i 选项完成交集运算A4:从游标中取出 500 条记录,并关闭游标(@x 选项)并集(union)A3:使用 @u 选项完成并集计算,其他 SPL 脚本完全相同差集(minus)A3:使用 @d 选项完成并集计算,其他 SPL 脚本完全相同性能表现下表对集算器 SPL 和数据库 SQL 分别输出第一个 500 条结果集的时间进行了比较:显然,交集和并集计算的性能得到了极大的提升。为什么差集运算很慢?差集运算依然很慢的原因是由数据特征所决定的。由于多对多关联后重复记录较多,要计算出符合条件的差集仍旧要遍历完 A 表(而另外两个计算获得 500 条结果集就可以不再遍历了),因此性能主要消耗在 IO 取数上。总结高性能算法需要根据数据和计算特征进行针对性设计,这要求程序猿首先能够想出高性能算法,然后以不太复杂的手段加以实现,否则就没有可行性了。对于 SQL 体系来说,由于其封闭性原因,一些高效算法可能即使能设计出来也很难,甚至无法实现。而集算器 SPL 则极大地改善了这个问题,使用者可以在设计出高性能算法后,基于 SPL 体系快速实现。 ...

March 8, 2019 · 2 min · jiezi

MySQL运维实战 之 PHP访问MySQL你使用对了吗

大家都知道,slow query系统做的好不好,直接决定了解决slow query的效率问题一个数据库管理平台,拥有一个好的slow query系统,基本上就拥有了解锁性能问题的钥匙但是今天主要分享的并不是平台,而是在平台中看到的奇葩指数五颗星的slow issue好了,关子卖完了,直接进入正题一、症状一堆如下慢查询# User@Host: cra[cra] @ [xx] Id: 3352884621# Query_time: 0.183673 Lock_time: 0.000000 Rows_sent: 0 Rows_examined: 0use xx_db;SET timestamp=1549900927;# administrator command: Prepare;# Time: 2019-02-12T00:02:07.516803+08:00# User@Host: cra[cra] @ [xx] Id: 3351119968# Query_time: 0.294081 Lock_time: 0.000000 Rows_sent: 0 Rows_examined: 0SET timestamp=1549900927;# administrator command: Prepare;从我们的监控图上可以看到,每天不定时间段的slow query 总数在攀升,但是却看不到任何query 语句这是我接触到的slow query优化案例中从来没有过的情况,比较好奇,也比较兴奋,至此决心要好好看看这个问题二、排查要解决这个问题,首先想到的是,如何复现这个问题,如何模拟复现这个症状MySQL客户端 模拟prepare* 模拟root:xx> prepare stmt1 from ‘select * from xx_operation_log where id = ?’;Query OK, 0 rows affected (0.00 sec)Statement prepared* 结果# Time: 2019-02-14T14:14:50.937462+08:00# User@Host: root[root] @ localhost [] Id: 369# Query_time: 0.000105 Lock_time: 0.000000 Rows_sent: 0 Rows_examined: 0SET timestamp=1550124890;prepare stmt1 from ‘select * from xx_operation_log where id = ?’;结论是: MySQL client 模拟出来的prepare 并不是我们期待的,并没有得到我们想要的 administrator command: Prepareperl 模拟prepare#!/usr/bin/perluse DBI;my $dsn = “dbi:mysql:database=${db_name};hostname=${db_host};port=${db_port}”;#数据源#获取数据库句柄my $dbh = DBI->connect(“DBI:mysql:database=xx;host=xx”, “xx”, “xx”, {‘RaiseError’ => 1});my $sql = qq{select * from xx_operation_log where id in (?)};my $sth = $dbh->prepare($sql);$sth->bind_param (1, ‘100’);sleep 3;$sth->execute();结论是:跟MySQL客户端一样,同样是看不到administrator command: Preparephp 模拟prepare1. 官方网址:https://dev.mysql.com/doc/apis-php/en/apis-php-mysqli-stmt.prepare.html<?php$link = mysqli_connect(“xx”, “dba”, “xx”, “xx_db”);/* check connection /if (mysqli_connect_errno()) { printf(“Connect failed: %s\n”, mysqli_connect_error()); exit();}$city = ‘1’;/ create a prepared statement /$stmt = mysqli_stmt_init($link);if (mysqli_stmt_prepare($stmt, ‘select * from xx_operation_log where id in (1,2,3)’){ / bind parameters for markers / / mysqli_stmt_bind_param($stmt, “s”, $city); /* execute query / mysqli_stmt_execute($stmt); / bind result variables / mysqli_stmt_bind_result($stmt, $district); / fetch value / mysqli_stmt_fetch($stmt); printf("%s is in district %s\n", $city, $district); / close statement / mysqli_stmt_close($stmt);}/ close connection */mysqli_close($link);?>php模拟得到的slow 结果[root@xx 20190211]# cat xx-slow.log | grep ‘administrator command: Prepare’ -B4 | grep ‘User@Host’ | grep ‘xx_rx’ | wc -l7891[root@xx 20190211]# cat xx-slow.log | grep ‘administrator command: Prepare’ -B4 | grep ‘User@Host’ | wc -l7908结论: 通过php代码,我们成功模拟出了想要的结果那顺藤摸瓜,抓取下这段时间有相同session id的整个sql执行过程MySQL开启slow=0的抓包模式可以定位到同一个session id(3415357118) 的 prepare + execute + close stmt# User@Host: xx_rx[xx_rx] @ [xx.xxx.xxx.132] Id: 3415357118# Query_time: 0.401453 Lock_time: 0.000000 Rows_sent: 0 Rows_examined: 0use xx_db;SET timestamp=1550017125;# administrator command: Prepare;# Time: 2019-02-13T08:18:45.624671+08:00–# User@Host: xx_rx[xx_rx] @ [xx.xxx.xxx.132] Id: 3415357118# Query_time: 0.001650 Lock_time: 0.000102 Rows_sent: 0 Rows_examined: 1use xx_db;SET timestamp=1550017125;update xx set updated_at = ‘2019-02-13 08:18:45’, has_sales_office_phone = 1, has_presale_permit = 1 where id = 28886;# Time: 2019-02-13T08:18:45.626138+08:00–# User@Host: xx_rx[xx_rx] @ [xx.xxx.xxx.132] Id: 3415357118# Query_time: 0.000029 Lock_time: 0.000000 Rows_sent: 0 Rows_examined: 1use xx_db;SET timestamp=1550017125;# administrator command: Close stmt;# Time: 2019-02-13T08:18:45.626430+08:00结论:我们发现,prepare时间的确很长,但是sql语句却执行的很快,这就很尴尬了本来是想通过抓包,看看是否能够验证我们的猜想: prepare的语句非常大,或者条件非常复杂,从而导致prepare在服务器端很慢结果发现query语句也都非常简单那么既然如此,我们就找了业务方,将对应业务的prepare方法一起看看结果发现,业务使用的是php-pdo的方式,所以我们就又有了如下发现php-pdo 两种prepare模式http://php.net/manual/zh/pdo.prepare.php1. 本地prepare $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES,true); 不会发送给MySQL Server2. 服务器端prepare $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES,false); 发送给MySQL Server验证两种prepare模式服务端prepare模式( ATTR_EMULATE_PREPARES = false)<?php$dbms=‘mysql’; //数据库类型$host=‘xxx’; //数据库主机名$dbName=‘test’; //使用的数据库$user=‘xx’; //数据库连接用户名$pass=‘123456’; //对应的密码$dsn="$dbms:host=$host;dbname=$dbName";try { $pdo = new PDO($dsn, $user, $pass); //初始化一个PDO对象 $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES,false); echo “—– prepare begin —–\n”; $stmt = $pdo->prepare(“select * from test.chanpin where id = ?”); echo “—– prepare after —–\n”; $stmt->execute([333333]); echo “—– execute after —–\n”; $rs = $stmt->fetchAll();} catch (PDOException $e) { die (“Error!: " . $e->getMessage() . “<br/>”);}strace -s200 -f php mysql1.php 跟踪大家可以看到这个模式下,prepare的时候,是将query+占位符 发送给服务端的本地prepare模式 (ATTR_EMULATE_PREPARES = true )<?php$dbms=‘mysql’; //数据库类型$host=‘xx’; //数据库主机名$dbName=‘test’; //使用的数据库$user=‘xx’; //数据库连接用户名$pass=‘123456’; //对应的密码$dsn="$dbms:host=$host;dbname=$dbName”;try { $pdo = new PDO($dsn, $user, $pass); //初始化一个PDO对象 $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES,true); echo “—– prepare begin —–\n”; $stmt = $pdo->prepare(“select * from test.chanpin where id = ?”); echo “—– prepare after —–\n”; $stmt->execute([333333]); echo “—– execute after —–\n”; $rs = $stmt->fetchAll();} catch (PDOException $e) { die (“Error!: " . $e->getMessage() . “<br/>”);}strace -s200 -f php mysql1.php 跟踪大家可以看到这个模式下,prepare的时候,是不会将query发送给服务端的,只有execute的时候才会发送跟业务方确认后,他们使用的是后者,也就是修改了默认值,他们原本是想提升数据库的性能,因为预处理后只需要传参数就好了但是对于我们的业务场景并不适合,我们的场景是频繁打开关闭连接,也就是预处理基本就用不到另外文档上面也明确指出prepared statements 性能会不好调整和验证如何验证业务方是否将prepare修改为local了呢?dba:(none)> show global status like ‘Com_stmt_prepare’;+——————+———–+| Variable_name | Value |+——————+———–+| Com_stmt_prepare | 716836596 |+——————+———–+1 row in set (0.00 sec)通过观察,发现这个值没有变化,说明调整已经生效总结prepare的优点1. 防止SQL注入2. 特定场景下提升性能 什么是特定场景: 就是先去服务端用占位符占位,后面可以直接发送请求来填空(参数值) 这样理论上来说, 你填空的次数非常多,性能才能发挥出来prepare的缺点1. 在服务器端的prepare毕竟有消耗,当并发量大,频繁prepare的时候,就会有性能问题2. 服务端的prepare模式还会带来的另外一个问题就是,排错和slow 优化有困难,因为大部分情况下是看不到真实query的3. 尽量设置php-pdo为 $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES,true) ,在本地prepare,不要给服务器造成额外压力建议1. 默认情况下,应该使用php-pdo的默认配置,采用本地prepare的方式,这样可以做到防SQL注入的效果,性能差不到哪里去2. 除非真的是有上述说的特定场景,可以考虑配置成服务器prepare模式,前提是要做好测试本文作者:兰春阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

March 8, 2019 · 3 min · jiezi

浅谈js防抖和节流

防抖和节流严格算起来应该属于性能优化的知识,但实际上遇到的频率相当高,处理不当或者放任不管就容易引起浏览器卡死。所以还是很有必要早点掌握的。(信我,你看完肯定就懂了)从滚动条监听的例子说起先说一个常见的功能,很多网站会提供这么一个按钮:用于返回顶部。这个按钮只会在滚动到距离顶部一定位置之后才出现,那么我们现在抽象出这个功能需求– 监听浏览器滚动事件,返回当前滚条与顶部的距离这个需求很简单,直接写:function showTop () { var scrollTop = document.body.scrollTop || document.documentElement.scrollTop; console.log(‘滚动条位置:’ + scrollTop);}window.onscroll = showTop但是!在运行的时候会发现存在一个问题:这个函数的默认执行频率,太!高!了!。 高到什么程度呢?以chrome为例,我们可以点击选中一个页面的滚动条,然后点击一次键盘的【向下方向键】,会发现函数执行了8-9次!然而实际上我们并不需要如此高频的反馈,毕竟浏览器的性能是有限的,不应该浪费在这里,所以接着讨论如何优化这种场景。防抖(debounce)基于上述场景,首先提出第一种思路:在第一次触发事件时,不立即执行函数,而是给出一个期限值比如200ms如果在200ms内没有再次触发滚动事件,那么久执行函数如果在200ms内再次触发滚动事件,那么当前的计时取消,重新开始计时效果:如果短时间内大量触发同一事件,只会执行一次函数。实现:既然前面都提到了计时,那实现的关键就在于setTimeOut这个函数,由于还需要一个变量来保存计时,考虑维护全局纯净,可以借助闭包来实现:/** fn [function] 需要防抖的函数* delay [number] 毫秒,防抖期限值*/function debounce(fn,delay){ let timer = null //借助闭包 return function() { if(timer){ clearTimeout(timer) //进入该分支语句 timer = setTimeOut(fn,delay) 说明如果当前正在计时过程中,又触发了相同事件。所以取消当前的计时,重新开始计时 }else{ timer = setTimeOut(fn,delay) // 进入该分支说明当前并没有在计时,那么就开始一个计时 } }}当然 上述代码是为了贴合思路,方便理解(这么贴心不给个赞咩?),写完会发现其实 time = setTimeOut(fn,delay)是一定会执行的,所以可以稍微简化下:/*****************************简化后的分割线 *****************************/function debounce(fn,delay){ let timer = null //借助闭包 return function() { if(timer){ clearTimeout(timer) } timer = setTimeout(fn,delay) // 简化写法 }}// 然后是旧代码function showTop () { var scrollTop = document.body.scrollTop || document.documentElement.scrollTop; console.log(‘滚动条位置:’ + scrollTop);}window.onscroll = debounce(showTop,1000) // 为了方便观察效果我们取个大点的间断值,实际使用根据需要来配置此时会发现,必须在停止滚动1秒以后,才会打印出滚动条位置。到这里,已经把防抖实现了,现在给出定义:对于短时间内连续触发的事件(上面的滚动事件),防抖就是限制某个时间段内(上面的1000毫秒)事件处理函数只会执行一次。节流(throttle)继续思考,使用上面的防抖方案来处理问题的结果是:如果在限定时间段内,不断触发滚动事件(比如某个用户闲着无聊,按住滚动不断的拖来拖去),只要不停止触发,理论上就永远不会输出当前距离顶部的距离。但是如果产品同学的期望处理方案是即使用户不断拖动滚动条,也能再某个时间间隔之后给出反馈呢?(此处暂且不论哪种方案更合适,既然产品爸爸说话了我们就先考虑怎么实现)其实很简单:在前面讲防抖的时候,遇到期限内的连续触发,我们的处理方案是【重新计时】;而这里我们不重新计时,而是类似控制阀门一样定期开放,也就是执行完一次让该函数暂时失效,等到一段时间后再重新激活。效果:如果短时间内大量触发同一事件,那么在执行一次函数之后,该函数在指定的时间期限内不再工作,直至过了这段时间才重新生效。实现 这里借助setTimeout来实现一个简单的方案,我们加上一个状态位valid来表示当前函数是否处于工作状态:function throttle(fn,delay){ let valid = true return function() { if(!valid){ //休息时间 暂不接客 return false } // 工作时间,执行函数并且在间隔期内把状态位设为无效 valid = false setTimeout(() => { fn() valid = true; }, delay) }}/ 节流函数并不止上面这种实现方案, 例如可以完全不借助setTimeout,可以把valid换成时间戳,然后利用时间戳想减是否大于指定间隔时间来写。 又或者不额外增加状态位 直接将setTimeout的返回标记当做状态位,判断当前定时器是否存在,并且在执行fn之后消除定时器即可,原理都一样 */// 以下照旧function showTop () { var scrollTop = document.body.scrollTop || document.documentElement.scrollTop; console.log(‘滚动条位置:’ + scrollTop);}window.onscroll = throttle(showTop,1000) 运行以上代码的结果是:如果一直拖着滚动条进行滚动,那么会以1s的时间间隔,持续输出当前位置和顶部的距离其他应用场景举例讲完了这两个技巧,下面介绍一下平时开发中常遇到的场景:搜索框input事件,例如要支持输入实时搜索可以使用节流方案(间隔一段时间就必须查询相关内容),或者实现输入间隔大于某个值(如500ms),就当做用户输入完成,然后开始搜索,具体使用哪种方案要看业务需求。页面resize事件,常见于需要做页面适配的时候。需要根据最终呈现的页面情况进行dom渲染(这种情形一般是使用防抖,因为只需要判断最后一次的变化情况)思考总结上述内容基于防抖和节流的核心思路设计了简单的实现算法,但是不代表实际的库(例如undercore js)的源码就直接是这样的,最起码的可以看出,在上述代码实现中,因为showTop本身的很简单,无需考虑作用域和参数传递,所以连apply都没有用到,实际上肯定还要考虑传递argument以及上下文环境(毕竟apply需要用到this对象)。这里的相关知识在本专栏《柯里化》和《this对象》的文章里也有提到。本文依然坚持突出核心代码,尽可能剥离无关功能点的思路行文因此不做赘述。惯例:如果内容有错误的地方欢迎指出(觉得看着不理解不舒服想吐槽也完全没问题);如果有帮助,欢迎点赞和收藏,转载请征得同意后著明出处,如果有问题也欢迎私信交流,主页有邮箱地址 ...

March 7, 2019 · 1 min · jiezi

前端性能优化——惰性加载

从需求出发:在实际的项目开发中,我遇到了一个这样的需求:一个页面模块有很多列表数据展示,每条数据都带有图片,而首次展示的图片只需要不到10张,那么我们还要一次性把所有图片都加载出来吗?显然这是不对的,不仅影响页面渲染速度,还浪费带宽(因为需要对列表进行拖动排序,需加载出全部列表,不能做分页)。我们可以在浏览器滚动到一定的位置的时候进行下载,这也就是们通常所说的惰性加载,技术上现实其中要用的技术就是图片懒加载–到可视区域再加载。实现方案:1、默认不加载图片,只加载占位符2、组件滚动条变化3、计算可视区域,触发条件4、<img>标签src属性加载资源知识点:scrollTop:外框元素的滚动高度offsetTop:元素相对于最近的包含该元素的定位元素(具有position属性且不是static)边框的距离。如果没有定位的元素,则默认body。offsetHeight:它返回该元素的像素高度,高度包含该元素的垂直内边距和边框,且是一个整数。计算:可视区域的高度(offsetHeight) + 滚动条卷去的高度(scrollTop) >= 元素相对于外框的距离(offsetTop) - 偏移量 (提前加载)代码实现:页面结构<style type=“text/css”>.container{ width:200px; height:200px; position:relative; overflow-y:scroll;}.img-area{ width:100px; height:100px;}</style><div class=“container”> <div class=“img-area”> <img class=“pic” alt=“loading” data-src="./img/img1.png" src=“image-placeholder-logo.svg”> </div> <div class=“img-area”> <img class=“pic” alt=“loading” data-src="./img/img2.png" src=“image-placeholder-logo.svg”> </div> <div class=“img-area”> <img class=“pic” alt=“loading” data-src="./img/img3.png" src=“image-placeholder-logo.svg”> </div> <div class=“img-area”> <img class=“pic” alt=“loading” data-src="./img/img4.png" src=“image-placeholder-logo.svg”> </div> <div class=“img-area”> <img class=“pic” alt=“loading” data-src="./img/img5.png" src=“image-placeholder-logo.svg”> </div></div>src属性统一用一个占位图片,alt属性是在图像无法显示时的替代文本。 data-src是自定义属性,用来保存实际的图片地址,可以通过HTMLElement.dataset来访问。脚本代码: var container = document.querySelector(’.container’); container.onscroll = function(){ checkImgs(); } function isInSight(el) { var sTop = container.scrollTop; var oHeight = container.offsetHeight; var oTop = el.offsetTop; return sTop + oHeight > oTop; } function checkImgs() { var imgs = document.querySelectorAll(’.pic’); Array.from(imgs).forEach(el => { if (isInSight(el)) { loadImg(el); } }) } function loadImg(el) { var source = el.dataset.src; el.src = source; } checkImgs();可以看出,页面加载时候,绑定外框的scroll事件,随着用户向下滚动鼠标,把img的src赋予新的值,网络重新发起请求,拉取图片。这里应该是有一些可以优化的地方,比如1、可以只监听向下滚动时候的事件,并设置延时(使用截流函数),防制多次调用回调函数。2、可以设一个标识符标识已经加载图片的index,当滚动条滚动时就不需要遍历所有的图片,只需要遍历未加载的图片即可。3、可以在计算的时候,增加偏移数据,提前加载图片,并使用淡入效果,提高流畅性。另一种计算方法:getClientRects()方法返回的一组矩形的集合, 即:是与该元素相关的CSS 边框集合 。包含边框的只读属性left、top、right和bottom,单位为像素。除了 width 和 height 外的属性都是相对于视口的左上角位置而言的。这种条件下,假设bound = el.getBoundingClientRect(),随着滚动条的向下滚动,bound.top会越来越小,也就是图片到可视区域顶部的距离越来越小,当bound.top===clientHeight时,图片的上沿应该是位于可视区域下沿的位置的临界点,再滚动一点点,图片就会进入可视区域。也就是说,在bound.top<=clientHeight时,图片是在可视区域内的。function isInSight(el) { var bound = el.getBoundingClientRect(); var clientHeight = window.innerHeight; return bound.top <= clientHeight;} 进一步考虑:以上监听scroll,并计算元素位置来实现惰性加载。当数据达到一定量的时候,事件绑定和循环位置计算会消耗大量的性能,每次调用 getBoundingClientRect() 都会强制浏览器 重新计算整个页面的布局 ,可能给你的网站造成相当大的闪烁。这种方式有些美中不足。交叉观察器:IntersectionObserver 就是为此而生的,它是HTML5新增的api,可以检测一个元素是否可见,IntersectionObserver 能让你知道一个被观测的元素什么时候进入或离开浏览器的视口。它兼容性有限,Chrome 51+(发布于 2016-05-25)Android 5+ (Chrome 56 发布于 2017-02-06)Edge 15 (2017-04-11)iOS 不支持不过不用担心,WICG 提供了一个polyfill,可以兼容到以下版本:它的用法也很简单,类似于rxjs中的observe。var observe = new IntersectionObserver(callback, option); IntersectionObserver是浏览器原生提供的构造函数,接受两个参数:callback是可见性变化时的回调函数,option是配置对象(可选)。返回一个观测实例observe,可以指定观测哪个DOM节点。// 开始观察observe.observe(document.getElementById(’example’));callback = function(entries){ entries.forEach((entry) => { if (entry.isIntersecting) { //开始进入,交叉状态,在此处理图片逻辑。 } else { //已完全进入或完全离开 } });}// 停止观察observe.unobserve(element);// 关闭观察器observe.disconnect(); entries是一个数组,每个成员都是一个IntersectionObserverEntry对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,entries数组就会有两个成员。isIntersecting,返回一个布尔值, 如果目标元素与交叉区域观察者对象的根相交,则返回 true 。如果返回 true,则描述了变换到交叉时的状态;如果返回 false, 那么可以由此判断,变换是从交叉状态到非交叉状态。IntersectionObserverEntry对象提供了很多有用的属性,比如target是被观察的目标元素,是一个 DOM 节点对象,intersectionRatio是目标元素的可见比例,即DOM节点的可见面积和总面积的比例,完全可见时为1,完全不可见时小于等于0,可以通过此属性设置图片的透明度,做成淡出的效果。下拉无限滚动:在页面底部有一个loading状态标签。一旦标签可见,就表示用户到达了页面底部,从而加载新的条目放在标签的前面。这样做的好处是,比监听scroll和计算节省了很多性能消耗,现有IntersectionObserver可以很简单的应用。下面是实现方法:var intersectionObserver = new IntersectionObserver( function (entries) { // 如果不可见,就返回 if (entries[0].intersectionRatio <= 0) return; //在此加载新的数据});intersectionObserver.observe(document.getElementById(’loading’)); 小结:图片(不只有图片,主要是图片占用的资源最多最常见)惰性加载是一种网页优化技术。通过多种方案对比,使图片仅在浏览器当前视窗内出现时才加载该图片,达到减少首屏图片请求数,优化前端性能,提高用户体验。不管哪种方法,都有其自己的优势和劣势,掌握其中的原理,灵活应用才是最重要的。这对开发中遇到的问题及解决方法进行了总结,都是实战得来的经验,描述不清或者不对的地方,请多多指教。 ...

February 28, 2019 · 1 min · jiezi

线上模块XX 在高QPS下耗时剧增的原因排查总结

1、确定问题原因:1)场景复现:方式1:根据现场日志确定问题原因;方式2:压测复现。存在的问题:方式1:日志中没有保存现场;方式2:压测可能与线上不符;压测可能无法满足某些特殊条件。2)一些手段:a.如果问题只在特定机器出现,确定机器硬件配置是否相同,cpu、meminfo、系统配置等;b.分阶段、逐步细化各步骤的处理时间、队列积压长度等;c.可以使用一些辅助性能分析工具进行分析,代价是学习成本,场景不一定符合性能分析工具作用的发挥。最终发现的高qps时,耗时主要集中在信息流配图阶段和it_proc特征收集阶段。2、分析现象本质:1)it_proc:内部循环过多:按照it内部的参数默认值计算,最大循环次数达到百万级别,在其他特征收集基本0ms的情况下,it的耗时是不可接受的。2)信息流配图:分阶段、逐步细化各步骤的处理时间并不能发现问题原因,这种情况就不能怀疑单次执行内部函数的耗时上,很有可能是系统在做切换、同步等处理时导致的异常。对这一部分程序重新研读,发现调用的std标准函数random_shuffle有重大嫌疑,在此怀疑的基础上,注释掉相关代码,确实不再发生同样的问题,问题得到确认。问题参考资料:http://www.cplusplus.com/refe…is-random-shuffle-threadsafe-and-using-rand-r-if-it-is-nothttps://github.com/mariusmuja…3、问题修复方式:1)it_proc:和策略同学讨论修复方式,谁开发谁维护的原则,发现是按权重抽样算法过于复杂,最后确定已现有ot的处理方式进行修改;2)信息流配图:a. 信息流配图也存在大量循环,首先将不需要再循环内部执行的代码移到循环体外部,其次尽量在不影响功能的情况下降低循环次数。b.对于random_shuffle的问题,采用基于rand_r自定义随机生成器的方式调用template void random_shuffle (RandomAccessIterator first, RandomAccessIterator last,RandomNumberGenerator&& gen);实现数组打散。4、总结:1)性能问题排查的思路和步骤(定位问题比解决问题更加困难,无法定位问题就更谈不上解决问题)2)代码优化的原则:a.阿姆达尔定律:不经常使用的代码不需要做较多优化考虑,即让经常执行的路径运行更加高效,而运行稀少的路径正确执行。b.先保证代码的正确性,再考虑优化。c.优化所需的时间常常是写代码时间的double。3)优化分为系统级别的优化和代码级别的优化系统级别的优化常常需要模块或子模块的重构,需要从顶端开始出方案和设计。推荐书目:《重构-改善既有代码的设计》代码级别的优化更多的是使用一下小技巧实现,如果可以参考这几个链接:http://www.jb51.net/article/5…http://blog.csdn.net/wind19/a…

February 27, 2019 · 1 min · jiezi

能用机器完成的,千万别堆工作量|持续集成中的性能自动化测试

1.背景当前闲鱼在精益开发模式下,整个技术团队面临了诸多的能力落地和挑战,尤其是效能方面的2-1-1的目标(2周需求交付周期,1周需求开发周期,1小时达到发布标准),具体可见 闲鱼工程师是如何构建持续集成流水线,让研发效率翻倍的 ,在这个大目标下,就必须把每个环节都做到极致。自动化的建设是决定CI成败的关键能力,今天分享一下闲鱼Android客户端性能自动化环节的实践。2.面临的问题2.1 主要是两个方面的问题工具缺失:目前淘宝系,对于线上性能水位的监控有一套完善的体系,但是针对新功能的性能测试,每个业务团队都有对应的性能专项小组,产出的工具都是根据自己业务特点的定制开发的,闲鱼客户端目前使用Flutter做为客户端主开发语言,对于Flutter性能数据的获取及UI自动化测试支撑工具目前是缺失的,同时业界对Flutter自动化和性能相关的实践几乎没有;测试工作量翻N倍(N=一个版本周期内的分支数):原先的开发模式是功能测试集成测试一起进行的,所以性能测试只需要针对集成后的包进行测试即可,到现在转变为泳道的开发模式,一个版本内会一般包含十几个左右的泳道分支甚至更多,我们必须确保每个泳道的分支的性能是达标的,如果有性能问题需要第一时间反馈出来,如果遗留到集成阶段,问题的排查(十几个分支中筛查),问题的解决将会耗费大量的时间,效率很难得到大的提升;2.2 问题思考体系化解决,要让每个泳道分支都得到有效测试覆盖,测试件能够自动化执行,持续反馈结果3. 解决方案综合上述问题,梳理如下解决方案:针对Flutter性能数据的获取(比如,Flutter有自己的SurfaceView,原有Native计算FPS的方式无法直接使用)针对Flutter UI自动化的实现(Flutter/Native UI混合栈的处理)性能自动化脚本 / 性能数据自动采集、上报 融入CI流程性能问题的通知 / 报表展示 / 分析3.1 性能数据[FPS]解析处理 adb shell dumpsys SurfaceFlinger –latency 的数据,详细请见文末参考链接(该方式兼容Flutter及Native的解决方案,已在Android4.x-9.x验证可行),处理SurfaceFlinger核心代码如下:dumpsys SurfaceFlinger –latency-clear#echo “dumpsys SurfaceFlinger…“if [[ $isflutter = 0 ]];then window=dumpsys window windows | grep mCurrent | $bb awk '{print $3}'|$bb tr -d '}' # Get the current window echo $windowfiif [[ $isflutter = 1 ]];then window=dumpsys SurfaceFlinger --list |grep '^SurfaceView'|$bb awk 'NR==1{print $0}'if [ -z “$window” ]; then window=“SurfaceView"fiecho $windowfi$bb usleep $sleep_tdumpsys SurfaceFlinger –latency “$window”|$bb awk -v time=$uptime -v target=$target -v kpi=$KPI ‘{if(NR==1){r=$1/1000000;if(r<0)r=$1/1000;b=0;n=0;w=1}else{if(n>0&&$0==”")O=1;if(NF==3&&$2!=0&&$2!=9223372036854775807){x=($3-$1)/1000000/r;if(b==0){b=$2;n=1;d=0;D=0;if(x<=1)C=r;if(x>1){d+=1;C=int(x)r;if(x%1>0)C+=r};if(x>2)D+=1;m=r;o=0}else{c=($2-b)/1000000;if(c>1000){O=1}else{n+=1;if(c>=r){C+=c;if(c>kpi)o+=1;if(c>=m)m=c;if(x>1)d+=1;if(x>2)D+=1;b=$2}else{C+=r;b=sprintf(”%.0f”,b+r1000000)}}};if(n==1)s=sprintf("%.3f",$2/1000000000)};if(n>0&&O==1){O=0;if(n==1)t=sprintf("%.3f",s+C/1000);else t=sprintf("%.3f",b/1000000000);T=strftime("%F %T",time+t)".“sprintf(”%.0f",(time+t)%11000);f=sprintf("%.2f",n1000/C);m=sprintf("%.0f",m);g=f/target;if(g>1)g=1;h=kpi/m;if(h>1)h=1;e=sprintf("%.2f",g60+h20+(1-o/n)*20);print s",“t”,“T”,“f+0”,“n”,“d”,“D”,“m”,“o”,“e”,“w;n=0;if($0==”"){b=0;w+=1}else{b=$2;n=1;d=0;D=0;if(x<=1)C=r;if(x>1){d+=1;C=int(x)*r;if(x%1>0)C+=r};if(x>2)D+=1;m=r;o=0}}}}’ >>$file[CPU]使用的是top的命令获取(该方式获取性能数据时,数据收集带来的损耗最少)export bb="/data/local/tmp/busybox"$bb top -b -n 1|$bb awk ‘NR==4{print NF-1}’[内存]解析 dumpsys meminfo $package 拿到 Java Heap,Java Heap Average,Java Heap Peak,Native Heap,Native Heap Average,Native Heap Peak,Graphics,Unknown,Pss 数据do_statistics() { ((COUNT+=1)) isExist="$(echo $OUTPUT | grep “Dalvik Heap”)" if [[ ! -n $isExist ]] ; then old_dumpsys=true else old_dumpsys=false fi if [[ $old_dumpsys = true ]] ; then java_heap="$(echo “$OUTPUT” | grep “Dalvik” | $bb awk ‘{print $6}’ | $bb tr -d ‘\r’)" else java_heap="$(echo “$OUTPUT” | grep “Dalvik Heap[^:]” | $bb awk ‘{print $8}’ | $bb tr -d ‘\r’)" fi echo “1."$JAVA_HEAP_TOTAL “2."$java_heap “3."$JAVA_HEAP_TOTAL ((JAVA_HEAP_TOTAL+=java_heap)) ((JAVA_HEAP_AVG=JAVA_HEAP_TOTAL/COUNT)) if [[ $java_heap -gt $JAVA_HEAP_PEAK ]] ; then JAVA_HEAP_PEAK=$java_heap fi if [[ $old_dumpsys = true ]] ; then native_heap="$(echo “$OUTPUT” | grep “Native” | $bb awk ‘{print $6}’ | $bb tr -d ‘\r’)” else native_heap="$(echo “$OUTPUT” | grep “Native Heap[^:]” | $bb awk ‘{print $8}’ | $bb tr -d ‘\r’ | $bb tr -d ‘\n’)” fi ((NATIVE_HEAP_TOTAL+=native_heap)) ((NATIVE_HEAP_AVG=NATIVE_HEAP_TOTAL/COUNT)) if [[ $native_heap -gt $NATIVE_HEAP_PEAK ]] ; then NATIVE_HEAP_PEAK=$native_heap fi g_Str=“Graphics” if [[ $OUTPUT == $g_Str ]] ; then echo “Found Graphics…” Graphics="$(echo “$OUTPUT” | grep “Graphics” | $bb awk ‘{print $2}’ | $bb tr -d ‘\r’)” else echo “Not Found Graphics…” Graphics=0 fi Unknown="$(echo “$OUTPUT” | grep “Unknown” | $bb awk ‘{print $2}’ | $bb tr -d ‘\r’)" total="$(echo “$OUTPUT” | grep “TOTAL”|$bb head -1| $bb awk ‘{print $2}’ | $bb tr -d ‘\r’)"}[流量]通过 dumpsys package packages 解析出当前待测试包来获取流量信息uid="$(dumpsys package packages|$bb grep -E “Package |userId”|$bb awk -v OFS=" " ‘{if($1==“Package”){P=substr($2,2,length($2)-2)}else{if(substr($1,1,6)==“userId”)print P,substr($1,8,length($1)-7)}}’|grep $package|$bb awk ‘{print $2}’)“echo “Net:"$uidinitreceive=$bb awk -v OFS=" " 'NR&gt;1{if($2=="wlan0"){wr[$4]+=$6;wt[$4]+=$8}else{if($2=="rmnet0"){rr[$4]+=$6;rt[$4]+=$8}}}END{for(i in wr){print i,wr[i]/1000,wt[i]/1000,"wifi"};for(i in rr){print i,rr[i]/1000,rt[i]/1000,"data"}}' /proc/net/xt_qtaguid/stats | grep $uid|$bb awk '{print $2}'inittransmit=$bb awk -v OFS=" " 'NR&gt;1{if($2=="wlan0"){wr[$4]+=$6;wt[$4]+=$8}else{if($2=="rmnet0"){rr[$4]+=$6;rt[$4]+=$8}}}END{for(i in wr){print i,wr[i]/1000,wt[i]/1000,"wifi"};for(i in rr){print i,rr[i]/1000,rt[i]/1000,"data"}}' /proc/net/xt_qtaguid/stats | grep $uid|$bb awk '{print $3}'echo “initnetarray”$initreceive”,"$inittransmitgetnet(){ local data_t=date +%Y/%m/%d" "%H:%M:%S netdetail=$bb awk -v OFS=, -v initreceive=$initreceive -v inittransmit=$inittransmit -v datat="$data_t" 'NR&gt;1{if($2=="wlan0"){wr[$4]+=$6;wt[$4]+=$8}else{if($2=="rmnet0"){rr[$4]+=$6;rt[$4]+=$8}}}END{for(i in wr){print datat,i,wr[i]/1000-initreceive,wt[i]/1000-inittransmit,"wifi"};for(i in rr){print datat,i,rr[i]/1000-initreceive,rt[i]/1000-inittransmit,"data"}}' /proc/net/xt_qtaguid/stats | grep $uid echo $netdetail>>$filenet}3.2 性能自动化脚本基于Appium的自动化用例,这个技术业界已经有非常多的实践了,这里我不再累述,如果不了解的同学,可以到Appium官网 http://appium.ioFlutter和Native页面切换使用App内的Schema跳转Flutter页面的文本输入等交互性较强的场景使用基于Flutter框架带的Integration Test来操作An integration testGenerally, an integration test runs on a real device or an OS emulator, such as iOS Simulator or Android Emulator. The app under test is typically isolated from the test driver code to avoid skewing the results.Flutter的UI自动化及Flutter/Native混合页面的处理在测试上的应用后续单独开文章介绍,原理相关可以先参考 千人千面录制回放技术3.3 性能自动化CI流程3.4 性能数据报表FPS相关Framediff: 绘制帧的开始时间和结束时间差FPS: 每秒展示的帧数Frames: 一个刷新周期内所有的帧jank: 一帧开始绘制到结束超过16.67ms 就记一次jank,jank非零代表硬件绘制掉帧,和屏幕硬件性能及相关驱 动性能有关jank2: 一帧开始绘制到结束超过33.34ms 就记一次jank2MFS: 在一个刷新周期内单帧最大耗时(每两行垂直同步的时间差代表两帧绘制的帧间隔)OKT: 在一个刷新周期内,帧耗时超过16.67ms的次数SS: 流畅度,通过FPS,MFS,OKT计算出来,流畅度 = 实际帧率比目标帧率比值60【目标帧率越高越好】 + 目标时间和两帧时间差比值20【两帧时间差越低越好】 + (1-超过16ms次数/帧数)*20【次数越少越好】CPUMemory4. 成果展示4.1 指定泳道分支性能监控泳道分支出现了性能问题再报表上一目了然4.2 性能专项支撑1、Flutter商品详情页重构 14轮测试2、客户端图片统一资源测试 4轮测试5. 总结性能自动化只是整个CI流程中的一个环节,为了极致效率的大目标,闲鱼质量团队还产出了很多支撑工具,CI平台,遍历测试,AI错误识别,用例自动生成等等,后续也会分享给大家。6. 参考https://testerhome.com/topics/2232https://testerhome.com/topics/4775本文作者:闲鱼技术-灯阳阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 22, 2019 · 3 min · jiezi

一到秒杀就瘫痪?压测大师保你后台稳健

作者:WeTest小编商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处。原文链接:https://wetest.qq.com/lab/view/442.htmlWeTest压测大师 - 新春元宵特惠礼**点击:https://wetest.qq.com/appoint/gapspro获取价值5888元的压测大师专家代金券(新用户需先注册)**活动细则:提交信息后,即可获得代金券代金券有效期为90天代金券使用规则请咨询企业客服QQ:2852350013不与其他优惠活动同时使用_WeTest 导读国内的电子商务经历了整个产业多年发展,依然在快速的增长,交易额仍在不断的递增,电子商务行业已经初步形成了功能完善的业态体系。与此同时,电子商务的不断普及直接带动了物流、金融和IT等服务类的行业发展,与之配套的第三方支付、电子认证、网络信息安全、网络保险、质量服务等电商生态圈中各子业态也在飞速的发展。在有庞大的客户体量下,电商的激烈竞争引出了对于服务需要高质量。在每次的节日活动中,服务器承受的压力往往是个重大的考验,于是服务器压测成为了一个必不可少的试金石。电商核心诉求场景 — “商品浏览选购顺畅”“结账下单支付成功”及“节日活动顺利成功”作为电子商务的购物,我们往往关注频率最高的几个场景是:秒杀、闪购活动时选购——结账无法操作,收入损失惨重节庆活动参加人数过多——服务器宕机、网站小程序APP瘫痪用户量一旦增加——页面响应越来越缓慢,不能正常浏览商品无法登录、无法支付及应用宕机可以发现在网站服务器业务上的场景主要的需求是:稳定使用和高并发使用。WeTest的压测大师专家打造一体式电商全链路测试服务一、专家深度打造压测的方案WeTest专家根据每个商户不同业务流程逻辑,从底层服务器架构分析,根据压测需求打造独有的测试方案。方案涵盖每个核心的业务场景,并且包含从核心场景的用例编写、执行、测试到产出报告的一体式服务。WeTest专家服务能提供的价值:评估后台性能是否能满足业务预期,比如满足双十一期间上万人同时支付探索系统能支持的最高并发量,为业务部门做活动时的推量提供决策依据分析出全链路中可能的性能瓶颈点,供开发团队优化能够通过长时间施压测试整套服务器系统的稳定性二、测试用例设计WeTest根据客户使用行为来进行用例的编写,从提供的服务器架构和时序图来分析后台交互的协议,同时根据实际用户行为,评估出并发量,进行用例编写测试。**单模块性能测试(如支付、登录、购物车)全链路测试(例:首页→品类页→商品详情页→加购物车→选择配送→…→提交订单→选择支付类型 →支付完成)**部分场景测试过程展示:提交购物车订单的成功率和响应时间提交购物车订单的CPU和内存的使用情况:三、在高并发下定位功能bug在高并发的服务器压力下,往往会容易出现概率性的功能bug。偶发性的bug形成的原因会极其复杂,可是有些bug造成的后果会很严重,虽然一般很少会遇到,但对于收集验证这些问题的开发来说,会碰到定位重现缺陷是件很困难的情况。WeTest专家在进行服务器压测的同时,会关注整体业务逻辑的功能是否会正常。例如账户多次切换数据错乱、购物车购买数据丢失等等。四、报告展示通过WeTest服务器性能测试报告,可以迅速了解到每个测试场景对应的测试过程,同时定位问题,分析瓶颈点。以下是部分报告里的内容展示:资深专家服务,规范化流程,腾讯标准保障以腾讯WeTest在十余年的产品压测经验为依托,目前推出资深专家服务,已应用于腾讯旗下各个行业的应用, “智慧零售”“企业微信”,“微信读书”,“QQ会员”“摩拜单车”,“NOW直播”等均使用压测大师专家服务,覆盖了电商、社交、交通出行、直播视频、新闻阅读等各行业应用,承载千万级用户产品的压测考验。现正值新春元宵佳节,压测大师隆重推出优惠活动:领取5888代金券,来体验专家模式一体式全流程的服务,保障电商全链路的通畅和稳定。最后,WeTest全体员工祝愿所有技术开发者们元宵快乐,阖家幸福~

February 20, 2019 · 1 min · jiezi

报表性能优化

【摘要】报表性能对用户的影响十分恶劣,所有用户查报表时都希望立等可取,超过 5 秒钟用户就会很不满意,更别提要求毫秒级响应的情况了。引起报表性能的原因有很多(数据量大、计算复杂、报表格式混乱),绝大部分是因为计算引起的,如果能将展现的数据能快速准备好,呈现的速度是飞快的!因此,解决报表性能问题的关键是报表数据准备!去乾学院看集算器是怎么实现的!报表性能优化

February 20, 2019 · 1 min · jiezi

Perseus-BERT——业内性能极致优化的BERT训练方案【阿里云弹性人工智能】

一,背景——横空出世的BERT全面超越人类2018年在自然语言处理(NLP)领域最具爆炸性的一朵“蘑菇云”莫过于Google Research提出的BERT(Bidirectional Encoder Representations from Transformers)模型。作为一种新型的语言表示模型,BERT以“摧枯拉朽”之势横扫包括语言问答、理解、预测等各项NLP锦标的桂冠,见图1和图2。【图1】SQuAD是基于Wikipedia文章的标准问答数据库的NLP锦标。目前SQuAD2.0排名前十名均为基于BERT的模型(图中列出前五名),前20名有16席均是出自BERT 【图2】GLUE是一项通用语言理解评估的benchmark,包含11项NLP任务。BERT自诞生日起长期压倒性霸占榜首(目前BERT排名第二,第一为Microsoft提交的BIGBIRD模型,由于没有URL链接无从知晓模型细节,网传BIGBIRD的名称上有借鉴BERT BIG模型之嫌) 业内将BERT在自然语言处理的地位比作ResNet之于计算机视觉领域的里程碑地位。在BERT横空出世之后,所有的自然语言处理任务都可以基于BERT模型为基础展开。一言以蔽之,现如今,作为NLP的研究者,如果不了解BERT,那就是落后的科技工作者;作为以自然语言处理为重要依托的科技公司,如果不落地BERT,那就是落后生产力的代表。二,痛点——算力成为BERT落地的拦路虎BERT强大的原因在哪里?让我们拂去云霭,窥探下硝烟下的奥秘。BERT模型分为预训练模型(Pretrain)和精调模型(Finetune)。Pretrain模型为通用的语言模型。Finetune只需要在Pretrain的基础上增加一层适配层就可以服务于从问答到语言推理等各类任务,无需为具体任务修改整体模型架构,如图3所示。这种设计方便BERT预处理模型适配于各类具体NLP模型(类似于CV领域基于ImageNet训练的各种Backbone模型)。【图3】左图基于BERT pretrain的模型用于语句问答任务(SQuAD)的finetune模型,右图为用于句对分类(Sentence Pair Classification Tasks)的finetune模型。他们均是在BERT Pretrain模型的基础上增加了一层具体任务的适配层因此,BERT的强大主要归功于精确度和鲁棒性俱佳的Pretrain语言模型。大部分的计算量也出自Pretrain模型。其主要运用了以下两项技术,都是极其耗费计算资源的模块。1. 双向Transformer架构图4可见,与其他pre-training的模型架构不同,BERT从左到右和从右到左地同时对语料进行transformer处理。这种双向技术能充分提取语料的时域相关性,但同时也大大增加了计算资源的负担。【关于Transformer是Google 17年在NLP上的大作,其用全Attention机制取代NLP常用的RNN及其变体LSTM等的常用架构,大大改善了NLP的预测准确度。本文不展开,该兴趣的同学可以自行搜索一下】。【图4】Pretrain架构对比。其中OpenAI GPT采用从左到右的Transformer架构,ELMo采用部分从左到右和部分从右到左的LSTM的级联方式。BERT采用同时从左到右和从右到左的双向Transformer架构。1. 词/句双任务随机预测BERT预训练模型在迭代计算中会同时进行单词预测和语句预测两项非监督预测任务。其一,单词预测任务对语料进行随机MASK操作(Masked LM)。在所有语料中随机选取15%的单词作为Mask数据。被选中Mask的语料单词在迭代计算过程中80%时间会被掩码覆盖用于预测、10%时间保持不变、10%时间随机替换为其他单词,如图5所示。其二,语句预测任务(Next Sentence Prediction)。对选中的前后句A和B,在整个迭代预测过程中,50%的时间B作为A的真实后续语句(Label=IsNext),另外50%的时间则从语料库里随机选取其他语句作为A的后续语句(Label=NotNext),如图5所示【图5】词/句双任务随机预测输入语料实例。蓝框和红框为同一个语料输入在不同时刻的随机状态。对单词预测任务,蓝框中的“went”为真实数据,到了红框则被[MASK],红框中的“the” 则相反;对于语句预测任务,蓝框中的句组为真实的前后句,而红框中的句组则为随机的组合。这种随机选取的单词/语句预测方式在功能上实现了非监督数据的输入的功能,有效防止模型的过拟合。但是按比例随机选取需要大大增加对语料库的迭代次数才能消化所有的语料数据,这给计算资源带来了极大的压力。综上,BERT预处理模型功能需要建立在极强的计算力基础之上。BERT论文显示,训练BERT BASE 预训练模型(L=12, H=768, A=12, Total Parameters=110M, 1000,000次迭代)需要1台Cloud TPU工作16天;而作为目前深度学习主流的Nvidia GPU加速卡面对如此海量的计算量更是力不从心。即使是目前主流最强劲的Nvidia V100加速卡,训练一个BERT-Base Pretrain模型需要一两个月的时间。而训练Large模型,需要花至少四五个月的时间。花几个月训练一个模型,对于绝大部分在GPU上训练BERT的用户来说真是伤不起。三,救星——擎天云加速框架为BERT披荆斩棘阿里云弹性人工智能团队依托阿里云强大的基础设施资源打磨业内极具竞争力的人工智能创新方案。基于BERT的训练痛点,团队打造了擎天优化版的Perseus-BERT, 极大地提升了BERT pretrain模型的训练速度。在云上一台V100 8卡实例上,只需4天不到即可训练一份BERT模型。Perseus-BERT是如何打造云上最佳的BERT训练实践?以下干货为您揭秘Perseus-BERT的独门绝技。1. Perseus 统一分布式通信框架 —— 赋予BERT分布式训练的轻功Perseus(擎天)统一分布式通信框架是团队针对人工智能云端训练的痛点,针对阿里云基础设施极致优化的分布式训练框架。其可轻便地嵌入主流人工智能框架的单机训练代码,在保证训练精度的同时高效地提升训练的多机扩展性。擎天分布式框架的干货介绍详见团队另一篇文章《Perseus(擎天):统一深度学习分布式通信框架》。针对tensorflow代码的BERT,Perseus提供horovod的python api方便嵌入BERT预训练代码。基本流程如下:让每块GPU对应一个Perseus rank进程;对global step和warmup step做基于rank数的校准;对训练数据根据rank-id做划分;给Optimizer增加DistributeOptimizer的wrapper。值得注意的是,BERT源码用的自定义的Optimizer,在计算梯度时采用了以下apigrads = tf.gradients(loss, tvars)Perseus的DistributeOptimizer继承标准的Optimizer实现,并在compute_gradients api 上实现分布式的梯度更新计算。因此对grads获取做了如下微调grads_and_vars = optimizer.compute_gradients(loss, tvars)grads = list()for grad, var in grads_and_vars: grads.append(grad)2. 混合精度训练和XLA编译优化——提升BERT单机性能的内功混合精度在深度学习中,混合精度训练指的是float32和float16混合的训练方式,一般的混合精度模式如图6所示【图6】混合精度训练示例。在Forward+Backward计算过程中用float16做计算,在梯度更新时转换为float32做梯度更新。混合梯度对Bert训练带来如下好处,增大训练时的batch size和sequence_size以保证模型训练的精度。 目前阿里云上提供的主流的Nvidia显卡的显存最大为16GB,对一个BERT-Base模型在float32模式只能最高设置为sequence_size=256,batch_size=26。BERT的随机预测模型设计对sequence_size和batch_size的大小有一定要求。为保证匹配BERT的原生训练精度,需要保证sequece_size=512的情况下batch_size不小于16。Float16的混合精度可以保证如上需求。混合精度能充分利用硬件的加速资源。 NVidia从Volta架构开始增加了Tensor Core资源,这是专门做4x4矩阵乘法的fp16/fp32混合精度的ASIC加速器,一块V100能提供125T的Tensor Core计算能力,只有在混合精度下计算才能利用上这一块强大的算力。 受限于float16的表示精度,混合精度训练的代码需要额外的编写,NVidia提供了在Tensorflow下做混合精度训练的教程 。其主要思路是通过tf.variable_scope的custom_getter 参数保证存储的参数为float32并用float16做计算。 在BERT预训练模型中,为了保证训练的精度,Perseus-BERT没有简单的利用custom_getter参数,而是显式指定训地参数中哪些可以利用float16不会影响精度,哪些必须用float32已保证精度。我们的经验如下:Embedding部分要保证float32精度;Attetion部分可以利用float16加速;Gradients相关的更新和验证需要保证float32精度;非线性激活等模块需要保证float32精度。XLA编译器优化XLA是Tensorflow新近提出的模型编译器,其可以将Graph编译成IR表示,Fuse冗余Ops,并对Ops做了性能优化、适配硬件资源。然而官方的Tensorflow release并不支持xla的分布式训练,为了保证分布式训练可以正常进行和精度,我们自己编译了带有额外patch的tensorflow来支持分布式训练,Perseus-BERT 通过启用XLA编译优化加速训练过程并增加了Batch size大小。3. 数据集预处理的加速Perseus BERT 同时对文本预处理做的word embedding和语句划分做了并行化的优化。这里就不展开说明。四,性能——计算时间单位从月降低到天图7展示了Perseus BERT在P100实例上的性能,与开源主流的horovod相比,Peseus-BERT双机16卡的分布式性能是前者的5倍之多。目前某大客户已在阿里云P100集群上大规模上线了Perseus BERT,用10台4卡P100只需要2.5天即可训练完成业务模型,如果用开源的horovod(Tensorflow分布式性能优化版)大概需要1个月的时间。【图7】Bert在阿里云上P100实例的对比(实验环境Bert on P100; Batch size: 22 ;Max seq length: 256 ;Data type:float32; Tensorflow 1.12; Perseus: 0.9.1;Horovod: 0.15.2)为了和Google TPU做对比,我们量化了TPU的性能,性能依据如图8。一个Cloud TPU可计算的BERT-Base性能 256 (1000000/4/4/24/60/60) = 185 exmaples/s。 而一台阿里云上的V100 单机八卡实例在相同的sequence_size=512下, 通过Perseus-BERT优化的Base模型训练可以做到 680 examples/s,接近一台Cloud TPU的4倍性能。对一台Cloud TPU花费16天才能训练完的BERT模型,一台阿里云的V100 8卡实例只需要4天不到便可训练完毕。【图8】BERT Pretain在Google Cloud TPU上的性能依据五,总结——基于阿里云基础设施的AI极致性能优化弹性人工智能团队一直致力基于阿里云基础设施的AI极致性能优化的创新方案。Perseus-BERT就是一个非常典型的案例,我们在框架层面上基于阿里云的基础设施做深度优化,充分释放阿里云上基础资源的计算能力,让阿里云的客户充分享受云上的AI计算优势,让天下没有难算的AI。本文作者:笋江阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 19, 2019 · 1 min · jiezi

优化体系结构 - 混合运算实现 T+0查询

【摘要】基于数据库系统的 T+0 全量实时查询,在数据量很大时一般只能进行数据库扩容(包括分库手段),成本高昂;如果采用文件系统和生产数据库混合运算,就可以实现低成本高性能的 T+0 查询!去乾学院看个究竟吧!优化体系结构 - 混合运算实现 T+0 查询【下载附件】优化体系结构 - 混合运算实现 T+0

February 18, 2019 · 1 min · jiezi

前端性能优化之gzip

gzip是GNUzip的缩写,它是一个GNU自由软件的文件压缩程序。它最早由Jean-loup Gailly和Mark Adler创建,用于UNⅨ系统的文件压缩。我们在Linux中经常会用到后缀为.gz的文件,它们就是GZIP格式的。现今已经成为Internet 上使用非常普遍的一种数据压缩格式,或者说一种文件格式。HTTP协议上的GZIP编码是一种用来改进WEB应用程序性能的技术。大流量的WEB站点常常使用GZIP压缩技术来让用户感受更快的速度。减少文件大小有两个明显的好处,一是可以减少存储空间,二是通过网络传输文件时,可以减少传输的时间。当然WEB服务器和客户端(浏览器)必须共同支持gzip。目前主流的浏览器Chrome,firefox,IE等都支持该协议。常见的服务器如Apache,Nginx,IIS同样支持gzip。下面就以Vue项目为例,介绍一下gzip的使用(vue-cli 2.*)1、在/config/index.js中,修改配置开启gzip// Gzip off by default as many popular static hosts such as// Surge or Netlify already gzip all static assets for you.// Before setting to true, make sure to:// npm install –save-dev compression-webpack-pluginproductionGzip: true,productionGzipExtensions: [‘js’, ‘css’],在修改productionGzip的默认值(false)为true之前,先安装所需的依赖npm install –save-dev compression-webpack-plugin。2、在nginx中开启gzip,/nginx/conf/nginx.conf中添加gzip配置http:{ #启用或禁用gzipping响应。# gzip on; #设置用于压缩响应的缓冲区number和size。默认情况下,缓冲区大小等于一个内存页面。这是4K或8K,具体取决于平台。# gzip_static on; #启用或禁用gzipping响应。# gzip_buffers 4 16k; #设置level响应的gzip压缩。可接受的值范围为1到9。# gzip_comp_level 5; #设置将被gzip压缩的响应的最小长度。长度仅由“Content-Length”响应头字段确定。# gzip_min_length 100; #匹配MIME类型进行压缩,text/html默认被压缩。# gzip_types text/plain application/javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;}修改完nginx配置,重启服务。关于gzip详细的配置和描述,请查阅 Module ngx_http_gzip_module至此,gzip已开启。你可以运行你的项目去检测一下。打开Chrome控制台,可以看到Network下的Response Headers中返回了Content-Encoding: gzip,表明gzip开启成功。而Request Headers里面的Accept-Encoding: gzip只是表示前端(用户浏览器)支持gzip的压缩方式。服务器支持gzip的方式可以有两种: 1、打包的时候生成对应的.gz文件,浏览器请求xx.js时,服务器返回对应的xxx.js.gz文件 2、浏览器请求xx.js时,服务器对xx.js进行gzip压缩后传输给浏览器 ...

February 13, 2019 · 1 min · jiezi

查询慢 跑批慢 性能低怎么办? | 润乾高性能计算专家

完整资料下载:查询慢、跑批慢、性能低怎么办?| 润乾高性能计算专家

February 13, 2019 · 1 min · jiezi

Android内存泄漏定位、分析、解决全方案

原文链接更多教程为什么会发生内存泄漏内存空间使用完毕之后未回收, 会导致内存泄漏。有人会问:Java不是有垃圾自动回收机制么?不幸的是,在Java中仍存在很多容易导致内存泄漏的逻辑(logical leak)。虽然垃圾回收器会帮我们干掉大部分无用的内存空间,但是对于还保持着引用,但逻辑上已经不会再用到的对象,垃圾回收器不会回收它们。例如忘记释放分配的内存的。(Cursor忘记关闭等)。应用不再需要这个对象,未释放该对象的所有引用。强引用持有的对象,垃圾回收器是无法在内存中回收这个对象。持有对象生命周期过长,导致无法回收。Java判断无效对象的原理Android内存回收管理策略图:图中的每个圆节点代表对象的内存资源,箭头代表可达路径。当圆节点与 GC Roots 存在可达路径时,表示当前资源正被引用,虚拟机是无法对其进行回收的(如图中的黄色节点)。反过来,如果圆节点与 GC Roots 不存在可达路径,则意味着这块对象的内存资源不再被程序引用,系统虚拟机可以在 GC 过程中将其回收掉。从定义上讲,Android(Java)平台的内存泄漏是指没有用的对象资源任与GC-Root保持可达路径,导致系统无法进行回收。内存泄漏带来的危害用户对单次的内存泄漏并没有什么感知,但当泄漏积累到内存都被消耗完,就会导致卡顿,崩溃。内存泄露是内存溢出OOM的重要原因之一,会导致CrashAndroid中常见的可能发生内存泄漏的地方1.在Android开发中,最容易引发的内存泄漏问题的是Context。比如Activity的Context,就包含大量的内存引用,一旦泄漏了Context,也意味泄漏它指向的所有对象。造成Activity泄漏的常见原因:Static Activities在类中定义了静态Activity变量,把当前运行的Activity实例赋值于这个静态变量。如果这个静态变量在Activity生命周期结束后没有清空,就导致内存泄漏。因为static变量是贯穿这个应用的生命周期的,所以被泄漏的Activity就会一直存在于应用的进程中,不会被垃圾回收器回收。static Activity activity; //这种代码要避免单例中保存Activity在单例模式中,如果Activity经常被用到,那么在内存中保存一个Activity实例是很实用的。但是由于单例的生命周期是应用程序的生命周期,这样会强制延长Activity的生命周期,这是相当危险而且不必要的,无论如何都不能在单例子中保存类似Activity的对象。举例:public class Singleton { private static Singleton instance; private Context mContext; private Singleton(Context context){ this.mContext = context; } public static Singleton getInstance(Context context){ if (instance == null){ synchronized (Singleton.class){ if (instance == null){ instance = new Singleton(context); } } } return instance; }}在调用Singleton的getInstance()方法时传入了Activity。那么当instance没有释放时,这个Activity会一直存在。因此造成内存泄露。解决方法:可以将new Singleton(context)改为new Singleton(context.getApplicationContext())即可,这样便和传入的Activity没关系了。Static Views同理,静态的View也是不建议的Inner Classes内部类的优势可以提高可读性和封装性,而且可以访问外部类,不幸的是,导致内存泄漏的原因,就是内部类持有外部类实例的强引用。 例如在内部类中持有Activity对象解决方法:1.将内部类变成静态内部类;2.如果有强引用Activity中的属性,则将该属性的引用方式改为弱引用;3.在业务允许的情况下,当Activity执行onDestory时,结束这些耗时任务;例如:发生内存泄漏的代码:public class LeakAct extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.aty_leak); test(); } //这儿发生泄漏 public void test() { new Thread(new Runnable() { @Override public void run() { while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } }解决方法:public class LeakAct extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.aty_leak); test(); } //加上static,变成静态匿名内部类 public static void test() { new Thread(new Runnable() { @Override public void run() { while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } }Anonymous Classes匿名类也维护了外部类的引用。当你在匿名类中执行耗时任务,如果用户退出,会导致匿名类持有的Activity实例就不会被垃圾回收器回收,直到异步任务结束。原文链接更多教程Handlerhandler中,Runnable内部类会持有外部类的隐式引用,被传递到Handler的消息队列MessageQueue中,在Message消息没有被处理之前,Activity实例不会被销毁了,于是导致内存泄漏。解决方法:1.可以把Handler类放在单独的类文件中,或者使用静态内部类便可以避免泄露;2.如果想在Handler内部去调用所在的Activity,那么可以在handler内部使用弱引用的方式去指向所在Activity.使用Static + WeakReference的方式来达到断开Handler与Activity之间存在引用关系的目的.3.在界面销毁是,释放handler资源@Override protected void doOnDestroy() { super.doOnDestroy(); if (mHandler != null) { mHandler.removeCallbacksAndMessages(null); } mHandler = null; mRenderCallback = null; }同样还有其他匿名类实例,如TimerTask、Threads等,执行耗时任务持有Activity的引用,都可能导致内存泄漏。线程产生内存泄露的主要原因在于线程生命周期的不可控。如果我们的线程是Activity的内部类,所以MyThread中保存了Activity的一个引用,当MyThread的run函数没有结束时,MyThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。要解决Activity的长期持有造成的内存泄漏,可以通过以下方法:传入Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,主动结束执行的任务,并释放Activity资源。将线程的内部类,改为静态内部类。因为非静态内部类会自动持有一个所属类的实例,如果所属类的实例已经结束生命周期,但内部类的方法仍在执行,就会hold其主体(引用)。也就使主体不能被释放,亦即内存泄露。静态类编译后和非内部类是一样的,有自己独立的类名。不会悄悄引用所属类的实例,所以就不容易泄露。如果需要引用Acitivity,使用弱引用。谨慎对context使用static关键字。2.Bitmap没调用recycle()Bitmap对象在不使用时,我们应该先调用recycle()释放内存,然后才设置为null.3.集合中对象没清理造成的内存泄露我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。解决方案:在Activity退出之前,将集合里的东西clear,然后置为null,再退出程序。4.注册没取消造成的内存泄露这种Android的内存泄露比纯Java的内存泄漏还要严重,因为其他一些Android程序可能引用系统的Android程序的对象(比如注册机制)。即使Android程序已经结束了,但是别的应用程序仍然还有对Android程序的某个对象的引用,泄漏的内存依然不能被垃圾回收。解决方案:1.使用ApplicationContext代替ActivityContext;2.在Activity执行onDestory时,调用反注册;5.资源对象没关闭造成的内存泄露资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。而不是等待GC来处理。6.占用内存较多的对象(图片过大)造成内存溢出因为Bitmap占用的内存实在是太多了,特别是分辨率大的图片,如果要显示多张那问题就更显著了。Android分配给Bitmap的大小只有8M.解决方法:1.等比例缩小图片BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 2;//图片宽高都为原来的二分之一,即图片为原来的四分之一 2.对图片采用软引用,及时地进行recycle()操作//软引用 SoftReference<Bitmap> bitmap = new SoftReference<Bitmap>(pBitmap); //回收操作 if(bitmap != null) { if(bitmap.get() != null && !bitmap.get().isRecycled()){ bitmap.get().recycle(); bitmap = null; } } 7.WebView内存泄露(影响较大)解决方案:用新的进程起含有WebView的Activity,并且在该Activity 的onDestory() 最后加上 System.exit(0); 杀死当前进程。检测内存泄漏的方法1.使用 静态代码分析工具-Lint 检查内存泄漏Lint 是 Android Studio 自带的工具,使用姿势很简单 Analyze -> Inspect Code 然后选择想要扫面的区域即可对可能引起泄漏的编码,Lint 都会进行温馨提示:2.LeakCanary 工具Square 公司出品的内存分析工具,官方地址如下:https://github.com/square/lea…LeakCanary 需要在项目代码中集成,不过代码也非常简单,如下的官方示例:在你的 build.gradle:dependencies { debugImplementation ‘com.squareup.leakcanary:leakcanary-android:1.6.3’ releaseImplementation ‘com.squareup.leakcanary:leakcanary-android-no-op:1.6.3’ // Optional, if you use support library fragments: debugImplementation ‘com.squareup.leakcanary:leakcanary-support-fragment:1.6.3’}在 Application 类:public class ExampleApplication extends Application { @Override public void onCreate() { super.onCreate(); if (LeakCanary.isInAnalyzerProcess(this)) { // This process is dedicated to LeakCanary for heap analysis. // You should not init your app in this process. return; } LeakCanary.install(this); // Normal app init code… }}当内存泄漏发生时,LeakCanary 会弹窗提示并生成对应的堆存储信息记录-3.Android Monitor开Android Studio,编译代码,在模拟器或者真机上运行App,然后点击,在Android Monitor下点击Monitor对应的Tab,进入如下界面在Memory一栏中,可以观察不同时间App内存的动态使用情况,点击可以手动触发GC,点击可以进入HPROF Viewer界面,查看Java的Heap,如下图Reference Tree代表指向该实例的引用,可以从这里面查看内存泄漏的原因,Shallow Size指的是该对象本身占用内存的大小,Retained Size代表该对象被释放后,垃圾回收器能回收的内存总和。扩展知识四种引用类型的介绍强引用(StrongReference):JVM 宁可抛出 OOM ,也不会让 GC 回收具有强引用的对象;软引用(SoftReference):只有在内存空间不足时,才会被回的对象;弱引用(WeakReference):在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;虚引用(PhantomReference):任何时候都可以被GC回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。原文链接更多教程 ...

February 11, 2019 · 2 min · jiezi

记一次性能优化,单台4核8G机器支撑5万QPS

前言这篇文章的主题是记录一次Python程序的性能优化,在优化的过程中遇到的问题,以及如何去解决的。为大家提供一个优化的思路,首先要声明的一点是,我的方式不是唯一的,大家在性能优化之路上遇到的问题都绝对不止一个解决方案。如何优化首先大家要明确的一点是,脱离需求谈优化都是耍流氓,所以有谁跟你说在xx机器上实现了百万并发,基本上可以认为是不懂装懂了,单纯的并发数完全是无意义的。其次,我们优化之前必须要有一个目标,需要优化到什么程度,没有明确目标的优化是不可控的。再然后,我们必须明确的找出性能瓶颈在哪里,而不能漫无目的的一通乱搞。需求描述这个项目是我在上家公司负责一个单独的模块,本来是集成在主站代码中的,后来因为并发太大,为了防止出现问题后拖累主站服务,所有由我一个人负责拆分出来。对这个模块的拆分要求是,压力测试QPS不能低于3万,数据库负责不能超过50%,服务器负载不能超过70%, 单次请求时长不能超过70ms,错误率不能超过5%。环境的配置如下:服务器:4核8G内存,centos7系统,ssd硬盘数据库:Mysql5.7,最大连接数800缓存: redis, 1G容量。以上环境都是购买自腾讯云的服务。压测工具:locust,使用腾讯的弹性伸缩实现分布式的压测。需求描述如下:用户进入首页,从数据库中查询是否有合适的弹窗配置,如果没有,则继续等待下一次请求、如果有合适的配置,则返回给前端。这里开始则有多个条件分支,如果用户点击了弹窗,则记录用户点击,并且在配置的时间内不再返回配置,如果用户未点击,则24小时后继续返回本次配置,如果用户点击了,但是后续没有配置了,则接着等待下一次。重点分析根据需求,我们知道了有几个重要的点,1、需要找出合适用户的弹窗配置,2、需要记录用户下一次返回配置的时间并记录到数据库中,3、需要记录用户对返回的配置执行了什么操作并记录到数据库中。调优我们可以看到,上述三个重点都存在数据库的操作,不只有读库,还有写库操作。从这里我们可以看到如果不加缓存的话,所有的请求都压到数据库,势必会占满全部连接数,出现拒绝访问的错误,同时因为sql执行过慢,导致请求无法及时返回。所以,我们首先要做的就是讲写库操作剥离开来,提升每一次请求响应速度,优化数据库连接。整个系统的架构图如下:将写库操作放到一个先进先出的消息队列中来做,为了减少复杂度,使用了redis的list来做这个消息队列。然后进行压测,结果如下:QPS在6000左右502错误大幅上升至30%,服务器cpu在60%-70%之间来回跳动,数据库连接数被占满tcp连接数为6000左右,很明显,问题还是出在数据库,经过排查sql语句,查询到原因就是找出合适用户的配置操作时每次请求都要读取数据库所导致的连接数被用完。因为我们的连接数只有800,一旦请求过多,势必会导致数据库瓶颈。好了,问题找到了,我们继续优化,更新的架构如下我们将全部的配置都加载到缓存中,只有在缓存中没有配置的时候才会去读取数据库。接下来我们再次压测,结果如下:QPS压到2万左右的时候就上不去了,服务器cpu在60%-80%之间跳动,数据库连接数为300个左右,每秒tpc连接数为1.5万左右。这个问题是困扰我比较久的一个问题,因为我们可以看到,我们2万的QPS,但是tcp连接数却并没有达到2万,我猜测,tcp连接数就是引发瓶颈的问题,但是因为什么原因所引发的暂时无法找出来。这个时候猜测,既然是无法建立tcp连接,是否有可能是服务器限制了socket连接数,验证猜测,我们看一下,在终端输入ulimit -n命令,显示的结果为65535,看到这里,觉得socket连接数并不是限制我们的原因,为了验证猜测,将socket连接数调大为100001.再次进行压测,结果如下:QPS压到2.2万左右的时候就上不去了,服务器cpu在60%-80%之间跳动,数据库连接数为300个左右,每秒tpc连接数为1.7万左右。虽然有一点提升,但是并没有实质性的变化,接下来的几天时间,我发现都无法找到优化的方案,那几天确实很难受,找不出来优化的方案,过了几天,再次将问题梳理了一遍,发现,虽然socket连接数足够,但是并没有全部被用上,猜测,每次请求过后,tcp连接并没有立即被释放,导致socket无法重用。经过查找资料,找到了问题所在,tcp链接在经过四次握手结束连接后并不会立即释放,而是处于timewait状态,会等待一段时间,以防止客户端后续的数据未被接收。好了,问题找到了,我们要接着优化,首先想到的就是调整tcp链接结束后等待时间,但是linux并没有提供这一内核参数的调整,如果要改,必须要自己重新编译内核,幸好还有另一个参数net.ipv4.tcp_max_tw_buckets, timewait 的数量,默认是 180000。我们调整为6000,然后打开timewait快速回收,和开启重用,完整的参数优化如下#timewait 的数量,默认是 180000。net.ipv4.tcp_max_tw_buckets = 6000net.ipv4.ip_local_port_range = 1024 65000#启用 timewait 快速回收。net.ipv4.tcp_tw_recycle = 1#开启重用。允许将 TIME-WAIT sockets 重新用于新的 TCP 连接。net.ipv4.tcp_tw_reuse = 1我们再次压测,结果显示:QPS5万,服务器cpu70%,数据库连接正常,tcp连接正常,响应时间平均为60ms,错误率为0%。结语到此为止,整个服务的开发、调优、和压测就结束了。回顾这一次调优,得到了很多经验,最重要的是,深刻理解了web开发不是一个独立的个体,而是网络、数据库、编程语言、操作系统等多门学科结合的工程实践,这就要求web开发人员有牢固的基础知识,否则出现了问题还不知道怎么分析查找。ps:服务端开启了 tcp_tw_recycle 和 tcp_tw_reuse是会导致一些问题的,我们为了优化选择牺牲了一部分,获得另一部分,这也是我们要明确的,具体的问题可以查看耗子叔的文章TCP 的那些事儿(上)

January 31, 2019 · 1 min · jiezi

跨越适配&性能那道坎,企鹅电竞Android weex优化

作者:龙泉,腾讯企鹅电竞工程师商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处。原文链接:https://wetest.qq.com/lab/view/441.htmlWeTest 导读企鹅电竞从17年6月接入weex,到现在已经有一年半的时间,这段时间里面,针对遇到的问题,企鹅电竞终端主要做了下面的优化:image组件预加载预渲染_Image组件weex的list组件和image组件非常容易出问题,企鹅电竞本身又存在很多无限列表的weex页面,list和image的组合爆发的内存问题,导致接入weex后app的内存问题导致的crash一直居高不下。list组件问题首先来说一下list,list对应的实现是WXListComponent,对应的view是BounceRecyclerView。RecyclerView应该大家都很熟悉,android support库里面提供的高性能的替代ListView的控件,它的存在就是为了列表中元素复用。本来weex使用了RecyclerView作为list的实现,是一件皆大欢喜的事情,但是RecyclerView中有一种使用不当的情况,会导致view不可复用。下图描述了RecyclerView的复用流程:[ RecyclerView复用 ]weex中的RecyclerView并没有设置stableId,所以RecyclerView的所有复用都依赖于ViewHolder的ViewType,Weex的ViewType生成见下图: private int generateViewType(WXComponent component) { long id; try { id = Integer.parseInt(component.getRef()); String type = component.getAttrs().getScope(); if (!TextUtils.isEmpty(type)) { if (mRefToViewType == null) { mRefToViewType = new ArrayMap<>(); } if (!mRefToViewType.containsKey(type)) { mRefToViewType.put(type, id); } id = mRefToViewType.get(type); } } catch (RuntimeException e) { WXLogUtils.eTag(TAG, e); id = RecyclerView.NO_ID; WXLogUtils.e(TAG, “getItemViewType: NO ID, this will crash the whole render system of WXListRecyclerView”); } return (int) id; }在没有设置scope的情况下,viewHolder的component的ref就是viewType,即所有的ViewHolder都是不同且不可复用的,此时的RecyclerView也就退化成了一个稍微复杂一点的ScrollView。如果设置了scope属性,但你绝对想不到,scope本身也是一个坑。下面直接上代码:// BasicListComponent.onBindViewHolder() public void onBindViewHolder(final ListBaseViewHolder holder, int position) { … if (holder.getComponent() != null && holder.getComponent() instanceof WXCell) { if(holder.isRecycled()) { holder.bindData(component); component.onRenderFinish(STATE_UI_FINISH); } … } } // ListBaseViewHolder.bindData() public void bindData(WXComponent component) { if (mComponent != null && mComponent.get() != null) { mComponent.get().bindData(component); isRecycled = false;`` } }上面代码中,可以看到,使用了scope,当复用Holder时,会把需要展示的component的数据绑定到复用的component中。那么问题来了,如果我不是只是想修改部分属性,而是需要改变component的层级关系呢?例如从a->b->c修改成a->c->b,那么是不是只能用不同的viewType或者是说变成下面的结构:a->b a->c b->b1 b->c1 c->c2 c->b2这样的结构,但是view的实例多了,必然又会导致内存等各种问题。最为致命的问题是,createViewHolder的时候,传给ViewHolder的component实例就是原件,而非拷贝,当bindData执行了以后,就等用于你复用的那个component的数据被修改了,当你再滑回去的时候,GG。所以scope属性基本不可用,留给我们的只有相当于scrollView的list。还好,为了解决list这么戳的性能,有了recyclerList,从vue的语法层,支持了模板的复用。但是坑爹的是,0.17 、 0.18 版本recyclerList都有这样那样的问题,重构同学觉得使用起来效率较低。0.19版本weex团队fix了这些问题后,企鹅电竞的前端同学也正在尝试往recyclerList去切换。image组件问题相信android开发们都清楚,图片的问题永远是大问题。OOM、GC等性能问题,经常就是伴随着图片操作。在0.17版本以前,WXImageView中bitmap的释放都是在component的recycle中执行,0.17版本之后,在detach时也会执行recycle,但是WXImageView的recycle只是把ImageView的drawable设置为null,并没有实际调用bitmap的recycle。而企鹅电竞在版本运行过程中发现,仅仅把bitmapDrawable设置为null,不去调用bitmap的recycle,部分机型上面的oom问题非常突出(这里一直没想明白,为啥这部分机型会出现这个问题,后面替换成fresco去管理就没这个问题了)。当然,如果直接recycle bitmap,不设置bitmapDrawable,会直接导致crash。回到企鹅电竞本身,企鹅电竞中的图片管理使用了fresco,在接入weex以前,我们已经针对fresco加载图片做了一系列优化,而且fresco本身已经包含了三级缓存等功能。接入weex后,首先想到的就是使用fresco的管线加载出bitmap后给WXImage使用。在这个过程中,先是遇到了对CloseableReference管理不恰当导致bitmap 还在使用却被recycle 掉了,然后又遇到了没有执行recycle导致bitmap无法释放的坑。在长列表中,图片无法释放的问题被无限放大,经常出现快速滑动几屏就oom的问题。而且随着业务发展使用WXImage无法播放gif和webp图片也成为瓶颈。后续版本中,企鹅电竞直接重写了image和img标签,使用Fresco的SimpleDraweeView替换了ImageView。该方案带来的收益是bitmap不在需要自己管理,即oom问题和bitmap recycle之后导致的crash问题会大大减少,且fresco默认就支持gif和webp图片。但是,这个方案也有个致命的问题:圆角。圆角问题得先从fresco和weex各自的圆角方案说起。weex圆角(盒模型-border):https://weex.apache.org/cn/wi…fresco圆角:https://www.fresco-cn.org/doc…fresco圆角方案具体可见RoundedBitmapDrawable,RoundedColorDrawable,RoundedCornersDrawable这3个类,fresco圆角属性的改变最终都只是修改这3个类的属性,圆角也是基于draw时候修改canvas画布内容实现,BtimapDrawable的裁减以及边框的绘制都是在draw的时候绘制上去。weex圆角方案具体可见ImageDrawable,实现方案为借助android的PaintDrawable,通过设置shader实现bitmapDrawable的裁减,但是边框的绘制则依赖于backgroundDrawable。而且在fresco中,封装了多层的drawable,较难修改drawabl的 draw的逻辑,而且边框参数的设置也不如weex众多样化。针对两者的差异性,企鹅电竞的解决方案是放弃fresco的圆角方案,通过fresco的后处理器裁减bitmap达到圆角的效果,边框复用weex的background的方案。这个方案唯一的问题后处理器中必须创建一份新的bitmap,但是通过复用fresco的bitmapPool,并不会导致内存有过多的问题。下面贴一下后处理器处理圆角的关键代码:public CloseableReference<Bitmap> process(Bitmap sourceBitmap, PlatformBitmapFactory bitmapFactory) { CloseableReference<Bitmap> bitmapRef = null; try { if (mInnerImageView instanceof FrescoImageView && sourceBitmap != null && !sourceBitmap.isRecycled() && sourceBitmap.getWidth() > 0 && sourceBitmap.getHeight() > 0) { … // 解决Bitmap绘制尺寸上限问题,比如:Bitmap too large to be uploaded into a texture (1302x9325, max=8192x8192) int maxSize = EGLUtil.getGLESTextureLimit(); int resizeWidth = mWidth; int resizeHeight = mHeight; float ratio = 0; if (maxSize > 0 && (mWidth > maxSize || mHeight > maxSize)) { ratio = Math.max((float) mWidth / maxSize, (float) mHeight / maxSize); resizeWidth = (int) (mWidth / ratio); resizeHeight = (int) (mHeight / ratio); } float[] borderRadius = ((FrescoImageView) mInnerImageView).getBorderRadius(); if (checkBorderRadiusValid(borderRadius)) { Drawable imageDrawable = ImageDrawable.createImageDrawable(sourceBitmap, mInnerImageView.getScaleType(), borderRadius, resizeWidth, resizeHeight, false); imageDrawable.setBounds(0, 0, resizeWidth, resizeHeight); CloseableReference<Bitmap> tmpBitmapRef = bitmapFactory.createBitmap(resizeWidth, resizeHeight, sourceBitmap.getConfig()); Canvas canvas = new Canvas(tmpBitmapRef.get()); imageDrawable.draw(canvas); bitmapRef = tmpBitmapRef; } else if (ratio != 0) { bitmapRef = bitmapFactory.createBitmap(sourceBitmap, 0, 0, resizeWidth, resizeHeight, sourceBitmap.getConfig()); } } if (bitmapRef == null) { bitmapRef = bitmapFactory.createBitmap(sourceBitmap); } } catch (Throwable e) { WeexLog.e(TAG, “process image error:” + e.toString()); } return bitmapRef; }当list和image组合在一起的时候,由于weex的image并没有recycle掉bitmap,而且没有bitmapPool的使用,会导致长列表weex页面占用内存特别高。而替换为fresco的bitmap内存管理模式后,由于weex导致的内存crash问题占比明显从最开始版本的2%下降到了0.1%-0.2%。预加载当踩完大大小小的坑,缓解了内存和crash问题之后,企鹅电竞在weex使用上又遇到了2大难题:调试困难页面加载慢调试困难weex的页面并不能给前端的开发同学丝滑的调试体验。最开始前端同学是采用终端日志或者弹框的方式调试(心疼前端同学就这么学会了看android日志),后面通过再三跟weex团队的沟通,终于确定了weex和weex_debuger对应的版本,前端同学可以在chrome上面调试weex页面。然而weex_deubgger并不是完美的解决方案,weex本身是jscore内核,而weex_debugger只是通过chrome调试协议开了个服务,等同于使用的是chrome的内核,内核的不一致性无法保证调试的准确性。连weex的开发同学自己都说了会遇到debug环境和正式环境结果不一致的情况。解决方案也很简单,那就是可以在mac的xcode和safari上面调试。当时由于替换mac的成功过高,就将就使用了weex_debugger的方案,后面怎么解决了相信大家心里有数。页面加载速度慢随着企鹅电竞业务的发展,很快前端同学就反馈过来,怎么weex页面打开的速度这么慢,这个菊花转了这么久。当时的内心是崩溃的,明明接入的时候好好的,一个页面轻轻松松500-600ms就加载回来了,哪里会有问题?业务的发展速度永远是你想象不到的,2个版本不到的时间,企鹅电竞中的weex页面轻轻松松从个位数突破到两位数,bundle大小也轻轻松松从几十kb突破到了上百kb,由此带来的问题是打开weex页面后能明显看到菊花转动了,甚至打开速度上还不如直出的web页面。首先从数据报表中发现,页面打开速度中,1s中有300-400ms是bundle从网络下载的时间,那是不是把这段时间省了,页面有轻轻松松回到毫秒级别打开速度了。下图展示了预加载的整体流程。[ 预加载流程 ]预加载方案上线后,页面成功节省了将近200ms的耗时。20M的LRUCache大小也是参考了http cache的默认大小值,页面打开的预加载率在75%-80%。预渲染做了预加载之后,很快又发现,就算没有网络请求,页面打开耗时还是超过了1s。这种情况下,现有的方案已经无法继续优化页面。这个时候突然有了个想法,weex本身是把前端的虚拟dom转化为终端的各种view控件,那么为什么weex页面的打开会慢终端页面打开这么多呢?定义问题解决问题之前,先来定义一下问题具体是什么。针对渲染速度慢,企鹅电竞对weex渲染的耗时定义如下:· renderStart = 调用WXSdkInstance.render()的时间点· httpFinish = httpAdapter请求回来之后调用WXSdkInstance.onHttpFinish()的时间点· renderFinish = 回调 IWXRenderListener.onRenderSuccess()的时间点· 页面打开耗时 = renderFinish - renderStart· 网络耗时 = httpFinish - renderStart· 渲染耗时 = renderFinish - httpFinish所以之前的预加载,已经优化了网络耗时,但是渲染耗时在页面大了之后,依旧会有很大的性能问题。为了揭开这个问题的本质,先来看一下weex整体的框架:[ weex框架图: ]JSFrameWork提供给前端的sdk,对vue的dom操作做了各种封装,JSFrameWork单独打包到apk包中。JavaScriptCore使用与safari的JavaScript引擎,专门处理JavaScript的虚拟机,对应chrome的v8,功能可以大体联想成java的jvm。JSSweex core的server端,封装了对JavaScripteCore的调用,封装了instance的沙盒,多进程实现中,JSS和JavaScriptCore的执行在另外的进程,防止JS执行异常导致主进程崩溃。JSCweex core的client端,作为WeexFrameWork和JSS桥接层,另外从0.18版本开始,cssLayout也下沉到了这一层。WeexFrameWork提供各种sdk接口的java调用,虚拟dom和Android控件树的转换,控件管理等。了解完了weex框架,再把关注点转移到js build之后生成的jsBundle,细心的同学肯定能够发现,生成的jsBundle本质上就是一个js方法,所以weex页面render的过程本质上是执行一个js方法。针对企鹅电竞关注的游戏首页,对整个weex框架加了完整的打点,看到在nexus 6上面,对应的耗时以及整体流程如下图:[ weex执行流程以及耗时 ]可以看到性能的热点主要在执行js方法以及虚拟dom的执行这两个关键步骤上,根据打点来看,单个js方法和单个虚拟dom的执行,耗时都很低。企鹅电竞抓了多次打点,看到启动时候执行js最慢的也仅仅是3ms,大多数执行都在0.1ms - 0 ms这个区间。但是,再快的执行耗时,也架不住量多,同样以企鹅电竞游戏首页为例,启动的时候该页面执行的js方法多大2000+个,这2000+个方法执行再加上方法调度的耗时,能成为性能热点一点也不意外。而虚拟dom的执行也同理,单次执行经过weex团队的优化,执行耗时基本在1ms-3ms之间,但是同样的架不住量多以及线程调度的时间问题。预渲染方案了解RN的同学应该也知道,js方法的执行和虚拟dom的执行是这种框架的核心所在,想要撬动整个核心,基本上难度等同于重写一个了。那么剩下的方案也就只有一个:提前渲染。[ 预渲染 ]预渲染的方案修改了WeexFrameWork虚拟dom和Android控件树转换的部分,在预渲染时,不生成真正的component和view结构,用抽象出来的ComponentNode存储虚拟dom的操作,并在RealRender的时候将node转换成一个个component以及View。这个方案的基本原理就是典型的以提前消费的空间换取时间,不去转换真正的component和View原因是view在不同context中的不可复用性以及view本身会占用大部分内存。预渲染优化数据内存消耗提前渲染必然导致类内存的提前消耗,在huawei nove3上测试得到,预渲染游戏首页时的峰值内存会去到10M,但是在最后预渲染完成后GC会释放这部分内存,最终常驻内存为0.3M。 真正渲染游戏首页的内存峰值会去到20M,最后的常驻内存为5.6M。可以看到预渲染对常驻内存的消耗极少,但是由于虚拟dom执行,导致峰值内存偏高,在某些内存敏感场景下,还是会有一定风险。页面打开耗时实验室中游戏首页的正常加载数据为900ms(已经预加载,无网络耗时),经过预渲染,页面打开仅需要150ms。现网数据:[ 预渲染页面打开上报 ]最后,来两张优化前后的对比图:[ 预渲染: ][ 非预渲染: ]_“深度兼容测试”现已对外,腾讯专家为您定制自动化测试脚本,覆盖应用核心场景,对上百款主流机型进行适配兼容测试,提供详细测试报告。另有客户端性能测试,一网打尽FPS、CPU等基础性能数据,详细展示各类渲染数据,极速定位性能问题。点击:https://wetest.qq.com/cloud/deepcompatibilitytesting 即可体验。如果使用当中有任何疑问,欢迎联系腾讯WeTest企业QQ:2852350015 ...

January 26, 2019 · 2 min · jiezi

wepy小程序长列表性能优化实践

背景wepy 1.7.3wepy-redux长列表交互问题wepy框架的列表性性能比较差,主要原因是修改列表中任意字段的时候,会给setData传递完整的列表,详细见这个issue;此时修改长列表任意字段,都会导致页面长时间不响应解决使用字典(Object)与长列表进行组合,因为一般情况下字典的数据量会远远小于列表场景任意弹窗对购物车cart进行修改,产品列表对应的购买数量同步修改// 数据结构// 产品列表(长度3000+)var products = [{id: “79”, name: “精致荤菜”}…]// 购物车字典 // key: productId, value: 购物车数据var cartDict = { 2407: { price: “1.02” num: 2 }}注意由于cartDict数据为用户手动添加,数据量远远小于列表。那么setData时速度也会相应提高此时我们使用组合方式渲染列表的购买量<view class=“num” wx:if="{{ cartDict[product.id] }}">{{cartDict[product.id].num}}</view>通过将每次修改列表转移为每次修改cartDict来达到提升性能的效果;上面那个issue也可以用类似思路制作一个展开产品的字典首次加载白屏问题我们的商品列表一般会比较长(目前最大有3000+个),此时第一次进入页面白屏时间很长(10s+);解决使用h5的优化思路,类似app。只渲染一部分屏幕内的产品,其他绝大部分产品使用骨架展示;使用此方法有一些限制产品高度需要已知,用来计算当前产品是否在屏幕内滚动体验没有不优化的好,小程序其实也是用的这种列表优化思路,因此快速滚动的时候实际效果是白屏(小程序优化) => 骨架(我们的优化) => 出现产品场景我们项目所有产品等高,因此比较好计算当前产品是否应该展示。首先是模板写法<repeat for="{{products}}" item=“dish” index=“index”> <dishItem :dish=“dish” wx:if="{{showTypeDict[type.id]}}"></dishItem> <view wx:else> <image src="{{_skeleton}}"></image> </view></repeat>说明:showTypeDict代表当前需要展示的产品字典,使用字典原因是基于长列表交互问题对产品进行分类,每次只比较分类的坐标然后展示整个分类dishItem是产品对应组件,比较复杂skeleton为骨架监听scroll,根据当前scrollTop和产品分类的坐标来决定showTypeDict的内容,注意截流;使用以上方法优化后3000+产品白屏时间与100+产品持平。滚动时无卡顿,快速滚动时需要等待一会儿产品才能渲染出来;以上

January 24, 2019 · 1 min · jiezi

批量随机键值查询测试

【摘要】当数据量巨大时,使用大批量随机键值集获取对应记录集合,不仅仅考验数据库软件本身,更在于程序员对数据的理解!如何在硬件资源有限的情况下将性能发挥到极致?点击:批量随机键值查询测试,来乾学院一探究竟!本次测试主要针对集算器组表索引实现的批量键值取数性能,并与 Oracle 进行同规模运算对比。一、测试环境二、数据描述2.1数据结构2.2数据规模按以上数据结构,造出 6 亿条记录的行存组表文件和对应的索引文件:三、测试过程3.1生成测试文件3.1.1 建组表A1:包含 26 个英文字母和 10 个阿拉伯数字的字符串。A2、A3:建立结构为 (id,data) 的组表文件,@r 选项表示使用行式存储方式。A4:循环 6000 次,循环体B4、B5,每次生成 10 万条对应结构的记录,并追加到组表文件。执行后,生成组表文件:id_600m.ctx3.1.2 建索引A2:根据组表文件的 id 列,建立组表索引。执行后,生成组表的索引文件:id_600m.ctx__id_idx3.2查询测试A2:循环一万次,每次获取对应组表文件 id 列中的随机一个,并排序。(可能会有少量重复值,但对测试影响不大)A4:在组表的 icursor()这个函数中,使用索引 id_idx,以条件 A2.contain(id) 来过滤组表。集算器会自动识别出 A2.contain(id) 这个条件可以使用索引,并会自动将 A2 的内容排序后从前向后查找。3.3奇怪的现象原本希望多次执行后,求得一个平均值作为测试结果。但是发现每执行完毕一次该测试代码,都会比上一次执行快一些,这里列出从第一次执行该代码后的 5 次测试查询耗时:手动一次次点击设计器中的执行按钮,并记录下查询耗时,太费劲了。为了找出规律,将代码改为以下形式:B7:将循环体中 icursor() 函数每一次查询的耗时,在 A1 中追加记录下来。执行过程中,观察 A1 中新追加的查询耗时与上一次的比较,发现经过大约 350 次循环后接近极限值 25 秒。再后续近千次循环中,查询耗时也都是如此,基本稳定。难道是集算器对数据进行了缓存?抱着怀疑的态度,重启了集算器设计器,再次执行了查询代码。发现重启后第一次的查询耗时也是 25 秒。这样看来提速的原因和集算器本身并没有什么直接的关系了。另一方面,可以想到基于目前测试的数据量,能够在短时间内完成查询,部分数据可能已经装载至内存,那么很可能是 linux 操作系统的文件缓存造成了这个现象。重启服务器后,再通过集算器设计器来执行查询,发现耗时又开始从 80 秒左右慢慢减少了。进一步的测试中,使用了 linux 的 free 命令查看系统内存使用情况。发现每完成一次组表的查询,其中的 cached 一项就会变大。而随着 cached 慢慢的变大,查询的耗时又逐步减少。3.4index@3的使用在网络上查询了一些资料,了解到 Linux 会存在缓存内存,通常叫做 Cache Memory。就是之前使用 free 命令看到其中的 cached 一项,执行 free -h:当我们读写文件的时候,Linux 内核为了提高读写效率与速度,会将文件在内存中进行缓存,这部分内存就是 Cache Memory(缓存内存)。即使我们的程序运行结束后,Cache Memory 也不会自动释放。这就会导致我们在 Linux 系统中程序频繁读写文件后,我们会发现可用物理内存会很少。其实这个缓存内存在我们需要使用内存的时候会自动释放,所以我们不必担心没有内存可用。并且手动去释放 Cache Memory 也是有办法的,但此处不再详细探讨。这个函数涉及数据量有 111G,比机器的物理内存 64G 更大,显然不可能把所有数据都缓存到内存中,那么到底缓存了哪些数据后就能稳定地提高查询性能呢?是不是可以事先就把需要这些数据先缓存起来以获得高性能?请教了高手后,发现果然还有选项可以来预先缓存索引的索引。在使用 icursor()函数查询之前,对组表索引使用了 T.index@2(idx) 使用了 T.index@3(idx)。代码如下: 集算器的索引有个分级缓存,@3 的意思是将索引的第三级缓存先加载进内存。经过 index@3 预处理,第一遍查询时间也能达到上面查询数百次后才能达到的极限值。四、与 Oracle 对比测试环境、数据结构和规模与上文一致,测试对象如下:Oracle建表语句为:create table ctx_600m (id number(13),data varchar2(200));数据由集算器生成同结构的文本文件后,使用 Oracle 的 SqlLoader 导入表中。Oracle建索引语句为:create unique index idx_id_600m on ctx_600m(id);使用 Oracle 进行批量随机取数测试时,我们使用这样的 SQL:select * from ctx_600m where id in (…)使用单线程连接 Oracle 进行查询的集算器脚本为:由于 oracle 的 in 个数有限制,脚本中进行分批执行后合并。使用 10 线程连接 Oracle 进行查询的集算器脚本为:使用单线程对行存组表进行查询的集算器脚本为:使用 10 线程对行存组表进行查询的集算器脚本为:从 6 亿条数据总量中取 1 万条批量随机键值,在都建立索引的测试结果:五、列存索引测试集算器列存采用了数据分块并压缩的算法,这样对于遍历运算来讲,访问数据量会变小,也就会具有更好的性能。但对于基于索引随机取数的场景,由于要有额外的解压过程,而且每次取数都会针对整个分块,运算复杂度会高很多。因此,从原理上分析,这时候的性能应当会比行存要差。上述代码中把生成组表的 create() 函数不用 @r 选项,即可生成列存文件。重复上面的运算,单线程情况下 6 亿行中取 1 万行耗时为 129120 毫秒,比行存方式慢了 6 倍多。不过平均到一行也只有 13 毫秒,对于大多数单条取数的场景仍然有足够的实用性。同一份数据不能在遍历运算和随机取数这两方面都达到最优性能,在实际应用中就要根据需求做一下取舍了,一定要追求各种运算的极限性能时,可能就要把数据冗余多份了。六、索引冗余机制集算器确实也提供了冗余索引机制,可以用于提高列存数据的随机访问性能,代码如下:在对组表建立索引时,当 index 函数有数据列名参数,如本例 A2 中的 data,就会在建索引时把数据列 data 复制进索引。当有多个数据列时,可以写为:index(id_idx;id;data1,data2,…)因为在索引中做了冗余,索引文件也自然会较大,本文中测试的列存组表和索引冗余后的文件大小为:当数据复制进索引后,实际上读取时不再访问原数据文件了。从 6 亿条数据总量中取 1 万条批量随机键值,完整的测试结果对比: ...

January 23, 2019 · 1 min · jiezi

Go:指针能优化性能吗?【译】

趁着元旦休假+春节,尝试把2018年期间让我受益的一些文章、问答,翻译一下。欢迎指正、讨论,希望对你也有所帮助。原文链接:Go: Are pointers a performance optimization?以下,开始正文过去几周时间,我回答了许多关于使用指针优化性能的问题。似乎很多人在这方面都感到困惑。这也可以理解,指针确实是个复杂的话题。 希望这篇文章对你有所帮助。简而言之:不是使用指针就一定代表着性能优化。如果要彻底解释这篇文章涉及的所有细节,那篇幅可能会长到没人愿意看。所以,我精简了一下,试图用中等篇幅也能涵盖想说明的高级概念。阅读时需要说明一点:本文讨论的是微优化,性能优化都是极其细微的。在进行微优化之前,需先进行基准测试,否则很可能看不到明显的效果。代码易读性才是第一要义。什么是指针?指针底层代表的就是内存地址,指针解引用可以访问到内存存储的具体数据值。应用指针之后是如何起到性能优化作用的?函数调用时,变量传递实际上是将变量重新复制了一份,传给函数。多数情况下,指针都要比变量本身占用更小的空间。通常,指针大小和系统的架构体系保持一致。32位系统就是32bit,64位系统,即是64bit大小。像bool、float等标量类型,占用的空间都小于等于指针;而多字段的符合类型,指针占的空间更少。所有,我们的想法就基于复制指针比复制原值更高效。一定程度上,这样想没问题。但是性能问题涉及广泛,除了复制成本之外,还有很多因素要考虑。指针是否会对性能产生负面影响?会。主要出于两方面的考量:解引用虽然耗能很小,但积少成多,不得不虑。通过指针共享的数据,是放在堆上的。堆数据的清理是GC负责的,这也会产生开销。随着堆上数据增多,GC的工作量变大,对项目的性能影响也不容忽视。堆与栈堆和栈是两个让人头疼的概念,但是我们不得不直面它们。我在这尽量用简短的篇幅讨论完。如果没能快速理解也没关系,我曾经也没能很快理解。栈:函数局部空间每当函数被调用,都会分配一块栈空间来存储函数局部变量。函数占用的栈空间大小在编译时已经确定。函数返回时,这块空间就给下一个函数调用使用了,也无需立刻清理。虽然这个分配和使用过程要耗费资源,但相对来讲,消耗很小。堆:共享数据空间如上所述,函数返回后,局部变量会被销毁(译者注:空间被复用或者彻底回收,局部变量不再存在)。如果返回是非指针变量,返回值会被复制给调用者,存在于调用者的栈空间中。但是,如果返回的是指针(译者注:也就是函数局部空间的地址),指针指向的数据就要保存在栈空间之外,这样才能保证函数返回后,数据仍然可以访问。这就是堆的用处。与堆相关的性能问题有这些:堆空间需要从runtime申请,虽然开销很小,也不能不考虑;如果运行时没有足够空间,就需要系统调用了,这是额外的开销;一旦数据占用了堆空间,就要一直占用到没有指针再指向它。这时候,需要GC来清理。GC会找到堆中所有没有被饮用的值,标记为空闲(译者注:请参考Go垃圾回收的三色标记)。垃圾越多,GC耗时越大,系统性能越差。那为什么还要用指针?指针能修改传参,提供了一种共享数据的方式。指针能区分零值,确定你的变量是否被赋值了。总结指针可以节省复制的开销,但同时要考虑解引用和垃圾回收带来的影响。在我看来,性能分析结果显示复制是瓶颈之前,不应该考虑把指针作为优化方案。计算机在复制方面的速度可是极快的。希望这篇文章可以让你认识到指针是可以派上用场的,但也不要因为要用而用。默认还是使用值而不是地址吧。除非语法满足不了你了。

January 21, 2019 · 1 min · jiezi

webpack构建和性能优化探索

前言随着业务复杂度的不断的增加,工程模块的体积也会不断增加,构建后的模块通常要以M为单位计算。在构建过程中,基于nodejs的webpack在单进程的情况下loader表现变得越来越慢,在不做任何特殊处理的情况下,构建完后的多项目之间公用基础资源存在重复打包,基础库代码复用率也不高,这都慢慢暴露出webpack的问题。正文针对存在的问题,社区涌出了各种解决方案,包括webpack自身也在不断优化。构建优化下面利用相关的方案对实际项目一步一步进行构建优化,提升我们的编译速度,本次优化相关属性如下:机器: Macbook Air 四核 8G内存Webpack: v4.10.2项目:922个模块构建优化方案如下:减少编译体积大小将大型库外链将库预先编译使用缓存并行编译初始构建时间如下:增量构建Development 构建Production 构建备注3088ms43702ms89371ms 减少编译体积大小初始构建时候,我们利用webpack-bundle-analyzer对编译结果进行分析,结果如下:可以看到,td-ui(类似于antd的ui组件库)、moment库的locale、BizCharts占了项目的大部分体积,而在没有全部使用这些库的全部内容的情况下,我们可以对齐进行按需加载。针对td-ui和BizCharts,我们对齐添加按需加载babel-plugin-import,这个包可以在使用ES6模块导入的时候,对其进行分析,解析成引入相应文件夹下面的模块,如下:首先,我们先添加babel的配置,在plugins中加入babel-plugin-import:{ … “plugins”: [ … [“import”, [ { libraryName: ’td-ui’, style: true }, { libraryName: ‘bizcharts’, libraryDirectory: ’lib/components’ }, ]] ]}可以看到,我们给bizcharts也添加了按需加载,配置中添加了按需加载的指定文件夹,针对bizcharts,编译前后代码对比如下:编译前:编译后:注意:bizcharts按需加载需要引入其核心代码bizcharts/lib/core;到此为止,td-ui和bizcharts的按需加载已经处理完毕,接下来是针对moment的处理。moment的主要体积来源于locale国际化文件夹,由于项目中有中英文国际化的需求,我们这里使用webpack.ContextReplacementPugin对该文件夹的上下文进行匹配,只匹配中文和英文的语言包,plugin配置如下:new webpack.ContextReplacementPugin( /moment[/\]locale$/, //匹配文件夹 /zh-cn|en-us/ // 中英文语言包)如果没有国际化的需求,可以使用webpack.IgnorePlugin对整个locale文件夹进行忽略,配置如下:new webpack.IgnorePlugin(/^./locale$/, /moment$/)减少编译体积大小完成之后得到如下构建对比结果:增量构建Development 构建Production 构建备注3088ms43702ms89371ms 2561ms27864ms67441ms减少编译体积大小将大型库外链 && 将库预先编译为了避免一些已经编译好的大型库重新编译,我们需要将这些库放在编译意外的地方,或者预先编译这些库。webpack也为我们提供了将模块外链的配置externals,比如我们把lodash外链,配置如下module.exports = { //… externals : { lodash: ‘window.’ }, // 或者 externals : { lodash : { commonjs: ’lodash’, amd: ’lodash’, root: ‘’ // 指向全局变量 } }};针对库预先编译,webpack也提供了相应的插件,那就是webpack.Dllplugin,这个插件可以预先编译制定好的库,最后在实际项目中使用webpack.DllReferencePlugin将预先编译好的库关联到当前的编译结果中,无需重新编译。Dllplugin配置文件webpack.dll.config.js如下:dllReference配置文件webpack.dll.reference.config.js如下:最后使用webpack-merge将webpack.dll.reference.config.js合并到到webpack配置中。注意:预先编译好的库文件需要在html中手动引入并且必须放在webpack的entry引入之前,否则会报错。其实,将大型库外链和将库预先编译也属于减少编译体积的一种,最后得到编译时间结果如下:增量构建Development 构建Production 构建备注3088ms43702ms89371ms 2561ms27864ms67441ms减少编译体积大小2246ms22870ms50601msDll优化后使用缓存首先,我们开启babel-loader自带的缓存功能(默认其实就是打开的)。另外,开启uglifyjs-webpack-plugin的缓存功能。添加缓存插件hard-source-webpack-plugin(当然也可以添加cache-loader)const hardSourcePlugin = require(‘hard-source-webpack-plugin’);moudle.exports = { // … plugins: [ new hardSourcePlugin() ], // …}添加缓存后编译结果如下:增量构建Development 构建Production 构建备注3088ms43702ms89371ms 2561ms27864ms67441ms减少编译体积大小2246ms22870ms50601msDll优化后1918ms10056ms17298ms使用缓存后可以看到,编译效果极好。并行编译由于nodejs为单线程,为了更好利用好电脑多核的特性,我们可以将编译并行开始,这里我们使用happypack,当然也可以使用thread-loader,我们将babel-loader和样式的loader交给happypck接管。babel-loader配置如下:less-loader配置如下:构建结果如下:增量构建Development 构建Production 构建备注3088ms43702ms89371ms 2561ms27864ms67441ms减少编译体积大小2246ms22870ms50601msDll优化后1918ms10056ms17298ms使用缓存后2252ms11846ms18727ms开启happypack后可以看到,添加happypack之后,编译时间有所增加,针对这个结果,我对webpack版本和项目大小进行了对比测试,如下:Webpack:v2.7.0项目:1013个模块全量production构建:105395ms添加happypack之后,全量production构建时间降低到58414ms。针对webpack版本:Webpack:v4.23.0项目:1013个模块全量development构建 : 12352ms添加happypack之后,全量development构建降低到11351ms。得到结论:Webpack v4 之后,happypack已经力不从心,效果并不明显,而且在小型中并不适用。所以针对并行加载方案要不要加,要具体项目具体分析。性能优化对于webpack编译出来的结果,也有相应的性能优化的措施。方案如下:减少模块数量及大小合理缓存合理拆包减少模块数量及大小针对减少模块数量及大小,我们在构建优化的章节中有提到很多,具体点如下:按需加载 babel-plugin-import(antd、iview、bizcharts)、babel-plugin-component(element-ui)减少无用模块webpack.ContextReplacementPlugin、webpack.IgnorePluginTree-shaking:树摇功能,消除无用代码,无用模块。Scope-Hoisting:作用域提升。babel-plugin-transform-runtime,针对babel-polyfill清除不必要的polyfill。前面两点我们就不具体描述,在构建优化章节中有说。Tree-shaking树摇功能,将树上没用的叶子摇下来,寓意将没有必要的代码删除。该功能在webapck V2中已被webpack默认开启,但是使用前提是,模块必须是ES6模块,因为ES6模块为静态分析,动态引入的特性,可以让webpack在构建模块的时候知道,那些模块内容在引入中被使用,那些模块没有被使用,然后将没有被引用的的模块在转为为AST后删除。由于必须使用ES6模块,我们需要将babel的自动模块转化功能关闭,否则你的es6模块将自动转化为commonjs模块,配置如下:{ “presets”: [ “react”, “stage-2”, [ “env”, { “modlues”: false // 关闭babel的自动转化模块功能,保留ES6模块语法 } ] ]}Tree-shaking编译时候可以在命令后使用–display-used-exports可以在shell打印出关于代码剔除的提示。Scope-Hoisting作用域提升,尽可能的把打散的模块合并到一个函数中,前提是不能造成代码冗余。因此只有那些被引用了一次的模块才能被合并。可能不好理解,下面demo对比一下有无Scope-Hoisting的编译结果。首先定义一个util.js文件export default ‘Hello,Webpack’;然后定义入口文件main.jsimport str from ‘./util.js’console.log(str);下面是无Scope-Hoisting结果:然后是Scope-Hoisting后的结果:与Tree-Shaking类似,使用Scope-Hoisting的前提也是必须是ES6模块,除此之外,还需要加入webpack内置插件,位于webpack文件夹,webpack/lib/optimize/ModuleConcatenationPlugin,配置如下:const ModuleConcatenationPlugin = require(‘webpack/lib/optimize/ModuleConcatenationPlugin’);module.exports = { //… plugins: [ new ModuleConcatenationPlugin() ] //…}另外,为了跟好的利用Scope-Hoisting,针对Npm的第三方模块,它们也可能提供了ES6模块,我们可以指定优先使用它们的ES6模块,而不是使用它们编译后的代码,webpack的配置如下:module.exports = { //… resolve: { // 优先采用jsnext:main中指定的ES6模块文件 mainFields: [‘jsnext:main’, ‘module’, ‘browser’, ‘main’] } //…}jsnext:main为业内大家约定好的存放ES6模块的文件夹,后续为了规范,更改为module文件夹。babel-plugin-transform-runtime在我们实际的项目中,为了兼容一些老式的浏览器,我们需要在项目加入babel-polyfill这个包。由于babel-polyfill太大,导致我们编译后的包体积增大,降低我们的加载性能,但是实际上,我们只需要加入我们使用到的不兼容的内容的polyfill就可以,这个时候babel-plugin-transform-runtime就可以帮我们去除那些我们没有使用到的polyfill,当然,你需要在babal-preset-env中配置你需要兼容的浏览器,否则会使用默认兼容浏览器。添加babel-plugin-transform-runtime的.babelrc配置如下:{ “presets”: [[“env”, { “targets”: { “browsers”: [“last 2 versions”, “safari >= 7”, “ie >= 9”, “chrome >= 52”] // 配置兼容浏览器版本 }, “modules”: false }], “stage-2”], “plugins”: [ “transform-class-properties”, “transform-runtime”, // 添加babel-plugin-transform-runtime “transform-decorators-legacy” ]}合理使用缓存webpack对应的缓存方案为添加hash,那我们为什么要给静态资源添加hash呢?避免覆盖旧文件回滚方便,只需要回滚html由于文件名唯一,可开启服务器永远缓然后,webpack对应的hash有两种,hash和chunkhash。hash是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值chunkhash根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。细想我们期望的最理想的hash就是当我们的编译后的文件,不管是初始化文件,还是chunk文件或者样式文件,只要文件内容一修改,我们的hash就应该更改,然后刷新缓存。可惜,hash和chunkhash的最终效果都没有达到我们的预期。另外,还有来自于的 extract-text-webpack-plugin的 contenthash,contenthash针对编译后的每个文件内容生成hash。只是extract-text-webpack-plugin在wbepack4中已经被弃用,而且这个插件只对css文件生效。webpack-md5-hash为了达到我们的预期效果,我们可以为webpack添加webpack-md5-hash插件,这个插件可以让webpack的chunkhash根据文件内容生成hash,相对稳定,这样就可以达到我们预期的效果了,配置如下:var WebpackMd5Hash = require(‘webpack-md5-hash’); module.exports = { // … output: { //… chunkFilename: “[chunkhash].[id].chunk.js” }, plugins: [ new WebpackMd5Hash() ]};合理拆包为了减少首屏加载的时候,我们需要将包拆分成多个包,然后需要的时候在加载,拆包方案有:第三方包,DllPlugin、externals。动态拆包,利用import()、require.ensure()语法拆包splitChunksPlugin针对第一点第三方包,我们也在第一章节构建优化中有介绍,这里就不详细说了。动态拆包首先是import(),这是webpack提供的语法,webpack在解析到这样的语法时,会将指定的目录文件打包成一个chunk,当成异步加载文件输出到编译结果中,语法如下:import(/* webpackChunkName: chunkName */ ‘./chunkFile.js’).then(_module => { // do something});import()遵循promise规范,可以在then的回调函数中处理模块。注意:import()的参数不能完全是动态的,如果是动态的字符串,需要预先指定前缀文件夹,然后webpack会把整个文件夹编译到结果中,按需加载。然后是require.ensure(),与import()类似,为webpack提供函数,也是用来生成异步加载模块,只是是使用callback的形式处理模块,语法如下:// require.ensure(dependencies: String[], callback: function(require), chunkName: String)require.ensure([], function(require){ const _module = require(‘chunkFile.js’);}, ‘chunkName’);splitChunksPluginwebpack4中,将commonChunksPlugin废弃,引入splitChunksPlugin,两个plugin的作用都是用来切割chunk。webpack 把 chunk 分为两种类型,initial和async。在webpack4的默认情况下,production构建会分析你的 entry、动态加载(import()、require.ensure)模块,找出这些模块之间共用的node_modules下的模块,并将这些模块提取到单独的chunk中,在需要的时候异步加载到页面当中。默认配置如下:module.exports = { //… optimization: { splitChunks: { chunks: ‘async’, // 标记为异步加载的chunk minSize: 30000, minChunks: 1, maxAsyncRequests: 5, maxInitialRequests: 3, automaticNameDelimiter: ‘~’, // 文件名中chunk的分隔符 name: true, cacheGroups: { vendors: { test: /[\/]node_modules[\/]/, priority: -10 }, default: { minChunks: 2, // 最小共享的chunk数 priority: -20, reuseExistingChunk: true } } } }};splitChunksPlugin提供了灵活的配置,开发者可以根据自己的需求分割chunk,比如下面官方的例子1代码:module.exports = { //… optimization: { splitChunks: { cacheGroups: { commons: { name: ‘commons’, chunks: ‘initial’, minChunks: 2 } } } }};意思是在所有的初始化模块中抽取公共部分,生成一个chunk,chunk名字为comons。在如官方例子2代码:module.exports = { //… optimization: { splitChunks: { cacheGroups: { commons: { test: /[\/]node_modules[\/]/, name: ‘vendors’, chunks: ‘all’ } } } }};意思是从所有模块中抽离来自于node_modules下的所有模块,生成一个chunk。当然这只是一个例子,实际生产环境中并不推荐,因为会使我们首屏加载的包增大。针对官方例子2,我们可以在开发环境中使用,因为在开发环境中,我们的node_modules下的所有文件是基本不会变动的,我们将其生产一个chunk之后,每次增量编译,webpack都不会去编译这个来自于node_modules的已经生产好的chunk,这样如果项目很大,来源于node_modules的模块非常多,这个时候可以大大降低我们的构建时间。最后现在大部分前端项目都是基于webpack进行构建的,面对这些项目,或多或少都有一些需要优化的地方,或许做优化不为完成KPI,仅为自己有更好的开发体验,也应该行动起来。 ...

January 19, 2019 · 2 min · jiezi

React Native 性能优化 (官网指南搬运)

最近在写React-Native 趁着这两天需求差不多完成了,实践了一些优化项。记录于此Life sucksPerformance参考 React native Performance查看性能打开开发者菜单(摇晃手机打开)???? 打开Show Perf Monitor 可以看到下图显示框UI 和 JS 的帧数都稳定保持在60 为最优情况。JS 的单线程所有的事件处理,API请求,等操作都在这个线程上,在this.setState大量数据时,状态的变动会导致re-render,这期间所有由JavaScript 控制的动画都会出现卡顿掉帧比如在切换路由时,帧数会有明显抖动。此时如果有一些在componentDidMount 执行的操作就会使得路由过渡动画非常卡顿。(后面会介绍一些可以尝试的解决方案开发环境性能比生产环境差开发环境下框架会有很多别的操作比如warning error 的输出,类型检测等等。如果要测试性能,最好在release 包测试。这样更加精准。生产环境移除console.开发时,会有很多console. 指令来帮助调试。并且一些依赖库也会有console.* 这些语句对JavaScript 线程来说是一个极大的消耗。可以通过Babel 在生产环境中移除掉console.*。安装插件npm i babel-plugin-transform-remove-console –save-dev配置.babelrc 文件{ “env”: { “production”: { “plugins”: [“transform-remove-console”] } }}处理大量数据列表时使用<FlatList />FlatList 组件更加适合来展示长列表,并且指定合适的 getItemLayout 方法, getItemLayout 会跳过渲染Item 时的布局计算,直接使用给定的配置(详情查看链接☝️)依赖懒加载在框架执行编写好的业务代码前,需要把在内存中加载并解析代码,代码量越大这个过程就更耗时,导致首屏渲染速度过慢。而且往往会出现一些页面或者组件根本不会被用户访问到。这时可以通过懒加载来优化。官网有给出例子VeryExpensive.jsimport React, { Component } from ‘react’;import { Text } from ‘react-native’;// … import some very expensive modules// You may want to log at the file level to verify when this is happeningconsole.log(‘VeryExpensive component loaded’);export default class VeryExpensive extends Component { // lots and lots of code render() { return <Text>Very Expensive Component</Text>; }}Optimized.jsimport React, { Component } from ‘react’;import { TouchableOpacity, View, Text } from ‘react-native’;let VeryExpensive = null; //定义变量export default class Optimized extends Component { state = { needsExpensive: false }; // 定义内部状态来控制组件是否需要加载 didPress = () => { // 在触发需要加载组件的事件时 if (VeryExpensive == null) { // 不重复引用 VeryExpensive = require(’./VeryExpensive’).default; // 把组件的引用赋给定义好的变量 } this.setState(() => ({ needsExpensive: true, // 更改控制的状态,触发组件re-render })); }; render() { return ( <View style={{ marginTop: 20 }}> <TouchableOpacity onPress={this.didPress}> <Text>Load</Text> </TouchableOpacity> {this.state.needsExpensive ? <VeryExpensive /> : null} </View> ); }}优化组件渲染次数React 在内部state 或者外部传入的props 发生改变时,会重新渲染组件。如果在短时间内有大量的组件要重新渲染就会造成严重的性能问题。这里有一个可以优化的点。使用PureComponent 让组件自己比较props 的变化来控制渲染次数,实践下来这种可控的方式比纯函数组件要靠谱。或者在Component 中使用 shouldComponentUpdate 方法,通过条件判断来控制组件的更新/重新渲染。使用PureComponent 时要注意这个组件内部是浅比较状态,如果props 的有大量引用类型对象,则这些对象的内部变化不会被比较出来。所以在编写代码时尽量避免复杂的数据结构细粒度组件,拆分动态/静态组件。需要在项目稳定并有一定规模后来统一规划。学习 immutable-js 异步,回调JavaScript 单线程,要利用好它的异步特性,和一些钩子回调。比如上面提到路由切换时componentDidMount 中的操作会导致卡顿,这里可以使用 InteractionManager.runAfterInteractions() 将需要执行的操作放到runAfterInteractions 的回调中执行。componentDidMount() { InteractionManager.runAfterInteractions(() => { // your actions })}需要注意的是 InteractionManager 是监听所有的动画/交互 完成之后才会触发 runAfterInteractions 中的回调,如果项目中有一些长时间动画或者交互,可能会出现长时间等待。所以 由于 InteractionManager 的不可控性,使用的时候要根据实际情况调整。在react-native 中的一些动画反馈,比如TouchableOpacity 在触摸时会响应 onPress 并且 自身的透明度会发生变化,这个过程中如果 onPress 中有复杂的操作,很可能会导致组件的透明反馈卡顿,这时可以将onPress 中的操作包裹在 requestAnimationFrame 中。这里给出一个我的实践(利用styled-component)import styled from ‘styled-components’export const TouchableOpacity = styled.TouchableOpacity.attrs({ onPress: props => () => { requestAnimationFrame(() => { props.onPressAsync && props.onPressAsync() }, 0) }})``这里把onPress 改成在 requestAnimationFrame 的回调中执行onPressAsync 传入的操作。同理,还在FlatList 的onReachEnd实践了这个操作,来避免iOS 中滚动回弹时执行操作的卡顿。以上,记录了近期写React-Native 的一些实践过的优化项。最后路漫漫其修远兮,吾将上下而求索May love & peace be with you参考React native Performance本文作者: Roy Luo本文链接: React Native 性能优化 (官网指南搬运) ...

January 18, 2019 · 2 min · jiezi

阿里大规模数据中心性能分析

郭健美,阿里巴巴高级技术专家,目前主要从事数据中心的性能分析和软硬件结合的性能优化。CCF 系统软件专委和软件工程专委的委员。曾主持国家自然科学基金面上项目、入选上海市浦江人才计划A类、获得 ACMSIGSOFT “杰出论文奖”。担任 ICSE'18NIER、ASE'18、FSE'19 等重要会议程序委员会委员。*数据中心已成为支撑大规模互联网服务的标准基础设施。随着数据中心的规模越来越大,数据中心里每一次软件(如 JVM)或硬件(如 CPU)的升级改造都会带来高昂的成本。合理的性能分析有助于数据中心的优化升级和成本节约,而错误的分析可能误导决策、甚至造成巨大的成本损耗。本文整理自阿里巴巴高级技术专家郭健美在 2018 年 12 月 GreenTea JUG Java Meetup上的分享,主要介绍阿里大规模数据中心性能监控与分析的挑战与实践。大家好,很高兴有机会与 Java 社区的开发者交流。我的研究领域在软件工程,主要集中在系统配置和性能方面。软件工程一个比较常见的活动是找 bug,当然找 bug 很重要,但后来也发现,即便 bug-free 的程序也会被人配置错,所以就衍生出了软件配置问题。很多软件需要配置化,比如 Java 程序或 JVM 启动时可以配置很多参数。通过配置,一套软件可以灵活地提供各种定制化的功能,同时,这些配置也会对软件整体性能产生不同的影响。当然这些还在软件配置方面,来了阿里以后,我有机会把这方面工作扩展到了硬件,会更多地结合硬件比如 CPU,来看系统的配置变更和升级改造对性能、可靠性以及业务上线效果的影响。今天主要谈谈我在这方面的一点工作。阿里最有代表性的事件是“双 11”。这里还是用的去年的数据,因为今年有些数据还没出来。左上角是双十一的销售额,去年大概是 253 亿美金,比美国同期 Thanksgiving、Black Friday、Cyber Monday 加起来的销售额还要多。当然这是从业务层面去看数据,技术同学会比较关注右边的数据,去年双十一的交易峰值达到 32.5 万笔/秒、支付峰值达到 25.6 万笔/秒。对于企业来说,这么高的峰值性能意味着什么?意味着成本!我们之所以关注性能,就是希望通过持续的技术创新,不断地提高性能、同时节省成本。双十一零点的峰值性能不是一个简单的数字,其背后需要一个大规模数据中心来支撑。 简单来说,阿里的基础架构的上层是各种各样的应用,比如淘宝、天猫、菜鸟、钉钉,还有云计算和支付宝等,这也是阿里的一个特色,即具有丰富的业务场景。底层是上百万台机器相连的大规模数据中心,这些机器的硬件架构不同、分布地点也不同,甚至分布在世界各地。中间这部分我们称之为中台,最贴近上层应用的是数据库、存储、中间件以及计算平台,然后是资源调度、集群管理和容器,再下面是系统软件,包括操作系统、JVM 和虚拟化等。中台这部分的产品是衔接社区与企业的纽带。这两年阿里开源了很多产品,比如 Dubbo、PouchContainer 等,可以看出阿里非常重视开源社区,也非常重视跟开发者对话。现在很多人都在讲开源社区和生态,外面也有各种各样的论坛,但是像今天这样与开发者直接对话的活动并不是那么多,而推动社区发展最终还是要依赖开发者。这样大规模的基础架构服务于整个阿里经济体。从业务层面,我们可以看到 253 亿美金的销售额、32.5 万笔交易/秒这样的指标。然而,这些业务指标如何分解下来、落到基础架构的各个部分就非常复杂了。比如,我们在做 Java 中间件或 JVM 开发时,都会做性能评估。大部分技术团队开发产品后都会有个性能提升指标,比如降低了 20% 的 CPU 利用率,然而这些单个产品的性能提升放到整个交易链路、整个数据中心里面,占比多少?对数据中心整体性能提升贡献多少?这个问题很复杂,涉及面很广,包括复杂关联的软件架构和各种异构的硬件。后面会提到我们在这方面的一些思考和工作。阿里的电商应用主要是用 Java 开发的,我们也开发了自己的 AJDK,这部分对 OpenJDK 做了很多定制化开发,包括:融入更多新技术、根据业务需要及时加入一些 patches、以及提供更好的 troubleshooting 服务和工具。大家也知道,今年阿里入选并连任了 JCPEC 职位,有效期两年,这对整个 Java 开发者社区、尤其是国内的 Java 生态都是一件大事。但是,不是每个人都了解这件事的影响。记得之前碰到一位同仁,提到 JCPEC 对阿里这种大业务量的公司是有帮助,对小公司就没意义了。其实不是这样的,参选 JCPEC 的时候,大公司、小公司以及一些社区开发者都有投票资格,小公司或开发者有一票,大公司也只有一票,地位是一样的。很多国外的小公司更愿意参与到社区活动,为什么?举个简单例子,由于业务需要,你在 JVM 8 上做了一个特性,费了很大的力气开发调试完成、业务上线成功,结果社区推荐升级到 JVM11 上,这时你可能又需要把该特性在 JVM 11 上重新开发调试一遍,可能还要多踩一些新的坑,这显然增加了开发代价、拉长了上线周期。但如果你能影响社区标准的制定呢?你可以提出将该特性融入社区下一个发布版本,有机会使得你的开发工作成为社区标准,也可以借助社区力量完善该特性,这样既提高了技术影响力也减少了开发成本,还是很有意义的。过去我们做性能分析主要依赖小规模的基准测试。比如,我们开发了一个 JVM 新特性, 模拟电商的场景,大家可能都会去跑SPECjbb2015 的基准测试。再比如,测试一个新型硬件,需要比较 SPEC 或 Linpack 的基准测试指标。这些基准测试有必要性,因为我们需要一个简单、可复现的方式来衡量性能。但基准测试也有局限性,因为每一次基准测试都有其限定的运行环境和软硬件配置,这些配置设定对性能的影响可能很大,同时这些软硬件配置是否符合企业需求、是否具有代表性,都是需要考虑的问题。阿里的数据中心里有上万种不同的业务应用,也有上百万台分布在世界各地的不同服务器。当我们考虑在数据中心里升级改造软件或硬件时,一个关键问题是小规模基准测试的效果是否能扩展到数据中心里复杂的线上生产环境?举个例子,我们开发了 JVM 的一个新特性,在 SPECjbb2015 的基准测试中看到了不错的性能收益,但到线上生产环境灰度测试的时候,发现该特性可以提升一个 Java 应用的性能、但会降低另一个 Java 应用的性能。同时,我们也可能发现即便对同一个 Java 应用,在不同硬件上得到的性能结果大不相同。这些情况普遍存在,但我们不可能针对每个应用、每种硬件都跑一遍测试,因而需要一个系统化方法来估计该特性对各种应用和硬件的整体性能影响。对数据中心来说,评估每个软件或硬件升级的整体性能影响非常重要。比如,“双11”的销售额和交易峰值,业务层面可能主要关心这两个指标,那么这两个指标翻一倍的时候我们需要买多少台新机器?需要多买一倍的机器么?这是衡量技术能力提升的一个手段,也是体现“新技术”对“新商业”影响的一个途径。我们提出了很多技术创新手段,也发现了很多性能提升的机会,但需要从业务上也能看出来。为了解决上面提到的问题,我们开发了 SPEED 平台。首先是估计当前线上发生了什么,即 Estimation,通过全域监控采集数据,再进行数据分析,发现可能的优化点。比如,某些硬件整体表现比较差,可以考虑替换。然后,我们会针对软件或硬件的升级改造做线上评估,即 Evaluation。比如,硬件厂商推出了一个新硬件,他们自己肯定会做一堆评测,得到一组比较好的性能数据,但刚才也提到了,这些评测和数据都是在特定场景下跑出来的,这些场景是否适合用户的特定需求?没有直接的答案。通常,用户也不会让硬件厂商到其业务环境里去跑评测。这时候就需要用户自己拿这个新硬件做灰度测试。当然灰度规模越大评测越准确,但线上环境都直接关联业务,为了降低风险,实际中通常都是从几十台甚至几台、到上百台、上千台的逐步灰度。SPEED 平台要解决的一个问题就是即便在灰度规模很小时也能做一个较好的估计,这会节约非常多的成本。随着灰度规模增大,平台会不断提高性能分析质量,进而辅助用户决策,即 Decision。这里的决策不光是判断要不要升级新硬件或新版软件,而且需要对软硬件全栈的性能有一个很好的理解,明白什么样的软硬件架构更适合目标应用场景,这样可以考虑软硬件优化定制的方向。比如,Intel 的 CPU 从 Broadwell 到 Skylake,其架构改动很大,但这个改动的直接效果是什么?Intel 只能从基准测试中给答案,但用户可能根据自己的应用场景给出自己的答案,从而提出定制化需求,这对成本有很大影响。最后是 Validation,就是通常规模化上线后的效果来验证上述方法是否合理,同时改进方法和平台。数据中心里软硬件升级的性能分析需要一个全局的性能指标,但目前还没有统一的标准。Google 今年在 ASPLOS 上发表了一篇论文,提出了一个叫 WSMeter 的性能指标,主要是基于 CPI 来衡量性能。在 SPEED 平台里,我们也提出了一个全局性能指标,叫资源使用效率 RUE。基本思想很简单,就是衡量每个单位 Work Done 所消耗的资源。这里的 Work Done 可以是电商里完成的一个 Query,也可以是大数据处理里的一个 Task。而资源主要涵盖四大类:CPU、内存、存储和网络。通常我们会主要关注 CPU 或内存,因为目前这两部分消费了服务器大部分的成本。RUE 的思路提供了一个多角度全面衡量性能的方法。举个例子,业务方反映某台机器上应用的 response time 升高了,这时登录到机器上也看到 load 和 CPU 利用率都升高了。这时候你可能开始紧张了,担心出了一个故障,而且很可能是由于刚刚上线的一个新特性造成的。然而,这时候应该去看下 QPS 指标,如果 QPS 也升高了,那么也许是合理的,因为使用更多资源完成了更多的工作,而且这个资源使用效率的提升可能就是由新特性带来的。所以,性能需要多角度全面地衡量,否则可能会造成不合理的评价,错失真正的性能优化机会。下面具体讲几个数据中心性能分析的挑战,基本上是线上碰到过的具体问题,希望能引起大家的一些思考。首先是性能指标。可能很多人都会说性能指标我每天都在用,这有什么好说的。其实,真正理解性能指标以及系统性能本身并不是那么容易。举个例子,在数据中心里最常用的一个性能指标是 CPU 利用率,给定一个场景,数据中心里每台机器平均 CPU 利用率是 50%,假定应用需求量不会再增长、并且软件之间也不会互相干扰,那么是否可以把数据中心的现有机器数量减半呢?这样,理想情况下 CPU 利用率达到 100% 就可以充分利用资源了,是否可以这样简单地理解 CPU 利用率和数据中心的性能呢?肯定不行。就像刚才说的,数据中心除了 CPU,还有内存、存储和网络资源,机器数量减半可能很多应用都跑不起来了。再举个例子,某个技术团队升级了其负责的软件版本以后,通过线上测试看到平均 CPU 利用率下降了 10%,因而声明性能提升了 10%。这个声明没有错,但我们更关心性能提升以后是否能节省成本,比如性能提升了 10%,是否可以把该应用涉及的 10%的机器关掉?这时候性能就不应该只看 CPU 利用率,而应该再看看对吞吐量的影响。所以,系统性能和各种性能指标,可能大家都熟悉也都在用,但还需要更全面地去理解。刚才提到 SPEED 的 Estimation 会收集线上性能数据,可是收集到的数据一定对吗?这里讲一个 Hyper-Threading 超线程的例子,可能对硬件了解的同学会比较熟悉。超线程是 Intel 的一个技术,比如我们的笔记本,一般现在都是双核的,也就是两个hardwarecores,如果支持超线程并打开以后,一个 hardware core 就会变成两个 hardware threads,即一台双核的机器会有四个逻辑 CPU。来看最上面一张图,这里有两个物理核,没有打开超线程,两边 CPU 资源都用满了,所以从任务管理器报出的整台机器平均 CPU 利用率是 100%。左下角的图也是两个物理核,打开了超线程,每个物理核上有一个 hardwarethread 被用满了,整台机器平均 CPU 利用率是 50%。再看右下角的图,也是两个物理核,也打开了超线程,有一个物理核的两个hardware threads 都被用满了,整台机器平均 CPU 利用率也是 50%。左下角和右下角的 CPU 使用情况完全不同,但是如果我们只是采集整机平均 CPU 利用率,看到的数据是一样的!所以,做性能数据分析时,不要只是想着数据处理和计算,还应该注意这些数据是怎么采集的,否则可能会得到一些误导性的结果。数据中心里的硬件异构性是性能分析的一大挑战,也是性能优化的一个方向。比如这里左边的 Broadwell 架构,是 Intel 过去几年服务器 CPU 的主流架构,近几年在推右边的 Skylake 架构,包含最新的 Cascade Lake CPU。Intel 在这两个架构上做了很大的改动,比如,Broadwell 下访问内存还是保持多年的环状方式,而到了 Skylake 改为网格状方式。再比如,L2 Cache 到了Skylake 上扩大了四倍,通常来说这可以提高 L2 Cache 的命中率,但是 cache 越大也不代表性能就一定好,因为维护 cache coherence 会带来额外的开销。这些改动有利有弊,但我们需要衡量利和弊对整体性能的影响,同时结合成本来考虑是否需要将数据中心的服务器都升级到 Skylake。了解硬件的差异还是很有必要的,因为这些差异可能影响所有在其上运行的应用,并且成为硬件优化定制的方向。现代互联网服务的软件架构非常复杂,比如阿里的电商体系架构,而复杂的软件架构也是性能分析的一个主要挑战。举个简单的例子,图中右边是优惠券应用,左上角是大促主会场应用,右下角是购物车应用,这三个都是电商里常见的业务场景。从 Java 开发的角度,每个业务场景都是一个 application。电商客户既可以从大促主会场选择优惠券,也可以从购物车里选择优惠券,这是用户使用习惯的不同。从软件架构角度看,大促主会场和购物车两个应用就形成了优惠券应用的两个入口,入口不同对于优惠券应用本身的调用路径不同,性能影响也就不同。所以,在分析优惠券应用的整体性能时需要考虑其在电商业务里的各种错综复杂的架构关联和调用路径。像这种复杂多样的业务场景和调用路径是很难在基准测试中完全复现的,这也是为什么我们需要做线上性能评估。这是数据分析里著名的辛普森悖论,在社会学和医学领域有很多常见案例,我们在数据中心的性能分析里也发现了。这是线上真实的案例,具体是什么 App 我们不用追究。假设还用前面的例子,比如 App 就是优惠券应用,在大促的时候上线了一个新特性 S,灰度测试的机器占比为 1%,那么根据 RUE 指标,该特性可以提升性能 8%,挺不错的结果。但是如果优惠券应用有三个不同的分组,分组假设就是刚才提到的不同入口应用,那么从每个分组看,该特性都降低了应用的性能。同样一组数据、同样的性能评估指标,通过整体聚集分析得到的结果与通过各部分单独分析得到的结果正好相反,这就是辛普森悖论。既然是悖论,说明有时候应该看总体评估结果,有时间应该看部分评估结果。在这个例子里面,我们选择看部分评估、也就是分组上的评估结果,所以看起来这个新特性造成了性能下降,应该继续修改并优化性能。所以,数据中心里的性能分析还要预防各种可能的数据分析陷阱,否则可能会严重误导决策。最后,还有几分钟,简单提一下性能分析师的要求。这里通常的要求包括数学、统计方面的,也有计算机科学、编程方面的,当然还有更重要的、也需要长期积累的领域知识这一块。这里的领域知识包括对软件、硬件以及全栈性能的理解。其实,我觉得每个开发者都可以思考一下,我们不光要做功能开发,还要考虑所开发功能的性能影响,尤其是对数据中心的整体性能影响。比如,JVM 的 GC 开发,社区里比较关心 GC 暂停时间,但这个指标与 Java 应用的 response time 以及所消耗的 CPU 资源是什么关系,我们也可以有所考虑。当然,符合三块要求的候选人不好找,我们也在总结系统化的训练流程,欢迎对系统性能有兴趣的同学加入我们。本文作者:amber涂南阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 17, 2019 · 2 min · jiezi

程序性能优化-局部性原理

更多文章概念一个编写良好的计算机程序常常具有良好的局部性,它们倾向于引用邻近于其他最近引用过的数据项的数据项,或者最近引用过的数据项本身,这种倾向性,被称为局部性原理。有良好局部性的程序比局部性差的程序运行得更快。局部性通常有两种不同的形式:时间局部性 在一个具有良好时间局部性的程序中,被引用过一次的内存位置很可能在不远的将来被多次引用。空间局部性 在一个具有良好空间局部性的程序中,如果一个内存位置被引用了一次,那么程序很可能在不远的将来引用附近的一个内存位置。时间局部性示例function sum(arry) { let i, sum = 0 let len = arry.length for (i = 0; i < len; i++) { sum += arry[i] } return sum}在这个例子中,变量sum在每次循环迭代中被引用一次,因此,对于sum来说,具有良好的时间局部性空间局部性示例具有良好空间局部性的程序// 二维数组 function sum1(arry, rows, cols) { let i, j, sum = 0 for (i = 0; i < rows; i++) { for (j = 0; j < cols; j++) { sum += arry[i][j] } } return sum}空间局部性差的程序// 二维数组 function sum2(arry, rows, cols) { let i, j, sum = 0 for (j = 0; j < cols; j++) { for (i = 0; i < rows; i++) { sum += arry[i][j] } } return sum}再回头看一下时间局部性的示例,像示例中按顺序访问一个数组每个元素的函数,具有步长为1的引用模式。如果在数组中,每隔k个元素进行访问,就称为步长为k的引用模式。一般而言,随着步长的增加,空间局部性下降。这两个例子有什么区别?区别在于第一个示例是按照列顺序来扫描数组,第二个示例是按照行顺序来扫描数组。数组在内存中是按照行顺序来存放的,结果就是按行顺序来扫描数组的示例得到了步长为rows的引用模式;而对于按列顺序来扫描数组的示例来说,其结果是得到一个很好的步长为1的引用模式,具有良好的空间局部性。性能测试运行环境cpu: i5-7400浏览器: chrome 70.0.3538.110对一个长度为9000的二维数组(子数组长度也为9000)进行10次空间局部性测试,时间(毫秒)取平均值,结果如下:所用示例为上述两个空间局部性示例按列排序按行排序1242316从以上测试结果来看,二维数组按列顺序访问比按行顺序访问快了1个数量级的速度。测试代码const arry = []let [num, n, cols, rows] = [9000, 9000, 9000, 9000]let temp = []while (num) { while (n) { temp.push(n) n– } arry.push(temp) n = 9000 temp = [] num–}let last, now, vallast = new Date()val = sum1(arry, rows, cols)now = new Date()console.log(now - last)console.log(val)last = new Date()val = sum2(arry, rows, cols)now = new Date()console.log(now - last)console.log(val)function sum1(arry, rows, cols) { let i, j, sum = 0 for (i = 0; i < rows; i++) { for (j = 0; j < cols; j++) { sum += arry[i][j] } } return sum}function sum2(arry, rows, cols) { let i, j, sum = 0 for (j = 0; j < cols; j++) { for (i = 0; i < rows; i++) { sum += arry[i][j] } } return sum}参考资料深入理解计算机系统 ...

January 17, 2019 · 2 min · jiezi

优化循环的方法-循环展开

更多文章循环展开是一种程序变换,通过增加每次迭代计算的元素的数量,减少循环的迭代次数。用代码来说明就是将for (i = 0; i < len; i++) { sum += arry[i]}替换为for (i = 0; i < len; i += 2) { newSum += arry[i] + arry[i + 1]}循环展开对于算术运算来说,优化的作用是很大的。我分别对整数运算和浮点数运算作了多次测试,得出表格如下:操作整数整数(优化后)浮点数浮点数(优化后)+360163354164-379167341177*350160364163/1185715263测试环境cpu:i5-7400浏览器: chrome 70.0.3538.110运算是用了1千万个数,取值是运行十次测试得出的平均数。附上加法测试的代码const arry = []let num = 10000000while (num) { arry.push(num) num–}let sum = 0let last = new Date()let i let len = arry.lengthfor (i = 0; i < len; i++) { sum += arry[i]}let now = new Date()console.log(now - last)let newSum = 0last = new Date()for (i = 0; i < len; i += 2) { newSum += arry[i] + arry[i + 1]}now = new Date()console.log(now - last)console.log(sum, newSum) ...

January 16, 2019 · 1 min · jiezi

实时报表 T+0 的实现方案

【摘要】基于数据库系统的 T+0 全量实时查询,在数据量很大时一般只能进行数据库扩容(包括分库手段),成本高昂;如果采用文件系统和生产数据库混合运算,就可以实现低成本高性能的 T+0 查询,而热导出机制则是这个方案的基础!让我们一起去乾学院看个究竟吧:实时报表 T+0 的实现方案!一 问题背景在报表的应用系统中,用户越来越关注数据的实时性,希望最新发生的数据能在报表中体现出来,也就是我们常说的T+0场景, 以此及时辅助决策、驱动运营。比如交通大数据应用的场景:需要结合实时数据了解车辆通行密度,合理进行道路规划,同时根据历史数据预测线路拥堵情况、事故多发地提醒等等。但常规的方案:报表+数据仓库+ETL工具很难实现此类实时报表,往往只能看到昨天、上周甚至是上个月的情况,也就是T+1、T+7、T+30等,我们统称为T+n报表。究其原因,困难大概体现在如下三个方面:1、如果报表的历史数据和最新数据都从生产系统读取,虽然可以实现T+0报表,但是会对生产数据库造成压力,当数据量越来越大时,产生性能瓶颈,直接影响业务;并且大量的历史数据会占用高昂的数据库成本(存储成本和性能成本)。2、如果采用数据仓库的方式,那么ETL从生产库中取出数据,需要较长的“窗口时间”,一般是业务人员下班之后,到第二天早上上班之前,所以能看到的最新数据也只能是T+1。3、虽然理论上可以从历史库中和生产库中同时取数据形成实时报表,但是一般的报表工具都不具备跨库混合计算的能力,其他的跨库计算方案又比较复杂,难以实施,并且性能较低。二 解决思路那么,是否有成本更低、实施起来更简单的T+0报表方案呢?下面将要介绍的润乾集算器,就是这样一款利器,利用集算器的混合数据源能力就能实现低成本的T+0实时报表。实现思路:把不再发生变动的大量历史数据采用数据文件存储,仅从生产库读取少量新数据,在保证报表实时性的同时,降低了历史数据存储的成本,减少了报表系统对生产数据库造成的负载。下图显示了常规T+n方案和集算器T+0方案的结构对比,应该说,引入集算器后,减少了很多不必要的成本和多余的组件,整个体系架构也变得更加清新与合理了:上图新处理方式体系结构中的”导出(非实时)”是指在非工作时间(例如晚上),定时将生产数据库的新增数据同步到存储历史数据的文件中;关于数据外置方案、设计数据存储组织、定时任务等相关准备和外围工作,具体做法可参考<<基于文件系统实现可追加的数据集市>>的相关章节,这里不再赘述。三 混合运算场景下面,我们就通过制作“实时流程工站不良柏拉图”这个例子,来看一下集算器是如何利用历史数据结合当期数据进行混合运算,实现T+0方案的。报表最终的展示效果如下图:这张报表清楚地显示了电子设备在生产过程中,80%的问题是由20%的原因造成的,对于找出产生大多数问题的关键原因很有优势。报表中数据的查询过程是:根据选择开始日期、结束日期进行过滤查询;先按照不良代码分组,统计汇总每个分类的不良数量,并按照汇总数量降序,然后计算出不良累计比率(算法为“(不良数量累计汇总/总不良数量汇总)100”)。报表上部的查询按钮是报表工具提供的“参数模板”功能,具体做法参见教程,这里不再赘述。3.1编写数据查询脚本我们假定已经将变化不大的历史数据搬出了数据库,采用集文件(集文件利用集算器提供的压缩格式,具有更好IO性能)存储,命名为MES-pre.btx,同时每天定时执行数据同步脚本,把前一天的数据追加到当前数据文件中;查询涉及的当天少量数据直接从生产数据库(demo)取出,以此保证数据的实时性。集算器SPL脚本如下(也支持仅查历史数据的情况):A1:连接预先配置好的生产数据库(demo)B1:查询字典表,不良代码、不良名称A2:建立数据库游标,用简单的sql读取数据表的数据。Sql的过滤条件部分会根据逻辑判断进行动态拼接,当结束日期>=当前系统日期时,代表查询当天的实时数据,否则做一次结果为空的查询动作,以适应只查历史数据的业务场景。@x选项是指读完数据库后关闭连接。A3:建立数据文件D:/PT/MES-pre.btx的游标。文件游标允许分批从大数据文件中读取数据,从而避免内存溢出。@b选项是指按照集算器提供的二进制格式来读取文件,同时根据传入的开始日期(Bfiledate)、结束日期(Efiledate)过滤出符合条件的记录B3:将数据库游标(新数据)和文件游标(历史数据)合并A4:利用groups函数,完成对合并后游标的分组汇总,同时多构造了几列:不良名称、不良累计数量、不良累计比率,方便后面的赋值计算。A5:通过switch()函数在A4结果的”不良名称”字段上建立指向B1表中code字段的指针引用记录,实现关联,如下图:B5:按照不良数量降序排列。如下图:A6:计算不良累计数量;可以看到,集算器用“不良累计数量[-1]”来表示上一行的不良数量,可以轻松进行相对位置的计算。B6:对不良数量进行总计A7:计算出不良累计比率,算法为“(不良数量累计汇总/总不良数量汇总)100”,同时保留两位小数,计算结果如下图:B7-A8:取出需要的字段,将关联了不良名称后的结果集返回给报表工具,如下图:3.2作为报表数据源在利用集算器完成了数据查询工作后,为了在报表中使用查询结果,可以在报表中直接将集算器设置为数据源,用法和使用数据库一样简单,具体做法如下:l 在报表中定义参数(Bfiledate、Efiledate),l 设置集算器数据集,并传递报表参数,l 设计报表统计图如下图所示:完成报表设计后,输入参数进行计算,就可以得到希望的报表了。四 数据预处理上一章节中,通过对历史数据(文件)和实时数据(数据库)进行混合计算,就能够轻松实现实时报表(T+0)方案;而为了做到这一点,相应的数据预处理,包括怎么导出到文件、设计怎样的存储组织等,也就显得尤为重要了。接下来将讨论历史数据导出到文件的几种模式及优缺点分析:冷导出、折中办法、热导出。4.1冷导出关于用文件存储历史数据能够带来的诸多好处,可以参考<<基于文件系统实现可追加的数据集市>>的相关章节,这里不再赘述。所谓冷导出,就是允许有一段“时间窗口” ,能够从生产库取出历史数据追加导出到文件中。例如每天的凌晨2-6点为定时执行任务的时间窗口。冷导出的缺点也很明显,在追加数据导出到文件的这段时间里,这个文件是不可读的,也就是说相关的查询也无法进行了,所以,从本质上说,冷导出并没有真正意义上做到T+0实时查询(生产系统不停机,查询系统也不停机)。不过,这里顺便解释一下:如果使用另一个数据库存储历史数据,就不会有这样的问题。原因在于关系型数据库支持事务一致性,数据写入的同时仍然可以很好地支持查询。当然这样做肯定也会牺牲一部分性能,当每天导出的数据量较多时对资源占用相当巨大(因为数据库回滚段会很大)。所以一致性和高性能在一定程度上是矛盾的。数据库虽然有一致性,但数据库本身太慢太贵;而集算器(集文件)可以获得高性能,但没有事务一致性,在维护数据的同时不能参与其他计算。不过,在对业务场景要求不是很高的情况下,冷导出也是够用了,下面我们还是简单举例说明一下如何编写集算器脚本,获取昨天的历史数据追加到当前集文件中,代码如下:A1:按路径打开需要导出的集文件路径B1:连接数据库(demo)A2:根据sql创建数据库游标,获取昨日数据,参数为昨天日期; @x选项是指读完数据库后关闭B2:执行结果追加写入到集文件4.2折中办法针对”冷导出”方案的不足,比较容易想到的折中办法就是:历史数据不再按照追加的模式写入到一个集文件中,而是把文件拆开,让彼此之间的耦合度更低,互不影响。这样做的话,就需要考虑以下两条规则:1、每天导出一个独立的集文件,可以用年月日命名,这样导出过程中,就不会影响对已导出的历史数据的查询。2、在查询脚本中增加时间范围判断,规避掉导出的”时间窗口”;比如定时任务的时间窗口为每天凌晨2-6点;在查询脚本中,可以根据查询动作的当前时间点进行逻辑判断,如果查询发生在当天6点以后,说明数据导出已经完成,那么数据来源就是集文件(到昨天为止的历史数据)+当前数据库(到今天当前时间点的新数据),若查询发生在当天6点以前的,那就是集文件(到前天为止的历史数据)+当前数据库(昨天到今天当前时间点的新数据)。这种办法的缺点就是在设计数据存储组织时,文件会分的比较碎,逻辑判断部分的代码也会显得比较冗长,而文件管理也会麻烦一些。但不管怎样,还是能够达到要求,实现真正意义上的实时报表(T+0)方案。下面介绍一下实现步骤。4.2.1设计数据存储组织历史数据按照业务模块进行划分,每天数据存一份集文件。目录结构为:/业务模块/数据明细表/年月日文件名,如下图所示:4.2.2同步昨天数据到文件改造“冷导出”方案中数据导出脚本,从数据库中获取昨天的历史数据每天存一份集文件,用年月日命名,代码如下:A1:按路径打开需要导出的集文件路径,每天一个,用年月日命名前面已经解释过的格子的代码这里不再赘述。4.2.3数据查询首先,我们需要写一个工具脚本,主要功能是能够根据传入的开始日期、结束日期,过滤出需要查询跨度范围的多个集文件路径,同时判断路径下的集文件对象是否存在。脚本命名为:判断读取文件的范围.dfx,编写代码如下:脚本接收3个参数,开始日期(startDate),结束日期(endDate),集文件的存储路径(path)A1:当传入的结束日期>=当前系统日期时,并且当前时间是在当天6点之后的,返回昨天日期,在当天6点之前的,返回前天日期,否则就返回传入的实际结束日期A2:根据开始日期,计算后的结束日期,默认按天间隔获取日期范围A3:循环A2,通过集文件的存储路径与该日期段内的年月日进行拼接,利用string()函数进行格式化A4:判断路径下的文件是否真实存在,由A5返回实际存在的文件路径,最终结果如下图:然后,我们需要对前面章节中“混合运算场景”数据查询的脚本做一些改造,值得注意的是这里将采用多路游标的概念,将多个游标合并成一个游标使用,改造后的脚本如下:前面已经解释过的格子代码这里不再赘述。A2:建立数据库游标,根据逻辑判断动态拼接sql,当查询的结束日期>=当前系统日期时,并且当前查询时间点是在当天6点之后的,只查询当天的实时数据,当前查询时间点发生在当天6点之前的,查询返回昨天和当天的实时数据;否则都不满足的情况下,做一次结果为空的查询动作,适应只查询历史数据的业务场景。A3:调用”判断读取文件的范围.dfx”,传入脚本参数开始日期、结束日期的值,获得起止日期内的所有集文件的集合A4:循环A3,分别打开每个集文件对象,根据文件创建游标,其中cursor()函数使用@b选项代表从集文件中读取。B4:利用集算器提供的多路游标概念,把数据结构相同的多个游标合并成一个游标使用。使用时,多路游标采用并行计算来处理各个游标的数据,可以通过设置cs.mcursor(n) 函数中的n来决定并行数,当n空缺时,将按默认自动设置并行数A9:最后返回结果集给报表工具使用4.3热导出所谓热导出,是相对于冷导出而言的。热导出要保证查询系统永不停机,在导出数据的过程中有查询请求进来,依然能够工作。热导出一般适用于实时查询场景要求较高的情况。4.3.1实现思路热导出需要利用文件的备份机制结合数据库的一致性来实现热切换动作。为了便于理解,可参考以下逻辑图:首先,在数据库中建备份表,主要目的是为了记录当前正在使用的是哪个备份文件,以及从DB中取的热数据的日期范围,查询系统启动时把这个表清空。其次,导出历史数据到集文件A,同时备份一个文件B,然后在数据库备份表中记录该文件A,以及设定从DB中取的热数据的日期(比如某个时刻之后);这个动作在系统初始化运行时,只做一次。然后,设计数据查询的流程:1、在数据库中建状态表,当数据查询时,先从备份表中查出可用的备份是哪个文件以及热数据的日期范围,然后加入一条记录到状态表中,表明该备份文件正有一个查询,当查询完成后将在状态表中把这条记录删除,可以用自增列的方式。再次,设计数据导出到集文件的流程:1、每天凌晨2点执行定时任务,先同步历史数据追加到文件B上,当导出完成后,修改数据库备份表的记录为使用文件B,同时修改从DB中取热数据的范围,以后新产生的查询动作都将使用文件B2、检查并等待状态表中A的使用记录都已清空(基于A的所有查询都结束了),这时才会同步历史数据追加到文件A上,否则每等待1分钟就循环检查一次。3、当步骤2的数据追加完成后,再修改数据库备份表为使用文件A,以后的新产生的查询又会回到了使用文件A,从而达到热切换的动作。4、直到等待状态表中B的使用记录都清空(基于B的查询也都结束了)5、整个过程执行完成,可以等待下一轮导出这里需要特别说明的是,备份表、状态表必须用数据库作为媒介,从而利用数据库的一致性;不能用文件记录备份表、状态表的内容,因为文件无法保持一致性,当多任务并发时可能就乱了。4.3.2数据查询第一步,在数据库中定义”备份表”,包含三个字段(文件名称/边界时间/标识),同时定义”查询状态表”,包含三个字段(唯一标识/文件名称/当前系统时间,其中定义唯一标识为自增列),数据结构分别如下图示:第二步,通过集文件A备份一个集文件B,然后在“备份表”中记录可查询的备份文件为A,并设定从DB中取的热数据边界时间(定义为每日的零点),此步操作如果用集算器脚本执行,样例代码如下:A1:根据导出的集文件A,复制备份同样的文件BA2:备份文件B完成后,往数据表中写入当前可用的集文件A,当前系统时间(零点),给定标识列为:WORKING_STATUS第三步,我们需要对前面章节中“混合运算场景”数据查询的脚本做一些改造,改造后的脚本如下(此例中也支持仅查历史数据的情况):前面已经解释过的格子代码这里不再赘述。A2:根据标识WORKING_STATUS作为条件,查询出来当前可用的集文件名称,以及热数据取值的边界日期时间B2:定义变量name, crashtime并赋值,便于后面单元格计算引用。B3:此单元格做了两步动作,首先,写入一条记录到状态表中,表明该当前备份文件正有一个查询,其中uniques为自增列;接着在插入记录后,通过执行【SELECT @@IDENTITY】获取上一条插入语句中生成的自增长字段的值,赋值给变量uniques,便于A9查询时引用。数据库中的效果如下图:A9:当查询完成后,根据变量uniques的值作为条件,在状态表中把这条记录删除,效果如下图:4.3.3同步数据与热切换改造“冷导出”方案中数据导出脚本,每天凌晨2点定时执行,代码如下:前面已经解释过的格子代码这里不再赘述。C2:当历史数据同步追加到文件B上,修改数据库”备份表”的记录为使用文件B,同时修改从DB中取热数据的边界日期范围,执行结果如下图:A3-B3:循环查询”状态表”中A的使用记录是否已清空,若发现还有基于A的查询没有结束,那就等待1分钟,然后接着循环,直到基于A的查询全部结束;其中A3的数据库连接表达式需要特别说明一下:通常情况下,在B1单元格中已经定义了数据库连接,在A3中,可直接引用,写成:for B1.query@1(“SELECT COUNT() FROM STATUS WHERE NAME=‘A’”)>0不过,有些数据库在默认情况下做成了一次连接只处理一个事务,这样会导致A3在循环的时候结果不会变化,总是按照第一次查询出来的结果为主,比如第一次查询返回是true,当数据库发生变化了,它还是返回true,为了保险期间,可以写成如下格式:for connect(“demo”).query@1x(“SELECT COUNT() FROM STATUS WHERE NAME=‘A’”)>0这个属于数据库配置的范畴,可以通过数据库的连接参数来控制,这里不再详解。C4:当文件A的数据追加完成后,再修改数据库”备份表”的记录为使用文件A,以后新产生的查询就会再使用文件A,执行结果如下图:A5-B5:循环查询”状态表”中B的使用记录是否已清空,若发现还有基于B的查询没有结束,那就等待1分钟,然后接着循环,直到基于B的查询全部结束, 此轮整个导出过程全部完成,然后等待下一轮导出五 总结实时报表(T+0)的场景下,数据的热导出是个有些复杂的话题,不过,利用集算器(集文件)的备份机制结合数据库的一致性就可以轻松应对这类难题了,其中主要用到了以下两个优势:1 、跨库混合计算集算器作为独立的计算引擎,可以并行指挥各个数据库分别计算,收集结果后再进行一轮汇总运算,然后向前端提交或者落地,从而可以很简单的实现T+0全量查询报表。同时,在集算器跨库混合计算模型下,也不要求数据库是否同构,历史数据可以选择存储在成本更低的开源数据库中,例如Oracle和MySQL的混搭集群。2 、高性价比、高性能的集文件无需构建数仓,将历史数据外置存放到文件系统中,不仅便于管理,而且可以获得更高效的IO性能和计算能力,从而很好的解决了关系型数据库中由于数据量大而导致的性能瓶颈和存储成本。

January 14, 2019 · 1 min · jiezi

车险往年保单关联计算的性能优化

【摘要】保险行业计算车险往年保单,需要按照车辆 vin 码、车架号、牌照种类和牌照号等多字段关联,涉及到几千万甚至上亿的大表,用存储过程计算非常耗时。点击车险往年保单关联计算的性能优化,去乾学院看看集算器如何把几个小时的计算缩短到十几分钟!问题的提出保险行业中,往往需要根据往年保单来快速计算和生成当年新的保单。以车险为例,在提醒老客户续保时就需要计算指定时间段的往年保单,例如某省级公司需要定期计算特定月份内可续保保单对应的历史保单。而目前在大多数保险营运系统中,这类批量数据处理任务都是由存储过程实现的,其中存在的典型问题就是存储过程性能差,运行时间长。如果只是计算一天的历史保单,运行时间尚可接受;如果时间跨度较大,运行时间就会长的无法忍受,基本就变成不可能完成的任务了。解决思路与过程案例场景说明下面我们将针对这种基于历史保单信息的计算任务的性能优化。实际业务中遇到的真实的存储过程很长,有2000多行。我们这里对问题进行了简化,只分析主体的部分,进而讨论集算器SPL语言优化类似计算的方法和思路。这个场景中计算用到的数据表包括:保单表和保单-车辆明细表。对于较大的省份,保单表和保单-车辆明细表都有几千万数据存量,每天新增保单的增量数据有一到两万条。经过简化的两个表结构如下:保单表 policyid char(22) not null ,– 保单编码(主键) policyno char(22),– 保单号 startdate datetime year to second,– 开始日期 enddate datetime year to second– 结束日期 保单 - 车辆明细表policyid char(22) not null , – 保单编码(主键) itemid decimal(8,0) not null ,– 明细编码(主键) licensenoid varchar(20),– 牌照编码 licensetype char(3),– 牌照种类 vinid varchar(18),–vin 编码 frameid varchar(30),– 车架编码新旧保单的对照表:policyid char(22) not null , – 保单编码oldpolicyid char(22)—上年保单编码往年保单的计算输入参数是起始日期(istart)和结束日期(iend),计算目标是新旧保单的对照表,找不到旧保单的将被舍弃。计算过程简化描述如下:1、 从保单表中,找出开始日期在指定时间段(istart和iend之间)内的新增保单。2、 用新增保单关联上一年的历史保单。关联的条件是:vin编码相同;或者车架编码相同;或者牌照种类、牌照编码同时相同。同时,要去掉旧的保险单号为null或者空字符串的数据,去掉新旧保险单相同的数据。3、 在所有旧保险单中找到和新保单结束日期在90天之内的,就是上年保单。优化思路1、 理解业务,采用更好的算法,而不是照搬存储过程。存储过程如果遇到了很难优化的性能问题,根本原因可能是采用的计算方法出了问题。这往往是因为SQL原理和模型造成的,要靠新的工具通过支持更好的计算方法来解决。如果用SPL简单翻译存储过程的语句,计算方法没有改变,性能也很难提升。推荐的做法是通过存储过程理解业务的需求,然后从原理层面思考更快的算法,在工具层面采用集算器SPL提供的更优化的算法重新实现。乾学院提供了很多性能优化的案例,可以帮助SPL程序员快速找到更好的计算方法。2、 数据外置,利用集算器获得更好性能。集算器提供了私有数据文件格式,具备压缩、列存、有序等有利于性能的特点。因此可以将数据库中的数据预先缓存到集算器数据文件中,利用数据外置优化整体性能。3、 针对对关联计算,区别分类加以优化。和SQL的关联计算不同,集算器中能够对不同类型的join采用不同的算法,包括主键相同的同维表、外键表、主子表、大表关联小表等等细分情况。而如果出现了两个大表cross join的情况,则有必要重新分析业务需求。数据准备1、 从数据库中导出保单表和保单车辆明细表。按照policyid排序之后,存放到组表文件POLICY.ctx和POLICY_CAR.ctx中。这里的排序很重要,是后续实现有序归并的前提条件。因为数据库JDBC性能较差,所以第一次导出全部历史数据的时候速度会比较慢。但是以后每天导出新增数据,增量更新组表文件就很快了。2、 针对POLICY.ctx的enddate字段新建索引index_enddate。3、 数据准备的具体代码可以参考教程的组表部分。解决办法和过程经过分析、测试发现,原存储过程性能优化的关键在于四个关联计算。首先是新增保单和保单-车辆明细表通过policyid关联来获得车辆信息,之后再与保单-车辆明细表分别通过vinid、frameid、licenseid以及licensetype关联三次,来获取历史保单。一天的新增保单有1万多条,这四次关联的时间尚可忍受。而一个月的新增保单有四十多万条,这四次关联的时间就会达到1到2个小时。对此,我们优化这个存储过程的思路就是利用SPL的计算能力,在对两个主表一次遍历的过程中,完成上述四个关联计算。这样,无论是一天还是一个月的新增保单,计算时间都不会明显延长。具体的SPL代码分为两大部分:一、过滤出指定时间段(istart和iend之间)的新增保单数据。一天的新增保单1到2万条,三十天的新增保单30到60万条,这个量级的数据可以直接存放在内存中。具体代码如下:A1、B1:打开组表文件“保单表”。A2、A3:从保单表中过滤出指定时间段(istart和iend之间)的新增保单,过滤时使用了预先生成的索引index_enddate。A4、B4:打开组表文件“保单车辆明细表”。A5、B5:用新增保单号,关联保单车辆表,找出车辆信息。A6、A7:关联新增保单信息和车辆信息,生成新增保单和车辆信息表。二、对历史保单完成三种方式的关联计算,得到新旧保单对照表。A8、B8:打开两个组表文件,保单表和保单车辆明细表,用需要的字段建立游标。A9:用policyid关联两个游标。如前所述,两个表都已经按照policyid排序了,所以这里的joinx是采用有序归并的方式,两个表都只需要遍历一次,复杂度较低。而SQL的HASH计算性能则只能靠运气了。关于有序归并的介绍参见【数据蒋堂】第 35 期:JOIN 提速 – 有序归并。A10:循环取出两个组表文件关联的结果,每次取出10万条形成序表。B10:关联结果生成新序表。其中,牌照和牌照种类用“|”合并成一个字段。B11、C11、B12分别按照三种方式做内连接,计算历史保单。C12:纵向合并三个内连接的结果。B13、C13:找出新保单id不等于旧保单id并且旧保单号不为空的数据,生成新序表。B14:结果合并到B14中。A15:过滤出结束日期大于旧保单结束日期,“旧保单结束日期”和“新保单开始日期”的间隔不超过90天的数据。A16、17、B17:对新保单、旧保单去掉重复,存入结果文件。性能优化效果在实际项目中,存储过程和集算器对比测试数据如下:从结果可以看出,同样的条件下:1、新增保单数据量越大,集算器性能提升越明显。30天新增保单计算时,性能提升6.6倍。2、存储过程计算时间随着数据量线性增加。集算器计算时间并不会随着数据量线性增加。3、数据量较小的时候,集算器和存储过程的计算性能都在可接受范围内;数据量较大时,存储过程需要计算几个小时,集算器的计算时间仍在十几分钟。4、在这次测试中,没有对保单表和保单车辆明细表建索引,计算过程中集算器需要对两个表做遍历查找。因此,数据量较小时,集算器也需要一个基本的遍历计算时间。而数据库建有索引,在小数据量时会有优势。如果集算器也建有索引,这个场景也可以再优化。但由于目前的指标已经可以达到实用,而用户方更关心的是大数据量场景,所以没有再做进一步的优化测试。优势总结采用压缩列存集算器采用压缩列存的方式保存数据。保险单表有70个字段,参与计算的只有十几个字段;保险单明细表有56个字段,参与计算字段不到十个。因此,采用列存方式对性能的提高效果较好。保险单表和保险单明细表存成集算器组表文件,压缩后只有3G多,也可以有效提高计算速度。数据有序存放集算器数据文件中的数据按照保险单号有序存放。保险单表和保险单明细表按照保险单号关联的时候,可以有序分段关联,速度提升明显。计算中间结果也是有序的,无需再重新建索引,有效节约了原来存储过程中建索引的时间。采用更快的计算方法用新保单去找上年、上上年和上三年的保单,需要按照vin码、车架号或者牌照加牌照种类三种方式来判断是否同一辆车。原存储过程是用新保单表去和保单表、保单明细表多次关联,计算的时间会随着新保单表的数据量而线性增长。而在集算器中采取的方式是:保单表和保单明细表有序关联之后,循环分批取出(比如:每次取10万条)。在内存中,每一批数据都和新保单通过三种方式关联。循环结束,三种方式的关联也都完成了。这样就实现了大表遍历一遍,同时完成三种方式的关联计算。对于存储过程来说,无法实现这种算法的根本原因是:1、无法有序关联两个大表;2、用临时表不能保证全内存计算。提高开发效率SPL语言代码更短,调试更简单,可以有效提高开发效率。原存储过程的完整代码约1800多行,用SPL改写后,仅约500格SPL语句即可实现。 ...

January 14, 2019 · 1 min · jiezi

JavaScript 性能优化

更多文章加载与执行将<script>标签放在</body>前面,不要放在<head>中,防止造成堵塞尽量减少请求,单个100KB的文件比4个25KB的文件更快,也就是说减少页面中外链的文件会改善性能数据存取使用局部变量和字面量比使用数组和对象有更少的读写消耗尽可能使用局部变量代替全局变量如无必要,不要使用闭包;闭包引用着其他作用域的变量,会造成更大的内存开销原型链不要过深、对象嵌套不要太多对于多次访问的嵌套对象,应该用变量缓存起来DOM编程不要频繁修改DOM,因为修改DOM样式会导致重绘(repaint)和重排(reflow)如果要修改DOM的多个样式可以用cssText一次性将要改的样式写入,或将样式写到class里,再修改DOM的class名称const el = document.querySelector(’.myDiv’)el.style.borderLeft = ‘1px’el.style.borderRight = ‘2px’el.style.padding = ‘5px’可以使用如下语句代替const el = document.querySelector(’.myDiv’)el.style.cssText = ‘border-left: 1px; border-right: 2px; padding: 5px;‘cssText会覆盖已存在的样式,如果不想覆盖已有样式,可以这样el.style.cssText += ‘;border-left: 1px; border-right: 2px; padding: 5px;‘避免大量使用:hover使用事件委托<ul> <li>苹果</li> <li>香蕉</li> <li>凤梨</li></ul>// gooddocument.querySelector(‘ul’).onclick = (event) => { let target = event.target if (target.nodeName === ‘LI’) { console.log(target.innerHTML) }}// baddocument.querySelectorAll(’li’).forEach((e) => { e.onclick = function() { console.log(this.innerHTML) }}) 批量修改DOM当你需要批量修改DOM时,可以通过以下步骤减少重绘和重排次数:使元素脱离文档流对其应用多重改变把元素带回文档中该过程会触发两次重排——第一步和第三步。如果你忽略这两个步骤,那么在第二步所产生的任何修改都会触发一次重排。有三种方法可以使DOM脱离文档:隐藏元素,应用修改,重新显示使用文档片断(document.fragment)在当前DOM之外构建一个子树,再把它拷回文档将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素算法和流程控制改善性能最佳的方式是减少每次迭代的运算量和减少循环迭代次数JavaScript四种循环中for while do-while for-in,只有for-in循环比其他其中明显要慢,因为for-in循环要搜索原型属性限制循环中耗时操作的数量基于函数的迭代forEach比一般的循环要慢,如果对运行速度要求很严格,不要使用if-else switch,条件数量越大,越倾向于使用switch在判断条件多时,可以使用查找表来代替if-else switch,速度更快switch(value) { case 0: return result0 break case 1: return result1 break case 2: return result2 break case 3: return result3 break}// 可以使用查找表代替const results = [result0, result1, result2, result3]如果遇到栈溢出错误,可以使用迭代来代替递归字符串str += ‘one’ + ’two’此代码运行时,会经历四个步骤:在内存中创建一个临时字符串连接后的字符串 onetwo 被赋值给该临时字符串临时字符串与str当前的值连接结果赋值给strstr += ‘one’ str += ’two’第二种方式比第一种方式要更快,因为它避免了临时字符串的产生你也可以用一个语句就能达到同样的性能提升str = str + ‘one’ + ’two’快速响应用户界面对于执行时间过长的大段代码,可以使用setTimeout和setInterval来对代码进行分割,避免对页面造成堵塞对于数据处理工作可以交由Web Workers来处理,因为Web Workers不占用浏览器UI线程的时间编程实践使用Object/Array字面量const obj = new Object()const newObj = {}const arry = new Array()const newArry = []使用字面量会运行得更快,并且节省代码量位操作在JavaScript中性能非常快,可以使用位运算来代替纯数学操作x =* x// 用位运算代替 x <<= 1如无必要,不要重写原生方法,因为原生方法底层是用C/C++实现的,速度更快参考资料高性能JavaScript ...

January 10, 2019 · 1 min · jiezi

前端性能优化JavaScript篇

关于前端性能优化的讨论一直都很多,包罗的知识也很多,可以说性能优化只有更好,没有最好。前面我写了一篇关于css优化的总结文章,今天再从javascript方面聊一聊。1.从资源加载方面来说,浏览器的加载顺序是按源码从上到下加载解析的,遇到link,script等资源都会阻塞页面渲染,所以我们会把script放在</body>前面,我们还可以结合构建工具(webpack,gulp…)压缩js文件,抽离公共js、去掉空格、注释,尽可能地让js文件变小,防止脚本阻塞页面渲染。2.在写代码的时候我们还要注意以下问题。(1)减少作用域链上的查找次数。我们知道,js代码在执行的时候,如果需要访问一个变量或者一个函数的时候,它需要从当前执行环境的作用域链一级一级地向上查找,直到全局作用域。如果我们需要经常访问全局环境的变量对象的时候,我们每次都必须在当前作用域链上一级一级的遍历,这显然是比较耗时的。function getTitle() { var h1 = document.getElementByTagName(“h1”); for(var i = 0, len = h1.length; i < len; i++) { h1[i].innerHTML = document.title + " 测试 " + i; }}上面这样写就非常耗时,我们可以优化一下:function getTitle() { var doc = document; var h1 = doc.getElementByTagName(“h1”); for(var i = 0, len = h1.length; i < len; i++) { h1[i].innerHTML = doc.title + " 测试 " + i; }}通过创建一个指向document的局部变量,就可以通过限制一次全局查找来改进这个函数的性能。(2)闭包导致的内存泄露。闭包可以保证函数内的变量安全,可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中,不会被自动清除。使用场合:设计私有的方法和变量。使用不当就会造成内存占用过高。我们需要手动销毁内存中的变量。(3)尽量少用全局变量,尽量使用局部变量。全局变量如果不手动销毁,会一直存在,造成全局变量污染,可能很产生一些意想不到的错误,而局部变量运行完成后,就被会被回收;(4)使用classname代替大量的内联样式修改。很多时候我们会在用户操作的时候,页面元素样式会进行相应的变化,这时候我们就可以把要变化的样式写成一个class,操作class变化,就能实现大量样式的变化。(5)批量元素绑定事件,可以使用事件委托。事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。比如我们有100个li,每个li都要绑定click点击事件,就可以用事件委托。举一个例子:我们需要给所有的button绑定click事件<div id=“box”> <input type=“button” id=“add” value=“添加” /> <input type=“button” id=“remove” value=“删除” /> <input type=“button” id=“move” value=“移动” /> <input type=“button” id=“select” value=“选择” /> </div>我们有可能会这样写window.onload = function(){ var Add = document.getElementById(“add”); var Remove = document.getElementById(“remove”); var Move = document.getElementById(“move”); var Select = document.getElementById(“select”); Add.onclick = function(){ alert(‘添加’); }; Remove.onclick = function(){ alert(‘删除’); }; Move.onclick = function(){ alert(‘移动’); }; Select.onclick = function(){ alert(‘选择’); } }用事件委托就可以这样写:window.onload = function(){ var oBox = document.getElementById(“box”); oBox.onclick = function (ev) { var ev = ev || window.event; var target = ev.target || ev.srcElement; if(target.nodeName.toLocaleLowerCase() == ‘input’){ switch(target.id){ case ‘add’ : alert(‘添加’); break; case ‘remove’ : alert(‘删除’); break; case ‘move’ : alert(‘移动’); break; case ‘select’ : alert(‘选择’); break; } } } }而且使用事件委托,还有一个好处就是,当你添加一个新的button,一样的会绑定上click事件,这就是委托事件的好处。上面这样的写法是原生js的写法,jquery可以这样写:$("#box").on(“click”,“input”,function(event){ var targetId = $(this).attr(‘id’); switch(targetId){ case ‘add’ : alert(‘添加’); break; case ‘remove’ : alert(‘删除’); break; case ‘move’ : alert(‘移动’); break; case ‘select’ : alert(‘选择’); break; } })这样写就简便得多。(6)避免不必要的DOM操作,尽量使用 ID 选择器:ID选择器是最快的,避免一层层地去查找节点。(7)类型转换:把数字转换成字符串使用number + "" 。虽然看起来比较丑一点,但事实上这个效率是最高的,性能上来说:("" + ) > String() > .toString() > new String() (8)对字符串进行循环操作,譬如替换、查找,应使用正则表达式。因为本身JavaScript的循环速度就比较慢,而正则表达式的操作是用C写成的语言的API,性能很好。(9)对象查询使用[""]查询要比.items()更快。这和前面的减少对象查找的思路是一样的,调用.items()增加了一次查询和函数的调用。(10)浮点数转换成整型使用Math.floor()或者Math.round()。parseInt()是用于将字符串转换成数字,Math是内部对象,所以Math.floor()其实并没有多少查询方法和调用的时间,速度是最快的。关于js性能优化来说,涉及到很多方面,特别是现在又出现很多的前端框架,优化方法又各有不同。上面说的这些只是很浅显的东西,在开发中多注意一下就可以了,尽量精简自己的代码。性能优化还需要继续深入研究。 ...

January 10, 2019 · 2 min · jiezi

小程序性能优化总结

历史总结:小程序倒计时深究小程序实战踩坑之B2B商城项目总结初试小刀自我简历小程序启动加载优化在小程序启动时,微信会在背后完成几项工作:下载小程序代码包、加载小程序代码包、初始化小程序首页。初始化小程序环境是微信环境做的工作,我们只需要控制代码包大小,和通过一些相关的缓存策略控制,和资源控制,逻辑控制,分包加载控制来进行启动加载优化。勾选开发者工具中, 上传时压缩代码(若采用wepy高级版本,自带压缩,请按官网文档采取点击)精简代码,去掉不必要的WXML结构和未使用的WXSS定义。减少在代码包中直接嵌入的资源文件。(比如全国地区库,微信有自带的,在没必要的时候,勿自用自己的库)及时清理无用的资源(js文件、图片、demo页面等)压缩图片,使用适当的图片格式,减少本地图片数量等如果小程序比较复杂,优化后的代码总量可能仍然比较大,此时可以采用分包加载的方式进行优化,分包加载初始化时只加载首评相关、高频访问的资源,其他的按需加载。提前做异步请求,页面最好在onLoad时异步请求数据,不要在onReady时请求启用缓存数据策略,请求时先展示缓存内容,让页面尽快展示,请求到最新数据之后再刷新避免白屏,使用骨架屏等数据通信优化为了提升数据更新的性能,开发者在执行setData调用时,最好遵循以下原则:不要过于频繁调用setData,应考虑将多次setData合并成一次setData调用;数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示且数据结构比较复杂或包含长字符串,则不应使用setData来设置这些数据;与界面渲染无关的数据最好不要设置在data中,可以考虑设置在page对象的其他字段下。提升数据更新性能方式的代码示例:Page({ onShow: function() { // 不要频繁调用setData this.setData({ a: 1 }) this.setData({ b: 2 }) // 绝大多数时候可优化为 this.setData({ a: 1, b: 2 }) // 不要设置不在界面渲染时使用的数据,并将界面无关的数据放在data外 this.setData({ myData: { a: ‘这个字符串在WXML中用到了’, b: ‘这个字符串未在WXML中用到,而且它很长…………………………’ } }) // 可以优化为 this.setData({ ‘myData.a’: ‘这个字符串在WXML中用到了’ }) this._myData = { b: ‘这个字符串未在WXML中用到,而且它很长…………………………’ } }})事件通信优化视图层会接受用户事件,如点击事件、触摸事件等。当一个用户事件被触发且有相关的事件监听器需要被触发时,视图层会将信息反馈给逻辑层。这个反馈是异步的,会产生延迟,降低延迟的方法有两个:去掉不必要的事件绑定(WXML中的bind和catch),从而减少通信的数据量和次数;事件绑定时需要传输target和currentTarget的dataset,因而不要在节点的data前缀属性中放置过大的数据。渲染优化页面方法onPageScroll使用, 每次页面滚动都会触发,避免在里面写过于复杂的逻辑 ,特别是一些执行重渲染页面的逻辑(另外,可以看我的文章——移动端滚动研究,说明了在滚动的情况下导致的渲染性能低下的各种分析和应付方法总结)在进行视图重渲染的时候,会进行当前节点树与新节点树的比较,去掉不必要设置的数据、减少setData的数据量也有助于提升这一个步骤的性能。

January 9, 2019 · 1 min · jiezi

懒加载的3种实现方式

优势性能收益:浏览器加载图片、decode、渲染都需要耗费资源,懒加载能节约性能消耗,缩短onload事件时间。节约带宽:这个不需要解释。通常,我们在html中展示图片,会有两种方式:img 标签css background-imageimg的懒加载实现img有两种方式实现懒加载:事件监听(scroll、resize、orientationChange)<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>event</title> <style> img { background: #F1F1FA; width: 400px; height: 300px; display: block; margin: 10px auto; border: 0; } </style></head><body> <img src=“https://ik.imagekit.io/demo/img/image1.jpeg?tr=w-400,h-300" /> <img src=“https://ik.imagekit.io/demo/img/image2.jpeg?tr=w-400,h-300" /> <img src=“https://ik.imagekit.io/demo/img/image3.jpg?tr=w-400,h-300" /> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image2.jpeg?tr=w-400,h-300" /> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image3.jpg?tr=w-400,h-300" /> –> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image4.jpeg?tr=w-400,h-300" /> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image5.jpeg?tr=w-400,h-300" /> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image6.jpeg?tr=w-400,h-300" /> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image7.jpeg?tr=w-400,h-300" /> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image8.jpeg?tr=w-400,h-300" /> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image9.jpeg?tr=w-400,h-300" /> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image10.jpeg?tr=w-400,h-300" /> <script> document.addEventListener(“DOMContentLoaded”, function() { var lazyloadImages = document.querySelectorAll(“img.lazy”); var lazyloadThrottleTimeout; function lazyload () { if(lazyloadThrottleTimeout) { clearTimeout(lazyloadThrottleTimeout); } lazyloadThrottleTimeout = setTimeout(function() { var scrollTop = window.pageYOffset; lazyloadImages.forEach(function(img) { if(img.offsetTop < (window.innerHeight + scrollTop)) { img.src = img.dataset.src; img.classList.remove(’lazy’); } }); if(lazyloadImages.length == 0) { document.removeEventListener(“scroll”, lazyload); window.removeEventListener(“resize”, lazyload); window.removeEventListener(“orientationChange”, lazyload); } }, 20); } document.addEventListener(“scroll”, lazyload); window.addEventListener(“resize”, lazyload); window.addEventListener(“orientationChange”, lazyload); }); </script></body></html>Intersection Observer(兼容性问题)<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>observer</title> <style> img { background: #F1F1FA; width: 400px; height: 300px; display: block; margin: 10px auto; border: 0; } </style></head><body> <img src=“https://ik.imagekit.io/demo/img/image2.jpeg?tr=w-400,h-300" /> <img src=“https://ik.imagekit.io/demo/img/image3.jpg?tr=w-400,h-300" /> –> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image1.jpeg?tr=w-400,h-300" /> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image2.jpeg?tr=w-400,h-300" /> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image3.jpg?tr=w-400,h-300" /> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image4.jpeg?tr=w-400,h-300" /> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image5.jpeg?tr=w-400,h-300" /> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image6.jpeg?tr=w-400,h-300" /> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image7.jpeg?tr=w-400,h-300" /> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image8.jpeg?tr=w-400,h-300" /> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image9.jpeg?tr=w-400,h-300" /> <img class=“lazy” data-src=“https://ik.imagekit.io/demo/img/image10.jpeg?tr=w-400,h-300" /> <script> document.addEventListener(“DOMContentLoaded”, function() { var lazyloadImages = document.querySelectorAll(".lazy”); var imageObserver = new IntersectionObserver(function(entries, observer) { entries.forEach(function(entry) { if (entry.isIntersecting) { var image = entry.target; image.src = image.dataset.src; image.classList.remove(“lazy”); imageObserver.unobserve(image); } }); }); lazyloadImages.forEach(function(image) { imageObserver.observe(image); }); }); </script></body></html>background-image的实现background-image的实现跟img的原理基本是一样的,区别是在对class的处理上:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>background</title> <style> body { margin: 0; } .bg { height: 200px; } #bg-image.lazy { background-image: none; background-color: #F1F1FA; } #bg-image { background-image: url(“https://ik.imagekit.io/demo/img/image1.jpeg?tr=w-400,h-300"); background-size: 100%; } </style></head><body> <div id=“bg-image” class=“bg lazy”></div> <div id=“bg-image” class=“bg lazy”></div> <div id=“bg-image” class=“bg lazy”></div> <div id=“bg-image” class=“bg lazy”></div> <div id=“bg-image” class=“bg lazy”></div> <div id=“bg-image” class=“bg lazy”></div> <div id=“bg-image” class=“bg lazy”></div> <div id=“bg-image” class=“bg lazy”></div> <script> document.addEventListener(“DOMContentLoaded”, function() { var lazyloadImages = document.querySelectorAll(".lazy”); var imageObserver = new IntersectionObserver(function(entries, observer) { entries.forEach(function(entry) { if (entry.isIntersecting) { var image = entry.target; image.classList.remove(“lazy”); imageObserver.unobserve(image); } }); }); lazyloadImages.forEach(function(image) { imageObserver.observe(image); }); }); </script></body></html>渐进式懒加载渐进式懒加载,指的是存在降级处理,通常html形式如下:<a href=“full.jpg” class=“progressive replace”> <img src=“tiny.jpg” class=“preview” alt=“image” /></a>这样的代码会有2个好处:如果js执行失败,可以点击预览大小与实际图一致的占位data URI,避免reflow最终的代码如下:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>progressive</title> <style> a.progressive { position: relative; display: block; overflow: hidden; outline: none; } a.progressive:not(.replace) { cursor: default; } a.progressive img { display: block; width: 100%; max-width: none; height: auto; border: 0 none; } a.progressive img.preview { filter: blur(2vw); transform: scale(1.05); } a.progressive img.reveal { position: absolute; left: 0; top: 0; will-change: transform, opacity; animation: reveal 1s ease-out; } @keyframes reveal { 0% {transform: scale(1.05); opacity: 0;} 100% {transform: scale(1); opacity: 1;} } </style></head><body> <a href=“https://s3-us-west-2.amazonaws.com/s.cdpn.io/123941/nature5.jpg" data-srcset=“https://s3-us-west-2.amazonaws.com/s.cdpn.io/123941/nature5.jpg 800w, https://s3-us-west-2.amazonaws.com/s.cdpn.io/123941/nature5big.jpg 1600w” class=“progressive replace”> <img src=“data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD//gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2NjIpLCBxdWFsaXR5ID0gNzAK/9sAQwAKBwcIBwYKCAgICwoKCw4YEA4NDQ4dFRYRGCMfJSQiHyIhJis3LyYpNCkhIjBBMTQ5Oz4+PiUuRElDPEg3PT47/9sAQwEKCwsODQ4cEBAcOygiKDs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7/8AAEQgABQAUAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A5yC3e2S3gM7OEQHdgA57VTuLAPqUoaUk7yM7R2BPSiivS5VN8stjmk2ldGVM3lyshG7bxk0UUV4skk2jdN2P/9k=” class=“preview” alt=“palm trees” /> </a> <a href=“https://s3-us-west-2.amazonaws.com/s.cdpn.io/123941/nature2.jpg" class=“progressive replace”> <img src=“http://lorempixel.com/20/15/nature/2/" class=“preview” alt=“sunset” /> </a> <a href=“https://s3-us-west-2.amazonaws.com/s.cdpn.io/123941/nature3.jpg" class=“progressive replace”> <img src=“http://lorempixel.com/20/15/nature/3/" class=“preview” alt=“tide” /> </a> <a href=“https://s3-us-west-2.amazonaws.com/s.cdpn.io/123941/nature5.jpg" data-srcset=“https://s3-us-west-2.amazonaws.com/s.cdpn.io/123941/nature5.jpg 800w, https://s3-us-west-2.amazonaws.com/s.cdpn.io/123941/nature5big.jpg 1600w” class=“progressive replace”> <img src=“data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD//gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2NjIpLCBxdWFsaXR5ID0gNzAK/9sAQwAKBwcIBwYKCAgICwoKCw4YEA4NDQ4dFRYRGCMfJSQiHyIhJis3LyYpNCkhIjBBMTQ5Oz4+PiUuRElDPEg3PT47/9sAQwEKCwsODQ4cEBAcOygiKDs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7/8AAEQgABQAUAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A5yC3e2S3gM7OEQHdgA57VTuLAPqUoaUk7yM7R2BPSiivS5VN8stjmk2ldGVM3lyshG7bxk0UUV4skk2jdN2P/9k=” class=“preview” alt=“palm trees” /> </a> <a href=“https://s3-us-west-2.amazonaws.com/s.cdpn.io/123941/nature2.jpg" class=“progressive replace”> <img src=“http://lorempixel.com/20/15/nature/2/" class=“preview” alt=“sunset” /> </a> <a href=“https://s3-us-west-2.amazonaws.com/s.cdpn.io/123941/nature3.jpg" class=“progressive replace”> <img src=“http://lorempixel.com/20/15/nature/3/" class=“preview” alt=“tide” /> </a> <script> window.addEventListener(’load’, function() { var pItem = document.getElementsByClassName(‘progressive replace’), timer; window.addEventListener(‘scroll’, scroller, false); window.addEventListener(‘resize’, scroller, false); inView(); function scroller(e) { timer = timer || setTimeout(function() { timer = null; requestAnimationFrame(inView); }, 300); } function inView() { var scrollTop = window.pageYOffset; var innerHeight = window.innerHeight; var p = 0; while (p < pItem.length) { var offsetTop = pItem[p].offsetTop; if (offsetTop < (scrollTop + innerHeight)) { loadFullImage(pItem[p]); pItem[p].classList.remove(‘replace’); } else p++; } } function loadFullImage(item) { var img = new Image(); if (item.dataset) { img.srcset = item.dataset.srcset || ‘’; img.sizes = item.dataset.sizes || ‘’; } img.src = item.href; img.className = ‘reveal’; if (img.complete) addImg(); else img.onload = addImg; function addImg() { item.addEventListener(‘click’, function(e) { e.preventDefault(); }, false); item.appendChild(img).addEventListener(‘animationend’, function(e) { var pImg = item.querySelector(‘img.preview’); if (pImg) { e.target.alt = pImg.alt || ‘’; item.removeChild(pImg); e.target.classList.remove(‘reveal’); } }); } } }, false); </script></body></html>现成库推荐下面这个库,使用非常简单:https://www.npmjs.com/package/lozad ...

January 7, 2019 · 4 min · jiezi

从“雅虎军规”看性能优化

一直以来,性能优化是开发的重中之中,而提及 前端性能优化 ,大家应该都会想到 雅虎军规,本文会结合 “雅虎军规” 融入自己的了解知识,进行的总结和梳理。希望对大家无论是开发中还是面试中都能有所帮助!内容部分1.尽量减少HTTP请求数 80%的终端用户响应时间都花在了前端上,其中大部分时间都在下载页面上的各种组件:图片,样式表,脚本,Flash等等。减少组件数必然能够减少页面提交的HTTP请求数。这是让页面更快的关键。 减少页面组件数的一种方式是简化页面设计。但有没有一种方法可以在构建复杂的页面同时加快响应时间呢?嗯,确实有鱼和熊掌兼得的办法。 合并文件是通过把所有脚本放在一个文件中的方式来减少请求数的,当然,也可以合并所有的CSS。如果各个页面的脚本和样式不一样的话,合并文件就是一项比较麻烦的工作了,但把这个作为站点发布过程的一部分确实可以提高响应时间。 CSS Sprites是减少图片请求数量的首选方式。把背景图片都整合到一张图片中,然后用CSS的background-image和background-position属性来定位要显示的部分。 图像映射可以把多张图片合并成单张图片,总大小是一样的,但减少了请求数并加速了页面加载。图片映射只有在图像在页面中连续的时候才有用,比如导航条。给image map设置坐标的过程既无聊又容易出错,用image map来做导航也不容易,所以不推荐用这种方式。 行内图片(Base64编码)用data: URL模式来把图片嵌入页面。这样会增加HTML文件的大小,把行内图片放在(缓存的)样式表中是个好办法,而且成功避免了页面变“重”。但目前主流浏览器并不能很好地支持行内图片。 减少页面的HTTP请求数是个起点,这是提升站点首次访问速度的重要指导原则。2.减少DNS查找 域名系统建立了主机名和IP地址间的映射,就像电话簿上人名和号码的映射一样。当你在浏览器输入www.yahoo.com的时候,浏览器就会联系DNS解析器返回服务器的IP地址。DNS是有成本的,它需要20到120毫秒去查找给定主机名的IP地址。在DNS查找完成之前,浏览器无法从主机名下载任何东西。 DNS查找被缓存起来更高效,由用户的ISP(网络服务提供商)或者本地网络存在一个特殊的缓存服务器上,但还可以缓存在个人用户的计算机上。DNS信息被保存在操作系统的DNS cache(微软Windows上的”DNS客户端服务”)里。大多数浏览器有独立于操作系统的自己的cache。只要浏览器在自己的cache里还保留着这条记录,它就不会向操作系统查询DNS。 IE默认缓存DNS查找30分钟,写在DnsCacheTimeout注册表设置中。Firefox缓存1分钟,可以用network.dnsCacheExpiration配置项设置。(Fasterfox把缓存时间改成了1小时 P.S. Fasterfox是FF的一个提速插件) 如果客户端的DNS cache是空的(包括浏览器的和操作系统的),DNS查找数等于页面上不同的主机名数,包括页面URL,图片,脚本文件,样式表,Flash对象等等组件中的主机名,减少不同的主机名就可以减少DNS查找。 减少不同主机名的数量同时也减少了页面能够并行下载的组件数量,避免DNS查找削减了响应时间,而减少并行下载数量却增加了响应时间。我的原则是把组件分散在2到4个主机名下,这是同时减少DNS查找和允许高并发下载的折中方案。3.避免重定向重定向用301和302状态码,下面是一个有301状态码的HTTP头:HTTP/1.1 301 Moved PermanentlyLocation: http://example.com/newuriContent-Type: text/html 浏览器会自动跳转到Location域指明的URL。重定向需要的所有信息都在HTTP头部,而响应体一般是空的。其实额外的HTTP头,比如Expires和Cache-Control也表示重定向。除此之外还有别的跳转方式:refresh元标签和JavaScript,但如果你必须得做重定向,最好用标准的3xxHTTP状态码,主要是为了让返回按钮能正常使用。 牢记重定向会拖慢用户体验,在用户和HTML文档之间插入重定向会延迟页面上的所有东西,页面无法渲染,组件也无法开始下载,直到HTML文档被送达浏览器。 有一种常见的极其浪费资源的重定向,而且web开发人员一般都意识不到这一点,就是URL尾部缺少一个斜线的时候。例如,跳转到http://astrology.yahoo.com/as…://astrology.yahoo.com/astrology/的301响应(注意添在尾部的斜线)。在Apache中可以用Alias,mod_rewrite或者DirectorySlash指令来取消不必要的重定向。 重定向最常见的用途是把旧站点连接到新的站点,还可以连接同一站点的不同部分,针对用户的不同情况(浏览器类型,用户帐号类型等等)做一些处理。用重定向来连接两个网站是最简单的,只需要少量的额外代码。虽然在这些时候使用重定向减少了开发人员的开发复杂度,但降低了用户体验。一种替代方案是用Alias和mod_rewrite,前提是两个代码路径都在相同的服务器上。如果是因为域名变化而使用了重定向,就可以创建一条CNAME(创建一个指向另一个域名的DNS记录作为别名)结合Alias或者mod_rewrite指令。4.让Ajax可缓存 Ajax的一个好处是可以给用户提供即时反馈,因为它能够从后台服务器异步请求信息。然而,用了Ajax就无法保证用户在等待异步JavaScript和XML响应返回期间不会非常无聊。在很多应用程序中,用户能够一直等待取决于如何使用Ajax。例如,在基于web的电子邮件客户端中,用户为了寻找符合他们搜索标准的邮件消息,将会保持对Ajax请求返回结果的关注。重要的是,要记得“异步”并不意味着“即时”。 要提高性能,优化这些Ajax响应至关重要。最重要的提高Ajax性能的方法就是让响应变得可缓存,就像在添上Expires或者Cache-Control HTTP头中讨论的一样。下面适用于Ajax的其它规则:Gzip组件减少DNS查找压缩JavaScript避免重定向配置ETags 我们一起看看例子,一个Web 2.0的电子邮件客户端用了Ajax来下载用户的通讯录,以便实现自动完成功能。如果用户从上一次使用之后再没有修改过她的通讯录,而且Ajax响应是可缓存的,有尚未过期的Expires或者Cache-Control HTTP头,那么之前的通讯录就可以从缓存中读出。必须通知浏览器,应该继续使用之前缓存的通讯录响应,还是去请求一个新的。可以通过给通讯录的Ajax URL里添加一个表明用户通讯录最后修改时间的时间戳来实现,例如&t=1190241612。如果通讯录从上一次下载之后再没有被修改过,时间戳不变,通讯录就将从浏览器缓存中直接读出,从而避免一次额外的HTTP往返消耗。如果用户已经修改了通讯录,时间戳也可以确保新的URL不会匹配缓存的响应,浏览器将请求新的通讯录条目。 即使Ajax响应是动态创建的,而且可能只适用于单用户,它们也可以被缓存,而这样会让你的Web 2.0应用更快。5.延迟加载组件 可以凑近看看页面并问自己:什么才是一开始渲染页面所必须的?其余内容都可以等会儿。 JavaScript是分隔onload事件之前和之后的一个理想选择。例如,如果有JavaScript代码和支持拖放以及动画的库,这些都可以先等会儿,因为拖放元素是在页面最初渲染之后的。其它可以延迟加载的部分包括隐藏内容(在某个交互动作之后才出现的内容)和折叠的图片。 工具可帮你减轻工作量:YUI Image Loader可以延迟加载折叠的图片,还有YUI Get utility是一种引入JS和CSS的简单方法。Yahoo!主页就是一个例子,可以打开Firebug的网络面板仔细看看。 最好让性能目标符合其它web开发最佳实践,比如“渐进增强”。如果客户端支持JavaScript,可以提高用户体验,但必须确保页面在不支持JavaScript时也能正常工作。所以,在确定页面运行正常之后,可以用一些延迟加载脚本增强它,以支持一些拖放和动画之类的华丽效果。6.预加载组件 预加载可能看起来和延迟加载是相反的,但它其实有不同的目标。通过预加载组件可以充分利用浏览器空闲的时间来请求将来会用到的组件(图片,样式和脚本)。用户访问下一页的时候,大部分组件都已经在缓存里了,所以在用户看来页面会加载得更快。实际应用中有以下几种预加载的类型:无条件预加载——尽快开始加载,获取一些额外的组件。google.com就是一个sprite图片预加载的好例子,这个sprite图片并不是google.com主页需要的,而是搜索结果页面上的内容。条件性预加载——根据用户操作猜测用户将要跳转到哪里并据此预加载。在search.yahoo.com的输入框里键入内容后,可以看到那些额外组件是怎样请求加载的。提前预加载——在推出新设计之前预加载。经常在重新设计之后会听到:“这个新网站不错,但比以前更慢了”,一部分原因是用户访问先前的页面都是有旧缓存的,但新的却是一种空缓存状态下的体验。可以通过在将要推出新设计之前预加载一些组件来减轻这种负面影响,老站可以利用浏览器空闲的时间来请求那些新站需要的图片和脚本。7.减少DOM元素的数量 一个复杂的页面意味着要下载更多的字节,而且用JavaScript访问DOM也会更慢。举个例子,想要添加一个事件处理器的时候,循环遍历页面上的500个DOM元素和5000个DOM元素是有区别的。 大量的DOM元素是一种征兆——页面上有些内容无关的标记需要清理。正在用嵌套表格来布局吗?还是为了修复布局问题而添了一堆的<div>s?或许应该用更好的语义化标记。YUI CSS utilities对布局有很大帮助:grids.css针对整体布局,fonts.css和reset.css可以用来去除浏览器的默认格式。这是个开始清理和思考标记的好机会,例如只在语义上有意义的时候使用<div>,而不是因为它能够渲染一个新行。 DOM元素的数量很容易测试,只需要在Firebug的控制台里输入:document.getElementsByTagName(’’).length 那么多少DOM元素才算是太多呢?可以参考其它类似的标记良好的页面,例如Yahoo!主页是一个相当繁忙的页面,但只有不到700个元素(HTML标签)。8.跨域分离组件 分离组件可以最大化并行下载,但要确保只用不超过2-4个域,因为存在DNS查找的代价。例如,可以把HTML和动态内容部署在www.example.org,而把静态组件分离到static1.example.org和static2.example.org。9.尽量少用iframe 用iframe可以把一个HTML文档插入到父文档里,重要的是明白iframe是如何工作的并高效地使用它。<iframe>的优点:引入缓慢的第三方内容,比如标志和广告安全沙箱并行下载脚本<iframe>的缺点:代价高昂,即使是空白的iframe阻塞页面加载非语义10.杜绝404 HTTP请求代价高昂,完全没有必要用一个HTTP请求去获取一个无用的响应(比如404 Not Found),只会拖慢用户体验而没有任何好处。 有些站点用的是有帮助的404——“你的意思是xxx?”,这样做有利于用户体验,,但也浪费了服务器资源(比如数据库等等)。最糟糕的是链接到的外部JavaScript有错误而且结果是404。首先,这种下载将阻塞并行下载。其次,浏览器会试图解析404响应体,因为它是JavaScript代码,需要找出其中可用的部分。css部分11.避免使用CSS表达式用CSS表达式动态设置CSS属性,是一种强大又危险的方式。从IE5开始支持,但从IE8起就不推荐使用了。例如,可以用CSS表达式把背景颜色设置成按小时交替的:background-color: expression( (new Date()).getHours()%2 ? “#B8D4FF” : “#F08A00” );12.选择<link>舍弃@import 前面提到了一个最佳实践:为了实现逐步渲染,CSS应该放在顶部。在IE中用@import与在底部用<link>效果一样,所以最好不要用它。13.避免使用滤镜 IE专有的AlphaImageLoader滤镜可以用来修复IE7之前的版本中半透明PNG图片的问题。在图片加载过程中,这个滤镜会阻塞渲染,卡住浏览器,还会增加内存消耗而且是被应用到每个元素的,而不是每个图片,所以会存在一大堆问题。最好的方法是干脆不要用AlphaImageLoader,而优雅地降级到用在IE中支持性很好的PNG8图片来代替。如果非要用AlphaImageLoader,应该用下划线hack:_filter来避免影响IE7及更高版本的用户。14.把样式表放在顶部 在Yahoo!研究性能的时候,我们发现把样式表放到文档的HEAD部分能让页面看起来加载地更快。这是因为把样式表放在head里能让页面逐步渲染。 关注性能的前端工程师想让页面逐步渲染。也就是说,我们想让浏览器尽快显示已有内容,这在页面上有一大堆内容或者用户网速很慢时显得尤为重要。给用户显示反馈(比如进度指标)的重要性已经被广泛研究过,并且被记录下来了。在我们的例子中,HTML页面就是进度指标!当浏览器逐渐加载页面头部,导航条,顶部logo等等内容的时候,这些都被正在等待页面加载的用户当作反馈,能够提高整体用户体验。js部分15.去除重复脚本 页面含有重复的脚本文件会影响性能,这可能和你想象的不一样。在对美国前10大web站点的评审中,发现只有2个站点含有重复脚本。两个主要原因增加了在单一页面中出现重复脚本的几率:团队大小和脚本数量。在这种情况下,重复脚本会创建不必要的HTTP请求,执行无用的JavaScript代码,而影响页面性能。 IE会产生不必要的HTTP请求,而Firefox不会。在IE中,如果一个不可缓存的外部脚本被页面引入了两次,它会在页面加载时产生两个HTTP请求。即使脚本是可缓存的,在用户重新加载页面时也会产生额外的HTTP请求。 除了产生没有意义的HTTP请求之外,多次对脚本求值也会浪费时间。因为无论脚本是否可缓存,在Firefox和IE中都会执行冗余的JavaScript代码。 避免不小心把相同脚本引入两次的一种方法就是在模版系统中实现脚本管理模块。典型的脚本引入方法就是在HTML页面中用SCRIPT标签:<script type=“text/javascript” src=“menu_1.0.17.js”></script>16.尽量减少DOM访问用JavaScript访问DOM元素是很慢的,所以,为了让页面反应更迅速,应该:缓存已访问过的元素的索引先“离线”更新节点,再把它们添到DOM树上避免用JavaScript修复布局问题17.用智能的事件处理器 有时候感觉页面反映不够灵敏,是因为有太多频繁执行的事件处理器被添加到了DOM树的不同元素上,这就是推荐使用事件委托的原因。如果一个div里面有10个按钮,应该只给div容器添加一个事件处理器,而不是给每个按钮都添加一个。事件能够冒泡,所以可以捕获事件并得知哪个按钮是事件源。18.把脚本放在底部 脚本会阻塞并行下载,HTTP/1.1官方文档建议浏览器每个主机名下并行下载的组件数不要超过两个,如果图片来自多个主机名,并行下载的数量就可以超过两个。如果脚本正在下载,浏览器就不开始任何其它下载任务,即使是在不同主机名下的。 有时候,并不容易把脚本移动到底部。举个例子,如果脚本是用document.write插入到页面内容中的,就没办法再往下移了。还可能存在作用域问题,在多数情况下,这些问题都是可以解决的。 一个常见的建议是用推迟(deferred)脚本,有DEFER属性的脚本意味着不能含有document.write,并且提示浏览器告诉他们可以继续渲染。不幸的是,Firefox不支持DEFER属性。在IE中,脚本可能被推迟,但不尽如人意。如果脚本可以推迟,我们就可以把它放到页面底部,页面就可以更快地载入。javascript, css19.把JavaScript和CSS放到外面 很多性能原则都是关于如何管理外部组件的,然而,在这些顾虑出现之前你应该问一个更基础的问题:应该把JavaScript和CSS放到外部文件中还是直接写在页面里?实际上,用外部文件可以让页面更快,因为JavaScript和CSS文件会被缓存在浏览器。HTML文档中的行内JavaScript和CSS在每次请求该HTML文档的时候都会重新下载。这样做减少了所需的HTTP请求数,但增加了HTML文档的大小。另一方面,如果JavaScript和CSS在外部文件中,并且已经被浏览器缓存起来了,那么我们就成功地把HTML文档变小了,而且还没有增加HTTP请求数。 20.压缩JavaScript和CSS 压缩具体来说就是从代码中去除不必要的字符以减少大小,从而提升加载速度。代码最小化就是去掉所有注释和不必要的空白字符(空格,换行和tab)。在JavaScript中这样做能够提高响应性能,因为要下载的文件变小了。两个最常用的JavaScript代码压缩工具是JSMin和YUI Compressor,YUI compressor还可以压缩CSS。 混淆是一种可选的源码优化措施,要比压缩更复杂,所以混淆过程也更容易产生bug。在对美国前十的网站调查中,压缩可以缩小21%,而混淆能缩小25%。虽然混淆的缩小程度更高,但比压缩风险更大。 除了压缩外部脚本和样式,行内的<script>和<style>块也可以压缩。即使启用了gzip模块,先进行压缩也能够缩小5%或者更多的大小。JavaScript和CSS的用处越来越多,所以压缩代码会有不错的效果。图片21.优化图片尝试把GIF格式转换成PNG格式,看看是否节省空间。在所有的PNG图片上运行pngcrush(或者其它PNG优化工具)22.优化CSS Sprite在Sprite图片中横向排列一般都比纵向排列的最终文件小组合Sprite图片中的相似颜色可以保持低色数,最理想的是256色以下PNG8格式“对移动端友好”,不要在Sprite图片中留下太大的空隙。虽然不会在很大程度上影响图片文件的大小,但这样做可以节省用户代理把图片解压成像素映射时消耗的内存。100×100的图片是1万个像素,而1000×1000的图片就是100万个像素了。23.不要用HTML缩放图片 不要因为在HTML中可以设置宽高而使用本不需要的大图。如果需要<img width=“100” height=“100” src=“mycat.jpg” alt=“My Cat” /> 那么图片本身(mycat.jpg)应该是100x100px的,而不是去缩小500x500px的图片。24.用小的可缓存的favicon.ico(P.S. 收藏夹图标) favicon.ico是放在服务器根目录的图片,它会带来一堆麻烦,因为即便你不管它,浏览器也会自动请求它,所以最好不要给一个404 Not Found响应。而且只要在同一个服务器上,每次请求它时都会发送cookie,此外这个图片还会干扰下载顺序,例如在IE中,当你在onload中请求额外组件时,将会先下载favicon。所以为了缓解favicon.ico的缺点,应该确保:足够小,最好在1K以下设置合适的有效期HTTP头(以后如果想换的话就不能重命名了),把有效期设置为几个月后一般比较安全,可以通过检查当前favicon.ico的最后修改日期来确保变更能让浏览器知道。cookie25.给Cookie减肥 使用cookie的原因有很多,比如授权和个性化。HTTP头中cookie信息在web服务器和浏览器之间交换。重要的是保证cookie尽可能的小,以最小化对用户响应时间的影响。清除不必要的cookie保证cookie尽可能小,以最小化对用户响应时间的影响注意给cookie设置合适的域级别,以免影响其它子域设置合适的有效期,更早的有效期或者none可以更快的删除cookie,提高用户响应时间26.把组件放在不含cookie的域下 当浏览器发送对静态图像的请求时,cookie也会一起发送,而服务器根本不需要这些cookie。所以它们只会造成没有意义的网络通信量,应该确保对静态组件的请求不含cookie。可以创建一个子域,把所有的静态组件都部署在那儿。 如果域名是www.example.org,可以把静态组件部署到static.example.org。然而,如果已经在顶级域example.org或者www.example.org设置了cookie,那么所有对static.example.org的请求都会含有这些cookie。这时候可以再买一个新域名,把所有的静态组件部署上去,并保持这个新域名不含cookie。Yahoo!用的是yimg.com,YouTube是ytimg.com,Amazon是images-amazon.com等等。 把静态组件部署在不含cookie的域下还有一个好处是有些代理可能会拒绝缓存带cookie的组件。有一点需要注意:如果不知道应该用example.org还是www.example.org作为主页,可以考虑一下cookie的影响。省略www的话,就只能把cookie写到.example.org,所以因为性能原因最好用www子域,并且把cookie写到这个子域下。移动端27.保证所有组件都小于25K 这个限制是因为iPhone不能缓存大于25K的组件,注意这里指的是未压缩的大小。这就是为什么缩减内容本身也很重要,因为单纯的gzip可能不够。28.把组件打包到一个复合文档里 把各个组件打包成一个像有附件的电子邮件一样的复合文档里,可以用一个HTTP请求获取多个组件(记住一点:HTTP请求是代价高昂的)。用这种方式的时候,要先检查用户代理是否支持(iPhone就不支持)。服务器29.Gzip组件 前端工程师可以想办法明显地缩短通过网络传输HTTP请求和响应的时间。毫无疑问,终端用户的带宽速度,网络服务商,对等交换点的距离等等,都是开发团队所无法控制的。但还有别的能够影响响应时间的因素,压缩可以通过减少HTTP响应的大小来缩短响应时间。从HTTP/1.1开始,web客户端就有了支持压缩的Accept-Encoding HTTP请求头。Accept-Encoding: gzip, deflate 如果web服务器看到这个请求头,它就会用客户端列出的一种方式来压缩响应。web服务器通过Content-Encoding相应头来通知客户端。Content-Encoding: gzip 尽可能多地用gzip压缩能够给页面减肥,这也是提升用户体验最简单的方法。30.避免图片src属性为空Image with empty string src属性是空字符串的图片很常见,主要以两种形式出现:1.straight HTML<img src=””>2.JavaScriptvar img = new Image();img.src = “”;这两种形式都会引起相同的问题:浏览器会向服务器发送另一个请求。 31.配置ETags 实体标签(ETags),是服务器和浏览器用来决定浏览器缓存中组件与源服务器中的组件是否匹配的一种机制(“实体”也就是组件:图片,脚本,样式表等等)。添加ETags可以提供一种实体验证机制,比最后修改日期更加灵活。一个ETag是一个字符串,作为一个组件某一具体版本的唯一标识符。唯一的格式约束是字符串必须用引号括起来,源服务器用相应头中的ETag来指定组件的ETag:HTTP/1.1 200 OK Last-Modified: Tue, 12 Dec 2006 03:03:59 GMT ETag: “10c24bc-4ab-457e1c1f” Content-Length: 12195 然后,如果浏览器必须验证一个组件,它用If-None-Match请求头来把ETag传回源服务器。如果ETags匹配成功,会返回一个304状态码,这样就减少了12195个字节的响应体。GET /i/yahoo.gif HTTP/1.1 Host: us.yimg.com If-Modified-Since: Tue, 12 Dec 2006 03:03:59 GMT If-None-Match: “10c24bc-4ab-457e1c1f” HTTP/1.1 304 Not Modified32.对Ajax用GET请求 Yahoo!邮箱团队发现使用XMLHttpRequest时,浏览器的POST请求是通过一个两步的过程来实现的:先发送HTTP头,在发送数据。所以最好用GET请求,它只需要发送一个TCP报文(除非cookie特别多)。IE的URL长度最大值是2K,所以如果要发送的数据超过2K就无法使用GET了。POST请求的一个有趣的副作用是实际上没有发送任何数据,就像GET请求一样。正如HTTP说明文档中描述的,GET请求是用来检索信息的。所以它的语义只是用GET请求来请求数据,而不是用来发送需要存储到服务器的数据。33.尽早清空缓冲区 当用户请求一个页面时,服务器需要用大约200到500毫秒来组装HTML页面,在这期间,浏览器闲等着数据到达。PHP中有一个flush()函数,允许给浏览器发送一部分已经准备完毕的HTML响应,以便浏览器可以在后台准备剩余部分的同时开始获取组件,好处主要体现在很忙的后台或者很“轻”的前端页面上(P.S. 也就是说,响应时耗主要在后台方面时最能体现优势)。 较理想的清空缓冲区的位置是HEAD后面,因为HTML的HEAD部分通常更容易生成,并且允许引入任何CSS和JavaScript文件,这样就可以让浏览器在后台还在处理的时候就开始并行获取组件。例如: … <!– css, js –> </head> <?php flush(); ?> <body> … <!– content –>34.使用CDN(内容分发网络) 用户与服务器的物理距离对响应时间也有影响。把内容部署在多个地理位置分散的服务器上能让用户更快地载入页面。但具体要怎么做呢? 实现内容在地理位置上分散的第一步是:不要尝试去重新设计你的web应用程序来适应分布式结构。这取决于应用程序,改变结构可能包括一些让人望而生畏的任务,比如同步会话状态和跨服务器复制数据库事务(翻译可能不准确)。缩短用户和内容之间距离的提议可能被推迟,或者根本不可能通过,就是因为这个难题。 记住终端用户80%到90%的响应时间都花在下载页面组件上了:图片,样式,脚本,Flash等等,这是业绩黄金法则。最好先分散静态内容,而不是一开始就重新设计应用程序结构。这不仅能够大大减少响应时间,还更容易表现出CDN的功劳。 内容分发网络(CDN)是一组分散在不同地理位置的web服务器,用来给用户更高效地发送内容。典型地,选择用来发送内容的服务器是基于网络距离的衡量标准的。例如:选跳数(hop)最少的或者响应时间最快的服务器。35.添上Expires或者Cache-Control HTTP头这条规则有两个方面:对于静态组件:通过设置一个遥远的将来时间作为Expires来实现永不失效多余动态组件:用合适的Cache-ControlHTTP头来让浏览器进行条件性的请求网页设计越来越丰富,这意味着页面里有更多的脚本,图片和Flash。站点的新访客可能还是不得不提交几个HTTP请求,但通过使用有效期能让组件变得可缓存,这避免了在接下来的浏览过程中不必要的HTTP请求。有效期HTTP头通常被用在图片上,但它们应该用在所有组件上,包括脚本、样式和Flash组件。 浏览器(和代理)用缓存来减少HTTP请求的数目和大小,让页面能够更快加载。web服务器通过有效期HTTP响应头来告诉客户端,页面的各个组件应该被缓存多久。用一个遥远的将来时间做有效期,告诉浏览器这个响应在2010年4月15日前不会改变。Expires: Thu, 15 Apr 2010 20:00:00 GMT如果你用的是Apache服务器,用ExpiresDefault指令来设置相对于当前日期的有效期。下面的例子设置了从请求时间起10年的有效期:ExpiresDefault “access plus 10 years"总结:前端的性能优化一直是很重要的,如果你有什么意见和见解,欢迎留言。参考:雅虎军规 ...

January 6, 2019 · 1 min · jiezi

使用jMeter构造逻辑上有依赖关系的一系列并发请求

相信前端开发工程师对CSRF(Cross-site request forgery)跨站请求伪造这个概念都非常熟悉,有的时候也简写成XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装成受信任用户的请求来利用受信任的网站。CSRF攻击的防御方式有多种,最简单最易实现的一种思路就是在客户端向服务器发起的请求中放入攻击者无法伪造的信息,并且该信息没有存储于 cookie 之中。技术上来说,当客户端向服务器发起请求执行一些敏感操作之前(比如用HTTP post实现的转账,扣款等功能),服务器端随机产生一个token,返回给客户端。客户端接下来的操作,必须在HTTP请求中以参数的形式把这个服务器端颁发的token带上。同时服务器端在实现给客户端分配token的同时,也要加入一个token校验机制。如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。这个token我们一般称为CSRF token。讲了这么多,是为了引入本文想要讨论的话题。假设我想用jMeter测试一个OOdata服务创建Service Ticket的性能。因为创建功能不像读操作,执行之后会对系统产生持久化影响(Persistence side-effect), 因此服务器端的实现加入了CSRF token的校验。这就是说,如果我们直接用jMeter构造并发的HTTP post请求,是没有办法完成测试的,这些请求因为没有包含CSRF token,会被服务器端直接拒绝掉。根据前面描述的CSRF攻防原理,CSRF token是服务器端随机生成的,客户端无法用任何技术进行伪造,因为为了测试接口HTTP post操作进行Service Ticket的创建,我们必须构造一个它的前置HTTP GET请求,专门用于得到服务器返回的CSRF token,然后再构造真正用于性能测试的HTTP POST请求,把第一步GET请求获得的CSRF token附到POST请求的头部中去。本文介绍在jMeter里如何维护并配置这种具有依赖关系的一组请求。当然如果您不喜欢用jMeter,想自己写代码实现,也是可以的。可以参考我放在github上的Java代码实现。用jMeter的好处是不需要编程,通过简单的配置就能实现这个性能测试需求,一般没有开发背景的测试人员也能独立完成。First let us have a look how JMeter could archive the same without even one line of programming.My project in JMeter is displayed with the following hierarchy. I have configured with “Number of 5 threads” in my thread group, so once executed, the response time of these 5 threads are displayed in result table together with average response time.从下图能看出,因为拿CSRF token的HTTP GET在逻辑上必须先于实际需要测试性能的HTTP POST请求,这实际上构成了一个Transaction-事务,所以我使用jMeter里提供的Transaction Controller来管理。Some key points for this JMeter project creation(1) Since now one thread should cover both XSRF token fetch via HTTP get and Service request creation via HTTP post, so a transaction controller is necessary to include both request.(2) Create the first HTTP request to fetch XSRF token. The setting could be found below: adding a http header field with name asx-csrf-token and value as “fetch”:在HTTP GET请求的头部加上一个名为x-csrf-token的字段,值赋成fetch。这样服务器接到这个请求,就知道这是客户端发起的CSRF token请求,于是服务器响应这个请求,把创建好的随机CSRF token通过HTTP response头部字段的方式返回给客户端。下一个问题就是,服务器返回给客户端合法的CSRF token后,jMeter如何读取到这个token,并用于接下来的请求?幸运的是,jMeter提供了正则表达式提取式,可以让我们很方便地从HTTP响应结构中提取出token来。Create a Regular Expression Extractor to parse the XSRF token from response header and stored it to a variable named “jerrycsrftoken”.下图构造了一个jMeter正则表达式提取器,工作于HTTP响应的头部字段,解析出的token值存储于变量jerrycsrftoken中。Before you continue, please make sure that the XSRF token is correctly parsed from request header, which could be confirmed by printing it out in a debug sample:这个请求构造完之后,我们先试着运行一次,确保在变量jerrycsrftoken里确实看到解析好的CSRF token。(3) Create another HTTP request with type POST.这时万事俱备,我们可以开始构造真正要进行性能测试的HTTP post,即Service Ticket的创建请求了。请求的报文正文:Just paste the following text to the tab “Body Data”:–batch_1Content-Type: multipart/mixed; boundary=changeset_1–changeset_1Content-Type: application/httpContent-Transfer-Encoding: binaryPOST ServiceRequestCollection HTTP/1.1Content-Length: 5000Accept: application/jsonContent-Type: application/json{“ServicePriorityCode”: “2”,“Name”: {“content”: “Jerry Testing ticket creation via JMeter ${uuid} “},“ServiceRequestDescription”: [{“Text”: “Piston Rattling 1 - Generic OData Test Create”,“TypeCode”: “10004”},{“Text”: “Piston Rattling 2 - Generic OData Test Create”,“TypeCode”: “10007”}]}–changeset_1—-batch_1–In the body text I use a user-defined variable ${uuid} which we could create it in last step. And for this post request, use the XSRF token fetched from previous HTTP get request.前面说过,POST请求的头部需要加上合法的CSRF token,此处我们使用前面GET请求已经拿到的并且存储于变量jerrycsrftoken中的token值:我希望最后通过并发测试生成的Service Ticket的描述信息的后缀是1到100的随机正整数,因此我使用jMeter里自带的一个随机数发生器:(4) As the last step, create a user variable by using JMeter built-in function __Random, to create a random number between 1 ~ 100 as a fragment of created Service Request description.Now execute the Thread group, and the execution detail for these three HTTP request could be reviewed separately in tree view:试着运行一下,发现这个POST操作确实按照我们期望的那样,在HTTP头部字段里加上了正确合法的CSRF token:For example, the XSRF token is successfully fetched in the first request: rdPy7zNj_uKDYvQLgfQCFA==And used as one header field in second HTTP Post request as expected:And finally in UI we could find the created Service request with random number between 1 ~ 100 as postfix:在UI上观测到我构造的5个并发请求创建的Service Ticket,说明CSRF token在服务器端的校验成功,同时发现描述信息都带上了随机数,说明我的jMeter随机数生成器的用法也正确。希望本文对大家的工作有所帮助。要获取更多Jerry的原创文章,请关注公众号"汪子熙”: ...

January 1, 2019 · 2 min · jiezi

10 行代码解决漏斗转换计算之性能优化

【摘要】庖丁解牛,给人的深刻印象是技艺酷炫!然而酷炫并非是庖丁的原意追求。本质上是对一个复杂的结构进行大量练习后,把细节融入了自己的身体,成为一种本能;流畅自然的动作给观赏者造成酷炫的感受,是一个副产品。数据处理的描述计算、性能优化也是类似的事情。成为数据界的庖丁同样需要两个必要条件:1、大量待解的牛 (复杂的需求和运行环境); 2、专业的解牛刀具 ( 是集算器的 SPL 语言吗?!)。让我们一起去乾学院看个究竟吧:10 行代码解决漏斗转换计算之性能优化!大话数据计算性能优化大数据分析的性能优化,说道底,就优化一个事情:针对确定的一个计算任务(数据确定,结果确定),以最经济的方案得到结果。这个最经济的方案主要考量三个成本:时间成本、硬件成本、软件成本。时间成本:根据计算任务的特点,能容忍的最长时间各不相同。那些 T+0 的计算任务,实时性要求就比较高,T+1 再算出结果就失去了意义。硬件成本:可以使用的硬件资源,对一个公司来说一般不是经常变化的,机器配置、可集群数量就那么多。即便使用云计算产品,也只是多了扩容的灵活性,成本是少不掉的。软件成本:编写出这个计算算法的人工费 + 软件环境的成本。这个成本也与前两项相关,程序控制力度粗犷一些,实现逻辑简单一些,程序就容易编写,那软件成本就会低一些,带来的副作用是运行时间超长或者需要昂贵的硬件。这三个因素里面,一般对于计算任务来说,自然是越快越好,当然只要不慢过能容忍的时长,也就还算是有意义的计算;而硬件因素的弹性就比较小,有多少资源是相对固定的;所以,剩下的可以大做文章的就是软件成本了。软件成本里,程序员的工资是很重要的一项,而有没有顺手的软件环境让程序员能高效的把计算描述出来,就成了关键。最典型的例子就是理论上用汇编程序能写出所有的程序,但它明显不如 SQL 或 JAVA 做个常规计算来的容易。说到 SQL 和 JAVA,成规模的计算中心的一些维护者估计也会皱眉,使用它们的时间越长,越能体会需求变动或优化算法过程中的痛苦,明明算法过程自己想的很清楚了,但编写成可运行的程序就困难重重。这些困难主要来自两个方面:首先,一些基础的数据操作方法是自己逐渐积累的,没有经过整体的优化设计,这些个人工具对个人的开发效率有不错的提升,但没法通用,也不全面,这个困难主要表现在用 JAVA 等高级语言实现的一些 UDF 上。第二,主要是思维方式上的,在生产场景下用习惯了 SQL 查询,在计算场景下遇到的性能问题自然而然就想通过优化 SQL 语句的方式把问题缓解掉。但实际上这可能是个温水煮青蛙的过程。越深入搞,把简单的过程问题越可能搞成庞大不可拆分的逻辑块,到最后只有原创作者或高手才敢碰它。我这个老程序员,十多年前刚入行的时候,八卦中耳闻过 ORACLE 的系统管理员,尤其是有性能优化能力的,比普通程序员贵多了,可见这个难题在数据规模相对较小的十年前已经凸显了。(注:生产场景和计算场景在初始阶段的软件系统里一般很难截然分开,数据都是从生产场景积累起来的,等积累多了,慢慢会增加计算需求,逐渐独立出计算中心和数据仓库。这个量变引起质变的过程,如果不在思维上转变,不引入新办法,那就将成为被煮的青蛙。)为了节省读者的时间,我们先把性能优化的常用手段总结一下,方便有需求的用户逐条对比进行实际操作。1、 只加载计算相关数据。列存方式存储数据;常用的字段和不常用的分开存储;用独立的维表存储维的属性,减少事实表的信息冗余;按照某些常用作查询条件的字段分开存储,如按年份、性别、地区等独立存储;2、 精简计算涉及到的数据用来分析时,一些冗长的编号,可以序号化处理,用 1、2、……替代 TJ001235-078、 TJ001235-079、……,这样即能加快加载数据的速度,又能加快计算速度。日期时间,如果用字符串类型按照我们熟悉的格式 (2011-03-08) 存储,那加载和计算都会慢。前面这个日期可以存储成 110308 这样的数值类型,也可以存储成相对于一个开始时间的毫秒数(如相对于最早的数据 2010-01-01 的毫秒数)。3、 算法的优化计算量小的条件写在前面,如 boolean 类型的判断,要早于字符串查找,这样用较少的计算就能排除掉不符合要求的数据;减少对大事实表的遍历次数。具体方法有:在一次遍历过程中,同时给多个独立的运算操作提供数据(后面会提到的集算器里的管道概念),而不是每个运算操作遍历一次数据;做 JOIN 时,在内存里的维表里检索事实表数据,而不是用每条维表数据去遍历一次事实表。查找时借用 HASH 索引、二分法、序号直接对位等方式加快速度。4、 并行计算加载数据和计算两个步骤都可以并行。考量计算特点,根据加载数据和运算哪个量更大来判断瓶颈是计算机的磁盘还是 CPU,磁盘阵列适合并行加载数据,多核 CPU 适合并行运算。多机集群的并行任务,要考虑主程序和子程序的通讯问题,尽量把复杂计算独立到节点机上完成,网络传输较慢,要减少节点机之间的数据交换。实操效果兵马未动粮草先行,有了上面这些指导思想,我们下面就切入正题实现漏斗计算的优化,看一下实际的优化效果。1、未做任何优化,直接开工数据:程序:(附件中的 1-First.dfx,也附带了测试数据文件,可在集算器里直接执行) 漏斗转换计算核心代码的逻辑细节在上一篇中详细介绍过,这里就不再赘述。结果:(注:之后的测试都以 118 万条数据为基础,成倍增加) 118 万条记录 /70MB/ 用户数量 8000/31 秒; 590 万条记录 /350MB/ 用户数量 4 万 /787 秒。分析: 数据量增加到 5 倍,但耗时增加到了 26 倍,性能下降得厉害,而且不是线性的。原因是被分析的用户列表扩大了 5 倍,同时被分析的记录数也扩大 5 倍,那检索用户次数理论上就扩大了 5*5 倍。接下来采用以下优化方式↓2、按用户 ID 顺序插入用户列表,用二分法查找用户。程序:(2-BinarySearch.dfx)B12 给 find 增加 @b 选项,指明用二分法查找;D13 中却去掉 insert 的第一个位置参数 0 后,新用户就不直接追加到最后了,而是按主键顺序插入。结果:118 万条记录 /70MB/ 用户数量 8000/10 秒;590 万条记录 /350MB/ 用户数量 4 万 /47 秒。分析:优化后,1 倍的数据量耗时缩减到 1/3;5 倍的数据量提速比较明显,缩减到 1/16。进一步观察,5 倍数据量是 350MB,从硬盘载入数据的速度慢点算也会有 100M/ 秒,假如 CPU 够快的话,极限速度应该能到 4 秒左右,而现在的 47 秒证明 CPU 耗时还比较严重,根据经验可以继续优化↓3、批量读入游标数据程序:(3-BatchReadFromCursor.dfx)1217 行整体剪切后,向右移一个格子之后,在 A12 增加一个批量加载游标数据的循环,表示 A11 中的游标每次取 10000 条,B12 再对取出来的这 10000 条数据循环处理。结果:118 万条记录 /70MB/ 用户数量 8000/4 秒;590 万条记录 /350MB/ 用户数量 4 万 /10 秒;5900 万条记录 /3.5GB/ 用户数量 40 万 /132 秒;11800 万条记录 /7GB/ 用户数量 80 万 /327 秒。分析:优化后,1 倍数据量耗时缩减到 2/5;5 倍的数据量缩减到 1/5;新测试的 50 倍、100 倍性能也大体随数据量保持了线性。注意到原始数据有一些字段用不到,用到的字段也可以通过序号化等手段再简化,简化后的文件会小几倍,从而达到从硬盘减少读取时间的目的,具体优化方式如下↓4、精简数据思路:先观察一下原始数据:用户 ID 用从 1 开始的序号替代,除了减少少许存储空间外,还可以在后续计算时通过序号快速定位到用户,减少查找时间。时间和年月日字段信息重复,去掉年月日,长整型的时间字段也可以进一步精简成相对 2017-01-01 这个开始时间的毫秒数;事前我们知道只有 10 种事件,那事件 ID 和事件名称可以单独提取出个维表记录,这个事实表里只保存序号化的事件 ID(1、2、3…10)就够了;事件属性是 JSON 格式,种类不多,那对于某一种事件,可以用序列存储事件属性的值,在序列中的位置表示某种属性,这样即缩减存储空间,又能提升查找属性的效率。除了上面这些字段值的精简,我们存储数据的格式弃用文本方式,改变成集算器二进制格式,存储空间更小,加载速度更快,精简后的事实表如下:实现:精简事实表数据之前,要先通过事实表生成用户表、事件表两个维表的(genDims.dfx,运行后生成 user.bin 和 event.bin):提取维表的这段程序,仍然有优化的手段体现。提取两个维表,常规思维是每遍历一遍数据,生成一个维表;从硬盘读入大量数据进行遍历,读入慢,但读入后的计算量却非常小。针对这种情况,那有什么手段可以在读入数据时,同时用于多种独立的计算呢,答案就是“管道”,多定义了几个管道,就多定义了几种运算。A4 针对 A3 游标定义管道,A5 定义 A4 管道的分组计算,A6 定义另外一个分组计算,A7 导出 A6 的结果,A8 导出 A4 管道的结果。最终得到的两个维表如下:基于上面两个维表对事实表进行精简(toSeq.dfx),6.8G 的文本文件精简后,得到 1.9G 的二进制文件,缩小了 3.5 倍。这段代码出现了一个新的知识点,第 712 行定义了一个函数来处理 json 格式的事件属性,B15 里精简每一行数据时,调用了这个函数。B16 把每次精简好的一万条记录追写入同一个二进制文件。程序:(4-Reduced.dfx)在上一次程序的基础上改造了这么几个格子:A3/A4 中的时间相对于 2017-01-01;A6 事件序列改用序号;A7 中属性过滤,用精确匹配值的方式替换以前低效的模糊匹配字符串方式; A10 初始化用户序列,长度为用户数,该序列中的位置代表用户的序号;C12 用序号方式查找用户;E13 用序号方式存储新用户:结果:11800 万条记录 /1.93GB/ 用户数量 80 万 /225 秒。分析:优化后,100 倍数据量耗时缩减到上一步的 2/3。除了精简涉及的查询字段,我们再看看另一种能有效缩减查询数据量的方法↓5、把数据预先拆分存储,计算的时候只加载涉及到的数据思路:如何拆分数据和查询特点有关,这个例子中经常查询不定时间段,那按照日期拆分比较合适,按照事件 ID 拆分就没有意义了。拆分数据的程序(splitData.dfx):A4 每次取出 10 万条数据;B4 循环 60 天;C6 按照日期查询到数据后,通过 C9 追加到各自日期的文件里。执行后生成 59 天的数据文件:程序:(5-SplitData.dfx)A2 中把以前被分析的文件定义换成目录;A3/A4 的起止日期条件有所变动,以前是查询日期字段,现在变成查找日期文件;A11 把目录下的日期文件排序,选出要分析的多个日期文件,然后组合成一个游标之后再进行事件过滤就可以了。结果:目标数据选择 2017-02-01 至 2017-02-05 这 5 天,全量扫描数据 168 秒;只扫描 5 个文件得到相同结果 7 秒,效果显著。到目前为止,读取数据和计算都是单线程的,下面我们再试试并行计算↓6、并行计算单线程加载数据,多线程计算程序:(6-mulit-calc.dfx)增加 B 列,B2 中启动 4 个线程处理 A12 里加载的 100000 条数据,C12 中依据用户 ID%4 的余数分成 4 组,分别给 4 个线程进行运算。结果:11800 万条 /1.93GB/ 用户数 80 万 /4 线程 / 一次性读入 10 万条数据 /262 秒;11800 万条 /1.93GB/ 用户数 80 万 /4 线程 / 一次性读入 40 万条数据 /161 秒;11800 万条 /1.93GB/ 用户数 80 万 /4 线程 / 一次性读入 80 万条数据 /233 秒;11800 万条 /1.93GB/ 用户数 80 万 /4 线程 / 一次性读入 400 万条数据 /256 秒。分析:笔者测试机器是单个机械硬盘,加载数据速度是瓶颈,所以对提速不太明显。但调整单次加载的数据量,还是会有明显的性能差异。每次处理 40 万条数据时性能最优。多线程加载数据预处理:(splitDataByUserId.dfx)虽然 4 个线程可以同时读全量数据的同一个文件,但每个线程读出 3/4 的无用数据必然拖慢速度,所以预先按照用户 ID%4 拆分一下文件能更快些。C3 查询出 ID%4 的数据,C6 把查询的数据存入相应的拆分文件。程序:(6-mulit-read.dfx)把多线程代码前移到 A11,每个线程内读取各自的文件进行计算 (B11)。结果:11800 万条记录 /1.93GB/ 用户数量 80 万 /4 线程 /113 秒。分析:同样受限于加载数据速度,提速也有限。如果用多台机器集群,每台机器处理 1/4 的数据,因为是多个硬盘并行,速度肯定会有大幅提升,下面我们就看一下如何实现多机并行↓多机集群并行计算集算器如何部署集群计算,如何写集群的主、子程序的知识点不是本文重点关注的,可以移步相关的文档详细了解:http://doc.raqsoft.com.cn/esproc/tutorial/jqjs.html。主程序:(6-multi-pc-main.dfx)A3 中用 callx 调用子程序 6-multi-pc-sub.dfx,参数序列 [1,2,3…] 传入每个子程序控制处理哪一部分数据;返回的结果再通过 B6 汇总到一起,结果存放在 A4 格子里。A3 得到结果序列:A4 汇总出最终结果:节点机子程序:(6-multi-pc-sub.dfx)相比较上一步单机多线程加载数据的程序,去掉 A11 的多线程 fork to(4);节点机计算哪个拆分文件是通过 taskSeq 参数由主程序传过来的(B11);A22 把 A20 里的结果返回给主程序。结果:11800 万条记录 /1.93GB/ 用户数量 80 万 / 单节点机处理四分之一数据 /38 秒。主程序汇总的时间很短忽略不计,也就是 4 个 PC 的四块硬盘并行加载数据时,能把速度提升到 38 秒。程序和测试数据在百度网盘下载。安装好集算器,修改下程序里的文件路径,就可以运行看效果了。结束语 - 前瞻看到上面这么多的优化细节,估计有人质疑,这么费力的把这事做到极致,是不是吹毛求疵了?数据库应该是内置了一些自动的优化算法,目前已有共识的是尤其 ORACLE 在这方面已经做的很细致,这些细节根本不需要用户操心。确实,自动性能优化的重要意义是肯定的,但近几年随着数据环境的复杂化,数据量的剧增,更精细的控制数据的能力也就有了越来越多的应用场景,虽然会增加学习成本,但也会带来更高的数据收益。而且这个学习成本除了解决性能问题外,还能更好地解决根本上的描述复杂计算、整理数据方面的业务需求,更何况这类问题是无法自动化的,因为是“决策要做什么”变复杂了,因此只能提供更方便的编程语言提高描述效率,正视问题。计算机再智能,也不能替代人类做决策。自动和手动两种方式不是对立,而是互补的关系!上面这些优化的思路是我们程序员能预先想到的,同时也大概能根据计算任务特点选择效果显著的优化方式。但我要说的是计算机系统太复杂了:特点迥异的计算需求、不稳定的硬盘读写速度、不稳定的网络速度、无法估量的 CPU 具体计算量!所以实际业务中我们还需要依靠经验根据实际优化的效果来选择优化方法。SPL 出现以前,因为优化方式的实现和维护都比较困难,因此试验动作就难以密集进行,优化成果不多也就是自然的了;同时因为缺乏密集“倒腾”数据的锻炼,优化经验的积累也不容易,这也从另一个角度验证了高级数据分析师人才昂贵的现状。使用高效工具的第一批人,永远是获益最大的那一群人,第一批用弓箭的,第一批用枪的,第一批用坦克的,第一个用原子弹的……而你就是第一批用 SPL 的程序员。程序员的庞大队伍里分化出一支专业搞数据处理、分析的数据程序员,形成一个有独立技能的职业,这是必然的趋势。您的职业规划,方向选择也要尽早有个打算,才有占领某一高地的可能。最后还要说一句,目前这个结果仍然还有优化余地。如果再将数据压缩存储,还可以进一步减少硬盘访问时间,而数据经过一定的排序并采用列式存储后确实还可以再压缩。另外,这里的集群运算拆分成了 4 个子任务,而即使配置相同的机器,也可能运算性能不同,这时候就会发生运算快的要等运算慢的,最终完成时间是以计算最慢的那台机器为准,如果我们能把任务拆得更细一些,就可以做到更平均的效率,从而进一步提高计算速度。这些内容,我们将在后面的文章继续讲述。 ...

December 29, 2018 · 2 min · jiezi

前端性能优化总结

1.原则多使用内存,缓存或者其他方法减少CPU计算,减少网络请求减少IO操作(硬盘读写)2.加载资源优化静态资源的合并和压缩。静态资源缓存(浏览器缓存策略)。使用CDN让静态资源加载更快。3. 渲染优化CSS放head中,JS放body后图片懒加载减少DOM操作,对DOM操作做缓存减少DOM操作,多个操作尽量合并在一起执行事件节流尽早执行操作 DOMContentLoaded4. 示例4.1 资源合并a.js b.js c.js — abc.js4.2 缓存通过连接名称控制缓存<script src=“abc_1.js” ></script>只有改变内容的时候,链接名称才会改变。4.3 懒加载 <img src=“preview.png” realsrc=“abc.png” id=“img1” /> <script> var i = document.getElementById(‘img1’); i.src = i.getAttribute(‘realsrc’); </script>4.4 缓存dom查询 //没有缓存dom for (let i = 0; i < document.getElementsByTagName(‘p’).length; i++) { } //缓存dom var p = document.getElementsByTagName(‘p’); for (let i = 0; i < p.length; i++) { }4.5 合并dom插入 var listNode = document.getElementById(’list’); var flag = document.createDocumentFragment(); var li; for (let i = 0; i < 10; i++) { li = document.createElement(’li’); li.innerHTML = i; flag.appendChild(li); } listNode.appendChild(flag);10次dom插入 —> 1次dom插入4.6 事件节流监听文字改变事件,无操作100毫秒后执行操作,不用每次触发。 var textarea = document.getElementById(’ta’); var timeoutId; textarea.addEventListener(‘keyup’,function(){ if(i){ clearTimeout(i); } timeoutId = setTimeout(() => { //操作 }, 100); });事件节流主要用于触发频率较高的事件,设定一个缓冲触发事件。补充异步加载非核心代码异步加载 – 异步加载的方式 – 区别1.动态脚本加载用js创建2.defer3.async<script src=“script.js”></script>没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。<script async src=“script.js”></script>有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。<script defer src=“myscript.js”></script>有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。关于 defer,我们还要记住的是它是按照加载顺序执行脚本的标记为async的脚本并不保证按照指定它们的先后顺序执行。对它来说脚本的加载和执行是紧紧挨着的,所以不管你声明的顺序如何,只要它加载完了就会立刻执行。浏览器缓存总结的非常好浏览器缓存 – 缓存的分类 – 缓存的原理缓存就是html文件在本地存在的副本,强缓存发现有缓存直接用。Expires: 绝对时间,判断客户端日期是否超过这个时间Cache-Control:相对时间,判断访问间隔是否大于3600秒//在设定时间之前不会和服务端进行通信了//如果两个都下发以后者为准协商缓存询问服务器缓存是否可以用,在进行判断是否用。Last-Modified/If-Modified-Since第一次请求,respone的header加上Last-Modified(最后修改时间)再次请求,在request的header上加上If-Modified-Since 和服务端的最后修改时间对比,如果没有变化则返回304 Not Modified,但是不会返回资源内容;如果有变化,就正常返回资源内容。浏览器收到304的响应后,就会从缓存中加载资源如果协商缓存没有命中,浏览器直接从服务器加载资源时,Last-Modified的Header在重新加载的时候会被更新Etag/If-None-Match这两个值是由服务器生成的每个资源的唯一标识字符串,只要资源有变化就这个值就会改变;其判断过程与Last-Modified/If-Modified-Since类似,他可以精确到秒的更高级别。DNS预解析<meta http-equiv=“x-dns-prefetch-control” content=“on”><link rel=“dns-prefetch” href="//www.zhix.net">在一些浏览器的a标签是默认打开dns预解析的,在https协议下dns预解析是关闭的,加入mate后会打开。 ...

December 28, 2018 · 1 min · jiezi

阿里专家梁笑:2018双十一下单成功率99.9%!供应链服务平台如何迎接大促

本篇文章来自于2018年12月22日举办的《阿里云栖开发者沙龙—Java技术专场》,梁笑专家是该专场第一位演讲的嘉宾,本篇文章是根据梁笑专家在《阿里云栖开发者沙龙—Java技术专场》的演讲视频以及PPT整理而成。摘要:2018年双十一平稳度过,海量订单、零点流量高峰,阿里是如何实现供应链99.9%的下单成功率?本次分享中,阿里2018年双十一大促供应链服务保障平台负责人向大家详细阐述大促前4个月的全程经历。面对大促第一步需要做什么,流量峰值如何评估,性能优化从何处着手,一套有条不紊的供应链服务平台迎接大促的解决方案至关重要。演讲嘉宾简介:梁笑,阿里新零售供应链平台事业部,擅长 Java、Spring、OOP、分布式、架构设计,参与过供应链服务决策平台、双十一大促等项目,对业务开发、大促保障、架构设计等有所涉猎。本次直播视频精彩回顾,戳这里!PPT下载地址:https://yq.aliyun.com/download/3183以下内容根据演讲嘉宾视频分享以及PPT整理而成。本文围绕双十一大促历程,从以下几方面介绍供应链服务平台如何迎接大促:1. 概述2. 梳理链路3. 峰值评估4. 容量评估5. 性能优化6. 依赖改造7. 保障协同8. 压测检验9. 项目协同10. 监控治理11. 战前准备12. 总结一、概述大家熟悉的供应链服务包括库存控制、计划调拨等,这里介绍的供应链服务更偏向于交易侧。例如在淘宝或天猫购买物品时,会有今日下单预计某日送达等物流提示,这种物流时效承诺即是由供应链服务平台提供。再如,购买电冰箱、空调等大件需要预约配送时,需要选择指定的配送商或者货到付款、拆单合单等物流透出,都需要供应链服务平台的支持。除了这些大家作为消费者可以直观感受到的服务外,商家的商品入库发货等操作也都离不开供应链服务平台。因为大促时物流服务需要实时透出,服务平台是需迎接双十一实时下单的高流量的,此外服务平台还需要承接物流发货的服务透出,例如发货的仓库、路线、发货时效及服务等。因此,供应链服务平台在阿里体系中扮演了一个上承交易、下接物流的腰部力量,主要包括物流服务、物流透出、商家订购、容量管控等功能。物流服务及物流透出上述已提及,商家订购包含仓订购和快递资源订购,容量管控是指物流干线的容量能力,例如从北京到上海,可以在当日送达的物流能力为一千单,超过一千单外的便无法达到这样的物流时效诉求。至此,大家应大致了解了供应链服务平台的概念。供应链服务平台保障的过程如下图所示:如何在业务容量峰值时保障系统,该采取哪些措施,这是一个不小的挑战。首先,需要分析出哪些接口需要重点保障,哪些薄弱点或性能低下的链路需要重点评估。接下来,需要对这些进行优化操作。然后,对优化后的接口通过自测或压测进行验证。验证过程中可能会发现,已有的问题解决了但是出现了新的问题,这就需要再次分析优化。这是个螺旋上升形的过程。上图右侧是保障过程的阶段描述,会在接下来进行详细阐释。二、梳理链路如果给你一个系统,在你不了解的情况下,让你在流量峰值下保障系统的稳定性,首先你需要明确要做的事情,清晰目标任务的范围。因此,第一步就是梳理系统业务的链路,对系统有宏观全面的认识,这是保障大促稳定性的前提。但是,以服务平台的现状来说,有以下几个通病:祖传代码、上古应用、文档缺失、无人可问。阿里的服务平台自2011年开始经历过三次较大的升级改造,每次升级大部分核心功能会下架升级,但仍有某些小功能无法改造。这就造成在2018年的大促保障时还需要阅读2011或2012年的代码,甚至某些应用都无法打印日志。文档缺失、无人可问也使系统保障更加困难。梳理链路可以分为以下四步进行:1. 梳理对外接口。在日常开发中,主要的业务逻辑代码大约包含四五个对外接口,但是通常来说实际会比这要多。在对服务平台的链路梳理中,新老三版系统包含大约80多个对外接口。但在实际工作中,只会涉及十个接口左右。可见,这些接口存在巨大的冗余。某些接口可能已不再使用,或某些接口有使用者,但使用者并没有意识到在调用该接口。因此,梳理对外接口就是梳理出接口是否在实际提供服务。2. 移除无用接口。在剩下的70多个接口中,一定有无用的部分,移除这些接口也是移除了潜在的bug出现。这可以通过查看调用者、分析流量或者咨询使用者来进行移除。在这个阶段,阿里的供应链服务系统大约下架了近40个废接口,如此大幅缩小了需要保障的系统范围。3. 梳理剩余接口。这个阶段中将各接口按照重要性进行保障力度的划分,例如某些接口是需要重点保障的,而某些接口无需重点保障,不应占用宝贵的机器资源、数据库资源等,例如一些查询功能的端页面,在大促时出现问题也不会产生重大影响。4. 确认强弱依赖。对划分出的需要重点保障的接口需再次梳理出相互间的强弱依赖关系,这就需要理清系统的业务逻辑。例如在阿里的大促系统中,如果库存出现问题,那么紧接其后的订单、配送等接口也都会混乱,这便是一个强依赖过程。因此,这里应设置成如果库存出现问题,下单只能返回失败。弱依赖过程则不同,例如根据历史数据得知某消费者经常使用某一个自提站点,如果在大促中,自提站点的链路挂掉,分配的不是这个自提点,虽然这可能会导致消费者的愉悦感下降,但下单过程不会产生太大的问题。此外,还需对接口的调用量和调用比例进行统计梳理,例如调用一次A接口,可能会在下游调用一次或若干次B或C接口,通过这个比例可以根据A接口流量的增长幅度计算下游的接口流量。如此梳理完成后便可以产出一份链路文档,大致掌握了需要重点保障的部分、可能的调用方、可能的消费者、消费的量级、涉及的数据库、缓存中间件、链路的强弱依赖等信息,这是后续工作的纲领性文件。三、峰值评估在有了这份纲领性文件之后,则需要明确任务目标。对这样实时下单的强依赖系统来说,最直观的的目标是要支撑住双十一零点的交易峰值。这就需要评估这个交易峰值是多少,毕竟每个系统被核心的交易系统调用的频率并不相同。峰值可以根据交易下单量、业务预测和历史经验来进行评估。交易下单量相对来说确定性较高,因为这在系统内部可以通过限流解决。当下单量超出了系统的承受力,则直接返回下单失败,让消费者再次下单即可。因此每秒的最高下单量是相对确定的值。业务预测是指对每个行业线的下单量进行预测。即使知道每秒的下单总量,却无法知道例如天猫超市或者某个行业线的细化下单量,那么便收集该行业线下的店铺信息,再根据一些专业知识进行评估。这样的业务预测值是缺少瞬值的,例如它可能可以预测出一天的单量,但无法预测0点那一秒的单量。因此,还是需要根据历史经验来弥补这个缺陷。例如根据前三年的大促流量分布、双十一前的三八大促九九大促流量分布等数据,进行流量预估得到流量漏斗。甚至在大促前,对某些链路进行了改造变更等操作的,也需要在流量预估的过程中详尽考虑。由上述可见,在这样输入有限的情况下,需要抽丝剥茧得出一份可能性较高的峰值评估。这样的峰值评估主要为两方面,如下图所示。一部分是接口峰值,这会用于指导保障整个系统。例如若下单量为十万,那么商品详情接口大约会产生三十万流量,渲染下单接口十二万到十五万,真正下单扣减约七到八万,这些值会指导进行系统下单时的重点保障。另一部分是行业峰值。例如下单量为十万时,快消品牌和美妆产品各自的单量为多少,这会直接显示出系统各行业所需要的下单承受能力。四、容量评估确定了任务目标后,就需要评估现有的机器容量是否可以支撑预估的流量峰值。阿里的供应链服务平台是一个单元化的应用,可以理解为一个跨城市跨机房的异地部署过程。每个单元(机房)有不同的流量承受能力,例如可以在华北设立机房承受30%流量,华南机房承受30%流量,华东机房承受40%流量。接着通过压测等方法可以方便的得出单机性能,测试出健康情况下单机的QPS流量。例如,UNSZ(深圳单元)需要承担6.52%的流量,结合单机性能预估需要的机器数量,根据比例得出预估QPS值。接下来需要考虑,现有的机器能满足预估状况嘛?是否有缺口?如果存在缺口,是可以通过优化还是申请预算解决呢?最短的木板(压力最大的部分)是哪里?因为各个机房的实际性能存在差异,这部分差异也需要考虑在内。根据最短木板确定限流保护值,重点保障最危险的单元。此外,在容量评估时,最好预留一定的buffer,保障可能超出的流量。五、性能优化基于已有的机器预算,和之前梳理出的接口链路,接下来需要一些手段优化性能来支持峰值流量。不仅包括对古老代码的优化,也需要对新产生的代码进行改进。下图展示了供应链服务平台的大促性能优化中采取的主要手段:这些细节化手段这里不再赘述,大家可以参考一些web技术文档即可了解,这里重点为大家介绍如何梳理上述优化过程。没有一个整体的方法论,贸然入手消耗的精力暂且不谈,达到的效果可能也不尽人意。这些优化手段主要通过分析阿里的业务特点及双十一的玩法特点得出。例如,若某一地区的仓配产能不高,无法实现物流时效承诺,那么撤下物流时效承诺的同时,与其相关的链路也可以一同撤下,这便大大提升了这一块的性能。因此,提升性能不只是通过内存调优等,也可以根据业务逻辑来调整。优化的各个步骤如下:1. 减少流量。在客户端或服务端屏蔽流量,降低不必要的调用,这样的性能优化效果最为可观。2. 移除或降级IO。根据业务需求尽可能的下线无用的IO逻辑或者根据业务特点降级部分IO。3. 减少IO调用。例如在货品查询IO中,可能会采用多个for循环来进行查询,那么便可以采用批量查询来代替单个查询。4. 移除本地IO。移除和检查没用的日志和滥用的日志。某些debug代码在平时产生的流量不会引起大家注意,但是在全链路压测验证时就可能产生非常多流量以致线程卡住。5. 使用缓存。短时间内数据不变的情况下,进行缓存,避免多余的IO。这种方式在大促时效果尤为明显。在电商行业,类似某个店铺与货品的绑定关系在大促时间内基本不会变化,这种不变的数据使用缓存效果最佳。6. 缓存命中率。在平时缓存可能设定失效时间为几十分钟,但是大促时可以禁止该类影响稳定性的操作,将失效时间设置为几个小时甚至十几小时。拉长缓存失效时间,可以极大的提高缓存命中率,平时的命中率大约在60%-70%左右,在大促时这个数据提升到了99.99%。此外,还需要充分预热。7. 缓存吞吐。除了本地缓存外,大家可能都会使用远程缓存,例如redis等。阿里巴巴使用tair缓存,对其序列化方式进行了修改。java自带的序列化方式性能较差,得到的序列化对象也字节较大,因此将其改成了Hessian方式(没有选择kryo或protobuf是因为当时转换成Hessian方式最为简便)。同时精简字段,假设某个返回对象有十个字段,但对外接口只需使用两三个。大家知道,带宽一定的情况下,单个数据量越大,同时能通行的数据就越少。流量峰值时,带宽通常都会比较紧张,那么便无需将剩下的废字段也存入缓存。进行了上述改进,缓存的吞吐有了很大程度的好转。8. 本地缓存。尽量使用本地缓存在tair前挡一层,避免网络IO。本地缓存不可能存太多,但是在某些极限情况,例如卡券类或红包等,当天调用极其频繁并且不会变化,这样的数据可以缓存在堆外内存中。9. 数据库。在解决了数据IO的大部分问题后,便可以采取一些更细致的优化手段。例如检查慢的SQL,索引使用是否正确,有没有离线拖库任务,是否可以清理一些碎片等。10. 本地代码。去掉高频率低效的本地代码。但根据经验,这种手段得到的优化效果有限。因此,纵观整个优化思路,减少IO和使用缓存是优化性能的效果较为明显的方法。六、依赖改造在性能优化后,大致不会在峰值时出现宕机的情况,但仍需要进行强弱依赖的改造,即强依赖改为弱依赖,弱依赖改为强依赖。强依赖是指如果对兄弟系统、数据库或缓存的调用出现问题,那么流程直接中断。弱依赖指即使这部分宕掉,可以直接设置超时或者熔断,并不会影响流程的后续。强弱依赖的改造其实是一个兜底的过程。将某些错误的强依赖改为弱依赖,使其不会影响核心流程,防止雪崩。其次,将一些弱依赖改为强依赖。第一种是确定性的依赖更改。例如如果下单成功需要将扣减库存数量,如果库存数量没有扣减成功,会导致后续各种问题,如果之前采用的措施是将这个异常catch住继续后面的流程,那么这里就需要更改为抛出异常阻断流程。第二种是下游重依赖的数据。第三种是可能会造成资损的数据。强弱依赖更改实际上就是对业务负责和保障的过程。七、保障协同作为系统的负责人,做到上述性能优化等工作对本系统来说足够了,但是其他兄弟系统的保障协同也是重要的一环。在阿里,每次中间件大规模要求升级时,大促也就近了。供应链服务平台重度依赖的其他系统,例如商品中心、库存中心、订购中心和容量中心等也各自针对大促做了不同程度的优化。例如商品中心预热手段发生了变化,重点流量做出分组操作,并专门留出了部分机器给服务平台系统做流量分割。库存中心升级了新模型,改进了缓存等,那么服务平台系统也需要针对这部分进行相应的协同。和订购中心的协同主要在预热和防雪崩,假如宕掉时间过长,就避免调用数据库,而去调用缓存。容量中心之前只支持单个扣减, 现在可以支持批量扣减,或者修改了表结构。这些兄弟系统诸如此类的优化也从侧面使得服务平台更加健康。八、压测检验完成优化和协同保障后,当然需要压测检验。在双十一期间总共经历12次全链路压测,因为无法再造一个全链路系统,所以只能使用真实系统压测。为了不影响真实系统的运行和消费者的感观,压测通常在半夜进行。在压测过程中,发现了不少问题,这里举出以下三个例子:1. 本地线程池满。jstack查看线程堆栈时,发现很多线程卡在logback输出日志的地方。查看源码后发现是使用log.error打印了一个很大的JSON对象,且每次调用都会打印,这就造成本地线程池立刻满了。因为系统较老,依靠个人的代码review无法发现此类问题,这就需要压测这种真实的流量手段来发现。2. 超时请求。鹰眼(阿里全链路监控系统)发现有一个长达3s的请求,一般请求为毫秒级别,检查后发现是该部分数据缓存未预热,查询数据库超时引起。因为该部分数据在大促时不会调用,因此在压测时是可以提前将其撤下的。但在压测时出现这个问题可能会影响到其他业务的稳定。3. Jmap超时。阿里的服务平台通过手工执行jmap命令触发GC来释放内存,发现在执行jmap时(会先下线对应服务),对应的机器仍然对外服务,并且大多超时。查看了对应的下线脚本后发现,由于镜像升级,导致Linux命令输出与老脚本预期不一致。这也影响了线上服务的稳定。上述问题只有压测验证才会检查出来,因此,压测的意义不容置否。每一次压测都是对上一轮优化结果的验证,同时,每次压测发现的问题,也是下一次优化的主要方向。九、项目协同服务平台内部的工作同事便有10人左右,更要包括对外的例如中间件的部门、依赖和被依赖的兄弟系统部门。人员众多导致各种协同问题的产生,这就需要一个协同机制。需要明确每个项目的负责人,有固定的联系方式。任务拆解要细致,每个部分的任务交给指定的人解决。另外需要文档沉淀,得出的文档不仅是这次大促的回顾总结,也是下次大促的一个参考。2018年的大促能够完成,也是依靠了之前的多次大促经验文档。验证考核也必不可少,验证此次的优化是否达到了设定的目标。某些在测试时没有显现出的问题会在真实的流量下被发现,因此事后的验证考核会帮助发现更潜在的bug。最后,风险识别,某些问题可能因为历史原因无法修复,那么这种问题要识别出来,和业务人员或产品人员共同找一个可以弥补的方法。例如可以将由于这个bug产生的问题订单使用工具单独抛出,再行安排。这些都是作为一个项目PM需要注意的重点。当然,如果能有一个项目间供大家face to face探讨各种问题是最好了。十、监控治理传统意义上的监控治理有两个诉求:看图和告警。看图主要关注各类曲线是否有波动是否稳定等,告警是在产生问题时可以通过短信、邮件等方式告知。除去传统的系统监控指标,例如GC次数、内存使用率、QPS高低、系统反应时间,还需要业务监控,这需要通过日志或离线采集进行统计,如此才能对系统和业务都有直观的认识。当然,“会出错的事总会出错”,过程中难免会出现问题。如果出现问题,则需要及时统计产生影响的商家数量、单量、消费者数量,以便后续紧急处理,采取拉单、扩容或下线服务等手段防止过大损失,这便是消防处理。十一、战前准备所有优化和监控措施完成后,大促开始前1-2天,按部就班执行各类计划动作。首先是执行前置预案,例如下线了某些业务链路要执行观察效果。其次是预热,在大促前一天将一切准备就绪,准备迎接峰值流量。接着进行一致性检查,单元化的核心应用涉及到成千的机器,可以通过计算MD5校验值,来确保所有的机器上的zip、jar、war包等是一致的。然后资源检查,对于大促当天的数据库、机器、中间件、内存等资源做一个检查,确保资源到位。清理数据库,如压测使用后废弃的表可以删除。最后,jmap释放内存,将可能会触发GC的内存全部释放。十二、总结回顾整个大促过程,今年的供应链服务平台比去年更加稳定,核心下单链路成功率接近99.9%,没有发生限流等严重影响用户体验的情况。从8月开始思考双十一的筹备工作,整个大促保障期间,涉及到众多的系统、人员和业务,每个系统和业务分别由不同的团队负责,需要做链路分析、模型分析、容量评估、目标评估,要协调和明确各节点间的职责划分、要做各类优化、压测、监控和反馈等等,持续了长达三四个月。“这个过程是一次人与人、人与对象间的碰撞,精密的仪器产生完美的作品。”本文作者:李博bluemind阅读原文本文为云栖社区原创内容,未经允许不得转载。

December 27, 2018 · 1 min · jiezi

【性能优化】quicklink:实现原理与给前端的启发

近来,GoogleChromeLabs 推出了 quicklink,用以实现链接资源的预加载(prefetch)。本文在介绍其实现思路的基础上,会进一步探讨在预加载方面前端工程师还可以做什么。1. quicklink 是什么的?quicklink 是一个通过预加载资源来提升后续方案速度的轻量级工具库。旨在提升浏览过程中,用户访问后续页面时的加载速度。当我们提到性能优化,往往都会着眼于对当前用户访问的这个页面,如何通过压缩资源大小、删减不必要资源、加快页面解析渲染等方式提升用户的访问速度;而 quicklink 用了另一种思路:我预先帮你加载(获取)你接下来最可能要用的资源,这样之后的真正使用到该资源(链接)时就会感觉非常顺畅。照着这个思路,我们需要解决的问题就是如何预先帮用户加载资源呢?这里其实涉及到两个问题:如何去预加载一个指定资源?(预加载的方式)如何确定某个资源是否要加载?(预加载的策略)下面就结合 quicklink 源码来看看如何解决这两个问题。注:下文提到的“预加载”/“预获取”均指 prefetch2. quicklink 实现原理2.1. 如何去预加载一个指定资源?首先要解决的是,通过什么方式来实现资源的预加载。即预加载的方式。我们这里的预加载对应的英文是 prefetch。提到 prefetch 自然会想到使用浏览器的 Resource Hints,通过提示浏览器做一些“预操作”(例如 DNS 解析、资源下载等)来加快后续的访问。如果对 prefetch 与 Resource Hints 不熟悉,可以看看这篇《使用Resource Hint提升页面加载性能与体验》。只需要下面这样一行代码就可以实现浏览器的资源预加载。是不是非常美妙?<link rel=“prefetch” href="/my.little.script.js" as=“script”>因此,要预加载一个资源可以通过下面四行代码:const link = document.createElement(link);link.rel = prefetch;link.href = url;document.head.appendChild(link);然而,我们不得不面对兼容性的问题,在低版本 IE 与移动端是重灾区。美梦破灭。既然如此,我们就需要一个类似 prefetch shim 的方式:在不支持 Resource Hints 的浏览器中,使用其他方式来预加载资源。对此,我们可以利用浏览器自身的缓存策略,“实实在在”预先请求这个资源,这也形成了一种资源的“预获取”。而这最方便的就是通过 XHR:const req = new XMLHttpRequest();req.open(GET, url, req.withCredentials=true);req.send();这样 shim 也完成了。最后,如何检测浏览器是否支持 prefetch 呢?我们可以通过 link 元素上 relList 属性的 support 方法来检查对 prefetch 的支持情况:const link = document.createElement(’link’);link.relList || {}).supports && link.relList.supports(‘prefetch’);结合这三个段代码,就形成了一个简易的 prefetcher:判断是否支持 Resource Hints 中的 prefetch,支持则使用它,否则回退使用 XHR 加载。值得一提的是,使用 Resource Hints 与使用 XHR 来预加载资源还是有一些重要差异的。草案中也提到了一些(主要是与性能以及与浏览器其他行为之间的冲突)。其中还有一点就是,Resource Hints 中的 prefetch 是否执行,完全是由浏览器决定的,草案里有句话非常明显 —— the user agent SHOULD fetch。因此,所有 prefetch 的资源并不一定会真正被 prefetch。相较之下,XHR 的方式“成功率”则更高。这点在 Netflix 实施的性能优化案例中也提到了。题外话:quicklink 中使用 fetch API 实现高优先级资源的加载。这是因为浏览器中会为所有的请求都设置一个优先级,高优请求会被优先执行;目前,fetch 在 Chrome 中属于高优先级,在 Safari 中属于中等优先级。2.2. 如何确定某个资源是否要预加载?有了资源预加载的方式,那么接下来就需要一个预加载的策略了。这其实是个见仁见智的问题。例如直接给你一个链接 https://my.test.com/somelink,在没有任何背景信息的情况下,恐怕你完全不知道是否需要预加载它。那对于这个问题,quicklink 是怎么解决的呢?或者说,quicklink 是通过什么策略来进行预加载的呢?quicklink 用了一个比较直观的策略:只对处于视口内的资源进行预加载。这一点也比较好理解,网络上大多的资源加载、页面跳转都伴随着用户点击这类行为,而它要是不在你的视野内,你也就无从点击了。这一定程度上算是个必要条件。这么一来,我们所要解决的问题就是,如果判断一个链接是否处于可视区域内?以前,对于这种问题,我们做的就是监听 scroll 事件,然后判断某元素的位置,从而来“得知”元素是否进入了视区。传统的图片懒加载库 lazysize 等也是用这种策略。document.addEventListener(‘scroll’, function () { // ……判断元素位置});注:目前 lazysize 也有了基于 IntersectionObserver 的实现当然,需要特别注意滚动监听的性能,例如使用截流、避免强制同步布局、 passive: true 等方式缓解性能问题。不过现在我们有了一个新的方式来实现这一功能 —— IntersectionObserver:const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { const link = entry.target; // 预加载链接 } });});// 对所有 a 标签添加观察者Array.from(options.el.querySelectorAll(‘a’), link => { observer.observe(link);});IntersectionObserver 会创建一个观察者,专门用来观察与通知元素进出视口的情况。如上述代码所示,IntersectionObserver 可以观察所有 a 元素的位置情况(主要是进入视野)。对 IntersectionObserver 不了解的同学可以参考 Google 的 IntersectionObserver 介绍文章。但是如下图所示, IntersectionObserver 存在兼容性问题,因此要在不兼容的浏览器中使用 quicklink,会需要一个 polyfill。目前,我们已经把 quicklink 的两大部分(预加载的方式和预加载的策略)的原理和简单实现讲完了。整个 quicklink 非常简洁,这些基本就是 quicklink 的核心。剩下的就是一些参数检查、额外的规则特性等。题外话:为了进一步保证性能,quicklink 使用 requestIdleCallback 在空闲时间查询页面 a 标签并挂载观察者。对 requestIdleCallback 不了解的同学可以看看 Google 的这篇文章。3. 到此为止?不,我们还能做更多到这里,quicklink 的实现就基本讲完了。仔细回想一下,quicklink 其实提供了我们一种通过“预加载”来实现性能优化的思路(粗略来说像是用流量换体验)。这种方式我在前面也提到了,其实可以分为两个部分:如何去预加载一个指定资源?(预加载的方式)如何确定某个资源是否要加载?(预加载的策略)其实两部分似乎都有可以作为的地方。例如如何保证 prefetcher(资源预加载器)的成功率能更高,以及目前使用的回退方案 XHR 其实在预加载无法缓存的资源时所受的限制等。此外,我们在这里还可以来聊一聊策略这块。由于 quicklink 是一个业务无关的轻量级功能库,所以它采用了一个简单但一定程度上有效的策略:预加载视野内的链接资源。然而在实际生产中,我们面对的是更复杂的环境,更复杂的业务,反而会需要更精准的预加载判断。因此,我们完全可以从 quicklink 中剥离出 prefetcher 来作为一个预加载器;而在策略部分使用自己的实现,例如:结合访问日志、打点记录的更精准的预加载。例如,我们可以通过访问日志、打点记录,根据 refer 来判断,从 A 页面来的 B、C、D 页面的比例,从而设置一个阈值,超过该阈值则认为访问 A 页面的用户接下来更容易访问它,从而对其预加载。结合用户行为数据来进行个性化的预加载。例如我们有一个阅读类或商品展示类站点,从用户行为发现,当该链接暴露在该用户视野内 XX 秒(用户阅读内容 XX 秒)后点击率达到 XX%。而不是简单的一刀切或进入视野就预加载。后置非必要资源,精简某类落地页。落地页就是要让新用户尽快“落地”,为此我们可以像 Netflix 介绍的那样,在宣贯页/登录页精简加载内容,而预加载后续主站的主包(主资源)。例如有些站点的首页大多偏静态,可以用原生 JavaScript 加 内联关键 CSS 的方式,加快加载,用户访问后再预加载 React、Vue 等一系列主站资源。等等。上面这些场景只是抛砖引玉,相信大家还会有更多更好的场景可以来助力我们的前端应用“起飞”。此外,我们完全可以借助一些构建工具、数据采集与分析平台来实现策略的自动提取与注入,优化整个预加载的流程。写在最后预加载、Resource Hints等由来已久。quicklink 通过提出了一种可行的方案让它又进入了大家的视野,给我们展现了性能优化的另一面。希望大家通过了解 quicklink 的实现,也能有自己的想法与启发。相信随着浏览器的不断进化,标准的不断前行,前端工程师对极致体验与性能要求的不断提高,我们的产品将会越来越好。 ...

December 25, 2018 · 2 min · jiezi

基于 Nginx 的 HTTPS 性能优化实践

摘要: 随着相关浏览器对HTTP协议的“不安全”、红色页面警告等严格措施的出台,以及向 iOS 应用的 ATS 要求和微信、支付宝小程序强制 HTTPS 需求,以及在合规方面如等级保护对传输安全性的要求都在推动 HTTPS 的发展。前言分享一个卓见云的较多客户遇到HTTPS优化案例。随着相关浏览器对HTTP协议的“不安全”、红色页面警告等严格措施的出台,以及向 iOS 应用的 ATS 要求和微信、支付宝小程序强制 HTTPS 需求,以及在合规方面如等级保护对传输安全性的要求都在推动 HTTPS 的发展。虽然 HTTPS 优化了网站访问体验(防劫持)以及让传输更加安全,但是很多网站主赶鸭子上架式的使用了 HTTPS 后往往都会遇到诸如:页面加载速度变慢、服务器负载过高以及证书过期不及时更新等问题。所以本文就来探讨一下 HTTPS 的优化实践。选型其实像 Apache Httpd、LigHttpd、Canddy 等 Web 服务软件都可以设置 HTTPS,但是在相应的扩展生态和更新率上都不如 Nginx。 Nginx 作为大型互联网网站的 Web 入口软件有着广泛的支持率,例如阿里系的 Tengine、CloudFlare 的 cloudflare-nginx、又拍云用的 OpenResty 都是基于 Nginx 而来的,Nginx 是接受过大规模访问验证的。同时大家也将自己开发的组件回馈给 Nginx 社区,让 Nginx 有着非常良好的扩展生态。 所以说 Nginx 是一款很好的 Web 服务软件,选择 Nginx 在提升性能的同时能极大的降低我们的扩展成本。新功能围绕 Web 服务已经有非常多的新功能需要我们关注并应用了,这里先罗列相关新功能。HTTP/2相比廉颇老矣的 HTTP/1.x,HTTP/2 在底层传输做了很大的改动和优化包括有:每个服务器只用一个连接,节省多次建立连接的时间,在TLS上效果尤为明显加速 TLS 交付,HTTP/2 只耗时一次 TLS 握手,通过一个连接上的多路利用实现最佳性能更安全,通过减少 TLS 的性能损失,让更多应用使用 TLS,从而让用户信息更安全在 Akamai 的 HTTP/2 DEMO中,加载300张图片,HTTP/2 的优越性极大的显现了出来,在 HTTP/1.X 需要 14.8s 的操作中,HTTP/2 仅需不到1s。HTTP/2 现在已经获得了绝大多数的现代浏览器的支持。只要我们保证 Nginx 版本大于 1.9.5 即可。当然建议保持最新的 Nginx 稳定版本以便更新相关补丁。同时 HTTP/2 在现代浏览器的支持上还需要 OpenSSL 版本大于 1.0.2。TLS 1.3和 HTTP/1.x 一样,目前受到主流支持的 TLS 协议版本是 1.1 和 1.2,分别发布于 2006年和2008年,也都已经落后于时代的需求了。在2018年8月份,IETF终于宣布TLS 1.3规范正式发布了,标准规范(Standards Track)定义在 rfc8446。TLS 1.3 相较之前版本的优化内容有:握手时间:同等情况下,TLSv1.3 比 TLSv1.2 少一个 RTT应用数据:在会话复用场景下,支持 0-RTT 发送应用数据握手消息:从 ServerHello 之后都是密文。会话复用机制:弃用了 Session ID 方式的会话复用,采用 PSK 机制的会话复用。密钥算法:TLSv1.3 只支持 PFS (即完全前向安全)的密钥交换算法,禁用 RSA 这种密钥交换算法。对称密钥算法只采用 AEAD 类型的加密算法,禁用CBC 模式的 AES、RC4 算法。密钥导出算法:TLSv1.3 使用新设计的叫做 HKDF 的算法,而 TLSv1.2 是使用PRF算法,稍后我们再来看看这两种算法的差别。总结一下就是在更安全的基础上还做到了更快,目前 TLS 1.3 的重要实现是 OpenSSL 1.1.1 开始支持了,并且 1.1.1 还是一个 LTS 版本,未来的 RHEL8、Debian10 都将其作为主要支持版本。在 Nginx 上的实现需要 Nginx 1.13+。BrotliBrotli 是由 Google 于 2015 年 9 月推出的无损压缩算法,它通过用变种的 LZ77 算法,Huffman 编码和二阶文本建模进行数据压缩,是一种压缩比很高的压缩方法。根据Google 发布的研究报告,Brotli 具有如下特点:针对常见的 Web 资源内容,Brotli 的性能要比 Gzip 好 17-25%;Brotli 压缩级别为 1 时,压缩速度是最快的,而且此时压缩率比 gzip 压缩等级为 9(最高)时还要高;在处理不同 HTML 文档时,brotli 依然提供了非常高的压缩率;在兼容 GZIP 的同时,相较 GZIP:JavaScript 上缩小 14%HTML上缩小 21%CSS上缩小 17%Brotli 的支持必须依赖 HTTPS,不过换句话说就是只有在 HTTPS 下才能实现 Brotli。ECC 证书椭圆曲线密码学(Elliptic curve cryptography,缩写为ECC),一种建立公开金钥加密的算法,基于椭圆曲线数学。椭圆曲线在密码学中的使用是在1985年由Neal Koblitz和Victor Miller分别独立提出的。内置 ECDSA 公钥的证书一般被称之为 ECC 证书,内置 RSA 公钥的证书就是 RSA 证书。由于 256 位 ECC Key 在安全性上等同于 3072 位 RSA Key,加上 ECC 运算速度更快,ECDHE 密钥交换 + ECDSA 数字签名无疑是最好的选择。由于同等安全条件下,ECC 算法所需的 Key 更短,所以 ECC 证书文件体积比 RSA 证书要小一些。ECC 证书不仅仅可以用于 HTTPS 场景当中,理论上可以代替所有 RSA 证书的应用场景,如 SSH 密钥登陆、SMTP 的 TLS 发件等。不过使用 ECC 证书有两个点需要注意:一、 并不是每一个证书类型都支持的,一般商业证书中带增强型字眼的才支持ECC证书的签发。二、 ECC证书在一些场景中可能还不被支持,因为一些产品或者软件可能还不支持 ECC。 这时候就要虚线解决问题了,例如针对部分旧操作系统和浏览器不支持ECC,可以通过ECC+RSA双证书模式来解决问题。安装下载源码综合上述我们要用到的新特性,我们整合一下需求:HTTP/2 要求 Nginx 1.9.5+,,OpenSSL 1.0.2+TLS 1.3 要求 Nginx 1.13+,OpenSSL 1.1.1+Brotli 要求 HTTPS,并在 Nginx 中添加扩展支持ECC 双证书 要求 Nginx 1.11+这里 Nginx,我个人推荐 1.15+,因为 1.14 虽然已经能支持TLS1.3了,但是一些 TLS1.3 的进阶特性还只在 1.15+ 中提供。然后我们定义一下版本号:# VersionOpenSSLVersion=‘openssl-1.1.1a’;nginxVersion=‘nginx-1.14.1’;建议去官网随时关注最新版:http://nginx.org/en/download.htmlhttps://www.openssl.org/source/https://github.com/eustas/ngx_brotli/releasesNginxcd /optwget http://nginx.org/download/$nginxVersion.tar.gztar xzf $nginxVersion.tar.gzOpenSSLcd /optwget https://www.openssl.org/source/$OpenSSLVersion.tar.gztar xzf $OpenSSLVersion.tar.gzBrotlicd /optgit clone https://github.com/eustas/ngx_brotli.gitcd ngx_brotligit submodule update –init –recursive编译cd /opt/$nginxVersion/./configure --prefix=/usr/local/nginx \ ## 编译后安装的目录位置–with-openssl=/opt/$OpenSSLVersion \ ## 指定单独编译入 OpenSSL 的源码位置–with-openssl-opt=enable-tls1_3 \ ## 开启 TLS 1.3 支持–with-http_v2_module \ ## 开启 HTTP/2 –with-http_ssl_module \ ## 开启 HTTPS 支持–with-http_gzip_static_module \ ## 开启 GZip 压缩–add-module=/opt/ngx_brotli ## 编译入 ngx_BroTli 扩展make && make install ## 编译并安装后续还有相关变量设置和设置服务、开启启动等步骤,篇幅限制就省略了,这篇文章有介绍在 Ubuntu 下的 Nginx 编译:https://www.mf8.biz/ubuntu-nginx/ 。配置接下来我们需要修改配置文件。HTTP2listen 443 ssl http2;只要在 server{} 下的lisen 443 ssl 后添加 http2 即可。而且从 1.15 开始,只要写了这一句话就不需要再写 ssl on 了,很多小伙伴可能用了 1.15+ 以后衍用原配置文件会报错,就是因为这一点。TLS 1.3ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;如果不打算继续支持 IE8,或者一些合规的要求,可以去掉TLSv1。然后我们再修改对应的加密算法,加入TLS1.3引入的新算法:ssl_ciphers TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:TLS13-AES-128-CCM-8-SHA256:TLS13-AES-128-CCM-SHA256:EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+ECDSA+AES128:EECDH+aRSA+AES128:RSA+AES128:EECDH+ECDSA+AES256:EECDH+aRSA+AES256:RSA+AES256:EECDH+ECDSA+3DES:EECDH+aRSA+3DES:RSA+3DES:!MD5;如果不打算继续支持 IE8,可以去掉包含 3DES 的 Cipher Suite。默认情况下 Nginx 因为安全原因,没有开启 TLS 1.3 0-RTT,可以通过添加 ssl_early_data on; 指令开启 0-RTT的支持。————实验性尝试众所周知,TLS1.3 由于更新了很久,很多浏览器的旧版本依旧只支持 Draft 版本,如 23 26 28 分别在 Chrome、FirFox 上有支持,反而正式版由于草案出来很久,导致TLS1.3在浏览器上兼容性不少太好。可以使用 https://github.com/hakasenyang/openssl-patch/ 提供的 OpenSSL Patch 让 OpenSSL 1.1.1 同时支持草案23,26,28和正式版输出。 不过由于不是官方脚本,稳定性和安全性有待考量。ECC双证书双证书配置的很简单了,保证域名的证书有RSA和ECC各一份即可。 ##证书部分 ssl_certificate /usr/local/nginx/conf/ssl/www.mf8.biz-ecc.crt; #ECC证书 ssl_certificate_key /usr/local/nginx/conf/ssl/www.mf8.biz-ecc.key; #ECC密钥 ssl_certificate /usr/local/nginx/conf/ssl/www.mf8.biz.crt; #RSA证书 ssl_certificate_key /usr/local/nginx/conf/ssl/www.mf8.biz.key; #RSA密钥Brotli需要在对应配置文件中,添加下面代码即可: brotli on; brotli_comp_level 6; brotli_min_length 1k; brotli_types text/plain text/css text/xml text/javascript text/x-component application/json application/javascript application/x-javascript application/xml application/xhtml+xml application/rss+xml application/atom+xml application/x-font-ttf application/vnd.ms-fontobject image/svg+xml image/x-icon font/opentype;为了防止大家看糊涂了,放一个完整的 server{}供大家参考: server { listen 443 ssl http2; # 开启 http/2 server_name mf8.biz www.mf8.biz; #证书部分 ssl_certificate /usr/local/nginx/conf/ssl/www.mf8.biz-ecc.crt; #ECC证书 ssl_certificate_key /usr/local/nginx/conf/ssl/www.mf8.biz-ecc.key; #ECC密钥 ssl_certificate /usr/local/nginx/conf/ssl/www.mf8.biz.crt; #RSA证书 sl_certificate_key /usr/local/nginx/conf/ssl/www.mf8.biz.key; #RSA密钥 #TLS 握手优化 ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m; keepalive_timeout 75s; keepalive_requests 100; #TLS 版本控制 ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; ssl_ciphers ‘TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:TLS13-AES-128-CCM-8-SHA256:TLS13-AES-128-CCM-SHA256:EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+ECDSA+AES128:EECDH+aRSA+AES128:RSA+AES128:EECDH+ECDSA+AES256:EECDH+aRSA+AES256:RSA+AES256:EECDH+ECDSA+3DES:EECDH+aRSA+3DES:RSA+3DES:!MD5’; # 开启 1.3 o-RTT ssl_early_data on; # GZip 和 Brotli gzip on; gzip_comp_level 6; gzip_min_length 1k; gzip_types text/plain text/css text/xml text/javascript text/x-component application/json application/javascript application/x-javascript application/xml application/xhtml+xml application/rss+xml application/atom+xml application/x-font-ttf application/vnd.ms-fontobject image/svg+xml image/x-icon font/opentype; brotli on; brotli_comp_level 6; brotli_min_length 1k; brotli_types text/plain text/css text/xml text/javascript text/x-component application/json application/javascript application/x-javascript application/xml application/xhtml+xml application/rss+xml application/atom+xml application/x-font-ttf application/vnd.ms-fontobject image/svg+xml image/x-icon font/opentype; location / { root html; index index.html index.htm; } }先验证一下配置文件是否有误:nginx -t如果反馈的是:nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is oknginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful就可以重启 Nginx ,然后到对应网站中去查看效果了。验证HTTP/2通过浏览器的开发者工具,我们可以在 Network 栏目中看到 Protocol 中显示 h2 有无来判断。TLS 1.3老地方,我们可以通过浏览器的开发者工具 中的 Security 栏目看到 Connection 栏目下是否有显示 TLS 1.3ECC 双证书ECC 双证书配置了以后无非就是在旧浏览器设别上的验证了。这里用足够老的上古XP虚拟机来给大家证明一波。XP系统上:现代操作系统上的:Brotli通过浏览器的开发者工具,我们可以在 Network 栏目中,打开具体页面的头信息,看到 accept-encoding 中有 br 字眼就行。总结通过上述手段应该可以让 HTTPS 访问的体验优化不少,而且会比没做 HTTPS 的网站访问可能更快。这样的模式比较适合云服务器单机或者简单集群上搭建,如果有应用 SLB 七层代理、WAF、CDN 这样的产品可能会让我们的这些操作都白费。 我们的这几项操作都是自建的 Web 七层服务,如果有设置 SLB 七层代理、WAF、CDN 这样设置在云服务器之前就会被覆盖掉。由于 SLB 七层和CDN这样的产品会更加追求广泛的兼容性和稳定性并不会第一时间就用上上述的这些新特性(HTTP/2 是普遍有的),但是他们都配备了阿里云的 Tengine 的外部专用算法加速硬件如 Intel® QuickAssist Technology(QAT) 加速器可以显著提高SSL/TLS握手阶段性能。 所有 HTTPS 的加密解密都在 SLB 或 CDN 上完成,而不会落到ECS上,可以显著降低 ECS 的负载压力,并且提升访问体验。目前云上的网络产品中能支持四层的都是可以继续兼容我们这套设计的,例如:SLB 的四层转发(TCP UDP)、DDOS高防的四层转发。本文作者:妙正灰阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 5, 2018 · 3 min · jiezi

全图化引擎(AI·OS)中的编译技术

全图化引擎又称算子执行引擎,它的介绍可以参考从HA3到AI OS - 全图化引擎破茧之路。本文从算子化的视角介绍了编译技术在全图化引擎中的运用主要内容有:1.通过脚本语言扩展通用算子上的用户订制能力,目前这些通用算子包括得分算子,过滤算子等。这一方面侧重于编译前端,我们开发了一种嵌入引擎的脚本语言cava ,解决了用户扩展引擎功能的一些痛点,包括插件的开发测试效率,兼容性,引擎版本升级效率等。2.通过代码技术优化全图化引擎性能,由于全图化引擎是基于tensorflow开发,它天生具备tensorflow xla编译能力,利用内核的熔丝提升性能,这部分内容可以参考XLA概述 .xla主要面向tensorflow内置的内核,能发挥的场景是在线预测模型算分。但是对于用户自己开发的算子,XLA很难发挥作用。本文第二部分主要介绍对于自定义算子我们是如何做的代码生成优化的。通用算子上的脚本语言静脉由于算子开发和组图逻辑对普通用户来说成本较高,全图化引擎内置了一些通用算子,比如说射手算子,过滤器算子。这些通用算子能加载C ++插件,也支持用静脉脚本写的插件。关于静脉参考可以这篇文章了解一下。和C ++插件相比,静脉插件有如下特点:1.类java的语法。扩大了插件开发的受众,让熟悉java的同学能快速上手使用引擎。2.性能高.cava是强类型,编译型语言,它能和c ++无损交互。这保证了cava插件的执行性能,在单值场景使用cava写的插件和c ++的插件性能相当。3.使用池管理内存.cava的内存管理可定制,服务端应用每个请求一个池是最高效的内存使用策略。4.安全。对数组越界,对象访问,除零异常做了保护。5.支持jit,编译快。支持upc时编译代码,插件的上线就和上线普通配置一样,极大的提升迭代效率6.兼容性:由于cava的编译过程和引擎版本是强绑定的,只要引擎提供的cava类库接口不变,cava的插件的兼容性很容易得到保证。而c ++插件兼容性很难保证,任何引擎内部对象内存布局的变动就可能带来兼容性问题。射手算子中的静脉插件cava scorer目前有如下场景在使用1.主搜海选场景,算法逻辑可以快速上线验证2.赛马引擎2.0的算分逻辑,赛马引擎重构后引入cava算分替代原先的战马算分样例如下:package test;import ha3.;/ * 将多值字段值累加,并乘以query里面传递的ratio,作为最后的分数 * /class DefaultScorer { MInt32Ref mref; double ratio; boolean init(IApiProvider provider) { IRefManager refManger = provider.getRefManager(); mref = refManger.requireMInt32(“ids”); KVMapApi kv = provider.getKVMapApi(); ratio = kv.getDoubleValue(“ratio”);//获取kvpair内参数 return true; } double process(MatchDoc doc) { int score = 0; MInt32 mint = mref.get(doc); for (int i = 0; i < mint.size(); i++) { score = score + mint.get(i); } return score * ratio; }}其中cava scorer的算分逻辑(process函数)调用次数是doc级别的,它的执行性能和c ++相比唯一的差距是多了安全保护(数组越界,对象访问,除零异常)。可以说cava是目前能嵌入C ++系统执行的性能最好的脚本语言。过滤算子中静脉插件filter算子中主要是表达式逻辑,例如filter =(0.5 * a + b)> 10.以前表达式的能力较弱,只能使用算术,逻辑和关系运算符。使用cava插件可进一步扩展表达式的能力,它支持类的Java语法,可以定义变量,使用分支循环等。计算 filter = (0.5 * a + b) > 10,用cava可定义如下:class MyFunc { public boolean init(FunctionProvider provider) { return true; } public boolean process(MatchDoc doc, double a, double b) { return (0.5 * a + b) > 10; }}filter = MyFunc(a, b)另外由于静脉是编译执行的,和原生的解释执行的表达式相比有天然的性能优势。关于静脉前端的展望静脉是全图化引擎上面向用户需求的语言,有用户定制扩展逻辑的需求都可以考虑用通用算子+静脉插件配合的模式来支持,例如全图化SQL上的UDF,规则引擎的匹配需求等等。后续静脉会进一步完善语言前端功能,完善类库,尽可能兼容的Java。依托苏伊士和全图化引擎支持更多的业务需求。自定义算子的代码生成优化过去几年,在OLAP领域codegen一直是一个比较热门的话题。原因在于大多数数据库系统采用的是Volcano Model模式。其中的下一个()通常为虚函数调用,开销较大。全图化引擎中也有类似的代码生成场景,例如统计算子,过滤算子等。此外,和XLA类似,全图化引擎中也有一些场景可以通过算子融合优化性能。目前我们的代码生成工作主要集中在CPU上对局部算子做优化,未来期望能在SQL场景做全图编译,并且在异构计算的编译器领域有所发展。单算子的代码生成优化统计算子例如统计语句:group_key:键,agg_fun:总和(VAL)#COUNT(),按键分组统计键出现的次数和缬氨酸的和在统计算子的实现中,键的取值有一次虚函数调用, sum和count的计算是两次虚函数调用,sum count计算出来的值和需要通过matchdoc存取,而matchdoc的访问有额外的开销:一次是定位到matchdoc storage,一次是通过偏移定位到存取位置。那么统计代码生成是怎么去除虚函数调用和matchdoc访问的呢?在运行时,我们可以根据用户的查询获取字段的类型,需要统计的功能等信息,根据这些信息我们可以把通用的统计实现特化成专用的统计实现。例如统计sum和count只需定义包含sum count字段的AggItem结构体,而不需要matchdoc;统计函数sum和count变成了结构体成员的+ =操作。假设键和VAL字段的类型都是整型,那么上面的统计语句最终的代码生成成的静脉代码如下:class AggItem { long sum0; long count1; int groupKey;}class JitAggregator { public AttributeExpression groupKeyExpr; public IntAggItemMap itemMap; public AggItemAllocator allocator; public AttributeExpression sumExpr0; … static public JitAggregator create(Aggregator aggregator) { …. } public void batch(MatchDocs docs, uint size) { for (uint i = 0; i < size; ++i) { MatchDoc doc = docs.get(i); //由c++实现,可被inline int key = groupKeyExpr.getInt32(doc); AggItem item = (AggItem)itemMap.get(key); if (item == null) { item = (AggItem)allocator.alloc(); item.sum0 = 0; item.count1 = 0; item.groupKey = key; itemMap.add(key, (Any)item); } int sum0 = sumExpr0.getInt32(doc); item.sum0 += sum0; item.count1 += 1; } }}这里总计数的虚函数被替换成和+ +和计数+ =,matchdoc的存取变成结构体成员的读写item.sum0和item.count0。经过llvm jit编译优化之后生成的ir如下:define void @_ZN3ha313JitAggregator5batchEP7CavaCtxPN6unsafe9MatchDocsEj(%“class.ha3::JitAggregator”* %this, %class.CavaCtx* %"@cavaCtx@", %“class.unsafe::MatchDocs”* %docs, i32 %size){entry: %lt39 = icmp eq i32 %size, 0 br i1 %lt39, label %for.end, label %for.body.lr.phfor.body.lr.ph: ; preds = %entry %wide.trip.count = zext i32 %size to i64 br label %for.bodyfor.body: ; preds = %for.inc, %for.body.lr.ph %lsr.iv42 = phi i64 [ %lsr.iv.next, %for.inc ], [ %wide.trip.count, %for.body.lr.ph ] %lsr.iv = phi %“class.unsafe::MatchDocs”* [ %scevgep, %for.inc ], [ %docs, %for.body.lr.ph ] %lsr.iv41 = bitcast %“class.unsafe::MatchDocs”* %lsr.iv to i64* // … prepare call for groupKeyExpr.getInt32 %7 = tail call i32 %5(%“class.suez::turing::AttributeExpressionTyped.64”* %1, i64 %6) // … prepare call for itemMap.get %9 = tail call i8* @_ZN6unsafe13IntAggItemMap3getEP7CavaCtxi(%“class.unsafe::IntAggItemMap”* %8, %class.CavaCtx* %"@cavaCtx@", i32 %7) %eq = icmp eq i8* %9, null br i1 %eq, label %if.then, label %if.end10// if (item == null) {if.then: ; preds = %for.body // … prepare call for allocator.alloc %15 = tail call i8* @_ZN6unsafe16AggItemAllocator5allocEP7CavaCtx(%“class.unsafe::AggItemAllocator”* %14, %class.CavaCtx* %"@cavaCtx@") // item.groupKey = key; %groupKey = getelementptr inbounds i8, i8* %15, i64 16 %16 = bitcast i8* %groupKey to i32* store i32 %7, i32* %16, align 4 // item.sum0 = 0; item.count1 = 0; call void @llvm.memset.p0i8.i64(i8* %15, i8 0, i64 16, i32 8, i1 false) // … prepare call for itemMap.add tail call void @_ZN6unsafe13IntAggItemMap3addEP7CavaCtxiPNS_3AnyE(%“class.unsafe::IntAggItemMap”* %17, %class.CavaCtx* %"@cavaCtx@", i32 %7, i8* %15) br label %if.end10if.end10: ; preds = %if.end, %for.body %item.0.in = phi i8* [ %15, %if.end ], [ %9, %for.body ] %18 = bitcast %“class.unsafe::MatchDocs”* %lsr.iv to i64* // … prepare call for sumExpr0.getInt32 %26 = tail call i32 %24(%“class.suez::turing::AttributeExpressionTyped.64”* %20, i64 %25) // item.sum0 += sum0; item.count1 += 1; %27 = sext i32 %26 to i64 %28 = bitcast i8* %item.0.in to <2 x i64>* %29 = load <2 x i64>, <2 x i64>* %28, align 8 %30 = insertelement <2 x i64> undef, i64 %27, i32 0 %31 = insertelement <2 x i64> %30, i64 1, i32 1 %32 = add <2 x i64> %29, %31 %33 = bitcast i8* %item.0.in to <2 x i64>* store <2 x i64> %32, <2 x i64>* %33, align 8 br label %for.incfor.inc: ; preds = %if.then, %if.end10 %scevgep = getelementptr %“class.unsafe::MatchDocs”, %“class.unsafe::MatchDocs”* %lsr.iv, i64 8 %lsr.iv.next = add nsw i64 %lsr.iv42, -1 %exitcond = icmp eq i64 %lsr.iv.next, 0 br i1 %exitcond, label %for.end, label %for.bodyfor.end: ; preds = %for.inc, %entry ret void}代码生成的代码中有不少函数是通过C ++实现的,如docs.get(i)中,itemMap.get(键)等。但是优化后的IR中并没有docs.get(I)的函数调用,这是由于经常调用的c ++中实现的函数会被提前编译成bc,由cava编译器加载,经过llvm inline优化pass后被消除。可以认为cava代码和llvm ir基本能做到无损映射(cava中不容易实现逻辑可由c ++实现,预编译成bc加载后被内联),有了cava这一层我们可以用常规面向对象的编码习惯来做codegen,不用关心llvm api细节,让codegen门槛进一步降低。这个例子中,统计规模是100瓦特文档1瓦特个键时,线下测试初步结论是延迟大约能降1倍左右(54ms-> 27ms),有待表达式计算进一步优化。2.过滤算子在通用过滤算子中,表达式计算是典型的可被codegen优化的场景。例如ha3的过滤语句:filter =(a + 2 * b - c)> 0:表达式计算是通过AttributeExpression实现的,AttributeExpression的评价是虚函数。对于单文档接口我们可以用和统计类似的方式,使用静脉对表达式计算做代码生成。对于批量接口,和统计不同的是,表达式的批量计算更容易运用向量化优化,利用CPU的SIMD指令,使计算效率有成倍的提升。但是并不是所有的表达式都能使用一致的向量化优化方法,比如filter = a> 0 AND b <0这类表达式,有短路逻辑,向量化会带来不必要的计算。因此表达式的编译优化需要有更好的codegen 抽象,我们发现Halide能比较好的满足我们的需求.Halide的核心思想:算法描述(做什么ir)和性能优化(怎么做schedule)解耦。种解耦能让我们更灵活的定制优化策略,比如某些场景走向量化,某些场景走普通的代码生成;更进一步,不同计算平台上使用不同的优化策略也成为可能。3.倒排召回算子在寻求算子中,倒排召回是通过QueryExecutor实现的,QueryExecutor的seek是虚函数。例如query = a AND b OR c。QueryExecutor的和或ANDNOT有比较复杂的逻辑,虚函数的开销相对占比没有表达式计算那么大,之前用VTUNE做过预估,求虚函数调用开销占比约10%(数字不一定准确,内联效果没法评估)和精确统计,表达式计算相比,查询的组合空间巨大,寻求的代码生成得更多的考虑对高性价比的查询做编译优化。海选与排序算子在HA3引擎中海选和精排逻辑中有大量比较操作例如排序= + RANK; ID字句,对应的比较函数是秩Compartor和标识Compartor的联合比较.compare的函数调用可被代码生成掉,并且还可和STL算法联合inline.std ::排序使用非在线的补偿函数带来的开销可以参考如下例子:bool myfunction (int i,int j) { return (i<j); }int docCount = 200000;std::random_device rd;std::mt19937_64 mt(rd());std::uniform_int_distribution<int> keyDist(0, 200000);std::vector<int> myvector1;for (int i = 0 ; i < docCount; i++) { myvector1.push_back(keyDist(mt));}std::vector<int> myvector2 = myvector1;std::sort (myvector1.begin(), myvector1.end()); // cost 15.475msstd::sort (myvector2.begin(), myvector2.end(), myfunction); // cost 19.757ms对20瓦特随机数排序,简单的比较直列带来30%的提升。当然在引擎场景,由于比较逻辑复杂,这部分收益可能不会太多。算子的保险丝和代码生成算子的fuse是tensorflow xla编译的核心思想,在全图化场景我们有一些自定义算子也可以运用这个思想,例如特征生成器。FG生成特征的英文模型训练中很重要的一个环节。在线FG是以子图+配置形式描述计算,这部分的代码生成能使数据从索引直接计算到张量上,省去了很多环节中间数据的拷贝。目前这部分的代码生成可以工作参考这篇文章关于编译优化的展望SQL场景全图的编译执行数据库领域全阶段代码生成早被提出并应用,例如Apache的火花作为编译器 ;还有现在比较火的GPU数据库MAPD,把整个执行计划编译成架构无关的中间表示(LLVM IR),借助LLVM编译到不同的目标执行。从实现上看,SQL场景的全图编译执行对全图化引擎还有更多意义,比如可以省去tensorflow算子执行带来的线程切换的开销,可以去除算子间matchdoc传递(matchdoc作为通用的数据布局性能较差)带来的性能损耗。面向异构计算的编译器随着摩尔定律触及天花板,未来异构计算一定是一个热门的领域.SQL大规模数据分析和在线预测就是异构计算可以发挥作用的典型场景,比如分析场景大数据量统计,在线预测场景深度模型大规模并行计算.cpu驱动其他计算平台如gpu fpga,相互配合各自做自己擅长的事情,在未来有可能是常态。这需要为开发人员提供更好的编程接口。全图化引擎已经领先了一步,集成了tensorflow计算框架,天生具备了异构计算的能力。但在编译领域,通用的异构计算编程接口还远未到成熟的地步。工业界和学术界有不少尝试,比如tensorflow的xla编译框架,TVM,Weld等等。借用焊接的概念图表达一下异构计算编译器设计的愿景:让数据分析,深度学习,图像算法等能用统一易用的编程接口充分发挥异构计算平台的算力。总结编译技术已经开始在引擎的用户体验,迭代效率,性能优化中发挥作用,后续会跟着全图化引擎的演进不断发展。能做的事情很多,挑战很大,感兴趣有同学的可以联系我们探讨交流。参考使用带宽优化存储平衡向量化查询执行,第3章有效地编译现代硬件的高效查询计划TensorFlow编译优化策略 - XLAWeld:重新思考数据密集型库之间的接口TVM:用于深度学习的自动化端到端优化编译器本文作者:sance阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 3, 2018 · 4 min · jiezi

最大限度的减少浏览器的重新布局(Reflow&Repaint)

减少浏览器重新布局是优化web性能的一个重要手段。这是因为重新布局是浏览器在请求网络资源后所做的一个必要的工作,这也是浏览器渲染web页面的重要机制(详情可参考浏览器的运行原理)。在浏览器获得新的资源后,它会重新计算文档中个元素的位置和形状,以便刷新web页面(可以是部分内容,也可以是全部),这个过程就是重新布局,有的人把这个过程称为web页面的重绘。但是在重新布局的过程中,浏览器会阻止用户在浏览器中的其它操作,那么很显然,了解重新布局对于提升web应用的性能很重要,尤其是它可以显著的提升用户的体验效果。当然除了了解重新布局外,我们还需要了解各种文档属性对浏览器重新布局的影响因素,如:DOM深度、CSS规则,以及样式的改变等。有的时候,对HTML文档中的单个元素进行重新布局可能会影响到它的父级元素,或者它的兄弟元素,以及它的子元素的重新布局。触发浏览器重新布局的因素用户操作页面的初始化加载调整浏览器窗口的大小HTML文档修改使用js修改样式而引起的计算,如:margin: 0 auto;在DOM中添加或移除元素修改某个元素的类(class & id)那么,有没有一个规范可以缩短页面进行重新布局的呢?答案是肯定的。减少浏览器重新布局的规范减少不必要的 DOM 深度。在 DOM 树中的一个级别进行修改可能会致使该树的所有级别(上至根节点,下至所修改节点的子级)都随之变化。这会导致花费更多的时间来执行重排。尽可能减少 CSS 规则的数量,并移除未使用的 CSS 规则。如果你想进行复杂的渲染修改(如:动画),请在浏览器重新布局流程外执行此操作。你可以使用 position-absolute 或 position-fixed 来实现此目的。避免使用不必要且复杂的 CSS 选择器,尤其是后代选择器,因为此类选择器会消耗更多的 CPU 处理能力来执行选择器匹配。具体的开发中要注意的地方可参考下面两篇文章,它们会告诉你如何书写css会有效减少浏览器重新布局。参考资料前端性能优化:细说浏览器渲染的重排与重绘回流 & 重绘:CSS性能让你的JAVASCRIPT慢了吗?

December 2, 2018 · 1 min · jiezi

关于Flutter初始化流程,我必须告诉你的是...

引言最近在做性能优化的时候发现,在混合栈开发中,第一次启动Flutter页面的耗时总会是第二次启动Flutter页面耗时的两倍左右,这样给人感觉很不好。分析发现第一次启动Flutter页面会做一些初始化工作,借此,我梳理了下Flutter的初始化流程。2. Flutter初始化时序Flutter初始化主要分四部分,FlutterMain初始化、FlutterNativeView初始化、FlutterView初始化和Flutter Bundle初始化。我们先看下Flutter初始化的时序图,来整体把握下Flutter初始化的一般流程: Flutter初始化时序3. 具体分析3.1 FlutterMain初始化这部分初始化工作是由Application.onCreate方法中调用开始的,在Application创建的时候就会初始化完成,不会影响Flutter页面的第一次启动,所以这里只是做一个简单分析。 从FlutterMain.startInitialization方法代码中可以轻易看出来,初始化主要分四部分。 前面三部分比较类似,分别是初始化配置信息、初始化AOT编译和初始化资源,最后一部分则是加载Flutter的Native环境。 这部分感兴趣的同学可以看下FlutterMain.java源码,逻辑还是比较清晰的。public static void startInitialization(Context applicationContext, Settings settings) { // other codes … initConfig(applicationContext); initAot(applicationContext); initResources(applicationContext); System.loadLibrary(“flutter”); // other codes …}3.2 FlutterNativeView初始化先用一个图来展现FlutterNativeView构造函数的调用栈: FlutterNativeView构造函数调用栈从上图的调用栈中我们知道FlutterNativeView的初始化主要做了些什么,我们再从源码角度较为深入的了解下: FlutterNativeView的构造函数最终主要调用了一个nativeAttach方法。到这里就需要分析引擎层代码了,我们可以在JNI文件中找到对应的jni方法调用。(具体文件为platform_view_android_jni.cc)static const JNINativeMethod native_view_methods[] = { { .name = “nativeAttach”, .signature = “(Lio/flutter/view/FlutterNativeView;)J”, .fnPtr = reinterpret_cast<void*>(&shell::Attach), }, // other codes …};从代码中很容易看出FlutterNativeView.attach方法最终调用了shell::Attach方法,而shell::Attach方法主要做了两件事: 1. 创建PlatformViewAndroid。 2. 调用PlatformViewAndroid::Attach。static jlong Attach(JNIEnv* env, jclass clazz, jobject flutterView) { auto view = new PlatformViewAndroid(); // other codes … view->Attach(); // other codes …}那我们再分析下PlatformViewAndroid的构造函数和Attach方法都做了些什么呢?PlatformViewAndroid::PlatformViewAndroid() : PlatformView(std::make_unique<NullRasterizer>()), android_surface_(InitializePlatformSurface()) {}void PlatformViewAndroid::Attach() { CreateEngine(); // Eagerly setup the IO thread context. We have already setup the surface. SetupResourceContextOnIOThread(); UpdateThreadPriorities();}其中: 1. PlatformViewAndroid的构造函数主要是调用了InitializePlatformSurface方法,这个方法主要是初始化了Surface,其中Surface有Vulkan、OpenGL和Software三种类型的区别。 2. PlatformViewAndroid::Attach方法这里主要调用三个方法:CreateEngine、SetupResourceContextOnIOThread和UpdateThreadPriorities。 2.1 CreateEngine比较好理解,创建Engine,这里会重新创建一个Engine对象。 2.2 SetupResourceContextOnIOThread是在IO线程去准备资源的上下文逻辑。 2.3 UpdateThreadPriorities是设置线程优先级,这设置GPU线程优先级为-2,UI线程优先级为-1。3.3 FlutterView初始化FlutterView的初始化就是纯粹的Android层啦,所以相对比较简单。分析FlutterView.java的构造函数就会发现,整个FlutterView的初始化在确保FlutterNativeView的创建成功和一些必要的view设置之外,主要做了两件事: 1. 注册SurfaceHolder监听,其中surfaceCreated回调会作为Flutter的第一帧回调使用。 2. 初始化了Flutter系统需要用到的一系列桥接方法。例如:localization、navigation、keyevent、system、settings、platform、textinput。 FlutterView初始化流程主要如下图所示: FlutterView初始化3.4 Flutter Bundle初始化Flutter Bundle的初始化是由调用FlutterActivityDelegate.runFlutterBundle开始的,先用一张图来说明下runFlutterBundle方法的调用栈: Flutter的Bundle初始化我们再从源码角度较为深入了解下: FlutterActivity的onCreate方法在执行完FlutterActivityDelegate的onCreate方法之后会调用它的runFlutterBundle方法。FlutterActivityDelegate.runFlutterBundle代码如下:public void runFlutterBundle(){ // other codes … String appBundlePath = FlutterMain.findAppBundlePath(activity.getApplicationContext()); if (appBundlePath != null) { flutterView.runFromBundle(appBundlePath, null, “main”, reuseIsolate); }}很明显,这个runFlutterBundle并没有做太多事情,而且直接调用了FlutterView.runFromBundle方法。而后兜兜转转最后会调用到PlatformViewAndroid::RunBundleAndSnapshot方法。void PlatformViewAndroid::RunBundleAndSnapshot(JNIEnv* env, std::string bundle_path, std::string snapshot_override, std::string entrypoint, bool reuse_runtime_controller, jobject assetManager) { // other codes … blink::Threads::UI()->PostTask( [engine = engine_->GetWeakPtr(), asset_provider = std::move(asset_provider), bundle_path = std::move(bundle_path), entrypoint = std::move(entrypoint), reuse_runtime_controller = reuse_runtime_controller] { if (engine) engine->RunBundleWithAssets( std::move(asset_provider), std::move(bundle_path), std::move(entrypoint), reuse_runtime_controller); });}PlatformViewAndroid::RunBundleAndSnapshot在UI线程中调用Engine::RunBundleWithAssets,最终调用Engine::DoRunBundle。 DoRunBundle方法最后只会调用RunFromPrecompiledSnapshot、RunFromKernel和RunFromScriptSnapshot三个方法中的一个。而这三个方法最终都会调用SendStartMessage方法。bool DartController::SendStartMessage(Dart_Handle root_library, const std::string& entrypoint) { // other codes … // Get the closure of main(). Dart_Handle main_closure = Dart_GetClosure( root_library, Dart_NewStringFromCString(entrypoint.c_str())); // other codes … // Grab the ‘dart:isolate’ library. Dart_Handle isolate_lib = Dart_LookupLibrary(ToDart(“dart:isolate”)); DART_CHECK_VALID(isolate_lib); // Send the start message containing the entry point by calling // _startMainIsolate in dart:isolate. const intptr_t kNumIsolateArgs = 2; Dart_Handle isolate_args[kNumIsolateArgs]; isolate_args[0] = main_closure; isolate_args[1] = Dart_Null(); Dart_Handle result = Dart_Invoke(isolate_lib, ToDart("_startMainIsolate"), kNumIsolateArgs, isolate_args); return LogIfError(result);}而SendStartMessage方法主要做了三件事: 1. 获取Flutter入口方法(例如main方法)的closure。2. 获取FlutterLibrary。 3. 发送消息来调用Flutter的入口方法。4. 总结一下本次主要分析了下FlutterActivity的onCreate方法中的Flutter初始化部分逻辑,很明显会发现主要耗时在FlutterNativeView、FlutterView和Flutter Bundle的初始化这三块,将这三部分的初始化工作前置就可以比较容易的解决引言中提出的问题。经测试发现,这样改动之后,Flutter页面第一次启动时长和后面几次启动时长差不多一样了。 对于FlutterMain.startInitialization的初始化逻辑、SendStartMessage发送的消息如何最终调用Flutter中的入口方法逻辑没有进一步深入分析,这些内容后续再继续分析撰文分享。本文作者:闲鱼技术-然道阅读原文本文为云栖社区原创内容,未经允许不得转载。

November 26, 2018 · 2 min · jiezi

SVG vs Image, SVG vs Iconfont

这可能是个别人写过很多次的话题,但貌似由于兼容性的原因?图标的显示还是用着 Iconfont 或者 CSS Sprite 的形式?希望通过自己新瓶装旧酒的方式能重新引导一下问题。SVG vs Image比方说现在要做下图这样的视觉效果:分析:可能需要三张图片鼠标移入时的背景图渐变色前景图鼠标移入时白色前景图独立图像现在对比一下背景图使用图片与使用 SVG 格式的体积大小(做图的时候拿错颜色了,其他都一样,能说明道理就行,见谅见谅)可以看出,在肉眼感觉差异不大的情况下,WebP 格式体积最小,其次是 SVG,而 PNG 的体积过大。 这个 SVG 是在 Sketch 设计稿中导出来的,源码包含了很多冗余无效的代码,实际上是可以优化的,如下。内部源码优化后优化后大约可以减去 1K 个字符。当然这个需要内联使用(Inline SVG)CSS Sprite使用 CSS Sprite 的方式可以减少 HTTP 请求,貌似还可以减少总体图片体积。这里用前景图来对比一下,实际上背景图和前景图都可以合成一张 sprite。可以看出,CSS Sprite 的体积比 Inline SVG + CSS 的方式大很多。SVG vs Image 结论绿色部分表示 SVG 比 Image 略胜一筹的地方,黄色部分表示有所欠缺的地方,灰绿色表示差不多。1、如今已接近 2019 年了,对于 IE9 (2011年) 这种古老的浏览器都支持 SVG,所以再过多强调更低的兼容性也没有什么意思。2、Inline SVG 在浏览器应该是被渲染成 DOM 节点,所以关于 DOM 节点的性能优化都有必要注意;一个 SVG 图像可能就会有很多路径,即 DOM 节点,太多的 DOM 节点必然会影响浏览器的渲染性能及内存占用,而纯位图的渲染方式应该是没有这方面的顾虑。(DOM 数量影响参考:Google WEB 开发者文档)综上结论:除开复杂图像,选择 Inline SVG 或者 <img/> 标签的方式引入 SVG,会比使用 独立图像 或 组合图像 (CSS sprite) 的方式更好。SVG vs Iconfont书写对比首先看下 Iconfont 与 SVG 图标的使用方式,来源 阿里 Iconfont 平台很明显 SVG Sprite 使用起来没有 Iconfont 方便,需要写 3 行代码, 而后者只需要写 1 行。当然上面的不是重点,重点是下面的换色与多色支持换色与多色支持换色1、Iconfont 通过 CSS color 可以轻松更换图标颜色。2、而 SVG Sprite 比较麻烦,SVG Sprite 的代码原理如下。// 定义 symbol<svg> <symbol id=“icon-arrow-left” viewBox=“0 0 1024 1024”> <path d=“M694 … 44.576-45.952”></path> </symbol> <symbol id=“icon-arrow-right” viewBox=“0 0 1024 1024”> <path d=“M693 … 0-0.48-46.4”></path> </symbol></svg>// 使用<svg><use xlink:href="#icon-arrow-left"/></svg><svg><use xlink:href="#icon-arrow-right"/></svg>渲染出来的 DOM 结构是这样的:渲染在了 Shadow DOM 中(关于 Shadow DOM 的知识可以阅读下这篇文章或这篇),这样的 DOM 元素样式就具有了作用域,外面的 CSS 对 shadow-root 内的元素不会生效,如果想要更换元素的颜色,需要使用 /deep/ 来穿透添加样式,如下。svg /deep/ path { fill: red;}当然,实际上在只需要在父级元素上添加 fill: red 这样的 CSS 也能起到同样的效果,里面的元素会继承父级的样式。PS: /deep/ 是 shadow DOM v0 的写法,v1 已经把这样的写法抛弃了,实际上支持 v1 的 shadow DOM, 父级的样式可以直接作用在 shadow-root 里面的元素。多色支持1、Iconfont 是不支持多色图标的。2、而 SVG Sprite 可以利用 CSS 变量或 shadow DOM 的方式支持多色图标,shadow DOM 的方式上面已经说明,下面借用他人的文章解释 CSS 变量实现多色,如下。不过使用 CSS 变量或 shadow DOM 的方式兼容性都不好,CSS 变量:Edge15+shadow DOM:更差。兼容性列表3、Inline SVG 可以良好地支持多色及多色变化。渐变色支持Iconfont 与 SVG Sprite 不支持渐变色。Inline SVG 支持渐变色,并且兼容性良好。渲染无抖动使用 Iconfont,因为字体文件是异步加载的,所以在字体文件还没有加载完毕之前,图标位会留空,加载完毕后才会显示出来,这个过程就会出现向下图(来自 GitHub blog)这样的抖动,而 SVG Sprite 或 Inline SVG 内联加载则不会出现这样的抖动。当然,Iconfont 也可以内联加载,不过需要转换成 base64 同样式表一起加载,转换后的文件体积则会变为原来的 1.3 倍左右这是由 base64 编码决定的(编码知识链接)。字体转换成 base64 的一个在线工具:https://transfonter.org/体积较大这个是 SVG 对比于 Iconfont 的一个不足之处,如下图。Inline SVG 与 SVG Sprite 体积差不多。开发成本三者的开发成本都差不多,不过 SVG 的两种方式都需要前期做些配置,后期开发就会顺手很多(单页应用)。以 vue + vue cli 为例说明 Inline SVG 便捷使用。1、 配置 Webpack loader:{ // 排除需要转换成 Inline SVG 的目录 exclude: [resolve(‘src/svgicons’)], test: /.(png|jpe?g|gif|svg)(?.*)?$/, loader: ‘url-loader’, options: { limit: 1, name: utils.assetsPath(‘img/[name].[hash:7].[ext]’) }},{ // 指定特定的目录用于 Inline SVG include: [resolve(‘src/svgicons’)], test: /.svg$/, use: [ // 读取 SVG 源代码 { loader: ‘raw-loader’ }, // 精简优化 SVG 源代码 { loader: ‘svgo-loader’, options: { plugins: [ { removeTitle: true }, { removeViewBox: false }, { removeDimensions: true }, // …其他参数 ] } } ]}2、 创建 SvgIcon.vue 组件:<template> <div class=“svg-icon”> <div class=“svg-icon-wrapper” v-html=“icon”></div> </div></template><script>export default { name: ‘SvgIcon’, props: { name: { type: String, required: true, }, }, data () { return { icon: this.getIcon(), } }, watch: { name () { this.icon = this.getIcon() }, }, methods: { getIcon () { return require(@/svgicons/${this.name}.svg) }, },}</script><style lang=“stylus” scoped>.svg-icon { overflow hidden display inline-block width 1em height 1em &-wrapper { display flex align-items center >>> svg { width 100% height 100% fill currentColor } }}</style>3、使用:<SvgIcon name=“arrow-right” />SVG vs Iconfont 结论应该是 Inline SVG vs SVG Sprite vs Iconfont 的结论,如下图。综上结论选择 Inline SVG 或许是一个不错地选择去替代 Iconfont 的使用方式。扩展阅读GitHub 网站很早之前已经将图标的展示方式由 Iconfont 转成了 Inline SVG, 这一篇文章是他们的描述:https://blog.github.com/2016-…很早的一篇文章关于两者的对比:https://css-tricks.com/icon-f…最后欢迎各抒己见谈论一下对 SVG 和 Iconfont 的看法,优缺点,欢迎留言。然后,本文同步发表于【凹凸实验室博客】或微信公众号,欢迎关注我们,么么哒。 ...

November 23, 2018 · 2 min · jiezi

始于阿里,回归社区:阿里8个项目进入CNCF云原生全景图

摘要: 一群技术理想主义者,与太平洋另一边的技术高手们正面PK,在这场躲不开的战役中,一起认真一把。破土而出的生命力,源自理想主义者心底对技术的信念。云原生技术正席卷全球,云原生基金会在去年KubeCon +CloudNativeCon NA的现场宣布:其正在孵化的项目已达14个,入驻的厂家或产品已超过300家,并吸引了2.2万开发者参与项目代码贡献,其明星产品Kubenetes 的GitHub 上Authors 和 Issues 量已排行开源领域的第二名。今年,KubeCon + CloudNativeCon 首次来到中国。在2018 KubeCon + CloudNativeCon的现场,阿里云研究员伯瑜向在场的开发者们宣布,CNCF已将阿里巴巴云原生镜像分发系统Dragonfly接纳为其沙箱项目(Sandbox),并有机会成为国内首个从CNCF毕业的开源项目。目前已经毕业的两个项目,一个是Kubernetes,另一个是Prometheus。据悉,目前阿里巴巴已经有8个项目进入CNCF云原生全景图,分别是分布式服务治理框架Dubbo、分布式消息引擎RocketMQ、流量控制组件Sentinel、企业级富容器技术PouchContainer、服务发现和管理Nacos、分布式消息标准OpenMessaging、云原生镜像分发系统Dragonfly和高可用服务AHAS。时间回到2016年2016年的那届双11,RocketMQ创始人冯嘉和他的团队首次将低延迟存储解决方案应用于双11的支撑,经受住了流量的大考,整个大促期间,99.996%的延迟落在了10ms以内,完成了保障交易稳定的既定目标。对于读写比例几乎均衡的分布式消息引擎来说,这一技术上的突破,即便是放在全球范围内,也绝对是值得称赞的。另一边,在历时3个月的开源重塑后,冯嘉和他的团队启动了RocketMQ向Apache 软件基金会的捐赠之路,但迈出这一步并不容易。“当时国内的开源氛围还没有现在那么活跃,开源之后,很多设计、源码和文档的维护工作还不够理想,但我们就是想证明国内的开源项目和开源社区也可以在世界的开源舞台上发挥价值。”经过近一年的努力,在2017年9月25日,Apache 软件基金会官方宣布,阿里巴巴捐赠给 Apache 社区的开源项目 RocketMQ 从 Apache 社区正式毕业,成为 Apache 顶级项目(TLP),这是国内首个非 Hadoop 生态体系的Apache 社区顶级项目。值得一提的是,根据项目毕业前的统计,RocketMQ有百分八十的新特性与生态集成来自于社区的贡献。2017年,消息领域出现一件里程碑事件分布式消息领域的国际标准OpenMessaging开源项目正式入驻Linux基金会,这是国内首个在全球范围发起的分布式计算领域的国际标准。消息通讯已经成为现代数据驱动架构的关键环节,但在全球范围内,消息领域仍然存在两大问题:一是缺乏供应商中立的行业标准,导致各种消息中间件的高复杂性和不兼容性,相应地造成了公司的产品低效、混乱和供应商锁定等问题。二是目前已有的方案框架并不能很好地适配云架构,即非云原生架构,因此无法有效地对大数据、流计算和物联网等新兴业务需求提供技术支持。这也是冯嘉和他的团队在开源RocketMQ过程中,开发者和合作伙伴经常会提到的问题:“在消息领域,市场上出现了各类不同的开源解决方案,这导致了用户更高的接入和维护成本,为了确保各个消息引擎间能正常通信,还要投入大量的精力去做兼容。”这时候,建立一套供应商中立,和语言无关的消息领域的事实标准,成为各社区成员共同的诉求。此后,在2017年9月,阿里巴巴发起OpenMessaging项目,并邀请了雅虎、滴滴出行、Streamlio共同参与,一年后,参与OpenMessaging开源标准社区的企业达10家之多,包括阿里巴巴、Datapipeline、滴滴出行、浩鲸科技、京东商城、青云QingCloud、Streamlio、微众银行、Yahoo、中国移动苏州研发中心(按首字母排序),此外,还获得了RocketMQ、RabbitMQ和Pulsar 3个顶级消息开源厂商的支持。相比于开源一个分布式消息项目,一套开源标准能被各家厂商所接受,对整个国内开源领域而言,是更具有里程碑意义的事件。2017年9月,Dubbo重启开源Dubbo 是阿里巴巴于2012年开源的分布式服务治理框架,是国内影响力最大、使用最广泛的开源服务框架之一,在2016年、2017年开源中国发起的最受欢迎的中国开源软件评选中,连续两年进入Top10名单。2017年9月7日,在Github将版本更新至2.5.4,重点升级所依赖的JDK及其组件,随后连续发布了11个版本。并于2018年2月捐献给 Apache 软件基金会,希望借助社区的力量来发展 Dubbo,打消大家对于 Dubbo 未来的顾虑。项目重启半年后,Dubbo 项目负责人阿里巴巴高级技术专家北纬在接受媒体采访的时候,从战略、社区、生态和回馈四个方面谈了Dubbo重启开源背后的原因。“集团近几年开始将开源提到了新的战略高度,这次投入资源重启开源,核心是希望让开源发挥更大的社会价值,并和广大开发者一起,建立一个繁荣的Dubbo生态,普惠所有使用 Dubbo 的人和Dubbo本身。”Dubbo项目组成员朱勇在今年上海的技术沙龙上分享Dubbo未来发展的过程中提到,其后续的规划是要解决好两个问题。第一个问题是重点关注技术趋势,例如云原生对Dubbo开源现状的影响。第二个问题是 Dubbo 本身定位的问题,除了保持技术上的领先性,还需要围绕 Dubbo 核心发展生态,和社区成员一起将 Dubbo 发展成一个服务化改造的整体解决方案。2017年11月,阿里自研容器技术PouchContainer开源在开源不到一年的时间里,PouchContainer 1.0 GA 版本发布,达到可生产级别。今年8月,PouchContainer 被纳入开源社区开放容器计划OCI;9月,被收录进高校教材《云计算导论》;11月,Pouch团队携蚂蚁金服容器团队、阿里云ACS团队,与容器生态 Containerd社区 Maintainer进行技术交流,有望发展成 Containerd 社区 Maintainer 席位,代表国内企业在世界容器技术领域发声。PouchContainer发展速度之快,超出了宏亮的想象。宏亮是 Docker Swarm 容器集群项目的核心代码维护者(Maintainer),并于2015年8月出版了《Docker 源码分析》一书,对 Docker 架构和源代码进行了深入的讲解,该书在Docker领域迅速成为畅销书籍。2017年,宏亮承担起阿里自有容器技术的对内支持和对外推广工作,秉承初心,希望在竞争激烈的容器开源领域能抢下属于国内容器技术的一席之地。在他和团队的努力下,阿里集团内部已实现100%的容器化,并已经开始涉及离线业务,实现在、离线业务的混合调度与部署。整个集团能实现100%的容器化,离不开阿里内部自研的P2P分发技术,该项目取名为蜻蜓 Dragonfly,寓意点与点之间的文件分发能如蜻蜓般轻盈和迅速,解决传统文件发布系统中的大规模下载、远距离传输、带宽成本和安全传输的问题。日前,Dragonfly 正式进入 CNCF, 并成为国内第三个被列为沙箱级别(Sandbox Level Project)的开源项目,可见,CNCF 在其云原生的技术版图中正希望借助蜻蜓等优秀的镜像分发技术,以提升企业微服务架构下应用的交付效率。始于阿里,回归社区。今年夏天,国内开源领域,迎来了两位新成员。作为微服务和云原生生态下的两款重要开源框架/组件,Nacos主打云原生应用中的动态服务发现、配置和服务管理,Sentinle则是聚焦在限流和降级两个方面。Nacos和Sentinel均是在阿里近10年的核心业务场景下沉淀所产生的,他们的开源是对微服务和元原生领域开源技术方案的有效补充,同时也非常强调融入开源生态,除了兼容Dubbo和Sentinel,也支持对Spring Cloud 和 Kubenetes等生态,以增强自身的生命力。“阿里巴巴早在 2007 年进行从 IOE 集中式应用架构升级为互联网分布式服务化架构的时候,就意识到在分布式环境中,诸如分布式服务治理,数据源容灾切换、异地多活、预案和限流规则等场景下的配置变更难题,因为在一个大型的分布式系统中,你没有办法把整个分布式系统停下来,去做一个软件、硬件或者系统的升级。”阿里巴巴高级技术专家坤宇在2017 QCon的现场分享到。在配置变更领域,我们从2008年的无 ConfigServer 时代,借用硬件负载设备F5提供的VIP功能,通过域名方式来实现服务提供方和调用方之间的通信,逐步经历了ConfigServer单机版、集群版的多次迭代,不断提高其稳定性。曾写下支付宝钱包服务端第一行代码的阿里高级技术专家慕义,在今年深圳的技术沙龙现场回忆了阿里注册中心自研的10年路:“这期间,集团业务经历了跨越式的发展,每年翻番的服务规模,不断的给ConfigServer的技术架构演进带来更高的要求和挑战,使得我们有更多的机会在生产环境发现和解决一个个问题的过程中,实现架构的一代代升级。Nacos便是在这样的背景下,经过几代技术人的技术攻坚所产生的。”我们希望Nacos可以帮助开发者获得有别于原生或其他第三方服务发现和动态配置管理解决方案所提供的能力,满足开发者们在微服务落地过程当中对工业级注册中心的诉求,缩短想法到实现的路径。巧的是,一边是 Nacos宣布开源,另一边是Spring Cloud生态下的服务注册和发现组件Netflix Eureka宣布闭源,勇敢者的游戏充满了变数,但在坤宇和他的团队看来,这场游戏自己可以走到最后,因为我们并不是一个人在战斗,Nacos只是阿里众多开源项目中的一员,随后还会有更多的开源项目反哺给社区,形成生态,例如轻量级限流降级组件 Sentinel。7月29日,Aliware Open Source•深圳站现场,只能容纳400人的场地,来了700多位开发者。阿里巴巴高级技术专家子矜在现场宣布了轻量级限流降级组件Sentinel的开源。作为阿里巴巴“大中台、小前台”架构中的基础模块,Sentinel经历了10年双11的考验覆盖了阿里的所有核心场景,也因此积累了大量的流量归整场景以及生产实践。Sentinel的出现,离不开阿里历届高可用架构团队的共同努力。“在双11备战中,容量规划是最重要也是最具挑战的环节之一。从第一年开始,双11的0点时刻就代表了我们的历史最高业务访问量,它通常是日常流量的几十倍甚至上百倍。因此,如何让一个技术和业务持续复杂的分布式站点去更平稳支撑好这突如其来的流量冲击,是我们这10年来一直在解的题。”阿里巴巴高可用架构团队资深技术专家游骥在今年的双11结束后分享道。这10年,容量规划经历了人工估算、线下压测、线上压测、全链路压测、全链路压测和隔离环境、弹性伸缩相结合的5个阶段。2013年双11结束后,全链路压测的诞生解决了容量的确定性问题。作为一项划时代的技术,全链路压测的实现,对整个集团而言,都是一件里程碑事件。随后,基于全链路压测为核心,打造了一系列容量规划相关的配套生态,提升能力的同时,降低了整个环节的成本、提升效率。随着容量规划技术的不断演进,2018年起,高可用架构团队希望可以把这些年在生成环境下的实践,贡献给社区,之后便有了Sentinel的开源。一边是作为发起者。将自己生产环境实践下沉淀出来的架构和技术贡献给社区。另一边是作为参与者。基于一些开源项目或云平台,输出可以解决开发者当前工作中存在的痛点的解决方案,例如近期新开源的项目Spring Cloud Alibaba 和 开发者工具 Alibaba Cloud Toolkit。相同的是,技术理想主义者都希望技术可以为让世界变得更好,这才是技术人的兴奋点。“让世界的技术因为阿里巴巴而变得更美好一点点”。这是阿里巴巴毕玄邮件签名中的一句话。他正和一群技术理想主义者,与太平洋另一边的技术高手们正面PK,在这场躲不开的战役中,一起认真一把。本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

November 20, 2018 · 1 min · jiezi

javascript性能优化

本文主要是在我读《高性能Javascript》之后,想要记录下一些有用的优化方案,并且就我本身的一些经验,来大家一起分享下,Javascript的加载与执行大家都知道,浏览器在解析DOM树的时候,当解析到script标签的时候,会阻塞其他的所有任务,直到该js文件下载、解析执行完成后,才会继续往下执行。因此,这个时候浏览器就会被阻塞在这里,如果将script标签放在head里的话,那么在该js文件加载执行前,用户只能看到空白的页面,这样的用户体验肯定是特别烂。对此,常用的方法有以下:将所有的script标签都放到body最底部,这样可以保证js文件是最后加载并执行的,可以先将页面展现给用户。但是,你首先得清楚,页面的首屏渲染是否依赖于你的部分js文件,如果是的话,则需要将这一部分js文件放到head上。使用defer,比如下面这种写法。使用defer这种写法时,虽然浏览器解析到该标签的时候,也会下载对应的js文件,不过它并不会马上执行,而是会等到DOM解析完后(DomContentLoader之前)才会执行这些js文件。因此,就不会阻塞到浏览器。<script src=“test.js” type=“text/javascript” defer></script>动态加载js文件,通过这种方式,可以在页面加载完成后,再去加载所需要的代码,也可以通过这种方式实现js文件懒加载/按需加载,比如现在比较常见的,就是webpack结合vue-router/react-router实现按需加载,只有访问到具体路由的时候,才加载相应的代码。具体的方法如下:1.动态的插入script标签来加载脚本,比如通过以下代码 function loadScript(url, callback) { const script = document.createElement(‘script’); script.type = ’text/javascript’; // 处理IE if (script.readyState) { script.onreadystatechange = function () { if (script.readyState === ’loaded’ || script.readyState === ‘complete’) { script.onreadystatechange = null; callback(); } } } else { // 处理其他浏览器的情况 script.onload = function () { callback(); } } script.src = url; document.body.append(script); } // 动态加载js loadScript(‘file.js’, function () { console.log(‘加载完成’); })2.通过xhr方式加载js文件,不过通过这种方式的话,就可能会面临着跨域的问题。例子如下: const xhr = new XMLHttpRequest(); xhr.open(‘get’, ‘file.js’); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) { const script = document.createElement(‘script’); script.type = ’text/javascript’; script.text = xhr.responseText; document.body.append(script); } } }3.将多个js文件合并为同一个,并且进行压缩。 原因:目前浏览器大多已经支持并行下载js文件了,但是并发下载还是有一定的数量限制了(基于浏览器,一部分浏览器只能下载4个),并且,每一个js文件都需要建立一次额外的http连接,加载4个25KB的文件比起加载一个100KB的文件消耗的时间要大。因此,我们最好就是将多个js文件合并为同一个,并且进行代码压缩。javascript作用域当一个函数执行的时候,会生成一个执行上下文,这个执行上下文定义了函数执行时的环境。当函数执行完毕后,这个执行上下文就会被销毁。因此,多次调用同一个函数会导致创建多个执行上下文。每隔执行上下文都有自己的作用域链。相信大家应该早就知道了作用域这个东西,对于一个函数而言,其第一个作用域就是它函数内部的变量。在函数执行过程中,每遇到一个变量,都会搜索函数的作用域链找到第一个匹配的变量,首先查找函数内部的变量,之后再沿着作用域链逐层寻找。因此,若我们要访问最外层的变量(全局变量),则相比直接访问内部的变量而言,会带来比较大的性能损耗。因此,我们可以将经常使用的全局变量引用储存在一个局部变量里。const a = 5;function outter () { const a = 2; function inner () { const b = 2; console.log(b); // 2 console.log(a); // 2 } inner();}对象的读取javascript中,主要分为字面量、局部变量、数组元素和对象这四种。访问字面量和局部变量的速度最快,而访问数组元素和对象成员相对较慢。而访问对象成员的时候,就和作用域链一样,是在原型链(prototype)上进行查找。因此,若查找的成员在原型链位置太深,则访问速度越慢。因此,我们应该尽可能的减少对象成员的查找次数和嵌套深度。比如以下代码 // 进行两次对象成员查找 function hasEitherClass(element, className1, className2) { return element.className === className1 || element.className === className2; } // 优化,如果该变量不会改变,则可以使用局部变量保存查找的内容 function hasEitherClass(element, className1, className2) { const currentClassName = element.className; return currentClassName === className1 || currentClassName === className2; }DOM操作优化最小化DOM的操作次数,尽可能的用javascript来处理,并且尽可能的使用局部变量储存DOM节点。比如以下的代码: // 优化前,在每次循环的时候,都要获取id为t的节点,并且设置它的innerHTML function innerHTMLLoop () { for (let count = 0; count < 15000; count++) { document.getElementById(’t’).innerHTML += ‘a’; } } // 优化后, function innerHTMLLoop () { const tNode = document.getElemenById(’t’); const insertHtml = ‘’; for (let count = 0; count < 15000; count++) { insertHtml += ‘a’; } tNode.innerHtml += insertHtml; }尽可能的减少重排和重绘,重排和重汇可能会代价非常昂贵,因此,为了减少重排重汇的发生次数,我们可以做以下的优化1.当我们要对Dom的样式进行修改的时候,我们应该尽可能的合并所有的修改并且一次处理,减少重排和重汇的次数。 // 优化前 const el = document.getElementById(’test’); el.style.borderLeft = ‘1px’; el.style.borderRight = ‘2px’; el.style.padding = ‘5px’; // 优化后,一次性修改样式,这样可以将三次重排减少到一次重排 const el = document.getElementById(’test’); el.style.cssText += ‘; border-left: 1px ;border-right: 2px; padding: 5px;‘2.当我们要批量修改DOM节点的时候,我们可以将DOM节点隐藏掉,然后进行一系列的修改操作,之后再将其设置为可见,这样就可以最多只进行两次重排。具体的方法如下: // 未优化前 const ele = document.getElementById(’test’); // 一系列dom修改操作 // 优化方案一,将要修改的节点设置为不显示,之后对它进行修改,修改完成后再显示该节点,从而只需要两次重排 const ele = document.getElementById(’test’); ele.style.display = ’none’; // 一系列dom修改操作 ele.style.display = ‘block’; // 优化方案二,首先创建一个文档片段(documentFragment),然后对该片段进行修改,之后将文档片段插入到文档中,只有最后将文档片段插入文档的时候会引起重排,因此只会触发一次重排。。 const fragment = document.createDocumentFragment(); const ele = document.getElementById(’test’); // 一系列dom修改操作 ele.appendChild(fragment);3.使用事件委托:事件委托就是将目标节点的事件移到父节点来处理,由于浏览器冒泡的特点,当目标节点触发了该事件的时候,父节点也会触发该事件。因此,由父节点来负责监听和处理该事件。那么,它的优点在哪里呢?假设你有一个列表,里面每一个列表项都需要绑定相同的事件,而这个列表可能会频繁的插入和删除。如果按照平常的方法,你只能给每一个列表项都绑定一个事件处理器,并且,每当插入新的列表项的时候,你也需要为新的列表项注册新的事件处理器。这样的话,如果列表项很大的话,就会导致有特别多的事件处理器,造成极大的性能问题。而通过事件委托,我们只需要在列表项的父节点监听这个事件,由它来统一处理就可以了。这样,对于新增的列表项也不需要做额外的处理。而且事件委托的用法其实也很简单:function handleClick(target) { // 点击列表项的处理事件}function delegate (e) { // 判断目标对象是否为列表项 if (e.target.nodeName === ‘LI’) { handleClick(e.target); }}const parent = document.getElementById(‘parent’);parent.addEventListener(‘click’, delegate);本文地址在->本人博客地址, 欢迎给个 start 或 follow ...

November 17, 2018 · 2 min · jiezi

前端性能优化之重排和重绘

前言,最近利用碎片时间拜读了一下尼古拉斯的另一巨作《高性能JavaScript》,今天写的文章从“老生常谈”的页面重绘和重排入手,去探究这两个概念在页面性能提升上的作用。一.重排 & 重绘有经验的大佬对这个概念一定不会陌生,“浏览器输入URL发生了什么”。估计大家已经烂熟于心了,从计算机网络到JS引擎,一路飞奔到浏览器渲染引擎。 经验越多就能理解的越深。感兴趣的同学可以看一下这篇文章,深度和广度俱佳 从输入 URL 到页面加载的过程?如何由一道题完善自己的前端知识体系!切回正题,我们继续探讨何为重排。浏览器下载完页面所有的资源后,就要开始构建DOM树,于此同时还会构建渲染树(Render Tree)。(其实在构建渲染树之前,和DOM树同期会构建Style Tree。DOM树与Style Tree合并为渲染树)DOM树表示页面的结构渲染树表示页面的节点如何显示一旦渲染树构建完成,就要开始绘制(paint)页面元素了。当DOM的变化引发了元素几何属性的变化,比如改变元素的宽高,元素的位置,导致浏览器不得不重新计算元素的几何属性,并重新构建渲染树,这个过程称为“重排”。完成重排后,要将重新构建的渲染树渲染到屏幕上,这个过程就是“重绘”。简单的说,重排负责元素的几何属性更新,重绘负责元素的样式更新。而且,重排必然带来重绘,但是重绘未必带来重排。比如,改变某个元素的背景,这个就不涉及元素的几何属性,所以只发生重绘。二. 重排触发机制上面已经提到了,重排发生的根本原理就是元素的几何属性发生了改变,那么我们就从能够改变元素几何属性的角度入手添加或删除可见的DOM元素元素位置改变元素本身的尺寸发生改变内容改变页面渲染器初始化浏览器窗口大小发生改变三. 如何进行性能优化重绘和重排的开销是非常昂贵的,如果我们不停的在改变页面的布局,就会造成浏览器耗费大量的开销在进行页面的计算,这样的话,我们页面在用户使用起来,就会出现明显的卡顿。现在的浏览器其实已经对重排进行了优化,比如如下代码:var div = document.querySelector(’.div’);div.style.width = ‘200px’;div.style.background = ‘red’;div.style.height = ‘300px’;比较久远的浏览器,这段代码会触发页面2次重排,在分别设置宽高的时候,触发2次,当代的浏览器对此进行了优化,这种思路类似于现在流行的MVVM框架使用的虚拟DOM,对改变的DOM节点进行依赖收集,确认没有改变的节点,就进行一次更新。但是浏览器针对重排的优化虽然思路和虚拟DOM接近,但是还是有本质的区别。大多数浏览器通过队列化修改并批量执行来优化重排过程。也就是说上面那段代码其实在现在的浏览器优化下,只构成一次重排。但是还是有一些特殊的元素几何属性会造成这种优化失效。比如:offsetTop, offsetLeft,…scrollTop, scrollLeft, …clientTop, clientLeft, …getComputedStyle() (currentStyle in IE)为什么造成优化失效呢?仔细看这些属性,都是需要实时回馈给用户的几何属性或者是布局属性,当然不能再依靠浏览器的优化,因此浏览器不得不立即执行渲染队列中的“待处理变化”,并随之触发重排返回正确的值。接下来深入的介绍几种性能优化的小TIPS3.1 最小化重绘和重排既然重排&重绘是会影响页面的性能,尤其是糟糕的JS代码更会将重排带来的性能问题放大。既然如此,我们首先想到的就是减少重排重绘。3.1.1. 改变样式考虑下面这个例子:// javascriptvar el = document.querySelector(’.el’);el.style.borderLeft = ‘1px’;el.style.borderRight = ‘2px’;el.style.padding = ‘5px’;这个例子其实和上面那个例子是一回事儿,在最糟糕的情况下,会触发浏览器三次重排。然鹅更高效的方式就是合并所有的改变一次处理。这样就只会修改DOM节点一次,比如改为使用cssText属性实现:var el = document.querySelector(’.el’);el.style.cssText = ‘border-left: 1px; border-right: 2px; padding: 5px’;沿着这个思路,聪明的老铁一定就说了,你直接改个类名不也妥妥的。没错,还有一种减少重排的方法就是切换类名,而不是使用内联样式的cssText方法。使用切换类名就变成了这样:// css .active { padding: 5px; border-left: 1px; border-right: 2px;}// javascriptvar el = document.querySelector(’.el’);el.className = ‘active’;3.1.2 批量修改DOM如果我们需要对DOM元素进行多次修改,怎么去减少重排和重绘的次数呢?有的同学又要说了,利用上面修改样式的方法不就行了吗。回过头看一下造成页面重排的几个要点里,可以明确的看到,造成元素几何属性发生改变就会触发重排,现在需要增加10个节点,必然涉及到DOM的修改,这个时候就需要利用批量修改DOM这种优化方式了,这里也能看到,改变样式最小化重绘和重排这种优化方式适用于单个存在的节点。批量修改DOM元素的核心思想是:让该元素脱离文档流对其进行多重改变将元素带回文档中打个比方,我们主机硬盘出现了故障,常见的办法就是把硬盘卸下来,用专业的工具测试哪里有问题,待修复后再安装上去。要是直接在主板上面用螺丝刀弄来弄去,估计主板一会儿也要坏了…这个过程引发俩次重排,第一步和第三步,如果没有这两步,可以想象一下,第二步每次对DOM的增删都会引发一次重排。那么知道批量修改DOM的核心思想后,我们再了解三种可以使元素可以脱离文档流的方法,注意,这里不使用css中的浮动&绝对定位,这是风马牛不相及的概念。隐藏元素,进行修改后,然后再显示该元素使用文档片段创建一个子树,然后再拷贝到文档中将原始元素拷贝到一个独立的节点中,操作这个节点,然后覆盖原始元素看一下下面这个代码示例:// html<ul id=“mylist”> <li><a href=“https://www.mi.com”>xiaomi</a></li> <li><a href=“https://www.miui.com”>miui</a></li></ul>// javascript 现在需要添加带有如下信息的li节点let data = [ { name: ’tom’, url: ‘https://www.baidu.com’, }, { name: ‘ann’, url: ‘https://www.techFE.com’ }]首先,我们先写一个通用的用于将新数据更新到指定节点的方法:// javascriptfunction appendNode($node, data) { var a, li; for(let i = 0, max = data.length; i < max; i++) { a = document.createElement(‘a’); li = document.createElement(’li’); a.href = data[i].url; a.appendChild(document.createTextNode(data[i].name)); li.appendChild(a); $node.appendChild(li); }}首先我们忽视所有的重排因素,大家肯定会这么写:let ul = document.querySelector(’#mylist’);appendNode(ul, data);使用这种方法,在没有任何优化的情况下,每次插入新的节点都会造成一次重排(这几部分我们都先讨论重排,因为重排是性能优化的第一步)。考虑这个场景,如果我们添加的节点数量众多,而且布局复杂,样式复杂,那么能想到的是你的页面一定非常卡顿。我们利用批量修改DOM的优化手段来进行重构1)隐藏元素,进行修改后,然后再显示该元素let ul = document.querySelector(’#mylist’);ul.style.display = ’none’;appendNode(ul, data);ul.style.display = ‘block’;这种方法造成俩次重排,分别是控制元素的显示与隐藏。对于复杂的,数量巨大的节点段落可以考虑这种方法。为啥使用display属性呢,因为display为none的时候,元素就不在文档流了,还不熟悉的老铁,手动Google一下,display:none, opacity: 0, visibility: hidden的区别2)使用文档片段创建一个子树,然后再拷贝到文档中let fragment = document.createDocumentFragment();appendNode(fragment, data);ul.appendChild(fragment);我是比较喜欢这种方法的,文档片段是一个轻量级的document对象,它设计的目的就是用于更新,移动节点之类的任务,而且文档片段还有一个好处就是,当向一个节点添加文档片段时,添加的是文档片段的子节点群,自身不会被添加进去。不同于第一种方法,这个方法并不会使元素短暂消失造成逻辑问题。上面这个例子,只在添加文档片段的时候涉及到了一次重排。3)将原始元素拷贝到一个独立的节点中,操作这个节点,然后覆盖原始元素let old = document.querySelector(’#mylist’);let clone = old.cloneNode(true);appendNode(clone, data);old.parentNode.replaceChild(clone, old);可以看到这种方法也是只有一次重排。总的来说,使用文档片段,可以操作更少的DOM(对比使用克隆节点),最小化重排重绘次数。3.1.3 缓存布局信息缓存布局信息这个概念,在《高性能JavaScript》DOM性能优化中,多次提到类似的思想,比如我现在要得到页面ul节点下面的100个li节点,最好的办法就是第一次获取后就保存起来,减少DOM的访问以提升性能,缓存布局信息也是同样的概念。前面有讲到,当访问诸如offsetLeft,clientTop这种属性时,会冲破浏览器自有的优化————通过队列化修改和批量运行的方法,减少重排/重绘版次。所以我们应该尽量减少对布局信息的查询次数,查询时,将其赋值给局部变量,使用局部变量参与计算。看以下样例:将元素div向右下方平移,每次移动1px,起始位置100px, 100px。性能糟糕的代码:div.style.left = 1 + div.offsetLeft + ‘px’;div.style.top = 1 + div.offsetTop + ‘px’;这样造成的问题就是,每次都会访问div的offsetLeft,造成浏览器强制刷新渲染队列以获取最新的offsetLeft值。更好的办法就是,将这个值保存下来,避免重复取值current = div.offsetLeft;div.style.left = 1 + ++current + ‘px’;div.style.top = 1 + ++current + ‘px’; ...

November 13, 2018 · 1 min · jiezi

想让安卓app不再卡顿?看这篇文章就够了

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦本文由likunhuang发表于云+社区专栏实现背景应用的使用流畅度,是衡量用户体验的重要标准之一。Android 由于机型配置和系统的不同,项目复杂App场景丰富,代码多人参与迭代历史较久,代码可能会存在很多UI线程耗时的操作,实际测试时候也会偶尔发现某些业务场景发生卡顿的现象,用户也经常反馈和投诉App使用遇到卡顿。因此,我们越来越关注和提升用户体验的流畅度问题。已有方案在这之前,我们将反馈的常见卡顿场景,或测试过程中常见的测试场景使用UI自动化来重复操作,用adb系统工具观察App的卡顿数据情况,试图重现场景来定位问题。常用的方式是使用adb SurfaceFlinger服务和adb gfxinfo功能,在自动化操作app的过程中,使用adb获取数据来监控app的流畅情况,发现出现出现卡顿的时间段,寻找出现卡顿的场景和操作。方式1:adb shell dumpsys SurfaceFlinger使用‘adb shell dumpsys SurfaceFlinger’命令即可获取最近127帧的数据,通过定期执行adb命令,获取帧数来计算出帧率FPS。优点:命令简单,获取方便,动态页面下数据直观显示页面的流畅度;缺点:对于静态页面,无法感知它的卡顿情况。使用FPS在静态页面情况下,由于获取数据不变,计算结果为0,无法有效地衡量静态页面卡顿程度;通过外部adb命令取得的数据信息衡量app页面卡顿情况的同时,app层面无法在运行时判断是否卡顿,也就无法记录下当时运行状态和现场信息。方式2:adb shell dumpsys gfxinfo使用‘adb shell dumpsys gfxinfo’命令即可获取最新128帧的绘制信息,详细包括每一帧绘制的Draw,Process,Execute三个过程的耗时,如果这三个时间总和超过16.6ms即认为是发生了卡顿。优点:命令简单,获取方便,不仅可以计算帧率,还可以观察卡顿时每一帧的瓶颈处于哪个维度(onDraw,onProcess,onExecute);缺点:同方式1拥有一样的缺点,无法衡量静态页面下的卡顿程度;app层面依然无法在发生卡顿时获取运行状态和信息,导致跟进和重现困难。已有的两种方案比较适合衡量回归卡顿问题的修复效果和判断某些特定场景下是否有卡顿情况,然而,这样的方式有几个明显的不足:1、一般很难构造实际用户卡顿的环境来重现;2、这种方式操作起来比较麻烦,需编写自动化用例,无法覆盖大量的可疑场景,测试重现耗时耗人力;3、无法衡量静态页面的卡顿情况;4、出现卡顿的时候app无法及时获取运行状态和信息,开发定位困难。全新方案基于这样的痛点,我们希望能使用一套有效的检测机制,能够覆盖各种可能出现的卡顿场景,一旦发生卡顿,能帮助我们更方便地定位耗时卡顿发生的地方,记录下具体的信息和堆栈,直接从代码程度给到开发定位卡顿问题。我们设想的Android卡顿监控系统需要达到几项基本功能:1、如何有效地监控到App发生卡顿,同时在发生卡顿时正确记录app的状态,如堆栈信息,CPU占用,内存占用,IO使用情况等等;2、统计到的卡顿信息上报到监控平台,需要处理分析分类上报内容,并通过平台Web直观简便地展示,供开发跟进处理。如何从App层面监控卡顿?我们的思路是,一般主线程过多的UI绘制、大量的IO操作或是大量的计算操作占用CPU,导致App界面卡顿。只要我们能在发生卡顿的时候,捕捉到主线程的堆栈信息和系统的资源使用信息,即可准确分析卡顿发生在什么函数,资源占用情况如何。那么问题就是如何有效检测Android主线程的卡顿发生,目前业界两种主流有效的app监控方式如下,在《Android卡顿监控方式实现》这篇文章中我将分别详细阐述这两者的特点和实现。1、利用UI线程的Looper打印的日志匹配;2、使用Choreographer.FrameCallback方式3: 利用UI线程的Looper打印的日志匹配判断是否卡顿Android主线程更新UI。如果界面1秒钟刷新少于60次,即FPS小于60,用户就会产生卡顿感觉。简单来说,Android使用消息机制进行UI更新,UI线程有个Looper,在其loop方法中会不断取出message,调用其绑定的Handler在UI线程执行。如果在handler的dispatchMesaage方法里有耗时操作,就会发生卡顿。只要检测msg.target.dispatchMessage(msg) 的执行时间,就能检测到部分UI线程是否有耗时的操作,从而判断是否发生了卡顿,并打印UI线程的堆栈信息。优点:用户使用app或者测试过程中都能从app层面来监控卡顿情况,一旦出现卡顿能记录app状态和信息, 只要dispatchMesaage执行耗时过大都会记录下来,不再有前面两种adb方式面临的问题与不足。缺点:需另开子线程获取堆栈信息,会消耗少量系统资源。方式4: 利用Choreographer.FrameCallback监控卡顿我们知道, Android系统每隔16ms发出VSYNC信号,来通知界面进行重绘、渲染,每一次同步的周期为16.6ms,代表一帧的刷新频率。SDK中包含了一个相关类,以及相关回调。理论上来说两次回调的时间周期应该在16ms,如果超过了16ms我们则认为发生了卡顿,利用两次回调间的时间周期来判断是否发生卡顿(这个方案是Android 4.1 API 16以上才支持)。这个方案的原理主要是通过Choreographer类设置它的FrameCallback函数,当每一帧被渲染时会触发回调FrameCallback, FrameCallback回调void doFrame (long frameTimeNanos)函数。一次界面渲染会回调doFrame方法,如果两次doFrame之间的间隔大于16.6ms说明发生了卡顿。优点:不仅可用来从app层面来监控卡顿,同时可以实时计算帧率和掉帧数,实时监测App页面的帧率数据,一旦发现帧率过低,可自动保存现场堆栈信息。缺点:需另开子线程获取堆栈信息,会消耗少量系统资源。总结下上述四种方案的对比情况: SurfaceFlingergfxinfoLooper.loopChoreographer.FrameCallback监控是否卡顿√√√√支持静态页面卡顿检测××√√支持计算帧率√√×√支持获取App运行信息××√√实际项目使用中,我们一开始两种监控方式都用上,上报的两种方式收集到的卡顿信息我们分开处理,发现卡顿的监控效果基本相当。同一个卡顿发生时,两种监控方式都能记录下来。 由于Choreographer.FrameCallback的监控方式不仅用来监控卡顿,也方便用来计算实时帧率,因此我们现在只使用Choreographer.FrameCallback来监控app卡顿情况。痛点1:如何保证捕获卡顿堆栈的准确性?细心的同学可以发现,我们通过上述两种方案(Looper.loop和Choreographer.FrameCallback)可以判断是当前主线程是否发生了卡顿,进而在计算发现卡顿后的时刻dump下了主线程的堆栈信息。实际上,通过一个子线程,监控主线程的活动情况,计算发现超过阈值后dump下主线程的堆栈,那么生成的堆栈文件只是捕捉了一个时刻的现场快照。打个不太恰当的比方,相当于闭路电视监控只拍下了凶案发生后的惨状,而并没有录下这个案件发生的过程,那么作为警察的你只看到了结局,依然很难判断案情和凶手。在实际的运用中,我们也发现这种方式下获取到的堆栈情况,查看相关的代码和函数,经常已经不是发生卡顿的代码了。 如图所示,主线程在T1T2时间段内发生卡顿,上述方案中获取卡顿堆栈的时机已经是T2时刻。实际卡顿可能是这段时间内某个函数的耗时过大导致卡顿,而不一定是T2时刻的问题,如此捕获的卡顿信息就无法如实反应卡顿的现场。我们看看在这之前微信iOS主线程卡顿监控系统是如何实现的捕获堆栈。微信iOS的方案是起检测线程每1秒检查一次,如果检测到主线程卡顿,就将所有线程的函数调用堆栈dump到内存中。本质上,微信iOS方案的计时起点是固定的,检查次数也是固定的。如果任务1执行花费了较长的时间导致卡顿,但由于监控线程是隔1秒扫一次的,可能到了任务N才发现并dump下来堆栈,并不能抓到关键任务1的堆栈。这样的情况的确是存在的,只不过现上监控量大走人海战术,通过概率分布抓到卡顿点,但依然不是最佳的捕获方案。因此,摆在我们面前的是如何更加精准地获取卡顿堆栈。为了卡顿堆栈的准确度,我们想要能获取一段时间内的堆栈,而不是一个点的堆栈,如下图:我们采用高频采集的方案来获取一段卡顿时间内的多个堆栈,而不再是只有一个点的堆栈。这样的方案的优点是保证了监控的完备性,整个卡顿过程的堆栈都得以采样、收集和落地。具体做法是在子线程监控的过程中,每一轮log输出或是每一帧开始启动monitor时,我们便已经开启了高频采样收集主线程堆栈的工作了。当下一轮log或者下一帧结束monitor时,我们判断是否发生卡顿(计算耗时是否超过阈值),来决定是否将内存中的这段堆栈集合落地到文件存储。也就是说,每一次卡顿的发生,我们记录了整个卡顿过程的多个高频采样堆栈。由此精确地记录下整个凶案发生的详细过程,供上报后分析处理(后文会阐述如何从一次卡顿中多个堆栈信息中提取出关键堆栈)。采样频率与性能消耗目前我们的策略是判断一个卡顿是否发生的耗时阈值是80ms(516.6ms),当一个卡顿达80ms的耗时,采集12个堆栈基本可以定位到耗时的堆栈。因此采样堆栈的频率我们设为52ms(经验值)。当然,高频采集堆栈的方案,必然会导致app性能上带来的影响。为此,为了评估对App的性能影响,在上述默认设置的情况下,我们做一个简单的测试实验观察。实验方法:ViVoX9 上运行微信读书App,使用卡顿监控与高频采样,和不使用卡顿监控的情况下,保持两次的操作动作相同,分析性能差异,数据如下: 关闭监控打开监控对比情况(上涨) CPU1.07%1.15%0.08% MemoryNative Heap3879438894100 kBDalvik Heap25889269841095 kB Dalvik Other29833099116 kB .so mmap3864438744100 kB 没有线程快照加上线程快照 性能指标2.4.5.368.912252.4.8.376.91678上涨CPUCPU63640.97%流量KBFlow2862428516 内存KBNativeHeap59438601831.25% DalvikHeap706671090.61% DalvikOther696569920.40% Sommap2220622164 日志大小KBfile size2948931561891430%压缩包大小KBzip size1546206%从实验结果可知,高频采样对性能消耗很小,可以不影响用户体验。监控使用Choreographer.FrameCallback, 采样频率设52ms),最终结果是性能消耗带来的影响很小,可忽略:1)监控代码本身对主线程有一定的耗时,但影响很小,约0.1ms/S;2)卡顿监控开启后,增加0.1%的CPU使用;3)卡顿监控开启后,增加Davilk Heap内存约1MB;4)对于流量,文件可按天写入,压缩文件最大约100KB,一天上传一次痛点2:海量卡顿堆栈后该如何处理?卡顿堆栈上报到平台后,需要对上报的文件进行分析,提取和聚类过程,最终展示到卡顿平台。前面我们提到,每一次卡顿发生时,会高频采样到多个堆栈信息描述着这一个卡顿。做个最小的估算,每天上报收集2000个用户卡顿文件,每个卡顿文件dump下了用户遇到的10个卡顿,每个卡顿高频收集到30个堆栈,这就已经产生20001030=60W个堆栈。按照这个量级发展,一个月可产生上千万的堆栈信息,每个堆栈还是几十行的函数调用关系。这么大量的信息对存储,分析,页面展示等均带来相当大的压力。很快就能撑爆存储层,平台无法展示这么大量的数据,开发更是没办法处理这些多的堆栈问题。因而,海量卡顿堆栈成为我们另外一个面对的难题。在一个卡顿过程中,一般卡顿发生在某个函数的调用上,在这多个堆栈列表中,我们把每个堆栈都做一次hash处理后进行排重分析,有很大的几率会是dump到同一个堆栈hash,如下图:我们对一个卡顿中多个堆栈进行统计,去重后找出最高重复次数的堆栈,发现堆栈C出现了3次,这次卡顿很有可能就是卡在堆栈3反映的函数调用上。由于采样频率不低,因此出现卡顿后一般都有不少的卡顿,如此可找出重复次数最高的堆栈,作为重点分析卡顿问题,从而进行修复。举个实际上报数据例子,可以由下图看到,一个卡顿如序号3,在T1T2时间段共收集到62个堆栈,我们发现大部分堆栈都是一样的,于是我们把堆栈hash后尝试去重,发现排重后只有2个堆栈,而其中某个堆栈重复了59次,我们可以重点关注和处理这个堆栈反映出的卡顿问题。把一个卡顿抽离成一个关键的堆栈的思路,可以大大降低了数据量, 前面提及60W个堆栈就可以缩减为2W个堆栈(2000101=2W)。 按照这个方法,处理后的每个卡顿只剩下一个堆栈,进而每个卡顿都有唯一的标识(hash)。到此,我们还可以对卡顿进行聚类操作,进一步排重和缩小数据量。分类前对每个堆栈,根据业务的不同设置好过滤关键字,提取出感兴趣的代码行,去除其他冗余的系统函数后进行归类。目前主要有两种方式的分类:1、按堆栈最外层分类,这种分类方法把同样入口的函数导致的卡顿收拢到一起,开发修复对应入口的函数来解决卡顿,然而这种方式有一定的风险,可能同样入口但最终调用不同的函数导致的卡顿则会被忽略;2、按堆栈最内层分类,这种分类方法能收拢同样根源问题的卡顿,缺点就是可能忽略调用方可能有多个业务入口,会造成fix不全面。当然,这两种方式的聚类,从一定程度上分类大量的卡顿,但不太好控制的是,究竟要取堆栈的多少层作为识别分类。层数越多,则聚类结果变多,分类更细,问题零碎;层数越少,则聚类结果变少,达不到分类的效果。这是一个权衡的过程,实际则按照一定的尝试效果后去划分层数,如微信iOS卡顿监控采用的策略是一级分类按最内层倒数2层分类,二级分类按最内层倒数4层。对于我们产品,目前我们没有按层数最内或最外来划分,直接过滤出感兴趣的关键字的代码后直接分类。这样的分类效果下来数据量级在承受范围内,如之前的2W堆栈可聚类剩下大约2000个(视具体聚类结果)。同时,每天新上报的堆栈都跟历史数据对比聚合,只过滤出未重复的堆栈,更进一步地缩减上报堆栈的真正存储量。卡顿监控系统的处理流程用户上报目前我们的策略是:1、通过后台配置下发,灰度0.2%的用户量进行卡顿监控和上报;2、如果用户反馈有卡顿问题,也可实时捞取卡顿日志来分析;3、每天灰度的用户一个机器上报一次,上报后删除文件不影响存储空间。后台解析1、主要负责处理上报的卡顿文件,过滤、去重、分类、反解堆栈、入库等流程;2、自动回归修复好的卡顿问题,读取tapd 卡顿bug单的修复结果,更新平台展示,计算修复好的卡顿问题,后续版本是否重新出现(修复不彻底)平台展示上报处理后的卡顿展示平台http://test.itil.rdgz.org/wel…主要展示卡顿处理后的数据:1、以版本为维度展示卡顿问题列表,按照卡顿上报重复的次数降序列出;2、归类后展示每个卡顿的关键耗时代码,也可查看全部堆栈内容;3、支持操作卡顿记录,如搜索卡顿,提tapd单,标注已解决等;4、展示每个版本的卡顿问题修复数据情况,版本分布,监控修复后是否重现等。自动提单实际使用中,为了增强跟进效果,我们设立一些规则,比如卡顿重复上报超过100次,卡顿耗时达到1000ms等,自动提tapd bug单给开发处理,系统也会自动更新卡顿问题的修复情况和数据,开发只需定期review tapd bug单处理修复卡顿问题即可,整个卡顿系统从监控,上报,分析,聚类,展示,提单到回归,整个流程自动化实现,不再需要人工介入。实际应用效果1、接入产品:微信读书,企业微信,QQ邮箱2、应用场景:现网用户的监控,发布前测试的监控,每天自动化运行的监控3、发现问题:三个多月时间,归类后的卡顿过万,提bug单约500,开发已解决超过200个卡顿问题卡顿监控的组件化考虑到Android卡顿监控的通用性,除了应用于Android WeRead中,我们也推广到广研的其他产品中,如企业微信,QQ邮箱。因此,在开发GG的努力下,推出了卡顿监控库http://git.code.oa.com/moai/m… ,其他Android产品可快速接入卡顿监控的SDK来监控app卡顿情况。目前monitor卡顿监控库主要有监控主线程卡顿情况,获取平均帧率使用情况,高频采样和获取卡顿信息等基本功能。这里要注意几点:1、采样堆栈信息的频率和卡顿耗时的阈值均可在SDK中设置;2、SDK默认判断一个卡顿是否发生的耗时阈值是80ms(516.6ms)3、采样堆栈的频率是52ms(约3帧+,尽量错开系统帧率的节奏,堆栈可尽量落到绘制帧过程中)4、启动监控后,卡顿日志就会不断通过内部的writer输出,实现MonitorLogWriter.setDelegate才能获取这些日志,具体的日志落地和上报策略因为各个App不同所以没有集成到SDK中5、monitor start后一直监控主线程, 包括切换到后台时也会,直到主动stop或者app被kill。所以在切后台时要主动stop monitor,切前台时要重新start1.组件引入方式2.主线程卡顿监控的使用方式1)启动监控2)停止监控3)获取卡顿信息app中加入监控卡顿SDK后,会实时输出卡顿的时间点和堆栈信息,我们将这些信息写入日志文件落地,同时每天固定场景上报到服务器,如每天上报一次,用户打开app后进行上报等策略。收集不同用户不同手机不同场景下的所有卡顿堆栈信息,可供分析,定位和优化问题。特别致谢此文最后特别感谢阳经理(ayangxu)、豪哥(veruszhong)、cginechen对Android卡顿监控组件化的鼎力支持,感谢姑姑(janetjiang)悉心指导与提议!希望卡顿监控系统能越来越多地暴露卡顿问题,在大家的共同努力下不断提升App的流畅体验!相关阅读Javascript框架设计思路图小程序优化36计【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识

October 17, 2018 · 1 min · jiezi

谈谈前端性能优化(一)

前言性能优化无非就是让页面的打开速度更快一些,以得到更好的用户体验。前端在这方面可以做到的有两方面,页面级别的优化,比如减少 Http 请求次数、加快资源的加载速度;二是代码级别的优化,页面重新渲染一次会经过浏览器的重排(reflow)和重绘(repaint),这两部操作是非常耗时的,本文将根据这两方面的优化途径,大致总结一下。页面级别优化1. 减少 HTTP请求数首先,每个请求都是有成本的,既包含时间成本也包含资源成本。一个完整的请求都需要经过 DNS寻址、与服务器建立连接、发送数据、等待服务器响应、接收数据这样一个 “漫长” 而复杂的过程。时间成本就是用户需要看到或者 “感受” 到这个资源是必须要等待这个过程结束的,资源上由于每个请求都需要携带数据,因此每个请求都需要占用带宽。另外,由于浏览器进行并发请求的请求数是有上限的 (具体参见此处 ),因此请求数多了以后,浏览器需要分批进行请求,因此会增加用户的等待时间,会给用户造成站点速度慢这样一个印象,即使可能用户能看到的第一屏的资源都已经请求完了,但是浏览器的进度条会一直存在。减少 http 请求次数的主要方法:设置 HTTP缓存http 缓存是 web 性能优化中非常重要的一种手段,把一些常用资源在首次加载时缓存到浏览器本地,再次加载时可大大减少请求次数,缓存的资源越多,性能当然越好。缓存的规则主要有两种,强制缓存和对比协商缓存,两种缓存分别通过Http报文头部不同的字段进行控制。具体缓存规则参照这里 或者 这里。资源合并压缩CSS、 Javascript、Image 都可以用相应的工具(Webpack)进行压缩,压缩后往往能省下不少空间。CSS Sprites合并 CSS图片,减少请求数的又一个好办法。懒加载这条策略实际上并不一定能减少 HTTP请求数,但是却能在某些条件下或者页面刚加载时减少 HTTP请求数。2. 把 js 脚本置底加载js 脚本是很容易形成阻塞,导致资源加载停滞,为了避免这种情况,先加载其他资源,最后加载脚本。3. inline 脚本异步执行inline 脚本与外链引用的脚本类似,也有可能会引起阻塞,所以也要将 inline 脚本放到页面底部或者异步方式来加载,例如使用script标签的defer 和async属性、使用setTimeOut。4. 动态加载 js 模块5. css 放在 head 中页面渲染过程还要经历重绘重排,这样做是避免会出现 DOM 加载完之后却没有样式的情况。代码级别优化DOM操作应该是脚本中最耗性能的一类操作,例如增加、修改、删除 DOM元素或者对 DOM集合进行操作。而修改 DOM 会引起网页的重新渲染。重新渲染,就需要重新生成布局和重新绘制。前者叫做"重排"(reflow),后者叫做"重绘"(repaint)。需要注意的是,“重绘"不一定需要"重排”,比如改变某个网页元素的颜色,就只会触发"重绘",不会触发"重排",因为布局没有改变。但是,“重排"必然导致"重绘”,比如改变一个网页元素的位置,就会同时触发"重排"和"重绘",因为布局改变了。这这两步只是网页生成的最后两部,关于页面的生成过程,主要有五步:1. HTML代码转化成DOM2. CSS代码转化成CSSOM(CSS Object Model)3. 结合DOM和CSSOM,生成一棵渲染树(包含每个节点的视觉信息)4. 生成布局(layout),即将所有渲染树的所有节点进行平面合成5. 将布局绘制(paint)在屏幕上这五步里面,第一步到第三步都非常快,耗时的是第四步和第五步。“生成布局”(flow)和"绘制"(paint)这两步,合称为"渲染"(render)。具体技巧参照 http://www.ruanyifeng.com/blo…。最后本文主要从页面和代码两个层面分析提高性能的方案,其中还有很多细节和其他技巧,后续慢慢完善补充。参考链接:https://blog.csdn.net/w2326ic…https://blog.csdn.net/w2326ic...https://www.cnblogs.com/cherr…

September 27, 2018 · 1 min · jiezi