Skip to main content

· 3 min read
CheverJohn

我的问题描述

  1. 无法跑测试(APISIX 的);
  2. Nginx 会启动多个进程。

然后我复现的命令如下可见

export PERL5LIB=.:$PERL5LIB:/home/api7/dev_cj/apisix

export PATH=/usr/local/openresty/nginx/sbin:$PATH

prove ....../example.t

然后就出现一个问题端口占用

解决办法:当跑测试的时候,因为 APISIX 会启动upstream 端口,所以如果不关闭 openresty 的阿虎,就会遇到端口占用的问题。

然后我运行

netstat -nultp

复现了 多个 nginx 进程的问题,

运行

ps -ef | grep nginx

发现了多个进程,意识到我的 openresty 还开着。

关闭 openresty

openresty -s stop

然后利用kill -9 杀掉了很多个进程。这边我采取了笨方法,一个进程一个进程杀掉了,可以不用 -9 ,这边 remark 一下。

kill -9 76007
kill -9 76008
kill -9 76009

APISIX 重启了一切正常

总结

ps -ef | grep nginx

这个命令还是要记记牢。

2022年 3月 8日

细节问题:没想到已经跑通测试的我,还是在新的开发机上跑测试框架遇到了问题。本次主要问题在于 一个安装包问题,所以说环境是真的无语,还是得设置到代理啊。我设置了git 的代理解决了问题, 详情方法,请参考 link ,我的主要疏忽在于,没注意端口, 因为之前实在 Windows 本上做开发的,用的是 v2ray,端口是 10808,没曾想就照着走错了,离谱啊。此处 mark 一下,需要注意。

然后还有一个点需要注意一下,make depsLUAROCKS_SERVER=https://luarocks.cn make deps 可以混着用,over。

放上遇到问题的图:

我又遇到问题啦呜呜呜

放上解决问题之后的图片:

解决问题就是爽啊!

· 2 min read
CheverJohn

本篇文章主要针对 APISIX 的插件开发,以及核心开发,配置相应的测试框架,并跑通。

首先我通过 git clone 将 APISIX 源码仓库 clone 到本地。

然后搭配另外安装的 etcd 和 OpenResty,将一些例子请求跑起来。

然后就开始我们主要的配置测试框架过程。这边对于 APISIX、OpenResty、NGINX 皆可用。

首先如果想要获取对测试框架的基本知识,请浏览此链接,这对接下来的很多操作都很有帮助。

然后还有一些基本的经验可以学习。

先配置

首先将 APISIX 加入到框架中去

export PERL5LIB=.:$PERL5LIB:/home/api7/dev_cj/apisix

然后配置OpenResty 中的 NGINX 的环境变量配置

export PATH=/usr/local/openresty/nginx/sbin:$PATH

小知识

--- ONLY

在测试案例中加入 --- ONLY or --- SKIP 可以仅测试一个案例,可以大大加快速度。

ONLY只做一个测试例子

--- SKIP

跳过这一个测试案例,其他都跑

小知识

这边有一个专属于 APISIX 的小知识,跑测试前要下载子模块(submoudle)。

git submodule update --init --recursive

如果你跑测试的时候遇到这样的报错: 缺依赖

只需要运行上面的命令即可

· One min read
CheverJohn

以 VMware 为示范

首先配置桥接网络,然后配置静态网络

然后进行以下命令

export https_proxy=https://192.168.1.102:10809

export http_proxy=http://192.168.1.102:10809

对于 git ,配置命令如下

git config --global http.proxy http://192.168.1.102:10809

git config --global https.proxy https://192.168.1.102:10809

git config --global --unset http.proxy

git config --global --unset https.proxy

· 14 min read
CheverJohn

本篇博客,详细讲述一个 issues 的解决过程,享受解决问题的快乐吧:)

起源

当我浏览我关注的 Apache APISIX 社区的时候,发现了一个非常符合我的 issues。这是一个关于 GraphQL 的issues,正好我有关于 GraphQL 的了解,便想着接下这个 issues 以锻炼自己的能力叭。

接下来讲述发现的过程:

issue 详情

看到 issue (标题为 bug: ctx.lua#59 parse_graphql(ctx) #6266 ),我刚开始看 issues 的时候,还以为是这位老哥不会使用 APISIX ,居然在发送请求的时候漏掉 -X POST (没想到最后还是我格局小了)

首先看 issue 的描述

use whole request body to parse graphql will get parse error. graphql request body is json , example :{"query":"query{getUser{name age}}","variables":null},

{"query":
"query{
getUser{
name age
}
}",
"variables":null
}

not query{getUser{name age}}

query{
getUser{
name
age
}
}

我简单描述一下,这个问题就是说当他使用请求体为 json 的请求时,出现 parse error 的问题。

这边还是要抽自己一下,他都明确说了,没有用 query 的方式,我还在后边用我的 query 跟他解释,离谱,我该反省~

curl -X POST http://127.0.0.1:9080/graphql -d '
query getUser {
owner {
name
}
repo {
created
}
}'

他给出了自己的环境配置

Environment

  • apisix version (cmd: apisix version): apache/apisix:2.12.0-alpine
  • OS (cmd: uname -a): docker
  • OpenResty / Nginx version (cmd: nginx -V or openresty -V): null
  • etcd version, if have (cmd: run curl http://127.0.0.1:9090/v1/server_info to get the info from server-info API): bitnami/etcd:3.4.15
  • apisix-dashboard version, if have: apache/apisix-dashboard:2.10.1-alpine

然后他给出了自己的复现过程

Reproduce

  1. define graphql
query {
getUser:User
}

type User{
name:String
age:String
}
  1. add route
curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
"methods": ["POST"],
"uri": "/graphql",
"vars": [
["graphql_operation", "==", "query"],
["graphql_name", "==", "getUser"]
],
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:1980": 1
}
}
}'
  1. perform graphql request by curl
curl 'http://127.0.0.1:9080/graphql' \
-H 'Content-Type: text/plain;charset=UTF-8' \
-H 'Accept: */*' \
--data-raw '{"query":"query getUser{getUser{name age}}","variables":null}' \
--compressed

上方请求化简

curl 'http://127.0.0.1:9080/graphql' \
-H 'Content-Type: text/plain;charset=UTF-8' \
-H 'Accept: */*' \
--data-raw '
{"query":"query getUser {
getUser {
name
age
}
}",
"variables":null
}' \
--compressed

Actual result

HTTP/1.1 404 Not Found Date: Tue, 08 Feb 2022 07:39:16 GMT Content-Type: text/plain; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive

{ "error_msg": "404 Route Not Found" }

Error log

2022/02/08 07:39:16 [error] 45#45: *1085159 [lua] ctx.lua:80: get_parsed_graphql(): failed to parse graphql: Syntax error near line 1 body:

Expected result

success

issue 提出者认为

一个正常的 graphql 请求应该是这样的:

curl 'https://api.mocki.io/v2/c4d7a195/graphql' \
-H 'authority: api.mocki.io' \
-H 'accept: */*' \
-H 'content-type: application/json' \
-H 'origin: https://api.mocki.io' \
--data-raw '{"operationName":"getUser","variables":{},"query":"query getUser {\n user(id: \"4dc70521-22bb-4396-b37a-4a927c66d43b\") {\n id\n email\n name\n }\n}\n"}' \
--compressed

会返回

{
"data": {
"user": {
"id": "Hello World",
"email": "Hello World",
"name": "Hello World"
}
}
}

而且

A standard GraphQL POST request should use the application/json content type, and include a JSON-encoded body of the following form:

{
"query": "...",
"operationName": "...",
"variables": { "myVariable": "someValue", ... }
}

see official graphql document, https://graphql.org/learn/serving-over-http/#post-request

and --data will perform request with POST method ,see curl document

A standard GraphQL POST request should use the application/json content type, and include a JSON-encoded body of the following form:

{
"query": "...",
"operationName": "...",
"variables": { "myVariable": "someValue", ... }
}

see official graphql document, https://graphql.org/learn/serving-over-http/#post-request

and --data will perform request with POST method ,see curl document

对于 curl 工具的使用

$ curl --help
Usage: curl [options...] <url>
-d, --data <data> **HTTP POST data**
...

use -v to print verbose log

curl -v 'https://api.mocki.io/v2/c4d7a195/graphql' \
-H 'authority: api.mocki.io' \
-H 'accept: */*' \
-H 'content-type: application/json' \
-H 'origin: https://api.mocki.io' \
--data-raw '{"operationName":"getUser","variables":{},"query":"query getUser {\n user(id: \"4dc70521-22bb-4396-b37a-4a927c66d43b\") {\n id\n email\n name\n }\n}\n"}' \
--compressed

issue 提出者原话:

it will print something like this > POST /v2/c4d7a195/graphql HTTP/2, thought i'm not use -X POST

sorry, I'm try to discuss about how APISIX deal with graphql request. it seems that the mock GraphQL data of APISIX is not a standard GraphQL request.

得出结论: mock GraphQL data of APISIX is not a standard GraphQL request.

评估需求

看过这个 issue 之后,思考了 APISIX 中的 GraphQL 到底是什么。或许 APISIX 支持的是假的 GraphQL?思考明白之后才能动手做。

之前应该是只做了这个:If the "application/graphql" Content-Type header is present, treat the HTTP POST body contents as the GraphQL query string.

curl -v -H "Content-Type: application/graphql" -d "{ hello }"  "localhost:3000/graphql" 

需要指定 content-type 了

所以我对于这个 issue 的结论就是:需要 fix 三部分

  1. 解决 POST JSON的问题,让 APISIX 支持 JSON 格式的 POST;
  2. 支持 GET 。

其实根据这篇文档

一个标准的 GraphQL POST 请求就应该使用 application/json content type, 然后包括 json 格式的body在里边。

但对于目前的 GraphQL 在 APISIX 中的应用来讲,是可以通过 "application/graphql" Content-Type 的形式绕过的。参考文档中的这句:

If the "application/graphql" Content-Type header is present, treat the HTTP POST body contents as the GraphQL query string.

重点:点睛之笔

https://graphql.org/learn/serving-over-http/ 参考官方的文档,实际上 APISIX 现在处理的场景是

If the "application/graphql" Content-Type header is present, treat the HTTP POST body contents as the GraphQL query string.

APISIX 暂时只能够实现 GraphQL query 的功能。

我们需要 json 格式的功能 最好还要加上 “GET” 的功能。

可参考的 GraphQL 官方文档

https://graphql.org/learn/serving-over-http/#post-request

https://graphql.org/learn/serving-over-http/#post-request

评估工作情况

第一次评估

我认为我需要修改 graphql-lua 库中的 parse.lua

curl 'http://127.0.0.1:9080/graphql' \
-H 'Content-Type: text/plain;charset=UTF-8' \
-H 'Accept: */*' \
-d '{"query":"query getUser{getUser{name age}}","variables":null}' \
--compressed

源码分析

找到 GraphQL 在 APISIX 中的代码,主要有关系的只有apisix/core/ctx.lua 中有相关代码。其实 APISIX 依靠的

--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You 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.
--
local core_str = require("apisix.core.string")
local core_tab = require("apisix.core.table")
local request = require("apisix.core.request")
local log = require("apisix.core.log")
local config_local = require("apisix.core.config_local")
local tablepool = require("tablepool")
local get_var = require("resty.ngxvar").fetch
local get_request = require("resty.ngxvar").request
local ck = require "resty.cookie"
local gq_parse = require("graphql").parse
local setmetatable = setmetatable
local sub_str = string.sub
local ngx = ngx
local ngx_var = ngx.var
local re_gsub = ngx.re.gsub
local ipairs = ipairs
local type = type
local error = error
local pcall = pcall


local _M = {version = 0.2}
local GRAPHQL_DEFAULT_MAX_SIZE = 1048576 -- 1MiB


local function parse_graphql(ctx)
local local_conf, err = config_local.local_conf()
if not local_conf then
return nil, "failed to get local conf: " .. err
end

local max_size = GRAPHQL_DEFAULT_MAX_SIZE
local size = core_tab.try_read_attr(local_conf, "graphql", "max_size")
if size then
max_size = size
end

local body, err = request.get_body(max_size, ctx)
if not body then
return nil, "failed to read graphql body: " .. err
end

local ok, res = pcall(gq_parse, body)
if not ok then
return nil, "failed to parse graphql: " .. res .. " body: " .. body
end

if #res.definitions == 0 then
return nil, "empty graphql: " .. body
end

return res
end


local function get_parsed_graphql()
local ctx = ngx.ctx.api_ctx
if ctx._graphql then
return ctx._graphql
end

local res, err = parse_graphql(ctx)
if not res then
log.error(err)
ctx._graphql = {}
return ctx._graphql
end

if #res.definitions > 1 then
log.warn("Multiple operations are not supported.",
"Only the first one is handled")
end

