Liangwu's Notes

Just note anything

之前一直通过SS使用VPS的IPv6是可以使用bt.byr.cn的,但是从几个月前一直无法使用,使用浏览器用代理后直接跳转到一个com.com的网站,不清楚是什么原因,但是使用curl确实是没有问题的curl -x socks5://127.0.0.1:1080 -I https://bt.byr.cn/login.php。前者返回的是302,后者是200。

后来找到了zYeoman的代码,也不清楚当时是怎么搜到的,可以用,但是HTML格式好像有变动,这个好解决,稍微程序动下就好了。

然后自己新建了个byr-spider库,方便自己使用吧,加了requirements.txt,加了代理,这个我可以在没有IPv6的情况下,通过代理一样下载种子,然后看有什么自己喜欢需要下载的,就把种子文件传到VPS自己下载就好了。

在家里的NAS机器上新增了定时任务,自己拉最新的种子文件。

1
@hourly cd ~/byr && pipenv run python byr.py > log.log 2>&1

起因

有一天查看OpenWrt设备状态的时候,突然发现一个熟悉的Mac地址,而且没有主机名。感觉心里不踏实,毕竟已经做了最初级的SSID隐藏,我脑袋里想到的第一反应就是之前有人来家里玩的时候,连了家里的WIFI,而且还使用了什么WIFI密码盒子之类的软件,这类软件拿到GPS定位或运营商定位,再结合扫描的SSID信息,可能已经把我的WIFI信息和密码传到服务器上面了。这样我就必须要做进一步的改善了,当然最简单的方法就是改密码,但是家里有其他人使用,改动比较费事;再想到的就是WIFI连接时的MAC白名单验证,但我想这个可能通过抓包也能好样的出来,不是太好。

解决思路

家里的路由器基本都是用的OpenWrt,应该能通防火墙规则来做点文章,思路是MAC地址白名单,可连LAN,无法使用WAN,放狗搜了下,找到一些文章。How to Block Device on OpenWRT Based on MAC Address

修改

其实就是在Network -> Firewall里的Zones中的Zone Lan => REJECT,然后再给需要使用Internet的设备增加相应规则。改动就是修改OpenWrt中的/etc/config/firewall

最后一点就是内网做好防护工作,OpenWrt的SSH禁用密码,只能使用密钥;uhttpd修改为SSL,且修改端口。s

NAS搭建

其实很早的时候就想搞一台NAS玩了,毕竟有很多想收藏的高清视频,还有一些自己私人资料,不方便存在云盘里面的。之前HP Gen8机器还是不错,扩展性也非常不错,只是感觉有点小贵,然后就是风扇噪音问题比较严重,查了下说是HP虚拟的一个Raid卡造成的问题,在*nix下面无法获取到硬盘的温度,导致风扇会以最大转速工作。当然查Gen8的资料是最近正式想整一台NAS后才开始。之前是用蛮早的一台上网本,好像是三星N2600之类的型号吧,装了CentOS,专门下载PT和存照片。后来空间不够用,就想起还是组一台NAS吧,正好家里有6台笔记本(Surface Pro 4, Thinkpad T400, Thinkpad E330, Thinkpad E420 x 2, Samung N2600),2.5寸硬盘有6个,本着不能浪费的原则,就开始整NAS了。

技术选型

19年春节前就一直想着这个事情,老天也是“给力”,春节期间还真是没有出过一个太阳,就宅在家里想着怎么组这个NAS。

FreeBSD/ZFS/Jail

FreeBSD很稳定,port也非常好用。之前工作的主力系统是Gentoo,所以对FreeBSD很有好感。但是ZFS一直没有正式接触过,于是把FreeBSD Handbook好好地看了下,确实ZFS很强大,可以忘掉Raid,忘掉分区,不同目录设置不能参数适应不同用途(加密,压缩等等),快照功能。还有Jail可用,用途有点类似Linux下的Docker,可以把程序关到Jail里运行,不影响主系统。

但是真正用起来感觉还有好多东西需要深入研究,但又没那么多时间。

  1. ZFS很强大,但同时也是内存大户,推荐是1T配1G内存,然后建议是配ECC内存(这个作者原意思是关心数据的话,不管是什么文件系统,都建议ECC)。现在内存不是太贵,但是要自己组NAS的话,ECC内存好像不好搞定。
  2. ZFS优化比较麻烦。在虚拟机里用ZFS和UFS测试NextCloud/MySQL时,发现ZFS明显慢很多。放狗搜了下,关于ZFS优化MySQL这方面,有蛮多参数需要配置的。
  3. ZFS快照功能暂时好像用不上,没这需要。但蛮喜欢加密和压缩功能的。
  4. Jail配置NAT网络比较麻烦,不如Docker来得方便。而且PF与iptables又不一样,还要查不少资料,烦。
  5. 担心FreeBSD的硬件兼容性,不能够让NAS达到最节能的状态。

所以最后还是放弃了FreeBSD。

OpenMediaVault(OMV)

之前本来确实是想用FreeBSD来组NAS的,后来@kurtyan推荐使用OMV。然后就是虚拟机里搞了一把,有几个槽点吧。

  1. OMV镜像安装不支持UEFI,这个锅应该让Debian 8背吧。。。然后就是OMV镜像安装时,好像要选整个硬盘,这个有点恶心,后来干脆自己先装好Debian 9然后再装OMV。
  2. 感觉Web管理页面上有太多我不想要,也用不着的功能,看着比较浪费。

感觉OMV比较适合新手,界面什么的还是比较友好的。后来一想这个web管理界面,FreeBSD的webadmin也可以管理啊,一查Linux也可以用webadmin。但最后还是没有用webadmin,自己从基层组算了。

Debian/LVM/Docker

OMV是基于Debian的,还不如自己搭建想要的功能呢。刚开始有考虑CentOS,但本人不喜欢它的图形安装界面,好像比Debian耗资源一些,就pass掉了。

既然是自己用的NAS就需要考虑下数据的安全问题,以防硬盘突然挂掉,开始考虑的是软raid,重要数据raid 1,视频数据raid 0,后来考虑再三还是放弃了软raid的方案,买的是4盘位机箱,比较少,还是用USB3.0外挂硬盘,定时备份重要数据,特别重要的文件就GnuPG加密后传网盘。还有一个就是使用LVM管理分区,这样可以最大限度地使用硬盘空间,后面系统配置的时候再详细谈下。其实ZFS对于这两点很适合,send/recv备份很方便,分区根本不需要管,把不同硬盘加到不同pool就OK了。

还有一个选Linux的原因应该就是Docker了。这样可以很大程序减少应用程序对系统的干扰,把能在Docker下面跑的都放进去,各自跑各自的。

所以我最后选择了Debian开干。

硬件配置

硬件配置最开始设想的是4个SATA3接口,2个网卡,PICe接口进行扩展,CPU性能无所谓。网上搜了不少,候选方案有下面几个:

  1. HP Gen8。2个数据网口,1个管理网口。这点对于服务器管理非常重要,丢在某个角落完全不用管,可直接iLO进行所有操作。有4个盘位,还有内置的USB、SD卡插口。就是有点小贵,还有风扇噪音。
  2. HP Gen10。2个数据网口,4个盘位,AMD CPU。感觉也OK,风扇不吵了,但是少了iLO,而且低配价格也很高。
  3. GA N3160主板。2个数据网口,4个盘位,PCI插槽。刚开始是冲是它去的,最后PCI插槽放弃了,而且价格比J3455贵100多大洋。
  4. ASRock J4105/J3455。1个数据网口,4个盘位,PCIe x1插槽。两块板CPU有差别,内存一个是DDR4,一个是DDR3。但是J3455支持16G,J4105只支持8G。最后选择了J3455,因为它便宜 :)

其实最早是蛮想2个网口链路聚合的,这样多处同时从NAS存取数据的时候,网络不会成为其瓶颈,后来想了想,估计同时多处使用NAS的情况最近几年都不会有,后面如果确实有需要,可以PCIe转双网卡就OK。家里网线是六类双屏蔽(那个硬啊,现在有点后悔用双屏蔽的了),交换机Linksys SLM2008,路由器Buffalo WZR-HP-AG300H,光纤入户100M,所以暂时屋内1G局域网应该够用了。

这里给自己上了一课吧,当时查ASRock官网J3455的时候,就注意到SATA的第3,4接口是ASMedia ASM1061出来的,跟我后来买的PCIe转SATA接口是一样的。好像最高是400MB/s,就是如果这几个接口上SSD的话,可能会影响期性能。而且PCIe 2.0 x1的最高速度是500MB/s,这块也成了影响其性能的要点了。

