不久前我们推送的《让数据库运行在浏览器里?TiDB + WebAssembly 告诉你答案》,向大家展示了TiDB-Wasm的魅力:TiDB-Wasm 项目实现了将 TiDB 编译成 Wasm 运行在浏览器里,让用户无需安装就可以使用TiDB。
本文将为大家详细介绍 TiDB-Wasm 设计与实现细节。
WebAssembly 简介
可执行指令格式
从高级语言到 Wasm
各种 runtime 以及 WASI
(module
;; type iov struct { iov_base, iov_len int32 }
;; func fd_write(id *iov, iovs_len int32, nwritten *int32) (written int32)
(import "wasi_unstable" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
(memory 1)(export "memory" (memory 0))
;; The first 8 bytes are reserved for the iov array, starting with address 8
(data (i32.const 8) "hello world\n")
;; _start is similar to main function, will be executed automatically
(func $main (export "_start")
(i32.store (i32.const 0) (i32.const 8)) ;; iov.iov_base - The string address is 8
(i32.store (i32.const 4) (i32.const 12)) ;; iov.iov_len - String length
(call $fd_write
(i32.const 1) ;; 1 is stdout
(i32.const 0) ;; *iovs - The first 8 bytes are reserved for the iov array
(i32.const 1) ;; len(iovs) - Only 1 string
(i32.const 20) ;; nwritten - Pointer, inside is the length of the data to be written
)
drop ;; Ignore return value
)
)
改造工作
浏览器安全限制
result = tk.MustQuery("select count(*) from t group by d order by c")
result.Check(testkit.Rows("3", "2", "2"))
tk
是个什么东西,借来用一下就行了。这是
tk
的主要函数:
// Exec executes a sql statement.
func (tk *TestKit) Exec(sql string, args ...interface{}) (sqlexec.RecordSet, error) {
var err error
if tk.Se == nil {
tk.Se, err = session.CreateSession4Test(tk.store)
tk.c.Assert(err, check.IsNil)
id := atomic.AddUint64(&connectionID, 1)
tk.Se.SetConnectionID(id)
}
ctx := context.Background()
if len(args) == 0 {
var rss []sqlexec.RecordSet
rss, err = tk.Se.Execute(ctx, sql)
if err == nil && len(rss) > 0 {
return rss[0], nil
}
return nil, errors.Trace(err)
}
stmtID, _, _, err := tk.Se.PrepareStmt(sql)
if err != nil {
return nil, errors.Trace(err)
}
params := make([]types.Datum, len(args))
for i := 0; i < len(params); i++ {
params[i] = types.NewDatum(args[i])
}
rs, err := tk.Se.ExecutePreparedStmt(ctx, stmtID, params)
if err != nil {
return nil, errors.Trace(err)
}
err = tk.Se.DropPreparedStmt(stmtID)
if err != nil {
return nil, errors.Trace(err)
}
return rs, nil
}
编译问题
file_storage_js.go
,然后给这些函数一个 unimplemented 的实现:
package storage
import (
"os"
"syscall"
)
func newFileLock(path string, readOnly bool) (fl fileLock, err error) {
return nil, syscall.ENOTSUP
}
func setFileLock(f *os.File, readOnly, lock bool) error {
return syscall.ENOTSUP
}
func rename(oldpath, newpath string) error {
return syscall.ENOTSUP
}
func isErrInvalid(err error) bool {
return false
}
func syncDir(name string) error {
return syscall.ENOTSUP
}
图 6 再次编译的结果
arith_decl.go
所在的目录看一下就知道怎么回事了:
arith_decl.go
的内容是一些列的函数声明,但是具体的实现放到了上面的各个平台相关的汇编文件中了。
mathutil
的库,然后
mathutil
依赖这个
bigfft
。悲催的是,这个
mathutil
的代码也不受我们控制,因此很直观的想到了两种方案:
mathutil
,但是基本上只用了几个函数:
MinUint64
,
MaxUint64
,
MinInt32
,
MaxInt32
等等,我们想到的方案是:
mathutil
目录,在这个目录里建立 mathutil_linux.go
和 mathutil_js.go
。
mathutil_linux.go
中 reexport 第三方包的几个函数。
mathutil_js.go
中自己实现这几个函数,不依赖第三方包。
mathutil
目录上。
mathutil
目录对外提供了原来
mathutil
包的函数,同时整个项目只有
mathutil
目录引入了这个不兼容 Wasm 的第三方包,并且只在
mathutil_linux.go
中引入(
mathutil_js.go
是自己实现的),因此编译 Wasm 的时候就不会再用到
mathutil
这个包。
图 8 编译成功
兼容性问题
wasm_exec.js
中 mock 了一个
fs
:
global.fs = {
writeSync(fd, buf) {
...
},
write(fd, buf, offset, length, position, callback) {
...
},
open(path, flags, mode, callback) {
...
},
...
}
fs
并没有实现
stat
,
lstat
,
unlink
,
mkdir
之类的调用,那么解决方案就是我们在启动之前在全局的
fs
对象上 mock 一下这几个函数:
function unimplemented(callback) {
const err = new Error("not implemented");
err.code = "ENOSYS";
callback(err);
}
function unimplemented1(_1, callback) { unimplemented(callback); }
function unimplemented2(_1, _2, callback) { unimplemented(callback); }
fs.stat = unimplemented1;
fs.lstat = unimplemented1;
fs.unlink = unimplemented1;
fs.rmdir = unimplemented1;
fs.mkdir = unimplemented2;
go.run(result.instance);
图 10 日志信息
用户接口
js.Global().Set("executeSQL", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go func() {
// Simplified code
sql := args[0].String()
args[1].Invoke(k.Exec(sql))
}()
return nil
}))
本地文件访问
js.Global().Get("upload").Invoke(js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go func() {
fileContent := args[0].String()
_, e := doSomething(fileContent)
c <- e
}()
return nil
}), js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go func() {
c <- errors.New(args[0].String())
}()
return nil
}))
CREATE DATABASE IF NOT EXISTS samp_db;
USE samp_db;
CREATE TABLE IF NOT EXISTS person (
number INT(11),
name VARCHAR(255),
birthday DATE
);
CREATE INDEX person_num ON person (number);
INSERT INTO person VALUES("1","tom","20170912");
UPDATE person SET birthday='20171010' WHERE name='tom';
图 14 source 命令执行(2/2)
总结与展望