Go 進階學習筆記

chenlixin發表於2021-06-06
[TOC]

課程筆記(VIP)

基礎

gobyexample.com/

Error

Error vs Exception

Error Type

Sentinel Error 預定義錯誤值

Sentinel errors 成為你 API 公共部分。

Sentinel errors 在兩個包之間建立了依賴。

結論: 儘可能避免 sentinel errors。

Error types 錯誤型別

type MyError struct {
    Msg string
    File string
    Line int
}

func (e *MyError) Error() string {
    return fmt.Sprintf("%s:%d: %s", e.File, e.Line, e.Msg)
}

func test() error {
    return &MyError{"Something happened", "server.go", 42}
}

func main() {
    err := test()
    switch err := err.(type) {
    case nil:
        // nothing
    case *MyError:
        fmt.Println("error occurred on line:", err.Line)
    default:
        // unknown
    }
}

呼叫者要使用型別斷言和型別 switch,就要讓自定義的 error 變為 public。這種模型會導致和呼叫者產生強耦合,從而導致 API 變得脆弱。

結論是儘量避免使用 error types,雖然錯誤型別比 sentinel errors 更好,因為它們可以捕獲關於出錯的更多上下文,但是 error types 共享 error values 許多相同的問題。

因此,我的建議是避免錯誤型別,或者至少避免將它們作為公共 API 的一部分。

Opaque errors 不透明錯誤處理

inport "github.com/quux/bar"

func fn error {
    x, err := bar.Foo()
    if err != nil {
        return err
    }
}

Handing Error

Indented flow is for errors 無錯誤的正常流程程式碼,將成為一條直線,而不是縮緊的程式碼

f, err := os.Open(path)
if err != nil {
    // handle error
}
// do stuff

Eliminate error handling by eliminating errors

下面程式碼有啥問題?

// 無意義的判斷
func AuthenticateRequest(r *Request) error {
    err := authenticate(r.User)
    if err != nil {
        return err
    }
    return nil
}

func AuthenticateRequest(r *Request) error {
    return authenticate(r.User)
}

統計 io.Reader 讀取內容的行數

func CountLines(r io.Reader) (int, error) {
    var (
        br = bufio.MewReader(r)
        lines int
        err error
    )

    for {
        _, err = br.ReadString('\n')
        lines++
        if err != nil {
            break
        }
    }

    if err != io.EOF {
        return 0, err
    }

    return lines, nil
}

改進版本
func CountLines(r io.Reader) (int, error) {
    sc := bufio.NewScanner(r)
    lines := 0

    for sc.Scan() {
        lines++
    }

    return lines, sc.Err()
}

//

type Header struct {
    Key, Value string
}

type Status struct {
    Code int
    Reason string
}

func WriteResponse(w io.Writer, st Status, headers []Header, body io.reader) error {
    _, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
    if err != nil {
        return err
    }

    for _, h := range headers {
        _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)
        if err != nil {
            return err
        }
    }

    if _, err := fmt.Fprint(w, "\r\n"); err != nil {
        return err
    }

    _, err = io.Copy(w, body)
    return err
}

// 改進版本
type errWriter struct {
    io.Writer
    err error
}

func (e *errWriter) Write(buf []byte) (int, error) {
    if e.err != nil {
        return 0, e.err
    }

    var n int
    n, e.err = e.Writer.Write(buf)
    return n, nil
}

func WriteResponse(w io.Writer, st Status, headers []Header, body io.reader) error {
    ew := &errWriter{Writer: w}
    fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)

    for _, h := range headers {
        fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)
    }

    fmt.Fprint(ew, "\r\n")
    io.Copy(ew, body)
    return ew.err
}

Wrap errors

Go 1.13 errors

%+v 、%w

Go 2 Error Inspection

時間節點

error (二)

02:30 答疑

02:48 例子

Concurrency

Goroutine

Memory model

Package sync

chan

Package context

// 3.5.go
import {
    "context"
    "fmt"
    "time"
}

func main() {
    tr := NewTracker()
    go tr.Run()
    _ = tr.Event(context.Background(), "test")
    _ = tr.Event(context.Background(), "test")
    _ = tr.Event(context.Background(), "test")
    ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))
    defer ctx.cancel()
    tr.Shutdown(ctx)
}

type Tracker struct {
    ch chan string
    stop chan struct{}
}

func (t *Tracker) Event(ctx context.Context, data string) error {
    select {
        case t.ch <- data:
            return nil
        case <-ctx.Done():
            return ctx.Err()
    }
}

func (t *Tracker) Run() {
    for data := range t.ch {
        time.Sleep(1 * time.Second)
        fmt.Println(data)
    }
    t.stop <- struct{}{}
}

func (t *Tracker) Shutdown(ctx context.Context) {
    close(t.ch)
    select {
        case <-t.stop
        case <-ctx.Done()
    }
}

