Construindo um unikernel que executa WebAssembly - Parte 1
(flavio.castelli.me)- Durante a Hackweek 22 da SUSE, foi criado um POC de um unikernel que executa módulos WebAssembly, e o processo de implementação foi organizado em várias partes
- Para portar uma aplicação comum diretamente para um unikernel, é preciso compatibilizar até as dependências, mas uma plataforma WebAssembly define com muito mais clareza os limites das funcionalidades que o runtime deve oferecer
- Uma aplicação Spiderlightning exige apenas recursos como Key/Value, e mesmo que o host implemente isso com Redis ou Azure Cosmos DB, o mesmo módulo
.wasmnão precisa saber da diferença - A base usada é o unikernel em Rust RustyHermit e, como Wasmtime e Wasmer não compilavam, a escolha foi o runtime totalmente em Rust wasmi
- Para alinhar com o Component Model e o WIT, foi adicionado suporte a wasmi no wit-bindgen; depois disso, foi feito o scaffolding da funcionalidade Key/Value no lado do host para executar o
keyvalue-demo
Projeto da Hackweek e objetivo
- Durante a Hackweek 22 da SUSE, foi realizado o projeto Construindo um unikernel que executa WebAssembly
- Como todo o processo de implementação seria longo demais para caber em um único texto, ele foi dividido em vários artigos, e este é a primeira parte
- O código do POC está publicado em outro lugar, mas o texto fornecido não inclui a URL real do link
Por que usar unikernel com WebAssembly
- Para desenvolvedores de aplicações, portar para unikernel é um fardo grande
- A aplicação e todas as dependências precisam oferecer suporte ao unikernel de destino
- Pode ser necessário aplicar patches em toda a stack da aplicação
- Quem mantém o unikernel também precisa gastar muita energia para fazer aplicações arbitrárias rodarem sem atrito
- Isso acontece porque é difícil prever quais primitivas de sistema uma aplicação de usuário vai usar
- Em contrapartida, ao mirar plataformas WebAssembly como Spin ou Spiderlightning, o conjunto de funcionalidades que o runtime precisa fornecer fica claro
- No cenário do Spiderlightning, a aplicação pode solicitar ao runtime recursos de armazenamento Key/Value
- Seja o host implementando isso com Redis ou com Azure Cosmos DB, isso é transparente para a aplicação
- O mesmo módulo
.wasmpode ser executado sobre diferentes implementações de host
Arquitetura alvo
- Se a aplicação unikernel conseguir executar módulos WebAssembly e oferecer suporte ao conjunto de APIs do Spiderlightning, a mesma aplicação Spiderlightning poderá rodar tanto no runtime
slightcomum quanto nesse unikernel - O desenvolvedor da aplicação não precisa fazer trabalho adicional, e o módulo Wasm também não precisa saber onde está sendo executado
- A complexidade fica concentrada no desenvolvedor do unikernel, mas o escopo a implementar é muito mais claro do que “dar suporte à execução de qualquer aplicação”
Implementação baseada em RustyHermit
- A base escolhida foi o RustyHermit
- É um unikernel escrito em Rust
- Está incluído no Rust nightly, oferecendo uma experiência de desenvolvimento parecida com a de criar aplicações Rust comuns
- O build de aplicações RustyHermit é relativamente direto
- A documentação está um pouco espalhada, mas é de boa qualidade, e os exemplos ajudam bastante
- Não dá para esperar que todo crate Rust funcione no RustyHermit sem mudanças, e essa limitação influenciou o desenvolvimento do POC
Escolha do runtime WebAssembly
- O preferido, Wasmtime, não compilava sobre o RustyHermit
- Muitas dependências esperam
libcou outras bibliotecas de baixo nível
- Muitas dependências esperam
- O wasmer tinha o mesmo problema
- O WebAssembly Micro Runtime também foi considerado, mas a decisão foi usar um runtime escrito em Rust para manter a “experiência RustyHermit completa”
- No fim, a escolha foi o wasmi, um runtime WebAssembly totalmente em Rust
- Ele funciona bem sobre o RustyHermit
- Seu design foi inspirado no Wasmtime, o que permitiu reaproveitar bastante conhecimento prévio
WebAssembly Component Model e WIT
- O Spiderlightning usa a proposta WebAssembly Component Model
- Ela fornece funcionalidades para guests WebAssembly
- E permite que o host consuma funcionalidades fornecidas pelos guests WebAssembly
- A comunicação entre host e guest usa tipos definidos pelo Wasm Interface Type
- A demo usa o Component Model no seguinte fluxo
- O guest pede ao host para iniciar um servidor HTTP e passa as rotas HTTP a registrar e os nomes das funções internas de handler
- Usa o tipo
http-server, no qual o guest consome uma funcionalidade oferecida pelo host
- Usa o tipo
- O host processa as requisições HTTP recebidas com base nas informações de roteamento fornecidas pelo guest
- O handler HTTP é uma função exposta pelo guest WebAssembly
- O servidor consome uma funcionalidade oferecida pelo guest e se comunica pelo tipo
http-handler
- Alguns handlers HTTP interagem com o armazenamento Key/Value
- Também nesse caso o guest consome uma funcionalidade oferecida pelo host, definida pelo tipo
keyvalue
- Também nesse caso o guest consome uma funcionalidade oferecida pelo host, definida pelo tipo
- O guest pede ao host para iniciar um servidor HTTP e passa as rotas HTTP a registrar e os nomes das funções internas de handler
Extensão do wit-bindgen e execução da demo
- Para cada tipo WIT, são necessários códigos com papel de SDK no lado do guest e de implementação no lado do host
- O wit-bindgen é uma ferramenta CLI que gera código de host/guest a partir de arquivos
.wit - Neste POC, bastava implementar apenas a interface do lado do host dentro do unikernel
- O código gerado pelo
wit-bindgenusa o runtime WebAssembly para realizar tarefas de baixo nível- O código gerado depende da linguagem de programação e do runtime WebAssembly no lado do host
- Como o
wasminão era suportado pelowit-bindgen, ele foi estendido para lidar com wasmi- O código está no fork com branch wasmi
- Depois disso, foi feito o scaffolding do código do lado do host para a funcionalidade Key/Value e adicionada uma implementação simples do trait do host
- O código do host ficava no nível de apenas imprimir informações de debug
- Nesse estado, foi possível executar sem modificações o keyvalue-demo do projeto Spiderlightning
Prévia da próxima parte
- Há uma gravação da aplicação unikernel executando a demo
http-serverdo Spiderlightning - Na próxima parte, serão abordados Rust async, Redis e alguns erros estranhos
1 comentários
Opiniões no Hacker News
Não vem imediatamente à mente https://www.destroyallsoftware.com/talks/the-birth-and-death...?
Para alguém que não é hacker de sistemas operacionais e quer um unikernel, qual seria a abordagem mais completa?
As opções que vêm à cabeça são transformar a aplicação em um módulo do kernel Linux e carregá-la em um kernel comum, ignorando o espaço de usuário; reduzir drasticamente o Linux e anexar seu próprio código; começar por algum projeto de unikernel no GitHub; ou aparar outro sistema operacional, como o FreeBSD
Gosto da ideia de uma máquina x64 em uma VM conectada a uma placa de rede funcionando como um recurso de computação de uso geral, recebendo tarefas por dados enviados pela rede. Por ser mais trabalhoso do que um daemon em espaço de usuário, ainda não parece ter tanto valor, mas tenho curiosidade de saber por onde seria bom começar a hackear em nível de sistema operacional quando eu tiver tempo algum dia
O Unikernel Linux (UKL) começou como uma tentativa de aproveitar a configurabilidade do Linux e tem como objetivo um kernel que cubra desde sistemas operacionais de uso geral até unikernels especializados em aplicações e hardware. io_uring e eBPF também são mencionados como áreas relacionadas: io_uring distribui o custo das chamadas de sistema, e eBPF é outra forma, ainda que limitada, de executar código no espaço do kernel
Código: https://github.com/unikernelLinux/ukl
O UKL é um pequeno patch para Linux e glibc que permite compilar muitos programas como unikernels sem modificações. O programa é linkado com o kernel Linux e com o vmlinuz final, executa no espaço do kernel, pode inicializar em bare metal ou em uma VM e pode usar quase todos os recursos e drivers do Linux
/init, empacotá-la com o kernel e inicializarAssim, o app se torna o PID 1 e, na prática, o único processo; exceto por algumas threads do kernel, você pode fazer o que quiser
Ele oferece suporte a várias linguagens e apps, x86/ARM64, QEMU/Firecracker, e também consegue executar como unikernel um ELF compilado no Linux: https://unikraft.org/guides/bincompat
O Discord fica em https://unikraft.org/discord
Se você quer aprender OCaml e também quer um unikernel, esse é um caminho possível
Mais detalhes podem ser vistos na documentação do Unikraft: https://unikraft.org/docs/concepts/design-principles#approac...
É um bom projeto. Gosto do fato de WASM ter sido projetado desde o início com sandboxing e portabilidade em mente
Gostaria que o WASM tivesse surgido nos anos 90 em vez do JavaScript, e acho que o WASM vai dominar o mundo. O que mais desejo é persistência. Hoje há muitos programas que já não conseguimos mais executar, e jogos antigos são o exemplo clássico. Uma especificação simples tem mais chance de sobreviver por muito tempo, então a adição de novos recursos me deixa um pouco apreensivo, mas o futuro dos binários parece interessante
Havia um motivo para o JS ter sido inicialmente limitado a scripting básico, como manipuladores de clique e validação de formulários. O fato de ele ter crescido para outra coisa não se deve apenas a falhas de design do JS, mas também aos usos enfiados à força nele. Usar o navegador como mecanismo de entrega para esses apps está longe do que Tim Berners-Lee ou Marc Andreesen imaginaram
Na época, o pessoal do “a rede é o computador” lançou thin clients X para apps mais ricos: https://en.wikipedia.org/wiki/Network_Computer
Tenho sentimentos mistos em relação ao WASM. No momento, há um grande véu de hype e novidade sobre ele. Se tratarmos o navegador apenas como um viewport para a fantasia de linguagem que designers de UI e desenvolvedores preferirem no momento, muita coisa ruim acontece em áreas como acessibilidade e leitores de tela
A tendência de tratar o WASM fora do navegador como uma VM universal também é um caminho que já percorremos 30 anos atrás. Era isso que a JVM tentava fazer, mas agora parece não ser “cool”
Não sei bem como deveria ser a zona intermediária, nem por que deveríamos querê-la. Mas, realisticamente, o WASM provavelmente vai engolir até o conteúdo documental, e aí bloqueadores de anúncios e modos de leitura podem estar acabados
Gostei muito. Havia várias tecnologias linkadas que eu ainda não tinha visto, então favoritei tudo
Em seguida, quero tentar configurar uma conexão WireGuard no hipervisor. O estabelecimento da conexão talvez possa passar por algo como o Tailscale
Assim, o WebAssembly desta máquina conversaria diretamente com o WebAssembly daquela outra. Não seria um processo abrindo uma conexão TCP para um local arbitrário, mas uma estrutura que se comunica com base na configuração e nas permissões fornecidas
Meio tarde, mas será que alguém já pensou em executar o Zephyr como unikernel? https://docs.zephyrproject.org/latest/boards/x86/acrn/doc/in...
Quanto tempo falta até surgir hardware dedicado para WASM?
Dito isso, alguém poderia criar algo do tipo “por fora é wasm, mas por baixo na verdade há RISC-V”
Mas acho que esse tipo de hardware sempre vai continuar sendo de nicho. Porque, em geral, rodar WASM em hardware comum de prateleira será mais rápido. O próprio WASM foi projetado para rodar rapidamente em hardware existente, e as economias de escala dos processadores de uso geral são muito melhores
A International Conference on Functional Programming também começou como uma conferência chamada Functional Programming and Computer Architecture, mas depois foram encontradas formas de compilar linguagens funcionais de avaliação preguiçosa, como Haskell, de maneira eficiente para hardware existente
Com máquinas Lisp e Java foi parecido. Um dos motivos pelos quais quase não se vê mais esse tipo de coisa é que a tecnologia de compiladores alcançou esse ponto
Quais seriam os casos de uso de unikernels e WASM?
Vejo o valor dos unikernels em 1) desempenho: descartar o que não é necessário e puxar o que é necessário para o “ring 0” para extrair até alguns ciclos a mais; 2) simplificação: a possibilidade de reduzir a complexidade eliminando partes desnecessárias; 3) segurança: também a possibilidade de mudar a superfície de ataque reduzindo o que é desnecessário
Ainda assim, não acho que seja uma abordagem adequada para escrever microsserviços ou webapps, como muita gente neste fórum faz. O uso está mais próximo de criar componentes de infraestrutura, como bancos de dados e balanceadores de carga
Por isso, alguns provedores de edge cloud convertem imagens Docker em micro VMs ao executá-las
Porém, no edge, WASM dentro de uma micro VM pode ter dificuldade para competir com WASM em sandbox no próprio edge. Do ponto de vista do provedor, esta última opção provavelmente facilita adicionar integrações e recursos úteis de fronteira
Como foi “previsto” há muito tempo em Birth & Death of Javascript, um dia surgiria um unikernel capaz de rodar um runtime seguro com coleta de lixo no espaço do kernel, e a ideia era que, com isso, seria possível remover do CPU o suporte a mapeamento de memória virtual para torná-lo mais rápido
Em 2014, o autor imaginava JS e asm.js, mas hoje o WASM parece ser esse caminho. Estou animado, haha
https://www.destroyallsoftware.com/talks/the-birth-and-death...
Mas depois aprendemos que navegadores de processo único são um pesadelo de segurança, e os navegadores atuais já não são mais de processo único, para permitir um sandboxing adequado
Ainda assim, é legal ver o quanto aquele vídeo chegou perto da resposta certa, e também é interessante ver de que forma ele errou
É difícil apontar exatamente, mas há algo familiar nisso. A dica é que aquilo também começa com “J”
https://en.wikipedia.org/wiki/JavaStation
O uso virtual de um processo pode exceder o RSS mesmo que não seja por causa de swapping, e o sistema operacional e o alocador trabalham juntos para lidar com isso de forma bastante inteligente nos casos comuns
Por isso, é difícil dizer que remover isso traria automaticamente ganhos de desempenho. Especialmente se ainda houver uma camada de VM WASM, que é bem lenta, no caminho
Para algumas aplicações, como bancos de dados, pode haver grandes benefícios em rodar como unikernel ou mais próximo do kernel, com acesso direto à MMU: https://github.com/tuhhosg/exmap & https://github.com/viktorleis/vmcache & https://www.cs.cit.tum.de/fileadmin/w00cfj/dis/_my_direct_up...
Mas tenho dúvidas quanto a aplicações comuns que assumem o padrão POSIX ou pressupõem que o ambiente de execução se pareça com um computador moderno genérico. No fim, parece que muita coisa que a camada VMM fazia acabaria sendo reescrita em código de usuário
Motores JS dependem do VMM, e o WASM também depende dele de várias maneiras. Quase todo programa não trivial que não seja embarcado assume sutilmente a existência do VMM. Em especial, algumas tecnologias de VM em torno de micro-VMs também usam VMM, e unikernels só fazem real sentido quando usados como VMs