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. ASP.NET Core 借助 Helm 部署应用至K8S

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

  6. Dapr-简介及环境搭建

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

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

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

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

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

  9. Kubernetes 架构(上)- 每天5分钟玩转 Docker 容器技术(120)

    Kubernetes Cluster 由 Master 和 Node 组成,节点上运行着若干 Kubernetes 服务. Master 节点 Master 是 Kubernetes Cluster ...

  10. Kubernetes 架构(下)- 每天5分钟玩转 Docker 容器技术(121)

    上一节我们讨论了 Kubernetes 架构 Master 上运行的服务,本节讨论 Node 节点. Node 是 Pod 运行的地方,Kubernetes 支持 Docker.rkt 等容器 Run ...

随机推荐

  1. cufflinks install

    [email protected]:~/bin/cufflinks-master$ ./configure --with-bam=/usr/local/include/bam checking for a BSD ...

  2. Java JDBC连接数据库 Access连接数据库

    1.加载JDBC驱动程序:  在连接数据库之前,首先要加载想要连接的数据库的驱动到JVM(Java虚拟机),再通过java.lang.Class类的静态方法forName(String  classN ...

  3. L2-015. 互评成绩

    学生互评作业的简单规则是这样定的:每个人的作业会被k个同学评审,得到k个成绩.系统需要去掉一个最高分和一个最低分,将剩下的分数取平均,就得到这个学生的最后成绩.本题就要求你编写这个互评系统的算分模块. ...

  4. Spring 与 Hibernate 集成 Transactional设置为只读

    @Transactional标签用于标记ServiceImpl使用事务,并且能够打开一个sessionFactory的session,并且打开事务. 如果在这个标签为@Transactional(pr ...

  5. jemalloc源码结构分析(一):内存申请处理过程

    一.5种malloc方法 1)tcache_alloc_small 2)arena_malloc_small 3)tcache_alloc_large 4)arena_malloc_large 5)h ...

  6. (转)wcf client与webservice通信(-)只修改配置文件而改变服务端

    http://www.cnblogs.com/yiyisawa/archive/2008/12/16/1356191.html 问题: 假设有一个大型系统新版本使用wcf 作为服务端,生成wcf cl ...

  7. 使用VLC创建组播流

    vlc既是一个播放器,又可以成为一个流媒体服务器.最近需要做udp组播播放相关的东西,需要先在本地搭建一个udp组播服务器,因为机器上本来就装有vlc,所以就用它了. 第一步: 点击媒体->流 ...

  8. ZOJ2110 HDU1010 搜索 Tempter of the Bone

    传送门:Tempter of the Bone 大意是给一个矩阵,叫你是否可以在给定的可走路径上不重复地走,在最后一秒走到终点. 我用了两个剪枝,且称其为简直001和剪枝002,事实证明001不要都可 ...

  9. TCP三次握手--syn攻击

    TCP握手协议 在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接.第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确 ...

  10. *jQuery选择器总结(原创:最全、最系统、实例展示)

    jquery选择器包括四部分:一.基本选择器二.层次选择器三.过滤选择器四.表单元素选择器 一.基本选择器1.ID选择器:$('#myDiv');2.类选择器:$('.className');3.元素 ...