新手玩GO語言,自己動手寫一個prometheus的exporter

最近想動手寫寫GO,正好自己在用prometheus當作監控平台的基底

就想說來寫個prometheus的exporter吧

這篇文章的目的是讓一個GO新手也能從無到有的做一個prometheus的exporter

如果想直接看code請參考:https://github.com/del680202/dummy_exporter

首先,prometheus的exporter在監控平台中扮演著什麼角色

可以參考下圖

prometheus+grafana是現在主流的監控搭配之一

prometheus自身擔任database的角色,搭配grafana我們可以可視化的監控各種時間序列數據

但是prometheus的數據從哪來呢,他還需要一個exporter的角色,讓prometheus可以拿到想監控的數據

現在主流的軟體平台幾乎都有人製作專屬的exporter了,像是spark/flink/mysql等等

上github用exporter當關鍵字搜尋就可以很容易找到

有趣的是甚至還有人做了bitcoin exporter

只要能自己客製化prometheus的exporter,那麼任何東西都能以時間序列數據呈現及監控

有此念頭因此開始研究怎麼寫prometheus exporter

雖然prometheus exporter支持多種語言,但是目前主流看到的都來是GO為主

自己上網找了幾個範例,想要嘗試修改

嘗試了半天怎麼編譯都不成功,最後決定從了解怎麼配置GO的環境開始入手

題外話,我自己目前學習go是從下面兩個網站著手,這裏順便分享一下

配置GO環境

首先要安裝GO,我筆電是mac 就先從下載安裝GO開始吧

GO下載位置:https://golang.org/dl/

安裝好之後先確定環境變數

$ go env
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/terrence/Library/Caches/go-build"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/terrence/golang"
GORACE=""
GOROOT="/usr/local/go"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
...

這邊有兩個重要的環境變數,GOROOTGOPATH

之前拿別人的範例來改的時候,因為忽略了這兩個環境變數的設定,所以編譯一直出現dependency找不到的問題

會出現類似下面的錯誤

server.go:6:2: cannot find package "github.com/[username]/[appname]/models" in any of:
    /usr/local/opt/go/libexec/src/github.com/[username]/[appname]/models (from $GOROOT)
    /Users/[username]/src/github.com/[username]/[appname]/models (from $GOPATH)

這邊先配置go的環境變數

$ vim ~/.bash_profile
...
export GOROOT=/usr/local/go  
export GOPATH=$HOME/golang
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

$ source ~/.bash_profile

設置了三個變數

  • GOROOT: go的安裝路徑
  • GOPATH: go的工作根目錄,一定要有,不然import時會有各種奇怪的dependency錯誤
  • PATH: 增加$GOROOT/bin$GOPATH/bin這樣就可以直接用go的各種工具

這邊值得一提的$GOPATH/bin的配置,一開始我不知道怎麼配,使用go安裝的工具都無法用

GOPATH底下在作業過程中會產生三個目錄pkg, src, bin

其中src目錄,裡面存放各種專案目錄跟dependency,也是我們要進行開發的地方

而bin目錄,當使用go get還是go install時,會把可執行檔放進去

舉個例子,假設我想用glide這個工具去管理dependency

我要先用如下指令去安裝glide

$ go get github.com/Masterminds/glide

然後我就可以看到glide這個工具被放到$GOPATH/bin下面去了,而我$GOPATH/bin已經設定到PATH環境變數去了

因此接下來我就能直接使用glide這個工具

建立專案目錄

好了,GO的環境配置好了,接下來是要開發一個dummy_exporter當作範例

首先我要建立一個新的GO專案目錄,目前常看的的格式是$GOPATH/src/DOMAIN/USER_NAME/PROJECT_NAME

  • DOMAIN: 你放專案的網站,常見是github.com
  • USER_NAME: 假設我打算之後把專案同步到github,那就是我的github帳號
  • PROJECT_NAME: 你的專案名稱

當然也是可以做成比較扁平的架構, $GOPATH/src/PROJECT_NAME

不過這會影響的是自己之後import的路徑 這也是我剛開始踩到的雷

在go的專案常常會看到如下用法

import (
    "fmt"
    "github.com/prometheus/client_golang/prometheus"
    "net/http"
)

go的import路徑會是先去GOROOT下面找,下一個則是去GOPATH/src下面找

由於現在go的一些dependency管理工具,會用DOMAIN/USER_NAME/PROJECT_NAME的形式展開

所以import的用法也常常是DOMAIN/USER_NAME/PROJECT_NAME的格式

所以這邊,我們還是循這種命名方式去建立我們的專案目錄

# 建立專案目錄,並移動過去
$ USER_NAME=del680202
$ PROJECT_NAME=dummy_exporter
$ mkdir -p $GOPATH/src/github.com/$USER_NAME/$PROJECT_NAME
$ cd $GOPATH/src/github.com/$USER_NAME/$PROJECT_NAME

安裝dep並初始化專案

到了專案目錄下之後,我們要開始開發一個新的GO專案

這邊我們選用dep當作我們的dependency管理工具

現在許多專案也慢慢從glide開始移植到dep去了

參考:日文注意

安裝方法如下

$ go get -v github.com/golang/dep
$ go install -v github.com/golang/dep/cmd/dep

安裝好後使用init指令去初始化專案

$ dep init

初始化好後會產生三個東西

  • vendor
  • Gopkg.toml
  • Gopkg.lock

其中vendor是用來存放第三方dependency套件的

Gopkg.toml等不用自己編寫,這是depglide最大不同的地方,這工具是直接參考source code去下載dependency

