Front-end встречает Go: новая практика инкрементного обновления статических ресурсов

Node.js Go внешний интерфейс JavaScript

Зачем делать добавочные обновления

За последнее время бизнес Meituan Finance очень быстро развивался. В то же время с ростом бизнеса мы также заметили, что платежная среда многих пользователей на самом деле находится в слабой сетевой среде.

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

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

Как бизнес с частыми выпусками, чтобы уменьшить влияние выпусков, можно выполнить две оптимизации:

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

Для первого пункта у нас есть собственный загрузчик модулей, здесь мы не будем его перечислять, остановимся на вопросе инкрементального обновления.

Что представляет собой процесс инкрементного обновления

Поговорите с картинкой.

增量更新的客户端流程图

Наши добавочные обновления инициируются путем развертывания SDK на стороне браузера, который мы называем Thunder.js.

Thunder.js считывает номер версии последнего статического ресурса со страницы при загрузке страницы. В то же время Thunder.js также будет считывать номер версии, который мы кэшировали, из кеша браузера (обычно это localStorage). Два номера версии совпадают. Если они совпадают, мы можем напрямую использовать версию в кеше; в противном случае мы инициируем запрос на инкрементное исправление в службу инкрементного обновления.

После того как инкрементный сервис получит запрос, он извлечет файлы старой и новой версий для сравнения и вернет разницу в виде исправления. Получив запрос, Thunder.js может исправить старый файл и получить новый.

Одним словом: старые файлы + патчи = новые файлы.

Генерация добавочных исправлений в основном зависит от алгоритма сравнения Майерса. Процесс создания инкрементных исправлений — это процесс нахождения кратчайшего пути редактирования двух строк. Сам алгоритм относительно сложен, вы можете найти более подробные описания алгоритмов в Интернете, например, этот«Алгоритм сравнения Майерса», здесь подробно описываться не будет.

Сам патч представляет собой крошечный DSL (Domain Specific Language). В этом DSL есть три вида микроинструкций, соответствующих трем строковым операциям резервирования, вставки и удаления, каждая из которых имеет свой собственный операнд.

Например, мы хотим генерировать из строки «ABCDEFG» на «ACDZ» на добавочный патч, затем патч на полном тексте, аналогично следующему:

=1\t-1\t=2\t-3\t+z

Во время этого патча вкладка\tразделитель команд,=экспресс-бронирование,-означает удалить,+значит вставить. Весь патч анализируется как:

  1. 1 символ зарезервирован
  2. удалить 1 символ
  3. 2 символа зарезервировано
  4. удалить 3 символа
  5. Вставьте 1 символ:z

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

Инкрементные обновления на самом деле не являются новой интерфейсной технологией.На стороне клиента инкрементные обновления используются уже много лет. видел нас«Практика оптимизации платежей Meituan Financial Scan Code при загрузке статических ресурсов»Друзья, вы должны знать, что мы фактически практиковались ранее. В то время только дополнительные обновления могут сэкономить более 30 ГБ трафика в день. И теперь этот номер стал выше объемом бизнеса.

Итак, мы все сделали без забот?

Что пошло не так с нашей предыдущей практикой инкрементного обновления

Наша главная проблема заключается в том, что инкрементальные вычисления недостаточно быстры.

В предыдущей практике оптимизации большинство наших оптимизаций фактически были направлены на оптимизацию скорости инкрементных вычислений. Скорость инкрементального расчета текста очень низкая, насколько она медленная? Инкрементальный расчет выполняется с общим размером ресурса JS во фронтенде — 200 КБ, время инкрементного расчета варьируется от десятков миллисекунд до десятков секунд или даже десятков секунд в зависимости от объема текста.

Для сервисов с низким трафиком однократное вычисление добавочных исправлений, а затем их кэширование не будет иметь большого значения, даже если первое вычисление займет некоторое время. Однако бизнес-трафик на стороне пользователя относительно велик, количество инкрементных вычислений в месяц превышает 100 000, а пиковое количество одновременных вычислений превышает 100 запросов в секунду.

Итак, каковы последствия недостаточной скорости?

Node.js 的事件循环

