goroutines em containers

como não ter seu processamento interrompido

bianca rosa
staff platform engineer @ homeaglow
prev: lumos, redhat & stone

tudo é lindo quando criamos nossas primeiras apps em Go...

"opa que legal dá pra fazer coisa em background, mega simples, gostei....!!!!!"

package main

import (
	"fmt"
	"time"
)

const (
	N       = 10
	Seconds = 10
)

func main() {
	fmt.Println("Hello world")

	for i := 0; i < N; i++ {
		fmt.Printf("%d of %d: Scheduling go routine\n", i, N)
		go func(i int) {
			fmt.Printf("%d of %d: A slow running goroutine started....\n", i, N)
			time.Sleep(Seconds * time.Second)
			fmt.Printf("%d of %d: A slow running goroutine finished....\n", i, N)
		}(i)
	}
	fmt.Println("Everything has shut down, goodbye")
}

não tem o que dar errado, né?

Program execution

(...)

Program execution begins by initializing the main package and then invoking the function main. When that function invocation returns, the program exits. It does not wait for other (non-main) goroutines to complete.

https://go.dev/ref/spec#Program_execution

molezinha então

package main

import (
	"fmt"
	"time"
)

const (
	N       = 10
	Seconds = 10
)

func main() {
	fmt.Println("Hello world")

	// criacao das goroutines

	time.Sleep(Seconds * time.Second * 2)

	fmt.Println("Everything has shut down, goodbye")
}

funciona mazomenos né?

mas só porque a gente sabe o quanto esse programa vai durar

 

sync.WaitGroup

 

 (...)

"To wait for multiple goroutines to finish, we can use a wait group."

https://gobyexample.com/waitgroups​

var wg sync.WaitGroup

func main() {
	fmt.Println("Hello world")

	for i := 0; i < N; i++ {
		fmt.Printf("%d of %d: Scheduling go routine\n", i, N)
		wg.Add(1)
		go func(i int) {
			fmt.Printf("%d of %d: A slow running goroutine started....\n", i, N)
			time.Sleep(Seconds * time.Second)
			fmt.Printf("%d of %d: A slow running goroutine finished....\n", i, N)
			wg.Done()
		}(i)
	}
	
	wg.Wait()

	fmt.Println("Everything has shut down, goodbye")
}

k8s: termination grace period

apiVersion: apps/v1
kind: Deployment
metadata:
  name: shutting-down-gracefully
  labels:
      app: shutting-down-gracefully
spec:
  replicas: 1
  selector:
    matchLabels:
      app: shutting-down-gracefully
  template:
    metadata:
      labels:
        app: shutting-down-gracefully
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: shutting-down-gracefully
          image: docker.io/biancarosa/shutting-down-gracefully:v2.0.0

amazon ecs

[
  {
    "name": "string",
    "image": "string",
    "repositoryCredentials": {
      "credentialsParameter": "string"
    },
    "cpu": integer,
    "memory": integer,
    "memoryReservation": integer,
    ...
    "startTimeout": integer,
    "stopTimeout": integer,
	...
  }
]

excelente, é isso

mas no mundo real, sua aplicação (provavelmente) não é um script que roda do inicio ao fim

e aí, o que você faz?

espera terminar?
em um mundo aonde lead time importa?

ela provavelmente roda recebendo requisições, consumindo mensagens etc

6h não deveria acontecer mas a vida não é o toddynho que a gente toma (ou gostaria de tomar) todo dia de manhã

e como resolve?

Sinais do Sistema Operacional

Containers recebem sinais do SO quando estão sendo encerrados

  • SIGTERM - Avisa que o processo será encerrado
  • SIGKILL - Mata o processo imediatamente
  • SIGINT - Interrupções (ex: keyboard interrupt)

signal.Notify

Conseguimos "escutar" esse sinal do SO através de uma função da std lib do Go

A channel provides a mechanism for concurrently executing functions to communicate by sending and receiving values of a specified element type."

 

https://go.dev/ref/spec#Channel_types

channels

então quando seu SO envia esse sinal, o seu programa recebe um valor nesse channel 

package main

// imports, vars...

func main() {
	// criacao das goroutines

	gracefulStop := make(chan struct{})
	go func() {
		sigint := make(chan os.Signal, 1)
		signal.Notify(sigint, syscall.SIGINT, syscall.SIGTERM)
		<-sigint
		wg.Wait()
		fmt.Println("Shutting down gracefully...")
		close(gracefulStop)
	}()
	
	<-gracefulStop
	fmt.Println("Everything has shut down, goodbye")
}
gracefulStop := make(chan struct{})

go func() {
  sigint := make(chan os.Signal, 1)

  signal.Notify(sigint, syscall.SIGINT, syscall.SIGTERM)

  <-sigint

  // stop consuming messages
  // stop http server from receiving reqs

  wg.Wait() // or do something else

  fmt.Println("Shutting down gracefully...")
  close(gracefulStop)
}()

<-gracefulStop
fmt.Println("Everything has shut down, goodbye")

Estratégias de Persistência

quando o tempo de graceful shutdown não é suficiente

(ou seja, você não pode simplesmente esperar as tasks)

simples > complexo

qual é o mínimo que você precisa fazer?

uma das formas de garantir integridade é salvar o estado

existem várias outras formas

só cuidado com over-engineering

perguntas a se fazer

  • qual o tempo de processamento das suas goroutines?
     
  • custo vs beneficio pra colocar kafka / rabbitmq / sqs / similar
    • ​e se você fizer isso, você consegue se livrar de persistir estado?
       
  • suas goroutines executam tasks que podem ser reprocessadas?
    • se não, quais são as unidades atômicas da sua goroutine? 
      ​levar isso em consideração na hora de "reprocessar"

Recursos

Obrigada!

  

go essentials & descomplicando o go: linuxtips.io
 

goroutines em containers

By Bianca Rosa

goroutines em containers

  • 460