開始寫一個dummy exporter

這裡就不細講GO的語法了,首先產生一個main.go

$ vim main.go
main.go
package main

import (
        "fmt"
        "github.com/prometheus/client_golang/prometheus"
        "net/http"
)

func main() {
        fmt.Println(`
  This is a dummy example of prometheus exporter
  Access: http://127.0.0.1:8081
  `)

        // Define parameters

        metricsPath := "/metrics"
        listenAddress := ":8081"

        // Launch http service

        http.Handle(metricsPath, prometheus.Handler())
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
                w.Write([]byte(`<html>
             <head><title>Dummy Exporter</title></head>
             <body>
             <h1>Dummy Exporter</h1>
             <p><a href='` + metricsPath + `'>Metrics</a></p>
             </body>
             </html>`))
        })
        fmt.Println(http.ListenAndServe(listenAddress, nil))
}

上面的code就是一個最簡單的prometheus exporter

只使用了github.com/prometheus/client_golang/prometheus這個外部包

其中用了http包去起了一個http service並用prometheus.Handler當作http handler

有了這支程式之後,可以用dep去下載對應的包

$ dep ensure

之後可以在vendor目錄下看到github.com/prometheus/client_golang/prometheus的套件被下載下來了

確認套件的方法可以使用dep status

$ dep status
PROJECT                                           CONSTRAINT     VERSION        REVISION  LATEST   PKGS USED
github.com/beorn7/perks                           branch master  branch master  4c0e845   4c0e845  1
github.com/golang/protobuf                        v1.0.0         v1.0.0         9255415   v1.0.0   1
github.com/matttproud/golang_protobuf_extensions  v1.0.0         v1.0.0         3247c84   v1.0.0   1
github.com/prometheus/client_golang               v0.8.0         v0.8.0         c5b7fcc   v0.8.0   1
github.com/prometheus/client_model                branch master  branch master  99fa1f4   99fa1f4  1
github.com/prometheus/common                      branch master  branch master  e4aa40a   e4aa40a  3
github.com/prometheus/procfs                      branch master  branch master  54d17b5   54d17b5  4

也可以輸出成圖片

$ dep status -dot | dot -Tpng -o pi-dependency.png

測試dummy exporter

測試的方法很簡單
可以用go run直接執行

$ go run main.go

或是編譯後在執行

$ go build
$ ./dummy_exporter    # build的結果會跟專案目錄同名

執行後看到如下訊息就是正確的

  This is a dummy example of prometheus exporter
  Access: http://127.0.0.1:8081
  

加入自己的metrics

有了一個dummy exporter之後試著加入自己的metrics吧

想要加入自己定義的metrics,需在啟動http service前註冊自己的exporter物件到prometheus

func main() {
...
  //註冊自己的exporter

  metricsPrefix := "dummy"
    exporter := NewExporter(metricsPrefix)
    prometheus.MustRegister(exporter)

    // Launch http service

    http.Handle(metricsPath, prometheus.Handler())
...

其中NewExporter是我們自己定義的

//定義兩個metric輸出

type Exporter struct {
    gauge    prometheus.Gauge
    gaugeVec prometheus.GaugeVec
}

func NewExporter(metricsPrefix string) *Exporter {
    gauge := prometheus.NewGauge(prometheus.GaugeOpts{
        Namespace: metricsPrefix,
        Name:      "gauge_metric",
        Help:      "This is a dummy gauge metric"})

    gaugeVec := *prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Namespace: metricsPrefix,
        Name:      "gauge_vec_metric",
        Help:      "This is a dummy gauga vece metric"},
        []string{"myLabel"})

    return &Exporter{
        gauge:    gauge,
        gaugeVec: gaugeVec,
    }
}

首先定義一個data struct,裡面包含prometheus的屬性,其他定義方法請參考flink_exporter

struct的結構不是太重要,只要包含prometheus的屬性就行了

prometheus目前支援4種類型的metrics

  • Counter: 重點方法inc ,一個累計型的metric
  • CounterVec: Counter支援Label
  • Gauge: 重點方法set,自己設定各種value 最常用
  • GaugeVec: Gauge支援Label
  • Histogram: 重點方法Observe,集計型的metric
  • HistogramVec: Histogram支援Label
  • Summary: 重點方法Observe,集計型的metric
  • SummaryVec: Summary支援Label

四種屬性都有vec的版本,差別在於支不支援Lable

沒Label: dummy_gauge_metric 0
有Label: dummy_gauge_vec_metric{myLabel="hello"} 0

有Label的話在使用grafana的時候可以設定一些條件來過濾,端看自己用途

這邊只是示範,只用了Gauge跟GaugeVec當範例

而重要的是NewExporter裡面對metrics的定義

gauge := prometheus.NewGauge(prometheus.GaugeOpts{
        Namespace: metricsPrefix,
        Name:      "gauge_metric",
        Help:      "This is a dummy gauge metric"})
gaugeVec := *prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Namespace: metricsPrefix,
        Name:      "gauge_vec_metric",
        Help:      "This is a dummy gauga vece metric"},
        []string{"myLabel"})

可以看到塞給Exporter的值來自prometheus的NewGauge等方法

參數

  • Namespace: 顯示在metric前面的名字
  • Name: 顯示在exporter上的名字
  • Help: 說明文字
  • Label名字:Vec類型才會有

下面舉個例子

prometheus.GaugeOpts{
        Namespace: "dummy",
        Name:      "gauge_metric",
        Help:      "This is a dummy gauge metric"}

用上面的參數做出來的metrics,在exporter上看起來會像是下面這樣

# HELP dummy_gauge_metric This is a dummy gauge metric
# TYPE dummy_gauge_metric gauge
dummy_gauge_metric 0

定義好exporter之後,接下來問題是怎麼收集/更新metrics

我們需要替這個exporter定義兩個方法

  • Collect: 實際收集metrics的function
  • Describe: 描述metrics

這兩個方法不能省略,一定要有

Describe理論上不用做什麼特別的事,只要讓exporter metrics呼叫Describe方法就好

而Collect則是要實作對metrics的收集

範例如下


//實作收集Metric的邏輯,這邊簡單起見直接設定成0

func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
    e.gauge.Set(float64(0))
    e.gaugeVec.WithLabelValues("hello").Set(float64(0))
    e.gauge.Collect(ch)
    e.gaugeVec.Collect(ch)
}

// 讓exporter的prometheus屬性呼叫Describe方法

func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
    e.gauge.Describe(ch)
    e.gaugeVec.Describe(ch)
}

如此一來就可以在dummy exporter上看到我們自己定義的dummy metrics了

最後,完整的code如下

main.go
package main

import (
    "fmt"
    "github.com/prometheus/client_golang/prometheus"
    "net/http"
)

type Exporter struct {
    gauge    prometheus.Gauge
    gaugeVec prometheus.GaugeVec
}

func NewExporter(metricsPrefix string) *Exporter {
    gauge := prometheus.NewGauge(prometheus.GaugeOpts{
        Namespace: metricsPrefix,
        Name:      "gauge_metric",
        Help:      "This is a dummy gauge metric"})

    gaugeVec := *prometheus.NewGaugeVec(prometheus.GaugeOpts{
        Namespace: metricsPrefix,
        Name:      "gauge_vec_metric",
        Help:      "This is a dummy gauga vece metric"},
        []string{"myLabel"})

    return &Exporter{
        gauge:    gauge,
        gaugeVec: gaugeVec,
    }
}

func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
    e.gauge.Set(float64(0))
    e.gaugeVec.WithLabelValues("hello").Set(float64(0))
    e.gauge.Collect(ch)
    e.gaugeVec.Collect(ch)
}

func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
    e.gauge.Describe(ch)
    e.gaugeVec.Describe(ch)
}

func main() {
    fmt.Println(`
  This is a dummy example of prometheus exporter
  Access: http://127.0.0.1:8081
  `)

    // Define parameters

    metricsPath := "/metrics"
    listenAddress := ":8081"
    metricsPrefix := "dummy"

    // Register dummy exporter, not necessary

    exporter := NewExporter(metricsPrefix)
    prometheus.MustRegister(exporter)

    // Launch http service

    http.Handle(metricsPath, prometheus.Handler())
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(`<html>
             <head><title>Dummy Exporter</title></head>
             <body>
             <h1>Dummy Exporter</h1>
             <p><a href='` + metricsPath + `'>Metrics</a></p>
             </body>
             </html>`))
    })
    fmt.Println(http.ListenAndServe(listenAddress, nil))
}

架設一個私有PyPI去裝自己python套件

這幾天自己寫了一個公司內部用的python library
想要找個方便部署的方式,如果能用pip去安裝就最理想了

想使用pip安裝自己的python library需要寫個setup.py然後發佈到PyPI
但是又不能公開到外面的PyPI
後來找了一下安裝私有PyPI的方式這裡記錄一下用法

懶人安裝方法如下

# 建立library存放目錄
$ mkdir -p /data/pypi/packages
$ cd /data/pypi
  
# 安裝 pypiserver
$ pip install pip --upgrade
$ pip install pypiserver
$ pip install passlib
 

# 啟動沒驗證的pypi-server
# $ pypi-server -p 9001 /data/pypi/packages &

# 啟動需要驗證的pypi-server

# create .htpasswd
$ htpasswd -b -m -c .htpasswd terrence mypasswd
$ pypi-server -p 9001 -P /data/pypi/.htpasswd /data/pypi/packages &

這樣就可以有個簡易版的PyPI
但是這個裝法弄起來的PyPI似乎不太穩定 之前遇過他死機幾次
不過當下來講也夠用了

安裝完了PyPI接下來介紹怎麼用

假設這個私有PyPI裝在myhost上

  • 使用pip安裝
$ pip install --extra-index-url http://myhost:9001/ PACKAGE

如果遇到連線安全問題可以加個trusted-host設定

$ pip install --extra-index-ur http://myhost:9001/ --trusted-host myhost PACKAGE
  • 使用easy_install安裝
easy_install -i http://myhost:9001/simple/ PACKAGE
  • 上傳自己的library

如果想上傳自己的library的話,先寫好setup.py
setup.py怎麼寫可以上github找別人的範例參考

然後修改~/.pypirc

[distutils]
index-servers =
  pypi
  local
 
[pypi]
username:
password:
 
[local]
repository: http://myhost:9001
username: terrence
password: mypasswd

然後用下面的指令上傳

$ python setup.py sdist upload -r local

如此一來就可以把自己的library上傳到PyPI了

Spark因為kinit跑在mesos上遇到的雷

今天遇到了在Mesos cluster A上跑的Spark Job,移植到Mesos cluster B上面的時候突然無法生成executor

開啟DEBUG模式出現了以下訊息

...
18/02/22 04:25:04 DEBUG MesosCoarseGrainedSchedulerBackend: Declining offer: 221f008d-6173-485e-aa0e-a810ea44c45c-O418 with attributes: Map(rack -> value: "dummy1"
, az -> value: "A"
) mem: 127702.0 cpu: 32.0 port: List((31000,32000))  (reason: Offer was declined due to unmet task launch constraints.)

嗯?明明資源都夠,也沒有設定constraints卻出現了unmet訊息?
啟動指令如下,只要了一個2G的executor

bin/pyspark --conf spark.executor.cores=1 --conf spark.cores.max=1

後來查詢Mesos的管理介面,我明明是用root身份去跑的spark,出現的身份卻是hadoopuser
訊息如下

Name:PySparkShellWeb 
UI:http://xxxxxx:4040
User:hadoopuser
Roles:*
Principal:
Registered:
....

翻遍了spark conf都沒有hadoopuser相關設定

後來發現我在執行spark前有用kinit去初始化hadoopuser的keytab

我在Mesos cluster A有hadoopuser,在Mesos cluster B上則沒有

原本我以為我用kinit只會影響我存取hadoop的資源

沒想到卻也改了我在Mesos上的啟動user

後來用kdestroy把kinit快取拿掉,啟動user變回root就解決了這問題

而即便我不使用kinit,改用spark參數依然有同樣的問題

bin/pyspark --conf spark.executor.cores=1 --conf spark.cores.max=1 --keytab=/etc/hadoopuser.keytab  --principal=hadoopuser

目前最直接的解決方法就是在Mesos cluster B上把hadoopuser加上去,這樣就能正確要到資源了

HUE/beeline/python + kerberos+Hadoop實現User Impersonation

最近接到了一個需求

要在使用了kerberos做驗證的Hadoop上,使用HUE等服務

以往我這邊的用法都是直接每個user申請自己的keytab然後用kinit去通過認證

但是如果使用了HUE服務就要得使用共享的keytab才行,但是又必須達成個人帳號的區分

單純的Hadoop的話直接新增一個hue user然後將其設定成proxy user就行了
http://gethue.com/how-to-configure-hue-in-your-hadoop-cluster/

但現在加上了kerberos,這要怎麼達成讓我煩惱了一陣子

我有兩個目標

  • 讓不同user使用他們自己的LDAP account使用一個共享的keytab去存取kerberos認證的hadoop
  • 儘管使用共享的keytab仍可以區分各個user

後來研究了一下,把做法紀錄在這邊

首先必須做成一個3-parts的keytab

當個人用戶申請自己的kerberos keytab的時候申請的是2-parts的keytab
其principal格式如下

USERNAME@REALM_DOMAIN

而像hue這種proxy user則需要申請3-parts的keytab來綁定某個host
其principal格式如下

USERNAME/HOST@REALM_DOMAIN

kerberos keytab怎麼製作,這邊就跳過了

首先就是想辦法把hue的3-parts keytab hue.keytab弄到手

這邊直接展示hue.ini (HUE 4.1)的設定

...
  [[kerberos]]

    # Path to Hue's Kerberos keytab file
    ## hue_keytab=
    hue_keytab=/etc/hue.keytab
    # Kerberos principal name for Hue
    ## hue_principal=hue/hostname.foo.com
    hue_principal=hue/my_hue_server.com@my_realm_server.com
    # Frequency in seconds with which Hue will renew its keytab
    ## keytab_reinit_frequency=3600
    # Path to keep Kerberos credentials cached
    ## ccache_path=/var/run/hue/hue_krb5_ccache
    # Path to kinit
    ## kinit_path=/path/to/kinit
    kinit_path=/bin/kinit

    # Mutual authentication from the server, attaches HTTP GSSAPI/Kerberos Authentication to the given Request object
    ## mutual_authentication="OPTIONAL" or "REQUIRED" or "DISABLED"
    mutual_authentication=DISABLED
...

只要keytab有了,HUE的kerberos設定對了,其他hadoop設定上去就能讓HUE使用kerberos認證的hadoop
同時每個user仍能keep他們的帳戶在hadoop上

接下來是我想藉著beeline/python存取Hive
如何有同樣的效果
關鍵是hive.server2.proxy.user這個設定,他代表了被proxy的user,也就是在hadoop看到的名字
https://www.cloudera.com/documentation/enterprise/5-8-x/topics/cdh_sg_hiveserver2_security.html

不管是用beeline還是python
首先先用hue user的keytab初始化

$ kinit -kt /etc/hue.keytab hue/my_hue_server.com@my_realm_server.com

在沒有設定hive.server2.proxy.user的時候,因為是用了hue user的keytab初始化

在hadoop上的身份就會是hue

再來就是想辦法把hive.server2.proxy.user設定上去

  • beeline
$ $HIVE_HOME/bin/beeline -u "jdbc:hive2://$HIVESERVER2_HOST:HIVESERVER2_PORT/;principal=hive/$HIVESERVER2_HOST@my_realm_server.com;hive.server2.proxy.user=terrence" -n "" -p ""

這邊要注意的是使用beeline連接hiveserver2的時候,principal要設定成hive user的

  • python

python則可以用impyla這個套件,pyhs2有點舊了這次先不測
先安裝impyla所需套件

$ pip install impyla 
$ pip install thrift_sasl==0.2.1 #https://github.com/cloudera/impyla/issues/268
$ yum install cyrus-sasl-*

然後用如下方法初始化測試

from impala.dbapi import connect
with connect(host='HIVESERVER2_HOST',port=HIVESERVER2_PORT, auth_mechanism='GSSAPI', kerberos_service_name='hive', database='default') as conn:
    #從cursor的configuration設定hive.server2.proxy.user

    with conn.cursor(configuration={'hive.server2.proxy.user':'terrence'}) as cursor:
        cursor.execute(" select count(1) from my_test_table where dt='2018-01-01'")
        print(cursor.fetchall())

上面的方法,儘管我使用的hue的keytab,但是在hadoop上執行的user仍是我的名字terrence

這樣一來就這次的目標就算達到了

建立一個支援LDAP登入+Python2/3的JupyterHub

這幾天被要求弄一個Jupyter給部門的同事用

但是讓人煩惱的是Jupyter預設似乎沒那麼容易支援支援多用戶

後來發現有一個JupyterHub可以用
https://github.com/jupyterhub/jupyterhub

這邊紀錄一下建置方法

先安裝Anaconda2跟Anaconda3來支援python 2/3 以python3為主

$ cd /usr/local
$ wget https://repo.continuum.io/archive/Anaconda3-5.0.1-Linux-x86_64.sh
$ sh Anaconda3-5.0.1-Linux-x86_64.sh
...
Anaconda3 will now be installed into this location:
/root/anaconda3

  - Press ENTER to confirm the location
  - Press CTRL-C to abort the installation
  - Or specify a different location below

[/root/anaconda3] >>> /usr/local/anaconda3
..
to PATH in your /root/.bashrc ? [yes|no]
[no] >>> yes


$ wget https://repo.continuum.io/archive/Anaconda2-5.0.1-Linux-x86_64.sh
$ sh Anaconda2-5.0.1-Linux-x86_64.sh
Anaconda3 will now be installed into this location:
/root/anaconda3

  - Press ENTER to confirm the location
  - Press CTRL-C to abort the installation
  - Or specify a different location below

[/root/anaconda3] >>> /usr/local/anaconda2
..
to PATH in your /root/.bashrc ? [yes|no]
[no] >>> no


# check version
$ source ~/.bashrc
$ python -V
Python 3.6.3 :: Anaconda, Inc.

安裝python2 kernel到python3 下

/usr/local/anaconda2/bin/python -m pip install ipykernel
/usr/local/anaconda2/bin/python -m ipykernel install --prefix=/usr/local/anaconda3 --name 'python2'

安裝jupyterhub

$ conda install -c conda-forge jupyterhub
$ conda install notebook
$ pip install jupyterhub-ldapauthenticator
$ jupyterhub --generate-config
$ mkdir -p /etc/juypterhub
$ mv jupyterhub_config.py /etc/juypterhub

# Edit config
$ vim /etc/juypterhub/jupyterhub_config.py
...
c.JupyterHub.ip = '0.0.0.0'
c.JupyterHub.hub_ip = '0.0.0.0'
c.JupyterHub.hub_port = 8081
c.JupyterHub.port = 80
c.JupyterHub.authenticator_class = 'ldapauthenticator.ldapauthenticator.LDAPLocalAuthenticator'
c.LDAPAuthenticator.bind_dn_template = 'uid={username},ou=people,ou=account,ou=development,o=example'
c.LDAPAuthenticator.server_address = 'ldap://myldap.com'
c.LocalAuthenticator.create_system_users = True
c.LocalAuthenticator.add_user_cmd = ['useradd', '-m']
c.LDAPAuthenticator.use_ssl = False

這邊要注意一點,我使用了LDAPLocalAuthenticator當作驗證類別
因為我希望LDAP帳號可以自動在local建立對應的home dir,不然的會出現如下錯誤

Traceback (most recent call last):
  File "/usr/local/anaconda3/lib/python3.6/site-packages/tornado/web.py", line 1511, in _execute
    result = yield result
  File "/usr/local/anaconda3/lib/python3.6/site-packages/jupyterhub/handlers/login.py", line 94, in post
    yield self.spawn_single_user(user)
  File "/usr/local/anaconda3/lib/python3.6/site-packages/jupyterhub/handlers/base.py", line 475, in spawn_single_user
    yield gen.with_timeout(timedelta(seconds=self.slow_spawn_timeout), finish_spawn_future)
  File "/usr/local/anaconda3/lib/python3.6/site-packages/jupyterhub/handlers/base.py", line 445, in finish_user_spawn
    yield spawn_future
  File "/usr/local/anaconda3/lib/python3.6/site-packages/jupyterhub/user.py", line 439, in spawn
    raise e
  File "/usr/local/anaconda3/lib/python3.6/site-packages/jupyterhub/user.py", line 378, in spawn
    ip_port = yield gen.with_timeout(timedelta(seconds=spawner.start_timeout), f)
  File "/usr/local/anaconda3/lib/python3.6/types.py", line 248, in wrapped
    coro = func(*args, **kwargs)
  File "/usr/local/anaconda3/lib/python3.6/site-packages/jupyterhub/spawner.py", line 968, in start
    env = self.get_env()
  File "/usr/local/anaconda3/lib/python3.6/site-packages/jupyterhub/spawner.py", line 960, in get_env
    env = self.user_env(env)
  File "/usr/local/anaconda3/lib/python3.6/site-packages/jupyterhub/spawner.py", line 947, in user_env
    home = pwd.getpwnam(self.user.name).pw_dir

而LDAPLocalAuthenticator目前沒有實作在官方的ldapauthenticator裡面
必須自己上patch 請參考: https://github.com/jupyterhub/ldapauthenticator/pull/36/files

$ wget https://patch-diff.githubusercontent.com/raw/jupyterhub/ldapauthenticator/pull/36.patch
$ patch -u /usr/local/anaconda3/lib/python3.6/site-packages/ldapauthenticator/ldapauthenticator.py < 36.patch

