显示导航

Grails YourKit 剖析

在本指南中,你将学习如何使用 YourKit Java Profiler 工具对 Grails 应用程序中的内存和 CPU 进行剖析。

作者:尼拉夫·阿萨尔

Grails 版本 4.0.1

1 Grails 培训

Grails 培训 - 由创建和活跃维护 Grails 框架的人员开发和交付!

2 入门

在本指南中,你将学习如何对 Grails 应用程序的内存和 CPU 进行剖析。已经为你创建了一个具有预设情形的应用程序,这些情形会触发内存和 CPU 参数。我们会检查这些情形及关联代码以剖析内存、CPU、堆和垃圾回收。然后我们会继续优化代码,看看它对性能有何影响。

请注意,本指南的重点是用分析器进行分析,不一定是 Grails 开发。因此,性能问题是基础的、显而易见的,代码解决方案也是如此。代码解决方案已经为你创建,可在遵循指南时注入。

2.1 你需要准备的

要完成本指南,你需要以下内容:

  • 一定的时间

  • 一个像样的文本编辑器或 IDE

  • 安装了 JDK 1.8 或更高版本,并且适当地配置了 JAVA_HOME

2.2 如何完成指南

要开始,请执行以下操作:

Grails 指南存储库包含两个文件夹

  • initial 初始项目。通常是简单的 Grails 应用程序,其中含有一些提供的附加代码,让你可以抢先一步。

  • complete 已完成示例。通过完成指南中介绍的步骤,并将这些更改应用于 initial 文件夹,就会生成该示例。

要完成指南,请转至 initial 文件夹

  • grails-guides/grails-yourkit-profiling/initial 中执行 cd

然后按照后续部分中的说明进行操作。

如果你在 grails-guides/grails-yourkit-profiling/complete 中执行 cd,则可以直接转到已完成示例

3 评估应用程序

该指南随内置的为你创建的简单应用程序一起提供。该应用程序的目的是准备好触发性能问题的代码的人为示例,并让该工具显示内存、CPU 和堆问题。

该应用程序的标题为 Grails YourKit Profile,且有一个简单的域对象 Student。该应用程序有几个触发操作的链接,旨在引发性能问题。总之,该应用程序能够

  • 在数据库中插入大量学生(使用随机名称和成绩)

  • 删除成绩低于 A 的学生

  • 将学生打印到屏幕

  • 导入学生条目的电子表格

浏览代码

请花点时间浏览代码,并熟悉该应用程序的重要类。

StudentController 将插入、删除、打印和导入功能路由到服务。

grails-app/controllers/demo/StudentController.groovy
package demo


import groovy.transform.CompileStatic
import org.springframework.context.MessageSource

@CompileStatic
class StudentController {

    static final int LARGE_NUMBER = 20000
    static scaffold = Student
    MessageSource messageSource

    StudentService studentService
    StudentDataService studentDataService

    def insert() {
        studentService.insertStudents(LARGE_NUMBER)
        render studentCountMessage()
    }

    def delete() {
        studentService.deleteStudentsWithGradleLessThanA()
        render studentCountMessage()
    }

    def print() {
        render studentService.htmlUnorderedListOfStudents()
    }

    def import25kStudents() {
        studentService.saveExcelStudents("src/resources/studentImport-25krows.xlsx")
        render studentCountMessage()
    }

    def import75kStudents() {
        studentService.saveExcelStudents("src/resources/studentImport-75krows.xlsx")
        render studentCountMessage()
    }

    protected String studentCountMessage() {
        int count = studentDataService.count()
        String defaultMsg = "Student Count: ${count}"
        messageSource.getMessage('student.count', [count] as Object[], defaultMsg, request.locale)
    }
}

StudentDataService:一个 GORM 数据服务,它让实现服务层逻辑变得轻松。

grails-app/services/demo/StudentDataService.groovy
package demo

import grails.gorm.services.Service
import groovy.transform.CompileStatic

@CompileStatic
@Service(Student)
interface StudentDataService {
    List<Student> findAll()
    List<Student> findByGradeLessThan(BigDecimal grade)
    int count()
    Student save(String name, BigDecimal grade)
}

Student - 用于存储学生的域对象

grails-app/domain/demo/Student.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class Student {
    String name
    BigDecimal grade

    String toString() {
        "${name}-Grade:$grade"
    }
}

StudentService - 服务,用于实现应用程序的操作

grails-app/services/demo/StudentService.groovy
package demo

