Generating Unique 64 bit IDs with Go on Kubernetes

Tin Rabzelj
06 November, 2017

cover

This article shows how to develop a service for generating globally unique IDs on a Kubernetes cluster. IDs will be generated similarly as with Twitter's Snowflake service, making them suitable for distributed systems where auto-incremental IDs fail and 128 bits for UUIDs is too inefficient. These IDs can also be "roughly" sorted by creation time, simply by sorting them lexicographically.

For the example given, IDs will be generated inside a container running on multiple pods, exposed by a REST API service. Ideally, this mechanism would be tightly coupled inside independent services, making it highly available and not needing any centralized coordination.

Getting started

Configure kubectl to use a running Kubernetes cluster, preferably with Minikube. Also install Docker.

Write the service

This example uses Sonyflake package, which works similarly to Twitter's Snowflake.

Create main.go file and initialize Sonyflake inside main function.

func main() {
	st := sonyflake.Settings{}
	st.MachineID = machineID
	sf := sonyflake.NewSonyflake(st)
	if sf == nil {
		log.Fatal("failed to initialize sonyflake")
  }
}

Write the machineID function, which generates a unique value by using the IP address provided with an environment variable MY_IP.

func machineID() (uint16, error) {
	ipStr := os.Getenv("MY_IP")
	if len(ipStr) == 0 {
		return 0, errors.New("'MY_IP' environment variable not set")
	}
	ip := net.ParseIP(ipStr)
	if len(ip) < 4 {
		return 0, errors.New("invalid IP")
	}
	return uint16(ip[2])<<8 + uint16(ip[3]), nil
}

Back in the main function, set up a router with Gin web framework. It handles only one endpoint, which returns generated ID. It's returned as string to ensure no data is lost if parsing the result in JavaScript.

r := gin.Default()
r.GET("/", func(c *gin.Context) {
  // Generate new ID
  id, err := sf.NextID()
  if err != nil {
    c.JSON(http.StatusInternalServerError, gin.H{
      "error": err.Error(),
    })
  } else {
    // Return ID as string
    c.JSON(http.StatusOK, gin.H{
      "id": fmt.Sprint(id),
    })
  }
})
if err := r.Run(":3000"); err != nil {
  log.Fatal("failed to run server: ", err)
}

Build Docker image

Create a Dockerfile file.

FROM golang:1.9.2

WORKDIR /go/src/app
# Copies only "main.go" file
COPY main.go .

RUN go-wrapper download
RUN go-wrapper install

EXPOSE 3000

CMD [ "app" ]

If you're using Minikube, switch to its Docker daemon, otherwise you'll have to push the image to some other registry.

$ eval $(minikube docker-env)

Build the image. Here it is named local/unique-id.

$ docker build -t local/unique-id .

Deploy to Kubernetes

Create a configuration deployment.yaml file for the Kubernetes Deployment object.

apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: unique-id
  labels:
    app: unique-id
spec:
  selector:
    matchLabels:
      app: unique-id
  replicas: 3
  template:
    metadata:
      labels:
        app: unique-id
    spec:
      containers:
      - name: unique-id
        image: local/unique-id
        imagePullPolicy: Never
        ports:
        - containerPort: 3000
        env:
        - name: MY_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP

The image field is set to local/unique-id and imagePullPolicy is set to Never, which makes Kubernetes use the local image from the Minikube's Docker registry. The MY_IP environment variable is set to pod's IP using Downward API.

Declare a service inside service.yaml, which targets pods labelled unique-id.

apiVersion: v1
kind: Service
metadata:
  name: unique-id
spec:
  type: LoadBalancer
  selector:
    app: unique-id
  ports:
  - port: 3000

Create resources.

$ kubectl create -f deployment.yaml
$ kubectl create -f service.yaml

Get the URL of the service.

$ minikube service unique-id --url

Finally, try calling it a couple of times.

$ curl $(minikube service unique-id --url)
{
  "id": "168514248039727104"
}

Wrapping up

Following this approach for each separate service that depends on globally unique IDs, allows for perfectly distributed generation of IDs. You can find slightly modified version of the example above on GitHub.

Newsletter

Get awesome articles delivered right to your doorstep

Protected by reCAPTCHA - Privacy - Terms

Comments

Sign in to post a comment