JoyLau's Blog

JoyLau 的技术学习与思考

时区问题

构建镜像

时区的配置在 /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

  1. docker pull registry

  2. docker run -itd -v /data/registry:/var/lib/registry -p 5000:5000 –restart=always –privileged=true –name registry registry:latest
    参数说明
    -itd:在容器中打开一个伪终端进行交互操作,并在后台运行;
    -v:把宿主机的/data/registry目录绑定 到 容器/var/lib/registry目录(这个目录是registry容器中存放镜像文件的目录),来实现数据的持久化;
    -p:映射端口;访问宿主机的5000端口就访问到registry容器的服务了;
    –restart=always:这是重启的策略,假如这个容器异常退出会自动重启容器;
    –privileged=true 在CentOS7中的安全模块selinux把权限禁掉了,参数给容器加特权,不加上传镜像会报权限错误OSError: [Errno 13] Permission denied: ‘/tmp/registry/repositories/liibrary’)或者(Received unexpected HTTP status: 500 Internal Server Error)错误
    –name registry:创建容器命名为registry,你可以随便命名;
    registry:latest:这个是刚才pull下来的镜像;

  3. 测试是否成功: curl http://127.0.0.1:5000/v2/_catalog, 返回仓库的镜像列表

  4. 在中央仓库下载一个镜像: docker pull openjdk

  5. 更改这个镜像的标签: docker tag imageId domain:5000/openjdk 或者 docker tag imageName:tag domain:5000/openjdk

  6. 上传镜像到私服: docker push domain:5000/openjdk

报错: Get https://172.18.18.90:5000/v2/: http: server gave HTTP response to HTTPS client

解决: 需要https的方法才能上传,我们可以修改下daemon.json
vim /etc/docker/daemon.json
{
“insecure-registries”: [ “domain:5000”]
}

无网络搭建

  1. 在有网络的机器上 docker pull registry
  2. docker save registry > registry.tar 保存到个 tar 包
  3. 拷贝到服务器上, docker load -i registry.tar 导入镜像
  4. docker images 查看镜像
  5. 再继续上面的操作

docker 开启 tcp 端口

  • vim /usr/lib/systemd/system/docker.service

修改

1
ExecStart=/usr/bin/dockerd-current -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock \

重启即可,之后 idea 可输入 tcp://ip:2375 连接

允许跨域请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
version: 0.1
log:
fields:
service: registry
storage:
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
Access-Control-Allow-Headers: ['Origin,Accept,Content-Type,Authorization']
Access-Control-Allow-Origin: ['*']
Access-Control-Allow-Methods: ['GET,POST,PUT,DELETE']
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3

head 添加

1
2
3
Access-Control-Allow-Headers: ['Origin,Accept,Content-Type,Authorization']
Access-Control-Allow-Origin: ['*']
Access-Control-Allow-Methods: ['GET,POST,PUT,DELETE']

之后保存到本地,再挂载到容器的 /etc/docker/registry/config.yml 中

Harbor 搭建 Docker 私服

上述方式搭建的 docker 私服,属于比较简单使用的方法,只能在命令行上操作,很不方便,比如不能直接删除镜像,无法添加用户,设置私有仓库
Harbor 是一个图形化的私服管理界面,安装使用更易于操作

Harbor是一个用于存储和分发Docker镜像的企业级Registry服务器,通过添加一些企业必需的功能特性,例如安全、标识和管理等,扩展了开源Docker Distribution。作为一个企业级私有Registry服务器,Harbor提供了更好的性能和安全。提升用户使用Registry构建和运行环境传输镜像的效率。

  1. 下载离线包: https://github.com/goharbor/harbor/releases
  2. 解压
  3. 更改配置文件 docker-compose.yml 私服的仓库端口我们默认设置为 5000,但是 docker-compose.yml 文件中并没有配置,我们需要添加一个 ports 配置
1
2
3
4
5
registry:
networks:
- harbor
ports:
- 5000:5000
  1. Harbor 默认使用的是 80 端口,不想使用的话可切换其他端口, 配置在 docker-compose.yml 的最下方
1
2
3
4
5
6
proxy:
image: goharbor/nginx-photon:v1.7.0
ports:
- 9339:80
- 443:443
- 4443:4443

此处需要注意的是,如果更改了其他端口,则需要在 common/templates/registry/config.yml 文件中更改一个配置 realm 加上端口,否则登录会出现错误

1
2
3
4
5
6
auth:
token:
issuer: harbor-token-issuer
realm: $public_url:9339/service/token
rootcertbundle: /etc/registry/root.crt
service: harbor-registry
  1. 修改配置文件 harbor.cfg
1
2
3
hostname = 34.0.7.183 ## 改为 IP 或者 域名,不要写错 localhost 或者 127.0.0.1
ui_url_protocol = http ## http 方式
harbor_admin_password = Hardor12345 ## admin 账号的默认登录密码
  1. ./prepare 完成配置

  2. ./install.sh 开始安装

  3. 打开浏览器

  4. 创建一个项目 joylau 注意这个名称很重要,名称对不上的话,会造成 image push 不成功,还有就是若果这个项目的是公开的话,则所有人都可以 pull ,但是 push 的话是需要登录的,登录的用户名和密码在该项目的成员下.默认的 admin 用户就可以

  5. 登录,退出命令 docker login 34.0.7.183:5000 ; docker logout 34.0.7.183:5000

  6. 之后的操作都是日常操作了

Docker Registry 添加认证

生成用户名密码

1
docker run --rm --entrypoint htpasswd registry -Bbn username password > ./htpasswd

假设将生成的文件放到 /registry/pwd/htpasswd

挂载用户名密码文件

-v /registry/pwd:/auth -e “REGISTRY_AUTH=htpasswd” -e “REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm” -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd

至此,添加用户名密码完成,现在 pull 和 push 都需要登录

Docker login 密码存储

默认存储位置为: $HOME/.docker/config.json
很不安全, 使用 base64 解密即可看到用户名密码

将密码存储到钥匙串:

  1. 下载工具: https://github.com/docker/docker-credential-helpers/releases
  2. 将 docker-credential-osxkeychain 配置到 path 路径
  3. 配置 config.json
1
2
3
{
"credsStore": "osxkeychain"
}

说明

ownCloud 除了传统的部署方式,在如今 docker 大行其道的环境下,使用 docker 部署 ownCloud 才是最方便的

第一种 owncloud 镜像直接安装

直接部署 owncloud 镜像,该镜像地址: https://hub.docker.com/r/_/owncloud/

1
2
docker pull owncloud
docker run -d -p 80:80 owncloud

这种方式的需要你提前装好 MariaDb 数据库,在启动完成后打开页面会按照流程填写数据库的链接信息,之后就可以使用 ownCloud 了

第二种 分别安装

分别先后使用 docker 按照 redis,mariadb,ownCloud
安装 redis 的 mariadb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
docker volume create owncloud_redis

docker run -d \
--name redis \
-e REDIS_DATABASES=1 \
--volume owncloud_redis:/var/lib/redis \
webhippie/redis:latest

docker volume create owncloud_mysql
docker volume create owncloud_backup

docker run -d \
--name mariadb \
-e MARIADB_ROOT_PASSWORD=owncloud \
-e MARIADB_USERNAME=owncloud \
-e MARIADB_PASSWORD=owncloud \
-e MARIADB_DATABASE=owncloud \
--volume owncloud_mysql:/var/lib/mysql \
--volume owncloud_backup:/var/lib/backup \
webhippie/mariadb:latest

接着我们配置一些 ownCloud web 服务的环境变量,并在启动容器时使用这些变量

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
export OWNCLOUD_VERSION=10.0
export OWNCLOUD_DOMAIN=localhost
export ADMIN_USERNAME=admin
export ADMIN_PASSWORD=admin
export HTTP_PORT=80

