APISIX网关在虎符网络的实践

APISIX网关在虎符网络的实践

背景

当前我们API网关的应用场景是外部客户调用公司内部业务API,以及公司内部业务调用外部客户API的集中入口,是公司内部业务和外部客户合作的纽带。

作为企业暴露API的集中出口这个很好理解,可以减少不同业务团队对API鉴权、认证、容错等功能的重复开发,管控所有API流量,做到集中监控、集中审计,而且统一了鉴权方式,为外部客户带来了一致的API调用体验。

那为什么要把外部业务API的调用也集中到网关上呢?除了集中管控的优势,核心目的是提高效率。比如外部提供了一个车辆违章查询API,企业内部每个业务团队去对接一遍外部鉴权流程效率就会很低,如果API鉴权的事情由网关去做,业务只需要用熟悉的方式使用网关的API即可,甚至网关的API内部环境访问可以不需要鉴权,调用车辆违章查询API就像调用内部服务一样简单。

出于以上的应用场景,我们把API网关作为公司的基础设施来建设,并对所有API的调用进行观测。

选型

选型初期,我们没有把编程语言作为限制条件,我们要选用最流行性能最好的方案,在深度使用和对比了大量的API网关后,确定了三个候选:Spring Cloud Gateway、Kong和Apisix。

Spring Cloud Gateway

Spring Cloud Gateway是Spring微服务生态下的API网关,而我们正好有一支在Java方面很能打的团队,团队也倾向于使用Spring Cloud Gateway。

但是网关作为流量入口,分布式扩展和强悍性能是必须要考虑的点,最终因为Java在网关类产品性能上的担忧,以及下图的网关性能对比,Spring Cloud Gateway的性能相比其它微服务网关差了一大截,虽然有足够资源进行分布式部署后性能不会是大问题,但我们还是放弃了这个方案。

图片来源于《Comparing API Gateway Performances》

Kong vs Apisix

Kong和Apisix是基于高性能的Openresty进行开发,都能满足我们对网关的基本诉求,Apisix官方也给出了两者的详细对比,我们最终选择了Apisix,主要原因有以下几点:

  • Kong虽然历史悠久和稳定,但二次开发比较复杂,提供的Dashboard不够友好
  • Apisix的性能比Kong更好
  • Apisix支持Dubbo和gRPC代理,作为深度使用dubbo的公司,对外暴露API会更便捷
  • 国产+Apache,活跃的社区

说的简单点,Apisix的高性能和插件化机制贴合我们对API网关的技术要求,Apisix的易用性和简洁架构贴合团队的技术能力,虽然个人在Golang和Lua语言上没有任何经验,但都不是问题。

并且Apisix作为国产软件,顺利从Apache毕业,又有什么理由不支持呢?

源码构建入门

Apisix的架构模块比较清晰,存储选用etcd,消息分发、高可用和可扩展性都是基于etcd进行,接下来演示在CentOS服务器(假定IP地址为192.168.2.240)上部署etcd,在本地MacOSX上搭建开发环境,本地开发环境包含三个部分:

  • apisix(Openresty+Lua)
  • Manage-API管理控制后台(Golang)
  • web管理控制台前端(Node+YARN)

CentOS安装etcd

# 安装 etcd
wget https://github.com/etcd-io/etcd/releases/download/v3.4.13/etcd-v3.4.13-linux-amd64.tar.gz
tar -xvf etcd-v3.4.13-linux-amd64.tar.gz
cd etcd-v3.4.13-linux-amd64
sudo cp -a etcd etcdctl /usr/bin/

# 启动 etcd server,关闭ps -aux | grep etc
nohup etcd &

注意的是,etcd默认只监听本地IP,如果要远程访问etcd,启动命令需要加上--listen-client-urls--advertise-client-urls,就像这样:

nohup etcd --listen-client-urls="http://0.0.0.0:2379"  --advertise-client-urls="http://0.0.0.0:2379" &

