背景

前段时间出了不少性能相关的市场反馈,某大型连锁店客户在使用云平台时频繁出现卡顿现象,基本不可用;甚至在其使用期间,由于大量占用了系统资源服务,导致后端接口响应时间明显边长,影响了其他用户的支持使用

客户配置信息

  • 连锁店项目,所有设备放在同一个项目中进行管理

  • 每个一级区域对应一个城市,每个二级区域对应一个门店

  • 每个门店添加一个NVR,关联若干IPC通道

设备 数量
总监控点数 10000
NVR 2000
每个 NVR 下的通道数 1 到 16
区域 数量
一级区域 500
二级区域 每个一级区域下包含 1-50 个二级区域,共 2000 个二级区域

问题原因

假设有以下带有层级的树状区域结构:

|-一级区域1
    |-二级区域1-1
    |-二级区域1-2
    |-二级区域1-3
    ...
|-一级区域2
    |-二级区域2-1
    |-二级区域2-2
    |-二级区域2-3
    ...
...

有多个一级区域,且每个区域下有多个二级区域

进入设备管理页面,前端会发送如下请求给后端:

请求 请求参数 作用
getRootRegions - 获取所有一级区域
getRegionChildren rootRegionId 获取特定一级区域下的所有二级区域
getDeviceList regionId 获取特定区域下的设备列表
  • 区域相关的所有请求,不分页,也就是 getRootRegions 会响应所有一级区域的信息,getRegionChildren 会响应所有特定区域下的子区域,不管响应结果会有多少个

  • 进入页面时,会完整请求所有区域,以及所有区域下的设备信息

  • 区域提供搜索功能,搜索由前端实现,所以要完整加载所有区域信息后才能实现搜索

请求数量 = 1(getRootRegions) + 总一级区域数量(getRegionChildren) + 总区域数量(getDeviceList)

这样一来,一进入页面,就会发送多个请求,请求数量随区域数量的增加而线性增加,导致在区域数量很多时,并行发送了大量请求给后端

从该客户的配置情况来看,前端会发送1+500+(500+2000)=3001个请求,并行发大量请求带来的影响有:

  • 浏览器一般会限制并行请求数量,前面的请求未得到响应时,后发的请求暂时阻塞,导致大量请求被阻塞,响应时间长,响应可能出现超时导致服务不可用

  • 并行请求数量太多,服务器资源占用突增,后端服务器响应时间变长

优化方案

该问题虽然可以认为是性能问题(配置少的情况下不会出现、配置多的情况下出现),但是从直接原因来看,是前后端设计实现不合理导致的,因此针对此页面提出如下几个改进方案:

  • 所有获取区域功能,添加分页接口,一次获取有限数量的信息,直到页面滚动或者用户手动点击加载更多才按需加载更多区域

  • 所有需要搜索区域的功能,添加搜索接口,搜索由后端实现

  • 所有请求设备的接口,只请求当前选中区域的设备、未选中区域的设备不请求

如此一来,就能大大减少页面发送的请求数量,而对用户体验基本又不会有影响,后续采用此方案优化效果明显

引入的其他问题

实际改动提测后,发现涉及到区域勾选+搜索的页面(设置角色权限时,可以批量勾选其拥有权限的区域),会遇到问题。

  • 因为当前是分页加载的,前端在加载完成之前并不知道有多少区域

  • 如果勾选了父区域,子区域也被选中,前端传参直接是父区域ID

  • 考虑如下场景:此时如果取消勾选某个子区域,父区域会变成半选状态,但是如果子区域过多、一页没有加载完,直接确认,前端传参就会变成这一页的所有勾选的子区域ID,剩下没加载的子区域因为前端不知道具体信息,所以不会传给后端,导致实际结果与预期不符

  • 进一步的,涉及勾选后再搜索、搜索后再清除某些已搜索的内容,都会因为前端没有获取过完整的树结构,导致一些不符合预期的结果(讨论过搜索清空已选的方案,认为与实际用户习惯不符,不能接受)

这个问题相对比较棘手,讨论了很久都没有好的解决方案,于是去看了看竞品有没有类似的页面(树组织结构+父子层级+多选)

解决方案

海康云眸社区:https://www.hik-cloud.com/neptune/index.html

有相似功能的有两个页面,其实现还不一样:

  • 父子组织树结构+搜索功能

    • 分页获取

    • 搜索后端完成

  • 父子组织树结构+勾选功能+搜索功能

    • 不分页直接获取完整组织树

    • 搜索前端完成

参考这个实现,又想到是不是可以不分页、前端获取完整组织树、搜索前端完成

之前出现性能问题的场景是,有多个根区域、每个根区域又有子区域,这时候获取完根区域之后再去遍历获取每个根区域的子区域,导致并行发送的请求数量很多

可以考虑新增一个获取完整区域树结构的接口:

  • 输入 projectId,输出所有区域信息

  • 输出区域信息不包含层级关系,通过 regionBranch(regionId 完整路径) 来标示父子接哦故

  • 前端用区域信息来构造完整的区域树(一个请求搞定)

  • 搜索功能仍然有前端来完成,保留勾选状态

  • 父子组织树结构+勾选功能+搜索功能的页面使用频次很少,低频次的完整组织树获取也不会对服务有太大影响

  • 从竞品情况来看,3000+个区域,单个接口的响应大小在200K左右(其regionId为40位string,相对较长),单个接口的响应时间在100ms左右,都可以接受

  • 后端接口性能上,响应的都是数据库已有字段,projectId 已加索引,性能风险不大 SELECT regionId, regionName, regionBranch, order form region_info where projectId='xxx';

  • 需要再评估一下前端性能

POST /tums/resources/getRegionTree
Request:
{
    "projectId": "123"
}
Response:
{
    "result": [
        {
            "regionId":"1",
            "regionName": "一",
            "regionBranch": "1-",
            "order": 1
        },
        {
            "regionId": "2",
            "regionName": "二",
            "regionBranch": "1-2-",
            "order": 1
        }
    ],
    "error_code": 0
}

总结与反思

  • 遇到性能问题,先从设计实现的角度去看看,有没有实现不合理的地方,再去着手考虑优化

  • 前端资源按需加载,一次加载不必要的资源,不仅加大了服务压力,还导致响应时间延长影响用户体验

  • 一个问题有解决方案后,不一定对所有业务功能都通用,要灵活考虑