65. Custom gRPC Name Resolver
title: 65. Custom gRPC Name Resolver: Optimizing Service Discovery Your Own Way
date: 2024-06-11
tags: [golang, grpc, networking, distributed systems, service discovery]
gRPC has long been a go-to choice for developers building fast, lightweight, and easy-to-maintain microservices. One crucial component that often gets overlooked is the Name Resolver. By default, gRPC ships with several resolvers such as dns:// or passthrough://, but the needs of modern distributed environments — like custom orchestrators, service meshes, or hybrid setups — frequently call for a “custom” touch. The problem is, very few engineers truly understand how powerful and flexible it is to build your own Custom Name Resolver.
In this article, I’ll take a deeper look at the Custom gRPC Name Resolver — covering its architecture, workflow, a simple implementation, and its use cases in real-world systems.
Why Do You Need a Custom Name Resolver?
When working with microservices, the way services discover each other’s endpoints (service discovery) is life and death. In the context of gRPC, a target endpoint like dns:///service.mydomain.com is processed by a name resolver. But what if a service’s source comes from a unique registry, a local file, or even a bespoke internal API? This is exactly where a Custom Name Resolver comes in.
Some common scenarios:
- Integration with an internal registry (HashiCorp Consul, ETCD, etc.)
- Dynamic microservices on Kubernetes with custom labels/filters
- Service discovery through a specialized API Gateway that doesn’t natively support DNS A/AAAA records
How Name Resolution Works in gRPC
Let’s break down a simple diagram of the name resolution process in gRPC.
sequenceDiagram
participant Client
participant gRPC Library
participant NameResolver
participant LoadBalancer
Client->>gRPC Library: Dial ("myres://user-service/id-1234")
gRPC Library->>NameResolver: Build("myres")
NameResolver->>gRPC Library: UpdateState(Resolved addresses)
gRPC Library->>LoadBalancer: Pass endpoint list
LoadBalancer->>Client: Ready to RPC!
Summary:
- The client initiates a connection with a specific target via
proto://target - gRPC selects a resolver based on the schema (proto)
- The Name Resolver fetches and computes the endpoints from the custom source
- It updates the state in the gRPC core to be passed along to the load balancer and dial the RPC
Anatomy of a Name Resolver in gRPC-Go
A name resolver is an implementation of the following interface (simplified):
1type Builder interface {
2 Scheme() string
3 Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
4}
5
6type Resolver interface {
7 ResolveNow(ResolveNowOptions)
8 Close()
9}- Builder: The initial registration that defines the resolver’s “scheme”
- Resolver: The process of fetching/monitoring the backend against the endpoint source
Case Study: A Custom In-Memory Resolver
Let’s say we have a service discovery mechanism based on a local YAML file.
File: services.yaml
1user-service:
2 - 10.0.0.2:50051
3 - 10.0.0.3:50051
4order-service:
5 - 10.0.0.4:50052Step 1: Define the Resolver Scheme
1const scheme = "yamlmem"
2
3func init() {
4 resolver.Register(&yamlMemBuilder{})
5}
6
7type yamlMemBuilder struct{}
8
9func (b *yamlMemBuilder) Scheme() string { return scheme }
10
11func (b *yamlMemBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
12 // Fetch endpoints from YAML based on target.Endpoint
13 addrs := resolveFromYaml(target.Endpoint)
14 cc.UpdateState(resolver.State{Addresses: addrs})
15 return &yamlMemResolver{cc: cc}, nil
16}Step 2: Process the YAML Resolution
1func resolveFromYaml(endpoint string) []resolver.Address {
2 // 1. Load the YAML file
3 // 2. Parse the endpoint
4 // 3. Return []resolver.Address in host:port format
5 // Here I'm simplifying with hardcoded values
6 if endpoint == "user-service" {
7 return []resolver.Address{
8 {Addr: "10.0.0.2:50051"},
9 {Addr: "10.0.0.3:50051"},
10 }
11 }
12 return nil
13}
14
15type yamlMemResolver struct{ cc resolver.ClientConn }
16
17func (r *yamlMemResolver) ResolveNow(o resolver.ResolveNowOptions) {}
18func (r *yamlMemResolver) Close() {}Step 3: Dial Using the Custom Scheme
1conn, err := grpc.Dial("yamlmem:///user-service", grpc.WithInsecure())
2if err != nil {
3 log.Fatalf("dial failed: %v", err)
4}With this pattern, every time we run grpc.Dial with the yamlmem:// scheme, the custom resolver will automatically fetch and refresh the endpoints from the YAML file.
Simulated Results: Resolution Table
| Client Dial Target | gRPC Scheme Resolver | Endpoints Obtained |
|---|---|---|
yamlmem:///user-service | yamlmem | 10.0.0.2:50051, 10.0.0.3:50051 |
yamlmem:///order-service | yamlmem | 10.0.0.4:50052 |
dns:///api.example.org | dns |
Going Further
a. Live Updates
Of course, in the real world the resolver will monitor a source (a file, registry, REST API) and call cc.UpdateState() whenever there’s a change.
1go func() {
2 for changes := range watchServiceChange() {
3 cc.UpdateState(resolver.State{Addresses: changes})
4 }
5}()b. Error Handling
cc.ReportError(err) can be used when the resolve process fails. This sends feedback back to the client/error balancer.
c. Advanced Use Case: A Resolver for Service Mesh Labels
Imagine you only want services labeled blue or canary to be resolvable. A custom resolver can filter the endpoints before updating the state.
When Should You Build a Custom Resolver?
| Use Case | Need Custom? | Reason |
|---|---|---|
| Ordinary DNS | No | The built-in resolver is sufficient |
| Loading from your own file/registry | Yes | Requires non-standard access logic |
| Connecting to an external API | Yes | The endpoint data source isn’t DNS-based |
| Filtering based on business logic | Yes | Filtering/logic lives only on the application side |
| A single static host | No | You can use passthrough:// |
Conclusion
The ability to create a Custom gRPC Name Resolver can be a superpower for engineers who want microservices that are more flexible, scalable, and tailored to modern service distribution needs. Whether it’s an internal registry, a service mesh, or a hybrid environment — with a custom resolver, we can define our own way of doing service discovery, without having to wait for a provider or “hack” DNS.
References & Tools
Leave a comment if you have any ideas or questions about unique resolvers or service discovery integrations at your company 👍
Follow me for more articles on distributed systems & scalable microservices!