Vorker开发笔记-自建的云函数平台


项目地址:https://github.com/VaalaCat/vorker

引言

博客看起来有一年没更新啦,加入跳不动公司过后真的跳不动了QwQ。最近琢磨着还是该写点什么免得让大家以为我赛博失踪,这次的项目我给它取名为Voker。其基于Cloudflare公司开源的Workerd,这个Workerd就是CF知名产品Cloudflare Worker的引擎。但CF仅开源了引擎,另外的能力如运行编排、KV存储、控制面板、版本控制、日志统计收集、用户权限等功能并没有公开,所以需要自己完善这方面的能力。

代码和使用说明参考Github:Vorker

这次要做的就是Cloudflare Worker的自托管(Self Hosting)版本,代码执行能力由CF开源workerd组件提供,其余能力自行开发完成

之所以叫Voker,因为做出来的东西相比Worker不完善,所以把W砍一半就变成V了,同时做的时候人快没了,所以少了个r(qqqq以前叫voker,现在拉了一个sems和我一起,人还是在的

需求介绍

此次的需求是完成一个自托管版本的Worker,主要原因是:

  1. CF的worker域名被墙,虽然可以用自己的域名,但总体来说国内使用体验并不好
  2. Free Plan有限额,虽然我用不完,但是看着还是不爽,毕竟自己有机器可以不限额的呢
  3. 可定制能力缺失,CF毕竟是CF,个人用户的一些feature还是考虑不太完善,用着有点憋屈,自己写就能很轻松的和Git仓库配合上CI/CD
  4. 我自己开新项目的时候不用写一大堆的环境配置脚本和CICD配置,极大提升了我开发效率和体验,对于一些小项目,无需再配置反向代理、路由规则、运行的节点信息、SSL、独立数据库表等,同时Debug也方便得多,不用在编译、上线等等等

明确目标

对于以上需求,需要达到的一些目标如下

  1. 需要一个控制面的API,能够轻松控制每套代码的部署情况
  2. 需要在控制面做反向代理,将Workerd的每一个worker映射到对应的域名
  3. 需要一个WebUI,方便随时更新代码(手机等
  4. 需要HA (High Availability)部署,不然一台机器坏了所有代码都寄了
  5. 需要支持外部反向代理,毕竟自己写的肯定没有Traefik Nginx等能力强

架构设计

对于以上目标,设计如下架构:

实现

后端

前端采用Next.js/React/TailwindCSS/semi.desgin,后端Gin/Golang/Workerd。

最开始还在思考如何去控制workerd想了很长时间,某一天看到Gitea里操作git是直接cmd.run,我直接就是震惊🤯,所以我也直接开抄😎,所以worker的启动参数是在Vorker里拼起来的。

Workerd运行配置文件基于capnp,这个IDL超级快,因为他的结构完全和内存布局一致。他有一个Go实现,本来打算是用这个库按照Workerd的Schema去生成Workerd的配置文件,但是看了一眼发现Workerd的配置并没有固定下来,所以暂时先写个简单的template生成配置文件。

对于api部分没什么好说的,就是CRUD,要注意一点是Vorker将Workerd的每一个Worker所有信息都存储在数据库中,然后通过数据库里的信息生成配置文件给Workerd使用。这个过程中可能会出现不一致,Vorker设计上是以数据库为唯一可信数据源,对文件只有写入操作,没有读取,因此定时调用接口刷新文件即可。

Workerd的定义用protobuf,定义如下

syntax = "proto3";
package defs;
option go_package = "../entities";

message Worker {
    string UID = 1; // Unique id of the worker
    string ExternalPath = 2; // External path of the worker, default is '/'
    string HostName = 3; // the workerd runner host name, default is 'localhost'
    string NodeName = 4; // for future HA feature, default is 'default'
    int32 Port = 5; // worker's port, platfrom will obtain free port while init worker
    string Entry = 6; // worker's entry file, default is 'entry.js'
    bytes Code = 7; // worker's code
    string Name = 8; // worker's name, also use at worker routing, must be unique, default is UID
}

// one WorkerList for one workerd instance
message WorkerList { 
    string ConfName = 1; // the name of the workerd instance
    repeated Worker Workers = 2;
    string NodeName = 3; // workerd runner host name, for HA
}

路由部分后续需要支持外部反向代理,在Traefik中注册路由实现更多功能。也需要支持内部反向代理,实现如下

package proxy

import (
    "fmt"
    "net/http/httputil"
    "net/url"
    "strings"
    "voker/entities"
    "voker/models"

    "github.com/gin-gonic/gin"
    "github.com/sirupsen/logrus"
)

func init() {
    proxy := entities.GetProxy()
    workerRecords, err := models.AdminGetAllWorkers()
    if err != nil {
        logrus.Errorf("failed to get all workers, err: %v", err)
    }
    workerList := &entities.WorkerList{
        Workers: models.Trans2Entities(workerRecords),
    }
    proxy.InitProxyMap(workerList)
}

func Endpoint(c *gin.Context) {
    host := c.Request.Host
    name := strings.Split(host, ".")[0]
    port := entities.GetProxy().GetProxyPort(name)
    if port == 0 {
        c.JSON(404, gin.H{"code": 1, "error": "not found"})
        return
    }

    remote, err := url.Parse(fmt.Sprintf("http://localhost:%v", port))
    if err != nil {
        logrus.Panic(err)
    }

    c.Request.URL.Path = c.Copy().Param("name")
    proxy := httputil.NewSingleHostReverseProxy(remote)
    proxy.ServeHTTP(c.Writer, c.Request)
}

在启动时就将路由数据写到内存里。

前端

前端由@sems推荐使用了semidesign,是字节新开源的组件库,不得不说确实好看,还能用设计稿先出一个代码初稿,属于是后端福音。

搓了四个页面,使用常见的侧边栏+header+内容布局

  • admin,主要用于worker列表、用户设置等
  • register,用户注册
  • login,用户登录
  • editor,编辑代码、设置worker

代码编辑器用了vscode同款的monaco,弄上了js的代码补全,写起来感觉还是不错的。

运行

  1. 首先要下载Workerd的binary

  2. docker

    docker run -dit --name=vorker \
        -e DB_PATH=/path/to/workerd/db.sqlite \
        -e WORKERD_DIR=/path/to/workerd \
        -e WORKERD_BIN_PATH=/bin/workerd \
        -e DB_TYPE=sqlite \
        -e WORKER_LIMIT=10000 \
        -e WORKER_PORT=8080 \
        -e API_PORT=8888 \
        -e LISTEN_ADDR=0.0.0.0 \
        -e WORKER_DOMAIN_SUFFIX=example.com \ # concat with worker name and scheme
        -e SCHEME=http \ # external scheme
        -e ENABLE_REGISTER=false \
        -e COOKIE_NAME=authorization \
        -e COOKIE_AGE=21600 \
        -e COOKIE_DOMAIN=localhost # change it to your domain \
        -e JWT_SECRET=xxxxxxx \
        -e JWT_EXPIRETIME=6 \
        -p 8080:8080 \
        -p 8888:8888 \
        -v $PWD/workerd:/path/to/workerd \
        -v $PWD/workerd-linux-64:/bin/workerd \
        vaalacat/vorker:latest
    
  3. 访问http://localhost:8888进入控制台,在header里的Host带上worker的名字即可

运行截图

管理界面

Worker编辑界面

Worker运行界面

总结

忙毕设忙了一阵,在这个项目上并没有投入很多的时间,试用了最新的用于生产的前端技术栈,至今还有HA、Log、Metrics、外部反向代理等未完全完成,但是这东西在未完成的状态就很好用了,看起来未来的Serverless+WASM还会大放异彩。顺便在这段时间还看了lagonWindmilllaf等serverless function平台,似乎对Serverless有了什么奇妙的感觉😳,希望以后Vorker也能和知名开源产品一样发展下去~~~

Vorker已经成为了Vaala☁️的API层的重要部分,后续我也会陆续写一些关于如何从裸金属机器构建一个企业级的云平台的文章,例如用或者不用K8S的统一网关、SCM、配置中心、容器调度、ServiceMesh等,尽请期待!

参考

- The End -