Skip to main content

通过 sql 经验学习 promql

· 5 min read

下面将以 Prometheus server 收集的 http_requests_total 时序数据为例子展开对比。

数据准备 scripts

MySQL 数据准备

mysql>
# 创建数据库
create database prometheus_practice;
use prometheus_practice;

# 创建 http_requests_total 表
CREATE TABLE http_requests_total (
code VARCHAR(256),
handler VARCHAR(256),
instance VARCHAR(256),
job VARCHAR(256),
method VARCHAR(256),
created_at DOUBLE NOT NULL,
value DOUBLE NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;

ALTER TABLE http_requests_total ADD INDEX created_at_index (created_at);

# 初始化数据
# time at 2017/5/22 14:45:27
INSERT INTO http_requests_total (code, handler, instance, job, method, created_at, value) values ("200", "query_range", "localhost:9090", "prometheus", "get", 1495435527, 3);
INSERT INTO http_requests_total (code, handler, instance, job, method, created_at, value) values ("400", "query_range", "localhost:9090", "prometheus", "get", 1495435527, 5);
INSERT INTO http_requests_total (code, handler, instance, job, method, created_at, value) values ("200", "prometheus", "localhost:9090", "prometheus", "get", 1495435527, 6418);
INSERT INTO http_requests_total (code, handler, instance, job, method, created_at, value) values ("200", "static", "localhost:9090", "prometheus", "get", 1495435527, 9);
INSERT INTO http_requests_total (code, handler, instance, job, method, created_at, value) values ("304", "static", "localhost:9090", "prometheus", "get", 1495435527, 19);
INSERT INTO http_requests_total (code, handler, instance, job, method, created_at, value) values ("200", "query", "localhost:9090", "prometheus", "get", 1495435527, 87);
INSERT INTO http_requests_total (code, handler, instance, job, method, created_at, value) values ("400", "query", "localhost:9090", "prometheus", "get", 1495435527, 26);
INSERT INTO http_requests_total (code, handler, instance, job, method, created_at, value) values ("200", "graph", "localhost:9090", "prometheus", "get", 1495435527, 7);
INSERT INTO http_requests_total (code, handler, instance, job, method, created_at, value) values ("200", "label_values", "localhost:9090", "prometheus", "get", 1495435527, 7);

# time at 2017/5/22 14:48:27
INSERT INTO http_requests_total (code, handler, instance, job, method, created_at, value) values ("200", "query_range", "localhost:9090", "prometheus", "get", 1495435707, 3);
INSERT INTO http_requests_total (code, handler, instance, job, method, created_at, value) values ("400", "query_range", "localhost:9090", "prometheus", "get", 1495435707, 5);
INSERT INTO http_requests_total (code, handler, instance, job, method, created_at, value) values ("200", "prometheus", "localhost:9090", "prometheus", "get", 1495435707, 6418);
INSERT INTO http_requests_total (code, handler, instance, job, method, created_at, value) values ("200", "static", "localhost:9090", "prometheus", "get", 1495435707, 9);
INSERT INTO http_requests_total (code, handler, instance, job, method, created_at, value) values ("304", "static", "localhost:9090", "prometheus", "get", 1495435707, 19);
INSERT INTO http_requests_total (code, handler, instance, job, method, created_at, value) values ("200", "query", "localhost:9090", "prometheus", "get", 1495435707, 87);
INSERT INTO http_requests_total (code, handler, instance, job, method, created_at, value) values ("400", "query", "localhost:9090", "prometheus", "get", 1495435707, 26);
INSERT INTO http_requests_total (code, handler, instance, job, method, created_at, value) values ("200", "graph", "localhost:9090", "prometheus", "get", 1495435707, 7);
INSERT INTO http_requests_total (code, handler, instance, job, method, created_at, value) values ("200", "label_values", "localhost:9090", "prometheus", "get", 1495435707, 7);

数据初始完成后,通过查询可以看到如下数据:

mysql>
mysql> select * from http_requests_total;
+------+--------------+----------------+------------+--------+------------+-------+
| code | handler | instance | job | method | created_at | value |
+------+--------------+----------------+------------+--------+------------+-------+
| 200 | query_range | localhost:9090 | prometheus | get | 1495435527 | 3 |
| 400 | query_range | localhost:9090 | prometheus | get | 1495435527 | 5 |
| 200 | prometheus | localhost:9090 | prometheus | get | 1495435527 | 6418 |
| 200 | static | localhost:9090 | prometheus | get | 1495435527 | 9 |
| 304 | static | localhost:9090 | prometheus | get | 1495435527 | 19 |
| 200 | query | localhost:9090 | prometheus | get | 1495435527 | 87 |
| 400 | query | localhost:9090 | prometheus | get | 1495435527 | 26 |
| 200 | graph | localhost:9090 | prometheus | get | 1495435527 | 7 |
| 200 | label_values | localhost:9090 | prometheus | get | 1495435527 | 7 |
| 200 | query_range | localhost:9090 | prometheus | get | 1495435707 | 3 |
| 400 | query_range | localhost:9090 | prometheus | get | 1495435707 | 5 |
| 200 | prometheus | localhost:9090 | prometheus | get | 1495435707 | 6418 |
| 200 | static | localhost:9090 | prometheus | get | 1495435707 | 9 |
| 304 | static | localhost:9090 | prometheus | get | 1495435707 | 19 |
| 200 | query | localhost:9090 | prometheus | get | 1495435707 | 87 |
| 400 | query | localhost:9090 | prometheus | get | 1495435707 | 26 |
| 200 | graph | localhost:9090 | prometheus | get | 1495435707 | 7 |
| 200 | label_values | localhost:9090 | prometheus | get | 1495435707 | 7 |
+------+--------------+----------------+------------+--------+------------+-------+
18 rows in set (0.00 sec)

基本查询对比

假设当前时间为 2017/5/22 14:48:30

  • 查询当前所有数据
// PromQL
http_requests_total

// MySQL
SELECT * from http_requests_total WHERE created_at BETWEEN 1495435700 AND 1495435710;

我们查询 MySQL 数据的时候,需要将当前时间向前推一定间隔,比如这里的 10s (Prometheus 数据抓取间隔),这样才能确保查询到数据,而 PromQL 自动帮我们实现了这个逻辑。

  • 条件查询
// PromQL
http_requests_total{code="200", handler="query"}

// MySQL
SELECT * from http_requests_total WHERE code="200" AND handler="query" AND created_at BETWEEN 1495435700 AND 1495435710;
  • 模糊查询: code 为 2xx 的数据
// PromQL
http_requests_total{code=~"2xx"}

// MySQL
SELECT * from http_requests_total WHERE code LIKE "%2%" AND created_at BETWEEN 1495435700 AND 1495435710;
  • 比较查询: value 大于 100 的数据
// PromQL
http_requests_total > 100

// MySQL
SELECT * from http_requests_total WHERE value > 100 AND created_at BETWEEN 1495435700 AND 1495435710;
  • 范围区间查询: 过去 5 分钟数据 (可以理解为一个滑动窗口)
// PromQL
http_requests_total[5m]

// MySQL
SELECT * from http_requests_total WHERE created_at BETWEEN 1495435410 AND 1495435710;

聚合, 统计高级查询

  • count 查询: 统计当前记录总数
// PromQL
count(http_requests_total)

// MySQL
SELECT COUNT(*) from http_requests_total WHERE created_at BETWEEN 1495435700 AND 1495435710;
  • sum 查询: 统计当前数据总值
// PromQL
sum(http_requests_total)

// MySQL
SELECT SUM(value) from http_requests_total WHERE created_at BETWEEN 1495435700 AND 1495435710;
  • avg 查询: 统计当前数据平均值
// PromQL
avg(http_requests_total)

// MySQL
SELECT AVG(value) from http_requests_total WHERE created_at BETWEEN 1495435700 AND 1495435710;
  • top 查询: 查询最靠前的 3 个值
// PromQL
topk(3, http_requests_total)

// MySQL
SELECT * from http_requests_total WHERE created_at BETWEEN 1495435700 AND 1495435710 ORDER BY value DESC LIMIT 3;
  • irate 查询,过去 5 分钟平均每秒数值
// PromQL
irate(http_requests_total[5m])

// MySQL
SELECT code, handler, instance, job, method, SUM(value)/300 AS value from http_requests_total WHERE created_at BETWEEN 1495435700 AND 1495435710 GROUP BY code, handler, instance, job, method;

AI provider 的配置收集

· One min read

都是我自己使用过的,放在这做个记录

Anthropic

# https://console.anthropic.com/settings/keys

> base_url
https://api.anthropic.com/v1/
> model
claude-opus-4-20250514
claude-sonnet-4-20250514

Open Router

# https://openrouter.ai/settings/keys

> base_url
https://openrouter.ai/api/v1
> model
deepseek/deepseek-chat-v3-0324:free
qwen/qwen3-coder:free

Xai

# https://console.x.ai/team/5affde3c-2a46-4d6f-a44c-1ca3fffd21f7/api-keys

> base_url
https://api.x.ai/v1/chat/completions
> model
grok-4-latest
grok-4-0709

Kimi

# https://platform.moonshot.cn/console/api-keys

> base_url
https://api.moonshot.cn/v1
> model
kimi-k2-0711-preview

JavaScript 数组全面操作参考指南

· 5 min read

创建数组

// 字面量语法
const arr1 = [1, 2, 3, 4, 5];

// Array 构造函数
const arr2 = new Array(1, 2, 3, 4, 5); // [1, 2, 3, 4, 5]
const arr3 = new Array(5); // [empty × 5] - 创建长度为5的空数组

// Array.of() - 创建具有可变数量参数的新数组实例
const arr4 = Array.of(5); // [5] (与 new Array(5) 不同)
const arr5 = Array.of(1, 2, 3); // [1, 2, 3]

// Array.from() - 从类数组或可迭代对象创建新数组
const arr6 = Array.from("hello"); // ["h", "e", "l", "l", "o"]
const arr7 = Array.from([1, 2, 3], x => x * 2); // [2, 4, 6]
const arr8 = Array.from({length: 5}, (_, i) => i); // [0, 1, 2, 3, 4]

// 扩展运算符
const arr9 = [...arr1, 6, 7]; // [1, 2, 3, 4, 5, 6, 7]
const arr10 = [...arr1, ...arr5]; // [1, 2, 3, 4, 5, 1, 2, 3]

// fill 方法创建并填充数组
const arr11 = new Array(5).fill(0); // [0, 0, 0, 0, 0]

添加/删除元素

const arr = [1, 2, 3, 4, 5];

// 末尾操作
arr.push(6); // [1, 2, 3, 4, 5, 6] 添加元素到末尾
arr.pop(); // [1, 2, 3, 4, 5] 删除末尾元素

// 开头操作
arr.unshift(0); // [0, 1, 2, 3, 4, 5] 添加元素到开头
arr.shift(); // [1, 2, 3, 4, 5] 删除开头元素

// 任意位置操作
arr.splice(2, 0, 2.5); // [1, 2, 2.5, 3, 4, 5] 在位置2添加元素2.5
arr.splice(2, 1); // [1, 2, 3, 4, 5] 删除位置2的元素
arr.splice(2, 1, 'a', 'b'); // [1, 2, 'a', 'b', 4, 5] 替换位置2的元素

// 删除元素但不改变原数组
const newArr = arr.filter((_, i) => i !== 2); // [1, 2, 'b', 4, 5] 删除索引2的元素

// 清空数组
arr.length = 0; // [] 清空数组

查找元素

const arr = [1, 2, 3, 4, 5, 3];

// 查找索引
arr.indexOf(3); // 2 找到第一个3的索引
arr.lastIndexOf(3); // 5 找到最后一个3的索引
arr.indexOf(3, 3); // 5 从索引3开始查找第一个3的索引

// 查找元素
arr.find(x => x > 3); // 4 找到第一个满足条件的元素
arr.findLast(x => x > 3); // 5 找到最后一个满足条件的元素
arr.findIndex(x => x > 3); // 3 找到第一个满足条件的元素的索引
arr.findLastIndex(x => x > 3); // 4 找到最后一个满足条件的元素的索引

// 检查是否包含
arr.includes(3); // true 数组是否包含3
arr.some(x => x > 4); // true 是否有元素满足条件
arr.every(x => x > 0); // true 是否所有元素都满足条件

转换与复制

const arr = [1, 2, 3, 4, 5];

// 转换为其他类型
arr.join("-"); // "1-2-3-4-5" 转换为字符串
arr.toString(); // "1,2,3,4,5" 转换为字符串
Array.from(arr.entries()); // [[0,1], [1,2], [2,3], [3,4], [4,5]] 转换为键值对数组

// 复制数组
const copy1 = [...arr]; // [1, 2, 3, 4, 5] 使用扩展运算符复制
const copy2 = arr.slice(); // [1, 2, 3, 4, 5] 使用slice复制
const copy3 = Array.from(arr); // [1, 2, 3, 4, 5] 使用Array.from复制

操作与转换

const arr = [1, 2, 3, 4, 5];

// 操作所有元素
arr.map(x => x * 2); // [2, 4, 6, 8, 10] 映射每个元素
arr.filter(x => x % 2 === 0); // [2, 4] 过滤元素
arr.reduce((sum, x) => sum + x, 0); // 15 累积元素
arr.reduceRight((sum, x) => sum + x, 0); // 15 从右到左累积元素

// 数组顺序操作
arr.reverse(); // [5, 4, 3, 2, 1] 反转数组
arr.sort(); // [1, 2, 3, 4, 5] 默认排序
arr.sort((a, b) => b - a); // [5, 4, 3, 2, 1] 自定义排序

// 填充与修改
arr.fill(0); // [0, 0, 0, 0, 0] 填充数组
arr.fill(9, 2, 4); // [0, 0, 9, 9, 0] 填充指定范围

// 平铺操作
const nested = [1, [2, [3, 4]]];
nested.flat(); // [1, 2, [3, 4]] 平铺一层
nested.flat(2); // [1, 2, 3, 4] 平铺多层
arr.flatMap(x => [x, x * 2]); // [1, 2, 2, 4, 3, 6, 4, 8, 5, 10] 映射并平铺

迭代器与遍历

const arr = [1, 2, 3, 4, 5];

// 迭代器
const entries = arr.entries(); // Iterator [[0,1], [1,2], ...] 键值对迭代器
const keys = arr.keys(); // Iterator [0, 1, 2, 3, 4] 键迭代器
const values = arr.values(); // Iterator [1, 2, 3, 4, 5] 值迭代器

// 遍历方法
arr.forEach((item, index) => console.log(item, index)); // 遍历每个元素

// for..of 循环
for (const item of arr) { console.log(item); } // 遍历值
for (const [index, value] of arr.entries()) { console.log(index, value); } // 遍历键值对

数组操作组合

const arr = [1, 2, 3, 4, 5];

// 链式操作
const result = arr
.filter(x => x % 2 === 0) // [2, 4]
.map(x => x * 3) // [6, 12]
.reduce((sum, x) => sum + x, 0); // 18

// 提取并处理子集
const subArr = arr.slice(1, 4) // [2, 3, 4]
.map(x => x * x); // [4, 9, 16]

// 查找并替换
const index = arr.findIndex(x => x === 3);
if (index !== -1) {
arr.splice(index, 1, 'three'); // [1, 2, 'three', 4, 5]
}

类型化数组

// 创建类型化数组
const int8Arr = new Int8Array([1, 2, 3]); // 8位有符号整数数组
const uint8Arr = new Uint8Array(3); // 8位无符号整数数组
const float32Arr = new Float32Array([1.1, 2.2, 3.3]); // 32位浮点数数组

// 视图
const buffer = new ArrayBuffer(16); // 16字节的缓冲区
const int32View = new Int32Array(buffer); // 在缓冲区上创建视图
int32View[0] = 42; // 设置值

高级 YAML 技巧:锚点与合并

· 2 min read

本文将介绍 YAML 配置中两个强大的功能:锚点(Anchors)和合并(Merge)。这些功能可以帮助我们消除重复配置,保持配置文件简洁易维护。我们将通过一个 Docker Compose 示例来演示这些技巧的实际应用。

为什么使用锚点和合并?

在复杂的配置文件中,经常会出现重复的片段。YAML 提供了锚点和合并功能来解决这个问题:

  • 锚点 (&):标记一个配置片段以便复用
  • 别名 (*):引用已定义的锚点
  • 合并 (<<):将多个配置片段组合在一起

这些功能如何提升效率?
它们允许我们定义通用配置模板,并在多个地方复用,大大减少配置冗余并简化更新过程。

实际应用示例

以下是 SigNoz 的 Docker Compose 配置片段,展示了这些高级 YAML 技巧:

x-common: &common
networks:
- signoz-net
restart: unless-stopped
logging:
options:
max-size: 50m
max-file: "3"

x-clickhouse-defaults: &clickhouse-defaults
!!merge <<: *common
image: clickhouse/clickhouse-server:24.1.2-alpine
# 其他 ClickHouse 特有配置...

services:
clickhouse:
!!merge <<: *clickhouse-defaults
container_name: signoz-clickhouse
volumes:
- ../common/clickhouse/config.xml:/etc/clickhouse-server/config.xml

关键组件解析

  1. 基础配置 (x-common)
    使用 &common 定义通用配置,包含网络、重启策略和日志设置

  2. 服务特定配置 (x-clickhouse-defaults)
    通过 !!merge <<: *common 继承通用配置并添加服务特有设置

  3. 最终服务配置
    在具体服务中再次合并服务模板配置并添加容器特有参数

主要优势

  1. 减少重复:通用配置只定义一次
  2. 统一管理:修改基础配置自动应用到所有服务
  3. 清晰结构:配置层次分明,易于理解
  4. 错误预防:避免因遗漏配置导致的部署问题

注意事项
使用这些技巧时要确保:

  • 锚点名称唯一且有意义
  • 合并操作符 << 正确缩进
  • 使用 !!merge 标签显式声明合并操作

总结

YAML 的锚点和合并功能能显著提高复杂配置的可维护性和可读性。通过定义通用模板和服务特定配置,我们可以创建更简洁、更健壮的 Docker Compose 文件。希望本文的技巧能帮助您优化自己的配置管理实践!

Docker vs Kubernetes 端口处理机制完全解析

· 8 min read

在容器化技术的发展过程中,Docker 和 Kubernetes 采用了截然不同的网络架构和端口处理方式。理解这两种技术的差异对于容器化应用的开发和运维至关重要。本文将深入对比 Docker 和 Kubernetes 的端口处理机制,解释为什么 Kubernetes 不需要像 Docker 那样进行手动端口映射,并详细分析各组件的网络架构。

核心差异概览

Docker 方式(需要手动端口映射)

# Docker 需要显式映射端口
docker run -p 8080:80 nginx
# ↑宿主机端口:容器端口

# 不映射的话从外部无法访问
docker run nginx # 外部无法直接访问容器的80端口

Kubernetes 方式(自动网络处理)

# Pod 定义 - 无需端口映射
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80 # 仅声明容器监听的端口

Kubernetes 免端口映射的原理

1. Pod 有独立的 IP 地址

# 每个 Pod 都有自己的 IP
kubectl get pods -o wide
# NAME IP
# nginx-pod 10.244.1.5

# 可以直接访问 Pod IP:Port
curl 10.244.1.5:80

2. 集群网络是扁平的

# 在集群内,任何 Pod 都可以直接访问其他 Pod
# 不需要端口转发
kubectl exec -it pod-a -- curl 10.244.1.5:80

3. Service 提供稳定的访问入口

apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx
ports:
- port: 80 # Service 端口
targetPort: 80 # Pod 端口
# 注意:这里不是"映射",而是"转发"

对比示例

Docker Compose(需要端口映射)

version: '3'
services:
web:
image: nginx
ports:
- "8080:80" # 必须映射才能从外部访问
app:
image: myapp
ports:
- "3000:3000"

Kubernetes(无需端口映射)

# Pod 定义
apiVersion: v1
kind: Pod
metadata:
name: web-pod
labels:
app: web
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80 # 仅声明,不映射

---
# Service 定义
apiVersion: v1
kind: Service
metadata:
name: web-service
spec:
selector:
app: web
ports:
- port: 80
targetPort: 80

---
# 另一个应用的 Pod
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
containers:
- name: myapp
image: myapp
ports:
- containerPort: 3000

访问方式对比

Docker

# 只能通过映射的端口访问
curl localhost:8080 # 访问 nginx
curl localhost:3000 # 访问 app

# 容器间通信需要通过宿主机端口或者 docker network

Kubernetes

# 多种访问方式:

# 1. 直接访问 Pod IP(集群内)
curl 10.244.1.5:80

# 2. 通过 Service(推荐)
curl web-service:80

# 3. 通过 DNS
curl web-service.default.svc.cluster.local:80

# 4. Pod 间直接通信
kubectl exec app-pod -- curl web-service:80

外部访问的区别

Docker

# 通过端口映射直接暴露到宿主机
docker run -p 80:80 nginx
curl localhost:80 # 直接访问

Kubernetes

# 方式1: NodePort
apiVersion: v1
kind: Service
metadata:
name: web-nodeport
spec:
type: NodePort
ports:
- port: 80
nodePort: 30080 # 自动分配或指定
selector:
app: web
# 通过任意节点IP访问
curl <node-ip>:30080

Pod 网络架构深度解析

Pod 本身没有端口,Pod 只有 IP

apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80 # 这是容器端口,不是Pod端口

Pod 内容器共享网络命名空间

# Pod IP: 10.244.1.5
# Pod 内有两个容器:

Container A (nginx) }
} 共享同一个 IP:10.244.1.5
Container B (redis) }

