扫了下 k8s 的基本概念和各种组件,很大很“美好”,是个吃经验的东西,想着通过实践的方式来学习 k8s,正好 Serverless 概念非常火,现在大概的表现形式就是 FaaS?这个东西很玄乎,并没有很明确清晰的定义,做个没用的 “FaaS” 框架就是本文的目标。文中关于 Serverless/FaaS 的很多东西都是道听途说然后自行脑补出来的,对 k8s 的使用大概也不符合最佳实践。
思路
FaaS
一
大概就是微服务框架的套路,只不过将业务逻辑完全抽离了,通过配置文件的手段声明各种功能需求,比如函数间的依赖是通过声明的方式,然后框架来做通信交互甚至是治理相关的事情,最后打包镜像的时候将框架和函数整合在一起编译。但是这样有个问题,每种语言都需要搞这么一套框架,大概可以通过某种 sidecar 的形式,类似 ServiceMesh,或者把其它的语言函数翻译成一种通用的语言形式。
二
通过事件流驱动函数执行,交互变成了消息总线的方式,流程上和 RPC 的方式有很大的差异,框架这一层或许能有一点简化。
比如使用 Kafka 作为消息系统,每个 Function 有两个 Topic:Input 和 Output,对外暴露的 Gateway 负责将事件塞进 Kafka InputTopic,然后从 OutputTopic 消费处理结果并返回给用户,框架消费 InputTopic 将输入传给函数,将输出塞进 OutputTopic。这种方式看起来并不适合简单的 WebService 场景,比如 Gateway 需要生成一个唯一的 ID 或者使用其它方式将 Input/Output Message 匹配起来,这样还需要每个 Gateway 和 Function 实例独享一个 Topic 或者 Partition,不然对应的 Output 可能被其它实例消费或者需要做大量的过滤工作做一些无用的消费。
然后使用 Kafka 这类消息系统可能需要使用某种方式将其与具体的函数解耦,比如如果使用 Kafka 的 Consumer Group 机制,那么一个 Partition 不能同时被 Consumer Group 内的多个 Consumer 消费,Consumer 和函数绑定在一起会导致的函数的处理能力受限,增多 Partition 数量会触发 Kafka 的 rebalance 和数据迁移等,带来潜在的问题和瓶颈,不使用 Consumer Group 就需要自己管理调度对 Partition 的消费非要这么玩也没啥好说的。Apache Bookkeeper 或者基于其之上的 Apache Pulsar 存储的模型是要优于 Kafka 的,或许是个更好的选择。
k8s
看到有实现通过添加一个叫 Trigger Controller 的组件消费 Kafka 并转发输入给对应的 Function,Controller 这个东西感觉玩法一般都是一主多备来保证 HA,但不作为一种可以横向扩容以提高处理能力的组件而存在?具体怎么用的没有细看,当然事件驱动这种方式不是针对 Web Service 的。
纯粹为了学习 k8s,套路简单点,k8s 的使用姿势如下:
- Function 通过 CRD 来表示。
- 自定义的 Controller 用来追踪管理 Function 资源的创建销毁等
- 提供 HA,避免单点故障以及多主带来的状态冲突,通过 k8s 提供的锁和选举 API(based on etcd)
- 需要手动编译打包 Function 的镜像,runtime 包括一个简单的 HTTP Server,只是把函数注入进去了..
- 监听到新的 Function 资源的创建后:
- 创建 Service 做服务发现和负载均衡相关的工作(Ingress 啥的就先不管了..)
- 创建一个包含 Function 的 Deployment
- 创建 HorizontalPodAutoscaler
- 删除的话干掉对应的资源即可,使用 ownerReferences 能简化这个步骤
然后 k8s 不支持缩容到零,仔细想想貌似也不太现实..
实现
值得一提的是如果资源给少了,minikube 可能会无限失败哦,这里只做参考:
minikube start --cpus=2 --memory=2048 --disk-size=2g \
--image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers
实现的具体过程就不展开了,见 https://github.com/damnever/useless
k8s 的 API 比较多给人的感觉比较乱,Golang 虽然简单但是确实差了点什么,代码长度没有限制加上只有 if/else 这种基本的流程控制方式还特么要八8个空格缩进(大概其它的语言也有这样那样的问题,纯粹的只是为了喷而喷哈哈).. 话又说回来,虽然只是简单的体验了下,但是总体来说很不错,基本上所有的东西都被定义为资源,都能被 CRUD,作为一个平台套路还是比较让人认可的,很多内置/基本/核心的组件和功能都是基于核心的框架流程实现的。
后话
Serverless/FaaS 初衷是很好的,主要目的和微服务框架、ServiceMesh 差不多就是需要把脏活累活收的更干净,然后充分利用云计算的各种优势,大概要解决的问题是如何优雅的去做脏活累活,甚至是能实现自举自治那也就没程序员啥事了..
如果实现一个生产环境可用的 “FaaS” 框架产品,还有很多问题需要解决,包括但不限于:
- 函数以什么形式执行,如果不是 Fire-and-Forget 会不会依赖进程内的全局共享状态?!
- 多语言、依赖包管理、环境变量、日志、指标等等一些基础的功能支持
- 配置管理
- CI/CD
- Web IDE 或者其它类似的工具也是很有必要的
- 对函数输入/输出进行裁剪转换
- 全功能的 gateway 支持比如 HTTP CORS 等等,甚至是其它协议
- 自动扩缩容
- 最小化额外的资源开销
- 降低容器、语言工具启动初始化以及销毁带来的资源消耗
- 比如常见的使用本地缓存或者 P2P 的方式来快速拉取函数对应的容器
- 除了函数运行之外的其它开销都需要尽可能避免,因为函数的粒度已经很小了
- 诸多类似 CGI 程序模型的问题
- 比如对于数据库等外部服务的依赖如何优雅的解决?
- 如果是直接在函数里即时的访问,会有很大的开销,比如无法重用连接
- 或者通过另一种常驻的 Worker/Service 来做数据库的操作,函数的输出 Pub 进消息总线,Worker/Service Sub 对应的数据库操作?诶.. 不也要通过某种方式通信么..
- 或许能用各种用户态软件、高端硬件的方式来解决一些问题
- 降低容器、语言工具启动初始化以及销毁带来的资源消耗
- 缓存功能:维度,一致性要求
- 通过 Namespace/Group 的方式组织函数集
- 很多 workflow 引擎的功能
- 定时调度甚至自定义调度引擎
- 带有依赖关系和优先级关系的函数集
- 生命周期和状态机管理等
- 各种主动被动的 Hook 或者其它方式方便的将状态导出和外部组件交互
- 函数维度比微服务更小?肯定会更乱,在有更好的规范和流程之前肯定会趟更多坑,就更离不开治理
- ..
仔细想下来 Serverless 应该看做是一种愿景或者理想的状态,这里说的 “FaaS” 也不能硬套在不合适的应用场景上。