• API Gateway 技术漫谈
  • 发布于 1周前
  • 61 热度
    0 评论
什么是 API Gateway?
笔者曾经做过多年的入口网关开发,一直认为所谓的 API Gateway 就是在数据面开发一些鉴权限流功能。后来笔者跟客户的交流越来越多,意识到自己一直是以开发人员来看待 API Gateway,而真正的  API Gateway 应该以用户视角来看待。

比如笔者最近遇到的一个客户,给我们提了一个需求,针对某些特定渠道的请求进行限流。笔者之前在接入网关里面开发限流插件都是针对来源 IP 或者带有特定 header 的请求进行设置。但是针对 api gateway 来说,限流插件更关心 consumer 这样的概念,针对不同的 consumer 进行不同的限流配置。除此之外,很多业务服务都会有通用的逻辑,比如对业务请求进行 metric 统计,对异常请求进行重试,对外提供统一的 API 协议等等,这些通用逻辑都可以放到 api gateway 中来实现。更近一步,api gateway 应该是一个对 api 进行托管的服务,提供对 API 的完整生命周期管理,包括创建、维护、发布、运行以及下线。

APISIX
笔者所在公司内部使用的 api gateway 产品就是基于 apisix为底座进行二次开发,主要考虑到 apisix 的以下优点:
1.丰富的开箱即用的插件,基本上常见的需求都能找到对应的插件,并且个性化的需求自己写扩展的插件也方便。这几年笔者也帮好几个初创公司推荐使用 apisix,基本上都能够满足需求。
2.apisix 所抽象的 route/service/upstream/consumer/plugin 这些概念都很优雅,作为网关来说,几乎能够满足所有的场景,比 k8s 里面的 ingress/service 这两个概念好很多,比如你想表达按百分比转发流量,这个用 ingress/service 就无法表达出来。(目前 k8s 也认识到 ingress/service 对流量管控表达能力弱的问题,推出了新的 gatewayapi,但是实际体验下来,对比 apisixroute 仍有一定的差距)
3.支持多集群管理:一套 apisix 可以代理多套集群,某些场景下确实存在这种需求。(开源的 nginx-ingress-controller 就不能一套网关代理多套 k8s 集群)
4.支持对接多种服务发现组件:ZooKeeper、Consul、Nacos、kubernetes等

5.高性能的路由匹配算法(用 radixtree 实现了一个路由匹配算法)


apisix 官方宣称是一个动态、实时、高性能 api gateway,前三个词 动态、实时、高性能其实不是 apisix 自身的功能,而是 openresty(nginx) 的功能。
1.高性能: apisix 底层依托于高性能的 nginx+luajit(openresty),当然这个高性能是跟 java 之类的网关相比,如果跟 envoy 或者一些使用 rust 写的网关比的话,性能平分秋色,没有绝对的优劣。
2.高稳定性:稳定是作为网关最核心的要素,而底层的 nginx 作为一个入口网关,经过了几十年的发展,使用基数也很庞大,很多特殊需求场景都考虑到,这个从  nginx 大量可配置的参数就能够感受到。而如果使用一个全新的网关底座,说不定哪天就会遇到一个坑。

3.动态:不仅修改配置无需重启服务,甚至修改 luajit 的代码也无需重启服务。这对于网关这种零停服要求的基础设施来说,是非常重要的功能。比如对于长连接的请求,这个连接可能存在几个小时甚至几天,如果直接重启网关服务,就会造成影响。笔者认为这是 openresty 时至今日依然无可替代的优点,目前几乎没有其他语言或者框架实现这一特性。因为对于静态编译语言来说,符号之间的引用关联非常复杂,很难实现解耦。笔者自己在做一些软件的时候,曾经也实现过一些 reload 的功能,发现这并非是一件易事。而 nginx 的内部组件依赖关系要更加复杂的多,可以想象这是一件难度很大的事情。


当然 apisix 也存在一些缺点:
1.官方提供的一些插件质量不高,没有经过较大规模的生产环境的验证。笔者曾在生产环境中使用过几款 apisix 官方的插件,均出现过一些小问题。
2.apisix 使用 etcd 作为它的数据存储,etcd v3 使用的是 grpc 协议(底层 http2 协议),但是由于 openresty 生态一直都缺乏一个 http 2.0 客户端(一是 http2.0 本身较复杂,二是 cosocket 也很难实现 http2.0),apisix 官方只能在 http1.1 基础上,通过 long poll 的方式与 etcd 进行通讯,这就导致每个 nginx worker 要与 etcd 建立多个连接,而如果使用 http2.0 ,那么每个 worker 只要建立一个连接即可。除此之外 apisix 官方实现的 lua-resty-etcd,存在某些问题,比如当你 watch 的版本号被 etcd 压缩后,etcd 就会取消这个 watcher,你需要重建 watcher,而之前 apisix 却没有处理这个错误,导致问题(目前该问题已经修复)。这里对比其他基于 openresty 开发的网关,kong 就是使用 pg 作为它的数据库,而 PG 有成熟稳定的开源库,它就不会遇到 apisix 的这些问题。

