hello云胜

技术与生活

0%

电子签章

数字证书常见标准

  • 符合PKI ITU-T X509标准,传统标准(.DER .PEM .CER .CRT)
  • 符合PKCS#7 加密消息语法标准(.P7B .P7C .SPC .P7R)
  • 符合PKCS#10 证书请求标准(.p10)
  • 符合PKCS#12 个人信息交换标准(.pfx *.p12)
    X509是数字证书的基本规范,而P7和P12则是两个实现规范,P7用于数字信封,P12则是带有私钥的证书实现规范。

生成p12证书文件

使用JDK的keytool工具

  1. keytool在jdk的bin目录下
  2. 生成数字文件
1
keytool -genkeypair -alias serverkey -keypass 111111 -storepass 111111  -dname "C=CN,ST=SD,L=QD,O=xxx,OU=dev,CN=xxx.com"   -keyalg RSA -keysize 2048 -validity 3650 -keystore D:\keystore\server.keystore

storepass keystore 文件存储密码

keypass 私钥加解密密码

alias 实体别名(包括证书私钥)

dname 证书个人信息

keyalt 采用公钥算法,默认是DSA keysize 密钥长度(DSA算法对应的默认算法是sha1withDSA,不支持2048长度,此时需指定RSA)

validity 有效期

keystore 指定keystore文件

image-20200317095752643

3,转换为p12格式

1
keytool -importkeystore -srckeystore D:\keystore\server.keystore -destkeystore D:\keystore\server.p12 -srcalias serverkey -destalias serverkey -srcstoretype jks -deststoretype pkcs12 -srcstorepass 111111 -deststorepass 111111 -noprompt

image-20200317095855778

使用IText对pdf文件进行数字签名

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
       public static final String sourceFolder = "./src/test/resources/com/itextpdf/signatures/sign/SigningTest/";
public static final String destinationFolder = "./target/test/com/itextpdf/signatures/sign/SigningTest/";
public static final String keystorePath = "D:\\keystore\\server.p12";
public static final char[] password = "111111".toCharArray();

public static final String stamperSrc = "./src/test/resources/seal.png";//印章路径
private Certificate[] chain; // 证书链
private PrivateKey pk;

@BeforeClass
public static void before() {
Security.addProvider(new BouncyCastleProvider());
createOrClearDestinationFolder(destinationFolder);
}

@Before
public void init() throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException {
pk = Pkcs12FileHelper.readFirstKey(keystorePath, password, password);
chain = Pkcs12FileHelper.readFirstChain(keystorePath, password);
}

@Test
public void testSign() {
String src = sourceFolder + "simpleDocument.pdf";
String fileName = "dest.pdf";
String dest = destinationFolder + fileName;
try {

ImageData img = ImageDataFactory.create(stamperSrc);
//读取图章图片,这个image是itext包的image
Image image = new Image(img);
float height = image.getImageHeight();
float width = image.getImageWidth();
Rectangle rectangle = new Rectangle(150, 200, width, height);

int pageNum = 1;
sign(src, String.format(dest, 1), img, pageNum, rectangle, chain, pk, DigestAlgorithms.SHA256, null, PdfSigner.CryptoStandard.CADES, "测试",
"青岛");
} catch (Exception e) {
JOptionPane.showMessageDialog(null, e.getMessage());
e.printStackTrace();
}
}
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
 public void sign(String src  //需要签章的pdf文件路径
, String dest // 签完章的pdf文件路径
, ImageData img // 印章图片
, int pageNum // 印在第几页
, Rectangle rectangle // 印章显示的位置
, Certificate[] chain //证书链
, PrivateKey pk //签名私钥
, String digestAlgorithm //摘要算法名称,例如SHA-1
, String provider // 密钥算法提供者,可以为null
, PdfSigner.CryptoStandard subfilter //数字签名格式,itext有2
, String reason //签名的原因,显示在pdf签名属性中,随便填
, String location) //签名的地点,显示在pdf签名属性中,随便填
throws GeneralSecurityException, IOException {
//下边的步骤都是固定的,照着写就行了,没啥要解释的
PdfReader reader = new PdfReader(src);
PdfDocument document = new PdfDocument(reader);
document.setDefaultPageSize(PageSize.TABLOID);
//目标文件输出流
FileOutputStream os = new FileOutputStream(dest);
//创建签章工具PdfSigner ,最后一个boolean参数
//false的话,pdf文件只允许被签名一次,多次签名,最后一次有效
//true的话,pdf可以被追加签名,验签工具可以识别出每次签名之后文档是否被修改
PdfReader reader2 = new PdfReader(src);
// PdfSigner stamper = new PdfSigner(reader2, os, true);
StampingProperties stampingProperties = new StampingProperties();
stampingProperties.useAppendMode();
PdfSigner stamper = new PdfSigner(reader2, os, stampingProperties);
// 获取数字签章属性对象,设定数字签章的属性
PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
appearance.setReason(reason);
appearance.setLocation(location);

//设置签名的位置,页码,签名域名称,多次追加签名的时候,签名与名称不能一样
//签名的位置,是图章相对于pdf页面的位置坐标,原点为pdf页面左下角
//四个参数的分别是,图章左下角x,图章左下角y,图章宽度,图章高度
appearance.setPageNumber(pageNum);
appearance.setPageRect(rectangle);
//插入盖章图片
appearance.setSignatureGraphic(img);
//设置图章的显示方式,如下选择的是只显示图章(还有其他的模式,可以图章和签名描述一同显示)
appearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC);
// 这里的itext提供了2个用于签名的接口,可以自己实现,后边着重说这个实现
// 摘要算法
IExternalDigest digest = new BouncyCastleDigest();
// 签名算法
IExternalSignature signature = new PrivateKeySignature(pk, digestAlgorithm, BouncyCastleProvider.PROVIDER_NAME);
// 调用itext签名方法完成pdf签章
stamper.setCertificationLevel(1);
stamper.signDetached(digest, signature, chain, null, null, null, 0, PdfSigner.CryptoStandard.CADES);
}

