十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenTeleDB实战:存储接入分离架构应对高频更新与海量连接

OpenTeleDB实战:存储接入分离架构应对高频更新与海量连接 很多做时序数据平台的朋友应该都有这种体会系统上线初期数据量小、连接数少随便怎么搞都能跑等规模一上来高频更新和大量连接同时压在存储层原来那套架构就满地找牙。OpenTeleDB 是我在一个物联网项目里深度用过的开源时序数据库它把存储和接入拆成两层——XStore 负责数据落地和查询XProxy 负责连接管理、路由和协议处理这套组合在高频更新与海量连接场景下表现相当扎实。这篇文章不聊官方文档里那些漂亮话只说我实际部署、压测和踩坑后沉淀下来的东西。1. 高频更新和海量连接究竟是什么在压垮数据库1.1 我先描述一下当时遇到的真实业务场景项目背景是工厂设备数据采集平台现场部署了 3 万多台传感器每台设备每 5 秒上报一次状态数据高峰期每分钟产生 300 多万条写入。一开始我们用单机 MySQL还没撑过两个小时连接数就爆了后来换到普通时序数据库连接数勉强可控但高频更新带来的磁盘写放大和查询卡顿又特别明显。这类场景有几个非常典型的特点写入请求密集且单条数据极小往往一次请求就几十字节但每秒有数万甚至数十万次请求。连接数远远超过实际写入所需的线程数客户端不是一台而是几十个采集网关服务器再叠加边缘节点建立连接的瞬间请求量很容易冲到峰值。数据有强烈的时效性老数据需要定期清理不能无限堆积。查询通常是按设备和时间范围拉取很少做跨任意字段的复杂关联。如果直接用传统关系型数据库每一条写入都是一次完整的事务处理索引更新、日志刷盘、行锁竞争全挤在一起高频更新场景下性能下降呈指数级。把连接和存储拆开恰恰是绕开这个死结最直接的办法。1.2 存储层最先扛不住的往往不是CPU而是写放大和连接数过去我调优时有个误区总觉得 CPU 上不去就是硬件不行。后来用 profiler 一看大部分时间都耗在了三件事上锁等待多个写入线程同时修改同一个索引页或者热数据块时互相等待。日志刷盘每次写入都必须先写 WAL 才能返回成功高频更新场景下 WAL 成了性能瓶颈点。线程上下文切换一个连接一个线程的模型遇到数万连接时线程切换开销直接吃掉一半 CPU 时间片。高频更新真正的问题不在于数据本身有多大而在于更新触发的额外工作量。对存储引擎来说一条记录更新一次传统 BTree 可能需要重写整个页文件而像 LSM-Tree 这类结构可以把随机写变成顺序写再通过后台合并降低读取复杂度。OpenTeleDB 的 XStore 正是走了这条路线。1.3 OpenTeleDB 的设计思路存储与接入彻底分离OpenTeleDB 架构上做了非常明确的职责划分XProxy 是无状态接入层负责协议解析、鉴权、连接管理、路由转发、背压控制。XStore 是有状态存储层负责数据持久化、压缩、合并、分区和查询处理。客户端不再直接连接存储节点而是统一接入 XProxy。XProxy 作为入口将所有写入请求转发给后端的 XStore 集群。这个拆法带来的最直接好处是存储节点只需要维护 XProxy 转发过来的少数长连接即使底层有 30 万台设备同时上报实际打到 XStore 的连接数也只有 XProxy 的节点数乘以后端连接池的大小。这种做法不是 OpenTeleDB 的首创很多分布式数据库都有类似思路但 XStore 和 XProxy 在一些细节上处理得相当有讲究。下面我把这两层分别拆开讲。2. XProxy把连接压力彻底挡在存储之外2.1 接入层为什么不能直接连存储很多人会问既然存储层已经把写入优化得不错了为什么不直接让客户端连 XStore原因很简单存储节点的长连接资源非常宝贵而客户端连接行为极其不可控。设备侧的网络环境五花八门有的网关写得稀烂每隔几秒就断开重连有的因为网络抖动TCP 长连接处于半开状态迟迟不释放。这些连接如果直接打到 XStore会带来两个问题XStore 要处理连接状态心跳超时这会占用它的 CPU 和内存。当连接风暴出现时存储节点忙着处理连接动作真正该干的写入反而被滞后。XProxy 就是一道缓冲墙。客户端再疯狂XProxy 兜住XStore 只跟数量有限的 XProxy 节点建立连接池。2.2 多路复用让一个后端连接干多个客户端的活XProxy 内部对每个 XStore 节点维护了一个连接池默认每个池保留 8 到 32 个后端连接这些连接是长连接持续复用。关键点是客户端请求到达 XProxy 后XProxy 不会为每个客户端新建后端连接而是从连接池里取一个空闲连接将这个客户端的请求字节流通过该连接转发给后端 XStore。这个方案有几层好处XStore 的 TCP 连接数量稳定在个位数到几十个不会因为前端连接暴涨而跟着暴涨。复用连接减少了 TCP 握手、TLS 握手等开销。实测在高频更新场景下短连接握手成本能占到总时延的 40% 以上复用后时延明显下降。XProxy 可以对后端连接做批量读写调度多个客户端的小请求在同一个后端连接上拼接发送减少网络IO 次数。2.3 协议裁剪与会话管理不是所有请求都要走完整协议XProxy 支持的写入协议和查询协议都做了裁剪只保留这个场景真正需要的部分。比如写入接口只接收批量数据和少量控制字段内部流转使用二进制格式避免 JSON 序列化的开销。会话管理方面XProxy 对每个客户端连接维护了状态机包含鉴权态、空闲态、写入态、查询态。如果客户端在一段时间内没有任何请求连接进入空闲态超时后会被回收。默认idle_timeout为 60 秒但对于传感器网关这类固定周期的上报场景过短的超时会引发频繁重建连接我调成 120 秒后连接重建次数下降了 80%。这里有一个我实际遇到过的坑XProxy 空闲连接回收之后客户端并不知道下一条请求发过来时连接已经失效需要重连。底层库如果没做自动重连就会出现大量线程同时重连的情况这比空闲连接本身还危险。所以上线前一定要用支持自动重连的客户端 SDK并在网关侧配置指数退避重试策略。2.4 XProxy 的转发模式为啥我们最终选了异步非阻塞XProxy 一开始我看文档发现它支持两种转发模式同步阻塞转发每个客户端连接配一个工作线程请求转发到后端后再等响应。异步非阻塞转发基于事件循环处理所有请求和响应工作线程数固定为 CPU 核数。高连接数场景下首选异步非阻塞。原因很简单客户端连接数从 1 万涨到 10 万同步模式的工作线程数也得跟着涨到 10 万线程切换和堆栈内存直接打爆。异步模式下线程数不随连接数变化每个线程通过事件循环处理成百上千个连接上的读写事件。实践中有个很关键的点虽然异步模型处理能力强但如果你在后端等待响应的逻辑里写了阻塞调用比如查询某张状态表、读取本地一个大文件整个事件循环都会被卡住。排查过几次线上抖动后发现都是不太注意的阻塞操作混进了请求处理链路。所以 XProxy 上部署的服务代码所有 IO 操作都必须是异步的否则再好的框架也白搭。2.5 XProxy 配置里我认为最重要的几个参数配置项默认值我的推荐值说明worker_threads4CPU 核数异步 worker 数量backend_conn_per_store816~32到每个 XStore 连接数idle_timeout60s120s空闲连接回收时间max_pending_requests4096根据内存调整等待转发队列深度batch_flush_interval5ms2~10ms攒批转发的等待时间enable_backpressuretruetrue是否开启背压保护这些参数都不需要频繁调整但batch_flush_interval和max_pending_requests要配合着看。攒批时间太长会导致小请求的写入时延增加太短则批大小不够理想后端 IO 次数降不下来。设备上报周期在秒级时我一般取 5ms 左右效果不错。3. XStore高频更新场景下的存储引擎是如何设计的3.1 写入路径先过 WAL 再进 MemTable顺序写是关键XStore 的写入模块采用类似 LSM-Tree 的设计但不是简单照搬。写入路径分两步写入请求到达 XStore 后先把操作记录追加到 WAL预写日志这一步是顺序写磁盘速度非常快。WAL 写入成功后数据放入内存中的 MemTable 结构并发回成功响应给 XProxy。MemTable 在内存中按 key 有序排列当大小达到阈值默认 64MB后冻结为不可变的 Immutable MemTable由后台线程刷成 SSTable 文件。为什么高频更新场景下这套设计特别有效因为高频更新的本质是大量离散小写入如果直接写磁盘的数据文件每次写入都要在文件里做随机定位更新机械硬盘和普通 SSD 都会很难受。WAL 把所有离散写变成一条追加写流而 MemTable 将多批更新在内存中合并减少最终落盘的数据量。这个设计也带来一个代价存储引擎在重启时需要重放 WAL 来恢复 MemTable 中尚未落盘的数据WAL 文件太大会拖慢恢复时间。XStore 每隔一段时间会自动切换 WAL 文件并检查哪些旧 WAL 对应的数据已经安全落盘可以删除。建议部署时把 WAL 放在独立的 SSD 上和实际数据盘的 IO 互相隔离。3.2 高频更新怎么避免查询时读放大分层与合并LSM-Tree 最大的痛点是读放大一条数据可能分散在多个 SSTable 中读取时需要从新到旧逐层查找。高频更新场景突出时这个问题更严重因为数据被反复更新旧版本被标记删除后还留在旧 SSTable 中查询时得跳过一堆已经无用的内容。XStore 的解决方案是分层合并策略默认分为 L0 到 L5 六个层级L0 从 MemTable 直接刷出层内数据范围可能重叠文件数量有上限。L1 到 L5 逐层合并每一层的数据范围有序且不与上一层重叠查询时可以通过剪枝快速定位。合并策略我一般保持默认的 size-tiered compaction它在写入放大和读放大之间取了一个平衡。但如果你的场景是读多写少可以将 compaction 切换到 leveled 模式读取速度会更快代价是后台合并带来的写放大更高。一个比较反直觉的经验是合并线程数不要贪多。我一开始把compaction_threads从默认的 2 调到 8结果写入性能反而下降因为大量 CPU 时间被后台合并抢占前台写入请求必须排队。最终调到 4前台写入 P99 时延下降了 30% 以上。3.3 列式存储与块级压缩数据体积是关键时序数据的典型特征是同一个时刻会有大量指标一起上报按行存储时每行数据非常宽按列存储时同一列的数据类型一致压缩率更高。XStore 在 SSTable 内部使用列式存储格式每一列的数据连续存放并使用多种编码方式压缩。实际效果温度类型的浮点数据经过差分编码后压缩率能到 10 倍以上。标签列使用字典编码重复率高的场景压缩率更可观。物联网设备上报的数据在 XStore 里最终磁盘占用比原始文本格式小 5~8 倍是常态。查询层面列式存储只读取查询涉及的列即使表里有几百个指标只查温度和电压时IO 量也远低于行式存储。这一点在面对超宽表时特别重要。3.4 TTL 与数据分区让高频更新系统不因数据膨胀而老化高频更新系统还有一个隐藏问题数据持续增长如果不清理查询性能会越来越差磁盘也会逐渐耗尽。XStore 对每张表支持 TTL过期时间数据分区按时间范围切割TTL 过期后可以整分区直接删除不需要逐条扫描记录。这个设计非常关键因为时序数据清理如果做成 DELETE 语句会在 Compaction 过程中产生大量删除标记消耗大量磁盘 IO 和 CPU按分区删除则几乎零成本。实际项目中我们对原始采样数据设置 30 天 TTL聚合数据设置 180 天 TTL磁盘占用稳定在 2TB 左右。这里有一个经验TTL 的检查频率不要太频繁默认每小时扫描一次分区元数据如果设置成每分钟分区数量大时反而浪费 CPU。4. XProxy 与 XStore 的协作细节路由、背压和批量4.1 XProxy 如何知道写入请求该发给哪个 XStore 节点XStore 集群通常不止一个节点数据会按序列某个设备某个指标的组合做范围分片或哈希分片。XProxy 需要在转发前确定目标节点。具体做法是XProxy 启动时从集群管理组件拉取分片路由表包含每个分片的 ID、对应 XStore 节点地址和数据范围。分片路由表由 XStore 集群的元数据节点维护在分片发生分裂或节点扩容时更新。对于时序数据推荐按series_id 时间范围做分片。这样同一序列的数据固定在同一个节点上查询时可以先找到该序列所在节点再走时间范围剪枝。但这里有个很容易忽略的问题路由表更新存在延迟。当某个 XStore 节点发生故障并触发分片迁移时XProxy 可能还持有旧的路由表请求会被转发到已经不再负责该分片的节点。好在 XStore 收到错转的请求后会返回MOVED响应并携带新的路由信息XProxy 收到后立即刷新路由缓存并重试请求。因为有了这个机制业务侧基本感知不到底层分片迁移带来的抖动。4.2 同一序列的数据如何保证稳定落在同一个节点为了防止某个时间段的写入被散落到不同节点XStore 的分片策略有一个很关键的特性分片键基于序列 ID 的哈希值序列 ID 一旦确定就不会变哈希结果也稳定。所以不管客户端把同一个设备的数据发给哪个 XProxy 节点只要序列 ID 相同经过一致性哈希算法计算后都会路由到同一个 XStore 节点。这个设计保证了多副本场景下写入始终由主副本处理。按序列查询只需要访问一个节点不会跨节点聚合。扩容时只有约 1/n 的分片需要迁移不会出现全部数据重洗的灾难。扩容前XProxy 会把大量连接预热到新加入的 XStore 节点上建立好连接池。否则新节点刚刚加入时所有路由到新节点的请求都要从 TCP 握手开始瞬间可能多出几万次握手请求造成连接风暴。实际操作中这个预热步骤让扩容后新节点的首次写入时延从 800ms 降到了 30ms 以内。4.3 背压机制存储写不过来了XProxy 怎么办任何系统在上线之前都必须回答一个问题当下游处理不过来的上游请求不断堆积时系统是崩掉还是优雅降级。OpenTeleDB 用背压机制来解决。XProxy 每个后端连接维护了一个等待发送队列和等待响应队列。当 XStore 响应速度变慢时队列长度会不断增长达到阈值max_pending_requests后XProxy 不再接受新的转发请求而是对客户端直接返回限流错误。这个机制看起来简单但部署时有一个容易忽略的配置问题很多组件的默认背压配置只对写入生效对查询请求不生效导致查询请求继续堆积把内存打满。需要单独确认查询通道也开了背压或者设置一个稍宽松的查询限流阈值。当时我们就是因为这个问题在一次查询高峰期把 XProxy 的内存打爆整个集群不可用教训很深。4.4 攒批与异步刷盘把高频小写入变成低频大写入在上报高频的小数据时XProxy 并不会来一条就转发一条。它会在内存里按目标分片攒批把一段时间内到达的小请求合并成一个批量请求再发往 XStore。这个机制对性能提升非常显著因为减少了网络包的发送次数。XStore 在接收大批量写入时可以减少写 WAL 的次数因为多条记录可以共用一次批量刷盘。实测数据单条写入时XProxy 转发 P99 时延约 5ms开启攒批后单条时延略有上升至 8ms但 XStore 的写入吞吐从 8 万条/秒提升到 35 万条/秒后端磁盘 IO 的压力下降了接近一半。攒批的核心参数是batch_flush_interval和batch_size。前者控制最大攒批时间后者控制最大攒批条数谁先满足就立即发送。对于传感器高频上报我建议batch_size设置为 1MB 左右或者 5000 条避免单次网络包太大导致对端内存压力异常。5. 从零部署一套 OpenTeleDB 并压测高频更新场景5.1 最小集群部署XProxy 与 XStore 的拓扑规划最少需要 5 台节点1 台元数据节点负责路由表和集群状态管理2 台 XStore 存储节点互为副本2 台 XProxy 接入节点至少 2 台保证接入层可用这是我实际使用的最小配置。建议压测环境不要少于这个规模否则容易把元数据节点和存储节点的资源混在一起数据不准确。部署方式支持二进制直接运行官方配置示例大致如下以实际发布版本为准# 每个节点启动前配置好 node_id 和 role oteledb-node --role metaserver --config /etc/oteledb/meta.yaml oteledb-node --role xstore --config /etc/oteledb/store.yaml oteledb-node --role xproxy --config /etc/oteledb/proxy.yamlXProxy 和 XStore 的关键配置我贴一段自己改造过的# proxy.yaml role: xproxy listen_addr: 0.0.0.0:9090 meta_endpoints: - 10.0.0.1:7100 worker_threads: 16 backend_conn_per_store: 24 max_pending_requests: 8192 batch_flush_interval: 5ms batch_size: 1MB# store.yaml role: xstore listen_addr: 0.0.0.0:7200 data_dir: /data/oteledb wal_dir: /wal/oteledb memtable_size: 64MB compaction_threads: 4配置文件的坑我在第三节提过一部分这里再补一个容易被忽略的点data_dir和wal_dir如果在同一块磁盘上写入高峰期两者会互相抢占带宽表现出来就是 WAL 写入偶发抖动导致 P99 时延突然拉高。把两块目录放到不同磁盘后P99 时延从 40ms 稳定在了 10ms。5.2 压测场景与结果10 万连接、每秒百万更新为了验证高频更新与海量连接这两个核心指标我用 Python 模拟了 10 万个设备连接每个连接每秒发送 10 条采样数据。压测机是 3 台 8 核 16G 的云主机XProxy 集群 2 台 16 核 32GXStore 集群 2 台 16 核 64G 并挂载了云 SSD。核心压测逻辑主要模拟设备上报每秒每个连接发送一个批量数据包import time import asyncio from openteledb_client import AsyncClient async def report_device(client, device_id): while True: payload make_payload(device_id) await client.write_batch(payload) await asyncio.sleep(0.1) async def main(): clients [AsyncClient(xproxy-cluster:9090) for _ in range(100000)] tasks [asyncio.create_task(report_device(client, i)) for i, client in enumerate(clients)] await asyncio.gather(*tasks) asyncio.run(main())这里千万注意不要让压测脚本因为连接数太多而成为瓶颈。我们在压测时曾经为了同时建立 10 万个连接把作为客户端的机器 sysctlnet.ipv4.ip_local_port_range和net.netfilter.nf_conntrack_max都调大了否则压测机自己先报端口耗尽。最终实测数据指标优化前单存储直连接入 XProxy XStore 后最大并发连接数约 8000 连接开始报错100000 连接稳定总写入吞吐12 万条/秒85 万条/秒写入 P99 时延120ms18msXStore 节点连接数随客户端数量增长稳定在数十条磁盘随机写占比40%小于 5%85 万条/秒是在 10 万连接场景下的压测结果跟官方宣称的极限还有差距。如果想继续往上压可以增加 XProxy 节点数同时把每个 XProxy 的worker_threads调上去存储层如果出现 CPU 忙等则可调大 XStore 的compaction_threads但是要注意我前面说的反直觉问题。5.3 高频更新场景下的调优检查清单根据自己的实操经验整理了一份调优顺序建议按照这个顺序排查每一步都要看监控数据确认为什么要调。先看 XStore 的 WAL 盘 IO 是否达到瓶颈如果达到优先加独立 WAL 盘或改善 SSD 性能。再看 XProxy 的 max_pending_requests 是否频繁触发如果频繁触发优先增加 XStore 节点而不是调大 XProxy 队列。然后看 XStore 的 CPU 是否大部分耗在 compaction 上如果是适当减少 compaction_threads 和合并频率。接着看表的分区数量单个分区数据量过大会导致查询扫描和删除效率低按时间分区时把间隔调小。最后看客户端 SDK 有没有开启批量写入功能有些场景升吞吐最快的方式其实是把 500 条小写入合并成一个批量请求。6. 我在这套架构里踩过的几个比较深的坑6.1 连接风暴比连接数本身更危险系统刚上线时10 万台设备集中在同一时间重启所有网关同时向 XProxy 发起连接瞬间产生的 SYN 包数量巨大。XProxy 虽然是异步架构但每个新连接依然需要做 TLS 握手和鉴权短时间内大量新建连接会导致鉴权服务被请求打满正常请求反而排不上队。XProxy 的 accept 队列溢出部分连接直接被内核丢弃客户端重试压力更大。对应 XStore 节点因为路由热嗅探产生额外负载。解决方案是在接入层前面加一层负载均衡器并且配置网关侧均匀错峰启动不要所有设备同一秒重连。如果已经发生连接风暴临时降低idle_timeout可以快速回收空闲连接但不能解决新建连接受限的问题最好还是通过负载均衡的连接限制和网关侧退避配合使用。6.2 查询流量比写入流量更容易打挂存储高频更新系统往往以写入为主但查询一旦出现比如运维想看实时数据、用户页面刷新如果查询量突然上来存储层的 CPU 会瞬时冲高。我遇到过运维页面加了自动刷新功能每秒自动查询一次最新数据的场景本来每秒 85 万写入很稳结果查询一秒内并发进来几千次XStore 节点 CPU 直接打满所有写入超时。后来做了两件事XProxy 上对查询请求做并发限流最多允许 200 个并发查询。热点查询结果在 XProxy 做本地缓存TTL 设为 1 秒相同序列的查询在缓存有效期内直接返回不再打到 XStore。查询限流的上线非常重要不然系统虽然扛得住写入也会被几个任性的查询拖垮。6.3 扩容不是把节点加进去就够了我最初以为 OpenTeleDB 扩容很简单多加一个 XStore 节点路由表自动更新数据自动均衡。实际做的时候才发现路由表更新后新分片开始接收新数据但老数据还留在旧节点查询时如果跨多个分片范围聚合会多一次跨节点读取。新节点加入后元数据节点会开始做分片迁移迁移过程需要占用网络带宽和 IO。如果迁移任务同时启动太多正在写入的数据会受到影响。我的做法是每次只新增一个节点分片迁移并发数限制为 1迁移完成后观察 10 分钟确认写入 P99 时延没有明显抖动再继续下一步。虽然慢一点但全程无感比一次性迁移 10 个分片导致写入中断几个小时要省心得多。6.4 监控别只盯吞吐要盯队列长度吞吐指标很容易骗人。有一段时间我们的 XProxy 连接数和写入吞吐看起来都很正常但用户反馈部分设备的数据延迟严重。后来查监控才发现问题出在部分后端连接的发送队列上因为某个 XStore 节点所在的磁盘掉速写入请求积压在这条连接对应的队列里队列满了之后请求开始超时。整体吞吐依然是平均水平但切到按连接维度看队列长度时问题一目了然。所以部署这套架构时建议至少监控以下指标XProxy 每个后端连接的队列长度和耗时时长。每个 XStore 节点的 MemTable 占用和刷盘耗时。WAL 盘和磁盘的 IO 等待时延。路由表版本和更新次数频繁更新说明集群处于不稳定状态。这些指标比单纯的吞吐更能反映系统健康度。我后续做容量评估时也都是优先看这些细粒度指标而不是总吞吐不然后知后觉是常态。
返回列表