- worker.h
worker的用户API;
SyncOpts:
std::vector<int> deps : dependencys, 这些timestamp的操作都完成后,本操作才会被执行;
std::function<void()> callback : 本操作执行完后,该callback即被立即调用;是取代Wait的方式;由KVWorker的接收线程调用的(是不是单线程??)
std::vector<Filter> filters; : 压缩,Key-Caching,等Filter们;
KVWorker:
KVWorker(int id = NextID()) : id不知道是什么意思。。。
Push: 1个Key可以对应k个Value;非阻塞立即返回;先拷贝了一份Keys和Vals;返回值是该操作的timestamp,可以交给Wait去等待;
Pull: 1个Key可以对应k个Value;非阻塞立即返回;先拷贝了一份Keys
Wait(int timestamp): 等待一个Push或Pull操作的彻底结束
VPush: 每个Key对应不同数目个Value
VPull: 每个Key对应不同数目个Value
ZPush: Zero-copy版的Push,不复制Keys和Values;用户需要保证该操作完成之前,参数内容不许修改;
ZPull: Zero-copy版的Pull,不复制Keys;用户需要保证该操作完成之前,参数内容不许修改;
ZVPush/ZVPull: Zero-copy版的VPush/VPull
KVCache<Key, Val>* cache_ : ZPush/ZPull/ZVPush/ZVPull/Wait的核心内部实现
- app.h
Customer:
KVCache, KVStore, App, 都继承自Customer
相同custmoer-id的两个Customer之间可以用Request-Response的方式通信
是个接口空壳类,里面的实现是Executor exec_来做的
流程:
1. 当CustomerA和CustomerB的Customer::id相同时,A调用Submit, 向B发送一条request Message;
2. 到达B之后,当该消息的dependency满足时(一维上的DAG,按timestamp时序依赖),调用用户自定义函数ProcessRequest
3. B调用Reply,向A发送一条response Message;
4. A接收到后,调用用户自定义函数ProcessResponse
Submit是异步接口,把request Message queue在后台后立即返回;返回值是一个timestamp,用于同步;B可以用这个timestamp调用WaitReceivedRequest来等待这个请求直到被自己处理完;A可以用这个timestamp调用Wait等待该消息的response到来;A也可使用callback来处理response(注意这里用的是很简易的单线程实现!!)
A可以向一个node group(parameter servers)发request Message, Customer子类定义的Slice函数会被调用,用来按KeyRange划分至多个小Message中去,发到node group中去;
Wait: 如果receiver是个node group, 那要等group里所有node都返回response后,Wait才返回;
ProcessRequest:在KVStore端调用;
ProcessResponse:在KVCache端调用;
WaitReceivedRequest:0次调用,无用;
FinishReceivedRequest:由Executor调用rnode->recv_req_tracker.Finish(timestamp);
NumDoneReceivedRequest:0次调用,无用;
Reply: 调用Executor的Reply,发送reponse;
Executor exec_:核心功能都是调用Executor 的
- executor.h && executor.cc
sent_req_tracker: 相对于请求本身而言,记录发送节点发送到该remote节点的消息,是否已处理完ACK;
recv_req_tracker:相对于请求本身而言,记录remote节点发送到本server节点的消息,是否已被server处理完了;
Run(): 后台一个thread_在服务,PickActiveMsg每次拿到1个再无dependency的消息,然后交给ProcessActiveMsg去处理;
PickActiveMsg(): 扫描刚接收的消息集合recv_msgs_,对每条消息:(扫一遍后仍未找到,则wait在signal上,等有了新消息到达或者有另一条消息Finish时(有解除其他消息dependency的可能)才唤醒)
如果消息发送者已死,则删去该消息,不处理;
如果该消息是request且已被本server处理完成,或者该消息是response且本worker已处理完ACK,则判为重复消息,删之;
对server端处理请求消息而言,必须在该消息的所有dependency消息都被处理完后,才能判定为Active;
response消息到这一步默认判定为Active;
设该消息到active_msg_,从recv_msgs_中删除他,Pick视为成功返回true
ProcessActiveMsg(): 处理active_msg_:
对于request消息:调用Customer的ProcessRequest来处理; 调用FinishRecvReq来标记recv_req_tracker上Finished;调用Customer的Reply来发送response消息;
对于response消息:调用Customer的ProcessResponse来处理; 设置sent_req_tracker上Finished; 如果该response来自group,则group所有response都处理完了才往后走;调用该request的用户自定义callback函数;唤醒PickActiveMsg。注意:pull操作,来自每个server的消息,都在ProcessResponse里merge到一起了,来一个merge一个,通过timestamp,得到当初发出去的哪个request,得知那个request的receiver是group,从而要确保group每个成员的消息都IsFinished(ts)才继续往下走去调用用户callback并唤醒信号量sent_req_cond_(激活ActiveMsg的检测,放行Wait)
CheckFinished(RemoteNode* rnode, int timestamp, bool sent): 判断该消息是否被处理完了,如果RemoteNode是group,则必须group每个成员node都完成了才算完成(如果是KVWorker的Wait, 判断rnode->sent_req_tracker的timestamp是否Finished)
NumFinished:类似CheckFinished,如果RemoteNode是group,统计总共多少个成员node完成了;不是group则返回0或者1
WaitSentReq(int timestamp): KVWorker的Wait; sent_reqs_[timestamp]得到消息,及其RemoteNode,调CheckFinished查是否处理完成,wait在sent_req_cond_信号量上;
WaitRecvReq:没被调过,无用;
QueryRecvReq:没被调过,无用;
FinishRecvReq:接收到消息后,将recv_req_tracker对应的消息置为Finished; 唤醒信号量;
Submit(Message *msg): 为msg打时间戳,时间戳+1;按receiver的range将msg切分成多个子消息;一个一个将子消息通过postoffice发送出去(空的子消息,则标记sent_req_tracker为Finish);
Reply(Message* request, Message* response): 设置response的时间戳(同request的时间戳)等信息,通过postoffice发送出去;
Accept(Message* msg):把msg加到recv_msgs_队列;唤醒Run()线程;
AddNode:太复杂,没看懂呢???
- postoffice.h && postoffice.cc
邮局;SINGLETON模式,进程内独一份,专管消息发送和接受的后台服务;
ThreadsafeQueue<Message*> sending_queue_:线程安全的消息队列,存放待发送的消息们;接收消息没有队列,由recv_thread_直接调用manager_的Recv功能;
std::unique_ptr<std::thread> recv_thread_ : 专门负责接收消息的后台线程;
std::unique_ptr<std::thread> send_thread_: 专门负责发送消息的后台线程;从发送队列里wait_and_pop消息
Manager manager_:发送消息,接受消息,处理control消息,处理task消息(Accept)
Msg分成几种,msg->task.request()表示是Request还是Respone, msg->task.control()表示是不是控制型消息;
- env.h && env.cc
在FLAGS_log_dir目录下初始化日志文件;
根据当前环境变量(或MPI的rank值),首先填充FLAGS_scheduler即scheduler的IP端口信息;再获取当前IP, port, role, id,序列化到字符串FLAGS_my_node中;
如果用MPI,则scheduler是独立进城;workers+servers整个是一个MPI-job;
- van.h && van.cc
网络层;使用ZeroMQ进行实际的网络发送和接收;
- node_assigner.h
为server node分配KeyRange(EvenDivide,按区间均匀划分的),同时为node分配rank id(servers一组,workers一组)
- kv_store.h:
SetReplica和GetReplica函数,是想实现冗余热备份,但目前为空函数暂时未实现该功能。
ProcessRequest里,pull操作就Reply, push操作就暂不Reply, 但是会在后面的Executor::ProcessActiveMsg里Reply一个ACK;
- kv_store_sparse_st.h:
kvstore的单线程版本,即每个请求只用1个线程(即当前线程)来处理。
使用unordered_map来存储<Key,Value>数据。
Handle handle_:用户自定义的Push和Pull操作;
HandlePull: 把data_交给用户去填充msg的values;
HandlePush: 把msg的values交给用户去改写data_;
- kv_store_sparse.h:
kvstore的多线程版本,适用于压力大的情况,把work-load分到该机器的多个CPU上;
std::vector<std::unordered_map<K, E>> data_ : 每个线程使用1个unordered_map,无锁,线程安全;
SliceKey:每个线程所处理的Key Range是定死的(长度为bucket_size_),在本Node的Key Range内均分;每来一个请求,就把这个请求的所有Key分到每个线程负责的区间上去;如果这组Key不均匀(比如未经过Hash函数或Hash得不够散或Hash范围不够64位),那么这些线程的负载会不均衡;之所以要按区间来划分,而不是按Key数量来划分至线程,应该是为了unordered_map的线程安全,如果按Key数目来均分至各线程,会造成多个线程同时访问同一个unordered_map的冲突;
ThreadPool:通过线程池来执行每个区间上的Pull和Push用户自定义操作;
- kv_cache.h
KVWorker发送接收功能的实际实现者;
继承自Customer;发送接收消息的功能还是调用Customer的接口来实现的;
Push(const Task& req, const SArray<K>& keys,
const SArray<V>& vals, const SArray<int>& vals_size,
const Message::Callback& cb):把东西都加到Message里面去,最后交给Custormer::Submit去发送;vals_size是变长value时每个Key对应多少个values的计数数组;msg的value[0]是实际的value数组;msg的value[1]是vals_size这个计数数组(如果是变长values的话);
Pull_: 把Pull请求的东西放到Message里面去,并且把该Message缓存起来(用于收到Response后填充对应的values);这里的callback主要负责Check正确性,调用户的callback,并删除缓存;
Slice:由系统来调用;把一个Message切分成多个小Message(这里按Key的区间范围来切分);里面有message.h的函数来实现;
ProcessResponse:由系统来调用;对push操作不做任何事;对pull操作:ParallelOrderedMatch把收到的裸Message里的数据(从多个Servers下来,不是完全有序的)摆放成有序的存在缓存里;(缓存的东西和用户buffer是同一份存储)
dynamic value size时,可以看到收到所有server的消息,才对所有消息进行拼接;而固定size时,为什么对每个消息都直接处理了???
std::unordered_map<int, KVPair> pull_data_ : 存放的是已发送走的Pull请求的数据存储;收到Response后即填充对应的values; 格式:[channel-id, kv-pair]
- parallel_ordered_match.h
把server上pull来的子range的message, 汇合到最终要返回给用户的大range的message上去;可使用多线程来实现(递归式的多线程函数技巧);但实际kvcache的ProcessResponse里,用的是每个key对应的value个数来做线程个数;里面用的是lower_bound来定位大概范围,不知道性能怎样;个人感觉可以优化,将收到的子消息都缓存起来,最后一个到来了按第一个key从小到大排序依次填充即可(他的dynamic value size部分就是这么干的)
- ps.h
CreateServerNode为什么找不到实现???
启动流程:
1. 进入main函数: 调用ps::StartSystem, 里面调用postoffice::Run和postoffice::Stop
2. postoffice::Run: 初始化日志,解析命令行,调manager_.Init, 创建发送线程和接受线程,调manager.Run
3. manager_.Init:env_.Init, van_.Init, net_usage_.AddMyNode,
调用App::Create (在用户的main.cpp里实现),根据角色创建worker或者server或者scheduler
对scheduler: 创建NodeAssigner; AddNode添加自己;
对worker和server: 向Scheduler发送控制消息REGISTER_NODE
Scheduler接收到REGISTER_NODE:NodeAssigner添加该node; AddNode添加该node;
AddNode: van_.Connect(node), alive_nodes_.insert, net_usage_.AddNode, customers_的executor()->AddNode,worker和server个数都达到预设数值时:对于scheduler,把所有node的信息放在控制消息ADD_NODE里,广播给所有节点;对于worker和server: 向Scheduler发送控制消息READY_TO_RUN
当woker和server接收到ADD_NODE:对消息里附带的所有node, 依次执行AddNode(node)
当Scheduler接收到READY_TO_RUN:active_nodes_里加上该node; 当active_nodes_达到预期数目,则向所有node发送READY_TO_RUN;inited_ = true
当worker和server接收到READY_TO_RUN:inited_ = true
4. manager.Run:等待(10s则超时报错)直到inited_为true; 执行app_->Run
5. Run:
Worker的Run: 干活儿
Server的Run: 啥事儿不干直接返回
Scheduler的Run:啥事儿不干直接返回(wormwhole例子是真的指挥干活儿了)
6. postoffice::Stop:调Manager::Stop:
对于worker和server:向Scheduler发送READY_TO_EXIT,等待在done_上直到为true(即收到来自Scheduler的EXIT消息)才往后走;
对于Scheduler:等待,直到active_nodes_为空才往后走(收到一个READY_TO_EXIT则把对应的node从active_nodes_里删去);把EXIT发送给所有node
7. 尘归尘,土归土,世界终于恢复了平静,一切都结束了!
=================================================
一条消息走过的路:
Worker端:
Worker.h:
ZPush, ZPull,ZVPush, ZVPull, 都调用cache_->Push或cache_->Pull
Wait,调用cache_->Wait
Kv_Cache.h:
Push: 把东西放到msg里,调用Submit提交msg
Pull: 调用Pull_;Pull_: 把东西放到msg里, 暂存在unordered_map里(pull_data_[chl]),为的是完成后作一些check; 注册callback函数(里面完成check, 调用用户callback); 最后Submit提交msg;
App.h:
Customer::Submit,调用Executor::Submit
Executor.cc:
把msg打上时间戳为time_, 递增本机的time_;
暂存到sent_reqs_[ts];
把msg按照server个数,来且分成多个子msg;
把这些子msg依次交给Postoffice发送(sys_.Queue(m))
Postoffice.h:
Queue:push到发送队列sending_queue_里; 在发送线程中被顺序发出;
Server端:
Postoffice.h:
接收线程的Recv里, 对control类型消息:调用manager_.Process;对普通消息(Push/Pull),调用manager_.customer(id)->executor()->Accept(msg)
Executor.cc:
Accept: recv_msgs_.push_back(msg),signal信号量;
Run循环, PickActiveMsg末尾的信号量被唤醒, 进入下一个PickActiveMsg;
PickActiveMsg: 扫描recv_msgs_;对里面每个msg:
发送方如果已死,则删掉该msg不处理了;
重复接收(XXXX_req_tracker.IsFinished(ts)),则删掉该msg不处理了;
对worker发往server的请求, 检查其所有的依赖(wait_time)是否完成(recv_req_tracker.IsFinished),都完成了就可以处理它了!
处理: 设该消息为active_msg_;从recv_msgs_中删去该消息; 调Run循环里的ProcessActiveMsg
ProcessActiveMsg:
对worker发往server的请求, 调用obj_.ProcessRequest;在tracker中记录该消息Finished; 对于Push操作调用obj_.Reply发送ACK
Kv_store.h:
ProcessRequest:HandlePush/ HandlePull; 对于Pull操作,发送response消息;(对于Push操作由后面调用端ProcessActiveMsg发送ACK;
HandlePush/HandlePull: 最后都调FinishReceivedRequest把消息Finish记在tracker了;
Worker端:
Postoffice.h: 同上;
Executor.cc:
Accept:同上;
Run循环: 同上;
PickActiveMsg: 同上,唯一不同的就是不用检查依赖了;
ProcessActiveMsg:对server发往worker的response: 调用obj_.ProcessResponse处理; Finish(ts); sent_reqs_.find(ts)拿到它; 判断ServerGroup里所有的Server的该ts消息都Finish了,才彻底Finish这条发往ServerGroup的消息并调用callback; notify信号量sent_req_cond_(Wait操作就是卡在他上面)
Kv_Cache.h:
ProcessResponse:对push操作返回的ACK,不处理;
对pull操作返回的数据: 拿到pull_data_里对应的东西; 调用ParallelOrderedMatch把本次受到的数据合并到buffer里面去
Wait操作:
Worker.h: Wait, 调用KVCache::Wait
App.h:
Wait:Customer::Wait, 调用Executor::WaitSentReq
Executor.cc:
WaitSentReq :Wait在sent_req_cond_上,唤醒后则CheckFinished(rnode, timestamp, false)
=================================================
使用经验:
Worker采用Wait方式+多线程并发,一开始跑起来的时候性能差,因为每个任务的任务量(计算量和通信量)几乎相等,所以(计算-》Pull-》计算-》Push)的流程,所有线程一上来都卡在计算上,CPU满但是网络为空,然后所有线程都卡在Pull的通信上,网络为满但是CPU为空;增大线程个数使得线程数为CPU个数的1.5~2倍后,性能有所改善,因为顺序被打乱了,原先近乎齐步走;理想方式:采用callback异步,且确保callback是被后台多线程并发执行的;如果callback被后台用单线程实现,那必须把callback里的计算操作enqueue给线程池去做;
ps-lite要求所有Keys按从小到大的顺序交给Push或Pull,为的是按Range区间均匀划分至各个Parameter Server;每个Server内部,开启多线程的话(默认是单线程,压力大的情况会造成单CPU性能瓶颈),也会按Range来均匀划分至各个thread;如果Keys没有严格Hash好,不均匀或者取值范围在uint64内只占一部分,那就会被影射到少数Server的少数thread上,导致负载极其不均衡;
===================================================================================
基础知识: