跳到主要内容

最近实施Openshift部署的一些心得

· 阅读需 13 分钟
Random Image
图片与正文无关

最近公司架起了私有容器云平台 Openshift,A 项目作为首个试点项目准备迁入 Openshift。迁入 Openshift 主要需要考虑以下几方面。

  1. 新的环境需要分配多少资源

  2. 新的环境容器内如何启动

  3. 如何平滑迁移

  4. 如何方便的查看日志

  5. 如何进行线上调试

  6. 如何进行热部署

  7. 如何回滚

  8. 如何处理滚动升级时原容器内未处理完的请求

下面进行逐一分析:

新的环境需要分配多少资源

项目以前的环境是阿里云 ECS 云主机,4 核 8G,资源比较充足,迁到 Openshift 时需要进行资源限制,运维同学开始给出的方案是默认开 2 个容器,每个容器初始内存 500M,0.3 个核的 CPU。在测试环境这个配置没有问题,但是在真实场景,我们业务的特点是偶尔有高 CPU 占用高内存占用的情况,后来运维同学暂时把资源限制给撤掉了。不过由于我在项目内部做了一些内存的限制,所以即使平台不限制,也不会占用太多内存,但是 CPU 的特点是平时都很低,偶尔比较高,这个我觉得不限制是对的。

新的环境容器内如何启动

关于容器启动方法,其实是项目自定义 Dockerfile 时,最后的 CMD 命令怎么写的问题。最简单的当然就是 node index.js 或者 npm start 这样的做法。但是实际为了项目的稳定性和灵活性还需要考虑的更多。

首先,前后端就是不一样的,前端实际输出的一些编译好的静态资源,我们需要考虑用什么方式提供 HTTP 服务,这方面无论是各种 npm 扩展还是各种老牌服务器软件都可以做到,这方面我们选择的是继续使用 Nginx,并且让运维同学帮助打包了一个 Nginx 的基础镜像。Nginx 毕竟专业一点,很多 npm 包虽然也能提供 HTTP 服务,但用在生产环境总是不让人放心的。

后端由于我们需要在线调试的机制,而 node 的默认启动方式是不支持的,所以就需要额外的手段,比如,forever 或者 pm2。由于我们之前就用 pm2,这里经验也可以复用。但是作为容器的启动命令,我们不需要用 daemon 的方式运行,经过调研,发现了 pm2 自带了一个 pm2-docker 命令,符合我的要求。为了让后端更加健壮,不至于稍有风吹草动就被 openshift 杀死并重新部署,每个容器内我启动了 2 个 pm2 实例,以集群模式运行,这样增加了容器的稳定性,有人可能会觉得一个容器就应该存在一个应用进程,然后靠健康检查来决定如何处置容器,我个人觉得容器在杀死和重建过程中的衔接还是不够让人放心,目前我更倾向于让容器先尝试自我恢复。

以下是我的 pm2 配置文件:

apps:
- script: ./index.js
exec_mode: cluster
instances: 2
max_memory_restart: 1024M
kill_timeout: 20000
env:
envValue: production
port: 8080

以下是生产环境的 Dockerfile:

FROM harbor.host.com/public/node:6.10.3


ENV envValue production

ADD . /home/node/code
RUN cd /home/node/code \
&& yarn install \
&& npm run apollon:install \
&& chown -R node.node /home/node/code \
&& git remote set-url origin git@code.host.com:o2o/apollon-backend.git

USER node
WORKDIR /home/node/code

EXPOSE 8080
CMD ["pm2-docker", "deploy/openshift.yml"]

如何平滑迁移

平滑迁移主要是两边要同时部署,然后通过改 DNS 指向让流量逐渐走到新运行环境,这里没有采用先内部代理再切 DNS 的方案,也没有采用可控的逐步放量的方式。

如何方便的查看日志

查看日志有两种方式,一种是进入 Openshift 后台,可以用集成的 EFK 日志收集平台查看日志,一种是本地电脑命令行查看。

在线查看有个问题是默认是单个容器的日志,如果需要把同一个项目的多个容器日志放一块查看,需要修改查询命令,EFK 的查询语法和 Lucence 的一样。例如:

kubernetes.pod_name:apollon-backend-production* AND kubernetes.namespace_name:"c-production”

如果是本地查看线上日志,需要运维同学给授权,之后就可以用 oc 系列的命令访问线上服务了,查看单个容器的日志可以这样:

oc logs -f --tail 1 apollon-backend-production-597136714-1n6tk

但是生产环境,我们一般部署多个日志,这时我们需要对日志输出流进行合并,示例如下:

cat <(oc logs -f --tail 1 apollon-backend-production-597136714-vdns0 & oc logs -f --tail 1 apollon-backend-production-597136714-1n6tk)

但是每次滚动升级,容器的名字末尾都会变化,这样每次组装命令都很繁琐,经过调查研究,发现可以这么写:

eval $(oc get pods | grep -v 'apollon-backend-production-test' |  grep apollon-backend-production | awk '{{printf"oc logs -f --tail 1 %s & ", $1}}' | awk '{{printf"cat <(%s)", substr($0,1,length($0)-3)}}')

每次复制粘贴都比较繁琐,所以想到可以写一个快捷的 shell 命令,把以下代码粘贴到本机.bashrc 或者.zshrc 里。

function prodlogs() {
oc project c-production -q
eval $(oc get pods | grep -v 'apollon-backend-production-test' | grep apollon-backend-production | awk '{{printf"oc logs -f --tail 1 %s & ", $1}}' | awk '{{printf"cat <(%s)", substr($0,1,length($0)-3)}}')
}

这样,每次看生产环境日志,只需要在命令行输入 prodlogs,然后敲回车即可,是不是很方便了呢?

如何进行线上调试

线上调试涉及到几个方面,在哪个容器调试,编辑器,如何登录容器。

如何保证需要测试时,测试的请求一定去指定的容器呢?这里我用了之前服务器的做法,就是单独部署一个调试容器,部署流程和配置和生产环境完全一样,但平时是没有流量的,只有当需要调试时,通过前端 Nginx 配合重定向,把指定的请求发到指定的容器。

关于编辑器,默认基础镜像里是没有编辑器的,为了减少镜像体积,我在 Dockfile 里也没有安装,只有当我需要调试时,在我的调试容器中使用 sudo apt-get install vim 来安装编辑器。但是默认安装的 vim 编辑时是中文乱码的,这时只要配置一下 ~/.vimrc 即可,配置方法如下:

set fileencodings=utf-8,gb2312,gb18030,gbk,ucs-bom,cp936,latin1
set enc=utf8
set fencs=utf8,gbk,gb2312,gb18030

那么,如何登录容器呢,oc 里有个 rsh 子命令用于登录容器。但是每次都先查找当前容器名称也很繁琐,这里按照上面的思路,封装了另一个 shell 函数命令:

function prodssh() {
oc project c-production -q
eval $(oc get pods | grep 'apollon-backend-production-test' | awk '{{printf"oc rsh %s", $1}}')
}

这样,我们就解决了调试所需要关心的一些问题。

如何进行热部署

经过测试,每次部署到 Openshift 的构建时间大概是前端 5 分钟左右,后端 2 分钟左右,加上平台部署升级,还要再花些时间,而之前的部署方式虽然比较低级,但是很快,大概是前端 1 分多钟,后端 30 秒左右。为了在极端情况下可以更快的部署上线,就想研究是否有办法更快的部署,或者叫热部署,也就是不重启容器,进行内部升级。

这里就要解决几个问题:

  1. 如何远程执行命令?这里主要是用 oc exec 命令和 oc rsync 来解决。

  2. 如何远程拉取最新代码,这里使用了基础镜像 root 账户自带的一个 private key,配置到 Gitlab 中。

  3. 前后端的热部署差异。后端主要就是拉取新代码,重启 pm2。但是前端需要构建,生产环境资源有限,我们需要本地部署,这里是用 oc rsync 命令将本地部署上传到生产环境,但是生产环境可能是多个容器,经过研究写出如下快捷命令:

oc project c-production -q && for pod in $(oc get pods|grep -v apollon-frontend-production-test|grep apollon-frontend-production|awk '{print $1}'); do echo $pod; oc rsync ./dist/ $pod:/home/node/code/public; done;

后端命令类似:

oc project c-production -q && for pod in $(oc get pods|grep -v grep apollon-backend-production-test|grep apollon-backend-production|awk '{print $1}'); do echo $pod; oc exec $pod -- sudo git pull origin production; oc exec $pod -- sudo chown -R node.node /home/node/code; oc exec $pod -- pm2 reload index; done;

如何回滚

回滚主要依靠 oc rollout 命令回滚到前一个版本,依靠 oc set 命令回滚到指定的版本,这里不深入展开,因为目前来讲,我这边还需要做一些调整还能够简化这个事情。

如何处理滚动升级时原容器内未处理完的请求

在滚动升级过程中,其实就是在部署新的杀掉旧的,但是如果旧的容器里有未服务完的请求,就会有部分用户受影响。Openshift 在这里借助 k8s 提供了一定的支持,比如杀旧容器之前等待若干秒再杀,但比较不灵活,这里我采用的方式是,借助 pm2 配置的 kill_timeout 提供最长时间限制,然后代码中,可以主动提前结束的方式。

后端代码加入了如下补充:

// Maintain a hash of all connected sockets
let sockets = {}, nextSocketId = 0;
server.on('connection', function (socket) {
// Add a newly connected socket
let socketId = nextSocketId++;
sockets[socketId] = socket;


// Remove the socket when it closes
socket.on('close', function () {
delete sockets[socketId];
});

});

const gracefulShutdown = function() {
console.log('Received kill signal, shutting down gracefully.');

Object.keys(sockets).map(socketId => {
sockets[socketId].setTimeout(15000);
})

server.close(function() {
console.log('Closed out remaining connections.');
process.exit(0);
});
}

// listen for TERM signal .e.g. kill
process.on ('SIGTERM', gracefulShutdown);

// listen for INT signal e.g. Ctrl-C
process.on ('SIGINT', gracefulShutdown);

个人觉得即使加了这些也未必能完美解决过渡问题,但是会留有一些日志,用于分析过渡期间的实际情况。

结束语

以上便是本次 Openshift 迁移中产生的一些经验心得,希望看到其他项目在迁移时不同的迁移方式和思考,欢迎交流。