后端接口优化的8大策略
来源:今日头条(作者-IT知识分享官)
如果让你进行后端接口的优化,你是首选优化代码行数?还是首选避免跨地域访问呢?在评估接口性能时,我们需要首先找出最耗时的部分,并优化它,这样优化效果才会立竿见影。
1. 优化前端接口
1.1 核心数据和非核心数据拆分为多个接口
我曾经对用户(会员)主页接口进行了优化,该接口返回的数据非常庞大。由于各个模块的数据都在同一个接口中,只要其中一部分数据的查询耗时较长,整体性能就会下降,导致接口的失败率增加,前端无法展示核心数据。这主要是因为核心数据和非核心数据没有进行隔离,耗时数据和非耗时数据没有分开。
对于庞大的接口,我们需要先梳理每个模块中数据的获取逻辑和性能情况,明确前端必须展示和重点关注的核心数据,并确保这些数据能够快速、稳定地响应给前端。而非核心的数据和性能较差的数据则可以拆分到另外的接口中,即使这些接口的失败率较高,对用户影响也不大。
这种优化方式除了能保证快速返回核心数据,也能提高稳定性。如果非核心数据故障,可以单独降级,不会影响核心数据展示,大大提高了稳定性。
1.2 前端并行调用多个接口
后端提供给前端的接口应保证能够独立调用,避免出现需要先调用A接口再调用B接口的情况。如果接口设计不合理,前端需要的总耗时将是A接口耗时与B接口耗时之和。相反,如果接口能够独立调用,总耗时将取决于A接口和B接口中耗时较长的那个。显然,后者的性能更优。
在A接口与B接口都依赖相同的公共数据的情况下,会导致重复查询。为了优化总耗时,重复查询是无法避免的,因此应着重优化公共数据的性能。
在代码设计层面,应封装每个模块的取值逻辑,避免A接口与B接口出现重复代码或拷贝代码的情况。
1.3 使用MD5加密,防篡改数据,减少重复校验
在提单接口中,需要校验用户对应商品的可见性、是否符合优惠活动规则以及是否可用对应的优惠券等内容。由于用户可能篡改报文来伪造提单请求,后端必须进行校验。然而,由于提单链路本身耗时较长,多次校验以上数据将大大增加接口的耗时。那么,是否可以不进行以上内容的校验呢?
是可以的。在用户提单页面,商品数据、优惠活动数据以及优惠券等数据都是预览接口校验过的。后端可以生成一个预览Token
,并将预览结果存在缓存中,前端在提单接口中指定预览Token。后端将校验提单数据和预览数据是否一致,如果不一致,则说明用户伪造了请求。
为了避免预览数据占用过多的缓存空间,可以设置一个过期时间,例如预览数据在15分钟内不进行下单操作,则会自动失效。另外,还可以对关键数据进行MD5
加密处理,加密后的数据只有64位,数据量大大减少。后端在提单接口中对关键数据进行MD5
加密,并与缓存中的MD5
值进行比对,如果不一致,则说明用户伪造了提单数据。
更详细请参考# 如何防止提单数据被篡改?
1.4 同步写接口改为异步写接口
在写接口耗时较高的情况下,可以采取将接口拆分为两步来优化性能。首先,第一步是接收请求并创建一个异步任务,然后将任务交给后端进行处理。第二步是前端轮训异步任务的执行结果,以获取最终结果。
通过将同步接口异步化,可以避免后端线程资源被长时间占用,并且可以避免浏览器和服务器的socket连接被长时间占用,从而提高系统的并发能力和稳定性。
此外,还可以在前端接口设置更长的轮训时间,以有效提高接口的成功率,降低同步接口超时失败的概率,提升系统的性能和用户体验。
1.5 页面静态化
在电商领域,商品详情页和活动详情页通常会有非常高的流量,特别是在秒杀场景或大促场景下,流量会更高。同时,商品详情页通常包含大量的信息,例如商品介绍、商品参数等,导致每次访问商品详情都需要访问后端接口,给后端接口带来很大的压力。
为了解决这个问题,可以考虑将商品详情页中不会变动的部分(如商品介绍、头图、商品参数等)静态化到html
文件中,前端浏览器直接访问这些静态文件,而无需访问后端接口。这样做可以极大地减轻商品详情接口的查询压力。
然而,对于未上架的商品详情页、后台管理等页面,仍然需要查询商品详情接口来获取最新的信息。
页面静态化需要先使用模版工具例如Thymeleaf
等,将商品详情数据渲染到Html
文件,然后使用运维工具(rsync
)将html
文件同步到各个nginx
机器。前端就可以访问对应的商品详情页。
当商品上下架状态变化时,将对应Html
文件重新覆盖或置为失效。
1.6 不变资源访问CDN
CDN(内容分发网络)是一种分布式网络架构,它将网站的静态内容缓存在全球各地的服务器上,使用户能够从最近的服务器获取所需内容,从而加速用户访问。这样,用户不需要从原始服务器请求内容,可以减少因网络延迟导致的等待时间,提高用户的访问速度和体验。
通过注入静态Html
文件到CDN
,可以避免每次用户的请求都访问原始服务器。相反,这些文件会被缓存在CDN
的服务器上,因此用户可以直接从离他们最近的服务器获取内容。这种方式可以大大减少因网络延迟导致的潜在用户流失,因为用户能够更快地获取所需的信息。
此外,CDN
的使用还可以提高系统在高并发场景下的稳定性。在高并发情况下,原始服务器可能无法承受大量的请求流量,并可能导致系统崩溃或响应变慢。但是,通过将静态Html
文件注入到CDN
,让CDN
来处理部分请求,分担了原始服务器的负载,从而提高了整个系统的稳定性。
通过将商品详情、活动详情等静态Html
文件注入到CDN
,可以加速用户访问速度,减少用户因网络延迟而流失的可能性,并提高系统在高并发场景下的稳定性。
2. 调用链路优化
调用链路优化重点减少RPC的调用、减少跨地域调用。
2.1 减少跨地域调用
刚才我提到了北京到上海的跨地域调用需要耗费大约30毫秒的时间,这个耗时是相当高的,所以我们应该特别关注调用链路上是否存在跨地域调用的情况。这些跨地域调用包括Rpc调用、Http调用、数据库调用、缓存调用以及MQ调用等等。在整理调用链路的时候,我们还应该标注出跨地域调用的次数,例如跨地域调用数据库可能会出现多次,在链路上我们需要明确标记。我们可以考虑通过降低调用次数来提高性能,因此在设计优化方案时,我们应该特别关注如何减少跨地域调用的次数。
举个例子,在某种情况下,假设上游服务在上海,而我们的服务在北京和上海都有部署,但是数据库和缓存的主节点都在北京,这时候就无法避免跨地域调用。那么我们该如何进行优化呢?考虑到我们的服务会更频繁地访问数据库和缓存,如果让我们上海节点的服务去访问北京的数据库和缓存,那么跨地域调用的次数就会非常多。因此,我们应该让上游服务去访问我们在北京的节点,这样只会有1次跨地域调用,而我们的服务在访问数据库和缓存时就无需进行跨地域调用。
2.2 单元化架构:不同的用户路由到不同的集群单元
如果主数据库位于北京,那么南方的用户每次写请求就只能通过跨地域访问来完成吗?实际上并非如此。数据库的主库不仅可以存在于一个地域,而是可以在多个地域上部署主数据库。将每个用户归属于最近的地域,该用户的请求都会被路由到所在地域的数据库。这样的部署不仅提升了系统性能,还提高了系统的容灾等级,即使单个机房发生故障也不会影响全网的用户。
这个思想类似于CDN(内容分发网络),它能够将用户请求路由到最近的节点。事实上,由于用户的存储数据已经在该地域的数据库中,用户的请求极少需要切换到其他地域。
为了实现这一点,我们需要一个用户路由服务来提供用户所在地域的查询,并且能够提供高并发的访问。
除了数据库之外,其他的存储中间件(如MQ、Redis等)以及Rpc框架都需要具备单元化架构能力。
当我们无法避免跨地域调用时,我们可以选择整体上跨地域调用次数最少的方案来进行优化。
2.3 微服务拆分过细会导致Rpc调用较多
微服务拆分过细会导致更多的RPC调用,一次简单的请求可能就涉及四五个服务,当访问量非常高时,多出来的三五次Rpc调用会导致接口耗时增加很多。
每个服务都需要处理网络IO,序列化反序列化,服务的GC 也会导致耗时增加,这样算下来一个大服务的性能往往优于5个微服务。
当然服务过于臃肿会降低开发维护效率,也不利于技术升级。微服务过多也有问题,例如增加整体链路耗时、基础架构升级工作量变大、单个需求代码变更的服务更多等弊端。需要你权衡开发效率、线上性能、领域划分等多方面因素。
总之应该极力避免微服务过多的情况。
怎么评估微服务过多呢?我的个人经验是:团队内平均一个人两个服务以上,就是微服务过多了。例如三个人的团队6个服务,5个人的团队10个服务。
2.4 去掉中间商,减少Rpc调用
当整个系统的调用链路中涉及到过多的Rpc调用时,可以通过去除中间服务的方式减少Rpc调用。例如从A服务到E服务的调用链路包含了4次Rpc调用(A->B->C->D->E),而我们可以评估中间的B、C、D三个服务的功能是否冗余,是否只是作为转发服务而没有太多的业务逻辑,如果是的话,我们可以考虑让A服务直接调用E服务,从而避免中间的Rpc调用,减少系统的负担。
总的来说,无论是调用链路过长或是微服务过多,都可能导致过多的Rpc请求,因此可以尝试去除中间的服务来优化系统性能。
2.5 提供Client工具方法处理,而非Rpc调用
如果中间服务有业务逻辑,不能直接移除,可以考虑使用基于Java Client工具方法的服务提供方式,而非Rpc方式。
举例来说,如果存在一个调用链路为A->B->C,其中B服务有自己的业务逻辑。此时B服务可以考虑提供一个Java Client jar包给A服务使用。B服务所依赖的数据可以由A服务提供,这样就减少1次 A 服务到B 服务的Rpc调用。
这样做有一个好处,当A、B都共同依赖相同的数据,A服务查询一遍就可以提供给自己和B服务Client使用。如果基于Rpc方式,A、B都需要查询一遍。微服务过多也不好啊!
通过改变服务提供方式,尽量减少Rpc调用次数和开销,从而优化整个系统的性能。
例如社交关注关系服务。在这个服务中,需要查询用户之间的关注关系。为了提高服务性能,关注服务内部使用缓存来存储关注关系。为了降低高并发场景下的调用延迟和机器负载,关注服务提供了一个Java Client Jar查询关注关系,放弃了上游调用rpc接口的方式。这样做的好处是可以减少一次Rpc调用,避免了下游服务因GC 停顿而导致的耗时。
2.6 单条调用改为批量调用
无论是查询还是写入,都可以使用批量调用来代替单条调用。比如,在查询用户订单的详情时,应该批量查询多个订单,而不是通过循环逐个查询订单详情。批量调用虽然会比单条调用稍微耗时多一些,但是循环调用的耗时却是单条调用的N倍,所以批量查询耗时要低很多。
在接口设计和代码流程中,我们应该尽量避免使用for循环进行单条查询或单条写入操作。正如此文所提到的,批量插入数据库的性能可能是单条插入的3-5倍。# 10亿数据如何插入Mysql,10连问,你想到了几个?
2.7 并行调用
在调用多个接口时,可以选择串行调用或并行调用的两种方式。串行调用是指依次调用每个接口,一个接口完成后才能调用下一个接口,而并行调用是指同时调用多个接口。可以看出并行调用的耗时更低,因为串行调用的耗时是多个接口耗时的总和,而并行调用的耗时是耗时最高的接口耗时。
为了灵活实现多个接口的调用顺序和依赖关系,可以使用Java中的CompletableFuture类。CompletableFuture可以将多个接口的调用任务编排成一个有序的执行流程,可以实现最大程度的并发查询或并发修改。
例如,可以并行调用两个接口,然后等待两个接口全部成功后,再对查询结果进行汇总处理。这样可以提高查询或修改的效率。
CompletableFuture<Void> first = CompletableFuture.runAsync(() -> {
System.out.println("do something first");
Thread.sleep(200);
});
CompletableFuture<Void> second = CompletableFuture.runAsync(() -> {
System.out.println("do something second");
Thread.sleep(300);
});
CompletableFuture<Void> allOfFuture = CompletableFuture.allOf(first, second).whenComplete((m, k) -> {
System.out.println("all finish do something");
});
allOfFuture.get();