您还未登录! 登录 | 注册 | 帮助  

您的位置: 首页 > 软件开发专栏 > 开发技术 > 正文

58速运架构实战:拆分服务与DB,突破“中心化”瓶颈

发表于:2018-08-22 作者:张凯 来源:51CTO技术栈

很高兴有这次机会,跟大家分享一下我们 58 速运微信小程序的事件。我是后端平台的负责人,从 2017 年底开始负责我们 58 速运的微信小程序的开发工作。

本次分享主要从以下几个方面来进行:

  • 58 速运模式
  • 小程序的意义
  • 小程序架构实战
  • 总结

58 速运模式


58 速运是覆盖中国及东南亚地区的同城货运平台,2018 年开始了全新的速运 2.0 时代:

速运模式

司机通过司机加盟的流程加入到我们,登录司机端 APP,就可以开始接单了;而用户通过 APP 或者是用户端的 H5 登录上去,可以跟司机下单,我们的推送系统经过一系列的算法,推送给附近的司机,然后司机抢单,到达目的地,将账单发送给用户,用户确定定单、收款,这个过程就结束了。

小程序的意义


那么问题来了,有了我们司机端的 APP 和用户端,为什么还要做微信小程序呢,它对我们的 58 速运究竟有什么意义呢?

首先来解释一下什么是速运的 2.0。

在旧的 1.0 时代,司机只能通过加盟接单,并且想要成功接单还需要一系列的审核流程,因为我们要保证服务质量,之后再登录我们的 APP 才能完成。

我们对司机有一系列的审核、管理工作。用户登录我们的 APP 以后,只能给我们的平台司机下单。

速运 2.0 就是要把这个中心化的过程打破,要做去中心化的过程。

司机只要登录了司机端的微信小程序就能够接单,不用加盟;用户登录微信小程序,就能给所有的司机下单,不管这个司机是否在我们的平台注册过。

小程序的架构实战

 

 

现有架构

这是现有的架构,如上图:

  • 接入层有安卓、iOS、H5;
  • 服务层就是司机端服务、用户端服务,定单服务、派单服务;
  • 数据层比如说 ES、DB 等等。

可以看到,我们的服务层都是一个个大而全的系统,业务发展的过程中,前期的业务功能并不复杂,一个系统一个服务就能够满足我们所有的业务现状,而且开发起来也比较快。

当我们的业务达到了一定的量级之后,这一个大而全的服务就会阻碍我们业务的发展,我们很多的团队在维护一个服务,就会出现很多的问题。

比如说我们开发的过程中就会有上线冲突;当业务达到一定的量级之后,DB 压力也很大。我们现在正在进行的一项工作就是对服务和 DB 的一个拆分。

小程序功能

架构肯定是为业务而设计的,那么我们的小程序有哪些功能?

对于用户来说,首先就是有一个会员商品的售卖;其次,用户只要购买了我们的会员商品,就会有一些会员等级;此外还有收藏司机的功能、用推广码下单的功能,如果用户扫描了这个码,就可以直接给司机下单。

那么针对司机来说有哪些功能?就是登录了微信小程序后会有一个二维码,司机可以自主接单。

面对这些功能,我们的思路是:

  • 避免大而全,我们就要对这些功能进行一个个的拆分,拆分成一个个的服务;
  • 从简单的开始着手,逐步进行细化的过程;
  • 微服务的架构,方便后续的拓展和维护。

会员服务和用户等级

来分析一下会员服务和用户等级。为什么把它们两个一块儿说?因为它们的核心功能点比较相似:

  • 会员服务就是买了会员商品后会有的等级和特权;
  • 而如果用户一个月下单数达到一定的阶段,就会有用户的等级和特权。

它们的核心功能点就是级别的展示、授权以及定期的发券。

首先是 Web 层,还有服务层。有升级就有降级,针对降级,我们使用的是定时任务来处理。

如果你单机部署,那这台机器挂了怎么办?如果是部署多台,那同时跑了怎么办?

我们有一个自研的基于 ZK 的调度平台,在跑的时候,首先会出一个临时节点,说明自己在跑,当机器挂掉之后,节点就消失;另一个到达时间节点的时候,就会在另外的一个机器上面跑,保证一个时间点只能有一个机器在跑这个 Job。

我们的用户等级升级是要求比较高的,比如说用户买了一个商品,立马就希望等级升上去。我们使用了一个消息队列,保证我们收到消息之后,立马把用户的等级升上去。

还有定时发券的场景,我们使用了延时消息,我们在用户发券之后去判断是否还需要发券,如果还需要的话,就接着发一个延时消息。

用户等级服务的核心功能点之一,是根据用户当月的订单数来实时地更新用户等级。

