2024年3月

change-rootfs-by-pivot-root.png
change-rootfs-by-pivot-root.png

本文为从零开始写 Docker 系列第四篇,在
mydocker run
基础上使用
pivotRoot
系统调用切换 rootfs 实现容器和宿主机之间的文件系统隔离。


完整代码见:
https://github.com/lixd/mydocker
欢迎 Star


推荐阅读以下文章对 docker 基本实现有一个大致认识:


开发环境如下:

root@mydocker:~# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.2 LTS
Release: 20.04
Codename: focal
root@mydocker:~# uname -r
5.4.0-74-generic

注意:需要使用 root 用户


如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。

搜索公众号【
探索云原生
】即可订阅



1. 概述

前面几节中,我们通过
Namespace

Cgroups
技术创建了一个简单的容器,实现了视图隔离和资源限制。

但是大家应该可以发现,
容器内的目录还是当前运行程序的宿主机目录
,而且如果运行一下 mount 命令可以看到继承自父进程的所有挂载点。

这貌似和平常使用的容器表现不同

因为这里缺少了
镜像
这么一个重要的特性。

Docker 镜像可以说是一项伟大的创举,它使得容器传递和迁移更加简单,那么这一节会做一个简单的镜像,让容器跑在有镜像的环境中。

即:
本章会为我们切换容器的 rootfs,以实现文件系统的隔离


2. 准备 rootfs

Docker 镜像包含了文件系统,所以可以直接运行,我们这里就先弄个简单的,直接将某个镜像中的所有内容作为我们的 rootfs 进行挂载。

即:
先在宿主机上某一个目录上准备一个精简的文件系统,然后容器运行时挂载这个目录作为 rootfs

首先使用一个最精简的镜像
busybox
来作为我们的文件系统。

busybox 是一个集合了非常多 UNIX 工具的箱子,它可以提供非常多在 UNIX 环境下经常使用的命令,可以说 busybox 提供了一个非常完整而且小巧的系统。

因此我们先使用它来作为第一个容器内运行的文件系统。

获得 busybox 文件系统的 rootfs 很简单,可以使用 docker export 将一个镜像打成一个 tar包,并解压,解压目录即可作为文件系统使用

首先拉取镜像

docker pull busybox

然后使用该镜像启动一个容器,并用 export 命令将其导出成一个 tar 包

# 执行一个交互式命令,让容器能一直后台运行
docker run -d busybox top
# 拿到刚创建的容器的 Id
containerId=$(docker ps --filter "ancestor=busybox:latest"|grep -v IMAGE|awk '{print $1}')
echo "containerId" $containerId
# export 从容器导出
docker export -o busybox.tar $containerId

最后将 tar 包解压

mkdir busybox
tar -xvf busybox.tar -C busybox/

这样就得到了 busybox 文件系统的 rootfs ,可以把这个作为我们的文件系统使用。

这里的 rootfs 指解压得到的 busybox 目录

busybox 中的内容大概是这样的:

[root@docker ~]# ls -l busybox
total 16
drwxr-xr-x 2 root      root      12288 Dec 29  2021 bin
drwxr-xr-x 4 root      root         43 Jan 12 03:17 dev
drwxr-xr-x 3 root      root        139 Jan 12 03:17 etc
drwxr-xr-x 2 nfsnobody nfsnobody     6 Dec 29  2021 home
drwxr-xr-x 2 root      root          6 Jan 12 03:17 proc
drwx------ 2 root      root          6 Dec 29  2021 root
drwxr-xr-x 2 root      root          6 Jan 12 03:17 sys
drwxrwxrwt 2 root      root          6 Dec 29  2021 tmp
drwxr-xr-x 3 root      root         18 Dec 29  2021 usr
drwxr-xr-x 4 root      root         30 Dec 29  2021 var

可以看到,内容和一个完整的文件系统基本是一模一样的。

注意:
rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核

在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。


3. 挂载 rootfs

把之前的 busybox rootfs 移动到
/root/busybox
目录下备用。


实现原理

使用
pivot_root
系统调用来切换整个系统的 rootfs

,配合上
/root/busybox
来实现一个类似镜像的功能。

pivot_root
是一个系统调用,主要功能是去改变当前的 root 文件系统

原型如下:

#include <unistd.h>

int pivot_root(const char *new_root, const char *put_old);
  • new_root :新的根文件系统的路径。
  • put_old :将原根文件系统移到的目录。

