之前弄的一套基于Kong+Consul的API网关项目, 觉得设计的还不错, 由于某些原因要下线了, 写文记录一下. 内容过长, 分为2part.

整体介绍

设计这个系统初期是打算做一个完整的微服务相关的组件的集成管理的系统, 到目前还没有那么大, 所以姑且缩小范围, 称为微服务API网关. 项目名Aster是立项时”物种日历”公众号当天的物种推文, 一种菊科植物, 从这个文章的发布日期看出, 立项是2019-01-07, 而今天是2020-01-09, 有点巧.

功能介绍

这个是最开始设计的功能模块, 截止目前涉及到前四个.

技术架构

最上面是我们开发的一个管理系统, 管理着中间那行的各个功能模块. 最下面是这个系统的用户的服务和实例.

  • Consul负责注册发现和健康检查. 可以通过管理系统的页面, 或者用户直接在自己的项目中使用HTTP接口或者java的sdk, 来注册用户的服务和实例. 可以通过HTTP或DNS方式进行查询. Consul也会通过向服务实例的健康检查url发送请求来检查健康状态.
  • Kong负责网关. 通过管理的页面进行配置. 域名解析到所有Kong节点上, 外部请求就会先请求到Kong, Kong执行自己的路由/负载均衡/鉴权等等逻辑, Kong会从Consul的DNS接口, 查询到这个请求对应的服务的后端实例地址, 然后将请求转发过去.
  • Kafka和Druid负责日志. Kong转发请求的同时, 会将请求日志通过rsyslog写入Kafka, Druid实时消费Kafka中的日志, 之后就可以从管理界面对请求日志进行报表查询.
  • Spring Cloud Config作为一个配置管理服务. 可以通过管理系统的界面, 或者用户直接在自己项目中加入SpringCloudConfig的sdk, 来进行配置的增删改查, 这个功能/用法和SpringCloudConfig是基本一样的.

做这个系统时主要考虑的还是南北流量(即从外部到后端服务的流量), 较少考虑东西流量(即服务直接互相调用的流量), 对于东西流量, 现在是只用上了Consul的注册发现功能, 然后服务之间直接通信, 没经过Kong, (可以自己弄客户端负载均衡), 所以也用不上这套系统的大多数功能.

技术选型

其实对应上面的每个功能, 都有做一些技术选型和调研, 做了一些对比:

Eureka对比Consul:

API网关对比, Zuul/SpringCloud Gateway/Kong等:

当然选型有以下这些想法吧:

  1. 首先排除自己开发, 有很多开源的项目了, 自己开发坑更多.
  2. 根据需求选择开源项目, 尽量选择需要定制开发较少的, 经过验证较成熟的.
  3. 整体看有几类项目吧: 基于Spring Cloud的一套, 基于Netflix的一套, CNCF和Service Mesh相关的, 容器相关的.

受限于自己的水平, 可能就这么选型了, 无所谓好坏.

核心模块

服务注册发现 - Consul

(上面两个图分别是Consul自带的页面和Aster服务注册的页面)

服务注册发现和健康检查这块是使用Consul来做的. 这个功能说简单点就是微服务拆分之后, 服务过多, 如何知道每个服务的各个实例的ip地址和端口号之类的信息? 配置文件写死肯定不好, 把这些信息统一由注册中心维护更佳.

需要考虑的功能和Consul的支持情况如下:

  • 如何注册? 支持通过HTTP接口进行注册
  • 如何查询? 支持通过HTTP和DNS进行查询
  • 支持注册哪些信息? ip,端口,协议,权重以及随意扩展的kv型的meta.
  • 多语言支持如何? HTTP/DNS接口当然比较通用, java有client sdk包(SpringCloudConsul)可用.
  • 对集群部署支持特别好, 支持多数据中心.
  • 支持HTTP, 脚本等健康检查方式, 失败节点自动下线.

Consul的关键概念

需要先要了解Consul内部各种概念.

  • agent. 后台运行的Consul daemon, 分为client/server两种运行mode.
  • client. client节点安装在业务服务实例机器上, 很轻量.
    1. 转发RPC请求的server
    2. 参与LAN gossip,
  • server. server节点在client那些事情基础上还要做更多.
    1. 最重要的, 集群的一致性协调, 即Raft协议过程, 选主等.
    2. 响应RPC请求, 非leader会转给leader.
    3. 与其他Datacenter中的server进行WAN gossip.
  • gossip. Consul使用Serf项目提供的gossip点对点协议, 实现membership, failure detection, and event broadcast等功能.
  • raft. 分布式一致性协议, 简化了传统的Paxos(Zookeeper)协议. Consul应该是自己实现的Raft协议, 这个介绍很不错.

