Kong代理向导
简介
在这篇文档中,我们会通过详细介绍Kong的路由功能和内部工作原理来涵盖其代理功能,Kong通过两个配置项对外暴露代理相关的接口:
-
proxy_listen
:其中定义了一系列地址和端口,接收来自客户端的公共流量,代理到用户的上游服务,默认为8000
-
admin_listen
:同样定义了一些列地址和端口,但限制为仅供管理员访问,其中暴露了Kong的配置功能,即 Admin API,默认为8001
术语
-
client
:向Kong代理端口发出请求的应用 -
upstream service
:位于Kong服务后台的用户自己的API服务,通过Kong转发客户端请求 -
service
:服务实体,每个上游服务的抽象 -
route
:Kong的路由实体,其中定义了请求路由到对应服务的规则 -
plugin
:在代理生命周期中运行的业务逻辑,可以全局设置,也可以设置在特定的路由和服务上
概要
纵观全局,Kong服务监听其配置代理端口的Http流量(默认为 8000
和 8443
端口),然后根据用户配置的路由尝试匹配流入的Hyyp请求,当找到匹配的路由时,Kong会代理该请求,每个路由都关联到一个服务,Kong会运行用户配置在路由和其关联的服务中的插件,然后代理到上游服务
用户可以通过 Admin API 管理路由,路由有 hosts
、paths
、methods
这些属性
如果Kong接收到一个请求,但是找不到匹配的路由,它会返回:
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"
}
回顾:如何配置服务
通过调用Admin API请求,向Kong添加服务:
curl -i -X POST http://localhost:8001/services/ -d 'namefoo-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
上游服务,url是一个简写参数,用于一次性填充 protocol
、hosts
、paths
、methods
这些属性
现在我们指定一个路由,作为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
}
这样我们就配置好了路由,通过指定的 hosts
和 paths
转发到 foo-service
服务
Kong是一个透明的代理,默认情况下它会将请求中的各种Header透传给上游服务
路由和匹配功能
现在我们讨论Kong是如何根据路由中配置的 hosts
、paths
、methods
等属性匹配请求的,这三个属性都是可选的,但是必须指定其中一个,匹配路由的请求需要满足:
- 请求必须包含路由中配置的所有属性;
- 对于单个属性,至少匹配属性中的某一个值;
示例路由:
{
"hosts": ["example.com", "foo-service.com"],
"paths": ["/foo", "/bar"],
"methods": ["GET"]
}
下面这些请求都匹配路由:
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
下面这些请求不能匹配路由:
GET / HTTP/1.1
Host: example.com
POST /foo HTTP/1.1
Host: example.com
GET /foo HTTP/1.1
Host: foo.com
请求中的 Host 头
基于 Host 头转发请求非常普遍,Kong配置路由的 hosts
属性非常简便,hosts
可传入多个值,使用,
分隔,添加多个 hosts
时使用JSON格式:
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
...
现在也支持使用 [ ]
这种方式设置 hosts
curl -i -X POST http://localhost:8001/routes/ -d 'hosts[]=example.com' -d 'hosts[]=foo-service.com'
HTTP/1.1 201 Created
...
使用通配符 hostname
Kong允许用户在 hosts
属性中使用通配符来提高灵活性,但是不允许在最左侧或最右侧都使用通配符,通配符 hosts
示例:
{
"hosts": ["*.example.com", "service.com"]
}
下面这些请求都可以匹配:
GET / HTTP/1.1
Host: an.example.com
GET / HTTP/1.1
Host: service.com
preserve_host
属性
Kong在代理过程中,默认会将上游服务中的 Host 头设置为服务中的 host
,可以增加 preserve_host
属性来调整Kong的策略
下面这个示例中,路由中没有设置 preserve_host
属性:
{
"hosts": ["service.com"],
"service": {
"id": "..."
}
}
客户端发送请求:
GET / HTTP/1.1
Host: service.com
Kong会提取服务层的 hosts
属性塞入 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:
GET / HTTP/1.1
Host: service.com
请求路径
还有一种方式是通过请求路径匹配路由,匹配条件是客户端的请求路径的前缀必须满足 paths
属性值中的一个
示例路由如下:
{
"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 stream 的时候不会更改URL路径,当路由匹配路径时,最长的路径前缀会优先匹配,比如用户可以在路由中定义两个路径:/service
和/service/resource
,并确保前者不会掩盖后者
在 paths 中使用正则表达式
Kong支持路径使用Perl形式的正则表达式,路径中可以同时包含正则表达式和前缀形式,示例路由如下:
{
"paths": ["/users/\d+/profile", "/following"]
}
下面这些请求都可以匹配:
GET /following HTTP/1.1
Host: ...
GET /users/123/profile HTTP/1.1
Host: ...
评估顺序
如上所述,Kong会根据路径的长度做评估,最长前缀路径优先匹配,同时Kong会根据 regex_priority
属性从高到低评估正则表达式的优先级,如下所示这些路由:
[
{
"paths": ["/status/\d+"],
"regex_priority": 0
},
{
"paths": ["/version/\d+/status/\d+"],
"regex_priority": 6
},
{
"paths": ["/version"],
},
{
"paths": ["/version/any/"],
}
]
这种情况下,Kong会按以下顺序评估传入的URI:
/version/any/
/version
/version/\d+/status/\d+
-
/status/\d+
前缀路径总是比正则表达式优先评估,并且一个请求还需要同时匹配hosts
和methods
,Kong会遍历用户配置的路径,找到匹配项最多的一个
捕获组
Kong的正则表达式还支持捕获组,这些从路径中提取的内容还可以在其他插件中使用,例如下面这个路由和请求路径:
/version/(?<version>\d+)/users/(?<user>\S+)
/version/1/users/john
如果请求与路由匹配,捕获组可以在插件中使用 ngx.ctx 变量获取:
local router_matches = ngx.ctx.router_matches
-- router_matches.uri_captures is:
-- { "1", "john", version = "1", user = "john" }
转义字符
curl -i -X POST http://localhost:8001/routes --data-urlencode 'uris[]=/status/\d+'
HTTP/1.1 201 Created
...
注意,curl
指令不会对自动对URL进行编码,并注意使用 --data-urlencode
,防止Kong的Admin API对 +
字符进行URL解码
strip_path
属性
有可能客户端的请求路径匹配了某条路由,但是在 upstream 中不包含这个路径,我们可以使用 strip_path
属性配置路由:
{
"paths": ["/service"],
"strip_path": true,
"service": {
"id": "..."
}
}
启用该标志位后,Kong在匹配路由会继续代理服务,然后在调用上游服务时在URL中去除匹配的部分,示例如下:
- 路由:
GET /service/path/to/resource HTTP/1.1
Host: ...
- 上游服务:
GET /path/to/resource HTTP/1.1
Host: ...
同样使用正则表达式的路由也同样支持 strip_path
属性:
{
"paths": ["/version/\d+/service"],
"strip_path": true,
"service": {
"id": "..."
}
}
路由和上游服务示例如下:
GET /version/1/service/path/to/resource HTTP/1.1
Host: ...
GET /path/to/resource HTTP/1.1
Host: ...
请求方法
methods
属性限定了请求的 Http 方法,默认值为空,示例路由可以匹配 GET
和 HEAD
方法:
{
"methods": ["GET", "HEAD"],
"service": {
"id": "..."
}
}
下面这些请求都可以匹配:
GET / HTTP/1.1
Host: ...
HEAD /resource HTTP/1.1
Host: ...
POST
和 DELETE
请求不会匹配路由,在路由上配置插件,可以更加细化服务的粒度,用户可以对同一个服务配置两条不同的路由,一个接收没有任何限制的 GET
请求,另外一个是 POST
请求,需要身份校验并且增加限流
匹配优先级
路由基于 hosts
,paths
和 methods
匹配请求,通常情况下Kong需要所有这些规则完全匹配,但是现实情况中可能会同时匹配多种路由,Kong定义了一个优先级规则来处理这种情况,这个规则就是,当评估一个请求时,Kong会选取匹配规则最多的路由,下面是两个路由示例:
{
"hosts": ["example.com"],
"service": {
"id": "..."
}
},
{
"hosts": ["example.com"],
"methods": ["POST"],
"service": {
"id": "..."
}
}
第二条路由包含 hosts
和 methods
属性,所以他将优先被评估
GET / HTTP/1.1
Host: example.com
这会匹配第一个路由
POST / HTTP/1.1
Host: example.com
这会匹配第二个路由
代理行为
上面的代理规则解释了Kong如何将外部请求转发到用户的内部服务,下面我们将详细描述Kong匹配了路由之后,如果转发到内部服务的细节
1. 负载均衡
Kong实现了对内部服务池的负载均衡,具体信息可以参考负载均衡向导
2. 插件执行
Kong可以通过插件
进行扩展,插件是一个钩子,生命周期从请求到响应,插件可以对用户环境做各种操作,也可以对代理的请求做各种转换
插件可以全局配置,也可以设置在特定的路由和服务上,所有这些都必须通过Admin API设置,当某个路由匹配时,Kong会运行关联在上面的插件,路由上的插件比服务上的插件优先运行,这些插件会运行 access
段中的内容,具体信息可以参考插件开发向导
3. 代理 & upstream 超时
当Kong执行完所有插件中包含的逻辑后,就准备把请求转发到 upstream 服务,这是通过Nginx的 ngx_http_proxy_module
模块完成的,用户可以配置下列服务属性,调整Kong和 upstream 服务之间的超时时间:
-
upstream_connect_timeout
:定义与服务建立连接的超时时间,单位是毫秒,默认值为60000
-
upstream_send_timeout
:定义连续两次向 upstream 服务发送请求的超时时间,单位是毫秒,默认值为60000
-
upstream_read_timeout
:定义连续两次向 upstream 服务读取请求的超时时间,单位是毫秒,默认值为60000
Kong会使用HTTP/1.1协议发送请求,并设置下列头文件: -
Host: <your_upstream_host>
:主机名 -
Connection: keep-alive
:允许重用 upstream 连接 -
X-Real-IP: <remote_addr>
:remote_addr 与 ngx_http_core_module 模块中定义的同名变量一致,但是这个值可能被 ngx_http_realip_module 覆盖 -
X-Forwarded-For: <address>
:待验证 -
X-Forwarded-Proto
:待验证 -
X-Forwarded-Host: <host>
:待验证 -
X-Forwarded-Port: <port>
:待验证
Kong会转发所有其他请求头,使用 WebSocket 协议时会出现一个例外,Kong会设置下列头文件: Connection: Upgrade
Upgrade: websocket
4. 错误 & 重试
每当在代理发生错误时,Kong会使用底层的Nginx重试机制将请求发送到下一个 upstream,这里有两个可配置的元素:
- 重试次数:可以使用
retries
属性配置服务的重试次数,具体信息可以参考Admin API - 错误详情:Kong使用Nginx的默认值
第二个选项基于Nginx的 proxy_next_upstream 指令,该选项不能通过Kong直接配置,需要自定义Nginx配置添加,详细信息可以参考配置向导
5. 响应
Kong接收来自 upstream service 的响应,以流的方式将其发送给下游客户端,此时添加在路由或者服务上的插件会依次执行 header_filter
段中的内容,当 header_filter
段中的内容全部执行完之后,以下这些头部文件会添加到返回给客户端的响应中:
-
Via: kong/x.x.x
:x.x.x是Kong的版本信息 -
X-Kong-Proxy-Latency: <latency>
:latency是Kong从客户端接收到请求到将请求转发到 upstream service 所经历的时间,单位为毫秒 -
X-Kong-Upstream-Latency: <latency>
:latency是Kong从 upstream service 接收到响应所等待的时间,单位为毫秒
当响应头发送给客户端后,Kong会执行插件body_filter
段中的内容,这个钩子可能会执行多次,这依赖Nginx的流特性,具体信息可以参考插件开发向导
配置 fallback 路由
Kong在实际使用中可以配置 fallback 路由增加路由的灵活性,这样可以避免返回 404
响应(找不到路由),用户可以捕获这些请求并将它们代理到一个特定的服务,或者对其使用一个特别的插件(比如可以使用一个不同的状态码返回或者中断代理该请求)
这是一个fallback路由的示例:
{
"paths": ["/"],
"service": {
"id": "..."
}
}
可以预见,所有的 Http 请求都会匹配此路由,因为所有的请求都是 /
前缀,在上几节我们已经知道了最长的URL会优先评估,所以 /
路径会被最后评估,这作为 fallback 路由相当有效,仅作为最后的处理手段,用户可以将它们路由到特殊的服务或者对其使用特别的插件
对路由配置 SSL 协议
Kong提供了对每个连接动态配置SSL协议的方法,SSL协议通过Admin API配置,由核心层直接处理,通过TLS协议连接Kong的客户端必须支持 SNI
扩展才能使用此功能,Kong Admin API通过下面两个参数处理SSL协议:
-
/certificates
:存储用户的密钥和证书 -
/snis
:与注册在SNI上的证书关联
你可以在 Admin API 向导中找到这两个参数的文档,这是如何配置SSL证书的示例,首先通过Admin API上传SSL证书和密钥:
curl -i -X POST http://localhost:8001/certificates -F "cert=@/path/to/cert.pem" -F "key=@/path/to/cert.key" -F "snis=*.ssl-example.com,other-ssl-example.com"
HTTP/1.1 201 Created
...
snis
参数是一个语法糖,插入了SNI,并关联了上传的的证书,在例子中的 SNI 数组中有一个 SNI 包含了通配符,SNI 不允许在最左侧或最右侧都使用通配符,这在维护多个子域名时非常实用,有效的通配符位置包括 mydomain.*
,*.mydomain.com
和 *.www.mydomain.com
snis
匹配的优先级顺序是:
- 无通配符
- 前缀
- 后缀
用户必须事先注册好这条路由,为了方便起见,我们仅使用 Host 做匹配条件:
curl -i -X POST http://localhost:8001/routes -d 'hosts=prefix.ssl-example.com,other-ssl-example.com' -d 'service.id=d54da06c-d69f-4910-8896-915c63c270cd'
HTTP/1.1 201 Created
...
现在这个路由可以代理Https请求
curl -i https://localhost:8443/ -H "Host: prefix.ssl-example.com"
HTTP/1.1 200 OK
...
现在如果客户发送的请求中将 prefix.ssl-example.com</font 作为 SNI 扩展,Kong在建立连接和 SSL 握手前,会提前配置好
cert.pem` 证书
限定客户端协议(HTTP/HTTPS/TCP/TLS)
路由可以配置 protocols
属性限定监听的客户端协议,这个参数接受一系列参数:http
、https
、tcp
或 tls
,路由同时支持 http
和 https
协议可以这样配置:
{
"hosts": ["..."],
"paths": ["..."],
"methods": ["..."],
"protocols": ["http", "https"],
"service": {
"id": "..."
}
}
protocols属性的默认值为 ["http", "https"]
仅使用Https协议的路由仅接收Https的流量
{
"hosts": ["..."],
"paths": ["..."],
"methods": ["..."],
"protocols": ["https"],
"service": {
"id": "..."
}
}
如果有路由匹配了这条请求,但是没有使用SSL协议,Kong会返回:
HTTP/1.1 426 Upgrade Required
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: Upgrade
Upgrade: TLS/1.2, HTTP/1.1
Server: kong/x.y.z
{"message":"Please use HTTPS protocol"}
从Kong1.0开始,可以使用原始TCP协议建立连接:
{
"hosts": ["..."],
"paths": ["..."],
"methods": ["..."],
"protocols": ["tcp"],
"service": {
"id": "..."
}
}
同样,也可以使用原始TLS协议:
{
"hosts": ["..."],
"paths": ["..."],
"methods": ["..."],
"protocols": ["tls"],
"service": {
"id": "..."
}
}
代理 WebSocket 流量
基于底层 Nginx 实现,Kong支持代理 WebSocket 流量,如果用户希望在客户端和 upstream service 之间建立连接,必须创建一个 WebSocket 握手,这是通过 Http 升级机制完成的,Kong会这样返回客户端:
GET / HTTP/1.1
Connection: Upgrade
Host: my-websocket-api.com
Upgrade: WebSocket
这样Kong会将 Connection
和 Upgrade
转发到 upstream service,而不会像标准的 Http 代理那样解除连接
WebSocket 和 TLS
Kong会在 http
和 https
协议对应的端口接收 ws
和 wss
连接,如果需要客户端强制使用TLS连接,需要将路由设置成仅支持https协议,当将服务指向 WebSocket 服务时,用户必须小心选择Kong与 upstream service 之间的协议,如果用户选择TLS(wss
),那么上游的WebSocket服务必须定义成使用 https
协议,通常使用443端口;选择不使用TLS(ws
),那么使用 http
协议,通常使用80端口
总结
通过本章指南,希望用户可以了解到Kong的代理机制