Skip to content

Apache APISIX

作者: ryan 发布于: 1970/1/1 更新于: 1970/1/1 字数: 0 字 阅读: 0 分钟

Apache APISIX 是一个基于 OpenResty 和 Etcd 实现的动态、实时、高性能的 API 网关,目前已经是 Apache 顶级项目。提供了丰富的流量管理功能,如负载均衡、动态路由、动态 upstream、A/B 测试、金丝雀发布、限速、熔断、防御恶意攻击、认证、监控指标、服务可观测性、服务治理等。可以使用 APISIX 来处理传统的南北流量以及服务之间的东西向流量。

img

📎APISIX.xmind

APISIX 基于 Nginx 和 etcd,与传统 API 网关相比,APISIX 具有动态路由和热加载插件功能,避免了配置之后的 reload 操作,同时 APISIX 支持 HTTP(S)、HTTP2、Dubbo、QUIC、MQTT、TCP/UDP 等更多的协议。而且还内置了 Dashboard,提供强大而灵活的界面。同样也提供了丰富的插件支持功能,而且还可以让用户自定义插件。

img

上图是 APISIX 的架构图,整体上分成数据面和控制面两个部分,控制面用来管理路由,主要通过 etcd 来实现配置中心,数据面用来处理客户端请求,通过 APISIX 自身来实现,会不断去 watch etcd 中的 route、upstream 等数据。

APISIX Ingress

同样作为一个 API 网关,APISIX 也支持作为 Kubernetes 的一个 Ingress 控制器进行使用。APISIX Ingress 在架构上分成了两部分,一部分是 APISIX Ingress Controller,作为控制面它将完成配置管理与分发。另一部分 APISIX(代理) 负责承载业务流量。

img

当 Client 发起请求,到达 Apache APISIX 后,会直接把相应的业务流量传输到后端(如 Service Pod),从而完成转发过程。

此过程不需要经过 Ingress Controller,这样做可以保证一旦有问题出现,或者是进行变更、扩缩容或者迁移处理等,都不会影响到用户和业务流量。

同时在配置端,用户通过 kubectl apply 创建资源,可将自定义 CRD 配置应用到 K8s 集群,Ingress Controller 会持续 watch 这些资源变更,来将相应配置应用到 Apache APISIX(通过 admin api)。

从上图可以看出 APISIX Ingress 采用了数据面与控制面的分离架构,所以用户可以选择将数据面部署在 K8s 集群内部或外部。但 Ingress Nginx 是将控制面和数据面放在了同一个 Pod 中,如果 Pod 或控制面出现一点闪失,整个 Pod 就会挂掉,进而影响到业务流量。这种架构分离,给用户提供了比较方便的部署选择,同时在业务架构调整场景下,也方便进行相关数据的迁移与使用。

APISIX Ingress 控制器目前支持的核心特性包括:

  • 全动态,支持高级路由匹配规则,可与 Apache APISIX 官方 50 多个插件 & 客户自定义插件进行扩展使用
  • 支持 CRD,更容易理解声明式配置
  • 兼容原生 Ingress 资源对象
  • 支持流量切分
  • 服务自动注册发现,无惧扩缩容
  • 更灵活的负载均衡策略,自带健康检查功能
  • 支持 gRPC plaintext 与 TCP 4 层代理

安装

我们这里在 Kubernetes 集群中来使用 APISIX,可以通过 Helm Chart 来进行安装,首先添加官方提供的 Helm Chart 仓库:

shell
$ helm repo add apisix https://charts.apiseven.com
$ helm repo update

由于 APISIX 的 Chart 包中包含 dashboard 和 ingress 控制器的依赖,我们只需要在 values 中启用即可安装 ingress 控制器了:

shell
$ helm fetch apisix/apisix
$ tar -xvf apisix-0.7.2.tgz
$ mkdir -p apisix/ci

在 apisix/ci 目录中新建一个用于安装的 values 文件,内容如下所示:

yaml
# ci/prod.yaml
apisix:
  enabled: true

  nodeSelector: # 固定在node2节点上
    kubernetes.io/hostname: node2

gateway:
  type: NodePort
  externalTrafficPolicy: Cluster
  http:
    enabled: true
    servicePort: 80
    containerPort: 9080
  tls:
    enabled: true # 启用 tls
    servicePort: 443
    containerPort: 9443

etcd:
  enabled: true # 会自动创建3个节点的etcd集群
  replicaCount: 1 # 多副本需要修改下模板,这里暂时运行一个etcd pod

dashboard:
  enabled: true

ingress-controller:
  enabled: true
  config:
    apisix:
      serviceName: apisix-admin
      serviceNamespace: apisix # 指定命名空间,如果不是 ingress-apisix 需要重新指定

经测试官方的 Helm Chart 包对 etcd 多节点集群支持不是很好,我测试跑 3 个节点会出问题,另外对外部的 etcd tls 集群兼容度也不好,比如 dashboard 的 Chart 需要自己修改模板去支持 tls,所以这里我们测试先改成 1 个副本的 etcd 集群。

APISIX 需要依赖 etcd,默认情况下 Helm Chart 会自动安装一个 3 副本的 etcd 集群,需要提供一个默认的 StorageClass(存储章节会详细讲解),如果你已经有默认的存储类则可以忽略下面的步骤,这里我们安装一个 nfs 的 provisioner,用下面的命令可以安装一个默认的 StorageClass:

shell
$ helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
$ helm upgrade --install nfs-subdir-external-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
--set nfs.server=192.168.31.31 \
--set nfs.path=/var/lib/k8s/data \
--set image.repository=cnych/nfs-subdir-external-provisioner \
--set storageClass.defaultClass=true -n kube-system

安装完成后会自动创建一个 StorageClass:

shell
$ kubectl get sc
NAME                   PROVISIONER                                     RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
nfs-client (default)   cluster.local/nfs-subdir-external-provisioner   Delete          Immediate              true                   35s

然后直接执行下面的命令进行一键安装:

shell
$ helm upgrade --install apisix ./apisix -f ./apisix/ci/prod.yaml -n apisix
Release "apisix" does not exist. Installing it now.
NAME: apisix
LAST DEPLOYED: Thu Dec 30 16:28:38 2021
NAMESPACE: apisix
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
  export NODE_PORT=$(kubectl get --namespace apisix -o jsonpath="{.spec.ports[0].nodePort}" services apisix-gateway)
  export NODE_IP=$(kubectl get nodes --namespace apisix -o jsonpath="{.items[0].status.addresses[0].address}")
  echo http://$NODE_IP:$NODE_PORT

正常就可以成功部署 apisix 了:

shell
$ kubectl get pods -n apisix
NAME                                         READY   STATUS    RESTARTS   AGE
apisix-dashboard-b69d5c768-r6tqk             1/1     Running   0          85m
apisix-etcd-0                                1/1     Running   0          90m
apisix-fb8cdb569-wz9gq                       1/1     Running   0          87m
apisix-ingress-controller-7d5bbf5dd5-r6khq   1/1     Running   0          85m
$ kubectl get svc -n apisix
NAME                        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
apisix-admin                ClusterIP   10.97.108.252    <none>        9180/TCP                     24h
apisix-dashboard            ClusterIP   10.108.202.136   <none>        80/TCP                       24h
apisix-etcd                 ClusterIP   10.107.150.100   <none>        2379/TCP,2380/TCP            24h
apisix-etcd-headless        ClusterIP   None             <none>        2379/TCP,2380/TCP            24h
apisix-gateway              NodePort    10.97.214.188    <none>        80:32200/TCP,443:31417/TCP   24h
apisix-ingress-controller   ClusterIP   10.103.176.26    <none>        80/TCP                       24h

