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

一、背景与效果

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

删除的视频怎样才能恢复?详尽指南

在日常生活中&#xff0c;我们有时会不小心删除一些重要的视频文件&#xff0c;或者在整理存储空间时不慎丢失了珍贵的记忆片段。这时候&#xff0c;我们可以通过一些数据恢复工具和技巧&#xff0c;找回这些被删除的视频。本文将详细介绍几种常见且有效的视频恢复方法&#xf…

C#,.NET常见算法

1.递归算法 1.1.C#递归算法计算阶乘的方法 using System;namespace C_Sharp_Example {public class Program{/// <summary>/// 阶乘&#xff1a;一个正整数的阶乘Factorial是所有小于以及等于该数的正整数的积&#xff0c;0的阶乘是1&#xff0c;n的阶乘是n&#xff0…

《操作系统》(学习笔记)(王道)

一、计算机系统概述 1.1 操作系统的基本概念 1.1.1 操作系统的概念 1.1.2 操作系统的特征 1.1.3 操作系统的目标和功能 1.2 操作系统的发展与分类 1.2.1 手工操作阶段&#xff08;此阶段无操作系统&#xff09; 1.2.2 批处理阶段&#xff08;操作系统开始出现&#xff0…

前端知识--前端访问后端技术Ajax及框架Axios

一、异步数据请求技术----Ajax Ajax是前端访问后端的技术&#xff0c;为异步请求&#xff08;不刷新页面&#xff0c;请求数据&#xff0c;只更新局部数据&#xff09;。 例如&#xff1a;在京东网站中搜索电脑&#xff0c;就会出现一些联想搜索&#xff0c;但此时页面并没有…

WEB开发-HTTP认证

1 需求 2 接口 3 示例 HTTP Authentication&#xff08;HTTP认证&#xff09;是Web服务器用来验证客户端请求的一种机制。它通常用于保护需要用户凭据&#xff08;如用户名和密码&#xff09;才能访问的资源。HTTP认证有几种不同的分类或方法&#xff0c;以下是其中一些主要的分…

手机怎么设置不同的ip地址

在数字化日益深入的今天&#xff0c;智能手机已成为我们生活、工作和学习中不可或缺的设备。然而&#xff0c;随着网络应用的广泛和深入&#xff0c;我们有时需要为手机设置不同的IP地址来满足特定需求。比如&#xff0c;避免网络限制、提高网络安全、或者进行网络测试等。本文…

解决R语言找不到系统库导致的报错

1、基本需知 1.1、系统库 系统库&#xff08;System library&#xff09;是一组预先编写和编译好的软件模块集合&#xff0c;用于支持操作系统的基本功能和提供一些常见的服务。这些库通常由操作系统或第三方开发者提供&#xff0c;并且在系统安装过程中被预装或者用户可以额…

分享 2 个 .NET EF 6 只更新某些字段的方法

前言 EF 更新数据时&#xff0c;通常情况下&#xff0c;是更新全部字段的&#xff0c;但实际业务中&#xff0c;更新全部字段的情况其实很少&#xff0c;一般都是修改其中某些字段&#xff0c;所以为了实现这个目标&#xff0c;很多程序员通常会这样作&#xff1a; 先从数据库…

数据治理之“财务一张表”

前言 信息技术的发展&#xff0c;伴随企业业务系统的纷纷建设&#xff0c;提升业务处理效率的同时&#xff0c;也将企业的整体主价值链流程分成了一段一段的业务子流程&#xff0c;很多情况下存在数据上报延迟、业务协作不顺畅、计划反馈不及时、库存积压占资多……都可以从数据…

力扣94题(java语言)

题目 思路 使用一个栈来模拟递归的过程&#xff0c;以非递归的方式完成中序遍历(使用栈可以避免递归调用的空间消耗)。 遍历顺序步骤&#xff1a; 遍历左子树访问根节点遍历右子树 package algorithm_leetcode;import java.util.ArrayList; import java.util.List; import…

