专栏名称: InfoQ
有内容的技术社区媒体。
目录
相关文章推荐
深响  ·  这个双11,淘宝商家不怕仅退款了 ·  6 天前  
51好读  ›  专栏  ›  InfoQ

蘑菇街11.11:移动流量猛增,如何设计高并发多终端的无线网关?

InfoQ  · 公众号  · 科技媒体  · 2016-11-11 07:59

正文

作者丨薛晨

编辑丨Cindy

本次双十一,InfoQ推出了《双十一丨电商生态技术创新》特别企划,后续还将登出京东、苏宁、唯品会、一号店等技术干货系列文章。

自2011年上线以来,蘑菇街一直沿用的是以PHP为核心的业务系统架构。但是,随着业务的增长、业务逻辑的复杂化,对技术架构有了更高要求。同时,随着移动互联网的普及,大量用户流量从PC端到无线端快速转移,故而移动端架构在保证稳定性的前提下,支持高效开发和迭代显得尤为重要。

电商的大促业务越来越常态化,双十一作为一年一度的电商大促高峰,更是一年比一年火爆。今年双十一,蘑菇街推出买手(红人)购物清单、红人买手直播、实时榜单等新功能:新的变化对技术上高并发、高可用的考验也越来越大。

MWP无线网关的设计

「美丽联合无线平台」(Meili Wireless Platform,以下简称「MWP」),是美丽联合集团为无线端开发的技术平台。它主要由无线网关以及围绕它开发的一系列技术组件和产品组成,是一套覆盖包括Android、iOS、H5等各类型无线终端技术组件和服务端通用业务开发框架在内的技术解决方案。

MWP面临的问题和挑战

基于PHP的架构是蘑菇街早期使用的,当时开发人员较少,业务逻辑相对简单,在业务迭代过程中更多地将就快速迭代试错,对于业务逻辑之外的系统优化相对比较少人关注。

上图中,客户端发起HTTP请求到公网Proxy,由Proxy将请求转发到业务服务器上,所有蘑菇街的业务都集中在一个PHP服务中,请求路径非常简单,基于这种架构的开发和运维也非常简单。从代码角度讲,所有业务代码都在一个工程里,相互调用都是简单的内部调用,使用非常方便,从系统角度讲,基于这种简单的系统架构排查定位问题也会降低很多难度,可以快速反应。

上述的架构在早期比较长一段时间都在使用,后面尝试过在服务化下对业务应用做一些简单的隔离,但是没有对架构有根本性的改变。这样的架构在当时的确行之非常有效,但是随着业务和团队成员的增长,越来越多的问题暴露出来。

  • 整体性能较差

从宏观的角度来看,对用户来说,性能上其中一个明显的表现就在于客户端请求的RT上。有研究显示,移动端用户在点开一个页面的时候,如果RT超过5秒,有74%的用户将会选择离开页面。在原架构中,客户端请求链路只支持HTTP短连接的方式,短链接的建立和释放会消耗很多时间,特别是移动端用户,在复杂的无线网络环境下,带宽资源非常宝贵,频繁地建立HTTP连接,所消耗的资源和时间成本会非常高,基于原有的架构优化的成本会非常高。

  • 存在严重的安全风险

上图中的HTTP请求链路是一个裸露的链路,架构层面没有机制对请求做任务安全校验,如果黑客修改了请求中的数据,业务也能正常走下去,而业务上如果需要接入这种安全校验,比如支付、交易这种高危业务,则需要自己去额外接入。

  • 开发效率随着业务复杂度和人员的增长快速下降

上文提到老架构中,各个业务都在一个工程中,开发人员少的时候开发起来会很方便,但是当业务膨胀之后,里面的大部分业务都从之前的一两个人维护转变为一个团队在维护,在局部代码里会同时有多人在并行开发,随之而来的是沟通成本变高,代码变得臃肿,线上故障也随之频发。随着代码臃肿复杂,也给新的业务迭代带来成本的提高,开发的效率急剧下降。

