2024年6月

PaddleOCR 是目前最好的开源OCR框架, 但paddle框架的兼容性实在不怎么好, 部署的时候容易出现各种各样的问题. 如果能把PaddleOCR转成ONNX, 就可以跳过paddle框架坑的同时, 又可以白嫖PaddleOCR的强大检测性能.

本文会介绍一下, 如何把最新的PP_OCRv4模型转成onxx格式, 并正确运行.

步骤1: PaddleOCR模型转ONNX

  1. 原始网络权重下载
    在PaddleOCR的项目主页
    https://github.com/PaddlePaddle/PaddleOCR
    下载最新的v4版检测与识别模型.

  2. 模型权重转ONNX

    用pip安装PaddlePaddle到ONNX的模型转换工具:
    https://github.com/PaddlePaddle/Paddle2ONNX
    .

    参照如下命令转换paddle格式模型到ONNX格式.


    paddle2onnx --model_dir /mnt/d/Downloads/202406/paddelocr/pp/ch_PP-OCRv4_det_infer/ --model_filename inference.pdmodel --params_filename inference.pdiparams --opset_version 11 --save_file /mnt/d/Downloads/202406/paddelocr/onnx/ch_PP-OCRv4_det_infer.onnx

    paddle2onnx
    --model_dir /mnt/d/Downloads/202406/paddelocr/pp/ch_PP-OCRv4_rec_infer/ --model_filename inference.pdmodel --params_filename inference.pdiparams --opset_version 11 --save_file /mnt/d/Downloads/202406/paddelocr/onnx/ch_PP-OCRv4_rec_infer.onnx

步骤2: ONNX推理运行

转换格式成功后得到的ONNX模型可以进行推理。若要用于识别任务,还需补充输入图片的预处理和检测结果的输出等外围数据处理步骤。

https://github.com/jingsongliujing/OnnxOCR

该项目基于转换后的ppocr_server_v2.0模型,已经完成了文本识别所需的外围数据处理部分。

经过测试,pp_ocr系列模型的预处理和后处理部分是通用的。只需在此代码基础上集成转换好的ONNX模型即可。

https://github.com/CKboss/pp_onnx

这是基于原项目fork后的修改版,支持运行PP_OCRv4,并能调用v4版本的det和rec OCR模型。旋转检测部分的cls模型保持不变,仍使用server_v2.0版本。此外,还修改了可能导致引用冲突的包名和部分模型输入参数。

注:

转换为ONNX格式后的模型精度损失情况尚不明确,但从简单的demo来看,模型能够正常进行推理。

在Python中,我们可以使用
requests
库来发送HTTP请求,并使用
threading

multiprocessing

asyncio
(配合
aiohttp
)或
concurrent.futures
等库来并发执行这些请求。这里,我将为我们展示使用
concurrent.futures.ThreadPoolExecutor

requests
库并发执行HTTP请求的示例。

1.使用
concurrent.futures.ThreadPoolExecutor
并发发送请求示例

首先,我们需要安装
requests
库(如果还没有安装的话):

bash复制代码

pip install requests

然后,我们可以使用以下代码来并发地发送HTTP GET请求:

import concurrent.futures  
import requests  
  
# 假设我们有一个URL列表  
urls = [  
    'http://example.com/api/data1',  
    'http://example.com/api/data2',  
    'http://example.com/api/data3',  
    # ... 添加更多URL  
]  
  
# 定义一个函数,该函数接收一个URL,发送GET请求,并打印响应内容  
def fetch_data(url):  
    try:  
        response = requests.get(url)  
        response.raise_for_status()  # 如果请求失败(例如,4xx、5xx),则抛出HTTPError异常  
        print(f"URL: {url}, Status Code: {response.status_code}, Content: {response.text[:100]}...")  
    except requests.RequestException as e:  
        print(f"Error fetching {url}: {e}")  
  
# 使用ThreadPoolExecutor并发地执行fetch_data函数  
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:  # 你可以根据需要调整max_workers的值  
    future_to_url = {executor.submit(fetch_data, url): url for url in urls}  
    for future in concurrent.futures.as_completed(future_to_url):  
        url = future_to_url[future]  
        try:  
            # 通过调用future.result()来获取函数的返回值,这会阻塞,直到结果可用  
            # 但是请注意,这里我们只是打印结果,没有返回值,所以调用future.result()只是为了等待函数完成  
            future.result()  
        except Exception as exc:  
            print(f'Generated an exception for {url}: {exc}')

在这里简单解释一下这个代码示例。

(1)我们首先定义了一个URL列表,这些是我们想要并发访问的URL。

(2)然后,我们定义了一个函数
fetch_data
,它接收一个URL作为参数,发送GET请求,并打印响应的状态码和内容(只打印前100个字符以节省空间)。如果发生任何请求异常(例如,网络错误、无效的URL、服务器错误等),它会捕获这些异常并打印错误消息。

(3)使用
concurrent.futures.ThreadPoolExecutor
,我们可以轻松地并发执行
fetch_data
函数。我们创建了一个
ThreadPoolExecutor
实例,并指定了最大工作线程数(在这个例子中是5,但我们可以根据需要调整这个值)。然后,我们使用列表推导式将每个URL与一个
Future
对象关联起来,该对象表示异步执行的函数。

(4)最后,我们使用
as_completed
函数迭代所有完成的
Future
对象。对于每个完成的
Future
对象,我们调用
result
方法来获取函数的返回值(尽管在这个例子中我们没有使用返回值)。如果函数执行期间发生任何异常,
result
方法会重新引发该异常,我们可以捕获并处理它。

这个示例展示了如何使用Python的
concurrent.futures
模块来并发地发送HTTP请求。这种方法在IO密集型任务(如网络请求)上特别有效,因为它允许在等待IO操作完成时释放CPU资源供其他线程使用。

2.requests库并发发送HTTP GET请求的完整Python代码示例

以下是一个使用
concurrent.futures.ThreadPoolExecutor

requests
库并发发送HTTP GET请求的完整Python代码示例:

import concurrent.futures  
import requests  
  
# 假设我们有一个URL列表  
urls = [  
    'https://www.example.com',  
    'https://httpbin.org/get',  
    'https://api.example.com/some/endpoint',  
    # ... 添加更多URL  
]  
  
# 定义一个函数来发送GET请求并处理响应  
def fetch_url(url):  
    try:  
        response = requests.get(url, timeout=5)  # 设置超时为5秒  
        response.raise_for_status()  # 如果请求失败,抛出HTTPError异常  
        return response.text  # 返回响应内容,这里只是作为示例,实际使用中可能不需要返回  
    except requests.RequestException as e:  
        print(f"Error fetching {url}: {e}")  
        return None  
  
# 使用ThreadPoolExecutor并发地发送请求  
def fetch_all_urls(urls):  
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:  
        # 使用executor.map来自动处理迭代和Future的获取  
        results = executor.map(fetch_url, urls)  
  
    # 处理结果(这里只是简单地打印出来)  
    for result in results:  
        if result is not None:  
            print(f"Fetched content from a URL (truncated): {result[:100]}...")  
  
# 调用函数  
fetch_all_urls(urls)

在这个示例中,我们定义了一个
fetch_url
函数,它接收一个URL,发送GET请求,并返回响应内容(或在出错时返回
None
)。然后,我们定义了一个
fetch_all_urls
函数,它使用
ThreadPoolExecutor
并发地调用
fetch_url
函数,并将结果收集在一个迭代器中。最后,我们遍历这个迭代器,并打印出每个成功获取到的响应内容(这里只打印了前100个字符作为示例)。

请注意,我们在
requests.get
中设置了一个超时参数(
timeout=5
),这是为了防止某个请求因为网络问题或其他原因而无限期地等待。在实际应用中,根据我们的需求调整这个值是很重要的。

此外,我们还使用了
executor.map
来自动处理迭代和
Future
的获取。
executor.map
函数会返回一个迭代器,它会产生
fetch_url
函数的返回值,这些值在函数完成后会自动从相应的
Future
对象中提取出来。这使得代码更加简洁,并且减少了显式处理
Future
对象的需要。

3.如何在Python中实现并发编程

在Python中实现并发编程,主要有以下几种方式:

(1)
使用
threading
模块

threading
模块提供了多线程编程的API。Python的线程是全局解释器锁(GIL)下的线程,这意味着在任意时刻只有一个线程能够执行Python字节码。然而,对于I/O密集型任务(如网络请求),多线程仍然可以通过并发地等待I/O操作来提高性能。

示例:

import threading  
import requests  
 
def fetch_url(url):  
    try:  
        response = requests.get(url)  
        response.raise_for_status()  
        print(f"URL: {url}, Status Code: {response.status_code}")  
    except requests.RequestException as e:  
        print(f"Error fetching {url}: {e}")  
 
threads = []  
for url in urls:  
    t = threading.Thread(target=fetch_url, args=(url,))  
    threads.append(t)  
    t.start()  
 
# 等待所有线程完成  
for t in threads:  
    t.join()

(2)
使用
multiprocessing
模块

multiprocessing
模块提供了跨多个Python解释器的进程间并行处理。这对于CPU密集型任务特别有用,因为每个进程都有自己的Python解释器和GIL,可以充分利用多核CPU的并行处理能力。

示例:

from multiprocessing import Pool  
import requests  
 
def fetch_url(url):  
    try:  
        response = requests.get(url)  
        response.raise_for_status()  
        return f"URL: {url}, Status Code: {response.status_code}"  
    except requests.RequestException as e:  
        return f"Error fetching {url}: {e}"  
 
with Pool(processes=4) as pool:  # 设定进程池的大小  
    results = pool.map(fetch_url, urls)  
 
for result in results:  
    print(result)

(3)
使用
asyncio
模块(针对异步I/O)

asyncio
是Python 3.4+中引入的用于编写单线程并发代码的库,特别适合编写网络客户端和服务器。它使用协程(coroutine)和事件循环(event loop)来管理并发。

示例(使用
aiohttp
库进行异步HTTP请求):

import asyncio  
import aiohttp  
 
async def fetch_url(url, session):  
    async with session.get(url) as response:  
        return await response.text()  
 
async def main():  
    async with aiohttp.ClientSession() as session:  
        tasks = []  
        for url in urls:  
            task = asyncio.create_task(fetch_url(url, session))  
            tasks.append(task)  
 
        results = await asyncio.gather(*tasks)  
        for result, url in zip(results, urls):  
            print(f"URL: {url}, Content: {result[:100]}...")  
 
# Python 3.7+ 可以使用下面的方式运行主协程  
asyncio.run(main())

注意:
asyncio.run()
是在Python 3.7中引入的,用于运行顶层入口点函数。在Python 3.6及以下版本中,我们需要自己设置和运行事件循环。

(4)
使用
concurrent.futures
模块

concurrent.futures
模块提供了高层次的接口,可以轻松地编写并发代码。它提供了
ThreadPoolExecutor
(用于线程池)和
ProcessPoolExecutor
(用于进程池)。

前面已经给出了
ThreadPoolExecutor
的示例,这里不再重复。
ProcessPoolExecutor
的用法与
ThreadPoolExecutor
类似,只是它是基于进程的。

选择哪种并发方式取决于我们的具体需求。对于I/O密集型任务,多线程或异步I/O通常是更好的选择;对于CPU密集型任务,多进程可能是更好的选择。此外,异步I/O通常比多线程具有更好的性能,特别是在高并发的网络应用中。

背景

前面写了一篇,k8s集群搭建及对一些组件的简单理解(一),主要讲了下背景和对一些组件的理解。

今天讲一下正式的安装,有网环境的,后续再说下无外网环境纯内网的。

k8s集群节点、组件

控制面节点,一般就是部署了如下组件:etcd、apiserver、kube-scheduler、kube-controller-manager,由于这些组件都是面向用户,通过kubectl或者UI来接收用户的请求,并对用户请求做出响应。

这些组件收到请求后,开始处理请求,如用户希望运行pod,则这些组件会寻找某个合适的node来部署pod,因此,实际运行用户pod的节点,一般认为是数据面节点。

这些概念可参考:
https://kubernetes.io/docs/concepts/overview/components/

控制面中的组件,理论上是可以分布在任意node,但是安装脚本一般选择将这些组件安装在同一台机器上,并且不在这些机器上运行用户的pod;为了高可用,还会在多台机器上来部署这些控制面组件,因此,我们可以把这些机器叫做控制面节点。

image-20240623115937949

上图可以看出,控制面节点中只包含了api-server/etcd/kube-scheduler等组件,实际上,控制面并没有强制说一定不能运行用户pod,而且,在我们使用kubeadm搭建集群时,一般控制面节点会同时包含控制面组件和数据面节点的组件(kubelet、kube-proxy、容器运行时等)。

下面是我整理的图,利用kubeadm搭建完控制面节点后,里面包含了:

  • 6个pod(4个绿色部分的:etcd、api-server、scheduler、control-manager;2个灰色部分:coredns、kube-proxy),这些pod可以理解为6个后台进程

  • 3个由systemctl管理的service:

    /usr/lib/systemd/system/kubelet.service

    /usr/lib/systemd/system/cri-docker.service

    /usr/lib/systemd/system/docker.service

    这3个service,也可以理解为3个后台进程,只是不是pod容器方式运行的;

    这3个service,也变相地提供了3个二进制文件给我们用:kubelet、cri-docker、dokcer

  • 2个cli:kubectl和kubeadm,这两个不是后台进程,只是单纯的cli客户端

k8s控制面节点组件

第一次装的时候,面对这近10个进程、几个cli,几个cli名字还像,真是有点晕。搞了几次后,稍微清晰点了,下面就正式开始安装的部署。

我们的机器配置:

virtualbox搞了两个干净的虚拟机,里面啥都没安装,但是有外网。

10.0.2.8 node4,准备作为主节点

10.0.2.9 node5,准备作为工作节点

操作系统都是centos 7.9。

容器运行时

参考文档:

https://kubernetes.io/docs/setup/production-environment/container-runtimes/

这里选择docker,这个个人用习惯了,空了再学其他的吧。

前置条件

  • ip转发

文档里提到,linux默认未开启ip转发,而k8s的很多网络插件都需要这个特性,所以需要打开。

By default, the Linux kernel does not allow IPv4 packets to be routed between interfaces. Most Kubernetes cluster networking implementations will change this setting (if needed)