import groovy.util.logging.Slf4j
import org.apache.poi.ss.usermodel.Workbook
import org.apache.poi.ss.usermodel.WorkbookFactory
import org.grails.plugins.excelimport.ExcelImportService
import groovy.transform.CompileStatic
import grails.gorm.transactions.Transactional

@Slf4j
@CompileStatic
class StudentService {

    StudentDataService studentDataService

    static final int boundary = 100
    static final BigDecimal A_GRADE = 90
    static final Map CONFIG_STUDENT_COLUMN_MAP = [
            sheet: 'Sheet1',
            startRow: 1,
            columnMap: [
                    'A': 'name',
                    'B': 'grade'
            ]
    ]

    Random random = new Random()

    @Transactional
    void insertStudents(int numberOfStudents) {
        numberOfStudents.times {
            BigDecimal grade = random.nextInt(boundary) as BigDecimal
            studentDataService.save(produceRandomName(), grade)
        }
    }

    //tag::deleteStudents[]
    @Transactional
    void deleteStudentsWithGradleLessThanA() {
        List<Student> students = studentDataService.findByGradeLessThan(A_GRADE)
        log.info '#{} students with less than A', students.size()
        for (Student s in students) {
            s.delete(flush: true)
        }
    }
    //end::deleteStudents[]

    //tag::htmlUnorderedListOfStudents[]
    String htmlUnorderedListOfStudents() {
        List<Student> students = studentDataService.findAll()
        String result = '<ul>'
        for (s in students) {
            result += "<li>${s.toString()}</li>"
        }
        result += '</ul>'
        result
    }
    //end::htmlUnorderedListOfStudents[]

    @Transactional
    void saveExcelStudents(String fileName) {
        List<Map> studentData = importStudents(fileName)
        for (Map s in studentData) {
            studentDataService.save(s.name as String, s.grade as BigDecimal)
        }
    }

    //tag::importStudents[]
    protected List<Map> importStudents(String fileName) {
        Workbook workbook = WorkbookFactory.create(new File(fileName))
        ExcelImportService excelImportService = new ExcelImportService()
        excelImportService.convertColumnMapConfigManyRows(workbook, CONFIG_STUDENT_COLUMN_MAP) as List<Map>
    }
    //end::importStudents[]

    protected String produceRandomName() {
        "Name${random.nextInt(2*boundary)}"
    }
}

index.gsp - 应用程序的用户界面,包含主页上的链接

grails-app/views/index.gsp
<!doctype html>
<html>
<head>
    <title>Grails Yourkit Profile</title>
    <meta name="layout" content="main"/>
</head>
<body>
<content tag="nav">
    <li class="dropdown">
        <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Application Status <span class="caret"></span></a>
        <ul class="dropdown-menu">
            <li><a href="#">Environment: ${grails.util.Environment.current.name}</a></li>
            <li><a href="#">App profile: ${grailsApplication.config.grails?.profile}</a></li>
            <li><a href="#">App version:
                <g:meta name="info.app.version"/></a>
            </li>
            <li role="separator" class="divider"></li>
            <li><a href="#">Grails version:
                <g:meta name="info.app.grailsVersion"/></a>
            </li>
            <li><a href="#">Groovy version: ${GroovySystem.getVersion()}</a></li>
            <li><a href="#">JVM version: ${System.getProperty('java.version')}</a></li>
            <li role="separator" class="divider"></li>
            <li><a href="#">Reloading active: ${grails.util.Environment.reloadingAgentEnabled}</a></li>
        </ul>
    </li>
    <li class="dropdown">
        <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Artefacts <span class="caret"></span></a>
        <ul class="dropdown-menu">
            <li><a href="#">Controllers: ${grailsApplication.controllerClasses.size()}</a></li>
            <li><a href="#">Domains: ${grailsApplication.domainClasses.size()}</a></li>
            <li><a href="#">Services: ${grailsApplication.serviceClasses.size()}</a></li>
            <li><a href="#">Tag Libraries: ${grailsApplication.tagLibClasses.size()}</a></li>
        </ul>
    </li>
    <li class="dropdown">
        <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Installed Plugins <span class="caret"></span></a>
        <ul class="dropdown-menu">
            <g:each var="plugin" in="${applicationContext.getBean('pluginManager').allPlugins}">
                <li><a href="#">${plugin.name} - ${plugin.version}</a></li>
            </g:each>
        </ul>
    </li>
