docker stats 跟 RSS的差距

今天用戶回報,他觀看memory usage的時候

發現使用docker stats指令看得使用量跟實際用ps aux看到的使用量有很大的差距

使用ps指令觀看RSS記憶體使用量

$ docker exec  my-app ps -o rss,vsz,sz 1
  RSS    VSZ    SZ
945120 23754744 5938686

RSS是物理記憶體使用量,當初設定記憶體監控的時候是以這個為準

可是登上server的時候使用docker指令去看卻差了很多

docker stats my-app
CONTAINER           CPU %               MEM USAGE / LIMIT   MEM %               NET I/O             BLOCK I/O           PIDS
my-app        14.02%              1.965GiB / 5GiB     33.31%              0B / 0B             8.25MB / 50.1MB     276

使用ps指令看到的只有900多MB,使用docker指令看卻有1.96GiB

上網收尋了一下資料

查到了

Note: On Linux, the Docker CLI reports memory usage by subtracting page cache usage from the total memory usage. The API does not perform such a calculation but rather provides the total memory usage and the amount from the page cache so that clients can use the data as needed.

使用docker stats的時候,顯示的是RSS+Cache的使用量

想要看詳細記憶體使用量可以用下面的指令

cat /sys/fs/cgroup/memory/docker/$CONTAINER_ID/memory.stat
cache 1203441664
rss 938504192
rss_huge 0
mapped_file 4300800
swap 0
pgpgin 4679655
pgpgout 4156719
pgfault 99018704
pgmajfault 388
inactive_anon 4096
active_anon 938471424
inactive_file 799027200
active_file 404389888
unevictable 0
hierarchical_memory_limit 5368709120
hierarchical_memsw_limit 10737418240
total_cache 1203441664
total_rss 938504192
total_rss_huge 0
total_mapped_file 4300800
total_swap 0
total_pgpgin 4679655
total_pgpgout 4156719
total_pgfault 99018704
total_pgmajfault 388
total_inactive_anon 4096
total_active_anon 938471424
total_inactive_file 799027200
total_active_file 404389888
total_unevictable 0

這會影響的是如果我設定docker container的memory limit的時候,必須考慮RSS+Cache的使用量才行

有個方法可以手動的清掉cache

$ free && sync && echo 3 > /proc/sys/vm/drop_caches && free

用上面的指令可以暫時舒緩container的記憶體使用

這邊做個紀錄

在k8s啟用rstudio的時候遇到302轉址的問題

研究k8s也有一段時間了,今天想從k8s上起一個rstudio搭配nginx controller來用用

當我把ingress的path設定成/rstudio的時候,如果我存取http://LB_URL/rstudio

他會回傳一個302 redirect轉址請求把我轉到http://LB_URL/auth-sign-in

原先設定的path跑掉了而出現404錯誤

ingress設定如下

{
  "kind": "Ingress",
  "apiVersion": "extensions/v1beta1",
  "metadata": {
    "annotations": {
      "kubernetes.io/ingress.class": "nginx",
      "nginx.ingress.kubernetes.io/rewrite-target": "/"  
    }
  },
  "spec": {
    "rules": [
      {
        "host": "LB_HOST",
        "http": {
          "paths": [
            {
              "path": "/rstudio",
              "backend": {
                "serviceName": "rstudio",
                "servicePort": 8787
              }
            }
          ]
        }
      }
    ]
  }
}

後來研究了一下這應該是nginx在轉發的時候出的問題

當有設定proxy_set_header Host xxxx的時候,redirect的對象會主動被改成xxxxx

所以需要新增proxy_redirect的設定把他轉回來

以前我在localhost設定nginx的時候設定如下

    location /rstudio/ {
      rewrite ^/rstudio/(.*)$ /$1 break;
      proxy_pass http://localhost:8787;
      proxy_redirect http://localhost:8787/ $scheme://$host/rstudio/;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
    }

那要怎麼在nginx-controller增加proxy_redirect的設定呢

查了一下官網有nginx.ingress.kubernetes.io/proxy-redirect-fromnginx.ingress.kubernetes.io/proxy-redirect-to可以用

又nginx controller在設定proxy的時候會自動新增一行proxy_set_header Host $best_http_host
得把proxy_redirect的對象改成$best_http_host

查看nginx controller config的方法如下

#查詢nginx controller的pod container名字
$ kubectl get pod -n ingress-nginx

#進入container
$ kubectl exec nginx-ingress-controller-xxxx -n ingress-nginx -i -t -- bash 

#查看config
$ cat etc/nginx/nginx.conf

修改過後的ingress設定如下

{
  "kind": "Ingress",
  "apiVersion": "extensions/v1beta1",
  "metadata": {
    "annotations": {
      "kubernetes.io/ingress.class": "nginx",
      "nginx.ingress.kubernetes.io/proxy-redirect-from": "http://$best_http_host/",
      "nginx.ingress.kubernetes.io/proxy-redirect-to": "$scheme://$host/rstudio/",
      "nginx.ingress.kubernetes.io/rewrite-target": "/"  
    }
  },
  "spec": {
    "rules": [
      {
        "host": "LB_HOST",
        "http": {
          "paths": [
            {
              "path": "/rstudio",
              "backend": {
                "serviceName": "rstudio",
                "servicePort": 8787
              }
            }
          ]
        }
      }
    ]
  }
}
            

這邊做個紀錄

讓nginx-controller使用external 80 port

在寫k8s使用nginx-controller的時候出現404這篇文章的時候

當時使用了NodePort去當Nginx-Controller的endpoint

設定檔如下

apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx
  namespace: ingress-nginx
spec:
  type: NodePort
  ports:
  - name: http
    port: 80
    targetPort: 80
    protocol: TCP
  - name: https
    port: 443
    targetPort: 443
    protocol: TCP
  selector:
    app: ingress-nginx

這樣有個問題,就是NodePort會隨機選一個port當做Nginx-Controller的出口

這樣一來每次restart就會不知道是什麼port

雖然我可以直接設定NodePort為80 port,但是k8s port range預設範圍是30000~32767

沒辦法指定80 port當作node port的設定

後來重新去看了官方文件,可以直接把type: NodePort改成type: LoadBalancer

這樣一來預設出口就是80 port了

檢查一下controller的endpoint

$ kubectl describe svc ingress-nginx -n ingress-nginx
Name:                     ingress-nginx
Namespace:                ingress-nginx
Labels:                   <none>
Annotations:              <none>
Selector:                 app=ingress-nginx
Type:                     LoadBalancer
IP:                       10.110.43.7
Port:                     http  80/TCP
TargetPort:               80/TCP
NodePort:                 http  31144/TCP
Endpoints:                10.46.0.6:80
Port:                     https  443/TCP
TargetPort:               443/TCP
NodePort:                 https  30474/TCP
Endpoints:                10.46.0.6:443
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

哎呀,拿到的IP是一個k8s的內部的IP,如次一來外面就無法存取了

後來查了一下可以替他設定externalIP,當作endpoint

修改了一下Nginx-Controller的endpoint設定

apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx
  namespace: ingress-nginx
spec:
  type: LoadBalancer
  externalIPs:
  - 你的external IP or VIP
  ports:
  - name: http
    port: 80
    targetPort: 80
    protocol: TCP
  - name: https
    port: 443
    targetPort: 443
    protocol: TCP
  selector:
    app: ingress-nginx

在重新檢查一下controller的endpoint

$ kubectl describe svc ingress-nginx -n ingress-nginx
Name:                     ingress-nginx
Namespace:                ingress-nginx
Labels:                   <none>
Annotations:              <none>
Selector:                 app=ingress-nginx
Type:                     LoadBalancer
IP:                       10.110.43.7
External IPs:             xxx.xxx.xxx.xxx
Port:                     http  80/TCP
TargetPort:               80/TCP
NodePort:                 http  31144/TCP
Endpoints:                10.46.0.6:80
Port:                     https  443/TCP
TargetPort:               443/TCP
NodePort:                 https  30474/TCP
Endpoints:                10.46.0.6:443
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

多了一個External IPs的設定

將service type改成LoadBalancer,然後設定externalIPs就可以讓Nginx-Controller使用external 80 port了

今天玩了一下這設定,總算有比較清楚service設定LoadBalancer的效果

k8s使用nginx-controller的時候出現404

最近再研究怎麼自己搭建HA k8s的集群

一個意外令我花了不少時間研究的問題是在於怎麼裝k8s的ingress-controller

一般來說要開放k8s上的服務給外邊的人使用的時候

簡單方法可以直接起一個NodePort的service把服務開放給別人

但這樣有一個麻煩一來不知道service會被部署到哪台伺服器,二來不知道會用哪個port

因此有個變通的方法是使用ingress

直接使用NodePort在存取服務的時候是使用

http://某個節點:某個port

這樣一個方式

而使用ingress的話,可以簡單變成

http://ingress-controller-url/某個路徑

這樣的好處是當服務一多的時候,有個統一的出口,只是配著不同的路徑

可以減少設定防火牆還是VIP等等時候的負擔

而一個ingress是個設定檔,單純描述服務跟path的映射像是下面這樣

...
    http:
      paths:
      - path: /app
        backend:
          serviceName: appsvc1
          servicePort: 80

除此之外還需要一個ingress-controller的角色,實際去擔任開放服務給外邊的任務

我這邊是使用nginx-ingress-controller去充當這任務
https://github.com/kubernetes/ingress-nginx

nginx-ingress-controller的簡化安裝方法如下

初始化nginx-ingress-controller環境

  1. 設定ingress-controller用的namespace
  2. 安裝default-backend
  3. 設定config
curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/namespace.yaml              | kubectl apply -f -
curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/default-backend.yaml        | kubectl apply -f -
curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/configmap.yaml              | kubectl apply -f -
curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/tcp-services-configmap.yaml | kubectl apply -f -
curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/udp-services-configmap.yaml | kubectl apply -f 

安裝nginx-ingress-controller

curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/rbac.yaml      | kubectl apply -f -
curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/with-rbac.yaml | kubectl apply -f -
curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/provider/baremetal/service-nodeport.yaml | kubectl apply -f -

上面裝完之後,nginx-ingress-controller會被安裝在某個node上並用NodePort的方式開放(測試時先這樣裝)

讓我卡住的地方是我在設定ingress的時候只能使用root path,如果使用其他path會回傳404的訊息

ingress設定如下

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: my-ingress
spec:
  rules:
  - host: ingress-controller-url
    http:
      paths:
      - path: /app   #失敗
        backend:
          serviceName: appsvc1
          servicePort: 80
      - path: /      #成功
        backend:
          serviceName: appsvc1
          servicePort: 80

一模一樣的設定,root path以外卻會失敗

後來我用下面方法登入了nginx-controller去看config

 $ get pod -n ingress-nginx
 ...
 nginx-ingress-controller-9fbd7596d-p8svz   1/1       Running   0          21h
 
 $ kubectl exec  nginx-ingress-controller-9fbd7596d-p8svz -n ingress-nginx  -i -t -- bash
 $ cat /etc/nginx/nginx.conf

兩者設定也是除了path之外一模一樣

後來重新看了官方的文件
https://kubernetes.io/docs/concepts/services-networking/ingress/

原來那些ingress-controller可以從annotations去設定參數

我的ingress少設定了一些參數,所以有些奇怪的狀況

更新後的ingress設定如下

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: my-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: ingress-controller-url
    http:
      paths:
      - path: /app 
        backend:
          serviceName: appsvc1
          servicePort: 80
      - path: /     
        backend:
          serviceName: appsvc1
          servicePort: 80

nginx.ingress.kubernetes.io/rewrite-target設定進去後就ok了這邊紀錄一下

題外話
ingress的host設定似乎可以把ingress-controller的endpoint設定到任一node上

還不暸解這之間的關係,似乎是kube-proxy把他導過去的

之後再來研究

新手玩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之類的指令直接抓檔案了