服务器做了两个优化 CPU 使用率减低 40%

1. 问题描述

大家应该注意到了最近社区访问速度有点慢,一直以为是家里 wifi 不给力覆盖范围不够,直到 @leo 喊我说服务器太慢,CPU 爆了:

file

上 UCloud 后台看数据比较直观:

file

CPU 使用率居高不下,有时候还伴随着 MySQL 爆掉的情况,如下图。当这种情况发生时,你就会发现网页请求卡住不动:

file

经过一番调查,定位到两个问题,下面分别讲解各自的解决方案。

问题一:话题显示时实时更新查看数

file

每次页面的查看,都要触发一次数据库 UPDATE 操作,当社区帖子 PV 很高时候,瓶颈就会很明显。

解决方案

核心思路是尽量少碰到数据库,利用较快的记录器来做记录。这里选择使用 Redis 的 Hash 数据类型来存储。Redis 运行在内存中,效率将会非常高。

实现逻辑:

  1. 按照日期分开存储 view_count 到 Redis 里,按日期命名 Hash,如:laravelchina:counter_cache_2018-06-17;
  2. 字段名按照命名规则 数据表_字段名_ID,如话题表里 ID 为 1 的 view_count 字段 —— topics_view_count_1
  3. 设置计划任务每天凌晨将昨天的 Hash 同步到 DB 中,并删除;

问题二:未读消息 Ajax 请求太频繁

file

登录用户情况下,每一个浏览器打开的社区页面,每过 15 秒钟就会发送一次未读消息的 Ajax 请求,像我查阅资料时,很多时候会同时打开几十个页面:

file

上图这种情况每 15 秒钟就会给服务器发送几十个请求… 看着自己写的 Bug,无奈感叹:

file

解决方案

核心思路是不论浏览器打开了多少窗口,浏览器内的所有窗口在单位时间内(15 秒),只能发送一个请求。怎么做到呢?利用现代浏览器内置的 localStorage 功能可以很容易实现:

  1. JS 端使用 localStorage 在请求成功后记录 notification_requested_at 的值为 Date.now()
  2. 在每一次请求发送前,拿当前时间 Date.now() 减去 notification_requested_at 时间;
  3. 如果大于 15 秒,就发送请求;
  4. 否则放弃请求,直接读取 localStorage 里的 notification_count
  5. 请求成功后将获取到未读消息数存入 localStorage 键为 notification_count
  6. 每次刷新页面,JS 初始化时未读消息数存入 localStorage 键为 notification_count

有同学在问为啥不使用长链接,首先这里要求的实时性不需要那么高,其次,我有意保持程序架构的简单,Keep it simple and stupid ,越简单越方便维护,够用就行。

优化结果

file

看来又可以延迟升级服务器配置的时间了。性能优化的路漫漫,准备找个时间好好优化下社区程序,顺便看看能不能输出一些优化日记。听说 PHP 7.2 性能要好个 10%, 现在也趋于稳定,有在考虑升级下 PHP 版本。

最后问大家一个问题:你写过最愚蠢的 Bug 是啥,分享下哈?

本作品采用《CC 协议》,转载必须注明作者和本文链接
摈弃世俗浮躁,追求技术精湛
本帖由系统于 5年前 自动加精
Summer
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 57
月光

好,服务器就是你弄爆的,开那么多页面 :smirk:

5年前 评论

站长何不开个踩坑板块,大家都来发这类的文章。

5年前 评论

:+1:希望多发一些这样的文章哦 嘿嘿,非常不错,谢谢老大

5年前 评论
gitxuzan

厉害

5年前 评论
农村闲散劳动力

:joy: :joy: :joy: :joy: :joy:

5年前 评论

学习了

5年前 评论
月光

好,服务器就是你弄爆的,开那么多页面 :smirk:

5年前 评论

我也开了好几个页面查资料

5年前 评论
JaguarJack

的确感觉比之前流畅了

5年前 评论

自从进了社区再也出不去了,全是干货。

5年前 评论

是一个解决办法的思路,但是感觉还有更好的思路

5年前 评论

希望有一套教程专门讲这种的

5年前 评论
nff93

获取未读消息这个,要是前端有一个统一的锁就好了~

5年前 评论

这个很稳

5年前 评论
DianWang

这种问题在测试阶段不能用ab提前发现吗?

5年前 评论

@DianWang ab的时候你会发现 c10的时候一般双核服务器的负载就满了。。。

5年前 评论

用户在增长,服务器需要自动预警。我们游戏服务器解决方案是:守护进程分析日志,来达到实时预警(当然主要是分析用户的短期异常输入),当然服务器运行状态预警其实也很方便做(一般都是运维来解决,研发不用动手) :stuck_out_tongue_closed_eyes:。

5年前 评论

