基于自建 Knative 的 k8s 集群部署 问题背景 Knative(下称 kn)的部署工具极为复杂,其对于 func
的部署流程也较为笨重,在 kn
中也不支持函数流的部署工作,因此我们需要在自己搭建的原生 k8s 集群上安装 kn,同时我自己编写了一套 kn 的部署工具,可以比原本的部署更加轻便快捷,也在原来的 kn
的基础上,增加了对多函数部署的支持。
问题定义 原生 Knative 部署的问题 kn 提供的函数部署工具 kn-func
通过 func.yaml
配置文件部署 kn
函数,其主要的流程可以概括为
从 gcr
仓库中拉取对应的镜像
将用户编写的代码移动到镜像中,在所拉取的镜像中构建新的镜像
将构建好的镜像推送到用户提供的 Docker 仓库中
部署 k8s 服务,从 Docker 仓库拉取用户所需要的镜像
以上的部署方式存在以下的问题
从 gcr
仓库中拉取镜像的过程由于国内的网络问题往往会拉取失败,即便配置了代理也会出现拉取失败的问题
重新构建一个镜像的时间过长,而且每次都要重新构建显然是多余的
Base-Image+Code-Volume 既然一个容器运行只需要用户的代码 + 运行环境,那么其实我们可以先将容器所需的运行环境准备好,然后想个办法将用户的代码打包到容器里头就好。然后我们只需要在 dockerhub 仓库上部署好自己的基础镜像,就可以复用该镜像来实现用户的所有 Serverless 函数。但是这样也会带来另外的一些问题:
解决方案 k8s 集群环境准备 由于我们用的是自建的 k8s 集群,因此不再直接通过 kn quickstart
插件安装 kn
服务
先决条件
k8s 版本 v1.27
已经安装了 kubeadm
,kubelet
,kubectl
实现步骤
1 2 wget https://github.com/knative/serving/releases/download/knative-v1.12.4/serving-crds.yaml kubectl apply -f serving-crds.yaml
1 2 wget https://github.com/knative/serving/releases/download/knative-v1.12.4/serving-core.yaml kubectl apply -f serving-core.yaml
1 2 wget https://github.com/knative/net-kourier/releases/download/knative-v1.12.3/kourier.yaml kubectl apply -f kourier.yaml
1 2 3 4 kubectl patch configmap/config-network --namespace knative-serving --type merge --patch '{"data":{"ingress-class":"kourier.ingress.networking.knative.dev"}}'
1 kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.12.4/serving-default-domain.yaml
1 2 3 4 kubectl patch configmap/config-domain \--namespace knative-serving \--type merge \--patch "{\"data\":{\"$EXTERNAL_IP .sslip.io\":\"\"}}" kubectl patch svc kourier -n kourier-system -p "{\"spec\": {\"type\": \"LoadBalancer\", \"externalIPs\": [\"$EXTERNAL_IP \"]}}"
1 kubectl --namespace kourier-system get service kourier
持久化卷的部署 通过创建 pv 以及 pvc 来部署持久化卷
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 apiVersion: v1 kind: PersistentVolume metadata: name: faasit-code-volume spec: capacity: storage: 1Gi volumeMode: Filesystem accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: "" hostPath: path: --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: faasit-code-volume-claim spec: resources: requests: storage: 512Mi volumeMode: Filesystem accessModes: - ReadWriteOnce
文件分发管理器 使用 Nginx 服务作为文件分发的管理器
构建 Nginx 镜像 1 2 3 4 5 6 7 8 9 FROM nginx:alpineRUN rm /etc/nginx/conf.d/default.conf COPY ./nginx.conf /etc/nginx/conf.d/default.conf RUN mkdir -p /data/uploads EXPOSE 80
其中配置文件如下,我也想做成一个纯 Nginx 服务器的,我也不知道为啥文件死活上传不上去
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 server { listen 80 ; server_name localhost; location / { root /usr/share/nginx/html; index index.html index.htm; } location /data { alias /data; autoindex on ; } }
然后执行
1 2 docker build -t docker.io/xdydy/faasit-file-server:v1 docker push docker.io/xdydy/faasit-file-server:v1
部署 Nginx 服务 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 43 44 45 46 apiVersion: apps/v1 kind: Deployment metadata: name: nginx-file-server spec: replicas: 1 selector: matchLabels: app: nginx-file-server template: metadata: labels: app: nginx-file-server spec: volumes: - name: data-volume persistentVolumeClaim: claimName: faasit-code-volume-claim containers: - name: nginx image: docker.io/xdydy/faasit-file-server:v1 imagePullPolicy: Always ports: - containerPort: 80 volumeMounts: - name: data-volume mountPath: /data/uploads resources: requests: cpu: 100m memory: 128Mi limits: cpu: 250m memory: 256Mi --- apiVersion: v1 kind: Service metadata: name: nginx-file-server spec: type: ClusterIP ports: - port: 80 targetPort: 80 selector: app: nginx-file-server
构建运行时环境镜像 以 python 运行时为例,由于容器里头没有用户提交的代码,于是我们可以在应用程序启动时,从 Nginx 服务上获取相应的代码文件
1 2 3 4 5 6 7 8 9 10 11 12 import zipfiledef check_and_load_code (): if not os.path.exists(f'/{codeDir} /{codeName} .py' ): code_url = f"http://10.102.131.24:80/data/uploads/{downLoadFile} " r = requests.get(code_url, stream=True , proxies={'http' : None , 'https' : None }) with open ('/tmp/code.zip' , 'wb' ) as f: for chunk in r.iter_content(chunk_size=1024 ): if chunk: f.write(chunk) with zipfile.ZipFile('/tmp/code.zip' , 'r' ) as z: z.extractall('/code' )
在镜像中,我们开启一个 HTTP
服务器响应用户的请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @app.route('/' , methods=['POST' , 'GET' ] ) async def local_invoke (): try : req = request.get_json() logger.info(f"[INPUT] {req} " ) check_and_load_code() code = importlib.import_module(codeName) result = await code.handler(req) logger.info(f"[OUTPUT] {result} " ) resp = jsonify(result) return resp except Exception as e: logger.error(f"[ERROR] {e} " ) return make_response(jsonify({'error' : str (e)}), 500 )
k8s 容器还需要服务提供一个就绪探针,用来让 k8s 知道服务已经就绪,于是我们随便写点代码让他返回就行
1 2 3 @app.route('/health' ) def health (): return jsonify({'status' : 'ok' })
在该环境下,我们提供了 faasit-runtime
作为一系列运行环境的支撑,在 python 环境下,我们构建 python 的运行时容器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 FROM python:3.10 WORKDIR /app COPY ./src /app COPY ./requirements.txt /app/requirements.txt COPY ./faasit-runtime /faasit-runtime RUN pip install --upgrade pip RUN pip install "flask[async]" RUN pip install -e /faasit-runtime RUN mkdir -p /code && cd /code WORKDIR /code ENV FAASIT_PROVIDER localCMD [ "bash" ]
构建,推送
1 2 docker build -t docker.io/xdydy/faasit-python-runtime:0.0 .1 docker push docker.io/xdydy/faasit-python-runtime:0.0 .1
Knative 服务部署 使用 kn
的 API 部署 kn
服务的示例配置如下
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 apiVersion: serving.knative.dev/v1 kind: Service metadata: name: namespace: default spec: template: spec: containers: - image: docker.io/xdydy/faasit-python-runtime:0.0.1 ports: - containerPort: 9000 readinessProbe: httpGet: path: /health port: 9000 initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 1 successThreshold: 1 failureThreshold: 3 securityContext: runAsNonRoot: false allowPrivilegeEscalation: false capabilities: drop: - ALL seccompProfile: type: RuntimeDefault env: - name: FAASIT_FUNC_NAME value: split - name: FAASIT_PROVIDER value: knative - name: FAASIT_CODE_DIR value: wordcount-split.zip - name: FAASIT_WORKFLOW_NAME value: wordcount command: - python args: - /app/server.py
至于容器里头的代码,可以通过 kubectl cp
命令将本地的代码文件拷贝过去,可以通过一个简单的命令来查找运行文件分发器的 pod
1 2 pod=kubectl get pod | grep nginx-file-server | awk '{print $1}' kubectl cp code.zip $(pod):/data/uploads
这样函数就部署完成了
也支持调用