下面我们来看一下MWP通过怎样的设计来解决这些问题的。

整体设计

MWP提供从客户端到服务端的一整套技术解决方案,包括如下内容。

  • 客户端SDK,为各客户端接入MWP而提供的客户端SDK。

  • MWP-Router,主要为服务端应用提供统一的服务暴露方式,并为客户端的请求提供路由分发机制。

  • Actionlet框架,为基于MWP的服务端业务应用提供统一的开发框架。

  • MWP-DSL,是MWP提供的一套面向业务数据的无线端、前后端分离解决方案。

客户端SDK

MWP-SDK作为客户端上业务访问后端服务的入口,与服务端的架构相对应,分层上将通用网络层和上层应用层分开解耦,最底层的通用网络层包含建联策略、HttpDNS、自有协议等网络优化需要的各方面,上层应用层包含对API请求逻辑、DSL的封装等,这样的分离,对网络层的长期优化是非常必要的。我们将网络能力整体封装成了标准的SDK,一方面将整体优化的收益推广给App群享受,另一方面也将Native通道的能力暴露给H5。

在应用层,基于Pipeline灵活的编排和扩展能力,方便集成了客户端鉴权、离线缓存、防刷、时间纠偏、状态管理、性能上报等功能,实现了不同的网络协议之间实现灵活的切换和降级重试,横向上与其他客户端基础组件打通,比如,与配置中心配合实现配置的准实时下放等。

为了解决页面请求过多、接口回调嵌套等问题,实现了MWP-DSL的调用方式,将原本客户端对数据的处理逻辑放到服务端的DSL层,解放服务端开发,合并客户端请求,减少页面上的网络请求损耗。

应用层的功能扩展和优化都基于网络层的稳定性和安全性上,所以网络层的优化显得尤为重要。由于移动网络的差异化和多样化,使得客户端的网络环境问题依然严峻,我们都或多或少遭遇到各种域名缓存、内容劫持、用户跨网访问缓慢等问题,网络安全性面临考验。

MWP的动态调度集成了HttpDNS组件来解决经常遇到的这些问题。动态调度通过策略的下发来控制客户端使用的协议(长链、短链、是否加密等)、端口等,通过策略优先级选择、不同网络环境下策略表的缓存和后台跑马等方式对建联策略进行优化。

在初期架构中,我们只对重要的接口使用HTTPS,因为传统的HTTPS的整个握手流程是非常繁重的,尤其是在复杂的无线网络环境,往往造成建链过慢,甚至超时的情况;但是从安全的角度考虑,又必须对用户数据的传输建立在一个安全加密的通道之上。

为了解决两者的平衡,我们加入了安全网关接入层,接入层基于长链和自有协议进行数据的传输,并通过合并请求、证书预置和优化加密算法实现了一套基于TLS1.3的0-RTT加密机制。在建链的效率和数据安全上找到平衡,在不牺牲用户体验的基础上,达到了安全传输的目的。另外也正在尝试接入HTTP2.0协议,为客户端网络层带来更多的优化。

MWP-Router

MWP-Router(以下简称Router)是MWP的路由层,它提供多种接入方式及RPC泛化调用方式,基于Servlet 3.0和Pipeline机制提供了高性能高可用的路由服务

作为蘑菇街无线业务的入口,性能和稳定性是最重要的指标。Router是基于Servlet 3.0和Actor模型的全异步架构,AsyncContext和Event-Loop充分发挥了现代cpu的性能,在隔离各个请求资源的同时,用极小的内存换取了最大的吞吐量,灵活的Pipeline机制提供了强大的流程编排能力,结合RPC泛化调用提供了一整套标准的API服务。

Router的其他特性如下:

  • 通过构建符合协议标准的头部信息可以方便集成鉴权、防刷、缓存、时间校准及配置准实时下放等特性。

  • 管理后台通过精细化的配置来管理App和API,包括路由、安全、流控、权限、别名等。

  • 提供定制化的DSL,客户端开发可以根据自己的业务场景任意组装和处理后端服务的元数据供端上展示使用,包括但不仅限于API聚合、API依赖分层、数据分段返回等。

