0%

配置主服务器

vim /etc/mysql/mysql.conf.d/mysqld.cnf
找到server-id附近的代码,修改为

1
2
3
4
5
6
7
server-id               = 1
log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 10
max_binlog_size = 100M
binlog_do_db = test # 需要写binlog的数据库
binlog_ignore_db = mysql # 不需要写binlog的数据库
# binlog_do_db和binlog_ignore_db可以仅指定一项,也可以都不指定,都不指定则全部写binlog

重启数据库
service mysql restart

防止出现打开文件数限制

ulimit -n 65535

备份数据库

xtrabackup --backup --host=127.0.0.1 --user=root --password=zrjm6E13RdhTPBmB --target-dir=/data/dbbackup/20221009
如果不存在xtrabackup命令
apt install percona-xtrabackup

复制到远程

rsync -avpP -e ssh /data/dbbackup/20221009 root@从库:/data/dbbackup/
或通过共享目录在从服务器挂载备份目录

配置从服务器

vim /etc/mysql/mysql.conf.d/mysqld.cnf
找到server-id附近的代码,修改为

1
2
3
4
5
6
7
8
9
10
server-id               = 2
log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 10
max_binlog_size = 100M
#binlog_do_db = include_database_name
#binlog_ignore_db = include_database_name
replicate-do-db = test #需要同步的数据库
replicate-ignore-db = mysql #不同步系统数据库
super_read_only = 1
read_only = 1

准备恢复

这步并没有连接数据库,只是对备份文件做一些设置,在主或从上执行都可以,安全一点应该再从库上执行,因为从库和主库的数据库版本未必完全一致。
xtrabackup --prepare --user=root --password=zrjm6E13RdhTPBmB --target-dir=/data/dbbackup/20221009

连上从数据库停止SLAVE

mysql -h127.0.0.1 -P3306 -uroot -p
STOP SLAVE;

停止从服务器数据库

service mysql stop

清空/var/lib/mysql目录,顺便备份

mv /var/lib/mysql /var/lib/mysql_backup
如果不需要备份直接删除也可以

还原数据库

xtrabackup --copy-back --target-dir=/data/dbbackup/20221009

修正文件权限

chown -R mysql:mysql /var/lib/mysql

查看并记下备份到的LOG_FILE和LOG_POS,后面设置用

1
2
cd /data/dbbackup/20221009
cat xtrabackup_binlog_info

显示类似
mysql-bin.000891 408016029

启动从服务器数据库

service mysql start

连上从数据库重设SLAVE

mysql -h127.0.0.1 -P3306 -uroot -p

修改其中的MASTER_LOG_FILE & MASTER_LOG_POS为前面查看得到的

1
2
3
4
5
6
7
8
CHANGE MASTER TO
MASTER_HOST='192.168.1.140',
MASTER_USER='repl',
MASTER_PASSWORD='4Bd9x25ERcacAqwX',
MASTER_LOG_FILE='mysql-bin.000891',
MASTER_LOG_POS=408016029;

START SLAVE;

查看本机作为从库的状态
SHOW SLAVE STATUS\G
关注下面3个字段

1
2
3
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 0

Slave_IO_Running & Slave_SQL_Running 应该都是Yes,Seconds_Behind_Master可能为一个正数表示从库落后主库的秒数,开启一段时间后应该会变成0。

PS:
1)mysql-bin文件在主服务器上是不断增加的,会保留一段时间旧的,一段时间后自动删除,在还原并配置从服务器时,要保证主服务器上的MASTER_LOG_FILE还存在才能成功,后面可以有新的文件会自动同步过来,所以在备份还原期间不用停止或锁住主数据库。
2)备份前、复制到远程前、还原前,都应该确保有足够的磁盘空间。
使用df - h来查看磁盘剩余空间,使用du -sh来查看当前文件夹占用空间。
3)如果还原时遇到 Error: copy_file() failed. 可以用md5sum校验复制的文件是否损坏,如果两边md5sum不一致或读取错误无法计算md5sum则说明损坏了。这时需要重新copy损坏的文件并重新执行恢复命令。
4)root在localhost的密码和在%的密码可能不同,连接mysql时分别尝试不指定-h或指定-h127.0.0.1来连接。
5)主从同步出错修复时跳过修改配置文件的步骤,确认备份点的MASTER_LOG_FILE是否存在,若存在也可跳过备份步骤,直接STOP、还原、START、STOP SLAVE; CHANGE MASTER……; START SLAVE;
6) 如果Seconds_Behind_Master不断增大,检查主库和从库的数据库版本,最好主次版本都完全一致,如果不一致,从库应该高于主库,而不能让主库高于从库。

之前讲过通过ngrok实现内网穿透,frp是和ngrok类似的工具,最近发现ngrok的一些不足,frp可以解决,主要有以下几点:

  1. ngrok不支持UDP穿透,这也是我更换成frp的最大原因。
  2. 映射TCP端口时不能指定服务器端口,只能接受随机分配的端口。
  3. ngrok在转发流量特别大的时候速度跟不上且不稳定,我用ngrok转发seafile网盘的流量,然后传1G以上的文件,会出现传不完就卡住失败甚至ngrok客户端出错崩溃的情况。
  4. ngrok是在编译客户端和服务器时预先成好证书,过程略烦琐,如果我有多个ngrok服务器,那么就要有多个不同的客户端可执行文件而不能通过相同的可执行文件不同的配置,这似乎不太合理。

搭建frp服务器

