Go zero uses Etcd for service registration code analysis

Posted by super_man on Sun, 19 Dec 2021 04:23:36 +0100

code analysis

github.com/tal-tech/go-zero@v1.2.3/core/discov/publisher.go

package discov

import (
    "github.com/tal-tech/go-zero/core/discov/internal"
    "github.com/tal-tech/go-zero/core/lang"
    "github.com/tal-tech/go-zero/core/logx"
    "github.com/tal-tech/go-zero/core/proc"
    "github.com/tal-tech/go-zero/core/syncx"
    "github.com/tal-tech/go-zero/core/threading"
    clientv3 "go.etcd.io/etcd/client/v3"
)

type (
    // PubOption defines the method to customize a Publisher.
    PubOption func(client *Publisher)

    // A Publisher can be used to publish the value to an etcd cluster on the given key.
    Publisher struct {
        endpoints  []string
        key        string
        fullKey    string
        id         int64
        value      string
        lease      clientv3.LeaseID
        quit       *syncx.DoneChan
        pauseChan  chan lang.PlaceholderType
        resumeChan chan lang.PlaceholderType
    }
)

// NewPublisher returns a Publisher.
// endpoints is the hosts of the etcd cluster.
// key:value are a pair to be published.
// opts are used to customize the Publisher.
func NewPublisher(endpoints []string, key, value string, opts ...PubOption) *Publisher {
    publisher := &Publisher{
        endpoints:  endpoints,
        key:        key,
        value:      value,
        quit:       syncx.NewDoneChan(),
        pauseChan:  make(chan lang.PlaceholderType),
        resumeChan: make(chan lang.PlaceholderType),
    }

    for _, opt := range opts {
        opt(publisher)
    }

    return publisher
}

// KeepAlive keeps key:value alive.
func (p *Publisher) KeepAlive() error {
    cli, err := internal.GetRegistry().GetConn(p.endpoints)
    if err != nil {
        return err
    }

    p.lease, err = p.register(cli)
    if err != nil {
        return err
    }

    proc.AddWrapUpListener(func() {
        p.Stop()
    })

    return p.keepAliveAsync(cli)
}

// Pause pauses the renewing of key:value.
func (p *Publisher) Pause() {
    p.pauseChan <- lang.Placeholder
}

// Resume resumes the renewing of key:value.
func (p *Publisher) Resume() {
    p.resumeChan <- lang.Placeholder
}

// Stop stops the renewing and revokes the registration.
func (p *Publisher) Stop() {
    p.quit.Close()
}

func (p *Publisher) keepAliveAsync(cli internal.EtcdClient) error {
    ch, err := cli.KeepAlive(cli.Ctx(), p.lease)
    if err != nil {
        return err
    }

    threading.GoSafe(func() {
        for {
            select {
            case _, ok := <-ch:
                if !ok {
                    p.revoke(cli)
                    if err := p.KeepAlive(); err != nil {
                        logx.Errorf("KeepAlive: %s", err.Error())
                    }
                    return
                }
            case <-p.pauseChan:
                logx.Infof("paused etcd renew, key: %s, value: %s", p.key, p.value)
                p.revoke(cli)
                select {
                case <-p.resumeChan:
                    if err := p.KeepAlive(); err != nil {
                        logx.Errorf("KeepAlive: %s", err.Error())
                    }
                    return
                case <-p.quit.Done():
                    return
                }
            case <-p.quit.Done():
                p.revoke(cli)
                return
            }
        }
    })

    return nil
}

func (p *Publisher) register(client internal.EtcdClient) (clientv3.LeaseID, error) {
    resp, err := client.Grant(client.Ctx(), TimeToLive)
    if err != nil {
        return clientv3.NoLease, err
    }

    lease := resp.ID
    if p.id > 0 {
        p.fullKey = makeEtcdKey(p.key, p.id)
    } else {
        p.fullKey = makeEtcdKey(p.key, int64(lease))
    }
    _, err = client.Put(client.Ctx(), p.fullKey, p.value, clientv3.WithLease(lease))

    return lease, err
}

func (p *Publisher) revoke(cli internal.EtcdClient) {
    if _, err := cli.Revoke(cli.Ctx(), p.lease); err != nil {
        logx.Error(err)
    }
}

// WithPubEtcdAccount provides the etcd username/password.
func WithPubEtcdAccount(user, pass string) PubOption {
    return func(pub *Publisher) {
        internal.AddAccount(pub.endpoints, user, pass)
    }
}

// WithId customizes a Publisher with the id.
func WithId(id int64) PubOption {
    return func(publisher *Publisher) {
        publisher.id = id
    }
}

