likes
comments
collection
share

使用 GraalVM 的快速 Spring Boot AWS Lambda

作者站长头像
站长
· 阅读数 33

冷启动是一个很大的缺点——Java 和 Spring Boot 的启动速度并不快,典型的全脂 Spring Boot 转换的 lambda 可能需要 10 到 90 秒,具体取决于内存量和你分配的 CPU。这可能会迫使您过度配置它们以补偿冷启动,但这是一个非常昂贵的大锤。总是有预置的并发性,但这也不会便宜得多(并且否定了 lambda 的响应可伸缩性,因为您必须提前预测需要多少)。

但是如果我告诉你同样的功能可以在 3 秒内从冷启动开始呢?与其他语言相比,它仍然有点迟钝,但考虑到容器或 Lambda 中的 Sprint Boot jar 的可比启动时间,它非常具有开创性。这可能是因为GraalVM

使用 GraalVM 的快速 Spring Boot AWS Lambda GraalVM 在过去几年中获得了很大的吸引力——它允许我们构建平台特定的二进制文件,无需 JVM 就可以直接运行,这样,我们可以加快我们的冷启动时间。职能。它仍处于起步阶段,但现在有一个强大的社区,您面临的许多常见问题都可以通过一点 Google-fu 来解决。

在本文中,我将向您展示如何将一个真实的 REST 应用程序示例 ( Spring Petclinic ) 适应spring-cloud-function,并使用 GraalVM 显着加快冷启动时间,同时减少内存/CPU 占用。

我将完成我放在一起的 GitHub 示例,请随时跟随并为您自己的目的借用。

免责声明 - 在撰写本文时,GraalVM 仍处于测试阶段,除了此处记录的问题之外,您可能还会遇到其他问题。如果将这种方法用于生产工作负载,建议谨慎考虑。

迁移到 GraalVM

首先,我遵循了 Spring 指南,了解如何开始使用 GraalVM

诀窍是尽可能优化构建时间。您可以推动构建时间初始化的越多越好。默认情况下,Spring Native 将在运行时初始化所有类(这并没有比通常的带有 JIT 组合的 JVM 提供太多好处),但是您可以显式声明应该在构建时初始化的类。 

这里有一篇很好的文章讨论了这种默认行为,以及如何确定哪些类是构建时初始化的候选对象。Spring Native 大大简化了这一点,因为它已经知道所有适合在启动时初始化的 Spring 框架类,并相应地配置native-image构建。

GraalVM 与 Spring 和 Spring Boot 相当兼容,但是,两者之间有一个已知的问题列表,虽然希望随着时间的推移得到修复,但现在值得注意,因为它们可能会让你绊倒。我已经收集了我在此过程中遇到的问题的列表 - 有一些方法可以解决这些问题,但它们可能不适用于每个应用程序。

首先,需要添加一些依赖项和插件来pom.xml支持 GraalVM 的使用。 

这是基于我之前的帖子,该帖子展示了如何将 Spring Boot 应用程序移植到 Lambda,因此我不会在此处包含这些详细信息。您可以在此处查看我的完整 pom,但具体而言,这是将以下内容添加到lambda配置文件的情况:

<properties>
                ...
                <repackage.classifier>exec</repackage.classifier>
            </properties>
             ...   
             <dependency>
                    <groupId>org.springframework.experimental</groupId>
                    <artifactId>spring-native</artifactId>
                    <version>0.10.3</version>
                </dependency>
            </dependencies>
            ...
            <plugin>
                        <groupId>org.springframework.experimental</groupId>
                        <artifactId>spring-aot-maven-plugin</artifactId>
                        <version>0.10.3</version>
                        <executions>
                            <execution>
                                <id>test-generate</id>
                                <goals>
                                    <goal>test-generate</goal>
                                </goals>
                            </execution>
                            <execution>
                                <id>generate</id>
                                <goals>
                                    <goal>generate</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <groupId>org.hibernate.orm.tooling</groupId>
                        <artifactId>hibernate-enhance-maven-plugin</artifactId>
                        <version>5.4.30.Final</version>
                        <executions>
                            <execution>
                                <configuration>
                                    <failOnError>true</failOnError>
                                    <enableLazyInitialization>true</enableLazyInitialization>
                                    <enableDirtyTracking>true</enableDirtyTracking>
                                    <enableAssociationManagement>true</enableAssociationManagement>
                                    <enableExtendedEnhancement>false</enableExtendedEnhancement>
                                </configuration>
                                <goals>
                                    <goal>enhance</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-deploy-plugin</artifactId>
                        <configuration>
                            <skip>true</skip>
                        </configuration>
                    </plugin>
                    <plugin>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-maven-plugin</artifactId>
                        <configuration>
                            <classifier>${repackage.classifier}</classifier>
                        </configuration>
                    </plugin>
                    <plugin>
                        <groupId>org.graalvm.buildtools</groupId>
                        <artifactId>native-maven-plugin</artifactId>
                        <version>0.9.4</version>
                        <executions>
                            <execution>
                                <goals>
                                    <goal>build</goal>
                                </goals>
                                <phase>package</phase>
                            </execution>
                            <execution>
                                <id>test</id>
                                <goals>
                                    <goal>test</goal>
                                </goals>
                                <phase>test</phase>
                            </execution>
                        </executions>
                        <configuration>
                            <buildArgs>
                                --enable-url-protocols=http
                                -H:+AddAllCharsets
                            </buildArgs>
                        </configuration>
                    </plugin>
                    <plugin>
                        <artifactId>maven-assembly-plugin</artifactId>
                        <executions>
                            <execution>
                                <id>native-zip</id>
                                <phase>package</phase>
                                <goals>
                                    <goal>single</goal>
                                </goals>
                                <inherited>false</inherited>
                            </execution>
                        </executions>
                        <configuration>
                            <descriptors>
                                <descriptor>src/assembly/native.xml</descriptor>
                            </descriptors>
                        </configuration>
                    </plugin>
```
```
上面的配置有几点值得一提:

-   `hibernate-enhance-maven-plugin`- 这允许 Hibernate 优化它在构建时所做的很多事情,以减少启动时间。不必与 LambdaGraalVM 一起使用 - 您也可以在标准应用程序上使用它
-   `spring-boot-maven-plugin``native-image`- 分类器属性阻止 Spring Boot 使用不兼容的 Spring Boot Uber Jar覆盖该工具使用的 jar
-   `native-maven-plugin`- 这是所有魔法发生的地方,稍后我将详细介绍。其中一个重要部分是`<configuration>`,它允许您控制本机映像构建过程的各个方面。
-   `maven-assembly-plugin`- 这用于获取我们将创建的二进制文件,并与 AWS Lambda 使用的引导脚本一起包装在一个 zip 存档中

这是获取 spring-cloud-function(或标准 Spring Boot 应用程序)并从中生成本机二进制文件所需的大部分配置。下一步是运行一个 Maven 包命令来启动它。如果你像我一样,你会想要在已经预配置了 JavaGraalVMDocker 容器中运行构建过程。这是我用来将应用程序代码和`.m2`目录挂载到容器中的图像和命令:

docker run -v $(pwd):/petclinic -v ~/.m2:/root/.m2 -it --name petclinic-graalvm ghcr.io/graalvm/graalvm-ce:latest bash

当在这个容器中时,您可以运行以下命令来触发构建(skipTests纯粹是从速度的角度来看,不推荐用于您的应用程序!):

./mvnw clean package -D skipTests -P lambda

我遇到的第一个问题(最后记录了更多)是 Devtools 还不支持:

使用 GraalVM 的快速 Spring Boot AWS Lambda 如果您使用 Devtools,那么您需要将其删除,或者将其移动到单独的配置文件中,您在构建二进制文件时有条件地禁用该配置文件,应该类似于以下内容:

<!--    <dependency>-->
<!--      <groupId>org.springframework.boot</groupId>-->
<!--      <artifactId>spring-boot-devtools</artifactId>-->
<!--      <optional>true</optional>-->
<!--    </dependency>-->

再次运行上述 Maven 命令和一个 cuppa,构建成功完成:

使用 GraalVM 的快速 Spring Boot AWS Lambda 所以此时,我们有一个编译好的二进制文件,到目前为止一切顺利!优化二进制文件的代价是更长的构建时间,但是,考虑到它提供的快速冷启动时间,我认为这是可以接受的(还有一些方法可以加快这个过程,例如在强大但短暂的构建代理上构建二进制文件) .

此时虽然我们有一个二进制文件,但无法在 AWS Lambda 中运行它。它不是一个 jar 文件,所以我们不能只上传它并告诉 Lambda 在 Java 运行时中执行它。

使用自定义运行时

接下来我需要了解的是如何让 GraalVM 原生镜像在 AWS Lambda 中运行。我知道有在 AWS Lambda 中构建自定义运行时的能力,但我以前从未尝试过这个,所以这对我来说是一个新领域。我对 AWS Lambda 如何获取 jar 和处理程序类并将其引导到 JVM 感到好奇。我想我需要了解这一点才能了解如何为我们的本机二进制文件构建等效的自定义运行时。

原来 AWS Lambda 将您的 jar 文件视为 zip。不是罐子。所以像 Jar Manifest 和 Main-Class 配置这样的元数据是无关紧要的。本文很好地了解了幕后发生的事情,以及如果您愿意,如何将您的工件直接构建为 zip 文件这是 TL;DR:Lambda 将展开的 jar 内容(包括您的处理程序类)添加为自定义类路径,而不是直接运行您的 jar(使用java -jar myjar.jar.

实际上,AWS Lambda 通过将该类和捆绑 zip 中的所有其他类包含到类路径中来运行您的处理程序,然后执行其自己的 Lambda Java 运行时,该运行时处理传入和传出处理程序类的请求和响应。如果您使用的是最新版本spring-cloud-function(撰写本文时为 3.2.0-M1),您可以看到 FunctionInvoker 类被配置为处理程序将 Spring Boot 应用程序上下文作为其构造函数的一部分进行初始化。

太好了,但是如何编写可以在 Lambda 和我的二进制文件之间进行互操作的自定义运行时?好吧,通过阅读更多内容,我了解到 Lambda API 是 RESTful 的,与此 API 交互取决于运行时。此外,此端点通过AWS_LAMBDA_RUNTIME_API环境变量提供给所有运行时。我开始考虑如何编写一个 bash 脚本来轮询此端点并调用我的二进制文件,传递事件有效负载,但这感觉非常麻烦,并且意味着应用程序必须在每个感觉错误的请求时重新生成。 

经过一番摸索,我终于明白了!我想知道spring-cloud-function团队是否已经提出了这个问题?当然,事实证明,通过在我的代码中快速搜索代码,AWS_LAMBDA_RUNTIME_API我找到了CustomRuntimeEventLoopCustomRuntimeInitializer类,完美!

自定义运行时事件循环

已经有一个如何使用 GraalVM 运行 spring cloud 功能的示例

确保设置以下内容以触发 spring-cloud 函数以运行 CustomRuntimeEventLoop

spring.cloud.function.web.export.enabled=true
spring.cloud.function.web.export.debug=true
spring.main.web-application-type=none
debug=true

实际上,在调试时我注意到你不应该启用spring.cloud.function.web.export.enabled,因为这会导致CustomRuntimeInitializer阻止CustomRuntimeEventLoop.

AWS Lambda 允许您通过提供bootstrapshell 脚本来提供在 Amazon Linux 上运行的自定义运行时。您可以使用它来引导以多种语言编写的应用程序。但对我们来说,我们需要做的就是执行我们的二进制文件:

#!/bin/sh

cd ${LAMBDA_TASK_ROOT:-.}

./spring-petclinic-rest

最后,我们只需要将这个bootstrap脚本和二进制文件捆绑到一个 zip 文件中,我们可以上传到 AWS Lambda。就是这样maven-assembly-plugin做的,使用以下配置/src/assembly/native.xml

<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 https://maven.apache.org/xsd/assembly-1.1.2.xsd">
    <id>native-zip</id>
    <formats>
        <format>zip</format>
    </formats>
    <baseDirectory></baseDirectory>
    <fileSets>
        <fileSet>
            <directory>src/shell</directory>
            <outputDirectory>/</outputDirectory>
            <useDefaultExcludes>true</useDefaultExcludes>
            <fileMode>0775</fileMode>
            <includes>
                <include>bootstrap</include>
            </includes>
        </fileSet>
        <fileSet>
            <directory>target</directory>
            <outputDirectory>/</outputDirectory>
            <useDefaultExcludes>true</useDefaultExcludes>
            <fileMode>0775</fileMode>
            <includes>
                <include>spring-petclinic-rest</include>
            </includes>
        </fileSet>
    </fileSets>
</assembly>

至此,我们有一个捆绑的 zip 文件,其中包含我们在 AWS 上的自定义运行时运行 GraalVM 二进制文件所需的一切,huzzah!

在 CDK 中配置

在我的 CDK 代码中,我有一个 lambda 堆栈,其中包含构建和部署 GraalVM lambda 所需的所有代码。

我之前用来构建二进制文件的graalvm-ce:latestdocker 镜像也可以在 CDK 进程中使用。主要区别在于,当在 CDK 框架中使用我们的代码时/asset-input,我们必须将最终的 .zip 文件放在/asset-output文件夹中,以便 CDK 可以将其提取并上传到 AWS Lambda:

const bundlingOptions = {
            bundling: {
                image: DockerImage.fromRegistry("ghcr.io/graalvm/graalvm-ce:21.2.0"),
                command: [
                    "/bin/sh",
                    "-c",
                    ["cd /asset-input/ ",
                        "./mvnw clean package -P lambda -D skipTests ",
                        "cp /asset-input/target/spring-petclinic-rest-2.4.2-native-zip.zip /asset-output/"].join(" && ")
                ],
                outputType: BundlingOutput.ARCHIVED,
                user: 'root',
                volumes: [{hostPath: `${homedir()}/.m2`, containerPath: '/root/.m2/'}]
            }
        };

要运行 GraalVM lambda 函数,它必须在 Amazon Linux 2 运行时中运行。我已经将一个函数的基本配置提取到下面,所以我可以在我的 2 个示例 lambda 中重用它:

const baseProps = {
            vpc: props?.vpc,
            runtime: Runtime.PROVIDED_AL2,
            code: Code.fromAsset(path.join(__dirname, '../../'), bundlingOptions),
            handler: 'duff.Class',
            vpcSubnets: {
                subnetType: ec2.SubnetType.PRIVATE
            },
            memorySize: 256,
            timeout: Duration.minutes(1),
            securityGroups: [lambdaSecurityGroup]
        }

如果您想了解 Java lambda 部署之间的差异,您可以在我的 Java 和 GraalVM 分支之间比较这个文件。一项重大改进是所需内存的显着减少 - 虽然 Java lambda 不一定需要 3GB 才能工作,但它需要将冷启动时间缩短到 20 秒左右,这仍然远非理想。

你们中的一些人可能会看到上面的内容并想“什么是duff.Class?”。我不确定这是否是我的疏忽或潜在的错误配置,但如果您使用org.springframework.cloud.function.adapter.aws.FunctionInvokerspring-cloud-function CustomEventRuntimeLoop不会启动。对此处理程序的使用进行了特定检查,看起来它假设它在标准中运行AWS Lambda 上的 Java 运行时(如果正在使用)。

将处理程序更改为除此之外的任何其他内容(甚至不必是真正的类)将触发CustomEventRuntimeLoop有效地充当自定义运行时中的入口点的处理程序,而不是在 Java 运行时中使用的 FunctionInvoker

学习更多JAVA知识与技巧,关注与私信博主

部署 Lambda

最后要做的是部署 Lambda 和支持资源(VPC、RDS MySQL 实例等)。如果您正在关注我的 GitHub 存储库,您可以执行以下操作,并在 30 分钟内从无到有完整的工作设置:

cdk deploy --require-approval=never --all

从那里开始,您将部署一个负载均衡器,将流量路由到新创建的 GraalVM Lambda,并具有令人印象深刻的(对于 Java)冷启动时间:

使用 GraalVM 的快速 Spring Boot AWS Lambda

结论

这就是一个真实的例子,它使用“全脂”Spring Boot 应用程序并将其转换为使用 GraalVM 的响应式 Lambda。您还可以优化 Spring Boot 和 GraalVM 以进一步改善冷启动时间,但只需最少的配置,这仍然会导致令人印象深刻的启动时间。

这不是一个轻松的旅程,我花了很长时间遇到各种兔子洞。为了帮助那些希望在自己的应用程序上尝试此功能的人,我整理了一份我在下面遇到的问题列表。

常见问题

  • 构建时问题

内存不足

对我来说,GraalVM 在其巅峰时期使用 10GB 内存来构建本机二进制文件。当我在 Docker 容器中运行构建时,我的 Mac 上的 Docker VM 运行时只有区区 2GB。令人沮丧的是,您所要做的就是这个神秘的错误代码 137:

使用 GraalVM 的快速 Spring Boot AWS Lambda 值得庆幸的是,这是一个记录在案的问题,将我的 Docker VM 内存增加到 12GB 就可以解决问题。

构建时无意初始化的类

Error: Classes that should be initialized at run time got initialized during image building:
 jdk.xml.internal.SecuritySupport was unintentionally initialized at build time. To see why jdk.xml.internal.SecuritySupport got initialized use --trace-class-initialization=jdk.xml.internal.SecuritySupport
javax.xml.parsers.FactoryFinder was unintentionally initialized at build time. To see why javax.xml.parsers.FactoryFinder got initialized use --trace-class-initialization=javax.xml.parsers.FactoryFinder

默认情况下,Spring Native 将类设置为在运行时初始化,除非在配置中在构建时明确声明。GraalVM 带来的许多好处是通过在构建时(而不是运行时)初始化合适的类来优化加载时间。Spring 做了很多这种 OOTB,识别可以在构建时初始化的类并设置此配置以供native-image使用。

spring-boot-aot-plugin做了很多自省并确定哪些类是构建时初始化的候选对象,然后生成 GraalVM 编译器使用的“native-image”属性文件,以了解哪些类在构建时初始化。如果您收到“应该在运行时初始化的类在图像构建期间被初始化”错误,那么很可能是因为在构建时被标记为初始化的类无意中初始化了另一个类,而该类没有被明确标记为在建造时间。

如果发生这种情况,您可以--trace-class-initialization在 pom 配置中使用该标志native-maven-plugin,然后重新运行构建:

<configuration>
                            <buildArgs>
                                --trace-class-initialization=jdk.xml.internal.SecuritySupport
                            </buildArgs>
                        </configuration>
                    </plugin>

然后这将输出导致类被初始化的调用堆栈。

使用 GraalVM 的快速 Spring Boot AWS Lambda 您还可以标记要在构建时初始化的其他类,方法是在下面创建一个native-image.properties文件,resources/META-INF/native-image其中包含您要初始化的以逗号分隔的类列表:

Args = --initialize-at-build-time=jdk.xml.internal.SecuritySupport

不幸的是,在这种情况下,一旦您开始发现越来越多需要在构建时初始化的类,它就会变得有点像兔子洞。最终,您会遇到一个试图生成一个在构建时不可能发生的线程。

在构建时启动的任何依赖于运行时处于活动状态的东西都不会那么好。GraalVM 内置了一些检查,可以检测各种情况并快速失败。一个例子是线程——如果一个类初始化由于某种原因产生了一个线程,GraalVM 会选择这些并通知你:

使用 GraalVM 的快速 Spring Boot AWS Lambda 在这种特殊情况下,幸运的是,通过阅读 spring-native 文档并找到这个开放的 GitHub 问题,我很快意识到这与使用logback.xml基于配置有关。删除此文件(这将需要移动到另一种配置日志的方式)解决了这个问题。

至此,我们已经构建了一个可以工作的二进制文件,太棒了!不幸的是,似乎很多问题往往会在运行时蔓延。

  • 运行时问题

这就是反馈循环变得如此之长的地方,因为您必须在问题出现之前构建映像(在我的笔记本电脑上花了 8 分钟才能完成)。

在这个实验中,我的测试循环涉及构建一个新的二进制文件,然后将其上传到 AWS,在我解决问题的过程中不断重复。理想情况下,我们会在本地对此进行测试,以显着加快反馈循环。我怀疑这很容易做到,但没有时间进一步探索,我可能会写一篇后续文章来展示如何做到这一点。

启动时出现 MalformedURLException

Caused by: java.net.MalformedURLException: Accessing an URL protocol that was not enabled. The URL protocol http is supported but not enabled by default. It must be enabled by adding the --enable-url-protocols=http option to the native-image command.

如何解决这个问题?在 native-image builder 中启用 http 支持:

 <plugin>
                        <groupId>org.graalvm.buildtools</groupId>
                        <artifactId>native-maven-plugin</artifactId>
                        <version>0.9.4</version>
                        ...
                        <configuration>
                            <buildArgs>
                                --enable-url-protocols=http
                            </buildArgs>
                        </configuration>
                    </plugin>

缺少反射配置

每当出现此消息时,这是因为该类是通过反射 API 引用的,您需要为所提及的类添加反射配置。

您可以使用配置类顶部的 Spring AOT @NativeHint 注释在代码中执行此操作(例如您的基本 @SpringBootApplication 类),或者您可以创建 native-image 工具读取的 reflect-config.json 文件。

有时这不太明显——Spring 试图在可能的地方给出建议,但它只能就它知道的内容提供建议。有许多错误并不能清楚地表明问题所在。

我遇到了这个错误的一些变体,但解决方案总是相同的 - 在中添加反射配置META-INF/native-image/reflect-config.json

[
  {
    "name": "org.springframework.context.annotation.ProfileCondition",
    "allDeclaredConstructors": true,
    "allPublicConstructors": true,
    "allDeclaredMethods": true,
    "allPublicMethods": true,
    "allDeclaredClasses": true,
    "allPublicClasses": true
  }
  ...
]

以上是一个相当粗略的反射配置 - 您可以更具体地说明要启用反射的内容。但是,执行上述操作会很快得到结果。

以下是我在移植到 GraalVM 期间遇到的所有错误,所有这些都需要为受影响的类添加上述配置。这些也是按照它们发生的顺序:

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.zaxxer.hikari.HikariDataSource]: Factory method 'dataSource' threw exception; nested exception is java.lang.IllegalStateException: Cannot load driver class: com.mysql.jdbc.Driver
Caused by: java.io.UncheckedIOException: com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
Caused by: java.lang.ClassCastException: com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent cannot be cast to byte[]
at org.springframework.cloud.function.adapter.aws.AWSLambdaUtils.generateOutput(AWSLambdaUtils.java:173) ~[na:na]

学习更多JAVA知识与技巧,关注与私信博主docs.qq.com/doc/DQ2Z0eE…

免费学习领取JAVA 课件,源码,安装包等资料

这个让我看了一会儿,因为它看起来不相关。我不记得我是否深入了解了这一点,但我相信这与泛型的使用、缺少反射信息APIGatewayProxyResponseEvent和类型擦除有关,导致事件byte[]在此过程的早期没有被 Jackson转换为应该。添加反射信息以APIGatewayProxyResponseEvent解决问题。

不支持的字符编码

github.com/oracle/graa…

Caused by: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLException: Unsupported character encoding 'CP1252'

结果是,在构建原生图像时,只有一部分字符编码嵌入到二进制文件中。如果你想要它们,你必须要求它们:

<plugin>
                        <groupId>org.graalvm.buildtools</groupId>
                        <artifactId>native-maven-plugin</artifactId>
                        <version>0.9.4</version>
                        ...
                        <configuration>
                            <buildArgs>
                                --enable-url-protocols=http
                                -H:+AddAllCharsets
                            </buildArgs>
                        </configuration>
                    </plugin>
```

###`

转载自:https://juejin.cn/post/7071499092655865864
评论
请登录