慢速攻击漏洞修复
在构建Web应用程序时,经常会进行大量优化以加快信息往返用户的速度。 加快此信息流的可能方式与Web应用程序本身一样多种多样。 在本文中,我们将重点介绍如何优化数据模型以适合该数据模型所针对的特定用例。
这些优化总是特定于用例的,这已不是什么秘密。 它们对预期的用例有积极影响,但对其他用例也有不利影响(在索引的情况下,这会增加插入/更新工作量)。 评估哪些用例会造成最大的瓶颈,并了解在生产环境中如何使用数据模型,这对于进行正确的权衡以获得所需的性能至关重要。
在这篇文章中,我将简要说明我通常用来确定关键瓶颈以及如何优化它们的步骤。
用例
为了展示我所经历的一些步骤,我将使用一个简单的演示应用程序作为参考。 我将使用一个简单的Spring引导应用程序,该应用程序使用JPA / Hibernate连接到MySQL数据库。 为了快速呈现一些HTML,我将使用Thymeleaf模板。 我们还将添加一些Lombok注释以节省时间。
让我们以一个非常基本的用例为例,其中有一个User实体。
@Getter @Setter @Entity @Builder @NoArgsConstructor @AllArgsConstructor @Table (name = "users" ) public class User {
@Id
@GeneratedValue
private Long id;
@Length (max = 64 )
@Column (name = "display_name" )
private String displayName;
@Length (max = 16 )
@Column (name = "first_name" )
private String firstName;
@Length (max = 16 )
@Column (name = "last_name" )
private String lastName;
@Length (max = 64 )
@Column (name = "email" )
private String email; }
使用带有两个分页查询方法的简单JPA存储库。
public interface UserRepository extends JpaRepository<User, Long> {
Page<User> findAllByDisplayName(String displayName, Pageable pageable);
Page<User> findAllByFirstNameOrLastName(String firstName, String lastName, Pageable pageable); }
我们为用户实体创建一个简单的Crud服务。
public interface UserService {
Page<User> listAllUsers(Pageable pageable);
Page<User> listUsersWithDisplayName(String displayName, Pageable pageable);
Page<User> listUsersWithFirstNameOrLastName(String name, Pageable pageable); } @Service @RequiredArgsConstructor public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Override
public Page<User> listAllUsers(Pageable pageable) {
return userRepository.findAll(pageable);
}
@Override
public Page<User> listUsersWithDisplayName(String displayName, Pageable pageable) {
return userRepository.findAllByDisplayName(displayName, pageable);
}
@Override
public Page<User> listUsersWithFirstNameOrLastName(String name, Pageable pageable) {
return userRepository.findAllByFirstNameOrLastName(name, name, pageable);
} }
我们还将创建一个在安装应用程序后运行的安装程序。 对于我们的示例,此安装程序将随机生成10000个用户。
@Component @RequiredArgsConstructor public class DummyUserInstaller {
public static final String[] FIRST_NAMES = {
"James" ,
"David" ,
"Christopher" ,
"George" ,
"Ronald" ,
"John" ,
"Richard"
};
public static final String[] LAST_NAMES = {
"Smith" ,
"Johnson" ,
"Williams" ,
"Jones" ,
"Brown" ,
"Davis" ,
"Miller" ,
"Wilson"
};
public static final String[] EMAIL_PROVIDERS = {
"gmail.com" ,
"hotmail.com" ,
"outlook.com" ,
"yahoo.com"
};
public static final int RANDOM_USER_COUNT = 10000 ;
private final UserRepository userRepository;
@PostConstruct
public void installUsers() {
userRepository.deleteAll();
Random random = new Random();
for ( int i = 0 ; i < RANDOM_USER_COUNT; i++) {
userRepository.save(createRandomUser(random));
}
}
private User createRandomUser(Random random) {
String randomFirstName = FIRST_NAMES[random.nextInt(FIRST_NAMES.length)];
String randomLastName = LAST_NAMES[random.nextInt(LAST_NAMES.length)];
return User.builder()
.displayName(String.format( "%s %s" , randomFirstName, randomLastName))
.firstName(randomFirstName)
.lastName(randomLastName)
.email(String.format( "%s_%s%d@%s" , randomFirstName, randomLastName,
random.nextInt( 100 ), EMAIL_PROVIDERS[random.nextInt(EMAIL_PROVIDERS.length)]))
.build();
} }
并且在控制器中,我们将注册一个端点,以获得简单的用户搜索功能。 该端点具有三个可选的请求参数:页面和大小(用于实现分页),以及一个能够在显示名称上进行搜索的参数。 如果未给出显示名称请求参数,则将返回所有结果。
@Controller @RequiredArgsConstructor public class UserController {
public static final int DEFAULT_PAGE = 1 ;
public static final int DEFAULT_PAGE_SIZE = 20 ;
private final UserService userService;
@GetMapping ( "/" )
public String overview( @RequestParam (name = "displayName" , required = false ) String displayName,
@RequestParam (name = "page" , required = false ) Integer page,
@RequestParam (name = "size" , required = false ) Integer size,
Model model
) {
if (displayName == null ) {
model.addAttribute( "users" , userService.listAllUsers(toPageRequest(page, size)));
} else {
model.addAttribute( "users" , userService.listUsersWithDisplayName(displayName, toPageRequest(page, size)));
}
return "user-overview" ;
}
private PageRequest toPageRequest(Integer page, Integer size) {
if (page == null ) {
page = DEFAULT_PAGE;
}
if (size == null ) {
size = DEFAULT_PAGE_SIZE;
}
return PageRequest.of(page, size);
} }
我们还创建了一个简单的Thymeleaf模板来显示我们的用户。
<!DOCTYPE html> <html xmlns:th= " http://www.w3.org/1999/xhtml " lang= "en" > <head>
<meta charset= "UTF-8" >
<title>Use case </title> </head> <body> <form action= "/" method= "GET" >
<label for = "displayName" >Display name : </label>
<input type= "text" id= "displayName" name= "displayName" name= "displayName" name= "displayName" >
<button type= "submit" >Search</button> </form> <table>
<thead>
<tr>
<th>Display name</th>
<th>First name</th>
<th>Last name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<tr th:each= "user : ${users}" >
<td th:text= "${user.displayName}" ></td>
<td th:text= "${user.firstName}" ></td>
<td th:text= "${user.lastName}" ></td>
<td th:text= "${user.email}" ></td>
</tr>
</tbody> </table> </body> </html>
如果我们运行此简单应用程序并在Web浏览器中打开http://localhost:8080
并查询随机用户之一,例如。 “詹姆斯·威廉姆斯”,我们可以看到页面加载时间介于100到200毫秒之间。 在添加了性能改进后,我们将把此页面的加载时间与加载时间进行比较,以查看改进后的实际效果。
如何识别他们
数据模型中瓶颈的一个很好指标是查询,查询执行得非常频繁,而且似乎比必要的查询慢。 有许多工具和方法可用于识别“慢查询”。 我将简要介绍两种识别慢速查询的方法:使用JetProfiler和使用说明计划。
像JetProfiler这样的工具可以通过测量数据库的实时负载来让您了解哪些查询的性能不佳。 如下面的屏幕快照所示,从对数据库执行的查询中抽取了一个样本,并根据检索到的查询对数据库的影响程度为其提供了评级。
JetProfiler还提供了有关查询为什么对数据库有影响的解释。 下面的屏幕快照显示了一个示例。
了解查询对数据库的影响程度的另一种方法是检索给定查询的解释计划。 这将为您提供一些信息,说明数据库必须执行哪种搜索操作才能返回给定查询的正确结果。 这样获得一个解释计划:
explain select * from users where display_name = 'James Wilson' explain select * from users where display_name =
返回的结果包含以下属性:
- 选择类型 :用于执行此查询的选择类型
- 表格 :用于获取信息的表格
- 类型 :使用的联接类型
- 可能的关键字 :执行此查询时可以选择的所有索引
- 关键字 :执行此查询时选择使用的索引
- 密钥长度 :所选密钥的长度
- 参考 :哪些列或常量用于与“键”返回值中给定的所选索引进行比较
- 行数 :执行查询时为了返回结果而要查看的行数(越低越好)
- 额外 :有关如何执行给定查询的额外信息。
JetProfiler解释的结果和explain语句都得出相同的结论:使用特定的显示名称来获取用户要求数据库查看所有插入的记录,以确定哪些记录符合查询的要求。 这不是执行查询的非常有效的方法,如果我们的系统中有数以百万计的用户,尤其是如果此查询是由许多用户执行的,则检查所有用户以查看其中一个用户是否匹配给定的字符串将成为问题。同时。
如何添加它们
现在,我们在应用程序中发现了一个缓慢的查询,我们可以尝试针对预期的用例优化此查询。 为了优化此查询,我们将在用户表上为display_name字段添加一个简单索引。 我们可以通过几种不同的方式添加索引:
–使用如下所示的创建索引语句:
Create index ix_display_name on users(display_name)
–或者将JPA / Hibernate与一些Java持久性注释一起使用。 通过在表批注中填充索引属性,我们可以通过其名称和需要索引的列的列表来创建索引。
@Table (name = "users" , indexes = @Index , indexes = (name = "ix_display_name" , columnList = "display_name" ))
当在用户表上为显示名称列添加索引时,可以看到索引正在使用中。 我们可以看到,使用同一查询检查的行数急剧下降。
当我们再次记录给定查询时,JetProfiler中会显示类似的结果:
现在,如果我们再次在浏览器中加载页面并搜索相同的随机用户,则可以看到页面加载速度现在介于30到50毫秒之间,这是一个很大的进步。
结论
因此,这是识别应用程序中慢速查询的两种不同方法:使用JetProfiler之类的工具快速查找应用程序中最慢的查询,或者使用解释计划找出特定查询对数据库的影响。 我还展示并测试了一个相当简单的解决方案,通过添加用例特定的索引来提高此特定查询的性能,但这显然非常取决于该情况。
翻译自: https://www.javacodegeeks.com/2019/09/improve-database-speeds-fixing-slow-queries.html
慢速攻击漏洞修复