Nginx作为WEB服务器被广泛使用。其自身支持热更新,在修改配置文件后,使用nginx -s reload命令可以不停服务重新加载配置。然而对于Dockerize的Nginx来说,如果每次都进到容器里执行对应命令去实现配置重载,这个过程是很痛苦的。本文介绍了一种kubernetes集群下nginx的热更新方案。
首先我们创建正常的一个nginx资源,资源清单如下:
apiVersion: v1kind: ConfigMapmetadata:name: nginx-configdata: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/v1kind: Deploymentmetadata:name: my-appspec:replicas: 1selector:matchLabels:app: my-apptemplate:metadata:labels:app: my-appspec:containers:- name: my-appimage: nginximagePullPolicy: IfNotPresentvolumeMounts:- name: nginx-configmountPath: /etc/nginx/conf.dvolumes:- name: nginx-configconfigMap:name: nginx-config
然后创建资源对象。
# kubectl get pod -o wideNAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATESmy-app-9bdd6cbbc-x9gnt 1/1 Running 0 112s 192.168.58.197 k8s-node02 <none> <none>
然后我们访问pod资源,如下:
# curl -I 192.168.58.197HTTP/1.1 200 OKServer: nginx/1.17.10Date: Tue, 26 May 2020 06:18:18 GMTContent-Type: text/htmlContent-Length: 612Last-Modified: Tue, 14 Apr 2020 14:19:26 GMTConnection: keep-aliveETag: "5e95c66e-264"Accept-Ranges: bytes
现在我们来更新一下ConfigMap,也就是更改配置文件如下:
apiVersion: v1kind: ConfigMapmetadata:name: nginx-configdata: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/bashroot@my-app-9bdd6cbbc-x9gnt:/# cat /etc/nginx/conf.d/default.confserver {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.197HTTP/1.1 200 OKServer: nginx/1.17.10Date: Tue, 26 May 2020 06:21:05 GMTContent-Type: text/htmlContent-Length: 612Last-Modified: Tue, 14 Apr 2020 14:19:26 GMTConnection: keep-aliveETag: "5e95c66e-264"Accept-Ranges: bytes[root@k8s-master nginx]# curl -I 192.168.58.197:8080curl: (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 buildRUN go get github.com/fsnotify/fsnotifyRUN go get github.com/shirou/gopsutil/processRUN mkdir -p /go/src/appADD main.go /go/src/app/WORKDIR /go/src/appRUN CGO_ENABLED=0 GOOS=linux go build -a -o nginx-reloader .# main imageFROM nginx:1.14.2-alpineCOPY --from=build /go/src/app/nginx-reloader /CMD ["/nginx-reloader"]
main.go脚本如下:
package mainimport ("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 int32for pid, ppid := range nginxProcesses {if ppid == 0 {masterNginxPid = pidbreak}}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.yamlapiVersion: v1kind: ConfigMapmetadata:name: nginx-configdata: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/v1kind: Deploymentmetadata:name: nginxspec:replicas: 1selector:matchLabels:app: nginxtemplate:metadata:name: nginxlabels:app: nginxspec:shareProcessNamespace: truecontainers:- name: nginximage: nginximagePullPolicy: IfNotPresentvolumeMounts:- name: nginx-configmountPath: /etc/nginx/conf.dreadOnly: true- name: nginx-reloaderimage: registry.cn-hangzhou.aliyuncs.com/rookieops/nginx-reloader:v1imagePullPolicy: IfNotPresentenv:- name: WATCH_NGINX_CONF_PATHvalue: /etc/nginx/conf.dvolumeMounts:- name: nginx-configmountPath: /etc/nginx/conf.dreadOnly: truevolumes:- name: nginx-configconfigMap:name: nginx-config
手动修改configmap后,reloader监测到configmap变化,会主动向nginx主进程发起HUP信号,实现配置热更新。
二、第三方插件reloader
项目地址:https://github.com/stakater/Reloader
资源清单如下,我修改了镜像地址:
---# Source: reloader/templates/clusterrole.yamlapiVersion: rbac.authorization.k8s.io/v1beta1kind: ClusterRolemetadata:labels:app: reloader-reloaderchart: "reloader-v0.0.58"release: "reloader"heritage: "Tiller"name: reloader-reloader-rolenamespace: defaultrules:- apiGroups:- ""resources:- secrets- configmapsverbs:- list- get- watch- apiGroups:- "apps"resources:- deployments- daemonsets- statefulsetsverbs:- list- get- update- patch- apiGroups:- "extensions"resources:- deployments- daemonsetsverbs:- list- get- update- patch---# Source: reloader/templates/clusterrolebinding.yamlapiVersion: rbac.authorization.k8s.io/v1beta1kind: ClusterRoleBindingmetadata:labels:app: reloader-reloaderchart: "reloader-v0.0.58"release: "reloader"heritage: "Tiller"name: reloader-reloader-role-bindingnamespace: defaultroleRef:apiGroup: rbac.authorization.k8s.iokind: ClusterRolename: reloader-reloader-rolesubjects:- kind: ServiceAccountname: reloader-reloadernamespace: default---# Source: reloader/templates/deployment.yamlapiVersion: apps/v1kind: Deploymentmetadata:labels:app: reloader-reloaderchart: "reloader-v0.0.58"release: "reloader"heritage: "Tiller"group: com.stakater.platformprovider: stakaterversion: v0.0.58name: reloader-reloaderspec:replicas: 1revisionHistoryLimit: 2selector:matchLabels:app: reloader-reloaderrelease: "reloader"template:metadata:labels:app: reloader-reloaderchart: "reloader-v0.0.58"release: "reloader"heritage: "Tiller"group: com.stakater.platformprovider: stakaterversion: v0.0.58spec:containers:- env:image: "registry.cn-hangzhou.aliyuncs.com/rookieops/stakater-reloader:v0.0.58"imagePullPolicy: IfNotPresentname: reloader-reloaderargs:serviceAccountName: reloader-reloader---# Source: reloader/templates/role.yaml---# Source: reloader/templates/rolebinding.yaml---# Source: reloader/templates/service.yaml---# Source: reloader/templates/serviceaccount.yamlapiVersion: v1kind: ServiceAccountmetadata:labels:app: reloader-reloaderchart: "reloader-v0.0.58"release: "reloader"heritage: "Tiller"name: reloader-reloader
然后部署资源,结果如下:
kubectl get podNAME READY STATUS RESTARTS AGEmy-app-9bdd6cbbc-x9gnt 1/1 Running 0 38mreloader-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 wideNAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATESmy-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 -IHTTP/1.1 200 OKServer: nginx/1.17.10Date: Tue, 26 May 2020 06:58:38 GMTContent-Type: text/htmlContent-Length: 612Last-Modified: Tue, 14 Apr 2020 14:19:26 GMTConnection: keep-aliveETag: "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 osimport reimport pyinotifyimport loggingfrom threading import Timer# ParamLOG_PATH = "/root/python/log"CONF_PATHS = ["/etc/nginx",]DELAY = 5SUDO = FalseRELOAD_COMMAND = "nginx -s reload"if SUDO:RELOAD_COMMAND = "sudo " + RELOAD_COMMAND# Loglogger = 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)# Reloaderdef 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 tif 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_DELETEwatcher = 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)# Startlogger.info("Start Monitoring")notifier.loop()
