layout: post
title: gophercon2018 心得整理
date: 2018-09-09 16:05 +0800
紀錄一些我覺得不錯的重點
From https://github.com/duffn/gophercon2018
Design Guidelines
Interface And Composition Design
Design Philosophy:
- Interfaces give programs structure.
- Interfaces encourage design by composition.
- Interfaces enable and enforce clean divisions between components.
- The standardization of interfaces can set clear and consistent expectations.
- Decoupling means reducing the dependencies between components and the types they use.
- This leads to correctness, quality and performance.
- Interfaces allow you to group concrete types by what they do.
- Don't group types by a common DNA but by a common behavior.
- Everyone can work together when we focus on what we do and not who we are.
- Interfaces help your code decouple itself from change.
- You must do your best to understand what could change and use interfaces to decouple.
- Interfaces with more than one method have more than one reason to change.
- Uncertainty about change is not a license to guess but a directive to STOP and learn more.
- You must distinguish between code that:
- defends against fraud vs protects against accidents
Validation:
Use an interface when:
- users of the API need to provide an implementation detail.
- API’s have multiple implementations they need to maintain internally.
- parts of the API that can change have been identified and require decoupling.
Don't use an interface:
- for the sake of using an interface.
- to generalize an algorithm.
- when users can declare their own interfaces.
- if it's not clear how the interface makes the code better.
Resources:
Methods, interfaces and Embedding - William Kennedy
Composition with Go - William Kennedy
Reducing type hierarchies - William Kennedy
Interface pollution in Go - Burcu Dogan
Application Focused API Design - William Kennedy
Avoid interface pollution - William Kennedy
Concurrent Software Design
Concurrency is about managing multiple things at once. Like one person washing the dishes while they are also cooking dinner. You're making progress on both but you're only ever doing one of those things at the same time. Parallelism is about doing multiple things at once. Like one person cooking and placing dirty dishes in the sink, while another washes the dishes. They are happening at the same time.
Both you and the runtime have a responsibility in managing the concurrency of the application. You are responsible for managing these three things when writing concurrent software:
Design Philosophy:
- The application must startup and shutdown with integrity.
- Know how and when every goroutine you create terminates.
- All goroutines you create should terminate before main returns.
- Applications should be capable of shutting down on demand, even under load, in a controlled way.
- You want to stop accepting new requests and finish the requests you have (load shedding).
- Identify and monitor critical points of back pressure that can exist inside your application.
- Channels, mutexes and atomic functions can create back pressure when goroutines are required to wait.
- A little back pressure is good, it means there is a good balance of concerns.
- A lot of back pressure is bad, it means things are imbalanced.
- Back pressure that is imbalanced will cause:
- Failures inside the software and across the entire platform.
- Your application to collapse, implode or freeze.
- Measuring back pressure is a way to measure the health of the application.
- Rate limit to prevent overwhelming back pressure inside your application.
- Every system has a breaking point, you must know what it is for your application.
- Applications should reject new requests as early as possible once they are overloaded.
- Don’t take in more work than you can reasonably work on at a time.
- Push back when you are at critical mass. Create your own external back pressure.
- Use an external system for rate limiting when it is reasonable and practical.
- Use timeouts to release the back pressure inside your application.
- No request or task is allowed to take forever.
- Identify how long users are willing to wait.
- Higher-level calls should tell lower-level calls how long they have to run.
- At the top level, the user should decide how long they are willing to wait.
- Use the
Context
package.
- Functions that users wait for should take a
Context
.
- These functions should select on <-ctx.Done() when they would otherwise block indefinitely.
- Set a timeout on a
Context
only when you have good reason to expect that a function's execution has a real time limit.
- Allow the upstream caller to decide when the
Context
should be canceled.
- Cancel a
Context
whenever the user abandons or explicitly aborts a call.
- Architect applications to:
- Identify problems when they are happening.
- Stop the bleeding.
- Return the system back to a normal state.
Channel Design
Channels allow goroutines to communicate with each other through the use of signaling semantics. Channels accomplish this signaling through the use of sending/receiving data or by identifying state changes on individual channels. Don't architect software with the idea of channels being a queue, focus on signaling and the semantics that simplify the orchestration required.
Language Mechanics:
- Use channels to orchestrate and coordinate goroutines.
- Focus on the signaling semantics and not the sharing of data.
- Signaling with data or without data.
- Question their use for synchronizing access to shared state.
- There are cases where channels can be simpler for this but initially question.
- Unbuffered channels:
- Receive happens before the Send.
- Benefit: 100% guarantee the signal has been received.
- Cost: Unknown latency on when the signal will be received.
- Buffered channels:
- Send happens before the Receive.
- Benefit: Reduce blocking latency between signaling.
- Cost: No guarantee when the signal has been received.
- The larger the buffer, the less guarantee.
- Buffer of 1 can give you one delayed send of guarantee.
- Closing channels:
- Close happens before the Receive. (like Buffered)
- Signaling without data.
- Perfect for signaling cancellations and deadlines.
- NIL channels:
- Send and Receive block.
- Turn off signaling
- Perfect for rate limiting or short term stoppages.
Design Philosophy:
Depending on the problem you are solving, you may require different channel semantics. Depending on the semantics you need, different architectural choices must be taken.
- If any given Send on a channel
CAN
cause the sending goroutine to block:
- Not allowed to use a Buffered channel larger than 1.
- Buffers larger than 1 must have reason/measurements.
- Must know what happens when the sending goroutine blocks.
- If any given Send on a channel
WONT
cause the sending goroutine to block:
- You have the exact number of buffers for each send.
- You have the buffer measured for max capacity.
- Less is more with buffers.
- Don’t think about performance when thinking about buffers.
- Buffers can help to reduce blocking latency between signaling.
- Reducing blocking latency towards zero does not necessarily mean better throughput.
- If a buffer of one is giving you good enough throughput then keep it.
- Question buffers that are larger than one and measure for size.
- Find the smallest buffer possible that provides good enough throughput.
From: https://github.com/ardanlabs/gotraining/blob/master/topics/go/README.md
Scheduler
切換時機
- goroutine creation
- goroutine blocking
- blocking system call
key point
- reuse threads
- limit goroutine running threads to number of CPU cores
- distributed runqueues with stealing and handoff
- handoff: no parked threads -> starts a new thread
- cooperative preemption
- global runqueue as lower priority runqueue
thread spinning
- check global runqueue
- poll the network
- attempt to run gc tasks
- work-steal
limitations
- FIFO runqueues -> no priority
- No strong preemption -> no fairness or latency guarantees
- no real locality -> cache miss
From: https://speakerdeck.com/kavya719/the-scheduler-saga
Patterns
DONT use callback:
func Fetch(name string, f func(string)) {
go func() {
...
f(s)
} ()
}
DO use Future:
func Fetch(name string) <-chan string {
ch := make(chan string, 1)
go func() {
...
c <- s
} ()
return c
}
func main() {
a := Fetch("a")
b := Fetch("b")
consume(<-a, <-b)
}
DO use producer-consumer queue:
func Glob(pat string) <-chan string {
ch := make(chan string)
go func() {
defer close(ch)
for ... {
...
ch <- s
}
}
return ch
}
func main() {
for s := range Glob("a*") {
...
}
}
- hard to handle error, so
- write sync API, let caller site handles concurrency.
- make concurrency an internal detail.
From: https://drive.google.com/file/d/1nPdvhB0PutEJzdCq5ms6UI58dp50fcAN/view
Allocation
- heap is divided into
- arenas (64MB each)
- spans
- flexible
- fixed size for small object(<= 32KB)
- maintain local caches
- GC is concurrent, but write barriers and mark assists can slow a program
curl localhost:6060/debug/pprof/trace?seconds=5 > trace.out
go tool trace trace.out
memory allocation analysis
https://github.com/loov/view-annotated-file
reduce allocation times because we need to do on every allocation:
- prevent preempted
- check assist GC
- find the next free slot
- set heap bitmap bits
recycle allocation: sync.Pool
- maintains slices of recycled objects
- allows lockfree get/put
- remember to clear recycled memory by ourselves
From: https://speakerdeck.com/emfree/allocator-wrestling