Fork me on GitHub

2022年4月

Envoy 学习笔记

Envoy 是一款专为大型的 SOA 架构(面向服务架构,service oriented architectures)设计的 L7 代理和通信总线,它的诞生源于以下理念:

对应用程序而言,网络应该是透明的。当网络和应用程序出现故障时,应该能够很容易确定问题的根源。

要实现上面的目标是非常困难的,为了做到这一点,Envoy 提供了以下特性:

  • 进程外架构

Envoy 是一个独立的进程,伴随着每个应用程序运行。所有的 Envoy 形成一个透明的通信网络,每个应用程序发送消息到本地主机或从本地主机接收消息,不需要知道网络拓扑。进程外架构的好处是与应用程序的语言无关,Envoy 可以和任意语言的应用程序一起工作,另外,Envoy 的部署和升级也非常方便。

这种模式也被称为 边车模式(Sidecar)

  • L3/L4 过滤器架构

Envoy 是一个 L3/L4 网络代理,通过插件化的 过滤器链(filter chain) 机制处理各种 TCP/UDP 代理任务,支持 TCP 代理,UDP 代理,TLS 证书认证,Redis 协议,MongoDB 协议,Postgres 协议等。

  • HTTP L7 过滤器架构

Envoy 不仅支持 L3/L4 代理,也支持 HTTP L7 代理,通过 HTTP 连接管理子系统(HTTP connection management subsystem) 可以实现诸如缓存、限流、路由等代理任务。

  • 支持 HTTP/2

在 HTTP 模式下,Envoy 同时支持 HTTP/1.1 和 HTTP/2。在 service to service 配置中,官方也推荐使用 HTTP/2 协议。

  • 支持 HTTP/3(alpha)

从 1.19.0 版本开始,Envoy 支持 HTTP/3。

  • HTTP L7 路由

Envoy 可以根据请求的路径(path)、认证信息(authority)、Content Type、运行时参数等来配置路由和重定向。这在 Envoy 作为前端代理或边缘代理时非常有用。

  • 支持 gRPC

gRPC 是 Google 基于 HTTP/2 开发的一个 RPC 框架。Envoy 完美的支持 HTTP/2,也可以很方便的支持 gRPC。

  • 服务发现和动态配置

Envoy 可以通过一套动态配置 API 来进行中心化管理,这套 API 被称为 xDS:EDS(Endpoint Discovery Service)、CDS(Cluster Discovery Service)、RDS(Route Discovery Service)、VHDS(Virtual Host Discovery Service)、LDS(Listener Discovery Service)、SDS(Secret Discovery Service)等等。

  • 健康状态检查

Envoy 通过对上游服务集群进行健康状态检查,并根据服务发现和健康检查的结果来决定负载均衡的目标。

  • 高级负载均衡

Envoy 支持很多高级负载均衡功能,比如:自动重试、熔断、全局限流、流量跟踪(request shadowing)、异常检测(outlier detection)等。

  • 支持前端代理和边缘代理

Envoy 一般有三种部署方式:

  1. Front Proxy:前端代理,也叫边缘代理,通常是部署在整个服务网格的边缘,用于接收来自于服务网格外的请求;
  2. Ingress Listener:服务代理,通常部署在服务网格内服务的前面,用于接收发给该服务的请求,并转发给该服务;
  3. Egress Listener:与 Ingress Listener 相反,用于代理服务发出的所有请求,并将请求转发给其他服务(可能是网格内服务,也可能是网格外服务)。

envoy-deployment.png

  • 可观测性

Envoy 的主要目标是使网络透明,可以生成许多流量方面的统计数据,这是其它代理软件很难取代的地方,内置 stats 模块,可以集成诸如 prometheus/statsd 等监控方案。还可以集成分布式追踪系统,对请求进行追踪。

Envoy 整体架构与基本概念

下图是 Envoy 代理的整体架构图:图片来源

envoy-architecture.png

Envoy 接收到请求后,会经过过滤器链(filter chain),通过 L3/L4 或 L7 的过滤器对请求进行微处理,然后路由到指定集群,并通过负载均衡获取一个目标地址,最后再转发出去。这个过程中的每一个环节,可以静态配置,也可以通过 xDS 动态配置。

  • Downstream:即客户端(Client),向 Envoy 发起请求的终端。
  • Upstream:后端服务器,处理客户端请求的服务。
  • Listener:监听器,它的作用就是打开一个监听端口,用于接收来自 Downstream 的请求。
  • Cluster:一组逻辑上相似的上游主机组成一个集群。
  • Route:用于将请求路由到不同的集群。
  • xDS:各种服务发现 API 的统称,如:CDS、EDS、LDS、RDS 和 SDS 等。

安装和运行 Envoy

安装 Envoy 最简单的方式是使用官方的 Docker 镜像,首先获取镜像:

[root@localhost ~]# docker pull envoyproxy/envoy:v1.22-latest

使用 docker run 运行:

[root@localhost ~]# docker run -d -p 10000:10000 -p 9901:9901 envoyproxy/envoy:v1.22-latest

此时使用的是 Envoy 的默认配置文件,默认会监听两个端口,9901 为 Envoy 的管理端口,10000 为 Envoy 监听的代理端口,后端地址为 Envoy 官网:www.envoyproxy.io

我们进入容器,查看 Envoy 配置文件如下:

root@localhost:/# cat /etc/envoy/envoy.yaml 
admin:
  address:
    socket_address:
      protocol: TCP
      address: 0.0.0.0
      port_value: 9901
static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address:
        protocol: TCP
        address: 0.0.0.0
        port_value: 10000
    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
          scheme_header_transformation:
            scheme_to_overwrite: https
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match:
                  prefix: "/"
                route:
                  host_rewrite_literal: www.envoyproxy.io
                  cluster: service_envoyproxy_io
          http_filters:
          - name: envoy.filters.http.router
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
  clusters:
  - name: service_envoyproxy_io
    connect_timeout: 30s
    type: LOGICAL_DNS
    # Comment out the following line to test on v6 networks
    dns_lookup_family: V4_ONLY
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: service_envoyproxy_io
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: www.envoyproxy.io
                port_value: 443
    transport_socket:
      name: envoy.transport_sockets.tls
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
        sni: www.envoyproxy.io

我们打开浏览器,访问 http://127.0.0.1:10000 就可以看到 Envoy 的首页了。

如果要使用自己的配置文件,可以写一个 yaml 文件,并挂载到容器中覆盖 /etc/envoy/envoy.yaml 文件,或者通过 -c 参数指定配置文件:

[root@localhost ~]# docker run -d \
    -v $(pwd)/envoy-custom.yaml:/envoy-custom.yaml \
    -p 10000:10000 \
    -p 9901:9901 \
    envoyproxy/envoy:v1.22-latest -c /envoy-custom.yaml

Envoy 静态配置

Envoy 的配置遵循 xDS API v3 规范,Evnoy 的配置文件也被称为 Bootstrap 配置,使用的接口为 config.bootstrap.v3.Bootstrap,它的整体结构如下:

{
  "node": "{...}",
  "static_resources": "{...}",
  "dynamic_resources": "{...}",
  "cluster_manager": "{...}",
  "hds_config": "{...}",
  "flags_path": "...",
  "stats_sinks": [],
  "stats_config": "{...}",
  "stats_flush_interval": "{...}",
  "stats_flush_on_admin": "...",
  "watchdog": "{...}",
  "watchdogs": "{...}",
  "tracing": "{...}",
  "layered_runtime": "{...}",
  "admin": "{...}",
  "overload_manager": "{...}",
  "enable_dispatcher_stats": "...",
  "header_prefix": "...",
  "stats_server_version_override": "{...}",
  "use_tcp_for_dns_lookups": "...",
  "dns_resolution_config": "{...}",
  "typed_dns_resolver_config": "{...}",
  "bootstrap_extensions": [],
  "fatal_actions": [],
  "default_socket_interface": "...",
  "inline_headers": [],
  "perf_tracing_file_path": "..."
}

Envoy 自带的默认配置文件中包含了两个部分:adminstatic_resources

admin 部分是 Envoy 管理接口的配置,接口定义为 config.bootstrap.v3.Admin

admin:
  address:
    socket_address:
      protocol: TCP
      address: 0.0.0.0
      port_value: 9901

该配置让 Envoy 暴露出 9901 的管理端口,我们可以通过 http://127.0.0.1:9901 访问 Envoy 的管理页面。

static_resources 部分就是 Envoy 的静态配置,接口定义为 config.bootstrap.v3.Bootstrap.StaticResources,结构如下:

{
  "listeners": [],
  "clusters": [],
  "secrets": []
}

其中,listeners 用于配置 Envoy 的监听地址,Envoy 会暴露一个或多个 Listener 来监听客户端的请求。而 clusters 用于配置服务集群,Envoy 通过服务发现定位集群成员并获取服务,具体路由到哪个集群成员由负载均衡策略决定。

listeners 的接口定义为 config.listener.v3.Listener,其中最重要的几项有:nameaddressfilter_chain

listeners:
- name: listener_0
  address:
    socket_address:
      protocol: TCP
      address: 0.0.0.0
      port_value: 10000
  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
        scheme_header_transformation:
          scheme_to_overwrite: https
        stat_prefix: ingress_http
        route_config:
          name: local_route
          virtual_hosts:
          - name: local_service
            domains: ["*"]
            routes:
            - match:
                prefix: "/"
              route:
                host_rewrite_literal: www.envoyproxy.io
                cluster: service_envoyproxy_io
        http_filters:
        - name: envoy.filters.http.router
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

其中 address 表示 Envoy 监听的地址,filter_chain 为过滤器链,Envoy 通过一系列的过滤器对请求进行处理,下面是 Envoy 包含的过滤器链示意图:

envoy-filters.png

这里使用的是 http_connection_manager 来代理 HTTP 请求,route_config 为路由配置,当请求路径以 / 开头时(match prefix "/"),路由到 service_envoyproxy_io 集群。集群使用 clusters 来配置,它的接口定义为 config.cluster.v3.Cluster,配置的内容如下:

clusters:
- name: service_envoyproxy_io
  connect_timeout: 30s
  type: LOGICAL_DNS
  # Comment out the following line to test on v6 networks
  dns_lookup_family: V4_ONLY
  lb_policy: ROUND_ROBIN
  load_assignment:
    cluster_name: service_envoyproxy_io
    endpoints:
    - lb_endpoints:
      - endpoint:
          address:
            socket_address:
              address: www.envoyproxy.io
              port_value: 443
  transport_socket:
    name: envoy.transport_sockets.tls
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
      sni: www.envoyproxy.io

Envoy 通过服务发现来定位集群成员,服务发现的方式 有以下几种:

然后 Envoy 使用某种负载均衡策略从集群中找出一个服务来调用,Envoy 支持的 负载均衡策略有

而在我们的例子中,集群的服务发现方式为 LOGICAL_DNS,并使用 ROUND_ROBIN 方式来负载均衡。

Envoy 动态配置

在上面的例子中,我们配置的地址都是固定的,但在实际应用中,我们更希望以一种动态的方式来配置,比如 K8S 环境下服务的地址随 Pod 地址变化而变化,我们不可能每次都去手工修改 Envoy 的配置文件。Envoy 使用了一套被称为 xDS 的 API 来动态发现资源。xDS 包括 LDS(Listener Discovery Service)、CDS(Cluster Discovery Service)、RDS(Route Discovery Service)、EDS(Endpoint Discovery Service),以及 ADS(Aggregated Discovery Service),每个 xDS 都对应着配置文件中的一小块内容,如下所示(图片来源):

xds.png

Envoy 通过订阅的方式来获取资源,如监控指定路径下的文件、启动 gRPC 流或轮询 REST-JSON URL。后两种方式会发送 DiscoveryRequest 请求消息,发现的对应资源则包含在响应消息 DiscoveryResponse 中。

基于文件的动态配置

基于文件的动态配置比较简单,Envoy 通过监听文件的变动来动态更新配置,我们创建一个文件 envoy.yaml,内容如下:

node:
  id: id_1
  cluster: test

admin:
  address:
    socket_address:
      protocol: TCP
      address: 0.0.0.0
      port_value: 9901

dynamic_resources:
  cds_config:
    path_config_source:
      path: /var/lib/envoy/cds.yaml
  lds_config:
    path_config_source:
      path: /var/lib/envoy/lds.yaml

这里我们可以看出,和静态配置的 static_resources 不一样,没有 clusterslisteners 的配置了,而是在 dynamic_resources 中定义了 cds_configlds_config,并指定了 clusterslisteners 的配置文件的路径。

注意:在动态配置中,node 参数是必须的,用于区分 Envoy 是属于哪个集群。

我们再分别创建 cds.yaml 文件和 lds.yaml 文件。cds.yaml 的内容如下:

resources:
- "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster
  name: service_envoyproxy_io
  connect_timeout: 30s
  type: LOGICAL_DNS
  # Comment out the following line to test on v6 networks
  dns_lookup_family: V4_ONLY
  lb_policy: ROUND_ROBIN
  load_assignment:
    cluster_name: service_envoyproxy_io
    endpoints:
    - lb_endpoints:
      - endpoint:
          address:
            socket_address:
              address: www.envoyproxy.io
              port_value: 443
  transport_socket:
    name: envoy.transport_sockets.tls
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
      sni: www.envoyproxy.io

和静态配置的 clusters 是一样的,只不过换成了 resources 配置,并指定了 typeenvoy.config.cluster.v3.Clusterlds.yaml 的内容如下:

resources:
- "@type": type.googleapis.com/envoy.config.listener.v3.Listener
  name: listener_0
  address:
    socket_address:
      protocol: TCP
      address: 0.0.0.0
      port_value: 10000
  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
        scheme_header_transformation:
          scheme_to_overwrite: https
        stat_prefix: ingress_http
        route_config:
          name: local_route
          virtual_hosts:
          - name: local_service
            domains: ["*"]
            routes:
            - match:
                prefix: "/"
              route:
                host_rewrite_literal: www.envoyproxy.io
                cluster: service_envoyproxy_io
        http_filters:
        - name: envoy.filters.http.router
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

这个配置和静态配置的 listeners 是一样的,只不过换成了 resources 配置,并指定了 typeenvoy.config.listener.v3.Listener

然后,运行下面的命令:

[root@localhost ~]# docker run -d \
    -v $(pwd):/var/lib/envoy \
    -p 10000:10000 \
    -p 9901:9901 \
    envoyproxy/envoy:v1.22-latest -c /var/lib/envoy/envoy.yaml

我们打开浏览器,访问 http://127.0.0.1:10000 就可以看到 Envoy 的首页了。

然后我们使用 sed 将文件中的 www.envoy.io 替换为 www.baidu.com

[root@localhost ~]# sed -i s/www.envoyproxy.io/www.baidu.com/ lds.yaml cds.yaml

刷新浏览器,可以看到页面变成了 Baidu 的首页了,我们没有重启 Envoy,就实现了配置的动态更新。

这里有一点需要特别注意,如果我们直接使用 vi 去编辑 lds.yamlcds.yaml 文件,有时候可能不会生效,这时我们可以将文件复制一份出来,编辑,然后再替换原文件,才可以让配置生效。而 sed -i 命令的 inplace edit 功能就是这样实现的。为什么需要这样做呢?因为 Docker 在有些环境下对 inotify 的支持不是很好,特别是 VirtualBox 环境。

基于控制平面(Control Plane)的动态配置

网络层一般被分为 数据平面(Data Plane)控制平面(Control Plane)。控制平面主要为数据包的快速转发准备必要信息;而数据平面则主要负责高速地处理和转发数据包。这样划分的目的是把不同类型的工作分离开,避免不同类型的处理相互干扰。数据平面的转发工作无疑是网络层的重要工作,需要最高的优先级;而控制平面的路由协议等不需要在短时间内处理大量的包,可以将其放到次一级的优先级中。数据平面可以专注使用定制序列化等各种技术来提高传输速率,而控制平面则可以借助于通用库来达到更好的控制与保护效果。

得益于 Envoy 的高性能易扩展等特性,Envoy 可以说已经是云原生时代数据平面的事实标准。新兴微服务网关如 GlooAmbassador 都基于 Envoy 进行扩展开发;而在服务网格中,Istio、Kong 社区的 Kuma、亚马逊的 AWS App Mesh 都使用 Envoy 作为默认的数据平面。那么作为数据平面的 Envoy 要怎么通过控制平面来动态配置呢?答案就是:xDS 协议

有很多已经实现的控制平面可以直接使用:

control-plane.png

也可以使用 Envoy 的 xDS API 开发自己的控制平面,官方提供了 Go 和 Java 的示例可供参考:

在官网的 Dynamic configuration (control plane) 例子中,使用 Go 实现了一个控制平面。

在控制平面服务启动之后,Envoy 的配置文件类似下面这样:

node:
  cluster: test-cluster
  id: test-id

dynamic_resources:
  ads_config:
    api_type: GRPC
    transport_api_version: V3
    grpc_services:
    - envoy_grpc:
        cluster_name: xds_cluster
  cds_config:
    resource_api_version: V3
    ads: {}
  lds_config:
    resource_api_version: V3
    ads: {}

static_resources:
  clusters:
  - type: STRICT_DNS
    typed_extension_protocol_options:
      envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
        "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
        explicit_http_config:
          http2_protocol_options: {}
    name: xds_cluster
    load_assignment:
      cluster_name: xds_cluster
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: go-control-plane
                port_value: 18000

其中,static_resources 中定义了控制平面的地址,而 dynamic_resources 中的 ads_config 则指定使用控制平面来动态获取配置。

参考

  1. Envoy 官方文档
  2. Envoy 官方文档中文版(ServiceMesher)
  3. Istio 服务网格进阶实战(ServiceMesher)
  4. Envoy 官方文档中文版(CloudNative)
  5. Envoy 基础教程(Jimmy Song)
  6. Kubernetes 中文指南(Jimmy Song)
  7. Envoy Handbook(米开朗基杨)
  8. What is Envoy
  9. Envoy基础介绍
  10. 史上最全的高性能代理服务器 Envoy 中文实战教程 !
  11. Envoy 中的 xDS REST 和 gRPC 协议详解
  12. Dynamic configuration (filesystem)
  13. Dynamic configuration (control plane)
  14. 通过 xDS 实现 Envoy 动态配置
  15. 如何为 Envoy 构建一个控制面来管理集群网络流量

更多

1. Envoy 的管理页面

默认情况下,Envoy 会暴露出 9901 的管理端口,我们访问 http://127.0.0.1:9901 可以看到 Envoy 的管理页面:

envoy-admin.png

这里可以看到有很多很有用的功能,比如:查看 Envoy 统计信息,查看 Prometheus 监控指标,开启或关闭 CPU Profiler,开启或关闭 Heap Profiler 等等。

关于管理接口可以参考官方文档 Administration interface

2. 体验 Sandboxes

Envoy 通过 Docker Compose 创建了很多沙盒环境用于测试 Envoy 的特性,感兴趣的同学可以挨个体验一下:

3. 如何从 Nginx 迁移到 Envoy?

https://github.com/yangchuansheng/envoy-handbook/blob/master/content/zh/docs/practice/migrating-from-nginx-to-envoy.md

扫描二维码,在手机上阅读!

Dapr 学习笔记

Dapr 的全称为 Distributed Application Runtime(分布式应用运行时),顾名思义,它的目的就是为分布式应用提供运行所依赖的的执行环境。Dapr 为开发者提供了服务间调用(service to service invocation)、消息队列(message queue)和事件驱动(event driven)等服务模型,它可以让开发人员更聚焦业务代码,而不用去关心分布式系统所带来的其他复杂挑战,比如:服务发现(service discovery)、状态存储(state management)、加密数据存储(secret management)、可观察性(observability)等。

下面这张图说明了 Dapr 在分布式系统中所承担的作用:

service-invocation.png

这是分布式系统中最常用的一种场景,你的应用需要去调用系统中的另一个应用提供的服务。在引入 Dapr 之后,Dapr 通过边车模式运行在你的应用之上,Dapr 会通过服务发现机制为你去调用另一个应用的服务,并使用 mTLS 提供了服务间的安全访问,而且每个 Dapr 会集成 OpenTelemetry 自动为你提供服务之间的链路追踪、日志和指标等可观察性功能。

下图是基于事件驱动模型的另一种调用场景:

pubsub.png

安装 Dapr

1. 安装 Dapr CLI

首先使用下面的一键安装脚本安装 Dapr CLI

[root@localhost ~]# wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash
Getting the latest Dapr CLI...
Your system is linux_amd64
Installing Dapr CLI...

Installing v1.6.0 Dapr CLI...
Downloading https://github.com/dapr/cli/releases/download/v1.6.0/dapr_linux_amd64.tar.gz ...
dapr installed into /usr/local/bin successfully.
CLI version: 1.6.0 
Runtime version: n/a

To get started with Dapr, please visit https://docs.dapr.io/getting-started/

使用 dapr -v 校验是否安装成功:

[root@localhost ~]# dapr -v
CLI version: 1.6.0 
Runtime version: n/a

注意,这里我们可以看到 Runtime versionn/a,所以下一步我们还需要安装 Dapr runtime

2. 初始化 Dapr

虽然 Dapr 也可以在非 Docker 环境下运行,但是官方更推荐使用 Docker,首先确保机器上已经安装有 Docker 环境,然后执行下面的 dapr init 命令:

[root@localhost ~]# dapr init
> Making the jump to hyperspace...
>> Installing runtime version 1.6.1
Dapr runtime installed to /root/.dapr/bin, you may run the following to add it to your path if you want to run daprd directly:
    export PATH=$PATH:/root/.dapr/bin
> Downloading binaries and setting up components...
> Downloaded binaries and completed components set up.
>> daprd binary has been installed to /root/.dapr/bin.
>> dapr_placement container is running.
>> dapr_redis container is running.
>> dapr_zipkin container is running.
>> Use `docker ps` to check running containers.
> Success! Dapr is up and running. To get started, go here: https://aka.ms/dapr-getting-started

