Building DNS-Based Service Discovery in Go Using SRV Records (Inspired by Kubernetes)
Understanding Kubernetes DNS and SRV-Based Service Discovery#
I have been reading about Kubernetes DNS and going through its official specification available on GitHub Kubernetes DNS specification
After studying the documentation, I understood how Kubernetes performs internal service discovery using DNS. This led me to an interesting idea.
Can I build my own client-server application where the client only resolves a service name and protocol (like _servicename._tcp or _servicename._udp), and automatically retrieves the port from DNS, without hardcoding the server address and port number?
The answer is yes, using SRV records.
Deep Dive into SRV Records (RFC 2782)#
To understand this better, I read the official SRV RFC 2782.
From RFC 2782, I learned that SRV records contain:
- Priority
- Weight
- Port
- Target (hostname)
The format:
_service._proto.domain TTL IN SRV priority weight port target
How Priority Works#
- Lower number = higher priority
- Clients MUST try the lowest priority first
- Higher priority numbers act as failover
Example:
_test._udp.0xmm.in. 1800 IN SRV 9 25 8888 hello3.0xmm.in.
_test._udp.0xmm.in. 1800 IN SRV 10 10 8888 hello1.0xmm.in.
_test._udp.0xmm.in. 1800 IN SRV 20 15 8888 hello2.0xmm.in.
In this case:
- Priority
9is preferred over10 - Priority
10is preferred over20
If hello3 is unreachable, the client moves to priority 10.
This provides DNS-based failover.
How Weight Works#
Weight is used only among records with the same priority.
Higher weight = higher probability of being selected.
Example:
_test._udp.0xmm.in. 1800 IN SRV 10 5 8888 hello1.0xmm.in.
_test._udp.0xmm.in. 1800 IN SRV 10 15 8888 hello2.0xmm.in.
Here:
- Both have priority
10 - Weight
15has higher selection probability than weight5
Selection is done proportionally:
hello1 → 5/(10+5) = 25%
hello2 → 15/(10+15) = 75%
This enables DNS-based load balancing.
Special Case: Weight = 0#
From RFC 2782:
Domain administrators SHOULD use Weight 0 when there isn’t any server selection to do. In the presence of records containing weights greater than 0, records with weight 0 should have a very small chance of being selected.
Building DNS-Based Load Balancing#
Using priority and weight together, we can design failover systemand load balancing mechanism
Example:
_test._udp.0xmm.in. 1800 IN SRV 9 25 8888 hello3.0xmm.in.
_test._udp.0xmm.in. 1800 IN SRV 10 10 8888 hello1.0xmm.in.
_test._udp.0xmm.in. 1800 IN SRV 20 15 8888 hello2.0xmm.in.
Behavior:
- Client selects priority
9 - If unreachable → try priority
10 - If unreachable → try priority
20
This creates automatic failover logic at the DNS level.
Retrieving Port and Target#
package main
import (
"fmt"
"net"
)
func main() {
_, addrs, err := net.LookupSRV("test", "udp", "0xmm.in")
if err != nil {
fmt.Println("Error:", err)
return
}
for _, a := range addrs {
fmt.Printf("host=%s port=%d\n", a.Target, a.Port)
}
}
Example output:
host=hello2.0xmm.in. port=8888
host=hello1.0xmm.in. port=8888
above can be same used for tcp based service/protocol
net.LookupSRV():
- Returns SRV records sorted by priority
- Does NOT automatically perform weighted random selection
- Does NOT automatically perform failover
- Your application must implement connection retry logic
Final thought#
By using SRV records:
- No need to hardcode server address
- No need to hardcode port number
:wq for now, until next time.