JoyLau's Blog

JoyLau 的技术学习与思考

Docker 容器中 IP 的配置

将 spring cloud 项目部署到 docker 容器中后,虽然可以配置容器的端口映射到宿主机的端口
但是在 eureka 界面显示的instance id 是一串随机的字符串,类似于 d97d725bf6ae 这样的
但是,事实上,我们想让他显示出 IP ,这样我们可以直接点击而打开 info 端点信息

修改 3 处配置项:

1
2
3
4
5
6
7
8
eureka:
client:
service-url:
defaultZone: http://34.0.7.183:9368/eureka/
instance:
prefer-ip-address: true
instance-id: ${eureka.instance.ip-address}:${server.port}
ip-address: 34.0.7.183
  1. eureka.instance.prefer-ip-address 配置为 true , 表示 instance 使用 ip 配置
  2. eureka.instance.prefer-ip-address 配置当前 instance 的物理 IP
  3. eureka.instance.prefer-instance-id 界面上的 instance-id 显示为 ip + 端口

docker-compose 的解决方法

通常情况下,我们使用 springcloud 都会有很多的服务需要部署,就会产生很多的容器,这么多的容器再使用 docker 一个个操作就显得很复杂
这时候需要一个编排工具,于是我们就使用 docker-compose 来部署 springcloud 服务

  1. 修改 eureka 的配置
1
2
3
4
5
6
spring:
application:
name: traffic-service-eureka
eureka:
instance:
hostname: ${spring.application.name}

使用 docker-compose 我们放弃使用 ip 来进行容器间的相互通信,继而使用 hostname,这就相当于在 /etc/hosts 添加了一条记录

  1. 接下来所有的 eureka 的 client 都使用 traffic-service-eureka 这个 hostname 来连接
1
2
3
4
eureka:
client:
service-url:
defaultZone: http://traffic-service-eureka:9368/eureka/
  1. 如果说想在 eureka 的界面上能够直接显示宿主机的 IP 和 连接地址的话,还需要设置
1
2
3
4
5
eureka:
instance:
prefer-ip-address: true
instance-id: ${eureka.instance.ip-address}:${server.port}
ip-address: 34.0.7.183
  1. docker-compose 的配置:
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
server:
image: 34.0.7.183:5000/joylau/traffic-service-server:1.2.0
container_name: traffic-service-server
ports:
- 9368:9368
restart: always
volumes:
- /Users/joylau/log/server:/home/liufa/app/server/logs
environment:
activeProfile: prod
hostname: traffic-service-eureka
healthcheck:
test: "/bin/netstat -anp | grep 9368"
interval: 10s
timeout: 3s
retries: 1
admin:
image: 34.0.7.183:5000/joylau/traffic-service-admin:1.2.0
container_name: traffic-service-admin
ports:
- 9335:9335
restart: always
volumes:
- /Users/joylau/log/admin:/home/liufa/app/admin/logs
environment:
activeProfile: prod
depends_on:
server:
condition: service_healthy
hostname: traffic-service-admin
links:
- server:traffic-service-eureka

service 模块 links server 模块,再起个别名 traffic-service-eureka ,因为我配置文件里配置的是 traffic-service-eureka,
这样 service 模块就可以通过 server 或者 traffic-service-eureka 来访问 server 了

另外,配置的 hostname,可以进入 容器中查看 /etc/hosts 该配置会在 文件中生成一个容器的 ip 和 hostname 的记录

多个服务加载顺序问题

详见 : http://blog.joylau.cn/2018/12/19/Docker-Compose-StartOrder/

  1. 查看路由表: netstat -nr

  2. 添加路由: sudo route add 34.0.7.0 34.0.7.1

  3. 删除路由: sudo route delete 0.0.0.0

  4. 清空路由表: networksetup -setadditionalroutes "Ethernet", “Ethernet” 指定路由走哪个设备(查看当前的设备可以使用这个命令 networksetup -listallnetworkservices

  5. 清空路由表: sudo route flush , 是否有效没测试过,通过 man route 看到的,等哪天试过了,再来更新这个内容是否有效

无线网卡和 USB 有线网卡同时使用

我这里的使用场景是无线接外网, USB 网卡接内网,无线路由的网关是 192.168.0.1, USB 网卡的网关是 34.0.7.1

  1. 删除默认路由: sudo route delete 0.0.0.0

  2. 添加默认路由走无线网卡: sudo route add 0.0.0.0 192.168.0.1

  3. 内网走 USB 网卡: sudo route add 34.0.7.0 34.0.7.1

  4. 调整网络顺序,网络属性里面的多个网卡的优先级顺序问题。基本原则是哪个网卡访问互联网,他的优先级就在上面就可以了

有个问题没搞明白, 按逻辑说这样添加的静态路由是临时的,在重启后会消失失效,可实际上我重启了之后并没有失效

配置永久静态路由

  1. networksetup mac 自带的工具,升级到最新的Sierra后拥有,是个“系统偏好设置”中网络设置工具的终端版

  2. networksetup –help 可以查看具体的帮助

  3. 添加静态永久路由: networksetup -setadditionalroutes "USB 10/100/1000 LAN" 10.188.12.0 255.255.255.0 192.168.8.254
    “USB 10/100/1000 LAN” 指定路由走哪个设备(查看当前的设备可以使用这个命令 networksetup -listallnetworkservices

  4. netstat -nr 查看路由表

背景

我们的程序在 Linux 上运行会产生大量日志文件,这些日志文件如果不定时清理的话会很快将磁盘占满

说明

1
2
3
4
5
6
7
8
9
10
# For details see man 4 crontabs

# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executed

配置

配置一个定时清理的任务

  1. crontab -e , 添加一个定时任务, 或者 vim /etc/crontab 添加一条记录
1
10 0 * * * /home/liufa/app/cron/del_log.sh > /dev/null 2>&1 &
1
10 0 * * * root sh /home/liufa/app/cron/del_log.sh > /dev/null 2>&1 &

每天 0 点 10 分运行上述命令文件

  1. 创建文件: del_log.sh

  2. 授权 chmod +x ./del_log.sh

  3. 删除 10 天的日志文件

1
2
#!/usr/bin/env bash
find /home/liufa/app/node/logs -mtime +10 -name "*.log" -exec rm -rf {} \;
  1. 重启定时任务, systemctl restart crond , 在 Ubuntu 上叫 cron systemctl restart cron

关于定时任务的配置目录

  1. /etc/crontab 文件, 系统级别的定时任务,需要加入用户名
  2. /var/spool/cron 目录, 以用户作为区分,一般会有一个和用户名相同的文件,里面记录了定时任务, 一般使用 crontab -e 创建, 语法中不需要指定用户名
  3. /etc/cron.d/ 和 crontab 文件类似,需要指定用户名

cron执行时,也就是要读取三个地方的配置文件

注意

  1. 执行脚本使用/bin/sh(防止脚本无执行权限),要执行的文件路径是从根开始的绝对路径(防止找不到文件)
  2. 尽量把要执行的命令放在脚本里,然后把脚本放在定时任务里。对于调用脚本的定时任务,可以把标准输出错误输出重定向到空。
  3. 定时任务中带%无法执行,需要加\转义
  4. 如果时上有值,分钟上必须有值
  5. 日和周不要同时使用,会冲突
  6. >>>/dev/null 2>&1 不要同时存在

日志位置

日志位置位于 /var/log/cron.log,如果没有看到日志,可能由于没有开启 cron 日志记录,开启方法:

vim /etc/rsyslog.d/50-default.conf

/var/log/cron.log相关行,将前面注释符#去掉

重启 rsyslog

service rsyslog restart

或者查看系统日志, 使用命令:

grep cron /var/log/syslog

能看到和 cron 相关的日志信息

任务脚本中变量不生效

在脚本里除了一些自动设置的全局变量,可能有些变量没有生效, 当手动执行脚本OK,但是crontab死活不执行时,在脚本里使用下面的方式

1)脚本中涉及文件路径时写全局路径;
2)脚本执行要用到java或其他环境变量时,通过source命令引入环境变量

1
2
3
#!/bin/sh
source /etc/profile

  1. */1 * * * * . /etc/profile;/bin/sh /path/run.sh

时区问题

构建镜像

时区的配置在 /etc/localtime

localtime 文件会指向 /usr/share/zoneinfo/Asia/ 目录下的某个文件

我们只需要将其指向 ShangHai 即可

Dockerfile 可以这样配置

1
2
RUN rm -rf /etc/localtime && \
ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

先删除,在创建一个软连接即可

如果是在容器内直接操作的话:

  1. apt-get install tzdata
  2. 然后依次选择 6 , 70 即可
  3. 使用 dpkg-reconfigure tzdata 来重写选择

已构建好的镜像

启动一个容器,加上如下参数,即可使用宿主机时间

1
-v /etc/localtime:/etc/localtime:ro

已经在运行的容器

1
docker cp /etc/localtime [container]:/etc/localtime

在检查下是否修改成功:

1
docker exec [container] date

springboot 应用的时区

如果构建的 springboot 项目的镜像,基于 jib 插件构建的话,并且基础镜像选择的是 openjdk:8
那么这时 jvmFlags 参数加上一个 -Duser.timezone=GMT+08 boot 服务启动时会使用东八区的时间
而且这不会改变容器的时区,如果你进入容器,执行 date 打印的话,还是会发现时间少 8 个小时
但是对于应用来说已经没有问题了

mariadb 容器的时区

默认官方的 mariadb 的镜像时区是 0 时区的,想要改变的话,添加执行参数 --default-time-zone='+8:00'
完整 docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
db:
image: mariadb
container_name: mariadb
restart: always
networks:
- job-net
ports:
- 3306:3306
volumes:
- /home/liufa/joy-job/db-data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=123456
- MYSQL_ROOT_HOST=%
command: --default-time-zone='+8:00'

pm2-web 命令错误问题

通常我们都是将 node_modules 文件夹直接复制到镜像中

有时候会出现问题,就比如 pm2-web ,构建成镜像后,命令无法使用

原因在于开发的机器的操作系统和镜像的操作系统不一致,会导致一些包出问题

解决的方式就是重新 nmp install

Dockerfile 如下:

1
2
3
RUN rm -rf ./node_modules && \
rm -rf ./package-lock.json && \
npm install

更改 docker数据目录

默认安装的 docker 数据目录在 /var/lib/docker
想要更改的话
在 docker 的 service 文件添加参数 --graph=/home/lifa
之后重启 docker 服务

查看 docker 使用的空间

有时我们下载镜像和运行的容器多了,会占用很多磁盘,该怎么查看呢?
du sh *
或者
du -m --max-depth=1

基于已有 docker 镜像制作自己的镜像

编写 Dockerfile 文件

这里就不细说了,了解 Dockerfile 文件里的命令即可

使用 docker commit提交容器

比如说 openjdk:8 ,我想让他支持 node , php , python 等该怎么办?
有个简单方法
先 pull 下来

docker run -it openjdk:8 /bin/bash
这样就进入到该镜像了,我们可以安装自己需要的东西了
我自己遇到了棘手的问题
就是 openjdk:8 是 debian 系统的,使用的是 apt-get 包管理器
要安装其他东西就要先更新源
可以使用 apt update 更新
但是我遇到的问题是更新了也无法安装….
于是我就想着切换到阿里或者科大的源
但是发现这个简单的镜像连 vi 和 gedit 等编辑器都没有,又没法安装他们
无法编辑 /etc/apt/source.list 源文件
后来我是 cp 先备份,在 echo "xxxx" >> /ect/apt/source.list 更新的
之后再 update 就可以安装了
安装好之后退出容器,之后
docker commit containID xxx/xxxx:latest
提交容器的改变,之后就看到一个新的镜像了

这里有个问题,就是我想定义自己的启动脚本,貌似无法做到,后来看了官方文档
docker commit 有个 -c 参数,解释是这样的:

commit Apply Dockerfile instruction to the created image

以为使用 Dockerfile 的语法来创建镜像, 还有 3 个参数

-a, –author string 制定个作者
-m, –message string 本次提交信息
-p, –pause 提交镜像时暂停容器,默认是 true

假如我有个 blog 的容器, 示例:

docker commit -c ‘CMD [“sh”, “/my-blog/bash/init.sh”]’ -c “EXPOSE 80” -c “EXPOSE 8080” -a “JoyLau” -m “JoyLau’s Blog Docker Image” blog nas.joylau.cn:5007/joy/blog.joylau.cn:2.1

docker 安装在内网服务器, 如何 pull 镜像

在命令行使用 export HTTP_PROXY=xxxx:xx , 命令行里绝大部分命令都可以使用此代理联网,但是安装的 docker 不行,无法 pull 下来镜像文件,想要 pull 使用代理的话,需要添加代理的变量
vim /usr/lib/systemd/system/docker.service
添加
Environment=HTTP_PROXY=http://xxxx:xxx
Environment=HTTPS_PROXY=http://xxxx:xxx
保存
systemctl deamon-reload
systemctl restart docker

背景

有时我们的服务器网络并不允许连接互联网,这时候 yum 安装软件就有很多麻烦事情了, 我们也许会通过 yumdownloader 来从可以连接互联网的机器上下载好 rpm 安装包,
然后再拷贝到 服务器上.
命令 : yumdownloader --resolve mariadb-server , 所有依赖下载到当前文件夹下

这样做会存在很多问题:

  1. 虽然上述命令已经加上了 --resolve 来解决依赖,但是一些基础的依赖包仍然没有下载到,这时安装就有问题了
  2. 下载的很多依赖包都有安装的先后顺序,包太多的话,根本无法搞清楚顺序

还可以使用 yum install --downloadonly --downloaddir=/tmp/vsftps/ vsftpd 来下载依赖和指定下载的位置
但是如果有一些基础依赖包已经安装过了,则不会下载, 这时可以使用 reinstall 来重新下载

yum reinstall --downloadonly --downloaddir=/tmp/vsftps/ vsftpd

rsync 同步科大的源

  1. yum install rsync
  2. df -h 查看磁盘上目录的存储的空间情况
  3. 找到最大的磁盘的空间目录,最好准备好 50 GB 以上的空间
  4. 新建目录如下:
1
2
3
4
mkdir -p ./yum_data/centos/7/os/x86_64
mkdir -p ./yum_data/centos/7/extras/x86_64
mkdir -p ./yum_data/centos/7/updates/x86_64
mkdir -p ./yum_data/centos/7/epel/x86_64
  1. 开始同步 base extras updates epel 源
1
2
3
4
5
cd yum_data
rsync -avh --progress rsync://rsync.mirrors.ustc.edu.cn/repo/centos/7/os/x86_64/ ./centos/7/os/x86_64/
rsync -avh --progress rsync://rsync.mirrors.ustc.edu.cn/repo/centos/7/extras/x86_64/ ./centos/7/extras/x86_64/
rsync -avh --progress rsync://rsync.mirrors.ustc.edu.cn/repo/centos/7/updates/x86_64/ ./centos/7/updates/x86_64/
rsync -avh --progress rsync://rsync.mirrors.ustc.edu.cn/repo/epel/7/x86_64/ ./epel/7/x86_64/
  1. 开始漫长的等待……
  2. 等待全部同步完毕, tar -czf yum_data.tar.gz ./yum_data ,压缩目录
  3. 压缩包拷贝到服务器上

rsync 增量同步

使用参数 -u, 即

1
rsync -avuh --progress rsync://rsync.mirrors.ustc.edu.cn/repo/centos/7/extras/x86_64/ ./centos/7/extras/x86_64/

rsync 使用及配置解释

6 种用法

  • rsync [OPTION]… SRC DEST
  • rsync[OPTION]… SRC [USER@]HOST:DEST
  • rsync [OPTION]… [USER@]HOST:SRC DEST
  • rsync [OPTION]… [USER@]HOST::SRC DEST
  • rsync [OPTION]… SRC [USER@]HOST::DEST
  • rsync [OPTION]… rsync://[USER@]HOST[:PORT]/SRC [DEST]

