Dapr 被设计成一个面向开发者的企业级微服务编程平台,它独立于具体的技术平台,可以运行在“任何地方”。Dapr本身并不提供“基础设施(infrastructure)”,而是利用自身的扩展来适配具体的部署环境。就目前的状态来说,如果希望真正将原生的Dapr应用与生产,只能部署在K8S环境下。虽然Dapr也提供针对Hashicorp Consul的支持,但是目前貌似没有稳定的版本支持。Kubernetes对于很多公司并非“标配”,由于某些原因,它们可以具有一套自研的微服务平台或者弹性云平台,让Dapr与之适配可能更有价值。这两周我们对此作了一些可行性研究,发现这其实不难,记下来我们就同通过一个非常简单的实例来介绍一下大致的解决方案。(拙著《ASP.NET Core 6框架揭秘》热卖中,首印送签名专属书签)。

目录

一、从NameResolution组件说起

二、Resolver

三、模拟服务注册与负载均衡

四、自定义NameResolution组件

五、注册自定义NameResolution组件

六、编译部署daprd.exe

七、配置svcreg

八、测试效果

一、NameResolution组件

虽然Dapr提供了一系列的编程模型,比如服务调用、发布订阅和Actor模型等,被广泛应用的应该还是服务调用。我们知道微服务环境下的服务调用需要解决服务注册与发现、负载均衡、弹性伸缩等问题,其实Dapr在这方面什么都没做,正如上面所说,Dapr自身不提供基础设施,它将这些功能交给具体的部署平台(比如K8S)来解决。Dapr中于此相关唯有一个简单得不能再简单的NameResolution组件而已。

从部署的角度来看,Dapr的所有功能都体现在与应用配对的Sidecar上。我们进行服务调用得时候只需要指定服务所在得目标应用的ID(AppID)就可以了。服务请求(HTTP或者gRPC)从应用转到sidecar,后者会将请求“路由”到合适的节点上。如果部署在Kubernetes集群上,如果指定了目标服务的标识和其他相关的元数据(命名空间和集群域名等),服务请求的寻址就不再是一个问题。实际上NameResolution组件体现的针对“名字(Name)”的“解析(Resolution)”解决的就是如将Dapr针对应用的标识AppID转换成基于部署环境的应用标识的问题。从dapr提供的代码来看,它目前注册了如下3种类型的NameResolution组件:

  • mdns:利用mDNS(Multicast DNS)实现服务注册与发现,如果没有显式配置,默认使用的就是此类型。由于mDNS仅仅是在小规模网络中采用广播通信实现的一种DNS,所以根本不适合正式的生成环境。
  • kubernetes:适配Kubernetes的名字解析,目前提供稳定的版本。
  • consul: 适配HashiCorp Consul的名字解析,目前最新为Alpha版本。

二、Resolver

一个注册的NameResolution组件旨在提供一个Resolver对象,该对象通过如下的接口来表示。如下面的代码片段所示,Resolver接口提供两个方法,Init方法会在应用启动的时候调用,作为参数的Metadata会携带于当前应用实例相关的元数据(包括应用标识和端口,以及Sidecar的HTTP和gRPC端口等)和针对当前NameResolution组件的配置。对于每一次服务调用,目标应用标识和命名空间等相关信息会被Sidecar封装成一个ResolveRequest 接口,并最为参数调用Resolver对象的ReolveID方法,最终得到一个于当前部署环境相匹配的表示,并利用此标识借助基础设施的利用完整目标服务的调用。

  1. package nameresolution
  2.  
  3. type Resolver interface {
  4. Init(metadata Metadata) error
  5. ResolveID(req ResolveRequest) (string, error)
  6. }
  7.  
  8. type Metadata struct {
  9. Properties map[string]string `json:"properties"`
  10. Configuration interface{}
  11. }
  12.  
  13. type ResolveRequest struct {
  14. ID string
  15. Namespace string
  16. Port int
  17. Data map[string]string
  18. }

三、模拟服务注册与负载均衡

