尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

5万骑手每5秒上报GPS:高并发坐标写入的扛住姿势

发布时间:2026/9/25 10:34:32

资讯中心
01
ARTICLE

5万骑手每5秒上报GPS:高并发坐标写入的扛住姿势

5万骑手每5秒上报GPS:高并发坐标写入的扛住姿势
骑手配送平台的后端同事上周问了我一个特别接地气的问题5万骑手每5秒向服务端上报一次GPS坐标系统会不会被直接“写死”我没急着回答先把账给他粗算了一遍5万除以5等于每秒1万次上报一天86400秒全天要落库8.64亿条坐标记录要是一年跑下来大概能攒出3000多亿条位置点。听到这个数字大多数人第一反应是“这数据库不得直接炸了”。但说句实话做LBS位置服务的团队里这个写入量只算中规中矩真正的关键根本不在“1万”这个数字上而在于你用什么样的姿势去接收和落库。如果后端是收到一条就INSERT一条而且用单库单表、每个字段都建索引我可以负责任地说不用等5万骑手全量上线哪怕只有5000人在跑两小时之内写入链路就会肉眼可见地卡死。反过来如果把缓冲、聚合、批量、分层这套组合拳打出来这个量级的生产系统我不止做过一套每秒几万点位写入也扛得住。这篇文章不是什么教科书讲解就是把我实测过的方案、算过的账和踩过的坑摊开聊。下面按五部分走先算清账再拆数据库写入链路为什么容易卡死接着讲最救命的三板斧缓冲、聚合、批量然后聊坐标数据特有的坑最后一节是线上运维排查实录。1. 先把账算明白这个量级到底是什么概念1.1 从“每秒1万条”推导出全链路指标很多同学对“每秒1万次写入”没有直观体感我先从数字层面把它彻底拆开。每秒写入条数50000 ÷ 5 10000条/秒也就是10K TPS级别的写入。每天写入总条数10000 × 86400 864000000条即8.64亿条/天。每月写入总条数约260亿条。如果系统跑满全年约3150亿条位置记录。光看条数已经够吓人了再看物理存储量。假设一条坐标记录包含骑手ID8字节、时间戳8字节、经度8字节、纬度8字节、速度4字节、方向4字节再算上协议头和状态字段粗略做到120字节。换算下来一天新增就是103GB左右的原始数据一年光是裸数据就接近37TB。这还不算索引、日志、副本实际落地存储至少再翻三倍。指标数值备注上报频率5秒/次全量骑手同一节奏峰值写TPS10K/s平均不算突发日写入条数8.64亿全量落库场景单条记录裸大小约120B骑手时间经纬度速度方向日增裸数据约103GB不含索引与副本年增裸数据约37TB恐怖但可控有这些数字打底后面所有选型判断都有据可依了。做架构不是拍脑袋先算出量级再决定用哪种姿势去扛。1.2 “写死”到底是哪一层先死产品经理说“系统写死了”通常是指页面转圈、接口超时、数据不更新。但从后端排查角度看得一层层拆因为“写死”可能发生在完全不同的层面。第一层是接入层。网关或者Web服务默认连接数有限1万TPS打进来连接池瞬间被占满新请求直接排队超时。这一层死掉的表现是CPU不高、数据库空闲但接口全部超时。第二层是应用层。如果是同步阻塞模型每次上报都等数据库返回再响应下一个请求线程就被写操作占住吞吐量上不去。就算数据库能扛应用自己也先把自己堵死了。第三层是数据库层。单条INSERT的提交每条都要过事务、写undo/redo日志、刷磁盘、更新索引1万条并发如果全走单条提交InnoDB的组提交能力会被打到极限更别提还有别的读写任务在抢磁盘。第四层是存储层。数据膨胀之后磁盘空间、备份窗口、冷数据清理这些问题会在几周内集中爆发。很多人前期只盯着“每秒能写多少条”结果忽略了今天写入的8.64亿条明天还要查询、还要归档、还要删。写死从来不是单一组件的问题它是一个系统性故障。所以后面讲方案时我不会只说数据库而是把接入层到存储层的整条链路都覆盖到。1.3 请求不是匀速的饭点高峰才是真杀手做骑手场景最容易犯的一个错误拿平均每秒1万次来设计容量。实际线上的请求曲线从来不是平的。早晚高峰、午餐晚餐、恶劣天气、节假日大促这些时段骑手在线数可能直接翻倍而且大家都在同一时间点触发上报瞬间写TPS可能是平时的3倍以上。比如晚高峰从1万冲到3万持续一两个小时这才是真正考验系统的时候。还有一个隐藏的“惊群”问题如果系统下发指令让骑手端统一调整上报间隔客户端会同时切到新节奏下一秒瞬时压力可能冲到平时5倍。所以做容量规划时我一般按平均值的3到5倍预留峰值水位不是拍脑袋而是所有线上系统都有这种共性流量不会匀速你的架构必须能在突发流量下“弹性处理”而不是“硬抗到底”。2. 一次坐标INSERT的完整旅程数据库为什么会卡死2.1 一次单条写入的成本解剖很多人觉得“INSERT一条数据能有多重”但要知道数据库不是只写一行就行。以MySQL InnoDB为例一条普通INSERT背后至少干了这些事解析SQL语句生成执行计划检查并获取事务和锁写入undo日志用于回滚和MVCC更新聚簇索引页如果页满了还要触发页分裂更新所有二级索引写入redo log并在提交时根据刷盘策略决定是否fsync写入binlog如果开启返回客户端成功结果。其中最贵的是刷盘。如果innodb_flush_log_at_trx_commit1每次事务提交都要把redo log刷到磁盘这一下就是一次磁盘IO。机械盘单次IO延迟大约5到10毫秒一块盘每秒最多做几百次fsync光这一项就封死了写入上限。SSD好一些但同样经不住1万次独立提交的压制。所以“每秒1万条单条INSERT”在默认配置下是妥妥的灾难。但是如果把多条INSERT合并到一个事务里提交情况立刻不同1万条分成10个事务每个事务批量写1000行磁盘刷盘次数从1万次降成10次写入能力直接提升几个数量级。这里点名一个核心结论批量是写入性能的第一生产力没有之一。2.2 索引越多写入越慢坐标上报场景还有个非常典型的“自杀式设计”为了查询方便给表建了一堆二级索引。uid索引、时间戳索引、经纬度索引、订单号索引……索引对写入的拖累是全方位的。每个索引都是一棵独立的B树插入一行数据时数据库要往每一棵索引树里都插入对应的键值。写入越频繁索引树节点分裂、页重写的次数就越多磁盘IO放大越严重。比如一条记录只有120字节但如果有6个二级索引实际写入放大可能到2KB甚至更多十倍以上的额外开销就是这么来的。我的建议非常直白写入热点表只保留最必要的索引能砍就砍。高频坐标写入表主键都建议用自增ID或雪花ID的聚簇结构时间字段如果想查也有办法后面讲分区时再说。业务查询优先走旁路不要直接拿热表去建花式索引。2.3 单表单库 vs 分批提交一张对比表说清差距我实际压测过一个简化场景同一台机器同样1万条坐标数据两种写法的差距大得离谱写入方式耗时说明单条循环INSERT且每次提交约30秒以上1万次独立事务全在等待刷盘单连接批量INSERT1000条/批约1.5秒只有10次事务提交效果完全不同并发10个连接批量写入每批1000条约0.3秒综合连接并发和批量提交接近压测上限注意这不是什么黑魔法就是最基础的批量提交适度并发。很多团队连这一步都没做就直接给系统判了“死刑”实在可惜。先把这项做到位后面无论选什么中间件压力都能小一大半。3. 扛住每秒上万次坐标写入的三板斧3.1 第一板斧消息队列削峰如果对可靠性有要求建议在应用和数据库之间加一层消息队列比如Kafka、RocketMQ或者Redis Stream。为什么要加这层两个原因。第一个是削峰填谷。早晚高峰的瞬时流量是平均值的几倍如果写入路径直连数据库数据库必须按峰值去买单。引入队列后写入端把消息投递到队列就返回消费端按照自己能力匀速拉取流量被削平了数据库承受的是平滑压力而不是洪峰。第二个是解耦和重试。消息在队列里可以留着消费端挂了重启后还能消费不会丢数据。这一点在骑手坐标场景至关重要坐标记录虽然单个价值不高但整体轨迹一旦断档后续复盘就无从谈起。不过要提醒一句队列不是银弹如果消费端本身没做批量队列只会把压力后移。我之前见过加了Kafka但消费端还是一条条INSERT的架构结果只是把“死”的姿势从接口超时变成了Kafka消费延迟。队列给了你缓冲空间但消费端的落地姿势同样要优化。3.2 第二板斧缓冲聚合把1万次压成几十次消息队列解决了削峰但真正能让数据库轻松的是消费端的批量落地。核心思路很简单消费端攒够一定批次再一次性写入。假设消费端每5秒聚合一次这一秒收到的1万条坐标数据落库时不是1万次INSERT而是大概10批批量INSERT每批1000行左右。加上批量提交事务后数据库的IO次数直接从每秒1万次降到每秒一二十次。这个量级即便是普通MySQL实例也能轻松吃下。写SQL时要注意一点批量INSERT的SQL不能无脑拼接。单条SQL过长会超过max_allowed_packet限制还容易让MySQL优化器犯愁。一般控制单批1000到2000行以不超过2MB为宜。批量写入示例大概是这样的形式INSERT INTO rider_position (rider_id, ts, lng, lat, speed, direction) VALUES (10001, 2024-06-01 18:00:01, 116.397, 39.908, 12.5, 90), (10002, 2024-06-01 18:00:01, 116.401, 39.905, 10.1, 85), ...每批只执行一次INSERT事务提交一次。实测下来1万条数据从“收一条写一条”的30秒降到“攒批再写”的1秒出头这个优化能顶十条服务器扩容。3.3 第三板斧内存本地缓冲与服务端兜底如果上报端本身也是我们可控的客户端还有一个常用手段SDK或网关层做本地缓冲。比如骑手的App端每5秒定位一次如果网络抖动请求失败就丢数据这肯定不行。更稳的姿势是客户端先把点位写到本地内存或小型嵌入式库定期批量上报或者服务端网关在进程内存里攒一批数据达到阈值或固定间隔后再批量转发。服务端Go语言写的网关用channel加定时器就能实现一个非常轻的聚合器buf : make([]Position, 0, 1000) ticker : time.NewTicker(5 * time.Second) for { select { case p : -positionCh: buf append(buf, p) if len(buf) 1000 { flush(buf) buf buf[:0] } case -ticker.C: if len(buf) 0 { flush(buf) buf buf[:0] } } }注意本地缓冲是有代价的。进程如果突然崩溃内存里没有落库的这批点位会丢。所以关键轨迹能不能丢取决于业务容忍度。骑手实时位置这种事丢几秒还能接受但如果涉及结算、里程核算就必须用持久化消息队列兜底了。设计时把这个容忍度搞清楚别为了省设备把钱省丢了。4. 坐标数据特有的坑格式、精度、空间查询4.1 坐标格式不统一地图上会“漂移”坐标数据的第一个大坑不是性能而是“坐标对不上”。很多人以为GPS定位拿到的经纬度都是同一套标准实际完全不是。常见的坐标系有WGS-84国际通用GPS标准、GCJ-02国内地图厂商加密后的坐标系、CGCS2000测绘领域使用的大地坐标系等。不同坐标系之间的同一地点经纬度可能差几十米典型的是GCJ-02和WGS-84之间有非线性偏移。如果采集端拿的是原始GPS坐标存储端却按另一个坐标系的数据渲染地图上骑手位置就会整体偏出去几条街。我踩过的真实案例是骑手端上报WGS-84坐标后台直接存了后来做热力图分析的时候调用地图API渲染所有点位往东南方向偏移了两百多米。排查了很久才发现是坐标系混用。解决思路也简单在接入层统一坐标标准所有外部进来的坐标都转成唯一约定坐标系再落库。转换公式是公开的但要注意转换不是恒定的精度要求高时得用格网参数做局部校正。一个原则宁可在写入前多算一次也别在查询时整表重算。4.2 轨迹不是每条都要过滤合并与压缩再回到性能问题。刚才说每天8.64亿条但这个数字是“全量存储”的结果。真的有必要把每个5秒点位都存下来吗骑手App实时展示轨迹确实需要每5秒一个点否则动画会卡。但这不代表后端必须存下每一个点。更合理的方案是分层存储实时热层只保留最近15分钟的高频点位用于地图实时渲染离线轨迹层按距离阈值或时间阈值抽稀比如位移超过20米或间隔超过30秒才保留一个点长期分析层只保留每天、每订单的汇总轨迹。抽稀算法可以用最简单的反距离阈值法也能用更精细的Douglas-Peucker变体。经过这一步需要长期落库的数据量能降到原来的五分之一甚至十分之一。存储成本直降查询速度反升这是整个方案里“性价比”最高的一环。4.3 空间索引与分桶让轨迹可查、可分析存储存好了后面还有查询需求比如“某商圈附近当前有哪些骑手”“某位骑手今天的行驶轨迹”。如果不做任何处理这就是全表扫描一万条也许没问题几千万条就是灾难。常规做法是给表加网格字段。比如用geohash或者H3六边形格网把经纬度映射成一个短字符串或整数写入时顺带计算查询“附近”就可以先按格网过滤再精算距离。格网层次选多少位合适取决于业务半径。骑手场景一般选6到8位的geohash每格几公里到几百米的粒度查询成本可以降几个数量级。另外如果查询维度主要是“按骑手时间”分区策略可以按骑手ID哈希加时间分桶这样单次查询只扫一个分区而不是全表。5. 线上踩坑实录从连接池打满到主从延迟5.1 连接池打满但CPU还很低真实线上第一个经典现场接口开始大面积超时数据库CPU只有30%但应用日志里全是“connection pool exhausted”。这种局面的本质是数据库单次写入慢连接一直被占着不放。假设每个写事务要等50ms一个连接每秒只能处理20个事务100个连接也就能扛每秒2000个请求。骑手上报量一上来连接池立刻见底新请求全在排队看起来就像“数据库写死”了。排查方法看数据库的Threads_connected、Innodb_row_lock_current_waits、慢查询日志和活跃会话数。如果Threads_connected接近上限而CPU不高基本可以断定是写入路径效率问题不是机器不行。我建议连接池别盲目调大。连接数越多上下文切换越严重数据库内部锁竞争越激烈反而更慢。更合理的方案是先优化SQL和批量策略把单次写入时间压到毫秒级再把连接池容量控制在几十个这个思路能解决90%的连接池打满问题。5.2 主从延迟引发连锁反应另一个高频事故是主从延迟。这种量级的写入从库的同步容易滞后。一旦主库写入持续高压从库可能落后主库几十秒甚至几分钟。业务侧的读取如果一直走从库就会看到“骑手位置几分钟不更新”的诡异现象。我们的处理思路有几个层面实时位置这类高一致性需求查主库或查缓存不要依赖从库历史轨迹查询走批量导入的OLAP分析库不占用从库压力从库的硬件配置必须跟主库对齐尤其在磁盘性能上否则同步延迟分分钟出现。5.3 数据膨胀与历史清理最后一个长期问题也是最容易被忽视的存储终归会爆炸。日增103GB裸数据就算抽稀后降到20GB三个月也有近2TB。如果不做分区和数据生命周期管理任何数据库都撑不住。我用的是最朴素也最有效的按时间分区。一张坐标表按天或按周做RANGE分区写入和查询都只命中当前分区而且清理历史数据时直接DROP PARTITION几秒就能删一天的数据比DELETE硬删高效太多。ALTER TABLE rider_position PARTITION BY RANGE (TO_DAYS(ts)) ( PARTITION p20240601 VALUES LESS THAN (TO_DAYS(2024-06-02)), PARTITION p20240602 VALUES LESS THAN (TO_DAYS(2024-06-03)), ... );归档策略上我会把超过30天的高频点位迁移到冷存储或分析型引擎在线表只保留近30天数据。这样表体积长期可控写入和查询性能都稳得住。说点实际的回到开头那个问题5万骑手每5秒上报一次坐标系统到底会不会被“写死”以我做这类系统的经验会写死的不是“每秒1万”这个量级而是那种“收到一条写一条、单库单表可劲建索引、完全没有批处理和缓冲”的写法。只要把批量落地、消息队列削峰、坐标抽稀、分区清理这几个动作做到位这个体量的系统放在普通MySQL上都能稳稳跑住根本不需要一开始就上毫无必要的大型分布式架构。如果让我给你一个行动清单就四件事先算账再压请求然后改批量最后做分区。别一上来就追逐花哨的技术栈先把这些基础功补上比什么都实在。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。