没有Kubernetes也可以玩转Dapr?

news2024/11/26 12:52:28

一、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方法,最终得到一个于当前部署环境相匹配的表示,并利用此标识借助基础设施的利用完整目标服务的调用。

package nameresolution

type Resolver interface {
    Init(metadata Metadata) error
    ResolveID(req ResolveRequest) (string, error)
}

type Metadata struct {
    Properties    map[string]string `json:"properties"`
    Configuration interface{}
}

type ResolveRequest struct {
    ID        string
    Namespace string
    Port      int
    Data     map[string]string
}

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

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

public class HomeController: Controller
{
    private static readonly ConcurrentDictionary<string, EndpointCollection> _applications = new();

    [HttpPost("/register")]
    public IActionResult Register([FromBody] RegisterRequest request)
    {
        var appId = request.Id;
        var endpoints = _applications.TryGetValue(appId, out var value) ? value : _applications[appId] = new();
        endpoints.TryAdd(request.HostAddress, request.Port);
        Console.WriteLine($"Register {request.Id} =>{request.HostAddress}:{request.Port}");
        return Ok();
    }

    [HttpPost("/resolve")]
    public IActionResult Resolve([FromBody] ResolveRequest request)
    {
        if (_applications.TryGetValue(request.ID, out var endpoints) && endpoints.TryGet(out var endpoint))
        {
            Console.WriteLine($"Resolve app {request.ID} =>{endpoint}");
            return Content(endpoint!);
        }
        return NotFound();
    }
}

public class EndpointCollection
{
    private readonly List<string> _endpoints = new();
    private int _index = 0;
    private readonly object _lock = new();

    public bool TryAdd(string ipAddress, int port)
    {
        lock (_lock)
        {
            var endpoint = $"{ipAddress}:{port}";
            if (_endpoints.Contains(endpoint))
            {
                return false;
            }
            _endpoints.Add(endpoint);
            return true;
        }
    }

    public bool TryGet(out string? endpoint)
    {
        lock (_lock)
        {
            if (_endpoints.Count == 0)
            {
                endpoint = null;
                return false;
            }
            _index++;
            if (_index >= _endpoints.Count)
            {
                _index = 0;
            }
            endpoint = _endpoints[_index];
            return true;
        }
    }
}

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

public class RegisterRequest
{
    public string Id { get; set; } = default!;
    public string HostAddress { get; set; } = default!;
    public int Port { get; set; }
}

public class ResolveRequest
{
    public string ID { get; set; } = default!;
    public string? Namespace { get; set; }
    public int Port { get; }
    public Dictionary<string, string> Data { get; } = new();
}

四、自定义NameResolution组件

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

image

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

package svcreg

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"strconv"

	"github.com/dapr/components-contrib/nameresolution"
	"github.com/dapr/kit/logger"
)

type Resolver struct {
	logger           logger.Logger
	registerEndpoint string
	resolveEndpoint  string
}

type RegisterRequest struct {
	Id, HostAddress string
	Port            int64
}

func (resolver *Resolver) Init(metadata nameresolution.Metadata) error {

	var endpoint, appId, hostAddress string
	var ok bool

	// Extracts register & resolve endpoint
	if dic, ok := metadata.Configuration.(map[interface{}]interface{}); ok {
		endpoint = fmt.Sprintf("%s", dic["endpointAddress"])
		resolver.registerEndpoint = fmt.Sprintf("%s/register", endpoint)
		resolver.resolveEndpoint = fmt.Sprintf("%s/resolve", endpoint)
	}
	if endpoint == "" {
		return errors.New("service registry endpoint is not configured")
	}

	// Extracts AppID, HostAddress and Port
	props := metadata.Properties
	if appId, ok = props[nameresolution.AppID]; !ok {
		return errors.New("AppId does not exist in the name resolution metadata")
	}
	if hostAddress, ok = props[nameresolution.HostAddress]; !ok {
		return errors.New("HostAddress does not exist in the name resolution metadata")
	}
	p, ok := props[nameresolution.DaprPort]
	if !ok {
		return errors.New("DaprPort does not exist in the name resolution metadata")
	}
	port, err := strconv.ParseInt(p, 10, 32)
	if err != nil {
		return errors.New("DaprPort is invalid")
	}

	// Register service (application)
	var request = RegisterRequest{appId, hostAddress, port}
	payload, err := json.Marshal(request)
	if err != nil {
		return errors.New("fail to marshal register request")
	}
	_, err = http.Post(resolver.registerEndpoint, "application/json", bytes.NewBuffer(payload))

	if err == nil {
		resolver.logger.Infof("App '%s (%s:%d)' is successfully registered.", request.Id, request.HostAddress, request.Port)
	}
	return err
}

