真·serverless的KV数据库

上次在云函数里面整了一个嵌入式的SQL数据库以后爽的连云开发数据库都不想用了。不过有的时候还是需要用到kv存储,那能不能也serverless一把呢?

level是一个serverless场景下KV存储的一个一个还不错的选择。打包一个层以后直接引用就可以了:

#levelDB.zip#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
'use strict';
const { Level } = require('level')
// Create a database
const db = new Level('/tmp/example', { valueEncoding: 'json' })

exports.main_handler = async (event, context) => {

var n = 10000,d=Date.now();
// Add an entry with key 'a' and value 1
for(var i=0;i<n;i++){
await db.put(Math.random().toString(16).substring(2), Math.random())
}
console.log("插入"+n+"个记录耗时"+(Date.now()-d)+"毫秒")


// Add multiple entries
let batch=[];
for(var i=0;i<n;i++){
batch.push({type:'put',key:Math.random().toString(16).substring(9), value:Math.random()})
}
d=Date.now();
await db.batch(batch)
console.log("批量插入"+n+"个记录耗时"+(Date.now()-d)+"毫秒")

// Get value of key 'a': 1
d=Date.now();
for(var i=0;i<n;i++){
try{
let v = await db.get(Math.random().toString(16).substring(9));
if(v) console.log("got value:"+v)
}catch(e){
if(e.code != "LEVEL_NOT_FOUND")
console.log(e)
}
}
console.log("查询"+n+"个记录耗时"+(Date.now()-d)+"毫秒")

return "all done"
};

(纯测试,保存路径用了/tmp/ 实际使用的时候应该挂上CFS)

这个level似乎是纯JS实现,比起通过node-gyp用C实现了关键计算的sqlite,读写性能上并没有太大优势,不过多一个选择还是不错的。以后小应用就可以纯云函数实现小规模提供服务了,小并发的时候性能甚至可能比云数据库服务更好。规模上去的时候再更换存储方案大部分主要的逻辑也能沿用。

facebook的rocksDB 是另一个选择。它和sqlite一样使用了node-gyp本地构建的方式,让人期待了一下它会不会有更好的性能表现。依赖node-gyp的层直接在mac上打包上传到linux服务器上是用不了的,因此使用了docker的linux + nodejs环境环境搭建

1
2
3
4
5
echo "cd /usr/src;npm install rocksdb --save">tmp.sh
chmod +x tmp.sh
docker run --rm -v "$PWD":/usr/src node:16 /usr/src/tmp.sh
zip -r rocksdb.zip node_modules
rm -rf node_modules tmp.sh package.json package-lock.json

这样就得到了一个layer,超过10M无法上传上来,需要的自己生成一下。

按照leveldown的api运行测试了一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
'use strict';
const rocksdb = require('rocksdb')

// Create a database

const db = new rocksdb("/tmp/rocksdb")
async function openDB(){
return new Promise(res=>{
db.open({createIfMissing:true},()=>{
console.log("db opened");
res();
})
})
}
async function closeDB(){
return new Promise(res=>{
db.close(function (err) {
console.log("db closed")
res()
})
})
}
exports.main_handler = async (event, context) => {
await openDB();
var n = 200,d=Date.now();
for(var i=0;i<n;i++){
db.put(Math.random().toString(16).substring(2), Math.random(),{'sync':true},()=>{})
}
console.log("同步插入"+n+"个记录耗时"+(Date.now()-d)+"毫秒(同步插入太多DB就崩溃了,并且会干扰后面的异步操作,不推荐)");
await closeDB().then(openDB);//重新打开一次数据库来消除同步操作的干扰
d=Date.now();
for(var i=0;i<n;i++){
await new Promise(res=>{
db.put(Math.random().toString(16).substring(2), Math.random(),()=>{res()})
})
}
console.log("异步步插入"+n+"个记录耗时"+(Date.now()-d)+"毫秒(会受到前面同步插入的干扰,需要重新打开一次DB来测试)")

n=10000
let batch = db.batch();
for(var i=0;i<n;i++){
batch.put(Math.random().toString(16).substring(9),Math.random())
}
d=Date.now();
await new Promise(res=>{batch.write(res)})
console.log("批量插入"+n+"个记录耗时"+(Date.now()-d)+"毫秒")

d=Date.now();
for(var i=0;i<n;i++){
let v = await new Promise(res=>{db.get(Math.random().toString(16).substring(9),(err,value)=>{
if(err == null)
res(value)
else
res()
})})
if(v) console.log("got value "+v)
}
console.log("查询"+n+"个记录耗时"+(Date.now()-d)+"毫秒")

await closeDB()
return "all done"

};

除了性能不咋地,数据量上去一点还很容易挂掉,可能使用的姿势还不大对?

还有一些更简单的jsonDB类小玩具,比如lowdb(这个是_pure ESM 包,引用的时候要注意一下_),jsondbsimple-json-db等,使用简单又各有特色,小数据量玩玩应该都不错。

本来还有一个选择的,BerkeleyDB据说也很香,但是尝试打包一个layer的时候发现接近120M,无法压缩到layer要求的50M以内

1
2
3
4
5
6
7
echo "cd /usr/src">tmp.sh
echo "npm init -y ">>tmp.sh
echo "npm install berkeleydb --save">>tmp.sh
chmod +x tmp.sh
docker run --rm -v "$PWD":/usr/src node:11 /usr/src/tmp.sh
zip -q -r berkeleydb_node11.zip node_modules
rm -rf node_modules package-lock.json package.json tmp.sh

将来有更多需求的时候再尝试用其他的方式把它打包进来用用吧。

最后,还是觉得就嵌入式数据库而言,sqlite是比较香的。