前言

我来自于AI Labs运营平台,这些年陆续实现了不少运营需求;在做的过程中,一直在思考运营平台的能力建设问题。

对于业务来说,如何更自然的划分业务与运营平台的边界,简化两边的合作模式;对于运营平台来说,如何快速沉淀运营知识与能力,赋能各个业务方;对于运营人员来说,有没有一种直观的运营能力地图,可以一目了然的查看整个业务板块的可干预功能。

在这个万物互联向前大步推进的新时代,我们已经有了数据互联、服务互联,作为这一切的根基,有没有代码互联?

扩展点尝试着回答以上的问题。

困境

目前AI Labs都是基于微服务架构(详见附录的微服务介绍)进行业务开发,在这种架构下很难做到合作的无缝对接,每个人都会受到自身业务之外的干扰。先来看看当前困扰我们的问题。

业务

运营平台很重要的一个特点是要对接各个垂直业务,垂直业务的发展非常迅速,每时每刻都会有新的垂直业务产生,运营平台很容易成为其中的瓶颈点。记得有一段时期,一下子来了七八个需求,都很急迫,要求一周之内对接好,两周内上线;辛辛苦苦对好接口后,有几个需求又不着急了,有的需求甚至直接砍掉了。

微服务架构下典型的合作场景如下:

cs架构算不算前后端一起的 cs架构一般用什么语言_thinkpad扩展坞怎么用

当垂直业务越来越多,有限的开发资源和无限的运营需求之间的矛盾就会很明显。业务需要等运营平台讨论需求,等运营平台给出微服务接口,等运营平台给出微服务的mock,等运营平台的微服务发布上线。业务等不了时,就干脆自己实现运营需求,或者直接砍掉运营需求,造成运营业务的散乱分布。

这里需要依赖反转。比较好的办法是由各业务方自行声明依赖的服务接口并设置好降级措施;然后运营平台提供统一的接入方式,并自动提供一个空的服务实现,支撑业务方无障碍的继续往下开发。运营平台发现有待实现的服务接口后,投入人力进行开发,最后发布上线,业务方的系统自动从空的服务切换至真实的服务。

技术

从技术的角度,微服务架构下有些棘手的问题:

  • 简单的运营配置变更,如果每次请求都走微服务获取配置,规则计算的成本不抵网络交互、线程调度的成本
  • 大流量的业务调用运营平台,运营平台只需要1%的流量,现有的微服务方案只能按照大流量来准备容量,大流量业务扩容时也需要跟着扩容
  • 在核心链路上,rt要求很高,比如10ms必须处理完毕,现有的微服务方案容易受到网络颠簸、gc、monitor采集等因素的干扰,很难解决
  • 微服务调用时需要将本地对象转换成微服务的参数,都需要序列化和反序列化处理,一次请求30个微服务,每一级无谓的转换太多
  • 工程的粒度不好把控,一个微服务一个工程,太麻烦;多个微服务一个工程,大的小的混一起,又不利于弹性伸缩微服务的远程调用天然带来了复杂度,某些场景下需要服务的本地执行机制。

往前的一小步--扩展点

针对前面提到的问题,我们设计了一套扩展点架构的体系,用于升级原有的微服务体系,这套体系包含:

  • 一套开发框架,解耦运营平台和各个垂直业务;进一步,也可以用来解耦存在依赖关系的任意两个业务。服务接口既可以由运营平台声明也可以由业务方声明;并且只要遵循一定的范式,就不需要显式提供服务接口jar
  • 一套执行引擎,支持服务本地化运行,服务没有对应的提供者时自动降级
  • 一套serverless的容器,服务可以在容器池中自由的迁移,可以组合部署,容器自动进行弹性伸缩

有了扩展点,业务方与运营平台的合作就会是如下的场景:

cs架构算不算前后端一起的 cs架构一般用什么语言_cs架构算不算前后端一起的_02

基于扩展点体系的开发流程如图:

cs架构算不算前后端一起的 cs架构一般用什么语言_怎么做到用定时器不闪屏_03

开发

