服务网格Istio中lua过滤器及测试样例

lua 过滤器

网络过滤器(L3/L4)构成 Envoy 连接处理的核心,envoy更是借助于网络过滤器下的http连接管理器(L7)实现了大量的http特定功能。

除了功能强大的http连接管理器之外,envoy还内置了诸多的http过滤器(L7),用于自定义扩展http请求代理功能,如故障注入外部授权限流等。

其中lua过滤器允许在请求和响应流期间运行 Lua 脚本,该特性为扩展自定义处理流程预留了很大的操作空间。

当前lua过滤器支持的高级特性包括:

  • 在流式传输的请求流和/或响应流中检查头部、正文和尾部。

  • 修改头部和尾部。

  • 阻塞并缓存整个请求/响应正文以进行检查。

  • 对上游主机执行出站异步 HTTP 调用。可以在缓冲正文数据的同时执行此类调用,以便在调用完成时可以修改上游头部。

  • 执行直接响应并跳过后续的过滤器迭代。例如,脚本可以向上游发起 HTTP 身份认证调用,然后直接响应 403 响应码。

配置

API引用参考官网

过滤器在envoy配置中被引用名为envoy.filters.http.lua

基于每条路由的配置全局配置

默认情况下,定义在 inline_code 中的 Lua 脚本会被认定为 GLOBAL 脚本。

 name: envoy.lua
 typed_config:
   "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua"
   inline_code: |-
     function envoy_on_request(request_handle)
       -- lua处理脚本
     end
     function envoy_on_response(response_handle)
       -- lua处理脚本
     end
   source_codes:
     hello.lua:
       inline_string: |
         function envoy_on_request(request_handle)
           request_handle:logInfo("Hello World.")
         end
     bye.lua:
       inline_string: |
         function envoy_on_response(response_handle)
           response_handle:logInfo("Bye Bye.")
         end  

过滤器类型为type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua

 {
   "inline_code": "...",
   "source_codes": "{...}"
 }
  • inline_code: 必填项,默认情况下,Envoy 将会在每个引用了lua filter的 HTTP 请求中执行inline_code中的脚本,相当于是无条件匹配

  • source_codes: source_codes中的配置则不会被默认执行,source_codes相当于以Map的方式(key为脚本名称,value为脚本内容)为后续其他地方引用lua脚本提供源头。

    source_codes中的脚本被引用时,将覆盖inline_code中的处理脚本,即执行source_codes中的脚本而不再执行inline_code

基于每条路由的配置

通过在虚拟主机路由加权集群上提供 LuaPerRoute 配置(属性typed_per_filter_config),还可以基于每条路由禁用或覆盖 Lua HTTP 过滤器。

 {
   "disabled": "...",
   "name": "...",
   "source_code": "{...}"
 }
  • disabled: 通过该属性禁用 LuaPerRoute中的lua脚本。

  • name: 引用全局配置source_codes内定义的lua脚本名称。

  • source_code: 通过RDS或者在线文本的方式提供脚本内容。

其中属性typed_per_filter_config类型为map[string][Any],即key用来匹配过滤器名,如envoy.filters.http.lua,value为Any类型用于特定的类型结构。

注意基于每条路由的配置只有在HTTP连接管理中配置了lua过滤器情况下才会得到执行,基于每条路由的配置是用来覆盖全局配置中定义的inline_code的。

流处理API

在lua语法的加持下,envoy提供了部分流处理API来支持对请求和响应进行处理。

其中最常用的两个函数为:

  • function envoy_on_request(request_handle): 处理http请求函数。

  • function envoy_on_response(response_handle): 处理http响应函数。

其参数handle提供一些常用的方法来对请求进行处理:

  • handle:headers(): 返回流的头部对象,在头部尚未被发送到头部链中的下一个过滤器,就可以对其进行修改。

  • handle:body(always_wrap_body): 返回流的正文,通过参数可指定即使正文为空也返回body对象

  • handle:bodyChunks(): 返回一个迭代器,可在所有接收到的正文块到达时用其进行迭代,返回的是缓冲对象

  • handle:trailers(): 返回流的尾部(头部对象),尾部在被发送到下一个过滤器前是可以被修改的。

  • handle:log*(message): 不同日志级别的envoy日志打印,message为字符串。

  • handle:httpCall(cluster, headers, body, timeout, asynchronous): 向上游主机发起一个 HTTP 调用,返回响应消息头和正文两个对象

    • cluster: envoy中集群管理器中已配置的集群,类型为字符串。

    • headers: 消息头,类型为map[string][string],其中value可以为字符串或字符串列表,其中消息头:method/:path/:authority必填。

    • body: 正文,类型为字符串,是可选项,可为nil。

    • timeout: 超时时间,类型为整形,单位为毫秒。

    • asynchronous: 异步调用,默认为false代表envoy暂停执行脚本直到调用完成或者发生错误,若为true则发生调用的同时envoy继续执行后续脚本或过滤器。

  • handle:respond(headers, body): 立即响应并且不再执行后续的过滤器迭代,仅在请求流中使用生效

    • headers: 消息头,类型为map[string][string],其中value可以为字符串或字符串列表,其中消息头:status状态码(字符串)必填。

    • body: 正文,类型为字符串,是可选项,可为nil。

Envoyfilter

Istio提供了envoyfilter资源来实现生成或修改原生envoy配置的功能。

在本文中,通过该资源可配置监听器及其过滤链、网络过滤器、http过滤器子项,http路由及其虚拟主机、http路由子项。

http过滤器

由Istio默认生成的http协议流量监听器中,http过滤器链配置如下:

 {
     "filter_chains": [{
      "filter_chain_match": {
       "transport_protocol": "raw_buffer",
       ......
      },
      "filters": [{
        "name": "envoy.filters.network.http_connection_manager",
        "typed_config": {
         "@type": "......",
         "rds": {......},
         "http_filters": [
          {
           "name": "istio.metadata_exchange",
            ......
           }},
          {
           "name": "istio.alpn",       
            ......},
          {
           "name": "envoy.filters.http.cors",
            ......},
          {
           "name": "envoy.filters.http.fault",
            ......},
          {
           "name": "istio.stats",
            ......},
          {
           "name": "envoy.filters.http.router",
            ......}]}}]}]}

在http连接管理器中默认配置了istio.metadata_exchange/istio.alpn/envoy.filters.http.cors/envoy.filters.http.fault/istio.stats/envoy.filters.http.router,即通过istio的一些数据、状态等搜集及跨域等配置处理,最后进行路由转发过滤器进行路由。

通过EnvoyFilter配置,我们可以指定在envoy.filters.http.router之前插入lua过滤器进行一些自定义请求处理操作:

 apiVersion: networking.istio.io/v1alpha3
 kind: EnvoyFilter
 metadata:
   name: <NAME>
   namespace: istio-system
 spec:
   configPatches:
     - applyTo: HTTP_FILTER
       match:
         context: SIDECAR_OUTBOUND
         listener:
           name: <LISTENER_NAME>
           filterChain:
             filter:
               name: "envoy.filters.network.http_connection_manager"
               subFilter:
                 name: "envoy.filters.http.router"
       patch:
         operation: INSERT_BEFORE
         value:
           name: envoy.lua
           typed_config:
             "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua"
             inline_code: "..."
             source_codes: {...}

其中,若要对请求流量进行处理,则配置应该在OUTBOUND上下文,但是在Istio或Envoy体系中只能确认流量发送目的地host,在很难确定下游具体服务情况下,EnvoyFilter资源应当配置在根命名空间,即对所有下游服务生效规则。

lua过滤器插入位置可进行自定义,一般在路由过滤器前进行相关处理即可。

此时可以通过导出envoy配置进行确认是否成功插入lua过滤器

值得一提的是,根据全局配置一节的描述,通过上文方式插入的过滤器,在满足过滤器等条件下,所有被监听的请求均会执行lua过滤器inline_code定义的lua脚本。

因此大部分场景下,都会进行特定匹配条件下的自定义请求处理。

http路由

通过上文基于每条路由的配置中描述,通过 LuaPerRoute 配置为满足特定匹配条件的虚拟主机路由加权集群上使用特定的http过滤器

本节以端口号为9090的http路由为例,在特定VirtualService/DestinationRule等istio资源应用后envoy配置如下:

 {
  "route_config": {
   "@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
   "name": "9090",
   "virtual_hosts": [
    {
     "name": "allow_any",
     ......
    },
    {
     "name": "goserver.full.svc.cluster.local:9090",
     "domains": [......],
     "routes": [
      {
       "match": {
        "path": "/healthz",
        "case_sensitive": true
       },
       "route": {
        "cluster": "outbound|9090||goserver.full.svc.cluster.local",
        "timeout": "4s",
        "retry_policy": {......},
        ......
       },
       ......
      },
      {
       "match": {
        "prefix": "/"
       },
       "route": {
        "cluster": "outbound|9090||goserver.full.svc.cluster.local",
        ......
    },
    {
     "name": "goserver.part.svc.cluster.local:9090",
     ......
 }

根据Envoy的路由规则,envoy通过HTTP消息头中的host/authority来匹配virtual_hosts.domains以选中特定的虚拟主机,在虚拟主机中再进行路由匹配等。

通过EnvoyFilter配置,我们可以指定在特性虚拟主机中首部插入lua过滤器进行一些自定义请求处理操作:

 apiVersion: networking.istio.io/v1alpha3
 kind: EnvoyFilter
 metadata:
   name: <NAME>
   namespace: istio-system
 spec:
   configPatches:
     - applyTo: HTTP_ROUTE
       match:
         context: SIDECAR_OUTBOUND
         routeConfiguration:
           name: "9090"
           vhost:
             name: goserver.full.svc.cluster.local:9090
             route:
               action: ROUTE
       patch:
         operation: INSERT_FIRST
         value:
           name: envoy.xprotocol.lua    # 自定义http过滤器名
           match:
             prefix: /
             headers:
               - name: key               # 自定义匹配条件
                 exact_match: value
           typed_per_filter_config:
             envoy.filters.http.lua:
               "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute"
               inline_string: "......"  
           route:
             cluster: "PassthroughCluster"

上文所示在虚拟主机goserver.full.svc.cluster.local:9090的首部插入了一条匹配特定消息头条件下的lua过滤器

此时可以通过导出envoy配置进行确认是否成功插入lua过滤器

值得一提的是,根据基于每条路由的配置一节的描述,只有在HTTP连接管理器中插入了lua过滤器的前提下,基于每条路由的配置中定义的lua脚本将覆盖全局配置中定义的inline_code得到执行。因此基于每条路由的配置需要结合全局配置一起使用。

自定义http请求代理规则

某些场景下,我们可能需要修改istio默认的http流量处理规则,如: http下游调用dubbo上游,由于不同协议不能直接调用,因此http下游可以代理经过协议转换器处理后间接调用dubb上游

样例测试

部署一个sleep下游客户端用于发送http流量,再部署一个上游http服务端goserver,验证两者正常运行:

 # kubectl -nfull exec -it deploy/sleep -c sleep -- sh
 $ curl -i  goserver.full:9090/healthz
 HTTP/1.1 200 OK
 date: Thu, 29 Jul 2021 05:26:17 GMT
 content-length: 58
 content-type: text/plain; charset=utf-8
 x-envoy-upstream-service-time: 29
 server: envoy
 xprotocol: false
 ​
 {"status":"healthy","hostName":"goserver-7c5cc7cf6-6mpqj"}

再部署一个模拟的协议转换器goproxy,验证其正常运行:

 # kubectl -nfull exec -it deploy/sleep -c sleep -- sh
 $ curl -i  goproxy.full:8080/healthz 
 HTTP/1.1 200 OK
 date: Thu, 29 Jul 2021 05:27:07 GMT
 content-length: 58
 content-type: text/plain; charset=utf-8
 x-envoy-upstream-service-time: 20
 server: envoy
 ​
 {"status":"healthy","hostName":"goproxy-84cb848675-zbczr"}

该协议转换器在路径/v1/proxy时会进行一个代理,去请求另一个第三方的服务(假定为dubbo服务,实际上是http服务,通过消息头addr指定地址),返回消息体为代理请求的响应状态码、消息头和消息体:

 # kubectl -nfull exec -it deploy/sleep -c sleep -- sh
 $ curl -i  goproxy.full:8080/v1/proxy -H 'addr:http://goserver-tls.https.svc.cluster.local:9090/healthz'
 HTTP/1.1 200 OK
 date: Thu, 29 Jul 2021 05:40:11 GMT
 content-length: 294
 content-type: text/plain; charset=utf-8
 x-envoy-upstream-service-time: 8
 server: envoy
 ​
 [Proxy] response from proxy http://goserver-tls.https.svc.cluster.local:9090/healthz:
 status code: 200
 headers: {"Content-Length":["63"],"Content-Type":["text/plain; charset=utf-8"],"Date":["Thu, 29 Jul 2021 05:40:11 GMT"]}
 body: {"status":"healthy","hostName":"goserver-tls-55866f8dfc-tdvtf"}

此时我们指定一些规则,如果HTTP请求消息头中携带了xprotocol: http,代表这个请求是个http协议需要经过协议转换器进行协议转换后再请求目标服务。

具体创建EnvoyFilter资源:

 apiVersion: networking.istio.io/v1alpha3
 kind: EnvoyFilter
 metadata:
   name: goproxy
   namespace: istio-system
 spec:
   configPatches:
     - applyTo: HTTP_FILTER
       match:
         context: SIDECAR_OUTBOUND
         listener:
           name: 0.0.0.0_9090
           filterChain:
             filter:
               name: "envoy.filters.network.http_connection_manager"
               subFilter:
                 name: "envoy.filters.http.router"
       patch:
         operation: INSERT_BEFORE
         value:
           name: envoy.lua
           typed_config:
             "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua"
             inline_code: |-
               function envoy_on_request(request_handle)
                 local headers = request_handle:headers()
                 request_handle:logWarn("Authority: "..headers:get(":authority"))
                 request_handle:logWarn("Method: "..headers:get(":method"))
                 request_handle:logWarn("Path: "..headers:get(":path"))
               end
 ​
               function envoy_on_response(response_handle)
                 response_handle:headers():add("xprotocol", "false")
               end
             source_codes: 
               xprotocol.lua:
                 inline_string: |-
                   function envoy_on_request(request_handle)
                     request_handle:logWarn("============in xprotocol.lua=========")
                     request_handle:logWarn("Authority: "..request_handle:headers():get(":authority"))
                     request_handle:logWarn("Method: "..request_handle:headers():get(":method"))
                     request_handle:logWarn("Path: "..request_handle:headers():get(":path"))
 ​
                     local method = request_handle:headers():get(":method")               
                     local cluster = "outbound|8080||goproxy.full.svc.cluster.local"     
                     local authority = "goproxy.full.svc.cluster.local:9090"
                     local headers, body = request_handle:httpCall(                       
                       cluster,
                       {
                         [":method"] = method,                                       
                         [":path"] = "/v1/proxy",
                         [":authority"] = authority,
                         ["addr"] = "http://goserver-tls.https.svc.cluster.local:9090"..request_handle:headers():get(":path")
                       },
                       "can be empty",
                       5000)
 ​
                     request_handle:logWarn("status code: "..headers[":status"]..". body: "..body)
 ​
                     request_handle:respond(           
                       {
                         [":status"] = headers[":status"],
                         ["xprotocol"] = "http"
                       },
                       body)
                   end
 ​
     - applyTo: HTTP_ROUTE
       match:
         context: SIDECAR_OUTBOUND
         routeConfiguration:
           name: "9090"
           vhost:
             name: goserver.full.svc.cluster.local:9090
             route:
               action: ROUTE
       patch:
         operation: INSERT_FIRST
         value:
           name: envoy.xprotocol.lua
           match:
             prefix: /
             headers:
               - name: xprotocol         # 代表源协议为http,需要经过xprotocol转换
                 exact_match: http
           typed_per_filter_config:
             envoy.filters.http.lua:
               "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute"
               name: xprotocol.lua
           route:
             cluster: "PassthroughCluster"

上述EnvoyFilter资源期望实现以下流量来处理功能:

  • 默认情况下envoy打印日志,打印请求流量的:authority/:method/:path消息头,且添加响应消息头xprotocol:false,代表未经过协议转换

  • 当请求中包含消息头xprotocol:http时,执行xprotocol.lua脚本,向协议转换器goproxy/v1/proxy路径发送一个流量,代理请求结束后直接进行响应返回,响应消息头中添加xprotocol:http代表经过了协议转换

此时再次进行测试:

 # kubectl -nfull exec -it deploy/sleep -c sleep -- sh
 $ curl -i  goserver.full:9090/healthz -H "xprotocol:http" 
 HTTP/1.1 200 OK
 xprotocol: http
 content-length: 294
 date: Thu, 29 Jul 2021 05:26:02 GMT
 server: envoy
 ​
 [Proxy] response from proxy http://goserver-tls.https.svc.cluster.local:9090/healthz:
 status code: 200
 headers: {"Content-Length":["63"],"Content-Type":["text/plain; charset=utf-8"],"Date":["Thu, 29 Jul 2021 05:26:02 GMT"]}
 body: {"status":"healthy","hostName":"goserver-tls-55866f8dfc-tdvtf"}

  • 0
    点赞
  • 3
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

a605692769

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值