My Little World

learn and share


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于
My Little World

关于报文压缩方法的探究

发表于 2024-03-03

问题发现

项目中需要对大数据量请求时间进行缩短优化的工作,优化过程中发现,浏览器响应报文压缩方法为br的情况会比gzip的时间要长11-13s,具体表现如下

服务端响应用时45s

但是浏览器等服务端返回却花了58s

这样浏览器就会比服务器响应多等了58-45= 13s,不是很正常

现在直接拿浏览器请求的cUrl 发起请求

可以看到非浏览器请求的响应使用了gzip压缩,总用时48s, 服务端用时46s, 耗时差2s

可见使用gzip压缩算法耗时是远优于br压缩的

解决办法

想办法禁用掉br压缩方法

  1. 指定service Mesh压缩方法

第一步,检查服务集群是否开启了service mesh,开启后指定才有效

第二步,直接在【通用流量平台->稳定性管理】指定压缩方法

Service mesh 在指定压缩方法后,会对所有请求按指定的压缩算法进行压缩,不管content-length 大小,也不管上游是否已经指定了其他压缩方法,简单粗暴,适合快速解决问题

  1. TLB + 项目配置

该方法是在探究原因过程中发现,过程比较曲折,需要排查修改两个地方,着急解决问题不适宜

  1. 确认下自己的服务是否为node服务且有使用koa-compress插件(注意排查框架是否有默认注入),需要将br 压缩算法关闭,具体关闭形式可能因框架不同配置姿势不同,但可以参考下插件官方配置
  2. 关闭TLB 路由Ngnix默认br 压缩算法配置,禁止使用br算法

虽然复杂,但是方案会比方法一更合理一些

为什么不在发起请求时直接更改accept-encoding?

解决这个问题的另一条途径就是从源头,请求发起端就去掉相关br的设置,也就是更改accept-encoding, 让它不包含br,如果客户端不支持br 压缩,那请求响应自然是不能使用br 压缩的,但是天有不测风云,accept-encoding 是一个不能通过代码去修改的请求报文(详见),所以这条路是行不通的。

这到底是怎么回事?

虽然使用方法一可以快速彻底的解决掉问题,但是不应用方法一时,可以发现的一个明显问题就是不同请求的压缩方法不同,而且存在不使用压缩方法的情况,这就激起了作者尘封已久的好奇心,到底是谁在指定content-encoding呢?

接下来就需要看一下从服务端到客户端,到底是哪个环节在决定content-encoding

Koa-compress

鉴于本人node服务项目基于ACE1.X构建,在搜索代码进行排查时,并没有在配置文件中搜到相关的配置,重新查阅框架文档的时候,才注意到框架有进行默认注入,这就从服务端源头找到了一个会更改content-encoding的地方,俗话说,灯下黑,不过如此。

既然有使用koa-compress, 而且源码不是很复杂,那就简单探索下它的压缩原理

查看源码可知,当content-length大于1024b时,会根据Accept-encoding进行压缩

在Accept-Encoding值是’gzip, deflate, br’情况下

压缩方法的选择逻辑就是accept-encoding有br 会优先使用br,如果br被禁用就使用gzip

由于默认注入时,没有指定压缩阈值,所以当我们的请求数据过大, 大于1024b时,自然就会触发koa-compress进行br压缩,也就是说上面问题的出现,罪魁祸首就是koa-compress

但是当数据量小于1024b时,又会出现br,甚至不进行压缩又是怎么回事呢?

whisle插曲

在排查过程中,相同条件请求,在本地开启whistle代理,通过域名进行本地访问,出现了响应始终是gzip 的情况,这对于大于1024b的响应就不对了,按上面koa-compress逻辑,应该是br才对

经过在http\://localhost:8899/#network 抓包,可以发现whistle给本地服务的请求报文accept-encoding是不带br的

经过与whistle开发者请教(issue),whistle确实会篡改我们的报文,把accept-encoding中的br 去掉,这样就实现了响应始终是gzip压缩的效果,因此,在本地的测试推荐大家直接使用localhost访问,避免代理的干扰

以下在本地进行的测试也均是在关闭代理情况下进行

TLB

根据请求响应链路,响应从node服务返回后,会依次经过Mesh, TLB然后到浏览器

由于mesh 在不指定压缩算法的情况下是不参与压缩的,所以对于小于1024的数据压缩,矛头指向了TLB

在开始验证前,先来了解下TLB的压缩原理TLB压缩问题oncall排查手册

文中对我们比较重要的信息是这部分

文中配置与tlb同学确认后就是默认配置,这样对于我们验证就有了参照物

在关掉koa-compress 的br 压缩后,我进行了如下实验

  1. 构造响应不同content-length的接口
  2. 分别通过本地localhost 访问,域名访问,以及关掉tlb 的br 压缩后再通过域名访问以上接口(保证经过tlb)

得到如下结果(no表示不压缩)

content-length localhost:3000 域名访问 tlb 设置 brotli = off
117 no no no
152 no br no
204 no br gzip
958 no br gzip
1208 gzip gzip gzip

从koa-compress 压缩原理我们可以知道从服务端响应的数据,大于1024采用gzip,小于则不压缩

所以本地访问是符合预期的

经过域名访问,我们可以看到小于1024大于150的响应被用br进行压缩了, 符合br 大于150就压缩

当把tlb 上nginx的br开启指令关掉,我们可以看到小于1024大于200的响应被用gzip压缩了,符合gzip 大于200就压缩的逻辑

再看大于1024的最后一行,当服务端已经指定content-encoding的时候,tlb 是不会进行压缩的,会沿用上游指定压缩算法

综上看来,TLB 会在上游响应未指定content-encoding的时候进行小于1M响应数据的压缩, 默认大于150b时会使用br压缩,大于200b且禁用br情况下才会使用gzip,如果上游指定了content-encoding, 就沿用上游压缩算法

至此,响应报文的content-encoding 来源我们搞清楚了,接下来回到解决办法一,验证下service mesh指定压缩方法后报文变化

集群插曲

虽然文档中指令是默认指令,但不并是所有TLB集群的默认Ngnix 配置,如果出现了与上述结论异常的情况,需要邀请TLB 的同学帮忙查一下域名依赖的TLB 集群是否就是文档中的默认配置(因为只有TLB同学有权限可以查)

比如,相同600B请求,Boe 环境是br压缩,但是线上则变成了gzip

按上面的结论,服务器不会对小于1024的请求进行压缩,经过tlb 默认配置会使用br,boe 环境是正常的,线上是不正常的,经过排查发现,线上tlb 依赖的集群默认配置没有开启br ,所以再走默认配置会进行gzip压缩

Cloud IDE

这里需要注意一点的是,上面我们在发现小于1024的压缩算法异常时,访问的是cloud IDE 上启动项目后帮我们生成的域名,我们在本地请求接口是没有进行压缩的,也就是说cloud IDE生成的域名是有经过TLB的,而且其集群默认开启了br压缩

Service Mesh

实验条件(复现问题):

TLB nginx 不禁用br

不禁用koa=compress的br压缩算法

content-length localhost:3000 域名访问 域名访问
117 no no gzip
152 no br gzip
204 no br gzip
958 no br gzip
1208 br br gzip

我们从浏览器发起请求

在最后一个中间件打印响应头,说明服务器没有参与数据压缩 (可以通过设置priority让中间件在最后一个执行)

然后通过监听端口报文

tcpdump -i eth0 port 3000 -nn

基本上通过上述表现我们基本上是可以判断是mesh 进行了压缩

但是,我们现在监听的是3000配置端口(其他服务监听实例输出的端口)

如果3000端口吐出来的是经过了mesh的话,那通信的结构应该是这样

往深了想一下,上面的判断逻辑并不是非常精确

  1. node 最后吐出来的数据的header 可能跟我们上面在最后一个中间件打印的header并不同,也就是说我们在最后一个中间件打印的header 并不是最终实例吐出数据的header,有一些 header 是会在最后吐数据的时候装的
  2. Gzip 的请求头真的是mesh 加上去的吗?实例和mesh 之间不会还有其他服务?

要解决上面两个疑问,就要想办法去抓取一下mesh 接收的数据,也就是服务吐给mesh的数据

抓取mesh socket

当给服务开启mesh 服务时,mesh 会给环境注入一些环境变量

其中SERVICE_MESH_HTTP_EGRESS_ADDR 这个变量对应的地址就是服务交给mesh 转发的数据

即服务会往这个地址吐数据,然后再由mesh从这里转发再吐出去

那我们接下来就要想办法去读这个socket

通过tcpdump对Unix Domain Socket 进行抓包解析

当我打算用curl 命令去执行相关方法时,却发现没有相应地址的socket

ok,拉mesh 同学onCall 说这种情况是因为服务器和mesh之间不是用的uds通信,用的ip PORT通信

ByteMesh HTTP 出流量接入

抓取PORT 9507

那我现在需要找到MESH_EGRESS_PORT具体是什么

无论是通过打印环境变量

还是通过 cat /proc/\${pid}/environ 查看配置文件,以及通过查看监听端口

基本都确定lookback通信的port 是9507

Ok 那我们再回到用tcpdump 抓包的方式,会发现什么也抓不到

陷入死胡同, 那就是说没有数据包经过mesh 接收数据的端口

重新认识Service Mesh

入流量

我们之前是通过入流量开启压缩算法的

入流量在整个通信链路中的作用是这样的

所以现在需要抓取的是入流量的端口ByteMesh WebSocket & HTTP/1.1 & HTTP/2协议接入约定

需要找到MESH_INGRESS_PORT 通过查看pid 下面 environ 文件可以看到port 为3000,也就是配置端口

然后尝试监听 https://www.cnblogs.com/zgq123456/articles/10251860.html

tcpdump -i any -A -s 0 ‘tcp port 3000 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)’

这一次看到了报文的整个变化过程如上两图

出流量

通过给服务开启出流量代理,可以看到两种通信地址

SERVICE_MESH_EGRESS_ADDR 即如果node 跟下游服务通信会走dsl 通信

MESH_EGRESS_PORT 即如果node 发起http 请求会通过这个端口与mesh 进行http 通信,过程同上面入流量过程

看一下UDS报文长啥样

还是参考这篇文章的方法

  1. 为方便后续指令执行,切到rpc.egress.sock所在文件夹,

  1. 将给到rpc.egress.sock 的数据转发到 8089

  1. 用curl 发起请求,并用tcpdump 对8089进行抓包

注意使用curl –-unix-socket /rpc.egress.sock 时 如果不支持–-unix-socket 参数,需要使用apt-get 升级curl 版本,如无法升级,可能是linux 版本不再维护,可尝试替换基础镜像(指定高版本linux 的)进行部署后再测试 虽然位于rpc.egress.sock 所在文件夹下执行,但是前面的/ 不能省

先用tcpdump -i any -netvv port 8089 看看能不能

加上-A -s , ===> tcpdump -i any -A -s 0 -netvv port 8089 看看具体报文

知识收获

cUrl

cUrl 命令相关参数

-v/–verbose 用于打印更多信息,包括发送的请求信息

-o /dev/null 把输出写到该文件中,保留远程文件的文件名

-w ‘%{size_download}\n’ 获取下载大小

--unix-socket 测试socket 地址,注意要求curl 版本7.50+,如果webshell 不支持,需要考虑更换tce基础镜像

常用linux命令

tcpdump

tcpdump -i eth0 port 3000 -nn

tcpdump -i eth0 -nn -vv

tcpdump -i lo -nn -vv

https://www.cnblogs.com/zgq123456/articles/10251860.html

查看

lsof -i | grep LISTEN

ps -le

ps -ef | grep node

安装

apt/apt-get update

apt/apt-get install 包名

My Little World

历史记录功能设计

发表于 2024-03-03

背景

根据用户反馈,查询条件多个时,想要重新看一下上次的查询结果,操作比较繁琐,希望可以有历史查询的功能,将最近查询的n次记录可以找到,方便回溯问题

方案

前端

在用户点击查询按钮的时候,将当前页面链接调接口保存起来,查询时链接会携带查询条件

后端

存储

历史记录需要跟用户身份做绑定,当前天级uv可达75人,不适宜用tcc或者wcc平台进行数据存储

所以需要申请资源进行数据存储

容量

一个连接大小按照500Byte算,如果只保存最近10条记录,那么一个用户需要5000b ==> 5kb

目前平台用户数以1000为底计算,一开始平台会需要 5kb * 1000 ==>5000kb ==> 5mb

(目前纯个人用户有530,加上以部门为单位申请的权限,各部门人数不确定)

假设半年后用户量翻倍那么存储空间需要增加一倍也就是10MB

负载

目前平台日pv 350,日uv 50, 大致计算一个用户一天会访问页面7次,四舍五入假设1天会进行10次查询

1个用户1天会进行10次数据库读写

那整个平台1天平均会进行500次读写,高峰假设1000次读写(75四舍五入)

平均 500 * 500 /(3600*24) ~~ 0.003kb/s 高峰1000*500/(3600*24) ~~~0.006kb/s

很低

数据结构

本来想如果数据库有数组的话,表结构就是用户id + 记录数组;

没有的话,我现在想了两种方案,

一个就是用字符串存这个数组,用户id + 记录数组字符串形式,相当于更新时要先获得这个字符串,转成数组后,看有没有10条,没有的话直接push,有的话,把时间最早的那条删除,push进数组,再转成字符串更新数据库,这样缺点就是展示的时候也得字符串转数组一下;

另一种就是用户id只和一条记录存在一起,不用一个字符串存整个10条记录,更新的时候我去拿数据的时候拿整个用户id所有的,超过10条的话就用数据库删除方法把时间早的删除了,再存进去最新的

看起来都挺麻烦

而且在实际接入数据库的过程中,还要手动执行命令行产生model相关文件

通过调研公司存储系统的各种方式,觉得redis可以更好的解决存储问题,redis支持List类型存储,

而且LPUSH, LPOP,EXPIRE方法可以很好的帮助实现数据存取更新缓存等问题,省了数据库建表等过程

缓存

redis可以很好的支持数据删除,在更新数据的时候重新设置过期时间即可保证删除不活跃用户的记录

实现

申请redis服务,用户工号做redis的key值,key值的value即用户的查询历史记录list,

写接口: 查记录,更新记录

前端在点击查询的时候调接口更新记录

参考文档

存储系统对比 (草稿)Storage System Comparision(Draft) #

数据结构与命令一览 List of data structure and commands #

https://redis.io/commands

My Little World

Mobx 运行机制深入研究

发表于 2024-03-03

追踪原理

官方文档

MobX 会对在执行 跟踪函数 期间 读取的任何现有的可观察属性做出反应

“读取” 是对象属性的间接引用,可以用过 . (例如 user.name) 或者 [] (例如 user[‘name’]) 的形式完成。

“追踪函数” 是 computed 表达式、observer 组件的 render() 方法和 when、reaction 和 autorun 的第一个入参函数。

“过程(during)” 意味着只追踪那些在函数执行时被读取的 observable 。这些值是否由追踪函数直接或间接使用并不重要。

换句话说,MobX 不会对其作出反应:

从 observable 获取的值,但是在追踪函数之外

在异步调用的代码块中读取的 observable

Mobx 5 以下 MobX 不会追踪还不存在的索引或者对象属性(当使用 observable 映射(map)时除外)。

所以建议总是使用 .length 来检查保护基于数组索引的访问。

所有数组的索引分配都可以检测到,但前提条件必须是提供的索引小于数组长度。

核心概念

追踪属性访问,而不是值

1
2
3
4
5
6
7
8
9
let message = observable({
title: "Foo",
author: {
name: "Michel"
},
likes: [
"John", "Sara"
]
})

mobx会追踪箭头有没有变化

如果箭头发生变化,就会执行追踪函数

使用注意

处理数据时

1.更改没有被obserable的箭头,追踪函数不执行

2.追踪函数里使用间接引用指向obserable属性,追踪函数不执行

3.对新增的属性,可以使用set,get实现obserable

4.在异步代码中访问的obserable属性,不会引起追踪函数执行

1.更改没有被obserable的箭头,追踪函数不执行
autorun(() => {
    console.log(message.title)
})
message = observable({ title: "Bar" }) //指向message的箭头没有被obervable
autorun(() => {
    message.likes;//箭头没变,又没有访问数组里面的属性
})
message.likes.push("Jennifer");

2.追踪函数里使用间接引用指向obserable属性,追踪函数不执行
var title = message.title;
autorun(() => {
    console.log(title) //访问箭头没有变,还是指向老值的位置
})
message.title = "Bar" //箭头改了,但autorun里没有用到
const author = message.author;
autorun(() => {
    console.log(author.name) 
})
message.author.name = "Sara";//会执行跟踪函数,autorun里有访问name属性,这里指向name值得箭头改了
message.author = { name: "John" };//不会执行,没有访问author属性的箭头

正确使用

A:
autorun(() => {
    console.log(message.author.name)
})
message.author.name = "Sara";
message.author = { name: "John" };
B:
function upperCaseAuthorName(author) {
    const baseName = author.name;
    return baseName.toUpperCase();
}
autorun(() => {
    console.log(upperCaseAuthorName(message.author))
})
message.author.name = "Chesterton"

3.异步
const message = observable({ title: "hello" })
autorun(() => {
    console.log(message) //会执行两次,因为console.log是异步的,请确保始终传递不变数据 ( immutable data ) 或防御副本给 console.log。
})
message.title = "Hello world"
autorun(() => {
    setTimeout(
        () => console.log(message.likes.join(", ")), //异步执行,访问原始数据打印一次
        10
    )
})
message.likes.push("Jennifer");//不会引起autorun执行

4.MobX 5 可以追踪还不存在的属性
autorun(() => {
    console.log(message.postDate)
})
message.postDate = new Date()

组件使用时

子组件问题

MobX 只会为数据是直接通过 render 存取的 observer 组件进行数据追踪

所以当需要将数据传递给子组件时,要保证子组件也是一个obserable组件,可以做出反应

解决办法:

1.将子组件使用obserable函数处理

它用 mobx.autorun 包装了组件的 render 函数以确保任何组件渲染中使用的数据变化时都可以强制刷新组件

2.使用mobx-react的Obserable组件包裹子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
方法一:将子组件使用obserable函数处理
const MyComponent = observer(({ message }) =>
<SomeContainer
title = {() => <TitleRenderer message={message} />}
/>
)
const TitleRenderer = observer(({ message }) =>
<div>{message.title}</div>}
)
message.title = "Bar"
方法二:使用mobx-react的Obserable组件包裹子组件
const MyComponent = ({ message }) =>
<SomeContainer
title = {() =>
<Observer>
{() => <div>{message.title}</div>}
</Observer>
}
/>
message.title = "Bar"

避免在本地字段中缓存 observable

1
2
3
4
5
6
7
8
9
10
@observer class MyComponent extends React.component {
author;
constructor(props) {
super(props)
this.author = props.message.author;//message.author发生变化时不会引起render
}
render() {
return <div>{this.author.name}</div> //.name可以引起render
}
}

优化,使用计算属性,或者在render函数中进行间接引用

@observer class MyComponent extends React.component {
    @computed get author() {
        return this.props.message.author
    }

其他

1.从性能上考虑,越晚进行间接引用越好

2.数组里面的是对象而不是字符串,那么对于发生在某个具体的对象中发生的变化,渲染数组的父组件将不会重新渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Message = observer(({ message }) =>
<div>
{message.title}
<Author author={ message.author } />
<Likes likes={ message.likes } />
</div>
)
const Author = observer(({ author }) =>
<span>{author.name}</span>
)
const Likes = observer(({ likes }) =>
<ul>
{likes.map(like =>
<li>{like}</li>
)}
</ul>
)
变化 重新渲染组件
message.title = “Bar” Message
message.author.name = “Susan” Author (.author 在 Message 中进行间接引用, 但没有改变)*
message.author = { name: “Susan”} Message, Author
message.likes[0] = “Michel” Likes

一些 对比

autorun vs compute

当使用 autorun 时,所提供的函数总是立即被触发一次,然后每次它的依赖关系改变时会再次被触发

computed(function) 创建的函数只有当它有自己的观察者时才会重新计算,否则它的值会被认为是不相关的

如果一个计算值不再被观察了,例如使用它的UI不复存在了,MobX 可以自动地将其垃圾回收。

而 autorun 中的值必须要手动清理才行

autorun vs reaction

reaction(() => data, (data, reaction) => { sideEffect }, options?)

它接收两个函数参数,第一个(数据函数)是用来追踪并返回数据作为第二个函数(效果函数)的输入。

传入 reaction 的第二个函数(副作用函数)当调用时会接收两个参数。

第一个参数是由 data 函数返回的值。

第二个参数是当前的 reaction,可以用来在执行期间清理 reaction

reaction 返回一个清理函数。

不同于 autorun 的是当创建时 **效果 **函数不会直接运行,只有在数据表达式首次返回一个新值后才会运行。

在执行 效果函数时访问的任何 observable 都不会被追踪。

效果函数仅对数据函数中访问的数据作出反应,这可能会比实际在效果函数使用的数据要少。

此外,效果 函数只会在表达式返回的数据发生更改时触发。 换句话说: reaction需要你生产 效果函数中

所需要的东西。

useObserver vs Observer vs observer

相关文档

1.虽然只是在返回DOM的地方使用 useObserver(), 但是,当dom中数据改变的时候,整个component都会重新render

1
2
3
4
5
6
7
8
9
10
function Person() {
console.log('in useObserver');//点击按钮会触发执行
const person = useLocalStore(() => ({ name: 'John' }));
return useObserver(() => (
<div>
{person.name}
<button onClick={() => (person.name = 'Mike')}>No! I am Mike</button>
</div>
));
}

2.Observer 标签组件可以更精准的控制想要重新渲染的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default function ObservePerson() {
console.log('in Observer');//点击按钮不会执行
const person = useLocalStore(() => ({name: 'John'}))
return (
<div>
The old name is: {person.name} //点击按钮不会更新
<div>
<Observer>{() => <div>{person.name}</div>}</Observer> //点击按钮会更新
<button onClick={() => (person.name = 'Mike')}>
I want to be Mike
</button>
</div>
</div>
)
}

3.与useObserver相比,除了使用方法不同,目前不知道区别在哪,有时间需要探究一下

const ObserverLowercasePerson: React.FC<any> = observer(() => {
    console.log('in Observer') //点击按钮也会执行
    const person = useLocalStore(() => ({name: 'John'}));
    return (
        <div>
            <div>The name is: {person.name}</div>
            <button onClick={() => (person.name = 'Mike')}>
                Change name
            </button>
        </div>
    )
})
```
My Little World

趋势图卡片实现原理

发表于 2024-03-02

背景

春节看板项目中有对数据进行趋势分析的展示,其中一种卡片的展示形式经过思考后可以提炼成组件向外提供服务,于是进行封装上传Semi物料市场

设计

  1. 既要支持一个卡片的展现,也要支持多个卡片的展示,所以数据源应该是一个数组 list
  2. 每个卡片的大小应该是一样的,所以应该给一个统一设置卡片大小的属性 size
  3. 卡片可以静态展示,也可以有响应事件,这里先支持一个点击事件 onClick
  4. 单个卡片的功能需要展示趋势折线图,标题,提示,数量,还要支持定制颜色

所以单个卡片的数据结构应该是这样的

name 标题名 string 或者 ReactNode
tip 提示(可选) string 或者 ReactNode
tipNormalShow 提示图标展示方式,默认值false,鼠标滑过才展示 boolean FALSE
hoverLayer 鼠标hover是否展示蒙层效果,默认false,不展示 boolean FALSE
lineColor 折线的颜色,涉及渐变色计算,配置成十六进制格式 string #E91E63 或者 #00B3A1根据卡片位置奇偶情况切换默认颜色
value 标题下的数据 string 或者 ReactNode
xData 折线图x轴数据 Array[string或者number]
yData 折线图y轴数据 Array[string或者number]
loading 数据加载状态开启,默认false,不开启 boolean
noDataTip 没有数据时的提示 string 或者 ReactNode 抱歉,没有数据可展示
errorInfo 错误展示 {text: ‘xxx’, color: ‘xxxx’}

实现

将List 传进来的数据,循环成多个卡片,将单个卡片信息,onClick 和size都传递给卡片组件

卡片根据传递进来属性的不同状态,展示相关信息

另外折线图依靠echart来画,所以需要根据颜色和卡片位置生产曲线配置,这里依靠getChartOption

计算渐变颜色同转换成rgb格式,设置透明度来实现渐变

体验

在线体验地址:https://semi.bytedance.net/material/zh-CN/playground/219

My Little World

拖拽渲染问题的深入研究

发表于 2024-03-01

背景

使用拖拽组件进行拖拽排序
1.原展示模块内容需要进行缩略展示,具备收缩展开的能力
2.排序的内容复杂,需要异步获取数据,循环的时候传进去的关键值(如id)作为参数拉取数据,渲染图表

方案一

保留原组件渲染逻辑,同时将数据源传入排序组件(排序组件显示标题类信息代表原模块),
然后根据是否进入排序状态,保留二者其一,就是排序时展示排序组件,非排序时展示模块内容
问题

从排序状态回到正常展示状态时,因为正常展示的组件DOM在进入排序状态时被销毁
这时再回来,相当于从无到有要重新创建,会引起数据重新请求

方案二

将原展示模块组件作为排序组件项进行渲染,在进入排序状态时将展示模块高度减小,仅保留标题部分充当缩略信息展示
因为展示模块DOM始终存在,所以可以解决掉方案一展示模块DOM消失再创建的数据拉取问题
原理

DOM的新建跟更新流程不同,在这种情况下,新建过程会需要去请求接口拉数据,而如果仅仅是更新的话,可以依赖react的key的关键作用减少DOM 的重建过程,只是进行调换顺序即可
这里在将数据源列表渲染出来的时候,将数据的特征值赋值给key,即排序前后,展示模块key不变就不会被重新新建渲染, 只是进行排序处理
解决方案一产生的问题

在进入排序状态时,将展示模块组件高度设置为0,overflow:hidden,就看不到展示组件,但DOM 依然存在
这时再使用排序组件展示缩略信息即可

小结

无论哪种方案,在结束排序后,都要更新数据源,但数据源里面的对象不能变,因为展示模块会依赖其中的具体对象里的信息进行数据拉取
即

My Little World

学习关于产品的一些思维

发表于 2022-09-03

产品经理决策力工具

象限法:
把想做的事情拆成两个指标去做
让这个两个指标做xy轴
在xy轴包围的空间内,分成四个象限
把要做的事情按照xy轴的值分布在四个象限中
然后决定要在四个象限中寻找最优解

假设思维: 把未来要做的事情一步一步的假设出来
用户思维: 用使用者的思维设计功能

产品路线图roadmap4个核心要素

  1. 里程碑是要有意义的
  2. 跟各个方向的工作协同进行
  3. 可能不是串行的而是并行的,需要准备多种方案
  4. 基于产品框架

五张图说明产品

  1. 核心功能体验图,主要功能的流程图
  2. 模块图,将功能具体的实现划分不同模块,即可以概览具备的的功能,也方便进行任务分配
  3. 功能树,一个模块具体具备的功能内容,相当于再细分
  4. 页面关系图,页面的操作流程,可以跟功能树对比查看是否有功能遗漏,上面提到的模块功能树都会最终落到页面上
  5. 交互设计图,不是最重要的,但要有自解释性,每个人都能看懂

用户留存率—-> 指标之王
算法

  1. 新增留存率 所有新用户中有多少比例下个时间周期会出现
  2. 活跃留存率 所有用户中(活跃用户),包括新用户,有多少比例会在下个时间周期出现,即有多少人会成为下个周期的活跃用户
    统计分析
    用户活跃度
    cohort, 横纵都是第1-n周
    每一行代表当前周用户留存率再往后几周的留存率请款
    每一列代表当前周中,阁用户留存率情况,可以看到每周留存率在这一周的变化情况
    对角线从左上到右下,上面数据表示次周留存率,如果呈下降趋势,说明产品在新客中粘性在下降,留存率整体在下降
    将对角线数据处理形成折线图可直观看到用户留存趋势

RFM,用户贡献值(下了多少单,总消费金额…),根据用户贡献值可采取不同的营销策略

DAU,WAU,MAU,日、周、月活跃用户,一般让DAY/MAU的值作为一个用户粘性的指标
以DAY/MAU为y轴,DAU为x轴,形成折线趋势图,让趋势保持稳定上升是一个产品的发展方向

如何提升留存?
不要去想现有总用户如何去留存,去观察哪些用户值得留存,想办法让这些用户实现留存提升

设计一套CRM系统
CRM系统: 维护公司与客户关系 ===> 用户运营战略执行系统

My Little World

redis 学习的一些笔记

发表于 2022-08-09

基础常识
磁盘
寻址:ms
带宽: G/M
内存
寻址:ns(纳秒级)
带宽:byte/s
秒>毫秒>微秒>纳秒
磁盘寻址上比内存少了10w倍

I/O BUffer:成本问题
磁盘有磁道和扇区,一个扇区512byte
会造成索引成本增大
因此进行4K对齐,操作系统无论读多少都最少从磁盘里面拿4k出来

数据库的表很大,性能会下降吗?
如果表有索引,
那么对于增删改的操作肯定会变慢
查询速度
如果是1个或者少量查询依然很快
但如果是并发大的时候会受到硬盘带宽的影响,从而影响速度

数据在内存和磁盘中体积不一样

redis出现原因:
内存 ==> 贵
磁盘 ==> 慢
两个基础设施限制:
冯诺依曼体系的硬件制约 ===> 硬盘io带宽问题
以太网,tcp/ip的网络 ===> 不稳定

https://db-engines.com/en/

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

上述数据类型是指value的数据类型
memcache 和redis区别在于,memcache value没有类型
将返回value所有数据到client端,会受到server 网卡IO的限制,而且client要有解码的逻辑
redis因为value有类型,所以对于数据的请求根据不同情况直接调用相应类型方法返回少量数据即可
做到了计算向数据移动

redis是单进程,单线程,单实例的,通过epoll快速处理并发请求
epoll是同步非阻塞的多路复用机制
jvm: 一个线程成本是1MB
线程多了会增加调度成本,从而使CPU浪费,也会增加内存成本
BIO—>NIO(同步非阻塞)—>多路复用—>epoll(引入共享空间避免fd相关数据考来考去)

redis 对不同数据类型不同encoding类型的数值具有不同的方法
key有两个属性:
type标识值的类型
encoding标识值的编码类型
二者决定对值的操作可以使用哪些方法,从而加速计算

String 类型
字符串的相关操作:
set,get,append,setRange,getRange,strlen
适用场景:
使用内存存储的session,对象,小文件

数值的相关操作:
incr,decr
适用场景:
限流,计数
秒杀一般会用数据库

bitmap的相关操作:
setbit, bitcount,bitpos,bittop
使用场景:web, 离线数据
1.用户系统,统计用户登录天数,且窗口随机
key标识用户,每个用户准备365位,每一位表示当天是否登录,登录置一
setbit sean 1 1
setbit sean 7 1
setbit sean 364 1
STRLEN sean ===>46 每个用户只需要46个字节来存储这些信息
BITCOUNT sean -2 -1 计算多少天范围内一共登录了几天

  1. 电商做618活动,需要给活跃用户登录后送礼物,请问应该备货多少礼物
    假设这里活跃用户统计规则为20220901-20220903三天内登录的用户,三天内只要登录一次就算活跃用户
    key标识当天用户登录情况,将用户编号映射到二进制位的相应位置上,每一位二进制代表一位用户是否登录
    setbit 20220901 1 1
    setbit 20220902 1 1
    setbit 20220902 7 1
    bittop or destkey 20220901 20220902
    bitcount destkey 0 -1

  2. 存储oa权限信息

  3. 布隆过滤器,模块调用

List 类型相关操作
按不同放入顺序排列
栈:同向命令
队列:反向命令
数组
阻塞,单播队列
ltrim
使用场景:
数据共享,迁出
无状态

Hash类型
对filed进行数值计算
场景:
点赞,收藏,详情页
聚合场景

Set类型
无序去重集合
元素变多扩容,会触发rehash,造成原顺序颠倒不稳定
集合操作多
随机事件:
RANDMEMBER key count
正数:取出一个去重的结果集(不能超过已有集)
负数:取出一个带重复的结果集,一定满足你要的数量
如果是0,不返回
应用场景:
随机事件==>抽奖:
10个精品,参与人数> 10 时,传正数,得到10个随机不重复值
10个精品,参与人数< 10 时,传负数,得到10个可重复的值
中奖是否重复
Spop 不重复取出一个
推荐系统:
共同好友:交集
推荐好友:差集

Sorted set
排序
物理内存左小右大(根据分值从左到右从小到大)
不随命令发生变化
集合操作(并集,交集)权重、聚合指令
排序是怎么实现的。 ==> skip List 跳跃表
增删改查的速度
场景:
排行榜,
有序事件
评论+分页

redis持久化
持久化意味着性能会下降
两个指标
快照:rdb,恢复的速度快,但缺失的多
日志:aof, ,
满足完整性好,恢复速度变慢,===>采用不同日志策略避免
指令追加造成冗余量比较大 ===> 使用重写

redis 分布式集群
可用性:
单点故障可通过主从主备一变多集群构建镜像,需要同步
强一致性,会破坏使用性
弱一致性,可用性强,但同步一致性低(默认)
最终一致性,使用黑盒可靠集群做中间缓存,保证主从数据最终一致性
数据存储压力问题(装不下),采用分片式集群代理集群,也是一变多,但不需要同步

AkF拆分原则
根据业务划分数据到不同redis实例

My Little World

系统设计案例

发表于 2022-02-02

案例:用户将存储内容粘贴到站点,站点给用户返回一个短地址,用户通过短地址,可以访问之前粘贴的内容或者跳转之前的原始站点

功能

  1. 对于用户来说操作要简单,生成的短地址要简单,而且要唯一,不同用户即使内容一样也要生成唯一的短地址

  2. 时间有效性,从存储角度来说,不可能一直帮用户存储所有生成的短地址,不然存储会越来越大,所以通过设置短地址有效的访问时间,可以减少存储成本

技术

  1. 可用性(high Availability), 保障用户功能可用
  2. 低延时(low latency),用户拿到短地址或者通过短地址跳转其他网站时,重定向时间不宜过长
  3. 安全性(non guessable),不能被猜出来,用户在生成一定短地址时如果携带一些个人信息,不应体现在短地址中,否则会造成用户信息泄露
  4. 对于ins/微博/小红书之类的社交功能还要保障一致性,博主发的照片,follower看到的内容应该是一样的

容量负载能力假设

对一个用户来说,可以抽象出两个主要请求

  1. 请求生成短url,我们要把请求参数或者原始信息存储起来 inbound
  2. 请求访问url,把生成的url返回给用户使用,进行重定向 outbound

假设一个短地址按500byte大小存储

容量

假设一个月会有100万个新短地址生成,
那么5年会产生

100万5年12个月 ==> 约等于60亿个短地址

60亿*500byte ==> 3TB 会有需要3TB大小的容量存储

负载能力

假设一个月有100个用户,每个用户会进行100万次访问短地址进行重定向的操作

那么每秒钟会有
(100 100万)/(30D 24H * 3600s) ==> 约等于4000个短url 要给到用户
同时会有
4000/ 100 ==> 大概40个短url 需要被生成

那么服务所需要的带宽就可以计算出来

inbound : 40 500 byte 约等于 20kb/s
outbound: 4000
500 byte 约等于 20MB/s

API 数据库设计

api

假设会用到简单的增删
生成短url createURL(api-key, originUrl, expired-Date, userId)
删除url deleteURL(api-key,shortUrl)

DataBase

对于shortUrl

pk: hash
originUrl,
expired-date,
userId
…

对于user
pk: userId
name
…

生成shortUrl

假如计划生成一个6个字符的短url,使用base64的加密算法的话可以生成64 ^ 6 大约640个短url,

满足之前5年会产生60个亿的唯一性需求

整体逻辑

用户 —>request shortUrl generation —> app

app —> base64 encoding + 从key generation DB中拿一个nonUse 的key —> 得到shortUrl —>DB

DB —> app –>用户

其他

Cache

2-8原则

用存储的20% 的url做cache内容,可以满足80% 的访问需求

load balance

均衡流量

过期后的url处理,key的处理

分布式存储,分片

对于社交功能的newFeed的推送

pull / push / pull + push hybrid

My Little World

一些其他小问题

发表于 2021-11-29

1.
在 useEffect 中如果使用了某些变量,却没有在依赖项中指定,会发生什么呢?

依赖那里没有传任何参数的话,会每次render都执行。
依赖项有传值但是,有部分依赖没有传,那么没有传的那部分,数据即使变化也不会执行副作用。

对于这节课中显示的 Blog 文章的例子,我们在 useEffect 中使用了 setBlogContent 这样一个函数,本质上它也是一个局部变量,那么这个函数需要被作为依赖项吗?为什么?

函数应该是不会变化的,所以不需要监听。

2.
useState 其实也是能够在组件的多次渲染之间共享数据的,那么在 useRef 的计时器例子中,能否用 state 去保存 window.setInterval() 返回的 timer 呢?

可以,只是没有 useRef 更优,因为在更新 state 值后会导致重新渲染,而 ref 值发生变化时,是不会触发组件的重新渲染的,这也是 useRef 区别于 useState 的地方。

3.
componentWillUnmount 近似的实现:组件销毁和文章 id 变化时执行。那么在函数组件中如果要实现严格的 componentWillUnmount,也就是只在组件销毁时执行,应该如何实现?\

1
2
3
4
5
useEffect(() => {
return () => {
// 这里只会在组件销毁前(componentWillUnmount)执行一次
}
}, [])

4.
每次调用 useArticle 这个 Hook 的时候都会触发副作用去获取数据。
但是有时候,我们希望在有些组件自动获取,但有的组件中需要点击某个按钮才去获取数据,那么你会如何设计这个 Hook?(可能这道题有一点难度。)

useArticle Hook 可以提供一个参数,用来标记本地调用是否默认触发副作用去获取数据;对于点击按钮才触发请求的功能,可以在 Hook 中将获取数据的方法 retrn 出去,供外部自由调用。

  1. Hook 一般都是使用的 useState 保存了状态数据,也就意味着状态的范围限定在组件内部,组件销毁后,数据就没了。那么如果希望数据直接缓存到全局状态,应该如何做呢?

可以借助 redux,配合 useContext 等 api ,将状态数据存储至全局中。

My Little World

如何提升应用打开速度

发表于 2021-11-29

使用 import 语句,定义按需加载的起始模块

对于这个需求,ECMA Script 标准有一个提案,专门用于动态加载模块,语法是 import(someModule)。
注意,这里的 import 和我们一般用于引入模块的静态声明方式不同,比如 import something from ‘somemodule’ 。
但这里的 import 是作为一个函数动态运行的,这个 import() 函数会返回一个 Promise。
这样,在模块加载成功后,就可以在 Promise 的 then 回调函数中去使用这个模块了。

虽然这只是一个提案,并没有成为标准,但是 Webpack 等打包工具利用了这样的语法去定义代码的分包。

也就是说,Webpack 实现了这样的语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function ProfilePage() {
// 定义一个 state 用于存放需要加载的组件
const [RealPage, setRealPage] = useState(null);

// 根据路径动态加载真正的组件实现
import('./RealProfilePage').then((comp) => {
setRealPage(Comp);
});
// 如果组件未加载则显示 Loading 状态
if (!RealPage) return 'Loading....';

// 组件加载成功后则将其渲染到界面
return <RealPage />
}

import() 这个语句完全是由 Webpack 进行处理的。

按需加载的实现原理:Webpack 利用了动态 import 语句,自动实现了整个应用的拆包。
而在实际开发中,其实并不需要关心 Webpack 是如何做到的,
而只需要考虑:该在哪个位置使用 import 语句去定义动态加载的拆分点。

总体需要采用的策略是:按业务模块为目标去做隔离,尽量在每个模块的起始页面去定义这个拆分点。

react-loadable,专门用于 React 组件的按需加载。

1
2
3
4
5
6
7
8
9
10
11
12
import Loadable from "react-loadable";


// 创建一个显示加载状态的组件
function Loading({ error }) {
return error ? 'Failed' : 'Loading';
}
// 创建加载器组件
const HelloLazyLoad = Loadable({
loader: () => import("./RealHelloLazyLoad"),
loading: Loading,
});

可以看到 Loadable 这个高阶组件主要就是两个 API。

loader:用于传入一个加载器回调,在组件渲染到页面时被执行。
在这个回调函数中,我们只需要直接使用 import 语句去加载需要的模块就可以了。

loading:表示用于显示加载状态的组件。在模块加载完成之前,加载器就会渲染这个组件。
如果模块加载失败,那么 react-loadable 会将 errors 属性传递给 Loading 组件,方便你根据错误状态来显示不同的信息给用户。

按需加载可以说是减少首屏加载时间最为有效的手段,它可以让用户在打开应用时,无需加载所有代码就能开始使用,从而提升用户体验。

使用 service worker 缓存前端资源

和浏览器自动的资源缓存机制相比,Service Worker 加上 Cache Storage 这个缓存机制,具有更高的准确性和可靠性。

因为它可以确保两点:
缓存永远不过期。你只要下载过一次,就永远不需要再重新下载,除非主动删除。
永远不会访问过期的资源。换句话说,如果发布了一个新版本,那么你可以通过版本化的一些机制,来确保用户访问到的一定是最新的资源。

1…456…27
YooHannah

YooHannah

262 日志
1 分类
23 标签
RSS
© 2025 YooHannah
由 Hexo 强力驱动
主题 - NexT.Pisces