MaWenge's Blog

筋斗云架构演进

前言

筋斗云出行 主营共享电瓶车租赁。和我们看到的市面上绝大多数做共享出行的公司一样,业务上有很大的相似性,但是有些地方也完全不一样,随着运营时间的增长,产品形态和需求也在逐渐发生着变化,我想每家公司都会总结出自己的一套管理运营心得。这个过程中做过一些根本没怎么用的功能,但是目前留下来的功能可以说大多数都是最需要的。

软件应用服务于业务,业务是核心,当接到一个需求的时候一定要理解需求的本质,然后再规划实现(在小公司尤其要这样,提需求的人也许根本无法准确描述自己的需求,磨刀不误砍柴工,问清楚了,多沟通,确认好,然后想好了再开始动手)。我们对共享出行的认识也是一个逐步深入的过程,所以我们的架构也是在这个基础上不断进化的。

目前公司整个工程可以分为服务端后台、Android和iOS移动端、网页管理后台以及官网,就是服务端+移动端+web前端。其中移动端又包括用户端和运维端。

业务分析

做了这么久的共享租车,成功完成了数百万的租车订单,对整个业务应该也是有了基本的了解,我觉得可以分成如下几个方面:

  • 智能硬件
    主要负责车辆控制、定位以及与服务器通信,包括硬件设计以及硬件上软件编写
  • 智能设备连接服务
    主要负责管理所有车辆与服务端的连接(TCP连接),包括连接管理、数据解析、接收数据、发送指令
  • 车辆管理服务
    车辆信息的持久化以及缓存服务,包括车子位置、电量等各种状态信息的存储以及读取相关服务
  • 订单服务
    租车订单的相关服务
  • 充值服务
    负责用户通过支付宝和微信向账户充值的相关服务
  • 用户服务
    包括普通用户以及管理人员服务
  • 校区服务
    校区电子栅栏管理以及校区特殊配置等服务
  • 工单服务
    车辆维护任务管理,协助运维人员管理维护车辆的服务
  • 用户端服务
    用户端租车等所有相关服务,依赖其他服务
  • 运维端服务
    运维端管理校区车辆的相关服务,也依赖其他服务
  • 分析服务
    对车辆、订单等相关数据的统计分析服务

以上是服务端的主要业务。

第一代筋斗云服务端架构

第一代系统是典型的spring mvc架构。spring 开发web项目可以说效率非常高,其ioc管理bean以及切面技术为开发带来了极大的方便。其切面技术的实现核心原理可以参考前面的一篇文章动态代理

部署

服务是单进程单机服务,也就是说只要这台机器上的服务挂了,那么我们的业务也就挂了。可以水平扩容吗?理论上是可以的。因为我们凡是状态相关的内容(例如token 、session、业务临时缓存数据等)都是保存在redis中,所以即使同一个业务多次访问到不同的机器上也是没有问题的。但是,如果部署多台机器,定时任务必需管理好不要重复执行,可以通过分布式锁来达到要求,也不是什么问题。但是我们在第一代系统运行期间没有进行过多任务部署,原因主要是业务量没有那么多,完全没有必要部署那么多。

那么如果服务重启怎么才能做到用户无感知呢?
我们是这样实现的,买了一台负载均衡,买了两台ecs,平常用的时候只有一个机器上跑着服务。所有的流量也都是指向那一台机器上。如果进行服务重启或升级,就在另一台机器上把服务起起来,此时流量会同时指向这两台机器上,然后迅速把前面那个服务停掉,这时候所有的流量就全部指向新部署的这台机器上了。下一次部署也是同样的方法。也就是说每次服务重启的时候会有短暂的几秒是双机运行。那么这样子能够做到平滑升级吗?不完全可以,因为当你停用旧服务的时候,里面肯定会有正在进行的请求,直接停掉肯定会对客户端造成短暂的请求失败,不过这种情况只要重新请求依然可以得到正确的数据。其实可以调整负载均衡流量,把所有的流量指向新起来的服务之后再把旧服务停掉的,这样基本上可以做到用户无感知,就是稍微麻烦了一点。

物联网服务

在这一代系统中,我们的智能硬件是向一家专做智能中控的服务商采购的。在这些智能硬件当中,又分为两批,前面一批是硬件直连硬件服务商的服务器,然后我们这边的服务器与硬件服务商的服务器进行通信,后面一批是硬件直连我们这边的服务器,由我们这边直接与硬件通信。