</content>

    <div id="content" role="main">
        <section class="row colset-2-its">
            <h1>Grails Yourkit Profile</h1>

            <div id="controllers" role="navigation">
                <h2>Available Controllers:</h2>
                <ul>
                    <g:each var="c" in="${grailsApplication.controllerClasses.sort { it.fullName } }">
                        <li class="controller">
                            <g:link controller="${c.logicalPropertyName}">${c.name}</g:link>
                        </li>
                    </g:each>
                </ul>

                <h2>Grails YourKit Profiling Operations</h2>
                <table id="profileOperations">
                    <tr>
                        <td><a href="${createLink (controller:"student", action: "insert")}">Insert Students</a>
                     </tr>
                    <tr>
                        <td><a href="${createLink (controller:"student", action: "delete")}">Delete Students</a>
                    </tr>
                    <tr>
                        <td><a href="${createLink (controller:"student", action: "print")}">Print Students</a>
                    </tr>
                    <tr>
                        <td><a href="${createLink (controller:"student", action: "import25kStudents")}">Import 25k Students</a>
                    </tr>
                    <tr>
                        <td><a href="${createLink (controller:"student", action: "import75kStudents")}">Import 75k Students</a>
                    </tr>
                </table>
            </div>
        </section>
    </div>

</body>
</html>

StudentServiceSpecStudentServiceIntegrationSpec - 用来验证服务功能的测试类(方便起见)

src/test/groovy/demo/StudentServiceSpec.groovy
package demo

import grails.testing.services.ServiceUnitTest
import spock.lang.Specification

class StudentServiceSpec extends Specification implements ServiceUnitTest<StudentService> {

    def 'test produceRandomName'() {
        when:
        String name = service.produceRandomName()

        then:
        !name.isEmpty()
    }

    def 'test importStudents'() {
        when:
        List<Map> studentData = service.importStudents("src/test/resources/studentImport-3rows.xlsx")

        then:
        studentData.size() == 3
    }
}
src/integration-test/groovy/demo/StudentServiceIntegrationSpec.groovy
package demo

import grails.gorm.transactions.Rollback
import grails.testing.mixin.integration.Integration
import spock.lang.Specification

@Rollback
@Integration
class StudentServiceIntegrationSpec extends Specification {

    StudentService studentService

    def 'test insertStudents'() {
        when:
        studentService.insertStudents(100)

        then:
        Student.count() == old(Student.count()) + 100
    }

    def 'test deleteStudents'() {
        when:
        studentService.insertStudents(100)
        studentService.deleteStudentsWithGradleLessThanA()

        then:
        Student.count() < (old(Student.count()) + 100)
    }

    def 'test htmlUnorderedListOfStudents'() {
        when:
        studentService.insertStudents(100)
        String results = studentService.htmlUnorderedListOfStudents()

        then:
        !results.isEmpty()
        results.count('<ul>') == 1
        results.count('<li>') == 100
        results.count('</li>') == 100
        results.count('</ul>') == 1
    }

    def 'test saveExcelStudents'() {
        when:
        studentService.saveExcelStudents("src/integration-test/resources/studentImport-3rows.xlsx")

        then:
        Student.count() == old(Student.count()) + 3
    }

}

运行应用程序

使用以下命令运行该应用程序。一开始触发一些功能,以熟悉。稍后,我们将在分析器上下文中运行该操作。

$ ./gradlew bootRun
应用程序主页

792

3.1 安装 YourKit

访问 YourKit 页面并下载相应的安装包。YourKit 提供 15 天试用许可证。它也提供按席位和公司许可证的选项。请注意,YourKit 为学生提供了一种更便宜的许可证,同时还为开源项目提供免费许可证。

IDE 集成

安装 YourKit 配置文件后,可将工具集成到 IDE 中。请注意,本指南使用 IntelliJ IDEA。按照 IDE 集成 中的说明进行操作

3.2 开始分析

