前言
Serverless如此火热似乎没啥好介绍的,我就不当搬运工了...
本文主要介绍笔者如何利用vscode插件+k8s+node来搭建一个可运行代码片段的serveless平台
主要有以下两个部分。
- vscode 插件
- k8S集群
先看一下最终效果
架构图:
实践过程
vscode插件
vscode 插件是平台的客户端,用户上传代码片段,触发执行,接受返回结果等作用。也是实践过程中较为容易的部分。
只需要注册右击按钮和对于文本的处理即可(相关代码省略)
let disposable = vscode.commands.registerTextEditorCommand('disheng-serverless-exec', (textEditor,edit) => {
const doc=textEditor.document;
const selection=textEditor.selection;
// 获取选取代码
const selectedText=doc.getText(selection);
// ..... post code
axios({
//.........
}).then((response)=>{
//处理返回并开始取回执行结果
let getResultTask=setInterval(()=>{
//.....
textEditor.edit((editBuilder)=>{
editBuilder.insert(selection.end,`\n//执行结果:${resultValue}\n`)
});
clearInterval(getResultTask);
//....
},1000);
}
});
})
环境准备(k8s集群)
一台2C2G 及其以上云服或者物理机器作为k8s master。(不要问为什么只用一台,因为穷)。
笔者采用是华为云新用户免费15天的2c4g的云服。
k8s的集群的安装笔者主要采用kubeadm 来安装.
国内安装主要是官方容器被墙的问题。
kubeadm config print init-defaults > kubeadm-init.yaml
修改repo 为registry.cn-hangzhou.aliyuncs.com/google_containers
再修改相关配置适合成自己的网络环境
执行
kubeadmin init --config kubeadmin-init.yaml
另由于笔者是采用单机来做k8s 集群,所以 很多非kube-system 的pod也会运行在master 节点上,所以我们需要更改master节点上的trait 的限制.
kubectl taint node {你的master节点的名称} node-role.kubernetes.io/master-
主要组件开发和部署
serverless-api
此组件主要是提供针对代码片段和执行结果的相关操作,笔者采用go&gin&redis 实现一个简单的resultful服务。
主要提供一下几个接口
- insertCode // 提交新的运行片段
- insertRunResult // 插入运行结果
- getNextRunCodeId // 获取下一个可以执行的代码片段的code的id
- getCode // 根据id获取code
- getResult // 根据id获取result
具体代码就不贴了,主要是对于redis的一些curd操作。
由于此pod即需要对外提供访问服务,有需要对内提供访问。所以笔者采用pod + service+
ingress 的方式来组织serverless-api。
以下笔者创建几类服务
serverless-api-deploymenet
apiVersion: apps/v1
kind: Deployment
metadata:
name: serverless-api-deployment
spec:
selector:
matchLabels:
app: serverless-api
template:
metadata:
labels:
app: serverless-api
spec:
containers:
- name: serverless-api
image: ********
ports:
- containerPort: 8080
- name: redis
image: redis:latest
ports:
- containerPort: 6379
apiVersion: apps/v1
kind: Service
metadata:
name: serverless-api-service
spec:
selector:
app: serverless-api
ports:
- protocol: TCP
port: 8080
targetPort: 8080
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: serverless-ingress
spec:
rules:
- host:*****.***.****
http:
paths:
- path: /
backend:
serviceName: serverless-api-service
servicePort: 8080
关于ingress的暴露 笔者主要采用的ingress controller + hostnework的方式来暴露。
// ........
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-ingress-controller
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
template:
metadata:
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
annotations:
prometheus.io/port: "10254"
prometheus.io/scrape: "true"
spec:
# wait up to five minutes for the drain of connections
terminationGracePeriodSeconds: 300
serviceAccountName: nginx-ingress-serviceaccount
nodeSelector:
kubernetes.io/os: linux
hostNetwork: true // 加入hostnewwork 。部署pod的node上会进行对应的端口绑定
containers:
- name: nginx-ingress-controller
image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.26.2
// .....
serverless-cronjob
此组件是集群内用于创建和调度作业容器,笔者这里设计了一个定时的任务。定时检查作业的容器,并且删除已经完成作业的任务的job容器,如果没有作业容器还在运行则获取下一段执行代码的id,创建新的作业容器
此组件 主要依赖 k8s.io/go-client
func main() {
config, err := rest.InClusterConfig()
if err != nil {
panic(err.Error())
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
panic(err.Error())
}
for {
pods, err := clientset.CoreV1().Pods("serverless-job").List(metav1.ListOptions{})
for pods,_ := range pods.Items{
// ... 删除已完成的pods,并决定是否获取并创建下一个job
}
response, err := http.Get("http://******/getNextRunCodeId")
// .......
jobClient := clientset.BatchV1().Jobs("serverless-job")
// 将获取的codeid利用env传递给作业容器内
job := batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "serverless-job-" + RandStr(12),
Namespace: "serverless-job",
},
Spec: batchv1.JobSpec{
Template: apiv1.PodTemplateSpec{
Spec: apiv1.PodSpec{
Containers: []apiv1.Container{
{
Name: "serverless-job-container",
Image:"*********",
Env: []apiv1.EnvVar{
{
Name: "CODEID",
Value: string(body),
},
},
},
},
RestartPolicy: apiv1.RestartPolicyNever,
},
},
},
}
// 创建job
createResult, err := jobClient.Create(&job)
// ****
time.Sleep(sleepTime * time.Second)
}
}
由于cronjob组件需要调用kubectl 且在集群内中运行,所以我们不能仅仅使用default token,我们需要使用高级一点的角色权限,并绑定到pods上。
以下是笔者的相关实践
创建新的命名空间为作业容器的建立做好准备
kubectl create namespaces serverless-job
apiVersion: apps/v1
kind: Deployment
metadata:
name: serverless-cronjob-deployment
spec:
selector:
matchLabels:
app: serverless-cronjob
template:
metadata:
labels:
app: serverless-cronjob
spec:
serviceAccountName: serverless-cronjob // 关联上我们创建的账户
containers:
- name: serverless-cronjob
image: ××××××
apiVersion: v1
kind: ServiceAccount
metadata:
name: serverless-cronjob
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: kubernetes-dashboard
labels:
k8s-app: kubernetes-dashboard
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: serverless-cronjob
namespace: default
于此同时,我们进入到pods内就可以看到秘钥成功的被挂在默认的目录下。
serverless-node-job
此组件主要是我们的作业容器。笔者这里仅仅实践了js的代码片段执行实践。
此组件主要作用就是获取代码 并利用node v8模块对代码运行并将结果插入到serverless-api中。
const { CODEID }=process.env
if(CODEID){
//.....
axios({
.../
}).then(response=>{
try{
const runResult=runCode(response)
}catch(err){
insertResult(err)
}
insertResult(runResult)
// .....
})
}
至此,笔者关于serverless平台搭建的实践到此结束。
总结
笔者在这里仅仅简单实践了nodejs代码片段的运行,对于serverless更广泛的应用其实还并未进行尝试。例如函数的注册和各类方式的触发器,资源调度/伸缩,日志/监控,长伺服应用等等. 关于serverless市面上主要是提供serverless函数计算和云应用两项服务。其实也就是长短伺服业务。
但仅仅在笔者简单的实践过程中,也出现了一些颇为头疼的问题,一是函数执行的实时性的问题,可以从实践成果的gif中看到从提交片段到返回结果的过程中也有肉眼可查明显的延时,容器创建和node启动过程的耗时都是比较大的。因此还是需要进行一定的优化。比如创建一个同执行环境的容器池,对外开放触发接口,把容器创建和node启动过程省略?
更多问题不在这里赘述,有兴趣的朋友可以随时交流。