共计 4936 个字符,预计需要花费 13 分钟才能阅读完成。
背景
目前微店中台团队为了满足公司大部分产品、运营以及部分后端开发人员的尝鲜和试错的需求,提供了一套基于图形化搭建的服务端接口交付方案,利用该方案及提供的系统可生成一副包含运行时环境定义可立即运行的工程代码,最后,通过 “某种 serverless 平台” 实现生成后代码的部署、CI、运行、反向代理、进程守、日志上报、进程分组扩容等功能。
这样,产品和运营人员可基于此种方式搭建的接口配合常用的 cms 系统实现简单查询需求如活动大促的自主“研发”上线,代码的可靠性、稳定性由中台研发侧提供的“某种 serverless 平台”保障,有效支撑了多个业务快速上线,节省后端开发人员的人力与硬件资源的开销(大多数需求下,nodejs 业务对虚拟机的资源开销小于 java 业务)。
接口搭建系统
此处并不讲解接口搭建系统的原理与实现,只说明其与上文提到的 “某种 serverless 平台” 的关系。
](https://si.geilicdn.com/viewm…
这是系统的全貌,部分细节由于敏感信息而省略。平台使用方可基于每个功能组件搭建出一套复杂的业务流,在搭建阶段,提供在线 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 匹配规则。
](https://si.geilicdn.com/viewm…
问题解决
- 自定义镜像:这里的镜像包括两部分:构建镜像和运行时镜像。运行时镜像需要解决宿主代码的鲁棒性,以及提供 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)