2024年2月

今天是春节的最后一天,因为工作上临时有点事,很不情愿的打开电脑看着也就 10 天没看代码觉得非常陌生。

之后便准备将迟迟未写的 2023 总结补完,这个传统从16年至今已经坚持将近 7 年时间了,今年当然也不能意外。

健身

今年要说最让我印象深刻的事就是健身了,为此我投入了大量的时间。



我记得是在 22 年四月份当时是因为确实长胖太明显了,下定决心找个教练进行训练,效果确实也有。

去年也分享过,最后从 75kg 减到 66kg;但大部分时间都是被动的进行训练,所以到了 23 年初的时候其实就反弹不少了。

而今年最大的不同是我由原先的被动健身改为主动了,甚至到后面一天不练还浑身不舒服。

所以今年我大部分时间都是自己锻炼,因为我是个 I 人,比较喜欢一个人,所以夏天的时候是每天早上 7 点多去健身房然后再去公司。

到了冬天早上确实是起不来,就改为了中午去训练。

就这样不知不觉就坚持了大半年,直到现在。

训练日志见文末。



甚至现在偶尔找教练训练时,他说我比他练的都勤

本文分享自华为云社区《
【理解云容器网络】2-基础篇-ipvs介绍
》,作者: 可以交个朋友。

IPVS简介

ipvs是工作在Linux内核态的4层负载均衡;和用户态的负载均衡软件(如nginx、haproxy)功能类似:作为客户端访问的统一入口并将访问请求根据调度算法转给后端真实的服务器。相比于用户态负载均衡,ipvs为Linux内核子模块性能更强,但ipvs仅工作在4层无法处理7层数据(比如SSL证书、修改HTTP请求头)。

IPVS调度算法

IPVS是如何决策应该把请求调度到哪个后端RS(Real Server)上的呢?这是由负载均衡调度算法决定的。IPVS常用的调度算法有:

  • 轮询(Round Robin):IPVS认为集群内每台RS都是相同的,会轮流进行调度分发。从数据统计上看,RR模式是调度最均衡的。

  • 加权轮询(Weighted Round Robin):IPVS会根据RS上配置的权重,将消息按权重比分发到不同的RS上。可以给性能更好的RS节点配置更高的权重,提升集群整体的性能。

  • 最小连接数(Least Connections):IPVS会根据集群内每台RS的连接数统计情况,将消息调度到连接数最少的RS节点上。在长连接业务场景下,LC算法对于系统整体负载均衡的情况较好;但是在短连接业务场景下,由于连接会迅速释放,可能会导致消息每次都调度到同一个RS节点,造成严重的负载不均衡。

  • 加权最小连接数(Weighted Least Connections):最小连接数算法的加权版~

  • 地址哈希(Address Hash):LB上会保存一张哈希表,通过哈希映射将客户端和RS节点关联起来。

IPVS转发模式

根据调度算法选择一个合适的后端RS节点,IPVS怎么将数据转发给后端RS呢?
IPVS支持三种转发模式:

  • DR模式(Direct Routing)
  • NAT模式(Network Address Translation)
  • IP隧道(IP tunneling)
三种转发模式性能从高到低:DR > NAT > IP隧道

DR模式

DR模式下,客户端的请求包到达负载均衡器的虚拟服务IP端口后,负载均衡器不会改写请求包的IP和端口,但是会改写请求包的MAC地址为后端RS的MAC地址,然后将数据包转发;真实服务器处理请求后,响应包直接回给客户端,不再经过负载均衡器。所以DR模式的转发效率是最高的。
image.png

