基礎
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()
}
}
www.jianshu.com/p/5e44168f47a3
網路程式設計
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)
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
// 最終版
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 模擬。
- 正確的對容器內服務進行健康檢測,避免unittest 啟動時候資源還未 ready。
- 應該交由 app 自己來初始化資料,比如 db 的scheme,初始的 sql 資料等,為了滿足測試的一致性,在每次結束後,都會銷燬容器。
- 在單元測試開始前,匯入封裝好的 testing 庫,方便啟動和銷燬容器。
- 對於 service 的單元測試,使用 gomock 等庫把 dao mock 掉,所以在設計包的時候,應該面向抽象程式設計。
- 在本地執行依賴 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
人力運維成本過高。需要自適應式的演算法。
計算系統臨近過載時的峰值吞吐作為限流的閾值來進行流量控制,達到系統保護。
伺服器臨近過載時,主動拋棄一定量的負載,目標是自保。
在系統穩定的前提下,保持系統的吞吐量。
常見做法:利特爾法則
CPU、記憶體作為訊號量進行節流。
佇列管理: 佇列長度、LIFO。
可控延遲演算法: CoDel。codel+bbr
如何計算接近峰值時的系統吞吐?
CPU: 使用一個獨立的執行緒取樣,每隔 250ms 觸發一次。在計算均值時,使用了簡單滑動平均去除峰值的影響。
Inflight: 當前服務中正在進行的請求的數量。
Pass&RT: 最近5s,pass 為每100ms取樣視窗內成功請求的數量,rt 為單個取樣視窗中平均響應時間
【冷卻思想】
我們使用 CPU 的滑動均值(CPU > 800(80%))作為啟發閾值,一旦觸發進入到過載保護階段,演算法為:(pass* rt) < inflight
限流效果生效後,CPU 會在臨界值(800)附近抖動,如果不使用冷卻時間,那麼一個短時間的 CPU 下降就可能導致大量請求被放行,嚴重時會打滿 CPU。
在冷卻時間後,重新判斷閾值(CPU > 800),是否持續進入過載保護。
過載保護:自己保護自己,每一個服務都做
限流
限流是指在一段時間內,定義某個客戶或應用可以接收或處理多少個請求的技術。例如,通過限流,你可以過濾掉產生流量峰值的客戶和微服務,或者可以確保你的應用程式在自動擴充套件(Auto Scaling)失效前都不會出現過載的情況。
- 令牌桶、漏桶 針對單個節點,無法分散式限流。
- QPS 限流
- 不同的請求可能需要數量迥異的資源來處理。
- 某種靜態 QPS 限流不是特別準。
- 給每個使用者設定限制
- 全域性過載發生時候,針對某些“異常”進行控制。
- 一定程度的“超賣”配額。
- 按照優先順序丟棄。
- 拒絕請求也需要成本。
分散式限流
分散式限流,是為了控制某個應用全域性的流量,而非真對單個節點緯度。
- 單個大流量的介面,使用 redis 容易產生熱點。
- pre-request 模式對效能有一定影響,高頻的網路往返。
思考:
從獲取單個 quota 升級成批量 quota。quota: 表示速率,獲取後使用令牌桶演算法來限制。
- 每次心跳後,非同步批量獲取 quota,可以大大減少請求 redis 的頻次,獲取完以後本地消費,基於令牌桶攔截。
- 每次申請的配額需要手動設定靜態值略欠靈活,比如每次要20,還是50。
如何基於單個節點按需申請,並且避免出現不公平的現象?
初次使用預設值,一旦有過去歷史視窗的資料,可以基於歷史視窗資料進行 quota 請求。
思考:
我們經常面臨給一組使用者劃分稀有資源的問題,他們都享有等價的權利來獲取資源,但是其中一些使用者實際上只需要比其他使用者少的資源。
那麼我們如何來分配資源呢?一種在實際中廣泛使用的分享技術稱作“最大最小公平分享”(Max-Min Fairness)。
直觀上,公平分享分配給每個使用者想要的可以滿足的最小需求,然後將沒有使用的資源均勻的分配給需要‘大資源’的使用者。
最大最小公平分配演算法的形式化定義如下:
資源按照需求遞增的順序進行分配。
不存在使用者得到的資源超過自己的需求。
未得到滿足的使用者等價的分享資源。
重要性
每個介面配置閾值,運營工作繁重,最簡單的我們配置服務級別 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...
Gutter Kafka
客戶端限流
github.com/go-kratos/kratos/blob/v...
02:10 DTO Case
02:35 程式碼演示:wire依賴注入,資料庫優雅實現?
降級
提供有損服務
重試
負載均衡
github.com/go-kratos/kratos/blob/v... (重點)
評論架構設計
播放歷史架構
分散式快取 & 分散式事務
日誌 & 指標 & 鏈路追蹤
DNS & CDN & 多活架構
kafka
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 協議》,轉載必須註明作者和本文連結