frp服务器可以通过docker搭建或者直接下载可执行文件
https://hub.docker.com/r/snowdreamtech/frps
https://github.com/fatedier/frp/releases
我是通过docker搭建的

1
docker run --restart=always --network host -d -v /etc/frp/frps.ini:/etc/frp/frps.ini --name frps snowdreamtech/frps

可以看到需要一个配置文件frps.ini

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[common]
# 监听端口
bind_port = 7000
# 面板端口
dashboard_port = 7500
# 登录面板账号设置
dashboard_user = admin
dashboard_pwd = password
# 设置http及https协议下代理端口
vhost_http_port = 7080
vhost_https_port = 7443
# 子域名配置
subdomain_host = frp.kyo86.com
# 身份验证
token = 随意填一些字符

子域名需要配合指定*.frp.kyo86.com泛域名解析使用

客户端使用

客户端也可以通过docker或直接下载可执行文件
https://hub.docker.com/r/snowdreamtech/frpc
https://github.com/fatedier/frp/releases
有两种使用方式,一种是通过frpc.ini配置文件进行配置,一种是通过命令参数进行配置。

配置文件样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[common]
server_addr = 服务器IP
server_port = 7000
# 身份验证
token = 与服务端frps.ini中保存一致

[jenkins]
type = http
local_ip = 127.0.0.1
local_port = 8080
use_encryption = false
use_compression = false
subdomain = jenkins

[joplin]
type = tcp
local_ip = 127.0.0.1
local_port = 22300
route_port = 22300
use_encryption = false
use_compression = false

命令行的方式样例

1
2
3
frpc http -n 代理名称 -s frp服务器地址:frp服务器端口 -l 本地端口 --sd 子域名 -t TOKEN
frpc tcp -n 代理名称 -s frp服务器地址:frp服务器端口 -l 本地端口 -r 远程端口 -t TOKEN
frpc udp -n 代理名称 -s frp服务器地址:frp服务器端口 -l 本地端口 -r 远程端口 -t TOKEN

容器启动命令

1
docker run -p 8080:8080 -p 50000:50000 -h jenkins --name=jenkins --restart=on-failure -v /etc/localtime:/etc/localtime -v jenkins_home:/var/jenkins_home jenkins/jenkins:lts-jdk11

我通常会把/root和/mnt也映射进容器,并且通过root用户启动容器,用起来方便但安全性差,需视情况选择。

1
2
3
4
5
6
docker run -u root --privileged -p 8080:8080 -p 50000:50000 -h jenkins --name=jenkins --restart=on-failure\
-v /etc/localtime:/etc/localtime\
-v jenkins_home:/var/jenkins_home\
-v /root:/root\
-v /mnt:/mnt\
jenkins/jenkins:lts-jdk11

修改Jenkins时区

系统管理(Manage Jenkins)-脚本命令行(Script Console)

执行

1
System.setProperty('org.apache.commons.jelly.tags.fmt.timeZone','Asia/Shanghai')

以root进入容器

1
docker exec -uroot -it jenkins bash

修改jenkins启动脚本

1
vim /usr/local/bin/jenkins.sh

找到java启动war的语句,增加启动参数

1
-Xms256M -Xmx256M -Xmn96M -Xss1M -Djava.awt.headless=true -Dorg.apache.commons.jelly.tags.fmt.timeZone=Asia/Shanghai

参照修改Linux时区

重启容器,在宿主机执行

1
docker restart jenkins

允许匿名通过Token构建项目

默认不允许匿名触发构建,也不建议匿名触发,在外部触发可以通过

1
curl -o /dev/null -s -w %{http_code} http://<用户名>:<密码或API_TOKEN>@<JENKINS_URL>/job/<PROJECT>/build?token=<PROJ_TOKEN>

或者

1
curl -w %{http_code} -u <用户名>:<API_TOKEN> http://<JENKINS_URL>/job/<PROJECT>/build?token=<PROJ_TOKEN>

其中API_TOKEN是属于用户的,在用户管理界面可以配置。PROJ_TOKEN是属于项目的,在项目配置界面构建选项可以配置。

如需匿名触发构建

系统管理-全局安全配置-授权策略-登录用户可以做任何事

勾选“匿名用户具有可读权限”

这样设置之后就可以通过

1
curl -o /dev/null -s -w %{http_code} <JENKINS_URL>/job/<PROJECT>/build?token=<TOKEN>

从外部来触发构建,token在项目里设置。

项目权限配置策略

依赖插件 Matrix Authorization Strategy

需求:

  1. 系统管理员可以看到和控制所有的项目
  2. 普通用户可以创建项目和完全控制自己创建的项目,以及被人别人授权控制的项目

配置方法

系统管理-全局安全配置
系统管理员会自动勾上Administer去权限,多个系统管理就手动Add user然后勾上Administer
对Authenticated Users 只勾上 全部Read 和任务Create
配置项目时勾上启动项目安全,如果是对所有人开放管理和使用的项目,就把Authenticated Users后面的勾都勾上
如果是仅自己使用,就把自己用户名后面的所有勾都勾上

BTW: 不方便的一点就是目前添加用户需要手动输入用户名,而不能选择,希望后面的版本会改进。

rsync可以用在两个目录间同步文件,无论是相同主机或不同主机间。

rsync命令格式

1
rsync -av --delete --exclude-from=exclude.lst --stop-after=240 源路径/ 目标路径/

其中-av是多个选项的缩写,表示递归的进行、不改变文件属性、不改变文件owner等,–delete表示删除目标路径下载源路径不存在的文件。