扩展点架构下,开发模式出现了一系列的变化。

接口声明

扩展点的基础,还是基于接口的服务协议。扩展点对于接口声明的主体不做限制,既可以是业务方;也可以沿用微服务的开发方式,由运营平台提供接口声明。一个简单的扩展点接口示例:

@ExtensionPointerpublic interface ActiveUserService {boolean contains(long activeId, long uid);void remind(long activeId, long uid);}

由业务方进行接口声明,好处是贴近接口使用者,避免无谓的参数转换;并且实现了依赖反转的目的,业务方可以自主接入运营平台,无须运营资源的前置投入。

接口声明完毕,会通过maven插件扫描、代码扫描、启动时由sdk扫描等方式自动上报至扩展点的后台,供运营平台后续开发时使用;对接口的声明者来说,会自动有一个空的接口实现供其调用。

开发环境搭建

要彻底解耦业务方和运营平台,就必须解决扩展点实现时的开发环境搭建问题。如果仅仅只是要求业务方声明接口并提供接口jar,运营平台再基于此接口jar搭建开发环境,和之前的合作模式相比,一切本质上并没有变化。

由业务方声明接口并提供接口jar,会带来jar依赖泛滥的问题,很可能一个接口仅仅用了四个类、接口jar却引入了50个依赖jar,这些额外引入的jar在应用启动时就会是额外的负担;而且,业务出于保密性的考虑,有时并不会将自身的jar deploy到公共库上,业务方提供jar时还需要对原有的jar进行拆分,这就需要额外的工作量。

有没有一种对业务方开发人员的干扰最小并能够自动完成开发环境搭建的办法?扩展点仔细思考了这些问题,目前是通过mock类的方式来形成开发环境:

  • 业务方声明接口后,框架自动进行类mock,并将mock的源码上报至扩展点后台,形成一个轻量的开发环境。
  • 当需要实现某个接口时,从扩展点的后台将开发环境下载到本地,导入ide,进行项目构建,然后就可以很顺畅的进行编码。mock的类,能够支撑开人员进行编译、构建、测试,那要部署运行时怎么办?目前接口的入参和出参都是数据对象,只要坚持都使用pojo类,那mock的类和实际类之间就可以划等号,也就可以作为测试、运行时的支撑。

类mock

类的mock不复杂,有多种方式:

  • maven构建时基于源码进行mock。通过maven插件在打包时获取源码,然后对源码做pojo裁剪,将非pojo特征的代码全部清理掉,插入一些编译用的衔接代码,就得到了需要的类代码;再递归处理每个类的依赖类,直到到达java的原生类、公共库的类,整个类mock即告完成
  • 提交代码至git库时mock。和maven方式类似
  • 运行时基于class mock。通过反射获取到类的字段、方法信息,按照pojo的方式生成类,再递归处理依赖类即可。这些mock出来的类,用来做编译、构建足够了。

接口实现-能力

一个扩展点接口的某种实现类称之为能力,示例代码如下

public class ActiveUserServiceAbility implements ActiveUserService {@Overridepublic boolean contains(long activeId, long uid) {return true;    }@Overridepublic void remind(long activeId, long uid) {}}

接口变更

随着业务发展,肯定会有一天,我们需要调整接口,给老接口增加一个method。扩展点针对这种情况做了兼容处理(需要jdk8),方便大家开发:

  • 接口中自行提供default方法,无须进行干预
@ExtensionPointerpublic interface UserService {default String getName(long uid) {return null;    }}
  • 接口没有提供default方法,扩展点框架会自动进行源码、class增强,为每个接口生成一个装饰接口,然后在扩展点的实现类中插入装饰接口
@ExtensionPointerpublic interface UserService {String getName(long uid);}//增强前public class UserServiceAbility extends UserService {}//装饰接口public interface __AE_Decorator_UserService extends UserService {default String getName(long uid) {return null;    }}//增强后public class UserServiceAbility extends UserService implements __AE_Decorator_UserService {}

有了接口变更的兼容处理机制,业务方就可以增量式的调整接口,扩展点后台会根据业务方的调整自动维持最新的开发环境。另外一方面,运营平台也不用担心业务方的调整导致原有的代码无法编译、运行。

本地化部署

一路很顺畅的搭建完开发环境,喝着咖啡敲完代码,下一步就是部署了。

在微服务架构下,运营平台都是将工程打包成一个大的fat jar,然后部署到镜像容器中,业务方调用时,会发起一个远程rpc请求。为了实现流量漏斗、rt精确控制、运营类配置无损获取等需求,扩展点提出了本地化执行的目标。

扩展点在本地执行时,代码有条不紊的通过cpu的流水线,执行的状态存放在L1、L2 cache、寄存器以及内存中,一切都以纳秒、微秒为量级进行;分布式部署时,一切上升到以毫秒为量级进行。本地化执行的优势很明显:快。

本地化执行很自然的就需要本地化部署,要将扩展点部署到调用方的jvm,有三种模式:

  • 静态打包。业务方打包时,将扩展点代码集成进业务方的jar。每次变更时都需要重新打包部署,不可取。
  • 静态部署。业务应用启动时,将扩展点代码下载到业务方本地,随着业务应用一起启动。每次变更时都需要重启机器,不可取。
  • 运行时部署。是最灵活的一种方式,现代的操作系统、中间件都提供了运行时的动态加载机制,比如动态链接库、apache的moudle机制、java的 classloader机制,技术上是可行的。

运行时部署对业务方没有额外要求,扩展点便采用了此种模式,大概流程如下:

  • 先准备好项目环境的git代码分支
  • 开始部署
  • 将一整套文件,包括源码、properties下发到本地
  • 动态编译class
  • 通过classloader加载class
  • 动态构建出新的扩展点对象
  • 替换掉本地的扩展点对象

jvm多租户隔离

本地化部署,会导致本地的计算资源被占用。比如一段计算密集型代码、死循环。

//造成业务失败的代码while(true){//code    }

业务方需要某种机制防止扩展点过度消耗自身的计算资源。在java世界里,jvm多租户隔离可以用来解决这个问题,java中的JSR121、JSR284、JSR342等规范,都对多租户体系进行了探索。目前阿里的ajdk已经提供了多租户的管控能力,可以为每个租户单独分配cpu、内存。

我们可以在部署时,将扩展点部署在单独的jvm租户上,这个租户的cpu、内存都进行了严格限制,这样就可以避免干扰业务方自身的执行。

本地化的风险

扩展点本地部署,本质就是代码注入,会导致安全风险,目前已知的风险:

  • rt增长,比如执行Thread.sleep(3000),这个可以通过线程池超时机制来控制
  • 数据泄露,接口由业务方声明,参数中包含了隐私信息,需要业务方谨慎提供接口
  • 恶意代码,删除磁盘文件、安装木马、退出jvm,需要使用java的安全控制机制,限制扩展点的执行能力后续再详细讨论这方面的安全措施

原子应用

应用拆分

解决了开发环境的问题,也有了一套基础的部署机制,我们很高兴,现在我们可以按照扩展点的模式进行业务开发了。

随着扩展点越写越多,我们又有新的烦恼了,每个扩展点都需要一个独立的工程作为基本的部署单元,扩展点越多,ide中的工程也就越多。微服务时是那么简洁,现在怎么这么多?如果我们在一个工程中提供三个扩展点,可不可以?部署时怎么处理?

前面搭建开发环境时,我们采用了类依赖递归扫描、挨个mock的方案。能否基于依赖扫描进行应用的自动拆分部署?细想一下,貌似可行。当我们开发完毕,进行代码提交,扩展点框架自动扫描项目工程(例如提供maven插件),收集扩展点实现类的依赖关系,根据依赖关系将应用拆分为若干个独立的原子应用,每个应用包含一个扩展点。

更进一步,我们可以对任何一个java工程进行扫描拆分,将其分解为一个个独立的原子应用。有了这些独立的原子应用,就可以按照最小粒度进行扩展点部署了。

有了应用拆分,就可以按照最自然的方式组织项目工程,于是,在扩展点的天空里,程序员们可以自由翱翔。