docker volume create owncloud_files

docker run -d \
--name owncloud \
--link mariadb:db \
--link redis:redis \
-p ${HTTP_PORT}:8080 \
-e OWNCLOUD_DOMAIN=${OWNCLOUD_DOMAIN} \
-e OWNCLOUD_DB_TYPE=mysql \
-e OWNCLOUD_DB_NAME=owncloud \
-e OWNCLOUD_DB_USERNAME=owncloud \
-e OWNCLOUD_DB_PASSWORD=owncloud \
-e OWNCLOUD_DB_HOST=db \
-e OWNCLOUD_ADMIN_USERNAME=${ADMIN_USERNAME} \
-e OWNCLOUD_ADMIN_PASSWORD=${ADMIN_PASSWORD} \
-e OWNCLOUD_REDIS_ENABLED=true \
-e OWNCLOUD_REDIS_HOST=redis \
--volume owncloud_files:/mnt/data \
owncloud/server:${OWNCLOUD_VERSION}

之后稍等片刻,打开网页即可

第三种 docker-compose 部署

首先保证 docker-compose 的版本在 1.12.0+

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 创建一个新的目录
mkdir owncloud-docker-server

cd owncloud-docker-server

# 下载 docker-compose.yml 文件
wget https://raw.githubusercontent.com/owncloud-docker/server/master/docker-compose.yml

# 配置环境变量文件
cat << EOF > .env
OWNCLOUD_VERSION=10.0
OWNCLOUD_DOMAIN=localhost
ADMIN_USERNAME=admin
ADMIN_PASSWORD=admin
HTTP_PORT=80
HTTPS_PORT=443
EOF

# 构建并启动容器
docker-compose up -d

当上面的流程都完成时,通过运行 docker-compose ps 检查所有容器是否已成功启动
还可以使用 docker-compose logs --follow owncloud 来查看日志
docker-compose stop 停止容器
docker-compose down 停止和删除容器