电源选择方面,可以选小的DC转24针这种,完全没有噪音,功耗也低,但是但是贵啊。。。。最后选了一个ENP 7025B 250W的Flex电源,有噪音,功耗也比DC高,但是但是便宜啊 :(

开始机箱自带的风扇是12cm,直接从电源取电,也就是说一直是全速状态。看了下J3455板是CPU和机箱风扇电源接口都是3线的,所以可以调节风扇转速,后来又购了1个12cm 1500rpm的3线风扇。可惜板子不支持4线的风扇。。。

嗯,后面再详细记录下系统方面的配置吧。

软件配置

Debian

Debian安装比较简单了,将网络安装的ISO写入到U盘,安装就OK了。其实完全可以用PXE来搞定,后面再记录下PXE吧。

LVM

LVM里面有几个名词,PV物理卷(可以是硬盘,分区,文件),VG卷组(由物理卷组成的组,有点类似ZFS的Pool),LV逻辑卷(类似普通分区,可格式化,挂载)。

PV操作

1
2
3
4
dd if=/dev/zero of=/dev/xxx bs=512 count=1
pvcreate /dev/xxx
pvs
pvremove /dev/xxx

VG操作

1
2
3
4
5
6
7
vgcreate vg-nas /dev/xxx /dev/xxx
vgextend vg-nas /dev/xxx
vgreduce vg-nas /dev/xxx
vgchange -a y/n vg-nas # activating/deactivating vg
vgremove vg-nas
vgrename vg-nas-old vg-nas-new
vgs

LV操作

1
2
3
4
5
6
lvcreate -L 1T -n lv-data vg-nas # linar
lvcreate -l 100%FREE -n lv-other vg-nas
lvs
lvextend -l +100%FREE -n vg-name lv-name # /dev/vg-name/lv-name
resize2fs /dev/vg-name/lv-name
lvreduce --resizefs -L 500G vg-name/lv-name

LVM调整挂载点容量大小就比较方便了,不明白的时候可以看RedHat的在线文档,写得很详细。

sshd

默认SSHD是密码登录的,可以用sshkey-gen生成密钥后,再用ssh-copy-id将公钥复制到远程主机。NAS我需要internet也能访问到,这样SSHD默认配置就不太合适了,需要改几个地方,其实打开/etc/ssh/sshd_config就一目了然,从配置字面上就能知道啥作用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Protocol 2

PermitRootLogin no
StrictModes yes
MaxAuthTries 6 #BAN IP的话可以用Fail2ban
MaxSessions 10

AuthorizedKeysCommand none
AuthorizedKeysCommandUser nobody

PasswordAuthentication no
PermitEmptyPasswords no

AllowGroups sshusers #将需要使用ssh的用户加入sshusers用户组

内网穿透

之前内网穿透使用的是ssh端口转发,NAS用远程端口转发-R 2222:127.0.0.1:22 VPS(VPS的2222端口转发到NAS的22端口),然后在终端机使用本地端口转发-L 4444:127.0.0.1:2222 VPS(终端机4444端口转发到VPS的2222端口),这样在终端机ssh -p 4444 localhost就可以访问NAS了,很方便不是?后来好像有时候网络不稳定,掉线后NAS不会自己进行端口转发,然后就找到了现在用的frpopenwrt frp

其中frp VPS Server端的配置如下。其实程序自带的说明文件里说得很详细,认真看下就可以配得很好了。

/etc/frp/frps.ini
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
[common]
bind_addr = 0.0.0.0
bind_port = 7000

bind_udp_port = 7001

tcp_bind_port = 7000

dashboard_addr = 127.0.0.1
dashboard_port = 7500

dashboard_user = user
dashboard_pwd = pwd

log_file = /var/log/frps.log

log_info = info
log_max_days = 3

privilege_token = privilege_token

max_pool_count = 5
max_ports_per_client = 0

authentication_timeout = 900
tcp_mux = true
frps.service
1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=frps daemon
After=network.target
Wants=network.target

[Service]
Type=simple
User=nobody
Group=nogroup
ExecStart=/usr/bin/frps -c /etc/frp/frps.ini

[Install]
WantedBy=multi-user.targe

其中NAS的frp配置如下。OpenWrt的话,直接使用screen,sleep 30 && screen -dmS frpc frpc -c /etc/frp/frpc.ini,加到/etc/rc.local

frpc.ini
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
[common]
server_addr = VPS_IP
server_port = 7000

log_file = /var/log/frpc.log
log_level = info
log_max_days = 3

privilege_token = privilege_token

admin_addr = 127.0.0.1
admin_port = 7400
admin_user = admin_user
admin_passwd = admin_pwd

pool_count = 5
tcp_mux = true
user = nas
log_fail_exit = true
protocol = tcp

[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 22
remote_port = 2222
frpc.service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Unit]
Description=FRP Client Daemon
After=network.target
Wants=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/frpc -c /etc/frp/frpc.ini
Restart=always
RestartSec=20s
User=nobody

[Install]
WantedBy=multi-user.target

下载工具

NAS的话必须要下载片子咯,BT/PT使用的是transmission,普通其它下载用的是aria2。使用transmission需要下载几个包apt install transmission-{daemon, remote{,-cli}},平时使用的话我在bash里加了下载几个alias。

1
2
3
4
alias lcp='rsync -avhW --no-compress --progress'
alias t-remote='transmission-remote -n user:password localhost'
alias t-remote='transmission-remote-cli -c user:password@localhost:9091'
alias a-d='aria2c -x 10 -j 1 --http-user user --http-passwd passwd'

NextCloud

其中架NAS最重要的一点就是网盘,放一些家庭照片和视频,常跟老婆説,等以后崽结婚的时候,把小时候的照片拿出来看,哈哈哈。

之前家里用的是FreeBSD下面架设的,这次换成Debian,那肯定就用Docker来跑这些咯,以后迁移起来也是非常快的,打包带走。。。

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

services:
db:
image: mariadb
restart: always
volumes:
- ./db:/var/lib/mysql
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
environment:
- MYSQL_ROOT_PASSWORD=pwd1
- MYSQL_PASSWORD=pwd2
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
nextcloud:
image: nextcloud:15-fpm-alpine
depends_on:
- db
volumes:
- ./html:/var/www/html
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
restart: always
nginx:
build: ./nginx
depends_on:
- db
- nextcloud
ports:
- "80:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./html:/var/www/html
restart: always

其中./nginx/default.conf可以直接参考官网文档,然后还有一点就是nginx和nextcloud的docker image中用户名ID不一致,这样共享的目录./html就不能同时使用。需要调整下,nginx的image很小,可以直接从它下手,nginx的Dockerfile如下。

nginx
1
2
3
4
FROM nginx:alpine

RUN sed -i 's/:100:/:82:/g' /etc/passwd; \
sed -i 's/:101:/:82:/g' /etc/passwd /etc/group

SAMBA

家里其他设备访问NAS估计用得最多的就是SAMBA了吧,直接在/etc/samba/smb.conf最后加上一些配置就OK了,也懒得再认真配置了,只要能访问到就OK。

smb.conf
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
[public]
comment = public
path = /data/BTDownloads
browsable = yes
create mask = 0660
directory mask = 0771
writable = no
guest ok = yes
[videos]
comment = public
path = /mnt/videos
browsable = yes
create mask = 0660
directory mask = 0771
writable = no
guest ok = yes
[win7]
comment = public
path = /mnt/cdrom
browsable = no
create mask = 0660
directory mask = 0771
writable = no
guest ok = yes
[personal]
comment = personal
path = /mnt/nas/personal
browsable = no
create mask = 0660
directory mask = 0771
writable = yes
guest ok = no

NFS

家里还有一些其他设备,比如路由器(OpenWrt)和Raspberry Pi,它们可以很方便的使用NFS,这样NAS提供NFS服务就可以了。修改/etc/exports

1
/mnt/nas/Tools  *(no_subtree_check,ro,all_squash,insecure,async)

其它

硬盘多了,功率也大,安装包hdparm可以设置硬盘standby的时间,据説可以延长使用时间,减小功耗,前提是不要设置得太小,硬盘磁头一停一启动地可能更耗硬盘。修改/etc/hdparm.conf

/etc/hdparm.conf
1
2
3
/dev/sdb {
spindown_time = 250
}

安装hddtemp sensors来读取温度值。

BIOS设置

关闭非必要功能,节能设置等

WOL,需要开启PCIE power on

tips

  1. NAS下载了很多片片,要传到手机上看,之前都是iTunes通过WIFI读取SAMBA共享文件,再传到VLC,慢得1B,还得再多开台电脑。后来了解到可以直接用curl传,方便了很多。curl -F "filename='filename'" -F "files[]=@filepath" http://VLC_IP/upload.json,本来想写个shell脚本自动上传目录下的所有文件的,但是没有找到好的方法,干脆用python吧,直接造成shell脚本运行。 :(不会直接使用Python上传到VLC,POST传的东西跟平时有点不一样。。。

  2. 下载Dropbox文件,Bash脚本Dropbox Uploader,在VPS上面下载Dropbox的照片,然后再传回来。

  3. 下载OneDrive文件,用windows里面的客户端可能是最简单的,但没有在NAS里面安装虚拟机,也不想平时再开台机器。后来想了一条路,Chrome下载OneDrive的照片目录,zip文件,然后打开Chrome开发工具,直接将刚才那个请求Copy as cURL,这样可以直接在NAS下载照片了。

  4. Picasaweb。long long ago,那时候Google还没有被封,传照片就到Picasa,哈哈,里面好多年轻的照片啊。这里需要设置下,在Google Drive里面设置下,将Google Photos照片导入到Google Drive,然后使用rclone将里面的照片复制到本地。rclone copy remote:Google\ Photos /mnt/photos -P

  5. 照片存了这么多份,肯定有重复的。最开始写了脚本进行查重,思路就是找出所有图片文件,对md5码,发现效率不高,后来放狗搜发现一个好工具fdepesfind . ! -empty -type f -size +500k -size -50M -exec md5sum {} + | sort > ~/allfiles,然后再对allfiles文件用unique就能找出重复文件了。fdupes -r -S -1 Data Documents nextcloud Pictures Work > ~/allfiles,这个也是找出所有重复文件,还可以直接使用fdupes快速删除重复文件。

后期优化

家庭网络换成1G带宽,扩展链路聚合。路由器换成了Buffalo AG300H,交换机换成了Cisio SLM2008,这样家里千兆OK了,但是发现丫我的无线是100M的WAN和LAN,电视机是100M口,一口老血吐出来。电视机的话,使用Xbox One S看得了,1000M口。无线路由器有空再换个好点的吧。开始装修的时候就是考虑NAS链路聚合的,毕竟多设备同时访问NAS的话,不链路聚合的话可能会成为瓶颈。到时候NAS换了PCIe的双网卡,硬盘只要4口,全换成4T盘,这样也OK的。

PXE

PXE的方案我使用的是DHCP Server在OpenWrt,然后tftp server在NAS。

DHCP配置/etc/config/dhcp

1
2
3
4
config boot 'linux'
option filename 'pxelinux.0'
option serveraddress '192.168.1.222'
option servername 'Liangwu PXE SERVER'

tftp-hpa配置。这里我在NAS上面直接将tftp跑在docker里面的。下载Debian netboot,并解压到res目录。

1
2
3
4
5
6
7
8
9
10
11
12
version: '3.7'

services:
tftp-hpa:
image: jumanjiman/tftp-hpa
restart: always
ports:
- "69:69/udp"
volumes:
- ./res/debian-installer/amd64/boot-screens/syslinux.cfg:/tftpboot/pxelinux.cfg/default:ro
- ./res/debian-installer:/tftpboot/debian-installer:ro
- ./res/windows:/tftpboot/windows:ro

修改./res/debian-installer/amd64/boot-screens/syslinux.cfg,增加下面内容支持windows安装。制作PE镜像,加入网卡驱动,这些在Microsoft文档可以找到。

1
2
3
4
5
6
7
label 9
menu label Install Windows 7
KERNEL memdisk
INITRD windows/winpe_7.iso
APPEND iso raw
prompt 0
timeout 0

安装

家里好几台电脑,现在有Surface Pro 4的Windows 10, Thinkpad T400的Windows 7和XUbuntu 18.04.1,老婆老的三星上网本NP110C的FreeBSD 11和CentOS 7, 公司电脑Thinkpad E550的Gentoo和XUbuntu 18.04.1。还有台破的Thinkpad E330,想着周末没啥事,就装个省点事的版本吧,本来是装好了Ubuntu,但是感觉不好,重新装个ArchLinux吧。

EFI/GPT

其实以前也装过ArchLinux,是在T400上面。现在在E330上面,有一些地方不一样,主要是GPT和EFI的问题,所以就记录一下吧。

首先分区的时候,用的是fdisk,用GPT。大概分区方式如下

物理分区文件系统挂载点
/dev/sda1EFI System/boot
/dev/sda2Ext4/
/dev/sda3Ext4/data
/dev/sda4Swap

EFI进行分区,mkfs.fat -F32 /dev/sda1。安装相应软件efibootmgr,不知道有没有用,反正我没有试过不安装有没有影响。

GRUB安装

EFI的GRUB有点不一样,以前只需要grub-install /dev/sda就好了,但是现在不行了。现在GRUB安装如下,其中/boot是之前分区的EFI分区,Ubuntu就是另外一分单独的分区,它是单独挂载在/boot/EFIbootloader-id是EFI启动界面显示的ID。

1
2
grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB
grub-mkconfig -o /etc/grub/grub.cfg

wireless

笔记本肯定要用无线上网嘛,可是我家SSID是隐藏的,就用了自带的netctl控制。在/etc/netctl/examples下面有好多示例文档,用wireless-wpa就好了。

1
2
cp /etc/netctl/examples /etc/netctl/liangwu
netctl start liangwu

这样在安装的时候就可以上网了。但是安装的新系统,如果不安装netctl或其它网络相关软件的话,新系统还是无法上网。其中netctl还有一些依赖需要安装。dialog用于wifi-menu使用,wpa_supplicant用于密钥使用。

其它

hostnamectl设置主机名

timedatectl设置时区等等

Desktop Environment

驱动需要根据自己环境变动。

1
2
3
4
5
6
pacman -S xorg-server xf86-video-intel xf86-video-nouveau xfce4 xfce4-goodies lightdm lightdm-gtk-greeter lightdm-gtk-greeter-settings networkmanager nm-connection-editor network-manager-applet papirus-icon-theme alsa-utils pavucontrol pasystray pulseaudio pulseaudio-alsa dnsutils fcitx fcitx-configtool fcitx-gtk3 fcitx-qt5
vim /etc/lightdm/lightdm.conf
vim /etc/lightdm/lightdm--gtk-greeter.conf
vim /etc/systemd/logind.conf
systemctl enable lightdm.service
systemctl enable NetworkManager

MyBatis FAQ

用MyBatis有点点时间了,也碰到一些问题。放狗搜了之后,发现官方github上的FAQ已经有一些东西,是可以先看看的。

批量插入

之前用MyBatis进行批量插入时,一直都在MyBatis的Mapper.xml里面用foreach拼成一个比较长的SQL,这个时候MySQL和Oracle的拼法还有点不一样。MySQL可以直接用INSERT INTO (id) VALUES (1),(2),(3)这种形式,而Oracle不行,Oracle需要且UNION ALLFROM DUAL或直接BEGIN ... END

后来了解看renren security的源码时候,发现有使用MyBatis Plus,在MyBatis Plus里面的ServiceImpl<M extends BaseMapper<T>, T>类中有个saveBatch方法,看日志发现他不是用接长SQL方法实现的,跟Groovy里面的batch操作是一样的。后来搜索后发现MyBatis其实有个Executor本身就带批量操作功能。。。

1
2
3
4
5
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, true)) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
userList.forEach(userMapper::insert);
sqlSession.flushStatements();
}

其实我感觉应该是自己对JDBC不熟悉的原因吧,等有时间了去专门学习学习。这个我碰到一个坑,就是Spring boot进行JUnit测试操作的时候,在方法上加上@Transactional,事务回滚。可能是Spring boot test里面本身对事务就是这样处理的?这个有时间也是需要去解决的。// TODO

基本类型(String, Integer等)的列表映射

如果一个查询结果只有一列,这样返回的列表很容易进行映射。但是当一个对象里面有两个属性,一个ID,一个基本类型列表时,在之前查MyBatis文档的时候没有发现,后来在网上搜到了,映射的时候还是用collection,但是里面的只填一个result column。刚好在MyBatis FAQ里面也看到了。搬过来吧。

比如SQL搜索结果是

idstr
1A
2B

Java对象

1
2
3
4
5
@Data
public class SomeBean {
private Integer id;
private List<String> strings;
}

XML映射如下

1
2
3
4
5
6
<resultMap id="" type="SomeBean">
<id column="id" property="id" />
<collection property="strings" ofType="string" javaType="list">
<result column="str" />
</collection>
</resultMap>

另外官方其它的FAQ还有变量使用时#{}${}的区别:$在生成ps的时候,就已经替换了,存在安全问题,#在生成ps的时候,是用?代替,运行的时候再进行值代入。