状态拆分

拆分原子应用时,一些基础配置,包括dockfile、properties,以及应用启动时的初始化对象需要重点考察。

  • 应用的启动参数,共享。参数由扩展点框架托管,在服务构建时获取,所有的扩展点共享同一套参数
  • properties,共享
  • dockfile,禁用。代码下发本地时已经无法更改镜像容器,服务实现可以在服务初始化时自行处理对系统组件的依赖
  • 初始化对象,受限。每个原子应用都保留一份公共对象(比如各种缓存组件、消息组件),保留扩展点自身能够触达的bean,忽略无法触达的bean。不使用间接的依赖对象,如有需要可以主动声明对bean的依赖对应运营平台来说,目前的扩展点大多是无状态的对象,也没有复杂的依赖关系,状态拆分起比较容易。

容器化

到目前为止,我们的扩展点还只是一个代码注入的框架,我们可以更进一步,吸收微服务的分布式能力,将扩展点进行容器化。

想象一下,系统内部有若干个本地化扩展点,当到达系统容量上限时,将某个扩展点拆分为远程的微服务。每时每刻,系统都会自动进行调度,根据当前的热点调整扩展点部署,扩展点某时在本地,某时在远程。调用方不关心扩展点在哪,提供方也不关心扩展点部署在哪。整个扩展点的部署,都是按需分配。

serverless

serverless架构下,开发再也不用关心具体的部署,扩展点天然就是serverless的架构。

当运营平台开发完毕,经过原子应用的拆分,系统获取到扩展点部署的所有信息,然后将代码下发到业务方应用进行部署。整个过程,运营平台只需要提供代码,自身并不提供服务器。

因为扩展点已经拆分成原子粒度的应用了,每个都可以独立部署,只须放宽扩展点本地化部署的约束,引入某种容器技术,扩展点就具备了serverless的能力;而且,相对于应用粒度的弹性伸缩,扩展点粒度的弹性伸缩,部署速度更快、资源的消耗更少。

目前,阿里内部有不少serverless框架,我们择其一集成即可。

合并部署

可以预见,一些长尾的扩展点,qps很低,可以进行合并部署。合并部署有两种,最简单就是一个jvm启动多个应用。更进一步,需要尝试若干个扩展点合并成一个应用进行部署。

合并的难题在于冲突jar的处理以及同名配置项的处理,没有共同依赖类的扩展点合并最简单;有共同依赖类的扩展点,就需要处理jar的冲突。

大部分共同依赖都是消息、缓存、日志、各种通用框架的jar,可以借鉴阿里pandora容器的思路:由容器统一管控class的加载,所有的扩展点都优先使用容器提供的classs。
通过合并部署,我们可以把相关的几个扩展点部署在一起,更好的控制rt和资源利用率。

代码互联与超级jvm

沿着扩展点的思路继续思考:拆分的级别细化到类的方法时会怎样?

每次提交代码时对其进行结构分析,拆分出一个个独立的原子方法;然后,对各个独立的原子方法做归一化计算,等价的原子方法自动进行合并、替换。这些处理后的原子方法都可以服务化,统一进行部署、伸缩、调度、监控。

这样的一个过程,先进行代码结构化,然后代码互联、按需组装服务,最终进行动态调度,可以将代码的复用层次提升到极致。这样的jvm,将是横跨编译、部署、执行各个环节,将所有的jvm融合为一体的一个超级jvm。

以上想法,一家之言,博君一笑。

写在最后

扩展点从一个想法到落地实现,其间种种,已成过去;目前,扩展点也正在有力支撑运营平台向前发展;相对于遥远的星辰大海来说,未来还有更遥远的路。

终有一天,我们会只需要写业务代码。

终有一天,我们会用人工智能来搞定代码。

一个AI Labs程序员的梦想。

附录

  • 微服务https://www.zhihu.com/question/65502802
  • default方法https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html
  • 动态编译http://openjdk.java.net/groups/compiler/guide/compilerAPI.html
  • ajdk多租户

(完)