VirtualBox 安装Centos 7 避坑指南 SSH连不上 镜像失效 静态网络配置等

背景 几乎每次安装Centos 7 时&#xff0c;都会遇到各种各样的问题&#xff0c;毕竟每次安装动辄就是半年几年&#xff0c;几乎都是在换工作时&#xff0c;有了新机器才会倒腾一次&#xff0c;时间久远&#xff0c;就会忘记一些细节&#xff0c;这次整理一下&#xff0c;避免以…

C语言 | Leetcode C语言题解之第275题H指数II

题目&#xff1a; 题解&#xff1a; int hIndex(int* citations, int citationsSize) {int left 0, right citationsSize - 1;while (left < right) {int mid left (right - left) / 2;if (citations[mid] > citationsSize - mid) {right mid - 1;} else {left mi…

Nacos 2.x 新增 grpc 端口,Nginx 需要配置TCP端口转发的注意事项

Nacos 2.x 开始&#xff0c;最大的变化就是端口。在默认主端口 8848 之外又新增了三个端口&#xff0c;新增端口是在配置的主端口 server.port 的基础上&#xff0c;进行一定偏移量自动生成。 8848&#xff08;主端口&#xff0c;默认8848&#xff09;web页面端口及相关http接口…

Spring Security认证授权介绍

一、目标 真正控制系统权限的&#xff0c;需要引入专门的安全框架才行&#xff0c;所以&#xff0c;我们今天重点来学习Spring家族中的一员Spring Security安全框架。最终呢&#xff0c;我们会使用Spring Security框架来控制养老项目的后台管理系统 能够熟悉常见的权限控制的方…

数据分析或处理中关于坐标系的一些事

通过对本文的阅读&#xff0c;你将获取坐标系的一些基础知识&#xff0c;以及学会如何使用pyproj实现地理数据的投影与转换。更重要的是&#xff0c;作为一个开发者&#xff0c;面对地理坐标系的图层数据&#xff0c;需要进行面积计算、距离量测、规则分块等需求时&#xff0c;…

IP 泄露: 原因与避免方法

始终关注您的IP信息&#xff01; 您的IP地址不仅显示您的位置&#xff0c;它包含几乎所有的互联网活动信息&#xff01; 如果出现IP泄漏&#xff0c;几乎所有的信息都会被捕获甚至非法利用&#xff01; 那么&#xff0c;网站究竟如何追踪您的IP地址&#xff1f;您又如何有效…

服务攻防-框架安全(漏洞复现)

关闭靶场 sudo docker-compose down 运行此靶场 sudo docker-compose up -d 查看启动环境 sudo docker ps 运行dockers容器 docker exec -it 64052abd288b /bin/bash thinkphp框架 thinkphp 2 - rce漏洞复现 docker exec -it 731dbae0e0b5 /bin/bash 集成化工具扫描 可以命令…

利用换元法计算积分的常见题型(考研高数复习)

考研中常见的几种换元法积分计算题 (1)被积式仅包含一个根式&#xff1a;根号下为有 a a a 和 x x x 的平方和/平方差 此种类型的积分题型&#xff0c;可以通过构造单个锐角大小为 t t t 的直角三角形&#xff0c;利用勾股定理和三角函数进行代换。 平方和的情况 形如 ∫…

sed利用脚本处理文件

一、sed是什么 sed 命令是利用脚本来处理文本文件。它可以依照脚本的指令来处理、编辑文本文件。主要用来自动编 辑一个或多个文件、简化对文件的反复操作、编写转换程序等。 二、sed的原理 读入新的一行内容到缓存空间&#xff1b; 从指定的操作指令中取出第一条指令&…

自定义协议(应用层协议)——网络版计算机基于TCP传输协议

应用层&#xff1a;自定义网络协议&#xff1a;序列化和反序列化&#xff0c;如果是TCP传输的&#xff1a;还要关心区分报文边界&#xff08;在序列化设计的时候设计好&#xff09;——粘包问题 1、首先想要使用TCP协议传输的网络&#xff0c;服务器和客户端都应该要创建自己…