local def = res.definitions[1]
local fields = def.selectionSet.selections
local root_fields = core_tab.new(#fields, 0)
for i, f in ipairs(fields) do
root_fields[i] = f.name.value
end

local name = ""
if def.name and def.name.value then
name = def.name.value
end

ctx._graphql = {
name = name,
operation = def.operation,
root_fields = root_fields,
}

return ctx._graphql
end


do
-- 获取特殊var的方法
local var_methods = {
method = ngx.req.get_method,
-- ref: https://github.com/cloudflare/lua-resty-cookie
cookie = function ()
if ngx.var.http_cookie then
return ck:new()
end
end
}

local no_cacheable_var_names = {
-- var.args should not be cached as it can be changed via set_uri_args
args = true,
is_args = true,
}

local ngx_var_names = {
upstream_scheme = true,
upstream_host = true,
upstream_upgrade = true,
upstream_connection = true,
upstream_uri = true,

upstream_mirror_host = true,

upstream_cache_zone = true,
upstream_cache_zone_info = true,
upstream_no_cache = true,
upstream_cache_key = true,
upstream_cache_bypass = true,

var_x_forwarded_proto = true,
}

local mt = {
-- 重载 hash 元方法
-- t 是 self
__index = function(t, key)

-- 若 cache table 存在直接返回
local cached = t._cache[key]
if cached ~= nil then
return cached
end

if type(key) ~= "string" then
error("invalid argument, expect string value", 2)
end

local val
-- 如果是特殊类型, 使用特定方法获取
local method = var_methods[key]
if method then
val = method()

elseif core_str.has_prefix(key, "cookie_") then
-- 通过 var_methods 访问到 resty.cookie
local cookie = t.cookie
if cookie then
local err
val, err = cookie:get(sub_str(key, 8))
if err then
log.warn("failed to fetch cookie value by key: ",
key, " error: ", err)
end
end

elseif core_str.has_prefix(key, "arg_") then
local arg_key = sub_str(key, 5)
local args = request.get_uri_args()[arg_key]
if args then
if type(args) == "table" then
val = args[1]
else
val = args
end
end

elseif core_str.has_prefix(key, "http_") then
key = key:lower()
key = re_gsub(key, "-", "_", "jo")
-- 最终通过 ngx.var 获取
val = get_var(key, t._request)

elseif core_str.has_prefix(key, "graphql_") then
-- trim the "graphql_" prefix
key = sub_str(key, 9)
val = get_parsed_graphql()[key]

elseif key == "route_id" then
val = ngx.ctx.api_ctx and ngx.ctx.api_ctx.route_id

elseif key == "service_id" then
val = ngx.ctx.api_ctx and ngx.ctx.api_ctx.service_id

elseif key == "consumer_name" then
val = ngx.ctx.api_ctx and ngx.ctx.api_ctx.consumer_name

elseif key == "route_name" then
val = ngx.ctx.api_ctx and ngx.ctx.api_ctx.route_name

elseif key == "service_name" then
val = ngx.ctx.api_ctx and ngx.ctx.api_ctx.service_name

elseif key == "balancer_ip" then
val = ngx.ctx.api_ctx and ngx.ctx.api_ctx.balancer_ip

elseif key == "balancer_port" then
val = ngx.ctx.api_ctx and ngx.ctx.api_ctx.balancer_port

else
val = get_var(key, t._request)
end

if val ~= nil and not no_cacheable_var_names[key] then
t._cache[key] = val
end

return val
end,

__newindex = function(t, key, val)
if ngx_var_names[key] then
ngx_var[key] = val
end

-- log.info("key: ", key, " new val: ", val)
t._cache[key] = val
end,
}

function _M.set_vars_meta(ctx)
local var = tablepool.fetch("ctx_var", 0, 32)
if not var._cache then
var._cache = {}
end

var._request = get_request()
setmetatable(var, mt)
ctx.var = var
end

function _M.release_vars(ctx)
if ctx.var == nil then
return
end

core_tab.clear(ctx.var._cache)
tablepool.release("ctx_var", ctx.var, true)
ctx.var = nil
end

end -- do


return _M

简单理一下函数框架

  • parse_graphql(ctx)
  • get_parsed_graphql()
  • do
    • var_methods
    • no_cacheable_var_names
    • ngx_var_names
    • mt
      • __index = function(t, key)
      • __newindex = function(t, key, val)
  • _M.set_vars_meta
  • _M.release_vars

然后中有一部分代码可以从 APISIX 的官方一个源码文档里得到学习。

地址为:请求生命周期

开始工作

我的第一版计划

  1. 向上游提交 json 格式的 PR
    1. 上游 PR 通过后,再进行 APISIX 的 issue 修复。
  2. APISIX 中只要对上游的函数进行使用,并输出报错结果就行。

大佬思路

来自APISIX PMC zexuan

大概意思就是,把 json 在APISIX 里解码成 query 字段,然后再将其query 喂给 graphql-lua。

我们并不需要支持 operationName、variable这些功能。

这个思路贼简单,那我为啥想不到呢?

轻微反思,是因为有点“眼高手低”处理实际问题的能力待加强。慢慢学习吧

有个调试问题没解决好,离谱,得加速了。

2022年2月11日的工作

  1. 找到需要更改的代码范围,将 graphql-lua 中的 parse.lua 代码理解清楚。
  2. ctx.lua 代码理解清楚。
  3. 确定思路

2022年2月14日的工作

  1. 完成测试框架的搭建

2022年2月15日的工作

  1. 成功跑通测试框架
  2. 开始正式开发,将问题锁定在具体的部分

问题解决需要在这里添加代码 需要添加代码的地方

很明显,当我从终端扫入一个 body ,它的内容可能是这样的。

2022/02/15 15:31:09 [info] 338683#338683: *77846 [lua] ctx.lua:59: parse_graphql(): booody: query getRepo {owner {name}repo {created}}, client: 127.0.0.1, server: _, request: "POST /graphql HTTP/1.1", host: "127.0.0.1:9080"

这对照了这样的请求:

curl -H 'content-type: application/graphql' -X POST http://127.0.0.1:9080/graphql -d 'query getRepo {owner {name}repo {created}}'

也可以是这样的:

2022/02/15 15:32:55 [info] 338682#338682: *84824 [lua] ctx.lua:59: parse_graphql(): booody: {"query":"query getUser{getUser{name age}}","variables":null}, client: 127.0.0.1, server: _, request: "POST /graphql HTTP/1.1", host: "127.0.0.1:9080"

这对照了这样的请求:

curl 'http://127.0.0.1:9080/graphql' \
-H 'Content-Type: application/json' \
-H 'Accept: */*' \
--data-raw '{"query":"query getUser{getUser{name age}}","variables":null}' \
--compressed

当然,第一个请求就是目前 APISIX 能够处理的 query 格式的 graphql 语句,第二个请求是目前 APISIX 不能够处理的 json 格式的 graphql 语句。

而我需要做的事情,就是把 json 格式转换为 query 格式,既然确认了,就开始做,寻找 lua 转换格式的方法。

· One min read
CheverJohn

rax

首先是 lua-resty-radixtree 的灵感项目 rax 。这个项目的作者挺值得敬佩的,深藏功与名。该串代码有效强化了 redis 的检所匹配能力,对了作者同时也是 redis 的作者(估计是,要么就是核心作者)

代码整体由 C 构成,如果用心学习,估计一两周能搞定!掌握的话,一定会对自己的技术有很大的提升。

ljsonschema

这个项目是一个偏编译原理底层的项目,可能需要花好几个月才能理解。

但是代码的性能绝对强悍,利用lua的动态特性,强超很多项目,比如腾讯现在只支持静态的 rapidjson。 本项目大概内容,lua 生成 lua,然后执行 lua。

· 8 min read
CheverJohn

本篇博客主要记录了我在使用 GraphQL 和 Apache APISIX 搭配过程中遇到的很多问题。纯属个人产出,有不对的地方还望指出。

基础命令

路由规则配置

# basic Apache APISIX config

curl http://127.0.0.1:9080/apisix/admin/routes/11 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
"methods": ["POST"],
"uri": "/graphql",
"vars": [
["graphql_operation", "==", "query"],
["graphql_name", "==", "getRepo"],
["graphql_root_fields", "has", "owner"]
],
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:1980": 1
}
}
}'

