AWS Auto Scaling Group based on Queue (Custom metric)

Posted by MinhHungTrinh on 2021-01-17
Estimated Reading Time 10 Minutes
Words 1.7k In Total
Viewed Times

Đây là bài mình viết cũng đã trình bày trên blog kaopiz.kipalog của công ty.
Anh em làm backend chắc hẳn có va chạm ít nhiều với việc xử lý các queue message bằng Workers. Ở local hay môi trường Test sẽ thật đơn giản khi chỉ chạy single Instance và thiết lập cố định số processes. Vấn đề bắt đầu xảy ra khi lên production, khách hàng yêu cầu 1 hệ thống chạy ổn định, nhưng lại phải tiết kiệm chi phí khi không cần thiết. Vậy là nó cần phải đảm bảo vài cái tính sau: Availability, ReliabilityScalability.

Có nhiều cách triển khai lắm. Trên AWS chẳng hạn thì có use-cases là sử dụng 1 autoscale group chứa các instance chạy các Workers này. Các worker sẽ pull messages từ Queue và xử lý. Vì có nhiều workers chạy đồng thời trên các instance nên theo lý thuyết là nó đảm bảo 3 tính vừa rồi. Luôn luôn đáp ứng kịp thời mà lại tiết kiệm chi phí khi cần thiết. Cơ mà từ từ đã, chúng ta thử đi sâu vào xem vì sao mình phải customize nó ở bài này nhé.

Vấn đề

Mình sẽ lấy bài toán default là chúng ta bắn message job lên SQS Standard (1 loại queue của AWS, bạn cũng có thể thay thế nó bằng các loại khác như Redis, RapidMq,… nhưng phải custom metric trên cloudwatch để sử dụng nó cho việc scale). Tiếp theo, Mình sẽ tạo 1 cụm Autoscale Group để launch các instance (min=1, max=6), trong đó các instance chứa code là các workers xử lý message job.

Với các API server, thường chúng ta scale theo CPU, memory của server. Nhưng với bài toán worker này, Nếu cấu hình là tối ưu thì các Worker phải chạy ở ngưỡng chấp nhận tối đa về CPU, memory trong thời gian dài (long running). Vì vậy nếu scale theo CPU hay memory thì sẽ gây ra hiện tượng tăng liên tục instance do CPU và memory luôn cao ==> lãng phí.

Thường thì bài toán này người ta scale theo số lượng message job. Vậy scale policy mặc định mình sẽ chọn là khi tổng số lượng messages trong queue (SQS có cung cấp default metric cho phép chúng ta theo dõi số lựng message job) đạt 1 ngưỡng nào đó thì mình sẽ tăng hoặc giảm 1 instance
=> OK có vẻ nó đảm bảo được Availability, Reliability. Vậy còn Scalability có vẻ mình thấy nó vẫn chưa hợp lý. Mình sẽ diễn giải như sau.

Cấu hình autoscale với mấy thông số cơ bản kiểu:

  • Hiện tại có Autoscale group của bạn có 1 instance
  • Bạn đặt ngưỡng TotalMessage để scale là 200 messages chẳng hạn
  • Health check 1 phút/1 lần. Chạm ngưỡng 2 lần thì scale => 2 phút.

Vấn đề mình thấy chưa tối ưu ở chỗ khi scale out tăng 1 instance. Cùng 1 thời điểm bạn nhận được 400 messages vào queue (do lượng người dùng tăng bất chợt). Sau đó Autoscale nhận thấy TotalMessage > 200 ==> nó sẽ thêm 1 instance vào.
Khoảng 2 phút sau, số message vẫn trên 200, và lại tăng thêm nữa. Ví dụ sau 10 phút, lượng TotalMessage bắt đầu <= 200 chúng ta sẽ có 6 instance.
Sau 10 phút tiếp theo, Số message bị xử lý hết, về 0. Scale In thực hiện, chúng ta lại tắt chỉ giữ lại 1 instance => bị tính phí thêm 5hours sử dụng cho lần scale này.

Vậy là việc tăng số lượng instance quá nhiều như thế này có vẻ không cần thiết. Nhất là tùy vào yêu cầu bài toán như 1 message phải được xử lý trong 10 phút hay 1 giờ hay 6 giờ chẳng hạn. Thì với cấu hình như ở trên chúng ta k thể đánh giá được.

Giải pháp

Với bài toán ở trên nếu muốn tối ưu mình nghĩ cần định hướng việc scale từ totalmessage thành Backlog Message per instance. Tức là vào 1 thời điểm thì 1 instance dự đoán sẽ phải có bao nhiêu message nằm trong backlog của nó. Như bài toán ở trên nếu yêu cầu là thì ví dụ:

Có 200 messages và có 2 instance => BacklogMessagePerInstance = 100. Vẫn cần tăng thêm
Có 200 messages và có 5 instance => BacklogMessagePerInstance = 40. Không cần tăng nữa
Từ con số BacklogMessagePerInstance bạn có thể đánh giá được sau thời gian bao lâu thì message sẽ được xử lý xong. Từ đó áp dụng với yêu cầu bổ sung của dự án.

