การพัฒนา Web Application แบบ (เกือบจะ) Zero Downtime บน Swarm Cluster

บทความโดย ผศ.ดร.ณัฐโชติ พรหมฤทธิ์
ภาควิชาคอมพิวเตอร์
คณะวิทยาศาสตร์
มหาวิทยาลัยศิลปากร

ในช่วงหนึ่งสัปดาห์ที่ผ่านมา ถ้าถามว่าเว็บไซต์ไหนถูกกล่าวถึงมากที่สุด คำตอบคงจะหนีไม่พ้น Website https://www.เราไม่ทิ้งกัน.com

www.เราไม่ทิ้งกัน.com เป็น Website ที่เปิดให้ลูกจ้างของสถานประกอบการ หรือผู้ได้รับผลกระทบจากการแพร่ระบาดของไวรัส COVID-19 ลงทะเบียนเพื่อขอรับเงินเยียวยาจำนวน 5,000 บาทต่อคน เป็นเวลา 3 เดือน ซึ่งจนถึงขณะที่เขียนบนความ ได้มียอดลงทะเบียนมากกว่า 19 ล้านรายแล้ว

แม้ว่าในช่วงแรกจะเกิดปัญหาเว็บล่ม แต่ก็มีการกู้คืนกลับมาได้ในเวลาไม่นาน

การพัฒนา Website ที่มี High Availability โดยสามารถรองรับ User จำนวนมาก ถึง 3.48 ล้านคนต่อนาที (ตามข่าว) เป็นเรื่องท้าทายมาก วันนี้เราจะพัฒนา Website ที่เป็น Web Application ซึ่งต้อง Online ได้ตลอดเวลา โดยใช้ความสามารถของ Docker Swarm ในเรื่องของการนําเอา Cloud Server หลายๆ เครื่องมาช่วยกันทํางาน รวมทั้งการทำ Load Balance ที่ทำให้ผู้พัฒนาสามารถ Scale Out Microservice เช่น ระบบลงทะเบียนนักศึกษาได้อย่างง่ายๆ

ซึ่งในที่สุดแล้วเราจะสามารถเพิ่มประสิทธิภาพให้ระบบสามารถรองรับ User ได้เป็นจำนวนเท่าไหร่นั้น โปรดติดตามกันได้ครับ

หมายเหตุ 1 สำหรับผู้อ่านใหม่ที่ต้องการจะ Implement ตาม สามารถอ่านบทความ 4 บทความก่อนหน้านี้ได้

หมายเหตุ 2 ให้เปลี่ยน Private IP Address 10.148.0.15 ในบนความนี้ทั้งหมดเป็น IP Address บน Cloud Server ของตัวเอง

Current Architecture on Cloud

การจะพัฒนา Web Application ให้มี High Availability หรือแบบ (เกือบจะ) Zero Downtime บน Swarm Cluster เราต้องเข้าใจโครงสร้างสถาปัตยกรรมทั้งระบบ เพื่อจะหาจุดที่จะเพิ่มประสิทธิภาพ

จากภาพ Traffic ด้านหน้าที่มาจาก Internet จะเป็น HTTPS ซึ่งในชั้นแรกจะวิ่งผ่าน Nginx Revers Proxy

โดย Traffic ที่มายัง www.labxx.cpsudevops.com จะถูก Route ไปที่ UI Register Service แต่เมื่อ UI Register Service มีการเรียกใช้ API มันจะถูกยิงผ่าน Kong ไปยัง Register Gateway และ OTP Gateway ซึ่งทำหน้าที่ประสานงานกับ Service อื่นๆ ที่อยู่ภายใน

เราจะมาดู Stack, Microservice, APIs และ Networks ที่กำลังใช้กัน โดยแบ่งเป็นกลุ่มได้ 10 กลุ่ม ได้แก่ 1) Programming/Markup Languages 2) Frameworks 3) Database 4) Message Queue Brokers 5) Monitoring Tools 6) Server and Tools 7) Infrastructure 8) Microservice 9) APIs และ 10) Networks

Programming/Markup Languages

  • Python เป็นภาษาหลักที่ใช้กับ Microservice มีจุดเด่นที่เขียนง่าย มี Libraries ให้ใช้เยอะ และเป็นภาษา Script ที่เหล่า Data Scientist นิยมใช้งาน
  • HTML ใช้แสดง UI บน Web Browser

Frameworks

  • Flask เป็น Web Framework สำหรับ Python สามารถสร้างหน้า UI ได้จากการนำไฟล์ HTML Template มา Render รวมทั้งใช้สร้าง RESTful API ด้วย
  • FastAPI เป็น Web Framework สำหรับ Python อีกตัว ที่ใช้ง่าย เขียน Code สั้น สำหรับใช้สร้าง RESTful API
  • Nameko สำหรับ Implement RPC ด้วย Python บน Protocol AMQP (Advanced Message Queuing Protocol)

Database

  • MariaDB คือ MySQL ที่ถูกนำมา Fork ใช้เก็บข้อมูลใน Microservice
  • Redis เป็นระบบเก็บข้อมูลแบบ Key/Value บนหน่วยความจำ นำมาจัดเก็บ OTP และ Session ของ Web Application
  • PostgreSQL เป็น DB Engine ที่ได้รับความนิยมเป็น อันดับ 4 ใช้เก็บข้อมูลของ Kong และ Konga

Message Queue Brokers

  • RabbitMQ ทำหน้าที่เป็นท่อส่งข้อมูล สำหรับ Service ต่างๆ ที่สื่อสารกันผ่าน RPC

Monitoring Tools

  • Prometheus สำหรับจัดเก็บข้อมูลต่างๆ ของ Cloud Server และ Service ในแบบ Time Series
  • Grafana ใช้ทำ Real Time Dashboard

Server and Tools

  • Nginx ใช้ทำ Reverse Proxy
  • Kong and Konga ใช้ทำ API Gateway ซึ่งเป็นจุดศูนย์รวมของการควบคุมการเข้าถึง Internal API ที่เราเปิด Public ให้ผู้ใช้ภายนอกเรียกใช้งาน
  • SMTP Server ใช้สำหรับ Relay Email ไปยัง Relay Host เช่น smtp.live.com
  • phpMyAdmin เป็นเครื่องมือแบบ Web-based UI สำหรับบริหารจัดการฐานข้อมูล
  • Portainer เป็น Web-based UI สำหรับบริหารจัดการ Containers และ Swarm Cluster

Infrastructure

  • Docker สำหรับสร้าง Containers บนระบบปฏิบัติการ Linux
Container ทั้งหมดบน Docker ในปัจจุบัน
  • Nipa.cloud ผู้ให้การสนับสนุน Cloud Server ในการเรียนการสอนวิชา DevOps and Cloud Enginering 101

