Envoy中的 gRPC限流服务

上一节我们大概介绍了一下Envoy中有关速率限制(限流)的一些内容,这一节我们看一下对于外部的 gRPC限流服务它又是如何工作和配置的。

在 Envoy 中对服务限流的配置除了可以在 Envoy 本身中实现外,还可以在通过外部服务实现,此时 Envoy 将通过 gRPC 协议调用外部限流服务,官方对此实现有一套现成的解决方案,主要是redis数据库+令牌桶算法实现,可参考官方 https://github.com/envoyproxy/ratelimit

本文中的限制器或限流器均是同一个意思。

Envoy 实现限流

此实现是基于令牌桶算法实现,本身比较的简单,比较适合一般的使用场景。

这里是官方提供的一个配置示例

13          http_filters:
14         - name: envoy.filters.http.local_ratelimit
15           typed_config:
16             "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
17             stat_prefix: http_local_rate_limiter
18             token_bucket:
19               max_tokens: 10000
20               tokens_per_fill: 1000
21               fill_interval: 1s
22             filter_enabled:
23               runtime_key: local_rate_limit_enabled
24               default_value:
25                 numerator: 100
26                 denominator: HUNDRED
27             filter_enforced:
28               runtime_key: local_rate_limit_enforced
29               default_value:
30                 numerator: 100
31                 denominator: HUNDRED
32             response_headers_to_add:
33             - append_action: OVERWRITE_IF_EXISTS_OR_ADD
34               header:
35                 key: x-local-rate-limit
36                 value: 'true'
37             local_rate_limit_per_downstream_connection: false

重点关注配置项 token_bucket ,这里的配置表示当前最多有 10000 个令牌可以被使用,其中令牌在使用的过程中,只要桶中不足10000 个令牌时,则会以每秒再产生 1000 个令牌的速度产生新的令牌并放入令牌桶中,这样就可以实现后期每秒 1000个请求的需求。

这种配置方法比较简单,也不需要依赖第三方组件,大部分场景下已经足够我们使用了。

gRPC限流服务

对于这种专业的限流服务,需要依赖于一些第三方组件,官方的方案主要是基于Redis数据库来实现的,当然也可以换成其它的数据库。

对于Envoy是如何与限流服务交互的其实也很好理解

  1. 当用户发送一个请求时,Envoy首先拦截到,并会通过gRPC服务调用限流服务,此时会携带一些请求标记类的信息;
  2. 当限流服务收到这个请求后,通过分析请求中的标记生成一个带有过期时间的键KEY(如果key已存在则忽略生成步骤),其值首次为0,本质上就是一个Redis中的计数器,以后每过来一个请求则累计1
  3. 限流服务对 gRPC 请求进行响应
  4. Envoy 收到限流服务响应时,根据响应类型作相应的处理,是直接允许本次请求通过,还是直接给客户端响应 429 码,表示请求过多

可以看到交互还是很简单的,其实我们最主要关注是 Envoy 与 gRPC 之间是如何协同工作的。

定义

应用程序请求是基于域(domain)和一组描述符(descriptors)的速率限制决定的,因此在 Envoy限流服务 的配置都是根据这两个概念来实现的。

Domain:域是一组速率限制的容器。 Ratelimit 服务已知的所有域必须是全局唯一的。它们作为不同团队/项目具有不冲突的速率限制配置的一种方式。

Descriptor:描述符是域拥有的键/值对列表,Ratelimit 服务使用它来选择在限制时使用的正确速率限制。描述符区分大小写。

描述符列表

每个配置都包含一个顶级描述符列表和其下可能的多个嵌套列表。格式为:

domain: <unique domain ID>
descriptors:
- key: <rule key: required>
  value: <rule value: optional>
  rate_limit: (optional block)
    name: (optional)
    replaces: (optional)
      - name: (optional)
    unit: <see below: required>
    requests_per_unit: <see below: required>
  shadow_mode: (optional)
  descriptors: (optional block)
    - ... (nested repetition of above)

描述符列表中的每个描述符都必须有一个key。它还可以选择具有一个值以启用更具体的匹配。 “rate_limit”块是可选的,如果存在则设置实际的速率限制规则。请参阅下文了解规则的定义方式。如果不存在速率限制并且没有嵌套描述符,则描述符实际上被列入白名单。否则,嵌套描述符允许更复杂的匹配和速率限制场景。

速率定义

rate_limit:
unit: <second, minute, hour, day>
requests_per_unit: <uint>

速率限流块指定匹配时将使用的实际速率限流。目前该服务支持每秒、分钟、小时和天的限制。未来可能会根据用户需求增加更多类型的限制。对于其它字段的定义请参考 https://github.com/envoyproxy/ratelimit

配置

上面我们介绍了一些与限流服务相关的概念,我们再看一下如何配置限流服务。要启用gRPC限流服务需要在Envoy端和gRPC服务端两个地方进行一些相关配置,且它们之间的配置要合理才可以,先看一下Envoy端配置

Envoy端

Envoy 配置在envoy.yaml

