广告:Codex Token 低价中转站稳定接口 · 快速接入 · 开发者备用通道
Engineering article

实战干货 | 链路追踪之读写分离

我用链路追踪 + 读写分离的组合方案,给高并发数据库服务加装了“哨兵”,这玩意儿真能救命。一个典型的场景是,应用层日志里突然出现大量“read-only”异常,搞不好是数据库主从同步没跟上。别看玩起来简单,实操真有门道。比如,用skywalking搞链路追踪,配置trace_id传到数据库层,然后通过分库分表策略把读写分离的逻辑压到应用里,

实战干货 | 链路追踪之读写分离
配图来源于网络和AI生成,仅供参考。
▌ 技术引导
我用链路追踪 + 读写分离的组合方案,给高并发数据库服务加装了“哨兵”,这玩意儿真能救命。一个典型的场景是,应用层日志里突然出现大量“read-only”异常,搞不好是数据库主从同步没跟上。别看玩起来简单,实操真有门道。比如,用skywalking搞链路追踪,配置trace_id传到数据库层,然后通过分库分表策略把读写分离的逻辑压到应用里,别把数据库搞成单节点。还有个坑,别把写操作和读操作混在一起,尤其是在主库连接池参数里,max_idle_connections和max_active_connections要区分开来。我见过有人闭着眼睛把读写分片搞成一个配置,结果线上直接炸了,关键数据丢失。记住,不是所有查询都要走从库,尤其是涉及事务、锁或者大表扫描的,必须走主库。另外,别把数据库分片和应用层的读写分离混淆,两者是不同维度的优化,不能混为一谈。

▌ 技术参考
数据库链路追踪是监控系统中一个容易被忽视的环节。某个业务高峰期,我用skywalking采集了5000+个trace_id,其中65%的数据库操作来自读请求。这让我意识到,很多读请求其实可以分流到从库。这时,我开始在应用层做读写分离,加装了redis缓存层。用redis实现查询缓存时,配置了TTL为300秒,同时用lua脚本保证缓存穿透。千万别在主库做全量查询,这样会锁住连接池,导致写请求排队。读写分离需要配合事务一致性,我在业务层加了canary策略,写操作必须走主库,读操作走从库,同时在trace_id里打上“read”标签,便于查问题。

应用层读写分离其实不难,关键在于配置。我记得用mybatis-plus做分库分表时,配置了multi-ds的参数,分别指定了主库和从库的连接池。主库连接池的max_active_connections设为200,从库设为500,这样能保证写操作优先。在springboot启动时,加载了application.yml,里面包含数据库的主从列表。比如,主库是jdbc:mysql://10.1.2.3:3306/db?useSSL=false,从库是jdbc:mysql://10.1.2.4:3306/db?useSSL=false。在代码中,通过@DS注解区分了不同的数据源。但别想当然地认为所有读操作都能走从库,像某些表有索引更新,或者涉及锁的场景,必须走主库。

写操作的高并发是读写分离的最大挑战。我曾用shardingSphere做分库分表,结果某个表的更新操作导致主库CPU打满。后来发现是批量写入没做事务隔离,直接用了innodb_autoinc_lock_mode=2,锁的粒度太粗。于是改用canary策略,先在小流量下验证主库性能,再逐步扩大。同时,用了binlog_do_db和binlog_ignore_db来控制主从同步的范围。你要是没做这个,从库会同步整个数据库,造成同步延迟。另外,别忘了设置binlog_format=ROW,这样能保证主从数据一致。

从库的同步延迟是另一个大坑。之前项目中,主库每秒1000次写入,从库却要等30秒才能同步。这时候,我用pt-table-sync工具做数据同步校验,发现延迟是由于多个只读副本同时拉数据,导致主库负载过高。于是,把从库数量从3个减到2个,并调整了主库的binlog_cache_size和binlog_format参数。同步延迟和网络、磁盘IO密切相关,别指望靠工具解决所有问题。某次线上故障,一个从库挂了,但主库里有数据,结果业务层没及时切换,导致数据不一致。

链路追踪在读写分离中的作用非常关键。我之前用skywalking发现,有些读操作在从库执行了30秒,而主库只要1秒。这时候,我开始分析这些慢查询的原因,发现是某些表没有索引。于是,用explain命令检查了这些SQL,优化了查询结构。同时,在trace_id里打上了“slave”标签,方便日志分析。别用单体日志分析,要上Elasticsearch做集中查询。某个项目里,通过trace_id+sql+耗时,找到了30%的慢查询,优化后响应时间降低了40%。

性能影响是绕不开的话题。我测过一个项目,读写分离后QPS提升了2.5倍。但代价是网络延迟增加了10ms,这在某些响应敏感的业务里会出问题。所以,要根据业务特点来决定是否采用。比如,电商秒杀场景,必须走主库,不能有延迟。但日志查询、报表分析这类,完全可以用从库。另外,别把主从同步的延迟当成性能优化的挡箭牌,该用缓存的用缓存,不该用的别硬上。我见过有人把所有查询都丢到从库,结果主库被写请求撑爆。

