Zookeeper is a strongly consistent distributed database. Multiple nodes form a distributed cluster. If any node fails, the database still works properly and the client does not detect failover. The client writes data to any node, and the other nodes can see the latest data immediately.

Zookeeper is a key/value storage engine. Keys form a multi-level hierarchy in the form of a tree. Each node can store data and serve as a directory to store lower-level nodes.

Zookeeper provides apis for creating, modifying, and deleting nodes. If the parent node is not created, the byte point creation fails. If the parent node has children, the parent node cannot be deleted.

Zookeeper communicates with the client in bidirectional socket mode. The client can invoke the API provided by the server, and the server can push events to the client. There are various events that can be watched, such as adding, deleting, or changing nodes, child nodes, session state, etc.

Zookeeper events have a transmission mechanism. Events triggered by the increase, deletion, and change of byte points are transmitted to the upper layer in sequence. All parent nodes can receive data change events of byte points.

Zookeeper satisfies the partition tolerance P and strong consistency C of the CAP theorem at the expense of high performance A [availability implication performance]. The storage capability of ZooKeeper is limited. If the node layer is too deep, there are too many child nodes, or the data on the node is too large, the database stability may be affected. Therefore, ZooKeeper is not a database for high concurrency and high performance. Zookeeper is generally used only to store configuration information.

The read performance of ZooKeeper increases as the number of nodes increases, but the write performance decreases as the number of nodes increases. Therefore, the number of nodes should be limited to three or five.

As you can see in the figure, the complexity increases as the number of server nodes increases. Because each node and other nodes between the p2p connection. Three nodes can tolerate the failure of one node and five nodes can tolerate the failure of two nodes.

When a client connects to ZooKeeper, it selects any node to maintain a long link. Subsequent communication is performed through this node. If the node fails, the client tries to connect to another node.

The server maintains a session object for each client connection, and the session ID is stored on the client. Session objects are also distributed, meaning that when a node fails, the client uses the old session ID to connect to other nodes, and the session object maintained by the server continues to exist without the need to create a new session.

If the client proactively sends a session closure message, the server’s session object is immediately deleted. If the client crashes accidentally and does not send a shutdown message, the server’s session object continues to exist for a while. This parameter is the expiration time of the session. The client provides this parameter when creating the session. It is generally 10 to 30 seconds.

You may ask why the client needs to actively send a shutdown message when the server can sense that the connection is down.

The connection may be temporarily disconnected because the server has to consider network jitter. To avoid repeated creation and destruction of complex session objects, as well as a series of event initialization operations after the creation of the session, the server tries to prolong the lifetime of the session.

Zookeeper nodes can be Persistent or Ephermeral. Temporary nodes are all temporary nodes created during a session that disappear immediately after the session is closed. It is generally used in the service discovery system to bind the life of the service process to the life of the ZooKeeper child node to monitor the service process in real time.

Zookeeper also provides sequence nodes. This is similar to the auto_increment attribute in mysql. The server automatically adds a unique suffix to the sequential node names to keep the node names unique and sequential.

Another type of node is called a Protected node. This node is very special, but also very common. In the case of application service discovery, after the client creates a temporary node, the server node hangs, the connection is disconnected, and the client tries to reconnect to another node. Because the session is not closed, the temporary node that was created before still exists, but the client cannot recognize whether the temporary node was created because the session ID field is not stored inside the node. So the client adds a GUID prefix to the node name, which is stored on the client so that it can recognize which temporary node was created before it reconnects.

Next, we use Go language to implement the registration and discovery functions of service discovery.

As shown in the figure, we need to provide a service such as api.user. This service has three nodes, and each node has a different service address. Each of these three nodes registers its own service into ZK, and then the consumer reads ZK to get the service address of api.user, and selects any node address for service invocation. For the sake of simplicity, no weight parameters are provided here. In a formal service discovery, there are usually weight parameters that adjust traffic allocation between service nodes.

go get github.com/samuel/go-zookeeper/zk
Copy the code

First, we define a ServiceNode structure. This structure data will be stored in the node data to represent the address information discovered by the service.

