ARTICLE DETAIL

资讯详情

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

高并发写入架构解析:5万骑手坐标上报系统如何设计

高并发写入架构解析:5万骑手坐标上报系统如何设计 5万骑手每5秒上报一次坐标系统会被“写死”吗这个问题我太熟了几乎每次聊到位置服务、物流大屏、外卖调度都会有人问同样的话。先把结论放在这如果设计成“来一条写一条数据库”别说是5万骑手每5秒一次哪怕只有5000骑手MySQL都撑不了多久。但换一套合理的架构这个量级不仅不会把系统写死甚至连汗都不会出。先把账算清楚。5万骑手、每5秒一次坐标意味着每秒涌入1万条位置记录。高峰期骑手集中在午晚两个时段在线率接近九成实际每秒写入量在9000到1万之间如果某段路网密集、骑手集中瞬时峰值冲到每秒2万也不是不可能。1万条/秒的写入单机MySQL用默认配置直接硬扛大概率扛不过30分钟。要是每条记录还带着GPS精度、电量、订单状态、方向角这些附加字段再配上两个索引那连10分钟都难说。但“会写死”这个说法本质上是在问这个写入量级能不能被架构消化掉。本文就按这个场景从流量测算、链路设计、存储选型、优化手段、高可用兜底、压测验收六个角度把“5万骑手坐标上报”这套系统从头到脚拆一遍。适合正在做位置服务、车联网、物流调度或者单纯想搞明白“高并发写入到底怎么扛”的朋友参考。1. 先算账5万骑手每5秒一次压力到底多大1.1 流量测算从“每秒1万条”说起很多人的第一反应是“每秒1万条好像也不是很多”。这个直觉不算错但要看这个1万是怎么来的。5万骑手除以5秒正好1万。可这是平均值的理想算法真实业务里有两个放大因素。第一个放大因素是时段效应。骑手的活跃时间高度集中在10:30-13:30、17:00-20:00两个饭点窗口非饭点时段大量骑手在休息、等单上报频率会明显下降。也就是说系统在饭点承受的写入压力可能是平均值的1.5到2倍真实计算要按峰值TPS来设计而不是平均值。假设峰值系数取1.8那就是1.8万条/秒不是1万。第二个放大因素是协议开销。一条坐标JSON消息带着经纬度、时间戳、骑手ID、订单号、速度、方向、电量这些字段至少有300字节加上TCP/IP层封装能到500字节。每秒1万条乘以500字节只是5MB/s的带宽网络层面毫无压力真正吃紧的是数据库的写入能力。这跟“带宽不够”完全是两个维度的事很多人一开始分不清以为带宽够就能扛住结果把数据库压垮了。注意评估写入系统永远按峰值TPS算不按平均值算。平均值会欺骗你峰值才会暴露问题。1.2 真正的挑战不只是并发是“持续写入”加“热点聚集”如果只是单纯的高并发其实有很多成熟方案。位置上报这个场景最独特的地方在于两点。第一它是持续性的写入不是脉冲式的。电商秒杀虽然峰值更高但持续几秒钟就结束了后续只需要慢慢消化队列。骑手坐标是全天候、全天、全时段都在写非饭点也停不下来MySQL想喘息都没有机会。这就像高速路收费站秒杀是瞬间挤进来一列车而位置上报是不断有车流通过后者的长期压力更大。第二骑手位置天然带热点聚集属性。城市里主要商圈、写字楼、餐饮聚集区的骑手密度是郊区的几十倍。对应的数据库分片如果按骑手ID哈希分布看起来均匀但按区域维度去看热点区域的行被不停更新部分分片承载了远超平均水平的热度。上一秒所有骑手还在同一个片区内移动下一秒就可能集体跨片区调度数据分布的热度是不均匀且动态变化的。这两点叠加起来系统的真实核心矛盾就是如何让一条高频持续写入链路在热点动态转移的情况下依然保持平稳的写入水位不要因为某一个环节的拥塞把整条链路堵死。2. 架构设计从源头把压力分流2.1 接入链路网关限流、鉴权、协议压缩架构的第一步不是选数据库而是先把接入链路想清楚。骑手的APP或车载终端通过4G/5G网络长连到接入网关这一步至少要承担三件事鉴权、限流、协议解析。鉴权不难理解每个骑手终端携带token网关层先做合法性校验避免无效请求打进数据库。限流则要精确到“每骑手每5秒一次”这个粒度防止某个终端因为网络重试、APP bug、异常循环把单个设备的流量放大几十倍。实际项目中我见过一个压测脚本忘了设间隔一台测试机每秒打了几百次请求进来直接把网关后的服务拖垮的情况。限流策略要按维度拆开全局限流、单设备限流、单区域限流三层叠加。协议压缩是容易被忽视的一环。如果用JSON明文传输一条位置消息300字节改用Protobuf或MessagePack之后能压缩到60-80字节。不要小看这个压缩它直接把内网带宽压力降低了一个数量级Kafka和数据库接收端的解析成本也同步下降。实测里同样一套链路切到Protobuf之后整体吞吐能提升40%以上。接入网关的线程模型也很关键。不要用“一个请求一个线程”的同步模型1万TPS下线程数会暴涨上下文切换开销直接吃光CPU。用Netty这种基于Reactor模式的异步模型连接数和线程数是解耦的几万个长连接只需要几十个IO线程就能扛住。2.2 消息队列削峰填谷的核心接入层之后下一步是消息队列。这是整个架构里“防写死”的关键枢纽。位置数据流的特征决定了它非常适合引入队列允许一定的延迟不需要严格事务数据量大但对单个消息的可靠性要求没那么敏感。骑手坐标丢失几条在大屏上根本看不出来轨迹回放还可以用前后点插值补上。这意味着我们可以在“来一条处理一条”和“积攒一批再处理”之间做文章。常见的选型是Kafka、RocketMQ或Pulsar。Kafka的吞吐和生态成熟度最高单分区顺序写盘的能力非常强几万TPS完全不在话下。RocketMQ在事务和消息可靠性上更稳但吞吐略逊。Pulsar的存算分离架构适合超大规模但对中小团队来说运维成本偏高。对于“5万骑手上报”这个场景Kafka是最稳妥的选择Topic按骑手ID做key分区既保证了同一骑手轨迹的顺序性又天然完成了数据分片。引入队列之后数据库的写入就从“跟随业务峰值”变成了“跟随消费能力”。业务高峰期Kafka先吞下所有流量消费端按自己的节奏慢慢写库这就是削峰填谷。数据库再也不会被瞬时洪峰冲垮这就是系统“写不死”的第一层保障。提示队列积压量要设监控。平时可能只有几千条堆积一旦下游存储故障堆积会瞬间涨到几百万条。积压是缓冲但积压无限增长说明下游已经出问题了必须告警。2.3 存储选型为什么不能只用MySQL这一段是全文最核心的部分直接回答“会不会被写死”的存储层面答案。如果按传统思路把5万骑手的坐标直接insert到MySQL的轨迹表每秒1万条insert单机MySQL的极限大概在3000-5000TPS带2-3个索引的情况下。再加上每5秒一条数据量按天增长一个月就是2.6亿条表体积大到一定程度之后连SELECT都开始变慢这时候就真的“写死”了。正解是用时序数据库来处理位置流。TDengine国内常用专门为时序数据设计写入吞吐极高单机轻松到10万TPS以上还内置了按时间分片、自动过期、降采样聚合能力。对轨迹数据这种自带时间戳的写入天然友好。InfluxDB生态成熟查询语法丰富但集群版成本高单机性能略逊于TDengine适合中小规模。IoTDB在工业物联网领域用得多支持复杂时序语义但社区和应用面相对窄。ClickHouse严格来说是OLAP分析引擎但列式存储批量写入的能力非常强作为轨迹查询层很合适缺点是并发更新能力弱不适合做高并发单条写入。个人建议的组合方式写入链路用TDengine或InfluxDB轨迹回放和分析场景用ClickHouse。写入库负责实时落盘、最新位置查询分析库负责历史轨迹聚合、热力图、调度分析两者通过消费Kafka各取所需互不干扰。为什么要拆两套因为“写入最新坐标”和“分析历史轨迹”是两种完全不同的访问模式。前者频繁写、少量读后者大量读、频繁聚合。让一套存储同时扛两种模式要么牺牲写入性能要么牺牲查询性能拆开之后各自的压力都被释放了。3. 核心优化细节把写入成本压下来的几个关键招3.1 批量写入攒一批、写一批即使选好了时序数据库写入方式也不是随便写的。每来一条坐标就一条insert哪怕数据库吞吐够也依然会有大量连接切换和日志刷盘开销。更稳的做法是批量写。以TDengine为例单条写入和批量写入的吞吐差距是数量级的。把Kafka消费到的数据先攒在内存里等攒够500条或者积压200毫秒再一次性写入能让数据库的写入吞吐轻松提升5-10倍。这跟“过收费站”一个道理每辆车单独停下来交费通行效率极低按车队批量放行效率完全不一样。批量大小的选择要观察。太小效果不明显太大内存占用和单次失败重放的成本都会上升。500条一个批次是常见的经验值实测效果稳定。3.2 数据分片按时间加骑手ID双维度打散不管用哪种存储数据都要做分片。位置数据有两个天然的分片维度时间和骑手。按时间分片是时序数据库的标配能力比如按天分区每天的轨迹数据落到独立分区。好处是过期清理可以直接删分区而不是逐条DELETE一天的数据删起来也就是一条内部命令的事。满30天的冷分区直接drop比任何DELETE语句都快几个数量级。按骑手ID分片则是为了均衡写入热度。Kafka按骑手ID作为key分区消费者按分区消费每个分区对应下游存储的一个子表或虚拟分区。这样同一个骑手的轨迹是连续写到一个分片里的查轨迹回放时只需要定位一个分片不需要跨全表扫描。注意分片键的选择千万别用“区域”或“商圈”。骑手在物理上高度聚集按区域分片会让热点区域的分片被打满而冷区分片空空如也数据分布完全不均匀。按骑手ID这种业务上随机均匀的字段分片才是均衡的打散策略。3.3 过期数据清理别让“写”被“读”拖死很多人只关注写入忽略了存储膨胀带来的连带问题。一个月2.6亿条轨迹数据如果不做清理半年就是15亿条。哪怕时序数据库能扛查询侧也会越来越慢因为扫描范围在持续膨胀。位置数据的业务价值是有时效性的。实时调度只需要最近几分钟的位置轨迹回放通常只看最近24小时到7天历史热力图分析才需要更多历史数据。所以存储策略可以拆三档热数据最近24小时放在TDengine或InfluxDB支持毫秒级查询温数据24小时到30天由任务定期把明细数据降采样后归档到ClickHouse比如把5秒一条的坐标降采样成1分钟一条的轨迹摘要冷数据超过30天直接丢弃或者仅保留按小时的聚合统计值比如某区域某小时的骑手分布密度。这套“热-温-冷”三级策略让系统的数据总量始终可控写入不会越来越慢查询也不会越来越慢。做过这类系统的人都知道存储膨胀是比并发更隐蔽的“写死”原因。3.4 经纬度精度坐标背后的“隐性坑”聊到这个话题必须提一下坐标本身。骑手端GPS上报的原始坐标通常是WGS84经纬度但国内民用地图产品实际展示时一般会用GCJ-02国测局坐标两者之间存在偏移。如果系统要做地理围栏、区域密度统计却混用两套坐标系查出来的结果会偏移几百米这个数据在业务上就是错的。还有热词里提到的“WGS1984和CGCS2000对不上”更偏测绘专业领域。CGCS2000是我国2000国家大地坐标系与WGS84在理论上有厘米级差异日常民用场景可以忽略但涉及工程测量、精准农机的轨迹回放就必须要做转换处理。实际落地建议上报链路统一收WGS84原始坐标落库时原样存储同时额外存储一列GCJ-02转换坐标用于展示和检索。两套坐标并存而不是覆盖存储。这个细节在排查“骑手位置显示偏移”类故障时能少踩一半的坑。4. 高可用与容灾系统不被“写死”的最后防线4.1 限流与降级宁可丢数据也不能让链路全挂再好的架构也挡不住所有极端情况。Kafka集群故障、下游数据库磁盘满了、机房网络抖动任何一层出问题都可能波及整条链路。所以限流和降级策略必须在设计之初就埋好。限流分两个层次。入口网关限制每秒总请求数超过阈值直接丢弃或者返回“稍后重试”消费端限制落库的最大写入速率防止数据库被超出预期的洪峰冲垮。有朋友可能会问消息丢了怎么办位置数据丢几条业务上是可以接受的这跟订单支付那种强一致数据本质不同。位置服务的核心是“绝大多数时间可用”而不是“每一条都必须落库”。接受这一点系统设计的自由度会大很多。降级策略则是当数据库写入延迟超过阈值时消费端暂停落库消息暂时留在Kafka里如果Kafka本身也异常则内存缓冲顶上去实在不行就丢弃非关键的消息优先保障核心业务链路不被拖死。降级预案平时就要写好开关演练过别等出故障了再临时想。4.2 写入幂等防止重复数据撑爆存储位置上报场景里重复数据是常态。终端网络超时会重发Kafka消费失败会重放消费者重启会重复处理。如果写入链路不做幂等控制同一位置可能落库好几次日积月累就是几十亿条废数据。这不是写入速度问题而是数据质量问题。幂等的做法很简单在写入表里加一个唯一键把“骑手ID设备生成的时间戳”组合作为唯一标识。批量写入时如果遇到相同ID直接忽略或覆盖。时序数据库里TDengine和InfluxDB都支持类似的去重机制ClickHouse也有ReplacingMergeTree表引擎可以按key去重。在存储层面做幂等比在应用层做要省事得多。4.3 监控与告警写不死靠的是“提前发现”“不写死”不是靠运气是靠监控。该盯的核心指标有四个写入TPS看整体吞吐是否在预期范围内突然涨上去可能是终端异常突然掉下来可能是链路堵塞Kafka积压量积压量持续上涨说明下游消费速度跟不上这是最需要警惕的信号数据库写入延迟单次批量写入耗时从10ms涨到100ms说明存储可能遇到了瓶颈慢查询和连接数连接数打满、大量慢查询出现是数据库即将崩溃的前兆。这几个指标要配置成仪表盘设置分级告警。积压超过10万条、写入延迟超过500ms、连接数超过80%都必须在5分钟内触发告警。做过高并发系统的人都明白故障本身不可怕可怕的是故障发现得太晚等数据库真的挂了再抢救恢复成本会成倍增加。5. 实测观测清单与常见坑5.1 压测什么样算合格系统上线前压测是必须做的。工具用JMeter或Locust模拟骑手终端从几千并发慢慢往上加直到逼近1.5万TPS。注意压测的目标不是“把系统压垮”而是找到系统在什么水位下开始雪崩以及雪崩前的预警信号是什么。合格的标准可以参考这个观测结果TPS稳定在1.5万、Kafka积压量维持稳定、数据库写入延迟曲线水平、整体CPU利用率不超过70%。如果测试中看到TPS上不去优先排查的不是数据库而是链路中最弱的环节常见的是接入网关线程模型不合理、消息序列化性能差、批量写入批次大小没调优。逐个优化直到整条链路打通。5.2 常见问题与排查思路速查表现象可能原因排查方向数据库写入延迟飙升批量批次过大或索引过多调小批次大小检查是否有冗余索引Kafka积压持续增长消费吞吐跟不上增加消费者分区数检查消费者处理逻辑是否有耗时操作TPS到一定值就上不去网关线程模型阻塞检查是否有同步IO、锁竞争、GC停顿轨迹回放卡顿查询扫描范围过大确认是否按时间分区轨迹点是否降采样位置显示偏移坐标系混用检查WGS84和GCJ-02是否混存某区域写入变慢数据按区域分片导致热点检查分片键是否选错改回骑手ID哈希5.3 内存缓冲的取舍什么时候用什么时候别用再补充一点个人经验不要在应用层过度使用内存缓冲。有的方案喜欢在服务内存里攒一批数据再批量写库这样做确实能提高吞吐但有一个致命问题——进程挂了内存里攒着的那批数据就全丢了。位置数据虽然丢了也能接受但也要看丢多少。如果缓冲区攒了5秒的数据进程重启一次就丢5秒全量数据5万骑手在那一瞬间的坐标集体消失调度大屏会出现一片空白区域业务影响还是很明显的。折中方案是应用层内存缓冲只做少量热数据暂存几百条级别主力缓冲交给Kafka。Kafka本身有持久化和多副本机制进程挂了消息不会丢这样既保证吞吐又有基本的数据可靠性。记住一条原则能用消息队列做的事不要让内存去抗。最后再分享一点实际体会做了这么多年位置相关的系统我的感受是高频坐标上报这个场景真正考验的不是某一种数据库的极限性能而是整条链路的设计思维。5万骑手、每5秒上报一次听起来吓人但拆开看每秒1万条写入用Kafka削峰、时序数据库承载、批量写入控制成本、幂等去重保证质量这套组合下来系统余量其实非常充足。我自己踩过最深的坑是早期按“区域”做了数据分片结果市中心那个分片被打到冒烟郊区几个分片闲着没事干整个系统的容量上不去。后来改成按骑手ID哈希分片同等硬件条件下吞吐直接翻倍。这些经验说复杂也复杂说简单也简单核心就是先想清楚数据的写入模式再动手设计存储和链路。设计对了5万骑手不会写死系统设计错了5000骑手都能让人通宵加班。
返回列表