版本更新

  1. 进入 .yaml 或 .env 目录
  2. 将 ownCloud 设置维护模式, docker-compose exec server occ maintenance:mode --on
  3. 停止容器, docker-compose down
  4. 修改. env 文件的版本号,手动或者 sed -i 's/^OWNCLOUD_VERSION=.*$/OWNCLOUD_VERSION=<newVersion>/' /compose/*/.env
  5. 重新构建并启动, docker-compose up -d

指定挂载目录

  1. owncloud-server : /mnt/data

注意挂载本地目录时,要设置递归文件夹的可读权限 chmod -R 777 ./owncloud/*

配置说明
OWNCLOUD_VERSION: ownCloud 版本
OWNCLOUD_DOMAIN: ownCloud 可访问的域
ADMIN_USERNAME: 管理员用户名
ADMIN_PASSWORD: 管理员密码
HTTP_PORT: 使用的端口
HTTPS_PORT: SSL使用的端口

总结来说,推荐使用第三种方式来部署.

docker-compose 文件备份

docker-compose.yml:

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
version: '2.1'

volumes:
files:
driver: local
mysql:
driver: local
backup:
driver: local
redis:
driver: local

services:
owncloud:
image: owncloud/server:${OWNCLOUD_VERSION}
restart: always
container_name: owncloud-server
ports:
- ${HTTP_PORT}:8080
depends_on:
- db
- redis
environment:
- OWNCLOUD_DOMAIN=${OWNCLOUD_DOMAIN}
- OWNCLOUD_DB_TYPE=mysql
- OWNCLOUD_DB_NAME=owncloud
- OWNCLOUD_DB_USERNAME=owncloud
- OWNCLOUD_DB_PASSWORD=owncloud
- OWNCLOUD_DB_HOST=db
- OWNCLOUD_ADMIN_USERNAME=${ADMIN_USERNAME}
- OWNCLOUD_ADMIN_PASSWORD=${ADMIN_PASSWORD}
- OWNCLOUD_MYSQL_UTF8MB4=true
- OWNCLOUD_REDIS_ENABLED=true
- OWNCLOUD_REDIS_HOST=redis
healthcheck:
test: ["CMD", "/usr/bin/healthcheck"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- /home/liufa/owncloud-data:/mnt/data

db:
image: webhippie/mariadb:latest
restart: always
container_name: owncloud-mysql
environment:
- MARIADB_ROOT_PASSWORD=owncloud
- MARIADB_USERNAME=owncloud
- MARIADB_PASSWORD=owncloud
- MARIADB_DATABASE=owncloud
- MARIADB_MAX_ALLOWED_PACKET=128M
- MARIADB_INNODB_LOG_FILE_SIZE=64M
healthcheck:
test: ["CMD", "/usr/bin/healthcheck"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- /home/liufa/owncloud-mysql:/var/lib/mysql
- /home/liufa/owncloud-mysql-backup:/var/lib/backup

redis:
image: webhippie/redis:latest
container_name: owncloud-redis
restart: always
environment:
- REDIS_DATABASES=1
healthcheck:
test: ["CMD", "/usr/bin/healthcheck"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- /home/liufa/owncloud-redis:/var/lib/redis

.env:

1
2
3
4
5
6
OWNCLOUD_VERSION=10.0
OWNCLOUD_DOMAIN=0.0.0.0
ADMIN_USERNAME=admin
ADMIN_PASSWORD=
HTTP_PORT=1194
HTTPS_PORT=443

nginx 反向代理时的配置

注意配置 请求头 和 限制上传文件的大小

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 80;
#listen [::]:80 default_server;
server_name cloud.joylau.cn;
location / {
# proxy_pass http://JoyCloud;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://127.0.0.1:1194;
client_max_body_size 10000m;
}
}

背景

我们经常能看到在各大电商网站搜索关键字的时候,底下下拉框会补全你要搜索的商品,或者类似的商品,有时候甚至连错别字也能纠正过来,其实ElasticSearch也能实现这样的功能

创建索引

首先,能够被自动补全的需要设置索引类型为”completion”,其次,还可以设置自动提示为中文分词

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
{
"settings": {
"analysis": {
"analyzer": {
"ik": {
"tokenizer": "ik_max_word"
},
"ngram_analyzer": {
"tokenizer": "ngram_tokenizer"
}
},
"tokenizer": {
"ngram_tokenizer": {
"type": "ngram",
"min_gram": 1,
"max_gram": 30,
"token_chars": [
"letter",
"digit"
]
}
}
}
},
"mappings": {
"knowledge_info": {
"properties": {
"infoId": {
"type": "string"
},
"infoTitle": {
"type": "string",
"search_analyzer": "ik_max_word",
"analyzer": "ik_max_word",
"fields": {
"suggest": {
"max_input_length": 30,
"preserve_position_increments": false,
"type": "completion",
"preserve_separators": false,
"analyzer": "ik_max_word"
},
"wordCloud": {
"type": "string",
"analyzer": "ik_smart",
"fielddata":"true"
}
}
},
"infoKeywords": {
"type": "string",
"search_analyzer": "ik_max_word",
"analyzer": "ik_max_word",
"fields": {
"suggest": {
"max_input_length": 30,
"preserve_position_increments": false,
"type": "completion",
"preserve_separators": false,
"analyzer": "ik_max_word"
},
"wordCloud": {
"type": "string",
"analyzer": "ik_smart",
"fielddata":"true"
}
}
},
"infoSummary": {
"type": "string",
"search_analyzer": "ik_max_word",
"analyzer": "ik_max_word",
"fields": {
"suggest": {
"max_input_length": 30,
"preserve_position_increments": false,
"type": "completion",
"preserve_separators": false,
"analyzer": "ik_max_word"
},
"wordCloud": {
"type": "string",
"analyzer": "ik_smart",
"fielddata":"true"
}
}
},
"infoContent": {
"type": "text",
"search_analyzer": "ik_max_word",
"analyzer": "ik_max_word"
},
"propertyAuthor": {
"type": "string",
"search_analyzer": "ik_max_word",
"analyzer": "ik_max_word"
},
"propertyIssueUnit": {
"type": "string",
"search_analyzer": "ik_max_word",
"analyzer": "ik_max_word"
},
"propertyStandardCode": {
"type": "string",
"search_analyzer": "ik_max_word",
"analyzer": "ik_max_word"
},
"propertyLiteratureCategory": {
"type": "string",
"search_analyzer": "ik_max_word",
"analyzer": "ik_max_word"
},
"propertyLcCode": {
"type": "string",
"search_analyzer": "ik_max_word",
"analyzer": "ik_max_word"
},
"propertyLiteratureCode": {
"type": "string",
"search_analyzer": "ik_max_word",
"analyzer": "ik_max_word"
},
"data": {
"type": "text"
},
"attachment.content": {
"type": "text",
"search_analyzer": "ik_max_word",
"analyzer": "ik_max_word"
},
"auditState": {
"type": "string"
},
"infoType": {
"type": "string"
},
"infoFileUrl": {
"type": "string"
},
"infoFileName": {
"type": "string",
"search_analyzer": "ik_max_word",
"analyzer": "ik_max_word",
"fields": {
"suggest": {
"max_input_length": 60,
"preserve_position_increments": false,
"type": "completion",
"preserve_separators": false,
"analyzer": "ik_max_word"
}
}
},
"createTime": {
"type": "string"
}
}
}
}
}

其中 elasticsearch 需要安装中文分词 ik 插件和附件处理插件 ingest-attachment

Java API 调用

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
/**
* 自动完成提示
* @param search search
* @return MessageBody
*/
public MessageBody autoCompleteKnowledgeInfo(KnowledgeSearch search) {
//设置搜索建议
CompletionSuggestionBuilder infoTitleSuggestion = new CompletionSuggestionBuilder("infoTitle.suggest")
.text(search.getQuery())
.size(6);
CompletionSuggestionBuilder infoKeywordsSuggestion = new CompletionSuggestionBuilder("infoKeywords.suggest")
.text(search.getQuery())
.size(6);
CompletionSuggestionBuilder infoSummarySuggestion = new CompletionSuggestionBuilder("infoSummary.suggest")
.text(search.getQuery())
.size(6);
CompletionSuggestionBuilder infoFileNameSuggestion = new CompletionSuggestionBuilder("infoFileName.suggest")
.text(search.getQuery())
.size(6);
SuggestBuilder suggestBuilder = new SuggestBuilder()
.addSuggestion("标题", infoTitleSuggestion)
.addSuggestion("关键字", infoKeywordsSuggestion)
.addSuggestion("摘要", infoSummarySuggestion)
.addSuggestion("附件",infoFileNameSuggestion);
SearchRequestBuilder searchRequest = client.prepareSearch(ES_KNOWLEDGE_INDEX)
.setFetchSource(false)
.suggest(suggestBuilder);
List<JSONObject> list = new ArrayList<>();

//查询结果
SearchResponse searchResponse = searchRequest.get();

/*没查到结果*/
if (searchResponse.getSuggest() == null) {
return MessageBody.success(list);
}
searchResponse.getSuggest().forEach(entries -> {
String name = entries.getName();
for (Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option> entry : entries) {
for (Suggest.Suggestion.Entry.Option option : entry.getOptions()) {
JSONObject object = new JSONObject();
object.put("name",name);
object.put("text",option.getText().string());
list.add(object);
}
}
});
return MessageBody.success(list);
}

代码摘取自项目中的部分, 另外前端还可以配合自动完成的插件,最终来实现效果.

0%