我感觉 runtime 应该是 dapr 的核心。从 runtime 的代码入手,我们可以更快的把握 dapr 的整体思路。

runtime 的初始化

说实话我感觉 dapr 的核心逻辑并不是特别复杂。初始化 runtime 无非就是读取 components 配置,然后根据配置初始化所有的组件。通过阅读 pkg/runtime/runtime.goRun 函数,就可以验证我们的想法。RuninitRuntime 的主干如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func (a *DaprRuntime) Run(opts ...Option) error {
// 创建 options
var o runtimeOpts
for _, opt := range opts {
opt(&o)
}

// 初始化 runtime
err := a.initRuntime(&o)
}

func (a *DaprRuntime) initRuntime(opts *runtimeOpts) error {
// 注册支持的各种类型的 component
a.xxxRegistry.Register(opts.xxx...)

// 读取 components 配置,加载 components
// go routine 负责不断地处理 pendingComponents 这个 channel 中的数据
go a.processComponents()
// loadComponents 则是读取 opts 中指定的配置文件,将要加载的 component
// 写入 pendingComponents
err = a.loadComponents(opts)

// 启动 http 和 grpc server
a.startGRPCAPIServer(...)
a.startHTTPServer(...)

// 到这里 dapr 自己需要做的初始化工作都已经完成了,
// 接下来是和实际的应用程序做初始化。

// 等应用程序 ready,通过不断和 app 建立 tcp 连接实现
a.blockUntilAppIsReady()

// 和应用程序建立连接
a.createAppChannel()

// 启动 actor
a.initActors()

// 开始 subscribing,开始从 binding reading
a.startSubscribing()
a.startReadingFromBindings()
}

Registry - 工厂集合

Registry 为 component 提供了各种实现类型的注册。每一种 component 的每一个实现,都需要提供工厂方法。daprd 在创建 runtime 的时候,会将所有支持的 component 实现都以参数的形式传入。在上面描述的 initRuntime 方法中,又会把这些传入的实现注册到 registry 中。在我们处理 component 的时候,会根据配置中指定的类型,寻找对应的工厂方法并加以调用。下面,我们用 state store 为例子,看看具体的调用链路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 在 initRuntime 中被调用,处理 pendingComponents 
// 中待处理的 component 配置。
processComponents() {
// -> processComponentAndDependents()
// -> doProcessOneComponent()
switch category {
case stateComponent:
return a.initState(comp)
// 其他类型
}
return nil
}

initState() {
// 调用 registry 的 create 方法。
// 它的实现就是从内存中维护的 map 中获取对应的工厂方法,然后调用它
store, err := a.stateStoreRegistry.Create(s.Spec.Type, s.Spec.Version) {
if method, ok := s.stateStores[name]; ok {
return method(), nil
}
}

// 用 component 配置中的参数初始化
store.Init(...)
}

Subscribing - 屏蔽消息实现

initRuntime 的最后,它调用了 startSubscribingstartReadingFromBindings。这两个我个人觉得有点类似。binding 更多的是为了建立和外部(i.e. 不被 dapr 所管理)服务通讯通道,例如读或者写一个外部部署的 Kafka。如果这个 Kafka 已经包含在了 component 配置中,我们则应该使用 subscribing。

startSubscribing 主要做的事情就是遍历所有注册了的 pubsub,对于每一个订阅的 topic,就启动一个 go routine。这个 go routine 会不断地将收到的消息通过 RPC 调用,转发给应用程序。应用程序只需要提供一个回调接口即可。这样设计的一个好处就是,应用程序完全不需要考虑底层的实现到底是 pull 还是 push 模式,dapr 会自动使用对应的模式获取消息进行转发。

总结

runtime 的结构非常简单。它的主要职责就是根据配置来创建各种 component,从而构建完整的运行时环境。