对于连接硬件服务商的硬件,我们这边不需要了解具体的连接细节,只需要跟硬件服务商的服务器通信即可。
硬件服务商服务器负责数据的中转,不负责持久化数据,顶多缓存每个设备的最新信息。所有数据持久化全部在我们这边进行。
对于直连硬件服务商的硬件,我们不需要了解与硬件连接以及通信的细节,只需要与硬件服务商的服务器通信即可。对于主动型的指令,例如控制指令以及更新车辆状态信息等指令,我们是通过HTTP请求与硬件服务商服务器通信,对于周期性的车辆位置以及电量等信息更新,是通过硬件服务商发送消息队列来更新。对于车辆上报的警报信息,是我们这边提供HTTP接口供硬件服务商的服务器调用。使用消息队列更新周期性的信息后来在我们每个月的支出里面占了不小的份额。

对于直连我们服务器的硬件,操作起来也就没有那么麻烦了,不需要经过第三方转发。具体就是netty管理设备的tcp连接,然后就是发送与接收数据。

数据库

业务持久化数据库我们使用的是阿里的mysql数据库,对应使用的orm框架是hibernate。其他的数据库以前也没怎么用过,mysql对于一般的业务场景可以说完全够用了。而且对于前期的业务量也基本没有什么压力,只有在第二代系统快要上的时候才会时不时出现一些CPU报警,但是这里面也有很大的一部分是因为sql语句优化不够导致的。
hibernate+jpa框架开发效率也很高,开发起来也是比较方便的。但是对于一些多表联合查询以及一些复杂对象的映射,我认为hibernate支持的不是很好,当然条件查询hibernate做的也是比较到位的。所以后来我也就切换到了mybatis来操作数据库,当然还有同事接着用hibernate。

还有一部分数据是存储在阿里云的tablestore中,这是一种nosql数据库。据说可以支持超大体量的数据查询。我们主要存储的是车辆的历史轨迹信息,就是一些经纬度坐标。因为每个设备快则几秒钟上报一个位置,慢则五分钟上报一个位置,如果所有的这些数据数据全都存储在mysql中,很快mysql就会达到性能瓶颈。而且这些数据有一个特点,查询非常少,只有需要的时候才会去查,一般也不需要查询。所以tablestore是一个不错的选择。

redis主要用来做一些缓存,或者状态变量,例如token,session,发送短信记录等等。

终端应用

终端应用主要分为三大块。用户端移动app(Android+iOS),运维端移动app(Android)以及网页端管理后台。用户端主要负责用户租车服务,运维端主要负责校区运维人员管理当前管辖范围内的车辆。网页端主要用来查看数据以及配置数据。由于前期开发人员较少,运维端和网页端主要由我负责。

移动端app主要就是获取数据以及地图相关显示。我们的地图使用的是高德地图,高德地图的sdk用了之后感觉还是比较友好,基本上完全满足了我们的需求。

网页端使用了一个管理后台框架AdminLTE。这是一个基于bootstrap和jQuery编写的一个框架,里面封装的各种组件用起来可以说是非常的方便,并且也支持响应式布局。管理后台其实是就是查询数据,提交一些表单数据等。最初我们使用的是theamleaf模板引擎在服务端生成填充好数据的HTML界面,浏览器端渲染,jQuery在浏览器端发送ajax请求。但是后来随着后台各种界面列表开始展示查询,每次提交一个查询条件都得所有数据重新从服务端拉回,并且还要恢复上次请求的状态数据,使用jQuery+模板引擎实现这一套功能实在是太麻烦。所以当时又把大部分的常用查询列表的界面全部换成vue实现的了。Vue的数据绑定渲染给我们的开发效率带来了很大的提高,并且大致实现了前后端分离。由于管理后台是不需要SEO优化,所以使用Vue也不会带来任何负面的影响。

以上就是第一代筋斗云架构情况,虽说是单体应用,但是总体下来还是很可靠的,期间出过几次问题,但都不是架构问题导致的性能瓶颈,主要是业务逻辑方面的问题。这个应用一共跑了五个月,产生了二百多万条数据,可以说成功的支撑了公司的初期业务。

第二代筋斗云服务端架构

单体应用虽然没有什么技术含量,但是运行稳定,业务也逐渐成熟,为什么要升级架构呢?
首先,由于是第一次做共享租车行业,初期的功能设计都是模仿膜拜单车 ofo的模式设计的,但是随着运营的开始,逐渐发现我们自己的模式跟别人还是有很大的区别,也衍生出很多没有想到的需求。当然在老系统上逐渐迭代维护也是可以完成的,但是如果重构实现这些将会更加优雅。

