科普文:后端性能优化的实战小结
一、背景与效果
ICBU的核心沟通场景有了10年的“积累”,核心场景的界面响应耗时被拉的越来越长,也让性能优化工作提上了日程,先说结论,经过这一波前后端齐心协力的优化努力,两个核心界面90分位的数据,FCP平均由2.6s下降到1.9s,LCP平均由2.8s下降到2s。
本文主要着眼于服务端在此次性能优化过程中做的工作,供大家参考讨论。
二、措施一:流式分块传输(核心)
2.1. HTTP分块传输介绍
分块传输编码(Chunked Transfer Encoding)是一种HTTP/1.1协议中的数据传输机制,它允许服务器在不知道整个内容大小的情况下,就开始传输动态生成的内容。这种机制特别适用于生成大量数据或者由于某种原因数据大小未知的情况。
在分块传输编码中,数据被分为一系列的“块”(chunk)。每一个块都包括一个长度标识(以十六进制格式表示)和紧随其后的数据本身,然后是一个CRLF(即"\r\n",代表回车和换行)来结束这个块。块的长度标识会告诉接收方这个块的数据部分有多长,使得接收方可以知道何时结束这一块并准备好读取下一块。
当所有数据都发送完毕时,服务器会发送一个长度为零的块,表明数据已经全部发送完毕。零长度块后面可能会跟随一些附加的头部信息(尾部头部),然后再用一个CRLF来结束整个消息体。
我们可以借助分块传输协议完成对切分好的vm进行分块推送,从而达到整体HTML界面流式渲染的效果,在实现时,只需要对HTTP的header进行改造即可:
public void chunked(HttpServletRequest request, HttpServletResponse response) {
try (PrintWriter writer = response.getWriter()) {
// 设置响应类型和编码
oriResponse.setContentType(MediaType.TEXT_HTML_VALUE + ";charset=UTF-8");
oriResponse.setHeader("Transfer-Encoding", "chunked");
oriResponse.addHeader("X-Accel-Buffering", "no");
// 第一段
Context modelMain = getmessengerMainContext(request, response, aliId);
flushVm("/velocity/layout/Main.vm", modelMain, writer);
// 第二段
Context modelSec = getmessengerSecondContext(request, response, aliId, user);
flushVm("/velocity/layout/Second.vm", modelSec, writer);
// 第三段
Context modelThird = getmessengerThirdContext(request, response, user);
flushVm("/velocity/layout/Third.vm", modelThird, writer);
} catch (Exception e) {
// logger
}
}
private void flushVm(String templateName, Context model, PrintWriter writer) throws Exception {
StringWriter tmpWri = new StringWriter();
// vm渲染
engine.mergeTemplate(templateName, "UTF-8", model, tmpWri);
// 数据写出
writer.write(tmpWri.toString());
writer.flush();
}
2.2. 页面流式分块传输优化方案
我们现在的大部分应用都是springmvc架构,浏览器发起请求,后端服务器进行数据准备与vm渲染,之后返回html给浏览器。
从请求到达服务端开始计算,一次HTML请求到页面加载完全要经过网络请求、网络传输与前端资源渲染三个阶段:
HTML流式输出,思路是对HTML界面进行拆分,之后由服务器分批进行推送,这样做有两个好处:
-
服务端分批进行数据准备,可以减少首次需要准备的数据量,极大缩短准备时间。
-
浏览器分批接收数据,当接收到第一部分的数据时,可以立刻进行js渲染,提升其利用率。
这个思路对需要加载资源较多的页面有很明显的效果,在我们此次的界面优化中,页面的FCP与LCP均有300ms-400ms的性能提升,在进行vm界面的数据拆分时,有以下几个技巧:
-
注意界面资源加载的依赖关系,前序界面不能依赖后序界面的变量。
-
将偏静态与核心的资源前置,后端服务器可以快速完成数据准备并返回第一段html供前端加载。
2.3. 注意事项
此次优化的应用与界面本身历史包袱很重,在进行流式改造的过程中,我们遇到了不少的阻力与挑战,在解决问题的过程也学到了很多东西,这部分主要对遇到的问题进行整理。
-
二方包或自定义的HTTP请求 filter 会改写 response 的 header,导致分块传输失效。如果应用中有这种情况,我们在进行流式推送时,可以获取到最原始的response,防止被其他filter影响:
/**
* 防止filter或者其他代理包装了response并开启缓存
* 这里获取到真实的response
*
* @param response
* @return
*/
private static HttpServletResponse getResponse(HttpServletResponse response) {
ServletResponse resp = response;
while (resp instanceof ServletResponseWrapper) {
ServletResponseWrapper responseWrapper = (ServletResponseWrapper) resp;
resp = responseWrapper.getResponse();
}
return (HttpServletResponse) resp;
}
-
谷歌浏览器禁止跨域名写入cookie,我们的应用界面会以iframe的形式嵌入其他界面,谷歌浏览器正在逐步禁止跨域名写cookie,如下所示:
为了确保cookie能正常写入,需要指定cookie的SameSite=None。
-
VelocityEngine模板引擎的自定义tool。
我们的项目中使用的模板引擎为VelocityEngine,在流式分块传输时,需要手动渲染vm:
private void flushVm(String templateName, Context model, PrintWriter writer) throws Exception {
StringWriter tmpWri = new StringWriter();
// vm渲染
engine.mergeTemplate(templateName, "UTF-8", model, tmpWri);
// 数据写出
writer.write(tmpWri.toString());
writer.flush();
}
需要注意的是VelocityEngine模板引擎支持自定义tool,在vm文件中是如下的形式,当vm引擎渲染到对应位置时,会调用配置好的方法进行解析:
<title>$tool.do("xx", "$!{arg}")</title>
如果用注解的形式进行vm渲染,框架本身会帮我们自动做tools的初始化。但如果我们想手动渲染vm,那么需要将这些tools初始化到context中:
/**
* 初始化 toolbox.xml 中的工具
*/
private Context initContext(HttpServletRequest request, HttpServletResponse response) {
ViewToolContext viewToolContext = null;
try {
ServletContext servletContext = request.getServletContext();
viewToolContext = new ViewToolContext(engine, request, response, servletContext);
VelocityToolsRepository velocityToolsRepository = VelocityToolsRepository.get(servletContext);
if (velocityToolsRepository != null) {
viewToolContext.putAll(velocityToolsRepository.getTools());
}
} catch (Exception e) {
LOGGER.error("createVelocityContext error", e);
return null;
}
}
对于比较古老的应用,VelocityToolsRepository需要将二方包版本进行升级,而且需要注意,velocity-spring-boot-starter升级后可能存在tool.xml文件失效的问题,建议可以采用注解的形式实现tool,并且注意tool对应java类的路径。
@DefaultKey("assetsVersion")
public class AssertsVersionTool extends SafeConfig {
public String get(String key) {
return AssetsVersionUtil.get(key);
}
}
-
Nginx 的 location 配置
server {
location ~ ^/chunked {
add_header X-Accel-Buffering no;
proxy_http_version 1.1;
proxy_cache off; # 关闭缓存
proxy_buffering off; # 关闭代理缓冲
chunked_transfer_encoding on; # 开启分块传输编码
proxy_pass http://backends;
}
}
-
ngnix配置本身可能存在对流式输出的不兼容,这个问题是很难枚举的,我们遇到的问题是如下配置,需要将SC_Enabled关闭。
SC_Enabled on;
SC_AppName gangesweb;
SC_OldDomains //b.alicdn.com;
SC_NewDomains //b.alicdn.com;
SC_OldDomains //bg.alicdn.com;
SC_NewDomains //bg.alicdn.com;
SC_FilterCntType text/html;
SC_AsyncVariableNames asyncResource;
SC_MaxUrlLen 1024;
详见:https://github.com/dinic/styleCombine3
-
ngnix缓冲区大小,在我们优化的过程中,某个应用并没有指定缓冲区大小,取的默认值,我们的改造导致http请求的header变大了,导致报错upstream sent too big header while reading response header from upstream
proxy_buffers 128 32k;
proxy_buffer_size 64k;
proxy_busy_buffers_size 128k;
client_header_buffer_size 32k;
large_client_header_buffers 4 16k;
如果页面在浏览器上有问题时,可以通过curl命令在服务器上直接访问,排查是否为ngnix的问题:
curl --trace - 'http://127.0.0.1:7001/chunked' \
-H 'cookie: xxx'
-
ThreadLocal与StreamingResponseBody
在开始,我们使用StreamingResponseBody来实现的分块传输:
@GetMapping("/chunked")
public ResponseEntity<StreamingResponseBody> streamChunkedData() {
StreamingResponseBody stream = outputStream -> {
// 第一段
Context modelMain = getmessengerMainContext(request, response, aliId);
flushVm("/velocity/layout/Main.vm", modelMain, writer);
// 第二段
Context modelSec = getmessengerSecondContext(request, response, aliId, user);
flushVm("/velocity/layout/Second.vm", modelSec, writer);
// 第三段
Context modelThird = getmessengerThirdContext(request, response, user);
flushVm("/velocity/layout/Third.vm", modelThird, writer);
}
};
return ResponseEntity.ok()
.contentType(MediaType.TEXT_HTML)
.body(stream);
}
}
但是我们在运行时发现vm的部分变量会渲染失败,卡点了不少时间,后面在排查过程中发现应用在处理http请求时会在ThreadLocal中进行用户数据、request数据与部分上下文的存储,而后续vm数据准备时,有一部分数据是直接从中读取或者间接依赖的,而StreamingResponseBody本身是异步的(可以看如下的代码注释),这就导致新开辟的线程读不到原线程ThreadLocal的数据,进而渲染错误:
/**
* A controller method return value type for asynchronous request processing
* where the application can write directly to the response {@code OutputStream}
* without holding up the Servlet container thread.
*
* <p><strong>Note:</strong> when using this option it is highly recommended to
* configure explicitly the TaskExecutor used in Spring MVC for executing
* asynchronous requests. Both the MVC Java config and the MVC namespaces provide
* options to configure asynchronous handling. If not using those, an application
* can set the {@code taskExecutor} property of
* {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
* RequestMappingHandlerAdapter}.
*
* @author Rossen Stoyanchev
* @since 4.2
*/
@FunctionalInterface
public interface StreamingResponseBody {
/**
* A callback for writing to the response body.
* @param outputStream the stream for the response body
* @throws IOException an exception while writing
*/
void writeTo(OutputStream outputStream) throws IOException;
}
三、措施二:非流量中间件优化
在性能优化过程中,我们发现在流量高峰期,某个服务接口的平均耗时会显著升高,结合arths分析发现,是由于在流量高峰期,对于配置中心的调用被限流了。原因是配置中心的使用不规范,每次都是调用getConfig方法从配置中心服务端拉取的数据。
在读取配置中心的配置时,更标准的使用方法是由配置中心主动推送变更,客户端监听配置信息缓存到本地,这样,每次读取配置其实读取的是机器的本地缓存,可以参考如下的方式:
public static void registerDynamicConfig(final String dataIdKey, final String groupName) {
IOException initError = null;
try {
String e = Diamond.getConfig(dataIdKey, groupName, DEFAULT_TIME_OUT);
if(e != null) {
getGroup(groupName).put(dataIdKey, e);
}
logger.info("Diamond config init: dataId=" + dataIdKey + ", groupName=" + groupName + "; initValue=" + e);
} catch (IOException e) {
logger.error("Diamond config init error: dataId=" + dataIdKey, e);
initError = e;
}
Diamond.addListener(dataIdKey, groupName, new ManagerListener() {
@Override
public Executor getExecutor() {
return null;
}
@Override
public void receiveConfigInfo(String s) {
String oldValue = (String)DynamicConfig.getGroup(groupName).get(dataIdKey);
DynamicConfig.getGroup(groupName).put(dataIdKey, s);
DynamicConfig.logger.warn(
"Receive config update: dataId=" + dataIdKey + ", newValue=" + s + ", oldValue=" + oldValue);
}
});
if(initError != null) {
throw new RuntimeException("Diamond config init error: dataId=" + dataIdKey, initError);
}
}
四、措施三:数据直出
-
静态图片直出,页面上有静态的loge图片,原本为cdn地址,在浏览器渲染时,需要建联并会抢占线程,对于这类不会发生发生变化的图片,可以直接替换为base64的形式,js可以直接加载。
-
加载数据直出,这部分需要根据具体业务来分析,部分业务数据是浏览器运行js脚本在本地二次请求加载的,由于低端机以及本地浏览器的能力限制,如果需要加载的数据很多,就很导致js线程的挤占,拖慢整体的时间,因此,可以考虑在服务器将部分数据预先加载好,随http请求一起给浏览器,减少这部分的卡点。
数据直出有利有弊,对于页面的加载性能有正向影响的同时,也会同时导致HTTP的response增大以及服务端RT的升高。数据直出与流式分块传输相结合的效果可能会更好,当服务端分块响应HTTP请求时,本身的response就被切割成多块,单次大小得到了控制,流式分块传输下,服务端分批执行数据准备的策略也能很好的缓冲RT增长的问题。
五、措施四:本地缓存
以我们遇到的一个问题为例,我们的云盘文件列表需要在后端准备好文件所属人的昵称,这是在后端服务器由用户id调用会员的rpc接口实时查询的。分析这个场景,我们不难发现,同一时间,IM场景下的文件所属人往往是其中归属在聊天的几个人名下的,因此,可以利用HashMap作为缓存rpc查询到的会员昵称,避免重复的查询与调用。
六、措施五:下线历史债务
针对有历史包袱的应用,历史债务导致的额外耗时往往很大,这些历史代码可能包括以下几类:
-
未下线的实验或者分流接口调用;
-
-
时间线拉长,这部分的代码残骸在所难免,而且积少成多,累计起来往往有几十上百毫秒的资源浪费,再加上业务开发时,大家往往没有额外资源去评估这部分的很多代码是否可以下线,因此可以借助性能优化的契机进行治理。
-
-
已经废弃的vm变量与重复变量治理。
-
-
对vm变量的盘点过程中发现有很多之前在使用但现在已经废弃的变量。当然,这部分变量的需要前后端同学共同梳理,防止下线线上依旧依赖的变量。
-
相关文章:

科普文:后端性能优化的实战小结
一、背景与效果 ICBU的核心沟通场景有了10年的“积累”,核心场景的界面响应耗时被拉的越来越长,也让性能优化工作提上了日程,先说结论,经过这一波前后端齐心协力的优化努力,两个核心界面90分位的数据,FCP平…...
LeetCode-day23-3098. 求出所有子序列的能量和
LeetCode-day23-3098. 求出所有子序列的能量和 题目描述示例示例1:示例2:示例3: 思路代码 题目描述 给你一个长度为 n 的整数数组 nums 和一个 正 整数 k 。 一个 子序列的 能量 定义为子序列中 任意 两个元素的差值绝对值的 最小值 。 请…...

CSS3雷达扫描效果
CSS3雷达扫描效果https://www.bootstrapmb.com/item/14840 要创建一个CSS3的雷达扫描效果,我们可以使用CSS的动画(keyframes)和transform属性。以下是一个简单的示例,展示了如何创建一个类似雷达扫描的动画效果: HTM…...
单例模式懒汉模式和饿汉模式
线程安全 单例模式在单线程中,当然是安全的。但是如果在多线程中,由于并行判断,可能会导致创建多个实例。那么如何保证在多线程中单例还是只有一个实例呢? 常见的三种方式: 局部静态变量 原理和饿汉模式相似,利用static只会初始…...
python __repr__和__str__区别
1. __repr__ __repr__ 方法由 repr() 内置函数调用,用于计算对象的“正式”字符串表示形式。理想情况下,这个字符串应该看起来像一个有效的 Python 表达式,可以在适当的环境下用来重新创建具有相同值的对象。如果这不可能实现,那…...