一般情况下,统计当月的订单数,都是使用定时任务每隔一段时间去计算。但是,因为我们对实时性要求较高,这样做并不合适。

所以我们使用了接收订单完成的 MQ 来实时进行计算。我们的做法是,先根据 MQ 实时更新每天的订单数,保证每天的订单数可查,同时更新每月的订单数。

通常情况下,在更新当月的订单数之前或者是之后,只需要清除一次缓存就行了,但是我们清除了两次,为什么?

用户访问了自己的用户等级,可能会出现一种情况就是用户看到的这个数据并不对,数据不一致。使用双缓存清除法能解决这个问题。流程如下图所示:

商品服务

会员商品服务的典型场景有四种:

  • 读多写少;
  • 商品不可变;
  • 针对单个的商品和用户是有一个限购的条件的;
  • 商品有可能会有一些库存的限制。

比如说我就想卖 100 个,针对这个场景我们能不能很简单的一个 Web、一个服务加上存储就搞得定呢?如果商品卖出去了,缓存是不是就失效了?

我们如何保证商品缓存的时效性?如果我的库存这一块儿出现了问题,那是不是商品会受到影响?比如说库存导致我们的服务挂了,那商品直接看不到……

针对这些问题,我们处理的方式:

  • 首先就是将这个可变的数据隔离,将商品服务不做成一个服务;
  • 针对消息一经发布不可变,而且访问量很大的问题,可以通过加缓存来缓解压力;
  • 至于怎么保持库存的一致性,就是用 CAS 乐观锁来保证库存服务的效率。

司机的 GPS 服务

我们是一个同城货运平台,大部分的场景是用户下单,司机接单。我们有 100 万的注册司机,要保证司机实时的 GPS 位置准确,2 秒钟上传一次 GPS,这个请求量是特别大的。

但 GPS 的服务对我们的实时性的要求又非常高,所以 MySQL 的压力非常大,如果再上一个层次,MySQL 肯定扛不住;如果放在缓存里面,又有另外的问题,也就是缓存无法搜索的情况;还有怎么样提高处理效率的问题。

针对这几个问题,我们的做法:

使用了生产者消费者模式,生产者发送 MQ 给消费者,消费者本身是一个 Job。接到消息后,先进行时效性的判断,如果超时,直接丢弃。

如果没有超时,异步调用 GPS 服务。GPS 服务接到调用后,先放到一个队列里面,然后后台有一个线程,批量地进行 ES 的存储。

订单服务

现状:

我们碰到的问题,主要是老订单因为业务的发展对我们的小程序已经不太适用;老订单的服务根据前台的业务进行了一个分表的处理,后台是一个单表,我们后台的单表查询会非常的慢。

前后台是采用了 canal 的方式同步的,最大的时候前后台订单有 6 个小时的同步,目前订单已经是 40 万的数据了。之前前台的订单服务、后台的订单服务包括订单的 ES 服务,很多人在调用的时候其实根本不知道调用哪些服务。

我们的思路,第一,订单服务要统一成一个;第二,我们要采用分库的方式来实现。

一般有水平拆分和垂直拆分:

第一想到的就是能不能把我们的数据进行一个隔离,比如说按照时间段做一个垂直的,2017 年的放一个库,2018 年的放一个库。

第二,水平的拆库拆表。首先用户肯定要查询自己的订单列表的,司机也需要查询,然后大部分的场景其实是司机的查看订单详情,还有我们公司自己的后台的运营人员,有一些复杂的查询。

那么如何确定我们的分库方案?

按照时间纬度的优点是订单分到最新时间段的库,直接查就行了,缺点在于如何确定时间纬度,一个月、一个季度或者是一两年。

还有一个问题就是说,如果确定了时间纬度之后,订单还有大的增长怎么办?我们的库是提前建好还是动态申请,资源上面也要权衡。

水平拆分,订单如果再有一个上升的阶段,就直接横向扩展了。我们也要解决跨库查询,也需要订阅方案。

我们 85% 的查询是根据订单的 ID 来查询的,比如说大部分的司机抢单,查看订单详情;用户下单查看详情之类的。

接下来会有 10% 的用户查看自己的 ID,我到底下了哪些单,或者是历史的订单是什么样子的。

还有 4% 是根据司机的场景来查询,只是偶尔空闲的时候他才会查自己今天抢多少单,最后只有 1% 的后台复杂查询,所以可以往后考虑。

如果需要满足 85% 的场景,根据用户 ID 来取模行不行?但是问题是司机 ID 的列表查询怎么来解决呢?方案就是索引表。

我需要查询任务,根据业务和订单 ID 来建立索引表,就可以查到所有的订单,然后能确定每一个库,就能搞定场景。

