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

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 这个接口。

这就是面向接口编程的价值:传输层是门,业务是房子,换门不影响居住。
三、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 往返,高频写入时协议开销明显高于长连接。

五、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(人读,带"结果只描述本主机本负载"的免责声明)。

跑矩阵时,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 的定位在这套代码里很明确:教学和兼容测试。它让新手能以最低门槛跑通链路,也让老脚本不至于失效。
八、小结:三路对比
| 维度 | WebSocket | REST | Native |
|---|---|---|---|
| 端口 | 6041 | 6041 | 6030 |
| 协议 | WS | HTTP | 私有二进制 |
| 依赖 | taosws | aiohttp | taosc + taospy |
| 参数绑定 | 支持 | 不支持 | 支持 |
| 订阅 | 支持 | 不支持 | 支持 |
| 适用 | 新系统首选 | 教学/脚本 | 原生能力场景 |
传输层的故事到这里就结束了。但数据从哪来?第 7 篇我们聊聊数据入口:模拟器与多源采集——毕竟写入前提是,得有数据写。
觉得有用?点个关注,持续获取优质内容。

376

被折叠的 条评论
为什么被折叠?



