刚哥谈架构 (十一) 开源API网关架构分析

春未老,风细柳斜斜。试上超然台上望,半壕春水一城花。烟雨暗千家。
寒食后,酒醒却咨嗟。休对故人思故国,且将新火试新茶。诗酒趁年华。
苏轼·送《望江南·超然台作》

春天来了,樱花开了,刚哥我今天就在这春色中和大家探讨一下API Gateway。

 

 

在微服务的架构下,API网关是一个常见的架构设计模式。以下是微服务中常见的问题,需要引入API网关来协助解决。

  • 微服务提供的API的粒度通常与客户端所需的粒度不同。微服务通常提供细粒度的API,这意味着客户端需要与多个服务进行交互。例如,如上所述,需要产品详细信息的客户需要从众多服务中获取数据。
  • 不同的客户端需要不同的数据。例如,产品详细信息页面桌面的桌面浏览器版本通常比移动版本更为详尽。
  • 对于不同类型的客户端,网络性能是不同的。例如,与非移动网络相比,移动网络通常要慢得多并且具有更高的延迟。而且,当然,任何WAN都比LAN慢得多。这意味着本机移动客户端使用的网络性能与服务器端Web应用程序使用的LAN的性能差异很大。服务器端Web应用程序可以向后端服务发出多个请求,而不会影响用户体验,而移动客户端只能提供几个请求。
  • 微服务实例数量及其位置(主机+端口)动态变化
  • 服务划分会随着时间的推移而变化,应该对客户端隐藏
  • 服务可能会使用多种协议,其中一些协议可能对网络不友好

常见的API网关主要提供以下的功能:

  • 反向代理和路由 - 大多数项目采用网关的解决方案的最主要的原因。给出了访问后端API的所有客户端的单一入口,并隐藏内部服务部署的细节。
  • 负载均衡 - 网关可以将单个传入的请求路由到多个后端目的地。
  • 身份验证和授权 - 网关应该能够成功进行身份验证并仅允许可信客户端访问API,并且还能够使用类似RBAC等方式来授权。
  • IP列表白名单/黑名单 - 允许或阻止某些IP地址通过。
  • 性能分析 - 提供一种记录与API调用相关的使用和其他有用度量的方法。
  • 限速和流控 - 控制API调用的能力。
  • 请求变形 - 在进一步转发之前,能够在转发之前转换请求和响应(包括Header和Body)。
  • 版本控制 - 同时使用不同版本的API选项或可能以金丝雀发布或蓝/绿部署的形式提供慢速推出API
  • 断路器 - 微服务架构模式有用,以避免使用中断
  • 多协议支持 WebSocket/GRPC
  • 缓存 - 减少网络带宽和往返时间消耗,如果可以缓存频繁要求的数据,则可以提高性能和响应时间
  • API文档 - 如果计划将API暴露给组织以外的开发人员,那么必须考虑使用API文档,例如Swagger或OpenAPI。

有很多的开源软件可以提供API 网关的支持,下面我们就看看他们各自的架构和功能。

为了对这些开源网关进行基本功能的验证,我创建了一些代码,使用OpenAPI生成了四个基本的API服务,包含Golang,Nodejs,Python Flask和Java Spring。API使用了常见的宠物商店的样例,声明如下:

openapi: "3.0.0"
info:
  version: 1.0.0
  title: Swagger Petstore
  license:
    name: MIT
servers:
  - url: http://petstore.swagger.io/v1
paths:
  /pets:
    get:
      summary: List all pets
      operationId: listPets
      tags:
        - pets
      parameters:
        - name: limit
          in: query
          description: How many items to return at one time (max 100)
          required: false
          schema:
            type: integer
            format: int32
      responses:
        '200':
          description: A paged array of pets
          headers:
            x-next:
              description: A link to the next page of responses
              schema:
                type: string
          content:
            application/json:    
              schema:
                $ref: "#/components/schemas/Pets"
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      summary: Create a pet
      operationId: createPets
      tags:
        - pets
      responses:
        '201':
          description: Null response
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /pets/{petId}:
    get:
      summary: Info for a specific pet
      operationId: showPetById
      tags:
        - pets
      parameters:
        - name: petId
          in: path
          required: true
          description: The id of the pet to retrieve
          schema:
            type: string
      responses:
        '200':
          description: Expected response to a valid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Pet"
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
components:
  schemas:
    Pet:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        tag:
          type: string
    Pets:
      type: array
      items:
        $ref: "#/components/schemas/Pet"
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: integer
          format: int32
        message:
          type: string