之後可以用如下command啟動

#前端模式
$ jupyterhub --no-ssl --config=/etc/juypterhub/jupyterhub_config.py
#背景模式, log導到syslog
$ jupyterhub --no-ssl --config=/etc/juypterhub/jupyterhub_config.py  | logger -t jupyterhub &

備註1:

我在測試途中有踩到一個ldap驗證的問題

  File "/usr/local/anaconda3/lib/python3.6/site-packages/tornado/web.py", line 1511, in _execute
    result = yield result
  File "/usr/local/anaconda3/lib/python3.6/site-packages/jupyterhub/handlers/login.py", line 83, in post
    user = yield self.login_user(data)
  File "/usr/local/anaconda3/lib/python3.6/site-packages/jupyterhub/handlers/base.py", line 328, in login_user
    authenticated = yield self.authenticate(data)
  File "/usr/local/anaconda3/lib/python3.6/site-packages/jupyterhub/auth.py", line 227, in get_authenticated_user
    authenticated = yield self.authenticate(handler, data)
  File "/usr/local/anaconda3/lib/python3.6/types.py", line 248, in wrapped
    coro = func(*args, **kwargs)
  File "/usr/local/anaconda3/lib/python3.6/site-packages/ldapauthenticator/ldapauthenticator.py", line 109, in authenticate
    if conn.bind():
  File "/usr/local/anaconda3/lib/python3.6/site-packages/ldap3/core/connection.py", line 594, in bind
    self.refresh_server_info()
  File "/usr/local/anaconda3/lib/python3.6/site-packages/ldap3/core/connection.py", line 1315, in refresh_server_info
    self.server.get_info_from_server(self)
  File "/usr/local/anaconda3/lib/python3.6/site-packages/ldap3/core/server.py", line 446, in get_info_from_server
    self._get_schema_info(connection)
  File "/usr/local/anaconda3/lib/python3.6/site-packages/ldap3/core/server.py", line 431, in _get_schema_info
    self._schema_info.other[attribute] = format_attribute_values(self._schema_info, attribute, self._schema_info.raw[attribute], self.custom_formatter)
  File "/usr/local/anaconda3/lib/python3.6/site-packages/ldap3/protocol/formatters/standard.py", line 200, in format_attribute_values
    formatted_values = [formatter(raw_value) for raw_value in values]  # executes formatter
TypeError: 'NoneType' object is not iterable

似乎是因為ldap3 2.4.0這個版本沒有處理到空的attribute,而理論上LDAP協議裡面也不該出現空的attribute
這應該是作為LDAP Server的實作有不完整的地方
當前dev的版本已經修掉了這個issue且應該會修正到2.4.1,在新版本出來之前,可以先用下面方法對應

$ pip uninstall ldap3
$ git clone -b dev https://github.com/cannatag/ldap3.git
$ cd ldap3
$ python setup.py install

備註2:

如果想用nginx當proxy的話,記得要注意base_url的對應
假設nginx設定/jupyterhub為代理路徑

    location /jupyterhub {
      proxy_pass http://jupyterhub_url:80;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
      proxy_set_header Origin "";
      client_max_body_size 5000m;
      proxy_connect_timeout 1d;
      proxy_send_timeout 1d;
      proxy_read_timeout 1d;
    }

base_url則要跟著設定成/jupyterhub

jupyterhub --no-ssl --config=/etc/juypterhub/jupyterhub_config.py --base-url=/jupyterhub

用Apahce+LDAP自建WebDAV當binary repository

這一陣子在用Ansible建立一套平台

當中遇到了一個問題,當使用Ansible去部署application的時候

對於一個需要編譯的application來講每次部署都會自動的在觸發編譯一次

這樣效率有點慢,後來想起來我前公司有用WebDAV去讓人放binary files來加速部署

今天試著自己把WebDAV建起來順變做個紀錄

環境

  • CentOS 7.3
  • Apache 2.4

然後我希望這個webDAV

  • 對於下載沒有限制
  • 對於上傳刪除需要LDAP帳號密碼認證

Apahce內建就有webDAV的功能了,直接用Apache就好

安裝部署如下

# Install apache

$ yum -y install httpd mod_ldap
$ httpd -version
>Server version: Apache/2.4.6 (CentOS)
>Server built:   Nov 14 2016 18:04:44
$ systemctl enable httpd.service
$ systemctl status httpd.service
 
# Make webdav directory

$ mkdir -p /data_hdd/webdav
$ chown apache /data_hdd/webdav
$ chgrp apache /data_hdd/webdav
$ chmod 775    /data_hdd/webdav
$ ls    -ld    /data_hdd/webdav
 
# Check config

$ cat /etc/httpd/conf.modules.d/00-dav.conf
$ vim /etc/httpd/conf.d/dav.conf
DAVLockDB "/tmp/WebDAV.lock"
Alias /webdav "/data_hdd/webdav"
<Directory /data_hdd/webdav>
    DAV On
    Options Indexes MultiViews
    AuthType Basic
    AuthName "intra-auth"
    AuthBasicProvider          ldap
    AuthLDAPGroupAttribute     groupOfNames
    AuthLDAPURL                ldap://$LDAP_URL/ou=people,ou=account,ou=development,o=example.com?uid
    require ldap-group         ou=authgroup,ou=development,o=example.com
    <RequireAny>
        Require method GET POST OPTIONS
        Require valid-user
    </RequireAny>
</Directory>
 
 
# Start apache

$ systemctl restart httpd.service

apache config其中的LDAP設定,AuthLDAPURL,AuthLDAPURL等等
LDAP相關找帳號設定需要依據實際的LDAP設定去修正才行

架好之後去http://你的host/webdav/ 就可以看到一個可以下載的位置列表

使用方法:

建立一個新的資料夾

curl -X MKCOL --user <YOUR_SHORT_ACCOUNT> <WEBDAV_URL>/<FOLDER_NAME>

Example:

curl -X MKCOL --user 你的LDAP帳號 http://你的host/webdav/test2

刪除一個資料夾

curl -X DELETE --user <YOUR_SHORT_ACCOUNT> <WEBDAV_URL>/<FOLDER_NAME>/

Example:

curl -X DELETE --user 你的LDAP帳號 http://你的host/webdav/test2

Note: "/" MUST exists after folder name.

上傳檔案

curl -T <LOCAL_FILE_TO_UPLOAD> --user <YOUR_SHORT_ACCOUNT> <WEBDAV_URL>/<FOLDER_NAME>

Example

curl -T ~/Downloads/oracle_BlueKai.pdf --user 你的LDAP帳號 http://你的host/webdav/test2

之後就可以用像wget之類的指令直接抓檔案了

Presto處理並行job時的效能問題

這一陣子在替人搭建Presto當做data warehouse,同時使用airbnb的superset當做資料可視化的工具

但是在測試過程中遇到了一個問題,superset的dashboard會"同時"的丟許多query到presto查詢(大約十幾個)

整個dashboard既然因為回應太慢而出現timeout錯誤

一開始以為是不是因為presto不支援並行job的處理

找了一下網路上資料
http://teradata.github.io/presto/docs/148t/admin/tuning.html#tuning-pref-task

照到理應該會支援並行job才對阿

但是實際測試結果,假設我一個Query需要花N秒,那我同時丟20個Query就必須大約等20*N秒才會有結果

測試腳本如下

prestocli='java -jar /usr/local/prestocli.jar --server server1 --catalog hive --schema default'
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20;
do
   time $prestocli --execute "SELECT count(1) FROM hive.mydb.mytable LIMIT 100" &
done

SELECT count(1) FROM hive.mydb.mytable LIMIT 100這個query單一執行原本只需要2秒
但是我同時丟20次,他要將近35~40秒才會全部跑完

後來上網查了一下也有人遇過類似的問題
https://groups.google.com/forum/#!topic/presto-users/IEdzrGdMNXQ

後來有找到可以從Query Config去調整一些參數,其中有個user_pipeline.${USER}預設值只有1
https://prestodb.io/docs/current/admin/queue.html

死馬當活馬醫試著把他加入環境中
首先修改$PRESTO_HOME/presto/etc/config.properties指定Query Config位置

$PRESTO_HOME/presto/etc/config.properties
...
query.queue-config-file=/usr/local/presto/etc/presto-queues.json

並且新增一個presto-queues.json

/usr/local/presto/etc/presto-queues.json
{
  "queues": {
    "user.${USER}": {
      "maxConcurrent": 100,
      "maxQueued": 200
    }
  },
  "user_pipeline.${USER}": {
      "maxConcurrent": 10,
      "maxQueued": 20
    },
    "pipeline": {
      "maxConcurrent": 10,
      "maxQueued": 100
    },
  "rules": [
    {
      "queues": ["user.${USER}"]
    }
  ]
}

後來又測試了一次,這次結束所花時間大約是28~32秒

嗯..沒有很明顯的改善,後來想到presto可以支援多個coordinator

那如果我把query的台數增多呢?

後來增加了兩台coordinator,總共三台coordinator來幫忙

重新測試一次,測試腳本如下

prestocli0='java -jar /usr/local/prestocli.jar --server server1 --catalog hive --schema default'
prestocli1='java -jar /usr/local/prestocli.jar --server server2 --catalog hive --schema default'
prestocli2='java -jar /usr/local/prestocli.jar --server server3 --catalog hive --schema default'
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20;
do
   ind=$(($i % 3))
   cmd=prestocli$ind
   time ${!cmd} --execute "SELECT count(1) FROM hive.mydb.mytable LIMIT 100" &
done

這次所花時間約是20~25秒,這次效果好多了
或許還有調整空間,以候有機會在試試

tensorflow升級到1.0的問題

前幾天tensorflow 1.0出來了,很順手的把手邊環境升級上去

這不升級還好一升級以前寫的一些程式一堆都不能跑了

這次升到1.0一堆程式的界面都被改掉了

像是原本LSTM cell是放在tf.nn.rnn_cell.BasicLSTMCell下,新版的移動到了tf.contrib.rnn.BasicLSTMCell

等等諸如此類,哇勒個去,看到快昏倒

tensorflow官方有放個升級手冊,這次升版的好處我還沒享受到 壞處倒是踩了一堆
https://www.tensorflow.org/install/migration

HDP 2.5 + YARN 升級到Spark 2

前言

我使用著HDP 2.5+YARN作+Spark 1.6為我的hadoop群集的環境

最近遇到一個狀況,一個使用者想要去查詢一個很巨大得hive table
這個hive table有30萬個左右的partition跟幾百TB的資料
使用Spark 1.6的spark sql去跑的話會直接出現記憶體不足的錯誤

java.lang.OutOfMemoryError: GC overhead limit exceeded
        at java.util.Arrays.copyOfRange(Arrays.java:3664)
        at java.lang.String.<init>(String.java:207)
        at java.lang.StringBuilder.toString(StringBuilder.java:407)
        at java.io.ObjectStreamClass.getClassSignature(ObjectStreamClass.java:1552)
        at java.io.ObjectStreamClass.getMethodSignature(ObjectStreamClass.java:1567)
        at java.io.ObjectStreamClass.access$2500(ObjectStreamClass.java:72)
        at java.io.ObjectStreamClass$MemberSignature.<init>(ObjectStreamClass.java:1892)
        at java.io.ObjectStreamClass.computeDefaultSUID(ObjectStreamClass.java:1819)
        at java.io.ObjectStreamClass.access$100(ObjectStreamClass.java:72)
        at java.io.ObjectStreamClass$1.run(ObjectStreamClass.java:253)
        at java.io.ObjectStreamClass$1.run(ObjectStreamClass.java:251)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.io.ObjectStreamClass.getSerialVersionUID(ObjectStreamClass.java:250)
        at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:611)
        at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1630)
        at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521)
        at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1630)
        at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521)
        at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1781)
        at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)
        at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2018)
        at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1942)
        at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1808)
        at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)
        at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2018)
        at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1942)
        at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1808)
        at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)
        at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2018)
        at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1942)
        at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1808)
        at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)

後來研究了一下發現spark 2之後可以解決這問題
就決定將環境升級到Spark 2了

做法

原本想說Spark 2直接抓下來應該就可以用了,沒想到遇到一堆奇怪的狀況
這邊把步驟記錄一下
參考:https://community.hortonworks.com/articles/53029/how-to-install-and-run-spark-20-on-hdp-25-sandbox.html

首先把最新的spark包給抓下來,我現在抓的是spark-2.1.0-bin-hadoop2.7.tgz 放到家目錄下
之後操作如下

$ sudo mkdir /usr/hdp/current/spark2-client
$ tar -xvf spark-2.1.0-bin-hadoop2.7.tgz
$ cd /usr/hdp/current/spark2-client
$ sudo mv ~/spark-2.1.0-bin-hadoop2.7/* .

複製舊spark client的設定檔

$ sudo cp ../spark-client/conf/* conf/

到這步為止spark 2的client還不能用
需要新增確定幾個設定
先將spark-env.sh的裏面舊路徑換成新的路徑

$ sudo vim conf/spark-env.sh

export SPARK_CONF_DIR=${SPARK_CONF_DIR:-/usr/hdp/current/spark2-client/conf}
....

修改conf/spark-defaults.conf 新增如下設定

sudo vim conf/spark-defaults.conf

...
spark.driver.extraLibraryPath /usr/hdp/current/hadoop-client/lib/native
spark.executor.extraLibraryPath /usr/hdp/current/hadoop-client/lib/native
 
spark.driver.extraJavaOptions -Dhdp.version=2.5.0.0-817
spark.yarn.am.extraJavaOptions -Dhdp.version=2.5.0.0-817
# Required: setting this parameter to 'false' turns off ATS timeline server for Spark

spark.hadoop.yarn.timeline-service.enabled false
...

這一步非常重要,沒有做的話使用yarn模式會跑不起來

之後就可以跑跑看正不正常了

$ bin/spark-shell  --master=yarn-client

2017/02/20

Spark2的client配置好之後,原本想讓Zeppelin直接使用的
但是一直跑出

...
$PWD/mr-framework/hadoop/share/hadoop/mapreduce/lib/*:$PWD/mr-framework/hadoop/share/hadoop/common/*:$PWD/mr-framework/hadoop/share/hadoop/common/lib/*:$PWD/mr-framework/hadoop/share/hadoop/yarn/*:$PWD/mr-framework/hadoop/share/hadoop/yarn/lib/*:$PWD/mr-framework/hadoop/share/hadoop/hdfs/*:$PWD/mr-framework/hadoop/share/hadoop/hdfs/lib/*:$PWD/mr-framework/hadoop/share/hadoop/tools/lib/*:/usr/hdp/${hdp.version}/hadoop/lib/hadoop-lzo-0.6.0.${hdp.version}.jar:
/etc/hadoop/conf/secure: bad substitution

這樣一個錯誤訊息,後來查詢了一下問題出在${hdp.version}無法被解析
我嘗試了各種設定參數的方式都沒辦法成功,後來終於找到一個可以work的方法
修改mapred-site.xml新增設定如下

...
    <property>
      <name>hdp.version</name>
      <value>2.5.xxxxx</value>
    </property>
...

其中hdp.version填入環境的HDP版本即可
這邊做個記錄

maven針對單一class做unit test的方法

這一陣子在研究zeppelin的source code
裏面有一堆的子專案,子專案又有許許多多大大小小的unit test
稍微研究了一下如果今天我想要針對特定class做unit test的時候指令要怎麼下

指令如下

$ mvn  -pl livy,zeppelin-interpreter -DfailIfNoTests=false  -Dtest=LivySQLInterpreterTest -Dcheckstyle.skip clean test 

我想要針對LivySQLInterpreterTest執行unitest
除了最基本的mvn clean test之外,需要而外指定幾個參數

  • -Dtest:想要執行unit test的類別
  • -pl: 要包含哪些子專案,我的情況要針對livy做測試,但是livy會依賴zeppelin-interpreter,所以兩個一起打包
  • -DfailIfNoTests:允許子專案不做測試,因為我沒有針對zeppelin-interpreter執行測試,所以要設定這選項
  • -Dcheckstyle.skip:不做style check

而專案結構不複雜的時候只要指定-Dtest就行了

$ mvn -Dtest=... clean test

以上,做個筆記