ceph-afr-calc/main.js

279 lines
12 KiB
JavaScript
Raw Normal View History

import * as preact from 'preact';
/** @jsx preact.h */
import { cluster_afr, cluster_afr_bruteforce } from './afr.js';
class Calc extends preact.Component
{
state = {
hosts: 10,
drives: 10,
afr_drive: 3,
afr_host: 0,
capacity: 8,
speed: 20,
pg_per_osd: 10,
ec: false,
replicas: 2,
ec_data: 2,
ec_parity: 1,
eager: false,
2021-01-16 22:21:06 +03:00
same_host: true,
result: 0,
error: null,
use_speed: true,
sim: true,
}
_timer = null
calc(st, force)
{
const empty_st = !st;
st = { ...this.state, ...(st||{}) };
if (st.sim && !force)
{
if (this._timer)
{
clearTimeout(this._timer);
}
this.setState(st);
this._timer = setTimeout(() => this.calc(null, true), 200);
return;
}
// !( >= ) not the same as < because of NaN
if (!(parseInt(st.hosts) >= (st.ec ? parseInt(st.ec_data)+parseInt(st.ec_parity) : parseInt(st.replicas))))
{
st.error = 'Число серверов (доменов отказа) меньше числа дисков в PG, расчёт невозможен';
st.result = 0;
}
else
{
const fn = st.sim ? cluster_afr_bruteforce : cluster_afr;
st.error = null;
st.result = 100*fn({
n_hosts: st.hosts,
n_drives: st.drives,
afr_drive: st.afr_drive/100,
afr_host: st.afr_host/100,
capacity: st.capacity*1000,
speed: st.use_speed ? st.speed/1000 : null,
disk_heal_hours: st.use_speed ? null : st.disk_heal_hours,
ec: st.ec,
ec_data: st.ec_data,
ec_parity: st.ec_parity,
replicas: st.replicas,
pgs: st.pg_per_osd,
osd_rm: !st.same_host,
degraded_replacement: st.eager,
down_out_interval: 600,
});
}
this.setState(empty_st ? { error: st.error, result: st.result } : st);
}
setter(field)
{
if (!this.setter[field])
{
this.setter[field] = (event) =>
{
this.calc({ [field]: event.target.value });
};
}
return this.setter[field];
}
setSim = () =>
{
if (!this.state.sim)
{
// Слишком много PG замедляет расчёт
this.calc({ sim: true, pgs: this.state.pgs > 10 ? 10 : this.state.pgs });
}
}
setThe = () =>
{
this.calc({ sim: false });
}
setRepl = () =>
{
this.calc({ ec: false });
}
setEC = () =>
{
this.calc({ ec: true });
}
setEager = (event) =>
{
this.calc({ eager: event.target.checked });
}
useSpeed = () =>
{
this.calc({ use_speed: true, speed: this.state.speed || 20 });
}
useTime = () =>
{
this.calc({ use_speed: false, disk_heal_hours: 12 });
}
2021-01-16 22:21:06 +03:00
setSameHost = (event) =>
{
this.calc({ same_host: event.target.checked });
}
2021-01-15 16:17:16 +03:00
format4 = (n) =>
{
if (n >= 1 || n <= -1)
return ''+(Math.round(n*10000)/10000);
if (n == 0)
return '0';
let s = '0.', i = 0, c = 0;
if (n < 0)
2021-01-15 16:17:16 +03:00
{
s = '-0.';
n = -n;
2021-01-15 16:17:16 +03:00
}
while (n && i < 4)
{
n = n*10;
s += (n|0);
c = c || (n|0);
n = n-(n|0);
if (c)
i++;
}
return s[s.length-1] == '.' ? '0' : s;
2021-01-15 16:17:16 +03:00
}
componentDidMount()
{
this.calc({});
}
render(props, state)
{
return (<div style="width: 750px; margin: 20px; padding: 20px; box-shadow: 0 19px 60px rgba(0, 0, 0, 0.3), 0 15px 20px rgba(0, 0, 0, 0.22);">
<h2 style="text-align: center; font-size: 150%; margin: 10px 0 20px 0; font-weight: bold">
Калькулятор вероятности отказа кластера Ceph/Vitastor
</h2>
<p>
Вероятность потери данных в кластере зависит от числа серверов и дисков
(чем их больше, тем вероятность больше), от схемы избыточности, скорости ребаланса (восстановления),
и, конечно, непосредственно вероятности выхода из строя самих дисков и серверов.
</p>
<p>
Расчёт ведётся в простом предположении, что отказы распределены равномерно во времени.
</p>
<div style={{textAlign: 'center'}}>
<div style={{display: 'inline-block'}}>
<label class={"switch l"+(state.sim ? "" : " sel")}>
<input type="radio" name="sim" checked={!state.sim} onclick={this.setThe} /> Теоретический расчёт (оценка сверху)
</label>
<label class={"switch r"+(state.sim ? " sel" : "")}>
<input type="radio" name="sim" checked={state.sim} onclick={this.setSim} /> Переборная модель (точно, но медленно)
</label>
</div>
</div>
<table>
<tr>
<th>Число <abbr title="Серверов либо других доменов отказа, если у вас домен отказа другой">серверов</abbr></th>
<td><input type="text" value={state.hosts} onchange={this.setter('hosts')} /></td>
</tr>
<tr>
<th>Число дисков в сервере</th>
<td><input type="text" value={state.drives} onchange={this.setter('drives')} /></td>
</tr>
<tr>
<th>Ёмкость дисков</th>
<td><input type="text" value={state.capacity} onchange={this.setter('capacity')} /> ТБ</td>
</tr>
<tr>
<th>Схема избыточности</th>
<td>
<label class={"switch l"+(state.ec ? "" : " sel")}>
<input type="radio" name="scheme" checked={!state.ec} onclick={this.setRepl} /> Репликация
</label>
<label class={"switch r"+(state.ec ? " sel" : "")}>
<input type="radio" name="scheme" checked={state.ec} onclick={this.setEC} /> EC (коды коррекции ошибок)
</label>
</td>
</tr>
{state.ec ? null : <tr>
<th>Число реплик</th>
<td><input type="text" value={state.replicas} onchange={this.setter('replicas')} /></td>
</tr>}
{state.ec ? <tr>
<th>Число дисков данных</th>
<td><input type="text" value={state.ec_data} onchange={this.setter('ec_data')} /></td>
</tr> : null}
{state.ec ? <tr>
<th>Число дисков чётности</th>
<td><input type="text" value={state.ec_parity} onchange={this.setter('ec_parity')} /></td>
</tr> : null}
<tr>
<th>
{state.use_speed ? 'Оценочная' : 'Оценочное'}&nbsp;
<span className="icombo">
{state.use_speed ? 'скорость' : 'время'} <span className="icon-arw-down"></span>
<span className="options">
<span className="option" onClick={this.useSpeed}>скорость</span>
<span className="option" onClick={this.useTime}>время</span>
</span>
</span>
<br />восстановления на 1 OSD
</th>
{state.use_speed
? <td><input type="text" value={state.speed} onchange={this.setter('speed')} /> МБ/с</td>
: <td><input type="text" value={state.disk_heal_hours} onchange={this.setter('disk_heal_hours')} /> час(ов)</td>}
</tr>
2021-07-20 16:25:41 +03:00
<tr>
<th><abbr title="Среднее число уникальных групп чётности (пар/троек и т.п.), включающих каждый отдельный диск. В Ceph нормой считается 100 PG на OSD">PG на OSD</abbr></th>
2021-07-20 16:25:41 +03:00
<td><input type="text" value={state.pg_per_osd} onchange={this.setter('pg_per_osd')} /></td>
</tr>
<tr>
<th><abbr title="Annualized Failure Rate, вероятность отказа в течение года в %">AFR</abbr> диска</th>
<td><input type="text" value={state.afr_drive} onchange={this.setter('afr_drive')} /> %</td>
</tr>
<tr>
<th><abbr title="Вероятность отказа сервера сразу со всеми дисками, без возвращения их в строй">AFR сервера</abbr></th>
<td><input disabled={state.sim} title={state.sim ? 'Не поддерживается в режиме симуляции' : ''}
type="text" value={state.afr_host} onchange={this.setter('afr_host')} /> %</td>
</tr>
</table>
2021-01-16 22:21:06 +03:00
<p>
<label><input type="checkbox" checked={state.same_host} onchange={this.setSameHost} />
При отказе диска данные распределяются только по другим дискам того же сервера,
как в Ceph
</label>
</p>
<p>
<label><input type="checkbox" checked={state.eager} onchange={this.setEager} />
Я нетерпеливый и заменяю отказавший диск сразу, не давая данным уехать на остальные диски
(либо данным уезжать некуда, например, сервера всего 3 при 3 репликах)
</label>
</p>
<div style="text-align: center; font-size: 150%; margin: 20px 0; font-weight: bold">
Вероятность потери данных в течение года:
</div>
{state.error
? <div style="text-align: center; color: red; margin: 20px 0">
{state.error}
</div>
: <div style="text-align: center; font-size: 200%; margin: 20px 0; font-weight: bold">
{this.format4(state.result)} %
</div>}
2021-01-15 15:59:26 +03:00
<div style="text-align: center; color: #aaa; margin: 10px 0">
&copy; Виталий Филиппов 2020+ <a style="color: inherit" href="https://yourcmc.ru/git/vitalif/ceph-afr-calc">(исходники)</a>
</div>
</div>);
}
}
preact.render(<Calc />, document.body);