type ServiceNode struct {
	Name string `json:"name"'// service name, here is user Host string' json:"host"`
	Port int    `json:"port"} in the definition of a service discovery client structure SdClient.typeStruct {zkServers [] server struct {zkServers [] server struct {zkServers [] server [] server struct {zkServers [] server [] server Func NewClient(zkServers []string, zkRoot String, timeout int) (*SdClient, Error) {client := new(SdClient) client.zkServers = zkServers client.zkroot = zkRoot // Connect to server conn, _, err := zk.Connect(zkServers, time.Duration(timeout)*time.Second)iferr ! = nil {returnNil, err} client.conn = conn // Create the service root nodeiferr := client.ensureRoot(); err ! = nil { client.Close()return nil, err
	}
	return// Close connection, release temporary node func (s *SdClient)Close() {
	s.conn.Close()
}

func (s *SdClient) ensureRoot() error {
	exists, _, err := s.conn.Exists(s.zkRoot)
	iferr ! = nil {return err
	}
	if! exists { _, err := s.conn.Create(s.zkRoot, []byte(""), 0, zk.WorldACL(zk.PermAll))
		iferr ! = nil && err ! = zk.ErrNodeExists {return err
		}
	}
	returnNil} It is important to note that the Create call in the code may return an existing error on the node, which is normal because it is possible for multiple processes to Create the node simultaneously. If you fail to create the root node, you also need to close the connection in time. We do not care about the permission control of the node, so we use zk.worldacl (zK.permall) to indicate that the node has no permission restrictions. Flag =0 in the Create parameter indicates that this is a persistent normal node. Func (s *SdClient) Register(node *ServiceNode) error {iferr := s.ensureName(node.Name); err ! = nil {return err
	}
	path := s.zkRoot + "/" + node.Name + "/n"
	data, err := json.Marshal(node)
	iferr ! = nil {return err
	}
	_, err = s.conn.CreateProtectedEphemeralSequential(path, data, zk.WorldACL(zk.PermAll))
	iferr ! = nil {return err
	}
	return nil}func (s *SdClient) ensureName(name string) error {
	path := s.zkRoot + "/" + name
	exists, _, err := s.conn.Exists(path)
	iferr ! = nil {return err
	}
	if! exists { _, err := s.conn.Create(path, []byte(""), 0, zk.WorldACL(zk.PermAll))
		iferr ! = nil && err ! = zk.ErrNodeExists {return err
		}
	}
	returnNil} starts by creating the/API /user node as the parent of the list of services. And then create a temporary protection order (ProtectedEphemeralSequential) child nodes, at the same time will address information stored in the node. What is a protected sequence temporary node? First, it is a temporary node. The node disappears automatically after the session is closed. Zookeeper automatically adds a suffix to the end of the node name to ensure that the node name is unique. It is also a protective node with a GUID field added to the node prefix to ensure that the temporary node can be connected to the client state after disconnection and reconnection. Func (s *SdClient) GetNodes(Name String) ([]*ServiceNode, error) {path := s.skroot +"/"Childs, _, err := s.conn.Children(path)iferr ! = nil {if err == zk.ErrNoNode {
			return []*ServiceNode{}, nil
		}
		return nil, err
	}
	nodes := []*ServiceNode{}
	for _, child := range childs {
		fullPath := path + "/" + child
		data, _, err := s.conn.Get(fullPath)
		iferr ! = nil {if err == zk.ErrNoNode {
				continue
			}
			return nil, err
		}
		node := new(ServiceNode)
		err = json.Unmarshal(data, node)
		iferr ! = nil {return nil, err
		}
		nodes = append(nodes, node)
	}
	return nodes, nil
}
Copy the code

To get the list of service nodes, we first get the list of byte names, and then read the contents in turn to get the service address. Because getting the bytecode name and the bytecode content are not an atomic operation, it is normal to expect a node-nonexistent error when you call Get to Get the content.

Put the above code together and a simple service discovery wrapper is implemented.

Finally, let’s look at how to use the code above. For convenience, we write multiple service providers and consumers in a main method.

func main() {// Servers := []string{"192.168.0.101:2118"."192.168.0.102:2118"."192.168.0.103:2118"}
	client, err := NewClient(servers, "/api", 10)
	iferr ! = nil { panic(err) } defer client.Close() node1 := &ServiceNode{"user"."127.0.0.1", 4000}
	node2 := &ServiceNode{"user"."127.0.0.1", 4001}
	node3 := &ServiceNode{"user"."127.0.0.1", 4002}
	iferr := client.Register(node1); err ! = nil { panic(err) }iferr := client.Register(node2); err ! = nil { panic(err) }iferr := client.Register(node3); err ! = nil { panic(err) } nodes, err := client.GetNodes("user")
	iferr ! = nil { panic(err) }for _, node := range nodes {
		fmt.Println(node.Host, node.Port)
	}
}
Copy the code

It is worth noting that the Close method must be called before the process exits, otherwise the ZooKeeper session will not be closed immediately, and the temporary node created by the server will not disappear immediately, but will be cleaned up by the server after timeout.

Read related articles, pay attention to the public account [code hole]