Microservice

  • OTP สำหรับเก็บรายการ Email ของผู้ผ่านการสอบสัมภาษณ์ รวมทั้งการพิสูจน์ตัวตนด้วย OTP
  • Send Email OTP ใช้ส่ง OTP สำหรับการพิสูจน์ตัวตนกลับทาง Email
  • OTP Gateway ทำหน้าที่ประสานงานกับ OTP Service และ Send Mail OTP Service รวมทั้งการรับ Request จากผู้ใช้ผ่าน RESTful
  • Student สำหรับเก็บข้อมูลนักศึกษา
  • Enroll สำหรับเก็บข้อมูลการลงทะเบียน
  • Email สำหรับส่งข้อความยืนยันการลงทะเบียนกลับทาง Email
  • Register Gateway ทำหน้าที่ประสานงานกับ Student Service,  Enroll RPC Service และ Email Service รวมทั้งการรับ Request จากผู้ใช้ผ่าน RESTful API
  • UI Register สำหรับสร้างหน้า UI ทั้งหมด 3 หน้า ได้แก่ 1) หน้าขอ OPT 2) หน้ายืนยันตัวตน และ 3) หน้าลงทะเบียนนักศึกษาใหม่

APIs

  • RESTful API สำหรับให้ UI Service เรียกใช้งาน
create_email_list
getotp
authen
register
  • RPC สำหรับให้ Microservice แต่ละ Service ที่เป็น Back-end คุยกัน
otp.create_email_list(email_list)
otp.create(email)
email_otp.send(otp, email)
otp.authen(email, otp)
otp.delete(email)
student.insert(firstname, lastname, email)
enroll.insert(studentID, firstname, lastname)
email.send(studentID, firstname, lastname, email)

Networks

  • Webproxy Network (Bridge)
  • Kong Network (Bridge)
  • Microservice Network (Bridge)
  • Student Network (Bridge)
  • Enroll Network (Bridge)
  • Email Network (Bridge)
  • OTP Network (Bridge)
  • Email OTP Network (Bridge)

Load Test with Apache JMeter

ภาพจาก https://jmeter.apache.org

Apache JMeter เป็นเครื่องมือในการทำ Performance Test แบบ Open Source Software ที่พัฒนาด้วยภาษา Java โดยสามารถติดตั้งได้ทั้งบน Windows และ MacOS

ในเบื้องต้นเราจะสร้าง Project ทำ Load Test ด้วย JMeter กับหน้าแรกของระบบลงทะเบียนนักศึกษา (www.labxx.cpsudevops.com) ซึ่ง Traffic จะถูก Route ไปที่ UI Register Service

JMeter Project สำหรับการทดลองของเราจะมีโครงสร้าง ดังนี้

Test Plan
|__ Thread Group1 (500)
|   |__ HTTP Request
|   |__ Constant Timer
|
|__ Thread Group2 (1000)
|   |__ HTTP Request
|   |__ Constant Timer
|
|__ Thread Group3 (1500)
|   |__ HTTP Request
|   |__ Constant Timer
|
|__ Thread Group4 (2000)
|   |__ HTTP Request
|   |__ Constant Timer
|
|__ Thread Group5 (2500)
|   |__ HTTP Request
|   |__ Constant Timer
|
|__ Summary Report
|__ Response Time Graph
|__ View Results Tree

Config JMeter ตามตัวอย่างด้านล่าง

  • สร้าง Thread Group จำนวน 5 Thread Group ให้ยิง Traffic ไปยัง www.labxx.cpsudevops.com วินาทีละ 500, 1000, 1500, 2000 และ 2500 Threads เพื่อจำลองการเข้าใช้งานพร้อมกันของ User และดูว่าระบบสามารถรองรับโหลดได้เท่าไหร่ก่อนที่จะเกิด Error
  • กำหนด HTTP Request ให้ยิงออกไปด้วย Protocol https ไปยัง Server Name www.labxx.cpsudevops.com และ Port Number 443 โดยใช้ Method GET
  • กำหนดช่วงเวลาพักในแต่ Thread Group 20 วินาที (Constant Timer)
  • เพิ่ม Summary Report จาก Test Plan เพื่อดู Response Time เปอร์เซ็นต์ Error และ Throughput ของแต่ละ Thread Group
  • เพิ่ม Response Time Graph จาก Test Plan เพื่อดู Response Time ทุกๆ 10 วินาที ตั้งแต่เริ่ม Test จนจบการ Test
  • เพิ่ม View Results Tree จาก Test Plan สำหรับดูข้อมูล Request/Reply ของแต่ละ Thread
  • กดปุ่ม Start แล้ว JMeter จะยิง Traffic ตามความถี่ที่กำหนดจนครบทุก Thread Group (500, 1000, 1500, 2000 และ 2500 User ต่อวินาที) แล้วดูผลการ Test จาก Summary Report, Response Time Graph และ View Results Tree

ซึ่งเราจะพิจารณาผลการ Test ได้จาก 3 Column หลักๆ จาก Summary Report คือ 1) Average (Response Time เฉลี่ย) 2) Error % (เปอร์เซ็นต์ของ Error เปรียบเทียบกับ Request Traffic ทั้งหมด) และ 3) Throughput (ความสามารถในการจัดการงาน หรือ Transaction มีหน่วยเป็น Request ต่อวินาที)

Statistics ของ register_ui Container ในขณะที่มีการ Test
Statistics ของ nginx-proxy Container ในขณะที่มีการ Test

จากการทดลองพบว่าเมื่อมีการยิง Traffic 500, 1000 และ 1500 User ต่อวินาที ระบบยังสามารถ Reply Message กลับมาได้ 100% โดยไม่เกิด Error ด้วย Response Time เฉลี่ย 1234, 2550 และ 3937 ms รวมทั้งมี Throughput 150.4, 145 และ 147 Request ต่อวินาที ตามลำดับ

ดังนั้นระบบสามารถรองรับ User ที่เข้ามายังหน้าแรกของ Website ได้ประมาณ 1500*60 = 90,000 คนต่อนาที โดยมี Throughput หรือความสามารถในการจัดการงาน 147*60 = 8,820 Request ต่อนาที ครับ

ทีนี้ลองเปรียบเทียบกับ Website Reg ใกล้ๆ เราบ้าง

จากการทดลองพบว่าเมื่อมีการยิง Traffic 500, 1000, 1500, 2000, 2500 User ต่อวินาที ระบบยังสามารถ Reply Message กลับมาได้ 100% โดยไม่เกิด Error ด้วย Response Time เฉลี่ย 419, 1228, 1861, 2929 และ 4280 ms รวมทั้งมี Throughput 293.9, 287, 290.4, 243 และ 281.3 Request ต่อวินาที ตามลำดับ

ดังนั้นระบบ Reg (ซึ่งไม่มี HTTPS) สามารถรองรับ User ที่เข้ามายังหน้าแรกของ Website ได้ไม่น้อยกว่า 2500*60 = 150,000 คนต่อนาที โดยมี Throughput หรือความสามารถในการจัดการงาน 281.3*60 = 16,878 Request ต่อนาที เร็วกว่าหน้าแรกของระบบลงทะเบียนนักศึกษาของเราถึง 2 เท่า!!

Web Cache

วิธีที่ง่ายวิธีหนึ่งในการเพิ่มประสิทธิภาพการ Load หน้าแรกของระบบลงทะเบียนนักศึกษา (www.labxx.cpsudevops.com) คือ การทำ Web Cache ด้วยการเก็บหน้าแรกไว้ในหน่วยความจำ โดยไม่ต้อง Read ไฟล์จาก HDD ทุกครั้งที่มีการ Request ตามขั้นตอนดังนี้

  • Remote Login ไปยัง Cloud Server โดยใช้ ssh
ssh nc-user@labxx.cpsudevops.com
  • ไปที่ Project register_ui_dock
  • แก้ไขไฟล์ requirements.txt
gunicorn
requests
Flask
Flask-Bootstrap
Flask-WTF
WTForms
redis
flask_session
flask_caching
email_validator
  • แก้ไขไฟล์ ui.py
from flask import Flask, request, render_template, redirect, session
from model import OTPForm, AuthenForm, RegForm
from flask_bootstrap import Bootstrap
from requests.auth import HTTPBasicAuth
import requests
import json
import redis
from flask_session import Session
from flask_caching import Cache

config = {
    'DEBUG': True,
    'CACHE_TYPE': 'simple',
    'CACHE_DEFAULT_TIMEOUT': 300
}

app = Flask(__name__)

app.config.from_mapping(config)
cache = Cache(app)

app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = redis.from_url('redis://session_server:6379')
sess = Session()
sess.init_app(app)

auth = HTTPBasicAuth('admin', 'devops101')

def getSession(key):
    return session.get(key, 'none')

def setSession(key, value):
    session[key] = value
    
def resetSession():
    session.clear()

app.config.from_mapping(
    SECRET_KEY=b'\xd6\x04\xbdj\xfe\xed$c\x1e@\xad\x0f\x13,@G')
Bootstrap(app)

@app.route('/', methods=['GET', 'POST'])
@cache.cached(timeout=50)
def otp():
    form = OTPForm(request.form)
    if request.method == 'POST' and form.validate_on_submit():

        headers = {'content-type': 'application/json'}
        URL = 'https://service.lab20.cpsudevops.com/getotp/'
        data = {'email': form.email.data}
        res = requests.post(URL, data = json.dumps(data), headers=headers, auth=auth)
        result = res.json().get('results')

        if result != '0':
            setSession('email', form.email.data)
            return redirect('https://www.lab20.cpsudevops.com/authen')
        else:
            return 'Email ของคุณไม่ถูกต้อง/คุณเคยลงทะเบียนแล้ว'

    return render_template('otp.html', form=form)

@app.route('/authen', methods=['GET', 'POST'])
@cache.cached(timeout=50)
def authen():
    form = AuthenForm(request.form)
    if request.method == 'POST' and form.validate_on_submit():

        headers = {'content-type': 'application/json'}
        URL = 'https://service.lab20.cpsudevops.com/authen/'
        data = {'email': getSession('email'), 'otp':  form.otp.data}
        res = requests.post(URL, data = json.dumps(data), headers=headers, auth=auth)
        result = res.json().get('results')

        if result == 1:
            setSession('authen', 'yes')
            return redirect('https://www.lab20.cpsudevops.com/reg')
        else:
            return 'กรุณาใส่ OTP/Email ใหม่'

    return render_template('authen.html', form=form)

@app.route('/reg', methods=['GET', 'POST'])
@cache.cached(timeout=50)
def registration():
    if getSession('authen') != 'yes':
        return 'คุณไม่ได้รับอนุญาตให้เข้าถึงหน้านี้'

    form = RegForm(request.form)
    if request.method == 'POST' and form.validate_on_submit():
        headers = {'content-type': 'application/json'}
        URL = 'https://service.lab20.cpsudevops.com/register/'
        data = {'firstname': form.name_first.data, 'lastname': form.name_last.data, 'email': getSession('email')}
        res = requests.post(URL, data = json.dumps(data), headers=headers, auth=auth)
        resetSession()
        return 'ระบบจะแจ้งยืนยันการลงทะเบียนทาง Email'

    return render_template('reg.html', form=form)
  • Stop/Delete Container และ Delete Image ด้วย Docker-compose
docker-compose down --rmi all
  • รัน Container ด้วย docker-compose และกลับไปที่ Terminal โดยใช้ parameter -d
docker-compose up -d
  • ดู Container ที่กำลังรันทั้งหมดที่ docker-compose.yml ดูแล
docker-compose ps
  • กดปุ่ม Clear All แล้วกด Start เพื่อทำ Load Test ด้วย JMeter อีกครั้ง

จากการทดลองพบว่าเมื่อมีการยิง Traffic 500, 1000 และ 1500 User ต่อวินาที ระบบยังสามารถ Reply Message กลับมาได้ 100% โดยไม่เกิด Error ด้วย Response Time เฉลี่ย 1019, 2370 และ 3916 ms รวมทั้งมี Throughput 158.4, 148.4 และ 148.7 Request ต่อวินาที ตามลำดับ

ดังนั้นระบบสามารถรองรับ User ที่เข้ามายังหน้าแรกของ Website ได้ประมาณ 1500*60 = 90,000 คนต่อนาที โดยมี Throughput หรือความสามารถในการจัดการงาน 148.7*60 = 8,920 Request ต่อนาที ซึ่งได้ประสิทธิภาพต่างกับการไม่ใช้ Cache ไม่มากครับ

Docker Swarm

ภาพจาก https://rominirani.com/docker-swarm-tutorial-b67470cf8872

ปัจจุบันมีซอฟต์แวร์ที่ใช้จัดการและควบคุม Container แบบ Cluster หรือที่เรียกว่า Container Orchestration หลักๆ อยู่ 2 ราย ได้แก่ Docker Swarm จาก Mirantis ซึ่งได้ซื้อกิจการที่เกี่ยวข้องกับ Docker Enterprise ไปทั้งหมดจาก Docker Inc. เมื่อเร็วๆ นี้ (และประกาศว่าจะสนับสนุนการใช้งาน Docker Swarm ต่อไปอีกสักระยะ) และ Kubernetes จาก Google

ด้วยความที่ Docker Swarm ใช้ง่ายกว่า และมี Learning Curve ตำ่กว่า Kubernetes หลายเท่า จึงเป็นตัวเลือกที่ถูกนำมาศึกษาในบทความนี้ เมื่อผู้อ่านได้ฝึกปฏิบัติจนเข้าใจแนวคิดดีแล้ว การจะศึกษา Kubernetes ต่อไป จึงเป็นเรื่องที่ไม่ยากจนเกินไป ซึ่งปัจจุบันเราสามารถ Deploy ระบบงานบน Kubernetes ด้วย Docker compose ได้แล้ว

