漫画:什么是 HTTPS 协议?

 

%title插图%num

%title插图%num

%title插图%num

%title插图%num

%title插图%num

%title插图%num

%title插图%num

%title插图%num

%title插图%num

什么是HTTP协议?

HTTP协议全称Hyper Text Transfer Protocol,翻译过来就是超文本传输协议,位于TCP/IP四层模型当中的应用层。

%title插图%num

HTTP协议通过请求/响应的方式,在客户端和服务端之间进行通信。%title插图%num

这一切看起来很美好,但是HTTP协议有一个致命的缺点:不够安全。

HTTP协议的信息传输完全以明文方式,不做任何加密,相当于是在网络上“裸奔”。这样会导致什么问题呢?让我们打一个比方:

小灰是客户端,小灰的同事小红是服务端,有一天小灰试图给小红发送请求。

%title插图%num

但是,由于传输信息是明文,这个信息有可能被某个中间人恶意截获甚至篡改。这种行为叫做中间人攻击。

%title插图%num

%title插图%num

%title插图%num

如何进行加密呢?

小灰和小红可以事先约定一种对称加密方式,并且约定一个随机生成的密钥。后续的通信中,信息发送方都使用密钥对信息加密,而信息接收方通过同样的密钥对信息解密。

%title插图%num

%title插图%num

这样做是不是就*对安全了呢?并不是。

虽然我们在后续的通信中对明文进行了加密,但是*次约定加密方式和密钥的通信仍然是明文,如果*次通信就已经被拦截了,那么密钥就会泄露给中间人,中间人仍然可以解密后续所有的通信内容。

%title插图%num

这可怎么办呢?别担心,我们可以使用非对称加密,为密钥的传输做一层额外的保护。

非对称加密的一组秘钥对中,包含一个公钥和一个私钥。明文既可以用公钥加密,用私钥解密;也可以用私钥加密,用公钥解密。

在小灰和小红建立通信的时候,小红首先把自己的公钥Key1发给小灰:

%title插图%num

收到小红的公钥以后,小灰自己生成一个用于对称加密的密钥Key2,并且用刚才接收的公钥Key1对Key2进行加密(这里有点绕),发送给小红:

%title插图%num

小红利用自己非对称加密的私钥,解开了公钥Key1的加密,获得了Key2的内容。从此以后,两人就可以利用Key2进行对称加密的通信了。

%title插图%num

在通信过程中,即使中间人在一开始就截获了公钥Key1,由于不知道私钥是什么,也无从解密。

%title插图%num

%title插图%num

是什么坏主意呢?中间人虽然不知道小红的私钥是什么,但是在截获了小红的公钥Key1之后,却可以偷天换日,自己另外生成一对公钥私钥,把自己的公钥Key3发送给小灰。

%title插图%num

小灰不知道公钥被偷偷换过,以为Key3就是小红的公钥。于是按照先前的流程,用Key3加密了自己生成的对称加密密钥Key2,发送给小红。

这一次通信再次被中间人截获,中间人先用自己的私钥解开了Key3的加密,获得Key2,然后再用当初小红发来的Key1重新加密,再发给小红。

%title插图%num

这样一来,两个人后续的通信尽管用Key2做了对称加密,但是中间人已经掌握了Key2,所以可以轻松进行解密。

%title插图%num

%title插图%num

是什么解决方案呢?难道再把公钥进行一次加密吗?这样只会陷入鸡生蛋蛋生鸡,永无止境的困局。

这时候,我们有必要引入第三方,一个权威的证书颁发机构(CA)来解决。

到底什么是证书呢?证书包含如下信息:

%title插图%num

为了便于说明,我们这里做了简化,只列出了一些关键信息。至于这些证书信息的用处,我们看看具体的通信流程就能够弄明白了。

流程如下:

1.作为服务端的小红,首先把自己的公钥发给证书颁发机构,向证书颁发机构申请证书。

%title插图%num

2.证书颁发机构自己也有一对公钥私钥。机构利用自己的私钥来加密Key1,并且通过服务端网址等信息生成一个证书签名,证书签名同样经过机构的私钥加密。证书制作完成后,机构把证书发送给了服务端小红。

%title插图%num

3.当小灰向小红请求通信的时候,小红不再直接返回自己的公钥,而是把自己申请的证书返回给小灰。

%title插图%num

4.小灰收到证书以后,要做的*件事情是验证证书的真伪。需要说明的是,各大浏览器和操作系统已经维护了所有权威证书机构的名称和公钥。所以小灰只需要知道是哪个机构颁布的证书,就可以从本地找到对应的机构公钥,解密出证书签名。

接下来,小灰按照同样的签名规则,自己也生成一个证书签名,如果两个签名一致,说明证书是有效的。

验证成功后,小灰就可以放心地再次利用机构公钥,解密出服务端小红的公钥Key1。

%title插图%num

5.像之前一样,小灰生成自己的对称加密密钥Key2,并且用服务端公钥Key1加密Key2,发送给小红。

%title插图%num

6.*后,小红用自己的私钥解开加密,得到对称加密密钥Key2。于是两人开始用Key2进行对称加密的通信。

%title插图%num

在这样的流程下,我们不妨想一想,中间人是否还具有使坏的空间呢?

%title插图%num

%title插图%num

%title插图%num

%title插图%num

%title插图%num

%title插图%num

%title插图%num

