Kubernetes集群中的Nginx配置熱更新方案

felixgis 9年前發布 | 51K 次閱讀 Nginx Web服務器 Kubernetes

Nginx已經是互聯網IT業界一個無敵的存在,作為反向代理、負載均衡、Web服務器等多種角色的扮演者,Nginx在全球各個互聯網公司落地、開花和結果,Ngnix已經成為了支撐全球互聯網應用的一個不可獲取的組成部分。

在我們的平臺中,Nginx同樣被拿來作為服務接入的最前端的反向代理,并且我們的Nginx也是作為一個Service跑在我們的Kubernetes集群中的。Ngnix背后的服務眾多,服務的生生死死都要在Nginx上這些服務路由的配置中有所體現,這就要求部署在Kubernetes集群中的Nginx需要有一個合理的配置熱更新方案。

Nginx自身是支持配置熱更新的,通過nginx -s reload命令可以實現這一點:

# sudo nginx -s reload

# sudo tail -100f /var/log/nginx/error.log
2016/11/18 08:21:03 [notice] 31516#31516: signal process started

這也是諸多nginx熱更新方案的基礎。

隨著Docker容器以及容器集群/云的出現,Nginx也被Dockerize了,Docker中Nginx的配置熱更新方案在 Jason Wilder 的 這篇文章 中有體現,在該方案中,你可以直接使用Jason Wilder開源的 Nginx-proxy 實現容器中Nginx的配置的熱更新。但這個方案并不能直接適用于Kubernetes,而且 作者也并沒有Plan support k8s 。

在Kubernetes集群中部署的Nginx,我其實也找到了一個配置熱更新的方案,這是普元的一份技術資料《 微服務動態路由實現:OpenResty與kubernetes 》中提供的,這個方案通過 OpenResty 與K8s的結合實現了配置熱更新。由于我對OpenResty并不熟悉,并且我個人更希望通過Kubernetes自身的一些Feature來實現這個方案,于是我開始了我自己的探索。

一、需求場景和方案原理

我們要實現的就是:當Kubernetes集群中的Service發生變化時,比如新創建一個Service或刪除了一個Service,這些Service在Nginx反向代理中的路由配置需要同步更新并生效。因此,這個過程的場景大致如下:

  • 管理員通過命令或程序通過API操作K8s集群創建或刪除Service;
  • 監聽API Server Event的某個程序獲取該Event,并從API Server讀取最新Service數據,重新生成/etc/nginx/conf.d/default.conf;
  • /etc/nginx/conf.d/default.conf文件的變動觸發文件變更事件,監聽該事件的腳本調用“nginx -s reload”命令實現Nginx的配置熱更新。

針對這一需求場景,我這里給出一個實現方案,先上圖:

簡答說明一下:

  • Nginx作為一個Service部署在Kubernetes集群中,可以有多個Pod副本;
  • 以一個nginx pod為例,該Pod中包含三個Container,分別是 init container 、nginx container和config-nginx-generator container;
  • 三個Container共同掛載且共享一個Pod volume,emptyDir類型即可,無需持久化的存儲卷,三個Container的掛載路徑均為/etc/nginx/conf.d;
  • Pod啟動時,init container首先啟動并訪問API Server,獲取Service列表,按照一定條件過濾后(比如通過label的key和Value值),初始創建/etc/nginx/conf.d/default.conf。創建成功后,Container退出;
  • nginx container啟動,加載配置,開始提供反向代理服務,并通過inotify工具監視/etc/nginx/conf.d/default.conf文件狀態變化,一般變化,就執行nginx -s reload熱加載最新配置。
  • config-nginx-generator container同時也啟動起來,監聽API Server的service變更Event,一旦有Event出現,就重新讀取API Server中的Service list,并重新生成一份新的default.conf,覆蓋old版本 default.conf。

二、環境

