尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

5万骑手每5秒上报一次坐标,系统会被写死吗?分布式轨迹链路全拆解

5万骑手每5秒上报一次坐标,系统会被写死吗?分布式轨迹链路全拆解 我第一次看到一个实时轨迹大屏时满屏的红色坐标点每几秒跳一次旁边同事脱口而出的问题就是这5万个点一直写数据库不会被写死吗这个问题很典型做LBS定位上报、轨迹追踪、外卖配送调度的工程师几乎都被问过。今天就来彻底拆一遍“5万骑手每5秒上报一次坐标系统会不会被写死”这件事。这个场景折算下来是每秒1万次写入一天接近1亿条数据一个月累计几十亿行单机MySQL肯定扛不住但放到一条设计合理的分布式链路里这个量级其实非常常规。真正把系统拖垮的往往不是写入本身而是写入之后那些基于坐标做的查询、围栏判断和调度计算。这篇文章会把整条链路每一步怎么设计、为什么这么设计、哪些地方最容易踩坑一次讲透。适合正在做定位上报、轨迹追踪、地图调度系统的同学参考也适合被老板一句“5万骑手会不会写死”问住的人直接拿去当答案。1. 先把账算明白5万骑手到底在制造多大压力1.1 每秒1万次写入这是一个“常规”量级先把数字算清楚。5万骑手每5秒上报一次就是每秒1万次写入也就是1万QPS。这个数字往上换算一分钟60万次一小时3600万次。按骑手一天工作12小时算一天就是4.3亿次不对5万骑手同时在线每5秒一次一天24小时持续上报那就是每天8640万条。算上骑手只在白天跑单实际大约也是这个量级。一个月就是25.9亿条。这个体量听起来很吓人但它真的不算极端高并发。很多人一听到“每秒1万次写入”就觉得天塌了其实在互联网公司里一个中等规模的直播弹幕、秒杀预约、日志采集系统都能轻松超过这个量级。问题从来不在于数据“多”而在于你的存储和写入链路有没有为这个量级做设计。MySQL单机每秒能扛的写入大概在2000到5000条用的是普通SSD还得分主从和索引开销所以5万骑手直接把数据往一张单表里怼那是真的会把你怼到锁等待、慢查询、磁盘IO拉满最后从“系统变慢”到“写不进去”。但只要加一层削峰加批量写加合理的分片1万QPS的写入对系统来说是很轻松的。需要强调一个容易忽视的换算每秒1万次是峰值不是平均值。骑手的上报分布并不均匀早高峰、晚高峰、午高峰出现的瞬间并发可能是平均值的数倍。如果我们把消息体算成200字节左右的JSON每秒1万次就是每秒2MB的原始数据一天接近170GB按一年算就是60多TB。原始坐标数据非常占空间这个存储压力比写入QPS更值得提前规划。1.2 比“写”更怕的是“写完还要算”回到那句“系统会不会被写死”绝大多数人问的是“数据库会不会崩”但我在实际项目里看到的真正把系统拖惨的从来不是写入本身而是写入后的衍生计算。举个例子每秒1万条坐标进来你要不要给运维大屏实时展示要不要给每个骑手算当前位置附近的订单要不要在骑手进入某个商圈时触发一次推送要不要把每个轨迹点都回放给用户端看每多一个“实时计算”就相当于把每秒1万条数据再乘以好几遍。写一条INSERT可能只需要1毫秒但如果你对每条数据都做一次地理围栏多边形判断、再做一次附近骑手空间查询那就不是1毫秒能解决的问题了。很多时候系统的瓶颈不在数据库写入而在这些“读后计算”。这也解释了为什么热词里会有大量“坐标转换”“天地图坐标拾取”“WGS1984和CGCS2000坐标对不上”之类的搜索——坐标数据处理绝不是“插入一条记录”那么简单。进入存储之前你得先搞清楚坐标是什么体系转换成什么体系这些转换动作本身也在消耗CPU。如果不做预处理把这些脏活全留给下游的查询链路那“写死”的不是数据库是整个服务集群。2. 上报链路的分层设计从手机到数据库要过几道门2.1 端侧5秒上报只是理想值别把系统建立在“准点”上很多系统的设计文档里写着“骑手每5秒上报一次”但真正到了手机端这个5秒是守不住的。GPS定位本身需要时间冷启动时可能几十秒都定不到位iOS后台定位有系统级限制App进后台后频率会被系统拉长到几分钟一次安卓各厂商的省电策略会把后台网络和定位掐掉。我见过一个App在部分机型上后台上报间隔从5秒变成3到7分钟这不是程序员能控制的事。所以做后端架构时第一个原则就是不能假设上报严格按固定周期到达。5秒只能作为平均值、作为容量估算的基础而不是作为一个硬约束。上报接口必须容忍一分钟前还在线、此时突然连续补报几条数据的场景。我踩过一个坑消费端按“每条记录间隔必须大于5秒”做去重结果骑手过隧道后一次性补了一串坐标把正常轨迹点全给过滤成“无意义数据”最后轨迹在图上断成好几截。端侧上报还需要区分运动状态。骑手静止等单时还强制5秒上报一次纯粹是浪费流量和电量。很多App会做“静止时不报、移动时按5秒报、高速移动时缩短到3秒”的动态策略这些逻辑如果做在后端根本来不及反应必须先做在端上。后端能做的是在接口里接收一个speed字段判断数据是否合理比如一个坐标点比上一个点5秒内移动了2公里那基本可以判定是GPS漂移该过滤就过滤而不是直接写进库里当真实轨迹。2.2 接入层与消息队列先把数据接住再慢慢落地骑手上报的场景是典型的“写多读少”且每次写入量不大但频率高。接入层设计我的经验是不要在这个环节做太多业务逻辑。鉴权、限流、参数校验、坐标基础校验做完了就赶紧把数据扔进消息队列让上游快速返回成功。上报接口对实时性的要求是“别丢”而不是“立刻进库”。架构上手机端经HTTP或TCP长连接打到接入层接入层后面挂Kafka或RocketMQ。为什么一定要加MQ最直接的原因是削峰。每秒1万次上报如果用数据库直写数据库要承受全部流量加了MQ之后数据库的消费速度可以自己控制晚高峰瞬时冲到每秒3万、4万次时MQ先扛住消息在队列里积压几十秒钟完全不影响业务数据库依旧按自己的节奏批量消费。Kafka在这种吞吐量下单分区每秒处理几万条毫无压力几十个分区轻松应对10万QPS级别。消费端还有一个容易被忽视的优化批量写。逐条INSERT的性能很差因为每条都要走一次SQL解析、事务提交、索引更新、刷盘。正确的做法是消费端攒够100条或者累计500毫秒再拼一个批量INSERT语句或者用小事务一次提交。同样是1万条数据批量写和逐条写的性能差距可以到5到10倍。我第一次优化这个场景时就靠批量写这一个改动把数据库写入耗时降了80%磁盘IO也明显平缓了。3. 存储选型与数据模型怎么存才不会被几十亿条数据拖垮3.1 存储选型对比单库、分库分表、TiDB、时序库怎么选写完这篇热词分析之前我先说一下结论5万骑手这个量级用单机MySQL硬扛是不明智的但也不至于一上来就上特别重的分布式数据库。选型要根据团队维护能力、已有基础、查询特点来决定。下面是我在实际项目里对比过几种方案的真实体会。方案写入能力轨迹查询运维复杂度适合场景MySQL单库约2000-5000 QPS简单但数据量大后很吃力低几千骑手以内数据量千万级MySQL分库分表数万QPS需带分片键查询中高要自己做迁移和聚合万级设备亿级到百亿级数据TiDB / OceanBase数万到十万级QPS弹性扩展SQL兼容好中但部署依赖较多数据量增长快不想自己运维分库分表的团队ClickHouse极高写入吞吐聚合查询极快但单点更新弱中需单独维护纯轨迹存储、分析型查询、报表统计TimescaleDB / TDengine高写入自动分区时间维度查询好用低到中IoT定位轨迹按时间范围查询为主我的经验是如果团队只是要做“存下来、能回放、能算距离”优先考虑MySQL分库分表或TiDB。如果还要做大量轨迹分析、区域统计、骑行距离聚合ClickHouse的压缩比和并行查询优势会明显拉开差距。不要一上来就追求极致技术栈运维一个分布式数据库的成本往往比流量本身更大。数据量在几十亿条这个规模时MySQL分库分表完全可以稳定跑住关键是分片策略要设计好。3.2 数据模型与分片策略轨迹表该长什么样坐标上报表的核心字段我建议这样设计CREATE TABLE rider_location_202501 ( id BIGINT AUTO_INCREMENT, rider_id BIGINT NOT NULL, order_id BIGINT DEFAULT NULL, lon DECIMAL(10,6) NOT NULL, lat DECIMAL(10,6) NOT NULL, coord_type TINYINT NOT NULL DEFAULT 1 COMMENT 1WGS84, 2GCJ02, 3CGCS2000, speed INT DEFAULT NULL COMMENT 速度单位km/h, direction SMALLINT DEFAULT NULL COMMENT 方向角单位度, report_time DATETIME NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id, report_time), KEY idx_rider_time (rider_id, report_time) ) PARTITION BY RANGE (TO_DAYS(report_time));这张表从设计上就回答了两个问题按时间分区因为轨迹数据天然按时间维度过期按骑手ID建索引因为绝大多数查询都是“查某个骑手某段时间的轨迹”。分库分表时核心是分片键。我强烈建议分片键用rider_id的哈希而不是按时间分库。原因是同一条轨迹的所有点必须落在同一个物理分区否则你查一个骑手一天的轨迹时要跨遍所有库再合并慢到想骂人。按rider_id哈希分32或64个库每个库内再按时间分月表查询时只要带上骑手ID精准定位到某个分片痛快很多。坐标字段必须用定点数选择DECIMAL(10,6)不要用FLOAT或DOUBLE。这是我在项目里踩过的很疼的坑FLOAT精度不够经纬度在小数点后第6位开始出现误差导致轨迹回放时点会在原地抖动甚至看起来像在马路两侧乱跳。DECIMAL(10,6)可以精确到0.1米级别存储上多几个字节但换来的是定位可靠值得。冷热分离也是一个必须做的事。近30天的热数据放高性能存储用于实时查询和调度超过30天的历史数据我不建议一直留在主库里一是占空间二是让索引越来越大、查询越来越慢。可以写一个定时任务把上个月的数据按天做分区导出压缩后放到对象存储或归档表里。热词里有很多“坐标转换”“天地图坐标拾取工具”的搜索从侧面说明很多人还在手工处理坐标体系但真正的工业化做法是把坐标转换做成一个实时的、幂等的预处理动作而不是靠人工去查工具。4. 实时坐标消费与轨迹计算写入之后的另一场硬仗4.1 轨迹查询为什么贵以及怎么让轨迹“画得快”如果系统只做“存坐标”那问题简单很多。但实际业务一定要查轨迹用户端要看骑手到哪了运营要看某骑手今天跑了多少公里客服要回放异常订单的完整路径。一次轨迹查询比如查某个骑手最近2小时的轨迹按5秒一个点算就是1440条记录数据库按骑手ID和时间索引去扫数据量不大时很轻松。但难的是高并发如果一个区域内有几千个用户同时刷新查看骑手位置等于每秒产生几千次轨迹查询请求每次都扫1440条记录如果不做优化数据库的CPU和IO会在瞬间被打满。解决办法是预聚合和抽稀。把一个骑手5分钟内的轨迹点聚合成一条折线存成一条聚合记录前端展示时直接读这条记录放大后才去请求原始点。这样查询量直接缩小几十倍。抽稀算法可以用道格拉斯-普克算法把轨迹上“近似直线”的中间点去掉保留拐点。我在项目里用这个方案后轨迹列表接口的响应时间从原来的800毫秒降到了100毫秒以内效果非常明显。另一个技巧是“最新坐标缓存”。骑手当前实时位置不应该每次都查数据库而应该在上报时同时写Redis用骑手ID做key存经纬度和时间。用户端看骑手当前位置走Redis毫秒级返回历史轨迹才查数据库。这套组合拳下来数据库读压力会小很多。我见过有团队让所有用户端的定位刷新请求都直接打到MySQL上结果5万骑手还没上线数据库先挂了。4.2 围栏判断与坐标体系每次判断都要精益求精骑手调度的经典需求是判断“骑手是否进入了某个商圈或配送范围”。如果每秒1万条坐标每条都要和几千个围栏做比对那计算量就爆炸了。最常见的优化是用GeoHash把坐标映射成网格先粗筛可能命中的围栏再对候选围栏做精确的射线法多边形判断。GeoHash第七位精度大约100米级别用来做“粗筛”刚好命中网格后再对落选的点做精细计算能过滤掉90%以上的无效判断。围栏判断还涉及一个容易被忽略的数据质量问题坐标体系不统一。WGS84是GPS原始坐标GCJ02是国测局加密坐标CGCS2000是国家大地坐标系。同一地点用WGS84和GCJ02表示的经纬度会差几百米。如果你在端上用的是高德或天地图SDK拿到的坐标是GCJ02但数据库里存的是GPS原始WGS84那围栏判断、距离计算全都会偏得离谱。很多人在热词里搜“WGS1984和CGCS2000坐标对不上”“天地图坐标拾取工具”其实就是吃了坐标体系不统一的亏。我在项目里处理这个问题采用的是“入库时统一转基准”的原则所有客户端上报的坐标在进入消息队列之前先经过一个坐标转换服务统一转成WGS84或者根据业务使用场景统一存成GCJ02。存库的坐标不再混用体系后续所有计算都在统一坐标系下做。这里要注意的是坐标转换有标准算法如GCJ02转WGS84的纠偏公式网上能找到实现但必须实测校验尤其是跨城市边界时一个错误的转换会让骑手位置偏到隔壁街区。5. 常见问题与排查实录这些坑我替你先踩过了5.1 GPS漂移、坐标系混存和数据质量陷阱写坐标系统最头疼的不是高并发而是数据看起来“写进去了但位置是错的”。GPS在城市峡谷里漂移非常频繁骑手在立交桥下、高楼密集区、隧道里定位点会突然跳到几百米外再跳回来。如果这些漂移点直接入库用户端刷新位置时看到的骑手就会在地图上瞬移体验极差。这个问题我的处理方案分两层端侧做简单的滤波比如速度超过120km/h的点直接判定为无效因为骑电动车不可能跑这么快后端再做一次轨迹平滑相邻两个点之间的距离除以时间超过物理上限就丢弃或修正。还有一次我们排查“某区域骑手位置全偏了300米”的问题折腾了两天才发现是部分Android终端上报的是GCJ02部分iOS终端上报的是WGS84两拨数据混存进了一张表。前端地图用的是GCJ02所以WGS84那部分点位全偏。从那以后我定了一条死规矩入库前必须有一个统一的“坐标坐标系”字段并且用转换服务强制统一。调试时也不要手动临时转坐标要形成工具嵌入CI每次代码变更自动校验。5.2 热点区域、消费积压与容量评估5万骑手上报还有一个很典型的坑骑手分布极不均匀。晚高峰时几十个骑手可能集中在同一个商圈而其他区域人很少。按骑手ID哈希分库能解决写入分散但区域热点的消费和计算还是会集中在某几个服务实例上。我遇到过一次某个区域围栏计算量大而那个围栏判断服务只有一个实例Kafka里消费积压了几百万条消息骑手位置更新延迟到了十几分钟用户端看到的全是旧位置。排查时最先看的就是消费积压指标。Kafka的Lag监控一定要做消费延迟超过30秒就要告警。解决这个问题的思路有两个一是把围栏判断从同步链路改成异步批量计算攒1000个点一起算不要来一个算一个二是把区域分片比如按GeoHash前缀把不同区域的计算分发到不同实例避免一个热点区域打爆一台机器。扩容不是唯一解法把“实时单点计算”拆成“准实时批量计算”往往更有效。容量评估也要提前做。我的习惯是做一个简单的推算表骑手数、上报间隔、峰值倍数、消息体大小、日均写入量、存储保留周期把这几个数填进去就能算出需要多少个Kafka分区、多少张分表、多大的SSD空间。5万骑手、每5秒、200字节、保留30天热数据大约需要5TB左右的在线存储加一倍索引空间。这个数字要在系统上线前就确认好别等数据把磁盘塞满的时候再扩容那时候扩都来不及。还有一个我强调过无数次的教训验证系统能不能扛住别只用工具模拟均匀流量。要拿真实的轨迹回放数据压测因为真实数据里包含漂移点、重复点、瞬时高密集区域的突发。模拟数据均匀分布时系统一切正常一上真实流量就超时这种事故我见过不止一次。压测时一定要把“局部密集”这个因素加进去比如设定一个商圈范围内突然涌入2000个骑手同时上报看系统还能不能平稳吞下。最后再分享一点个人体会。我做了几年定位系统后最大的感受是不要把“实时写入”当成唯一的目标更不要追求每一秒的坐标都完美。真正成熟的设计是想清楚哪些数据必须实时、哪些可以延迟、哪些可以允许丢失。骑手的轨迹数据最新位置走缓存要实时历史轨迹走聚合和归档要可靠而某些中间态的原始点丢了也没关系。把这个优先级排清楚5万骑手这个量级就一点都不吓人了。
返回列表