Redis Ha HAProxy

Share on:

2020 It邦幫忙鐵人賽 系列文章

由於我比較熟悉 GCP / GKE 的服務,這篇的操作過程都會以 GCP 平台作為範例,不過操作過程大體上是跨平台通用的。

寫文章真的是體力活,覺得我的文章還有參考價值,請左邊幫我點讚按個喜歡,右上角幫我按個追縱,底下歡迎留言討論。給我一點繼續走下去的動力。

對我的文章有興趣,歡迎到我的網站上 https://chechia.net 閱讀其他技術文章,有任何謬誤也請各方大德直接聯繫我,感激不盡。

Exausted Cat Face


摘要

  • HAProxy Introduction
  • Redis Sentinel with HAProxy

HAProxy Intro

HAproxy 全名是 High Availability Proxy,是一款開源 TCP/HTTP load balancer,他可以

  • 聽 tcp socket,連 server,然後把 socket 接在一起讓雙向流通
  • 可做 Http reverse-proxy (Http gateway),自己作為代理 server,把接受到的 connection 傳到後端的 server。
  • SSL 終端,可支援 client-side 與 server-side 的 ssl/tls
  • 當 tcp/http normalizer
  • 更改 http 的 request 與 response
  • 當 switch,決定 request 後送的目標
  • 做 load balancer,為後端 server 做負載均衡
  • 調節流量,設定 rate limit,或是根據內容調整流量

HAProxy 還有其他非常多的功能,想了解細節可以來看原理解說文件

Topology

我們今天的範例是在後端的 redis 與 clients 中間多放一層 HAProxys

1
2   +-------+   +--------+    +------------+    +---------+
3   |Clients|---|HAProxys|----|redis master|----|sentinels|
4   +-------+   +--------+    +------------+    +---------+
5

可能有人會問說,那前兩天講的 redis sentinel,跑去哪裡了。

sentinel 還在正常運作,負責監測 redis 狀態與 failover,只是 client 不再透過 sentinel 去取得 master,而是透過 HAProxy。

Deploy HAProxy

我把我的寶藏都在這了https://github.com/chechiachang/haproxy-kubernetes

下載下來的 .sh ,跑之前養成習慣貓一下

 1cat install.sh
 2
 3#!/bin/bash
 4
 5# redis-db-credentials should already exists
 6#kubectl create secret generic redis-db-credentials \
 7   --from-literal=REDIS_PASSWORD=123456
 8
 9# Update haproxy.cfg as configmap
10kubectl create configmap haproxy-config \
11   --from-file=haproxy.cfg \
12   --output yaml \
13   --dry-run | kubectl apply -f -
14
15kubectl apply -f deployment.yaml
16kubectl apply -f service.yaml

這邊做的事情有幾件

  • 取得 redis 的 auth REDIS_PASSWORD 放在 secret 中,如果前面是照我們的範例,那都已經設定了
  • 把 haproxy.cfg 的設定檔,使用 configmap 的方式放到 kubernetes 上
  • 部屬 HAProxy deployment
  • 部屬 HAProxy service

簡單看一下 deployment

 1apiVersion: apps/v1beta1
 2kind: Deployment
 3metadata:
 4  name: haproxy
 5spec:
 6  replicas: 3
 7  template:
 8    metadata:
 9      labels:
10        app: haproxy
11        app.kubernetes.io/name: haproxy
12        component: haproxy
13    spec:
14      volumes:
15      - name: haproxy-config
16        configMap:
17          name: haproxy-config
18      affinity:
19        podAntiAffinity:
20          preferredDuringSchedulingIgnoredDuringExecution:
21          - weight: 100
22            podAffinityTerm:
23              labelSelector:
24                matchExpressions:
25                - key: "app"
26                  operator: "In"
27                  values:
28                  - "haproxy"
29              topologyKey: kubernetes.io/hostname
30      containers:
31      - name: haproxy
32        image: haproxy:2.0.3-alpine
33        command: ["haproxy", "-f", "/usr/local/etc/haproxy/config/haproxy.cfg"]
34        readinessProbe:
35          initialDelaySeconds: 15
36          periodSeconds: 5
37          timeoutSeconds: 1
38          successThreshold: 2
39          failureThreshold: 2
40          tcpSocket:
41            port: 26999
42            port: 6379
43        volumeMounts:
44        - name: haproxy-config
45          mountPath: /usr/local/etc/haproxy/config
46        resources:
47          requests:
48            cpu: 10m
49            memory: 30Mi
50        env:
51        - name: REDIS_PASSWORD
52          valueFrom:
53            secretKeyRef:
54              name: redis-db-credentials
55              key: REDIS_PASSWORD
56        ports:
57        - containerPort: 8000
58          name: http
59        - containerPort: 9000
60          name: https
61        - containerPort: 26999
62          name: stats
63        - containerPort: 6379
64          name: redis
  • Replicas: 3 ,開起來是三個 HAProxy
  • podAntiAffinity,三個分布到不同 node 上,盡量維持 HA
  • readinessProbe,等 tcpSocket 26999 (HAProxy Stats) 與 6370 (Redis Proxy) 通了才 READY
  • 把 redis password 掛進去
  • 把 haproxy.cfg 掛進去
  • 開幾個 port

看一下 service

 1kind: Service
 2apiVersion: v1
 3metadata:
 4  name: haproxy-service
 5spec:
 6  sessionAffinity: ClientIP
 7  sessionAffinityConfig:
 8    clientIP:
 9      timeoutSeconds: 10800 # 3 hr
10  selector:
11    app: haproxy
12  ports:
13    - name: http
14      protocol: TCP
15      port: 8000
16    - name: https
17      protocol: TCP
18      port: 9000
19    - name: stats
20      protocol: TCP
21      port: 26999
22    - name: redis
23      protocol: TCP
24      port: 6379
25    - name: redis-exporter
26      protocol: TCP
27      port: 8404
  • 很單純,就是把幾個 port 接出來
  • 把 sessionAffinity 開起來
    • 這邊希望來自相同 clientIP (kubernetes 內部 app clients) 的 session 能持續走同一個 server
    • 可以降低進到 service 往後送到一直重連浪費資源
    • 但一直連著也不好,可能會 connection not closed 一直佔著
    • HAProxy1 HAProxy2 HAProxy3,上次 Client1 連 HAProxy1,service 也盡量讓你下個 request 也走 HAPRoxy1
1kubectl get po | grep haproxy
2
3haproxy-56d94f857f-gmd4s                                 1/1     Running     0          47d
4haproxy-56d94f857f-p2vj6                                 1/1     Running     0          47d
5haproxy-56d94f857f-vhz8b                                 1/1     Running     0          47d

HAProxy Config

看一下 haproxy.cfg

 1# https://cbonte.github.io/haproxy-dconv/2.0/configuration.html
 2
 3# https://github.com/prometheus/haproxy_exporter
 4# https://www.haproxy.com/blog/haproxy-exposes-a-prometheus-metrics-endpoint/
 5# curl http://localhost:8404/metrics
 6# curl http://localhost:8404/stats
 7frontend stats
 8 mode http
 9 timeout client 30s
10 bind *:8404
11 option http-use-htx
12 http-request use-service prometheus-exporter if { path /metrics }
13 stats enable
14 stats uri /stats
15 stats refresh 10s
16
17# Redis
18frontend redis_gate
19 mode tcp
20 timeout client 7d
21 bind 0.0.0.0:6379 name redis
22 default_backend redis_servers
23
24backend redis_servers
25 mode tcp
26 timeout connect 3s
27 timeout server 7d
28 option tcp-check
29 tcp-check connect
30 tcp-check send AUTH\ "${REDIS_PASSWORD}"\r\n
31 tcp-check send PING\r\n
32 tcp-check expect string PONG
33 tcp-check send info\ replication\r\n
34 tcp-check expect string role:master
35 tcp-check send QUIT\r\n
36 tcp-check expect string +OK
37 server R1 redis-2-redis-ha-announce-0:6379 check inter 1s
38 server R2 redis-2-redis-ha-announce-1:6379 check inter 1s
39 server R3 redis-2-redis-ha-announce-2:6379 check inter 1s
  • 兩個 frontend,吃前端 (client) 來的 request
    • frontend stats 是 HAProxy 本身服務的 stats
      • 把 prometheus-exporter 開起來,讓 prometheus 進來 scrape metrics
    • frontend redis_gate 是用來服務 redis client
      • 邏輯很簡單,進來的 request 往有效的 backend redis_server 送,這邊的有效指的是 redis master
      • timeout 7d,因為我們的服務有長時間不間斷的 pubsub,可以視需求調整
  • 一個 backend,HAProxy 會維護並監測狀態,然後把 frontend proxy 過去
    • mode tcp,使用 tcp 去 probe
    • option tcp-check,下面是一串 tcp checklist,配合 redis 的 tcp auth protocol 去取得
      • tcp connect 連上
      • send AUTH 密碼 到 redis
      • send ping,redis 要回 pong
      • send info replication 直接打 redis tcp info API
      • 預期 string 內有 role:master 意思是這台 redis 是 master
      • 退出,redis 要回 ok
    • server 有三台,透過 redis 各自的 ha-announce service 去打

HAProxy 會維護 backend 的 proxy stats,找到三台 redis 中,是 master 的這台

Running Log

1kubectl logs -f haproxy-123-123456789
2
3[WARNING] 273/153936 (1) : Server redis_servers/R2 is DOWN, reason: Layer7 timeout, info: " at step 6 of tcp-check (expect string 'role:master')", check duration: 1000ms. 2 active and 0 backup servers left. 0 sessions active, 0 requeued, 0 remaining in queue.
4[WARNING] 273/153937 (1) : Server redis_servers/R3 is DOWN, reason: Layer7 timeout, info: " at step 6 of tcp-check (expect string 'role:master')", check duration: 1001ms. 1 active and 0 backup servers left. 0 sessions active, 0 requeued, 0 remaining in queue.

HAProxy 去 redis 問,你是 master 嗎,兩個人回不是,只有一個回 role:master,所以把 client 導過去

HAProxy vs Sentinel

  • Client 不用知道中間的 proxy,只要知道透過 HAproxy service 就會被 proxy 到 master
  • HAproxy 是 stateless,非常好 scale
  • Client 不用支援 sentinel,只要一般的 redis-cli 就可以連入