效果如下:

image-20200317100341684

可以查看下证书,会看到我们生成数字证书时的信息

image-20200317100308547

ip rule

查看服务器上的路由规则

1
2
3
4
# ip rule
0: from all lookup local
32766: from all lookup main
32767: from all lookup default

前面的数字代表优先级,0是最高优先级。

local、main、default是三个路由表的名字

ip route list table local

查看local路由表中的路由策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
broadcast 1x.xxx.5.0 dev eth0 proto kernel scope link src 1x.xxx.5.1
local 1x.xxx.5.1 dev eth0 proto kernel scope host src 1x.xxx.5.1
broadcast 1x.xxx.5.255 dev eth0 proto kernel scope link src 1x.xxx.5.1
local 100.125.97.128 dev tunl0 proto kernel scope host src 100.125.97.128
broadcast 127.0.0.0 dev lo proto kernel scope link src 127.0.0.1
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
broadcast 127.255.255.255 dev lo proto kernel scope link src 127.0.0.1
broadcast 172.17.0.0 dev docker0 proto kernel scope link src 172.17.0.1
local 172.17.0.1 dev docker0 proto kernel scope host src 172.17.0.1
broadcast 172.17.255.255 dev docker0 proto kernel scope link src 172.17.0.1
...
local 1x.xx.0.1 dev kube-ipvs0 proto kernel scope host src 1x.xx.0.1
local 1x.xx.0.9 dev kube-ipvs0 proto kernel scope host src 1x.xx.0.9
local 1x.xx.0.10 dev kube-ipvs0 proto kernel scope host src 1x.xx.0.10
...

ip route

查看的是外部路由表,实际上查看的就是main表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ip route
default via 1x.xxx.5.254 dev eth0
1x.xxx.5.0/24 dev eth0 proto kernel scope link src 1x.xxx.5.1
100.116.168.192/26 via 1x.xxx.5.3 dev tunl0 proto bird onlink
100.121.66.192/26 via 1x.xxx.5.2 dev tunl0 proto bird onlink
100.121.236.192/26 via 1x.xxx.5.4 dev tunl0 proto bird onlink
blackhole 100.125.97.128/26 proto bird
100.125.97.132 dev cali8bb97db0aa4 scope link
100.125.97.135 dev calide035c655d8 scope link
100.125.97.136 dev caliac4b211f131 scope link
100.125.97.138 dev cali5e5ab1418d1 scope link
100.125.97.139 dev cali98e6dbf06d5 scope link
100.125.97.140 dev cali08b0447e81a scope link
100.125.97.144 dev cali85142d4df17 scope link
100.125.176.128/26 via 1x.xxx.5.5 dev tunl0 proto bird onlink
169.254.0.0/16 dev eth0 scope link metric 1002
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1

ip route == ip route list table main

解读一下结果

1
default via 1x.xxx.5.254 dev eth0

是一个default,就是说如果本地都没有找到合适的路由,那默认就发到这个default网关,地址是1x.xxx.5.254,通过网卡eth0转发。

route

1
route -n

防火墙路由转发

开启防火墙的路由转发功能

1
iptables -A FORWARD -j ACCEPT

配置linux内核允许路由转发

1
/proc/sys/net/ipv4/ip_forward

修改其值为1,让内核允许一个网卡的网络包可以转发到另一个网卡上

ip netns

网络命令空间

1
2
3
4
5
6
7
8
# ip netns
cni-91ad63e9-fde5-c435-f26c-e26a103394ee (id: 6)
cni-011e1c20-604d-f94e-61a3-15c3ab74aad5 (id: 0)
cni-a26a0f96-9931-8632-c628-b2c3c68fe0c0 (id: 2)
cni-63de9e9e-13fd-7202-6ee0-f202abb91423 (id: 1)
cni-0b3b4d60-883c-25bb-f3d4-000a243e30e8 (id: 4)
cni-5a0f1e59-1497-e367-f0a5-5fa6d1ab6b9d (id: 5)
cni-c4a598a3-88bd-b3cd-558c-1d241c40a5a7 (id: 3)

新建网络命名空间

1
ip netns add ns-xxxx

注意ns的隔离

因为网络空间的隔离问题,所以对某个具体ns下执行命令,要进如该ns去执行

1
ip netns exex ns-xxxx 后面加命令

比如加上 ping 192.168.15.2

就是在网络空间ns-xxxx里执行ping

而不是默认的主机网络空间

brctl

bridge,网桥。可以理解为linux上的虚拟交换机。交换机是只认mac地址,不管ip。交换机是一个二层转发设备

1
brctl show

但是,网桥有点特殊,就特殊在它可以绑定一个ip地址段。

比如看docker0网桥

1
2
3
4
5
6
7
8
9
#ip addr
...
docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:3f:90:62:8d brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:3fff:fe90:628d/64 scope link
valid_lft forever preferred_lft forever
...

创建网桥

1
brctl addbr br-xxxx

veth设备

创建veth pair

1
ip link add veth-xxxx type veth peer name veth-xxxx-pr

把veth pair的一端放到创建的网络ns中

1
ip link set veth-xxxx netns xxxx

把veth pair的另一端连接到网桥上

1
ip link set veth-xxxx-pr master br-xxxx

这其实就是创建一个docker容器的网络步骤

启动各个网络设备

启动网桥

1
ip link set br-xxxx up

启动veth pair在网桥上的一端

1
ip link set veth-xxxx-pr up

启动veth pair在ns上的一端

1
ip netns exec ns-xxxx ip link set veth-xxxx up

给网络设备加上ip

给veth pair添加ip

1
ip netns exec ns-xxxx ip addr add 192.168.15.1/24 dev veth-xxxx

给网桥加上ip

1
ip addr add 192.168.15.10/24 dev br-xxxx

这样,服务器就可以通过网桥和容器通信

容器访问外网

目前在容器里是不能访问外网的,因为找不到路由