huawei USG6001v1学习----NAT和智能选路
目录 1.NAT的分类 2.智能选路 1.就近选路 2.策略路由 3.智能选路 NAT:(Network Address Translation,网络地址转换) 指网络地址转换,1994年提出的。NAT是用于在本地网络中使用私有地址,在连接互联网时转而使用全局…...

FPGA JTAG最小系统 EP2C5T144C8N
FPGA的文档没有相应的基础还真不容易看懂,下面是B站上对FPGA文档的解读(本文非对文档解读,只是为个人记录第三期:CycloneIV E最小系统板设计(一)从Datasheet上获取FPGA的基本参数_哔哩哔哩_bilibili 电源部份 核心电…...

Android 15 之如何快速适配 16K Page Size
在此之前,我们通过 《Android 15 上 16K Page Size 为什么是最坑》 介绍了: 什么是16K Page Size为什么它对于 Android 很坑如何测试 如果你还没了解,建议先去了解下前文,然后本篇主要是提供适配的思路,因为这类适配…...
学习unity官方的网络插件Netcode【一】
对bool值的个人理解: using Unity.Netcode; using UnityEngine; //个人理解:通过Rpc完成了一次客户端给服务端发消息,服务端再向所有客户端广播消息 public class RpcTest : NetworkBehaviour {public override void OnNetworkSpawn(){if (!…...

QT写一个mainWindow
切换风格的写法: 先看看样式效果: mian_window.h文件 #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow>class MainWindow : public QMainWindow {Q_OBJECTpublic:MainWindow(QWidget *parent nullptr);~MainWindow();void Ini…...
Java查找算法练习(2024.7.23)
顺序查找 package SearchExercise20240723; import java.util.Scanner; public class SearchExercise {public static void main(String[] args) {Scanner sc new Scanner(System.in);System.out.println("需要多大的数组?");int size sc.nextInt();int[] array …...

洗地机哪个牌子好?四款口碑最好的洗地机排名推荐
随着“懒人经济”的出现,越来越多的人开始使用洗地机。洗地机哪个牌子好?为了帮助大家在这个琳琅满目的市场中做出明智决策,本文特别整理了四款口碑最好的洗地机排名推荐,它们凭借出色的清洁效果、智能化的操作体验以及用户的高度…...

如何提升短视频的曝光量和获客效能?云微客来解决
在流量至上的当下,短视频凭借其优势,迅速成为了众多企业获客引流的核心营销手段。进入短视频赛道后,如何提升短视频的曝光量和获客效能,就成为了众多企业亟待解决的焦点。 如果你不想投入大量的广告预算,还想在短视频平…...
SpringBoot开发中如何缓存数据, 减少数据库的访问频率?
一:自定义是否开启缓存 方法一: 在不同环境的配置文件中如application-dev.yml、application-test.yml、application-prod.yml,修改 spring.cache.type none; spring:cache:type: none 方法二: 自定义配置 application.yml&…...
PostgreSQL如何在windows/linux开启归档
linux开启归档: archive_mode onarchive_command test ! -f /mnt/pg12/archivedir/%f && cp %p /mnt/pg12/archivedir/%fwindows开启归档: archive_mode onarchive_command copy "%p" "C:\\server\\pg12\\archivedir\\%f&q…...

【启明智显分享】基于国产Model3芯片的7寸触摸屏助力智慧医疗,电子床头屏提升护理交互
未来医院必然是以信息化为基础,以物联网为特征,以医疗为核心的服务型医院。病房作为医院的重要服务场所,成为智慧医院建设的重要一环。 为提高医护人员与患者的互动交流,给医疗注入智慧元素,让患者享受智能服务&#…...

从理论到实践:如何用 TDengine 打造完美数据模型
在用 TDengine 进行数据建模之前,我们需要回答两个关键问题:建模的目标用户是谁?他们的具体需求是什么?在一个典型的时序数据管理方案中,数据采集和数据应用是两个主要环节。如下图所示: 对于数据采集工程师…...

可以免费合并pdf的软件 合并pdf文件的软件免费 合并pdf的软件免费
在数字化办公的今天,pdf格式因其稳定性和跨平台兼容性被广泛使用。然而,当我们需要将多个 pdf 文件合并为一个时,却往往感到力不从心。本文将为你介绍几款强大的pdf文件合并软件,让你轻松管理文档。 方法一、使用pdf转换器 步骤1…...

【排序 滑动窗口 】1498. 满足条件的子序列数目
本文涉及至知识点 排序 C算法:滑动窗口总结 LeetCode1498. 满足条件的子序列数目 给你一个整数数组 nums 和一个整数 target 。 请你统计并返回 nums 中能满足其最小元素与最大元素的 和 小于或等于 target 的 非空 子序列的数目。 由于答案可能很大,…...
RabbitMQ普通集群搭建指南
RabbitMQ普通集群搭建指南 本文已经完全迁移至,www.geekery.cn 后续不在此更新 目标架构 本次搭建的目标是构建一个由三个节点组成的RabbitMQ集群,节点信息如下: rabbit02: IP地址 192.168.10.132rabbit03: IP地址 192.168.10.133rabbit04:…...

日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻
在如今就业市场竞争日益激烈的背景下,越来越多的求职者将目光投向了日本及中日双语岗位。但是,一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧?面对生疏的日语交流环境,即便提前恶补了…...

C++实现分布式网络通信框架RPC(3)--rpc调用端
目录 一、前言 二、UserServiceRpc_Stub 三、 CallMethod方法的重写 头文件 实现 四、rpc调用端的调用 实现 五、 google::protobuf::RpcController *controller 头文件 实现 六、总结 一、前言 在前边的文章中,我们已经大致实现了rpc服务端的各项功能代…...
基于大模型的 UI 自动化系统
基于大模型的 UI 自动化系统 下面是一个完整的 Python 系统,利用大模型实现智能 UI 自动化,结合计算机视觉和自然语言处理技术,实现"看屏操作"的能力。 系统架构设计 #mermaid-svg-2gn2GRvh5WCP2ktF {font-family:"trebuchet ms",verdana,arial,sans-…...
Spring Boot 实现流式响应(兼容 2.7.x)
在实际开发中,我们可能会遇到一些流式数据处理的场景,比如接收来自上游接口的 Server-Sent Events(SSE) 或 流式 JSON 内容,并将其原样中转给前端页面或客户端。这种情况下,传统的 RestTemplate 缓存机制会…...
Oracle查询表空间大小
1 查询数据库中所有的表空间以及表空间所占空间的大小 SELECTtablespace_name,sum( bytes ) / 1024 / 1024 FROMdba_data_files GROUP BYtablespace_name; 2 Oracle查询表空间大小及每个表所占空间的大小 SELECTtablespace_name,file_id,file_name,round( bytes / ( 1024 …...
可靠性+灵活性:电力载波技术在楼宇自控中的核心价值
可靠性灵活性:电力载波技术在楼宇自控中的核心价值 在智能楼宇的自动化控制中,电力载波技术(PLC)凭借其独特的优势,正成为构建高效、稳定、灵活系统的核心解决方案。它利用现有电力线路传输数据,无需额外布…...

visual studio 2022更改主题为深色
visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中,选择 环境 -> 常规 ,将其中的颜色主题改成深色 点击确定,更改完成...
FastAPI 教程:从入门到实践
FastAPI 是一个现代、快速(高性能)的 Web 框架,用于构建 API,支持 Python 3.6。它基于标准 Python 类型提示,易于学习且功能强大。以下是一个完整的 FastAPI 入门教程,涵盖从环境搭建到创建并运行一个简单的…...
Golang dig框架与GraphQL的完美结合
将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用,可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器,能够帮助开发者更好地管理复杂的依赖关系,而 GraphQL 则是一种用于 API 的查询语言,能够提…...
三体问题详解
从物理学角度,三体问题之所以不稳定,是因为三个天体在万有引力作用下相互作用,形成一个非线性耦合系统。我们可以从牛顿经典力学出发,列出具体的运动方程,并说明为何这个系统本质上是混沌的,无法得到一般解…...