36 性能:百万级TPS系统的性能优化之道
你好,我是康杨。今天我们来聊聊性能优化。
性能优化是一个既涉及技术又涉及业务的复杂领域。它不仅关乎用户体验和系统的稳定性,也涉及团队的成本。当技术储备、业务需求和团队能力等复杂元素交织在一起的时候,性能优化更是显得复杂和深奥。而且我们需要关注多个维度,比如CPU、内存、磁盘和网络等,任何一个环节都可能成为我们的性能瓶颈。
所以这节课我就来从代码层面为你详细解析如何进行Java应用的性能优化。当然,性能优化不仅需要对Java应用的代码进行优化,还需要对底层的操作系统、硬件和网络进行优化,不过后者更受限于应用程序的具体需求和环境,所以今天我们重点还是从代码优化的层面来探讨。
代码优化
首先,我们需要仔细审查应用程序的代码。优化代码的基本准则是: 尽量减少对象的创建,尽量减少方法的调用,以及尽量避免使用昂贵的函数。 对于Java应用程序来说,大部分性能问题都源于“创建过多不必要的对象”和“过度使用映射和循环”。因此,我们可以从以下几个方面对代码进行优化。
减少对象的创建
我们通过一个简单的字符串拼接的例子,来看一下如何减少对象的创建,你先看优化前的代码,每次“+”操作都会创建一个新的String对象。
public String concatStr(String... strArray){
String result = "";
for(String str : strArray){
result += str;
}
return result;
}
再来看优化后的示例代码。
public String concatStr(String... strArray){
StringBuilder sb = new StringBuilder();
for(String str : strArray){
sb.append(str);
}
return sb.toString();
}
在优化后的代码示例里,我们使用StringBuilder,避免创建大量不必要的临时对象,从而减少了垃圾回收的压力。此外,我们还可以通过使用对象池、重用对象、延迟初始化、静态方法、泛型等方式来减少对象创建。
避免不必要对象的创建
避免在循环中进行不必要的对象创建。例如,下面的代码示例里每次循环都会创建一个新的 SimpleDateFormat 对象。
SimpleDateFormat 是一个线程不安全的类,每个线程都需要创建自己的 SimpleDateFormat 对象,为了保证线程安全,需要每次使用完后手动释放资源,否则会导致资源泄露和内存溢出。而SimpleDateFormat 的对象创建和销毁是比较耗时的操作,会消耗大量的CPU和内存,从而导致程序运行缓慢,对性能的影响较大。
你可以看一下优化前的代码。
for (int i = 0; i < 1000000; i++) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = sdf.format(new Date());
}
再对比优化后的代码,现在只需要在循环外创建一次 SimpleDateFormat 对象,然后在循环中复用这个对象就可以了。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (int i = 0; i < 1000000; i++) {
String dateStr = sdf.format(new Date());
}
减少方法的调用
如果一个Java方法中频繁地调用另一个Java方法,并且每次调用后的效果是一样的,那么可以尽量减少对这个方法的调用。
你可以对比一下优化前和优化后的代码。
优化前;
public void callMethod1(){
for(int i=0;i<10000;i++){
method1();
}
}
public void method1(){
// 业务逻辑
}
优化后:
我们看到,在第一个例子里,调用了10000次method1(),扩大了方法调用栈,占用更多的内存空间,反而降低了程序的性能。第二段示例是优化后的版本,它把method1()的操作直接放在callMethod1()里,避免了大量Method调用。
避免使用昂贵的函数
在Java里,某些函数或操作比其他函数更消耗资源。这些函数包括 反射API、序列化和正则表达式 等。这些函数在运行时动态生成和解析代码或数据,会导致大量的计算和内存开销。所以除非必要,不然的话我们应该尽量避免使用这些昂贵的函数。下面我们通过一些示例来说明如何避免使用这些昂贵的函数。
反射API
反射API是Java提供的一种功能强大的工具,可以在运行时检查类、接口、字段和方法的信息,甚至可以动态调用方法。然而反射操作会带来很多额外的开销,包括类加载、实例化、安全性检查和方法访问等。因此,我们应尽量在必要的地方使用反射,而不是无所顾忌地使用。有些时候,我们可以通过创建对象实例或者显式调用方法来代替反射。
昂贵的反射代码:
Class<?> c = Class.forName("com.example.MyClass");
Method m = c.getDeclaredMethod("myMethod");
m.invoke(c.newInstance());
更好的方式:
Serialization
序列化操作可以把对象的状态转换成字节流,以便存储在物理介质上或在网络中传输。因为需要对对象的所有成员变量进行读取和转换,并将转换后的数据进行打包和发送,所以这个过程也是很消耗资源的。不过我们可以通过使用更轻量级的序列化框架,比如Protobuf、Fastjson等,来代替Java自带的序列化。
昂贵的序列化代码:
FileOutputStream fos = new FileOutputStream("temp.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(myObject);
使用更优雅的方式(Fastjson):
Regular Expression
正则表达式是一个非常有用的工具,可以用来处理字符串等任务。然而正则表达式的复杂性往往会导致大量计算开销。使用一些简单的字符串操作,比如String的split()、replace()、indexOf()等方法,可以有效地替代正则表达式。
昂贵的正则表达式:
简单的字符串操作:
以上就是我们从代码角度对性能做出的优化,此外我们还可以从JVM角度出发去优化程序的性能。
JVM调优:G1 垃圾收集器参数
优化JVM的目标是提高程序的性能和响应速度,降低系统的资源消耗。完成这个目标可以从G1垃圾收集器的最大吞吐量、最小的停顿时间,以及最低的内存占用三个方面入手。
对于G1 垃圾收集器,我们可以调整以下参数来提升性能:
- -XX:G1NewSizePercent:最小新生代比例,默认值是5%,根据当前系统状态和业务需求调整。
- -XX:G1MaxNewSizePercent:最大新生代比例,默认值为60%,根据系统状态及业务需求调整。
以下是一些调优示例,调整这些参数,能够提高系统的整体吞吐量。
-XX:+UseG1GC
-XX:G1ReservePercent=15
-XX:InitiatingHeapOccupancyPercent=70
-XX:ConcGCThreads=10
-XX:ParallelGCThreads=20
JVM调优包含很多方面,而不仅仅是垃圾收集器。我这里只提到了G1垃圾收集器,是因为G1是当前比较流行的垃圾收集器,而且它提供了很多可调优的参数,对于优化性能有很好的效果。但是,其他垃圾收集器,比如CMS、Parallel GC等,它们的调优方法可能会和G1 有些不一样。
如果你的应用使用的是其他垃圾收集器,那么调整G1相关的参数可能不会有什么效果。在这种情况下,你需要查阅相应的垃圾收集器文档,了解它们的调优方法。例如,CMS垃圾收集器主要通过调整-XX:CMSInitiatingHeapOccupancyPercent参数来优化性能,而Parallel GC则主要通过调整-XX:ParallelGCThreads和-XX:InitiatingHeapOccupancyPercent参数来优化性能。
我们再来看如何实现最小停顿时间和最低内存占用。
-
最小停顿时间:这个目标主要是通过调整垃圾收集器的参数来实现的。例如,对于G1垃圾收集器,你可以通过调整-XX:GCTimeRatio参数来控制垃圾收集器停顿的时间。此外,你还可以通过调整-XX:ParallelGCThreads参数来控制并行垃圾收集线程的数量,从而影响停顿时间。
-
最低内存占用:这个目标主要是通过调整堆内存的大小和分配策略来实现的。例如,你可以通过调整-Xms和-Xmx参数来控制堆内存的大小。此外,你还可以通过调整-XX:MaxHeapFreeRatio和-XX:MinHeapFreeRatio参数来控制堆内存的分配策略,从而影响内存占用。
需要注意的是,JVM调优是一个复杂的过程,需要根据具体的应用场景和系统环境来进行。在调整参数时,建议你先进行压力测试,以确保应用的性能和稳定性。
除了代码层和JVM层,我们还可以考虑通过优化数据库来优化程序的性能,下面我们通过一个实际的案例来综合看一下,里面会涉及架构上的优化和数据库优化。
百万级系统优化
项目背景
一个大型社交网络平台,随着平台的发展用户数量不断增长,对平台的并发访问请求呈现出爆炸性的增长,这导致了服务器的响应速度下降,用户体验急剧下滑。为了提升系统的性能,满足处理百万级TPS的需求,我们需要对平台进行全面的性能优化。
针对社交平台的特性,我们以获取好友列表这个常用请求为例,来展开性能优化的工作。这个优化方案主要有三个部分,分别是 使用微服务架构、对数据库进行优化操作,以及引入缓存技术。
问题分析
我们把任务拆分成3个部分,然后逐个击破。
- 单独的服务器面临处理所有请求的压力,资源分配困难,而且单点故障可能导致全局服务中断。
- 数据库对查询操作优化不足,频繁的磁盘I/O操作导致查询响应速度缓慢。
- 系统没有采用合适的缓存机制,大量重复查询操作使数据库压力过大。
解决方案设计
针对上述3个问题,我们也设计了不同的解决方案。
- 实施微服务架构:将查询好友列表功能独立为一个服务部署,降低服务间的依赖,提高系统的扩展性和容错能力。
- 优化数据库:针对用户的ID添加索引,提升查询效率。
- 引入缓存技术:引入Redis缓存技术,把常用的查询结果存储在内存中,减轻数据库的负载。
具体步骤
微服务架构
通过把原有的单体应用拆分为多个微服务,每个微服务具有独立处理请求的能力。通过负载均衡技术,让请求均匀地分配到各个微服务上处理。
假设我们有一个电商应用,其中包括几项微服务,分别是用户服务、订单服务、商品服务和支付服务。每个微服务负责处理相应的业务请求。
首先,我们需要创建这些微服务并定义它们的接口。这里我给出了一个简化的例子供你参考。
// UserService.java
public interface UserService {
User getUserById(int userId);
void createUser(User user);
}
// OrderService.java
public interface OrderService {
List<Order> getOrdersByUserId(int userId);
Order createOrder(Order order);
}
// ProductService.java
public interface ProductService {
Product getProductById(int productId);
List<Product> getAllProducts();
}
// PaymentService.java
public interface PaymentService {
void payOrder(Order order);
}
接下来,我们可以实现这些接口。
// UserServiceImpl.java
@Service
public class UserServiceImpl implements UserService {
// ... 实现getUserById、createUser方法
}
// OrderServiceImpl.java
@Service
public class OrderServiceImpl implements OrderService {
// ... 实现getOrdersByUserId、createOrder方法
}
// ProductServiceImpl.java
@Service
public class ProductServiceImpl implements ProductService {
// ... 实现getProductById、getAllProducts方法
}
// PaymentServiceImpl.java
@Service
public class PaymentServiceImpl implements PaymentService {
// ... 实现payOrder方法
}
最后,我们需要配置这些微服务以便在运行时能够发现并调用它们。
// application.properties
spring.application.name=user-service
user-service.url=http://localhost:8081
spring.application.name=order-service
order-service.url=http://localhost:8082
spring.application.name=product-service
product-service.url=http://localhost:8083
spring.application.name=payment-service
payment-service.url=http://localhost:8084
优化数据库
假设我们有一个名为 user_table
的用户表,其中包含 user_id
列。为了提高查询效率,我们可以为 user_id
列创建索引。以下是创建索引的SQL语句:
在实际应用中,你可能需要根据业务需求创建更多的索引,以提高查询效率。不过要注意,创建索引会影响数据库性能,因此在 创建索引时要权衡好性能和存储空间。
此外,为了更好地优化数据库性能,你还可以考虑一些其他的方法。
- 优化SQL查询:编写高效的SQL查询,避免使用子查询、临时表和大量的JOIN操作。
- 创建数据库缓存:使用缓存技术,如Redis或Memcached,来减少对数据库的访问次数。
- 数据库分库分表:对于大型应用,我们可以考虑把数据拆分到多个数据库和表中,来提高查询效率。
- 数据库连接池:使用数据库连接池来减少建立和关闭数据库连接的开销。
- 数据库优化器:根据数据库的类型,使用数据库优化器来调整数据库的性能。
引入缓存
使用Redis作为缓存服务。在处理完用户的查询请求后,可以把查询结果保存到Redis中。当下一次出现相同的查询请求时,可以直接从Redis中获取结果,而不必访问数据库。
你可以看一下我给出的示例代码。
@Service
public class FriendsService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private FriendsRepository friendsRepository;
public List<User> getFriends(String userId) {
// 从Redis中获取好友
List<User> friends = (List<User>)redisTemplate.opsForValue().get("FRIENDS_" + userId);
// 如果Redis中没有,则从数据库获取
if (friends == null) {
friends = friendsRepository.findByUserId(userId);
// 把获取到的好友存储到Redis中
redisTemplate.opsForValue().set("FRIENDS_" + userId, friends);
}
return friends;
}
}
重点回顾
性能优化就像是一场没有终点的马拉松,需要持续关注和改进。这也是一个全系统的工作,每一次系统优化都需要从代码、系统、硬件等多个角度去考量。
这节课我们就从代码、JVM层和数据库三个角度出发,对性能优化过程中需要关注的点进行了总结,如果你的程序遇到了性能问题,不妨从这几个角度尝试做出改变。 每一次优化都可能带来明显的性能提升,但也需注意,过分追求性能优化有可能导致系统的可维护性、可读性下降,所以需要你适度把握。
希望通过实践和摸索,你可以一步步提高系统的性能,让系统满足更多的用户需求,更好地服务于业务。
思考题
学完这节课之后,你能不能说一说性能优化还可以从哪些方面着手?此外你可以分析一下你目前系统的性能瓶颈,并尝试给出解决方案。
希望你把自己的想法分享到评论区,我们一起讨论,如果有收获的话,也欢迎你把这节课的内容分享给需要的朋友,我们下节课再见!