Интервьюер ByteDance, я также внедрил загрузку больших файлов и возобновление точки останова.

полный стек
Интервьюер ByteDance, я также внедрил загрузку больших файлов и возобновление точки останова.

предисловие

Я видел статью несколько дней назад и был глубоко тронут

ByteDance Interviewer: Пожалуйста, реализуйте загрузку больших файлов и возобновление работы точки останова.

Автор реализовал функции загрузки больших файлов фрагментами, возобновления загрузки в точках останова, загрузки в секундах и паузы с 0, а также всесторонне проанализировал этот вопрос интервью простыми словами.

Радужные пуки сильно не дуют, я решил потереть горячие точки, записать видео, добавить авторский полный процесс написания кода, а затем написать эту статью, поэтому, пожалуйста, прочитайте вышеуказанную статью перед едой, я сделал некоторые расширения следующим образом

  1. рассчитатьhashпроблемы, требующие много времени, могут не толькоweb-workder, вы также можете обратиться кReactизFFiberархитектура, черезrequestIdleCallbackЧтобы использовать время простоя браузера для расчета, он не будет блокировать основной поток
  2. документhashРасчет заключается в том, чтобы определить, существует ли файл, а затем реализовать функцию второй передачи, поэтому мы можем обратиться к布隆过滤Концепция устройства, жертвуя небольшой скоростью распознавания в обмен на время, например, мы можем抽样算hash
  3. прошел в текстеweb-workderПозволятьhashВычисление не замораживает основной поток, но в большом файле слишком много фрагментов из-за слишком большого количества фрагментов.HTTPСсылка так же будет зависать в браузере (я пробовал 4G, и он прямо застрял), мы можем управлять асинхронным запросом по并发数Чтобы решить, я помню, что это также вопрос интервью в заголовках
  4. Ход загрузки каждого фрагмента не нужно отображать в таблице, мы заменим его квадратным индикатором выполнения, чтобы он был более прямым (как показано на рисунке).
  5. Как повторить попытку при одновременной загрузке, если сообщается об ошибке. Например, мы разрешаем две попытки для каждого фрагмента, а затем завершаем три раза.
  6. Из-за разных размеров файлов немного неуклюже устанавливать фиксированный размер каждого фрагмента, мы можем обратиться кTCPпротокол慢启动Стратегия, установите начальный размер и динамически настраивайте размер следующего фрагмента в соответствии с завершением задачи загрузки, чтобы гарантировать, что размер фрагмента файла соответствует текущей скорости сети.
  7. Небольшая оптимизация опыта, например, при загрузке
  8. дефрагментация файлов

Существующие фрагменты второго прохода отмечены зеленым цветом, те, которые загружаются, — синим, а параллелизм — 4. Не много чепухи, давайте расцветать код вместе.

Хэш файла расчета кванта времени

На самом деле этоtime-sliceконцепция,ReactсерединаFiberОсновная концепция архитектуры заключается в использовании времени простоя браузера для расчета большого процесса сравнения, а любые высокоприоритетные задачи в середине, такие как анимация и ввод, будут прерывать задачу сравнения. не было уменьшено, это значительно улучшило интерактивный опыт пользователя.

Это, пожалуй, самый распространенный способ открыть React Fiber (разрезка по времени)

requestIdleCallback

window.requestIdleCallback()Метод ставит в очередь функции, которые вызываются в период простоя браузера. Это позволяет разработчикам выполнять фоновую и низкоприоритетную работу в основном цикле событий.requestIdelCallbackВыполняемому методу будет переданdeadlineпараметр, может знать оставшееся время текущего кадра, использование выглядит следующим образом

    requestIdelCallback(myNonEssentialWork);
    
    
    function myNonEssentialWork (deadline) {
    
      // deadline.timeRemaining()可以获取到当前帧剩余时间
      // 当前帧还有时间 并且任务队列不为空
      while (deadline.timeRemaining() > 0 && tasks.length > 0) {
        doWorkIfNeeded();
      }
      if (tasks.length > 0){
        requestIdleCallback(myNonEssentialWork);
      }
    }

Структура дедлайна следующая

interface Dealine {
  didTimeout: boolean // 表示任务执行是否超过约定时间
  timeRemaining(): DOMHighResTimeStamp // 任务可供执行的剩余时间
}

Два кадра на этом рисунке, внутри каждого кадра,TASKа такжеrederingЭто заняло лишь часть времени и не занимало весь кадр, то это время, как показано на рисункеidle periodЧасть — это время простоя, а время простоя в каждом кадре варьируется в зависимости от количества вещей, обрабатываемых в кадре, сложности и т. д., поэтому время простоя не равно.