1)拷贝本地文件。当SRC和DES路径信息都不包含有单个冒号”:”分隔符时就启动这种工作模式。如:rsync -a /data /backup
2)使用一个远程shell程序(如rsh、ssh)来实现将本地机器的内容拷贝到远程机器。当DST路径地址包含单个冒号”:”分隔符时启动该模式。如:rsync -avz *.c foo:src
3)使用一个远程shell程序(如rsh、ssh)来实现将远程机器的内容拷贝到本地机器。当SRC地址路径包含单个冒号”:”分隔符时启动该模式。如:rsync -avz foo:src/bar /data
4)从远程rsync服务器中拷贝文件到本地机。当SRC路径信息包含”::”分隔符时启动该模式。如:rsync -av root@172.16.78.192::www /databack
5)从本地机器拷贝文件到远程rsync服务器中。当DST路径信息包含”::”分隔符时启动该模式。如:rsync -av /databack root@172.16.78.192::www
6)列远程机的文件列表。这类似于rsync传输,不过只要在命令中省略掉本地机信息即可。如:rsync -v rsync://172.16.78.192/www

参数解释

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
-v, --verbose 详细模式输出
-q, --quiet 精简输出模式
-c, --checksum 打开校验开关,强制对文件传输进行校验
-a, --archive 归档模式,表示以递归方式传输文件,并保持所有文件属性,等于-rlptgoD
-r, --recursive 对子目录以递归模式处理
-R, --relative 使用相对路径信息
-b, --backup 创建备份,也就是对于目的已经存在有同样的文件名时,将老的文件重新命名为~filename。可以使用--suffix选项来指定不同的备份文件前缀。
--backup-dir 将备份文件(如~filename)存放在在目录下。
-suffix=SUFFIX 定义备份文件前缀
-u, --update 仅仅进行更新,也就是跳过所有已经存在于DST,并且文件时间晚于要备份的文件。(不覆盖更新的文件)
-l, --links 保留软链结
-L, --copy-links 想对待常规文件一样处理软链结
--copy-unsafe-links 仅仅拷贝指向SRC路径目录树以外的链结
--safe-links 忽略指向SRC路径目录树以外的链结
-H, --hard-links 保留硬链结
-p, --perms 保持文件权限
-o, --owner 保持文件属主信息
-g, --group 保持文件属组信息
-D, --devices 保持设备文件信息
-t, --times 保持文件时间信息
-S, --sparse 对稀疏文件进行特殊处理以节省DST的空间
-n, --dry-run现实哪些文件将被传输
-W, --whole-file 拷贝文件,不进行增量检测
-x, --one-file-system 不要跨越文件系统边界
-B, --block-size=SIZE 检验算法使用的块尺寸,默认是700字节
-e, --rsh=COMMAND 指定使用rsh、ssh方式进行数据同步
--rsync-path=PATH 指定远程服务器上的rsync命令所在路径信息
-C, --cvs-exclude 使用和CVS一样的方法自动忽略文件,用来排除那些不希望传输的文件
--existing 仅仅更新那些已经存在于DST的文件,而不备份那些新创建的文件
--delete 删除那些DST中SRC没有的文件
--delete-excluded 同样删除接收端那些被该选项指定排除的文件
--delete-after 传输结束以后再删除
--ignore-errors 及时出现IO错误也进行删除
--max-delete=NUM 最多删除NUM个文件
--partial 保留那些因故没有完全传输的文件,以是加快随后的再次传输
--force 强制删除目录,即使不为空
--numeric-ids 不将数字的用户和组ID匹配为用户名和组名
--timeout=TIME IP超时时间,单位为秒
-I, --ignore-times 不跳过那些有同样的时间和长度的文件
--size-only 当决定是否要备份文件时,仅仅察看文件大小而不考虑文件时间
--modify-window=NUM 决定文件是否时间相同时使用的时间戳窗口,默认为0
-T --temp-dir=DIR 在DIR中创建临时文件
--compare-dest=DIR 同样比较DIR中的文件来决定是否需要备份
-P 等同于 --partial
--progress 显示备份过程
-z, --compress 对备份的文件在传输时进行压缩处理
--exclude=PATTERN 指定排除不需要传输的文件模式
--include=PATTERN 指定不排除而需要传输的文件模式
--exclude-from=FILE 排除FILE中指定模式的文件
--include-from=FILE 不排除FILE指定模式匹配的文件
--version 打印版本信息
--address 绑定到特定的地址
--config=FILE 指定其他的配置文件,不使用默认的rsyncd.conf文件
--port=PORT 指定其他的rsync服务端口
--blocking-io 对远程shell使用阻塞IO
-stats 给出某些文件的传输状态
--progress 在传输时现实传输过程
--log-format=formAT 指定日志文件格式
--password-file=FILE 从FILE中得到密码
--bwlimit=KBPS 限制I/O带宽,KBytes per second
-h, --help 显示帮助信息

配置本地 yum 源

  1. 找到一个空间大的目录下,解压包: tar -xvf yum_data.tar.gz
  2. 创建一个新的源配置: touch /etc/yum.repos.d/private.repo
  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
[local-base]
name=Base Server Repository
baseurl=file:///home/liufa/yum_data/centos/7/os/x86_64
enabled=1
gpgcheck=0
priority=1
[local-extras]
name=Extras Repository
baseurl=file:///home/liufa/yum_data/centos/7/extras/x86_64
enabled=1
gpgcheck=0
priority=2
[local-updates]
name=Updates Server Repository
baseurl=file:///home/liufa/yum_data/centos/7/updates/x86_64
enabled=1
gpgcheck=0
priority=3
[local-epel]
name=Epel Server Repository
baseurl=file:///home/liufa/yum_data/centos/7/epel/x86_64
enabled=1
gpgcheck=0
priority=4
  1. 禁用原来的 Base Extras Updates 源: yum-config-manager --disable Base,Extras,Updates
  2. yum clean all
  3. yum makecache
  4. yum repolist 查看源信息

