阿里云部署调试 Java 服务记录

最后修改于:

背景

最近在做一个 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;
}
本文总阅读量 次 本文总访客量 人 本站总访问量 次 本站总访客数
发表了20篇文章 · 总计32.36k字
本博客已稳定运行
使用 Hugo 构建
主题 StackJimmy 设计