# sysctl params required by setup, params persist across reboots
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.ipv4.ip_forward = 1
EOF

# Apply sysctl params without reboot
sudo sysctl --system

检查是否已经设为1:
sysctl net.ipv4.ip_forward

  • cgroup drivers

https://kubernetes.io/docs/setup/production-environment/container-runtimes/

在linux中,control group用于对进程可以使用的资源进行限制。而kubelet和容器运行时,都需要和control group打交道,对pod和容器的资源进行管理、对其可以使用的cpu、内存等资源进行限制。要和control group打交道,也不是直接打交道,而是要通过一个control group driver,感觉可以理解为驱动。很重要的一点是,容器运行时和kubelet,需要保证使用同样的cgroup driver。

目前有两种cgroup driver:cgroupfs、systemd。

cgroupfs是kubelet中默认的driver,fs表示文件系统file system的意思,它是 cgroup 的文件系统接口,用户可以操作对应的文件来进行资源控制。

默认的目录为:/sys/fs/cgroup。我们系统下:

[root@node4 ~]# ll /sys/fs/cgroup
total 0
drwxr-xr-x. 2 root root  0 Jun 23 12:53 blkio
lrwxrwxrwx. 1 root root 11 Jun 23 12:53 cpu -> cpu,cpuacct
lrwxrwxrwx. 1 root root 11 Jun 23 12:53 cpuacct -> cpu,cpuacct
drwxr-xr-x. 2 root root  0 Jun 23 12:53 cpu,cpuacct
drwxr-xr-x. 2 root root  0 Jun 23 12:53 cpuset
drwxr-xr-x. 4 root root  0 Jun 23 12:53 devices
drwxr-xr-x. 2 root root  0 Jun 23 12:53 freezer
drwxr-xr-x. 2 root root  0 Jun 23 12:53 hugetlb
drwxr-xr-x. 2 root root  0 Jun 23 12:53 memory
lrwxrwxrwx. 1 root root 16 Jun 23 12:53 net_cls -> net_cls,net_prio
drwxr-xr-x. 2 root root  0 Jun 23 12:53 net_cls,net_prio
lrwxrwxrwx. 1 root root 16 Jun 23 12:53 net_prio -> net_cls,net_prio
drwxr-xr-x. 2 root root  0 Jun 23 12:53 perf_event
drwxr-xr-x. 2 root root  0 Jun 23 12:53 pids
drwxr-xr-x. 4 root root  0 Jun 23 12:53 systemd

总之,简单理解,这种driver是直接操作这个目录下的文件来和内核中的control group交互,比如可以在这边创建一个group,然后把某些进程的pid加入,这样,这些pid就会应用这个group的资源限制配置。

但是,在systemd管理的系统中(比如centos 7基本就是systemd管理的),就不推荐使用这种cgroupfs驱动了。

因为,在这种系统中,systemd会为每一个unit配置一个control group,比如:

https://serverfault.com/questions/683911/use-of-cpuquota-in-systemd

[Unit]
Description=Virtual Distributed Ethernet

[Service]
ExecStart=/usr/bin/ddcommand
CPUQuota=10%

[Install]
WantedBy=multi-user.target

这里面就通过CPUQuota限制了可以使用的cpu为10%。

关于systemd的资源限制这块,可以查看:
man systemd.resource-control

因此,在这种systemd系统中,就得统一都使用systemd这种cgroup driver。

后面再说说具体怎么设置。

docker 安装

我们准备采用yum安装,这种安装之后,rpm包就找不到了,由于我们需要把rpm包保存下来供后面搭建内网k8s,所以我们就先把rpm包下载下来再手动安装。

参考文档:
https://docs.docker.com/engine/install/centos/

在yum安装前,得先安装docker的yum仓库,否则,你直接yum安装的话,可能版本会非常老。比如,在centos 7.9中:

[root@node4 ~]# yum info docker
Available Packages
Name        : docker
Arch        : x86_64
Epoch       : 2
Version     : 1.13.1
Release     : 210.git7d71120.el7.centos
Size        : 17 M
Repo        : extras/7/x86_64

这个就是1.13.1,该版本是2017年的:
https://docs.docker.com/engine/release-notes/prior-releases/

后来改成了17.03这样的格式(一开始说是YY.MM格式,现在也不是很遵从了),最近几年的几个版本是23.0/24.0/25.0/26.0,目前最新是26.1。

所以,要安装新版本,先弄一下docker的yum仓库。

官方是这个,但是被q了:
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

那就搞个镜像,如果大家使用云服务器的话,可以找找自己云厂商提供的docker镜像,有的是只给自己的云服务器用的,这种一般速度比较快:
wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

下载rpm包:

[root@node4 ~]# mkdir /root/docker-package
[root@node4 ~]# cd /root/docker-package/
[root@node4 docker-package]# yumdownloader --resolve --destdir=. docker-ce docker-ce-cli containerd.io

我们没安装docker-buildx-plugin docker-compose-plugin,暂时感觉用不上。

下载完成后,查看:
[root@node4 docker-package]# ll
total 118488
-rw-r--r--. 1 root root    78256 Aug 23  2019 audit-libs-python-2.8.5-4.el7.x86_64.rpm
-rw-r--r--. 1 root root   302068 Nov 12  2018 checkpolicy-2.5-8.el7.x86_64.rpm
-rw-r--r--. 1 root root 37045876 Jun 21 18:38 containerd.io-1.6.33-3.1.el7.x86_64.rpm
...

使用rpm包安装:

rpm -ivh *.rpm

安装完成后,会有多个systemd管理的unit:

cd /usr/lib/systemd/system
ll 
-rw-r--r--  1 root root 1264 Jun  5 16:36 containerd.service
-rw-r--r--  1 root root  295 Jun  5 19:31 docker.socket
-rw-r--r--  1 root root 1962 Jun  5 19:31 docker.service

然后就是设置下docker的配置文件(没有就新建):

[root@app1 ~]# vim /etc/docker/daemon.json
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "debug": true
}

如果需要配私服、镜像的,也基本就是改这个文件,比如增加如下行,我就先不加了:

  "registry-mirrors": ["http://10.0.218.xxx:8083"],
  "insecure-registries": ["http://10.0.218.xxx:8083"]

启动服务:

systemctl start docker
systemctl status docker

开机启动:
systemctl enable docker

检查是否cgroup driver为systemd:
[root@app1 cri-docker]# docker info|grep group
 Cgroup Driver: systemd
 Cgroup Version: 1

检查下能拉不:

[root@node4 docker-package]# docker pull hello-world
Using default tag: latest
latest: Pulling from library/hello-world
c1ec31eb5944: Retrying in 1 second 
error pulling image configuration: download failed after attempts=6: dial tcp 103.252.114.61:443: i/o timeout

拉不下来,也可以查看下日志:
journalctl -u docker 
实时查看:
journalctl -u docker -f 

嗯,不能。。。因为docker pull的时候要去访问一些网站
https://production.cloudflare.docker.com
,被q了。

那如果不学上网技术,就只能使用docker.io这个官方仓库的国内镜像仓库了。

我一开始看到网上总结的几个镜像站:

docker.io mirror.ccs.tencentyun.com 仅腾讯云vpc内部访问, registry2 proxy
docker.nju.edu.cn 南京大学开源镜像站, nexus3
docker.mirrors.sjtug.sjtu.edu.cn 上海交通大学, registry2 proxy
docker.m.daocloud.io 国内可用, 带宽低
*****.mirror.aliyuncs.com 国内可用,更新慢

除了腾讯是内部用(云服务器上可以用),其他的,除了倒数第二个,其他的几个全用不了了。(当前时间20240623)。

docker pull docker.m.daocloud.io/hello-world
 
[root@node4 docker-package]# docker images
REPOSITORY                         TAG       IMAGE ID       CREATED         SIZE
docker.m.daocloud.io/hello-world   latest    d2c94e258dcb   13 months ago   13.3kB

另外,可以关注下docker对外部提供的接口,为domain unix socket:

[root@node4 docker-package]# netstat -nlp |grep docker
unix  2      [ ACC ]     STREAM     LISTENING     40562    1/systemd            /run/docker.sock

cri-docker 安装

https://mirantis.github.io/cri-dockerd/

cri-docker是docker公司和Mirantis 公司(docker商业化公司)来维护,实现了k8s的CRI接口,将CRI接口进行适配,转换为对docker的调用并响应。

安装的话,最简单就是安装rpm包 ,
https://github.com/Mirantis/cri-dockerd/releases这里下载即可。

我这边下载的是cri-dockerd-0.3.14-3.el7.x86_64.rpm。

cd /root/upload
上传rpm
rpm -ivh cri-dockerd-0.3.14-3.el7.x86_64.rpm 

安装完成后,会有如下service:

/usr/lib/systemd/system/cri-docker.service

查看内容,主要命令就是:
ExecStart=/usr/bin/cri-dockerd --container-runtime-endpoint fd://

查看帮助:

/usr/bin/cri-dockerd -h
这个里面选项不少,要多看看。

比如这里就可以指定docker的socket的地址:
--docker-endpoint string  Use this for the docker endpoint to communicate with. 
(default "unix:///var/run/docker.sock")

还可以指定pod中的基础容器的镜像坐标:
--pod-infra-container-image string        The image whose network/ipc namespaces containers in each pod will use (default "registry.k8s.io/pause:3.9")

启动:

[root@node4 upload]# systemctl start cri-docker
[root@node4 upload]# systemctl status cri-docker

它监听的端口为/run/cri-dockerd.sock或者/var/run/cri-dockerd.sock,这两个指向同一个文件:

[root@node4 upload]# netstat -nlxp|grep cri
unix  2      [ ACC ]     STREAM     LISTENING     68693    1/systemd            /run/cri-dockerd.sock

日志查看:

journalctl -u cri-docker

kubelet安装

https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/

前置

https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#before-you-begin

关闭SeLinux:

sudo setenforce 0

sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config

关闭swap:

sudo vi /etc/fstab
注释掉如下行:
# /dev/mapper/centos-swap swap                    swap    defaults        0 0

[root@app1 ~]# sudo swapoff -a
检查:
[root@app1 ~]# free -h

安装kubelet

cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.30/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.30/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF

然后,首先安装kubelet:

rpm安装:
mkdir /root/kubelet-kubeadm
cd /root/kubelet-kubeadm
yumdownloader --resolve --destdir=. kubelet --disableexcludes=kubernetes
rpm -ivh *.rpm

如果直接装就是:
yum install  kubelet --disableexcludes=kubernetes

可以观察到,其依赖于:kubernetes-cni这个包。

/usr/lib/systemd/system/kubelet.service

systemctl status kubelet

cd /usr/lib/systemd/system/

[root@node4 system]# ll kubelet*
-rw-r--r--. 1 root root 278 Jun 12 05:15 kubelet.service

我们如果此时启动,大家看看效果:

systemctl start kubelet
systemctl enable kubelet
systemctl status kubelet

是启动失败的。
查看日志:
journalctl -u kubelet

image-20240623152551701

可以发现,此时进入的是standalone模式,这个模式下,kubelet可以独立管理pod,不需要api-server的存在,具体大家可以搜一下。

安装kubeadm

rpm安装:
cd /root/kubelet-kubeadm
yumdownloader --resolve --destdir=. kubeadm --disableexcludes=kubernetes
rpm -ivh *.rpm

如果直接装就是:
yum install  kubeadm --disableexcludes=kubernetes
发现其会依赖cri-tools

对kubelet的影响

安装完成后,我们再去看kubelet的service目录下:

cd /usr/lib/systemd/system/


[root@node4 system]# ll kubelet*
-rw-r--r--. 1 root root 278 Jun 12 05:15 kubelet.service

kubelet.service.d:
total 4
-rw-r--r--. 1 root root 900 Jun 12 05:14 10-kubeadm.conf

发现多了个目录,目录下还有配置文件10-kubeadm.conf. 这个配置文件其实就是给kubelet指定了些配置,比如后续怎么去连接api-server(就不在是standalone模式了)、以及指定了一些kubelet自身的配置项

image-20240623153350228

另外,也可以查看下kubelet的选项,非常多:

[root@node4 system]# /usr/bin/kubelet -h

kubeadm的流程

此时,其实就可以准备创建集群了。语法很简单:

kubeadm init    --pod-network-cidr=192.168.0.0/16 --cri-socket unix:///var/run/cri-dockerd.sock --kubernetes-version v1.30.1 --v=5

具体选项可以看:

 kubeadm init -h

我们指定的选项的意思:

--pod-network-cidr,这个是部分cni插件需要指定,我们选的是calico,去calico官网查看文档,发现其默认需要这么指定:192.168.0.0/16,意思就是pod的ip的网段

--cri-socket,这个是因为我们机器上,既有cri-docker,又有containerd,两个cri实现,必须手动指定一个。

--kubernetes-version v1.30.1,联网环境下可以不指定,就是指定k8s的版本;离线环境下指定了就不用去联网发请求

--v,日志级别

但,实际这样还不够,k8s的控制面组件不是有好几个pod吗,pod内容器的镜像,是维护在registry.k8s.io这个仓库下的。

我们直接去拉取镜像会失败:

[root@node4 system]#  docker pull registry.k8s.io/kube-apiserver:v1.30.1
Error response from daemon: Head "https://asia-east1-docker.pkg.dev/v2/k8s-artifacts-prod/images/kube-apiserver/manifests/v1.30.1": dial tcp: lookup asia-east1-docker.pkg.dev on 10.0.2.1:53: no such host

这些网址是由Google团队提供的,被q了。

所以,我们只能找找registry.k8s.io的镜像仓库,我这边用的registry-k8s-io.mirrors.sjtug.sjtu.edu.cn。

因此,最后命令是:

kubeadm init --image-repository registry-k8s-io.mirrors.sjtug.sjtu.edu.cn   --pod-network-cidr=192.168.0.0/16 --cri-socket unix:///var/run/cri-dockerd.sock --kubernetes-version v1.30.1 --v=5 

另外,我们加上--dry-run,先试着跑一下,但是,我建议,还是先提前拉取好镜像,再来跑这个好一点:

kubeadm init --image-repository registry-k8s-io.mirrors.sjtug.sjtu.edu.cn   --pod-network-cidr=192.168.0.0/16 --cri-socket unix:///var/run/cri-dockerd.sock --kubernetes-version v1.30.1 --v=5 --dry-run

为啥呢,因为k8s这几个组件的镜像大小有大几百兆,所以界面会一直卡在这里,不知道后台到底怎么样了,是卡死了还是怎么了。

我们如果要提前拉取镜像,先得知道要拉取哪些镜像:

[root@node4 system]# kubeadm config images list
registry.k8s.io/kube-apiserver:v1.30.2
registry.k8s.io/kube-controller-manager:v1.30.2
registry.k8s.io/kube-scheduler:v1.30.2
registry.k8s.io/kube-proxy:v1.30.2
registry.k8s.io/coredns/coredns:v1.11.1
registry.k8s.io/pause:3.9
registry.k8s.io/etcd:3.5.12-0

然后,转换为:

docker pull registry-k8s-io.mirrors.sjtug.sjtu.edu.cn/kube-apiserver:v1.30.2
docker pull registry-k8s-io.mirrors.sjtug.sjtu.edu.cn/kube-controller-manager:v1.30.2
docker pull registry-k8s-io.mirrors.sjtug.sjtu.edu.cn/kube-scheduler:v1.30.2
docker pull registry-k8s-io.mirrors.sjtug.sjtu.edu.cn/kube-proxy:v1.30.2
docker pull registry-k8s-io.mirrors.sjtug.sjtu.edu.cn/coredns/coredns:v1.11.1
docker pull registry-k8s-io.mirrors.sjtug.sjtu.edu.cn/pause:3.9
docker pull registry-k8s-io.mirrors.sjtug.sjtu.edu.cn/etcd:3.5.12-0


docker images,确保镜像都存在了,再次运行:

发现会卡在一个拉取coreDns的地方,

pulling: registry-k8s-io.mirrors.sjtug.sjtu.edu.cn/coredns:v1.11.1

这个主要是两个仓库的标签打得有点不一样,我们就把我们已经存在的镜像,重新打个tag:

docker tag registry-k8s-io.mirrors.sjtug.sjtu.edu.cn/coredns/coredns:v1.11.1 registry-k8s-io.mirrors.sjtug.sjtu.edu.cn/coredns:v1.11.1 

结果再次运行,发现还是卡:

[api-check] Waiting for a healthy API server. This can take up to 4m0s

检查了几个服务的日志,发现docker日志显示还在拉取:



...
registry.k8s.io/pause from https://registry.k8s.io

后面查了下,是因为就是我们忽略了一句kubeadm打印的提示:

W0623 16:26:12.928077   22235 checks.go:844] detected that the sandbox image "registry.k8s.io/pause:3.9" of the container runtime is inconsistent with that used by kubeadm.It is recommended to use "registry-k8s-io.mirrors.sjtug.sjtu.edu.cn/pause:3.9" as the CRI sandbox image.

我们在cri-docker那里,需要修改下镜像坐标。

ExecStart=/usr/bin/cri-dockerd --container-runtime-endpoint fd:// --pod-infra-container-image registry-k8s-io.mirrors.sjtug.sjtu.edu.cn/pause:3.9
systemctl daemon-reload
systemctl restart cri-docker

再次执行,又说一堆东西已经存在了。我们可以先reset下:

 kubeadm reset -f --cri-socket unix:///var/run/cri-dockerd.sock 

这次基本就能成功了,把最下面的命令存下来:

Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

Alternatively, if you are the root user, you can run:

  export KUBECONFIG=/etc/kubernetes/admin.conf

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 10.0.2.x:6443 --token k3nmtk.kx1k6cbxbsuaqysd \
        --discovery-token-ca-cert-hash sha256:9659ceb5bf342b1a9fa1ef1888a3ce26a1f9c881dbbf4bbadcbb62d5dcde37dd 

安装kubectl

这个东西是啥呢,只是个cli,客户端工具,给我们用的,就像redis-cli和mysql命令行客户端一样,主要是和控制面组件中的api-server进行交互。

事实上,各个容器厂商都是做一套界面出来给用户用的,也是直接对接api-server。

这里我们安装下。

rpm安装:
cd /root/kubelet-kubeadm
yumdownloader --resolve --destdir=. kubectl --disableexcludes=kubernetes
rpm -ivh *.rpm

如果直接装就是:
yum install  kubectl --disableexcludes=kubernetes

然后执行下:

[root@node4 system]#  kubectl get pods -A
E0623 16:40:18.035470   25920 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp [::1]:8080: connect: connection refused

因为还需要指定api-server的地址:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

再来:

[root@node4 system]#  kubectl get pods -A
NAMESPACE     NAME                            READY   STATUS    RESTARTS   AGE
kube-system   coredns-86c4446b65-6sr2p        0/1     Pending   0          7m13s
kube-system   coredns-86c4446b65-n4xcl        0/1     Pending   0          7m13s
kube-system   etcd-node4                      1/1     Running   0          7m28s
kube-system   kube-apiserver-node4            1/1     Running   0          7m28s
kube-system   kube-controller-manager-node4   1/1     Running   0          7m28s
kube-system   kube-proxy-rxxc9                1/1     Running   0          7m13s
kube-system   kube-scheduler-node4            1/1     Running   0          7m28s

可以发现,coreDns这两个pod是pending状态。看一下这个pod的状态:发现是说没有可用的ready的node。

[root@node4 system]# kubectl get nodes -A
NAME    STATUS     ROLES           AGE   VERSION
node4   NotReady   control-plane   13m   v1.30.2

我们的node确实是NotReady。

[root@node4 system]# kubectl describe node node4
...
  Ready            False   Sun, 23 Jun 2024 16:43:43 +0800   Sun, 23 Jun 2024 16:33:18 +0800   KubeletNotReady              container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized

里面可以看到,是因为cni网络插件尚未ready的原因。

安装网络插件calico

网络插件有好多种,主流用的也有好几种,各种的差别后面我们再讲,其中呢,calico算是一款优秀的网络插件。

https://docs.tigera.io/calico/latest/getting-started/kubernetes/quickstart

安装operator

wget https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/tigera-operator.yaml

执行:
k create -f tigera-operator.yaml
执行完后,会新增一个pod:
[root@node4 calico-install]# kubectl get pods -A |grep tigera
tigera-operator   tigera-operator-5ddc799ffd-fqsps   0/1     ContainerCreating   0          49s

这里又涉及到拉取镜像,这次的镜像是在:

[root@node4 calico-install]# grep image tigera-operator.yaml |grep quay
          image: quay.io/tigera/operator:v1.34.0

还好,这个quay.io/tigera/operator:v1.34.0直接网络可以拉取。。不需要镜像

运行网络插件相关pod

https://docs.tigera.io/calico/latest/getting-started/kubernetes/quickstart

wget https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/custom-resources.yaml

// 这个动作又要拉取镜像,有大几百兆
kubectl create -f custom-resources.yaml

我在我以前成功的机器上看了下,拉取了这些(版本不一定和最新一样):