配置网络 yum 源

有时候我们搭建的私有 yum 还需要提供给其他的机器使用,这时候再做一个网络的 yum 即可,用 Apache 或者 Nginx 搭建个服务即可

  1. yum install nginx
  2. vim /etc/nginx/nginx.conf 修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /home/liufa/yum_data;

# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;

location / {
}

error_page 404 /404.html;
location = /40x.html {
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
  1. 这时 private.repo 里的 baseurl 全改为网络地址即可

403 权限问题

修改 nginx.conf 配置文件的 user 为 root

背景

想着写个 demo, 用个简单的 jq 的 post 请求传递数组,却发现遇到了不少问题…
一顿研究,总结如下:

$.post()

语法:
$.post(url,data,success(data, textStatus, jqXHR),dataType)

url 必需。规定把请求发送到哪个 URL。
data 可选。映射或字符串值。规定连同请求发送到服务器的数据。
success(data, textStatus, jqXHR) 可选。请求成功时执行的回调函数。
dataType 可选。规定预期的服务器响应的数据类型。默认执行智能判断(xml、json、script 或 html)

该语法等价于:

1
2
3
4
5
6
7
8
9
10
$.ajax({
type: 'POST',
url: url,
data: {
pageNumber: 10
},
success: function (res, status) {
console.info(res)
},
});

总结需要注意的是:

  • 请求的 Content-Type 是 application/x-www-form-urlencoded; charset=UTF-8 就是表单提交的, dataType 指得是规定服务器的响应方式
  • 第二个参数 data 的类型是键值对的对象,不能为 JSON.stringify 后的 json 字符串
  • 传数组会有问题,会将数组中每个对象的拆开然后堆到一起作为键值对传输数据, 可以通过 jQuery.ajaxSettings.traditional = true; 在 post 请求之前设置,防止这样的情况发生,但是对象不会被序列化,会变成 Object 这样的格式,这也不是我们想要的结果
  • traditional 可在 $.ajax 的配置项里显式声明

注意:

Content-Type 是 application/x-www-form-urlencoded; charset=UTF-8 传参时
Spirng Boot 后台的接受的参数的形式可以为对象, 不需要加注解
该对象的属性需要包含 data 里的值, 否则的话,接受到的对象里的属性为空

$.ajax()

很传统的使用方式了:
发送 post 请求
我们的 points 的是数组,里面是多个对象
数据传输使用 Request Payload 方式

1
2
3
4
5
6
7
8
9
10
11
$.ajax({
type: 'POST',
url: location.origin + "/trafficService/pixelToLngLat",
data: JSON.stringify(points),
contentType: "application/json; charset=UTF-8",
success: function (res, status) {
res.map(point => {
console.info(point)
});
},
});

后台使用方式:

1
2
3
4
@PostMapping("/pixelToLngLat")
public JSONArray pixelToLngLat(@RequestBody JSONArray points){
return restTemplate.postForObject(baiduApi.getNodeService() + "/traffic/pixelToLngLat",points,JSONArray.class);
}

很传统的使用方式

这种请求的 Content-Type 是 application/json; charset=UTF-8, 后台必须用 @RequestBody 注解接受参数
这种方式容错性比较高, @RequestBody 注解的对象可以是 HashMap, JSONObject, JSONArray, String, Object 都可以获取到数据
不像 application/x-www-form-urlencoded, 需要对象和传的值一一对应才能获取到值

强行使用 $.post()

这个时候我们参数还是传输的键值对方式,只不过将值转化为 json 字符串进行传输

1
2
3
4
5
6
jQuery.ajaxSettings.traditional = true;
$.post(location.origin + "/trafficService/pixelToLngLat", {points:JSON.stringify(points)},function (res, status) {
res.map(point => {
console.info(point)
});
},"json")

后台使用

1
2
3
4
5
@PostMapping("/pixelToLngLat")
public JSONArray pixelToLngLat(@RequestParam("points") String points){
JSONArray array = JSONArray.parseArray(points);
return restTemplate.postForObject(baiduApi.getNodeService() + "/traffic/pixelToLngLat",array,JSONArray.class);
}

@RequestParam(“points”) 显式指定了接受 points 的值, 即字符串 JSON.stringify(points)
其实完全可以在后台定义一个 Points 对象, 属性和前台的传的数据属性一一对应, 用来接受前台传过来的数据

总结

  1. 表单提交方式,如果后台有相应的对象的来接受参数的话,直接在方法是使用对象即可,这种方式需要事先定义一个对象, 前台传的数据就按照这个对象的属性来, 这种方式看起来数据很清晰
  2. application/json 需要注解 @RequestBody, 注解的对象可以是 HashMap, JSONObject, JSONArray, String, Object 都可以获取到数据, 灵活性高

spring 的线程池 ThreadPoolTaskExecutor

spring 为我们实现了一个基于 ThreadPoolExecutor 线程池

使用

  1. yml
1
2
3
4
5
6
7
traffic:
executor:
name: "trafficServiceExecutor"
core-pool-size: 5
max-pool-size: 10
queue-capacity: 20
thread-name-prefix: "traffic-service-"
  1. Configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
@Configuration
@ConfigurationProperties(prefix = "traffic.executor")
public class Executor {
private String name;

private Integer corePoolSize;

private Integer maxPoolSize;

private Integer queueCapacity;

private String threadNamePrefix;
}
  1. Bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@ConditionalOnBean(Executor.class)
public class ExecutorConfig {
@Bean
public ThreadPoolTaskExecutor trafficServiceExecutor(@Autowired Executor executor) {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(executor.getCorePoolSize());
threadPoolTaskExecutor.setMaxPoolSize(executor.getMaxPoolSize());
threadPoolTaskExecutor.setQueueCapacity(executor.getQueueCapacity());
threadPoolTaskExecutor.setThreadNamePrefix(executor.getThreadNamePrefix());
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
}

仅此步骤,我们在使用的时候,只需要注解 @Async(“trafficServiceExecutor”) 配置好 name 即可

个人理解

理解图

看点数据

在线程池整个运作过程中,想看下运行状态的话可以这么做:
常用状态:

  • taskCount:线程需要执行的任务个数。
  • completedTaskCount:线程池在运行过程中已完成的任务数。
  • largestPoolSize:线程池曾经创建过的最大线程数量。
  • getPoolSize: 获取当前线程池的线程数量。
  • getActiveCount:获取活动的线程的数量

通过继承线程池,重写beforeExecute,afterExecute和terminated方法来在线程执行任务前,线程执行任务结束,和线程终结前获取线程的运行情况,根据具体情况调整线程池的线程数量

重写一波

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
@Slf4j
public class MyExecutor extends ExecutorConfigurationSupport
implements AsyncListenableTaskExecutor, SchedulingTaskExecutor {

private final Object poolSizeMonitor = new Object();

private int corePoolSize = 1;

private int maxPoolSize = Integer.MAX_VALUE;

private int keepAliveSeconds = 60;

private int queueCapacity = Integer.MAX_VALUE;

private boolean allowCoreThreadTimeOut = false;

@Nullable
private TaskDecorator taskDecorator;

@Nullable
private ThreadPoolExecutor threadPoolExecutor;

// Runnable decorator to user-level FutureTask, if different
private final Map<Runnable, Object> decoratedTaskMap =
new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK);


public void setCorePoolSize(int corePoolSize) {
synchronized (this.poolSizeMonitor) {
this.corePoolSize = corePoolSize;
if (this.threadPoolExecutor != null) {
this.threadPoolExecutor.setCorePoolSize(corePoolSize);
}
}
}

public int getCorePoolSize() {
synchronized (this.poolSizeMonitor) {
return this.corePoolSize;
}
}

public void setMaxPoolSize(int maxPoolSize) {
synchronized (this.poolSizeMonitor) {
this.maxPoolSize = maxPoolSize;
if (this.threadPoolExecutor != null) {
this.threadPoolExecutor.setMaximumPoolSize(maxPoolSize);
}
}
}

public int getMaxPoolSize() {
synchronized (this.poolSizeMonitor) {
return this.maxPoolSize;
}
}

public void setKeepAliveSeconds(int keepAliveSeconds) {
synchronized (this.poolSizeMonitor) {
this.keepAliveSeconds = keepAliveSeconds;
if (this.threadPoolExecutor != null) {
this.threadPoolExecutor.setKeepAliveTime(keepAliveSeconds, TimeUnit.SECONDS);
}
}
}

public int getKeepAliveSeconds() {
synchronized (this.poolSizeMonitor) {
return this.keepAliveSeconds;
}
}

public void setQueueCapacity(int queueCapacity) {
this.queueCapacity = queueCapacity;
}

public void setAllowCoreThreadTimeOut(boolean allowCoreThreadTimeOut) {
this.allowCoreThreadTimeOut = allowCoreThreadTimeOut;
}

public void setTaskDecorator(TaskDecorator taskDecorator) {
this.taskDecorator = taskDecorator;
}


@Override
protected ExecutorService initializeExecutor(
ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {

BlockingQueue<Runnable> queue = createQueue(this.queueCapacity);

ThreadPoolExecutor executor;
if (this.taskDecorator != null) {
executor = new ThreadPoolExecutor(
this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS,
queue, threadFactory, rejectedExecutionHandler) {
@Override
public void execute(Runnable command) {
Runnable decorated = taskDecorator.decorate(command);
if (decorated != command) {
decoratedTaskMap.put(decorated, command);
}
super.execute(decorated);
}

};
}
else {
executor = new ThreadPoolExecutor(
this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS,
queue, threadFactory, rejectedExecutionHandler){
@Override
public void beforeExecute(Thread t, Runnable r) {
// log.error("线程开始......");
// log.error("当前线程池的线程数量:{}",MyExecutor.this.getPoolSize());
// log.error("活动的线程的数量:{}",MyExecutor.this.getActiveCount());
// log.error("线程需要执行的任务个数:{}",getTaskCount());
// log.error("线程池在运行过程中已完成的任务数:{}",getCompletedTaskCount());
}
@Override
public void afterExecute(Runnable r, Throwable t) {
log.error("线程池在运行过程中已完成的任务数:{}",getCompletedTaskCount());
}
};

}

if (this.allowCoreThreadTimeOut) {
executor.allowCoreThreadTimeOut(true);
}

this.threadPoolExecutor = executor;
return executor;
}

protected BlockingQueue<Runnable> createQueue(int queueCapacity) {
if (queueCapacity > 0) {
return new LinkedBlockingQueue<>(queueCapacity);
}
else {
return new SynchronousQueue<>();
}
}

public ThreadPoolExecutor getThreadPoolExecutor() throws IllegalStateException {
Assert.state(this.threadPoolExecutor != null, "ThreadPoolTaskExecutor not initialized");
return this.threadPoolExecutor;
}

public int getPoolSize() {
if (this.threadPoolExecutor == null) {
// Not initialized yet: assume core pool size.
return this.corePoolSize;
}
return this.threadPoolExecutor.getPoolSize();
}

public int getActiveCount() {
if (this.threadPoolExecutor == null) {
// Not initialized yet: assume no active threads.
return 0;
}
return this.threadPoolExecutor.getActiveCount();
}


@Override
public void execute(Runnable task) {
Executor executor = getThreadPoolExecutor();
try {
executor.execute(task);
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex);
}
}

@Override
public void execute(Runnable task, long startTimeout) {
execute(task);
}

@Override
public Future<?> submit(Runnable task) {
ExecutorService executor = getThreadPoolExecutor();
try {
return executor.submit(task);
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex);
}
}

@Override
public <T> Future<T> submit(Callable<T> task) {
ExecutorService executor = getThreadPoolExecutor();
try {
return executor.submit(task);
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex);
}
}