–exclude-from可以让我们编写一个列表排除不想同步的文件

1
--exclude-from=exclude.lst

exclude.lst样例

1
2
3
4
test
*_test
test.*
*_test.*

–stop-after指定同步超时时间(分钟)

1
--stop-after=240

大量数据首次同步时会耗时很长,而后续增量同步则耗时较少,我们可能不想影响服务器的其他工作选择在指定时段同步,那么加上这个参数可以防止首次同步持续很久,这样将通过连续多天把首次完整同步做完。

在低版本上这个参数叫--time-limit但是我测试下来指定这个参数超时后并不会停下来,不知道是不是BUG。

通常情况我只选择性使用上面这些参数,更多的参数可以通过rsync --help查看

一般来说源路径和目标路径都是目录,最好在结尾加上 / ,以避免在目标路径下新建出源路径文件夹的歧义。

如果路径是本地的,没什么特别的。如果路径是远程的,可以使用

1
[user@]HOST[:PORT]:/path/

也可以使用

1
rsync://[USER@]HOST[:PORT]/MODULE/path/

前者通过ssh服务(默认22端口)

后者通过rsync服务(默认873端口)

如果是少量文件或临时的同步,使用ssh就够了,如果是大量的定期的同步,应该使用rsync,因为rsync需要在一端开启rsync服务,另一端使用rsync相当于客户端连接服务器,而有理由相信rsync服务器会做更多的事来减少不必要的网络流量。

rsync服务器配置文件样例

/etc/rsyncd.conf

1
2
3
4
5
6
7
8
9
10
11
12
use chroot = false
max connections = 0
syslog facility = deamon
log file = /var/log/rsyncd.log
pid file = /var/run/rsyncd.pid

[database]
path = /data/data_manager/database
comment = database
strict modes = false
read only = 1
list = no

rsync服务器配置文件详解

全局参数

在文件中 [module] 之外的所有配置行都是全局参数。当然也可以在全局参数部分定义模块参数,这时该参数的值就是所有模块的默认值。

参数 说明 默认值
address 在独立运行时,用于指定的服务器运行的 IP 地址。由 xinetd 运行时将忽略此参数,使用命令行上的 –address 选项替代。 本地所有IP
port 指定 rsync 守护进程监听的端口号。 由 xinetd 运行时将忽略此参数,使用命令行上的–port 选项替代。 873
motd file 指定一个消息文件,当客户连接服务器时该文件的内容显示给客户。
pid file rsync 的守护进程将其 PID 写入指定的文件。
log file 指定 rsync 守护进程的日志文件,而不将日志发送给 syslog。
syslog facility 指定 rsync 发送日志消息给 syslog 时的消息级别。 daemon
socket options 指定自定义 TCP 选项。

模块参数

模块参数主要用于定义 rsync 服务器哪个目录要被同步。模块声明的格式必须为 [module] 形式,这个名字就是在 rsync 客户端看到的名字,类似于 Samba 服务器提供的共享名。而服务器真正同步的数据是通过 path 来指定的。可以根据自己的需要,来指定多个模块,模块中可以定义以下参数:

a. 基本模块参数

参数 说明 默认值
path 指定当前模块在 rsync 服务器上的同步路径,该参数是必须指定的。
comment 给模块指定一个描述,该描述连同模块名在客户连接得到模块列表时显示给客户。

b. 模块控制参数

参数 说明 默认值
use chroot 若为 true,则 rsync 在传输文件之前首先 chroot 到 path 参数所指定的目录下。这样做的原因是实现额外的安全防护,但是缺点是需要 root 权限,并且不能备份指向 path 外部的符号连接所指向的目录文件。 true
uid 指定该模块以指定的 UID 传输文件。 nobody
gid 指定该模块以指定的 GID 传输文件。 nobody
max connections 指定该模块的最大并发连接数量以保护服务器,超过限制的连接请求将被告知随后再试。 0(没有限制)
lock file 指定支持 max connections 参数的锁文件。 /var/run/rsyncd.lock
list 指定当客户请求列出可以使用的模块列表时,该模块是否应该被列出。如果设置该选项为 false,可以创建隐藏的模块。 true
read only 指定是否允许客户上传文件。若为 true 则不允许上传;若为 false 并且服务器目录也具有读写权限则允许上传。 true
write only 指定是否允许客户下载文件。若为 true 则不允许下载;若为 false 并且服务器目录也具有读权限则允许下载。 false
ignore errors 指定在 rsync 服务器上运行 delete 操作时是否忽略 I/O 错误。一般来说 rsync 在出现 I/O 错误时将将跳过 –delete 操作,以防止因为暂时的资源不足或其它 I/O 错误导致的严重问题。 true
ignore nonreadable 指定 rysnc 服务器完全忽略那些用户没有访问权限的文件。这对于在需要备份的目录中有些不应该被备份者获得的文件时是有意义的。 false
timeout 该选项可以覆盖客户指定的 IP 超时时间。从而确保 rsync 服务器不会永远等待一个崩溃的客户端。对于匿名 rsync 服务器来说,理想的数字是 600(单位为秒)。 0 (未限制)
dont compress 用来指定那些在传输之前不进行压缩处理的文件。该选项可以定义一些不允许客户对该模块使用的命令选项列表。必须使用选项全名,而不能是简称。当发生拒绝某个选项的情况时,服务器将报告错误信息然后退出。例如,要防止使用压缩,应该是:”dont compress = *”。 *.gz *.tgz *.zip *.z *.rpm *.deb *.iso *.bz2 *.tbz