使用
pivot_root
系统调用后,原先的根文件系统会被移到
put_old
指定的目录,而新的根文件系统会变为
new_root
指定的目录。这样,当前进程就可以在新的根文件系统中执行操作。

注意:new_root 和 put_old 不能同时存在当前 root 的同一个文件系统中。

pivotroot 和 chroot 有什么区别?

  • pivot_root 是把整个系统切换到一个新的 root 目录,会移除对之前 root 文件系统的依赖,这样你就能够 umount 原先的 root 文件系统。

  • 而 chroot 是针对某个进程,系统的其他部分依旧运行于老的 root 目录中。


具体实现

具体实现如下:

/*
*
Init 挂载点
*/

func setUpMount() {
 pwd, err := os.Getwd()
 if err != nil {
  log.Errorf("Get current location error %v", err)
  return
 }
 log.Infof("Current location is %s", pwd)

 // systemd 加入linux之后, mount namespace 就变成 shared by default, 所以你必须显示
 // 声明你要这个新的mount namespace独立。
 // 如果不先做 private mount,会导致挂载事件外泄,后续执行 pivotRoot 会出现 invalid argument 错误
 err = syscall.Mount("""/""", syscall.MS_PRIVATE|syscall.MS_REC, "")

 err = pivotRoot(pwd)
 if err != nil {
  log.Errorf("pivotRoot failed,detail: %v", err)
  return
 }

 // mount /proc
 defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
 _ = syscall.Mount("proc""/proc""proc"uintptr(defaultMountFlags), "")
 // 由于前面 pivotRoot 切换了 rootfs,因此这里重新 mount 一下 /dev 目录
 // tmpfs 是基于 件系 使用 RAM、swap 分区来存储。
 // 不挂载 /dev,会导致容器内部无法访问和使用许多设备,这可能导致系统无法正常工作
 syscall.Mount("tmpfs""/dev""tmpfs", syscall.MS_NOSUID|syscall.MS_STRICTATIME, "mode=755")
}

func pivotRoot(root string) error {
 /**
   NOTE:PivotRoot调用有限制,newRoot和oldRoot不能在同一个文件系统下。
   因此,为了使当前root的老root和新root不在同一个文件系统下,这里把root重新mount了一次。
   bind mount是把相同的内容换了一个挂载点的挂载方法
 */

 if err := syscall.Mount(root, root, "bind", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
  return errors.Wrap(err, "mount rootfs to itself")
 }
 // 创建 rootfs/.pivot_root 目录用于存储 old_root
 pivotDir := filepath.Join(root, ".pivot_root")
 if err := os.Mkdir(pivotDir, 0777); err != nil {
  return err
 }
 // 执行pivot_root调用,将系统rootfs切换到新的rootfs,
 // PivotRoot调用会把 old_root挂载到pivotDir,也就是rootfs/.pivot_root,挂载点现在依然可以在mount命令中看到
 if err := syscall.PivotRoot(root, pivotDir); err != nil {
  return errors.WithMessagef(err, "pivotRoot failed,new_root:%v old_put:%v", root, pivotDir)
 }
 // 修改当前的工作目录到根目录
 if err := syscall.Chdir("/"); err != nil {
  return errors.WithMessage(err, "chdir to / failed")
 }

 // 最后再把old_root umount了,即 umount rootfs/.pivot_root
 // 由于当前已经是在 rootfs 下了,就不能再用上面的rootfs/.pivot_root这个路径了,现在直接用/.pivot_root这个路径即可
 pivotDir = filepath.Join("/"".pivot_root")
 if err := syscall.Unmount(pivotDir, syscall.MNT_DETACH); err != nil {
  return errors.WithMessage(err, "unmount pivot_root dir")
 }
 // 删除临时文件夹
 return os.Remove(pivotDir)
}

然后再 build cmd 的时候指定:

func NewParentProcess(tty bool) (*exec.Cmd, *os.File) {
    cmd := exec.Command("/proc/self/exe""init")
    // .. 省略其他代码
    // 指定 cmd 的工作目录为我们前面准备好的用于存放busybox rootfs的目录
    cmd.Dir = "/root/busybox"
    return cmd, writePipe
}

到此这一小节就完成了,测试一下。


4. 测试

测试比较简单,只需要执行 ls 命令,即可根据输出内容确定文件系统是否切换了。

