Kong-proxy

3,859 阅读5分钟

最近项目中需要做一个支持多种协议(http,ws,grpc)的网关,本着不重复造轮子的想法,调研了市面上常用的网关,最后从性能、社区活跃度、稳定性、可扩展性上选中了kong。

下面是kong的核心功能proxy的学习笔记。

简介

kong会暴露很多的接口,可以归纳为3个配置:

  1. proxy_listen:它定义了一系列的address/port,他们用于接收公共的http、ws、grpc等请求然后把这些请求代理到你的upstream service上去。(默认8000端口)
  2. admin_listen:它定义了一系列的address/port,但是这些地址最好只限管理员访问,它们用于kong的配置管理。(Admin API,默认8001端口)
  3. stream_listen:和proxy_listen比较像,它适用于4层代理的。默认是关闭的。


术语

  1. client:向proxy发请求的下游用户。
  2. upstream service:用户请求经过kong转发到的服务。
  3. Service:很明显,对你的每个upstream service的抽象,服务的例子可以是数据转换微服务,账单API等。
  4. Route:指kong的route实体。是kong的入口点,根据定义的规则把请求路由到制定的Service。
  5. Plugin:是一系列运行在proxy整个生命周期的业务逻辑。可以通过Adman API配置,可以是全局的(针对全部的输入)或者是针对特定的Routes或Services。


概览

从全局来看,kong在它配置的proxy端口上(http & https 默认8000和8443、L4流量在stream_listen指定的port上)监听输入流量。然后根据配置的Route对流量进行匹配,如果匹配成功就开始proxy这些请求。

kong会运行配置在Route和该route关联的service上的plugin,然后把请求proxy到upstream上去。用户可以通过Admin API管理route。route有专门的属性用于路由,并且这些属性会根据协议的不同有所不同。具体如下:

  • http: methods, hosts, headers, paths (and snis, if https)
  • tcp: sources, destinations (and snis, if tls)
  • grpc: hosts, headers, paths (and snis, if grpcs)

如果用户给Route配置了它不支持的属性,如http route配置了source和destination,就会收到一个错误的信息,如下:

HTTP/1.1 400 Bad Request
Content-Type: application/json
Server: kong/<x.x.x>

{
    "code": 2,
    "fields": {
        "sources": "cannot set 'sources' when 'protocols' is 'http' or 'https'"
    },
    "message": "schema violation (sources: cannot set 'sources' when 'protocols' is 'http' or 'https')",
    "name": "schema violation"
}

如果kong收到了一个request没有匹配到任何的route,会返回如下错误:

HTTP/1.1 404 Not Found
Content-Type: application/json
Server: kong/<x.x.x>

{
    "message": "no route and no Service found with those values"
}

如何去配置一个service

通过向Admin API发送一个http请求来增加Service

curl -i -X POST http://localhost:8001/services/ \
    -d 'name=foo-service' \
    -d 'url=http://foo-service.com'
HTTP/1.1 201 Created
...

{
    "connect_timeout": 60000,
    "created_at": 1515537771,
    "host": "foo-service.com",
    "id": "d54da06c-d69f-4910-8896-915c63c270cd",
    "name": "foo-service",
    "path": "/",
    "port": 80,
    "protocol": "http",
    "read_timeout": 60000,
    "retries": 5,
    "updated_at": 1515537771,
    "write_timeout": 60000
}

在上面这个例子中回想kong注册一个名为foo-service的服务,这个服务指向地址为http://foo-service.com的一个upstream。

现在为了能够通过kong给上面的service发请求,我们需要定义一个Route,它扮演者kong的入口的角色:

curl -i -X POST http://localhost:8001/routes/ \
    -d 'hosts[]=example.com' \
    -d 'paths[]=/foo' \
    -d 'service.id=d54da06c-d69f-4910-8896-915c63c270cd'
HTTP/1.1 201 Created
...

{
    "created_at": 1515539858,
    "hosts": [
        "example.com"
    ],
    "id": "ee794195-6783-4056-a5cc-a7e0fde88c81",
    "methods": null,
    "paths": [
        "/foo"
    ],
    "preserve_host": false,
    "priority": 0,
    "protocols": [
        "http",
        "https"
    ],
    "service": {
        "id": "d54da06c-d69f-4910-8896-915c63c270cd"
    },
    "strip_path": true,
    "updated_at": 1515539858
}

我们配置了一个route,它会根据入口流量是否匹配host和path来转发到foo-service。

kong的代理是透明的,它默认会把请求原封不动的发送给你的upstream service,除了一些特殊的headers(如Connection等,下面有详细说明)

Routes如何匹配

kong原生的支持HTTP/HTTPS, TCL/TLS 和 GRPC/GRPCS。每种不同的协议接受不通的路由属性。如下所示:

  • http: methods, hosts, headers, paths (and snis, if https)
  • tcp: sources, destinations (and snis, if tls)
  • grpc: hosts, headers, paths (and snis, if grpcs)

下面我看看一些简单的例子

如果route配置如下:

{
    "hosts": ["example.com", "foo-service.com"],
    "paths": ["/foo", "/bar"],
    "methods": ["GET"]
}

那下面的这些请求就会匹配到该route

GET /foo HTTP/1.1
Host: example.com

GET /bar HTTP/1.1
Host: foo-service.com

GET /foo/hello/world HTTP/1.1
Host: example.com

但是下面的请求就不能匹配route

GET / HTTP/1.1
Host: example.com

POST /foo HTTP/1.1
Host: example.com

GET /foo HTTP/1.1
Host: foo.com

了解了route的工作机制以后,我们逐个看一下每个属性。

Request Header

从kong 1.3开始,kong的route就可以支持人意的httpheader。比如上述例子中的host header,让转发看起来很直观。

host可以使用逗号隔开,接受多个值。下面展示了一个发往Admin API的多个值得host的例子:

$ curl -i -X POST http://localhost:8001/routes/ \
    -H 'Content-Type: application/json' \
    -d '{"hosts":["example.com", "foo-service.com"]}'
HTTP/1.1 201 Created
...

Admin API也支持form-urlencoded格式,所以也可以通过[ ] 来表示一个数组:

$ curl -i -X POST http://localhost:8001/routes/ \
    -d 'hosts[]=example.com' \
    -d 'hosts[]=foo-service.com'
HTTP/1.1 201 Created
...

当然其他的header也可以用做route:

$ curl -i -X POST http://localhost:8001/routes/ \
    -d 'headers.region=north'
HTTP/1.1 201 Created

输入的请求包含Region header,并且值为North的将被该路由匹配。

Region: North

hostname使用通配符

为了提高灵活性,kong允许在hosts字段上使用通配符。

hostname通配符最多只能在最左边 和 最右边有一个星号,例如:

  • *.example.com 允许 a.example.com 和 x.y.example.com 匹配.
  • example.* 允许 example.com 和 example.org 匹配.

preserve_host属性

当代理时,kong的默认做法是将upstream request的host设置为service里面制定的host。preserve_host接受一个boolean值用于标明是否保留客户端请求里的host。

例如:当preserve_host属性没有设置时,一个配置如下的route:

{
    "hosts": ["service.com"],
    "service": {
        "id": "..."
    }
}

客户端请求如下:

GET / HTTP/1.1
Host: service.com

kong会提取service里的host属性,然后把它发送给upstream:

GET / HTTP/1.1
Host: <my-service-host.com>

当时当我们手动设置了preserve_host=true后:

{
    "hosts": ["service.com"],
    "preserve_host": true,
    "service": {
        "id": "..."
    }
}

一个同样的客户端请求:

GET / HTTP/1.1
Host: service.com

kong就会保留客户端请求中的host,然后把它发给upstream,所以upstream收到的请求就如下:

GET / HTTP/1.1
Host: service.com

Request headers(除Host)

kong1.3.0以后,route支持了除host以外的其他header。

例如以如下方式定义了一个route:

{
    "headers": { "version": ["v1", "v2"] },
    "service": {
        "id": "..."
    }
}

下面两种请求都可以匹配上该route

GET / HTTP/1.1
version: v1

GET / HTTP/1.1
version: v2

但下面的就不行

GET / HTTP/1.1
version: v3

注意:header的key的逻辑是AND,value的逻辑是OR

Request Path

另一种用于route匹配的方式是request path。为了满足这种route,客户端的请求路径中必须以path属性中的值作为前缀。

比如route定义如下:

{
    "paths": ["/service", "/hello/world"]
}

下面的三种请求都会匹配:

GET /service HTTP/1.1
Host: example.com

GET /service/resource?param=value HTTP/1.1
Host: example.com

GET /hello/world/resource HTTP/1.1
Host: anything.com

默认情况下kong把请求抓发给upstream时不会修改url。路径的匹配规则符合最长路径优先原则,这保证了如果你定义两个路径为/service/service/resource前一个路径不会影响后一个路径的匹配。

Path中的正则表达式

path的使用PCRE (Perl Compatible Regular Expression),可以同时给path设置为前缀模式 和 正则表达式模式。如下例:

{
    "paths": ["/users/\d+/profile", "/following"]
}

下面的请求都会匹配这个route:

GET /following HTTP/1.1
Host: ...

GET /users/123/profile HTTP/1.1
Host: ...

匹配顺序

  1. 前缀表达式按照最长匹配原则。
  2. 正则表达式基于regex_priority属性,该值越大,优先级越高。
  3. 正则表达式优先级高于前缀表达式。

如下例:

[
    {
        "paths": ["/status/\d+"],
        "regex_priority": 0
    },
    {
        "paths": ["/version/\d+/status/\d+"],
        "regex_priority": 6
    },
    {
        "paths": ["/version"],
    },
    {
        "paths": ["/version/any/"],
    }
]

优先级如下:

  1. /version/\d+/status/\d+
  2. /status/\d+
  3. /version/any/
  4. /version
Capturing groups

正则表达式支持capturing group,而且可以被plugin消费,例如正则如下:

/version/(?<version>\d+)/users/(?<user>\S+)

请求地址如下:

/version/(?<version>\d+)/users/(?<user>\S+)

如果我们想在ngx.ctx插件中使用上述匹配中的capturing group,结果如下:

local router_matches = ngx.ctx.router_matches

-- router_matches.uri_captures is:
-- { "1", "john", version = "1", user = "john" }

The strip_path property

strip_path用于指定当前path前缀匹配Route,但是不把该前缀放到upstream的请求中。如下:

{
    "paths": ["/service"],
    "strip_path": true,
    "service": {
        "id": "..."
    }
}

 此时如果请求如下:

GET /service/path/to/resource HTTP/1.1
Host: ...

发往upstream的请求如下:

GET /path/to/resource HTTP/1.1
Host: ...

针对正则表达式,例子如下:

{
    "paths": ["/version/\d+/service"],
    "strip_path": true,
    "service": {
        "id": "..."
    }
}

客户端请求如下:

GET /version/1/service/path/to/resource HTTP/1.1
Host: ...

发往upstream的请求如下:

GET /path/to/resource HTTP/1.1
Host: ...

Request HTTP method

这个没啥好说的,就是根据http的method来route

Request Source/Destination

注意:这个是只针对TCP和TLS的路由

sources路由属性支持匹配一系列的源ip/port。例子如下:

{
    "protocols": ["tcp", "tls"],
    "sources": [{"ip":"10.1.0.0/16", "port":1234}, {"ip":"10.2.2.2"}, {"port":9123}],
    "id": "...",
}

Request SNI

当使用安全协议(https、grpc、tls)时,sni(server name indication)可以作为一个route的属性。例如:

{
  "snis": ["foo.test", "example.com"],
  "id": "..."
}

入口流量和TLS链接的snis设置的hostname匹配的会被该路由识别。并且SNI路由不仅可以用于TLS,也可以用于其他在TLS之上的协议(比如:https等)。这里面多个hostname是OR的关系。

SNI在TLS握手时被确定,并且在整个链接中是无法修改的。这就意味着在同一个keepalive链接上的多个请求拥有相同的SNI路由匹配。注意,理论上可以创建一个route,他的SNI和host header值不一样,但是不建议这样。

匹配优先级

一个路由的匹配规则可以基于headers, hosts, paths, 和 methods (还有snis用于sercure路由 - "https", "grpcs", "tls")  当规则有交集的时候,kong的优先级规则是:kong会寻找规则最多的路由。

比如下例:

{
    "hosts": ["example.com"],
    "service": {
        "id": "..."
    }
},
{
    "hosts": ["example.com"],
    "methods": ["POST"],
    "service": {
        "id": "..."
    }
}

第一个规则不会遮挡住第二个规则

// 匹配第一个规则
GET / HTTP/1.1
Host: example.com

//匹配第二个规则
POST / HTTP/1.1
Host: example.com

如果当两个route(A和B)的规则数相同时,按照如下规则依次判断,如有符合,则A优先于B

  • A仅有纯文本的Host header,B有一个或多个通配符的Host header。
  • A有更多的非Host的header
  • A的Path中至少包含一个正则表达式,而B的Path是存文本
  • A的Path比B的长
  • A.created_at < B.create_at


代理行为

上面我们聊了kong如何把输入流量导入到你的upstream中来,下面我们聊聊在kong匹配到一个http请求到route后,并且在实际把流量转发之前发生了什么。

1. load balancing

kong支持负载均衡,这个后面会独立章节讲。

2. 执行插件

kong支持通过插件的方式扩展代理,插件可以是全局的,也可以是Route级或者Service级。可以通过Admin API来创建plugin。当Route匹配后,kong会执行相关的plugin,执行顺序为先执行route上的,在执行Service上的,具体关于插件的内容后面也会有单独的章节聊。

3. Proxying & upstream timeouts

当Kong执行完了所有的必要逻辑(包括插件),它就可以把请求发送到指定的upstream服务上了。它是通过nginx的ngx_http_proxy_module来做的。用户可以通过Service的如下属性来配置kong和给定upstream的timeout时间:

  • upstream_connect_timeout: 以毫秒为单位,定义了kong和upstream建立连接的时间. 默认是 60000.
  • upstream_send_timeout: 以毫秒为单位,定义了两个连续写的间隔,默认是 60000.
  • upstream_read_timeout: 以毫秒为单位,定义了两个连续读的间隔. 默认是 60000.

kong还可以设置如下的header:

  • Host: <your_upstream_host>
  • Connection: keep-alive
  • X-Real-IP: <remote_addr>
  • X-Forwarded-For: <address>
  • X-Forwarded-Proto: <protocol>
  • X-Forwarded-Host: <host>
  • X-Forwarded-Port: <port>

除此之外kong会直接转发其他的header。

有一个例外,当使用ws协议时,kong会设置下面的header用于在client和upstream之间升级协议:

  • Connection: Upgrade
  • Upgrade: websocket

4. 错误 & 重试

无论何时遇到proxy的错误,kong会使用底层的nginx的错误尝试机制,把请求转发到下一个upstream。

有两种可配置的元素:

  1. 重试次数:可以通过retries属性为每一个Service配置。
  2. 什么构成了错误:这里Kong使用Nginx默认值,这意味着在建立与服务器的连接,传递请求或读取响应头文件时发生错误或超时。

第二个是基于nginx的 [proxy_next_upstream][proxy_next_upstream]指令的,他不能直接通过kong配置,但是可以通过增加自定义nginx配置的方式配置。

5. Response

kong接收upstream的response,然后把它发送给下游的客户端。这时kong会执行关联到Route或Service上,并且实现了header_filter钩子的plugin。执行完成后,下面这几个header会添加到返回客户端的结果中。

  • Via: kong/x.x.xx.x.x 代表kong的版本
  • X-Kong-Proxy-Latency: <latency>, latency代表的是kong从客户端收到请求 到 发送请求到upstream service。
  • X-Kong-Upstream-Latency: <latency>, latency代表kong等待upstream返回第一个byte的时间。 

当header发送到客户端以后,kong就会执行该Route/Service上实现了body_filter的钩子。这个钩子在发送每个chunk时都会执行。

兜底 Route

可以使用如下方式创建一个兜底路由:

{
    "paths": ["/"],
    "service": {
        "id": "..."
    }
}

Configuring TLS for a Route

kong提供了给每个链接动态提供TLS认证的功能。TLS证书使用Admin API配置,并由core直接处理。客户端需要支持SNI(server name indication)扩展才能使用该功能呢。Admin API中有两个api处理TLS:

  • /certificates, 它用于存储key和certificates
  • /snis, 关联SNI和注册的certificate

配置TLS certificates到一个Route的步骤如下:

首先上传TLS certificates 和 key:

$ curl -i -X POST http://localhost:8001/certificates \
    -F "cert=@/path/to/cert.pem" \
    -F "key=@/path/to/cert.key" \
    -F "snis=*.tls-example.com,other-tls-example.com"
HTTP/1.1 201 Created
...

注意上述参数中的snis是一个语法糖,它直接关联SNI和上传的certificates。

现在开始注册route,为了方便起见我们直接使用Host header匹配该路由。

$ curl -i -X POST http://localhost:8001/routes \
    -d 'hosts=prefix.tls-example.com,other-tls-example.com' \
    -d 'service.id=d54da06c-d69f-4910-8896-915c63c270cd'
HTTP/1.1 201 Created
...

现在你可以使用这个route来接收https请求了。

$ curl -i https://localhost:8443/ \
  -H "Host: prefix.tls-example.com"
HTTP/1.1 200 OK
...

当建立TLS握手时,如果客户端发送了prefix.tls-example.com作为SNI,kong就会使用上述提供的certificates了。

Proxy WebSocket traffic

从1.3版本开始,kong就原生支持grpc了,具体情况后面会详细聊。并且1.3里kong提供的grpc的插件只有观测和日志的。(这个可能不满足我们的业务需要,待确认)