自从 2011 年蘑菇街上线,蘑菇街一直沿用的是以 PHP 为核心的业务系统架构。但是,随着业务的增长、业务逻辑的复杂化,对技术架构有了更高要求。另外,随着移动互联网的普及,大量用户流量从 PC 端到无线端快速转移,故而移动端架构在保证稳定性的前提下,支持高效开发和迭代显得尤为重要。
而且,电商的大促业务越来越常态化,双十一作为一年一度的电商大促高峰,更是一年比一年火爆。蘑菇街 2016 双十一, 推出买手(红人)购物清单、红人买手直播、实时榜单等新功能。这些新的变化对技术上高并发、高可用的考验自然越来越大。
美丽联合无线平台(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。
Valve 是 Pipeline 中的概念,而这里详细提出来,是因为 Actionlet 的执行流程中很多功能是通过 Valve 来实现的。比如,请求的路由 RouterValve、请求的执行 InvokeValve,都是 Valve。
那我们是如何通过 Valve 来对流程进行定义和控制的呢?其实默认的 ActionletExecutor 就是基于 Pipeline 来实现的,它在初始化的时候就预先定义了一组 Valve,在请求进来时依序执行各个 Valve。如上图中,接收到请求后会依次执行 RouterValve、ParameterValve、SessionValve、InvokeValve,而如果后续扩展想改变流程或在流程中加入另外自定义的流程就非常方便了,只要在流程定义的地方修改就可以。
Valve 的排列顺序也是有要求的,因为请求是从第一个 Valve 执行到最后一个,再从最后一个执行到第一个,这是一个责任链模式。但是和拦截器不同,Valve 本身还可以做一定的流程控制,比如直接 breakPipeline,或直接 goto 到某个 Valve。
首先,我们先来看一段 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 调用的方式即可,对代码侵入性也比较小。
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 目的
-
客户端 MVC 强制分离,避免 callback 嵌套,提高客户端代码可维护性。
-
Android、iOS、H5 三端 Control 层逻辑复用。
-
客户端 Control 层逻辑变更不依赖发版,控制力更强。
-
专人做专事,面向业务数据的无线领域前后端分离方案,后端同学专注 Module 层,客户端同学专注在 View 层和 Control 层。甚至只要业务需求没有底层数据 Schema 的改变,完全不需求服务端同学介入,只要相应客户端同学自己组合下数据接口就好,减少前后端联调成本。
-
通过 BigPipe 支持分段返回,从而支持客户端诸如 Lazy Load 等,提升客户端用户体验。
-
DSL 对于 MWP 异步化和并行改造,提升整体接口性能。
业界现状
Fackbook GraphQL 专注于提供面向业务数据的一种新的数据查询和检索方案,关注点在客户端数据查询的易用性,本质上希望客户端直接通过写类似 SQL 的方式(但是比 SQL 更直接,类似于面向数据 JSON)来对后台数据(把后台的一个接口类比于数据库中的一张表)做过滤和查询。
相比之下,本质上 MWP DSL 支持的业务场景更为复杂。
-
DSL 包含大量的业务逻辑,也就是 if else 和 for。
-
同时,我们对于元数据的新增和变更比较灵活,而不仅仅是数据的筛选。
所以,和 GraphQL 的异同可以理解为 MapReduce 和 Hive 的区别。
MWP-DSL 的挑战
-
真实业务场景足够复杂,会出现任意 N 个 MWP 接口随机组合和 callback 情况。
-
MWP-DSL 提供的能力如何即受限又足够,同时易扩展,并且易用,也就是学习接入成本低
-
从轻量级 MWP 请求转发到多 MWP 组合并运算带来的系统压力。
-
为了做到非阻塞、全异步编码,带来的排查问题、线程模型与调度复杂度的增加。
MWP 特点(高稳定性、高 QPS、低 RT、性能问题)会被放大。
MWP-DSL 的解决方案
-
M 个 flush 到客户端(M>1,即为 BigPipe 的情况)。
-
T 个独立的 callback(包含错误的细粒度处理,完全由业务方自己定制)。
-
三种基本原子情况组合(独立、merge、时序依赖)。
-
多个 flushkey 相互隔离,更细粒度的错误处理。
-
全异步化与线程调度模型(rxjava、netty eventloop, 多 callback 仍然交给触发线程,避免加锁的并发控制与线程拷贝)。
-
高性能 Groovy 集成(静态编译执行效率与原生 java 接近、jvm 调优与 GroovyClassLoader 隔离避免 GC 问题,与 perm 区无用类爆炸、Groovy 版本自身的 bug)。
-
DSL 代码静态扫描,通过白名单和黑名单机制,明确业务方同学用 DSL 可以做什么和不可以做什么。
-
DSL 接口级别资源控制,比如,限制 DSL 中的循环次数避免死循环对 CPU 的消耗,以及对于总体内存的监控与报警。
-
DSL 接口级别性能监控与自动化运维,比如,监控接口 rt、自动对异常接口做降级操作等。
MWP 已经上线运行接近一年,集团蘑菇街业务相关的主要服务都已经从老的系统架构迁移到 MWP 上,目前整体运行非常稳定,对整体服务质量有了很大的提升。
MWP 目前通过对网络链路做的优化,已经使用长连接的方式替换原来短连接的请求,这样建立连接的资源消耗只在打开或唤醒 App 的时候产生,而不会每次请求都重新建立连接。在实际应用中,客户端平均 RT 时间从原来的 841 毫秒优化到现在的 282 毫秒,优化非常明显。
MWP 通过收敛请求链路,在网络链路上做安全校验,防止数据包篡改等安全问题。针对 HTTP 短链方式,MWP 对请求头和数据包进行验签,验签失败的请求直接返回客户端失败信息,而针对长连接,我们自己实现的 TLS1.3 0-RTT 机制,有效保障数据的安全,并在此技术上做了更多优化。
通过 MWP 的路由分发和 Actionlet 框架,为业务提供快速业务迭代的可能。按照过去的方式,各业务代码杂糅在一起,各业务开发需要考虑请求的上下文信息,比如从移动端过来请求和从 PC 端过来请求的不同处理方式,参数防篡改,以及安全和反垃圾等。而如今接入 MWP 之后,各业务应用天然独立,业务开发只需关注业务逻辑,其他的像网络链路、上下文解析等 MWP 都已经封装处理掉,无需业务关心,有效提升多人协作下的开发效率。
支持横向功能的扩展
对于业务系统而言,安全、反垃圾、限流等这些横向的功能是每个业务都需要去考虑和实现的。过去的方式是,每个业务都需要引入一堆的依赖来实现每一个功能,甚至有些功能各个业务都自己实现一套,工作量复杂又冗余,业务开发的注意力被分散在周边逻辑中而不是聚焦在业务逻辑。而 MWP 已经实现或者接入了这些功能,对于具体业务开发来说只要接入 MWP,默认地或者可以用简单的配置来接入这些功能,非常方便。后续如果有更多的横向功能,只要 MWP 来实现就可以,业务应用只要拿来就用即可。
外部系统接入
对于客户端 App 和服务端应用而言,一些周边系统的功能非常重要,比如一个强大的配置中心提供配置管理,方便地修改客户端配置,你想随时推送最新的启动图到客户端,又或者让客户端网络连接方式在 HTTP 和长连接之间切换。MWP 就为这些系统提供了一个强大的平台,支持了这些系统的接入,如目前支持通过通道准实时推送配置等。
API 和应用管理
MWP 提供了配套的管理后台,对 API、DSL、服务端应用和客户端 App 进行管理。在此基础上,用户可以在后台查看 API 的 QPS、RT 这些实时基础数据,方便了解线上运行情况。除此之外,还支持在后台对接口进行简单的测试,以及对客户端权限、接口流控、超时控制等参数进行配置,并实时生效。
目前我们正在使用 Go 重写网关接入层,优化现有的长连接机制。一方面,MWP 客户端和服务端之间的链路主要支持上行请求,这样服务端的一些变更只有在客户端主动请求或拉取的时候才能下发到用户,如果能支持下行通道,不管是对于业务的拓展还是系统机制的优化都会打来很大好处。另一方面,MWP-Router 是 MWP 系统中的重心节点,如果网关接入层足够强大,后续可以轻松地将 Router 的功能下沉到业务服务,实现去中心化。
MWP 是一个基础平台,随着集团业务的发展,还需要围绕这个平台建立更多更完善的功能和系统,以支撑集团业务更长远的业务发展。
评论