ceph のピアリング プロセスは非常に複雑なプロセスであり、その主な目的は PG の OSD を一貫した状態に到達させることです。マスター/スレーブ レプリカが一貫した状態に達すると、PG はアクティブ状態になり、ピアリング プロセスの状態は終了します。しかし現時点では、PG の 3 つの OSD コピーのデータは完全には一致していません。
PG は、次の 2 つの状況でピアリング プロセスをトリガーします。
-
システムが初期化されると、OSD が再起動して PG がリロードされます。または、PG が新しく作成されると、PG はピアリング プロセスを開始します。
-
OSD に障害が発生した場合、OSD の追加または削除によって PG の動作セットが変更され、PG はピアリング プロセスを再開始します。
1. 基本的な考え方
ピアリングについて詳しく説明する前に、最初にいくつかの概念を紹介します。
1.1 演技セットとセットアップセット
動作セットは、PG のコピーが配置されている OSD のリストです。リストは順序付けされており、リスト内の最初の OSD がマスター OSD になります。通常の状況では、アップ セットと演技セットのリストはまったく同じですが、それらが存在する場合にのみ、pg temp
この 2 つは異なります。
まず、動作セットとアップセットの計算プロセスを見てみましょう。
/**
* map a pg to its acting set as well as its up set. You must use
* the acting set for data mapping purposes, but some users will
* also find the up set useful for things like deciding what to
* set as pg_temp.
* Each of these pointers must be non-NULL.
*/
void pg_to_up_acting_osds(pg_t pg, vector<int> *up, int *up_primary,
vector<int> *acting, int *acting_primary) const {
_pg_to_up_acting_osds(pg, up, up_primary, acting, acting_primary);
}
上記のコメントから、演技セットは主にデータ マッピングに使用されることがわかりますが、アップ セットはクラッシュ マップに従ってのみ計算され、一部のユーザーにとっては重要な役割を果たすこともわかります。セットアップが完了すると、pg_temp が何であるかがわかります。
_pg_to_up_acting_osds() 関数の実装を見てみましょう。
void OSDMap::_pg_to_up_acting_osds(const pg_t& pg, vector<int> *up, int *up_primary,
vector<int> *acting, int *acting_primary) const
{
const pg_pool_t *pool = get_pg_pool(pg.pool());
if (!pool) {
if (up)
up->clear();
if (up_primary)
*up_primary = -1;
if (acting)
acting->clear();
if (acting_primary)
*acting_primary = -1;
return;
}
vector<int> raw;
vector<int> _up;
vector<int> _acting;
int _up_primary;
int _acting_primary;
ps_t pps;
_pg_to_osds(*pool, pg, &raw, &_up_primary, &pps);
_raw_to_up_osds(*pool, raw, &_up, &_up_primary);
_apply_primary_affinity(pps, *pool, &_up, &_up_primary);
_get_temp_osds(*pool, pg, &_acting, &_acting_primary);
if (_acting.empty()) {
_acting = _up;
if (_acting_primary == -1) {
_acting_primary = _up_primary;
}
}
if (up)
up->swap(_up);
if (up_primary)
*up_primary = _up_primary;
if (acting)
acting->swap(_acting);
if (acting_primary)
*acting_primary = _acting_primary;
}
上記から、PG マッピングには複数の手順があることがわかります。
1) _pg_to_osds()
int OSDMap::_pg_to_osds(const pg_pool_t& pool, pg_t pg,
vector<int> *osds, int *primary,ps_t *ppps) const
{
// map to osds[]
ps_t pps = pool.raw_pg_to_pps(pg); // placement ps
unsigned size = pool.get_size();
// what crush rule?
int ruleno = crush->find_rule(pool.get_crush_ruleset(), pool.get_type(), size);
if (ruleno >= 0)
crush->do_rule(ruleno, pps, *osds, size, osd_weight);
_remove_nonexistent_osds(pool, *osds);
*primary = -1;
for (unsigned i = 0; i < osds->size(); ++i) {
if ((*osds)[i] != CRUSH_ITEM_NONE) {
*primary = (*osds)[i];
break;
}
}
if (ppps)
*ppps = pps;
return osds->size();
}
bool exists(int osd) const
{
//assert(osd >= 0);
return osd >= 0 && osd < max_osd && (osd_state[osd] & CEPH_OSD_EXISTS);
}
void OSDMap::_remove_nonexistent_osds(const pg_pool_t& pool,
vector<int>& osds) const
{
if (pool.can_shift_osds()) {
unsigned removed = 0;
for (unsigned i = 0; i < osds.size(); i++) {
if (!exists(osds[i])) {
removed++;
continue;
}
if (removed) {
osds[i - removed] = osds[i];
}
}
if (removed)
osds.resize(osds.size() - removed);
} else {
for (vector<int>::iterator p = osds.begin(); p != osds.end(); ++p) {
if (!exists(*p))
*p = CRUSH_ITEM_NONE;
}
}
}
_pg_to_osds() 関数の実装は比較的単純で、PG がマッピングされている OSD をクラッシュマップから取得します。ここで、crushmap から取得したマッピングは OSD の動作状態とは関係がないことに注意してください (ウェイトに関係します。OSD がアウトになるとウェイトは 0 になり、クラッシュすると OSD にマッピングされなくなります) ->do_rule()が実行されます。次に、_remove_nonexistent_osds() 関数を呼び出して、この OSDMap に存在しない OSD を削除します。
_pg_to_osds() 関数は、最も基本的な PG から OSD へのマッピングを返し、最初のマッピングがプライマリになります。
2) _raw_to_up_osds()
void OSDMap::_raw_to_up_osds(const pg_pool_t& pool, const vector<int>& raw,
vector<int> *up, int *primary) const
{
if (pool.can_shift_osds()) {
// shift left
up->clear();
for (unsigned i=0; i<raw.size(); i++) {
if (!exists(raw[i]) || is_down(raw[i]))
continue;
up->push_back(raw[i]);
}
*primary = (up->empty() ? -1 : up->front());
} else {
// set down/dne devices to NONE
*primary = -1;
up->resize(raw.size());
for (int i = raw.size() - 1; i >= 0; --i) {
if (!exists(raw[i]) || is_down(raw[i])) {
(*up)[i] = CRUSH_ITEM_NONE;
} else {
*primary = (*up)[i] = raw[i];
}
}
}
}
_pg_to_osds() 関数を通じて、元の PG から OSD へのマッピング関係を取得します。_raw_to_up_osds() 関数は、主に OSDMap でダウン状態の OSD を削除します。つまり、元のアップ OSD を取得し、最初の OSD をアップ プライマリとして設定します。
3) _apply_primary_affinity()
この機能は主にプライマリ アフィニティの設定を扱いますが、ここでは紹介しません(デフォルトでは設定されていません)。
4) _get_temp_osds()
void OSDMap::_get_temp_osds(const pg_pool_t& pool, pg_t pg,
vector<int> *temp_pg, int *temp_primary) const
{
pg = pool.raw_pg_to_pg(pg);
map<pg_t,vector<int32_t> >::const_iterator p = pg_temp->find(pg);
temp_pg->clear();
if (p != pg_temp->end()) {
for (unsigned i=0; i<p->second.size(); i++) {
if (!exists(p->second[i]) || is_down(p->second[i])) {
if (pool.can_shift_osds()) {
continue;
} else {
temp_pg->push_back(CRUSH_ITEM_NONE);
}
} else {
temp_pg->push_back(p->second[i]);
}
}
}
map<pg_t,int32_t>::const_iterator pp = primary_temp->find(pg);
*temp_primary = -1;
if (pp != primary_temp->end()) {
*temp_primary = pp->second;
} else if (!temp_pg->empty()) { // apply pg_temp's primary
for (unsigned i = 0; i < temp_pg->size(); ++i) {
if ((*temp_pg)[i] != CRUSH_ITEM_NONE) {
*temp_primary = (*temp_pg)[i];
break;
}
}
}
}
_get_temp_osds() は、指定された OSDMap で指定された PG に対応する temp_pg にマッピングされた OSD を取得するために使用されます。
5) PG のアップセットとアクティングセットのマッピングを完了する
_pg_to_up_acting_osds() 関数の最後に戻りましょう。現在 temp_pg がない場合、PG の動作セットがアップ セットになります。
アップ セットと動作セットに関しては、次のように結論付けることができます: アップ セットはクラッシュマップに従って直接計算され、信頼できるログとは何の関係もありません; そして、動作セットは、読み取り時に実際に依存する OSD シーケンスです。データの書き込み: OSD はプライマリとしてデータの読み取りと書き込みを担当するため、信頼できるログが必要です。
1.2 一時的なPG
初期状態の PG のup set
合計がacting set
[0,1,2] のリストであるとします。このとき、OSD0 が失敗した場合は、ピアリング動作を再開始します。CRUSH アルゴリズムによれば、アップ セットは [3,1,2]、アクティブ セットも [3,1,2] となります。ただし、osd3は、OSD::choose_acting() のときに見つかります。 新しく追加された OSD は、PG で読み取り操作を行うことができず、PG のメイン OSD になることができません。したがって、PG は一時的な PG の Monitor に適用され、osd1 が一時的なメイン OSD になります。このときup set
[3,1,2] であったのがacting set
[1,2,3] となり、状況がup set
異なりacting set
ます。osd3 がバックフィル プロセスを完了すると、一時的な PG はキャンセルされ、PG の動作セットは [3,2,1] に変更され、アップ セットと動作セットは再び等しくなります。
ceph の関連ソースコードを読むと、次の 2 つの場合に pg_temp が生成されることがわかります。
-
OSD はアクティブにオフラインになり、モニターはオフライン メッセージを検出すると、OSD の目的の PG の PG プライマリの pg_temp を適用します。
-
PG は、ピアリング プロセス中に要求と動作が矛盾していることを検出すると、pg_temp を積極的に適用します。
以下では、これら 2 つのケースを個別に紹介します。
1.2.1 ピアリングプロセスは pg_temp にアクティブに適用されます
OSD::queue_pg_temp() 関数を検索したところ、次の 2 つの呼び出しがあることがわかりました。
bool PG::choose_acting(pg_shard_t &auth_log_shard_id,
bool restrict_to_up_acting,
bool *history_les_bound)
{
...
if (auth_log_shard == all_info.end()) {
if (up != acting) {
dout(10) << "choose_acting no suitable info found (incomplete backfills?),"<< " reverting to up" << dendl;
want_acting = up;
vector<int> empty;
osd->queue_want_pg_temp(info.pgid.pgid, empty);
} else {
dout(10) << "choose_acting failed" << dendl;
assert(want_acting.empty());
}
return false;
}
...
if (want != acting) {
dout(10) << "choose_acting want " << want << " != acting " << acting<< ", requesting pg_temp change" << dendl;
want_acting = want;
if (want_acting == up) {
// There can't be any pending backfill if
// want is the same as crush map up OSDs.
assert(compat_mode || want_backfill.empty());
vector<int> empty;
osd->queue_want_pg_temp(info.pgid.pgid, empty);
} else
osd->queue_want_pg_temp(info.pgid.pgid, want);
return false;
}
...
}
void PG::start_peering_interval(
const OSDMapRef lastmap,
const vector<int>& newup, int new_up_primary,
const vector<int>& newacting, int new_acting_primary,
ObjectStore::Transaction *t)
{
...
if (acting.empty() && !up.empty() && up_primary == pg_whoami) {
dout(10) << " acting empty, but i am up[0], clearing pg_temp" << dendl;
osd->queue_want_pg_temp(info.pgid.pgid, acting);
}
}
上記のコードでは、OSD::queue_want_pg_temp() が呼び出され、PG によって要求された動作セットをキューに入れます。
注: 要求された動作セットが空の場合、対応する pg temp がクリアされることを意味します。
OSD::queue_want_pg_temp() の実装と、対応するリクエストがどのように送信されるかを見てみましょう。
void OSDService::queue_want_pg_temp(pg_t pgid, vector<int>& want)
{
Mutex::Locker l(pg_temp_lock);
map<pg_t,vector<int> >::iterator p = pg_temp_pending.find(pgid);
if (p == pg_temp_pending.end() || p->second != want) {
pg_temp_wanted[pgid] = want;
}
}
このうち、OSDService::pg_temp_pending は、現在 pg_temp リクエストを Monitor に送信しているマッピング テーブルを格納するために使用され已经
、OSDService::pg_temp_wanted は、将要
pg_temp リクエストを開始するマッピング テーブルを示します。具体的な送信関数を見てみましょう。
void OSDService::send_pg_temp()
{
Mutex::Locker l(pg_temp_lock);
if (pg_temp_wanted.empty())
return;
dout(10) << "send_pg_temp " << pg_temp_wanted << dendl;
MOSDPGTemp *m = new MOSDPGTemp(osdmap->get_epoch());
m->pg_temp = pg_temp_wanted;
monc->send_mon_message(m);
_sent_pg_temp();
}
void OSDService::_sent_pg_temp()
{
for (map<pg_t,vector<int> >::iterator p = pg_temp_wanted.begin();p != pg_temp_wanted.end();++p)
pg_temp_pending[p->first] = p->second;
pg_temp_wanted.clear();
}
void OSD::ms_handle_connect(Connection *con)
{
...
service.requeue_pg_temp();
service.send_pg_temp();
...
}
void OSD::process_peering_events(
const list<PG*> &pgs,
ThreadPool::TPHandle &handle
)
{
...
service.send_pg_temp();
}
OSDService::send_pg_temp() 関数では、pg_temp_wanted 内のすべてのマッピング テーブルが OSDMonitor に送信されます。この関数を見つけて、次の 2 つの場所で呼び出されることを確認します。
-
OSD::ms_handle_connect(): OSD がモニターに再接続すると、以前の pg_temp が再度キューに入れられ、再送信されます。これは、前回の pg_temp が送信されているかどうかが不明なため、ここで再送信します。
-
OSD::process_peering_events(): ピアリングプロセス中に必要な pg_temp を送信します。
1.2.2 OSDMonitor は、OSD がアクティブにオフラインであることを検出すると、pg_temp を自動的に生成します
OSD がアクティブにオフラインになると、次のように MOSDMarkMeDown メッセージがモニターに送信されます。
int OSD::shutdown()
{
if (!service.prepare_to_stop())
return 0; // already shutting down
....
}
bool OSDService::prepare_to_stop()
{
Mutex::Locker l(is_stopping_lock);
if (get_state() != NOT_STOPPING)
return false;
OSDMapRef osdmap = get_osdmap();
if (osdmap && osdmap->is_up(whoami)) {
dout(0) << __func__ << " telling mon we are shutting down" << dendl;
set_state(PREPARING_TO_STOP);
monc->send_mon_message(new MOSDMarkMeDown(monc->get_fsid(),
osdmap->get_inst(whoami),
osdmap->get_epoch(),
true // request ack
));
utime_t now = ceph_clock_now(cct);
utime_t timeout;
timeout.set_from_double(now + cct->_conf->osd_mon_shutdown_timeout);
while ((ceph_clock_now(cct) < timeout) &&
(get_state() != STOPPING)) {
is_stopping_cond.WaitUntil(is_stopping_lock, timeout);
}
}
dout(0) << __func__ << " starting shutdown" << dendl;
set_state(STOPPING);
return true;
}
Monitor はメッセージを受信すると、応答を処理します (Monitor クラスは Dispatcher から継承するため、メッセージを配布できます)。以下のプロセスを見てみましょう。
void Monitor::_ms_dispatch(Message *m){
...
if ((is_synchronizing() || (s->global_id == 0 && !exited_quorum.is_zero())) &&
!src_is_mon && m->get_type() != CEPH_MSG_PING) {
waitlist_or_zap_client(op);
} else {
dispatch_op(op);
}
...
}
void Monitor::dispatch_op(MonOpRequestRef op){
...
switch (op->get_req()->get_type()) {
// OSDs
case CEPH_MSG_MON_GET_OSDMAP:
case MSG_OSD_MARK_ME_DOWN:
case MSG_OSD_FAILURE:
case MSG_OSD_BOOT:
case MSG_OSD_ALIVE:
case MSG_OSD_PGTEMP:
case MSG_REMOVE_SNAPS:
paxos_service[PAXOS_OSDMAP]->dispatch(op);
break;
...
}
...
}
この時点で、OSDMonitor のdispatch() 関数を呼び出してメッセージを配信します。
bool PaxosService::dispatch(MonOpRequestRef op){
....
// update
if (prepare_update(op)) {
double delay = 0.0;
if (should_propose(delay)) {
if (delay == 0.0) {
propose_pending();
} else {
// delay a bit
if (!proposal_timer) {
/**
* Callback class used to propose the pending value once the proposal_timer
* fires up.
*/
proposal_timer = new C_MonContext(mon, [this](int r) {
proposal_timer = 0;
if (r >= 0)
propose_pending();
else if (r == -ECANCELED || r == -EAGAIN)
return;
else
assert(0 == "bad return value for proposal_timer");
});
dout(10) << " setting proposal_timer " << proposal_timer << " with delay of " << delay << dendl;
mon->timer.add_event_after(delay, proposal_timer);
} else {
dout(10) << " proposal_timer already set" << dendl;
}
}
} else {
dout(10) << " not proposing" << dendl;
}
}
return true;
}
pg_temp を生成する具体的なプロセスを見てみましょう。
1) MSG_OSD_MARK_ME_DOWN メッセージを処理する
bool OSDMonitor::prepare_update(MonOpRequestRef op)
{
...
switch (m->get_type()) {
// damp updates
case MSG_OSD_MARK_ME_DOWN:
return prepare_mark_me_down(op);
....
}
return false;
}
bool OSDMonitor::prepare_mark_me_down(MonOpRequestRef op)
{
op->mark_osdmon_event(__func__);
MOSDMarkMeDown *m = static_cast<MOSDMarkMeDown*>(op->get_req());
int target_osd = m->get_target().name.num();
assert(osdmap.is_up(target_osd));
assert(osdmap.get_addr(target_osd) == m->get_target().addr);
mon->clog->info() << "osd." << target_osd << " marked itself down\n";
pending_inc.new_state[target_osd] = CEPH_OSD_UP;
if (m->request_ack)
wait_for_finished_proposal(op, new C_AckMarkedDown(this, op));
return true;
}
上記のように、関数内で提案 C_AckMarkedDown が構築され、waiting_for_finished_proposal リストに挿入され、提案に投票できるようになります。Paxos は各プロポーザルを永続化する必要があるため、次のように呼び出します。
void OSDMonitor::encode_pending(MonitorDBStore::TransactionRef t){
...
if (g_conf->mon_osd_prime_pg_temp)
maybe_prime_pg_temp();
...
}
ここを追跡すると、maybe_prime_pg_temp() がアクティブ シャットダウン OSD の pg_temp を事前生成する可能性があることがわかりました。この関数の実装を見てみましょう。
2)OSDMonitor::maybe_prime_pg_temp()
void OSDMonitor::maybe_prime_pg_temp(){
...
utime_t stop = ceph_clock_now(NULL);
stop += g_conf->mon_osd_prime_pg_temp_max_time;
...
if(all){
...
}else{
dout(10) << __func__ << " " << osds.size() << " interesting osds" << dendl;
for (set<int>::iterator p = osds.begin(); p != osds.end(); ++p) {
n -= prime_pg_temp(next, pg_map, *p);
if (--n <= 0) {
n = chunk;
if (ceph_clock_now(NULL) > stop) {
dout(10) << __func__ << " consumed more than "<< g_conf->mon_osd_prime_pg_temp_max_time
<< " seconds, stopping"<< dendl;
break;
}
}
}
}
}
上記の関数は、prime_pg_temp() を呼び出して、指定された OSD の pg_temp を構築します。
int OSDMonitor::prime_pg_temp(OSDMap& next, PGMap *pg_map, int osd)
{
dout(10) << __func__ << " osd." << osd << dendl;
int num = 0;
ceph::unordered_map<int, set<pg_t> >::iterator po = pg_map->pg_by_osd.find(osd);
if (po != pg_map->pg_by_osd.end()) {
for (set<pg_t>::iterator p = po->second.begin();p != po->second.end();++p, ++num) {
ceph::unordered_map<pg_t, pg_stat_t>::iterator pp = pg_map->pg_stat.find(*p);
if (pp == pg_map->pg_stat.end())
continue;
prime_pg_temp(next, pp);
}
}
return num;
}
void OSDMonitor::prime_pg_temp(OSDMap& next,
ceph::unordered_map<pg_t, pg_stat_t>::iterator pp)
{
// do not prime creating pgs
if (pp->second.state & PG_STATE_CREATING)
return;
// do not touch a mapping if a change is pending
if (pending_inc.new_pg_temp.count(pp->first))
return;
vector<int> up, acting;
int up_primary, acting_primary;
next.pg_to_up_acting_osds(pp->first, &up, &up_primary, &acting, &acting_primary);
if (acting == pp->second.acting)
return; // no change since last pg update, skip
vector<int> cur_up, cur_acting;
osdmap.pg_to_up_acting_osds(pp->first, &cur_up, &up_primary,&cur_acting, &acting_primary);
if (cur_acting == acting)
return; // no change this epoch; must be stale pg_stat
if (cur_acting.empty())
return; // if previously empty now we can be no worse off
const pg_pool_t *pool = next.get_pg_pool(pp->first.pool());
if (pool && cur_acting.size() < pool->min_size)
return; // can be no worse off than before
dout(20) << __func__ << " " << pp->first << " " << cur_up << "/" << cur_acting<< " -> " << up << "/" << acting
<< ", priming " << cur_acting<< dendl;
pending_inc.new_pg_temp[pp->first] = cur_acting;
}
上記は、まず OSD 上の各 PG を走査し、次に対応する条件に従って pg_temp を生成します。
1.3 pg_history_t データ構造
/**
* pg_history_t - information about recent pg peering/mapping history
*
* This is aggressively shared between OSDs to bound the amount of past
* history they need to worry about.
*/
struct pg_history_t {
epoch_t epoch_created; // epoch in which PG was created
epoch_t last_epoch_started; // lower bound on last epoch started (anywhere, not necessarily locally)
epoch_t last_epoch_clean; // lower bound on last epoch the PG was completely clean.
epoch_t last_epoch_split; // as parent
epoch_t last_epoch_marked_full; // pool or cluster
/**
* In the event of a map discontinuity, same_*_since may reflect the first
* map the osd has seen in the new map sequence rather than the actual start
* of the interval. This is ok since a discontinuity at epoch e means there
* must have been a clean interval between e and now and that we cannot be
* in the active set during the interval containing e.
*/
epoch_t same_up_since; // same acting set since
epoch_t same_interval_since; // same acting AND up set since
epoch_t same_primary_since; // same primary at least back through this epoch.
eversion_t last_scrub;
eversion_t last_deep_scrub;
utime_t last_scrub_stamp;
utime_t last_deep_scrub_stamp;
utime_t last_clean_scrub_stamp;
};
pg_history_t データ構造は PG の実行プロセスにおいて重要な役割を果たし、一般に発生した内容を記録するために使用され、一定の権限を持ち、OSD 間で共有できます。
-
epoch_created: PGMonitor によって生成された、PG 作成時のエポック値
-
last_epoch_started: PG が最後にアクティブ化されたときのエポック値 (activate)
-
last_epoch_clean: PG が最後にクリーン状態になったときのエポック値
-
Same_up_since: 現在のアップセットが表示された最も早い瞬間を記録します。
-
Same_interval_since: 間隔の最初の osdmap バージョン番号を示します
1.4 past_interval の概要
いわゆる、一連past_interval
の osdmap バージョン番号エポックであり、PG の動作セットとアップ セットは変更されません。
/**
* pg_interval_t - information about a past interval
*/
struct pg_interval_t {
vector<int32_t> up, acting;
epoch_t first, last;
bool maybe_went_rw;
int32_t primary;
int32_t up_primary;
pg_interval_t()
: first(0), last(0),maybe_went_rw(false),primary(-1),up_primary(-1)
{}
};
class PG : DoutPrefixProvider {
public:
map<epoch_t,pg_interval_t> past_intervals;
};
各 PG は、キーがpast_interval
最初のエポック値である past_intervals マッピング テーブルを維持します。以下に、pg_interval_t の各フィールドの値を簡単に紹介します。
-
pg_interval_t::up => PG はインターバル中に設定されます。
-
pg_interval_t::acting => インターバル中の PG の動作セット
-
pg_interval_t::first => 間隔の最初のエポック値
-
pg_interval_t::last => 間隔の最後のエポック値
-
pg_interval_t::maybe_went_rw => この間隔中に書き込み操作が実行された可能性があることを示します
-
pg_interval_t::primary => インターバル中に PG がプライマリとして動作する
-
pg_interval_t::up_primary => インターバル中にプライマリをPGアップします
1.4.1 pg_interval_t のいくつかの重要な関数
1) is_new_interval(): バージョン 1
bool pg_interval_t::is_new_interval(
int old_acting_primary,
int new_acting_primary,
const vector<int> &old_acting,
const vector<int> &new_acting,
int old_up_primary,
int new_up_primary,
const vector<int> &old_up,
const vector<int> &new_up,
int old_size,
int new_size,
int old_min_size,
int new_min_size,
unsigned old_pg_num,
unsigned new_pg_num,
bool old_sort_bitwise,
bool new_sort_bitwise,
pg_t pgid) {
return old_acting_primary != new_acting_primary ||
new_acting != old_acting ||
old_up_primary != new_up_primary ||
new_up != old_up ||
old_min_size != new_min_size ||
old_size != new_size ||
pgid.is_split(old_pg_num, new_pg_num, 0) ||
old_sort_bitwise != new_sort_bitwise;
}
上記のことから、PG のアップ セットおよび動作セットのいずれかが変更されるか、PG が分割される限り、新しい間隔が生成されることがわかります。
2) is_new_interval(): バージョン 2
bool pg_interval_t::is_new_interval(
int old_acting_primary,
int new_acting_primary,
const vector<int> &old_acting,
const vector<int> &new_acting,
int old_up_primary,
int new_up_primary,
const vector<int> &old_up,
const vector<int> &new_up,
OSDMapRef osdmap,
OSDMapRef lastmap,
pg_t pgid) {
return !(lastmap->get_pools().count(pgid.pool())) ||
is_new_interval(old_acting_primary,
new_acting_primary,
old_acting,
new_acting,
old_up_primary,
new_up_primary,
old_up,
new_up,
lastmap->get_pools().find(pgid.pool())->second.size,
osdmap->get_pools().find(pgid.pool())->second.size,
lastmap->get_pools().find(pgid.pool())->second.min_size,
osdmap->get_pools().find(pgid.pool())->second.min_size,
lastmap->get_pg_num(pgid.pool()),
osdmap->get_pg_num(pgid.pool()),
lastmap->test_flag(CEPH_OSDMAP_SORTBITWISE),
osdmap->test_flag(CEPH_OSDMAP_SORTBITWISE),
pgid);
}
上記のことから、現在の PG が配置されているプールが lastmap に含まれていない場合、それは新しい間隔であることがわかります。さらに、PG のアップ セットと動作セットのいずれかが変更された場合、または PG が変更された場合、それは新しい間隔になります。分割すると、新しい間隔が生成されます。
3)check_new_interval()
bool pg_interval_t::check_new_interval(
int old_acting_primary, ///< [in] primary as of lastmap
int new_acting_primary, ///< [in] primary as of osdmap
const vector<int> &old_acting, ///< [in] acting as of lastmap
const vector<int> &new_acting, ///< [in] acting as of osdmap
int old_up_primary, ///< [in] up primary of lastmap
int new_up_primary, ///< [in] up primary of osdmap
const vector<int> &old_up, ///< [in] up as of lastmap
const vector<int> &new_up, ///< [in] up as of osdmap
epoch_t same_interval_since, ///< [in] as of osdmap
epoch_t last_epoch_clean, ///< [in] current
OSDMapRef osdmap, ///< [in] current map
OSDMapRef lastmap, ///< [in] last map
pg_t pgid, ///< [in] pgid for pg
IsPGRecoverablePredicate *could_have_gone_active, /// [in] predicate whether the pg can be active
map<epoch_t, pg_interval_t> *past_intervals, ///< [out] intervals
std::ostream *out) ///< [out] debug ostream
{
// remember past interval
// NOTE: a change in the up set primary triggers an interval
// change, even though the interval members in the pg_interval_t
// do not change.
if (is_new_interval(
old_acting_primary,
new_acting_primary,
old_acting,
new_acting,
old_up_primary,
new_up_primary,
old_up,
new_up,
osdmap,
lastmap,
pgid)) {
pg_interval_t& i = (*past_intervals)[same_interval_since];
i.first = same_interval_since;
i.last = osdmap->get_epoch() - 1;
assert(i.first <= i.last);
i.acting = old_acting;
i.up = old_up;
i.primary = old_acting_primary;
i.up_primary = old_up_primary;
unsigned num_acting = 0;
for (vector<int>::const_iterator p = i.acting.begin(); p != i.acting.end();++p)
if (*p != CRUSH_ITEM_NONE)
++num_acting;
const pg_pool_t& old_pg_pool = lastmap->get_pools().find(pgid.pool())->second;
set<pg_shard_t> old_acting_shards;
old_pg_pool.convert_to_pg_shards(old_acting, &old_acting_shards);
if (num_acting && i.primary != -1 && num_acting >= old_pg_pool.min_size && (*could_have_gone_active)(old_acting_shards)) {
if (out)
*out << "generate_past_intervals " << i<< ": not rw," << " up_thru " << lastmap->get_up_thru(i.primary)
<< " up_from " << lastmap->get_up_from(i.primary)<< " last_epoch_clean " << last_epoch_clean << std::endl;
if (lastmap->get_up_thru(i.primary) >= i.first && lastmap->get_up_from(i.primary) <= i.first) {
i.maybe_went_rw = true;
if (out)
*out << "generate_past_intervals " << i << " : primary up " << lastmap->get_up_from(i.primary)
<< "-" << lastmap->get_up_thru(i.primary) << " includes interval"<< std::endl;
} else if (last_epoch_clean >= i.first && last_epoch_clean <= i.last) {
// If the last_epoch_clean is included in this interval, then
// the pg must have been rw (for recovery to have completed).
// This is important because we won't know the _real_
// first_epoch because we stop at last_epoch_clean, and we
// don't want the oldest interval to randomly have
// maybe_went_rw false depending on the relative up_thru vs
// last_epoch_clean timing.
i.maybe_went_rw = true;
if (out)
*out << "generate_past_intervals " << in<< " : includes last_epoch_clean " << last_epoch_clean
<< " and presumed to have been rw" << std::endl;
} else {
i.maybe_went_rw = false;
if (out)
*out << "generate_past_intervals " << i << " : primary up " << lastmap->get_up_from(i.primary)
<< "-" << lastmap->get_up_thru(i.primary) << " does not include interval" << std::endl;
}
} else {
i.maybe_went_rw = false;
if (out)
*out << "generate_past_intervals " << i << " : acting set is too small" << std::endl;
}
return true;
} else {
return false;
}
}
注: Same_interval_since は、間隔の最初の osdmap バージョン番号を表します。
lastmap
この関数は、と を比較することでosdmap
現在の間隔が新しい間隔であるかどうかを判断し、新しい場合は lastmap をpast_intervals
その中に入れてから、 past_interval が書き込み操作を実行した可能性があるかどうかを判断します。past_interval に動作セットがあり、その数が対応するプールの min_size に達し、old_acting_shards がアクティブ状態に入るのに十分な場合:
-
lastmap での PG のプライマリ OSD の値が
up_thru
past_interval.first 以上で、OSD の up_from 値が past_interval.first 以下である場合、書き込み操作が発生した可能性があるため、past_interval を設定します。 might_went_rw を true に設定します。 -
last_epoch_clean の値が [past_interval.first, past_interval.last] の間にある場合、リカバリ回復の完了により現在の past_interval が生成される必要があり、その後、動作変更イベントが生成されて動作セットを変更することも可能です。このプロセス中に書き込み操作が行われるため、past_interval.maybe_went_rw を true に設定します。
-
それ以外の場合は、書き込み操作を実行できないため、past_interval.maybe_went_rw を false に設定します。
1.4.2 PG::past_intervalsの生成
past_intervals の生成には主に 2 つの場所があります。以下で分析してみましょう。
1.4.2.1 PG::generate_past_intervals()
void PG::generate_past_intervals()
{
epoch_t cur_epoch, end_epoch;
if (!_calc_past_interval_range(&cur_epoch, &end_epoch,osd->get_superblock().oldest_map)) {
if (info.history.same_interval_since == 0) {
info.history.same_interval_since = end_epoch;
dirty_info = true;
}
return;
}
OSDMapRef last_map, cur_map;
int primary = -1;
int up_primary = -1;
vector<int> acting, up, old_acting, old_up;
cur_map = osd->get_map(cur_epoch);
cur_map->pg_to_up_acting_osds(
get_pgid().pgid, &up, &up_primary, &acting, &primary);
epoch_t same_interval_since = cur_epoch;
dout(10) << __func__ << " over epochs " << cur_epoch << "-"<< end_epoch << dendl;
++cur_epoch;
for (; cur_epoch <= end_epoch; ++cur_epoch) {
int old_primary = primary;
int old_up_primary = up_primary;
last_map.swap(cur_map);
old_up.swap(up);
old_acting.swap(acting);
cur_map = osd->get_map(cur_epoch);
pg_t pgid = get_pgid().pgid;
if (last_map->get_pools().count(pgid.pool()))
pgid = pgid.get_ancestor(last_map->get_pg_num(pgid.pool()));
cur_map->pg_to_up_acting_osds(pgid, &up, &up_primary, &acting, &primary);
boost::scoped_ptr<IsPGRecoverablePredicate> recoverable(get_is_recoverable_predicate());
std::stringstream debug;
bool new_interval = pg_interval_t::check_new_interval(
old_primary,
primary,
old_acting,
acting,
old_up_primary,
up_primary,
old_up,
up,
same_interval_since,
info.history.last_epoch_clean,
cur_map,
last_map,
pgid,
recoverable.get(),
&past_intervals,
&debug);
if (new_interval) {
dout(10) << debug.str() << dendl;
same_interval_since = cur_epoch;
}
}
// PG import needs recalculated same_interval_since
if (info.history.same_interval_since == 0) {
assert(same_interval_since);
dout(10) << __func__ << " fix same_interval_since " << same_interval_since << " pg " << *this << dendl;
dout(10) << __func__ << " past_intervals " << past_intervals << dendl;
// Fix it
info.history.same_interval_since = same_interval_since;
}
// record our work.
dirty_info = true;
dirty_big_info = true;
}
この関数の実装を分析してみましょう。
1) _calc_past_interval_range() を呼び出して、現在の PG の past_interval の可能な範囲を計算します。
bool PG::_calc_past_interval_range(epoch_t *start, epoch_t *end, epoch_t oldest_map)
{
if (info.history.same_interval_since) {
*end = info.history.same_interval_since;
} else {
// PG must be imported, so let's calculate the whole range.
*end = osdmap_ref->get_epoch();
}
// Do we already have the intervals we want?
map<epoch_t,pg_interval_t>::const_iterator pif = past_intervals.begin();
if (pif != past_intervals.end()) {
if (pif->first <= info.history.last_epoch_clean) {
dout(10) << __func__ << ": already have past intervals back to "<< info.history.last_epoch_clean << dendl;
return false;
}
*end = past_intervals.begin()->first;
}
*start = MAX(MAX(info.history.epoch_created,
info.history.last_epoch_clean),
oldest_map);
if (*start >= *end) {
dout(10) << __func__ << " start epoch " << *start << " >= end epoch " << *end<< ", nothing to do" << dendl;
return false;
}
return true;
}
- 終了値を計算する
a) info.history.same_interval_since は、current_interval
現在の間隔 ( ) の最初の osdmap バージョン番号を指します。したがって、same_interval_since 値が 0 でない場合は、past_intervals
計算される現在の終了をこの値に設定します。
if (info.history.same_interval_since) {
*end = info.history.same_interval_since;
} else {
// PG must be imported, so let's calculate the whole range.
*end = osdmap_ref->get_epoch();
}
b) past_intervals で計算した past_interval の最初のエポックを確認し、それが info.history.last_epoch_clean より小さい場合は計算せずに false を直接返します。それ以外の場合は、end を最初の値に設定します。
*end = past_intervals.begin()->first;
- 開始値を計算する
a) start は、最後の last_epoch_clean から数えて、info.history.last_epoch_clean に設定されます。
b) PGが新しい場合、info.history.epoch_startedから計算を開始します
c) Oldest_map 値は、保存された最も古い osd マップ値です。start がこの値より小さい場合、関連する osdmap 情報が欠落しているため、計算できません。
したがって、start を 3 つのうちの最大値に設定します。
*start = MAX(MAX(info.history.epoch_created,
info.history.last_epoch_clean),
oldest_map);
以下に、past_intervals を計算するプロセスを示す例を示します。
- past_intervals の計算例
上の表に示すように、PG には 4 つのインターバルがあります。past_interval 1、開始エポックは 4、終了エポックは 8、past_interval 2 のエポック間隔は (9,11)、past_interval 3 の間隔は (12,13)、current_interval の間隔は (14,16) )。最新のエポックは 16 で、info.history.same_interval_since は 14 です。これは、エポック 14 から開始して、後続のエポック値と現在のエポック値が同じ間隔内にあることを意味します。info.history.last_epoch_clean は 8 です。これは、エポック値が 8 の場合、PG はクリーン状態であることを意味します。
開始と終了の計算方法は次のとおりです。
a) start の値は、値 8 を持つ info.history.last_epoch_clean 値に設定されます。
b) 終了値は 14 から計算されます。現在計算されている past_intervals の値を確認します。past_interval の計算は後ろから前に向かって計算されます。最初の past_interval の最初の値が 8 以下の場合、つまり past_interval 1 が計算されている場合、次の past_interval 2 と past_interval 3 が計算されており、そのまま終了します。それ以外の場合は、計算されていない past_interval 値の検索を続けます。
1.4.2.2 OSD::build_past_intervals_Parallel()
/*
* build past_intervals efficiently on old, degraded, and buried
* clusters. this is important for efficiently catching up osds that
* are way behind on maps to the current cluster state.
*
* this is a parallel version of PG::generate_past_intervals().
* follow the same logic, but do all pgs at the same time so that we
* can make a single pass across the osdmap history.
*/
struct pistate {
epoch_t start, end;
vector<int> old_acting, old_up;
epoch_t same_interval_since;
int primary;
int up_primary;
};
void OSD::build_past_intervals_parallel()
{
map<PG*,pistate> pis;
// calculate junction of map range
epoch_t end_epoch = superblock.oldest_map;
epoch_t cur_epoch = superblock.newest_map;
{
RWLock::RLocker l(pg_map_lock);
for (ceph::unordered_map<spg_t, PG*>::iterator i = pg_map.begin();i != pg_map.end();++i) {
PG *pg = i->second;
epoch_t start, end;
if (!pg->_calc_past_interval_range(&start, &end, superblock.oldest_map)) {
if (pg->info.history.same_interval_since == 0)
pg->info.history.same_interval_since = end;
continue;
}
dout(10) << pg->info.pgid << " needs " << start << "-" << end << dendl;
pistate& p = pis[pg];
p.start = start;
p.end = end;
p.same_interval_since = 0;
if (start < cur_epoch)
cur_epoch = start;
if (end > end_epoch)
end_epoch = end;
}
}
if (pis.empty()) {
dout(10) << __func__ << " nothing to build" << dendl;
return;
}
dout(1) << __func__ << " over " << cur_epoch << "-" << end_epoch << dendl;
assert(cur_epoch <= end_epoch);
OSDMapRef cur_map, last_map;
for ( ; cur_epoch <= end_epoch; cur_epoch++) {
dout(10) << __func__ << " epoch " << cur_epoch << dendl;
last_map = cur_map;
cur_map = get_map(cur_epoch);
for (map<PG*,pistate>::iterator i = pis.begin(); i != pis.end(); ++i) {
PG *pg = i->first;
pistate& p = i->second;
if (cur_epoch < p.start || cur_epoch > p.end)
continue;
vector<int> acting, up;
int up_primary;
int primary;
pg_t pgid = pg->info.pgid.pgid;
if (p.same_interval_since && last_map->get_pools().count(pgid.pool()))
pgid = pgid.get_ancestor(last_map->get_pg_num(pgid.pool()));
cur_map->pg_to_up_acting_osds(
pgid, &up, &up_primary, &acting, &primary);
if (p.same_interval_since == 0) {
dout(10) << __func__ << " epoch " << cur_epoch << " pg " << pg->info.pgid<< " first map, acting " << acting
<< " up " << up << ", same_interval_since = " << cur_epoch << dendl;
p.same_interval_since = cur_epoch;
p.old_up = up;
p.old_acting = acting;
p.primary = primary;
p.up_primary = up_primary;
continue;
}
assert(last_map);
boost::scoped_ptr<IsPGRecoverablePredicate> recoverable(pg->get_is_recoverable_predicate());
std::stringstream debug;
bool new_interval = pg_interval_t::check_new_interval(
p.primary,
primary,
p.old_acting, acting,
p.up_primary,
up_primary,
p.old_up, up,
p.same_interval_since,
pg->info.history.last_epoch_clean,
cur_map, last_map,
pgid,
recoverable.get(),
&pg->past_intervals,
&debug);
if (new_interval) {
dout(10) << __func__ << " epoch " << cur_epoch << " pg " << pg->info.pgid<< " " << debug.str() << dendl;
p.old_up = up;
p.old_acting = acting;
p.primary = primary;
p.up_primary = up_primary;
p.same_interval_since = cur_epoch;
}
}
}
// Now that past_intervals have been recomputed let's fix the same_interval_since
// if it was cleared by import.
for (map<PG*,pistate>::iterator i = pis.begin(); i != pis.end(); ++i) {
PG *pg = i->first;
pistate& p = i->second;
if (pg->info.history.same_interval_since == 0) {
assert(p.same_interval_since);
dout(10) << __func__ << " fix same_interval_since " << p.same_interval_since << " pg " << *pg << dendl;
dout(10) << __func__ << " past_intervals " << pg->past_intervals << dendl;
// Fix it
pg->info.history.same_interval_since = p.same_interval_since;
}
}
// write info only at the end. this is necessary because we check
// whether the past_intervals go far enough back or forward in time,
// but we don't check for holes. we could avoid it by discarding
// the previous past_intervals and rebuilding from scratch, or we
// can just do this and commit all our work at the end.
ObjectStore::Transaction t;
int num = 0;
for (map<PG*,pistate>::iterator i = pis.begin(); i != pis.end(); ++i) {
PG *pg = i->first;
pg->lock();
pg->dirty_big_info = true;
pg->dirty_info = true;
pg->write_if_dirty(t);
pg->unlock();
// don't let the transaction get too big
if (++num >= cct->_conf->osd_target_transaction_size) {
store->apply_transaction(service.meta_osr.get(), std::move(t));
t = ObjectStore::Transaction();
num = 0;
}
}
if (!t.empty())
store->apply_transaction(service.meta_osr.get(), std::move(t));
}
この関数は PG::generate_past_intervals() に似ていますが、すべての PG の past_intervals をすばやく計算し (注: pg Primary はこの OSD の pgs です)、計算の完了後に保存します。
1.4.3 PG::past_intervals の使用シナリオ
PG::generate_past_intervals() を検索すると、主に次の場所で呼び出されていることがわかりました。
1) リセット状態は AdvMap イベントを受け取ります
boost::statechart::result PG::RecoveryState::Reset::react(const AdvMap& advmap)
{
PG *pg = context< RecoveryMachine >().pg;
dout(10) << "Reset advmap" << dendl;
// make sure we have past_intervals filled in. hopefully this will happen
// _before_ we are active.
pg->generate_past_intervals();
pg->check_full_transition(advmap.lastmap, advmap.osdmap);
if (pg->should_restart_peering(
advmap.up_primary,
advmap.acting_primary,
advmap.newup,
advmap.newacting,
advmap.lastmap,
advmap.osdmap)) {
dout(10) << "should restart peering, calling start_peering_interval again"<< dendl;
pg->start_peering_interval(
advmap.lastmap,
advmap.newup, advmap.up_primary,
advmap.newacting, advmap.acting_primary,
context< RecoveryMachine >().get_cur_transaction());
}
pg->remove_down_peer_info(advmap.osdmap);
return discard_event();
}
通常、これは基本的に past_interval が初めて生成される場所です。
2) GetInfo ステージは past_interval を生成します
PG::RecoveryState::GetInfo::GetInfo(my_context ctx)
: my_base(ctx),
NamedState(context< RecoveryMachine >().pg->cct, "Started/Primary/Peering/GetInfo")
{
context< RecoveryMachine >().log_enter(state_name);
PG *pg = context< RecoveryMachine >().pg;
pg->generate_past_intervals();
unique_ptr<PriorSet> &prior_set = context< Peering >().prior_set;
assert(pg->blocked_by.empty());
if (!prior_set.get())
pg->build_prior(prior_set);
pg->reset_min_peer_features();
get_infos();
if (peer_info_requested.empty() && !prior_set->pg_down) {
post_event(GotInfo());
}
}
past_intervals を再計算する必要がある理由は一時的に不明です。
3)プライマリ OSD リカバリ中に might_have_unfound コレクションを構築する場合
/* Build the might_have_unfound set.
*
* This is used by the primary OSD during recovery.
*
* This set tracks the OSDs which might have unfound objects that the primary
* OSD needs. As we receive pg_missing_t from each OSD in might_have_unfound, we
* will remove the OSD from the set.
*/
void PG::build_might_have_unfound()
{
assert(might_have_unfound.empty());
assert(is_primary());
dout(10) << __func__ << dendl;
// Make sure that we have past intervals.
generate_past_intervals();
...
}
1.4.3 PG::past_intervals のクリーンアップ
PG::past_intervals は操作中に常に大規模に維持できるわけではないため、クリーンアップする必要があります。
/*
* Trim past_intervals.
*
* This gets rid of all the past_intervals that happened before last_epoch_clean.
*/
void PG::trim_past_intervals()
{
std::map<epoch_t,pg_interval_t>::iterator pif = past_intervals.begin();
std::map<epoch_t,pg_interval_t>::iterator end = past_intervals.end();
while (pif != end) {
if (pif->second.last >= info.history.last_epoch_clean)
return;
dout(10) << __func__ << ": trimming " << pif->second << dendl;
past_intervals.erase(pif++);
dirty_big_info = true;
}
}
上記のコードからわかるように、最後の past_interval の最後は info.history.last_epoch_clean 以上である必要があります。
次に、trim_past_intervals() の呼び出しをいつトリガーするのでしょうか?
void PG::mark_clean()
{
// only mark CLEAN if we have the desired number of replicas AND we
// are not remapped.
if (actingset.size() == get_osdmap()->get_pg_size(info.pgid.pgid) &&
up == acting)
state_set(PG_STATE_CLEAN);
// NOTE: this is actually a bit premature: we haven't purged the
// strays yet.
info.history.last_epoch_clean = get_osdmap()->get_epoch();
trim_past_intervals();
if (is_clean() && !snap_trimq.empty())
queue_snap_trim();
dirty_info = true;
}
クリーン状態に入るときは、まず現在の osdmap バージョン番号を info.history.last_epoch_clean に割り当ててから、trim_past_intervals() を呼び出して不要な past_intervals をクリアします。
2.アップスルー
OSDMap の機能の 1 つが Ceph クラスター OSD のステータス情報を維持することであることは誰もが知っています。そのため、これに基づいて質問したいのですが、Ceph クラスター内の 1 つの OSD がダウンした場合、osdmap はどうなりますか? ? osdmapは何回更新されますか? この疑問を念頭に置いて、この記事では up_thru について詳しく説明します。
2.1 up_thru導入の目的
up_thru の導入は、次のような極端なシナリオを解決することです。
たとえば、クラスターには、ビジネス IO を提供する PG のバッチを運ぶ 2 つの osd (osd.1、osd.2) 機能があります。ある時点で osd.1 がダウンし、その後 osd.2 もダウンし、その後 osd.1 が再びアップした場合、この時点で osd.1 はサービスを提供できますか?
osd.1 がダウンしている間に osd.1 にデータ更新がある場合、osd.1 が再び起動した後は明らかに osd.1 はサービスを提供できませんが、osd.2 にデータ更新がない場合、osd.1 は再び起動した後にサービスを提供できます。 。
2.2 up_thru とは正確には何ですか?
up_thru が何であるかを知りたい場合は、まず次のように、関連するデータ構造を通してそれを感じてください。
class OSDMap { // osdmap数据结构
vector<osd_info_t> osd_info; // osd信息
}
struct osd_info_t {
epoch_t up_thru; // up_thru本尊
}
データ構造から、up_thru が osdmap に osd 情報として格納されており、その種類が osdmap のバージョン番号 (epoch_t) であることがわかります。
これは osdmap に保存されているため、次のように osdmap をダンプすることで確認できます。
usrname@hostname:# ceph osd dump epoch *** fsid *** // up_thru は次のように確認できます。277 は osdmap osd のバージョン番号です。1 up in Weight 1 up_from 330 up_thru 277 down_at 328 ... osd。 2 体重増加 1 増加 329 から増加 277 減少 328 …
2.3 up_thru の詳細
up_thru のライフサイクル全体とその仕組みについて教えてください。全体の処理は非常に長いので、記事を短く簡潔にするために、up_thru とあまり関係のない処理はコード解析には追加せず、簡単に説明します。
up_thru の詳細を説明するために、上の 2 つの OSD 例から始めましょう。
3.3.1 up_thruのアップデート
osd.1 がハングした後はどうなりますか?
osd.1 がハングすると、クラスター全体が次のように応答します。
- OSDレポート月
osd.1 がハングするか、osd.1 が率先して報告するか、他の osd が osd.1 がハングしていることを mon に報告すると、この時点で mon は osd.1 がハングしていることを感知します。
- 月にOSDマップを更新
osdmap で、ダウンしている osd 状態をダウンとしてマークし、新しい osdmap を osd.2 に送信します。
- osd.2 が新しい osdmap を(初めて)受信します
osd.2 が新しい osdmap を受信した後、関連する PG はピアリングを開始し、up_thru 情報を更新するために mon に適用する必要があると PG が判断した場合、PG のステータスは WaitUpThru に変わります。
osd.2 は、mon からの up_thru メッセージに対して更新申請が必要かどうかを判断し、必要であればその情報を mon に送信します。
- 月にOSDマップを更新
メッセージを受信した後、mon は osdmap 内の osd.2 の up_thru 情報を更新し、新しい osdmap を osd.2 に送信します。
- osd.2 が新しい osdmap を受信します (2 回目)
osd.2 が新しい osdmap を受信すると、関連する PG がピアリングを開始し、関連する PG のステータスが WaitUpThru からアクティブに変わり、ビジネス IO の提供を開始できるようになります。
具体的な up_thru アップデート関連のプロセスは次のとおりです。
以下は、osd.2 が新しい osdmap を (初めて) 受信した後の関連操作です。
1) PG は、対応するメイン OSD が up_thru を更新する必要があるかどうかを判断します。
PG はピアリングを開始します。ピアリング関連の機能は次のとおりです。
PG::build_prior(std::unique_ptr<PriorSet> &prior_set)
{
/* 这里需要引入一个概念past_interval:
past_interval是osdmap版本号epoch 的一个序列。在该序列内一个PG的 acting set 和 up set不会变化。
如果osd.1挂了,那么pg的up set肯定发生变化了,也即产生了一个新的past_interval,那么此时会更新info.history.same_interval_since为新的osdmap版本号
因为same_interval_since表示的是最近一个past_interval的第一个osdmap的版本号
所以如果osd.1挂了后,此时这个条件肯定满足
*/
if (get_osdmap()->get_up_thru(osd->whoami) < info.history.same_interval_since)
{
need_up_thru = true;
} else {
need_up_thru = false;
}
}
2) osd は、今回ピアリングしているすべての PG が up_thru を申請する必要があるかどうかを判断します。その場合、OSD は up_thru のモニターに適用されます。
OSD::process_peering_events(
const list<PG*> &pgs, //本次peering的所有PG
ThreadPool::TPHandle &handle)
{
bool need_up_thru = false;
epoch_t same_interval_since = 0;
for (list<PG*>::const_iterator i = pgs.begin();i != pgs.end();++i)
{
......
need_up_thru = pg->need_up_thru || need_up_thru;
//获得所有PG中最大的info.history.same_interval_since
same_interval_since = MAX(pg->info.history.same_interval_since,same_interval_since);
}
if (need_up_thru) //只要有一个PG需要申请up_thru,则申请
queue_want_up_thru(same_interval_since);
}
OSD::queue_want_up_thru(epoch_t want)
{
epoch_t cur = osdmap->get_up_thru(whoami); //获得当前osdmap中该osd对应的osdmap版本
if (want > up_thru_wanted) //如果所有PG中最大的info.history.same_interval_since大于当前的,说明该osd需要申请up_thru
{
up_thru_wanted = want;
send_alive();
}
}
OSD::send_alive()
{
// 向mon发送更新up_thru的消息
monc->send_mon_message(new MOSDAlive(osdmap->get_epoch(), up_thru_wanted));
}
3) モニターは OSD のアプリケーションを受け入れます
OSDMonitor::preprocess_alive(MonOpRequestRef op)
{
//如果最新的osdmap中该osd的up_thru大于等于osd想要申请的。那么monitor不需要决议了,只需要把osd中缺失的所有osdmap发送给它
if (osdmap.get_up_thru(from) >= m->want)
{
_reply_map(op, m->version);
return true;
}
return false; //返回false,monitor还需要继续决议
}
// 若monitor还需要继续决议,则继续往下走
OSDMonitor::prepare_alive(MonOpRequestRef op)
{
update_up_thru(from, m->version);
//决议完成后调用回调C_ReplyMap。把新的osdmap发给osd.2
wait_for_finished_proposal(op, new C_ReplyMap(this, op, m->version));
}
OSDMonitor::update_up_thru(int from, epoch_t up_thru)
{
pending_inc.new_up_thru[from] = up_thru;
}
// monitor决议完之后,最终会调用下面的函数进行应用,更新OSDMap
OSDMap::apply_incremental(const Incremental &inc)
{
//更新osdmap中该osd对应的up_thru字段
for (map<int32_t,epoch_t>::const_iterator i = inc.new_up_thru.begin();i != inc.new_up_thru.end();
{
osd_info[i->first].up_thru = i->second;
}
}
PGステータス変更
osd.2 が新しい osdmap を (初めて) 受信した後、PG がピアリングに入り、up_thru を更新するために mon が必要な場合、最初に NeedUpThru 状態に入ります。
PG::RecoveryState::GetMissing::GetMissing()
{
if (pg->need_up_thru) //如果该PG的need_up_thru标志被置位。这是在上文的build_prior函数中操作的
{
post_event(NeedUpThru()); //pg进入WaitUpThru状态
return;
}
}
osd.2 が新しい osdmap を受信した後 (2 回目)、osdmap は up_thru が更新された後の新しい osdmap です。PG は現在 WaitUpThru 状態にあります。WaitUpThru 状態の PG が OSDMap を受信すると、pg- > 次のように、need_up_thru が false に設定されます。
PG::RecoveryState::Peering::react(const AdvMap& advmap)
{
/*
当本身已经处于peering时,收到Advmap,会先看下是否会对prio有影响,只有有的情况下,才会进入reset
像本身已经处于wait_up_thru状态的PG则不会进入reset,它会执行下面的adjust_need_up_thru,然后把PG->need_up_thru标记设置为false。
*/
if (prior_set.get()->affected_by_map(advmap.osdmap, pg))
{
post_event(advmap);
return transit< Reset >();
}
// 把PG->need_up_thru标记设置为false
pg->adjust_need_up_thru(advmap.osdmap);
return forward_event();
}
bool PG::adjust_need_up_thru(const OSDMapRef osdmap)
{
epoch_t up_thru = get_osdmap()->get_up_thru(osd->whoami);
if (need_up_thru &&up_thru >= info.history.same_interval_since) {
dout(10) << "adjust_need_up_thru now " << up_thru << ", need_up_thru now false" << dendl;
need_up_thru = false;
return true;
}
return false;
}
ActMap イベントを受信した後、need_up_thru フラグが false に設定されているため、wait_up_thru 状態の PG はアクティブ状態への移行を開始します。アクティブ状態に入った後、io サービスを開始できます。
boost::statechart::result PG::RecoveryState::WaitUpThru::react(const ActMap& am)
{
PG *pg = context< RecoveryMachine >().pg;
if (!pg->need_up_thru) {
post_event(Activate(pg->get_osdmap()->get_epoch()));
}
return forward_event();
}
2.4 アップスルーアプリケーション
上記は、up_thru の更新を含む、osd.1 がハングしたときのクラスター全体の応答を説明しています。このセクションでは、この記事の冒頭で例に挙げた up_thru の適用について説明します。osd.1 が再び起動したら、サービスを提供できるでしょうか。
シーンの説明
最初の瞬間、つまり両方が正常であると仮定すると、対応する PG がアクティブでクリーンなときの osdmap のバージョン番号エポックは 80 であり、この時点で osd.1 と osd.2 の up_thru は 80 未満である必要がありますosd.1
。osd.2
1)サービスが提供できない場合
osd.1 がダウンすると、osdmap のバージョン番号エポックは 81 になります。
osd.2 が mon に適用されて up_thru が正常に更新された場合、osdmap のバージョン番号エポックは 82 になります (osd.2 の up_thru は 81 になります)。osd.2 が新しい osdmap を受信した後、PG のステータスはアクティブになります。 、io サービスを提供できるため、up_thru の更新が成功した場合、osd.2 に新しい書き込みがあると判断できます (もちろん、up_thru は更新される可能性がありますが、osd.2 はアクティブになる前にハングします。これも同様です)データ更新なし);
osd.2 がダウンすると、osdmap のバージョン番号は 83 になります。
osd.1 プロセスが起動し、osdmap のバージョン番号は 84 になります。
2) IOサービスを提供できるシナリオ
osd.1 がダウンすると、osdmap のバージョン番号エポックは 81 になります。
osd.2 は正常にピアリングせず、対応する up_thru をモニターに報告せず、操作を更新しませんでした (たとえば、osd.2 は up_thru を mon に更新するための適用に失敗し、報告する前にハングアップしました)
osd.2 がダウンすると、osdmap のバージョン番号エポックは 82 になります。
osd.1 プロセスが起動すると、osdmap のバージョン番号は 83 になります。
IOがサービスを提供できるかどうかのコードロジック判定
次のプロセスを使用して、対応する PG が IO サービスを提供できるかどうかを判断します。
1) past_interval を生成する
ピアリングフェーズでは、past_interval を生成するために次の関数が呼び出されます (osd の開始時に、最下層から関連情報も読み取られて、past_interval が生成されます)。
bool pg_interval_t::check_new_interval( ... )
{
if (is_new_interval(...)) {
if (num_acting &&i.primary != -1 &&num_acting >= old_pg_pool.min_size &&(*could_have_gone_active)(old_acting_shards)) {
...
}
}
}
ここでも、上記の 2 つのシナリオを使用して、past_interval の更新を説明します。
- IOサービスが提供できないシナリオ
前述したように、osdmap のバージョン番号エポックはそれぞれ 80 ~ 84 であり、これらの osdmap バージョン番号は次の 4 つの past_interval に対応します。
{80}、{81、82}、{83}、{84}
そして、past_interval {81,82} では、maybe_went_rw が true に設定されます。これは、osdmap バージョン番号が 82 の場合、osd.2 に対応する up_thru が 81 であり、この past_interval の最初の osdmap バージョン番号 81 に等しいためです。
- IOサービスを提供できるシナリオ
前述したように、osdmap のバージョン番号のエポックはそれぞれ 80 ~ 83 であり、これらの osdmap のバージョン番号は次の 4 つの past_interval に対応します。
{80}、{81}、{82}、{83}
2) past_interval に基づいて IO が実行可能かどうかを判断する
ここでは、次のように IO を提供できないシナリオを例に挙げます。
// 当osd.1再次up后,peering阶段调用该函数
PG::build_prior(std::unique_ptr<PriorSet> &prior_set)
{
prior_set.reset(
new PriorSet(
pool.info.ec_pool(),
get_pgbackend()->get_is_recoverable_predicate(),
*get_osdmap(),
past_intervals,
up,
acting,
info,
this));
PriorSet &prior(*prior_set.get());
// 如果prior.pg_down为true,pg状态被设置为down,此时pg状态为down+peering
if (prior.pg_down)
{
state_set(PG_STATE_DOWN);
}
}
PriorSet::PriorSet(map<epoch_t, pg_interval_t> &past_intervals,...)
{
//遍历所有的past_intervals{80},{81,82},{83},{84}
for (map<epoch_t,pg_interval_t>::const_reverse_iterator p = past_intervals.rbegin();p != past_intervals.rend(); ++p)
{
const pg_interval_t &interval = p->second; //取得pg_interval_t
//还是上面的例子,当这里运行到{81,82}这个interval时,由于maybe_went_rw为true了,故而无法continue退出,还要继续走
if (!interval.maybe_went_rw)
continue;
for (unsigned i = 0; i < interval.acting.size(); i++)
{
int o = interval.acting[i]; //acting列表中的osd
if (osdmap.is_up(o)) //如果该OSD处于up状态,就加入到up_now列表中,同时加到probe列表。用于获取权威日志以及后续数据恢复
{
probe.insert(so);
up_now.insert(so);
}
else if (!pinfo)
{
//该列表保存了所有down列表
down.insert(o);
}
else
{
// 该列表保存了所有down列表
// 当这里运行到{81,82}这个interval时,
// 由于这个past_interval中的osd.2此时down了,所以会走到这里
down.insert(o);
any_down_now = true;
}
}
// peering由于在线的osd不足,所以不能够继续进行。母函数会根据这个值从而把PG状态设置为DOWN
// 当这里运行到{81,82}这个interval时,由于这个past_interval中的唯一的一个osd.2此时down了,所以pg_down肯定会被置为true)
if (!(*pcontdec)(up_now) && any_down_now) {
// fixme: how do we identify a "clean" shutdown anyway?
dout(10) << "build_prior possibly went active+rw, insufficient up;"<< " including down osds" << dendl;
for (vector<int>::const_iterator i = interval.acting.begin();i != interval.acting.end();++i) {
if (osdmap.exists(*i) && // if it doesn't exist, we already consider it lost.
osdmap.is_down(*i)) {
pg_down = true;
// make note of when any down osd in the cur set was lost, so that
// we can notice changes in prior_set_affected.
blocked_by[*i] = osdmap.get_info(*i).lost_at;
}
}
}
}
}
まとめると、osd.1 が再び起動し、最終的に {84} の間隔に達すると、上記の一連の関数呼び出しに従って、この時点の PG ステータスは最終的にピアリング + ダウンに変化し、ピアリングができなくなります。現時点ではサービス IO に応答します。
3.5 は後で書きます
この章の冒頭で、Ceph クラスター内で osd が停止した場合、osdmap はどうなるのかという疑問を提起しました。osdmapは何回更新されますか?
答えは次のとおりです。必ずしもそうではありません。主に次の 2 つの状況が当てはまります。
- 一時停止された OSD には、他の OSD と共有される PG がありません
現時点では、OSDMap は 1 回だけ更新され、osdmap 内の OSD のステータスが上から下に更新されることが変更されます。関連する PG がないため、PG ピアリングがなく、up_thru 更新もないため、osdmap は 1 回変更されます。
- 一時停止された OSD と他の OSD には機能的なベアラー PG があります
このとき、osdmap は少なくとも 2 回更新されます。1 回目は osdmap 内の osd の状態を更新し、2 回目は関連する osd の up_thru を更新します。
osdmap の変更を示す例は次のとおりです。
1) クラスタの初期状態情報は以下のとおりです。
# ceph osd ツリー ID クラス 重み タイプ名 ステータス リウェイト PRI-AFF -1 1.74658 root デフォルト -5 0.87329 ホスト pubt2-ceph1-dg-163-org 0 ssd 0.87329 osd.0 アップ 1.00000 1.00000 -3 0.87329 ホスト pubt2-ceph2-ディグ-163-org 1 ssd 0.87329 osd.1 アップ 1.00000 1.00000 # ceph osd ダンプ | grep epoch epoch 416 // osdmap版本号
2) osd.0 ダウン後のクラスター情報は次のようになります。
# ceph osd ツリー ID クラス 重み タイプ 名前 ステータス リウェイト PRI-AFF -1 1.74658 root デフォルト -5 0.87329 ホスト pubt2-ceph1-dg-163-org 0 ssd 0.87329 osd.0 ダウン 1.00000 1.00000 -3 0.87329 ホスト pubt2-ceph2-ディグ-163-org 1 ssd 0.87329 osd.1 アップ 1.00000 1.00000 # ceph osd ダンプ | grep epoch epoch 418 // osdmap版本号
上に示したように、osd.0 がダウンすると、osdmap のバージョン番号が 2 増加します。次に、これら 2 つの osdmap の変更を見てみましょう。ceph osd dump epoch
対応する osdmap を印刷できます。
# ceph osd ダンプ 416 # ceph osd ダンプ 417 # ceph osd ダンプ 418
osdmap.416
比較すると、と の違いosdmap.417
は osd.0 の状態変化、osdmap.417
と の違いはosdmap.418
osd.1 の up_thru の更新であることがわかります。