反馈

一般在我之前的OpenResty中正常配置好1980服务器后,我配置APISIX路由正确之后会返回如下的信息

HTTP/1.1 200 OK
Date: Fri, 04 Feb 2022 22:00:03 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/2.10.3
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
Access-Control-Max-Age: 3600

{"action":"set","node":{"value":{"update_time":1644012003,"create_time":1642620298,"methods":["POST"],"uri":"\/graphql","priority":0,"upstream":{"hash_on":"vars","scheme":"http","type":"roundrobin","nodes":{"127.0.0.1:1980":1},"pass_host":"pass"},"vars":[["graphql_operation","==","query"],["graphql_name","==","getRepo"],["graphql_root_fields","has","owner"]],"id":"11","status":1},"key":"\/apisix\/routes\/11"}}

简单请求

# basic request

curl -H 'content-type: application/graphql' -X POST http://127.0.0.1:9080/graphql -d '
query getRepo {
owner {
name
}
repo {
created
}
}'

反馈

当APISIX路由配置正确之后,请求一个基础请求

---Headers
x-real-ip:127.0.0.1
host:127.0.0.1:9080
x-forwarded-proto:http
x-forwarded-host:127.0.0.1
x-forwarded-port:9080
content-length:82
content-type:application/x-www-form-urlencoded
accept:*/*
user-agent:curl/7.29.0
x-forwarded-for:127.0.0.1
---Args
---URI
/graphql
---Service Node
Centos-port: 1980

进阶操作

感谢大佬时刻

这边感谢一下我泽轩大佬,谢谢他给我提出的宝贵意见。非常感谢!

体现 roundrobin 均衡策略

简单记一下逻辑,其实就是不断配置APISIX的路由规则

这边 upstream 里配置了分别架设在 OpenResty 上端口为 1980 和 1981 的两个 node

curl http://127.0.0.1:9080/apisix/admin/routes/11 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
"methods": ["POST"],
"uri": "/graphql",
"vars": [
["graphql_operation", "==", "query"],
["graphql_name", "==", "getRepo"],
["graphql_root_fields", "has", "owner"]
],
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:1980": 1,
"127.0.0.1:1981": 1
}
}
}'

两个服务器的权重都设置为 1,一个等级,这里边 2 的权重大于 1。

然后分别发出请求的话,会按照顺序,1 > 2 > 1 > 2 > 1......的顺序得到 upstream 服务器的响应。

根据 graphql_name 匹配 upstream 服务器

错误示范

这一串是给第一个 upstream 服务器配置 graphql_name 为getRepo111

curl http://127.0.0.1:9080/apisix/admin/routes/11 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
"methods": ["POST"],
"uri": "/graphql",
"vars": [
["graphql_operation", "==", "query"],
["graphql_name", "==", "getRepo111"],
["graphql_root_fields", "has", "owner"]
],
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:1980": 1
}
}
}'

第二个 upstream 服务器配置 graphql_name 为 getRepo222

curl http://127.0.0.1:9080/apisix/admin/routes/11 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
"methods": ["POST"],
"uri": "/graphql",
"vars": [
["graphql_operation", "==", "query"],
["graphql_name", "==", "getRepo222"],
["graphql_root_fields", "has", "owner"]
],
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:1981": 1
}
}
}'

然后我们可以根据不同的 graphql query 来进行不同的匹配,即泽轩大佬说的

泽轩:Apache APISIX 还可以针对不同的 graphql_operation 进行不同的权限校验、针对不同的 graphql_name 转发到不同的 upstream。

开始 query

curl -H 'content-type: application/graphql' -i -X POST http://127.0.0.1:9080/graphql -d '
query getRepo111 {
owner {
name
}
repo {
created
}
}'

上面的query 转发到了1980 端口的 graphql server上

curl -H 'content-type: application/graphql' -i -X POST http://127.0.0.1:9080/graphql -d '
query getRepo222 {
owner {
name
}
repo {
created
}
}'

上面的 query 转发到了 1981 端口的 graphql server 上

就是这样,先简单做一下,明天再写详细一点。

这边可能对APISIX的 upstream 配置有点问题,所以暂停一下。

如果你这样设置,会遇到一个很明显的问题,后边的配置会覆盖掉前面的配置。

主要原因是 upstream 应该分组!接下来开始正式的工作

成功示范

对第一个 upstream 服务器的配置

首先创建一个上游 upstream 对象:

curl http://127.0.0.1:9080/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"type": "chash",
"key": "remote_addr",
"nodes": {
"127.0.0.1:1980": 1
}
}'

上游 upstream 对象创建后,均可以被具体 Route 或者 Service 引用,例如:

curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
"methods": ["POST"],
"uri": "/graphql",
"vars": [
["graphql_operation", "==", "query"],
["graphql_name", "==", "getRepo111"],
["graphql_root_fields", "has", "owner"]
],
"upstream_id": "1"
}'

这里边我稍微解释一下,其中 curl http://127.0.0.1:9080/apisix/admin/routes/1 之后最后的 1,我认为就是 "upstream_id": "1"。因为从源码中解析 curl 请求的那个函数来看,就应该是这样的,如果有错误,可以来找我哈。

然后进行最后的正式请求:

curl -H 'content-type: application/graphql' -i -X POST http://127.0.0.1:9080/graphql -d '
query getRepo111 {
owner {
name
}
repo {
created
}
}'

得到正确的响应:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Date: Mon, 07 Feb 2022 22:57:24 GMT
Server: APISIX/2.10.3

---Headers
x-forwarded-port:9080
content-length:85
user-agent:curl/7.29.0
accept:*/*
content-type:application/x-www-form-urlencoded
host:127.0.0.1:9080
x-real-ip:127.0.0.1
x-forwarded-for:127.0.0.1
x-forwarded-proto:http
x-forwarded-host:127.0.0.1
---Args
---URI
/graphql111
---Service Node
Centos-port: 1980
John Chever's 1980 port is working......

完成第一个 upstream 上游服务器的配置了。

对第二个 upstream 服务器的配置

首先创建一个上游 upstream 对象:

curl http://127.0.0.1:9080/apisix/admin/upstreams/2 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"type": "chash",
"key": "remote_addr",
"nodes": {
"127.0.0.1:1981": 1
}
}'

上游 upstream 对象创建后,均可以被具体 Route 或者 Service 引用,例如:

curl http://127.0.0.1:9080/apisix/admin/routes/2 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
"methods": ["POST"],
"uri": "/graphql",
"vars": [
["graphql_operation", "==", "query"],
["graphql_name", "==", "getRepo222"],
["graphql_root_fields", "has", "owner"]
],
"upstream_id": 2
}'

然后进行最后的正式请求:

curl -H 'content-type: application/graphql' -i -X POST http://127.0.0.1:9080/graphql -d '
query getRepo222 {
owner {
name
}
repo {
created
}
}'

得到正确的响应:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Date: Mon, 07 Feb 2022 23:04:44 GMT
Server: APISIX/2.10.3

---Headers
x-forwarded-port:9080
content-length:85
user-agent:curl/7.29.0
accept:*/*
content-type:application/x-www-form-urlencoded
host:127.0.0.1:9080
x-real-ip:127.0.0.1
x-forwarded-for:127.0.0.1
x-forwarded-proto:http
x-forwarded-host:127.0.0.1
---Args
---URI
/graphql222
---Service Node
Centos-port: 1981
John Chever's 1981 port is working......
小结

这样配置好,就可以根据不同的 graphql_name 来匹配不同的上游 upstream 啦。

根据 graphql_operation 进行不同的权限校验

首先配置好上游对象实例

curl http://192.168.1.200:9080/apisix/admin/routes/11 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"methods": ["POST"],
"uri": "/hello",
"vars": [
["graphql_operation", "==", "mutation"],
["graphql_name", "==", "repo"]
],
"upstream": {
"nodes": {
"192.168.1.200:1982": 1
},
"type": "roundrobin"
}
}'

然后发送请求以验证配置:

curl -i -X POST http://127.0.0.1:9080/hello -d '
mutation repo($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}'

返回响应:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Date: Tue, 08 Feb 2022 22:05:51 GMT
Server: APISIX/2.10.3

---Headers
content-type:application/x-www-form-urlencoded
host:127.0.0.1:9080
x-real-ip:127.0.0.1
x-forwarded-for:127.0.0.1
x-forwarded-proto:http
x-forwarded-host:127.0.0.1
x-forwarded-port:9080
content-length:133
user-agent:curl/7.29.0
accept:*/*
---Args
---URI
/hello
---Service Node
Centos-port: 1982
John Chever's 1982 port is working......

自此实现了 Apache APISIX 针对不同的 graphql_operation 进行不同的权限校验、针对不同的 graphql_name 转发到不同的 upstream。

· 4 min read
CheverJohn

明天就是新春佳节了,已经鸽了两天的博客了,不能再继续下去咯!!! 然后祝大家新春快乐哈!

本次博客记录的是我使用 Apache APISIX 中的 GraphQL 特性时,遇到的新手级尴尬问题,淦!主要还是基础网络工具不会用。文章主要围绕两个点:一是 curl 命令工具的使用总结,二是 OpenResty 建立测试服务器的方法(以后还是要单独开张帖子,把 OpenResty,也可以说是 NGINX 的奇淫巧技都梳理一遍)

好吧又得鸽一天,看春晚去咯!:)

春节过完咯,开始肝!

教训妹妹的一晚!

完成文章的抽象部分内容 > 实操部分内容

前言

正如引言中所讲的,本篇博文主要是为了记录我使用 Apache APISIX 的 GraphQL 特性的过程。其中获得了一些对 Apache APISIX,亦或者说是 OpenResty,亦或者说是 NGINX 的最新感悟。首先我先以一张图缕清我所要做的事情的逻辑吧。

GraphQL流量在Apache_APISIX里的轨迹

当然上图这一层面还是太保守了,如果我们有多个 GraphQL Server 呢?其实 Apache APISIX 还具备根据搜索项的参数更精细化的匹配 GraphQL Server (多亏泽轩大佬跟我讲了)。那我们换一个场景。

根据三个参数更加精细化

graphql_operation
graphql_name
graphql_root_fields

逻辑图如下

GraphQL流量在Apache_APISIX里的轨迹_三个参数实现更精细化操作

以上是抽象层面的逻辑图,

部署配置

这一篇你可以搭配这两篇博客来看:

问题解决:跑通 GraphQL》: 这一篇介绍了我第一次跑通的情形,里边有我在虚拟机中的Centos7中实操的记录。

Support GraphQL In APISIX》: 主要讲了 GraphQL 在 APISIX 里的地位是什么样子的。

基础命令_graphql在APISIX中的应用

部署配置的架构图应如下所示:

GraphQL在centos中的具体部署方案

这边我们依靠了 OpenResty 搭建了两台 GraphQL Server 上游(Upstream)服务器。并使用命令配置 Apache APISIX的路由匹配规则。这样我们接下来发出 GraphQL 请求的时候,便可以有上游服务器进行响应了。

· 8 min read
CheverJohn

初始配置篇

我想用ubuntu(based RaspberryPi 4)尝试做一段开发环境

安装

我选择树莓派官方的安装器

树莓派安装界面安装ubuntu

图1. 树莓派安装界面安装Ubuntu中

配置

等这个界面安装成功,先不要急插入树莓派!!!我们先配置一些参数。打开资源管理器,找到启动盘system-boot(F:)

Ubuntu启动盘的位置

图2. 资源管理器里Ubuntu启动盘的位置

然后打开system-boot(F:),我们可以看到如下图所示的文件夹列表

两个配置文件位置

图3. 两个配置文件的具体位置

我们需要注意的是config.txtnetwork-config这两个文件,第一个可以用来调整我树莓派连接7寸显示屏的分辨率参数。第二个文件用来配置树莓派ubuntu连WiFi的能力。

连接网络

树莓派上的Ubuntu该如何连接网络呢?其实官方网站文档都说的很清楚了

我们只需要将文件中一些注释解除就可以了。

网络配置文件

图4.上图是文件原先的样子

下面开始正式配置,可以看到其中TP-LINK_A826即我要连接的WiFi SSIDpassword自然就是该wifi的密码啦。

网络配置详情

图5. 网络配置文件最初始的样子

这边有一个小提示,当我们修改完配置文件后,启动树莓派还是会遇到无法正常联网的情况,那这个时候只需要重启即可,这一块应该是有某些配置文件没有启动,期待有时间的时候,我可以去研究研究看看。

七寸屏幕分辨率

既然已经配置好了配置文件,那我们接下来得把树莓派连接七寸显示器分辨率异常的问题解决一下咯。

首先打开config.txt文件,然后按照下图在文件末尾添加

disable_overscan=1
hdmi_force_hotplug=1 # 强制树莓派使用HDMI端口,即使树莓派没有检测到显示器连接仍然使用HDMI端口。
#该值为0时允许树莓派尝试检测显示器,当该值为1时,强制树莓派使用HDMI。
hdmi_drive=2 # 可以使用该配置项来改变HDMI端口的电压输出:
# 1-DVI输出电压。该模式下,HDMI输出中不包含音频信号。
# 2-HDMI输出电压。该模式下,HDMI输出中包含音频信号。
hdmi_group=2 # 决定的分辨率
# DMT分辨率是hdmi_group=2,计算机显示器使用的分辨率;hdmi_group=1是CEA分辨率 ,CEA规定的电视规格分辨率
hdmi_mode=4

6

图6. 添加完配置信息之后的config.txt

7

图7. 是我最终成功启动树莓派Ubuntu的样子

root用户

当我正常启动了树莓派Ubuntu之后,其实就会遇到两个问题

  1. root账号和ubuntu账号密码不知道
  2. 无法通过ssh工具远程登录root账户

当然第一个问题其实是ubuntu约定成俗的东西。ubuntu在没有设置root账户密码的前提下,每次登录系统密码都是随机生成的。而ubuntu账号的密码默认就是ubuntu,牢记!然后还会要求你创建ubuntu账号的新密码,这边我设计成了我家路由器管理员密码,over。

8

图8. 发现密码不对劲,怎么都无法正常登录

那我们的root账号密码该怎么设立呢? 其实只需要我们使用ubuntu账号修改密码即可

sudo passwd root

然后输入你的新密码即可。

9

图9. 按照方法解决了问题

登录之后配置镜像源

这边需要注意的是,我们是arm架构的ubuntu系统,要在清华源上选择arm架构的镜像源,切记。

10清华镜像源

图10. 根据清华源官网上arm版本配置镜像源

配置镜像源的详细过程就不用多说了,百度一下你就知道。

大功告成!

以上便是在树莓派(RaspberryPi4)中第一次安装ubuntu20.04.3版本的一系列操作。

其中遇到的很多问题在一开始是真的难受,就比如说网络配置,我有上官网看文档的习惯,可以第一次看到官网的配置我也愣是好一会儿没有解决(官网文档在此)。最终呢,我是直接去看了ubuntu系统的启动盘的配置文件,在这个文件里才最终发现,其实我只要解除注释,就可以解决问题了。还有之前各种文章介绍的方法,是真的难以理解啊。比如这篇文章,我直接拉出来鞭尸,什么文章啊这,直接在关键地址配置了两个双引号,我咋知道具体是什么???写上一个真实案例又不会令别人蹭到你家WiFi,再说你可以改密码啊这。反正十分地令我生气。

气死我了气死我了,刚刚发现一篇很好的文章,居然在我写完之后,淦,非常地淦!

文章地址附下:https://mrxiuxing.com/posts/2f81a42d.html

然后也没遇到啥问题了。就这样,解散!

如果需要重新配置网络

当然你可以选择根据上面推荐的很不错的文档教程,一步一步去做。

ubuntu 的网络配置文件地址在 /etc/netplan/ 中。

进入到配置目录中,你可以看到有一个叫做 50-cloud-init.yaml 的文件名:

root@ubuntu:/etc/netplan# ls -la
total 16
drwxr-xr-x 2 root root 4096 Mar 1 05:02 .
drwxr-xr-x 98 root root 4096 Feb 24 06:08 ..
-rw-r--r-- 1 root root 822 Mar 1 05:02 50-cloud-init.yaml

查看其中,将网络根据自己的需求进行配置,你想 dhcp 还是 static 都可以。

最后使用下面命令让配置生效:

sudo netplan --debug apply

然后网络就配好了,over!

· 4 min read
CheverJohn

讲真为什么我会遇到这一块的问题呢?主要还是不熟悉开发流程。 虽然我已经安装好了APISIX,且APISIX的基本内容我都有所了解了。 可是在我实操过程中,还是遇到了很多问题,当然最终发现其实都是基本环境没做好(没有上游服务器端口应该算是基本环境吧?)

开干!

淦,拖了好久了,开始!——2022年2月13日

其实就是给 APISIX 的上游( upstream )添加一个服务器,这里边讨巧选择了 OpenResty。

首先我们已经按照 APISIX 的前置教程安装好了 OpenResty 了,这个必须先确定下来。

安装命令如下:

# 添加 OpenResty 源
sudo yum install yum-utils
sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

# 安装 OpenResty 和 编译工具
sudo yum install -y openresty curl git gcc openresty-openssl111-devel unzip pcre pcre-devel

# 安装 LuaRocks
curl https://raw.githubusercontent.com/apache/apisix/master/utils/linux-install-luarocks.sh -sL | bash -

然后这边再强化一下观念,安装之前本地的代理呀、基本软件(如 git、gcc、unzip等)都要先配置好!

然后我们本地的 OpenResty 的地址可以通过以下命令找回:

OpenResty 的位置

开始配置

跟配置 NGINX 一样

cd /usr/local/openresty/nginx/conf/vhost/1980.conf

首先你先在 conf 文件夹里创建 vhost 文件夹,然后在 /usr/local/openresty/nginx/conf/nginx.conf 文件最下面添加。

添加位置如图

NGINX 文件配置

然后添加如下内容

server {
listen 1980;
access_log logs/access-1980.log main;
error_log logs/error.log;
location / {
content_by_lua_block {
ngx.header["Content-Type"] = "text/html"
local headers = ngx.req.get_headers()
ngx.say("---Headers")
for k, v in pairs(headers) do
ngx.say(k .. ":" .. v)
end

local args = ngx.req.get_uri_args()
ngx.say("---Args")
for k, v in pairs(args) do
ngx.say(k .. ":" .. v)
end

ngx.say("---URI")
ngx.say(ngx.var.uri)

ngx.say("---Service Node")
ngx.say("Ubuntu-DEV-1980")
}
}
}

如图 1980 配置文件内容以及文件树

然后就直接

openresty

启动 1980 服务器即可。

可以看到后端端口,基本上应该打开的都打开咯!

所有端口

来一套基本操作

进行一套基本操作,来试试 APISIX 哈

使用 GraphQL 的配置请求说明问题

命令来自于下面的链接:https://www.cheverjohn.xyz/blog/%E5%9F%BA%E7%A1%80%E5%91%BD%E4%BB%A4_graphql%E5%9C%A8APISIX%E4%B8%AD%E7%9A%84%E5%BA%94%E7%94%A8

实现配置 GraphQL 在 APISIX 中

配置

然后发出请求

请求

如此以来,我就配置好了我的一系列测试,新春快乐!

· 21 min read
CheverJohn

了解网关

In computer networking and telecommunications, a gateway is a component that is part of two networks, which use different protocols. The gateway will translate one protocol into the other. A router is a special case of a gateway.

Gateways, also called protocol converters, can operate at any network layer. The activities of a gateway are more complex than that of the router or switch as it communicates using more than one protocol.

Both the computers of internet users that serve pages to users are host nodes. The nodes that connect the networks in between are gateways. These are gateway nodes:

  • the computers that control traffic between company networks
  • the computers used by internet service providers (ISPs) to connect users to the internet

gateway must be implied on larger networks to interconnect them.

计算机网络和通信交流中,网关是一个承载两个网络之间的一个部分,两个网络可能会使用不同的协议。网关将会进行对协议的翻译工作。路由是网关的一个特殊例子。

Gateway,也叫做协议转换。我们需要知道的是,网关它是可以在任何一个网络层进行操作的。此外,网关使用多个协议进行通信时,它的行为活动却远比路由器or交换机更为复杂。

Gateway_quick_look

在互联网中,假设有两个主机节点,其中一个主机节点为另外一个提供页面服务。这里面连接网络之间的节点称为网关。这些网关节点可以是:

  • 控制公司网络之间流量的计算机
  • 互联网服务提供商(ISP)所使用的将用户连接到互联网的计算机

以上便是主要来自于wiki的网关解释。其实还是有点虚,读完一切之后,我想你只会记住以下几点:协议转换、连接网络之间的节点

但其实这些感觉还是远远不够的,继续再找点资料吧。

从当代互联网业务层面去理解网关服务

没有Gateway服务之前

众所周知,现在的互联网中,碎片化的微服务一定是主流,比如会员、商品、推荐服务等都可以做其中一个小小的微服务。

那这个时候就会出现问题了, 开始描述场景:如果业务比较简单的话,我们可以通过给每个业务都分配一个独立的域名(https://service.api.company.com) 开始描述问题:

  • 每个业务都会需要进行鉴权、限流、权限校验等逻辑。这很麻烦,新增一个业务就得新增一套逻辑。

  • 如果业务简单,应该还是可以cover需求的。但是咱们比如说淘宝、亚马逊一类的公司业务,每次一打开页面,可能会涉及到数百个微服务系统工作,如果每一个微服务都分配一个域名的话

    • 一方面客户端代码会很难维护;
    • 另一方面是连接数会非常多,这在调用中会很低效
  • 采用域名的方法,对于环境的隔离也不太友好。

  • 另外一个最重要的问题,后端每个微服务可能都是由不同语言编写的,可能会采用不同的协议,比如HTTP、Dubbo、gRPC等等。我们不可能要求客户端适配这么多种协议,这是一项非常有挑战性的工作。项目会变得非常复杂且难以维护。

  • 后期如果微服务需要进行重构,那必须要客户端配合你一起进行改造,不合理呢。

    Gateway_service_in_internet

有了API Gateway服务之后

于是为了解决以上痛点,聪明的工程师们,实现了API网关,实现一个API网关接管所有的入口流量,类似于Nginx的作用,将所有用户的请求转发给后端的服务器们。但网关做的不仅仅只是简单的转发,也会针对流量做一些扩展,比如:

  • 鉴权
  • 限流
  • 熔断
  • 权限
  • 协议转换
  • 错误码统一
  • 缓存
  • 日志
  • 监控
  • 告警等

这样将通用的逻辑抽出来,由网关统一去做,业务方也能够更加专注于业务逻辑,提升迭代的效率。

Gateway_in_network_begin

通过引入API网关,客户端只需要与API网关交互,而不用与各个业务方的接口分别通讯。但是一个高性能、稳定的网关,也需要很多点。

对网关的一些功能进行解释

协议转换

内部的API可能是由很多种不同的协议实现的,比如HTTP、Dubbo、GRPC等,但对于用户来说其中很多都不是很友好,或者根本没法对外暴露,比如Dubbo服务,因此需要在网关层做一次协议转换,将用户的HTTP协议请求,在网关层转换成底层对应的协议,比如HTTP -> Dubbo, 但这里需要注意很多问题,比如参数类型,如果类型搞错了,导致转换出问题,而日志又不够详细的话,问题会很难定位。

服务发现

网关作为流量的入口,负责请求的转发,但首先需要知道转发给谁,如何寻址,这里有几种方式:

  • 写死在代码/配置文件里,这种方式虽然比较挫,但也能使用,比如线上仍然使用的是物理机,IP变动不会很频繁,但扩缩容、包括应用上下线都会很麻烦,网关自身甚至需要实现一套健康监测机制。
  • 域名。采用域名也是一种不错的方案,对于所有的语言都适用,但对于内部的服务,走域名会很低效,另外环境隔离也不太友好,比如预发、线上通常是同一个数据库,因此网关读取到的可能是同一个域名,这时候预发的网关调用的就是线上的服务。
  • 注册中心。采用注册中心就不会有上述的这些问题,即使是在容器环境下,节点的IP变更比较频繁,但节点列表的实时维护会由注册中心搞定,对网关是透明的,另外应用的正常上下线、包括异常宕机等情况,也会由注册中心的健康检查机制检测到,并实时反馈给网关。并且采用注册中心性能也没有额外的性能损耗,采用域名的方式,额外需要走一次DNS解析、Nginx转发等,中间多了很多跳,性能会有很大的下降,但采用注册中心,网关是和业务方直接点对点的通讯,不会有额外的损耗。

服务调用

网关由于对接很多种不同的协议,因此可能需要实现很多种调用方式,比如HTTP、Dubbo等,基于性能原因,最好都采用异步的方式,而Http、Dubbo都是支持异步的,比如apache就提供了基于NIO实现的异步HTTP客户端。 因为网关会涉及到很多异步调用,比如拦截器、HTTP客户端、dubbo、redis等,因此需要考虑下异步调用的方式,如果基于回调或者future的话,代码嵌套会很深,可读性很差,可以参考zuul和spring cloud gateway的方案,基于响应式进行改造。

优雅下线

优雅下线也是网关需要关注的一个问题,网关底层会涉及到很多种协议,比如HTTP、Dubbo,而HTTP又可以继续细分,比如域名、注册中心等,有些自身就支持优雅下线,比如Nginx自身是支持健康监测机制的,如果检测到某一个节点已经挂掉了,就会把这个节点摘掉,对于应用正常下线,需要结合发布系统,首先进行逻辑下线,然后对后续Nginx的健康监测请求直接返回失败(比如直接返回500),然后等待一段时间(根据Nginx配置决定),然后再将应用实际下线掉。另外对于注册中心的其实也类似,一般注册中心是只支持手动下线的,可以在逻辑下线阶段调用注册中心的接口将节点下线掉,而有些不支持主动下线的,需要结合缓存的配置,让应用延迟下线。另外对于其他比如Dubbo等原理也是类似。

性能

网关作为所有流量的入口,性能是重中之重,早期大部分网关都是基于同步阻塞模型构建的,比如Zuul 1.x。但这种同步的模型我们都知道,每个请求/连接都会占用一个线程,而线程在JVM中是一个很重的资源,比如Tomcat默认就是200个线程,如果网关隔离没有做好的话,当发生网络延迟、FullGC、第三方服务慢等情况造成上游服务延迟时,线程池很容易会被打满,造成新的请求被拒绝,但这个时候其实线程都阻塞在IO上,系统的资源被没有得到充分的利用。另外一点,容易受网络、磁盘IO等延迟影响。需要谨慎设置超时时间,如果设置不当,且服务隔离做的不是很完善的话,网关很容易被一个慢接口拖垮。

而异步化的方式则完全不同,通常情况下一个CPU核启动一个线程即可处理所有的请求、响应。一个请求的生命周期不再固定于一个线程,而是会分成不同的阶段交由不同的线程池处理,系统的资源能够得到更充分的利用。而且因为线程不再被某一个连接独占,一个连接所占用的系统资源也会低得多,只是一个文件描述符加上几个监听器等,而在阻塞模型中,每条连接都会独占一个线程,而线程是一个非常重的资源。对于上游服务的延迟情况,也能够得到很大的缓解,因为在阻塞模型中,慢请求会独占一个线程资源,而异步化之后,因为单条连接所占用的资源变的非常低,系统可以同时处理大量的请求。 如果是JVM平台,Zuul 2、Spring Cloud gateway等都是不错的异步网关选型,另外也可以基于Netty、Spring Boot2.x的webflux、vert.x或者servlet3.1的异步支持进行自研。

缓存

对于一些幂等的get请求,可以在网关层面根据业务方指定的缓存头做一层缓存,存储到Redis等二级缓存中,这样一些重复的请求,可以在网关层直接处理,而不用打到业务线,降低业务方的压力,另外如果业务方节点挂掉,网关也能够返回自身的缓存。

限流

限流对于每个业务组件来说,可以说都是一个必须的组件,如果限流做不好的话,当请求量突增时,很容易导致业务方的服务挂掉,比如双11、双12等大促时,接口的请求量是平时的数倍,如果没有评估好容量,又没有做限流的话,很容易服务整个不可用,因此需要根据业务方接口的处理能力,做好限流策略,相信大家都见过淘宝、百度抢红包时的降级页面。 因此一定要在接入层做好限流策略,对于非核心接口可以直接将降级掉,保障核心服务的可用性,对于核心接口,需要根据压测时得到的接口容量,制定对应的限流策略。限流又分为几种:

  • 单机。单机性能比较高,不涉及远程调用,只是本地计数,对接口RT影响最小。但需要考虑下限流数的设置,比如是针对单台网关、还是整个网关集群,如果是整个集群的话,需要考虑到网关缩容、扩容时修改对应的限流数。
  • 分布式。分布式的就需要一个存储节点维护当前接口的调用数,比如redis、sentinel等,这种方式由于涉及到远程调用,会有些性能损耗,另外也需要考虑到存储挂掉的问题,比如redis如果挂掉,网关需要考虑降级方案,是降级到本地限流,还是直接将限流功能本身降级掉。 另外还有不同的策略:简单计数、令牌桶等,大部分场景下其实简单计数已经够用了,但如果需要支持突发流量等场景时,可以采用令牌桶等方案。还需要考虑根据什么限流,比如是IP、接口、用户维度、还是请求参数中的某些值,这里可以采用表达式,相对比较灵活。

稳定性

稳定性是网关非常重要的一环,监控、告警需要做的很完善才可以,比如接口调用量、响应时间、异常、错误码、成功率等相关的监控告警,还有线程池相关的一些,比如活跃线程数、队列积压等,还有些系统层面的,比如CPU、内存、FullGC这些基本的。 网关是所有服务的入口,对于网关的稳定性的要求相对于其他服务会更高,最好能够一直稳定的运行,尽量少重启,但当新增功能、或者加日志排查问题时,不可避免的需要重新发布,因此可以参考zuul的方式,将所有的核心功能都基于不同的拦截器实现,拦截器的代码采用Groovy编写,存储到数据库中,支持动态加载、编译、运行,这样在出了问题的时候能够第一时间定位并解决,并且如果网关需要开发新功能,只需要增加新的拦截器,并动态添加到网关即可,不需要重新发布。

熔断降级

熔断机制也是非常重要的一项。若某一个服务挂掉、接口响应严重超时等发生,则可能整个网关都被一个接口拖垮,因此需要增加熔断降级,当发生特定异常的时候,对接口降级由网关直接返回,可以基于Hystrix或者Resilience4j实现。

日志

由于所有的请求都是由网关处理的,因此日志也需要相对比较完善,比如接口的耗时、请求方式、请求IP、请求参数、响应参数(注意脱敏)等,另外由于可能涉及到很多微服务,因此需要提供一个统一的traceId方便关联所有的日志,可以将这个traceId置于响应头中,方便排查问题。

隔离

比如线程池、http连接池、redis等应用层面的隔离,另外也可以根据业务场景,将核心业务部署带单独的网关集群,与其他非核心业务隔离开。

网关管控平台

这块也是非常重要的一环,需要考虑好整个流程的用户体验,比如接入到网关的这个流程,能不能尽量简化、智能,比如如果是dubbo接口,我们可以通过到git仓库中获取源码、解析对应的类、方法,从而实现自动填充,尽量帮用户减少操作;另外接口一般是从测试->预发->线上,如果每次都要填写一遍表单会非常麻烦,我们能不能自动把这个事情做掉,另外如果网关部署到了多个可用区、甚至不同的国家,那这个时候,我们还需要接口数据同步功能,不然用户需要到每个后台都操作一遍,非常麻烦。 这块个人的建议是直接参考阿里云、aws等提供的网关服务即可,功能非常全面。

其他

。。。当然可以扩展的功能还有很多很多。