root@mydocker:~/feat-rootfs/mydocker# go build .
root@mydocker:~/feat-rootfs/mydocker# ./mydocker run -it  /bin/ls
{"level":"info","msg":"resConf:\u0026{ 0  }","time":"2024-01-12T16:19:32+08:00"}
{"level":"info","msg":"command all is /bin/ls","time":"2024-01-12T16:19:32+08:00"}
{"level":"info","msg":"init come on","time":"2024-01-12T16:19:32+08:00"}
{"level":"info","msg":"Current location is /root/busybox","time":"2024-01-12T16:19:32+08:00"}
{"level":"info","msg":"Find path /bin/ls","time":"2024-01-12T16:19:32+08:00"}
bin   dev   etc   home  proc  root  sys   tmp   usr   var

可以看到,现在打印出来的就是
/root/busybox
目录下的内容了,说明我们的 rootfs 切换完成。


如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。

搜索公众号【
探索云原生
】即可订阅



5.小结

本章核心如下:

  • 准备 rootfs:将运行中的 busybox 容器导出并解压后作为 rootfs
  • 挂载 rootfs:使用 pivotRoot 系统调用,将前面准备好的目录作为容器的 rootfs 使用

在切换 rootfs 之后,容器就实现了和宿主机的文件系统隔离。

本章使用 pivotRoot 实现文件系统隔离,加上前面基于 Namespace 实现的视图隔离,基于 Cgroups 实现的资源限制,至此我们已经实现了一个 Docker 容器的几大核心功能。


完整代码见:
https://github.com/lixd/mydocker
欢迎 Star

相关代码见
feat-rootfs
分支,测试脚本如下:

需要提前在 /root/busybox 目录准备好 rootfs,具体看本文第二节。

# 克隆代码
git clone -b feat-rootfs https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依赖并编译
go mod tidy
go build .
# 测试 查看文件系统是否变化
./mydocker run -it  /bin/ls

TCP和UDP可以使用同一个端口号吗?

首先说答案:可以。怎么理解呢?

我想这个问题要从计算机网络通信谈起,学过计算机网络的同学,可能都还记得7层或者4层网络模型,TCP/UDP属于其中的传输层协议,在传输层之下是网络层,网络层主要通过IP协议来进行通信,这也是我们日常程序开发中能够接触到的最底层了,再往下的数据链路层和物理层就不是我们这些普通程序员需要关心的了。

IP

我们先具体看下网络层。在IP网路层,发送者向接收者传输数据的时候,首先需要知道接收者的IP地址,
IP地址可以在网络中唯一标识一台计算机


然后数据就可以根据IP协议抵达接收者所在的计算机,但是接收者所在的计算机上运行了几十个程序,计算机应该把这个数据交给哪个程序呢?

端口号

这就像快递员到达了一栋大楼,下一步它怎么把快递送到对应的用户手中呢?聪明的你一定想到了,那就是门牌号。

在计算机中,端口号就是门牌号。计算机的操作系统可以为不同的程序绑定不同的端口号,这样发送者发送数据时不仅要设置接收者的IP,还要加上接收者的端口号,如此接收者所在的计算机就能把数据转发给正确的程序了。

TCP/UDP

那么TCP和UDP能不能使用同一个端口号呢?其实在查找端口号之前还有一个传输层协议的处理过程,操作系统收到数据后,会先查看数据包使用的是TCP协议还是UDP协议,然后再根据协议进行不同的解析处理,提取到数据后,再转发到拥有对应端口的程序。

所以TCP和UDP是可以使用相同的端口号的,这在现实中也是常见的。比如 DNS(域名系统)可能需要同时支持 TCP 和 UDP 查询,这两种查询就都可以通过53这个标准端口来进行接收和响应。

但是在同一个传输协议下,端口号就不能相同了。如果相同,操作系统的协议栈就不知道该把这个数据包转给哪个程序了,这种设计会增加很多麻烦。

有的同学可能会观察到一个现象,那就是同一个计算机上的多个网站可以共享80或者443端口,这其实是应用层的能力,这些网站都寄宿在同一个Web服务器程序上,这个Web服务器程序绑定了80端口,Web服务器收到数据后再根据HTTP协议中的主机头(可以理解成域名)转发给不同的网站程序。

还有,如果你的电脑上有多个IP,那就更没有问题了。不同的IP代表不同的网络接口,即使都使用TCP协议,只要IP不同,端口号一样也完全不会冲突。