# 访问方式:
curl 10.244.1.5:80 # 访问 nginx(因为nginx监听80)
curl 10.244.1.5:6379 # 访问 redis(因为redis监听6379)

多容器 Pod 示例

apiVersion: v1
kind: Pod
metadata:
name: multi-app
spec:
containers:
- name: nginx
image: nginx
# nginx 监听 80 端口
- name: redis
image: redis
# redis 监听 6379 端口
- name: app
image: myapp
# app 监听 3000 端口

结果是:

# Pod IP: 10.244.1.5(一个IP)

# 但有多个端口被监听:
netstat -tlnp # 在Pod内执行
# 80 nginx进程
# 6379 redis进程
# 3000 app进程

# 外部访问:
curl 10.244.1.5:80 # → nginx
curl 10.244.1.5:6379 # → redis
curl 10.244.1.5:3000 # → app

Docker vs Kubernetes 网络对比

Docker 容器(隔离的网络)

docker run -d --name nginx nginx
docker run -d --name redis redis

# 每个容器有自己的网络:
docker inspect nginx | grep IPAddress
# "IPAddress": "172.17.0.2"

docker inspect redis | grep IPAddress
# "IPAddress": "172.17.0.3"

# 访问:
curl 172.17.0.2:80 # nginx
curl 172.17.0.3:6379 # redis

Kubernetes Pod(共享的网络)

# 一个Pod内的多个容器:
kubectl get pod multi-app -o wide
# IP: 10.244.1.5

# 所有容器共享这个IP:
curl 10.244.1.5:80 # nginx
curl 10.244.1.5:6379 # redis
curl 10.244.1.5:3000 # app

端口冲突问题

# 这样会有问题!
apiVersion: v1
kind: Pod
metadata:
name: conflict-pod
spec:
containers:
- name: nginx1
image: nginx # 监听 80
- name: nginx2
image: nginx # 也监听 80 - 冲突!

因为它们共享网络,不能同时监听同一个端口!

网络架构对比图

Docker 模式:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Container A │ │ Container B │ │ Container C │
│ IP: .0.2:80 │ │ IP: .0.3:80 │ │ IP: .0.4:80 │
└─────────────┘ └─────────────┘ └─────────────┘

K8s Pod 模式:
┌─────────────────────────────────────────────────┐
│ Pod (IP: 10.244.1.5) │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │Container A│ │Container B│ │Container C│ │
│ │ :80 │ │ :6379 │ │ :3000 │ │
│ └───────────┘ └───────────┘ └───────────┘ │
└─────────────────────────────────────────────────┘

Node 网络架构分析

Node 确实有 IP

# 查看 Node IP
kubectl get nodes -o wide
# NAME STATUS ROLES VERSION INTERNAL-IP EXTERNAL-IP
# node1 Ready master v1.24.0 192.168.1.10 203.0.113.10
# node2 Ready worker v1.24.0 192.168.1.11 203.0.113.11
# node3 Ready worker v1.24.0 192.168.1.12 203.0.113.12

Node 上运行的端口分类

1. Kubernetes 系统组件端口

# 在 Node 上查看端口占用
netstat -tlnp | grep LISTEN

# Master Node 典型端口:
6443 # kube-apiserver
2379 # etcd client
2380 # etcd peer
10250 # kubelet API
10251 # kube-scheduler
10252 # kube-controller-manager

# Worker Node 典型端口:
10250 # kubelet API
10256 # kube-proxy
30000-32767 # NodePort 服务范围

2. NodePort 服务端口

apiVersion: v1
kind: Service
metadata:
name: web-nodeport
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
nodePort: 30080 # 这个端口会在所有 Node 上开放
selector:
app: web
# NodePort 服务会在每个 Node 上开放端口
curl node1:30080 # 访问服务
curl node2:30080 # 访问同一个服务
curl node3:30080 # 负载均衡到后端 Pod

3. Pod 端口(通过 hostPort)

apiVersion: v1
kind: Pod
metadata:
name: nginx-hostport
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
hostPort: 8080 # 直接绑定到 Node 的 8080 端口
# 通过 Node IP:hostPort 直接访问
curl 192.168.1.10:8080

4. 宿主机服务端口

# Node 作为普通服务器运行的服务
22 # SSH
80 # 可能的 Web 服务
443 # HTTPS
3306 # 可能的数据库服务

完整的 Node 架构视图

Node (192.168.1.10):
├── 系统端口
│ ├── 22 (SSH)
│ ├── 6443 (kube-apiserver)
│ └── 10250 (kubelet)
├── NodePort 服务端口
│ ├── 30080 → Service A
│ ├── 30081 → Service B
│ └── 30082 → Service C
├── hostPort 端口
│ └── 8080 → 直接绑定的 Pod
└── Pod 网络 (通过 CNI)
├── 10.244.1.5:80 (Pod A)
├── 10.244.1.6:3000 (Pod B)
└── 10.244.1.7:6379 (Pod C)

核心概念总结

