大家好,我是藍胖子,關於效能分析的視訊和文章我也大大小小出了有一二十篇了,算是已經有了一個系列,之前的程式碼已經上傳到github.com/HobbyBear/performance-analyze,接下來這段時間我將在之前內容的基礎上,結合自己在公司生產上構建監控系統的經驗,詳細的展示如何對線上服務進行監控,內容涉及到的指標設計,軟體設定,監控方案等等你都可以拿來直接復刻到你的專案裡,這是一套非常適合中小企業的監控體系。
在上一節我們是講解了如何對應用服務進行監控,這一節我將會介紹如何對mysql進行監控,在傳統監控mysql(對mysql整體服務質量的監控)的情況下,建立對錶級別的監控,以及長事務,複雜sql的監控,並能定位到具體程式碼。
監控系列的程式碼已經上傳到github
github.com/HobbyBear/easymonitor
無論是前文提到的 機器監控還是應用監控,我們都提到了四大黃金指標原則,對mysql 建立監控指標,我們依然可以從這幾個維度去對mysql的指標進行分析。
四大黃金指標是流量,延遲,飽和度,錯誤數。
對於流量而言可以體現在mysql資料庫操作的qps,資料庫伺服器進行流量大小。對於延遲,可以體現在慢查詢記錄上,飽和度可以用資料庫的連線數,執行緒數,或者磁碟空間,cpu,記憶體等各種硬體資源來反映資料庫的飽和情況。錯誤數則可以用,資料庫存取報錯資訊來反應,比如連線不足,超時等錯誤。
由於我們是用的雲資料庫,上面提到的這些監控維度以及面板在雲廠商那裡其實都基本覆蓋了,我稱這些監控面板或者維度是資料庫的傳統監控指標。 這些指標能夠反應資料庫監控狀況,但對於開發來講,去進行問題排查還遠遠不足的,下面我講下如果只有此型別的監控會有什麼缺點以及我的解決思路。
在使用它們對mysql進行監控時當異常發生時,不是能很好的確定是哪部分業務導致的問題。比如,當你發現資料庫的qps突然升高,但是介面qps比較低的時候,如何確定資料庫qps升高的原因呢? 這中間存在的問題在於mysql的資料監控指標和應用服務程式碼邏輯沒有很好的關聯性,我們要如何去建立這種關聯絡?
答案就是建立表級別的監控,你可以發現傳統的監控指標都是對mysql整體服務質量進行的監控,而應用業務邏輯程式碼本質上是對錶進行操作,如果建立了表級別的監控,就能將業務與資料庫監控指標聯絡起來。比如按表級別建立單個表的qps,當發現資料庫整體的qps升高時,可以發現這是由於哪張表引起的,進而定位到具體業務,檢視程式碼邏輯看看是哪部分邏輯會操作這張表那麼多次。
下面我們就來看看如何建立表級別的監控。
mysql的performance schema其實已經暴露了表級別的某些監控項,不過由於某些原因我們線上並沒有開啟它,並且由於直接使用performance schema暴露的監控指標不能客製化化,所以我將介紹一種在應用伺服器端埋點的方式建立表級別的qps。我們生產上是golang的應用服務,所以我會用它來舉例。
用github.com/go-sql-driver/mysql 在golang中開啟一個mysql連線是這樣做:
db, err = sql.Open("mysql", connStr)
sql.Open的第一個引數是驅動名,預設的驅動名是mysql,這個驅動是引入github.com/go-sql-driver/mysql時自動去建立的。
func init() {
sql.Register("mysql", &MySQLDriver{})
}
所以,我們完全可以包裝預設驅動,自定義一個自己的驅動,驅動實現了open介面返回一個連線Conn的介面型別。
type Driver interface {
Open(name string) (Conn, error)
}
type Conn interface {
Prepare(query string) (Stmt, error)
Close() error
Begin() (Tx, error)
}
type Stmt interface {
Close() error
NumInput() int
Exec(args []Value) (Result, error)
Query(args []Value) (Rows, error)
}
自定義的驅動型別在實現Open方法時,也可以自定義一個Conn連線型別,然後再實現它的查詢介面,進行sql語句分析,解析表名後進行埋點統計。
完整的定義驅動程式碼已經上傳到文章開頭的github地址,總之,你需要明白,通過對預設驅動的包裝,我們可以在sql執行前後做一些自定義的監控分析。我們定義了3個勾點函數,分別針對sql執行前後以及報錯做了監控分析。
對於sql埋點的原理更詳細的講解可以參考go database sql介面分析及sql埋點實現
// sql執行前做監控
if ctx, err = stmt.hooks.Before(ctx, stmt.query, list...); err != nil {
return nil, err
}
// sql執行
results, err := stmt.execContext(ctx, args)
if err != nil {
// sql 報錯時做監控
return results, handlerErr(ctx, stmt.hooks, err, stmt.query, list...)
}
// sql執行後做監控
if _, err := stmt.hooks.After(ctx, stmt.query, list...); err != nil {
return nil, err
}
在sql執行前,通過SqlMonitor.parseTable對sql語句的分析,解析出當前sql涉及的表名,以及操作型別,是insert,select,delete,還是update,並且如果sql涉及到了多張表,那麼會對其打上MultiTable的標籤(這在下面講sql審計時會提到),sql執行前的勾點函數如下所示:
func (h *HookDb) Before(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
ctx = context.WithValue(ctx, ctxKeyBeginTime, time.Now())
// 得到sql涉及的表名,以及這條sql是屬於什麼crud的哪種型別
tables, op, err := SqlMonitor.parseTable(query)
if err != nil || op == Unknown {
log.WithError(err).WithField(ctxKeySql, query).WithField("op", op).Error("parse sql fail")
}
if len(tables) >= 2 {
ctx = context.WithValue(ctx, ctxKeyMultiTable, 1)
}
if len(tables) >= 1 {
ctx = context.WithValue(ctx, ctxKeyTbName, tables[0])
}
if op != Unknown {
ctx = context.WithValue(ctx, ctxKeyOp, op)
}
return ctx, nil
}
分析出了表名並且記錄上了sql的執行時長,我們可以利用prometheus的histogram 型別的指標建立表維度的p99延遲分位數,並且能夠知道表級別的qps數量,如下,我們可以在sql執行後的勾點函數裡完成統計,MetricMonitor.RecordClientHandlerSeconds封裝了這個邏輯。
func (h *HookDb) After(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
// ....
if tbnameInf := ctx.Value(ctxKeyTbName); tbnameInf != nil && len(tbnameInf.(string)) != 0 {
tableName = tbnameInf.(string)
MetricMonitor.RecordClientHandlerSeconds(TypeMySQL, string(ctx.Value(ctxKeyOp).(SqlOp)), tbnameInf.(string), h.dbName, now.Sub(beginTime).Seconds())
// .....
}
我們可以利用這個指標在grafana上完成表級別的監控面板。
對於資料庫還有要需要注意的地方,那就是長事務和複雜sql,慢sql的監控,往往出現上述情況時,就容易出現資料庫的效能問題。現在我們來看看如何監控它們。
首先,來看下長事務的監控,我們可以為連線型別實現BeginTx方法,對原始driver.ConnBeginTx 事務型別進行包裝,讓事務攜帶上開始時間。
func (conn *Conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
tx, err := conn.Conn.(driver.ConnBeginTx).BeginTx(ctx, opts)
if err != nil {
return tx, err
}
return &DriveTx{tx: tx, start: time.Now()}, nil
}
接著,在提交事務的時候,判斷時間是不是超過某個8s,超過了則記錄一條錯誤紀錄檔,並把堆疊資訊也列印出來,這樣方便定位是哪段邏輯產生的長事務。由於我們的錯誤等級的紀錄檔會被收集起來自動報警,這樣就完成了長事務的實時監控報警。
func (d *DriveTx) Commit() error {
err := d.tx.Commit()
d.cost = time.Now().Sub(d.start).Milliseconds()
if d.cost > 8000 {
data := log.Fields{
Cost: d.cost,
MetricType: "longTx",
Stack: fmt.Sprintf("%+v", getStack()),
}
log.WithFields(data).Errorf("mysqlongTxlog ")
}
return err
}
接著,我們看下sql審計如何做,mysql可以開啟sql審計的設定項,不過開啟後將會採集所有執行的sql語句,這樣會導致sql太多,我們往往只用關心那些影響效能的sql或者讓資料產生變化的sql。
程式碼如下,我們在sql完成執行後,通過sql的執行時長,對慢sql進行告警出來,並且對涉及到兩個表的sql進行紀錄檔列印,也會對修改資料的sql語句(insert,update,delete)進行記錄,這對我們排查業務資料會很有幫助。
func (h *HookDb) After(ctx context.Context, query string, args ...interface{}) (context.Context, error) {
beginTime := time.Now()
if begin := ctx.Value(ctxKeyBeginTime); begin != nil {
beginTime = begin.(time.Time)
}
now := time.Now()
tableName := ""
if tbnameInf := ctx.Value(ctxKeyTbName); tbnameInf != nil && len(tbnameInf.(string)) != 0 {
tableName = tbnameInf.(string)
MetricMonitor.RecordClientHandlerSeconds(TypeMySQL, string(ctx.Value(ctxKeyOp).(SqlOp)), tbnameInf.(string), h.dbName, now.Sub(beginTime).Seconds())
}
// 對慢sql進行實時監控,超過1s則認為是慢sql
slowquery := false
if now.Sub(beginTime).Seconds() >= 1 {
slowquery = true
data := log.Fields{
Cost: now.Sub(beginTime).Milliseconds(),
"query": truncateKey(1024, query),
"args": truncateKey(1024, fmt.Sprintf("%v", args)),
MetricType: "slowLog",
"app": h.app,
"dbName": h.dbName,
"tableName": tableName,
"op": ctx.Value(ctxKeyOp),
}
log.WithFields(data).Errorf("mysqlslowlog")
}
op := ctx.Value(ctxKeyOp).(SqlOp)
multitable := ctx.Value(ctxKeyMultiTable)
if !slowquery && (multitable != nil && multitable.(int) == 1) && op == Select {
// 對複雜sql進行監控,如果不是慢sql,但是sql涉及到兩個表也會紀錄檔進行記錄
data := log.Fields{
Cost: now.Sub(beginTime).Milliseconds(),
"query": truncateKey(1024, query),
"args": truncateKey(1024, fmt.Sprintf("%v", args)),
MetricType: "multiTables",
"app": h.app,
"dbName": h.dbName,
"tableName": tableName,
"op": ctx.Value(ctxKeyOp),
}
log.WithFields(data).Warnf("mysqlmultitableslog")
}
// 對修改資料的sql進行紀錄檔記錄
if op != Select && op != Unknown {
data := log.Fields{
Cost: now.Sub(beginTime).Milliseconds(),
"query": truncateKey(1024, query),
"args": truncateKey(1024, fmt.Sprintf("%v", args)),
MetricType: "oplog",
"app": h.app,
"dbName": h.dbName,
"tableName": tableName,
"op": ctx.Value(ctxKeyOp),
}
log.WithFields(data).Infof("mysqloplog")
}
return ctx, nil
}
這一節我們完成了對mysql的監控,不過這個監控指標是在傳統資料庫監控項基礎上建立的,目的是為了讓監控指標更加容易反映到業務上,方便問題定位,在下一節我將會演示如何對redis進行監控,與mysql監控類似,我們也需要從業務維度思考對redis的監控。