关注小众语言、AI技术,记录、分享技术点滴!

0%

k8s-device-plugin用于是Kubernetes调用GPU的插件。

nvidia的k8s插件安装之后作为daemonset在每个节点上运行,插件运行之后会向master汇报每个节点的GPU数量,运行需要GPU的POD。

安装k8s-device-plugin的前提条件如下:

  • Nvidia的驱动版本 ~= 384.81
  • nvidia-docker >= 2.0 || nvidia-container-toolkit >= 1.7.0
  • 配置nvidia-container-runtime 为默认的运行时
  • Kubernetes的版本≥1.10

安装驱动

在每个节点上nvidia驱动。

安装nvidia-container-toolkit

1
2
3
4
5
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/libnvidia-container/gpgkey | apt-key add -
curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | tee /etc/apt/sources.list.d/libnvidia-container.list

apt-get update && apt-get install -y nvidia-container-toolkit

kubernetes基于docker运行时配置
将docker的默认运行时改成nvidia-container-runtime,新建/etc/docker/daemon.json文件,添加以下内容:

1
2
3
4
5
6
7
8
9
{
"default-runtime": "nvidia",
"runtimes": {
"nvidia": {
"path": "/usr/bin/nvidia-container-runtime",
"runtimeArgs": []
}
}
}

之后重启docker:

1
2
systemctl daemon-reload
systemctl restart docker

kubernetes基于containerd运行时配置
将containerd的默认运行时改成nvidia-container-runtime,修改/etc/containerd/config.toml文件,添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
[plugins."io.containerd.grpc.v1.cri".containerd]
default_runtime_name = "nvidia"
...

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia]
privileged_without_host_devices = false
runtime_engine = ""
runtime_root = ""
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia.options]
BinaryName = "/usr/bin/nvidia-container-runtime"

配置参考,如下图所示:

之后重启containerd:

1
systemctl restart containerd

安装k8s-device-plugin

安装k8s-device-plugin有两种方式:

使用yaml文件安装
执行以下命令,以daemonset的方式运行nvidia-device-plugin

1
$ kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.12.2/nvidia-device-plugin.yml

这种方式的nvidia-device-plugin只是简单的静态daemonset,只能使用nvidia-device-plugin最基础的功能。

使用helm安装

nvidia-device-plugin的一些参数可供配置:

Flag Envvar Default Value 作用
–mig-strategy $MIG_STRATEGY “none”
–fail-on-init-error $FAIL_ON_INIT_ERROR true
–nvidia-driver-root $NVIDIA_DRIVER_ROOT “/“
–pass-device-specs $PASS_DEVICE_SPECS false
–device-list-strategy $DEVICE_LIST_STRATEGY “envvar”
–device-id-strategy $DEVICE_ID_STRATEGY “uuid”
–config-file $CONFIG_FILE “”

在使用helm来部署nvidia-device-plugin时可以设置这些参数,按下面的配置文件所示:

1
2
3
4
5
6
7
8
9
version: v1
flags:
migStrategy: "none"
failOnInitError: true
nvidiaDriverRoot: "/"
plugin:
passDeviceSpecs: false
deviceListStrategy: "envvar"
deviceIDStrategy: "uuid"

基于时间片共享GPU

nvidia-device-plugin允许GPU超额订阅,使多个进程共享同一个GPU。但是GPU并没有对进程进行计算资源和显存方面的隔离,不同的进程运行在相同的故障域。这意味着如果一个workload崩溃,所有的workload都会崩溃。

配置文件模块如下:

1
2
3
4
5
6
7
8
9
version: v1
sharing:
timeSlicing:
renameByDefault: <bool>
failRequestsGreaterThanOne: <bool>
resources:
- name: <resource-name>
replicas: <num-replicas>
...

当resources中配置了replicas,就可以为一个GPU指定多个副本,这样多个进程就可以共享同一个GPU。

如果renameByDefault设置为true,每个资源的名字都会变成<resource-name>.shared,而不是简单的<resource-name>

如果failRequestsGreaterThanOne设置为true,插件将无法为请求大于1个GPU的容器分配资源。容器的 pod 将失败并出现 UnexpectedAdmissionError,需要手动删除、更新和重新部署。

配置示例如下:

1
2
3
4
5
6
version: v1
sharing:
timeSlicing:
resources:
- name: nvidia.com/gpu
replicas: 15

当该配置被应用后,一个拥有1个GPU的节点会出现15个nvidia.com/gpu资源。插件简单地为每个GPU创建了15个引用,然后不加选择地将它们分配给容器。

部署插件

首先安装helm

1
2
3
4
5
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | tee /usr/share/keyrings/helm.gpg > /dev/null
apt-get install apt-transport-https --yes
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | tee /etc/apt/sources.list.d/helm-stable-debian.list
apt-get update
apt-get install helm

配置插件的helm仓库

1
2
$ helm repo add nvdp https://nvidia.github.io/k8s-device-plugin
$ helm repo update

检验插件是否可用

1
2
3
$ helm search repo nvdp --devel
NAME CHART VERSION APP VERSION DESCRIPTION
nvdp/nvidia-device-plugin 0.15.0 0.15.0 A Helm chart for ...

简单部署插件

1
2
3
4
helm upgrade -i nvdp nvdp/nvidia-device-plugin \
--namespace nvidia-device-plugin \
--create-namespace \
--version 0.15.0

配置time-slicing

一、使用配置文件部署

新建yaml配置文件:

1
2
3
4
5
6
7
8
cat << EOF > dp-config.yaml
version: v1
sharing:
timeSlicing:
resources:
- name: nvidia.com/gpu
replicas: 15
EOF

使用yaml配置文件部署插件:

1
2
3
4
5
helm upgrade -i nvdp nvdp/nvidia-device-plugin \
--namespace nvidia-device-plugin \
--create-namespace \
--version 0.15.0 \
--set-file config.map.config=dp-config.yaml

二、使用ConfigMap配置部署

如果您不希望 Helm 为您创建 ConfigMap,您也可以将其指向预先创建的 ConfigMap,如下所示:

创建命名空间:

1
$ kubectl create ns nvidia-device-plugin

创建ConfigMap:

1
2
$ kubectl create cm -n nvidia-device-plugin nvidia-plugin-configs \
--from-file=config=/tmp/dp-example-config0.yaml

使用ConfigMap配置部署插件:

1
2
3
4
5
$ helm upgrade -i nvdp nvdp/nvidia-device-plugin \
--version=0.15.0 \
--namespace nvidia-device-plugin \
--create-namespace \
--set config.name=nvidia-plugin-configs

安装之后,我们发现一个拥有1个GPU的节点可以看到有15个nvidia.com/gpu资源

运行GPU任务
当nvidia-device-plugin的daemonset部署之后,kubernetes就可以下发使用gpu的pod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
restartPolicy: Never
containers:
- name: cuda-container
image: nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda10.2
resources:
limits:
nvidia.com/gpu: 1 # requesting 1 GPU
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
EOF

查看pod的日志:

1
2
3
4
5
6
7
$ kubectl logs gpu-pod
[Vector addition of 50000 elements]
Copy input data from the host memory to the CUDA device
CUDA kernel launch with 196 blocks of 256 threads
Copy output data from the CUDA device to the host memory
Test PASSED
Done

至此gpu已经可以使用。

llama.cpp是一个基于C++实现的大模型推理工具,通过优化底层计算和内存管理,可以在不牺牲模型性能的前提下提高推理速度。

方法一(使用python:3.10-bullseye docker镜像)

一、下载python镜像(docker)

1
2
# 下载的是python 3.10 Debian 11的版本
$ docker pull python:3.10-bullseye

二、下载CUDA Toolkit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ mdkir /src/ && cd /src/
# CUDA 12.0 支持 Debian 11
$ wget https://developer.download.nvidia.com/compute/cuda/12.0.0/local_installers/cuda_12.0.0_525.60.13_linux.run

# 只安装toolkit
$ sh cuda_12.0.0_525.60.13_linux.run --silent --toolkit

# 删除cuda安装文件
$ rm -f cuda_12.0.0_525.60.13_linux.run

# 将 nvcc 添加到环境变量
$ export PATH=$PATH:/usr/local/cuda/bin

# 确保已经成功安装了cuda toolkit
$ nvcc -V
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2022 NVIDIA Corporation
Built on Mon_Oct_24_19:12:58_PDT_2022
Cuda compilation tools, release 12.0, V12.0.76
Build cuda_12.0.r12.0/compiler.31968024_0

三、安装llama-cpp-python库

1
2
3
# To install with CUDA support, set the `LLAMA_CUDA=on` environment variable before installing:

$ CMAKE_ARGS="-DLLAMA_CUDA=on" pip install llama-cpp-python

四、下载GGUF格式模型文件
GGUF格式是用于存储大型模型预训练结果的,相较于Hugging Face和torch的bin文件,它采用了紧凑的二进制编码格式、优化的数据结构以及内存映射等技术,提供了更高效的数据存储和访问方式。llama-cpp-python 主要是使用GGUF格式的大模型文件。

从Hugging Face平台上下载

1
$ wget https://huggingface.co/lmstudio-community/Meta-Llama-3-8B-Instruct-GGUF/resolve/main/Meta-Llama-3-8B-Instruct-Q4_K_M.gguf?download=true

五、使用llama-cpp-python进行推理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from llama_cpp import Llama

# 加载模型
llm = Llama(
model_path="./Meta-Llama-3-8B-Instruct-Q4_K_M.gguf",
n_gpu_layers=-1, # Use GPU acceleration
)

# 使用OpenAI接口规范进行推理
output = llm.create_chat_completion(
messages = [
{"role": "system", "content": "You are an assistant who perfectly describes images."},
{
"role": "user",
"content": "Describe this image in detail please."
}
]
)
# 输出结果
print(output)

六、查看GPU占用情况(nvidia-smi)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ nvidia-smi 
Fri Apr 26 22:17:42 2024
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 525.105.17 Driver Version: 525.105.17 CUDA Version: 12.0 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 Tesla T4 On | 00000000:00:07.0 Off | 0 |
| N/A 39C P0 81W / 70W | 5153MiB / 15360MiB | 61% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| 0 N/A N/A 4788 C python 5150MiB |
+-----------------------------------------------------------------------------+

至此,让llama-cpp-python使用GPU加速推理,大功告成!

方法二(使用cog方式制作镜像)

Cog 是一种开源工具,可让您将机器学习模型打包到标准的生产就绪容器中。您可以将打包的模型部署到您自己的基础架构或Replicate平台上。

使用Cog方式进行镜像制作,详情请查看以下github仓库。
https://github.com/shideqin/cog-codellama3-cpp

你有没有在使用k8s过程中遇到过这种情况: 通过kubectl delete指令删除一些资源时,一直处于Terminating状态。 这是为什么呢?

本文将介绍当你执行kubectl delete语句时,K8s内部都执行了哪些操作。 以及为何有些资源’删除不掉’(具体表现为一直Terminating,删除namespace时很容易遇到这种情况)

接下来,我们聚焦讨论以下四个方面:

1)资源的哪些属性会对删除操作产生影响?
2)finalizers与owner references属性是如何影响删除操作的?
3)如何利用Propagation Policy(分发策略)更改删除顺序?
4)删除操作的工作原理?

1. 词汇表

1)资源: k8s的资源对象(如configmap, secret, pod…)
2)finalizers: 终结器,存放键的列表。列表内的键为空时资源才可被删除
3)owner references: 所有者引用(归谁管理/父资源对象是谁)
4)kubectl: K8s客户端工具

2. 强制删除pod

每当删除namespace或pod等一些Kubernetes资源时,有时资源状态会卡在terminating,很长时间无法删除,甚至有时增加–force flag(强制删除)之后还是无法正常删除。这时就需要edit该资源,将字段finalizers设置为null,之后Kubernetes资源就正常删除了。

当删除pod时有时会卡住,pod状态变为terminating,无法删除pod

1)强制删除pod

1
$ kubectl delete pod xxx -n xxx --force --grace-period=0

2)如果强制删除还不行,设置finalizers为空
如果一个容器已经在运行,这时需要对一些容器属性进行修改,又不想删除容器,或不方便通过replace的方式进行更新。kubernetes还提供了一种在容器运行时,直接对容器进行修改的方式,就是patch命令。

1
$ kubectl patch pod xxx -n xxx -p '{"metadata":{"finalizers":null}}'

这样pod就可以删除了。

3. k8s删除流程

删除操作看似简单,但是有很多因素可能会干扰删除,包括finalizers与owner references属性

4. Finalizers是什么?
上面我们提到了两个属性:finalizers与owner references可能会干扰删除操作,导致删除阻塞或失败。 那Finalizers是什么?会对删除有何影响呢?

当要理解Kubernetes中的资源删除原理时,了解finalizers(以下我们称finalizers为终结器)的工作原理是很有帮助的, 可以帮助您理解为什么有些对象无法被删除。

终结器是资源发出预删除操作信号的属性, 控制着资源的垃圾收集,并用于提示控制器在删除资源之前执行哪些清理操作。

finalizers本质是包含键的列表,不具有实际意义。与annotations(注释)类似,finalizers是可以被操作的(增删改)。

以下终结器您可能遇到过:

  • kubernetes.io/pv-protection
  • kubernetes.io/pvc-protection

这两个终结器作用于卷,以防止卷被意外删除。

类似地,一些终结器可用于防止资源被删除,但不由任何控制器管理。 下面是一个自定义的configmap,它没有具体值,但包含一个终结器:

1
2
3
4
5
6
7
8
$ cat <<EOF | kubectl create -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: mymap
finalizers:
- kubernetes
EOF

终结器通常用于名称空间(namespace),而管理configmap资源的控制器不知道该如何处理finalizers字段。 下面我们尝试删除这个configmap对象:

1
2
3
4
$ kubectl delete configmap/mymap &
configmap "mymap" deleted
$ jobs
[1]+ Running kubectl delete configmap/mymap

Kubernetes返回该对象已被删除,然而它并没有真正意义上被删除,而是在删除的过程中。 当我们试图再次获取该对象时,我们发现该对象多了个deletionTimestamp(删除时间戳)字段。

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
$ kubectl get cm mymap -o yaml
apiVersion: v1
kind: ConfigMap
metadata:
creationTimestamp: "2021-09-29T11:04:40Z"
deletionGracePeriodSeconds: 0
deletionTimestamp: "2021-09-29T11:04:55Z"
finalizers:
- kubernetes
managedFields:
- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:metadata:
f:finalizers:
.: {}
v:"kubernetes": {}
manager: kubectl
operation: Update
time: "2021-09-29T11:04:40Z"
name: mymap
namespace: default
resourceVersion: "1378430"
selfLink: /api/v1/namespaces/default/configmaps/mymap
uid: 8d6ca0b1-4840-4597-8164-a63b526dbf5f

简而言之,当我们删除带有finalizers字段的对象时,该对象仅仅是被更新了,而不是被删除了。 这是因为Kubernetes获取到该对象包含终结器,通过添加deletionTimestamp(删除时间戳)字段将其置于只读状态(删除终结器键更新除外)。 换句话说,在删除该对象终结器之前,删除都不会完成。

