Build your own go program into a high-availability system based on etcdserver package (Part 1)

Build your own go program into a high-availability system based on the etcdserver package (Part 1)

Background

Each of our system developers hopes that their programs will never go down, and high availability is the goal of many systems. So how do we transform our system into a highly available system? With this question in mind, this article will show you how to do it yourself and transform our existing system based on the raft protocol from scratch. Many students know that the Raft protocol is a distributed consensus algorithm. From the user’s perspective, the functions it provides to programmers mainly include the following two aspects:

  1. It provides a globally consistent storage state so that our program can store information on multiple nodes through it
  2. It provides a fault-tolerant function. When the leader becomes unavailable, the system automatically starts to elect a new leader. And each node knows whether its identity is a follower or a leader. In this way, we can use this function to achieve read-write separation.

Of course, many students have said that we can directly deploy the highly available distributed key-value storage system etcd, which has the characteristics of high availability, high concurrency, and consistency, and has been widely used in cloud computing, microservices, containers and other fields. , is one of the underlying cornerstones of many cloud native systems. But everyone soon discovered that doing so increased the dependence of our system, so the solution given in this article is to directly use the etcdserver package (
go.etcd.io/etcd/server/v3/etcdserver is the Go language implementation of etcd, which provides the main functions of etcd server, including cluster management, data storage, data synchronization, etc.) The following content of this article is mainly divided into two parts. First, we will introduce the use of etcdserver, and then use an example to explain how to use the etcdserver package to build a highly available system. This construction method does not rely on external third-party components, so its distribution and deployment are relatively light and simple. of. Since there is quite a lot of content, we will tentatively introduce it in two installments.

Start etcdserver through embed

we pass
go.etcd.io/etcd/server/v3/embed this package to quickly start the integrated etcdserver.

package main

import (
   _ "context"
   "go.etcd.io/etcd/server/v3/embed"
   "log"
)

func main() {<!-- -->
   cfg := embed.NewConfig()
   cfg.Dir = "/Users/dongluyang1/Documents/workspace/toutiao/etcdserversample" //The directory where etcd data is stored, used to persistently store etcd data.
   cfg.WalDir = ""
   cfg.Name = "test" //Node name
   cfg.InitialCluster = "test=http://localhost:2380" //Cluster name
   cfg.ClusterState = embed.ClusterStateFlagNew //etcd The initial state of the cluster, which can be new or existing. When set to new, a new etcd cluster will be started; when set to existing, an existing etcd cluster will be added.
   cfg.AutoCompactionMode = "periodic"
   cfg.AutoCompactionRetention = "1"
   cfg.QuotaBackendBytes = 8 * 1024 * 1024 * 1024

   e, err := embed.StartEtcd(cfg)
   if err != nil {<!-- -->
      log.Fatalf("Failed to start etcd: %v", err)
   }
   defer e.Close()
  
   select {<!-- -->} //Prevent the main program from exiting, causing etcdserver to exit.
}

The above cfg parameter is passed into embed through the StartEtcd method, as shown below. In fact, its value is eventually passed to config.ServerConfig to configure etcdserver.

The value of embed.NewConfig is passed to config.ServerConfig to control the configuration of etcdserver
Our code above simply gives the commonly used configurations. The meaning of the configurations is given below.

