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

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操作,daprcomponents-contrib,前者为核心运行时,后者为社区驱动贡献得组件。我们将克隆下来的源代码放在同一个目录下。

我们将自定义的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”。

七、配置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组件是否有效。我们采用《ASP.NET Core 6框架揭秘实例演示[03]:Dapr初体验》提供的服务调用的例子。具有如下定义的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正在被使用。

由于应用启动的时候会调用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. 2021.08.06 P2441 角色属性树(树形结构)

    2021.08.06 P2441 角色属性树(树形结构) P2441 角色属性树 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 题意: 求离x最近的祖先y且(x,y)>1. ...

  2. 前端优化建议:合理利用JavaScript的条件运算符

    在最近的项目中要使用到一个格式化文件大小的算法,于是不假思索写了如下代码: function formatSize(size){ if(size<1024){ return size+" ...

  3. Unity制作一个小星球

    制作过程 在场景中新建一个球体(Planet)和一个胶囊(Player),适当缩放并添加材质,这里胶囊会被视为玩家 然后将摄像机设为胶囊(Player)的子物体 自行调整合适的摄像机视角 新建脚本Gr ...

  4. HTTP协议4.14

    测试开发学习笔记 一. Saas software as a service 软件即服务 Platform as a service 平台即服务 单体架构---垂直架构---面向服务架构---微服务架 ...

  5. MeteoInfo-Java解析与绘图教程(八)_java解析卫星FY-4A一级产品文件(HDF举例)

    MeteoInfo-Java解析与绘图教程(八)_java解析卫星一级产品文件(HDF举例) 最近解析卫星数据遇到了一级产品,它的解析方式与之前文章说的有些不同,特此补充一下 卫星的一级产品,里面是没 ...

  6. [笔记] 2-sat

    定义 简单的说就是给出 \(n\) 个集合,每个集合有两个元素,已知形如选 \(a\) 则必须选 \(b\) 的若干个条件, 问是否存在从每个集合选择一个元素满足条件的方案,通常可以题目只要求任意一种 ...

  7. java基础4.19

    1.JAVA 的反射机制的原理. JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法:这种动态获取的信息以及动态调用对象的方 ...

  8. 虚拟机(Vmvare)与配置,得到一台学习机

    准备: 1.Vmvare 2.CentOS7.4镜像 安装与配置操作系统: 1.配置虚拟机上网 2.配置静态ip地址 开始安装 1. 2.直接下一步选择我们准备好的镜像,然后下一步 3.修改虚拟机的名 ...

  9. 『现学现忘』Git基础 — 21、git diff命令

    目录 1.git diff 命令说明 2.比较工作区与暂存区中文件的差别 3.比较暂存区与本地库中文件的差别 4.总结git diff命令常见用法 5.总结 1.git diff 命令说明 在comm ...

  10. S2-045远程命令执行漏洞的利用

    Apache Struts2 远程命令执行 (S2-045) 漏洞介绍: 漏洞编号:S2-045CVE编号:CVE-2017-5638漏洞类型:远程代码执行漏洞级别:高危漏洞风险:黑客通过利用漏洞可以 ...