接下来我们尝试通过patch命令删除终结器,并观察configmap/mymap是否会被’真正’删除。

1
2
3
$ kubectl patch configmap/mymap \
--type json \
--patch='[ { "op": "remove", "path": "/metadata/finalizers" } ]'

再次检索该对象

1
2
$ kubectl get cm mymap
Error from server (NotFound): configmaps "mymap" not found

发现该对象已被真正删除,下图描述了带有finalizers字段的对象删除流程:

总结:当您试图删除一个带有终结器的对象,它将一直处于预删除只读状态, 直到控制器删除了终结器键或使用Kubectl删除了终结器。一旦终结器列表为空,Kubernetes就可以回收该对象,并将其放入要从注册表中删除的队列中

带有finalizers字段的对象无法删除的原因大致如下:

  • 对象存在finalizers,关联的控制器故障未能执行或执行finalizer函数hang住: 比如namespace控制器无法删除完空间内所有的对象, 特别是在使用aggregated apiserver时,第三方apiserver服务故障导致无法删除其对象。 此时,需要会恢复第三方apiserver服务或移除该apiserver的聚合,具体选择哪种方案需根据实际情况而定。
  • 集群内安装的控制器给一些对象增加了自定义finalizers,未删除完fianlizers就下线了该控制器,导致这些fianlizers没有控制器来移除他们。 此时,需要恢复该控制器会手动移除finalizers(多出现于自定义operator),具体选择哪种方案根据实际情况而定。

5. Owner References又是什么?
上面我们提到了两个属性:finalizers与owner references可能会干扰删除操作,导致删除阻塞或失败。 并介绍了Finalizers,接下来我们聊聊Owner References.

Owner References(所有者引用或所有者归属)描述了对象组之间的关系。 指定了资源彼此关联的属性,因此可以级联删除整个资源树。

当存在所有者引用时,将处理终结器规则。所有者引用由名称和UID组成

所有者引用相同名称空间内的链接资源,它还需要UID以使该引用生效(确保唯一)。 Pods通常具有对所属副本集的所有者引用。 因此,当Deloyment或有StatefulSet被删除时,子ReplicaSet和Pod将在流程中被删除。

我们通过下面的例子,来理解Owner References(所有者引用)的工作原理:

1.创建cm/mymap-parent对象

1
2
3
4
5
6
$ cat <<EOF | kubectl create -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: mymap-parent
EOF

2.获取cm/mymap-parent的UID

1
$ CM_UID=$(kubectl get configmap mymap-parent -o jsonpath="{.metadata.uid}")

3.创建cm/mymap-child对象,并设置ownerReferences字段声明所有者引用(通过kind、name、uid字段确保选择器可以匹配到)

1
2
3
4
5
6
7
8
9
10
11
$ cat <<EOF | kubectl create -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: mymap-child
ownerReferences:
- apiVersion: v1
kind: ConfigMap
name: mymap-parent
uid: $CM_UID
EOF

即cm/mymap-parent为cm/mymap-child的父对象,此时我们删除cm/mymap-parent对象并观察cm/mymap-child对象状态

1
2
3
4
5
6
7
8
9
10
$ kubectl get cm
NAME DATA AGE
mymap-child 0 2m44s
mymap-parent 0 3m

$ kubectl delete cm mymap-parent
configmap "mymap-parent" deleted

$ kubectl get cm
No resources found in default namespace.

即我们通过删除父对象,间接删除了父对象下的所有子对象。 这种删除k8s中被称为级联删除。我们可不可以只删除父对象,而不删除子对象呢?

答案是: 可以的,删除时通过添加–cascade=false参数实现,我们通过下面的例子来验证:

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
$ cat <<EOF | kubectl create -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: mymap-parent
EOF

$ CM_UID=$(kubectl get configmap mymap-parent -o jsonpath="{.metadata.uid}")

$ cat <<EOF | kubectl create -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: mymap-child
ownerReferences:
- apiVersion: v1
kind: ConfigMap
name: mymap-parent
uid: $CM_UID
EOF

$ kubectl delete --cascade=false configmap/mymap-parent
configmap "mymap-parent" deleted

$ kubectl get cm
NAME DATA AGE
mymap-child 0 107s

–cascade=false参数实际改变了父-子资源的删除顺序,k8s中关于父-子资源删除策略有以下三种:

  • Foreground: 子资源在父资源之前被删除(post-order)
  • Background: 父资源在子资源之前被删除(pre-order)
  • Orphan: 忽略所有者引用进行删除

6. 强制删除命名空间
有一种情况可能需要强制删除命名空间:

如果您已经删除了一个命名空间,并删除了它下面的所有对象,但名称空间仍然存在,一般为Terminating状态。 则可以通过更新名称空间的finalize属性来强制删除该名称空间。

  1. 会话1

    1
    $ kubectl proxy
  2. 会话2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    $ NAMESPACE_NAME=test
    cat <<EOF | curl -X PUT \
    127.0.0.1:8001/api/v1/namespaces/$NAMESPACE_NAME/finalize \
    -H "Content-Type: application/json" \
    --data-binary @-
    {
    "kind": "Namespace",
    "apiVersion": "v1",
    "metadata": {
    "name": "$NAMESPACE_NAME"
    },
    "spec": {
    "finalizers": null
    }
    }
    EOF

