APIs are forever

We knew that designing APIs was a very important task as we’d only have one chance to get it right.

By Werner Vogels, AWS CTO

有句俗语说的好:没有什么是包一层解决不了的,如果不行,那就再包一层。从 K8S 在蘑菇街的落地经验来看,包错一层,痛苦两年。

为什么要包一层

出于某些需要,对于开源项目,很多公司喜欢在其基础之上进行一定包装,或是替换 portal,或是再封装一层 API。

落地开源项目时,需要和实际的业务场景相结合。比如每个公司会形成自身特色的前端风格,而开源项目的 portal 大体做的比较基础,所以往往容易自研 portal。对于认证系统,出于安全考虑,故要对接公司的统一认证系统,同样的还有资源配置管理(CMDB),监控系统等运维基础组件。具体以 K8S 创建 Pod 为例,创建之前需要校验用户的权限,创建之后需要将 Pod 的信息添加到资源管理系统中等,这些步骤较为繁琐,为了提升用户体验,我们在 K8S 之上包了一层,这一层负责完成认证以及和运维等基础系统交互,仅对用户暴露一个简单的 API。随着接入的业务越来越多,我们部署了多个集群,屏蔽多个集群的差异和对外暴露一个入口也是促使增加一层的因素。

从另外一个角度,包一层比较容易突出创新点和工作量,原因不言而喻。同时避免大幅度直接修改开源的代码,因为修改开源项目一来耗费大量能力,二来和社区的同步是个问题,不利于升级和维护。

包错一层的痛苦经历

包一层本没有错,但是用错误的姿势包错一层,就是大错特错,且由于 API 变更成本非常之高的,又导致将错就错,造成长达两年的负面影响,直到全部废弃这一层,给业务方暴露原生的 K8S API。

首先介绍下当初的架构图,新包的一层名字叫 Orchestrator,它对用户暴露统一的对外接口,屏蔽了下层的多个 K8S 集群,同时对接认证,运维等系统。

                     Users
                     
                       |         
                       v
              +------------------+
              |   Orchestrator   |  -----> Auth, CMDB ...
              +------------------+

     +---------------+    +---------------+        
     |  K8S Cluster  |    |  K8S Cluster  |    ......
     +---------------+    +---------------+            

单纯从架构图出发,这样的一层似乎很合理,好像没有问题。真正的问题就出现在 Orchestrator 对 API 的封装上,主要犯了两个错误:

  • 把异步的 API 封装成同步,将声明式的风格变成过程式的风格,带来巨大的问题。
  • 完全设计了一套 API,URL 和 Body 和 K8S 存在巨大差异。

K8S 非查询类的 API 几乎都是异步的,以创建 pod 为例,当请求数据写成功 etcd 之后,服务端即返回请求,之后客户端轮询查看 pod 状态。为了避免用户轮询,前小伙们(非甩锅)将轮询的功能沉淀到了 orchestractor 模块中,即 orchestrator 收到客户端创建 pod 的请求后,它首先调 K8S API 创建 pod,然后周期性轮询 K8S 查看 pod 状态,直到 pod 成为 running 状态后,才向客户端返回结果(包括 pod IP 信息)。从易用性的角度出发,的确简化了业务逻辑,但是带来了更为负面的后果。比如在轮询的过程,如果客户端发出删除或者更新的操作,就会带来非常复杂的状态处理逻辑,为了简化逻辑,orchestractor 对每个资源加上了一把写锁……可想而知,客户端访问 orchestractor 非查询类 API 的时间长达数十秒甚至数分钟,pod 在创建的过程中无法被删除,极大的影响 PaaS 的弹性能力和故障快速恢复能力,业务方对此的反馈也很负面。

K8S 的参数非常之多,本着简化参数的好意,orchestrator 又将参数封装了一层,仅暴露部分参数。那么问题来了,如果要启用 K8S 的其它功能,就需要修改 orchestrator 的代码,大大的增加了维护成本。还导致 K8S 原生的客户端无法使用,增加业务方的接入成本。

从两年多的经验来看,1.5 版本下的 K8S 已比较稳定,极少碰到问题,但是公司 PaaS 的维护成本依然比较高,原因就是出在 orchestractor 上,不断引发大大小小的问题。但却很难移除它,因为众多的业务都是基于 orchestrator 的 API 进行设计的。直到借用一次大版本升级的机会,才将 orchestrator 移除,直接给业务方暴露 K8S 原生的 API,大大的降低了维护成本,提高了稳定性。

反思:如何包一层

在探讨如何包一层之前,我的观点是:能用原生就尽量用原生,认证等相关功能沉淀到前端就尽量沉淀到前端。

如果实在因某些因素需要在开源项目增加一层 API,我认为最好遵循如下规则:

  • 保持兼容原生 API,HTTP 的 Method,HTTP 的 ULR,以及 Body 等。
  • 保持风格不变性,即不能将异步的 API 改成同步的 API,声明式的 API 改成过程式的 API。
  • 扩展性字段尽可能放在 HTTP 头部,比如认证的 token 等。

兼容原生 API 最大的好处就是可以直接使用原生的 SDK,降低接入成本。很多开源项目都有丰富的文档,这些文档可以直接给用户做参考,降低维护文档相关的工作量。开源项目的 API 一般都比较稳定,不会随意的变更,特别在升级方面具有很大的优势。

对于 HTTP 类型的 API,某些扩展性的功能可以放在 HTTP 头部,比如认证的 token 等,如此即保证了兼容性,又能扩展新功能。当然,很多 web server 和 proxy 会限制 HTTP 头部大小,对于占用空间大的参数就不适合放在头部了。

具体以蘑菇街改造后的 PaaS 为例,它对外完全暴露原生的 API,认证模块采用 Dex 和 K8S 无缝对接,再由 dex 对接统一认证平台。对于和资源配置管理等运维模块的交互,我们开发了一个组件向 K8S list-watch 对应的 pod 资源,异步的将 pod 的信息同步到 CDMB 中。从半年多的经历来看,新的 PaaS 平台稳定性得到较大提升,维护成本也随之降低。

                            Users
                              |
                              v
                      +---------------+    list-watch
   Auth <--- dex <--- |      K8S      |  --------------> CMDB
                      +---------------+