Web 性能优化
高并发
QPS
每秒钟请求或者查询的数量, 在互联网领域, 指每秒响应请求数(HTTP请求)- 峰值每秒请求数(QPS) = (总PV数 80%) / (6小时秒数 20%)
- 80% 的访问量集中在 20% 的时间
吞吐量
单位时间内处理的请求数量(通常由 QPS 与 并发数 决定)- QPS 是每秒 HTTP 请求数量, 并发连接数 是系统同时处理的请求数量
- 一个并发连接数 会有多个 HTTP请求
响应时间
从请求发出到收到响应花费的时间PV
页面浏览量UV
独立访客, 即一定时间范围内相同访客多次访问网站, 只计算为一个独立访客带宽
计算带宽大小需关注两个指标, 峰值流量(PV / 统计时间s) 和 页面的平均大小
压力测试
ab、wrk、http_load、Web Bench、Siege、Apache JMeter
ab
apache benchmark, 是 Apache 官方推出的工具
创建多个并发访问线程, 模拟多个访问者同时对某一 URL 地址进行访问。它的测试目标是基于 URL 的, 因此, 它既可以用来测试 Apache 的负载压力, 也可以测试 Nginx、lighthttp、Tomcat、IIS 等其它 Web 服务器的压力
模拟并发请求100, 总共请求5000次
ab -c 100 -n 5000 https://www.github.com/
测试机器与被测试机器分开
不要对线上服务做压力测试
观察测试工具 ab 所在机器, 以及被测试机的 CPU, 内存, 网络等都不超过最高限度的 75%
- QPS 达到 50
- 小型网站, 一般的服务器就可以应付
- QPS 达到 100
- 假设关系型数据库的每次请求在 0.01s 完成
- 假设单页面只有一个 SQL 查询, 那么 100QPS 意味着 1s 完成 100 次请求, 但是此时我们并不能保证数据库查询能完成 100次
- 方案:
数据库缓存层、数据库的负载均衡
- QPS 达到 800
- 假设我们使用百兆带宽, 意味着网站出口的实际带宽是 8M 左右
- 假设每个页面只有 10kb, 在这个并发条件下, 百兆宽带已经吃完
- 方案:
CDN 静态加速、Nginx 负载均衡
- QPS 达到 1000
- 假设使用 Memcache 缓存数据库查询数据, 每个页面对 Memcache 的请求远大于直接对 DB 的请求
- Memcache 的悲观并发数在 2w 左右, 但有可能在之前内网宽带已经吃光, QPS 在 800 左右 Memcache 就不是很稳定了
方案:静态HTML缓存
- QPS 达到 2000
- 这个级别下, 文件系统访问锁都成为了灾难
- 方案:
做业务分离, 分布式存储
流量优化
防盗链
referer
- Nginx 模块 ngx_http_referer_module, 用来阻挡来源非法的域名请求
- Nginx 指令 valid_referers, 全局变量 $invalid_referer
- valid_referers 值包含 none|blocked|server_names|string…
none
Referer 来源头部为空的情况blocked
Referer 来源头部不为空, 但是里面的值被代理或者防火墙删除了, 这些值都不以 http 或者 https 开头server_names
Referer 来源头部包含当前的 server_names
- valid_referers 值包含 none|blocked|server_names|string…
1 | location ~.*\.(gif|jpg|png|flv|swf|rar|zip)$ |
加密签名
使用第三方模块 HTTPAccessKeyModule 实现 Nginx 防盗链
- accesskey on|off 模块开关
- accesskey_hashmethod md5|sha-1 签名加密方式
- accesskey_arg GET参数名称
- accesskey_signature 加密规则
1 | location ~.*\.(gif|jpg|png|flv|swf|rar|zip)$ |
1 | $sign = md5('daidai' . $_SERVER['REMOTE_ADDR']); |
前端优化
减少 HTTP 请求
- CSS Sprites
- 合并脚本和样式表
- 小图片 Base64 编码
浏览器缓存
- 适合缓存的内容
- 不变的图像, 如logo、图标
- js、css 静态文件
- 可下载的内容, 媒体文件
- 建议使用协商缓存
- html 静态文件
- 经常替换的图片
- 经常修改的 js、css 文件
- index.css?版本Version
- index.Hash签名.js
- 不建议缓存的内容
- 用户隐私等敏感数据
- 经常改变的 api 数据接口
1 | $since = $_SERVER['HTTP_IF_MODIFIED_SINCE']; |
Nginx 配置缓存策略
- add_header 指令, 添加状态码为 2xx 和 3xx 的响应头信息
- add_header name value [always];
- 可以设置 Pragma/Expires/Cache-Control, 可以继承
- expires 指令, 通知浏览器过期时长
expires time;
- 为负值时表示 Cache-Control: no-cache;
- 当为正或者0时, 就表示 Cache-Control: max-age=指定的时间
- expires max 会把 Expires 设置为 Thu, 31 Dec 2037 23:55:55 GMT, 相当于 Cache-Control 设置到 10年
- expires 指令, 通知浏览器过期时长
文件压缩
- JS
- UglifyJs、(Yahoo)YUI Compressor、(Google)Closure Compliler
- CSS
- YUI Compressor、CSS Compressor
- HTML
- 不建议使用代码压缩, 有时会破坏代码结构, 可以使用 Gzip 压缩
- 当然也可以使用 htmlcompressor 工具, 不过转换后要检查代码结构
- html-minifier
- 图片
- Gzip 压缩
Nginx 配置
1
2
3
4
5
6
7
8
9gzip on|off; # 是否开启gzip
gzip_buffers 32 4k|16 8k # 缓冲(在内存中缓冲几块 每块多大)
gzip_comp_level [1-9] # 推荐 6 压缩级别(级别越高, 压的越小, 越消耗 CPU 计算资源)
gzip_disable # 正则匹配 UA 什么样的 Uri 不进行 gzip
gzip_min_length 200 # 开始压缩的最小长度
gzip_http_version 1.0|1.1 # 压缩的 http 协议版本
gzip_proxied # 设置请求者代理服务器, 该如何缓存内容
gzip_types text/plain application/javascript image/gif # 对哪些类型的文件压缩
gzip_vary on|off # 是否传输 gzip 压缩标志
CDN 静态加速
内容分发网络 Content Delivery Network
- 可用 LVS 做 4层 负载均衡
- 可用 Nginx, Varnish, Squid, Apache TrafficServer 做 7层 负载均衡 和 Cache
- 使用 Squid 反向代理, 或者 Nginx
独立图片服务器
分担 Web 服务器的 I/O 负载, 将耗费资源的图片服务分离出来, 提高服务器的性能和稳定性
- 采用独立域名, 二级域名不算
同一域名下浏览器的并发连接数有限制6~8
由于 cookie 的原因, 对缓存不利, 大部分 Web cache 都只缓存不带 cookie 的请求, 导致每次的图片请求都不能被命中 - 图片上传和同步
- NFS 共享
- FTP 同步
- 阿里云对象存储 OSS
前端监控
页面埋点
性能监控
首屏优化
性能优化
- 路由懒加载
- 异步组件按需加载
- SSR 服务端渲染 SEO
传统的 SPA 方式过程繁多
- 下载 html,解析,渲染
- 下载 js,执行
- ajax 异步加载数据
- 重新渲染页面
而 SSR 则只有一步
- 下载 html,解析,渲染
- App 预取
- 分页加载
- 图片懒加载
体验优化
骨架屏
loading
一个页面很慢?
加载慢
性能指标
- First Paint(FP) 首次绘制像素
- First Contentful Paint(FCP) 首次绘制来自 DOM 的内容
- First Meaningful Paint(FMP) 主要内容 👎 不好衡量
- DomContentLoaded(DCL)
DOMContentLoaded
- Largest Contentful Paint(LCP) 可视区域中最大的内容元素呈现到屏幕上的时间,用以估算页面的主要内容对用户可见时间 👍
- Load(L)
window.onload
性能分析工具
- Chrome devtools Performance
- Lighthouse
lighthouse https://www.github.com/ --view --preset=desktop
性能统计 持续跟进
1
2
3
4
5
6
7
8
9
10
11
12
13
14// console.table(performance.timing)
let timing = window.performance && window.performance.timing
// let navigation = window.performance && window.performance.navigation
// DNS解析
let dns = timing.domainLookupEnd - timing.domainLookupStart
// 网络耗时
let network = timing.responseEnd - timing.navigationStart
// 渲染处理
let processing = (timing.domComplete || timing.domLoading) - timing.domLoading
// 可交互
let active = timing.domInteractive - timing.navigationStart
运行慢、渲染慢
内存泄漏、节流防抖、重绘回流、SSR
错误监控
即时运行错误
- try…catch
- window.onerror
资源加载错误
- object.onerror
- performance.getEntries()
- performance.getEntriesByType(‘navigation’)
- 根据已经成功加载好的资源和代码中本需要加载的资源对比
- Error 事件捕获
- window.addEventListener(‘error’, (e) => {}, true)
跨域的 JS 运行错误
- 可以捕获到, 但是没有权限拿到具体的错误信息
- 在 script 标签中增加 crossorigin 属性
- 后端设置 js 资源响应头 Access-Control-Allow-Origin: *
Promise 错误
- window.onunhandledrejection
利用 Image 对象上报错误
(new Image()).src
兼容性好 可跨域
服务端优化
页面静态化
- 使用模板引擎: ejs smarty jsp
- 利用 ob 系列的函数
- ob_start() 打开输出控制缓冲
- ob_get_contents() 返回输出缓冲区内容
- ob_clean() 清空输出缓冲区
- ob_end_flush() 冲刷出(送出)输出缓冲区内容并关闭缓冲
1 | $cache_name = md5(__FILE__) . '.html'; |
并发处理、防雪崩
- 一个
线程
可以有多个协程, 一个进程
也可以单独拥有多个协程 - 线程进程都是
同步
机制, 而协程则是异步
协程
能保留上一次调用时的状态, 每次过程重入时, 就相当于进入上一次调用的状态
同步阻塞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16$sockserver = stream_socket_server('tcp://0.0.0.0:8000', $errno, $errstr);
for ($i = 0; $i < 5; $i++) {
if (pcntl_fork() == 0) {
while (true) {
$connect = stream_socket_accept($sockserver);
if ($connect == false) {
continue;
}
$request = fread($connect, 9000);
$response = 'hello world';
fwrite($connect, $response);
fclose($connect);
}
exit();
}
}异步非阻塞
- 现在各种高并发异步 IO 的服务器程序都是基于 epoll 实现的
Nginx Golang Nodejs
Nginx
多线程 ReactorSwoole
多线程 Reactor + 多进程 Worker
- 现在各种高并发异步 IO 的服务器程序都是基于 epoll 实现的
PHP 并发编程
- Swoole 扩展
- 消息队列
Kafka ActiveMQ ZeroMQ RabbitMQ Redis
- 用户注册
- 用户注册后, 需要发送注册邮件和短信
- 应用解耦
- 用户下单后, 订单系统完成持久化处理, 将消息写入消息队列, 返回用户订单下单成功
- 订阅下单的消息, 采用拉/推的方式, 获取下单信息, 库存系统根据下单信息, 进行库存操作
- 流量削峰
- 秒杀活动, 流量瞬时激增, 服务器压力大
- 用户发起请求, 服务器接收后, 先写入消息队列; 假如消息队列长度超过最大值, 则直接报错或提示用户
- 日志处理
- 解决大量日志的传输
- 日志采集程序将程序写入消息队列, 然后通过日志处理程序的订阅来消费日志
- 消息通讯
- 聊天室, 多个客户端订阅同一主题, 进行消息发布和接收
- 用户注册
- 接口的并发处理
curl_multi_init()
数据库优化
数据库缓存层优化
- MySQL 查询缓存
- 启用 MySQL 查询缓存, 极大地降低 CPU 使用率
- query_cache_type 0不使用查询缓存 1始终使用查询缓存 2按需使用查询缓存
- query_cache_type 为 1 时, 也可关闭查询缓存
select SQL_NO_CACHE * from my_table where condition; - query_cache_type 为 2 时, 也可使用查询缓存
select SQL_CACHE * from my_table where condition; - query_cache_size 默认为 0, 表示查询缓存预留的内存为 0, 无法使用查询缓存
set global query_cache_size = 134217728;
- 查询缓存可以看做是 SQL文本 和 查询结果 的映射
- 第二次查询的 SQL 和第一次查询的 SQL 完全相同, 则会使用缓存
- 查看查询缓存命中次数
show status like 'Qcache_hits';
- 表的结构或数据发生改变时, 查询缓存中的数据不再有效
- 清理查询缓存内存碎片
flush query cache;
- 从查询缓存中移出所有查询
reset query cache;
- 关闭所有打开的表, 清空查询缓存
flush tables;
- 启用 MySQL 查询缓存, 极大地降低 CPU 使用率
- Memcache 查询缓存
- Redis 查询缓存
- Redis 在 2.0 版本后增加了自己的 VM 特性, 突破物理内存的限制; Memcache 可以修改最大可用内存, 采用 LRU 算法
- Redis 依赖客户端来实现分布式读写
- Memcache 本身没有数据冗余机制
- Redis 支持
快照, AOF
, 依赖快照进行持久化
, AOF 增强了可靠性的同时, 对性能有所影响 - Memcache 不支持持久化, 通常做缓存, 提升性能
- Memcache 在并发场景下, 用 cas 保证一致性; Redis 事务支持比较弱, 只能保证事务中的每个操作连续执行
- Redis 支持丰富的数据类型
- Redis 用于数据量较小的高性能操作和运算上
- Memcache 用于在动态系统中减少数据库负载, 提升性能; 适合做缓存, 提高性能
MySQL数据层优化
- 数据类型优化
- 索引优化
- SQL 语句优化
- 存储引擎优化
- 表结构优化
- 分库分表、分区设计
- 数据库服务器架构
- 日志
- 主从复制
- 读写分离
- 双主热备
- 负载均衡
- LVS
- MyCat 数据库中间件
- 日志
负载均衡 分发请求
- Nginx 反向代理
- 七层模型, 基于 URL 等应用层信息的负载均衡
- 功能强大, 性能卓越, 运行稳定
- 配置简单灵活
- 能够自动剔除工作不正常的后端服务器
- 上传文件使用异步模式
- 支持多种分配策略, 可以分配权重, 分配方式灵活
- 内置策略: IP Hash、加权轮询
- 扩展策略: fair 策略、通用 hash、一致性 hash
- 四层模型
通过报文中的目标地址和端口, 再加上负载均衡设备设置的服务器选择方式, 决定最终选择的内部服务器
- 软件: LVS
NAT DR IP-TUNNELING - 硬件: F5
正向代理 CDN 翻墙
隐藏了真实的请求客户端 代理的对象是客户端
反向代理 devServe 负载均衡
隐藏了真实的服务端 代理的对象是服务端
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 寻梦环游记!