前言
由于目前就职的是一家做物联网相关产品的科技公司, 为了帮助公司解决后端发布更新时导致服务不可用问题及帮助项目实现未来业务扩张后集群运行等问题, 结合公司实际情况, 探索合适的技术栈来解决这些问题。最终决定使用 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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/ |
存储路径 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| # 包名 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
是 容器
的标签
附录
1 2 3 4 5 6 7 8
| #!/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
|