風雪and風雪 a089b14908 fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): measure process and system CPU by delta, honour cgroup limits (#82)
* fix(metrics): measure process and system CPU by delta, honour cgroup limits
The process card called gopsutil's Process.CPUPercent(), which returns
100 * lifetimeCPUTime / uptime -- an average over the process' entire
history rather than what it is doing now. Two failure modes followed:
- a long-lived panel settles on a near-constant figure that lags real
spikes by hours;
- when the start time cannot be resolved (BootTime from sysinfo
disagrees with /proc/stat inside some containers) totalTime goes
negative and the call silently yields 0. Measured here: a permanent
0.0% regardless of load.
The system card had the opposite bias inside a container: cpu.Percent()
reads /proc/stat, so a container capped at 4 of 32 host cores topped out
around 21% while actually being throttled. It also only described 200ms
of each second and blocked the request thread doing it.
Both cards now difference cumulative counters across two samples:
- CPU:     cgroup v2 cpu.stat / v1 cpuacct.usage, host /proc/stat fallback
- process: /proc/<pid>/stat user+system (Iowait excluded: it is block
I/O delay, waiting rather than consuming)
- shared denominator cpuCapacity() = min(CFS quota, cpuset, online CPUs)
so the process and system cards finally speak one unit.
Measured on a 4-of-32-core container:
system CPU busy    21%  -> 100%
process CPU busy    0%  ->  89%   (returns to ~0% once idle)
Also fixed along the way:
- GetSystemStats sampled outside its lock, so two concurrent requests
differenced the network counters over ~0s and reported huge spikes.
Sampling now serialises behind a barrier with a minimum interval.
- The disk card hard-coded "/" even when the SQLite database lives on a
mounted volume; it now anchors on the database directory, which
disk.Usage resolves to its containing filesystem via statfs.
- The Telegram resource alerts used the same broken samplers, so a CPU
threshold inside a limited container could never be reached and the
alert would never fire.
New tests cover load tracking, cgroup and quota arithmetic, cpuset
parsing and the already-guarded process memory denominator invariant.
Full suite plus -race passes; darwin/linux-arm64/linux-386 cross-compile.
* fix(memory): report cgroup working set instead of gross usage
The previous commit moved the memory card onto cgroup accounting, which
fixed the two cards disagreeing about their source but swapped in a new
error on the other side: it used memory.current verbatim, and that is a
gross figure billing the cgroup for reclaimable page cache.
Measured on this host against an 8GB cap:
memory.current        4.44 GB
anon                0.90 GB   <- real process memory
inactive_file       3.05 GB   <- reclaimable by the kernel on demand
current / limit       51.7%     <- what the card showed
working set / limit   16.2%     <- actual pressure
So the bar sat in the orange band (>=50%) while 84% of the budget was
free. Reading a large file once -- mihomo's geoip database, a rule
provider download, SQLite growth -- is enough to trigger it, because the
kernel keeps those pages cached and simply hands them back under demand.
"Used" is now the kubelet working-set figure, usage minus inactive_file,
which is what container runtimes report as a percentage. Only *inactive*
cache is discounted, so pages actively mapped and in use stay charged and
the bar can still hold every process' RSS. Three related points:
- The label finally means the same thing on both branches. The
/proc/meminfo fallback already excluded buffers and cache, so gross
cgroup usage and host "used" were two different quantities behind one
label depending on how the panel was deployed.
- The denominator no longer requires a cap to use cgroup accounting. An
unlimited container previously fell back to host-wide figures and so
tracked other tenants' activity, while the CPU card (which always
uses the container's own counter) did not. It falls back to the host
total now only because that genuinely is the cap.
- PAGE_COUNTER_MAX from cgroup v1 is still treated as "no limit".
New tests pin the arithmetic against the real numbers above, the three
ways the limit resolves, and the clamp when cache exceeds usage.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
2026-09-27 00:10:01 +08:00
2026-08-19 21:18:31 +08:00

3m-ui

3m-ui logo

Languages / 语言: README · English · 简体中文 · 繁體中文 · 日本語 · 한국어 · Español · Français · Deutsch · Русский · Português (Brasil) · Tiếng Việt · Bahasa Indonesia · ไทย · Türkçe · العربية · हिन्दी · Polski · Українська

Mihomo 服务端 Web 管理面板

稳定版优先: 默认安装和更新只使用正式 Release。预发布需要主动选择。完整安装包及 Docker 镜像包含固定版本的 Mihomo;详见 安装、升级与恢复。

轻量、自托管,用于在 Linux 上管理 Mihomo Listener、用户、订阅与运行状态。

License Go

文档:https://3m-ui.top/docs/ · 官网:https://3m-ui.top/


特性

类别 能力
节点 协议注册表驱动的 Listener:VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC 等;Reality、TLS 自签、一键批量证书、流量倍率、传输层字段互斥校验
用户 一键创建(随机用户名/密码/UUID,可选绑定全部节点)、绑定节点、流量限额、按节点分别计量(计费 = 实际 × 节点倍率)、到期、IP/订阅拉取限制、首次使用起算、周期续期/重置、分组标签、外部订阅合并、批量操作、订阅 Token
订阅 UA 自动识别 Clash/Mihomo YAML、v2ray Base64(?target=v2ray 始终 Base64)、sing-box JSON;可选 ?target=;HTML 订阅页;TUIC/HY2 分享链含 SNI/allow_insecure 等客户端参数
配置 生成 → 校验 → 应用 分离;失败回滚上一份 config.yaml
Telegram 告警与管理命令(需 Token + Chat ID)
多机 登记远程面板、健康检查、同步节点镜像、合并订阅;按名称选择本机节点推送到远程(默认禁用,可改名/端口)
安全 JWT、首登改密、TOTP 两步验证、可选 GitHub OAuth、web_path 路径前缀、凭据加密、CORS
运维 核心启停/更新、Geo、面板 SSL/ACME、备份、Cloudflare WARP 一键注册(WireGuard/MASQUE YAML)

补充说明(用户限制 / 证书 / 订阅)

前端:React + Ant Design + Lucide Icons(frontend/),构建后嵌入单一 Go 二进制。

发布产物:纯静态链接(CGO_ENABLED=0 + modernc SQLite),无需系统 libsqlite3,兼容 glibc / musl。

完整安装包 / Docker:linux/amd64 · arm64。高级独立面板二进制另外支持 armv7 · armv6 · 386 · riscv64 · loong64 · ppc64le · s390x,需要自行准备兼容内核。


快速安装

curl -fsSL https://raw.githubusercontent.com/kazeyukiro/3m-ui/main/scripts/install.sh | sudo sh

完整安装支持 Linux amd64 / arm64,自动安装面板、配套 Mihomo 和系统服务。首次配置和管理员密码由程序统一生成;配置已存在时保持原值。其他架构可使用独立二进制和自行准备的内核,见 详细安装说明。

安装完成后:

面板地址   http://SERVER_IP:8080/
默认账号   admin
初始密码   安装时随机生成并显示一次,首次登录必须修改
管理命令   3m-ui

更新:

# 推荐(已安装后)
sudo 3m-ui update
# 指定版本: sudo 3m-ui update v1.0.6
# 从旧安装脚本迁移时,先重新执行上面的最新安装命令,配置和账号会保留。

卸载:

sudo 3m-ui uninstall

AI 提示词安装

把提示词复制到 ChatGPT / Claude / Cursor 等助手,并说明你的系统(如 Ubuntu 22.04)与是否已有 root SSH。助手应只协助执行官方脚本,不要改写成不明来源命令。完整提示词及使用说明见 AI 提示词安装文档。

无需手动复制大段文本,用 curl 直接把纯文本提示词加载到剪贴板再粘贴:

curl -fsSL https://raw.githubusercontent.com/kazeyukiro/3m-ui/main/docs/ai-install-prompt.zh.txt | pbcopy        # macOS
curl -fsSL https://raw.githubusercontent.com/kazeyukiro/3m-ui/main/docs/ai-install-prompt.zh.txt | xclip -sel clip # Linux X11
curl -fsSL https://raw.githubusercontent.com/kazeyukiro/3m-ui/main/docs/ai-install-prompt.zh.txt | wl-copy       # Linux Wayland

提示词只引导使用下方官方一键脚本,所有下载均校验 SHA256SUMS:

curl -fsSL https://raw.githubusercontent.com/kazeyukiro/3m-ui/main/scripts/install.sh | sudo sh

测试 / 预发布通道(可选,非默认)见 安装文档。


Docker 安装

Linux VPS 下载发布版本中的 docker-compose.yml,在同一目录执行:

docker compose up -d
docker compose logs 3m-ui

镜像为 ghcr.io/kazeyukiro/3m-ui:latest,包含面板及固定版本的 Mihomo;latest 只跟随稳定版。配置、随机密钥和管理员会自动初始化,首次密码只在首次启动输出一次。默认 host 网络,新增节点后放行实际使用的 TCP/UDP 端口。另提供 docker-compose.bridge.yml,以及版本固定、证书配置和持久化恢复说明,见 安装文档。


从源码构建

# 前端
cd frontend && npm install && npm run build

# 嵌入并编译后端(示例 amd64)
rm -rf backend/cmd/server/web/dist
cp -r frontend/dist backend/cmd/server/web/dist
cd backend
CGO_ENABLED=0 go build -tags sqlite_modernc -trimpath -ldflags='-s -w' -o ../3m-ui ./cmd/server

需要本机已安装兼容的 Mihomo 二进制,并在配置中指向其路径。


配置要点

默认数据与配置大致位于:

路径 用途
/etc/3m-ui/config.yaml 面板配置
/var/lib/3m-ui/3m-ui.db SQLite
/var/lib/3m-ui/listener-certs/ 节点自签证书(请与数据库一并备份)
/var/lib/3m-ui/mihomo/ Mihomo 工作目录与 config.yaml

JWT / 凭据密钥请使用独立随机值(≥ 32 字节),不要使用文档示例。

自定义面板端口:3m-ui config port <port> 或编辑配置后重启服务。 新建节点默认随机填写 10000~60000 的监听端口(轻量抽样并避开已加载节点端口;支持倒序范围),可手动修改或点击「随机端口」。


架构示意

  Browser (Ant Design + Lucide)
        │  REST + JWT
        ▼
  Gin API  ── Telegram Bot
        │
   Service layer (users / listeners / sub / cluster / …)
        │
   SQLite · certstore · visual config
        │
        ▼
     Mihomo (-t 校验 → 应用 / 回滚)

备份建议

至少备份:

  1. 3m-ui.db
  2. listener-certs/
  3. 面板 config.yaml 与 Mihomo 配置目录

仅更新二进制时,证书会尽量从磁盘 hydrate;若目录丢失会重新自签,客户端需重新拉取订阅。


致谢

特别感谢以下开源项目和社区:

贡献者:Contributors


Star History

Star History Chart

许可证

Eclipse Public License 2.0

第三方组件见 THIRD-PARTY-NOTICES.md。



English & other languages

Full translations (18 languages): see the language links at the top, or open README.en.md.

S
Description
Lightweight self-hosted web panel for Mihomo: listeners, users, subscriptions, SSL/ACME, Telegram, and multi-node cluster.
https://3m-ui.top
Readme EPL-2.0
6.3 MiB
0 Stars 1 Watchers 0 Forks
Languages
Go 49.9%
TypeScript 44.7%
Shell 2.5%
CSS 0.9%
Python 0.9%
Other 0.9%