Общая идея нашего предыдущего дизайна заключается в том, чтобы использовать один сервис для сбора трафика, а затем использовать другой сервис для выполнения инкрементных вычислений. Обе службы реализованы на Node.js. Для первого случая модель цикла событий Node.js подходит для бизнеса с интенсивным вводом-выводом, однако для второго это фактически слабость Node.js. Модель цикла событий Node.js требует, чтобы использование Node.js гарантировало, что цикл Node.js может работать в любое время.Если есть очень трудоемкая функция, цикл событий попадет в нее и не сможет выполнять другие задачи вовремя. Распространенный метод — открыть несколько процессов Node.js на компьютере. Однако у обычного сервера всего 8 логических ЦП.Для добавочных вычислений, когда мы сталкиваемся с задачами с большим объемом вычислений, 8 параллельных вычислений может затруднить продолжение ответа службы Node.js. Дальнейшее увеличение количества процессов повлечет за собой дополнительные затраты на переключение процессов, что не является для нас оптимальным выбором.

Возможные решения для повышения производительности

Проблема «заставить JavaScript работать быстрее» изучалась многими предшественниками. Пока мы размышляли над этим вопросом, рассматривались три варианта.

Node.js Addon

Надстройка Node.js — это официальный подключаемый модуль для Node.js. Это решение позволяет разработчикам писать код на C/C++, а затем загружать и вызывать его с помощью Node.js. Поскольку производительность самого нативного кода относительно высока, это очень простая оптимизация.

ASM.js / WebAssembly

Последние две схемы являются схемами на стороне браузера.

Среди них ASM.js был предложен Mozilla и использует легко оптимизируемое подмножество JavaScript. Сейчас от этой схемы отказались.

Вместо этого WebAssembly, возглавляемый W3C, использует более компактный байт-код, похожий на ассемблер, для ускорения работы. В настоящее время он только появился на рынке, и связанная с ним цепочка инструментов еще совершенствуется. В некоторых случаях Mozilla уже попробовала себя, например, скомпилировала код Rust в WebAssembly для ускорения парсинга исходной карты.

Однако, рассмотрев эти три варианта, хорошего вывода мы не получили. Все три схемы могут улучшить производительность JavaScript, но какая бы из них ни была принята, время расчета одного патча не может быть уменьшено с десятков секунд до миллисекунд. Более того, если эти три решения не модифицировать, они все равно будут работать в основном потоке JavaScript, что по-прежнему будет вызывать серьезную блокировку для Node.js.

Поэтому мы начали думать об альтернативах Node.js. Возникла идея смены языков.

изменить язык

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

  • скорость бега
  • Параллельная обработка
  • система типов
  • управление зависимостями
  • Сообщество

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

В конце концов, мы решили использовать язык Go для новой практики инкрементных вычислительных сервисов.

Что дает выбор Go

высокая производительность

Алгоритм генерации инкрементных патчей, в реализации Node.js, соответствуетdiffpackage; в реализации Go соответствующийgo-diffМешок.

Прежде чем мы начали, мы сначала протестировали инкрементные модули в Go и Node.js с двумя фактическими наборами файлов, чтобы убедиться, что мы на правильном пути.

相同算法、相同文件的计算时间对比

Результаты показывают, что высокая производительность Go по-прежнему превосходит Node.js с точки зрения вычислительной производительности, несмотря на разные ситуации для разных файлов. Здесь следует отметить, что длина файла — не единственный фактор, влияющий на время расчета, еще одним важным фактором является разница в размере файла.

Различные модели параллелизма

Язык Go — это язык системного программирования, запущенный Google. Он имеет простой синтаксис, легкую отладку, отличную производительность и хорошую экологическую среду сообщества. В отличие от того, как Node.js выполняет параллелизм, язык Go использует облегченные потоки или сопрограммы для параллелизма.

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

Как упоминалось выше, если основной поток Node.js пойман в функции с большим объемом вычислений, весь цикл событий будет заблокирован. Сопрограммы разные, у каждой сопрограммы есть вычислительные задачи, и эти вычислительные задачи планируются с помощью планирования сопрограммы. Вообще говоря, система планирования не будет отдавать все ресурсы ЦП одной и той же сопрограмме, а будет координировать использование ресурсов каждой сопрограммой и делить ресурсы ЦП настолько поровну, насколько это возможно.

Go 的 goroutine

По сравнению с Node.js этот метод больше подходит для сервисов, которые требуют больших вычислительных ресурсов и операций ввода-вывода.

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

На самом деле приложения типа Web-сервиса обычно начинаются с请求 -> 返回Для запуска модели каждый запрос редко взаимодействует с другими запросами, поэтому блокировки используются редко. Некоторые требования класса «счетчик» также могут быть легко выполнены с помощью атомарных переменных.

Различный модульный механизм

Управление зависимостями модулей в Go не так развито, как в Node.js. Хотя есть много людей, которые жалуются на node_modules, мы должны признать, что механизм CMD Node.js не только прост для нас в освоении, но и обязанности и границы каждого модуля очень ясны.

В частности, модуль Node.js заботится только о том, от каких модулей и где он зависит, но не о том, как от него зависят другие. Это можно получить изrequireЗвонок показывает:

const util = require('./util');
const http = require('http');

module.exports = {};

Это очень простой модуль, который зависит от двух других модулей, гдеutilиз нашего локального каталога, аhttpЭто происходит из встроенного Node.js. В этом случае, пока у вас есть хорошие зависимости модуля, если модуль, который вы написали сами, хочет быть повторно использованным другими, вам нужно только загрузить весь каталог независимо, чтобыnpmна.

Проще говоря, иерархия модулей Node.js представляет собой дерево, а конечный локальный модуль выглядит так:

|- src
    |- module-a
        |- submodule-aa
        |- submodule-ab
    |- module-b
    |- module-c
        |- submodule-ca
            |- subsubmodule-caa
|- bin
|- docs

Но язык Go отличается. В Go каждый модуль имеет не только короткое имя модуля, но и «уникальный путь» внутри проекта. Если вам нужно сослаться на модуль, вам нужно использовать этот «уникальный путь» для ссылки на него. Например:

package main

import (
    "fmt"
    "github.com/valyala/fasthttp"
    "path/to/another/local/module"
)

первый иждивенецfmtЭто простой и понятный модуль, поставляемый с Go. Второй модуль — это сторонний модуль с открытым исходным кодом, расположенный на Github.Из формы пути можно сделать грубый вывод, что это сторонний модуль. Третий — повторно используемый модуль в нашем проекте, что немного неуместно. На самом деле, если Go поддерживает отношения вложенных модулей, это эквивалентно подсчету каждой зависимости от корневого каталога, что позволяет избежать возникновения../../../../root/somethingТакой неловкий взгляд вверх. Однако Go не поддерживает вложение папок между локальными зависимостями. Таким образом, все локальные модули будут расположены в одном каталоге, который в итоге станет таким:

|- src
    |- module-a
    |- submodule-aa
    |- submodule-ab
    |- module-b
    |- module-c
    |- submodule-ca
    |- subsubmodule-caa
|- bin
|- docs

Теперь вы вряд ли будете напрямую удалять модуль по каталогу, потому что связь между ними вообще не может определяться каталогом.

В более новых версиях Go рекомендуется размещать сторонние модули вvendorкаталог иsrcэто равноправные отношения. Ранее эти сторонние зависимости также размещались наsrcЗдесь очень запутанно.

В настоящее время масштаб кода нашего проекта не очень велик, и его можно отличить по именованию, но когда проект продолжит расти, потребуется лучшее решение.

Чрезмерно простое децентрализованное стороннее управление пакетами

И тамnpmЕще одно отличие Node.js: язык Go не имеет собственной платформы управления пакетами. Для цепочки инструментов Go не имеет значения, кто размещает ваши сторонние пакеты. Сторонние пакеты Go в сообществе находятся на различных платформах хостинга Git, что не только заставляет нас тратить больше времени на поиск пакетов, но и усложняет задачу, связанную с тем, что мы не можем создать аналогичный пакет.npmЗеркальная платформа сокращает время, затрачиваемое всеми на загрузку сторонних пакетов каждый раз, а также затрудняет бесплатную установку пакетов, не полагаясь на внешнюю сеть.