BacklogMessagePerInstance thì không có sẵn trong các cloudwatch metric. Vậy tức là chúng ta cần custom metric này. Mình sẽ chọn 1 instance nhỏ tí (t2 micro là quá nhiều) để thực hiện logic này. Tạo 1 file shell script với nội dung dưới. Mình xin cung cấp source của script tính BacklogMessagePerInstance, mọi người nên customize để phù hợp với bài toán của mình hơn:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/bin/bash

# Please update below variables as per your production setup
REGION=${AWS::Region}
WEB_DIR="/home/ubuntu"
SERVICE_ASG=Service-asg
ACCOUNT_ID=${AWS::AccountId}
QueueURL=${QueueURL}
MetricName=BacklogPerInstance
Namespace=ServiceGroup

cd $WEB_DIR

#Get messages number in SQS
ApproximateNumberOfMessages=$(aws sqs get-queue-attributes --region $REGION --queue-url $QueueURL --attribute-names ApproximateNumberOfMessages | jq -r '.Attributes.ApproximateNumberOfMessages')
ApproximateNumberOfMessagesNotVisible=$(aws sqs get-queue-attributes --region $REGION --queue-url $QueueURL --attribute-names ApproximateNumberOfMessagesNotVisible | jq -r '.Attributes.ApproximateNumberOfMessagesNotVisible')
ApproximateNumberOfMessagesDelayed=$(aws sqs get-queue-attributes --region $REGION --queue-url $QueueURL --attribute-names ApproximateNumberOfMessagesDelayed | jq -r '.Attributes.ApproximateNumberOfMessagesDelayed')
NumberOfMessages=$(($ApproximateNumberOfMessages+$ApproximateNumberOfMessagesNotVisible+$ApproximateNumberOfMessagesDelayed))
echo "NumberOfMessages: " $NumberOfMessages

#Get instances number in autoscalegroup
InstancesNumber=$(aws autoscaling describe-auto-scaling-groups --region $REGION --auto-scaling-group-names $SERVICE_ASG --query "AutoScalingGroups[0]" | jq '.Instances | length')
echo "InstancesNumber: " $InstancesNumber

if [ $InstancesNumber -eq 0 -a $NumberOfMessages -eq 0 ]
then
echo "Insufficient Data";
else
#Set default InstancesNumber
if [ $InstancesNumber -eq 0 ]
then
echo "set default is 1";
InstancesNumber=1
fi

#Caculate Backlog per Instance
ServiceBacklogPerWorker=$((($NumberOfMessages / $InstancesNumber) + ($NumberOfMessages % $InstancesNumber > 0)))
echo "MyBacklogPerWorker: " $ServiceBacklogPerWorker

#Put to Cloudwatch
aws cloudwatch put-metric-data --region $REGION --metric-name $MetricName --namespace $Namespace --unit Count --value $ServiceBacklogPerWorker --dimensions SQS-Queue=CustomQueue
fi

Sau đó hãy thêm nó vào cronjob để bắn metric lên cloudwatch nhé.

1
2
3
4
echo "* * * * * ubuntu $WEB_DIR/$SERVICE_SCRIPT_NAME.sh > /dev/null 2>&1" >> $WEB_DIR/ec2schedule
chmod 0600 $WEB_DIR/ec2schedule
mv $WEB_DIR/ec2schedule /etc/cron.d/
service cron restart

OK. vây là custom metric sẽ được tạo. Nếu bạn vào cloudwatch sẽ thấy nó hiện mới. Hãy sử dụng metric này cho worker autoscale group của mình nhé.

Kết luận

Thực ra bài toán chỉ là 1 ví dụ nhỏ, mục đích mình muốn nói về việc mình tự custom metric và tạo 1 scale policy theo ý mình muốn cho autoscale group. Ví dụ mình tạo custom metric từ 1 thông số trời ơi đất hỡi nào cũng được, thay vì lấy từ SQS thì lấy từ Redis hoặc lấy data từ database ,…
Sau khi viết xong bài này mình cũng thấy có vài bài viết con chi tiết hơn, thiết nghĩ ngại dịch lại nên thôi suggest thêm, biết đâu đọc dễ hiểu:
AWS Docs: https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-using-sqs-queue.html
Example với ECS cluster: https://allaboutaws.com/how-to-auto-scale-aws-ecs-containers-sqs-queue-metrics
Blog chủ đề về scale, có thể mình sẽ viết thêm các bài khác tham khảo từ đây chẳng hạn: https://aws.amazon.com/blogs/aws/category/auto-scaling/


Đây là Blog cá nhân của MinhHungTrinh, nơi mình chia sẻ, lưu giữ kiến thức. Nếu các bạn có góp ý, thắc mắc thì vui lòng comment bên dưới cho mình biết nhé. Mình luôn là người lắng nghe và ham học hỏi. Các vấn đề đặc biệt hoặc tế nhị mọi người có thể gửi email tới minhhungtrinhvn@gmail.com. Cảm ơn Mọi Người đã đọc Blog của mình. Yolo!