static_resources:
clusters:
  - name: ratelimit
    type: STRICT_DNS
    connect_timeout: 1s
    lb_policy: ROUND_ROBIN
    protocol_selection: USE_CONFIGURED_PROTOCOL
    http2_protocol_options: {}
    load_assignment:
      cluster_name: ratelimit
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 127.0.0.1
                    port_value: 8081
  - name: webserver
    connect_timeout: 1s
    type: STRICT_DNS
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: webserver
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 192.168.3.206
                    port_value: 80                      
listeners:
  - address:
      socket_address:
        address: 0.0.0.0
        port_value: 8888
    filter_chains:
      - filters:
        - name: envoy.filters.network.http_connection_manager
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
            codec_type: AUTO
            stat_prefix: ingress
            http_filters:
            - name: envoy.filters.http.ratelimit
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
                domain: mydomain
                request_type: external
                stage: 0
                rate_limited_as_resource_exhausted: true
                failure_mode_deny: false
                enable_x_ratelimit_headers: DRAFT_VERSION_03
                rate_limit_service:
                  grpc_service:
                    envoy_grpc:
                      cluster_name: ratelimit
                  transport_api_version: V3

这里我们首先通过static_resources.cluster声明了一个grpc限流服务集群 ratelimit, 监听地址为 127.0.0.1:8080

接着就是对限流的配置,这里使用了全局限流器,并使用了 HTTPHTTP Filter 过滤器扩展envoy.extensions.filters.http.ratelimit.v3.RateLimit,并指定了 domain:mydomain域,因此在限流服务端这个域必须得存在;request_type: external 表示启用外部限流服务;rate_limit_service 指定了限流服务集群为 ratelimit。到此为止我们也只是声明了一些限流服务相关的信息,那到底具体怎么使用呢?

接着我们通过为每个 Route 指定将在该标头中设置的任何值以及请求的路径传递给速率限制器服务,这里指定了根路由 /,也就是说整个域名都是有效的。

route_config:
name: route
virtual_hosts:
- name: backend
  domains: ["*"]
  routes:
  - match: { prefix: "/" }
    route:
      cluster: webserver
      rate_limits:
      - stage: 0
        actions:
        - {request_headers: {header_name: "x-ext-auth-ratelimit", descriptor_key: "ratelimitkey"}}
        - {request_headers: {header_name: ":path", descriptor_key: "path"}}

这里使用了多个 request_headers 项,此时将表达 joint key 限流服务,除了request_headers外还有其它几个字段,它必须是下面的其中一项:

有一点需要特别注意,当为多个路由指定了不同的限流配置时,其先后顺序是有一定的影响的,对于Envoy来讲,是从上到下进行服务请求,因此都是将根路由/ 放在配置的最下方,如

route_config:
name: route
virtual_hosts:
- name: backend
  domains: ["*"]
  rate_limits:
    actions:
    - generic_key:
        descriptor_value: "bar"
        descriptor_key: "bar"
  routes:
  - match:
      prefix: /header/
    route:
      cluster: webserver
      rate_limits:
      - actions: # 支持多项配置
        - generic_key:
            descriptor_value: "foo"
            descriptor_key: "foo"
  # 请求头
  - match:
      prefix: /post
    route:
      cluster: httpbin
      rate_limits:
        stage: 0
        actions:
        - header_value_match:
            descriptor_key: "request"
            descriptor_value: "post_method"
            headers:
              name: ":method"
              string_match:
                exact: "GET"
  - match:
      prefix: /anything/
    route:
      cluster: httpbin
      rate_limits:
        actions:
        - request_headers:
            descriptor_key: "ratelimitkey"
            header_name: "x-ext-ratelimit"
        - request_headers:
            descriptor_key: "ratelimitkey-2"
            header_name: "x-ext-value"
  # 域名全局限制
  - match:
      prefix: /
    route:
      cluster: webserver

限流服务端

上面是Envoy端的配置,下面我们再看看gRPC限制服务端的配置

domain: mydomain
descriptors:
- key: ratelimitkey
  descriptors:
    - key: path
      rate_limit:
        requests_per_unit: 2
        unit: second
- key: database
  value: default
  rate_limit:
    unit: second
    requests_per_unit: 500          

指定域为 mydomain 与Envoy端的一致,而 descriptiors 则表示描述符,并且描述符是支持嵌套的。

此配置表示采用 ratelimitkeypath 附带的值,并将它们构建为用于速率限制的联合密钥。

我们这里只指定了两个配置,但本文章中我们只用到了第一个配置项,看到配置还是挺简单的。

然后我们参考官方的方案,先设置一些环境变量,再启用服务

git clone https://github.com/envoyproxy/ratelimit.git
cd ratelimit
make compile


export USE_STATSD=false LOG_LEVEL=debug REDIS_SOCKET_TYPE=tcp REDIS_URL=192.168.3.58:6379 RUNTIME_ROOT=/home/sxf/workspace/ratelimit RUNTIME_SUBDIRECTORY=ratelimit

环境变量 RUNTIME_ROOT 表示 RUNTIME 根目录,而 RUNTIME_SUBDIRCTORY 表示配置文件所在的子目录,服务启用从 RUNTIME_ROOT/RUNTIME_SUBDIRECTORY/config/ 目录里查找所有 *.conf 配置文件,参考 https://github.com/envoyproxy/ratelimit#loading-configuration