и для каждогоdeadline.timeRemaining()Возвращаемое значение показано на рисунке,Idle CallbackВремя до конца кадра (в мс)

расчет кванта времени

Продолжим код предыдущей статьи и трансформируем егоcalculateHash


    async calculateHashIdle(chunks) {
      return new Promise(resolve => {
        const spark = new SparkMD5.ArrayBuffer();
        let count = 0;
        // 根据文件内容追加计算
        const appendToSpark = async file => {
          return new Promise(resolve => {
            const reader = new FileReader();
            reader.readAsArrayBuffer(file);
            reader.onload = e => {
              spark.append(e.target.result);
              resolve();
            };
          });
        };
        const workLoop = async deadline => {
          // 有任务,并且当前帧还没结束
          while (count < chunks.length && deadline.timeRemaining() > 1) {
            await appendToSpark(chunks[count].file);
            count++;
            // 没有了 计算完毕
            if (count < chunks.length) {
              // 计算中
              this.hashProgress = Number(
                ((100 * count) / chunks.length).toFixed(2)
              );
              // console.log(this.hashProgress)
            } else {
              // 计算完毕
              this.hashProgress = 100;
              resolve(spark.end());
            }
          }
          window.requestIdleCallback(workLoop);
        };
        window.requestIdleCallback(workLoop);
      });
    },

В процессе расчета поместите поле ввода на страницу, введите мощность без давления, квант времени

Картинка вышеReact15 иFiberПо сравнению с архитектурой видно, что количество задач на рисунке ниже безгранично, но оно стало рассеянным, а основной поток не перемешан.

образец хэша

файл расчетаmd5Роль значения заключается не более чем в том, чтобы определить, существует ли файл, мы можем рассмотреть возможность разработки выборкиhash, жертвуя некоторыми показателями попаданий для повышения эффективности, идеи дизайна заключаются в следующем.

  1. Файл нарезан на кусочки по 2M
  2. Все содержимое первого и последнего слайсов, а остальные слайсы занимают по 2 байта в каждом из первых, средних и последних трех мест.
  3. Суммарное содержание, рассчитанноеmd5, назови это影分身Hash
  4. этоhashВ результате получается, что файл существует, есть небольшая вероятность неправильной оценки, но если он не существует, то он точен на 100%, что чем-то похоже на идею фильтра Блума, можно рассмотреть дваhashС использованием
  5. Я попробовал файл 1,5G на своем компьютере.Полный объем занимает около 20 секунд, чтобы получить полный объем около 20 секунд.Выборка составляет около 1 секунды, что все еще очень хорошо.Его можно использовать, чтобы определить, существует ли файл .
  6. я немного умный

抽样md5: 1028.006103515625ms

全量md5: 21745.13916015625ms

    async calculateHashSample() {
      return new Promise(resolve => {
        const spark = new SparkMD5.ArrayBuffer();
        const reader = new FileReader();
        const file = this.container.file;
        // 文件大小
        const size = this.container.file.size;
        let offset = 2 * 1024 * 1024;

        let chunks = [file.slice(0, offset)];

        // 前面100K

        let cur = offset;
        while (cur < size) {
          // 最后一块全部加进来
          if (cur + offset >= size) {
            chunks.push(file.slice(cur, cur + offset));
          } else {
            // 中间的 前中后去两个字节
            const mid = cur + offset / 2;
            const end = cur + offset;
            chunks.push(file.slice(cur, cur + 2));
            chunks.push(file.slice(mid, mid + 2));
            chunks.push(file.slice(end - 2, end));
          }
          // 前取两个字节
          cur += offset;
        }
        // 拼接
        reader.readAsArrayBuffer(new Blob(chunks));
        reader.onload = e => {
          spark.append(e.target.result);

          resolve(spark.end());
        };
      });
    }

Контроль параллелизма сетевых запросов

большой файлhashПосле расчета отправляйте сотни за разhttpЗапрос, не рассчитал результаты хеш-картыTCPПроцесс создания убил браузер, и я помню, что сам контроль количества одновременных асинхронных запросов - это вопрос интервью в Toutiao

Идея на самом деле не сложная, то есть мы ставим асинхронные запросы в очередь, например, если количество параллелизма равно 3, мы сначала инициируем 3 запроса одновременно, а потом запрос завершается, а затем мы можно инициировать следующий запрос.Идея ясна,и код тоже готов выйти

Мы управляем параллелизмом через concurrency max и инициируем запросmax--, чтобы завершить запросmax++Только что


+async sendRequest(forms, max=4) {
+  return new Promise(resolve => {
+    const len = forms.length;
+    let idx = 0;
+    let counter = 0;
+    const start = async ()=> {
+      // 有请求,有通道
+      while (idx < len && max > 0) {
+        max--; // 占用通道
+        console.log(idx, "start");
+        const form = forms[idx].form;
+        const index = forms[idx].index;
+        idx++
+        request({
+          url: '/upload',
+          data: form,
+          onProgress: this.createProgresshandler(this.chunks[index]),
+          requestList: this.requestList
+        }).then(() => {
+          max++; // 释放通道
+          counter++;
+          if (counter === len) {
+            resolve();
+          } else {
+            start();
+          }
+        });
+      }
+    }
+    start();
+  });
+}

async uploadChunks(uploadedList = []) {
  // 这里一起上传,碰见大文件就是灾难
  // 没被hash计算打到,被一次性的tcp链接把浏览器稿挂了
  // 异步并发控制策略,我记得这个也是头条一个面试题
  // 比如并发量控制成4
  const list = this.chunks
    .filter(chunk => uploadedList.indexOf(chunk.hash) == -1)
    .map(({ chunk, hash, index }, i) => {
      const form = new FormData();
      form.append("chunk", chunk);
      form.append("hash", hash);
      form.append("filename", this.container.file.name);
      form.append("fileHash", this.container.hash);
      return { form, index };
    })
-     .map(({ form, index }) =>
-       request({
-           url: "/upload",
-         data: form,
-         onProgress: this.createProgresshandler(this.chunks[index]),
-         requestList: this.requestList
-       })
-     );
-   // 直接全量并发
-   await Promise.all(list);
     // 控制并发  
+   const ret =  await this.sendRequest(list,4)

  if (uploadedList.length + list.length === this.chunks.length) {
    // 上传和已经存在之和 等于全部的再合并
    await this.mergeRequest();
  }
},

Кстати, я также задал еще один вопрос интервью на ByteDance, я не знаю, смогу ли я пройти их сторону.

Реализация стратегии медленного старта

Проблема управления перегрузкой TCPПо сути, это динамическая настройка размера слайса в соответствии с текущей ситуацией в сети.

  1. chunkремень наsizeзначение, но количество индикаторов выполнения неизвестно, изменитеcreateFileChunk, запрос плюс статистика времени)
  2. Например, наш идеал — доставить одну за 30 секунд.
  3. Начальный размер настроен на 1 м, если потребовалось 10 секунд для загрузки, то следующий размер блока становится 3М
  4. Если загрузка заняла 60 секунд, размер следующего блока становится 500 КБ и так далее.
  5. Логика параллелизма + медленный старт немного сложна, я пока не разобрался, 囧 поэтому сначала пропускайте только один слайс за раз, чтобы продемонстрировать эту логику, создайте новыйhandleUpload1функция
async handleUpload1(){
      // @todo数据缩放的比率 可以更平缓  
      // @todo 并发+慢启动

      // 慢启动上传逻辑 
      const file = this.container.file
      if (!file) return;
      this.status = Status.uploading;
      const fileSize = file.size
      let offset = 1024*1024 
      let cur = 0 
      let count =0
      this.container.hash = await this.calculateHashSample();

      while(cur<fileSize){
        // 切割offfset大小
        const chunk = file.slice(cur, cur+offset)
        cur+=offset
        const chunkName = this.container.hash + "-" + count;
        const form = new FormData();
        form.append("chunk", chunk);
        form.append("hash", chunkName);
        form.append("filename", file.name);
        form.append("fileHash", this.container.hash);
        form.append("size", chunk.size);

        let start = new Date().getTime()
        await request({ url: '/upload',data: form })
        const now = new Date().getTime()
        
        const time = ((now -start)/1000).toFixed(4)
        let rate = time/30
        // 速率有最大2和最小0.5
        if(rate<0.5) rate=0.5
        if(rate>2) rate=2
        // 新的切片大小等比变化
        console.log(`切片${count}大小是${this.format(offset)},耗时${time}秒,是30秒的${rate}倍,修正大小为${this.format(offset/rate)}`)
        // 动态调整offset
        offset = parseInt(offset/rate)
        // if(time)
        count++
      }
    }

Отрегулируйте медленную скорость сети 3G, чтобы увидеть эффект