В Go есть инструмент командной строки для загрузки сторонних пакетов под названием «go-get". Вопреки тому, что все думают, этот инструментнет файла описания версии. В мире Го нетpackage.jsonтакой файл. Прямое влияние, которое это оказывает на нас, заключается в том, что наши зависимости не только размещаются во внешней сети, но и не могут эффективно ограничивать версию. тот самыйgo-getКоманда, версия которой была загружена в этом месяце, может незаметно измениться к следующему месяцу.

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

Go 社区的各种第三方包管理工具

Для зеркалирования на данный момент нет хорошего решения, ссылаемся на практику moby (то есть docker) и храним сторонний пакет прямо в Git собственного проекта. Таким образом, хотя размер исходного кода проекта стал больше, будь то новички, участвующие в проекте или запускающие версию, нет необходимости выходить во внешнюю сеть, чтобы тянуть зависимости.

Отсутствие поддержки внутренней инфраструктуры

Язык Go редко используется в Meituan.Прямым следствием этого является то, что значительная часть инфраструктуры Meituan не поддерживает SDK языка Go. Например, в созданный компанией Redis Cluster самостоятельно внесены некоторые изменения в соответствии с бизнес-потребностями компании, поэтому SDK Redis Cluster с открытым исходным кодом нельзя использовать напрямую. Для другого примера компания использовала базу данных KV с открытым исходным кодом Taobao — Tair, вероятно, потому, что опенсорс был раньше, а Go SDK не было.

Поскольку нам нужно полагаться на базу данных KV для хранения в нашей архитектуре, мы, наконец, решили реализовать Tair SDK на языке Go. Так называемый «чтобы хорошо работать, нужно сначала заточить свои инструменты», в процессе написания SDK мы постепенно ознакомились с некоторыми парадигмами программирования Go, что сыграло очень полезную роль при реализации нашей системы. в будущем. Так что иногда отсутствие доступных мощностей - это не обязательно плохо, но нельзя делать колеса вслепую, а надо думать о смысле изготовления собственных колес и судить по результатам.

внешний язык

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

Как бороться с массовым всплеском трафика

Благодаря извлеченным урокам мы очень хорошо знаем, с каким уровнем трафика мы сталкиваемся. Поэтому на этот раз при архитектурном проектировании системы мы отдали приоритет тому, как справиться с внезапным массовым трафиком.

Сначала поговорим о том, почему у нас всплеск трафика.

Что касается внешнего интерфейса, каждый раз, когда веб-страница обновляется и выпускается, она фактически высвобождает новые статические ресурсы и соответствующие файлы HTML. Для служб добавочного обновления новые статические ресурсы также означают новые вычисления.

Опытные студенты, изучающие фронтенд, могут сказать, что, хотя запуск новой версии создаст новые вычисления, пока уровень CDN размещен впереди, а результаты вычислений кэшируются, давление можно легко снять, верно?

Это имеет смысл, но это не так просто. У продуктов C-end для рядовых потребителей есть особенность, то есть частота посещения пользователями варьируется в широких пределах. Конкретно для добавочных обновлений будет большое количество разных добавочных запросов. Поэтому мы сделали больше дизайнов, чтобы облегчить эту ситуацию.

增量服务架构设计

Это наш дизайн системы инкрементных обновлений.

В первую очередь это CDN. Перед лицом массовых запросов, помимо того, что помогает нам сократить пики, это также может помочь пользователям в разных регионах быстрее получать ресурсы.

增量服务 API 层