假设我们具有一套私有的微服务平台,实现了基本的服务注册、负载均衡,甚至是弹性伸缩的功能,如果希望在这个平台上使用Dapr,我们只需要利用自定义的NameResolution组件提供一个对应的Resolver对象就可以了。我们利用一个ASP.NET Core MVC应用来模拟我们希望适配的微服务平台,如下这个HomeController利用静态字段_applications维护了一组应用和终结点列表(IP+端口)。对于针对某个应用的服务调用,我们通过轮询对应终结点的方式实现了简单的负载均衡。便于后面的叙述,我们将该应用简称为“ServiceRegistry”。

  1. public class HomeController: Controller
  2. {
  3. private static readonly ConcurrentDictionary<string, EndpointCollection> _applications = new();
  4.  
  5. [HttpPost("/register")]
  6. public IActionResult Register([FromBody] RegisterRequest request)
  7. {
  8. var appId = request.Id;
  9. var endpoints = _applications.TryGetValue(appId, out var value) ? value : _applications[appId] = new();
  10. endpoints.TryAdd(request.HostAddress, request.Port);
  11. Console.WriteLine($"Register {request.Id} =>{request.HostAddress}:{request.Port}");
  12. return Ok();
  13. }
  14.  
  15. [HttpPost("/resolve")]
  16. public IActionResult Resolve([FromBody] ResolveRequest request)
  17. {
  18. if (_applications.TryGetValue(request.ID, out var endpoints) && endpoints.TryGet(out var endpoint))
  19. {
  20. Console.WriteLine($"Resolve app {request.ID} =>{endpoint}");
  21. return Content(endpoint!);
  22. }
  23. return NotFound();
  24. }
  25. }
  26.  
  27. public class EndpointCollection
  28. {
  29. private readonly List<string> _endpoints = new();
  30. private int _index = 0;
  31. private readonly object _lock = new();
  32.  
  33. public bool TryAdd(string ipAddress, int port)
  34. {
  35. lock (_lock)
  36. {
  37. var endpoint = $"{ipAddress}:{port}";
  38. if (_endpoints.Contains(endpoint))
  39. {
  40. return false;
  41. }
  42. _endpoints.Add(endpoint);
  43. return true;
  44. }
  45. }
  46.  
  47. public bool TryGet(out string? endpoint)
  48. {
  49. lock (_lock)
  50. {
  51. if (_endpoints.Count == 0)
  52. {
  53. endpoint = null;
  54. return false;
  55. }
  56. _index++;
  57. if (_index >= _endpoints.Count)
  58. {
  59. _index = 0;
  60. }
  61. endpoint = _endpoints[_index];
  62. return true;
  63. }
  64. }
  65. }

HomeController提供了两个Action方法,Register方法用来注册应用,自定义Resolver的Init方法会调用它。另一个方法Resolve则用来完成根据请求的应用表示得到一个具体的终结点,自定义Resolver的ResolveID方法会调用它。这两个方法的参数类型RegisterRequest和ResolveRequest定义如下,后者和前面给出的同名接口具有一致的定义。两个Action都会在控制台输出相应的文字显示注册的应用信息和解析出来的终结点。

  1. public class RegisterRequest
  2. {
  3. public string Id { get; set; } = default!;
  4. public string HostAddress { get; set; } = default!;
  5. public int Port { get; set; }
  6. }
  7.  
  8. public class ResolveRequest
  9. {
  10. public string ID { get; set; } = default!;
  11. public string? Namespace { get; set; }
  12. public int Port { get; }
  13. public Dictionary<string, string> Data { get; } = new();
  14. }

四、自定义NameResolution组件

由于Dapr并不支持组件的动态注册,所以我们得将其源代码拉下来,修改后进行重新编译。这里涉及到两个git操作,daprcomponents-contrib,前者为核心运行时,后者为社区驱动贡献得组件。我们将克隆下来的源代码放在同一个目录下。