3.apisix 支持 webassemby,但是它实现的 runtime 还是过于简陋,暴露出来的接口太少,达不到生产可用的状态。另外虽然也支持 java/go/php 等 其他语言写插件,但是一旦用了这些语言写的插件,性能就会指数级下降。目前只有用 luajit 写的插件才会有高性能。


OpenResty
开源社区中基于 openresty 推出的 apigateway 产品有 apisix 和 kong,笔者之前一直有个疑问,为什么 openresty 不推出自己的 ingress 网关和 apigateway 产品,后来了解到,openresty 官方其实有对应的商业产品 openresty edge,只是没有开源。笔者曾经是 openresty 的重度爱好者,但是笔者对 openresty 的现状感到惋惜。在笔者看来,openresty 错过了一次出圈的机会,就是借助于 kubernetes。

毫无疑问,过去 10 年由 Kubernetes 代表的云原生方向是 IT 基础设施领域最火的一个技术方向。其中 kubernetes 的一个核心理念就是 pod 的动态变化,而这个理念与 openresty 的动态不谋而合(先有 openresty,后有 kubernetes),所以 kubernetes 官方所推出的 nginx-ingress-controller 的底层就是使用 nginx-lua-module (openresty 的核心模块)所实现。

另外依托于 kubernetes 所诞生的 service mesh 技术,它的数据面目前使用的是 envoy,其实在早期的时候,谷歌最开始的计划是使用 nginx,但是由于一些问题最终放弃了 nginx。

所以笔者认为 openresty 本该可以在云原生行业中引领 api gateway 和 service mesh 这两个方向,成为像 kubernetes 那样的基础设施。后来笔者跟 openresty 的作者章亦春有过交流,得知他在过去几年重点专注在动态追踪这个领域和 openresty 公司的建设,也推出了 openresty xray 这一牛逼产品,而代价就是错过了这个出圈的机会。

回到 openresty 本身来看,笔者使用多年的 openresty,发现其依然存在一些问题:
luajit:
生态较差,比如你要把数据发送到 kafka,或者从 zookeeper 拿点数据,就会缺少稳定的开源库(有些场景虽然存在开源库,但是稳定性又不够)。唯一的解决办法就是代理到别的服务去做,如果这样为何要用?

luajit 的高性能是建立在能够 jit 的情况下,但是实际很难 100% 保证 jit,经常遇到不能 jit 的情况。作者 Mike Pall 这些年也不怎么维护 luajit,处于退休的状态,也一直没有找到合适的接班人。


nginx 多进程架构:

访问 nginx 的请求被调度到哪个进程其实是随机,如果一个进程所处理的连接里面的http请求很多很繁忙,那么它也无法委托给其他进程来代劳,即便其他进程是空闲的。这点对于最新的 http2 而言非常明显,因为 http2.0 一旦建立了一个连接,它就只能固定在一个进程里面,这带来的问题就是前端应用在迁移到 http2.0 之后,如果遇到 nginx worker 繁忙,它的 TTFB(首字节时间)性能下降。


多进程之间无法安全地共享资源。nginx的方案是放数据在共享内存里面,并且只能将字符串和数字放入共享内存,每次访问共享内存都必须使用互斥锁。openresty 的 share_dict 就是放在共享内存中的红黑树,每次对它进行修改,就要涉及多个地方的改动(红黑树的操作比较复杂,涉及较多的代码),而如果在改动期间,恰好 worker 进程崩溃了,虽然 nginx 会捕获 SIGCHILD 信号,进行释放锁,但是父进程并不会对这些操作到一半的过程进行撤销恢复,导致数据处于一个诡异的状态。


ingress-controller
由于 kubernetes 越来越流行,新时代的 api gateway 必须要支持 kuberentes/ingress。目前谷歌官方为 kubernetes  ingress 推出的产品是 nginx-ingress-controller ,但是它的问题在于,数据面与控制面绑定在一起(一个 pod 中含有两个容器),这就导致它的横向扩展能力受限。对于一家大型公司来说,它的流量可能会非常的庞大,那么它的数据面需要的节点数量可能会有几十上百个,这就导致控制面也相应的增加,从而对 k8s 产生一定的压力。另外针对大规模 k8s 集群,控制面的内存消耗也会比较多(需要在本地内存中缓存所有的 pod/service/endpoint/ingress 的资源)。

apisix 官方推出的 apisix-ingress-controller 让 apisix 支持 kubernetes 作为服务发现,并且是控制面和数据面分离的架构。其原理是 apisix-ingress-controller watch kubernetes resource 的变化,将数据同步写入到 apisix 的 etcd 中,但目前官方 apisix-ingress-controller 存在一些问题:

1.目前官方 apisix-ingress-controller  对 ingress/service 的支持不够完善,部分特性依然不支持(部分 ingress 的 annotation 不支持)。其实 apisix-ingress-controller 推荐使用 apisix 自己的 apisixroute/apisixupstream 等 CRD。虽然笔者认为 apisixroute 的表达能力确实是比 ingress 更好,但是对于很多存量客户来说,他们的历史系统依然是使用 ingress,这就导致迁移到 apisix 会存在不兼容。我们团队也移植了一些 nginx-ingress-controller 的功能到 apisix-ingress-controller 中。


2.apisix-ingress-controller 的代码质量不高的,很多模块耦合严重,并且有一些 bug,我们团队也给他修了一些的 bug。除此之外,还有一些低级错误,比如官方 1.6 的镜像,里面的代码却是 1.7 的代码,严格来说是达不到生产环境的标准的。开源的 nginx-ingress-controller 虽然也有很多问题,并且架构也不合理,但是无奈它的用户多,维护的人也多,反馈的 bug 多,导致它的稳定性其实是很高的。


apisix 需要一个etcd 来存储数据,而 apisix-ingress-controller 的本质却是使用 k8s 的 etcd ,你会发现这里有两个 etcd。如果 apisix 需要同时代理 kuberentes 的流量和非 kubernetes 的流量,两套 etcd 没有问题,但如果 apisix 只代理 kubernetes 的流量的话,严格来说只需要一套存储,并且始终应该以 kubernetes 的数据为准。两套存储不仅增加维护的成本,还会带来数据不一致的问题。

所以为了解决这个弊端,在新版本的 apisix-ingress-controller 中实现了 etcd server 的功能,让 apisix 直接从 apisix-ingress-controller 中读取数据,大幅优化了系统架构。当然这套架构现在还达不到生产的标准,目前笔者所在公司也在推进和改善这一方案。

未来
openresty 为了追求极致的性能,引入了 luajit 来扩展网关功能的方式,相比于 nginx 早期只能使用 c 来进行扩展的方式,确实达到了在保证性能的前提下,大大提升了研发效率。但是 lua/luajit 经过了这么多年的发展,生态一直没有发展起来,依然定位于一个小众嵌入式语言,而差不多同时期的诞生的 golang,其生态却发展的非常繁荣。

当然一方面 golang 后面有 google 在背后支持,另一方面就是 kubernetes 这一 killer app 带来的影响。而 luajit 却是 Mike Pall 个人天才创新,一直以来都缺乏社区维护,最近几年基本不怎么维护,并且自 lua 5.2 后,luajit 也不在和官方 lua 兼容。(主要还是难度太大,懂的人很少)再加上其本身定位就是一个嵌入式的小语言,笔者估计未来 luajit 也不会达到繁荣的状态。所以从生态的角度来看,这大大限制了 openresty 的发展。

笔者早期曾经是 go 的重度爱好者,相比 openresty 的 cosocket 只提供了 sokcet 的协程化,golang 的协程(goroutine)更加完备,不仅是 socket,甚至 cpu 密集型任务也能用协程来做。除此之外,很难有一门编程语言像 go 一样能够达到开发效率和运行效率一致。笔者曾经认为如果哪天 golang 内置的 http server 性能足够高的时候,openresty 可以退出历史的舞台,但是经过这么多年的发展,其依然没有达到比拟 nginx 这样的高性能,所以这一预言目前来看基本宣告失败。(这里补充一下,目前 http  server 性能排行榜中,排名第一的是 golang 写的 fasthttp,但是目前它还存在一些问题,比如不支持 http2.0 和流式读取 request body 等,所以它还不算一个功能完备的 http server)

后来笔者使用了一段时间的 rust,觉得 rust 才是未来。通过 ownership 既解决了 c/c++ 的内存泄露问题,也不需要付出 gc 的代价。而且 golang 的协程在 rust 里面,可以用 async/await 来做,开发效率几乎一样,但是运行效率却比 golang 更高。目前 rust 存在的唯一问题就是其较高的学习门槛导致它的生态不够完备,很多功能库缺少。如果未来rust 能够出现一个类似 kubernetes 这样的 killer app,那么笔者会坚定的认为 rust 就是未来,但现阶段还不能下这个结论。

得益于 kubernetes 和 service mesh 的流行,东西向网络代理的程序 envoy,也开始在南北向有所建树。目前产生了一种新的网关形式,envoy+X(lua/golang) 的网关,笔者当下认为这个可能是未来网关形式的最优解。

一个有意思的现象,东西向的 envoy 在逐渐蚕食南北向 nginx 的市场,而 nginx 也在开始发力,切入 envoy 的东西向市场,apisix  和 kong 都推出了基于 openresty 的 service mesh 产品。

目前 envoy 和 nginx 处在一个相持阶段,并且由于网关这个极其注重稳定性的因素,对于存量的企业来说,除非外部有足够的驱动力,是不太可能轻易变换网关技术栈的。所以,未来谁能彻底统一东西南北向,成为新时代的王者,现在还不能下结论。

总结
本文以 api gateway 为切入点,探讨了 nginx 和 openresty 的一些底层技术,并融合了对编程语言及其技术栈和异步编程的一些简单思考。以上均为作者个人主观观点,如有错误,还请指正。
用户评论