Первые шаги с Golang и WebAssembly

Последние пару недель я работал над Discoblock, нашим новым решением для конфигурирования декларативных томов в Kubernetes с открытым исходным кодом. Для получения более подробной информации вам следует прочесть мой пост. Итак, не вдаваясь в подробности, Discoblocks — это проект на Golang, и он должен поддерживать несколько валидаторов для различных вариантов облачных дисков. Я думаю, что эта область является одним из слабых мест Go, я имею в виду, что любой новый валидатор требует новой сборки бинарника. Легко признать, что это далеко не идеальный вариант, когда нам нужно поддерживать огромное количество облачных драйверов, не говоря уже о будущем.

Один из вариантов — компилировать бинарник с CGO_ENABLED=1 (поведение по умолчанию в любом случае) и загружать библиотеки — в данном случае валидаторы — динамически.

  • + Встроенное решение
  • — Только скомпилированные языки могут создавать so файлы
  • — Убедиться, что все файлы so находятся в контейнере, должно быть кошмаром, любой новый из них требует отладки запущенного контейнера
  • — Разработчики должны использовать некоторые Linux для разработки, по крайней мере для тестирования

Альтернативным решением в мире Kubernetes является создание небольшого unix socket-based HTTP сервиса для каждого Pod в качестве sidecar, но мы не хотели двигаться в этом направлении из-за сложности.

Третий вариант — подправить код Go для выполнения валидаторов как WebAssembly, точнее, WASI-модулей.

В чем разница между WASM и WASI? Если очень коротко, то WASM предназначен для веб и не поддерживает вызовы функций, что является ключевой особенностью, которая нам нужна.

  • + Многие языки могут создавать WASI.
  • + Модули WASI не нуждаются в дополнительных зависимостях
  • + Это модно 🙂
  • — Все еще нуждается в CGO_ENABLED=1, но зависит только от ограниченного числа библиотек
  • — Встроенная компиляция поддерживает только WASM, но не WASI
    • Мы должны использовать TinyGo для компиляции модуля WASI
    • Поддерживается только несколько типов ввода и вывода числовых данных (обходной путь будет найден позже)
  • — Отсутствует встроенное выполнение WASI
  • — Интеграция не является бесшовной
    • Есть и другие неподдерживаемые функции, пожалуйста, следите за документацией TinyGo.

Время кода, мое любимое:

import (
    "fmt"
    "os"

    "github.com/valyala/fastjson"
)

func main() {}

//export IsStorageClassValid
func IsStorageClassValid() {
    json := []byte(os.Getenv("STORAGE_CLASS_JSON"))

    if fastjson.Exists(json, "volumeBindingMode") && fastjson.GetString(json, "volumeBindingMode") != "WaitForFirstConsumer" {
        fmt.Fprint(os.Stderr, "only volumeBindingMode WaitForFirstConsumer is supported")
        fmt.Fprint(os.Stdout, false)
        return
    }

    fmt.Fprint(os.Stdout, true)
}
Вход в полноэкранный режим Выход из полноэкранного режима
  • Fastjson хорошо работает в WASI, и это быстро 😉
  • Пустая func main() {} необходима.
  • Функция не имеет входных параметров, чтобы избежать проблем с типами, вместо этого она считывает переменные окружения
  • Нет возвращаемого значения по той же причине, что и одной строкой выше, вместо этого она пишет в стандартный вывод или ошибку

Давайте скомпилируем модуль.

go mod init
docker run -v $(PWD):/go/src/ebs.csi.aws.com -w /go/src/ebs.csi.aws.com tinygo/tinygo:0.23.0 bash -c "go mod tidy && tinygo build -o main.wasm -target wasi --no-debug main.go"
Вход в полноэкранный режим Выход из полноэкранного режима

Пришло время реализовать другую сторону истории. Я нашел WebAssembly runtime для Go. Wasmer-go — это полная и зрелая среда выполнения WebAssembly для Go, основанная на Wasmer.

// DriversDir driver location, configure with -ldflags -X github.com/ondat/discoblocks/pkg/drivers.DriversDir=/yourpath
var DriversDir = "/drivers"

