最近项目中需要做一个支持多种协议(http,ws,grpc)的网关,本着不重复造轮子的想法,调研了市面上常用的网关,最后从性能、社区活跃度、稳定性、可扩展性上选中了kong。
下面是kong的核心功能proxy的学习笔记。
简介
kong会暴露很多的接口,可以归纳为3个配置:
- proxy_listen:它定义了一系列的address/port,他们用于接收公共的http、ws、grpc等请求然后把这些请求代理到你的upstream service上去。(默认8000端口)
- admin_listen:它定义了一系列的address/port,但是这些地址最好只限管理员访问,它们用于kong的配置管理。(Admin API,默认8001端口)
- stream_listen:和proxy_listen比较像,它适用于4层代理的。默认是关闭的。
术语
- client:向proxy发请求的下游用户。
- upstream service:用户请求经过kong转发到的服务。
- Service:很明显,对你的每个upstream service的抽象,服务的例子可以是数据转换微服务,账单API等。
- Route:指kong的route实体。是kong的入口点,根据定义的规则把请求路由到制定的Service。
- 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(andsnis, ifhttps)tcp:sources,destinations(andsnis, iftls)grpc:hosts,headers,paths(andsnis, ifgrpcs)
如果用户给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(andsnis, ifhttps)tcp:sources,destinations(andsnis, iftls)grpc:hosts,headers,paths(andsnis, ifgrpcs)
下面我看看一些简单的例子
如果route配置如下:
{
"hosts": ["example.com", "foo-service.com"],
"paths": ["/foo", "/bar"],
"methods": ["GET"]
}那下面的这些请求就会匹配到该route
GET /foo HTTP/1.1
Host: example.comGET /bar HTTP/1.1
Host: foo-service.comGET /foo/hello/world HTTP/1.1
Host: example.com但是下面的请求就不能匹配route
GET / HTTP/1.1
Host: example.comPOST /foo HTTP/1.1
Host: example.comGET /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: Northhostname使用通配符
为了提高灵活性,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.comkong会提取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.comkong就会保留客户端请求中的host,然后把它发给upstream,所以upstream收到的请求就如下:
GET / HTTP/1.1
Host: service.comRequest 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.comGET /service/resource?param=value HTTP/1.1
Host: example.comGET /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: ...匹配顺序
- 前缀表达式按照最长匹配原则。
- 正则表达式基于regex_priority属性,该值越大,优先级越高。
- 正则表达式优先级高于前缀表达式。
如下例:
[
{
"paths": ["/status/\d+"],
"regex_priority": 0
},
{
"paths": ["/version/\d+/status/\d+"],
"regex_priority": 6
},
{
"paths": ["/version"],
},
{
"paths": ["/version/any/"],
}
]优先级如下:
/version/\d+/status/\d+/status/\d+/version/any//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-aliveX-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: UpgradeUpgrade: websocket
4. 错误 & 重试
无论何时遇到proxy的错误,kong会使用底层的nginx的错误尝试机制,把请求转发到下一个upstream。
有两种可配置的元素:
- 重试次数:可以通过retries属性为每一个Service配置。
- 什么构成了错误:这里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.x,x.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的插件只有观测和日志的。(这个可能不满足我们的业务需要,待确认)