Grafana Dskit

repository·main·Indexed 20 days ago

https://github.com/grafana/dskit

A collection of utilities for building distributed services. It provides common abstractions for exponential backoff, caching (including Memcached), hedging, key-value storage (Consul, Etcd, Memberlist), RPC middleware, and a service lifecycle management model based on Google Guava. Additionally, it includes httpgrpc for bidirectional translation between HTTP and gRPC, and a span profiler for integrating CPU profiling with OpenTracing and Jaeger.

Tokens
9.7K
Snippets
30
Records
42
Agent score
66%

What's inside grafana-dskit

  1. Overview of Grafana Dskit utilities

    main

    Grafana Dskit is a library of utilities designed for building distributed services. It provides common patterns and implementations for reliability and service management, including:

    • Exponential backoff: For implementing retry logic.
    • Cache API: A common interface for caching, including a Memcached implementation.
    • Hedging: A mechanism to send duplicate requests to improve success rates and latency.
    • Key-Value API: A common interface for KV stores, with implementations for Consul, Etcd, and Memberlist.
    • RPC Middlewares: Tools for adding cross-cutting concerns like metrics and logging to RPC calls.
    • Services Model: A framework to manage the lifecycle (start-up and shut-down) of services.
  2. Overview of httpgrpc

    main
    The httpgrpc package provides a service and a client designed to embed HTTP requests and responses into a gRPC service. This allows for bidirectional translation between HTTP and gRPC, enabling you to use your preferred HTTP mux while benefiting from gRPC features such as protobuf encoding, HTTP/2, snappy compression, load balancing, persistent connections, and native Kubernetes load balancing.
  3. Manage multiple services with a Manager

    main

    A Manager (equivalent to Guava's ServiceManager) allows you to control a group of services as a single unit.

    Capabilities:

    • Initialize a Manager with a list of New services.
    • Start all services in the group and wait until they are all in the Running state (the "Healthy" state).
    • Stop the Manager, which triggers the shutdown of all managed services.
    • Determine if the Manager is "stopped" by checking if all its services have reached a terminal state (Terminated or Failed).
  4. How Span Profiler implements pprof labeling

    main

    When a span is eligible for profiling, the tracer sets span_id and span_name as pprof labels. These labels are stored in the goroutine's local storage and inherited by subsequent child goroutines.

    Querying Profiles

    • span_name is available as a regular label for query expressions. To find code not covered by traces, use: {service_name="my-service",span_name=""}.
    • Trace spans are identified by the pyroscope.profile.id attribute. This allows you to find specific spans in the trace view and fetch their associated profiles.

    Important: The presence of the pyroscope.profile.id attribute does not guarantee a profile is available; stack trace samples might not be collected if the CPU time used is below the sample interval (10ms).

    Profiler Initialization

    This module does not initialize the pprof profiler itself. You must initialize profiling using either the runtime/pprof package or the Pyroscope client.

  5. How the Service model works

    main

    The Service model is a Go implementation of the Google Guava service pattern. It provides well-defined, observable states for long-running tasks, allowing for asynchronous startup/shutdown and dependency management between services.

    Service States

    • New: The initial state after instantiation. The service is ready to be started.
    • Starting: Transitioned to via StartAsync. The service is performing initialization.
    • Running: The service has successfully initialized and is performing its primary work (e.g., responding to requests, background tasks).
    • Stopping: Transitioned to via StopAsync. The service is performing cleanup.
    • Terminated: A terminal state reached after successful cleanup in the Stopping state.
    • Failed: A terminal state reached if an error occurs during Starting, Running, or Stopping.

    Lifecycle Flow

    • Successful path: New $\rightarrow$ Starting $\rightarrow$ Running $\rightarrow$ Stopping $\rightarrow$ Terminated.
    • Failure path: Any error in Starting, Running, or Stopping transitions the service to Failed.
    • Note: Once in Terminated or Failed, a service cannot be restarted.
  6. How zone-aware routing works in Memberlist

    main

    Memberlist zone-aware routing is an optional feature designed to reduce cross-Availability Zone (AZ) data transfer costs. It works by categorizing nodes into two specific roles: member and bridge.

    Node Roles

    • member: A standard application instance. It only performs gossip and push/pull operations with other nodes (both members and bridges) located within the same zone. This keeps the majority of data traffic local to the zone.
    • bridge: A specialized instance that facilitates inter-zone communication. A bridge can communicate with nodes in its own zone and with bridges in other zones. It acts as the gateway that allows different zones to propagate messages to one another.

    Inter-zone Propagation Logic

    To ensure messages eventually reach all zones without flooding the network, bridges follow specific selection rules:

    • Broadcast messages: When selecting $N$ nodes to broadcast a message, a bridge will always select at least one node from the pool of bridges in other zones. The remaining $N-1$ nodes are selected randomly from the local zone and other-zone bridges.
    • Push/pull syncs: When a bridge initiates a sync, it always contacts a random bridge in another zone. If no bridges exist in other zones, it falls back to selecting a random node in its own zone.

    Note that node probes (health checks) may still cross AZ boundaries, but they represent a minimal fraction of total data transfer.

  7. Enable PROXY protocol support in a Server

    main

    To enable PROXY protocol support for both HTTP and gRPC servers, set ProxyProtocolEnabled to true in your Config object before initializing a Server using NewServer.

    Enabling this feature does not break existing setups; non-PROXY connections will still be accepted, though there is a small overhead added to connection handling. The implementation supports both PROXY v1 and PROXY v2 via the go-proxyproto library.

    cfg := &Config{
        ProxyProtocolEnabled: true,
        // ...
    }
    
    server := NewServer(cfg)
    // ...
  8. Build and run a local cluster using `Ring`

    main

    The local package provides an example for building a local cluster of multiple processes using the loopback interface. This allows you to simulate a distributed ring on a single machine by binding different processes to different loopback IP addresses.

    Steps to run a local cluster:

    1. Build the binary:

      go build local.go
    2. Start the first peer: Bind it to a specific loopback address:

      ./local -bindaddr=127.0.0.1
    3. Start additional peers: To join the existing ring, start a new process and use the -join-member flag to point to the address of an existing peer:

      ./local -bindaddr=127.0.0.2 -join-member=127.0.0.1
    4. Scale the cluster: You can continue starting peers with unique loopback bindaddr values and joining them to the cluster.

    Verifying the cluster

    • Ring Status Page: Access the ring page of any peer to see all members in the ring. For example: http://127.0.0.1:8100/ring.
    • Memberlist Status Page: Check the memberlist status at http://127.0.0.1:8100/kv.

    Using as a client

    You can interact with the ring information in client mode:

    ./local -mode=client
    # Build
    go build local.go
    
    # Start first peer
    ./local -bindaddr=127.0.0.1
    
    # Start second peer and join the first
    ./local -bindaddr=127.0.0.2 -join-member=127.0.0.1
    
    # Run in client mode
    ./local -mode=client
  9. Integrate Span Profiler by wrapping the Global Tracer

    main

    To automatically profile root spans (the initial local spans in a process), wrap your existing OpenTracing tracer using spanprofiler.NewTracer.

    This method is efficient because it selectively records profiles for root spans only. All stack trace samples accumulated during the execution of child spans contribute to the root span's profile. For example, an HTTP request results in a single profile regardless of how many spans are within that trace.

    Note: This approach only captures spans created within the same goroutine (or its children) as the parent. For asynchronous execution where context is passed to detached goroutines, you must use explicit profiling via spanprofiler.StartSpanFromContext.

    import (
        "github.com/opentracing/opentracing-go"
        "github.com/grafana/dskit/spanprofiler"
    )
    
    func main() {
        // Initialize your OpenTracing tracer
        tracer := opentracing.GlobalTracer()
        // Wrap it with the tracer-profiler 
        wrappedTracer := spanprofiler.NewTracer(tracer)
        // Use the wrapped tracer in your application
        opentracing.SetGlobalTracer(wrappedTracer)
    
        // Or, as an oneliner:
        // opentracing.SetGlobalTracer(spanprofiler.NewTracer(opentracing.GlobalTracer()))
    }
  10. Retrieve the original source address using PROXY protocol

    main

    When PROXY protocol is enabled, the server checks incoming connections for a PROXY header. If present, the connection information is updated to reflect the original source address.

    You can access this original IP address through the standard http.Request.RemoteAddr field. For example, you can use net.SplitHostPort to extract the IP from the remote address string.

    server.HTTP.HandleFunc("/your-endpoint", func(w http.ResponseWriter, r *http.Request) {
        ip, _, err := net.SplitHostPort(r.RemoteAddr)
        // ...
    })
  11. Implement a custom service

    main

    There are several ways to implement a Service depending on your requirements:

    1. NewService: The simplest method. You provide three functions: StartingFn, RunningFn, and StoppingFn. The service transitions through these states sequentially.
    2. NewIdleService: Use this if you need to run code during Starting or Stopping but do not need a long-running Running function (e.g., registering HTTP/gRPC handlers).
    3. NewTimerService: Use this for periodic tasks. It runs a supplied function at a defined time.Duration interval. If the function returns an error, the service fails.
    4. BasicService struct: For complex implementations, embed the BasicService struct into your own custom struct. This allows you to maintain internal state and provides access to ServiceContext().