DR模式的特点:

  • 数据包在LB转发过程中,源/目的IP端口都不会变化。LB只是将数据包的MAC地址改写为RS的MAC地址,然后转发给相应的RS。所以LB必须和后端RS节点在同一个子网
  • 每台RS上都必须在环回网卡(lo)上绑定VIP。因为LB转发时并不会改写数据包的目的IP,所以RS收到的数据包的目的IP仍是VIP,为了保证RS能够正确处理该数据包,而不是丢弃,必须在RS的环回网卡上绑定VIP。这样RS会认为这个虚拟服务IP是自己的IP,自己是能够处理这个数据包的,否则RS会直接丢弃该数据包。
  • RS上的业务进程必须监听在环回网卡的VIP上,且端口必须和LB上的虚拟服务端口一致。因为LB不会改写数据包的目的端口,所以RS服务的监听端口必须和LB上虚拟服务端口一致,否则RS会直接拒绝该数据包。
  • RS处理完请求后,响应直接回给客户端,不再经过LB。因为RS收到的请求数据包的源IP是客户端的IP,所以理所当然RS的响应会直接回给客户端,而不会再经过LB。这时候要求RS和客户端之间的网络是可达的。

NAT模式

NAT模式下请求包和响应包都需要经过LB处理。当客户端的请求到达LB后,LB会对请求包做目的地址转换(DNAT),将请求包的目的IP改写为RS的IP。RS处理请求后将响应返回给LB,当LB收到RS的响应后,LB会对响应包做源地址转换(SNAT),将响应包的源IP改写为LB的VIP。
image.png

NAT模式的特点:

  • LB会修改数据包的地址。对于请求包,会进行DNAT;对于响应包,会进行SNAT。

  • LB会透传客户端IP到RS(DR模式也会透传)。虽然LB在转发过程中做了NAT转换,但是因为只是做了部分地址转发,所以RS收到的请求包里是能看到客户端IP的。

  • 需要将RS的默认网关地址配置为LB的浮动IP地址。因为RS收到的请求包源IP是客户端的IP,为了保证响应包在返回时能走到LB上面,所以需要将RS的默认网关地址配置为LB的虚拟服务IP地址。当然,如果客户端的IP是固定的,也可以在RS上添加明细路由指向LB的虚拟服务IP,不用改默认网关。

  • LB和RS须位于同一个子网,并且客户端不能和LB/RS位于同一子网。因为需要将RS的默认网关配置为LB的虚拟服务IP地址,所以需要保证LB和RS位于同一子网。又因为需要保证RS的响应包能走回到LB上,则客户端不能和RS位于同一子网。否则RS直接就能获取到客户端的MAC,响应包就直接回给客户端了,也就走不到LB上面了。这时候由于没有LB做SNAT,客户端收到的响应包源IP是RS的IP,而客户端的请求包目的IP是LB的虚拟服务IP,这时候客户端无法识别响应包,会直接丢弃。

IP隧道模式

隧道模式下LB将原始请求报文封装在另一个IP报文中,再将封装好的IP报文转发给后端RS;后端RS服务器收到报文后,先将报文解封获得原报文中目标地址为VIP的报文,服务器发现VIP地址被配置在本地的IP隧道设备上,所以就处理这个请求,然后根据路由表将响应报文直接返回给客户。

image.png

隧道模式的特点:

  • LB和RS节点不用处于同一子网;解除了NAT模式和DR模式的限制。
  • LB和RS节点必须都支持隧道技术,且RS节点也需要在TUN网卡上配置VIP地址;因为LB通过隧道发送报文,RS节点必须支持隧道才能解封装,解封装后拿到原始报文的目的地址为VIP,如果RS节点上没有配置VIP,则会丢弃报文。
  • RS节点必须能访问互联网;因为RS节点是直接将响应报文返回给客户端,所以必须能访问外网。

命令演示

IPVS为内核子模块,需要用ipvsadm命令添加虚拟服务规则;IPVS与ipvsadm的关系就和netfilter与iptables一样。
ipvsadm命令参数展示

Commands:--add-service     -A         增加一个虚拟服务--edit-service     -E        修改一个虚拟服务--delete-service   -D         删除一个虚拟服务--clear           -C       清理所有虚拟服务--restore         -R        从标准输入获取ipvsadm命令。一般结合下边的-S使用。--save            -S       从标准输出输出虚拟服务器的规则。可以将虚拟服务器的规则保存,在以后通过-R直接读入,以实现自动化配置。--add-server      -a        为虚拟服务添加一个real server(RS)--edit-server     -e        修改虚拟服务中的RS--delete-server   -d         删除虚拟服务中的RS--list            -L|-l    列出虚拟服务表中的所有虚拟服务。可以指定地址。添加-c显示连接表。--help            -h        显示帮助信息

Options:
--tcp-service -t service-address 指定虚拟服务为tcp服务。service-address要是host[:port]的形式。--udp-service -u service-address 指定虚拟服务为udp服务。service-address要是host[:port]的形式。--scheduler -s scheduler 指定调度算法。调度算法可以指定以下10种:rr(轮询),wrr(权重),lc(最后连接),wlc(权重),lblc(本地最后连接),lblcr(带复制的本地最后连接),dh(目的地址哈希),sh(源地址哈希),sed(最小期望延迟),nq(永不排队)。默认调度算法为wlc。--real-server -r server-address 为虚拟服务指定数据可以转发到的真实服务器的地址。可以添加端口号。如果没有指定端口号,则等效于使用虚拟地址的端口号。--gatewaying -g 指定转发模式为DR(direct routing) (default)--ipip -i 指定转发模式为ip隧道(tunneling)--masquerading -m 指定转发模式为NAT模式(NAT)--connection -c 列出当前的IPVS连接。
环境准备;VM1/VM2/VM3都是在client上的VMware虚拟机。VMware网络模式为NAT。
设备名称 设备ip
client 7.249.241.35
VM1(LB) ip:192.168.81.128
vip: 192.168.81.100
VM2(RS1) 192.168.81.129
VM3(RS2) 192.168.81.130

确保LB节点上开启contrack和forward功能

echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
echo
"net.ipv4.vs.conntrack=1" >> /etc/sysctl.conf
sysctl
-p

在虚拟机VM1(LB)上安装ipvsadm命令

yum install ipvsadm

在虚拟机VM1(LB)上为网卡添加一个VIP

[root@vm1 ~]# ip addr add 192.168.81.100/24dev eth0
[root@vm1
~]# ip a s eth02: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000link/ether fa:16:3e:68:5a:12brd ff:ff:ff:ff:ff:ff
inet
192.168.81.128/24 brd 172.16.2.255 scope global noprefixroute dynamiceth0
valid_lft 102241123sec preferred_lft 102241123sec
inet
192.168.81.100/24 scope globalsecondary eth0
valid_lft forever preferred_lft forever
inet6 fe80::f816:3eff:fe68:5a12
/64scope link
valid_lft forever preferred_lft forever

在虚拟机VM1(LB)上添加ipvs虚拟配置,并指定调度算法为轮询

ipvsadm -At 192.168.81.100:80 -s rr

在虚拟机VM1(LB)上添加RS节点

ipvsadm -at 192.168.81.100:80 -r 192.168.81.129:80 -m
ipvsadm
-at 192.168.81.100:80 -r 192.168.81.130:80 -m

在虚拟机VM1(LB)上查看虚拟配置

[root@test ~]# ipvsadm -ln
IP Virtual Server version
1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
->RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP
192.168.81.100:80rr-> 192.168.81.129:80 Masq 1 0 0 -> 192.168.81.130:80 Masq 1 0 0

为了使client能访问vip,确保client机器上有访问vip的路由,
192.168.81.1
为VMware的虚拟网卡VMnet8的ip

image.png

由于本次环境的LB/RS都是通过VMware虚拟出来的,虚拟机和client互通,为了使RS节点将响应报文返回给LB,需要在两个RS节点上添加路由,使响应报文经过LB从而把响应报文的源地址换回vip