安装完成后,可以在服务器上通过以下命令验证是否安装成功:

etcdctl put mykey "this is awesome"
etcdctl get mykey

安装etcdkeeper

对于我来说,命令行操作etcd终究没有一个好用的GUI工具效率高,正如Sequel操作Mysql,etcdkeeper是连接和操作etcd的管理工具,这里我采用Docker方式在服务器上安装:

docker pull evildecay/etcdkeeper
docker run -d --name etcdkeeper -p 7080:8080 evildecay/etcdkeeper:latest

安装完成后,浏览器访问192.168.2.240:7080/etcd,输入ETCD地址:192.168.2.240:2379,连接成功后即可对存储数据进行操作和编辑。

开发apisix

安装环境

apisix是整个API网关的核心,在构建源码前需要安装Openresty和Lua环境,如在本地MacOSX上进行依赖安装:

brew install openresty/brew/openresty luarocks lua@5.1

源码下载

安装完成后,下载apisix源码,这里直接选择主干代码,目前代码版本是2.7待发布状态,你也可以选择一个稳定分支:

git clone https://github.com/apache/apisix.git

配置

apisix的配置文件是conf/config-default.yaml(或者conf/config.yaml,会覆盖config-default.yaml中的配置),首先要配置etcd.host地址,其次apisix启动后默认只监听本地的9080端口,可以设置allow_admin允许任何IP访问。

etcd:
  host:
    - "http://192.168.2.240:2379"

开发和编译

apisix核心部分主要是由Lua语言编写,Lua是一种脚本语言,使用VSCode作为开发工具,编译并运行apisix:

# 编译
make deps

# 检查版本号
./bin/apisix version

# 启动和停止
./bin/apisix start
./bin/apisix stop

Makefile文件每个版本都在迭代,我在apisix多个版本中都没有编译成功,在当前的主干代码里依赖HOMEBREW_PREFIX环境变量,而我本地正好没有设置过这个变量,运行编译命令前要设置正确的变量值:

export HOMEBREW_PREFIX="/usr/local";make deps

开发Manage-API

安装环境

apisix的数据面和控制面是分离的,Manage-API作为控制后台,将配置数据写入etcd,etcd通过消息分发机制推送给apisix网关,首先我们需要安装Golang环境:

# 安装最新版本go
brew install go
# 加快下载
go env -w GOPROXY=https://goproxy.cn,direct

源码下载

安装完成后,下载apisix-dashboard源码,需要选择一个和apisix适配的版本,否则管理界面会提示版本不匹配:

git clone https://github.com/apache/apisix-dashboard.git

apisix-dashboard源码包含了Manage-API项目和web前端项目,分别在api和web目录下。

配置

Manage-API的配置文件是apisix-dashboard/api/conf/conf.yaml,首先我们也要配置etcd.endpoints地址,其次apisix-dashboard启动后默认只监听本地的9000端口,可以修改监听的IP地址。

etcd:
  endpoints:
    - 192.168.2.240:2379

开发和编译

Go开发我选择Goland作为开发工具(注意Goland是一款商业软件,使用前需要获得授权),安装Goland后导入apisix-dashboard/api目录下的代码,自动编译完成后运行main.go的main方法即可启动管理后台:

The manager-api is running successfully!

Version : 
GitHash : 
Listen  : 0.0.0.0:9000
Loglevel: warn
Logfile : /Users/Sayi/apisix-dashboard/api/logs/error.log

开发web前端

安装环境

apisix管理控制台的前端是Node+AntDesign技术栈,开发前需要安装一些依赖:

nvm install v14.16.0
node -v
# 设置taobao源
npm config set registry http://registry.npm.taobao.org
# 安装yarn
npm install -g yarn
yarn -v
# 安装nrm
npm i nrm -g
nrm ls
nrm use taobao

配置

前端项目地址在apisix-dashboard/web下,导入到VSCode中,修改配置文件config/defaultSettings.ts,将开发环境指向Manage-API后端地址:

serveUrlMap: {
  dev: 'http://localhost:9000',
},

开发和编译

编译和运行前端项目:

# 安装依赖
npm install

# 启动
npm run start:no-ui

启动完成后,浏览器访问http://localhost:8000/,默认登录账号密码为admin/admin。

插件

在正式开发插件前,应当深入了解Apisix的设计理念和一些核心概念,比如路由、上游、消费者、服务等,插件可以配置在路由上,也可以配置在服务、消费者、插件模板和全局插件上,Apisix给予了很大的配置灵活性,但是也损失了易用性;

你还需要学习Apisix官方的插件开发入门和一些Nginx的基础知识,下图是Openresty的生命周期,具体阶段的作用就不一一介绍了:

毫不夸张的讲,插件是一个网关的灵魂,通过插件Hook API请求执行的每个阶段,让产品具有高度扩展性。

MOCK插件

开发和测试阶段经常需要模拟某个API的调用,即不真实调用后端上游,直接返回模拟的响应内容。接下来我们编写这样一个MOCK插件,在创建API时可以不选择上游或者服务,直接启用MOCK插件,支持配置响应码、响应内容类型和响应内容。

定义Schema

插件的脚本文件名为mock.lua,首先我们要设计MOCK插件的数据结构,即Schema:

local schema = {
  type = "object",
  properties = {
      code = {type = "integer", minimum = 200, maximum = 599},
      content_type = {
        type = "string",
        enum = {"application/json", "text/html", "text/plain"},
        default = "application/json"
      },
      body = {type = "string"}
  },
  required = {"code"}
}

function _M.check_schema(conf)
  return core.schema.check(schema, conf)
end

我们定义了三个字段,分别表示响应码code、响应内容类型content_type和响应内容body,其中响应类型可以是JSON也可以是网页,code必填。

插件名称

其次我们要定义插件的基础属性:

local plugin_name = "mock"

local _M = {
    version = 0.1,
    priority = 2200,
    name = plugin_name,
    schema = schema
}

Apisix中插件执行优先级是由priority字段唯一定义,优先级越高的插件优先执行,在编写新插件前我们要知道现有所有插件的优先级,并且为自己的插件选择一个合适的优先级,这一步对开发来说并不是那么友好。

插件执行阶段

可以选择在access阶段或者rewrite阶段设置响应码和响应体,

如果在rewrite阶段执行并且mock插件的优先级高于认证插件,那么认证插件将永远不会执行,因为mock插件的rewrite阶段提前返回,认证插件的rewrite阶段不会被执行。
function _M.access(conf, ctx)
  core.log.warn("plugin access phase, conf: ", core.json.encode(conf))
  return conf.code, conf.body
end

由于更改了响应体内容,我们需要在返回头中重置content-length、last-modified和etag:

function _M.header_filter(conf, ctx)
  core.response.clear_header_as_body_modified()
  ngx.header["Content-Type"] = conf.content_type
end

加载插件

为了让插件在网关和管理控制台生效,还需要做一些配置:

  • 在apisix/conf/config.yaml中启用mock插件
  • 在apisix-dashboard/api/conf/conf.yaml中启用mock插件,并且在apisix-dashboard/api/conf/schema.json中新增mock插件的配置

使用插件

打开管理控制台,创建路由,输入路由名称和路径,不选择任何上游和服务,启用mock插件:


MOCK插件配置如下:

{
  "mock": {
    "body": "{\"hello\":\"mock\"}",
    "code": 200,
    "content_type": "application/json"
  }
}

浏览器访问配置好的API路由来验证效果。

其它插件

为了满足内部功能需求,大部分时间都在编写业务插件,当然其中也有一些使用的比较好的插件。

Response-Query

类似GraphSQL,在调用查询接口时可以指定返回值的领域模型,路由会很好的工作并且只返回你想要的数据,这个插件只要你习惯了,就再也回不去了。

