othe ffiles
هذا الالتزام موجود في:
20
.ghaymah.json
Normal file
20
.ghaymah.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000001",
|
||||
"name": "nodeapp-production",
|
||||
"projectId": "00000000-0000-0000-0000-000000000002",
|
||||
"ports": [
|
||||
{
|
||||
"expose": true,
|
||||
"number": 5000
|
||||
}
|
||||
],
|
||||
"publicAccess": {
|
||||
"enabled": true,
|
||||
"domain": "auto"
|
||||
},
|
||||
"resourceTier": "t1",
|
||||
"container": {
|
||||
"image": "docker.io/your-username/nodeapp:production",
|
||||
"pullSecretName": ""
|
||||
}
|
||||
}
|
||||
15
metrics.json
Normal file
15
metrics.json
Normal file
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"timestamp": "2026-07-29T02:06:33.694428Z",
|
||||
"latency_ms": 486.49,
|
||||
"uptime": true,
|
||||
"status_code": 200,
|
||||
"dns_ms": 0.48,
|
||||
"ssl_valid": false,
|
||||
"ssl_expires": "Sep 15 13:10:47 2026 GMT",
|
||||
"ssl_days_remaining": null,
|
||||
"search_ms": 473.48,
|
||||
"search_status_code": 200,
|
||||
"overall": false
|
||||
}
|
||||
]
|
||||
6
q1-deploy-monitor/Dockerfile
Normal file
6
q1-deploy-monitor/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM node:16.3.0-alpine
|
||||
COPY . /app
|
||||
RUN mkdir -p /logs && touch /logs/logs.txt
|
||||
WORKDIR /app
|
||||
RUN npm install
|
||||
CMD ["node", "/app/app.js"]
|
||||
82
q1-deploy-monitor/app.js
Normal file
82
q1-deploy-monitor/app.js
Normal file
@@ -0,0 +1,82 @@
|
||||
const express = require('express')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const app = express()
|
||||
const port = 5000
|
||||
const logDir = '/logs'
|
||||
const logFile = path.join(logDir, 'logs.txt')
|
||||
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true })
|
||||
}
|
||||
|
||||
function getStatsFromLogFile() {
|
||||
if (!fs.existsSync(logFile)) {
|
||||
return { requestCount: 0, averageResponseTime: 0 }
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(logFile, 'utf8')
|
||||
const lines = content.split('\n').filter(Boolean)
|
||||
const requestCount = lines.length
|
||||
|
||||
if (requestCount === 0) {
|
||||
return { requestCount: 0, averageResponseTime: 0 }
|
||||
}
|
||||
|
||||
const totalResponseTime = lines.reduce((total, line) => {
|
||||
const parts = line.trim().split(' ')
|
||||
const duration = parseFloat(parts[1])
|
||||
return total + (Number.isNaN(duration) ? 0 : duration)
|
||||
}, 0)
|
||||
|
||||
return {
|
||||
requestCount,
|
||||
averageResponseTime: totalResponseTime / requestCount
|
||||
}
|
||||
}
|
||||
|
||||
app.use('/dashboard', express.static(path.join(__dirname, 'dashboard'), { index: false }))
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const startTime = Date.now()
|
||||
const requestTime = new Date(startTime).toISOString()
|
||||
|
||||
res.on('finish', () => {
|
||||
const durationSeconds = (Date.now() - startTime) / 1000
|
||||
const line = `${req.path} ${durationSeconds.toFixed(3)} ${requestTime} ${res.statusCode}\n`
|
||||
|
||||
fs.appendFile(logFile, line, (err) => {
|
||||
if (err) console.error('Failed to write request log:', err)
|
||||
})
|
||||
})
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
app.get('/dashboard', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'dashboard', 'index.html'))
|
||||
})
|
||||
|
||||
app.get('/api/stats', (req, res) => {
|
||||
const { requestCount, averageResponseTime } = getStatsFromLogFile()
|
||||
|
||||
res.json({
|
||||
requestCount,
|
||||
averageResponseTime,
|
||||
status: 'up'
|
||||
})
|
||||
})
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.send('Hello World from GHAYMAH')
|
||||
})
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.send('The app up and runing')
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Example app listening at http://localhost:${port}`)
|
||||
console.log(`Dashboard available at http://localhost:${port}/dashboard`)
|
||||
})
|
||||
49
q1-deploy-monitor/dashboard.js
Normal file
49
q1-deploy-monitor/dashboard.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const requestCountEl = document.getElementById('requestCount')
|
||||
const averageTimeEl = document.getElementById('averageTime')
|
||||
const appStatusEl = document.getElementById('appStatus')
|
||||
const statusBadgeEl = document.getElementById('statusBadge')
|
||||
const statusTextEl = document.getElementById('statusText')
|
||||
const lastUpdatedEl = document.getElementById('lastUpdated')
|
||||
|
||||
function setStatus(isUp) {
|
||||
statusBadgeEl.classList.remove('up', 'down')
|
||||
appStatusEl.classList.remove('up', 'down')
|
||||
|
||||
if (isUp) {
|
||||
statusBadgeEl.classList.add('up')
|
||||
appStatusEl.classList.add('up')
|
||||
statusTextEl.textContent = 'Online'
|
||||
appStatusEl.textContent = 'Up'
|
||||
return
|
||||
}
|
||||
|
||||
statusBadgeEl.classList.add('down')
|
||||
appStatusEl.classList.add('down')
|
||||
statusTextEl.textContent = 'Offline'
|
||||
appStatusEl.textContent = 'Down'
|
||||
}
|
||||
|
||||
async function fetchStats() {
|
||||
try {
|
||||
const response = await fetch('/api/stats')
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Stats request failed')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
requestCountEl.textContent = data.requestCount.toLocaleString()
|
||||
averageTimeEl.textContent = `${data.averageResponseTime.toFixed(3)}s`
|
||||
setStatus(data.status === 'up')
|
||||
lastUpdatedEl.textContent = `Last updated: ${new Date().toLocaleTimeString()}`
|
||||
} catch (error) {
|
||||
requestCountEl.textContent = '—'
|
||||
averageTimeEl.textContent = '—'
|
||||
setStatus(false)
|
||||
lastUpdatedEl.textContent = 'Last updated: failed to connect'
|
||||
}
|
||||
}
|
||||
|
||||
fetchStats()
|
||||
setInterval(fetchStats, 5000)
|
||||
13
q1-deploy-monitor/health-check.py
Normal file
13
q1-deploy-monitor/health-check.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from urllib.request import urlopen
|
||||
from urllib.error import HTTPError
|
||||
import time
|
||||
|
||||
|
||||
while True:
|
||||
try:
|
||||
with urlopen('https://my-application-2f85feb054d8.hosted.ghaymah.systems/health') as response:
|
||||
print(f"Status Code: {response.status}")
|
||||
|
||||
except HTTPError as error:
|
||||
print(f"Error Status Code: {error.code}")
|
||||
time.sleep(30)
|
||||
50
q1-deploy-monitor/index.html
Normal file
50
q1-deploy-monitor/index.html
Normal file
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Server Dashboard</title>
|
||||
<link rel="stylesheet" href="/dashboard/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="dashboard">
|
||||
<header class="header">
|
||||
<div>
|
||||
<h1>Server Dashboard</h1>
|
||||
<p class="subtitle">Live monitoring for your Node.js application</p>
|
||||
</div>
|
||||
<div class="status-badge" id="statusBadge">
|
||||
<span class="status-dot"></span>
|
||||
<span id="statusText">Checking...</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="cards">
|
||||
<section class="card">
|
||||
<p class="card-label">Total Requests</p>
|
||||
<p class="card-value" id="requestCount">—</p>
|
||||
<p class="card-hint">All requests received by the server</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<p class="card-label">Average Response Time</p>
|
||||
<p class="card-value" id="averageTime">—</p>
|
||||
<p class="card-hint">Mean duration in seconds</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<p class="card-label">Application Status</p>
|
||||
<p class="card-value status-value" id="appStatus">—</p>
|
||||
<p class="card-hint">Based on health check</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<span id="lastUpdated">Last updated: —</span>
|
||||
<span>Auto-refresh every 5 seconds</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="/dashboard/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
18
q1-deploy-monitor/package.json
Normal file
18
q1-deploy-monitor/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "nodeapp",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.14"
|
||||
}
|
||||
}
|
||||
158
q1-deploy-monitor/style.css
Normal file
158
q1-deploy-monitor/style.css
Normal file
@@ -0,0 +1,158 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
|
||||
min-height: 100vh;
|
||||
color: #e2e8f0;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-top: 0.35rem;
|
||||
color: #94a3b8;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.65rem 1.1rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #64748b;
|
||||
box-shadow: 0 0 0 0 rgba(100, 116, 139, 0.5);
|
||||
}
|
||||
|
||||
.status-badge.up .status-dot {
|
||||
background: #22c55e;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-badge.down .status-dot {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.status-badge.up {
|
||||
border-color: rgba(34, 197, 94, 0.35);
|
||||
color: #bbf7d0;
|
||||
}
|
||||
|
||||
.status-badge.down {
|
||||
border-color: rgba(239, 68, 68, 0.35);
|
||||
color: #fecaca;
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(30, 41, 59, 0.85);
|
||||
border: 1px solid rgba(148, 163, 184, 0.15);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
transition: transform 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(96, 165, 250, 0.35);
|
||||
}
|
||||
|
||||
.card-label {
|
||||
color: #94a3b8;
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 2.4rem;
|
||||
font-weight: 700;
|
||||
color: #f8fafc;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.status-value.up {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.status-value.down {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.card-hint {
|
||||
margin-top: 0.75rem;
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.5);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 8px rgba(34, 197, 94, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
0
q2-postmortem/postmortem-report.md
Normal file
0
q2-postmortem/postmortem-report.md
Normal file
175
q3-cicd/workflow.yml
Normal file
175
q3-cicd/workflow.yml
Normal file
@@ -0,0 +1,175 @@
|
||||
name: CI/CD - Ghaymah Cloud
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, staging]
|
||||
pull_request:
|
||||
branches: [main, staging]
|
||||
|
||||
env:
|
||||
DOCKERHUB_REPOSITORY: ${{ secrets.DOCKERHUB_REPOSITORY }}
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and Push to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
outputs:
|
||||
environment: ${{ steps.env.outputs.environment }}
|
||||
image_uri: ${{ steps.image.outputs.image_uri }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
|
||||
- name: Set deployment environment
|
||||
id: env
|
||||
run: |
|
||||
if [ "${{ github.ref_name }}" = "main" ]; then
|
||||
echo "environment=production" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "environment=staging" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./nodeapp
|
||||
push: true
|
||||
tags: |
|
||||
docker.io/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.DOCKERHUB_REPOSITORY }}:production
|
||||
docker.io/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.DOCKERHUB_REPOSITORY }}:staging
|
||||
docker.io/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.DOCKERHUB_REPOSITORY }}:${{ github.sha }}
|
||||
|
||||
- name: Set deployment image
|
||||
id: image
|
||||
run: |
|
||||
IMAGE_BASE="docker.io/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.DOCKERHUB_REPOSITORY }}"
|
||||
if [ "${{ steps.env.outputs.environment }}" = "production" ]; then
|
||||
echo "image_uri=${IMAGE_BASE}:production" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "image_uri=${IMAGE_BASE}:staging" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build:
|
||||
name: Build Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Build Docker image
|
||||
run: docker build ./nodeapp
|
||||
|
||||
deploy-staging:
|
||||
name: Deploy to Staging
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-and-push
|
||||
if: github.event_name == 'push' && github.ref_name == 'staging'
|
||||
environment:
|
||||
name: staging
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Install Ghaymah CLI
|
||||
run: curl -sSL https://cli.ghaymah.systems/install.sh | bash
|
||||
|
||||
- name: Login to Ghaymah
|
||||
run: $HOME/ghaymah/bin/gy auth login --email "${{ secrets.GHAYMAH_EMAIL }}" --password "${{ secrets.GHAYMAH_PW }}"
|
||||
|
||||
- name: Create Ghaymah configuration
|
||||
working-directory: nodeapp
|
||||
env:
|
||||
IMAGE_URI: ${{ needs.build-and-push.outputs.image_uri }}
|
||||
PROJECT_ID: ${{ secrets.GHAYMAH_PROJECT_ID }}
|
||||
APP_ID: ${{ secrets.GHAYMAH_APP_ID_STAGING }}
|
||||
PULL_SECRET: ${{ secrets.DOCKERHUB_PULL_SECRET_NAME }}
|
||||
run: |
|
||||
jq -n \
|
||||
--arg id "${APP_ID}" \
|
||||
--arg name "nodeapp-staging" \
|
||||
--arg projectId "${PROJECT_ID}" \
|
||||
--arg image "${IMAGE_URI}" \
|
||||
--arg pullSecret "${PULL_SECRET}" \
|
||||
'{
|
||||
id: $id,
|
||||
name: $name,
|
||||
projectId: $projectId,
|
||||
ports: [{ expose: true, number: 5000 }],
|
||||
publicAccess: {
|
||||
enabled: true,
|
||||
domain: "auto"
|
||||
},
|
||||
resourceTier: "t1",
|
||||
container: {
|
||||
image: $image,
|
||||
pullSecretName: $pullSecret
|
||||
}
|
||||
}' > .ghaymah.json
|
||||
|
||||
cat .ghaymah.json
|
||||
|
||||
- name: Deploy to Ghaymah
|
||||
working-directory: nodeapp
|
||||
run: $HOME/ghaymah/bin/gy resource app launch --env staging
|
||||
|
||||
deploy-production:
|
||||
name: Deploy to Production
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-and-push
|
||||
if: github.event_name == 'push' && github.ref_name == 'main'
|
||||
environment:
|
||||
name: production
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Install Ghaymah CLI
|
||||
run: curl -sSL https://cli.ghaymah.systems/install.sh | bash
|
||||
|
||||
- name: Login to Ghaymah
|
||||
run: $HOME/ghaymah/bin/gy auth login --email "${{ secrets.GHAYMAH_EMAIL }}" --password "${{ secrets.GHAYMAH_PW }}"
|
||||
|
||||
- name: Create Ghaymah configuration
|
||||
working-directory: nodeapp
|
||||
env:
|
||||
IMAGE_URI: ${{ needs.build-and-push.outputs.image_uri }}
|
||||
PROJECT_ID: ${{ secrets.GHAYMAH_PROJECT_ID }}
|
||||
APP_ID: ${{ secrets.GHAYMAH_APP_ID_PRODUCTION }}
|
||||
PULL_SECRET: ${{ secrets.DOCKERHUB_PULL_SECRET_NAME }}
|
||||
run: |
|
||||
jq -n \
|
||||
--arg id "${APP_ID}" \
|
||||
--arg name "nodeapp-production" \
|
||||
--arg projectId "${PROJECT_ID}" \
|
||||
--arg image "${IMAGE_URI}" \
|
||||
--arg pullSecret "${PULL_SECRET}" \
|
||||
'{
|
||||
id: $id,
|
||||
name: $name,
|
||||
projectId: $projectId,
|
||||
ports: [{ expose: true, number: 5000 }],
|
||||
publicAccess: {
|
||||
enabled: true,
|
||||
domain: "auto"
|
||||
},
|
||||
resourceTier: "t1",
|
||||
container: {
|
||||
image: $image,
|
||||
pullSecretName: $pullSecret
|
||||
}
|
||||
}' > .ghaymah.json
|
||||
|
||||
cat .ghaymah.json
|
||||
|
||||
- name: Deploy to Ghaymah
|
||||
working-directory: nodeapp
|
||||
run: $HOME/ghaymah/bin/gy resource app launch --env production
|
||||
ثنائية
q4-scalability/architecture.png
Normal file
ثنائية
q4-scalability/architecture.png
Normal file
ملف ثنائي غير معروض.
|
بعد العرض: | الارتفاع: | الحجم: 1.4 MiB |
170
q4-scalability/calculations.md
Normal file
170
q4-scalability/calculations.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# Q4 — قابلية التوسع وتوزيع الأحمال
|
||||
|
||||
## 1. Architecture Diagram
|
||||
|
||||
راجع الملف `architecture.png` في هذا المجلد.
|
||||
|
||||
يوضّح المخطط تدفّق **15,000 req/s** عبر:
|
||||
|
||||
| الطبقة | المكوّن | الدور |
|
||||
|--------|---------|-------|
|
||||
| Edge | Ghaymah Load Balancer (L7) | توزيع الطلبات، SSL، Health Checks |
|
||||
| Compute | Auto Scaling Group — 43 حاوية | معالجة الطلبات (500 req/s لكل حاوية) |
|
||||
| Stateful | Ghaymah Block Storage | تخزين دائم للـ stateful workloads |
|
||||
| Cache/CDN | Object Storage / CDN | الملفات الثابتة والوسائط |
|
||||
| Ops | Monitoring & Alerts | مقاييس، تنبيهات، قرارات التوسع |
|
||||
| Warm Pool | Cold Start Pool | حاويات مُسخَّنة مسبقاً لتقليل زمن الإقلاع |
|
||||
|
||||
---
|
||||
|
||||
## 2. حساب عدد الحاويات
|
||||
|
||||
### المعطيات
|
||||
|
||||
| المتغير | القيمة |
|
||||
|---------|--------|
|
||||
| إجمالي الحمل | **15,000 req/s** |
|
||||
| سعة الحاوية الواحدة | **500 req/s** |
|
||||
| هامش الأمان | **30%** |
|
||||
|
||||
### المنطق
|
||||
|
||||
الهامش 30% يعني **عدم تشغيل الحاويات فوق 70%** من سعتها القصوى، لترك مساحة للذروات والتقلبات:
|
||||
|
||||
```
|
||||
السعة الفعلية القابلة للتخطيط = 500 × (1 − 0.30) = 500 × 0.70 = 350 req/s لكل حاوية
|
||||
```
|
||||
|
||||
### الحساب
|
||||
|
||||
```
|
||||
عدد الحاويات = ⌈ 15,000 ÷ 350 ⌉
|
||||
= ⌈ 42.857 ⌉
|
||||
= 43 حاوية
|
||||
```
|
||||
|
||||
### التحقق
|
||||
|
||||
```
|
||||
43 × 350 = 15,050 req/s ✅ (يغطي 15,000 req/s مع هامش 30%)
|
||||
```
|
||||
|
||||
### ملخص
|
||||
|
||||
| البند | القيمة |
|
||||
|-------|--------|
|
||||
| الحد الأدنى نظرياً (بدون هامش) | 30 حاوية |
|
||||
| **العدد الموصى به (مع هامش 30%)** | **43 حاوية** |
|
||||
| السعة الإجمالية الفعلية | 15,050 req/s |
|
||||
| نسبة الاستخدام عند 15K req/s | ≈ 99.7% من السعة المُخطَّطة |
|
||||
|
||||
> **ملاحظة:** إذا قصدت إضافة 30% فوق العدد الأساسي: `30 × 1.30 = 39 حاوية`.
|
||||
> في سياق SRE، هامش السعة (capacity headroom) هو التفسير الأدق — أي 43 حاوية.
|
||||
|
||||
---
|
||||
|
||||
## 3. استراتيجية Cold Start للحاويات الجديدة
|
||||
|
||||
عند التوسع الأفقي (scale-out)، الحاوية الجديدة تحتاج وقتاً قبل أن تستقبل حركة. الاستراتيجية المقترحة:
|
||||
|
||||
### أ) Warm Pool (مجموعة تسخين)
|
||||
|
||||
- الإبقاء على **3–5 حاويات جاهزة** في حالة `standby` (مُشغَّلة ومُسخَّنة، لكن خارج rotation الـ Load Balancer).
|
||||
- عند ارتفاع الحمل، تُضاف فوراً إلى الـ LB **بدون** انتظار pull للصورة أو boot.
|
||||
|
||||
### ب) Pre-pull الصور
|
||||
|
||||
- تخزين صورة Docker في **Ghaymah Internal Registry** داخل نفس المنطقة.
|
||||
- تقليل زمن `image pull` من دقائق إلى ثوانٍ.
|
||||
|
||||
### ج) Readiness Probe تدريجي
|
||||
|
||||
```
|
||||
Startup Probe → Readiness Probe → إضافة للـ LB
|
||||
(30s) (HTTP /health)
|
||||
```
|
||||
|
||||
- لا تُوجَّه الطلبات للحاوية حتى تمر `/health` بنجاح.
|
||||
- يمنع إرسال حركة لحاوية لم تكتمل تهيئتها.
|
||||
|
||||
### د) Graceful Scale-In
|
||||
|
||||
- عند التقليص: إرسال `SIGTERM` → انتظار إنهاء الطلبات الجارية (drain) → إزالة من LB → إيقاف.
|
||||
- يمنع قطع الطلبات أثناء التوسع العكسي.
|
||||
|
||||
### هـ) Predictive Scaling
|
||||
|
||||
- مراقبة CPU / RPS / latency.
|
||||
- بدء التوسع **قبل** الوصول للحد (مثلاً عند 60% utilization) وليس عند 90%.
|
||||
|
||||
### ف) Application Warm-up
|
||||
|
||||
- عند الإقلاع: تحميل cache محلي، pre-connect لقاعدة البيانات، JIT warm-up.
|
||||
- endpoint `/warmup` يُستدعى تلقائياً بعد readiness.
|
||||
|
||||
---
|
||||
|
||||
## 4. Ghaymah Block Storage للـ Stateful Workloads
|
||||
|
||||
### ما هو Block Storage؟
|
||||
|
||||
تخزين **كتلة (block-level)** يُ attach كقرص افتراضي (volume) لحاوية أو خدمة — مثل `/dev/vdb` — ويحتفظ بالبيانات **بعد** إعادة تشغيل أو استبدال الحاوية.
|
||||
|
||||
### متى نستخدمه؟
|
||||
|
||||
| Stateful | Stateless (لا يحتاج Block Storage) |
|
||||
|----------|-------------------------------------|
|
||||
| PostgreSQL / MySQL | API servers (43 حاوية أعلاه) |
|
||||
| Redis persistence (AOF/RDB) | Static assets → Object Storage |
|
||||
| File uploads / media processing | Session في Redis مشترك |
|
||||
| Logs طويلة الأمد | |
|
||||
|
||||
### كيف يُستخدم على غيمة؟
|
||||
|
||||
1. **إنشاء Volume**
|
||||
- حجم مناسب (مثلاً 100 GB SSD) في نفس منطقة التطبيق.
|
||||
|
||||
2. **Mount على الحاوية Stateful**
|
||||
```text
|
||||
/data/postgresql ← Ghaymah Block Volume (persistent)
|
||||
```
|
||||
|
||||
3. **فصل Compute عن Storage**
|
||||
- حاويات الـ API **stateless** — تتوسع أفقياً بحرية.
|
||||
- قاعدة البيانات **stateful** — volume واحد (أو cluster مع replication).
|
||||
|
||||
4. **Snapshots & Backup**
|
||||
- Ghaymah Backup: snapshots دورية للـ volume.
|
||||
- استرداد Point-in-Time عند الفشل.
|
||||
|
||||
5. **High Availability**
|
||||
- Primary DB على volume + Replica على volume منفصل.
|
||||
- Failover تلقائي عند سقوط Primary.
|
||||
|
||||
### في مخططنا (architecture.png)
|
||||
|
||||
```
|
||||
Load Balancer → 43 App Containers (stateless)
|
||||
↓
|
||||
Stateful DB Pod ← Ghaymah Block Storage (persistent volume)
|
||||
```
|
||||
|
||||
- الـ **43 حاوية** لا تخزّن state محلياً.
|
||||
- كل البيانات الدائمة على **Block Storage** م attached لطبقة DB/Cache الم persistent.
|
||||
|
||||
### الفوائد
|
||||
|
||||
| الفائدة | الشرح |
|
||||
|---------|--------|
|
||||
| Persistence | البيانات تبقى بعد restart/redeploy |
|
||||
| Performance | IOPS مضمون لقواعد البيانات |
|
||||
| Scalability | فصل التوسع الأفقي (API) عن التخزين (DB) |
|
||||
| DR | Snapshots + restore سريع |
|
||||
|
||||
---
|
||||
|
||||
## المراجع
|
||||
|
||||
- [Ghaymah Cloud](https://ghaymah.systems/)
|
||||
- [Ghaymah CLI — `gy resource app launch`](https://cli.ghaymah.systems/)
|
||||
- Health & Retry: HTTP/TCP probes — down-nodes تخرج فوراً من rotation
|
||||
المرجع في مشكلة جديدة
حظر مستخدم