这是一台4核心的机器吗?我看第一张top图负载一直在5。
我在4核的云主机上压测laravel空框架,关闭session中间件,开启opcache。在c100的时候负载就18了,太吓人了。
负载4的时候cpu应该全都是100%满负荷运行。
站长有试过c多少的时候负载到4吗?

5年前 评论
circle

我最愚蠢的 bug:在 php 一个进程内,往数据库插入一条记录后随即根据 id 读取该记录发现返回为空。当初排查起来又是捕获请求,又是各种日志查看,结果最后找到原因后真的是想把自己打一顿?

5年前 评论
babyObama 2年前
GanymedeNil

:joy: 阅读量这类数据 不是一般都直接进radis吗

5年前 评论

消息提醒这个为什么不用长连接嘞

5年前 评论
Summer

@风中的白鸽 就是为了保持程序架构简单,部署省事,并且这个场景中实时性要求不强。

5年前 评论
Summer

@canyuexiang 好久以前开发的,为了省事直接更新数据库

5年前 评论

4核才并发100吗,

5年前 评论

社区的notification是通过发送ajax来获取的。为什么不用workerman或者swoolen从服务端推送呀。

5年前 评论

现在的查看数还是实时更新的啊,刷新一下就能开到变化

5年前 评论

站长何不开个踩坑板块,大家都来发这类的文章。

5年前 评论

@Kevinvinvin 应该是查询数据库的值和 redis 当天值求和

5年前 评论
Summer

@chaofei
@Kevinvinvin 不碰数据库哈,直接更新 Redis 里的,因为是内存存储,性能要好太多了。每天凌晨再同步 Redis 到数据库里,因为要保持数据一致性。

5年前 评论

多发些 实用的

5年前 评论
blankqwq

学到了,hhh

5年前 评论

很实用啊,刚好项目也存在这样的问题

5年前 评论
chongyi

我想起之前我们那个程序并发上万时,做了很多优化包括你这些,100 台顶配服务器都扛不住的恐惧。。。

5年前 评论
Summer

@chongyi 找个时间分享下哈

5年前 评论
chongyi

@Summer 好啊 :smile:

5年前 评论

经常犯的错误,每次写SQL关联查询的时候,条件没传,也硬要关联查询一次,而不写判断条件。

5年前 评论

我们做股票行情的更是,没上长连接之前,每5秒定时请求把服务器刷爆😂

5年前 评论

哈哈,当初我做一个项目的时候辛亏讨论的人数较多,以上几点都说到了。

5年前 评论

@Summer 大佬,特想知道本文中的【定位】这一步是怎么做的,论坛有教程吗?

5年前 评论

@Summer 大佬,特想知道本文中的【定位】这一步是怎么做的,论坛有教程吗?

5年前 评论

支持大佬,支持你

5年前 评论
luckwang

想知道是怎么定位到这两个问题的

5年前 评论

@chongyi 那种就只能上 swoole golang node.js 这些,异步非阻塞 ,常驻内存,数据库连接池。

5年前 评论
chongyi

@依剑听雨 改用 Rust 了

5年前 评论
  1. JS 端使用 localStorage 在请求成功后记录 notification_requested_at 的值为 Date.now();
    其实在请求前就更新这个值更好?
// 定时请求 message 方法
function message()
{
    if (没到时间) {
        // 把 localStorage 的值显示到页面
        return false;
    }
    // 更新当前 localStorage 的请求时间
    // ajax 请求
}

如果是先ajax请求,如果需要请求消息总数的那个页面稍慢一点,其他页面还是可以趁机去请求接口。

5年前 评论

@侧面 有更好的办法可以分享出来~

5年前 评论

经常做开发这些都容易遇到呢。

5年前 评论

@侧面 不要感觉,举出栗子来

5年前 评论

哈哈,按照这个配置又可以好好的装逼了。

5年前 评论

我遇到的排查很久的bug,但是并不愚蠢,因为项目需求用了rabbitmq,用workerman做的,因为有和其他公司的rabbitmq服务建立长连接,为了避免连接断掉,执行了定时检查连接状态进行重连,但是重点在下面,大家看代码

   public static function getInsChannel(AMQPStreamConnection $rabconn)
    {
        if (empty(self::$channel)) {
            self::$channel = $rabconn->channel();
        }
        return self::$channel;
    }

这样处理在连接断掉时,就会出现问题,获取到的channel是旧连接拿到的channel,而那个连接实际已经断掉了,最后我是将这块代码抽出来,在检测到断掉且执行重连前将其销毁,最后问题解决。

4年前 评论
aodaobi

@ikin 我们也差不多行业,不过这一块我是使用websocket+mongodb

4年前 评论
ikin 4年前

献上我的膝盖,请收好~

4年前 评论

最蠢的应该是死循环吧,然后服务器远程不了,只能重启。

4年前 评论
Coolest 3年前

哈哈 同习惯开几十个页面

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!