在最初的架构中,Router是基于HTTP协议的,重要信息API使用HTTPS。众所周知,在无线网络复杂而恶劣的环境下,数据安全和用户体验很难取得很好的平衡。为了解决以上问题,最大程度保证用户体验,我们增加了网关接入层来管理连接。

接入层使用自定义协议和App建立长连接,基于我们自己实现的TLS1.3 0-RTT机制来保证建连的效率保障数据的安全,配合session-ticket-reuse、证书预埋、App加固等机制保证了协议本身的高效稳定及安全性。接入层缓存了部分基于连接的协议数据,对于优化网络io的效果也非常明显。

另外,我们也接入了SDPY协议,并正在尝试接入HTTP2.0协议,期间对Nginx性能调优、内核参数调优、协议参数调优等都积累了大量的优化经验,针对当下流行的微信小程序,后续还会接入WebSocket协议。

Actionlet框架

Actionlet框架的目的

MWP为内部调用定义了API泛化调用方式,后端的业务应用若需要接入MWP需要遵循这种调用方式,所以MWP提供了Actionlet框架,为业务应用提供接入MWP的快速便捷方式,同时也为各业务应用带来一些额外的好处。

  • 规范化业务对外输出接口,所有接入MWP的业务都需要按照一定的规则,有统一的输入和输出方式,这也是方便后续对API和应用进行统一管理的前提条件。

  • Pipeline等模式隔离开环境和接入方式对业务逻辑的侵入,如果没有Actionlet框架,各业务开发需要关心请求上下文信息,比如Servlet上下文等,这样可以提高Actionlet业务代码在多端接入方式(Android、iOS、H5等)下的复用。

  • 统一的Actionlet框架可以为一些通用的横向逻辑提供统一实现,各业务开发只要高度关注自己的业务逻辑即可,而不用每个业务都需要接入依赖甚至自己实现这些逻辑,比如用户Session的处理就是一个很好的例子。

Actionlet框架的技术挑战

对于一个对外提供服务的业务应用,最关心的应该是服务的输入和输出,而各个不同业务API的输入输出又会有很大差异。比如,一个注册接口需要输入用户名、密码和其他用户信息,而返回的是是否注册成功的结果,而一个商品列表页则需要输入商品类型,返回的则是一个商品列表以及商品内部详细信息,甚至对应用户信息的复杂数据结构。

在Java这种强类型语言中,如何抽象出一种统一API的规范,满足各种各样不通的业务,又能为外部提供统一的接口模型?

在Actionlet框架的目的里我们提到,业务应用本身应该是关注纯业务代码的实现,但是接入Actionlet的业务应用又是面向最终用户的。那么这里就会有一个矛盾,面向最终用户的接口必然会带上环境的上下文,比如,走Web请求就会有Servlet相关的上下文,甚至HTTP的上下文。怎样处理这些上下文信息,让业务开发能完全关注业务代码的实现,而不用花很多心思在处理请求的上下文上?

在接收到请求时,系统需要处理很多逻辑才会走到业务代码中,比如参数的解析、用户Session的校验、API路由的选择等,这些逻辑串联在一起作为请求处理的前置流程,框架以怎样的方式控制这些流程的执行,又如何支持后续在这些流程中添加或修改?

Actionlet框架的设计

上图中,由MWPBaseService接收请求,下发给ActionletExecutor,ActionletExecutor作为真正的执行器入口。如果要使用整套Actionlet的框架,所有的请求需要由ActionletExecutor为入口来执行,再经过一连串的Valve流程,Valve可以简单理解是拦截器,实际是阀门配合Pipeline做到对流程的控制,最后调用执行具体的业务Actionlet。

  • Pipeline和Valve

Valve是Pipeline中的概念,而这里详细提出来,是因为Actionlet的执行流程中很多功能是通过Valve来实现的。比如,请求的路由RouterValve、请求的执行InvokeValve,都是Valve。

那我们是如何通过Valve来对流程进行定义和控制的呢?其实默认的ActionletExecutor就是基于Pipeline来实现的,它在初始化的时候就预先定义了一组Valve,在请求进来时依序执行各个Valve。如上图中,接收到请求后会依次执行RouterValve、ParameterValve、SessionValve、InvokeValve,而如果后续扩展想改变流程或在流程中加入另外自定义的流程就非常方便了,只要在流程定义的地方修改就可以。

Valve的排列顺序也是有要求的,因为请求是从第一个Valve执行到最后一个,再从最后一个执行到第一个,这是一个责任链模式。但是和拦截器不同,Valve本身还可以做一定的流程控制,比如直接breakPipeline,或直接goto到某个Valve。

  • Actionlet的具体设计

首先,我们先来看一段Actionlet的接口定义。 

从上面的定义中,我们约束了Actionlet的入参parameter和返回的接口ActionResult,强制约束了入参和返回结果只有一个,业务方可以自由定义自己具体的Domain来作为输入和输出,这样做方便使用规约的方式来对外暴露接口,减少要对参数做映射的工作量。而负责在请求Request和返回结果的Response中,这两个Domain将会被序列化和反序列化成Json来进行传输。

那么有人可能会有疑问,大部分业务在获得自己业务输入之外,还会需要一些额外的请求信息,比如客户端来源,甚至HTTP头等数据,在这么严格的封装之下,如何拿到原始的ActionRequest和ActionResponse呢?可以通过Actionlet的上下文ActionletContext来获取,因为目前Actionlet都是同步的请求,所以请求的上下文放在ThreadLocal中。

  • 上下文的隔离

上图中ActionletExecutor配合ActionRequest和ActionResponse,就是为了将环境的上下文抽象出来,从而使Actionlet能更专注在纯业务代码上。

其中,ActionRequest的作用就是将环境上下文中的请求给抽象成通用的模型,比如Servlet中ActionRequest就可以解析HttpServletRequest中的参数,从而封装成可以被Actionlet直接使用的Request。而ActionResponse就是将Actionlet返回的数据结果进行对应环境的输出,比如,Servlet中ActionResponse会将结果进行渲染然后输出给HttpServletResponse。ActionletExecutor就会将整个流程串联起来。

因为Actionlet的业务逻辑可能会对接多个环境实现,那么就可以针对不同的环境来实现不同的ActionletExecutor和相应的Valve,来达到对环境的隔离。

  • 异步Actionlet的支持

上文提到的业务Actionlet都是同步场景下的Actionlet,在大部分场景下同步Actionlet已经满足绝大多数业务的请求,而在很多高并发场景下,异步Actionlet会是更好的选择。Actionlet框架提供了后续提供异步Actionlet的扩展,只需重写现有Actionlet调用的方式即可,对代码侵入性也比较小。

MWP-DSL

MWP-DSL在MWP中提供一套DSL,针对无线端(Android、iOS、H5)中和展现层强相关的业务数据的组装、拼接和转换,集成原有服务端部分Control层代码和客户端View层的代码,本质上是一套面向业务数据的无线端前后端分离解决方案。 

服务端、客户端开发对接场景

客户端没有太多的Control逻辑,也没有太深的回调嵌套回调,服务端的Control层做了很多直接琐碎的直接关系展现层的数据的组装、拼接和转换。客户端一般只有一个大的Callback,数据拿来后直接Mapping,同时整个Activity都会依赖这个Callback。

这种场景下的问题是,客户端没法做到分块加载渲染和BigPipe,同时依赖一个大的Callback,如果后台有任意接口超时会等待很久,此外服务端同学不能专注自己的Module,任何小的需求改动(包括不需要后台提供数据Schema无变更的场景)都需要服务端同学参与,并联调。

服务端同学不为客户端的个性化展示需求做适配和拼接,客户端同学需要自己去调用多个不同服务提供方的多个接口,将Control层的逻辑已Callback嵌套的方式写在客户端。

在这种场景下,客户端同学代码Callback嵌套严重难以维护,三端Control层代码没法复用,任何业务上微小的改动都需要客户端同学发版。

MWP-DSL目的

  • 工程上

  1. 客户端MVC强制分离,避免callback嵌套,提高客户端代码可维护性。

  2. Android、iOS、H5三端Control层逻辑复用。

  3. 客户端Control层逻辑变更不依赖发版,控制力更强。

  4. 专人做专事,面向业务数据的无线领域前后端分离方案,后端同学专注Module层,客户端同学专注在View层和Control层。甚至只要业务需求没有底层数据Schema的改变,完全不需求服务端同学介入,只要相应客户端同学自己组合下数据接口就好,减少前后端联调成本。

  5. 有限的DSL,提高上手速度,可维护性、安全性等。

  • 技术上

  1. MWP-DSL具备热更新的能力。

  2. 通过BigPipe支持分段返回,从而支持客户端诸如Lazy Load等,提升客户端用户体验。

  3. DSL对于MWP异步化和并行改造,提升整体接口性能。

  4. 所谓微服务的可能落地方式。

业界现状

Fackbook GraphQL专注于提供面向业务数据的一种新的数据查询和检索方案,关注点在客户端数据查询的易用性,本质上希望客户端直接通过写类似SQL的方式(但是比SQL更直接,类似于面向数据JSON)来对后台数据(把后台的一个接口类比于数据库中的一张表)做过滤和查询。

相比之下,本质上MWP DSL支持的业务场景更为复杂。

  1. DSL包含大量的业务逻辑,也就是if else和for。

  1. 同时,我们对于元数据的新增和变更比较灵活,而不仅仅是数据的筛选。

所以,和GraphQL的异同可以理解为MapReduce和Hive的区别。

 MWP-DSL的挑战

  • 业务上

  1. 真实业务场景足够复杂,会出现任意N个MWP接口随机组合和callback情况。

  2. MWP-DSL提供的能力如何即受限又足够,同时易扩展,并且易用,也就是学习接入成本低

  • 技术上(主要是性能、稳定性与安全)

  1. 从轻量级MWP请求转发到多MWP组合并运算带来的系统压力。

  2. 为了做到非阻塞、全异步编码,带来的排查问题、线程模型与调度复杂度的增加。

MWP特点(高稳定性、高QPS、低RT、性能问题)会被放大。

MWP-DSL的解决方案

  • MWP-DSL的业务本质(业务模型)

N个接口任意情况组合。

M个flush到客户端(M>1,即为BigPipe的情况)。

T个独立的callback(包含错误的细粒度处理,完全由业务方自己定制)。

三种基本原子情况组合(独立、merge、时序依赖)。

  • DSL客户端编码框架

  1. 多个flush处理各自的callback。

  2. flushkey和BigPipe解耦。

  3. 多个flushkey相互隔离,更细粒度的错误处理。

  • 性能方面

  1. 全异步化与线程调度模型(rxjava、netty eventloop, 多callback仍然交给触发线程,避免加锁的并发控制与线程拷贝)。

  2. 高性能Groovy集成(静态编译执行效率与原生java接近、jvm调优与GroovyClassLoader隔离避免GC问题,与perm区无用类爆炸、Groovy版本自身的bug)。

  • 稳定性方面

  1. 线程隔离,避免极端情况影响MWP。

  2. DSL接口隔离与容错。

  3. DSL相关开关。

  4. 灰度发布与切流量。

  • 安全方面

  1. DSL代码静态扫描,通过白名单和黑名单机制,明确业务方同学用DSL可以做什么和不可以做什么。

  2. DSL接口错误隔离。

  3. DSL接口级别资源控制,比如,限制DSL中的循环次数避免死循环对CPU的消耗,以及对于总体内存的监控与报警。

  4. DSL接口级别性能监控与自动化运维,比如,监控接口rt、自动对异常接口做降级操作等。

MWP上线运行情况

MWP已经上线运行接近一年,集团蘑菇街业务相关的主要服务都已经从老的系统架构迁移到MWP上,目前整体运行非常稳定,对整体服务质量有了很大的提升。

  • 性能方面

MWP目前通过对网络链路做的优化,已经使用长连接的方式替换原来短连接的请求,这样建立连接的资源消耗只在打开或唤醒App的时候产生,而不会每次请求都重新建立连接。在实际应用中,客户端平均RT时间从原来的841毫秒优化到现在的282毫秒,优化非常明显。

  • 安全方面

MWP通过收敛请求链路,在网络链路上做安全校验,防止数据包篡改等安全问题。针对HTTP短链方式,MWP对请求头和数据包进行验签,验签失败的请求直接返回客户端失败信息,而针对长连接,我们自己实现的TLS1.3 0-RTT机制,有效保障数据的安全,并在此技术上做了更多优化。

  • 开发效率方面

通过MWP的路由分发和Actionlet框架,为业务提供快速业务迭代的可能。按照过去的方式,各业务代码杂糅在一起,各业务开发需要考虑请求的上下文信息,比如从移动端过来请求和从PC端过来请求的不同处理方式,参数防篡改,以及安全和反垃圾等。而如今接入MWP之后,各业务应用天然独立,业务开发只需关注业务逻辑,其他的像网络链路、上下文解析等MWP都已经封装处理掉,无需业务关心,有效提升多人协作下的开发效率。

基于MWP的周边生态的建设

支持横向功能的扩展

对于业务系统而言,安全、反垃圾、限流等这些横向的功能是每个业务都需要去考虑和实现的。过去的方式是,每个业务都需要引入一堆的依赖来实现每一个功能,甚至有些功能各个业务都自己实现一套,工作量复杂又冗余,业务开发的注意力被分散在周边逻辑中而不是聚焦在业务逻辑。

而MWP已经实现或者接入了这些功能,对于具体业务开发来说只要接入MWP,默认地或者可以用简单的配置来接入这些功能,非常方便。后续如果有更多的横向功能,只要MWP来实现就可以,业务应用只要拿来就用即可。

外部系统接入

对于客户端App和服务端应用而言,一些周边系统的功能非常重要,比如一个强大的配置中心提供配置管理,方便地修改客户端配置,你想随时推送最新的启动图到客户端,又或者让客户端网络连接方式在HTTP和长连接之间切换。MWP就为这些系统提供了一个强大的平台,支持了这些系统的接入,如目前支持通过通道准实时推送配置等。

API和应用管理

MWP提供了配套的管理后台,对API、DSL、服务端应用和客户端App进行管理。在此基础上,用户可以在后台查看API的QPS、RT这些实时基础数据,方便了解线上运行情况。除此之外,还支持在后台对接口进行简单的测试,以及对客户端权限、接口流控、超时控制等参数进行配置,并实时生效。

未来展望

目前我们正在使用Go重写网关接入层,优化现有的长连接机制。一方面,MWP客户端和服务端之间的链路主要支持上行请求,这样服务端的一些变更只有在客户端主动请求或拉取的时候才能下发到用户,如果能支持下行通道,不管是对于业务的拓展还是系统机制的优化都会打来很大好处。

另一方面,MWP-Router是MWP系统中的重心节点,如果网关接入层足够强大,后续可以轻松地将Router的功能下沉到业务服务,实现去中心化。

MWP是一个基础平台,随着集团业务的发展,还需要围绕这个平台建立更多更完善的功能和系统,以支撑集团业务更长远的业务发展。

作者简介

薛晨,目前就职于蘑菇街无线应用团队,主要负责MWP相关开发工作。

在本次ArchSummit全球架构师峰会北京站,设置了《电商专题:系统架构如何应对业务爆发式增长》和《阿里双11技术架构突破》专题,来深入解读双11等大促背后的技术故事,大会将于2016年12月2日-3日在北京国际会议中心召开,欢迎关注。详情请戳阅读原文!


今日荐文

点击下方图片即可阅读

深度剖析开源分布式监控系统CAT的设计思路



喜欢我们的会点赞,爱我们的会分享!