1 项目背景
为了配合 APP 大改版,旧的 APP 后台服务系统因多年维护与需求迭代变更,造成维护成本越来越高,且不能满足新版 APP 的很多功能。因此,决定启动乐高系统来承载新版 APP 的所有需求。时间很紧迫,任务也非常重。经过思考后,就有了第一次小组会议的初步架构设想图:
2 项目分析
作为 APP 后台的管理系统,主要就是操作 APP 每个原生页面的具体数据结构。要求各种数据动态配置,各种属性随意修改,在不进行版本升级的前提下,即时生效。简言之,也就是项目负责 APP 页面数据的下发,内容动态配置管理。
为了应对内容数据的可随意更改,可随时生效这个需求,我们觉得 Spring Express Language(后面统一称为 SpEL/EL)特别适合这个业务场景。所以,在系统里大量应用在页面数据调用接口动态取数据所需配置的地方,也成了整套系统的核心价值所在。如下图为 SpEL 在乐高平台的应用形式:
以及对于此类内容输出系统的特点分析如下:
3 架构设计思考
通过上述特点分析,可以知道这是一个典型的重读操作的系统,因此项目分成了两部分:
一个后台管理负责配置基础页面数据,也就是模板的配置信息。
一个负责响应 APP 的接口请求返回页面结构数据,也就是动态渲染 APP 所需要的真实数据结构和内容,包含千变万化的规则需求和运营逻辑。
而新项目需要新的框架去实现,我们拒绝墨守成规,希望适度的引用新技术去驱动业务的发展。因此,选型最新的 Spring Boot2 与 JDK8 配合。Spring Boot2 是基于 Spring5 上集成而来的,Spring5 是目前非常优秀且最新的 Java 应用框架,这个大版本增加了很多新的特性。比如:
JDK 基线更新
核心框架修正
核心容器更新
含 Kotlin 在内的函数式编程
响应式编程模型(重点)
测试改进
库支持
中止支持
而 JDK8 的 Stream,Lambda 表达式等特性的应用,也成了系统开发过程中的常见姿势。具体详细架构如图:
页面数据渲染流程:
请求流:
整套系统有如下优化处理:
整个应对 APP 访问的页面数据渲染系统,不依赖于 MySQL 数据源,所有页面基础数据均读 Redis 缓存数据。
可封装或不封装业务接口,根据业务场景复杂度而定,一切以页面配置简易化为主。所有需要动态配置数据结构的模板元素配置项,都用 EL 表达式来代替。当然虽然这个方式大大提高了动态灵活性,但加大了运维人员的门槛高度,所以,第一版上线的所有表达式配置项,由研发配置。
业务接口熔断处理,基于 Hystrix 的熔断器与方法降级处理,做页面请求接口托底方案。另一个托底方案,是运营可以配置多个相同页面模板,根据用 EL 表达式描述的显示规则来做模板托底方案。
为了提升系统性能,主要做了如下几点优化处理:
页面全局接口基于正则匹配的表达式抽取,相同方法参数统一请求处理,将请求结果保存在 Spring EL 渲染模板的上下文中作为结果变量,将原先配置的表达式替换为从结果变量中提取结果数据。这样就可做到:一个接口相同的方法,只要参数一致,无论在页面中配置了多少处表达式,在单次渲染整个页面周期时,只做一次调用。大大提升了性能,降低了延迟。
异步结果请求。上面所有需要调用接口取数据的表达式,均被分配到了单独一个线程去请求结果,其实保存在上下文中的环境变量就是封装结果的 Future。页面渲染时,会先分配 N 个线程去请求数据,等到后续真正组装结果时,才会去调用 get 阻塞获取结果。当然这样做,提升速度的代价就是极大消耗 CPU 性能,因为线程池需要设置的比较大,而具体设置为多大,还要在不断试验的基础上不断优化调整,以达到服务器最优效能和最大并发支持。
基于 Actor 模型 Reactor 响应式处理页面数据。页面数据是有规则的,也就是多个模板集合组合起来的,而每个模板里又是一个元素集合,每个元素里又是一个属性集合,是个比较规范的数据结构,虽然最大的坑在于层级深度不固定,但是已经用 JSON 去填了这个坑。而针对这个相对规范的数据层级结构,用响应式编程(流式处理)再合适不过了。So,基于 Flux 使用和 Subscriber 的大量使用不再赘述。
数据读取走本机缓存。因为从 Redis 读取速度还不到极致,虽然不读 MySQL,但是每次构建页面数据,读多次 Redis 的网络开销,也是不想耗费的。所以,首要考虑的还是本机内存级缓存的实现。因此,基于 Guava 的 LoadingCache 构建本地缓存方案就集成进来了,当然效果也是非常显著的,但是本地缓存方案并不能长久支撑下去,对于后续支撑业务线的增长,能够支撑超过内存容量的缓存方案还是要考虑进来的!不排除后续随着业务的增长,缓存数据的急剧增长,增加本地硬盘数据缓存以及 ElasticSearch 等支撑系统。
规则链式执行。因规则的多样性,一个页面每个模板甚至于每个元素都是可以去配置各式各样的规则的。比如 A|BTest,坑位优先级,分流,随机等等规则均可以应用在任何模板或元素组上。为了高效执行配置的所有规则逻辑,执行规则前,将规则集创建成规则链。以模板为粒度,以第一个规则执行为起点,执行结束后,判断有无下游规则。进而每个模板被传递给下游规则去执行相应规则逻辑。基于 Reactor 的 Flux 原理图:
至于为什么会选型 Reactor,主要还是基于业务场景的综合考量,对于『多模板->多元素』数据结构渲染很符合流式编程引擎的适用场景。页面规则执行的切入点,分为准备数据和调整数据,如图所示:
4 项目开发中的问题
项目的研发进度还是很快的,这源于团队的高效工作与优秀的执行力。但由于时间紧迫,各业务方提供的接口也参差不齐,返回结构也形态各异。这也是没有一上来就做接口规范所挖的坑。
因此有一个很大的工作量就是重新封装各业务方接口,包装成自己所负责页面方面用表达式 SpEL 描述的样子。而这些工作的确是可以通过事先规范好接口返回标准来避免的。
其次有个很大的工作量就是 SpEL 表达式在各业务页上面的配置,因为需要验证表达式正确性,需要到预发环境验证,因为没有测试环境,对工作效率造成了很大的影响。后来,为了应对这个 SpEL 验证表达式正确与否的问题,专门开发了一个测试接口,才一定程度上解决了这个问题。
最后一个对团队比较有挑战的是管理端 UI 的开发。整个团队是纯后台开发团队,对于前端开发经验较少,为了让全体成员快速进阶全栈工程师,大家也是恶补了以 VUE,ElementUI 为基础的前端基础开发知识。目前系统配置效果如下:
5 回归分析
为了应对需求的动态可配置,采取了页面配置里各种 SpEL 表达式;但造成最大的一个问题就是——运营体验不佳!
“这都是啥呀?完全看不懂!”
“稍微一改,页面就崩了”
“完全不敢动啊,一动就坏!”
各式各样的的吐槽声接憧而至,表现了运营对系统易用性的各种不满。的确,表达式就是代码,看不懂而且稍微改动一个标点符号的确就会造成异常!用户是上帝,如何满足易用性的需求呢!因此进行了一些优化:
表达式配置改为用户选择方法,让 SpEL 表达式在用户看不到的地方动态生成。(这点已基本实现,先配置在系统专门维护的方法表里,然后供页面选择)
尽量剥离系统中业务接口的直接引用,保持系统轻量化。采用 RPC 框架的泛华调用方式,避免 jar 包引入方式。
6 未来规划
关于乐高 2.0 的未来规划,一方面期望以提供平台型页面运营数据输出服务为基础,可视化配置体验以及完整活动页面输出能力。另一方面,拓展支持范畴,例如正在规划和进行的活动支持系统(暂未命名),作为乐高系统群或乐高运营支撑生态的重要一部分也在积极规划调研与实现中。
最后,乐高平台目前仍在积极的接入各部门的页面搭建需求,并提供稳定高效的服务。在欢迎大家接入乐高平台的同时,我们将积极接受来自四面八方的批评与建议,希望稳固迭代,支撑起大家对页面搭建的需求与重任。
评论