注:*新推出的TLS协议,是SSL 3.0协议的升级版,和SSL协议的大体原理是相同的。

%title插图%num

Nodejs 升级怎么保留全局安装的包

rt, 今天用 fs/promises 报错,发现是 nodejs 版本低了。于是下载*新 lts 版本 zip,打算直接解压覆盖,仔细一看发现 npm 全局安装的包生成的命令都在 NODEJS_HOME/node_modules/npm 目录里面, 就保留了 node_modules 只覆盖了 node.exe npx 等文件。

nodejs 是升级成功了 ,但 npm 就不干了 npm WARN npm npm does not support Node.js v14.17.3 *后清空目录,解压进去好了,全局命令以后缺了再装。

想问问各位大佬,你们 nodejs 是怎么升级的,有没有办法保留这些全局包(命令)

还有 nodejs 在服务器上部署时怎么限制 npm 配置路径,服务器是多人共享 root 的,怕影响到其他人,所以全局包(例如 pm2)要装到项目目录里面,看文档.npmrc 的搜索路径,/root/.npmrc 是肯定会被读到的,不知道有没有什么好办法解决

npm Nodejs 全局 npmrc11 条回复 • 2021-08-04 22:34:13 +08:00
zzk1989 1
zzk1989 42 天前
npx 可以指定 node 版本,不影响全局
$ npx -p node@0.12.8 node -v

如果你有一系列东西,可以单独封装起来,提供一个”bin”命令就可以了
比如我就封装了一个 vite vue2 的打包环境,全局安装一次就可以在其他项目里面使用了
coffeedeveloper 2
coffeedeveloper 42 天前 ❤️ 1
用 nvm https://github.com/nvm-sh/nvm,然后安装的使用 –reinstall-packages-from 参数
noe132 3
noe132 42 天前
不使用全局包 / 使用 docker 部署
lc1450 4
lc1450 42 天前
@zzk1989 @coffeedeveloper 好的,谢谢 有空试试

@noe132 是个好办法, 谢谢了
libook 5
libook 42 天前
npm 自己其实也是被当做一个包放在全局的 node_modules 下的,你可以把新版的 npm 覆盖过去。

另外一直在 MacOS 和 Linux 下用 nvm:
nvm install 新版本号 –reinstall-packages-from=旧版本号
可以做到连着全局包一起迁移过去。
几年前尝试过在 Windows 上用 nvm 无果,nvm 本身实际是个 Shell 脚本,原版不能在 Windows 上用,有个 nvm-windows,不知道现在是否好用。
dfkjgklfdjg 6
dfkjgklfdjg 42 天前
使用 NVM 来管理,然后指定一下全局安装目录就好了,我是直接指定到一个外部目录,然后不管怎么切换版本都全局依赖都不会丢。
dengshen 7
dengshen 42 天前 via iPhone
Mac ?用 n 来管理版本
Jirajine 8
Jirajine 42 天前 via Android
可以看看 volta,像 rustup 一样管理 node 环境。
threeEggs123 9
threeEggs123 42 天前 via Android
借个楼问一下,node 12.x 版本,create-react-app,npm run start 后,cpu 直接蹦到一百,依赖就是那些 ts,antd,谷歌都没有找到解决方案,怎么定位问题呢。cpu 高,内存没多少。
zhaihaoxyz1 10
zhaihaoxyz1 41 天前
可以使用容器管理

magicdawn 11
magicdawn 15 天前
几年前看到了知乎安利 nvs, 就是看中了每次升级不用重新安装所有 global modules

https://zhuanlan.zhihu.com/p/63403762

npm config set prefix ~/.npm-global

编译完的内核如何快速删除没有编译的.c 文件

编译完的内核如何快速删除没有编译的.c 文件?目的是我只需要关注被编译的 code 就可以。这样不会受多余的代码干扰。放到代码阅读软件中比如 source insight 也简洁很多。当然如果能删除一些.h 文件,就更好了。但我感觉不一定可以。
编译 删除 内核 文件5 条回复 • 2021-08-19 12:24:11 +08:00
yanqiyu 1
yanqiyu 1 天前 via Android
虽然我猜肯定有专门的工具干这事情,但是编译一轮之后单纯靠 atime 应该就可以排除没被使用的文件。

不过 Linux 内核的目录结构都很清晰,看代码也不太容易被干扰啊。
abutter 2
abutter 1 天前
一种办法是是使用 objdump 生成带文件路径信息反汇编代码,然后从里面提取文件路径,去掉行号,去重。
huangya 3
huangya 1 天前
@yanqiyu
>不过 Linux 内核的目录结构都很清晰,看代码也不太容易被干扰啊。
对新手或者不了解的人还是很有好处的。另外同一个目录下,也有一些 C 文件可能不会编译
bfdh 4
bfdh 23 小时 57 分钟前
#!/bin/bash
set -e
#set -x

#脚本说明
#该脚本用于在编译后的 Linux Kernel 目录中找出实际编译的全部.c 文件及其依赖的.h 文件,
#并将结果保存在 files.list 中,可直接导入到 sourceinsight 。
#执行方法:将脚本放到 Linux Kernel 编译目录直接执行即可。

TMPDIR=`mktemp -d`
MASTERDIR=”$PWD”
OCMDFILE=$TMPDIR/file.ocmd
TFILE=$TMPDIR/file.t
AFILE=$TMPDIR/file.a
BFILE=$TMPDIR/file.b
CFILE=$TMPDIR/file.c
DFILE=$TMPDIR/file.d
HFILE=$TMPDIR/file.h

function get_header_file_from_ocmd()
{
local prefix=$(echo $MASTERDIR/ | sed ‘s/\//\\\//g’)
oc=$1 #name of input file,input file is mixed content of all .o.cmd file
out=$2 #name of output file

echo “handle ocmd file $oc for header file”

grep -E “\.h)?$” $oc > $out
sed -i -e ‘s/\.h)$/.h/g’ -e “s/^\([^\/]\)/${prefix}\1/g” $out

echo “handle ocmd file $oc for header file done”
}

function merge_result()
{
cat $@ > files.list

echo All .c and .h files are listed in files.list, now you can import them to sourceinsight project.
}

function get_defination_from_ocmd()
{

oc=$1 #name of input file,input file is mixed content of all .o.cmd file
out=$2 #name of output file

echo “handle ocmd file $oc for defination”
rm -fv $out

sort -u $oc > $out
cp -v $out $oc

grep “\-\bD” $oc |awk -F’ ‘ ‘{for(i=1;i < NF;i++)printf(“%s\n”, $i);}’ |grep “\-\bD”|sed -e “s/\-D/\#define\ /g” -e “s/=/\ /g” -e ‘/KBUILD_/’d | sort -u > $out

echo “handle ocmd file $oc for defination done”
}

find -name “*.o.cmd” | sed -e ‘/\.mod\./’d -e ‘/\.built-in\./d’ > $OCMDFILE #no “.*.modmake -j24.cmd” and “.built-in.o.cmd”
cat $OCMDFILE | awk -F ‘.’ ‘{printf(“%s%s%s.c\n”, “‘$MASTERDIR'”, $2, $3)}’ > $TFILE #add $MASTERDIR to the start of line
sort -u $TFILE > $CFILE
echo “System:List of C file is in $CFILE”

echo “Mix content of all .o.cmd file”
while read line; do
cat $line
done < $OCMDFILE >> $AFILE
echo “Mix content of all .o.cmd file,done,$counts files has been handled”

echo “Genrate defination file”
cp -v $AFILE $BFILE

get_defination_from_ocmd $BFILE $DFILE
echo “Genrate defination file,done”
echo “System:List of D file is in $DFILE”

echo “Put all unit of information to single line mode”
sed ‘s/\ /\n/g’ $AFILE > $TFILE
echo “Put all unit of information to single line mode,done”

echo “Sort and Uniq all unit of information”
sort -u $TFILE > $AFILE
echo “Sort and Uniq all unit of information,done”

echo “Get all name of header file”
get_header_file_from_ocmd $AFILE $TFILE
echo “Get all name of header file,done”

echo “Sort and Uniq all name of header file”
sort -u $TFILE > $AFILE
echo “Sort and Uniq all name of header file,done”

echo “Delete the name of header file of host”
sed ‘/^\/usr/d’ $AFILE > $HFILE #delete line which start with “/usr”,it’s host’s header file
echo “Delete the name of header file of host,done”

echo “System:List of H file is in $HFILE”

merge_result $CFILE $HFILE

rm -fr $TMPDIR

补充:如果不删 TMPDIR,还可以在 file.d 文件中查看通过命令行 /Makefile 传递进去的宏定义。
nsynet57 5
nsynet57 22 小时 12 分钟前
其实必要性不大,,,

阿里云 OSS Bucket 被墙了怎么搞?

OSS 都能墙也是*次见,昨天好好的,早上有客户报告说图片无法显示,于是

发工单客户回复说:

您好, 由于香港与中国大陆的网络受到网络管制,导致经常性的被墙。这个对公网跨境流量而言,属于不可抗力因素。

随便搜了一下,很多人都中招了

globalpro.oss-cn-hongkong.aliyuncs.com
dpom.oss-cn-hongkong.aliyuncs.com
chaoji001.oss-cn-hongkong.aliyuncs.com
然后想着那没办法走 CDN 试试,结果阿里云自己的 CDN 在大陆也访问不了自家香港 OSS 源站。 客服非让我开通快速传输功能,这玩意儿价格 1.25 元 /G,开了之后有一个快速传输专属的 Endpoint 域名,IP 换了当然可以访问了,但流量确实用不起,源站下行流量再加上快速传输流量,差不多 2 块钱 1G 。

那就用大陆服务器中转一下吧,居然大陆服务器内网都无法访问自家的香港 OSS

阿里云这种明明换个 IP 就能解决的问题,为何要用户自己认倒霉。

跟客服扯皮,解决方案只有新建一个 Bucket 把数据迁移过去,下行流量费自负。另一个方案是开通快速传输给贵价流量费。

OSS 源站 流量费 CDN18 条回复 • 2021-08-20 09:59:13 +08:00
lshero 1
lshero 2 天前
阿里云腾讯云都在 cloudflare 的带宽联盟中
lance6716 2
lance6716 2 天前 via Android
缺钱了
shuimugan 3
shuimugan 2 天前 ❤️ 1
这种无非就是套娃,一层一层套.

比如利用 Cloudflare 可以反代被墙的 ip,那么也可以用来反代被墙的 bucket.

如果是私有的 bucket 需要计算签名的话,之前不知道哪里看到 Cloudflare 好像还能计算好那些对象存储的签名,但我没考证过.

也可以自己写个自动计算签名的 proxy,我之前用 nodejs 写过一个,200 来行代码搞定,然后部署到香港服务器比如轻量云上,保险起见还能再套一层 Cloudflare
locoz 4
locoz 2 天前 via Android
阿里云好像有个私有专线,在弹性公网 IP 那个里面可以选择加速服务,跨境流量可以直接走阿里云专线直通,不知道会不会过墙…
kingfalse 5
kingfalse 2 天前 via Android
就是养肥了该宰了,没有别的意思
eason1874 6
eason1874 2 天前 ❤️ 1
在同地域开一台便宜竞价机器,内网下载到机器硬盘流量免费,再从机器上传到任意地域 OSS 流量也免费,只需要付 OSS 请求费和临时机器费用
a719031256 7
a719031256 2 天前
鉴定:楼主肥羊一只
xingyuc 8
xingyuc 2 天前 ❤️ 14
对内,他们称香港是境外;
对外,他们坚持一个中国。
cubecube 9
cubecube 1 天前
这个和宰客没关系吧,楼上的也太恶意推测了。
wall 的行为不可预判而已,哪怕是运营商和云服务商也没办法呀。
smileawei 10
smileawei 1 天前
OSS 有个功能是全球加速。按流量收费的。你值得拥有。

salmon5 11
salmon5 1 天前
我觉得阿里云没问题,就像你的身份证号,换成北京户口不就是换个号吗,你办个试试?看看是不是这么简单
myqoo 12
myqoo 1 天前
多申请几个域名 /IP 做冗余,然后在客户端自动选择*快的。
butanediol2d 13
butanediol2d 1 天前 via iPhone
@xingyuc 歪个楼?
《中华人民共和国出入境管理法》第八章,第八十九条:

入境,是指由其他国家或者地区进入中国内地,由香港特别行政区、澳门特别行政区进入中国内地,由台湾地区进入中国大陆。

可见香港确实是境外。
LGA1150 14
LGA1150 1 天前 via Android
@salmon5 然而迁户口并不会变身份证号
stimw 15
stimw 21 小时 38 分钟前
@lshero #1 阿里不知道,腾讯是需要国际站用户才能用带宽联盟的。
lshero 16
lshero 20 小时 12 分钟前
@stimw https://www.aliyun.com/product/news/detail?spm=5176.20947395.xingqu.1.6bbd2857lLqc4u&id=17749
好像可用区是境外的就行了

@eason1874 直接用 oss 自带的工具迁移就行了,机器都不用开 https://help.aliyun.com/document_detail/95074.html
stimw 17
stimw 18 小时 29 分钟前
@lshero #15 他这个产品文档给的网址进去就是国际站…估计就是国际站没跑了,腾讯阿里对带宽联盟是一个尿性
xingyuc 18
xingyuc 36 分钟前
@butanediol2d 其他国家我不清楚,但是我想有一天去自己国家也不用办各种证件……虽然也不一定会去看

为了一个HTTPS,浏览器操碎了心···

浏览器

我是一个浏览器,每到夜深人静的时候,主人就打开我开始学习。

为了不让别人看到浏览记录,主人选择了“无痕模式”。但网络中总是有很多坏人,他们通过抓包截获我和服务器的通信,主人干了什么,请求了什么数据全被他们知道了!

%title插图%num

浏览器

光窃听也就罢了,他们还经常篡改内容,在网页里面插入诱人的小广告,真是太坏了!为了保护主人的隐私还他一个干净的上网环境,我决定对通信加密!

%title插图%num

1

直接简单加密

加密嘛,很简单,把原来要发送的数据加密处理后再发给服务器就行了。

%title插图%num

为了安全,密钥当然不能固定,每一次通信都要随机生成。

不过接下来我犯难了,我该怎么把这个秘钥告诉服务器呢,服务器没有秘钥就解不了密,也就不知道我在请求什么资源了。

也不能直接弄个字段告诉服务器密钥,那样别人也能拿到,就跟没加密一样了。

我左思右想,灵机一动,决定把密钥放在数据的开头几个字节藏起来,只要私下跟服务器约定好,他用这前几个字节作为密钥解密,就能解开我发送的数据了。

%title插图%num

你还别说,这办法还真好使,我跟服务器开始秘密通信起来。

后来,找我使用这种办法通信的服务器变得越来越多。

再后来这事就在圈子里传开了,大家都知道数据的前几个字节是密钥了,谁都能解密了。

看来这个办法不行,我得重新思考加密方法了。

2

非对称加密

服务器告诉我,我们之前用的那种加密算法叫对称加密算法,也就是加密和解密使用的同一个秘钥。

还有一种叫非对称加密算法,这种算法有两个秘钥,一个公开的叫公钥,一个私藏的叫私钥。

%title插图%num

*关键的是,公钥加密后只能用私钥解开,反过来也一样。

只要在正式的数据传输前,服务器把他的公钥告诉我,我后面用它加密数据就行了,就算被别人抓包,他也解不开,因为只有拥有私钥的服务器才能解开。

%title插图%num

不得不说,这非对称加密真是个好东西啊!

不过这样一来只能单程加密,服务器能解密我发的,但他发给我的,我却解不了,也不能让他用私钥加密,我用公钥解密,因为公钥是公开的,谁收到都能解,不安全。

没办法,我也弄了一对儿秘钥,通信之前我们双方都交换一下彼此的公钥,这样就可以双向加解密了!

%title插图%num

虽然是有点麻烦,但为了数据安全,忍了吧!

3

非对称与对称加密结合

但我忍了没几天就忍不住了。

这个非对称加密算法好是好,就是加解密太费时间了,导致我渲染一个网页要花很久时间,卡的不行。

我打算去跟服务器商量一下办法,没想到服务器比我更头疼,他要服务很多浏览器,每一个都这么加解密,把他累的够呛。

于是我们决定,还是用原来的对称加密算法,这样快得多。但是一开始的时候可以用非对称加密算法来传输后面要用的秘钥,把两种算法的优势结合起来。

%title插图%num

这一来,我只需要把后面要用到的秘钥,通过服务器公钥加密后发给他就行了,我省去了不少事儿。

4

秘钥计算

有一天,服务器告诉我,我们现在的秘钥就是一个随机数,而随机数并不是真正随机的,可能被预测出来,所以我们得提升这个秘钥的安全性。

一个随机数不够,那就多弄几个!

一端容易被猜出来,那就两端一起生成!

我们决定各自生成一个随机数发给对方,我再额外加密传输一个随机数给服务器,这一来,咱们双方都有3个随机数了,然后双方都用这三个随机数计算出真正的秘钥,这可比一个单纯的随机数要安全得多了。

%title插图%num

不过为了验证双方计算出来的秘钥是一样的,我们在正式数据传输前,需要先来测试一下,现在的流程变成了这个样子:

%title插图%num

我们的这一方案很快得到了大家的认可,圈子里的浏览器和服务器们纷纷用上了这套方案。

5

数字证明

原以为这个方案已经万无一失了,没想到我和服务器的通信还是泄露了···

原来有个家伙冒充服务器跟我通信,然后又冒充我跟服务器通信,把我的请求进行了转发,我们俩都被蒙在鼓里,这就是中间人攻击。

%title插图%num

看来还缺乏一个认证机制!我得知道和我通信的是不是真的服务器。

经过大家的商量,圈子里的服务器们推选了一个德高望重的前辈做公证人,让这公证人准备一对非对称加密的密钥,并在圈子里公开了公钥,所有人都得把他的公钥记下来。

服务器得去公证人这里先登记,把自己的公钥、名字等等信息报上去,公证人拿到这些信息后,计算一个Hash值,然后再用公证人的私钥把Hash值进行加密,加密后的结果就是数字签名。

%title插图%num

证书的签发

*后,公证人把登记的信息和这个数字签名合在一起,封装了一个新的文件发给服务器,登记就完成了,而这个新的文件就是数字证书。

服务器拿到证书后,可要好生保管,因为通信的时候,服务器须要将他们的证书发给我们浏览器验证。

%title插图%num

证书的验证

我们浏览器拿到证书后,把证书里面的信息也计算一遍Hash,再用提前记录好的公证人的公钥把证书里的数字签名进行解密,得到公证人计算的Hash,两个一对比,就知道这证书是不是公证人签发的,以及有没有被篡改过了!

只有验证成功才能继续后面的流程,要不然就是冒充的!

这一下总算解决了中间人冒充的问题,除非中间人偷到了公证人的私钥,否则他是没办法伪造出一个证书来的。

非对称加密除了加密数据,还能用来验证身份,真是YYDS!

6

信任链

我们这加密方案一传十,十传百,很快就传遍了整个互联网,想要使用这套方案的服务器越来越多,毕竟,谁都不希望自己的网站被人插入小广告。

可原来的那个公证人有些忙不过来了,于是,大家开始推选更多的公证人,公证人开始多了起来,不仅多了起来,而且还形成了产业链。

原来的公证人变成了一代目,一代目可以给新的公证人签发证书,新的公证人就变成了二代目,还有三代目,搞得跟传销似的。

原来只有一个公证人的时候,大家直接保存他的公钥就行了。现在公证人越来越多,我们没办法保存所有的公证人的公钥了,就算能保存得下,但有新的公证人出现的时候我们也做不到实时更新。

%title插图%num

于是,大家约定,让所有的一代目公证人自己给自己签发一个证书,叫做根证书,并安装在我们的操作系统中。

%title插图%num

以后在验证网站服务器的证书时,就得先去验证证书的签发者,然后再继续验证上一级签发者,直到验证*终的签发者是不是在根证书列表中。

只要*终的签发者在系统的根证书列表中,那这条链上签署的证书就都是受信任的,否则我们就会弹窗提醒用户:

%title插图%num

如今,这套方案已经推广到了全世界,现在遇到使用这套方案的网站服务器时,我们浏览器就会在地址栏加上一把小锁,表示网站很安全,还把URL地址,从HTTP,改成了HTTPS···

%title插图%num

PS:本文用故事形式讲述了HTTPS是如何工作的,只是起一个引领入门的作用,略去了很多细节,实际情况远比这复杂,比如对称加密秘钥的计算方式、秘钥的交换算法(RSA、DH、ECDH还有区别),双方测试秘钥正确性的方式都没有体现出来,有机会再写一篇正经的技术文来详细抓包剖析HTTPS详细流程。

Python 关于字典的操作

需求是将两个列表合并,如果 err_data 中的 id 与 new_data 中的 id 重复,那么就把 err_data 中的项去掉,各位彦祖,合并后的顺序无所谓,求一个较好的方法。

err_data = [
    {'id': 1, 'data': 23},
    {'id': 2, 'data': 24},
    {'id': 3, 'data': 25},
    {'id': 4, 'data': 222},
    {'id': 5, 'data': 134},
    {'id': 6, 'data': 255}
]

new_data = [
    {'id': 1, 'data': 345},
    {'id': 2, 'data': 124},
    {'id': 28, 'data': 124},
    {'id': 29, 'data': 124},
    {'id': 30, 'data': 124}
]

# 需求是将两个列表合并,如果 err_data 中的 id 与 new_data 中的 id 重复,那么就把 err_data 中的项去掉,
# new_data + err_data 的结果如下
result = [
    {'id': 3, 'data': 25},
    {'id': 4, 'data': 222},
    {'id': 5, 'data': 134},
    {'id': 6, 'data': 255},
    {'id': 28, 'data': 124},
    {'id': 29, 'data': 124},
    {'id': 30, 'data': 124},
    {'id': 1, 'data': 345},
    {'id': 2, 'data': 124},
]

日志树的生成

各位大佬,请教一个问题 这种日志树是怎么生成的? 。。。 发现了一个问题 V2EX 无法发图片 算了 直接看他的返回格式

com.cityminsu.rba.baseinfo.api.vo.ProductVo
├──价格结束时间:'Mon Nov 15 00:00:00 CST 2021'->'Tue Mar 22 16:29:00 CST 2022'
├──产品状态:'下架'->'上架'
├──规则模版
│    ├──是否可退:'取消扣全款'->'限时取消'
│    ├──rbaRatePlanScope
│    │    └──rbaRatePlanScopeList
│    │         ├──:添加 '[houseList=[0]=0;orgId=0]'
│    │         └──:删除 '[houseList=[0]=0;orgId=0]'
│    └──stayDaysProduct:添加 '[[StayDaysProductVo{productId=null, stayDays=2, discountRatio=80, productStatus=1, active=0}]=[active=0;discountRatio=80;productStatus=1;stayDays=2];[StayDaysProductVo{productId=null, stayDays=3, discountRatio=79, productStatus=1, active=0}]=[active=0;discountRatio=79;productStatus=1;stayDays=3];[StayDaysProductVo{productId=null, stayDays=5, discountRatio=77, productStatus=1, active=0}]=[active=0;discountRatio=77;productStatus=1;stayDays=5];[StayDaysProductVo{productId=null, stayDays=7, discountRatio=75, productStatus=1, active=0}]=[active=0;discountRatio=75;productStatus=1;stayDays=7]]'
├──价格开始时间:'Wed Aug 18 00:00:00 CST 2021'->'Wed Aug 18 16:29:00 CST 2021'
├──平日价:'0.00'->'1000.00'
└──周末价:'0.00'->'1000.00'
  覆盖方式: '不覆盖日历上单独设立价格'

可以看到他返回的就是这种树状结构 请问是怎么实现的

时序数据库永远的难关 — 时间线膨胀(高基数 Cardinality)问题的解决方案

前序

随着移动端发展走向饱和,现在整个 IT 行业都期待着“万物互联”的物联网时代。在物联网场景中,往往有许多各类不同的终端设备,布署在不同的位置,去采集各种数据,比如某一区域有 10万个 loT 设备,每个 loT 设备每 5 秒发送一次数据。那么每年会产生 6307亿 个数据点。而这些数据都是顺序产生的,并且 loT 设备产生数据的格式全部是一致的,并且没有删除和修改的需求。针对这样按时海量写入无更新场景,时序数据库应运而生。

时序数据库在假定没有数据插入和更新需求,数据结构稳定的前提下,*限追求快速写入,高压缩,快速检索数据。时序数据的 Label(tag)会建立索引,以提高查询性能,以便你可以快速找到与所有指定标签匹配的值。如果 Label(tag)值的数量过多时(高基数 Cardinality 问题),索引会出现各种各样的问题, 本文主要讨论 influxdb 在遇到写入的数据出现高基数 Cardinality 问题时,一些可行的解决方案。

Part 1

高基数Cardinality问题

(时间线膨胀)

时序数据库主要存储的是 metric 数据,每一条数据称为一个样本(sample),样本由以下三部分组成:

  • 指标(时间线 time-series):metric name 和描述当前样本特征的 labelsets;
  • 时间戳(timestamp):一个精确到毫秒的时间戳;
  • 样本值(value):表示当前样本的值。

<————– time-series=”” ——–=””><-timestamp —–=””> <-value->
node_cpu{cpu=”cpu0″,mode=”idle”} @1627339366586 70node_cpu{cpu=”cpu0″,mode=”sys”} @1627339366586 5node_cpu{cpu=”cpu0″,mode=”user”} @1627339366586 25

通常情况下, time-series 中的 lablelsets 是有限的,可枚举的,比如上面的例子 model 可选值为 idle,sys,user。

prometheus 官方文档中对于 Label 的建议:

CAUTION: Remember that every unique combination of key-value label pairs represents a new time series, which can dramatically increase the amount of data stored. Do not use labels to store dimensions with high cardinality (many different label values), such as user IDs, email addresses, or other unbounded sets of values.

时序数据库的设计时,也是假定在时间线低基数的前提下。但是随着 metric 的广泛使用,在很多场景下无法避免出现时间线膨胀。

比如,在云原生场景下 tag 出现 pod/container ID之类,也有些 tag 出现 userId,甚至有些 tag 是 url,而这些 tag 组合时,时间线膨胀得非常厉害。

这个矛盾出现是必然的,怎么解决呢?是写入数据方调整写入数据时,控制写入的 time-series的数量,还是时序数据库去更改设计来适用这种场景?这个问题没有完美的解决方案,我们需要做出平衡。

从实际情况出发,如果时间线膨胀后,时序数据库不会出现不可用,性能也不会出现指数级别下降。也就是说时间线不膨胀时,性能优秀。时间线膨胀后,性能能达到良好或者及格就好。

那怎么让时序数据库在时间线膨胀的情况下性能还能良好呢?接下来我们通过influxdb的源码来讨论这个问题。

Part 2

时间线的处理逻辑

influxdb 的 tsm 结构,主要的逻辑处理过程类似 lsm。数据上报后,会添加到 cache 和日志文件(wal)。为了加快检索速度或者压缩比例,会对上报的数据进行 compaction(数据文件合并,重新构建索引)。
索引涉及到三个方面:

  • TSI(Time Series Index)检索Measurement,tag,tagval,time
  • TSM(Time-Structured Merge Tree)用来检索time-series -> value
  • Series Segment Index 用来检索 time-series key <–> time-series Id

具体influxdb的索引实现可以参照官方文章。

(https://github.com/influxdata/influxdb/blob/master/tsdb/index/tsi1/doc.go)

%title插图%num

当时间线膨胀后,TSI 和 TSM 的检索性能下降并不严重,问题主要是出现在 Series Segment Index 里。

这节我们会讨论influxdb的时间线文件的正排索引(time-series key ->id, id->time-series key):

  • SeriesFile 是 Database(bucket)级别的。
  • SeriesIndex 主要处理 key->Id, key->id 的索引映射。
  • SeriesSegment 主要存放的是 Series 的 Id 和 key。
  • SeriesIndex 里面是存放 Series 的 Id 和 key 等索引。

    (可以理解是两个 hashmap)

  • keyIDMap 通过 key 来查找对应的 Id。
  • idOffsetMap 通过 Id 查到到 offset,通过这个 offset(对应 SeriesSegment 的位置)来查找 SeriesSegment 文件获取 key。

%title插图%num

具体的代码(influxdb 2.0.7)如下:

  1. tsdb/series_partition.go:30 // SeriesPartition represents a subset of series file data. type SeriesPartition struct { … segments []*SeriesSegment index *SeriesIndex seq uint64 // series id sequence …. } tsdb/series_index.go:36 // SeriesIndex represents an index of key-to-id & id-to-offset mappings. type SeriesIndex struct { path string … data []byte // mmap data keyIDData []byte // key/id mmap data idOffsetData []byte // id/offset mmap data // In-memory data since rebuild. keyIDMap *rhh.HashMap idOffsetMap map[uint64]int64 tombstones map[uint64]struct{} }

对 series key 进行检索时,会先在内存 map 中查找,然后在磁盘的 map 上查找,具体的实现代码如下:

  1. tsdb/series_index.go:185 func (idx *SeriesIndex) FindIDBySeriesKey(segments []*SeriesSegment, key []byte) uint64 { // 内存map查找 if v := idx.keyIDMap.Get(key); v != nil { if id, _ := v.(uint64); id != 0 && !idx.IsDeleted(id) { return id } } if len(idx.data) == 0 { return 0 } hash := rhh.HashKey(key) for d, pos := int64(0), hash&idx.mask; ; d, pos = d+1, (pos+1)&idx.mask { // 磁盘map查找offset elem := idx.keyIDData[(pos * SeriesIndexElemSize):] elemOffset := int64(binary.BigEndian.Uint64(elem[:8])) if elemOffset == 0 { return 0 } // 通过offset获取对于的id elemKey := ReadSeriesKeyFromSegments(segments, elemOffset+SeriesEntryHeaderSize) elemHash := rhh.HashKey(elemKey) if d > rhh.Dist(elemHash, pos, idx.capacity) { return 0 } else if elemHash == hash && bytes.Equal(elemKey, key) { id := binary.BigEndian.Uint64(elem[8:]) if idx.IsDeleted(id) { return 0 } return id } } }

这里补充一个知识点,将内存 hashmap 转成磁盘 hashmap 的实现。我们都知道 hashmap 的存储是数组,influfxdb 中的实现是通过 mmap 方式映射磁盘空间(见 SeriesIndex 的 keyIDData),然后通过 hash 访问数组地址,采用的 Robin Hood Hashing,符合内存局部性原理(查找逻辑的代码如上 series_index.go 中)。将 Robin Hood Hashtable 纯手动移植磁盘 hashtable, 开发人员还是花了不少心思。

那内存 map 和磁盘 map 是如何生成的,为什么需要两个 map?
influxdb 的做法是将新增的 series key 先放到内存 hashmap 里面,当内存 hashmap  增长大于阈值时,将内存 hashmap 和磁盘 hashmap 进行 merge(遍历所有 SeriesSegment,过滤已经删除的 series key)生成一个新的磁盘 hashmap,这个过程叫做 compaction。compation 结束后内存 hashmap 被清空,然后继续存放新增的 series key。

%title插图%num

  1. tsdb/series_partition.go:200 // Check if we’ve crossed the compaction threshold. if p.compactionsEnabled() && !p.compacting && p.CompactThreshold != 0 && p.index.InMemCount() >= uint64(p.CompactThreshold) && p.compactionLimiter.TryTake() { p.compacting = true log, logEnd := logger.NewOperation(context.TODO(), p.Logger, “Series partition compaction”, “series_partition_compaction”, zap.String(“path”, p.path)) p.wg.Add(1) go func() { defer p.wg.Done() defer p.compactionLimiter.Release() compactor := NewSeriesPartitionCompactor() compactor.cancel = p.closing if err := compactor.Compact(p); err != nil { log.Error(“series partition compaction failed”, zap.Error(err)) } logEnd() // Clear compaction flag. p.mu.Lock() p.compacting = false p.mu.Unlock() }() }
  1. tsdb/series_partition.go:569 func (c *SeriesPartitionCompactor) compactIndexTo(index *SeriesIndex, seriesN uint64, segments []*SeriesSegment, path stringerror { hdr := NewSeriesIndexHeader() hdr.Count = seriesN hdr.Capacity = pow2((int64(hdr.Count) * 100) / SeriesIndexLoadFactor) // Allocate space for maps. keyIDMap := make([]byte, (hdr.Capacity * SeriesIndexElemSize)) idOffsetMap := make([]byte, (hdr.Capacity * SeriesIndexElemSize)) // Reindex all partitions. var entryN int for _, segment := range segments { errDone := errors.New(“done”) if err := segment.ForEachEntry(func(flag uint8, id uint64, offset int64, key []byte) error { … // Save max series identifier processed. hdr.MaxSeriesID, hdr.MaxOffset = id, offset // Ignore entry if tombstoned. if index.IsDeleted(id) { return nil } // Insert into maps. c.insertIDOffsetMap(idOffsetMap, hdr.Capacity, id, offset) return c.insertKeyIDMap(keyIDMap, hdr.Capacity, segments, key, offset, id) }); err == errDone { break } else if err != nil { return err } }

这样设计有两个缺陷:

1. 做 compaction 时,当 io 访问 SeriesSegments 文件, 内存加载所有的 series key,会构建一个新的 hashtable,然后将这个 hashtable mmap 存储到磁盘,当 series key 超过几千万或者更多时,会出现内存不够,oom 问题。

2. 做 compaction 时, 对于已经删除的 series key(tombstone 标记)做了过滤,不生成 series index,但是 SeriesSegment 中已经删除 series key 只有做了 tombstone 标记,不会做物理删除,这样会导致 SeriesSegment 一直膨胀,在实际生产环境一个 partition 下的所有 segmeng 文件超过几十 G,做 compaction 时,会产生大量 io 访问。

Part 3

可行的解决方案

1

增加partition或者database

influxdb 的正排索引是 database 级别的,有两个方式可以减少 compaction 时的内存,一个是增加 partition 数量或者将多个 Measurement 划到不同的 database 里面。

但这样做的问题是,已经存在数据的 influxdb 不好调整两个数据。

2

修改时间线存储策略

我们知道 hash 索引是 O1 的查询,效率非常高,但是对于增长性的数据,存在扩容问题。那我们做个折中的选择。当 partition 大于某个阈值时,将 hash 索引变成 b+tree 索引。b+tree 对于数据膨胀性能下降有限,更适合高基数问题,而且不再需要全局的 compaction。

3

将series key的正排索引下沉到shard级别

influxdb 里面每个 shard 都是有时间区间的,某个时间区间内的时间线数据并不大。比如 database 里面保存的是 180天 的 series key,而 shard 一般只有一天甚至 1 个小时的跨度,两者存放的 series key 存在 1~ 2 个数量级的差距。另外将 series key 正排索引下沉到 shard 级别对删除操作更友好,当 shard 过期删除时,会将当前 shard 的所有 series key 和其他 shard 做 diff,当 series key 不存在时再去删除 series key。

4

根据Measurement修改时间线存储策略

在实际生产环境中,时间线膨胀和 Measurement 有很大关系,一般是少数的 Measurement 存在时间线膨胀问题,但是*大部分的 Measurement 不存在时间线爆炸的问题。

我们可以对做 series key 的正排索引的 compaction 时,可以添加 Measurement 时间线统计,如果某个 Measurement 的时间线膨胀时,可以将这个 Measurement 的所有 series key 切换到 B+ tree。而不膨胀的 series key 继续保留走 hash 索引。这样方案性能比第二个方案更好,开发成本会更高一些。

目前高基数问题主要体现在 series key 正排索引。个人觉得短期先做第二个方案过度到第四个方案的方式。这样可以比较好的解决时间线增长的问题,性能下降不多,成本不高。第三个方案改动比较大,设计更合理,可以作为一个长期修复方案。

总结

本文主要通过 influxdb 来讲解时序数据库的高基数 Cardinality 问题,以及可行的方案。metric 的维度爆炸导致数据线膨胀问题,很多同学都认为这是对时序数据库的误用或者是滥用。但是信息数据爆炸的今天,让数据维度收敛,不发散成本非常高,甚至远高于数据存储成本。

个人觉得需要对这个问题进行分而治之的方式,提升时序数据库对维度爆炸的容忍度。换句话说,出现时间线膨胀后,时序数据库不会出现崩溃情况,对时间线未膨胀的 metric 继续高效运行,而出现时间线膨胀的 metic 可以出现性能下降,单不会线性下降。提升对时间线膨胀的容忍度,控制时间线膨胀的爆炸半径,将会成为时序数据库的核心能力。