【系列:TDengine 工业物联网实战:从零搭起可运行系统 · 第 6 篇】

同一个数据库,三种进门方式:WebSocket 走 6041、REST 也走 6041、Native 独占 6030。端口背后藏着协议实现的本质差异。读懂这里,你才能写出公平的基准测试,也才知道日常开发该走哪扇门。

一、为什么 6041 端口能同时住两种协议?

先看一张拓扑图。

信息图:6041端口同时承载WebSocket与REST,6030端口为Native私有协议直连

TDengine 的架构里有一个叫 taosAdapter 的网关组件。它监听 6041 端口,把外部的 HTTP/WebSocket 请求翻译成数据库内部调用。所以同一个端口,既能收到 REST 的 HTTP POST,也能维持 WebSocket 的长连接——协议不同,入口相同。

Native 则完全不同。它走 6030 端口,使用 TDengine 自有的二进制私有协议,绕过 taosAdapter,直连数据库服务。协议开销更小,但环境要求更高——这一点后面"基准"一节会说明白,没跑过基准之前,谁更快别下结论。

一句话记住:6041 是接待大厅,6030 是内部通道。

二、统一抽象:管线不关心你走哪扇门

第 4、5 篇的写入管线,依赖一个叫 TelemetryWriter 的抽象类。它的定义非常干净:

class TelemetryWriter(ABC):
    transport: str

    @abstractmethod
    async def connect(self) -> None: ...
    @abstractmethod
    async def write(self, records: Sequence[Record]) -> WriteResult: ...
    @abstractmethod
    async def health_check(self) -> bool: ...
    @abstractmethod
    async def close(self) -> None: ...

    async def __aenter__(self) -> TelemetryWriter: ...
    async def __aexit__(self, ...) -> None: ...

四个抽象方法加异步上下文管理器,就是全部约定。

工厂模式负责三选一:

def create_writer(settings: Settings, transport: str | None = None) -> TelemetryWriter:
    selected = transport or settings.transport
    if selected == "websocket":
        return WebSocketWriter(settings)
    if selected == "rest":
        return RestWriter(settings)
    if selected == "native":
        return NativeWriter(settings)
    raise ValueError(f"unknown transport: {selected}")

换传输方式?改一个配置项的事。背压、攒批、重试、落盘逻辑一概不动——它们只看 TelemetryWriter 这个接口。

信息图:TelemetryWriter抽象接口,三个具体实现类,业务管线无感切换

这就是面向接口编程的价值:传输层是门,业务是房子,换门不影响居住。

三、WebSocket:一条连接,一把锁,一个 cursor

WebSocketWriter 的核心写逻辑,浓缩在 write 方法里:

async def write(self, records: Sequence[Record]) -> WriteResult:
    if not records:
        return WriteResult(0, 0, 0.0, self.transport)
    if self._cursor is None:
        raise RuntimeError("writer is not connected")
    sql = build_insert_sql(records, self.settings.tdengine_database)
    started = time.perf_counter()
    async with self._lock:
        affected = await asyncio.to_thread(self._cursor.execute, sql)
    elapsed = time.perf_counter() - started
    written = int(affected) if isinstance(affected, int) and affected >= 0 else len(records)
    return WriteResult(len(records), written, elapsed, self.transport)

几个关键点:

  • 连接串是 DSN 格式:ws://user:pass@host:6041/{db}
  • 一个连接对应一个 cursor,所有写操作通过 asyncio.Lock 串行化——防止并发踩踏
  • taosws 是官方 Python 连接器,纯客户端协议实现,不需要本地装 taosc

注意 asyncio.to_thread:cursor 的 execute 是同步阻塞调用,这里丢到线程池里执行,避免卡死事件循环。这是个很重要的工程细节。

四、REST:两代响应,一个兼容层

RestWriter 基于 aiohttp 实现,核心是 _execute

async def _execute(self, sql: str) -> dict[str, object]:
    if self._session is None:
        raise RuntimeError("writer is not connected")
    async with self._session.post(self.settings.rest_url, data=sql.encode()) as response:
        body = await response.json(content_type=None)
        if response.status >= 400:
            raise OSError(f"TDengine REST returned HTTP {response.status}: {body}")
        if not isinstance(body, dict):
            raise OSError("TDengine REST returned an invalid JSON body")
        if not response_is_success(body):
            raise OSError(f"TDengine REST SQL failed: {body}")
        return body

请求本身平淡无奇:POST 到 http://host:6041/rest/sql/{db},BasicAuth 认证,body 直接放 SQL 文本。

有意思的是响应兼容层:

def response_is_success(body: dict[str, object]) -> bool:
    """Accept both legacy taosAdapter and current REST response envelopes."""
    return body.get("status") == "succ" or body.get("code") == 0

老版 taosAdapter 返回 {"status": "succ"},新版返回 {"code": 0}。代码必须同时认。

affected_rows 的解析更繁琐:老版取 rows 字段,新版取 data[0][0],还得带类型检查——防止把结果集行数误当写入行数。

这些兼容代码说明一个现实:REST 协议演进过,不兼容的历史包袱都在这里偿还。

但 REST 有个硬伤:没有参数绑定,没有订阅能力。每次写入都是一次 HTTP 往返,高频写入时协议开销明显高于长连接。

信息图:REST接口需兼容新旧两代响应格式,status与code双信封判断

五、Native:私有协议直连,但要养一个客户端依赖

NativeWriter 的代码和 WebSocket 几乎一样:

async def write(self, records: Sequence[Record]) -> WriteResult:
    if not records:
        return WriteResult(0, 0, 0.0, self.transport)
    if self._connection is None:
        raise RuntimeError("writer is not connected")
    sql = build_insert_sql(records, self.settings.tdengine_database)
    started = time.perf_counter()
    async with self._lock:
        affected = await asyncio.to_thread(self._connection.execute, sql)
    elapsed = time.perf_counter() - started
    written = int(affected) if isinstance(affected, int) and affected >= 0 else len(records)
    return WriteResult(len(records), written, elapsed, self.transport)

差别在底层:taos.connect(host, port=6030) 走私有协议直连,不经过 taosAdapter。

代价是依赖:必须安装与服务器版本匹配的 taosc 和 taospy,安装命令是 pip install -e './python-services[native]'。版本不兼容,连接直接失败。连接过程还是阻塞的,所以 connect 也得用 to_thread 包一层。

选 Native 等于选了一条快速但需要自己铺路的路。

六、公平基准怎么跑?不是拉个脚本就完事

项目里 benchmark 的设计非常讲究,核心四点:

一、warmup 不计时。 先跑 2 批数据,让连接、驱动初始化这些一次性开销不污染计时。

二、批次间设备轮转。 (written + index) % devices,让数据均匀分布到所有子表,避免热点。

三、计时只覆盖批次提交。writer.write 开始到返回,不包含数据生成时间。数据来自 simulator factory,确定性 seed,可复现。

四、百分位线性插值。 样本少也能算出稳定的 P95/P99:

position = (n-1) * q,在相邻样本间线性插值。

最终产出三份报告:benchmark.json(机器可读)、benchmark.csv(表格)、benchmark.md(人读,带"结果只描述本主机本负载"的免责声明)。

流程图:基准流程从warmup到批次计时再到P95P99统计与三份报告输出

跑矩阵时,batch_size 从 100 到 5000 五档循环。默认参数:1000 设备、100000 行、batch 2000、warmup 2 批。

但请注意:这个项目没有实测报告,任何数字都要自己跑出来。 本文只教方法论,不编数据。docs/04 里的调优顺序也值得记下:设备量 → 批量逐级(500/1000/2000/5000)→ worker 数 → VGROUPS → BUFFER。

记录清单同样重要:TDengine 版本、CPU、内存、批量、吞吐、P95/P99、错误率、WAL、副本数、VGROUPS、BUFFER——少记一项,报告就失去可复现性。

七、怎么选?文档说得很实在

docs/03 里有一段话,直接引用:

“新系统使用 WebSocket。REST 适合简单管理脚本,但缺少参数绑定和订阅。Python Native 可用于必须使用原生能力且能管理客户端依赖的场景。最终选择应基于相同数据、相同批量、相同主机的重复基准测试,而不是凭经验预判。”

翻译成决策树:

  • 新系统、生产环境 → WebSocket,兼得协议性能和部署轻便
  • 管理脚本、临时查询 → REST,简单直接
  • 必须用原生能力、能管好依赖 → Native,但要接受版本兼容的运维成本

REST 的定位在这套代码里很明确:教学和兼容测试。它让新手能以最低门槛跑通链路,也让老脚本不至于失效。

八、小结:三路对比

维度WebSocketRESTNative
端口604160416030
协议WSHTTP私有二进制
依赖taoswsaiohttptaosc + taospy
参数绑定支持不支持支持
订阅支持不支持支持
适用新系统首选教学/脚本原生能力场景

传输层的故事到这里就结束了。但数据从哪来?第 7 篇我们聊聊数据入口:模拟器与多源采集——毕竟写入前提是,得有数据写。


觉得有用?点个关注,持续获取优质内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值