SQL LIKE,这个有好多实现的方法,比如String自带%,使用bind,直接在SQL里面使用字符拼接。

插入时将自动生成的key返回,在insert映射的时候加入useGeneratedKeys="true" keyProperty="id"

mapper方法传参使用变量名,这个直接使用注解@Param,或直接包装成对象或Map,或mybatis.configuration.use-actual-param-name= true

SSH端口转发

ssh真的是很好用啊,传文件,本地/远程/动态端口转发,远程,X转发等等。X转发当时搞过一次,服务器没有安装X环境,无图形界面,要安装Oracle,当时搞了半天,用静默安装的形式安装好了一台。后来发现可以用SSH X转发,图形界面在我的机器上,实际安装程序在服务器上,好好玩啊。

嗯嗯,总结下在实际工作中使用最多的就是端口转发了。

本地端口转发

本地端口转发的命令是:

1
ssh -L [bind_address:]port:host:hostport gate-way-server

ssh文档中对本地端口转发的说明:

Specifies that the given port on the local (client) host is to be forwarded to the given host and port on the remote side. This works by allocating a socket to listen to port on the local side, optionally bound to the specified bind_address. Whenever a connection is made to this port, the connection is forwarded over the secure channel, and a connection is made to host port hostport from the remote machine. Port forwardings can also be specified in the configuration file. IPv6 addresses can be specified with an alternative syntax:
[bind_address/]port/host/hostport or by enclosing the address in square brackets. Only the superuser can forward privileged ports. By default, the local port is bound in accordance with the GatewayPorts setting. However, an explicit bind_address may be used to bind the connection to a specific address. The bind_address of ‘’localhost’’ indicates that the listening port be bound for local use only, while an empty address or ‘*’ indicates that the port should be available from all interfaces.

