当前位置: 首页 > Linux

马蜂窝火车票系统服务转型初探

时间:2023-04-06 22:47:05 Linux

交通方式是用户出行前应该考虑的核心要素之一。为了帮助用户更好地完成消费决策的闭环,马蜂窝推出了大交通服务。现在,用户还可以在马蜂窝完成购买机票、火车票等操作。和大多数业务系统一样,我们也经历过从无到有,然后快速发展的过程。本文将以火车票业务系统为例,主要从技术角度,与大家分享一些新兴业务发展不同阶段背后的系统建设和架构演进的经验。第一阶段:从无到有这一阶段的首要目标是快速支撑业务,填补业务空白。基于这样的考虑,当时的火车票业务模式是供应商代购;从技术角度来说,需要在马蜂窝App中优先实现用户查询剩余火车票信息、购票、支付、退票、退票等核心功能。功能开发。图1-核心功能及流程技术架构综合考虑项目目标、时间、成本、人力等因素,我们当时的网站服务架构选择了LNMP(Linux系统下的Nginx+MySQL+PHP)。从物理层来看,整个系统分为接入层(App、H5、PC运行后台)、接入层(Nginx)、应用层(PHP程序)、中间件层(MQ、ElasticSearch)、存储层(MySQL、Redis)).对外部系统的依赖主要包括公司内部的支付、对账、订单中心等二方系统,以及外部供应商系统。图2——火车票系统V1.0的技术架构如图所示。外显功能主要分为两部分,一是C端App和H5,二是运营后台。两者分别通过外网Nginx和内网Nginx连接到phptrain应用。程序内部主要有四个入口,分别是:其他二方系统调用的门面模块、运营后台调用的admin模块、处理App和H5请求的train核心模块、外部供应商回调模块。这四个入口将依赖于下层模块模块实现各自的功能。外部调用有两种情况,一种是调用二方系统的门面模块,满足公司内部依赖;另一种是调用外部供应商。基础设施依赖于搜索、消息中间件、数据库、缓存等,这是典型的单体架构模型,部署简单,层次结构清晰,实现简单快速,能够满足初期产品快速迭代的需求,并且基于公司相对成熟的PHP技术,因此无需过多担心稳定性和可靠性问题。这个架构支撑了近一年的火车票业务的发展。简单易维护的架构为火车票业务的快速发展做出了巨大贡献。但随着业务的推进,单体架构的缺陷逐渐暴露出来:所有功能聚合在一起,代码修改重构成本增加,研发团队规模逐渐扩大。很难评估。在自动化测试机制不完善的情况下,容易导致“修得越多,缺陷越多”的恶性循环扩展性差,只能横向扩展,不能按模块纵向扩展可靠性差,一个bug可能导致系统崩溃且阻碍技术创新,升级难,小改小改,牵一发而动全身。为了解决单体架构带来的一系列问题,我们开始尝试向微服务架构演进,并将其作为后续系统建设的方向。第二阶段:结构转型和服务化初步研究从2018年开始,整个大运业务开始从LNMP结构向服务化演进。架构转型——从单体应用到服务“工欲善其事,必先利其器”。首先,简单介绍一下大通在实施服务过程中积累的一些核心设施和部件。我们主要的变化是把开发语言从PHP改成Java,所以技术选型主要围绕Java生态系统展开。开发框架及组件图3-大流量基础组件如上图所示,整体开发框架及组件自下而上分为四层。这里主要介绍顶层大流量业务场景的封装框架和组件层:mlang:大流量内部工具包mes-client-starter:大流量MES技术埋点采集上报dubbo-filter:跟踪Dubbo调用的mratelimit:API限流保护组件deploy-starter:部署流量提取工具tul:统一登录组件cat-client:增强封装CAT调用的统一组件基础设施系统服务的实现离不开基础设施系统的支持。基于公司现有基础,我们先后构建了:敏捷基础设施:基于Kubernetes和Docker基础设施监控告警:Zabbix、Prometheus、Grafana业务告警:基于ES日志、MES埋点+TAlarm日志系统:ELKCI/CD系统:基于Gitlab+Jekins+Docker+Kubernetes配置中心:Apollo服务支持:Springboot2.x+Dubbo服务治理:Dubbo-admin、灰度控制TOMPS:公交应用管理平台MPC消息中心:基于RocketMQ定时任务:基于Elastic-JobAPM系统:PinPoint、CATPHP、Java双向互通支持如前所述,初步搭建了一个比较完整的DevOps+微服务开发体系。整体架构如下:图4——大交通基础设施体系从上到下分为:接入层——目前有App、H5和微信小程序;接入层——使用公司的公共Nginx、OpenResty;业务层应用——包括无线API、Dubbo服务、消息中心和定时任务——部署在Kubernetes+Docker中间件层——包括ElasticSearch、RocketMQ和其他存储层——MySQL、Redis、FastDFS、HBase等。此外,外设支持系统包括CI/CD、服务治理与配置、APM系统、日志系统、监控告警系统。CI/CD系统CI基于Sonar+Maven(依赖检查、版本检查、编译打包)+JekinsCD基于Jekins+Docker+Kubernetes。Prod开发的OPS权限我们暂未放行,计划在新版CD系统中逐步开放。图5-CI/CD系统服务框架Dubbo我们选择Dubbo作为分布式微服务框架主要有以下考虑:一个成熟的高性能分布式框架。目前很多公司都在使用,经受住了各种性能测试,比较稳定;它可以与Spring框架无缝集成。我们的架构是基于Spring的,连接Dubbo时,代码可以被侵入,访问非常方便;具有服务注册、发现、路由、负载均衡、服务降级、权重调整等能力;代码是开源的。可以根据需要定制,扩展功能,进行自主开发图6-Dubbo架构除了与Dubbo官方和社区保持密切联系外,我们也在不断增强和完善Dubbo,比如基于dubbo的日志跟踪-fitler、Dubbo基于大流量统一应用管理中心的统一配置管理和服务治理体系建设。服务化初探——抢票系统向服务化演进一定不是大跃进,只会把应用割裂成碎片,最终不仅大大增加运维成本,而且看不到任何收益。为了保证整个系统向服务化演进更加顺畅,我们首先选择了抢票系统进行实际探索。抢票是火车票业务的重要组成部分,抢票业务相对独立,与现有的PHP电子票业务冲突较少。这是我们实现服务的更好场景。在拆分和设计抢票系统的服务时,我们积累了一些经验和心得,主要与大家分享以下几点。功能与边界简单来说,抢票就是用户提前下单抢票,正式开售后系统继续为用户尝试抢票的过程。抢票本质上是一种通过不断检测所选日期和车次的余票信息,在余票时为用户发起占座的方法。至于占座成功后的处理,与普通电子客票无异。了解了这个过程后,在尽可能不改变原有PHP系统的前提下,我们将它们之间的功能边界划分如下:占座成功后,我们将后续的出票工作交给PHP电子客票系统。同理,在抢票的反面,只需要实现“未抢票全额退票”和“抢票差价退票”功能即可。已出票的线上退票和线下退票均由PHP系统完成。这大大减少了抢票的开发任务。服务设计服务的设计原则包括隔离、自治、单一职责、限界上下文、异步通信、独立部署等,其他部分相对容易控制,限界上下文一般反映了服务的粒度,这也是做服务拆分时绕不开的话题。如果粒度过大,会出现类似单体架构的问题。如果粒度太细,会受到业务和团队规模的限制。结合实际情况,我们从两个维度拆分抢票系统:1、从业务角度,系统分为供应商服务(同步和推送)、正向交易服务、反向交易服务、赛事服务。图8-抢票服务设计正向交易服务:包括抢票、支付、退票、出票、查询、通知等功能反向交易服务:包括反向下单、退款、退款、查询、通知等功能供应方:请求资源方完成相应的业务操作,订单抢票、取消、占座、出票等活动服务:包括日常活动、分享、活动排名统计等。2.从系统层面,分为前端H5层分离,API访问层,RPC服务层,PHP之间的桥接层,异步消息处理,定时任务,供应商外部调用,推送网关。图9抢票系统分层展示层:H5和小程序,前后端分离API层:为H5和小程序提供统一的API入口网关,负责后台服务的聚合,服务的暴露HTTPREST风格层:包括上一节提到的业务服务,对外提供RPC服务桥接层:包括调用PHP代理服务,在Java端提供DubboRPC服务,以HTTP形式调用统一的PHP内部网关;PHP提供统一的GW,PHP以HTTP的形式通过GW调用Java服务。消息层:异步消息处理程序,包括订单状态变化通知、优惠券等处理定时任务层:提供各种补偿任务,或业务轮询处理数据元素对于交易系统,无论使用什么语言和架构,核心要考虑的部分是数据。数据结构基本上反映了业务模型,也影响着程序的设计、开发、扩展、升级和维护。下面简单梳理一下抢票系统涉及的核心数据表:1.订单创建链接:用户选择车次进入填表页面后,需要选择乘客并添加联系人,所以乘客表将首先涉及。块复用PHP电子票功能2、用户提交创建订单申请后,会涉及到以下数据表:订单快照表——首先存储用户的创建订单请求元素,抢票订单表(order):为用户创建抢票段表(segment):用于一个订单中可能存在的多趟车次连续乘坐(类似于车票段)乘客表(passenger):抢票表包括乘坐人信息活动表(activity):反映订单可能包含的活动信息项目表(item):反映机票、保险等信息性能表:用户购买机票和保险后,最后会回填票号,我们也称之为票号信息表3.占座结果生成后:用户占座失败涉及全额退款,占座成功可能涉及退差价,所以我们会有一个退款单(退款订单);虽然只涉及退款,但是也会有一个refund_item表来记录退款明细。订单状态订单系统的核心点是订单状态的定义和流转,这两个要素贯穿于订单的整个生命周期。我们从以往的系统体验中总结出两大痛点。一是订单状态定义复杂。我们尝试用一个状态字段来连接前端展示和后端逻辑处理,导致单笔订单状态多达18个;二是状态流逻辑复杂,流的前置因素判断和后置方向的ifelse判断过多,代码维护成本高。因此,我们使用有限状态机来梳理抢票远期订单的状态和状态流。关于状态机的应用,可以参考之前的一篇文章《状态机在马蜂窝机票交易系统中的应用与优化》。下图是抢票订单的状态流图:图10-抢票订单状态流我们将状态分为订单状态和支付状态,通过事件机制促进状态的传递。达到目标状态有两个先决条件:一是原始状态,二是触发事件。状态按照预设的条件和路由流动,业务逻辑的执行和事件触发与状态流分离,达到解耦的目的,便于扩展维护。如上图所示,订单状态定义为:初始化→下单成功→交易成功→关闭。支付状态定义为:初始化→待支付→已支付→已关闭。正常情况下,用户下单成功后,会进入下单成功和待付款;用户通过收银台支付后,订单状态不变,支付状态为已支付;之后系统会开始帮助用户占座,占座成功后,订单进入交易成功,支付状态不变。如果只是上面的双态,那么业务程序的执行是简单的,但是无法满足前台给用户丰富的单态展示,所以我们也会记录下订单的关闭原因。目前关闭订单的原因有7种:未关闭、创建订单失败、用户注销、支付超时、操作关闭订单、订单到期、抢票失败。我们会根据订单状态、支付状态、关闭订单原因计算出订单的对外展示状态。幂等性设计所谓幂等性,就是对一个接口的一次调用和多次调用的结果应该是一致的。幂等性是系统设计中高可用和容错的有效保证,它不仅仅存在于分布式系统中。我们知道,在HTTP中,GET接口天生就是幂等的。多次执行GET操作不会对系统数据产生不一致的影响,但重复调用POST、PUT和DELETE可能会产生不一致的结果。具体到我们的订单状态,上面说了,状态机的流程需要通过事件来触发。目前抢票的正向触发事件有:下单成功、支付成功、占座成功、关闭订单、关闭支付订单等。我们的事件一般是由用户操作或者异步消息推送触发的,这两者都避免不了重复请求的可能。以占座成功事件为例,除了修改自身表的状态外,还需要将状态与订单中心同步,订单信息与PHP电子客票同步。如果不进行幂等控制,后果会很严重。有很多方法可以确保幂等性。以占座消息为例,我们有两个措施保证幂等性:所有占座消息都有一个唯一的serialNo,受协议约束,推送服务可以判断消息是否被正常处理。业务端的修改实现了CAS(CompareAndSwap),简单来说就是数据库乐观锁,比如updateordersetorder_status=2whereorder_id=『1234'andorder_status=1andpay_status=2。有一定的成本,需要一定的人员基础和基础设施。在起步阶段,从一个相对独立的新业务入手,做好与老系统的集成复用,才能快速出结果。抢票系统在不到一个月的时间内就完成了产品设计、开发联调、测试上线,也很好地印证了这一点。第三阶段:服务提升和系统能力提升抢票系统建设的完成对我们来说是一小步,也只是一小步。毕竟抢票是周期性的生意。更多时候,电子客票是我们业务量的主要支撑。新旧系统并行期间,主要存在以下痛点:由于当时的因素,原有的电子客票系统与特定的供应商紧密绑定,受到供应商的极大限制;高昂的兼容性成本使得我们很难实现统一的链路跟踪、环境隔离、监控告警;PHP和Java桥接层承担的服务过多,性能无法保证。因此,我们应该卸下历史包袱,尽快完善旧制度。服务化迁移,统一技术栈,更强大的系统对各大业务的支持是我们下一步的目标。与业务同行:电子客票流程改造我们希望通过电子客票流程的改造,重塑之前建立在应急模式下的火车票项目,最终实现以下目标:建立马蜂窝火车票的业务规则,改变以往的业务功能和流程服从供应方规则;提升用户体验和功能,新增在线选座功能,优化搜索和下单流程,优化退票速度,提升用户体验;提高数据指标和稳定性,引入新的供给侧服务,提高可靠性;供应商订单分配系统,提高座位成功率和出票率;技术上完成Java服务的迁移,为后续业务打下基础。我们需要完成的不仅仅是技术重构,而是结合新的业务需求,不断丰富新的系统,努力实现业务和技术目标的一致性。所以我们会结合服务迁移和业务系统建设来推进。下图为火车票电子票流程改造后的整体结构:图11-电子票部分改造后的火车票结构。除了类似于抢票系统的供应商接入、正向交易、反向交易外,还包括搜索和基础数据系统,供应端也增加了电子票的业务功能。同时,我们也建立了新的运营后台,保证运营支持的连续性。在项目实施过程中,除了抢票提到的一些问题外,我们还重点解决了以下问题。对于搜索优化,我们先看看用户在一次站内搜索中可能经过的系统:图12-站内查询调用流程请求先到twlapi层,再到tsearch查询服务,从tsearch到tjs访问服务,再到供应端,整个调用环节还是比较长的。如果每次调用都是全链路调用,结果就不太乐观了。所以tsearch对查询结果有一个redis缓存,缓存也是缩短链接,提高性能的关键。缓存网站查询有几个难点:对实时数据的要求非常高。核心是剩余票数。如果数据不是实时的,用户下单占座的成功率会很低,数据也会很分散。比如出发站、到达站、出发日期、缓存命中率不高,供给端接口不稳定。综合以上因素,我们将tsearch站点搜索流程设计如下:图13-搜索设计流程如图所示,首先对于一个查询条件,我们会缓存多个渠道的结果。另一方面,比较哪个通道的结果更准确,可以提高系统可靠性和缓存命中率。我们设置Redis的过期时间为10分钟,缓存结果定义的有效期为10s,有效期先取;如果有效性为空,则取无效;如果失效也为空,则同步时限为3s调用通道获取,同时将失效和不存在的缓存通道交给异步任务进行更新。请注意,分布式锁用于防止通道结果的并发更新。最终缓存结果如下:缓存命中率将在96%以上,RT平均在500ms左右,既能保证良好的用户体验,又能做到数据更新及时。消息的消费我们有大量的业务是通过异步消息来处理的,比如订单状态变化消息、座位占用通知消息、支付消息等等。除了正常的消息消费,还有一些特殊的场景,比如顺序消费、事务消费、重复消费等,主要是基于RocketMQ实现的。顺序消费主要用于对消息有顺序依赖的场景,例如订单创建消息必须在占座消息之前处理。RocketMQ本身支持消息的顺序消费,我们基于它来实现这个业务场景。原理上也很简单。RocketMQ限制生产者只能向一个队列发送消息,限制消费者只能有一个线程读取。这样全局的单队列单消费者保证了消息的顺序消费。重复消费RocketMQ保证AtLeastOnce,但不保证ExactlyOnlyOnce。前面我们也提到了要求业务端保持幂等性,通过数据库表message_produce_record和message_consume_record来保证准确的一次性交付和消费结果确认。事务性消费基于RocketMQ的事务性消息功能。支持两阶段提交,先发送预处理消息,然后回调执行本地事务,最后提交或回滚,有助于保证修改数据库的信息与发送异步消息一致。灰度运营的歼10战斗机总设计师曾说过:“造飞机不是最难的事,最难的是让它飞上天”,我们也一样。3月份是春游旺季,业务量逐日增加。在此期间,完成了一次重大的系统切换。我们需要一个完整的解决方案来保证业务的顺利切换。方案设计的灰度分为白名单部分和百分比灰度部分。我们内部先进行白名单灰度,稳定后进入20%流量灰度期。Grayscale的核心是入口问题。由于这次前端也进行了全面改版,我们从网站搜索入口向用户介绍不同的页面,用户将分别在新旧系统中完成业务。图14-灰度运营计划搜索订单流程App在站点搜索入口调用灰度接口获取跳转地址,实现入口导流。图15-与最近的计划相比,搜索和订单转移的效果。我们只是初步实现了火车票业务线的服务落地。同时,还有一些东西是我们未来会继续推进和完善的:1.服务粒度细化:目前的服务粒度还是比较粗糙的。随着功能的不断增加,细化粒度是我们改进的重点,比如将交易服务拆分为订单查询服务、订单创建服务、占座服务、出票服务等。这也是DevOps的必然趋势。只有细粒度的服务才能最大程度满足我们快速开发、快速部署、风险控制的需求。2、服务资源隔离:仅仅做到服务粒度上的隔离是不够的。DB隔离、缓存隔离、MQ隔离也是非常必要的。随着系统的不断扩展和数据量的增长,资源的细粒度隔离是另一个重要的点。3、灰度多版本发布:目前我们的灰度策略只能支持新旧版本并行。未来除了多版本并行验证,我们还会结合业务定制化需求,让灰度策略更加灵活。写在最后,业务的发展离不开技术的发展。同样,技术的发展也必须充分考虑当时场景中业务的现状和条件,两者相辅相成。我们需要避免过度设计而不是设计不足。技术架构是演进的,不是一开始就设计的。我们需要根据业务发展的规律,分阶段分解长期的技术方案,逐步实现目标。同时,也要考虑到服务化会带来很多新的问题,比如复杂度骤增、业务拆分、一致性、服务粒度、长链接、幂等性、性能等等。比服务支撑更难的是服务治理,这是我们都需要深入思考和去做的事情。本文作者:李战平,马蜂窝大流量业务研发技术专家。(题图来源:网络)关注马蜂窝技术,发现更多你想要的内容