构建好的Web服务通过Docker Compose来进行容器化的部署。

version: "3.7"
services:
  goapi:
    container_name: goapi
    image: naughtytao/goapi:0.1
    ports:
      - "18000:8080"
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 256M
        reservations:
          memory: 256M
  nodeapi:
    container_name: nodeapi
    image: naughtytao/nodeapi:0.1
    ports:
      - "18001:8080"
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 256M
        reservations:
          memory: 256M
  flaskapi:
    container_name: flaskapi
    image: naughtytao/flaskapi:0.1
    ports:
      - "18002:8080"
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 256M
        reservations:
          memory: 256M
  springapi:
    container_name: springapi
    image: naughtytao/springapi:0.1
    ports:
      - "18003:8080"
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 256M
        reservations:
          memory: 256M

 

 

我们在学习这些开源网关架构的同时,也会对其最基本的路由转发功能作出验证。这里用户发送的请求
http://server/service_name/v1/pets会发送给API网关,网关通过service name来路由到不同的后端服务。

 

我们使用K6用100个并发跑1000次测试的结果如上图,我们看到直连的综合响应,每秒可以处理的请求数量大概是 1100+

Nginx

Nginx是异步框架的网页服务器,也可以用作反向代理、负载平衡器和HTTP缓存。该软件由伊戈尔·赛索耶夫创建并于2004年首次公开发布。2011年成立同名公司以提供支持。2019年3月11日,Nginx公司被F5 Networks以6.7亿美元收购。

Nginx有以下的特点

  1. 由C编写,占用的资源和内存低,性能高。
  2. 单进程多线程,当启动nginx服务器,会生成一个master进程,master进程会fork出多个worker进程,由worker线程处理客户端的请求。
  3. 支持反向代理,支持7层负载均衡(拓展负载均衡的好处)。
  4. 高并发,nginx是异步非阻塞型处理请求,采用的epollandqueue模式
  5. 处理静态文件速度快
  6. 高度模块化,配置简单。社区活跃,各种高性能模块出品迅速。

 

如上图所示,Nginx主要由Master,Worker和Proxy Cache三个部分组成。

  • Master 主控:
    NGINX遵循主从架构。它将根据客户的要求为Worker分配工作。将工作分配给Worker后,Master将寻找客户的下一个请求,因为它不会等待Worker的响应。一旦响应来自Worker,Master就会将响应发送给客户端
  • Worker 工作单元:
    Worker是NGINX架构中的Slave。每个工作单元可以单线程方式一次处理1000个以上的请求。一旦处理完成,响应将被发送到主服务器。单线程将通过在相同的内存空间而不是不同的内存空间上工作来节省RAM和ROM的大小。多线程将在不同的内存空间上工作。
  • Cache 缓存:
    Nginx缓存用于通过从缓存而不是从服务器获取来非常快速地呈现页面。在第一个页面请求时,页面将被存储在高速缓存中。

为了实现API的路由转发,需要只需要对Nginx作出如下的配置:

server {
    listen 80 default_server; 

    location /goapi {
        rewrite ^/goapi(.*) $1 break;
        proxy_pass  http://goapi:8080;
    }

    location /nodeapi {
        rewrite ^/nodeapi(.*) $1 break;
        proxy_pass  http://nodeapi:8080;
    }

    location /flaskapi {
        rewrite ^/flaskapi(.*) $1 break;
        proxy_pass  http://flaskapi:8080;
    }

    location /springapi {
        rewrite ^/springapi(.*) $1 break;
        proxy_pass  http://springapi:8080;
    }
}

 

我们基于不同的服务goapi,nodeapi,flaskapi和springapi,分别配置一条路由,在转发之前,需要利用rewrite来去掉服务名,并发送给对应的服务。

使用容器把ngnix和后端的四个服务部署在同一个网络下,通过网关连接路由转发的。Nginx的部署如下:

version: "3.7"
services:
  web:
    container_name: nginx
    image: nginx
    volumes:
      - ./templates:/etc/nginx/templates
      - ./conf/default.conf:/etc/nginx/conf.d/default.conf
    ports:
      - "8080:80"
    environment:
      - NGINX_HOST=localhost
      - NGINX_PORT=80
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 256M
        reservations:
          memory: 256M

K6通过Nginx网关的测试结果如下:

 

每秒处理的请求数量是1093,和不通过网关转发相比非常接近。

从功能上看,Nginx可以满足用户对于API网关的大部分需求,可以通过配置和插件的方式来支持不同的功能,性能非常优秀,缺点是没有管理的UI和管理API,大部分的工作都需要手工配置config文件的方式来进行。商业版本的功能会更加完善。

 

Kong

Kong是基于NGINX和 OpenResty的开源API网关。

Kong的总体基础结构由三个主要部分组成:NGINX提供协议实现和工作进程管理,OpenResty提供Lua集成并挂钩到NGINX的请求处理阶段,而Kong本身利用这些挂钩来路由和转换请求。数据库支持Cassandra或Postgres存储所有配置。

 

Kong附带各种插件,提供访问控制,安全性,缓存和文档等功能。它还允许使用Lua语言编写和使用自定义插件。Kong也可以部署为Kubernetes Ingress并支持GRPC和WebSockets代理。

NGINX提供了强大的HTTP服务器基础结构。它处理HTTP请求处理,TLS加密,请求日志记录和操作系统资源分配(例如,侦听和管理客户端连接以及产生新进程)。

NGINX具有一个声明性配置文件,该文件位于其主机操作系统的文件系统中。虽然仅通过NGINX配置就可以实现某些Kong功能(例如,基于请求的URL确定上游请求路由),但修改该配置需要一定级别的操作系统访问权限,以编辑配置文件并要求NGINX重新加载它们,而Kong允许用户执行以下操作:通过RESTful HTTP API更新配置。Kong的NGINX配置是相当基本的:除了配置标准标头,侦听端口和日志路径外,大多数配置都委托给OpenResty。

在某些情况下,在Kong的旁边添加自己的NGINX配置非常有用,例如在API网关旁边提供静态网站。在这种情况下,您可以修改Kong使用的配置模板。

NGINX处理的请求经过一系列阶段。NGINX的许多功能(例如,使用C语言编写的模块)都提供了进入这些阶段的功能(例如,使用gzip压缩的功能)。虽然可以编写自己的模块,但是每次添加或更新模块时都必须重新编译NGINX。为了简化添加新功能的过程,Kong使用了OpenResty。

OpenResty是一个软件套件,捆绑了NGINX,一组模块,LuaJIT和一组Lua库。其中最主要的是ngx_http_lua_module一个NGINX模块,该模块嵌入Lua并为大多数NGINX请求阶段提供Lua等效项。这有效地允许在Lua中开发NGINX模块,同时保持高性能(LuaJIT相当快),并且Kong用它来提供其核心配置管理和插件管理基础结构。

Kong通过其插件体系结构提供了一个框架,可以挂接到上述请求阶段。从上面的示例开始,Key Auth和ACL插件都控制客户端(也称为使用者)是否应该能够发出请求。每个插件都在其处理程序中定义了自己的访问函数,并且该函数针对通过给定路由或服务启用的每个插件执行kong.access()。执行顺序由优先级值决定-如果Key Auth的优先级为1003,ACL的优先级为950,则Kong将首先执行Key Auth的访问功能,如果它不放弃请求,则将执行ACL,然后再通过将该ACL传递给上游proxy_pass。

由于Kong的请求路由和处理配置是通过其admin API控制的,因此可以在不编辑底层NGINX配置的情况下即时添加和删除插件配置,因为Kong本质上提供了一种在API中注入位置块(通过API定义)和配置的方法。它们(通过将插件,证书等分配给这些API)。

我们使用以下的配置部署Kong到容器中(省略四个微服务的部署)

version: '3.7'

volumes:
  kong_data: {}

networks:
  kong-net:
    external: false

services:
  kong:
    image: "${KONG_DOCKER_TAG:-kong:latest}"
    user: "${KONG_USER:-kong}"
    depends_on:
      - db
    environment:
      KONG_ADMIN_ACCESS_LOG: /dev/stdout
      KONG_ADMIN_ERROR_LOG: /dev/stderr
      KONG_ADMIN_LISTEN: '0.0.0.0:8001'
      KONG_CASSANDRA_CONTACT_POINTS: db
      KONG_DATABASE: postgres
      KONG_PG_DATABASE: ${KONG_PG_DATABASE:-kong}
      KONG_PG_HOST: db
      KONG_PG_USER: ${KONG_PG_USER:-kong}
      KONG_PROXY_ACCESS_LOG: /dev/stdout
      KONG_PROXY_ERROR_LOG: /dev/stderr
      KONG_PG_PASSWORD_FILE: /run/secrets/kong_postgres_password
    secrets:
      - kong_postgres_password
    networks:
      - kong-net
    ports:
      - "8080:8000/tcp"
      - "127.0.0.1:8001:8001/tcp"
      - "8443:8443/tcp"
      - "127.0.0.1:8444:8444/tcp"
    healthcheck:
      test: ["CMD", "kong", "health"]
      interval: 10s
      timeout: 10s
      retries: 10
    restart: on-failure
    deploy:
      restart_policy:
        condition: on-failure
    
  db:
    image: postgres:9.5
    environment:
      POSTGRES_DB: ${KONG_PG_DATABASE:-kong}
      POSTGRES_USER: ${KONG_PG_USER:-kong}
      POSTGRES_PASSWORD_FILE: /run/secrets/kong_postgres_password
    secrets:
      - kong_postgres_password
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "${KONG_PG_USER:-kong}"]
      interval: 30s
      timeout: 30s
      retries: 3
    restart: on-failure
    deploy:
      restart_policy:
        condition: on-failure
    stdin_open: true
    tty: true
    networks:
      - kong-net
    volumes:
      - kong_data:/var/lib/postgresql/data
secrets:
  kong_postgres_password:
    file: ./POSTGRES_PASSWORD

数据库选择了PostgreSQL

开源版本没有Dashboard,我们使用RestAPI创建所有的网关路由:

	curl -i -X POST http://localhost:8001/services \
  		--data name=goapi \
  		--data url='http://goapi:8080'
	curl -i -X POST http://localhost:8001/services/goapi/routes \
		--data 'paths[]=/goapi' \
  		--data name=goapi

需要先创建一个service,然后在该service下创建一条路由。

使用K6压力测试的结果如下:

 

每秒请求数705要明显弱于Nginx,所以所有的功能都是有成本的。

 

APISIX

Apache APISIX 是一个动态、实时、高性能的 API 网关, 提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。

APISIX于2019年4月由中国的支流科技创建,于6月开源,并于同年10月进入Apache孵化器。支流科技对应的商业化产品的名字叫API7 :)。APISIX旨在处理大量请求,并具有较低的二次开发门槛。

APISIX的主要功能和特点有:

  • 云原生设计,轻巧且易于容器化
  • 集成了统计和监视组件,例如Prometheus,Apache Skywalking和Zipkin。
  • 支持gRPC,Dubbo,WebSocket,MQTT等代理协议,以及从HTTP到gRPC的协议转码,以适应各种情况
  • 担当OpenID依赖方的角色,与Auth0,Okta和其他身份验证提供程序的服务连接
  • 通过在运行时动态执行用户功能来支持无服务器,从而使网关的边缘节点更加灵活
  • 支持插件热加载
  • 不锁定用户,支持混合云部署架构
  • 网关节点无状态,可以灵活扩展

从这个角度来看,API网关可以替代Nginx来处理南北流量,也可以扮演Istio控制平面和Envoy数据平面的角色来处理东西向流量。

APISIX的架构如下图所示:

 

APISIX包含一个数据平面,用于动态控制请求流量;一个用于存储和同步网关数据配置的控制平面,一个用于协调插件的AI平面,以及对请求流量的实时分析和处理。

它构建在Nginx反向代理服务器和键值存储etcd的之上,以提供轻量级的网关。它主要用Lua编写,Lua是类似于Python的编程语言。它使用Radix树进行路由,并使用前缀树进行IP匹配。

使用etcd而不是关系数据库来存储配置可以使它更接近云原生,但是即使在任何服务器宕机的情况下,也可以确保整个网关系统的可用性。

所有组件都是作为插件编写的,因此其模块化设计意味着功能开发人员只需要关心自己的项目即可。

内置的插件包括流控和速度限制,身份认证,请求重写,URI重定向,开放式跟踪和无服务器。APISIX支持OpenResty和Tengine运行环境,并且可以在Kubernetes的裸机上运行。它同时支持X86和ARM64。

我们同样使用Docker Compose来部署APISIX

version: "3.7"

services:
  apisix-dashboard:
    image: apache/apisix-dashboard:2.4
    restart: always
    volumes:
    - ./dashboard_conf/conf.yaml:/usr/local/apisix-dashboard/conf/conf.yaml
    ports:
    - "9000:9000"
    networks:
      apisix:
        ipv4_address: 172.18.5.18

  apisix:
    image: apache/apisix:2.3-alpine
    restart: always
    volumes:
      - ./apisix_log:/usr/local/apisix/logs
      - ./apisix_conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro
    depends_on:
      - etcd
    ##network_mode: host
    ports:
      - "8080:9080/tcp"
      - "9443:9443/tcp"
    networks:
      apisix:
        ipv4_address: 172.18.5.11
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 256M
        reservations:
          memory: 256M

  etcd:
    image: bitnami/etcd:3.4.9
    user: root
    restart: always
    volumes:
      - ./etcd_data:/etcd_data
    environment:
      ETCD_DATA_DIR: /etcd_data
      ETCD_ENABLE_V2: "true"
      ALLOW_NONE_AUTHENTICATION: "yes"
      ETCD_ADVERTISE_CLIENT_URLS: "http://0.0.0.0:2379"
      ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"
    ports:
      - "2379:2379/tcp"
    networks:
      apisix:
        ipv4_address: 172.18.5.10

networks:
  apisix:
    driver: bridge
    ipam:
      config:
      - subnet: 172.18.0.0/16

开源的APISIX支持Dashboard的方式来管理路由,而不是像KONG把仪表盘功能限制在商业版本中。但是APISIX的仪表盘不支持对路由URI进行改写,所以我们只好使用RestAPI来创建路由。

创建一个服务的路由的命令如下:

curl --location --request PUT 'http://127.0.0.1:8080/apisix/admin/routes/1' \
--header 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
--header 'Content-Type: text/plain' \
--data-raw '{
    "uri": "/goapi/*",
    "plugins": {
        "proxy-rewrite": {
            "regex_uri": ["^/goapi(.*)$","$1"]
        }
    },
    "upstream": {
        "type": "roundrobin",
        "nodes": {
            "goapi:8080": 1
        }
    }
}'

使用K6压力测试的结果如下:

 

APISix取得了1155的好成绩,表现出接近不经过网关的性能,可能缓存起到了很好的效果。

 

Tyk

Tyk是一款基于Golang和Redis构建的开源API网关。它于2014年创建,比AWS的API网关即服务功能早。Tyk用Golang编写,并使用Golang自己的HTTP服务器。

Tyk支持不同的运行方式:云,混合(在自己的基础架构中为GW)和本地。

 

Tyk由3个组件组成:

  • 网关:处理所有应用流量的代理。
  • 仪表板:可以从中管理Tyk,显示指标和组织API的界面。
  • Pump:负责持久保存指标数据,并将其导出到MongoDB(内置),ElasticSearch或InfluxDB等。

我们同样使用Docker Compose来创建Tyk 网关来进行功能验证。

version: '3.7'
services:
  tyk-gateway:
    image: tykio/tyk-gateway:v3.1.1
    ports:
      - 8080:8080
    volumes:
      - ./tyk.standalone.conf:/opt/tyk-gateway/tyk.conf
      - ./apps:/opt/tyk-gateway/apps
      - ./middleware:/opt/tyk-gateway/middleware
      - ./certs:/opt/tyk-gateway/certs
    environment:
      - TYK_GW_SECRET=foo
    depends_on:
      - tyk-redis
  tyk-redis:
    image: redis:5.0-alpine
    ports:
      - 6379:6379

Tyk的Dashboard也是属于商业版本的范畴,所我们又一次需要借助API来创建路由,Tyk是通过app的概念来创建和管理路由的,你也可以直接写app文件。

curl --location --request POST 'http://localhost:8080/tyk/apis/' \
--header 'x-tyk-authorization: foo' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "GO API",
    "slug": "go-api",
    "api_id": "goapi",
    "org_id": "goapi",
    "use_keyless": true,
    "auth": {
      "auth_header_name": "Authorization"
    },
    "definition": {
      "location": "header",
      "key": "x-api-version"
    },
    "version_data": {
      "not_versioned": true,
      "versions": {
        "Default": {
          "name": "Default",
          "use_extended_paths": true
        }
      }
    },
    "proxy": {
      "listen_path": "/goapi/",
      "target_url": "http://host.docker.internal:18000/",
      "strip_listen_path": true
    },
    "active": true
}'

使用K6压力测试的结果如下:

 

Tyk的结果在400-600左右,性能上和KONG接近。

 

Zuul

Zuul是Netflix开源的基于Java的API网关组件。

 

Zuul包含多个组件:

  • zuul-core:该库包含编译和执行过滤器的核心功能。
  • zuul-simple-webapp:该Webapp展示了一个简单的示例,说明如何使用zuul-core构建应用程序。
  • zuul-netflix:将其他NetflixOSS组件添加到Zuul的库-例如,使用Ribbon路由请求。
  • zuul-netflix-webapp: 将zuul-core和zuul-netflix打包到一个易于使用的程序包中的webapp。

Zuul提供了灵活性和弹性,部分是通过利用其他Netflix OSS组件进行的:

  • Hystrix 用于流控。包装对始发地的呼叫,这使我们可以在发生问题时丢弃流量并确定流量的优先级。
  • Ribbon 是来自Zuul的所有出站请求的客户,它提供有关网络性能和错误的详细信息,并处理软件负载平衡以实现均匀的负载分配。
  • Turbine 实时汇总细粒度的指标,以便我们可以快速观察问题并做出反应。
  • Archaius 处理配置并提供动态更改属性的能力。

Zuul的核心是一系列过滤器,它们能够在路由HTTP请求和响应期间执行一系列操作。以下是Zuul过滤器的主要特征:

  • 类型:通常定义路由流程中应用过滤器的阶段(尽管它可以是任何自定义字符串)
  • 执行顺序:在类型中应用,定义跨多个过滤器的执行顺序
  • 准则:执行过滤器所需的条件
  • 动作:如果符合条件,则要执行的动作
class DeviceDelayFilter extends ZuulFilter {

    def static Random rand = new Random()
    
    @Override
    String filterType() {
       return 'pre'
    }

    @Override
    int filterOrder() {
       return 5
    }

    @Override
    boolean shouldFilter() {
       return  RequestContext.getRequest().
       getParameter("deviceType")?equals("BrokenDevice"):false
    }

    @Override
    Object run() {
       sleep(rand.nextInt(20000)) // Sleep for a random number of
                                  // seconds between [0-20]
    }
}

 

Zuul提供了一个框架来动态读取,编译和运行这些过滤器。过滤器不直接相互通信-而是通过每个请求唯一的RequestContext共享状态。过滤器使用Groovy编写。

 

有几种与请求的典型生命周期相对应的标准过滤器类型:

  • Pre过滤器在路由到原点之前执行。示例包括请求身份验证,选择原始服务器以及记录调试信息。
  • Route路由过滤器处理将请求路由到源。这是使用Apache HttpClient或Netflix Ribbon构建和发送原始HTTP请求的地方。
  • 在将请求路由到源之后,将执行Post过滤器。示例包括将标准HTTP标头添加到响应,收集统计信息和指标以及将响应从源流传输到客户端。
  • 在其他阶段之一发生错误时,将执行Error过滤器。

Spring Cloud创建了一个嵌入式Zuul代理,以简化一个非常常见的用例的开发,在该用例中,UI应用程序希望代理对一个或多个后端服务的调用。此功能对于用户界面代理所需的后端服务很有用,从而避免了为所有后端独立管理CORS和身份验证问题的需求 。

要启用它,请使用@EnableZuulProxy注解一个Spring Boot主类,这会将本地调用转发到适当的服务。

Zuul是Java的一个库,他并不是一款开箱即用的API网关,所以需要用Zuul开发一个应用来对其功能进行测试。

对应的Java的POM如下:

<project
    xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>naughtytao.apigateway</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.4.7.RELEASE</version>
        <relativePath />
        <!-- lookup parent from repository -->
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <!-- Dependencies -->
        <spring-cloud.version>Camden.SR7</spring-cloud.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>

        <!-- enable authentication if security is included -->
        <!-- <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency> -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- API, java.xml.bind module -->
        <dependency>
            <groupId>jakarta.xml.bind</groupId>
            <artifactId>jakarta.xml.bind-api</artifactId>
            <version>2.3.2</version>
        </dependency>

        <!-- Runtime, com.sun.xml.bind module -->
        <dependency>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-runtime</artifactId>
            <version>2.3.2</version>
        </dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.junit.jupiter</groupId>
			<artifactId>junit-jupiter-api</artifactId>
			<version>5.0.0-M5</version>
			<scope>test</scope>
		</dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.3</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
        </plugins>
    </build>
</project>

主要应用代码如下:

package naughtytao.apigateway.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Bean;

import naughtytao.apigateway.demo.filters.ErrorFilter;
import naughtytao.apigateway.demo.filters.PostFilter;
import naughtytao.apigateway.demo.filters.PreFilter;
import naughtytao.apigateway.demo.filters.RouteFilter;

@SpringBootApplication
@EnableAutoConfiguration(exclude = { RabbitAutoConfiguration.class })
@EnableZuulProxy
@ComponentScan("naughtytao.apigateway.demo")
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

 

Docker 构建文件如下:

FROM maven:3.6.3-openjdk-11
WORKDIR /usr/src/app
COPY src ./src
COPY pom.xml ./
RUN mvn -f ./pom.xml clean package -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true -Dmaven.wagon.http.ssl.ignore.validity.dates=true 

EXPOSE 8080
ENTRYPOINT ["java","-jar","/usr/src/app/target/demo-0.0.1-SNAPSHOT.jar"]

路由的配置写在application.properties中

#Zuul routes.
zuul.routes.goapi.url=http://goapi:8080
zuul.routes.nodeapi.url=http://nodeapi:8080
zuul.routes.flaskapi.url=http://flaskapi:8080
zuul.routes.springapi.url=http://springapi:8080

ribbon.eureka.enabled=false
server.port=8080

 

我们同样使用Docker Compose运行Zuul的网关来进行验证:

version: '3.7'
services:
  gateway:
    image: naughtytao/zuulgateway:0.1 
    ports:
      - 8080:8080
    volumes:
      - ./config/application.properties:/usr/src/app/config/application.properties
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 256M
        reservations:
          memory: 256M

 

使用K6压力测试的结果如下:

 

在相同的配置条件下(单核,256M),Zuul的压测结果要明显差于其它几个,只有200左右。

 

在分配更多资源的情况下,4核 2G,Zuul的性能提升到600-800,所以Zuul对于资源的需求还是比较明显的。

另外需要提及的是,我们使用的是Zuul1,Netflix已经推出了Zuul2。Zuul2对架构做出了较大的改进。Zuul1本质上就是一个同步Servlet,采用多线程阻塞模型。Zuul2的基于Netty实现了异步非阻塞编程模型。同步的方式,比较容易调试,但是多线程本身需要消耗CPU和内存资源,所以它的性能要差一些。而采用非阻塞模式的Zuul,因为线程开销小,所支持的链接数量要更多,也更节省资源。

 

Gravitee

Gravitee是Gravitee.io开源的,基于Java的,简单易用,性能高,且具成本效益的开源API平台,可帮助组织保护,发布和分析您的API。

 

Gravitee可以通过设计工作室和路径的两种方式来创建和管理API

 

Gravity提供网关,API门户和API管理,其中网关和管理API部分是开源的,门户需要注册许可证来使用。

 

 

后台使用MongoDB作为存储,支持ES接入。

我们同样使用Docker Compose来部署整个Gravitee的栈。

#
# Copyright (C) 2015 The Gravitee team (http://gravitee.io)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
version: '3.7'

networks:
  frontend:
    name: frontend
  storage:
    name: storage

volumes:
  data-elasticsearch:
  data-mongo:

services:
  mongodb:
    image: mongo:${MONGODB_VERSION:-3.6}
    container_name: gio_apim_mongodb
    restart: always
    volumes:
      - data-mongo:/data/db
      - ./logs/apim-mongodb:/var/log/mongodb
    networks:
      - storage

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION:-7.7.0}
    container_name: gio_apim_elasticsearch
    restart: always
    volumes:
      - data-elasticsearch:/usr/share/elasticsearch/data
    environment:
      - http.host=0.0.0.0
      - transport.host=0.0.0.0
      - xpack.security.enabled=false
      - xpack.monitoring.enabled=false
      - cluster.name=elasticsearch
      - bootstrap.memory_lock=true
      - discovery.type=single-node
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile: 65536
    networks:
      - storage

  gateway:
    image: graviteeio/apim-gateway:${APIM_VERSION:-3}
    container_name: gio_apim_gateway
    restart: always
    ports:
      - "8082:8082"
    depends_on:
      - mongodb
      - elasticsearch
    volumes:
      - ./logs/apim-gateway:/opt/graviteeio-gateway/logs
    environment:
      - gravitee_management_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000
      - gravitee_ratelimit_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000
      - gravitee_reporters_elasticsearch_endpoints_0=http://elasticsearch:9200
    networks:
      - storage
      - frontend
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 256M
        reservations:
          memory: 256M

  management_api:
    image: graviteeio/apim-management-api:${APIM_VERSION:-3}
    container_name: gio_apim_management_api
    restart: always
    ports:
      - "8083:8083"
    links:
      - mongodb
      - elasticsearch
    depends_on:
      - mongodb
      - elasticsearch
    volumes:
      - ./logs/apim-management-api:/opt/graviteeio-management-api/logs
    environment:
      - gravitee_management_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000
      - gravitee_analytics_elasticsearch_endpoints_0=http://elasticsearch:9200
    networks:
      - storage
      - frontend

  management_ui:
    image: graviteeio/apim-management-ui:${APIM_VERSION:-3}
    container_name: gio_apim_management_ui
    restart: always
    ports:
      - "8084:8080"
    depends_on:
      - management_api
    environment:
      - MGMT_API_URL=http://localhost:8083/management/organizations/DEFAULT/environments/DEFAULT/
    volumes:
      - ./logs/apim-management-ui:/var/log/nginx
    networks:
      - frontend

  portal_ui:
    image: graviteeio/apim-portal-ui:${APIM_VERSION:-3}
    container_name: gio_apim_portal_ui
    restart: always
    ports:
      - "8085:8080"
    depends_on:
      - management_api
    environment:
      - PORTAL_API_URL=http://localhost:8083/portal/environments/DEFAULT
    volumes:
      - ./logs/apim-portal-ui:/var/log/nginx
    networks:
      - frontend
  

我们使用管理UI来创建四个对应的API来进行网关的路由,也可以用API的方式,Gravitee是这个开源网关中,唯一管理UI也开源的产品。

 

使用K6压力测试的结果如下:

 

和同样采用Java的Zuul类似,Gravitee的响应只能达到200左右,而且还出现了一些错误。我们只好再一次提高网关的资源分配到4核2G。

 

提高资源分配后的性能来到了500-700,稍微好于Zuul。

 

总结

本文分析了几种开源API网关的架构和基本功能,为大家在架构选型的时候提供一些基本的参考信息,本文做作的测试数据比较简单,场景也比较单一,不能作为实际选型的依据。

  • Nginx
    Nginx基于C开发的高性能API网关,拥有众多的插件,如果你的API管理的需求比较简单,接受手工配置路由,Nginx是个不错的选择。
  • Kong
    Kong是基于Nginx的API网关,使用OpenResty和Lua扩展,后台使用PostgreSQL,功能众多,社区的热度很高,但是性能上看比起Nginx有相当的损失。如果你对功能和扩展性有要求,可以考虑Kong。
  • APISIX
    APISIX和Kong的架构类似,但是采用了云原生的设计,使用ETCD作为后台,性能上比起Kong有相当的优势,适合对性能要求高的云原生部署的场景。特别提一下,APISIX支持MQTT协议,对于构建IOT应用非常友好。
  • Tyk
    Tyk使用Golang开发,后台使用Redis,性能不错,如果你喜欢Golang,可以考虑一下。要注意的是Tyk的开源协议是MPL,是属于修改代码后不能闭源,对于商业化应用不是很友好。
  • Zuul
    Zuul是Netflix开源的基于Java的API网关组件,他并不是一款开箱即用的API网关,需要和你的Java应用一起构建,所有的功能都是通过集成其它组件的方式来使用,适合对于Java比较熟悉,用Java构建的应用的场景,缺点是性能其他的开源产品要差一些,同样的性能条件下,对于资源的要求会更多。
  • Gravitee
    Gravitee是Gravitee.io开源的基于Java的API管理平台,它能对API的生命周期进行管理,即使是开源版本,也有很好的UI支持。但是因为采用了Java构建,性能同样是短板,适合对于API管理有强烈需求的场景。

本文所有的代码可以从这里获得
https://github.com/gangtao/api-gateway

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章