“IP+传输层协议+端口号”就是我们常说的套接字,它能确保数据从一个网络程序传递到另一个网络程序。大家如果直接使用TCP和UDP编程,就需要手动为套接字设置这几个参数。

示例

口说无凭,再给大家写个demo,使用go语言,简单易懂:

下边的程序会启动一个TCP服务器和一个UDP服务器,它们绑定相同的IP和端口号。这里为了方便测试,使用了127.0.0.1这个本机IP,你也可以换成局域网或者公网IP。

package main

import (
    "fmt"
    "net"
    "os"
)

func main() {
    // 定义监听的端口
    port := "127.0.0.1:12345"

    // 启动TCP服务器
    go startTCPServer(port)

    // 启动UDP服务器
    startUDPServer(port)
}

func startTCPServer(port string) {
    // 通过TCP协议监听端口
    l, err := net.Listen("tcp", port)
    if err != nil {
        fmt.Println("Error listening:", err.Error())
        os.Exit(1)
    }
    defer l.Close()
    fmt.Println("TCP Server Listening on " + port)
    
    // 持续接收TCP数据
    for {
        conn, err := l.Accept()
        if err != nil {
            fmt.Println("Error accepting: ", err.Error())
            os.Exit(1)
        }
        fmt.Println("Received TCP connection")
        conn.Close()
    }
}

func startUDPServer(port string) {
    // 通过UDP协议监听端口
    addr, err := net.ResolveUDPAddr("udp", port)
    if err != nil {
        fmt.Println("Error resolving: ", err.Error())
        os.Exit(1)
    }

    conn, err := net.ListenUDP("udp", addr)
    if err != nil {
        fmt.Println("Error listening: ", err.Error())
        os.Exit(1)
    }
    defer conn.Close()
    fmt.Println("UDP Server Listening on " + port)

    buffer := make([]byte, 1024)

    // 持续接收UDP数据
    for {
        n, _, err := conn.ReadFromUDP(buffer)
        if err != nil {
            fmt.Println("Error reading: ", err.Error())
            continue
        }
        fmt.Printf("Received UDP packet: %s\n", string(buffer[:n]))
    }
}

然后再创建两个客户端,一个是TCP客户端:

package main

import (
	"fmt"
	"net"
	"os"
)

func main() {
	// 连接到服务器
	conn, err := net.Dial("tcp", "localhost:12345")
	if err != nil {
		fmt.Println("Error connecting:", err.Error())
		os.Exit(1)
	}
	defer conn.Close()

	// 发送数据
	_, err = conn.Write([]byte("Hello TCP Server!"))
	if err != nil {
		fmt.Println("Error sending data:", err.Error())
		return
	}
	fmt.Println("Message sent to TCP server")
}

另一个是UDP客户端:

package main

import (
	"fmt"
	"net"
	"os"
)

func main() {
	ServerAddr, err := net.ResolveUDPAddr("udp", "localhost:12345")
	if err != nil {
		fmt.Println("Error resolving: ", err.Error())
		os.Exit(1)
	}

	conn, err := net.DialUDP("udp", nil, ServerAddr)
	if err != nil {
		fmt.Println("Error dialing: ", err.Error())
		os.Exit(1)
	}
	defer conn.Close()

	// 发送数据
	_, err = conn.Write([]byte("Hello UDP Server!"))
	if err != nil {
		fmt.Println("Error sending data:", err.Error())
		return
	}
	fmt.Println("Message sent to UDP server")
}

我们可以看到,客户端发起请求的时候都使用了 localhost:12345 这个目标地址,其中的localhost 实际上是个域名,它会被本地计算机解析为 127.0.0.1。这块不清楚的可以看我之前写的这篇:

实际运行效果如下:


最后总结下:在网络通信中,同一台计算机中,TCP和UDP协议可以使用相同的端口号。每个网络进程中的套接字地址都是唯一的,由三元组(IP地址,传输层协议,端口号)标识。操作系统会根据数据包中的传输层协议(TCP或UDP)以及端口号,将接收到的数据正确地交付给相应的应用程序。

本文分享自华为云社区《
nginx.conf以configmap文件形式挂载到nginx容器中以及subpath使用场景
》,作者:可以交个朋友。

背景

nginx.conf通过configmap文件形式挂载到容器内,可以更加方便的修改nginx.conf配置