@Override
public ListenableFuture<?> submitListenable(Runnable task) {
ExecutorService executor = getThreadPoolExecutor();
try {
ListenableFutureTask<Object> future = new ListenableFutureTask<>(task, null);
executor.execute(future);
return future;
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex);
}
}

@Override
public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
ExecutorService executor = getThreadPoolExecutor();
try {
ListenableFutureTask<T> future = new ListenableFutureTask<>(task);
executor.execute(future);
return future;
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex);
}
}

@Override
protected void cancelRemainingTask(Runnable task) {
super.cancelRemainingTask(task);
// Cancel associated user-level Future handle as well
Object original = this.decoratedTaskMap.get(task);
if (original instanceof Future) {
((Future<?>) original).cancel(true);
}
}
}

主要看 initializeExecutor 方法,我重写了 ThreadPoolExecutorbeforeExecuteafterExecute 打印了一些信息,可以帮助理解整个过程

配置参考

  • 如果是CPU密集型任务,那么线程池的线程个数应该尽量少一些,一般为CPU的个数+1条线程。 linux 查看 CPU 信息 : cat /proc/cpuinfo
  • 如果是IO密集型任务,那么线程池的线程可以放的很大,如2*CPU的个数。
  • 对于混合型任务,如果可以拆分的话,通过拆分成CPU密集型和IO密集型两种来提高执行效率;如果不能拆分的的话就可以根据实际情况来调整线程池中线程的个数。

