TDengine 上关于 Lua 连接器开发的一些总结和思考

为什么是 TDengine

首先,TDengine Database 符合我的审美。尽管我一直在关注时序数据库(Time-Series Database,TSDB)这个领域,但直到遇见 TDengine 才算找到符合预期的产品。理想中的产品所采用的语言应该自带运行时,属于强类型的编译语言,这样打包编译后对环境的依赖比较小,用起来比较方便;代码工程应该尽可能精巧,便于理解掌握;产品运行起来要快,资源消耗相对较小。如果一个产品像一辆卡车,载重 10 吨,自重 5 吨,它显然不是理想选项。以上解释都是先射箭后画靶,当然 C 语言才是决定项,这是我在 emacs 环境中配置最完善的语言。

TDengine Database 的每个 Dnode 节点既负责存储也负责计算,在时兴“计算与存储分离”的当下,这个技术方案显得有些另类。不过实践才是检验真理的标准,目前还没看到明显问题,我们会在 K8s 的 CSI 方案中继续测试。

Lua连接器

我为 TDengine 编写了 Lua 版的连接器,主要面向两个用户群体,一是 OpenResty(Nginx+Lua),另一个是 Skynet。这两个产品也是我格外欣赏的两款开源产品。经过测试,在我的个人笔记本电脑(SSD 硬盘、8G RAM)上,基于 TDengine 2.0.18,用我自己编写的 Lua 连接器单线程写入只有 3 个列的记录(时间戳、整数、tag),平均每秒可写入 1 万条,见下图。我已提交了 benchmark 测试代码,大家可以在自己的工作环境下测试感兴趣的条目。

Lua demo

目前社区里看到的 Lua 连接器分别基于 Lua5.1 和 Lua5.3,这主要是因为 OpenResty 采用的是基于 5.1 版本的 LuaJIT,而 skynet 虽然跟随 Lua 社区升级到了 Lua5.4,但我并没用遇到兼容问题,所以只在本地针对 Lua5.4 的兼容性进行了验证,并没有提交。

从 Lua 角度来看,Lua5.1 和 Lua5.2 及以上版本属于两个世界。这主要是因为 Lua5.2 版本上的一个重要改进——“yieldable pcall and metamethods”。Lua5.2 这个改进允许调用 C 函数时不马上返回,而这是异步操作必须的特性。所以大家在基于 Lua5.1 API 的 OpenResty 社区里经常看到有人在问如何解决“attempt to yield across C-call boundary”问题。

我在实现连接器时也必须面对这个问题,当然也有其他 API 差异,所以编写了两个版本的代码,而不是用编译开关控制。事实上,有别于 OpenResty 过度依赖 LuaJIT,我更赞同云风的观点,跟随主流社区升级版本带来的综合收益应该高于在某个版本上定制。

OpenResty 用户可以直接在 http 请求处理中通过 Lua 连接器访问 TDengine Database,如同使用 MySQL 的体验一样,不需要交给服务器处理,整体架构非常简洁。由于在此 Lua 版本上只能实现同步访问数据库,出于性能考虑,我试验了一个连接池,避免频繁地建立然后释放数据库连接。很遗憾的是,Lua 的非抢占特性导致一段代码未执行完时不会释放 CPU,所以并没有机会处理其他请求,观察到的 WaterMark 也一直是 1,这个问题如何解决,暂时还没有结论。如果在目前基础上想尽可能地多榨出一点性能来,我的建议是尽可能推迟从连接池里申请连接,并尽可能提前归还连接。

Skynet 用户也可以仿照 MySQL 的使用方式来使用 Lua 连接器,不过我建议遵从 Skynet 的建议,在 simpledb 这样的服务中保持连接,做具体的请求处理工作。因为 Skynet 本身实现了比较完善的 Actor 模型,所以我并不确定目前的方案有没有带来瓶颈。如果同步访问带来瓶颈,可以尝试一下异步调用,但因为 TDengine 关于异步的设计中要求前一个访问结果返回后才能执行下一个访问,所以异步调用带来的瓶颈会向哪里转移,目前也不清楚。

尽管可能性很小,但是连接器不能回避数据库连接失效的问题。幸好 TDengine 提供了一个心跳机制,用来检测连接是否有效。目前实现的连接器还没有完善这个功能,所以如果通往数据库的链路失效,需要应用重建连接。

在实际生产环境中,我们发现 TDengine Dnode 向 Client 端返回的字符串类型的数据并没有附加结尾符,而出于性能考虑,在向连接器返回数据时也没有重新申请内存做一次拷贝以追加结束符,进而导致往数据库中存入网络类型 4G 和 WIFI,在查询时返回“4GFI”这样的结果。这是一个隐蔽性很强的默认规则,其他语言连接器的设计者注意提前规避潜在问题。

自定义函数

自定义函数UDF)能降低应用的复杂度,或者实现预置查询函数无法实现的功能,我判断 Lua 是最适合承担这个使命的语言。因为 Lua 的设计初衷就是与 C 语言集成,两者堪称倚天屠龙。

目前 TDengine 官方已经实现了用户自定义函数的框架,基于 Lua5.1 实现了基础模型,并集成了 Lua5.1。因为 Lua 社区的分裂状态,预置的 Lua 开发库给我的开发工作带来一些小麻烦,用户肯定不能同时使用两个 Lua 版本,最终还是要在 TDengine 中同时集成 Lua5.1 和一个高版本(即将发布版本为 Lua5.4.4),靠宏开关选择一个 Lua 版本参与编译。

具体实现起来,需要分别为两个版本的 C API 设计接口,Lua 每个版本升级都会带来一些功能上的变化和升级,因此能否抽象出一套通用接口对 Lua 用户屏蔽差异,对于这个问题的答案我是持比较悲观的态度的。

展望

以上就是我在使用 TDengine 时的经验汇总。目前,连接器已部署在我们的生产环境上,并经历了两次较大规模的生产活动,轻松完成了使命。接下来会在项目应用中继续完善上面提到的问题,支持用 Lua 实现UDF是我的下一个工作重点,这将进一步降低应用的复杂度。