func (resolver *Resolver) ResolveID(req nameresolution.ResolveRequest) (string, error) {

	// Invoke resolve service and get resolved target app's endpoint ("{ip}:{port}")
	payload, err := json.Marshal(req)
	if err != nil {
		return "", err
	}
	response, err := http.Post(resolver.resolveEndpoint, "application/json", bytes.NewBuffer(payload))
	if err != nil {
		return "", err
	}
	defer response.Body.Close()
	result, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return "", err
	}
	return string(result), nil
}

func NewResolver(logger logger.Logger) *Resolver {
	return &Resolver{
		logger: logger,
	}
}

如上面的代码片段所示,我们定义核心的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”。

// Name resolutions.
nr "github.com/dapr/components-contrib/nameresolution"
nr_consul "github.com/dapr/components-contrib/nameresolution/consul"
nr_kubernetes "github.com/dapr/components-contrib/nameresolution/kubernetes"
nr_mdns "github.com/dapr/components-contrib/nameresolution/mdns"
nr_svcreg "github.com/dapr/components-contrib/nameresolution/svcreg"

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

runtime.WithNameResolutions(
	nr_loader.New("svcreg", func() nr.Resolver {
		return nr_svcreg.NewResolver(logContrib)
	}),
	nr_loader.New("mdns", func() nr.Resolver {
		return nr_mdns.NewResolver(logContrib)
	}),
	nr_loader.New("kubernetes", func() nr.Resolver {
		return nr_kubernetes.NewResolver(logContrib)
	}),
	nr_loader.New("consul", func() nr.Resolver {
		return nr_consul.NewResolver(logContrib)
	}),
),

六、编译部署daprd.exe

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

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

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

image

七、配置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就来源于这里)。

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: daprConfig
spec:
  nameResolution:
    component: "svcreg"
    configuration:
      endpointAddress: http://127.0.0.1:3721
  tracing:
    samplingRate: "1"
    zipkin:
      endpointAddress: http://localhost:9411/api/v2/spans

八、测试效果

我们现在编写一个Dapr应用来验证一下自定义的NameResolution组件是否有效。具有如下定义的App2是一个ASP.NET Core应用,它利用路由提供了用来进行加、减、乘、除运算的API。

 using Microsoft.AspNetCore.Mvc;
 using Shared;

 var app = WebApplication.Create(args);
 app.MapPost("{method}", Calculate);
 app.Run("http://localhost:9999");

 static IResult Calculate(string method, [FromBody] Input input)
 {
     var result = method.ToLower() switch
     {
         "add" => input.X + input.Y,
         "sub" => input.X - input.Y,
         "mul" => input.X * input.Y,
         "div" => input.X / input.Y,
         _ => throw new InvalidOperationException($"Invalid method {method}")
     };
     return Results.Json(new Output { Result = result });
 }
public class Input
{
    public int X { get; set; }
    public int Y { get; set; }
}

public class Output
{
    public int 		Result { get; set; }
    public DateTimeOffset 	Timestamp { get; set; } = DateTimeOffset.Now;
}

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

 using Dapr.Client;
 using Shared;

 HttpClient client = DaprClient.CreateInvokeHttpClient(appId: "app2");
 var input = new Input(2, 1);

 await InvokeAsync("add", "+");
 await InvokeAsync("sub", "-");
 await InvokeAsync("mul", "*");
 await InvokeAsync("div", "/");

 async Task InvokeAsync(string method, string @operator)
 {
     var response = await client.PostAsync(method, JsonContent.Create(input));
     var output = await response.Content.ReadFromJsonAsync<Output>();
     Console.WriteLine( $"{input.X} {@operator} {input.Y} = {output.Result} ({output.Timestamp})");
 }

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

image

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

image

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

image

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

image

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1354547.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Golang拼接字符串性能对比

g o l a n g golang golang的 s t r i n g string string类型是不可修改的&#xff0c;对于拼接字符串来说&#xff0c;本质上还是创建一个新的对象将数据放进去。主要有以下几种拼接方式 拼接方式介绍 1.使用 s t r i n g string string自带的运算符 ans ans s2. 使用…

STM32 基础知识(探索者开发板)--135讲 ADC转换

ADC定义&#xff1a; ADC即模拟数字转换器&#xff0c;英文详称 Analog-to-digital converter&#xff0c;可以将外部的模拟信号转换 ADC数模转换中一些常用函数&#xff1a; 1. HAL_ADC_Init 函数 HAL_StatusTypeDef HAL_ADC_Init(ADC_HandleTypeDef *hadc); 初始化ADC 形参&…

网络连通性批量检测工具

一、背景介绍 企业网络安全防护中&#xff0c;都会要求配置物理网络防火墙以及主机防火墙&#xff0c;加强对网络安全的防护。云改数转之际&#xff0c;多系统上云过程中都会申请开通大量各类网络配置&#xff0c;针对这些复杂且庞大的网络策略开通配置&#xff0c;那么在网络配…

【12】ES6:模块化

一、JavaScript 模块化 JavaScript 模块化是一种组织和管理 JavaScript 代码的方法&#xff0c;它将代码分割为独立的模块&#xff0c;每个模块都有自己的作用域&#xff0c;并且可以导出和导入功能。模块化可以提高代码的可维护性、可重用性和可扩展性。 在JavaScript中&…

AIGC时代-GPT-4和DALL·E 3的结合

在当今这个快速发展的数字时代&#xff0c;人工智能&#xff08;AI&#xff09;已经成为了我们生活中不可或缺的一部分。从简单的自动化任务到复杂的决策制定&#xff0c;AI的应用范围日益扩大。而在这个广阔的领域中&#xff0c;有两个特别引人注目的名字&#xff1a;GPT-4和D…

2020年认证杯SPSSPRO杯数学建模A题(第二阶段)听音辨位全过程文档及程序

2020年认证杯SPSSPRO杯数学建模 A题 听音辨位 原题再现&#xff1a; 把若干 (⩾ 1) 支同样型号的麦克风固定安装在一个刚性的枝形架子上 (架子下面带万向轮&#xff0c;在平地上可以被水平推动或旋转&#xff0c;但不会歪斜)&#xff0c;这样的设备称为一个麦克风树。不同的麦…

STM32CubeMX RS485接口使用

一、基本知识 TTL&#xff08;Transistor-Transistor Logic&#xff09;&#xff1a; 电平范围&#xff1a; 逻辑1对应于2.4V–5V&#xff0c;逻辑0对应于0V–0.5V。通信特点&#xff1a; 全双工。特点&#xff1a; 常见于单片机和微控制器的IO电平&#xff0c;USB转TTL模块通常…

stable diffusion 人物高级提示词(一)头部篇

一、女生发型 prompt描述推荐用法Long hair长发一定不要和 high ponytail 一同使用Short hair短发-Curly hair卷发-Straight hair直发-Ponytail马尾high ponytail 高马尾&#xff0c;一定不要和 long hair一起使用&#xff0c;会冲突Pigtails2条辫子-Braid辫子只写braid也会生…

算法通关村番外篇-数组实现队列

大家好我是苏麟 , 今天来用数组实现一下队列 . 数组实现队列 顺序存储结构存储的队列称为顺序队列&#xff0c;内部使用一个一维数组存储&#xff0c;用一个队头指针 front 指向队列头部节点(即使用int类型front来表示队头元素的下标)&#xff0c;用一个队尾指针rear(有的地方…

HTTP 代理原理及实现(二)

在上篇《HTTP 代理原理及实现&#xff08;一&#xff09;》里&#xff0c;我介绍了 HTTP 代理的两种形式&#xff0c;并用 Node.js 实现了一个可用的普通 / 隧道代理。普通代理可以用来承载 HTTP 流量&#xff1b;隧道代理可以用来承载任何 TCP 流量&#xff0c;包括 HTTP 和 H…

