天真貝葉斯學習機 | TiDB Hackathon 優秀專案分享

PingCAP發表於2018-12-05

Ti Hack 系列

TiDB Hackathon 2018 共評選出六組優秀專案,本系列文章將由這六組專案的成員主筆,分享他們的參賽經驗和成果。我們非常希望本屆 Hackathon 誕生的優秀專案能夠在社群中延續下去,感興趣的小夥伴們可以加入進來哦~

本文作者是來自 DSG 團隊的楊文同學,他們的專案《天真貝葉斯學習機》在本屆 Hackathon 中獲得了三等獎+最佳創意獎。

天真貝葉斯學習機 | TiDB Hackathon 優秀專案分享

“在 TiDB Hackathon 2018 學習到不少東西,希望明年再來”

簡述

“pd ctl 天真學習機”

具體做法:用 naive bayes 模型來根據系統指標和人的 pd ctl 呼叫,來得到一個模型去根據系統指標去自動提供 pd ctl 呼叫的命令。

1.貝葉斯演算法舉例

貝葉斯模型可以用來幹這種事:

比如一個媽媽根據天氣預報來跟兒子在出們的時候叮囑:

天氣預報[ 晴, 溫度: 28, 風力: 中 ], 媽媽會說 [好好玩]
天氣預報[ 雨, 溫度: 15, 風力: 低 ], 媽媽會說 [帶上傘]
天氣預報[ 陰, 溫度: 02, 風力: 大 ], 媽媽會說 [多穿點]...
複製程式碼

把這些輸入輸入到貝葉斯模型裡以後, 模型可以根據天氣預報來輸出:

天氣預報[ 晴, 溫度: 00, 風力中], 模型會說 [ 多穿點:0.7, 好好玩0.2, 帶上傘0.1]
天氣預報[ 雨, 溫度: 10, 風力大], 模型會說 [ 帶上傘:0.8, 多穿點0.1, 好好玩0.1]
複製程式碼

這樣通過一個媽媽的叮囑就可以訓練出一個也會根據天氣預報給出叮囑的模型。

2. 初步想法

我們可以把一個模型單獨的部署在一個 pod 裡, 暴露一個 service ,然後叢集上每次有人去呼叫 pd_ctl 的時候就在後臺用 rest call 到模型服務上記錄一下操作(叮囑)和當前的系統指標(好比天氣預報). 這樣慢慢用一段時間以後,積累的操作多了以後,就可以開啟某個自動響應,或者開啟自動建議應該執行的命令的功能。

這樣模型可以在某一組系統指標出現之前類似學習過的狀態之後,給出相應的建議,當這些建議都很正確的時候直接讓 pd 直接採納,完全智慧的自動化運作。

3. 實際 Hackathon 方案

在跟導師交流探討後發現,目前 PD 已經比較自動化了,很少需要人為介入進行操作,需要的時候也是比較複雜的場景,或者自動化運作比較慢的場景。

我們團隊在跟多名導師的溝通交流下,將初步想法進行了一些調整:

  • 從熱點排程策略入手,用熱點排程策略的數值去用 naive bayes 模型去訓練他們,然後再根據這些數值再去模型中去獲取建議值。

  • 統計建議值和熱點排程策略進行比較;(從開始的測試結果來看,大概有 70% 匹配,但是我們實測發現,使用我們模型的建議值去真正的排程,熱點 region 還是非常均衡的)

  • 三組對照試驗:不進行排程,只列印排程資料;正常使用原來的熱點排程策略;使用原來的熱點排程策略的數值,但是使用模型訓練的建議值進行實際排程;

Hackathon 回顧

首先,介紹一下我們團隊(DSG),分別來自:丹麥、北京(山西)、廣州。

D 先生是在比賽前一天早上到達北京的,我是比賽前一天晚上從廣州出發,於比賽當日早上 6:38 才抵達北京的。

說實話,時差和疲憊對於參賽還是有一點影響的。

廢話不多說,我就來回顧一下我的整個參賽過程。

  • 比賽前一日 20:05 從廣州南站出發,次日 6:38 抵達北京西站。
  • 7:58 抵達地鐵西小口

天真貝葉斯學習機 | TiDB Hackathon 優秀專案分享

  • 8:06 經過轉轉
  • 8:12 抵達比賽所在地:東昇科技園 C-1 樓
  • 8:16 簽到,逛 PingCAP

天真貝葉斯學習機 | TiDB Hackathon 優秀專案分享

  • 8:40 跟 D 先生匯合,瞭解貝葉斯模型
  • 9:20 DSG 團隊成員全部集結完畢

天真貝葉斯學習機 | TiDB Hackathon 優秀專案分享

  • 10:00 比賽正式開始
  • 10:00 Hacking Time: Trello 構建整個比賽分工、準備工作、需求分析
  • 搭建 TiDB 叢集(2套)【熟悉 TiDB 叢集,實操 PD-CTL】
  • 12:17 午餐
  • 13:00 Hacking Time: 熟悉 PD Command,貝葉斯模型,導師指導,本地 TiDB 環境構建(坑),分析 PD 熱點排程,剖析排程流程,模擬熱點資料
  • 18:20 外出用餐(蘆月軒羊蠍子(西三旗店))【沾 D 先生的光,蹭吃蹭喝】
  • 20:40 回到東昇科技園
  • 20:50 ~ 次日 1:10 Hacking Time: 模擬熱點資料,實測排程上報和獲取模型返回結果,本地測通排程引數上報和得到模型返回值
  • 次日 1:10 ~ 5:50 會議室休息(在此期間,我的隊友 D 先生,調好了模型,並將此模型通過 Docker 構建部署到 PD 機器上)
  • 次日 5:50 Hacking Time: 部署修改過的 PD 服務到線上伺服器,並打通 rust-nb-server,實時上報和實時獲取模型返回結果
  • 次日 7:30 早餐
  • 次日 8:00 正式除錯
  • 次日 9:00 抽籤確定 Demo 時間
  • 次日 9:00 ~ 12:00 Hacking Time: 調優
  • 次日 12:00 ~ 12:30 午餐時間
  • 次日 13:00 ~ 14:00 Hacking Time: PPT,調優
  • 次日 14:30 ~ 18:30 Demo Time(B 站直播)

天真貝葉斯學習機 | TiDB Hackathon 優秀專案分享
天真貝葉斯學習機 | TiDB Hackathon 優秀專案分享
天真貝葉斯學習機 | TiDB Hackathon 優秀專案分享

  • 次日 18:30 ~ 19:00 頒獎(B 站直播)

天真貝葉斯學習機 | TiDB Hackathon 優秀專案分享
天真貝葉斯學習機 | TiDB Hackathon 優秀專案分享

Hackathon 實操

1. 搭建 TiDB 叢集

完全參考文件

測試 TiDB 叢集,可能遇到的坑(MySQL 8 client On MacOSX):

  • mysql client connect : Unknown charset 255 (MySQL 8 Client 不支援字符集,需要指定預設字符集為 UTF8)

    mysql -hx.x.x.x --default-character-set utf8

2. 天真貝葉斯的服務介面

  • /model/service1 PUT 上報資料:
{
  "updates": [
    [
      "transfer leader from store 7 to store 2",
      [
        {
          "feature_type": "Category",
          "name": "hotRegionsCount1",
          "value": "true"
        },
        {
          "feature_type": "Category",
          "name": "minRegionsCount1",
          "value": "true"
        },
        {
          "feature_type": "Category",
          "name": "hotRegionsCount2",
          "value": "true"
        },
        {
          "feature_type": "Category",
          "name": "minRegionsCount2",
          "value": "true"
        },
        {
          "feature_type": "Category",
          "name": "srcRegion",
          "value": "7"
        }
      ]
    ],
  ]}
複製程式碼
  • /model/service1 POST 獲取模型結果: 輸入引數:上報的引數
{
  "predictions": [
    {
      "transfer leader from store 1 to store 2": 0.27432775221072137,
      "transfer leader from store 1 to store 7": 0.6209064350448428,
      "transfer leader from store 2 to store 1": 0.024587894827775753,
      "transfer leader from store 2 to store 7": 0.01862719305134528,
      "transfer leader from store 7 to store 1": 0.02591609468013258,
      "transfer leader from store 7 to store 2": 0.03563463018518229
    }
  ]}
複製程式碼

3. PD 叢集部署

首先將 pd-server 替換到叢集所在 ansible/resources/bin 目錄下,那如何讓叢集上的 PD 更新生效呢?

更新:

$ ansible-playbook rolling_update.yml --tags=pd
複製程式碼

在實操過程中, 如果你在更新到一半的時候就關門了,可能會導致整個 PD 掛掉(非叢集環境),可能是因為邏輯不嚴謹所導致的問題

直接停止了 ansible,導致 PD 叢集機器節點有停止的情況,這個時候你可以通過以下命令啟動它。

啟動:

$ ansible-playbook start.yml --tags=pd
複製程式碼

4. PD 排程

4.1 取消熱點資料排程

大家都以為可以通過配置來解決:(排程開關方法: 用 config set xxx 0 來關閉排程)

配置如下:(雖然找的地方錯誤了,但是錯打錯著,我們來到了 Demo Time:

config set leader-schedule-limit 0
config set region-schedule-limit 0
scheduler add hot-region-scheduler
config show
config set leader-schedule-limit 4
config set region-schedule-limit 8
複製程式碼

實測發現,根本不生效,必須要改原始碼。

func (h *balanceHotRegionsScheduler) dispatch(typ BalanceType, cluster schedule.Cluster) []*schedule.Operator {
    h.Lock()
    defer h.Unlock()
    switch typ {
    case hotReadRegionBalance:
        h.stats.readStatAsLeader = h.calcScore(cluster.RegionReadStats(), cluster, core.LeaderKind)
        // return h.balanceHotReadRegions(cluster) // 將這一行註釋
    case hotWriteRegionBalance:
        h.stats.writeStatAsLeader = h.calcScore(cluster.RegionWriteStats(), cluster, core.LeaderKind)
        h.stats.writeStatAsPeer = h.calcScore(cluster.RegionWriteStats(), cluster, core.RegionKind)
        // return h.balanceHotWriteRegions(cluster) // 將這一行註釋
    }
    return nil
}
複製程式碼

但是,我們要的不是不排程,而只是不給排程結果:

func (h *balanceHotRegionsScheduler) balanceHotReadRegions(cluster schedule.Cluster) []*schedule.Operator {
    // balance by leader
    srcRegion, newLeader := h.balanceByLeader(cluster, h.stats.readStatAsLeader)
    if srcRegion != nil {
        schedulerCounter.WithLabelValues(h.GetName(), "move_leader").Inc()
        // step := schedule.TransferLeader{FromStore: srcRegion.GetLeader().GetStoreId(), ToStore: newLeader.GetStoreId()} // 修改為不返回值或者返回 _
        _ = schedule.TransferLeader{FromStore: srcRegion.GetLeader().GetStoreId(), ToStore: newLeader.GetStoreId()}
        // return []*schedule.Operator{schedule.NewOperator("transferHotReadLeader", srcRegion.GetID(), srcRegion.GetRegionEpoch(), schedule.OpHotRegion|schedule.OpLeader, step)} // 註釋這一行,並 return nil
        return nil
    }

    // balance by peer
    srcRegion, srcPeer, destPeer := h.balanceByPeer(cluster, h.stats.readStatAsLeader)
    if srcRegion != nil {
        schedulerCounter.WithLabelValues(h.GetName(), "move_peer").Inc()
        return []*schedule.Operator{schedule.CreateMovePeerOperator("moveHotReadRegion", cluster, srcRegion, schedule.OpHotRegion, srcPeer.GetStoreId(), destPeer.GetStoreId(), destPeer.GetId())}
    }
    schedulerCounter.WithLabelValues(h.GetName(), "skip").Inc()
    return nil
}

......

func (h *balanceHotRegionsScheduler) balanceHotWriteRegions(cluster schedule.Cluster) []*schedule.Operator {
    for i := 0; i < balanceHotRetryLimit; i++ {
        switch h.r.Int() % 2 {
        case 0:
            // balance by peer
            srcRegion, srcPeer, destPeer := h.balanceByPeer(cluster, h.stats.writeStatAsPeer)
            if srcRegion != nil {
                schedulerCounter.WithLabelValues(h.GetName(), "move_peer").Inc()
                fmt.Println(srcRegion, srcPeer, destPeer)
                // return []*schedule.Operator{schedule.CreateMovePeerOperator("moveHotWriteRegion", cluster, srcRegion, schedule.OpHotRegion, srcPeer.GetStoreId(), destPeer.GetStoreId(), destPeer.GetId())} // 註釋這一行,並 return nil
                return nil
            }
        case 1:
            // balance by leader
            srcRegion, newLeader := h.balanceByLeader(cluster, h.stats.writeStatAsLeader)
            if srcRegion != nil {
                schedulerCounter.WithLabelValues(h.GetName(), "move_leader").Inc()
                // step := schedule.TransferLeader{FromStore: srcRegion.GetLeader().GetStoreId(), ToStore: newLeader.GetStoreId()} // 修改為不返回值或者返回 _
                _ = schedule.TransferLeader{FromStore: srcRegion.GetLeader().GetStoreId(), ToStore: newLeader.GetStoreId()}

                // return []*schedule.Operator{schedule.NewOperator("transferHotWriteLeader", srcRegion.GetID(), srcRegion.GetRegionEpoch(), schedule.OpHotRegion|schedule.OpLeader, step)} // 註釋這一行,並 return nil
                return nil
            }
        }
    }

    schedulerCounter.WithLabelValues(h.GetName(), "skip").Inc()
    return nil
}
複製程式碼

當修改了 PD 再重新編譯得到 pd-server,將其放到

tidb-ansible/resources/bin/pd-server 並替換原來的檔案,然後執行

ansible-playbook rolling_update.yml --tags=pd,即可重啟 pd-server 服務。

在調優的過程中發現,當前 hot-region-scheduler 的排程時對於目標機器的選擇並不是最優的,程式碼如下:

github.com/pingcap/pd/…

簡述:迴圈遍歷 candidateStoreIDs 的時候,如果滿足條件有多臺,那麼最後一個總會覆蓋前面已經儲存到 destStoreID 裡面的資料,最終我們拿到的 destStoreID 有可能不是最優的。

// selectDestStore selects a target store to hold the region of the source region.
// We choose a target store based on the hot region number and flow bytes of this store.
func (h *balanceHotRegionsScheduler) selectDestStore(candidateStoreIDs []uint64, regionFlowBytes uint64, srcStoreID uint64, storesStat core.StoreHotRegionsStat) (destStoreID uint64) {
    sr := storesStat[srcStoreID]
    srcFlowBytes := sr.TotalFlowBytes
    srcHotRegionsCount := sr.RegionsStat.Len()

    var (
        minFlowBytes    uint64 = math.MaxUint64
        minRegionsCount        = int(math.MaxInt32)
    )
    for _, storeID := range candidateStoreIDs {
        if s, ok := storesStat[storeID]; ok {
            if srcHotRegionsCount-s.RegionsStat.Len() > 1 && minRegionsCount > s.RegionsStat.Len() {
                destStoreID = storeID
                minFlowBytes = s.TotalFlowBytes
                minRegionsCount = s.RegionsStat.Len()
                continue // 這裡
            }
            if minRegionsCount == s.RegionsStat.Len() && minFlowBytes > s.TotalFlowBytes &&
                uint64(float64(srcFlowBytes)*hotRegionScheduleFactor) > s.TotalFlowBytes+2*regionFlowBytes {
                minFlowBytes = s.TotalFlowBytes
                destStoreID = storeID
            }
        } else {
            destStoreID = storeID
            return
        }
    }
    return
}
複製程式碼

4.2 PD 重要監控指標詳解之 HotRegion:

  • Hot write Region’s leader distribution:每個 TiKV 例項上是寫入熱點的 leader 的數量
  • Hot write Region’s peer distribution:每個 TiKV 例項上是寫入熱點的 peer 的數量
  • Hot write Region’s leader written bytes:每個 TiKV 例項上熱點的 leader 的寫入大小
  • Hot write Region’s peer written bytes:每個 TiKV 例項上熱點的 peer 的寫入大小
  • Hot read Region’s leader distribution:每個 TiKV 例項上是讀取熱點的 leader 的數量
  • Hot read Region’s peer distribution:每個 TiKV 例項上是讀取熱點的 peer 的數量
  • Hot read Region’s leader read bytes:每個 TiKV 例項上熱點的 leader 的讀取大小
  • Hot read Region’s peer read bytes:每個 TiKV 例項上熱點的 peer 的讀取大小

本次我們只 hack 驗證了 Write Region Leader 這部分,所以我們重點關注一下監控和問題:

  • Hot write Region's leader distribution

監控資料有一定的延時(粗略估計1-2分鐘)

5. 模擬熱點資料

  • 從本地往伺服器 load 資料:

修改 tidb-bench 的 Makefile#load 模組對應的主機地址,然後執行 make tbl, make load 即可往伺服器 load 資料了。

注意,這裡你也需要進行一些配置修改:--default-character-set utf8

犯的錯:受限於本地-伺服器間網路頻寬,匯入資料很慢。

  • 線上伺服器上:
$ ./go-ycsb run mysql -p mysql.host=10.9.x.x -p mysql.port=4000 -p mysql.db=test1 -P workloads/workloada
複製程式碼

注:go-ycsb 支援 insert,也支援 update,你可以根據你的需要進行相對應的調整 workloada#recordcountworkloada#operationcount 引數。

6.本地構建 rust-nb-server

rust 一天速成……

Demo Time 的時候聽好幾個團隊都說失敗了。我以前也嘗試過,但是被編譯的速度以及耗能給擊敗了。

環境都可以把你 de 自信心擊潰。

rustup install nightly
cargo run
...
複製程式碼

Mac 本地打包 Linux 失敗:缺少 std 庫,通過 Docker 臨時解決。

7. 導師指導

從比賽一開始,導師團就非常積極和主動,直接去每個專案組,給予直接指導和建議,我們遇到問題去找導師時,他們也非常的配合。

導師不僅幫我們解決問題(特別是熱點資料構建,包括對於程式碼級別的指導),還跟我們一起探討課題方向和實際可操作性,以及可以達到的目標。

非常感謝!!!

我們的準備和主動性真的不足,值得反思--也希望大家以後不要怕麻煩,有問題就大膽的去問。

Hackathon Demo

整個 Demo show 進行的非常順利,為每一個團隊點贊!

很多團隊的作品都讓人尖叫,可想而知他們的作品是多麼的酷炫和牛逼,印象中只有一個團隊在 Demo 環境出現了演示時程式崩潰的問題(用Java Netty 基於 TiKV 做的 memcache(實現了大部分的協議))。

Hackathon 頒獎

遺憾!!!

我們 DSG 團隊榮獲三等獎+最佳創意兩項大獎,但是很遺憾我未能跟團隊一起分享這一刻。

因為我要趕著去火車站,所以在週日下午6點的時候,我跟隊友和一些朋友道別後,我就去火車站了,後面幾組的 Demo Show 也很非常遺憾未能參加。

得獎感言:

謝謝 DSG 團隊,謝謝導師,謝謝評委老師,謝謝 PingCAP 給大家籌備了這麼好的一次黑客馬拉松比賽活動。

TiDB Hackathon 2018 總結

本次比賽的各個方面都做的完美,除了網路。

  1. 環境(一定要提前準備)----這次被坑了不少時間和精力;

  2. 配置文件中有一些注意事項,一定要認真閱讀:ext4 必須要每臺機器都更新;

  3. [10.9.97.254]: Ansible FAILED! => playbook: bootstrap.yml; TASK: check_system_optional : Preflight check - Check TiDB server's RAM; message: {"changed": false, "msg": "This machine does not have sufficient RAM to run TiDB, at least 16000 MB."} - 記憶體不足的問題

  • 可以在執行的時候增加引數來避免

  • ansible-playbook bootstrap.yml --extra-vars "dev_mode=True"

  1. 如果磁碟掛載有問題,可以重新清除資料後再重新啟動;
  • ansible-playbook unsafe_cleanup_data.yml

(https://github.com/pingcap/docs/blob/master/op-guide/ansible-operation.md)

參考資料

  1. github.com/pingcap/pd
  2. tidb-bench tpch
  3. github.com/pingcap/go-…
  4. Ansible 部署
  5. PD 重要監控指標詳解
  6. 使用 TiDB-Ansible 升級 TiDB
  7. 線上程式碼格式化
  8. rust-nb-server

後續楊文同學會在 個人部落格 中更新更多專案細節。

天真貝葉斯學習機 | TiDB Hackathon 優秀專案分享

相關文章