从运行结果可以看到,Dapr 的初始化过程包含以下几个部分:

  • 安装 Dapr 运行时(daprd),安装位置为 /root/.dapr/bin,同时会创建一个 components 目录用于默认组件的定义
  • 运行 dapr_placement 容器,dapr placement 服务 用于实现本地 actor 支持
  • 运行 dapr_redis 容器,用于本地状态存储(local state store)和消息代理(message broker
  • 运行 dapr_zipkin 容器,用于实现服务的可观察性(observability

初始化完成后,再次使用 dapr -v 校验是否安装成功:

[root@localhost ~]# dapr -v
CLI version: 1.6.0 
Runtime version: 1.6.1

并使用 docker ps 查看容器运行状态:

[root@localhost ~]# docker ps
CONTAINER ID   IMAGE               COMMAND                  CREATED             STATUS                       PORTS                                                 NAMES
63dd751ec5eb   daprio/dapr:1.6.1   "./placement"            About an hour ago   Up About an hour             0.0.0.0:50005->50005/tcp, :::50005->50005/tcp         dapr_placement
a8d3a7c93e12   redis               "docker-entrypoint.s…"   About an hour ago   Up About an hour             0.0.0.0:6379->6379/tcp, :::6379->6379/tcp             dapr_redis
52586882abea   openzipkin/zipkin   "start-zipkin"           About an hour ago   Up About an hour (healthy)   9410/tcp, 0.0.0.0:9411->9411/tcp, :::9411->9411/tcp   dapr_zipkin

使用 Dapr API

dapr run 是最常用的 Dapr 命令之一, 这个命令用于启动一个应用,同时启动一个 Dapr 边车进程,你也可以不指定应用,只启动 Dapr 边车:

[root@localhost ~]# dapr run --app-id myapp --dapr-http-port 3500
WARNING: no application command found.
ℹ️  Starting Dapr with id myapp. HTTP Port: 3500. gRPC Port: 39736
ℹ️  Checking if Dapr sidecar is listening on HTTP port 3500
INFO[0000] starting Dapr Runtime -- version 1.6.1 -- commit 2fa6bd832d34f5a78c5e336190207d46b761093a  app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
INFO[0000] log level set to: info                        app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
INFO[0000] metrics server started on :46773/             app_id=myapp instance=localhost.localdomain scope=dapr.metrics type=log ver=1.6.1
INFO[0000] standalone mode configured                    app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
INFO[0000] app id: myapp                                 app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
INFO[0000] mTLS is disabled. Skipping certificate request and tls validation  app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
INFO[0000] local service entry announced: myapp -> 10.0.2.8:45527  app_id=myapp instance=localhost.localdomain scope=dapr.contrib type=log ver=1.6.1
INFO[0000] Initialized name resolution to mdns           app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
INFO[0000] loading components                            app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
INFO[0000] component loaded. name: pubsub, type: pubsub.redis/v1  app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
INFO[0000] waiting for all outstanding components to be processed  app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
INFO[0000] detected actor state store: statestore        app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
INFO[0000] component loaded. name: statestore, type: state.redis/v1  app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
INFO[0000] all outstanding components processed          app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
INFO[0000] enabled gRPC tracing middleware               app_id=myapp instance=localhost.localdomain scope=dapr.runtime.grpc.api type=log ver=1.6.1
INFO[0000] enabled gRPC metrics middleware               app_id=myapp instance=localhost.localdomain scope=dapr.runtime.grpc.api type=log ver=1.6.1
INFO[0000] API gRPC server is running on port 39736      app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
INFO[0000] enabled metrics http middleware               app_id=myapp instance=localhost.localdomain scope=dapr.runtime.http type=log ver=1.6.1
INFO[0000] enabled tracing http middleware               app_id=myapp instance=localhost.localdomain scope=dapr.runtime.http type=log ver=1.6.1
INFO[0000] http server is running on port 3500           app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
INFO[0000] The request body size parameter is: 4         app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
INFO[0000] enabled gRPC tracing middleware               app_id=myapp instance=localhost.localdomain scope=dapr.runtime.grpc.internal type=log ver=1.6.1
INFO[0000] enabled gRPC metrics middleware               app_id=myapp instance=localhost.localdomain scope=dapr.runtime.grpc.internal type=log ver=1.6.1
INFO[0000] internal gRPC server is running on port 45527  app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
WARN[0000] app channel is not initialized. did you make sure to configure an app-port?  app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
INFO[0000] actor runtime started. actor idle timeout: 1h0m0s. actor scan interval: 30s  app_id=myapp instance=localhost.localdomain scope=dapr.runtime.actor type=log ver=1.6.1
WARN[0000] app channel not initialized, make sure -app-port is specified if pubsub subscription is required  app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
WARN[0000] failed to read from bindings: app channel not initialized   app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
INFO[0000] dapr initialized. Status: Running. Init Elapsed 267.56237799999997ms  app_id=myapp instance=localhost.localdomain scope=dapr.runtime type=log ver=1.6.1
ℹ️  Checking if Dapr sidecar is listening on GRPC port 39736
ℹ️  Dapr sidecar is up and running.
✅  You're up and running! Dapr logs will appear here.

INFO[0002] placement tables updated, version: 0          app_id=myapp instance=localhost.localdomain scope=dapr.runtime.actor.internal.placement type=log ver=1.6.1

上面的命令通过参数 --app-id myapp 启动了一个名为 myapp 的空白应用,并让 Dapr 监听 3500 HTTP 端口(--dapr-http-port 3500)。由于没有指定组件目录,Dapr 会使用 dapr init 时创建的默认组件定义,可以在 /root/.dapr/components 目录查看:

[root@localhost ~]# ls /root/.dapr/components
pubsub.yaml  statestore.yaml

这个目录默认有两个组件:pubsub 和 statestore。我们查看组件的定义,可以发现两个组件都是基于 Redis 实现的。

pubsub 定义:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: pubsub
spec:
  type: pubsub.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: ""

statestore 定义:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: ""
  - name: actorStateStore
    value: "true"

statestore 组件可以为分布式应用提供状态管理功能,你可以使用 Dapr 的状态管理 API 来对状态进行保存、读取和查询等。通过配置,Dapr 支持 不同类型的存储后端,如:Redis、MySQL、MongoDB、Zookeeper 等,如下图所示:

state-management-overview.png

下面我们来体验下 Dapr 的状态管理 API。

使用 POST 请求向 statestore 中保存一个 keynamevalueBruce Wayne 的键值对(注意这里的 statestore 必须和组件定义中的 name 一致):

[root@localhost ~]# curl -X POST -H "Content-Type: application/json" -d '[{ "key": "name", "value": "Bruce Wayne"}]' http://localhost:3500/v1.0/state/statestore

使用 GET 请求查询 statestorekeyname 的值:

[root@localhost ~]# curl http://localhost:3500/v1.0/state/statestore/name
"Bruce Wayne"

为了进一步了解状态信息是如何保存在 Redis 中的,可以使用下面的命令进到容器里查看:

[root@localhost ~]# docker exec -it dapr_redis redis-cli
127.0.0.1:6379> keys *
1) "myapp||name"
127.0.0.1:6379> hgetall myapp||name
1) "data"
2) "\"Bruce Wayne\""
3) "version"
4) "1"
127.0.0.1:6379> exit

