前言
由于目前就职的是一家做物联网相关产品的科技公司, 为了帮助公司解决后端发布更新时导致服务不可用问题及帮助项目实现未来业务扩张后集群运行等问题, 结合公司实际情况, 探索合适的技术栈来解决这些问题。最终决定使用
Jenkins
+Swarm
来进行实施. 至于采用这套技术栈的原因, 大概可以总结为以下几点:
- 公司业务需要保证服务高度可用, 发版/更新期间服务不被中断
- 公司规模小, 提供的云上资源也同样很少, 需要轻量化的解决方案
- 后端框架陈旧, 资源分布混乱, 需要一套合理的项目管理方法
- 没有专职的运维人员, 要求配置和管理尽量简单, 降低使用者学习难度曲线
综上所述, 在容器编排选型上面就直接抛弃了
k8s
而选择轻量化的swarm
. 包括镜像管理这一块, 也是直接去掉了公共的存储仓库而选择直接在目标机器操作.
技术要点
- 项目代码通过
Gitlab
管理, 并遵循工作流方式编写和提交 - 编译打包通过
Jenkins
+Git
+Maven
实现, 要求项目能够参数化构建 - 镜像生成使用
ssh
远程编译(由于不使用镜像仓库, 所以只打包不推送) - 集群方案使用
Swarm
+docker-compose
方式管理, 使用compose
的目的是因为公司的资源不多, 单台4C8G的ECS
要跑4个J2EE
项目服务以及还有各种中间件, 所以部分有状态的中间件之类的服务通过compose
来跑尽量节省资源 - 反向代理使用
Traefik
, 减少nginx
配置(主要原因还是没有k8s
那样成熟的ingeress
) - 容器健康检查, 实现滚动更新时保证新版本服务可用之后才 down 掉旧服务
- 容器通信使用
Swarm
模式原生方式, 为了保证docker-compose
部署的服务和Swarm
的服务能够相互连接, 需要使用自定义的Swarm
网络 - 统一日志收集, 集群化之后日志将变得分散, 需要使用有效手段来集中管理
实施过程
docker & swarm 安装
参见 Dokcer-CE & Docker Compose 安装
为了做到足够简单, 并没有去配置
Docker
的远程API, 后面统一使用Jenkins
远程SSH操作
Traefik部署
参见 Traefik-V2.0 & Docker-Compose 最佳实践
项目使用的是Swarm
模式, 所以上面文章的内容需要作适量改动, 详情参照官网
https://docs.traefik.io/v2.0/providers/docker/#swarmmode
项目因为只有单节点, 所以Traefik
只是以compose
部署的非集群方式运行
Jenkins部署
参见Jenkins + Git + Maven持续构建部署环境搭建
这次搭建过程处理上述的3个插件之外, 还使用到一个轻量的插件 Publish Over SSH, 通过它来实现传输文件和执行命令以及管理主机(不使用
scp
&ssh
的原因是这个插件可以通过密码登录, 不用修改sshd
服务)
另外赠送一篇记录解决Jenkins
安装插件缓慢问题的文章 Jenkins for Docker 跳过插件安装及插件加速镜像设置
另外Jenkins
需要的Git
Maven
JDK
等组件我这边都是通过 Jenkins 内部安装的, 就不细讲了
ELK部署
日志收集分析使用的业界常用的
ELK
方案(找了很久没有找到合适的轻量替代方案), 搭配阿里的log-pilot
收集器体验非常好,为了减少系统采销, 这里使用的是实际实施发现5.6.16
版本5.x
功能太弱7.x
消耗太高, 最终使用的6.8.5
版本
docker-compose.yaml
version: '3.7'
services:
# https://yq.aliyun.com/articles/674327
log-pilot:
image: registry.cn-hangzhou.aliyuncs.com/acs/log-pilot:0.9.7-filebeat
restart: always
privileged: true
environment:
- PILOT_TYPE=filebeat
- LOGGING_OUTPUT=elasticsearch
- ELASTICSEARCH_HOSTS=elasticsearch:9200
# - ELASTICSEARCH_USER=
# - ELASTICSEARCH_PASSWORD=
volumes:
- "/:/host:ro"
- "/etc/localtime:/etc/localtime:ro"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
# https://blog.csdn.net/qq_38906421/article/details/88644315
# https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html
elasticsearch:
image: elasticsearch:6.8.5
restart: always
environment:
- ES_JAVA_OPTS=-Xms256m -Xmx256m
- xpack.security.enabled=true
volumes:
- "{es_data_path}:/usr/share/elasticsearch/data/:rw"
# https://www.elastic.co/guide/en/kibana/current/docker.html
kibana:
image: kibana:6.8.5
restart: always
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
- XPACK_MONITORING_ENABLED=true
- ELASTICSEARCH_USERNAME=kibana
- ELASTICSEARCH_PASSWORD={password}
ports:
- 5601:5601
综合实现
上面的部分基本就是环境准备, 主要的实现还是在 Jenkins 里面的脚本, 主要方案就是打包项目之后, 将项目发送到目标机器之后, 在通过
Docker
编译, 最终使用docker stack deploy
命令发布服务到Swarm
简单记录一下使用到的脚本配置(下面的脚本属于和项目绑定的关系, 不适用于任何其他项目, 仅作为参考)
KEY | VALUE | 备注 |
---|---|---|
Source files | module/target/app.war | 传输的文件 |
Remove prefix | module/target/ | 忽略目录 |
Remote directory | /root/docker-build/$JOB_BASE_NAME-$BUILD_NUMBER/ | 存储路径 |
# 包名
PN=app.war
# 镜像名字
IN=domain/project:$BUILD_NUMBER
# 工作目录
WD=/root/docker-build/$JOB_BASE_NAME-$BUILD_NUMBER/
# 准备
cd $WD
# 构建
cat << EOF > Dockerfile
FROM tomcat:8.5.47-jdk8-openjdk
RUN \
echo 'tomcat.util.http.parser.HttpParser.requestTargetAllow=|{}' >> /usr/local/tomcat/conf/catalina.properties && \
rm -rf /usr/local/tomcat/webapps/*
ENV TZ=Asia/Shanghai
ENV JAVA_OPTS="-Xmx256m -Djava.security.egd=file:/dev/urandom"
ADD $PN /usr/local/tomcat/webapps/$PN
HEALTHCHECK --interval=10s --timeout=3s --start-period=5m --retries=3 CMD curl --silent --fail http://localhost:8080/app/ || exit 1
EOF
docker build -t $IN .
# 发布
cat << EOF > docker-compose.yaml
version: '3.7'
services:
app:
image: $IN
networks:
- global
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.gzip.compress=true"
- "traefik.http.middlewares.ssl.headers.sslRedirect=true"
- "traefik.http.routers.app_http.entrypoints=http"
- "traefik.http.routers.app_http.rule=Host(\`api.domain.org\`)"
- "traefik.http.routers.app_http.middlewares=gzip"
- "traefik.http.routers.app_https.entrypoints=https"
- "traefik.http.routers.app_https.rule=Host(\`api.domain.org\`)"
- "traefik.http.routers.app_https.middlewares=gzip"
- "traefik.http.routers.app_https.tls.certresolver=acme-resolver"
- "traefik.http.services.app.loadbalancer.server.port=8080"
# index_topic 实测不能出现下划线
- "aliyun.logs.${index_topic}=stdout"
deploy:
mode: replicated
replicas: 2
update_config:
parallelism: 1
order: start-first
networks:
global:
name: net-global
external: true
EOF
docker stack deploy --resolve-image never --orchestrator swarm -c docker-compose.yaml $JOB_BASE_NAME
之所以没有将
labels
放到deploy
之下, 是因为Traefik
是以非集群模式运行的
labels
在deploy
之下时labels
是服务
的标签
labels
在service
之下时labels
是容器
的标签
附录
- 用于测试Api的脚本
#!/bin/sh
while true
do
current=`date "+%Y-%m-%d %H:%M:%S"`
timeStamp=`date -d "$current" +%s`
echo $timeStamp-`curl -I -m 1 -f http://domain:port/path -o /dev/null -s -w %{http_code}`
sleep 1
done