作者:纳海、孤弋
前言
从 Kubernetes 诞生以来,以 DevOps、容器化、可观测、微服务、Serverless 等技术为代表的云原生,催生了应用架构新一轮的升级。有意思的是,与以往的技术迭代更新不同,原本是一个技术圈常规的一次技术实践,在千行百业数字化转型大背景,叠加持续疫情冲击的双重影响之下,加上部分传统行业科技自主政策的催化;这一次的技术迭代几乎变成了 IT 从业人员全民参与的一次盛宴。但是陡峭的学习曲线、复杂的技术体系、瞬态的基础资源形态,让企业的信息体系建设在研发、构建、交付、运维等多方面都带来了不少的挑战。这些挑战也加深了日益更新的技术栈与习惯于聚焦在一线业务开发的开发者之间矛盾,这个矛盾直接催生了最近的平台工程理念的诞生。这个理念抓住这个冲突点,提出了“内部研发自助平台”的构想:“企业应该以平台化建设的方式,提供一系列的自助型工具,协助开发者在各个环节中解决遇到的各种技术问题”。这个说法一下戳中众多开发者的痒点,这也是这一概念突然之间大火的原因之一。理念背后又引申出来了一个更为直接的问题:这个工具里面应该有点啥?
揭开问题的面纱
早在 2018 年,EDAS 产研团队拜访一家具有百人研发团队的客户,当时客户正在进行微服务拆分和迁移上云,他们遇到了一些新问题:
- 本地因为依赖问题,没法起动完整的环境,导致开发困难。
- 云上环境调用关系复杂,无法做调试。
客户期望将特定的实例启动到本地,云端能调本地,本地调云端,实现微服务端云联调。对于这些问题我们没有任何准备,于是回来后赶紧开始调研分析,于是慢慢揭开了藏在水面下的冰山。
客户的诉求很简单,就是想把微服务应用起在本地,应用能跟云端微服务互相调用,如下所示:
迁移上云后,客户的网关、应用、消息队列、缓存、数据库等组件模块都部署在云端网络内,本地需要经过堡垒机才能进行访问。在这种情况下,本地节点是不可能正常启动的(因为连不通数据库等关键组件),也是不可能跟云端服务互相调用的。采取了云原生架构之后,客户却再也回不去原来简单高效的本地开发方式了。
这个问题只有这个客户遇到吗?不是的,这几年我们跟很多客户聊下来他们都有这个问题。但其实也不是一点解决办法都没有,最直接的办法是:通过架设私有网络的方式,连通本地办公网跟云端网络,实现网络互通。但实际上这个办法有三大缺陷,导致并不是很多客户采用。
- 成本高昂:搭建专线网络的投入相当大,相比于收益来说并不划算。
- 安全性低:打通本地到云上网络,对本地办公网和云上生产网都带来了不稳定因素,本质上扩大了安全域,也扩大了攻击面。
- 运维复杂:网络运维是相当复杂的,在高度可伸缩的云原生架构下打平本地和云端网络,这个是很多网络工程师的噩梦。本地和云端两张网络必须做好规划,两边网段不可冲突,同时双向网络路由和安全策略都需要人工管理,复杂费力且容易出现问题。
对于这些问题,有的企业采取折中的办法,在云端找一台机器作为 VPN 服务器,搭建本地到云端的 VPN 链路。这个方案同样需要维护网络路由以实现网络互通,另外 OpenVPN 虽便宜但不稳定,专用 VPN 性能高但费用昂贵,鱼与熊掌不可兼得。
意识到这些问题之后,我们便开始了“路漫漫其修远兮”的探索之路。
端云互联,问题的初解答
在一开始我们就确定两个目标:一是双向打通本地和云端链路,二是不需要对网络架构进行伤筋动骨的改造。
在历经三个月的闭关研发之后,我们在 2018 年底研发出来了这个工具。它支持双向联通,而且是插件化开箱即用,支持 Windows 和 MacOS 系统。我们把它命名为端云互联,其整体组成如下所示:
端云互联插件会在启动微服务的时候拉起一个sidecar进程--通道服务,通道服务负责接收本地微服务的流量,并通过堡垒机转发至云端目标微服务。下面进一步说明其中三个核心要点。
本地微服务调用转发到 sidecar
我们使用了 Java 原生的流量代理技术,通过注入启动参数,可以使得本地微服务流量以 socks 协议转发至通道服务 sidecar 上。关于具体参数细节,可阅读 Java Networking and Proxies 来了解细节。
sidecar 将调用转发到云端微服务
其实 SSH 本身就可以用来进行数据转发,充当正向代理和反向代理。SSH 协议从下到上分为传输层、认证层和连接层三层协议:
- 传输层协议(Transport Layer Protocol):这层协议负责建立起安全的连接通道,是整个 SSH 的安全性基石。
- 用户认证协议(User Authentication Protocol):这层协议负责完成远程身份认证。
- 连接协议(Connection Protocol):这层协议实现 SSH 通道的多路复用和信息交互,可以通过它来实现远程命令执行和数据转发等多种功能。
我们基于 SSH 的连接协议来使得通道服务 sidecar 将调用转发到云端微服务,虽然SSH底层原理有点复杂,但上层使用是挺简单的,主流的编程语言也基本都有现成的库来使用。
云端微服务调用转发到堡垒机
这里我们利用了微服务的机制,将堡垒机的 IP 和特定端口作为本地微服务的地址信息注册到注册中心。这样云端微服务调用时就会通过注册中心发现堡垒机,并发起服务请求,再结合 SSH 的数据转发就能回到本地微服务。
众里寻他千百度
端云互联工具上线后,受到了很多客户的欢迎,但客户使用过程中遇到了新的问题:
- NIO 流量代理问题:Java Networking and Proxies 里的流量代理参数只对 BIO 的流量生效,NIO 框架并不支持。这个问题影响面非常大,因为微服务应用基本都会直接或间接地使用 Java NIO 框架。具体简单的例子,Netty 本身就是基于 Java Nio 的,而很多流行的中间件框架都使用 Netty 作为传输框架。
- 域名解析问题:对于域名解析,本地微服务应用在访问之前会发起域名解析,而这些域名解析同样是不支持 Java 流量代理的。也就是说,如果这个域名只能在云端完成解析,那么整个调用就会失败。例如,K8s 里的 service 域名只能在集群节点上完成 DNS 解析,本地是无法解析的,从而导致本地无法调用 K8s service。
对于这些问题,业界通常的做法是采取容器来解决(例如 Telepresence),通过将应用跑在容器内,再结合 iptables 来拦截并转发整个容器内的流量。这种方式是可行的,我们也支持了这种方式,整体架构如下所示:
这个链路跟之前是差不多的,除了本地引入了容器技术之外。在上图中,我们通过 docker network connect,可以使得应用容器和 sidecard 容器共享网络栈,这样通道服务便可以通过 iptables 去拦截本地微服务进程的流量(包括 NIO 和 DNS 流量)并进行转发。
方案很美好,但现实很骨感,很多客户用不起来。究其原因,那就是:本地开发需要引入容器这个重量级的依赖。这里有两个问题,一个是“重”,另一个是“依赖”。“重”,是因为本地开发机器的算力是非常小的。Chrome、IDE 和通信软件等应用往往已经占据了大部分的本地机器资源,在这背景下本地再启动容器往往会导致死机。另外,Windows 和 MacOS 等主流操作系统也并不自带 Docker 等容器软件,开发者需要自己在本地自行安装,由于网络带宽等原因整个安装过程也会遇到许多问题。
那除了使用容器,还有别的办法来解决应用的流量代理问题吗?还是有的,不过局限性也非常大。例如 torsocks 这个工具就可以实现进程级别的 TCP 和 DNS 流量拦截并转发,但问题是它并不支持 Windows 系统。MacOS 也存在问题。由于 torsocks 是基于 LD_PRELOAD/DYLD_INSERT_LIBRARIES 机制来改写系统调用来进行流量拦截的,而 MacOS 系统本身有系统完整性保护,会阻止特定系统调用被改写,因此并不是所有的流量都能被拦截到。
难道就没有更好的方案了吗?
那人却在,灯火阑珊处
回顾一下我们所面临的问题:Java 原生流量代理不支持 NIO 和 DNS 流量转发。这里有一个非常重要的信息--Java。业界或者开源社区的流量拦截方案普遍追求通用性,从而引入了容器依赖,顾此失彼。
既然追求通用性有诸多问题,那么聚焦到 Java 语言是否有更优的解法?答案是肯定的。Java 可以通过 Agent 字节码技术,动态修改应用运行时的行为,而上层代码无需任何变动。比如像 Pinpoint、SkyWalking 等链路跟踪工具,它们都是通过注入一个字节码Agent来实现无侵入的链路埋点的。再比如诊断领域流行的 Arthas 工具,它也是基于字节码技术来实现 Java 进程的调用跟踪和 Profiling。
于是,我们开始探索基于字节码技术的解决方案。整个探索过程是艰难且有趣的。在微服务框架层面,我们需要适配 SpringCloud、Dubbo、HSF 甚至是 gRPC 等主流框架;在组件层面,我们需要支持微服务、数据库、消息队列、任务调度、缓存等等组件;在 JDK 版本上,我们需要兼容从 JDK 1.7 到 JDK18 之间的版本...在这过程中,我们不断进行迭代改进,也不断收到客户的正面反馈,让工具日趋完美。
在经过 1 年时间的打磨之后,我们终于自研出基于字节码的 Java 流量代理技术,架构如下所示:
这个方案只需要引入一个代理字节码 Agent,并没有引入外部依赖。这个代理 Agent 相比于容器来说轻量得多,而且会在启动阶段被端云互联插件自动拉取并注入,上层使用是无感知的。至此,我们终于很好地解决了本地微服务应用的流量代理问题。
独上高楼,望尽天涯路
在这几年里,我们一直在低头赶路,不断地发现问题并解决问题。与此同时,云原生社区也在逐步演进。同样在 2018 年,kubernetes 社区发表了一篇名为 Developing on Kubernetes 的文章,上面对不同的开发模式有一个非常好的总结:
remote 表示云端,local 表示本地。cluster 为 K8s 集群,dev 为开发环境。针对 dev 和 cluster 不同的位置,整体可分为四种开发模式:
- pure off-line:这种模式表示你的 K8s 集群和开发环境都在本地。K3s 、 Minikube和 EDAS Core(这里先卖个关子,下文再进行介绍)都属于这种模式,你可以在本地直接启动一个轻量级的开发集群。
- proxied:这种模式表示 K8s 集群运行在云端,开发环境在本地,云端集群和本地开发环境通过代理进行互联。这个模式的典型代表为社区的 Telepresence 和 EDAS 端云互联。在多语言通用性上 Telepresence 略胜一筹,而在 Java 上 EDAS 端云互联更加易用。
- live:这种模式表示 K8s 集群运行在云端,开发环境在本地,本地代码通过 CICD 等方式来更新云端的应用。这个模式是最常见的模式,也是普遍效率最低的模式。如果通过 CICD 部署,意味着你每次代码修改都需要经过漫长的构建和部署才能生效到集群里面。如果在开发过程中需要不断修改代码来调试,这个迭代部署过程是非常耗时的。
- remote:这种模式表示 K8s 集群和开发环境都在云端。Cloud IDE 是典型的例子,代码和运行环境都在云端,本地通过浏览器或者轻量级的端应用来编辑代码。实际上来看,这种方式仍未得到广大开发者认可,本地 IDE 的体验优于 Cloud IDE 体验,本地开发仍然是主流。
在 proxied 模式上,我们已经把端云互联打磨的相当不错了,但它不能解掉所有问题。这样的场景并不罕见:本地开发调试都好好的,但一部署上去就是有问题。这种问题的根源是,本地运行的环境和云端集群里运行环境是不一致的,这个不一致会产生种种问题。例如,本地能正常运行一个需要 2c4g 的 Java 进程,不代表云上集群也能正常分配一个 2c4g 的 Pod,因为当前集群内可能是没有多余资源的。
这样的问题有很多,不可一一枚举。这也促使我们进一步思考应该如何解决这些问题。在经过半年的调研、探索和研发,我们研发出云原生工具箱(Cloud Native Development Kit,简称 CNKit),由它来解决这些问题,并提供云原生架构下的开发、调试和诊断能力。
云原生工具箱(Cloud Native Development Kit)
我们解决问题的思路是,要解决环境不一致的问题,只能回到环境中去。一个应用在云原生环境下启动虽然看上去很简单,但实际上是要经历很多个步骤的。应用需要经过从 K8s 调度,到 Pod 初始化,再到服务拉起,最终才能完成应用运行。在这个过程中,你可能会遇到以下问题:
对于这些问题,我们进行归纳总结,沉淀出一套解决方案:通过 CNKit 来快速复制 Pod,然后进行迭代开发、部署、调试和诊断。整体功能如下所示:
通过与 EDAS 全流量流控集成,CNKit 可使得只有符合特定规则的调试流量进入复制的 Pod,而不影响其他正常流量。对于复制的 Pod,你可使用 CNKit 开箱即用的部署、调试和诊断能力,并且可以使用基于审计的命令终端。下面来具体说明复制、部署、调试和诊断能力。
复制
这个复制的 Pod 相当于我们自己的一个临时工作空间,我们可以不断通过浏览器或者 IDE 来部署自己的应用包,并进行调试和诊断。复制 Pod 当前支持如下配置:
具体作用为:
- 启动命令:即 Pod 的启动命令。默认下使用原镜像的启动命令,而如果需要进行迭代部署或者开启调试的话,需要自定义启动命令。这是因为在生产环境中,原镜像启动命令往往会使用应用进程来作为 1 号进程,一旦应用退出或重启,这个 Pod 就会随之释放。因此,需要设置一个特殊的启动命令来防止 Pod 随应用退出而被释放。
- 复制模式:支持基于 Pod 复制或者从 Deployment 的 spec 进行创建。
- 目标节点:即 Pod 运行在那个集群节点上。默认通过 K8s 调度来运行 Pod,但你也可以直接指定特定集群节点来运行此 Pod。
- Pod 日志:配置将 Pod 日志打到标准输出或者重定向到文件。
- 流量控制:通过全链路流控,可使得只有符合特定规则的请求进入该 Pod 节点。
- 诊断选项:支持应用启动时立即运行 tcpdump 来进行监测、一键打开 JVM 异常记录和去除 Liveness 探针。
这些选项都是基于 EDAS 长年累月的客户支持所总结出来的经验,可能看上去并不酷炫,但却是非常实用的。拿“去除 Liveness 探针”这个配置项来说明。如果应用启动阶段就出现异常,这种情况下 Liveness 探针是会失败的,K8s 会直接 Kill 掉这个 Pod 容器并重新拉起。我们会陷入 Liveness 失败,Pod 容器被杀死,然后容器重启导致现场丢失,Liveness 又失败的无限循环当中。去除 Liveness 探针之后,你就可以进去 Pod 容器中进行问题排查了。
这里还有一个非常有意思的配置项--全链路流控。全链路流控是 EDAS 上微服务治理的杀手锏,可以使得整体微服务链路的流量指哪打哪。对于复制的 Pod,我们可能会希望只有自己的请求才进入这个 Pod,而不影响其他人的调用请求。这种情况只需要在界面上勾选加入特定流控分组即可,使用上非常简单。
部署
还记得最初的问题吗?在云原生架构下,我们每次部署都需要经过 CICD、EDAS 部署和 K8s 调度来拉起应用,这个过程是漫长而痛苦的。而且,我们在排查问题时往往需要来临时安装特定工具,每次拉起新的应用 Pod 意味着需要重新安装所需工具。
对于这个问题,我们推荐采取“一次复制,多次使用”策略。在上面提到,我们可以通过复制 Pod 来创建出属于自己的“临时工作区”(实质也是一个 Pod),然后可以通过浏览器或者 IDE 来直接把应用包部署到临时工作区,并且进行调试诊断。基于 CICD 和 CNKit 的开发流程是截然不同的,如下所示:
CICD 部署路径适合生产环境,通过标准流程保证了线上业务的稳定性。但同时它又是流程冗长的,在开发阶段优势不明显,反而会降低开发效率。这种情况下 CNKit 部署流程是很好的互补,你只需要复制 Pod,然后便可以通过浏览器或者 IDE 来不断更新应用包调试代码。
调试
调试(这里特指 remote debug)是应用开发过程中相当重要的一环,如果无法调试,那么应用开发效率将会大大降低。在云原生架构下,调试应用并不是那么简单,但总是可以完成的。在这一方面,CNKit 除了简化调试流程外,还提供了流量控制能力:
- 简化流程:你只需要在页面上点击“开启调试”,CNKit 便会重启 Pod 里的应用来开启调试端口。然后本地通过 IDE 来一键连接到 CNKit,接着就可以开始断点调试了。
- 流量控制:通过集成 EDAS 全链路流控,CNKit 可使得只有特定请求能进入复制 Pod,触发断点调试逻辑。
通过这两点,你可以非常方便地完成代码调试。下图是一个简单的说明样例,假如你在开发一个商品中心,上游为交易中心,下游为库存中心,使用 CNKit 进行调试的整体链路如下所示:
标记为开发版本的商品中心即为复制出来的 Pod 节点,云端环境中通过全链路流控来将特定流量转发到该 Pod 中,开发者本地则通过 CNKit proxy 来连接到该 Pod 的调试端口。
实际上,在一个多人并行开发的服务中,每个人都可以拥有属于自己的开发版本节点,只需要设定不同的流量控制规则即可,这样可并行开发可互不干扰。
诊断
我们将问题诊断为 K8s 调度、应用启动、应用运行和应用下线四个阶段。在不同阶段,采取的诊断手段并不相同:
在 K8s 调度过程中,我们主要关注其产生的相关事件,发生调度异常时 K8s 会给出相关原因。下图为正常调度时的 K8s 事件:
当出现调度异常(例如资源不足导致调度失败)的问题时,K8s 会产生相应事件:
而在应用启动阶段,除了 K8s 事件,我们还可以观察 Pod 日志,这部分日志是应用产生的,里面包含更详尽的信息。下图为 Pod 的标准输出日志样例:
另外,应用启动阶段会产生较多网络访问,应用启动失败很多情况下都是由于网络请求异常引起的,因此 CNKit 支持在启动前自动运行 Tcpdump 来记录网络请求。下图为应用启动时自动抓取的 Tcpdump 包,CNKit 支持文本和 pcap 两种格式,下图为文本格式的 Tcpdump 数据:
最后,在应用运行和下线阶段,你仍然可以使用 K8s 事件、Pod 日志和 Tcpdump,另外还可以一键使用 CNKit 集成的 Arthas 工具。通过在页面上一键运行 Arthas,CNKit 会自动完成 Arthas 安装并运行,整体交互如下所示:
至此,CNKit 的复制、部署、调试和诊断都一一分享完毕。但除了这些能力,CNKit 还有一些隐藏彩蛋,例如审计 Webshell 等等,这些地方留给读者来慢慢探索,此处不再赘述。
EDAS Core
除了端云互联和 CNKit,我们还开放了 EDAS Core。如果按照上面 Developing on Kubernetes 划分的标准来看,端云互联属于 proxied 模式,CNKit 则为 live 模式,而 EDAS Core 则为 pure off-line 模式。
EDAS 本身是收费的商业化产品,它是一个应用托管和微服务管理的云原生 PaaS 平台,提供应用开发、部署、监控、运维等全栈式解决方案,同时支持 Spring Cloud 和 Apache Dubbo 等微服务运行环境。而 EDAS Core 则为免费的轻量级 EDAS 内核版本,同样支持上述能力,但剥离了商业化特性,不提供服务 SLA 和实时的运维支持,适合在开发阶段使用。
EDAS Core 最低只需要 4 核 8g 的机器资源,我们完全可以在本地笔记本上来运行一个离线的 EDAS 平台,并进行微服务开发。EDAS Core 整体架构如下所示:
这里进行简单说明:
- EDAS Core:包含了 EDAS 应用托管能力,支持 Nacos 服务注册发现和 Minio 持久化存储,可运行于 Kind、K3s、Docker-Desktop 和 K8s 集群之上,只需 4c8g 的资源占用。
- 开发者工具:支持使用 Jenkins 插件进行持续部署、Terraform 进行基础设施维护、ACT 进行本地开发,并兼容 EDAS 开放 API 和 SDK。
- 安装介质:支持通过 Helm、OSS 和 ADP 进行 EDAS Core 安装。
- K8s Cluster:此 K8s 集群即为 EDAS Core 托管的集群,上面运行微服务应用(并自动注入服务治 OneAgent)。
- 服务集成:在横向服务集成上,EDAS Core 支持和 EDAS 商业化应用进行一键转换,并集成了 ARMS 和 SkyWalking 等链路跟踪产品,同时支持使用 ACR 进行镜像托管。
下面为 EDAS Core 的运行界面(EDAS 老用户应该对这个界面比较熟悉了):
当前 EDAS Core 处于内部邀测状态,如果希望使用此能力,欢迎在阿里云上向 EDAS 产品发起工单咨询:)。
结语
云原生架构和微服务开发这两个都是非常流行的技术领域,但“云原生架构下的微服务开发”这个命题却甚少见国内厂商提及。EDAS 作为微服务托管领域的先行者很早就开始了云原生架构的支持,并一直在关注新架构下的微服务开发问题。
从最早的端云互联模式开始,到最近推出的云原生工具箱(CNKit)和 EDAS Core,EDAS 一直站在开发者角度来思考云原生技术演进所面临的新问题,并不断提供解决这些问题的工具和产品。最后,对于这几个工具产品进行简单的总结来结束本文:
除了以上这些,如果你遇到有意思的场景和工具,请钉钉扫码(群号:21958624)和我们联系吧!
参考资料
- torsocks:
https://github.com/dgoulet/torsocks
- docker network connect:
https://docs.docker.com/engine/reference/commandline/network_connect
- The Secure Shell (SSH) Connection Protocol:
https://www.rfc-editor.org/rfc/rfc4254
- Java Networking and Proxies:
https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html
- Developing on Kubernetes:
https://kubernetes.io/blog/2018/05/01/developing-on-kubernetes/