StatusGuardは高可用性を実現する分散型の死活監視プラットフォームです。複数のエージェントノードによる冗長化と多数決による合意アルゴリズムにより、単一障害点を排除し、信頼性の高い監視を提供します。
- 役割: 管理画面、設定管理、結果表示
- 技術: AdonisJS + Inertia.js + Vue.js
- データベース: PostgreSQL(マスターデータ)
- 認証: GitHub OAuth
- 役割: 実際の死活監視処理を実行
- 配置: 複数リージョン、複数プロバイダー対応
- プロトコル: HTTP/HTTPS, TCP, ICMP Ping, DNS
- レポート: JSON形式でコントローラーに送信
- 判定方式: 過半数による多数決(Byzantine Fault Tolerance)
- タイムウィンドウ: 設定可能(デフォルト5分)
- 最小エージェント数: 3ノード以上推奨
- 分散アルゴリズム: Modified Raft Consensus
- HTTP/HTTPS監視: ステータスコード、レスポンス時間、コンテンツ検証
- TCP監視: ポート接続性、レスポンス時間
- ICMP監視: Ping到達性、パケットロス測定
- DNS監視: 名前解決、DNSレコード検証
- SSL証明書監視: 有効期限、証明書チェーン検証
- 通知チャンネル: Slack, Discord, メール, Webhook
- エスカレーション: 段階的通知設定
- メンテナンス: 計画停止時の通知抑制
- インシデント管理: 障害履歴、復旧時間記録
- リアルタイム監視ダッシュボード
- SLA レポート: 稼働率、MTTR、MTBF
- パフォーマンストレンド: レスポンス時間推移
- 公開ステータスページ: カスタマイズ可能
- マルチテナント: プロジェクト単位の分離
- チーム管理: ロールベースアクセス制御
- API: RESTful API + Webhook
- 設定テンプレート: 監視設定の一括適用
リージョン1: agent-tokyo-1, agent-tokyo-2
リージョン2: agent-oregon-1, agent-oregon-2
リージョン3: agent-ireland-1, agent-ireland-2
- 監視実行: 各エージェントが独立して監視実行
- 結果収集: 設定されたタイムウィンドウ内の結果を収集
- 多数決判定: Byzantine Fault Toleranceアルゴリズムで最終判定
- 状態更新: 合意結果をマスターDBに記録
- 通知実行: 状態変化時にアラート送信
- ネットワーク分断: 各リージョンで独立動作可能
- エージェント障害: 最小限必要数での継続監視
- データベース冗長化: レプリケーション + 自動フェイルオーバー
- ロードバランシング: 複数コントローラーノード対応
- Vue.js 3 + TypeScript
- Inertia.js (SPA風体験)
- Tailwind CSS (レスポンシブデザイン)
- Chart.js (グラフ表示)
- AdonisJS 6 + TypeScript
- Lucid ORM (PostgreSQL)
- Bull Queue (非同期処理)
- Redis (キャッシュ・セッション)
- Docker Compose (開発環境)
- Kubernetes (本番環境)
- PostgreSQL 15+ (プライマリDB)
- Redis 7+ (キャッシュ・Queue)
- Node.js + TypeScript
- Prometheus metrics準拠
- gRPC通信 (コントローラー間)
- Docker化対応
StatusGuardでは、Modified Raftベースの合意アルゴリズムにByzantine Fault Tolerance要素を組み込んだハイブリッド型合意メカニズムを採用。
interface MonitorCheck {
checkId: string; // UUID v4
serviceId: string;
agentId: string;
timestamp: number; // Unix timestamp
result: {
status: 'up' | 'down' | 'degraded';
responseTime: number; // ミリ秒
statusCode?: number; // HTTP用
errorMessage?: string;
metadata: Record<string, any>;
};
signature: string; // HMAC-SHA256署名
}- タイムウィンドウ: 設定可能(30秒〜10分)
- 収集条件: 各エージェントから最新1件ずつ
- 最小必要数: ceil(totalAgents / 2) + 1
- タイムアウト: タイムウィンドウの80%時点で強制判定
function verifyAgentSignature(check: MonitorCheck): boolean {
const payload = `${check.checkId}:${check.result.status}:${check.timestamp}`;
const expectedSignature = hmacSha256(payload, agentSecretKey);
return check.signature === expectedSignature;
}interface AgentWeight {
agentId: string;
reliability: number; // 0.0-1.0 (過去の成功率)
networkLatency: number; // RTT (ms)
region: string;
weight: number; // 計算後重み
}
function calculateWeight(agent: AgentWeight): number {
const reliabilityFactor = agent.reliability;
const latencyFactor = Math.max(0.1, 1 - (agent.networkLatency / 1000));
const regionFactor = getRegionDiversityBonus(agent.region);
return reliabilityFactor * latencyFactor * regionFactor;
}interface ConsensusVote {
status: 'up' | 'down' | 'degraded';
weight: number;
confidence: number; // 0.0-1.0
}
function byzantineConsensus(votes: ConsensusVote[]): ConsensusResult {
// ビザンチン障害ノード検出
const outliers = detectOutliers(votes);
const validVotes = votes.filter(v => !outliers.includes(v));
// 重み付き多数決
const weightedResults = calculateWeightedMajority(validVotes);
// 信頼度閾値チェック
const finalResult = validateConfidenceThreshold(weightedResults);
return finalResult;
}class ConsensusCoordinator {
private currentTerm: number = 0;
private votedFor: string | null = null;
private state: 'follower' | 'candidate' | 'leader' = 'follower';
async electLeader(): Promise<string> {
// 1. タームインクリメント
this.currentTerm++;
this.state = 'candidate';
this.votedFor = this.agentId;
// 2. 他ノードに投票要求
const votes = await this.requestVotes();
// 3. 過半数取得チェック
if (votes.length > Math.floor(this.totalAgents / 2)) {
this.state = 'leader';
return this.agentId;
}
this.state = 'follower';
return null;
}
}interface DecisionMatrix {
up: { weight: number; confidence: number; agents: string[] };
down: { weight: number; confidence: number; agents: string[] };
degraded: { weight: number; confidence: number; agents: string[] };
}
function buildDecisionMatrix(checks: MonitorCheck[]): DecisionMatrix {
const matrix: DecisionMatrix = {
up: { weight: 0, confidence: 0, agents: [] },
down: { weight: 0, confidence: 0, agents: [] },
degraded: { weight: 0, confidence: 0, agents: [] }
};
for (const check of checks) {
const agent = getAgentInfo(check.agentId);
const weight = calculateWeight(agent);
const confidence = calculateConfidence(check);
matrix[check.result.status].weight += weight;
matrix[check.result.status].confidence += confidence * weight;
matrix[check.result.status].agents.push(check.agentId);
}
return matrix;
}function preventSplitBrain(activeAgents: Agent[]): boolean {
const totalWeight = activeAgents.reduce((sum, agent) => sum + agent.weight, 0);
const requiredWeight = this.originalTotalWeight * 0.51; // 51%閾値
return totalWeight >= requiredWeight;
}interface PartitionStrategy {
strategy: 'majority' | 'regional' | 'manual';
fallbackBehavior: 'conservative' | 'optimistic' | 'last_known';
partitionTimeout: number; // 秒
}
function handleNetworkPartition(partition: Agent[]): ConsensusResult {
if (partition.length < this.minimumQuorum) {
// 保守的判定: 前回の状態を維持
return { status: 'unknown', fallback: true };
}
// 部分的合意での判定継続
return performPartialConsensus(partition);
}interface ConsensusCache {
serviceId: string;
lastConsensus: ConsensusResult;
timestamp: number;
participatingAgents: string[];
cacheValidityMs: number;
}
function getCachedConsensus(serviceId: string): ConsensusResult | null {
const cached = this.consensusCache.get(serviceId);
if (!cached) return null;
const age = Date.now() - cached.timestamp;
if (age > cached.cacheValidityMs) return null;
return cached.lastConsensus;
}interface ConsensusBatch {
batchId: string;
services: string[];
scheduledAt: number;
maxBatchSize: number;
processingTimeMs: number;
}
async function processBatchConsensus(batch: ConsensusBatch): Promise<void> {
const promises = batch.services.map(serviceId =>
this.performConsensus(serviceId)
);
await Promise.allSettled(promises);
}interface ConsensusAudit {
consensusId: string;
serviceId: string;
timestamp: number;
participatingAgents: AgentVote[];
decisionMatrix: DecisionMatrix;
finalResult: ConsensusResult;
processingTimeMs: number;
byzantineFlags: string[];
}// proto定義例
service ConsensusService {
rpc RequestVote(VoteRequest) returns (VoteResponse);
rpc AppendEntries(AppendEntriesRequest) returns (AppendEntriesResponse);
rpc SubmitConsensus(ConsensusRequest) returns (ConsensusResponse);
rpc HeartBeat(HeartBeatRequest) returns (HeartBeatResponse);
}
message VoteRequest {
string term = 1;
string candidate_id = 2;
int64 last_log_index = 3;
string last_log_term = 4;
int64 timestamp = 5;
}メリット:
- 高性能: Protocol Buffersでバイナリシリアライゼーション
- 型安全: 強い型チェック、自動コード生成
- ストリーミング: リアルタイム通信に最適
- 多言語対応: Node.js、Go、Rust等で統一
class ConsensusWebSocket {
private connections: Map<string, WebSocket> = new Map();
async broadcastVoteRequest(voteRequest: VoteRequest): Promise<VoteResponse[]> {
const promises: Promise<VoteResponse>[] = [];
for (const [agentId, ws] of this.connections) {
if (ws.readyState === WebSocket.OPEN) {
promises.push(this.sendVoteRequest(ws, voteRequest));
}
}
return Promise.allSettled(promises);
}
private sendVoteRequest(ws: WebSocket, request: VoteRequest): Promise<VoteResponse> {
return new Promise((resolve, reject) => {
const messageId = generateUUID();
const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
this.pendingRequests.set(messageId, { resolve, reject, timeout });
ws.send(JSON.stringify({
type: 'VOTE_REQUEST',
messageId,
payload: request
}));
});
}
}// Raftリーダー選出用HTTP API
app.post('/consensus/vote', async (ctx) => {
const voteRequest: VoteRequest = ctx.request.body;
// 署名検証
if (!verifySignature(voteRequest)) {
return ctx.response.status(401).send({ error: 'Invalid signature' });
}
// Raftロジック
const voteResponse = await this.raftNode.handleVoteRequest(voteRequest);
return ctx.response.send(voteResponse);
});
// リアルタイム状態同期用SSE
app.get('/consensus/stream', (ctx) => {
ctx.response.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
const stream = this.consensusEventStream.subscribe(ctx.response);
ctx.request.on('close', () => {
stream.unsubscribe();
});
});class RaftNode {
private peers: Map<string, NodeConnection> = new Map();
private state: NodeState = 'follower';
private currentTerm = 0;
private log: LogEntry[] = [];
async startElection(): Promise<boolean> {
this.state = 'candidate';
this.currentTerm++;
// 全ピアに並列投票要求
const votePromises = Array.from(this.peers.values()).map(peer =>
peer.requestVote({
term: this.currentTerm,
candidateId: this.nodeId,
lastLogIndex: this.log.length - 1,
lastLogTerm: this.getLastLogTerm()
})
);
const votes = await Promise.allSettled(votePromises);
const supportVotes = votes.filter(v =>
v.status === 'fulfilled' && v.value.voteGranted
).length;
return (supportVotes + 1) > Math.floor(this.peers.size / 2); // +1は自分の票
}
}class ConsensusCoordinator {
private agents: Map<string, AgentConnection> = new Map();
async performByzantineConsensus(serviceId: string): Promise<ConsensusResult> {
// Phase 1: 監視結果収集
const checkPromises = Array.from(this.agents.values()).map(agent =>
agent.performMonitorCheck(serviceId)
);
const checks = await Promise.allSettled(checkPromises);
const validChecks = checks
.filter(c => c.status === 'fulfilled')
.map(c => c.value)
.filter(check => this.verifyCheckSignature(check));
// Phase 2: Byzantine合意
const consensus = this.calculateByzantineConsensus(validChecks);
// Phase 3: 結果ブロードキャスト
await this.broadcastConsensusResult(consensus);
return consensus;
}
}| 方式 | レイテンシ | 実装難易度 | 障害検出 | スケーラビリティ |
|---|---|---|---|---|
| gRPC | 🔥 極低 | 🟡 中 | 🔥 高精度 | 🔥 優秀 |
| WebSocket | 🔥 極低 | 🟢 低 | 🟡 中程度 | 🟡 中程度 |
| HTTP/2 | 🟡 低 | 🟢 低 | 🟢 簡単 | 🟢 良好 |
| TCP Socket | 🔥 極低 | 🔴 高 | 🔥 高精度 | 🟡 中程度 |
# 依存関係
npm install @grpc/grpc-js @grpc/proto-loader
npm install -D grpc-tools// フォールバック機能
class HybridConsensusTransport {
async sendMessage(target: string, message: any): Promise<any> {
try {
// メイン: gRPC
return await this.grpcClient.send(target, message);
} catch (error) {
// フォールバック: HTTP/2
console.warn('gRPC failed, falling back to HTTP/2');
return await this.httpClient.post(`http://${target}/consensus`, message);
}
}
}// UI更新用
class ConsensusEventBroadcaster {
broadcast(event: ConsensusEvent): void {
this.webSocketServer.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
type: 'CONSENSUS_UPDATE',
data: event
}));
}
});
}
}# プロジェクト作成
mkdir grpc-learning
cd grpc-learning
npm init -y
npm install @grpc/grpc-js @grpc/proto-loader
npm install -D typescript @types/node ts-node// protos/greeter.proto
syntax = "proto3";
package greeter;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloResponse);
rpc SayHelloStream (HelloRequest) returns (stream HelloResponse);
}
message HelloRequest {
string name = 1;
int32 age = 2;
}
message HelloResponse {
string message = 1;
int64 timestamp = 2;
}// server.ts
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import * as path from 'path';
const PROTO_PATH = path.join(__dirname, 'protos/greeter.proto');
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const greeterProto = grpc.loadPackageDefinition(packageDefinition) as any;
// サービス実装
const greeterService = {
sayHello: (call: any, callback: any) => {
const { name, age } = call.request;
callback(null, {
message: `Hello ${name}! You are ${age} years old.`,
timestamp: Date.now()
});
},
sayHelloStream: (call: any) => {
const { name, age } = call.request;
let count = 0;
const interval = setInterval(() => {
call.write({
message: `Stream message ${++count} for ${name}`,
timestamp: Date.now()
});
if (count >= 5) {
clearInterval(interval);
call.end();
}
}, 1000);
}
};
const server = new grpc.Server();
server.addService(greeterProto.greeter.Greeter.service, greeterService);
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
console.log('🚀 gRPC Server running on port 50051');
server.start();
});// client.ts
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import * as path from 'path';
const PROTO_PATH = path.join(__dirname, 'protos/greeter.proto');
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const greeterProto = grpc.loadPackageDefinition(packageDefinition) as any;
const client = new greeterProto.greeter.Greeter('localhost:50051', grpc.credentials.createInsecure());
// 通常のRPC呼び出し
client.sayHello({ name: 'Alice', age: 25 }, (error: any, response: any) => {
if (error) {
console.error('Error:', error);
} else {
console.log('Response:', response);
}
});
// ストリーミングRPC
const streamCall = client.sayHelloStream({ name: 'Bob', age: 30 });
streamCall.on('data', (response: any) => {
console.log('Stream data:', response);
});
streamCall.on('end', () => {
console.log('Stream ended');
});// protos/monitor.proto
syntax = "proto3";
package monitor;
service MonitorService {
rpc CheckHealth (HealthRequest) returns (HealthResponse);
rpc StartMonitoring (MonitorRequest) returns (stream MonitorEvent);
rpc RegisterAgent (AgentInfo) returns (AgentResponse);
}
message HealthRequest {
string target_url = 1;
int32 timeout_ms = 2;
map<string, string> headers = 3;
}
message HealthResponse {
bool is_healthy = 1;
int32 response_time_ms = 2;
int32 status_code = 3;
string error_message = 4;
int64 timestamp = 5;
}
message MonitorRequest {
string service_id = 1;
string target_url = 2;
int32 interval_seconds = 3;
}
message MonitorEvent {
string service_id = 1;
HealthResponse health = 2;
string event_type = 3; // "check_result", "service_down", "service_up"
}
message AgentInfo {
string agent_id = 1;
string region = 2;
repeated string capabilities = 3;
}
message AgentResponse {
bool success = 1;
string message = 2;
}- HTTP監視機能: axios等でURL監視
- ストリーミング: リアルタイム監視結果配信
- エラーハンドリング: gRPCエラーコード活用
- メタデータ: 認証ヘッダー等の追加
// protos/consensus.proto
syntax = "proto3";
package consensus;
service ConsensusService {
rpc RequestVote (VoteRequest) returns (VoteResponse);
rpc SubmitProposal (Proposal) returns (ProposalResponse);
rpc GetConsensusState (Empty) returns (ConsensusState);
}
message VoteRequest {
string term = 1;
string candidate_id = 2;
string voter_id = 3;
}
message VoteResponse {
bool vote_granted = 1;
string current_term = 2;
string message = 3;
}
message Proposal {
string proposal_id = 1;
string data = 2;
string proposer_id = 3;
}
message ProposalResponse {
bool accepted = 1;
int32 vote_count = 2;
int32 required_votes = 3;
}
message Empty {}
message ConsensusState {
string current_leader = 1;
string current_term = 2;
int32 total_nodes = 3;
repeated string active_nodes = 4;
}// gRPCエラーハンドリング
import { status } from '@grpc/grpc-js';
const handleGrpcError = (error: any) => {
switch (error.code) {
case status.UNAVAILABLE:
console.log('🔴 Service unavailable');
break;
case status.DEADLINE_EXCEEDED:
console.log('⏱️ Request timeout');
break;
case status.PERMISSION_DENIED:
console.log('🚫 Permission denied');
break;
default:
console.log('❌ Unknown error:', error.message);
}
};// レスポンス時間測定
const measureGrpcCall = async (client: any, method: string, request: any) => {
const start = Date.now();
return new Promise((resolve, reject) => {
client[method](request, (error: any, response: any) => {
const duration = Date.now() - start;
console.log(`📊 ${method} took ${duration}ms`);
if (error) reject(error);
else resolve(response);
});
});
};- Level 1: 基本的なgRPCサーバー・クライアント作成
- Level 2: ストリーミングRPC実装
- Level 3: エラーハンドリング完璧マスター
- Level 4: メタデータとインターセプター理解
- Level 5: パフォーマンス最適化とロードバランシング
Level 1〜3をクリアしたら、StatusGuardの実際の合意アルゴリズム実装に挑戦〜!💪✨