После CDN мы разделили систему добавочного обновления на два отдельных уровня, называемых уровнем API и уровнем вычислений. Зачем делить? В прошлой практике мы обнаружили, что даже если мы будем внимательны и осторожны, все равно будут ошибки, что требует от нас достаточной гибкости в развертывании и онлайн; с другой стороны, для массовых вычислительных задач, если мы действительно не можем справиться с этим Вживую нам нужно поддерживать самую элементарную отзывчивость. Основываясь на этом соображении, мы разделяем службу обратного исходного кода CDN на службу. Этот уровень обслуживания выполняет три функции:

  1. Через доступ к системе хранения, если есть рассчитанный инкрементальный патч, его можно вернуть напрямую, и на вычислительный уровень передаются только те задачи, которые требуют наибольшего расчета.
  2. Если возникает проблема на уровне вычислений, уровень API сохраняет скорость реагирования, может понизить версию службы и вернуть полное содержимое файла.
  3. Управляйте внешними интерфейсами, чтобы избежать влияния изменений интерфейса на основные службы. На этой основе могут быть выполнены некоторые простые услуги агрегации для предоставления таких услуг, как слияние запросов.

Что, если уровень API не сможет перехватить трафик и передать его дальше вычислительному уровню?

增量服务计算层

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

预热的设计

С другой стороны, у нас также есть соответствующий автономный механизм предварительного нагрева. Мы предоставляем инструмент для подготовки для бизнес-стороны.Если бизнес-сторона вызывает наш инструмент для подготовки перед подключением к сети, добавочные исправления могут быть получены и кэшированы перед запуском. Наш прогревочный кластер и онлайн-вычислительный кластер разделены и используют только распределенное хранилище, поэтому они не влияют друг на друга в практических приложениях.

Как оправиться от катастрофы

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

  • Сбой линии. У нас есть встроенные автономные кеши в каждом слое сервисов.С одной стороны роль этого кеша заключается в отводе флуда.С другой стороны в случае сбоя линии автономный кеш также может уменьшить зависимость на линии в определенной степени.
  • Сбой хранилища. Для хранения мы напрямую используем две очень зрелые распределенные системы хранения в компании, которые дублируют друг друга.
  • Сбой CDN. Студенты, изучающие интерфейс, более или менее сталкивались с отказами CDN, и мы не исключение. Поэтому мы подготовили две разные CDN, чтобы эффективно изолировать риск отказа CDN.

Наконец, в дополнение к этому набору сервисов наш браузерный SDK также имеет собственный механизм аварийного восстановления. В дополнение к системе инкрементных обновлений мы развернули отдельный CDN, в котором хранятся только полные файлы. Как только система добавочного обновления перестанет работать, SDK перейдет к этому CDN, чтобы получить полный объем файлов, чтобы обеспечить доступность внешнего интерфейса.

Обзор и резюме

После того, как сервис был в сети в течение определенного периода времени, мы суммируем результаты новой практики:

Средняя ежедневная доля успешных расчетов прироста Среднее количество ежедневных добавочных обновлений Пиковая экономия трафика на человека в день Всего статических файлов проекта
99.97% 64.91% 164.07 KB 1184 KB

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

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

Теперь давайте рассмотрим, в нашей новой практике есть некоторые моменты, из которых вы действительно можете извлечь уроки:

  1. Разные языки и инструменты имеют разное применение, не пытайтесь пилить дерево молотком. Измените язык, когда пришло время измениться, не думайте об одном языке или инструменте, чтобы решить все.
  2. Переключение языков — важное решение, и вам нужно подумать, стоит ли вам это делать, прежде чем принимать решение.
  3. Язык решает больше локальных проблем, а архитектура решает больше системных проблем. Просто смена языка не означает, что все будет хорошо.
  4. Создавая систему, сначала подумайте о том, как она сломается. Выясните, где находятся потенциальные узкие места вашей системы, как ее улучшить и как рассмотреть варианты ее резервного копирования.

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

Наконец, если вы также заинтересованы в том, что мы делаем, и хотите создать вместе с нами большую команду, отправьте свое резюме по адресу liuyanghe02@meituan.com.

об авторе

Янхе присоединился к Ctrip UED в качестве стажера в 2013 году и участвовал в разработке первого проекта с открытым исходным кодом на Github с более чем 100 звездами в своей жизни. Он присоединился к облачной платформе Xiaomi в 2014 году и отвечает за интерфейсную разработку веб-страниц, разработку клиентов и прошивок маршрутизаторов, а также накопил богатый опыт сквозной разработки. Он присоединился к Meituan в 2017 году и в настоящее время отвечает за разработку основных компонентов платформы финансовых услуг.