解决的办法是添加一个默认路由,通过网口veth-xxxx,下一条到网桥,然后就可以进到服务器主机的网络空间,进而通外网

1
ip netns exec ns-xxxx ip route add default via 192.168.15.10 dev veth-xxxx

查看一下

1
ip netns exec ns-xxxx route -n

此时还是不通,因为现在我们在容器里ping外网,出去的icmp包的源ip写的是容器的ip。这样的包肯定是回不来的,因为外网没有这个ip的路由。

所以,需要开启snat

1
iptables -t nat -A POSTROUTING -s 192.168.15.0/24 -j MASQUERADE

这个配置的意思是,在包出去的时候,把源ip是192.168.15.0/24这个段的,从哪个网卡出去,源ip就改成这个网卡的物理ip,这就是MASQUERADE的作用。

这样外网的包就能回来了。

抓包排查

抓包网桥

1
tcpdump -i br-xxxx

supervisor

安装

1
2
3
4
5
6
7
8
9
10
11
12
yum install -y epel-release
yum install -y supervisor

# systemctl enable supervisord # 开机自启动

# systemctl start supervisord # 启动supervisord服务



# systemctl status supervisord # 查看supervisord服务状态

# ps -ef|grep supervisord # 查看是否存在supervisord进程

使用supervisor启动mongo

安装后,写了mongo.ini

1
2
3
4
5
6
7
8
9
10
11
12
13
[program:mongo] ;
command=numactl --interleave=all /home/xxxadmin/mongodb-linux-x86_64-rhel62-3.2.7/bin/mongod -f /home/xxxadmin/mongodb-linux-x86_64-rhel62-3.2.7/bin/conf/mongo.conf ; 程序启动命令
autostart=false ; 在supervisord启动的时候也自动启动
startsecs=10 ; 启动10秒后没有异常退出,就表示进程正常启动了,默认为1秒
autorestart=true ; 程序退出后自动重启,可选值:[unexpected,true,false],默认为unexpected,表示进程意外杀死后才重启
startretries=1 ; 启动失败自动重试次数,默认是3
user=xxxadmin ; 用哪个用户启动进程,默认是root
directory=/home/xxxadmin/mongodb-linux-x86_64-rhel62-3.2.7/bin/ ;
priority=999 ; 进程启动优先级,默认999,值小的优先启动
redirect_stderr=true ; 把stderr重定向到stdout,默认false
stdout_logfile_maxbytes=20MB ; stdout 日志文件大小,默认50MB
stdout_logfile_backups = 20 ; stdout 日志文件备份数,默认是10
stdout_logfile=/var/log/supervisor/mongo.log #当指定目录不存在时无法正常启动,所以需要手动创建目录(supervisord 会自动创建日志文件)

重新加载下

1
supervisorctl reload

启动mongo

1
[root@pfsmongo1 run]# supervisorctl start mongo

使用 supervisorctl 管理进程

  • 查看托管的服务状态

    1
    supervisorctl status
  • 停止某一个进程,program_name 为 [program:x] 里的 x:

1
supervisorctl stop program_name
  • 启动某个进程:
1
supervisorctl start program_name
  • 重启某个进程:
1
supervisorctl restart program_name
  • 停止全部进程,注:start、restart、stop 都不会载入最新的配置文件:
1
supervisorctl stop all
  • 载入最新的配置文件,停止原有进程并按新的配置启动、管理所有进程:
1
supervisorctl reload
  • 根据最新的配置文件,启动新配置或有改动的进程,配置没有改动的进程不会受影响而重启:
1
supervisorctl update

以上命令也可以先输入supervisorctl进入交互界面,再进行具体命令

把子进程一起杀掉

大多数情况下建议开启 stopasgroup=true,这样使用 supervisorctl 进行 stop 的时候,会把子进程一起杀掉,不然可能只杀父进程没杀子进程,而如果你的 program 是占用端口的服务时,下次 supervisorctl start 可能会失败。

例子 : Flask 在 DEBUG 模式下启动(使用 flask run 方式启动)时,会启动 2 个 flask 进程,stopasgroup 如果是 false,supervisorctl stop 只会杀死其中一个 flask 进程。

分号前面记得补一个空格

配置文件中,分号后面是注释,这没有问题,可是,如果要加分号,分号前面记得补一个空格。不能紧跟

使用ROOK部署Ceph集群

前置条件

需要服务器有裸盘

1
# lsblk -f

image-20210727135518965

裸盘,没有分区没有文件系统。

安装lvm2,ceph的osd需要依赖lvm2。k8s集群所有节点安装lvm2

1
yum install -y lvm2

准备rook包

1
2
3
wget https://github.com/rook/rook/archive/refs/tags/v1.6.8.tar.gz
tar -zxvf v1.6.8.tar.gz
cd rook-1.6.8/cluster/examples/kubernetes/ceph

我们需要的yaml都已经写好,放在rook-1.6.8/cluster/examples/kubernetes/ceph目录下

修改operator.yaml

CSI_PLUGIN需要在所有节点上启动,master默认被打上了NoSchedule的taint,需要在operator上面添加容忍

1
2
3
4
5
6
7
8
# vim operator.yaml
CSI_PLUGIN_TOLERATIONS: |
- effect: NoSchedule
key: node-role.kubernetes.io/master
operator: Exists
- effect: NoExecute
key: node-role.kubernetes.io/etcd
operator: Exists

部署operator

1
kubectl create -f crds.yaml -f common.yaml -f operator.yaml

顺序不能变,crds.yaml和 common.yaml 必须在operator.yaml之前创建

查看pod状态

1
2
3
[root@paas-m-k8s-master-1 ceph]# kubectl -n rook-ceph get pod
NAME READY STATUS RESTARTS AGE
rook-ceph-operator-674c87d477-j8xwp 1/1 Running 0 5m34s

部署ceph cluster

1
kubectl create -f cluster.yaml

查看pod状态,发现很多失败。查看原因,是pull image失败。

