https://wenku.baidu.com/view/65df554068eae009581b6bd97f1922791688be8a.html
https://blog.csdn.net/skdkjzz/article/details/43278517
https://www.xsky.com/tec/ceph-osdmap/
https://blog.csdn.net/changtao381/article/details/72590476
https://blog.csdn.net/ygtlovezf/article/details/72330822
ceph源码分析-(常涛)
1.主osd发送给副本osd,副本osd在journal写完之前挂了,怎么处理这个IO?
2.PG对应的osd set为[0,1],完成写数据之后,又新加了新的OSD,对应的set变为[0,1,2,3],此时2和3上还没有之前写的数据,而PG如何从2,3读取到数据?
3.单个osd请求爆炸,如何针对?throttle阻塞发生怎么办?IO是否一直等待?
4.如何观测已经发生了throttle阻塞?
5.ceph线上有哪些调试工具?
6.OSD启动时会建立几个会话连接?几个心跳?
以下内容不一定正确,只是学习记录,仅供参考:
1.主osd发送给副本osd,副本osd在journal写完之前挂了,怎么处理这个IO?
ceph的读写没有超时机制,但是osd有一个机制,会检测某个线程的执行时间,超时会suicide.
osd端正常的写操作流程中,在函数ReplicatedBackend::submit_transaction会把请求加入in_progress_ops中,该map保存了所有正在处理的请求。
一个请求必须等所有的up的osd的请求都返回才能返回给客户端。
如果在写的过程中,副本journal写失败,monitor会通过heartbeat检测到该osd的状态变化,引起了osdmap的变化。
monitor会把最新的osd map推送给集群中的其他节点。导致受影响的PG重新进行peering操作。
OSD::handle_osd_map负责处理osdmap的变化,调用consume_map,d对每一个PG调用pg->queue_null,把PG加入到peering_wq中。该队列的处理函数process_peering_events调用OSD::advance_pg函数,在该函数里调用PG::handle_advance_map给PG的状态机发送AdvMap事件,当处于Started状态,收到这个事件就会调用pg->should_restart_peering检查,如果是new_interval,就跳转到Reset状态,重新Peering。
peering过程中,PG::start_peering_interval函数会调用ReplicatedBackend::on_change(),将正在处理的请求从in_progress_ops中删除。
而客户端收到新的osdmap后,调用Objecter::handle_osd_map,会检查所有正在进行的请求,如果哪些请求受到了影响,会在客户端重发请求。
2.PG对应的osd 为0,1,完成写数据之后,又新加了新的OSD,此时2和3上还没有之前写的数据,而PG如何从2,3读取到数据?
首先:读操作有平衡读(从主osd)和本地读(主从均可)。这里讨论平衡读。
当pg对应的osd列表:acting和up都为[0,1]时,pg处于clean状态。
此时,新加入新的osd 2和3,引起crush重新分配pg,新加入的osd有可能成为新的主osd,也有可能成为副本osd。
如果成为主osd,则无法负担读操作,因为其上还没有之前的数据。
假定crush重新分配后,acting和up列表变为[2,0,1,3]此时需要选出拥有权威日志的osd,假定为osd0.
经过calc_replicated_acting算法,want列表为[0,2,1,3],acting_backfill为[0,2,1,3],want_backfill为[2,3]
want[0,2,1,3]不等于acting[2,0,1,3],并且不等于up[2,0,1,3]时,需要向Monitor申请pg_temp,也就是want
申请成功后,acting为[2,0,1,3],up为[0,2,1,3],osd0作为临时的主osd,处理读请求。
同时,osd0向osd2和3,发送之前的pg数据。等2和3处于clean状态,pg_temp取消,
此时acting和up都为[2,0,1,3].
3.单个osd请求爆炸,如何针对?throttle阻塞发生怎么办?IO是否一直等待?
有Throttle机制。
在JS中有两个概念函数节流(throttle)和函数去抖(debounce)
debounce:一般调用该动作后,一段时间后才会执行,如果在这段时间内,又调用该动作,将重新计算时间。
throttle:一般会预设一个周期,当调用该动作的时间大于预设的周期,才会执行,否则阻塞。
这两者都是通过减少实际逻辑处理过程的执行来提高时间处理函数运行性能的手段,实际上并没有减少时间触发的次数。
对于一个大型系统,上下游特别多,如果没有流量控制,很可能因为处理速度不匹配,导致大量事务积压在某处,导致系统处于不可控的状态。throttle其实是一个限速功能,我们关注的指标到了一定的值,我们就拒绝接受,让会导致指标上涨的进程或线程陷入等待,直到指标回落到正常的方位。
一般分为刚性throttle和柔性throttle。
刚性throttle:当到达阈值,之后的请求无限期阻塞,直到有空位留出。
柔性throttle:有两个阈值,当前指标如果处于低阈值之下,无需调节,立刻处理;如果高于低阈值,小于高阈值,则稍微调节,让线程或进程等待少量时间再处理请求;如果高于高阈值,则让线程或进程等待稍微长的时间再处理请求。
在ceph(0.94)中:刚性throttle
class Throttle {
CephContext *cct;
std::string name;
PerfCounters *logger;
ceph::atomic_t count, max;
Mutex lock;
list<Cond*> cond;
bool use_perf;
max:阈值
count:当前的请求数量
如果count超过max,就会阻塞
如果当前值加上请求的值超过上限,就返回true,等待。
bool Throttle::_wait(int64_t c)
{
utime_t start;
bool waited = false;
if (_should_wait(c) || !cond.empty()) { // always wait behind other waiters.
Cond *cv = new Cond;
cond.push_back(cv);
do {
if (!waited) {
ldout(cct, 2) << "_wait waiting..." << dendl;
if (logger)
start = ceph_clock_now(cct);
}
waited = true;
cv->Wait(lock);
} while (_should_wait(c) || cv != cond.front());
if (waited) {
ldout(cct, 3) << "_wait finished waiting" << dendl;
if (logger) {
utime_t dur = ceph_clock_now(cct) - start;
logger->tinc(l_throttle_wait, dur);
}
}
delete cv;
cond.pop_front();
// wake up the next guy
if (!cond.empty())
cond.front()->SignalOne();
}
return waited;
}
如果需要等待的话,将创建一个条件变量,推送到条件变量列表,然后通过cv->Wait函数等待在互斥量上。除非有资源释放,这时候,释放资源的人会调用cond.front()->SignalOne(),唤醒排在列表第一个等待变量上的线程。这就是先来先服务。也有可能存在虚假唤醒,醒来的时候,还要在此判断是否需要继续等待,也就是do while循环的条件部分。
如果等待结束,被唤醒了,那么会更新等待时间,同时删除条件变量
int64_t Throttle::put(int64_t c)
{
if (0 == max.read()) {
return 0;
}
assert(c >= 0);
ldout(cct, 10) << "put " << c << " (" << count.read() << " -> " << (count.read()-c) << ")" << dendl;
Mutex::Locker l(lock);
if (c) {
if (!cond.empty())
cond.front()->SignalOne();
assert(((int64_t)count.read()) >= c); //if count goes negative, we failed somewhere!
count.sub(c);
if (logger) {
logger->inc(l_throttle_put);
logger->inc(l_throttle_put_sum, c);
logger->set(l_throttle_val, count.read());
}
}
return count.read();
}
如果线程释放了资源,还需要通知陷入阻塞的线程(如果存在的话),还要更新count的值。
如果有很多线程被阻塞,都通过条件变量等待满足条件,Throttle::put会唤醒condition_variable列表的第一个线程,而第一个线程在Throttle::_wait会尝试唤醒下一个。释放资源可能不止释放一个,而在等待的线程所需的资源个数也不尽相同,如果第一个线程被唤醒,那么它负责唤醒第二个,第二个唤醒第三个,直到某个被唤醒的线程发现剩余的资源不足,就会再次陷入阻塞。
ceph后续版本也已经实现了柔性throttle:BackoffThrottle(它会根据情况决定是死等还是等待有限时间)
c除此之外还有一个简单的SimpleThrottle(死等)。
4.如何观测已经发生了throttle阻塞?
Ceph自家的Calamari长得不错,但是不够实用,而且它的部署、打包还不完善。通常实用以下工具:
- 收集:使用diamond,增加新的colloctor,用于收集更详细的数据。
- 保存:使用graphite,设置好采集精度和保存精度。
- 展示:使用grafana,挑了十几个工具,发现还是grafana好看好用。
- 报警:zabbix agent && ceph health
通过diamond监控OSD可以得到 throttle的监控数据,还有IOPS、吞吐率、OSD Journal延迟、读请求延迟、容量使用率等。(本人还没用过)。
5.ceph线上有哪些调试工具?
6.OSD启动时会建立几个会话连接?几个心跳?
七个messenger,四个和心跳相关,两个dispatchr
messenger | ||
*ms_public | 用来处理OSD和Client之间的消息 | |
*ms_cluster | 用来处理OSD和集群之间的消息 | |
心跳 | *ms_hb_front_client | 用来向其它OSD发送心跳的消息 |
*ms_hb_back_client | 用来向其它OSD发送心跳的消息 | |
*ms_hb_back_server | 用来接收其他OSD的心跳消息 | |
*ms_hb_front_server |
用来接收其他OSD的心跳消息 | |
*ms_objecter | 用来处理OSD和Objecter之间的消息 | |
dispatcher | ||
*OSD | 可以处理部分osd节点的消息 | |
*heartbeat_dispatcher | 处理心跳连接 |