tutorial: GopherCon UK 2024 tutorial resources.
Signed-off-by: bwplotka <bwplotka@gmail.com>
This commit is contained in:
parent
4a89126c84
commit
8b36dcd0c3
|
@ -0,0 +1,6 @@
|
|||
# Use amd64 sha.
|
||||
FROM curlimages/curl@sha256:8addc281f0ea517409209f76832b6ddc2cabc3264feb1ebbec2a2521ffad24e4
|
||||
|
||||
COPY brokenapp /brokenapp
|
||||
CMD ["/brokenapp"]
|
||||
ENTRYPOINT ["/brokenapp"]
|
|
@ -0,0 +1,34 @@
|
|||
.PHONY: help
|
||||
help: ## Displays help.
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n\nTargets:\n"} /^[a-z0-9A-Z_-]+:.*?##/ { printf " \033[36m%-10s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST)
|
||||
|
||||
.PHONY: deploy
|
||||
deploy: ## init
|
||||
@bash setup-gke.sh bwplotka-stdtest
|
||||
|
||||
.PHONY: run
|
||||
run: ## run brokenapp
|
||||
@go run brokenapp.go -listen-address=":99"
|
||||
|
||||
THE_CHOSEN_POD=$(shell kubectl get pods -l 'app=brokenapp' -o jsonpath='{.items[*].metadata.name}' | sort | awk '{print $$3}')
|
||||
|
||||
.PHONY: port-forward
|
||||
port-forward: ## forward traffic to one pod
|
||||
@kubectl port-forward pod/$(THE_CHOSEN_POD) 9999
|
||||
|
||||
.PHONY: metrics
|
||||
metrics: ## get metrics from one pod
|
||||
@kubectl exec $(THE_CHOSEN_POD) -- curl -s http://localhost:9999/metrics
|
||||
|
||||
.PHONY: open-metrics
|
||||
open-metrics: ## get metrics from one pod
|
||||
@kubectl exec $(THE_CHOSEN_POD) -- curl -s -H 'Accept: application/openmetrics-text' http://localhost:9999/metrics
|
||||
|
||||
CASE=0
|
||||
.PHONY: break
|
||||
break: ## break one pod
|
||||
@kubectl exec $(THE_CHOSEN_POD) -- curl -s http://localhost:9999/break/$(CASE)
|
||||
|
||||
.PHONY: fix
|
||||
fix: ## fix one pod
|
||||
@kubectl exec $(THE_CHOSEN_POD) -- curl -s http://localhost:9999/fix/$(CASE)
|
|
@ -0,0 +1,3 @@
|
|||
# client_golang Tutorial: runtime wheel of misfortune
|
||||
|
||||
[Slides](https://docs.google.com/presentation/d/1blGS0rey7-nDWHAJrZnX_0kwbDuS0M3cnzDt3_ct9Ss/edit#slide=id.g2f3249de911_4_146)
|
|
@ -0,0 +1,132 @@
|
|||
// Copyright 2023 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
httppprof "net/http/pprof"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"github.com/efficientgo/core/errors"
|
||||
"github.com/oklog/run"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
var (
|
||||
addr = flag.String("listen-address", ":9999", "The address to listen on for HTTP requests.")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if err := runMain(*addr); err != nil {
|
||||
// Use %+v for github.com/efficientgo/core/errors error to print with stack.
|
||||
log.Fatalf("Error: %+v", errors.Wrapf(err, "%s", flag.Arg(0)))
|
||||
}
|
||||
}
|
||||
|
||||
func runMain(addr string) (err error) {
|
||||
// Create registry for Prometheus metrics.
|
||||
reg := prometheus.NewRegistry()
|
||||
reg.MustRegister(
|
||||
collectors.NewGoCollector(collectors.WithGoCollectorRuntimeMetrics( // Metrics from Go runtime.
|
||||
collectors.GoRuntimeMetricsRule{
|
||||
Matcher: regexp.MustCompile("/sched/latencies:seconds"), // One more recommended metric on top of the default.
|
||||
},
|
||||
)),
|
||||
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), // Metrics about the current UNIX process.
|
||||
)
|
||||
|
||||
m := http.NewServeMux()
|
||||
|
||||
// Create HTTP handler for Prometheus metrics.
|
||||
m.Handle("/metrics", promhttp.HandlerFor(
|
||||
reg,
|
||||
promhttp.HandlerOpts{EnableOpenMetrics: true},
|
||||
))
|
||||
|
||||
// Debug profiling endpoints.
|
||||
m.HandleFunc("/debug/pprof/", httppprof.Index)
|
||||
m.HandleFunc("/debug/pprof/cmdline", httppprof.Cmdline)
|
||||
m.HandleFunc("/debug/pprof/profile", httppprof.Profile)
|
||||
m.HandleFunc("/debug/pprof/symbol", httppprof.Symbol)
|
||||
|
||||
s := &scenarios{}
|
||||
m.HandleFunc("/break/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := s.SetFromParam(strings.TrimPrefix(r.URL.Path, "/break/"), true); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
})
|
||||
m.HandleFunc("/fix/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := s.SetFromParam(strings.TrimPrefix(r.URL.Path, "/fix/"), false); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
})
|
||||
|
||||
srv := http.Server{Addr: addr, Handler: m}
|
||||
g := &run.Group{}
|
||||
{
|
||||
g.Add(func() error {
|
||||
log.Println("Starting HTTP server", "addr", addr)
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
return errors.Wrap(err, "starting web server")
|
||||
}
|
||||
return nil
|
||||
}, func(error) {
|
||||
if err := srv.Close(); err != nil {
|
||||
log.Println("Error: Failed to stop web server", "err", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
g.Add(run.SignalHandler(context.Background(), syscall.SIGINT, syscall.SIGTERM))
|
||||
addContextNotCanceledGroup(g, reg, func() bool { return s.IsEnabled(contextNotCanceled) })
|
||||
addGoroutineJumpGroup(g, func() bool { return s.IsEnabled(goroutineJump) })
|
||||
return g.Run()
|
||||
}
|
||||
|
||||
func doOp(ctx context.Context) int64 {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(10)
|
||||
var sum int64
|
||||
for i := 0; i < 10; i++ {
|
||||
atomic.StoreInt64(&sum, int64(fib(ctx, 1e5)))
|
||||
wg.Done()
|
||||
}
|
||||
wg.Wait()
|
||||
return sum
|
||||
}
|
||||
|
||||
func fib(ctx context.Context, n int) int {
|
||||
if n <= 1 {
|
||||
return n
|
||||
}
|
||||
var n2, n1 = 0, 1
|
||||
for i := 2; i <= n; i++ {
|
||||
if ctx.Err() != nil {
|
||||
return -1
|
||||
}
|
||||
n2, n1 = n1, n1+n2
|
||||
}
|
||||
return n1
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: brokenapp
|
||||
labels:
|
||||
app: brokenapp
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: brokenapp
|
||||
replicas: 5
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: brokenapp
|
||||
spec:
|
||||
containers:
|
||||
- name: brokenapp
|
||||
imagePullPolicy: Always
|
||||
image: gcr.io/gpe-test-1/bwplotka-brokenapp:latest
|
||||
args:
|
||||
- "/app"
|
||||
- "-listen-address=:9999"
|
||||
ports:
|
||||
- containerPort: 9999
|
||||
name: metrics
|
||||
resources:
|
||||
limits:
|
||||
memory: 1G
|
||||
---
|
||||
apiVersion: monitoring.googleapis.com/v1
|
||||
kind: PodMonitoring
|
||||
metadata:
|
||||
name: brokenapp
|
||||
labels:
|
||||
app: brokenapp
|
||||
spec:
|
||||
endpoints:
|
||||
- port: metrics
|
||||
interval: 15s
|
||||
path: /metrics
|
||||
selector:
|
||||
matchLabels:
|
||||
app: brokenapp
|
|
@ -0,0 +1,23 @@
|
|||
module github.com/prometheus/client_golang/tutorials/runtime/wheelofmisfortune
|
||||
|
||||
go 1.21.4
|
||||
|
||||
require (
|
||||
github.com/efficientgo/core v1.0.0-rc.2
|
||||
github.com/oklog/run v1.1.0
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
)
|
||||
|
||||
replace github.com/prometheus/client_golang => github.com/prometheus/client_golang v1.19.1-0.20240811171649-aa3c00d2ee32
|
|
@ -0,0 +1,30 @@
|
|||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/efficientgo/core v1.0.0-rc.2 h1:7j62qHLnrZqO3V3UA0AqOGd5d5aXV3AX6m/NZBHp78I=
|
||||
github.com/efficientgo/core v1.0.0-rc.2/go.mod h1:FfGdkzWarkuzOlY04VY+bGfb1lWrjaL6x/GLcQ4vJps=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
|
||||
github.com/prometheus/client_golang v1.19.1-0.20240811171649-aa3c00d2ee32 h1:HKADPlynLA8q5KEF/f7KYo8pnyRFuvKz9f/QPfXs2nk=
|
||||
github.com/prometheus/client_golang v1.19.1-0.20240811171649-aa3c00d2ee32/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
|
@ -0,0 +1,150 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/oklog/run"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
type scenario int
|
||||
|
||||
const (
|
||||
contextNotCanceled scenario = 0
|
||||
goroutineJump scenario = 1
|
||||
)
|
||||
|
||||
type scenarios struct {
|
||||
enabled [2]bool
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (s *scenarios) SetFromParam(c string, v bool) error {
|
||||
if c == "" {
|
||||
return errors.New("no {case} parameter in path")
|
||||
}
|
||||
cN, err := strconv.Atoi(c)
|
||||
if err != nil {
|
||||
return errors.New("{case} is not a number")
|
||||
}
|
||||
if cN < 0 || cN >= len(s.enabled) {
|
||||
return fmt.Errorf("{case} should be a number from 0 to %d", len(s.enabled)-1)
|
||||
}
|
||||
s.set(scenario(cN), v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenarios) set(choice scenario, v bool) {
|
||||
s.mu.Lock()
|
||||
s.enabled[choice] = v
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *scenarios) IsEnabled(choice scenario) bool {
|
||||
s.mu.RLock()
|
||||
ret := s.enabled[choice]
|
||||
s.mu.RUnlock()
|
||||
return ret
|
||||
}
|
||||
|
||||
func addContextNotCanceledGroup(g *run.Group, reg *prometheus.Registry, shouldBreak func() bool) {
|
||||
// Create latency metric for our app operation.
|
||||
opLatency := promauto.With(reg).NewHistogram(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "brokenapp_operation_latency_seconds",
|
||||
Help: "Tracks the latencies for calls.",
|
||||
Buckets: []float64{0.01, 0.05, 0.1, 0.3, 0.6, 1, 3, 6, 9, 20},
|
||||
},
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Custom contexts can happen...
|
||||
// Without it, Go has many clever tricks to avoid extra goroutines per context
|
||||
// cancel setup or timers.
|
||||
ctx = withCustomContext(ctx)
|
||||
g.Add(func() error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
broken := shouldBreak()
|
||||
|
||||
// Do an operation.
|
||||
ctx, cancel := context.WithTimeout(ctx, 1*time.Hour)
|
||||
if broken {
|
||||
// Bug: Cancel will run until the end of this function... so until program
|
||||
// exit of timeout. This means we are leaking goroutines here with
|
||||
// all their allocated memory (and a bit of memory for defer).
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
ret := doOp(ctx)
|
||||
since := time.Since(start)
|
||||
opLatency.Observe(float64(since.Nanoseconds()) * 1e-9)
|
||||
|
||||
fmt.Println("10 * 1e5th fibonacci number is", ret, "; elapsed", since.String())
|
||||
|
||||
if !broken {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}, func(err error) {
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
|
||||
func addGoroutineJumpGroup(g *run.Group, shouldBreak func() bool) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
g.Add(func() error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(30 * time.Second):
|
||||
}
|
||||
|
||||
if !shouldBreak() {
|
||||
continue
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
done := make(chan struct{})
|
||||
|
||||
for i := 0; i < 300; i++ {
|
||||
time.Sleep(1 * time.Second)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
<-done
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
time.Sleep(30 * time.Second)
|
||||
close(done)
|
||||
wg.Wait()
|
||||
}
|
||||
}, func(err error) {
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
|
||||
type customCtx struct {
|
||||
context.Context
|
||||
}
|
||||
|
||||
func withCustomContext(ctx context.Context) context.Context {
|
||||
return customCtx{Context: ctx}
|
||||
}
|
||||
|
||||
func (c customCtx) Value(any) any {
|
||||
return nil // Noop to avoid optimizations to highlight the negative effect.
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
#!/bin/bash
|
||||
set -efo pipefail
|
||||
export SHELLOPTS # propagate set to children by default
|
||||
IFS=$'\t\n'
|
||||
|
||||
CLUSTER_NAME=$1
|
||||
if [ -z "${CLUSTER_NAME}" ]; then
|
||||
echo "cluster name is required as the first parameter!"
|
||||
fi
|
||||
|
||||
ZONE="us-central1-a"
|
||||
PROJECT_ID=$(gcloud config get project)
|
||||
|
||||
# Do nothing if cluster already exists.
|
||||
if gcloud container clusters list --filter="name: ${CLUSTER_NAME}" 2>&1 | grep -q "^${CLUSTER_NAME} "
|
||||
then
|
||||
echo "WARN: Cluster ${CLUSTER_NAME} already exists, skipping creation"
|
||||
gcloud container clusters get-credentials ${CLUSTER_NAME} --zone ${ZONE} --project ${PROJECT_ID}
|
||||
else
|
||||
# Start a new one-node cluster.
|
||||
# https://cloud.google.com/sdk/gcloud/reference/container/clusters/create
|
||||
# n2-standard-4 -- 4 vCPUs 16 GB
|
||||
gcloud container clusters create ${CLUSTER_NAME} \
|
||||
--project=${PROJECT_ID} \
|
||||
--location=${ZONE} \
|
||||
--workload-pool=${PROJECT_ID}.svc.id.goog \
|
||||
--release-channel=rapid \
|
||||
--num-nodes=1 \
|
||||
--machine-type="n2-standard-4"
|
||||
fi
|
||||
|
||||
CLUSTER_API_URL=$(kubectl config view --minify -o jsonpath="{.clusters[?(@.name == \"kind-${CLUSTER_NAME}\")].cluster.server}")
|
||||
echo "## Cluster is now running, kubectl should point to the new cluster at ${CLUSTER_API_URL}"
|
||||
kubectl cluster-info
|
||||
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./brokenapp
|
||||
CGO_ENABLED=0 go build -o ./brokenapp-mac
|
||||
|
||||
# Build image.
|
||||
docker buildx build --platform linux/amd64 --tag gcr.io/gpe-test-1/bwplotka-brokenapp:latest --load .
|
||||
|
||||
# Push image.
|
||||
docker push gcr.io/gpe-test-1/bwplotka-brokenapp:latest
|
||||
kubectl delete deployment brokenapp
|
||||
# Deploy.
|
||||
kubectl apply -f brokenapp.yaml
|
Loading…
Reference in New Issue