golang.org/ref/mem

www.jianshu.com/p/5e44168f47a3

網路程式設計

goim

runtime

Goroutine 原理

Goroutine

和執行緒的區別

GMP排程模型

G goroutine
M 執行緒
P 佇列,與m繫結

M的建立時機?
go func

Work-steadling排程演算法

避免飢餓

全域性->本地->嘗試竊取->全域性->去網路撈

Syscall

Sysmon

Network poller

觸發時機
sysmon
schedule
start the world

記憶體分配原理

mcache -> mcentral(多種mspan) -> mheap(準備mspan) -> 系統記憶體(終點站)

垃圾回收原理(GC)

白話Go語言記憶體管理三部曲(三)垃圾回收原理

What every programmer should know about memory, Part 1

工程化實踐

layout

v1 github.com/bilibili/kratos-demo

model <- dao <- service <- server

我們老的佈局,app 目錄下有 api、cmd、configs、internal 目錄,目錄裡一般還會放置 README、CHANGELOG、OWNERS。

  • api: 放置 API 定義(protobuf),以及對應的生成的 client 程式碼,基於 pb 生成的 swagger.json。
  • configs: 放服務所需要的配置檔案,比如database.yaml、redis.yaml、application.yaml。
  • internal: 是為了避免有同業務下有人跨目錄引用了內部的 model、dao 等內部 struct。
  • server: 放置 HTTP/gRPC 的路由程式碼,以及 DTO 轉換的程式碼。
    DTO(Data Transfer Object):資料傳輸物件,這個概念來源於J2EE 的設計模式。但在這裡,泛指用於展示層/API 層與服務層(業務邏輯層)之間的資料傳輸物件。

專案的依賴路徑為: model -> dao -> service -> api,model struct 串聯各個層,直到 api 需要做 DTO 物件轉換。

  • model: 放對應“儲存層”的結構體,是對儲存的一一隱射。
  • dao: 資料讀寫層,資料庫和快取全部在這層統一處理,包括 cache miss 處理。
  • service: 組合各種資料訪問來構建業務邏輯。
  • server: 依賴 proto 定義的服務作為入參,提供快捷的啟動服務全域性方法。
  • api: 定義了 API proto 檔案,和生成的 stub 程式碼,它生成的 interface,其實現者在 service 中。
  • service 的方法簽名因為實現了 API 的 介面定義,DTO 直接在業務邏輯層直接使用了,更有 dao 直接使用,最簡化程式碼。

DO(Domain Object): 領域物件,就是從現實世界中抽象出來的有形或無形的業務實體。缺乏 DTO -> DO 的物件轉換。

v2 github.com/go-kratos/kratos-layout

app 目錄下有 api、cmd、configs、internal 目錄,目錄裡一般還會放置 README、CHANGELOG、OWNERS。

  • internal: 是為了避免有同業務下有人跨目錄引用了內部的 biz、data、service 等內部 struct。
  • biz: 業務邏輯的組裝層,類似 DDD 的 domain 層,data 類似 DDD 的 repo,repo 介面在這裡定義,使用依賴倒置的原則。
  • data: 業務資料訪問,包含 cache、db 等封裝,實現了 biz 的 repo 介面。我們可能會把 data 與 dao 混淆在一起,data 偏重業務的含義,它所要做的是將領域物件重新拿出來,我們去掉了 DDD 的 infra層。
  • service: 實現了 api 定義的服務層,類似 DDD 的 application 層,處理 DTO 到 biz 領域實體的轉換(DTO -> DO),同時協同各類 biz 互動,但是不應處理複雜邏輯。

PO(Persistent Object): 持久化物件,它跟持久層(通常是關係型資料庫)的資料結構形成一一對應的對映關係,如果持久層是關係型資料庫,那麼資料表中的每個欄位(或若干個)就對應 PO 的一個(或若干個)屬性。github.com/facebook/ent

啟動

github.com/go-kratos/kratos/blob/m...

下篇

00:41 Case

00:56 Error Case

service error -> gRpc error -> service error

01:24 example demo/v1/greeter_grpc.pb.go

01:33 介面只更新部分欄位,fieldMask標識欄位需要被更新

01:35 谷歌設計指南 cloud.google.com/apis/design?hl=zh...

01:50 Redis Config Case

Functional options

// 最終版
func main() {
  // load config file from yaml.
  c := new(redis.Config)
  _ = ApplyYAML(c, loadConfig())
  r, _ := redis.Dial(c.Network, c.Address, Options(c)...)
}

02:30 關於測試

單元測試的基本要求:

  • 快速
  • 環境一致
  • 任意順序
  • 並行