[root@app1 ~]# docker images |grep calico
calico/typha                                                        v3.28.0    a9372c0f51b5       71.1MB
calico/kube-controllers                                             v3.28.0    428d92b02253       79.1MB
calico/apiserver                                                    v3.28.0    6c07591fd1cf       97.9MB
calico/cni                                                          v3.28.0    107014d9f4c8       209MB
calico/node-driver-registrar                                        v3.28.0    0f80feca743f       23.5MB
calico/csi                                                          v3.28.0    1a094aeaf152       18.3MB
calico/pod2daemon-flexvol                                           v3.28.0    587b28ecfc62       13.4MB
calico/node                                                         v3.28.0    4e42b6f329bc       353MB

我看了下docker日志,发现又跑去docker.io这个官方仓库拉取calico镜像去了,这尼玛。还是得配个镜像仓库啊。

vim /etc/docker/daemon.json
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "debug": true,
  "registry-mirrors": ["https://docker.m.daocloud.io"]
}

只要镜像下下来了,就没问题了,等一会,pod就会就绪,coreDns也会就绪:

image-20240623173939648

worker节点安装

10.0.2.9 node5

容器运行时

参照主节点,全都需要执行。

rpm包我们scp拷贝过去。

scp * 10.0.2.9:/root/docker-package
scp  /etc/docker/daemon.json  10.0.2.9:/etc/docker/daemon.json
systemctl start docker
systemctl status docker
systemctl enable docker

cri-docker安装

参照主节点。

记得修改service文件:

/usr/lib/systemd/system/cri-docker.service
加参数:
--pod-infra-container-image registry-k8s-io.mirrors.sjtug.sjtu.edu.cn/pause:3.9

systemctl start cri-docker
systemctl status cri-docker
systemctl enable cri-docker

kubelet安装

直接装,或者拿主节点的rpm来装:
yum install  kubelet --disableexcludes=kubernetes
systemctl start kubelet
systemctl enable kubelet

kubeadm安装

直接装,或者拿主节点的rpm来装:
yum install  kubeadm --disableexcludes=kubernetes

拷贝并加载镜像

从主节点中,执行:

[root@node4 docker-package]# docker save -o calico_images.tar $(docker images --format "{{.Repository}}:{{.Tag}}" | grep "^calico")
[root@node4 docker-package]# docker save -o pause.tar $(docker images --format "{{.Repository}}:{{.Tag}}" | grep "pause")
[root@node4 docker-package]# docker save -o kube-proxy.tar $(docker images --format "{{.Repository}}:{{.Tag}}" | grep "kube-proxy")
[root@node4 docker-package]# scp calico_images.tar pause.tar kube-proxy.tar 10.0.2.9:/root/images-for-load

在worker节点上:

cd /root/images-for-load

docker load < kube-proxy.tar 
docker load < calico_images.tar 
docker load < pause.tar 
docker images

join集群

找出之前存下来的join语句:

 kubeadm join 10.0.2.8:6443 --token k3nmtk.kx1k6cbxbsuaqysd \
        --discovery-token-ca-cert-hash sha256:9659ceb5bf342b1a9fa1ef1888a3ce26a1f9c881dbbf4bbadcbb62d5dcde37dd         --cri-socket unix:///var/run/cri-dockerd.sock --v=5

image-20240623182820091

注意,worker节点不能执行kubectl是正常的,因为一般是在控制节点上执行kubectl。当然,如果有需要,也可以像主节点那样操作,弄一下就行了。

测试

我这边在主节点上:

kubectl create deploy www-nginx-demo --image nginx --replicas=2		

如果发现调度到了两台机器上,等nginx镜像拉取完成后(在主、工作节点都要拉取),可以看到pod状态变成正常。

image-20240623183525165

在主节点上,直接访问pod:

image-20240623183608453

在工作节点上,直接访问pod:

image-20240623183644084

容器到容器的网络也是通的,只是容器内太简单了,缺少ping这些基础的工具,以后再说说这块。

总结

后续再讲下完全没有网络的环境中的安装吧,也算是给自己备忘了。

大家好,我是狂师。

今天给大家介绍一款行为驱动开发测试框架:
Cucumber

1、介绍

Cucumber是一个行为驱动开发(BDD)工具,它结合了文本描述和自动化测试脚本。它使用一种名为Gherkin的特定语言来描述应用程序的行为,这种语言非常接近自然语言,使得非技术人员也能够理解和参与测试。

知识扩展:

Gherkin语言是一种用于描述业务行为的领域特定语言(Domain Specific Language, DSL),它允许用户不关注具体实现细节地描述软件系统需要执行的操作。这种语言具有类似于自然语言的易读性,使其成为业务人员和开发人员在编写自动化测试用例时的理想选择。Gherkin特别适用于Behavior Driven Development(BDD)方法,因为它能够将业务需求转换为清晰、易于理解和维护的测试步骤。

Gherkin它使用一组特殊的关键字来构建结构化和有意义的测试步骤。它的设计是为了描述而非直接执行,但它与Cucumber工具相结合,从而实现自动化的测试过程,它旨在让不同背景的人(如业务人员、开发人员和测试人员)都能够通过同一文档理解需求并达成共识。
一个典型的Gherkin测试脚本由多个"steps"组成,每个步骤代表一个最小的测试单元。这些步骤可以组合成"Scenarios",进而构成"Features"。Feature文件通常以"Feature:"开头,而每个步骤则包含一系列的条件语句(如"Given"、"When"和"Then"),以及可能的其他关键字。

2、优缺点、适用场景

总的来说,Cucumber是一个强大的BDD工具,适用于需要与业务人员紧密合作的项目,可以促进团队协作,减少测试脚本的维护成本。然而,需要权衡其学习成本和执行速度。

适用场景:

  1. 针对需要与业务人员紧密合作的项目,Cucumber可以帮助编写易于理解的测试用例,促进开发人员、测试人员和业务人员之间的沟通和协作。
  2. 对于需要频繁更新和变更的项目,Cucumber的特性可以减少测试脚本的维护成本,因为测试用例是用自然语言编写的,不需要频繁修改。
  3. 适用于Web应用程序、移动应用程序和API的自动化测试。

优点:

  1. 促进团队协作:Cucumber测试用例使用自然语言编写,使得开发人员、测试人员和业务人员可以更好地理解和参与测试。
  2. 减少维护成本:由于测试用例是用自然语言编写的,不需要频繁修改,可以减少测试脚本的维护成本。
  3. 支持多种编程语言:Cucumber支持多种编程语言,如Java、Ruby、Python等,可以方便团队根据自身技术栈进行选择。

缺点:

  1. 学习成本较高:对于新手来说,学习Cucumber和Gherkin语言可能需要一些时间。
  2. 执行速度较慢:由于Cucumber测试用例是用自然语言编写的,执行速度可能比较慢,特别是在大型项目中。

3、如何使用

3.1 Cucumber+Java实现Web应用程序自动化测试

当使用Cucumber进行Web应用程序自动化测试时,通常会结合Selenium WebDriver来实现。下面是一个简单的示例,演示了如何使用Cucumber和Selenium WebDriver来编写自动化测试用例。

假设我们要测试一个简单的注册页面,包括输入用户名、密码和确认密码,然后点击注册按钮进行注册。我们将使用Cucumber来编写测试用例,使用Selenium WebDriver来模拟用户在浏览器中的操作。

首先,我们需要在项目中引入Cucumber和Selenium WebDriver的相关依赖,并创建一个.feature文件来编写测试用例。假设我们的.feature文件名为registration.feature,内容如下:

Feature: User Registration
  Scenario: User can register with valid credentials
    Given User is on the registration page
    When User enters "john_doe" as username
    And User enters "password123" as password
    And User enters "password123" as confirm password
    And User clicks on register button
    Then User should be registered successfully

接下来,我们需要创建Step Definitions来实现.feature文件中定义的步骤。假设我们将Step Definitions定义在一个名为RegistrationStepDefs.java的文件中:

import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

public class RegistrationStepDefs {
    WebDriver driver;

    @Given("User is on the registration page")
    public void userIsOnRegistrationPage() {
        System.setProperty("webdriver.chrome.driver", "path_to_chrome_driver");
        driver = new ChromeDriver();
        driver.get("url_of_registration_page");
    }

    @When("User enters {string} as username")
    public void userEntersUsername(String username) {
        driver.findElement(By.id("username")).sendKeys(username);
    }

    @When("User enters {string} as password")
    public void userEntersPassword(String password) {
        driver.findElement(By.id("password")).sendKeys(password);
    }

    @When("User enters {string} as confirm password")
    public void userEntersConfirmPassword(String confirmPassword) {
        driver.findElement(By.id("confirmPassword")).sendKeys(confirmPassword);
    }

    @When("User clicks on register button")
    public void userClicksOnRegisterButton() {
        driver.findElement(By.id("registerButton")).click();
    }

    @Then("User should be registered successfully")
    public void userShouldBeRegisteredSuccessfully() {
        // Add assertions to verify successful registration
        driver.quit();
    }
}

在这个示例中,我们使用了Cucumber的注解来定义测试步骤,并使用Selenium WebDriver来模拟用户在浏览器中的操作。

最后,我们可以使用JUnit或TestNG来运行Cucumber测试用例。在Maven项目中,可以使用Maven Surefire插件来运行Cucumber测试。

这只是一个简单的示例,实际项目中可能会有更多复杂的测试场景和操作。但是,通过这个示例,你可以了解如何使用Cucumber和Selenium WebDriver来实现Web应用程序的自动化测试。

3.2 Cucumber+Python 实现Web应用程序自动化测试示例

当使用Cucumber和Python进行Web应用程序自动化测试时,我们通常会使用Behave作为BDD框架,结合Selenium WebDriver来实现。下面是一个简单的示例,演示了如何使用Behave和Selenium WebDriver来编写自动化测试用例。

首先,我们需要安装必要的库。在Python中,我们可以使用pip来安装Behave和Selenium WebDriver:

pip install behave
pip install selenium

接下来,我们创建一个.feature文件来编写测试用例。假设我们的.feature文件名为registration.feature,内容如下:

Feature: User Registration
  Scenario: User can register with valid credentials
    Given User is on the registration page
    When User enters "john_doe" as username
    And User enters "password123" as password
    And User enters "password123" as confirm password
    And User clicks on register button
    Then User should be registered successfully

然后,我们需要创建Step Definitions来实现.feature文件中定义的步骤。我们将Step Definitions定义在一个名为registration_steps.py的文件中:

from behave import given, when, then
from selenium import webdriver

@given('User is on the registration page')
def user_is_on_registration_page(context):
    context.driver = webdriver.Chrome()
    context.driver.get('url_of_registration_page')

@when('User enters "{text}" as username')
def user_enters_username(context, text):
    username_input = context.driver.find_element_by_id('username')
    username_input.send_keys(text)

@when('User enters "{text}" as password')
def user_enters_password(context, text):
    password_input = context.driver.find_element_by_id('password')
    password_input.send_keys(text)

@when('User enters "{text}" as confirm password')
def user_enters_confirm_password(context, text):
    confirm_password_input = context.driver.find_element_by_id('confirmPassword')
    confirm_password_input.send_keys(text)

@when('User clicks on register button')
def user_clicks_on_register_button(context):
    register_button = context.driver.find_element_by_id('registerButton')
    register_button.click()

@then('User should be registered successfully')
def user_should_be_registered_successfully(context):
    # Add assertions to verify successful registration
    context.driver.quit()

在这个示例中,我们使用了Behave的注解来定义测试步骤,并使用Selenium WebDriver来模拟用户在浏览器中的操作。

最后,我们可以使用命令行来运行Behave测试:

behave

这将执行我们编写的测试用例,并输出测试结果。

3.3 Cucumber+Python 实现API接口自动化测试示例

当使用Cucumber和Python进行API接口自动化测试时,我们通常会使用Behave作为BDD框架,结合requests库来实现。下面是一个简单的示例,演示了如何使用Behave和requests库来编写自动化测试用例。

首先,我们需要安装必要的库。在Python中,我们可以使用pip来安装Behave和requests库:

pip install behave
pip install requests

接下来,我们创建一个.feature文件来编写测试用例。假设我们的.feature文件名为api_test.feature,内容如下:

Feature: API Test
  Scenario: Verify API response
    Given API endpoint is "https://api.example.com/users"
    When User sends a GET request to the API
    Then API should respond with status code 200
    And API response should contain user data

然后,我们需要创建Step Definitions来实现.feature文件中定义的步骤。我们将Step Definitions定义在一个名为api_test_steps.py的文件中:

from behave import given, when, then
import requests

@given('API endpoint is "{url}"')
def set_api_endpoint(context, url):
    context.api_url = url

@when('User sends a GET request to the API')
def send_get_request(context):
    context.response = requests.get(context.api_url)

@then('API should respond with status code {status_code}')
def verify_status_code(context, status_code):
    assert context.response.status_code == int(status_code)

@then('API response should contain user data')
def verify_user_data_in_response(context):
    # Add assertions to verify user data in API response
    # For example, check if certain fields are present in the response
    pass

在这个示例中,我们使用了Behave的注解来定义测试步骤,并使用requests库来发送API请求并验证API响应。

最后,我们可以使用命令行来运行Behave测试:

behave

这将执行我们编写的测试用例,并输出测试结果。

通过上述你可以了解如何使用Behave和requests库来实现API接口的自动化测试,实际项目中可能会有更多复杂的测试场景和操作,具体可自行探究。

作为LLM(大模型)开发框架的宠儿,LangChain在短短几年内迅速崛起,成为开发者们不可或缺的工具。本文将带你探讨LangChain和LangChainHub的发展历程。

1. LLM开发框架的宠儿

这两年人工智能领域发展迅猛,LLM(大模型)的出现功不可没。LLM的发展将整个人工智能领域往前推进了一大步,将人工智能这道狭窄的门撑宽了不少,让我们这些平凡的普通人也有机会挤进人工智能的发展中。

普通开发者在人工智能领域能做什么呢?目前可以发力的领域是:开发基于LLM的App。开发LLM的App有多种实现方式,LangChain是比较流行的一种。

伴随着人工智能的发展,这两年LangChain发展迅猛,GitHub上的star数飙升,近期更是成功融资2亿美刀,足见其受欢迎程度。

早期的LangChain只是作为一个工具或者胶水,集成了多个LLM和多种外部组件(比如记忆、检索、向量数据库、工具集等等),方便开发者快速开发基于LLM的App。

我估计,当时LangChain的创始人对LangChain的定位也不是很清晰,只是觉得
AI应用开发
是个不错的风口,先搞了再说,这也很符合创业和商业逻辑。

2. 现在的LangChain

随着LLM的发展,一切逐步走向确定性(业内一致认为LLM App是未来的方向),而且可能伴随着商业化的要求,LangChain对自己的定位发生了一些改变。

早期的LangChain可能只是想抓住一波LLM的风口,对于自身的定位也不是很清晰,随着LLM领域更多的发力点在构建基于LLM的App上,LangChain也赶紧调整了自身的定位。

不再定位在:胶水、工具集合等等理念了。更多的强调自己是:
构建LLM App的最大社区,基于LangChain可以开发出可推理的应用程序。

调整了定位,生态也自然做了调整,同时软件架构也随之变化,比如:软件包上做了很多重构和重组,核心能力 和 周边社区生态 的边界越发清晰。

同时,还加入了LangSmith用于监控LLM应用,还有LangServe用于部署LLM应用。一切的步伐都在朝着商业化发展。

至此LangChain调整为以下几个核心模块:

  • LangChain-Core:抽象LangChain的内核 和 LangChain 表达式语言。
  • LangChain-Community:集成的各种第三方部件。
  • LangChain:构成LLM应用程序需要的 链、代理和检索等。
  • LangSmith:开发者平台,可让 调试、测试、评估和监控 基于任何 LLM 框架上构建的链,并与 LangChain 无缝集成。
  • LangServe:用于将 LangChain 的应用 部署为 REST API。

这几点我们从官网的架构图清晰可见。也可以认为此时的LangChain变成了一个SAAS化的开发者平台。而它提供的组件、工具、监控、部署等生态,也都是为了让开发者方便的开发出LLM App,然后部署到这个平台上。

LangChain是一个优秀的LLM开源框架,也无法避免走上常规的开源软件商业化的套路。

3. 早期的LangChainHub

LangChain早期推出的各种组件中
LangChainHub
是其中一个比较有意思的项目。

早期LangChainHub对自己的定位如下:LangChainHub 受 Hugging Face Hub 启发,是一个用于发现和提交常用的 提示、链、代理等的平台。早期,LangChainHub以Prompt集合为起点,然后很快扩展到 链 和 代理。

这个定位我们从之前的LangChainHub在github仓库上的目录可见一斑。

此时的
LangChainHub
可以理解为
LangChain
工具包 或者说 组件中心,里面提供了高质量的组件方便开发者使用。确确实实是一个分享和探索Prompt、链 和Agent的地方。

比如,我们要基于reAct机制实现一个Agent,如果自己写一堆Prompt(提示词)那就太费劲了。此时,在
LangChainHub
就有许多现成可用的Prompt模板,使用简单又省事,所以
LangChainHub
迅速流行开。

4. 现在的LangChainHub

那现在的LangChainHub在新的LangChain架构图的哪里呢?我也觉得挺奇怪的,LangChainHub也是算生态里较为重要的版块,架构图里居然没提。

后来发现,LangChainHub被放到了LangSmith里。这个从新版的官方文档也是清晰可见。

这也算合情合理吧,毕竟LangSmith是一个开发者平台,用于调试、测试、评估、监控基于LLM框架的链,在这个平台上,可以使用和创建Prompt。

早期的时候LangChainHub有Prompt、Chain、Agent,现在也只有Prompt了。我个人对LangChainHub的定位多少觉得有些悲凉了,LangChainHub沦为了Prompt模板仓库。

我认为这背后存在两种可能的原因:

  • 可能是商业化的要求吧,将常用的Prompt模板挪到开发者平台LangSmith里,毕竟LangSmith是有各种付费计划的。
  • 可能是官方对于LangChainHub里的内容开始做收缩,聚焦于Prompt,毕竟Prompt比较独立 而且易于交付。而Chains 和 Agents 相对来说,交付难度高一些,变动性也低,所以官方直接将 Chains 和 Agents 沉淀到自己的核心库里,这样也能保证Chains 和 Agents 的质量,保证自己的口碑。

LangChainHub真的是始于Prompt,终于Prompt!!!当然啦,以后的LangChainHub怎么发展就不得而知了。

庆幸的是,我们大部分场景还是使用Prompt模板居多,此时的LangChainHub里依旧能找到大咖们贡献的复杂常用的Prompt模板,降低了我们使用ReAct、Tool的门槛。

5. LangChainHub的使用

下面我们通过一个示例,来看看如何使用LangChainHub。

5.1. 拆解LangChainHub的Prompt

比如:要实现一个reAct机制,如果我们自己写提示词,那太复杂了。但是LangChainHub上已经有大佬定义好了相关提示词。

比如:
structured-chat-agent

https://smith.langchain.com/hub/hwchase17/structured-chat-agent
),提示词写的还是有点复杂的,大致意思就是:告诉LLM,它可以使用一堆什么格式的工具,然后先推理,选择合适的工具,执行之后,进行观察,观察完了之后,继续推理,如果有答案了,就回复用户。

具体内容如下:

5.2. 使用LangChainHub

使用LangChainHub上的Prompt就2步:

  1. 导入LangChainHub库
  2. 从Hub上拉取对应的提示词

接下来举个例子,比如,LLM在直接计算浮点数加减时会出现错误,我要做一个基于reAct框架的AI Agent,让这个Agent帮我精准计算浮点数。这里使用LangChainHub里的
structured-chat-agent
来简化我的流程。

具体代码如下:

from langchain import hub
from langchain.agents import create_structured_chat_agent, AgentExecutor, tool
from langchain.memory import ConversationBufferMemory
from langchain.schema import HumanMessage
from langchain.tools import BaseTool
from langchain_openai import ChatOpenAI

model = ChatOpenAI(
    model="gpt-3.5-turbo",
    openai_api_key="sk-xnAKVC6V0LzBwqGK9fE59cFcBc3f40DcBf96C135112dFb63",
    openai_api_base="https://api.aigc369.com/v1",
)


# 定义工具
class SumNumberTool(BaseTool):
    name = "数字相加计算工具"
    description = "当你被要求计算2个数字相加时,使用此工具"

    def _run(self, a, b):
        return a["title"] + b["title"]


# 加入到工具合集
tools = [SumNumberTool()]

# 使用reAct的提示词
prompt = hub.pull("hwchase17/structured-chat-agent")

# 创建Agent
agent = create_structured_chat_agent(llm=model, tools=tools, prompt=prompt)

# 创建记忆组件
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)

# 创建Agent执行器
agent_executor = AgentExecutor.from_agent_and_tools(
    agent=agent, tools=tools, memory=memory, verbose=True, handle_parsing_errors=True
)

agent_executor.invoke({"input": "你帮我算下 3.941592623412424 + 4.3434532535353的结果"})

6. 总结

本文主要聊了LangChain和LangChainHub的发展变迁,还介绍了LangChainHub的使用,希望对你有帮助!

=====>>>>>>
关于我
<<<<<<=====

本篇完结!欢迎点赞 关注 收藏!!!

原文链接:
https://mp.weixin.qq.com/s/lpv3q4y34m1VSmR0AEN6Rw