关键点总结:

  1. Pod 没有端口概念 - Pod 只是一个网络命名空间
  2. 容器有端口 - 容器内的进程监听特定端口
  3. Pod 内容器共享网络 - 同一个 IP,不同端口
  4. 不是映射关系 - 是共享关系
# 验证共享网络:
kubectl exec -it multi-app -c nginx -- netstat -tlnp
# 可以看到所有容器监听的端口都在同一个网络命名空间内

所以不是"Pod和Container端口映射",而是"Pod内所有容器共享同一个网络空间"!

特性对比总结

特性DockerKubernetes
端口映射手动 -p 映射不需要映射
网络模型桥接网络扁平网络
容器通信通过宿主机或自定义网络直接通过 Pod IP
服务发现手动管理Service + DNS
负载均衡需要外部组件Service 内置
组件IPPort特点
Container共享 Pod 网络,有 containerPort
Pod有 Pod IP,容器监听端口
Service有 Cluster IP 和 Service Port
Node有 Node IP,运行多种服务的端口

实际访问示例

# 1. 通过 Node IP + NodePort 访问服务
curl 192.168.1.10:30080

# 2. 通过 Node IP + hostPort 访问特定 Pod
curl 192.168.1.10:8080

# 3. 通过 Node IP 访问 K8s 组件
curl 192.168.1.10:6443/version # API Server

# 4. Pod 之间通信(不经过 Node 端口)
kubectl exec pod-a -- curl 10.244.1.5:80

结论

Kubernetes 的网络设计更加简洁和强大,免去了 Docker 那样的端口映射麻烦!

通过本文的对比分析,我们可以看到:

  • Docker 采用传统的端口映射模式,需要手动管理端口转发
  • Kubernetes 采用扁平网络架构,Pod 拥有独立 IP,容器共享网络命名空间
  • Node 有 IP,运行着监听各种端口的服务和组件,但本身没有像 Service 那样的固定服务端口概念

理解这些差异有助于更好地设计和部署容器化应用,选择合适的网络架构和访问方式。

解决 Docker overlay2 目录占用大量磁盘空间的问题

· 4 min read

在使用 Docker 的过程中,你可能会发现 /var/lib/docker/overlay2 目录占用了大量磁盘空间。这是一个常见问题,本文将详细解释原因并提供有效的解决方案。

什么是 overlay2

overlay2 是 Docker 的默认存储驱动程序,它使用 Linux 内核的 OverlayFS 文件系统来管理容器镜像层。每个 Docker 镜像和容器都会在 overlay2 目录中创建相应的存储层。

overlay2 占用空间过大的常见原因

1. 容器镜像层堆积

Docker 使用分层文件系统,每个镜像层都会占用 overlay2 中的存储空间:

# 查看所有镜像及其大小
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"

# 查看 Docker 系统整体使用情况
docker system df

2. 停止的容器未清理

已停止但未删除的容器仍会占用磁盘空间:

# 查看所有容器(包括停止的)
docker ps -a

# 查看容器大小
docker ps -s

3. 悬挂镜像(Dangling Images)

构建过程中产生的中间层镜像可能变成悬挂镜像:

# 查看悬挂镜像
docker images -f dangling=true

4. 容器内产生的大文件

运行中的容器可能在其文件系统中生成大量数据:

# 检查容器内磁盘使用情况
docker exec -it <container_id> df -h

诊断磁盘占用情况

首先,使用以下命令分析磁盘使用情况:

# 查看 Docker 目录各子目录的大小
du -h --max-depth=2 /var/lib/docker | sort -hr | head -10

# 详细查看 Docker 系统资源使用
docker system df -v

清理 overlay2 空间的解决方案

安全的分步清理方法

步骤 1:清理停止的容器

# 清理所有停止的容器
docker container prune

步骤 2:清理悬挂镜像

# 清理悬挂镜像
docker image prune

步骤 3:清理未使用的镜像

# 清理所有未使用的镜像
docker image prune -a

步骤 4:清理网络和构建缓存

# 清理未使用的网络
docker network prune

# 清理构建缓存
docker builder prune

一键清理命令

⚠️ 警告:以下命令会删除所有未使用的 Docker 资源,请谨慎使用!

# 清理所有未使用的容器、网络、镜像、构建缓存
docker system prune -a

# 更彻底的清理(包括数据卷)
docker system prune -a --volumes

预防措施

1. 配置日志轮转

/etc/docker/daemon.json 中配置日志大小限制:

{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}

2. 定期清理任务

创建定期清理脚本:

#!/bin/bash
# 每周执行的 Docker 清理脚本
docker container prune -f
docker image prune -f
docker network prune -f
docker builder prune -f

3. 监控磁盘使用情况

定期检查 Docker 磁盘使用情况:

# 添加到 crontab 中定期执行
docker system df

TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 90 6 44.87GB 43.32GB (96%)
Containers 6 6 37.96MB 0B (0%)
Local Volumes 27 6 6.615GB 3.329GB (50%)
Build Cache 188 0 6.139GB 6.139GB

最佳实践建议

  1. 及时清理:定期删除不需要的容器和镜像
  2. 使用多阶段构建:减少最终镜像大小
  3. 优化 Dockerfile:合并 RUN 指令,减少镜像层数
  4. 监控空间:设置磁盘空间监控告警
  5. 选择合适的基础镜像:使用轻量级基础镜像如 Alpine Linux

总结

overlay2 目录占用大量空间是 Docker 使用过程中的常见问题,主要原因包括镜像层堆积、未清理的容器和悬挂镜像等。通过定期清理和合理配置,可以有效控制磁盘空间占用。建议建立定期维护机制,避免空间问题影响系统正常运行。

Prometheus 直接抓取 vs OpenTelemetry 导出:监控架构的选择

· 5 min read

Pair with ClaudeCode, Aider, ClaudeCodeRouter, KimiK2

在实际监控系统设计中,选择合适的 metrics 采集方式是一个非常重要的决策。目前主要有两种方式:metrics 直接被 Prometheus 抓取通过 OpenTelemetry (OTel) 导出 metrics 再被 Prometheus 抓取。本文将从架构、灵活性、兼容性、标准化和使用场景等方面进行详细对比分析。

架构区别

Prometheus 直接抓取 metrics

