系统中间件与云虚拟化实践

YuDou dreaming...

1. 基于阿里云云效Codeup的Git代码管理

1.1 实验目标与相关知识技能

• 了解Git常用指令及其基本原理,如:git push,git pull,git init,git add,git config,git branch,git commit等

• 了解一些常见的Linux系统命令,如:cd,cat,touch等

• 学会使用ssh方式访问仓库,减少不必要的交互以提高效率

1.2 实验步骤与对应成果展示

​ 创建test.py文件,暂存至暂存区后观察文件状态,提交至本地仓库后观察文件状态,推送至远程仓库并观察文件状态

1
2
3
4
5
6
7
echo "import json" > test.py  #创建test.py
git add test.py #暂存test.py
git status #观察文件状态
git commit -m "feat(test.py):引入json" #提交至本地仓库
git status #观察文件状态
git push origin master #推送至远程仓库
git status #观察文件状态

​ 至此,已完成暂存、提交、推送三个基本操作,本地代码已同步至云端Codeup

​ 修改test.py文件后观察文件状态,暂存修改后的文件并观察文件状态,添加"import re"test.py中,观察文件状态,提交至本地仓库后观察文件状态,推送至云端仓库后观察文件状态,查看历史提交记录

1
2
3
4
5
6
7
8
9
10
11
echo "import random" > test.py  #修正test.py
git status
git add test.py
git status
echo "import re" >> test.py
git status
git commit -a -m "feat(test.py):引入random,re"
git status
git push
git status
git log #git log用于查看历史提交记录

​ 修改test.py,暂存文件test.py,取消暂存test.py,提交至本地仓库,撤销提交,再次提交至本地仓库,推送至云端仓库,本地回滚后再次强行推送至远程仓库,期间不断git status观察文件状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
echo "print('Hello world')" >> test.py
git status
git add test.py
git status
git restore --staged test.py #将本地仓库HEAD指向的版本复制到暂存区
git status
git add test.py
git reset HEAD test.py
git commit -m "feat(test.py):打印Hello World"
git status
git reset HEAD~ #HEAD~是HEAD的父节点,设置HEAD指向当前提交的上一次提交
git status
git commit -a -m "feat(test.py):打印Hello World"
git push origin master
git reset HEAD~
git push -f origin master

​ 取消对test.py的跟踪并恢复,与最近一次修改前的test.py进行比较,暂存test.py至暂存区,删除test.py,查看test.py的内容,从暂存区中恢复test.py,查看test.py内容,删除test.py,再强制删除已暂存的test.py,尝试恢复被删除的文件,查看test.py是否被恢复,从暂存区中恢复test.py,查看test.py的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Plain Text
git rm --cached test.py #取消对test.py的跟踪
git status
git add test.py
git diff test.py
git status
git add test.py
git status
rm test.py #删除test.py
cat test.py #在终端中打印test.py的内容
git status
git restore test.py
cat test.py
git status
git rm test.py
git rm -f test.py
git status
git restore test.py
git restore --staged test.py
git status
git restore test.py
cat test.py

img

img

1.3 遇到的主要问题、解决思路和收获

​ 该实验暂未遇到问题

2. 基于阿里云 ECS 与 ACR 的容器镜像管理

2.1 实验目标与相关知识技能

• 掌握ECS的基本操作,能较为熟练地使用Linux平台,如ECS远程连接,Xftp 7传输文件等

• 掌握Docker的常用命令,包括拉取镜像,启动容器,查看镜像,删除镜像,暂停容器等

• 学会撰写Dockerfile文件来构建镜像,并基于阿里云ACR完成镜像的pull和push

2.2 实验步骤与对应成果展示

​ 使用docker pull拉取nginx镜像

1
docker pull nginx

​ 使用docker images查看已拉取的镜像

1
docker images

​ 使用docker run运行docker容器

1
docker run -d --name nginx -p 80:80 nginx

​ 使用docker ps查看当前运行的容器信息

1
docker ps  

​ 此时可用本地浏览器访问该ECS服务器公网,即http://<ECS公网IP>以此验证容器是否正常运行

img

​ 将Nginx服务的配置文件、日志文件及Web服务的根目录分别建立持久化映射,可以理解为让容器中的目录与宿主目录进行同步

1
2
3
4
5
6
7
8
mkdir -p /opt/docker/nginx/conf.d
mkdir -p /opt/docker/nginx/logs
mkdir -p /opt/docker/nginx/html
# -p --parents:如果路径中任意一级父目录不存在,则创建

docker cp nginx:/etc/nginx/nginx.conf /opt/docker/nginx/
docker cp nginx:/etc/nginx/conf.d /opt/docker/nginx/
docker cp nginx:/usr/share/nginx/html /opt/docker/nginx/

​ 现在要想Nginx服务使用持久化存储的数据,需要先停止再删除当前运行的nginx容器

1
2
3
docker stop nginx
docker rm nginx
docker ps -a #观察是否删除成功

​ 重新执行docker run,同时配置好相关参数

1
2
3
4
5
6
7
8
# -v, --volume list:挂在目录,<宿主目录>:<容器目录>
docker run -d --restart=always \
--name nginx \
-p 80:80 \
-v /opt/docker/nginx/nginx.conf:/etc/nginx/nginx.conf \
-v /opt/docker/nginx/html:/usr/share/nginx/html \
-v /opt/docker/nginx/logs:/var/log/nginx \
nginx

​ 使用docker stop,docker start进行练习

1
2
docker stop nginx
docker start nginx

​ 使用curl命令获取网页内容

1
curl 127.0.0.1:80  #在ECS终端上输入,所以地址是127.0.0.1

​ 使用docker exec命令进入容器。该命令定义为在容器内部运行一条指定命令,可以指定命令为Shell程序,如/bin/bash,配合参数-it,可以实现进入容器进行命令行交互式操作

1
2
3
4
5
docker exec -it nginx /bin/bash
# -i --interactive,保持交互模式
# -t --tty,分配一个伪终端(模拟终端)
# nginx,容器名称
# /bin/bash,待执行的命令,进入bash shell

​ 进入容器后,在nginx服务的Web服务的根目录中创建test.html文件。由于容器本身不支持vim,因此用echo命令进行编写,最后用exit命令退出容器

1
2
3
4
5
cd /usr/share/nginx/html
echo "<h1>Hello,world</h1>" > test.html
exit
ls /opt/docker/nginx/html #查看是否成功持久化储存到宿主系统
curl 127.0.0.1:80/test.html

​ 除了curl命令,一样可以用浏览器访问http://<ECS公网地址>/test.html来验证

img

​ 使用docker logs查看容器日志,可以通过此方法来判断容器是否正常工作

1
docker logs nginx

​ 使用dcoker rmi删除不必要的容器镜像时,要先删除使用该镜像的容器,想删除容器又得先停止容器实例运行。故删除镜像的顺序如下:

1
2
3
4
docker stop nginx
docker rm nginx
docker rmi nginx
docker images #判断镜像是否删除成功

​ 构建Docker容器镜像可以通过编写并运行Dockerfile文件来实现,一般而言,Dockerfile的文件指令逻辑应按照以下模式建立:选择合适的基础镜像、安装基础工具与依赖、添加其他应用、清理缓存、声明镜像端口暴露情况、设置默认启动命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 使用Ubuntu作为基础镜像
FROM ubuntu:22.04
# 维护者信息
LABEL maintainer="<yud0u@qq.com>"

# 设置工作目录
WORKDIR /app

# 安装nginx及Python
RUN apt-get update && apt-get install -y \
nginx \
python3 \
python3-distutils

# 清理apt软件包缓存
RUN rm -rf /var/lib/apt/lists/*

#声明暴露端口
EXPOSE 80

#设置启动命令
CMD["nginx", "-g", "daemon off;"]

​ 使用docker build命令创建容器镜像(最好在Dockerfile同一目录下运行)

1
2
3
4
5
# -t, tag:镜像名称(REPOSITORY:TAG)
# npu:Nginx Python Ubuntu,自定义镜像名称
# .:PATH,执行命令的上下文路径,构造过程中可以引用该上下文中的任何文件
docker build -t npu .
docker images #查看镜像是否创建成功

​ 运行该镜像,检测Nginx服务;进入容器后检测Python解析能力

1
2
3
4
5
6
docker run -d --name npu -p 80:80 npu
curl 127.0.0.1
docker exec -it npu /bin/bash
(nginx)python3
exit()
(nginx)exit

​ 将创建的npu镜像推送至ACR,在此之前先在ACR控制台创建镜像仓库npu

1
2
3
docker login --username=鱼豆YuDou crpi-n4sctgr4gzd05xye.cn-hangzhou.personal.cr.aliyuncs.com
docker tag [ImageId] crpi-n4sctgr4gzd05xye.cn-hangzhou.personal.cr.aliyuncs.com/exp_yd/npu:[镜像版本号]
docker push crpi-n4sctgr4gzd05xye.cn-hangzhou.personal.cr.aliyuncs.com/exp_yd/npu:[镜像版本号]

​ 推送成功后可以在该镜像仓库中查看提交记录

img

​ 从ACR中拉取容器镜像进行测试,需要先删除本地容器及其镜像

1
2
3
4
5
docker stop npu
docker rm npu
docker rmi npu
docker rmi crpi-n4sctgr4gzd05xye.cn-hangzhou.personal.cr.aliyuncs.com/exp_yd/npu
docker images #查看镜像是否删除成功

​ 删除后再进行拉取操作,并进行测试

1
2
3
docker pull crpi-n4sctgr4gzd05xye.cn-hangzhou.personal.cr.aliyuncs.com/exp_yd/npu:latest
docker run -d --name npu -p 80:80 crpi-n4sctgr4gzd05xye.cn-hangzhou.personal.cr.aliyuncs.com/exp_yd/npu:latest
curl 127.0.0.1 #查看容器是否正常运行

2.4 遇到的主要问题、解决思路和收获

(✅)本实验遇到的最大问题为:docker拉取镜像超时。

​ 为解决此问题,共尝试了三种方法:

  1. (❌)在/etc/docker/daemon.json下配置docker镜像源,包括各种华为云等镜像源,配置方法参考以下链接https://blog.csdn.net/weixin_50160384/article/details/139861337,但均以失败告终
  2. (❌)在/etc/docker/daemon.json下配置阿里云镜像加速器,在ACR中获取加速器地址,并按如下方式配置,不添加其他镜像源,但效果并不稳定,有时可以拉取成功有时又不行
1
2
3
{
"registry-mirrors":["https://***.mirror.aliyuncs.com"]
}

image-20250113233043464

  1. (✅)为ECS服务器配置代理,在网上阅读了许多文章后,我将具体方法总结在我的个人博客上:https://sweetyudou.github.io/2024/11/14/Docker/。简而言之,我使用clash成功为ECS服务器配置代理,并且成功做到了稳定拉取镜像,是一劳永逸的好方法

3. 基于阿里云ECS与ACR的Python微服务镜像构建、部署与接口访问

3.1 实验目标与相关知识技能

• 掌握Dockerfile构建容器镜像

• 理解HTTP协议基本概念,掌握HTTP调试工具的使用

• 理解网络服务API的概念,学会HTTP RESTful API的使用

• 理解并掌握Python Requests

3.2 实验步骤与对应成果展示

📂:exp_pyms_api_demo

​ 在ACR中创建镜像仓库,以备后续的镜像上传

img

​ 启动本实验提供的微服务范例,体验HTTP RESTful API接口,本服务范例在8000端口监听,启动命令为:

1
2
./exp_pyms_api_demo ./device.csv
# ./exp_pyms_api_demo <csv_file>,csv_file为存储设备信息的CSV文件路径。若该路径中存在CSV文件则读取信息,否则自动创建5条设备信息并生成CSV文件

​ 此时访问http://<IP>:8000/docs将会看到以下页面:

image-20250114213605484

​ 此时当前目录创建device.csv文件:

1
ls -F  #查看是否创建device.csv文件

Ctrl + C退出程序后,创建Dockerfile文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 使用Ubuntu 22.04 作为基础镜像
FROM ubuntu:22.04
LABEL maintainer="<yud0u@qq.com>"

# 指定工作目录为 /app
WORKDIR /app
#将主机文件系统中的 exp_pyms_api_demo 复制到镜像中
COPY exp_pyms_api_demo exp_pyms_api_demo

# 暴露服务端口 8000
EXPOSE 8000

# 指定容器的启动命令,devcie.csv 将存储在容器的/var/lib/exp_pyms_data/ 目录下,因此该目录应持久化
CMD ["/app/exp_pyms_api_demo", "/var/lib/exp_pyms_data/device.csv"]

​ 使用docker build命令构建容器镜像,镜像名为exp_pyms_api_demo:1.0

1
2
docker build -t exp_pyms_api_demo:1.0 .
docker images #查看镜像是否构建成功

​ 将镜像上传至ACR镜像仓库

1
2
3
docker login --username=鱼豆YuDou crpi-n4sctgr4gzd05xye.cn-hangzhou.personal.cr.aliyuncs.com
docker tag [ImageId] crpi-n4sctgr4gzd05xye.cn-hangzhou.personal.cr.aliyuncs.com/exp_yd/exp_pyms_api_demo:[镜像版本号]
docker push crpi-n4sctgr4gzd05xye.cn-hangzhou.personal.cr.aliyuncs.com/exp_yd/exp_pyms_api_demo:[镜像版本号]

​ 推送成功后,可在镜像仓库exp_pyms_api_demo的页面中查看刚刚推送的镜像

img

​ 远程登录ECS并拉取镜像

1
docker pull crpi-n4sctgr4gzd05xye.cn-hangzhou.personal.cr.aliyuncs.com/exp_yd/exp_pyms_api_demo:[镜像版本号]

​ 在ECS工作目录创建持久化目录exp_pyms_data,用于存储容器内服务生成的业务数据文档,即设备信息数据,随后运行该镜像

1
2
3
4
5
6
mkdir exp_pyms_data
docker run -d --restart=always\
--name exp_pyms_api_demo \
-v ./exp_pyms_data/:/var/lib/exp_pyms_data/ \
-p 8000:8000 \
crpi-n4sctgr4gzd05xye.cn-hangzhou.personal.cr.aliyuncs.com/exp_yd/exp_pyms_api_demo:1.0

​ 使用docker logs查看服务是否成功启动

1
2
docker logs exp_pyms_api_demo  
ls exp_pyms_data/ #若有device.csv文件,则成功创建5条设备信息且服务成功运行

​ 使用OpenAPI查看微服务接口,在浏览器中通过<ecs_ip>:8000/docs来访问Redocly文档地址

img

​ 同理,本实验Swagger文档地址为<ecs_ip>:8000/docs/swagger

img

​ 配置Python虚拟环境exp_venv

1
2
3
python3 -m venv exp_venv
source exp_venv/bin/activate
pip install requests

​ 进入Python交互式界面进行初步验证

1
2
3
4
python3
import requests #验证requests库是否可导入
requests.__version__ #查看requests版本
exit()

​ 查看先前生成的device.csv文件内容,通过Requests库访问接口来获取设备信息

1
2
3
4
5
6
7
8
cat exp_pyms_data/device.csv
python3
import requests
resp = requests.get("http://<ecs_ip>:8000/v1/devices",json={"id":"6713124465"}) #通过id来检索设备并获取信息,该id为随机生成的例子
resp
resp.status_code #查看状态码判断访问是否成功,访问成功的HTTP状态码为200
resp.text
resp.json()

​ 同时还支持用“序号SN-类型”来检索设备

1
2
3
resp = requests.get("http://<ecs_ip>:8000/v1/devices", json={"sn": "XRmiI", "model": "Raspberry Pi 4"})  #该组合信息为随机生成的例子
resp.status_code
resp.json()

​ 体验删除接口

1
2
3
resp = requests.delete("http://<ecs_ip>:8000/v1/devices", json={"ids":["3116641919", "94791787847", "1234567890"]})
resp.json()
cat exp_pyms_data/device.csv #查看设备信息是否被删除

​ 体验添加设备接口

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
resp = requests.post(
"htpp://<ecs_ip>:8000/v1/devices",
json={
"name": "test-name",
"type": "controller",
"hardware":{
"model": "test-model",
"sn": "test-sn"
},
"software": {
"version": "0.1",
"last_update": "2023-08-06 20:00:00"
},
"nic": [
{
"type": "wifi",
"mac": "12:34:56:78:9a:bc",
"ipv4": "192.168.1.2"
}
],
"status": "online"
}
)
resp.json()
cat exp_pyms_data/device.csv #查看设备信息是否成功添加

​ 体验更新设备信息接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
resp = requests.put(
"http://127.0.0.1:8000/v1/devices",
json={
"id": "0922282528",
"name": "new-name",
"software": {
"version": "0.5",
"last_update": "2023-08-06 10:00:00"
},
"status": "offline"
}
)
resp.json()
cat exp_pyms_data/device.csv #查看是否成功更新设备信息

3.3 遇到的主要问题、解决思路和收获

​ 该实验暂未遇到问题

4. 基于阿里云云效Flow的Python Web服务构建与部署

4.1 实验目标与相关知识技能

• 掌握Python虚拟环境的应用和操作

• 熟练运用阿里云云服务器ECS、阿里云云效代码管理Codeup与流水线Flow,以及阿里云容器镜像服务ACR

• 掌握Python项目部署的多种方式,初步体验流水线的应用模式

4.2 实验步骤与对应成果展示

📂:exp_pyms_demo

​ 用Xftp 7把范例服务包上传至云端解压,并在ECS服务器上的虚拟环境部署范例服务

1
2
3
4
5
tar -xzvf exp_pyms_demo.tar.gz
cd exp_pyms_demo
python3 -m venv exp_venv
source exp_env/bin/activate
pip3 install -r requirements.txt

​ 启动Python Web服务:

1
python3 server.py

​ 使用本地浏览器访问<ECS_IP>:8000/,查看服务页面输出

img

​ 确认无误后在终端输入Ctrl + C停止服务,退出虚拟环境venv并删除虚拟目录

1
2
deactivate
rm -r exp_venv

​ 在exp_pyms_demo目录下创建Dockerfile文件,将Python Web服务范例代码打包进容器镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 基于Python 最新基础镜像
FROM python:3.10-slim-buster
LABEL maintainer="<yud0u@qq.com>"

# 工作目录
WORKDIR /app

# 安装Python库
COPY requirements.txt requirements.txt
RUN apt-get update && apt-get install -y \
&& pip3 install -r requirements.txt

# 清理 apt 软件包缓存
RUN rm -rf /var/lib/apt/lists/*

# 复制当前文件夹中全部文件到镜像中
COPY . .

# 暴露服务端口 8000
EXPOSE 8000

# 容器启动时执行命令
CMD [ "python3", "server.py"]

​ 使用docker build命令构建镜像

1
2
docker build -t exp_pyms_demo .
docker images #查询镜像是否构建成功

​ 启动相关容器

1
docker run -d --restart=always --name exp_pyms_demo -p 8000:8000 exp_pyms_demo

​ 同样在浏览器上访问http://<ECS_IP>:8000/,查看服务页面输出来验证。或者用curl命令来验证

1
curl http://127.0.0.l:8000

​ 验证成功后停止、删除容器后,删除镜像

1
2
3
docker stop exp_pyms_demo
docker rm exp_pyms_demo
docker rmi exp_pyms_demo

​ 本地下载范例服务包后,进行本地仓库初始化并关联远程仓库,创建venv分支为后续实验做准备

1
2
3
4
5
6
7
8
git init
git remote add origin git@codeup.aliyun.com:66e7c39e4244e4202214531d/exp_pyms_demo.git
git pull origin master
git add .
git commit -m "init"
git push -u origin master
git chechout -b venv #创建venv分支并切换
git push --set-upstream origin venv #创建远程仓库,与本地仓库关联

​ 配置云效Flow以支持ECS虚拟环境部署,选择代码源为Codeup的代码仓库exp_pyms_demo,默认分支为venv,将ECS服务器配置到主机部署中,运行流水线

image-20250115214222161

​ 此时可以通过curl命令测试服务接口是否可访问,或通过浏览器访问<ECS_IP>:8000/

1
curl http://127.0.0.1:8000

​ 修改devices.csv,添加一条设备信息,并推送至远程仓库触发流水线执行,用浏览器重新访问

img

​ 可以看到已显示添加的设备信息,关闭范例服务

​ 将之前准备好的Dockerfile文件用于在流水线中制作承载范例服务的Docker镜像

1
2
3
git add Dockerfile
git commit -m "docs: Add Dockerfile"
git push --set-upsstream origin docker

​ 重新配置好流水线源后运行流水线,改用Docker部署

img

​ 根据「Python代码扫码阶段」所提示的信息,修改server.py存在的格式问题后提交、推送

img

4.3 遇到的主要问题、解决思路和收获

(✅)本实验遇到的最大问题为:Dockerfile中拉取python:3.10-slim-buster超时

img

img

  1. (❌)根据该页面的智能排查,我尝试从阿里云ACR制品中心中拉取alinux3/python来替代,后续出现一系列如:apt-get命令不存在,需替换成apk等问题,最后发现是操作系统之间不兼容,下载的python是基于alinux3的

img

  1. (✅)咨询了阿里云在线客服后,把「Python镜像构建」配置中的构建集群改为「云效中国香港构建集群」,即可顺利拉取

img

5. 基于阿里云函数计算的简单邮件发送服务设计与体验

5.1 实验目标与相关知识技能

• 理解并掌握函数计算FC的设计部署过程

• 理解并体验函数计算FC的弹性伸缩能力

5.2 实验步骤与对应成果展示

​ 创建自定义公共层,提供Python Sanic依赖,以供后续实验进行。兼容运行时选择Debian 10,构建环境选择Python 3.10requirements.txt文件中输入sanic

img

​ 创建Web函数fun-alarm-email-send,,基于Sanic框架编写接口代码,构建并部署告警邮件发送接口

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
# -*- coding: utf-8 -*-
from sanic import Sanic
from sanic.response import json
from smtplib import SMTP
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
app = Sanic("EmailSender")

# 邮件配置 需要自行修改。邮箱授权码(password)的获取,自行前往对应
EMAIL_CONFIG = {
"host": "smtp.example.com",
"port": 587,
"username": "your-email@example.com",
"password": "your-password",
"sender": "your-email@example.com"
}

def send_email(recipient, subject, body):
# 创建邮件对象
msg = MIMEMultipart()
msg['From'] = EMAIL_CONFIG["sender"]
msg['To'] = recipient
msg['Subject'] = subject
# 添加邮件正文
msg.attach(MIMEText(body, 'plain'))
# 连接SMTP服务器
server = SMTP(EMAIL_CONFIG["host"], EMAIL_CONFIG["port"])
server.starttls() # 启动TLS加密
server.login(EMAIL_CONFIG["username"], EMAIL_CONFIG["password"])
# 发送邮件
server.send_message(msg)
# 关闭连接
server.quit()

@app.route("/send", methods=["POST"])
def send_email_route(request):
data = request.json
recipient = data.get("recipient")
subject = data.get("subject")
body = data.get("body")
if not all([recipient, subject, body]):
return json({"error": "Missing required fields"})
try:
send_email(recipient, subject, body)
return json({"message": "Email sent successfully"})
except Exception as e:
return json({"error": str(e)}, status=500)

if __name__ == "__main__":
app.run(host="0.0.0.0", port=9000)

​ 代码修改后,删除现有的官方公共层Flask,并添加自定义创建的sanic-custom-layer层后部署,最后部署代码。利用Apifox工具编写对邮件发送接口发起HTTP接口

img

img

​ 点进函数详情页面的配置-运行时按钮,修改单实例并发度为2,最后部署。随后在Apifox上点击自动化测试按钮新增测试场景,填写请求信息后,将线程数设置为10,即同时并发执行的线程数一共10个。监控函数详情页面的实例列表

img

5.3 遇到的主要问题、解决思路和收获

​ 该实验暂未遇到问题

6. 基于阿里云函数计算的云工作流CloudFlow设计与体验

6.1 实验目标与相关知识技能

• 理解并掌握阿里云CloudFlow的基本概念,及其与函数计算点的可视化编排工具,能够按需设计CloudFlow,通过编排多个函数计算的自动执行工作流,从而构建服务接口

6.2 实验步骤与对应成果展示

​ 在https://ram.console.aliyun.com/roles中创建角色,分配的权限包括:`AliyunFCFullAccess`、`AliyunFnFFullAccess`、`AliyunEventBridgeFullAccess`

​ 在https://eventbridge.console.aliyun.com/overview中一键授权,对`EventBridge`进行授权

​ 构建名为fun-temperature-and-humidity-data-upload的FC函数

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
# -*- coding: utf-8 -*-
import datetime
from sanic import Sanic
from sanic.response import json

app = Sanic("MyApp")

# 温度阈值
t_threshold = (25, 28)
# 湿度阈值
h_threshold = (30, 33)

@app.route("/upload", methods=["POST"])
def data_upload(request):
try:
data = request.json
sn = data.get("sn")
temperature = data.get("temperature")
humidity = data.get("humidity")
if not all([sn, temperature, humidity]):
return json({"error": "Missing required fields"}, status=400)

# 判断温湿度是否超出阈值
t_out_flag = not (t_threshold[0] <= temperature <= t_threshold[1])
h_out_flag = not (h_threshold[0] <= humidity <= h_threshold[1])

email_body = generate_email_body(sn, temperature, humidity, t_threshold, h_threshold)
res = {
"status": 1 if t_out_flag or h_out_flag else 0,
"message": "异常" if t_out_flag or h_out_flag else "正常",
"data": {
# 需要自定替换成收件人邮箱地址
"recipient": "synx@example.com",
"subject": "告警邮件 - 温湿度异常",
"body": email_body
} if t_out_flag or h_out_flag else None
}
return json(res)
except Exception as e:
return json({"error": str(e)}, status=500)


def generate_email_body(sn, temperature, humidity, t_threshold, h_threshold):
return (
"告警通知:\n\n"
"当前设备({sn})的温湿度数据超出正常范围。\n\n"
"设备温度:{temperature}°C\n"
"温度阈值:{t_threshold[0]}°C - {t_threshold[1]}°C\n"
"设备湿度:{humidity}%\n"
"湿度阈值:{h_threshold[0]}% - {h_threshold[1]}%\n"
"请尽快检查设备并采取相应措施。\n\n"
"时间:{datetime.datetime.now().isoformat()}"
)


if __name__ == "__main__":
app.run(host="0.0.0.0", port=9000)

​ 此时,FC函数列表中应该有两个函数

img

​ 创建云工作流,地域需与FC所在地域保持一致。采用快速模式来设置工作流,点击CloudFlow Studio按钮回到画布编辑页面,删除工作流默认创建的Hello World, 并从左侧操作拖拽InvokeFunction任务状态到画布上,命名为fun-temperature-and-humidity-data-upload,右侧基本配置中切换YAML编辑,在YAML框内添加body.$: $Input.body,点击保存。在输出配置中,勾选使用JsonPath选择部分参数,填入$Output.Body,点击保存。

​ 再拖拽一个InvokeFunction任务,命名为fun-alarm-email-end,配置YAML数据负载时,填写body.$: $Input.data

​ 从左侧拖拽Choice放置在两个InvokeFunction之间,再拖拽Succeed补全步骤,Choice默认规则的下个状态设置为Succeed,自定义 #1卡片添加条件$Input.status == 1,并设置下个状态为fun-alarm-email-send,点击保存

img

​ 修改fun-alarm-email-sendFC代码,以适配云工作流调用

1
2
3
4
5
6
Python
# 源代码:
@app.route("/send", methods=["POST"])
# 修改为:
@app.route("/send", methods=["POST"], name='send')
@app.route("/invoke", methods=["POST"], name='invoke')

​ 修改fun-temperature-and-humidity-data-uploadFC代码

1
2
3
4
5
# 源代码:
@app.route("/upload", methods=["POST"])
# 修改为:
@app.route("/upload", methods=["POST"], name='upload')
@app.route("/invoke", methods=["POST"], name='invoke')

​ 返回工作流详情界面,点击工作流调度标签页,创建工作流调度并选择HTTP/HTTPS触发,请求类型选择HTTPS,请求方法为POST,返回工作流详情页面后点击详情按钮,复制公网访问地址

​ 使用Apifox对云工作流公网发起POST请求

img

​ 返回工作流详情页面,点击执行记录标签,将出现一条执行记录

img

​ 点击详情可查看详细的执行流程

img

​ 修改请求的数据,将temperature改为40,模拟设备出现异常情况。发起请求后查看详细执行流程

img

6.3 遇到的主要问题、解决思路和收获

​ 本实验仅遇到一个乌龙,花了一个多小时才debug出来:即在flow-temperature-and-humidity-data-upload的YAML编辑中,错把$Input.body填成$Input.Body,填写了大写的B。导致后续用Apifox发起请求时返回结果与范例不同。调用云工作流内部的测试功能,观察每个InvokeFunction的输入与输出后,意识到请求数据从未进入过流程当中,于是问题锁定在第一个InvokeFunction中,在研究数据如何导入的过程中,发现数据是根据$Input.body导入的,最终发现并解决了这个乌龙

img

7. 基于阿里云函数计算的简单邮件发送服务之数据库访问中间件

7.1 实验目标与相关知识技能

• 掌握基于PyMySQL使用原生SQL开展数据库表访问的基本操作

• 掌握基于ORM框架的SQLAlchemy开展数据库表访问

7.2 实验步骤与对应成果展示

​ 进入PolarDB的控制台,创建集群后,为集群配置白名单,允许所有IP访问(0.0.0.0/0)

image-20250125215109634

​ 在基本信息中找到数据库连接,复制私网地址以供后续实验使用

image-20250125215128017

​ 查看当前专有网络VPC和交换机vSwitch信息,保证后续实验产品共用同一套VPC和vSwitch

​ 构建FC自定义公共层,提供Python sqlalchemy依赖,并为fun-alarm-email-send函数配置相同的VPC和vSwitch

​ 使用阿里云提供的云数据库统一控制台(https://dmslab.aliyun.com/)对PolarDB进行操作,登录实例后进入管理页面,点击`创建库`按钮新建`db_message`数据库,字符集选择utf8mb4

​ 打开db_message的SQL控制台,使用以下SQL语句创建tbl_config表结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE `tbl_config` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '配置记录的唯一标识符',
`host` varchar(255) NOT NULL COMMENT '邮件服务器的主机地址',
`post` int(5) NOT NULL COMMENT '邮件服务器的端口号',
`username` varchar(100) NOT NULL COMMENT '登录邮件服务器的用户名,示例:1464935327@qq.com',
`password` varchar(100) NOT NULL COMMENT '登录邮件服务器的授权码',
`sender` varchar(255) NOT NULL COMMENT '邮件发送人的地址,示例:1464935327@qq.com',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录最后更新时间',
PRIMARY KEY(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='邮件配置表';


SELECT * FROM `tbl_config`
LIMIT 20;

​ 重写fun-alarm-email-send函数,新建email_config_service.py,代码如下:

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import json
from typing import Any, Dict
import pymysql
from custom_json_encoder import DateTimeEncoder

def get_db_connection():
return pymysql.connect(
host='pc-bp1f07b8tpl06q3qu.mysql.polardb.rds.aliyuncs.com',
port=3306,
user='yudou',
password='ZYDzyd917917',
db='db_message',
charset='utf8mb4',
)

async def create_config(data) -> Dict[str, Any]:
try:
with get_db_connection() as connection:
with connection.cursor() as cursor:
cursor.execute("""
INSERT INTO tbl_config (
host,
port,
username,
password,
sender)
VALUES (%s, %s, %s, %s, %s)
""", (data['host'], data['port'], data['username'], data['password'], data['sender']))
connection.commit()
return {"message": "Config created successfully"}
except Exception as e:
connection.rollback()
return {"error": str(e)}

async def read_config(data) -> Dict[str, Any]:
try:
with get_db_connection() as connection:
with connection.cursor() as cursor:
cursor.execute("SELECT * FROM tbl_config LIMIT 1")
result = cursor.fetchone()
if result:
config = {
"id": result[0],
"host": result[1],
"port": result[2],
"username": result[3],
"password": result[4],
"sender": result[5],
"create_time": result[6],
"update_time": result[7],
}
return json.loads(json.dumps(config, cls=DateTimeEncoder))
else:
return {"error": "No configuration found"}
except Exception as e:
return {"error": str(e)}

async def update_config(data) -> Dict[str, Any]:
try:
with get_db_connection as connection:
with connection.cursor() as cursor:
if not data.get("id"):
raise ValueError("id is required")

sql = "UPDATE tbl_config SET"
params = []

for key in ['host', 'port', 'username', 'password', 'sender']:
if data.get("key"):
sql += f" {key} = %s,"
params.append(data[key])

sql = sql.rstrip(",") + " WHERE id = %s"
params.append(data["id"])

if cursor.execute(sql, tuple(params)) == 0:
raise ValueError("Config not found")
connection.commit()
return {"message": "Config updated successfully"}
except Exception as e:
connection.rollback()
return {"error": str(e)}

async def delete_config(data) -> Dict[str, Any]:
try:
with get_db_connection() as connection:
with connection.cursor() as cursor:
if not data.get("id"):
raise ValueError("id is required")

if cursor.execute("DELETE FROM tbl_config WHERE id = %s", (data["id"],)) == 0:
raise ValueError("Config not found")
connection.commit()
return {"message": "Config deleted successfully"}
except Exception as e:
connection.rollback()
return {"error": str(e)}

​ 新建custom_json_encoder.py,代码如下:

1
2
3
4
5
6
7
8
import json
from datetime import datetime

class DateTimeEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetme):
return obj.isoformat()
return super().default(obj)

​ 修改app.py代码

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
import json as std_json
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from smtplib import SMTP

from sanic import Sanic,response
from sanic.response import json

from email_config_service import (create_config,
delete_config,
read_config,
update_config)

app = Sanic("EmailSender")

async def send_email(data):
email_config = await read_config(None)

msg = MIMEMultipart()
msg['From'] = email_config["sender"]
msg['To'] = data.get("recipient")
msg['Subject'] = data.get("subject")

msg.attach(MIMEText(data.get("body"), 'plain'))

server = SMTP(email_config["host"], email_config["port"])
server.starttls()
server.login(email_config["username"],email_config["password"])

server.send_message(msg)

server.quit()
return {"message": "Email sent successfully"}

@app.route("/invoke", methods=["POST"])

async def send_email_route(request):
action = request.json.get("action")
data = request.json.get("data",{})

actions = {
"create_config": create_config,
"read_config": read_config,
"update_config": update_config,
"delete_config": delete_config,
"send_email": send_email
}

func = actions.get(action)
if func:
return json(await func(data))
else:
return response.json({"error": "Invalid action"},status=400)

if __name__ == "__main__":
app.run(host="0.0.0.0", port=9000, dev=True)

​ 使用Apifox进行测试新增数据库数据

image-20250125215710922

​ 打开db_message库中的tbl_config表,可发现数据已存储

image-20250125215756245

​ 测试告警邮件

image-20250125215813828

image-20250125215827190

​ 基于sqlalchemy重写email_config_service.py

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import json
import urllib.parse
from typing import Any, Dict

from sqlalchemy import Column, DateTime, Integer, String, create_engine, func
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

from custom_json_encoder import DateTimeEncoder


Base = declarative_base()


class Config(Base):
__tablename__ = 'tbl_config'
id = Column(Integer, primary_key=True, autoincrement=True)
host = Column(String(255))
port = Column(Integer)
username = Column(String(255))
password = Column(String(255))
sender = Column(String(255))
create_time = Column(DateTime, server_default=func.now())
update_time = Column(DateTime, server_default=func.now(), onupdate=func.now())


def get_db_connection():
DB_HOST = 'pc-bp1f07b8tpl06q3qu.mysql.polardb.rds.aliyuncs.com'
DB_PORT = 3306
DB_USER = 'yudou'
DB_PASSWORD = 'ZYDzyd917917'
DB_NAME = 'db_message'
encoded_password = urllib.parse.quote_plus(DB_PASSWORD)
DATABASE_URL = f'mysql+pymysql://{DB_USER}:{encoded_password}@{DB_HOST}:{DB_PORT}/{DB_NAME}'
engine = create_engine(DATABASE_URL, echo=True)
Session = sessionmaker(bind=engine)
return Session()


async def create_config(data) -> Dict[str, Any]:
"""创建配置"""
with get_db_connection() as session:
try:
config = Config(
host=data['host'],
port=data['port'],
username=data['username'],
password=data['password'],
sender=data['sender']
)
session.add(config)
session.commit()
return {"message": "Config created successfully"}
except SQLAlchemyError as e:
session.rollback()
return {"error": str(e)}


async def read_config(data) -> Dict[str, Any]:
"""读取配置,此处仅做最简单的查询,实际应用中需要根据业务需求进行查询"""
with get_db_connection() as session:
try:
config = session.query(Config).first()
if config:
config_dict = {
"id": config.id,
"host": config.host,
"port": config.port,
"username": config.username,
"password": config.password,
"sender": config.sender,
"create_time": config.create_time,
"update_time": config.update_time
}
encoded_result = json.dumps(config_dict, cls=DateTimeEncoder)
return json.loads(encoded_result)
else:
return {"error": "No configuration found"}
except SQLAlchemyError as e:
session.rollback()
return {"error": str(e)}


async def update_config(data) -> Dict[str, Any]:
"""更新配置"""
with get_db_connection() as session:
try:
config = Config(
host=data['host'],
port=data['port'],
username=data['username'],
password=data['password'],
sender=data['sender']
)
session.add(config)
session.commit()
return {"message": "Config updated successfully"}
except SQLAlchemyError as e:
session.rollback()
return {"error": str(e)}


async def delete_config(data) -> Dict[str, Any]:
"""删除配置"""
with get_db_connection() as session:
try:
config_id = data.get('id', None)
if not config_id:
raise ValueError("id is required")
config = session.query(Config).filter(Config.id == config_id).first()
if not config:
raise ValueError("Config not found")
session.delete(config)
session.commit()
return {"message": "Config deleted successfully"}
except SQLAlchemyError as e:
session.rollback()
return {"error": str(e)}

​ 同上进行测试,结果应该不变

image-20250125215934920

7.3 遇到的主要问题、解决思路和收获

​ 该实验暂未遇到问题

8. 基于阿里云Serverless应用引擎的服务部署迁移

8.1 实验目标与相关知识技能

• 熟练运用云效Codeup代码仓库的使用

• 理解并掌握阿里云Serverless应用引擎部署应用的方法

8.2 实验步骤与对应成果展示

📂:user_admin

📂:device_control

​ 将以上用户管理服务代码与设备管理服务代码上传到云效Codeup

image-20250125224215016

​ 修订user-admin中application.yml的代码。(1214行)中MySQL数据库配置更改为PolarDB MySQL的内网访问地址及对应的用户名密码。(4244行)中Redis数据库配置为Redis数据库配置信息

image-20250125224225824

image-20250125224243501

​ 打开device的代码,修改server-config.ini文件中MySQL数据库配置为PolarDB MySQL的内网访问地址及其对应的用户名密码。callback_url替换为CloudFlow的内网触发地址

image-20250125224256334

​ 进入阿里云DMS控制台,在PolarDB MySQL数据库中创建db_user_admin数据库,点击左侧常用功能-数据导入,选择批量数据导入,数据库选择db_user_admin,文件类型为SQL脚本,最后上传deviced_control的init.sql文件

image-20250125224308225

​ 修改fun-temperature-and-humidity-data-uploadFC函数

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
# 原来温湿度数据上报FC的res数据结构如下
res = {
"status": 1 if t_out_flag or h_out_flag else 0,
"message": "异常" if t_out_flag or h_out_flag else "正常",
"data": {
# 需要自定转换成收件人邮箱地址
"recipient": "1464935327@qq.com",
"subject": "告警邮件 - 温湿度异常",
"body": "email_body"
}
}
if t_out_flag or h_out_flag else None

# 应该修订为以下格式
res = {
"status": 1 if t_out_flag or h_out_flag else 0,
"message": "异常" if t_out_flag or h_out_flag else "正常",
"data": {
"action": "send_email",
"data": {
# 需要自定转换成收件人邮箱地址
"recipient": "1464935327@qq.com",
"subject": "告警邮件 - 温湿度异常",
"body": "email_body"
}
}
}
if t_out_flag or h_out_flag else None

​ 进入SAE控制台创建Web应用,VPC与vSwitch不变,代码仓库类型选择Codeup,仓库选择serverless/user-admin

 启动命令如下设置:
1
java -jar target/user_admin-2024.jar

image-20250125222818655

​ 使用Apifox对User-Admin登录接口(POST /login/account)发起请求

​ 请求实例:

1
2
3
4
5
POST https://cn-hanger-admin-hvryfblqme.cn-hangzhou.sae.run/login/account
{
"account": "yudou",
"password": "123456"
}

​ 响应示例:

1
2
3
4
5
{
"status": 1,
"message": "操作执行成功",
"data": "65086510285720102"
}

​ 同理,基于SAE部署Device Control服务。设置启动命令为如下命令:

1
python server.py

image-20250125223132687

​ 使用Apifox对Device-Control的设备添加接口(POST /devices)发起请求

​ 请求示例:

1
2
3
4
5
6
7
8
9
POST https://cn-hang-control-rcuipmgcxu.cn-hangzhou.sae.run
[
{
"name": "温湿度设备",
"type": "null",
"sn": "SNTAH",
"passwd": "123456"
}
]

​ 响应示例:

1
2
3
4
{
"status": 1,
"message": "设备信息添加成功!"
}

📂:device_client

​ 以上为模拟设备客户端代码示例,设备客户端用于模拟真实设备,构造温湿度数据并定期通过WebSocket通道上传数据至Device-Control

1
2
3
4
5
6
7
8
9
10
11
12
13
# 第一个参数为Device - Control应用的地址(注意:不需要https://,只需要域名) 
# 第二个参数是前面步骤创建的设备sn
# 第三个参数是该设备的密码
(venv)$ python client.py ws://<your_sae_domain>/devices/auth/ws SNT AH 123456

INFO:root:成功连接到 ws://<your_sae_domain>/devices/auth/ws? sn=SNT AH&passwd=123456

REC:身份验证成功
REC:{"sn":"SNT AH","temperature":26.15,"humidity":31.97}
REC:{"sn":"SNT AH","temperature":25.25,"humidity":31.43}
REC:{"sn":"SNT AH","temperature":26.85,"humidity":30.08}
REC:{"sn":"SNT AH","temperature":35.8,"humidity":27.67}
REC:{"sn":"SNT AH","temperature":25.46,"humidity":30.67}

​ 向Device-Control发送温湿度数据后,可以自行查看CloudFlow的云效情况,若CloudFlow正常工作,在温湿度超出阈值时将向指定的管理员邮箱发生告警邮件

8.3 遇到的主要问题、解决思路和收获

(❌)该实验并未完成,在测试两个SAE服务时遇到无法解决的bug

image-20250125223842250

​ 上图为user-admin服务的日志,显示无法连接到PolarDB MySQL,但我反复确认各个阿里云产品的VPC和vSwitch都是一样的

image-20250125223910346 image-20250125223919940

​ 以上为Apifox测试时的报错,第一反应是Redis和PolarDB MySQL地址填写错误,检查Codeup内代码发现并未出错。再依次检查各个云产品的VPC,vSwitch,以及白名单,也未修复该bug。最后尝试删除user-adminWeb服务重做,还是出现相同bug

9. 基于Nacos的服务注册与配置部署

9.1 实验目标与相关知识技能

• 理解并掌握微服务系统中,服务注册与发现机制的基本原理,相关中间件的系统定位和功能

• 理解并掌握服务注册与发现中间件Nacos的具体功能与应用模式

9.2 实验步骤与对应成果展示

📂:user_admin

📂:device_control

​ 创建ACR镜像仓库,使用Docker拉取Nacos镜像,并上传至阿里云容器镜像仓库ACR

1
2
3
4
5
docker pull nacos

docker login --username=鱼豆YuDou crpi-n4sctgr4gzd05xye.cn-hangzhou.personal.cr.aliyuncs.com
docker tag [ImageId] crpi-n4sctgr4gzd05xye.cn-hangzhou.personal.cr.aliyuncs.com/exp_yd/nacos:[镜像版本号]
docker push crpi-n4sctgr4gzd05xye.cn-hangzhou.personal.cr.aliyuncs.com/exp_yd/nacos:[镜像版本号]

​ 在SAE控制台创建微服务,应用名称为Nacos,保证VPC,vSwitch,安全组与先前实验保持一致,添加环境变量MODE=standalone,部署Nacos

​ 开通CLB负载均衡后,用浏览器访问http://<CLB公网IP>:8848/nacos,即可观察Nacos UI界面

image-20250125225940154

​ 在user-admin包中找到pom.xml,添加Nacos的依赖坐标

1
2
3
4
5
6
7
8
<dependencies>
...... (省略之前的依赖)
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>nacos-discovery-spring-boot-starter</artifactId>
<version>0.3.0-RC</version>
</dependency>
</dependencies>

​ 修改src/main/resources/application.yml文件,添加Nacos配置,实现自动注册

1
2
3
4
5
6
7
8
9
nacos:
discovery:
server-addr: 172.28.97.37:8848
auto-register: true
register:
service-name: user-admin
ip: https://cn-hanger-admin-hvryfblqme.cn-hangzhou-vpc.sae.run
ephemeral: false
healthy: true

​ 修改后推送代码至Codeup触发自动部署,进入Nacos UI界面可以看到user-admin服务已注册

image-20250125230138535

image-20250125230228281

​ 修改Device-Control中的server.py代码,实现应用启动时自动向Nacos注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@app.listener('before_server_start')
async def register_nacos(app, loop):
try:
import nacos

# 自行替换为 Nacos 内网ip地址
SERVER_ADDRESSES = "http://172.28.97.37:8848"
# Nacos注册的信息
SERVICE_NAME = "device-control"
# 自行替换为SAE中获取的内网访问地址,只需要域名,无需携带https://
IP = "https://cn-hang-control-rcuipmgcxu.cn-hangzhou.sae.run"
PORT = 9000

client = nacos.NacosClient(SERVER_ADDRESSES)
# 向 Nacos 注册实例
client.add_naming_instance(SERVICE_NAME, IP, PORT, ephemeral=False, healthy=True)
print(f"向 Nacos 注册成功,注册信息为: {SERVICE_NAME}, {IP}, {PORT}")
except Exception as e:
raise RuntimeError(f"向 Nacos 注册失败,错误信息: {e}")

​ 同时修改requirements.txt文件,添加一行nacos-python-sdk依赖

1
2
3
4
5
sanic~=24.6.0
pymysql~=1.1.1
websockets~=13.0.1
requests~=2.32.3
nacos-python-sdk==0.2

​ 修改代码后推送至Codeup,触发自动部署。打开Nacos UI界面即可验证是否注册

image-20250125230622890

image-20250125230658403

​ 在部署Nacos SAE应用实例的终端环境下执行以下命令,实现邮件发送FC函数计算的静态注册

1
curl --location --request POST 'http://127.0.0.1:8848/nacos/v2/cs/config?group=DEFAULT_GROUP&dataId=serverless.fc.address.email_send&content=https://fun-alaail-send-ylnkfxeuva.cn-hangzhou-vpc.fcapp.run' 

image-20250125230731235

​ 配置完成后观察Nacos UI界面,可发现send_email配置已成功向Nacos发布

image-20250125230838394

​ 同理,将CloudFlow工作流的触发接口以静态注册的方式进行

1
2
3
4
5
curl --location --request POST
'http://127.0.0.1:8848/nacos/v2/cs/config?
group=DEFAULT_GROUP&
dataId=serverless.fc.address.cloudflare&
content=https://1237899087648526.eventbridge.cn-hangzhou-vpc.aliyuncs.com/webhook/putEvents?token=1ced1dfd257949609f8cd8861b0a914bba5389a992f04ac8a17616ba7e360318f463ec2f6fa14a79b4406e8ed277e4eea86fe72c7e384b82a8fa3825705a6a6d'

image-20250125231030892

image-20250125231042340

​ 构建用户服务FC,同样配置相同的VPC、vSwitch,代码如下:

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
from sanic import Sanic
from sanic.response import json, text
import requests

app = Sanic("UserAdminFC")

# Nacos配置
NACOS_URL = "http://172.28.97.37:8848" # 自行替换为你的nacos内网地址
SERVICE_NAME = "user-admin"

# 在全局变量中存储user - admin服务的实例URL信息
service_instance_url = None

def get_service_instance_url():
global service_instance_url
if service_instance_url:
return service_instance_url

params = {
"serviceName": SERVICE_NAME,
}
response = requests.get(
f"{NACOS_URL}/nacos/v2/ns/instance/list",
params=params)
if response.status_code == 200:
data = data = response.json().get("data",
{'hosts': []}).get('hosts')
if data and data[0]:
service_instance_url = data[0].get('ip')
return service_instance_url
return None

@app.route("/register/account",methods=["POST"])
async def register_account(request):
"""用户注册"""
instance_url = get_service_instance_url()
if instance_url:
url = f"http://{instance_url}/register/account"
response = requests.post(url, json=request.json)
return json(response.json())
else:
return text("Service instance not found", status=404)

@app.listener('before_server_start')
async def setup(app, loop):
"""在启动时获取服务实例信息"""
instance_url = get_service_instance_url()
if instance_url is None:
raise RuntimeError("Service instance not found")
print("成功获取到服务实例地址:" + instance_url)

if __name__ == "__main__":
app.run(host="0.0.0.0", port=9000)

​ 使用Apifox对User-Admin FC的用户注册接口(POST /register/account)发起请求,接口请求示例如下:

1
2
3
4
5
POST https://useradminfc-zovpsrnfjk.cn-hangzhou.fcapp.run/register/account
{
"account": "yudou",
"password": "123456"
}

​ 响应示例:

1
2
3
4
5
{
"status": 1,
"message": "操作执行成功",
"data": "65086872090576958"
}

​ 构建设备管理FC代码:

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
import requests
from sanic import Sanic
from sanic.response import json, text

app = Sanic("DeviceControlFC")

# Nacos配置
NACOS_URL = "http://172.28.97.37:8848" # 自行替换为你的nacos内网地址
SERVICE_NAME = "device-control" # 服务名称

# 在全局变量中存储device - control服务的实例URL信息
service_instance_url = None

def get_service_instance_url():
global service_instance_url
if service_instance_url:
return service_instance_url

params = {
"serviceName": SERVICE_NAME,
}
response = requests.get(
f"{NACOS_URL}/nacos/v2/ns/instance/list",
params=params)
if response.status_code == 200:
data = response.json().get("data", {'hosts': []}).get('hosts')
if data and data[0]:
service_instance_url = data[0].get('ip')
return service_instance_url
return None

@app.post("/devices")
async def create_device(request):
"""设备添加"""
instance_url = get_service_instance_url()
if instance_url:
url = f"http://{instance_url}/devices"
response = requests.post(url, json=request.json)
return json(response.json())
else:
return text("Service instance not found", status=404)

@app.get("/devices", ignore_body=False)
async def query_device(request):
"""设备查询"""
instance_url = get_service_instance_url()
if instance_url:
url = f"http://{instance_url}/devices"
headers = {
"Authorization": request.headers.get("Authorization")
}
response = requests.get(url, json=request.json, headers=headers)
return json(response.json())
else:
return text("Service instance not found", status=404)

@app.listener('before_server_start')
async def setup(app, loop):
"""在启动时获取服务实例信息"""
instance_url = get_service_instance_url()
if instance_url is None:
raise RuntimeError("Service instance not found")
print("成功获取到服务实例地址:" + instance_url)

if __name__ == "__main__":
app.run(host="0.0.0.0", port=9000)

​ 同理,用Apifox对Device-Control FC的设备添加接口(POST /devices)发起请求,接口请求示例如下:

1
2
3
4
5
6
7
8
9
POST https://device-ontrolfc-rrubtancws.cn-hangzhou.fcapp.run/devices
[
{
"name": "温湿度设备",
"type": "null",
"sn": "SNTAH",
"passwd": "123456"
}
]

​ 接口响应示例:

1
2
3
4
{
"status": 1,
"message": "获取设备添加成功!"
}

​ 对设备查询接口(GET /devices)发起请求,接口请求示例如下:

1
GET https://device-ontrolfc-rrubtancws.cn-hangzhou.fcapp.run/devices

​ 接口响应示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"status": 1,
"message": "获取设备信息成功!",
"data": [
{
"id": "1",
"name": "温湿度设备",
"type": "null",
"sn": "SNTAH",
"passwd": "123456"
},
...
]
}

​ 构建温湿度业务FC,示例代码如下:

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
import requests
from sanic import Sanic, response
from sanic.request import Request

app = Sanic("BizFC")

# Nacos配置
NACOS_URL = "http://172.28.97.37:8848"

def get_services_from_nacos():
"""从Nacos获取服务列表"""
response = requests.get(f"{NACOS_URL}/nacos/v2/ns/service/list")
data = response.json()
return data['data']['services']

def get_instance_from_nacos(service_name):
"""从Nacos获取指定服务的实例IP"""
response = requests.get(f"{NACOS_URL}/nacos/v2/ns/instance/list?serviceName={service_name}")
data = response.json()
hosts = data['data'].get('hosts', [])
if len(hosts) > 0:
return data['data']['hosts'][0]['ip']
return ""

def get_config_from_nacos(data_id, group):
"""从Nacos获取指定配置"""
response = requests.get(f"{NACOS_URL}/nacos/v2/cs/config?dataId={data_id}&group={group}")
data = response.json()
if data['data']:
return data['data']
return ""

@app.get("/getServices")
async def get_services(request: Request):
"""获取服务及其实例IP,并返回JSON格式的服务映射"""
services = get_services_from_nacos()
service_map = {}
for service in services:
instance_ip = get_instance_from_nacos(service)
service_map[service.replace('-', '_')] = instance_ip
# 从静态配置中读取
static_services = {
"message_url": get_config_from_nacos("serverless.fc.address.email_send", "DEFAULT_GROUP"),
"cloudflow_url": get_config_from_nacos("serverless.fc.address.cloudflow", "DEFAULT_GROUP")
}
service_map.update(static_services)
return response.json(service_map)

if __name__ == "__main__":
app.run(host="0.0.0.0", port=9000)

​ 使用Apifox对Biz FC的获取的所有服务地址接口(GET /getServices)发起请求,接口请求示例如下:

1
GET  https://bizfc-cpklnjfjoq.cn-hangzhou.fcapp.run/getServices

​ 接口响应示例:

1
2
3
4
5
6
{
"device_control": "https://cn-hang-control-rcuipmgcxu.cn-hangzhou.sae.run",
"user_admin": "https://cn-hanger-admin-hvryfblqme.cn-hangzhou.sae.run",
"message_url": "config data not exist",
"cloudflow_url": "https://1237899087648526.eventbridge.cn-hangzhou.aliyuncs.com/webhook/putEvents?token=1ced1dfd257949609f8cd8861b0a914bba5389a992f04ac8a17616ba7e360318f463ec2f6fa14a79b4406e8ed277e4eea86fe72c7e384b82a8fa3825705a6a6d"
}

image-20250125233826811

9.3 遇到的主要问题、解决思路和收获

(❌)本实验未完成,Apifox测试用户服务FC和设备管理FC时返回的响应是错误的

image-20250125234026794

image-20250125234035534

​ 同样的,在出现错误后检查了一遍所有云产品VPC、vSwitch配置。发现没问题后选择在函数计算FC云端上进行测试。在测试user-admin FC和device-control FC时,从日志输出中看不出问题所在,于是我开通了实时日志功能

image-20250125234102110

image-20250125234112269

image-20250125234121706

​ 发现在日志中好像整个请求流程正常,日志打印“成功获取到服务器实例地址”,说明成功获取服务实例信息。且它的在线测试报错为404,与我在Apifox上的HTTP码500并不同,因此怀疑是返回响应的途中出现问题。但我无从下手debug了,询问阿里云在线客服也未能解决

  • Title: 系统中间件与云虚拟化实践
  • Author: YuDou
  • Created at : 2025-01-13 00:02:29
  • Updated at : 2025-01-26 00:17:58
  • Link: https://sweetyudou.github.io/2025/01/13/yjs/
  • License: All Rights Reserved © YuDou