基於 docker-compose 實現跨平臺跨語言環境的容器依賴管理方案,以解決執行 unittest 場景下的(mysql, redis, mc)容器依賴問題:

  • 本地安裝 Docker。
  • 無侵入式的環境初始化。
  • 快速重置環境。
  • 隨時隨地執行(不依賴外部服務)。
  • 語義式 API 宣告資源。
  • 真實外部依賴,而非 in-process 模擬。
  1. 正確的對容器內服務進行健康檢測,避免unittest 啟動時候資源還未 ready。
  2. 應該交由 app 自己來初始化資料,比如 db 的scheme,初始的 sql 資料等,為了滿足測試的一致性,在每次結束後,都會銷燬容器。
  3. 在單元測試開始前,匯入封裝好的 testing 庫,方便啟動和銷燬容器。
  4. 對於 service 的單元測試,使用 gomock 等庫把 dao mock 掉,所以在設計包的時候,應該面向抽象程式設計。
  5. 在本地執行依賴 Docker,在 CI 環境裡執行Unittest,需要考慮在物理機裡的 Docker 網路,或者在 Docker 裡再次啟動一個 Docker

利用 go 官方提供的: Subtests + Gomock 完成整個單元測試。

  • /api
    比較適合進行整合測試,直接測試 API,使用 API 測試框架(例如: yapi),維護大量業務測試 case。
  • /data
    docker compose 把底層基礎設施真實模擬,因此可以去掉 infra 的抽象層。
  • /biz
    依賴 repo、rpc client,利用 gomock 模擬 interface 的實現,來進行業務單元測試。
  • /service
    依賴 biz 的實現,構建 biz 的實現類傳入,進行單元測試。

基於 git branch 進行 feature 開發,本地進行 unittest,之後提交 gitlab merge request 進行 CI 的單元測試,基於 feature branch 進行構建,完成功能測試,之後合併 master,進行整合測試,上線後進行迴歸測試。

微服務可用性設計

隔離

  • 服務隔離

    • 動靜隔離、讀寫隔離
  • 輕重隔離

    • 核心、快慢、熱點
  • 物理隔離

    • 執行緒、程式、叢集、機房

超時控制

code:504
。。。

01:22 head of line blocking

VDSO:blog.csdn.net/juana1/article/detai...

過載保護

code:429

token-bucket rate limit algorithm: /x/time/rate

leaky-bucket rate limit algorithm: /go.uber.org/ratelimit

人力運維成本過高。需要自適應式的演算法。

計算系統臨近過載時的峰值吞吐作為限流的閾值來進行流量控制,達到系統保護。
伺服器臨近過載時,主動拋棄一定量的負載,目標是自保。
在系統穩定的前提下,保持系統的吞吐量。
常見做法:利特爾法則

課程筆記(VIP)

CPU、記憶體作為訊號量進行節流。
佇列管理: 佇列長度、LIFO。
可控延遲演算法: CoDel。codel+bbr

如何計算接近峰值時的系統吞吐?
CPU: 使用一個獨立的執行緒取樣,每隔 250ms 觸發一次。在計算均值時,使用了簡單滑動平均去除峰值的影響。
Inflight: 當前服務中正在進行的請求的數量。
Pass&RT: 最近5s,pass 為每100ms取樣視窗內成功請求的數量,rt 為單個取樣視窗中平均響應時間

【冷卻思想】
我們使用 CPU 的滑動均值(CPU > 80080%)作為啟發閾值,一旦觸發進入到過載保護階段,演算法為:(pass* rt) < inflight

限流效果生效後,CPU 會在臨界值(800)附近抖動,如果不使用冷卻時間,那麼一個短時間的 CPU 下降就可能導致大量請求被放行,嚴重時會打滿 CPU在冷卻時間後,重新判斷閾值(CPU > 800),是否持續進入過載保護。

過載保護:自己保護自己,每一個服務都做

限流

限流是指在一段時間內,定義某個客戶或應用可以接收或處理多少個請求的技術。例如,通過限流,你可以過濾掉產生流量峰值的客戶和微服務,或者可以確保你的應用程式在自動擴充套件(Auto Scaling)失效前都不會出現過載的情況。

  • 令牌桶、漏桶 針對單個節點,無法分散式限流。
  • QPS 限流
    • 不同的請求可能需要數量迥異的資源來處理。
    • 某種靜態 QPS 限流不是特別準。
  • 給每個使用者設定限制
    • 全域性過載發生時候,針對某些“異常”進行控制。
    • 一定程度的“超賣”配額。
  • 按照優先順序丟棄。
  • 拒絕請求也需要成本。

分散式限流

分散式限流,是為了控制某個應用全域性的流量,而非真對單個節點緯度。

  • 單個大流量的介面,使用 redis 容易產生熱點。
  • pre-request 模式對效能有一定影響,高頻的網路往返。

思考:
從獲取單個 quota 升級成批量 quota。quota: 表示速率,獲取後使用令牌桶演算法來限制。