Dashboard

现在我们可以为 Dashboard 创建一个路由规则,新建一个如下所示的 ApisixRoute 资源对象即可:

yaml
apiVersion: apisix.apache.org/v2beta2
kind: ApisixRoute
metadata:
  name: dashboard
  namespace: apisix
spec:
  http:
    - name: root
      match:
        hosts:
          - apisix.qikqiak.com
        paths:
          - '/*'
      backends:
        - serviceName: apisix-dashboard
          servicePort: 80

创建后 apisix-ingress-controller 会将上面的资源对象通过 admin api 映射成 APISIX 中的配置:

shell
$ kubectl get apisixroute -n apisix
NAME        HOSTS                    URIS     AGE
dashboard   ["apisix.qikqiak.com"]   ["/*"]   75m

所以其实我们的访问入口是 APISIX,而 apisix-ingress-controller 只是一个用于监听 crds,然后将 crds 翻译成 APISIX 的配置的工具而已,现在就可以通过 apisix-gateway 的 NodePort 端口去访问我们的 dashboard 了:

img

当然如果不想在访问的时候域名后面带上端口,在云端环境可以直接将 apisix-gateway 这个 Service 设置成 LoadBalancer 模式,在本地测试的时候可以使用 kubectl port-forward 将服务暴露在节点的 80 端口上:

shell
# node2 节点暴露 apisix-gateway 服务
$ kubectl port-forward --address 0.0.0.0 svc/apisix-gateway 80:80 443:443 -n apisix

默认登录用户名和密码都是 admin,登录后在路由菜单下正常可以看到上面我们创建的这个 dashboard 的路由信息:

img

点击更多下面的查看就可以看到在 APISIX 下面真正的路由配置信息:

img

所以我们要使用 APISIX,也一定要理解其中的路由 Route 这个概念,路由(Route)是请求的入口点,它定义了客户端请求与服务之间的匹配规则,路由可以与服务(Service)、上游(Upstream)关联,一个服务可对应一组路由,一个路由可以对应一个上游对象(一组后端服务节点),因此,每个匹配到路由的请求将被网关代理到路由绑定的上游服务中。

理解了路由后自然就知道了我们还需要一个上游 Upstream 进行关联,这个概念和 Nginx 中的 Upstream 基本是一致的,在上游菜单下可以看到我们上面创建的 dashboard 对应的上游服务:

img

其实就是将 Kubernetes 中的 Endpoints 映射成 APISIX 中的 Upstream,然后我们可以自己在 APISIX 这边进行负载。

APISIX 提供的 Dashboard 功能还是非常全面的,我们甚至都可以直接在页面上进行所有的配置,包括插件这些,非常方便。

img

当然还有很多其他高级的功能,比如流量切分、请求认证等等,这些高级功能在 crds 中去使用则更加方便了,当然也是支持原生的 Ingress 资源对象的,关于 APISIX 的更多用法,后续再进行说明。

自定义插件

除了 APISIX 官方内置的插件之外,我们也可以根据自己的需求去自定义插件,要自定义插件需要使用到 APISIX 提供的 Runner,目前已经支持 Java、Go 和 Python 语言的 Runner,这个 Runner 相当于是 APISIX 和自定义插件之间的桥梁,比如 apache-apisix-python-runner 这个项目通过 Python Runner 可以把 Python 直接应用到 APISIX 的插件开发中,整体架构如下所示:

img

左边是 APISIX 的工作流程,右边的 Plugin Runner 是各语言的插件运行器,当在 APISIX 中配置一个 Plugin Runner 时,APISIX 会启动一个子进程运行 Plugin Runner,该子进程与 APISIX 进程属于同一个用户,当我们重启或重新加载 APISIX 时,Plugin Runner 也将被重启。