背景

出差在外或者在家工作都需要连接公司网络,没有 VPN 怎么能行

OpenVPN 服务端部署

  1. 全局变量配置: OVPN_DATA=”/home/joylau/ovpn-data”
  2. mkdir ${OVPN_DATA} , cd ${OVPN_DATA}
  3. 这里我使用的是 tcp, udp 的好像没映射, 我用起来有问题,后来换的 tcp 方式, docker run -v ${OVPN_DATA}:/etc/openvpn --rm kylemanna/openvpn ovpn_genconfig -u tcp://公网 IP
  4. 初始化,这里的密码我们都设置为 123456, docker run -v ${OVPN_DATA}:/etc/openvpn --rm -it kylemanna/openvpn ovpn_initpki
  5. 创建用户 liufa , 不使用密码的话在最后面加上 nopass, 使用密码就键入密码,这里我们使用 123456, docker run -v ${OVPN_DATA}:/etc/openvpn --rm -it kylemanna/openvpn easyrsa build-client-full liufa
  6. 为用户 liufa 生成秘钥, docker run -v ${OVPN_DATA}:/etc/openvpn --rm kylemanna/openvpn ovpn_getclient liufa > ${OVPN_DATA}/liufa.ovpn
  7. 创建的文件中端口默认使用的是 1194, 而我用的是 6001,那我们还得修改下 liufa.ovpn 文件的端口
  8. 运行容器,这里我的宿主机端口为 6001, docker run --name openvpn -v ${OVPN_DATA}:/etc/openvpn -d -p 6001:1194 --privileged kylemanna/openvpn

OpenVPN 管理接口

服务端配置文件加入 management 0.0.0.0 5555

可以使用 telnet ip 5555 来使用管理接口, 输入 help 查看详细命令, 具体如下:

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
Commands:
auth-retry t : Auth failure retry mode (none,interact,nointeract).
bytecount n : Show bytes in/out, update every n secs (0=off).
echo [on|off] [N|all] : Like log, but only show messages in echo buffer.
exit|quit : Close management session.
forget-passwords : Forget passwords entered so far.
help : Print this message.
hold [on|off|release] : Set/show hold flag to on/off state, or
release current hold and start tunnel.
kill cn : Kill the client instance(s) having common name cn.
kill IP:port : Kill the client instance connecting from IP:port.
load-stats : Show global server load stats.
log [on|off] [N|all] : Turn on/off realtime log display
+ show last N lines or 'all' for entire history.
mute [n] : Set log mute level to n, or show level if n is absent.
needok type action : Enter confirmation for NEED-OK request of 'type',
where action = 'ok' or 'cancel'.
needstr type action : Enter confirmation for NEED-STR request of 'type',
where action is reply string.
net : (Windows only) Show network info and routing table.
password type p : Enter password p for a queried OpenVPN password.
remote type [host port] : Override remote directive, type=ACCEPT|MOD|SKIP.
proxy type [host port flags] : Enter dynamic proxy server info.
pid : Show process ID of the current OpenVPN process.
client-auth CID KID : Authenticate client-id/key-id CID/KID (MULTILINE)
client-auth-nt CID KID : Authenticate client-id/key-id CID/KID
client-deny CID KID R [CR] : Deny auth client-id/key-id CID/KID with log reason
text R and optional client reason text CR
client-kill CID [M] : Kill client instance CID with message M (def=RESTART)
env-filter [level] : Set env-var filter level
client-pf CID : Define packet filter for client CID (MULTILINE)
rsa-sig : Enter an RSA signature in response to >RSA_SIGN challenge
Enter signature base64 on subsequent lines followed by END
certificate : Enter a client certificate in response to >NEED-CERT challenge
Enter certificate base64 on subsequent lines followed by END
signal s : Send signal s to daemon,
s = SIGHUP|SIGTERM|SIGUSR1|SIGUSR2.
state [on|off] [N|all] : Like log, but show state history.
status [n] : Show current daemon status info using format #n.
test n : Produce n lines of output for testing/debugging.
username type u : Enter username u for a queried OpenVPN username.
verb [n] : Set log verbosity level to n, or show if n is absent.
version : Show current version number.
END


