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 如何完成指南
要开始,请执行以下操作:
-
下载并解压缩源代码
或
-
克隆 Git 存储库
git clone https://github.com/grails-guides/grails-yourkit-profiling.git
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
将插入、删除、打印和导入功能路由到服务。
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 数据服务,它让实现服务层逻辑变得轻松。
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
- 用于存储学生的域对象
package demo
import groovy.transform.CompileStatic
@CompileStatic
class Student {
String name
BigDecimal grade
String toString() {
"${name}-Grade:$grade"
}
}
StudentService
- 服务,用于实现应用程序的操作
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
- 应用程序的用户界面,包含主页上的链接
<!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>
StudentServiceSpec
和 StudentServiceIntegrationSpec
- 用来验证服务功能的测试类(方便起见)
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
}
}
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
应用程序主页
3.1 安装 YourKit
访问 YourKit 页面并下载相应的安装包。YourKit 提供 15 天试用许可证。它也提供按席位和公司许可证的选项。请注意,YourKit 为学生提供了一种更便宜的许可证,同时还为开源项目提供免费许可证。
IDE 集成
安装 YourKit 配置文件后,可将工具集成到 IDE 中。请注意,本指南使用 IntelliJ IDEA。按照 IDE 集成 中的说明进行操作
3.2 开始分析
有两种方法可将分析器绑定到正在运行的应用程序。两种方法都很好
-
在应用程序运行后绑定
-
此类绑定依赖于应用程序的常规 Grails 启动,但有时此绑定可能无法立即对附加或其他分析函数使用。
-
使用命令
grails run-app
启动应用程序,或在 IDE 中使用运行配置Grails: initial
-
启动 YourKit 分析器,并在“监控本地应用程序”部分中,单击
应用程序
PID。 -
在应用程序启动过程的早期阶段启动绑定,以获得最优效果。
-
-
在启动时绑定
-
此类绑定导致响应时间更快,但需要一些启动配置。
-
使用主类
demo.Application
创建运行配置。务必将工作目录
设置为grails-yourkit-profiling\initial
,并将类路径模块的使用
设置为initial_main
。 -
在
VM 选项中:
将其设置为-Xmx512m -Xms512m
。此启动中的 VM 会忽略 build.gradle 条目。 -
从 IDE 中使用旋转图标启动应用程序。由于 Grails 3 依靠 Gradle 进行生命周期管理,因此在 IntelliJ 中,除非从
Application.groovy
启动应用程序,否则旋转图标才不会处于活动状态。
-
旋转图标
监控本地应用程序
3.3 插入学生
应用程序绑定到分析器后,便可以执行操作了。我们需要在应用程序中生成一些数据。
执行 插入学生
操作,此操作将生成 20,000 个具有随机姓名和成绩的学生条目。在这一步,我们便可以继续在以下部分中进行分析了。
4 低效删除
该应用程序具有低效的删除功能。在 删除学生
时,该应用程序会花费大量时间删除成绩低于 90 分的学生。让我们演示如何使用 YourKit 对此进行分析。
在分析器中进行分析
-
开始使用采样选项进行 CPU 分析 - 此选项可跟踪低开销的方法时间。
-
-
执行
删除学生
。请注意,删除功能花费了很长时间。注意控制台的日志输出重复显示删除 hql 语句。在删除过程中暂停约 2-3 分钟。 -
现在我们必须捕获性能快照。快照视图比 CPU 配置文件拥有更多详细信息。我们可以深入了解应用程序的性能统计。快照将捕获从启动分析器到发起快照那一刻的时间段的统计信息。一旦准备就绪,请打开它。
-
-
导航至 Java EE 统计信息→数据库。在此,我们可以看到过多的对数据库执行的删除语句,以及其执行时间。在 Reverse Call Tree 中,我们可以看到负责的应用程序方法调用:
demo.StudentService.deleteStudents()
。您可以右键单击并选择在 IDE 中打开,它将直接带您转到代码。 -
检查代码
deleteStudents()
的代码查找成绩低于 A 的所有学生。它随后处理循环遍历每个对象并发出删除语句。很明显,这是速度变慢的原因。
@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 数据服务
void deleteStudentsWithGradleLessThanA() {
studentDataService.deleteByGradleLessThan(A_GRADE)
}
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 次)来设置下一个方案,以设置方案。
在分析器中进行分析
-
开始对象分配记录 - 这用于内存和垃圾回收问题。
-
-
执行“打印学生”。观察打印功能需要很长时间。
-
现在我们必须捕获内存快照。快照将有更详细的视图,并让我们精确找出垃圾回收的问题区域。
-
-
导航至垃圾对象选项卡,然后导航到按对象计数分类的热点。查看方法区域并观察类
demo.StudentService.printStudents()
方法。它负责产生几十万个 GC’ed 对象。这是问题的根源。 -
-
我们还可以查看进一步说明问题的垃圾回收图表。导航至内存选项卡→内存和 GC 遥测,并查看垃圾回收图表。值得注意的是,在“打印学生”操作期间垃圾回收活动激增。
-
检查代码
printStudents()
的代码检索数据库中的所有学生。它随后采用一个字符串并重复将其添加到 result
字符串对象中。这将在每次迭代时导致新的字符串对象。
String htmlUnorderedListOfStudents() {
List<Student> students = studentDataService.findAll()
String result = '<ul>'
for (s in students) {
result += "<li>${s.toString()}</li>"
}
result += '</ul>'
result
}
提高代码质量
我们可以通过使用 StringBuffer
来改进代码。
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 个学生
并观察到该应用程序有响应并且成功导入了这些记录。
导入负载的增加将导致内存不足异常。在我们遇到此情况时,我们必须开始分析。
在分析器中进行分析
-
开始对象分配记录。
-
-
转到“内存”选项卡→“内存和 GC 遥测”。在这里,您可以在实时中查看堆的活动图表。
-
-
执行
导入 75k 个学生
。请注意,处理会持续几分钟,然后会发生内存不足异常。发生错误时,YourKit 会自动生成一个.hprof
内存转储文件。这是标准 JVM 分析格式。加载该文件并单击类
,然后查看第一个对象Xobj$AttrXobj
。这个类的实例超出了当前 JVM 堆的大小。 -
-
现在让我们
捕获内存快照
。快照将具有更详细的视图,让我们能够找出内存使用问题的区域。 -
-
转到
按站点分配
,然后选择按对象计数排序的热点
,单击方法区(第 1 项),然后单击右下角的反向跟踪
选项卡。展开跟踪以查看demo.StudentService.importStudents(String)
。此方法是生成对象的地方。 -
检查代码
importStudents()
的代码调用 POI API 加载WorkbookFactory.
正是在此调用中,JVM 堆内存大幅增加。Xobj$AttrXobj
的对象实例,以及 chars
和 Strings
会随着 xlsx
文件的大小成比例地增加。因此,JVM 堆会超标。
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>
}
增加内存
这类内存问题很难追踪,但幸运的是,解决方法很简单。合理地增加内存将解决对象分配问题。不需要更改代码。调整下面的文件
7 结论
性能问题很难找出、排除故障并最终解决。根据我的经验,瓶颈通常出现在大型数据集处理或算法问题中。工具只能帮助识别问题,不能为您解释数据。通过系统地分解问题空间、分析代码和使用功能强大的工具集,我们可以更顺畅地找到解决方案。
7.1 YourKit 的最佳功能
总之,以下是 YourKit 最具吸引力的品质
-
易于安装,并与您的 IDE 集成
-
包含方法执行时间和次数的分析
-
可以在工具中设置与触发器关联的条件
-
本地或远程监控
-
管理数据的导出和转储
-
与传统工具(jvisualvm、JConsole javamelody)相比,这些特性更易于用户使用且更复杂