切片0大小是1024.00KB,耗时13.2770秒,是30秒的0.5倍,修正大小为2.00MB
切片1大小是2.00MB,耗时25.4130秒,是30秒的0.8471倍,修正大小为2.36MB
切片2大小是2.36MB,耗时14.1260秒,是30秒的0.5倍,修正大小为4.72MB

возьми

Оптимизация индикатора выполнения

Это небольшая оптимизация, которая удобна для нас, чтобы проверить существующие блоки файлов и числа параллелизма, вдохновленные сканированием жесткого диска.

  <div class="cube-container" :style="{width:cubeWidth+'px'}">
    <div class="cube" 
      v-for="chunk in chunks" 
      :key="chunk.hash">
      <div           
        :class="{
        'uploading':chunk.progress>0&&chunk.progress<100, 
        'success':chunk.progress==100
        }" 
        :style="{height:chunk.progress+'%'}"
        >
        
        <i v-if="chunk.progress>0&&chunk.progress<100" class="el-icon-loading" style="color:#F56C6C;"></i>
      </div>
    </div>
  </div>

.cube-container
  width 100px
  overflow hidden
.cube
  width 14px
  height 14px
  line-height 12px;
  border 1px solid black
  background  #eee
  float left
  >.success
    background #67C23A
  >.uploading
    background #409EFF

// 方块进度条尽可能的正方形 切片的数量平方根向上取整 控制进度条的宽度

cubeWidth(){
  return Math.ceil(Math.sqrt(this.chunks.length))*16
},

Эффект снова виден 🐶

Параллельная повторная попытка + ошибка

  1. Запросить error.catch, поставить задачу обратно в очередь
  2. После ошибки прогресс устанавливается в -1 и индикатор выполнения отображается красным цветом
  3. Массив хранит количество повторных попыток для каждого запроса хэша файла и накапливает их, например[1,0,2], то есть 0-й слайс файла сообщает об ошибке один раз, а второй сообщает об ошибке дважды
  4. более 3 прямыхreject

Во-первых, внутренняя симуляция сообщает об ошибке

      if(Math.random()<0.5){
        // 概率报错
        console.log('概率报错了')
        res.statusCode=500
        res.end()
        return 
      }

