说明
我们知道,k8s在发布编译型语言的应用时,几乎不用多考虑,就会选择将编译好jar/war包(java语言)或者二进制文件(golang/c++)直接打到镜像当中,生成新的应用镜像,然后将镜像推到镜像仓库,再触发k8s完成应用发布或者版本更新。可是相反,解释型语言(php/lua/python)我们一般不会这么做。为什么呢?
因为我们知道,打镜像和推送镜像的一个过程是相对比较耗时的。对于编译型语言来讲,编译过程本身就比较耗时,在编译完成之后,再进行镜像打包和推送镜像的过程所消耗的时间跟编译过程本身相比,根本微不足道。对于开发人员来讲,他们有一个这样心理预期,多了这么一点点的时间,对于他们来讲,几乎是无感知的。
而对于解释型语言的应用开发者来讲则不然,我们以php为例来作说明。对于一个php开发者来讲,有的时候,一次发布,可能就只修改了一个文件,通过传统的发布方式,完成一次发布可能只需要10秒钟。而如果只是修改了一个文件却需要将整个代码完整打包,生成一个新的镜像,打镜像推镜像的时间可能就需要1分钟,再加上镜像替换,流动更新的时间,整个更新操作的时间就会拉的更长。虽然这在生产环境中的表现可能还并不明显,但在需要持续集成,频繁迭代的测试环境中,这种表现在发布时间上的巨大心理落差几乎无法让开发人员接受。事实上,这也是大多数解释型语言的开发者抵制docker的一个原因之一。
那么到底有没有更好的解决办法呢?
单独发布代码
事实上,有很多公司在实际使用中针对解释型语言的应用都选择了使用容器标准化运行时环境,代码单独发布的方式。
具体的做法是,使用镜像打包标准的php运行环境,然后将代码直接发布到k8s的所有node节点上,然后在任意节点上启动php标准镜像,并挂载所在节点的指定代码目录即可。因为所有节点上都有代码,所以php容器可以在任意节点之间漂移。如果不想把代码发布到所有节点,只想发布到某几个节点,也很好解决,只需要给这几个节点打上label,然后限定php容器只运行在带有该label的节点上即可。
这么做的优点显而易见:
通过容器的方式标准化了php环境,而且简化了php环境的部署操作。
php代码发布仍然采用传统的发布方式独立发布,不受打镜像推镜像的时间影响,做到了快速迭代。
缺点同样显而易见:
首先镜像和代码拆开发布的方式本身就不是容器化标准所推荐的方法
我们需要通过label的方式严格对node和pod的对应关系做限定,一旦出错,就会出现pod正常运行,访问报错的现象,因为pod无法正常挂载代码。
因为一台宿主机上只有一份代码,如果该台宿主机上同时运行了同一个应用的多个pod,那么这些pod将会挂载同一份代码,当多个进程同时读写同一份代码时,需要代码自行解决冲突,虽然这种情况其实很少出现(绝大多数时候,应用程序的开发者都会避免在代码目录执行写操作,而对于读操作,多进程是可以共存的)。当然另一个可行的解决办法是通过pod的反亲和性调度避免将同一个应用的多个pod运行在同一个node节点上,毫无疑问,这也增加了配置的复杂性。
应用容器主动拉取代码
事实上,这是合乎容器化标准的一种实现。
而要实现这种方式,需要解决两个问题:
应用容器第一次启动时,如何获取代码
应用代码更新时,容器如何更新代码
第一个问题,我们使用kubernetes的init container技术来解决(在某些应用场中,使用pod启动后的钩子事件也可以解决,但在类似lua的应用场中,在应用容器启动之前就需要代码就位的情况下,这种方式并不合适,所以init container才是最好的选择)。其原理是在应用容器启动之前,先启动一个初始化容器,然后在初始化容器中执行从git仓库,或者从发布服务器拉取代码的操作,将代码直接拉取到应用容器的存储位置,然后启动应用容器,这样应用容器一启动,就有了代码。
第二个问题,可以直接调用k8s api,获取到当前应用的所有pod信息,然后触发循环调用pod上的rsync,执行主动拉取操作。
事实上,在我们当前的测试环境的lua代码的发布场景中,尤其适用。因为我们在测试环境中,持续集成的过程是自动完成的,每分钟执行一次持续集成操作。会先从git仓库将代码拉取到发布节点,发布节点启动rsync server端。然后各node节点上配置定时任务,每分钟从发布节点同步代码。对于lua应用来讲,每次代码更新就需要执行一次Nginx重载。通过这种发布方式,我们可以实现只有在有代码更新时,才执行nginx重载,而不是每分钟一次。
缺点:
增加了发布服务器的压力,所有pod不再共享宿主机代码,而是每个pod一套独立代码,所有pod都会从发布服务器拉取代码,在pod数量较大时,对发布服务器的io是个考验。另外如果某节点宕机时,所有pod漂移到新节点上重新启动,会同一时间向发布服务器拉取代码,对发布服务器的io压力也会较大
其配置相对于独立发布代码的方式其实是同样复杂的,但是其不用限定pod与node的对应关系,增加了调度的灵活性。
下面是一个应用容器主动拉取代码的k8s deployment配置文件示例:
apiVersion: extensions/v1beta1kind: Deploymentmetadata: name: dyland-lua-api-pre namespace: phpspec: replicas: 1 selector: matchLabels: name: dyland-lua-api-pre envrion: php template: metadata: annotations: prometheus.io/scrape: "true" prometheus.io/port: "9913" prometheus.io/scheme: "http" prometheus.io/path: "/metrics" labels: name: dyland-lua-api-pre envrion: php version: "" spec: initContainers: - name: rsync-code image: hub.dz11.com/op-base/rsync:v3.1.3 command: - "sh" - "-c" - > /bin/echo '123456' > /home/www/server/rsync.pwd; chmod 400 /home/www/server/rsync.pwd; /usr/bin/rsync -avzLu --password-file=/home/www/server/rsync.pwd www@$(RSYNC_SERVER)::pre-lua/dyland-lua-api.pre.wh03 /home/www/server/ > /dev/stdout 2>&1; env: - name: RSYNC_SERVER value: 192.168.1.10 volumeMounts: - name: www-root mountPath: "/home/www/server" containers: - name: dyland-lua-api-pre image: hub.dz11.com/op-base/openresty:1.11.2.4 imagePullPolicy: Always lifecycle: postStart: exec: command: - "sh" - "-c" - > /bin/echo 'options single-request-reopen' >> /etc/resolv.conf; ports: - containerPort: 80 env: - name: APP_NAME value: dyland-lua-api.pre.wh03 volumeMounts: - name: nginx-conf mountPath: /usr/local/ngx_openresty/nginx/conf/vhost - name: applogdir mountPath: /home/www/logs/applogs - name: srvlogdir mountPath: /home/www/logs/srvlogs - name: www-root mountPath: /home/www/server # mountPath: /home/www/server/dyland-lua-api.pre.wh03 - name: nginx-vts-exporter image: hub.dz11.com/library/nginx-vts-exporter:v0.10.3 ports: - containerPort: 9913 env: - name: NGINX_HOST value: "http://localhost/status/format/json" volumes: - name: nginx-conf configMap: name: dyland-lua-api-pre-configmap - name: applogdir hostPath: path: /home/www/logs/applogs - name: srvlogdir hostPath: path: /home/www/logs/srvlogs - name: www-root emptyDir: {} # hostPath: # path: /home/www/server/dyland-lua-api.pre.wh03 imagePullSecrets: - name: dk-reg nodeSelector: testenv: pre