适用场景和局限性要分清楚。读写分离适合写少读多的系统,比如内容管理系统、日志分析平台。但像支付、订单处理这类高并发写入的业务,不适合。我在一个金融项目里试过,结果主库写操作瓶颈没解决,反而增加了从库的负担。所以,要先评估业务模式,再决定是否使用。另外,分库分表和读写分离不能混用,两者是独立的优化手段,要分开处理。

替代方案和进阶技巧有很多种。比如,用redis+canal做数据同步,这样既能缓存也能做读写分离。在某个项目里,通过canal把主库的binlog同步到redis里,然后用读写分离策略分发到不同的后端。但这种方式容易产生数据延迟,需要配合缓存失效策略。进阶一点的话,可以考虑数据库代理,比如proxySQL,它能根据SQL类型自动路由到主从。不过,代理层本身也是性能瓶颈,别指望它能扛住所有流量。

读写分离的配置需要非常谨慎。我在一个项目里,用shardingSphere做分片,结果发现主库写入量是1000次/秒,从库只有200次,这说明分片策略有问题。后来发现是分片键设计不当,导致数据分布不均。于是,改用user_id作为分片键,结果写入量均匀了,主库负载也降低了。配置的时候,记得在application.yml里设置shardingSphere的配置项,比如shardingRule下的表分片策略,还有数据源的主从配置。别用自动分片,手动配置更可控。

数据库连接池的配置是读写分离的关键。主库连接池的max_active_connections设为200,从库设为500,这样能保证写操作不会被读操作阻塞。同时,使用HikariCP做连接池,因为它对连接池的管理和资源回收更高效。在连接池配置里,设置idleTimeout为60秒,maxLifetime为300秒,这样能避免连接泄漏。读写分离的连接池可以单独配置,别混在一起,否则容易出现死锁。

链路追踪的配置要和数据库层打通。我之前用skywalking,发现trace_id在数据库层没显示,于是手动在SQL里加了trace_id作为参数。这样能直接在数据库日志里看到请求来源。同时,在数据库连接池配置里添加了trace_id传递的逻辑,比如使用拦截器做埋点。别指望数据库本身支持,得自己加。这样做的好处是能精准定位问题,比如某个SQL在从库执行了30秒,可能意味着索引缺失或者锁竞争。

读写分离中的缓存策略也需要讲究。我之前用redis做缓存,发现某些高频查询会导致缓存雪崩。于是,用随机过期时间来避免这种情况。在Redis的配置文件里,设置maxmemory-policy为allkeys-lfu,这样能自动淘汰不常用的缓存。同时,用lua脚本做缓存穿透,比如在查询前先校验缓存是否存在,不存在再走数据库。别用简单的if-else,用Redis的Lua脚本能保证原子性。

主从同步的性能优化也是个重点。我之前用MySQL的主从复制,发现从库同步速度慢,于是调整了主库的binlog_format为ROW,这样同步更准确。同时,主库的binlog_cache_size调大到1M,避免频繁切换缓存。从库的only_slave参数要打开,这样能防止写操作进入从库。优化同步性能的关键是减少主库的写压力,比如用批量写入代替单条插入。

链路追踪的采集粒度需要细化。我之前在skywalking里只采集了SQL语句,后来发现同一个trace_id可能被多个数据库操作调用,所以加了SQL语句的tag,比如“read”、“write”、“slave”、“master”。这样能更精确地分析不同操作的性能表现。同时,在日志里记录trace_id,方便对齐请求链路。别把trace_id当成随便一个字符串,它是整个请求的唯一标识。

读写分离的监控指标也要有。比如,主库的QPS、TPS、连接数、慢查询数,这些指标要和从库的同步延迟、查询命中率做对比。我之前用prometheus+grafana做监控,发现某个从库的同步延迟达到了40秒,马上重启了从库,避免后续数据一致性问题。监控指标要实时,不能滞后,否则会错过关键问题。

链路追踪和数据库性能分析要结合使用。我之前用skywalking+mysql慢查询日志,发现某个SQL在从库执行了20秒,而主库只要5秒。这时候,我开始分析这个SQL的执行计划,发现它缺少索引。于是,加了联合索引,结果执行时间从20秒降到了1秒。链路追踪和数据库日志要联动分析,别单独看。

在实际部署中,读写分离的配置要灵活调整。我之前用shardingSphere,发现某个分片的数据量太大,导致查询效率低下,于是手动拆分了分片。在配置文件里,设置了分片键为user_id,同时调整了分片数量,从2个增加到4个。这样能分散压力,避免热点问题。分片数和业务数据量要匹配,别盲目扩张。