从Gitlab CI启动tomcat的坑,到tty与进程组

问题症状

为了做一个Spring MVC的Java Web项目的CI,我写了个编译war包后启动tomcat的脚本。CI脚本很简单:

1
2
3
4
5
6
sit:
script:
- cd /root/
- ./test-publish-xxx-sit.sh
only:
- develop

SSH到服务器手工执行脚本一切顺利。但通过gitlab-runner执行脚本,到最后一步执行./startup.sh启动tomcat的时候,遇到了两个很奇怪的现象:

  • 和SSH下执行./startup.sh不同,没有打印环境变量(例如Using CATALINA_BASE:)。只显示了最后一句“Tomcat started.”
  • 虽然打印了“Tomcat started.”,tomcat却没有正常启动。catalina.out里完全没有启动日志信息
    尝试过从权限和执行用户方向排查,都没有找到原因。

解决方法

在gitlab的论坛看到有人回答需要部署为linux的service,或者加个setsid,才能启动。结果证明这两种方式都是可行的解决方案。

问题是解决了。但疑问还是没解决:

  • 为何同样的用户执行,打印的日志不一样?
  • 为何普通脚本可以成功执行,但执行tomcat的启动脚本startup.sh的时候就会出问题?
    前一个问题和tty有关,后一个问题和Linux进程组有关。

引申1:tty

以前在python脚本排查的时候遇到过一个诡异的问题:sudo -i切换root下时可以正常执行的命令,到su - root切换到root下就执行失败了。最终发现问题和PATH环境变量有关。但这次明显不是这个原因,要不然也不会打印“Tomcat started.”的日志。
在翻了tomcat的catalina.sh后,找到了这段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Bugzilla 37848: only output this if we have a TTY
if [ $have_tty -eq 1 ]; then
echo "Using CATALINA_BASE: $CATALINA_BASE"
echo "Using CATALINA_HOME: $CATALINA_HOME"
echo "Using CATALINA_TMPDIR: $CATALINA_TMPDIR"
if [ "$1" = "debug" ] ; then
echo "Using JAVA_HOME: $JAVA_HOME"
else
echo "Using JRE_HOME: $JRE_HOME"
fi
echo "Using CLASSPATH: $CLASSPATH"
if [ ! -z "$CATALINA_PID" ]; then
echo "Using CATALINA_PID: $CATALINA_PID"
fi
fi

而have_tty这个变量是执行tty后的结果:

1
2
3
4
have_tty=0
if [ "`tty`" != "not a tty" ]; then
have_tty=1
fi

SSH的时候执行tty的结果是/dev/pts/{数字},而gitlab-runner执行的结果是not a tty。
tty的含义可以参见文末的参考资料,可以简单理解为终端。gitlab与gitlab-runner通信的时候是通过https请求,没有终端。所以按照tomcat启动脚本的逻辑不会输出环境变量。

引申2:进程组

排查时最疑惑的点在于:输出日志里打印了“Tomcat started.”,表示tomcat的启动脚本已经跑完了。但为何tomcat的进程不存在,catalina.out里也完全没有相关日志?
要解释这个问题,需要从进程组开始解释。

当开两个SSH连到Linux服务器上,执行ps auxf命令,可以得到如下结果:

1
2
3
4
5
6
7
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root 1101 0.0 0.1 106084 4136 ? Ss Mar20 0:00 /usr/sbin/sshd -D
root 14590 0.0 0.1 145788 5244 ? Ss 23:00 0:00 \_ sshd: root@pts/0
root 14594 0.0 0.0 115440 2028 pts/0 Ss+ 23:00 0:00 | \_ -bash
root 14631 3.2 0.1 145788 5240 ? Ss 23:23 0:00 \_ sshd: root@pts/1
root 14635 0.2 0.0 115436 2084 pts/1 Ss 23:23 0:00 \_ -bash
root 14651 0.0 0.0 151244 1928 pts/1 R+ 23:23 0:00 \_ ps auxf

这个界面展现了ssh相关的进程和进程间的父子关系。
TTY那一列中的pts/0和pts/1分别对应两个SSH终端。sshd对应下图中的ssh server。 bash是sshd进程创建的子进程。
当在第二个终端上通过bash执行ps auxf命令时,由bash进程创建ps auxf子进程。

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
+----------+       +------------+
| Keyboard |------>| |
+----------+ | Terminal |
| Monitor |<------| |
+----------+ +------------+
|
| ssh protocol
|

+------------+
| |
| ssh server |--------------------------+
| | fork |
+------------+ |
| ↑ |
| | |
write | | read |
| | |
+-----|---|-------------------+ |
| | | | ↓
| ↓ | +-------+ | +-------+
| +--------+ | pts/0 |<---------->| shell |
| | | +-------+ | +-------+
| | ptmx |<->| pts/1 |<---------->| shell |
| | | +-------+ | +-------+
| +--------+ | pts/2 |<---------->| shell |
| +-------+ | +-------+
| Kernel |
+-----------------------------+

状态列STAT中的加号“+”表示前台进程(可以通过man ps命令查看各种状态的详情)。第一个大S表示进程在中断睡眠,大R表示运行中。第二个小s表示是会话的session leader。
每个SSH窗口对应一个session会话。一个会话可以由多个进程组构成。一个进程组成为会话的前台工作(foreground),而其他的进程组是后台工作(background)。
我们也可以执行命令的时候添加&,使进程组成为后台进程组。

在熟悉了这些知识后,我们来回顾一下我们的gitlab-runner脚本。
我们是通过./test-publish-xxx-sit.sh命令来调用脚本。在我修改之前,脚本是通过调用./startup.sh启动tomcat。
./xxx.sh是fork调用,即从当前进程创建一个子进程来执行脚本。(另外两种是source和exec)
startup.sh fork调用了catalina.sh。而catalina.sh通过

1
java 【省略参数】 org.apache.catalina.startup.Bootstrap start 【省略参数】 &

这条命令启动了tomcat。总结一下,父子关系大致如下:

1
2
3
4
5
6
/usr/lib/gitlab-runner/gitlab-runner run
\_ /bin/bash
\_ /bin/sh ./test-publish-xxx-sit.sh
\_ /bin/sh ./startup.sh
\_ /bin/sh ./catalina.sh
\_ /usr/bin/java org.apache.catalina.startup.Bootstrap start

通过gitlab runner的源代码可以看到,gitlab runner在执行完每条命令,对该命令的进程组执行KillProcessGroup操作。

1
2
3
4
5
6
7
select {
case err = <-waitCh:
return err

case <-cmd.Context.Done():
return s.killAndWait(c, waitCh)
}
1
2
3
4
5
6
7
8
9
10
11
func (s *executor) killAndWait(cmd *exec.Cmd, waitCh chan error) error {
for {
s.Debugln("Aborting command...")
helpers.KillProcessGroup(cmd)
select {
case <-time.After(time.Second):
case err := <-waitCh:
return err
}
}
}

所以当gitlab-runner执行完/bin/sh ./test-publish-xxx-sit.sh这个命令,杀掉进程组后,tomcat进程也被跟着一起杀掉了。
这也解释了为什么tomcat部署为服务和setsid命令会起效。
当setsid后,tomcat的启动脚本进程和原父进程脱离关系,成为了孤儿进程。
当部署为服务后,tomcat成为了守护进程,自然也和gitlab-runner的进程没有了关系。

后记

想起来当初刚开始玩Spring Boot的时候,在Linux服务器上用java -jar加上&后台启动应用后,过了两小时后进程被自动杀掉了。一开始还以为是Spring Boot的Bug。。。在总结了发生规律后,才发现是和SSH session有关。改为了nohup+&启动后问题解决。之后又改为优雅一些的注册为系统服务。但对原理的不甚了了,最终还是导致这次栽坑了。
知其然,知其所以然。
不过这次相比之前也有一点改进:总算储备的shell知识积累到有胆子去翻tomcat启动脚本了。这次顺带解答了我之前的一个疑惑:为什么启动tomcat的启动命令./startup.sh时不用后面加&。这是因为启动脚本里已经带了:

1
2
3
4
5
6
7
8
eval \{ $_NOHUP "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
-D$ENDORSED_PROP="\"$JAVA_ENDORSED_DIRS\"" \
-classpath "\"$CLASSPATH\"" \
-Dcatalina.base="\"$CATALINA_BASE\"" \
-Dcatalina.home="\"$CATALINA_HOME\"" \
-Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
org.apache.catalina.startup.Bootstrap "$@" start \
2\>\&1 \& echo \$! \>\"$catalina_pid_file\" \; \} $catalina_out_command "&"

但NOHUP参数默认不加,所以还是会被父进程杀掉。

参考资料

Attempting to restart tomcat 8 with gitlab-runner, pid file created, log empty, server not started - Server Fault
感谢作者解决问题后补充的回答。要不然我还钻在Google里,想不到去看tomcat启动脚本和gitlab-runner的源代码。

Linux TTY/PTS概述 - Linux程序员 - SegmentFault 思否
非常生动形象地用ASCII图展现了TTY的原理。

Linux 技巧:让进程在后台可靠运行的几种方法
解释了为什么setsid和disown命令可以起效。

终端断开导致Tomcat进程被kill问题分析 | El Psy Congroo
tomcat的另一种非正常死法,通过进程组实验的方式解释了原理。我没有产生过作者那样的疑问,主要是个人习惯太好了,从来不会做不退出脚本就直接关闭终端的行为(雾

本文永久链接 [ https://galaxyyao.github.io/2019/03/28/Gitlab-CI-pipeline启动tomcat中遇到的坑/ ]

分布式配置中心 - 1. 配置中心介绍

实施计划

  • 先以测试环境一个应用作为试点,实施配置中心
  • 稳定运行至少两周后,在生产环境的该应用上实施配置中心
  • 再稳定运行两周后,在测试和生产环境的所有应用都改为使用配置中心
  • 配置中心先使用Spring Cloud Config。不够用的话再考虑评估Apollo和Nacos

选型的考量

Spring Cloud Config的原理和架构相对比较容易理解,所以先以Spring Cloud Config作为一个具体的实例,介绍一个简化版本的配置中心。如果只是小团队的话,Spring Cloud Config已经足够用了。
但对于大一些的团队来说,还是存在以下两个不足:

  • UI只能依赖Gitlab的界面
  • 依赖于Git的权限控制粒度比较粗,也很难扩展

什么是配置中心

这里引用几篇配置中心的科普文。

第一篇

动态调整的基础 —— 配置中心
这篇应该是淘宝在2016年之前的方案。
虽然相对于现在已经比较out-of-date,但其中提到的几点比较有意思:

  • 配置动态化的需求发展历程
  • 应用 - 平台 - 版本 - 模块的元信息模型。在spring cloud config里,对应{application}/{profile}/{label}的三个层次。在Apollo里,对应Namespace。
  • 把配置中心拆分成网关、核心服务和界面系统三个部分。在Apollo里,也拆分为Meta Server/Config Service/Admin Service三块。
    去年的一次分享上听一个阿里的技术专家说,他进阿里第一个月做的事情就是写开关配置。光20多行的代码就加了5个开关。阿里的这个“不要写死”的要求,在加强了灵活性的同时,对配置管理也提出了很高的要求。

第二篇

下面这篇提出了一个通用版的配置中心的架构图。
为什么需要分布式配置中心? - 徐刘根

第三篇

一篇好TM长的关于配置中心的文章 | 阿里中间件团队博客
顺便提一句,文中提到的淘宝的开源配置中心项目Diamond,但已经5年没有维护了。。。

配置中心的优点与缺点

优点

  • 避免敏感信息在源代码中暴露,安全性提升
  • 可以实现不重启应用就动态刷新配置
  • 可以在应用间共享配置。原本只能在应用内共享配置
  • 配置集中化管理,易于全局管理所有配置
  • 对配置进行权限管理

缺点

  • 原本下载应用代码就可以启动,现在还增加依赖spring cloud config server服务/Apollo服务
  • 如果config服务挂了,应用会无法重启
  • 不如配置放在项目中那么直观
  • 多分支开发的时候,处理不当的话可能会互相影响

本文永久链接 [ https://galaxyyao.github.io/2019/03/26/分布式配置中心-1-配置中心介绍/ ]

Twelve-Factor 12要素12原则

概述

英文版
https://12factor.net/zh_cn/
中文版
https://12factor.net/

这篇文章是Spring Cloud文档的总述部分提到的。12要素指的是构建SaaS应用的方法论的总结,一共有12条。
当得知这12要素是在2011年就提出的时候,我不禁由衷地钦佩。想想11年的时候我[消音–]都在做些啥。。。
从另外一个角度可以学习到的是如何从实践中提炼和总结经验,然后总结为理论。通过“实践-理论-实践-理论”这样的循环,实现螺旋式地提升。

意义

类似“社会主义核心价值观”(手动狗头),12要素对我们的意义主要在对实现的指导性。我们有些时候会觉得当前的代码管理/配置管理/部署方案/运维方案不够好,但又不知道怎么改进;有些时候看到了多种改进方案,但不知道哪一种更好。在这种时候可以参考12要素来决策。

12要素归类

我认为12要素可以粗略分为几类:

代码管理

I. 基准代码/II. 依赖

配置管理

III. 配置/IV. 后端服务

部署

V. 构建,发布,运行/VI. 进程/VII. 端口绑定/VIII. 并发

线上运维

IX. 易处理/X. 开发环境与线上环境等价/XI. 日志/XII. 管理进程

12要素的修订和引申

毕竟是在8年前的2011年提出的,随着技术的发展,也有对12要素提出了一些修订。比如如下这篇:
MRA(Microservices Reference Architecture), Part 5: Adapting the Twelve Factor App for Microservices

主要有以下几点:

  • 对于“II. 依赖”,不仅限于类库依赖,还包括部署环境依赖。非容器环境,使用运维管理工具(Chef, Puppet, Ansible)来安装系统依赖;对于容器环境,使用Dockerfile。后面也有几点是基于容器化方案的改进意见。
  • 对于“VII. 端口绑定”,将原则扩展到数据隔离。即如果要获取另一个微服务的数据,只能通过API,而不能通过其他方式(例如读取数据库)另外要避免微服务之间有明显的依赖。

反思我们当前不符合12要素的实现

配置

现在的生产配置的加密方式还很粗糙。
另外现在配置还没有实现从环境变量中读取。
以上两点将在实施配置中心后改善。

服务间依赖

部分微服务之间还有明显的依赖,无法避免雪崩效应。后续将引入服务发现和服务注册来改善。

进程无状态

部分应用的登录接口还带有粘性session。这是12-Factor极力反对的。
但避免粘性Session的一个必要条件是保持缓存服务的高可用。后续需要对Redis的高可用进行升级。

日志

日志分析/统计/告警已经迈出了第一步,还有待后续改进措施的实施。

本文永久链接 [ https://galaxyyao.github.io/2019/03/22/Twelve-Factor-12要素12原则/ ]

github级技术博客起航,以及ARTS

陈皓在极客时间的专栏中提出过一个ARTS运动

  • Algorithm:每周至少做一道leetcode算法题
  • Review:每周阅读并点评至少一篇英文技术文章
  • Tip:每周学习至少一个技术技巧
  • Share:每周分享一篇有观点和思考的技术文章
    我曾经在cnblog上写过博客,也在公司里做过一个名为技术沙龙的专栏,每2周写一个主题。虽然保持更新不轻松,但回想起来,有技术输出的那段时间也是技术上成长最快的时期。

虽然大多数内容是蜻蜓点水,但也会尽量使博客的内容不是简单的copy & paste。希望这个博客里有一点内容能对你有帮助,save your days。
希望在女儿出世后也还能保持月更。

有兴趣沟通的话可以通过邮件联系我:galaxyyao[at]live.com

本文永久链接 [ https://galaxyyao.github.io/2019/03/21/hello-world/ ]