安装部署的tips

  • Consul下载之后就是一个单一的可执行文件, 给他一个yaml配置就可以启动.
  • 怎么组集群. 先启动一个Server节点, 使用Bootstrap模式, 他就作为leader运行起来了. 然后再启动其余的server和client节点, 配置里面都join第一个节点即可. 配置里面不需要指定整个Consul集群的所有节点, 因为有gossip协议可以自动发现整个集群的所有节点. (不像某些集群组件, 启动的时候要不集群所有节点配置好, 说你呢ZK)
  • 注意各个节点之间的需要使用的端口, 当然内网端口全开的就不需要注意了.
  • Consul推荐使用sidecar模式部署(即在每个注册服务的实例的机器上, 运行一个Consul的agent), 这算是一个缺点吧, 不过我们是使用sidecar + Consul esm两种模式来解决这个问题. Consul的一个目标是向Service Mesh方向走, 目前不适用于我们的场景.
  • 我踩的一个坑: 一台机器A重装系统后打算加入一个Consul集群, 但是A每次启动Consul尝试加入集群就会崩. 后来抓包发现, 有另一台被忽略的机器B一直在尝试和A组队, 原来A和B曾经组过一个测试集群, 虽然A重装了, 但B一直在, 所以A只要一启动Consul, B就会和他同步, 导致A没法加入新的集群了.

Consul ESM

理解了上述Consul client和server的概念后, 又要引入一个和这俩都不一样的esm程序(External service monitoring for Consul). client和esm好像都是负责用户服务信息注册查询的, 区别在哪呢?

  • 当然最重要的client要和服务实例运行在同一台机器上, esm运行在任意的地方.
  • client既然和服务是同一机器, 也就强绑定: 这个实例在Consul看来从属于这个client node, 只有这个client node负责这个实例的健康检查. (整个Consul中一个服务可以用多个重名的实例, 只要他们是不同client就行). client要是挂了, 即使服务实例没挂, 在Consul中也查不到了. 其实查看Consul文档也会发现它的API都分为两类, agent类的和catalog类的, agent类的api只查询管理当前节点的服务信息, catalog是把整个Consul集群所有节点的服务信息聚合起来对外查询. 为什么在Consul里会有这么奇怪的设计呢?
  • esm是负责外部节点监控的(只负责监控, 没有正常Consul节点的查询注册等功能). esm节点和服务实例没什么特别大的毛线关系. 使用这种模式时, 服务实例要找一个远程的任意的Consul节点注册, 注册时说明自己是外部服务, 这个服务实例和它连接进行注册的节点没有绑定关系, 服务实例在Consul看来从属于外部节点(在Consul页面中也可以看到以服务机器ip对应的node, 但是那台机器上没有运行任何Consul有关的东西). 而esm节点只是被分配了一些任务说, 你来检查这些外部服务的状态, 一个esm挂了, 就分配另一个esm来检查这个状态. 一个典型的例子是, 我可以把baidu.com的ip端口用esm模式注册到我的Consul集群里.

Consul和管理系统的交互

搭建好Consul集群后, 管理系统则使用sdk(com.ecwid.Consul, 就是http接口)连接Consul集群进行管理(对服务进行增删改查), 但其实有些小问题

  • 管理系统会维护用户注册的服务和实例信息, 这个信息又要注册到Consul集群中, 一定要保证两者的信息是一致的, 事务要控制好.
  • 有一些服务和实例, 不是从管理系统页面而是自己用sdk直接注册的, 这个也要保证能对应上.
  • 管理系统也需要衔接Consul注册的服务和Kong中服务的对应, 按名字就行.
  • Consul安全. Consul本身是有一套ACL的, 可以控制不同的人能干不同的事, 这里我们暂时没有开启这个功能.

网关 - Kong

这个功能是整个系统的重点, API网关, 负责请求转发, 负载均衡, 鉴权, 限流等等, 可以理解为一个功能丰富的Nginx. Kong本身就基于OpenResty(nginx+lua)开发的一套API网关系统, 进行了插件化的设计, 可以通过添加插件实现各种各样的功能.

Kong的关键概念

  • service, 服务, 后端应用. 可以理解为Nginx的proxy_pass. 我们把Kong里的服务和Consul里的服务和整个管理系统的服务是一一对应的.
  • route, 路由. 可以理解为Nginx的server/location的配置, 决定一个到来的请求应该转发给哪个服务. 与服务是多对一的关系.
  • plugin, 插件. Kong提供的插件体系, 可以用lua脚本, 配合Nginx/OpenResty定义的请求生命周期, 以及Kong提供的pdk的api, 来编写插件.
  • consumer, 用户, 或者叫Kong上面服务的使用者.(Kong把这个概念定义出来, 可能为了方便权限控制, 但是我在这被坑了一下, 见下文)

安装部署的tips

  • Kong有打好的RPM包直接安装. 有带数据库和不用数据库两种使用模式, 其实就是配置信息存在哪里, 我们使用了PG数据库.
  • Kong的配置是Kong.conf, 但其实Kong目录下也有nginx的配置文件, 这个不要手动修改.
  • Kong也通过HTTP的api进行管理. 管理的端口和转发请求的端口是分开的, 其实就是借用Nginx的功能把管理请求单独路由出来, 因为Kong肯定是集群部署, 借用control plane之类的概念, 可以将集群中某个节点单独拿出来负责admin api. 这个管理其实就是接受配置修改请求, 修改数据库, 其他Kong节点要监听数据库的变化来更新自己的配置信息, 所以还有一定延迟. 除此之外Kong的集群节点间就没啥交互了, 无状态的东西就是好扩展.
  • 官方的可视化页面是企业版的, 部署了一个开源的Kong-dashboard可以用来查看信息.
  • Kong也推出了一个Service Mesh模式的用法, (刚查了下甚至把这种单独为一个项目Kuma) (Consul也推出Service Mesh, Kong也是, 还有原生Service Mesh的Istio等等, 都是凑热闹么, 还是Service Mesh是大方向?)

Kong和Consul的交互

在我们这个设计里, Kong和Consul都受管理系统管理, 然后Kong和Consul还要有些交互:

  • 管理系统给到Kong和Consul的服务是一致的, 按照名字对应.
  • Kong需要从Consul处获得服务的实例地址, 这里使用的是Consul的DNS功能
    • Consul处不需要做任何修改, 确保dig @127.0.0.1 -p 8600 my-Consul-service-name.service.Consul SRV可用就行.
    • Kong.conf里需要修改dns_resolver = 127.0.0.1:8600 (这里是Consul的dns端口, 可以看出Kong和Consul部署在同样的机器上了).
    • 管理系统给Kong配置服务的时候, 地址(host)那个参数传的是Consul服务域名如my-Consul-service-name.service.Consul, 然后Kong就根据DNS server去解析这个内部服务域名的真实实例地址了.
  • 上面没有用Kong的upstream功能, 其实用这个应该也可以配合, 实现更复杂的逻辑.

Kong和管理系统的交互

(图为Aster网关路由配置页面)

Aster的操作逻辑是, 先在刚才的实例注册页面, 注册服务名, 填好实例ip和端口. 然后在路由配置界面, 给这个服务配置域名和paths, 这个信息是给到Kong的, 就是Kong的服务和路由的配置. 之后请求来到后Kong就知道该转发给谁了.

Aster藉由Kong实现的功能除了路由转发还有负载均衡, 鉴权, 限流等, 都是Aster通过api控制kong中的插件配置.

插件

Kong的各种功能都靠插件来实现, 插件如前所说”可以用lua脚本, 配合Nginx/OpenResty定义的请求生命周期, 以及Kong提供的pdk的api, 来编写插件”. 一个插件的推荐目录结构如上图, 如果编写自己的插件最好直接复制一个官方插件代码过来, 在上面改.

  • 插件本质是一个luarocks管理的lua module, 打包发布安装都是luarocks负责的, 所以可以看到要有一个.rockspec文件, 可以理解为maven的pom.xml. 简单的了解一下luarocks即可. 注意的是目录结构要如上图那样, luarocks的模块名必须是Kong.plugins.xxx
  • 如上图所示, 需要编写几个lua脚本. 固定的几个lua脚本的用途如下:

  • 最关键的handler.lua中要做的就是之前所说配合Nginx/OpenResty定义的请求生命周期, 在各个插入点, 写自己的逻辑. 比如nginx的worker初始化的时候我要做什么, 请求到来的时候我要插入什么逻辑等等.
    -- Extending the Base Plugin handler is optional, as there is no real
    -- concept of interface in Lua, but the Base Plugin handler's methods
    -- can be called from your child implementation and will print logs
    -- in your `error.log` file (where all logs are printed).
    local BasePlugin = require "Kong.plugins.base_plugin"
    local CustomHandler = BasePlugin:extend()

    CustomHandler.VERSION  = "1.0.0"
    CustomHandler.PRIORITY = 10

    -- Your plugin handler's constructor. If you are extending the
    -- Base Plugin handler, it's only role is to instantiate itself
    -- with a name. The name is your plugin name as it will be printed in the logs.
    function CustomHandler:new()
    CustomHandler.super.new(self, "my-custom-plugin")
    end

    function CustomHandler:init_worker()
    -- Eventually, execute the parent implementation
    -- (will log that your plugin is entering this context)
    CustomHandler.super.init_worker(self)

    -- Implement any custom logic here
    end

    function CustomHandler:rewrite(config)
    -- Eventually, execute the parent implementation
    -- (will log that your plugin is entering this context)
    CustomHandler.super.rewrite(self)

    -- Implement any custom logic here
    end

    function CustomHandler:access(config)
    -- Eventually, execute the parent implementation
    -- (will log that your plugin is entering this context)
    CustomHandler.super.access(self)

    -- Implement any custom logic here
    end
    -- ...
    
  • 所有的插入点(OpenResty定义的请求生命周期)如下:

  • 我们实现了一个自定义token校验的插件, 由于lua本身的库较少, 像我们需要用到加解密和哈希都没有, 一般都是直接调用C语言的库, 还需要了解一下这个.
  • 我踩的一个坑: 配置key-auth插件必须依赖consumer概念, 而consumer又是全局的. 也就意味着, 给一个指定consumer开通了key-auth的token, 那么所有使用key-auth插件的service, 拿着这个token都可以访问?! 这是consumer设计的问题.