Trong bài toán AI, nếu như Job và CronJob phù hợp nhất cho các tác vụ thực hiện không liên tục, không realtime (VD: Batch Inference, Training, …) thì Deployment lại là lựa chọn tốt nhất cho các tác vụ cần chạy liên tục, realtime (VD: Online Inference, …). Trong bài này, hãy cùng tìm hiểu về Deployment và cách sử dụng nó.
1. Kubernetes Deployment là gì?
Deployment có thể hiểu là một tập các Pods giống nhau chạy trên một Kubernetes Cluster. Giống như Job, nó cũng quản lý các Pods trong việc thực hiện một nhiệm vụ nào đó. Sự khác nhau giữa Job và Deployment ở tính chất nhiệm vụ mà chúng thực hiện. Đối với Job, các tasks của nó chỉ chạy một lần, sau đó kết thúc luôn. Ngược lại, các tasks của Deployment chạy liên tục từ lúc được khởi tạo và chỉ kết thúc khi có sự can thiệp của người quản trị hoặc một ngoại lệ bất thường.
Một số đặc điểm trong cách quản lý Pod của Deployment:
Chính vì vậy mà Deployment rất phù hợp với nhiệm vụ Online Inference trong bài toán AI. Chúng ta train một model, tạo một REST API để lắng nghe các yêu cầu dự đoán. Sau đó, tạo ra một Deployment để chấp nhận và thực hiện các yêu cầu đó một các realtime. Nếu số lượng các yêu cầu tăng lên cao, Deployment sẽ tự động tạo thêm các Pod để xử lý và ngược lại. Nếu có một phiên bản mới của model, ta có thể dễ dàng đưa luôn vào sử dụng mà không phải downtime. Và nếu model mới đó không hiệu quả bằng model cũ, ta hoàn toàn có thể quay về sử dụng model cũ đó.
2. Làm việc với Kubernetes Deployment
Chúng ta sẽ thực hiện tạo một Deployment để phục vụ nhiệm vụ Online Inference trong bài toán AI.
Hãy xem cấu trúc thư mục làm việc:
kubernetes_deployment
│ ├── deployment
│ │ └── deployment-online-inference.yaml
│ └── docker
│ ├── api.py
│ ├── Dockerfile
│ └── train.py
2.1 Train model AI và tạo REST API
Tạo thư mục docker và hai file code python bên trong nó:
train.py
: Train model AI và lưu file model.api.py
: Tạo API để cho phép yêu cầu dự đoán gửi đến và trả về kết quả.Nội dung của file train.py
như sau:
import json
import os
from joblib import dump
import matplotlib.pyplot as plt
import numpy as np
from sklearn import ensemble
from sklearn import datasets
from sklearn.utils import shuffle
from sklearn.metrics import mean_squared_error
MODEL_DIR = os.environ["MODEL_DIR"]
MODEL_FILE = os.environ["MODEL_FILE"]
METADATA_FILE = os.environ["METADATA_FILE"]
MODEL_PATH = os.path.join(MODEL_DIR, MODEL_FILE)
METADATA_PATH = os.path.join(MODEL_DIR, METADATA_FILE)
# #############################################################################
# Load data
print("Loading data...")
boston = datasets.load_boston()
print("Splitting data...")
X, y = shuffle(boston.data, boston.target, random_state=13)
X = X.astype(np.float32)
offset = int(X.shape[0] * 0.9)
X_train, y_train = X[:offset], y[:offset]
X_test, y_test = X[offset:], y[offset:]
# #############################################################################
# Fit regression model
print("Fitting model...")
params = {'n_estimators': 500, 'max_depth': 4, 'min_samples_split': 2,
'learning_rate': 0.01, 'loss': 'ls'}
clf = ensemble.GradientBoostingRegressor(**params)
clf.fit(X_train, y_train)
train_mse = mean_squared_error(y_train, clf.predict(X_train))
test_mse = mean_squared_error(y_test, clf.predict(X_test))
metadata = {
"train_mean_square_error": train_mse,
"test_mean_square_error": test_mse
}
print("Serializing model to: {}".format(MODEL_PATH))
dump(clf, MODEL_PATH)
print("Serializing metadata to: {}".format(METADATA_PATH))
with open(METADATA_PATH, 'w') as outfile:
json.dump(metadata, outfile)
Nội dung của file api.py
như sau:
import os
from flask import Flask
from flask_restful import Resource, Api, reqparse
from joblib import load
import numpy as np
MODEL_DIR = os.environ["MODEL_DIR"]
MODEL_FILE = os.environ["MODEL_FILE"]
METADATA_FILE = os.environ["METADATA_FILE"]
MODEL_PATH = os.path.join(MODEL_DIR, MODEL_FILE)
METADATA_PATH = os.path.join(MODEL_DIR, METADATA_FILE)
print("Loading model from: {}".format(MODEL_PATH))
clf = load(MODEL_PATH)
app = Flask(__name__)
api = Api(app)
class Prediction(Resource):
def __init__(self):
self._required_features = ['CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM',
'AGE', 'DIS', 'RAD', 'TAX', 'PTRATIO', 'B',
'LSTAT']
self.reqparse = reqparse.RequestParser()
for feature in self._required_features:
self.reqparse.add_argument(
feature, type = float, required = True, location = 'json',
help = 'No {} provided'.format(feature))
super(Prediction, self).__init__()
def post(self):
args = self.reqparse.parse_args()
X = np.array([args[f] for f in self._required_features]).reshape(1, -1)
y_pred = clf.predict(X)
return {'prediction': y_pred.tolist()[0]}
api.add_resource(Prediction, '/predict')
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
Code của 2 files này khá đơn giản nên mình không giải thích gì thêm, hi vọng các bạn có thể tự hiểu được.
2.2 Chuẩn bị Docker Image
Cũng trong thư mục docker, ta file Dockerfile như sau:
FROM jupyter/scipy-notebook
USER root
WORKDIR /docker
ADD . /docker
RUN pip install flask flask-restful joblib
RUN mkdir /docker/model
ENV MODEL_DIR=/docker/model
ENV MODEL_FILE=clf.joblib
ENV METADATA_FILE=metadata.json
RUN python3 train.py
Sau đó tiến hành build Docker Image:
$ docker build -t docker-ml-online .
Sending build context to Docker daemon 6.656kB
Step 1/10 : FROM jupyter/scipy-notebook
---> c1a7c7ef5e27
Step 2/10 : USER root
---> Using cache
---> 0d9f55e9c7e0
Step 3/10 : WORKDIR /docker
---> Using cache
---> 4ed21d81d110
Step 4/10 : ADD . /docker
---> a266bfc5ca35
Step 5/10 : RUN pip install flask flask-restful joblib
---> Running in 97888ed0b989
Collecting flask
Downloading Flask-1.1.2-py2.py3-none-any.whl (94 kB)
Collecting flask-restful
Downloading Flask_RESTful-0.3.8-py2.py3-none-any.whl (25 kB)
Requirement already satisfied: joblib in /opt/conda/lib/python3.8/site-packages (1.0.0)
Requirement already satisfied: click>=5.1 in /opt/conda/lib/python3.8/site-packages (from flask) (7.1.2)
Collecting Werkzeug>=0.15
Downloading Werkzeug-1.0.1-py2.py3-none-any.whl (298 kB)
Requirement already satisfied: Jinja2>=2.10.1 in /opt/conda/lib/python3.8/site-packages (from flask) (2.11.2)
Collecting itsdangerous>=0.24
Downloading itsdangerous-1.1.0-py2.py3-none-any.whl (16 kB)
Requirement already satisfied: MarkupSafe>=0.23 in /opt/conda/lib/python3.8/site-packages (from Jinja2>=2.10.1->flask) (1.1.1)
Requirement already satisfied: six>=1.3.0 in /opt/conda/lib/python3.8/site-packages (from flask-restful) (1.15.0)
Requirement already satisfied: pytz in /opt/conda/lib/python3.8/site-packages (from flask-restful) (2020.5)
Collecting aniso8601>=0.82
Downloading aniso8601-8.1.1-py2.py3-none-any.whl (44 kB)
Installing collected packages: Werkzeug, itsdangerous, flask, aniso8601, flask-restful
Successfully installed Werkzeug-1.0.1 aniso8601-8.1.1 flask-1.1.2 flask-restful-0.3.8 itsdangerous-1.1.0
Removing intermediate container 97888ed0b989
---> d9f31d7e7c83
Step 6/10 : RUN mkdir /docker/model
---> Running in 89b237f6427c
Removing intermediate container 89b237f6427c
---> b2778ed90f4a
Step 7/10 : ENV MODEL_DIR=/docker/model
---> Running in d7a52c9249f9
Removing intermediate container d7a52c9249f9
---> 5157d919abd5
Step 8/10 : ENV MODEL_FILE=clf.joblib
---> Running in a7f75c6f79e5
Removing intermediate container a7f75c6f79e5
---> 790a21e54588
Step 9/10 : ENV METADATA_FILE=metadata.json
---> Running in b0b94567182c
Removing intermediate container b0b94567182c
---> 92a98ce95a8d
Step 10/10 : RUN python3 train.py
---> Running in d8055e4ef00d
Loading data...
Splitting data...
Fitting model...
Serializing model to: /docker/model/clf.joblib
Serializing metadata to: /docker/model/metadata.json
Removing intermediate container d8055e4ef00d
---> b1fb95b775ec
Successfully built b1fb95b775ec
Successfully tagged docker-ml-online:latest
Có Docker Image rồi, tiến hành push nó lên Docker Hub:
$ docker push tiensu/ml-model-online-infer:latest
The push refers to repository [docker.io/tiensu/ml-model-online-infer]
f0e40a44cb9c: Pushed
a079ef4fd38e: Pushed
76cba4a3a958: Pushed
3451a539eae2: Pushed
66f4cc63b50c: Mounted from tiensu/docker-ml
5f70bf18a086: Mounted from tiensu/docker-ml
6f5a41ae77fd: Mounted from tiensu/docker-ml
5a1b9a3f9355: Mounted from tiensu/docker-ml
b1d7816bac14: Mounted from tiensu/docker-ml
c91fed2d1998: Mounted from tiensu/docker-ml
cc70098d00e3: Mounted from tiensu/docker-ml
88727e93cbac: Mounted from tiensu/docker-ml
cadaf24035f3: Mounted from tiensu/docker-ml
8f170f4774e3: Mounted from tiensu/docker-ml
33bd52db887f: Mounted from tiensu/docker-ml
21e5dd010f50: Mounted from tiensu/docker-ml
ea370ab22368: Mounted from tiensu/docker-ml
421d1408f872: Mounted from tiensu/docker-ml
18fd1ca0de51: Mounted from tiensu/docker-ml
8f01aab6d756: Mounted from tiensu/docker-ml
e18a1c4e1d31: Mounted from tiensu/docker-ml
8552f27c3cd8: Mounted from tiensu/docker-ml
1a4c57efcc23: Mounted from tiensu/docker-ml
94b8fe888eac: Mounted from tiensu/docker-ml
02473afd360b: Mounted from tiensu/docker-ml
dbf2c0f42a39: Mounted from tiensu/docker-ml
9f32931c9d28: Mounted from tiensu/docker-ml
latest: digest: sha256:67c219ed32f9748c0c3ce64e8c4274932a8dadaf05510402f5d64a038bca2165 size: 6790
2.3 Tạo Kubernetes Deployment
Trong thư mục deployment, tạo file cấu hình (deployment-online-inference.yaml) của Deployment với nội dung như sau:
apiVersion: apps/v1
kind: Deployment
metadata:
name: online-inference-deployment
spec:
replicas: 2
selector:
matchLabels:
app: model-api
template:
metadata:
labels:
app: model-api
spec:
containers:
- name: model-api
imagePullPolicy: Always
image: tiensu/ml-model-online-infer:latest
command: ["python3", "api.py"]
ports:
- containerPort: 5000
Một số thông tin cần lưu ý ở đây:
Chạy các lệnh sau để tạo và kiểm tra Deployment:
$ kubectl create -f deployment-online-inference.yaml
deployment.apps/online-inference-deployment created
$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
online-inference-deployment 2/2 2 2 41s
Thực ra, Deployment không trực tiếp quản lý các Pods. Thay vào đó, nó sẽ tạo ra các ReplicaSet với mục đích duy trì sự ổn định của các Pods tại bất kì thời điểm nào trong suốt quá trình hoạt động.
Kiểm tra ReplicaSet được Deployment tạo ra:
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
online-inference-deployment-59c8579f48 2 2 2 68s
Chú ý: Tên của ReplicaSet = Tên của Deployment + chuỗi ngẫu nhiên.
Kiểm tra thử các Pods được quản lý bởi online-inference-deployment-59c8579f48
ReplicaSet:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
online-inference-deployment-59c8579f48-bg2vj 1/1 Running 0 97s
online-inference-deployment-59c8579f48-j9fl2 1/1 Running 0 97s
Chú ý: Tên của Pod = Tên của ReplicaSet + chuỗi ngẫu nhiên.
Thử debug một Pod xem có gì bất thường không?
$ kubectl describe pod online-inference-deployment-59c8579f48-bg2vj
Name: online-inference-deployment-59c8579f48-bg2vj
Namespace: default
Priority: 0
Node: duynm-vostro-3670/10.1.30.130
Start Time: Mon, 01 Feb 2021 18:15:27 +0700
Labels: app=model-api
pod-template-hash=59c8579f48
Annotations: cni.projectcalico.org/podIP: 192.168.24.197/32
cni.projectcalico.org/podIPs: 192.168.24.197/32
Status: Running
IP: 192.168.24.197
IPs:
IP: 192.168.24.197
Controlled By: ReplicaSet/online-inference-deployment-59c8579f48
Containers:
model-api:
Container ID: docker://4cde562c962b48ff4c6bc3c812b140d2555e1984f064108bd8bf607b122cef9a
Image: tiensu/ml-model-online-infer
Image ID: docker-pullable://tiensu/ml-model-online-infer@sha256:67c219ed32f9748c0c3ce64e8c4274932a8dadaf05510402f5d64a038bca2165
Port: 5000/TCP
Host Port: 0/TCP
Command:
python3
api.py
State: Running
Started: Mon, 01 Feb 2021 18:15:45 +0700
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-wp4xr (ro)
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
default-token-wp4xr:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-wp4xr
Optional: false
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2m25s default-scheduler Successfully assigned default/online-inference-deployment-59c8579f48-bg2vj to duynm-vostro-3670
Normal Pulling 2m22s kubelet Pulling image "tiensu/ml-model-online-infer"
Normal Pulled 2m8s kubelet Successfully pulled image "tiensu/ml-model-online-infer" in 14.038005704s
Normal Created 2m7s kubelet Created container model-api
Normal Started 2m7s kubelet Started container model-api
OK, mọi thứ đều đang hoạt động đúng như mong muốn.
Ta cũng có thể xem logs của Pod khi chạy:
$ kubectl logs -f online-inference-development-5d46c5c7dc-bg2vj
Loading model from: /docker/model/clf.joblib
* Serving Flask app "api" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 263-920-719
10.1.30.130 - - [02/Feb/2021 11:06:49] "POST /predict HTTP/1.1" 200 -
Tham số -f
dùng để xem log một các realtime.
2.4 Chạy Online Inference
Bây giờ ta sẽ thử gửi một yêu cầu dự đoán thông qua REST API để xem kết quả trả về. Tuy nhiên, có một chú ý quan trọng là REST API này chỉ mới hoạt động được bên trong phạm vi của Kubernetes Cluster. Để mở rộng nó ra ngoài Internet, chúng ta cần phải sử dụng thêm Service. Service sẽ được trình bày trong bài viết tiếp theo.
Chúng ta sẽ thực hiện Online Inference từ một Pod trong cùng Cluster với Deployment. Sử dụng lệnh sau để chạy và truy cập vào Pod python3
:
$ kubectl run python3 -ti --image=python:3.6 --command=true bash
If you don't see a command prompt, try pressing enter.
root@python3:/#
Phần xử lý Inference bây giờ đang nằm trên 2 Pods mà Deployment tạo ra. Ta sẽ gửi yêu cầu dự đoán đến chúng. Xem lại phần debug bên trên của Pod online-inference-deployment-59c8579f48-bg2vj
ta thấy Internal IP của nó là 192.168.24.197
Từ trong Pod python3
, thực hiện lệnh sau để gửi yêu cầu dự đoán:
$ curl -i -H "Content-Type: application/json" -X POST -d '{"CRIM": 15.02, "ZN": 0.0, "INDUS": 18.1, "CHAS": 0.0, "NOX": 0.614, "RM": 5.3, "AGE": 97.3, "DIS": 2.1, "RAD": 24.0, "TAX": 666.0, "PTRATIO": 20.2, "B": 349.48, "LSTAT": 24.9}' 192.168.24.197:5000/predict
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 41
Server: Werkzeug/1.0.1 Python/3.8.6
Date: Mon, 01 Feb 2021 11:22:25 GMT
{
"prediction": 12.273424794987877
}
Như vậy là ta đã nhận được kết quả dự đoán trả về, chứng tỏ Deployment của chúng ta đã hoạt động đúng như ta dự tính.
2.5 Xóa Deployment khi không sử dụng
Nếu không sử dụng nữa, ta thực hiện lệnh sau để xóa Deployment và các tài nguyên của nó:
$ kubectl delete deployment online-inference-development
deployment.apps "online-inference-development" deleted
Kiểm tra lại:
$ kubectl get rs
No resources found.
$ kubectl get pods
No resources found.
3. Kết luận
Xong, chúng ta đã thực hành thành công với Deployment, và ta cũng biết một thiếu sót của Deployment phải cần đến Service để giải quyết. Đó chính là nội dung của bài tiếp theo. Mời các bạn đón đọc!
Source code của bài này các bạn tham khảo tại đây.
8. Tham khảo