Chemmy's Blog

chengming0916@outlook.com

一、问题背景与诊断

在某些云服务器环境(如本文提到的宿迁服务器)中,运营商可能会屏蔽标准的 NTP 端口(如 UDP 123),导致系统默认的时间同步服务失效。本文将介绍两种解决方案。

1.1 检查当前时间同步状态

首先,使用以下命令查看系统时间与同步状态:

1
timedatectl status

关键输出解读

  • **Local time**:本地时间(时区转换后)。
  • **Universal time**:协调世界时(UTC)。
  • System clock synchronized核心指标。显示 yes 表示已同步,no 表示未同步。
  • **NTP service**:显示当前活跃的时间同步服务(activen/a)。

如果 System clock synchronized 显示为 no,则需要按照本指南进行配置。

1.2 设置正确的时区(可选但推荐)

确保系统时区设置为中国标准时间:

1
sudo timedatectl set-timezone Asia/Shanghai

二、方法一:使用系统自带的 systemd-timesyncd(推荐)

Ubuntu 16.04 及更高版本默认使用 systemd-timesyncd 服务进行时间同步。它轻量、集成度高,是首选方案。

2.1 配置自定义 NTP 服务器

编辑配置文件:

1
sudo nano /etc/systemd/timesyncd.conf

找到 [Time] 部分,取消 NTP= 行的注释,并填入自定义的 NTP 服务器地址。可以指定多个,用空格分隔。

1
2
3
[Time]
NTP=ntp.aliyun.com ntp.tencent.com
#FallbackNTP=0.debian.pool.ntp.org 1.debian.pool.ntp.org 2.debian.pool.ntp.org 3.debian.pool.ntp.org

常用国内 NTP 服务器

  • 阿里云:ntp.aliyun.com
  • 腾讯云:ntp.tencent.com
  • 国家授时中心:cn.pool.ntp.orgntp.ntsc.ac.cn

2.2 重启服务并启用

1
2
3
4
5
6
# 重启 timesyncd 服务
sudo systemctl restart systemd-timesyncd
# 设置开机自启
sudo systemctl enable systemd-timesyncd
# 查看服务状态
sudo systemctl status systemd-timesyncd

2.3 验证同步结果

等待几分钟后,再次运行 timedatectl status。当 System clock synchronized 变为 yes 时,表示配置成功。

重要提示:同步不是即时的,需要等待服务完成一轮时间同步(通常几分钟内)。


三、方法二:安装并配置 ntp 服务

如果 systemd-timesyncd 因故无法工作(例如,安装了其他时间服务导致冲突),可以安装传统的 ntp 包。

3.1 安装 NTP 软件包

1
2
sudo apt update
sudo apt install ntp

安装过程会自动停止并禁用 systemd-timesyncd 服务。

3.2 配置 NTP 服务器

编辑 NTP 主配置文件:

1
sudo nano /etc/ntp.conf

找到 serverpool 配置行。注释掉默认的 pool *.debian.pool.ntp.org 行,添加自定义的服务器。

1
2
3
4
5
6
7
8
# 默认池(可注释掉)
# pool 0.debian.pool.ntp.org iburst
# pool 1.debian.pool.ntp.org iburst

# 添加自定义服务器(例如阿里云和腾讯云)
server ntp.aliyun.com iburst
server ntp.tencent.com iburst
server cn.pool.ntp.org iburst
  • iburst 选项可以在服务启动时快速进行初始同步。

3.3 重启 NTP 服务

1
2
3
sudo systemctl restart ntp
sudo systemctl enable ntp
sudo systemctl status ntp

3.4 检查 NTP 同步状态

使用 ntpq 工具查看与上游 NTP 服务器的连接状态:

1
ntpq -p

输出关键列解读

  • **remote**:NTP 服务器地址。
  • **refid**:服务器本身参考的上一级时间源。
  • **st**:层级(stratum),数值越小越接近权威时间源。
  • **t**:类型(u=单播,p=池)。
  • **when**:距上次查询的秒数。
  • **poll**:查询间隔(秒)。
  • **reach**:八进制表示的连接成功历史(值 377 表示最近8次查询全部成功)。
  • **delay**:网络延迟(毫秒)。
  • offset:时间偏移量(毫秒),关键指标,绝对值越小越好。
  • **jitter**:时间抖动(毫秒)。

理想状态:至少有一个服务器前有 *(当前主同步源)或 +(良好的备用源),且 reach 值不为 0

3.5 手动强制同步(可选)

如果自动同步不理想,可以手动触发:

1
sudo ntpdate -s ntp.aliyun.com

注意ntpdate 可能与正在运行的 ntpd 服务冲突。更安全的方式是重启 ntp 服务或使用 ntpd -gq(需先停止服务)。


四、故障排除与高级管理

4.1 查看详细的系统日志

1
2
3
4
# 查看 systemd-timesyncd 日志
sudo journalctl -u systemd-timesyncd
# 查看 ntp 服务日志
sudo journalctl -u ntp

4.2 处理服务冲突

如果同时安装了多个时间服务(如 chronyntpsystemd-timesyncd),它们会相互冲突。确保只启用一个:

1
2
3
4
# 禁用 systemd-timesyncd(如果使用 ntp)
sudo systemctl disable --now systemd-timesyncd
# 禁用 ntp(如果使用 systemd-timesyncd)
sudo systemctl disable --now ntp

4.3 检查硬件时钟(RTC)设置

确保硬件时钟设置为 UTC,避免双系统时间混乱:

1
2
3
4
# 查看当前设置
timedatectl | grep “RTC in local TZ”
# 如果显示 “yes”,则设置为 UTC
sudo timedatectl set-local-rtc 0

4.4 防火墙配置

如果使用自定义防火墙(如 ufw),确保放行 NTP 流量(UDP 123端口):

1
sudo ufw allow out 123/udp

五、总结与建议

特性 systemd-timesyncd (方法一) ntp (方法二)
复杂度 简单,集成于 systemd 稍复杂,功能更全
配置 编辑 /etc/systemd/timesyncd.conf 编辑 /etc/ntp.conf
控制 systemctl systemctlntpq
适用场景 大多数桌面和服务器,简单同步 需要更精确时间、复杂网络或作为 NTP 服务器

操作流程建议

  1. 首选方法一:配置 systemd-timesyncd。它更现代、更简单。
  2. 如果方法一失败:再考虑安装 ntp。安装前可尝试卸载冲突软件:sudo apt remove ntp chrony
  3. 验证:无论哪种方法,最终都以 timedatectl status 显示 System clock synchronized: yes 为准。
  4. 耐心等待:时间同步服务需要周期运行,配置后请等待数分钟再验证。

本文参考了 OPNsense中文手册OPNsense 防火墙系列一 等优秀资料。

一、简介与核心概念

1.1 什么是 OPNsense?

OPNsense® 是一个基于 FreeBSD开源、易用且易于构建的防火墙和路由平台。它集成了商业防火墙中的大部分高级功能,同时具备开源项目的透明性和可验证性。

使命:为用户、开发人员和企业提供友好、稳定和透明的环境,使其成为使用最广泛的开源安全平台。

1.2 核心功能概览

  • 防火墙与路由:状态检测防火墙、高级路由策略。
  • VPN支持:IPsec、OpenVPN(站点到站点、移动用户)。
  • 网络服务:DHCP/DHCPv6服务器、DNS转发器/解析器、动态DNS。
  • 高级功能
    • 流量整形与策略路由。
    • 入侵检测与防御(IDS/IPS)。
    • 正向缓存代理(Squid)。
    • 强制门户(多区域、凭证支持)。
    • 高可用性与硬件故障转移(含配置同步)。
  • 管理与监控:内置报告工具、RRD图表、数据包捕获、配置备份与还原。

1.3 重要注意事项(避坑指南)

在开始前,请务必了解以下关键点:

  1. NAT类型:OPNsense默认不支持全锥型NAT(NAT1),最高支持端口限制型NAT。可通过UPnP或端口转发缓解,但更推荐后者。
  2. 性能影响
    • PPPoE拨号等变动IP的接口上启用入侵检测,可能导致CPU占用过高。
    • 流量分析、图表等功能在异常断电后可能数据损坏,同样会引起高CPU占用。
  3. 网络规划:前期规划至关重要!随意添加网卡会导致标识符设备映射变动,影响高可用等功能的稳定性。
  4. 虚拟化平台选择
    • 强烈推荐使用PVE。在ESXi上运行高可用可能出现DUP!问题,且内存占用计算不准确。
    • ESXi上开启端口组混杂模式可解决DUP!,但会导致OpenWrt无法访问OPNsense,且影响Docker MacVlan。
  5. 硬件兼容性
    • 部分Intel 2.5G网卡存在BUG,可能导致无法发出DHCP Offer,请避免使用。
    • VLAN配置在物理机上可能存在问题。

二、安装准备与硬件要求

2.1 准备工作

  • 两台电脑(用于测试,双网口以上)。
  • 存储:大于20GB(推荐50GB)。
  • 其他:RJ45网线、显示器和键盘。

2.2 硬件要求参考

以下为作者环境(i5-1135G7, 64G DDR4, 1T NVMe, ESXi 8)下的占用情况:

场景 处理器占用 (ESXi) 内存占用 (ESXi/OPNsense) 存储
轻负载 761 MHz 16G / 7% 120G
满速下载 (92MB/s) 4.6 GHz 16G / 高 120G

2.3 官方硬件要求指南

等级 处理器 内存 存储目标
最低要求 500 MHz 单核 512 MB 4GB SD/CF卡 (使用nano镜像)
合理要求 1 GHz 双核 1 GB 40 GB SSD
推荐要求 1.5 GHz 多核 4 GB 120 GB SSD

注意:实际需求取决于吞吐量启用功能。例如,Squid代理、大型状态表、高并发强制门户会显著增加CPU和内存消耗。推荐使用英特尔(Intel)网卡以获得最佳性能和稳定性。

系统架构:支持 x86-32 (i386)x86-64 (amd64)。对于新部署,强烈建议使用64位版本


三、安装与基础配置

3.1 下载镜像

  1. 访问 OPNsense官网下载页
  2. 选择:Architecture: amd64 -> Image Type: DVD -> Mirror: AivianPeking University

3.2 安装系统

  1. 在虚拟机或物理机上启动镜像。
  2. 在登录提示符 (login:) 处输入:用户名 installer密码 opnsense
  3. 选择键盘布局(默认回车即可)。
  4. 选择 Install (UFS)
  5. 选择目标磁盘(通常为 da0)。
  6. 按提示创建交换空间并确认安装。
  7. 安装完成后,设置root密码并重启。务必在重启前移除安装介质

3.3 初始设置向导

  1. 使用同一网段的设备访问 https://192.168.1.1
  2. 跟随向导进行设置:
    • 语言:中文。
    • DNS:首选 223.5.5.5,辅助 119.29.29.29
    • 启用DNSSEC支持强化DNSSEC数据(推荐,可防DNS劫持。自建DNS请勿开启)。
    • 时区Asia/Shanghai
    • 配置WAN/LAN接口
    • 重设root密码(可选)。
    • 重载系统完成配置。

3.4 更换软件镜像源

默认源速度较慢,建议更换:

  1. 进入 系统 -> 固件 -> 设置
  2. 镜像 下拉框中选择 Aivian (HTTPS,Shangxing,CN)
  3. 或自定义源:选择 (custom),填入:
    • 北京大学镜像源:http://mirrors.pku.edu.cn/opnsense/
    • 网易镜像源:http://mirrors.163.com/opnsense/

      注意:请勿自行改为 https

  4. 保存后,进入 状态 选项卡,点击 检查升级

四、网络接口配置

4.1 接口分配

进入 接口 -> 分配,查看和管理接口。

  • 标识符:如 WAN, LAN
  • 接口设备:系统识别的网卡名称(如 vmx0)。
  • 如需添加新接口(如WAN),在下方 Assign a new interface 选择设备并填写描述(英文/数字)。

4.2 WAN口设置

进入 **接口 -> [WAN]**。

  1. 基本设置:勾选 阻止私有网络拦截bogon网络
  2. IPv4配置类型
    • PPPoE拨号:选择 PPPoE,在下方的PPPoE配置中填入宽带账号密码。
    • DHCP:选择 DHCP
    • 静态IP:选择 静态IPv4,填写IP、掩码,并添加上游网关(标记为默认网关)。
  3. 保存

4.3 LAN口设置

进入 **接口 -> [LAN]**。

  1. 基本设置取消勾选 阻止私有网络拦截bogon网络
  2. IPv4配置类型:选择 静态IPv4
  3. 静态IPv4配置:填写内网IP地址(如 192.168.1.1)和子网掩码(如 24)。
  4. 保存

4.4 终端紧急配置网卡

如果不慎删除了管理口,可通过控制台恢复:

  1. 登录终端,进入管理选项菜单。
  2. 选择 1) 接口分配,按提示重新分配WAN、LAN等接口的物理网卡。
  3. 选择 2) 接口设置,为指定接口(如LAN)重新配置IP地址等。
  4. 按提示完成并重启相关服务。

五、防火墙规则配置

5.1 创建允许规则(示例:允许LAN访问外网)

进入 防火墙 -> 规则 -> LAN,点击 + 添加。

选项 设置
操作 通过
接口 LAN
方向 in
TCP/IP版本 IPv4
协议 any
LAN网络
目标 any
目标端口范围 any
描述 Default allow LAN to any rule

5.2 创建阻断规则(示例:禁止访问阿里DNS)

同样在LAN规则页面,点击 + 添加。

选项 设置
操作 阻止(或拒绝)
接口 LAN
方向 in
TCP/IP版本 IPv4
协议 any
LAN网络
目标 223.5.5.5 (单个主机或网络)
目标端口范围 53
描述 禁止访问阿里DNS

阻止:直接丢弃数据包。拒绝:返回拒绝数据包。


六、基础服务配置

6.1 系统DNS与Unbound DNS

  1. 系统DNS(网关DNS)
    • 进入 系统 -> 设置 -> 常规
    • 网络 部分的 DNS服务器 中填入公共DNS(如 223.5.5.5),并为其选择对应的WAN网关。
  2. Unbound DNS(局域网DNS)
    • 进入 服务 -> Unbound DNS -> 常规,勾选 启用Unbound启用DNSSEC支持
    • 进入 查询转发,取消 使用系统DNS服务器,手动添加转发服务器(如 223.5.5.5)。
    • DoT加密查询(可选):进入 DoT 选项卡,添加服务器,例如:
      • 服务器IP: 223.5.5.5
      • 端口: 853
      • 验证CN: dns.alidns.com

6.2 DHCPv4 服务

进入 **服务 -> DHCPv4 -> [LAN]**。

  • 勾选 启用
  • 设置地址池范围,例如:从 192.168.1.10192.168.1.245
  • 网关和DNS可不填,默认使用路由器地址。

6.3 网络时间协议(NTP)

进入 服务 -> 网络时间 -> 常规,可添加以下推荐的NTP服务器:

  • 国内:ntp.aliyun.com, ntp.tencent.com, ntp.sjtu.edu.cn, cn.pool.ntp.org
  • 国际:time.google.com, time.cloudflare.com, pool.ntp.org

七、IPv6 配置

7.1 公网IPv6(有PD前缀)

  1. WAN口接口 -> [WAN]IPv6配置类型DHCPv6。在DHCPv6客户端配置中,打开 仅请求IPv6前缀发送IPv6前缀提示前缀委派大小 设为 60(需与运营商下发一致)。
  2. LAN口接口 -> [LAN]IPv6配置类型跟踪接口,并在 跟踪IPv6接口 中打开 允许手动调整DHCPv6和路由器通告
  3. 启用IPv6防火墙 -> 设置 -> 高级,在IPv6选项中打开 允许IPv6
  4. 分配地址
    • DHCPv6服务 -> DHCPv6 -> [LAN] 中启用并设置范围。
    • 或路由器通告服务 -> 路由器通告 -> [LAN]路由器通告Assisted,勾选 通告默认网关,填入DNS服务器(如 2400:3200::1)。

7.2 NATv6(无PD前缀,使用内网IPv6)

  1. WAN口:同上,配置DHCPv6获取地址。
  2. LAN口接口 -> [LAN]IPv6配置类型静态IPv6,使用ULA地址(如 fd00::1/64)。
  3. 启用IPv6:同上。
  4. 配置NAT出站规则
    • 防火墙 -> NAT -> 出站,模式改为 混合生成出站NAT规则
    • 添加两条手动规则(IPv4和IPv6),关键是将 转换/目标 设为 接口地址,并勾选 静态端口。这是实现端口限制型NAT的关键。
  5. 分配地址:同7.1第4步,为LAN配置DHCPv6或路由器通告。

7.3 解决IPv6大包问题

test-ipv6.com 提示大包错误,需调整MSS:

  • 接口 -> [WAN] -> 通用配置 中,设置 MSS 值。
  • 计算公式:MSS = PPPoE MTU(通常1492) - 60,约为 1430。可尝试 14201400
  • 测试命令(Windows):ping -l 1500 baidu.com 逐步减小 -l 后数值直到通,该值即为可用MSS。

八、进阶功能配置

8.1 流量整形(流控)

以创建HTTP流量优先级为例:

  1. 创建管道防火墙 -> 流控 -> 管道,创建上传/下载管道,设置带宽(如 160Mbit/s上传,1000Mbit/s下载)。
  2. 创建队列防火墙 -> 流控 -> 队列,为不同协议创建队列并绑定到管道,设置权重(值越低优先级越高)。
    • 例如:HTTP上传队列 权重50, 其他流量队列上传 权重80。
  3. 创建规则防火墙 -> 流控 -> 规则,创建规则将特定流量(如目标端口80/443)指向高优先级队列。
  4. 防火墙 -> 流控 -> 状态 中查看队列状态。

8.2 别名(Aliases)

别名用于批量管理IP、端口等,是防火墙规则的强大工具。

  • 类型:主机、网络、端口、URL Table (IPs)(可定期更新)、GeoIP等。
  • 创建防火墙 -> 别名,点击 **+**,选择类型并填入内容(如IP列表、URL)。
  • 嵌套:可以在一个别名中引用其他别名。

8.3 端口转发

相比UPnP更安全可控。

  1. 普通转发防火墙 -> NAT -> 转发,点击 **+**。
    • 接口:WAN
    • 协议:TCP/UDP
    • 目标:WAN地址
    • 目标端口:外部端口
    • 重定向目标IP:内网主机IP
    • 重定向目标端口:内部端口
  2. DNS重定向示例:将内网DNS请求重定向到指定服务器。
    • 接口:LAN
    • 勾选 目标 / 反转
    • 目标:LAN网络
    • 目标端口:DNS
    • 重定向目标IP:192.168.1.2
    • 重定向目标端口:DNS

8.4 高可用(HA)配置

前提:两台OPNsense,接口分配完全一致,并有一个专用HA接口用于心跳同步。

  1. 配置CARP虚拟IP:在主防火墙的 接口 -> 虚拟IP -> 设置 中,为LAN(或WAN)添加CARP虚拟IP(如 192.168.1.1/24),设置VHID和密码。
  2. 配置同步
    • 主防火墙:系统 -> 高可用 -> 设置,启用同步,指定同步接口(HA口)、对端IP和密码,选择同步模块。
    • 备防火墙:同样位置,启用同步并勾选 禁用抢占断开拨号接口
  3. 同步配置:在主防火墙状态页点击上传图标进行初始同步。
  4. 检查:在 接口 -> 概况 中,主防火墙的虚拟IP旁有绿色图标,备防火墙为灰色。

8.5 性能优化(可调参数)

进入 系统 -> 设置 -> 可调参数,可添加以下参数提升性能(部分):

1
2
3
4
5
6
7
hw.ibrs_disable=1                # 禁用Spectre V2缓解,提升性能
net.isr.maxthreads=-1 # 使用所有CPU核心处理网络中断
net.isr.bindthreads=1 # 将中断线程绑定到核心
net.inet.rss.enabled=1 # 启用接收方缩放
net.inet.rss.bits=2 # RSS位,根据核心数调整(核心数/4)
net.inet.tcp.soreceive_stream=1 # 启用优化的内核套接字接口
kern.ipc.maxsockbuf=16777216 # 增大套接字缓冲区(10G网卡)

注意:调整需谨慎,建议参考 FreeBSD网络性能调优指南

8.6 网关组(故障转移/负载均衡)

  1. 配置网关系统 -> 网关 -> 配置,确保各WAN口网关监控状态为“正常”。
  2. 创建网关组系统 -> 网关 -> 组,点击 **+**。
    • 添加网关并设置优先级(故障转移),例如:WAN_GW为层级1,WAN2_DHCP为层级2
    • 触发条件:丢包
  3. 修改策略路由:在LAN的防火墙出站规则中,将网关选项改为新建的网关组。

九、故障排除

9.1 CSRF check failure 错误

登录后出现此白色错误页,通常是因为浏览器缓存或会话问题。

  • 解决方法:清除浏览器缓存和Cookie,使用无痕模式,或直接访问 https://192.168.1.1/(不带后续路径)。如仍不行,可尝试重启OPNsense的Web服务(非重启系统)。

9.2 网页反向代理设置

若通过Nginx等反向代理访问OPNsense管理界面,需在OPNsense中关闭某些检查:

  • 进入 系统 -> 设置 -> 管理
  • 勾选:禁用Web GUI重定向规则禁用DNS重绑定检查禁用HTTP_REFERER强制检查

9.3 开启SSH访问

  • 进入 系统 -> 设置 -> 管理
  • 勾选:启用安全Shell允许root用户登录允许密码登录
  • 监听接口:可选择全部或指定接口。

版权声明:本文部分内容参考并整合了多位社区作者的经验,特别感谢 OPNsense中文手册 和博主“鐵血男兒”的分享。本文旨在为初学者提供一个清晰的指引,更多深入细节请查阅官方文档和社区论坛。

解决SuperMicro主板风扇转速过低告警

现象

系统启动后风扇忽高忽低,进入IPMI后台可以看到,看到传感器日志里大量的告警

告警

造成此问题的原因是风扇转速过低,触发了超微的风扇转速允许的下限,从而强制满速运转,而在满速后主板又很快发现没有问题,且此时温度较低,风扇开始降速,直到降速到下限以下,重复此过程。

在进入IPMI后台管理界面后风扇速度有四种智能模式可调

  • Standard: zone0和zone1 风速为50%

  • Optimal: 风速为30%

  • Full: 风速为100%

  • Heavy IO: zone0 为50%,zone1 为75%

解决方法

1. 在服务器上安装IPMItool

1
2
apt install ipmitool # Ubuntu/Debian指令
yum install ipmitool # CentOS指令

2. 设置风扇转速

1
2
3
# 风扇名可以看告警里边的对应风扇名
ipmitool sensor thresh FAN1 lower 100 125 125
ipmitool sensor thresh FANA lower 100 125 125

3. Windows 下远程操作IPMI

ipmitool windows 版 下载地址

1
2
ipmitool -H [IPMI网口IP地址] -U [IMPI账户] -P [IPMI密码] sensor thresh FAN1 lower 100 125 125
ipmitool -H [IPMI网口IP地址] -U [IMPI账户] -P [IPMI密码] sensor thresh FANA lower 100 125 125

问题

如果你运行上面的命令后,风扇转速回落后马上又返回原样,这表明服务器的自动调速覆盖了你手动设置的转速。你需要切换服务器风扇策略为全速(Full Speed),在这个策略下服务器不会使用自动调节转速,因此也不会覆盖你手动设置的转速。

运行下面的命令切换到全速模式(也可以进入IPMI界面调整):

1
2
ipmitool -H [IPMI网口IP地址] -U [IMPI账户] -P [IPMI密码] 0x30 0x45 0x01 0x01
# 最后一个0x01表示全速模式。如果为0x00则表示标准(Standard);0x02表示最优(Optimal)

参考

1.解决超微 SuperMicro 主板风扇反复高低转速问题 - 哔哩哔哩 (bilibili.com)

超微服务器Supermicro X9/X10/X11设置风扇转速 - 辰宸的备忘录 (licc.tech)

ipmitool常用命令详解_ipmitool lan set_owlcity123的博客-CSDN博客

服务端配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[common]
# TCP通信端口
bind_port = 7000

#UDP通信端口
bind_udp_port = 7001

# 最大连接数
max_pool_count = 50

# 仪表板界面配置
dashboard_port=7500
dashboard_user=admin
dashboard_pwd=admin

# 允许使用的端口号,可以指定范围也可以用‘,’分割
allow_ports = 18081-18090,8080

服务端开机自启配置

1
2
3
sudo vim /etc/systemd/system/frps.service
sudo systemctl enable frps.service
sudo systemctl start frps.service

启动文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Unit]
Description = Frp Server Service
After = network.target

[Service]
Type = simple
User = nobody
Restart = on-failure
RestartSec = 5s
ExecStart = /usr/local/bin/frps -c /usr/local/etc/frp/frps.ini

[Install]
WantedBy = multi-user.target

客户端配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[common]
#替换IP地址为服务端IP
server_addr=0.0.0.0
server_port=7000

# windows远程桌面
[rdp]
type=tcp
# 映射IP
local_ip=127.0.0.1
# 映射端口(本地)
local_port=3389
# 远程端口(服务器)注意端口要在允许端口内切未被占用
remote_port=18087

准备工作

制作启动盘

官方镜像
Minimal CD Stage
国内加速
清华大学开源软件镜像站
中国科技大学开源镜像站

使用Rufus制作启动U盘.

image-20240829112011985

连接网络

Linux配置网络及SSH配置

分区规划

参考 Linux硬盘分区

挂载分区

1
2
3
4
5
6
7
8
mkdir /mnt/gentoo
mount /dev/sdx? /mnt/gentoo
mkdir /mnt/gentoo/home
mount /dev/sdx? /mnt/gentoo/home
mkdir /mnt/gentoo/boot
mount /dev/sdx? /mnt/gentoo/boot
mkdir /mnt/gentoo/boot/efi
mount /dev/sdx? /mnt/gentoo/boot/efi

配置Portage

释放stage

1
2
3
4
5
6
7
8
# 使用命令行浏览器下载stage
links http://www.gentoo.org/main/en/mirrors.xml

# 发送stage3
scp stage3-amd64-*.tar.xz root@192.168.0.2:/mnt/gentoo
cd /mnt/gentoo
# 释放stage3
tar xpvf stage3-*.tar.bz2 --xattrs-include='*.*' --numeric-owner

挂载系统必要环境

1
2
3
4
5
6
7
mount --types proc /proc /mnt/gentoo/proc
mount --rbind /sys /mnt/gentoo/sys
#mount --make-rslave /mnt/gentoo/sys (不使用systemd,所以注释掉)
mount --rbind /dev /mnt/gentoo/dev
#mount --make-rslave /mnt/gentoo/dev (不使用systemd,所以注释掉)
mount --rbind /run /mnt/gentoo/run
#mount --make-slave /mnt/gentoo/run (不使用systemd,所以注释掉)

复制DNS

1
cp --dereference /etc/resolv.conf /mnt/gentoo/etc/

配置软件源

常规源

选择中国源,这一步是非必须的,提供的/etc/portage/make.conf里已经有中国的所有源了

1
mirrorselect -i -o >> /mnt/gentoo/etc/portage/make.conf   
创建主仓库
1
2
3
4
5
6
7
8
9
10
11
mkdir -p -v /mnt/gentoo/etc/portage/repos.conf

cp -v /mnt/gentoo/usr/share/portage/config/repos.conf /mnt/gentoo/etc/portage/repos.conf/gentoo.conf

# 加入中国源
nano -w /mnt/gentoo/etc/portage/repos.conf/gentoo.conf:

## 源地址
sync-uri = rsync://mirrors.tuna.tsinghua.edu.cn/gentoo-portage/
#sync-uri = rsync://rsync.mirrors.ustc.edu.cn/gentoo-portage/
#sync-uri = rsync://mirrors.yun-idc.com/gentoo-portage/
二进制源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 修改二进制源地址为国内源
nano -w /etc/portage/binrepos.conf/gentoobinhost.conf

# 原有内容
# ---
# These settings were set by the catalyst build script that automatically
# built this stage.
# Please consider using a local mirror.

[gentoobinhost]
priority = 1
# sync-uri = https://distfiles.gentoo.org/releases/amd64/binpackages/23.0/x86-64
sync-uri = https://mirrors.tuna.tsinghua.edu.cn/gentoo/releases/amd64/binpackages/23.0/x86-64/
# sync-uri = https://mirrors.ustc.edu.cn/gentoo/releases/amd64/binpackages/23.0/x86-64/

如果启用二进制源需要在USE中添加 getbinpkg binpkg-request-signature

生成fstab

1
genfstab -U /mnt/gentoo >> /mnt/gentoo/etc/fstab

生成的fstab格式如下

1
2
3
UUID=......      /boot/efi      vfat      noauto,defaults,noatime,umask=0077                               0 2
UUID=...... / xfs defaults,noatime 0 1
UUID=...... /home xfs noatime,discard

系统配置

进入新系统环境

从现在开始,所有的动作将立即在新 Gentoo Linux 环境里生效。

1
2
3
4
chroot /mnt/gentoo /bin/bash
env-update
source /etc/profile
export PS1="(chroot) ${PS1}" # 切换提示符,避免混淆

同步stage

1
emerge-webrsync

^注意: gentoo handbook上提到可以使用emerge -rsync升级软件包数据库到最近2小时的最新版,这是没有必要的,而且下载的速度会极其慢,所以不推荐这样做。单用emerge-webrsync就可以同步数据库到最近3~4天内的最新版了。

设置profile

1
2
eselect profile list     #查看profile予设值
eselect profile set X #这里先保持选择默认值,即“default/linux/amd64/17.1 (stable)”

检测cpu指令集

1
2
3
4
5
6
7
# 安装cpuid2cpuflags
emerge --ask app-portage/cpuid2cpuflags

# 查看CPU指令集
cpuid2cpuflags

echo "*/* $(cpuid2cpuflags)" > /etc/portage/package.use/00cpu-flags

安装CCache(可选,加速编译)

1
2
3
4
emerge --ask ccache 
mkdir -p /var/cache/ccache
chown root:portage /var/cache/ccache -R
chmod 2775 /var/cache/ccache -R

在portage/make.conf中添加

1
2
3
4
FEATURES="ccache -test" 
CCACHE_DIR="/var/cache/ccache"

USE="... ${FEATURES}" # 在USE中添加${FEATURES}

安装Aria2(可选,加快包下载)

1
emerge --ask net-misc/aria2

在portage/make.conf中添加配置

1
2
3
4
5
6
7
DISTDIR="/var/cache/distfiles"
FETCHCOMMAND="/usr/bin/aria2c -d \${DISTDIR} -o \${FILE} \
--allow-overwrite=true --max-tries=5 --max-file-not-found=2 \
--max-concurrent-downloads=5 --connect-timeout=5 --timeout=5 \
--split=5 --min-split-size=2M --lowest-speed-limit=20K \
--max-connection-per-server=9 --uri-selector=feedback \${URI}"
RESUMECOMMAND="${FETCHCOMMAND}"

配置编译选项

1
nano /mnt/gentoo/etc/portage/make.conf

^注意: 设置编译标志 -march=native (如果你知道自己处理器的代号,就用自己的处理器代号替换这里的native 比如我的是skylake,如果不确定就使用native)
^注意: 设置 MAKEOPTS=”-j8” 来定义安装软件时并行编译的数量 这个数字等于你的CPU线程数(也称为逻辑CPU数)参考MAKEOPTS WiKi

完整的配置文件如下(转自Gentoo安装流程分享(step by step),第一篇之基本系统的安装,修改了下注释格式,删除不用的部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# These settings were set by the catalyst build script that automatically
# built this stage.
# Please consult /usr/share/portage/config/make.conf.example for a more
# detailed example.
# GCC编译配置 -O3代表优化级别,如果采用更高的-Ofast可能会导致部分软件包编译错误,
# -march=native代表为本机cpu进行编译,如果是交叉编译需要去掉
COMMON_FLAGS="-march=skylake -O2 -pipe"
CFLAGS="${COMMON_FLAGS}"
CXXFLAGS="${COMMON_FLAGS}"
FCFLAGS="${COMMON_FLAGS}"
FFLAGS="${COMMON_FLAGS}"

# 源代码包构建时传递给`make`的参数
# 同时编译的线程数,根据cpu线程数和内存大小/2中较小的
MAKEOPTS="-j8"

# 系统上托管的主软件包存储库,其默认值为 `/var/db/repos/gentoo`
PORTDIR="/var/db/repos/gentoo"

# Portage存储下载的源代码归档的位置,默认为新安装的`/var/cache/distfiles`
DISTDIR="/var/cache/distfiles"

#Portage临时文件的位置,默认为`/var/tmp`
# 如果内存足够大(8G、16G),那么建议把编译程序时存放临时中间文件的目录设置
# 为内存的tmpfs(/tmp目录),以减少编译时对硬盘的大量读写、延长硬盘使用寿命、
# 并加快编译速度;但如果你的内存较小(<=4G),那么建议把此项注释掉,否则很多
# 程序会因内存容量不足而导致编译失败
PORTAGE_TMPDIR="/tmp"

# NOTE: This stage was built with the bindist Use flag enabled

# This sets the language of build output to English.
# Please keep this setting intact when reporting bugs.
LC_MESSAGES=C

# 同步镜像
GENTOO_MIRRORS="https://mirrors.tuna.tsinghua.edu.cn/gentoo"
# 备选
# GENTOO_MIRRORS="https://mirrors.ustc.edu.cn/gentoo/"
# GENTOO_MIRRORS="https://mirrors.aliyun.com/gentoo/"
# GENTOO_MIRRORS="https://mirrors.cloud.tencent.com/gentoo/"
# GENTOO_MIRRORS="https://mirrors.huaweicloud.com/gentoo/"

# emerge的默认选项
EMERGE_DEFAULT_OPTS="--keep-going --with-bdeps=y --quiet --ask --verbose"

# 每次安装完包之后自动清理
AUTO_CLEAN="yes"

# 指定软件包的可用性和稳定性级别。
# 如果更喜欢最新那这里用~amd64(接受安装和更新处于测试阶段的软件包)
ACCEPT_KEYWORDS="amd64"
# 接受所有许可证的软件
ACCEPT_LICENSE="*"

# 语言设置
L10N="en-US zh-CN en zh"
LINGUAS="en_US zh_CN en zh"

# intel集成显卡和nvidia显卡(不使用novueau)
VIDEO_CARDS="intel i965 iris nvidia"

# intel声卡
ALSA_CARDS="hda_intel"

# 输入设备 非笔记本去除后面的synaptics
INPUT_DEVICES="libinput synaptics"

# 设置GRUB版本
GRUB_PLATFORMS="efi-64"

# 使用ccache来大大提高重新编译时的速度,安装ccache后解除注释
# CCACHE="parallel-fetch ccache"
# ccache使用的目录
# CCACHE_DIR="/var/cache/ccache"

# 使用aria2提高下载速度(不设置也无大碍,设置的话一定要注意指令拼写正确),
# 安装aria2后解除注释
# FETCHCOMMAND="/usr/bin/aria2c -d \${DISTDIR} -o \${FILE} \
# --allow-overwrite=true --max-tries=5 --max-file-not-found=2 \
# --max-concurrent-downloads=5 --connect-timeout=5 --timeout=5 \
# --split=5 --min-split-size=2M --lowest-speed-limit=20K \
# --max-connection-per-server=9 --uri-selector=feedback \${URI}"
# RESUMECOMMAND="${FETCHCOMMAND}"

# USE变量
# 用户希望在系统中启用的Portage特性列表,影响Portage的行为。
# 由于这是一个增量变量,可以在不直接覆盖通过 Gentoo profile
# 实现的FEATURES值的情况下添加FEATURES值。
FEATURES=""

# gnome和kde及其相关组件
DESK_ENV="-gnome -gnome-shell -gnome-keyring -nautilus -kde icu"

# 不使用systemd plymouth consolekit 只使用elogind
# 旧教程会使用consolekit,elogind是consolekit未来的替代品
FUCKSV="-systemd -bindist -mdev elogind -oss -grub -plymouth -consolekit"

# 对于音频相关软件使用pulseaudio alsa jack特性
AUDIO="alsa jack pulseaudio"

SOFTWARE="sudo client git openmp minizip udev blkid efi hwdb smack \
acpi ccache dbus policykit udisks cjk emoji -test"

# 网络相关
NET="network networkmanager connection-sharing wifi http2 dhclient \
-dhcpcd policykit nftables"

# 图形相关
VIDEO="X vulkan layers glamor nvidia gallium"

# 定义需要的USE变量
USE="${DESK_ENV} ${FUCKSV} ${AUDIO} ${NET} ${VIDEO} ${SOFTWARE}"

# 二进制包保存路径
# PKGDIR="/var/cache/binpkgs"

# 使用二进制软件包,加入USE生效
# BIN_PKG="getbinpkg"

# emerge时用到的代理 需要代理时候自行设置
# http_proxy="http://127.0.0.1:8889"
# https_proxy="http://127.0.0.1:8889"

永久禁用nouveau驱动模块

强烈要求你禁用Nouveau驱动!!能省掉以后很多莫名其妙的麻烦!

1
2
3
4
5
6
7
mkdir /etc/modprobe.d/
nano -w /etc/modprobe.d/blacklist.conf

# 写入以下内容
blacklist nouveau
blacklist lbm-nouveau
options nouveau modeset=0

即便在编译内核前就已经设置内核禁用Nouveau驱动了,但是内核安装时还是会默认把nouveau驱动作为内核模块自动加载。启用了nouveau驱动模块的内核会出现各式各样的莫名其妙的数不清的问题,所以为了避免以后出现这些问题,必须禁用nouveau模块。

内核配置和编译

安装内核源码

1
2
3
4
5
emerge --ask sys-kernel/gentoo-sources

# 如果安装多个版本内核时执行
eselect kernel list # 查看内核列表
eselect kernel set 1 # 选择内核版本

某些驱动程序在工作之前需要在系统上安装其他固件。这通常是网络接口的情况,尤其是无线网络接口。此外,在使用开源驱动程序时,来自AMD,Nvidia和Intel等供应商的现代视频芯片通常需要外部固件文件。大多数固件都封装在sys-kernel / linux-firmware中:

1
emerge --ask --quiet sys-kernel/linux-firmware

除了独立显卡硬件和网络接口之外,CPU 可能也需要固件更新。通常这种固件被称为微码(microcode)。有时需要更新版本的微码来修补 CPU 硬件中的不稳定性、安全问题或其他复杂的错误。

AMD CPU 的微码更新在前面提到的 sys-kernel/linux-firmware 软件包内分发。Intel CPU 的微码可以在 sys-firmware/intel-microcode 包中找到,并且需要单独安装

1
2
# Intel CPU 执行
emerge --ask sys-firmware/intel-microcode

三种方法安装内核

安装系统时可选择安装二进制内核,系统安装完后再配置编译内核,参考 配置Linux内核 - Gentoo Wiki

  1. 全自动安装

    当为基于 amd64 的系统安装和编译内核时,Gentoo 推荐使用 sys-kernel/gentoo-sources 软件包

    1
    emerge --ask sys-kernel/installkernel
  2. 混合安装(推荐方式)

    生成内核配置文件

    将genkernel的默认内核配置文件“generated-config”复制过来,里面已经设置好了绝大部分应用场景以及绝大部分硬件驱动的配置,非常方便,值得借过来使用,只需要在自己手动配置内核的时候将其加载,在其基础上做一点点轻微的修改或完全不修改都可以,对内核新手极其友好!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    emerge --ask sys-kernel/genkernel

    # 以genkernel的配置文件为基础进行自定义配置
    cp /usr/share/genkernel/arch/x86_64/generated-config /usr/src/linux/

    # 备份
    cp /usr/src/linux/generated-config /usr/src/linux/generated-config.bak

    # 编译
    genkernel --mountboot --install all

    ^注意如果想在以后支持jack低延迟实时音频组件(Jack-Audio-Connection-Kit),则需要vim generated-config,手动设置“CONFIG_CGROUPS=y”、“CONFIG_CGROUP_SCHED=y”、“CONFIG_RT_GROUP_SCHED=y”,然后重新make menuconfig载入保存generated-config一遍,接下来再编译内核。
    ^注意: 使用nvidia显卡闭源驱动,需要将内核配置中“CONFIG_I2C_NVIDIA_GPU”这一项禁用,否则会和官方nvidia-drivers冲突!!!

  3. 全手动安装

    1
    2
    3
    4
    5
    emerge sys-apps/pciutils

    cd /usr/src/linux
    # 配置内核
    make menuconfig

![[Gentoo安装/IMG-20241210170128644.png]]
有些内核选项是必须的,必须编译到内核中,而不是作为模块加载。*表示包括到内核中,M表示作为模块加载,[]只有包括到内核中和排除在外两种选项,<>则有包括到内核中、排除在外和以模块加载三种选项。下面这些选项都必须以*方式编译到内核中。

devtmpfs支持。

1
2
3
4
Device Drivers --->
Generic Driver Options --->
[*] Maintain a devtmpfs filesystem to mount at /dev
[*] Automount devtmpfs at /dev, after the kernel mounted the rootfs

SCSI磁盘支持。

1
2
3
Device Drivers --->
SCSI device support --->
<*> SCSI disk support

选择支持的文件系统。因为ESP分区用的FAT32格式化的,根目录用的XFS格式化的,所以这里这两项(FAT32也就是VFAT)必须包括到内核中,虚拟内存和proc文件系统也是必选的。其实这里还可以取消掉不需要的文件系统,但是对于新手不建议取消任何自己不明白的东西,很容易弄的最后内核没办法启动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
File systems --->
< > Second extended fs support
< > The Extended 3 (ext3) filesystem
<*> The Extended 4 (ext4) filesystem
< > Reiserfs support
< > JFS filesystem support
< > XFS filesystem support
< > Btrfs filesystem support
DOS/FAT/NT Filesystems --->
<*> MSDOS fs support
<*> VFAT (Windows-95) fs support

Pseudo Filesystems --->
[*] /proc file system support
[*] Tmpfs virtual memory file system support (former shm fs)

如果处理器是多核的,还需要开启SMP(对称多处理器支持)。

1
2
Processor type and features  --->
[*] Symmetric multi-processing support

USB也必须启用

1
2
3
4
5
6
7
8
9
10
11
Device Drivers --->
HID support --->
-*- HID bus support
<*> Generic HID driver
[*] Battery level reporting for HID devices
USB HID support --->
<*> USB HID transport layer
[*] USB support --->
<*> xHCI HCD (USB 3.0) support
<*> EHCI HCD (USB 2.0) support
<*> OHCI HCD (USB 1.1) support

系统体系相关的内核配置

因为选择了multlib,所以32和64位的程序都会安装。为了支持32位程序,必须启用32位程序模拟功能。这里其实倒是不用怎么改,默认已经都选上了。

1
2
3
4
5
6
7
8
9
10
11
12
Processor type and features  --->
[*] Machine Check / overheating reporting
[*] Intel MCE Features
[*] AMD MCE Features
Processor family (AMD-Opteron/Athlon64) --->
( ) Opteron/Athlon64/Hammer/K8
( ) Intel P4 / older Netburst based Xeon
( ) Core 2/newer Xeon
( ) Intel Atom
(*) Generic-x86-64
Binary Emulations --->
[*] IA32 Emulation

启用GPT支持,因为前面我用的GPT分区表,EFI启动方式,所以这两项也必须启用。

1
2
3
4
-*- Enable the block layer --->
Partition Types --->
[*] Advanced partition selection
[*] EFI GUID Partition support

EFI的支持。

1
2
3
4
5
6
7
8
Processor type and features  --->
[*] EFI runtime service support
[*] EFI stub support
[*] EFI mixed-mode support

Firmware Drivers --->
EFI (Extensible Firmware Interface) Support --->
<*> EFI Variable Support via sysfs
1
2
3
4
# 编译内核
make -j12 #(CPU核心数根据机器cpu调整)
make modules_install
make install

使用二进制内核

1
2
# 安装二进制内核
emerge --ask sys-kernel/gentoo-kernel-bin

可选:生成一个initramfs

在某些情况中需要建立一个initramfs——一个基于内存的初始化文件系统。最觉的原因是当重要的文件系统位置(如/usr/或/var/)在分离的分区。通过一个initramfs,这些分区可以使用initramfs里面的工具来完成挂载。

用dracut生成内核的initramfs,快速且方便,新手友好

1
2
3
4
5
emerge --ask sys-kernel/dracut

cd /boot

dracut --hostonly

或者使用genkernel生成内核的initramfs

1
2
cp /usr/src/linux/generated-config /etc/kernels/kernel-config-<内核版本号>-gentoo-x86_64
genkernel --install initramfs

系统环境配置

配置主机名

1
2
3
4
#nano -w /etc/conf.d/hostname

echo "HOSTNAME" > /etc/hostname

配置系统时区

1
2
3
4
5
6
7
8
ls /usr/share/zoneinfo
echo "Asia/Shanghai" > /etc/timezone

# 解决时间差8小时问题(双系统时会遇到Windows时间不对)
sudo rm /etc/localtime
sudo ln -sv /usr/share/zoneinfo/Universal /etc/localtime

sudo emerge --config sys-libs/timezone-data

配置编码

1
2
3
4
5
6
7
8
nano -w /etc/locale.gen 		#将以下几项取消注释,如果没有手动输入

en_US ISO-8859-1
en_US.UTF-8 UTF-8
zh_CN GBK
zh_CN.UTF-8 UTF-8

locale-gen # 更新

设置系统locale

1
2
3
4
5
6
7
8
#查看可用系统时区和地区配置
eselect locale list

#这里只能选择“en-US.utf8”!!假如设置成了中文后,整个系统的终端命令行会乱码!!!
eselect locale set X

# 更新环境
env-update && source /etc/profile && export PS1="(chroot) ${PS1}"

配置sudo自动补全

1
2
3
4
5
6
7
8
9
10
11
12
13
sudo emerge --ask app-shells/bash-completion

# 添加 bash-completion 全局 USE 标记
sudo vim /etc/protage/make.conf
USE="... bash-completion"
sudo emerge --avuDN world

# 启用bash-completion的功能
sudo eselect bashcomp enable base
# sudo bashcomp-config enable base

# 查看哪些命令支持bash-completion
sudo eselect bashcomp list

安装网络工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 无线
emerge --ask net-wireless/iw
emerge --ask net-wireless/wpa_supplicant

# 有线
emerge --ask net-misc/netifrc # openrc 自带
# emerge --ask net-misc/systemd-networkd

# PPPoE环境
emerge --ask net-dialup/ppp

# 如果使用GUI可跳过
# 要在引导时激活网络接口,需要将它们添加到默认运行级别
# 首先使用 ifconfig 查看网络接口名称
cd /etc/init.d/
ln -s net.lo net.eno1 # 此处网卡名称需要和实际网卡名对应
rc-update add net.eno1 default

![[Gentoo安装/IMG-20241210170128726.png]]
安装配置networkmanager

最方便支持多种联网方式的工具是NetworkManager,基本满足所有需求,但同时它的依赖有点多。如果使用桌面环境的话建议安装。

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
nano -w /etc/portage/make.conf:
USE=“networkmanager connection-sharing dhclient policykit ppp wifi -dhcpcd”

emerge net-misc/networkmanager

nano -w /etc/dhcp/dhclient.conf:
send host-name "Gentoo" #your hostname

nano -w /etc/NetworkManager/NetworkManager.conf
[connectivity]
uri=http://nmcheck.gnome.org/check_network_status.txt


nano -w /etc/NetworkManager/NetworkManager.conf
[main]
plugins=keyfile
dns=dnsmasq
hostname-mode=none

rc-update del dhcpcd
rc-update add NetworkManager default
# systemctl disable dhcpcd
# systemctl enable networkmanager

#gpasswd -a <你的桌面使用用户名> plugdev #没有这一步,用户将不能使用networkmanager,也就不能上网。不过先跳过这一步,在设置系统用户的时候再做。

nano -w /etc/dnsmasq.conf:
server=114.114.114.114

安装必要的工具

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
# 系统日志
emerge --ask app-admin/sysklogd
rc-update add sysklogd default
# systemctl enable sysklogd
# systemctl start sysklogd

# 计划任务管理
emerge --ask sys-process/cronie
rc-update add cronie default
# systemctl enable cronie
# systemctl start cronie

# 文件索引
emerge --ask sys-apps/mlocate

# 电源管理
emerge --ask sys-power/acpid
rc-update add acpid default
# systemctl enable acpid
# systemctl start acpid

# CPU温度管理
emerge sys-power/thermald
rc-update add thermald default
# systemctl enable thermald
# systemctl start thermald

# 设备管理工具
emerge --ask virtual/udev
rc-update add udev sysinit
# systemctl enable udev
# systemctl start udev

配置系统用户

安装sudo

1
2
3
emerge app-admin/sudo

nano -w /etc/sudoers

%wheel ALL=(ALL) ALL 这一行去掉注释,如果希望执行sudu不需要密码则取消注释%wheel ALL=(ALL) NOPASSWD:ALL

添加用户

1
2
3
groupadd sudo 
useradd -m -G users,wheel,usb,portage,video,audio,sudo -s /bin/bash [用户名]
chmod 700 /home/[用户名] -R

设置密码

1
2
3
4
# 设置root密码
passwd root

passwd {用户名}

添加操作系统启动项

/etc/portage/make.conf中添加grub配置

1
GRUB_PLATFORMS="efi-64"

安装grub2

1
2
emerge --ask sys-boot/grub:2
emerge --ask sys-boot/os-prober # 用于识别其他分区的系统(双系统)

grub安装到硬盘并生成开机启动项

1
2
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=Gentoo
grub-mkconfig -o /boot/grub/grub.cfg
1
mount -o remount,rw /sys/firmware/efi/efivarsos-prober

如果出现No space left on device,请运行以下命令,之后再重复上述步骤

1
2
mount -t efivarfs efivarfs /sys/firmware/efi/efivars
rm /sys/firmware/efi/efivars/dump-*

grub默认配置添加自定义配置,可提高intel cpu的稳定性和性能

1
2
3
4
nano -w /etc/default/grub:
GRUB_CMDLINE_LINUX_DEFAULT="intel_idle.max_cstate=0 processor.max_cstate=1"

grub-mkconfig -o /boot/grub/grub.cfg

清理

1
2
3
4
5
6
7
8
9
10
11
12
rm /stage3-*.tag.ge
# 退出chroot
exit

# 卸载
umount -lR /mnt/gentoo

# 重启
reboot

# 成功开机并进入系统后
grub-mkconfig -o /boot/grub/grub.cfg

如果开机不正常参考挂载文件系统(不要执行分区和mkfs操作),可重新chroot进入系统修改错误的配置。

桌面环境

安装基础环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 从 x11-base/xorg-drivers-21.1 开始,x11-base/xorg-drivers更改 [USE 标志]设置
# 这将弃用x11-drivers/xf86-video-intel驱动程序,以支持内置的通用模式设置DDX驱动程序
# 具有video_cards_i915USE 标志集将继续安装 Intel DDX 驱动程序。
emerge --ask x11-base/xorg-drivers

# 英伟达显卡
emerge --ask x11-drivers/nvidia-drivers

# 安装xorg-server
emerge --ask x11-base/xorg-server

# 安装双显卡设置工具
emerge --ask x11-apps/xrandr

# 让nvidia自动设置双显卡prime配置
sudo rm /etc/X11/xorg.conf
sudo nvidia-xconfig --prime

# 安装完更新当前系统组件环境
env-update && source /etc/profile

^注意: 以后每次重新编译安装内核kernel后,均须要运行一遍“emerge @module-rebuild”,重新编译安装nvidia驱动模块加载到内核之中,否则nvidia驱动无法加载!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
lsmod | grep nvidia
sudo rmmod nvidia
sudo modprobe nvidia

lsmod|grep nvidia

sudo vim /etc/modules-load.d/nvidia.conf:
nvidia

sudo vim /etc/modprobe.d/nvidia-drm.conf:
options nvidia-drm modeset=1

sudo rc-update add modules boot
sudo reboot

LightDM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 安装LightDM,使用KDE可忽略
sudo emerge --ask gui-libs/display-manager-init
sudo emerge --ask x11-misc/lightdm

# 如果不安装桌面管理器需要加入环境,登录后自动启动桌面
echo "XSESSION=\"awesome\"" > /etc/env.d/90xsession
env-update && source /etc/profile

# openrc
nano -w /etc/conf.d/display-manager
DISPLAYMANAGER="lightdm"

# 设置默认开机启动
rc-update add display-manager default
# 设置dubs默认开机启动,虽然display-manager也会启动它,但有时候会出现奇怪的问题
rc-update add dbus default

# 手动启动
rc-service dbus start
rc-service display-manager start

# systemd
# systemctl enable lightdm.service

KDE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 安装KDE桌面可忽略上边LightDM,同时需要删除USE中`-kde`
sudo emerge --ask x11-misc/sddm
sudo emerge --ask kde-plasma/plasma-meta

# 安装Dock
# 安装完打开latte-dock后会自动设置为开机自启动
sudo emerge --ask kde-misc/latte-dock

# 修改登陆管理器配置文件
nano -w /etc/conf.d/display-manager

# --- xdm内容
DISPLAYMANAGER="sddm"

#---

# 添加SDDM开机启动
sudo rc-update add xdm default
# 启动SDDM
sudo rc-service xdm start

# Systemd
# sudo systemctl enable xdm
# sudo systemctl start xdm

Awesome

1
2
3
4
5
6
7
8
9
10
11
12
13
# awesome 平铺式桌面
emerge --ask x11-wm/awesome

# 测试
mkdir -p ~/.config/awesome/
cp /etc/xdg/awesome/rc.lua ~/.config/awesome/rc.lua
awesome -k

# 壁纸支持
emerge --ask media-gfx/feh

# 在~/.config/awesome/theme/theme中添加一下内容
theme.wallpaper_cmd = { "wesetbg -f .config/awesome/themes/awesome-wallpaper.png" }

Mate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 选择profile
eselect profile list
eselect profile set 0 # 选择default/linux/amd64/23.0/desktop
# 更新
emerge -auvDU @world

# Meta 桌面
emerge --ask mate-base/mate

# 修改xsession配置
nano -w /etc/env.d/90xsession
#--- 90xsession内容
XSESSION="Mate"

#---

# 修改LightDM配置(需要安装LightDM)
nano -w /etc/conf.d/display-manager
#--- display-manager内容
DISPLAYMANAGER="lightdm"

Xfce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# xfce4桌面
emerge --ask xfce-base/xfce4-meta

# 测试桌面启动指令
startxfce4 # 启动Xfce桌面

# Pulseaudio音量控制
emerge --ask xfce-extra/xfce4-volumed-pulse

# 蓝牙音乐播放组件
emerge --ask xfce-extra/xfce4-pulseaudio-plugin

# 显示所有正在运行的程序的列表,以及每个程序占用的CPU和内存消耗。
emerge --ask xfce-extra/xfce4-taskmanager

# 监视和管理电源使用情况的应用程序。 这对笔记本电脑特别重要!
# 电源管理器允许用户调节屏幕亮度,选择最大性能或节电模式,
# 并在盖子关闭或按下按钮时设置休眠,暂停和关闭操作
emerge --ask xfce-extra/xfce4-power-manager

# 适合笔记本电脑用户。 它显示电池百分比,剩余时间,电源(交流或电池),风扇状态,警告,
# 甚至可以配置为在特定功率级别执行命令。 此功能可用于在电池电量几乎耗尽时将笔记本电脑置于休眠模式。
xfce-extra/xfce4-battery-plugin

# 添加几个窗口管理器主题
emerge --ask x11-themes/xfwm4-themes

# 一个X11终端,比准系统更可配置和有用 xterm
emerge --ask x11-terms/xfce4-terminal

# Xfce的默认图形文件管理器。
emerge --ask xfce-base/thunar

# 允许用户从Thunar内预览某些类型的文件,例如图像和字体。
emerge --ask xfce-extra/tumbler

# manages自动挂载可移动介质和驱动器。
emerge --ask xfce-extra/thunar-volman

# 嵌入面板的一个小命令行。 它比打开终端运行命令更快。
emerge --ask xfce-extra/xfce4-verve-plugin

# 提供一种方便的方法,只需点击鼠标即可安装/etc/fstab中列出的设备。
emerge --ask xfce-extra/xfce4-mount-plugin

# 允许用户监视硬件传感器,例如CPU温度,风扇RPM,硬盘驱动器温度,主板电压等。
emerge --ask xfce-extra/xfce4-sensors-plugin

音频控制

1
2
emerge --ask alsa-utils
emerge --ask alsa-plugins

中文字体

1
2
3
4
5
6
7
8
emerge --ask media-fonts/arphicfonts
emerge --ask media-fonts/noto-cjk
emerge --ask media-fonts/source-han-sans
emerge --ask media-fonts/wqy-microhei
emerge --ask media-fonts/wqy-zenhei

eselect fontconfig list
eselect fontconfig enable X X X # 选择所有wqy开头的项

输入法

1
2
3
4
5
6
7
8
9
10
11
12
# 输入法主题框架
# 其中, app-i18n/fcitx:5 是 fcitx 的主程序
#     app-i18n/fcitx-configtool:5 是它的配置工具
#     app-i18n/fcitx-qt:5 用于支持在 qt 程序上使用它
#     app-i18n/fcitx-gtk:5 用于支持在 gtk 程序上使用它
emerge -vj app-i18n/fcitx:5 app-i18n/fcitx-configtool:5 app-i18n/fcitx-qt:5 app-i18n/fcitx-gtk:5

# 安装完成后再用户的~/.xsession文件内添加
export XMODIFIERS="@im=fcitx"
export QT_IM_MODULE=fcitx
export GTK_IM_MODULE=fcitx
export SDL_IM_MODULE=fcitx

常用命令

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
# 使用常规(基于源)更新
# --ask(-a)控制Portage显示要更新的软件列表,并提供是否更新选择
# --verbose(-v)在屏幕上输出完整的文件列表
# --update(-u)更新包的最佳版本
# --deep(-D)更新系统中的每个软件包
# --newuse(-N)USE标记变更后,要使用Portage检查USE标记的变动是否导致需要安装新的软件或将现有的软件包重新编译
sudo emerge --ask --verbose --update --deep --newuse @world

# 等价简写
emerge -avuDN @world

emerge -av --deepclean

#gentookit包里的一个软件,用来检查系统的依赖是否都满足,自动安装缺失的依赖
revdep-rebuild

# 使用二进制包更新系统
emerge --ask --verbose --update --deep --changed-use --getbinpkg @world

# 告诉 Portage 不要对一些指定的包或分类创建二进制包
emerge -uDN @world --buildpkg --buildpkg-exclude "virtual/* sys-kernel/*-sources"

# 合并use标记
etc-update
-3 # 自动合并

# 刷新环境变量
source /etc/profile

# 清理旧版本的内核
emerge --prune sys-kernel/gentoo-kernel sys-kernel/gentoo-kernel-bin

emerge 使用二进制包选项说明

选项 说明
–usepkg (-k) 尝试使用本地可用的 packages 目录中的二进制包。如果未找到二进制包,将执行常规(基于源)安装。
–usepkgonly (-K) 类似 –usepkg (-k) ,但如果找不到二进制包,则失败。
–getbinpkg (-g) 从远程二进制包主机下载二进制包。如果未找到二进制包,将执行常规(基于源)安装。
–getbinpkgonly (-G) 类似于 –getbinpkg (-g) ,但如果无法下载二进制包,则会失败

USE变量说明 官方文档

USE是Gentoo为用户提供的最具威力的变量之一。很多程序通过它可以选择编译或者不编译某些可选的支持。例如,一些程 序可以在编译时加入对gtk或是对qt的支持。其它的程序可以在编译时加入或不加入对于SLL的支持。有些程序甚至可以在编译时加入对 framebuffer的支持(svgalib)以取代X11(X服务器)。
大多数的发行版会使用尽可能多的支持特性编译它们的软件包,这既增加了软件的大小也减慢了启动时间,而这些还没有算上可能会涉及到的大量依赖性问题。Gentoo可以让你自己定义软件编译的选项,而这正是USE要做的事。
在USE变量里可以定义关键字,它被用来对应相应的编译选项。例如,ssl将会把ssl支持编译到程序中以支持它。-X会移除其对于X服务器的支持(注意前面的减号)。gnome gtk -kde -qt4将会以支持GNOME(和GTK)但不支持KDE(和Qt)的方式编译软件,使系统为GNOME做完全调整(如果架构支持)。
默认的USE设置全放在了系统所使用的Gentoo配置文件的make.defaults文件中。Gentoo对它的配置文件们使用了一个(复杂的)继承系统,在这个阶段我们不去深入。最简单的检查当前活动的USE标记的办法是运行emerge –info并选择以USE开头的那一行:

1
emerge --info |grep ^USE

![[Gentoo安装/IMG-20241210170128821.png]]
可以在系统的/usr/portage/profiles/use.desc中找到可用的USE标记的完整描述。

1
less /usr/share/portage/profile/use.desc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
### 常用软件

```bash
sudo emerge --ask media-video/mpv

# 电子邮件客户端
sudo emerge mail-client/thunderbird

# 视频播放器
sudo emerge media-video/mplayer

# 音乐播放器
sudo emerge media-sound/exaile

# 虚拟机
sudo emerge app-emulation/virt-manager
sudo emerge app-emulation/virtualbox

# VIM
sudo emerge app-editors/vim

参考文章

Gentoo AMD64 Handbook - Gentoo Wiki

开始使用gentoo linux——gentoo安装笔记(上)

开始使用gentoo linux——gentoo安装笔记(下)

Gentoo安装流程分享(step by step),第二篇之KDE Plasma桌面的安装配置 - 知乎 (zhihu.com)

gentoo linux配置intel和nvidia双显卡电脑,使用prime方案 - 简书 (jianshu.com)

OpenIddict是一个ASP.NET Core身份验证库,可帮助您添加OpenID Connect和OAuth 2.0支持到ASP.NET Core应用程序中。下面是OpenIddict使用教程的步骤:

  1. 安装OpenIddict,在项目中添加OpenIddict.Core和OpenIddict.EntityFrameworkCore Nuget包。
  2. 配置OpenIddict,在Startup.cs文件中添加OpenIddict服务的配置。您可以选择使用内存或EFCore进行配置。以下是使用EF Core进行配置的示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    services.AddDbContext<ApplicationDbContext>(options =>
    {
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
    options.UseOpenIddict();
    });

    services.AddCustomOpenIddictApplication();
    services.AddCustomOpenIddictAuthorization();
    services.AddCustomOpenIddictScope();
    services.AddCustomOpenIddictToken();
    services.AddCustomOpenIddictValidation();
    services.AddCustomOpenIddictUser();

    services.AddOpenIddict()
    .AddCore(options =>
    {
    options.UseEntityFrameworkCore()
    .UseDbContext<ApplicationDbContext>()
    .ReplaceDefaultEntities<ApplicationDbContext>();
    })
    .AddServer(options =>
    {
    options.UseMvc();
    options.EnableAuthorizationEndpoint("/connect/authorize")
    .EnableLogoutEndpoint("/connect/logout")
    .EnableTokenEndpoint("/connect/token")
    .EnableUserinfoEndpoint("/connect/userinfo");
    options.RegisterScopes("openid", "profile", "email", "offline_access");

    options.AllowImplicitFlow();
    options.DisableHttpsRequirement();

    options.AddSigningCertificate(File.ReadAllBytes(Configuration["Auth:Certificates:Path"]),
    Configuration["Auth:Certificates:Password"]);

    options.DisableAccessTokenEncryption();
    options.SetAccessTokenLifetime(TimeSpan.FromHours(6));
    });
  3. 添加授权策略,在Startup.cs文件添加需要的授权策略。以下是一个例子:
    1
    2
    3
    4
    5
    services.AddAuthorization(options =>
    {
    options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
    options.AddPolicy("AdministratorOnly", policy => policy.RequireRole("Administrator"));
    });
  4. 在您的应用程序中使用OpenIddict,您可以使用OpenIddict来实现您的OAuth 2.0或OpenID Connect需求。以下是一些常见的用例:

4.1 登录页面

使用OpenIddict进行身份验证,您可以使用如下代码在您的控制器中。您可以使用请求重定向到触发OpenID Connect流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
[HttpGet("~/login")]
public IActionResult Login()
{
var request = HttpContext.GetOpenIddictServerRequest();

return View(new LoginViewModel
{
Nonce = RandomNumberGenerator.GetInt32(),
ReturnUrl = request.RedirectUri,
Ticket = request.GetOpenIddictServerTransactionId(),
});
}

[HttpPost("~/login")]
public IActionResult Login(LoginViewModel model)
{
if (ModelState.IsValid)
{
var user = await _userManager.FindByNameAsync(model.Username);
if (user == null)
{
ModelState.AddModelError("Username", "Username or password is incorrect.");
}
else if (!await _userManager.IsEmailConfirmedAsync(user))
{
ModelState.AddModelError("Email", "You must have a confirmed email to log in.");
}
else if (!await _userManager.CheckPasswordAsync(user, model.Password))
{
ModelState.AddModelError("Username", "Username or password is incorrect.");
}
else
{
// 创建一个新的身份验证票据.
var ticket = await CreateTicketAsync(user);

return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
}

ViewData["returnUrl"] = model.ReturnUrl;
ViewData["nonce"] = model.Nonce;
ViewData["transactionId"] = model.Ticket;
return View(model);
}

4.2 注册页面

您还可以使用OpenIddict来实现您的注册页面。以下是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
[HttpGet("~/register")]
public IActionResult Register()
{
return View();
}

[HttpPost("~/register")]
public async Task<IActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser
{
UserName = model.Email,
Email = model.Email,
FirstName = model.FirstName,
LastName = model.LastName,
};

var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
var code = await _userManager.GenerateEmailConfirmationAsync(user);
var callbackUrl= Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);

await _emailSender.SendEmailAsync(model.Email, "Confirm your email",
$"Please confirm your account by clicking this link: {callbackUrl}");

return RedirectToAction(nameof(RegisterConfirmation));
}

foreach (var error in result.Errors)
{
ModelState.AddModelError("Email", error.Description);
}
}

return View(model);
}

[HttpGet("~/register/confirmation")]
public IActionResult RegisterConfirmation()
{
return View();
}

4.3 访问受保护的资源

最后,您可以使用OpenIddict来实现访问受保护资源的身份验证和授权。以下是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[HttpGet("~/manager")]
[Authorize(Roles = "Manager")]
public IActionResult ManagerDashboard()
{
return View();
}

[HttpGet("~/employee")]
[Authorize(Policy = "EmployeeOnly")]
public IActionResult EmployeeDashboard()
{
return View();
}

[HttpGet("~/administrator")]
[Authorize(Policy = "AdministratorOnly")]
public IActionResult AdministratorDashboard()
{
return View();
}
  1. 通过OpenIddict实现Token刷新

当访问受保护的API时,您可以使用OpenIddict来实现使用token刷新。以下是实现Token刷新的一个示例方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
[HttpPost("~/api/token/refresh")]
public async Task<IActionResult> Refresh([FromForm]string refreshToken)
{
var info = await HttpContext.AuthenticateAsync(OpenIddictServerDefaults.AuthenticationScheme);

if (info == null)
{
return BadRequest(new
{
error = OpenIddictConstants.Errors.InvalidRequest,
error_description = "The refresh token is no longer valid."
});
}

var principal = info.Principal;

var user = await _userManager.GetUserAsync(principal);
if (user == null)
{
return BadRequest(new
{
error = OpenIddictConstants.Errors.InvalidRequest,
error_description = "The refresh token is no longer valid."
});
}

// 确保刷新令牌没有被撤销.
if (!await _tokenManager.ValidateAsync(
principal.GetId(),
principal.GetClaim(OpenIddictConstants.Claims.JwtId)))
{
return BadRequest(new
{
error = OpenIddictConstants.Errors.InvalidRequest,
error_description = "The refresh token is no longer valid."
});
}

// 从数据库得到客户端应用程序详细信息
var application = await _applicationManager.FindByClientIdAsync(
principal.GetClaim(OpenIddictConstants.Claims.ClientId));
if (application == null)
{
return BadRequest(new
{
error = OpenIddictConstants.Errors.InvalidRequest,
error_description = "The client application associated with this token is no longer valid."
});
}

var identity = await _userManager.CreateIdentityAsync(user, principal.GetScopes());

var ticket = await CreateTicketAsync(application, identity, principal);

return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
  1. 通过OpenIddict实现密码恢复流程OpenIddict还可以实现忘记密码流程的重置密码,以下是一个简单的示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    [HttpPost("~/forgot-password")]
    [AllowAnonymous]
    public async Task<IActionResult> ForgotPassword([FromForm] string email)
    {
    var user = await _userManager.FindByEmailAsync(email);
    if (user == null)
    {
    // 不要显示用户不存在,懂的都懂~
    return Ok();
    }

    var code = await _userManager.GeneratePasswordResetTokenAsync(user);
    code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));

    var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Reque

    await _emailSender.SendEmailAsync(
    email,
    "Password Reset",
    $"Please reset your password by clicking here: <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>link</a>.");

    return Ok();
    }

    [HttpGet("~/reset-password")]
    [AllowAnonymous]
    public IActionResult ResetPassword(string code = null, string userId = null)
    {
    return View(new ResetPasswordViewModel { Code = code, UserId = userId });
    }

    [HttpPost("~/reset-password")]
    [AllowAnonymous]
    public async Task<IActionResult> ResetPassword([FromForm] ResetPasswordViewModel model)
    {
    if (!ModelState.IsValid)
    {
    return View(model);
    }

    var user = await _userManager.FindByIdAsync(model.UserId);
    if (user == null)
    {
    // 不要显示用户不存在
    return View("ResetPasswordConfirmation");
    }

    var decodedCode = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(model.Code));
    var result = await _userManager.ResetPasswordAsync(user, decodedCode, model.Password);
    if (result.Succeeded)
    {
    return RedirectToAction(nameof(ResetPasswordConfirmation));
    }

    foreach (var error in result.Errors)
    {
    ModelState.AddModelError(string.Empty, error.Description);
    }

    return View(model);
    }

    [HttpGet("~/reset-password-confirmation")]
    [AllowAnonymous]
    public IActionResult ResetPasswordConfirmation()
    {
    return View();
    }
  2. 使用OpenIddict实现自定义Token发布方案

OpenIddict支持自定义Token发布方案,以适应各种需求。在以下示例中,我们将实现自定义发布方案来控制Token的过期时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class CustomTokenEndpointHandler : OpenIddictServerHandler<OpenIddictServerOptions>
{
public CustomTokenEndpointHandler(IServiceProvider services)
: base(services)
{
}

public override async Task HandleAsync([NotNull] OpenIddictServerHandleContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

// 从数据库检索客户机应用程序.
var application = await context.HttpContext.GetOpenIddictServerApplicationAsync();
if (application == null)
{
throw new InvalidOperationException("The client application cannot be retrieved.");
}

// 从授权服务器设置检索用户主体.
var principal = context.HttpContext.User;

// 确保允许应用程序使用指定的授权类型。
if (!await ValidateClientRedirectUriAsync(application, context.Request))
{
throw new InvalidOperationException("The grant type is not allowed for this application.");
}

//注意:这个自定义令牌终端点总是忽略“scopes”参数,并根据授予的scopes/roles自动定义声明。
var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(),
OpenIddictServerDefaults.AuthenticationScheme);

// 根据请求的自定义授权类型自定义令牌生命周期.
if (string.Equals(context.Request.GrantType, "urn:custom_grant", StringComparison.OrdinalIgnoreCase))
{
// Set the token expiration to 1 hour.
ticket.Properties.ExpiresUtc = context.Options.SystemClock.UtcNow.AddHours(1);
}
else
{
// 将令牌过期时间设置为默认持续时间(5分钟)
ticket.Properties.ExpiresUtc = context.Options.SystemClock.UtcNow.Add(
context.Options.AccessTokenLifetime ?? TimeSpan.FromMinutes(5));
}

context.Logger.LogInformation("The custom token request was successfully processed.");

await context.HttpContext.SignInAsync(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);

// 将响应标记为已处理,以跳过管道的其余部分.
context.HandleRequest();
}
}

您需要将其添加到OpenIddict配置中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
services.AddOpenIddict()
.AddCore(options =>
{
// ...
})
.AddServer(options =>{
// ...

options.Handlers.Add(new CustomTokenEndpointHandler(services));

// ...
})
.AddValidation(options =>
{
// ...
});

此时,您可以使用 urn:custom_grant 授权类型来发出过期时间为1小时的Token,这可以通过以下方式完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost:5000/connect/token");
request.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "urn:custom_grant",
["client_id"] = "your_client_id",
["client_secret"] = "your_client_secret",
["scope"] = "your_scopes_separated_by_spaces"
});

var response = await client.SendAsync(request);
var payload = await response.Content.ReadAsStringAsync();

总结

本文介绍了如何使用OpenIddict创建一个基本的身份验证和授权服务器。当然,在实现身份验证和授权服务器时有很多细节需要考虑,例如维护安全性、处理错误、管理用户和客户端应用程序等。希望这篇文章对您有所帮助!

Neovim 配置美化完整流程


环境说明

项目 说明
操作系统 Ubuntu 20.04 / macOS
终端 Windows Terminal + WSL2 / iTerm2
Neovim版本 v0.7+
配置需求 流畅的 GitHub 连接(用于拉取插件)

一、配置文件整体结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
.
├── init.lua # 配置入口文件
└── lua
├── autocmds.lua # 自动命令
├── basic.lua # 基础配置
├── colorscheme.lua # 主题配置
├── keybindings.lua # 快捷键设置
├── lsp
│ ├── cmp.lua # 代码补全配置
│ ├── config # 各语言服务器配置
│ │ ├── bash.lua
│ │ ├── css.lua
│ │ ├── html.lua
│ │ ├── json.lua
│ │ ├── lua.lua
│ │ ├── markdown.lua
│ │ ├── pyright.lua
│ │ ├── rust.lua
│ │ └── ts.lua
│ ├── formatter.lua # 代码格式化
│ ├── null-ls.lua # 格式化/诊断
│ ├── setup.lua # LSP 初始化
│ └── ui.lua # UI 美化
├── plugin-config
│ ├── bufferline.lua # 顶部标签页
│ ├── comment.lua # 注释插件
│ ├── dashboard.lua # 启动页面
│ ├── gitsigns.lua # Git 增强
│ ├── indent-blankline.lua # 缩进线
│ ├── lualine.lua # 底部状态栏
│ ├── nvim-autopairs.lua # 自动括号
│ ├── nvim-tree.lua # 文件浏览器
│ ├── nvim-treesitter.lua # 语法高亮
│ ├── project.lua # 项目管理
│ ├── surround.lua # 成对编辑
│ ├── telescope.lua # 模糊搜索
│ ├── toggleterm.lua # 终端
│ ├── vimspector.lua # 调试
│ └── which-key.lua # 快捷键提示
├── plugins.lua # 插件管理
└── utils
├── fix-yank.lua
├── global.lua
└── im-select.lua

文件说明

文件 说明
init.lua 整个配置的入口文件,负责引用所有其他模块
basic.lua 基础配置,对默认配置的重置
colorscheme.lua 主题皮肤配置
keybindings.lua 快捷键设置,所有插件的快捷键
plugins.lua 插件安装管理
lsp/ 内置 LSP 功能配置,包括编程语言与语法提示
plugin-config/ 第三方插件的独立配置文件
utils/ 常见问题的修改,包括输入法切换等

二、安装 Neovim

2.1 卸载旧版本(可选)

1
sudo apt-get remove neovim

2.2 Ubuntu 安装

1
2
3
4
sudo apt-get install software-properties-common
sudo add-apt-repository ppa:neovim-ppa/unstable
sudo apt-get update
sudo apt-get install neovim

2.3 macOS 安装

1
brew install neovim

2.4 验证版本

1
2
nvim --version
# 确保版本为 0.7 及以上

2.5 配置别名

1
2
3
4
5
6
# ~/.bashrc
alias vim='nvim'
alias vi='nvim'
alias v='nvim'

source ~/.bashrc

三、配置 Nerd Fonts

3.1 下载字体

官网:Nerd Fonts

下载 FiraCodeNerdFont-Regular.ttf,双击安装即可。

3.2 配置终端字体

在终端设置中选择安装的 Nerd Font 字体。


四、配置入口 init.lua

~/.config/nvim/init.lua

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
-- 基础设置
require('basic')

-- 快捷键映射
require("keybindings")

-- Packer 插件管理
require("plugins")

-- 主题设置
require("colorscheme")

-- 插件配置
require("plugin-config.nvim-tree")
require("plugin-config.bufferline")
require("plugin-config.lualine")
require("plugin-config.dashboard")
require("plugin-config.project")
require("plugin-config.nvim-treesitter")

-- 内置 LSP
require("lsp.setup")
require("lsp.cmp")
require("lsp.ui")
require("lsp.null-ls")

五、基础配置 basic.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
-- utf8
vim.g.encoding = "UTF-8"
vim.o.fileencoding = 'utf-8'

-- jkhl 移动时光标周围保留8行
vim.o.scrolloff = 8
vim.o.sidescrolloff = 8

-- 使用相对行号
vim.wo.number = true
vim.wo.relativenumber = true

-- 高亮所在行
vim.wo.cursorline = true

-- 显示左侧图标指示列
vim.wo.signcolumn = "yes"

-- 右侧参考线
vim.wo.colorcolumn = "80"

-- 缩进2个空格等于一个Tab
vim.o.tabstop = 2
vim.bo.tabstop = 2
vim.o.softtabstop = 2
vim.o.shiftround = true
vim.o.shiftwidth = 2
vim.bo.shiftwidth = 2

-- 空格替代tab
vim.o.expandtab = true
vim.bo.expandtab = true

-- 新行对齐当前行
vim.o.autoindent = true
vim.bo.autoindent = true
vim.o.smartindent = true

-- 搜索大小写
vim.o.ignorecase = true
vim.o.smartcase = true
vim.o.hlsearch = false
vim.o.incsearch = true

-- 命令行高度
vim.o.cmdheight = 2

-- 文件修改自动加载
vim.o.autoread = true
vim.bo.autoread = true

-- 禁止折行
vim.wo.wrap = false

-- 光标移动
vim.o.whichwrap = '<,>,[,]'

-- 允许隐藏buffer
vim.o.hidden = true

-- 鼠标支持
vim.o.mouse = "a"

-- 禁止备份文件
vim.o.backup = false
vim.o.writebackup = false
vim.o.swapfile = false

-- 更新间隔
vim.o.updatetime = 300
vim.o.timeoutlen = 500

-- 分屏位置
vim.o.splitbelow = true
vim.o.splitright = true

-- 自动补全
vim.g.completeopt = "menu,menuone,noselect,noinsert"

-- 样式
vim.o.background = "dark"
vim.o.termguicolors = true
vim.opt.termguicolors = true

-- 不可见字符
vim.o.list = true
vim.o.listchars = "space:·"

-- 补全增强
vim.o.wildmenu = true
vim.o.shortmess = vim.o.shortmess .. 'c'
vim.o.pumheight = 10
vim.o.showtabline = 2
vim.o.showmode = false

-- 复制粘贴联通系统粘贴板
vim.o.clipboard = "unnamedplus"

配置项说明

配置 说明
vim.g.{name} 全局变量
vim.b.{name} 缓冲区变量
vim.w.{name} 窗口变量
vim.bo.{option} buffer-local 选项
vim.wo.{option} window-local 选项
vim.opt 通用选项设置

六、快捷键设置 keybindings.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
vim.g.mapleader = " "
vim.g.maplocalleader = " "

local map = vim.api.nvim_set_keymap
local opt = { noremap = true, silent = true }

-- 取消 s 默认功能
map("n", "s", "", opt)

-- windows 分屏快捷键
map("n", "sv", ":vsp<CR>", opt)
map("n", "sh", ":sp<CR>", opt)
map("n", "sc", "<C-w>c", opt)
map("n", "so", "<C-w>o", opt)

-- Alt + hjkl 窗口之间跳转
map("n", "<A-h>", "<C-w>h", opt)
map("n", "<A-j>", "<C-w>j", opt)
map("n", "<A-k>", "<C-w>k", opt)
map("n", "<A-l>", "<C-w>l", opt)

-- 左右比例控制
map("n", "<C-Left>", ":vertical resize -2<CR>", opt)
map("n", "<C-Right>", ":vertical resize +2<CR>", opt)
map("n", "s,", ":vertical resize -20<CR>", opt)
map("n", "s.", ":vertical resize +20<CR>", opt)

-- 上下比例
map("n", "sj", ":resize +10<CR>", opt)
map("n", "sk", ":resize -10<CR>", opt)

-- Terminal相关
map("n", "<leader>t", ":sp | terminal<CR>", opt)
map("n", "<leader>vt", ":vsp | terminal<CR>", opt)
map("t", "<Esc>", "<C-\\><C-n>", opt)
map("t", "<A-h>", "<C-\\><C-N><C-w>h", opt)
map("t", "<A-j>", "<C-\\><C-N><C-w>j", opt)
map("t", "<A-k>", "<C-\\><C-N><C-w>k", opt)
map("t", "<A-l>", "<C-\\><C-N><C-w>l", opt)

-- visual模式下缩进
map("v", "<", "<gv", opt)
map("v", ">", ">gv", opt)

-- 上下移动选中文本
map("v", "J", ":move '>+1<CR>gv-gv", opt)
map("v", "K", ":move '<-2<CR>gv-gv", opt)

-- 上下滚动
map("n", "<C-j>", "4j", opt)
map("n", "<C-k>", "4k", opt)
map("n", "<C-u>", "9k", opt)
map("n", "<C-d>", "9j", opt)

-- 粘贴不复制
map("v", "p", '"_dP', opt)

-- 退出
map("n", "q", ":q<CR>", opt)
map("n", "qq", ":q!<CR>", opt)
map("n", "Q", ":qa!<CR>", opt)

-- insert 模式跳转
map("i", "<C-h>", "<ESC>I", opt)
map("i", "<C-l>", "<ESC>A", opt)

-- 代码注释插件
pluginKeys.comment = {
toggler = { line = "gcc", block = "gbc" },
opleader = { line = "gc", block = "gb" },
}

-- nvim-cmp 自动补全
pluginKeys.cmp = function(cmp)
return {
["<A-.>"] = cmp.mapping(cmp.mapping.complete(), { "i", "c" }),
["<A-,>"] = cmp.mapping({ i = cmp.mapping.abort(), c = cmp.mapping.close() }),
["<C-k>"] = cmp.mapping.select_prev_item(),
["<C-j>"] = cmp.mapping.select_next_item(),
["<CR>"] = cmp.mapping.confirm({ select = true, behavior = cmp.ConfirmBehavior.Replace }),
["<C-u>"] = cmp.mapping(cmp.mapping.scroll_docs(-4), { "i", "c" }),
["<C-d>"] = cmp.mapping(cmp.mapping.scroll_docs(4), { "i", "c" }),
}
end

-- LSP 快捷键
pluginKeys.mapLSP = function(mapbuf)
mapbuf("n", "<leader>rn", "<cmd>lua vim.lsp.buf.rename()<CR>", opt)
mapbuf("n", "<leader>ca", "<cmd>lua vim.lsp.buf.code_action()<CR>", opt)
mapbuf("n", "gd", "<cmd>lua vim.lsp.buf.definition()<CR>", opt)
mapbuf("n", "gh", "<cmd>lua vim.lsp.buf.hover()<CR>", opt)
mapbuf("n", "gD", "<cmd>lua vim.lsp.buf.declaration()<CR>", opt)
mapbuf("n", "gi", "<cmd>lua vim.lsp.buf.implementation()<CR>", opt)
mapbuf("n", "gr", "<cmd>lua vim.lsp.buf.references()<CR>", opt)
mapbuf("n", "gp", "<cmd>lua vim.diagnostic.open_float()<CR>", opt)
mapbuf("n", "gk", "<cmd>lua vim.diagnostic.goto_prev()<CR>", opt)
mapbuf("n", "gj", "<cmd>lua vim.diagnostic.goto_next()<CR>", opt)
mapbuf("n", "<leader>f", "<cmd>lua vim.lsp.buf.format()<CR>", opt)
end

-- TypeScript 快捷键
pluginKeys.mapTsLSP = function(mapbuf)
mapbuf("n", "gs", ":TSLspOrganize<CR>", opt)
mapbuf("n", "gr", ":TSLspRenameFile<CR>", opt)
mapbuf("n", "gi", ":TSLspImportAll<CR>", opt)
end

-- nvim-tree 快捷键
pluginKeys.nvimTreeList = {
{ key = { "<CR>", "o", "<2-LeftMouse>" }, action = "edit" },
{ key = "v", action = "vsplit" },
{ key = "h", action = "split" },
{ key = "i", action = "toggle_custom" },
{ key = ".", action = "toggle_dotfiles" },
{ key = "<F5>", action = "refresh" },
{ key = "a", action = "create" },
{ key = "d", action = "remove" },
{ key = "r", action = "rename" },
{ key = "x", action = "cut" },
{ key = "c", action = "copy" },
{ key = "p", action = "paste" },
{ key = "s", action = "system_open" },
}

七、插件配置

7.1 安装 Packer

1
git clone --depth 1 https://github.com/wbthomason/packer.nvim ~/.local/share/nvim/site/pack/packer/start/packer.nvim

7.2 plugins.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
local packer = require("packer")
packer.startup(function(use)
-- Packer 可以管理自己本身
use 'wbthomason/packer.nvim'

-- -------------------- colorschemes --------------------
use("folke/tokyonight.nvim")

-- -------------------- 文件浏览器 --------------------
use({ "kyazdani42/nvim-tree.lua", requires = "kyazdani42/nvim-web-devicons" })

-- -------------------- 顶部标签页 --------------------
use({ "akinsho/bufferline.nvim", requires = { "kyazdani42/nvim-web-devicons", "moll/vim-bbye" } })

-- -------------------- 底部状态栏 --------------------
use({ "nvim-lualine/lualine.nvim", requires = "kyazdani42/nvim-web-devicons" })
use("arkav/lualine-lsp-progress")

-- -------------------- 模糊搜索 --------------------
use { 'nvim-telescope/telescope.nvim', requires = { "nvim-lua/plenary.nvim" } }
use("ahmedkhalf/project.nvim")

-- -------------------- 启动页面 --------------------
use("glepnir/dashboard-nvim")

-- -------------------- 语法高亮 --------------------
use({ "nvim-treesitter/nvim-treesitter", run = ":TSUpdate" })

-- -------------------- LSP --------------------
use("williamboman/mason.nvim")
use({ "williamboman/mason-lspconfig.nvim" })
use("neovim/nvim-lspconfig")
use("jose-elias-alvarez/null-ls.nvim")

-- -------------------- 代码补全 --------------------
use("hrsh7th/nvim-cmp")
use("hrsh7th/vim-vsnip")
use("hrsh7th/cmp-vsnip")
use("hrsh7th/cmp-nvim-lsp")
use("hrsh7th/cmp-buffer")
use("hrsh7th/cmp-path")
use("hrsh7th/cmp-cmdline")
use("rafamadriz/friendly-snippets")

-- -------------------- UI 美化 --------------------
use("tami5/lspsaga.nvim")

-- -------------------- 调试 --------------------
use("CRAG666/code_runner.nvim")
use("windwp/nvim-autopairs")
use("numToStr/Comment.nvim")
use("ur4ltz/surround.nvim")

-- -------------------- TypeScript 增强 --------------------
use({ "jose-elias-alvarez/nvim-lsp-ts-utils", requires = "nvim-lua/plenary.nvim" })
use("b0o/schemastore.nvim")

-- -------------------- Rust 增强 --------------------
use("simrat39/rust-tools.nvim")
end)

7.3 Packer 命令

命令 说明
:PackerCompile 重新生成编译的加载文件
:PackerClean 清除不用的插件
:PackerInstall 安装缺失的插件
:PackerUpdate 更新并安装插件
:PackerSync 更新 + 编译
:PackerLoad 立刻加载 opt 插件

7.4 自动安装

1
2
3
4
5
6
7
-- lua/plugins.lua 末尾添加
pcall(vim.cmd, [[
augroup packer_user_config
autocmd!
autocmd BufWritePost plugins.lua source <afile> | PackerSync
augroup end
]])

八、主题配置 colorscheme.lua

1
2
3
4
5
6
local colorscheme = "tokyonight"
local status_ok, _ = pcall(vim.cmd, "colorscheme " .. colorscheme)
if not status_ok then
vim.notify("colorscheme " .. colorscheme .. " 没有找到!")
return
end

推荐主题

  • tokyonight
  • monokai.nvim
  • dracula
  • nord

九、插件配置详解

9.1 nvim-tree.lua

lua/plugin-config/nvim-tree.lua

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
local status, nvim_tree = pcall(require, "nvim-tree")
if not status then return end

nvim_tree.setup({
git = { enable = false },
update_cwd = true,
update_focused_file = { enable = true, update_cwd = true },
filters = { dotfiles = true, custom = { 'node_modules' } },
view = {
width = 40,
side = 'left',
hide_root_folder = false,
number = false,
relativenumber = false,
signcolumn = 'yes',
},
actions = {
open_file = {
resize_window = true,
quit_on_open = true,
},
},
system_open = { cmd = 'wsl-open' },
})

vim.cmd([[
autocmd BufEnter * ++nested if winnr('$') == 1 && bufname() == 'NvimTree_' . tabpagenr() | quit | endif
]])

9.2 bufferline.lua

lua/plugin-config/bufferline.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
local status, bufferline = pcall(require, "bufferline")
if not status then return end

bufferline.setup({
options = {
close_command = "Bdelete! %d",
right_mouse_command = "Bdelete! %d",
offsets = { { filetype = "NvimTree", text = "File Explorer", highlight = "Directory" } },
diagnostics = "nvim_lsp",
diagnostics_indicator = function(count, level, diagnostics_dict, context)
local s = " "
for e, n in pairs(diagnostics_dict) do
local sym = e == "error" and " " or (e == "warning" and " " or "")
s = s .. n .. sym
end
return s
end,
},
})

快捷键:

1
2
3
4
5
6
map("n", "<C-h>", ":BufferLineCyclePrev<CR>", opt)
map("n", "<C-l>", ":BufferLineCycleNext<CR>", opt)
map("n", "<C-w>", ":Bdelete!<CR>", opt)
map("n", "<leader>bl", ":BufferLineCloseRight<CR>", opt)
map("n", "<leader>bh", ":BufferLineCloseLeft<CR>", opt)
map("n", "<leader>bc", ":BufferLinePickClose<CR>", opt)

9.3 lualine.lua

lua/plugin-config/lualine.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
local status, lualine = pcall(require, "lualine")
if not status then return end

lualine.setup({
options = {
theme = "tokyonight",
component_separators = { left = "|", right = "|" },
section_separators = { left = " ", right = "" },
},
extensions = { "nvim-tree", "toggleterm" },
sections = {
lualine_c = {
"filename",
{ "lsp_progress", spinner_symbols = { " ", " ", " ", " ", " ", " " } },
},
lualine_x = { "filesize", "fileformat", "encoding", "filetype" },
},
})

9.4 nvim-treesitter.lua

lua/plugin-config/nvim-treesitter.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
local status, treesitter = pcall(require, "nvim-treesitter.configs")
if not status then return end

treesitter.setup({
ensure_installed = { "json", "html", "css", "vim", "lua", "javascript", "typescript", "tsx", "rust", "c", "cpp" },
highlight = { enable = true, additional_vim_regex_highlighting = false },
incremental_selection = {
enable = true,
keymaps = {
init_selection = "<CR>",
node_incremental = "<CR>",
node_decremental = "<BS>",
scope_incremental = "<TAB>",
},
},
indent = { enable = true },
})

vim.opt.foldmethod = "expr"
vim.opt.foldexpr = "nvim_treesitter#foldexpr()"
vim.opt.foldlevel = 99

9.5 dashboard.lua

lua/plugin-config/dashboard.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
local status, db = pcall(require, "dashboard")
if not status then return end

db.setup({
theme = 'doom',
config = {
header = {
[[ ]],
[[██████╗ ███████╗████████╗████████╗███╗ ███╗██████╗ ]],
[[██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝████╗ ████║██╔══██╗]],
[[██████╔╝█████╗ ██║ ██║ ██╔████╔██║██████╔╝]],
[[██╔══██╗██╔══╝ ██║ ██║ ██║╚██╔╝██║██╔═══╝ ]],
[[██║ ██║███████╗ ██║ ██║ ██║ ╚═╝ ██║██║ ]],
[[╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ]],
},
center = {
{ icon = " ", desc = "Projects", action = "Telescope projects" },
{ icon = " ", desc = "Recently files", action = "Telescope oldfiles" },
{ icon = " ", desc = "Edit keybindings", action = "edit ~/.config/nvim/lua/keybindings.lua" },
{ icon = " ", desc = "Edit Projects", action = "edit ~/.local/share/nvim/project_nvim/project_history" },
},
footer = {},
},
})

9.6 nvim-autopairs.lua

lua/plugin-config/nvim-autopairs.lua

1
2
3
4
5
6
7
8
9
10
11
local status, autopairs = pcall(require, "nvim-autopairs")
if not status then return end

autopairs.setup({
check_ts = true,
ts_config = { lua = { "string" }, javascript = { "template_string" }, java = false },
})

local cmp_autopairs = require("nvim-autopairs.completion.cmp")
local cmp = require("cmp")
cmp.event:on("confirm_done", cmp_autopairs.on_confirm_done())

9.7 comment.lua

lua/plugin-config/comment.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local status, comment = pcall(require, "Comment")
if not status then return end

comment.setup({
padding = true,
sticky = true,
toggler = { line = "gcc", block = "gbc" },
opleader = { line = "gc", block = "gb" },
extra = { above = "gcO", below = "gc", eol = "gcA" },
mappings = {
extra = false,
},
})

vim.api.nvim_set_keymap("n", "<leader>/", "<cmd>lua require('Comment').toggle()<CR>", { noremap = true })
vim.api.nvim_set_keymap("v", "<leader>/", "<cmd>lua require('Comment').toggle()<CR>", { noremap = true })

9.8 surround.lua

lua/plugin-config/surround.lua

1
2
3
4
local status, surround = pcall(require, "surround")
if not status then return end

surround.setup({ style_separator_prefix = " " })

快捷键:

快捷键 说明
ds + ( ) } " 删除成对符号
cs + ( ) } " 修改成对符号
ys + 动作 + 符号 添加成对符号

9.9 telescope.lua

lua/plugin-config/telescope.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
local status, telescope = pcall(require, "telescope")
if not status then return end

telescope.setup({
defaults = {
initial_mode = "insert",
selection_strategy = "reset",
color_devkit = true,
file_sorter = sorters.get_fuzzy_sorter,
file_ignore_patterns = { "node_modules", ".git", "dist", "target", "__pycache__" },
generic_sorter = sorters.get_fuzzy_sorter,
winblend = 0,
border = {},
borderchars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" },
color_devkit = true,
path_display = { "shorten" },
set_env = { ["COLORTERM"] = "truecolor" },
file_previewer = previewers.vim_buffer_cat.new,
grep_previewer = previewers.vim_buffer_vimgrep.new,
qflist_previewer = previewers.vim_buffer_qflist.new,
},
extensions_list = { "themes", "terms", "projects" },
})

快捷键:

1
2
3
4
5
6
7
8
9
map("n", "<C-p>", "<cmd>Telescope projects<CR>", opt)
map("n", "<C-f>", "<cmd>Telescope live_grep<CR>", opt)
map("n", "<C-s>", "<cmd>Telescope grep_string<CR>", opt)
map("n", "<leader>ff", "<cmd>Telescope find_files<CR>", opt)
map("n", "<leader>fb", "<cmd>Telescope buffers<CR>", opt)
map("n", "<leader>fh", "<cmd>Telescope help_tags<CR>", opt)
map("n", "<leader>fo", "<cmd>Telescope oldfiles<CR>", opt)
map("n", "<leader>fc", "<cmd>Telescope commands<CR>", opt)
map("n", "<leader>fp", "<cmd>Telescope project<CR>", opt)

9.10 toggleterm.lua

lua/plugin-config/toggleterm.lua

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
local status, toggleterm = pcall(require, "toggleterm")
if not status then return end

toggleterm.setup({
size = 12,
open_mapping = [[<F5>]],
hide_numbers = true,
shade_filetypes = {},
shade_terminals = true,
shading_factor = 2,
start_in_insert = true,
insert_mappings = true,
persist_size = false,
direction = "float",
close_on_exit = true,
shell = vim.o.shell,
float_opts = { border = "curved", winblend = 3, highlights = { border = "Normal", background = "Normal" } },
})

function _G.set_terminal_keymaps()
vim.api.nvim_buf_set_keymap(0, "t", "<esc>", [[<C-\><C-n>]], { noremap = true })
vim.api.nvim_buf_set_keymap(0, "t", "<A-h>", [[<C-\><C-n><C-w>h]], { noremap = true })
vim.api.nvim_buf_set_keymap(0, "t", "<A-j>", [[<C-\><C-n><C-w>j]], { noremap = true })
vim.api.nvim_buf_set_keymap(0, "t", "<A-k>", [[<C-\><C-n><C-w>k]], { noremap = true })
vim.api.nvim_buf_set_keymap(0, "t", "<A-l>", [[<C-\><C-n><C-w>l]], { noremap = true })
end

vim.cmd("autocmd! TermOpen term://* lua set_terminal_keymaps()")

local Terminal = require("toggleterm.terminal").Terminal
local htop = Terminal:new({ cmd = "htop", hidden = true, direction = "float" })
function _HTOP_HIDE() htop:toggle() end
vim.api.nvim_set_keymap("n", "<F4>", "<cmd>lua _HTOP_HIDE()<CR>", { silent = true })

9.11 lspsaga.nvim

lua/lsp/ui.lua

1
2
3
4
5
6
7
8
local status, saga = pcall(require, "lspsaga")
if not status then return end

saga.init_lsp_saga({
loader = {
load_charsets_async = true,
},
})

9.12 project.lua

lua/plugin-config/project.lua

1
2
3
4
5
6
7
8
9
10
11
12
local status, project = pcall(require, "project_nvim")
if not status then return end

project.setup({
detection_methods = { "lsp", "pattern" },
patterns = { ".git", "package.json", "CMakeLists.txt", "Makefile" },
})

local status_telescope, _ = pcall(require, "telescope._extensions")
if status_telescope then
pcall(require("telescope").load_extension, "project")
end

9.13 gitsigns.lua

lua/plugin-config/gitsigns.lua

1
2
3
4
local status, gitsigns = pcall(require, "gitsigns")
if not status then return end

gitsigns.setup()

快捷键:

快捷键 说明
]c 跳至下一个差异
[c 跳至上一个差异
<leader>gs Git 显示

9.14 indent-blankline.lua

lua/plugin-config/indent-blankline.lua

1
2
3
4
5
6
7
8
9
local status, indent = pcall(require, "indent_blankline")
if not status then return end

indent.setup({
space_char_blankline = " ",
show_current_context = true,
show_current_context_start = true,
context_pattern_backup = {},
})

十、LSP 配置

10.1 setup.lua

lua/lsp/setup.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
local lspconfig = require("lspconfig")
local protocol = require("vim.lsp.protocol")
local protocol_callbacks = protocol.callbacks
local method_is_available = vim.lsp.handlers["textDocument/hover"]

vim.lsp.handlers["textDocument/hover"] = function(_, result, method, ...)
if vim.tbl_isempty(result or {}) then
return method_is_available(_, result, method, ...)
end
return protocol_callbacks.hover(_, result, method, ...)
end

protocol.symbol_name_provider = {
work_done_progress = false,
}

vim.cmd([[
function! LspSymbol(name, icon) abort
let l:symbols = {
\ 'Text': '🔤',
\ 'Method': '🍔',
\ 'Function': '🍞',
\ 'Variable': '🍜',
\ 'Interface': '🍙',
\ 'File': '🍚',
\ 'Module': '🍛',
\ 'Property': '🍣',
\ 'Field': '🍤',
\ 'Enum': '🍥',
\ 'Keyword': '🍣',
\ 'Constant': '🍣',
\ 'Class': '🍣',
\ 'Struct': '🍣',
\ 'Event': '🍣',
\ 'Operator': '🍣',
\ 'Ref': '🍣',
\ 'TypeParameter': '🍣',
\ 'Parameter': '🍣',
\ 'StaticMethod': '🍣',
\ 'Namespace': '🍣',
\ }
return get(l:symbols, a:name, '📖')
endfunction

augroup LspSymbol_highlight
autocmd!
autocmd WinEnter * silent! lua if vim.tbl_isempty(vim.lsp.buf_get_active_clients()) == false then vim.cmd('highlight LspSymbol guifg=#b4d51c')| endif
augroup END
]])

local list = {
"pyright",
"rust_analyzer",
"tsserver",
"gopls",
"jsonls",
"html",
"cssls",
"julials",
"bashls",
"dockerls",
"yamlls",
"vimls",
"cmake",
"lemminx",
}

for _, server in pairs(list) do
lspconfig[server].setup({
flags = { debounce_text_changes = 500 },
capabilities = require("lsp.cmp").capabilities,
on_attach = function(client, bufnr)
require("lsp.ui").on_attach(client, bufnr)
end,
})
end

10.2 cmp.lua

lua/lsp/cmp.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
local lsp_status_ok, cmp_config = pcall(require, "copilot_cmp.config")
if not lsp_status_ok then
local status_cmp, cmp = pcall(require, "cmp")
if not status_cmp then
return
end

local status_cmp_lsp, cmp_lsp = pcall(require, "cmp_nvim_lsp")
if not status_cmp_lsp then
return
end

local snip_status_ok, luasnip = pcall(require, "luasnip")
if not snip_status_ok then
return
end

local border = {
{ "╭", "LspSagaBorderTitle" },
{ "─", "LspSagaBorderTitle" },
{ "╮", "LspSagaBorderTitle" },
{ "│", "LspSagaBorderTitle" },
{ "╯", "LspSagaBorderTitle" },
{ "─", "LspSagaBorderTitle" },
{ "╰", "LspSagaBorderTitle" },
{ "│", "LspSagaBorderTitle" },
}

local options = {
window = {
completion = {
border = border,
},
documentation = {
border = border,
},
},
snippet = {
expand = function(args)
luasnip.lsp_expand(args.body)
end,
},
mapping = require("keybindings").pluginKeys.cmp(cmp),
sources = cmp.config.sources({
{ name = "nvim_lsp", priority = 1000 },
{ name = "vsnip", priority = 750 },
{ name = "buffer", priority = 500, max_item_count = 10 },
{ name = "path", priority = 250 },
}),
formatting = {
format = function(entry, vim_item)
local lsp_source_icons = {
vscode = " ",
nvimg = " ",
}
vim_item.kind = string.format("%s %s", lsp_source_icons[entry.source.name] or " ", vim_item.kind)
vim_item.menu = ({
nvim_lsp = "[LSP]",
vsnip = "[SNIP]",
buffer = "[BUF]",
path = "[PATH]",
})[entry.source.name]
vim_item.icons = string.format("%s %s", lsp_source_icons[entry.source.name] or " ", vim_item.kind)
return vim_item
end,
},
confirm_opts = {
behavior = cmp.ConfirmBehavior.Replace,
select = false,
},
}

cmp.setup("n", { "<leader>rn", "LspSource" })
cmp.setup(options)

cmp.setup.cmdline("/", {
mapping = cmp.mapping.preset.cmdline(),
sources = {
{ name = "buffer" },
},
})

cmp.setup.cmdline(":", {
mapping = cmp.mapping.preset.cmdline(),
sources = cmp.config.sources({
{ name = "path" },
}, {
{ name = "cmdline" },
}),
})
end

10.3 formatter.lua

lua/lsp/formatter.lua

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
local status, null_ls = pcall(require, "null-ls")
if not status then
return
end

local formatting = null_ls.builtins.formatting
local diagnostics = null_ls.builtins.diagnostics

null_ls.setup({
debug = false,
sources = {
formatting.prettier.with({ extra_filetypes = { "toml", "tsx", "jsx" } }),
formatting.black.with({ extra_args = { "--fast" } }),
formatting.stylua,
formatting.shfmt,
formatting.codespell,
diagnostics.eslint,
formatting.eslint,
},
on_attach = function(client, bufnr)
if client.supports_method("textDocument/formatting") then
vim.api.nvim_clear_autocmds({ group = augroup, buffer = bufnr })
vim.api.nvim_create_autocmd("BufWritePre", {
group = augroup,
buffer = bufnr,
callback = function()
vim.lsp.buf.format({ async = true, bufnr = bufnr })
end,
})
end
end,
})

十一、常见问题

11.1 Neovim 输入法切换

安装 im-select

1
npm install -g im-select

配置 lua/utils/im-select.lua

1
2
3
-- 自动切换输入法
vim.g.im_select_enable = 1
vim.g.im_select_default = 1

11.2 WSL2 打开 Windows 文件管理器

安装 wsl-open

1
npm i -g wsl-open

11.3 禁用自动备份

1
2
3
4
-- ~/.config/nvim/init.lua
vim.o.backup = false
vim.o.writebackup = false
vim.o.swapfile = false

11.4 配置查找与帮助

命令 说明
:h {subject} 查看帮助
:h <C-d> 列出所有主题
:h <option> 查看选项含义
:checkhealth 检查健康状态

一、概述

在分布式系统中,特别是在分库分表场景下,生成全局唯一ID是一个关键的技术挑战。单纯的生成全局ID并不困难,但生成的ID需要满足分布式系统的特定要求:

  1. 无单点故障:ID生成服务必须具备高可用性
  2. 时间有序性:ID应包含时间信息或按时间排序,便于索引优化和冷热数据分离
  3. 分片可控性:能够控制ShardingId,使相关数据位于同一分片,提高查询和修改效率
  4. 长度适中:最好为64bit,便于使用long类型操作,避免组件兼容性问题

本文将详细介绍几种主流的全局唯一ID生成方案。

二、Snowflake算法(Twitter方案)

2.1 背景

Twitter在将存储系统从MySQL迁移到Cassandra时,由于Cassandra没有内置的顺序ID生成机制,开发了Snowflake全局唯一ID生成服务。

2.2 算法结构

Snowflake生成的ID为64位,结构如下:

1
0 | 41位时间戳 | 10位机器标识 | 12位序列号

各字段说明

  • 符号位:1位,始终为0
  • 时间戳:41位,精确到毫秒,可使用69年(从自定义起始时间开始)
  • 机器标识:10位,最多支持1024个节点
  • 序列号:12位,每个节点每毫秒可生成4096个ID

2.3 核心原理

![[Snowflake算法原理图.png]]

2.4 Java实现示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class IdWorker {
private final long workerId;
private final static long twepoch = 1288834974657L;
private long sequence = 0L;
private final static long workerIdBits = 4L;
public final static long maxWorkerId = -1L ^ -1L << workerIdBits;
private final static long sequenceBits = 10L;
private final static long workerIdShift = sequenceBits;
private final static long timestampLeftShift = sequenceBits + workerIdBits;
public final static long sequenceMask = -1L ^ -1L << sequenceBits;
private long lastTimestamp = -1L;

public IdWorker(final long workerId) {
super();
if (workerId > this.maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format(
"worker Id can't be greater than %d or less than 0",
this.maxWorkerId));
}
this.workerId = workerId;
}

public synchronized long nextId() {
long timestamp = this.timeGen();
if (this.lastTimestamp == timestamp) {
this.sequence = (this.sequence + 1) & this.sequenceMask;
if (this.sequence == 0) {
timestamp = this.tilNextMillis(this.lastTimestamp);
}
} else {
this.sequence = 0;
}

if (timestamp < this.lastTimestamp) {
try {
throw new Exception(
String.format(
"Clock moved backwards. Refusing to generate id for %d milliseconds",
this.lastTimestamp - timestamp));
} catch (Exception e) {
e.printStackTrace();
}
}

this.lastTimestamp = timestamp;
long nextId = ((timestamp - twepoch) << timestampLeftShift)
| (this.workerId << this.workerIdShift) | (this.sequence);
return nextId;
}

private long tilNextMillis(final long lastTimestamp) {
long timestamp = this.timeGen();
while (timestamp <= lastTimestamp) {
timestamp = this.timeGen();
}
return timestamp;
}

private long timeGen() {
return System.currentTimeMillis();
}
}

2.5 优缺点分析

优点

  • 高性能,低延迟
  • 独立应用,不依赖外部服务
  • ID按时间有序

缺点

  • 需要独立开发和部署
  • 时钟回拨问题需要处理
  • 机器标识需要管理

三、Flickr数据库方案

3.1 方案概述

Flickr利用MySQL的自增ID特性,通过auto_incrementreplace into和MyISAM引擎实现全局ID生成。

3.2 实现步骤

创建专用表

1
2
3
4
5
6
CREATE TABLE Tickets64 (
id bigint(20) unsigned NOT NULL auto_increment,
stub char(1) NOT NULL default '',
PRIMARY KEY (id),
UNIQUE KEY stub (stub)
) ENGINE=MyISAM

生成ID操作

在事务会话中执行:

1
2
REPLACE INTO Tickets64 (stub) VALUES ('a');
SELECT LAST_INSERT_ID();

高可用配置

通过配置两台MySQL服务器,设置不同的起始值和步长来生成奇偶数ID:

1
2
3
4
5
6
7
# TicketServer1配置
auto-increment-increment = 2
auto-increment-offset = 1

# TicketServer2配置
auto-increment-increment = 2
auto-increment-offset = 2

客户端通过轮询方式获取ID。

3.3 优缺点分析

优点

  • 利用数据库自增ID机制,可靠性高
  • 生成的ID有序
  • 实现相对简单

缺点

  • 需要独立的MySQL实例,资源消耗大
  • 性能受数据库限制
  • 存在单点故障风险(需额外配置高可用)

四、UUID方案

4.1 基本概念

UUID(Universally Unique Identifier)生成的是32位16进制格式的字符串,转换为byte数组为16个字节,即128bit。

4.2 生成原理

UUID算法的核心思想是结合机器的网卡地址、当地时间和一个随机数来生成唯一标识符。

4.3 唯一性保证

理论上,如果一台机器每秒产生1000万个GUID,可以保证(概率意义上)3240年不重复。

4.4 优缺点分析

优点

  • 本地生成,无需远程调用,延迟低
  • 扩展性好,基本无性能上限
  • 全球唯一性保证

缺点

  • 无法保证趋势递增
  • 长度过长(128bit),作为主键时索引效率低
  • 常见优化方案存在局限性:
    • 转化为两个uint64整数存储
    • 折半存储(可能影响唯一性)

五、基于Redis的分布式ID生成器

5.1 实现原理

利用Redis的Lua脚本执行功能,在每个节点上通过Lua脚本生成唯一ID。

5.2 ID结构

生成的ID为64位,结构如下:

  • 41位时间戳:精确到毫秒,可使用41年
  • 12位逻辑分片ID:最大分片ID为4095
  • 10位自增长ID:每个节点每毫秒最多生成1024个ID

5.3 生成示例

假设:

  • GTM时间:Fri Mar 13 10:00:00 CST 2015
  • 毫秒数:1426212000000
  • 分片ID:53
  • 自增长序列:4

生成ID计算:

1
5981966696448054276 = 1426212000000 << 22 + 53 << 10 + 4

5.4 Redis命令使用

Redis提供TIME命令获取服务器上的秒数和微秒数。Lua脚本返回四元组:(second, microSecond, partition, seq)

客户端处理逻辑:

1
2
long id = ((second * 1000 + microSecond / 1000) << (12 + 10)) 
+ (shardId << 10) + seq;

六、MongoDB ObjectId方案

6.1 设计考虑

MongoDB的_id字段需要满足分布式环境下的全局唯一性要求,因此不能使用自增主键,而是采用ObjectId对象。

6.2 ObjectId结构

ObjectId使用12字节存储空间,结构如下:

字节位置 0-3 4-6 7-8 9-11
内容 时间戳 机器ID 进程ID 计数器

各字段说明

  • 时间戳(4字节):从标准纪元开始的秒数
  • 机器ID(3字节):服务器主机标识,通常是主机名的散列值
  • 进程ID(2字节):mongod进程标识符
  • 计数器(3字节):自动增加的计数器,每个进程独立

6.3 唯一性保证机制

  1. 时间戳:保证秒级唯一性
  2. 机器ID:考虑分布式环境,避免时钟同步问题
  3. 进程ID:保证同一服务器上多个mongod实例的唯一性
  4. 计数器:保证同一秒内的唯一性(最多16777216个)

6.4 生成位置

_id可以在服务器端生成,也可以在客户端生成。客户端生成可以降低服务器端压力。

七、方案对比

方案 优点 缺点 适用场景
Snowflake 高性能、有序、独立 需独立部署、时钟问题 大规模分布式系统
Flickr数据库 简单、可靠、有序 性能受限、资源消耗大 中小规模系统
UUID 全球唯一、无中心化 无序、存储效率低 需要全局唯一标识
Redis方案 性能好、可扩展 依赖Redis、配置复杂 Redis环境下的系统
MongoDB ObjectId 内置支持、分布式友好 MongoDB专用 MongoDB数据库系统

八、选择建议

8.1 考虑因素

  1. 系统规模:大规模系统推荐Snowflake,中小规模可考虑数据库方案
  2. 性能要求:高并发场景选择Snowflake或Redis方案
  3. 有序性需求:需要有序ID时选择Snowflake或数据库方案
  4. 技术栈:现有技术栈影响方案选择(如已使用MongoDB)

8.2 最佳实践

  1. Snowflake:适合需要高性能、有序ID的大规模分布式系统
  2. 数据库方案:适合对数据库依赖较强的系统,实现简单
  3. 混合方案:可根据业务场景组合使用不同方案

九、总结

全局唯一ID生成是分布式系统中的基础且重要的技术。不同的方案各有优劣,选择时需要综合考虑系统规模、性能要求、有序性需求和技术栈等因素。

在实际应用中,建议:

  1. 明确业务需求,选择最适合的方案
  2. 考虑系统的扩展性和维护成本
  3. 做好异常处理,特别是时钟回拨等问题
  4. 定期评估和优化ID生成策略

随着技术发展,新的ID生成方案不断涌现,但核心原则不变:在保证唯一性的前提下,追求更高的性能和更好的扩展性。


参考资源

  • Twitter Snowflake官方文档
  • Flickr技术博客
  • MongoDB官方文档
  • Redis官方文档

如何在高并发分布式系统中生成全局唯一Id - 滴答的雨 - 博客园

Excerpt

如何在高并发分布式系统中生成全局唯一Id。
1、使用数据库自增Id
2、单独开一个数据库,获取全局唯一的自增序列号或各表的MaxId
3、Sequence特性
4、通过数据库集群编号+集群内的自增类型两个字段共同组成唯一主键
5、通过设置每个集群中自增 ID 起始点
6、GU


  最近公司用到,并且在找最合适的方案,希望大家多参与讨论和提出新方案。我和我的小伙伴们也讨论了这个主题,我受益匪浅啊……

博文示例:

1.         GUID生成Int64值后是否还具有唯一性测试

2.         Random生成高唯一性随机码

今天分享的主题是:如何在高并发分布式系统中生成全局唯一Id。

但这篇博文实际上是“半分享半讨论”的博文:

1)         半分享是我将说下我所了解到的关于今天主题所涉及的几种方案。

2)         半讨论是我希望大家对各个方案都说说自己的见解,更加希望大家能提出更好的方案。(我还另外提问在此:http://q.cnblogs.com/q/53552/

我了解的方案如下……………………………………………………………………

1、  使用数据库自增Id

优势:编码简单,无需考虑记录唯一标识的问题。

缺陷:

1)         在大表做水平分表时,就不能使用自增Id,因为Insert的记录插入到哪个分表依分表规则判定决定,若是自增Id,各个分表中Id各自增长就会重复

2)         在业务上操作父、子表(即关联表)插入时,需要在插入数据库之前获取max(id)用于标识父表和子表关系,若存在并发获取max(id)的情况,max(id)会同时被别的线程获取到。

3)         DB数据记录都是可以根据ID号进行推测出来,对于一些数据敏感的场景,不建议采用

结论:适合小应用,无需分表,低并发。

2、  单独开一个数据库,获取全局唯一的自增序列号或各表的MaxId

使用MaxId表存储各表的MaxId值

专门一个数据库,记录各个表的MaxId值,建一个存储过程来取Id,逻辑大致为:开启事物,对于在表中不存在记录,直接返回一个默认值为1的键值,同时插入该条记录到table_key表中。而对于已存在的记录,key值直接在原来的key基础上加1更新到MaxId表中并返回key。(给table_key中为每个表初始化一条key为1的记录,这样就不用每次if来判断了—@辉_辉 提议)

使用此方案的问题是:每次的查询MaxId是一个性能损耗;

详细可参考:《使用MaxId表存储各表的MaxId值,以获取全局唯一Id》

                   我截取此文中的sql语法如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

第一步:创建表

create table table_key

(

       table_name   varchar(50) not null primary key,

       key_value    int         not null

)

第二步:创建存储过程来取自增ID

create procedure up_get_table_key

(

   @table_name     varchar(50),

   @key_value      int output

)

as

begin

     begin tran

         declare @key  int

         set @key=1

         if not exists(select table_name from table_key where table_name=@table_name)

            begin

              insert into table_key values(@table_name,@key)       

            end

         else   

            begin

                select @key=key_value from table_key with (nolock) where table_name=@table_name

                set @key=@key+1

                update table_key set key_value=@key where table_name=@table_name

            end

    set @key_value=@key

    commit tran

        if @@error>0

      rollback tran

end

2.         (@乐活的CodeMonkey)提醒提高获取ID时存储过程的隔离级别,避免读取到未提交事务导致并发ID重复的问题。(MSSQL事务隔离级别详解

1

2

3

4

5

6

7

eg:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED

GO

BEGIN TRANSACTION;

……

GO

COMMIT TRANSACTION;

3.         (@土豆烤肉)存储过程中不使用事物,一旦使用到事物性能就急剧下滑。直接使用UPDATE获取到的更新锁,即SQL SERVER会保证UPDATE的顺序执行。(已在用户过千万的并发系统中使用)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

create procedure [dbo].[up_get_table_key]

(

   @table_name     varchar(50),

   @key_value      int output

)

as

begin

    SET NOCOUNT ON;

    DECLARE @maxId INT

    UPDATE table_key

    SET @maxId = key_value,key_value = key_value + 1

    WHERE table_name=@table_name

    SELECT @maxId

end

结论:适用中型应用,此方案解决了分表,关联表插入记录的问题。但是无法满足高并发性能要求。存在单点问题

        改进方案:时间信息 + 缓存总的maxid  (@wee616 提议)

              从redis中用lpop指令取指定key值的数据。(lpop:移除并返回列表的头元素)
              如果将指定key值的数据取完了,会触发初始化。
              初次初始化:
                  1)用for update锁表,存储最小值1和最大值50到数据库中。
                  2)将这50个数字放入redis中。
              下次初始化:
                  1)用for update锁表,存储最小值51和最大值100到数据库中。
                  2)将这50个数字放入redis中。

               数据库每天有脚本定时清理这个表,每天都将最小值归0,避免最大值过大。

结论:适合大型应用,生成Id顺序性,可读性比较好。

3、  Sequence特性

这个特性在SQL Server 2012、Oracle中可用。这个特性是数据库级别的,允许在多个表之间共享序列号。它可以解决分表在同一个数据库的情况,但倘若分表放在不同数据库,那将共享不到此序列号。(eg:Sequence使用场景:你需要在多个表之间公用一个流水号。以往的做法是额外建立一个表,然后存储流水号)

相关Sequence特性资料:

SQL Server2012中的SequenceNumber尝试

SQL Server 2012 开发新功能——序列对象(Sequence)

identity和sequence的区别

Difference between Identity and Sequence in SQL Server 2012

结论:适用中型应用,此方案不能完全解决分表问题。

4、  通过数据库集群编号+集群内的自增类型两个字段共同组成唯一主键

优点:实现简单,维护也比较简单。

缺点:关联表操作相对比较复杂,需要两个字段。并且业务逻辑必须是一开始就设计为处理复合主键的逻辑,倘若是到了后期,由单主键转为复合主键那改动成本就太大了。

结论:适合大型应用,但需要业务逻辑配合处理复合主键。

5、  通过设置每个集群中自增 ID 起始点(auto_increment_offset),将各个集群的ID进行绝对的分段来实现全局唯一。当遇到某个集群数据增长过快后,通过命令调整下一个 ID 起始位置跳过可能存在的冲突。

优点:实现简单,且比较容易根据 ID 大小直接判断出数据处在哪个集群,对应用透明。缺点:维护相对较复杂,需要高度关注各个集群 ID 增长状况。

结论:适合大型应用,但需要高度关注各个集群 ID 增长状况。

6、  GUID(Globally Unique Identifier,全局唯一标识符)

GUID通常表示成32个16进制数字(0-9,A-F)组成的字符串,如:{21EC2020-3AEA-1069-A2DD-08002B30309D},它实质上是一个128位长的二进制整数。

GUID制定的算法中使用到用户的网卡MAC地址,以保证在计算机集群中生成唯一GUID;在相同计算机上随机生成两个相同GUID的可能性是非常小的,但并不为0。所以,用于生成GUID的算法通常都加入了非随机的参数(如时间),以保证这种重复的情况不会发生。

优点:GUID是最简单的方案,跨平台,跨语言,跨业务逻辑,全局唯一的Id,数据间同步、迁移都能简单实现。

缺点:

1)         存储占了32位,且无可读性;

2)         插入时因为GUID是无需的,在聚集索引的排序规则下可能移动大量的记录。

有两位园友主推GUID,无须顺序GUID方案原因如下:

@徐少侠           GUID无序在并发下效率高,并且一个数据页内添加新行,是在B树内增加,本质没有什么数据被移动,唯一可能的,是页填充因子满了,需要拆页。而GUID方案导致的拆页比顺序ID要低太多了

@无色                我们要明白id是什么,是身份标识,标识身份是id最大的业务逻辑,不要引入什么时间,什么用户业务逻辑,那是另外一个字段干的事,使用base64(guid,uuid),是通盘考虑,完全可以更好的兼容nosql,key-value存储。

结论:适合大型应用;生成的Id不够友好;占据了32位;

改进:

1)         (@dudu告知)在SQL Server 2005中新增了NEWSEQUENTIALID函数。

详细请看:《理解newid()和newsequentialid()》

在指定计算机上创建大于先前通过该函数生成的任何 GUID 的 GUID。 newsequentialid 产生的新的值是有规律的,则索引B+树的变化是有规律的,就不会导致索引列插入时移动大量记录的问题。

但一旦服务器重新启动,其再次生成的GUID可能反而变小(但仍然保持唯一)。这在很大程度上提高了索引的性能,但并不能保证所生成的GUID一直增大。SQL的这个函数产生的GUID很简单就可以预测,因此不适合用于安全目的。

a)         只能做为数据库列的DEFAULT VALUE,不能执行类似SELECT NEWSEQUENTIALID()的语句.

b)         如何获得生成的GUID.

如果生成的GUID所在字段做为外键要被其他表使用,我们就需要得到这个生成的值。通常,PK是一个IDENTITY字段,我们可以在INSERT之后执行 SELECT SCOPE_IDENTITY()来获得新生成的ID,但是由于NEWSEQUENTIALID()不是一个INDETITY类型,这个办法是做不到了,而他本身又只能在默认值中使用,不可以事先SELECT好再插入,那么我们如何得到呢?有以下两种方法:

1

2

3

4

5

6

7

8

9

10

11

12

DECLARE @outputTable TABLE(ID uniqueidentifier)

INSERT INTO TABLE1(col1, col2)

OUTPUT INSERTED.ID INTO @outputTable

VALUES('value1', 'value2')

SELECT ID FROM @outputTable

INSERT INTO TABLE1(col1, col2)

VALUES('value1', 'value2')

SELECT ROWGUIDCOL FROM TABLE1

结论:适合大型应用,解决了GUID无序特性导致索引列插入移动大量记录的问题。但是在关联表插入时需要返回数据库中生成的GUID;生成的Id不够友好;占据了32位。

2)         “COMB”(combined guid/timestamp,意思是:组合GUID/时间截)

(感谢:@ ethan-luo ,@lcs-帅

COMB数据类型的基本设计思路是这样的:既然GUID数据因毫无规律可言造成索引效率低下,影响了系统的性能,那么能不能通过组合的方式,保留GUID的10个字节,用另6个字节表示GUID生成的时间(DateTime),这样我们将时间信息与GUID组合起来,在保留GUID的唯一性的同时增加了有序性,以此来提高索引效率。

在NHibernate中,COMB型主键的生成代码如下所示:

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

/// <summary> /// Generate a new <see cref="Guid"/> using the comb algorithm.

/// </summary>

private Guid GenerateComb()

{

    byte[] guidArray = Guid.NewGuid().ToByteArray();

    DateTime baseDate = new DateTime(1900, 1, 1);

    DateTime now = DateTime.Now;

    TimeSpan days = new TimeSpan(now.Ticks - baseDate.Ticks);

    TimeSpan msecs = now.TimeOfDay;

    byte[] daysArray = BitConverter.GetBytes(days.Days);

    byte[] msecsArray = BitConverter.GetBytes((long)

      (msecs.TotalMilliseconds / 3.333333));

    Array.Reverse(daysArray);

    Array.Reverse(msecsArray);

    Array.Copy(daysArray, daysArray.Length - 2, guidArray,

      guidArray.Length - 6, 2);

    Array.Copy(msecsArray, msecsArray.Length - 4, guidArray,

      guidArray.Length - 4, 4);

    return new Guid(guidArray);

}

结论:适合大型应用。即保留GUID的唯一性的同时增加了GUID有序性,提高了索引效率;解决了关联表业务问题;生成的Id不够友好;占据了32位。

3)         长度问题,使用Base64或Ascii85编码解决。(要注意的是上述有序性方案在进行编码后也会变得无序)

如:

GUID:{3F2504E0-4F89-11D3-9A0C-0305E82C3301}

当需要使用更少的字符表示GUID时,可能会使用Base64或Ascii85编码。Base64编码的GUID有22-24个字符,如:

7QDBkvCA1+B9K/U0vrQx1A

7QDBkvCA1+B9K/U0vrQx1A==

Ascii85编码后是20个字符,如:

5:$Hj:Pf\4RLB9%kU\Lj

                   代码如:

         Guid guid = Guid.NewGuid();

         byte[] buffer = guid.ToByteArray();

         var shortGuid = Convert.ToBase64String(buffer);

                   结论:适合大型应用,缩短GUID的长度。生成的Id不够友好;

7、  GUID TO Int64

对于GUID的可读性,有园友给出如下方案:(感谢:@黑色的羽翼

即将GUID转为了19位数字,数字反馈给客户可以一定程度上缓解友好性问题。EG:

GUID: cfdab168-211d-41e6-8634-ef5ba6502a22    (不友好)

Int64: 5717212979449746068                                      (友好性还行)

不过我的小伙伴说ToInt64后就不唯一了。因此我专门写了个并发测试程序,后文将给出测试结果截图及代码简单说明。

(唯一性、业务适合性是可以权衡的,这个唯一性肯定比不过GUID的,一般程序上都会安排错误处理机制,比如异常后执行一次重插的方案……)

结论:适合大型应用,生成相对友好的Id****(纯数字)。****

8、  自己写编码规则

优点:全局唯一Id,符合业务后续长远的发展(可能具体业务需要自己的编码规则等等)。

缺陷:根据具体编码规则实现而不同;还要考虑倘若主键在业务上允许改变的,会带来外键同步的麻烦。

我这边写两个编码规则方案:(可能不唯一,只是个人方案,也请大家提出自己的编码规则)

1)         12位年月日时分秒+5位随机码+3位服务器编码  (这样就完全单机完成生成全局唯一编码)-–共20位

缺陷:因为附带随机码,所以编码缺少一定的顺序感。(生成高唯一性随机码的方案稍后给给出程序)

2)         12位年月日时分秒+5位流水码+3位服务器编码 (这样流水码就需要结合数据库和缓存)-–共20位   (将影响顺序权重大的“流水码”放前面,影响顺序权重小的服务器编码放后)

缺陷:因为使用到流水码,流水码的生成必然会遇到和MaxId、序列表、Sequence方案中类似的问题

(为什么没有毫秒?毫秒也不具备业务可读性,我改用5位随机码、流水码代替,推测1秒内应该不会下99999[五位]条语法)

结论:适合大型应用,从业务上来说,有一个规则的编码能体现产品的专业成度。

GUID生成Int64值后是否还具有唯一性测试

测试环境

clip_image002

主要测试思路:

1.         根据内核数使用多线程并发生成Guid后再转为Int64位值,放入集合A、B、…N,多少个线程就有多少个集合。

2.         再使用Dictionary字典高效查key的特性,将步骤1中生成的多个集合全部加到Dictionary中,看是否有重复值。

示例注解:测了 Dictionary<long,bool> 最大容量就在5999470左右,所以每次并发生成的唯一值总数控制在此范围内,让测试达到最有效话。

主要代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

for (int i = 0; i <= Environment.ProcessorCount - 1; i++)

{

    ThreadPool.QueueUserWorkItem(

        (list) =>

        {

            List<long> tempList = list as List<long>;

            for (int j = 1; j < listLength; j++)

            {

                byte[] buffer = Guid.NewGuid().ToByteArray();

                tempList.Add(BitConverter.ToInt64(buffer, 0));

            }

            barrier.SignalAndWait();

        }, totalList[i]);

}

测试数据截图:                                                                           

clip_image004

数据一(循环1000次,测试数:1000*5999470)

image

数据二(循环5000次,测试数:5000*5999470)--跑了一个晚上……

image

**感谢@Justany_WhiteSnow****的专业回答:(**大家分析下,我数学比较差,稍后再说自己的理解)

GUID桶数量:(2 ^ 4) ^ 32 = 2 ^ 128

Int64桶数量: 2 ^ 64

倘若每个桶的机会是均等的,则每个桶的GUID数量为:

(2 ^ 128) / (2 ^ 64) = 2 ^ 64 = 18446744073709551616

也就是说,其实重复的机会是有的,只是概率问题。

楼主测试数是29997350000,发生重复的概率是:

1 - ((1 - (1 / (2 ^ 64))) ^ 29997350000) ≈ 1 - ((1 - 1 / (2 ^ 64)) ^ (2 ^ 32)) < 1 - 1 + 1 / (2 ^ 32) = 1 / (2 ^ 32) ≈ 2.3283064e-10

(唯一性、业务适合性是可以权衡的,这个唯一性肯定比不过GUID的,一般程序上都会安排错误处理机制,比如异常后执行一次重插的方案……)

(唯一性、业务适合性是可以权衡的,这个唯一性肯定比不过GUID的,一般程序上都会安排错误处理机制,比如异常后执行一次重插的方案……)

结论:GUID转为Int64值后,也具有高唯一性,可以使用与项目中。

Random生成高唯一性随机码

我使用了五种Random生成方案,要Random生成唯一主要因素就是种子参数要唯一。

不过该测试是在单线程下的,多线程应使用不同的Random实例,所以对结果影响不会太大。

1.         使用Environment.TickCount做为Random参数(即Random的默认参数),重复性最大。

2.         使用DateTime.Now.Ticks做为Random参数,存在重复。

3.         使用unchecked((int)DateTime.Now.Ticks)做为Random参数,存在重复。

4.         使用Guid.NewGuid().GetHashCode()做为random参数,测试不存在重复(或存在性极小)。

5.         使用RNGCryptoServiceProvider做为random参数,测试不存在重复(或存在性极小)。

即:

        static int GetRandomSeed()

        {

            byte[] bytes = new byte[4];

            System.Security.Cryptography.RNGCryptoServiceProvider rng

= new System.Security.Cryptography.RNGCryptoServiceProvider();

            rng.GetBytes(bytes);

            return BitConverter.ToInt32(bytes, 0);

        }

测试结果:

clip_image007

*结论:随机码使用RNGCryptoServiceProvider***或Guid.NewGuid().GetHashCode()**生成的唯一性较高。

一些精彩评论(部分更新到原博文对应的地方)

一、

数据库文件体积只是一个参考值,可水平扩展系统性能(如nosql,缓存系统)并不和文件体积有高指数的线性相关。

如taobao/qq的系统比拼byte系统慢,关键在于索引的命中率,缓存,系统的水平扩展。

如果数据库很少,你搞这么多byte能提高性能?

如果数据库很大,你搞这么多byte不兼容索引不兼容缓存,不是害自已吗?

如果数据库要求伸缩性,你搞这么多byte,需要不断改程序,不是自找苦吗?

如果数据库要求移植性,你搞这么多byte,移植起来不如重新设计,这是不是很多公司不断加班的原因?

不依赖于数据存储系统是分层设计思想的精华,实现战略性能最大化,而不是追求战术单机性能最大化。

不要迷信数据库性能,不要迷信三范式,不要使用外键,不要使用byte,不要使用自增id,不要使用存储过程,不要使用内部函数,不要使用非标准sql,存储系统只做存储系统的事。当出现系统性能时,如此设计的数据库可以更好的实现迁移数据库(如mysql->oracle),实现nosql改造((mongodb/hadoop),实现key-value缓存(redis,memcache)。

二、

很多程序员有对性能认识有误区,如使用存储过程代替正常程序,其实使用存储过程只是追求单服务器的高性能,当需要服务器水平扩展时,存储过程中的业务逻辑就是你的噩运。(web服务器可以简单伸缩,但是数据库伸缩比较复杂)

三、

除数字日期,能用字符串存储的字段尽量使用字符串存储,不要为节省那不值钱的1个g的硬盘而使用类似字节之类的字段,进而大幅牺牲系统可伸缩性和可扩展性。

不要为了追求所谓的性能,引入byte,使用byte注定是短命和难于移植,想想为什么html,email一直流行,因为它们使用的是字符串表示法,只要有人类永远都能解析,如email把二进制转成base64存储。除了实时系统,视频外,建议使用字符串来存储数据,系统性能的关键在于分布式,在于水平扩展。

本次博文到此结束,希望大家对本次主题“如何在高并发分布式系统中生成全局唯一Id”多提出自己宝贵的意见。另外看着感觉舒服,还请多帮推荐…推荐……

一、前言

Gitea Actions 是 Gitea 1.19+ 内置的 CI/CD 组件,兼容 GitHub Actions 语法与生态,基于 Docker 可实现一键部署、环境隔离、运维极简,适合自建代码托管与自动化流水线。

本文全程使用 Docker + Docker Compose 部署,命令可直接复制执行。


二、环境要求

  • 系统:Linux x86_64

  • 依赖:Docker、Docker Compose

  • 内存:≥4GB

  • 端口:3000(Web)、222(Git SSH)


三、Docker Compose 部署 Gitea

1. 创建部署目录

1
mkdir -p /opt/gitea && cd /opt/gitea

2. 编写 docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
version: "3"

services:
gitea:
image: gitea/gitea:latest
container_name: gitea
restart: always
ports:
- "3000:3000"
- "222:22"
volumes:
- ./data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
environment:
- GITEA__actions__ENABLED=true

3. 启动 Gitea

1
docker compose up -d

4. 初始化配置

访问 http://服务器IP:3000,按页面提示完成初始化。


四、Docker 部署 Act Runner

1. 创建 Runner 数据目录

1
2
mkdir -p /opt/gitea/act-runner
chmod 777 /opt/gitea/act-runner

2. 获取 Runner 注册令牌

  1. 管理员登录 Gitea

  2. 管理后台 → Actions → Runners

  3. 点击「注册新 Runner」,复制令牌

3. 注册 Runner

1
2
3
4
5
6
7
8
docker run --rm \
-v /opt/gitea/act-runner:/data \
gitea/act_runner:latest \
act_runner register \
--instance http://服务器IP:3000 \
--token 你的注册令牌 \
--labels ubuntu-latest:docker://node:16-bullseye \
--no-interactive

4. 启动 Runner 容器

1
2
3
4
5
6
7
docker run -d \
--name act-runner \
--restart always \
-v /opt/gitea/act-runner:/data \
-v /var/run/docker.sock:/var/run/docker.sock \
gitea/act_runner:latest \
act_runner daemon

五、仓库启用 Actions

  1. 进入目标仓库 → 设置

  2. 勾选「启用 Actions」

  3. 保存设置


六、创建 CI/CD 工作流

在仓库创建 .gitea/workflows/demo.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
name: Gitea Actions Demo
on: [push]
jobs:
Explore-Gitea-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 本次任务由 ${{ gitea.event_name }} 事件触发。"
- run: echo "🐧 任务正在 Gitea 提供的 ${{ runner.os }} 服务器上运行!"
- name: 检出代码
uses: actions/checkout@v3
- name: 列出文件
run: |
ls -la ${{ gitea.workspace }}
- run: echo "✅ 工作流执行完成。"

七、触发流水线与查看日志

  1. 提交并推送工作流文件
1
2
3
git add .gitea/workflows/demo.yaml
git commit -m "add ci workflow"
git push
  1. 进入仓库 → Actions 查看运行状态与实时日志

八、常用运维命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 重启 Gitea
docker compose restart gitea

# 重启 Runner
docker restart act-runner

# 查看 Gitea 日志
docker logs -f gitea

# 查看 Runner 日志
docker logs -f act-runner

# 停止服务
docker compose down
docker stop act-runner

九、总结

  • Docker 部署 Gitea 开箱即用,通过环境变量可直接开启 Actions 功能,无需复杂配置,极大降低部署门槛;

  • Act Runner 以 Docker 容器形式运行,通过挂载 /var/run/docker.sock 实现容器内任务执行,保证环境隔离性与兼容性;

  • Gitea Actions 高度兼容 GitHub Actions 工作流语法与生态,无需额外学习新知识点,可直接复用现有 GitHub Actions 插件与配置;

  • 实现代码托管与 CI/CD 一体化,流程简洁、运维便捷,非常适合个人及小团队自建轻量型 DevOps 平台

0%