我的理解就是:本地开一个端口(port),访问这个端口的效果和gate-way-server访问host:hostport的效果一样。这样我体会到的最大的用处就是可以访问受限制(gate-way-server)的内网端口。

比如我在外面出差,想访问公司内部server-A3389端口,server-B3306端口,server-C1521端口。但是这些端口肯定是不会对外开放的,我访问server-outer服务器对外暴露的22端口。于是我通过下面的命令就可以解决我的需求。

1
ssh -L 3389:server-A:3389 -L 3306:server-B:3306 -L 1521:server-C:1521 -L 2222:localhost:22 server-outer -fNnC

这样,我就可以直接访问本地的3389, 3306, 1521, 2222端口来达到访问server-A:3389, server-B:3306, server-C:1521, server-outer:22端口的目的了。可能表述的不是太清楚,网上有几张图说明得很直白。

Local port forwarding

远程端口转发

1
ssh -R [bind_address:]port:host:hostport remote-server

Specifies that the given port on the remote (server) host is to be forwarded to the given host and port on the local side. This works by allocating a socket to listen to port on the remote side, and whenever a connection is made to this port, the connection is forwarded over the secure channel, and a connection is made to host port hostport from the local machine.

Port forwardings can also be specified in the configuration file. Privileged ports can be forwarded only when logging in as root on the remote machine. IPv6 addresses can be specified by enclosing the address in square braces or using an alternative syntax:
[bind_address/]host/port/hostport.

By default, the listening socket on the server will be bound to the loopback interface only. This may be overridden by specifying a bind_address. An empty bind_address, or the address ‘*’, indicates that the remote socket should listen on all interfaces. Specifying a remote bind_address will only succeed if the server’s GatewayPorts option is enabled (see sshd_config(5)).

If the port argument is ‘0’, the listen port will be dynamically allocated on the server and reported to the client at run time.

大概意思就是在remote-server上面绑定一个port,访问这个port的效果,就和本机现在访问host:hostport效果一样。

这种情况我也有使用,比如某处有两台电脑,pc-1能访问外网,pc-2不能访问外网,但pc-1pc-2能互通。这时我在公司想访问pc-1pc-23389端口,这个时候我就可以在pc-1机器上执行:

1
ssh -R 13389:localhost:3389 -R 23389:pc-2:3389 remote-server

其中remote-server为公司的一台电脑,互联网可以访问。这样我直接访问remote-server1338923389端口就可以直接连接到pc-1:3389pc-2:3389端口了,十分好用啊。

哈哈,是不是相当于一个简单的内网穿透功能了。

Remote port forwarding

动态端口转发

1
ssh -D port remote-server

Specifies a local ‘’dynamic’’ application-level port forwarding. This works by allocating a socket to listen to port on the local side, optionally bound to the specified bind_address. Whenever a connection is made to this port, the connection is forwarded over the secure channel, and the application protocol is then used to determine where to connect to from the remote machine. Currently the SOCKS4 and SOCKS5 protocols are supported, and ssh will act as a SOCKS server. Only root can forward privileged ports. Dynamic port forwardings can also be specified in the configuration file.

IPv6 addresses can be specified with an alternative syntax:
[bind_address/]port or by enclosing the address in square brackets. Only the superuser can forward privileged ports. By default, the local port is bound in accordance with the GatewayPorts setting. However, an explicit bind_address may be used to bind the connection to a specific address. The bind_address of ‘’localhost’’ indicates that the listening port be bound for local use only, while an empty address or ‘*’ indicates that the port should be available from all interfaces.

通过本地的port访问互联网,相当于通过remote-server来访问互联网。

嗯嗯,以前用来当梯子用,后来墙升级,很不稳定,后来好像大家都没用它来当梯子了。当然墙应该不是破解了ssh,应该是只流量识别吧。

Dynmaic port forwarding

参考文章

SSH 安全性和配置入门

实战 SSH 端口转发

What’s ssh port forwarding and what’s the difference between ssh local and remote port forwarding [duplicate]

npm包安装问题

之前在家基本都是用Surface Pro 4所以基本也就没有安装Nodejs。今天晚上正好有点时间,就把老的Thinkpad T400拿出来,写点东西总结东东吧,要不一直不总结,埋头处理工作,感觉一直没有进步。

感觉还有好多东西没有写,SSH Local/Remote Proxykubernetes环境搭建(not hard way)gitlab及runner在k8s里面搭建MyBatis学习,这些东西下周的时间全要写下来吧,要不全都要忘记了。

npm在天朝第一步应该是用cnpm吧,这一点我还是蛮喜欢alibaba的,在国内搞了一些方便开发者的镜像。