组件和构建块

Dapr 有两个基本概念我们需要了解:组件(component)构建块(building block)。每个组件代表一个原子功能,有一个接口定义,并且可以有不同的实现,比如 statestore 组件,可以使用 Redis 实现,也可以使用 MySQL 实现。而构建块是基于一个或多个组件实现的一套用于在分布式系统中使用的 API 接口(HTTP 或 gRPC),比如上面的例子中我们使用的接口 /v1.0/state/** 就是 一个 State management 构建块,它是由 statestore 组件组成的,而这个 statestore 组件是由 Redis 实现的。下面是构建块和组件的示例图:

concepts-building-blocks.png

Dapr 提供了下面这些组件:

可以在 这里 查看 Dapr 支持的组件接口定义和实现。

Dapr 提供了下面这些构建块:

下面我们来体验下最常用的两个构建块:Service-to-service invocationPublish and subscribe

服务调用(Service-to-service Invocation)

使用 Dapr 的服务调用构建块,可以让你的应用可靠并安全地与其他应用进行通信。

service-invocation-overview.png

Dapr 采用了边车架构,每个应用都有一个 Dapr 作为反向代理,在调用其他应用时,实际上是调用本应用的 Dapr 代理,并由 Dapr 来调用其他应用的 Dapr 代理,最后请求到你要调用的应用。这样做的好处是由 Dapr 来管理你的所有请求,实现了下面这些特性:

  • 命名空间隔离(Namespace scoping)
  • 服务调用安全(Service-to-service security)
  • 访问控制(Access control)
  • 重试(Retries)
  • 插件化的服务发现机制(Pluggable service discovery)
  • mDNS 负载均衡(Round robin load balancing with mDNS)
  • 链路跟踪和监控指标(Tracing and metrics with observability)
  • 服务调用 API(Service invocation API)
  • gRPC 代理(gRPC proxying)

关于服务调用构建块,官方提供了多种语言的示例,可以下载下面的源码后,在 service_invocation 目录下找到你需要的语言:

[root@localhost ~]# git clone https://github.com/dapr/quickstarts.git

这里我们使用 Java 的示例来体验一下 Dapr 的服务调用,首先使用 mvn clean package 分别构建 checkoutorder-processor 两个项目,得到 CheckoutService-0.0.1-SNAPSHOT.jarOrderProcessingService-0.0.1-SNAPSHOT.jar 两个文件。

我们先使用 dapr run 启动 order-processor 服务:

[root@localhost ~]# dapr run --app-id order-processor \
  --app-port 6001 \
  --app-protocol http \
  --dapr-http-port 3501 \
  -- java -jar target/OrderProcessingService-0.0.1-SNAPSHOT.jar

这时我们同时启动了两个进程,Dapr 服务监听在 3501 端口,order-processor 服务监听在 6001 端口。order-processor 服务是一个非常简单的 Spring Boot Web 项目,暴露了一个 /orders API 模拟订单处理流程,我们可以使用 curl 直接访问它的接口:

[root@localhost ~]# curl -H Content-Type:application/json \
  -X POST \
  --data '{"orderId":"123"}' \
  http://127.0.0.1:6001/orders

当然这里我们更希望通过 Dapr 来调用它的接口:

[root@localhost ~]# curl -H Content-Type:application/json \
  -H dapr-app-id:order-processor \
  -X POST \
  --data '{"orderId":"123"}' \
  http://127.0.0.1:3501/orders

这里我们请求的是 Dapr 代理的端口,接口地址仍然是 /orders,特别注意的是,我们在请求里加了一个 dapr-app-id 头部,它的值就是我们要调用的应用 ID order-processor。但是这种做法官方是不推荐的,官方推荐每个应用都调用自己的 Dapr 代理,而不要去调别人的 Dapr 代理。

于是我们使用 dapr run 启动 checkout 服务:

[root@localhost ~]# dapr run --app-id checkout \
  --app-protocol http \
  --dapr-http-port 3500 \
  -- java -jar target/CheckoutService-0.0.1-SNAPSHOT.jar

这个命令同样会启动两个进程,checkout 服务是一个简单的命令行程序,它的 Dapr 代理服务监听在 3500 端口,可以看到想让 checkout 调用 order-processor,只需要调用它自己的 Dapr 代理即可,并在请求里加了一个 dapr-app-id 头部:

private static final String DAPR_HTTP_PORT = System.getenv().getOrDefault("DAPR_HTTP_PORT", "3500");

public static void main(String[] args) throws InterruptedException, IOException {
  String dapr_url = "http://localhost:"+DAPR_HTTP_PORT+"/orders";
  for (int i=1; i<=10; i++) {
    int orderId = i;
    JSONObject obj = new JSONObject();
    obj.put("orderId", orderId);

    HttpRequest request = HttpRequest.newBuilder()
        .POST(HttpRequest.BodyPublishers.ofString(obj.toString()))
        .uri(URI.create(dapr_url))
        .header("Content-Type", "application/json")
        .header("dapr-app-id", "order-processor")
        .build();

    HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
    System.out.println("Order passed: "+ orderId);
    TimeUnit.MILLISECONDS.sleep(1000);
  }
}

我们还可以使用同一个 app-id 启动多个实例(要注意端口号不要和之前的冲突):

[root@localhost ~]# dapr run --app-id order-processor \
  --app-port 6002 \
  --app-protocol http \
  --dapr-http-port 3502 \
  -- java -jar target/OrderProcessingService-0.0.1-SNAPSHOT.jar --server.port=6002

这样在请求 order-processor 服务时,Dapr 会为我们自动进行负载均衡。

发布订阅(Publish and Subscribe)

在分布式系统中,发布订阅是另一种常见的服务模型,应用之间通过消息或事件来进行通信,而不是直接调用,可以实现应用之间的解耦。

Dapr 的发布订阅构建块如下图所示:

pubsub-diagram.png

和上面一样,我们使用 Java 语言的示例来体验一下 Dapr 的发布订阅。使用 mvn clean package 分别构建 checkoutorder-processor 两个项目,得到 CheckoutService-0.0.1-SNAPSHOT.jarOrderProcessingService-0.0.1-SNAPSHOT.jar 两个文件。

使用 dapr run 启动 order-processor 服务:

[root@localhost ~]# dapr run --app-id order-processor \
  --app-port 6001 \
  --components-path ./components \
  -- java -jar target/OrderProcessingService-0.0.1-SNAPSHOT.jar

和上面的服务调用示例相比,少了 --app-protocol http--dapr-http-port 3501 两个参数,这是因为我们不再使用 HTTP 来进行服务间调用了,而是改成通过消息来进行通信。既然是消息通信,那么就少不了消息中间件,Dapr 支持常见的消息中间件来作为 Pub/sub brokers,如:Kafka、RabbitMQ、Redis 等。

我们在 components 目录下准备了一个 pubsub.yaml 文件,在这里对 Pub/sub brokers 组件进行配置:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: order_pub_sub
spec:
  type: pubsub.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: ""

其中,metadata.name 定义了组件名称为 order_pub_sub,应用就是使用这个名称来和组件进行通信的;spec.type 定义了组件类型是 pubsub,并使用 redis 实现;spec.metadata 为组件的一些配置信息。

在运行 dapr run 时,通过 --components-path 参数来指定应用运行所需要的组件的定义,components 目录下可同时包含多个组件定义。

和之前的例子一样,order-processor 服务是一个非常简单的 Spring Boot Web 项目,它仍然是暴露 /orders 接口来模拟订单处理流程,我们可以使用 curl 直接访问它的接口:

[root@localhost ~]# curl -H Content-Type:application/json \
  -X POST \
  --data '{"data":{"orderId":"123"}}' \
  http://127.0.0.1:6001/orders

不过这里的数据结构发生了一点变化,对于消息通信的应用,Dapr 使用了一个统一的类来表示接受到的消息:

public class SubscriptionData<T> {
    private T data;
}

查看 order-processor 的日志,可以看到它成功接收到了订单:

== APP == 2022-04-03 16:26:27.265  INFO 16263 --- [nio-6001-exec-6] c.s.c.OrderProcessingServiceController   : Subscriber received: 123

既然 order-processor 也是使用 /orders 接口,那和服务调用的例子有啥区别呢?仔细翻看代码我们可以发现,order-processor 除了有一个 /orders 接口,还新增了一个 /dapr/subscribe 接口:

@GetMapping(path = "/dapr/subscribe", produces = MediaType.APPLICATION_JSON_VALUE)
public DaprSubscription[] getSubscription() {
    DaprSubscription daprSubscription = DaprSubscription.builder()
            .pubSubName("order_pub_sub")
            .topic("orders")
            .route("orders")
            .build();
    logger.info("Subscribed to Pubsubname {} and topic {}", "order_pub_sub", "orders");
    DaprSubscription[] arr = new DaprSubscription[]{daprSubscription};
    return arr;
}

这是一个简单的 GET 接口,直接返回下面这样的数据:

[
    {
        "pubSubName": "order_pub_sub",
        "topic": "orders",
        "route": "orders"
    }
]

这实际上是 Dapr 的一种接口规范,被称为 Programmatically subscriptions(编程式订阅)。Dapr 在启动时会调用这个接口,这样 Dapr 就知道你的应用需要订阅什么主题,接受到消息后通过什么接口来处理。

除了编程式订阅,Dapr 还支持一种被称为 Declarative subscriptions(声明式订阅)的方式,这种方式的好处是对代码没有侵入性,可以不改任何代码就能将一个已有应用改造成主题订阅的模式。我们将声明式订阅定义在 subscription.yaml 文件中:

apiVersion: dapr.io/v1alpha1
kind: Subscription
metadata:
  name: order-pub-sub
spec:
  topic: orders
  route: orders
  pubsubname: order_pub_sub
scopes:
- order-processor
- checkout

声明式订阅和编程式订阅的效果是一样的,其目的都是让 Dapr 订阅 某个 Pub/subpubsubname: order_pub_sub)的 某个主题topic: orders),并将接受到的消息路由到 某个接口route: orders)来处理。区别在于,声明式订阅方便一个主题注册多个应用,编程式订阅方便一个应用注册多个主题。

我们使用 dapr publish 发布一个消息:

[root@localhost ~]# dapr publish --publish-app-id order-processor \
  --pubsub order_pub_sub \
  --topic orders \
  --data '{"orderId": 100}'
✅  Event published successfully

查看 order-processor 的日志,可以看到它又一次成功接收到了订单。这一次我们不是直接调用 /orders 接口,而是通过 order-processor 的 Dapr 将消息发送到 Redis,同时 order-processor 的 Dapr 通过订阅接受到该消息,并转发给 /orders 接口。

实际上这里我偷懒了,发布和订阅用的都是 order-processor 的 Dapr,在真实场景下,发布者会使用发布者自己的 Dapr。我们启一个新的 Dapr 实例(注意使用和 order-processor 相同的组件):

[root@localhost ~]# dapr run --app-id checkout \
  --dapr-http-port 3601 \
  --components-path ./components/

然后通过 checkout 发布消息:

[root@localhost ~]# dapr publish --publish-app-id checkout \
  --pubsub order_pub_sub \
  --topic orders \
  --data '{"orderId": 100}'
✅  Event published successfully

到这里我们实现了两个 Dapr 之间的消息通信,一个 Dapr 发布消息,另一个 Dapr
订阅消息,并将接受到的消息转发给我们的应用接口。不过这里我们是使用 dapr publish 命令来发布消息的,在我们的应用代码中,当然不能使用这种方式,而应用使用发布订阅构建块提供的接口。

我们使用 dapr run 启动 checkout 服务:

[root@localhost ~]# dapr run --app-id checkout \
  --components-path ./components \
  -- java -jar target/CheckoutService-0.0.1-SNAPSHOT.jar

查看 checkout 和 order-processor 的日志,它们分别完成了订单信息的发布和接受处理。浏览代码可以发现,checkout 服务其实使用了发布订阅构建块提供的 /v1.0/publish 接口:

private static final String PUBSUB_NAME = "order_pub_sub";
private static final String TOPIC = "orders";
private static String DAPR_HOST = System.getenv().getOrDefault("DAPR_HOST", "http://localhost");
private static String DAPR_HTTP_PORT = System.getenv().getOrDefault("DAPR_HTTP_PORT", "3500");

public static void main(String[] args) throws InterruptedException, IOException {
    String uri = DAPR_HOST +":"+ DAPR_HTTP_PORT + "/v1.0/publish/"+PUBSUB_NAME+"/"+TOPIC;
    for (int i = 0; i <= 10; i++) {
        int orderId = i;
        JSONObject obj = new JSONObject();
        obj.put("orderId", orderId);

        // Publish an event/message using Dapr PubSub via HTTP Post
        HttpRequest request = HttpRequest.newBuilder()
                .POST(HttpRequest.BodyPublishers.ofString(obj.toString()))
                .uri(URI.create(uri))
                .header("Content-Type", "application/json")
                .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        logger.info("Published data: {}", orderId);
        TimeUnit.MILLISECONDS.sleep(3000);
    }
}

知道原理后,使用 curl 命令也可以模拟出完全相同的请求:

[root@localhost ~]# curl -H Content-Type:application/json \
  -X POST \
  --data '{"orderId": "100"}' \
  http://localhost:3601/v1.0/publish/order_pub_sub/orders

参考

  1. https://github.com/dapr/dapr
  2. Dapr 官方文档
  3. Dapr 知多少 | 分布式应用运行时

更多

1. Dapr Tutorials

https://docs.dapr.io/getting-started/tutorials/

2. Dapr 组件一览

  • State stores

    • Aerospike
    • Apache Cassandra
    • Couchbase
    • Hashicorp Consul
    • Hazelcast
    • Memcached
    • MongoDB
    • MySQL
    • PostgreSQL
    • Redis
    • RethinkDB
    • Zookeeper
    • AWS DynamoDB
    • GCP Firestore
    • Azure Blob Storage
    • Azure CosmosDB
    • Azure SQL Server
    • Azure Table Storage
    • OCI Object Storage
  • Name resolution

    • HashiCorp Consul
    • mDNS
    • Kubernetes
  • Pub/sub brokers

    • Apache Kafka
    • Hazelcast
    • MQTT
    • NATS Streaming
    • In Memory
    • JetStream
    • Pulsar
    • RabbitMQ
    • Redis Streams
    • AWS SNS/SQS
    • GCP Pub/Sub
    • Azure Event Hubs
    • Azure Service Bus
  • Bindings

    • Apple Push Notifications (APN)
    • Cron (Scheduler)
    • GraphQL
    • HTTP
    • InfluxDB
    • Kafka
    • Kubernetes Events
    • Local Storage
    • MQTT
    • MySQL
    • PostgreSql
    • Postmark
    • RabbitMQ
    • Redis
    • SMTP
    • Twilio
    • Twitter
    • SendGrid
    • Alibaba Cloud DingTalk
    • Alibaba Cloud OSS
    • Alibaba Cloud Tablestore
    • AWS DynamoDB
    • AWS S3
    • AWS SES
    • AWS SNS
    • AWS SQS
    • AWS Kinesis
    • GCP Cloud Pub/Sub
    • GCP Storage Bucket
    • Azure Blob Storage
    • Azure CosmosDB
    • Azure CosmosDBGremlinAPI
    • Azure Event Grid
    • Azure Event Hubs
    • Azure Service Bus Queues
    • Azure SignalR
    • Azure Storage Queues
    • Zeebe Command
    • Zeebe Job Worker
  • Secret stores

    • Local environment variables
    • Local file
    • HashiCorp Vault
    • Kubernetes secrets
    • AWS Secrets Manager
    • AWS SSM Parameter Store
    • GCP Secret Manager
    • Azure Key Vault
    • AlibabaCloud OOS Parameter Store
  • Configuration stores

    • Redis
  • Middleware

    • Rate limit
    • OAuth2
    • OAuth2 client credentials
    • Bearer
    • Open Policy Agent
    • Uppercase

3. 开发自己的组件

https://github.com/dapr/components-contrib/blob/master/docs/developing-component.md

扫描二维码,在手机上阅读!