但是我们的数据量达到一定的阶段,索引表也需要分库怎么办?这样所有的用户的东西都在一个库里面,查询用户列表的时候就不用分库了,这样解决了我们 10% 的问题,那 80% 多的问题怎么解决?就是基因法。

根据用户 ID 得到的数字,其实就是我们的分库基因,大家都知道 Java 里面是 64 位的数字,前 40 位用做一个时间,这个时间并不是说我们直接调用系统的当前时间,而是拿 2018 年,拿一个固定的起始时间。

比如说 2018 年 1 月 1 号,再用当前的时间减去起始时间的毫秒数,得到的时间左移 23 位,放到我们的 40 位的位置。

接下来的是我们的机器位,为什么会这样呢?因为我们的 ID 生成器不可能是在单机上面用的,是在多个机器上面用。

接下来的是我们的分库基因,还有自动的序列,是为了保证同一毫秒生成的 ID 不会有重复。

比如说同一秒内支持的 ID 生成是 6 万多个,如果不够用怎么办?将时间的秒数量再加一就可以了。

ID 生成搞定了,怎么根据这个生成找到我们所在的库?下面这个其实就是一个反向的过程,能确定到我们的一个库。

解决了 95% 的场景,剩下的怎么搞?使用 ES 就行了。

因为司机不会实时的去查看自己的订单,运维人员对订单的实时性要求也并不高,所以说直接使用 ES 就行了,最后我们的订单服务就是这个样子。

做一个总结,就是按照用户的纬度来分布,订单 ID 使用 Snowflake 算法生成,订单中记录分库因子,然后复杂查询使用 ES 来解决。

老旧服务的兼容

这样的话订单服务并没有完,我们还有老旧的服务必须做一个兼容:

针对订单的写,我们是做了一个双写,写了新订单之后,会去同步写一次老订单;针对订单的读,我们是先查询新订单,如果查不到,会在老订单里面查一遍再返回客户端;我们对历史数据也有一套完整的迁移方案。

推送服务的改造

微信小程序还涉及到了推送服务的两个方面的改造:

因为我们新增了两种推送模式:

  • 一对一的推送,相对来说比较简单,下单的时候,如果用户选择的是一对一推送的,比如说用户只选了一个司机,我们就默认司机就是中单了,不管这个司机在不在线,如果能推给司机就推给他,如果不能推给他,就给他发一个短信。
  • 一对多的推送,省去了推送的算法,用户选择多个司机,然后我们的系统根据用户筛选的司机,挑选出在线的列表,然后全部推送给那些司机,直到司机又抢单就结束了。

这是我们改造之后的一个微信小程序的总体架构图:

我们分了接入层、业务层、服务层、基础服务、数据层。接入层就是对每一个服务做了一个划分,进行了一层业务的评定之类的,反馈给我们的接入层。

我们的程序想要上线,首先要接入我们的服务治理平台:

我们的服务治理平台会提供一些功能:

  • 动态机器的管理,比如说我们的业务撑不住了,可以通过这个加一点机器;
  • 对整个服务的流量的监控;
  • 访问耗时的监控;
  • 还有我们的抛弃量监控。

接下来就是接入我们的监控平台,会有针对的关键字监控,也有 URL 的监控,针对不同的监控有一些监控的策略。

最后就是接入我们的 Dtrack 调用链,可以知道整个服务之间的调用关系:

如果服务的量级上来了,那么我们可能自己都不知道是调用了哪个服务,它的层次关系靠人已经分不清了。如果接入调用链的话,会打印出一个服务的清晰的调用关系。

  • 它给我们提供了全局跟踪,比如说调用了哪些服务,耗时有多少;
  • 哪个服务有问题的,会立马有一个异常的报警;
  • 针对服务之间会有清晰调用结构;
  • 对整个服务也会有一个效果监测。

它的技术点在哪儿:

我们在框架里面提供了插件,每一次调用的时候,就会形成 traceid,通过框架传递下去,每调用一个服务,它的 ID 会 +1。将这些调用关系通过日志打印出来,通过 Flume 采集之后展现出来就行了。

总结


最后总结一下,准确的理解需求很重要,架构是为业务服务的;碰到一个大的需求,对需求进行拆分,由简单到复杂的拆分;根据业务需求进行合适的技术选型,任何脱离业务的架构设计都是耍流氓,监控特别的重要,谢谢大家。

张凯,58 速运后端平台部负责人。7 年开发经验,涉及 CRM、微信钱包、卡券系统等;参与 58 到家钱包入口的优化拆分改版,保证了系统平稳过渡;参与 58 大促,保证了大促期间卡券系统的稳定运行。现在负责后端平台的开发工作。