其次,第一代系统当中,难免会因为没有经验写出一些效率低,不优雅的代码,这样的代码散落在整个工程的各个角落,必须要花时间把这样的代码逐渐优化。

还有,随着业务量的增大,开发维护整个项目不再是我们几个人可以完成的了,如果继续在这个单体应用上,也是可以的,就是管理起来不是很方便,任务划分也不是非常明确,业务耦合比较严重。

最后,追求技术的码农总是想尝试新的技术,这不最近微服务非常火,所以我们也就开始了微服务的调研。

推荐两篇我认为写的比较好的关于微服务的文章 重新理解微服务中小型互联网公司微服务实践-经验和教训。在做项目之前与做完项目之后读文章,感受也不同。可以说经过自己的实践再次读感触会更深。期间,我还参加了沪江举办的一次关于微服务的技术沙龙,当时没有什么感觉,如果现在再去听,我想感触会更加深刻。
技术调研之后,我们一致觉得可行,事实证明确实可行。

服务划分

服务划分粒度不能太大,也不能太小,强联系的业务放在一个服务当中。那么这又会出现一个问题,强联系业务放在一个服务当中,这个服务有可能不知不觉就会变大。我总结了几个服务划分心得:

  • 每一张数据表只能由一个服务访问
  • 一个服务开发周期大致在两人两周的工作量
  • 尽量避免出现使用分布式事务的场景
  • 按层次划分服务(分层设计

新一代筋斗云服务划分如下:

  • 基础服务
    包括短信服务,缓存服务,消息服务,跟业务没有任何关系,相当于工具包。事实上我们没有所谓的基础服务层,因为这些基础服务我们使用的也是第三方服务,以阿里提供的服务为主。所以我们没有单独部署服务提供这些基础服务,而是把相关服务封装到一个个module当中,上层的服务直接在pom里面引入直接调用接口服务即可。后期如果业务量起来,需要自己实现这些基础服务,可以直接自己实现,然后在调用的地方把本地调用直接替换即可。由于前期设计的时候也都是接口调用,切换成远程服务调用也是非常方便的。
  • 共享服务
    这里面的服务有一个共同的特点:以提供服务为主,相互之间基本没有调用。这个也是我们服务划分要达到的目的。
    智能设备连接服务:负责服务端管理与硬件的tcp连接服务,向智能硬件发送指令,接收信息,以及相关解码。
    车辆管理服务:调用智能设备连接服务,管理车辆相关所有功能。
    订单服务:所有租车订单相关的服务。
    充值服务:所有用户进行微信,支付宝相关的充值,退款等服务。
    用户服务:所有用户相关的管理,包括外部用户和公司内部管理人员。
    校区服务:所有校区相关管理,例如电子栅栏,校区优惠活动,校区结算等等。
    工单服务:车辆维护的服务。
  • 业务服务:
    这里面的服务的共同点是:调用共享服务和基础服务,除了分析服务提供服务,其他三个都不提供服务,直接对外暴露业务。那么为什么把分析服务放在业务服务层呢?因为它也是强依赖各种共享服务和基础服务,属于业务服务内的低一层。
    用户租车服务:处理用户端app的所有业务流程。
    后台管理服务:处理浏览器端的管理平台业务流程。
    运维管理服务:处理移动运维管理平台的业务流程。与后台管理服务有部分业务重合,侧重点不同。

    服务实现

    既然决定使用微服务架构,而且服务也已经划分好,接着就确定微服务框架了。我们主要考虑了两个微服务框架:spring cloud阿里云edas
    最终我们选择了阿里云的edas。主要基于以下几个原因:
  • 使用简单,结合spring,服务提供方配置发布服务,服务消费方直接注入接口类即可,无需其他配置。其实RMI hessian等用起来也非常简单。
  • 大公司出品,文档比较全,应该不会有问题。毕竟是付费服务,毕竟是阿里出品,事实证明开发起来虽然遇到一些问题,大多数自己都解决了。
  • 省事,很多工作edas都包了,基本上每个人只要集中在开发自己的业务上面。
    首先,部署非常省事,使用edas控制台,部署只要上传应用,剩下的选项配置一下马上就发布,而且可以分批发布,回滚也是一键回滚,可以回滚到以前的任意版本。
    其次,服务调用安全性不用管,已经做好了,只需要在edas下添加ecs即可,然后只要在上面部署服务就行,剩下的不需要考虑。
    服务监控也可以不用做,有现成的,各种性能都有监控。
    rpc通信使用的是hsf,据说是dubbo的升级版,基于tcp,效率更高,具体不得知。
    edas原理有一篇文章讲的非常好。

服务九月底上线,目前趋于稳定,没有因为edas出现过宕机,可以说,稳定性没有问题。这里没有打广告,真心觉得。

数据库

数据库运行到后来出现的问题是有两个表逐渐变大,订单表和充值表。基本上订单表是充值表数据的五倍。其实历史订单数据查询需求比较少,比较热的数据都是最近几天生成的,可是随着表格数据的不断增大,插入和修改数据变得越来越慢。解决方法想到两个:1把旧数据存到其他地方,始终保持主业务使用的表格数据量控制在一个较小的范围内。2分库分表。两个方法都是可行的,第一个方法比较麻烦,最终选择了第二个方案。
最终使用阿里云的一个中间件–drds。这相当于在应用和mysql服务器中加了一层。拦截sql请求,根据sql请求的具体内容,如果有分库分表键,则把请求发送到指定的数据库实例上,然后把返回内容发送给应用。或者有些sql指令无法确定数据库实例或者请求需要在多个数据库实例上执行,那么就需要把请求发送到多个数据库上,然后再把请求回来的数据组装,返回给应用。总之,这些就是drds做的工作,而服务调用方,只需要在创建表格的时候指定好分库分表键,请求的时候带上分库分表键就行了。还有一点就是有些mysql自带的函数没法用了,这个也很好理解,因为你的请求有可能发送到好几个mysql上,有些参数必须保持一致,如果函数在多个mysql上执行,得到的就有可能不准确了。
所以,在使用了drds之后,数据库性能问题也解决了。

应用中的orm框架不再是单独使用hibernate,其中有很大一部分切换成了mybatis,这个也是根据开发人员的习惯选择的。这也是微服务的另一个好处,技术选型不用是规定哪个就是哪个,不同的服务可以选择不同的技术,只要给其他服务提供标准的接口即可。

再说说数据迁移方案,在迁移的时候,数据量总共有220多万条数据,我们的迁移方案是把老库上的数据表原封不动的复制到新库上,然后在新库上面建好新表,直接在控制台里面用sql语句把旧表当中的数据复制到新表里面。当时也想过用脚本迁移,但是速度太慢,利用sql迁移,整个过程控制在了一分钟之内。

终端应用

终端应用还是分为三大块,这个没有变化。值得一说的是我们的管理后台在一位大神的带领下进行了重构。新的框架是基于vue+element实现的,完全没有使用模板引擎,真正实现了前后端分离,并且前端代码部署在整个服务外的一台机器上作为静态资源访问。最重要的是前端也是完全面向对象的编程模式,可以说非常先进,不说了,还没消化完。

部署


每个服务启动后都会向注册中心注册服务,服务调用不会经过注册中心,服务之间直接调用。这也是去中心化的思想。
每个服务基本上部署两份,edas会自动把请求分配到两个服务中,而且部署的时候可以分批部署,这样就可以做到平滑部署。

总结

以上就是筋斗云最初的单体应用进化成微服务架构的整个过程。由于历史包袱比较小,整个系统升级也是非常顺利。服务器也从最初的两台扩展到现在的19台。以前如果要改个什么小东西整个应用都会受到影响,现在可以单独部署,局部部署,还可以分批部署,基本没有什么感知。而且整个应用也逐渐趋向稳定,横向扩容也就是增加节点部署应用即可。如果想要增加新的功能,不再向以前一样思前想后,可以在现有的应用上增加就在现有的应用上增加,如果不行,可以直接增加一个服务,非常方便。当然,微服务也会带来其他的一些影响,例如如果某个应用发生了变化,影响到其他服务,比如一个枚举类型新增了一项,如果跟其相关的服务没有部署,那么马上就会报错,客服电话就会马上反应出来。而且部署的顺序有时候也要注意。这些都是非常需要注意的地方。
微服务的理念可以说现在也有了一定的认识,我觉得系统变大,业务变多,服务拆分是未来的趋势,大型互联网在这方面走的也是比较前的,这也是值得我们未来探究的一个重要的方向。