#目的地址为什么不是客户端ip?因为VMware用的nat模式,client的请求到达LB时,VMware会把数据包源ip改为VMnet8网卡的地址`192.168.81.1`,也就是会做SNAT
ip route add
192.168.81.1 via 192.168.81.128 dev eth0

访问测试,LB将请求轮询转发给后端RS节点

image.png

点击关注,第一时间了解华为云新鲜技术~

在 Git 中,通常使用 git merge 命令来将一个分支的更改合并到另一个分支。如果你只想合并某个分支的一部分代码,可以使用以下两种方法:

1.批量文件合并

1.1.创建并切换到一个新的临时分支

首先,从要合并的源分支(即要提取代码的分支)中创建并切换到一个新的临时分支。这样可以在该分支上进行修改,以便选择性地合并代码

git checkout -b temp-branch source-branch
  • temp-branch 是临时分支的名称
  • source-branch 是要提取代码的源分支的名称。

1.2.重置临时分支

使用 git reset 命令来将临时分支重置到源分支的某个特定提交,这样就可以选择性地选择要合并的代码

git reset <commit-hash>
  • commit-hash 是源分支中你想要合并代码的特定提交的哈希值
  • 如果你只想获取最新提交的哈希值,可以使用 git log 命令的 --oneline 选项,以简化输出
  • git log --oneline
  • file

1.3.添加、提交和推送更改

在临时分支上进行必要的更改,然后将这些更改添加、提交并推送到远程仓库。

git add .
git commit -m "Partial merge from source-branch"
git push origin temp-branch

1.4.合并到目标分支

现在,你可以切换到目标分支,并使用 git merge 命令将临时分支中的更改合并到目标分支中。

git checkout target-branch
git merge temp-branch

1.5.解决可能的冲突

如果有任何冲突,在合并过程中会被提示,并且需要手动解决这些冲突

1.6.删除临时分支

如果你已经成功地合并了临时分支中的部分代码,那么可以将它删除

git branch -d temp-branch

通过以上步骤,你可以在 Git 中选择性地合并某个分支的部分代码到另一个分支中。记得在操作前做好备份,确保不会丢失重要的更改

2.部分文件合并

如果你只想合并分支 A 中的某几个文件到当前分支(假设为目标分支),你可以使用 git checkout 命令来检出分支 A 中的指定文件,然后将这些文件复制到当前分支,最后提交更改。以下是具体的步骤:

2.1.检出分支 A 中的指定文件

git checkout A <path/to/file1> <path/to/file2> ...
  • 其中
    path/to/file1
    ,
    path/to/file2
    , 等等是你想要合并的文件的路径。

2.2.将文件复制到当前分支

如果只是简单地想要覆盖当前分支上的对应文件,你可以直接将文件复制到当前工作目录中

2.3.添加、提交更改

添加并提交你所复制的文件到当前分支

git add .
git commit -m "Merge selected files from branch A"

这样就完成了只合并分支 A 中的指定文件到当前分支的操作。需要注意的是,这种方法不会保留分支 A 中的提交历史,它只是将特定文件的最新版本复制到当前分支,并创建一个新的提交。如果需要保留提交历史,你可能需要考虑使用 git cherry-pick 命令来选择性地将分支 A 中的特定提交合并到当前分支

3.git cherry-pick选择性合并文件

git cherry-pick 命令用于选择性地将一个或多个提交从一个分支应用到另一个分支上。这个命令可以用于合并单个提交或一系列提交,而不需要将整个分支合并过来。

3.1.git cherry-pick 的基本用法

git cherry-pick <commit-hash-1> <commit-hash-2> ...
  • commit-hash-1
  • commit-hash-2
    , 等等是你想要应用的提交的哈希值。

3.2.切换到目标分支

首先,确保你在要应用更改的目标分支上

git checkout target-branch

3.3.应用提交

然后使用 git cherry-pick 命令来应用你想要合并的提交