1
2
3
4
5
awaiting headers)
Normal BackOff 22m (x4138 over 19h) kubelet, paas-m-k8s-node-2 Back-off pulling image "k8s.gcr.io/sig-storage/csi-snapshotter:v4.1.1"
Normal BackOff 12m (x4176 over 19h) kubelet, paas-m-k8s-node-2 Back-off pulling image "k8s.gcr.io/sig-storage/csi-resizer:v1.2.0"
Normal BackOff 7m38s (x4197 over 19h) kubelet, paas-m-k8s-node-2 Back-off pulling image "k8s.gcr.io/sig-storage/csi-provisioner:v2.2.2"

describe看是从quay.io下载镜像。我们的网络即使配置了镜像加速也下载不了quay.io的image。

手动从quay.io下载镜像,传到k8s集群的所有主机上。

方法是可以借助阿里云构建镜像,然后从aliyun拉取镜像,再改tag

先去github建个仓库,里面建一个文件Dockerfile,就放一句FROM

1
FROM k8s.gcr.io/sig-storage/csi-provisioner:v2.2.2

然后使用阿里云的容器镜像服务,建空间,建仓库

仓库挂载上面创建github的仓库,注意选上海外机器构建

img

添加规则

img

master是我的分支的名字

img

成功后可以拉镜像了

mobaXterm有个批量执行命令的功能

在这里插入图片描述

在所有节点执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
docker login --username=yangyunsheng1989@126.com registry.cn-hangzhou.aliyuncs.com
docker pull registry.cn-hangzhou.aliyuncs.com/myk8s123/k8simg:v2.2.0
docker tag registry.cn-hangzhou.aliyuncs.com/myk8s123/k8simg:v2.2.0 k8s.gcr.io/sig-storage/csi-node-driver-registrar:v2.2.0
docker rmi registry.cn-hangzhou.aliyuncs.com/myk8s123/k8simg:v2.2.0


docker pull registry.cn-hangzhou.aliyuncs.com/myk8s123/k8simg:v3.2.1
docker tag registry.cn-hangzhou.aliyuncs.com/myk8s123/k8simg:v3.2.1 k8s.gcr.io/sig-storage/csi-attacher:v3.2.1
docker rmi registry.cn-hangzhou.aliyuncs.com/myk8s123/k8simg:v3.2.1

docker pull registry.cn-hangzhou.aliyuncs.com/myk8s123/k8simg:v4.1.1
docker tag registry.cn-hangzhou.aliyuncs.com/myk8s123/k8simg:v4.1.1 k8s.gcr.io/sig-storage/csi-snapshotter:v4.1.1
docker rmi registry.cn-hangzhou.aliyuncs.com/myk8s123/k8simg:v4.1.1


docker pull registry.cn-hangzhou.aliyuncs.com/myk8s123/k8simg:v1.2.0
docker tag registry.cn-hangzhou.aliyuncs.com/myk8s123/k8simg:v1.2.0 k8s.gcr.io/sig-storage/csi-resizer:v1.2.0
docker rmi registry.cn-hangzhou.aliyuncs.com/myk8s123/k8simg:v1.2.0

docker pull registry.cn-hangzhou.aliyuncs.com/myk8s123/k8simg:v2.2.2
docker tag registry.cn-hangzhou.aliyuncs.com/myk8s123/k8simg:v2.2.2 k8s.gcr.io/sig-storage/csi-provisioner:v2.2.2
docker rmi registry.cn-hangzhou.aliyuncs.com/myk8s123/k8simg:v2.2.2

验证ceph的状态

1
2
3
kubectl create -f toolbox.yaml
#进入toolbox pod
kubectl -n rook-ceph exec -it $(kubectl -n rook-ceph get pod -l "app=rook-ceph-tools" -o jsonpath='{.items[0].metadata.name}') -- bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@rook-ceph-tools-79fcdcf697-8nk7m /]# ceph status
cluster:
id: c1797d91-e3ec-488c-bd59-32773a34cc1d
health: HEALTH_OK

services:
mon: 3 daemons, quorum a,b,c (age 20h)
mgr: a(active, since 20h)
osd: 6 osds: 6 up (since 20h), 6 in (since 20h) #集群内总共有几块裸盘就应该有几个osd。本例有6个node节点

data:
pools: 1 pools, 1 pgs
objects: 0 objects, 0 B
usage: 6.0 GiB used, 2.9 TiB / 2.9 TiB avail
pgs: 1 active+clean

查看之前的裸盘

1
2
3
4
5
6
7
[root@paas-m-k8s-node-1 ~]# lsblk -f
NAME FSTYPE LABEL UUID MOUNTPOINT
vda
└─vda1 ext4 207b19eb-8170-4983-acb5-9098af381e72 /
vdb LVM2_member 6nWGCq-SdMg-FdqH-fpFn-R6kc-T4gN-JKRjmJ
└─ceph--d4b6fe80--9e55--479a--874f--7fc49cf0ca2e-osd--block--f66a2af8--3a00--4bf1--8bde--be149f4d2e31

创建storageClass

1
2
3
4
5
6
[root@paas-m-k8s-master-1 rbd]# pwd
/root/rook/rook-1.6.8/cluster/examples/kubernetes/ceph/csi/rbd
[root@paas-m-k8s-master-1 rbd]# kubectl apply -f storageclass.yaml
cephblockpool.ceph.rook.io/replicapool created
storageclass.storage.k8s.io/rook-ceph-block created

注意这里进入的目录是rbd。也就是块存储,ceph还提供了文件存储的storage-class,是在cephfs目录下

rbd相较于cephfs性能要更优,但是不支持多mount,也就是每个pvc只能被一个pod挂载。

cephfs在读写大文件时性能比较优秀,读写小文件时性能较差,但是其支持多mount,所以在需要多个pod共享存储时就需要选用cephfs作为storage-class

1
2
3
4
5

[root@paas-m-k8s-master-1 ~]# kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
nfs-client (default) cluster.local/nfs-client-nfs-client-provisioner Delete Immediate true 15d
rook-ceph-block rook-ceph.rbd.csi.ceph.com Delete Immediate true 174m

设置为默认storageclass

1
[root@d-paas-k8s-master-0 rbd]# kubectl patch storageclass rook-ceph-block -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

创建pvc 验证csi

1
2
3
[root@paas-m-k8s-master-1 rbd]# pwd
/root/rook/rook-1.6.8/cluster/examples/kubernetes/ceph/csi/rbd
[root@paas-m-k8s-master-1 rbd]# kubectl apply -f pvc.yaml

查看在default namespace下是否生成了name为rbd-pvc的pvc,status为bound

1
2
3
[root@paas-m-k8s-master-1 rbd]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
rbd-pvc Bound pvc-4ef98f5f-462e-4834-bf3c-bd96f9b32b94 1Gi RWO rook-ceph-block 2m9s

验证完成删除rdb-pvc

1
2
[root@paas-m-k8s-master-1 rbd]# kubectl delete pvc rbd-pvc
persistentvolumeclaim "rbd-pvc" deleted

查看Dashboard

ceph提供了一个控制台服务。默认已经是开启的。

1
2
3
4
5
6
7
8
9
[root@paas-m-k8s-master-1 rbd]# kubectl get svc -n rook-ceph
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
csi-cephfsplugin-metrics ClusterIP 10.110.141.175 <none> 8080/TCP,8081/TCP 23h
csi-rbdplugin-metrics ClusterIP 10.104.228.180 <none> 8080/TCP,8081/TCP 23h
rook-ceph-mgr ClusterIP 10.102.85.146 <none> 9283/TCP 23h
rook-ceph-mgr-dashboard ClusterIP 10.110.80.91 <none> 8443/TCP 23h
rook-ceph-mon-a ClusterIP 10.110.117.117 <none> 6789/TCP,3300/TCP 23h
rook-ceph-mon-b ClusterIP 10.102.192.7 <none> 6789/TCP,3300/TCP 23h
rook-ceph-mon-c ClusterIP 10.98.8.230 <none> 6789/TCP,3300/TCP 23h

rook-ceph-mgr是暴露给Prometheus收集用的,rook-ceph-mgr-dashboard就是控制台服务。

然后创建个service暴露给集群外访问。dashboard-external-https.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: v1
kind: Service
metadata:
name: rook-ceph-mgr-dashboard-external-https
namespace: rook-ceph
labels:
app: rook-ceph-mgr
rook_cluster: rook-ceph
spec:
ports:
- name: dashboard
port: 8443
protocol: TCP
targetPort: 8443
selector:
app: rook-ceph-mgr
rook_cluster: rook-ceph
sessionAffinity: None
type: NodePort

其实这个yaml已经存在了。/root/rook/rook-1.6.8/cluster/examples/kubernetes/ceph下就有

1
2
3
4
5
6
7
8
9
10
11
12
[root@paas-m-k8s-master-1 ceph]# kubectl apply -f dashboard-external-https.yaml
service/rook-ceph-mgr-dashboard-external-https created
[root@paas-m-k8s-master-1 ceph]# kubectl -n rook-ceph get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
csi-cephfsplugin-metrics ClusterIP 10.110.141.175 <none> 8080/TCP,8081/TCP 23h
csi-rbdplugin-metrics ClusterIP 10.104.228.180 <none> 8080/TCP,8081/TCP 23h
rook-ceph-mgr ClusterIP 10.102.85.146 <none> 9283/TCP 23h
rook-ceph-mgr-dashboard ClusterIP 10.110.80.91 <none> 8443/TCP 23h
rook-ceph-mgr-dashboard-external-https NodePort 10.104.156.86 <none> 8443:31867/TCP 26s
rook-ceph-mon-a ClusterIP 10.110.117.117 <none> 6789/TCP,3300/TCP 23h
rook-ceph-mon-b ClusterIP 10.102.192.7 <none> 6789/TCP,3300/TCP 23h
rook-ceph-mon-c ClusterIP 10.98.8.230 <none> 6789/TCP,3300/TCP 23h

https://1x.xxx.151.208:31867/

获取登录密码

用户名是:admin

密码获取:kubectl -n rook-ceph get secret rook-ceph-dashboard-password -o jsonpath=”{[‘data’][‘password’]}” | base64 –decode && echo

(],(-W|G^rJ’JO,2!<#R

MongoDB单机环境搭建

1,下载

去mongo的官网下载最新发行版,我安装的时候是4.2.7

image-20200605100407309

2,规划

本次搭建的是单机环境,主要用于个人测试使用。

3,系统参数修改

官方建议的ulimit参数配置

image-20200605103128453

不使用hugepages

cat /proc/sys/vm/nr_hugepages

确保其值为零,如果不是请修改执行下面操作。

echo 0>/proc/sys/vm/nr_hugepages

echo never >> /sys/kernel/mm/transparent_hugepage/enabled

echo never >> /sys/kernel/mm/transparent_hugepage/defrag

防火墙端口

我这里规划使用11000端口,要提前打开。需root权限

firewall-cmd –zone=public –add-port=11000/tcp –permanent

firewall-cmd –reload

firewall-cmd –list-ports

安装numactl

在NUMA架构的机器上,要以numactl运行,否则有告警

1
yum install numactl

4,开始部署

4.1 目录规划

~/mongo 放mongo安装文件
~/mongodata/data 放数据文件
~/mongodata/logs/mongod.log mongo运行日志

4.2 解压

~/mongo
1
2
3
4
5
cd ~/mongo
wget http://我自己的服务/U9gUl/mongodb-linux-x86_64-rhel70-4.2.7.tgz
tar -zxvf mongodb-linux-x86_64-rhel70-4.2.7.tgz
cd mongodb-linux-x86_64-rhel70-4.2.7
cd bin

4.3 配置文件

当前所在路径为 mongo/mongodb-linux-x86_64-rhel70-4.2.7/bin

创建配置文件

1
2
3
mkdir conf
cd conf
vim mongod.conf

配置文件内容:

具体解释,可以看官方文档

https://docs.mongodb.com/manual/reference/configuration-options/#file-format

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
systemLog:
destination: file
path: "/home/xxx/mongodata/logs/mongod.log"
logAppend: true
storage:
journal:
enabled: true
dbPath: "/home/xxx/mongodata/data"
directoryPerDB: true
engine: wiredTiger
wiredTiger:
engineConfig:
cacheSizeGB: 1
directoryForIndexes: true
journalCompressor: zlib
collectionConfig:
blockCompressor: zlib
indexConfig:
prefixCompression: true
net:
bindIp: 0.0.0.0
port: 11000
processManagement:
fork: true

4.4 启动脚本

写个start.sh,以后方便点

1
numactl --interleave=all /home/xxxadmin/mongo/mongodb-linux-x86_64-rhel70-4.2.7/bin/mongod -f ./conf/mongod.conf
1
chmod +x start.sh

4.5 启动

1
sh start.sh

4.6 关闭

1
./mongod --shutdown -f ./conf/mongod.conf

5 配置管理员用户

登录

1
./mongo localhost:11000

会看到有告警

** WARNING: Access control is not enabled for the database.

应该以认证的安全模式使用mongo。否则都能登录mongo。

先切到admin库下,默认是在test库。这点容易忘记

1
use admin
1
db.createUser({user: "admin", pwd: "Hello,123", roles:[{role:"root", db:"admin"}]})

现在用auth模式重启,解决warning

修改start.sh

1
numactl --interleave=all /home/xxxadmin/mongo/mongodb-linux-x86_64-rhel70-4.2.7/bin/mongod --auth -f ./conf/mongod.conf

重启mongo。重新登录,要加上用户名密码

1
./mongo localhost:11000  -u admin -p Hello,123 --authenticationDatabase admin

6, 创建普通用户及业务数据库

记得切到admin库下

1
2
use admin
db.createUser({ user: 'baas', pwd: 'hibaas',roles: [ { role: "dbOwner", db: "baas_bbs" }] })

切到baas_bbs库下

插入一条数据试试

1
2
use baas_bbs
db.testCollection.save({"name":"yunsh","age":"18"})

WriteResult({ “nInserted” : 1 })

ok,完成

https://blog.csdn.net/qq_35494808/article/details/89844590?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param

为对方创建特定用户

操作指令 代码
创建登录账户 CREATE USER ‘用户名’ @‘IP/localhost’ IDENTIFIED BY ‘密码’;
授权 GRANT 权限 ON 数据库名.表 TO ‘用户名’ @‘IP/localhost’;
查看所有账户 SELECT USER,HOST FROM mysql.USER
查看账户权限 SHOW GRANTS FOR ‘用户名’ @‘IP/localhost’
1
CREATE USER 'c3' @'%' IDENTIFIED BY 'xxx202009';

创建好视图回来做

1
GRANT SELECT ON ts_prod.v_expense_record TO 'c3' @'%';

mysql 视图相关操作

操作指令 代码
创建视图 CREATE VIEW 视图名(列1,列2…) AS SELECT (列1,列2…) FROM …;
使用视图 和普通表一样操作
修改视图 CREATE OR REPLACE VIEW 视图名 AS SELECT […] FROM […];
删除视图 DROP VIEW 视图名
查看数据库已有视图 SHOW TABLES [like…];(可以使用模糊查找)
查看视图详情 DESC 视图名或者SHOW FIELDS FROM 视图名
视图条件限制 [WITH CHECK OPTION]
1
create VIEW v_expense_record(user_code, c3_lsh, machine_code, expenses_date, base_money, self_money) as SELECT user_code, c3_lsh, machine_code, expenses_date, base_money, self_money from t_expenses_records WHERE expenses_date >= (NOW() - interval 24 hour)

ansible学习笔记

安装

  1. 设置EPEL仓库
    Ansible仓库默认不在yum仓库中,因此我们需要使用下面的命令启用epel仓库
1
# yum install epel-release -y
  1. 使用yum安装Ansible
1
# yum install ansible
  1. 查看ansible的版本
1
2
# ansible --version
ansible 2.9.27

第一个ansible命令尝鲜

说到底,ansible的作用就是连接远端客户机,执行命令。

所以第一步就是连接。

配置ssh鉴权

ansible通过ssh登录客户机。所以,第一步先配置好ssh登录。

客户机要允许我们进行ssh登录,需要提前配置好防火墙

1
2
3
4
firewall-cmd --permanent --zone=public --add-rich-rule="rule family='ipv4' source address='1x.xxx.6.xx' service name='ssh' accept"
firewall-cmd --reload

echo sshd: 1x.xxx.6.xx >> /etc/hosts.allow
  1. 生成密钥

    1
    # ssh-keygen

    默认情况下,在/root/.ssh下生成id_rsa.pub就是公钥,一会要copy到客户机

  2. 把密钥拷贝到客户机

    把第一步生成的公钥追加到客户机的/root/.ssh/authorized_keys文件里

或者

1
2
3
ssh-copy-id -i .ssh/id_rsa.pub redis@1x.xxx.5.6    # 将公钥文件copy到远程主机上
ssh-agent bash # 通过ssh-agent来管理密钥
ssh-add ~/.ssh/id_rsa # 将私钥交给ssh-agent管理,避免每次连接都需要输入密钥密码

ssh-copy-id这种方式要求你提前知道密码。

通常公司里是不会root密码的,只能通过堡垒机登录,使用第一种方式手动copy

如果报ssh-copy-id: command not found,安装一下

1
yum -y install openssh-clientsansible

ping一下

我们先创建一个hosts文件,在hosts文件里写上客户机ip。(ansible默认是找/etc/ansible/hosts)

1
2
3
4
5
6
7
[root@paas-m-k8s-master-1 demo]# pwd
/root/ansible/demo
[root@paas-m-k8s-master-1 demo]# cat hosts
[demo]
1x.xxx.151.21
1x.xxx.151.22
1x.xxx.151.23

[demo]是我对这组ip的一个命名,用[]括起来,这是andible的规范。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@paas-m-k8s-master-1 demo]# ansible -i hosts demo -m ping
1x.xxx.151.21 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": false,
"ping": "pong"
}
1x.xxx.151.22 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": false,
"ping": "pong"
}
1x.xxx.151.23 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": false,
"ping": "pong"
}