这里同时指定了Redis 一些配置相关信息,并启用了Debug模式,禁用了统计功能。

# 将上面的配置内容写入 /home/sxf/workspace/ratelimit/ratelimit/config/config.yaml,然后启用服务
bin/ratelimit

如果一切正常的话,服务将输出

WARN[0000] statsd is not in use
INFO[0000] Tracing disabled
WARN[0000] connecting to redis on 192.168.3.58:6379 with pool size 10
DEBU[0000] Implicit pipelining enabled: false
DEBU[0000] loading domain: mydomain
DEBU[0000] Creating stats for key: 'mydomain.foo_foo'
DEBU[0000] loading descriptor: key=mydomain.foo_foo ratelimit={requests_per_unit=2, unit=MINUTE, unlimited=false, shadow_mode=false}
DEBU[0000] Creating stats for key: 'mydomain.bar_bar'
DEBU[0000] loading descriptor: key=mydomain.bar_bar ratelimit={requests_per_unit=1, unit=MINUTE, unlimited=false, shadow_mode=false}
DEBU[0000] Creating stats for key: 'mydomain.request_post_method'
DEBU[0000] loading descriptor: key=mydomain.request_post_method ratelimit={requests_per_unit=3, unit=MINUTE, unlimited=false, shadow_mode=false}
DEBU[0000] loading descriptor: key=mydomain.ratelimitkey_foo
DEBU[0000] Creating stats for key: 'mydomain.ratelimitkey_foo.ratelimitkey-2'
DEBU[0000] loading descriptor: key=mydomain.ratelimitkey_foo.ratelimitkey-2 ratelimit={requests_per_unit=3, unit=MINUTE, unlimited=false, shadow_mode=false}
DEBU[0000] waiting for runtime update
WARN[0000] Listening for gRPC on '0.0.0.0:8081'
WARN[0000] Listening for debug on '0.0.0.0:6070'
WARN[0000] Listening for HTTP on '0.0.0.0:8080'

最终启用了三个端口

:8081 gRPC服务端口,与Envoy通讯使用

:6070 golang中 pprof 性能分析,https://github.com/envoyproxy/ratelimit#debug-port

:8080 查看交互端点和服务健康检查,https://github.com/envoyproxy/ratelimit#http-port

这里只是简单介绍了其用法,更多配置信息可查看官方网站

到此,两边的配置都基本完成了,我们可以将Envoy服务启用,并用压力测试工具访问url,会发现限流服务正在发挥作用。

参考资料

Envoy 中的速率限制 ratelimit

在 Envoy 架构中 Rate limit service 共支持 global rate limitinglocal rate limit filter 两种速率限制。推荐使用 https://github.com/envoyproxy/ratelimit 库。

Global rate limiting

Envoy 提供了两种全局限速实现

  1. 每个连接 或 每个HTTP请求 速率限制检查。
  2. 基于配额,具有定期负载报告,允许在多个 Envoy 实例之间公平共享全局速率限制。此实现适用于每秒请求负载较高的大型 Envoy 部署,这些负载可能无法在所有 Envoy 实例之间均匀平衡。

Per connection or per HTTP request rate limiting

Envoy 直接与全局 gRPC rate limiting service 集成,配置参考 https://www.envoyproxy.io/docs/envoy/latest/configuration/other_features/rate_limit#config-rate-limit-service

速率服务可以使用任何 RPC/IDL 协议实现,但Envoy 提供了一个用 Go 编写的参考实现,它使用 Redis 作为后端。速率集成有以下两种特征:

  1. Network 级别过滤器:对应 Per connection ,Envoy 将为安装过滤器的侦听器上的每个 新连接 调用速率限制服务。该配置指定一个特定的域名和描述符设置为速率限制。这具有限制每秒传输侦听器的连接速率的最终效果。配置参考 https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/network_filters/rate_limit_filter#config-network-filters-rate-limit
  1. HTTP 级别过滤器: 对应 Per HTTP request,Envoy 将为安装过滤器的 Listener 以及路由表指定应调用全局速率限制服务的侦听器上的每个 新请求 调用速率限制服务。对目标上游集群的所有请求以及从原始集群到目标集群的所有请求都可以进行速率限制。配置参考 https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/rate_limit_filter#config-http-filters-rate-limit

Envoy 还支持本地速率限制 local rate limiting,本地速率限制可以与全局速率限制结合使用,以减少全局速率限制服务的负载。例如,本地令牌桶速率限制可以吸收非常大的负载突发,否则可能会压倒全局速率限制服务。因此,速率限制分两个阶段应用,在细粒度全局限制完成作业之前,由令牌桶限制执行初始粗粒度限制。

Quota based rate limiting

速率限制服务的开源参考实现目前不可用。费率限制配额扩展目前可以与Google Cloud费率限制服务一起使用。

基于配额的全局速率限制只能应用于 HTTP 请求。 Envoy 将使用 HTTP 过滤器配置对请求进行分桶,并从速率限制配额服务请求配额分配。配置参考 https://www.envoyproxy.io/docs/envoy/latest/configuration/other_features/rate_limit#config-rate-limit-quota-service

Local rate limiting

Envoy 除了支持通过 local rate limit filter 过滤器对 L4 连接进行本地(非分布式)速率限制。同时还支持通过 HTTP local rate limit filter 对 HTTP 请求进行本地速率限制。这可以在 侦听器级别或更具体的级别(例如: virtual hostroute level)全局激活。

也就是说对于 local rate limit 可以在两个地方对其进行配置,一个是 Listener 中的 Network filter ,另一个是 HTTP 中的 HTTP Filters,注意配置在这两点的不同之处。

一般本地速率限制与全局速率限制结合使用,以减少全局速率限制服务的负载。

当请求的路由或虚拟主机具有每个过滤器本地速率限制配置时,HTTP 本地速率限制过滤器应用令牌桶速率限制。

如果检查了本地速率限制令牌桶,并且没有可用令牌,则返回 429 响应(响应是可配置的)。本地速率限制过滤器然后设置 x-envoy-ratelimited 响应标头。可以配置要返回的其他响应标头

根据配置项 local_rate_limit_per_downstream_connection 的值,令牌桶在所有 workers 之间共享或在 a per connection 的基础上共享。这导致每个 Envoy 进程或每个下游连接应用本地速率限制。默认情况下,速率限制适用于每个 Envoy 进程。

配置示例

以下配置来自官方提供的示例文件 https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/local_rate_limit_filter#example-configuration

  1. 全局设置速率限制器的示例过滤器配置(例如:所有虚拟主机/路由共享相同的令牌桶)下载
13          http_filters:
14         - name: envoy.filters.http.local_ratelimit
15           typed_config:
16             "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
17             stat_prefix: http_local_rate_limiter
18             token_bucket:
19               max_tokens: 10000
20               tokens_per_fill: 1000
21               fill_interval: 1s
22             filter_enabled:
23               runtime_key: local_rate_limit_enabled
24               default_value:
25                 numerator: 100
26                 denominator: HUNDRED
27             filter_enforced:
28               runtime_key: local_rate_limit_enforced
29               default_value:
30                 numerator: 100
31                 denominator: HUNDRED
32             response_headers_to_add:
33             - append_action: OVERWRITE_IF_EXISTS_OR_ADD
34               header:
35                 key: x-local-rate-limit
36                 value: 'true'
37             local_rate_limit_per_downstream_connection: false

这里 rate limit 工作在HTTP 中的 HTTP Filters 过滤器,使用扩展 extensions.filters.http.local_ratelimit.v3.LocalRateLimit

