58

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

 5 years ago
source link: https://mp.weixin.qq.com/s/dk82n1bbGuh-iRKMyK7f3A?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

aURFJb6.gif

本文根据张凯老师在〖2018 Gdevops全球敏捷运维峰会成都站〗现场演讲内容整理而成。

qUFJ32m.jpg!web

(点击“阅读原文”可获取张凯演讲完整PPT)

讲师介绍

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

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

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

  • 58速运模式

  • 小程序的意义

  • 小程序架构实战

  • 总结

一、58速运模式

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

速运模式

QBfErun.jpg!web

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

二、小程序的意义

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

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

在旧的1.0时代,司机只能通过加盟接单,并且想要成功接单还需要一系列的审核流程,因为我们要保证服务质量,之后再登录我们的APP才能完成。我们对司机有一系列的审核、管理工作。用户登录我们的APP以后,只能给我们的平台司机下单。

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

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

三、小程序的架构实战

1、现有架构

这是现有的架构:

IvURRvm.png!web

  • 接入层有安卓、IOS、H5;

  • 服务层就是司机端服务、用户端服务,定单服务、派单服务;

  • 数据层比如说ES、DB等等。

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

当我们的业务达到了一定的量级之后,这一个大而全的服务就会阻碍我们业务的发展,我们很多的团队在维护一个服务,就会出现很多的问题。比如说我们开发的过程中就会有上线冲突;当业务达到一定的量级之后,DB压力也很大。我们现在正在进行的一项工作就是对服务和DB的一个拆分。

2、小程序功能

uAf22e7.png!web

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

对于用户来说,首先就是有一个会员商品的售卖;其次,用户只要购买了我们的会员商品,就会有一些会员等级;此外还有收藏司机的功能、用推广码下单的功能,如果用户扫描了这个码,就可以直接给司机下单。那么针对司机来说有哪些功能?就是登录了微信小程序后会有一个二维码,司机可以自主接单。

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

  1. 避免大而全,我们就要对这些功能进行一个个的拆分,拆分成一个个的服务;

  2. 从简单的开始着手,逐步进行细化的过程;

  3. 微服务的架构,方便后续的拓展和维护。

会员服务和用户等级

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

  • 会员服务就是说买了会员商品后会有等级和特权;

  • 而如果用户一个月下单数达到一定的阶段,就会有用户的等级和特权。

BJzqaev.png!web

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

N3UzEnj.png!web

首先是WEB层,还有服务层。有升级就有降级,针对降级,我们使用的是定时任务来处理。如果你单机部署,那这台机器挂了怎么办?如果是部署多台,那同时跑了怎么办?我们有一个自研的基于ZK的调度平台,在跑的时候,首先会出一个临时节点,说明自己在跑,当机器挂掉之后,节点就消失,另一个到达时间节点的时候,就会在另外的一个机器上面跑,保证一个时间点只能有一个机器在跑这个Job。

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

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

用户等级服务的核心功能点之一,是根据用户当月的订单数来实时地更新用户等级。一般情况下,统计当月的订单数,都是使用定时任务每隔一段时间去计算。但是,因为我们对实时性要求较高,这样做并不合适。所以我们使用了接收订单完成的MQ来实时进行计算。我们的做法是,先根据MQ实时更新每天的订单数,保证每天的订单数可查,同时更新每月的订单数。 通常情况下,在更新当月的订单数之前或者是之后,只需要清除一次缓存就行了,但是我们清除了两次,为什么?用户访问了自己的用户等级,可能会出现一种情况就是用户看到的这个数据并不对,数据不一致。使用双缓存清除法能解决这个问题。流程如下图所示:

JfURnuI.jpg!web

商品服务

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

  • 读多写少;

  • 商品不可变;

  • 针对单个的商品和用户是有一个限购的条件的;

  • 商品有可能会有一些库存的限制。

VnMFfq3.png!web

比如说我就想卖100个,针对这个场景我们能不能很简单的一个Web、一个服务加上存储就搞得定呢?如果商品卖出去了,缓存是不是就失效了?我们如何保证商品缓存的时效性?如果我的库存这一块儿出现了问题,那是不是商品会受到影响?比如说库存导致我们的服务挂了,那商品直接看不到……

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

Jfq2qi3.png!web

  • 首先就是将这个可变的数据隔离,将商品服务不做成一个服务;

  • 针对消息一经发布不可变,而且访问量很大的问题,可以通过加缓存来缓解压力;

  • 至于怎么保持库存的一致性,就是用CAS乐观锁来保证库存服务的效率。

司机的GPS服务

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

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

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

iUnIjqZ.png!web

使用了生产者消费者模式,生产者发送MQ给消费者,消费者本身是一个Job。接到消息后,先进行时效性的判断,如果超时,直接丢弃。如果没有超时,异步调用GPS服务。GPS服务接到调用后,先放到一个队列里面,然后后台有一个线程,批量地进行ES的存储。

订单服务

现状:

VFB3Ybe.png!web

我们碰到的问题,主要是老订单因为业务的发展对我们的小程序已经不太适用;老订单的服务根据前台的业务进行了一个分表的处理,后台是一个单表,我们后台的单表查询会非常的慢;前后台是采用了canal的方式同步的,最大的时候前后台订单有6个小时的同步,目前订单已经是40万的数据了,之前前台的订单服务、后台的订单服务包括订单的ES服务,很多人在调用的时候其实根本不知道调用哪些服务。

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

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

Zf2Ibmu.png!web

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

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

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

2EB3ue2.png!web

按照时间纬度的优点是订单分到最新时间段的库,直接查就行了,缺点在于如何确定时间纬度,一个月、一个季度或者是一两年。还有一个问题就是说,如果确定了时间纬度之后,订单还有大的增长怎么办?我们的库是提前建好还是动态申请,资源上面也要权衡。

MbaQVzF.png!web

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

我们85%的查询是根据订单的ID来查询的,比如说大部分的司机抢单,查看订单详情;用户下单查看详情之类的。接下来会有10%的用户查看自己的ID,我到底下了哪些单,或者是历史的订单是什么样子的。还有4%是根据司机的场景来查询,只是偶尔空闲的时候他才会查自己今天抢多少单,最后只有1%的后台复杂查询,所以可以往后考虑。

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

RRru2qY.png!web

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

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

b2yEryj.png!web

根据用户ID得到的数字,其实就是我们的分库基因,大家都知道JAVA里面是64位的数字,前40位用做一个时间,这个时间并不是说我们直接调用系统的当前时间,而是拿2018年,拿一个固定的起始时间,比如说2018年1月1号,再用当前的时间减去起始时间的毫秒数,得到的时间左移23位,放到我们的40位的位置。

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

接下来的其实是我们的分库基因,还有自动的序列,是为了保证同一毫秒生成的ID不会有重复。比如说同一秒内支持的ID生成是6万多个,如果不够用怎么办?将时间的秒数量再加一就可以了。ID生成搞定了,怎么根据这个生成找到我们所在的库?下面这个其实就是一个反向的过程,能确定到我们的一个库。

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

Rfq6FrN.png!web

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

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

老旧服务的兼容

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

7BFBRjA.jpg!web

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

推送服务的改造

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

jqUbyi7.jpg!web

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

  • 一对一的推送,相对来说比较简单,下单的时候,如果用户选择的是一对一推送的,比如说用户只选了一个司机,我们就默认司机就是中单了,不管这个司机在不在线,如果能推给司机就推给他,如果不能推给他,够给他发一个短信。

  • 一对多的推送,省去了推送的算法,用户选择多个司机,然后我们的系统根据用户筛选的司机,挑选出在线的列表,然后全部推送给那些司机,直到司机又抢单就结束了。

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

JbeUNrB.jpg!web

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

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

Yza2iuM.jpg!web

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

  • 动态机器的管理,比如说我们的业务撑不住了,可以通过这个加一点机器;

  • 对整个服务的流量的监控;

  • 访问耗时的监控;

  • 还有我们的抛弃量监控。

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

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

RjIRbmZ.jpg!web

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

eIVVZru.jpg!web

  • 它给我们提供了全局跟踪,比如说调用了哪些服务,耗时有多少;

  • 哪个服务有问题的,会立马有一个异常的报警;

  • 针对服务之间会有清晰调用结构;

  • 对整个服务也会有一个效果监测。

它的技术点在哪儿:

Ufy6Fra.jpg!web

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

四、总结

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

Q1:调用链那一块儿我有一点疑问,我们在日常系统级的异常,或者是我们的业务出现问题的时候,可能会很多,而且我们的调用关系很复杂。对于海量日志怎么来判断哪些异常是对于我的业务,或者是对于我的应用,造成真正的影响?

A1 实际上是这样的,业务日志和这个调用链的日志是分开打的,肯定不是一个日志,不然你放在一块儿,肯定没有办法区分究竟是哪个异常。调用链的日志是打印在一个单独的框架里面,它会发现你不是一个完整的调用。异常报警是通过监测平台来实现的,是两种不同的业务报警。

Q2:是不是我们是以两个纬度来接入,然后优先可能是基于业务的去监控报警,然后具体的去定位,可能会基于那个调用链?

A2 这两个是完全不同的东西,所以会不同的报警,如果你的业务上,比如说我们自己打的一些异常的话,那肯定是通过监控平台来报的,是另外一个方向来报的。如果是Dtrack,是另外一个方向的,它会告诉你哪一个调用,因为每一次调用都会有一个ID,把这个ID输到一个查询平台里面,会根据这个ID把你整个访问的请求打印出来,然后把它给打出来。

Q3:订单水平拆分的问题,我看上面写了水平拆分的分库基因。当你需要发生更改的时候,订单ID是怎么去改的?

A3 其实我们的分库基因并不是直接对分库数取模,比如我有9个bit是用来记录分库基因的。我最大支持512个库,分库基因呢我可以对512进行取模,或者其他任何一致性hash算法都行,只要算出来的数小于512。这时,如果要找库,我就拿出分库基因,然后对我的dbcount,也就是分库数进行一个取模。或者我有一个算法,来计算我这个数对应的是哪个库,用一个环型数组也是可以的。针对对dbcount取模,这样扩库的时候,我就可以以2的倍数进行扩库操作,比如2-4,4-8。这样,扩完库,改一下我们目前的dbcount,然后对历史数据进行清理就行了。如果是环型数组等其他算法来算库,修改一下算法,做相应的数据迁移,清理一样可以达到效果。

更多架构设计的经典案例分析, 尽在 2018 Gdevops全球敏捷运维峰会 北京站! 峰会议题覆盖AIOps与DevOps落地、数据库选型、SQL优化、技术管理等多方面实战,全方位为你充电!

↓ 点击链接了解 更多详情  ↓↓↓

2018 Gdevops全球敏捷运维峰会-北京站

AVfiyaA.jpg!web


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK