通过Dapr实现一个简单的基于.net的微服务电商系统(二)——通讯框架讲解

曾宇平 dotNET跨平台 今天

首先感谢张队@geffzhang公众号转发了上一篇文章,希望广大.neter多多推广dapr,让云原生更快更好的在.net这片土地上落地生根。

  书接上回通过Dapr实现一个简单的基于.net的微服务电商系统,今天来分享一下这套电商demo的通讯部分到底是如何工作的,看看它是如何屏蔽与dapr繁琐的沟通工作让开发者专注于解决业务问题的。

  首先我们再回顾一下dapr的sidecar是如何与应用相互协同的。和istio类似,dapr的sidecar注入可以分为自动注册和手动注册,下面以手动加注解注册的方式我们来聊一聊dapr的工作逻辑。首先当我们设置一个应用(deployment)的时候,在template-metadata配置了dapr相关注解之后,凡是安装dapr集群的k8s会自动将dapr的sidecar注册到我们的pod中,如下图:

当服务启动后,我们可以用kubectl describe po xxx的方式看到当前该pod会产生两个容器:

  凡是了解k8s的开发人员应该知道。在同一个pod之中,container实例之间的通讯应该是基于同一个虚拟内网的,通俗的说就是两者通讯可以直接通过localhost:port的方式,这是dapr与应用交互的基础。和istio通过iptables 来做流量劫持让Envoy代理可以拦截所有的进出Pod的流量,即将入站流量重定向到 Sidecar,再拦截应用容器的出站流量经过 Sidecar 处理的方案相比,dapr选择了一个更加灵活的方式,也就是它只是主动暴露一个端口(默认3500),将是否和dapr通讯的选择权留给了应用本身。

  当我们发起一个rpc请求时,实际上我们是通过http(or grpc这里不展开)的方式,访问了了http://localhost:3500/v1.0/{invoke}/{servicename}/method/{path} 这么一个地址。sidecar通过解析这个地址得到远程服务名{servicename},以及一个谓词{invoke}以及远程服务的endpoint:{path}。它会通过内部的dns服务名查询servicename得到一个该服务在集群内的实例列表,通过负载均衡的方式发起一个下游调用。这个下游调用也并非直接像普通k8s应用内通过调用service name的方式去调用下游pod的container,而是访问下游pod内的sidecar,通过sidecar再去访问pod内的应用实例。他们之间的调用关系如图所示:  

  通过这样的设计,实际上应用只需要和daprd这个sidcar打交道即可。同时dapr实现了通过谓词解析成不同的服务类型实现。比如服务间调用通过谓词:{invoke}、状态读写的谓词是{state}、订阅发布的谓词是{publish}、{subscribe},几乎所有的行为都可以分解成谓词+服务名+endpoint这种模式(所有api可参考:https://docs.dapr.io/reference/api/),这也是实现这套通讯框架的基础。

  所以剩下的事情就比较简单了,dapr通讯基于http/grpc,所以我们只需要启动一套kestrel+httpclient or grpc service/client即可简单快捷的接入dapr。首先我们还是看看整个repo(https://github.com/sd797994/Oxygen-Dapr)的结构:

  Oxygen这部分主要是包含通用工具层、IOC依赖注入(基于autofac)、本地代理生成器ProxyGenerator。Client主要包含一些远程服务attr标记以及客户端代理工厂。而在Mesh这个单独分层里主要是对Dapr的Actor实现了相关封装、Service层比较简单,只是在hostbuilder启动了一个kestrel并获取所有标记了远程服务的接口来构建路由字典方便将我们的Application服务暴露成restapi。

  本地代理ProxyGenerator实现比较简单,使用了微软自带的代理类DispatchProxy。通过Autofac依赖注入接口的时候将接口和代理类实现注册到ioc容器中,这样当我们通过IServiceProxyFactory.CreateProxy时实际上是从ioc容器中拿到的DispatchProxy实例,这样调用任意该接口的方法都会被路由到DispatchProxy实例,从而实现方法拦截并最终通过RemoteMessageSender类型里的HttpClient发起对dapr的sidecar请求。

  Client层的ServerProxyFactory也比较简单,其实就三个东西,一个是IServiceProxyFactory,这个主要用于发起对远程rpc和actor的调用、一个是IEventBus以及IStateManager,分别用于发布事件和调用dapr的状态管理器。

  Service.Kestrel层主要是通过启动时由RequestDelegateFactory.CreateDelegate的方式将所有注册为remoteservice的接口实现为其构造一个Func<Tservice, Tin, Task<Tout>>这样的匿名委托并将其路由键和该委托注册到一个全局静态字典中。当收到请求时通过kv键值对的方式查询当前key(router)对应的匿名委托,并通过ioc容器构造一个Tservice实例(为什么要请求时创建一个实例?因为这样可以模拟MVC创建controller的方式将Tservice作为一个scope生命周期的对象创建出来,避免Tservice内部的构造函数依赖的非单例对象生命周期失效)

  整个请求收发流程如下:

    1、当客户端通过IServiceProxyFactory.CreateProxy<IxxxServcice>()时获取到该接口的DispatchProxy实例。

    2、实例解析各种参数后发起一个http调用,http请求localhost:3500的sidecar后等待回调。

    3、sidecar将请求组装后发给下游sidecar并由下游sidecar转发给pod内的应用。

    4、应用收到请求后解析path得到对应的RequestDelegate,调用RequestDelegate将请求打到具体的xxxServcice服务上,由服务完成具体的业务。

  Mesh.Dapr则是对Actor行为的一个具体封装,由于原始的dapr sdk需要继承BasicActor然后进行各种actor作业,我采用了另外一种方式,通过emit静态代理的方式创建了一个Actor服务,由其代为接收actor请求后再转发给具体的xxxServcice。同时这个Actor服务会启动一个timer,当timer到期时会进行一次model的版本检查,当版本变化后(一般是由于xxxServcice被调用),会通知xxxServcice继承自基类并重写的SaveData方法,由xxxServcice自身考虑是否需要做业务层的持久化(默认Actor代理服务会自动持久化到dapr的状态设备里),这一步是完全异步的并不会阻塞Actor代理原方法的执行,另外在Actor的使用中,我们也尽量避免在同步调用时去读取第三方的设备可能导致IO阻塞actor。在源码中涉及对actor调用xxxServcice异步的支持,我主要参考了async/await生成状态机的方式创建了一个ActorAsyncStateMachine,由该状态机来完成actor服务调用xxxServcice的async/await实现。

  sample包含一对客户端/服务端案例包含上述涉及的所有远程call,大家可以多参考一下。

  Dapr原始提供了一套sdk用于远程服务,该框架主要是用于实现rpc以及对dapr这些api的自定义封装,当使用这套框架后我们就可以不用再考虑创建具体的webapi控制器,由iapplicationservice申明远程服务后框架即可自动生成代理服务即可。

  该框架的实现方式当然还有诸多不完善或者我没考虑到的地方,主要起到一个抛砖引玉的作用,另外也是通过这个来了解dapr是如何统一了我们网络编程模型的,只有更了解dapr才能更好的使用和推广它。惯例,欢迎fork+star:

  https://github.com/sd797994/Oxygen-Dapr

  https://github.com/sd797994/Oxygen-Dapr.EshopSample

(0)

相关推荐