经常会有人问 “当你们说可编程代理的时候,那么什么是可编程代理,为什么需要可编程代理”?本文从不同角度回答这个问题。首先会简单地介绍代理;然后讨论下代理在发展过程中的阶段划分;基于这些阶段的划分,讨论每一个阶段相比于上一个阶段的改进之处,以及为什么需要这些改进,同时我们讨论下 “可编程” 所包含的几个层面;最后我们总结下 “为什么需要可编程代理”。
什么是代理及代理的功能
代理是代理服务器的简称,代理服务器通常部署在两个互相隔离的网络的中间处,既能访问一侧网络也能访问另一侧网络,通过把一侧的数据搬运到另一侧,实现了网络的连通。代理是一种串路网络设备,自从计算机网络诞生,代理就存在了。由于代理是串路的,因此代理在实现网络连通功能的同时也衍生出新的功能和使用场景:
- 路由:代理在转发数据的时候,根据数据的特征,转发到不同的目的地
- 负载均衡:在转发过程中,通过把数据分发到不同的目的地,提高吞吐量、避免目的地单点故障。负载均衡逐渐成为代理细分功能的一个领域
- 故障迁移:在转发过程中,当目的地出现故障时候,代理可以把数据转发到备用的目标,对请求方提供不间断的服务
- 访问控制:代理可以决定某些流量可以通过,哪些流量需要被拦截。WAF 是典型的代理在细分领域的应用
- 身份识别:访问控制很多时候需要基于身份信息,因此代理通常也具有身份识别的功能
- 网络加速:代理通过缓存数据的方式加速网络访问
- 指标采集:代理对经过的数据进行统计,汇总给 NPM 软件用于网络优化及网络规划
- 信息安全:除了访问控制外,代理还可以用于安全审计、TLS/SSL 卸载、数据加密等,满足安全需求
提供桥接两个网络功能的除了代理还有路由器。路由器工作在网络的 3 层;而代理工作在 3 层以上,或者说 4 层和 7 层。
软件代理的发展
软件代理服务器在发展过程中,大致上经历了如下几个阶段:
配置文件时代
代理类软件(最主要是开源软件),占到了网络基础设施类软件的大多数,这些软件在细分领域提供了不同的功能,比如针对不同协议的代理、比如侧重负载均衡的代理、比如侧重缓存加速的代理。这一大类软件,都是基于配置的。用户在配置文件中设置参数、配置规则,然后启动服务进程执行这些规则
配置语言时代
配置难于表达复杂逻辑,所以很多代理软件在配置基础上引入了很薄的脚本能力,我们一般称为 “配置语言” 或者说 DSL,比如 Haprxoy 的 ACL,Varnish 的 VCL。
脚本语言时代
当逻辑进一步复杂的时候,配置语言也会难于表达;同时,当配置语言数量多到一定程度时候,配置语言本身的管理,会有很大难度。
就像 shell 脚本可以写简单的逻辑,但是当 shell 代码多到一定程度的时候,通常会进一步选择 Perl 或者 Python 这些更加结构化的脚本语言。Proxy 支持脚本语言,既有脚本语言的便利性,也有编程语言结构化的优势。
这类的例子如 Openresty(Nginx + Lua)、Nginx Plus(Nginx + NJS). 同时,这类例子里也包括大量应用类编程语言实现的代理服务器,比如基于 NodeJS 的 StrongLoop、Spring Cloud Gateway 等,这些应用类编程语言往往自己就有脚本支持能力。
集群时代
脚本语言解决了代理中复杂逻辑的模块化、结构化实现难点。此时进一步的需求是把代理和其他的管理控制工具集成,因此需要有 REST 接口。外部的控制平面可以通过 REST 接口动态的设置脚本中的逻辑。
同时,人们对代理的使用也从单实例上升到集群化,因此这一类代理通常都自身支持集群能力,比如 Enovy 和基于 Openresty 的 Kong,他们通过某种集中或者共享方式实现集群能力,同时对外提供 REST 接口。
对于这个时代的代理,通过配置管理,一般也可以实现集群管理;并且配置管理工具也可以对外暴露 REST 接口。比如使用 Ansible + Nginx 的方案,实现了和云时代的能力。相比之下,集群时代的方案需要要更多的组件形成方案,而云时代的方案更收敛。
云时代
在 #5 的基础上,代理采用分布式的方式部署,最常见的场景是为每个应用进程部署一个代理,也就是 sidecar proxy 模式。
在采用分布式以后,针对不同的上游服务,采用不同的规则和策略,也就是多租户能力。不同上游服务,不仅在逻辑上有独立的规则和策略;在物理上也进一步提供了隔离,实现进程级和接口级的细粒度管理。如果我们把服务网格的控制平面和数据平面看作一个整体,那么服务网格是这个领域的代表,典型的比如 Istio+Envoy,Linkerd+Linkerd Proxy。Pipy 就是这个阶段的产物。
代理发展总结
在如上的各个阶段里,每一个阶段都比上一个阶段有所改进,概要的说:
配置语言时代比配置文件增加了基本的脚本能力。这种基本的脚本能力,在配置文件的基础上,增加了动态能力。比如在运行期获取请求的特征(如获取 HTTP Header),然后根据这些特征做动态的逻辑判断,进行特定的操作。
脚本语言时代比配置语言时代增加完整的脚本能力,此时可以结构化和模块化的编写脚本逻辑。在配置语言的时代,当逻辑复杂的时候,脚本的量也会大幅增加,此时结构化的脚本能力成为一种必需
集群时代比脚本语言时代多了 REST 接口和集群能力。当需要水平扩容代理能力的时候,需要把多个代理实例组成集群;集群内的实例共享配置和脚本,并且用户可以通过 REST 接口去管理配置和脚本。
云时代比集群时代多了分布式能力,主要体现在同一个集群内不同的实例所运行的脚本和配置是不同的。在集群模式下,对于不同的上游服务,通常会有不同的配置和策略,比如不同的认证方式、不同的访问控制机制等;当上游服务逐渐增多的时候,这些不同上游服务的配置在逻辑上是分离的,但是在物理上都运行在同一个代理进程里。这种逻辑上分离的配置和策略运行在同一个物理进程中的情况,带来一些弊端:更多的逻辑运行在一个进程内,带来了更多的复杂性;不同上游服务的共享 CPU 和内存等资源,导致互相影响;如果某一个上游服务的脚本出现了安全漏洞,会导致其他上游服务的配置泄漏,存在安全隐患。
云模式对于集群模式的改进在于每个上游服务的代理进程是独立的、彼此隔离的。他们受同一个集群管理者管理,但是在运行中的配置和脚本是独立和隔离的。这种隔离的特性,是多租户环境中的一种强需求 – 不同的上游服务属于不同的租户,租户之间不应该互相影响,也不应该知道彼此的配置。云时代可以认为是集群时代多集群的极限模式 – 最极端情况下,每一个进程都有自己的配置。
代理的需求演化
让我再从另一个视角看下代理的演化过程–需求的演化。
配置文件时代
第一代的代理主要是实现了代理功能,并且提供了基础的可配置能力;同时,网络设备,尤其是串路网络设备的特性,要求代理是高可靠的;网络的海量数据实时传输的特性,要求代理高吞吐、低延迟、低资源。和所有的软件一样,代理也需要支持模块化和可扩展,这个阶段的代理主要采用 C 语言开发,相应的开发扩展模块也使用 C 语言,模块在进程启动时加载。概括起来说,这个阶段的代理需求是:连通性(网络功能)、易用性(可以通过配置文件配置)、可靠性(串路设备的要求)、高性能、扩展性。
配置语言时代
第二代代理的改进体现在进一步提高了扩展性和灵活性,如一些动态的数据获取和配套的逻辑判断。脚本的引入,进一步增强了易用性;对于组合逻辑和动态数据获取的支持,提供了灵活性,同时改进了扩展性。
脚本语言时代
第三代代理相比于第二代代理的改进主要是可管理性、开发者友好和可编程。脚本大量地使用,一方面是因为使用 C 语言等做扩展开发难度大、维护难度大,一方面是脚本在现场开发的效率要优于编译型语言。
开发者的开发效率和大量脚本维护带来的难度,要求这一代代理使用更为结构化的脚本语言,并且需要保持不低于上一代的性能、资源占用等核心能力。
结构化和模块化的脚本语言的使用,开启了代理的可编程时代,此时扩展代理服务器的功能就包含了两个层面和可能性,一个是使用 C 语言等开发核心模块,一个是使用脚本开发动态逻辑;或者说可编程包含了核心模块 可编程 和动态逻辑 可编程
集群时代
第四代代理开始了集群支持能力,属于可管理性的改进。
对 REST 接口的支持,使得代理作为网络基础设施(network infra),开始融入到了整体的管理中,是 infra as code 的一个落地点。REST 接口能力,提升了代理的被管理能力,也是管理易用性的一部分。外部接口也是可编程的一个重要特征,而 REST 作为最常用的接口形式也广泛的出现在代理服务器领域。
此时可编程就包含了三个层面:#3 中描述的核心模块可编程,动态逻辑可编程,以及提供对外接口供调用的外部接口 可编程。代理服务器集群的出现,体现了扩展性从功能扩展向资源扩展的变化。REST 接口的出现,为进一步的自服务与托管服务提供了技术基础
云时代
第五代代理的演化是云计算普及和高速发展驱动的。云的弹性、自服务、租户、隔离、计量,要求代理服务软件具备云化的能力。
如果说第四代代理是面向系统管理员的,那么第五代代理就是面向云服务的。在充分保持了之前几代代理软件特征的同时,进一步实现了Cloud Ready。
随着云计算向边缘侧拓展,第五代代理也向着硬件异构、软件异构、低能耗的方向发展,因此这一代代理开始呈现了云边一体的能力。
第五代代理在可编程方面进一步演化,从核心模块、动态逻辑、外部接口,增加了云化的能力;包括支持分布式、多租户、可计量等。可计量是多租户的衍生需求,多租户一方面要求隔离,另一方面要求资源可以被尽可能小的粒度进行计量
我们把如上的讨论汇总成一个表格,第一列标识代理所满足的某方面需求;第一行表示不同阶段的代理;在每个单元格里,我们用来表示是否有该类能力,以及能力的程度(1-5个,5个表示充分支持,1个表示基本支持)。同时,我们还列出了各个阶段的标志性软件:
- 配置文件阶段:Squid、Httpd、Nginx。
- 配置语言阶段:Varnish、Haproxy
- 脚本语言阶段:Nginx+Lua、Nginx+JS
- 集群阶段:Kong、Envoy
- 云时代:Istio+Envoy、Linkerd、Pipy
序号 | 需求 | 配置文件阶段 | 配置语言阶段 | 脚本语言阶段 | 集群阶段 | 云时代 | 备注 |
---|---|---|---|---|---|---|---|
1 | 连通性 | * * * * * | * * * * * | * * * * * | * * * * * | * * * * * | 连通性在云时代开始使用内核技术,如 iptables 和 ebpf;之前都只有 user space 进程模式 |
2 | 可靠性 | * * * * * | * * * * * | * * * * * | * * * * * | * * * * * | 可靠性一直是代理软件的最重要的基础能力 |
3 | 高性能 | * * * | * * * * | * * * * * | * * * * * | * * * * * | 性能包括吞吐率、延迟、错误率、偏离均值的幅度。其中延迟采用 P99,P999 等度量指标。早期代理软件有长尾效应,因此 P99 以上指标没有后期软件好。采用高性能脚本的代理,在返回相同内容时,通常性能优于前一代。采用 proactive 技术的代理,在提供相同性能的同时更稳定(偏离均值的幅度更小) |
4 | 灵活性 | * | * * | * * * | * * * * | * * * * * | 第五代代理相比第四代显著的增强了多协议支持能力,因此我们给这一代五星的评价。并且第五代的处理模型可以适配多种协议,具有通用性,这方面要优于第四代 |
5 | 扩展性 | * | * * | * * * | * * * | * * * * | 和 灵活性 类似,第五代代理除了支持核心功能扩展开发、7 层逻辑扩展开发,还支持多协议,因此我们给出比第四代多一星的评价 |
6 | 硬件兼容性 | * * * * | * * * * | * * * * | * * * * | * * * * | 使用 C 或者 C++ 开发的代理,通常在硬件兼容性上都更好一些,社区也更活跃的迁移程序到新的硬件架构。使用 RUST 和 Go 和 Lua 开发的代理,在硬件兼容的迁移进度向相对缓慢 |
7 | 系统兼容性 | * * * | * * * | * * * * | * * * * | * * * * * | 系统 主要包括两个方面,一个是操作系统,一个是云平台。在操作系统兼容性方面,每一代的代理相差不多;但是在云平台兼容性方面,第四代和第五代代理都更充分的考虑并实现了和云的兼容。相比之下,第五代比第四代显著的差异是对多租户的支持能力 |
8 | 管理易用性 | * * | * * | * * | * * * | * * * * | 管理易用性是指针对运维和管理员角色的功能。第一二代主要以配置文件为主,基于此使用配置管理工具实现了自动化和批量的管理。第三代除了管理配置文件,需要进一步管理脚本源文件;但本质上和第一二代的管理易用性没有有显著差异。第四代提供 REST 接口,显著提升了管理的易用性。第五代除了 REST 以外,通常提供了针对云的控制平面,用来管理代理;同时对外提供多种接口以适配其他的管理需求,如监控、审计、统计等 |
9 | 用户易用性 | * | * | * | * * | * * * | 前三代代理主要用户就是运维和管理员。第四代时候,管理员开始把部分功能对用户提供,开始出现 as-a-Service 的模式。第五代则更多的考虑了用户使用的场景,更多的提供了面向租户的能力 |
10 | 开发易用性 | * | * * | * * * | * * * * | * * * * * | 围绕代理的开发包括两个方面,一个是在代理内部实现功能,一个是在代理外边实现对代理的管理能力。前三代都提供了内部开发的接口;后两代同时提供了内外接口。第五代相比第四代显著的改进是提供了云接口 |
11 | 核心接口可编程 | * | * | * | * | * | 每一代代理都提供了核心接口扩展的能力,但是这些接口过于底层,掌握难度较大 |
12 | 功能扩展可编程 | * | * * | * * * | * * * * | * * * * * | 提供更高效的功能扩展能力,是每一代代理都在进步的部分。是 可编程 代理的核心指标 |
13 | 协议扩展可编程 | * * | * * * | 前三代主要面向单一协议,或者固定协议。从第四代开始,用户开始寻求多协议和定制协议的支持。第五代则把协议扩展作为核心能力在设计中就做了充分的考虑 | |||
14 | 结构化脚本编程 | * * * | * * * * | * * * * | 第三代代理开始显著的关注脚本的结构化问题;而第四代和第五代则努力为更加结构化的编程做了准备,如 Envoy 尝试通过 WASM 提供对多语言的支持;pipy 则是引入高性能 JS 脚本,提供更好的结构化 | ||
15 | 配置管理可编程 | * * | * * * | 前三代代理的配置主要面向运维管理人员,外部的配置管理工具都是基于这个前提。第四代开始支持 REST 管理接口;第五代则进一步提供了标准的云接口做配置管理 | |||
16 | 资源扩展可编程 | * | * | * | * * | * * * * | 前三代代理扩容主要是增加线程或者进程的数量。第四代提供了进程的横向扩展能力,也就是集群能力。第五代在第四代基础上,一方面提供了横向扩展能力,一方面提供了更小资源下的能力,以支持更细粒度的计量与计费;也就是不仅支持增量扩展,也提供了减量扩展的能力,而这些能力,都提供编程接口 |
17 | 租户扩展可编程 | * * * | 云是和第四代代理同步出现的事物,租户 作为云的核心特征,并没有在第四代中获得很好的支持。第五代则以云为大前提进行了设计,考虑和提供了租户自己编程扩展的可能性 |
总结
上表中的 #11~#17 是代理 可编程 的具体的多个方面,这些方面也同时构成了 Why Programming Proxy 的答案:
- 代理的内部功能需要扩展,既包括底层核心能力的扩展,也包括支持更多协议的扩展,还包括面向七层的处理能力(转发、路由、判断、访问控制等);这些七层的处理能力,要求更为便捷的编程方式,也就是脚本化、结构化的编程能力
- 代理需要对外部提供接口,以集成到更大的管理体系中(如云平台),包括配置管理、资源管理等
- 代理需要提供面向不同角色的扩展能力,包括运维、管理员、资源提供者、租户,这些扩展能力在某种程度上都需要 可编程
- 同时,就像任何 可编程 组件一样,可编程代理 需要有配套的文档、开发手册、代码管理、依赖管理、构建和部署工具,并且最好有可视化的开发、调试环境。这些得到充分满足以后,用户才能够更好的管理网络流量,以及流量之上所承载的业务