ใน Docker Swarm จะมีการนําเอา Server หรือ Node หลายๆ เครื่องมาช่วยกันทํางานเป็น Cluster ซึ่งแบ่ง Node ใน Swarm Cluster เป็น 2 ประเภท ได้แก่ Manager Node ที่ทำหน้าที่บริหารจัดการ Cluster (รวมทั้งอาจจะ Execute Containers ด้วย) และ Worker Node ซึ่งทำหน้าที่ Execute Containers เพียงอย่างเดียว

การสร้าง Manager Node และเพิ่ม Worker Node ใน Swarm Cluster มีขั้นตอนดังนี้

  • สร้าง Manager Node ด้วยคำสั่งด้านล่าง แล้ว Copy Token จากหน้า Command Line ของตัวเอง
docker swarm init
  • Remote Login ไปยัง Cloud Server เครื่องอื่น ที่จะเพิ่มลงใน Swarm Cluster โดยใช้ ssh แล้วใช้คำสั่ง docker swarm join --token
ssh nc-user@labxx.cpsudevops.com
docker swarm join --token SWMTKN-1-2fl8x3h8g3h67ea5up748sruf11jj6yjfy7ofo266yy2s0rqwj-6m0zorbjtsxddiqop5ufpfs6q 10.148.0.15:2377
  • ดู Swarm Cluster ผ่าน Portainer โดยกดที่เมนู Dashboard แล้วกด Go to cluster visualizer  จะเห็น Node ใน Docker Swarm 3 Node ซึ่งมี Manager Node 1 Node คือ Student-lab-20 และ Worker Node 2 Node คือ Student-lab-1 และ Student-lab-19
  • นอกจากนี้เราสามารถดู Node ใน Swarm Cluster ได้จากคำสั่งด้านล่าง
docker node ls
  • สร้าง Overlay Network โดยตั้งชื่อเป็น registry_swarm
docker network create -d overlay registry_swarm
  • ติดตั้ง Local Registry ซึ่งเป็นที่เก็บ Image (Image Repository) ที่เรา Build เอง ทำให้สามารถ Push/Pull Image จาก Internal Network ได้อย่างสะดวกรวดเร็ว โดยใช้คำสั่งต่อไปนี้
docker service create --name registry --publish published=5000,target=5000 --network=registry_swarm registry:2
  • ไปที่ Project session_server_dock Stop/Delete Container และ Delete Image ด้วย Docker-compose
docker-compose down --rmi all
  • ไปที่ Project register_ui_dock แก้ไข docker-compose.yml ตามตัวอย่างด้านล่าง
version: '3'

services:
  register_ui:
    build: python/
    image: localhost:5000/register_ui
    ports:
      - "8080:80"
    deploy:
      restart_policy:
        condition: on-failure
        delay: 10s
      replicas: 2

  session_server:
    image: bitnami/redis:latest
    ports:
      - "6379:6379"
    environment:
      ALLOW_EMPTY_PASSWORD: 'yes'

    volumes:
      - redis_persistence:/bitnami/redis/data

    deploy:
      restart_policy:
        condition: on-failure
        delay: 10s
      replicas: 1

volumes:
  redis_persistence:

networks:
  default:
    external:
      name: register_ui_swarm
  • สร้าง Overlay Network โดยตั้งชื่อเป็น register_ui_swarm
docker network create -d overlay register_ui_swarm
  • Build Image
docker-compose build
  • นำ Image ที่ Build แล้ว ไปเก็บใน Local Registry
docker-compose push
  • Execute Containers บน Swarm ด้วยคำสั่ง docker stack deploy
docker stack deploy --compose-file docker-compose.yml ui
  • ดู Service ทั้งหมด ของ ui Stack บน Swarm ผ่าน Command Line และผ่าน Portainer
docker stack services ui

จะเห็นว่า ui Stack จะมี Service อยู่ 2 Service คือ ui_register_ui และ ui_session_ui ซึ่งแต่ละ Service ในที่นี้คือกลุ่มของ Container ที่ทำงานบน Swarm Cluster ไม่ใช่หมายถึง Microservice แต่อย่างใด

โดย ui_register_ui Service ประกอบด้วย Container 2 ตัวที่ Execute บน Node student-lab-1 และ Student-lab-20 ตามลำดับ

  • ลำดับต่อไป นำ UI Register ไปไว้หลัง Kong เพื่อให้สามารถทำ Monitoring และควบคุมการเข้าถึง Service ได้ง่ายขึ้น โดยการ ADD NEW SERVICE และ ADD ROUTE จาก Konga
  • เปลี่ยน Server Name เป็น service.labxx.cpsudevops.com/getotpui กดปุ่ม Clear All แล้วกด Start เพื่อทำ Load Test ด้วย JMeter อีกครั้ง

ซึ่งพบว่าระบบสามารถรองรับ User ที่เข้ามายังหน้าขอ OPT ได้ประมาณ 1500*60 = 90,000 คนต่อนาที โดยมี Throughput หรือความสามารถในการจัดการงาน 177.4*60 = 10,644 Request ต่อนาที โดยมีประสิทธิภาพมากกว่าเดิมตอนก่อนขึ้น Swarm Cluster  (10,644-8,920)/8,920*100 = 19.33%

  • ทดลองลบ Stack
docker stack rm ui
  • Execute Containers บน Swarm ด้วยคำสั่ง docker stack deploy อีกครั้ง
docker stack deploy --compose-file docker-compose.yml ui
  • ใน Stack อาจจะประกอบด้วย Service หลาย Service แต่ละ Service สามารถมี Container ที่เกิดจากการ Replicate ตัวมันเองได้หลายตัว เราสามารดู Service ของ Stack โดยใช้คำสั่ง docker stack services
docker stack services ui
Stack ui ประกอบด้วย Service 1 Service ชื่อ ui_register_ui ที่มีสำเนาตัวเอง 2 ชุด

Load Balance with Docker Swarm

Docker Swarm จะทำ Load Balance ให้โดยอัตโนมัติ เมื่อมีการเรียกใช้งาน Container ที่เกิดจากการ Replicate ตัวมันเองบน Swarm Cluster

เราจะปรับ Code ใน Register UI เล็กน้อยเพื่อจะดูว่าตอนทำงานจริง ๆ มันจะเรียกใช้ Container ตัวไหน ตามขั้นตอนต่อไปนี้

  • ไปที่ Project register_ui_dock แล้วแก้ไขไฟล์ ui.py
from flask import Flask, request, render_template, redirect, session
from model import OTPForm, AuthenForm, RegForm
from flask_bootstrap import Bootstrap
from requests.auth import HTTPBasicAuth
import requests
import json
import redis
from flask_session import Session
from flask_caching import Cache
import socket

config = {
    'DEBUG': True,
    'CACHE_TYPE': 'simple',
    'CACHE_DEFAULT_TIMEOUT': 300
}

app = Flask(__name__)

app.config.from_mapping(config)
cache = Cache(app)

app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = redis.from_url('redis://session_server:6379')

sess = Session()
sess.init_app(app)

auth = HTTPBasicAuth('admin', 'devops101')

def getSession(key):
    return session.get(key, 'none')

def setSession(key, value):
    session[key] = value
    
def resetSession():
    session.clear()

app.config.from_mapping(
    SECRET_KEY=b'\xd6\x04\xbdj\xfe\xed$c\x1e@\xad\x0f\x13,@G')
Bootstrap(app)

@app.route('/', methods=['GET', 'POST'])
@cache.cached(timeout=50)
def otp():
    name = socket.gethostname()

    form = OTPForm(request.form)
    if request.method == 'POST' and form.validate_on_submit():

        headers = {'content-type': 'application/json'}
        URL = 'https://service.lab20.cpsudevops.com/getotp/'
        data = {'email': form.email.data}
        res = requests.post(URL, data = json.dumps(data), headers=headers, auth=auth)
        result = res.json().get('results')

        if result != '0':
            setSession('email', form.email.data)
            return redirect('https://service.lab20.cpsudevops.com/authenui')
        else:
            return 'Email ของคุณไม่ถูกต้อง/คุณเคยลงทะเบียนแล้ว'

    return render_template('otp.html', form=form, name=name)

@app.route('/authen', methods=['GET', 'POST'])
@cache.cached(timeout=50)
def authen():
    name = socket.gethostname()

    form = AuthenForm(request.form)
    if request.method == 'POST' and form.validate_on_submit():

        headers = {'content-type': 'application/json'}
        URL = 'https://service.lab20.cpsudevops.com/authen/'
        data = {'email': getSession('email'), 'otp':  form.otp.data}
        res = requests.post(URL, data = json.dumps(data), headers=headers, auth=auth)
        result = res.json().get('results')

        if result == 1:
            setSession('authen', 'yes')
            return redirect('https://service.lab20.cpsudevops.com/regui')
        else:
            return 'กรุณาใส่ OTP/Email ใหม่'

    return render_template('authen.html', form=form, name=name)

