大厂性能优化 10大顶奢方案
尼恩说在前面
高并发下,如何设计秒杀系统?这是一个高频面试题。
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、shein 希音、shopee、百度、网易的面试资格,遇到很多很重要的面试题:
如何做性能优化,有哪些方案?
你用过哪些 性能优化方案?
前几天 小伙伴面试 shopee,遇到了这个问题。但是由于 没有回答好,导致面试挂了。
小伙伴面试完了之后,来求助尼恩:遇到性能优化,该如何才能回答得很漂亮,才能 让面试官刮目相看、口水直流。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典》V145版本PDF集群,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,后台回复:领电子书
下面给大家总结一下大厂 性能优化的10方案 , 组成一个《大厂 性能优化圣经》
大厂 性能优化的 10大方案
1. 代码优化
代码优化是提升系统性能的重要手段,它涉及到编写高效、简洁且易于维护的代码。以下是对代码优化方案的详解:
1.1 性能问题的识别
首先,必须通过性能分析工具识别出代码中的瓶颈。这些工具可以帮助开发者了解程序的执行时间、内存使用情况以及热点函数等关键信息。
1.2 循环优化
循环是程序中常见的性能瓶颈之一。优化循环包括减少循环次数、优化循环内部的计算以及使用更高效的数据结构。
比如说:将循环内的重复计算移到循环外部。
// 原始代码
for (int i = 0; i < n; i++) {
double x = pow(a[i], 2); // 重复计算平方
y += x;
}
// 优化后的代码
double x;
for (int i = 0; i < n; i++) {
x = a[i] * a[i]; // 直接计算乘积,避免使用pow函数
y += x;
}
1.3 算法优化
选择正确的算法对性能至关重要。例如,对于大数据集的操作,使用排序算法时,快速排序通常比简单排序更高效。
1.4 减少函数调用
函数调用会引入额外的开销。在某些情况下,内联函数可以减少调用的开销,特别是在函数体较小的情况下。
1.5 内存管理
有效的内存管理可以减少程序的内存占用和提高缓存利用率。避免内存泄漏、合理使用缓存以及减少内存分配和释放操作都是重要的优化手段。
1.6 并发和多线程
在多核处理器上,利用并发和多线程可以显著提高程序的执行效率。合理分配任务、减少线程间的竞争以及同步开销是并发编程中的关键。
1.7 代码重构
代码重构是改善现有代码设计而不改变其外部行为的过程。它可以帮助开发者去除重复代码、提高代码的模块化和可读性,从而间接提升性能。
1.8 避免不必要的计算
消除代码中的冗余计算,
例如:
- 将重复使用的计算结果存储起来,避免在每次需要时重新计算。
- 通过重新组织代码逻辑,可以完全避免无效循环。
通过重新组织代码逻辑,可以完全避免无效循环,实例如下:
// 原始代码
for (int i = 0; i < n; i++) {
if (a[i] > threshold) {
process(a[i]);
}
}
// 优化后的代码
for (int i = 0; i < n; i++) {
if (a[i] <= threshold) continue;
process(a[i]);
}
1.9 编译器优化
充分利用编译器的优化选项,如开启高级优化等级,可以让编译器帮助我们进行一些代码优化。
1.10 性能测试
最后,任何优化都应该通过性能测试来验证其效果。确保优化后的代码不仅在理论上更快,而且在实际运行中也能带来性能提升。
2. 缓存优化
2.1 常规优化的三大银弹
1 缓存优化
在架构设计中,缓存策略的合理运用可以显著提高系统性能。常见的缓存策略包括LRU(最近最少使用)、LFU(最少频繁使用)等,以及分布式缓存的使用,如Redis或Memcached。
2 多线程优化
多线程与分布式计算是性能优化中的重要策略,它们通过利用更多的处理器核心或分布计算任务到多个物理或虚拟机器上来提升系统的整体性能。
3 异步优化
异步处理可以提高系统的响应性和吞吐量。
消息队列(如RabbitMQ、Kafka)在分布式系统中用于解耦服务,实现异步通信,提高系统的可扩展性和容错性。
先看第一个银弹: 缓存优化。
2.2 缓存的作用与重要性
缓存是提升系统性能的关键技术之一,通过减少数据访问延迟和降低后端存储压力,显著加快数据检索速度。在现代应用架构中,缓存不仅用于提升用户体验,也是实现高并发处理的基础。
2.3 常见的缓存实现方式
2.3.1 内存缓存
内存缓存利用服务器的RAM来存储热点数据,提供快速的数据访问能力。常见的内存缓存实现包括Ehcache、Memcached等。
2.3.2 分布式缓存
分布式缓存通过多台服务器共享缓存数据,解决了单机内存限制的问题,并提供了更好的扩展性和可用性。例如,Redis和Hazelcast是流行的分布式缓存解决方案。
2.3.3 浏览器缓存
浏览器缓存利用客户端的存储能力,减少网络传输的数据量,加快页面加载速度。通过设置合适的HTTP头信息,如Cache-Control和Expires,可以控制浏览器缓存的生命周期。
2.3.4 CDN缓存
内容分发网络(CDN)通过在网络边缘节点存储静态资源,使用户可以就近获取数据,从而减少延迟和带宽消耗。
2.4 缓存策略的优化技巧
2.4.1 缓存粒度控制
合理选择缓存的粒度,可以平衡内存使用和访问速度。过细的粒度可能导致缓存碎片化,而过粗的粒度可能降低缓存效率。
2.4.2 缓存失效策略
缓存失效策略(如LRU、FIFO等)决定了哪些数据应该被移除缓存,以适应新的数据更新和访问模式。
2.4.3 缓存预热
缓存预热是在系统启动或低负载时段预先加载热点数据到缓存中,避免在高负载时发生缓存缺失。
2.4.4 缓存一致性
在分布式系统中,保持缓存与后端存储的一致性是一个挑战。可以通过发布/订阅模式、消息队列等机制来实现缓存的同步更新。
2.5 缓存的实际应用案例
2.5.1 电商网站
在电商网站中,商品详情页、购物车等热点数据通过缓存策略减少数据库访问,提高系统响应速度。
2.5.2 社交媒体平台
社交媒体平台通过缓存用户动态、热门话题等数据,应对大规模的用户访问和数据请求。
2.5.3 金融服务
金融服务应用缓存技术在交易处理、风险评估等场景中,以满足低延迟和高吞吐量的需求。
2.5.4 缓存策略的监控与调优
缓存策略的有效性需要通过持续的监控和调优来保证。监控指标包括缓存命中率、响应时间、内存使用率等,根据这些指标调整缓存大小、失效策略和数据更新频率。
2.5.5 缓存的三大经典问题
通常情况下,我们需要在redis中保存商品信息,里面包含:商品id、商品名称、规格属性、库存等信息,同时数据库中也要有相关信息,毕竟缓存并不完全可靠。
用户在点击秒杀按钮,请求秒杀接口的过程中,需要传入的商品id参数,然后服务端需要校验该商品是否合法。
大致流程如下图所示:
根据商品id,先从缓存中查询商品,如果商品存在,则参与秒杀。
如果不存在,则需要从数据库中查询商品,如果存在,则将商品信息放入缓存,然后参与秒杀。
如果商品不存在,则直接提示失败。
这个过程表面上看起来是OK的,但是如果深入分析一下会发现一些问题。
具体的分析过程,请参考《尼恩的秒杀圣经》。
3. 异步优化
3.1 异步优化的概念与优势
异步处理是一种编程模式,允许程序在等待特定操作完成时继续执行其他任务。这种技术可以显著提高应用程序的响应性和性能。
- 非阻塞操作:异步操作不会阻塞主线程,用户界面(UI)可以保持流畅,提升用户体验。
- 提高资源利用率:通过异步执行,系统资源得到更高效的利用,尤其是在I/O密集型操作中。
3.2 异步优化的应用场景
异步处理适用于多种场景,特别是在需要提升性能和用户体验的应用程序中。
- 网络请求:在等待网络响应时,异步处理允许应用程序继续处理用户交互。
- 文件读写:处理大文件或慢速存储设备时,异步读写可以避免界面冻结。
- 数据处理:对大量数据进行处理时,异步执行可以避免长时间占用CPU资源。
3.3 实现优化处理的技术
多种编程语言和框架提供了实现异步处理的技术,包括回调函数、事件循环、Promise、Async/Await等。
- 回调函数:一种传统的异步处理方式,通过将函数作为参数传递给另一个函数来处理完成时的逻辑。
- 事件循环:Node.js等环境使用事件循环来处理异步事件,提高性能和可伸缩性。
- Promise:现代JavaScript中用于异步编程的对象,提供了一种更易读和更易管理的方式来处理异步操作。
- Async/Await:基于Promise的语法糖,允许以同步的方式编写异步代码,提高代码的可读性。
3.4 异步优化的最佳实践
为了充分利用异步处理的优势,开发者需要遵循一些最佳实践。
- 错误处理:异步代码中的错误需要特别处理,确保程序的稳定性和可靠性。
- 避免回调地狱:深度嵌套的回调函数难以阅读和维护,应使用Promise或Async/Await来简化代码结构。
- 资源管理:确保异步操作中使用的资源(如数据库连接、文件句柄)在使用后正确释放。
- 测试:异步代码的测试需要特别注意,确保并发和时序问题得到妥善处理。
3.5 全链路 异步
随着业务的发展,微服务应用的流量越来越大,使用到的资源也越来越多。
在微服务架构下,大量的应用都是 SpringCloud 分布式架构,这种架构,总体是全链路同步模式。
同步编程模式不仅造成了资源的极大浪费,并且在流量发生激增波动的时候,受制于系统资源而无法快速的扩容。
全球后疫情时代,降本增效是大背景。
如何降本增效?可以通过技术升级,全链路同步模式 ,升级为 全链路异步模式。
全链路同步模式架构图
需要 全链路、多层次、多维度进行 优化和改造。
3.6 异步优化的性能考量
异步处理可以显著提升性能,但也需要考虑到一些性能方面的细节。
- 减少等待时间:通过异步处理,可以减少程序的等待时间,提高整体的执行效率。
- 优化并发:合理控制并发数量,避免过多的并发导致资源竞争和性能下降。
- 监控和调优:使用性能监控工具来分析异步操作的性能,根据实际情况进行调优。
4. 多线程优化
多线程与分布式计算是性能优化中的重要策略,它们通过利用更多的处理器核心或分布计算任务到多个物理或虚拟机器上来提升系统的整体性能。
4.1 多线程优化
多线程技术允许程序中的不同部分同时执行,从而提高程序的执行效率和响应性。
- 线程管理:合理创建和管理线程,避免线程过多导致的上下文切换开销。
- 同步机制:使用互斥锁、信号量等同步机制来避免竞态条件和死锁。
- 线程池:使用线程池来重复利用线程资源,减少线程创建和销毁的开销。
- 任务分解:将大任务分解为可以并行处理的小任务,提高资源利用率。
4.2 示例代码
Java 中可以使用 CompletableFuture
用于处理异步任务。它提供了一种灵活的方式来处理异步计算和多线程优化。
以下是一个使用 CompletableFuture
来并行下载文件的示例。
java复制代码import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
public class MultiThreadedDownloader {
// 要下载的文件列表
private static final List<String> URLs = Arrays.asList(
"http://example.com/file1",
"http://example.com/file2",
"http://example.com/file3",
"http://example.com/file4",
"http://example.com/file5"
);
public static void main(String[] args) {
// 创建一个包含下载任务的 CompletableFuture 列表
List<CompletableFuture<Void>> futures = new ArrayList<>();
// 遍历每个 URL,并为每个 URL 创建一个 CompletableFuture 任务
for (String url : URLs) {
futures.add(CompletableFuture.runAsync(() -> downloadFile(url)));
}
// 使用 allOf 方法等待所有任务完成
CompletableFuture<Void> allOf = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
// 当所有任务完成时,输出完成信息
allOf.thenRun(() -> System.out.println("All files downloaded."));
// 阻塞主线程,直到所有任务完成
allOf.join();
}
private static void downloadFile(String url) {
try {
URL website = new URL(url);
try (InputStream in = website.openStream()) {
Files.copy(in, Paths.get(url.substring(url.lastIndexOf('/') + 1)), StandardCopyOption.REPLACE_EXISTING);
System.out.println(url.substring(url.lastIndexOf('/') + 1) + " downloaded.");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
CompletableFuture是否使用默认线程池的依据,和机器的CPU核心数有关。
场景一:当 (CPU核心数-1) 大于1时,才会使用默认的线程池,forkjoin 线程池 ForkJoinPool.commonPool()。
场景二:当 (CPU核心数-1)不大于 1时,也就是 单核或者双核,将会为每个CompletableFuture的任务创建一个新线程去执行。
换句话说,forkjoin 线程池 作为 CompletableFuture的默认线程池,只有在双核以上的机器内才会使用。
在双核及以下的机器中,会为每个任务创建一个新线程,这种场景,等于没有使用线程池,且有资源耗尽的风险。
另外,forkjoin 线程池 也有问题,这个默认线程池,池内的核心线程数,也为机器核心数-1。
也就意味着假设你是4核机器,那最多也只有3个核心线程,对于CPU密集型的任务来说倒还好,但是我们平常写业务代码,更多的是IO密集型任务/混合型任务,这就问题很大。
对于IO密集型、混合型的任务来说,机器核心数-1 的线程数量 其实远远不够用的,会导致大量的 任务在等待,导致吞吐率大幅度下降,即默认线程池比较适用于CPU密集型任务。
具体的线程数的设置,包括CPU密集型 、IO密集型任务/混合型任务 的线程数设置,具体的算法 请参见尼恩 的《JAVA 高并发核心编程 卷2》
总结一下,默认 ForkJoinPool.commonPool() 线程池缺点
1、CompletableFuture
默认使用的线程池是 ForkJoinPool.commonPool()
,commonPool是当前 JVM(进程) 上的所有 CompletableFuture、并行 Stream 共享的,commonPool 的目标场景是非阻塞的 CPU 密集型任务,其线程数默认为 CPU 数量减1,所以对于我们用java常做的IO密集型任务,默认线程池是远远不够使用的
2、CompletableFuture
是否使用默认线程池的依据,和机器的CPU核心数有关。当CPU核心数-1>1时,才会使用默认的线程池,否则将会为每个CompletableFuture的任务创建一个新线程去执行。
也就是说,CompletableFuture的默认线程池,只有在双核以上的机器内才会使用。在双核及以下的机器中,会为每个任务创建一个新线程,等于没有使用线程池,且有资源耗尽的风险。
因此建议,在使用CompletableFuture时,务必要自定义线程池。
4.3 实例2: CompletableFuture+ 自定义线程池
@Configuration
public class ThreadPoolConfig {
//参数初始化
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
//核心线程数量大小
private static final int corePoolSize = Math.max(2, Math.min(CPU_COUNT-1,4));
//线程池最大容纳线程数
private static final int maxPoolSize = CPU_COUNT * 2 + 1;
//阻塞队列
private static final int workQueue = 20;
//线程空闲后的存活时长
private static final int keepAliveTime = 30;
@Bean("asyncTaskExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
//核心线程数
threadPoolTaskExecutor.setCorePoolSize(corePoolSize);
//最大线程数
threadPoolTaskExecutor.setMaxPoolSize(maxPoolSize);
//等待队列
threadPoolTaskExecutor.setQueueCapacity(workQueue);
//线程前缀
threadPoolTaskExecutor.setThreadNamePrefix("asyncTaskExecutor-");
//线程池维护线程所允许的空闲时间,单位为秒
threadPoolTaskExecutor.setKeepAliveSeconds(keepAliveTime);
// 线程池对拒绝任务(无线程可用)的处理策略
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
}
@RestController
@RequestMapping("/task")
public class CompletableTaskController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
@Qualifier("asyncTaskExecutor")
private Executor asyncTaskExecutor;
@RequestMapping("testOrderTask")
public String testOrderTask(){
List<CompletableFuture<List<Integer>>> futureList = Lists.newArrayList();
// 任务1,计算3秒
CompletableFuture<List<Integer>> task1 = CompletableFuture.supplyAsync(() -> {
sleepSeconds(3L);
return Lists.newArrayList(1,2,3);
}, asyncTaskExecutor);
futureList.add(task1);
// 任务2,计算2秒,得答案5
CompletableFuture<List<Integer>> task2 = CompletableFuture.supplyAsync(() -> {
sleepSeconds(1L);
return Lists.newArrayList(4,5,6);
}, asyncTaskExecutor);
futureList.add(task2);
// 任务3,计算3秒,得答案5
CompletableFuture<List<Integer>> task3 = CompletableFuture.supplyAsync(() -> {
sleepSeconds(2L);
return Lists.newArrayList(7,8,9);
}, asyncTaskExecutor);
futureList.add(task3);
// 写法1
List<Integer> newList = futureList.stream().map(CompletableFuture::join).flatMap(List::stream).collect(Collectors.toList());
return JSON.toJSONString(newList);
}
}
5. 前端优化
5.1 浏览器访问优化
浏览器访问优化是提升用户体验的关键步骤。通过减少HTTP请求次数,合并CSS和JavaScript文件,以及利用浏览器缓存等策略,可以显著减少页面加载时间。
- 减少HTTP请求:通过合并CSS和JavaScript文件,减少服务器请求次数,降低数据传输开销。
- 浏览器缓存利用:通过设置合理的Cache-Control和Expires头部,使得浏览器能够缓存静态资源,减少重复加载时间。
- 启用压缩:服务器端启用Gzip等压缩算法,减少传输数据量,加快加载速度。
5.2 CDN加速
以秒杀场景为例。
秒杀详情页面是用户流量的第一入口,所以是并发量最大的地方。
如果这些流量都能直接访问服务端,恐怕服务端会因为承受不住这么大的压力,而直接挂掉。
秒杀详情页面绝大多数内容是固定的,比如:商品名称、商品描述、图片等。
为了减少不必要的服务端请求,通常情况下,会对活动页面做静态化
处理。
用户浏览商品等常规操作,并不会请求到服务端。
为了性能考虑 动静分离,分离出下面的两大部分:
- 静态 秒杀页面资源:一个html文件,包括 css、js和图片等,内容包括秒杀产品的介绍,详情,参数等等。静态秒杀静态资源文件提前缓存到CDN上,让用户能够就近访问秒杀页面。
- 动态秒杀的exposed-key,通过JS异步获取。秒杀暴露就是将符合条件的秒杀 暴露给用户,以便互联网用户能参与商品的秒杀。这个操作可以是商户手动完成,生产场景下的更合理的方式是系统定时任务去完成。秒杀暴露主要是生成一个 具备实效性的 exposed-key。
但只做页面静态化还不够,因为用户分布在全国各地,有些人在北京,有些人在成都,有些人在深圳,地域相差很远,网速各不相同。
如何才能让用户最快访问到活动页面呢?
这就需要使用CDN,它的全称是Content Delivery Network,即内容分发网络。
使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。
CDN服务器就是内容分发网络,把资源内容放在了全国各地的各服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,一般都是到阿里云买CDN服务器。
尼恩提示:如果没有CDN,也可以放在Nginx中做动静分离。
内容分发网络(CDN)通过将内容缓存到离用户更近的节点,加快了静态资源的加载速度,提升了全球用户的访问体验。
- 静态资源CDN缓存:图片、CSS、JavaScript等静态资源通过CDN分发,减少主服务器的负担,加快加载速度。
- 动态内容CDN支持:对于部分动态内容,使用CDN进行缓存,可以减少源站压力,提高响应速度。
5.3 反向代理
反向代理服务器作为Web服务器的前置,可以提供额外的安全层,并通过缓存机制加速内容的分发。
- 安全增强:反向代理隐藏了真实的Web服务器,增加了一层安全防护。
- 缓存静态内容:通过缓存机制,减少对后端服务器的请求,提高响应速度。
5.4 Web组件分离
通过将Web应用的各个组件分离到不同的域名下,可以提高浏览器的并发下载能力,加快页面渲染速度。
- 组件并行加载:利用浏览器对不同域名的并行下载优势,提升资源加载效率。
- 减少Cookie传输:静态资源使用独立域名,减少Cookie在HTTP请求中的传输,降低请求大小。
5.5 前端服务化
将前端应用拆分成多个服务,可以独立部署和扩展,提高应用的可维护性和可扩展性。
- 按业务拆分服务:根据业务逻辑将应用拆分成多个微服务,实现独立部署和扩展。
- 数据和服务分离:将数据存储和业务逻辑分离,提高数据处理效率和应用性能。
6. 微服务架构优化
6.1 架构设计原则
架构设计是性能优化中的关键环节,其核心原则包括模块化、单一职责、可扩展性和高内聚低耦合。
这些原则有助于构建一个易于维护、升级和扩展的系统。
6.2 微服务架构
微服务架构通过将单一应用程序划分为一组小的服务,每个服务运行在其独立的进程中,服务之间通过轻量级的通信机制进行交互。这种架构有助于提高系统的可维护性和可扩展性,同时也便于进行持续集成和持续部署。
我们可以将电商系统的微服务架构简化,成下图所示:
由图所示,我们可以简单的将电商系统的核心层分为:接入层、服务层和持久层。
接下来,可以一层一层的进行优化。
- 假如流量接入层使用的是高性能的Nginx,则我们可以预估Nginx最大的并发度为:10W+,这里是以万为单位。
- 假设服务层我们使用的是Tomcat,而Tomcat的最大并发度可以预估为800左右,这里是以百为单位。
- 假设持久层的缓存使用的是Redis,数据库使用的是MySQL,MySQL的最大并发度可以预估为1000左右,以千为单位。Redis的最大并发度可以预估为5W左右,以万为单位。
微服务架构 ≈ 模块化开发 + 分布式计算。
需要注意的是“微服务”与“微服务架构”是有本质区别的。“微服务”强调的是服务的大小,它关注的是某一个点。而“微服务架构”则是一种架构思想,需要从整体上对软件系统进行通盘的考虑。
微服务架构示意图:
常见的微服务组件及概念:
- 服务注册:服务提供方将自己调用地址注册到服务注册中心,让服务调用方能够方便地找到自己。
- 服务发现:服务调用方从服务注册中心找到自己需要调用的服务的地址。
- 负载均衡:服务提供方一般以多实例的形式提供服务,负载均衡功能能够让服务调用方连接到合适的服务节点。并且,节点选择的工作对服务调用方来说是透明的。
- 服务网关:服务网关是服务调用的唯一入口,可以在这个组件是实现用户鉴权、动态路由、灰度发布、A/B 测试、负载限流等功能。
- 配置中心:将本地化的配置信息(properties, xml, yaml 等)注册到配置中心,实现程序包在开发、测试、生产环境的无差别性,方便程序包的迁移。
- API 管理:以方便的形式编写及更新 API 文档,并以方便的形式供调用者查看和测试。
- 集成框架:微服务组件都以职责单一的程序包对外提供服务,集成框架以配置的形式将所有微服务组件(特别是管理端组件)集成到统一的界面框架下,让用户能够在统一的界面中使用系统。
- 分布式事务:对于重要的业务,需要通过分布式事务技术(TCC、高可用消息服务、最大努力通知)保证数据的一致性。
- 调用链:记录完成一个业务逻辑时调用到的微服务,并将这种串行或并行的调用关系展示出来。在系统出错时,可以方便地找到出错点。
- 支撑平台:系统微服务化后,系统变得更加碎片化,系统的部署、运维、监控等都比单体架构更加复杂,那么,就需要将大部分的工作自动化。现在,可以通过 Docker 等工具来中和这些微服务架构带来的弊端。 例如持续集成、蓝绿发布、健康检查、性能健康等等。严重点,以我们两年的实践经验,可以这么说,如果没有合适的支撑平台或工具,就不要使用微服务架构。
微服务架构的优点:
- 降低系统复杂度:每个服务都比较简单,只关注于一个业务功能。
- 松耦合:微服务架构方式是松耦合的,每个微服务可由不同团队独立开发,互不影响。
- 跨语言:只要符合服务 API 契约,开发人员可以自由选择开发技术。这就意味着开发人员可以采用新技术编写或重构服务,由于服务相对较小,所以这并不会对整体应用造成太大影响。
- 独立部署:微服务架构可以使每个微服务独立部署。开发人员无需协调对服务升级或更改的部署。这些更改可以在测试通过后立即部署。所以微服务架构也使得 CI/CD 成为可能。
- Docker 容器:和 Docker 容器结合的更好。
- DDD 领域驱动设计:和 DDD 的概念契合,结合开发会更好。
微服务架构的缺点:
- 微服务强调了服务大小,但实际上这并没有一个统一的标准:业务逻辑应该按照什么规则划分为微服务,这本身就是一个经验工程。有些开发者主张 10-100 行代码就应该建立一个微服务。虽然建立小型服务是微服务架构崇尚的,但要记住,微服务是达到目的的手段,而不是目标。微服务的目标是充分分解应用程序,以促进敏捷开发和持续集成部署。
- 微服务的分布式特点带来的复杂性:开发人员需要基于 RPC 或者消息实现微服务之间的调用和通信,而这就使得服务之间的发现、服务调用链的跟踪和质量问题变得的相当棘手。
- 分区的数据库体系和分布式事务:更新多个业务实体的业务交易相当普遍,不同服务可能拥有不同的数据库。CAP 原理的约束,使得我们不得不放弃传统的强一致性,而转而追求最终一致性,这个对开发人员来说是一个挑战。
- 测试挑战:传统的单体WEB应用只需测试单一的 REST API 即可,而对微服务进行测试,需要启动它依赖的所有其他服务。这种复杂性不可低估。
- 跨多个服务的更改:比如在传统单体应用中,若有 A、B、C 三个服务需要更改,A 依赖 B,B 依赖 C。我们只需更改相应的模块,然后一次性部署即可。但是在微服务架构中,我们需要仔细规划和协调每个服务的变更部署。我们需要先更新 C,然后更新 B,最后更新 A。
- 部署复杂:微服务由不同的大量服务构成。每种服务可能拥有自己的配置、应用实例数量以及基础服务地址。这里就需要不同的配置、部署、扩展和监控组件。此外,我们还需要服务发现机制,以便服务可以发现与其通信的其他服务的地址。因此,成功部署微服务应用需要开发人员有更好地部署策略和高度自动化的水平。
- 总的来说(问题和挑战):API Gateway、服务间调用、服务发现、服务容错、服务部署、数据调用。
不过,现在很多微服务的框架(比如 Spring Cloud、Dubbo)已经很好的解决了上面的问题。
6.3 服务网格与无入侵的微服务治理
服务网格(如Istio)提供了一种将服务间通信控制和安全性从应用程序代码中抽象出来的方式。
它支持服务发现、负载均衡、故障恢复、度量和监控等微服务治理功能,有助于提升系统的性能和稳定性。
2017 年底,非侵入式的 Service Mesh 技术从萌芽到走向了成熟。
Service Mesh 又译作“服务网格”,作为服务间通信的基础设施层。
如果用一句话来解释什么是 Service Mesh,可以将它比作是应用程序或者说微服务间的 TCP/IP,负责服务之间的RPC调用、限流、熔断和监控。
对于编写应用程序来说一般无须关心 TCP/IP 这一层(比如通过 HTTP 协议的 RESTful 应用),同样使用 Service Mesh 也就无须关系服务之间的那些原来是通过应用程序或者其他框架实现的事情,比如 Spring Cloud、OSS,现在只要交给 Service Mesh 就可以了。
Service Mesh 的来龙去脉:
- 从最原始的主机之间直接使用网线相连
- 网络层的出现
- 集成到应用程序内部的控制流
- 分解到应用程序外部的控制流
- 应用程序的中集成服务发现和断路器
- 出现了专门用于服务发现和断路器的软件包/库,如 Twitter 的 Finagle 和 Facebook 的 Proxygen,这时候还是集成在应用程序内部
- 出现了专门用于服务发现和断路器的开源软件,如 Netflix OSS、Airbnb 的 synapse 和 nerve
- 最后作为微服务的中间层 Service Mesh 出现
Service Mesh 有如下几个特点:
- 应用程序间通讯的中间层
- 轻量级网络代理
- 应用程序无感知
- 解耦应用程序的重试/超时、监控、追踪和服务发现
Service Mesh 架构图:
目前流行的 Service Mesh 开源软件有 Linkerd、Envoy 和 Istio,而最近 Buoyant(开源 Linkerd 的公司)又发布了基于 Kubernetes 的 Service Mesh 开源项目 Conduit。
Service Mesh 开源项目简介:
- Linkerd(https://github.com/linkerd/linkerd):第一代 Service Mesh,2016 年 1 月 15 日首发布,业界第一个 Service Mesh 项目,由 Buoyant 创业小公司开发(前 Twitter 工程师),2017 年 7 月 11 日,宣布和 Istio 集成,成为 Istio 的数据面板。
- Envoy(https://github.com/envoyproxy/envoy):第一代 Service Mesh,2016 年 9 月 13 日首发布,由 Matt Klein 个人开发(Lyft 工程师),之后默默发展,版本较稳定。
- Istio(https://github.com/istio/istio):第二代 Service Mesh,2017 年 5 月 24 日首发布,由 Google、IBM 和 Lyft 联合开发,只支持 Kubernetes 平台,2017 年 11 月 30 日发布 0.3 版本,开始支持非 Kubernetes 平台,之后稳定的开发和发布。
- Conduit(https://github.com/runconduit/conduit):第二代 Service Mesh,2017 年 12 月 5 日首发布,由 Buoyant 公司开发(借鉴 Istio 整体架构,部分进行了优化),对抗 Istio 压力山大,也期待 Buoyant 公司的毅力。
- nginMesh(https://github.com/nginmesh/nginmesh):2017 年 9 月首发布,由 Nginx 开发,定位是作为 Istio 的服务代理,也就是替代 Envoy,思路跟 Linkerd 之前和 Istio 集成很相似,极度低调,GitHub 上的 star 也只有不到 100。
- Kong(https://github.com/Kong/kong):比 nginMesh 更加低调,默默发展中。
关于微服务和服务网格的区别,尼恩的一些理解:
- 微服务是弱入侵的服务治理框架: 业务程序 需要 依赖 服务治理框架的 注解、配置文件、api 接口,完成RPC调用、限流、熔断和监控 等工作。
- 服务网格是 无入侵的 服务治理框架: 业务程序 专注 业务逻辑,业务程序对 服务直接的 RPC调用、限流、熔断和监控 ,彻底无感知。
6.4 容器化与编排
容器化技术(如Docker)可以将应用及其依赖打包在一起,确保应用在不同环境中的一致性。
容器编排工具(如Kubernetes)则提供了自动化部署、扩展和管理容器化应用的能力,有助于提升资源利用率和系统弹性。
这部分内容,请参见尼恩的 《第28章视频:K8S学习圣经》
6.5 大型中台化架构
为解决系统重复建设、能力复用性低的问题,启动了中台化建设步伐。
中台建设并非从零开始,前期已经积累了行业中多个场景的业务和技术的中台能力。因系统建设的复杂,亟需一个中台大脑站在全局视角进行公司中台能力的梳理和建设。
DDD建模流程、设计流程是 中台化架构 的绝配。 通过引入新的建模流程,完成 中台化的宏观分析和架构建模。
Tips:
在云服务厂商的支持下,集群架构已经能够支撑较大的用户流量了。云服务器、云数据库等云端基础服务的支撑能力,也比前些年要好了很多,升级扩容也方便了许多,已经足够满足一般规模下的系统性能需求了。所以,不要觉得业务量一上来,就立马要改系统架构,因为这反而可能带来不必要的麻烦。有时,直接通过升级云服务器/云数据库等的配置,就可以解决问题了。(通常来说,常规业务场景下,通过一些优化改造,顶住1万以内的QPS是没有太大问题的)。
具体请参见尼恩的《DDD学习圣经》
看看尼恩给小伙伴出的一个 经典的技术架构图,就知道有点干货了:
7. 硬件升级
硬件升级是提升系统性能的重要手段之一,特别是在性能瓶颈由硬件限制导致的情况下,硬件升级可以带来显著的性能提升。
硬件升级可以直接影响应用的性能表现,特别是在处理大量数据和复杂计算时。
- 使用高性能硬件:升级CPU、内存和存储设备,提高应用的运行速度和响应能力。
- 利用固态硬盘(SSD):相比传统硬盘,SSD提供更快的数据读写速度,提升前端应用的加载性能。
7.1 存储设备升级
存储设备的性能直接影响数据的读写速度,升级到更快的存储设备可以显著提高系统的整体性能。
- 传统机械硬盘(HDD)升级到固态硬盘(SSD),SSD的读写速度远超HDD,可以大幅度减少数据访问时间。
- 使用NVMe接口的SSD,相比SATA接口的SSD,NVMe SSD提供更高的传输速率和更低的延迟。
7.2 内存扩展
增加系统内存可以提供更多的数据缓存空间,减少因内存不足导致的页面交换(swap),从而提高系统响应速度。
- 对于服务器和工作站,增加RAM是常见的性能提升手段,特别是在处理大量数据或运行内存密集型应用时。
7.3 CPU升级
CPU作为计算核心,其性能直接影响到处理速度。升级到更高性能的CPU可以加速计算任务的完成。
- 选择具有更多核心或更高主频的CPU,可以提升并行处理能力和单线程任务的处理速度。
7.4 网络设备升级
网络设备的性能同样影响数据传输效率,特别是在分布式系统中,高速网络设备可以减少数据传输时间。
- 升级网络接口卡(NIC)到10Gbps或更高速率,可以提高数据传输速度,减少网络延迟。
7.5 显卡升级
对于图形密集型或需要GPU加速的应用,显卡的性能至关重要。
- 升级到更高性能的GPU,可以提供更快的图形处理能力和更高效的并行计算性能。
7.6 其他硬件组件
除了上述主要硬件外,还有其他组件的升级也会影响系统性能,如电源供应器(PSU)、散热系统等。
- 高质量的电源供应器可以提供稳定的电力,避免电力问题影响系统稳定性。
- 有效的散热系统可以防止硬件过热,保持系统在最佳状态运行。
硬件升级需要考虑与现有系统的兼容性以及成本效益比,选择合适的硬件进行升级,以达到最优的性能提升效果。
8. 数据库优化
8.1 索引优化
索引是提升数据库查询性能的关键技术。合理的索引设计可以显著提高查询速度,减少数据检索时间。在索引优化中,应关注以下几点:
- 选择性索引:为查询条件中的列创建索引,特别是那些具有高选择性的列,可以快速定位到特定的数据行。
- 复合索引:在多个列上创建索引,可以提高查询效率,尤其是当查询条件中包含这些列时。
- 索引维护:定期对索引进行维护,如重建索引,以减少索引碎片,保持索引的高效性。
8.2 查询优化
查询语句的优化是提升数据库性能的直接方式。优化查询包括:
- 减少子查询:避免在查询中使用子查询,改用连接查询(JOIN)来提高性能。
- 避免SELECT :指定需要的列名,而不是使用SELECT *,减少数据的检索量。
- 使用合适的JOIN类型:根据实际情况选择合适的JOIN操作,如INNER JOIN、LEFT JOIN等。
8.3 缓存优化
缓存是减轻数据库负担的有效手段。通过缓存优化,可以:
- 使用内存缓存:如Redis或Memcached,将频繁查询的结果缓存起来,减少对数据库的直接访问。
- 本地缓存:在应用层面实现数据缓存,减少对远程数据库的请求。
8.4 硬件优化
硬件的性能直接影响数据库的处理能力。硬件优化包括:
- 升级CPU:使用多核处理器可以提高并行处理能力。
- 增加内存:更多的内存可以提供更大的缓存空间,减少磁盘I/O操作。
- 使用SSD:固态硬盘相比传统硬盘有更快的读写速度,可以大幅提升数据库性能。
8.5 数据库配置优化
合理的数据库配置可以提升系统性能:
- 调整缓冲区大小:根据系统内存和数据库使用情况,合理设置缓冲区大小。
- 并发连接数:根据服务器性能和应用需求,调整数据库的最大连接数。
8.6 分库分表
先看看数据库瓶颈是什么?
1、IO 瓶颈
第一种:磁盘读 IO 瓶颈,热点数据太多,数据库缓存放不下,每次查询时会产生大量的 IO,降低查询速度 -> 分库和垂直分表。
第二种:网络 IO 瓶颈,请求的数据太多,网络带宽不够 -> 分库。
2、CPU 瓶颈
第一种CPU 瓶颈:SQL 问题,如 SQL 中包含 join,group by,order by,非索引字段条件查询等,增加 CPU 运算的操作 -> SQL 优化,建立合适的索引,在业务 Service 层进行业务计算。
第二种CPU 瓶颈:单表数据量太大,查询时扫描的行太多,SQL 效率低,CPU 率先出现瓶颈 -> 水平分表。
不管是 IO 瓶颈,还是 CPU 瓶颈,最终都会导致数据库的活跃连接数增加,进而逼近甚至达到数据库可承载活跃连接数的阈值。
对于大数据量的数据库,分库分表是一种有效的优化手段:
比如: 可以按照水平、垂直两大维度进行拆分:
- 垂直分割:将表中的列分成不同的部分,存储在不同的表或数据库中。
- 水平分割:按照某种规则,将表中的数据行分布到多个表或数据库中。
比如:可以按业务功能拆分数据库,如:用户数据、订单数据......等分别存储。
也可以,将同一表的数据按一定规则分散到多个表中,比如:
- 按范围分表:根据某一字段的范围进行拆分,如:按日期、ID范围...等。
- 按哈希分表:根据某一字段的哈希值进行拆分,保证数据均匀分布。
8.7 读写分离
通过读写分离,可以将查询操作和更新操作分散到不同的数据库服务器:
- 主从复制:使用主数据库处理写操作,从数据库处理读操作。
- 负载均衡:通过负载均衡技术,合理分配读操作到各个从数据库。
读写分离旨在通过将读操作、和写操作分开,是一种常见的优化方案, 大致流程 如下图所示:
大致的原理,如下:
- 主库(Master):负责处理所有的写操作(比如:插入、更新、删除......)、和写操作相关的事务;
- 从库(Slave):负责处理读操作(查询),通过主从复制机制从主库同步数据;
- 复制机制:主库将数据更改记录到二进制日志(binlog),从库读取并执行这些日志中的操作,以保持与主库的数据一致性。
8.8 索引重建、适当反范式化、批量执行
在MySQL中,通过索引重建、适当反范式化、批量执行等技术手段,可以显著提升SQL的执行效率。以下是详细的说明和示例代码。
1. 索引重建
重建索引可以提高查询性能,特别是在数据频繁变动导致索引碎片化的情况下。
示例代码
sql复制代码-- 重建指定表上的所有索引
ALTER TABLE table_name ENGINE=InnoDB;
-- 重建指定索引
ALTER TABLE table_name DROP INDEX index_name, ADD INDEX index_name (column_name);
2. 适当反范式化
适度的反范式化可以减少复杂的连接操作,从而提高查询效率。
示例代码
假设我们有一个标准化的数据库结构:
-- 用户表
CREATE TABLE users (
user_id INT AUTO_INCREMENT PRIMARY KEY,
user_name VARCHAR(100)
);
-- 订单表
CREATE TABLE orders (
order_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
order_total DECIMAL(10, 2),
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
我们可以通过反范式化来提高查询性能:
-- 反范式化后的订单表
CREATE TABLE orders (
order_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
user_name VARCHAR(100),
order_total DECIMAL(10, 2)
);
3. 批量执行
批量执行可以减少SQL语句的开销,提高数据处理效率。特别是在插入大量数据时,使用批量插入能显著提高性能。
示例代码
-- 批量插入示例
INSERT INTO orders (user_id, user_name, order_total) VALUES
(1, 'Alice', 99.99),
(2, 'Bob', 49.99),
(3, 'Charlie', 19.99);
8.9 定期维护
定期对数据库进行维护,包括:
- 数据清理:定期清理无用数据,释放存储空间。
- 索引重建:定期重建索引,避免索引碎片化。
8.10 监控与分析
通过监控和分析数据库性能,及时发现并解决问题:
- 性能监控:使用工具监控数据库的运行状态,如CPU使用率、内存使用率、磁盘I/O等。
- 慢查询日志:分析慢查询日志,找出性能瓶颈并进行优化。
9 过载保护优化
过载保护架构是另一种确保系统稳定性和可靠性的机制,特别是在面对高流量或异常流量时。过载保护通常包括:
- 限流(Rate Limiting):对进入系统的请求数量进行限制,以防止系统过载。
- 熔断(Circuit Breaker):当系统检测到一定数量的连续错误或异常时,自动断开服务的某些部分,以防止问题扩散。
- 快速失败(Fail Fast):在检测到严重问题时,系统快速失败并通知用户,而不是尝试继续执行可能导致更多问题的复杂操作。
- 降级服务(Service Degradation):在系统负载过高时,有选择地降低某些非关键服务的优先级或质量,以保证关键服务的正常运行。
- 资源隔离:将关键资源与非关键资源隔离,确保关键操作即使在高负载下也能获得所需资源。
- 动态扩缩容:根据系统负载动态调整资源分配,如自动增加服务器数量或资源分配。
在设计链路保护和过载保护架构时,需要综合考虑系统的业务需求、性能指标、成本和复杂性等因素,以实现最佳的保护效果。
10. 度量与监控系统
度量与监控系统是确保系统性能优化方案有效实施的关键环节。以下是对10大性能优化方案的度量与监控系统的详解:
10.1 度量指标的确立
确立度量指标是监控系统的第一步。需要根据性能优化方案的特点,选择合适的度量指标,如响应时间、吞吐量、资源利用率等。
10.2 监控工具的选择
选择合适的监控工具对于数据的准确收集至关重要。常用的监控工具包括但不限于Prometheus、Zabbix、Nagios等。
这个请参考尼恩的:Prometheus圣经的上下两部分
- prometheus学习圣经Part1:使用prometheus+Grafana实现指标监控
- prometheus学习圣经Part2:使用prometheus+Alertmanager实现指标预警
完整的PDF,请在 技术自由圈 公众号获取。
10.3 数据采集与分析
数据采集是度量性能的基础。通过自动化工具定期采集性能数据,并进行深入分析,以识别性能瓶颈。
10.4 实时监控与告警
实现实时监控,当性能指标超出预设阈值时,系统应能自动发出告警,以便及时采取措施。
10.5 可视化展示
通过可视化手段,如Grafana等工具,将监控数据以图表的形式展现出来,使性能状况一目了然。
10.6 性能基线的建立
建立性能基线,用于衡量性能优化前后的变化,评估优化方案的效果。
10.7 历史数据的存储与对比
存储历史监控数据,用于长期趋势分析和问题回溯。
10.8 度量结果的应用
将度量结果应用于持续的性能优化过程中,形成闭环优化机制。
10.9 监控系统的可靠性与健壮性
确保监控系统本身的稳定性和可靠性,避免因监控系统故障而影响性能优化工作。
10.10 性能优化方案的综合评估
定期对性能优化方案进行综合评估,结合度量与监控结果,不断调整和完善优化策略。
总结:架构学问,也是艺术
架构师是学问,也是艺术。
架构师学问,这里架构构师至少需要掌握网络知识,硬件,软件,架构理论、架构哲学等方方面面的知识:
1.硬件知识。CPU/硬盘/内存/物理网络
2.软件知识。操作系统/数据库/应用服务器...。
3.通讯协议。TCP/IP/HTTP/MQTT....。
4.分布式知识。
架构知识。 比如说尼恩的3高架构知识图谱。
架构哲学。 比如尼恩的架构师哲学。
意志坚强。但不偏执。
善于沟通。但不花言巧语。
尼恩写的一系列架构文章,已经帮助很多小伙伴拿到了大厂offer,大家也可以收藏起来, 作为 架构的参考资料。
3万长文 秒杀圣经 : 16大绝招,完成10Wqps秒杀架构
百亿级存储架构: ElasticSearch+HBase 海量存储架构与实现
说在最后:有问题找老架构取经
如何做性能优化? 以上的内容,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。
最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。
很多小伙伴刷完后, 吊打面试官, 大厂横着走。
在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会,可以找尼恩来改简历、做帮扶。
遇到职业难题,找老架构取经, 可以省去太多的折腾,省去太多的弯路。
尼恩指导了大量的小伙伴上岸,前段时间,刚指导一个40岁+被裁小伙伴,拿到了一个年薪100W的offer。
尼恩技术圣经系列PDF
- 《NIO圣经:一次穿透NIO、Selector、Epoll底层原理》
- 《Docker圣经:大白话说Docker底层原理,6W字实现Docker自由》
- 《K8S学习圣经:大白话说K8S底层原理,14W字实现K8S自由》
- 《SpringCloud Alibaba 学习圣经,10万字实现SpringCloud 自由》
- 《大数据HBase学习圣经:一本书实现HBase学习自由》
- 《大数据Flink学习圣经:一本书实现大数据Flink自由》
- 《响应式圣经:10W字,实现Spring响应式编程自由》
- 《Go学习圣经:Go语言实现高并发CRUD业务开发》
……完整版尼恩技术圣经PDF集群,请找尼恩领取
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