我们应该谨慎思考是否强制删除命名空间,因为这样做可能只删除名称空间,命名空间下的其他资源删不完全,最终导致留下孤儿对象。 比如资源对象A存在于ddd命名空间,此时若强制删除ddd命名空间, 且对象A又未被删除,那么对象A便成了孤儿对象。

当出现孤儿对象时,可以手动重新创建名称空间,随后可以手动清理和恢复该对象。

在lua原生语法特性中是不具备面向对象设计的特性。因此,要想在lua上像其他高级语言一样使用面向对象的设计方法,就需要使用原生的元表(metatable)来模拟面向对象设计。

一、元表setmetatable
对指定 table 设置元表(metatable),如果元表(metatable)中存在__metatable键值,setmetatable会失败。

以下实例演示了如何对指定的表设置元表:

1
2
3
mytable = {}                          -- 普通表
mymetatable = {} -- 元表
setmetatable(mytable,mymetatable) -- 把 mymetatable 设为 mytable 的元表

二、元表__index 元方法
这是 metatable 最常用的键。
当你通过键来访问 table 的时候,如果这个键没有值,那么Lua就会寻找该table的metatable(假定有metatable)中的_index 键。如果_index包含一个表格,Lua会在表格中查找相应的键。

1
2
3
4
5
6
other = { foo = 3 }
t = setmetatable({}, { __index = other })
print(t.foo)
3
print(t.bar)
nil

三、使用metatable实现继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
local _M = {
version = 'lua 1.0'
}

local parent = {
__index = _M
}

function parent.new()
-- 初始化new,如果没有这句,那么类所建立的对象如果有一个改变,其他对象都会改变
local new = {}
-- 使用setmetatable来实现继承
setmetatable(new, parent)
return new
end

function _M:echo()
print("M:echo "..self.version)
end

四、使用metatable实现重载和多态

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
local _M = {
version = 'lua 1.0'
}

local parent = {
__index = _M
}

function parent.new()
-- 初始化new,如果没有这句,那么类所建立的对象如果有一个改变,其他对象都会改变
local new = {}
-- 使用setmetatable来实现继承
setmetatable(new, parent)
return new
end

function _M:echo()
print("M:echo "..self.version)
end


local _M2 = parent.new()

local child = {
__index = _M2
}

function child.new()
-- 初始化new,如果没有这句,那么类所建立的对象如果有一个改变,其他对象都会改变
local new = {}
-- 使用setmetatable来实现继承
setmetatable(new, child)
return new
end

-- 重载echo方法
function _M2:echo()
print("M2:echo "..self.version)
end


test = parent.new()
test:echo()

test = child.new()
test:echo()

luaosslluacrypto的代替版本,是针对 Lua 5.1、5.2、5.3 和 LuaJIT 的 OpenSSL 综合绑定。是 Lua 世界中最全面的 OpenSSL 绑定。luaosll安装依赖OpenSSL库,所以需要先安装OpenSSL。

一、安装OpenSSL
下载好的OpenSSL源码目录下,编译安装OpenSSL(以下均使用默认路径“/usr/local”进行安装)

1)生成Makefile文件

1
2
# 根据编译平台及环境自动生成Makefile文件,可以通过./config --prefix指定安装路径,-Wl,-rpath参数指定OpenSSL运行时依赖libcrypto、libssl库的路径。
./config -Wl,-rpath,/usr/local/lib

2)安装OpenSSL

1
2
make
make install

3)查看OpenSSL版本

1
2
3
4
openssl version

# 显示如下格式内容说明安装成功(本文以安装OpenSSL 1.1.1q为例)
OpenSSL 1.1.1q 5 Jul 2022 (Library: OpenSSL 1.1.1k FIPS 25 Mar 2021)

二、安装luaosll

1
2
3
4
5
6
7
8
9
10
11
12
luarocks install luaossl

# 显示如下格式内容说明安装成功
Installing https://luarocks.org/luaossl-20220711-0.src.rock

luaossl 20220711-0 depends on lua (5.1-1 provided by VM)
Applying patch config.h.diff...
Hunk 1 found at offset 2...
gcc -O2 -fPIC -I/usr/local/include -c src/openssl.c -o src/openssl.o -D_REENTRANT -D_THREAD_SAFE -DCOMPAT53_PREFIX=luaossl -D_GNU_SOURCE -I/usr/local/include -I/usr/local/include
gcc -O2 -fPIC -I/usr/local/include -c vendor/compat53/c-api/compat-5.3.c -o vendor/compat53/c-api/compat-5.3.o -D_REENTRANT -D_THREAD_SAFE -DCOMPAT53_PREFIX=luaossl -D_GNU_SOURCE -I/usr/local/include -I/usr/local/include
gcc -shared -o _openssl.so src/openssl.o vendor/compat53/c-api/compat-5.3.o -L/usr/local/lib64 -L/usr/local/lib64 -Wl,-rpath,/usr/local/lib64 -Wl,-rpath,/usr/local/lib64 -lssl -lcrypto -lpthread -lm -ldl
luaossl 20220711-0 is now installed in /usr/local (license: MIT/X11)

