eCapture开源项目三周年, v1.0.0 稳定版发布 eCapture旁观者是一款基于 eBPF 技术的高级网络工具,专注于 SSL/TLS 协议的明文数据捕获与解密。凭借其强大的功能和丰富的特性,eCapture 已成为网络调试和安全分析领域的重要工具。自2022年3月发布至今三年,累计1.4万颗星。 核心功能 1. 多协议支持 加密库支持:兼容 OpenSSL、GnuTLS、BoringSSL 等多种加密库,支持 HTTP/2 和 QUIC 协议捕获。 协议兼容性:涵盖 TLS 1.2 和 TLS 1.3,确保对最新协议的全面支持。 2. 智能数据捕获 eBPF 引擎:基于 eBPF 技术实现高效的网络数据捕获和协议解析。 双栈支持:支持 IPv4 和 IPv6 的双栈模式,确保在不同网络环境中的灵活应用。 四元组过滤:提供灵活的过滤功能,精准捕获目标流量。 3. 主密钥捕获 自动化捕获:自动检测并捕获 TLS 1.2 和 TLS 1.3 的主密钥,实现加密流量的实时解密。 Wireshark 集成:生成与 Wireshark 兼容的流量文件,支持离线解密和分析。 4. 多种捕获模式 Pcap 模式:生成 Pcapng 格式的流量文件,便于后续分析。 Keylog 模式:捕获通信密钥,生成与 Wireshark 兼容的密钥日志文件。 Text 模式:实时输出明文数据,方便开发者快速调试和分析。 5. 跨平台支持 操作系统:支持 Linux 和 Android 系统,确保在不同操作环境中的稳定运行。 架构兼容:兼容 x86_64 和 ARM64 架构,适用于多种设备和服务器环境。 6. 自动化与智能检测 智能模式选择:自动识别 CO-RE 和非 CO-RE 模式,优化运行性能。 内存优化:提供灵活的内存配置选项,提升资源利用率和系统稳定性。 7. 高性能与兼容性 高效处理:基于 eBPF 的高效数据处理,支持大规模并发和长时间抓包。 稳定性提升:经过多次版本迭代和优化,v1.0.0 版本具有更高的稳定性和兼容性。 8. 灵活配置 自定义过滤:支持灵活的数据过滤配置,精准捕获目标流量。 多种输出格式:支持多种数据输出格式,满足不同场景的需求。 日志管理:提供详细的日志记录,方便后续分析和审计。 9. 命令审计 Shell 审计:捕获 Bash 和 Zsh 命令行的输入和输出,支持实时监控和记录。 数据库监控:监控 MySQL 和 PostgreSQL 的 SQL 查询,提供数据库操作审计功能。 10. Wireshark 插件支持 流量分析:生成兼容 Wireshark 的流量文件,方便使用 Wireshark 进行深入分析。 密钥日志集成:支持在 Wireshark 中加载密钥日志,实现加密流量的实时解密。 技术特点 1. 模块化架构 功能扩展:采用模块化设计,便于功能扩展和维护。 灵活配置:支持多种配置选项,满足不同用户的需求。 2. 多语言开发 内核空间:使用 C 语言实现内核空间功能,确保高效和稳定。 用户空间:使用 Golang 和 CGO 实现用户空间功能,提升开发效率和代码维护性。 3. 交叉编译支持 多架构支持:支持在 amd64 和 ARM64 两种架构下进行交叉编译,生成适用于不同平台的二进制文件。 工具链支持:提供丰富的交叉编译工具链,方便用户根据需求进行编译和配置。 版本更新 v1.0.0 版本 新增功能 支持 Docker 镜像,简化部署和使用。 新增对 OpenSSL 3.4.0 和 GnuTLS 的密钥捕获支持。 支持 IPv6 数据包捕获,扩展网络协议支持。 增加 Zsh 命令审计功能,提升安全监控能力。 关键修复 修复 Ubuntu 24.04 系统上初始化脚本的兼容性问题。 解决 ARM64 版本无法正常工作的问题。 优化构建过程,提升整体稳定性和性能。 社区与开发 开源社区 项目星数:自 2022 年 3 月创建以来,累计获得 14,000+ 颗星,社区活跃度高。 贡献者:吸引了众多开发者参与贡献,共同推动项目发展。 作者信息 开发者:由陈驰(CFC4N)开发,详见其个人博客 www.cnxct.com。 开源精神:秉持开源精神,欢迎更多开发者加入社区,共同改进和完善项目。 部署与使用 易用部署 二进制文件:提供预编译的二进制文件,支持 Linux 和 Android 系统。 Docker 镜像:发布 Docker 镜像,简化部署和使用过程。 文档支持:提供详细的使用文档和配置指南,帮助用户快速上手。 总结 eCapture v1.0.0 是一款功能强大、技术先进的网络工具,凭借其多协议支持、智能数据捕获、跨平台兼容性和高性能,成为开发者和安全研究人员的得力助手。通过三年的不断开发和优化,eCapture 在网络调试和安全分析领域树立了良好的口碑。 未来,随着社区的不断贡献,eCapture 将继续保持其技术领先,服务于更多用户。 2025-08-22 0 0
eCapture v0.9.0支持IPv6、GnuTLS密钥捕获、Zsh审计,国内CDN上线 自上篇文章〔L4LB四层负载均衡IP伪造漏洞〕以来,已经快四五个月没更新,不是笔者太懒,而是实在太忙了,忙得连放屁的时间都没有。得益于社区朋友的贡献,增加了一些新特性,今天来分享给大家。 eCapture v0.9.0版本发布 新功能: 支持Docker镜像拉取,添加对 OpenSSL 3.4.0 的支持,支持 gnutls 的 keylog 和 pcap 模式,允许捕获 IPv6 数据包,拆分日志记录器。 错误修复: 修复了多个系统和功能的关键错误,包括在 Ubuntu 24.04 上的初始化脚本问题和 arm64 版本的兼容性问题。 改进: 更新了日志记录器,优化了构建过程,添加了新的工作流和文档更新。 科普:eCapture是什么 eCapture旁观者是一个无需CA证书,无侵入的HTTPS/TLS明文抓包工具。可以在Linux 4.18以上版本使用,同时也支持Android arm64 5.5以上版本。项目发布三年累计14000颗星。 功能介绍 eCapture 有8个模块,分别支持openssl/gnutls/nspr/boringssl/gotls等类库的TLS/SSL加密类库的明文捕获、Bash、Mysql、PostGres软件审计。 bash 捕获bash、zsh命令行的输入输出 gnutls 捕获基于gnutls类库加密通讯的明文内容 gotls 捕获使用Golang语言编写的,基于内置crypt类库实现TLS/HTTPS加密通讯的明文内容 mysqld 捕获Mysqld的SQL查询,适用于数据库审计场景,支持Mysqld 5.6/5.7/8.0等 nss 捕获基于nss类库加密通讯的明文内容 postgres 支持postgres 10+的数据库审计,捕获查询语句 tls 捕获基于Openssl/Boringssl的加密通讯的明文内容,支持Openssl 1.0.x/1.1.x/3.x系列所有版本,支持BoringSSL所有发行版本 你可以通过ecapture -h来查看这些自命令列表。 重大更新 支持Docker Image镜像启动,可以到Docker Hub下载。 支持gnutls模块的keylog 和 pcap 模式对Gnutls TLS类库的密钥捕获、pcapNG格式文件存储; 支持文本模式-m text下,HTTP 2.0的内容解码; 支持IPv6 协议的网络包捕获; 支持非Android的boringSSL库加密通讯的明文、密钥、pcapNG捕获; 支持Zsh Shell的命令执行安全审计; 支持OpenSSL模块的文本模式下,五元组关联数据; 增加eCapture项目的国内下载地址:https://ecapture.cc/zh/download/ (自费CDN小水管,各位黑客大哥别把我帐号刷欠费了) Gnutls TLS类库捕获 在eCapture的gnutls模块中,新增TLS加密通讯的密钥捕获,以及保存为pcapNG格式文件。 目标进程类库确认 以使用gnutls类库的git命令为例,当执行git pull时,会调用git-remote-http程序来访问远程仓库,该程序使用Gnutls类库 ldd /usr/lib/git-core/git-remote-http linux-vdso.so.1 (0x0000ffff9c6ff000) libcurl-gnutls.so.4 => /lib/aarch64-linux-gnu/libcurl-gnutls.so.4 (0x0000ffff9c550000) libpcre2-8.so.0 => /lib/aarch64-linux-gnu/libpcre2-8.so.0 (0x0000ffff9c4b0000) ... 演示过程 完整演示如下: 首先,在终端中启动ecapture sudo ecapture gnutls -m pcapng -i ens160 -w gnutls-github.pcapng tcp port 443 之后,在另外一个终端拉取git仓库 git pull 演示结果 eCapture的结果输出: sudo ecapture gnutls -m pcapng -i ens160 -w gnutls-github.pcapng tcp port 443 2024-12-15T13:31:38Z INF AppName="eCapture(旁观者)" 2024-12-15T13:31:38Z INF HomePage=https://ecapture.cc 2024-12-15T13:31:38Z INF Repository=https://github.com/gojue/ecapture 2024-12-15T13:31:38Z INF Author="CFC4N <cfc4ncs@gmail.com>" 2024-12-15T13:31:38Z INF Description="Capturing SSL/TLS plaintext without a CA certificate using eBPF. Supported on Linux/Android kernels for amd64/arm64." 2024-12-15T13:31:38Z INF Version=linux_arm64:v0.9.0:6.5.0-1025-azure 2024-12-15T13:31:38Z INF Listen=localhost:28256 2024-12-15T13:31:38Z INF eCapture running logs logger= 2024-12-15T13:31:38Z INF the file handler that receives the captured event eventCollector= 2024-12-15T13:31:38Z INF listen=localhost:28256 2024-12-15T13:31:38Z INF https server starting...You can upgrade the configuration file via the HTTP interface. 2024-12-15T13:31:38Z INF Kernel Info=5.15.167 Pid=150461 2024-12-15T13:31:38Z INF BTF bytecode mode: CORE. btfMode=0 2024-12-15T13:31:38Z INF GnuTlsProbe init eBPFProgramType=PcapNG model=PcapNG 2024-12-15T13:31:38Z INF module initialization. isReload=false moduleName=EBPFProbeGNUTLS 2024-12-15T13:31:38Z INF Module.Run() 2024-12-15T13:31:38Z INF GnuTLS version found Version=3.7.3 2024-12-15T13:31:38Z INF GnuTLS binary path binaryPath=/lib/aarch64-linux-gnu/libgnutls.so.30 elfType=2 2024-12-15T13:31:38Z INF BPF bytecode loaded bytecode filename=user/bytecode/gnutls_3_7_3_kern_core.o 2024-12-15T13:31:38Z INF Hook type: Gnutls elf ElfType=2 IFindex=2 IFname=ens160 PcapFilter="tcp port 443" binrayPath=/lib/aarch64-linux-gnu/libgnutls.so.30 2024-12-15T13:31:38Z INF Hook masterKey function: gnutls_handshake 2024-12-15T13:31:38Z INF target all process. 2024-12-15T13:31:38Z INF packets saved into pcapng file. pcapng path=/home/cfc4n/gnutls-github.pcapng 2024-12-15T13:31:39Z INF perfEventReader created mapSize(MB)=4 2024-12-15T13:31:39Z INF perfEventReader created mapSize(MB)=4 2024-12-15T13:31:39Z INF module started successfully. isReload=false moduleName=EBPFProbeGNUTLS 2024-12-15T13:31:43Z INF CLIENT_RANDOM save success ClientRandom=8517a6741b2a67369079309c2eac84094aacecef39a5514c6da25da7e219401d TlsVersion=GNUTLS_TLS1_3 eBPFProgramType=PcapNG 2024-12-15T13:31:44Z INF packets saved into pcapng file. count=35 2024-12-15T13:31:46Z INF packets saved into pcapng file. count=2 ^C2024-12-15T13:32:15Z INF packets saved into pcapng file. count=37 2024-12-15T13:32:15Z INF Module closed,message recived from Context 2024-12-15T13:32:15Z INF iModule module close 2024-12-15T13:32:15Z INF bye bye. 可以清楚得看到TLS通讯协议是GNUTLS_TLS1_3,密钥的ClientRandom是8517a6741b2a67369079309c2eac84094aacecef39a5514c6da25da7e219401d。 Wirehark查看网络包 如图,不光能看到网络包对应发起的进程信息,还能看到HTTPS的明文内容。 Docker Hub eCapture的Docker Hub仓库地址为:https://hub.docker.com/r/gojue/ecapture ,当前最新版为 v0.9.1 。 # 拉取镜像 docker pull gojue/ecapture:latest # 运行 docker run --rm --privileged=true --net=host -v ${宿主机文件路径}:${容器内路径} gojue/ecapture ARGS Zsh Shell审计 启动命令sudo ecapture zsh,即可捕获zsh上运行的所有命令,对于安全审计场景,还是比较方便快捷的。 其他更多功能,请查看项目变更日志。 变更日志 功能 feat: 在 gnutls 中支持 keylog 和 pcap 模式 feat: 移除 tcp 数据包限制 kern: 支持 openssl 最新版本(3.4.0, 1.1.1w 等) feat: 以文本模式解析 http2 数据 feat: 允许捕获 ipv6 数据包 kern: 在 ebpf TC hook 中支持 uid/pid 过滤 user: 拆分记录器,将其分为记录器和事件收集器 kern: 添加对非 Android boringssl 库的支持 kern: 调整密钥获取的时机,以区分 TLS feat: 支持捕获 zsh 命令 feat: 用远程地址信息丰富地址信息 feat: 支持新版本检测功能 Bug 修复 builder: 修复 init 脚本在 ubuntu 24.04 系统上无法运行的问题 #667 修复 uprobe 类型 hook 的参数错误问题 修复 arm64 版本无法工作的 bug (#649) builder: docerk 构建错误:未找到头文件 修复 Golang TLS 模块中读取函数返回值的问题 fix: 修复在调试模式下 make 时未声明标识符的错误 fix: 回退到默认版本并发出警告 fix: 创建 output.log 失败 pkg: 修复在关闭的通道上发送数据的问题 fix: HEAD 请求中的 DumpResponse 错误 fix: 截断的主体转储错误 user: 修复 #553,hashLen 不允许超过 64 字节 android 版本编译失败 user: 修复 #542,masterkey 被多次写入 pcapng fix: pcap 过滤器未按预期工作 fix: 修复 ecapture Docker 镜像 CVE-2024-24790 fix: 处理器打印 incoming chan is full 并退出 改进 cli: 使用格式化记录器 rs/zerolog chore: 在构建过程中使用 -tags 'netgo' 以避免由于不同 Linux 发行版中的 glibc 版本不同而导致的 SIGSEGV 常量参数通知 cli: 更新 docker 使用说明 添加可能的 Linux 内核配置路径 user: 在 gotls 模块中格式化 clientRandom 字符串 cli: 支持记录器级别 使用 kprobe/__sys_connect 替代 uprobe/connect makefile: 优化内核头文件的生成机制 添加 dockerfile BPF 名称应在 _core/_noncore 后附加 user: 为面向服务的架构做准备 docs: 修复日语翻译 chore: 在 rpmBuild.spec 中移除未使用的标志 BuildRequires workflows: 更新 Linux 源 tgz 文件版本 build(deps): 将 golang.org/x/crypto 从 0.23.0 升级到 0.31.0 feat: 检测 CAP_BPF pkg: 添加 http2 请求/响应单元测试 feat: 在销毁 socket 时清理映射 user: 调整内核版本显示的时机,避免过低 感谢 感谢社区小伙伴的贡献:Leon Hwang、SenberHu、yuweizzz、sancppp、Nadeshiko Manju、行旅途、w568w 等,一起继续为爱发电。 2024-12-26 0 0
ecapture v0.7.4发布,支持Pcap FIlter包过滤语法 eCapture是什么 eCapture旁观者一个无需CA证书,无侵入的HTTPS/TLS明文抓包工具。可以在Linux 4.18以上版本使用,同时也支持Android arm64 5.5以上版本。项目在2022年4月发布,目前7900颗星。 超强特性 :Pcap Filter 包过滤 eCapture v0.7.4版本发布支持Pcap Filter Syntax,你可以像tcpdump一样使用pcap filter表达式来过滤网络包。 在tls或gotls模块中,当运行模式参数-m为pcap时,支持pcap filter表达式。在命令行最后的参数中设定,例如: sudo bin/ecapture tls -m pcap -i ens160 -w a.pcapng host 1.1.1.1 or src port 443 [sudo] password for cfc4n: tls_2024/02/25 07:23:27 ECAPTURE :: ecapture Version : linux_aarch64:0.7.3-20240222-f50b9de:[CORE] tls_2024/02/25 07:23:27 ECAPTURE :: Pid Info : 884394 tls_2024/02/25 07:23:27 ECAPTURE :: Kernel Info : 5.15.131 tls_2024/02/25 07:23:27 EBPFProbeOPENSSL module initialization tls_2024/02/25 07:23:27 ECAPTURE :: Module.Run() tls_2024/02/25 07:23:27 EBPFProbeOPENSSL: Pcapng MODEL tls_2024/02/25 07:23:27 EBPFProbeOPENSSL OpenSSL/BoringSSL version not found from shared library file, used default version:linux_default_3_0 tls_2024/02/25 07:23:27 EBPFProbeOPENSSL HOOK type: 2, binrayPath: /usr/lib/aarch64-linux-gnu/libssl.so.3 tls_2024/02/25 07:23:27 EBPFProbeOPENSSL PcapFilter: host 1.1.1.1 or src port 443 tls_2024/02/25 07:23:27 EBPFProbeOPENSSL Ifname: ens160, Ifindex: 2 tls_2024/02/25 07:23:27 EBPFProbeOPENSSL Hook masterKey function: [SSL_get_wbio SSL_in_before SSL_do_handshake] tls_2024/02/25 07:23:27 EBPFProbeOPENSSL target all process. tls_2024/02/25 07:23:27 EBPFProbeOPENSSL target all users. tls_2024/02/25 07:23:27 EBPFProbeOPENSSL: BPF bytecode filename:user/bytecode/openssl_3_0_0_kern.o tls_2024/02/25 07:23:27 EBPFProbeOPENSSL saving pcapng file: /home/cfc4n/project/ecapture/a.pcapng tls_2024/02/25 07:23:27 EBPFProbeOPENSSL: perfEventReader created. mapSize:4 MB tls_2024/02/25 07:23:27 EBPFProbeOPENSSL: perfEventReader created. mapSize:4 MB tls_2024/02/25 07:23:27 EBPFProbeOPENSSL module started successfully. tls_2024/02/25 07:23:27 ECAPTURE :: start 1 modules eCapture 指令图解 很多同学对ecapture的使用方式不了解,eCapture的module模块、mode模式两个功能点经常混淆,运行出错后,文档不看,help命令也不看。遇到问题后,直接到公众号问我,弄得我很头大。 这次也按照思维导图的方式,简单画个图解,以便更直观地熟悉eCapture。 注 在个别Android的发行版上,pcap filter过滤语法可能没有效果,未来会修复。 技术原理 本功能由Leon Hwang大神贡献,感谢他春节期间的付出。 详情见:feat: Support pcap-filter expression for pcap mode #478 libpcap 编译 pcap filter 表达式的过滤内容为cbpf字节码 cbpf字节码转为ebpf字节码 ebpf字节码插入到eCapture的TC HOOK字节码中 由内核加载全部eBPF字节码,实现表达式过滤 依赖 elibpcap cbpfc ebpfmanager v0.4.5 (eBPF bytecode InstructionPatchers) 下载 eCapture v0.7.4 下载地址 2024-02-29 0 0
eCapture旁观者:Android HTTPS明文抓包,无需CA证书 前言 Android上抓包HTTPS是不是越来越难了?高版本无法添加CA证书,抓包软件依赖太多,VPN模式、或HOOK程序时,会被APP检测到。对抗成本愈加增高。有什么万能的工具吗? 是的,eCapture for Android来了。以后在Android上抓HTTPS通讯包,再也不用安装CA证书了,再也不用下载一堆python依赖环境了,再也不用重打包ssl类库了,再也不用改一堆手机参数了,一键启用,简单明了。 eCapture简介 eCapture是一款无需CA证书即可抓获HTTPS明文的软件。支持pcapng格式,支持Wireshark直接查看。基于eBPF技术,仅需root权限,即可一键抓包。 eCapture中文名旁观者,即 当局者迷,旁观者清。 2022年年初上海疫情期间,笔者开始编写并开源,至今已经半年,GitHub上已经 4200个星星。 eCapture是基于eBPF技术实现的抓包软件,依赖系统内核是否支持eBPF。目前支持在操作系统上,支持了X86_64\ARM64的Linux kernel 4.18以上内核,支持了ARM64 Android(Linux) kernel 5.4以上版本。最新版是在2022年9月9日发布的v0.4.3版本。 演示视频 下载后,一条命令启动,干净利索。 ./ecapture tls -w ecapture.pcapng 先看演示视频,演示环境为Ubuntu 21.04、Android 5.4 (Pixel 6)。 模块功能 eCapture支持tls、bash、mysqld、postgres等模块的信息提取与捕获。本文仅讨论tls这个HTTPS/TLS明文捕获模块。 加密通讯明文捕获--tls模块 tls模块在加密通讯类库上,支持了openssl、gnutls、nspr/nss、boringssl等类库。 但在Android上,pcapng模式只支持boringssl,文本模式则都支持。 如何使用eCapture 环境依赖 操作系统 Linux kernel 4.18以上,Android kernel 5.4以上。 支持BPF,可选支持BTF(eCapture版本不同) root权限 版本选择 BPF CO-RE特性为BTF通用的格式,用于做跨内核版本兼容。有的Android手机是没开启BTF。可以查看系统配置确认,CONFIG_DEBUG_INFO_BTF=y为开启;CONFIG_DEBUG_INFO_BTF=n为关闭;其他为不支持BPF,无法使用eCapture。 cfc4n@vm-server:~$# cat /boot/config-`uname -r` | grep CONFIG_DEBUG_INFO_BTF CONFIG_DEBUG_INFO_BTF=y Android系统上,config是gzip压缩的,且配置文件目录也变了。可使用zcat /proc/config.gz命令代替。 eCapture默认发行了支持CO-RE的ELF程序。Android版会发行一个5.4内核不支持BTF(即没有CO-RE)的版本。 下载后,可以通过./ecapture -v确认。 非CO-RE版本的version信息中包含编译时的内核版本。 # no CO-RE eCapture version: linux_aarch64:0.4.2-20220906-fb34467:5.4.0-104-generic # CO-RE eCapture version: linux_aarch64:0.4.2-20220906-fb34467:[CORE] 若版本不符合自己需求,可以自行编译,步骤见文末。 全局参数介绍 全局参数重点看如下几个 root@vm-server:/home/cfc4n/# ecapture -h --hex[=false] print byte strings as hex encoded strings -l, --log-file="" -l save the packets to file -p, --pid=0 if pid is 0 then we target all pids -u, --uid=0 if uid is 0 then we target all users --hex 用于stdout输出场景,展示结果的十六进制,用于查看非ASCII字符,在内容加密、编码的场景特别有必要。 -l, --log-file= 保存结果的文件路径。 -p, --pid=0 捕获的目标进程,默认为0,则捕获所有进程。 -u, --uid=0 捕获的目标用户,默认为0,则捕获所有用户,对Android来说,是很需要的参数。 模块参数 root@vm-server:/home/cfc4n/project/ssldump# bin/ecapture tls -h OPTIONS: --curl="" curl or wget file path, use to dectet openssl.so path, default:/usr/bin/curl --firefox="" firefox file path, default: /usr/lib/firefox/firefox. --gnutls="" libgnutls.so file path, will automatically find it from curl default. --gobin="" path to binary built with Go toolchain. -h, --help[=false] help for tls -i, --ifname="" (TC Classifier) Interface name on which the probe will be attached. --libssl="" libssl.so file path, will automatically find it from curl default. --nspr="" libnspr44.so file path, will automatically find it from curl default. --port=443 port number to capture, default:443. --pthread="" libpthread.so file path, use to hook connect to capture socket FD.will automatically find it from curl. --wget="" wget file path, default: /usr/bin/wget. -w, --write="" write the raw packets to file as pcapng format. -i参数 -i参数为网卡的名字,Linux上默认为eth0,Android上默认为wlan0,你可以用这个参数自行指定。 输出模式 输出格式支持两种格式,文本跟pcapng文件。有三个参数, 默认,全局参数,输出文本结果到stdout -l 全局参数,保存文本结果的文件路径 -w 仅tls模块参数,保存pcapng结果的文件路径 类库路径 Linux上支持多种类库,不同类库的路径也不一样。 类库 参数路径 默认值 openssl/boringssl --libssl Linux自动查找,Android为/apex/com.android.conscrypt/lib64/libssl.so gnutls --gnutls Linux自动查找,Android pcapng模式暂未支持 nspr/nss --nspr Linux自动查找,Android pcapng模式暂未支持 文本模式 -l 或者不加 -w 参数将启用该模式。 支持openssl、boringssl、gnutls、nspr/nss等多种TLS加密类库。 支持DTLS、TLS1.0至TLS1.3等所有版本的加密协议。 支持-p、-u等所有全局过滤参数。 pcapng模式 -w 参数启用该模式,并用-i选择网卡名,Linux系统默认为eth0,Android系统默认为wlan0, 仅支持openssl、boringssl两个类库的数据捕获。暂不支持TLS 1.3协议。 类库与参数支持 在Linux系统上,大部分类库与参数都是可以支持的。但在Android系统上,因为内核与ARM架构的原因,支持的参数上,有一定的差异。 不同模式的参数支持 -p、-u两个全局参数,支持文本模式,不支持pcapng模式。这是因为pcapng模式是使用eBPF TC技术实现。 模式 -p -u --libsso --port 文本 ✅ ✅ ✅ ✅ pcapng ❌ ❌ ✅ ✅ 不同模式的类库以协议支持 模式 openssl(类库) boringssl(类库) TLS 1.0/1.1/1.2(协议) TLS 1.3(协议) 文本 ✅ ✅ ✅ ✅ pcapng ✅ ✅ ✅ ❌ pcapng模式暂时不支持TLS 1.3,TLS 1.3密钥捕获功能已经开发完成,只是遇到一些BUG,还在解决中。 笔者不是openssl的专家,对TLS 协议也不太熟。需要补充这两块的知识,解决起来成本比较高,也欢迎对这块擅长的朋友一起来解决。 与tcpdump联合使用 eCapture基于eBPF TC,实现了流量捕获,并保存到pcapng文件中。基于eBPF Uprobe实现了TLS Master Secret的捕获。并基于Wireshark的Decryption Secrets Block (DSB)标准,实现了gopacket的DSB功能,合并网络包与密钥,保存到pcapng中。 eCapture在网络包捕获上,没有tcpdump强大,不支持丰富的参数。你可以用eCapture捕获master secrets,用tcpdump捕获网络包,然后使用wiresahrk自定义设置密钥文件,配合使用。 网络包捕获 tcpdump 的常规用法,不再赘述。 密钥捕获 同时启用ecapture ,模式可以选文本或者pcapng,都会保存TLS的master secrets密钥数据到ecapture_masterkey.log中。 网络包查看 用Wireshark打开网络包文件,设置这个master key文件,之后就可以看到TLS解密后的明文了。 配置路径:Wireshark --> Preferences --> Protocols --> TLS --> (Pre)-Master-Secret log filename 参数 指定路径 默认路径 在Android上,Google使用了boring ssl类库,也就是C++语言在libssl基础上的包装。默认情况下,会使用/apex/com.android.conscrypt/lib64/libssl.so路径。 APP的类库确认 你可以使用lsof -p {APP PID}|grep libssl来确认。 若不是默认路径,则可以使用--libssl参数来指定。 高级用法 如果你需要查看的APP是自定义SSL类库,那么你可以自助修改eCapture来实现。 自定义函数名与offset 首先,需要确定HOOK函数的函数名或者符号表地址。 没有源码 如果你没有类库源码可以通过IDA等软件静态分析、动态调试,确定SSL Write的地址offset。在配置填写在user/module/probe_openssl.go文件中,对应的probe配置部分。 { Section: "uprobe/SSL_write", EbpfFuncName: "probe_entry_SSL_write", AttachToFuncName: "SSL_write", UprobeOffset: 0xFFFF00, // TODO BinaryPath: binaryPath, }, offset自动计算 如果你有源码,则可以自行阅读源码确定函数名或者符号表的地址。对于结构体的成员属性读取,则可以通过offsetof宏来自动计算。 // g++ -I include/ -I src/ ./src/offset.c -o off #include <stdio.h> #include <stddef.h> #include <ssl/internal.h> #include <openssl/base.h> #include <openssl/crypto.h> #define SSL_STRUCT_OFFSETS \ X(ssl_st, session) \ X(ssl_st, s3) \ X(ssl_session_st, secret) \ X(ssl_session_st, secret_length) \ X(bssl::SSL3_STATE, client_random) \ X(bssl::SSL_HANDSHAKE, new_session) \ X(bssl::SSL_HANDSHAKE, early_session) \ X(bssl::SSL3_STATE, hs) \ X(bssl::SSL3_STATE, established_session) \ X(bssl::SSL_HANDSHAKE, expected_client_finished_) struct offset_test { /* data */ int t1; bssl::UniquePtr<SSL_SESSION> session; }; int main() { printf("typedef struct ssl_offsets { // DEF \n"); #define X(struct_name, field_name) \ printf(" int " #struct_name "_" #field_name "; // DEF\n"); SSL_STRUCT_OFFSETS #undef X printf("} ssl_offsets; // DEF\n\n"); printf("/* %s */\nssl_offsets openssl_offset_%d = { \n", OPENSSL_VERSION_TEXT, OPENSSL_VERSION_NUMBER); #define X(struct_name, field_name) \ printf(" ." #struct_name "_" #field_name " = %ld,\n", \ offsetof(struct struct_name, field_name)); SSL_STRUCT_OFFSETS #undef X printf("};\n"); return 0; } 参数提取 对于参数,你需要确认被HOOK函数的参数类型,以便确认读取方式,可以参考kern/openssl_kern.c内的SSL_write函数实现。 编译 ARM Linux 编译 公有云厂商大部分都提供了ARM64 CPU服务器,笔者选择了腾讯云的。在广州六区中,名字叫标准型SR1(SR1即ARM 64CPU),最低配的SR1.MEDIUM2 2核2G即满足编译环境。可以按照按量计费方式购买,随时释放,比较划算。 操作系统选择ubuntu 20.04 arm64。 ubuntu@VM-0-5-ubuntu:~$sudo apt-get update ubuntu@VM-0-5-ubuntu:~$sudo apt-get install --yes wget git build-essential pkgconf libelf-dev llvm-12 clang-12 linux-tools-generic linux-tools-common ubuntu@VM-0-5-ubuntu:~$wget https://golang.google.cn/dl/go1.18.linux-arm64.tar.gz ubuntu@VM-0-5-ubuntu:~$sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.18.linux-arm64.tar.gz ubuntu@VM-0-5-ubuntu:~$for tool in "clang" "llc" "llvm-strip" do sudo rm -f /usr/bin/$tool sudo ln -s /usr/bin/$tool-12 /usr/bin/$tool done ubuntu@VM-0-5-ubuntu:~$export GOPROXY=https://goproxy.cn ubuntu@VM-0-5-ubuntu:~$export PATH=$PATH:/usr/local/go/bin 编译方法 ANDROID=1 make 命令编译支持core版本的二进制程序。 ANDROID=1 make nocore命令编译仅支持当前内核版本的二进制程序。 当Wireshark被eBPF增强 Wireshark + eBPF = eCapture 贡献者 感谢chriskaliX 、chenhengqi、vincentmli、huzai9527、yihong0618、sfx(4ft35t)等朋友的贡献。 感谢tiann Weishu在Android这个需求上的推动。 招聘 Leader直招,没有中间商赚差价。面向RASP、HIDS产品,JVM、Linux内核入侵检测等安全研发职位 JAVA资深研发工程师 golang高级工程师 更多职位见 https://www.cnxct.com/jobs/ 2022-09-13 4 0
eCapture:无需CA证书抓https网络明文通讯 eCapture介绍 eCapture是一款基于eBPF技术实现的用户态数据捕获工具。不需要CA证书,即可捕获https/tls的通讯明文。 项目在2022年3月中旬创建,一经发布,广受大家喜爱,至今不到两周已经1200多个Star。 作用 不需要CA证书,即可捕获HTTPS/TLS通信数据的明文。 在bash审计场景,可以捕获bash命令。 数据库审计场景,可以捕获mysqld/mariadDB的SQL查询。 官网 代码仓库见:https://github.com/ehids/ecapture ,也可以关注微信公众号「榫卯江湖」获取最新动态。 产品架构 eCapture系统用户态程序使用Golang语言开发,具有良好的系统兼容性,无依赖快速部署,更适合云原生场景。 内核态代码使用C编写,使用clang/llvm编译,生产bpf字节码后,采用go-bindata转化为golang语法文件,之后采用ehids/ebpfmanager类库,调用bpf syscall进行加载、HOOK、map读取。 golang编译后,无其他任何依赖即可运行,兼容linux kernel 4.18以上所有版本。 eBPF加载机制 关于eBPF详细加载机制,可到https://ebpf.io/ 查阅相关原理。 实现原理 如工作原理的图所示,在用户态的加密解密函数中下钩子。 tcpdump(libpcap)是在数据包接收到,XDP处理后,进行clone packet,进行包的复制,发送给用户态进程。二者工作的所在层不一样。 功能介绍 eCapture有三个模块 tls/ssl明文数据捕获 bash命令审计 mysqld数据库审计 第一个功能适用于基于tls/ssl解密需求的运维监控、故障排查、抽样分析场景。 第二个功能适用于安全领域的bash入侵发现场景,这里只是简单的功能,可以在此基础上增加其他功能。 第三个功能适用于数据库审计场景,尤其是做数据安全、数据防泄漏,甚至入侵检测等。同样,可以在此基础上扩充其他功能。 查看其使用说明,如下 cfc4n@vm-desktop:~/ehids/ecapture$ ./bin/ecapture NAME: ecapture - capture text SSL content without CA cert by ebpf hook. USAGE: ecapture [flags] VERSION: 0.1.5-20220325-47edbed COMMANDS: bash capture bash command help Help about any command mysqld56 capture sql queries from mysqld >5.6 . tls alias name:openssl , use to capture tls/ssl text content without CA cert. DESCRIPTION: ecapture是一款无需安装CA证书,即可抓去HTTPS、TLS等明文数据包的工具。 也可以捕获bash的命令,适用于安全审计场景。包括mysqld的数据库审计等。 仓库地址: https://github.com/ehids/ecapture OPTIONS: --debug[=false] enable debug logging -h, --help[=false] help for ecapture --hex[=false] print byte strings as hex encoded strings -p, --pid=0 if target_pid is 0 then we target all pids 其中,有四个全局参数,分别是 --debug , 用于启动调试日志 --help , 查看帮助 --hex ,按照hex模式打印字符,用与查看不可见字符 --pid ,用于针对特定进程进行数据捕获 HOOK机制 eCapture采用eBPF uprobe相关函数进行HOOK,故需要目标用户态函数信息,包含函数符号表(symbol table),函数偏移地址(offset)。 在大部分linux发行版中,使用的二进制可执行文件(ELF)都是包含符号表的;少部分发行版,会去掉ELF中的符号表。那么针对这种场景,就需要用户自行定位目标函数所在ELF/SO中的偏移地址,通过工具的参数来指定。 对于ELF文件,可以将目标类库静态编译到自身,也可以通过动态链接库的方式引用。那么对于这两种形式,eCapture根据不同场景进行自动查找。若查找不到,用户可以通过命令行参数指定。 故eCapture支持HOOK ELF,以及HOOK SO两种模式。会自动分析ELF文件,读取.dynamic和.dynsym等段信息,查找相关链接库名以及函数名、偏移地址。 查找原理如下图: tls/ssl ecapture tls命令用于启动tls/ssl模块,支持了三类tls/ssl加密类库,分别是 openssl ,动态链接库名字为libssl.so gnutls ,动态链接库名字为libgnutls.so nss/nspr ,动态链接库名字为libnspr4.so 在不同的linux发行版中,因为各种原因,会选择不同的类库。比如wget程序,在ubuntu跟centos中就会使用不同的类库。有的是openssl,有的是gnutls,甚至两个库都引入了。 具体情况,你可以使用ldd $ELF_PATH | grep -E "tls|ssl|nspr|nss"来查看一个ELF文件使用类库情况。 cfc4n@vm-desktop:~$ ldd `which wget` |grep -E "tls|ssl|nspr|nss" libssl.so.1.1 => /lib/x86_64-linux-gnu/libssl.so.1.1 (0x00007f50699f6000) 对于firefox、chrome这种进程,需要在程序启动后才能看到tls类库依赖情况,那么,你可以使用sudo pldd $PID | grep -E "tls|ssl|nspr|nss" 来查看 cfc4n@vm-desktop:~$ ps -ef|grep firefox cfc4n 6846 1432 45 17:50 ? 00:00:04 /usr/lib/firefox/firefox -new-window cfc4n@vm-desktop:~$ sudo pldd 6846 |grep -E "tls|ssl|nspr|nss" /usr/lib/firefox/libnspr4.so /usr/lib/firefox/libnssutil3.so /usr/lib/firefox/libnss3.so /usr/lib/firefox/libssl3.so /lib/x86_64-linux-gnu/libnss_files.so.2 /lib/x86_64-linux-gnu/libnss_mdns4_minimal.so.2 /lib/x86_64-linux-gnu/libnss_dns.so.2 /usr/lib/firefox/libnssckbi.so eCapture的tls模块命令行参数如下,用户可以使用默认配置外,也可以根据自己环境自行指定。 OPTIONS: --curl="" curl or wget file path, use to dectet openssl.so path, default:/usr/bin/curl --firefox="" firefox file path, default: /usr/lib/firefox/firefox. --gnutls="" libgnutls.so file path, will automatically find it from curl default. -h, --help[=false] help for tls --libssl="" libssl.so file path, will automatically find it from curl default. --nspr="" libnspr44.so file path, will automatically find it from curl default. --wget="" wget file path, default: /usr/bin/wget. 同时,使用方法也比较简单,./ecapture tls --hex命令即可。 在linux上,firefox程序中,有很多通讯都使用了/usr/lib/firefox/libnspr4.so,但实际上业务请求是可以通过Socket Thread进程来发送的。可以通过这个特点来过滤,对于chrome程序,相信细心的你,也能搞定。 bash 笔者在安全部门工作,接到过bash审计需求,其实现方法无非是修改系统类库、使用内核模块等技术实现,对系统稳定性有一定风险。基于eBPF技术实现,可以避开这些问题。这里的bash命令的监控,是作为eBPF技术在安全审计场景中的一个探索。 eCapture在实现时首先查找ENV的$SHELL值,作为bash的二进制文件路径进行HOOK。对于bash加载了libreadline.so的场景,也会自动分析,进行符号表查找、offset定位,再进行HOOK。 bash模块的参数有三个,用户可以自定义bash、readlineso的路径。 OPTIONS: --bash="" $SHELL file path, eg: /bin/bash , will automatically find it from $ENV default. -h, --help[=false] help for bash --readlineso="" readline.so file path, will automatically find it from $BASH_PATH default. mysql/mariadb 与bash模块一样,也是作为数据库审计的一个探索。笔者环境为ubuntu 21.04,mysqld也因为协议关系,使用了衍生的MariadDB,用户也可以根据自己实际场景,使用命令行参数进行指定。 mysqld模块,核心原理是HOOK了dispatch_command函数, 第一个参数为CMD类型,值为COM_QUERY时,为查询场景,即审计需求的查询类型。 第二个参数是THD的结构体,在这里我们用不到。 第三个是查询的SQL语句 第四个参数是SQL语句的长度, // https://github.com/MariaDB/server/blob/b5852ffbeebc3000982988383daeefb0549e058a/sql/sql_parse.h#L112 dispatch_command_return dispatch_command(enum enum_server_command command, THD *thd, char* packet, uint packet_length, bool blocking = true); mysqld审计模块参数如下: OPTIONS: -f, --funcname="" function name to hook -h, --help[=false] help for mysqld56 -m, --mysqld="/usr/sbin/mariadbd" mysqld binary file path, use to hook --offset=0 0x710410 其中,--mysqld是用来指定mysqld的路径。 mysqld二进制程序符号表里虽然有dispatch_command信息,但dispatch_command这个函数名每次编译都是变化的,故不能写死。 eCapture的查找方式是读取mysqld二进制的.dynamic段信息,正则语法\w+dispatch_command\w+去匹配所有符号信息,找到其函数名、偏移地址,再使用。 你也可以通过objdump命令来查找,再通过命令行参数自行指定funcname。 mariadbd version : 10.5.13-MariaDB-0ubuntu0.21.04.1 objdump -T /usr/sbin/mariadbd |grep dispatch_command 0000000000710410 g DF .text 0000000000002f35 Base _Z16dispatch_command19enum_server_commandP3THDPcjbb 即offset为0x710410,函数名为_Z16dispatch_command19enum_server_commandP3THDPcjbb。 使用 下载二进制包 eCapture发布在https://github.com/ehids/ecapture/releases ,目前最新版为eCapture v0.1.5。 可在linux kernel 4.18以上版本运行。 二进制包地址: https://github.com/ehids/ecapture/releases/download/v0.1.5/ecapture_v0.1.5.zip 国内加速地址: https://github.do/https://github.com/ehids/ecapture/releases/download/v0.1.5/ecapture_v0.1.5.zip 自行编译 代码仓库在https://github.com/ehids/ecapture ,你可以自行修改源码编译。 演示视频 腾讯视频: B站: https://www.bilibili.com/video/BV1si4y1Q74a Github仓库 2022-03-27 +3 7 0
etcd clientv3的lease keepalive租约频繁续期bug 排查小记 背景: etcd 3.2.23 golang 1.9.1 centos 6 etcd cluster集群节点7台,节点member之间 https 通讯,与 client 之间也是 https 通讯 测试环境 2W 台主机,每台都要安装这个 agent,每个 agent注册一个包含lease的key,key的 ttl 设置为10s 设置keepalive 产品等着发布上线,持续遇到集群性能问题,长时间找不到原因,感觉我要崩溃了 集群性能问题不解决,线上更多的服务器更是没法部署,领导不认可,不满意,我就要被开除了,我就要辞职了。。。 Etcd github官方提了issue,官方没时间帮我们看(理解官方的苦衷,issue 太多,很多都是使用者的问题)... 问题现象: 带有lease id的 key 无故丢失 leader节点 大部分时间 TIME-WAIT数量非常高 leader 节点的 CLOSE-WAIT 有时也非常高 leader 的 CPU 使用率持续在80-90%的样子 leader 节点的内存使用率持续上升,疑似内存泄露的症状 监控图如下: etcd leader的 etcd 进程入手: 先分析「疑似内存溢出」问题,在 etcd 里,已经有pprof的信息,golang的tool里也有直接读取 http pprof 性能分析的工具,只是不支持 https 的。需要将 pprof 文件下载到本地,再从本地读取 结果如下: 可以看到大量 math 相关的包函数占用了大量的 cpu,而我们程序的业务中,并不涉及数学计算相关业务,也不会引用 math 包。唯一可能性就是在 https 的 tls 证书在做 RSA 算法校验时,用到了大量数学计算。我把 pprof 导出问 svg 图确认了一下,确实是 tls 的证书计算占用了大量 CPU。 但是 https 的链接,不应该是建立1次后就不再建立了吗?为什么会有这么大量的 cpu 计算呢。难道 TIMEWAIT 这么高跟这个有关系吗?回到 TIMEWAIT 的问题上来,此刻,leader 节点是 xxxx-etcdserver-xxx02,且TIME_WAIT非常高 leader 节点上的time-wait来源,都是来自各个member节点,平均每个节点将近几万个 TIMEWAIT,相隔几秒再次查询,还是5万多个。而各个 member 节点上,查到的是跟 leader 节点的2380端口 保持1千多个 ESTABLISHED 状态的连接,也确实是频繁的创建链接。为什么member节点频繁的创建链接呢?tcpdump抓点TCP 通讯包看一下: 如上抓包的图,发现了什么? 120140 2018-06-22 20:37:30.219 10.72.198.234 74 10.20.127.136 TCP 12902 → etcd-server(2380) [SYN] Seq=0 Win=14600 Len=0 MSS=1460 SACK_PERM=1 TSval=3008487334 TSecr=0 WS=128 120141 2018-06-22 20:37:30.219 10.20.127.136 74 10.72.198.234 TCP etcd-server(2380) → 12902 [SYN, ACK] Seq=0 Ack=1 Win=14480 Len=0 MSS=1460 SACK_PERM=1 TSval=2796204056 TSecr=3008487334 WS=128 120362 2018-06-22 20:37:30.242 10.72.198.234 66 10.20.127.136 TCP 12902 → etcd-server(2380) [ACK] Seq=1 Ack=1 Win=14720 Len=0 TSval=3008487358 TSecr=2796204056 120368 2018-06-22 20:37:30.242 10.72.198.234 204 10.20.127.136 TCP 12902 → etcd-server(2380) [PSH, ACK] Seq=1 Ack=1 Win=14720 Len=138 TSval=3008487358 TSecr=2796204056 120369 2018-06-22 20:37:30.242 10.20.127.136 66 10.72.198.234 TCP etcd-server(2380) → 12902 [ACK] Seq=1 Ack=139 Win=15616 Len=0 TSval=2796204080 TSecr=3008487358 174076 2018-06-22 20:37:40.242 10.72.198.234 66 10.20.127.136 TCP 12902 → etcd-server(2380) [FIN, ACK] Seq=139 Ack=1 Win=14720 Len=0 TSval=3008497358 TSecr=2796204080 174377 2018-06-22 20:37:40.282 10.20.127.136 66 10.72.198.234 TCP etcd-server(2380) → 12902 [ACK] Seq=1 Ack=140 Win=15616 Len=0 TSval=2796214120 TSecr=3008497358 从这些数据包中,你能看出什么问题?想一想,打开这张图,想5分钟再往下看。。。 120140是10.72.198.234 的 member 节点作为客户端,向10.20.127.136的2380的 leader 节点发起TCP 连接请求。 120141是 leader 回的握手确认包。120362是 member 发的 ACK 握手完成确认包。这些都很简单明了。 120368是 member 向 leader 发的数据包,120369是 leader 回 member 收到前面数据包的 ack。嗯,这些也很好明白。 174076是什么? member向 leader 发的FIN 关闭 TCP 连接的数据包,为什么?为什么member要关闭连接? 再抽查几个数据包看看,从 “专家信息”里预览的1000多个“SYN”里找.... 可以看出,都是 member主动向 leader 发 FIN 关闭包,这是为什么呢? 仅仅是看出来这些吗? 有没有注意到 member 发的消息包,收到 leader 回的 ack 之后,距离 member 主动发的 FIN 之间的时间间隔?都是10s ?为什么是10s? leader 回了 ack,但没主动向 member 发“FIN,ACK”,为什么没回呢?若没回,那么 leader 上的这个 TCP 五元组的状态是什么?是的,是“CLOSE-WAIT”,难道这就是造成leader上大量 CLOSE-WAIT 的原因?先怀疑一下,继续往下看... 如上所说,member 向 leader 发了一个PSH数据包,发来什么内容呢?为什么 leader 没回信息? payload的总长度是138,完全看不出来是什么数据包?不,应该还是有眼熟的,看前3个字节,0x16,0x03,0x01,如果对 HTTPS 熟悉的话,会联想到这是 TLS 的magic number,0x16对应十进制的22 即“Client Hello 的 Handshake”,后面的0x31,0x01是 TLS 的版本号。再配合我们的 member 跟 leader 之间确实是 https 通讯,可以确定这个地方是 tls 的client Hello握手包。也就是说配合前面发现的 10s 的间隔,可以看出 member 与 leader 之间的 TLS 握手并未完成,leader 并没有回 Server Hello... 整个TCP 包比较短,没有发现其他有用的信息... 去掉 HTTPS,减轻 cpu 负担: 回到之前的 CPU pprof 情况来看,是 TLS 相关证书计算,导致 CPU 使用率较高,但大量的 TCP 连接的发起,才是导致大量 TLS 计算的原因,导致内存飙升,消息积压的原因。不过,我们还是尝试去掉 member 与 leader 时间的 tls 证书校验,尝试减轻 leader 的CPU 使用。但结果影响不大,CPU 使用率依旧很高,不过,这也在意料之中,毕竟不是问题根本原因,查看pprof 如下 CPU 使用率较高的,是 runtime.futex 跟系统调用的 syscall.Syscall,以及runtime下的包,heapBitsForObject、scanobject、greyobject是 golang GC 时的运行时函数,意味着GC 已经占用很大的 CPU 了。 配合memory pprof来看下 如上,可以看出,大约65%的内存都用在了recvBuffer.put 这里的 put 方法是将收到的recvMsg写入到b.backlog这个 slice 中的,func (b recvBuffer) put(r recvMsg)跟 (b recvBuffer) load()两个函数都对b.backlog有读写操作,也就是说,这个slice 写入、读取时,都会进行加锁,严重占用 CPU 以及影响性能,增大系统负载。而且若 (b recvBuffer) load()处理速度跟不上func (b recvBuffer) put(r recvMsg)写入速度,意味着这个slice会持续增长,造成看似内存溢出的现象。。。 那问题来了,func (b recvBuffer) put(r recvMsg)为什么有这么大的调用量,为什么在频繁写入...为什么呢??? 定位到内存溢出的疑似原因: grpc/server.go func (s Server) serveStreams(st transport.ServerTransport) grpc/transport/handler_server.go 文件284行 func (ht serverHandlerTransport) HandleStreams 函数中的334行 然后再走到了,func (b *recvBuffer) put(r recvMsg)函数里,也就是说,这里的消息slice堆积,是因为接收到了大量http request。那接下来就是要找到这些http request 是来自谁了? 阅读 etcd 源码: 配合 member 之间大量的 TCP TIME-WAIT 状态链接,排查方向改到 member 节点上。 要查找 etcd member 建立 TCP/HTTP 链接的地方, 在源码里,还是非常多的,一时间没找到更好的bug 定位方式,只要从etcd 源码去逐步分析排查。 etcdmain/main.go 39行 startEtcdOrProxyV2() etcdmain/etcd.go 103行 startEtcd() etcdmain/etcd.go 186行 embed.StartEtcd(cfg) embed/etcd.go 107行 startPeerListeners() ,用 rafthttp.NewListener 创建了一个Listener。用于 etcd member 之间通讯。依赖github.con/coreos/etcd/pkg/transport包 embed/etcd.go 110行 startClientListeners() 用 net.Listen 创建了一个Listener。用于 client 跟 member 之间通讯。keepalive 使用github.con/coreos/etcd/pkg/transport包 157行 etcdserver.NewServer() 对本地配置读取,数据库目录读取,设置mvcc多版本控制函数,设置 lease 租约续期函数等等 166行 e.servePeers() 进行对 Linster 启动 Serve 接受请求 317行 etcdhttp.NewPeerHandler(e.Server) 设置HTTP Handler,里面函数如下: 如上,etcd 节点之间的通讯,设定了这几个Handler "/" --> http.NotFound "/raft"、"/raft/" --> raftHandler "/members" --> mh "/leases/internal" 、 "/leases" --> raftHandler "/version" --> versionHandler 也就是说,member 之间通讯的大量通讯只会是这些 handler 中间的一个,除非源码找漏了。。。 从这个开源产品功能特性上来看,member 之间需要大量同步的,应该就是数据了,应该把线索放到raftHandler上吗?头大.... raftHandler是s.RaftHandler() 返回的, 值是 EtcdServer.r.transport.Handler()返回值。 EtcdServer.r是在etcdserver/server.go的414行赋值的 etcdserver/server.go 的511行 EtcdServer.r.transport = tr进行赋值,类型为rafthttp.Transport ,其Handler 如下代码: 这里又有几个 Handler ,而且都是 raft 处理相关的,代码读起来比较吃力,很难理解作者意图, 打印http.dial堆栈: 要想从这里查找 TCP 连接的发起代码,恐怕是难上加难,所以,我换了个思路,直接在 TCP 连接 Dail 的地方,判断 addr 是 etcd leader 的地址,打印调用栈。使用 runtime/debug下的debug.PrintStack()函数输出。同时打印了连接完成后的remote.addr,用于校对。不过,这改动是 golang 的系统 runtime 类库,(改了系统库,记得改回来)。要小心谨慎,在http/transport.go 882行 增加如下 重新编译 etcd server,替换到集群中的某一个 member 节点上。 开始压测这个节点,重现大量 TIME-WAIT 的现象。现象重现后,在日志里也拿到了运行堆栈 在压测时,我用 netstat 抽样打印几次当前 member 上的 TCP 连接信息数据,也是用于校对日志输出的数据。 在压测的日志数据里,每建立一个 TCP 连接,都打印了这些堆栈,并且打印了远程 IP 的地址、端口。跟前面抽样打印的 TCP 数据核对一下,发现连接 leader 2380端口的几个 TCP 连接五元组跟 上面打印的remote.addr的五元组一致。 也就是说,是这个函数创建了大量的从 member 到 leader 之间的 TCP 连接。好了,如果找到大量 TCP 连接发起的函数了,又找到了该函数的调用栈了,那岂不是找到问题了? 然而,事情并没有这么简单,在上面的调用栈日志里,只能定位到coreos/etcd/pkg/transport.(*rwTimeoutDialer).Dial,并不能打印出所有调用堆栈,为什么? 找到 etcd member 10s 后主动断开 TCP 连接的原因: 在系统类库里的net/http/transport.go的Transport.dial方法中,如果该Transport自定义了Dial方法,则调用自定义的...这个自定义的Dial方法是在github.com/coreos/etcd/pkg/transport/transport.go里定义的,如下: 在代码dial := func(net, addr string) (net.Conn, error) ... 这里,自定义了一个 Dial 方法,赋值到http.Transport.Dial 函数属性上,并且设定TLS 的超时时间是 TLSHandshakeTimeout: 10 time.Second,这也就是之前抓包时,看到的10s 后,member 节点主动断开连接的原因----超过 TLS 设定的timeout时间了。 函数NewTransport的调用地方,只在NewTimeoutTransport这里找到,如下: 也就是说,找到NewTimeoutTransport调用着,就能确定发起大量 TCP 的代码...但之前的堆栈打印,并没有打印到这些函数调用,只好再想其他办法。 在NewTimeoutTransport的代码里,有一句对 HTTP Keepalive的最大持有空闲长连接数字的设置tr.MaxIdleConnsPerHost = 1024,这个参数决定 golang 的 http client 包对 server 发起 http 请求时,能持有的最大空闲 TCP 连接数量,在 etcd 的代码里,被设置为1024了,也就是说,etcd member 跟 etcd leader 之间空闲时,持有TCP 连接数可以是1024个,便于下次复用。(插一句,有人说,http 是最好的 RPC,这里我是不认同的,光从TCP 连接复用上,就有非常大的差异,在http 1.1 TCP 链接 「另类」复用,http 1.1 的keepalive属性也是仅当前一个请求响应后,这个链接才能继续复用,请求响应串行化。而在 http2里,是不需要等待上一个请求返回的,才有真正意义上的多路复用见:Streams and Multiplexing , 如果你做过 rpc 的功能实现,你就知道 rpc 在 tcp 上是如何实现请求响应的一一对应的,你就能很快明白http2\http1.1的 TCP 链接复用的区别了。) 排查 etcd member 跟 etcd leader 之间的通讯: 回到正题,对于代码这里的定位,我尝试去阅读代码来协助定位,但 etcd 的源码太复杂了,我读到 raft 协议节点之间数据同步这块,卡住了。开始从其他路子尝试解决,之前的 cpu 占用较高问题,我们在 member 节点之间去掉了 tls 证书验证,那么我们在 member 上做 tcp 抓包,抓2380端口数据包,看看数据发送的都是什么内容。 数据包中,可以看出 member 向 leader 上发送了大量的 /leases的 POST 请求。根据这个 uri,找到发起 HTTP 请求的代码,在etcdserver/v3_server.go的252行 阅读这段代码,可以看到函数调用leasehttp.RenewHTTP将请求发送到leader.PeerURLs,即发送到 leader 节点上。继续跟进leasehttp.RenewHTTP, 可以看出,该请求使用 golang 标准包的http.Client.Do来将请求发送出去。http.Client.Transport使用参数传入的rt http.RoundTripper。 定位到10s TLS timeout 的相关函数调用: 回到上一层代码 s.peerRt参数来自*EtcdServer.peerRt,在etcdserver/server.go的429行被赋值peerRt: prt prt变量在该文件的288行被赋值prt, err := rafthttp.NewRoundTripper(cfg.PeerTLSInfo, cfg.peerDialTimeout()) rafthttp.NewRoundTripper函数调用rafthttp.NewTimeoutTransport 没错,rafthttp.NewTimeoutTransport调用了rafthttp.NewTransport函数,也就是之前堆栈打印时,发现大量 TCP 链接创建使用的这个函数,也就是10s TLS timeout 的地方。 这里可以关联上了,确认大量发起 TCP 连接的代码调用关系,证明方向正确,剩下的就是顺藤摸瓜,找到发起大量请求的原因即可。 回到LeaseRenew函数调用的地方,调用func (s *EtcdServer) LeaseRenew函数代码见github.com/coreos/etcd/etcdserver/api/v3rpc/lease.go的88行附近 71行 func (ls *LeaseServer) LeaseKeepAlive函数调用func (ls *LeaseServer) leaseKeepAlive() etcdserver/etcdserverpb/rpc.pb.go的3417行func _Lease_LeaseKeepAlive_Handler(srv interface{}, stream grpc.ServerStream)调用了func (ls *LeaseServer) LeaseKeepAlive函数 etcdserver/etcdserverpb/rpc.pb.go的3480行将函数_Lease_LeaseKeepAlive_Handler设置为 StreamName: "LeaseKeepAlive"的 Handler,在通过*grpc.Server的RegisterService方法,在启动时,注册到监听器中 leaseKeepAlive函数是一个事件循环,从stream.Recv()拿到数据,则进行发送到 leader 节点上 stream.Recv()函数是google.golang.org/grpc/stream.go中的函数,这个不是 etcd 官方自己的代码,而是另外一个开源项目,意味着我还需要再把这块代码再阅读阅读,理解理解。 通过漫长的阅读,可以看到这里收到的消息都是来自 client 发送来的消息,其实可以从变量命名上看到端倪,比如 client 跟 member 之间通讯处理的变量都包含stream字样,而member到 leader 或 member 节点之间都带有peer字样,其实,client 跟 etcd 节点之间通讯用的是 http2传输协议,消息编码用的是grpc+protobuf;而etcd member节点之间是http1.1传输,消息编码也是 protobuf。 etcd lease 租约续期的时机: 就是说,这里的大量lease 租约续期的请求,来自 client 的请求,在我的案例中,我把主机 lease 的 TTL 设置为10S,这块会有异常吗?client 的 lease 续期是谁来发送?client 自己吗?多久发送一次?过期前的1秒? 在 etcd 的 client 类库中,对于租约续期的代码在clientv3/lease.go的474行 nextKeepAlive变量是 lease TTL 的三分之一,用于下次续期时的触发时刻,在后面的sendKeepAliveLoop函数续期时,回每隔500ms 进行一次循环,检测是否续期。也就是说,我们场景下,10s 的 TTL 会每隔3S 一次续期。3W 客户端的话,每秒1W 的写请求,而之前我们压测结果是集群节点可以有4W 以上的 (官方压测数据)qps,不至于这点都撑不住。这是个疑点。不过,前面的分析判断,大量的续约续期导致大量 TCP 链接的建立,大量 TCP 链接的建立,导致 leader 节点上大量对象分配,大量内存申请,golang gc 时,消耗大量 CPU,那这个问题也应该要优化一下,为此,我们把 TTL 设定为600S,即10分钟,也就是说,会在200秒后进行一次续期。 扩大lease TTL,尝试优化集群负载: 修复后的版本,我们在线下发布了6000多台,观察效果,果然,在发布后,内存有明显的下降,CPU 指标也有,但是在1个小时后,指标又上升了。。。 问题依旧: 这是为什么呢?中间大约1个小时的负载指标下降是因为发布过程带来的假象吗?上次变更的 lease 有效果吗?可我更改时,是测试过的,测试报告都有呢----agent 的 etcd lease租约时长 调整 ,TTL 续期是200S 一次了,但这是为什么?为了确认这个问题,我在其中一台 member 上抓包确认 上面确实是大量的 lease 包,那么每个 lease id 之间的发送间隔是多少?是我们预期的200秒吗?或者这些 lease 包是同一个 lease id 吗?一共有多少个 lease id 呢? 分析 tcpdump 的pcapng文件: 之前从 etcd 的源码分析可以看出,etcd node 节点之间的通讯消息编码也是 protobuf 的,我们需要把 http response body 的字节流复制出来,用 protobuf 的对应结果体解码即可。可是从 2018-07-05 16:38:31.015981 到 2018-07-05 16:43:20.420401 之间,间隔5分钟之间,一共103万个数据包,手动复制,实在没法统计。借用pcap包,对整个 tcpdump抓包的pcapng文件,分析, 过滤响应类型是application/protobuf的,取 http response body内容,并对其做 pb.LeaseKeepAliveRequest protobuf的结构体解析,计算 lease 总数与唯一数 运行结果: lease id 总数数: 77550, lease id 唯一数:1382。 这些结果说明,相同的 lease id 在频繁发送续约请求。再次写程序,把这些 lease id 取出来,到 etcd 集群上查看 TTL 剩余时间是否是预期的400-600秒之间,但结果却总是599秒,以其中一个 lease id 为例,每隔1秒去抓取 TTL 信息,但每次结果 TTL 都是599秒, grantTTL 是600,意味着 etcd member 持续不断的为这些 lease id 不停的续期。 我们之前排查结果是, member 的续期是来自 client 的 lease keepalive 请求,那么方向得放到 client 上。 抽查其中一台 client,抓包分析,也确实是在频繁发送/etcdserverpb.Lease/LeaseKeepAlive 请求,这就很奇怪,跟之前我的测试结果完全不一样,明明测试时,是每个200S 一起续期请求,为什么这里变得这么频繁,每秒都发起。 回到 etcd clientv3的代码上: 现象很奇怪,排查这个问题,只能再次从源码层面入手,方向也放到 /etcdserverpb.Lease/LeaseKeepAlive的地方,函数sendKeepAliveLoop这里开始循环处理判断是否需要续期租约 如上代码,每隔 500毫秒一次循环,遍历l.keepAlives map,判断每个LeaseID的nextKeepAlive是否到了,client 决定是否发起LeaseKeepAliveRequest。这个nextKeepAlive的赋值是在clientv3/lease.go的recvKeepAlive函数内,第473行 是的,这块代码之前看过,之前关注的只是 TTL 时间的设定,但仔细看看这段代码ka.nextKeepAlive = nextKeepAlive,这里做下一次lease 续期的赋值,判断方法是select case,判断条件是ch <- karesp,在 golang 里,这种写法表示该channle能写入的话,则进入case里面的流程,对ka.nextKeepAlive进行赋值;否则不赋值。。。否则,不赋值。。。否则,不赋值。。。 那么,这么 channel 长度多少呢?会不会写入的速度大于读取导致没写进去,导致下一次的续期时间没赋值成功呢?继续追查,在clientv3/lease.go的222行KeepAlive函数里 ch := make(chan *LeaseKeepAliveResponse, leaseResponseChSize)进行chan 的申请,长度leaseResponseChSize是常量,值是16。chs是个slice,长度1,只有这一个。那么,接下来,去确认这个 ch 读取的代码吧,可是我读完整个 lease.go也没找到哪里读取这个 channel 的代码,除了几处 close 的回收操作。那问题来了,会不会是这里把 ch channel 写满了,就无法再写入进去,导致ka.nextKeepAlive = nextKeepAlive流程走不到,导致ka.nextKeepAlive一直是上次的过去的续期时间,导致频繁进行续期呢? 找到疑点,每秒2次 lease keepalive 的请求: 开始测试,在 ch channel 写入的 default 分支里,打印“无法写入”等字样,并在for 后面打印这个ka的 TTL 等信息,用于核对。log.Println(fmt.Sprintf("karesp.TTL:%d, nextKeepAlive:%s,ka.nextKeepAlive:%s, ka.deadline:%s",karesp.TTL, nextKeepAlive.String(),ka.nextKeepAlive.String(), ka.deadline.String())) 改动后,重新编译代码,上传至某台 client 上 ,关掉 DEBUG 模式,观察日志输出。 文件见 /xxxx/xxx/core/nohup.out.1 ,在 2018/07/05 21:10:42 启动程序,经过1个多小时的观察,果然在2018/07/05 22:04:05时,lease 续约续期的请求从200秒变成了每秒2次。同时到 etcd 集群上查询响应 lease id,发现 TTL 均为599,grantTTL 为600,则可说明 是客户端在频繁重新发送租约请求。 第一个版本没打印ka.nextKeepAlive ,调整后,重新打印,为了让等待时间变短些,我把600s 改成60,便于观察。得到如下日志 2018/07/05 22:40:02 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:40:02 karesp.TTL:60, nextKeepAlive:2018-07-05 22:40:22.811626819 +0800 CST m=+40.135091508,ka.nextKeepAlive:2018-07-05 22:40:22.811626819 +0800 CST m=+40.135091508, ka.deadline:2018-07-05 22:41:02.811627601 +0800 CST m=+80.135092217 2018/07/05 22:40:22 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:40:22 karesp.TTL:60, nextKeepAlive:2018-07-05 22:40:42.922698966 +0800 CST m=+60.246163715,ka.nextKeepAlive:2018-07-05 22:40:42.922698966 +0800 CST m=+60.246163715, ka.deadline:2018-07-05 22:41:22.922699917 +0800 CST m=+100.246164470 2018/07/05 22:40:43 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:40:43 karesp.TTL:60, nextKeepAlive:2018-07-05 22:41:03.330494852 +0800 CST m=+80.653959932,ka.nextKeepAlive:2018-07-05 22:41:03.330494852 +0800 CST m=+80.653959932, ka.deadline:2018-07-05 22:41:43.330496602 +0800 CST m=+120.653961489 2018/07/05 22:41:03 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:41:03 karesp.TTL:60, nextKeepAlive:2018-07-05 22:41:23.341112484 +0800 CST m=+100.664577164,ka.nextKeepAlive:2018-07-05 22:41:23.341112484 +0800 CST m=+100.664577164, ka.deadline:2018-07-05 22:42:03.341113662 +0800 CST m=+140.664578212 2018/07/05 22:41:23 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:41:23 karesp.TTL:60, nextKeepAlive:2018-07-05 22:41:43.350264635 +0800 CST m=+120.673729313,ka.nextKeepAlive:2018-07-05 22:41:43.350264635 +0800 CST m=+120.673729313, ka.deadline:2018-07-05 22:42:23.350265498 +0800 CST m=+160.673730050 2018/07/05 22:41:43 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:41:43 karesp.TTL:60, nextKeepAlive:2018-07-05 22:42:03.572888276 +0800 CST m=+140.896353032,ka.nextKeepAlive:2018-07-05 22:42:03.572888276 +0800 CST m=+140.896353032, ka.deadline:2018-07-05 22:42:43.572889271 +0800 CST m=+180.896353840 2018/07/05 22:42:03 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:42:03 karesp.TTL:60, nextKeepAlive:2018-07-05 22:42:23.86879775 +0800 CST m=+161.192262488,ka.nextKeepAlive:2018-07-05 22:42:23.86879775 +0800 CST m=+161.192262488, ka.deadline:2018-07-05 22:43:03.868798687 +0800 CST m=+201.192263243 2018/07/05 22:42:23 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:42:23 karesp.TTL:60, nextKeepAlive:2018-07-05 22:42:43.877613966 +0800 CST m=+181.201079020,ka.nextKeepAlive:2018-07-05 22:42:43.877613966 +0800 CST m=+181.201079020, ka.deadline:2018-07-05 22:43:23.877615637 +0800 CST m=+221.201080417 2018/07/05 22:42:43 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:42:43 karesp.TTL:60, nextKeepAlive:2018-07-05 22:43:03.884576518 +0800 CST m=+201.208041122,ka.nextKeepAlive:2018-07-05 22:43:03.884576518 +0800 CST m=+201.208041122, ka.deadline:2018-07-05 22:43:43.884577361 +0800 CST m=+241.208041904 2018/07/05 22:43:03 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:43:03 karesp.TTL:60, nextKeepAlive:2018-07-05 22:43:23.893274193 +0800 CST m=+221.216738996,ka.nextKeepAlive:2018-07-05 22:43:23.893274193 +0800 CST m=+221.216738996, ka.deadline:2018-07-05 22:44:03.893275148 +0800 CST m=+261.216739702 2018/07/05 22:43:23 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:43:24 karesp.TTL:60, nextKeepAlive:2018-07-05 22:43:44.058766258 +0800 CST m=+241.382230998,ka.nextKeepAlive:2018-07-05 22:43:44.058766258 +0800 CST m=+241.382230998, ka.deadline:2018-07-05 22:44:24.058767168 +0800 CST m=+281.382231711 2018/07/05 22:43:44 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:43:44 karesp.TTL:60, nextKeepAlive:2018-07-05 22:44:04.413012991 +0800 CST m=+261.736477737,ka.nextKeepAlive:2018-07-05 22:44:04.413012991 +0800 CST m=+261.736477737, ka.deadline:2018-07-05 22:44:44.41301386 +0800 CST m=+301.736478391 2018/07/05 22:44:04 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:44:04 karesp.TTL:60, nextKeepAlive:2018-07-05 22:44:24.420796492 +0800 CST m=+281.744261106,ka.nextKeepAlive:2018-07-05 22:44:24.420796492 +0800 CST m=+281.744261106, ka.deadline:2018-07-05 22:45:04.420797607 +0800 CST m=+321.744262161 2018/07/05 22:44:24 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:44:24 karesp.TTL:60, nextKeepAlive:2018-07-05 22:44:44.428145652 +0800 CST m=+301.751610589,ka.nextKeepAlive:2018-07-05 22:44:44.428145652 +0800 CST m=+301.751610589, ka.deadline:2018-07-05 22:45:24.428147239 +0800 CST m=+341.751612018 2018/07/05 22:44:44 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:44:44 karesp.TTL:60, nextKeepAlive:2018-07-05 22:45:04.436151288 +0800 CST m=+321.759615961,ka.nextKeepAlive:2018-07-05 22:45:04.436151288 +0800 CST m=+321.759615961, ka.deadline:2018-07-05 22:45:44.436152168 +0800 CST m=+361.759616723 2018/07/05 22:45:04 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:45:04 无法写入 2018/07/05 22:45:04 karesp.TTL:60, nextKeepAlive:2018-07-05 22:45:24.441563525 +0800 CST m=+341.765028110,ka.nextKeepAlive:2018-07-05 22:45:04.436151288 +0800 CST m=+321.759615961, ka.deadline:2018-07-05 22:46:04.44156426 +0800 CST m=+381.765028789 2018/07/05 22:45:04 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:45:04 无法写入 2018/07/05 22:45:04 karesp.TTL:60, nextKeepAlive:2018-07-05 22:45:24.941897369 +0800 CST m=+342.265361977,ka.nextKeepAlive:2018-07-05 22:45:04.436151288 +0800 CST m=+321.759615961, ka.deadline:2018-07-05 22:46:04.941898136 +0800 CST m=+382.265362694 2018/07/05 22:45:05 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:45:05 无法写入 再16次之后,即第17次,开始恢复每秒2次的租约请求。。。所以,也验证了此 bug。 也就是说 不管grant的租约TTL设置多久,最终都会变成0.5秒发送lease 续期一次!!!!! 也就是说 不管grant的租约TTL设置多久,最终都会变成0.5秒发送lease 续期一次!!!!! 也就是说 不管grant的租约TTL设置多久,最终都会变成0.5秒发送lease 续期一次!!!!! 之前的134版本灰度灰度后,中间CPU 降低了大约1个小时的样子,这是为什么呢? 解释 cpu 降低1小时的疑点: 我们这么计算, TTL 600,续约续约是200, ka.chs的 数组长度是1,第一个chan <- LeasesKeepAliveResponse的长度是16 ,全部写满的时间是20016/60 = 53分钟。 也非常契合 falcon 上的 cpu 信息截图 另外再从我加日志的第一次测试时间来算: 2018/07/05 22:04:05 - 2018/07/05 21:10:42 约为53分钟。也就是说,将近1小时,跟 falcon 的统计吻合的。。。 找到真凶: 定问题了,就是修复方式了,我并没看到 ch channel 的使用的地方,或许我没理解作者的用意,我觉得,不管 ch channel 是否写入,都必须将下一次nextKeepAlive的时间赋值为最新时间。将ka.nextKeepAlive = nextKeepAlive移到for select外面即可。 不过,我们产品急于推广,保险起见,牺牲了 lease 的需求,去掉了这个功能。发布到服务器后,负载情况有非常大的改善,CPU 从40%-50%降低到2%-5%左右。 回顾总结: 回顾etcd 集群遇到的所有现象,配合这个问题,所有问题都好解释了,那么,我按照事情发生发展的顺序总结一下原因: etcd clientv3的lease keepalive 类库有 bug,不管 ttl 设置多久,都会在续期16次后,变为0.5s 一次 agent 部署到大量服务器上后(2W 台),每个 agent 持有一个 lease id agent(相对于 etcd 来说,是client)连接到 etcd member 上,将lease 续期请求每秒2次发送到 member 上 etcd member 跟 leader 在续期 lease 上是使用 http 1.1,TCP 连接在上一个请求没响应之前,没法复用,几乎都是新创建的 TCP 连接 etcd leader 节点每秒收到 2W * 2 个 TCP 连接请求 早期 etcd node 之间是 https 通讯,TCP 建立后,会进行 TLS 握手,TLS 校验是 RSA 的数学计算,会消耗大量 CPU,导致 leader 节点 CPU 飙升 etcd leader 使用 grpc 类库监听的 http server,每一个 client 连接过来时,都会创建对象、结构体、channel用来通讯,占用更多内存 etcd leader 收到 etcd member转发来的 lease keepalive请求, 放到一个slice里(实际上这块是grpc做的) etcd leader 对存放recvMsg的b.backlog 写入速度大于消费速度,导致 slice 越来越大,占用更多内存,导致内存不停上升,导致内存溢出假象 etcd leader 处理请求的速度小于写入速度,导致请求堆积,超过 lease id 的 TTL,导致 lease id 判断过期,删除 TTL,也解释了 hids server 的 key 莫名其妙消失的现象 etcd member 大量新建 TCP 请求到 etcd leader,leader 处理后就关闭连接,导致 leader 侧会出现大量 TIME-WAIT 状态的链接。 etcd leader 处理 member lease 的请求太慢,超过 TLS 的10s 后,member 触发 TLS 超时,主动断开连接,但 leader 仍处理不过来,TCP 连接变为 CLOSE-WAIT 状态 最终,稻草压死骆驼。 该 bug 已反馈至 etcd 官方 clientv3 sdk will send /etcdserverpb.Lease/LeaseKeepAlive twice every second for lease id after over time 困扰我1个多月的 bug,不会忘记这个时刻 关于 etcd 过载保护的想法: etcd lease 的 keepalive 放到2/3的时间点去做是不是更合适? grpc 的过载保护是不是也要做下,比如接受 client 请求这里从数组改用定长队列,超过丢弃,来保证集群可用。 etcd 节点之间通讯改用 http2是不是更合适 声明 本文可转载,但不得删除文中 招聘信息,以及QQ 群信息。 原文来自:www.cnxct.com/etcd-lease-keepalive-debug-note/ 招聘[上海/北京]安全研发专家 月薪 40k-60k 上海市/ 5-10 年/本科/研发 招聘[上海/北京]美团点评--50-80K 招 Java 高级技术专家 简历请发至 cfc4n[{at}]cnxct.com ,谢谢。 对你的etcd 产品是否影响: 使用etcd clientv3(golang 的客户端类库) ,所有v3版本的客户端(包括3.0.x,3.1.x,3.2.x,3.3.x,3.4.x)。 使用 lease 租约,并设置 keepalive client 量比较大能看出来效果,几百台的话,对性能影响不大。 官方已修复 coreos etcd在2018年7月24日已经修复该 bug,涉及etcd client v3的3个版本效率非常高。详情如下 https://github.com/coreos/etcd/blob/master/CHANGELOG-3.1.md https://github.com/coreos/etcd/blob/master/CHANGELOG-3.2.md https://github.com/coreos/etcd/blob/master/CHANGELOG-3.3.md Fix lease keepalive interval updates when response queue is full. If <-chan *clientv3LeaseKeepAliveResponse from clientv3.Lease.KeepAlive was never consumed or channel is full, client was sending keepalive request every 500ms instead of expected rate of every "TTL / 3" duration. 2018-07-18 +4 8 0