Generating Unique 64 bit IDs with Go on Kubernetes
November 6, 2017
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.