前端接口容灾

133 阅读3分钟

开篇

你说,万一接口挂了会怎么样?

还能咋样,白屏呗。

有没有不白屏的方案?

有啊,还挺简单的。

容我细细细细分析。

原因就是接口挂了,拿不到数据了。那把数据储存起来就可以解决问题。

存哪里?

第一时间反应浏览器本地存储,想起了四兄弟。

选型对比

考虑到需要存储的数据量,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,甚至在某些场景下都不会有用户反馈。