有两种方法可将分析器绑定到正在运行的应用程序。两种方法都很好

  1. 在应用程序运行后绑定

    • 此类绑定依赖于应用程序的常规 Grails 启动,但有时此绑定可能无法立即对附加或其他分析函数使用。

    • 使用命令 grails run-app 启动应用程序,或在 IDE 中使用运行配置 Grails: initial

    • 启动 YourKit 分析器,并在“监控本地应用程序”部分中,单击 应用程序PID。

    • 在应用程序启动过程的早期阶段启动绑定,以获得最优效果。

  2. 在启动时绑定

    • 此类绑定导致响应时间更快,但需要一些启动配置。

    • 使用主类 demo.Application 创建运行配置。务必将 工作目录 设置为 grails-yourkit-profiling\initial,并将 类路径模块的使用 设置为 initial_main

    • VM 选项中:将其设置为 -Xmx512m -Xms512m。此启动中的 VM 会忽略 build.gradle 条目。

    • 从 IDE 中使用旋转图标启动应用程序。由于 Grails 3 依靠 Gradle 进行生命周期管理,因此在 IntelliJ 中,除非从 Application.groovy 启动应用程序,否则旋转图标才不会处于活动状态。

旋转图标
20
监控本地应用程序
500

3.3 插入学生

应用程序绑定到分析器后,便可以执行操作了。我们需要在应用程序中生成一些数据。

执行 插入学生 操作,此操作将生成 20,000 个具有随机姓名和成绩的学生条目。在这一步,我们便可以继续在以下部分中进行分析了。

4 低效删除

该应用程序具有低效的删除功能。在 删除学生 时,该应用程序会花费大量时间删除成绩低于 90 分的学生。让我们演示如何使用 YourKit 对此进行分析。

在分析器中进行分析

  1. 开始使用采样选项进行 CPU 分析 - 此选项可跟踪低开销的方法时间。

    1. 700

  2. 执行 删除学生。请注意,删除功能花费了很长时间。注意控制台的日志输出重复显示删除 hql 语句。在删除过程中暂停约 2-3 分钟。

  3. 现在我们必须捕获性能快照。快照视图比 CPU 配置文件拥有更多详细信息。我们可以深入了解应用程序的性能统计。快照将捕获从启动分析器到发起快照那一刻的时间段的统计信息。一旦准备就绪,请打开它。

    1. 700

  4. 导航至 Java EE 统计信息→数据库。在此,我们可以看到过多的对数据库执行的删除语句,以及其执行时间。在 Reverse Call Tree 中,我们可以看到负责的应用程序方法调用:demo.StudentService.deleteStudents()。您可以右键单击并选择在 IDE 中打开,它将直接带您转到代码。

    1. 700

检查代码

deleteStudents() 的代码查找成绩低于 A 的所有学生。它随后处理循环遍历每个对象并发出删除语句。很明显,这是速度变慢的原因。

grails-app/services/demo/StudentService.groovy
    @Transactional
    void deleteStudentsWithGradleLessThanA() {
        List<Student> students = studentDataService.findByGradeLessThan(A_GRADE)
        log.info '#{} students with less than A', students.size()
        for (Student s in students) {
            s.delete(flush: true)
        }
    }

提高代码质量

仅仅是使用 where 子句发出一个删除 SQL 查询效率更高。

我们将使用带有使用 JPA-QL 的删除操作的 GORM 数据服务

grails-app/services/demo/StudentService.groovy
    void deleteStudentsWithGradleLessThanA() {
        studentDataService.deleteByGradleLessThan(A_GRADE)
    }
grails-app/services/demo/StudentDataService.groovy
import grails.gorm.services.Query
import grails.gorm.services.Service
import groovy.transform.CompileStatic

@CompileStatic
@Service(Student)
interface StudentDataService {
  ...
    @Query("delete ${Student student} where $student.grade <= $grade")
    void deleteByGradleLessThan(BigDecimal grade)
}
...
}

既然代码已经得到改进,我们应该使用分析器重新启动应用程序。再次重复“插入学生”。重复“在分析器中分析”中的步骤,并观察数据库 delete 调用减少到 1。

5 过量的垃圾回收

应用程序以非常低效的方式将所有学生打印到屏幕上。在“打印学生”时,应用程序将创建过多的对象。这导致 JVM 过度补偿垃圾回收。

通过使用分析功能重新启动应用程序并执行“插入学生”(2 次)来设置下一个方案,以设置方案。

在分析器中进行分析

  1. 开始对象分配记录 - 这用于内存和垃圾回收问题。

    1. 700

  2. 执行“打印学生”。观察打印功能需要很长时间。

  3. 现在我们必须捕获内存快照。快照将有更详细的视图,并让我们精确找出垃圾回收的问题区域。

    1. 700

  4. 导航至垃圾对象选项卡,然后导航到按对象计数分类的热点。查看方法区域并观察类 demo.StudentService.printStudents() 方法。它负责产生几十万个 GC’ed 对象。这是问题的根源。

    1. 800

  5. 我们还可以查看进一步说明问题的垃圾回收图表。导航至内存选项卡→内存和 GC 遥测,并查看垃圾回收图表。值得注意的是,在“打印学生”操作期间垃圾回收活动激增。

    1. 800

