知识广场
按学科筛选:计算机科学 / 分布式与云计算
«计算机科学 / 分布式与云计算» 分类下共 24 篇帖子
## 起因 K8s 全套 (kubeadm) 在小机器(2-4 vCPU / 4 GB)跑得费力: - etcd + apiserver + controller-manager + scheduler 几 GB RAM - 启动 5 分钟 - 升级痛苦 但有时只想: - 边缘设备(树莓派 / IoT gateway)跑 K8s 编排 - 笔记本本地 dev - 单租户产品打包"K8s on appliance" - 小公司 1-2 server prod **k3s**(Rancher,2019+):单 binary(~50 MB),裁剪 K8s 到 ~512 MB RAM。 **microk8s**(Canonical):类似目标,更 batteries-included。 ## k3s 装 ```bash # 一键装(server) curl -sfL https://get.k3s.io | sh - # kubectl 立刻可用 sudo k3s kubectl get nodes # 或者复制 kubeconfig 用普通 kubectl sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config sudo chown $USER ~/.kube/config ``` 完事。1 分钟起一个 K8s。 ## 加 worker node ```bash # server 拿 token sudo cat /var/lib/rancher/k3s/server/node-token # worker 上 curl -sfL https://get.k3s.io | K3S_URL=https://server:6443 K3S_TOKEN=xxx sh - ``` ## k3s 是什么 K8s - **完整 K8s API**(不是 mini K8s) - 替代 etcd 用 SQLite(也支持 etcd / MySQL / PG external) - 替代 docker 用 containerd - 内置 traefik ingress(可关) - 内置 helm controller / local storage provisioner / metrics-server - 删了 in-tree cloud provider / 实验性 feature 90% workload 跟 vanilla K8s 一样跑。 ## 资源占用 | | k3s | kubeadm K8s | docker swarm | |---|---|---|---| | RAM | 512 MB | 2 GB+ | 50 MB | | 启动 | 30s | 5 min | 5s | | Binary | 50 MB | 几百 MB | 50 MB | 树莓派 4 (4 GB RAM) 跑 k3s 余 3+ GB 给应用 → 完全实用。 ## HA k3s ```bash # server 1 curl -sfL https://get.k3s.io | sh -s - server --cluster-init # server 2 / 3 curl -sfL https://get.k3s.io | sh -s - server --server https://server1:6443 --token xxx ``` 3 个 server 自动 embedded etcd HA。 worker 同前面加。 ## microk8s Ubuntu 系强推: ```bash sudo snap install microk8s --classic sudo usermod -aG microk8s $USER microk8s status microk8s kubectl get nodes # 启用 addon microk8s enable dns ingress storage cert-manager ``` addon 系统比 k3s 友好: ``` dns / ingress / cert-manager / metallb / observability / istio / linkerd / hostpath-storage / openebs / cilium / dashboard ``` 一行启用,省手动 helm。 ## k3s vs microk8s | | k3s | microk8s | |---|---|---| | 母公司 | Rancher / SUSE | Canonical | | 分发 | shell script | snap | | 多 distro | 是 | snap 限制(Ubuntu / 部分) | | HA | embedded etcd | 自动 (3+ nodes) | | addon | 内置少,手动 helm | addon 丰富 | | 升级 | 手动 | snap 自动 | | 默认 ingress | traefik | nginx | | 体量 | 50 MB | 200 MB+ | 我倾向 k3s(更轻 + 跨 distro),但 ubuntu 系统 microk8s 顺手。 ## 适合场景 - **edge / IoT**:低资源 + 远程管理 - **CI / 测试**:临时拉一个 k3s 测 manifest - **个人 / 小 团队 prod**:1-3 server 足够 - **embedded product**:appliance 内置 K8s 不适合: - 千节点集群(k3s SQLite 不行;要 PostgreSQL 后端) - 重 K8s feature(webhooks 多 / CRD 多) 仍 OK 但优势减 - 合规要求企业级 K8s 发行版 ## k3d (k3s in docker) ```bash brew install k3d k3d cluster create mycluster --servers 3 --agents 2 kubectl get nodes ``` 3 master + 2 worker 在 5 个 docker container 起来 → laptop 测多 节点行为。 比 kind 轻量。 ## 真实部署 case 某 IoT 客户: - 100 个边缘 gateway(4 vCPU / 8 GB) - 每 gateway 跑 k3s + 几个微服务 + 本地 DB - 中心 cluster 用 Rancher 管 100 个 k3s 集群 - 应用更新通过 Fleet / ArgoCD 推 效果: - 单 gateway 装 K8s 5 分钟(自动化 script) - 中心可视化所有 edge 状态 - 应用迭代独立各 gateway vs 老方案(docker compose + ansible):管理统一 + K8s API 标准化 + 故障恢复更智能。 ## 本地 dev: k3s vs kind vs minikube ```bash # kind (K8s in docker) kind create cluster # k3d (k3s in docker) k3d cluster create # minikube (VM) minikube start ``` | | 启动 | 资源 | 多节点 | |---|---|---|---| | kind | 30s | 中 | ✅ | | k3d | 15s | 低 | ✅ | | minikube | 1 min | 高(VM) | 慢 | 我本地 dev 用 k3d。 ## 升级 k3s ```bash # 同样 curl 命令 + INSTALL_K3S_VERSION curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.30.0+k3s1 sh - ``` 或者用 Rancher / system-upgrade-controller 自动滚动升级 多 node。 ## monitoring ```bash helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \ -n monitoring --create-namespace ``` 跟 vanilla K8s 一样 helm install。 metrics-server 已内置 (k3s)。 ## 与 Talos Linux 对比 Talos Linux:Linux distro for K8s。OS 本身是 K8s。 极简 + 安全 + immutable。 | | k3s | Talos | |---|---|---| | Host OS | 任意 | Talos only | | 管理 | systemd | API only(无 ssh) | | 升级 | 手动 / 工具 | 强 immutable | | 学习 | 易 | 中 | Talos 是新潮选择,适合"K8s as appliance"。 ## 踩过的坑 1. **SQLite 单 server 限制**:k3s 默认 SQLite 不支持多 server HA。 3+ server 要 embedded etcd(默认 `--cluster-init`)。 2. **storage 默认 hostpath**:pod restart 跨 node 数据丢。生产用 longhorn / NFS / cloud volume。 3. **traefik 内置版本旧**:k3s 自带 traefik 跟最新版本可能差几个版本。 `--disable traefik` + 自装最新。 4. **resource 限制**:CPU 紧张时 etcd / apiserver 慢 → pod schedule 失败。监控 system pod CPU。 5. **firewall blocking 6443**:worker 加入失败常见原因。`ufw allow 6443` / 防火墙规则。
## 起因 K8s 默认 CNI(flannel / calico iptables 模式)问题: - 服务多了 iptables 规则数万条 → packet 经过 N rules 慢 - network policy 通过 iptables 模拟 → 性能差 + 难调试 - 没原生 L7 (HTTP) policy - 跨节点流量 encap (VXLAN) 开销 **Cilium** 用 eBPF 在内核态做: - pod 间通信(直接路由 / VXLAN / WireGuard) - network policy(L3/4/7) - service LB 替代 kube-proxy - observability(hubble) - mTLS eBPF 不走 iptables 链 → 性能高 + 灵活。 ## 装 (kind 本地) ```bash # kind cluster 不带默认 CNI kind create cluster --config kind-config.yaml cat > kind-config.yaml <<EOF kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 networking: disableDefaultCNI: true kubeProxyMode: none # cilium 替代 EOF # 装 cilium cilium install --version 1.16.0 cilium status ``` ## 网络 policy(L3/L4) ```yaml apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: name: api-policy namespace: app spec: endpointSelector: matchLabels: app: api ingress: - fromEndpoints: - matchLabels: app: frontend toPorts: - ports: - port: "8080" protocol: TCP ``` `api` pod 只接受 `frontend` pod 的 8080 流量。 其它源(包括同 namespace 别的 pod)阻断。 ## L7 policy (HTTP) ```yaml spec: endpointSelector: matchLabels: app: api ingress: - fromEndpoints: - matchLabels: app: frontend toPorts: - ports: - port: "8080" rules: http: - method: "GET" path: "/api/users/.*" - method: "POST" path: "/api/login" ``` frontend 只能 GET `/api/users/*` 和 POST `/api/login`,其它 404。 传统 iptables 做不到。 ## hubble (observability) ```bash cilium hubble enable # CLI hubble observe --namespace=app TIMESTAMP SOURCE DESTINATION TYPE VERDICT 12:34:56 frontend-xxx:34521 api-yyy:8080 L7 ALLOWED (GET /api/users/42) 12:34:57 frontend-xxx:34522 api-yyy:8080 L7 DROPPED (POST /admin/delete) ``` 每 packet 看 source / dest / verdict。 debug network 神器。 hubble UI 浏览器看: ```bash cilium hubble ui ``` 实时 service map + flow log。 ## kube-proxy 替代 ```bash # install 时 --set kubeProxyReplacement=true helm install cilium ... --set kubeProxyReplacement=true ``` Cilium 用 eBPF 实现 service routing → 删 kube-proxy → 删几万条 iptables。 性能: | | iptables | Cilium eBPF | |---|---|---| | service connect 延迟 | 50 μs | 5 μs | | pod-to-pod throughput | 7 Gbps | 9.5 Gbps | | iptables rule 数 | 几万 | 0 | 大 cluster 显著。 ## 跨节点流量 模式选: - **VXLAN**:兼容性最好(默认) - **Geneve**:VXLAN 替代 - **直接路由**:节点同 L2 网(性能最好) - **WireGuard**:跨 region 加密 - **IPsec**:类似 我们 prod 用直接路由(节点同 VPC)+ WireGuard 用于跨 region。 ## bandwidth manager ```yaml apiVersion: v1 kind: Pod metadata: annotations: kubernetes.io/egress-bandwidth: "10M" ``` cilium 用 eBPF 限 pod 出口带宽 → 防 noisy neighbor。 ## clustermesh (多 cluster) ```bash cilium clustermesh enable --context cluster1 cilium clustermesh connect --context cluster1 --destination-context cluster2 ``` 两个 cluster 互通 service: ```yaml # Service on cluster2 metadata: annotations: service.cilium.io/global: "true" ``` cluster1 的 pod 访问该 service → cilium 跨 cluster 路由。 无需 service mesh / API gateway。 ## mTLS (cilium 1.14+) ```yaml spec: endpointSelector: matchLabels: { app: api } ingress: - fromEndpoints: - matchLabels: { app: frontend } authentication: mode: required # 强制 mTLS ``` cilium 用 SPIFFE 标识自动 mTLS。 比 Istio 简单(不需要 sidecar)。 ## 性能 CNI throughput benchmark(10 Gbps 网络): | | Pod-to-Pod | |---|---| | flannel VXLAN | 6 Gbps | | calico iptables | 7 Gbps | | calico eBPF | 9 Gbps | | Cilium (native routing) | 9.5 Gbps | cilium 几乎跑满。 ## 与 calico eBPF 对比 | | Cilium | Calico (eBPF mode) | |---|---|---| | L7 policy | ✅ | 弱 | | Hubble observability | ✅ 强 | 基本 | | clustermesh | ✅ | 弱 | | mTLS | ✅ | 计划中 | | 复杂度 | 高 | 中 | | 生态 | 大(CNCF) | 大 | 两者都好。cilium 是更现代 / 功能多。calico 更老更广。 ## 与 Istio 对比 | | Cilium | Istio | |---|---|---| | 层次 | CNI + L7 policy | service mesh (L7+) | | sidecar | 不需要 | envoy sidecar | | 资源开销 | 低 (eBPF kernel) | 高 (sidecar 每 pod) | | mTLS | ✅ | ✅ | | traffic policy | 中 | 强 (canary / mirror 等) | | 复杂度 | 中 | 高 | Cilium Service Mesh(cilium 1.12+)能部分替代 Istio。 重 traffic management 还是 Istio。 ## 实战 case:从 calico 迁 cilium 我们 prod cluster 用 calico 几年,问题: - network policy 调试痛苦 - 没 L7 policy - 缺 observability 迁 cilium: 1. 装 cilium + 关 calico(drain 一台一台测) 2. policy 翻译(calico NetworkPolicy → CiliumNetworkPolicy,多数语法兼容) 3. 启用 hubble + kube-proxy replacement 4. 启用 mTLS(替代 Istio mTLS) 效果: - service-to-service latency 平均 -30%(无 iptables) - 一次 debug "为啥 A 连不上 B" 从一上午 → 5 分钟(hubble 直接看 verdict) - 节点 CPU -10%(kube-proxy 不跑了) 迁移挑战: - L7 policy 需要 sidecar (envoy embed in cilium),资源 +20% - WireGuard 跨 region 需要内核 5.6+ ## 监控 prometheus metrics 几百个: - `cilium_drop_count_total` - `cilium_policy_l7_total` - `cilium_endpoint_state` Grafana 官方 dashboard 拉来直接看。 ## 踩过的坑 1. **内核版本**:很多 eBPF feature 要内核 5.10+。Ubuntu 20.04 默认 5.4 → 升级。 2. **kube-proxy replacement + HostNetwork**:HostNetwork pod 跟 cilium service 不交互 → 部分场景 fall back iptables。 3. **policy 默认 allow**:没 CiliumNetworkPolicy 时 default 允许所有 流量。要 default-deny → 加 catch-all policy。 4. **hubble retention**:默认内存只存 4096 flow → 大 cluster 看不到 历史。配 hubble-export 推 ELK。 5. **multi-cluster identity 冲突**:clustermesh 时不同 cluster pod labels 同 → 路由错。namespace / label 命名规范。
云厂商的 K8s(EKS / GKE / AKS)按月几十美元。自己买几台便宜 VPS 也能跑 K8s——一台 control plane + 2 台 worker,每月几美元。 适合自学 / 个人项目 / 小规模生产。 下面用 kubeadm 起 v1.30 集群。 ## 准备(每台机器都做) ```bash # 关 swap(K8s 要求) sudo swapoff -a sudo sed -i '/swap/d' /etc/fstab # 内核模块 cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay br_netfilter # sysctl cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOF sudo sysctl --system ``` ## 装 containerd ```bash sudo apt update sudo apt install -y containerd sudo mkdir -p /etc/containerd containerd config default | sudo tee /etc/containerd/config.toml # 启 SystemdCgroup(K8s 要求) sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml sudo systemctl restart containerd ``` ## 装 kubeadm / kubelet / kubectl ```bash sudo apt install -y apt-transport-https ca-certificates curl gpg curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key \ | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' \ | sudo tee /etc/apt/sources.list.d/kubernetes.list sudo apt update sudo apt install -y kubelet kubeadm kubectl sudo apt-mark hold kubelet kubeadm kubectl ``` ## 初始化 control plane(只在 master 上) ```bash sudo kubeadm init \ --pod-network-cidr=10.244.0.0/16 \ --apiserver-advertise-address=<MASTER_IP> ``` 完成后会打印一段 `kubeadm join ...` 命令——保存下来,下面 worker 用。 让普通用户用 kubectl: ```bash mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config kubectl get nodes # 状态会是 NotReady —— 因为没装 CNI ``` ## 装 CNI(这里用 Flannel) ```bash kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml # 等 1-2 分钟 kubectl get pods -A kubectl get nodes # Ready 了 ``` 其它 CNI 选择: - **Calico**:网络策略 + BGP,生产更常用 - **Cilium**:eBPF,性能 + 观测最好 - **Flannel**:最简单,仅 overlay 网络 ## Worker 加入 每台 worker 上: ```bash sudo kubeadm join <MASTER_IP>:6443 \ --token <TOKEN> \ --discovery-token-ca-cert-hash sha256:<HASH> ``` Master 上: ```bash kubectl get nodes # NAME STATUS ROLES AGE VERSION # master Ready control-plane 5m v1.30.0 # worker-1 Ready <none> 1m v1.30.0 # worker-2 Ready <none> 1m v1.30.0 ``` ## 部署个 demo ```yaml # nginx-demo.yaml apiVersion: apps/v1 kind: Deployment metadata: name: nginx spec: replicas: 3 selector: matchLabels: { app: nginx } template: metadata: { labels: { app: nginx } } spec: containers: - name: nginx image: nginx:alpine ports: [{ containerPort: 80 }] --- apiVersion: v1 kind: Service metadata: name: nginx spec: type: NodePort selector: { app: nginx } ports: - port: 80 nodePort: 30080 ``` ```bash kubectl apply -f nginx-demo.yaml kubectl get pods -o wide curl http://<任意-Node-IP>:30080 ``` ## Ingress(暴露 80/443 到公网) NodePort 30000+ 端口不友好,用 Ingress: ```bash kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/baremetal/deploy.yaml ``` ```yaml # ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: example annotations: nginx.ingress.kubernetes.io/rewrite-target: / spec: ingressClassName: nginx rules: - host: app.example.com http: paths: - path: / pathType: Prefix backend: service: { name: nginx, port: { number: 80 } } ``` 把 `<MASTER_IP>` 或 worker IP DNS 指到 `app.example.com`, 80/443 流量进 ingress-nginx,再分发到 service。 ## cert-manager 自动 HTTPS ```bash kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml ``` ```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: { name: letsencrypt-prod } spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: [email protected] privateKeySecretRef: { name: letsencrypt-prod } solvers: - http01: ingress: { class: nginx } ``` Ingress 加 annotation `cert-manager.io/cluster-issuer: letsencrypt-prod`, 自动签发 + 续期证书。 ## 备份 etcd ```bash # 在 master 上 sudo ETCDCTL_API=3 etcdctl \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key \ snapshot save /tmp/etcd-$(date +%F).db ``` 定时备份这个文件到对象存储是单 master K8s 集群唯一的 DR 手段。 ## 升级 ```bash sudo apt-mark unhold kubeadm sudo apt install -y kubeadm=1.30.5-* sudo apt-mark hold kubeadm sudo kubeadm upgrade plan sudo kubeadm upgrade apply v1.30.5 # 然后升 kubelet / kubectl ``` ## 踩过的坑 - swap 没关 kubelet 启动失败:v1.22 之前默认要求;v1.22+ 可以保留 swap 但要在 kubelet 配置里显式 `failSwapOn: false`。 - 防火墙:control plane 需要开 6443、10250、2379-2380,CNI 用的端口 各异(Flannel UDP 8472)。最简单:把 master / worker 之间的网络 完全开放(内网或 VPC)。 - Token 24 小时过期:worker 加入时 `kubeadm token create --print-join-command` 生成新 token。 - 单 master 没冗余:宕机就完蛋。生产多 master + HA load balancer + 外部 etcd cluster。kubeadm 也支持。
## 起因 经常需要"起一台干净 Ubuntu 试个东西"。从 ISO 装 + 改源 + 加 user + 配 SSH 要十几分钟。如果用 cloud-init 配合官方 cloud image,30 秒 能起一台已经配好用户 / 密钥 / 时区 / 软件的 VM,全自动。 ## 解决方案 ### 1. 拿官方 cloud image ```bash # Ubuntu 22.04 LTS cloud image wget https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img # 几百 MB,已预装 cloud-init ``` cloud-init 是个开机时跑的脚本框架,能读 metadata 自动配置: - 用户 + SSH key - hostname / 时区 / locale - 软件包安装 - 自定义脚本 ### 2. 写 user-data `user-data` 是 cloud-init 读的 YAML 配置: ```yaml #cloud-config hostname: devbox timezone: Asia/Shanghai locale: zh_CN.UTF-8 users: - name: alice sudo: ALL=(ALL) NOPASSWD:ALL shell: /bin/bash ssh_authorized_keys: - ssh-ed25519 AAAA... alice@laptop ssh_pwauth: false disable_root: true package_update: true package_upgrade: true packages: - vim - git - curl - htop - tmux - python3-pip - build-essential write_files: - path: /etc/motd content: | Welcome to devbox - provisioned by cloud-init runcmd: - curl -fsSL https://get.docker.com | sh - usermod -aG docker alice - systemctl enable --now docker power_state: mode: reboot delay: 'now' message: 'rebooting after initial setup' ``` 第一行 `#cloud-config` 必须,告诉 cloud-init 这是它能读的格式。 ### 3. 也写 meta-data ```yaml # meta-data instance-id: devbox-001 local-hostname: devbox ``` ### 4. 打成 ISO(NoCloud datasource) cloud-init 启动时找 `cidata` label 的盘读 user-data + meta-data: ```bash mkdir cloud mv user-data meta-data cloud/ genisoimage -output cloud-init.iso -volid cidata -joliet -rock cloud/ # 或 cloud-localds(更直接) cloud-localds cloud-init.iso user-data meta-data ``` ### 5. 用 qemu / libvirt 起 VM ```bash # 拷贝 image(避免污染原始) cp jammy-server-cloudimg-amd64.img devbox.qcow2 # 扩容到 30G qemu-img resize devbox.qcow2 30G # 启动(无图形,串口输出) qemu-system-x86_64 \ -enable-kvm \ -m 4G -smp 4 \ -drive file=devbox.qcow2,if=virtio \ -drive file=cloud-init.iso,if=virtio,format=raw \ -nic user,hostfwd=tcp::2222-:22 \ -nographic ``` 30 秒后 cloud-init 跑完 + reboot,外部 SSH: ```bash ssh -p 2222 alice@localhost # 进去就是配好的环境,docker 可用,时区对 ``` ### 6. 用 virt-install / virsh(更正式) ```bash sudo virt-install \ --name devbox \ --memory 4096 --vcpus 4 \ --disk path=/var/lib/libvirt/images/devbox.qcow2,size=30 \ --disk path=/var/lib/libvirt/images/cloud-init.iso,device=cdrom \ --os-variant ubuntu22.04 \ --network bridge=virbr0 \ --import \ --noautoconsole virsh list --all virsh console devbox # 串口 virsh shutdown devbox virsh start devbox ``` ### 7. 在 LXD / Multipass / Proxmox 用 **LXD**(前面章节有提到): ```bash lxc launch ubuntu:22.04 devbox \ --config user.user-data="$(cat user-data)" ``` **Multipass**(macOS / Windows 上跑 Ubuntu VM 最简单): ```bash multipass launch jammy --name devbox \ --cloud-init user-data --cpus 4 --memory 4G --disk 30G ``` **Proxmox**:直接在 UI 给 VM 挂 cloud-init drive,在 web 表单填用户 名 / 密钥 / IP。 ### 8. 公有云 AWS / GCP / Azure 创建实例时 user-data 字段贴上面的 YAML, 开机自动跑。整套 IaC 用 Terraform: ```hcl resource "aws_instance" "web" { ami = "ami-..." # Ubuntu official AMI instance_type = "t3.small" user_data = file("cloud-config.yaml") } ``` ## 效果 - 实验室常备一个 user-data 模板 + Makefile,`make vm name=test` 30 秒 起一台 - 新员工 onboarding 给个 user-data 文件让他在自己机器上起,零摩擦 - 部署模板化:dev / staging / prod 共用 user-data,只改少量变量 - 真实灾备:服务器挂了 cloud-init + 备份恢复,重新 provision 半小时 ## 调试 cloud-init 机器跑起来发现配置没生效: ```bash sudo cloud-init status # done / running / error sudo cloud-init query userdata # 当前 user-data 内容 sudo cat /var/log/cloud-init.log sudo cat /var/log/cloud-init-output.log # runcmd 等的 stdout/stderr ``` `/var/log/cloud-init-output.log` 是最常看的——所有 runcmd 输出在这里。 强制重新跑(**测试用,会改 instance-id**): ```bash sudo cloud-init clean --logs --seed --machine-id sudo reboot ``` ## 踩过的坑 1. **YAML 缩进错**:cloud-init 静默跳过出错段,配置部分生效部分不生效。 `cloud-init schema --config-file user-data` 校验语法。 2. **第二次启动不再跑 user-data**:cloud-init 默认只在首次启动跑。 要重跑改 instance-id:`/var/lib/cloud/data/instance-id` 或者 `cloud-init clean`。 3. **package_upgrade 卡 grub menu prompt**:apt 升级遇到内核配置 选择卡住。`debconf-show grub-pc` 或者 user-data 加: ```yaml bootcmd: - DEBIAN_FRONTEND=noninteractive apt-get update ``` 4. **runcmd 提早 exit**:`runcmd:` 里某条失败不影响后面(默认 set +e)。 要按顺序 fail-fast 用 `bash -e` 包: ```yaml runcmd: - bash -e -c 'curl ... && systemctl restart ...' ``` 5. **私钥不在 user-data**:user-data 是明文。永远只写 public key, secret 通过其它机制注入(vault / env / fetch from S3)。
## 起因 需要分布式跑 Python: - ML 训练(多 GPU 多机) - 批 inference(百万张图过 model) - hyperparameter sweep(试 1000 个组合) - 数据预处理(pandas 不够大) 老办法: - spark:scala-friendly,PySpark 写得别扭,启动重 - multiprocessing:单机 - celery:任务队列,不为 compute 设计 `Ray`(UC Berkeley 出,2018+)让 Python 函数加 `@ray.remote` 即分布式。 轻量 + Python 原生。 ## 装 ```bash pip install ray ``` ## 本地用 ```python import ray @ray.remote def square(x): return x * x ray.init() futures = [square.remote(i) for i in range(100)] results = ray.get(futures) # 等结果 print(results) ``` `square.remote(i)` 异步丢 task,返回 future。 `ray.get()` 阻塞拿结果。 本地多核就用多核(自动)。 ## actor (持久 state) ```python @ray.remote class Counter: def __init__(self): self.count = 0 def inc(self): self.count += 1 return self.count c = Counter.remote() c.inc.remote() # 异步调 c.inc.remote() print(ray.get(c.inc.remote())) # 3 ``` actor 跑在某 worker 上,state 持久。多 caller 共享。 适合:load 一个大 model 在 actor 里,多个 request 调它(避免重复加载)。 ## 多机集群 启动 head node: ```bash ray start --head --port=6379 ``` worker node 加入: ```bash ray start --address='head_node:6379' ``` 代码不变: ```python ray.init(address='auto') # 连接 cluster ``` `square.remote` 自动 schedule 到任意 worker。 ## 资源 / GPU 请求 ```python @ray.remote(num_cpus=2, num_gpus=1) def train(data): # 跑 GPU 训练 ... ``` Ray scheduler 把 task 放到有 GPU + 2 CPU 的 worker 上。 不够资源 → 排队等。 ## 数据:Ray Data ```python import ray ds = ray.data.read_parquet('s3://bucket/data/*.parquet') # 分布式 map processed = ds.map(lambda r: {**r, 'doubled': r['x'] * 2}) # 分布式 group + agg result = processed.groupby('country').sum() result.write_parquet('s3://bucket/out/') ``` 类似 spark RDD / dataframe,但 Python 原生。 ## ML:Ray Train ```python from ray.train.torch import TorchTrainer from ray.train import ScalingConfig def train_func(config): model = ... for epoch in range(config['epochs']): ... trainer = TorchTrainer( train_func, scaling_config=ScalingConfig(num_workers=4, use_gpu=True), train_loop_config={'lr': 1e-3, 'epochs': 10}, ) result = trainer.fit() ``` 4 GPU 分布式 train,PyTorch DDP 自动配置。 ## ML:Ray Tune (hyperparameter) ```python from ray import tune def trainable(config): score = train_model(config['lr'], config['dropout']) tune.report(score=score) tuner = tune.Tuner( trainable, param_space={ 'lr': tune.loguniform(1e-5, 1e-2), 'dropout': tune.uniform(0, 0.5), }, tune_config=tune.TuneConfig(num_samples=100), ) results = tuner.fit() ``` 跑 100 个 trial 在集群 → 自动 schedule。 内置 HyperBand / Population-Based Training 等算法。 ## ML:Ray Serve (推理 server) ```python from ray import serve @serve.deployment(num_replicas=4, ray_actor_options={'num_gpus': 1}) class Predictor: def __init__(self): self.model = load_model() async def __call__(self, request): data = await request.json() return self.model.predict(data) serve.run(Predictor.bind()) ``` 4 replica 在 4 个 GPU,前面 router 自动 LB。 比手写 FastAPI + load model worker 简单。 ## 与 spark 对比 | | Ray | Spark | |---|---|---| | 语言 | Python first | Scala / Java first | | 数据 size | < 10 TB | PB | | ML 集成 | 强(Ray Train/Tune/Serve) | 中(MLlib) | | 启动 | < 5s | 30s+ | | 心智模型 | Python actor / task | RDD / dataframe | | 生态 | ML / RL / serving | 通用大数据 | - 数据 < 10 TB + Python 重 → Ray - 数据 > 10 TB + Java/Scala 团队 → Spark - ML + 分布式训练 → Ray - ETL 跑数仓 → Spark / dbt ## 与 dask 对比 dask 也是 Python 分布式。 | | Ray | dask | |---|---|---| | API | actor + task | array / dataframe / bag | | ML | Ray Train/Tune/Serve | dask-ml(弱) | | 资源 model | 强(GPU / 自定义) | 中 | | 通用 compute | 是 | 是 | dask 更"分布式 pandas/numpy",Ray 更"分布式 Python compute + ML"。 ML 选 Ray;纯数据 ETL 选 dask 或 polars streaming。 ## 一个 case:分布式批 inference 需求:1 亿张图过 vision model。 ```python import ray import torch ray.init(address='auto') # cluster 8 GPU node @ray.remote(num_gpus=1) class Inferencer: def __init__(self): self.model = torch.load('model.pt').cuda().eval() @torch.no_grad() def predict(self, batch): return self.model(batch.cuda()).cpu() # 4 actor 4 GPU predictors = [Inferencer.remote() for _ in range(4)] # stream 数据 ds = ray.data.read_images('s3://bucket/images/').map_batches(preprocess, batch_size=64) # 分布式调 results = ds.map_batches( lambda batch: ray.get(predictors[0].predict.remote(batch)), # 简化 compute=ray.data.ActorPoolStrategy(size=4), ) results.write_parquet('s3://bucket/predictions/') ``` 8 GPU 跑满 → 1 亿张图几小时跑完。 ## 监控 ray dashboard 8265 端口默认开: - 集群资源使用 - task / actor 状态 - log 聚合 - profiler 调试比 spark UI 友好。 ## 部署 - **KubeRay**:k8s operator 部署 Ray cluster - **Anyscale**:Ray 公司的托管服务 - **手动**:ssh 到每 node `ray start` prod 用 KubeRay 多。 ## 踩过的坑 1. **object size 大**:ray 传 object 用 plasma store,几 GB object 序列化贵。共享大 dataset 用 `ray.put(data)` 一次 + 引用。 2. **import 依赖差**:worker node 没装跟 head 一样的 Python package → import error。runtime_env 指定 deps,或者 docker image 统一。 3. **GPU 假资源**:`num_gpus=1` 只是 Ray scheduler 视角,task 内部 仍要 `cuda()` 实际用。配错 → 多 task 抢同 GPU。 4. **schedule 不平衡**:data skew → 某 worker 一直累。 `ray.data.repartition()` 重新均衡。 5. **Ray 版本兼容**:head + worker Ray 版本必须一致。docker image pin 版本。
消息队列两个最主流:RabbitMQ(AMQP)和 Kafka(log-based)。设计哲学 完全不同,选错会很疼。 ## 一句话区分 - **RabbitMQ**:消息**任务**,消费后即删,适合"事件触发处理" - **Kafka**:消息**日志**,消费后保留,适合"事件流回放 / 多消费者" ## 详细对比 | 维度 | RabbitMQ | Kafka | |---|---|---| | 数据模型 | queue + exchange | partitioned log | | 消息 retention | 消费后删 | 时间 / 大小(默认 7 天) | | 顺序保证 | 单 queue | 单 partition | | 多消费者 | competing consumers(争抢) | consumer group(offset 独立) | | 持久化 | 可选 | 总是 | | 单 broker 吞吐 | 1k-50k msg/s | 100k-1M msg/s | | 重试 / 死信 | 内置 DLX | 自己处理 | | 路由 | 灵活(topic / fanout / direct / headers) | 简单(topic + partition key) | | 协议 | AMQP / STOMP / MQTT | 自家二进制 | | 复杂度 | 中 | 高 | ## 何时选 RabbitMQ - "用户注册了,发欢迎邮件"——单次任务,一个消费者处理 - 任务队列(Celery 用的就是 RabbitMQ / Redis) - RPC over message - 中等吞吐(< 50k msg/s) ## 何时选 Kafka - "用户做了 N 个动作,多个下游各自处理"——同一消息多消费者 - 事件流 / 实时分析(订单流 → 多个团队订阅) - 日志聚合管道(应用日志 → Kafka → Logstash / ELK) - 大吞吐(> 100k msg/s) ## RabbitMQ 最小部署 ```bash docker run -d --hostname rabbit \ --name rabbit -p 5672:5672 -p 15672:15672 \ rabbitmq:4-management ``` Web 管理界面 http://localhost:15672 默认 guest/guest。 ### 发 / 收(Python pika) ```bash uv add pika ``` ```python # producer.py import pika conn = pika.BlockingConnection(pika.ConnectionParameters('localhost')) ch = conn.channel() ch.queue_declare(queue='tasks', durable=True) for i in range(100): ch.basic_publish( exchange='', routing_key='tasks', body=f'task {i}'.encode(), properties=pika.BasicProperties(delivery_mode=2), # persistent ) conn.close() ``` ```python # consumer.py import pika def callback(ch, method, props, body): print(f'got: {body!r}') # 假装处理 ch.basic_ack(delivery_tag=method.delivery_tag) conn = pika.BlockingConnection(pika.ConnectionParameters('localhost')) ch = conn.channel() ch.queue_declare(queue='tasks', durable=True) ch.basic_qos(prefetch_count=1) # 一次只抓一条,处理完才下一条 ch.basic_consume(queue='tasks', on_message_callback=callback) ch.start_consuming() ``` 启动多个 consumer:消息在它们之间分配(competing consumers)。 ### 死信队列(DLX) 任务失败重试几次后丢进 DLQ 人工检查: ```python ch.queue_declare( queue='tasks', durable=True, arguments={ 'x-dead-letter-exchange': '', 'x-dead-letter-routing-key': 'tasks-dlq', 'x-message-ttl': 30000, # 30 秒处理超时 'x-max-length': 100000, # 队列上限 }, ) ch.queue_declare(queue='tasks-dlq', durable=True) ``` ## Kafka 最小部署 Kafka 4.x KRaft 模式(不再需要 ZooKeeper): ```yaml # docker-compose.yml services: kafka: image: apache/kafka:3.8.0 ports: [ "9092:9092", "9093:9093" ] environment: KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: broker,controller KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 CLUSTER_ID: 'MkU3OEVBNTcwNTJENDM2Qk' ``` ```bash docker compose up -d ``` ### 发 / 收(Python kafka-python) ```bash uv add kafka-python ``` ```python # producer.py from kafka import KafkaProducer import json p = KafkaProducer( bootstrap_servers='localhost:9092', value_serializer=lambda v: json.dumps(v).encode(), acks='all', # 等所有副本确认(最安全) ) for i in range(100): p.send('events', value={'id': i, 'type': 'click'}) p.flush() ``` ```python # consumer.py from kafka import KafkaConsumer c = KafkaConsumer( 'events', bootstrap_servers='localhost:9092', group_id='analytics', auto_offset_reset='earliest', # 从头开始(如果是新 group) value_deserializer=lambda v: json.loads(v.decode()), enable_auto_commit=False, ) for msg in c: print(f'partition={msg.partition} offset={msg.offset} value={msg.value}') # 处理... c.commit() # 手动 commit offset,处理失败下次重读 ``` ### 多消费组 ```python KafkaConsumer('events', group_id='analytics', ...) KafkaConsumer('events', group_id='billing', ...) KafkaConsumer('events', group_id='email', ...) ``` 三个 group 独立从头消费同一 topic —— 这是 Kafka 的核心能力。 ### partition + 顺序 ```python p.send('events', key=b'user-42', value=...) # 同一 key 总是去同一 partition → 同一 user 的事件保证顺序 ``` partition 数决定并行度上限:一个 partition 同时只能被同 group 内一个 consumer 消费。 ## 运维差异 **RabbitMQ**:装在一台机器开心用,复杂集群 erlang 配很麻烦 **Kafka**:单机能跑但生产至少 3 broker;K8s 用 Strimzi operator 简化 ## 监控 **RabbitMQ**:内置 Web UI + Prometheus exporter **Kafka**:Kafka Manager / Conduktor / Kowl / kafka_exporter ## 替代 / 衍生 - **NATS / JetStream**:超轻量 message + streaming,Go 写的 - **Pulsar**:Kafka + RabbitMQ 优点合一,复杂度也合一 - **Redis Streams**:Redis 内置 stream,5.x+,比 Kafka 简单 50x - **AWS SQS / Google PubSub**:托管,省运维 ## 踩过的坑 - RabbitMQ 没设 `prefetch_count` → 一个 consumer 抓走全部消息, 其它 consumer 闲着。设 prefetch=1 或较小数。 - Kafka offset 自动 commit + 处理失败:消息已经"算消费过了",重启后丢失。 生产用手动 commit。 - Kafka 一开始 partition 数定得太少:扩 partition 复杂(顺序 / rebalance)。 规划时按峰值并发 × 2-3 倍。 - 把 RabbitMQ 当 log 用(永久保留消息):会胀死 broker。换 Kafka。
## 起因 cloud 资源(RDS / S3 / VPC)管理工具: - **Terraform**:HCL,最广泛,但状态文件 / drift 问题多 - **Pulumi**:用 TS/Python 写 - **Crossplane**:K8s controller,cloud 资源当 CR 管 最近用 Crossplane 一个项目,下面对比经验。 ## Terraform 简单回顾 ```hcl resource "aws_db_instance" "main" { identifier = "myapp-db" engine = "postgres" instance_class = "db.t3.medium" allocated_storage = 100 username = "admin" password = var.db_password } ``` ```bash terraform plan terraform apply ``` - `.tfstate` 文件存当前资源映射 - 多人协作要 remote state(S3 + DynamoDB lock) - drift 检测靠 `terraform plan` ## Crossplane 思路 cloud 资源是 K8s CRD。`kubectl apply` 创建 cloud 资源。 ```bash # 装 Crossplane helm install crossplane crossplane-stable/crossplane -n crossplane-system # 装 AWS provider kubectl apply -f - <<EOF apiVersion: pkg.crossplane.io/v1 kind: Provider metadata: name: provider-aws spec: package: xpkg.upbound.io/upbound/provider-aws:v1.0.0 EOF ``` ## 创建 RDS ```yaml apiVersion: rds.aws.upbound.io/v1beta1 kind: Instance metadata: name: myapp-db spec: forProvider: region: us-east-1 engine: postgres engineVersion: "16" instanceClass: db.t3.medium allocatedStorage: 100 username: admin passwordSecretRef: name: db-pass key: password namespace: default ``` ```bash kubectl apply -f rds.yaml # Crossplane controller 调用 AWS API 创建 RDS ``` cluster 里看: ```bash kubectl get instance.rds.aws.upbound.io NAME SYNCED READY ... myapp-db True True ``` ## composition (高层抽象) 写一个 "AppDatabase" abstraction,dev 不用写 50 行 RDS yaml: ```yaml apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition metadata: name: appdatabases.example.com spec: group: example.com names: kind: AppDatabase plural: appdatabases versions: - name: v1 served: true referenceable: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: size: type: string enum: [small, medium, large] env: type: string ``` ```yaml apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: appdatabase-aws spec: compositeTypeRef: apiVersion: example.com/v1 kind: AppDatabase resources: - name: rds-instance base: apiVersion: rds.aws.upbound.io/v1beta1 kind: Instance spec: forProvider: engine: postgres engineVersion: "16" patches: - fromFieldPath: spec.size toFieldPath: spec.forProvider.instanceClass transforms: - type: map map: small: db.t3.small medium: db.t3.medium large: db.r6g.xlarge ``` dev 写: ```yaml apiVersion: example.com/v1 kind: AppDatabase metadata: name: my-app-db spec: size: medium env: prod ``` → Crossplane 自动创建 RDS + 关联资源。 平台团队 control 实际 cloud impl,dev 用简单接口。 ## 对比 | | Terraform | Crossplane | |---|---|---| | 状态存储 | tfstate file | k8s etcd | | 语言 | HCL | YAML + composition | | Drift 检测 | terraform plan 主动 | controller 持续 reconcile | | 自愈 | 不 | 是(detect drift 自动修) | | 多人协作 | state lock | k8s 原生 RBAC | | 学习曲线 | 中 | 高(要懂 K8s) | | 生态 | 极大 | 中 | | GitOps | 需 Atlantis 等 | 原生(kubectl apply) | ## 优势:自愈 Crossplane controller 每 N 分钟比 cluster CR 与实际 cloud 资源: ``` desired (yaml): RDS storage = 100 GB actual (AWS): storage = 200 GB (手动改了) ↓ Crossplane: 调 API 改回 100 GB ``` terraform 不主动 reconcile,要手动 `apply` 才修。 不想 reconcile 某字段: ```yaml spec: managementPolicies: [Observe] # 只读不改 ``` 或者 `ignoreFields`。 ## 优势:GitOps 一致 ArgoCD 同步 Crossplane CR → cluster apply → Crossplane 调 cloud API。 整条链 git → cluster → cloud 都声明式。 Terraform 配 ArgoCD 不容易(Terraform 是命令式 apply)。 ## 缺点:复杂 - composition 写起来累(patches / transforms) - debug 调试链长(CR → composition → managed resource → cloud API) - provider 质量参差(aws 强,azure / gcp 较新) - 没 terraform 那种 module 生态 ## 缺点:要 K8s Crossplane 跑在 K8s cluster 里。 要管 cloud 资源 → 必须先有 K8s cluster(鸡生蛋问题)。 我们方案:一个 "management cluster"(小 EKS)跑 Crossplane → 它管 其它 cluster + cloud 资源。 ## 适合的场景 - 已经深度 K8s - 多团队 self-service(dev 写 yaml,平台控制 composition) - ArgoCD 已用 → 想统一 GitOps - 容忍 K8s 复杂度 不适合: - 单 cloud 资源 / 简单项目 → Terraform 够 - 团队不会 K8s - 需要快速 prototype ## 与 Pulumi 对比 | | Terraform | Pulumi | Crossplane | |---|---|---|---| | 语言 | HCL | TS/Python/Go | YAML | | 真正编程 | 弱 | 强 | 弱 | | state | tfstate | own backend | k8s etcd | | 多人 | locking | locking | k8s | | 学习 | 易 | 中 (语言+SDK) | 高 | Pulumi 是"真编程语言写基础设施"。 Crossplane 是"K8s 思维管基础设施"。 Terraform 是"DSL 描述基础设施"。 ## 真实 case 某客户多 cluster + 多 cloud 资源: - Terraform:3 个团队各自有 tfstate,conflict / drift 频发 - 改 Crossplane: - 平台团队建 AppDatabase / AppQueue / AppBucket 等 abstraction - 业务团队 yaml 申请:`kind: AppDatabase, size: medium` - Crossplane 创建 + 自愈 - ArgoCD 跟踪 效果: - 业务 dev 不学 RDS 细节 - 资源命名 / tag 强制规范(composition 模板控制) - drift 自动修 - 一切都在 ArgoCD UI 可见 挑战: - composition 写起来痛苦(YAML 模板表达力差) - provider bug 偶尔(aws provider 几个版本 ec2 有问题) ## 与 ACK (AWS Controllers for K8s) ACK 是 AWS 官方出的 K8s controller for AWS 资源。 跟 Crossplane provider-aws 重叠。 | | Crossplane | ACK | |---|---|---| | Composition | ✅ | ❌ | | 多 cloud | ✅ | AWS only | | 抽象 | 强 | 弱 | | 稳定 | 中 | 高(AWS 维护) | 如果只 AWS + 不要 composition → ACK。 多 cloud / 要 abstraction → Crossplane。 ## 踩过的坑 1. **YAML 长**:1 个 RDS 的 yaml 100 行。composition 抽象后 dev 写 5 行, 但 platform team 写 composition 累。 2. **provider 升级 breaking**:provider-aws v1 → v2 schema 变 → 所有 CR 要改。锁 provider 版本 + 计划升级。 3. **delete 危险**:删 CR 默认 delete cloud 资源。`deletionPolicy: Orphan` 保留 cloud 资源仅删 CR。 4. **多 region**:每 region 一个 ProviderConfig。CR 用 `providerConfigRef` 指。 5. **state recovery**:万一 etcd 丢 → cloud 资源仍在但 cluster 不知道。 `crossplane beta` 有 import 功能。
## 起因 之前用 Jaeger 做 trace 后端。痛点: - ES 后端吃内存 + 维护 ES 集群烦 - UI 比 Grafana 弱,跟 Prometheus / Loki 不在一处看 - trace 数据保留几天就磁盘满 Tempo 是 Grafana Labs 的开源 trace backend,专为对象存储设计: - 后端是 S3 / GCS / 任意对象存储(便宜 + 无限容量) - 不用 ES,省内存 - 跟 Grafana / Loki / Prometheus 同生态,单 UI 串起 metric / log / trace ## 安装 `docker-compose.yml`(开发 / 单机生产): ```yaml services: tempo: image: grafana/tempo:2.6.0 command: ['-config.file=/etc/tempo/tempo.yaml'] volumes: - ./tempo.yaml:/etc/tempo/tempo.yaml - tempo-data:/var/tempo ports: - "3200:3200" # tempo API - "4317:4317" # OTLP gRPC - "4318:4318" # OTLP HTTP grafana: image: grafana/grafana:11.2.0 ports: ["3000:3000"] environment: GF_AUTH_ANONYMOUS_ENABLED: 'true' GF_AUTH_ANONYMOUS_ORG_ROLE: 'Admin' volumes: - ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/ds.yaml volumes: tempo-data: ``` `tempo.yaml`: ```yaml server: http_listen_port: 3200 distributor: receivers: otlp: protocols: grpc: { endpoint: 0.0.0.0:4317 } http: { endpoint: 0.0.0.0:4318 } ingester: trace_idle_period: 10s max_block_duration: 5m compactor: compaction: block_retention: 168h # 7 天保留 storage: trace: backend: local # 本地磁盘;生产换 s3 / gcs local: path: /var/tempo/traces wal: path: /var/tempo/wal ``` 生产 storage 切到 S3: ```yaml storage: trace: backend: s3 s3: endpoint: s3.us-east-1.amazonaws.com bucket: my-tempo-traces access_key: ${AWS_ACCESS_KEY_ID} secret_key: ${AWS_SECRET_ACCESS_KEY} wal: path: /var/tempo/wal ``` S3 / Backblaze B2 / Cloudflare R2 都可。Tempo 把 trace 块存对象存储, "无限"容量 + 极便宜(B2 $6/TB/月)。 ## Grafana 接 Tempo `grafana-datasources.yaml`: ```yaml apiVersion: 1 datasources: - name: Tempo type: tempo access: proxy url: http://tempo:3200 isDefault: true jsonData: tracesToLogsV2: datasourceUid: 'loki' spanStartTimeShift: '-1m' spanEndTimeShift: '1m' tags: ['service.name', 'pod', 'container'] serviceMap: datasourceUid: 'prometheus' nodeGraph: enabled: true ``` 启动后 Grafana → Explore → 选 Tempo → 输 trace ID 直接查; 或用 Search 按 service / operation / tag 找。 ## 应用端发 trace 跟之前 OpenTelemetry 那篇一样,应用配 OTLP 上报到 `tempo:4317`: ```bash OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4317 \ opentelemetry-instrument myapp ``` Tempo 完全 OTLP 兼容,不需要换 SDK。 ## TraceQL(强大查询语言) Tempo 2.x 支持 TraceQL,类似 LogQL 但针对 trace: ``` # 找耗时 > 1s 的 trace { duration > 1s } # 找特定 service 的 error { service.name = "api" && status = error } # 找 span 含特定 attr { span.http.status_code = 500 } # 找包含两个特定操作的 trace { name = "db.query" } && { name = "redis.set" } ``` 比 Jaeger UI 表单过滤强大很多。复杂查询能精确定位"哪类调用慢 / 错"。 ## 与 Loki 关联:trace ↔ log trace 视图里点 span → 显示该时段对应服务的 log(Loki 拉)。 反过来:Loki 日志里有 trace_id → 点击直接跳 trace 视图。 让 OTel SDK 在 log 中注入 trace_id: ```python import logging from opentelemetry.instrumentation.logging import LoggingInstrumentor LoggingInstrumentor().instrument(set_logging_format=True) ``` log 自动带 trace_id / span_id。Loki 配置认识这个字段。 ## 与 Prometheus 关联:service graph + metric ```yaml # tempo.yaml metrics_generator: registry: external_labels: source: tempo storage: path: /var/tempo/generator/wal remote_write: - url: http://prometheus:9090/api/v1/write overrides: defaults: metrics_generator: processors: [service-graphs, span-metrics] ``` Tempo 自动从 trace 生成: - `traces_service_graph_request_total`:服务调用关系(A 调 B 多少次) - `traces_spanmetrics_latency_bucket`:每 endpoint 的延迟分布 这些指标进 Prometheus 后 Grafana 仪表盘 + 告警。 **不需要应用端写 metric 代码**,trace 自动衍生 metric。 ## 资源占用对比 我们生产 25 个服务,trace 量大约 10k spans/s: | | Jaeger + ES | Tempo + S3 | |---|---|---| | 总 RAM 占用 | ~12 GB | ~3 GB | | 存储 7 天 | 200 GB SSD ($30/月 EBS) | 80 GB S3 ($2/月) | | 维护负担 | ES 集群 ops | tempo single binary | | UI 体验 | Jaeger UI ok | Grafana 统一 | | TraceQL | ❌ | ✅ | Tempo 显著省 + 体验更好。 ## 与替代品 | | Tempo | Jaeger | SigNoz | DataDog APM | |---|---|---|---|---| | 后端 | 对象存储 | ES / Cassandra | ClickHouse | 商业云 | | 自托管 | ✅ 简单 | ✅ 但 ES 麻烦 | ✅ 中 | ❌ | | 价格 | 极便宜 | 维护贵 | 中 | 贵 | | 与 metric/log 集成 | Grafana 统一 | 分散 | 内置 | DD 统一 | 预算紧 → Tempo / Jaeger。 要 metric/log/trace one-stop → Grafana stack 或 DataDog(贵但省心)。 ## 采样 100% 采样 trace 数据爆炸。生产建议: ```yaml processors: probabilistic_sampler: sampling_percentage: 10 ``` 10% 全采。或更智能 tail sampling:保留所有 error + 慢 trace + 5% 普通。 收集端(OTel Collector)配采样,Tempo 端不再处理。 ## 完整 stack 图 ``` 应用 (Python/Go/Node) ↓ OTLP OTel Collector (采样 + 路由) ↓ OTLP Tempo (trace 存 S3) ↑ TraceQL Grafana (UI) ← Loki (logs) ← Prometheus (metrics) ``` 3 个数据库(trace/log/metric),1 个 UI(Grafana),完全开源。 ## 踩过的坑 1. **block_retention 配错**:`168h` 7 天;如果想 30 天写 `720h`。 单位 `m` `h` `d` 都识别。 2. **对象存储 list 频繁**:Tempo 每几秒 list bucket 找新 block, B2 / R2 收 API 调用费。配 `query_frontend.search.cache_control` 减少 list 频率。 3. **WAL 损坏**:单机突然断电 → wal 损坏 → tempo 启动失败。 `rm -rf /var/tempo/wal/*` 丢失正在 ingest 的几秒数据,重启 OK。 生产用 PV / 持久 disk。 4. **多 tenant 没配**:默认 single-tenant;多团队共用同一 Tempo 要开 multitenancy + Auth header。 5. **search 索引慢**:trace 查找需要 search index,老版本 search 慢。 2.0+ 显著改进;用最新。 ## 切换的实际感受 从 Jaeger + ES 切到 Tempo + S3 用了 1 周(双跑 + 切流量): - 内存释放 9 GB - 存储费用降 15x - Grafana 统一仪表盘体验"看 metric → 跳 trace → 跳 log" 流畅 - TraceQL 让"高 latency P99 的 trace" 一行 query 出来 强烈推荐新项目用 Tempo + Grafana stack 而非 Jaeger。
## 起因 要做服务间异步消息:常用选择 Kafka / RabbitMQ / Redis Streams。 Kafka 要装 ZooKeeper 或 KRaft,集群配置复杂;RabbitMQ 单机够但 "事件流"语义弱;Redis Streams 简单但不算 first-class。 NATS 是 CNCF 项目,单二进制、< 20MB 内存、跨平台。 JetStream 是 NATS 内置的持久化层,把 NATS pub/sub 升级到 Kafka-like 能力:persistent stream / consumer / replay / 多副本。 ## 5 分钟起服务 ### 1. 装 ```bash # 二进制下载 curl -fsSL https://github.com/nats-io/nats-server/releases/latest/download/nats-server-v2.10.20-linux-amd64.tar.gz \ | tar xz sudo install nats-server-v2.10.20-linux-amd64/nats-server /usr/local/bin/ # 或 Docker docker run -p 4222:4222 -p 8222:8222 \ nats:latest -js -m 8222 # -js 开 JetStream;-m 开监控 endpoint ``` CLI 工具: ```bash go install github.com/nats-io/natscli/nats@latest nats account info ``` ### 2. 创建 stream ```bash nats stream add ORDERS \ --subjects "orders.>" \ --storage file \ --retention limits \ --max-msgs 1000000 \ --max-age 30d \ --discard old \ --replicas 1 ``` "`orders.>`" 通配符:所有以 orders. 开头的 subject 都进这个 stream (orders.created / orders.shipped / orders.cancelled)。 ### 3. 发消息 ```bash nats pub orders.created '{"id": 1, "amount": 99.5}' nats pub orders.shipped '{"id": 1, "carrier": "fedex"}' ``` ### 4. 创建 consumer + 消费 ```bash nats consumer add ORDERS analytics \ --filter "orders.>" \ --ack explicit \ --deliver all \ --replay instant # Pull-based consumer nats consumer next ORDERS analytics --count 10 # 拿 10 条 ``` ## Python 客户端 ```bash uv add nats-py ``` ```python import asyncio import nats from nats.js.api import StreamConfig, ConsumerConfig, RetentionPolicy async def main(): nc = await nats.connect('nats://localhost:4222') js = nc.jetstream() # 确保 stream 存在(幂等) await js.add_stream(name='ORDERS', subjects=['orders.>'], retention=RetentionPolicy.LIMITS, max_age=30 * 24 * 60 * 60) # 发 await js.publish('orders.created', b'{"id": 1, "amount": 99.5}') # 收(pull-based subscriber) psub = await js.pull_subscribe('orders.>', 'analytics', stream='ORDERS') while True: msgs = await psub.fetch(10, timeout=5) for m in msgs: print(m.subject, m.data) await m.ack() asyncio.run(main()) ``` ## 与 Kafka / RabbitMQ 对比 | | NATS JetStream | Kafka | RabbitMQ | |---|---|---|---| | 二进制大小 | 20MB | 100MB+ | 50MB+ | | 内存 | < 50MB | 几百 MB | 几百 MB | | 集群安装 | 一行命令 | 复杂 | 中等 | | 消息 retention | ✅ | ✅ | 弱 | | Stream 模式 | ✅ | ✅ | 弱 | | Queue 模式 | ✅ | 需要外部 | ✅ | | 性能 | 100k+ msg/s | 1M+ msg/s | 几十 k | | 跨地域副本 | 容易(NATS leaf) | 复杂(MirrorMaker) | 中 | | 生态成熟度 | 中(快增) | 极成熟 | 极成熟 | **适合 NATS**:小到中规模、要求轻量、跨地域、自托管简单的场景。 **Kafka 仍然适合**:百万级吞吐 / 已有 Kafka 生态 / 大数据 pipeline。 ## 几个 NATS 杀手 feature ### 1. Request-Reply (RPC) ```python # server async def handler(msg): await msg.respond(b'pong') await nc.subscribe('rpc.ping', cb=handler) # client response = await nc.request('rpc.ping', b'', timeout=1) print(response.data) # b'pong' ``` 不需要 HTTP framework,直接通过 NATS 做 RPC。 比 gRPC 简单:无需 .proto,subject 即 route。 ### 2. Key-Value Store ```python kv = await js.create_key_value(bucket='configs') await kv.put('app.version', b'1.2.3') entry = await kv.get('app.version') print(entry.value) # b'1.2.3' # Watch changes async for entry in kv.watch_all(): print('changed:', entry.key, entry.value) ``` JetStream 上的小 K/V,replicated + watch 通知。 替代 etcd 的简单场景。 ### 3. Object Store ```python obs = await js.create_object_store(bucket='photos') await obs.put('logo.png', b'<png bytes>') data = await obs.get('logo.png') ``` S3 替代品(小规模),数据在 stream 里。 ### 4. Leaf node(跨地域 / 边缘) 总部跑 main NATS server,分公司跑 leaf node 连过来: ``` # leaf node config leafnodes { remotes = [ { url: "nats://hq.example.com:7422" } ] } ``` leaf 上发的消息自动同步到总部。流量本地优先,节省跨地域带宽。 ## 实战:订单事件总线 ``` service: order-api ↘ service: payment-svc ↘ publish orders.* ↘ service: shipment-svc ↗ ↘ JetStream ↗ ↙ service: analytics (subscribe orders.> as 'analytics') service: email-svc (subscribe orders.created as 'email-notifier') service: webhook-svc (subscribe orders.> as 'webhook-publisher') ``` 每个消费方独立 consumer,独立维护 offset。 order-api 一次 publish,5 个下游各自异步处理。 加新下游不影响现有: ```python # 新加 'audit-log' consumer await js.add_consumer(stream='ORDERS', config=ConsumerConfig( durable_name='audit-log', filter_subject='orders.>', deliver_policy=DeliverPolicy.ALL, # 从最早开始 )) ``` `deliver_policy=ALL` 让新 consumer 从 stream 最早消息开始处理, 回放历史(Kafka 一样能力)。 ## 监控 NATS 自带 `:8222/varz` /`:8222/jsz` 等 endpoint。Prometheus exporter: ```bash docker run -p 7777:7777 \ natsio/prometheus-nats-exporter:latest \ -varz -connz -channelz -subz http://nats:8222 ``` Grafana 用现成 dashboard ID 2279 / 2280。 ## 高可用:cluster + replicas 3 node cluster: ```bash nats-server -p 4222 -n n1 -cluster_listen 0.0.0.0:6222 -routes nats://n2:6222,nats://n3:6222 -js -sd /data/n1 nats-server -p 4223 -n n2 -cluster_listen 0.0.0.0:6222 -routes nats://n1:6222,nats://n3:6222 -js -sd /data/n2 nats-server -p 4224 -n n3 -cluster_listen 0.0.0.0:6222 -routes nats://n1:6222,nats://n2:6222 -js -sd /data/n3 ``` Stream `--replicas 3` → 3 副本(Raft 共识)。一台挂仍可用。 ## 效果 我们一个中型 SaaS 把消息中间件从 Redis Streams 换 NATS JetStream: - 容器内存从 250MB(Redis 加事件订阅逻辑)→ 60MB(NATS 单进程) - 跨地域分公司间消息同步 native 支持(之前要自己写桥) - delivery semantics 更清楚(at-least-once 默认,exactly-once 配置开) - consumer 重连 / replay 等行为类 Kafka,无需自己处理 ## 踩过的坑 1. **`-js` 没加忘了启 JetStream**:以为 stream 创建失败, `failed to find storage directory`。第一次启动一定加 `-js`。 2. **`--max-msgs` / `--max-bytes` 一定要设**:默认 unlimited, 磁盘吃满。 3. **consumer ack 超时配置**:处理慢的 consumer 默认 30s ack 不及 消息被 redelivered。`--ack-wait` 调大。 4. **多语言客户端兼容性**:Go / Python / Node / Java / Rust / C# 都有 官方 client;version 一致性要注意(client 落后 server 可能少 feature)。 5. **TLS / auth**:开放公网必须配 `--tls` + `--auth` + 用户密码或 nkey。默认 anonymous + plain 安全为零。