構建屬於你自己的dapr服務發現

語言: CN / TW / HK

寫在最前: 這篇文章其實算是馬後炮了,因為一直拖延症的問題,順帶過了一個五一假期,結果發現已經有社群貢獻者提供了Consul的服務發現實現,於是本來寫了一半的文章只能進行調整了。拖延症害人啊!幾個草稿的文章看來要儘快趕出來了 ‍♂️

在上一篇文章中,我其實遺留了一個問題:如何定義dapr的服務發現呢?其實在後面閱讀dapr的原始碼之後也前一篇文章的評論中提到了答案:目前dapr提供了內建兩種服務發現模式:K8s模式和用於獨立部署的mDNS模式。mDNS模式在某些網路環境下可能存在問題(比如跨機房),不過沒有關係,dapr同時提供了可擴充套件能力,可以通過定義自主的服務發現能力擴充套件dapr的邊界。

從 NameResolution 到 Resolver 介面

pkg/components/nameresolution/registry.go 檔案中,dapr定義了一個 NameResolution 結構體用於服務註冊和發現:

type (
	// NameResolution is a name resolution component definition.
	NameResolution struct {
		Name          string
		FactoryMethod func() nr.Resolver
	}

	// Registry handles registering and creating name resolution components.
	Registry interface {
		Register(components ...NameResolution)
		Create(name, version string) (nr.Resolver, error)
	}

	nameResolutionRegistry struct {
		resolvers map[string]func() nr.Resolver
	}
)

其中真正的服務解析則是依靠 components-contrib 中實現了 Resolver 介面的具體實現執行。

// Resolver is the interface of name resolver.
type Resolver interface {
	// Init initializes name resolver.
	Init(metadata Metadata) error
	// ResolveID resolves name to address.
	ResolveID(req ResolveRequest) (string, error)
}

其中 Init 會在 Runtime 初始化時被呼叫,而 ResolveID 則會在服務查詢時呼叫。比如在 pkg/messaging/direct_messaging.go 的方法 getRemoteApp 中進行服務的解析:

func (d *directMessaging) getRemoteApp(appID string) (remoteApp, error) {
	id, namespace, err := d.requestAppIDAndNamespace(appID)
	if err != nil {
		return remoteApp{}, err
	}

	request := nr.ResolveRequest{ID: id, Namespace: namespace, Port: d.grpcPort}
	address, err := d.resolver.ResolveID(request)
	if err != nil {
		return remoteApp{}, err
	}

	return remoteApp{
		namespace: namespace,
		id:        id,
		address:   address,
	}, nil
}

當然,事實上這樣並不完全足夠,還需要把這個服務註冊放入dapr 支援的服務中 去:

runtime.WithNameResolutions(
    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)
    }),
),

上面的這些是設定的dpar目前支援的一些服務發現功能,而我們之前服務發現也一直使用的 Consul 實現,已經滿足我們的需求了…:sweat:拖延症害人啊!

從原理到實現

上面提到了我們需要實現一個 Resolver 介面的實現,我們可以預見到我們大概會需要這麼一個東西:

type resolver struct {}

// NewResolver creates Consul name resolver.
func NewResolver() nr.Resolver

// Init will configure component. It will also register service or validate client connection based on config
func (r *resolver) Init(metadata nr.Metadata) error 

// ResolveID resolves name to address via consul
func (r *resolver) ResolveID(req nr.ResolveRequest) (string, error)

接下來就需要一個 client *consul.Client 去實現服務的註冊:

type resolver struct {
    client *consul.Client
}


func (r *resolver) Init(metadata nr.Metadata) error {
    // ...

    if err := r.client.Agent().ServiceRegister(regData); err != nil {
        return fmt.Errorf("failed to register consul service: %w", err)
    }

    // ...
}

註冊服務完成後,在呼叫具體的服務時,我們需要獲取具體的服務地址:

func (r *resolver) ResolveID(req nr.ResolveRequest) (string, error) {
    // ...

    services, _, err := r.client.Health().Service(req.ID, "", true, cfg.QueryOptions)

    // ...
}

當然上面的演示程式碼只是部分核心功能程式碼,如果需要拓展更多的實現細節內容,需要檢視具體的官方接收社群貢獻的實現: components-contrib/nameresolution/consul