云原生网关apisix

apisix是什么?

apisix和kong非常相似,是一种基于OpenResty的网关。目前已经进入apache:
项目地址:https://github.com/apache/apisix
这里apisix的开发者写了这样一篇文章介绍为什么要有apisix:https://www.apiseven.com/blog/why-we-need-apache-apisix
这里简单摘要一下nginx和kong的痛点:

  • 原生nginx不支持热更新配置,对于大型集群的网关,重新载入nginx配置会极为耗时,这个问题让他作为重度负载的网关非常致命,所以大多数情况下人们都会使用OpenResty的方案,在lua层面做路由。
  • nginx原生不支持集群方案,只能在外部批量统一配置的情况保证集群一致。
  • kong不支持mysql,而且可见的未来内似乎也没有支持的计划 https://github.com/Kong/kong/issues/1867 ┓( ´∀` )┏,一些社区支持的mysql-kong也都处于数年未更新的状态。对于国内mysql远流行于PG的环境不是很友好。
  • Kong 的路由使用的是遍历查找,当网关内有超过上千个路由时,它的性能就会出现比较急剧的下降。

apisix的架构

先来认识下其基本架构
1
与kong将元数据保存在PG中不同,apisix选择将元数据保存在etcd。etcd能提供更强大的可用性,以及数据更新的及时性,能够使用etcd的watch机制毫秒级将控制面对配置的修改传播到数据面。

数据面架构如下:
3
也是基于OpenResty。按照文档的描述,apisix支持丰富的插件,lua插件,多语言插件运行器,wasm支持。

部署

我这里就直接helm部署到Kubernetes了。

1
2
3
helm repo add apisix https://charts.apiseven.com
helm repo update
helm install apisix apisix/apisix --create-namespace --namespace apisix

可以调整下其中的配置把dashboard打开。

4
和konga非常类似。

启动好了大概这些容器:

1
2
3
4
5
6
7
8
[root@node1 apisix]# kubectl get po -napisix
NAME READY STATUS RESTARTS AGE
apisix-7dfd477dbd-bh82q 1/1 Running 1 (7h40m ago) 17h
apisix-dashboard-777b9d7f5b-wt5sq 1/1 Running 3 (7h39m ago) 17h
apisix-etcd-0 1/1 Running 1 (7h40m ago) 16h
apisix-etcd-1 1/1 Running 1 (7h40m ago) 17h
apisix-etcd-2 1/1 Running 1 (7h40m ago) 17h

一套etcd,apisix网关本体,和一个dashboard。

此外为了便于测试rewrite相关插件的效果,我这边还部署了一套httpbin:
镜像 : kennethreitz/httpbin

1
2
3
4
5
6
7
8
9
[root@node1 apisix]# kubectl get po -napisix
NAME READY STATUS RESTARTS AGE
apisix-7dfd477dbd-bh82q 1/1 Running 1 (7h40m ago) 17h
apisix-dashboard-777b9d7f5b-wt5sq 1/1 Running 3 (7h39m ago) 17h
apisix-etcd-0 1/1 Running 1 (7h40m ago) 16h
apisix-etcd-1 1/1 Running 1 (7h40m ago) 17h
apisix-etcd-2 1/1 Running 1 (7h40m ago) 17h
httpbin-6bbddd855-fd7qj 1/1 Running 0 33m

并建立httpbin对应的service。

实验

  1. 创建路由

5
我们基于域名匹配

6
为httpbin

插件配置里可以选择普通模式和编排模式,编排模式可视化的插件拼接还是比较酷的。这里我就先不配置插件了。

7

生成路由后,就可以通过NodePort 访问apisix的apisix-gateway这个service。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[root@node1 apisix]# curl -iL -X GET "http://192.168.31.170:30599/get?foo1=bar1&foo2=bar2" -H "Host: abc.com"
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 278
Connection: keep-alive
Date: Wed, 05 Apr 2023 10:30:07 GMT
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Server: APISIX/3.2.0

{
"args": {
"foo1": "bar1",
"foo2": "bar2"
},
"headers": {
"Accept": "*/*",
"Host": "abc.com",
"User-Agent": "curl/7.29.0",
"X-Forwarded-Host": "abc.com"
},
"origin": "192.168.31.170",
"url": "http://abc.com/get?foo1=bar1&foo2=bar2"
}

然后我们加些插件试试
添加一个重新请求的。
8
点开proxy-rewrite插件 配置上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"disable": false,
"headers": {
"add": {
"X-Request-ID": "112233"
},
"remove": [
"X-test"
],
"set": {
"X-Api-Engine": "apisix",
"X-Api-Version": "v1",
"X-Api-useless": "",
"X-remote_addr": "$remote_addr",
"X-remote_port": "$remote_port"
}
}
}

其中add是用于添加请求头,remove用于删除请求头,set用于覆盖请求头。

client -> 代理重写 -> upstream(httpbin) -> client

这里请求到httpbin,整个http请求将会被封装为json返回。我们看看代理重写的插件是否能加上我们的几个请求头。

再次请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[root@node1 apisix]# curl -iL -X GET "http://192.168.31.170:30599/get?foo1=bar1&foo2=bar2" -H "Host: abc.com"
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 408
Connection: keep-alive
Date: Wed, 05 Apr 2023 10:40:54 GMT
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Server: APISIX/3.2.0

{
"args": {
"foo1": "bar1",
"foo2": "bar2"
},
"headers": {
"Accept": "*/*",
"Host": "abc.com",
"User-Agent": "curl/7.29.0",
"X-Api-Engine": "apisix",
"X-Api-Version": "v1",
"X-Forwarded-Host": "abc.com",
"X-Remote-Addr": "192.168.31.170",
"X-Remote-Port": "31539"
},
"origin": "192.168.31.170",
"url": "http://abc.com/get?foo1=bar1&foo2=bar2"
}

再测试下限流插件:
9

点开插件配置为

1
2
3
4
5
6
7
{
"rate": 1,
"burst": 0,
"key_type": "var",
"key": "remote_addr",
"rejected_code": 503
}

反复请求会报出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@node1 apisix]# curl -iL -X GET "http://192.168.31.170:30599/get?foo1=bar1&foo2=bar2" -H "Host: abc.com"
HTTP/1.1 503 Service Temporarily Unavailable
Date: Wed, 05 Apr 2023 10:50:30 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 269
Connection: keep-alive
Server: APISIX/3.2.0

<html>
<head><title>503 Service Temporarily Unavailable</title></head>
<body>
<center><h1>503 Service Temporarily Unavailable</h1></center>
<hr><center>openresty</center>
<p><em>Powered by <a href="https://apisix.apache.org/">APISIX</a>.</em></p></body>
</html>
[root@node1 apisix]#

其他插件不一一测试了,文档非常详细。

https

在证书中配置了证书后,https访问对应域名会自动使用相关证书。我们用cfssl生成一个自签名证书证书试试。将其上传到证书里:
10

这里补充个知识点 SNI和SAN有什么区别:
SAN和SNI都是在SSL证书中用于多域名支持的技术,不过它们的作用不同:
● SAN(Subject Alternative Names):是在证书中设置的一个字段,用于指定该证书可以绑定哪些域名或主机名。在TLS握手的过程中,客户端会将请求的域名信息发送给服务器,在服务器返回证书的时候,如果证书中包含了请求的域名,就可以建立安全连接。SAN可以实现一个证书绑定多个域名的效果,比如 *.example.com 和 www.example.com 可以在同一个证书中绑定,也可以绑定不同的二级域名或顶级域名。
● SNI(Server Name Indication):是在TLS握手的过程中,客户端发送请求的域名给服务器的一个扩展字段。服务器根据这个字段,选择使用哪个SSL证书返回给客户端。SNI可以实现同一个IP地址上部署多个域名,使用不同的SSL证书加密通信的效果。在使用SNI前,客户端和服务器之间的通信只能使用IP地址和端口号,无法实现多域名共享一个IP地址的效果。
因此,SAN和SNI虽然都与SSL证书中多域名支持有关,但是作用不同。SAN是证书中的一个字段,用于指定可以绑定的多个域名;而SNI是TLS握手过程中的一个扩展字段,用于选择使用哪个SSL证书返回给客户端,实现同一个IP地址上部署多个域名的效果。

GRPC转码

还有个强大的功能是将外界的http请求转为grpc。

这里我用tonic中的example快速启动了一个helloworld server
https://github.com/hyperium/tonic/blob/master/examples/src/helloworld/server.rs

里面的监听地址改为0.0.0.0:50051即可部署在k8s集群节点上或者pod内都可以。

对应的proto为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
syntax = "proto3";

option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";

package helloworld;

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

调用admin API配置proto协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
curl http://node1:31004/apisix/admin/protos/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"content" : "syntax = \"proto3\";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}"
}'

配置路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
curl http://node1:31004/apisix/admin/routes/111 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"methods": ["GET"],
"uri": "/grpctest",
"plugins": {
"grpc-transcode": {
"proto_id": "1",
"service": "helloworld.Greeter",
"method": "SayHello"
}
},
"upstream": {
"scheme": "grpc",
"type": "roundrobin",
"nodes": {
"node1:50051": 1
}
}
}'

这里指明了我们需要的grpc转码插件。并指定了proto配置id。

最后我们验证下:

1
2
3
 curl -i http://node1:31001/ggrpc?name=world

{"message":"Hello world!"}

云原生

除了网关之外,apisix也推出了ServiceMesh,总的来说是想东西南北流量一把抓。但是总的来说,apisix最合适的定位还是ingress-controller。

此外,Gateway API 是 Kubernetes 中下一代的 Ingress 规范,致力于提供富有表现力,可扩展和面向角色的接口来发展 Kubernetes 的网络,各个 Ingress controller 项目都在积极推进对该规范的支持。

Gateway 对比 Ingress 有那些改进

  • Gateway支持基于头的匹配,支持对请求头增删修改,这些功能之前只能将相关配置放在annotation中的方式,由ingress自行处理。 现在这些配置终于被k8s收编了。
  • 跨命名空间引用,ingress-class虽然可以跨命名空间生效,但是ingress API却只能对当前命名空间生效,有时我们想代理另一个命名空间的资源,但是没有这个命名空间的权限就比较蛋疼了。Gateway目前是支持跨命名空间引用服务的。

还有其他一些优点这里就不一一列举了。总之后续ingress有可能被Gateway Api取代,目前nginx-ingress还没支持Gateway的计划,但是apisix目前已经支持了,这也是选择apisix一个有分量的理由。

  • 本文作者: fenix
  • 本文链接: https://fenix0.com/try-apisix/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC 许可协议。转载请注明出处!