如果你为一个给定的路由配置了 ext-plugin-* 插件,请求命中该路由时将触发 APISIX 通过 Unix Socket 向 Plugin Runner 发起 RPC 调用。调用分为两个阶段:

  • ext-plugin-pre-req:在执行 APISIX 内置插件之前
  • ext-plugin-post-req:在执行 APISIX 内置插件之后

接下来我们就以 Python 为例来说明如何自定义插件,首先获取 apache-apisix-python-runner 项目:

shell
$ git clone https://github.com/apache/apisix-python-plugin-runner.git
$ cd apisix-python-plugin-runner
$ git checkout 0.1.0  # 切换刀0.1.0版本

如果是开发模式,则我们可以直接使用下面的命令启动 Python Runner:

shell
$ APISIX_LISTEN_ADDRESS=unix:/tmp/runner.sock python3 apisix/main.py start

启动后需要在 APISIX 配置文件中新增外部插件配置,如下所示:

shell
$ vim /path/to/apisix/conf/config.yaml
apisix:
  admin_key:
    - name: "admin"
      key: edd1c9f034335f136f87ad84b625c8f1
      role: admin

ext-plugin:
  path_for_test: /tmp/runner.sock

通过 ext-plugin.path_for_test 指定 Python Runner 的 unix socket 文件路径即可,如果是生产环境则可以通过 ext-plugin.cmd 来指定 Runner 的启动命令即可:

shell
ext-plugin:
  cmd: [ "python3", "/path/to/apisix-python-plugin-runner/apisix/main.py", "start" ]

我们这里的 APISIX 是运行 Kubernetes 集群中的,所以要在 APISIX 的 Pod 中去执行 Python Runner 的代码,我们自然需要将我们的 Python 代码放到 APISIX 的容器中去,然后安装自定义插件的相关依赖,直接在 APISIX 配置文件中添加上面的配置即可,所以我们这里基于 APISIX 的镜像来重新定制包含插件的镜像,在 apisix-python-plugin-runner 项目根目录下新增如下所示的 Dockerfile 文件:

plain
FROM apache/apisix:2.10.0-alpine

ADD . /apisix-python-plugin-runner

RUN apk add --update python3 py3-pip && \
    cd /apisix-python-plugin-runner && \
    python3 -m pip install --upgrade pip && \
    python3 -m pip install -r requirements.txt --ignore-installed && \
    python3 setup.py install --force

基于上面 Dockerfile 构建一个新的镜像,推送到 Docker Hub:

shell
$ docker build -t cnych/apisix:py3-plugin-2.10.0-alpine .
# 推送到DockerHub
$ docker push cnych/apisix:py3-plugin-2.10.0-alpine

接下来我们需要使用上面构建的镜像来安装 APISIX,我们这里使用的是 Helm Chart 进行安装的,所以需要通过 Values 文件进行覆盖,如下所示:

yaml
# ci/prod.yaml
apisix:
  enabled: true

  image:
    repository: cnych/apisix
    tag: py3-plugin-2.10.0-alpine
......

由于官方的 Helm Chart 没有提供对 ext-plugin 配置的支持,所以需要我们手动修改模板文件 templates/configmap.yaml,在 apisix 属性同级目录下面新增 ext-plugin 相关配置,如下所示:

yaml
{{- if .Values.extPlugins.enabled }}
ext-plugin:
  {{- if .Values.extPlugins.pathForTest }}
  path_for_test: {{ .Values.extPlugins.pathForTest }}
  {{- end }}
  {{- if .Values.extPlugins.cmds }}
  cmd:
  {{- range $cmd := .Values.extPlugins.cmds }}
  - {{ $cmd }}
  {{- end }}
  {{- end }}
{{- end }}

nginx_config:
  user: root  # fix 执行 python runner没权限的问题

然后在定制的 Values 文件中添加如下所示的配置:

yaml
# ci/prod.yaml
extPlugins:
  enabled: true
  cmds: ['python3', '/apisix-python-plugin-runner/apisix/main.py', 'start']

