Debian 12 配置 DNS 教程

  1. 使用 DHCP 的情况下固定 DNS(Debian 12 推荐)
  2. 传统在 /etc/network/interfaces 里修改静态 IP 和 DNS

我会一步步说明操作原因、命令、验证方法。


Debian 12 配置 DNS 教程

前提检查

  1. 查看当前网络配置:
1
2
ip addr show
cat /etc/network/interfaces

你现在 /etc/network/interfaces 内容类似:

1
2
3
4
5
6
7
source /etc/network/interfaces.d/*

auto lo
iface lo inet loopback

auto ens17
iface ens17 inet dhcp

说明 ens17 是 DHCP 获取 IP。

  1. 检查当前 DNS:
1
cat /etc/resolv.conf

如果看到:

1
2
nameserver 114.114.114.114
nameserver 8.8.8.8

说明 DHCP 覆盖了 DNS。


方法一:DHCP 下固定 DNS(推荐)

步骤 1:修复主机名解析(防止 sudo 报错)

1
sudo nano /etc/hosts

添加或确认:

1
2
127.0.0.1   localhost
127.0.1.1 ser124183219857

保存后退出。

验证:

1
sudo echo "hosts OK"

不应该再有 unable to resolve host 错误。


步骤 2:编辑 DHCP 客户端配置

1
sudo nano /etc/dhcp/dhclient.conf

在文件末尾添加:

1
supersede domain-name-servers 8.8.8.8, 1.1.1.1;

supersede 表示强制覆盖 DHCP 服务器提供的 DNS


步骤 3:重新申请 IP

1
2
sudo dhclient -r ens17      # 释放当前 IP
sudo dhclient ens17 # 重新获取 IP 并应用 DNS

步骤 4:验证 DNS 是否生效

1
cat /etc/resolv.conf

输出应为:

1
2
nameserver 8.8.8.8
nameserver 1.1.1.1

测试网络:

1
ping -c 3 google.com

如果能收到响应,说明 DNS 配置成功。


方法二:静态 IP 下在 /etc/network/interfaces 里配置 DNS

适合不使用 DHCP,而是手动指定 IP 的情况。

  1. 编辑 /etc/network/interfaces
1
sudo nano /etc/network/interfaces

修改为:

1
2
3
4
5
6
7
8
9
auto lo
iface lo inet loopback

auto ens17
iface ens17 inet static
address 10.0.0.100
netmask 255.255.255.0
gateway 10.0.0.1
dns-nameservers 8.8.8.8 1.1.1.1
  • address:你的静态 IP
  • netmask:子网掩码
  • gateway:网关
  • dns-nameservers:指定 DNS(可以写多个,用空格分隔)
  1. 重启网络:
1
sudo systemctl restart networking

或者:

1
sudo ifdown ens17 && sudo ifup ens17
  1. 验证:
1
2
cat /etc/resolv.conf
ping -c 3 google.com

注意:如果 DHCP 同时在运行,可能会覆盖 DNS,这种方式适合纯静态 IP 配置。


小结

方法 适用场景 配置文件 备注
DHCP 下固定 DNS ens17 使用 DHCP /etc/dhcp/dhclient.conf 使用 supersede domain-name-servers,防止 /etc/resolv.conf 被覆盖
静态 IP 配置 DNS 手动分配 IP /etc/network/interfaces 直接在 dns-nameservers 设置 DNS

✅ 建议:如果服务器使用 DHCP,用 dhclient.conf 方法最稳;如果服务器固定 IP,可以直接在 /etc/network/interfaces 配置。


如果你愿意,我可以写一个完整命令脚本,一次性修复主机名、固定 ens17 DNS 为 8.8.8.8 和 1.1.1.1,开机自动生效,无需手动操作。

你希望我写吗?

Django-HTTPS-CSRF问题解决记录

Django HTTPS CSRF 验证失败问题解决记录

📋 问题描述

访问 https://test.bismih520.com 时出现 403 禁止访问错误,错误信息显示:

1
2
3
4
CSRF验证失败. 请求被中断.

Reason given for failure:
Origin checking failed - https://test.bismih520.com does not match any trusted origins.

🔍 问题原因分析

根本原因

  1. Django 4.0+ 版本变更

    • Django 4.0 引入了更严格的 CSRF 保护机制
    • 使用 HTTPS 时,必须明确配置 CSRF_TRUSTED_ORIGINS 来允许来自特定域的请求
    • 如果不配置,Django 会拒绝所有来自 HTTPS 域的 POST 请求
  2. 反向代理配置缺失

    • 应用部署在 Traefik 反向代理后面
    • Django 需要通过 SECURE_PROXY_SSL_HEADER 来识别来自代理的 HTTPS 请求
    • 否则 Django 可能无法正确识别请求的协议类型

错误信息解读

1
Origin checking failed - https://test.bismih520.com does not match any trusted origins.

这表明 Django 的 CSRF 中间件检查请求的 Origin 头时,发现 https://test.bismih520.com 不在受信任的源列表中。

✅ 解决方案

步骤 1: 修改 Django 配置文件

编辑 myproject/settings.py,在 ALLOWED_HOSTS 配置后添加以下内容:

1
2
3
4
5
6
7
ALLOWED_HOSTS = ['*']

# CSRF settings for HTTPS
CSRF_TRUSTED_ORIGINS = ['https://test.bismih520.com']

# Security settings for reverse proxy (Traefik)
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

配置说明

  • **CSRF_TRUSTED_ORIGINS**:

    • 指定允许的 HTTPS 源域名
    • 必须包含完整的协议和域名(https://test.bismih520.com
    • 可以添加多个域名,例如:
      1
      2
      3
      4
      CSRF_TRUSTED_ORIGINS = [
      'https://test.bismih520.com',
      'https://www.bismih520.com',
      ]
  • **SECURE_PROXY_SSL_HEADER**:

    • 告诉 Django 如何从反向代理的 HTTP 头中识别 HTTPS 请求
    • Traefik 会自动设置 X-Forwarded-Proto: https
    • Django 通过这个头来判断请求是否使用 HTTPS

步骤 2: 重新构建 Docker 镜像

1
2
cd /root/code
docker build --no-cache -t web-app:latest .

注意:使用 --no-cache 确保使用最新的配置文件,避免缓存导致的问题。

步骤 3: 更新 Docker Swarm 服务

1
2
3
4
5
# 更新服务使用新镜像
docker service update --image web-app:latest app_web

# 如果更新后配置仍未生效,强制重启服务
docker service update --force app_web

步骤 4: 验证配置

检查容器内的配置文件

1
2
# 查看容器内的 settings.py
docker exec $(docker ps -q -f name=app_web | head -1) cat /app/myproject/settings.py | grep -A 5 "CSRF_TRUSTED_ORIGINS"

验证 Django 配置加载

1
2
3
4
5
6
7
8
9
10
# 检查 Django 是否正确加载了配置
docker exec $(docker ps -q -f name=app_web | head -1) python -c "
import django
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
django.setup()
from django.conf import settings
print('CSRF_TRUSTED_ORIGINS:', settings.CSRF_TRUSTED_ORIGINS)
print('SECURE_PROXY_SSL_HEADER:', settings.SECURE_PROXY_SSL_HEADER)
"

预期输出

1
2
CSRF_TRUSTED_ORIGINS: ['https://test.bismih520.com']
SECURE_PROXY_SSL_HEADER: ('HTTP_X_FORWARDED_PROTO', 'https')

🔧 完整修复流程总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 1. 修改配置文件(已在 settings.py 中添加配置)

# 2. 重新构建镜像
cd /root/code
docker build --no-cache -t web-app:latest .

# 3. 更新服务
docker service update --image web-app:latest app_web

# 4. 如果配置未生效,强制重启
docker service update --force app_web

# 5. 等待服务启动(约 10-30 秒)
sleep 10

# 6. 验证配置
docker exec $(docker ps -q -f name=app_web | head -1) python -c "
import django
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
django.setup()
from django.conf import settings
print('CSRF_TRUSTED_ORIGINS:', settings.CSRF_TRUSTED_ORIGINS)
"

📝 相关配置文件

settings.py 关键配置片段

1
2
3
4
5
6
7
8
9
# 位置:myproject/settings.py

ALLOWED_HOSTS = ['*']

# CSRF settings for HTTPS
CSRF_TRUSTED_ORIGINS = ['https://test.bismih520.com']

# Security settings for reverse proxy (Traefik)
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

⚠️ 注意事项

  1. 多域名支持

    • 如果有多个域名,需要在 CSRF_TRUSTED_ORIGINS 中添加所有域名
    • 例如:CSRF_TRUSTED_ORIGINS = ['https://test.bismih520.com', 'https://www.bismih520.com']
  2. 环境变量配置(可选):

    • 生产环境建议使用环境变量来配置,避免硬编码:
      1
      2
      import os
      CSRF_TRUSTED_ORIGINS = os.environ.get('CSRF_TRUSTED_ORIGINS', '').split(',')
  3. 浏览器缓存

    • 修复后如果仍有问题,请清除浏览器缓存和 Cookie
    • 或使用无痕模式访问
  4. Traefik 配置

    • Traefik 默认会自动设置 X-Forwarded-Proto
    • 无需额外配置 Traefik
  5. DEBUG 模式

    • 当前配置中 DEBUG = True,生产环境应设置为 False
    • 修改后需要重新部署

🐛 故障排查

如果配置仍未生效

  1. 检查镜像是否更新

    1
    docker run --rm --entrypoint cat web-app:latest /app/myproject/settings.py | grep CSRF_TRUSTED_ORIGINS
  2. 检查服务状态

    1
    2
    docker service ps app_web
    docker service logs app_web --tail 50
  3. 强制重新部署

    1
    2
    3
    4
    # 删除并重新部署服务栈
    docker stack rm app
    sleep 5
    docker stack deploy -c docker-compose.yml app

查看 Traefik 日志

1
docker service logs app_traefik --tail 50

📚 参考资料

✅ 验证结果

修复完成后,访问 https://test.bismih520.com 应该:

  • ✅ 不再出现 CSRF 验证失败错误
  • ✅ 可以正常提交表单(登录、注册等)
  • ✅ POST 请求正常工作

记录时间:2024年11月16日
Django 版本:4.2.7
部署方式:Docker Swarm + Traefik
域名:test.bismih520.com

Docker Swarm 部署教程(2025)

🐳 Docker Swarm 部署教程(2025)

Docker Swarm 是 Docker 官方的集群编排工具,比 Kubernetes 简单得多,但依然能实现高可用、负载均衡、滚动更新。


📌 一、准备工作

1. 服务器要求

至少准备 2 台以上服务器

  • 1 台 Manager
  • 1~N 台 Worker

系统推荐:Ubuntu 20/22、Debian 12、Rocky Linux 都 OK。

2. 服务器安装 Docker

1
2
3
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker

查看版本:

1
2
docker version


📌 二、初始化 Swarm 集群(Manager 节点)

在你选的 Manager 服务器执行:

1
2
docker swarm init --advertise-addr <manager-ip>

例如:

1
2
docker swarm init --advertise-addr 192.168.1.10

执行成功后会输出 Worker 加入命令,例如:

1
2
docker swarm join --token SWMTKN-1-xxxx 192.168.1.10:2377


📌 三、加入 Worker 节点

在所有 Worker 节点执行刚才生成的命令,例如:

1
2
docker swarm join --token SWMTKN-1-abc123 192.168.1.10:2377

查看节点状态(在 Manager 上):

1
2
docker node ls


📌 四、部署你的第一个 Swarm 服务

示例部署一个负载均衡网站 Nginx:

1
2
3
4
5
6
docker service create \
--name myweb \
--replicas 3 \
-p 80:80 \
nginx:latest

检查服务是否成功:

1
2
3
docker service ls
docker service ps myweb

访问任意 Worker 或 Manager 的 IP:80,都能看到 nginx 首页。

Swarm 自带负载均衡,会自动轮询。


📌 五、滚动更新(不宕机)

更新镜像:

1
2
docker service update --image nginx:alpine myweb

更新副本数:

1
2
docker service scale myweb=5


📌 六、Stack 部署(最推荐)

Swarm 的最佳方式是使用 docker-compose.yml 文件部署整个应用。

1. 创建 stack 文件

创建一个 stack.yml

1
2
3
4
5
6
7
8
9
10
11
12
version: "3.9"

services:
web:
image: nginx
ports:
- "80:80"
deploy:
replicas: 3
restart_policy:
condition: on-failure

2. 部署 stack

1
2
docker stack deploy -c stack.yml mystack

查看:

1
2
3
4
docker stack ls
docker stack ps mystack
docker stack services mystack

删除 stack:

1
2
docker stack rm mystack


📌 七、常用 Swarm 命令总结

节点

1
2
3
4
5
docker node ls
docker node inspect <node>
docker node rm <node>
docker node update --availability drain <node>

服务

1
2
3
4
docker service ls
docker service ps myweb
docker service logs -f myweb

Stack

1
2
3
docker stack deploy -c stack.yml mystack
docker stack rm mystack


📌 八、生产环境建议

功能 推荐方案
数据持久化 NFS / GlusterFS / Local volume
服务发现 内置 DNS(自动)
负载均衡 Swarm 自带(VIP / IPVS)
自动重启 使用 deploy.restart_policy
更新不宕机 update_config 滚动更新

例如一个更高级的 stack 配置:

1
2
3
4
5
6
7
8
deploy:
replicas: 4
update_config:
parallelism: 1
delay: 5s
restart_policy:
condition: on-failure


📌 九、如何卸载 Swarm

在每个 Worker 上:

1
2
docker swarm leave

在 Manager 上:

1
2
docker swarm leave --force

Linux Locale 警告问题解决教程

Linux Locale 警告问题解决教程

问题现象

在运行bash脚本或命令时,出现以下警告信息:

1
bash: warning: setlocale: LC_ALL: cannot change locale (zh_CN.UTF-8)

问题原因

1. 根本原因

系统环境变量中设置了 LC_ALL=zh_CN.UTF-8,但系统中并没有安装和生成这个locale,导致bash无法设置该locale,从而产生警告。

2. 为什么会出现这个问题

  • 环境变量配置:系统或用户的配置文件(如 /etc/default/locale/etc/environment~/.bashrc 等)中设置了 LC_ALL=zh_CN.UTF-8
  • Locale未安装:虽然配置了中文locale,但系统并没有实际生成这个locale文件
  • Locale生成机制:Linux系统为了节省空间,默认只生成部分常用locale,需要手动启用并生成

3. 影响

  • 功能影响:警告不影响系统基本功能,但会在每次运行bash命令时显示,影响输出可读性
  • 脚本影响:可能影响脚本输出的美观性,特别是在自动化脚本中

解决思路

有两种解决方案:

方案一:安装缺失的locale(推荐)

如果确实需要使用中文locale,应该安装并生成 zh_CN.UTF-8 locale。

优点

  • 彻底解决问题
  • 支持中文环境
  • 符合系统配置意图

缺点

  • 需要生成locale文件(占用少量空间)

方案二:修改系统配置使用已安装的locale

如果不需要中文locale,可以修改系统配置,使用已安装的locale(如 en_US.UTF-8)。

优点

  • 不需要生成新locale
  • 配置简单

缺点

  • 如果后续需要中文支持,还需要重新配置

解决方案

方案一:安装 zh_CN.UTF-8 Locale(推荐)

步骤1:检查当前locale状态

1
2
3
4
5
6
7
8
# 查看当前locale配置
locale

# 查看已安装的locale
locale -a

# 检查zh_CN.UTF-8是否存在
locale -a | grep zh_CN

步骤2:编辑locale生成配置文件

1
2
3
4
# 编辑 /etc/locale.gen 文件
sudo nano /etc/locale.gen
# 或
sudo vi /etc/locale.gen

找到以下行(通常在文件中搜索 zh_CN.UTF-8):

1
# zh_CN.UTF-8 UTF-8

取消注释(删除 # 号):

1
zh_CN.UTF-8 UTF-8

或者使用sed命令快速修改

1
sudo sed -i 's/^# zh_CN.UTF-8 UTF-8/zh_CN.UTF-8 UTF-8/' /etc/locale.gen

步骤3:生成locale

1
2
# 生成locale(可能需要一些时间)
sudo locale-gen

步骤4:验证安装

1
2
3
4
5
6
# 检查zh_CN.UTF-8是否已生成
locale -a | grep zh_CN

# 测试设置locale
export LC_ALL=zh_CN.UTF-8
locale

如果不再出现警告,说明安装成功。

步骤5:更新系统默认配置(可选)

如果需要系统默认使用中文locale,可以更新 /etc/default/locale

1
sudo nano /etc/default/locale

添加或修改为:

1
2
LANG="zh_CN.UTF-8"
LC_ALL="zh_CN.UTF-8"

然后运行:

1
sudo update-locale

方案二:修改系统配置使用已安装的locale

步骤1:检查已安装的locale

1
locale -a

常见已安装的locale:

  • en_US.UTF-8en_US.utf8
  • C.UTF-8
  • POSIX

步骤2:修改系统locale配置

编辑 /etc/default/locale

1
sudo nano /etc/default/locale

修改为:

1
2
LANG="en_US.UTF-8"
LC_ALL=""

说明

  • LANG 设置默认语言
  • LC_ALL="" 表示不强制设置,让系统使用 LANG 的值

步骤3:更新环境变量配置

编辑 /etc/environment

1
sudo nano /etc/environment

添加或修改为:

1
2
LC_ALL=""
LANG="en_US.UTF-8"

步骤4:应用更改

1
2
3
4
# 更新locale配置
sudo update-locale

# 或者重新登录系统使配置生效

验证方法

方法1:测试新shell

打开新的终端或运行:

1
bash -c 'locale'

如果不再出现警告,说明问题已解决。

方法2:测试脚本

运行之前出现警告的脚本:

1
bash your_script.sh

检查是否还有警告信息。

方法3:检查环境变量

1
env | grep -E "^(LC_|LANG)"

确认 LC_ALL 的值是否正确。

常见问题

Q1: 为什么修改后当前终端还有警告?

A: 当前终端会话已经加载了旧的环境变量。需要:

  • 关闭当前终端,打开新终端
  • 或者运行 source /etc/default/locale(如果支持)
  • 或者重新登录系统

Q2: locale-gen 命令找不到?

A: 确保已安装 locales 包:

1
2
3
4
5
6
# Debian/Ubuntu
sudo apt-get install locales

# CentOS/RHEL
sudo yum install glibc-locale-source glibc-langpack-zh
sudo localedef -i zh_CN -f UTF-8 zh_CN.UTF-8

Q3: 如何查看哪些locale可用?

A: 查看支持的所有locale:

1
cat /usr/share/i18n/SUPPORTED | grep zh_CN

Q4: 如何临时解决(不修改系统配置)?

A: 在当前shell中临时设置:

1
2
unset LC_ALL
export LC_ALL=""

或者在脚本开头添加:

1
2
3
4
#!/bin/bash
export LC_ALL=C
# 或
export LC_ALL=""

总结

  1. 问题本质:系统配置了不存在的locale
  2. 最佳解决:安装并生成缺失的locale(方案一)
  3. 快速解决:修改配置使用已安装的locale(方案二)
  4. 验证方法:在新shell中测试,确认无警告

相关文件

  • /etc/default/locale - 系统默认locale配置
  • /etc/environment - 系统环境变量配置
  • /etc/locale.gen - locale生成配置文件
  • ~/.bashrc - 用户bash配置(可能包含locale设置)
  • /etc/profile - 系统profile配置

参考命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看当前locale
locale

# 查看已安装的locale
locale -a

# 生成locale
sudo locale-gen

# 更新locale配置
sudo update-locale

# 查看支持的locale
cat /usr/share/i18n/SUPPORTED

文档创建时间:2024年
适用系统:Debian/Ubuntu/CentOS/RHEL等Linux发行版

RealSense人脸识别系统学习

RealSense 人脸识别系统 - 完整技术教程

从零开始构建一个基于 Intel RealSense 深度相机的人脸识别系统,包含人脸检测、识别、语音问候等功能。

📚 目录


🎯 项目概述

项目目标

构建一个实时人脸识别系统,具备以下功能:

  • ✅ 实时人脸检测与识别
  • ✅ 深度信息获取
  • ✅ 语音问候(根据时间自动播报)
  • ✅ 中文显示支持
  • ✅ ROS 话题发布

技术栈

  • 硬件: Intel RealSense D435i 深度相机
  • 操作系统: Ubuntu 20.04
  • 框架: ROS1 Noetic
  • 检测: RetinaFace (InsightFace)
  • 识别: ArcFace (InsightFace)
  • 语音: Microsoft Edge TTS
  • 图像处理: OpenCV, PIL
  • 语言: Python 3.8

📖 核心技术知识点

1. Intel RealSense SDK

知识点

  • RealSense SDK 2.0: Intel 提供的深度相机开发套件
  • pyrealsense2: Python 绑定库
  • Pipeline: 数据流管道,用于获取相机数据
  • Stream: 数据流类型(Color, Depth, Infrared, IMU)
  • Align: 深度图对齐到彩色图

关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import pyrealsense2 as rs

# 创建管道
pipeline = rs.pipeline()
config = rs.config()

# 配置流
config.enable_stream(rs.stream.depth, 640, 480, rs.format.z16, 30)
config.enable_stream(rs.stream.color, 640, 480, rs.format.bgr8, 30)

# 启动管道
profile = pipeline.start(config)

# 获取帧
frames = pipeline.wait_for_frames()
depth_frame = frames.get_depth_frame()
color_frame = frames.get_color_frame()

2. ROS1 Noetic 框架

知识点

  • Node: ROS 节点,独立运行的计算单元
  • Topic: 话题,节点间通信的通道
  • Message: 消息类型(Image, CameraInfo)
  • Publisher/Subscriber: 发布者/订阅者模式
  • Launch File: 启动配置文件

关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import rospy
from sensor_msgs.msg import Image, CameraInfo
from cv_bridge import CvBridge

# 初始化节点
rospy.init_node('realsense_camera_node')

# 创建发布者
color_pub = rospy.Publisher('/camera/color/image_raw', Image, queue_size=1)

# 转换图像
bridge = CvBridge()
image_msg = bridge.cv2_to_imgmsg(cv_image, "bgr8")
color_pub.publish(image_msg)

3. 人脸检测技术

知识点

  • OpenCV DNN: 深度学习模块,支持加载预训练模型
  • ResNet-SSD: 单阶段检测器,速度快
  • 置信度阈值: 控制检测精度与召回率
  • 边界框: 人脸位置坐标

关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import cv2

# 加载模型
net = cv2.dnn.readNetFromCaffe(prototxt_path, model_path)

# 创建 blob
blob = cv2.dnn.blobFromImage(image, 1.0, (300, 300), (104.0, 177.0, 123.0))

# 前向传播
net.setInput(blob)
detections = net.forward()

# 解析结果
for i in range(detections.shape[2]):
confidence = detections[0, 0, i, 2]
if confidence > threshold:
box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])

4. 人脸识别技术(RetinaFace + ArcFace)

知识点

  • RetinaFace: 高精度人脸检测,支持关键点检测
  • ArcFace: 角度边距损失函数,提取 512 维特征向量
  • InsightFace: 开源人脸识别框架
  • 余弦相似度: 特征向量相似度计算方法
  • 特征向量归一化: L2 归一化,提高比对准确性

关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from insightface.app import FaceAnalysis

# 初始化模型
app = FaceAnalysis(providers=['CPUExecutionProvider'], name='buffalo_l')
app.prepare(ctx_id=-1, det_size=(640, 640))

# 检测并提取特征
faces = app.get(image)
for face in faces:
embedding = face.embedding # 512维特征向量
bbox = face.bbox # 边界框
det_score = face.det_score # 检测置信度

# 计算相似度
query_emb = query_embedding / np.linalg.norm(query_embedding)
db_emb = db_embedding / np.linalg.norm(db_embedding)
similarity = np.dot(query_emb, db_emb) # 余弦相似度

5. 语音播报技术

知识点

  • Microsoft Edge TTS: 微软提供的文本转语音服务
  • edge-tts: Python 命令行工具
  • ffplay: FFmpeg 的音频播放器
  • PulseAudio: Linux 音频系统
  • 临时文件管理: 生成音频文件并异步清理

关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import subprocess
import tempfile
import threading

# 生成音频
subprocess.run([
'edge-tts',
'--voice', 'zh-CN-XiaoxiaoNeural',
'--text', '早上好,张三',
'--write-media', tmp_file
])

# 播放音频
subprocess.Popen([
'ffplay', '-nodisp', '-autoexit', '-loglevel', 'quiet', tmp_file
])

# 异步清理
def cleanup():
time.sleep(5)
os.unlink(tmp_file)
threading.Thread(target=cleanup, daemon=True).start()

6. 中文显示技术

知识点

  • OpenCV 限制: cv2.putText 不支持中文字符
  • PIL/Pillow: Python 图像处理库,支持中文
  • 字体加载: 系统字体路径查找
  • 图像格式转换: BGR ↔ RGB

关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from PIL import Image, ImageDraw, ImageFont

# 转换为 PIL 图像
pil_image = PILImage.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(pil_image)

# 加载中文字体
font = ImageFont.truetype('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', 20)

# 绘制中文
draw.text((x, y), "曾福明", font=font, fill=(255, 255, 255))

# 转换回 OpenCV 格式
result = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)

🏗️ 系统架构设计

整体架构

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
┌─────────────────────────────────────────────────────────┐
│ RealSense D435i │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Color │ │ Depth │ │ IMU │ │
│ │ Stream │ │ Stream │ │ Stream │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ realsense_camera_node.py (ROS Node) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ RealSense Pipeline │ │
│ │ - 获取彩色帧和深度帧 │ │
│ │ - 深度图对齐到彩色图 │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Face Recognition Manager │ │
│ │ - RetinaFace 检测 │ │
│ │ - ArcFace 特征提取 │ │
│ │ - 特征向量比对 │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Voice Announcement │ │
│ │ - 时间判断(早上/中午/下午/晚上) │ │
│ │ - Microsoft TTS 生成音频 │ │
│ │ - ffplay 播放 │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Image Drawing │ │
│ │ - PIL 绘制中文 │ │
│ │ - OpenCV 绘制框和标签 │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘

┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ ROS Topics │ │ Face DB │ │ Audio Out │
│ - /camera/ │ │ - database. │ │ - PulseAudio │
│ color/ │ │ json │ │ - Speakers │
│ - /camera/ │ │ - faces/ │ │ │
│ depth/ │ │ │ │ │
│ - /camera/ │ │ │ │ │
│ face_ │ │ │ │ │
│ detection │ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘

数据流

  1. 图像采集: RealSense → Pipeline → 帧数据
  2. 人脸检测: 图像 → RetinaFace → 人脸边界框
  3. 特征提取: 人脸区域 → ArcFace → 512维特征向量
  4. 特征比对: 查询特征 ↔ 数据库特征 → 相似度
  5. 识别结果: 相似度 > 阈值 → 识别成功 → 语音播报
  6. 图像绘制: 识别结果 → PIL绘制中文 → OpenCV绘制框
  7. ROS发布: 处理后的图像 → ROS Topic → 订阅者

模块划分

1. RealSenseCameraNode

  • 职责: ROS 节点主类
  • 功能:
    • 相机数据采集
    • ROS 话题发布
    • 协调各模块

2. FaceRecognitionManager

  • 职责: 人脸识别管理
  • 功能:
    • 模型加载(RetinaFace + ArcFace)
    • 特征提取
    • 特征比对
    • 数据库管理

3. 语音播报模块

  • 职责: 文本转语音
  • 功能:
    • 时间判断
    • TTS 生成
    • 音频播放

🔧 开发历程与问题解决

阶段一:RealSense SDK 安装

问题 1: 仓库错误

现象:

1
E: 仓库 "http://download.opensuse.org/repositories/home:/ashish-bosch/xUbuntu_20.04 Release" 没有 Release 文件。

原因: 第三方仓库不可用或已失效

解决方案:

1
2
# 在脚本中添加错误容忍
sudo apt update || true

经验: 对于非关键依赖,使用 || true 允许部分失败

问题 2: Python 绑定问题

现象:

1
CMake Error: Could NOT find PythonInterp (missing: PYTHON_EXECUTABLE)

原因: CMake 无法自动找到 Python 解释器

解决方案:

1
2
3
# 显式指定 Python 路径
PYTHON_EXEC=$(which python3)
cmake .. -DPYTHON_EXECUTABLE="$PYTHON_EXEC"

经验: 在构建脚本中显式指定关键路径,避免自动检测失败

阶段二:ROS 节点开发

问题 3: 模块导入错误

现象:

1
ModuleNotFoundError: No module named 'face_recognition_manager'

原因: ROS 运行时,Python 路径不包含脚本目录

解决方案:

1
2
3
# 在脚本开头添加路径
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, script_dir)

经验: ROS 包装脚本会改变工作目录,需要显式添加模块路径

阶段三:人脸识别集成

问题 4: 识别失败(关键问题)

现象: 检测到人脸但识别失败,一直显示”未知”

原因分析:

  1. detect_and_recognize 先提取特征向量
  2. 然后裁剪人脸区域
  3. recognize_face 对裁剪区域重新检测
  4. 裁剪区域太小,RetinaFace 检测不到 → 返回 0 个特征

解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 方案:直接使用已提取的特征向量,避免重复检测
def detect_and_recognize(self, image):
faces, embeddings = self.extract_face_embedding(image)
for face, embedding in zip(faces, embeddings):
# 直接使用 embedding,不重新检测
person_id, name, role, confidence = self.recognize_face_from_embedding(
embedding, threshold=self.recognition_threshold
)

def recognize_face_from_embedding(self, embedding, threshold=None):
# 直接比对特征向量,不重新提取
query_emb = embedding / np.linalg.norm(embedding)
# ... 比对逻辑

经验:

  • 避免重复计算,提高效率
  • 理解每个函数的输入输出,避免不必要的转换
  • 关键问题需要深入调试,添加详细日志

问题 5: 阈值传递问题

现象: 识别阈值设置无效

原因: FaceRecognitionManager 初始化时未接收阈值参数

解决方案:

1
2
3
4
5
6
7
8
9
10
11
# 修改初始化方法
def __init__(self, database_dir, ctx_id=0, recognition_threshold=0.5):
self.recognition_threshold = recognition_threshold

# 在节点中传递参数
recognition_threshold = rospy.get_param('~recognition_threshold', 0.5)
self.face_recognition_manager = FaceRecognitionManager(
database_dir,
ctx_id=ctx_id,
recognition_threshold=recognition_threshold
)

经验: 参数传递要完整,从配置到实现的每一层都要传递

阶段四:用户体验优化

问题 6: 中文显示为问号

现象: 图像上中文显示为 ???

原因: OpenCV 的 cv2.putText 不支持中文字符

解决方案:

1
2
3
4
5
6
7
# 使用 PIL 绘制中文
def draw_chinese_text(self, image, text, position, font_size=20, color=(255, 255, 255)):
pil_image = PILImage.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(pil_image)
font = ImageFont.truetype('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', font_size)
draw.text(position, text, font=font, fill=color)
return cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)

经验: 不同库有不同限制,需要组合使用

问题 7: 一直播报”检测到人脸”

现象: 即使识别成功,仍会播报”检测到人脸”

原因: 旧的语音播报逻辑未禁用

解决方案:

1
2
3
4
# 如果启用了人脸识别,禁用旧的播报
if (not self.enable_face_recognition and len(faces) > 0 and
(current_time - self.last_announcement_time) >= self.voice_announcement_interval):
self.speak_distance(min_distance, len(faces))

经验: 功能升级时要清理旧代码,避免冲突

问题 8: 语音质量差

现象: espeak 中文发音不自然

原因: espeak 对中文支持有限

解决方案: 切换到 Microsoft Edge TTS

经验: 选择成熟的技术方案,避免重复造轮子


💡 关键技术实现

1. 特征向量比对优化

问题

  • 重复提取特征导致失败
  • 效率低下

解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 优化前:重复检测
def detect_and_recognize(self, image):
faces, embeddings = self.extract_face_embedding(image)
for face, embedding in zip(faces, embeddings):
face_roi = image[bbox[1]:bbox[3], bbox[0]:bbox[2]]
# 重新检测,可能失败
person_id, name, role, confidence = self.recognize_face(face_roi)

# 优化后:直接使用特征
def detect_and_recognize(self, image):
faces, embeddings = self.extract_face_embedding(image)
for face, embedding in zip(faces, embeddings):
# 直接比对,不重新检测
person_id, name, role, confidence = self.recognize_face_from_embedding(embedding)

2. 延迟优化

策略

  1. 提高帧率: 30 FPS → 减少延迟
  2. 每帧检测: face_detection_skip=1
  3. 图像缩放: 大图先缩小再检测,结果按比例放大
  4. 超时控制: wait_for_frames(timeout_ms=500)

代码

1
2
3
4
5
6
# 图像缩放优化
if w > 640 or h > 480:
scale_factor = 640.0 / w
new_w, new_h = 640, int(h * scale_factor)
resized_image = cv2.resize(image, (new_w, new_h))
# 检测后按比例放大结果

3. 防重复播报机制

实现

1
2
3
4
5
6
7
8
9
10
11
# 记录最后识别的人和时间
self.last_recognized_person = None
self.last_recognition_time = 0
self.recognition_cooldown = 10.0 # 10秒冷却

# 检查是否需要播报
if (person_id != self.last_recognized_person or
(current_time - self.last_recognition_time) >= self.recognition_cooldown):
self.speak_greeting(text)
self.last_recognized_person = person_id
self.last_recognition_time = current_time

4. 时间判断逻辑

实现

1
2
3
4
5
6
7
8
9
10
def get_greeting(self):
hour = datetime.now().hour
if 5 <= hour < 12:
return "早上好"
elif 12 <= hour < 14:
return "中午好"
elif 14 <= hour < 18:
return "下午好"
else:
return "晚上好"

🚀 部署与使用

一、环境准备

1. 系统要求

  • Ubuntu 20.04 (Focal Fossa)
  • Python 3.8+
  • ROS1 Noetic
  • USB 3.0 端口

2. 安装 RealSense SDK

1
2
3
cd /home/pc/realsense
chmod +x build_realsense_in_ubuntu_2004.sh
./build_realsense_in_ubuntu_2004.sh

3. 安装 ROS 依赖

1
sudo apt install ros-noetic-cv-bridge ros-noetic-image-transport

4. 安装 Python 依赖

1
2
3
4
5
6
7
8
# 配置 pip 镜像(可选)
pip3 install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple \
opencv-python numpy pyrealsense2 insightface edge-tts pillow

# 或使用代理下载(如果有)
export HTTP_PROXY=http://127.0.0.1:7890
export HTTPS_PROXY=http://127.0.0.1:7890
pip3 install insightface edge-tts pillow

5. 安装中文字体

1
sudo apt install fonts-wqy-microhei fonts-wqy-zenhei

6. 安装音频工具

1
sudo apt install ffmpeg pulseaudio-utils

二、编译工作空间

1
2
3
4
cd /home/pc/realsense
source /opt/ros/noetic/setup.bash
catkin_make
source devel/setup.bash

三、添加人物到数据库

1
2
3
4
cd /home/pc/realsense
python3 src/realsense_camera/scripts/add_person.py \
person001 曾福明 访客 \
picture/曾福明-1.jpg picture/曾福明-2.jpg

参数说明:

  • person001: 人物唯一ID
  • 曾福明: 姓名
  • 访客: 身份/角色
  • picture/曾福明-1.jpg: 人脸图片路径(可多张)

四、启动系统

方法一:一键启动(推荐)

1
2
cd /home/pc/realsense
./start.sh

方法二:手动启动

1
2
3
4
5
6
7
8
9
10
# 终端1:启动 roscore
roscore

# 终端2:启动相机节点
cd /home/pc/realsense
source devel/setup.bash
roslaunch realsense_camera realsense_camera.launch

# 终端3:查看识别结果(可选)
rosrun image_view image_view image:=/camera/face_detection/image_raw

五、配置参数

编辑 src/realsense_camera/launch/realsense_camera.launch:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 相机参数 -->
<param name="width" value="640" />
<param name="height" value="480" />
<param name="fps" value="30" />

<!-- 识别参数 -->
<param name="enable_face_recognition" value="true" />
<param name="recognition_threshold" value="0.5" /> <!-- 0.3-0.7,越低越容易识别 -->
<param name="recognition_cooldown" value="10.0" /> <!-- 防重复播报时间(秒) -->

<!-- 语音参数 -->
<param name="enable_voice_announcement" value="true" />
<param name="tts_voice" value="zh-CN-XiaoxiaoNeural" /> <!-- 中文语音 -->

六、测试识别

1
2
3
# 使用测试脚本
cd /home/pc/realsense
python3 test_recognition.py picture/曾福明-2.jpg

📊 总结与思考

技术选型总结

技术 选择 原因
检测 RetinaFace 精度高,支持关键点
识别 ArcFace 工业级精度,特征向量质量好
框架 InsightFace 开源,模型成熟
语音 Edge TTS 中文自然,免费
显示 PIL 支持中文,易用
框架 ROS1 模块化,易扩展

关键经验

  1. 避免重复计算

    • 特征提取一次,多次使用
    • 避免不必要的图像转换
  2. 错误处理

    • 添加详细日志
    • 优雅降级(识别失败时仍显示检测结果)
  3. 用户体验

    • 中文显示支持
    • 自然语音播报
    • 实时反馈
  4. 性能优化

    • 图像缩放检测
    • 帧率控制
    • 超时处理

改进方向

  1. 性能优化

    • GPU 加速(CUDA)
    • 多线程处理
    • 模型量化
  2. 功能扩展

    • 多人同时识别
    • 识别历史记录
    • Web 界面
  3. 鲁棒性

    • 光照自适应
    • 角度容差
    • 遮挡处理

学习收获

  1. 深度相机应用

    • 深度信息获取
    • 深度图对齐
    • 距离计算
  2. 人脸识别技术

    • 检测 vs 识别
    • 特征向量比对
    • 相似度计算
  3. ROS 开发

    • 节点设计
    • 话题发布
    • 参数配置
  4. 问题解决能力

    • 调试技巧
    • 日志分析
    • 方案优化

📚 参考资料


🎓 学习路径建议

初学者

  1. 理解 RealSense SDK 基础
  2. 学习 ROS 基本概念
  3. 掌握 OpenCV 图像处理

进阶

  1. 深入学习人脸识别算法
  2. 优化系统性能
  3. 扩展功能模块

高级

  1. 模型训练与优化
  2. 系统架构设计
  3. 工程化部署

文档版本: 1.0
最后更新: 2024年11月
作者: RealSense 人脸识别项目组

Tailscale初体验

Tailscale 使用教程 + 本次排错完整记录(无敏感 IP)

本教程包含:

  1. Tailscale 是什么、能做什么
  2. 如何互联多台设备
  3. 本次“收集所有节点公网 IP”过程中遇到的问题
  4. 解决方案
  5. 反思与经验总结

1. Tailscale 是什么?

Tailscale 是基于 WireGuard 的零配置“内网组网工具”,可以让世界各地的电脑、服务器、手机像在同一个局域网内一样互相访问。

主要特点:

  • 每台设备获得一个稳定的 Tailscale 内网 IP(100.x.x.x)

  • 自带打洞,能直连就直连,不行就自动走中继

  • 多平台支持:Linux / Windows / macOS / Android / iOS / NAS / 路由器等

  • 支持 MagicDNS:每台设备自动获得一个类似

    hostname.tailXXXX.ts.net 的域名

  • 带 Tailscale SSH:不需要公网 22 端口,也不需要 known_hosts

常见用途:

  • 多台云服务器互联成“一个内网”
  • 从任何地方 SSH 登录家里的电脑或服务器
  • 手机访问 NAS、本地开发环境
  • 服务器自动化管理、集群管理
  • 私人 Mesh 网络

2. Tailscale 如何互联?

以下以 Linux 为例:

步骤 1:安装 Tailscale

1
2
curl -fsSL https://tailscale.com/install.sh | sh

步骤 2:登录并联网

1
2
tailscale up

终端会给你一个 URL,用浏览器打开后确认登录。

登录后:

  • 每个节点都会分配一个“内网 IP(100.x.x.x)”
  • 会在控制台看到所有节点

步骤 3:查看所有节点

1
2
tailscale status

示例输出(IP 已替换为随机示例):

1
2
3
4
5
6
100.64.10.21    hk-node       linux   idle
100.72.91.55 us-node linux idle
100.11.22.33 jp-node linux idle
100.88.20.45 home-pc windows idle
100.90.11.99 phone android idle


3. MagicDNS(域名访问)

每台设备都有一个自动域名,例如:

1
2
3
hk-node.tail1234.ts.net.
us-node.tail1234.ts.net.

使用时要去掉最后的点(.):

1
2
hk-node.tail1234.ts.net

可以直接:

1
2
ssh root@hk-node.tail1234.ts.net

不用记 IP,非常方便。


4. Tailscale SSH 是什么?

Tailscale SSH = 不用暴露 22 端口,也不需要 known_hosts,

通过 Tailscale 安全通道实现 SSH。

要启用它:

每台节点执行:

1
2
tailscale up --ssh=true

查看是否启用成功:

1
2
tailscale debug features | grep ssh

应该看到:

1
2
ssh: true

控制台要启用 Tailscale SSH

打开 https://login.tailscale.com/admin/acls → Settings:

  • Enable Tailscale SSH:打开
  • Allow inbound SSH:打开

第一次连接某个节点,会要求你通过一个 URL 授权:

1
2
# To authenticate, visit: https://login.tailscale.com/a/xxxxxx

浏览器打开,点 Allow 即可。


5. 本次实战目标

“从管理机查看每一台节点的:

  • Tailscale 内网 IP(100.x.x.x)
  • 节点名
  • 每个节点自己访问外网时的公网 IP(curl 获取)

最终希望看到类似这样的输出(随机示例):

1
2
3
4
5
6
100.72.91.55     hk-node.tail1234.ts.net     45.66.23.101
100.11.22.33 us-node.tail1234.ts.net 112.45.55.99
100.64.10.21 jp-node.tail1234.ts.net 23.12.77.155
100.55.99.77 home-pc.tail1234.ts.net N/A
(跳过手机节点)


6. 本次过程中遇到的问题 + 解决方案

问题 1:出现 HostKey 错误

错误类似:

1
2
3
No ED25519 host key is known ...
Host key verification failed.

一开始误以为是 SSH 配置问题,但检查后发现:

1
2
3
ssh -G host | grep strict
stricthostkeychecking false

说明不是系统 SSH 的问题。

实际原因是:

  • 节点没有执行 tailscale up --ssh=true
  • 控制台没启用 Tailscale SSH
  • 没有点网页上的 Allow 授权

解决:

  • 每个节点开启 tailscale ssh
  • 控制台 Settings 启用 Tailscale SSH
  • 第一次连接时点击 Allow 授权

问题 2:有节点出现“sudo 无法解析主机名”

例如:

1
2
sudo: unable to resolve host myserver: Name or service not known

解决方法:

1
2
echo "127.0.1.1   myserver" >> /etc/hosts

修复主机名解析即可。


问题 3:使用脚本时只输出一行

一开始以为脚本错误,后来发现原因如下:

  • 有些节点无法访问外网
  • 有些节点没有 curl 或 wget
  • 有些节点直接返回空值 (pub_ip="")

脚本判断 pub_ip 为空就跳过 → 最终只输出一行。

解决:

最稳的方法是:

每台节点自己执行一条命令,让每台机器自己查自己的公网 IP。


7. 最稳方案:每台节点自己查公网 IP

在每个节点执行:

1
2
echo "$(tailscale ip -4) $(hostname) $(curl -s https://ipinfo.io/ip || curl -s https://api.ipify.org || curl -s ifconfig.me || echo N/A)"

输出示例:

1
2
3
4
5
100.72.91.55 hk-node 45.66.23.101
100.11.22.33 us-node 112.45.55.99
100.64.10.21 jp-node 23.12.77.155
100.55.99.77 home-pc N/A

然后手工或脚本汇总即可。

这个方案不受:

  • Tailscale SSH 授权

  • curl 安装

  • 节点网络差异

  • Shell 管道

    的影响,是最稳定、最安全的方案。


8. 反思与经验

这次排查,核心经验如下:

经验 1:分层排查非常重要

Tailscale/SSH/脚本容易混在一起,需要分层:

1)SSH 配置问题?

→ 用 ssh -G host | grep strict 检查最终生效配置

2)Tailscale SSH 是否启用?

→ 每节点检查 tailscale debug features | grep ssh

3)是否需要网页授权?

→ 看是否弹出 “visit https://login.tailscale.com/a/...”

4)节点能否访问外网?

curl https://ipinfo.io/ip 自己能不能执行

5)脚本逻辑是否跳过空值?

→ 在脚本里对空值加打印

经验 2:脚本不如“每台节点自己查自己”稳

跨节点批量获取公网 IP 受:

  • 节点系统不同
  • curl/wget 不一致
  • 外网不可访问
  • Tailscale SSH 权限
  • 管道中混合输出

影响太大。

最终发现:

让每台节点自己打印“内网 IP + 主机名 + 公网 IP”,然后汇总最靠谱。

经验 3:MagicDNS 对自动化脚本非常好用

使用:

1
2
hostname.tail1234.ts.net

不需要管理 hosts,不需要定 IP。


9. 总结

  • Tailscale 是一个强大的内网互联工具,让所有设备像局域网一样互通
  • Tailscale SSH 不用公网端口和密钥文件,但需要:
    • 节点启用 ssh
    • 控制台开启
    • 第一次授权
  • 在多节点环境中,批量从远程取公网 IP 容易遇到个别机器失败
  • 最稳做法:每台节点自己查自己的公网 IP,然后汇总
  • 这样不需要脚本跨机器调用,也不依赖 tailscale ssh 成功率

WireGuard转发ssh教程

🟩 WireGuard 实现:只让 B 的 SSH 走 A,不影响 B 的公网访问(完整教程)

目标:

  • B 的公网 IP 可正常 SSH
  • B 的所有网络流量继续走自己的公网
  • 但 SSH(22)流量走 A → 从 A 转发
  • A 与 B 通过 WireGuard 建立一条只用于 SSH 的隧道

🟦 1. 网络拓扑(理解非常重要)

1
2
3
4
5
6
7
8
9
10
11
[ B 服务器 ]
├ 公网 IP:XX.XX.XX.XX ← 保持可直接 SSH
├ 本地 SSH 22
└ VPN 内网:10.7.0.2
↑ SSH over WG

[A 中转服务器]
├ 公网 IP:YY.YY.YY.YY
├ 监听 SSH 端口 60022 → 转发给 B
└ VPN 内网:10.7.0.1

最终效果:

1
2
ssh -p 60022 root@A公网IP   → 实际进入 B


🟦 2. A 服务器配置(作为 WireGuard 服务端)

/etc/wireguard/wg0.conf

(敏感内容已隐藏)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Interface]
Address = 10.7.0.1/24
ListenPort = 51820
PrivateKey = <A_PRIVATE_KEY>

# 允许 B 访问 A
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -t nat -A PREROUTING -p tcp --dport 60022 -j DNAT --to-destination 10.7.0.2:22
PostUp = iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE

PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -t nat -D PREROUTING -p tcp --dport 60022 -j DNAT --to-destination 10.7.0.2:22
PostDown = iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE

② 添加 B 为 Peer

1
2
3
4
[Peer]
PublicKey = <B_PUBLIC_KEY>
AllowedIPs = 10.7.0.2/32

③ 启动 A

1
2
sudo wg-quick up wg0

检测:

1
2
sudo wg show


🟦 3. B 服务器配置(作为 WireGuard 客户端)

/etc/wireguard/wg0.conf

1
2
3
4
5
6
7
8
9
10
11
12
[Interface]
Address = 10.7.0.2/24
DNS = 8.8.8.8
PrivateKey = <B_PRIVATE_KEY>

[Peer]
PublicKey = <A_PUBLIC_KEY>
PresharedKey = <PSK>
AllowedIPs = 10.7.0.1/32
Endpoint = <A_PUBLIC_IP>:51820
PersistentKeepalive = 25

⚠️ 注意 这里不能写 0.0.0.0/0,否则 B 会全流量走 A,公网 SSH 会失效!

你现在使用的是正确的:

1
2
AllowedIPs = 10.7.0.1/32


🟦 4. 启动 B

1
2
sudo wg-quick up wg0

检查:

1
2
sudo wg show


🟦 5. 最关键:A 做 SSH 转发

A 上执行:

1
2
3
sudo iptables -t nat -A PREROUTING -p tcp --dport 60022 -j DNAT --to-destination 10.7.0.2:22
sudo iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE

现在你可以在任何地方这样 SSH 到 B:

1
2
ssh -p 60022 root@A公网IP


🟦 6. 验证是否成功

在 B 中执行:

(1) 查看出口 IP

1
2
curl ip.sb

结果是:

B 自己的公网 IP → ✅说明普通流量没有走 VPN

(2) 测试内网互通

1
2
ping 10.7.0.1

(3) 测试从 A 转发 SSH

在任意客户端执行:

1
2
ssh -p 60022 root@A公网IP

最终会进入 B → 成功!


🟦 7. 常见问题 & 修复

❌ 现象:B 公网无法访问

原因:你把 B 的 AllowedIPs 设置成了:

1
2
AllowedIPs = 0.0.0.0/0

导致默认路由被重写 → 公网 SSH 被“吃掉”。

✔ 修复(正确写法):

1
2
AllowedIPs = 10.7.0.1/32


❌ ping 外网不通

检查 B 是否用了错误 PostUp:

1
2
ip route


❌ 转发不成功

检查 A 端口是否开放:

1
2
ss -lntp | grep 60022


❌ WireGuard 没连上

检查 handshake:

1
2
sudo wg show

latest handshake: 显示几秒内 → 正常。


🟦 8. 注意事项(非常重要)

✔ B 的公网 SSH 不会被影响

因为没有修改默认路由。

✔ 永远不要在客户端写 0.0.0.0/0

除非你需要全隧道代理。

✔ 端口转发要确保安全

如果你不想别人知道你的转发端口,可以:

  • 60022 改成随机高端口
  • 限制来源 IP

🟩 9. 全流程总结(快速版)

A(服务端)

  1. 创建 wg0.conf
  2. ListenPort=51820
  3. Peer AllowedIPs=10.7.0.2/32
  4. 端口转发:60022 → 10.7.0.2:22

B(客户端)

  1. Address=10.7.0.2
  2. AllowedIPs=10.7.0.1/32(只访问 A)
  3. Endpoint=A:51820

最终使用方式

1
2
ssh -p 60022 root@A公网IP

实际上登录的是 B

固定ch341串口

1
/etc/udev/rules.d/99-ch341.rules
1
udevadm info -a -n /dev/ttyUSB0 | grep KERNELS
1
2
sudo udevadm control --reload-rules
sudo udevadm trigger
1
2
3
4
5
6
7
# /etc/udev/rules.d/99-ch341.rules
# 使用 VID/PID 固定 symlink
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", SYMLINK+="ttyCH341_A"

# 如果有多个相同设备,可加序列号区分
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", ATTRS{serial}=="123456", SYMLINK+="ttyCH341_B"

1
2
3
4
5
6
# YMBotHead
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", ATTRS{serial}=="XXXXXX", SYMLINK+="ymbothead"

# LeftHand
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", ATTRS{serial}=="YYYYYY", SYMLINK+="lefthand"

1
2
3
4
5
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", KERNELS=="1-4.1", SYMLINK+="ymbothead"
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", KERNELS=="1-9.3", SYMLINK+="righthand"

SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", KERNELS=="1-9.4", SYMLINK+="ymbothead"

Ansible 入门与进阶教程

Ansible 入门与进阶教程

目录


简介

Ansible 是一个自动化配置管理、应用部署和任务编排的工具。它使用 SSH 协议连接远程主机,无需在受控端安装客户端(只需要 Python)。

为什么选择 Ansible?

  • 无代理架构:不需要在受控端安装客户端
  • 简单易学:使用 YAML 语法,易于阅读和编写
  • 幂等性:多次执行相同操作,结果一致
  • 模块化:丰富的内置模块和插件

安装 Ansible

在主控端(Control Node)安装

在 Debian 12 上安装 Ansible:

1
2
3
4
5
6
7
8
# 更新软件包列表
sudo apt update

# 安装 Ansible
sudo apt install ansible -y

# 验证安装
ansible --version

受控端(Managed Node)准备

受控端需要满足以下条件:

  1. 可以通过 SSH 连接
  2. 安装了 Python(Debian 12 默认已安装 Python 3)
1
2
# 检查 Python 版本(在受控端)
python3 --version

SSH 密钥配置(推荐)

为了免密登录,配置 SSH 密钥:

1
2
3
4
5
6
7
8
# 在主控端生成 SSH 密钥(如果还没有)
ssh-keygen -t rsa -b 4096

# 将公钥复制到受控端
ssh-copy-id username@target_host

# 测试连接
ssh username@target_host

基础概念

1. Inventory(清单)

Inventory 文件定义了要管理的主机列表。

默认位置/etc/ansible/hosts

示例 inventory 文件inventory.ini):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 单台主机
web1 ansible_host=192.168.1.10 ansible_user=debian

# 主机组
[webservers]
web1 ansible_host=192.168.1.10
web2 ansible_host=192.168.1.11

[dbservers]
db1 ansible_host=192.168.1.20

# 使用变量
[webservers:vars]
ansible_user=debian
ansible_ssh_private_key_file=~/.ssh/id_rsa

2. Playbook(剧本)

Playbook 是 Ansible 的配置、部署和编排语言,使用 YAML 格式。

基本结构

1
2
3
4
5
6
7
8
9
---
- name: 示例 Playbook
hosts: webservers
become: yes
tasks:
- name: 安装 Nginx
apt:
name: nginx
state: present

3. Module(模块)

Ansible 使用模块执行任务。常用模块:

  • apt:管理 Debian/Ubuntu 软件包
  • copy:复制文件
  • template:模板文件
  • service:管理服务
  • user:管理用户
  • file:管理文件和目录

4. Task(任务)

任务是一个动作单元,通常调用一个模块。

5. Play(剧本)

Play 是任务的有序列表,映射到一组主机。


入门教程

第一步:测试连接

创建测试 inventory 文件 test-inventory.ini

1
2
[test]
localhost ansible_connection=local

测试连接:

1
2
3
4
5
# 测试 ping
ansible -i test-inventory.ini test -m ping

# 测试所有主机
ansible all -i test-inventory.ini -m ping

第二步:执行简单命令

1
2
3
4
5
# 执行 shell 命令
ansible -i test-inventory.ini test -a "uptime"

# 执行带 sudo 的命令
ansible -i test-inventory.ini test -a "apt update" --become

第三步:创建第一个 Playbook

创建 first-playbook.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
---
- name: 我的第一个 Playbook
hosts: localhost
connection: local
tasks:
- name: 显示系统信息
command: uname -a
register: system_info

- name: 显示结果
debug:
msg: "{{ system_info.stdout }}"

- name: 创建测试目录
file:
path: /tmp/ansible-test
state: directory
mode: '0755'

- name: 创建测试文件
copy:
content: "Hello Ansible!\n"
dest: /tmp/ansible-test/hello.txt

执行 Playbook:

1
ansible-playbook first-playbook.yml

第四步:管理软件包

创建 install-packages.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
---
- name: 安装常用软件包
hosts: localhost
become: yes
tasks:
- name: 更新软件包列表
apt:
update_cache: yes

- name: 安装软件包
apt:
name:
- curl
- git
- vim
state: present

- name: 移除不需要的软件包
apt:
name: nano
state: absent

第五步:管理服务

创建 manage-service.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
---
- name: 管理 Nginx 服务
hosts: localhost
become: yes
tasks:
- name: 安装 Nginx
apt:
name: nginx
state: present

- name: 启动并启用 Nginx
service:
name: nginx
state: started
enabled: yes

- name: 检查服务状态
service_facts:

- name: 显示服务状态
debug:
msg: "Nginx 状态: {{ ansible_facts.services['nginx.service'].state }}"

进阶教程

1. 使用变量

在 Playbook 中定义变量

1
2
3
4
5
6
7
8
9
10
11
---
- name: 使用变量示例
hosts: localhost
vars:
app_name: myapp
app_port: 8080
app_user: www-data
tasks:
- name: 显示变量
debug:
msg: "应用名称: {{ app_name }}, 端口: {{ app_port }}"

使用变量文件

创建 vars.yml

1
2
3
4
5
---
app_name: myapp
app_port: 8080
app_user: www-data
database_host: db.example.com

在 Playbook 中使用:

1
2
3
4
5
6
7
8
9
---
- name: 使用变量文件
hosts: localhost
vars_files:
- vars.yml
tasks:
- name: 显示变量
debug:
var: app_name

使用 Inventory 变量

inventory.ini 中:

1
2
3
4
5
6
7
[webservers]
web1 ansible_host=192.168.1.10 app_port=8080
web2 ansible_host=192.168.1.11 app_port=8081

[webservers:vars]
app_name=myapp
app_user=www-data

2. 使用条件判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
---
- name: 条件判断示例
hosts: localhost
tasks:
- name: 检查系统类型
debug:
msg: "这是 Debian 系统"
when: ansible_os_family == "Debian"

- name: 安装软件包(仅当不存在时)
apt:
name: htop
state: present
when: ansible_distribution_version == "12"

- name: 根据变量执行不同任务
apt:
name: "{{ item }}"
state: present
loop:
- curl
- wget
when: install_tools | default(true)

3. 使用循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
---
- name: 循环示例
hosts: localhost
become: yes
tasks:
- name: 创建多个用户
user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
shell: /bin/bash
loop:
- { name: 'user1', groups: 'sudo' }
- { name: 'user2', groups: 'users' }
- { name: 'user3', groups: 'docker' }

- name: 创建多个目录
file:
path: "{{ item }}"
state: directory
mode: '0755'
loop:
- /opt/app1
- /opt/app2
- /opt/app3

4. 使用 Handlers(处理器)

Handlers 在任务发生变化时执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
---
- name: 使用 Handlers
hosts: localhost
become: yes
tasks:
- name: 安装 Nginx
apt:
name: nginx
state: present
notify: restart nginx

- name: 配置 Nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx

handlers:
- name: restart nginx
service:
name: nginx
state: restarted

5. 使用 Templates(模板)

创建模板文件 nginx.conf.j2

1
2
3
4
5
6
7
8
9
10
11
server {
listen {{ app_port }};
server_name {{ server_name }};

root {{ web_root }};
index index.html;

location / {
try_files $uri $uri/ =404;
}
}

在 Playbook 中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---
- name: 使用模板
hosts: webservers
become: yes
vars:
app_port: 8080
server_name: example.com
web_root: /var/www/html
tasks:
- name: 部署 Nginx 配置
template:
src: nginx.conf.j2
dest: /etc/nginx/sites-available/default
notify: restart nginx

handlers:
- name: restart nginx
service:
name: nginx
state: restarted

6. 使用 Roles(角色)

Roles 用于组织 Playbook,使其更模块化和可重用。

创建 Role 结构

1
mkdir -p roles/myapp/{tasks,handlers,templates,files,vars,defaults,meta}

目录说明

  • tasks/:任务文件
  • handlers/:处理器文件
  • templates/:模板文件
  • files/:静态文件
  • vars/:变量文件
  • defaults/:默认变量
  • meta/:元数据

示例 Role:roles/myapp/tasks/main.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---
- name: 安装依赖
apt:
name: "{{ item }}"
state: present
loop: "{{ myapp_dependencies }}"

- name: 创建应用目录
file:
path: "{{ myapp_path }}"
state: directory
owner: "{{ myapp_user }}"
group: "{{ myapp_group }}"
mode: '0755'

- name: 部署配置文件
template:
src: app.conf.j2
dest: "{{ myapp_path }}/app.conf"
notify: restart myapp

使用 Role:

1
2
3
4
5
6
7
8
9
---
- name: 部署应用
hosts: webservers
become: yes
roles:
- role: myapp
vars:
myapp_path: /opt/myapp
myapp_user: www-data

7. 使用 Tags(标签)

Tags 允许选择性执行任务:

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
---
- name: 使用 Tags
hosts: localhost
become: yes
tasks:
- name: 安装软件包
apt:
name: nginx
state: present
tags:
- install
- nginx

- name: 配置服务
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
tags:
- config
- nginx

- name: 重启服务
service:
name: nginx
state: restarted
tags:
- restart
- nginx

执行特定标签:

1
2
3
4
5
6
7
8
# 只执行 install 标签
ansible-playbook playbook.yml --tags install

# 跳过 restart 标签
ansible-playbook playbook.yml --skip-tags restart

# 只执行 nginx 相关任务
ansible-playbook playbook.yml --tags nginx

8. 错误处理

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
---
- name: 错误处理示例
hosts: localhost
tasks:
- name: 可能失败的任务
command: /bin/false
ignore_errors: yes
register: result

- name: 检查失败
debug:
msg: "任务失败,但继续执行"
when: result.failed

- name: 重试任务
command: /usr/bin/curl http://example.com
retries: 3
delay: 5
until: result.rc == 0

- name: 失败时执行清理
file:
path: /tmp/temp-file
state: absent
when: result.failed

9. 使用 Vault 加密敏感数据

创建加密文件:

1
2
# 创建加密变量文件
ansible-vault create secrets.yml

编辑加密文件:

1
ansible-vault edit secrets.yml

查看加密文件:

1
ansible-vault view secrets.yml

在 Playbook 中使用:

1
2
3
4
5
6
7
8
9
---
- name: 使用加密变量
hosts: localhost
vars_files:
- secrets.yml
tasks:
- name: 使用加密变量
debug:
msg: "密码: {{ db_password }}"

执行时输入密码:

1
ansible-playbook playbook.yml --ask-vault-pass

或使用密码文件:

1
ansible-playbook playbook.yml --vault-password-file ~/.vault_pass

10. 使用 Facts(事实)

Facts 是 Ansible 自动收集的系统信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
---
- name: 收集 Facts
hosts: localhost
tasks:
- name: 显示所有 Facts
debug:
var: ansible_facts

- name: 显示特定信息
debug:
msg: |
主机名: {{ ansible_hostname }}
操作系统: {{ ansible_distribution }} {{ ansible_distribution_version }}
IP 地址: {{ ansible_default_ipv4.address }}
内存: {{ ansible_memtotal_mb }} MB
CPU 核心数: {{ ansible_processor_vcpus }}

11. 使用 Blocks(块)

Blocks 用于组织任务和处理错误:

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
---
- name: 使用 Blocks
hosts: localhost
become: yes
tasks:
- name: 配置块
block:
- name: 安装软件包
apt:
name: nginx
state: present

- name: 配置 Nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf

- name: 启动服务
service:
name: nginx
state: started

rescue:
- name: 错误处理
debug:
msg: "配置失败,执行清理"

- name: 移除软件包
apt:
name: nginx
state: absent

always:
- name: 清理临时文件
file:
path: /tmp/nginx-config-backup
state: absent

12. 动态 Inventory

使用脚本生成 Inventory:

创建 dynamic-inventory.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python3
import json

inventory = {
"webservers": {
"hosts": ["web1.example.com", "web2.example.com"],
"vars": {
"ansible_user": "debian"
}
},
"dbservers": {
"hosts": ["db1.example.com"]
},
"_meta": {
"hostvars": {
"web1.example.com": {
"ansible_host": "192.168.1.10"
}
}
}
}

print(json.dumps(inventory))

使用动态 Inventory:

1
2
chmod +x dynamic-inventory.py
ansible-playbook -i dynamic-inventory.py playbook.yml

最佳实践

1. 项目结构

推荐的项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ansible-project/
├── inventory/
│ ├── production.ini
│ └── staging.ini
├── group_vars/
│ ├── all.yml
│ ├── webservers.yml
│ └── dbservers.yml
├── host_vars/
│ └── web1.example.com.yml
├── roles/
│ ├── common/
│ ├── nginx/
│ └── mysql/
├── playbooks/
│ ├── site.yml
│ ├── webservers.yml
│ └── dbservers.yml
├── files/
├── templates/
└── ansible.cfg

2. ansible.cfg 配置

创建 ansible.cfg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[defaults]
inventory = inventory/production.ini
host_key_checking = False
retry_files_enabled = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600

[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
pipelining = True

3. 使用 --check 模式

在执行前检查变更:

1
ansible-playbook playbook.yml --check

4. 使用 --diff 显示差异

1
ansible-playbook playbook.yml --diff

5. 限制执行主机

1
2
3
4
5
# 只对特定主机执行
ansible-playbook playbook.yml --limit webservers

# 只对特定主机执行
ansible-playbook playbook.yml --limit web1.example.com

6. 使用 gather_facts: no 加速

如果不需要系统信息,可以禁用:

1
2
3
4
5
6
7
8
---
- name: 快速执行
hosts: localhost
gather_facts: no
tasks:
- name: 简单任务
debug:
msg: "Hello"

7. 使用 asyncpoll 执行长时间任务

1
2
3
4
5
6
7
8
9
---
- name: 长时间任务
hosts: localhost
tasks:
- name: 长时间运行的任务
command: /usr/bin/long-running-script.sh
async: 3600
poll: 10
register: long_task

8. 使用 delegate_to 在特定主机执行

1
2
3
4
5
6
7
8
9
10
11
---
- name: 委托任务
hosts: webservers
tasks:
- name: 在本地执行
command: echo "local"
delegate_to: localhost

- name: 在特定主机执行
command: echo "specific"
delegate_to: db1.example.com

常用命令速查

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
# 测试连接
ansible all -i inventory.ini -m ping

# 执行命令
ansible all -i inventory.ini -a "uptime"

# 执行 Playbook
ansible-playbook playbook.yml

# 指定 Inventory
ansible-playbook playbook.yml -i inventory.ini

# 指定标签
ansible-playbook playbook.yml --tags install

# 检查模式
ansible-playbook playbook.yml --check

# 显示差异
ansible-playbook playbook.yml --diff

# 限制主机
ansible-playbook playbook.yml --limit webservers

# 增加详细输出
ansible-playbook playbook.yml -v
ansible-playbook playbook.yml -vv
ansible-playbook playbook.yml -vvv

# 使用 Vault
ansible-vault create secrets.yml
ansible-vault edit secrets.yml
ansible-playbook playbook.yml --ask-vault-pass

总结

本教程涵盖了 Ansible 从入门到进阶的主要内容:

  1. 基础:安装、连接测试、简单命令
  2. Playbook:编写和执行 Playbook
  3. 变量:使用变量和变量文件
  4. 控制流:条件判断、循环、错误处理
  5. 高级特性:Roles、Templates、Handlers、Tags
  6. 安全:Vault 加密敏感数据
  7. 最佳实践:项目结构、配置优化

下一步学习方向

  • 学习更多 Ansible 模块
  • 创建自定义模块
  • 使用 Ansible Galaxy 分享和使用 Roles
  • 学习 Ansible Tower/AWX(Web UI)
  • 集成到 CI/CD 流程

参考资源


祝你学习愉快!