token_bucket: 是令牌桶的配置(参考,这里的意思是说令牌桶共 10000 个,每 token_bucket.fill_interval 个周期定时向桶中填充 token_bucket.tokens_per_fill 个令牌。

filter_enable 启用状态。

  1. 全局禁用速率限制器但为特定路由启用的示例过滤器配置 下载
13          http_filters:
14         - name: envoy.filters.http.local_ratelimit
15           typed_config:
16             "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
17             stat_prefix: http_local_rate_limiter
  1. 路由具体配置 下载:
21          route_config:
22           name: local_route
23           virtual_hosts:
24           - name: local_service
25             domains: ["*"]
26             routes:
27             - match: {prefix: "/path/with/rate/limit"}
28               route: {cluster: service_protected_by_rate_limit}
29               typed_per_filter_config:
30                 envoy.filters.http.local_ratelimit:
31                   "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
32                   stat_prefix: http_local_rate_limiter
33                   token_bucket:
34                     max_tokens: 10000
35                     tokens_per_fill: 1000
36                     fill_interval: 1s
37                   filter_enabled:
38                     runtime_key: local_rate_limit_enabled
39                     default_value:
40                       numerator: 100
41                       denominator: HUNDRED
42                   filter_enforced:
43                     runtime_key: local_rate_limit_enforced
44                     default_value:
45                       numerator: 100
46                       denominator: HUNDRED
47                   response_headers_to_add:
48                   - append_action: OVERWRITE_IF_EXISTS_OR_ADD
49                     header:
50                       key: x-local-rate-limit
51                       value: 'true'
52             - match: {prefix: "/"}
53               route: {cluster: default_service}

这里一共配置了两个路由,对其中的一个路由 “/path/with/rate/limit” 进行了限制,第二个路由不做任何限制。

请注意,如果此过滤器配置为全局禁用并且没有虚拟主机或路由级别令牌桶,则不会应用任何速率限制。

总结

  1. 对于 global rate limitlocal rate limit 两者即可以工作在 Listeners 中的 Network filters,也可以工作在 HTTP 中的 HTTP filters 中。
  2. 通常情况下 local rate limitglobal rate limit 两者配合使用,优先使用 local rate limit 服务,以减少对 global rate limit 服务的负载。
  3. 对于 local rate limit 主要是基于令牌桶限制算法,见 https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/local_ratelimit/v3/local_rate_limit.proto#envoy-v3-api-field-extensions-filters-http-local-ratelimit-v3-localratelimit-local-rate-limit-per-downstream-connection

参考

了解eBPF技术

eBPF 的全称“扩展的伯克利数据包过滤器 (Extended Berkeley Packet Filter)” 来看,它是一种数据包过滤技术,是从 BPF (Berkeley Packet Filter) 技术扩展而来的。

BPF 提供了一种在 内核事件 和 用户程序 事件发生时安全注入代码的机制,这就让非内核开发人员也可以对内核进行控制。随着内核的发展,BPF 逐步从最初的数据包过滤扩展到了网络、内核、安全、跟踪等,而且它的功能特性还在快速发展中,这种扩展后的 BPF 被简称为 eBPF(早期的 BPF 被称为经典 BPF,简称 cBPF)。实际上,现代内核所运行的都是 eBPF,如果没有特殊说明,内核和开源社区中提到的 BPF 等同于 eBPF 。

使用场景及分类

根据 eBPF 的功能和使用场景,主要分类三类:

跟踪

从内核和程序的运行状态中提取跟踪信息,来了解当前系统正在发生什么。

跟踪类 eBPF 程序主要用于从系统中提取跟踪信息,进而为监控、排错、性能优化等提供数据支撑。

其中 BCC 工具集中包含的绝大部分工具也都属于这个类型。

网络

对网络数据包进行过滤和处理,以便了解和控制网络数据包的收发过程。

网络类 eBPF 程序主要用于对网络数据包进行过滤和处理,进而实现网络的观测、过滤、流量控制以及性能优化等各种丰富的功能。根据事件触发位置的不同,网络类 eBPF 程序又可以分为 XDP(eXpress Data Path,高速数据路径)程序、TC(Traffic Control,流量控制)程序、套接字程序以及 cgroup 程序。

如著名的开源项目 cilium ,主要用到了ebpf 中的 XDP (eXpress Data Path,高速数据路径)技术。

安全

第三类是除跟踪和网络之外的其他类型,包括安全控制、BPF 扩展等等。

插桩技术

eBPF支持多种事件源,可以在整个软件栈中提供能见度,其实现目前主要通过两种技术,分别为 动态插桩静态插桩,有时候只用其中一种方式是无法实现我们的需求,这时就需要两者相互配合使用了。

动态插桩: kprobes 和 uprobes

动态插桩方式可以做到在程序运行期间,动态的插入观察点,而软件的运行不会受到任何影响,在这点做到了零开销。

kprobes 一般指内核态级别的函数插桩,而 uprobes 则指用户态级别的函数插桩。

动态插桩技术一般需要在 内核函数应用函数开始位置结束位置进行插桩。例如

#!/usr/local/bin/bpftrace

// this program times vfs_read()

kprobe:vfs_read
{
@start[tid] = nsecs;
}

kretbrobe:vfs_read
/@start[tid]/
{
$duration_us = (nsecs - @start[tid]) / 1000;
@us = hist($duration_us);
delete(@start[tid]));
}

这里kprobe:vfs_read 表示是内核函数bfs_read的起始插桩,而kretbroke:vfs_read则是函数的结果插桩,通过对这两个地方分别插桩就可以计算出当前函数的执行时间。

被插桩的函数在整个软件栈中有成千上万个,所以我们可以在任意关注的地方进行插桩,并编写自己的代码实现想要的功能。

动态插桩技术有一点不好的地方就是随着软件版本的迭代变更,被插桩的函数有可能会被重命名或者被移除,这时候会导致一些BPF工具没有办法直接使用,要想继续使用这些BPF工具只能跟着将其进行调整,这个问题十分令人头疼;除此之外还有一个问题就是编译器可能会将一些函数进行 inline 化,这时候会导致这些函数无法使用 kprobesuprobes 动态插桩。

静态插桩: tracepoint 和 USDT

静态插桩会将一些稳定的事件名字编码到软件代码中,由开发者自行维护。BPF跟踪工具支持内核的静态插桩技术,也支持用户态的静态定义跟踪插桩 USDT(user level statically defined tracing)。

不过静态插桩技术也存在一定的问题,那就是会增加开发者的维护成本,同时静态插桩点数量一般很有限。

所有如果我们要开发 BPF工具的话,一般都推荐优先使用静态插桩技术,如果仍无法满足需求的话,再考虑使用动态插桩技(kprobes 或 uprobes)

开发工具

直接通过BPF指令编写BPF程序是件非常繁琐的一件事,因此出现了一些高级语言支持的BPF前端工具,主流开发工具主要是 BCCbpftrade

bcc、bpftrace 与bpf
bcc、bpftrace 与bpf

BCC

BBC(BPF编译器集合,BPF Compiler Collection) 是最早的开发BPF的开发框架。它提供了一个编写内核BPF程序的C语言环境,同时还提供了其它高级语言如Python、Lua 或 C++环境来实现用户端接口,它也是 libbcclibbpf 库的前身,这两个库提供了使用BPF程序对事件进行观测的库函数。

用户可以直接在系统上 安装BCC 即可,不用手动亲自编码代码就可以直接使用自带的一些工具,这些命令的使用场景及用法请参考官方文档 https://github.com/iovisor/bcc/blob/master/docs/tutorial.md

