科普文:后端性能优化的实战小结

一、背景与效果

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. 注意事项

此次优化的应用与界面本身历史包袱很重,在进行流式改造的过程中,我们遇到了不少的阻力与挑战,在解决问题的过程也学到了很多东西,这部分主要对遇到的问题进行整理。

  1. 二方包或自定义的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;}
  1. 谷歌浏览器禁止跨域名写入cookie,我们的应用界面会以iframe的形式嵌入其他界面,谷歌浏览器正在逐步禁止跨域名写cookie,如下所示:

图片

为了确保cookie能正常写入,需要指定cookie的SameSite=None。

  1. 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);    }}
  1. 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;    } }
  1. 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

  1. 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'
  1. 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 */@FunctionalInterfacepublic 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);    }}

四、措施三:数据直出

  1. 静态图片直出,页面上有静态的loge图片,原本为cdn地址,在浏览器渲染时,需要建联并会抢占线程,对于这类不会发生发生变化的图片,可以直接替换为base64的形式,js可以直接加载。

  2. 加载数据直出,这部分需要根据具体业务来分析,部分业务数据是浏览器运行js脚本在本地二次请求加载的,由于低端机以及本地浏览器的能力限制,如果需要加载的数据很多,就很导致js线程的挤占,拖慢整体的时间,因此,可以考虑在服务器将部分数据预先加载好,随http请求一起给浏览器,减少这部分的卡点。

数据直出有利有弊,对于页面的加载性能有正向影响的同时,也会同时导致HTTP的response增大以及服务端RT的升高。数据直出与流式分块传输相结合的效果可能会更好,当服务端分块响应HTTP请求时,本身的response就被切割成多块,单次大小得到了控制,流式分块传输下,服务端分批执行数据准备的策略也能很好的缓冲RT增长的问题。

五、措施四:本地缓存

以我们遇到的一个问题为例,我们的云盘文件列表需要在后端准备好文件所属人的昵称,这是在后端服务器由用户id调用会员的rpc接口实时查询的。分析这个场景,我们不难发现,同一时间,IM场景下的文件所属人往往是其中归属在聊天的几个人名下的,因此,可以利用HashMap作为缓存rpc查询到的会员昵称,避免重复的查询与调用。

六、措施五:下线历史债务

针对有历史包袱的应用,历史债务导致的额外耗时往往很大,这些历史代码可能包括以下几类:

  • 未下线的实验或者分流接口调用;

    • 时间线拉长,这部分的代码残骸在所难免,而且积少成多,累计起来往往有几十上百毫秒的资源浪费,再加上业务开发时,大家往往没有额外资源去评估这部分的很多代码是否可以下线,因此可以借助性能优化的契机进行治理。

  • 已经废弃的vm变量与重复变量治理。

    • 对vm变量的盘点过程中发现有很多之前在使用但现在已经废弃的变量。当然,这部分变量的需要前后端同学共同梳理,防止下线线上依旧依赖的变量。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/48931.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Day05-readinessProbe探针,startupProbe探针,Pod生命周期,静态Pod,初始化容器,rc控制器的升级和回滚,rs控制器精讲

Day05-readinessProbe探针&#xff0c;startupProbe探针&#xff0c;Pod生命周期&#xff0c;静态Pod&#xff0c;初始化容器&#xff0c;rc控制器的升级和回滚&#xff0c;rs控制器精讲 0、昨日内容回顾1、readinessProbe可用性检查探针之exec案例2、可用性检查之httpGet案例3…

[数据集][目标检测]躺坐站识别检测数据集VOC+YOLO格式9488张3类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;9488 标注数量(xml文件个数)&#xff1a;9488 标注数量(txt文件个数)&#xff1a;9488 标注…

C语言 | Leetcode C语言题解之第242题有效的字母异位词

题目&#xff1a; 题解&#xff1a; bool isAnagram(char* s, char* t) {int len_s strlen(s), len_t strlen(t);if (len_s ! len_t) {return false;}int table[26];memset(table, 0, sizeof(table));for (int i 0; i < len_s; i) {table[s[i] - a];}for (int i 0; i &…

EMQX 跨域集群:增强可扩展性,打破地域限制

跨域集群的概念 提到 EMQX&#xff0c;人们通常首先会想到它的可扩展性。尽管 EMQX 能随着硬件数量的增加几乎实现线性扩展&#xff0c;但在单个计算实例上的扩展能力终究有限&#xff1a;资源总会耗尽&#xff0c;升级成本也会急剧上升。这时&#xff0c;分布式部署就显得尤为…

JavaScript(11)——对象

对象 声明&#xff1a; let 对象名 { 属性名&#xff1a;属性值, 方法名&#xff1a;函数 } let 对象名 new Object() 对象的操作 先创建一个对象 let op {name:jvav,id:4,num:1001} 查 对象名.属性 console.log(op.name) 对象名[属性名] 改 对象名.属性 新值 op.name …

Pytorch学习笔记day4——训练mnist数据集和初步研读

该来的还是来了hhhhhhhhhh&#xff0c;基本上机器学习的初学者都躲不开这个例子。开源&#xff0c;数据质量高&#xff0c;数据尺寸整齐&#xff0c;问题简单&#xff0c;实在太适合初学者食用了。 今天把代码跑通&#xff0c;趁着周末好好的琢磨一下里面的各种细节。 代码实…

Spring MVC的高级功能——拦截器(三)拦截器的执行流程

一、单个拦截器的执行流程 如果在项目中只定义了一个拦截器&#xff0c;单个拦截器的执行流程如图所示。 二、单个拦截器的执行流程分析 从单个拦截器的执行流程图中可以看出&#xff0c;程序收到请求后&#xff0c;首先会执行拦截器中的preHandle()方法&#xff0c;如果preHa…

bug诞生记——动态库加载错乱导致程序执行异常

大纲 背景问题发生问题猜测和分析过程是不是编译了本工程中的其他代码是不是有缓存是不是编译了非本工程的文件是不是调用了其他可执行文件查看CMakefiles分析源码检查正在运行程序的动态库 解决方案 这个案例发生在我研究ROS 2的测试Demo时发生的。 整体现象是&#xff1a;修改…

聊一聊前端动画的种类,以及动画的触发方式有哪些?

引言 动画在前端开发中扮演着重要的角色。它不仅可以提升用户体验&#xff0c;还可以使界面更加生动和有趣。在这篇文章中&#xff0c;我们将深入探讨前端动画的各种实现方式&#xff0c;包括 CSS 动画、JavaScript 动画、SVG 动画等。我们还将讨论一些触发动画的方式和动画在…

【MQTT(2)】开发一个客户端,ubuntu版本

基本流程如下&#xff0c;先生成Mosquitto的库&#xff0c;然后qt调用库进行开发界面。 文章目录 0 生成库1 有界面的QT版本2 无界面版本 0 生成库 下载源码&#xff1a;https://github.com/eclipse/mosquitto.git 编译ubuntu 版本很简单&#xff0c;安装官方说明直接make&am…

rk3568 OpenHarmony4.1 Launcher定制开发—桌面壁纸替换

Launcher 作为系统人机交互的首要入口&#xff0c;提供应用图标的显示、点击启动、卸载应用&#xff0c;并提供桌面布局设置以及最近任务管理等功能。本文将介绍如何使用Deveco Studio进行单独launcher定制开发、然后编译并下载到开发板&#xff0c;以通过Launcher修改桌面背景…

记录|如何打包C#项目

参考文章&#xff1a; c#窗体应用程序怎么打包 经过检验确实有效 Step1. 生成发布文件 在Visual Studio的菜单中&#xff0c;找到“生成”->“发布” 第一次会有个向导&#xff0c;基本上一路next下来既可以 最后&#xff0c;点击完成即可以 Step2. 获得publish文件 自…

【JavaEE】AQS原理

本文将介绍AQS的简单原理。 首先有个整体认识&#xff0c;全称是 AbstractQueuedSynchronizer&#xff0c;是阻塞式锁和相关的同步器工具的框架。常用的ReentrantLock、Semaphore、CountDownLatch等都有实现它。 本文参考&#xff1a; 深入理解AbstractQueuedSynchronizer只需…

[C++]TinyWebServer

TinyWebServer 文章目录 TinyWebServer1 主体框架2 Buffer2.1 向Buffer写入数据2.2 从Buffer读取数据2.3 动态扩容2.4 从socket中读取数据2.5 具体实现 3 日志系统3.1 生产者-消费者模型3.2 数据一致3.3 代码 4 定时器4.1 调整堆中元素操作4.2 堆的操作4.2.1 增4.2.2 删4.2.3 改…

微信小程序-应用,页面和组件生命周期总结

情景1&#xff1a;小程序冷启动时候的顺序 情景2: 使用navigator&#xff08;保留并打开另一个页面&#xff09;和redirect&#xff08;关闭并打开另一个页面&#xff09;的执行顺序 情景3&#xff1a;切后台和切前台

Linux——组管理和权限管理

目录 组管理 Linux 组基本介绍 文件/目录所有者 组的创建 查看&修改文件/目录所在组 改变用户所在组 权限管理 基本介绍 rwx 文件/目录权限详解 chmod 修改文件或目录权限 chown 修改文件所有者 组管理 Linux 组基本介绍 关于第二张图中问题&#xff0c;答案…

【Qt】Qt的坐标转换(mapToGlobal)

1、QPoint QWidget::mapToGlobal(const QPoint &pos) const 将小部件坐标转换为全局坐标。mapToGlobal(QPoint(0,0))可以得到小部件左上角像素的全局坐标。2、QPoint QWidget::mapToParent(const QPoint &pos) const 将小部件坐标转换为父部件坐标。如果小部件没有父部…

Jmeter之count函数

counter函数 1、功能解释 count函数--计数器&#xff0c;每调用这个函数一次&#xff0c;它就会自动加1。它有两个参数&#xff0c;第一个参数是布尔型的&#xff0c;只能设置成 “TRUE”或者“FALSE”&#xff0c;如果是TRUE&#xff0c;那么每个用户有自己的计数器&#xf…

常用的网络爬虫工具推荐

在推荐常用的网络爬虫工具时&#xff0c;我们可以根据工具的易用性、功能强大性、用户口碑以及是否支持多种操作系统等多个维度进行考量。以下是一些常用的网络爬虫工具推荐&#xff1a; 1. 八爪鱼 简介&#xff1a;八爪鱼是一款免费且功能强大的网站爬虫&#xff0c;能够满足…

vxe-table——实现切换页码时排序状态的回显问题(ant-design+elementUi中table排序不同时回显的bug)——js技能提升

之前写的后台管理系统&#xff0c;都是用的antdelement&#xff0c;table组件中的【排序】问题是有一定的缺陷的。 想要实现的效果&#xff1a; antv——table组件一次只支持一个参数的排序 如下图&#xff1a; 就算是可以自行将排序字段拼接到列表接口的入参中&#xff0c…