git cherry-pick <commit-hash-1> <commit-hash-2> ...

这将会将指定的提交应用到当前分支中

3.4.解决冲突

如果在 cherry-pick 过程中出现了冲突,需要手动解决这些冲突

3.5.提交更改

解决冲突后,使用 git commit 来提交这些更改

git commit

如果你只是想要使用默认提交消息,你可以直接运行
git commit
命令,
Git
将会使用预设的提交消息。

这样,你就可以使用
git cherry-pick
命令将特定提交从一个分支合并到另一个分支中

前言

这不是高支模项目需要嘛,他们用传统算法切那个横杆竖杆流程复杂耗时很长,所以想能不能用机器学习完成这些工作,所以我就来整这个工作了。

工欲善其事,必先利其器,在正式开始之前,我们先要搞懂如何切分数据集。

本系列文章所用的核心骨干网络代码主要来自
点云处理:实现PointNet点云分割

使用的数据集类型主要为SharpNet,这篇文章里主要是讲如何使用CC切出指定的对象,并将其转换成我们想要的SharpNet数据集。

之后可能写一个番外,简单说说如何使用semantic-segmentation-editor工具进行简单的点云分割和解析吧,最近也摸了一下,但是发现这个工具貌似没有CC好用。

如果有人问起再写吧,有点折腾,不过也还好。

什么是SharpNet数据集?

我们可以在
LARGE-SCALE 3D SHAPE RECONSTRUCTION AND SEGMENTATION
FROM SHAPENET CORE55

网站上下载到SharpNet的数据集和标签,我们下载下来解压看看里面的结构
在这里插入图片描述
以下是训练集点云文件组
在这里插入图片描述
在这里插入图片描述

以下是训练集点云的标签组

也就是说实际上是一个pts文件对应一个.seg文件。

其中pts文件好理解,就是一个个的明文点云,内容如下:

在这里插入图片描述

打开seg文件,里面行数和同名的pts文件行数相同,

在这里插入图片描述

这个.seg文件中代表的意思就是对应行数的点所对应的label标签,通常以一个数字来表示,比如1是背景,2,3,4代表各种各样的对象,具体每个数字对应的对象是什么。

如何标注点云文件

上文中简单说了下SharpNet的规则,那么本章就简单说说如何标注点云文件

主要可以参考这篇文章,我这里仅展示简单的流程:

如何利用CloudCompare软件进行点云数据标注

比如我现在有一个这样的高支模点云,如果我想要做一个横杆的检测,那么我们就需要把横杆全部截出来

1.切割

在这里插入图片描述

先点击需要切片的点云文件,然后点这个剪刀进入剪切模式
在这里插入图片描述
先用左键划线工具框选住一个横杆,选完了之后单击右键确定选框,这个时候点击这个红色的多边形(选中框选内容)完成切割,再点击右边的这个绿色勾

在这里插入图片描述
在这里插入图片描述

这个时候切片就切出来了,可以看看效果

在这里插入图片描述

在这里插入图片描述

2. 分类

完成了切割工作之后,要给这个切出来的片加上一个名字,就点上面这个加号,然后给定一个对象的名称,再给定一个值

在这里插入图片描述
我们切分的是水平支撑,那么就给它起个名字叫Support

在这里插入图片描述

至于值的话随便声明就行,无所谓,这里声明的是1.00,这个和后面的处理有关,当然了你不懂也无所谓,如果你看懂了的话可以自己改这块的逻辑。

然后选中所有的点云,然后合并就行了
在这里插入图片描述
合并后可以看到被截取的这一块点云已经和原来的点云不一样了
在这里插入图片描述
在属性中找到Active可以找到被切分的点云分类

在这里插入图片描述
在这里插入图片描述
保存一下这个点云,保存成ASCII码的格式,以便我们对这个点云文件重新进行操作,以文本格式打开:
在这里插入图片描述
每个属性从上到下对应end_header后从左到右的一条条内容,比如第一行

