JoyLau's Blog

JoyLau 的技术学习与思考

背景

想着写个 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);
}

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

背景

有时我们希望查询 固定条件下的全部数据
ES 默认的策略是返回10条数据
虽然可以 setSize()
但是默认上限是 10 万还是 100 万条数据,这不够优雅,一般不这么干

TransportClient 方法

1
2
3
4
5
6
7
8
9
10
TimeValue keepAlive = TimeValue.timeValueMinutes(30);
SearchRequestBuilder searchRequest = client.prepareSearch(ES_KNOWLEDGE_INDEX)
.setScroll(keepAlive)
.setSize(10000);
SearchResponse searchResponse = searchRequest.get();
do {
//处理的业务 saveIds(searchResponse);
searchResponse = client.prepareSearchScroll(searchResponse.getScrollId()).setScroll(keepAlive).execute()
.actionGet();
} while (searchResponse.getHits().getHits().length != 0);

背景

因项目需求,需要一个自动提示的功能,想到之前有 jquery 的 jQuery-Autocomplete 插件,于是就直接拿来用了,
直接在github 上找到了一个 starts 最多的项目 jQuery-Autocomplete
看了下插件的 API 可配置项很多,有一个 appendTo 配置,是我想要的,于是就决定使用这个差价

直接把 插件下载下来 放到项目中去,直接 $(…).autocomplete is not a function
……

项目中我写的只是其中的一个模块,页面的代码是纯 html 页面写的,然后通过 panel 引入 html 代码片段
很奇怪,为什么插件无法加载

于是就就把官方的demo跑了一下,没有问题

又怀疑是 jQuery 版本的问题,
官方的demo jQuery 版本是 1.8.2,项目使用的是1.11.1,
于是又在官方的 demo 下替换jQuery的版本
发现使用没有问题

又怀疑是插件的版本过高,于是再 GitHub 的 release 上找了个2014年发布的1.2.2的版本,这已经是能找到的最低版本了
发现还是不行

这就奇怪了,我之前也引入过其他的插件,正常使用都没有问题,偏偏使用这个有问题
于是想着插件的引入方式有问题,打开一看,jQuery插件的引入方式都是大同小异的
本人前端不擅长,也不知道怎么改…..

于是又在 GitHub上找了其他的插件,有的能用,但是没有我想要的功能….

一直这么来来回回的测试,已经晚上 10 点了…..
从吃完晚饭一直研究到现在还是没有解决
心里好气啊!!!!!
空调一关,直接回家了!!!!

解决

今天早上来又差了点资料,找到了个不太靠谱,但又想尝试了下的方法
TypeError: $(…).autocomplete is not a function

试一下吧,没想到真的可以

发一张对比图

query-Load-Plugins

0%