1
$ npm install -g cnpm --registry=https://registry.npm.taobao.org

这里-g就全局安装需要root权限,所以需要改下npm包管理的配置。

  1. 创建包安装目录
1
mkdir ~/.npm-packages
  1. 配置npm,将全局安装的包安装到上一步创建的目录
1
echo "prefix=${HOME}/.npm-packages" >> ~/.npmrc
  1. 配置环境PATH去正确目录查找npm包和man文件。将下面内容加入到~/.bashrc~/.zshrc
1
2
3
4
5
NPM_PACKAGES="${HOME}/.npm-packages"
PATH="$NPM_PACKAGES/bin:$PATH"

unset MANPATH
export MANPATH="$NPM_PACKAGES/share/man:$(manpath)"
  1. 更新环境
1
source ~/.zshrc

这个时候再去全局安装npm包的话,就不需要root权限,直接将包安装到~/.npm-packages目录下面了。

参考文章

Install npm packages globally without sudo on macOS and Linux

Resolving EACCES permissions errors when installing packages globally

源起

服务器已经跑起来很久了,但是有需求要限制客户端使用我们服务,一同事说到了SSL双向认证的问题,之前没有玩过,所以查了一些资料。后来想了下,以前其实也碰到过,就是银行和支付宝的证书认证嘛,通过证书对帐号进行认证。还有一些登录的时候不填帐号,只填密码,其实就是在客户证书来当用户名嘛。这个在Nginx双向认证的时候就可以解决,Nginx收到证书信息,然后反向代理到后面服务的时候,可以在头信息里面加一点东西,就可以解决客户登录的时候的帐号填写问题了。当然还可以又填用户名,又使用证书,然后核对证书的用户名和用户填的用户名是否一致,不致的情况下,可以直接吊销证书等等,可以做的事情就很多了。简单记下之前配置的过程吧。

准备工作

可以编辑openssl.cnf,设置一部分默认值,比如国别、组织、机构啊什么的。CentOS在/etc/pki/tls目录下面,Gentoo在/etc/ssl目录下面。因为服务是CentOS,下面默认是在CentOS下操作。如果非root用户操作,做部分修改就行。

1
2
3
4
5
cd /etc/pki/CA
# 好像是证书db
touch index.txt
# 证书序号,每生成一个证书自增1
echo '00' > serial

生成CA密钥和证书

自已充当CA认证机构(一般为第三方权威机构),需要生成一个CA的密钥和证书,用于对其它用户的证书请求进行认证。

1
2
openssl genrsa -des3 -out ca-key.pem 4096
openssl req -new -x509 -days 3650 -key ca-key.pem -out ca-cert.pem

生成自签名服务器证书(非必需)

如果可以,最好去第三方权威机构申请证书(非WoSign),或用letsencrypt的证书。实在不行,就像我们这样,自已生成证书算球的,就可以走下面的流程了。

1
2
3
4
5
6
7
openssl genrsa -des3 -out server-key.pem 1024
#生成无密码的私钥,这样nginx加载的时候不会提示输入密码的地方卡住。或者直接上面那句不加-des3选项
openssl rsa -in server-key.pem -out server-key.key
#生成csr签名申请,用于CA对证书进行签名。重要:其中提示输入的CN common name必须填服务器的域名、泛域名或IP地址。
openssl req -new -key server-key.pem -out server-req.csr
#用CA对服务器证书进行签名
openssl ca -policy policy_anything -days 3650 -CA ca-cert.pem -CAkey ca-key.pem -in server-req.csr -out server-cert.pem

生成客户证书

与生成服务器证书基本一致,对于CA而言,是一样的。申请过来,进行认证。

1
2
3
4
5
6
7
8
openssl genrsa -des3 -out client-key.pem 1024
#生成csr签名申请,用于CA对证书进行签名。
openssl req -new -key client-key.pem -out client-req.csr
#用CA对证书进行签名
openssl ca -policy policy_anything -days 3650 -CA ca-cert.pem -CAkey ca-key.pem -in client-req.csr -out client-cert.pem
#客户端证书转换为PKCS #12格式,客户端浏览器导入即可
#openssl pkcs12 -export -out client.pfx -inkey client-key.pem -in client-cert.pem -certfile ca-cert.pem
openssl pkcs12 -export -clcerts -in client-cert.pem -inkey client-key.pem -out client.p12

Nginx配置

/etc/nginx/conf.d/default.conf
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
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name test.test;

ssl on;
# 可使用其它机构颁发的证书
ssl_certificate /etc/nginx/ssl/server-cert.pem;
# 服务器私钥
ssl_certificate_key /etc/nginx/ssl/server-key.key;

# 客户端证书认证
ssl_client_certificate /etc/nginx/ssl/ca-cert.pem;
# optional表示无证书可访问,用于无证书情况下的提示信息。
ssl_verify_client optional;

ssl_session_cache shared:SSL:5m;
ssl_session_timeout 1h;

ssl_protocols TLSv1.1 TLSv1.2;
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;

client_max_body_size 10M;

# 客户端证书认证失败显示400信息
if ($ssl_client_verify != "SUCCESS") {
return 400;
}
......

参考

问题

现在项目中有个单据有三个表关系,主表、明细表、次明细表,大概关系如下面代码所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Main{
private String mtId;
private List<Sub> subs;
......
}

class Sub{
private String mtId;
private List<Bat> bats;
......
}

class Bat{
private String mtId;
......
}

之前有人最笨最笨的办法就是从数据库拉三次出来,然后再组成整个数据结构。其实本来MyBatis里面就有Collection,用起来很方便。然后我就用下面的Mapper.xml来改试了一下。

尝试

MyBatis官网已经讲了Collection的使用,很详细了。然后就自己试下看如何更爽的解决之前那个问题。MyBatis已经把配置荐mapUnderscoreToCamelCase设置成true了,这样就可以少写Entity的映射关系了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<select id="getWholeBill" resultMap="EntirBillResultMap">
SELECT
m.mt_id AS mt_id, s.mt_id AS s_mt_id, b.mt_id AS s_b_mt_it
FROM
main m
INNER JOIN
sub s ON (s.pid = m.id)
INNER JOIN
bat b ON (m.pid = s.id)
WHERE m.mt_id = #{id}
</select>

<resultMap id="EntirBillResultMap" type="Main" autoMapping="true">
<collection property="subs" columnPrefix="s_" ofType="Sub" autoMapping="true">
<collection property="bats" columnPrefix="b_" ofType="Bat" autoMapping="true">
</collection>
</collection>
</resultMap>

本来以为这样可以很好地解决我的问题,但是但是,测试的时候发现,主表和明细表好像没有group by的状态。

修改

碰到这个问题,其实查下源码可能就好解决了。但是其实我一直没有碰MyBatis的源码,所以可能查起来会很头痛。网上稍微搜了下,没有找到答案。应该是映射关系写得有问题吧。尝试了几次,觉得就是没有group by嘛,但是如果告诉MyBatis去group by哪个字段呢?好像平时写得最多的那个id标签没有用哦,太懒了,把这个都省掉。果然加上去就解决了,嗯嗯,很爽。

1
2
3
4
5
6
7
8
9
<resultMap id="EntirBillResultMap" type="Main" autoMapping="true">
<id column="mt_id" property="mtId"/>
<collection property="subs" columnPrefix="s_" ofType="Sub" autoMapping="true">
<id column="mt_id" property="mtId"/>
<collection property="bats" columnPrefix="b_" ofType="Bat" autoMapping="true">
<id column="mt_id" property="mtId"/>
</collection>
</collection>
</resultMap>

CI是个好东西

本身现在所在的团队使用Git的人不多,所以往GitLab上提交代码的时候往往有一些不合格的代码,后来发现GitLab CI可以完美地解决这个问题。

需求及实现

  1. service目录中不包含platformService目录。
  2. 所有项目中不能包含.properties文件,以.properties.sample文件提供给大家,然后.gitignore目录中也忽略.properties文件
  3. config目录中不包含index.js文件,以index.js.sample文件提供给大家
  4. 所有vue代码中不能包含调试用代码”debugger|console.log”
  5. Java代码能编译通过

只有通过以上5点的代码才能合并到主仓库,避免污染代码库。其实原因很简单,看下面就清楚。

.gitlab-ci.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
image: 10.17.xx.xx/maven:3-jdk-8-alpine

variables:
MAVEN_OPTS: "-Dmaven.repo.local=/root/.m2/repository -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
MAVEN_CLI_OPTS: "--batch-mode --errors --show-version -Dmaven.test.skip=true -s /root/.m2/settings.xml"

test:validate:
stage: test
script:
- test $(find service -type f -path "*/platformService/*" | wc -l) -eq 0
- test $(find service -name "*.properties" -path "*/properties/*" | wc -l) -eq 0
- test $(find web -name "index.js" -path "*/config/*" | wc -l) -eq 0
- test $(find web -name "*.vue" -type f -exec grep -E 'debugger|console\.log' {} \; | wc -l) -eq 0
tags:
- common

test:compile:
stage: test
before_script:
- wget http://10.17.xx.xx/download/settings.xml -O /root/.m2/settings.xml
script:
- mvn $MAVEN_CLI_OPTS -f service/spdHERPService/pom.xml clean compile
tags:
- common

Docker设置

其中Runner可能会用到Docker私服,现在我们是用Nexus搭建(IP:10.17.xx.xx)的,Docker需要登录才能使用。下面是Docker的配置,外面配置了两个代理,这样下载外面的镜像速度就很快了。

daemon.json
1
2
3
4
5
6
7
{
"registry-mirrors": ["https://registry.docker-cn.com", "https://docker.mirrors.ustc.edu.cn"],
"insecure-registries": ["10.17.xx.xx"],
"disable-legacy-registry": true,
"log-driver": "json-file",
"log-opts": {"max-size": "5m", "max-file": "3"}
}

待Docker配置好后就需要配置Runner了。首先获取Docker登录私服的登录信息,官网有两个方法。一个是
docker login registry.example.com --username my_username --password my_password,然后查看~/.docker/config.json;另外一个是echo -n "my_username:my_password" | base64。然后就是把登录信息加到Runner的配置文件中去。万事OK

config.toml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
concurrent = 10
check_interval = 0

[[runners]]
name = "122-runner"
url = "http://10.17.xx.xx/gitlab/"
token = "xxxtoken"
executor = "docker"
environment = ["DOCKER_AUTH_CONFIG={ \"auths\": { \"10.17.xx.xx\": { \"auth\": \"authtoken\" } } }"]
[runners.docker]
tls_verify = false
image = "alpine:latest"
privileged = false
disable_cache = false
volumes = ["/cache", "/root/.m2"]
shm_size = 0
[runners.cache]
0%