services • <10ms strict SLA • ~200 million requests internal to services per day • Serve ads < 300ms (includes bidding on different ad servers) • ~200 million events per day • ~140GB of data ingested
mechanism in our micro services (restricted by resources) • Most of data/configuration is stale for a short period • Helps in keeping latency low • Cores always used, RAM hardly utilized • Maps are not thread safe • How sharded structure helps • How spawning n goroutines for different shards is easy, can’t do in Python or any other language easily
implement an interface by implementing the methods • Decouples implementation from the package that defines it • Makes testing easier, just mock the interface
in the number of array slots causes nearly all keys to be remapped because the mapping between the keys and the slots is defined by a modular operation. • Special kind of hashing such that when a hash table is resized, only K/n keys need to be remapped on average, where K is the number of keys, and n is the number of slots. • Maps the key to the same server, irrespective of the number of nodes • Provides Horizontal Scaling
space partitioning • Decentralized, the nodes collectively form the system without any central coordination • Scalable, the system functions efficiently even with thousands or millions of nodes. • Fault Tolerant, the system is reliable (in some sense) even with nodes continuously joining, leaving, and failing • CAN, CHORD, PASTRY, TAPESTRY, KADEMLIA
peer-to-peer distributed hash table. • A DHT stores key-value pairs by assigning keys to different nodes; a node will store the values for all the keys for which it is responsible. • Chord specifies how keys are assigned to nodes, and how a node can discover the value for a given key by first locating the node responsible for that key • Chord is based on consistent hashing • Chose to implement CHORD because of it’s simplicity
struct { Id []byte // ID hash of (n + 2^i) mod (2^m) Node *internal.Node } func newFingerTable(node *internal.Node, m int) fingerTable { ft := make([]*fingerEntry, m) for i := range ft { ft[i] = newFingerEntry(fingerID(node.Id, i, m), node) } return ft } // newFingerEntry returns an allocated new finger entry with the attributes set func newFingerEntry(id []byte, node *internal.Node) *fingerEntry { return &fingerEntry{ Id: id, Node: node, } } // Computes the offset by (n + 2^i) mod (2^m) func fingerID(n []byte, i int, m int) []byte { // Convert the ID to a bigint idInt := (&big.Int{}).SetBytes(n) // Get the offset two := big.NewInt(2) offset := big.Int{} offset.Exp(two, big.NewInt(int64(i)), nil) // Sum sum := big.Int{} sum.Add(idInt, &offset) // Get the ceiling ceil := big.Int{} ceil.Exp(two, big.NewInt(int64(m)), nil) // Apply the mod idInt.Mod(&sum, &ceil) // Add together return idInt.Bytes() }
joined node n: 1) Initialize node n (the predecessor and the finger table). 2) Notify other nodes to update their predecessors and finger tables. 3) The new node takes over its responsible keys from its successor.
the finger table on the node to determine where the entry lies • The i{th} entry of node n will contain successor((n+2^{i-1}) mod 2^m) • Every time a node wants to look up a key k, it will pass the query to the closest successor or predecessor (depending on the finger table) of k in its finger table (the “largest” one on the circle whose ID is smaller than k), until a node finds out the key is stored in its immediate successor • O(logN) lookup time
background which updates finger tables and successor pointers. The stabilization protocol works as follows: 1) Stabilize(): n asks its successor for its predecessor p and decides whether p should be n‘s successor instead (this is the case if p recently joined the system). 2) Notify(): notifies n‘s successor of its existence, so it can change its predecessor to n 3) Fix_fingers(): updates finger tables 4) check_predecessor(): Periodically checks in predecessor is alive
change its predecessor pointer to our predecessor. // Do nothing if we are our own successor (i.e. we are the only node in the // ring). n.succMtx.RLock() succ := n.successor n.succMtx.RUnlock() n.predMtx.RLock() pred := n.predecessor n.predMtx.RUnlock() if n.Node.Addr != succ.Addr && pred != nil { n.moveKeysFromLocal(pred, succ) predErr := n.setPredecessorRPC(succ, pred) succErr := n.setSuccessorRPC(pred, succ) fmt.Println("stop errors: ", predErr, succErr) } n.transport.Stop() }