我们一起聊聊前端接口容灾

发表于:2023-12-01 09:18

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:小歪    来源:政采云技术

#
前端
  开篇
  你说,万一接口挂了会怎么样?
  还能咋样,白屏呗。
  有没有不白屏的方案?
  有啊,还挺简单的。
  容我细细细细分析。
  原因就是接口挂了,拿不到数据了。那把数据储存起来就可以解决问题。
  思考
  存哪里?
  第一时间反应浏览器本地存储,想起了四兄弟。
  选型对比
  考虑到需要存储的数据量,5MB 一定不够的,所以选择了 IndexDB。
  考虑新用户或者长时间未访问老用户,会取不到缓存数据与陈旧的数据。
  因此准备上云,用阿里云存储,用 CDN 来保障。
  总结下:线上 CDN、线下 IndexDB。
  整体方案
  整体流程图
  CDN
  先讲讲线上 CDN。
  通常情况下可以让后端支撑,本质就是更新策略问题,这里不细说。
  我们讲讲另外一种方案,单独启个 Node 服务更新 CDN 数据。
  流程图
  劫持逻辑
  劫持所有接口,判断接口状态与缓存标识。从而进行更新数据、获取数据、缓存策略三种操作
  通过配置白名单来控制接口存与取
  axios.interceptors.response.use(
        async (resp) => {
          const { config } = resp
          const { url } = config
          // 是否有缓存tag,用于更新CDN数据。目前是定时服务在跑,访问页面带上tag
          if (this.hasCdnTag() && this.isWhiteApi(url)) {
            this.updateCDN(config, resp)
          }
          return resp;
        },
        async (err) => {
          const { config } = err
          const { url } = config
          // 是否命中缓存策略
          if (this.isWhiteApi(url) && this.useCache()) {
            return this.fetchCDN(config).then(res => {
              pushLog(`cdn缓存数据已命中,请处理`, SentryTypeEnum.error)
              return res
            }).catch(()=>{
             pushLog(`cdn缓存数据未同步,请处理`, SentryTypeEnum.error)
            })
          }
        }
      );
  缓存策略
  累计接口异常发生 maxCount 次,打开缓存开关,expiresSeconds 秒后关闭。
  缓存开关用避免网络波动导致命中缓存,设置了阀值。
  /*
  * 缓存策略
  */
  useCache = () => {
    if (this.expiresStamp > +new Date()) {
      const d = new Date(this.expiresStamp)
      console.warn(`
      ---------------------------------------
      ---------------------------------------
      启用缓存中
      关闭时间:${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}
      ---------------------------------------
      ---------------------------------------
      `)
      return true
    }
    this.errorCount += 1
    localStorage.setItem(CACHE_ERROR_COUNT_KEY, `${this.errorCount}`)
    if (this.errorCount > this.maxCount) {
      this.expiresStamp = +new Date() + this.expiresSeconds * 1000
      this.errorCount = 0
      localStorage.setItem(CACHE_EXPIRES_KEY, `${this.expiresStamp}`)
      localStorage.removeItem(CACHE_ERROR_COUNT_KEY)
      return true
    }
    return false
  }
  唯一标识
  根据 method、url、data 三者来标识接口,保证接口的唯一性
  带动态标识,譬如时间戳等可以手动过滤
  /**
   * 生成接口唯一键值
  */
  generateCacheKey = (config) => {
    // 请求方式,参数,请求地址,
    const { method, url, data, params } = config;
    let rawData = ''
    if (method === 'get') {
      rawData = params
    }
    if (method === 'post') {
      rawData = JSON.parse(data)
    }
    // 返回拼接key
    return `${encodeURIComponent([method, url, stringify(rawData)].join('_'))}.json`;
  };
  更新数据
  /**
   * 更新cdn缓存数据
  */
  updateCDN = (config, data) => {
    const fileName = this.generateCacheKey(config)
    const cdnUrl = `${this.prefix}/${fileName}`
    axios.post(`${this.nodeDomain}/cdn/update`, {
      cdnUrl,
      data
    })
  }
  Node定时任务
  构建定时任务,用 puppeteer 去访问、带上缓存标识,去更新 CDN 数据
  import schedule from 'node-schedule';
  const scheduleJob = {};
  export const xxxJob = (ctx) => {
    const { xxx } = ctx.config;
    ctx.logger.info(xxx, 'xxx');
    const { key, url, rule } = xxx;
    if (scheduleJob[key]) {
      scheduleJob[key].cancel();
    }
    scheduleJob[key] = schedule.scheduleJob(rule, async () => {
      ctx.logger.info(url, new Date());
      await browserIndex(ctx, url);
    });
  };
  export const browserIndex = async (ctx, domain) => {
    ctx.logger.info('browser --start', domain);
    if (!domain) {
      ctx.logger.error('domain为空');
      return false;
    }
    const browser = await puppeteer.launch({
      args: [
        '--use-gl=egl',
        '--disable-gpu',
        '--no-sandbox',
        '--disable-setuid-sandbox',
      ],
      executablePath: process.env.CHROMIUM_PATH,
      headless: true,
      timeout: 0,
    });
    const page = await browser.newPage();
    await page.goto(`${domain}?${URL_CACHE_KEY}`);
    await sleep(10000);
    // 访问首页所有查询接口
    const list = await page.$$('.po-tabs__item');
    if (list?.length) {
      for (let i = 0; i < list.length; i++) {
        await list[i].click();
      }
    }
    await browser.close();
    ctx.logger.info('browser --finish', domain);
    return true;
  };
  效果
  手动 block 整个 domain,整个页面正常展示:
  IndexDB
  线上有 CDN 保证了,线下就轮到 IndexDB 了,基于业务简单的增删改查,选用 localForage 三方库足矣。
  axios.interceptors.response.use(
        async (resp) => {
          const { config } = resp
          const { url } = config
          // 是否有缓存tag,用于更新CDN数据。目前是定时服务在跑,访问页面带上tag
          if (this.hasCdnTag() && this.isWhiteApi(url)) {
            this.updateCDN(config, resp)
          }
          if(this.isIndexDBWhiteApi(url)){
            this.updateIndexDB(config, resp)
          }
          return resp;
        },
        async (err) => {
          const { config } = err
          const { url } = config
          // 是否命中缓存策略
          if (this.isWhiteApi(url) && this.useCache()) {
            return this.fetchCDN(config).then(res => {
              pushLog(`cdn缓存数据已命中,请处理`, SentryTypeEnum.error)
              return res
            }).catch(()=>{
             pushLog(`cdn缓存数据未同步,请处理`, SentryTypeEnum.error)
             if(this.isIndexDBWhiteApi(url)){
               return this.fetchIndexDB(config).then(res => {
                pushLog(`IndexDB缓存数据已命中,请处理`, SentryTypeEnum.error)
                return res
              }).catch(()=>{
               pushLog(`IndexDB缓存数据未同步,请处理`, SentryTypeEnum.error)
              })
             }
            })
          }
        }
      );
  总结
  总结下,优点包括不入侵业务代码,不影响现有业务,随上随用,尽可能避免前端纯白屏的场景,成本低。劣势包括使用局限,不适合对数据实效性比较高的业务场景,不支持 IE 浏览器。
  接口容灾我们也是刚弄不久,有许多细节与不足,欢迎沟通交流。
  接口容灾本意是预防发生接口服务挂了的场景,我们不会很被动。原来是P0的故障,能被它降低为 P2、P3,甚至在某些场景下都不会有用户反馈。
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号