前言
在k8s中,业务通常以deployment的方式部署,这种无状态的部署方式,对服务升级和部署极为友好。
但是,我们需要这个deployment里面的pod分工合作的时候,就无能为力了。
也许你会想到statefulset,这会带来更多问题,而且我们并不想牺牲无状态集群的好处。
一些可以行的方案
- 选主,当业务不是特别复杂的时候,选一个主节点就可以解决问题。比如一些数量不大的补偿操作。
- 任务派发器。不管是哪种访问方式,请求通常被均匀的分配给所有的pod,所以只需要将全量任务找到,然后服务实现一个处理接口。然后借助外部的某个程序或者脚本将这些任务 以请求的分方式发给这些pod。
- 使用分布式锁,比如通过redis的set实现。
- 借助mq处理一些有状态的需求。
- 等等
根据不同的场景可以有很多方案,本文重点实现下面这方案。
- 首先创建一批标签,称为一个任务
- 节点在启动后加入这个任务,会得到一些标签,(一个标签只能分配给一个节点)
- 节点不断监控标签的变化情况,并不断向任务发送心跳。
- 节点准备退出,告诉任务回收当前节点拥有的标签。
- 任务自身会监控节点活跃情况,如果节点加入或退出,则对标签进行重分配
这样做的好处是借助一个协调系统,我们的无状态集群 就会变为有状态的集群,并且能够让所有的节点同时工作。
这样做的思路来自redis集群方案,可以将这些标签看作是槽。
先实现这个协调系统coordinate
coordinate服务的git地址:github.com/woshihaoren…
先写pb文件
首先需要一个管理任务的service。很简单的对任务进行增删改查。
service TaskService{
rpc CreateTask(CreateTaskRequest) returns (CreateTaskResponse){
option (google.api.http) = {
post: "/api/v1/task/create"
body: "*"
};
}
rpc SearchTasks(SearchTasksRequest) returns (SearchTasksResponse){
option (google.api.http) = {
get: "/api/v1/task/search"
};
}
rpc TaskDetail(TaskDetailRequest) returns (TaskDetailResponse){
option (google.api.http) = {
get: "/api/v1/task/{task_id}/detail"
};
}
rpc TaskDelete(TaskDeleteRequest) returns (TaskDeleteResponse){
option (google.api.http) = {
post: "/api/v1/task/delete"
body: "*"
};
}
}
然后需要一个节点的service。用于节点的加入,退出,心跳,更新版本。
- 任务会有一个版本号,每次标签重分配都会增加一个版本号.
- 节点在监控到这个变化后,会通过
UpdateNodeVersion将自身版本号更新为任务的版本号,表示自己知道了这次变化。
service NodeService{
//节点加入任务
rpc JoinTask(JoinTaskRequest)returns(JoinTaskResponse){
option (google.api.http) = {
post: "/api/v1/node/join"
body: "*"
};
}
//节点退出任务
rpc ExitTask(ExitTaskRequest)returns(ExitTaskResponse){
option (google.api.http) = {
post: "/api/v1/node/exit"
body: "*"
};
}
//节点心跳保活
rpc Ping(PingRequest)returns(PingResponse){
option (google.api.http) = {
get: "/api/v1/node/ping"
};
}
//更新node的version
rpc UpdateNodeVersion(UpdateNodeVersionRequest)returns(UpdateNodeVersionResponse){
option (google.api.http) = {
post: "/api/v1/node/update_version"
body: "*"
};
}
//查询标签的分配情况
rpc SlotDistributions(SlotDistributionsRequest)returns(SlotDistributionsResponse){
option (google.api.http) = {
post: "/api/v1/node/{node_code}/slot"
body: "*"
};
}
}
相关选项
- 编程语言使用rust,就追求个稳定。
- 接口使用gRPC,也就变形的实现了http。
- 数据库使用etcd。首先etcd完全符合coordinate对一致性的要求。其次节点可以直接watch到便签变化情况,变相提供了主动推送的能力。
代码实现
代码有点多,放在了git上,有兴趣的可以看一下,也欢迎感兴趣的小伙伴参与进来。 github.com/woshihaoren…
其中核心重平衡的代码在这里:github.com/woshihaoren…
这段写的有点冗长。简化过程如下:
-
- 先计算每个节点应该分配标签数,和总标签集
-
- 遍历节点,找出所有自由标签。找出所有需要待分配的节点。
-
- 将这些自由标签分给这些待分配节点。
自由标签:是指不在任何节点上的标签+节点标签数超过应分配标签数的那部分标签。 这样做的目的是为了保证在扩缩节点的时候,尽量减少那些已分配节点的标签的变化。
再实现一个节点模拟工具
这里用rust的egui实现可视化界面的节点。
git地址:github.com/woshihaoren…
界面效果
开始测试
启动测试环境
为了方便测试,我们coordinate使用docker运行,所以需要提前安装一下docker,同时需要部署一个etcd错位存储节点。
为了方便节点界面的开发,我们使用了http的接口,所以需要用rust-grpc-proxy做一下代理
首先 git clone 这个github.com/woshihaoren…
运行cmd.sh 脚本,命令如下,需要指定一个etcd节点:
./cmd.sh start http://127.0.0.1:2379
这个命令流程大概是:
- 根据输入生成一个配置文件,这个配置文件是
coordinate和rust-grpc-proxy共用的。 - 启动
coordinate服务 - 启动
rust-grpc-proxy代理
可以运行一下测试,看看请求是否成功
./cmd.sh test
编译
用如下命令编译项目,生成节点应用。
./cmd.sh build
最终在项目根目录下生成wd_eui这个程序
创建任务
先启动测试工具
./wd_eui
启动多个节点进行测试
再启动一个节点加入任务,可以看到标签被重置了
停止一个节点的心跳,几分钟后coordinate认为该节点死亡,会重新将标签分配。
当然,也可以主动退出任务,标签会立即重新分配,目前测试工具还没写这部分(懒)
这里节点是间隔30秒拉取一次信息,也可以watch etcd的节点信息变化,能够更快地获取重分配信息,这里又犯懒了。
清理
运行./cmd.sh clean 命令,用于清理环境。
尾语
欢迎感兴趣的小伙伴留言讨论