检查代码

printStudents() 的代码检索数据库中的所有学生。它随后采用一个字符串并重复将其添加到 result 字符串对象中。这将在每次迭代时导致新的字符串对象。

grails-app/services/demo/StudentService.groovy
    String htmlUnorderedListOfStudents() {
        List<Student> students = studentDataService.findAll()
        String result = '<ul>'
        for (s in students) {
            result += "<li>${s.toString()}</li>"
        }
        result += '</ul>'
        result
    }

提高代码质量

我们可以通过使用 StringBuffer 来改进代码。

grails-app/services/demo/StudentService.groovy
    String htmlUnorderedListOfStudents() {
        List<Student> students = studentDataService.findAll()
        StringBuffer result = new StringBuffer()
        result.append('<ul>')
        for (s in students) {
            result.append('<li>')
            result.append(s.toString())
            result.append('</li>')
        }
        result.append('</ul>')
        result.toString()
    }

现在已进行代码改进,我们应该使用分析器重新启动应用程序。再次重复插入学生。重复Analyzer in Profiler中的步骤。观察垃圾收集统计信息大幅下降。

6 堆内存过多

应用程序 JVM 内存设置为512m。这对于适量的 xls 导入而言足够了,但随着负载的增加,它会失败。我们首先尝试导入适量的学生。单击导入 25k 个学生并观察到该应用程序有响应并且成功导入了这些记录。

导入负载的增加将导致内存不足异常。在我们遇到此情况时,我们必须开始分析。

在分析器中进行分析

  1. 开始对象分配记录。

    1. 700

  2. 转到“内存”选项卡→“内存和 GC 遥测”。在这里,您可以在实时中查看堆的活动图表。

    1. 700

  3. 执行导入 75k 个学生。请注意,处理会持续几分钟,然后会发生内存不足异常。发生错误时,YourKit 会自动生成一个.hprof内存转储文件。这是标准 JVM 分析格式。加载该文件并单击,然后查看第一个对象Xobj$AttrXobj。这个类的实例超出了当前 JVM 堆的大小。

    1. 700

  4. 现在让我们 捕获内存快照。快照将具有更详细的视图,让我们能够找出内存使用问题的区域。

    1. 700

  5. 转到按站点分配,然后选择按对象计数排序的热点,单击方法区(第 1 项),然后单击右下角的反向跟踪选项卡。展开跟踪以查看demo.StudentService.importStudents(String)。此方法是生成对象的地方。

    1. 700

检查代码

importStudents() 的代码调用 POI API 加载WorkbookFactory. 正是在此调用中,JVM 堆内存大幅增加。Xobj$AttrXobj 的对象实例,以及 charsStrings 会随着 xlsx 文件的大小成比例地增加。因此,JVM 堆会超标。

grails-app/services/demo/StudentService.groovy
    protected List<Map> importStudents(String fileName) {
        Workbook workbook = WorkbookFactory.create(new File(fileName))
        ExcelImportService excelImportService = new ExcelImportService()
        excelImportService.convertColumnMapConfigManyRows(workbook, CONFIG_STUDENT_COLUMN_MAP) as List<Map>
    }

增加内存

这类内存问题很难追踪,但幸运的是,解决方法很简单。合理地增加内存将解决对象分配问题。不需要更改代码。调整下面的文件

build.gradle

7 结论

性能问题很难找出、排除故障并最终解决。根据我的经验,瓶颈通常出现在大型数据集处理或算法问题中。工具只能帮助识别问题,不能为您解释数据。通过系统地分解问题空间、分析代码和使用功能强大的工具集,我们可以更顺畅地找到解决方案。

7.1 YourKit 的最佳功能

总之,以下是 YourKit 最具吸引力的品质

  • 易于安装,并与您的 IDE 集成

  • 包含方法执行时间和次数的分析

  • 可以在工具中设置与触发器关联的条件

  • 本地或远程监控

  • 管理数据的导出和转储

  • 与传统工具(jvisualvm、JConsole javamelody)相比,这些特性更易于用户使用且更复杂

8 Grails 帮助

Object Computing, Inc. (OCI) 赞助了本指南的创建。提供各种咨询和支持服务。

OCI 是 Grails 的家

认识团队