c. 模块文件筛选参数

参数 说明 默认值
exclude 指定多个由空格隔开的多个文件或目录(相对路径),并将其添加到 exclude 列表中。这等同于在客户端命令中使用 –exclude 来指定模式。
exclude from 指定一个包含 exclude 规则定义的文件名,服务器从该文件中读取 exclude 列表定义。
include 指定多个由空格隔开的多个文件或目录(相对路径),并将其添加到 include 列表中。这等同于在客户端命令中使用 –include 来指定模式 。
include from 指定一个包含 include 规则定义的文件名,服务器从该文件中读取 include 列表定义。
  • 一个模块只能指定一个exclude 参数、一个include 参数。
  • 结合 include 和 exclude 可以定义复杂的exclude/include 规则 。
  • 这几个参数分别与相应的rsync 客户命令选项等价,唯一不同的是它们作用在服务器端。
  • 关于如何书写规则文件的内容请参考http://www.howtocn.org/rsync:use_rsync。

d. 模块用户认证参数

参数 说明 默认值
auth users 指定由空格或逗号分隔的用户名列表,只有这些用户才允许连接该模块。这里的用户和系统用户没有任何关系。用户名和口令以明文方式存放在 secrets file 参数指定的文件中。 (匿名方式)
secrets file 指定一个 rsync 认证口令文件。只有在 auth users 被定义时,该文件才起作用。
strict modes 指定是否监测口令文件的权限。若为 true 则口令文件只能被 rsync 服务器运行身份的用户访问,其他任何用户不可以访问该文件。 true
  • rsync 认证口令文件的权限一定是 600,否则客户端将不能连接服务器。
  • rsync 认证口令文件中每一行指定一个 用户名:口令 对,格式为:

username:passwd

  • 一般来说口令最好不要超过8个字符。若您只配置匿名访问的 rsync 服务器,则无需设置上述参数。

e. 模块访问控制参数

参数 说明 默认值
hosts allow 用一个主机列表指定哪些主机客户允许连接该模块。不匹配主机列表的主机将被拒绝。 *
hosts deny 用一个主机列表指定哪些主机客户不允许连接该模块。

客户主机列表定义可以是以下形式:

  • 单个IP地址。例如:192.168.0.1
  • 整个网段。例如:192.168.0.0/24,192.168.0.0/255.255.255.0
  • 可解析的单个主机名。例如:centos,centos.bsmart.cn
  • 域内的所有主机。例如:*.bsmart.cn
  • “*”则表示所有。
  • 多个列表项要用空格间隔。

f. 模块日志参数

参数 说明 默认值
transfer logging 使 rsync 服务器将传输操作记录到传输日志文件。 false
log format 指定传输日志文件的字段。 ”%o %h [%a] %m (%u) %f %l”

设置了”log file”参数时,在日志每行的开始会添加”%t [%p]“。

可以使用的日志格式定义符如下所示:

  • %a - 远程IP地址

  • %h - 远程主机名

  • %l - 文件长度字符数

  • %p - 该次 rsync 会话的 PID

  • %o - 操作类型:”send” 或 “recv”

  • %f - 文件名

  • %P - 模块路径

  • %m - 模块名

  • %t - 当前时间

  • %u - 认证的用户名(匿名时是 null)

  • %b - 实际传输的字节数

  • %c - 当发送文件时,记录该文件的校验码

Ubuntu上默认使用的是netplan管理网络(至少18.04~22.04都是,其他更早的版本我没用过)

在18.04上查看 /etc/network/interfaces文件可以看到

1
2
3
4
# ifupdown has been replaced by netplan(5) on this system.  See
# /etc/netplan for current configuration.
# To re-enable ifupdown on this system, you can run:
# sudo apt install ifupdown

在22.04上索性这个文件都没了。

在没有网络的情况下无法 sudo apt install ifupdown,只能去配置netplan,/etc/netplan是一个目录,下面会有一个默认的 *.yaml 文件。

我遇到过 50-cloud-init.yaml 以及 00-installer-config.yaml,总就是通过它配置网络。

可以先用ifconfig -a查看一下当前的网络状态和网卡名称。如果ifconfig命令也没有,可以用ip ad

假设我们有两块网卡,一个叫enp2s0是有线网卡,一个叫wlp3s0是无线网卡。

分别看一下配置方案:

有线网卡固定IP配置

先启用网卡

1
ifconfig enp2s0 up