这是最传统、最直接的方式:

  • 应用暴露 /metrics HTTP 接口(通常使用 Prometheus SDK,例如 Go 的 prometheus/client_golang
  • Prometheus 主动抓取该接口(Pull 模式)

示意图:

[应用程序] --/metrics--> [Prometheus]

OTel 导出 metrics 再被 Prometheus 抓取

现代可观测性架构中更灵活的做法:

  • 应用使用 OpenTelemetry SDK 生成指标
  • 通过 OTel Collector 或 OTel SDK exporter 暴露 /metrics
  • Prometheus 抓取 OTel Collector 暴露的 metrics 接口

示意图:

[应用程序] --OTel SDK--> [OTel Collector] --/metrics--> [Prometheus]

灵活性和扩展性对比

方式灵活性说明
直接暴露 /metrics⭐ 普通不支持多种协议、多种后端
通过 OTel Collector🌟 很高可以同时导出到多个系统(Prometheus、OTLP、New Relic、Datadog、logging 等)

OTel 支持"标准采集 + 分发",可统一处理 metrics、logs、traces,不仅限于 Prometheus。

标准化程度

  • 直接 metrics:Prometheus 的格式是特有的(text-based exposition format)
  • OTel:采用 OpenTelemetry 的标准(如 OTLP 协议),更偏向于"供应商无关"的可观测性架构

性能和开销

方式性能备注
直接抓取较高性能少一层转发和抽象
OTel 采集稍有开销多一层 SDK + Collector,但可控制和缓冲采样数据

使用场景对比

使用场景推荐方式原因
小型系统、单一 Prometheus 环境直接暴露 /metrics简单、易用
多云、多后端、需要导出到多个系统OTel + Prometheus exporter灵活、统一标准
想统一指标、日志、链路追踪OTel 全家桶架构清晰、可扩展性强

实际举例

demo repo: https://github.com/AmonsLintao/otel-promethus-metrics

直接暴露 metrics(如 Go 应用)

import "github.com/prometheus/client_golang/prometheus"

应用运行后直接在 /metrics 提供 Prometheus 可抓的格式。

OTel 方式

import go.opentelemetry.io/otel
import go.opentelemetry.io/otel/metric

然后由 OTel SDK/Collector 负责暴露和转发,甚至可以转为 OTLP,再由 Collector 转为 Prometheus 格式。

Pull vs Push 模型的区别

需要注意的是,两种方式在数据传输模型上有本质区别:

Prometheus 模型(Pull-based)

  • 应用通过 SDK 暴露一个 HTTP endpoint,通常是 /metrics
  • Prometheus 按配置的 scrape_interval 定期去拉(pull)这个 endpoint 的内容

OTel 模型(Push-based)

  • 应用使用 OTel SDK 采集 metrics(比如计数器、仪表等)
  • 默认通过 OTLP 协议**主动推送(push)**到一个 OTel Collector 或其他接收端

OTLP 协议详解

当应用使用 OpenTelemetry 推送 metrics 到 OTel Collector 时,它使用的是 OTLP(OpenTelemetry Protocol)协议,通过 gRPC 或 HTTP/Protobuf 传输。

Metric 的 payload 格式

check the frontend metrics send to otel collector

OTLP 是基于 Protocol Buffers(proto3)定义的。metrics 的 payload 是一个 ExportMetricsServiceRequest 消息体。

一个 Gauge Metric 的 JSON 表示形式:

{
"resourceMetrics": [
{
"resource": {
"attributes": [
{ "key": "service.name", "value": { "stringValue": "my-app" } }
]
},
"scopeMetrics": [
{
"metrics": [
{
"name": "http_requests_total",
"description": "Total HTTP requests",
"unit": "1",
"gauge": {
"dataPoints": [
{
"attributes": [
{ "key": "method", "value": { "stringValue": "GET" } },
{ "key": "status", "value": { "stringValue": "200" } }
],
"timeUnixNano": "1718600000000000000",
"asDouble": 158.0
}
]
}
}
]
}
]
}
]
}

OTel vs Prometheus 数据模型的差异

OTel 的 metrics 数据模型比 Prometheus 更丰富,当通过 prometheus exporter 将其导出为 /metrics 供 Prometheus 拉取时,会发生信息丢失或降级

为什么 OTel 的 metrics 更丰富?

特性OTel 支持Prometheus 支持说明
时间戳❌(非原生)OTel 的每个 data point 带时间戳,Prometheus 不支持显式指定
多值聚合指标(Sum, Histogram, ExponentialHistogram)✅(部分)Prometheus 不支持 OTel 的 ExponentialHistogram
Metric scope(哪个 SDK 或库生成)OTel 保留 instrumentation scope 信息
资源属性(service.name, host.name 等)Prometheus 不支持结构化资源属性,必须编码到 label
统一关联 tracing/loggingPrometheus 只处理 metrics,OTel 可以跨 domain

导出到 Prometheus 时的信息丢失

OTel 信息Prometheus 映射方式或结果
时间戳丢弃,Prometheus 使用 scrape 时间
Scope / Library Name丢弃
Resource 属性可选择作为 label 注入,部分保留
ExponentialHistogram无法导出,需转为普通 histogram 或忽略
Exemplars(trace id)通常不导出,或需特殊配置

自部署场景的考虑

在自部署 Prometheus + Grafana 的场景中,metrics 通常必须先导出到 Prometheus,Grafana 再通过 Prometheus 数据源读取可视化。这是目前最常见、也是最兼容的方式。

标准流程:

[应用] → (OTLP Push) → [OTel Collector]
↓ Prometheus Exporter
[Prometheus ← /metrics Scrape]

[Grafana ← Prometheus 数据源]

优化建议

如果希望保留更多 OTel 的语义信息,可以考虑:

  1. 注入关键 resource 属性到 label(通过 OTel Processor 实现)
  2. 使用 Grafana Agent 替代 Prometheus(兼容 Prometheus + OTel)
  3. 追加 OTLP exporter 到其他后端作为扩展

总结

对比项直接被 Prometheus 抓取经 OTel 导出后抓取
架构简单直接更灵活、可扩展
标准Prometheus 专有格式OTel 标准,支持多后端
性能更高一些多一层开销
可组合性一对一一对多(OTLP、logs、traces)
推荐场景小项目/只用 Prometheus多系统/现代观测平台

选择建议:

  • 如果你只用 Prometheus 做指标采集,直接暴露 metrics 是最简单的方式
  • 如果你希望统一处理 metrics、logs、traces,并可能迁移到其他后端系统(如 Grafana Cloud、New Relic、DataDog),那使用 OTel 更具优势
  • 在自部署情况下,Prometheus 是必须环节,但仍然可以通过配置 OTel processor 和精心设计来最大限度保留信息

TailwindCSS 文本换行与断行实用类详解:break-normal、break-words、break-all 的差异

· 7 min read

在前端开发中,文本内容的显示方式对用户体验至关重要。特别是在处理长单词、URL 或连续的字符序列时,如何优雅地控制文本的换行和断行成为了一个常见但又容易混淆的问题。TailwindCSS 提供了三个主要的断行实用类:break-normalbreak-wordsbreak-all,每种都有其特定的应用场景和行为特征。

为什么需要关注文本断行?

什么是文本断行问题? 当文本内容超出其容器的宽度限制时,浏览器需要决定如何显示这些内容。默认情况下,浏览器只会在单词边界(空格、连字符等)处进行换行,这在大多数情况下都能正常工作。

但实际开发中经常遇到:

  • 包含长 URL 的文本段落
  • 技术文档中的代码片段
  • 文件名或路径信息
  • 多语言内容中的长单词
  • 移动设备上的响应式布局

这些情况如果处理不当,会导致布局破坏、水平滚动条出现,或者文本被截断,严重影响用户体验。

TailwindCSS 断行实用类详解

1. break-normal:默认行为

break-normal 是浏览器的默认断行行为,也是 TailwindCSS 的默认值。

工作原理:

  • 使用浏览器默认的断行算法
  • 单词之间通过空格、连字符、标点符号等边界进行换行
  • 不会破坏完整的单词(除非单词本身包含连字符)

适用场景:

  • 正常的中文文本内容
  • 英文文章、博客内容
  • 任何不希望单词被强行拆分的场景
<div class="max-w-xs border bg-gray-100 p-4 break-normal">
This is a normal text that will break at appropriate places like spaces and hyphens-which-are-used-as-breaking-points.
</div>

行为特点:

  • 优先保持单词的完整性
  • 在中文、日文等 CJK 语言中,可以在任意字符间换行
  • 对于英文等拉丁语系,只在明确的分隔符处换行

2. break-words:智能断行

break-words 是最常用的断行类,它在保持内容可读性的同时,允许长单词在必要时断开。

工作原理:

  • 首先尝试在单词边界处换行
  • 当单词长度超过容器宽度时,允许在单词内部断行
  • 会避免在小单词或短字符序列中断行
  • 通常配合overflow-wrap: break-word实现

适用场景:

  • 包含长URL的内容
  • 文件路径或代码片段
  • 表格中的内容
  • 响应式布局中的文本
<div class="max-w-xs border bg-blue-100 p-4 break-words">
Visit our website: https://very-long-website-url-example.com/path/to/some/page
</div>

break-normal 的关键差异: 当容器宽度不足以容纳整个单词时:

  • break-normal:不会断行,可能造成溢出或水平滚动
  • break-words:允许在单词内部断行,确保内容完全可见

3. break-all:强制断行

break-all 是最激进的断行方式,它会在任何字符边界处强制断行。

工作原理:

  • 在任意字符间进行强制断行
  • 完全无视单词完整性
  • 配合word-break: break-all实现

适用场景:

  • 表格单元格中的内容
  • 固定宽度的展示区域
  • 需要严格适应容器宽度的场景
  • 日文、中文等字符语言内容(每字符可独立)
<div class="max-w-xs border bg-red-100 p-4 break-all">
ThisVeryLongWordWillBeBrokenAtAnyCharacterWithoutConsideringWordBoundaries
</div>

警告: 使用 break-all 可能破坏英文单词的可读性,特别是对于拉丁语系文本,应谨慎使用。

实际应用案例分析

场景 1:博客文章内容

对于正常的博客文章内容,推荐break-words

<article class="prose prose-lg max-w-none">
<p class="break-words">
在今天的技术分享中,我们要讨论一个很有趣的问题:当URL地址https://example.com/very-long/path/to/some/deep/resource变得过长时,如何优雅地处理文本显示?
</p>
</article>

场景 2:表格数据展示

在表格中使用 break-all 确保单元格宽度固定:

<table class="min-w-full divide-y divide-gray-200">
<tbody class="bg-white divide-y divide-gray-200">
<tr>
<td class="break-all px-6 py-4 text-sm text-gray-900">
/usr/local/bin/very-long-path-to-an-executable-file.sh
</td>
</tr>
</tbody>
</table>

场景 3:代码展示

代码块一般使用 break-words 保持可读性:

<pre class="bg-gray-900 text-white p-4 rounded-lg overflow-x-auto break-words">
const configuration = {
apiUrl: "https://api.service.com/v1/very-long-endpoint-name-with-lots-of-parameters"
};
</pre>

中文内容特殊考虑

对于中文、日文、韩文等 CJK 语言内容,处理方式有所不同:

CJK 语言特点:

  • 每个字符相对独立,可以单独换行
  • 没有单词边界的概念(中文没有空格分隔)
  • 断行规则与英文截然不同

推荐设置:

  • 中文正文:break-normalbreak-words
  • 中文表格:可能使用 break-all 更合适
  • 中英混合:break-words 通常能正确处理
<div class="max-w-md border p-4 break-words">
这是一个包含英文technical-terms和中文的技术文档,网址https://example.com/really-long-url-path演示了混合内容的处理。
</div>

响应式设计中的断行策略

在不同屏幕尺寸下,可能需要不同的断行行为:

<!-- 移动端使用更激进的断行 -->
<div class="break-words md:break-normal">
This content adapts its breakage behavior based on viewport size.
</div>

<!-- 或者针对不同容器宽度 -->
<div class="break-normal sm:break-words lg:break-normal">
Responsive text wrapping based on container width.
</div>

最佳实践建议

选择合适断行类的决策树:

  1. 标准文本内容? → 使用 break-normal
  2. 包含长单词或URL? → 使用 break-words
  3. 表格或固定宽度容器? → 考虑 break-all
  4. 中英混合内容?break-words 通常为最佳选择

性能考虑:

  • 这些类的CSS开销极小
  • 可能影响浏览器的文本布局算法
  • 在大量文本时,渲染性能差异可以忽略

测试建议:

  • 在不同屏幕尺寸测试文本显示
  • 检查极端情况下的边界值
  • 验证不同语言内容的显示效果
  • 使用真实数据测试长URL和文件路径

常见问题解答

Q: break-wordsoverflow-wrap: break-word 有什么区别? A: 在TailwindCSS中,break-words实际上就是overflow-wrap: break-word的封装,二者是等价的。

Q: 为什么设置了break-words还是出现了水平滚动条? A: 检查父容器是否有overflow-x: hiddenoverflow-x: scroll的样式,这可能覆盖了断行行为。

Q: break-all会不会影响SEO? A: 不会直接影响SEO,但过度使用可能影响用户体验,间接影响跳出率等指标。

结论

选择合适的文本断行策略需要考虑内容类型、目标用户、设备类型等多个因素。break-normal适合标准文本内容,break-words是解决overflow问题的万能钥匙,而break-all则在特定场景下提供精确控制。理解这些差异将帮助你创建更加专业和用户友好的Web界面。

通过合理使用TailwindCSS的断行实用类,我们可以确保文本内容在各种设备和屏幕尺寸下都能优雅地显示,提供一致的用户体验。

超高效自动化:用SSH密钥+通配符配置+mDNS快速连接Hyper-V虚拟机

· 3 min read

每次新建虚拟机都要重复配置SSH?手动输入IP地址太麻烦?本文将介绍一套全自动化方案:

  1. 初始化脚本自动配置SSH密钥
  2. 通配符SSH配置一键连接所有VM
  3. mDNS解决动态IP问题
    让你彻底告别手动操作!

一、初始化脚本:自动注入SSH密钥

在虚拟机初始化时,直接通过脚本完成SSH设置:

#!/bin/sh
# init_vm.sh

# 1. 安装必备工具
sudo apt update -y
sudo apt install -y openssh-server avahi-daemon

# 2. 启用SSH并允许密钥登录
sudo systemctl enable --now ssh
sudo sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config

# 3. 注入你的公钥(替换为你的实际公钥)
PUB_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA..."
mkdir -p ~/.ssh
echo "$PUB_KEY" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

# 4. 启用mDNS(用于自动发现IP)
sudo systemctl enable --now avahi-daemon

关键点:

  • 通过avahi-daemon实现零配置网络发现
  • 密钥注入后立即禁用密码登录(可选)

二、客户端配置:通配符SSH魔法

在本地电脑的~/.ssh/config中添加:

# 通用设置
Host vm-*
User ubuntu
IdentityFile ~/.ssh/id_rsa_vm # 你的私钥
StrictHostKeyChecking no # 自动接受新主机密钥

# 动态解析规则(配合mDNS)
Host vm-*.local
HostName %h.local

现在只需执行:

ssh vm-01.local  # 直接连接,无需输入IP!

原理说明:

  • vm-*匹配所有以vm-开头的别名
  • %h自动替换为输入的完整主机名
  • .local后缀由mDNS自动解析

三、mDNS:动态IP的终极解决方案

当虚拟机IP变化时,传统方法需要手动更新配置。使用mDNS可实现:

  1. 自动主机名发现
    虚拟机启动后自动广播hostname.local地址

  2. 跨平台支持

    • Linux: Avahi(默认安装)
    • Windows: Bonjour打印服务
    • macOS: 原生支持
  3. 验证mDNS是否工作

    ping vm-01.local  # 应返回虚拟机当前IP

四、完整工作流示例

场景:快速创建10台测试VM

  1. 批量初始化

    for i in {1..10}; do 
    hyper-v create vm-$i --image ubuntu-lts
    scp init_vm.sh vm-$i:/tmp/
    ssh vm-$i "sh /tmp/init_vm.sh"
    done
  2. 一键连接

    # 直接按编号连接
    ssh vm-07.local
  3. 批量执行命令

    parallel-ssh -H "vm-{1..10}.local" "docker pull nginx"

五、安全增强建议

  1. 最终锁定SSH配置

    sudo sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
  2. 防火墙规则

    sudo ufw allow from 192.168.1.0/24 to any port 22
  3. 密钥管理
    建议为每个项目使用独立密钥:

    ssh-keygen -t ed25519 -f ~/.ssh/id_team_proj

结语

这套组合拳实现了:
零手动SSH配置
无视IP变化
统一访问入口
秒级横向扩展

Linux与Windows常用命令对照表

· 3 min read

在不同操作系统间工作时,掌握常用命令的对应关系能显著提高工作效率。本文整理了Linux和Windows系统中最常用的命令对照表,帮助开发者快速在两个系统间切换。

文件和目录操作

功能LinuxWindows (CMD)Windows (PowerShell)
列出文件和目录lsdirGet-ChildItem / ls
显示当前路径pwdcdGet-Location / pwd
改变目录cdcdSet-Location / cd
创建目录mkdirmkdir / mdNew-Item -ItemType Directory / mkdir
删除空目录rmdirrmdir / rdRemove-Item / rmdir
删除文件rmdel / eraseRemove-Item / rm
递归删除目录rm -rrmdir /sRemove-Item -Recurse
复制文件cpcopyCopy-Item / cp
移动/重命名文件mvmove / renMove-Item / mv
查看文件内容cattypeGet-Content / cat
创建空文件touchtype nul >New-Item -ItemType File

文本处理和搜索

功能LinuxWindows (CMD)Windows (PowerShell)
在文件中搜索文本grepfindstrSelect-String
显示文件前几行headmoreGet-Content -Head
显示文件后几行tailN/AGet-Content -Tail
文本排序sortsortSort-Object
统计行数/字数wcfind /cMeasure-Object
查找文件findwhere / dir /sGet-ChildItem -Recurse

系统信息和进程管理

功能LinuxWindows (CMD)Windows (PowerShell)
显示进程列表pstasklistGet-Process / ps
终止进程killtaskkillStop-Process / kill
显示系统信息unamesysteminfoGet-ComputerInfo
显示磁盘使用情况dfdirGet-PSDrive
显示内存使用情况freesysteminfoGet-WmiObject Win32_Memory
显示运行时间uptimesysteminfoGet-Uptime

网络操作

功能LinuxWindows (CMD)Windows (PowerShell)
测试网络连通性pingpingTest-Connection / ping
显示网络配置ifconfigipconfigGet-NetIPConfiguration
显示网络连接netstatnetstatGet-NetTCPConnection
下载文件wget / curlcurlInvoke-WebRequest / curl

压缩和解压

功能LinuxWindows (CMD)Windows (PowerShell)
创建tar压缩包tar -czfN/ACompress-Archive
解压tar包tar -xzfN/AExpand-Archive
创建zip压缩包zippowershell Compress-ArchiveCompress-Archive
解压zip包unzippowershell Expand-ArchiveExpand-Archive

环境变量和系统设置

功能LinuxWindows (CMD)Windows (PowerShell)
显示环境变量envsetGet-ChildItem Env:
设置环境变量export VAR=valueset VAR=value$env:VAR = "value"
显示路径echo $PATHecho %PATH%$env:PATH
清屏clearclsClear-Host / cls

权限和用户管理

功能LinuxWindows (CMD)Windows (PowerShell)
显示当前用户whoamiwhoami$env:USERNAME
改变文件权限chmodicaclsSet-Acl
改变文件所有者chowntakeownSet-Acl
切换用户surunasStart-Process -Credential

使用建议

对于Linux用户转Windows:

  • 推荐使用PowerShell而非CMD,因为PowerShell提供了更多类似Linux的命令别名
  • 可以安装Windows Subsystem for Linux (WSL) 来获得原生Linux命令体验
  • 考虑使用Git Bash,它提供了许多Linux命令的Windows实现

对于Windows用户转Linux:

  • 大多数基本操作概念相似,主要是命令名称和参数格式的差异
  • 建议熟悉man命令来查看帮助文档
  • 注意Linux的文件路径使用正斜杠(/)而非反斜杠()

结论

虽然Linux和Windows在命令行界面上有所不同,但核心功能基本相同。掌握这些常用命令的对应关系,可以帮助开发者在不同系统间无缝切换,提高工作效率。随着PowerShell的发展和跨平台特性,两个系统的命令行体验正在逐渐趋同。