这是ansible的命令,-i 指定客户机清单文件,不写的话就是找默认的/etc/ansible/hosts文件,

-m 指定要执行的命令

基础命令

anisble命令语法: ansible [-i 主机文件] [-f 并发数量] [组名] [-m 模块名称] [-a 模块参数]

参数 功能
-v 详细模式,如果执行成功,输出详细结果
-i 指定host文件路径,默认在/etc/ansible/hosts
-f,-forks=NUM NUM默认是整数5,指定fork开启同步进程的个数
-m 指定使用的module名称,默认command模块
-a 指定模块的参数
-k 提示输入SSH密码,而不是使用基于ssh密钥认证
-sudo 指定使用sudo获取root权限
-K 提示输入sudo密码
-u 指定客户端的执行用户
-C 测试命令执行会改变什么内容,不会真正的去执行

最常用的就是-i和-m, -a

-m后面指定的是模块,默认是command模块,即执行命令。

比如查看磁盘:

1
# ansible -i /root/ansible/demo/hosts demo -m command -a "df -h"

高级用法

执行模块

上面说到使用command模块执行常见的shell命令。

但是需要明白的一点是,command模块执行命令并不是通过shell来执行。所以常见的命令都可以使用,但是像>> < > | 和 & 这些操作都不可以,不支持管道,没法批量执行命令

那要怎么办呢?

这就说到ansible的有三个远程执行模块:command,shell和scripts
command :就是前面测试用的这个,你已经熟悉了。

shell模块: 使用shell模块的时候默认是通过/bin/sh来执行的,所以在终端输入的各种命令都可以使用

scripts模块:可以在本地写一个脚本,在远程服务器上执行这个脚本

shell模块示例
1
2
3
# ansible -i hosts demo -m shell -a "ps -ef | grep java"
1x.xxx.151.211 | CHANGED | rc=0 >>
root 1477 1 0 2021 ? 04:37:28 .......

你可以换成command模块看看效果

script模块示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@paas-m-k8s-master-1 demo]# cat > demo.sh << EOF
> date
> echo "hello devops"
> EOF
[root@paas-m-k8s-master-1 demo]# ansible -i hosts demo -m script -a "demo.sh"
1x.xxx.151.211 | CHANGED => {
"changed": true,
"rc": 0,
"stderr": "Shared connection to 1x.xxx.151.211 closed.\r\n",
"stderr_lines": [
"Shared connection to 1x.xxx.151.211 closed."
],
"stdout": "2023年 03月 01日 星期三 15:22:10 CST\r\nhello devops\r\n",
"stdout_lines": [
"2023年 03月 01日 星期三 15:22:10 CST",
"hello devops"
]
}

除了执行模块,ansible还自带很多其他有用的模块,不做演示了。

其他模块

copy:把主机的文件copy到客户机上,功能类似scp

file:设置文件权限

stat:获取客户机的文件的信息

get_url: 让客户机执行url的get下载

yum:客户机上执行yum安装软件

cron:客户机上配置定时任务,就是crontab

service:客户机管理服务。如关闭开启防火墙

user:管理用户

fetch: 从远程某主机获取(复制)文件到本地

setup: 主要用于收集客户端主机信息。是通过调用facts组件来实现的。
  facts组件是Ansible用于采集被管机器设备信息的一个功能,我们可以使用setup模块查机器的所有facts信息,可以使用filter来查看指定信息。整个facts信息被包装在一个JSON格式的数据结构中,ansible_facts是最上层的值。

先了解ansible的能力,深入学习靠自己。这些模块的功能其实靠command,shell,script等也可以实现。

查看模块用法

列出都有哪些模块

1
ansible-doc -l

查看你具体想要了解的模块

1
2
ansible-doc -l | grep shell
ansible-doc shell

常用功能命令

平常大部分工作我都习惯直接用shell模块。

但是很多内置模块的合理使用,可以极大的提高效率。

修改文件

使用lineinfile模块。path指定文件路径,line指定内容。

该命令会先检查文件种是否有这行,有的话不做任何操作。没有的话就插入一行

1
ansible -i es-host es -m lineinfile -a 'path=/usr/local/elasticsearch/config/elasticsearch.yml line="cluster.initial_master_nodes: [\"{{host_name}}\"]"'

其实也就是保证这一行存在。

那如果要删除这一行,用下面的语句

1
ansible -i es-host es -m lineinfile -a 'path=/usr/local/elasticsearch/config/elasticsearch.yml line="cluster.initial_master_nodes: [\"{{host_name}}\"]" state=absent'

就是加了一个state=absent。就是保证这一行不存在。

所以其实所谓插入的语句,隐含了一个state=present

k8s Pod服务通过IP对外访问
前言
k8s v1.14.2版本提供了5种访问Pod的方式(hostNetwork,hostPort,NodePort,LoadBalancer和Ingress),但都不符合通过PodIp直接访问容器的需求,这边的需求是直接访问Pod的Ip,并指定Ip创建Pod

外部访问逻辑
指定k8s生成PodIp范围关闭calico默认隧道模式(IPIP),添加路由器路由(PodIP范围内的ip跳转到k8s-master节点),允许k8s-master节点转发并添加iptables转发规则,指定Ip创建Pod(可选,看需求)

操作
以下步骤需要重新搭建

1、修改calico配置文件
修改calico.yaml文件配置

vim calico.yaml
1
把默认IPIP禁用掉,这一步是因为我们访问是通过传统的BGP模式,而calico默认设置为IPIP模式,这一步是为了统一路由协议,能够正确的转发数据包。修改完成后,k8s会在k8s集群内自动添加内部BGP路由。

