故障排查系列:记一次线上图片 401 问题排查事件

问题描述

一个很小的图片预览功能,在列表里,鼠标浮上去显示 popover,鼠标划走消失,demo 示意图如下:

js 复制代码
<div className='cursor-a' onMouseEnter={handlePopoverOpen} onBlur={handlePopoverClose} onMouseLeave={handlePopoverClose}>

....
// popover 开关
const open = Boolean(anchorEl);

const handlePopoverOpen = (event) => {
  setAnchorEl(event.currentTarget);
};

但是在线上灰度环境测试时,偶尔会出现如下的问题:

图床用的是公司自己的对象存储,每次 hover 打开 popover 后,调用后台 API 获取对象存储的访问 url,然后放到img 的 src 中,这个 url 有 3 秒的时效,超过就401。但是鼠标划上去的动作只有几毫秒,按理说不会存在过期的问题呀。

问题排查

首先怀疑是后端问题[坏笑],会不会是后台获取图床 url 时留有延时,导致压力测试下某一次请求延迟时间超过了 3 s?

打开控制台,找到出现 401 的那次请求,看请求后台的接口:

找到对应的base64编码,解码看一下过期时间:

解析一下看看:

再看看这次 401 请求的地址(src的地址):

解析一下过期时间:

一个 11 秒,一个 14 秒,啊这... 冤枉后端老哥了。是前端请求图片时的 src 用了过去老的 url 了。


重新看了一下,前端组件初次加载没有问题,多次反复加载图片弹窗就会出现 401 的出现。

然后,在弹窗中打印显示这个图片的 url,理论上应该是在初始化加载时是 undefined 的,通过 API 获取后 setInfo,通过 info 里的信息获取 url 给到 img 标签。这里初步排查发现是弹窗中图片的 url 在弹窗销毁时,数据没有清理,导致再次挂载弹窗组件时,在接口请求之前还是会用原来的 url (这里的字段叫 MediaFile) 来请求一次。

美滋滋的处理一下:

js 复制代码
const handlePopoverClose = () => {
  setAnchorEl(null);
  // 清理数据
  setInfo({
    ...info,
    MediaFile: ''
  })
};

于是满怀信心的开始自测,刚开始确实没有出现 401 过期的问题,但是当鼠标不停划过表格各行的对应位置时,还是出现了 401 的问题.... 看来问题不止这一点。

只能继续排查。

期间为了尝试减少其他 props 变动造成的影响,在图片显示的时候,控制单一变量 info.MediaFile, 做了如下缓存:

js 复制代码
const ImageShow = useMemo(() => {
    if (info.MediaFile) {
      return <img
        alt="preview"
        style={{ maxWidth: '80%', height: 'auto', display: 'block', borderRadius: '15px', marginBottom: '4px' }}
        src={`${info.MediaFile}`}
        onError={console.error}
      />
    }

    return null;
}, [info.MediaFile]);

发现还是有问题。看来还是 MediaFile 自身的问题。

最后发现是异步请求的问题😂,下面是 API 获取预览结果的代码示意:

js 复制代码
useEffect(() => {
    if (open && TemplateId) {
      setLoading(true);
      setTimeout(() => {
        QueryMMSTemplate({ TemplateIds: [TemplateId], AccountId })
          .then((res) => {
            if (res && res.Data && res.Data.length && open) {
              console.log('这个时候可能页面已经关闭了')
              setInfo(res.Data[0] || {});
            } else {
              setInfo({});
            }
  
            setLoading(false);
          })
          .catch(() => {
            setInfo({});
            setLoading(false);
          });
      });
    }
}, [open, TemplateId, AccountId]);

异步请求导致返回结果设置 info 不可控,如果请求回来的慢了,刚刚清理数据的 setInfo 就会被覆盖掉,导致 MediaFile 没有被清理掉。但是之前也考虑了这个问题,加了这么一行:

js 复制代码
if (res && res.Data && res.Data.length && open) {}

现在发现,这个里边是个闭包,open 可能一直是 true,即使在接口请求过程中鼠标划走....

于是做一下优化,使用外部 ref:

js 复制代码
const openRef = useRef();
...
 
const handlePopoverOpen = (event) => {
    openRef.current = true;
    setAnchorEl(event.currentTarget);
};

const handlePopoverClose = () => {
    openRef.current = false;  // 加上这样一行
    setAnchorEl(null);
    setInfo({
      ...info,
      MediaFile: ''
    })
};

在 API 请求返回的判断里改写判断条件:

js 复制代码
if (res && res.Data && res.Data.length && openRef.current)

此时再进测试环境测试,发现原来 401 的问题解决了!url 保证每次是最新的即可。

体验优化

在故障解决后,要回归测试,保证原功能没问题的前提下解决新的故障。

测试发现,虽然原故障解决了,但是这个解决方案又引入了新的问题:在关闭 popover 动画执行前,图片url 被手动清空,导致弹窗中图片会突然消失一下,视觉上会造成突变。这里给出两种解决方案:

  1. 无图片或者图片加载时提供模糊展示
js 复制代码
filter: `blur(${(!img && !imgUrl) ? '5px' : 0})`
  1. 用一个占位图片表示

还有个问题,如何API节流,用户不停的在列表上划来划去,API 预览接口一直在调用,造成网络资源浪费。 :

js 复制代码
setLoading(true)
setTimeout(() => {
    if (openRef.current) {
      QueryMMSTemplate({ TemplateIds: [TemplateId], AccountId })
      ...
    }
}, 200);

这里在请求 API 时添加加载冷静期,并在 loading 时放上加载动画:

js 复制代码
{loading ? (
    <CircularProgress />
) : (
    <Review forPopover value={info.Text} ... imgUrl={info.MediaFile} />
)}

复盘

该问题的出现,在于写作时没有注意数据清理,导致老的数据干扰了正常数据。

该问题的避免,一方面是通过代码审查和单元测试提高代码质量,一方面是靠 QA 的充分测试。但是测试的再充分,还是百密一疏。还好该问题的出现是隐式的报错,不会干扰用户的使用。

此外,还要有一套规范的纠错机制,总结一下流程:

graph TD 出现错误 --> 技术支持反馈 --> 技术负责人分析并划分责任人 --> 责任人纠错 --> 补充单元测试 --> QA回归 --> 灰度测试 --> BugFix发布 技术负责人分析并划分责任人 --> 记录问题 责任人纠错 --> 分析原因 记录问题 --> 分析原因 --> 问题归档-可追溯
相关推荐
Gogeof1 天前
云原生化 - 工具镜像(简约版)
微服务·云原生·debug·工具
songgz2 天前
Zig开发环境搭建
vscode·debug·zig
organic6 天前
linux内核调试痛点之函数参数抓捕记
linux·debug·kernel
程序员晚枫8 天前
一不小心,给腾讯云提了一个Bug
github·debug·腾讯
zmc@2 个月前
从MacOS goland无法debug到dns无法解析localhost
macos·golang·debug
G果2 个月前
matlab 小数取余 rem 和 mod有 bug
matlab·debug·rem·mod·取余
计算小屋2 个月前
Linux 安装 GDB (无Root 权限)
linux·c语言·debug·gdb
tekin2 个月前
vue项目源码调试方法 ,chrome调试控制台工作区使用,利用chrome控制台调试vue项目源码的方法 图解
前端·javascript·chrome·webpack·vue·node·debug
理想还很年轻!3 个月前
多表联合的查询(实例)、对于前端返回数据有很多表,可以分开操作、debug调试教程
debug·多表联合查询的实例·分开操作复杂的东西
Feel_狗焕3 个月前
Linux下GDB调试一篇入魂(GDB调试详解)
linux·debug