接着就可以重新部署 APISIX 了:

shell
$ helm upgrade --install apisix ./apisix -f ./apisix/ci/prod.yaml -n apisix

部署完成后在 APISIX 的 Pod 中可以看到会启动一个 Python Runner 的子进程:

img

在插件目录 /apisix-python-plugin-runner/apisix/plugins 中的 .py 文件都会被自动加载,上面示例中有两个插件 stop.py 和 rewrite.py,我们以 stop.py 为例进行说明,该插件代码如下所示:

plain
from apisix.runner.plugin.base import Base
from apisix.runner.http.request import Request
from apisix.runner.http.response import Response


class Stop(Base):
    def __init__(self):
        super(Stop, self).__init__(self.__class__.__name__)

    def filter(self, request: Request, response: Response):
        # 可以通过 `self.config` 获取配置信息,如果插件配置为JSON将自动转换为字典结构
        # print(self.config)

        # 设置响应 Header 头
        response.headers["X-Resp-A6-Runner"] = "Python"
        # 设置响应body
        response.body = "Hello, Python Runner of APISIX"
        # 设置响应状态码
        response.status_code = 201

        # 通过调用 `self.stop()` 中断请求流程,此时将立即响应请求给客户端
        # 如果未显示调用 `self.stop()` 或 显示调用 `self.rewrite()`将继续将请求
        # 默认为 `self.rewrite()`
        self.stop()

实现插件首先必须要继承 Base 类,必须实现 filter 函数,插件执行核心业务逻辑就是在 filter 函数中,该函数只包含 Request 和 Response 类对象作为参数,Request 对象参数可以获取请求信息,Response 对象参数可以设置响应信息 ,self.config 可以获取插件配置信息,在 filter 函数中调用 self.stop() 时将马上中断请求,响应数据,调用 self.rewrite() 时,将会继续请求。

然后我们在前面的 Nexus 应用中新增一个路由来测试我们上面的 stop 插件,在 ApisixRoute 对象中新增一个路由规则,如下所示:

yaml
apiVersion: apisix.apache.org/v2beta2
kind: ApisixRoute
metadata:
  name: nexus
  namespace: default
spec:
  http:
    - name: ext
      match:
        hosts:
          - ops.qikqiak.com
        paths:
          - '/extPlugin'
      plugins:
        - name: ext-plugin-pre-req # 启用ext-plugin-pre-req插件
          enable: true
          config:
            conf:
              - name: 'stop' # 使用 stop 这个自定义插件
                value: '{"body":"hello"}'
      backends:
        - serviceName: nexus
          servicePort: 8081

直接创建上面的路由即可,核心配置是启用 ext-plugin-pre-req 插件(前提是在配置文件中已经启用该插件,在 Helm Chart 的 Values 中添加上),然后在 config 下面使用 conf 属性进行配置,conf 为数组格式可以同时设置多个插件,插件配置对象中 name 为插件名称,该名称需要与插件代码文件和对象名称一致,value 为插件配置,可以为 JSON 字符串。

创建后同样在 Dashboard 中也可以看到 APISIX 中的路由配置格式:

img

接着我们可以来访问 http://ops.qikqiak.com/extPlugin 这个路径来验证我们的自定义插件:

shell
$ curl -i http://ops.qikqiak.com/extPlugin
HTTP/1.1 201 Created
Date: Thu, 13 Jan 2022 07:04:50 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
accept: */*
user-agent: curl/7.64.1
host: ops.qikqiak.com
X-Resp-A6-Runner: Python
Server: APISIX/2.10.0

Hello, Python Runner of APISIX

访问请求结果中有一个 X-Resp-A6-Runner: Python 头信息,返回的 body 数据为 Hello, Python Runner of APISIX,和我们在插件中的定义是符合的。到这里就完成了使用 Python 进行 APISIX 自定义插件,我们有任何的业务逻辑需要处理直接去定义一个对应的插件即可。