The function of this file is to register the Etcd service. A struct: Publisher is defined in the file

  Publisher struct {
        endpoints  []string
        key        string
        fullKey    string
        id         int64
        value      string
        lease      clientv3.LeaseID
        quit       *syncx.DoneChan
        pauseChan  chan lang.PlaceholderType
       resumeChan chan lang.PlaceholderType
    }
  • Endpoints: address of EDCT
  • Key: the key prefix of the service in etcd, taken from the configuration file
  • fullkey: if id is set for the key in etcd, it is a combination of key and id; If no id is set, it is a combination of key and lease
  • id: if set, it is used to generate fullKey
  • Value: the value of etcd, which is the address of the RPC service
  • Lease: lease id of etcd
  • quit: used to exit the operation
  • pauseChan: used to suspend the service registration. If suspended, the lease of etcd will be cancelled. After the suspension, there are two subsequent operations to restart or close
  • resumeChan: used to restart after service suspension

Publisher provides the following public methods

func NewPublisher(endpoints []string, key, value string, opts ...PubOption) *Publisher 
func (p *Publisher) KeepAlive() error
func (p *Publisher) Pause()
func (p *Publisher) Resume() 
func (p *Publisher) Stop()
  • Newpublisher: construction method of publish
  • KeepAlive(): service publishing and connection
  • Pause(): the service is suspended
  • Resume(): Service restart
  • Stop(): service stop
    The core method is the KeepAlive() method. Its code is as follows
func (p *Publisher) KeepAlive() error {
    cli, err := internal.GetRegistry().GetConn(p.endpoints)
    if err != nil {
        return err
    }

    p.lease, err = p.register(cli)
    if err != nil {
        return err
    }

    proc.AddWrapUpListener(func() {
        p.Stop()
    })

    return p.keepAliveAsync(cli)
}

line2-5: etcd connection operation
line7-10: create lease
line12-14: I'm not sure about this. It should be added to the listening queue. If the Rpc service is stopped, the service also needs to be stopped
line 16: asynchronous listening is mainly used to listen to the connection with etcd. If the connection is broken, it needs to be restarted; There is also the command to listen for pause, restart and stop

Let's first look at the line7-10 register method

func (p *Publisher) register(client internal.EtcdClient) (clientv3.LeaseID, error) {
    resp, err := client.Grant(client.Ctx(), TimeToLive)
    if err != nil {
        return clientv3.NoLease, err
    }

    lease := resp.ID
    if p.id > 0 {
        p.fullKey = makeEtcdKey(p.key, p.id)
    } else {
        p.fullKey = makeEtcdKey(p.key, int64(lease))
    }
    _, err = client.Put(client.Ctx(), p.fullKey, p.value, clientv3.WithLease(lease))

    return lease, err
}

line2-7: create etcd lease. The expiration time of TimeToLive is 10 seconds
line8-12: create a fullKey. If the id is set, the fullKey will be generated based on the key and id; If the id is not set, it is generated with key and lease id
line13: store value in etcd and bind it to the newly created lease

Look again at the line 16 keepAliveAsync method

func (p *Publisher) keepAliveAsync(cli internal.EtcdClient) error {
    ch, err := cli.KeepAlive(cli.Ctx(), p.lease)
    if err != nil {
        return err
    }

    threading.GoSafe(func() {
        for {
            select {
            case _, ok := <-ch:
                if !ok {
                    p.revoke(cli)
                    if err := p.KeepAlive(); err != nil {
                        logx.Errorf("KeepAlive: %s", err.Error())
                    }
                    return
                }
            case <-p.pauseChan:
                logx.Infof("paused etcd renew, key: %s, value: %s", p.key, p.value)
                p.revoke(cli)
                select {
                case <-p.resumeChan:
                    if err := p.KeepAlive(); err != nil {
                        logx.Errorf("KeepAlive: %s", err.Error())
                    }
                    return
                case <-p.quit.Done():
                    return
                }
            case <-p.quit.Done():
                p.revoke(cli)
                return
            }
        }
    })

    return nil
}

line2-5: lease regular renewal
line7: start a Ctrip monitor
line10-17: listen to etcd keeplive status. If the returned value is not true, first cancel the current lease and then re KeepLive()
line18-29: monitor whether to pause the operation through Publisher's pauseChan. If you need to pause, first cancel the current lease, and then monitor whether to restart or stop the subsequent operation through resumeChan and quit. If you restart, restart keepLive()
line30-32: monitor whether there is a stop operation. If it is necessary to stop, directly cancel the current lease

demo display

Use go zero to create a user's rpc service. The specific creation steps skim over, and finally in etc / user Yaml is configured as follows

Name: user.rpc
ListenOn: 127.0.0.1:8081
Etcd:
  Hosts:
    - 172.16.1.36:12379
  Key: user.rpc

Then start the user service. After successful startup, go to etcd to check

./etcdctl --endpoints=127.0.0.1:12379 get user.rpc --prefix

The result is

Topics: Go etcd