由于 Kubernetes 和Docker都在Active Develop的過程中,兩個項目的變動都很快,因此,特定的Feature(比如k8s的init container)、操作和說明在某些版本是好用的,但對另外一些版本卻是不靈光的。這里先把環境確定清楚,避免誤導。

OS:
Ubuntu 14.04.4 LTS Kernel:3.19.0-70-generic #78~14.04.1-Ubuntu SMP Fri Sep 23 17:39:18 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

Docker:
# docker version
Client:
 Version:      1.12.2
 API version:  1.24
 Go version:   go1.6.3
 Git commit:   bb80604
 Built:        Tue Oct 11 17:00:50 2016
 OS/Arch:      linux/amd64

Server:
 Version:      1.12.2
 API version:  1.24
 Go version:   go1.6.3
 Git commit:   bb80604
 Built:        Tue Oct 11 17:00:50 2016
 OS/Arch:      linux/amd64

Kubernetes集群:1.3.7

私有鏡像倉庫:阿里云鏡像倉庫

三、實現

1、nginx image的創建

nginx image實現了兩個功能,一個自然是nginx自身了,另外一個就是監聽/etc/nginx/conf.d/default.conf文件的變化,并適時調用nginx -s reload更新nginx配置。在kubernetes的源碼目錄kubernetes/examples下有一個例子: https-nginx ,這里面已經為我們實現了一個基于 auto-reload-nginx.sh 的Nginx image Dockerfile,我們稍作改造就可以直接使用了:

//Dockerfile

FROM nginx
MAINTAINER Tony Bai <bigwhite.cn@aliyun.com>

COPY auto-reload-nginx.sh /home/auto-reload-nginx.sh
RUN chmod +x /home/auto-reload-nginx.sh

# install inotify
RUN apt-get update && apt-get install -y inotify-tools

基于該Dockefile構建image:

# docker build -t xxxx/nginx

# docker images
REPOSITORY                                             TAG                 IMAGE ID            CREATED             SIZE
xxxx/nginx                                            latest              a1503b1c2b70        42 seconds ago      191.9 MB

官方nginx image基于debian jessie版本構建,apt-get update & install時需要耐心等待一下。

打標簽并推送到我們的阿里云私有鏡像庫:

# docker tag a1503b1c2b70 registry.cn-hangzhou.aliyuncs.com/xxxx/nginx

# docker images
REPOSITORY                                             TAG                 IMAGE ID            CREATED             SIZE
xxxx/nginx                                            latest              a1503b1c2b70        12 minutes ago      191.9 MB
registry.cn-hangzhou.aliyuncs.com/xxxx/nginx          latest              a1503b1c2b70        12 minutes ago      191.9 MB

# docker push registry.cn-hangzhou.aliyuncs.com/xxxx/nginx

2、編寫Pod yaml

由于init container和config-nginx-generator container在真實場景中都是要與Kubernetes的API Server交互,并生成/etc/nginx/conf.d/default.conf,這需要一個實現過程,在這里我們暫不給出兩個Container的具體Dockerfile以及實現功能的實際程序,而是用兩個通用docker image,并通過“手動”方式實現它們各自的功能。因此,我們在這一節中就可以給出Nginx Pod的yaml描述文件了:

//nginx-reload-on-k8s.yaml

apiVersion: v1
kind: Pod
metadata:
  name: nginx-reload-on-k8s
  annotations:
    pod.beta.kubernetes.io/init-containers: '[
      {
           "name": "nginx-reload-on-k8s-init-1",
           "image": "busybox",
           "command": ["wget", "-O", "/etc/nginx/conf.d/index1.html", "http://www.baidu.com"],
           "volumeMounts": [
               {
                  "name": "conf-volume",
                  "mountPath": "/etc/nginx/conf.d"
               }
           ]
      },
      {
           "name": "nginx-reload-on-k8s-init-2",
           "image": "busybox",
           "command": ["wget", "-O", "/etc/nginx/conf.d/index2.html", "http://dict.cn"],
           "volumeMounts": [
               {
                  "name": "conf-volume",
                  "mountPath": "/etc/nginx/conf.d"
               }
           ]
      }
    ]'
spec:
  containers:
  - name: nginx-config-generator
    volumeMounts:
    - mountPath: /etc/nginx/conf.d
      name: conf-volume
    image: registry.cn-hangzhou.aliyuncs.com/xxxx/test:latest
    imagePullPolicy: IfNotPresent
    command:
       - "tail"
       - "-f"
       - "/var/log/bootstrap.log"
  - name: nginx-origin
    volumeMounts:
    - mountPath: /etc/nginx/conf.d
      name: conf-volume
    image: registry.cn-hangzhou.aliyuncs.com/xxxx/nginx:latest
    imagePullPolicy: IfNotPresent
    command: ["/home/auto-reload-nginx.sh"]
    ports:
    - containerPort: 80
  volumes:
  - name: conf-volume
    emptyDir: {}

Yaml中,我們創建了兩個init container,分別用于從baidu.com和dict.cn抓取主頁,并存儲于/etc/nginx/conf.d的下面備用。nginx-config-generator我們使用image xxxx/test,這就是一個基于ubuntu且安裝了諸多網絡工具的鏡像,用于做目標鏡像調試的;nginx container用的就是上面push到私有鏡像倉庫的那個鏡像,command則是執行/home/auto-reload-nginx.sh這個腳本,從而啟動nginx和通過inotify監控/etc/nginx/conf.d/default.conf文件。

我們來創建這個Pod(注意:只有用kubectl apply命令時,init container才會被創建和執行,如果用kubectl create -f ,那么將忽略init container):

# kubectl apply -f nginx-reload-on-k8s.yaml
pod "nginx-reload-on-k8s" created

# kubectl get pod
NAME                           READY     STATUS             RESTARTS   AGE
nginx-reload-on-k8s            2/2       Running            0          41s

通過describe pod/nginx-reload-on-k8s,我們能看到一些Container創建的詳細信息:

# kubectl describe pod/nginx-reload-on-k8s
Name:        nginx-reload-on-k8s
Namespace:    default
Node:        10.46.181.146/10.46.181.146
Start Time:    Thu, 17 Nov 2016 21:39:55 +0800
Labels:        <none>
Status:        Running
IP:        172.16.57.9
... ...

Events:
  FirstSeen    LastSeen    Count    From            SubobjectPath                    Type        Reason        Message
  ---------    --------    -----    ----            -------------                    --------    ------        -------
  57s        57s        1    {default-scheduler }                            Normal        Scheduled    Successfully assigned nginx-reload-on-k8s to 10.46.181.146
  39s        39s        1    {kubelet 10.46.181.146}    spec.initContainers{nginx-reload-on-k8s-init-1}    Normal        Created        Created container with docker id 0e21afb58eee
  39s        39s        1    {kubelet 10.46.181.146}    spec.initContainers{nginx-reload-on-k8s-init-1}    Normal        Started        Started container with docker id 0e21afb58eee
  56s        38s        2    {kubelet 10.46.181.146}    spec.initContainers{nginx-reload-on-k8s-init-1}    Normal        Pulling        pulling image "busybox"
  39s        26s        2    {kubelet 10.46.181.146}    spec.initContainers{nginx-reload-on-k8s-init-1}    Normal        Pulled        Successfully pulled image "busybox"
  26s        26s        1    {kubelet 10.46.181.146}    spec.initContainers{nginx-reload-on-k8s-init-2}    Normal        Created        Created container with docker id 85632ff73ea8
  26s        26s        1    {kubelet 10.46.181.146}    spec.initContainers{nginx-reload-on-k8s-init-2}    Normal        Started        Started container with docker id 85632ff73ea8
  25s        25s        1    {kubelet 10.46.181.146}    spec.containers{nginx-config-generator}        Normal        Pulled        Container image "registry.cn-hangzhou.aliyuncs.com/xxxx/test:latest" already present on machine
  25s        25s        1    {kubelet 10.46.181.146}    spec.containers{nginx-config-generator}        Normal        Created        Created container with docker id 1ce8c6d8a8af
  25s        25s        1    {kubelet 10.46.181.146}    spec.containers{nginx-config-generator}        Normal        Started        Started container with docker id 1ce8c6d8a8af
  25s        25s        1    {kubelet 10.46.181.146}    spec.containers{nginx-origin}            Normal        Pulled        Container image "registry.cn-hangzhou.aliyuncs.com/xxxx/nginx:latest" already present on machine
  25s        25s        1    {kubelet 10.46.181.146}    spec.containers{nginx-origin}            Normal        Created        Created container with docker id 0c692ec28acd
  25s        25s        1    {kubelet 10.46.181.146}    spec.containers{nginx-origin}            Normal        Started        Started container with docker id 0c692ec28acd

... ...

可以看到四個container依次被pull and create。

四、測試

現在我們就來測試一下nginx的reload。

之前的兩個init container分別在/etc/nginx/conf.d下創建了index1.html和index2.html,我們就用這兩個文件分別作為配置變更前和變更后的首頁。

注意:這時我們還沒有/etc/nginx/conf.d/default.conf文件,我們在Pod內訪問localhost:80將會得到失敗結果:

# curl localhost:80
curl: (7) Failed to connect to localhost port 80: Connection refused

我們進入nginx-config-generator,創建/etc/nginx/conf.d/default.conf文件,與此同時,通過docker logs -f 監控nginx-origin容器的日志:

//default.conf

server {
    listen       80;
    server_name  localhost;

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;

    location / {
        root   /etc/nginx/conf.d;
        index  index1.html index1.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

我們把/etc/nginx/conf.d/index1.html作為服務站點的首頁了。文件創建完畢后,我們同時就可以從nginx-origin容器的日志能看到如下內容:

At 14:07 on 17/11/16, config file update detected.
2016/11/17 14:07:25 [notice] 20#20: signal process started

我們再從Pod中訪問localhost:80(注意:Pod中的多個container共享network namespace,通過localhost就可以進行互訪):

root@nginx-reload-on-k8s:/etc/nginx# curl localhost:80
<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> .... </html>

我們順利得到index1.html的內容,這說明配置實時生效了。

我們再來“觸發”一次配置變更。我們將default.conf中的:

location / {
        root   /etc/nginx/conf.d;
        index  index1.html index1.htm;
    }

改為:

location / {
        root   /etc/nginx/conf.d;
        index  index2.html index2.htm;
    }

保存!

從nginx-origin容器日志可以看到如下輸出:

At 14:17 on 17/11/16, config file update detected.
2016/11/17 14:17:46 [notice] 32#32: signal process started

在Pod中再次訪問站點首頁:

# curl localhost:80
<!DOCTYPE HTML>
<html>
    <head>
        <meta name="renderer" content="webkit"/>
                <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" />
                <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>海詞詞典_在線詞典_在線翻譯_海量正版權威詞典官方網站</title>
... ...

可以看到配置更新成功,首頁換成了dict.cn的首頁。

五、測試

通過上述這些“手動”的觸發和測試,可以看出這個方案是可行的。并且我們可以看出,這個方案是有一些好處的:

  • 不需要依賴外部持久化存儲卷;
  • 通過k8s api server獲取當前所有 service列表,通過service label來過濾,無需依賴額外的redis server或etcd服務;

剩下的就是具體init container以及config-generator的實現了。這個留給我以及大家后續去完成^_^。

? 2016,bigwhite. 版權所有.

 

來自:http://tonybai.com/2016/11/17/nginx-config-hot-reloading-approach-for-kubernetes-cluster/

 

 本文由用戶 felixgis 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!