背景
最近在做一个 APP 的后端开发,为了方便和客户端同学联调,用了一台阿里云的机器来部署服务,本文简单记录相关部署和调试流程,以备查阅。
服务环境准备
考虑到测试环境对性能和稳定性要求不高,为了节省成本,选择之间将 MySQL 和 Redis 都安装在同一台机器上,其中 Linux 版本为 CentOS Linux release 7.9
。
配置 SSH 密钥登录
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
|
# 1. 客户端生成密钥对,系统有提示,一路回车即可
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_aliyun -C "your_email@example.com"
# 2. 将私钥添加到 ssh-agent
eval (ssh-agent -c) # 启动 ssh-agent (fish 写法)
ssh-add ~/.ssh/id_ed25519_aliyun
ssh-add -l # 查看已添加的公钥
# 3. 将公钥添加到远程服务器
cat ~/.ssh/id_ed25519_aliyun.pub | ssh username@server_ip "cat >> ~/.ssh/authorized_keys"
# 4. 配置 fish 自动启动 ssh-agent
# 在 ~/.config/fish/config.fish 中添加
if status is-interactive
# 检查 ssh-agent 是否已运行
if not test -e $SSH_AUTH_SOCK
eval (ssh-agent -c)
set -Ux SSH_AUTH_SOCK $SSH_AUTH_SOCK
set -Ux SSH_AGENT_PID $SSH_AGENT_PID
end
# 添加密钥(如果未添加)
ssh-add -l >/dev/null 2>&1
if test $status -eq 1
ssh-add ~/.ssh/id_ed25519_aliyun
ssh-add ~/.ssh/id_ed25519
end
end
# 5. 编辑 SSH 配置文件
# 在 ~/.ssh/config 添加主机配置(之后直接 ssh aliyun_test_server 就可以登录服务器了)
Host aliyun_test_server
HostName server_ip
User root
UseKeychain yes
AddKeysToAgent yes
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_ed25519_aliyun
|
安装 Java、MySQL、Redis 等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
# 1. Java(如果安装的是 java-1.8.0-openjdk,则仅仅是 JRE,不包含额外的开发工具(如 javac、jps、jstack 等)
sudo yum install java-1.8.0-openjdk-devel
# 2. Redis
# 安装
sudo yum install -y redis
# 启动 Redis
systemctl start redis
# 设置开机启动
systemctl enable redis
# 验证 Redis
redis-cli ping
# 3. MySQL
sudo yum install -y mysql-community-server
# 启动 MySQL 服务
sudo systemctl start mysqld
# 设置开机启动
sudo systemctl enable mysqld
# 查看默认的 root 用户密码
grep 'temporary password' /var/log/mysqld.log
# 登录 MySQL
mysql -u root -p
|
部署 Java 服务
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
|
1. 本地构建得到 jar 包
mvn clean package
2. 打包本地 lib 包(其中包含所有 maven 依赖 jar)
# 考虑使用 maven-assembly-plugin 插件生成包含所有依赖的 JAR 文件,省略该步骤
zip -r lib.zip lib/
2. 使用 scp 传输到远程服务器
scp target/your-project-jar-with-dependencies.jar user@remote-server:/path/to/deploy
scp target/lib.zip user@remote-server:/path/to/deploy
unzip lib.zip -d lib/ # 登录远程服务器后解压
3. 编写启动脚本启动服务
# vim restart.sh
#!/bin/bash
echo "************ 查找进程 **************"
str=`ps aux | grep "biz-1.0.0.jar" | grep -v grep | awk '{print $2}'`
echo $str biz
kill -9 $str
if [ "$?" -eq 0 ]; then
echo "kill success"
else
echo "kill failed"
fi
echo "************ 杀掉进程 **************"
export SUPERVISOR_LOG_HOME="logs"
JAVA_OPTS="-server -Xmx512m -Xms512m -Xmn256m -Xss256k -XX:MetaspaceSize=96m"
JAVA_OPTS="$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=localhost:5005"
JAVA_OPTS="$JAVA_OPTS -XX:+DisableExplicitGC"
JAVA_OPTS="$JAVA_OPTS -XX:+UseConcMarkSweepGC"
JAVA_OPTS="$JAVA_OPTS -XX:CMSMaxAbortablePrecleanTime=5000"
JAVA_OPTS="$JAVA_OPTS -XX:+CMSClassUnloadingEnabled "
JAVA_OPTS="$JAVA_OPTS -XX:+CMSParallelRemarkEnabled"
JAVA_OPTS="$JAVA_OPTS -XX:+UseFastAccessorMethods"
JAVA_OPTS="$JAVA_OPTS -XX:+UseCMSInitiatingOccupancyOnly"
JAVA_OPTS="$JAVA_OPTS -XX:+ExplicitGCInvokesConcurrent"
JAVA_OPTS="$JAVA_OPTS -XX:CMSInitiatingOccupancyFraction=80"
JAVA_OPTS="$JAVA_OPTS -XX:SurvivorRatio=8"
JAVA_OPTS="$JAVA_OPTS -XX:+HeapDumpOnOutOfMemoryError -XX:+OmitStackTraceInFastThrow -XX:HeapDumpPath=$SUPERVISOR_LOG_HOME/java.hprof"
JAVA_OPTS="$JAVA_OPTS -Xloggc:$SUPERVISOR_LOG_HOME/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps"
JAVA_OPTS="$JAVA_OPTS -Dlog.path=$SUPERVISOR_LOG_HOME"
nohup java $JAVA_OPTS -Dspring.profiles.active=test -jar biz-1.0.0.jar 2>&1 &
echo "************ 启动成功 **************"
|
目前这个非自动化部署的流程,操作起来还是比较繁琐。
使用 SSH 隧道进行远程调试
出于安全考虑,debug 绑定只绑定本地回环接口,然后通过 SSH 隧道进行连接。
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
|
# 1. 在远程服务器上启动 Java 应用时添加调试参数
# 配置 Java 应用只监听 localhost
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=localhost:5005 YourApp
# 2. 建立 SSH 隧道
# 语法:ssh -L 本地端口:远程主机:远程端口 用户名@远程主机
# 这里我实际用的是配置在 .ssh/config 中的主键昵称-aliyun_test_server
ssh -L 5005:localhost:5005 aliyun_test_server
# 如果需要在后台运行
ssh -fNL 5005:localhost:5005 aliyun_test_server
# 3.配置 IDE
- 配置远程调试
- 连接地址:localhost:5005(使用本地转发的端口
# 4. 常用的 SSH 隧道命令选项
# -f: 后台运行
# -N: 不执行远程命令
# -L: 本地端口转发
# -v: 显示详细信息(调试用)
# 详细模式(便于调试)
ssh -vNL 5005:localhost:5005 root@server_ip
# 后台运行模式
ssh -fNL 5005:localhost:5005 root@server_ip
# 指定密钥文件
ssh -i ~/.ssh/id_ed25519_aliyun.pub -fNL 5005:localhost:5005 root@server_ip
# 5. 检查隧道状态
# 查看已建立的连接
netstat -tunlp | grep 5005
# 查看 SSH 进程
ps aux | grep ssh
# 测试端口转发是否成功
nc -zv localhost 5005
# 6. 关闭 SSH 隧道
# 查找 SSH 进程
ps aux | grep ssh
# 关闭特定的 SSH 隧道
kill <进程ID>
# 或者关闭所有 SSH 隧道(谨慎使用)
pkill -f "ssh -fNL"
|
遇到的问题
一个负责登录认证的拦截器未注册
比较奇怪的点在于,在本地这个拦截器是正常的,但部署到远程之后它就失效了(远程 Debug 类 HandlerExecutionChain#applyPreHandle
发现确实没有执行这个拦截器的逻辑),导致一些需要登录认证的接口无法使用。
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Configuration
public class JwtInterceptorConfig extends WebMvcConfigurationSupport {
@Bean
public JwtInterceptor authenticationInterceptor() {
return new JwtInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**");
}
}
|
直接原因
- 排查发现当前服务还有另一个
WebMvcConfig
类也继承了 WebMvcConfigurationSupport
,根据 Spring 机制只会有一个类生效;
- 而具体哪个类生效,与
.class
文件从磁盘加载的顺序有关;
Spring 怎么保证只会有一个 WebMvcConfigurationSupport
子类生效?
- `ConfigurationClassParser#doProcessConfigurationClass` 会缓存已处理的父类来防止循环继承和重复处理;
- 当有多个继承 `WebMvcConfigurationSupport` 的子类存在时,只有第一个被加载的类 `beanMethod`(如`addInterceptors`) 会被处理,也即生效;
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
|
// ConfigurationClassParser(配置类解析器)
/**
* Apply processing and build a complete {@link ConfigurationClass} by reading the
* annotations, members and methods from the source class. This method can be called
* multiple times as relevant sources are discovered.
* @param configClass the configuration class being build
* @param sourceClass a source class
* @return the superclass, or {@code null} if none found or previously processed
*/
@Nullable
protected final SourceClass doProcessConfigurationClass(
ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
throws IOException {
// ... 此处代码省略 ...
// Process individual @Bean methods
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}
// Process default methods on interfaces
processInterfaces(configClass, sourceClass);
// 以下为父类缓存逻辑
// Process superclass, if any
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&
!this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
// Superclass found, return its annotation metadata and recurse
return sourceClass.getSuperClass();
}
}
// No superclass -> processing is complete
return null;
}
|
.class
文件具体是如何加载的?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// PathMatchingResourcePatternResolver(类加载器). 核心代码为 cl.getResources(path)
/**
* Find all class location resources with the given path via the ClassLoader.
* Called by {@link #findAllClassPathResources(String)}.
* @param path the absolute path within the classpath (never a leading slash)
* @return a mutable Set of matching Resource instances
* @since 4.1.1
*/
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
Set<Resource> result = new LinkedHashSet<>(16);
ClassLoader cl = getClassLoader();
Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
while (resourceUrls.hasMoreElements()) {
URL url = resourceUrls.nextElement();
result.add(convertClassLoaderURL(url));
}
if (!StringUtils.hasLength(path)) {
// The above result is likely to be incomplete, i.e. only containing file system references.
// We need to have pointers to each of the jar files on the classpath as well...
addAllClassLoaderJarRoots(cl, result);
}
return result;
}
|