Enable IPIP

  • name: CALICO_IPV4POOL_IPIP
    value: “Off”
    1
    2
    3
    同时修改value生成范围(与–pod-network-cidr参数一致,这里的192.168.128.0/17范围为192.168.128.0到192.168.255.255)确保k8s的节点不在这个IP范围内,不然会导致很多不可预知的问题。比如coredns启动失败或calico-kube-controllers启动失败,报错(Get https://1x.xx.0.1:443/api/v1/services?resourceVersion=0: dial tcp 1x.xx.0.1:443: getsockopt: no route to host.)等问题

  • name: CALICO_IPV4POOL_CIDR
    value: “192.168.128.0/17”
    1
    2
    2、k8s初始化
    kubeadm init时指定 –pod-network-cidr 参数(与calico.yaml中的 CALICO_IPV4POOL_CIDR 保持一致),

我这里执行的命令为

kubeadm init –kubernetes-version=v1.14.1 –pod-network-cidr=192.168.128.0/17 –apiserver-advertise-address=192.168.1.22 –image-repository registry.aliyuncs.com/google_containers
1
3、在k8s-master节点打开转发功能和添加路由
打开转发功能

echo 1 > /proc/sys/net/ipv4/ip_forward
1
配置转发规则(接收192.168.3.0/24段的包,并进行转发)

iptables -I FORWARD -s 192.168.3.0/24 -j ACCEPT
1
4、在路由器上添加路由
在路由器上添加pod-network-cidr范围内的包下一跳指向k8s-master节点IP

5、指定PodIP创建Pod(可以不指定,k8s会自动分配并添加内部路由,由于特殊需求,我这里是指定的)
样例:指定创建ip为192.168.217.217

vim busybox.yaml
1
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
labels:
app: myapp
annotations:
cni.projectcalico.org/ipAddrs: “["192.168.217.217"]”
spec:
containers:

  • name: myapp-container
    image: busybox
    command: [‘sh’, ‘-c’, ‘echo Hello Kubernetes! && sleep 3600’]
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    然后看一下

kubectl get pods
1
创建成功

myapp-pod 1/1 Running 0 16s 192.168.217.217 node1
1
6、测试
最后ping一下

[C:~]$ ping 192.168.217.217

正在 Ping 192.168.217.217 具有 32 字节的数据:
来自 192.168.217.217 的回复: 字节=32 时间=1ms TTL=61
来自 192.168.217.217 的回复: 字节=32 时间=1ms TTL=61
来自 192.168.217.217 的回复: 字节=32 时间=1ms TTL=61
来自 192.168.217.217 的回复: 字节=32 时间=1ms TTL=61
1
2
3
4
5
6
7
测试成功

0,规划

主机名 IP 集群角色 操作系统 磁盘规划
dts-paas-middleware-dev-master-0 1x.xxx.1.157 master CentOS Linux 7.9.2009
dts-paas-middleware-dev-node-4 1x.xxx.1.162 node CentOS Linux 7.9.2009
dts-paas-middleware-dev-node-5 1x.xxx.1.163 node CentOS Linux 7.9.2009

配置免密登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
master节点生产sshkey
dts-paas-middleware-dev-master-0# ssh-keygen

在master和node节点都执行:
wget http://1x.xx.66.1/1574wU/id_rsa.pub
cat id_rsa.pub >> .ssh/authorized_keys
systemctl start firewalld.service
firewall-cmd --permanent --add-rich-rule="rule family="ipv4" source address="1x.xxx.1.157/32" port protocol="tcp" port="22" accept"
systemctl restart firewalld
firewall-cmd --reload
systemctl enable firewalld
systemctl disable firewalld
systemctl stop firewalld
echo "sshd: 1x.xxx.1.157" >> /etc/hosts.allow

1,安装k8s

指定cidr

1
2
3
4
5
6
7
sealos init --master 1x.xxx.1.157 \
--node 1x.xxx.1.162 \
--node 1x.xxx.1.163 \
--user root \
--version v1.18.8 \
--pkg-url kube1.18.8.tar.gz \
--podcidr 1x.xxx.132.0/23

2,配置pvc

基于之前的文档已经配置好nfs

1
2
3
4
cd yaml
kubectl apply -f rbac.yaml
kubectl apply -f nfs-provisioner.yaml
kubectl apply -f storageClass.yaml

配置网络

关闭ipip模式,使用bgp模式

1
kubectl edit ippool -o yaml

spec:
blockSize: 26
cidr: 1x.xxx.130.0/23
ipipMode: Never 表示使用bgp模式,Always使用ipip模式

1
kubectl edit ds calico-node -n kube-system
   name: CALICO_IPV4POOL_IPIP
   value: Never   表示关闭ipip模式,Always是启用

查看网络路由

bgp模式没有tunel了

跨界点pod无法ping通排查

这两个pod不通

image-20210603090707772

先用calicoctl查看容器A的workloadEndpoint:

1
calicoctl get workloadendpoint -o yaml

image-20210603092232495

可以看到pod的网卡情况

hfs-pro-101:ip:1x.xxx.130.69,mac: 网卡:cali51ddf6e78e5, node:node1

进入容器,查看容器路由

image-20210603095142729

去node1上看cali网卡的mac

image-20210603095212260

使用calico后,在容器内只有一条默认路由,所有的报文都通过169.254.1.1送出。但是这个IP是RFC约定保留的无效IP,报文怎么还能送出去呢?
秘密就是容器内的arp记录,在容器A内记录的169.254.1.1的mac地址是:
node上的caliXX网卡的mac。
node上的caliXX网卡和容器内的eth0网卡,是一对veth设备。veth网卡的特性是,向eth0写入的报文,会通过caliXX流出。
在容器A中向eth0写入的报文,它目的mac是caliXX网卡的mac,当报文经caliXX流出时,就 进入到了node的协议栈中,开始在node的网络空间中流转。