Docker容器如何通过DHCP,实现自动获取宿主机局域网IP

思路

  • Docker使用MacVLAN建立一个network
  • 容器中使用udhcpc自动获取IP
  • 宿主机配置MacVLAN Link来访问容器IP

Docker建立MacVLAN network

networks:
  lan:
    driver: macvlan
    driver_opts:
      parent: enp2s0

services:
  service_A:
    cap_add:
      - NET_ADMIN
    mac_address: "02:42:dd:cc:bb:aa"
    networks:
      - lan

这里使用docker-compose,先建立一个macvlan驱动的network,并且不配置subnetgateway
之所以这样做,是因为如果配置了外面局域网的网段,Docker会自动分配一个IP给容器,而这个IP很可能在外部局域网里已经存在了,且这个IP不被局域网路由器所管理。

以当前配置将容器启动之后,Docker会自动给lan network一个网段,自动给容器分配一个IP(和外部局域网网段不同)。

容器中使用udhcpc自动获取IP

在上一步容器启动之后,我们可以尝试进入运行中容器,看看是否有udhcpc命令,并且尝试通过DHCP获取局域网IP。

docker exec -it service_A bash

udhcpc

如果一切顺利,应该会看到类似下面的输出,说明就是成功从局域网DHCP获取到IP,并且这个IP可以通过路由器进行管理。
如果出现permission相关报错,可能是容器缺少NET_ADMIN权限。

udhcpc: started, v1.37.0
udhcpc: broadcasting discover
udhcpc: broadcasting select for 192.168.2.173, server 192.168.2.1
udhcpc: lease of 192.168.2.173 obtained from 192.168.2.1, lease time 43200

尝试成功之后,我们可以修改docker-compose.ymlserviceendpoint字段,让容器在启动时自动运行udhcpc来获取IP。
并且推荐在路由器上将这个IP设置为静态IP,和容器MAC绑定,方便我们下面做宿主机的MacVLAN Link。

宿主机添加MacVLAN Link

完成上面一步之后,我们会发现在局域网的其他设备可以通过容器IP访问容器的服务,但是在宿主机上不能通过容器IP访问容器。
这里我们需要配置MacVLAN Link来解决这个问题。
打开/etc/systemd/network文件夹,添加以下两个文件:

30-macvlan0.netdev

[NetDev]
Name=macvlan0
Kind=macvlan

[MACVLAN]
Mode=bridge

30-macvlan0.network

[Match]
Name=macvlan0

[Route]
Destination=192.168.2.173
Gateway=192.168.2.1
Metric=200

这里要注意,在.network文件里,Destination只能为容器IP,不然可能路由表会出错。
并且配置合适的Metric路由跃点。

最后,修改已有的DHCP接口配置,可能是这样的:
20-wired.network

[Match]
Name=enp2s0

[Network]
DHCP=yes
MACVLAN=macvlan0

[DHCP]
RouteMetric=100

在其中Network部分添加MACVLAN字段。
最后重启systemd-networkd服务,宿主机应该就可以访问容器IP了。
sudo systemctl restart systemd-networkd

WireGuard的简单使用,以及自动Mesh相关想法

安装WireGuard

因为我手上的机器都是 ArchLinux,并且内核版本均>5.6,因此只需要简单地开启WireGuard模块并下载工具即可。

sudo modprobe wireguard
sudo pacman -S wireguard-tools

配置WireGuard

这里主要使用配置文件进行WireGuard的配置。
参考:

[Interface]
Address = 192.168.233.201/24
ListenPort = 45678
PrivateKey = VPS1 Private Key
PostUp = iptables -I FORWARD -i wg0 -j ACCEPT; iptables -I FORWARD -o wg0 -j ACCEPT; iptables -I INPUT -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -D INPUT -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
SaveConfig = true

[Peer]
PublicKey = VPS2 Public Key
AllowedIPs = 192.168.233.20/32
Endpoint = 123.117.181.233:6658
PersistentKeepalive = 25

其中,若Peer在NAT/Firewall后,可能需要与Peer保持链接,这时就需要配置PersistentKeepalive配置。
这里给出的配置文件只是一台机器,另一台机器换一换Interface.PrivateKey和对应的Peer.PublicKey等参数即可。

最后,将配置文件放入/etc/wireguard/文件夹内即可,如/etc/wireguard/wg0.conf
使用下面命令可以启动wg0,并配置其自启动:

sudo systemctl start wg-quick@wg0
sudo systemctl enable wg-quick@wg0

自动Mesh设想

这里发现了一个工具叫wgsd,它是使用CoreDNS来进行Peer连接信息的共享。
主要原理为:
1. 建立一台Register注册服务器
1. 两台不同NAT后的节点均配置好Register服务器的Peer信息,并成功建立连接
1. Register使用DNS服务器去记录两个Peer的Endpoint信息
1. 两台Peer分别通过Register的DNS服务,请求到另一个PeerEndpoint信息,并加载
1. 两台不同NAT后的节点实现互联

因此,这个自动Mesh的主要机制就是实现一个自动注册、自动分发Endpoint的服务。

关于内网的互联

我们可以通过配置WireGuard以及设置iptablesip route来实现某台服务器内网的互联。
假设:
* 服务器A
* WireGuard IP:10.0.0.1
* 服务器B
* WireGuard IP:10.0.0.2
* 内网网段:192.168.0.0/24

为了实现服务器A访问192.168.0.20上的服务,我们需要以下几个步骤:
1. 在服务器APeer中,为服务器B所在的PeerAllowedIps增加一个网段192.168.0.0/24
1. 在服务器A上增加到192.168.0.0/24的路由:ip route add 192.168.0.0/24 via 10.0.0.2

当然,服务器B上的iptables也是需要配置的,具体可以参考上面配置文件的PostUp字段。

QNAP NAS:安装Emby,使用MPD配合upmpdcli实现本机DLNA播放

概述

  • 安装一个影音管理软件,将NAS中的影片、音乐都统一管理起来,并且从网上自动获取影片、音乐的元数据
  • NAS直连音箱,使得NAS可以直接播放音乐,不需要开电脑

安装 Emby

由于我的机器是TS-532X,可能QNAP从App Center把Emby删掉了,只能从Emby官方网站去下载安装。
Emby Download | QNAP Version
选好适合自己机器架构的软件包,下载,通过App Center安装就行。

安装 MPD-UPNP镜像

为了能够通过NAS直接播放音乐,我们需要安装mpd。而QNAP的机器最好还是使用容器进行安装。
在此之外,为了使用DLNA功能,将我们的NAS改造成一个DLNA播放器(相当于给音箱上了个DLNA),我们还需要一个UPNP Render客户端。
这里,我选择自己Build一个mpd + upmpdcli的镜像来实现。
mpd-upnp镜像
Container Station里可以直接在Docker hub的源上搜索到该镜像。
比较重要的点有:
1. 开放端口
* 6600/tcp
* 49153/tcp
2. 共享主机设备
* Sound and ALSA (14, 116)
* 注意,务必打开容器的授权模式
3. 挂载共享文件夹
* /config -> /share/Container/config/mpd
* 这里的目标文件夹可以指定为你自己的文件夹

配置 MPD-UPNP

该镜像主要需要两个配置:
1. mpd.conf

bind_to_address                 "any"
port                            "6600"
password                        "yourpassword@read,add,control,admin"
max_playlist_length             "65535"
max_command_list_size           "65535"

volume_normalization            "yes"

audio_output {
    type            "alsa"
    name            "KAB-001"
    mixer_type      "hardware"      # optional
    mixer_control   "Speaker"
}
  1. upmpdcli.conf
pidfile = /var/run/upmpdcli.pid
friendlyname = Ricky-NAS
upnpav = 1
openhome = 0
lumincompat = 0
saveohcredentials = 1
checkcontentformat = 1

mpdhost = localhost
mpdport = 6600
mpdpassword = yourpassword
mpdtimeoutms = 2000
ownqueue = 1

保存好配置文件之后,直接启动容器即可。

Raspberry Pi 4:将树莓派作为网桥使用

概述

由于家里网线布局问题,只有一条网线能够到达我卧室,但是PC和树莓派均需要网线上网,因此,一个奇怪的需求诞生了:

  • 给树莓派加一个USB网卡
  • 将USB网卡和树莓派自身网卡桥接
  • PC直连树莓派USB网卡,树莓派网卡直连路由器
  • PC通过树莓派的网卡桥接进行上网

需求

  • 树莓派
  • USB网卡
  • 网线

准备操作

首先,我们可以将树莓派连上Wifi,防止在之后的网络真空期不能使用SSH连接树莓派。

创建网桥

这里,我们将直接通过配置文件的方式,在/etc/systemd/network文件夹下创建多个文件,让树莓派在重启后能够自动创建相关的网络设备。

  • br-lan.netdev:自动创建网桥
[NetDev]
Name=br-lan
Kind=bridge
  • br-lan.network:配置网桥的网络,打开DHCP
[Match]
Name=br-lan
[Network]
DHCP=yes
DNSSEC=no
  • en.network:USB网卡接入网桥
[Match]
Name=en*
[Network]
DHCP=no
DNSSEC=no
Bridge=br-lan
  • eth.network:树莓派网卡接入网桥
[Match]
Name=eth*
[Network]
DHCP=no
DNSSEC=no
Bridge=br-lan

完成以上几个文件后,在树莓派重启后将会自动完成网桥的建立。但是,现在先别重启,还需要对iptables进行转发配置。

备注

树莓派接入更多网卡之后,可能需要编辑/usr/lib/systemd/system/systemd-networkd-wait-online.service,忽略树莓派本身网卡eth0和USB网卡:

ExecStart=/usr/lib/systemd/systemd-networkd-wait-online --ignore eth0 --ignore enp1s0u1u2

不然可能出现systemd-netword-wait-online启动超时的问题。

iptables

增加网桥转发规则,并持久化

# iptables -I FORWARD_direct -i br-lan -o br-lan -j ACCEPT
# iptables-save > /etc/iptables/iptables.rules
# systemctl enable iptables

如果有IPv6环境,也需要进行配置:

# ip6tables -I FORWARD_direct -i br-lan -o br-lan -j ACCEPT
# ip6tables-save > /etc/iptables/ip6tables.rules
# systemctl enable ip6tables

结果

最后,重启树莓派,我们的电脑就能够通过树莓派连接到路由器,通过路由器的DHCP获得IP地址上网了,同时,树莓派上的网桥br-lan也能够通过DHCP获取到树莓派的IP地址。

参考

Raspberry Pi 4:使用bluez和bluealsa,将树莓派改造为蓝牙音箱

概述

家里有个不带蓝牙的音箱,而带蓝牙的小爱同学Mini又居然是单声道,因此,为了让音箱能连上蓝牙,只能掏出我珍藏已久的树莓派4了。
我的Raspberry Pi 4装的是ArchLinux AArch64,至于别的发行版不是很清楚,不过应该可以作为参考使用

安装

bluez

可以使用pacman或者yay等其他包管理直接安装(官方源就有)

bluez-alsa-git

这个包仅在AUR源有,我使用的是yay,因此能够直接安装。
也可以通过以下步骤手动编译安装:

  • git clone https://aur.archlinux.org/bluez-alsa-git.git
  • makepkg -sri

配置

config.txt

树莓派默认是未启动Bluetooth蓝牙和Audio音频功能的,我们需要在/boot/config.txt内增加以下语句来打开蓝牙和音频,并强制将音频输出改为3.5mm接口:

dtparam=krnbt=on,audio=on
audio_pwm_mode=2
hdmi_drive=1
hdmi_ignore_edid_audio=1

bluez

首先,我们需要设置bluetooth.service为自启动:

  • sudo systemctl enable bluetooth

然后,编辑/etc/bluetooth/main.conf,修改以下选项来实现蓝牙设备的开机自启:

[General]
Name = RaspberryPi4     # 可以改为自己的设备名称
DiscoverableTimeout = 0
Discoverable = true     # 默认开启可发现模式(实测可能没用)
[Policy]
AutoEnable = true       # 自动启动蓝牙设备

bluez-alsa

接下来,我们需要对bluez-alsa进行配置。
编辑/etc/default/bluealsa

OPTIONS="-p a2dp-source -p a2dp-sink -p hfp-hf -p hfp-ag -p hsp-hs -p hsp-ag"

这一步的作用是设置bluez-alsa的默认支持配置。
之后,我们编辑/etc/dbus-1/system.d/bluealsa.conf,在其中的<policy user="root">下增加两行:

<allow send_destination="org.bluealsa.sink" />
<allow send_destination="org.bluealsa.source" />

然后我们设置bluez-alsa自启动:

sudo systemctl enable bluealsa.service

为了能够让bluealsa-aplay自启动,我们还需要增加一个/usr/lib/systemd/system/bluealsa-aplay.service服务:

[Unit]
Description=Bluealsa Aplay daemon
Documentation=https://github.com/Arkq/bluez-alsa/
After=bluealsa.service
Requires=bluealsa.service

[Service]
Type=simple
ExecStart=/usr/bin/bluealsa-aplay
Restart=on-failure

[Install]
WantedBy=bluetooth.target

这个服务用于将我们蓝牙传输的音频,发送到ALSA设备:

sudo systemctl enable bluealsa-aplay

最后,重启树莓派。

连接蓝牙

树莓派重启后,先给自己当前用户增加bluez的用户组lp

sudo usermod -a -G lp ricky

通过bluetoothctl,进行蓝牙配置:

$ sudo bluetoothctl
[Bluetooth] discoverable on

然后手机搜索并连接树莓派蓝牙,期间会出现几个(yes/no)的询问,一致yes就行。
最后,设置信任设备:

[Bluetooth] trust xx:xx:xx:xx

这样,我们就可以将手机通过蓝牙连接到树莓派播放音乐了。

参考

Celery:Prefetch与Acknowledged相关配置

概述

首先,我们要了解什么是Celery的PrefetchAcknowledged
顾名思义,Prefetch指的是一个Celery Worker节点,能够提前获取一些还还未被其他节点执行的任务,这样可以提高Worker节点的运行效率。
Acknowledged则是一个任务执行完后,只有确认返回发送了Acknowledged确认信息后,该任务才算完成。

Prefetch Limit

在这里,Celery给我们提供了一个节点级别的变量,worker_prefetch_multiplier,用以控制一个工作节点所能预取的任务倍数。这里倍数的意思是:该节点可同时执行的任务数量N倍(默认设置是4倍)。即,一个并行100个任务的节点,它默认能够预取的任务数量是400
在这里,我们可以根据我们的任务性质,对其进行一些改动。
假如一个节点运行的任务以长时间任务为主,那么该节点可以设置worker_prefetch_multiplier1,即仅预取1倍的未运行任务
若是一个节点以短时间任务为主,那么该节点可以增大worker_prefetch_multiplier的值(如64),这样能够提高节点的效率。
而在这里,很多人可能会疑惑,假如一个100并发的节点,worker_prefetch_multiplier=4,那么该节点最大能够保留多少正在执行与未执行的任务呢?
这里就涉及到Acknowledged相关的知识了。

Acknowledged

Acknowledged机制是设计用来确定一个任务已经被执行/已完成的。
Celery默认的ACK行为是,当一个任务被执行后,立刻发送Acknowledged信息,标记该任务已被执行。
比如一个任务被节点执行后,节点发送Acknowledged信号标记该任务已被执行。结果执行过程中该节点出现由断电、运行中被结束等异常行为,那么该任务则不会被重新分发到其他节点,因为该任务已经被标记为Acknowledged了。
以上是Celery的默认Acknowledged机制,而我们有时候需要一个任务确实被一个节点执行完成后才发送Acknowledged消息,这就需要提到一个配置task_acks_late
当我们设置一个节点为task_acks_late=True之后,那么这个节点上正在执行的任务若是遇到断电,运行中被结束等情况,这些任务会被重新分发到其他节点进行重试。
注意:要求被重试的任务是幂等的,即多次运行不会改变结果
同时,Celery还提供了一个Task级别的acks_late设置,可以单独控制某一个任务是否是采用Acknowledged Late模式的。

Prefetch与Acknowledged

回到上面的问题,当一个节点的Acknowledged是Celery默认模式(提前确认)时,一个节点的任务组成是这样样子的:
100个正在执行中的任务(已确认) + 400个预取任务(未确认)
而若我们将节点的Acknowledged设置为Late Acknowledged模式,那么一个节点的任务组成会变为:
100个正在执行中的任务(未确认) + 300个预取任务(未确认)
也就是说,假如我们需要一个节点仅保留它正在运行的任务,而不需要任何预取的时候,我们需要设置该节点为Late Acknowledged模式(task_acks_late=True),并把预取倍率调为1worker_prefetch_multiplier=1

参考

Celery 与 RabbitMQ:关于Exchange和Queue的那些事

RabbitMQ

RabbitMQ的消息流主要由两个部分组成:ExchangeQueue

Exchange

Exchange其实可以类比为一个交换机?其根据Exchange的类型和一些规则,来将消息分发到特定的Queue队列中。
RabbitMQ支持以下几种Exchange类型

Direct Exchange

将每个Message(消息)的routing_key与其下的Queue进行匹配,若一致则将Message下发到对应的Queue

Default Exchange

RabbitMQ预定义的一个Exchange,当一个MessageExchange空字符串时,就会交由这个Default Exchange去处理。且该Message会被路由到和它routing_key同名的Queue去。
每个Queue都与Default Exchange进行绑定,绑定的routing_keyQueue的名称

Topic Exchange

  • routing_key.分割成多个单词
  • *匹配一个单词
  • #匹配一个或零个单词
  • test.*匹配test.fun,不匹配test
  • test.#匹配test.funtest

Fanout Exchange

Message下发到其下的所有Queue里(相当于一次针对Queue的广播)

Headers Exchange

Topic Exchange类似,但是是按header参数进行匹配。
ExchangeQueueBinding中有一个特殊参数x-match,用来表示需要全部参数匹配还是部分参数匹配
* x-match = allheader的所有参数都要和Binding的条件匹配才行,默认
* x-match = anyheader只要有一个参数与Binding的条件匹配就行

若出现多个匹配的Queue,则将消息分发到每个匹配的Queue

Dead Letter Exchange

用于捕捉没有匹配到Queue的所有Message

Celery

Celery的配置有两个参数,分别是task_routestask_queues
当仅指定task_routes,会自动合适的生成QueueExchange,但是很多时候我们想要对ExchangeQueue进行定制化,这就到task_queues出场的时候了。

task_queues

task_queues的默认值为None,但若是我们需要对Queue进行定制化,则需要给他赋值:

app.conf.task_queues = [
    Queue(...),
    Queue(...)
]

task_routes

task_routes是一个用于描述task.name(任务名称)和Queue(队列)关系的字典。
当一个任务的task.name符合(完全匹配、正则匹配等)某个key时,该任务就会被分发到queue名称相对应的Queue

app.conf.task_routes = {
    'test': {'queue': 'test_queue'},
    'test.*' {'queue': 'test_prefix_queue', 'routing_key': 'test.a'},
    re.compile(r'\d{10}'): {'queue': '10_digits_queue'}
}

注意一点,这里的key仅用于和Celerytask.name进行匹配,而不是最后生成Messagerouting_key
最后生成的routing_key是由在task_queues里的Queue()对象进行指定的。

Exchange

用于生成特定类型的Exchange

from kombu import Exchange
direct_exchange = Exchange(name='direct_exchange', type='direct')
fanout_exchange = Exchange(name='fanout_exchange', type='fanout')
topic_exchange = Exchange(name='topic_exchange', type='topic')
headers_exchange = Exchange(name='headers_exchange', type='headers')

其还支持以下参数:
* durablt:在服务器重启后,是否保留该Exchange,默认为True
* auto_delete:在其下的所有Queue都完成之后,自动删除Exchange,默认为False
* delivery_mode:默认的消息分发方式
* 1:消息仅保留在内存里,重启丢失
* 2:消息保存在内存和硬盘里,重启不丢失,默认值
* arguments:其他一些参数(比如Headers Exchangex-match参数)

Queue

用于生成特定的Queue

from kombu import Queue
Queue(name='test', exchange=direct_exchange, routing_key='test')
Queue(name='topic.a', exchange=topic_exchange, routing_key='fanout.*')
Queue(name='fanout_queue', exchange=fanout_exchange)

Queue主要有以下参数:
* name:队列名称
* exchange:队列绑定的Exchange
* routing_key:队列绑定时采用的routing key,根据不同类型的Exchange而不同
* durable:服务器重启后是否保留队列,默认为True
* auto_delete:当所有的消费者完成之后,是否自动删除队列,默认为False
* expires:当队列不再被使用(没有任何消费者,没有被重新声明)一定时间后,自动删除队列
* message_ttl:队列内消息的ttl
* max_length:队列能够储存的消息总数量的上限
* mex_length_bytes:队列能储存的消息总大小的上限
* max_priority:队列的最大优先级

优先级相关

RabbitMQ原生支持优先级设定,但是默认不开启优先级
我们需要在声明Queue队列的时候,指定x-max-priority参数,才能开启优先级功能。
优先级范围是0 ~ 255,越大优先级越高,若x-max-priority设置越大,则越消耗服务器计算资源。

from kombu import Queue
Queue(name='test', exchange=direct_exchange, routing_key='test', max_priority=10)

这里的优先级其实指的是,在一个队列里,优先级高的消息先被消费。
若在Celery里,一个Task在没有指定priority,则该消息的优先级为0

参考

GitHub Action使用杂记

GitHub Action

这是GitHub推出的一个类似Azure Pipeline的工具。GitHub Action
这里记录在初次使用GitHub Action时的一些小技巧。

设置环境变量

目前的GitHub Action的环境变量,若是在stage里进行export等操作,那么该环境变量是不会继承到下一个stage里的。若需要一个全局环境变量,则要按下面的方式进行设置。

设置GOPATH

在官方默认的GO环境中,仅设置了GOROOT环境变量,而我们常用的GOPATH并不存在。针对这个情况,我们可以手动设置GOPATH环境变量:

name: Go
on:
  schedule:
  - cron: 0 0 * * 3
jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
    - name: Set up Go 1.12
      uses: actions/setup-go@v1
      with:
        go-version: 1.12
      id: go
    - name: Set GOPATH
      run: |
        echo "##[set-env name=GOPATH;]$(dirname $GITHUB_WORKSPACE)"
        echo "##[add-path]$(dirname $GITHUB_WORKSPACE)/bin"
      shell: bash

关键点就在Set GOPATH这个step,通过set-env操作设置GOPATH

其他环境变量

针对其他环境变量,我们也可以使用类似的语句进行设置:

echo "##[set-env name=TAG_NAME;]$(date +%Y%m%d)"
echo "##[set-env name=NAME;]$(date +%Y%m%d)"

GitHub Secrets

在工作流中,我们不免需要设置一些敏感的token, password等数据,这时候就可以使用Github提供的Secrets功能。
当我们在Secrets里设置了所需的数据之后(注意:现在的Secret名称不支持以GITHUB_开头,如GITHUB_TOKEN),就可以在配置文件中通过${{ secrets.TOKEN }}的形式进行引用。

发布Release

目前我有两个项目需要每周定时构建,进行一次Release并发布数据文件。而GitHub Action官方暂时没有支持Release操作的容器,我们需要在Marketplace搜索合用的Action
我常用的一个Release相关的Actionopspresso/action-release。而这个Action有个问题,它自己的Docker镜像缺少了file命令,因此我fork了一份并替换了基础镜像。Ricky-Hao/action-release

使用方法

    - name: Release
      uses: Ricky-Hao/action-release@master
      env:
        GITHUB_TOKEN: ${{ secrets.TOKEN }}
        ASSET_PATH: release
        TAG_NAME: Latest
        NAME: Latest

只要在最后加上一个如上的Release step就行。其中:
* GITHUB_TOKENToken-Github,若token仅需要发布Release,那么只要给public repos权限即可。
* ASSET_PATH:发布的文件目录
* TAG_NAME:发布的Tag名称
* NAME:发布的Release名称

Action还有其他环境变量选项:
* TARGET_COMMITISH:设置创建Tag的特定commit,默认是master
* BODY:描述Tag的文本
* DRAFT:设置为true,则该次发布为草稿形式,默认为false
* PRERELEASE:设置为true,则该次发布为预发布形式,默认为false

发布到PYPI

我有个V2ray.Stats项目,是用于记录V2Ray的流量信息并定期发送邮件的。该项目每个版本会发布在PYPI,因此我需要一个Action进行自动发布。
在这里,有一个很好用的Actionross/python-actions,它内置了twine,可以根据PYPI的用户名和密码进行自动发布。

    - name: Publish
      uses: ross/python-actions/twine@master
      env:
        TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
        TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }}
      with:
        args: "upload --skip-existing ./dist/*"

这里的args参数就是指定运行该Action时的CMD(每个Action其实都是一个Docker容器)。

GitHub Action Budget

GitHub还提供了Action的Budget给我们使用。

[![Actions Status](https://github.com/Ricky-Hao/V2Ray.Stats/workflows/Build/badge.svg)](https://github.com/Ricky-Hao/V2Ray.Stats/actions)

Actions Status

创建Action

每个GitHub Action其实都是一个Docker镜像,通过Dockerfile设定好运行环境,提供一个entrypoint.sh就可以运行。
其中,step.with.args这个参数其实就是提交给容器的CMD,因此不能在这里使用bash命令。

参考文档

MongoDB Shard集群大批量数据导入优化

0x00

起因是需要往MongoDBShard分片集群导入两个T(十亿文档)级别的数据。其中遇到了各种很严重的性能问题,在这里做一下记录。

0x01 索引

在导入数据时,尽量不提前建立索引,仅建立必须的shard_key

0x02 批量写入

在进行大批量导入时,尽量使用insert_many进行批量写入。若写入顺序不重要的话,可以设置ordered=False来提高速度。

0x03 SHARD_FILTER问题

若从一个Shard集群读取数据,并且读取的规则为Secondary Preferred,在查询时会缺少SHARD_FILTER阶段。
在项目中遇到一个问题:集群中两个分片都保有同一条数据。
在这种情况下,若是没指定Primary Preferred,则可能造成一条数据返回两次的情况。而经过了SHARD_FILTER阶段,会自动过滤掉多余的数据。

0x04 其他问题

  • 假若分片不均,可以使用hashed索引作为分片索引
  • 假若导入的集合已存在使用过的索引,那么这个索引存在缓存,可能会导致分片不均。此时删除重建即可。
  • 使用insert_many批量导入完成之后,可能会存在文档数量过多的情况,这是正常的,等待数据库自动平衡去重即可。

Linux 关闭USB Auto Suspend

起因

入了个硬盘盒配合树莓派做NAS,发现有时候dmesg里会出现硬盘相关的错误,如:Aborting journal on device dm-1-8.
在网上搜了一波,发现有可能是由于Linux自己的USB Auto Suspend机制导致。

关闭USB Auto Suspend

编辑文件/etc/modprobe.d/usb-disable-autosuspend.conf
文件内容:

options usbcore autosuspend=-1

参考

Kernel-USB-PowerManagement