编辑/etc/netplan/*.yaml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
network:
renderer: networkd
ethernets:
enp2s0:
dhcp4: no
addresses:
- 192.168.1.10/24
gateway4: 192.168.1.1
nameservers:
addresses:
- 114.114.114.114
- 8.8.8.8
search: []
version: 2

应用生效

1
netplan apply

有线网卡动态IP配置

先启用网卡

1
ifconfig enp2s0 up

编辑/etc/netplan/*.yaml配置

1
2
3
4
5
6
network:
renderer: networkd
ethernets:
enp2s0:
dhcp4: yes
version: 2

应用生效

1
netplan apply

无线网卡固定IP配置

配置无线网卡要先配好有限然后安装两个东西

1
apt update && apt install -y wpasupplicant network-manager

然后关闭有线网卡,开启无线网卡

1
2
ifconfig enp2s0 down
ifconfig wlp3s0 up

编辑/etc/netplan/*.yaml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
network:
renderer: NetworkManager
ethernets:
wlp3s0:
dhcp4: no
addresses:
- 192.168.1.10/24
gateway4: 192.168.1.1
nameservers:
addresses:
- 114.114.114.114
- 8.8.8.8
search: []
access-points:
"WIFI名称":
password: "WIFI密码"
version: 2

应用生效

1
2
sudo netplan generate
sudo netplan apply

无线网卡动态IP配置

类似无线网卡固定IP的步骤,配置改为

1
2
3
4
5
6
7
8
9
network:
renderer: NetworkManager
ethernets:
wlp3s0:
dhcp4: yes
access-points:
"WIFI名称":
password: "WIFI密码"
version: 2

常用网络命令

临时指定IP(重启失效)

1
ifconfig <网口号> <IP地址> netmask <子网掩码>

Example:

1
ifconfig eth0 192.168.1.141 netmask 255.255.255.0

如果没有ifconfig命令,可以用ip命令

1
ip addr add 192.168.1.141/24 dev eth0

配置默认网关

1
route add default gw 192.168.1.1

查看路由表

1
netstat -rn

1
route -n

1
ip route

Destination 0.0.0.0的 GateWay就是默认网关

添加一条路由

1
ip route add 192.168.10.0/24 via 192.168.1.99

手动动态获取IP地址

比如我们把网络接口插上网线UP起来,然后什么都不配置,直接dhclient,如果路由器上开启了DHCP,那么应该会分到一个IP,有时候明明配置好了,但是重启不会自动获取IP地址,手动执行一次dhclient就可以获取到,不清楚原因

1
dhclient

查看开放端口和对应的进程ID

1
netstat -anp

在发现一个端口被占用,想找出对应的进程

1
netstat -anp|grep <port>

测试一个端口是否开放

1
telnet <ip> <port>

1
nc -v <ip> <port>

BTW:如果一些网络命令不能执行,安装这几个包试试

1
apt update && apt install -y iproute2 iputils-ping net-tools

之前在公司内网搭建了PPTPD服务器解决疫情期间远程办公从家里连接公司内网服务器的问题,但一直不太稳定,平时随便用用还行,偶尔断开可以重连,一直想换个方式搭建,之前尝试过其他搭建方式没有成功,后来恢复线下办公后需求不强了就没有继续尝试,今天发现在Ubuntu系统下基于Docker搭建相对简单,于是记录下来

使用Docker Hub上的kylemanna/openvpn镜像

kylemanna/openvpn - Docker Image | Docker Hub

拉取镜像

1
docker pull kylemanna/openvpn

设置卷名环境变量

取一个名称用来创建用于存放openvpn数据的docker volume,建议用ovpn-data-前缀,后面的example换成自己取的

1
OVPN_DATA="ovpn-data-example"

初始化数据

VPN.SERVERNAME.COM换成自己的IP地址或域名

1
2
3
docker volume create --name $OVPN_DATA
docker run -v $OVPN_DATA:/etc/openvpn --rm kylemanna/openvpn ovpn_genconfig -u udp://VPN.SERVERNAME.COM
docker run -v $OVPN_DATA:/etc/openvpn --rm -it kylemanna/openvpn ovpn_initpki

启动OpenVPN服务器

1
docker run --name=openvpn -v $OVPN_DATA:/etc/openvpn -d -p 1194:1194/udp --cap-add=NET_ADMIN kylemanna/openvpn

生成不带密码的客户端证书

CLIENTNAME换成自定义的客户端标识,可以取形如USERNAME@SERVERNAME方便管理

1
docker run -v $OVPN_DATA:/etc/openvpn --rm -it kylemanna/openvpn easyrsa build-client-full CLIENTNAME nopass

导出包含证书的客户端配置文件

CLIENTNAME同样换成自定义的客户端标识,导出为当前目录的CLIENTNAME.ovpn文件

1
docker run -v $OVPN_DATA:/etc/openvpn --rm kylemanna/openvpn ovpn_getclient CLIENTNAME > CLIENTNAME.ovpn

如果服务器端口做过NAT映射,可以打开CLIENTNAME.ovpn修改其中的端口号

作废客户端证书

先查看openvpn数据卷的所在位置

1
docker volume inspect ovpn-data-example

删除文件

1
rm ***/_data/pki/private/CLIENTNAME.key

编辑

1
vim ***/_data/pki/index.txt

删除CLIENTNAME所在的行

取消全局代理

默认配置会将全部对外流量通过VPN,如果想要指定仅访问公司局域网时走VPN,其他流量走默认路由不变,可以通过在.ovpn配置文件中追加

1
2
3
4
5
route-nopull
route-metric 150
route remote_host 255.255.255.255 net_gateway
route 172.19.0.0 255.255.0.0 net_gateway
route 192.168.1.0 255.255.0.0 vpn_gateway

.ovpn配置文件的结尾找到

1
# redirect-gateway def1

注释掉这一行

其中route-nopull表示不接受服务器对于修改路由表的推送,而可以按照我们之后指定的规则去修改。

我所在的内网网段是172.19.0.0/16,所以明确指定路由,走net_gateway,也就是原有网关。

192.168.1.0/24是需要走VPN的,所以也明确指定了路由,走vpn_gateway。

redirect-gateway def1作用是将默认网关重定向到 VPN 服务器上,所以要注释掉。

修改服务器配置

<openvpn容器卷路径>/_data/openvpn.conf

1
#push "block-outside-dns"

<openvpn容器卷路径>/_data/ovpn_env.sh

1
declare -x OVPN_DISABLE_PUSH_BLOCK_DNS=1

重启服务器,重连客户端

客户端

Windows 7

Windows 10

macOS v2

macOS v3

Ubuntu

安装openvpn

1
apt -y install openvpn

客户端和服务器会同时安装,我们不用服务器可以把它关掉

1
service openvpn stop

禁止openvpn服务器开启自启动

1
systemctl disable openvpn

用客户端配置文件在后台创建VPN连接

1
openvpn --daemon --config CLIENTNAME.ovpn

BTW:根据测试,同一份配置文件似乎不能在多个主机重复使用,如果使用会造成两边连接频繁断开。

最长公共前缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 预处理字符串s,得到所有s任意后缀即s[i:]和s[j:]的所有最长公共前缀
class LCP {
vector<vector<int>> dp;
public:
LCP(const string& s) {
dp.resize(s.size()+1, vector<int>(s.size()+1));
for(int j=s.size()-1;j>=0;j--) {
for(int i=j;i>=0;i--) {
dp[i][j]=(s[i]==s[j]?dp[i+1][j+1]+1:0);
}
}
}

int operator()(int i, int j) const {
return dp[min(i,j)][max(i,j)];
}
};

这个算法本身本身就是动态规划,并不难,而有时这个问题会嵌套于另一个问题里,使整体变得略复杂,而如果能想到LCP问题可以用O(n^2)预处理,则复杂问题迎刃而解。

6195. 对字母串可执行的最大删除数 - 力扣(LeetCode)

一个朴素的解法是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int deleteString(string s) {
int n = s.size();
// 用dp[i] 表示删除s[i:]的最大操作次数
// 删除一个长度为j的前缀之后得到的是一个子问题:s[i+j:]的最大操作次数
// 能删除s[i:i+j]的前提是s[i:i+j]==s[i+j:i+j+j]
vector<int> dp(n, 1);
for(int i=n-2;i>=0;i--) {
for(int j=1;j<=(n-i)/2;j++) {
if(memcmp(&s[i], &s[i+j], j) == 0) { // s[i:i+j]==s[i+j:i+j+j]
dp[i] = max(dp[i], 1+dp[i+j]);
}
}
}
return dp[0];
}
};

如果将memcmp(&s[i], &s[i+j], j)视为O(n)的复杂度,那么算法整体是O(n^3)复杂度。

但如果我们预处理得到s所有后缀的最长公共前缀的长度,就可以判断长度是否>=j来代替判断子串相等

从而把O(n3)的复杂度降到O(n2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int deleteString(string s) {
int n = s.size();
// 用dp[i] 表示删除s[i:]的最大操作次数
// 删除一个长度为j的前缀之后得到的是一个子问题:s[i+j:]的最大操作次数
// 能删除s[i:i+j]的前提是s[i:i+j]==s[i+j:i+j+j]
LCP lcp(s);
vector<int> dp(n, 1);
for(int i=n-2;i>=0;i--) {
for(int j=1;j<=(n-i)/2;j++) {
if(lcp(i, i+j)>= j) { // s[i:i+j]==s[i+j:i+j+j]
dp[i] = max(dp[i], 1+dp[i+j]);
}
}
}
return dp[0];
}
};

起因:文件共享服务器原本开启的NFS共享,发现在Windows客户端访问并不稳定,想改用Samba共享,但是很多人在用NFS访问,想先同时开启两种共享同时支持访问,遇到的问题是通过NFS客户端创建出的文件权限是0755,通过Samba客户端不可写,而通过Samba客户端创建的文件可以通过create mask和 force create mask指定成任意的权限,可以保证NFS客户端可写,而且没有找到NFS如何配置可以控制新建文件的权限。于是想换个思路监视文件创建,在创建后立刻修改权限。

inotifywait 异步文件系统监控机制

inotify 一种强大的、细粒度的、异步文件系统监控机制,它满足各种各样的文件监控需要,可以监控文件系统的访问属性、读写属性、权限属性、删除创建、移动等操作,也就是可以监控文件发生的一切变化。
inotify-tools 是一个C库和一组命令行的工作提供Linux下inotify的简单接口。inotify-tools安装后会得到inotifywait和inotifywatch这两条命令:

inotifywait命令 可以用来收集有关文件访问信息,Linux发行版一般没有包括这个命令,需要安装inotify-tools,这个命令还需要将inotify支持编译入Linux内核,好在大多数Linux发行版都在内核中启用了inotify。
inotifywatch命令 用于收集关于被监视的文件系统的统计数据,包括每个 inotify 事件发生多少次。

安装

1
apt install inotify-tools

修改最大监控文件数

1
sysctl -w fs.inotify.max_user_watches="1000000"

可以通过下面的命令确认修改成功

1
cat /proc/sys/fs/inotify/max_user_watches

永久修改(重启后自动修改)

vim /etc/sysctl.conf

追加 fs.inotify.max_user_watches=1000000

编写监控脚本

1
2
3
4
5
6
7
#!/bin/bash
inotifywait -mrq --timefmt '%y-%m-%d %H:%M' --format '%T %e %w%f' -e create /data/share/ | while read DATE TIME EVENT FILEPATH
do
echo "${FILEPATH} was ${EVENT} at ${DATE}_${TIME}"
chmod 777 "${FILEPATH}"
done

此脚本监控/data/share目录下的create事件,一旦出现create事件则调用chmod把刚创建的文件权限修改为0777。

BTW:通常情况可以正常工作,实际测试时,如果在创建后立即重命名,可能会导致监控脚本来不及处理,在执行到chmod时文件已经不存在。

注意这里把%w%f拼在一起用FILEPATH作为最后一个变量读取出来是可以解决路径或文件名有空格的问题。

其他选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
inotifywait 3.14
Wait for a particular event on a file or set of files.
Usage: inotifywait [ options ] file1 [ file2 ] [ file3 ] [ ... ]
Options:
-h|--help Show this help text.
@<file> Exclude the specified file from being watched.
--exclude <pattern>
Exclude all events on files matching the
extended regular expression <pattern>.
--excludei <pattern>
Like --exclude but case insensitive.
-m|--monitor Keep listening for events forever. Without
this option, inotifywait will exit after one
event is received.
-d|--daemon Same as --monitor, except run in the background
logging events to a file specified by --outfile.
Implies --syslog.
-r|--recursive Watch directories recursively.
--fromfile <file>
Read files to watch from <file> or `-' for stdin.
-o|--outfile <file>
Print events to <file> rather than stdout.
-s|--syslog Send errors to syslog rather than stderr.
-q|--quiet Print less (only print events).
-qq Print nothing (not even events).
--format <fmt> Print using a specified printf-like format
string; read the man page for more details.
--timefmt <fmt> strftime-compatible format string for use with
%T in --format string.
-c|--csv Print events in CSV format.
-t|--timeout <seconds>
When listening for a single event, time out after
waiting for an event for <seconds> seconds.
If <seconds> is 0, inotifywait will never time out.
-e|--event <event1> [ -e|--event <event2> ... ]
Listen for specific event(s). If omitted, all events are
listened for.

Exit status:
0 - An event you asked to watch for was received.
1 - An event you did not ask to watch for was received
(usually delete_self or unmount), or some error occurred.
2 - The --timeout option was given and no events occurred
in the specified interval of time.

Events:
access file or directory contents were read
modify file or directory contents were written
attrib file or directory attributes changed
close_write file or directory closed, after being opened in
writable mode
close_nowrite file or directory closed, after being opened in
read-only mode
close file or directory closed, regardless of read/write mode
open file or directory opened
moved_to file or directory moved to watched directory
moved_from file or directory moved from watched directory
move file or directory moved to or from watched directory
create file or directory created within watched directory
delete file or directory deleted within watched directory
delete_self file or directory was deleted
unmount file system containing file or directory unmounted

文件共享主流协议:NFS/CIFS,Samba是Linux上创建CIFS共享的工具

NFS是Linux间常用的共享协议

Linux服务端需要安装nfs-kernel-server,服务名称nfs-kernel-server别名nfs-server,依赖服务rpcbind,共享配置文件/etc/exports

​ NFS 服务器需要端口 111 2049 4045-4049

Linux客户端需要安装nfs-common,通过mount命令挂载使用,实际上nfs-common安装出的是mount的子命令mount.nfs

Windows通常不用做NFS服务端,不清楚是否支持

Windows客户端“启用或关闭Windows功能”中找到“NFS客户端”勾选并安装,通过mount命令挂载使用。

配置NFS服务器

通过NFS挂载的共享,如果指定了no_all_squash则权限按照与客户端登录的用户id相同id的服务端用户的权限计算,简直是乱来,所以我觉得应该始终指定
all_squash, root_squash,让所有访问者按照nobody:nogroup的权限计算

只读共享参考:

1
/data *(insecure,ro,async,all_squash,root_squash,no_subtree_check)

读写共享参考:

1
/data 192.168.1.123(insecure,rw,sync,all_squash,root_squash,no_subtree_check) 192.168.1.124(insecure,rw,sync,all_squash,root_squash,no_subtree_check)

NFS客户端挂载命令

1
mount 172.17.0.2:/data <挂载点>

CIFS是Windows默认的共享协议

Windows服务端依赖Windows的Server服务(默认存在并自动启动)

Windows客户端通过net use 命令使用,或者打开“此电脑”-计算机菜单-映射网络驱动器,或者直接在文件夹的地址栏输入共享路径即可。

Linux服务端需要安装samba,服务名称smbd,共享配置文件/etc/samba/smb.conf

​ samba 服务器需要端口 137-139 445

Linux客户端需要安装cifs-utils,通过mount命令挂载使用,实际上cifs-utils安装出来的是mount的子命令mount.cifs

配置Samba服务器

首先需要添加Samba服务器的用户,这个用户同时必须已经是系统用户,这里我们新建系统用户samba

1
useradd samba

将samba添加为Samba服务器用户

1
smbpasswd -a samba

在配置文件/etc/samba/smb.conf追加

1
2
3
4
5
6
7
8
9
10
11
12
13
[data]
comment = Shared Data
path = /data # 共享路径
workgroup = WORKGROUP # 和WINDOWS工作组一致,从局域网内其他WINDOWS系统的属性可以看到
public = yes # 是否允许游客访问
guest account = nobody # 游客用户
browseable = yes # 是否允许列出文件
writable = yes # 是否可写
write list = samba # 具有写权限的用户(多用户空格分割)
create mask = 0777 # 创建文件时赋权,相当于执行chmod
directory mask = 0777
force user = nobody # 创建文件时强制所有者,相当于执行chown
force group = nogroup

Samba客户端挂载命令

游客

1
mount //172.17.0.2/data <挂载点> -ouser=guest,pass=

其他用户

1
mount //172.17.0.2/data <挂载点> -ouser=samba,pass=<密码>

在Windows 上可以用net use来挂载Linux的Samba共享就像挂载Windows的共享一样。

1
net use X: \\172.17.0.2\data <密码> /user:samba

PS:虽然Windows可以安装NFS客户端来访问Linux的NFS共享,但是不稳定,推荐还是在Linux用samba做共享,如果只在Linux间共享可以用NFS。而且samba有着更完善的权限控制,可以用配置文件指定通过共享创建的文件或文件夹的owner和权限,并且可以通过带有通配符的ip指定访问黑名单或白名单,同时允许通过密码访问或仅限制IP访问。而NFS似乎只能通过IP指定访问权限而不能通过用户+密码的方式,并且只能指定创建出文件的owner不能指定权限。

我们知道对于线性表,有两种基础的数据结构:数组和链表。

对于数组而言,支持O(1)的随机访问,结尾插入删除不考虑扩容可以近似认为是O(1),而中间插入或删除则需要O(n)的时间移动插入点后的所有数据。

对于链表而言,不支持随机访问,访问第k个元素需要从头遍历需要O(n)的时间,而插入和删除只需要改变next指针即O(1)的时间,当然定位到插入删除点还是需要O(n)的时间。

如果我们把数组和链表结合起来,有链表连接大块的数组而不是单个元素,那么访问、插入、删除都会折中,有时这很有用,我们先思考这个大块数组要取多大合适,如果数据的最大长度是n,我们块大小取k,则会形成n/k个块,那么定位到任意一个元素的最坏时间是n/k(即在链表的最后一个结点上,需要从头遍历的时间),而插入删除一个元素的最坏时间是k,即在大块的头上插入需要移动整个大块的数据。

k越大n/k越小,如果我们折中,显然是让k=n/k,即k=sqrt(n)。那么上述操作的复杂度最坏情况下都是O(sqrt(n))。

思路就是这么简单,还有一些实现细节,插入时考虑分裂大块,删除时考虑合并大块,具体直接看代码吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113

//一个节点的初始大小
const int NODESIZE=160; //如果数据大小最大为n,取在sqrt(n)/2会比较好,对于10w的数据这个大小应该比较优
//加上缓冲区一个节点应该占用NODESIZE*2的空间
//任何一个节点大小不会>=NODESIZE*2,任何两个相邻的节点大小不会<=NODESIZE

template<typename T>
class BlockList{
public:
struct BLNode{
T data[NODESIZE<<1];
int size;//该结点中已经使用的大小
BLNode* next;
BLNode():size(0),next(NULL){}
};
BLNode* _head;
BLNode* _tail;
int _size; //整个表中的元素数

void split(BLNode* p){
BLNode* q = new BLNode();
q->next = p->next;
p->next = q;
memcpy(q->data, p->data+NODESIZE, sizeof(T)*(p->size-NODESIZE));
q->size = p->size-NODESIZE;
p->size = NODESIZE;
if(_tail == p) _tail = q;
}

void join(BLNode* p, BLNode* q) {
memcpy(p->data+p->size, q->data, sizeof(T)*q->size );
p->size += q->size;
p->next = q->next;
if(_tail == q) _tail = p;
delete q;
}
public:
BlockList(int size=0, const T& val=T()):_size(size) {
_head = _tail = new BLNode();
if(size != 0) {
while(size>NODESIZE){
std::fill(_tail->data, _tail->data+NODESIZE, val);
size -= NODESIZE;
_tail->size = NODESIZE;
if(!_tail->next) {
_tail->next = new BLNode();
}
_tail = _tail->next;
}
std::fill(_tail->data, _tail->data+size, val);
_tail->size = size;
}
}
int size(){return _size;}

//通过下标随机访问,复杂度O(sqrt(size))
T& operator [] (int i){
assert(0<=i && i<_size);
BLNode* p = _head;
while(i>=p->size){
i -= p->size;
p = p->next;
}
return p->data[i];
}

//在尾部追加数据
void push_back(const T& d) {
_size++;
_tail->data[_tail->size++] = d;
if(_tail->size==(NODESIZE<<1)){
split(_tail);
}
}

//在下标i处插入数据d,原数据依次后移
void insert(int i, const T& d){
assert(0<=i && i<=_size);
_size++;
BLNode* p=_head;
while(i>p->size){
i -= p->size;
p = p->next;
}
for(int j=p->size-1; j>=i; j--){
p->data[j+1]=p->data[j];
}
p->data[i]=d;
p->size++;
if(p->size==(NODESIZE<<1)){
split(p);
}
}

//删除在下标i处的数据,后续数据依次前移
void erase(int i){
assert(0<=i && i<_size);
_size--;
BLNode* p=_head;
while(i>=p->size){
i -= p->size;
p = p->next;
}
for(int j=i;j<p->size-1;j++){
p->data[j]=p->data[j+1];
}
p->size--;
BLNode* q = p->next;
if(q && p->size+q->size<=NODESIZE){
join(p, q);
}
}
};

406. 根据身高重建队列

这道题可以把人的身高从高到低排序,然后依次根据ki插入到数组的ki位置就可以了,如果用数组实现就是O(n2)的,改用块状链表可以降到O(n1.5)

把初始结点大小取在16~32有可能跑出击败100%用户的时间,sqrt(2000)/2是22左右。

1
2
3
4
5
6
执行用时:
12 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:
11.6 MB, 在所有 C++ 提交中击败了82.61%的用户
通过测试用例:
36 / 36