func init() {
    files, err := os.ReadDir(filepath.Clean(DriversDir))
    if err != nil {
        log.Fatal(fmt.Errorf("unable to load drivers: %w", err))
    }

    for _, file := range files {
        if !file.IsDir() {
            continue
        }

        driverPath := fmt.Sprintf("%s/%s/main.wasm", DriversDir, file.Name())

        if _, err := os.Stat(driverPath); err != nil {
            log.Printf("unable to found main.wasm for %s: %s", file.Name(), err.Error())
            continue
        }

        wasmBytes, err := os.ReadFile(filepath.Clean(driverPath))
        if err != nil {
            log.Fatal(fmt.Errorf("unable to load driver content for %s: %w", driverPath, err))
        }

        engine := wasmer.NewEngine()
        store := wasmer.NewStore(engine)
        module, err := wasmer.NewModule(store, wasmBytes)
        if err != nil {
            log.Fatal(fmt.Errorf("unable to compile module %s: %w", driverPath, err))
        }

        drivers[file.Name()] = &Driver{
            store:  store,
            module: module,
        }
    }
}

var drivers = map[string]*Driver{}

// GetDriver returns given service
func GetDriver(name string) *Driver {
    return drivers[name]
}

// Driver is the bridge to WASI modules
type Driver struct {
    store  *wasmer.Store
    module *wasmer.Module
}

// IsStorageClassValid validates StorageClass
func (d *Driver) IsStorageClassValid(sc *storagev1.StorageClass) (bool, error) {
    rawSc, err := json.Marshal(sc)
    if err != nil {
        return false, fmt.Errorf("unable to parse StorageClass: %w", err)
    }

    wasiEnv, instance, err := d.init(map[string]string{
        "STORAGE_CLASS_JSON": string(rawSc),
    })
    if err != nil {
        return false, fmt.Errorf("unable to init instance: %w", err)
    }

    isStorageClassValid, err := instance.Exports.GetRawFunction("IsStorageClassValid")
    if err != nil {
        return false, fmt.Errorf("unable to find IsStorageClassValid: %w", err)
    }

    _, err = isStorageClassValid.Native()()
    if err != nil {
        return false, fmt.Errorf("unable to call IsStorageClassValid: %w", err)
    }

    errOut := string(wasiEnv.ReadStderr())
    if errOut != "" {
        return false, fmt.Errorf("function error IsStorageClassValid: %s", errOut)
    }

    resp, err := strconv.ParseBool(string(wasiEnv.ReadStdout()))
    if err != nil {
        return false, fmt.Errorf("unable to parse output: %w", err)
    }

    return resp, nil
}

func (d *Driver) init(envs map[string]string) (*wasmer.WasiEnvironment, *wasmer.Instance, error) {
    builder := wasmer.NewWasiStateBuilder("wasi-program").
        CaptureStdout().CaptureStderr()

    for k, v := range envs {
        builder = builder.Environment(k, v)
    }

    wasiEnv, err := builder.Finalize()
    if err != nil {
        return nil, nil, fmt.Errorf("unable to build module: %w", err)
    }

    importObject, err := wasiEnv.GenerateImportObject(d.store, d.module)
    if err != nil {
        return nil, nil, fmt.Errorf("unable to generate imports: %w", err)
    }

    instance, err := wasmer.NewInstance(d.module, importObject)
    if err != nil {
        return nil, nil, fmt.Errorf("unable to create instance: %w", err)
    }

    start, err := instance.Exports.GetWasiStartFunction()
    if err != nil {
        return nil, nil, fmt.Errorf("unable to get start: %w", err)
    }

    _, err = start()
    if err != nil {
        return nil, nil, fmt.Errorf("unable to start instance: %w", err)
    }

    return wasiEnv, instance, nil
}
Вход в полноэкранный режим Выход из полноэкранного режима

На стороне вызывающей стороны.

driver := drivers.GetDriver(storageClass.Provisioner)
if driver == nil {
    return errors.New("driver not found")
}

valid, err := driver.IsStorageClassValid(&storageClass)
if err != nil {
    return fmt.Errorf("failed to call driver: %w", err)
} else if !valid {
    return fmt.Errorf("invalid StorageClass: %w", err)
}
Ввести полноэкранный режим Выход из полноэкранного режима

Есть еще одна вещь: отправьте все в контейнерный образ.

FROM tinygo/tinygo:0.23.0 as drivers

COPY ebs.csi.aws.com/ /go/src/ebs.csi.aws.com

RUN cd /go/src/ebs.csi.aws.com ; go mod tidy && tinygo build -o main.wasm -target wasi --no-debug main.go

...

FROM redhat/ubi8-micro:8.6

COPY --from=drivers /go/src /drivers
COPY --from=builder /go/pkg/mod/github.com/wasmerio/wasmer-go@v1.0.4/wasmer/packaged/lib/linux-amd64/libwasmer.so /lib64
Войти в полноэкранный режим Выход из полноэкранного режима

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

Вот и все, друзья!!!

Оцените статью
Procodings.ru
Добавить комментарий