我们来了解一下业界网关的技术选型 (这里先简单介绍,部分技术名词有缘在未来介绍,如果无缘就只有搜索了,毕竟每一个都解释清楚是挺浩大的工程)

从零开始

这无疑是成本巨大的,除了开发成本之外,还有极大的信任成本,如何让别人能相信而采用编写的网关,特别是大家偏信“大厂背书”四字的时代?

信任成本无法估量,而开发成本大体可以从技术内容上估量

  • 网络编程
    • 首选必然是 socket, 套接字编程已经封装了网络细节,极大简化了我们网络底层的复杂度。
    • 当然这并不是性能最好的方式,我们还可以减少系统内核在网络协议栈的开销,比如利用ebpf以及xdp,facebook的Katran 和 cilium就是典型代表
    • 更进一步是选择网卡硬件支持的DPDK,当然需要网卡支持这点也是它的最大局限。生态完善,社区强大(一线大厂支持)的应用层开发项目是FD.io(The Fast Data Project),有思科开源支持的VPP,比较完善的协议支持,ARP、VLAN、Multipath、IPv4/v6、MPLS等。用户态传输协议UDP/TCP有TLDK。从项目定位到社区支持力度算比较靠谱的框架
  • 并发模型
    • 多进程

      进程是操作系统资源分配的基本单位。

      进程的空间是独立,各个进程相互不干扰,每个进程拥有自己的进程内存,上下文环境,进程控制块,一个进程至少有一个或者多个线程。

      典型代表 nginx 和 nodejs,早年都为当年多线程技术不成熟而采用的设计,在资源分配和共享数据等方面有比较大的局限。在单机上很少有现代软件会在性能方面选择这样方案了。

    • 多线程

      线程是 CPU 调度的基本单位。

      线程属于进程,线程要存在必须依赖于进程,线程共享进程的内存,但线程有自己的栈空间,能创建多少个线程也取决于进程内存的大小。

      线程的上下文切换代价比进程要小的多。

      现代允许操作线程的编程语言,一般提供了线程池支持,甚至提供了简化使用成本的task/promise库,“async/await” 语法糖,比如 c# 、java,当然 js除外,它语言本身为单线程,类似的语法并非多线程的处理。

    • 协程

      协程就是一种非抢占式(协作式)的任务调度模式,程序可以主动挂起或者恢复执行。

      简单来说每一个协程就是一个干什么事情的任务,每个线程就是一个干活的工人,每个工人连座位都不用起身就一个任务一个任务干活干下去,肯定比原来干完一个任务,就收拾回家,等包工头再召唤回来的方式效率高。

      协程没有增加线程数量,只是在线程的基础之上通过分时复用的方式运行多个协程,而且协程的切换在用户态完成,切换的代价比线程从用户态到内核态的代价小很多。

      c#中基于线程池的task,go语言中的goroutine ,erlang语言都是协程思想的具体实践。

    • Actor

      Actor模型是一个通用的并发编程模型,而非某个语言或框架所有,几乎可以用在任何一门编程语言中,其中最典型的是erlang,在语言层面就提供了Actor模型的支持,杀手锏应用RabbitMQ 就是基于erlang开发的。可以说在现代分布式领域,Actor是简化其处理复杂度的最佳模式。

      Actor模型描述了一组为了避免并发编程的常见问题的公理:

        1. 所有Actor状态是Actor本地的,外部无法访问。
        2. Actor必须只有通过消息传递进行通信。
        3. 一个Actor可以响应消息:推出新Actor,改变其内部状态,或将消息发送到一个或多个其他参与者。
        4. Actor可能会堵塞自己,但Actor不应该堵塞它运行的线程

    api gateway 中通信数据成本比较大,而一般自身处理都较为简单,所以Actor这样到处是通信的设计不太适合,一般都使用多线程或协程,nginx是唯一一直采用多进程架构,haproxy都已经转为多进程多线程架构。

  • 异步io

    • epoll

      epoll是一种IO多路复用的机制,一般搭配非阻塞IO实现,是一种同步IO。工作逻辑上体现为向一个epoll实例注册一批需要监听的套接字和期望获得通知的事件,然后等待内核对到来的事件进行通知,通过收割到来的不同事件来执行具体的操作。

    • io_uring

      io_uring是linux于2019年引入内核的异步IO,支持普通的任务提交模式和轮询模式,用户向其一次性提交多个需要完成的系统调用任务,然后内核会对任务进行收割并返回任务完成的结果,用户只需获取任务完成的结果并进行相应的处理,而无需一直等待系统调用的完成。

      在实际交互上,用户和内核将在内存中共享一块环形队列,用户需要提交的任务和收割完成的任务都是在这一区域中进行,内核亦然,然后用户根据设置时的选项决定是否使用系统调用提交和收割任务,这就避免了内核态和用户态之间的数据拷贝,降低了开销。

    目前这是性能最好的两个底层io事件驱动设计,io_uring 理论上是纯正的异步设计,性能最好,但相对较新,且只有liunx高版本才有,所以如需考虑支持windows或低版本liunx,epoll依然是首选。

基于巨人肩上

理论所有开源的现成代理程序源码都是我们可以学习的巨人。

站在巨人肩膀上有两种方式:

  • 改造成另外一个巨人,即开发一个独立分支程序,大多这种情况在后续维护中都与原分支逐渐独立开。就算保持一致也会有一定程度滞后,比如基于nginx的openresty。
  • 成为巨人的外置装甲,当原来的巨人具有比较灵活的模块机制,那么当我们的程序无需变更底层设计时,这种方式便为最简单的方法。当然,如果巨人有巨大的模块机制变更,对于外置装甲将是巨大的灾难。

后续如何开展构建网关的说明

相信经过这几篇非常简单的文章,除了少部分牛人之外,大家可能或多或少都发现仅仅技术概念就有很多不太了解的。

为了降低大家对于底层技术概念理解以及相关技术复杂的学习门槛,后续会基于openresty这样的巨人,以外置装甲的方式为大家介绍api gateway 如何建立。

选择openresty主要为以下两个原因:

  1. nginx 确实很强大,无论功能和性能都是业界顶尖,优秀的模块设计,无论patch或者扩展功能都很方便,并且业界的信任度高。
  2. 虽然需要学习lua这一小众语言,但是其学习成本非常低,语言非常小,比大家写c,或者了解go、c#等语言本身所需时间极少,这样大家可以更关注api gateway网关的本身的内容。

当然openresty并不是完美的,在有些方面也存在一些局限,这里简单列举两点:

  1. 多进程模型在很多方面比多线程编程复杂

    比如数据同步,原本简单地访问次数计数,多进程实现不得不借助进程间通信等复杂技术才能实现

  2. nginx 静态配置机制导致流量有损,特别在tcp情况特别麻烦

所以我们先通过openresty了解api gateway,未来有缘说不定也许大概还可以介绍其他技术

目录