async sendRequest(urls, max=4) {
-      return new Promise(resolve => {
+      return new Promise((resolve,reject) => {
         const len = urls.length;
         let idx = 0;
         let counter = 0;
+        const retryArr = []

 
         const start = async ()=> {
           // 有请求,有通道
-          while (idx < len && max > 0) {
+          while (counter < len && max > 0) {
             max--; // 占用通道
             console.log(idx, "start");
-            const form = urls[idx].form;
-            const index = urls[idx].index;
-            idx++
+            // 任务不能仅仅累加获取,而是要根据状态
+            // wait和error的可以发出请求 方便重试
+            const i = urls.findIndex(v=>v.status==Status.wait || v.status==Status.error )// 等待或者error
+            urls[i].status = Status.uploading
+            const form = urls[i].form;
+            const index = urls[i].index;
+            if(typeof retryArr[index]=='number'){
+              console.log(index,'开始重试')
+            }
             request({
               url: '/upload',
               data: form,
               onProgress: this.createProgresshandler(this.chunks[index]),
               requestList: this.requestList
             }).then(() => {
+               urls[i].status = Status.done
               max++; // 释放通道
               counter++;
+              urls[counter].done=true
               if (counter === len) {
                 resolve();
               } else {
                 start();
               }
-            });
+            }).catch(()=>{
+               urls[i].status = Status.error
+               if(typeof retryArr[index]!=='number'){
+                  retryArr[index] = 0
+               }
+              // 次数累加
+              retryArr[index]++
+              // 一个请求报错3次的
+              if(retryArr[index]>=2){
+                return reject()
+              }
+              console.log(index, retryArr[index],'次报错')
+              // 3次报错以内的 重启
+              this.chunks[index].progress = -1 // 报错的进度条
+              max++; // 释放当前占用的通道,但是counter不累加
+              
+              start()
+            })
           }
         }
         start();

}

Как показано на рисунке, после сообщения об ошибке блок станет красным, но попытка будет повторена.

дефрагментация файлов

Если многие люди уйдут после того, как пройдут половину, эти ломтики не имеют смысла существовать. Вы можете подумать о том, чтобы регулярно их чистить. Конечно, мы можем использоватьnode-scheduleуправлять запланированными задачами Например, мы сканируем один раз в день.target, если время модификации файла месяц назад, удалите его напрямую

// 为了方便测试,我改成每5秒扫一次, 过期1钟的删除做演示
const fse = require('fs-extra')
const path = require('path')
const schedule = require('node-schedule')


// 空目录删除
function remove(file,stats){
    const now = new Date().getTime()
    const offset = now - stats.ctimeMs 
    if(offset>1000*60){
        // 大于60秒的碎片
        console.log(file,'过期了,浪费空间的玩意,删除')
        fse.unlinkSync(file)
    }
}

async function scan(dir,callback){
    const files = fse.readdirSync(dir)
    files.forEach(filename=>{
        const fileDir = path.resolve(dir,filename)
        const stats = fse.statSync(fileDir)
        if(stats.isDirectory()){
            return scan(fileDir,remove)
        }
        if(callback){
            callback(fileDir,stats)
        }
    })
}
// *    *    *    *    *    *
// ┬    ┬    ┬    ┬    ┬    ┬
// │    │    │    │    │    │
// │    │    │    │    │    └ day of week (0 - 7) (0 or 7 is Sun)
// │    │    │    │    └───── month (1 - 12)
// │    │    │    └────────── day of month (1 - 31)
// │    │    └─────────────── hour (0 - 23)
// │    └──────────────────── minute (0 - 59)
// └───────────────────────── second (0 - 59, OPTIONAL)
let start = function(UPLOAD_DIR){
    // 每5秒
    schedule.scheduleJob("*/5 * * * * *",function(){
        console.log('开始扫描')
        scan(UPLOAD_DIR)
    })
}
exports.start = start

开始扫描
/upload/target/625c.../625c...-0 过期了,删除
/upload/target/625c.../625c...-1 过期了,删除
/upload/target/625c.../625c...-10 过期了,删除
/upload/target/625c.../625c...-11 过期了,删除
/upload/target/625c.../625c...-12 过期了,删除


Последующее расширение и мышление

Оставьте несколько вопросов на размышление, и напишите статью в следующий раз, чтобы это реализовать, чтобы было удобно продолжать тереть тепло

  1. Совместимость с requestIdleCallback, как реализовать самостоятельно
    1. React — это еще и логика планирования, написанная сама собой, у меня будет возможность написать статью в будущем.
    2. Собственная реализация React requestIdleCallback
  2. Параллелизм + медленное начало сотрудничества
  3. Хэш сэмплирования + полный хеш + координация кванта времени
  4. Загрузка большого куска файла
    1. Та же логика нарезки, получение content-Length через запрос axios.head
    2. Вы можете загружать фрагментами, используя заголовок http Range, а остальная логика аналогична загрузке
  5. Небольшая оптимизация опыта
    1. Такие как напоминание покинуть страницу и т. д. небольшие советы
  6. Изменения с медленным запуском должны быть более плавными, например, с использованием тригонометрических функций, чтобы ограничить плавность скорости изменения от 0,5 до 1,5.
  7. продвижение веб-сокета

думать и суммирование

  1. Любые, казалось бы, простые потребности станут очень сложными после увеличения величины, и, как и жизнь
  2. Загрузка файлов проста, большие файлы сложны, автономные — простые, а распределенные — сложные.
  3. Даже простая функция leftPad (заполнение символов слева), учитывая производительность, производительность метода дихотомии убивает соединение массива за секунды, вызывая много дискуссий
    1. О реализации функции левой панели
    2. Любая точка знаний стоит копаться в
  4. В следующий раз продакт-менеджер скажет, что спрос простой, просто дайте ему
  5. Я собираюсь объединить предыдущее и записать трогательное видео с анализом загрузки большого файла, так что следите за обновлениями.

код

Первая половина была плагиатом@yeyan1996Код в основном предназначен для иллюстрации идеи, реализация относительно грубая, пожалуйста, слегка распылитеGitHub.com/Worry Gold…

Добро пожаловать, чтобы следовать

использованная литература

Некоторые картинки также скопированы мной напрямую по ссылке ниже

  1. Различные стратегии загрузки файлов, написанные для начинающих пользователей.
  2. ByteDance Interviewer: Пожалуйста, реализуйте загрузку больших файлов и возобновление работы точки останова.
  3. Это, пожалуй, самый распространенный способ открыть React Fiber (разрезка по времени)
  4. народный фильтр Блума
  5. requestIdleCallback
  6. Проблема управления перегрузкой TCP
  7. Анализ вопросов интервью 丨 Загрузка фрагмента файла реализации Python
  8. requestIdleCallback — планирование фоновых задач