sudo apt-get install bpfcc-tools linux-headers-$(uname -r)

工具集将安装在 /sbin (/usr/sbin in Ubuntu 18.04) 目录,他们的扩展名为 -bpfcc .

$ ls /sbin | grep bpfcc
argdist-bpfcc
bashreadline-bpfcc
bindsnoop-bpfcc
biolatency-bpfcc
biolatpcts-bpfcc
biosnoop-bpfcc
biotop-bpfcc
bitesize-bpfcc
bpflist-bpfcc
btrfsdist-bpfcc
btrfsslower-bpfcc
cachestat-bpfcc
cachetop-bpfcc
capable-bpfcc
cobjnew-bpfcc
compactsnoop-bpfcc
cpudist-bpfcc
cpuunclaimed-bpfcc
criticalstat-bpfcc
dbslower-bpfcc
dbstat-bpfcc
dcsnoop-bpfcc
dcstat-bpfcc
deadlock-bpfcc
dirtop-bpfcc
drsnoop-bpfcc
execsnoop-bpfcc
exitsnoop-bpfcc
ext4dist-bpfcc
ext4slower-bpfcc
filelife-bpfcc
fileslower-bpfcc
filetop-bpfcc
funccount-bpfcc
funcinterval-bpfcc
funclatency-bpfcc
funcslower-bpfcc
gethostlatency-bpfcc
hardirqs-bpfcc
inject-bpfcc
javacalls-bpfcc
javaflow-bpfcc
javagc-bpfcc
javaobjnew-bpfcc
javastat-bpfcc
javathreads-bpfcc
killsnoop-bpfcc
klockstat-bpfcc
llcstat-bpfcc
mdflush-bpfcc
memleak-bpfcc
mountsnoop-bpfcc
mysqld_qslower-bpfcc
netqtop-bpfcc
nfsdist-bpfcc
nfsslower-bpfcc
nodegc-bpfcc
nodestat-bpfcc
offcputime-bpfcc
offwaketime-bpfcc
oomkill-bpfcc
opensnoop-bpfcc
perlcalls-bpfcc
perlflow-bpfcc
perlstat-bpfcc
phpcalls-bpfcc
phpflow-bpfcc
phpstat-bpfcc
pidpersec-bpfcc
profile-bpfcc
pythoncalls-bpfcc
pythonflow-bpfcc
pythongc-bpfcc
pythonstat-bpfcc
readahead-bpfcc
reset-trace-bpfcc
rubycalls-bpfcc
rubyflow-bpfcc
rubygc-bpfcc
rubyobjnew-bpfcc
rubystat-bpfcc
runqlat-bpfcc
runqlen-bpfcc
runqslower-bpfcc
shmsnoop-bpfcc
slabratetop-bpfcc
sofdsnoop-bpfcc
softirqs-bpfcc
solisten-bpfcc
sslsniff-bpfcc
stackcount-bpfcc
statsnoop-bpfcc
swapin-bpfcc
syncsnoop-bpfcc
syscount-bpfcc
tclcalls-bpfcc
tclflow-bpfcc
tclobjnew-bpfcc
tclstat-bpfcc
tcpaccept-bpfcc
tcpconnect-bpfcc
tcpconnlat-bpfcc
tcpdrop-bpfcc
tcplife-bpfcc
tcpretrans-bpfcc
tcprtt-bpfcc
tcpstates-bpfcc
tcpsubnet-bpfcc
tcpsynbl-bpfcc
tcptop-bpfcc
tcptracer-bpfcc
threadsnoop-bpfcc
tplist-bpfcc
trace-bpfcc
ttysnoop-bpfcc
vfscount-bpfcc
vfsstat-bpfcc
wakeuptime-bpfcc
xfsdist-bpfcc
xfsslower-bpfcc
zfsdist-bpfcc
zfsslower-bpfcc

目前大概有120多个命令,可以直接执行这些命令

sudo opensnoop-bpfcc

相关文档

开发教程 https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md
参考指南 https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md

如果要用 BCC 开发其它程序的话,一般是通过  TRACEPOINT_PROBE(category, event)  来定义一个跟踪点处理函数。

对我们要跟踪的短时进程问题来说,也就是下面这两个跟踪点:

// 定义sys_enter_execve跟踪点处理函数.
TRACEPOINT_PROBE(syscalls, sys_enter_execve)
{
    //待添加处理逻辑
}

// 定义sys_exit_execve跟踪点处理函数.
TRACEPOINT_PROBE(syscalls, sys_exit_execve)
{
    //待添加处理逻辑
}

其中 syscalls 是分类,而 sys_enter_execve 是跟踪点。它其实就是下面即将介绍的 bpftrace 中的 tracepoint:syscalls:sys_enter_execve 跟踪点。

bpftrace

bpftrace是一个新兴的前端,其源代码非常简洁,它同样也是基于 libbcclibbpf 库构建的。

bpftrace 原理

bpftrace 会把你开发的脚本借助 BCC 编译加载到内核中执行,再通过 BPF 映射获取执行的结果。

对于 bpftrace 安装很简单

# Ubuntu 19.04
sudo apt-get install -y bpftrace

# RHEL8/CentOS8
sudo dnf install -y bpftrace

安装好 bpftrace 之后,你就可以执行  bpftrace -l  来查询 内核插桩跟踪点

# 查询所有 内核插桩 和 跟踪点
sudo bpftrace -l

# 使用通配符查询所有的系统调用 跟踪点
sudo bpftrace -l 'tracepoint:syscalls:*'

# 使用通配符查询所有名字包含"execve"的跟踪点
sudo bpftrace -l '*execve*'

对于 跟踪点 来说,你还可以加上  -v  参数查询函数的入口参数或返回值。

# 查询execve入口参数格式
$ sudo bpftrace -lv tracepoint:syscalls:sys_enter_execve
tracepoint:syscalls:sys_enter_execve
    int __syscall_nr
    const char * filename
    const char *const * argv
    const char *const * envp

# 查询execve返回值格式
$ sudo bpftrace -lv tracepoint:syscalls:sys_exit_execve
tracepoint:syscalls:sys_exit_execve
    int __syscall_nr
    long ret

而由于内核函数属于不稳定的 API,在 bpftrace 中只能通过  arg0、arg1  这样的参数来访问,具体的参数格式还需要参考内核源代码( https://www.kernel.org/ ),注意内核的版本号差异。

相关文档

bpftrace一行教程 https://github.com/iovisor/bpftrace/blob/master/docs/developers.md
参考指南 https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md

对比

BCC 与 bpftrace 两者具有互补性,bpftrace在编写功能强大的单行程序或短小的脚本方便十分理解;BCC则更适合开发大型复杂的脚本和作为后台进程使用,同时它还可以调用其它的库。比如目前很多用Python来开发BCC程序,它们使用Python的 argparse 库来提供复杂的命令行参数运行。

BCC 和 bpftrace 它们并不属于内核代码仓库的项目,而是托管在 Github 上一个名为 IO Visor的Linux基金会。

推荐阅读:https://www.brendangregg.com/ebpf.html

参考资料

k8s解决证书过期问题

在k8s中的时间会提示证书过期问题,如

# kubectl get nodes
Unable to connect to the server: x509: certificate has expired or is not yet valid

这里我们介绍一下续期方法。

注意:当前集群通过 kubeadm 命令创建。

kubeadm 安装得证书默认为 1 年,注意原证书文件必须保留在服务器上才能做延期操作,否则就会重新生成,集群可能无法恢复

准备

这里先查看一下测试集群的证书过期时间

# kubeadm certs check-expiration
[check-expiration] Reading configuration from the cluster...
[check-expiration] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'

CERTIFICATE                EXPIRES                  RESIDUAL TIME   CERTIFICATE AUTHORITY   EXTERNALLY MANAGED
admin.conf                 Aug 30, 2022 03:18 UTC   324d                                    no      
apiserver                  Aug 30, 2022 03:18 UTC   324d            ca                      no      
apiserver-etcd-client      Aug 30, 2022 03:18 UTC   324d            etcd-ca                 no      
apiserver-kubelet-client   Aug 30, 2022 03:18 UTC   324d            ca                      no      
controller-manager.conf    Aug 30, 2022 03:18 UTC   324d                                    no      
etcd-healthcheck-client    Aug 30, 2022 03:18 UTC   324d            etcd-ca                 no      
etcd-peer                  Aug 30, 2022 03:18 UTC   324d            etcd-ca                 no      
etcd-server                Aug 30, 2022 03:18 UTC   324d            etcd-ca                 no      
front-proxy-client         Aug 30, 2022 03:18 UTC   324d            front-proxy-ca          no      
scheduler.conf             Aug 30, 2022 03:18 UTC   324d                                    no      

CERTIFICATE AUTHORITY   EXPIRES                  RESIDUAL TIME   EXTERNALLY MANAGED
ca                      Aug 28, 2031 03:18 UTC   9y              no      
etcd-ca                 Aug 28, 2031 03:18 UTC   9y              no      
front-proxy-ca          Aug 28, 2031 03:18 UTC   9y              no  

可以看到过期时间为 2022-08-30。

Continue reading

istio在虚拟机vm下的安装方法

建议参考官方文档 https://istio.io/latest/zh/docs/setup/install/virtual-machine/ ,这里提醒大家对于命令中文版部分命令与英文版不一致,请以 英文版 为准。

对于istio在vm上的安装教程主要分为三部分。首先是在k8s的master节点生成vm连接主节点的一些配置信息,其实是在vm上应用这些配置信息,最后也就是验证连接是否成功。

本篇主要介绍“单网络”的情况, 对于”多网络“请自行参考官方文档。

vm环境准备

生成vm通讯配置信息

这里主要介绍一些新手迷惑的部分。如环境变量设置及vm注册的方式

设置环境变量

在设置变量时,对于”单网络“来讲 CLUSTER_NETWORK 和 VM_NETWORK 保留空值即可。如我这里设置如下

$ VM_APP="myapp"
$ VM_NAMESPACE="vm"
$ WORK_DIR="/root/myapp"
$ SERVICE_ACCOUNT="vm-sa"
$ CLUSTER_NETWORK=""
$ VM_NETWORK=""
$ CLUSTER="Kubernetes"

每个环境变量的解释:
VM_APP 表示vm上应用的名称
VM_NAMESPACE 表示应用所在的namespace
WORK_DIR 生成vm配置信息保留的目录,任何位置即可
SERVICE_ACCOUNT 服务运行的账号 ,即yaml文件中的 ServiceAccount 字段
CLUSTER 集群名称,默认为 Kubernetes 即可。

Continue reading

利用 docker buildx 构建多平台镜像

什么是 docker buildx

Docker Buildx是一个CLI插件,它扩展了Docker命令,完全支持Moby BuildKit builder toolkit提供的功能。它提供了与docker build相同的用户体验,并提供了许多新功能,如创建作用域生成器实例和针对多个节点并发构建。

Docker Buildx包含在Docker 19.03中,并与以下Docker Desktop版本捆绑在一起。请注意,必须启用“实验特性”选项才能使用Docker Buildx。

Docker Desktop Enterprise version 2.1.0
Docker Desktop Edge version 2.0.4.0 or higher

创建 builder 实例

由于 Docker 默认的 builder 实例不支持同时指定多个 –platform,所有我们必须先创建一个 builder 实例

$ docker buildx create --use --name=mybuilder-cn --driver docker-container

--use 表示使用当前创建的 builder 实例
--name 实例名称
--driver 实例驱动(docker、 docker-container 和 kubernetes)

更多用法通过命令 docker buildx create -h 查看

Continue reading

服务网格Istio之服务入口 ServiceEntry

使用服务入口(Service Entry) 来添加一个服务入口到 Istio 内部维护的服务注册中心。添加了服务入口后,Envoy 代理可以向服务发送流量,就好像它是网格内部的服务一样,可参考 https://istio.io/latest/zh/docs/concepts/traffic-management/#service-entries

简单的理解就是允许内网向外网服务发送流量请求,但你可能会说正常情况下在pod里也是可以访问外网的,这两者有什么区别呢?

确实默认情况下,Istio 配置 Envoy 代理可以将请求传递给外部服务。但是无法使用 Istio 的特性来控制没有在网格中注册的目标流量。这也正是 ServiceEntry 真正发挥的作用,通过配置服务入口允许您管理运行在网格外的服务的流量。

此外,可以配置虚拟服务和目标规则,以更精细的方式控制到服务条目的流量,就像为网格中的其他任何服务配置流量一样。

为了更好的理解这一块的内容,我们先看一下普通POD发送请求的流程图

普通 Pod 请求

Continue reading

在linux下安装Kubernetes

环境 ubuntu18.04 64位

为了解决国内访问一些国外网站慢的问题,本文使用了国内阿里云的镜像。

更换apt包源

这里使用aliyun镜像 https://developer.aliyun.com/mirror/, 为了安全起见,建议备份原来系统默认的 /etc/apt/sources.list 文件

编辑文件 /etc/apt/sources.list,将默认网址 http://archive.ubuntu.com 或 http://cn.archive.ubuntu.com 替换为 http://mirrors.aliyun.com

更新缓存

$ sudo apt-get clean all
$ sudo apt-get update

安装Docker

参考官方文档 https://docs.docker.com/engine/install/ubuntu/ 或 aliyun 文档 https://developer.aliyun.com/mirror/docker-ce

Continue reading

初识kubernetes

对于一个刚刚接触kubernetes(k8s)的新手来说,想好更好的学习它,首先就要对它有一个大概的认知,所以本文我们先以全局观来介绍一个kubernetes。

kubernetes架构

8ee9f2fa987eccb490cfaa91c6484f67
kubernetes 架构图

kubernets整体可以分为两大部分,分别为 MasterNode ,我们一般称其为节点,这两种角色分别对应着控制节点和计算节点,根据我们的经验可以清楚的知道 Master 是控制节点。

Master 节点

控制节点 Master 节点由三部分组成,分别为 Controller ManagerAPI ServerScheduler ,它们相互紧密协作,每个部分负责不同的工作职责。

  • controller-manager 全称为 kube-controler-manager 组件,主要用来负责容器编排。如一个容器(实际上是pod,pod是最基本的调度单元。一般一个pod里会部署一个容器服务)服务可以指定副本数量,如果实际运行的副本数据与期望的不一致,则会自动再启动几个容器副本,最终实现期望的数量。这个组件,就是一系列控制器的集合。我们可以查看一下 Kubernetes 项目的 pkg/controller 目录, 伪代码如下:
for {
  实际状态 := 获取集群中对象X的实际状态(Actual State)
  期望状态 := 获取集群中对象X的期望状态(Desired State)
  if 实际状态 == 期望状态{
    什么都不做
  } else {
    执行编排动作,将实际状态调整为期望状态
  }
}
  • api server 对外提供api服务,用来接收命令进行集群管理。对内负责与etcd注册中心进行通讯,进行一些配置信息的存储与读取
  • scheduler 负责调度。如一个容器存放到k8s集群中的哪个node节点最为合适

实际上这三个节点的功能远远多于我们描述的。

Continue reading