数据结构与 方法(增删改查)
-
‘go’ 不是内部或外部命令,也不是可运行的程序
或批处理文件。
在 Windows 搜索栏中输入“环境变量”,然后选择“编辑系统环境变量”或“编辑环境变量”。
在系统属性窗口中,点击“环境变量”按钮。
在“系统变量”区域,找到并选择“Path”变量,然后点击“编辑”。
在弹出的窗口中,点击“新建”,然后输入 Go 的 bin 目录的路径,例如 C:\Go\bin。
点击“确定”保存更改。
-
枚举用 iota自动就依次排号变成枚举了,不用你赋值了。枚举就是变量代替正整数
有类型的枚举的话,就不能 float64+ int了,因为有严格的类型校验,没指明类型的可以 -
字符串拼接可以用 +
数据不可变,它的值改变不了
字符串不可变: 意味着你不能改变字符串内部的单个字符。
变量可变: 变量本身可以被重新赋值,指向另一个字符串。
var newName string
newName = name + " " + "day"
fmt.Println(newName)
newName += "s"
fmt.Println(newName)
从行为上看,newName 的内容变了,但这是因为你指向了一个全新的字符串。在 Go 中,这种操作不是修改原有字符串,而是创建了一个新的字符串,并让 newName 指向这个新的字符串。原有的字符串 “lucky day” 仍然存在于内存中,直到 Go 的垃圾收集器决定回收它。
- 结构体就是复合类型的聚合
指针,如果p = &s(结构体) ,通过取地址运算符取到指针, 那么p就能像 s一样点出来所有内含的字段 - 数组
长度固定,存储同一类型的数据。数组是值传递的,不是引用传递,每次作为参数都是值传递,数组太大,复制会引起性能损耗。解决方案就是使用切片
切片 【指针,len, cap容量 cap>=len】
可以动态改变长度(每次大于容量再append元素,容量是翻倍的涨,比如1,2,4,8),一旦扩容,切片就会和原数组解绑,然后绑定到一个新数组上。频繁扩容耗费性能。所以初始化的时候,len可以设置为0,但是容量最好设置一个预估的值
len就是切片里元素的个数,
切片是对底层数组的描述的意思是当改动切片中的某个值,从切片用中括号截取的切片对应的值也会改变,它从根上就变化了。
容量cap就是你把原切片切出一部分,容量就是原容量减去中括号前面切去的就是现在的容量
- golang是编译语言,python是脚本语言
脚本语言:适用于快速开发、自动化任务、Web开发和应用程序扩展。它们的价值在于提高开发效率和灵活性。
编译语言:适用于高性能应用、系统编程、大型企业应用和安全性要求高的场景。它们的价值在于提供高效的执行速度和对底层系统的控制。
-
make 只能用于内置的切片、映射和通道类型,不能用于用户定义的类型或其他内置类型(如整型、浮点型等)。对于这些类型,你需要使用其他方式(如直接初始化或使用 new 函数)来创建和初始化变量。
-
map 哈希表或者字典,k-v。
遍历map的时候是无序的。map也是有容量的,如果频繁扩容也会造成性能消耗 -
切片(slice)、映射(map)和通道(channel)是引用类型,其他的是值类型
-
这里num的作用域就是if 函数内。err经常用这一招
-
for循环重复执行一段代码,直到不满足条件为止
for range用来遍历 切片 map 数组,string 等用的,下面是range和传统形式两种
[]string{“lucky”,“nike”,“allen”}中括号里没东西就是切片,有数值就是数组
-
包就是import那个,
package中 首字母大写的函数是可以被导出的,首字母小写的就是内部使用的
构建就是 go build xx.go,xx.go就是你的那个package xx脚本。然后就可以在命令行直接执行 ./xx 跟 go run xx.go一样的效果
如果有两个errors包(不需要go build的,脚本形式就行),只需要前面加上来源,比如github.com/pkg/errors,并且给它取个别名来解决包冲突
包名一般跟顶层文件夹名保持一致
交叉编译(Cross-Compilation)是指在一种平台上编译生成能够在另一种平台上运行的程序。也就是说,编译代码的机器和运行该代码的目标机器使用不同的操作系统或硬件架构。交叉编译通常用于开发需要在多个平台(如 Linux、Windows、macOS,或不同的处理器架构如 ARM、x86)上运行的程序。
Go 语言内置了对交叉编译的强大支持。Go 的编译器允许开发者通过设置两个环境变量GOOS
(目标操作系统)和GOARCH
(目标硬件架构)来进行交叉编译。
例如,想要在 Linux 上编译一个能在 Windows 运行的程序,开发者可以这样做:
GOOS=windows GOARCH=amd64 go build -o myapp.exe
同理,想要编译一个能在 ARM 架构的 Linux 设备上运行的 Go 程序,可以设置:
GOOS=linux GOARCH=arm64 go build -o myapp
-
函数
func init(){}
func 函数名(参数)(返回值){
函数体
defer 函数:当前函数结束时候调用 //主要用于释放资源。因为中间函数发生错误,return掉了,这时就没法执行关闭资源的代码,这时候defer就能派上用场了。比如ioutil.ReadAll(file)的时候,需要 file.Close()
} -
方法
相较于函数,多了一个 接收者 receiver,用来连接方法与类型,比如结构体的方法,不能是基础类型,int,string这些
receiver参数可以是值类型(用于只读),也可以是指针类型(读写)。
-
接口,里面有没有实现的方法。我开放了我的系统,但你实现要符合我的规则,也就是接口
接口组合就是 接口套接口或者 结构体里面套接口 -
并发与并行
同时运行多个任务,交替进行,不一定在同一时刻都在运行的是并发。 都在同一时刻运行的是并行。并行需要多核cpu或者分布式计算系统
并行那么棒,为啥需要并发
1)并发可以让 I/O 操作等待的时间里去做其他事情(其他协程或者线程切进来工作)。比如在等朋友的时候,你还可以玩玩手机,不是干等。
2)防止死锁
协程
用了协程,干一件事用10ms,干5件事也是10ms
创建协程,使用WaitGroup 等待协程结束(防止协程还没执行,main函数先执行完了,就都结束了)
解决并发同时访问一个资源不安全的问题,用mutex.Lock() 互斥锁。访问的时候变成了串行。锁的位置要放准,锁太多执行效率会下降
- channel 两个协程通信,生产者 消费者模型
数据写进channel,然后从channel里面读出来
ch:=make(chan int, 3) 这个3就使 channel变成有缓冲的了,这样有空间就往里面写。否则只能写一个,读一个,变成串行了,有缓冲就并发,处理快得多
如果无缓冲,又没人读,就变成死锁,报错,解决就是用 select。它监听多个channel,谁有数据时,就执行对应 case 分支
收发都有select。
channel 还可以执行上报心跳
-
error 处理 可预期的、可以处理的错误
透明错误 – 什么问题报什么
哨兵错误 – 比如搜数据库没有这条数据,就写入,用户看不到的,后台就把这个错误解决了
自定义错误 – error 中包含 错误码,追踪 id 等等
行为错误 – 给错误分组。比如网络错误,连接错误。对应分组有相应的应对方式 -
panic 适用于 不可恢复的错误
二、project
- Gin web框架(启动个后端,监听8080端口),在GitHub上有 安装和简单示例
postman可以测试它的接口 - 路由
Gin提供了 增删改查(PUT PATCH POST…) 等 用于处理 HTTP
CMS 内容管理
对应/api/cms/各种路由,api会单独拎出来
main函数加载路由表,route对应的url,而 services对应的是url对应的处理方法
-
中间件 剥洋葱
在请求被处理前后做一些额外的操作
比如 session 鉴权,就是postman带个session id -
模型绑定与验证 自动转换
将json转换为我们在go语言中定义的结构体
结构体的字段与json中的字段一一映射。这个往往是请求和响应两个结构体成对出现的,和客户端有来有回 -
注册接口
后端定义POST 注册接口 + handle 处理逻辑
密码加密 避免暴力破解
1)生成随机盐值,纵使输入相同的密码,得到的哈希值也是不同的
2)内部多次哈希的迭代次数就是工作因子
3)这样通过密码 存储的盐值和工作因子,就能验证密码是否正确
database tool
goland自带的工具,连接数据库,操作数据库
user.sql里面包含着表要创建成什么样子,就是sql语句的合集
在 Go 语言中,DAO(Data Access Object)模式是一种设计模式,主要用于将应用程序与底层数据存储(例如数据库)的交互逻辑分离。DAO 层负责处理与数据库的所有操作,如 CRUD(创建、读取、更新、删除)等。
GORM
就是go 的 ORM (对象关系映射)。数据库操作用的
用结构体映射数据库中的表,元素等。跟前端那个映射类似
先连上表,再增删改查
分表用重写TableName
分页查询用链式调用。比如 db.Where().Offset().Limit().Find()
model里面就是结构体们
- 登录接口
登录成功后 返回session id,并且session id持久化
redis
github上有 go-redis
也可以通过命令行redis-cli 登进去看
redis存了两个,一个是 userid : sessionid. 另一个是session_auth:sessionid : time.Now().Unix(),这个是鉴权用的
session id
用 go get github.com/google/uuid 这个库生成 session id := uuid.New().String()
鉴权
带着sessionID去访问,如果session_auth还在的话,就能通过鉴权,不用输入用户名和密码,直接进去
- 内容库
goland编辑器右键自带单元测试。验证写得函数是否正确,省的每次还得开关整个服务
查询的时候涉及到翻页逻辑
比如查哪个id,作者,title,查找范围是第几页的前多少个数据,这是前端传过去的。后端就会返回回来具体内容
- 加工流
在 Go 语言中,DAG(Directed Acyclic Graph, 有向无环图) 是一种数据结构,用于表示具有方向性的、无环的图形。它常被用于表示依赖关系、任务调度、数据流等问题,特别适合在分布式系统、工作流调度、数据处理流水线等场景中。
GoFlow 是 Go 语言中的一个框架,它利用 DAG 来处理数据流问题。它的主要功能是通过 DAG 来定义和执行一系列具有依赖关系的任务,使得数据能够沿着这些任务进行流动,帮助开发者轻松管理复杂的依赖关系和任务调度。
go get github.com/s8sg/goflow
Input —> 累加 1到10 —> 累加 100到200 —> output。
redis用于工作流临时存储
上面是简单的,下面是个更复杂的加工流
做两次累加(它们是并行,异步执行的,不一定谁先),需要都完成之后,再执行聚合的逻辑。直接调现成的方法完成聚合
也提供了条件分支现成的方法
- 自己项目的dag加工流
thumbnail是缩略图
加工流和内容创建是如何结合起来的呢?
三、微服务
大应用拆分成一组小型、自治的微服务
服务用http,rpc 通信
中间件
划分了微服务,但也需要中间件在中间辅助
- 服务注册与发现。注册进去,别人想通信就能发现
在微服务架构中,服务实例是动态的,它们可能随时上线、下线、扩缩容,因此它们的 IP 和端口会经常变化。如果没有注册中心,其他服务将无法知道这些变化,导致通信失败。
注册中心可以定期检查服务实例的健康状况。如果某个服务实例宕机或不可用,注册中心会从服务列表中移除它,防止向其发送请求。- 负载均衡:资源调度均衡
- API 网关,入口,根据path分发给不同的后端服务
- 消息队列,我把任务发出去,不关心谁来处理。谁关心谁就去处理
- 分布式缓存,比如多个后端服务公用一个缓存,后端服务又在不同的机器上
- 日志与监控
上图说明
该架构图展示了一个通过负载均衡器进行流量管理的系统,涉及到网关、内容管理、内容加工、注册中心、分布式缓存、日志监控等多个模块。具体解释如下:
-
网关:最外层的组件,负责处理来自外部的请求。网关会将请求转发到内部的不同服务,通常用来做统一入口管理、负载均衡和安全策略。
-
负载均衡器:网关接收到请求后,通过负载均衡器将流量分发到多个实例,以提高系统的处理能力。这个模块负责根据流量和服务的健康状况将请求分发到不同的“工作节点”(work)。
-
内容管理和内容加工:
- 内容管理:负责管理内容的增删改查功能,通常用于处理用户请求中的内容操作。它会分发到不同的“工作节点”(work 1, work 2, work 3),这些节点可能是多个副本,处理并行任务来分担负载。
- 内容加工:可能是对内容进行一些预处理或转换的模块。同样会分发任务给多个工作节点,以处理大量并发任务。
-
注册登录:系统的用户登录注册模块,这个模块也有多个工作节点来提供服务,通常与内容管理交互。
-
注册中心:注册中心可能是服务发现的模块,允许不同服务在集群中找到彼此的位置,确保服务间通信的正常进行。它是整个系统的中枢,确保不同模块之间的依赖关系。
-
分布式缓存:用于缓存数据,降低数据库的访问压力,提高系统性能。缓存可以加快频繁读取的内容的访问速度,减少对后端数据库的依赖。
-
日志与监控:这是系统的监控模块,负责记录日志和系统运行情况,帮助开发者和运维人员了解系统的健康状况,进行故障排查和性能分析。
-
消息队列:消息队列位于系统的最右侧,用于在模块之间传递消息。消息队列可以确保消息异步处理,提供可靠性和削峰填谷功能,常用于解耦系统的不同部分。
整条链路的流程可以总结为:
用户请求通过网关进入系统,经过负载均衡器,将请求分发到不同的工作节点进行处理(注册登录、内容管理、内容加工等)。这些节点依赖注册中心进行服务发现,并且利用分布式缓存加快访问。处理完成后,通过消息队列进行异步通信和日志记录,同时监控系统的健康状况。
这样设计的系统具有高扩展性、负载均衡能力强,支持分布式缓存和异步消息处理,适合大规模并发请求的场景。
RPC
RPC(Remote Procedure Call,远程过程调用)是一种让不同计算机上的程序像调用本地函数一样进行通信的技术,它解决了跨网络调用远程服务的复杂性。通过RPC,程序可以很容易地调用其他机器上的函数,而不必关心底层网络通信细节。
RPC 解决的问题:
-
隐藏了网络通信的复杂性:
通常,程序要在不同机器之间通信,需要处理网络连接、序列化数据、发送请求、接收响应等复杂步骤。RPC 将这些步骤隐藏起来,开发者只需调用函数,RPC 底层会自动完成数据的发送和接收。 -
让分布式系统的调用像本地调用一样简单:
在本地,调用函数只需要传入参数,得到返回值。RPC 模拟了这种方式,开发者可以用相同的方式调用远程函数,不需要手动处理跨网络的通信。 -
简化了跨服务通信:
在微服务或分布式系统中,服务通常分布在多个不同的机器上。RPC 提供了标准的调用方式,使得各个服务之间的通信变得简单、统一。
例子解释:
想象一下,你有两个程序,一个运行在电脑A上,一个运行在电脑B上。电脑A上的程序想调用电脑B上的某个函数。如果没有RPC,电脑A的程序要这么做:
- 打开一个网络连接,连接到电脑B。
- 将要调用的函数名称和参数手动打包(序列化)成网络消息。
- 将消息发送到电脑B。
- 等待电脑B处理请求。
- 接收到电脑B的响应后,再把返回结果解析出来(反序列化)。
- 关闭网络连接。
RPC 让这一切变得简单:
- 你只需要像在本地一样调用函数:
result = some_function(params)
- RPC 框架会自动处理所有的底层细节:它会在电脑A和电脑B之间建立连接,传递参数,调用电脑B上的函数,获取结果并返回给你。
举例:gRPC(现代RPC实现)
在微服务架构中,gRPC 是一种流行的 RPC 框架。比如,假设你有一个订单服务和一个支付服务:
- 当用户下单时,订单服务需要调用支付服务处理付款。
- 使用 gRPC,订单服务就像调用本地函数一样简单地调用支付服务:
payment_result = process_payment(order_details)
。 - 实际上,gRPC 会在订单服务和支付服务之间建立网络连接,发送订单信息,等待支付服务处理,并返回支付结果。
总结:
RPC 通过隐藏底层的网络通信细节,让开发者可以像调用本地函数一样轻松地调用远程服务。这大大简化了分布式系统和微服务之间的交互,提升了开发效率。
proto 是 Protocol Buffers(简称 protobuf)的核心部分。定义了数据结构和通信接口,解决了不同语言之间通信,
proto可以被编译成比 json XML 传输更快的高效二进制格式。
Protocol Buffers 支持向后兼容。其中一个服务增加了字段,其他服务仍可以继续使用旧的字段进行通信
有了这个 proto 文件后,你可以用 protobuf 编译器自动生成各个语言的代码,比如 user.pb.go(用于 Go 语言),user_pb2.py(用于 Python),user_pb.js(用于 JavaScript)
pb.go 文件是 proto 文件编译后生成的 Go 代码,它包含了 protobuf 的序列化、反序列化等操作,供 Go 程序直接调用。
kratos 开源微服务框架 哔哩公司的
Gin 主要关注于 Web 应用和 API 的高性能开发,适合构建轻量级的 Web 服务。
Kratos 主要关注于构建和管理微服务架构,提供了微服务所需的各种工具和功能,适合构建复杂的分布式系统。
当用户发起一个请求时,整个链路通常如下所示:
-
app 层(应用入口):
- 作用:应用的启动入口,主要负责启动 HTTP/gRPC 服务,加载配置文件和依赖的外部服务。服务启动后,会监听请求并将请求传递给下一层。
- 示例:当你启动 Kratos 服务时,它会通过
app
层创建 HTTP/gRPC 服务器,并注册相应的路由。比如启动一个订单服务后,API 路由会映射到/api/v1/order/create
,当客户端发出请求时,它会通过这个路由进入service
层。
-
service 层(服务层):
- 作用:接收并处理来自外部的请求。
service
层通常做一些基础的请求参数校验、转换,以及将请求数据转交给biz
层进行业务处理。 - 示例:客户端通过 POST 请求
/api/v1/order/create
创建一个订单,service
层首先会校验传入的参数,例如订单中的用户 ID、商品信息等是否完整,如果校验通过,将请求传递给biz
层。
- 作用:接收并处理来自外部的请求。
-
biz 层(业务逻辑层):
- 作用:这是业务逻辑的核心层,负责处理应用的复杂业务逻辑。
biz
层不会直接和数据库交互,而是通过调用data
层来获取和存储数据。 - 示例:在订单服务中,
biz
层负责处理订单的核心逻辑,比如检查商品库存、用户的购买力,计算订单价格等。如果库存充足且用户可以下单,biz
层会准备订单数据并准备传递给data
层进行存储。
- 作用:这是业务逻辑的核心层,负责处理应用的复杂业务逻辑。
-
data 层(数据层):
- 作用:与数据库、缓存、外部 API 等外部系统交互。它封装了所有与外部数据源的交互逻辑,保证
biz
层不直接依赖数据库或第三方 API。 - 示例:
data
层会处理数据库的读写操作,比如将订单信息写入数据库,并从库存系统或缓存中获取库存数据。如果商品库存不足,data
层会将库存不足的信息返回给biz
层。
- 作用:与数据库、缓存、外部 API 等外部系统交互。它封装了所有与外部数据源的交互逻辑,保证
-
pkg 层(工具库层):
- 作用:存放一些公共的工具函数和库,供
service
、biz
和data
层调用。 - 示例:
pkg
层可以包含一些通用的日志工具、配置工具等,确保每个层级都可以使用标准化的工具函数来记录日志或加载配置。
- 作用:存放一些公共的工具函数和库,供
-
middleware 层(中间件层):
- 作用:在请求进入
service
层之前,Kratos 提供了一层中间件,用来做一些全局处理,比如鉴权、限流、日志记录、监控等。 - 示例:每个请求在进入
service
层之前,都会经过中间件进行权限校验,比如检查用户是否已登录、是否有操作权限,或者记录日志跟踪请求的详细信息。
- 作用:在请求进入
-
conf 层(配置层):
- 作用:存储服务的配置信息,比如数据库连接、外部 API 地址等,方便应用加载和使用。
- 示例:
conf
层存储订单服务的数据库连接信息、Redis 缓存信息等,服务启动时,conf
层会读取这些配置文件并注入到应用中。
-
cmd 层(命令行工具层):
- 作用:主要用于生成服务启动入口的代码,Kratos 使用
cmd
目录的代码来启动应用。 - 示例:
cmd
层的main.go
文件是整个服务的启动点,服务的启动、配置加载、依赖注入等都是在这里进行的。
- 作用:主要用于生成服务启动入口的代码,Kratos 使用
-
test 层(测试层):
- 作用:用于编写单元测试、集成测试的代码,确保应用的各个功能模块工作正常。
- 示例:测试层可以包含模拟
service
层、biz
层、data
层交互的代码,以便验证不同模块是否正常工作。例如,测试创建订单的流程时,可以模拟调用service
层并检查订单是否成功写入数据库。
在 Kratos 框架中,在微服务之间通过 gRPC 进行通信时,proto
文件起到了非常重要的作用。它的作用主要体现在 API 层 和 服务之间通信 上。
api层生成pb.go
svc层实现,并调用biz
proto
文件的作用具体体现在哪些层?
-
API 层:
proto
文件定义的接口规范位于api
层,Kratos 项目通常将所有对外暴露的接口规范都存放在api
目录下。- 举例来说,订单服务的 API 接口就通过
proto
文件定义,其他服务调用这个订单服务时,直接按照proto
中定义的规范进行调用。
-
service 层:
service
层负责实现proto
文件中定义的接口逻辑。通过.pb.go
文件,Kratos 框架已经生成了接口的模板代码,开发者只需要在service
层实现具体的业务逻辑即可。- 例如,在
service
层实现CreateOrder
逻辑,处理订单创建请求,并将处理结果返回给客户端。
-
biz 层:
biz
层实现具体的业务逻辑,而service
层从proto
文件中生成的接口代码会调用biz
层来处理核心业务。- 比如,
CreateOrderRequest
中的user_id
和product_id
会传递给biz
层,biz
层完成订单的逻辑处理后,将处理结果返回给service
层。
在 gRPC 服务中,greeter.go 可能包含一个 SayHello 函数,当客户端调用此函数时,服务会返回一条“Hello, World”的消息。greeter.go 会调用业务逻辑层(biz)的相关逻辑来完成这类任务。
在一个用户管理系统中,repo 会包含用于访问数据库的代码,比如 GetUserByID 函数,用于根据用户 ID 查询用户信息。repo 提供接口,供 biz 层调用
wire.go
是用来定义“哪些组件需要依赖哪些东西”,相当于一份“依赖关系的规划图”。wire_gen.go
是wire
工具根据wire.go
自动生成的代码,负责实际“组装”这些依赖关系,确保应用能够顺利运行。
服务的注册与发现 ETCD
ttl 心跳,如果超过15秒没有回应,就说明它挂了,就要踢出去它
etcd监听的端口是2379,要在一台机器上把etcd启动起来
etcd 是键值对的。往etcd 里面注册
在 Kratos 中,etcd 主要用于服务注册与发现、配置管理和分布式协调等功能。它在微服务架构中帮助不同服务之间实现动态连接和配置,解决了服务间通信的可靠性和可扩展性问题。
etcd 解决的问题
假设你有一个基于 Kratos 的微服务项目,其中包含用户服务、订单服务和支付服务。每个服务部署了多个副本以提高可靠性和性能。
-
服务注册与发现:
- 当用户服务启动时,它会将自身的地址信息注册到 etcd 中。订单服务和支付服务可以通过查询 etcd 来找到最新的用户服务地址,而不用硬编码固定的 IP 或者端口。
- 当用户服务因为扩容增加了新的实例时,etcd 会自动更新其注册信息,其他服务不需要重启或修改配置,就可以动态发现这些变化。
-
配置管理:
- 假设支付服务的支付限额配置存储在 etcd 中,支付服务会定期从 etcd 获取最新的限额数据。如果你在 etcd 中修改了限额,支付服务可以监听这个配置的变化并自动更新,而不用手动重启支付服务。
-
分布式锁:
- 假设订单服务需要处理来自多个用户的大量订单请求,且为了防止并发冲突,某些资源只能被一个服务实例处理。通过 etcd,订单服务可以在操作之前获取一个分布式锁,确保其他实例不会同时处理相同的资源,保证一致性。
分库分表
为了提升查询性能和避免数据库压力,可以进行分库分表:
分表:将 orders 表按用户ID(user_id)进行拆分,比如把用户 ID 为 1-10000 的订单放在 orders_1 表中,ID 为 10001-20000 的订单放在 orders_2 表中。这样每个表的数据量减少,查询和写入效率提高。
分库:进一步地,假如你有多个数据库实例,可以把不同的表放在不同的数据库上。例如,将 orders_1 放在数据库 A,orders_2 放在数据库 B。这样,即使一个数据库实例过载,其他数据库实例仍然可以正常服务。
一致性哈希
减少节点变更时数据迁移的影响。
假设你有一个缓存服务(如分布式 Redis 集群),最初你有 3 个缓存节点,数据按 key 进行哈希分配到这些节点上。当业务增长后,你决定增加一个新的缓存节点:
传统哈希算法可能导致大量缓存数据从原来的 3 个节点被重新分配到 4 个节点上,引发大量的数据迁移,影响系统性能。
一致性哈希解决了这个问题。在一致性哈希下,新增的节点只会接管环上部分范围内的数据,只有这一部分数据会被重新分配到新节点,而其他大部分数据保持不变。
kartos项目
根据content_id映射到不同的表里面。内容id 替换掉了原来的 id
errgroup.Group是协程,查询多表的时候用这个查,快
分布式加工流 去中心化
server加工 content, redis就是缓冲区, work就是workflow
监控
故障检测和报警,找出性能瓶颈,查看资源使用情况,安全和合规性,
Prometheus grafana
Zipkin: 展示请求链路和性能数据
Zipkin 的工作链路
Zipkin 的工作链路可以分为四个步骤:数据收集、数据传输、数据存储 和 数据展示。
-
数据收集(Instrumenting Services):
微服务中的每个服务通过客户端库(Zipkin instrumentation)来采集追踪数据。每次有请求进来时,服务会生成一个trace
,并在链路上的每一步创建一个span
。一个trace
包含了多个span
,每个span
代表服务调用中的一个阶段,如数据库查询、RPC 请求等。 -
数据传输(Collecting Spans):
每个span
都会通过 HTTP 或 Kafka 等传输方式被传递到 Zipkin 的收集器(collector)。span
中包含了请求的开始时间、结束时间、服务名、调用的资源、请求的标识符、父子关系等信息。 -
数据存储(Storage):
收集器会将这些span
数据存储到后端数据库中,Zipkin 支持多种存储方式,包括内存、MySQL、Cassandra、Elasticsearch 等。通过将这些数据存储起来,可以实现持久化追踪和后续分析。 -
数据展示(Query and Visualization):
开发人员可以通过 Zipkin 提供的 UI 界面查询和展示追踪数据。UI 展示每个请求的trace
,并详细列出每个span
的调用细节,包括耗时、请求路径、失败点等。通过可视化的方式,用户可以直观地看到请求的流转路径和性能瓶颈。
Zipkin 追踪流程举例:
假设一个电商系统中,用户下单的请求需要经过多个服务:前端网关、订单服务、库存服务、支付服务、通知服务。Zipkin 追踪的流程如下:
- 用户通过网关提交订单,网关服务生成一个新的
trace
,并为此请求生成一个span
,记录进入网关的时间。 - 网关将请求转发给订单服务,订单服务接收该请求时继续沿用相同的
trace
,并生成一个新的span
,记录订单服务的处理时间。 - 订单服务请求库存服务,库存服务生成另一个
span
,记录库存查询和更新的耗时。 - 库存服务完成后,订单服务继续请求支付服务,支付服务生成一个新的
span
,记录支付处理的时间。 - 最后,订单服务请求通知服务向用户发送确认邮件,通知服务生成一个
span
,记录邮件发送的耗时。
在整个过程中,每个服务的 span
都会被收集并关联到同一个 trace
,Zipkin 最终可以展示出这次用户下单的完整链路,每个服务调用的耗时情况、请求的路径及其性能瓶颈。
gorm-zipkin 插件
gorm-zipkin
插件 是 GORM(Go语言的ORM库)和 Zipkin 的集成插件,它通过自动采集数据库操作的分布式追踪数据,解决了在分布式系统中数据库操作的可观测性问题。
问题背景
在使用 GORM 进行数据库操作时,通常很难知道一个 SQL 查询的执行时间、数据库响应的性能瓶颈以及这些操作在整个请求链路中的位置。尤其是在微服务架构中,数据库操作常常是性能瓶颈或问题的根源之一。如果没有详细的追踪数据,开发者无法直观地了解数据库查询的耗时及其对系统整体性能的影响。
gorm-zipkin
插件解决的问题
-
自动化数据库操作追踪:
gorm-zipkin
插件可以将 GORM 的数据库操作集成到 Zipkin 的追踪系统中。每当通过 GORM 进行数据库操作(如SELECT
,INSERT
,UPDATE
,DELETE
)时,插件会自动生成相应的span
,记录这些操作的详细信息,包括开始时间、结束时间、SQL 语句、数据库名称等。这使得数据库操作成为整个请求链路的一部分,提供了端到端的可视化追踪。 -
数据库性能分析:
插件可以帮助开发者分析数据库操作的性能,包括 SQL 查询的执行时间、延迟等。这样,当某个请求出现性能问题时,可以快速定位到是哪个具体的 SQL 查询导致了延迟,从而有针对性地进行优化。 -
故障排查:
当数据库操作出错时(如查询失败、超时等),gorm-zipkin
插件会将错误信息记录到 Zipkin 中,方便开发者通过追踪链路快速定位到问题的 SQL 语句,便于故障排查。 -
数据库调用的可观测性增强:
在微服务架构中,数据库操作往往是请求链路中的重要环节。通过gorm-zipkin
插件,数据库调用不仅能够与其他服务的调用链无缝集成,还能提升数据库层面的可观测性。这为开发者提供了全面了解系统性能的能力。
使用场景
- 性能调优:开发者可以通过 Zipkin 直观地查看每个数据库操作的耗时,识别出执行时间过长的 SQL 查询,并进行优化。
- 问题排查:当请求链路中出现错误时,插件会显示出具体的 SQL 错误信息,帮助开发者快速排查数据库相关问题。
- 全链路追踪:在微服务架构下,系统通常需要从 API 请求开始,到服务间的调用、数据库操作的全流程追踪。
gorm-zipkin
插件确保了数据库操作成为这个追踪链路的一部分。
工作流程
- 每次 GORM 执行数据库操作时,
gorm-zipkin
插件会创建一个新的span
,记录操作的起止时间。 - 该
span
包含的详细信息如数据库操作类型(查询、插入等)、SQL 语句、执行时间、是否出错等。 - 插件将这些
span
数据发送到 Zipkin 的收集器,Zipkin 再将它们存储并可视化展示。 - 最终,开发者可以在 Zipkin UI 中查看这些数据库操作的追踪数据,并将它们与其他服务调用的
span
一起分析。
interview
1. 分布式事务
要不全部成功,要不全部失败。
确保事务从一个一致的状态转换到另一个一致的状态
事务看不到系统中其他并发事务的中间态
一旦事务提交对事务的更改就是永久性的。即使之后系统发生故障,事务的结果也不会丢失
2. 分布式环境下为啥会有事务问题
网络延迟和网络分区(网络故障导致系统的一部分无法通信)
系统包含很多组件,任何一个组件失败都会影响事务的成功
并发时候多个事务可能同时访问和修改相同的数据
3. 解决分布式事务问题
1. 两阶段提交协议(2PC - Two-Phase Commit)
问题解决:
2PC 是经典的分布式事务解决方案,它通过两个阶段确保所有参与的节点要么全部提交事务,要么全部回滚。
工作原理:
- 阶段一:协调者向每个参与者发送预提交请求,参与者执行预检查并返回是否准备提交。
- 阶段二:如果所有参与者都准备好了,协调者发送提交请求,所有参与者最终提交。如果有一个参与者失败,协调者发送回滚请求,所有参与者回滚操作。
项目实例:
假设在一个电商系统中,用户下单需要操作两个数据库:一个是订单数据库,另一个是库存数据库。使用 2PC,当订单服务准备写入订单信息时,首先会向库存服务确认是否能减少库存。如果库存服务返回确认,协调者会发送最终提交请求,确保订单和库存两者都提交。否则,所有操作都会回滚。
优缺点:
- 优点:简单易理解,适合跨多个数据库进行操作。
- 缺点:性能开销大,参与者会长时间锁定资源,容易导致性能瓶颈,特别是在高并发的情况下。
2. 补偿事务(TCC - Try, Confirm, Cancel)
问题解决:
TCC 是一种柔性事务解决方案,它通过为每个操作设计三步(尝试、确认、取消)来处理分布式事务问题。相比 2PC,TCC 允许更灵活的事务操作。
工作原理:
- Try:预留资源,进行初步的操作检查。
- Confirm:确认并正式提交操作。
- Cancel:如果确认阶段失败,则取消操作,释放预留资源。
项目实例:
仍以电商系统为例,用户支付需要预先冻结资金,并检查库存是否足够。Try
阶段冻结资金并预留库存,Confirm
阶段真正扣款并减少库存,Cancel
阶段则在失败时解冻资金并恢复库存。每个服务都会有独立的 Try
、Confirm
和 Cancel
逻辑,从而在失败时保证一致性。
优缺点:
- 优点:更加灵活,可以减少长时间锁定资源的问题。
- 缺点:需要额外实现
Try
、Confirm
和Cancel
逻辑,增加了开发复杂度。
3. 最终一致性(Eventual Consistency)
问题解决:
在分布式系统中,追求强一致性往往会导致性能瓶颈。最终一致性放宽了实时一致性的要求,允许各个节点的数据在一段时间内不一致,但最终通过某种机制达到一致。
工作原理:
最终一致性常结合消息队列使用,确保数据异步传输到各个服务,通过重试机制确保所有服务的数据在一段时间后一致。例如,事件源和 CQRS(Command Query Responsibility Segregation)常用于支持最终一致性。
项目实例:
假设一个用户订单的创建涉及库存系统、支付系统和物流系统,订单服务将“订单创建”的事件异步发送到消息队列中,库存服务和物流服务监听这个事件并做出相应操作。如果其中一个服务处理失败,它可以重试,确保最终达成一致状态。
优缺点:
- 优点:性能更好,适合高并发的场景,减轻了系统的锁定压力。
- 缺点:短时间内数据可能不一致,需要设计合适的补偿机制和监控。
4. 消息队列事务(Transactional Messaging)
问题解决:
通过消息队列进行事务控制,确保操作和消息的发送是一致的。适用于微服务架构中的跨服务通信,特别是在异步场景下。
工作原理:
事务消息将业务操作和消息发送绑定在一个事务中。如果业务操作成功,消息才会被发送到消息队列,否则回滚。
项目实例:
假设电商系统中,用户下单后需要给库存服务发送一个减库存的请求。订单服务在写入订单信息的同时,会把“减库存”的消息放入消息队列。如果订单写入成功,消息才会发送,否则不会发送。库存服务通过监听队列中的消息来处理库存减少的逻辑。
优缺点:
- 优点:保证了业务和消息的最终一致性,避免了分布式锁的开销。
- 缺点:依赖于消息队列的实现,可能需要处理消息丢失和重复消费问题。
5. 分布式锁
问题解决:
分布式锁用于控制多个服务对共享资源的并发访问,防止资源冲突。在需要保护关键资源不被多个事务同时修改时,分布式锁可以确保只有一个事务能获得资源的修改权限。
工作原理:
服务在操作共享资源前,首先获取分布式锁。只有持有锁的服务可以进行修改,其他服务需等待锁释放。如果服务完成操作或超时,锁会被释放。
项目实例:
假设多个微服务同时更新库存,可能导致库存不一致问题。通过 Redis 或 Zookeeper 实现分布式锁,服务在操作库存前必须获得锁。只有一个服务可以修改库存,确保并发操作的正确性。
优缺点:
- 优点:简单有效,防止并发修改问题。
- 缺点:需要谨慎处理锁的超时、死锁问题,适合少量关键资源保护。
6. Saga 模式
问题解决:
Saga 模式是一种分布式事务的另一种柔性事务方案,它将事务分解为一系列子事务,每个子事务都有相应的补偿操作。与 TCC 类似,Saga 更注重通过独立的事务协调来实现最终一致性。
工作原理:
Saga 中每个服务独立执行自己的事务,如果某个事务失败,则触发对应的补偿事务(如回滚),其他事务继续执行,确保最终状态一致。
项目实例:
假设一个订单系统有三个步骤:创建订单、扣除库存、发放优惠券。每个步骤都是一个子事务。若创建订单成功但库存不足,则触发补偿操作来取消订单。每个操作失败时都有对应的补偿机制,保证整体事务的完整性。
优缺点:
- 优点:更适合长时间运行的事务操作,减少了锁资源问题。
- 缺点:需要开发每个步骤的补偿逻辑,增加开发复杂度。
总结
分布式事务问题的解决方案各有优缺点,项目中的选择通常根据系统的性能需求、并发量、操作复杂性等因素来决定。例如,TCC 和 Saga 适合具有较高容错性、需要柔性事务的场景,而 2PC 则适合对强一致性要求更高的系统。
ETCD
强一致性:任何时候,所有副本完全一致
任期的作用
比如一个网络分区有ABCD四台机器,其中A的任期号是1,但是它有毛病了,BCD选举B为leader,任期号是2。过一段时间A恢复了,还以为自己的leader,但是一和B比较任期号,它就变成follower 了
它们是搞选举的,半数通过才行
last_applied
和 commit_index
是分布式一致性协议(如 Raft 协议)中的两个关键概念,用于解决分布式系统中的数据一致性问题。
1. commit_index 解决的问题
commit_index
代表在当前任期中,已经被提交到大多数节点上的最新日志条目的索引。这个索引保证了:
- 一个日志条目一旦被提交(replicated),它就不会丢失,即使系统发生崩溃或故障。
- 这个条目可以安全地被应用到状态机上,确保系统的一致性。
例子:
假设在一个分布式系统中,Leader 将某个操作(比如将余额从账户 A 转到账户 B)记录在日志条目中,并将其发送给所有 Follower 节点。如果大多数 Follower 节点确认接收到这个条目,那么该条目会被认为是“已提交”,commit_index
会更新到这个条目的位置。这时,即使有节点发生故障或宕机,该操作也不会丢失,因为大多数节点已经复制了这个操作。
2. last_applied 解决的问题
last_applied
表示某个节点已经将日志条目应用到状态机上的进度。它确保:
- 每个节点在将某个日志条目应用到状态机时,保证顺序性。
- 即使系统发生故障,恢复后也可以通过
last_applied
确定状态机的最新应用状态。
例子:
继续上面的例子,commit_index
确认日志条目已经被提交,但这些条目尚未被应用到每个节点的状态机上。各节点会独立将这些已提交的日志条目按顺序应用到自己的状态机,并更新它们的 last_applied
。假设某个节点处理到了第 4 条日志,last_applied
会指向第 4 条。即使这个节点宕机,恢复后它也可以从第 5 条日志继续应用,而不需要重复应用已经处理过的日志。
总结
commit_index
确保系统中日志条目的一致复制和安全提交。last_applied
确保每个节点按顺序应用已提交的日志,避免重复操作。
通过这两个机制,分布式系统可以在处理日志条目时实现一致性和可靠性。
状态机确保每个节点在分布式系统中能够按顺序执行相同的操作,并保持一致的状态。
etcd raft
etcd 使用 Raft 协议来实现一致性和领导者选举。以下是 Raft 选举过程的详细步骤和链条:
1. 节点状态
在 Raft 中,节点可以处于以下三种状态之一:
- Follower:初始状态,接收来自其他节点的请求。
- Candidate:在选举期间,节点会转变为候选人并请求选票。
- Leader:当节点获得多数选票后,成为领导者,负责处理客户端请求和日志复制。
2. 节点时间和心跳
每个节点维护一个选举超时计时器。如果在选举超时之前未收到领导者的心跳消息,节点将认为当前没有领导者,并开始进行选举。
3. 选举流程
- 超时触发:当一个 Follower 超时未收到心跳时,它将转变为 Candidate。
- 增加任期:Candidate 将其任期加 1,并开始进行选举。
- 请求投票:Candidate 向其他节点发送
RequestVote
消息,请求他们的选票。消息中包含 Candidate 的当前任期和最后日志索引。 - 投票逻辑:
- Follower 接收到
RequestVote
消息后,会检查:- 如果 Candidate 的任期小于当前任期,则拒绝投票。
- 如果 Candidate 的任期等于或大于当前任期,且 Candidate 的日志至少与 Follower 的日志一样新,Follwer 将给 Candidate 投票。
- Follower 接收到
- 返回投票结果:Follower 将
RequestVote
的结果(投票或拒绝)返回给 Candidate。
4. 选举结果
- 获得多数选票:如果 Candidate 收到多数节点的选票(超过半数),它将转变为 Leader,并向所有节点发送
AppendEntries
消息以建立心跳。 - 未获得多数选票:如果没有获得多数选票,Candidate 将回到 Follower 状态,等待下次选举超时。
5. 处理分区
在网络分区的情况下,可能会出现两个或多个 Candidate 同时被选为 Leader。Raft 通过以下方式处理:
- 任期检查:一旦节点接收到高任期的消息(例如心跳),它将放弃当前的选举状态并回到 Follower 状态。
- 日志一致性:Candidate 在请求投票时会检查自己的日志是否与 Follower 的日志兼容,确保日志一致性。
6. 重启和恢复
如果 Leader 失败,其他节点会在心跳超时后开始新的选举,保持系统的可用性和一致性。
总结
etcd 的 Raft 选举过程依赖于节点的状态转换、超时机制、请求投票、日志一致性和网络分区的处理,确保在分布式环境中实现高可用和一致性。这一过程确保了在任何时刻都只有一个 Leader,避免了冲突和不一致性。
Prometheus
TSDB
-
高频采集和存储优化
问题:在监控系统中,成千上万的指标数据每隔几秒就要采集一次(例如 CPU 使用率、内存占用、磁盘 IO 等),这些数据在短时间内产生大量的条目,传统的存储系统无法高效地存储和管理这些频繁产生的时间序列数据。解决方案:Prometheus tsdb 使用了块存储的方式,将时间序列数据按块(chunk)进行存储,每个块存储一段时间的数据(默认是2小时)。这种方式减少了存储重复时间戳的开销,并且通过适当的压缩技术来降低存储成本。例如,浮点数和时间戳的重复部分可以被高度压缩。
-
快速查询
问题:在大量时间序列数据中进行实时查询,尤其是在短时间内获取大量数据(例如,查询过去 1 小时的所有 CPU 使用情况)时,传统数据库的性能往往难以满足监控的实时需求,查询时间长,无法及时反馈系统状态。解决方案:tsdb 为每个时间序列都构建了索引,通过标签(label)来高效定位具体的时间序列。这种标签机制使得 Prometheus 可以快速筛选出相关的时间序列,结合时间范围的过滤,可以极大地加速查询。例如,查询某一时间范围内具有特定标签(例如
job="web_server"
,instance="10.0.0.1"
)的 CPU 使用率时,可以通过索引快速锁定并返回结果。 -
历史数据压缩与持久化
问题:时间序列数据的规模随着时间推移迅速增加,长期存储这些数据可能会占用大量磁盘空间,尤其是对长期保留的历史监控数据而言,存储效率至关重要。解决方案:tsdb 通过块合并(block compaction)和样本去重等技术对历史数据进行压缩,并减少不必要的存储开销。例如,类似时间段内变化不大的指标数据可以使用少量的存储空间进行表示。Prometheus 还可以定期将旧的时间块合并,进一步减少存储的占用。
实际应用场景:
在一个大型的服务器集群中,Prometheus 每隔 15 秒采集集群中的 10,000 个时间序列(例如 CPU、内存、网络等),这些数据在短时间内就会积累成百上千万条。如果没有 tsdb 的优化,存储这些数据会非常耗费空间,而且查询变得非常缓慢。但有了 tsdb 的块存储、标签索引和压缩机制,即使在几天、几周内存储大量的时间序列数据,Prometheus 依然能够快速响应查询请求,并且占用的存储空间也在可控范围内。
实际工作流示例:
-
数据抓取:Prometheus 每 15 秒抓取一个 Web 服务器的请求量和 CPU 使用率,产生时间序列
http_requests_total{job="web_server", method="GET", instance="10.0.0.1"}
和cpu_usage{job="web_server", instance="10.0.0.1"}
。 -
存储:抓取的数据会先写入 WAL 日志,随后在内存中缓存一段时间。每隔 2 小时,Prometheus 会将数据打包成一个块,并将其存储到磁盘中。
-
查询:用户使用 PromQL 查询过去 1 小时 Web 服务器的请求量,例如:
sum(rate(http_requests_total{job="web_server"}[5m]))
Prometheus 首先通过索引快速找到
http_requests_total
的相关时间序列,并读取块文件中的样本,计算过去 1 小时的请求速率。 -
报警:如果 Web 服务器的请求量突然激增,Prometheus 的报警规则会检测到这种异常情况,并发送报警通知。
通过这种工作链条,Prometheus 可以高效地采集、存储、查询大量的时间序列数据,保障监控系统的实时性和性能。
整体链条关系
-
时间序列(Time Series) 是基础单位。
- 一个时间序列由 度量名称 和 标签(labels) 唯一标识。
- 每个时间序列包含一组有序的 样本(Samples),每个样本包含一个时间戳和对应的数值(浮点数)。
- 同一度量的不同标签组合会生成不同的时间序列。
-
样本(Sample) 是时间序列中的数据点。
- 样本代表某个时间点的监控数据值,具有唯一的 时间戳 和 值(数值数据)。
- 样本通过标签归属于某个时间序列。
- 样本数据最先被写入 WAL(Write-Ahead Log) 进行持久化(写之前的日志,这样即使内存中数据丢失,还可以通过日志重建),确保数据不丢失。
-
块(Block) 是存储的基本单元。
- tsdb 将时间序列数据划分为固定时长(默认是 2 小时)的 块。每个块会包含多个时间序列的部分样本。
- 当一定量的数据写满内存缓存后,会被写入块并持久化到磁盘。
-
索引(Index) 帮助快速定位数据。
- 索引文件 记录了块中的所有时间序列及其对应的标签、样本的位置信息。
- 索引帮助在查询时通过标签快速找到与查询匹配的时间序列,并进一步从块中获取具体的数据点。
实际工作链条示例
假设我们有一个 HTTP 服务的请求计数指标 http_requests_total
,按如下链条处理数据:
-
时间序列:
Prometheus 从一个 Web 服务器抓取http_requests_total
,并为不同的标签组合创建多个时间序列。例如:http_requests_total{job="web_server", instance="10.0.0.1", method="GET"} http_requests_total{job="web_server", instance="10.0.0.2", method="POST"}
这两个不同的标签组合构成了两条时间序列,它们分别记录了不同实例和请求方法的 HTTP 请求总数。
-
样本:
对于http_requests_total{job="web_server", instance="10.0.0.1", method="GET"}
这一时间序列,Prometheus 每隔 15 秒采集一次样本数据,比如每次的请求总数为:{time: 1695204000, value: 100} {time: 1695204015, value: 105} {time: 1695204030, value: 110}
这些样本表示在特定时间点内,
10.0.0.1
实例的 GET 请求总数分别为 100、105 和 110。 -
块:
当 Prometheus 持续采集多个时间序列的样本数据后,它将数据按时间划分存储在块中。假设 Prometheus 采集了 2 小时的 HTTP 请求数据,它会将这 2 小时的数据打包成一个块:- 块会包含所有时间序列的样本,例如 GET 和 POST 请求的计数样本。
-
索引:
Prometheus 为块建立索引文件,记录每个时间序列的标签组合及其样本的存储位置:- 比如
http_requests_total{job="web_server", instance="10.0.0.1", method="GET"}
在该块中的样本从哪个偏移量开始,到哪个偏移量结束。 - 索引可以让 Prometheus 在查询时通过标签组合快速找到相关的时间序列,而无需遍历整个块。
- 比如
查询链条示例
假设用户想要查询过去 1 小时内,job="web_server"
的所有 GET 请求数量,查询过程如下:
-
PromQL 查询:
用户输入查询:sum(rate(http_requests_total{job="web_server", method="GET"}[5m]))
这意味着 Prometheus 需要计算过去 1 小时内的 GET 请求速率。
-
索引查找:
Prometheus 首先通过索引文件查找符合job="web_server"
和method="GET"
的所有时间序列,快速定位到http_requests_total{job="web_server", instance="10.0.0.1", method="GET"}
等时间序列。 -
样本提取:
Prometheus 根据索引中的位置信息,从相关块文件中提取过去 1 小时的样本数据,如:{time: 1695204060, value: 115} {time: 1695204075, value: 120} {time: 1695204090, value: 125}
这些样本表示在最近的时间内,每 15 秒时 GET 请求的累计数量。
-
计算:
Prometheus 将提取到的样本数据按时间段进行计算,最终计算出请求速率并返回给用户。