【InnoDB数据存储结构】第2章节:InnoDB行格式

目录结构 之前整篇文章太长&#xff0c;阅读体验不好&#xff0c;将其拆分为几个子篇章。 本篇章讲解 InnoDB 行格式。 InnoDB 行格式 InnoDB 一行记录是如何存储的&#xff1f; 这个问题是本文的重点&#xff0c;也是面试中经常问到的问题&#xff0c;所以就引出了下文的 …

水面漂浮物监测识别摄像机

水面漂浮物监测识别摄像机是一种用于监测水体表面上漂浮物的高科技设备。它主要通过安装在水域周边的摄像头实时捕捉水面情况&#xff0c;利用图像识别技术自动识别和监测水面漂浮物。这种设备在环境保护、水域清洁和水质监测等方面具有广泛的应用价值。 水面漂浮物包括各类垃圾…

vc2017编译从github网站上下载的源码

以ZLmediakit为例 1.下载软件 cmakehttps://github.com/Kitware/CMake/releases/download/v3.20.5/cmake-3.20.5-windows-x86_64.zip Microsoft Visual Studio https://my.visualstudio.com/Downloads?qvisual%20studio%202017&wt.mc_ido~msft~vscom~older-downloads …

一文搞懂SiLM824x系列SiLM8243BBCL-DG 双通道死区可编程隔离驱动 主要特性与应用 让技术变得更有价值

SiLM824x系列SiLM8243BBCL-DG是一款具有不同配置的隔离双通道门极驱动器。SiLM8243BBCL-DG配置为高、低边驱动&#xff0c;SiLM8243BBCL-DG可提供4A的输出源电流和6A的灌电流能力&#xff0c;并且其驱动输出电压可以支持到33V。支持死区可编程&#xff0c;通过调整DT脚外部的电…

Ansible、Saltstack、Puppet自动化运维工具介绍

本文主要是分享介绍三款主流批量操控工具Ansible、Saltstack、Puppet主要对比区别&#xff0c;以及Ansible和saltstack的基础安装和使用示例&#xff0c;如果觉得本文对你有帮助&#xff0c;欢迎点赞、收藏、评论&#xff01; There are many things that can not be broken&am…

LeetCode刷题---矩阵置零

解题思路&#xff1a; 本题要求原地置换元素 对矩阵进行第一轮遍历&#xff0c;使用第一行第一列来充当该行该列是否要置换为0的标记位&#xff0c;如果第一行或第一列本身就含有零元素&#xff0c;我们使用colZero和rowZero变量来对其标记。如果第i行第j列的那个元素为0&#…

互联网分布式应用之SpringDataJPA

SpringDataJPA Java 是第一大编程语言和开发平台。它有助于企业降低成本、缩短开发周期、推动创新以及改善应用服务。如今全球有数百万开发人员运行着超过 51 亿个 Java 虚拟机&#xff0c;Java 仍是企业和开发人员的首选开发平台。 课程内容的介绍 1. Spring整合Hibernate 2…

Spring配置文件

一&#xff1a; Bean标签基本配置 1&#xff1a;用途 用于配置对象交由Spring来创建&#xff0c;默认情况下它调用的是类中的无参构造函数&#xff0c;如果没有无参构造函数则不能创建成功。 2&#xff1a;基本属性&#xff08;id&#xff09; Bean实例在Spring容器中的唯一…

APK 瘦身

APK 瘦身的主要原因是考虑应用的下载转化率和留存率&#xff0c;应用太大了&#xff0c;用户可能就不下载了。再者&#xff0c;因为手机空间问题&#xff0c;用户有可能会卸载一些占用空间比较大的应用&#xff0c;所以&#xff0c;应用的大小也会影响留存率。 1 APK 的结构 …

台灯哪个品牌比较护眼?2024学生考研台灯推荐

在近几年&#xff0c;儿童青少年近视率非常高。很多家长认为孩子近视的原因是没有养成正确的用眼习惯&#xff0c;例如经常趴桌子写作业、眯眼看书等&#xff0c;但实际上这些坏习惯是因为没有合适的光线而导致的。所以安排一盏合适的台灯给孩子学习是非常重要的。但是市面上护…