@app.route('/reg', methods=['GET', 'POST'])
@cache.cached(timeout=50)
def registration():
    if getSession('authen') != 'yes':
        return 'คุณไม่ได้รับอนุญาตให้เข้าถึงหน้านี้'

    name = socket.gethostname()

    form = RegForm(request.form)
    if request.method == 'POST' and form.validate_on_submit():
        headers = {'content-type': 'application/json'}
        URL = 'https://service.lab20.cpsudevops.com/register/'
        data = {'firstname': form.name_first.data, 'lastname': form.name_last.data, 'email': getSession('email')}
        res = requests.post(URL, data = json.dumps(data), headers=headers, auth=auth)
        resetSession()
        return 'ระบบจะแจ้งยืนยันการลงทะเบียนทาง Email'

    return render_template('reg.html', form=form, name=name)
  • แก้ไขไฟล์ otp.html
{% extends "bootstrap/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block content %}
<div class="container">
  <h3>ลงทะเบียนนักศึกษาใหม่ - OTP</h3>
  <br>
  <h4>Container ID <mark>{{name}}</mark><h4>
  <hr>
  <form action="" method="post" class="form" role="form">
    {{ form.csrf_token() }}
    <div class="form-group">
      {{ wtf.form_field(form.email, type='email', class='form-control', placeholder='Email Address') }}
    </div>

    <button type="submit" class="btn btn-primary">ขอ OTP</button>
  </form>
  <hr>
  <p>Devops and Cloud Engineering - CPSU Next</p>
</div>
{% endblock %}
  • แก้ไขไฟล์ authen.html
{% extends "bootstrap/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block content %}
<div class="container">
  <h3>ลงทะเบียนนักศึกษาใหม่ - ยืนยันตัวตน</h3>
  <br>
  <h4>Container ID <mark>{{name}}</mark><h4>
  <hr>
  <form action="" method="post" class="form" role="form">
    {{ form.csrf_token() }}

    <div class="form-group">
      {{ wtf.form_field(form.otp, class='form-control', placeholder='OTP') }}
    </div>

    <button type="submit" class="btn btn-primary">ยืนยัน</button>
  </form>
  <hr>
  <p>Devops and Cloud Engineering - CPSU Next</p>
</div>
{% endblock %}
  • แก้ไขไฟล์ reg.html
{% extends "bootstrap/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block content %}
<div class="container">
  <h3>ลงทะเบียนนักศึกษาใหม่</h3>
  <br>
  <h4>Container ID <mark>{{name}}</mark><h4>
  <hr>
  <form action="" method="post" class="form" role="form">
    {{ form.csrf_token() }}

    <div class="row">
      <div class="form-group col-md-6">
        {{ wtf.form_field(form.name_first, class='form-control', placeholder='ชื่อ') }}
      </div>
      <div class="form-group col-md-6">
        {{ wtf.form_field(form.name_last, class='form-control', placeholder='นามสกุล') }}
      </div>
    </div>

    <button type="submit" class="btn btn-primary">ลงทะเบียน</button>
  </form>
  <hr>
  <p>Devops and Cloud Engineering - CPSU Next</p>
</div>
{% endblock %}
  • Build Image
docker-compose build
  • Push ขึ้น Local Registry
docker-compose push
  • Update Service ui_register_ui
docker service update ui_register_ui --image localhost:5000/register_ui
  • เปิด Browser ไปที่ https://service.labxx.cpsudevops.com/getotpui แล้ว Refresh หน้าจอ จะเห็นการสลับไปมาระหว่าง 2 Container

Microservice Migration

เราจะนำ Microservice ที่เหลืออีก 5 Service รวมทั้ง RabbitMQ ไป Execute บน Swarm Cluster (โดยจะยังไม่นำ Student และ Enroll RPC Service ที่มีการเรียกใช้งานฐานข้อมูลขึ้น Swarm Cluster ในขณะนี้) ซึ่งแต่ละส่วนจะมีการใช้ Network ดังนี้

Docker Swarm

  • Register Gateway (register_gateway_swarm)
  • OTP Gateway (otp_gateway_swarm)
  • Email (email_swarm)
  • OTP (otp_swarm)
  • Send Email OTP (email_otp_swarm)
  • RabbitMQ (rabbitmq_swarm)

Docker

  • Student (webproxy, student_network)
  • Enroll RPC (webproxy, enroll_network)

ขั้นตอน Migration มีดังนี้

Register Gateway Service

  • สร้าง Network แบบ Overlay ชื่อ register_gateway_swarm
docker network create -d overlay register_gateway_swarm
  • ไปที่ Project register_gateway_dock แล้ว Stop/Delete Container และ Delete Image ด้วย Docker-compose
docker-compose down --rmi all
  • แก้ไขไฟล์ docker-compose.yml ดังนี้
version: '3'

services:
  register_gateway:
    build: python/
    image: localhost:5000/register_gateway
    ports:
      - "7001:80"
    deploy:
      restart_policy:
        condition: on-failure
        delay: 10s
      replicas: 1
      
networks:
  default:
    external:
      name:  register_gateway_swarm
  • แก้ไขไฟล์ api.py ดังนี้
from fastapi import FastAPI
from pydantic import BaseModel
from nameko.rpc import rpc
from nameko.standalone.rpc import ClusterRpcProxy

class Student(BaseModel):
    firstname:str
    lastname:str
    email:str

app = FastAPI()

broker_cfg = {'AMQP_URI': "amqp://guest:guest@10.148.0.15"}

@app.post("/register/")
def api(student_item: Student):
    with ClusterRpcProxy(broker_cfg) as rpc:
        sid =rpc.student.insert(student_item.firstname, student_item.lastname, student_item.email)
        rpc.enroll.insert.call_async(sid, student_item.firstname, student_item.lastname)
        rpc.email.send.call_async(sid, student_item.firstname, student_item.lastname, student_item.email)
    
    print(sid)
    return {'results': 'registered'}
  • Build Image
docker-compose build
  • Push ขึ้น Local Registry
docker-compose push

  • Execute Containers บน Swarm ด้วยคำสั่ง docker stack deploy
docker stack deploy --compose-file docker-compose.yml register_gateway
  • ดู Stack บน Swarm ที่สร้างผ่านไฟล์ docker-compose.yml
docker stack ls
  • ดู Service ของ Stack
docker stack services register_gateway

OTP Gateway Service

  • สร้าง Network แบบ Overlay ชื่อ otp_gateway_swarm
docker network create -d overlay otp_gateway_swarm
  • ไปที่ Project otp_gateway_dock แล้ว Stop/Delete Container และ Delete Image ด้วย Docker-compose
docker-compose down --rmi all
  • แก้ไขไฟล์ docker-compose.yml ดังนี้
version: '3'

services:
  otp_gateway:
    build: python/
    image: localhost:5000/otp_gateway
    ports:
      - "7002:80"
    deploy:
      restart_policy:
        condition: on-failure
        delay: 10s
      replicas: 1
      
networks:
  default:
    external:
      name: otp_gateway_swarm
  • แก้ไขไฟล์ api.py ดังนี้
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
from nameko.rpc import rpc
from nameko.standalone.rpc import ClusterRpcProxy

class Email(BaseModel):
    email:str

class EmailList(BaseModel):
    emails:List[str] = []

class Authen(BaseModel):
    email:str
    otp:str

app = FastAPI()

broker_cfg = {'AMQP_URI': "amqp://guest:guest@10.148.0.15"}

@app.post("/getotp/")
def get_otp(email: Email):
    with ClusterRpcProxy(broker_cfg) as rpc:
        otp = rpc.otp.create(email.email)
        if otp != 0:
           rpc.email_otp.send.call_async(otp, email.email)

    return {'results': str(otp)}

@app.post("/create_email_list/")
def create_email_list(email_list: EmailList):
    with ClusterRpcProxy(broker_cfg) as rpc:
        val = rpc.otp.create_email_list(email_list.emails)

    return {'results': val}

@app.post("/authen/")
def authen(authen: Authen):
    with ClusterRpcProxy(broker_cfg) as rpc:
        success = rpc.otp.authen(authen.email, authen.otp)
        if success == 1:
           rpc.otp.delete.call_async(authen.email)


    return {'results': success}
  • Build Image
docker-compose build
  • Push ขึ้น Local Registry
docker-compose push
  • Execute Containers บน Swarm ด้วยคำสั่ง docker stack deploy
docker stack deploy --compose-file docker-compose.yml otp_gateway
  • ดู Stack บน Swarm ที่สร้างผ่านไฟล์ docker-compose.yml
docker stack ls
  • ดู Service ของ Stack
docker stack services otp_gateway

Student Service

  • ไปที่ Project student_dock แล้ว Stop/Delete Container และ Delete Image ด้วย Docker-compose
docker-compose down --rmi all
  • แก้ไขไฟล์ docker-compose.yml ดังนี้
version: '3'

services:
  student_rpc:
    container_name: student_rpc
    build: python/
    restart: always
    
    depends_on:
      - db
      
  db:
    container_name: mariadb
    image: mariadb:latest
    restart: always
    volumes:
      - ./mariadb/initdb/:/docker-entrypoint-initdb.d
      - ./mariadb/data/:/var/lib/mysql/
    environment:
      - MYSQL_ROOT_PASSWORD=devops101
      - MYSQL_DATABASE=devops_db
      - MYSQL_USER=devops
      - MYSQL_PASSWORD=devops101
      
  pma:
    container_name: student_phpmyadmin
    image: phpmyadmin/phpmyadmin
    restart: always

    networks:
      - webproxy
      - default

    environment:
      VIRTUAL_HOST: mydb.lab20.cpsudevops.com
      LETSENCRYPT_HOST: mydb.lab20.cpsudevops.com
    
    expose:
      - "80"
      
networks:
  default:
    external:
      name: student_network
  webproxy:
    external:
      name: webproxy
  • แก้ไขไฟล์ Dockerfile ดังนี้
FROM python:3.7.3-alpine3.8
RUN apk add --no-cache mariadb-dev build-base
WORKDIR /app
COPY rpc.py .
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
CMD nameko run rpc --broker amqp://guest:guest@10.148.0.15:5672

  • รัน Container ด้วย docker-compose และกลับไปที่ Terminal โดยใช้ parameter -d
docker-compose up -d
  • ดู Container ที่กำลังรันทั้งหมดที่ docker-compose.yml ดูแล
docker-compose ps

Enroll RPC Service

  • ไปที่ Project enroll_dock แล้ว Stop/Delete Container และ Delete Image ด้วย Docker-compose
docker-compose down --rmi all
  • แก้ไขไฟล์ docker-compose.yml ดังนี้
version: '3'

services:
  enroll_rpc:
    container_name: enroll_rpc
    build: python/
    restart: always

    depends_on:
      - db

  db:
    container_name: enroll_mariadb
    image: mariadb:latest
    restart: always
    volumes:
      - ./mariadb/initdb/:/docker-entrypoint-initdb.d
      - ./mariadb/data/:/var/lib/mysql/
    environment:
      - MYSQL_ROOT_PASSWORD=devops101
      - MYSQL_DATABASE=devops_db
      - MYSQL_USER=devops
      - MYSQL_PASSWORD=devops101
  pma:
    container_name: enroll-phpmyadmin
    image: phpmyadmin/phpmyadmin
    restart: always

    networks:
      - webproxy
      - default

    environment:
      VIRTUAL_HOST: mydb2.lab20.cpsudevops.com
      LETSENCRYPT_HOST: mydb2.lab20.cpsudevops.com

    expose:
      - "80"

networks:
  default:
    external:
      name: enroll_network
  webproxy:
    external:
      name: webproxy
  • แก้ไขไฟล์ Dockerfile ดังนี้
FROM python:3.7.3-alpine3.8
RUN apk add --no-cache mariadb-dev build-base
WORKDIR /app
COPY rpc.py .
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
CMD nameko run rpc --broker amqp://guest:guest@10.148.0.15:5672
  • รัน Container ด้วย docker-compose และกลับไปที่ Terminal โดยใช้ parameter -d
docker-compose up -d
  • ดู Container ที่กำลังรันทั้งหมดที่ docker-compose.yml ดูแล
docker-compose ps

Email Service

  • สร้าง Network แบบ Overlay ชื่อ email_swarm
docker network create -d overlay email_swarm
  • ไปที่ Project email_dock แล้ว Stop/Delete Container และ Delete Image ด้วย Docker-compose
docker-compose down --rmi all
  • แก้ไขไฟล์ docker-compose.yml ดังนี้
version: '3'

services:
  email_rpc:
    build: python/
    image: localhost:5000/email_rpc
    
    deploy:
      restart_policy:
        condition: on-failure
        delay: 10s
      replicas: 1

  smtp:
    image: bytemark/smtp
    deploy:
      restart_policy:
        condition: on-failure
        delay: 10s
      replicas: 1
    environment:
      RELAY_HOST: smtp.live.com
      RELAY_PORT: 587
      RELAY_USERNAME: nuttachot@hotmail.com
      RELAY_PASSWORD: xxx

networks:
  default:
    external:
      name: email_swarm
  • แก้ไขไฟล์ Dockerfile ดังนี้
FROM python:3.7.3-alpine3.8
RUN apk add --no-cache  build-base
WORKDIR /app
COPY rpc.py .
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
CMD nameko run rpc --broker amqp://guest:guest@10.148.0.15:5672
  • Build Image
docker-compose build
  • Push ขึ้น Local Registry
docker-compose push
  • Execute Containers บน Swarm ด้วยคำสั่ง docker stack deploy
docker stack deploy --compose-file docker-compose.yml email
  • ดู Stack บน Swarm ที่สร้างผ่านไฟล์ docker-compose.yml
docker stack ls
  • ดู Service ของ Stack
docker stack services email

OTP Service

  • สร้าง Network แบบ Overlay ชื่อ otp_swarm
docker network create -d overlay otp_swarm
  • ไปที่ Project otp_dock แล้ว Stop/Delete Container และ Delete Image ด้วย Docker-compose
docker-compose down --rmi all
  • แก้ไขไฟล์ docker-compose.yml ดังนี้
version: '3'

services:
  otp:
    build: python/
    image: localhost:5000/otp
    deploy:
      restart_policy:
        condition: on-failure
        delay: 10s
      replicas: 1

    depends_on:
      - otp_redis

  otp_redis:
    image: "redis:alpine"
    deploy:
      restart_policy:
        condition: on-failure
        delay: 10s
      replicas: 1

networks:
  default:
    external:
      name: otp_swarm
  • แก้ไขไฟล์ Dockerfile ดังนี้
FROM python:3.7.3-alpine3.8
RUN apk add --no-cache build-base
WORKDIR /app
COPY rpc.py .
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
CMD nameko run rpc --broker amqp://guest:guest@10.148.0.15:5672
  • Build Image
docker-compose build
  • Push ขึ้น Local Registry
docker-compose push
  • Execute Containers บน Swarm ด้วยคำสั่ง docker stack deploy
docker stack deploy --compose-file docker-compose.yml otp
  • ดู Stack บน Swarm ที่สร้างผ่านไฟล์ docker-compose.yml
docker stack ls
  • ดู Service ของ Stack
docker stack services otp

Send Email OTP Service

  • สร้าง Network แบบ Overlay ชื่อ email_otp_swarm
docker network create -d overlay email_otp_swarm
  • ไปที่ Project send_email_otp_dock แล้ว Stop/Delete Container และ Delete Image ด้วย Docker-compose
docker-compose down --rmi all
  • แก้ไขไฟล์ docker-compose.yml ดังนี้
version: '3'

services:
  send_email_otp:
    build: python/
    image: localhost:5000/send_email_otp
    deploy:
      restart_policy:
        condition: on-failure
        delay: 10s
      replicas: 1

  smtp_otp:
    image: bytemark/smtp
    deploy:
      restart_policy:
        condition: on-failure
        delay: 10s
      replicas: 1
    environment:
      RELAY_HOST: smtp.live.com
      RELAY_PORT: 587
      RELAY_USERNAME: nuttachot@hotmail.com
      RELAY_PASSWORD: xxx

networks:
  default:
    external:
      name: email_otp_swarm
  • แก้ไขไฟล์ Dockerfile ดังนี้
FROM python:3.7.3-alpine3.8
RUN apk add --no-cache build-base
WORKDIR /app
COPY rpc.py .
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
CMD nameko run rpc --broker amqp://guest:guest@10.148.0.10:5672
  • Build Image
docker-compose build
  • Push ขึ้น Local Registry
docker-compose push
  • Execute Containers บน Swarm ด้วยคำสั่ง docker stack deploy
docker stack deploy --compose-file docker-compose.yml send_email_otp
  • ดู Stack บน Swarm ที่สร้างผ่านไฟล์ docker-compose.yml
docker stack ls
  • ดู Service ของ Stack
docker stack services send_email_otp

RabbitMQ

  • สร้าง Network แบบ Overlay ชื่อ rabbitmq_swarm
docker network create -d overlay rabbitmq_swarm
  • ไปที่ Project mq_dock แล้ว Stop/Delete Container และ Delete Image ด้วย Docker-compose
docker-compose down --rmi all
  • แก้ไขไฟล์ docker-compose.yml ดังนี้
version: "3"
services:
  rabbitmq:
    image: "rabbitmq:management"
    ports:
      - "5672:5672"
    deploy:
      restart_policy:
        condition: on-failure
        delay: 10s
      replicas: 1
networks:
  default:
    external:
      name: rabbitmq_swarm
  • Execute Containers บน Swarm ด้วยคำสั่ง docker stack deploy
docker stack deploy --compose-file docker-compose.yml rabbitmq
  • ดู Stack บน Swarm ที่สร้างผ่านไฟล์ docker-compose.yml
docker stack ls
  • ดู Service ของ Stack
docker stack services rabbitmq
  • ADD NEW SERVICE และ ADD ROUTE จาก Konga ดังนี้
Add authenUI Service
Add regUI Service
Add authenUI Route
Add regUI Route

Session on Swarm Cluster

เทคนิคหนึ่งในการทำให้ Session ไม่หลุด เมื่อมีการทำ Load Balance และเมื่อมีการเพิ่ม หรือลดจำนวน Service แบบ Zero Downtime ก็คือการจัดเก็บ Session บน Session Server (Redis Server)

เราจะทดสอบการทำ Session บน Swarm Cluster กับระบบลงทะเบียนนักศึกษา ที่มี 3 หน้า ได้แก่ 1) หน้าขอ OPT 2) หน้ายืนยันตัวตน และ 3) หน้าลงทะเบียนนักศึกษาใหม่