方案简介

将配置文件nginx.conf以configmap文件的方式挂载到容器中。为了更通用,可以将使用主nginx.conf include 指定xx.conf方式,主nginx.conf作为一个cm,具体xx.conf对应一个cm

configmap可以通过ENV环境变量和文件两种方式挂载到容器中,修改configmap后容器中对应的ENV环境变量不会更新;修改configmap后容器中对应的file会自动更新,如果以subpath方式挂载文件,文件内容不会自动更新

将nginx.conf作为configmap挂载到容器中

1.创建configmap

apiVersion: v1
kind: ConfigMap
metadata:
name: nginx
-confignamespace: defaultdata:
nginx.conf:
|+user nginx;
worker_processes
8;
error_log
/var/log/nginx/error.log warn;
pid
/var/run/nginx.pid;
events {
worker_connections
1024;
}
http {
include
/etc/nginx/mime.types;
default_type application
/octet-stream;
log_format main
'$remote_addr - $remote_user [$time_local] "$request"' '$status $body_bytes_sent "$http_referer"' '"$http_user_agent" "$http_x_forwarded_for"';
access_log
/var/log/nginx/access.log main;
sendfile on;
keepalive_timeout
65;
#gzip on;
include
/etc/nginx/conf.d/*.conf;
}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-server-config
namespace: default
data:
server1.conf: |+
server {
listen 80;
server_name server1.com;
location / {
root /usr/share/nginx/html/;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
server2.conf: |+
server {
listen 81;
server_name server2.com;
location / {
root /usr/share/nginx/html/;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}

2.部署nginx业务使用对应的cm

apiVersion: apps/v1
kind: Deployment
metadata:
labels:
version: v1
name: test
-reloadnamespace: defaultspec:
progressDeadlineSeconds:
600replicas:1revisionHistoryLimit:10selector:
matchLabels:
app: test
-reload
template:
metadata:
labels:
app: test
-reload
spec:
containers:
-image: nginx:latest
imagePullPolicy: Always
name: container
-1volumeMounts:- mountPath: /etc/nginx/conf.d
name: vol
-168233491311961268 - mountPath: /etc/nginx/nginx.conf
name: vol
-168249948123126427readOnly:truesubPath: nginx.conf
dnsPolicy: ClusterFirst
imagePullSecrets:
- name: default-secret
restartPolicy: Always
volumes:
-configMap:
defaultMode:
420name: nginx-server-config
name: vol
-168233491311961268 -configMap:
defaultMode:
420name: nginx-config
name: vol
-168249948123126427

subpath拓展

subpath的作用如下:

  • 避免覆盖。如果挂载路径是一个已存在的目录,则目录下的内容不会被覆盖。直接将configMap/Secret挂载在容器的路径,会覆盖掉容器路径下原有的文件,使用subpath选定configMap/Secret的指定的key-value挂载在容器中,则不会覆盖掉原目录下的其他文件
  • 文件隔离。pod中含有多个容器公用一个日志volume,不同容器日志路径挂载的到不同的子目录,而不是根路径(Subpath目录会在底层存储自动创建且权限为777,无需手动创建)

避免覆盖效果演示

1.创建一个工作负载nginx,并用普通方式挂载configmap配置文件

apiVersion: v1
kind: ConfigMap
metadata:
name: config
data:
test
-subpath.conf: |+test subpath;---apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: test
name: test
spec:
replicas:
1selector:
matchLabels:
app: test
template:
metadata:
labels:
app: test
spec:
volumes:
-configMap:
defaultMode:
420name: config
name: vol
-168249948123126427containers:-image: centos:latest
name: centos
command:
- /bin/bash
args:
- -c- while true;do sleep 1 &&echo hello;done
volumeMounts:
- mountPath: /tmp
name: vol
-168249948123126427

2.使用docker inspect ${容器id}命令查看容器挂载信息,挂载目标为tmp目录,tmp目录下原有内容被覆盖

cke_137.png

[root@test-746c64649c-pzztn /]# ls -l /tmp/total0lrwxrwxrwx1 root root 24 Feb 27 03:02 test-subpath.conf -> ..data/test-subpath.conf

3.创建一个工作负载nginx,并用subpath方式挂载configmap配置文件

apiVersion: v1
kind: ConfigMap
metadata:
name: config
data:
test
-subpath.conf: |+test subpath;---apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: test
name: test
spec:
replicas:
1selector:
matchLabels:
app: test
template:
metadata:
labels:
app: test
spec:
volumes:
-configMap:
defaultMode:
420name: config
name: vol
-168249948123126427containers:-image: centos:latest
name: centos
command:
- /bin/bash
args:
- -c- while true;do sleep 1 &&echo hello;done
volumeMounts:
- mountPath: /tmp/test-subpath.conf
name: vol
-168249948123126427subPath: test-subpath.conf

4.使用docker inspect ${容器Id}命令查看容器挂载信息,挂载目标为test-subpath.conf文件,所以tmp目录下原来的文件不会被覆盖

cke_138.png

[root@test-7b64fd6bb-56lpp /]# ls -l /tmp/total12
-rwx------ 1 root root 701 Dec  4  2020 ks-script-esd4my7v-rwx------ 1 root root 671 Dec  4  2020 ks-script-eusq_sc5-rw-r--r-- 1 root root  14 Feb 27 03:07 test-subpath.conf

文件隔离演示

1.创建工作负载test,使用hostPath卷类型持久化日志文件

apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: test
name: test
spec:
replicas:
2selector:
matchLabels:
app: test
template:
metadata:
labels:
app: test
spec:
volumes:
-hostPath:
path:
/tmp/log #该路径必须在节点上已存在
name: vol
-168249948123126427containers:-image: centos:latest
name: centos
env:
-name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
command:
- /bin/bash
args:
- -c- while true;do echo $(POD_NAME) >> /tmp/log/app.log && sleep 900;done
volumeMounts:
- mountPath: /tmp/log
name: vol
-168249948123126427subPathExpr: $(POD_NAME)

2.两个Pod实例调度至同一个节点

[root@test ~]# kubectl get pod -owide -l app=test
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
test
-69dfc665cd-2nhg5 1/1 Running 0 95s 172.16.4.59 172.16.2.172 <none> <none>test-69dfc665cd-z7rsj 1/1 Running 0 77s 172.16.4.25 172.16.2.172 <none> <none>

3.进入容器内查看日志文件

[root@test ~]# kubectl exec -it test-69dfc665cd-2nhg5 bash
[root@test
-69dfc665cd-2nhg5 /]# cat /tmp/log/app.log
test
-69dfc665cd-2nhg5
[root@test
-69dfc665cd-2nhg5 /]# exit
exit
[root@test
~]# kubectl exec -it test-69dfc665cd-z7rsj bash
[root@test
-69dfc665cd-z7rsj /]# cat /tmp/log/app.log
test
-69dfc665cd-z7rsj

4.在节点上查看挂载路径,每个Pod的日志文件用目录进行隔离,目录名为Pod名称

[root@172log]# pwd/tmp/log
[root@
172log]# ll
total
0drwxr-xr-x 2 root root 60 Feb 27 15:08 test-69dfc665cd-2nhg5
drwxr
-xr-x 2 root root 60 Feb 27 15:09 test-69dfc665cd-z7rsj
[root@
172 log]# cat test-69dfc665cd-2nhg5/app.log
test
-69dfc665cd-2nhg5
[root@
172 log]# cat test-69dfc665cd-z7rsj/app.log
test
-69dfc665cd-z7rsj

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

今年我们有一个眼高手低的计划,打算基于
Semantic Kernel
+
DashScope(阿里云模型服务灵积)
+
Qwen(通义千问大模型)
,结合园子已有的产品与应用场景,开发面向开发者的 AI 应用,并将整个过程与大家分享。

目前处于准备阶段,这篇博文分享的是遇到的第一个问题,并由此发起一个小开源项目。

通过
Semantic Kernel
使用大模型服务,必须要有对应这个模型服务的 Connector,而 Semantic Kernel 目前只提供了
OpenAI

HuggingFace
的 Connector,没有提供
DashScope
的 Connector。

面对这个问题,我们有2个选择:
1)使用使用已有的 OpenAI Connector,借助
one-api
(相当是一个 api 网关)连接 DashScope。
2)自己开发 DashScope Connector。

经过权衡考虑,我们选择了后者,选择了更难走的一条路。

当我们准备自己开发 DashScope Connector 时,发现 DashScope 没有提供官方版 .NET SDK,阿里云只提供了 Python 与 Java 版的 SDK。

找了非官方的 .NET 版 DashScope SDK,又不能满足我们的需求,于是我们在更难的路上选择了难上加难,决定自己开发一个简单版本的 DashScope SDK for .NET,并以此为基础发起一个小开源项目。

在动手开发 DashScope SDK 的时候,我们又发现官方 Java 与 Python 版 SDK 都没开源,连个参考都没有,难度系数又进一步增加。

面对这些小困难,我们没有知难而退,经过一周左右时间的努力,终于完成了一个初步版本的 DashScope SDK,今天发布出来,想抛砖引玉,借此发起一个小开源项目,如果您对基于
Semantic Kernel
+
DashScope
开发 AI 应用感兴趣,期待您的关注,更期待您贡献代码,github 仓库地址
https://github.com/cnblogs/dashscope-sdk

接下来,我们会基于
DashScope SDK for .NET
开发
Cnblogs.SemanticKernel.Connectors.DashScope
,为基于通义千问大模型的 RAG(Retrieval Augmented Generation) 做准备。

原文

代码

摘要

本文研究的是文档级关系抽取,即从文档中抽取出多个实体之间的关系。现有的方法主要是基于图或基于Transformer的模型,它们只考虑实体自身的信息,而忽略了关系三元组之间的全局信息。为了解决这个问题,本文提出了一种新的方法,它通过预测一个实体级关系矩阵来同时捕获局部和全局信息。这种方法类似于计算机视觉中的语义分割任务。本文的主要贡献是提出了一个
文档U形网络
,它由一个编码器模块和一个U形分割模块组成。编码器模块用于捕获实体的上下文信息,U形分割模块用于捕获图像风格特征图上的三元组之间的全局相互依赖性。本文在三个公开的数据集DocRED、CDR和GDA上进行了实验,结果表明,本文的方法可以达到最先进的性能。

1 Introduction

每个关系三元组可以向同一文本中的其他关系三元组提供信息。
为了捕获文档中多个三元组的相互依赖关系,本文将文档级关系抽取任务转化为一种实体级的分类问题[Jiang et al., 2019],也叫做表格填充[Miwa and Sasaki, 2014; Gupta et al., 2016],如图2所示。这种问题类似于语义分割(一个广泛应用的计算机视觉任务),它的目标是用卷积网络给图像的每个像素分配一个对应的类别标签。受此启发,本文提出了一种新颖的模型,名为文档U形网络(DocuNet),它将文档级关系抽取建模为语义分割问题。在这个模型中,本文将实体对之间的相关特征作为图像输入,然后预测每个实体对的关系类型作为像素级的输出。具体来说,本文设计了一个编码器模块来获取实体的上下文信息,以及一个U形分割模块来获取三元组之间的全局依赖信息。本文还提出了一种平衡的softmax方法来处理关系类别的不平衡分布。本文的主要贡献有以下几点:
  1. 首次将文档级关系抽取问题建模为语义分割问题。
  2. 提出了模型DocuNet,它能够有效地捕获文档级关系抽取的局部和全局信息。
  3. 在三个公开的数据集上进行了实验,证明了本文模型相比于现有的方法有显著的性能提升。

2 Related Work

一方面,本文受到了[
Jin et al., 2020
]的启发,他们是
首次考虑关系之间全局交互的研究
,而这方面的研究还很少见。另一方面,本文注意到卷积神经网络(CNN)在关系抽取(RE)领域已经有了很多应用,这些研究[Nguyen and Grishman, 2015; Shen and Huang, 2016]表明CNN可以有效地提取图像风格的特征图。因此,本文的工作也与[
Liu et al., 2020
]的研究有关,他们
将不完整的话语重写视为一种语义分割任务
,这激发了本文从计算机视觉的角度来探索RE问题。本文采用了U-Net [
Ronneberger et al., 2015
],它由一个收缩路径和一个对称的扩展路径组成,分别用于捕获上下文信息和实现精确的定位。
据本文所知,这是第一次将RE问题建模为语义分割任务

3 Methodology

3.1 Preliminary

给定一个文档

,其中包含一组实体

,任务是提取这些实体之间的关系。一个文档中可能有多个相同的实体。为了抽取实体



之间的关系,本文用一个

的矩阵

表示,其中





的关系类型。本文的目标是得到矩阵

的输出,这类似于语义分割的任务。本文按照实体在文档中首次出现的顺序排列矩阵

中的实体,利用实体间的相关性估计生成特征图,并把特征图当作一张图像。注意,输出的实体级关系矩阵

和语义分割中的像素级掩码是对应的,这就把关系抽取和语义分割联系起来了。本文的方法也适用于句子级别的关系抽取。由于文档中的实体更多,所以实体级关系矩阵可以学习更多的全局信息,从而提高性能。

3.2 Encoder Module

考虑一个文档

,其中包含了一些实体提及。为了标记实体的位置,在每个提及的开始和结束处添加特殊符号



。使用一个预训练的语言模型作为编码器,将文档中的每个词

转换为一个向量

,得到文档的嵌入表示:
由于有些文档的长度超过了语言模型的最大输入限制(例如 512),本文使用一个
滑动窗口
的方法来编码整个文档。对于每个窗口,取其嵌入的平均值作为最终的表示。然后,使用

的嵌入来表示每个实体提及,这是一种简单而有效的方法,参考了 [
Verga et al., 2018
]。
为了得到每个实体的嵌入,使用一种平滑的最大池化操作,即 logsumexp 池化,将一个实体的所有提及的嵌入进行聚合:
其中

是第

个实体的嵌入,

是该实体在文档中出现的次数,

是该实体的第

个提及的嵌入。这种池化操作可以有效地捕捉文档中提及的信息,得到一个
全局的实体表示
为了计算文档中实体之间的关系,本文构建一个实体级的关系矩阵,其中每个元素表示两个实体的相关性。使用一个

维的特征向量

来表示源实体

和目标实体

之间的
关系特征
。本文提出了两种计算

的方法,一种是
基于相似度
的方法,另一种是
基于上下文
的方法。基于相似度的方法是将



之间的三种相似度度量拼接起来,分别是
哈达玛积(element-wise product)、余弦相似度(cosine similarity)和双线性相似度(bi-linear similarity)
其中

是一个可学习的权重矩阵。这种方法
可以直接捕捉实体嵌入之间的相似程度,反映它们的关系强度

基于上下文的方法是利用实体感知的注意力机制,根据文档的嵌入和实体的重要性,计算出一个特征向量
其中

是一个可学习的权重矩阵,

是文档的嵌入,

是 transformer 中的头数,



分别是第

个头的源实体和目标实体的注意力权重,

是实体感知的注意力权重。这种方法可以利用文档的上下文信息,生成更加丰富和灵活的关系特征。

3.3 U-shaped Segmentation Module

本文将实体级关系矩阵

看作是一个

通道的图像,并将文档级关系预测问题转化为在

上进行像素级的分割。其中

是数据集中所有样本的最大实体数。为了实现这一目标,本文借鉴了计算机视觉领域的经典语义分割模型 U-Net。如图 3 所示,该模型由一个 U 型的分割网络组成,包括两个下采样模块和两个上采样模块,并通过跳跃连接实现信息的融合。在下采样模块中,每个模块包含两个最大池化层和两个卷积层,同时每个模块的通道数翻倍。如图 2 所示,实体级关系矩阵中的分割区域对应于实体对之间的关系共现情况。U 型分割网络能够在接受域内实现实体对之间的信息交换,类似于一种隐式的推理过程。具体来说,CNN 和下采样模块能够扩大当前实体对嵌入

的感受野,从而为表示学习提供更多的全局信息。在上采样模块中,每个模块包含一个反卷积层和两个卷积层,同时每个模块的通道数减半,这有助于将聚合的信息分配到每个像素上。最后,通过一个编码模块和一个 U 型分割模块来融合局部和全局信息

,如下式所示:
其中



分别表示 U 型分割模块和实体级关系矩阵。

是一个可学习的权重矩阵,用于降低

的维度,使其与

相匹配,而

远小于

3.4 Classification Module

本文使用前馈神经网络,将实体对嵌入



以及实体级关系矩阵

映射到隐藏表示

。然后,利用双线性函数计算关系的概率。具体地,有:
其中,

是矩阵



的实体对表示,



,是模型的参数。
由于之前的工作发现 RE 的关系分布不平衡(大部分实体对的关系为 NA),本文采用了一个平衡的 softmax 方法进行训练,这受到了计算机视觉领域的圆形损失 [
Sun et al., 2020
] 的影响。具体地,增加了一个额外的类别 0,使得目标类别的分数都高于

,而非目标类别的分数都低于

。形式地,有:
为了简化,将阈值设为零,得到以下公式: