Nginx作为WEB服务器被广泛使用。其自身支持热更新,在修改配置文件后,使用nginx -s reload
命令可以不停服务重新加载配置。然而对于Dockerize的Nginx来说,如果每次都进到容器里执行对应命令去实现配置重载,这个过程是很痛苦的。本文介绍了一种kubernetes集群下nginx的热更新方案。
首先我们创建正常的一个nginx资源,资源清单如下:
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
data:
default.conf: |-
server {
server_name localhost;
listen 80 default_server;
location = /healthz {
add_header Content-Type text/plain;
return 200 'ok';
}
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 1
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: nginx
imagePullPolicy: IfNotPresent
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/conf.d
volumes:
- name: nginx-config
configMap:
name: nginx-config
然后创建资源对象。
# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
my-app-9bdd6cbbc-x9gnt 1/1 Running 0 112s 192.168.58.197 k8s-node02 <none> <none>
然后我们访问pod资源,如下:
# curl -I 192.168.58.197
HTTP/1.1 200 OK
Server: nginx/1.17.10
Date: Tue, 26 May 2020 06:18:18 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 14 Apr 2020 14:19:26 GMT
Connection: keep-alive
ETag: "5e95c66e-264"
Accept-Ranges: bytes
现在我们来更新一下ConfigMap,也就是更改配置文件如下:
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
data:
default.conf: |-
server {
server_name localhost;
listen 8080 default_server;
location = /healthz {
add_header Content-Type text/plain;
return 200 'ok';
}
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
等待数秒…..
然后我们可以看到nginx pod里的配置信息已经更改为如下:
# kubectl exec -it my-app-9bdd6cbbc-x9gnt -- /bin/bash
root@my-app-9bdd6cbbc-x9gnt:/# cat /etc/nginx/conf.d/default.conf
server {
server_name localhost;
listen 8080 default_server;
location = /healthz {
add_header Content-Type text/plain;
return 200 'ok';
}
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
root@my-app-9bdd6cbbc-x9gnt:/#
这时候我们访问8080是不通的,访问80是没问题的,如下:
[root@k8s-master nginx]# curl -I 192.168.58.197
HTTP/1.1 200 OK
Server: nginx/1.17.10
Date: Tue, 26 May 2020 06:21:05 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 14 Apr 2020 14:19:26 GMT
Connection: keep-alive
ETag: "5e95c66e-264"
Accept-Ranges: bytes
[root@k8s-master nginx]# curl -I 192.168.58.197:8080
curl: (7) Failed connect to 192.168.58.197:8080; Connection refused
我们可以看到,我们需要的配置文件已经更新的,但是并没有使用上,pod里的nginx也没有重载配置文件,这时候如果我们重新部署Pod,资源对象肯定就生效了。
但是这并不我们想要的效果,我们希望配置文件更改了,服务也跟着reload,并不需要我们手动的去干预。
目前有三种方法:
- 应用本身可以检测配置文件,然后自动reload
- 给Pod增加一个sidecar,用它来检测配置文件
- 第三方组件reloader,在deployment的annotations增加字段
reloader.stakater.com/auto: "true"
,即可检测configmap的更改来重启pod
应用本身检测的话这里就不做介绍了。这里主要来实验一下第2,3种方法
一、以sidecar形式
1.1、方法
- Kubernetes集群中部署Nginx Pod。该Pod包含两个Container,一个是nginx container,实现nginx自身的功能;另一个是nginx-reloader container,负责实时监测目标configmap的变化,当发现configmap更新以后,会主动向nginx的master进程发送HUP信号,实现配置的热加载。
- 配置文件是通过ConfigMap的形式挂载到Nginx Pod上,两个Container共享该ConfigMap。
- 依赖K8s集群的shareProcessNamespace特性(版本需在1.12之后),两个Container需要在Pod中共享进程名字空间。
1.2、实现
1.2.1、镜像制作
(1)、主容器使用官方NG容器即可
(2)、sidecar容器制作
Dockerfile如下:
FROM golang:1.12.0 as build
RUN go get github.com/fsnotify/fsnotify
RUN go get github.com/shirou/gopsutil/process
RUN mkdir -p /go/src/app
ADD main.go /go/src/app/
WORKDIR /go/src/app
RUN CGO_ENABLED=0 GOOS=linux go build -a -o nginx-reloader .
# main image
FROM nginx:1.14.2-alpine
COPY --from=build /go/src/app/nginx-reloader /
CMD ["/nginx-reloader"]
main.go脚本如下:
package main
import (
"log"
"os"
"path/filepath"
"syscall"
"github.com/fsnotify/fsnotify"
proc "github.com/shirou/gopsutil/process"
)
const (
nginxProcessName = "nginx"
defaultNginxConfPath = "/etc/nginx"
watchPathEnvVarName = "WATCH_NGINX_CONF_PATH"
)
var stderrLogger = log.New(os.Stderr, "error: ", log.Lshortfile)
var stdoutLogger = log.New(os.Stdout, "", log.Lshortfile)
func getMasterNginxPid() (int, error) {
processes, processesErr := proc.Processes()
if processesErr != nil {
return 0, processesErr
}
nginxProcesses := map[int32]int32{}
for _, process := range processes {
processName, processNameErr := process.Name()
if processNameErr != nil {
return 0, processNameErr
}
if processName == nginxProcessName {
ppid, ppidErr := process.Ppid()
if ppidErr != nil {
return 0, ppidErr
}
nginxProcesses[process.Pid] = ppid
}
}
var masterNginxPid int32
for pid, ppid := range nginxProcesses {
if ppid == 0 {
masterNginxPid = pid
break
}
}
stdoutLogger.Println("found master nginx pid:", masterNginxPid)
return int(masterNginxPid), nil
}
func signalNginxReload(pid int) error {
stdoutLogger.Printf("signaling master nginx process (pid: %d) -> SIGHUP\n", pid)
nginxProcess, nginxProcessErr := os.FindProcess(pid)
if nginxProcessErr != nil {
return nginxProcessErr
}
return nginxProcess.Signal(syscall.SIGHUP)
}
func main() {
watcher, watcherErr := fsnotify.NewWatcher()
if watcherErr != nil {
stderrLogger.Fatal(watcherErr)
}
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Create == fsnotify.Create {
if filepath.Base(event.Name) == "..data" {
stdoutLogger.Println("config map updated")
nginxPid, nginxPidErr := getMasterNginxPid()
if nginxPidErr != nil {
stderrLogger.Printf("getting master nginx pid failed: %s", nginxPidErr.Error())
continue
}
if err := signalNginxReload(nginxPid); err != nil {
stderrLogger.Printf("signaling master nginx process failed: %s", err)
}
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
stderrLogger.Printf("received watcher.Error: %s", err)
}
}
}()
pathToWatch, ok := os.LookupEnv(watchPathEnvVarName)
if !ok {
pathToWatch = defaultNginxConfPath
}
stdoutLogger.Printf("adding path: `%s` to watch\n", pathToWatch)
if err := watcher.Add(pathToWatch); err != nil {
stderrLogger.Fatal(err)
}
<-done
}
1.2.2、部署NG
(1)、NG的配置以configMap进行部署:
nginx-config.yaml
// nginx-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
data:
default.conf: |-
server {
server_name localhost;
listen 80 default_server;
location = /healthz {
add_header Content-Type text/plain;
return 200 'ok';
}
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
(2)、NG的Deployment清单(需打开共享进程命名空间特性:shareProcessNamespace: true):
nginx-deploy.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
name: nginx
labels:
app: nginx
spec:
shareProcessNamespace: true
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/conf.d
readOnly: true
- name: nginx-reloader
image: registry.cn-hangzhou.aliyuncs.com/rookieops/nginx-reloader:v1
imagePullPolicy: IfNotPresent
env:
- name: WATCH_NGINX_CONF_PATH
value: /etc/nginx/conf.d
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/conf.d
readOnly: true
volumes:
- name: nginx-config
configMap:
name: nginx-config
手动修改configmap后,reloader监测到configmap变化,会主动向nginx主进程发起HUP信号,实现配置热更新。
二、第三方插件reloader
项目地址:https://github.com/stakater/Reloader
资源清单如下,我修改了镜像地址:
---
# Source: reloader/templates/clusterrole.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
labels:
app: reloader-reloader
chart: "reloader-v0.0.58"
release: "reloader"
heritage: "Tiller"
name: reloader-reloader-role
namespace: default
rules:
- apiGroups:
- ""
resources:
- secrets
- configmaps
verbs:
- list
- get
- watch
- apiGroups:
- "apps"
resources:
- deployments
- daemonsets
- statefulsets
verbs:
- list
- get
- update
- patch
- apiGroups:
- "extensions"
resources:
- deployments
- daemonsets
verbs:
- list
- get
- update
- patch
---
# Source: reloader/templates/clusterrolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
labels:
app: reloader-reloader
chart: "reloader-v0.0.58"
release: "reloader"
heritage: "Tiller"
name: reloader-reloader-role-binding
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: reloader-reloader-role
subjects:
- kind: ServiceAccount
name: reloader-reloader
namespace: default
---
# Source: reloader/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: reloader-reloader
chart: "reloader-v0.0.58"
release: "reloader"
heritage: "Tiller"
group: com.stakater.platform
provider: stakater
version: v0.0.58
name: reloader-reloader
spec:
replicas: 1
revisionHistoryLimit: 2
selector:
matchLabels:
app: reloader-reloader
release: "reloader"
template:
metadata:
labels:
app: reloader-reloader
chart: "reloader-v0.0.58"
release: "reloader"
heritage: "Tiller"
group: com.stakater.platform
provider: stakater
version: v0.0.58
spec:
containers:
- env:
image: "registry.cn-hangzhou.aliyuncs.com/rookieops/stakater-reloader:v0.0.58"
imagePullPolicy: IfNotPresent
name: reloader-reloader
args:
serviceAccountName: reloader-reloader
---
# Source: reloader/templates/role.yaml
---
# Source: reloader/templates/rolebinding.yaml
---
# Source: reloader/templates/service.yaml
---
# Source: reloader/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app: reloader-reloader
chart: "reloader-v0.0.58"
release: "reloader"
heritage: "Tiller"
name: reloader-reloader
然后部署资源,结果如下:
kubectl get pod
NAME READY STATUS RESTARTS AGE
my-app-9bdd6cbbc-x9gnt 1/1 Running 0 38m
reloader-reloader-ff767bb8-cpzgz 1/1 Running 0 56s
然后给deployment增加一个annotations。如下:
kubectl patch deployments.apps my-app -p '{"metadata": {"annotations": {"reloader.stakater.com/auto": "true"}}}'
然后我们更改configMap清单,重新apply过后,我们可以看到pod会删除重启,如下:
kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
my-app-7c4fc77f5f-w4mbn 1/1 Running 0 3s 192.168.58.202 k8s-node02 <none> <none>
my-app-df6fbdb67-bnftb 1/1 Terminating 0 35s 192.168.58.201 k8s-node02 <none> <none>
reloader-reloader-ff767bb8-cpzgz 1/1 Running 0 3m47s 192.168.85.195 k8s-node01 <none> <none>
然后我们curl pod也可以通了,如下:
# curl 192.168.58.202:8080 -I
HTTP/1.1 200 OK
Server: nginx/1.17.10
Date: Tue, 26 May 2020 06:58:38 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 14 Apr 2020 14:19:26 GMT
Connection: keep-alive
ETag: "5e95c66e-264"
Accept-Ranges: bytes
三、附加
附加一个sidecar形式的python脚本
#!/usr/bin/env python
# -*- encoding: utf8 -*-
"""
需求:nginx配置文件变化,自动更新配置文件,类似nginx -s reload
实现:
1、用pyinotify实时监控nginx配置文件变化
2、如果配置文件变化,给系统发送HUP来reload nginx
"""
import os
import re
import pyinotify
import logging
from threading import Timer
# Param
LOG_PATH = "/root/python/log"
CONF_PATHS = [
"/etc/nginx",
]
DELAY = 5
SUDO = False
RELOAD_COMMAND = "nginx -s reload"
if SUDO:
RELOAD_COMMAND = "sudo " + RELOAD_COMMAND
# Log
logger = logging.getLogger(__name__)
logger.setLevel(level = logging.INFO)
log_handler = logging.FileHandler(LOG_PATH)
log_handler.setLevel(logging.INFO)
log_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
log_handler.setFormatter(log_formatter)
logger.addHandler(log_handler)
# Reloader
def reload_nginx():
os.system(RELOAD_COMMAND)
logger.info("nginx is reloaded")
t = Timer(DELAY, reload_nginx)
def trigger_reload_nginx(pathname, action):
logger.info("nginx monitor is triggered because %s is %s" % (pathname, action))
global t
if t.is_alive():
t.cancel()
t = Timer(DELAY, reload_nginx)
t.start()
else:
t = Timer(DELAY, reload_nginx)
t.start()
events = pyinotify.IN_MODIFY | pyinotify.IN_CREATE | pyinotify.IN_DELETE
watcher = pyinotify.WatchManager()
watcher.add_watch(CONF_PATHS, events, rec=True, auto_add=True)
class EventHandler(pyinotify.ProcessEvent):
def process_default(self, event):
if event.name.endswith(".conf"):
if event.mask == pyinotify.IN_CREATE:
action = "created"
if event.mask == pyinotify.IN_MODIFY:
action = "modified"
if event.mask == pyinotify.IN_DELETE:
action = "deleted"
trigger_reload_nginx(event.pathname, action)
handler = EventHandler()
notifier = pyinotify.Notifier(watcher, handler)
# Start
logger.info("Start Monitoring")
notifier.loop()