当前位置: 首页 > 科技观察

私家车架构进化史:好的架构来自进化,而不是设计

时间:2023-03-18 00:10:04 科技观察

很多年前,看过子流的《淘宝技术这十年》。这本书成了我的架构启蒙书。书中的一句话像种子一样埋在了我的脑海里:“好的架构来自于进化,而不是设计。”2015年加入中国专车订单研发团队,体验专车数据层“架构演进”的过程。这段工作经历对我很有启发,也让我时常感叹:“好的架构真的是一点点演化出来的。”1、在单体数据库架构产品的起步阶段,技术团队的核心目标是:“快速实现产品需求,尽快对外提供服务”。当时专车服务是连接一个SQLServer数据库,服务层已经按照业务领域做了一定的拆分。这种结构非常简单,团队可以各自协作,效率极高。随着用车订单量的不断增加,在早晚高峰时段,用户需要打车时,点击下单后往往没有任何反应。系统层面:出现数据库瓶颈。频繁的磁盘操作导致数据库服务器的IO消耗增加。同时,多表关联、排序、分组、非索引字段条件查询也会增加CPU,最终导致数据库连接数激增;网关会大规模超时。高并发场景下,大量请求直接操作数据库,数据库连接资源不够,大量请求阻塞。2、SQL优化和读写分离为了减轻主库的压力,很容易想到一个策略:SQL优化。通过性能监控平台和DBA同学,对慢业务SQL进行了分析,梳理出优化方案:合理添加索引;减少多表JOIN关联,通过程序组装降低数据库读取压力;减少大事务,尽快释放数据库连接。另一种策略是:读写分离。读写分离的基本原则是让主库处理事务性的增、改、删操作(INSERT、UPDATE、DELETE),从库处理SELECT查询操作。在专车架构组提供的框架中,支持读写分离,所以数据层架构演变为下图:读写分离可以减轻主库的写入压力,同时,读从库可以水平扩展。当然,读写分离还是有局限性的:读写分离可能会面临主从延迟的问题,订单服务和载客过程中实时性要求高。由于担心延迟,大量操作仍然使用主库查询;读写分离读压力可以缓解,但是随着业务的爆发式增长,写操作的压力并没有得到有效缓解。3、业务领域的分库虽然在应用层面做了优化,在数据层做了读写分离,但是对主库的压力还是很大的。接下来大家一致想到了业务领域分库,即:将数据库按照业务领域拆分成不同的业务数据库,每个系统只访问业务对应的数据库。业务领域的分库可以缓解核心订单库的性能压力,同时减少系统间的交互,提高系统的整体稳定性。随之而来的问题是:在单库的情况下,简单的使用JOIN就可以满足需求,但是拆分业务库在不同实例上后,JOIN就不能跨库使用了,需要重新梳理系统边界,业务系统也需要重构。重构主要集中在两部分:将原来需要JOIN关联的查询改为RPC调用,在程序中组装数据;业务表有适当的冗余字段,通过消息队列或异构工具同步。4.在缓存和MQ私家车服务中,订单服务的并发量和请求量最高,也是业务中的核心服务。虽然通过业务领域的分库和SQL优化,系统性能有了很大的提升,但是订单数据库的写入压力还是很大,系统的瓶颈还是比较明显的。因此,订单服务引入了缓存和MQ。乘客在客户端点击立即叫车,订单服务创建订单,先保存到数据库,然后将订单信息同步保存到缓存。在订单的乘客生命周期中,订单的修改操作先修改缓存,然后发送消息给MetaQ,下单服务消费消息,判断订单信息是否正常(如是否乱序)).如果订单数据正确,则存储在数据库中。核心逻辑有两点:缓存集群存储最近7天的订单明细,大量的订单读取请求直接从缓存中获取;在订单的乘客生命周期中,写操作首先修改缓存,通过消息队列异步下单,这样消息队列可以起到消峰的作用,也可以减轻数据库的压力.本次优化提升了订单服务的整体性能,也为后续订单服务分库分表、异构化打下了坚实的基础。5、从SQLServer到MySQL的业务还在爆发式增长。每天几十万的订单,订单表的数据量很快就会过亿,触及数据库天花板是迟早的事。订单分库分表已经成为技术团队的共识。业界很多分库分表方案都是基于MySQL数据库。专车技术管理层决定先将整个订单数据库从SQLServer迁移到MySQL。迁移前的准备工作非常重要:SQLServer和MySQL这两个数据库的语法存在一些差异,订单服务必须适配MySQL语法。订单order_id是自增主键,但是不适合分布式场景,需要调整订单id为分布式。准备工作完成后,开始迁移。迁移过程分为两部分:历史全量数据迁移和增量数据迁移。历史数据全量迁移主要是DBA同学通过工具将订单数据库同步到独立的MySQL数据库。增量数据迁移:由于SQLServer没有binlog日志概念,所以无法使用maxwell、canal等类似方案。订单团队重构了订单服务代码,每写入一个订单,都会向MetaQ发送一条MQ消息。为了保证迁移的可靠性,还需要将新数据库的数据同步到旧数据库,即需要双向同步。迁移过程:首先,订单服务(SQLServer版)向MetaQ发送订单变更消息。此时不开启“旧库消息消费”,让消息先在MetaQ中积累;然后开始迁移历史全量数据。当全量迁移完成后,再开启“旧数据库消息消费”,这样新订单数据库就可以与旧订单数据库数据同步;打开“新建数据库消息消费”,然后部署订单服务(MySQL版)。此时有两个版本的订单服务同时运行,检测数据无误后,逐渐增加新订单服务的流量,直到旧订单服务完全下线。6、自研分库分表组件分库分表在业界一般分为两种:proxy和client。▍Proxy模式Proxy层分片方案,业界有Mycat、cobar等。它的优点:应用程序零改动,与语言无关,可以通过连接共享减少连接的消耗。缺点:因为是代理层,所以有额外的延迟。▍Client模式应用层sharding解决方案,业界有sharding-jdbc、TDDL等。其优点:直接连接数据库,开销小,实现简单,中间件轻量级。缺点:无法减少连接的消耗,有些侵入性,而且大部分只支持Java语言。神舟架构团队选择了自主研发的分库分表组件,采用客户端模式,并将组件命名为:SDDL。订单服务需要导入SDDLjar包,并在配置中心配置数据源信息、shardingkey、路由规则等。订单服务只需要配置一个datasourceId。7.分库分表策略7.1乘客维度查询私家车订单数据库的主要维度是:passenger,根据乘客user_id和order_id查询频率最高的客运站,我们选择user_id作为shardingkey,并且同一个用户的订单数据存储在同一个数据库中。分库分表组件SDDL与阿里开源数据库中间件的cobar路由算法非常相似。为了方便扩展思路,先简单介绍一下cobar的分片算法。假设订单表需要平分4个分库shard0、shard1、shard2、shard3。先把[0-1023]分成4段:[0-255]、[256-511]、[512-767]、[768-1023],然后把字符串(或子串,由用户自定义)做hash,hash结果取模1024,最终结果slot落入的segment会路由到哪个分库。cobar默认的路由算法可以很自然的和snowflake算法融合。order_id使用雪花算法,我们可以将slot的值保存在10位的worker机器ID中。可以通过订单order_id反向检测slot,定位用户订单数据所在的分区。IntegergetWorkerId(LongorderId){LongworkerId=(orderId>>12)&0x03ff;returnworkerId.intValue();}私车SDDL分片算法和cobar的区别在于cobar支持最大分片数为1024,而SDDL最大支持分库数为1024*8=8192,并且也分为四个订单库,每个子分片的slot区间范围为2048;因为需要支持8192个子分片,需要对雪花算法进行微调,将雪花算法的10位工作机修改为13位工作机,时间戳也调整为:38位时间戳(从某个时间点算起的毫秒数)。7.2司机维度虽然解决了主维度乘客分库分表的问题,但是专车还有另外一个查询维度。在司机端,司机需要查询分配给自己的订单信息。我们使用乘客user_id作为分片键。如果我们按照驱动程序driver_id查询顺序,需要广播到各个分库聚合返回。基于此,技术团队选择将订单数据从乘客维度异构到司机维度数据库。司机维度的分库分表策略与乘客维度相同,只是shardingkey变成了driver_id。异构神器通道解析乘客维度四个分库的binlog,通过SDDL写入司机维度四个分库。说到这里大家可能会有疑问:虽然订单可以异构同步到司机维度的分库,但是毕竟有一点延迟,如何保证司机端可以查询到最新的订单数据呢?在缓存和MQ部分提到缓存集群存储最近7天的订单明细,大量的订单读取请求直接从缓存中获取。订单服务会缓存驱动和当前订单的映射关系,这样驱动端的大量请求可以直接从缓存中获取,而驱动端查询订单列表的频率没有那么高,异构复制延迟在10毫秒到30毫秒之间。是完全可以接受的。7.3运营维度专车管理后台,运营商经常需要查询订单信息,查询条件会比较复杂。专车技术团队采用的方法是:将订单数据放入乘客维度的订单子库后,通过canal同步到ElasticSearch。7.4小表广播服务中有一些配置表,存放重要的配置,多读少写。在实际业务查询中,很多业务表会与配置表进行联合数据查询。但是数据库水平拆分后,配置表就不能拆分了。小表广播的原理是:自动将小表的所有数据(包括增量更新)广播(也就是复制)到大表的机器上。这样,原来的分布式JOIN查询就变成了单机的本地查询,大大提高了效率。在私家车场景下,小表播报是一个很实际的需求。例如:城市表是一个非常重要的配置表,数据量非常小,但是订单服务、派单服务、用户服务都依赖这张表。将基础配置库city表通过canal同步到订单库、调度库、用户库。8.平滑迁移分库分表组件SDDL开发完成,生产环境经过一定程度的验证后,订单服务从单库MySQL模式迁移到分库分表模式已经成熟。迁移思路其实和从SQLServer到MySQL非常相似。总体迁移流程:DBA同学准备了四个乘客分库和四个司机分库。每个子数据库包含最近一个时间点的完整数据;分库分表规则,删除八个分库的冗余数据;开启正向同步,旧的订单数据会按照分库分表的策略转移到乘客维度的分库中,将乘客维度分库的订单数据异构复制到运河在驱动维度的子数据库中;开启反向同步,修改订单应用的数据源配置,重启订单服务,订单服务新建的订单将放入乘客维度的子数据库中。构建全订单数据库和司机维度数据库;验证数据无误后,逐步更新订单服务的数据源配置,完成整体迁移。9、数据交换平台车单已经分库分表,很多细节值得回顾:全历史数据迁移需要DBA介入,技术团队没有成熟的工具或产品可以轻松完成;通过canal实现增量数据迁移。随着私家车业务的爆发式增长,对数据库镜像、实时索引构建、异构分库的需求越来越大。canal虽然很好,但还是有缺陷,比如缺少任务控制台、数据源管理能力、任务层级。监控告警、运行审计等功能。面对这些问题,架构团队的目标是搭建一个能够满足各种异构数据源之间的实时增量同步和离线全量同步的平台,支撑公司业务的快速发展。基于这个目标,架构团队自研dataLink实现增量数据同步,深度定制阿里巴巴开源dataX实现全量数据同步。10、写到最后,私家车架构的演进并非一帆风顺,也有波折,但一步一步,私家车的技术储备越来越深。