我们将自定义的NameResolution组件命名为“svcreg”(服务注册之意),所我们在components-contrib/nameresolution目录(该目录下我们会看到上面提到的几种NameResolution组件的定义)下创建一个同名的目录,并组件代码定义在该目录下的svcreg.go文件中。如下所示的就是该NameResolution组件的完整定义。

  1. package svcreg
  2.  
  3. import (
  4. "bytes"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "io/ioutil"
  9. "net/http"
  10. "strconv"
  11.  
  12. "github.com/dapr/components-contrib/nameresolution"
  13. "github.com/dapr/kit/logger"
  14. )
  15.  
  16. type Resolver struct {
  17. logger logger.Logger
  18. registerEndpoint string
  19. resolveEndpoint string
  20. }
  21.  
  22. type RegisterRequest struct {
  23. Id, HostAddress string
  24. Port int64
  25. }
  26.  
  27. func (resolver *Resolver) Init(metadata nameresolution.Metadata) error {
  28.  
  29. var endpoint, appId, hostAddress string
  30. var ok bool
  31.  
  32. // Extracts register & resolve endpoint
  33. if dic, ok := metadata.Configuration.(map[interface{}]interface{}); ok {
  34. endpoint = fmt.Sprintf("%s", dic["endpointAddress"])
  35. resolver.registerEndpoint = fmt.Sprintf("%s/register", endpoint)
  36. resolver.resolveEndpoint = fmt.Sprintf("%s/resolve", endpoint)
  37. }
  38. if endpoint == "" {
  39. return errors.New("service registry endpoint is not configured")
  40. }
  41.  
  42. // Extracts AppID, HostAddress and Port
  43. props := metadata.Properties
  44. if appId, ok = props[nameresolution.AppID]; !ok {
  45. return errors.New("AppId does not exist in the name resolution metadata")
  46. }
  47. if hostAddress, ok = props[nameresolution.HostAddress]; !ok {
  48. return errors.New("HostAddress does not exist in the name resolution metadata")
  49. }
  50. p, ok := props[nameresolution.DaprPort]
  51. if !ok {
  52. return errors.New("DaprPort does not exist in the name resolution metadata")
  53. }
  54. port, err := strconv.ParseInt(p, 10, 32)
  55. if err != nil {
  56. return errors.New("DaprPort is invalid")
  57. }
  58.  
  59. // Register service (application)
  60. var request = RegisterRequest{appId, hostAddress, port}
  61. payload, err := json.Marshal(request)
  62. if err != nil {
  63. return errors.New("fail to marshal register request")
  64. }
  65. _, err = http.Post(resolver.registerEndpoint, "application/json", bytes.NewBuffer(payload))
  66.  
  67. if err == nil {
  68. resolver.logger.Infof("App '%s (%s:%d)' is successfully registered.", request.Id, request.HostAddress, request.Port)
  69. }
  70. return err
  71. }
  72.  
  73. func (resolver *Resolver) ResolveID(req nameresolution.ResolveRequest) (string, error) {
  74.  
  75. // Invoke resolve service and get resolved target app's endpoint ("{ip}:{port}")
  76. payload, err := json.Marshal(req)
  77. if err != nil {
  78. return "", err
  79. }
  80. response, err := http.Post(resolver.resolveEndpoint, "application/json", bytes.NewBuffer(payload))
  81. if err != nil {
  82. return "", err
  83. }
  84. defer response.Body.Close()
  85. result, err := ioutil.ReadAll(response.Body)
  86. if err != nil {
  87. return "", err
  88. }
  89. return string(result), nil
  90. }
  91.  
  92. func NewResolver(logger logger.Logger) *Resolver {
  93. return &Resolver{
  94. logger: logger,
  95. }
  96. }

如上面的代码片段所示,我们定义核心的Resolver结构,该接口除了具有一个用来记录日志的logger字段,还有两个额外的字段registerEndpoint和resolveEndpoint,分别代表ServiceRegistry提供的两个API的URL。在为Resolver结构实现的Init方法中,我们从作为参数的元数据中提取出配置,并进一步从配置中提取出ServiceRegistry的地址,并在此基础上添加路由路径“/register”和“/resolve”对Resolver结构的registerEndpoint和resolveEndpoint字段进行初始化。接下来我们从元数据中提取出AppID、IP地址和内部gRPC端口号(外部应用通过此端口调用当前应用的Sidecar),它们被封装成RegisterRequest结构之后被序列化成JSON字符串,并作为输入调用对应的Web API完成对应的服务注册。

在实现的ResolveID中,我们直接将作为参数的ResolveRequest结构序列化成JSON,调用Resolve API。响应主体部分携带的字符串就是为目标应用解析出来的终结点(IP+Port),我们直接将其作为ResolveID的返回值。

五、注册自定义NameResolution组件

自定义的NameResolution组件需要显式注册到代表Sidecar的可以执行程序daprd中,入口程序所在的源文件为dapr/cmd/daprd/main.go。我们首先按照如下的方式导入svcreg所在的包”github.com/dapr/components-contrib/nameresolution/svcreg”。

  1. // Name resolutions.
  2. nr "github.com/dapr/components-contrib/nameresolution"
  3. nr_consul "github.com/dapr/components-contrib/nameresolution/consul"
  4. nr_kubernetes "github.com/dapr/components-contrib/nameresolution/kubernetes"
  5. nr_mdns "github.com/dapr/components-contrib/nameresolution/mdns"
  6. nr_svcreg "github.com/dapr/components-contrib/nameresolution/svcreg"

在main函数中,我们找到用来注册NameResolution组件的那部分代码,按照其他NameResolution组件注册那样,依葫芦画瓢完成针对svcreg的注册即可。注册代码中用来提供Resolver的NewResolver函数定义在上述的svcreg.go文件中。

  1. runtime.WithNameResolutions(
  2. nr_loader.New("svcreg", func() nr.Resolver {
  3. return nr_svcreg.NewResolver(logContrib)
  4. }),
  5. nr_loader.New("mdns", func() nr.Resolver {
  6. return nr_mdns.NewResolver(logContrib)
  7. }),
  8. nr_loader.New("kubernetes", func() nr.Resolver {
  9. return nr_kubernetes.NewResolver(logContrib)
  10. }),
  11. nr_loader.New("consul", func() nr.Resolver {
  12. return nr_consul.NewResolver(logContrib)
  13. }),
  14. ),

六、编译部署daprd.exe

到目前为止,所有的编程工作已经完成,接下来我们需要重新编译代表Sidecar的daprd.exe。从上面的代码片段可以看出,dapr的包路径都以“github.com/dapr”为前缀,所以我们需要修改go.mod文件(dapr/go.mod)将依赖路径重定向到本地目录,所以我们按照如下的方式添加了针对“github.com/dapr/components-contrib”的替换规则。

  1. replace (
  2. go.opentelemetry.io/otel => go.opentelemetry.io/otel v0.20.0
  3. gopkg.in/couchbaselabs/gocbconnstr.v1 => github.com/couchbaselabs/gocbconnstr v1.0.5
  4. k8s.io/client => github.com/kubernetes-client/go v0.0.0-20190928040339-c757968c4c36
  5. github.com/dapr/components-contrib => ../components-contrib
  6. )

在将当前目录切换到“dapr/cmd/daprd/”后,以命令行的方式执行“go build”后会在当前目录下生成一个daprd.exe可执行文件。现在我们需要使用这个新的daprd.exe将当前使用使用的替换掉,该文件所在的目录在“%userprofile%.dapr\bin”。

七、配置svcreg

我们之间已经说过,Dapr默认使用的是基于mDNS的NameResolution组件(对于的注册名为为“mdns”)。若要使我们自定义的组件“svcreg”生效,需要修改Dapr的配置文件(%userprofile%.dapr\config.yaml)。如下面的代码片段所示,我们不仅将使用的组件名称设置为“svcreg”(在dapr/cmd/daprd/main.go中注册NameResolution组件时提供的名称),还将服务注册API的URL(http://127.0.0.1:3721)放在了配置中(Resolver的Init方法提取的URL就来源于这里)。

  1. apiVersion: dapr.io/v1alpha1
  2. kind: Configuration
  3. metadata:
  4. name: daprConfig
  5. spec:
  6. nameResolution:
  7. component: "svcreg"
  8. configuration:
  9. endpointAddress: http://127.0.0.1:3721
  10. tracing:
  11. samplingRate: "1"
  12. zipkin:
  13. endpointAddress: http://localhost:9411/api/v2/spans

八、测试效果

我们现在编写一个Dapr应用来验证一下自定义的NameResolution组件是否有效。我们采用《ASP.NET Core 6框架揭秘实例演示[03]:Dapr初体验》提供的服务调用的例子。具有如下定义的App2是一个ASP.NET Core应用,它利用路由提供了用来进行加、减、乘、除预算的API。

  1. using Microsoft.AspNetCore.Mvc;
  2. using Shared;
  3.  
  4. var app = WebApplication.Create(args);
  5. app.MapPost("{method}", Calculate);
  6. app.Run("http://localhost:9999");
  7.  
  8. static IResult Calculate(string method, [FromBody] Input input)
  9. {
  10. var result = method.ToLower() switch
  11. {
  12. "add" => input.X + input.Y,
  13. "sub" => input.X - input.Y,
  14. "mul" => input.X * input.Y,
  15. "div" => input.X / input.Y,
  16. _ => throw new InvalidOperationException($"Invalid method {method}")
  17. };
  18. return Results.Json(new Output { Result = result });
  19. }
  20. public class Input
  21. {
  22. public int X { get; set; }
  23. public int Y { get; set; }
  24. }
  25.  
  26. public class Output
  27. {
  28. public int Result { get; set; }
  29. public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.Now;
  30. }

具有如下定义的App1是一个控制台程序,它利用Dapr客户端SDK调用了上诉四个API。

  1. using Dapr.Client;
  2. using Shared;
  3.  
  4. HttpClient client = DaprClient.CreateInvokeHttpClient(appId: "app2");
  5. var input = new Input(2, 1);
  6.  
  7. await InvokeAsync("add", "+");
  8. await InvokeAsync("sub", "-");
  9. await InvokeAsync("mul", "*");
  10. await InvokeAsync("div", "/");
  11.  
  12. async Task InvokeAsync(string method, string @operator)
  13. {
  14. var response = await client.PostAsync(method, JsonContent.Create(input));
  15. var output = await response.Content.ReadFromJsonAsync<Output>();
  16. Console.WriteLine( $"{input.X} {@operator} {input.Y} = {output.Result} ({output.Timestamp})");
  17. }

在启动ServiceRegistry之后,我们启动App2,控制台上会阐述如下的输出。从输出的NameResolution组件名称可以看出,我们自定义的svcreg正在被使用。

由于应用启动的时候会调用Resolver的Init方法进行注册,这一点也反映在ServiceRegistry如下所示的输出上。可以看出注册实例的AppID为”app2”,对应的终结点为“10.181.22.4:60840”。

然后我们再启动App1,如下所示的输出表明四次服务调用均成功完成。

启动的App1的应用实例同样会在ServiceRegistry中注册。而四次服务调用会导致四次针对Resolver的ResolveID方法的调用,这也体现在ServiceRegistry的输出上。

没有Kubernetes怎么玩Dapr?的更多相关文章

  1. Dapr | 云原生的抽象与实现

    引言 Dapr 是微软主导的云原生开源项目,2019年10月首次发布,到今年2月正式发布 V1.0 版本.在不到一年半的时间内,github star 数达到了 1.2 万,超过同期的 kuberne ...

  2. 乘风破浪,.Net Core遇见Dapr,为云原生而生的分布式应用运行时

    Dapr是一个由微软主导的云原生开源项目,国内云计算巨头阿里云也积极参与其中,2019年10月首次发布,到今年2月正式发布V1.0版本.在不到一年半的时间内,github star数达到了1.2万,超 ...

  3. Dapr实战(一) 基础概念与环境搭建

    什么是Dapr Dapr 是一个可移植的.事件驱动的运行时,可运行在云平台或边缘计算中.支持多种编程语言和开发框架. 上面是官方对Dapr的介绍.有点难以理解,大白话可以理解为:Dapr是一个运行时, ...

  4. 手把手教你学Dapr - 3. 使用Dapr运行第一个.Net程序

    上一篇:手把手教你学Dapr - 2. 必须知道的概念 注意: 文章中提到的命令行工具即是Windows Terminal/PowerShell/cmd其中的一个,推荐使用Windows Termin ...

  5. Dapr v1.8 正式发布

    Dapr是一套开源.可移植的事件驱动型运行时,允许开发人员轻松立足云端与边缘位置运行弹性.微服务.无状态以及有状态等应用程序类型.Dapr能够确保开发人员专注于编写业务逻辑,而不必分神于解决分布式系统 ...

  6. ASP.NET Core 借助 Helm 部署应用至K8S

    前言 玩K8S也有一段时间了,借助云服务提供商的K8S控制台,已经可以很方便的快速部署应用至K8S.通过简单的点击,可以一次性帮忙创建K8S 对象:Deployment.Service.Ingress ...

  7. Dapr-简介及环境搭建

    一.Dapr是什么? Dapr 是一个可移植的.事件驱动的运行时,它使任何开发人员能够轻松构建出弹性的.无状态和有状态的应用程序,并可运行在云平台或边缘计算中,它同时也支持多种编程语言和开发框架. 在 ...

  8. PowerDotNet平台化软件架构设计与实现系列(04):服务治理平台

    系统和系统之间,少不了数据的互联互通.随着微服务的流行,一个系统内的不同应用进行互联互通也是常态. PowerDotNet的服务治理平台发源于早期的个人项目Power.Apix.这个项目借鉴了工作过的 ...

  9. 学习 Kubernetes 的 Why 和 How - 每天5分钟玩转 Docker 容器技术(114)

    这是一个系统学习 Kubernetes 的教程,有下面两个特点: 系统讲解当前最流行的容器编排引擎 Kubernetes包括了安装部署.应用管理.网络.存储.监控.日志管理等多各个方面. 重实践并兼顾 ...

随机推荐

  1. JVM内存管理面试常见问题全解

    目录 一.什么是JVM 1.jvm的三个组成部分 二.类加载系统 1.类的加载过程 2.类加载器 三.双亲委派机制 1.双亲委派机制介绍 2.为什么要双亲委派机制 3.双亲委派机制的核心源码 4.全盘 ...

  2. Unity减小安装包的体积(210MB减小到7MB)

    概述 项目简介 由于是公司内做的项目,不方便开源,就只分享优化过程吧. 项目信息 逐日是一个移动端单机小游戏,使用Unity开发,目前已将项目使用的Unity升级到2019.4.14f1c1 (3e5 ...

  3. DFA算法之内容敏感词过滤

    DFA 算法是通过提前构造出一个 树状查找结构,之后根据输入在该树状结构中就可以进行非常高效的查找. 设我们有一个敏感词库,词酷中的词汇为:我爱你我爱他我爱她我爱你呀我爱他呀我爱她呀我爱她啊 那么就可 ...

  4. Nginx作为高性能服务器的缘由以及请求过程

    Nginx作为高性能服务器的缘由以及请求过程 简介: Nginxx采用的是多进程(单线程)&多路IO复用模型,使用I/O多路复用技术的Nginx,就成了"并发事件驱动"的服 ...

  5. XCTF练习题---CRYPTO---告诉你个秘密

    XCTF练习题---CRYPTO---告诉你个秘密 flag:TONGYUAN 步骤解读: 1.观察题目,下载附件 2.打开附件,内容好像有点像十六进制,先进行一下十六进制转换,得到一串字符 网址:h ...

  6. js实时查询,为空提示

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  7. OracleRAC ACFS安装与卸载

    目录 ACFS安装与卸载: 一.在RAC上手动安装ACFS/ADVM 模块的步骤如下: 1.验证内存中是否存在 ACFS/ADVM 模块: 2.用root用户重新安装ACFS/ADVM 模块: 3.A ...

  8. AngularJS搭建环境

    一.搭建环境 1.1 调试工具:batarang Chrome浏览器插件 主要功能:查看作用域.输出高度信息.性能监控 1.2 依赖软件:Node.js 下载:https://nodejs.org/e ...

  9. Vue 中 watch 的一个坑

    开发所用 Vue 版本 2.6.11 子组件 coma 中两个属性: props: { url: { type: String, default: '' }, oriurl:{ type: Strin ...

  10. css3常用动画

    //有道云笔记链接 http://note.youdao.com/s/72qbBVyv