課程筆記(VIP)

  • 每次心跳後,非同步批量獲取 quota,可以大大減少請求 redis 的頻次,獲取完以後本地消費,基於令牌桶攔截。
  • 每次申請的配額需要手動設定靜態值略欠靈活,比如每次要20,還是50。

如何基於單個節點按需申請,並且避免出現不公平的現象?
初次使用預設值,一旦有過去歷史視窗的資料,可以基於歷史視窗資料進行 quota 請求。

課程筆記(VIP)

思考:
我們經常面臨給一組使用者劃分稀有資源的問題,他們都享有等價的權利來獲取資源,但是其中一些使用者實際上只需要比其他使用者少的資源。

那麼我們如何來分配資源呢?一種在實際中廣泛使用的分享技術稱作“最大最小公平分享”(Max-Min Fairness)。

直觀上,公平分享分配給每個使用者想要的可以滿足的最小需求,然後將沒有使用的資源均勻的分配給需要‘大資源’的使用者。

最大最小公平分配演算法的形式化定義如下:
資源按照需求遞增的順序進行分配。
不存在使用者得到的資源超過自己的需求。
未得到滿足的使用者等價的分享資源。

課程筆記(VIP)

課程筆記(VIP)

重要性

每個介面配置閾值,運營工作繁重,最簡單的我們配置服務級別 quota,更細粒度的,我們可以根據不同重要性設定 quota,我們引入了重要性(criticality):

  • 最重要 CRITICAL_PLUS,為最終的要求預留的型別,拒絕這些請求會造成非常嚴重的使用者可見的問題。
  • 重要 CRITICAL,生產任務發出的預設請求型別。拒絕這些請求也會造成使用者可見的問題。但是可能沒那麼嚴重。
  • 可丟棄的 SHEDDABLE_PLUS 這些流量可以容忍某種程度的不可用性。這是批量任務發出的請求的預設值。這些請求通常可以過幾分鐘、幾小時後重試。
  • 可丟棄的 SHEDDABLE 這些流量可能會經常遇到部分不可用情況,偶爾會完全不可用。

gRPC 系統之間,需要自動傳遞重要性資訊。如果後端接受到請求 A,在處理過程中發出了請求 B 和 C 給其他後端,請求 B 和 C 會使用與 A 相同的重要性屬性。

  • 全域性配額不足時,優先拒絕低優先順序的。
  • 全域性配額,可以按照重要性分別設定。
  • 過載保護時,低優先順序的請求先被拒絕。

熔斷

斷路器(Circuit Breakers): 為了限制操作的持續時間,我們可以使用超時,超時可以防止掛起操作並保證系統可以響應。因為我們處於高度動態的環境中,幾乎不可能確定在每種情況下都能正常工作的準確的時間限制。斷路器以現實世界的電子元件命名,因為它們的行為是都是相同的。斷路器在分散式系統中非常有用,因為重複的故障可能會導致雪球效應,並使整個系統崩潰。

  • 服務依賴的資源出現大量錯誤。
  • 某個使用者超過資源配額時,後端任務會快速拒絕請求,返回“配額不足”的錯誤,但是拒絕回覆仍然會消耗一定資源。有可能後端忙著不停傳送拒絕請求,導致過載。

Google SRE
max(0, (requests - K*accepts) / (requests + 1))

github.com/go-kratos/kratos/blob/v...

課程筆記(VIP)

Gutter Kafka

客戶端限流

github.com/go-kratos/kratos/blob/v...

課程筆記(VIP)

02:10 DTO Case

02:35 程式碼演示:wire依賴注入,資料庫優雅實現?

降級

提供有損服務

重試

負載均衡

github.com/go-kratos/kratos/blob/v... (重點)

評論架構設計

播放歷史架構

分散式快取 & 分散式事務

日誌 & 指標 & 鏈路追蹤

DNS & CDN & 多活架構

kafka

Flink的Watermark機制

位元組跳動基於Flink的MQ-Hive實時資料整合

Kafka 基礎概念

儲存原理:高度依賴檔案系統

利用順序I/O、Page Cache達成的超高吞吐

保留所有釋出的massage,不管有沒有被消費過

Topic & Partition

offset:對於當前 Partition 的偏移量

同一個 Topic 有多個不同的 Partition

一個 Partition 為同一個目錄

(3,497)全域性第 MessageNo + 3 個訊息

稀疏索引,減少索引檔案大小,對映到記憶體

.timeindex -> .index -> .log ?

Producer & Consumer

01:40 Producer

Consumer

位元組link方案?

push vs pull

codel+ bbr

Leader & Follower

租約制

Partition 分佈不均衡 (揹包演算法??)、核心是儘量均衡

資料可靠性

HW(high watermark)、LEO(log end offset)

效能優化

MAD 演算法

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章