开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Linux部署RocketMQ 安装RocketMq:

发表于 2021-11-05

「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」

如果你的linux服务器未安装Maven环境,则需要先安装Maven环境:

1.下载:maven.apache.org/download.cg…

2.安装包放在usr/local下,解压:tar zvxf apache-maven-3.6.3-bin.tar.gz

3.配置环境变量:

  • 打开etc/profile:
  • 添加配置:
1
2
c复制代码 export MAVEN_HOME=/usr/local/apache-maven-3.6.3
export PATH=$PATH:$MAVEN_HOME/bin
  • 保存后刷新配置:source /etc/profile
  • 添加权限:chmod a+x /usr/local/apache-maven-3.6.3/bin/mvn

注意路径,完毕后输入:mvn -v
在这里插入图片描述

安装RocketMq:

1.下载:rocketmq.apache.org/docs/quick-…
在这里插入图片描述
2.把压缩包放入到user/local目录,解压:unzip rocketmq-all-4.7.1-source-release.zip

3.进入到解压后的目录:cd rocketmq-all-4.7.1/(文件夹名称可以修改)

4.使用Maven安装:mvn -Prelease-all -DskipTests clean install -U(需要几分钟时间,耐心等待)

5.安装完毕后,使用xftp查看rocketmq安装后的目录:distribution/target/rocketmq-4.7.1/rocketmq-4.7.1,表明安装成功

6.打开根目录etc/profile添加配置:

1
2
3
c复制代码# rocketmq环境变量配置
export ROCKETMQ=/usr/local/rocketmq-all-4.7.1/distribution/target/rocketmq-4.7.1/rocketmq-4.7.1
export PATH=$PATH:$ROCKETMQ/bin

7.保存后刷新配置:source /etc/profile

8.添加权限:chmod +x mqnamesrv mqbroker mqshutdown

这样直接就可以使用命令 ,启动或关闭 namesrv 和 broker了!

启动前,我们需要创建一下log目录:

  • 进入到/root目录,创建log文件夹;
  • 在log文件夹内创建:rocketmqlogs文件夹;
  • 在rocketmqlogs文件夹内创建namesrv.log文件和broker.log文件;

打开rocketmq-4.7.1/bin,找到runserver.sh和runbroker.sh文件,根据自己的内存环境配置

1
c复制代码JAVA_OPT="${JAVA_OPT} -server -Xms512m -Xmx512m -Xmn256m"

保存后返回,打开conf目录下的broker.conf文件,在最后面添加外网IP配置:

1
c复制代码brokerIP1 = 39.xxx.x.xx

不配置默认是内网IP!

启动namesrv:

mqnamesrv &
在这里插入图片描述
启动namesrv成功,按crtl+c退出命令!

启动broker:

mqbroker -n 39.xxx.x.xx:9876 -c /usr/local/rocketmq-all-4.7.1/distribution/target/rocketmq-4.7.1/rocketmq-4.7.1/conf/broker.conf &

在这里插入图片描述
查看启动状态:jps

在这里插入图片描述

关闭broker:mqshutdown broker

关闭namesrv:`mqshutdown name

安装rocketmq控制台:rocketmq-console-ng

官方GIthub地址:github.com/apache/rock…

在这里插入图片描述

把rocket-console下载下来,并用IDEA打开:

在这里插入图片描述

注意这两个配置文件,以及属性配置:
rocketmq.config.namesrvAddr:namesrv地址,不用说了,你的服务器地址:9876;
rocketmq.config.dataPath=/usr/local/rocketmq-console/data:可以自己在服务器创建一个日志文件夹配置上去;
rocketmq.config.loginRequired=false:控制台是否启用登录密码,true的话,你就打开users.properties文件配置用户名和登录密码即可;

使用maven打包为jar:

在这里插入图片描述

你会发现,你很有可能会打包失败,解决办法:
1.修改项目jdk版本为1.8;
2.修改项目中爆红的错误代码,可能在UserServiceImpl.java或其它一两个文件中(可以直接运行项目,就能看到错误信息);
3.修改springboot版本,我使用的是目前最新版:2.3.4;

在这里插入图片描述

4.修改surefire版本为最新版,目前最新版为3.0.0-M5;

在这里插入图片描述

此时再次打包,就不会报错了,可以将target下得到的rocketmq-console-ng-2.0.0.jar,放入linux服务器启动,浏览器访问:39.xxx.x.xx:8082

在这里插入图片描述

当然,你也可以在本地直接运行:http://localhost:8082/

在这里插入图片描述

注意:只有集群模块下,显示你自己配置的brokerName名字时,才表示部署成功,如果不显示,大概率是broker没有连接到namesrv,此时请检查你namesrv和broker的启动是否有误。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

积跬步至千里:QUIC 协议在蚂蚁集团落地之综述 QUIC

发表于 2021-11-05

作者:孔令涛

自 2015 年以来,QUIC 协议开始在 IETF 进行标准化并被国内外各大厂商相继落地。鉴于 QUIC 具备“0RTT 建联”、“支持连接迁移”等诸多优势,并将成为下一代互联网协议:HTTP3.0 的底层传输协议,蚂蚁集团支付宝客户端团队与接入网关团队于 2018 年下半年开始在移动支付、海外加速等场景落地 QUIC。

本文是综述篇,介绍 QUIC 在蚂蚁的整体落地情况。之所以是综述,是因为 QUIC 协议过于复杂,如果对标已有的协议,QUIC 近似等于 HTTP + TLS +TCP,无法详细的毕其功于一役,因此我们通过综述的方式将落地的重点呈现给读者,主要介绍如下几个部分:

  • QUIC背景:简单全面的介绍下 QUIC 相关的背景知识;
  • 方案选型设计:详细介绍蚂蚁的落地方案如何另辟蹊径、优雅的支撑 QUIC 的诸多特性,包括连接迁移等;
  • 落地场景:介绍 QUIC 在蚂蚁的两个落地场景,包括:支付宝客户端链路以及海外加速链路;
  • 几项关键技术:介绍落地 QUIC 过程中核心需要解决的问题,以及我们使用的方案,包括:“支持连接迁移”、“提升 0RTT 比例”, “支持 UDP 无损升级”以及“客户端智能选路” 等;
  • 几项关键的技术专利。

QUIC 背景介绍

鉴于读者的背景可能不同,在开始本文之前,我们先简单介绍下 QUIC 相关的背景知识,如果您对这个协议的更多设计细节感兴趣,可以参见相关 Draft:datatracker.ietf.org/wg/quic/doc…

一、QUIC 是什么?

简单来说,QUIC (Quick UDP Internet Connections) 是一种基于 UDP 封装的安全 可靠传输协议,他的目标是取代 TCP 并自包含 TLS 成为标准的安全传输协议。下图是 QUIC 在协议栈中的位置,基于 QUIC 承载的 HTTP 协议进一步被标准化为 HTTP3.0。

二、为什么是 QUIC ?

在 QUIC 出现之前,TCP 承载了 90% 多的互联网流量,似乎也没什么问题,那又为何会出现革命者 QUIC 呢?这主要是因为发展了几十年的 TCP 面临 “协议僵化问题”,表现在几方面:

  1. 网络设备支持 TCP 时的僵化,表现在:对于一些防火墙或者 NAT 等设备,如果 TCP 引入了新的特性,比如增加了某些 TCP OPTION 等,可能会被认为是攻击而丢包,导致新特性在老的网络设备上无法工作。
  2. 网络操作系统升级困难导致的 TCP 僵化,一些 TCP 的特性无法快速的被演进
  3. 除此之外,当应用层协议优化到 TLS1.3、 HTTP2.0 后, 传输层的优化也提上了议程,QUIC 在 TCP 基础上,取其精华去其糟粕具有如下的硬核优势:

三、QUIC 生态圈发展简史

下图是 QUIC 从创建到现在为止的一些比较重要的时间节点,2021 年,QUIC V1 成为 RFC,结束百花齐放的态势。

介绍完 QUIC 相关背景,之后我们来介绍蚂蚁的整个落地的内容,这里为了便于阐述,我们用蚂蚁 QUIC 的 一、二、三、四 来进行概括总结,即 “一套落地框架”、“两个落地场景”、“三篇创新专利保护”、“四项关键技术”。

一套落地框架

蚂蚁的接入网关是基于多进程的 NGINX 开发的 (内部称为 Spanner,协议卸载的扳手),而 UDP 在多进程编程模型上存在诸多挑战,典型的像无损升级等。为了设计一套完备的框架,我们在落地前充分考虑了服务端在云上部署上的方便性、扩展性、以及性能问题,设计了如下的落地框架以支撑不同的落地场景:

在这套框架中,包括如下两个组件:

  1. QUIC LB 组件:基于 NGINX 4层 UDP Stream 模块开发,用来基于 QUIC DCID 中携带的服务端信息进行路由,以支持连接迁移;
  2. NGINX QUIC 服务器:开发了 NGINX_QUIC_MODULE,每个 Worker 监听两种类型的端口:
  3. BASE PORT ,每个 Worker 使用的相同的端口号,以 Reuseport 的形式监听,并暴露给 QUIC LB,用以接收客户端过来的第一个 RTT 中的数据包,这类包的特点是 DCID 由客户端生成,没有路由信息;
  4. Working PORT,每个 Worker 使用的不同的端口号,是真正的工作端口,用以接收第一个 RTT 之后的 QUIC 包,这类包的特定是 DCID 由服务端的进程生成携带有服务端的信息。

当前框架支持的能力包括如下:

  1. 在不用修改内核的情况下,完全在用户态支持 QUIC 的连接迁移,以及连接迁移时 CID 的 Update;
  2. 在不用修改内核的情况下,完全在用户态支持 QUIC 的无损升级以及其他运维问题;
  3. 支持真正意义上的 0RTT ,并可提升 0RTT 的比例。

为何能支持上述能力,我们后面会展开叙述。

两个落地场景

我们由近及远的两个落地场景如下:

场景一、支付宝移动端落地

如下为我们落地架构的示意图,支付宝手机客户端通过 QUIC 携带 HTTP 请求,通过 QUIC LB 等四层网关将请求转发到 Spanner (蚂蚁内部基于 NGINX 开发的 7 层网关),在 Spanner 上我们将 QUIC 请求 Proxy 成 TCP 请求,发送给业务网关(RS)。

具体的方案选型如下:

  • 支持的 QUIC 版本是 gQUIC Q46;
  • NGINX QUIC MODULE 支持 QUIC 的接入和 PROXY 成 TCP 的能力;
  • 支持包括移动支付、基金、蚂蚁森林在内的所有的 RPC 请求;
  • 当前选择 QUIC 链路的方式有两种 :
    • Backup 模式,即在 TCP 链路无法使用的情况下,降级到 QUIC 链路;
    • Smart 模式,即 TCP和 QUIC 竞速,在 TCP 表现力弱于 QUIC 的情况下,下次请求主动使用 QUIC 链路。

在此场景下,通过使用 QUIC 可以获得的红利包括:

  1. 在客户端连接发生迁移的时候,可以不断链继续服务;
  2. 客户端在首次发起连接时,可以节省 TCP 三次握手的时间;
  3. 对于弱网情况,QUIC 的传输控制可以带来传输性能提升。

场景二、海外加速落地

蚂蚁集团从 2018 年开始自研了海外的动态加速平台 AGNA(Ant Global Network Accelerator)以替换第三方厂商的加速服务。AGNA 通过在海外部署接入点:Local Proxy(LP) 以及在国内部署接出点:Remote Proxy (RP)的方式,将用户的海外请求通过 LP 和 RP 的加速链路回源国内。如下图所示,我们将 QUIC 部署在 LP 和 RP 之间的链路。

在海外接入点上(LP),每一个 TCP 连接都被 Proxy 成 QUIC 上的一个 Stream 进行承载,在国内接出点上(RP), 每一个 QUIC Stream 又被 Proxy 成一个 TCP 连接,LP 和 RP 之间使用 QUIC 长连接。

在此场景下,通过使用 QUIC 可以获得的红利包括:

  1. 通过 QUIC 长连接的上的 Stream 承载 TCP 请求,避免每次的跨海建联;
  2. 对于跨海的网络,QUIC 的传输控制可以带来传输性能提升。

三篇关键专利

到目前为止,我们把落地过程中一些创新的技术点通过申请专利进行了保护,并积极在 IETF 进行标准化分享我们的研究成果,包括:

专利一

我们将落地场景 2 中,通过 QUIC Stream 进行四层代理的手段来进行海外回源的加速方法进行专利保护,提出:“一种基于 QUIC 协议代理的的链路加速方法”,目前此专利已经获得美国专利授权,专利号:CN110213241A。

专利二

将我们落地框架中的 QUIC LB 组件作为专利进行保护,提出:“一种无状态、一致性、分布式的 QUIC 负载均衡设备”,目前此专利还在受理中。由于通过 QUIC LB 可以很好的支持 QUIC 协议的连接迁移问题,所以目前 IETF QUIC WG 上有关于 QUIC LB 相关的草案,我们目前已经参与到 Draft 的讨论和制定中,后序相关的方案也会继续推广到云上产品。

专利三

将我们解决的 UDP 的无损升级方法进行专利保护,提出 “一种 QUIC 服务器无损升级方案”,目前此专利还在受理中。由于 UDP 无损升级问题是一个业界难题,当前有些手段需要在用户态进行跳转,性能损失较大,我们的方案可以在我们的落地框架中解决当前问题,关于这个方案的细节我们再后面的关键技术中进行介绍。

四项关键技术

在整个的落地中,我们设计的方案围绕解决几个核心问题展开,形成了四项关键技术,分别如下:

技术点1:优雅的支持连接迁移能力

先说 连接迁移面临的问题,上文有提到,QUIC 有一项比较重要的功能是支持连接迁移。这里的连接迁移是指:如果客户端在长连接保持的情况下切换网络,比如从 4G 切换到 Wifi , 或者因为 NAT Rebinding 导致五元组发生变化,QUIC 依然可以在新的五元组上继续进行连接状态。QUIC 之所以能支持连接迁移,一个原因是 QUIC 底层是基于无连接的 UDP,另一个重要原因是因为 QUIC 使用唯一的 CID 来标识一个连接,而不是五元组。

如下图所示,是 QUIC 支持连接的一个示意图,当客户端出口地址从 A 切换成 B 的时候,因为 CID 保持不变,所以在 QUIC 服务器上,依然可以查询到对应的 Session 状态。

然而,理论很丰满,落地却很艰难,在端到端的落地过程中,因为引入了负载均衡设备,会导致在连接迁移时,所有依赖五元组 Hash 做转发或者关联 Session 的机制失效。以 LVS 为例,连接迁移后, LVS 依靠五元组寻址会导致寻址的服务器存在不一致。即便 LVS 寻址正确,当报文到达服务器时,内核根据五元组关联进程,依然会寻址出错。同时,IETF Draft 要求,连接迁移时 CID 需要更新掉,这就为仅依靠 CID 来转发的计划同样变的不可行。

再说 我们的解决方法,为了解决此问题,我们设计了开篇介绍的落地框架,这里我们将方案做一些简化和抽象,整体思路如下图所示:

1、在四层负载均衡上,我们设计了 QUIC LoadBalancer 的机制:

  • 我们在 QUIC 的 CID 中扩展了一些字段(ServerInfo)用来关联 QUIC Server 的 IP 和 Working Port 信息;
  • 在发生连接迁移的时候,QUIC LoadBalancer 可以依赖 CID 中的 ServerInfo 进行路由,避免依赖五元组关联 Session 导致的问题;
  • 在 CID 需要 Update 的时候,NewCID 中的 ServerInfo 保留不变,这样就避免在 CID 发生 Update 时,仅依赖 CID Hash 挑选后端导致的寻址不一致问题;

2、在 QUIC 服务器多进程工作模式上,我们突破了 NGINX 固有的多 Worker 监听在相同端口上的桎梏,设计了多端口监听的机制,每个 Worker 在工作端口上进行隔离,并将端口的信息携带在对 First Initial Packet 的回包的 CID 中,这样代理的好处是:

  • 无论是否连接迁移,QUIC LB 都可以根据 ServerInfo,将报文转发到正确的进程;
  • 而业界普遍的方案是修改内核,将 Reuse port 机制改为 Reuse CID 机制,即内核根据 CID 挑选进程。即便后面可以通过 ebpf 等手段支持,但我们认为这种修改内核的机制对底层过于依赖,不利于方案的大规模部署和运维,尤其在公有云上;
  • 使用独立端口,也有利于多进程模式下,UDP 无损升级问题的解决,这个我们在技术点 3 中介绍。

技术点2:提升 0RTT 握手比例

这里先 介绍 QUIC 0RTT 原理。前文我们介绍过, QUIC 支持传输层握手和安全加密层握手都在一个 0RTT 内完成。TLS1.3 本身就支持加密层握手的 0RTT,所以不足为奇。而 QUIC 如何实现传输层握手支持 0RTT 呢?我们先看下传输层握手的目的,即:服务端校验客户端是真正想握手的客户端,地址不存在欺骗,从而避免伪造源地址攻击。在 TCP 中,服务端依赖三次握手的最后一个 ACK 来校验客户端是真正的客户端,即只有真正的客户端才会收到 Sever 的 syn_ack 并回复。

QUIC 同样需要对握手的源地址做校验,否则便会存在 UDP 本身的 DDOS 问题,那 QUIC 是如何实现的?依赖 STK(Source Address Token) 机制。这里我们先声明下,跟 TLS 类似,QUIC 的 0RTT 握手,是建立在已经同一个服务器建立过连接的基础上,所以如果是纯的第一次连接,仍然需要一个 RTT 来获取这个 STK。如下图所示,我们介绍下这个原理:

  1. 类似于 Session Ticket 原理,Server 会将客户端的地址和当前的 Timestamp 通过自己的 KEY 加密生成 STK。
  2. Client 下次握手的时候,将 STK 携带过来,由于 STK 无法篡改,所以 Server 通过自己的 KEY 解密,如果解出来的地址和客户端此次握手的地址一致,且时间在有效期内,则表示客户端可信,便可以建立连接。
  3. 由于客户端第一次握手的时候,没有这个 STK,所以服务度会回复 REJ 这次握手的信息,并携带 STK。

理论上说,只要客户端缓存了这个 STK,下次握手的时候带过来,服务端便可以直接校验通过,即实现传输层的 0RTT。但是真实的场景却存在如下两个问题:

  1. 因为 STK 是服务端加密的,所以如果下次这个客户端路由到别的服务器上了,则这个服务器也需要可以识别出来;
  2. STK 中 encode 的是上一次客户端的地址,如果下一次客户端携带的地址发生了变化,则同样会导致校验失败。此现象在移动端发生的概率非常大,尤其是 IPV6 场景下,客户端的出口地址会经常发生变化。

再介绍下我们的解决方法。第一个问题比较好解,我们只要保证集群内的机器生成 STK 的秘钥一致即可。第二个问题,我们的解题思路是:

  1. 我们在 STK 中扩展了一个 Client ID, 这个 Clinet ID 是客户端通过无线保镖黑盒生成并全局唯一不变的,类似于一个设备的 SIMID,客户端通过加密的 Trasnport Parameter 传递给服务端,服务端在 STK 中包含这个 ID;
  2. 如果因为 Client IP 发生变化导致校验 STK 校验失败,便会去校验 Client ID,因为 ID 对于一个 Client 是永远不变的,所以可以校验成功,当然前提是,这个客户端是真实的。为了防止 Client ID 的泄露等,我们会选择性对 Client ID 校验能力做限流保护。

技术点3:支持 QUIC 无损升级

我们知道UDP 无损升级是业界难题。无损升级是指在 reload 或者更新二进制时,老的进程可以处理完存量连接上的数据后优雅退出。以 NGINX 为例,这里先介绍下 TCP 是如何处理无损升级的,主要是如下的两个步骤:

  1. 老进程先关闭 listening socket,待存量连接请求都结束后,再关闭连接套接字;
  2. 新进程从老进程继承 listening socket , 开始 accept 新的请求。

而 UDP 无法做到无损升级是因为 UDP 只有一个 listening socket 没有类似 TCP 的连接套接字,所有的收发数据包都在这个 socket 上,导致下面的热升级步骤会存在问题:

  1. 在热升级的时候,old process fork 出 new process 后,new process 会继承 listening socket 并开始 recv msg;
  2. 而 old process 此时如果关闭 listenging socket, 则在途的数据包便无法接收,达不到优雅退出的目的;
  3. 而如果继续监听,则新老进程都会同时收取新连接上的报文,导致老进程无法退出。

这里介绍下相关的解决方法。针对此问题,业界有一些方法,比如:在数据包中携带进程号,当数据包收发错乱后,在新老进程之间做一次转发。考虑到接入层上的性能等原因,我们不希望数据再做一次跳转。结合我们的落地架构,我们设计了如下的 基于多端口轮转的无损升级方案,简单来说,我们让新老进程监听在不同的端口组并携带在 CID 中,这样 QUIC LB 就可以根据端口转发到新老进程。为了便于运维,我们采用端口轮转的方式,新老进程会在 reload N 次之后,重新开始之前选中的端口。如下图所示:

  1. 无损升级期间,老进程的 Baseport 端口关闭,这样不会再接受 first intial packet, 类似于关闭了 tcp 的 listening socket;
  2. 老进程的工作端口,继续工作,用来接收当前进程上残余的流量;
  3. 新进程的 Baseport 开始工作,用来接收 first initial packet, 开启新的连接,类似于开启了 tcp 的 listening socket;
  4. 新进程的 working port = (I + 1) mod N, N 是指同时支持新老进程的状态的次数,例如 N = 4, 表示可以同时 reload 四次,四种 Old, New1, New2, New3 四种状态同时并存,I 是上一个进程工作的端口号,这里 + 1 是因为只有一个 worker, 如果 worker 数有 M 个,则加 M;
  5. 建好的连接便被 Load Balancer 转移到新进程的监听端口的 Working Port 上。

技术点4:客户端智能选路

尽管落地 QUIC 的愿望是好的,但是新事物的发展并不是一帆风顺的。由于 QUIC 是基于 UDP 的,而 UDP 相比于 TCP 在运营商的支持上并非友好,表现在:

  1. 在带宽紧张的时候,UDP 会经常被限流;
  2. 一些防火墙对于 UDP 包会直接 Drop;
  3. NAT 网关针对 UDP 的 Session 存活时间也较短。

同时,根据观察发现,不同的手机厂商对于 UDP 的支持能力也不同,所以在落地过程中,如果盲目的将所有流量完全切为 QUIC 可能会导致一些难以预料的结果。为此,我们在客户端上,设计了开篇介绍的 TCP 和 QUIC 相互 Backup 的链路,如下图所示,我们实时探测 TCP 链路和 QUIC 链路的 RTT、丢包率、请求完成时间、错误率等指标情况,并根据一定的量化方法对两种链路进行打分,根据评分高低,决定选择走哪种链路,从而避免寻址只走一条链路导致的问题。

做个总结

本文主要综述性的介绍了 QUIC 在蚂蚁的落地方案、场景以及一些关键技术。关键技术上,主要介绍了我们如何通过创造性的提出 QUIC LB 组件、以及多端口监听的机制来优雅的支持 QUIC 的连接迁移机制、QUIC 服务端的无损升级等,依赖这套方案我们的接入网关不需要像业界一样依赖底层内核的改动,这极大的方便了我们方案的部署,尤其在公有云场景下。除了连接迁移以外,我们还提出了 0RTT 建联提升方案、客户端智能选路方案,以最大化 QUIC 在移动端上的收益。截止当前,QUIC 已经在支付宝移动端以及全球加速链路两个场景上平稳运行,并带来了较好的业务收益。

未来规划

两年来,我们主要以社区的 gQuic 为基础,充分发挥 QUIC 的协议优势,并结合蚂蚁的业务特征以最大化移动端收益为目标,创造性的提出了一些解决方案,并积极向社区和 IETF 进行推广。在未来,随着蚂蚁在更多业务上的开展和探索以及HTTP3.0/QUIC 即将成为标准,我们会主要围绕以下几个方向继续深挖 QUIC 的价值:

  1. 我们将利用 QUIC 在应用层实现的优势,设计一套统一的具备自适应业务类型和网络类型的 QUIC 传输控制框架,对不同类型的业务和网络类型,做传输上的调优,以优化业务的网络传输体验;
  2. 将 gQUIC 切换成 IETF QUIC,推进标准的 HTTP3.0 在蚂蚁的进一步落地;
  3. 将蚂蚁的 QUIC LB 技术点向 IETF QUIC LB 进行推进,并最终演变为标准的 QUIC LB;
  4. 探索并落地 MPQUIC(多路径 QUIC) 技术,最大化在移动端的收益;
  5. 继续 QUIC 的性能优化工作,使用 UDP GSO, eBPF,io_uring 等内核技术;
  6. 探索 QUIC 在内网承载东西向流量的机会。

关注我们,每周 3 篇移动技术实践&干货给你思考!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用Rust获取猫咪表情包

发表于 2021-11-05

一起用代码吸猫!本文正在参与【喵星人征文活动】。

使用Rust获取可爱的猫咪表情包。

所需依赖

本文使用到的依赖有hyper、hyper-tls、scraper和tokio

  • hyper: HTTP底层实现库
  • hyper-tls: HTTPS实现库
  • scraper: 解析html库
  • tokio: Rust编程语言的异步运行时,提供异步事件驱动平台,构建快速,可靠和轻量级网络应用

如果目标网站不是HTTPS类型的话,可以不用hyper-tls依赖。创建client的时候使用Client::new()即可

步骤

  1. 创建项目
    在命令窗口中运行cargo new 项目名命令创建一个rust项目
  2. 加入依赖
    在Cargo.toml文件中加入上面所说的依赖。Cargo.toml如下:
1
2
3
4
5
6
7
8
9
10
11
12
toml复制代码[package]
edition = "2021"
name = "cat"
version = "0.1.0"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
hyper = {version = "0.14", features = ["full"]}
hyper-tls = "0.5.0"
scraper = "0.12.0"
tokio = {version = "1", features = ["full"]}
  1. 分析目标网站
    打开表情包网站搜索猫咪,按F12分析页面结构。使用定位按钮随便定位一张表情包元素,左键在出现的选项中负责该元素的选择器
    image.png

将复制的选择器粘贴在文本中,如下:
#post_container > li:nth-child(2) > div.thumbnail > a > img
结合页面元素分析可知,使用选择器div.thumbnail > a > img可以获得所有的表情包元素

  1. 构造并发起http/https请求, 获取页面数据
1
2
3
4
5
6
7
rs复制代码// 构建请求客户端
let https = HttpsConnector::new();
let client = Client::builder().build::<_, hyper::Body>(https);

let url = "http://www.*********.com/?s=%E7%8C%AB%E5%92%AA";
// 发起请求获取网页数据,
let response = client.get(url.parse()?).await?;
  1. 使用scraper解析响应数据
1
2
3
4
rs复制代码// 获取响应数据
let bytes = body::to_bytes(response.into_body()).await?;
// 解析HTML
let document = Html::parse_document(String::from_utf8(bytes.to_vec()).unwrap().as_ref());
  1. 使用CSS选择器找出需要的元素
1
2
3
4
5
rs复制代码let selector = Selector::parse("div.thumbnail > a > img").unwrap();

// 循环处理选择出来的img标签
for element in document.select(&selector) {
}
  1. 获取元素的src和alt属性
  • src:表情包的网络地址
  • alt:用于给表情包重命名
1
2
3
rs复制代码// 获取img标签的src属性
let src = element.value().attr("src").unwrap();
let alt = element.value().attr("alt").unwrap().to_string();
  1. 创建存储目录及构建表情包的名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
rs复制代码/**
* 根据img标签的alt属性,拼接文件保存的路径
*
*/
fn get_file_name(alt: &String) -> String {
let mut tmp_dir = "E:\\img\\".to_string();

// 如果该目标不存在则创建
fs::create_dir_all(&tmp_dir).unwrap();

let names: Vec<_> = alt.split("[").collect();
let name = names.first().unwrap();
let name = name.trim();
let name = name.replace("?", "");
tmp_dir += &name;
tmp_dir += &".gif";

return tmp_dir;
}
  1. 获取表情包并保存到本地
    请求上面src属性获取的地址,并使用fs::write方法将表情包保存到本地
1
2
3
4
5
6
rs复制代码 // 请求表情包的网络地址,获取表情包文件
let response = client.get(src.parse()?).await?;
let body = hyper::body::to_bytes(response).await?;

// 将文件写入本地磁盘
fs::write(&tmp_dir, body.iter())?;

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
rs复制代码use hyper::body;
use hyper::Client;
use hyper_tls::HttpsConnector;
use scraper::Html;
use scraper::Selector;
use std::fs;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 构建请求客户端
let https = HttpsConnector::new();
let client = Client::builder().build::<_, hyper::Body>(https);

let url = "http://www.*********.com/?s=%E7%8C%AB%E5%92%AA";
// 发起请求获取网页数据,
let response = client.get(url.parse()?).await?;

if res.status() != 200 {
panic!("请求失败");
}

// 获取响应数据
let bytes = body::to_bytes(res.into_body()).await?;
// 解析HTML
let document = Html::parse_document(String::from_utf8(bytes.to_vec()).unwrap().as_ref());
let selector = Selector::parse("div.thumbnail > a > img").unwrap();

// 循环处理选择出来的img标签
for element in document.select(&selector) {
println!("{:?}", element.value());

// 获取img标签的src属性
let src = element.value().attr("src").unwrap();
let alt = element.value().attr("alt").unwrap().to_string();

// 根据img标签的alt属性,拼接文件保存的路径
let tmp_dir = get_file_name(&alt);
println!("path: {}", tmp_dir);

// 请求表情包的网络地址,获取表情包文件
let response = client.get(src.parse()?).await?;
let body = hyper::body::to_bytes(response).await?;

// 将文件写入本地磁盘
fs::write(&tmp_dir, body.iter())?;
}

Ok(())
}

/**
* 根据img标签的alt属性,拼接文件保存的路径
*
*/
fn get_file_name(alt: &String) -> String {
let mut tmp_dir = "E:\\img\\".to_string();
// 如果该目标不存在则创建
fs::create_dir_all(&tmp_dir).unwrap();

let names: Vec<_> = alt.split("[").collect();
let name = names.first().unwrap();
let name = name.trim();
let name = name.replace("?", "");
tmp_dir += &name;
tmp_dir += &".gif";

return tmp_dir;
}

运行效果

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

浅谈函数栈帧

发表于 2021-11-05

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战」

很抱歉我用的是vs19,这个笔记本不可以装两个vs,性能不够,不是什么好电脑,但vs19只有调用main函数的那个“东西”看不到,其他的还是可以看到很多的,在开辟空间上优化很多。废话不多说直接上

错误的地方本文有的地方ebp写成edp 由于修改地方太多就不改了,基本是写到一半才发现哎

什么叫栈帧

可以直接说他就是一个空间,再准确点是存储空间。

该明白的一点知识

1.ebp(栈底指针),esp(栈顶指针)这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。

2.从高地址向低地址移动

3.压栈push :esp上移也就是朝低地址移动

出栈pop:也叫弹出,栈顶元素弹出,esp下移

4.每一个函数调用,都要在栈区创建一个空间

讲例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include复制代码int Add(int a, int b)
{
int sum = 0;
sum = a + b;
return sum;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
printf("%d\n",c);
return 0;
}

调试

我们首先进入调试,然后进入反汇编,只有底层才能讲的透彻。打开调用堆栈窗口。我们粗略调一遍会发现在调用堆栈里面我们看到了main函数,说明什么,有人在调用它,vs19我没看到,所以借vs13的来用一下

我们非常模糊的看到main函数被__tmainCRTStartup()调用,而这个函数还被mainCRTStartup调用

push ebp 就是说push压栈 把ebp压到esp顶部导致esp上移

mov ebp,esp 是把esp的地址交给edp

这两步实现了esp,edp都指向了同一个地址

sub esp,0E4h sub是把esp减去0E4h导致esp上移也就是开辟了空间

3push 依次压栈edx,esi,edi

lea (load effective address)加载有效地址

word是两个字节,dword(double word)双字四字节

上面的意思就是从edi开始向下的9个 字节全都初始化成eax的内容CCCC…

(这里当时忘了监测,重新调试了,所以地址都变了)但基本现象没怎么变。

到了这里也就意味着main函数栈帧的开辟 也就准备完了

mov 把0Ah(也就是10)放到ebp-8的位置上,而ebp-8实际上就是为a开辟一个空间

至于为什么a,b,c为什么不是紧密排的,这是编译器的原因,是为了防止数据紧密容易出错


mov 把ebp-20给eax,上面可以看到那是b的空间。

然后把eax压栈一下。

把a给ecx再把ecx压栈一下。

然后call我愿称他是奇计,我们可以非常清楚的看到a’上面的就是call指令的下一条指令的地址。他这一步是 在调用函数的同时把下一条指令的地址给压上去,这是回来的标记

这时才是真正的来到我们的Add函数上面和我们讲main函数栈帧一样

参数是从右向左压栈的,从上面我们也可以清楚的看到形参不是在Add函数内部创建的,而是回来找我们传参的空间,也直接的证明了形参是实参的临时拷贝这句话

请注意大风大浪过去了,千万不要在阴沟里面翻船

这里就是返回的变量明明销毁了,那如何把值给带回来的。把ebp-8里面的值也就是sum 放到eax里面,这里的eax可是寄存器啊,寄存器是不会因为程序退出就销毁的,相当于拿一个全局的寄存器把他保存起来,等我们到main函数里面再把它拿出来

pop弹出 把栈顶的元素取出放到edi里面去,依次pop三次,esp就加4加4往下走。当我们函数调用完了那这个空间就没必要存在了,所以把ebp的地址给esp

当esp指到这的时候pop一下把栈顶的元素弹出来,他里面放的是main函数的栈底指针,把结果弹到ebp里面去就可以瞬间到main栈底了

ret这条指令就是栈顶弹出call下一条指令地址然后跳过去,回来后就到了call下一条指令地方

这个add 就是把形参的空间还给操作系统

然后把eax的值给ebp-32空间就是c空间

结束

到了这里也就结束啦,只是非常浅显的讲解了一点,肯定还有好多地方没有讲到,寄存器这些就更底层了,也不是我们函数栈帧讲的主要内容,有机会以后再写寄存器的文章吧。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

代码覆盖率在敏捷式软件开发过程中的实践 五、总结&规划

发表于 2021-11-05

一、前言

代码测试覆盖率是一种度量,它描述了对程序源代码的测试程度,是白盒测试的一种手段,能够直观暴露测试用例无法覆盖到的代码块。作为提升代码质量的利器,爱奇艺开发团队和QA团队在如何接入、使用等方面做了一些探索性的尝试与实践。

目前代码覆盖率大多停留在整体层面,没有细化到更小的粒度。在协同开发过程中,如果能精细到需求粒度级别覆盖率,关联到具体的负责人,将能有效提高测试效率和精度。

本文将从代码覆盖率接入和使用的角度,叙述了如何结合代码管理、项目管理等DevOps工具进行精细化代码覆盖率统计以帮助提高代码质量。

二、背景

互联网公司软件开发模式多为敏捷式或者迭代式开发模型,弥补了传统开发模式的一些弱点,具有更高的成功率和开发效率,能够先尽快地将产品投向市场,再通过用户的广泛参与,不断修改优化产品,实现产品的快速迭代,更好地适应用户市场变化。

不过随着项目的迭代,功能场景变多,代码工程会愈加庞大复杂,代码质量也因此至关重要。如何在开发测试周期提升代码质量,代码测试覆盖率作为一个可以衡量代码质量的标准,如何接入使用覆盖率数据,是一个值得深入探索实践的方向。

  • 定义

代码覆盖(英语:Codecoverage)是软件测试中的一种度量,描述程序中源代码被测试的比例和程度,所得比例称为代码覆盖率。

  • 代码覆盖率的意义
  1. 完善测试用例:从代码覆盖率报告可以明确,哪些代码是执行过的,哪些代码是没有执行过的。对于没有执行过的代码,测试人员需要思考下是代码逻辑设计问题还是测试用例问题。如果是代码逻辑问题,那么需要与开发人员和产品人员沟通,达成需求的一致性理解。如果是测试用例问题,那么需要补足缺失的测试场景,尽可能保证所有重要的场景都覆盖到了,避免未测试的代码上线造成服务故障。
  1. 提升代码质量:开发人员从覆盖率报告可以获得代码的执行流程走向,帮助理解代码逻辑。分析生产环境的代码覆盖率报告,还可以区分出真实的用户请求执行覆盖情况,区分“有用/无用”代码,有利于优化简化代码逻辑。
  1. 代码质量标准:代码覆盖率是一个确切的数值,将模糊定义的代码质量用精准的数据来量化,是白盒测试和代码质量的衡量指标。

从代码覆盖率报告可以明确,哪些代码是执行过的,哪些代码是没有执行过的,从未执行过的代码可以推算出测试用例是否充足,帮助测试人员理清代码逻辑和完善测试用例。代码测试覆盖率作为一项衡量代码质量的重要标准,可以在测试阶段发现隐藏问题,将隐性问题消灭在萌芽状态,减少线上问题发生。

  • 现状

开源的覆盖率实现工具有很多,但是在实际使用中,还需要做比较多的准备工作,经常遇到以下问题:

  • 无法区分增量代码

增量代码是指新修改的代码,从全量代码中区分出增量代码的好处是,让使用者只关注于自己负责的代码,去除干扰,减少人工筛选成本。而覆盖率工具只是用于生成代码覆盖率(全量)的工具,没有区分增量代码。

  • 使用成本高

覆盖率报告只是一份综合的报告,使用者通常只会关心自己的代码覆盖率,在多人协同开发过程中,如何将某一处代码覆盖率与具体的需求(开发人员、测试人员)关联是一个问题。

  • 测试环境多人协同多次修改变化性大,维护成本高

在实际的开发测试周期中,可能会出现修改代码再测试,测试后再修改代码的现象,代码覆盖情况是一个变化的过程。考虑到覆盖率工具只能对固定的代码进行收集,如何减少此变化过程的维护成本是需要解决的。

三、目标和功能

基于以上问题,爱奇艺开发团队在覆盖率生成工具基础上,整合DevOps工具,以相对较小的成本快速接入生成代码覆盖率,并支持计算生成代码分支级别、需求级别的精细粒度增量代码覆盖率报告。以下以Java为例,简单阐述实现方案和原理。

业务架构

  • 简化接入流程

使用方仅关心如何将生产出来的java文件、class文件和exec文件推送到指定位置,即可自动化生成详细的覆盖率报告。

  • 增量代码报告

利用diff算法,区分出增量代码,生成单独的增量报告和全量报告。

[增量&全量代码报告]

  • 代码分支级别代码覆盖率

计算测试分支中(某一个代码分支所占的)代码覆盖率。

[代码分支级别报告]

  • 需求级别代码覆盖率

计算Jira中某一个需求(issue涉及到的)代码覆盖率。

[需求级别报告]

四、技术实现

下面以Java为例,简要说明技术实现原理。考虑到目前维护性比较好的覆盖率工具,选择了JaCoCo开源工具。

运行原理

[运行原理]

JaCoCo生成覆盖率报告需要三个依赖数据:源码Java文件、编译class文件和代码探针打桩执行的结果exec文件。

  1. 源码Java文件和编译class文件是通过CI job的方式push到服务端,exec文件是jvm 通过tcp连接将内存数据导出到服务端。
  1. 汇总三部分文件,利用JaCoCo工具生成代码的全量代码覆盖率报告。
  1. 从Gitlab平台获取到测试分支与master分支代码diff结果,即增量代码,结合其他三部分数据,生成增量代码覆盖率报告。
  1. 通常测试代码分支是多人协作的结果,很多feature代码分支都会合并到测试分支中,再通过git commit溯源关系,得到增量代码的初始来源分支,结合覆盖率报告可以得到每个代码分支的覆盖率报告。
  1. 最后,将Jira需求和代码分支关联,得到需求涉及的代码覆盖率报告。

核心技术实现

  • JaCoCo改造

JaCoCo覆盖率报告生成条件是Java源代码、编译后的Class文件和生成的exec(jvmexecution data,.exec类型)覆盖率数据。其中exec覆盖率数据是由probe探针生成的二进制文件,包括SessionInfo(此次数据的来源id、开始时间、结束时间)和 ExecutionData(每个类文件对应的探针boolean类型数组)。Jacoco官方给出导出exec数据的方式有三种:File System、TCP Socket Server和TCP SocketClient。现有的几种方式存在一定局限性,无法满足灵活自动上报的要求,并且如果jvm异常关闭,会导致数据直接丢失。所以,在TCP Socket Client基础上进行优化改造,支持了Schedule定时上报功能,通过配置化定时参数,允许随时上报,灵活配置。

  • 增量代码覆盖率的生成

利用git工具对比测试分支与master分支,获取增量代码(新增和修改的代码)。根据增量代码找出匹配的Jacoco执行exec文件,再利用asm框架,分析计算得到增量覆盖率数据。

  • 代码分支级别覆盖率

得到增量代码覆盖率后,再通过git commit信息,溯源找到所有代码的初始提交分支。

  • 需求级别代码覆盖率

为了便于关联代码和需求,以及快速识别相关开发和测试负责人,对代码分支的命名做了一些规范。

从代码分支中检索出符合规范的代码,查询Jira项目管理系统,进行关联。Jira需求对应的代码分支级别覆盖率即为需求的代码测试覆盖率。

需求类型 | 分支命名前缀 | 示例

—|—|—

跟版需求 | feature/v{version}/{issue_id}_xxx| feature/v11.7.0/JIRA-9443_xxx

纯后端需求 | feature/backend/{issue_id}_xxx |feature/backend/JIRA-9444_xxx

  • 使用要求

为了提升代码质量和减少线上问题的出现,对增量代码的覆盖率提出了比较严格要求,低于标准的情况都需要备注说明原因。

| 类型 | 标准 |

—|—

| 版本交付标准 | 所有代码增量覆盖率>=90% |

| 测试通过标准 | 需求分支增量覆盖率>=90% |

| 代码上线标准 | 代码分支增量覆盖率>=90% |

覆盖率高不等同于代码质量高,但覆盖率不高说明存在有未测试过的代码,这些代码上线后可能会成为服务稳定性方面的隐性问题。但是一味得追求高覆盖率也是不可取的。一方面,无效的代码(例如判空类型的代码)可能无法覆盖;另一方面,追求高覆盖率付出的人力成本会比较高。

  • 需求测试上线流程

[需求测试上线流程]

测试人员根据需求内容制定测试用例,在测试环境进行测试验证完毕后,根据生成的需求测试覆盖报告,衡量是否达标(检查覆盖率是否达到预期、关键的分支判断是否覆盖等)。如果不达标,需要进行补充用例或者需要开发人员进行代码逻辑修复,再进行重复测试,达标后的需求方可进行审批上线到生产环境。在此过程中,平台会自动生成该需求关联的变更代码覆盖情况,避免了复杂协作环境下其他人代码对该需求影响,测试人员可以较快地发现未覆盖代码,并与上线流程打通,减少未覆盖代码带来的隐性问题上线情况,一定程度上保证了服务质量。

五、总结&规划

本文介绍了爱奇艺开发团队和QA团队在测试覆盖率方面的探索和实践,在代码测试覆盖率工具基础深入研究改造,结合其他开发协作平台,支持更快速的集成接入方式,计算展示了增量代码报告、分支覆盖报告、需求覆盖报告等更多维度报告,为使用者提供了极大的便利性。在平台搭建后,平台能快速接入更多的服务,为测试人员提供更多维度的参考衡量标准,重点后端应用服务代码测试覆盖率从不足70%提升到90%+,在一定程度上有效地保证了服务质量。

目前的覆盖率数据是来源于测试环境的,只限于开发测试周期使用。后续会在生产环境的代码覆盖率方向上做出一些尝试,例如,利用生产环境的代码执行的覆盖率报告自动化筛选一些关键接口请求作为拨测用例、将生产环境的流量请求测试环境对比生产环境和测试环境的覆盖率报告差别以衡量改动影响等。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

垃圾代码和优质代码的区别?

发表于 2021-11-05

作者:三省吾身丶丶

链接:zhuanlan.zhihu.com/p/99246269

几个业务场景中的重构示例

请求顺序依赖

在这种场景中,首先还是业务的复杂度决定了代码的复杂度。首先我们来看一个在前端和node都有可能出现的一个简单的例子:

我们有 A, B, C, D 四个请求获取数据的函数(函数自己实现), C 依赖 B 的结果,D 依赖 ABC 的结果,最终输出 D 的结果。

错误示例

虽然这个代码是故意写成这样的,不过确实也有在一些初学者身上看到过。这份代码还是能正确给出结果的,但是写法丑陋,回调地狱。如果后来人不进行重构,还有请求依赖,得继续回调嵌套。性能太差,没有考虑 A 和 B 实际上是可以并发的。

这里介绍了一下最原始的 callback … 中间大家可以去回顾一下 整个 ES2015+ ,callback (async.js) –> Promise –> generator + co –> async + await 的进化过程。其实是从原生的语法层面不断去简化和增强我们对于异步的控制能力。

下面直接给目前阶段原生提供的终极方案:基于 Promise + async/await

正确示例

我们重新思考了一下上面的问题,理清楚了逻辑顺序的依赖。并且用最新的语法。

使用 Promise.all 结合 async/await 的形式,考虑了并发和串行,写法简洁,达到了在示例要求下的最快方案。解决了无限嵌套的问题。这是跟随语言进化本身带给我们可以进行的优化。

但又不仅仅如此。我们将问题进行归类 将 B,C 有依赖顺序的请求,抽离出单独的函数。让他们去处理自身的逻辑。这个点我们稍后再提。

折磨人的 if else

可能存在下面一些问题

  1. 过多的嵌套
  2. 逻辑处理冗余
  3. 没有做好防御编程(错误处理

直接来一个代码例子,这是一个获取背景颜色的方法,但是随着业务的不断变化,背景颜色的来源越来越多,在一些业务人员的处理下可能是这样的:

错误示例

相信你在读上面的代码的时候是极为痛苦的,想要一目了然的知道最终会进入哪个分支,基本不可能。于是基于下面两个原则

  • 合理的抽取成函数
  • 错误优先返回

有了一个基础版本的重构:

正确示例

可以看到整个逻辑,经过了重新梳理。拆分成了三个函数,子方法分别去处理对应层级的逻辑,由一个主方法负责调度。整体都变得一目了然了。

当然,在我们基于上面的原则进行重构之后,这个代码有没有问题呢?当然有。可以看到我们这三个函数,都依赖了全局变量。函数本身就不纯了。如果是全局的问题,还是不易于排查。

我们可以将其修改为纯函数,让这一份代码易于理解和测试。

以一个函数的修改为示例:我们将 全局变量变成了参数,只需要在调用的时候,将全局变量传入即可,但是这样,我们得到了一个纯函数。

为什么会在这里特别强调这个点呢,其实在函数式编程中的一个最基础的问题那就是纯函数。只有这样输入输出才是可被观测的,一个输入一定会有一个输出。也只有通过这样的方式,才能让系统中非纯的函数越来越少。让代码变得更易于测试。

当然作为我们如果以重构的角度去思考的话,我们还需要关注到这个点:

这里的逻辑会将会 最后一个被匹配到的数据,设置为 bgColor 。(我们都知道 find indexOf 等基本都是从前匹配。)是否真的是业务的需求呢?

可以看到将业务代码写好/重构的过程中其实也是对业务逻辑和业务理解的再一次提升。不论是抽取成函数还是错误优先返回的设计,这其实也都是可以解决这样一个问题:能在不去读懂全局的情况下,了解某一个区域的细节逻辑,也就做到了让代码易于理解和修改。

… 这里的代码即便是经过这样的重构后,依然有可以考虑进一步优化的空间,比如函数与参数的命名,完整的测试用例等等,受限于文章篇幅,暂不展开说明。

一些代码中可能存在的其他问题

  1. 逻辑耦合在视图层。

a === 'a' && b ==='b' && c==='c' && d ==='d'? <div>...</div>:null
2. 组件复用,函数复用,不封装,代码重复。
3. 函数功能不单一,一个函数处理太多职责。且这些职责没有任何关联,但是都耦合在同一个区块内。
4. 参数列表混乱,有做好防御编程,不处理错误(接口错误,超时,重复提交等等
5. 魔法数字,魔法字符串,且没说明。
6. 糟糕数据结构 / 糟糕命名 (其实上面的具体代码示例也存在)

关于优化代码的思想准备

首先来说一下为什么会说需要优化代码?

  1. 技术追求。
  2. 公司要求,线上有系统在用。有用户在用,不写好出问题实际上苦的还是自己。
  3. 团队协作,我不好好写,团队成员其他人也不好好写,恶性循环苦的还是自己。
  4. 快速迭代。系统需要不断的增加新功能。必须要写好代码才能做到。
  5. 其他人的看法,怕别人觉得自己技术能力差… xxxx….

那么就会有下面这些要求:

  • 易于理解系统的架构
  • 易于理解系统的生命周期与执行流程
  • 易于理解每一个函数的作用
  • 易于理解函数之间是如何调用与传递的(输入输出)
  • 易于理解变量的含义,表达式的含义。
  • 易于扩展…

最终实际上又回到了写出来的代码应该是 整洁的代码,要使代码易于理解/修改/测试。(这里其实大部分时候,都隐含了一个人员协作的条件在里面,所以,既要写好代码,又不能过度封装,让团队其他成员看不懂(当然如果确实有些人经验不够,那么是他自身的问题,需要他自己去加强。))

一些建议

  1. 更加清晰的去了解业务,去思考可能的变化。思考和设计清楚再动手。
  2. 看一些开源项目与业界最佳实践,明白什么样的是好代码,什么样的是不好的代码。
  3. 建立明白代码虽然是给计算机运行的,但最终还是人看的。不仅仅是没有 bug 就行了,这样的心智模型。
  4. 建立业务与代码质量同等重要的思考模型。避免因为时间导致的不得不这么写的代码。
  5. 明白 code review 本身可能能发现和指出来一些问题,但最终的落实还的靠自己,不能变成形式,而是需要融合成自身的思考。
  6. 使用错误优先原则。尽可能的让出错的先返回, 这样后面就会得到干净的代码。(写代码的时候,不仅仅正向,反向的判断也需要思考)
  7. 合理的拆分成独立的函数。明确输入输出,错误处理等在函数内部的处理。(比如在一些场景中确实会存在大量逻辑判断,首先就要思考在判断内部的语句是否能被归类与拆分出去)
  8. 对于多种状态的判断与组合,可以使用 组合状态表 (map表)状态机等模式
  9. 学习设计模式与重构等相关知识。
  10. 重构!!只要你觉得这个地方有问题了,那就不要等到以后。以后往往就是再也不。

结束

说到这可能会有一种戛然而止的感觉。在这一篇文章里面,我们首先以两个优化代码的具体实例为引子,让大家明白了一些业务代码的优化思路。在之后从列举了一些其他可能出现的错误,以及是优化代码的思想准备和理论指导。

其实都是希望大家能够在业务中去发现问题,再去思考如何解决问题,因为说了那么多,到底能不把代码写好。还是得靠自己。

近期热文推荐:

1.1,000+ 道 Java面试题及答案整理(2021最新版)

2.别在再满屏的 if/ else 了,试试策略模式,真香!!

3.卧槽!Java 中的 xx ≠ null 是什么新语法?

4.Spring Boot 2.5 重磅发布,黑暗模式太炸了!

5.《Java开发手册(嵩山版)》最新发布,速速下载!

觉得不错,别忘了随手点赞+转发哦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

idea jvm调优

发表于 2021-11-05

1、背景

idea作为一个高频使用的java IDE。性能的好坏,影响着开发的心情。工欲善其事必先利其器。

2、优化后的参数

优化后的感觉是拙匠常怪工具差。明明可以流畅的编码,为啥之前选择的是默默忍受,不去改变。真的是流畅了很多。

当前电脑的配置是 windows i5 8核16G

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
ruby复制代码# custom IntelliJ IDEA VM options

# 堆大小,按常规操作,设成相同的,避免自动扩容
-Xms1536m
-Xmx1536m
# 年轻代大小,Sun推荐设置为堆大小的3/8
-Xmn576m
# 在JVM启动时即预初始化堆中的所有页,能够快速利用
-XX:+AlwaysPreTouch

# 设置一个较大的元空间初始值,避免频繁GC扩容
-XX:MetaspaceSize=256m
# 元空间最大默认不限制,设一个值保护一下
-XX:MaxMetaspaceSize=768m

# 启用G1 GC
# -XX:+UseG1GC

# 启用CMS GC
-XX:+UseConcMarkSweepGC
# CMS并行标记,降低标记阶段停顿时间
-XX:+CMSParallelRemarkEnabled
# 重新标记前先执行一次新生代GC
-XX:+CMSScavengeBeforeRemark
# 触发CMS GC的堆内存占用比例,调大点以降低GC频率
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly

# 对象晋升到老年代的年龄,默认15。根据观察,对IDEA来说设成10就足够了
-XX:MaxTenuringThreshold=10

# 压缩普通对象指针
-XX:+UseCompressedOops

# 指定服务器版JIT编译器,其实不用写,默认已经是了
-server
# JIT代码缓存的大小,默认是240M
-XX:ReservedCodeCacheSize=360M
# 打开JIT分层编译,默认是开启的了
-XX:+TieredCompilation
# 每MB堆空间中的软引用能够存活的近似毫秒数
-XX:SoftRefLRUPolicyMSPerMB=50

# OOM时输出堆dump转储文件
-XX:+HeapDumpOnOutOfMemoryError
# 禁止把某些异常的stack trace优化掉,防止信息被吃了找不到问题
-XX:-OmitStackTraceInFastThrow
# 禁用字节码验证。IDEA的代码足够可靠,不用验证
-Xverify:none
# 启用断言机制(enable assertion)
-ea

-Dfile.encoding=UTF-8
-Dsun.io.useCanonCaches=false
-Djava.net.preferIPv4Stack=true
-Djdk.http.auth.tunneling.disabledSchemes=""

-XX:ErrorFile=$USER_HOME/java_error_in_idea_%p.log
-XX:HeapDumpPath=$USER_HOME/java_error_in_idea.hprof
-javaagent:D:\software\JetBrains\IntelliJ IDEA 2019.2\bin\jetbrains-agent.jar

3、cutom vm options和idea.exe.vmoptions的区别

  • idea.exe.vmoptions是开发工具自带的,不建议修改,因为升级的时候会进行覆盖
  • cutom vm options是用户自定义的,是留给使用者个性化配置的。默认保存在用户目录下的 .IntelliJIdea2019.2/config
  • 实际操作是idea.exe.vmoptions是全局的配置,cutom vm options会对定义的配置进行覆盖

4、修改配置后无法启动

报错的信息如下:

MaxJavaStackTraceDepth=-1 is outside the allowed range,本质是配置文件的格式不正确或包含了不能被识别的属性。

实际上的 -XX:+UseParNewGC:设置年轻代为多线程收集 这个属性被废弃了。

jdk10以上UseParNewGC被废弃了

idea自带的jdk是jdk11,所以是不包含这个属性的。与表象是一致的。
idea自带的jdk是jdk11

idea jvm调优,MaxJavaStackTraceDepth=-1 is outside the allowed range

还在使用idea的默认jvm参数吗?那么是时候动手优化一下你的idea了,体验一下飞一般的感觉。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Terraform 基于Azure上使用模块(三)

发表于 2021-11-05

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

五、使用模块部署和输出

当我们在TerraformDemo目录中运行我们的Terraform init时,我们可以看到模块初始化。

1
2
3
4
5
6
7
8
erlang复制代码Initializing modules...
- storage_account in modules/storage-account

Initializing the backend...

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "azurerm" (hashicorp/azurerm) 1.38.0...

当我们运行terraform apply时,它将引用存储帐户模块,并使用模块输入中声明的设置创建我们的存储帐户。此外,我们可以在具有不同参数字符串的配置中多次使用相同的模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ini复制代码provider "azurerm" {
version = "1.38.0"
}

#create resource group
resource "azurerm_resource_group" "rg" {
name = "rg-MyFirstTerraform"
location = "westus"
}

#Create Storage Account
module "storage_account" {
source = "./modules/storage-account"

saname = "statfdemosa234"
rgname = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
}

#Create Storage Account
module "storage_account2" {
source = "./modules/storage-account"

saname = "statfdemosa241"
rgname = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
}

我们刚刚创建了第一个模块。此模块非常简单,但是,对于使用加密磁盘部署虚拟机的更复杂的场景,模块可以完美地抽象几个输入的所有复杂性。

可变输入不是模块的唯一重要部分。产出同样重要。它们允许我们将信息传输到模块,以便它们可以彼此构建。为模块创建输出与常规Terraform配置相同。

创建一个输出.TF文件并使用输出块声明输出值。下面我们正在为我们的存储帐户创建一个输出块主访问键,所以我们可以在创建它之后将其存储在Azure Key Vault中:

1
2
3
4
5
ini复制代码output "primary_key" {
description = "The primary access key for the storage account"
value = azurerm_storage_account.sa.primary_access_key
sensitive = true
}

另请注意,我们正在使用敏感的参数来指定我们的存储帐户的Primary_Access_Key输出包含敏感数据。Terraform将把这些信息视为机密,并在运行Terraform时将其从控制台显示隐藏。

这不会保护Terraform的状态文件中的值;它仍然是清晰的文本,这就是为什么在真实的生产方案中,我们希望使用远程状态。

要在另一个资源中使用模块的输出值,请通过在模块中引用它来指定值,例子如下:

1
2
3
4
5
6
7
ini复制代码#<modulename> <outputname>
resource "azurerm_key_vault_secret" "stasecret" {
name = "statfdemosa234-secret"
value = module.storage_account.primary_key
key_vault_id = azurerm_key_vault.kv.id

}

少年,没看够?点击石头的主页,随便点点看看,说不定有惊喜呢?欢迎支持点赞/关注/评论,有你们的支持是我更文最大的动力,多谢啦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

springmvc的定时任务

发表于 2021-11-05

这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

定时任务需求是springmvc提供的一个常见功能,但是配置特别繁琐,本文将介绍如何配置并且使用定时任务。

1.配置定时任务

需要在spring-mvc的配置文件中加入以下配置。

1
2
3
4
js复制代码    <task:executor id="executor" pool-size="10" queue-capacity="128" />
<task:scheduler id="scheduler" pool-size="10" />
<task:annotation-driven executor="executor"
scheduler="scheduler" proxy-target-class="true" />

注意:需要在xml引用上加入以下头信息,否则会报异常。

1
2
3
4
5
6
7
js复制代码<!-- 任务线程池 -->    
<!-- 任务执行器线程数量(一个任务需要并发执行的线程数) -->
xmlns:task="http://www.springframework.org/schema/task
<!-- 任务调度器线程数量(根据定时器的个数设置) -->
http://www.springframework.org/schema/task
<!-- 启用annotation 识别@Scheduled注解-->
http://www.springframework.org/schema/task/spring-task-3.0.xsd

在使用时,直接使用@Scheduled注解即可,可以按照下文编写。

1
2
3
4
5
6
7
8
9
10
js复制代码@Component
public class SchedulingConfigN {
private static final Logger LOG = LoggerFactory.getLogger(SchedulingConfigN.class);
@Scheduled(cron = "0/20 * * * * ?") /从第0秒开始,每隔20秒执行一次 从前往后分别是 秒 分 时 日 月 年
// 如果一个小时执行一次就是 0 0 */1 * * ?
// 每天的11点执行一次 0 0 11 * * ?
public void scheduing() {
System.out.println("定时任务启动1111");
}
}

2.从数据库获取定时任务执行频率

上文配置有一个问题,就是如果想更改执行的频率,需要更改代码,然后打包,才能生效。那么是否能在数据库中配置执行频率,通过修改数据库就能够更改呢。下文提供代码供大家参考。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
js复制代码@Configuration
@EnableScheduling
public class JobConfig implements SchedulingConfigurer {

@Autowired
@Qualifier("timingTasks")
TimingTasks timingTasks;

//获取定时任务的时间service
@Bean(name={"timingTasks"})
public TimingTasks timingTasks() {
return new TimingTasks();
}

/**
* 执行定时任务.
*/
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(
// 1.添加任务内容(Runnable)
() -> System.out.println("执行定时任务2: " + LocalDateTime.now().toLocalTime()),
// 2.设置执行周期(Trigger)
triggerContext -> {
// 2.1 从数据库获取执行周期
System.out.println("开始执行");
List<SaleProductFileBO> resultBO = timingTasks.selectTimingTasks(Long.valueOf(2));
//获取定时任务的时间
String cron = resultBO.get(0).getFiles();
// 2.3 返回执行周期(Date)
return new CronTrigger(cron).nextExecutionTime(triggerContext);
});
}

}

如果大家生产环境有该需求,建议了解和使用xxl-job。开源且好评如潮。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

深入理解 Linux I/O 系统

发表于 2021-11-05

传统的 System Call I/O

在 Linux 系统中,传统的访问方式是通过 write() 和 read() 两个系统调用实现的,通过 read() 函数读取文件到到缓存区中,然后通过 write() 方法把缓存中的数据输出到网络端口。

1
2
lua复制代码read(file_fd, tmp_buf, len);
write(socket_fd, tmp_buf, len);

下图分别对应传统 I/O 操作的数据读写流程,整个过程涉及 2 次 CPU 拷贝、2 次 DMA 拷贝,总共 4 次拷贝,以及 4 次上下文切换。

CPU 拷贝:由 CPU 直接处理数据的传送,数据拷贝时会一直占用 CPU 的资源。

DMA 拷贝:由 CPU 向DMA磁盘控制器下达指令,让 DMA 控制器来处理数据的传送,数据传送完毕再把信息反馈给 CPU,从而减轻了 CPU 资源的占有率。

上下文切换:当用户程序向内核发起系统调用时,CPU 将用户进程从用户态切换到内核态;当系统调用返回时,CPU 将用户进程从内核态切换回用户态。

读操作

当应用程序执行 read 系统调用读取一块数据的时候,如果这块数据已经存在于用户进程的页内存中,就直接从内存中读取数据。

如果数据不存在,则先将数据从磁盘加载数据到内核空间的读缓存(Read Buffer)中,再从读缓存拷贝到用户进程的页内存中。

)

read(file_fd, tmp_buf, len);

基于传统的 I/O 读取方式,read 系统调用会触发 2 次上下文切换,1 次 DMA 拷贝和 1 次 CPU 拷贝。

发起数据读取的流程如下:

用户进程通过 read() 函数向 Kernel 发起 System Call,上下文从 user space 切换为 kernel space。

CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到 kernel space 的读缓冲区(Read Buffer)。

CPU 将读缓冲区(Read Buffer)中的数据拷贝到 user space 的用户缓冲区(User Buffer)。

上下文从 kernel space 切换回用户态(User Space),read 调用执行返回。

写操作

当应用程序准备好数据,执行 write 系统调用发送网络数据时,先将数据从用户空间的页缓存拷贝到内核空间的网络缓冲区(Socket Buffer)中,然后再将写缓存中的数据拷贝到网卡设备完成数据发送。

write(socket_fd, tmp_buf, len);

基于传统的 I/O 写入方式,write() 系统调用会触发 2 次上下文切换,1 次 CPU 拷贝和 1 次 DMA 拷贝。

用户程序发送网络数据的流程如下:

用户进程通过 write() 函数向 kernel 发起 System Call,上下文从 user space 切换为 kernel space。

CPU 将用户缓冲区(User Buffer)中的数据拷贝到 kernel space 的网络缓冲区(Socket Buffer)。

CPU 利用 DMA 控制器将数据从网络缓冲区(Socket Buffer)拷贝到 NIC 进行数据传输。

上下文从 kernel space 切换回 user space,write 系统调用执行返回。

网络 I/O

)

磁盘 I/O

)

高性能优化的 I/O

零拷贝技术。

多路复用技术。

页缓存(PageCache)技术。

其中,页缓存(PageCache)是操作系统对文件的缓存,用来减少对磁盘的 I/O 操作,以页为单位的,内容就是磁盘上的物理块,页缓存能帮助程序对文件进行顺序读写的速度几乎接近于内存的读写速度,主要原因就是由于 OS 使用 PageCache 机制对读写访问操作进行了性能优化。

页缓存读取策略:当进程发起一个读操作 (比如,进程发起一个 read() 系统调用),它首先会检查需要的数据是否在页缓存中:

如果在,则放弃访问磁盘,而直接从页缓存中读取。

如果不在,则内核调度块 I/O 操作从磁盘去读取数据,并读入紧随其后的少数几个页面(不少于一个页面,通常是三个页面),然后将数据放入页缓存中。

)

页缓存写策略:当进程发起 write 系统调用写数据到文件中,先写到页缓存,然后方法返回。此时数据还没有真正的保存到文件中去,Linux 仅仅将页缓存中的这一页数据标记为 “脏”,并且被加入到脏页链表中。

然后,由 flusher 回写线程周期性将脏页链表中的页写到磁盘,让磁盘中的数据和内存中保持一致,最后清理“脏”标识。在以下三种情况下,脏页会被写回磁盘:

空闲内存低于一个特定阈值。

脏页在内存中驻留超过一个特定的阈值时。

当用户进程调用 sync() 和 fsync() 系统调用时。

存储设备的 I/O 栈

)

由图可见,从系统调用的接口再往下,Linux 下的 IO 栈致大致有三个层次:

文件系统层,以 write 为例,内核拷贝了 write 参数指定的用户态数据到文件系统 Cache 中,并适时向下层同步。

块层,管理块设备的 IO 队列,对 IO 请求进行合并、排序(还记得操作系统课程学习过的 IO 调度算法吗?)。

设备层,通过 DMA 与内存直接交互,完成数据和具体设备之间的交互。

结合这个图,想想 Linux 系统编程里用到的 Buffered IO、mmap、Direct IO,这些机制怎么和 Linux I/O 栈联系起来呢?上面的图有点复杂,我画一幅简图,把这些机制所在的位置添加进去:

)

这下一目了然了吧?传统的 Buffered IO 使用 read 读取文件的过程什么样的?假设要去读一个冷文件(Cache 中不存在),open 打开文件内核后建立了一系列的数据结构,接下来调用 read,到达文件系统这一层,发现 Page Cache 中不存在该位置的磁盘映射,然后创建相应的 Page Cache 并和相关的扇区关联。然后请求继续到达块设备层,在 IO 队列里排队,接受一系列的调度后到达设备驱动层,此时一般使用 DMA 方式读取相应的磁盘扇区到 Cache 中,然后 read 拷贝数据到用户提供的用户态 buffer 中去(read 的参数指出的)。

整个过程有几次拷贝?从磁盘到 Page Cache 算第一次的话,从 Page Cache 到用户态 buffer 就是第二次了。而 mmap 做了什么?mmap 直接把 Page Cache 映射到了用户态的地址空间里了,所以 mmap 的方式读文件是没有第二次拷贝过程的。

那 Direct IO 做了什么?这个机制更狠,直接让用户态和块 IO 层对接,直接放弃 Page Cache,从磁盘直接和用户态拷贝数据。好处是什么?写操作直接映射进程的buffer到磁盘扇区,以 DMA 的方式传输数据,减少了原本需要到 Page Cache 层的一次拷贝,提升了写的效率。对于读而言,第一次肯定也是快于传统的方式的,但是之后的读就不如传统方式了(当然也可以在用户态自己做 Cache,有些商用数据库就是这么做的)。

除了传统的 Buffered IO 可以比较自由的用偏移+长度的方式读写文件之外,mmap 和 Direct IO 均有数据按页对齐的要求,Direct IO 还限制读写必须是底层存储设备块大小的整数倍(甚至 Linux 2.4 还要求是文件系统逻辑块的整数倍)。所以接口越来越底层,换来表面上的效率提升的背后,需要在应用程序这一层做更多的事情。所以想用好这些高级特性,除了深刻理解其背后的机制之外,也要在系统设计上下一番功夫。

I/O Buffering

)

如图,当程序调用各类文件操作函数后,用户数据(User Data)到达磁盘(Disk)的流程如图所示。

图中描述了 Linux 下文件操作函数的层级关系和内存缓存层的存在位置。中间的黑色实线是用户态和内核态的分界线。

从上往下分析这张图:

1.首先是 C 语言 stdio 库定义的相关文件操作函数,这些都是用户态实现的跨平台封装函数。stdio 中实现的文件操作函数有自己的 stdio buffer,这是在用户态实现的缓存。此处使用缓存的原因很简单 — 系统调用总是昂贵的。如果用户代码以较小的 size 不断的读或写文件的话,stdio 库将多次的读或者写操作通过 buffer 进行聚合是可以提高程序运行效率的。stdio 库同时也支持 fflush 函数来主动的刷新 buffer,主动的调用底层的系统调用立即更新 buffer 里的数据。特别地,setbuf 函数可以对 stdio 库的用户态 buffer 进行设置,甚至取消 buffer 的使用。

2.系统调用的 read/write 和真实的磁盘读写之间也存在一层 buffer,这里用术语 Kernel buffer cache 来指代这一层缓存。在 Linux 下,文件的缓存习惯性的称之为 Page Cache,而更低一级的设备的缓存称之为 Buffer Cache。这两个概念很容易混淆,这里简单的介绍下概念上的区别:Page Cache 用于缓存文件的内容,和文件系统比较相关。文件的内容需要映射到实际的物理磁盘,这种映射关系由文件系统来完成;Buffer Cache 用于缓存存储设备块(比如磁盘扇区)的数据,而不关心是否有文件系统的存在(文件系统的元数据缓存在 Buffer Cache 中)。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…414415416…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%