黄东旭解析 TiDB 的核心优势
465
2019-11-12
内容来源:http://mp.weixin.qq.com/s?__biz=MzI3NDIxNTQyOQ==&mid=2247490145&idx=1&sn=2733fe2115e96ee673eb6a8f1fa23ce8&chksm=eb163d0bdc61b41d0827898cc7166feefeacfe18ad523e81ac9eab8f77b4b0da958875f131e1#rd
上周我们推送了《让数据库运行在浏览器里?TiDB + WebAssembly 告诉你答案》,向大家展示了 TiDB-Wasm 的魅力:
TiDB-Wasm 项目是 TiDB Hackathon 2019 中诞生的二等奖项目,实现了将 TiDB 编译成 Wasm 运行在浏览器里,让用户无需安装就可以使用 TiDB。
本文由 Ti-Cool 队成员主笔,为大家详细介绍 TiDB-Wasm 设计与实现细节。
10 月 27 日,为期两天的 Hackathon 落下帷幕,我们用一枚二等奖为此次上海之行画上了圆满的句号,不枉我们风尘仆仆跑去异地参赛(强烈期待明年杭州能作为赛场,主办方也该鼓励鼓励杭州当地的小伙伴呀 :D )。
我们几个 PingCAP 的小伙伴找到了 Tony 同学一起组队,组队之后找了一个周末进行了“秘密会晤”——Hackathon kick off。想了 N 个 idea,包括使用 unikernel 技术将 TiDB 直接跑在裸机上,或者将网络协议栈做到用户态以提升 TiDB 集群性能,亦或是使用异步 io 技术提升 TiKV 的读写能力,这些都被一一否决,原因是这些 idea 不是和 Tony 的工作内容相关,就是和我们 PingCAP 小伙伴的日常工作相关,做这些相当于我们在 Hackathon 加了两天班,这一点都不酷。本着「与工作无关」的标准,我们想了一个 idea:把 TiDB 编译成 Wasm 运行在浏览器里,让用户无需安装就可以使用 TiDB。我们一致认为这很酷,于是给队伍命名为 Ti-Cool(太酷了)。
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)
总结与展望
延展阅读
让数据库运行在浏览器里?TiDB + WebAssembly 告诉你答案
据说今年黑客马拉松项目又多又猛?| TiDB Hackathon 回顾
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。