大模型和发现规律

科幻作家 Ted Chiang 在评价 ChatGPT 是网上所有文本的模糊图像,大模型对它的描述是对知识的压缩
大模型对大量的数据,这里的大量是指人类有史以来创建的所有文本(音频&视频)。把文本转换成一段向量,对向量进行 transformer 计算。最后得到一个模型。 模型里面记录的是什么呢?从数学上看,它是一堆的数字,或者是概率。如果把文本比作全部北京人的通勤数据,那么得到的模型就是每个路口的信息。在导航的情况下,我们是知道了每个路口的信息,然后从北京西站到西二旗。

有一部《别惹蚂蚁》的动画电影,如果在蚂蚁洞里没有方向,没有坐标。只知道蚂蚁的运动轨迹,需要怎么来确定最佳的路线呢。只要运动轨迹足够多,那么每个洞口的信息就足够清晰,这就是大模型干的事情,通过大量的输入确定某个点的的描述。 只要这样的描述足够多,那么我们就越接近真实。


印度的数学家拉马努金在数学上,给出了很多恒等式,而且很多等式在其他人看都是靠他的直觉来计算出来的。

在使用大模型的时候,大多数时候都是在寻找已经存在的知识,比如某段代码怎么写,某个文章怎么描述,我们期望找到好的题词,进而生成更加优秀的内容。

大模型通过向量,将知识映射到了多维空间,作为个人很难去理解多维空间,在科幻小说里可能会提到四维空间,比如三体:黑暗森林里面的女巫在四维空间里摘取三维空间人的大脑。星际穿越里,在四维空间里,能够看到不同三维时空。当维度升高时,事物之间的联系会变得千丝万缕。

我们生活在三维世界,理解三维世界。当维度升高时,世界就会变得我们无法理解。大模型是一个简陋的多维世界的描述,它通过多个维度,为所有存在的文字,创建了它们内在的联系。就像拉马努金脑袋里的数字之间的联系,要比我们普通人对数字的联系要多得多。

那么大模型除了是对知识的压缩外(向量空间里面的点),它还是对知识之间直觉(点和点组成的边)的描述。


知识在大模型里到底是有损还是无损的。前两天看到一篇文章,讲到大人和儿童的学习知识的方式,儿童很容易学会,而大人学习起来会很困难。里面提到人类的学习能力,其实是一种强化学习,随着年龄的增加,某些神经突触会被强化,变成一种本能的反应,当某些地方的连接被强化的同时,就会有某些地方的连接被弱化。而儿童的认知是白纸,任何的方向都很容易被强化,所以学习会比大人更加容易。

在学习一个新的知识的时候,比如大学的时候编程里面学习面相对象编程。“多态”这个名词对于新人来说是个新的名词。课本告诉我们多态是什么,其实我们是不能够理解的。甚至面向对象编程也是一个很模糊的概念,因为大学编程的实践非常的少。很难去理解这些概念,但是这并不妨碍编程的学习。

新的知识是对某个突触的强化,它是建立在现有知识里,并对现有知识的增强。从本身来说,知识被压缩到了某个点上,它是有损的。从全局来看,它又是对其他内容的扩充,它是不久是无损,而且还增加了新的内容。我们会通过旧知识来扩展和联想新知识,让点变成面。


在发现世界的规律时,当找到一个简单的规律,比如万有引力,如何通过一个简单的规律去发现更加复杂的规律,这可能才是大模型最终的能力,它不是有成百上千的直觉,而是上亿的直觉,通过这种亿级的直觉。 我们能够发现更多的规律。

三体里面宇宙社会学,如果在大模型世界里创建三条公理,能通过三条公理发现什么规律呢。在学习数学的时候,对于会有各种概念。公理、定理、推理等等。通过公理也就是不需要论证,它就是那样。通过公理推出定理,还有各种的公式. 通过万有引力公式,能够算出逃逸速度。内部的计算逻辑在大模型里是否能够,通过现有的知识,发现新的规律呢。材料科学里少许物质的变化会形成完全不同的物理特性,它们是否有一个我们尚未发现的通用公式呢。


从现实主义的角度来讲,我们需要什么样的大模型,如果不计成本的话,当然是模型越大越好。现实世界往往都是技术在成本上妥协,或者说是同样的成本提供更好的服务。比如搜索的成本,当出现使用小型机通过集群的方式代替大型机,将单个搜索的成本下降 1000 倍,当年淘宝也有去 IOE ,如果按现在的模式,整个 AI 行业基本都是在替英伟达打工。就像 20 年前整个网络都在替思科打工。 那么未来会怎么样呢。

  1. 未来同样算力的成本会下降,每一年半同样的成本算力会翻倍(摩尔定律)
  2. 本地大模型优先,在线大模型为辅,普及到每个人,交互不是 prompt ,而是自然语言。
    如果在成本不变的情况下,算力提高 10000 倍,大模型会给交互设备来带什么惊喜?
    会有新的模型来代替 transformer ,会有算力更强,能耗更低的推理设备(成本下降 10000 倍),会在 chat 之外的场景(可能是任何时间,任何地点,任何设备)。

我看可以让 AI 像大师一样作画,却没法像小孩一样去打酱油。如果 AI 是那个小孩,他知道打酱油需要 2 快钱,知道要去村东头小卖部。
每个人的期望都是突然有一天,有一个天神下凡一样的人物,突然的解决所有的问题。现实是需要慢慢打怪升级。
也许 AI 是陪伴,理解,然后成为自己,它记得你所有的往事,就像银河帝国里的克里昂大帝,永远在那里,和银河消亡。

均衡负载里的算法

在均衡负载重,轮询算法是最常用的一个算法。 通常会使用带权重的轮询(wrr).

轮询

轮询的实现非常简单,假设我们有一组节点 [a,b,c,d] ,在不带有权重的时候,只需要一个 next 变量就可以完成。
使用 next 变量记录当前的位置,下一个就是当前位置加一后与节点的长度求余。

1
2
3
4
5
6
7
next := 0 // 当前选择
.....

func rr(){
next = (next+1)%len(servers)
return next
}

带权重的轮询

通常情况下,我们都会选择带权重的轮询来作为均衡算法,带权重有几个好处:

  • 将某个机器的权重调整为 0 来进行上下线的操作
  • 上线新功能或者新机器,可以逐步调大权重,来灰度部分流量来验证功能。

假设我们有这样一组节点 [ a:10 ,b:20,c:30] , 用来表示 3 个节点和其对应的权重。
我们在做计算的时候,需要对所有的权重求出他们的最小公倍数,让他们的表示成[a:1,b:2,c:3] 这样的值。

这里涉及到一个 gcd 的算法,用来求最小公倍数

gcd

gcd 的算法非常简单,叫做辗转求余,代码如下

1
2
3
4
5
6
func gcd(a int, b int) int {
if b == 0 {
return a
}
return gcd(b, a%b)
}

具体可以参看 gcd

wrr

通过 gcd 计算得到 [a:1,b:2,c:3] 后,我们需要按照权重来选择节点。这个时候,可以将前面的节点变成这样的数组 [a,b,b,c,c,c] ,数组中的每个节点都出现的权重指定的次数,这样我们就可以通过前面 rr 里面的算法 next = (next+1)%len(servers) 来进行计算。
但是这里会出现一个问题,a,b,c 的访问在时间片上面是不均衡的,可能会导致某个时间片对某个区间的直接访问。 这就需要对数组里的内容进行打散。
在 nginx 的 wrr 算法: https://github.com/phusion/nginx/commit/27e94984486058d73157038f7950a0a36ecc6e35 保证节点选择的平滑。

算法如下: 对于节点权重 {5,1,1} ,当前一个节点被选中后,权重减去所有权重和,这里是 7 ,当开始选择时,每个节点加上自身的权重。

这里稍微有一些理论,可以想象一下,这些节点在跑道上排成一队,在选择之前,每个人向前走自己的权重步,如果谁在最前面的话,就能够被选中,被选中的人需要向后退权重和的步数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 a  b  c
0 0 0 (initial state)

5 1 1 (a selected) 第一次选择了 a ,那么 a 的权重 - 7
-2 1 1 第一次选择后的结果

对第一次选择结果加 5 1 1 得到 3 2 2
3 2 2 (a selected) 第二次选择 a ,那么 a 的权重 -7 得到 -4 2 2
-4 2 2 如此反复整过过程。

1 3 3 (b selected)
1 -4 3

6 -3 4 (a selected)
-1 -3 4

4 -2 5 (c selected)
4 -2 -2

9 -1 -1 (a selected)
2 -1 -1

7 0 0 (a selected)
0 0 0

这个算法能够很好的使算法能够均衡。

hash

使用 hash 作为均衡负载算法,会应用到很多地方,通常情况下我们会根据请求的一些值来作为均衡的 key。

特别是一致性的 hash 算法,处在网络请求的很多位置都会用到,可能所用到的 hash 算法不同,但是本质都是为了保证同一个数据包能够发送到一个位置。
通常会根据不同的需求,对 源 ip、源端口 、目标 ip、源端口、目标端口、协议号 这五元组来做 hash 算法。 比如以下的场景

  • 数据包到达网口,网卡多队列
  • cpu 支持 DUMA 的时,需要让相同的数据包进来在一个核上处理
  • 四层均衡负载中保证 tcp/udp 是到同一个后端
  • 七层均衡负载中保证同一个客户端到同一个后端处理

meglev

Maglev算法是Google开发的一种网络负载均衡器。在很多新的四层均衡负载中喜欢使用,比如 facebook 的 katran。
maglev 使用的一个大的槽位,通过一致性 hash 来将计算分布到槽位上。
使用时固定了槽位,查询效率为 O(1).

https://github.com/zhangweidev/meglevgo

NAT小记

NAT小记

NAT 的分类

  • 全锥形: 一旦内部主机端口对(iAddr:iPort)被NAT网关映射到(eAddr:ePort),所有后续的(iAddr:iPort)报文都会被转换为(eAddr:ePort);任何一个外部主机发送到(eAddr:ePort)的报文将会被转换后发到(iAddr:iPort)

    外部主机不限制 ip 和端口

  • 限制锥形:一旦内部主机端口对(iAddr:iPort)被映射到(eAddr:ePort),所有后续的(iAddr:iPort)报文都会被转换为(eAddr:ePort);只有 (iAddr:iPort)向特定的外部主机hAddr发送过数据,主机hAddr从任意端口发送到(eAddr:ePort)的报文将会被转发到(iAddr:iPort)。

    外部主机限制ip不限制端口

  • 端口限制锥形: 一旦内部主机端口对(iAddr:iPort)被映射到(eAddr:ePort),所有后续的(iAddr:iPort)报文都会被转换为(eAddr:ePort);只有(iAddr:iPort)向特定的外部主机端口对(hAddr:hPort)发送过数据,由 (hAddr:hPort)发送到(eAddr:ePort)的报文将会被转发到(iAddr:iPort)。

    外部主机限制 ip 和端口

  • 对称型: NAT网关会把内部主机“地址端口对”和外部主机“地址端口对”完全相同的报文看作一个连接,在网关上创建一个公网“地址端口对”映射进行转换,只有收到报文的外部主机从对应的端口对发送回应的报文,才能被转换。即使内部主机使用之前用过的地址端口对去连接不同外部主机(或端口)时,NAT网关也会建立新的映射关系。

    外部主机的响应包才能发送

STUN、TURN、ICE

https://developer.aliyun.com/article/243540

STUN: 为终端提供一种能够获取自己经过NAT映射后的地址.

客户端向公网 STUN 服务器发送 Binding Request ,服务器收到后获取公网 IP:PORT,附加在 Binding Request 返回给客户端. 

TURN: TURN 作为通讯中间人,由服务器负责两方的数据转发.

ICE: 一种框架,可以整合现有的NAT穿透协议,尽可能的找到NAT穿透的数据通道.

打洞过程

  • 两个客户端处于同一 NAT 设备后

Untitled.png

当A向集中服务器发出消息请求与B进行连接,集中服务器S将B的外网地址二元组以及内网地址二元组发给A,同时把A的外网以及内网的地址二元组信息发给B。A和B发往对方公网地址二元组信息的UDP数据包不一定会被对方收到,这取决于当前的NAT设备是否支持不同端口之间的UDP数据包能否到达(即Hairpin转换特性),无论如何A与B发往对方内网的地址二元组信息的UDP数据包是一定可以到达的,内网数据包不需要路由,且速度更快。A与B推荐采用内网的地址二元组信息进行常规的P2P通信。

  • 两个客户端处于不同 NAT 设备后

Untitled.png

同上一个例子一样, A 和 B 得到了对方的外网 ip:port

当 A 往 NAT-B 发送 UDP 消息,经过 NAT-A ,并在 NAT-A 上生成会话表项,根据NAT类型可知除了全锥形NAT,NAT-B 认为设备 A 得消息未授权外网消息,会丢弃该数据包.

这时B设备向A发送一个UDP消息. NAT-B 上也会生成一个到NAT-A 的会话表项.

此时 NAT-A 和NAT-B 都有了对方在外网的二元组,打开了 A 和 B 之间的洞.A 和 B 可以开始数据传输.

  • 两个客户端位于两层 NAT 设备后

Untitled.png

当出现多层级的 NAT 时(这是我们常见的类型),我们通过外网服务器S来打洞,可能存在某个 NAT是最优的选择,但是外网服务器S并不能够观察到,只能够选择离服务器S最近的NAT-C来打洞.

NAT 设备在空闲状态下会对转换表进行清理,比如一些家用的路由设备保存 NAT 时间大概是2-3分钟.某些设备可能短的只有 20s .为了维持可以通过心跳包的方式来维持连接.在连接超时后进行重新打洞.

http://www.52im.net/thread-542-1-1.html

如何创建一个小的容器镜像

如何创建一个小的容器镜像

对 golang 程序上线到了容器,在使用 scratch 的时候遇到了一个小小的问题,顺手对编译做了一些整理. 以下:

编写一个简单的Go应用程序,例如 gin 的例子 **main.go**:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"net/http"

"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

例子1: Dockerfile 编译

使用容器来编译运行程序

1
2
3
4
5
6
7
# 使用官方的 Golang 镜像作为基础镜像
FROM golang:1.20
WORKDIR /app
COPY . .
RUN go build -o main
EXPOSE 8080
CMD ["./main"]

通过构建的到了容器大小为 1.16GB

1
docker-golang-build-example1              latest               cd34e318087d   40 seconds ago   1.16GB

这个时候会容器的内容又一些大了. 可以看看 https://hub.docker.com/_/golang

golang:1.20 是基于 Debian 来作为基础镜像,本身就很大.

例子2: 使用 apline

我们可以看到提供了 alpine 作为基础镜像的包 golang:<version>-alpine ,只需要将 FROM 后面的镜像加上 alpine 即可

1
2
3
4
5
6
7
# 使用官方的 Golang alpine 镜像作为基础镜像
FROM golang:1.20-alpine
WORKDIR /app
COPY . .
RUN go build -o main
EXPOSE 8080
CMD ["./main"]

Untitled.png

使用 alpine 作为基础镜像会存在一些问题, 它使用了 musl libc 来代替 glibc ,小的镜像和稳定性的讨论会有一些不同的意见: https://news.ycombinator.com/item?id=10782897

镜像的大小从 1.16G 变成了 570M

例子3: 分段编译,使用 busybox

在编译过程中, go build 会拉取依赖,使用分段将编译和运行的镜像分开. golang 在运行时不需要编译环境,这里我直接使用 busybox 来作为基础镜像,同时例子2中使用 alpine 会存在一些问题,所以我们使用 busybox 的 glibc 的包.

1
2
3
4
5
6
7
8
9
10
11
FROM golang:1.20 AS build
WORKDIR /app
COPY . .
RUN go build -o main

# 最终阶段
FROM busybox:glibc
WORKDIR /app
COPY --from=build /app/main /app
EXPOSE 8080
CMD ["./main"]

Untitled.png

编译后得到了 15.5M 的镜像.

例子4: 静态编译,使用 scratch

在容器镜像中有一个 0M 的基础镜像 scratch ,它是所以镜像的基础.

容器本质上是基于Linux内核的Namespace、Cgroups和Union FS等技术对进程进行封装隔离的操作系统层面的虚拟化技术。

我们可以通过静态编译的方式,基于 scratch 来运行. 构建Go应用程序,并确保它是静态链接的。可以使用以下命令来构建:

1
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app main.go

在编译中添加了 -a -installsuffix cgo 两个条件,并指定了CGO_ENABLED=0:

-a : 强制重新构建
-installsuffix cgo: 指示编译器将生成的目标文件和包安装到一个特定的目录中,以便将与C语言绑定的包(使用**cgo**工具)与其他Go包区分开。
CGO_ENABLED=0 : cgo 工具标识为禁用

对 例子三 中的 dockerfile 做编译做调整,这个Dockerfile分为两个阶段:构建阶段和最终阶段。在构建阶段,它使用了官方的**golang镜像来构建Go应用程序,然后在最终阶段使用了scratch**基础镜像来创建最终的容器。

1
2
3
4
5
6
7
8
9
10
# 构建阶段
FROM golang:1.20 AS build
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app

# 最终阶段
FROM scratch
COPY --from=build /app/app /app
CMD ["/app"]

Untitled.png

编译后 docker-golang-build-example4 的大小从 15.5M 西江到了 11.2M, 这一点点的差值几乎就是 busybox 的存储占用.

要注意的问题

使用 scratch 镜像是一个空镜像,所以需要特别注意一些依赖和系统调用.

涉及到依赖时,需要解依赖,比如证书依赖:

COPY --from=builder /etc/ssl/certs/ /etc/ssl/certs/

涉及到系统调用,无法使用 scratch ,就需要考虑 apline 的方案.

例子5 加快编译速度

当把运行镜像的大小降下来后,还存在另一个问题,运行 go build 的时候都需要重新拉一遍golang 的环境. 可以通过挂在缓存的位置来减少应用程序编译时的时间.比如 golang 的 pkg目录 $GOPATH/pkg .在编译前提前做好 pkg 缓存,将缓存复制到目录中.

假设已经准备好了缓存目录 pkg. 在 例子4 中添加 COPY pkg/* /go/pkg/ 来加快编译速度.

1
2
3
4
5
6
7
8
9
10
11
# 构建阶段
FROM golang:1.20 AS build
WORKDIR /app
COPY pkg/* /go/pkg/
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app

# 最终阶段
FROM scratch
COPY --from=build /app/app /app
CMD ["/app"]

这个例子可能对 golang 的意义不是特别大, 对一些依赖比较多的 node 程序的 node_modele 目录缓存就很有用.

总结

通过四种方式,不断的降低了容器镜像的大小. 但是四种方式并没有特别的优劣之分,在不同的场景下,可以通过不同的策略来做镜像编译的方式.

  • 使用更小的镜像 apline 、busybox、scratch 来降低镜像大小
  • 使用分阶段构建来减少
  • 使用缓存来加快编译

在没有多阶段编译的时期,还有将多条命令写成一条的方式来降低容器的层数,或者借助一些工具来合并层.

以上例子放在了 github ,可以使用 docker-compose 来验证

docker-golang-build-example

使用 rust 和 golang 体验 wasm

使用 rust 和 golang 体验 wasm

本来是来记录使用 rust 来做 wasm 的开发.但是整个过程相比 golang 体验 wasm 的开发实在是太流畅了,几乎是跟着文档走就结束了. 整体的感受是相比 golang , rust 的 wasm 的工具链非常完善.

跟着文档,几个命令就把整体的流程走通,没什么可记录的.

Rust

https://rustwasm.github.io/docs/book/

工具准备

  • yarn global add wasm-pack

https://rustwasm.github.io/wasm-pack/installer/

  • cargo install cargo-generate
  • npm install npm@latest -g

项目

创建项目: cargo generate --git https://github.com/rustwasm/wasm-pack-template

创建后设置项目名称,比如’rest_wasm_example’就完成了项目的创建,文件的 src 中包含了 lib.rs 导入了 alert函数, 导出了 greet 函数.

构建: wasm-pack build

Golang

相比 golang 需要使用 tinygo 来实现相关的开发,而且工具链没有 rust 完善. tinygo 的整个的文档就在下面的文档中,在官方代码中还有几个示例

https://tinygo.org/docs/guides/webassembly/wasm/

这个也足够我们搞清楚使用 tinygo 来完成 wasm 的开发.

感受

tinygo 相比 golang 生成出来的 wasm 文件要小得多. 但是和 rust 相比还是大了一些

在我运行生成的例子中,rust 生成的是 330 字节, tinygo 生成的 56KB.

rust 工具链完善,生成的 js 文件也更加工程化.用来和前端项目做集成会方便一些.

wasm 在浏览器里运行,有内存的限制.wasm 引擎预先分配一段内存,被称为线性内存. 在运行过程中,wasm 只能访问这段内存.着相当于一个虚拟的空间,在写程序时要时刻考虑内存占用的问题.

wasm 虽然是为浏览器出现的,但是它体现出的跨平台的特性,可能会成为一种基础底层的能力.

wasm 的出现,让浏览器作为载体,可能会出现真正的基于云的操作系统.

运行时场景思考

  • 从运行时的角度看

wasm 的运行时场景,有一些像 java 或 C# 的运行时, 比如在编译的时候会编译成为字节码,然后字节码在放到运行时里去运行.

wasm 的运行时就是 java 的 JRE 或者 c# 的 CLR . 可以通过任何的语言编译成为 wasm 后运行.java、c# 跨平台的特性变成的所有语言的特性. 可以想象 java 能做的事情, wasm 都能做.

  • 从容器的角度看

在看一些技术文章时,也有从容器的角度看看待 wasm , 假设运行时是一个安全的容器.那么 wasm 还欠缺什么呢.

程序运行时需要的资源有两种

  1. 计算资源:指的是 cpu 或者 gpu

    如何将计算资源标准化后供提供到 wasm 里面. 或者说 wasm 的运行时,如何提供底层计算资源的分配和限制.

  2. 存储资源:指的是内存

    wasm 提供了线性内存分配方式, 在 web 中是没有问题的, 但是在服务端作为密集运算的时候,可能会存在性能的问题.

  3. 输入输出端

    输入输出对于计算机语言已经有一个很好的虚拟,所有的语言都会提供一个 io 的基础库来负责输入输出. wasm 的运行时需要做一些输入输出的标准化的工作. 比如网卡(虚拟网络)、硬盘(虚拟文件)、键盘(虚拟输入输出)等等.

bookmark

在看各种运行时时,有些运行时已经做了上述的事情,比如 wasmedge 运行时为操作系统资源(例如文件系统、套接字、环境变量、进程)和内存空间提供隔离和保护.

从支持的语言看 wasmer 更多一些. wasmtime 作为字节码联盟,的到了一些大公司的支持, wasmedge 是 CNCF 的项目. 从资源的角度来看,可能 wasmtime 的发展会更好一些.

其他资源

https://wasmbyexample.dev/home.en-us.html 一些例子

https://developer.mozilla.org/zh-CN/docs/WebAssembly

https://wazero.io/ 使用 go 实现的 wasm 运行时

https://wasmtime.dev/ rust 实现的运行时,字节码联盟

https://wasmedge.org/ 运行时, CNCF 项目,

https://github.com/wasmerio/wasmer rust运行时

https://github.com/wapm-packages

机器学习记录

机器学习记录

不同的维度,看到的事物的形态也是不同的. 横看成岭侧成峰,远近高低各不同.

最简单的公式

$$
f(x)=XW+b
$$

有一堆数据,能够知道 x 和 y .现在我们通过一些方法来求出 Wb ,这样当有未知的 $X_n$ 通过 W 和 b 就能够求出 $f(x_n)$ 的值.

当把维度增加,公式就变成了

$$
f(x_1..xn)=X_1W_1+…X_nW_n+b
$$

https://microsoft.github.io/ai-edu/基础教程/

反向传播:

https://www.jiqizhixin.com/graph/technologies/7332347c-8073-4783-bfc1-1698a6257db3

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

正向传播: 传播信号从输入层到隐藏层到输出层,一层一层的传播,最后得到结果.

反向传播: 输出结果和真实结果存在误差,通过误差反向的传递给前面的个层,来调整网络的参数.

$$
f(x) = ax_1+bx_2
$$

如果 f(x) = z 而我们计算的值为y 那么误差为 m=z-y,那么我们需要通过误差来调整参数 a 、b 的值. 当误差值不断往前传播,最后通过误差计算出新的权重的过程.

https://cloud.tencent.com/developer/article/1897045

CNN: 卷积神经网络

http://arthurchiao.art/blog/cnn-intuitive-explanation-zh/

卷积的四种操作

  • 卷积 : 通过小矩阵对输入矩阵进行运算,学习图像特征,通过 ilter 保留像素空间关系.

Untitled.png

  • 非线性:卷积后通过一个称为 ReLU 的运算
  • 池化或降采样: 对卷积+ ReLU 的特征做降采样,比如44 降为22

Untitled.png

  • 分类/全连接

    Untitled.png

比如上图做两次卷积后,在做全连接.

卷积+降采样作为特征提取,全连接作为分类器

RNN 循环神经网络

http://fancyerii.github.io/books/rnn-intro/

Untitled.png

$x_t$ 为 t 时刻的输入

$s_t$ 为 t 时刻的隐状态,它可以看作是网络的记忆

$s_t = f(Ux_t+Ws_{t-1})$

使用上一层的输出和当前的输入作为当前的输入.

上一层的输入是前面权重计算,层数越多,前面层的影响越小(权重会越来越小),存在短期记忆的问题.

另外的

  • 双向 RNN
  • 深度双向 RNN
  • LSTM
  • GRU

transformer

https://cloud.tencent.com/developer/article/1897045

Transformer的优势在于,它可以不受梯度消失的影响,能够保留任意长的长期记忆。而RNN的记忆窗口很短;LSTM和GRU虽然解决了一部分梯度消失的问题,但是它们的记忆窗口也是有限的。

encoder-decoder 结构

https://luweikxy.gitbook.io/machine-learning-notes/seq2seq-and-attention-mechanism

给定的输入 encoder 后计算得到中间语义,使用 decoder 解码.

Untitled.png

attention 注意力机制

不在将输入编码成固定长度 .而是根据当前生成的新单词计算新的$C_i$

Untitled.png

transformer结构

https://www.tensorflow.org/tutorials/text/transformer?hl=zh-cn

https://erickun.com/2020/04/11/Transformer-原理-源码分析总结-Tensorflow官方源码/

Untitled.png

Transformer 结构分为编码器和解码器两部分. 编码器有 N 个层,解码器也有 N 个层.

Encoding

  • Positional Encoding

    https://www.cnblogs.com/emanlee/p/17137698.html

    transformer 是将所有词一起输入,并行操作,所以需要提供位置信息.

    位置嵌入的维度为[max sequence length ,embedding dimension] (输入的最大单句长度 、 词的维度)

    Untitled.png

    在论文中使用了 sin 和cos 函数的线性变换提供了模型的位置信息

    Untitled.png

    d:输出嵌入空间的维度

    pos:输入序列中的单词位置,0≤pos≤L-1

    这个公式的意义是,对于每个位置 pos 和每个维度 ,计算出对应的角度速率(angle_rate),用于位置编码中的计算。这样可以保证不同位置和不同维度的编码具有不同的特征,有助于模型更好地学习序列的位置关系。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def get_angles(pos,i,d_model,n):
    # pos/10000^(2*i/d_model)
    denominator = np.power(n, 2*(i/d_model))
    return pos/denominator

    def getPositionEncoding(seq_len,dim,n = 10000):
    PE = np.zeros((seq_len, dim))
    for pos in range(seq_len):
    for i in range(int(dim/2)):
    PE[pos,2*i] = np.sin(get_angles(pos,i,dim,n))
    PE[pos,2*i+1] = np.cos(get_angles(pos,i,dim,n))
    return PE

    疑问:

    使用 sin cos 交替的意义,奇数位和偶数位的位置描述不同? 如果直接使用随机数可行吗?

    2*i/d_model 在值域上的变化为 0-1-2,中值为1 ,对应的指数函数的变化不同,表

    示前半段的位置信息和后半段的位置信息不同?

在小说降临里面,对语言的描述是立体的,电影里面是一个环,而信息在环上延伸.最后学会了七支桶的语言,从而能够预测未来. 环描述了位置,也描述了信息.位置信息可能是相对的.

Multi- HeadAttention

https://imzhanghao.com/2021/09/15/self-attention-multi-head-attention/

https://www.cvmart.net/community/detail/5563

https://mp.weixin.qq.com/s/cJqhESxTMy5cfj0EXh9s4w

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

  • self Attention 自注意力机制

    Untitled.png

    1.初始化Q,K,V

    首先初始化三个权重矩阵$ W_Q 、 W_K、 W_V$ 然后将 X_embedding 与这三个权重矩阵相乘,得到 Q、K、V

    Untitled.png

    1. 计算 Self-Attention Score

    Untitled.png

    1. 对Self-Attention Socre进行缩放和归一化,得到Softmax Socre

    Untitled.png

    1. Softmax Socre乘以Value向量,求和得到Attention Value

Untitled.png

Multi-Head Attention的作用

将 Scaled Dot-Product Attention 的过程做 H 次(H=8)

把输出合并起来

$$
head_i = Attention(Q_i,K_i,V_i),i=1…8 \ MultiHead(Q,K,V) = Concact(head_1,…head_8)W^o
$$

Untitled.png

  • Add&Norm

    Add & Norm 由 Add 和 Norm 两个部分组成

    $$
    LayerNorm(X+MultiHeadAttention(X)) \
    LayerNorm(X+FeedForward(X))
    $$

    X 表示 MultiHeadAttention 或 FeedForward 的输入

    ADD 指的是 X + MultiHeadAttention(X) ,是一种残差链接,让网络指关注当前差异的部分.

Untitled.png

LayerNorm

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

对维度进行均值方差计算,LN 对 hidden 的维度做归一化操作.

Untitled.png

  • Feed Forward

    Feed Forward 是两个全连接层 ,第一层激活函数为 Relu ,第二层不使用激活函数

    $$
    max(0,XW_1+b_1)W_2+b_2
    $$

    X 是输入,Feed Forward 最终的到输出矩阵的维度与 X 一致.

encode

Untitled.png

Untitled.png

encode 的部分由 Multi-Head Attention , Add & Norm, Feed Forward, Add & Norm

decode

Untitled.png

解码器由三层结构组成

  • 第一层由带掩码(masked)的多头注意力层和一个残差连接

    Masked 操作目的是将后面的内容做掩盖,因为在生成过程中,生成了第i个字,才能生成第 i+1 个. 通过 masked 可以防止第i个字知道i+1的内容.

Untitled.png

Untitled.png

Untitled.png

Untitled.png

通过 masked 后的输出内容只有前 i 个字符的信息.

  • 第二层也是一个多头注意力层和一个残差连接

根据 Encoder 的输出 C 计算得到 K,V ,根据上一次输出的 Z 计算出 Q.进行多头注意力计算和残差连接. (通过掩码的 Q 去 Encode 的 K V 里查询出可能的内容. )

  • 第三层是前溃全连接层和一个残差连接.

计算方式与 encode 中的一致

最后线性层和 Softmax层

最后通过 Softmax 预测所有的单词

Untitled.png

链接

google develop 的llm简介: https://developers.google.com/machine-learning/resources/intro-llms?hl=zh-cn

用 Rust 求子串绝对值最大

用 Rust 求子串绝对值最大

这两天学习了 rust 的语法

bookmark

尝试使用 rust 来写今天的 leetcode 的每日一题

https://leetcode.cn/problems/maximum-absolute-sum-of-any-subarray

给你一个整数数组 nums 。一个子数组 [numsl, numsl+1, ..., numsr-1, numsr] 的 和的绝对值 为 abs(numsl + numsl+1 + ... + numsr-1 + numsr) 。

请你找出 nums 中 和的绝对值 最大的任意子数组(可能为空),并返回该 最大值 。

边查文档边写的代码:

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
impl Solution {

pub fn max_abs(a:i32,b:i32) -> i32 {

if a <0{
a= -a
}
if b<0{
b= -b
}

if a > b {
a
}else{
b
}
}

pub fn max_absolute_sum(nums: Vec<i32>) -> i32 {
let mut res = 0;

let mut dp_max = vec![0;nums.length()];
let mut dp_min = vec![0;nums.length()];

for (i,num) in nums.iter().enumerate() {
if i == 0 {
dp_max[0] = *num
dp_min[0] = *num
res = Solution::max_abs(dp_max[0],dp_min[0])
}else{
let max_num = dp_max[i-1] + *num
if max_num > *num{
dp_max[i] = max_num
}else{
dp_max[i] =*num
}

let min_num = dp_min[i-1] + *num
if min_num < *num{
dp_min[i] = min_num
}else{
dp_min[i] = *num
}

max_num = Solution::max_abs(dp_max[i],dp_min[i])
res = Solution::max_abs(max_num,res)
}
}
res
}
}

编译错误提示

  • 忘记写 ;
1
2
3
4
5
6
7
error: expected `;`, found `dp_min`
--> src/main.rs:60:33
|
60 | dp_max[0] = *num
| ^ help: add `;` here
61 | dp_min[0] = *num
| ------ unexpected token
  • 获取数组长度 len 写成 length
1
2
|         let mut dp_max = vec![0;nums.length()];
| ^^^^^^ help: there is a method with a similar name: `len`
  • 未定义的结构体
1
2
34 | impl Solution {
| ^^^^^^^^ not found in this scope
  • 最后的返回没有分号
1
2
3
4
5
6
7
pub fn max_absolute_sum(nums: Vec<i32>) -> i32 {
| ---------------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
...
87 | res;
| - help: remove this semicolon to return this value
  • 计算最大绝对值函数的所有权
1
2
3
4
5
6
7
pub fn max_absolute_sum(nums: Vec<i32>) -> i32 {
| ---------------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
...
87 | res;
| - help: remove this semicolon to return this value
  • 所有权只读
1
2
3
4
5
6
7
8
70 |                 let max_num = dp_max[i-1] + *num;
| -------
| |
| first assignment to `max_num`
| help: consider making this binding mutable: `mut max_num`
...
84 | max_num = Solution::max_abs(dp_max[i],dp_min[i]);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable

修改的结果

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
struct Solution{

}


impl Solution {

pub fn max_abs(a:i32,b:i32) -> i32 {
let mut a = a ;
let mut b = b ;
if a <0{
a= -a;
}
if b<0{
b= -b;
}

if a > b {
a
}else{
b
}
}

pub fn max_absolute_sum(nums: Vec<i32>) -> i32 {
let mut res = 0;

let mut dp_max = vec![0;nums.len()];
let mut dp_min = vec![0;nums.len()];

for (i,num) in nums.iter().enumerate() {
if i == 0 {
dp_max[0] = *num;
dp_min[0] = *num;
res = Solution::max_abs(dp_max[0],dp_min[0])
}else{
let mut max_num = dp_max[i-1] + *num;
if max_num > *num{
dp_max[i] = max_num;
}else{
dp_max[i] =*num;
}

let min_num = dp_min[i-1] + *num;
if min_num < *num{
dp_min[i] = min_num;
}else{
dp_min[i] = *num;
}

max_num = Solution::max_abs(dp_max[i],dp_min[i]);
res = Solution::max_abs(max_num,res);
}
}
res
}
}


fn main(){
Solution::max_absolute_sum(vec![1,-3,2,3,-4]);
}

总结

参看其他人写的代码

  • 直接使用 for i in 0..n 要比 nums.iter().enumerate() 要好一些
  • 数值型函数本身有 max min abs 函数,不需要自己写.
  • 计算时由于过程已经保存 dp 的结果也不用保存,只需要保存上一次的结果给下一次循环用即可.
  • rust 的语法确实很难描述,需要大量的实践,特别是开始写的时候变量的传递有些懵.

最终版本:

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

//f(n) 绝对值最大 = n 的 最大值和最小值的绝对值取大
// f(n) 最大值 = f(n-1) 的最大值+n 和 n 取大
// f(n) 最小值 = f(n-1) 的最小值+n 和 n 取小

impl Solution {
pub fn max_absolute_sum(nums: Vec<i32>) -> i32 {
let n = nums.len();
let mut res = 0;

let mut dp_max = 0;
let mut dp_min = 0;

for i in 0..n {
if i == 0 {
dp_max = nums[0];
dp_min = nums[0];
res = dp_max.abs().max(dp_min.abs());
}else{

let max_num = dp_max + nums[i];
dp_max = max_num.max(nums[i]);

let min_num = dp_min + nums[i];
dp_min = min_num.min(nums[i]);

res = res.max(dp_max.abs().max(dp_min.abs()));

}
}
res
}
}

Rust 小记

编程语言是用出来的, rust 文档已经看过几遍,但是依然不得要领. 通过和golang 做类比,以下是一些记录

变量默认不可变,变量可隐藏 .

let 不可变 ,let mut 可变,相比其他语言默认为可变 . 另外就是变了可以通过重新定义的方式.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义一个不可变的变量
let x = 5;
// 下面的代码将会报错,因为x是不可变的
x = 10;

// 定义一个可变的变量
let mut y = 5;
// 修改y的值
y = 10;

// 重新定义一个变量z
let z = 5;
let z = z + 10; // z现在的值为15,但是它是一个新的变量

数据类型

整型、浮点型、布尔类型和字符类是基础类型,字符串切片、结构体 和 golang 大体一致.

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
// 整型类型
let x: i32 = 5;
println!("x = {}", x);
// 浮点型类型
let y: f64 = 3.14;
println!("y = {}", y);
// 布尔类型
let is_rust_fun: bool = true;
println!("is_rust_fun = {}", is_rust_fun);
// 字符类型
let c: char = 'A';
println!("c = {}", c);
// 字符串切片类型
let s: &str = "hello, world";
println!("s = {}", s);


// 结构体类型
// 定义一个结构体
struct Person {
name: String,
age: u8,
is_male: bool,
}
let person = Person {
name: String::from("Tom"),
age: 18,
is_male: true,
};

元组看使用示例是作为返回场景,golang 的多值返回一致. 其他地方的使用和结构体区别?写法的区别?

元组可以作为函数的返回值或者作为变量的值进行赋值。元组和结构体的区别在于元组的成员没有命名,而结构体的成员是有命名的。

在使用元组时需要注意,如果元组的成员是可变的,那么整个元组也是可变的。如果需要保证元组的成员不可变,可以使用 & 符号将元组的引用作为函数的返回值。

在同组值的时候可以很方便,比如表示坐标 Point(x,y)

看上去元组只时结构体的一种特殊表示.

函数返回值为表达式. 最后一个表达式的值作为函数的返回值.

如何提前返回?

rust 也有 return 关键字, 用于提前返回 ,那最后省略 return 的用意时什么?

在 Rust 中,函数的最后一个表达式的值将自动成为函数的返回值,因此可以省略 return 关键字。

1
2
3
4
5
6
7
8
// 使用 `return` 关键字
fn add_one(x: i32) -> i32 {
return x + 1;
}
// 省略 `return` 关键字
fn add_one(x: i32) -> i32 {
x + 1
}

循环

包括了 loop while for ,和 c 语言差不多.相比来说 golang 直接使用 for 来表示循环要简洁一些.

所有权

rust 的每个值都有一个所有权, 那么 = 的意思在其他语言里面的意思是: 把某值给予某变量. rust 里的意思是,把某值的拥有权交给谁.

前者的意思是,把东西放到你的仓库

后者的意思是给你一把仓库的钥匙,而且这个钥匙只有一把(只能有一个所有者,等你离开这里,这把钥匙就销毁,而且仓库也同时清理. (离开作用域,值被丢弃)

  • 值传递给函数时,所有权会交给函数.
  • println! 宏传递的是引用,不会转移所有权

bookmark

引用与借用

引用允许在没有所有权的情况下访问变量地址. 有点像 c 语言的指针的概念.

引用能对变量做什么?

引用的变量不可变,所以尝试修改会报错.可以使用 &mut 来定义可变引用 ,这里称之为借用.

借用: 不能在同一时间多次将变量作为可变变量借用. 也就是借用只能同一时间借给一个人,再次使用,只能等前一个还回来.(钥匙只有一把,只能借一个人)

在借用的同时,不能在做引用, 不可变的引用不希望变量被意外改变.

  • 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
  • 引用必须总是有效的。

枚举

在 golang 里面通常使用常变量的方式来定义一组变量,作为枚举值. rest 不久提供了枚举,还提供了一个通用枚举 Option

从后面的错误处理上看,rust 的枚举并不是单纯的枚举,而是通过枚举来设计了语言特性,比如 match 、错误处理

vector 和 array

vector 和 array 的区别?

vector 的大小不固定,可以动态的扩容. 区别是 vecter 是分配在栈上面.

  1. 大小可变性:Array 的大小是固定的,一旦定义就不能再改变。而 Vector 的大小是可变的,可以在程序运行时动态地添加或删除元素。
  2. 分配方式:Array 在栈上分配,而 Vector 在堆上分配。因为 Vector 的大小是可变的,所以需要在堆上分配内存。而 Array 的大小是固定的,所以可以在栈上分配内存,这样可以更快地访问元素。
  3. 索引访问:Array 的元素可以使用下标直接访问,比如 arr[0]。而 Vector 的元素也可以使用下标访问,但是需要使用 get 方法,比如 vec.get(0)。这是因为 Vector 的大小是可变的,有可能访问不存在的元素,所以需要在访问时进行一些检查。
  4. 用途:Array 适用于大小固定的情况,比如存储一组固定长度的数据。而 Vector 适用于大小不确定的情况,比如读取文件的内容、网络传输等场景。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义一个数组
let arr = [1, 2, 3, 4];
// 定义一个向量
let mut vec = vec![1, 2, 3, 4];
// 访问数组的元素
println!("The first element of array is {}", arr[0]);
// 访问向量的元素
println!("The first element of vector is {}", vec[0]);
// 向向量中添加元素
vec.push(5);
// 计算数组的长度
println!("The length of array is {}", arr.len());
// 计算向量的长度
println!("The length of vector is {}", vec.len());

hashmap

hash map 用法和 golang 基本一样,需要注意一下所有权.

错误处理

rest 使用 Result<T,E> 的枚举来返回是否出现了错误,等同 golang 里面的返回 res,err := function(xxx)

Result<T,E> 定义了一些辅助方法来处理一些情况,比如

1
let greeting_file = File::open("hello.txt").unwrap();

unwrap 成功返回,失败直接 panic

expect 可以自定义错误信息.

可恢复错误: 当出现错误时,不是所有的错误都需要 panic. 所以可以将错误向上传播,使用 ? 来作为向上传播的标识符, ? 实际是一个宏,用来做 match 返回.

1
2
3
4
5
6
let mut f = match f {
// 打开文件成功,将file句柄赋值给f
Ok(file) => file,
// 打开文件失败,将错误返回(向上传播)
Err(e) => return Err(e),
};

? 给我的感觉是 golang 的 err ≠ nil 的方式相比不够优雅. 不过这都是建立在返回是枚举的基础上.

trait 类似于 golang 的 interface

(rust 的命名如果能和其他语言保持一致的话,可能更加好学一些吧,比如 match 和 switch )

智能指针

智能指针 像是为了解决对应引用问题定义的类型?

从文档来看是来源于 c++ 的概念

  • Box<T>,用于在堆上分配值

    • 解引用 Deref
    • 释放资源 Drop

    解引用可以将智能指针当普通指针来使用, 通过 deref 将引用返回

    释放资源在值离开作用域时调用(手动回收堆上的数据?)

  • Rc<T>,一个引用计数类型,其数据可以有多个所有者

    • 对一个Rc变量进行clone()时,不会将其内部的数据复制,只会增加引用计数。
    • 当一个Rc变量离开作用域被drop()时,只会减少引用计数,直到引用计数为零时,才会真正清除其拥有数据的堆内存。
    • Arc<T> : Atomic Rc 原子化的 Rc<T>,相比来说,带来线程安全和性能损耗
  • Ref<T> 和 RefMut<T>,通过 RefCell<T> 访问。( RefCell<T> 是一个在运行时而不是在编译时执行借用规则的类型)。

    • Cell 只适用于 Copy 类型,用于提供值,而 RefCell 用于提供引用
    • Cell 不会 panic,而 RefCell 会

并发

rust 提供了线程 、锁,提供了像 golang channel 的类型

async 提供了类似 golang 协程的概念.

总结

这里只是对rust 做了片面的理解, rust 所描述的没有垃圾回收,实际是任何时候,都明确变量在哪个位置被回收

定义好的变量在堆上还是在栈上也是确定的,所以回收的时机也是明确的.

所以不需要垃圾回收算法

rust 的设计上处处都体现着与其他语言的不同,变量、描述、定义,从语法上吸收了 c++ 的语法, 但依然过于复杂. 比如很多约定俗成的描述,依然换了一种描述方式,比如 match 而不是用 switch .

参考:

bookmark

bookmark

Test::Nginx 使用

https://github.com/openresty/test-nginx

https://openresty.gitbooks.io/programming-openresty/content/testing/test-nginx.html

Test::Nginx 使用 Perl 便携的 nginx 测试组件

安装

1
cpan Test::Nginx

测试布局

按照惯例,会在项目的根目录创建一个 t/ 的目录来作为测试文件的目录. 当有很多测试文件时,可以在 t/ 中进一步的分组 ,比如 t/001-test/

本质上每个 .t 文件都是一个 Perl 脚本文件 ,可以使用 Prove (perl 的通用测试工具)来运行

比如 prove t/001.t

测试文件布局

这里有一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use Test::Nginx::Socket 'no_plan';

run_tests;

__DATA__

=== TEST 1: max pool size is 200
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
ngx.print("hello world")
}
}

--- request
GET /t

--- response_body chomp
hello world

测试文件通常以 .t 作为扩展名 ,每个测试文件本身就是一个 Perl 脚本.

每个测试文件分为两个部分,两个部分使用 __DATA__ 来分隔

  • 第一部分是简短的 Perl 代码
1
2
3
4
# 调用对应的测试模块 一般使用 'no_plan' 即可
use Test::Nginx::Socket 'no_plan';
# 运行测试用例
run_tests;
  • 第二部分是测试用例

测试用例格式 https://openresty.gitbooks.io/programming-openresty/content/testing/test-file-layout.html

数据块通过三个横线来设置某种数据块,比如如下的四种类型:

1
2
3
4
5
6
7
8
9
10
11
12
--- config
location = /t {
echo "hello, world!";
}

--- request
GET /t

--- response_body
hello, world!

--- error_code: 200

数据块的过滤器

在类型后可以跟一个和多个过滤器

1
2
3
4
5
6
7
# chomp : 去掉换行符
--- error_code chomp
200

# eval: 执行一个 Perl 代码 :以下是生产 4096 个 a 字符串的body .
--- response_body eval
"a" x 4096

运行

Test::Nginx 依赖 Perl 的 prove 命令来运行测试用例.

运行时 Test::Nginx 会调用 nginx 服务器和 socket 客户端来运行测试,它会自动在系统环境的 Path 中找到 nginx . 所有需要在环境中指定 nginx 的程序路径

1
export PATH=/usr/local/openresty/nginx/sbin:$PATH

可以在程序的根目录添加一个 go.sh 的文件

1
2
3
4
5
6
7
8
#!/usr/bin/env bash

export PATH=/usr/local/Cellar/openresty/1.21.4.2_1/nginx/sbin:$PATH

# 部分系统perl环境会提示(Can't locate t/.pm in @INC)
export PERL5LIB=$(pwd):$PERL5LIB

exec prove "$@"

然后使用这个脚本来做测试 ,比如测试 t/aa.t 这个文件.

1
2
3
4
5
$ ./go.sh t/aa.t 
t/aa.t .. ok
All tests successful.
Files=1, Tests=2, 0 wallclock secs ( 0.02 usr 0.01 sys + 0.19 cusr 0.10 csys = 0.32 CPU)
Result: PASS

可以通过 -v 的选项来生成详细的测试报告

prove -v t/aa.t

运行多个文件

prove -v t/foo.t t/bar.t t/baz.t

通配符运行多个文件

prove -v t/*.t

递归运行目录 -r

prove -r t/

测试单个用例,只需要在用例中为单个用例添加 --- ONLY

测试文件运行的顺序

测试文件通常按照字母顺序运行, 可以通过在测试文件名前面添加数字序列来控制运行的顺序. 例如

1
2
3
4
5
6
t/000-sanity.t
t/001-set.t
t/002-content.t
t/003-errors.t
...
t/139-ssl-cert-by.t

虽然 Prove 可以通过 -jN 来支持作业并行运行,但是 Test::Nginx 并真正不支持这种模式.

测试块运行的顺序

测试块会打乱每个文件中的测试块 ,可以通过加入 no_shuffle() 来禁用这种行为

1
2
3
4
5
6
7
use Test::Nginx::Socket 'no_plan';

no_shuffle();
run_tests();

__DATA__
...

测试组件

Test::Nginx::Socket::Lua

用于 ngx_lua 相关的测试框架

https://metacpan.org/pod/Test%3A%3ANginx%3A%3ASocket%3A%3ALua

通过环境变了 TEST_NGINX_INIT_BY_LUA 导入 ngx_lua module

1
export TEST_NGINX_INIT_BY_LUA="package.path = '$PWD/../lua-resty-core/lib/?.lua;' .. (package.path or '') require 'resty.core'"

example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use Test::Nginx::Socket::Lua;

repeat_each(2);
plan tests => repeat_each() * 3 * blocks();

no_shuffle();
run_tests();

__DATA__

=== TEST 1: sanity
--- config
location = /t {
content_by_lua '
ngx.say("hello world")
';
}
--- request
GET /t
--- response_body
hello world
--- error_code: 200
--- no_error_log
[error]

Test::Nginx::Socket::Lua::Stream

测试 stream

example

1
2
3
4
5
6
stream {
server {
listen 1985;
echo "Hello, stream echo!";
}
}

Test::Nginx::Socket::Lua::Dgram

测试 upd stream

example

1
2
3
4
5
6
stream {
server {
listen 1985 udp;
echo "Hello, stream echo!";
}
}
微服务还是单体服务?

微服务还是单体服务?

前两天看到了腾讯云开发者的账号发布以一篇 QQ 浏览器服务的优化.

bookmark

在做架构设计时将微服务变成单体服务. 我在下面做了评价.

很多做设计的没有意识到 rpc 也是需要时间的,无限度的搞微服务,复杂性和网络调用链导致问题排查不下去. 一个单体应用加个缓存的事情,最后搞成一堆服务相互依赖调用,浪费机器、浪费人力.

在做架构拆分,很容易忘记初衷是什么,而是为了架构而架构.

在做微服务的时候,基本都是目前架构冗余,业务杂糅在一起,扩容困难. 或者是数据库表太多,大量的表关联查询缓慢,所以要做微服务架构的改造,跟随一起的还有,服务要跟着一起上容器.

单体应用与微服务并不冲突

做微服务拆分,但不要拆得那么细,比如有些过分的拆分恨不得 tab 就成为一个微服务.

本来一个很简单的业务,一个微服务/单体应用就够了, 非得拆出好几个来,比如一个业务有几个任务,业务把每个任务都拆成一个服务,可每个服务的80%的代码都是一样的,甚至数据都是一个数据库来源. 没有这个必要嘛. 而且过多的拆分会导致业务代码的割裂.

grpc 很快,但是也需要时间

grpc 使用 http2 协议,相比 http 协议对比,长链接和流要快很多,但是还是需要花时间的,每多一次的 grpc 的交互,网络请求会多出 1-2ms 的时间.

有些给前端的页面渲染拆成了多个模块,当想要把所有数据一次渲染出来.内部可能需要做几次甚至是十几次的 rpc 调用. 导致在网络上的时间就超过了 20ms. 还是不考虑高负载和网络抖动的情况下.

当 rpc 出现错误重试的成本要远远高于单体应用出错重试.

让缓存前置,让请求后置

当对服务进行拆分,有时候会出现这样的情况,每个微服务配套一套缓存和 DB.通常我们在内网微服务通信,然后通过 http 来提供外网服务.如果可能的话,可以尝试让对外的服务做前置的缓存.

不要过早的优化,容器并不是银弹

容器也是有损耗的, 使用容器会带来物理机大概 5% 的性能损耗,如果业务本身很稳定,也不是高并发的系统,没有什么扩容的需求,要不还是用物理机吧.

容器带来的无状态服务,扩容等让运维变得简单,但是同时也带来了查问题的成本变高,全链路的监控和运维的成本,对于小团队来说几乎是个黑洞.

做架构升级不要为了兼容旧的东西做妥协

在做架构升级会碰到很多的技术债,为了兼容可能是错误的技术债,不得不为此妥协,当妥协的内容越来越多的时候,会发现架构升级做了个寂寞. (这个和微服务没关系)

架构的目的,不是为了炫耀新技术,而是为了稳定,作为一个技术人员,要最求新技术,善用老技术.