The serverConfig structure in the go.etcd.io/etcd/server/v3/config package contains the configuration information of the etcd server. The following is the meaning of each parameter in the structure:

  1. Name: The name of the current node in etcd cluster, which can be any string. It is recommended to be a unique name in the cluster.
  2. DataDir: The directory where etcd data is stored, used to persistently store etcd data.
  3. ListenClientUrls: The etcd server listens to the URL address requested by the client, supporting multiple
    URLs, separated by commas. For example: http://localhost:2379, http://localhost:4001. If not filled in, the default is 2379
  4. ListenPeerUrls: URL address for etcd server to listen for communication between nodes, supports multiple
    URLs, separated by commas. For example: http://localhost:2380, http://localhost:7001. Leave blank and default to 2380
  5. InitialCluster: Information about all nodes in etcd cluster, with name=URL
    Expressed in the form of, each node information is separated by commas. For example: node1=http://localhost:2380,node2=http://localhost:7001.
  6. InitialClusterState: The initial state of etcd cluster, which can be new or existing. When set to new
    When set to existing, a new etcd cluster will be started; when set to existing, an existing etcd cluster will be added.
  7. InitialClusterToken: The unique identifier of etcd cluster, used to distinguish different etcd clusters. When starting a new etcd
    When clustering, you need to specify a unique identifier. AutoCompactionRetention: The retention time of etcd automatic compression function, in days. when
    When etcd enables the automatic compression function, data within the specified number of days will be retained, and expired data will be deleted.
  8. AutoCompactionMode: The mode of etcd automatic compression function, which can be periodic or revision. When set to
    When periodic, data will be compressed by time interval; when set to revision, data will be compressed by transaction number.

Client access etcdserver

cli, err := clientv3.New(clientv3.Config{<!-- -->
   Endpoints: []string{<!-- -->"localhost:2379"},
   DialTimeout: 5 * time.Second,
})
go func() {<!-- -->
   if err == nil {<!-- -->
      for {<!-- -->
         _, err = cli.Put(context.Background(), "yandaojiumozhi", fmt.Sprintf("mibao-%d", rand.Intn(100)))
         if err != nil {<!-- -->
            //handle error
         } else {<!-- -->
            resp, err := cli.Get(context.Background(), "yandaojiumozhi")
            if err != nil {<!-- -->
               //handle error
            }
            for _, ev := range resp.Kvs {<!-- -->
               fmt.Printf("%s : %s\\
", ev.Key, ev.Value)
            }
         }
         time.Sleep(5 * time.Second)
      }
   }
}()
defer cli.Close()

The above code is simply tested, randomly writing yandaojiumozhi through localhost:2379, and then reading this key

Demo results
The above code eliminates the need for us to deploy and maintain third-party etcd components. We can start our background program and start etcdserver through embed to implement storage.

Embed’s logic for starting etcdserver

The go.etcd.io/etcd/server/v3/etcdserver package is the core package of etcd server, which contains all the core logic of etcd server. The EtcdServer structure is the core of the etcd server. It is responsible for managing all components of the etcd server, monitoring client requests, processing transactions, and maintaining the etcd database and other core tasks. This package is mainly responsible for starting, stopping and managing the etcd server. and
The go.etcd.io/etcd/server/v3/embed package is responsible for
go.etcd.io/etcd/server/v3/etcdserver is packaged into an embeddable package, making it easier to embed etcd servers in applications. So after understanding how embed starts etcdserver, we can start etcdserver directly in our code, so that we can have greater flexibility to perform some functions. For example, judging whether it is a leader, etc.

The flow chart of embed starting etcdserver is as follows. Its core is to start the listener at 2379 and 2380, then configure config.ServerConfig, call NewServer, and finally start it.

embed the process of creating and starting etcdserver
So we can also write an embed ourselves to create and start etcdserver, and then use the following method to determine whether it is the leader. Through the judgment of isLeader, we can complete what different roles should do in the distributed environment.

This issue will introduce these first. In the next issue, we will demonstrate node joining, election and other related operations in a distributed environment.

func check(srv *etcdserver.EtcdServer, ctx context.Context) {<!-- -->
   log.Info("start check LeaderChanged")
   for {<!-- -->
      select {<!-- -->
      case <-ctx.Done():
         log.Info("has Done")
         return
      case <-srv.LeaderChangedNotify():
         {<!-- -->
            log.Info("Leader changed")
           /*This isLeader can determine whether the current node is the leader. If it is the leader, it can do some things.
           The business that the leader can do, for example, it can accept write requests, and other write requests will be forwarded to the leader.
           */
            isLeader := srv.Leader() == srv.ID()
           ...
         }

      }
   }
}

Welcome everyone to follow me, I will continue to work hard to share useful and informative programs and techniques. thank you all