剛哥談架構 (十一) 開源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

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