Cloud Native Go

created : Wed, 02 Nov 2022 00:20:40 +0900
modified : Sun, 05 Mar 2023 19:41:57 +0900
go

누가 이 책을 읽어야하는가

Part 1. Going Cloud Native

Chapter 1. What is a “Cloud Native” Application?

What Is Cloud Native?

Chapter 2. Why Go Rules the Cloud Native World

The Motivation Behind Go

Features for a Cloud Native World

Part II. Cloud Native Go Constructs

Chapter 3. Go Language Foundations

Chapter 4. Cloud Native Patterns

The Context Package

type Context interface {
  // Done returns a channel that's closed when this Context is canceeled.
  Done() <-chan struct{}

  // Err indicates why this context was cancelled after the Done channel is
  // closed. If Done is not yet closed, Err returns nil.
  Err() error

  // Deadline returns the time when this Context should be cancelled; it
  // returns ok==false if no deadline is set.
  Deadline() (daedline time.Time, ok bool)

  // Value returns the value associated with this context for key, or nil
  // if no value is associated with key. Use with care.
  Value(key interface{}) interface{}
}

Stability Patterns

Circuit Breaker

Debounce

Retry

Throttle

Timeout

Concurrency Patterns

Fan-In

Fan-Out

Future

Sharding

Chapter 5. Building a Cloud Native Service

Building an HTTP Server with net/http

type Handler interface {
  ServeHTTP(ResponseWriter, *Request)
}

Building an HTTP Server with gorilla/mux

r := mux.NewRouter()
r.HandleFunc("/products/{key}", ProductHandler)
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)

vars := mux.Vars(request)
category := vars["category"]

Chapter 6. It’s All about Dependability

The Continuing Relevance of the Twelve-Factor App

Twelve-Factor App

Chatpr 7. Scalability

The Four Common Bottlenecks

Advantages of Statelessness

Scaling Postponed: Efficiency

The pros and cons of serverlessness

Chapter 8. Loose Coupling

Tight Coupling

Communications Between Services

Request-Response Messging

package main

import (
  "fmt"
  "io"
  "net/http"
  "strings"
)

const json = `{ "name":"Matt", "age": 44}`

func main() {
  in := strings.NewReader(json)

  resp, err := http.Post("http://example.com/upload", "text/json", in)
  if err != nil {
    panic(err)
  }
  defer resp.Body.Close()

  message, err := io.ReadAll(resp.Body)
  if err != nil {
    panic(err)
  }

  fmt.Printf(string(message))
}

Loose Coupling Local Resources with Plug-ins

Hexagonal Architecture

The Architecture

Chapter 9. Resilience

Cascading Failures

type Effector func(context.Context) (string, error)
type Throttled func(context.Context, string) (bool, string, error)

type bucket struct {
  tokens uint
  time   time.Time
}

func Throttle(e Effector, max uint, refill uint, d time.Duration) Throttled {
  buckets := map[string]*bucket{}

  return func(ctx context.Context, uid string) (bool, string, error) {
    b := buckets[uid]

    if b == nil {
      buckets[uid] = &bucket{
        tokens: max -1,
        time: time.Now(),
      }

      str, err := e(ctx)
      return true, str, err
    }

    refillInterval := uint(time.Since(b.time) / d)
    tokensAdded := refill * refillInterval
    currentTokens := b.tokens + tokensAdded

    if currentTokens < 1 {
      return false, "", nil
    }

    if currentTokens > max {
      b.time = time.Now()
      b.tokens = max - 1
    } else {
      deltaTokens := currentTokens - b.tokens
      deltaRefills := deltaTokens / refill
      deltaTime := time.Duration(deltaRefills) * d

      b.time = b.time.Add(deltaTime)
      b.tokens = currentTokens - 1
    }

    str, err := e(ctx)

    return true, str, err
  }
}
var throttled = Throttle(getHostname, 1, 1, time.Second)

func getHostname(ctx context.Context) (string, error) {
  if ctx.Err() != nil {
    return "", ctx.Err()
  }

  return os.Hostname()
}

func throttledHandler(w http.ResposneWriter, r *http.Request) {
  ok, hostname, err := throttled(r.Context(), r.RemoteAddr)

  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
  }

  if !ok {
    http.Error(w, "Too many requests", http.StatusTooManyRequests)
    return
  }

  w.WriteHeader(http.StatusOK)
  w.Write([]byte(hostname))
}

func main() {
  r := mux.NewRouter()
  r.HandleFunc("/hostname", throttledHandler)
  log.Fatal(http.ListenAndServe(":8080", r))
}
const MaxQueueDepth = 1000

func loadSheddingMiddleware(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    if CurrentQueueDepth() > MaxQueueDepth {
      log.Println("load shedding engaged")

      http.Error(w,
        err.Error(),
        http.StatusServiceUnavailable)
      return
    }

    next.ServeHTTP(w, r)
  })
}

func main() {
  r := mux.NewRouter()

  r.Use(loadSheddingMiddleware)

  log.Fatal(http.ListenAndServe(":8080", r))
}

Play It Again: Retrying Requests

Backoff Algorithms

res, err := SendRequest()
base, cap := time.Second, time.Minute

for backoff := base; err != nil; backoff <<= 1 {
  if backoff > cap {
    backoff = cap
  }
  time.Sleep(backoff)
  res, err = SendRequest()
}
res, err := SendRequest()
base, cap := time.Second, time.Minute

for backoff := base; err != nil; backoff <<= 1 {
  if backoff > cap {
    backoff = cap
  }

  jitter := rand.Int63n(int64(backoff * 3))
  sleep := base + time.Duration(jitter)
  time.Sleep(sleep)
  res, err = SendRequest()
}

Circuit Breaking

Timeouts

func UserName(ctx context.Context, id int) (string, error) {
  const query = "SELECT username FROM users WHERE id=?"

  dctx, cancel := context.WithTimeout(ctx, 15 * time.Second)
  defer cancel()

  var username string
  err := db.QueryRowContext(dctx, query, id.Scan(&username))

  return username, err
}

func UserGetHandler(w http.ResponseWriter, r *http.Request) {
  vars := mux.Vars(r)
  id := vars["id"]

  rctx := r.Context()

  ctx, cancel := context.WithTimeout(rctx, 10*time.Second)
  defer cancel()

  username, err := UserName(ctx, id)

  switch {
  case errors.Is(err, sql.ErrNoRows):
    http.Error(w, "no such user", http.StatusNotFound)
  case errors.Is(err, context.DeadlineExceeded):
    http.Error(w, "database timeout", http.StatusGatewayTimeout)
  case err != nil:
    http.Error(w, err.Error(), http.StatusInternalServerError)
  default:
    w.Write([]byte(username))
  }
}

Idempotence

Service Redundancy

Designing for Redundancy

Autoscaling

Healthy Health Checks

Chapter 10. Manageability

What Is Manageability and Why Should I Care?

Configuring Your Application

Configuration Good Practice

Configuring with Environment Variables

name := os.Getenv("NAME")
place := os.Getenv("CITY")

fmt.Printf("%s lives in %s.\n", name, place)

if val, ok := os.LookupEnv(key); ok {
  fmt.Printf("%s=%s\n", key, val)
} else {
  fmt.Printf("%s not set\n", key)
}

Configurint with Command-Line Arguments

pakcage main

import (
  "flag"
  "fmt"
)

func main() {
  strp := flag.String("string", "foo", "a string")

  intp := flag.Int("number", 42, "an integer")
  boolp := flag.Bool("boolean", false, "a boolean")

  flag.Parse()

  fmt.Println("string:", *strp)
  fmt.Println("integer:", *intp)
  fmt.Println("boolean:", *boolp)
  fmt.Println("args:", flag.Args())
}
package main

import (
  "fmt"
  "os"
  "github.com/spf13/cobra"
)

var strp string
var intp int
var boolp bool

var rootCmd = &cobra.Command{
  Use: "flags",
  Long: "A simple flags experimentation command, built with Cobra.",
  Run: flagsFunc,
}

func init() {
  rootCmd.Flags().StringVarP(&strp, "string", "s", "foo", "a string")
  rootCmd.Flags().IntVarP(&intp, "number", "n", 42, "an integer")
  rootCmd.Flags().BoolVarP(&boolp, "boolean", "b", false, "a boolean")
}

func flagsFunc(cmd *cobra.Command, args []string) {
  fmt.Println("string:", strp)
  fmt.Println("integer:", intp)
  fmt.Println("boolean:", boolp)
  fmt.Println("args:", args)
}

func main() {
  if err := rootCmd.Execute(); err != nil {
    fmt.Println(err)
    os.Exit(1)
  }
}
func loadConfiguration(filepath string) (Config, error) {
  dat, err := ioutil.ReadFile(filepath)
  if err != nil {
    return Config{}, err
  }

  config := Config{}

  err = yaml.Unmarshal(dat, &config)
  if err != nil {
    return Config{}, err
  }

  return config, nil
}

func startListening(update <- chan string, errors <- chan err) {
  for {
    select {
    case filepath := <-updates:
      c, err := loadConfiguration(filepath)
      if err != nil {
        log.Println("error loading config:", err)
        continue
      }
      config = c

    case err := <-errors:
      log.Println("error watching config:", err)
    }
  }
}

func init() {
  updates, errors, err := watchConfig("config.yaml")
  if err != nil {
    panic(err)
  }

  go startListening(updates, errors)
}

func calculateFileHash(filepath string) (string, error) {
  file, err := os.Open(filepath)
  if err != nil {
    return "", err
  }
  defer file.Close()

  hash := sha256.New()

  if _, err := io.Copy(hash, file); err != nil {
    return "", err
  }

  sum := fmt.Sprintf("%x", hash.Sum(nil))

  return sum, nil
}

func watchConfig(filepath string) (<- chan string, <- chan error, error) {
  errs := make(chan error)
  changes := make(chan string)
  hash := ""

  go func() {
    ticker := time.NewTicker(time.Second)

    for range ticker.C {
      newhash, err := calculateFileHash(filepath)
      if err != nil {
        errs <- err
        continue
      }
    }

    if hash != newhash {
      hash = newhash
      changes <- filepath
    }
  }()

  return changes, errs, nil
}
func watchConfigNotify(filepath string) (<-chan string, <- chan error, error) {
  changes := make(chan string)

  watcher, err := fsnotify.NewWatcher()
  if err != nil {
    return nil, nil, err
  }

  err = watcher.Add(filepath)
  if err != nil {
    return nil, nil, err
  }

  go func() {
    changes <- filepath

    for event := range watcher.Events {
      if event.Op&fsnotify.Write == fsnotify.Write {
        changes <- event.Name
      }
    }
  }()

  return changes, watcher.Errors, nil
}
viper.Set("Verbose", true)
viper.Set("LogFile", LogFile)

var rootCmd = &cobra.Command{ /* */ }

func init() {
  rootCmd.Flags().IntP("number", "n", 42, "an integer")
  viper.BindPFlag("number", rootCmd.Flags().Lookup("number"))
}

n := viper.GetInt("number")
viper.BindEnv("id")
viper.BindEnv("port", "SERVICE_PORT")

id := viper.GetInt("id")
port := viper.GetInt("port")

viper.SetConfigName("config")
viper.SetConfigType("yaml")

viper.AddConfigPath("/etc/service/")
viper.AddConfigPath("$HOME/.service")
viper.AddConfigPath(".")

if err := viper.ReadInConfig(); err != nil {
  panic(fmt.Errorf("fatal error reading config: %w", err))
}

viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
  fmt.Println("Config file changed:", e.Name)
})

Feature Management with Feature Flags

Chapter 11. Observability

The Three Pillars of Observability

OpenTelemetry

The OpenTelemetry Components

Tracing Concepts

Tracing with OpenTelemetry

stdExporter, err := stdout.NewExporter(
  stdout.WithPrettyPrint(),
)
jaegerEndpoint := "http://localhost:14268/api/traces"
serviceName := "fibonacci"

jaegerExporter, err := jaeger.NewRawExporter(
  jaeger.WithCollectorEndpoint(jaegerEndpoint),
  jaeger.WithProcess(jaeger.Process{
    ServiceName: serviceName,
  }),
)

Push Versus Pull Metric Collection

Logging

package main

import (
  "fmt"

  "go.uber.org/zap"
  "go.uber.org/zap/zapcore"
)

func init() {
  cfg := zap.newDevelopmentConfig()
  cfg.EncoderConfig.TimeKey = ""
  cfg.Sampling = &zap.SamplingConfig{
    Initial: 3,
    Thereafter: 3,
    Hook: func(e zapcore.Entry, d zapcore.SamplingDecision) {
      if d == zapcore.LogDropped {
        fmt.Println("event dropped...")
      }
    },
  }

  logger, _ := cfg.Build()
  zap.ReplaceGlobals(logger)
}