โดยในหน้า ยืนยันตัวตน จะมีการนำ OTP ที่ผู้ใช้ได้กรอกไว้ + Email จาก Session Server ไปขอตรวจสอบตัวตนจาก authen API

ขณะที่หน้า ลงทะเบียนนักศึกษาใหม่ จะมีการตรวจสอบ สถานะการยืนยันตัวตน จาก Session Server และนำ ชื่อ นามสกุล ที่ผู้ใช้กรอก + Email จาก Session Server ส่งไปยัง register API

ซึ่งจะมีการทดสอบ ดังต่อไปนี้

  • เพิ่มรายชื่อผู้มีสิทธิ์ลงทะเบียนจากหน้า http://labxx.cpsudevops.com:7002/docs
  • ไปยังหน้าขอ OTP (https://service.labxx.cpsudevops.com/getotpui) กรอก Email แล้วกด ขอ OTP
  • กรอก OTP ที่ได้รับทาง Email แล้วกด ยืนยัน
  • ลบ Container ของ ui_register_ui Service ทั้งหมด โดยใช้คำสั่ง docker service scale ui_register_ui=0
docker service scale ui_register_ui=0
  • สร้าง Contain ของ ui_register_ui Service ใหม่ 2 Container โดยใช้คำสั่ง docker service scale ui_register_ui=2
docker service scale ui_register_ui=2
  • กลับไปกรอก ชื่อ นามสกุล แล้วกด ลงทะเบียน โดย Container ที่สร้างใหม่จะตรวจสอบ สถานะการยันตัวตน จาก Session Server และนำ ชื่อ นามสกุล + Email จาก Session Server ส่งไปยัง register API

Scaling and Update

ขณะนี้เรามี Stack ที่ถูกสร้างจากการ Config ไฟล์ docker-compose.yml บน Docker Swarm ทั้งหมด 7 Stack ทั้งที่เป็น Microservice และ Message Queue Broker

Stack

Microservice

  • ui (Swarm - ui_register_ui, ui_session_server)
  • register_gateway (Swarm- register_gateway_register_gateway)
  • email (Swarm - email_email_rpc, email_smtp)
  • otp_gateway (Swarm - otp_gateway_otp_gateway)
  • otp (Swarm - otp_otp, otp_otp_redis)
  • send_email_otp (Swarm - send_email_otp_send_email_otp, send_email_otp_smtp_otp)

Message Queue Broker

  • rabbitmq (Swarm - rabbitmq_rabbitmq)

ในการ Scale เราจะทำในระดับ Service ซึ่ง Service ที่สามารถ Scale ได้โดยไม่ต้อง Config เพิ่มเติม ต้องเป็น Service ที่ไม่มีหน้าที่ในการจัดเก็บข้อมูล ได้แก่

  • ui_register_ui
  • register_gateway_register_gateway
  • email_email_rpc
  • email_smtp
  • otp_gateway_otp_gateway
  • otp_otp
  • send_email_otp_send_email_otp
  • send_email_otp_smtp_otp

เราจะทดลอง Scale และ Update ui_register_ui ซึ่งเป็น Web UI หน้าการ ขอ OTP

https://service.labxx.cpsudevops.com/getotpui

โดยใช้คำสั่งหลักๆ 5 คำสั่ง ได้แก่

docker service scale สำหรับการ Scale Service บน Docker Swarm
docker-compose build สำหรับสร้าง Image ก่อนนำไปเก็บใน Local Registry
docker-compose push สำหรับ Push Image ไปเก็บใน Local Registry
docker service update สำหรับการ Update Service
docker service rollback สำหรับถอนการ Update Service

Scale Out

ก่อนอื่นเราจะทดลองเพิ่มจำนวน Container ของ ui_register_ui Service จากเดิม 2 Container เป็น 6 Container โดยมีขั้นตอนดังนี้

  • ดู Stack ทั้งหมด บน Swarm Cluster
docker stack ls
  • ดู Service ของ ui Stack
docker stack services ui
  • ดูว่า Container ของ ui_register_ui Service มีการ Execute อยู่บน Node ไหนบ้าง
docker service ps ui_register_ui
  • Scale Out ui_register_ui Service เป็น 6 Container
docker service scale ui_register_ui=6
  • ดูว่าทั้ง 6 Container ของ ui_register_ui Service มีการ Execute อยู่ที่ Node ไหน
docker service ps ui_register_ui

Update Service

ทดลองเปลี่ยนสี Font หน้า ขอ OPT ตามขั้นตอนดังนี้

  • ไปที่ Project register_ui_dock แล้วแก้ไขไฟล์ otp.html
{% extends "bootstrap/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block content %}
<div class="container">
  <h3><font color="blue">ลงทะเบียนนักศึกษาใหม่ - OTP</font></h3>
  <br>
  <h4>Container ID <mark>{{name}}</mark><h4>
  <hr>
  <form action="" method="post" class="form" role="form">
    {{ form.csrf_token() }}
    <div class="form-group">
      {{ wtf.form_field(form.email, type='email', class='form-control', placeholder='Email Address') }}
    </div>

    <button type="submit" class="btn btn-primary">ขอ OTP</button>
  </form>
  <hr>
  <p>Devops and Cloud Engineering - CPSU Next</p>
</div>
{% endblock %}
  • Build Image ใหม่
docker-compose build
  • Push Images ใหม่ขึ้น Local Registry
docker-compose push
  • Update Service ทีลง 2 Container เว้นช่วงละ 10 วินาที จาก Image ที่ได้ Push ไว้บน Local Registry โดยสีของ Font หน้า ขอ OTP จาก 2 Container จะทยอยเปลี่ยน ช่วงนี้ท่านสามารถ Refresh Browser เพื่อดูการเปลี่ยนแปลงได้
docker service update ui_register_ui --image localhost:5000/register_ui --update-parallelism 2 --update-delay 10s
  • ดู Service ของ ui Stack
docker stack services ui

Rollback Service

  • เปลี่ยนสี Font หน้า ขอ OTP กลับตามเดิม
docker service rollback ui_register_ui
ขอขอบคุณ Nipa.Cloud ที่ให้การสนับสนุน Environment ในการเรียนการสอน
รายวิชา Dev-Ops and Cloud Engineering 101