KEDA 是一个基于 Kubernetes 的事件驱动自动缩放器。使用 KEDA,您可以根据需要处理的事件数量来驱动 Kubernetes 中任何容器的扩展。KEDA可以支持很多自定义事件源,如:Mysql、MongoDB、Redis、ActiveMQ、Kafka、Prometheus、Metrics API等。本文使用nginx中的stub_status数据 + Prometheus为事件源进行KEDA HPA配置。

一、开启nginx(stub_status)
nginx中的stub_status模块主要用于查看Nginx的一些状态信息。

1)查看nginx时候有安装该模块。

1
/usr/local/nginx/sbin/nginx -V

2)安装stub_status模块
(注意:有的话可以忽略此步骤,就不用安装了)
在nginx编译安装的时候加上参数 “–with-http_stub_status_module”,就安装了这个模块。

1
./configure --with-http_stub_status_module

3)开启stub_status

1
2
3
4
5
location /nginx_status {
stub_status on;
allow 127.0.0.1; #only allow requests from localhost
deny all; #deny all other hosts
}

二、在kubernetes中运行nginx-prometheus-exporter
nginx-prometheus-exporter 是将 stub_status 指标转换为 Prometheus 指标类型,最终可以由 Prometheus 进行收集。

在Kubernetes里运行

1)创建一个无状态服务(Deployment)

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
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-prometheus
labels:
workload.user.cattle.io/workloadselector: apps.deployment-default-nginx-prometheus
namespace: default
spec:
selector:
matchLabels:
workload.user.cattle.io/workloadselector: apps.deployment-default-nginx-prometheus
template:
metadata:
labels:
workload.user.cattle.io/workloadselector: apps.deployment-default-nginx-prometheus
spec:
containers:
- imagePullPolicy: Always
name: nginx-prometheus
image: nginx/nginx-prometheus-exporter:0.10.0
command:
- nginx-prometheus-exporter
args:
- '-nginx.scrape-uri=http://127.0.0.1/nginx_status'
restartPolicy: Always

2)创建一个Service服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
name: nginx-prometheus
labels:
metrics-prometheus-discovery: 'true' # for monitoring discovery
namespace: default
spec:
ports:
- name: prometheus
port: 9113
protocol: TCP
targetPort: 9113
type: ClusterIP

三、配置kubernetes中的Monitor服务

1)创建一个ServiceMonitor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: nginx-monitor
namespace: default
spec:
selector:
matchLabels:
metrics-prometheus-discovery: 'true' # for monitoring discovery
namespaceSelector:
matchNames:
- default
endpoints:
- port: prometheus
interval: 5s

2)创建KEDA(ScaledObject)缩放规则

Prometheus的集群内访问地址是:http://prometheus-operated.cattle-prometheus.svc:9090

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: nginx-prometheus-scaledobject
namespace: default
spec:
scaleTargetRef:
name: nginx
pollingInterval: 5 # Optional. Default: 30 seconds
cooldownPeriod: 60 # Optional. Default: 300 seconds
minReplicaCount: 1 # Optional. Default: 0
maxReplicaCount: 3 # Optional. Default: 100
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus-operated.cattle-monitoring-system.svc.cluster.local:9090
metricName: nginx_http_requests_total
threshold: '100'
query: sum(rate(nginx_http_requests_total[10s]))

ps:

在Docker中运行

1
docker run -p 9113:9113 nginx/nginx-prometheus-exporter:0.10.0 -nginx.scrape-uri=http://127.0.0.1/nginx_status

运行后,可以通过9113 端口来访问 Prometheus 收集的数据,可以配合Grafana来实现数据可视化。

1
curl http://localhost:9113/metrics

在开发环境及私有环境下需要使用SSL,于是使用openssl创建自签发证书,支持多域名、泛域名、直接IP访问。

一、使用openssl生成证书自签名

openssl在centos中是标配,所以直接在centos中操作,因为要多个域名和IP,故而需要编辑一个配置文件,如下:

1
$ vim req.cnf
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
# 定义输入用户信息选项的"特征名称"字段名,该扩展字段定义了多项用户信息。
distinguished_name = req_distinguished_name
# 生成自签名证书时要使用的证书扩展项字段名,该扩展字段定义了要加入到证书中的一系列扩展项。
x509_extensions = v3_req

# 如果设为no,那么 req 指令将直接从配置文件中读取证书字段的信息,而不提示用户输入。
prompt = no

[req_distinguished_name]
#国家代码,一般都是CN(大写)
C = CN
#省份
ST = Beijing
#城市
L = Beijing
#企业/单位名称
O = phpkoo
#企业部门
OU = phpkoo
#证书的主域名
CN = phpkoo.com

##### 要加入到证书请求中的一系列扩展项 #####
[v3_req]
keyUsage = critical, digitalSignature, keyAgreement
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[ alt_names ]
DNS.1=第一个域名
DNS.2=第二个域名
DNS.N=第N个域名
IP.1=第一个IP
IP.2=第二个IP
IP.N=第N个IP

