Чого коштує перетин межі WASM
У хості плагінів на WebAssembly Component Model кожен аргумент і кожне повернуте значення перетинають межу між двома просторами пам'яті. Здебільшого на це можна не зважати. З тілами запитів — не можна, і цифри достатньо різкі, щоб змінити дизайн.
Тіла як буфери
Перша версія оголошувала HTTP-тіла найочевиднішим способом:
record http-request {
method: string,
uri: string,
headers: list<http-header>,
body: option<list<u8>>,
}
list<u8> — це буфер. Тож завантаження приходить у хост, буферизується там,
копіюється через межу й матеріалізується ще раз у лінійній пам'яті гостя — три
копії тих самих байтів ще до того, як плагін на них глянув. Гірше: цю ціну
платить кожен плагін, зокрема ті, що тіла взагалі не торкаються.
Перехід в обидва боки на stream<u8> змінив дві речі:
Тіло, яке плагін ігнорує, стало майже безкоштовним. Запит на 8 МіБ пройшов шлях від 2638 мкс до 8 мкс. Нічого не копіюється, бо нічого не читається.
Тіло, яке плагін читає, стало повільнішим. Приблизно в 4–5 разів за граничною вартістю на МіБ: 543 мкс проти 112 мкс у старого масового копіювання. Невеликі JSON-запити платять десятки мікросекунд, яких раніше не платили.
Це справжній розмін, а не безкоштовна перемога. Просто тут він правильний: більшість маршрутів плагінів — це перевірки статусу й короткі команди, а поодинокі великі завантаження змушували все інше платити за сам факт свого існування. Якби навантаження складалося переважно з великих тіл, які всі читаються, виграла б буферна версія.
Стримінг змінює те, чим може бути помилка
Щойно хост повертає статус і заголовки одразу, як їх видав гість, — а саме в
цьому сенс стримінгу, — збій посеред тіла стається вже після того, як рядок
статусу пішов у мережу. Він не може стати 500-ю. Гість може впасти, паливо може
закінчитися посеред тіла, а клієнт усе одно побачить 200 і обрізану відповідь.
Тож дизайн мусить сказати, що це означає. Тут call_http зберігає бюджет у
десять мільйонів одиниць палива, велике тіло тепер може його вичерпати, а падіння
посеред тіла не може змінити вже надісланий статус. Нічого екзотичного — це те
саме обмеження, що й у будь-якого стримінгового сервера, — але перехід від
буферів до стрімів тихо перетворює категорію «повернути помилку» на категорію
«клієнт отримав пів відповіді».
У пісочниці немає годинника
Другий урок не мав стосунку до продуктивності. Плагіни не могли спати. Не
«спати було повільно» — не було способу це виразити. Гості збираються під
wasm32-wasip2, а хост не надавав таймера, тож усе, пов'язане з часом, було
просто недоступним.
Додати це не означало підключити крейт. WASI має лінкувати хост, а розміщення
його в одному світі з інтерфейсом плагіна змусило bindgen хоста вимагати
реалізацію Host і зіткнутися з рантаймом p3. Розв'язанням стало розділення
світів: світ plugin-guest, який додає wasi:clocks@0.3 поверх інтерфейсу
плагіна, з визначенням годинника, скопійованим із тієї версії wasmtime-wasi,
яку хост справді лінкує, — щоб обидві сторони збігалися.
Загальна думка: пісочницю визначає те, що хост погодився передати. Легко думати про неї як про «звичайний код, тільки ізольований». Насправді це радше навпаки: нічого не існує, доки не надано, — включно з можливостями настільки буденними, що ви ніколи не сприймали їх як можливості. Мене здивував час. Це навряд чи останній сюрприз.
Чи вартий був Component Model
Для системи плагінів — так. Плагіни компілюються один раз у .wasm,
завантажуються в хост, з яким їх не збирали, оголошують свої маршрути в
специфікації, яку хост об'єднує, і працюють під лімітами палива без доступу до
чогось, що не було явно злінковане. Отримати це від динамічної бібліотеки
означало б їй довіряти.
Ціна в тому, що кожне рішення про інтерфейс стає рішенням про представлення даних, ухваленим у WIT-файлі, з наслідками для продуктивності, які видно лише під навантаженням. За ізоляцію це чесна ціна. Але не безкоштовна.
- Rust
- WebAssembly
- Wasmtime
- Архітектура