TiDB-Wasm 原理と実現

クイックスタート:https://play.pingcap.com/

tidb-wasm-demo

WebAssembly の概要

読者にWebAssembly大体理解ために、先ずはこの技術の基本的な紹介である。

WebAssembly公式紹介はこちら。

WebAssembly(以下略記Wasm)は、スタックベースの仮想マシン用のバイナリ命令形式であり、 Wasmは、CC++Rustなどの高レベル言語のコンパイル用のポータブルターゲットとして設計されており、クライアントおよびサーバー上のアプリケーション用にWeb上に展開できます。

言い換えれば、以下の結論で。

  • Wasmは実行ファイルです。

  • CC++Rustなどの高レベル言語のプログラムをWasmにコンパイルできます。

  • Wasmはブラウザに実行できます。

実行の命令形式

以上の三つの結論見てとうり、質問があるかもしれます、命令形式は何ですか?一般的なELFファイルUnixシステムの共通の実行バイナリ命令形式、ローダーによって解析され、メモリにはいります、そして実行です。Wasmも同様、対応のランタイムを解析および実行である。現在、主流のブラウザー、Node.js、およびWasmerと呼ばれるWasm専用に設計された一般的なランタイムです。さらに一歩進む、Wasm作成したプログラムをカーネルモードで簡単に実行できるために、Wasmランタイムをカーネルに統合する機能をLinuxカーネルに提供する人もいます。

主要なブラウザのWasmサポート

wasm-support

高レベル言語からWasmまで

上記の背景知識があれば、高レベル言語がどのようにWasmにコンパイルされる方は理解できます。まず、高水準言語のコンパイルプロセスを見てください。

compile-process

アセンブリと比較して、高レベル言語の特徴の1つは移植性です。例えば、CC++x86マシンのターゲットにコンパイルできます、およびARMのマシンのターゲット。WasmARMx86は実際には同じ種類のものであり、バイトコードの実行をサポートする仮想マシンと考えることができます。これはJavaに非常に似ていますが、実際には、CC++JVMにコンパイルおよび実行できます。

さまざまなランタイムとWASI

以上はWasmのデザインの目標はプログラムにweb上に実行です。実際、Wasmの元々デザインのターゲットはJavaScriptの実行効率を補うためください。開発が進むにつれて、この技術は仮想マシンとして扱う、そしてプログラムに移植するはまた、良いアイデアである。そのため、NodeJSの実行環境、Wasmerの実行環境、さらにはカーネル環境もあります。それから質問が来ます、こんあ沢山の実行環境であり、しかも各環境のインターフェースは違います、例えば、NodeJSはファイルシステムのインターフェースはできます、ブラウザはこのインターフェースはできまさん。Wasmの移植性に対処するには、WASI(WebAssembly System Interface)が生まれました。WASIは低レベルの標準のインターフェースを定義します、コンパイラとWasmランタイム環境がこの標準をサポートしている限り、生成されたWasmはさまざまな環境にに移植できます。Wasmランタイムは仮想マシンに相当し、Wasmはこのマシンの実行可能プログラムであり、WASIはこのマシンで実行されているシステムであり、そしてWasmの基礎的なインターフェースを提供します、例えばファイルシステムおよびソケットなど。

WasmWASIが何であるかをより良く説明するために、Hello Worldを使用して説明します、デモソース

(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
    )
)

特定の命令の説明は、命令セットを参照できます。

こちらのhello.watWasmのテキストコードです、watWasmの関係は、アセンブリとELFの関係に似ています。次に、watWasmにコンパイルし、Wasmer(一般的なWasmランタイム実装)を使用して実行します。

先ずは依存関係をインストールする。

そしてコンパイルおよび実行。

~ » wat2wasm hello.wat -o hello.wasm
~ » wasmer run hello.wasm
hello world

TiDB の改修

未知は恐怖の源です、Wasmの原理を知っているなら、TiDBをブラウザに移動できます。

ブラウザのセキュリティポリシー

よく知られている、ブラウザは本質的にサンドボックスです、内部のプログラムは危険のアクションは許可されていません、例えばポートをリスニングおよびファイルシステム。しかし、TiDBは使用の方がポートをリスニング、そしてユーザーはMySQLクライアントを使用して接続できます。そのために、TiDBはポートのをリスニングは必要であり。少し考えて、ポートをリスニングの限界突破、そしてMySQLクライアントを使用して接続のことはいいじゃないは理解するできます。理想的な方法はMySQLクライアントのターミナルがウェブページの中にある、そしてこのクライアントはTiDBは接続されています。

先ずは、TiDBはターミナルを統合するのことは考えて。ユーザー入力SQLを受け入れ、そして出力はSQLの実行結果です。このTiDBのエグゼクティブターミナルを実装するのは難しい、ショートカットを考えました、TiDBのテストコードを使用してターミナルを後付けすることを計画。これはテストコードで見つかったスニペットです。

result = tk.MustQuery("select count(*) from t group by d order by c")
result.Check(testkit.Rows("3", "2", "2"))

tkという名前のこのオブジェクトは、SQLターミナルとして使用できます。これは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
}

後では簡単です、Read-Eval-Print-Loop(REPL)プログラムを作成してユーザー入力を読み取り、入力を上記のExec関数に渡し、Exec関数の出力を標準出力にフォーマットしてから、ループでユーザー入力の読み取りを続けます。

コンパイルの問題

ターミナルの統合は最初の手順です、次には重要な質問を検証する必要があります。TiDBWasmのターゲットにコンパイルできますか?TiDBGolangで製されていますが、しかし、参照されたライブラリは、プラットフォーム固有のコードを直接コンパイルできない場合があります。公式のGolangドキュメントに従ってコンパイルはできません。

~/go/src/github.com/pingcap/tidb(master*) » GOOS=js GOARCH=wasm go build -o bin/tidb.wasm tidb-server/main.go
build command-line-arguments: cannot load github.com/pingcap/tidb/util/signal: no Go source files

~/go/src/github.com/pingcap/tidb(master*) » ls util/signal
signal_posix.go  signal_windows.go

signal関連の関数にWasmプラットフォームの実装がないため、コンパイルに失敗しました。ネコをマネてトラを描くのことのように、signal wasm.goを実装する。

package signal

// SetupSignalHandler setup signal handler for TiDB Server
func SetupSignalHandler(shutdownFunc func(bool)) {
}

そして、コンパイルを再開する。

~/go/src/github.com/pingcap/tidb(master*) » GOOS=js GOARCH=wasm go build -o bin/tidb.wasm tidb-server/main.go
# github.com/coreos/go-systemd/journal
../../../../pkg/mod/github.com/coreos/go-systemd@v0.0.0-20181031085051-9002847aa142/journal/journal.go:99:13: undefined: syscall.UnixRights
# github.com/pingcap/goleveldb/leveldb/storage
../../../../pkg/mod/github.com/pingcap/goleveldb@v0.0.0-20171020122428-b9ff6c35079e/leveldb/storage/file_storage.go:81:16: undefined: newFileLock
../../../../pkg/mod/github.com/pingcap/goleveldb@v0.0.0-20171020122428-b9ff6c35079e/leveldb/storage/file_storage.go:166:3: undefined: rename
../../../../pkg/mod/github.com/pingcap/goleveldb@v0.0.0-20171020122428-b9ff6c35079e/leveldb/storage/file_storage.go:252:11: undefined: rename
../../../../pkg/mod/github.com/pingcap/goleveldb@v0.0.0-20171020122428-b9ff6c35079e/leveldb/storage/file_storage.go:257:11: undefined: syncDir
../../../../pkg/mod/github.com/pingcap/goleveldb@v0.0.0-20171020122428-b9ff6c35079e/leveldb/storage/file_storage.go:354:14: undefined: rename
../../../../pkg/mod/github.com/pingcap/goleveldb@v0.0.0-20171020122428-b9ff6c35079e/leveldb/storage/file_storage.go:483:9: undefined: rename
../../../../pkg/mod/github.com/pingcap/goleveldb@v0.0.0-20171020122428-b9ff6c35079e/leveldb/storage/file_storage.go:519:13: undefined: syncDir
# github.com/remyoudompheng/bigfft
../../../../pkg/mod/github.com/remyoudompheng/bigfft@v0.0.0-20190512091148-babf20351dd7/arith_decl.go:10:6: missing function body
../../../../pkg/mod/github.com/remyoudompheng/bigfft@v0.0.0-20190512091148-babf20351dd7/arith_decl.go:11:6: missing function body
../../../../pkg/mod/github.com/remyoudompheng/bigfft@v0.0.0-20190512091148-babf20351dd7/arith_decl.go:12:6: missing function body
../../../../pkg/mod/github.com/remyoudompheng/bigfft@v0.0.0-20190512091148-babf20351dd7/arith_decl.go:13:6: missing function body
../../../../pkg/mod/github.com/remyoudompheng/bigfft@v0.0.0-20190512091148-babf20351dd7/arith_decl.go:14:6: missing function body
../../../../pkg/mod/github.com/remyoudompheng/bigfft@v0.0.0-20190512091148-babf20351dd7/arith_decl.go:15:6: missing function body
../../../../pkg/mod/github.com/remyoudompheng/bigfft@v0.0.0-20190512091148-babf20351dd7/arith_decl.go:16:6: missing function body

これは同じ問題だ、偽物の実装を使用する、コンパイルは可能です。

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
}

同じ方法を使用してarith_declの問題を解決はできません、missing function bodyエラーとは何ですか?arith_decl.goのディレクトリは見てとうり、何が起こったのかがわかります。

~ » ls ~/go/pkg/mod/github.com/remyoudompheng/bigfft@v0.0.0-20190512091148-babf20351dd7
arith_386.s    arith_arm64.s  arith_decl.go    arith_mipsx.s  calibrate_test.go  fermat_test.go  fft_test.go  LICENSE  scan.go
arith_amd64.s  arith_arm.s    arith_mips64x.s  benchmarks     fermat.go          fft.go          go.mod       README   scan_test.go

arith_decl.goは関数の宣言であり、関数の特定の実装は、各プラットフォームに関連するなアセンブリファイルにあります。

対応の関数は実装、問題は解除できる、そう考えるのは簡単すぎる。このbigfftはライブラリです、だからそちのコードの編集は簡単な作業ではありません。TiDBはこのライブラリに直接依存せず、代わりにmathutilに依存し、mathutillはこのbigfftに依存します。

TiDBは、mathutilのライブラリが提供する関数を使用します、理想的な対策はデフォルトでこれら2つのライブラリを使用する、Wasmのコンパイル時には使用されません。そのために、util/mathutilパッケージを作成する、中にリmathutil_linux.gomathutil_js.goを入れて。元々参照されていた関数をmathutil_linux.goに再輸出します、これらの関数はmathutil_js.goに再実装されます。これから、Wasmのターゲットをコンパイルの時はオリジナルのmathutilライブラリを使用しません。

~/go/src/github.com/pingcap/tidb(feature/wasm) » GOOS=js GOARCH=wasm go build -o bin/tidb.wasm tidb-server/main.go
~/go/src/github.com/pingcap/tidb(feature/wasm) » ls -l bin
total 85816
-rwxrwxr-x 1 coder coder 87868180 Dec 14 18:16 tidb.wasm

成功しました!

互換性の問題

コンパイル出したのtidb.wasmos.Stdinを介して入力したSQLを読み取り、os.Stdoutを介して結果を出力します。だから、ウェブページはまだブランクした。ただし、TiDBのログはos.Stdoutに送られるため、ブラウザコンソールでTiDBの通常の起動のログを確認する必要があります。しかし、残念ながら例外のスタックトレースが表示されます。

exception-stack

このエラーが発生するの理由はos.Statは実装しない。現在、GolangWASIのサポートはあまり良くない、wasm_exec.jsfsを部分的に実装しています:

global.fs = {
  writeSync(fd, buf) {
    ...
  },
  write(fd, buf, offset, length, position, callback) {
    ...
  },
  open(path, flags, mode, callback) {
    ...
  },
  ...
}

このfsの実装はstatlstatunlinkmkdirなどの呼び出しを実装しません。幸いなことに、JavaScriptは動的プログラミング言語です、関数をfsに動的に追加できます。

function unimplemented() {
  const err = new Error('not implemented');
  err.code = 'ENOSYS';
  arguments[arguments.length - 1](err)
}

fs.stat = unimplemented;
fs.lstat = unimplemented;
fs.unlink = unimplemented;
fs.rmdir = unimplemented;
fs.mkdir = unimplemented;
go.run(result.instance);

ウェブページを再読み込み、起動ログがコンソールに表示されました!

startup-log

これまで、TiDBWasmにコンパイルするの技術的な問題はすべて解決されました。後では、以前に作成されたのREPLターミナルを置き換える適切なターミナルを見つけることなら、ページにSQLを入力してのことはできます。

ユーザーインターフェース

上記の仕事により、SQLを受け入れてその実行結果を出力するExec関数ができました。ブラウザで実行するには、この関数とインタラクションするためのブラウザバージョンのSQLターミナルも必要です。その対策はwindowExec関数を登録する、ならJavaScriptの関数はExec呼び出すことはできます。

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
}))

それで、コンソールの場合はSQLを実行できます。

console-call-go-func

jquery.console.jsを使用してSQLターミナルを構築し、executeSQLをコールバックとして渡します。完了します!

show-databases

ローカルファイルアクセス

TiDBには、ローカルファイルシステムにアクセスする必要があるいくつかの関数があります、例えばLOAD DATAおよびLOAD STATSLOAD DATAの機能は、ローカルファイルからデータベースにデータをインポートする。交換方法はブラウザのファイルアップロードウィンドウを使用した、LOAD STATSの実装は同じです。

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
}))

SOURCEコマンドはローカルファイルを読み取り、まとはMySQLクライアントで実行します。TiDBを初めて使用するユーザーは、TiDBMySQLの互換性を確認したいと考えています。このコマンドは、ユーザーがテストデータをすば速いインポートするのに役立ちます。

SOURCEコマンドの効果を示す例としてtest.sqlファイルを取り上げますtest.sqlファイルの内容は下記です。

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';

SOURCEコマンドを入力すると、ファイル選択ボックスがポップアップします。

source-file-select

選択後、SQLが自動的に実行され、TiDBのテストを続行できます。

source-test

まとめと展望

TiDBを移植するために、主にいくつかの問題を解決しました。

  • ブラウザはポートをリスニングのことはできません。SQLターミナルをTiDBに埋め込みました。

  • goleveldbWasmコンパイル。

  • bigfftWasmコンパイル。

  • GolangWASIのサポートが不完全なため、fs関連の機能がありません。

  • TiDBはローカルファイルの読み込みをブラウザのアップロードファイルの読み込みに変換します。

  • SQLをバッチで実行するSOURCEコマンドをサポート。

現在PingCAPは、このプロジェクトをTiDB PlaygroundおよびTiDB Tourのユーザーが利用できるようにしました。ユーザーはインストールと構成を行う必要がないため、ユーザーはドキュメントを読みながら試してみることができます。これにより、ユーザーがTiDBは使用して学ぶ安いです。コミュニティにはすでにTiDB Wasmに基づくデータベースチュートリアルがありますtidb-wasm-markdown

このプロジェクトはPingCAP Hackathonで二等賞を受賞しました。スケジュールが厳しいため、まだたくさんの機能は実装されていません、例えば:

  • IndexedDBを使用したデータの永続化:IndexedDBのストレージインターフェイスのセットを実装する。

  • P2Pを使用した他のブラウザーにサービスを提供する:将来、ますます多くのアプリケーションがWasmに移行されるでしょう、多くのデータベースが必要、TiDB Wasmを使用できます。

  • TiDB Wasmのバイナリスリミング:現在のはほぼ80MB、読み込みが遅い、実行時によりたくさんのメモリを使用する、これらは最適化する必要があります。

興味のあるコミュニティの友人は、このプロジェクトに参加して一緒に楽しんでください、プロジェクトinfo@pingcap.comでお問い合わせいただくこともできます。