背景
目前微店中台团队为了满足公司大部分产品、运营以及部分后端开发人员的尝鲜和试错的需求,提供了一套基于图形化搭建的服务端接口交付方案,利用该方案及提供的系统可生成一副包含运行时环境定义可立即运行的工程代码,最后,通过 “某种serverless平台” 实现生成后代码的部署、CI、运行、反向代理、进程守、日志上报、进程分组扩容等功能。
这样,产品和运营人员可基于此种方式搭建的接口配合常用的cms系统实现简单查询需求如活动大促的自主“研发”上线,代码的可靠性、稳定性由中台研发侧提供的“某种serverless平台”保障,有效支撑了多个业务快速上线,节省后端开发人员的人力与硬件资源的开销(大多数需求下,nodejs业务对虚拟机的资源开销小于java业务)。
接口搭建系统
此处并不讲解接口搭建系统的原理与实现,只说明其与上文提到的 “某种serverless平台” 的关系。
这是系统的全貌,部分细节由于敏感信息而省略。平台使用方可基于每个功能组件搭建出一套复杂的业务流,在搭建阶段,提供在线debug和日志功能,可用于排错;在部署CI阶段,可集成不同的运行时平台,既可以是自主实现的运行时,也可是第三方云平台;在运行阶段,通过使用agentool工具实时监控当前服务的性能,并可通过traceId一览请求在各系统的全貌。
serverless方案
本节以资源隔离粒度为度量,介绍了我对三种serverless方案的取舍以及最终为何选择了隔离程度更高的kubeless云平台。
基于函数隔离的Parse Server方案
Parse Server提供了基础功能:基于类与对象的权限控制、基于document的nosql存储、简易的用户身份认证、基于hook的自定义逻辑等,但经过笔者的调查与论证,最终并没有采用类似单进程下基于函数隔离的Parse Server及类似方案,这有几点原因:
- Parse Server方案很重,额外引入了非常多不需要的功能,如权限控制、认证、存储等
- 服务隔离级别低,多个服务在一个进程运行,多个服务会在libuv层互相抢占CPU,互相影响对方的业务处理
- 水平扩容难度大,针对单个服务的扩容无法做到
- 底层基于express框架,无法满足运行时接口调用链路的trace追踪
- 当多个服务同时引入不同的资源如db、es或者服务创建的对象足够多时,会存在Parse Server主进程溢出的风险,毕竟64位机的node堆内存是有1.4GB上限的,尽管这个上限是可配置的
- Parser Server发布的接口需通过其client调用,这在公司商用情况下需要做许多额外的配置才能满足
Parse Server官网
基于进程隔离的super-agent方案
为了解决多个服务抢占libuv的问题,我选择了自主研发的 super-agent方案,通过其名称便可知它是一个超级代理,但它不仅是代理,还是一个具有极简功能且可靠的发布系统和运行时容器;它是一个分布式应用,节点间功能划分明确;它同时提供实时调试功能。
super-agent是一个多角色分布式系统,它即可以看做node容器,也可看成serverless的node实现,它以进程为粒度管理服务。它分为“协调者”和“参与者”,协调者实现 应用CI部署、启动、进程维护与管理和反向代理功能,参与者实现 业务请求代理、接受协调者调度。
在super-agent架构中,端口是区分服务的唯一标识,端口对客户端而言是透明的,这层端口资源的隔离由super-agent来做掉,因此多个服务可避免在libuv层的互相竞争,提供水平扩容的可能性。
反向代理
super-agent最核心的功能在于反向代理。由于每个服务都被包装成有单独端口的独立HTTP应用,因此当用户请求流量经过前端转发层后进入super-agent,由其根据相关规则做二次转发,目前支持基于 “路径、端口”规则的转发。
部署
后端应用部署需要进行 “优雅降级、流量摘除、健康检查、应用初始化完毕检查、流量导入、所有参与节点的部署状态查询” 等步骤,需要妥善处理;同时,协调者负责协调众多参与节点全部完成部署操作,这是一个分布式事务,需要做好事务出错后的相关业务补偿。
关于流量
上图并未画出节点角色的区别,实际上只有参与者节点才真正接受用户请求。
协调者流量全部来自于内部系统,包括接受 “接口搭建系统”调用或者其他系统实现的dashboard服务;同时其会向参与者发送相关信令信息,如部署、扩容、下线、debug等。
参与者流量来自于内部系统和外部流量,其中大部分来自于外部流量。内部流量则承载协调者的信令信息。
水平扩容
服务的水平扩容重要性不言而喻。super-agent方案中,协调者负责管理服务的扩容与逻辑分组。
此处的服务是通过服务搭建平台通过拖拽生成的nodejs代码,它是一个包含复杂业务逻辑的函数,可以是多文件。具体的,super-agent通过将该服务包装成一个HTTP服务在单独的进程中执行。因此,如果要对服务进行水平扩容,提供多种策略的选择:
- 默认每台虚拟机或物理机一个服务进程,总体上N个机器N个服务进程
- 扩容时默认每台机器再fork一个服务进程,N机器2*N个服务进程
- 为了更充分利用资源,可为每台机器划分为逻辑组,同时选择在某几个组的机器单独扩容
这些策略对下游应用透明,只需选择相关策略即可。
水平扩容的缺点:每台虚拟机或物理机资源有上限,一个正常的node进程最多消耗1.4GB内存,计算资源共享,在一台8C16G的虚拟机上,最多可同时运行16个服务。及时通过分组扩容的方式,每当扩展新的虚拟机或物理机,都需要由super-agent根据分组信息实现进程守护,同时每次服务CI部署也同样如此。运维管理上需要配合非常清晰明了的dashboard后台才能快速定位问题,这点在多服务的问题上尤其突出。
在线调试
super-agent提供消息机制,由搭建平台中组件开发人员使用提供的serverless-toolkit工具debug相关逻辑,最终可在super-agent的协调者后台查看实时debug结果。
总结
super-agent是符合常规的基于业务进程隔离的解决方案,它有效的支撑了微店的几个活动及产品,虽然峰值QPS不高(100左右),但它也论证了super-agent的稳定性及可靠性(线上无事故,服务无重启,平稳升级)。
但是,super-agent仍然存在几个问题,它让我们不得不另觅他法:
- 日常运维困难,需要开发一系列后台系统辅助运维,这需要不少人力成本
- 这是一个典型的一机多应用场景,当部署super-agent时会对运行其上的服务有所影响(重启),尽管这个影响并不影响用户访问(此时流量已摘除),但仍然是个风险点
- 水平扩容实现繁琐,当下掉某几台参与节点时会带来不少影响
基于内核namespace隔离的kubeless方案
基于kubeless的方案则是隔离最为彻底的解决方法,kubeless是建立在K8s之上的serverless框架,因此它可以利用K8s实现一些非常有用的特性:
- 敏捷构建 - 能够基于用户提交的源码迅速构建可执行的函数,简化部署流程;
- 灵活触发 - 能够方便地基于各类事件触发函数的执行,并能方便快捷地集成新的事件源;
- 自动伸缩 - 能够根据业务需求,自动完成扩容缩容,无须人工干预。
其中,自动伸缩则解决了 super-agent 的痛点。
kubeless中与我们紧密相关的有两个概念:“function和runtime” ,function是一个统称,它包括运行时代码、依赖以及其他配置文件;runtime则定义运行时依赖的环境,是一个docker镜像。
若要接入kubeless平台,我们需要解决如下几个问题:
- 开发自定义运行时,满足商用需求,如trace、日志分片、上报采集
- 自定义构建镜像,实现ts编译
- 基于yaml的function创建、更新、删除流程探索,并自动化
- function部署,包括流量摘除、流量导入、业务健康检查
- 中间件日志、业务日志、trace日志隔离与挂载
- function运行时用户权限问题
- 水平扩容探索与尝试
- 资源申请规范指定与部署规范约定
因此,前进的道路仍然很曲折,而且很多需求需要自己从源码上去寻找解决方法。
一些说明
kubeless实现的serverless体系中,function所在pod中的所有容器共享网络和存储namespace,但是默认外网是不可访问k8s集群的所有pods,因此需要通过一层代理实现请求的转发,这就是“Service”。Service负责服务发现及转发(iptables四层),因此在Kubeless或者K8s中不会直接通过pod IP来访问服务,而是通过Service转发四层流量完成。Service有K8s分配的cluserIp,clusterIp是集群内部虚拟IP,无法被外部寻址,而是通过Kube-Proxy在容器网络之上又抽象了一层虚拟网络,Kube-Proxy负责Service的路由与转发(关于kube-proxy细节,请看参考资料)。
Service后端对应是一个或多个pods,这些pods中的一个容器则运行相同的业务代码。那么流量是如何路由至Service上来呢?这就涉及到Service的“发布”,常用的是Ingress。Ingress包括HTTP代理服务器和ingress controller,代理服务器负责将请求按照规则路由至对应Service,这层需要Kube-Proxy实现虚拟网络的路由;ingress controller负责从K8s API拉取最新的HTTP匹配规则。
问题解决
- 自定义镜像:这里的镜像包括两部分:构建镜像和运行时镜像。运行时镜像需要解决宿主代码的鲁棒性,以及提供 livenessProbe、readinessProbe、metric接口实现;构建镜像则负责构建阶段的操作,如编译、依赖安装、环境变量注入等操作。具体编写可参考 implement runtime images。
- 构建镜像参考1
- 关于function的CRUD操作,笔者先通过命令行走通整个流程后,又切换成基于yaml的配置文件启动,使用yaml启动好处在于:a,可使用kubeless自带的流量导入与摘除策略 b,水平扩容简单 c,命令简单 d,配置文件模板化,自动化
- 部署策略由于涉及到业务特点,此处不详细介绍
- 日志的挂载是必要的,否则pod一旦重启,容器内的所有日志全部丢失。尽管会存在日志收集的操作,可是日志收集进程大多数都是异步进行,因此会存在丢失日志的情况。因此,必须通过挂载volumn的形式在K8s node上映射文件。但在这过程中会出现权限的问题,这在下一点说明
- 权限问题在于kubeless将function的执行权限设置为非root。这是安全且符合常理的设定,但有时function需要root权限,这需要修改K8s的security context配置,需要谨慎处理
- 水平扩容基于K8s的HPA组件完成,目前支持基于CPU和QPS等指标进行扩容,目前笔者并未专门测试这项内容,因为它足够可靠
- 资源申请的指定需要符合每个公司的实际情况以及业务特点,以node技术栈为例,pod中每个容器设置1C2GB的内存符合实际情况;而至于部署规范,则要兼顾运行时容器的特点,合理配置K8s的node与pod、function的对应关系
总结
运行在kubeless 中的函数和运行在super-agent的代码没有什么不同,可是周边的环境准备可大大不同。为了让kubeless中的function可以接入公司内部中间件服务,笔者费了不少功夫,主要集中在日志及收集部分。好在事在人为,解决的办法总是多于失败的方法。
进度
目前,super-agent方案已承载了10+个线上应用或活动,稳定运行4个月,资源使用率符合预期;
kubeless方案还未正式接入流量,等待进一步做相关异常测试。
参考
kubeless介绍
security-context
kube-proxy