OpenVPN 客户端使用说明

Windows

  1. 安装 openVPN windows 客户端,地址:https://swupdate.openvpn.org/community/releases/openvpn-install-2.4.6-I602.exe , 该地址需要梯子
  2. 启动客户端,右键,选择 import file, 导入 ovpn 文件,文件请 联系管理员发给你
  3. 右键 connect,如果弹出框提示输入密码,输入默认密码 123456 ,等待连接成功即可

Linux

  1. 安装 openvpn:sudo yum install openvpn 或者 sudo apt install openvpn
  2. 找到 ovpn 文件所在目录: sudo openvpn --config ./liufa.ovpn, 看到成功信息时即连接成功
  3. --daemon 参数以守护进程运行
  4. 或者写个 service 文件以守护进程并且开机启动运行

GUI 客户端 [2020-04-29更新]

可以使用开源客户端工具: pritunl-client-electron
安装方法:
Ubuntu 16.04:

1
2
3
4
5
6
7
sudo tee /etc/apt/sources.list.d/pritunl.list << EOF
deb http://repo.pritunl.com/stable/apt xenial main
EOF

sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com --recv 7568D9BB55FF9E5287D586017AE645C0CF8E292A
sudo apt-get update
sudo apt-get install pritunl-client-electron

注意 apt 安装需要科学上网来设置代理

或者从 GitHub 上下载软件包: https://github.com/pritunl/pritunl-client-electron/releases

MacOS

  1. 安装 Tunnelblick,地址:https://tunnelblick.net/
  2. 导入 ovpn文件
  3. 状态栏上点击连接VPN

路由设置

连接上 VPN 后,默认所有流量都走的 VPN,但事实上我们并不想这么做

Windows 上路由手动配置

  • ⽐如公司内网的网段为 192.168.10.0 网段,我们先删除 2 个 0.0.0.0 的路由: route delete 0.0.0.0
  • 然后添加 0.0.0.0 到本机的网段 route add 0.0.0.0 mask 255.255.255.0 本机内网网关
  • 再指定 10 网段走 VPN 通道 route add 192.168.10.0 mask 255.255.255.0 VPN 网关
  • 以上路由添加默认是临时的,重启失效,⽤久保存可加 -p 参数

OpenVPN 服务端配置

修改配置文件 openvpn.conf

1
2
### Route Configurations Below
route 192.168.254.0 255.255.255.0

下面添加路由即可, 客户端连接时会收到服务端推送的路由

OpenVPN 客户端设置

很多时候我们希望自己的客户端能够自定义路由,而且更该服务端的配置并不是一个相对较好的做法

找到我们的 ovpn 配置文件

到最后一行

redirect-gateway def1
即是我们全部流量走 VPN 的配置

route-nopull

客户端加入这个参数后,OpenVPN 连接后不会添加路由,也就是不会有任何网络请求走 OpenVPN

vpn_gateway

当客户端加入 route-nopull 后,所有出去的访问都不从 OpenVPN 出去,但可通过添加 vpn_gateway 参数使部分IP访问走 OpenVPN 出去

1
2
route 192.168.255.0 255.255.255.0 vpn_gateway
route 192.168.10.0 255.255.255.0 vpn_gateway

net_gateway

vpn_gateway 相反,他表示在默认出去的访问全部走 OpenVPN 时,强行指定部分 IP 访问不通过 OpenVPN 出去

1
2
max-routes 1000 # 表示可以添加路由的条数,默认只允许添加100条路由,如果少于100条路由可不加这个参数
route 172.121.0.0 255.255.0.0 net_gateway

客户端互相访问

  1. 配置 client-to-client
  2. 192.168.255.0 的路由要能够走VPN通道, 可以配置 redirect-gateway def1 或者 route-nopull route 192.168.255.0 255.255.255.0 vpn_gateway

OpenVPN 服务端配置静态 ip

  1. 配置文件配置 ifconfig-pool-persist /etc/openvpn/ipp.txt 0

ipp.txt 文件的格式

1
2
3
user1,192.168.255.10
user2,192.168.255.11
user3,192.168.255.12

经自己测试, 该方式配置静态 IP 没生效,实际得到的 IP 会大 2 位

  1. 配置文件配置 client-config-dir ccd

然后在 ccd 目录下以用户名为文件名命名,写入内容: ifconfig-push 192.168.255.10 255.255.255.0 来为单个用户配置 IP

0%