Compose-auth

组合认证插件,这是为了解决对于同一个路由,有多个消费者,并且每个消费者拥有不同认证方式Apisix无法配置的问题,按照目前Apisix的设计,所有消费者的认证方式都需要在路由中启用,而路由中的所有认证方式都会执行,这就导致任何一个消费者都无法正确调用该路由。

组合认证插件可以组合目前已有的认证插件,消费者只需要满足任意一种认证方式就可以调用API,有点类似于Apisix的插件编排。

Dashboard

我们在管理控制台上也有很多优化,新增了用户管理、权限控制、审计日志、统计报表和安全等功能。

用户权限

管理控制台的管理员登录名和密码默认存放在api/conf/conf.yaml配置文件中,这样的单一身份比较弱。

我们把Apisix与公司的单点登录打通,为Apisix提供了对接OAuth2-Server的能力,Manage-api通过code获取access_token的请求构造源码如下:

input := c.Input().(*GetInput)
form := url.Values{}
form.Add("grant_type", "authorization_code")
form.Add("code", input.Code)
form.Add("client_id", conf.OAuthConf.ClientId)
form.Add("client_secret", conf.OAuthConf.ClientSecret)
req, err := http.NewRequest("POST", conf.OAuthConf.AccessTokenUrl, strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if err != nil {
    return nil, err
}

http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
client := http.DefaultClient
resp, err := client.Do(req)
if err != nil {
    return nil, err
}

var result map[string]string
json.NewDecoder(resp.Body).Decode(&result)

同时也新建了一套用户角色权限模型,账号数据存储在etcd中,系统提供了多个默认角色,如管理员、操作员、审计员等,前端使用umi-access框架,达到菜单和页面级元素的权限控制能力。

日志和报表

Apisix提供了很多日志插件,通过http-logger插件和elastic的RestAPI可以将日志导入到ES中。

虽然是异步调用,在网关访问流量比较大的时候也会出现ES API调用失败,我们重新修改了插件,加上bufferpool并且使用ES的批量bulk API得到了很好的优化效果。

默认的监控页面是基于Prometheus和Grafana进行展示,社区的这个方案并不完善,我们的解决方案是完全丢弃Grafana,重写了整个监控和仪表盘。统计的方案有两种:

  • Prometheus

Apisix提供了Prometheus插件将监控数据导入时序数据库,这样我们可以编写PromQL来实现监控页,比如:

查询TOP 5的消费者:

topk(5, sum(apisix_http_status) by (consumer))&start=1618713193&end=1618282631

时间范围内500状态码的统计:

sum(apisix_http_status{code=~"5.."})&start=1617677830&end=1618282631
  • ElasticSearch

既然所有原始日志都存在ES中,那么可以通过ES的查询和聚合API对请求日志进行分析和统计,最终使用了这个方案,可以通过简单的查询语法生成众多统计图表。

插件配置界面

JSON Schema是一个标准协议,主要用来验证数据的有效性,Apisix的插件由JSON Schema定义和约束,在Dashboard的插件配置页面,有JSON和YAML二种方式,更友好的方式肯定是通过表单配置。

React组件react-jsonschema-form支持从JSON Schema直接生成各种主题(AntD、BootStrap4等)的表单,试用后样式和apisix-dashboard还是格格不入,最终自己实现了一个生成表单的组件,能支持目前所有插件配置,有一些更复杂的语义并不支持,但是提供了UI扩展,可以定制具体的表单项。


总结

作为API网关,Apisix可能在某些细节功能上还有待完善,比如不支持path路径的配置:user/{userid}/addr,但是他们有着一个活跃的社区,并且在不断进步。

下一步我们将重点在两个方向进行建设:整合告警平台,并且加强API文档的展现。


「如发现文章有错误、对内容有疑问,或者进行技术交流,都可以扫码关注虎符技术团队微信公众号在后台给我们留言。」

编辑于 2021-11-12 15:27