7.099000 7.473000 4.869000 0 59 255 7.000000 1.000000 1.000000 0.000000

代表了一个点的
x坐标 y坐标 z坐标 r色 g色 b色 scalar_Intensity scalar_HSupport scalar_Support scalar_Original_cloud_index

我们在这里只需要判断Support的值就可以了,后面的几个scalar值就是标签的值,我们在这里只需要判断是不是Support对象,然后一行行地制作出.pts文件和.seg文件即可。

这里给出一段示例代码,需要注意的是,这个代码并不是自适应的识别所有标签,所以需要自己根据业务和自己的需要调整

CCSeperator.h

#pragma once

#include <QtWidgets/QMainWindow>
#include "ui_CCSeperator.h"
#include "qpoint.h"
#include "qvector.h"
#include "qfile.h"
#include "qfileinfo.h"
#include "qtextstream.h"
#include "qdir.h"
#include "qdebug.h"
//CC数据清洗工具
enum class PointType {
	None = 0,
	Support = 1,
	VSupport = 2
};

struct CCPoint {
	float x = 0.00;
	float y = 0.00;
	float z = 0.00;

	PointType type = PointType::None;
};

class CCSeperator
{
	Q_OBJECT

public:
	CCSeperator();
	~CCSeperator();

	/// <summary>
	/// 读取指定点云文件并尝试解析到指定目录下
	/// </summary>
	void ReadFile(const QString& filePath, const QString& outputPath);
	QVector<CCPoint> vec_points;
};

CCSeperator.cpp

#include "CCSeperator.h"

CCSeperator::CCSeperator()
{

	this->ReadFile("J:\\output\\GF3_7.ply", "J:\\output");
}

CCSeperator::~CCSeperator()
{

}

void CCSeperator::ReadFile(const QString& filePath, const QString& outputPath)
{
	//尝试读取指定目录下的文件
	QFile file(filePath);
	QString fileName = QFileInfo(file).baseName();
	if (!file.exists()) {
		qDebug() << " file not exist";
		return;
	}
	this->vec_points.clear();
	qDebug() << file.open(QIODevice::ReadWrite | QIODevice::Text);

	QTextStream in(&file);
	bool blnEndHead = false;
	while (!in.atEnd()) {
		QString line = in.readLine();
		if (line.contains("end_header")) {
			blnEndHead = true;
			continue;
		}

		if (!blnEndHead) continue;

		QStringList list = line.split(" ");
		CCPoint point;
		point.x = list[0].toFloat();
		point.y = list[1].toFloat();
		point.z = list[2].toFloat();

		//这个对应的是识别的列,这里是第七行
		if (list[6].toFloat() == 1.0000) {
			point.type = PointType::Support;
		}
		else {
			point.type = PointType::None;
		}
		this->vec_points.append(point);

	}
	//注入点完成后,需要将其导出到指定目录下

	QDir dir(outputPath);
	if (!dir.exists()) {
		dir.mkpath(dir.absolutePath());
	}
	QFile file_out_data(outputPath + QString("/Data/%1.pts").arg(fileName));
	QTextStream out(&file_out_data);

	QFile file_out_label(outputPath + QString("/Label/%1.seg").arg(fileName));
	QTextStream out_label(&file_out_label);

	file_out_data.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate);
	file_out_label.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate);
	for (auto item : this->vec_points) {
		//将所有的点写入到指定目录下


		//无论如何,正常的点都需要写入
		QString fileContent = QString("%1 %2 %3").arg(item.x).arg(item.y).arg(item.z);
		out << fileContent << endl;

		QString label = QString("%1").arg(static_cast<qint32>(item.type));
		out_label << label << endl;

	}
	file_out_data.close();
	file_out_label.close();
}

这样洗出来的数据就是这样的:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这样我们就完成了自制SharpNet数据集的过程