其中IP配置项,可有可无。

1
2
$ mkdir -p ssl/
$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ./ssl/private.key -out ./ssl/ssl.crt -config ./req.cnf -sha256

至此证书生成完成

可以下载ssl目录下的private.key和ssl.crt文件

openssl 命令参数说明:

req 大致有3个功能:生成证书请求文件、验证证书请求文件和创建根CA。
-x509 说明生成自签名证书。
-nodes openssl req在自动创建私钥时,将总是加密该私钥文件,并提示输入加密的密码。可以使用”-nodes”选项禁止加密私钥文件。
-days 指定所颁发的证书有效期。
-key 指定输入的密钥,如果不指定此选项会根据 -newkey 选项的参数生成密钥对。
-newkey 指定生成一个新的密钥对,只有在没有 -key 选项的时候才生效,参数形式为rsa:numbits或者dsa:file,例如:rsa:2048 rsa表示创建rsa私钥,2048表示私钥的长度。
-keyout 指定私钥保存位置。
-out 新的证书请求文件位置。
-config 指定req的配置文件,指定后将忽略所有的其他配置文件。如果不指定则默认使用/etc/pki/tls/openssl.cnf中req段落的值。

二、证书如何使用

此时访问会出现如下问题

你还得在系统上安装一下证书,安装步骤如下
1、在Windows中导入证书

  • 第一步:双击ssl.crt文件打开证书文件,出现如下界面

  • 第二步:

  • 第三步:

  • 第四步:

  • 第五步:

证书安装,完成

重启浏览器,才能生效

2、测试效果

3、火狐浏览器处理

Firefox默认情况不,还是不信任自签名证书

打开火狐浏览器输入about:config进行设置界面

  • 第一步:

  • 第二步:

输入security.enterprise_roots.enabled修改为true

  • 第三步:

设置完成

重启浏览器,才能生效

在一次和国内某云厂商对接对象存储时,因为他们对象存储服务,是直接使用的是第三方开源服务,开源服务默认并不支持chunked编码(而国内其它大厂一般都是支持chunked编码)。导致上传文件一直报HTTP/1.1 411 Length Required错误,出现问题的原因是说,我没有传Content-Length。

而明明我在header里设置了Content-Length,但是抓包发现,实际却变成了chunked

经过反复的测试情况都依旧,直到在github上找到了这个https://github.com/golang/go/issues/16264

然后查看了golang源码

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
func NewRequest(method, url string, body io.Reader) (*Request, error) {
if method == "" {
// We document that "" means "GET" for Request.Method, and people have
// relied on that from NewRequest, so keep that working.
// We still enforce validMethod for non-empty methods.
method = "GET"
}
if !validMethod(method) {
return nil, fmt.Errorf("net/http: invalid method %q", method)
}
u, err := parseURL(url) // Just url.Parse (url is shadowed for godoc).
if err != nil {
return nil, err
}
rc, ok := body.(io.ReadCloser)
if !ok && body != nil {
rc = ioutil.NopCloser(body)
}
// The host's colon:port should be normalized. See Issue 14836.
u.Host = removeEmptyPort(u.Host)
req := &Request{
Method: method,
URL: u,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(Header),
Body: rc,
Host: u.Host,
}
if body != nil {
switch v := body.(type) {
case *bytes.Buffer:
req.ContentLength = int64(v.Len())
buf := v.Bytes()
req.GetBody = func() (io.ReadCloser, error) {
r := bytes.NewReader(buf)
return ioutil.NopCloser(r), nil
}
case *bytes.Reader:
req.ContentLength = int64(v.Len())
snapshot := *v
req.GetBody = func() (io.ReadCloser, error) {
r := snapshot
return ioutil.NopCloser(&r), nil
}
case *strings.Reader:
req.ContentLength = int64(v.Len())
snapshot := *v
req.GetBody = func() (io.ReadCloser, error) {
r := snapshot
return ioutil.NopCloser(&r), nil
}
default:
// This is where we'd set it to -1 (at least
// if body != NoBody) to mean unknown, but
// that broke people during the Go 1.8 testing
// period. People depend on it being 0 I
// guess. Maybe retry later. See Issue 18117.
}
// For client requests, Request.ContentLength of 0
// means either actually 0, or unknown. The only way
// to explicitly say that the ContentLength is zero is
// to set the Body to nil. But turns out too much code
// depends on NewRequest returning a non-nil Body,
// so we use a well-known ReadCloser variable instead
// and have the http package also treat that sentinel
// variable to mean explicitly zero.
if req.GetBody != nil && req.ContentLength == 0 {
req.Body = NoBody
req.GetBody = func() (io.ReadCloser, error) { return NoBody, nil }
}
}

return req, nil
}

可以看到,这里面居然有个switch,当你使用bytes.Buffer,bytes.Reader或者strings.Reader作为Body的时候,它会自动给你设置req.ContentLength…

所以,问题不是当你Post一个ReadCloser的时候,就会变成chunked,而是你Post非这三种类型的body进来的时候都没有Content-Length,需要自己显式设置。代码如下:

1
2
req, _ := http.NewRequest(method, url, bodyReader)
req.ContentLength = req.Header.Get("Content-Length")

设置了req.ContentLength之后,抓包如下确实不再是chunked编码了,抓包如下

至此问题解决。

lua-mongo 是对 MongoDB C Driver 1.16 或更高版本的 Lua 的绑定,githup仓库:https://github.com/neoxic/lua-mongo

通过luarocks方式安装lua模块

一、安装luarocks工具,官网:https://luarocks.org/
二、安装lua-mongo模块

1
luarocks install lua-mongo

安装时,报以下错误信息

Error: Could not find expected file libmongoc-1.0/mongoc.h, or libmongoc-1.0/mongoc.h for LIBMONGOC – you may have to install LIBMONGOC in your system and/or pass LIBMONGOC_DIR or LIBMONGOC_INCDIR to the luarocks command. Example: luarocks install mongorover LIBMONGOC_DIR=/usr/local

三、安装cmake

因为mongo-c-driver需要使用cmake进行编译

1
2
3
4
5
6
7
yum install gcc gcc-c++ ncurses-devel
wget wget https://cmake.org/files/v3.3/cmake-3.3.2.tar.gz
tar -xzf cmake-3.3.2.tar.gz
cd cmake-3.3.2/
./bootstrap
gmake
gmake install

四、安装libmongoc库

官方安装参照:http://mongoc.org/

1
2
3
4
5
6
wget https://github.com/mongodb/mongo-c-driver/releases/download/1.17.6/mongo-c-driver-1.17.6.tar.gz
tar -xzf mongo-c-driver-1.17.6.tar.gz
cd mongo-c-driver-1.17.6/
cmake .
make
make install

再执行,安装lua-mongo模块

1
luarocks install lua-mongo

安装成功

nginx想支持openresty的功能,需要安装以下模块及依赖luajit2lua-resty-corelua-resty-lrucachelua-nginx-modulengx_devel_kit以下5个依赖包必须先下载,另外如果需要同时支持SSL的话,还需要下载openssl依赖包(ssl只需要下载nginx编译时使用)。

一、安装openresty依赖包
1、安装luajit2(下载最新版本)

1
2
3
4
5
6
7
8
9
wget https://github.com/openresty/luajit2/archive/v2.1-20201229.tar.gz
tar -zxvf luajit2-2.1-20201229.tar.gz
cd luajit2-2.1-20201229
make
make install

#导入环境变量(编译nginx时需要)
export LUAJIT_LIB=/usr/local/lib
export LUAJIT_INC=/usr/local/include/luajit-2.1

2、安装lua-resty-core

1
2
3
4
wget https://github.com/openresty/lua-resty-core/archive/v0.1.21.tar.gz
tar -zxvf lua-resty-core-0.1.21.tar.gz
cd lua-resty-core-0.1.21
make install

3、安装lua-resty-lrucache

1
2
3
4
wget https://github.com/openresty/lua-resty-lrucache/archive/v0.10.tar.gz
tar -zxvf lua-resty-lrucache-0.10.tar.gz
cd lua-resty-lrucache-0.10
make install

4、解压lua-nginx-module

1
2
wget https://github.com/openresty/lua-nginx-module/archive/v0.10.19.tar.gz
tar -zxvf lua-nginx-module-0.10.19.tar.gz

5、解压ngx_devel_kit

1
2
wget https://github.com/vision5/ngx_devel_kit/archive/v0.3.1.tar.gz
tar -zxvf ngx_devel_kit-0.3.1.tar.gz

6、解压openssl

1
2
wget https://www.openssl.org/source/openssl-1.1.1k.tar.gz
tar -zxvf openssl-1.1.1k.tar.gz

二、编译及安装nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
wget http://nginx.org/download/nginx-1.18.0.tar.gz
tar -zxvf nginx-1.18.0.tar.gz

./configure --prefix=/usr/local/nginx \
--with-openssl=/usr/local/src/openssl-1.1.1k \
--with-http_ssl_module \
--with-http_stub_status_module \
--with-http_auth_request_module \
--with-http_gzip_static_module \
--with-threads \
--with-stream \
--with-stream_ssl_module \
--with-stream_ssl_preread_module \
--with-http_slice_module \
--with-compat \
--with-file-aio \
--with-http_v2_module \
--with-ld-opt="-Wl,-rpath,/usr/local/lib" \
--add-module=/usr/local/src/ngx_devel_kit-0.3.1 \
--add-module=/usr/local/src/lua-nginx-module-0.10.19

make && make install

三、nginx配置

1
2
3
4
5
6
#设置openresty模块目录
lua_package_path "/usr/local/lib/lua/?.lua;/usr/local/nginx/conf/lua/?.lua;;";

#指定dns解析服务器,实现动态upstream
resolver 114.114.114.114 223.5.5.5 1.1.1.1 8.8.8.8 valid=30;
resolver_timeout 5;

四、hello openresty(/usr/local/nginx/conf/lua/hello.lua)

1
ngx.print("hello openresty!")